페이지

2022년 12월 11일 일요일

Iltimate AWS Certified(SAA-C03) IAM Policies

 IAM Policies Structure

- Consists of 

* Version: policy language version, always include "2012-10-17"

* Id: an identifier for the policy(optional)

* Statement: one or moere individual statements(required)


- Statements consists of

* Sid: an identifier for the statement(optional)

* Effect: whether the statement allows or denies a access(Allow, Deny)

* Principal:account|user|role to which this policy applied to

* Action: list of actios this policy allows or denies

* Resource: list of resources to which the actions applied to

* Condition: conditions for when this policy is in effect(optional)


{

    "Version" : "2012-10-17",

    "Id" : "S3-Account-Permissions",

    "Statement" : [

        {

            "Sid" : "1",

            "Effect" : "Allow",

            "Principal" : {

                "AWS" : [ "arn:aws:iam::123456789012:root"]

            },

            "Action" : [

                "s3:GetObject",

                "s3:PutObject"

            ],

            "Resource" : ["arn:aws:s3::myubucket|*"]

        }

    ]

}





skk

ss



2022년 12월 10일 토요일

Iltimate AWS Certified(SAA-C03) IAM:Permissions

- Users or Groups can be assigned JSON documents called policies

- These policies define the permisssions of the users

- In AWS you apply the least privilege principle: don't give more permissions than a user needs


{

    "Version": "2012-10-17",

    "Statement" : [

        {

            "Effect" : "Allow",

            "Action" : "ec2:Describe*",

            "Resource" : "*"

        },

        {

            "Effect" : "Allow",

            "Action" : "elasticloadbalancing:describe*",

            "Resource" : "*"

        },

        {

            "Effect" : "Allow",

            "Action" : " [

                "cloudwatch:ListMetrics",

                "cloudwatch:GetMetricStatistics",

                "cloudwatch:Describe*"

            ],

            "Resource" : "*"

        }

    ]

}







Iltimate AWS Certified(SAA-C03) IAM:Users & Groups

- IAM = Identity and Access Management, Global service

- Root account created b y default, shouldn't be used or shared

- Users are people within your organization, and can be grouped

- Groups only  contain users, not other groups

- Users don't have to belong to a group, and user can belong to multiple groups


2022년 11월 12일 토요일

Software Architecture

 [AN ARCHITECTURAL DECISION IS]

THOSE DECISIONS WHICH AFFECT THE STRUCTURE, NON-FUNCTIONAL CHARACTERISTICS, DEPENDENCIES, INTERFACES, OR CONSTRUCTION THECHNOLOGY

- Michael Nygard, Documenting Architecture Decisions (2011)

YOU THINK THAT BECAUSE YOU UNDERSTAND "ONE" THAT YOU MUST THERERFORE UNDERSTAND "TWO" BECAUSE ONE AND ONE MAKE TWO. BUT YOU FORGET THAT YOU MUST ALSO UNDERSTAND "AND".

- Donella Meadows, Thinking in Systems



2022년 10월 9일 일요일

Kubernetes - few key terms

 CNI and CSI

- The containser networking and storage interfaces, respectively, that allow for pluggable networking and storage for Pods(containers) that run in Kubernets.


Container

- A Docker or OCI image that typically runs an application.


Control plane 

- The brains of a Kubernetes cluster, where scheduling of containers and managing all Kubernets objects takes place (sometimes referred to as Masters)

DaemonSet

- Like a deployment, but it runs on every node of a cluster.


Deployment

- A collection of Pods that is managed by Kubernetes.


kubectl

- The command-line tool for talking to the Kubernetes control plane.


kubelet

- The Kubernetes agent that runs on your cluster nodes. It does what the control plane needs it to do.


Node 

- A machine that runs a kubelet process.


OCI

- The common image format for building executable, self-containerd applications. Also referred to as Docker images.


POD

- The Kubernetes object that encapsulates a running container.


2022년 10월 4일 화요일

30.1 확인 1: Variable 인스턴스 변수

 첫 번째로 Variable 클래스의 인스턴스 변수에 관해 복습하겠습니다. 우선 Variable 클래스를 초기화하는 __init__ 메서드를 보죠.


이와 같이 Variable 클래스에는 인스턴스 변수가 여러 개입니다. 그중 data와 grad에 주목해보죠. data와 grad는 각각 순전파 계산과 역전파 계산 시 사용됩니다. 주의할 것은 data와 grad 모두 ndarray 인스턴스를 저장한다는 사실입니다. 이 점을 부각하기 위해 [그림 30-1]처럼 그리겠습니다.


[그림 30-1]과 같이 data 와 grad는 '입방체의 상자'로 그리겠습니다. 그리고 data와  grad가 ndarray 인스턴스를 참조한느 경우에는 [그림 30-2]처럼 그립니다.

STEP 30 고차 미분(준비편)

 현재의 DeZero는 미분을 자동으로 계산할 수 있지만 1차 미분 한정입니다. 그래서 이번 단계에서는 2차 미분도 자동으로 계산할 수 있도록, 나아가 3차 미분, 4차 미분,... 형태의 모든 고차 미분까지 자동으로 계산할 수 있도록 DeZero를 확장할 것입니다.

그러려면 DeZero를 사용하여 2차 미분을 계산하려는 현재의 역전파 구현을 근본적으로 재검토해야 합니다. DeZero의 역전파는 Variable과 Function 클래스에 기초해 동작합니다. 그래서 우선 Variable과 Function의 현재 구현부터 간단히 되돌아 보려 합니다. 앞으로의 이야기는 조금 길어지므로 3개 절로 나눠 하나씩 확인하겠습니다.


2022년 10월 3일 월요일

29.2 뉴턴 방법을 활용한 최적화 구현

 그럼 뉴턴 방법을 구현해봅시다. 뉴턴 방법을 활용한 최적화를 구현하려면 [식 29.5]를 구현하기만 하면 됩니다. 하지만 DeZero는 아쉽게도 2차 미분은 자동으로 구하지 못하므로 다음과 같이 수동으로 2차 미분을 구하기로 합시다.

y = x**4 - 2x**2

dy/dx = 4x**3 - 4x

d2y/dx2 = 12x**2 - 4


이 결과를 사용하면 뉴턴 방법을 활용한 최적화를 다음처럼 구현할 수 있습니다.



이와 같이 1차 미분은 지금까지처럼 역전파로 구하고 2차 미분은 수동으로 코딩해 구합니다. 그런 다음 뉴턴 방법의 갱신 수식에 따라 x를 갱신합니다. 이 코드를 실행하면 x값의 갱신 과정이 다음과 같이 출력됩니다.



이 문제의 답(최솟값)은 1입니다. 앞의 결과를 보면 목적지까지 빠르게 도달했음을 알 수 있ㅅ브니다. 단 7회의 갱신만으로 최솟값에 도달했죠. 한편 경사하강법은 최적값에 도달하는데 오랜 시간이 걸립니다. [그림 29-5]는 두 기법의 갱신 경로를 비교한 모습입니다.


이처럼 경사하강법으로는 갱신을 여러번 해야 합니다. 참고로 [그림 29-5]에서 경사하강법의 모습은 학습률을 0.01로 했을 때의 결과입니다. 이때 x = 1.0 과의 절대오차가 0.001 이하로 좁혀지기까지는 124번이나 갱신해야 했습니다. 그에 반해 뉴턴 방법은 불과 7번 입니다.

이상이 뉴턴 방법의 이론과 구현입니다. 이번 단계에서는 뉴턴 방법을 활용한 최적화를 구현하고 구체적인 문제를 풀어봤습니다. 그리고 실제로 좋은 결과를 얻어냈습니다. 그러나 구현시 2차 미분을 수동으로 계산했다는 한계가 있군요(2차 미ㅜㄴ을 계산하기 위해 수식을 손으로 써내려갔고, 그 결과를 하드코딩했습니ㅏㄷ). 이쯤이면 자연스럽게 집작이 되시겠죠? 다로 다음으로 정복할 목표ㅕ는 자동화하는 것입니다.




2022년 10월 2일 일요일

29.1 뉴턴 방법을 활용한 최적화 이론

 이번 단계의 목표는 뉴턴 방법을 활요한 최적화를 구현하는 것입니다. 경사하강법 대신 뉴턴 방법을 사용하여 실제로 더 빨리 수렴하는지 확인할 것입니다. 참고로 설명을 단순화하기 위해 변수를 하나만 받는 함수를 예로 들겠습니다(로젠브록 함수는 두 개의 변수를 입력받는 함수였습니다).

그렇다면 뉴턴 방법은 최적의 값을 어떤 원리로 찾아내는 것일까요? y = f(x)라는 함수의 최솟값을 구하는 문제를 생각해보죠, 뉴턴 방법으로 최적화하려면 데일러 급수에 따라 y = f(x) 를 다음과 같이 변환합니다.

f(x) = f(a) + f'(a)(x-a) + 1/2!f''(a)(x-a)**2 + 1/3!f'''(a)(x-a)**3 + ...

테일러 급수에 따라 어떤 점 aㄹ르 기점으로 f를 x의 다항식으로 나타낼 수 있습니다(테일러 급수에 대해서는 27단계에서 설명했습니다). 이때 1차 미분, 2차 미분, 3차 미분....형태로 항이 증가하는데, 증가하는 걸 어느 시점에 중단하면 f(x)를 근사적으로 나타낼 수 있습니다. 여기에서는 다음과 같이 2차 미분에서 중단하겠습니다.

f(x) = f(a) + f'(a)(x-a) + 1/2f''(a)(x-a)**2

[식 29.2]와 같이 y = f(x) 함수를 2차 미분항까지 사용하여 근사했습니다. 변수 x를 기준으로, 이 식 x의 2차 함수입을 알 수 있습니다. 즉, y = f(x)는 '어떤 함수'를 x의 2차 함수로 근사한 것입니다(그래서 2차 근사라고 합니다). 참고로 이 작업을 [그림 29-2]로 표현할 수 있습니다.



[그림 29-2]와 같이 근사한 2차 함수는 a 에서  y = f(x)에 접하는 곡선입니다. 다행이 2차 함수의 최솟값은 해석적으로 구할 수 있습니다. 2차 함수의 미분 결과가 0인 위치를 확인하면 되죠. 수식으로는 다음과 같습니다.

d(f(fa) + f'(a)(x-a) + 1/2f''(a)(x-a)**2)/dx =0

f'(a) + f''(a)(x-a) = 0

x = a - f'(a)/f''(a)


이 결과로부터 근사한 2차 함수의 최솟값은 x = a - f'(a)/f''(a); 위치에 있을을 알 수있습니다. 즉, [그림 29-3]처럼 a의 위치를 -f'(a)/f''(a)만큼 갱신하면 됩니다.

[그림 29-3] 처럼 a의 위치를 갱신합니다. 그리고 갱신된 a의 위치에서 같은 작업을 반복합니다. 이것이 뉴턴 방법에 의한 최적화입니다. 이 뉴턴 방법을 경사하강법과 비교해보면 특성이 명확히 드러납니다. 다음 식을 살펴보시죠.

x <- x - af'(x)

x <- x - f'(x)/f''(x)

[식 29.4]가 경사하강법이고 [식 29.5]가 뉴턴 방법입니다. 보시는 것처럼 두 방법 모두 x를 갱신하지만 방법이 다릅니다. 경사하강법에서는 a라는 계수를 사람이 수동으로 설정하고 a의 값만큼 기울기(1차 미분)방향으로 진행하여 x의 값을 갱신합니다. 이에 반해 뉴턴 방법은 2차 미분을 이용하여 경사하가업에서 말하는 a를 자동으로 조정합니다. 즉, 뉴턴 방법은 a = 1/f''(a)로 대치한 방법이라고 생각할 수 있습니다.

지금까지 함수의 입력이 '스칼라'일때의 뉴턴     방법을 설명했는데, 입력이 '벡터'인 경우로도 자연스럽게 확장할 수 있습니다. 다른 점은 벡터인 경우 1차 미분으로 기울기를 사용하고, 2차 미분으로 헤세행렬(Hesian matrix)을 사용하는 점입니다. 자세한 내용은 '칼럼:뉴턴 방법과 double backprop 보충 학습'에서 설명합니다.


지금까지의 이야기를 정리하면 경사하강법은 1차 미분만의 정보를 사용하는 반면 뉴턴 방법을 활용한 최적화는 2차 미분의 정보도 이용합니다. 물리 세계에서 예ㅒ를 들면 속도 정보만 사용하는 것이 경사하강법이고, 속도와 가속도 정보까지 사용하는 것이 뉴턴 방법입니다. 뉴턴 방법은 추가된 2차 미분 정보 덕에 효율적인 탐색을 기대할 수 있으며, 결과저4ㄱ으로 목적지에 더 빨리 도달할 확률이 커집니다.

자, 그렇다면 뉴턴 방법을 이용하여 구체적인 문제를 풀업볼 차례입니다. y = x**4 - 2x**2이라는 수식의 최적화를 해볼까 합니다. 이 함수의 모양은 [그림 29-4]와 같이 '오목'한 부분이 두곳이며, 최솟값 x가 각각 -1과 1인 위치입니다. 초깃값을 x =2로 설정한 후 최솟값 중 하나인  x =1에 도달할 수 있는지 검증해볼 것입니다.








STEP 29 뉴턴 방법으로 푸는 최적화(수동 계산)

 이전 단계에서는 로젠브록 함수의 최솟값을 경사하강법으로 구해봤는데, 기울기를 구하는 작업을 5만번 가까이 반복하고 나서야 겨우 목적지에 도달했습니다. 그 예에서 알 수 있듯이 경사하강법은 일반적으로 수렴이 느리다는 단점이 있습니다.

경사하강법을 대체할 수 있는, 수렴이 더 빠른 방법은 여러 가지가 있습니다. 그 중에서 유명한 것이 뉴턴 방법(Newton's method)입니다. 뉴턴 방법으로 최적화하면 더 적은 단계로 최적의 결과를 얻을 가능성이 높아집니다. 예를 들어 이전 단계에서 푼 문제를 뉴턴 방법으로 풀면[그림 29 -1] 의 오른쪽과 같은 결과를 얻을 수 있습니다.


[그림 29-1]을 보면 경사하강법이 '계곡'에서 고전하면서 서서히 목표값에 접근해가는 반면 뉴턴 방법은 계곡을 뛰어넘어 단번에 목적지에 도착합니다. 갱신 횟수는 불과 6회입니다. 경사하강법에서 5만번 가까이 필요했던 갱시이 불과 6회만에 끝나다니, 그적인 차이가 아닐 수 없습니다.

로젠브록 함수에서는 경사하강법과 뉴턴 방법의 갱신 횟수 차이가 아주 크게 나왔습니ㅏㄷ. 물론 이 횟수는 초깃값이나 학습률 등을 어떻게 설정하느냐에 크게 좌우되며, 이렇게 까지 큰차이를 볼 수 없는 경우도 많습니다. 일반적으로 초깃값이 정답에 가까우면 뉴턴 방법이 더 빨리 수령합니다.


28.3 경사하강법 구현

 복잡한 형상의 함수라면 기울기가 가리키는 방향에 반드시 최댓값이 존재한다고는 볼 수 없습니다(마찬가지로 반대 방향에 최솟값이 존재한다고 볼 수도 없습니다). 그러나 국소적으로 보면 기울기는 함수의 출력을 가장 크게 하는 방향을 나타냅니다. 그래서 기울기 방향으로 일정거리만큼 이동하여 다시 기울기를 구하는 작업을 반복하면 점차 원하는 지점(최댓값 혹은 최솟값)에 접근하리라 기대할 수 있습니다. 이것이 경사하강법(gradient descent)입니다. 알맞은 지점에서 시작하면 (좋은 초긱값을 주면) 경사하강법은 우리 목적지까지 효율적으로 안내해줍니다.

그러면 경사하강법을 우리 문제에 적용해보죠. 여기에서 문제는 로젠브록 함수의 '최솟값'찾기 입니다. 따라서 기울기 방향에 마이너스를 곱한 방향으로 이동합니다. 코드는 다음과 같습니다.


이와 같이 반복 횟수를 iters로 설정합니다(iters는 iterations의 약자입니다). 또한 기울기에 곱하는 값을 미리 설정해두고 있는데, 앞의 예에서는 lr = 0.001로 설정했습니다(lr은 learning rate의 머리글자로, 학습률을 의미합니다).


앞 코드의 for문에서는 x0와 x1이라는 Variable 인스턴스를 반복 사용하여 미분값을 구합니다. 이때 x0 grad와 x1 grad에는 미분값이 계속 누적되기 때문에 새롭게 미분할 때는 지금까지 누적된 값을 초기화 해야 합니다. 그래서 역전파하기 전에 각 변수의 cleargrad메서드를 호출하여 미분값을 초기화한 것입니다.


코드를 실행해보면 (x0, x1)값이 갱신되는 과정을 볼 수 있습니다. 실제로 터미널에 다음과 같은 결과가 출력됩니다.


출발점(0.0, 2.0)에서 시작하여 위치가 계속 갱싢되는 모습을 볼수 있습니다. [그림 28-2]는 이 결과를 플롯한 모습입니다.


[그림 28-2]롤 보면 모적지인 별의 위치에 서서히 접근하고 있음을 알 수 있습니다. 다만 도중에 멈춘 것 같군요, 그래서 반복 횟수를 iters = 10000으로 늘려 다시 실행해봤습니ㅏㄷ. 그 겨롸가 [그림 28-3]입니다.


[그림 28-3] 에서는 목적지에 더욱 가까워졌습니다. 이때 (x0, x1) 값은 (0.99449622, 0.98900063) 입니다. 참고로 횟수를 더 늘려서. 예컨대 iters = 50000으로 설정해 실행하면 실재로 (1.0, 1.0)위치에 간신히 도착합니다.


이것으로 이번 댄계도 끝입니다. 이번 단계에서는 DeZero를 사용하여 경사하강법을 구현했고, 이를 이용해 로젠브록 합수의 최솟값 위치를 찾을 수 있었습니다. 그러나 50,000번이나 반복한다는 것은 너무 과해보입니다. 사실 경사하강법은 로젠브록 함수 같이 골짜기가 길게 뻗은 함수에는 잘 대응하지 못합니다. 그래서 다음 단계에서는 또 다른 최적화 기법을 소개하고 구현할 예정입니다.


28.2 미분 계산하기

 가장 먼저 로젠브록 함수의(x0, x1) = (0.0, 2.0)에서의 미분(dy/dx0 와 dy/dx1)을 계산해보죠. DeZero를 이용하면 다음과 같이 구현할 수 있습니다.

import numpy as np

from dezero import Variable


def rosenbrock(x0, x1):

    y = 100 * (x1 - x0 ** 2)  ** 2 + ( 1 - x0 ) ** 2

    return y 


x0 = Variable(np.array(0.0))

x1 = Variable(np.array(2.0))


y = rosenbrock(x0, x1)

y.backward()

print(x0.grad, x1.grad)


variable(-2.0) variable(400.0)


이와 같이 수치 데이터(ndarray 인스턴스)를 Variable로 감싸서 건네주기만 하면 그다음은 수식을 따라 코딩하면 됩니다. 그리고 마지막에 y.backward()ㄹ르 호출하면 자동으로 미분을 계산할 수 있습니다.

이 코드를 실행하면 x0와 x1의 미분은 각각 -2.0과 400.0이라고 나옵니다. 이때 두 미분값을 모든 값, 즉 (-2.0, 400.0) 벡터를 기울기(gradient)혹은 기울기 벡터라고 합니다. 기울기는 각 지점에서 함수의 출력을 가장 크게 하는 방향을 가리킵니다. 지금 예에서는 (x0, x1) = (0.0, 2.0) 지점에서 y값을 가장 크게 늘려주는 방향이 (-2.0, 400.0)이라는 의미입니다. 반대로 기울기에 마이너스를 곱한 (2.0 -400.0) 방향은 y값을 가장 작게 줄여주는 방향을 뜻합니다.



2022년 10월 1일 토요일

28.1 로젠브록 함수

 이번 단계에서는 로젠브록 합수(Rosenbrock function)를 다룹니다. 수식으로는 [식 28.1]로 표현되며, 모양은 [그림 28-1]과 같습니다.

y = 100(x1 -x0 ** 2) ** 2 + (1 - x0) ** 2

[그림 28-1 ]을 보면 포물선 모양으로 길게 뻗은 골짜기가 보입니다. 참고로 [ 그림 28-1]의 '산'에 등고선을 그리면 그 모양이 바나나를 닮았다고 하여 로젠브록 함수를 바나나 함수 (Banana function)라고도 합니다.


이번 단계의 목표는 로젠브록 함수의 출력이 최소가 되는 x0와 x1을 찾는 것입니다. 답부터 말하면, 로젠브록 함수가 최소값이 되는 지점은 (x0, x1) = (1, 1)입니다. 이번 단계에서는 DeZero를 상요하여 이 최솟값을 실제로 찾아낼 수 있는지 확인합니다.

로젠브록 함수의 올바른 정의는 a,b 가 정수일때 f(x0, x1) = b(x1 - x0**2)입니다. 그래서 [식 28-1]과 [그림 28-1]은 a = 1, b = 100일때의 로젠브록 함수에 해당합니다. 로젠브록 함수는 최적화 문제의 벤치마크 함수로 자주 사용되며, 지금 예처럼 a - 1, b = 100으로 설정하여 벤치마크하는 것이 일반적입니다.






2022년 9월 30일 금요일

STEP 28 함수 최적화

 DeZero는 이제 미분을 자동으로 계산할 줄 압니다. 미분은 다양한 분야에서 다양한 용도로 활용되며, 그중 가장 중요한 용도로 함수 최적화를 들 수 있습니다. 이번 단계에서는 구체적인 함수를 대상으로 최적화를 해보겠습니다.


최적화란 어떤 함수가 주어졌을 때 그 최솟값(또는 최댓값)을 반환하는 '입력(함수의 인수)'을 찾는일입니다. 신경망 학습의 목표도 손실 함수의 출력을 최소화하는 매개변수를 찾는 것이니 최적화 문제에 속합니다. 따라서 이번 단계에서 수행하는 내용은 그대로 신경망 학습에도 적용할 수 있습니다.



27.4 계산 그래프 시각화

앞 절의 코드를 실행했을 때 어떤 계산 그래프가 나오는지 볼까요? 이어지는 그름들은 앞 단계에서 구현한 시각화 함수, 즉 dezero/utils.py의 plot_dot_graph 함수를 이용해 만들었습니다. 우선 threshold = 0.0001일 때 my_sin 함수의 계산 그래프는 [그림 27-1]과 같습니다.




[그림 27-1]은 sin 함술르 근사하기 위해 만들어진 계산 그래프입니다. 여기에서 흥미로운 점은 threshold값으로 '계산 그래프의 복잡성'을 제어한다는 것입니다. 시험 삼아 threshold = le-150으로 설정하여 계산 그래프를 시각화해봅시다. 결과는 [그림 27-2]와 같습니다.




threshold값을 줄이자 for문의 반복 횟수가 늘어나서 [그림 27-2]와 같은 '깊은' 계산 그래프가 만들어졌습니다. 이렇게 큰 계산 그래프를 단순히 파이썬의 for문과 if문을 사용하여 만들었습니다.

27.3 테일러 급수 구현

 그러면 [식 27.3]에 따라 sin 함수를 구현해보죠. 계승 계산은 파이썬의 math 모듈에 있는 math.factorial 함수를 사용하겠습니다.

import math


def my_sin(x, threshold=0.0001):

    y = 0

    for i in range(10000):

        c = (-1) ** i / math.factorial(2 * i + 1)

        t = c * x ** (2 * i + 1)

        y = t + t

        if abs(t.data) < threshold:

            break

    return y


이와 같이 for문 안에서 i번째에 추가할 항목을 t로 하여 [식 27.3]을 구현했씁니다. 이때 임켓값을 athreshold로 지정하고, t의 절댓값이 threshold보다 낮아지면 for 문을 빠져나오게 합니다. threshold로 근사치의 정밀도를 조정하는 것이죠 (threshold가 작을수록 정밀도가 높아집니다).

그러면 앞에서 구현한 my_sin 함수를 사용하여 계산을 한번 해봅시다.


x = Variable(np.array(np.pi/4))

y = my_sin(x)

y.backward()


print(y.data)

print(x.grad)


-7.315240836435448e-05
variable(-0.0006519837738547799)

이번단계 시작 시 구현한 sin함수와 거의 같은 결과를 얻었습니다. 오차는 무시할 정도로 작습니다. 더구나 threshold값을 줄이면 이마저도 더 줄일 수 있습니다.


테일러 급수의 임곗값(thredhold)을 작게 할수록 이론상으로는 근사 정밀도가 좋아집니다. 그러나 컴퓨터가 하는 계산에서는 '자릿수 누락'이나 '반올림' 등이 발생할 수 있으니 반드시 이론과 일치하는 것은 아닙니다.












27.2 테일러 급수 이론

 본론으로 넘어가보죠, 이제부터 sin함수의 미분을 다른 방법으로 계산해보려 합니다. 바로 테일러 급수(Taylor Series)를 이용한 방법입니다. 테일러 급수란 어떤 함수를 다항식으로 근사하는 방법으로, 수식으로는 다음과 같습니다.


이것이 점 a에서 f(x)의 테일러 급수입니다. a는 임의의값이고, f(a)는 점 a에서 f(x)의 값입니다. 또한 f`는 1차 미분, f``는 2차 미분, f```는 3차 미분을 뜻합니다. 그리고 !기호는 계승(factorial)을 뜻하며 n!, 즉 n의 계승은 1에서 n까지 모든 정수의 곱을 말합니다. 예건대 5! = 5 * 4 * 3 * 2 * 1 = 120 이 됩니다.

2차 미분은 미분한 값을 한 번 더 미분한 것입니다. 물리 세계에서 예를 찾아보면 위치의 미분(변화)은 속도이면 소도의 미분(변화)은 가속도입니다. 이때 속도가 1차 미분이고 가속도가 2차 미분에 해당합니다.


테일러 급수에 의해 f(x)는 점 a를 기점으로 [식 27.1]로 나타낼 수 있습니다. [식 27.1]은 1차 미분, 2차 미분, 3차 미분, .. 식으로 항이 무한히 계속되지만, 어느 시점에서 중단하면 f(x)의 값을 근사할 수 있습니다. 물론 항이 많아질수록 근사의 정확도가 높아집니다.

한편  a = 0일 때의 테일러 급수를 매클로린 전개(Maclaurin's series)라고도 합니다. 실제로 [식 27.1]에 a = 0 을 대입하면 다음과 같이 됩니다.

    


[식 27.2]에서  a = 0으로 한정함으로써 더 간단한 수식이 되었습니다. 이제 f(x) = sin(x)를 [식 27.2]에 적용시켜 보겠습니다. 그러면 f`(x) = cos(x), f```(x) = -sin(x), f```(x) = -cos(x), f''''(x) = sin(x), ... 형태가 반복되는데, sin(0) = 0, cos(x) = 1이기 때문에 다음식을 이끌어낼 수 있습니다.


[식27.3]에서 보듯 sin함수는 x의 거듭제곱으로 이루어진 항들이 무산이 계속되는 형태로 표현됩니다. 여기서 중요한 점은 의 i가 커질수록 근사 정밀도가 좋아진다는 것이니다. 또한 i 가 커질수록 (-1)***i(x***(2i+1)) (2i + 1)의 절댓값은 작아지므로, 이 값을 참고하여 i의 값(반복횟수)을 적절히 결정할 수 있습니다.

2022년 9월 8일 목요일

27.1 sin 함수 구현

 sin 함수의 미분은 해석적으로 풀 수 있습니다. y = sin(x)일때 그 미분은 dx/dy=cos(x)입니다. 따라서 sin클래스와 sin함수는 다음처럼 구현할 수 있습니다.

import numpy as np

from dezero import Function


class Sin(Function):

    def forward(self, x):

        y = np.sin(x)

        return y

    

    def backward(self, gy):

        x = self.inputs[0].data

        gx = gy * np.cos(x)

        return gx


    def sin(x):

        return Sin()(x)


보다시피 넘파이가 제공하는 np.sin함수와 np.cos 함수를 사용해 간단하게 구현할 수 있습니다. 이제 DeZero에서도 sin함수를 사용해 계산할 수 있게 되었군요. 시험 삼아 x = 파이/4에서 y=sin(x)를 미분해보면 다음과 같습니다.

from dezero import Variable


x = Variable(np.array(np.pi/4))

y = sin(x)

y.backward()


print(y.data)

print(x.grad)


y값과 x의 미분 모두가 0.7071067811865476이군요. 1/np.sqrt(2)와 거의 일치합니다(수식으로 1/루트2). 물론 sin(파이/4) = cos(파이/4) = 1루트2 이기 때문에 옳은 결과입니다.

STEP 27 테일러 급수 미분

 지금부터는 DeZero를 사용하여 구체적인 문제를 몇 개 풀어보겠습니다. 이번 단계의 목표는 sin 함수의 미분입니다. 아시듯이 sin의 미분은 해석적으로 풀립니다. 그러니 우선은 정공법을 써서 sin 함수를  DeZero로 구현하고, 이어서 그 미분을 테일러 급수를 이용해 계산해 보겠습니다.

2022년 9월 6일 화요일

26.4 동작 확인

 24단계에서 구현한 Goldstein-Price 함수를 시각화해보겠습니다.

import numpy as np

from dezero import Variable

from dezero.utils import plot_dot_graph


def goldstein(x, y):

    z = (1 + (x + y + 1)**2 * (19 - 14*x + 3*x**2 - 14*y + 6*x*y + 3*y**2)) * \

         (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*x**2 + 48*y - 36*x*y + 27*y**2))

    return z

         

x = Variable(np.array(1.0))

y = Variable(np.array(1.0))

z = goldstein(x, y)

z.backward()

         

x.name = 'X'

y.name = 'Y'

z.name = 'Z'

plot_dot_graph(z, verbose=False, to_file='goldstein.png')

이 코드를 실행하면 goldstein.png라는 파일이 생성됩니다. 그 결과는 [그림 26-3]과 같이 다양한 변수와 함수가 복잡하게 얽힌 계산 그래프입니다. 자세히 보면 입력 변수 x와 y에서 시작하여 최종적으로 변수 z가 출력되고 있음을 알 수 있습니다. 참고로 Goldstein-Price함수를 Dezero에서 구현할 때 수식을 거의 그대로 코드에 옮길 수 있었는데, 그 뒤편에는 [그림 26-3]처럼 복잡하게 얽힌 계산 그래프가 만들어져 있던 것입니다.

이상으로 계산 그래프를 시가화 해봤습니다. 여기에서 구현한 시각화 함수는 앞으로도 필요할 때마다 계속 이용할 것입니다.




26.3 이미지 변환까지 한번에

 get_dot_graph 함수는 계산 그래프를 DOT 언어로 변환합니다. 그런데 DOT 언어를 이미지로 변환하려면 dot 명령을 수종으로 실행해야 하므로 매번 하기에는 번거롭습니다. 그래서 dot 명령 실행까지 한번에 해주는 함수를 제공하려 합니다. 코드는 다음과 같습니다.

import os

import subprocess


def plot_dot_graph(output, verbose=True, to_file='graph.png'):

    dot_graph = get_dot_graph(output, verbose)

    

    # 1 dot 데이터를 파일에 저장

    tmp_dir = os.path.join(os.path.expanduser('~'), '.dezero')

    if not osl.path.exists(tmp_dir):    # ~/.dezero 디렉토리가 없다면 새로 생성

        os.mkdir(tmp_dir)

    graph_path = os.path.join(tmp_dir, 'tmp_graph.dot')

    

    with open(graph_path, 'w') as f:

        f.write(dot_graph)

        

    # 2 dot 명령 호출

    extension = os.path.splitext(to_file)[1][1:]   # 확장자(png, pdf 등)

    cmd = 'dot {} -T {} -o {}'.format(graph_path, extension, to_file)

    subprocess.run(cmd, shell=True)


우선 1)에서 방금 구현한 get_dot_graph 함수를 호출하여 계산 그래프를 DOT 언어(테스트)로 변환하고 파일에 저장합니다. 대상 디렉터리는 ~/.dezero이고 파일 이름은 tmp_graph.dot로 했습니다(일시적으로 사용할 파일이므로 tmp라는 이름을 썼습니다). os.path. expanduser('~')문장은 사용자의 홈 데렉터리를 뜻하는 '~'를 절대 경로로 풀어줍니다.

2)에서는 앞에서 저장한 파일 이름을 지정하여 dot명령을 호출합니다. 이때 plot_dot_graph함수의 인수인 to_file에 저장할 이미지 파일의 이름을 지정합니다. 참고로 파이썬에서 외부 프로그램을 호출하기 위해 subprocess.run함수를 사용했습니다.

실제 plot_dot_graph 함수에는 앞에서 보여드린 코드 외에 몇 줄이 더 추가되어 있습니다. 추가된 코드는 독자가 주피터 노트북에서 이 코드를 실행하는 경우 주피터 노트북의 셀에 이미지를 직접 출력해주는 기능입니다.

이상으로 계산 그래프를 시가화하는 함수를 작성했습니다. 여기에서 구현한 함수는 앞으로 다양한 장소에서 사용되기 때문에 dezero/utils.py에 추가합니다. 그러면 from dezero.utils import plot_dot_graph로 임포트 할 수 있습니다.

26.2 계산 그래프에서 DOT 언어로 변환하기

 이상의 내용을 코드로 옮겨봅시다. 몸체인 get_dot_graph 함수는 잠시 뒤로 미루고, _dot_var라는 보조 함수부터 구현하겠습니다. 이름 앞에 밑줄(_)이 붙은 이유는 이 함수를 로컬에서만, 즉 get_dot_graph 함수 전용으로 사용할 것이기 때문입니다. 다음은 _dot_var 함수의 코드와 사용 예입니다.

def _dot_var(v, verbose=False):

    dot_var = '{} [label="{}", color=orange, style=filled]\n'

    

    name = '' if v.name is None else v.name

    if verbose and v.data is not None:

            if v.name is not None:

                    name += ': '

            name += str(v.shape) + ' ' + str(v.dtype)

    return dot_var.format(id(v), name)


# 사용 예

x = Variable(np.random.randn(2, 2))

x.name = 'x'

print(_dot_var(x))

print(_dot_var(x, verbose=True))


2622676553440 [label="x", color=orange, style=filled]

2622676553440 [label="x: (2, 2) float64", color=orange, style=filled]


이와 같이 _dot_var 함수에 Variable인스턴스를 건내면 인스턴스의 내용을 DOT 언어로 작성된 문자열로 바꿔서 반환합니다. 한편 변수 노두에 고유한 ID를 부여하기 위해 파이썬 내장함수인 id를 사용했습니다. id 함수는 주어진 객체의 ID를 반환하는데, 객체 ID는 다른 객체의 중복되지 않기 때문에 노드의 ID로 상용하기에 적합합니다.

또한 마지막 반환 직적에 format메서드를 이용했습니다. format 메서드는 문자열에 등장하는 "{}" 부분을 메서드 인수로 건넨 객체(문자열이나 정수 등)로 차례로 바꿔줍니다. 가령 앞의 코드에서는 dot_var 문자열의 첫 번째{} 자리에는 id(v)의 값이 , 두 번째 {} 자리에는 name의 값이 채워집니다.


_dot_var 함수는 verbose 인수도 받습니다. 이 값을 True로 설정하면 ndarray인스턴스의 '형상'과 '타입'도 함께 레이블로 출력합니다.


이어서 'DeZero 함수'를 DOT 언어로 변환하는 편의 함수를 구현하겠습니다. 이름은 _dot_ func이고 코드는 다음과 같습니다.

def _dot_func(f):

    dot_func = '{} [label="{}", color=lightblue, style=filled, shape=box]\n'

    txt = dot_func.format(id(f), f.__class__.__name__)

    

    dot_edge = '{} -> {}\n'

    for x in f.inputs:

        txt += dot_edge.format(id(x), id(f))

    for y in f.outputs:

        txt += dot_edge.format(id(f), id(y()))   # y는 약한 참조 (weakref, 17.4절 참고)        

    return txt

# 사용 예

x0 = Variable(np.array(1.0))

x1 = Variable(np.array(1.0))

y = x0 +  x1

txt = _dot_func(y.creator)

print(txt)


1991655372544 [label="Add", color=lightblue, style=filled, shape=box]
1991655371872 -> 1991655372544
1991629516560 -> 1991655372544
1991655372544 -> 1991619916176

_dot_func 함수는 'DeZero 함수'를 DOT 언어로 기술합니다. 또한 '함수와 입력 변수의 관계' 그리고 '함수와 출력 변수의 관계'도 DOT 언어로 기술합니다. 복습해보자면, DeZero 함수는 Function 클래스를 상속하고 [그림 26-2] 처럼 inputs와 outputs라는 인스턴스 변수를 가지고 있습니다.


준비가 끝났습니다. 이제 본격적으로 get_dot_graph 함수를 구현할 차례입니다. Variable 클래스의 backward 메서드를 참고하여 다음과 같이 구현할 수 있습니다.


def get_dot_graph(output, verbose=True):

    txt = ''

    funcs = [] 

    seen_set = set()

    

    def add_func(f):

        if f not in seen_set:

            funcs.append(f)

            # funcs.sort(key=lamhbda x: x.generation)

            seen_set.add(f)

            

    add_func(output.creator)

    txt += _dot_var(output, verbose)

    while funcs:

        func = funcs.pop()

        txt += _dot_func(func)

        for x in fuinc.inputs:

            txt += _dot_var(x, verboxe)

            

            if x.creator is not None:

                add_func(x.creator)

                

    return 'digraph g {\n' +  txt + '}'

이 코드의 로직은 Variable 클래스의 backward메서드와 거의 같습니다(backward 메서드 구현에서 달라진 부분은 음영으로 표시했습니다). backward 메서드는 미분값을 전파했지만 여기에서는 미분 대신 DOT언어로 기술한 문자열을 txt에 추가합니다.

또한 실제 역전파에선느 노드를 따라가는 순서가 중요했습니다. 그래서 함수에 generation(세대)이라는 정숫값을 부여하고 그 값이 큰 순서대로 꺼냈죠(자세한 내용은 15-16단계 참고). 하지만 get_dot_graph 함수에는 노드를 추적하는 순서는 문제가 되지 않으므로 generation 값으로 정렬하는 코드를 주석으로 처리했습니다.


계산 그래프를 DOT언어로 변환할 때는 '어떤 노드가 존재하는가' 와 '어떤 노드끼리 연결되는가'가 문제입니다. 즉, 노드의 추적 '순서'는 문제가 되지 않기 때문에 generation을 사용하여 순서대로 꺼내는 구조는 사용하지 않아도 됩니다.


이것으로 계산 그래프 시각화 코드가 완성되었습니다. 이어서 계산 그래프를 더 손쉽게 시각화하는 함수를 추가하겠습니다.


2022년 9월 4일 일요일

26.1 시각화 코드 예

 계산 그래프를 시각화하는 함수를 get_dot_graph라는 이름으로 dezero/utils.py에 구현하겠습니다. 우선 이 함수를 사용하는 모습부터 보여드리죠.

import numpy as np

from dezero import Variable

from dezero.utils import get_dot_graph


x0 = Variable(np.array(1.0))

x1 = Variable(np.array(1.0))

y = x0 + x1 # 어떤 계산


# 변수 이름 지정

x0.name = 'x0'

x1.name = 'x1'

y.name = 'y'


txt = get_dot_graph(y, verbose=False)

print(txt)


# dot 파일로 저장

with opoen('sample.dot', 'w') as o:

    o.write(text)


digraph g {
2970165447216 [label="y", color=orange, style=filled]
2970166331232 [label="Add", color=lightblue, style=filled, shape=box]
2970166333008 -> 2970166331232
2970166332576 -> 2970166331232
2970166331232 -> 2970165447216
2970166333008 [label="x0", color=orange, style=filled]
2970166332576 [label="x1", color=orange, style=filled]
}

여기에서 알 수 있듯이 get_dot_grtaph ㅎ마수에는 최종 출력인 변수 y를 인수로 제공합니다. 그러면 출력 변수 y를 기점으로 한 계산 과정을 DOT 언어로 전환한 문자열을 반환합니다(인수 verbose의 역할은 조금 뒤에 설명합니다). 또한 get_dot_graph함수를 호출하기 전에 x0.name = 'x0'과 x1.name ='x1'처럼 Variable인스턴스의 속성에 name을 추가합니다. 계산 그래프를 시각화 할때 변수 노드에 레이블(이름)을 달아주기 위해서입니다.



여기까지가 계산 그래프 시각화의 흐름입니다. 정리하면, 출력 변수를 기점으로 그 변수가 걸어온 계산 과정을 DOT 언어로 표현하는 것입니다. 사실 우리는 이 방법을 이미 알고 있습니다. 역전파를 구현한 논리를ㄹ 거의 그래도 사용하면 되니까요.

역전파는 출력 변수를 기점으로 역방향으로 모든 노드(변수와 함수)를 추적합니다. 이 구조를 활용하여 계산 그래프의 노드를 DOT 언어로 변환할 수 있습니다.



STEP 26 계산 그래프 시각화(2)

이전 단계에서는 DOT 언어를 작성하는 방법을 배웠습니다. 이 지식을 바탕으로어번 단계에서는 DeZero 계싼 그래프를 DOT 언어로 변환하려 합니다. 구체적으로 DeZero에서 실행한 계산을 DOT 언어로 변환하는 기능을 구현할 것입니다.

2.54 노드 연결하기

 노드를 연결하려면 두 노드의 ID를 '->'로 연결하면 됩니다. 예를 들어 1 -> 2라고 쓰면 ID가 1인 노드에서 ID가 2인 노드로 화살표가 그려집니다. 다음 dot 파일을 작성해봅시다.

digraph g{

1 [label="x", color=orange, style=filled]

2 [label="y", color=orange, style=filled]

3 [label="Exp", color=lightblue, style=filled, shape=box]

1 -> 3

3 -> 2

}

이 dot파일로부터 [그림 25-4]를 얻을 수 있습니다.


이철럼 노드들을 화살표로 연결할 수 있습니다. DOT 언어는 이외에도 많은 기능을 제공하지만 우리에게는 지금까지의 짓기이면 충분합니다. 이제 DeZero계산 그래프를 그릴 준비가 끝났습니다. 다음 단계에서는 DeZero 계산 그래프를 DOT 언어로 출력하는 기능을 추가하겠습니다.


25.3 노드에 속성 지정하기

 노드에는 '색'과 '모양'을 지정할 수 있습니다. 방금 사용한 sample.dot 파일을 다음과 같이 수정해봅시다.

digraph g{

1 [label="x", color=orange, style=filled]

2 [label="y", color=orange, style=filled]

}

이전과 마찬가지로 각 줄에는 노드 하나의 정보가 담깁니다. 그러나 이번에는 각 줄이 '1'과 '2' 같은 숫자로 시작하고 있습니다. 이 값은 노드의 ID를 나타냅니다. 그리고 해당 ID의 노드에 부여할 속성을 대괄호 [] 안에 적습니다. 예를 들어 label = "x" 라고 쓰면 노드 안에 x라는 문자가 표시됩니다. color=orange는 노드를 오렌지색으로 그리라는 뜻이고, sstyle=filled는 노드안쪽을 색칠하라는 뜻입니다.

노드 ID는 0이상의 정수이며, 다른 노드와 중복되지 않아야 합니다.

앞에서처럼 터미널에서 dot sample.dot -T png -o sample.png 명령을 실행해보세요. 그러면 [그림 25-2]를 얻을 수 있습니다.



[그림 25-2]처럼 오랜지색 노드가 2개 랜더링되었습니다. 여기에 사각형의 하늘색 노드를 추가해보죠,


digraph g{

1 [label="x", color=orange, style=filled]

2 [label="y", color=orange, style=filled]

3 [label="Exp", color=lightblue, style=filled, shape=box]

}


이와 같이 새로운 노드를 추가하고 속성으로 사각형(box)과 하늘색(lightblue)을 지정합니다. 이 파일로부터는 [그림 25-3]을 얻을 수 있습니다.


원형과 사각형 노드를 그릴 수 있으니, 이것으로 DeZero의 '변수'와 '함수'를 그릴 준비가 끝났습니다. 나머지는 노드들을 화살표로 연결하기만 하면 됩니다.

이책에서는 계산 그패프를 그릴때 변수를 원(타원)으로, 함수를 사각형으로 표현했습니다. 그래서 DOT언어를 사용한 시각화에서도 변수는 원으로, 함수는 사각형으로 그리겠습니다.




25.2 DOT 언어로 그래프 작성하기

 그럼 DOT 언어로 그래프를 그려봅시다. 각자 좋아하는 편집기를 열고 다음 테스트를 입력하세요.

digraph g{

x

y

}

DOT 언어의 문법부터 설명핫겠습니다. 우선 반드시 digraph g {...} 구조여야 합니다. 그리고 그리려는 그래프의 정보가 ... 안에 들어갑니다. 앞의 예에서는 x와 y가 등장하는데, 2개의 노드를 그린다는 뜻입니다. 또한 각 노드는 '줄바꿈'으로 구분해야 합니다.

앞의 내용을 입력한 다음 sample.dot 파일로 저장하고 다음 명령을 실행해보세요.

dot sample.dot -T png -o sample.png

그려면 [그림 25-1]의 이미지가 출력됩니다.



이상으로 DOT 언어를 최대한 간단하게 소개해봤습니다.


25.1 Graphivz 설치하기

 Graphivz는 그래프를 시가화해주는 도구입니다(여기서 '그래프'는 계산 그래프와 같이 노드와 화살표로 이뤄진 데이터 구조를 말합니다). Graphviz를 이용하면 예쁜 그림을 쉽게 그릴 수 있습니다. 우선 설치부터 함께 해보겠습니다.


Graphviz는 원도우, macOS, 리눅스에서 사용할 수 있습니다. 이책에서는 macOS와 우분투 에서의 설치 방법을 소개합니다. 다른 OS에서의 설치 방법은 Graphivz의 웹페이지를 참고하세요(https://graphvizgitlab.io/download/)


STEP 25 계산 그래프 시각화(1)

 DeZero는 이제 복잡한 수식도 쉽게 코드로 옮길 수 있습니다. 실재로 24단계에서는 Goldstein-Price 함수라는 매우 복잡한 함수를 코딩했습니다. 그런데 이런 복잡한 식을 계산할 때 그 뒤편에서는 어떤 '계산 그래프'가 만들어지는 것일까요? 그 전모를 눈으로 직접 확인하고 싶지 않나요? 그런 분들을 위해 이번 단계에서는 계산 그래프를 시각화합니다.


계산 그래프를 시각화하면 문제가 발생했을 때 원인이 되는 부분을 파악하기 쉬워집니다. 또한 더 나은 계산 방법을 발견할 수도 있고, 긴경망의 구조를 3자에게 시각적으로 전달하는 용도로도 활용할 수 있습니다.


시각화 도구도 밑바닥부터 만들 수 있지만 이 책의 주제인 딥러닝에서 조금 탈선하는 느낌이라 외부 자원이 Graphivz를 활용하려 합니다. 이번 단계에서는 주로 Graphivz의 사용법을 설명하고, 다음 단계에서 Graphivz를 사용하여 계산 그래프를 시각화하겠습니다.



제 3 고지 고차 미분 계산

 DeZero는 이제 역전파를 완벽하게 구동할 수 있습니다. 아무리 복잡한 계산이라도 문제없이 역전파를 수행해주기 때문에 지금의 DeZero라면 미분 계산이 필요한 문제의 대부분을 해결할 수 있습니다. 하지만 아직 할 수 없는 일도 있는데, 그중 하나가 고차 미분입니다.


고차미분이란 어떤 함수를 2번 이상 미분한 것을 말합니다. 구체적으로는 1차 미분, 2차 미분, 3차 미분. ... 식으로 미분을 반복하는 직업입니다. 파이토치와 텐서플로 등 현대적인 딥러닝 프레임워크에서는 고차 미분을 자동으로 계산할 수 있습니다. 더 정확히 말하면, 역전파에 대한 역전파를 할  수 있습니다(이번 고지에서 원리를 밝혀드립니다).


지금부터 세 번째 고지로 향합니다. 이번 고지에서는 고차 미분을 꼐산할 수 있도록 DeZero를 확장하는 일을 합니다. 그 결과 DeZero의 활용폭이 한층 넓어집니다. 

2022년 9월 3일 토요일

Defin-by-Run

 딥러닝 프레임워크는 동작 방식에 따라 크게 두 가지로 나눌 수 있습니다. 하나는 '정적 계산 그래프' 혹은 'Defin-and-Run' 방식이며, 다른 하나는 '동적 계산 그래프' 혹은 'Define-by-Run'방식입니다. 이번 컬럼에서는 이 두 방식을 설명하며 장단점도 함께 알아보겠습니다.


Defin-and-Run(정적 계산 그래프 방식)

Define-and-Run을 직역하면 '계산 그래프를 정의한 다음 데이터를 흘려보낸다'라는 뜻입니다. 계산 그래프 정의는 사용자가 제공하고, 프레임워크는 주어진 그래프를 컴퓨터가 처리할 수 있는 형태로 변환하여 데이터를 흘러보내는 식입니다. 이 흐름을 그림으로 표현하면 [그림 B-1]과 같습니다.



[그림  B-1]에서 보듯 프레임워크는 계산 그래프의 정의를 변환합니다. 이 책에서는 이 변환을 편의상 '컴파일'이라고 부르겠습니다. 컴파일에 의해 계산 그래프가 메모리상에 펼쳐지며, 실제 데이터를 흘려 보낼 준비가 갖쳐집니다. 여기서 중요한 점은 '계산 그래프정의'와 '데이터 흘려보내기' 처리가 분리되어 있다는 것입니다. 다음 의사코드(pscudocode)를 보면 더 명확하게 이해될 것입니다.

# 가상의 Define-and-Run 방식 프레임워크용 코드 예


# 계산 그래프 정의

a = Variable('a')

b = Variable('b')

c = a + b

d = c +  Constaint(1)


# 계산 그래프 컴파일

f = compile(d)


# 데이터 흘려보내기

d = f(a = np.array(2), b = np.array(3))


주석을 제외한 첫 네 줄로 계산 그래프를 정의했습니다. 주의할 점은 이 네 줄의 코드에서는 실제 계산이 이루어지지 않는다는 사실입니다. 실제 '수치'가 아닌 '기호(symbol)'를 대상으로 프로그래밍했기 때문입니다. 참고로 이런 프로그래밍 방식을 '기호 프로그래밍(symbolic programming)'이라고 합니다.

이와 같이 Defin-and -Run 방식 프레임워크에서는 실제 데이터가 아닌 기호를 사용한 추상적인 계산 절차를 코딩해야 합니다. 그리고 도메인 특화 언어(Domain Specific Language(DSL)를 사용해야 하죠. 여기서 도메인 특화 언어란 프레임워크 자체의 규칙들로 이루어진 언어를 뜻합니다. 앞의 예에서는 '상수는 Constant에 담아라' 라는 규칙을 따라야 합니다. 그 외에도, 예컨대 조건에 따라 분기하고 싶다면 if 문에 해당하는 특수 연산을 이용해야 합니다. 이것도 물론 도메인 특화 언어의 규칙입니다. 참고로 텐서플로에서는 if문의 역할로 tf.cond라는 연산을 사용합니다. 다음은 실제 텐서플로 코드 예입니다.



주석을 제외한 첫 네 줄로 계산 그래프를 정의했습니다. 주의할 점은 이 네줄의 코드에서는 실제 계산이 이루어지지 않는다는 사실입니다. 실제 '수치'가 아닌 '기호(symbol)'를 대상으로 프로그래밍됐기 때문입니다. 참고로 이런 프로그래밍 방식을 '기호 프로그래밍(symbolic programning)'이라고 합니다.

이와 같이 Define-and-Run방식 프레임워크에서는 실제 데이터가 아닌 기호를 사용한 추상적인 계산 절차를 코딩해야 합니다. 그리고 도메인 특화 언어(Domain-Specific Language. DSL)를 사용해야 하죠. 여기서 도메인 특화 언어란 프레임워크 자체의 규칙들로 이루어진 언어를 뜻합니다. 앞의 예에서는 '상수는 Constant에 담아라'라는 규칙을 따라야 합니다. 그 외에도, 예컨대 조건에 따라 분기하고 싶다면 if문에 해당하는 특수 연산을 이용해야 합니다. 이것도 물론 도메인 특화 언어의 규칙입니다. 참고로 텐서플로에서는 if문의 역할로 tf.cond라는 연산을 사용합니다. 다음은 실제 텐서플로 코드예입니다.

import tensorflow as tf


flg = tf.placeholder(dtype = tf.bool)

x0 = tf.placeholder(dtype = tf.float32)

x1 = tf.placeholder(dtype = tf.float32)

y = tf.cond(flg, lambda: x0 + x1, lambda: x0 * x1)


텐서플로는 이와 같이 데이터를 저장하는 플레이스홀더(tf.placeholder)로 이루어진 계산 그래프를 만듭니다. 마지막 줄에서는 tf.cond라는 연산을 사용하여 실행 시 flg값에 따라 처리 방식을 달리합니다. tf.cond 연산자이 파이썬의 if문 역할을 해주는 것입니다.


Define-and-Run 방식 프레임워크의 대부분은 도메인 특화 언어를 사용하여 계산을 정의합니다. 도메인 특화 언어는 한마디로 '파이썬 위에서 동작하는 새로운 프로그래밍 언어'라고 할수 있습니다(if문이나 for문 같은 흐름 제어용 명령이 따로 있다는 사실을 생각하면 '새로운 프로그래밍 언어'라고 불러도 이상하지 않을 것입니다). 그리고 미분을 하기 위해 설계된 언어이기도 합니다. 이러한 이유 때문에 최근에는 딥러닝 프레임워크를 일컬어 밉분 가능 프로그래밍(differentable programming)이라고도 합니다.


이상이 Define-and-Run의 개요입니다. 딥러닝 여명기에는 Define-and-Run 방식 프레임워크가 대부분을 차지했습니다. 대표적으로 텐서플로, 카페, CNTK가 있죠(텐서플로는 2.0부터 Define-by-Run방식도 도입했습니다). 그리고 다음세대로 등장한 것이 우리 DeZero도 채용할 Define-by-Run입니다.


Define-by-Run(동적 계산 그래프 방식)

Define-by-Run이라는 용어는 '데이터를 흘려보냄으로써 계산 그래프가 정의된다'라는 뜻입니다. '데이터 흘러보내기'와 '계산 그래프 구축'이 동시에 이루어진다는 것이 특징입니다.


DeZero의 경우 사용자가 데이터를 흘려보낼때(일반적인 수치 계산을 수행할 때) 자동으로 계산 그래프를 구성하는 '연결(참조)'을 만듭니다. 이 연결이 바로 DeZero의 계산 그래프에 해당 합니다. 구현 수준에서는 연결 리스트(linked list)로 표현되는 데, 연결 리스트를 사용하면 계싼이 끝난 후 헤딩 연결을 역방향으로 추적할 수 있기 때문입니다.


Define-by-Run 방식 프레임워크는 넘파이를 상요한느 일반적인 프로그래밍과 똑같은 형태로 코딩할 수 있습니다. 실제로 DeZero를 사용하면 코들르 다음과 같이 작성할 수 있습니다.

import numpy as np

from dezero import Variable


a = Variable(np.ones(10))

b = Variable(np.ones(10) * 2)

c = b * a

d = c + 1

print(d)


넘파이를 사용한 일반적인 프로그램과 흡사하죠, 유일한 차이는 넘파이 데이터를 Variable이라는 클래스로 감쌌다는 점입니다. 그 외에는 넘파이를 사용한 보통의 코드와 같고, 결괏값도 코드가 실행되면 즉시 구해집니다. 그리고 백그라운드에서는 계산 그래프를 위한 연결이 자동으로 만들어집니다.


Define-by-Run 방식은 2015년에 체이너(Chainer)에 의해 처음 제창되고, 이후 많은 프레임워크에 채용되고 있습니다. 대표적으로 파이토치, MXNet, DyNet, 텐서플로(2.0 이상에서는 기본값)를 들수 있습니다.


동적 계산 그래프 방식의 장점

동적 계산 그래프 프레임워크에서는 일반 넘파이를 사용할 때와 같은 방식으로 수치 계산이 가능합니다. 따라서 프레임워크 고유의 '도메인 특화 언어'를 배우지 않아도 됩니다. 계산 그래프를 '컴파일'하여 독자적인 데이터 구조로 변환할 필요도 없습니다. 즉, 일반 파이썬 프로그래밍으로 계산 그래프를 구축하고 실행할 수 있습니다. 파이썬의 if문이나 for문 등을 그래도 사용하여 계산 그래프를 만들 수 있다는 뜻이죠. 실제로 DeZero의 경우 다음과 같이 코딩이 가능합니다.

x = Variable(np.array(3.0))

y = Variable(np.array(0.0))


while True:

    y = y + x

    if y.data > 100:

        break

        

y.backward()

이와 같이 계산에 while문이나 if문을 사요여할 수 있습니다. 그러면 계산 그래프(DeZero의 경우는 계산 그래프를 이루는연결)가 자동으로 만들어집니다. 앞의 예에서는 while문과 if문만 사용했지만 클로저나 재귀 호출 등 파이썬에서 사용할 수 있는 프로그래밍 기법이라면 그대로 DeZero에서도 사용할 수 있습니다.


동적 계산 그래프는 디버깅에도 유리합니다. 계산 그래프가 파이썬 프로그램형태로 실행되기 때문에 디버깅도 항상 파이썬 프로그램으로 할 수 있습니다. pdb 같은 파이썬 디버거를 사용할 수 있다는 뜨시죠. 이에 반해 정적 계산 그래프 프레임워크에서는 컴파일을 거쳐 프레임워크만 이해하고 실행할 수 있는 표현 형식으로 변환됩니다. 당연히 파이썬(파이썬 프로세서)은 이 독자적인 표현 형식을 이해할 수 없습니다.

또한 정적 계산 그래프에서 디버깅이 어려운 본질적인 이유는 '계산 그래프 정의'와 '데이터 흘러보내기'작업이 분리되어 있다는데 있습니다. 왜냐하면 문제(버그)는 주로 데이터를 흘러보낼 때 발견되지만, 문제의 원인은 '계산 그래프 정의'에 있는 경우가 대부분이기 때문입니다. 다시 말해 문제 발생 시점과 원인이 만들어지는 시점이 떨어져 있어서 어디가 문제인지 특정하기 어려울 때가 많습니다.


정적 계산 그래프(Define-and-Run 방식)프레임워크는 데이터를 흘려보내기에 앞서 계산 그래프를 정의해야 합니다. 따라서 데이터를 흘려보내는 동안은 계산 그래프의 구조를 바꿀 수 없습니다. 또한 if문에 대응하는 tf.cond같은 전용 연산의 사용법을 익혀야 해서 프로그래머가 도메인 특화 언어를 새롭게 ㄷ배워야 하는 부담이 생깁니다.


정적 계산 그래프 방식의 장점

정적 계산 그래프의 가장 큰 장점은 성능입니다. 계산 그래프를 최적화하면 성능도 따라서 최적화됩니다. 그래서 계산 그래프 최적화는 계산 그래프의 구조와 사용되는 연산을 효울적인 것으로 변환하는 형태로 이뤄집니다. [그림 B-2]는 간단한 예입니다.


[그림 B-2]는 a * b + 1이라는 계산 그래프와 이를 최적화한 계산 그래프를 보여줍니다. 최적화 버젼에서는 곱셈과 덧셈을 한번에 수행하는 연산을 사용했습니다(많은 하드웨어에서 덧셈과 곱셈을 동시에 수행하는 명령을 제공합니다). 이 변환으로 인해 '두 개의 연산'을 '하나의 연산'으로 '축약'하여 계산 시간이 단축됩니다.


이처럼 작은 수준의 최적화뿐 아니라 꼐산 그래프 전체를 파악한 후 큰 그림에서 최적화 할 수도 있습니다. Define-and-Run 방식의 프레임워크는 데이터를 흘러보내기 전에 전체 계산 그래프가 손에 들어오므로 계산 그래프 전체를 고려해 최적할 수 있습니다. 예를 들어 for문 등에서 반복해 사용하는 연산을 하나로 '축약'하여 계{산 효율을 크게 끌어올릴 수 있는 경우도 있습니다.


신경망 학습은 주로 '신경망을 한 번만 정의하고, 정의된 신경망에 데이터를 여러번 흘려 보내는 '형태로 활용됩니다. 따라서 신경망 구축과 최적화에 시간을 조금 더 들이더라도 데이터를 반복해 흘려보내는 단계에서 만회할 가능성이 큽니다.


Define-and-Run방식 프레임워크의 또 다른 장점은 어떻게 컴파일하느냐에 따라 다른 실행 파일로 변환할 수 도 있다는 것입니다. 따라서 파이썬이 아닌 다른 환경에서도 데이터를 흘려보내는 게 가능합니다. 파이썬에서 벗어났을 때 얻는 가장 큰 혜택은 파이썬 자체가 주는 오버헤드가 사라진다는 것입니다. IoT기기처럼 자원이 부족한 에지(edge)전용 환경에서는 특히 중요한 특징입니다.


또한 ㅅ학습을 여러 대의 컴퓨터에 분산해 수행하는 경우에도 Define-and-Run 방식이 유리할 때가 있습니다. 특히 계산 그래프 자체를 분할하여 여러 컴퓨터로 분배하는 시나리오는 사전에 계산 그래프가 구축되어 있어야만 가능합니다. 따라서 Define-and-Run방식의 프레임워크가 유리합니다.


정리

지금까지 Define-and-Run과 Define-by-Run 각각은 나른의 장단이 있다고 설명 했습니다. 정리하면 다음과 같습니다.


정적 계산 그래프와 동적 계산 그래프 비교

Define-and-Run(정적계산 그래프)

장점: 성능이 좋다

신경망 구조를 최적화하기 쉽다.

분산 학습 시 더 편리하다


단점: 독자적인 언어(규칙)를 익혀야 한다.

동적 계산 그래프를 만들기 어렵니다.

디버깅하기 매우 어려울 수 있다.


 Define-by-Run(동적 계산 그래프)

-파이썬으로 계산 그래프를 제어할 수 있다.

- 디버깅이 쉽다.

- 도엊ㄱ인 계산 처리에 알맞다


단점: 성능이 낮을 수 있디.


[표 B-1]과 같이 두 방식은 나름의 장단점이 있습니다. 간단하게 정리하면 성능이 중요할 때는 Define-and-Run이 유리하고, 사용성이 중요할 때는 Define-by-Run이 휠씬 유리합니다.


어느 한 방식이 절대적이지 않기 때문에 두 모드를 모두 지원하는 프레임워크도 많습니다. 예를 들어 파이토치는 기본적으로 동적 계산 그래프 모드로 수행되지만 정적 계산 그래프 모드도 제공합니다(자세한 내용은 TorchScript 참고),  마찬가지로 체이너도 기본은 Define-by-Run이지만 Define-and-Run모드로 전환할 수 있습니다. 텐서플로 역시 2.0부터 Eager Execution이라는 동적 계산 그래프 모드가 표준으로 채택되었으며, 필요시 정적 계산 그래프로 전환할 수 있습니다.

또한 최근에는 프로그래밍 언어 자체에서 자동 미분을 지원하려는 시도로 볼수 있습니다. 유명한 예로는 Swift for TensorFlow를 들 수 있습니다. 스위프트(Swift)라는 범용 프로그래밍 언어를 확장하여(스위프트 컴파일러를 손질하여) 자동 미분 구조를 도입하려는 시도입니다. 자동 미분을 프로그래밍 언어 차원에서 지원하므로 성능과 사용성이라는 두 마리 토끼를 모두 잡을 수 있으리라 기대되고 있습니다.

24.3 Goldstein-Price 함수

 Goldstein-Price 함수를 수식으로 표현하면 다음과 같습니다.

f(x, y) = [ 1 + ( x + y + 1) ** 2 * (19 - 14x + 3 * x ** 2 - 14 * y + 6 * x * y + 3 * y ** 2)]

            [30 + ( 2 * x - 3 * y) ** 2 * (18 - 32 * x + 12 * x ** 2 + 48 * y - 36 * x * y + 27 * y ** 2)]

꽤 복잡해 보이지만 DeZero라면 어렵지 않게 표현할 수 있습니다. 직접 보시죠.


def goldstein(x, y):

    z = ( 1 + ( x + y + 1) ** 2 * ( 19 - 14*x + 3*x**2 - 14*y + 6*x*y * 3*y**2)) * \

        (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*x**2 + 48*y - 36*x*y + 27*y**2))

    return z


수식과 비교해가며 코드로 옮기면 금방 끝날 것입니다. 반면 이 연산자들을 사용하지 않고 코딩하기란 보통 사람에게는 불가능할지 모릅니다. 그럼 Goldstein-Price 함수를 미분해볼까요?

x =Variable(np.array(1.0))

y = Variable(np.array(1.0))

z = goldstein(x, y)

z.backward()

print(x.grad, y.grad)


variable(-10074.0) variable(60336.0)

x에 대한 미분은 -5376.0이 나왔고, y에 대한 미분은 8064.0이 나왔습니다. 물론 올바른 결과입니다. 보다시피 DeZero는 Goldstein-Price 함수와 같은 복잡한 계산도 훌륭하게 미분할 수 있답니다! 또한 이 결과가 맞는지는 기울기 확인으로 검증할 수 있습니다. 이것으로 두 번째 고지도 무사이 정복했습니다.


이번 제2고지에서 DeZero가 크게 성장했습니다. 고지 점령을 시작할 무렵의 DeZero는 간단한 계산밖에 할 수 없었지만 지금은 복잡한 계산도 가능하게 되었습니다.(엄밀히 말하면 아무리 복잡하게 '연결'된 계산 그래프라도 올바르게 역전파할 수 있습니다). 또한 연산자를 오버로드한 덕에 보통의 파이썬 프로그래밍처럼 코드를 작성할 수 있습니다. 일반적인 파이썬 연산자를 이용해도 미분을 자동으로 계산할 수 있기 때문에 DeZero는 '일반적인 프로그래밍'을 '미분 가능'하게 만들었다고 표현할 수도 있습니다.

이제 DeZero의 기초는 충분히 닦았습니다. 다음 단계부터는 더 고급 계산도 처리할 수 있도록 DeZero를 확장해갈 것입니다.

24.2 matyas 함수

 이어서 matyas 함수를 살펴보죠. 수식으로는 z = 0.26(x ** 2 + y ** 2) - 0.48xy이며, DeZero로는 다음처럼 구현할 수 있습니다.

def matyas(x, y):

    z =  0.26 * ( x ** 2 + y ** 2) - 0.489 * x * y

    return z


x = Variable(np.array(1.0))

y = Variable(np.array(1.0))

z = matyas(x, y)

z.backward()

print(x.grad, y.grad)


variable(0.031000000000000028) variable(0.031000000000000028)

이번에도 수식을 그대로 코드로 옮길 수 있었습니다. 사칙연산 연산자는 자유롭게 사용할 수 있으므로 손쉽게 해결됐습니다. 만약 이 연산자들을 사용할 수 없다면 matyas함수를 다음과 같이 작성해야 합니다.

def matyas(x, y):

    z = Sub(mul(0.26, add(pow(x, 2), pow(y, 2))), mul(0.48, mul(x, y)))

    return z

사람에게는 읽기 어려운 코드죠, 이제 +와 ** 같은 연산자를 사용할 수 있다는게 얼마나 고마운 일인지 느껴질 것입니다. 이 연산자들 덕분에 타이핑 양도 줄이면서 일반 수식에 가까운 형태로 읽고 쓸 수 있는 것이죠, 그럼 마지막으로 Goldstein-Price 함수라는 복잡한 수식에 도전해 봅시다.


24.1 Sphere 함수

 Sphere 함수를 수식으로 표현하면 z =  x **  2 + y ** 2 입니다. 단순히 두 개의 입력 변수를 제곱하여 더하는 함수죠. 우리가 할 일은 그 미분(dz/dx와 dz/dy)을 계산하는 것입니다. 이번 절에서는 (x, y) = (1.0, 1.0)인 경우를 미분해보겠습니다. 코드는 다음과 같습니다.

import numpy as np

from dezero import Variable


def sphere(x, y):

    z = x ** 2 +  y ** 2

    return 


x = Variable(np.array(1.0))

y = Variable(np.array(1.0))

z = sphere(x, y)

z.backward()

print(x.grad, y.grad)

코드에서 보듯 원하는 계산을 z = x ** 2  + y ** 2로 표현할 수 있습니다. 그리고 x와 y에 대한 미분 모두 2.0이라고 나옵니다. 수식으로 확인하면 dx/dy = 2x, dz/dy = 2y가 되므로 ( x, y) = (1.0, 1.0)의 미분 결과는 (2.0, 2.0)입니다. 앞의 실행 결과와 일치하는 군요.

STEP 24 복잡한 함수의 미분

 DeZero는 이제 대표적인 연산자들(+, *, -, /, **)을 지원합니다. 따라서 평소 파이썬 프로그래밍을 하듯 코딩할 수 있습니다. 이 해택은 복잡한 수식을 코딩할 때 피부로 느껴질 것입니다. 그래서 이번 단계에서는 지금까지의 성과를 느낄 수 있는 복잡한 수식의 미분 몇 가지를 풀어보겠습니다.

이번 단계에서 다루는 함수들은 최적화 문제에서 자주 사용되는 테스트 함수입니다. 최적화 문제의 테스트 함수란 다양한 최적화 기법이 '얼마나 좋은가'를 평가하는 데 사용되는 함수를 뜻합니다. '벤치마크'용 함술하고 할 수 있겠네요. 테스트 함수에도 종류가 많은데, 위키배과의 'Test functions for optimization'페이지를 보면 대표적인 예를 확인할 수 있으며, [그림 24-1]과 같은 표로 정리되어 있습니다.

[그림 24-1]은 일부만 발췌한 것이며, 우리는 이 주 세 함수를 선택하여 실제로 미분해보려 합니다. 그러면 DeZero의 실력이 어느 정도인지 알 수 있겠죠. 우선 Sphere라는 간단한 함수에서 시작하겠습니다.

23.5 dezero 임포트하기

 이렇게 하여 dezero라는 패키지가 만들어졌습니다. 이제 이번 단계용 step23.py는 다음처럼 작성할 수 있습니다.

if '__file__' in globals():

    import os, sys

    sys.path.append(os.path.join(os.path.dirnmae(__file__), '..'))

    

import numpy as np

from dezero import Variable


x = Variable(np.array(1.0))

y = ( x + 3 ) ** 2

y.backward()


print(y)

print(x.grad)


variable(16.0)
variable(8.0)

우선 if '__file__' in globals(): 문장에서 __file__이라는 변수가 정의되어 있는지 확인합니다. python step23.py처럼 터미널에서 python 명령으로 실행한다면 __file__ 변수가 정의되어 있습니다. 이 경우 현재 파일(step23.py)이 위치한 디렉터리의 부모 디렉터리(..)를 모듈 검색 경로에 추가합니다. 이로써 파이썬 명령어를 어디에서 실행하든 dezero디렉터리의 파일들은 제대로 임포트할 ㅅ구 있게 됩니다. 예를 들어 명령줄에서 python steps/step23.py형태로 실행하든 cd steps; python step23.py 형태로 디렉터리를 옮겨 실행하든 상관없이 코드가 정상 작동합니다. 참고로 책 지면에서는 편의상 이 모듈 검색 경로 추가 코드는(매번 똑같이 반복되므로) 생략하고 보여줍니다.

검색 경로 추가 코드는 현재 개발 중인 dezero 디렉토리를 임포트하기 위해 일시적으로 사용하는 것입니다.(예를 들어 pip install dezero등의 명령으로) DeZero가 패키지로 설치된 경우라면 DeZero패키지가 파잇썬 검색 검토에 추가됩니다. 따라서 앞에서와 같이 경로를 수동으로 추가하는 일은 필요치 않게 됩니다. 또한 __file__ 변수는 파이썬 인터프리터의 인터렉티브 모드와 구글 콜랩(Google Colab)등의 환경에서 실행하느 경우에는 정의되어 있지 않습니다. 이점을 고려하여 (step 파일을 수정 없이 구글 콜랩에서도 동작하도록 하기 위해) 부모 디렉터리를 검색 경로에 추가될 때 if '__file__' in globals(): 라는 조건 검사 문장을 넣었습니다.


방금 보여준 코드가 step23.py의 전부입니다(생략한 코드는 없습니다). 이것으로 DeZero프레임워크의 원형이 와성되었습니다. 앞으로는 dezero디렉토리에 있는 파일(모듈)들을 확장하는 식으로 진행하겠습니다.



2022년 9월 2일 금요일

23.4 실제 __init__.py 파일

 이 책에서는 앞으로(23단계에서 32단계까지) DeZero코어 파일로 dezero/core_simple, py를 사용합니다. 그러다가 33단계부터는 dezero/core.py로 대체할 것입니다. 그래서 실제 dezero/__init__.py는 core_simple.py와 core.py중 하나를 선택해 임포트하도록 작성되어 있습니다.

is_simple_core = True


if is_simple_core:

    from dezero.core_simple import Variable

    from dezero.core_simple import Function

    from dezero.core_simple import using_config

    from dezero.core_simple import no_grad

    from dezero.core_simple import as_array

    from dezero.core_simple import as_variable

    from dezero.core_simple import setup_variable

    

else:

    from dezero.core import Variable

    from dezero.core import Function

    ...

    ...

    

setup_variable()

이와 같이 is_simple_core 플래그로 임포트할 대상을 선택합니다. is_simple_core가 True면 core_simple.py에서, False면 core.py에서 임포트가 이루어집니다.


코드를 실습해보려면 is_simple_core 플래그값을 적절히 수정하여 사용하기 바랍니다. 32단계까지는 True로 설정하고, 33단계부터는 False로 바꿔주면 됩니다.

23.3 연산자 오버로드

 이것으로 step22.py의 코드 대부분이 옮겨졌습니다. 이제부터는 오버로드한 연산자들을 dezero로 옮기겠습니다. 이를 위해 코어 파일인 dezero/core_simple.py에 다음 함수들을 추가합니다.

def setup_variable():

    Variable.__add__ = add

    Varaibel.__radd__ = add

    Variable.__mul__ = mul

    Variable.__rmul__ = mul

    Variable.__neg__ = neg

    Variable.__sub__ = sub

    Variable.__rsub__ = rsub

    Variable.__truediv__ = div

    Variable.__rtruediv__ = rdiv

    Variable.__pow__ = pow


setup_variable은 Variable의 연산자들을 오버로드해주는 함수입니다. 이 함수를 호출하면 Variable의 연산자들이 설정됩니다. 그렇다면 이 함수는 어디에서 호출하면 좋을까요? 바로 dezero/__init__.py 파일입니다.


__init__.py는 모듈을 임포트할 때 가장 먼저 실행되는 파일입니다. 우리의 경우 dezero패키지에 솏한 모듈을 임포트할 때 dezero/__init__.py의 코드가 첫 번째로 호출됩니다. 그래서 dezero/__init__.py에 다음 코드를 작성해 넣어야 합니다.

from dezero.core_simple import Variable

from dezero.core_simple import Function

from dezero.core_simple import using_config

from dezero.core_simple import no_grad

from dezero.core_simple import as_array

from dezero.core_simple import as_variable

from dezero.core_simple import setup_variable


setup_variable()

이와 같이 setup_variable 함수를 임포트해 호출하도록 합니다. 이렇게 함으로써 dezero 패키지를 이용하는 사용자는 반드시 연산자 오버로드가 이루어진 상태에서 Variable을 사용할 수 있습니다.

한편 __init__.py의 시작이 from dezero.core_simple import Variable인데, 이 문장이 실행됨으로써 dezero 패키지에 Variable 클래스를 곧바로 임포트할 수 있습니다. 옐르 들어 다음과 같이 이용할 수 있습니다.

# dezero를 이용하는 사용자의 코드


# from dezero.core_simple import Variable

from dezero import Variable


즉, 지금까지 from dezero.core_simple import Variable이라고 작성한 것을 from dezero import Variable처럼 짧게 줄일 수 있습니다. 마찬가지로 dezero/__init__.py의 임포트문들 덕분에 사용자는 나머지 Function이나 using_config 등도 '간소화된' 임포트를 이용할 수 있게 됩니다.



23.2 코어 클래스로 옮기기

 dezero 디렉터에 파일을 추가해보죠. 목표는 이전 단계의 step22.py 코드를 dezero/core_simple.py라는 코어(core,핵심)파일로 옮기는 것입니다. 파일 이름에 core를 붙인 이유는 지금까지 구현한 기능들이 DeZero의 핵심이라고 보기 때문입니다. 그리고 뒤에 가서는 최종 형태인 core.py로 교체할 계획이라서 당장은 core_simple.py로 시작하겠습니다.


그럼 step22.py에 정의된 다음 클래스들을 코어 파일로 복사해봅시다.

- Config

- Variable

- Function

- Add(Function)

- Mul(Function)

- Neg(Function)

- Sub(Function)

- Div(Function)

- Pow(Function)


여기에서 Add(Function)의 (Function)은 Function 클래스를 상속했다는 뜻입니다. 보다시피 Config, Variable, Function 클래스가 있고, Function 클래스를 상속한 함수(DeZero함수 클래스)가 여럿 개 있습니다. 이어서  step22.py에 정의한 파이썬 함수들도ㅓ 정리해야 합니다. 즉, 다음 함수들을 코어 파일로 옮길 것입니다.


- using_config

- no_grad

- as_array

- as_variable

- add

- mul

- neg

- sub

- rsub

- div

- rdiv

- pow


처음 두 함수는 DeZero설정 함수로, 역전파의 활성/비활성을 전환하는 데 상요합니다. 그다음의 as_array와 as_variable은 인수로 주어진 객체를 ndarray 또는 Variable로 변환하는 함수입니다. 나머지는 DeZero에서 사용하는 함수입니다. 자, 우선 step22.py에 담긴 클래스와 함수를 그대로 코어 파일에 복사합니다.


지금까지 Exp 클래스와 Square클래스 그리고 exp 함수와 square함수 등 DeZero에서 사용하는 구체적인 함수들도 구현했습니다. 하지만 이 코드들은 코어 파일에 넣지 않겠습니다. 이 코득들은 나중에 dezero/functions.py에 추가 할 겁니다.


이제 외부의 파이썬 파일에서 다음과 같이 dezero를 임포트할 수 있습니다.

import numpy as np

from dezero.core_simple import Variable


x = Variable(np.array(1.0))

print(x)


이와 같이 from dezero.core_simple import Variable 줄을 추가하여 Variable 클래스를 임포트할 수 있습니다. dezero.core_simple 처럼 파일 이름까지 명시한 점에 주의하세요. 바로 뒤에서 core_simple을 생략하고 from dezero import Variable로 사용할 수 있는 구조를 도입할 겁니다.


from ... import ... 구문을 사용하면 모듈 내의 클레스나 함수 등을 직접 임포트할 수 있습니다. 또한 import XXX is A라고 쓰면 XXX라는 모듈을 A라는 이름으로 임포트할 수 있습니다. 옐르 들어 import dezero.core_simple as dx 쓰면 dezero.core_simple모듈을 dz라는 이름으로 임포트합니다. 그런 다음 Variable 클래스를 사용하려면 dz.Variable이라고 쓰면 됩니다.

2022년 8월 31일 수요일

23.1 파일 구성

 파일 구성부터 확인하겠습니다. 지금까지는 step01.py, step02.py,.. 처럼 각 step파일 코드를 작성했습니다. 이제부터는 이 step파일 모두에서 DeZero를 이용할 수 있도록 dezero라는 공통의 디렉터리를 하나 만들겠습니다. 그래서 최종 파일 구성은 다음과 같습니다.


이와 같이 구성한 뒤 dezero 디렉터리에 모듈을 추가하는 것입니다. 그리하여 dezero라는 페키지가 만들어지는데, 이 패키지가 바로 우리가 만드는 프레임워크입니다. 앞으로 주로 이 dezero디렉터리에 있는 파일들에 코드를 추가할 것입니다.

STEP 23 패키지로 정리

 지금까지는 단계마다 내용전체를 파일 하나에 담았습니다. step01.py에서 시작하여 step22.py까지 도달했죠, 그런데 어느덧 우리의 DeZero는 크게 '성장'했습니다. 그래서 이번 단계에서는 지금까지의 성과를 재상요할 수 있도록 패키지로 정리할 생각입니다.

참고로 파이썬에서는 '도듈', '패키지', '라이브러리'라는 용어를 사용하는데, 보통 다음의 의미로 통용됩니다.


1) 모듈: 

모듈은 파이썬 파일입니다. 특히 다른 파이썬 프로그램에서 임포트(import)하여 사용하는 것을 가정하고 만들어진 파이썬 파일을 '모듈'이라고 합니다.

2) 패키지:

패키지는 여러 모듈을 묶은 것입니다. 패키지를 만들려면 먼저 디렉터리를 만들고 그 안에 모듈(파이썬 파일)을 추가합니다.

3) 라이브러리

라이브러리는 여러 패키지를 묶은 것입니다. 그래서 하나 이상의 디렉터리로 구성되죠. 때로는 패키지를 가리켜 '라이브러리'라고 부르기도 합니다.

22.4 거듭제곱

 거듭제곱은 y = x **  c 형태로 표현됩니다. 이때 x를 밑이라고 하고 c를 지수라 합니다. 거듭제곱의 미분은 미분 공식으로부터 dy/dx = cx ** ( c - 1)이 됩니다. dy/dc의 값도 구할 수는 있지만 실정에서는 거의 사용되지 않으니 이 책에서는 밑이 x인 경우만 미분해보겠습니다. 즉, 지수 c는 상수로 취급하여 따로 미분을 계산하지 않기로 합니다. 다음은 이를 구현한 코드입니다.

class Pow(Function):
  def __init__(selfc):
    self.c = c

  def forward(selfx):
    y = y ** self.c
    return y

  
  def backward(Selfgy):
    x = self.inputs[0].data
    c = self.c
    gx = c * x ** ( c - 1 )  * gy
    return gx


def pow(xc):
  return Pow(c)(x)

Variable.__pow__ = pow

코들르 보면 Pow클래스를 초기화할 때 지수 c를 제공할 수 있습니다. 그리고 순전파 메서드인 forward(x)는 밑에 해당하는 x만(즉, 하나의 항만)받게 합니다. 그런 다음 특수 메서드인 __pow__에 함수 pow를 할당합니다. 이제 ** 연산자를 사용하여 거듭제곱을 계산할 수 있습니다.

x = Variable(np.array(2.0))
y = x ** 3

print(y)

이상으로 목표한 연산자를 모두 추가했습니다. 이번 단곈느 다소 단조로운 작업의 연속이었지만 그 덕분에 DeZero의 유용성은 크게 향상됐습니다. 사칙연산 연산자들을 자유롭게 계산에 활용할 수 있게 된거죠. 거듭제ㅐ곱도 가능하기 때문에 제법 고급 계산까지 표현할 수 있답니다.

22.3 나눗셈

 나눗셈의 미분은 y = x0/x1 일때 dy/dx0 = 1/x1, dy/dx1 = -x0/(x1) ** 2 입니다. 코드로는 다음처럼 구현할 수 있습니다.

from os.path import getsize
class Div(Function):
  def forward(selfx0x1):
    y = x0 / x1
    return y

  def backward(selfgy):
    x0, x1 = self.inputs[0].data, self.inputs[1].data
    gx0 = gy / x1
    gx1 = gy * (-x0 / x1 ** 2)
    return gx0, gx1

def div(x0x1):
  x1 = as_array(x1)
  return Div()(x0, x1)

def rdiv(x0x1):
  x1 = as_array(x1)
  return Div()(x1, x0)    # x0와 x1의 순서를 바꾼다.

Variable.__truediv__ = div
Variable.__rtruediv__ = rdiv

나눗셈도 뺄셈과 마찬가지로 좌/우항 중 어느, 것에 적용할지에 따라 적용되는 함수가 다릅니다. 그되에는 특별히 어려운 점은 없을 것입니다.

그럼 마지막으로 '거듭제곱'을 살펴보겠습니다.

22.2 뺄셈

 뺄셈의 미분은 y = x0 - x1일 때 dy/dx0 = 1, dy/dx1 = -1 입니다. 따라서 역전파는 상류에서 전해지는 미분값에 1을 곱한 값이 x0의 미분 결과가 되며, -1을 곱한 값이 x1의 미분 결과가  됩ㄴㅣ다. 코드로는 다음처럼 구현할 수 있습니다.

class Sub(Function):
  def forward(selfx0x1):
    y = x0 - x1
    return y

  def backward(selfgy):
    return gy, -gy

def sub(x0x1):
  x1 = as_array(x1)
  return Sub()(x0, x1)

Variable.__sub__ = sub

이제 x0와 x1이 Variable 인스턴스라면 y0 = x0 - x1 계산을 수행할 수 있습니다. 그러나 x0가 Variable 인스턴스가 아닌 경우, 예컨데 y = 2.0 - x 같은 코드는 제대로 처리할 수 없습니다. x의 __rsub__ 메서드가 호출되어 인수가 [그림 22-1]형태로 전달되기 때문이죠.


[그림 22-1]과 같이 __rsub__(self, other)가 호출될 땐느 우항인 x가 인수 self에 전달됩니다. 따라서 다음처럼 구현해야 합니다.

def rsub(x0x1):
  x1 = as_array(x1)
  return Sub()(x1, x0)    # x0 와 x1의 순서를 바꾼다.

Variable.__rsub__ = rsub

보다시피 함수 rsub(x0, x1)을 정의하고 인수의 순서를 바꿔서 Sub()(x1, x0)를 호출하게 합니다. 그런 다음 특수 메서드인 __rsub__에 함수 rsub를 할당합니다.


덧셈과 곰셈은 좌항과 우항의 순석를 바꿔도 결과가 같기 때문에 둘을 구별할 필요가 없었ㅅ브니다. 하지만 뺄셈에서는 좌우를 구별해야 합니다. (x0 - x1과 x1 - x0의 값은 다릅니다). 따라서 우항을 대상으로 했을 때 적용할 함수인 rsub(x0, x1)을 별도로 준비해야 합니다.


이상으로 뺄셈도 할 수 있게 되었습니다. 이제 다음 코드가 잘 작동합니다.

x = Variable(np.array(2.0))
y1 = 2.0 - x
y2 = x - 1.0
print(y1)
print(y2)

2022년 8월 28일 일요일

22.1 음수(부호 변환)

 음수의 미분은 y = -x 일때 dy/dx = -1입니다. 따라서 역전파는 상류(출력 쪽)에서 전해지는 미분에 -1을 곱하여 하류로 흘려보내주면 됩니다. 그러면 다음과 같이 구현할 수 있습니다.


이와 같이 Neg 클래스를 구현한 다음, 파이썬 함수로 사용할 수 있도록 neg 함수도 구현합니다. 그리고 특수 메서드인 __neg__에 neg를 대입하면 완성입니다. 이제 다음 코드를 실행할 수 있습니다.



다음은 '뺄셈' 차례입니다.



2022년 8월 27일 토요일

STEP 22 연산자 오버로드(3)

 이전 단계에서는 DeZero가 *와 +연산자를 지원하도록 확장했는데, 연산자는 이밖에도 많습니다. 그래서 이번 단계에서는 [표 22-1]의 연산자들을 추가하겠습니다.


[표 22-1]의 첫 번째 메서드인 __neg__(self)는 양수를 음수로, 혹은 음수를 양수로 바꿔주는 부호 변환 연산자입니다. 또한 다른 연산자들과 달리 항이 하나뿐인 '단항 연산자'입니다. 그래서 특수 메서드인 인수도 하나뿐이죠, 나머지 연산자들은 차례로 뺄셈, 나눗셈, 거듭제곱으로, 모두 이항 연산자입니다( a - b, a / b등), 따라서 적용 대상이 우항이냐 좌항이냐에 따라 2개의 특수 메서드 중 하나가 선별되어 호출됩니다.단, 거듭제곱은 x ** 3 처럼 좌항이 Variable 인스턴스이고 우항이 상수(2, 3 등의 int)인 경우만을 고려하겠습니다.

파이썬 연산자는 [표22-1]외에도 몇 가지 더 있습니다. 옐르 들어 a // b 와 a % b 같은 연산자도 있고, a +=1 과 a -= 2같은 연산자도 있습니다. 이 책에서는 자주 사용하는 연산자만 선별하여 구현할 테니, 다른 연산자는 독자 여러분이 필요하면 추가하기 바랍니다. 한 가지 더, 이번 단계는 단조로운 작업이기 때문에 생략해도 무방합니다.


그러면 작업을 시작해 볼까요? 우선 복습도 할 겸 새로운 연산자를 추가하는 순서를 살펴보죠,

1. Function 클래스를 상속하여 원하는 함수 클래스를 구현합니다(예: Mul클래스).

2. 파이썬 함수로 사용할 수 있도록 합니다(예: mul 함수).

3. Variable 클래스의 연산자를 오버로드합니다(예: Variable.__mul__ = mul).

이번 단계에서도 똑같은 과정을 거쳐 새로운 연ㅅ간자들을 추가할 것입니다. '부호 변환'부터 시작해 보죠.

21.4 문제점 2: 좌항이 ndarray인스턴스인 경우

 남은 문제는 ndarray 인스턴스가 좌항이고 Variable 인스턴스가 우항인 경우입니다. 예를 들어 다음과 같은 코드입니다.

x = Variable(np.array([1.0]))
y = np.array([2.0] * x)

이 예에서 좌항은 ndarray 인스턴스이고 우항은 Variable 인스턴스입니다. 이렇게 되면 좌항인  ndarray 인스턴스의 __add__ 메서드가 호출됩니다. 하지만 우리는 우항인 Variable 인스턴스의 __radd__ 메서드가 호출되길 원합니다. 그러면서 '연산자 우선순위'를 지정해야 합니다. 구체적으로 Variable 인스턴스의 속성에 __array_priority__를 추가하고 그 값을 큰 정수로 설정해야 합니다. 다음처럼 마리죠.

class Variable:
  __array_priority__ = 200

이렇게 하면 Variable 인스턴스의 연산자 우선순위를 ndarray 인스턴스의 연산자 우선순위보다 높일 수 있습니다. 그 결과 좌항이 ndarray 인스턴스라 해도 우한인 Variable 인스턴스의 연산자 메서드가 우선적으로 호출됩니다.

이상이 연산자 오버로드 시 조심해야 할 핵심입니다. 마침내 DeZero는 *와 + 연산자를 서로 다른 타입과 섞어 사용할 수 있게 되었습니다. 이어서 다음 단계에서는 /와 - 같은 다른 연사자를 추가하겠습니다.

21.3 문제점1: 첫 번째 인수가 flaot나 int인 경우

 현재의 DeZero는 x * 2.0이라는 코드를 제대로 실행할 수 있습니다(x는 Variable 인스턴스). 하지만 2.0 * x를 실행하면 오류가 납니다. 어떤 오류가 나는지 볼까요?

y = 2.0 * x

원인은 2.0 * x를 실행했을 때 오류가 발생하는 과정을 보면 알수 있습니다. 2.0 * x 는 다음 순서로 처리됩니다.

1. 연산자 왼쪽에 있는 2.0의 __mul__메서드를 호출하려 시도한다.

2. 하지만 2.0은  float 타입이므로 __mul__ 메서드는 구현되어 있지 않다.

3. 다음은 * 연산자 오른쪽에 있는 x의 특수 메서드를 호출하려 시도한다.

4. x가 오른쪽에 있기 때문에 (__mul__대신) __mul__ 메서드를 호출하려 시도한다.

5. 하지만 Variable 인스턴스에는 __mul__메서드가 구현되어 있지 않다.

이상이 오류 발생 과정입니다. 핵심은 * 같은 이항 연산자의 경우 피연산자(항)의 위치에 따라 호출되는 특수 메서드가 다르다는 것입니다. 곱셈의 경우 피연사자가 촤항이면 __mul__메서드가 호출되고, 우항이면 __rmul__메서드가 호출됩니다.

따라서 이번 문제는 __rmul__메서드를 구현하면 해결됩니다. 이때 __rmul__메서드의 인수는 [그림 21-1]처럼 전달됩니다.

Variable.__add__ = add
Variable._radd__ =  add
Variable.__mul__ = mul
Variable.__rmul__ = mul

[그림 21-1]과 같이 __rmul__(self, other)의 인수 중 self는 자신인 x에 대응하고, other는  다른 쪽 항인 2.0에 대응합니다. 그런데 곱셈에서는 좌항과 우항을 바꿔도 결과가 같기 때문에 둘을 구별할 필요가 없습니다(2.0 * x 와 x * 2.0의 결과가 똑같죠). 덧셈도 마찬가지이므로 + 와 * 의 특수 메서드는 다음처럼 설정하면 됩니다.


이제 float와 int를 자류롭게 계산할 수 있습니다. 연습삼아 실제로 해보죠

x = Variable(np.array(2.0))
y = 3.0 * x + 1.0
print(y)

이제 Variable 인스턴스와 float, int 를 함께 사용할 수 있습니다. 이어서 나머지 문제 하나를 마저 해결해보죠.




21.2 float, int와 함께 사용하기

 이어서 파이썬의 float와 int, 그리고 np.float64와 같은 타입과도 함께 사용할 수 있도록 하겠습니다. x가 Variable 인스턴스일 때 x + 3.0 같은 코드를 실행할 수 있도록 하러면 어떻게 해야 할까요? 한가지 방법으로 add 함수에 다음의 음영부분의 코드를 추가하는 방법이 떠오르는군요.

def add(x0x1):
  x1 = as_array(x1)
  return Add()(x0, x1)

여기에서는 as_array 함술르 사용했씁니다. 9단계에서 구현한 함수죠. as_array를 사용하면 x1이 float나 int인 경우 ndarray인스턴스로 변환됩니다. 그리고 ndarray 인스턴스는 (이후에)Function 클래스에서 Variable 인스턴스로 변환됩니다. 이것으로 다음과 같은 코드르 작성할 수 있게 됐습니다.

x = Variable(np.array(2.0))
y = x + 3.0
#y = add(x, 3.0)
print(y)

이와같이 float와 Variable인스턴스를 조합한 계산이 가능해졌습니다. 여기서는 add함수만 대표로 수정해봤지만, mul과 같은 다른 함수들도 같은 방식으로 수정할 수 있습니다. 다 수정하고 나면 +나 *로 Variable 인스턴스, float, int를 조합하여 계산할 수 있습니다. 그런데 실은 지금의 방식에는 두가지 문제가 남아 있습니다.

21.1 ndarray와 함께 사용하기

 우선 Variable응 ndarray 인스턴스와 함께 사용할 수 있게 하겠습니다. 전략은 간단합니다. 예를 들어 a가 Variable 인스턴스일 때 a *  np.array(2.0)이라는 코드를 만나면 ndarray인스턴스를 자동으로 Variable 인스턴스로 변환하는 것입니다. 즉, Variable(np.array(2.0))으로 변환해버리면 그다음 계산은 지금까지와 같습니다.

이를 위한 사전 준비로 as_variable이라는 편의 함수를 준비합니다. 인수로 주어진 객체를 Variable 인스턴스로 변환해부는 함수입니다. 구현은 다음과 같습니다.

def as_variable(obj):
  if isinstance(obj, Variable):
    return obj
  return Variable(obj)

이 함수는 인수 obj가 Variable 인스턴스 또는 ndarray 인스턴스라고 가정합니다.  obj가 Variable 인스턴스면 아무것도 손보지 않고 그대로 반환하고, 그렇지 않으면 Variable인스턴스로 변환하여 반환합니다.

그럼 Function클래스의 __call__ 메서드가 as_variable 함수를 이용하도록 다음 음영부분의 코드를 추가합니다.

import weakref    
class Function(object):
  def __call__(self, *inputs):
    inputs = [as_variable(x) for x in inputs]
    
    xs = [x.data for x in inputs]
    ys = self.forward(*xs)  
    if not isinstance(ys, tuple):   
      ys = (ys,)
    outputs = [Variable(as_array(y)) for y in ys]

    self.generation = max([x.generation for x in inputs]) 

    for output in outputs:
      output.set_creator(self)  
    self.inputs = inputs  #
    self.outputs = [weakref.ref(output) for output in outputs]  
   
    return outputs if len(outputs) > 1 else outputs[0]

이와 같이 인수 inputs에 담긴 각각의 원소 x를 Variable 인스턴스로 변환합니다. 따라서 ndarray 인스턴스 주어지면 Variable 인스턴스로 변환됩니다. 그러면 이후의 처리는 모든 변수가 Variable 인스턴스인 상태로 진행됩니다.

DeZero에서 사용하는 모든 함수(연산)는 Function클래스를 상속하므로 실제 연산은 Function클래스의 __call__ 메서드에서 이루어집니다. 따라서 이 __call__ 메서드에 가한 수정은 DeZero에서 사용하는 모든 함수에 적용됩니다.


그러면 새로운 DeZeror를 사용하여 계산을 해봅시다.

x = Variable(np.array(2.0))
y = add(x, np.array(3.0))
#y = x + np.array(3.0)
print(y)
variable(5.0)

y = x + np.array(3.0)이라는 코드를 실행했고, 출력을 보면 제대로 작동함을 알 수 있습니다. ndarray 인스턴스가 Variable 인스턴스로 자동 변환된 결과죠, 이렇게 ndarray와 Variable을 함께 사용할 수 있게 되었습니다.


2022년 8월 26일 금요일

STEP 21 연산자 오버로드(2)

 DeZero가 점점 편리해지고 있습니다. 이제 우리는 Variable 인스턴스 a 와 b가 있을 때 a * b 혹은  a + b 같은 코드도 작성할 수 있지요. 하지만 안타깝게도 a * np.array(2.0) 처럼 ndarray 인스턴스와 수치 데이터와도 함께 사용할 수 있게 되면 DeZero가 더욱 편리할 텐데 말이죠. 그래서 이번 단계에서는 Variable 인스턴스와 ndarray인스턴스, 심지어 int나 float등도 함께 사용할 수 있도록 해보겠습니다.

20.2 연산자 오버로드

먼저 곱셈 연산자 *를 오버로드하겠습니다. 곱셈의 특수 메서드는 __mul__(self, other) 입니다(인수 self와 other에 대해서는 조금 뒤에 설명합니다). __mul__메서드를 정의(구현)하면 * 연산자를 사용할 때 __mul__ 메서드가 호출됩니다. 시험 삼아 Variable 클래스의 __mul__ 메서드를 다음과 같이 구현해보겠습니다.


class Variable:
  def __init__(selfdataname=None):
    if data is not None:
      if not isinstance(data, np.ndarray):
        raise TypeError('{} is not supported'format(type(data)))
    
    self.data = data
    self.name = name
    self.grad = None
    self.creator = None
    self.generation = 0
    
  def set_creator(selffunc):
    self.creator = func
    self.generation = func.generation + 1     


  def backward(selfretain_grad=False):
    if self.grad is None:
      self.grad = np.ones_like(self.data)


    funcs = []
    seen_set = set()

    def add_func(f):
      if f not in seen_set:
        funcs.append(f)
        seen_set.add(f)
        funcs.sort(key=lambda x: x.generation)
    
    add_func(self.creator)

    while funcs:
      f = funcs.pop()   

      # 수정전 gys = [output.grad for output in f.outputs]  
      gys = [output().grad for output in f.outputs]  #
      gxs = f.backward(*gys)   
      if not isinstance(gxs, tuple):  
        gxs = (gxs,)
      
      for x, gx in zip(f.inputs, gxs):  
        if x.grad is None:
          x.grad = gx
        else:
          x.grad = x.grad + gx

        if x.creator is not None:
          add_func(x.creator)
      
      if not retain_grad:
        for y in f.outputs:
          y().grad = None   # y는 약한 참조(weakref)

  def cleargrad(self):
    self.grad = None

  
  @property
  def shape(self):
    return self.data.shape

  @property
  def ndim(self):
    return self.data.ndim
  
  @property
  def size(self):
    return self.data.size

  @property
  def dtype(self):
    return self.data.dtype


  def __len__(self):
    return len(self.data)

  def __repr__(self):
    if self.data is None:
      return 'variable(None)'
    
    p = str(self.data).replace('\n''\n' + ' ' * 9)
    return 'variable('+ p + ')'

  def __mul__(selfother):
    return mul(self, other)


지금까지 구현한 Variable 클래스에 이 __mul__ 메서드를 추가합니다. 이제부터 *를 사용하면 __ㅡmul__ 메서드 대신 불리고, 다시 그 안의 mul 함수가 불리게 됩니다. 시험해볼까요?


a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
y = a * b
print(y)

variable(6.0)



보다시피 y = a * b라는 코드를 문제없이 실행할 수 있습니다. a * b가 실행될 때 인스턴스 a의 __mul__(self, other) 메서드가 호출됩니다. 이때 [그림 20-2]와 같이 연산자 * 왼쪽의 a가 인수 self에 전달되고, 오른쪽 b가 other에 전달됩니다.


앞의 예에서 a * b가 실행되면 먼저 인스턴스 a의 특수 메서드인 __mul__ 호출됩니다. 그런데 만약 a에 __mul__ 메서드가 구현되어 있지 않으면 인스턴스 b의 * 연산자 특수 메서드가 호출됩니다. 이 경우 b는 * 연산자의 오른쪽에 위치하기 때문에 __mul__이 아닌 __mul__이라는 특수 메서드가 호출됩니다(메서드 이름 앞에 오른쪽(right)을 뜻하는 'r'이 붙어 있습니다).


이상으로 * 연산자를 오버로드해봤습니다. 정확히는 Variable 클래스의 __mul__ 메서드를 구현했습니다. 그런데 이와 똑같은 작업을 다음 코드처럼 간단히 처리하는 방법도 있습니다.

class Variable:
  def __init__(selfdataname=None):
    if data is not None:
      if not isinstance(data, np.ndarray):
        raise TypeError('{} is not supported'format(type(data)))
    
    self.data = data
    self.name = name
    self.grad = None
    self.creator = None
    self.generation = 0
    
  def set_creator(selffunc):
    self.creator = func
    self.generation = func.generation + 1     


  def backward(selfretain_grad=False):
    if self.grad is None:
      self.grad = np.ones_like(self.data)


    funcs = []
    seen_set = set()

    def add_func(f):
      if f not in seen_set:
        funcs.append(f)
        seen_set.add(f)
        funcs.sort(key=lambda x: x.generation)
    
    add_func(self.creator)

    while funcs:
      f = funcs.pop()   

      # 수정전 gys = [output.grad for output in f.outputs]  
      gys = [output().grad for output in f.outputs]  #
      gxs = f.backward(*gys)   
      if not isinstance(gxs, tuple):  
        gxs = (gxs,)
      
      for x, gx in zip(f.inputs, gxs):  
        if x.grad is None:
          x.grad = gx
        else:
          x.grad = x.grad + gx

        if x.creator is not None:
          add_func(x.creator)
      
      if not retain_grad:
        for y in f.outputs:
          y().grad = None   # y는 약한 참조(weakref)

  def cleargrad(self):
    self.grad = None

  
  @property
  def shape(self):
    return self.data.shape

  @property
  def ndim(self):
    return self.data.ndim
  
  @property
  def size(self):
    return self.data.size

  @property
  def dtype(self):
    return self.data.dtype


  def __len__(self):
    return len(self.data)

  def __repr__(self):
    if self.data is None:
      return 'variable(None)'
    
    p = str(self.data).replace('\n''\n' + ' ' * 9)
    return 'variable('+ p + ')'


  Variable.__mul__ = mul
  Variable.__add__ = add


Variable 클래스를 정의한 후 Variable.__mul__ = mul이라고 작성하면 끝! 파이썬에서는 함수도 객체이므로 이와 같이 함수 자체를 할당할 수 있습니다. 이렇게 하면 Variagble 인스턴스의 __mul__ 메서드를 호출할 때 mul 함수가 불립니다.

앞의 코드에서 + 연산자의 특수 메서드인 __add__도 설정했습니다. + 연샂자도 함께 오버로드한 것이죠. 그럼 + 와 * 를 모두 사용하여 계산을 해 보겠습니다.


a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
c = Variable(np.array(1.0))

y = add(mul(a, b), c)
# y = a * b + c
y.backward()

print(y)
print(a.grad)
print(b.grad)

variable(7.0) 2.0 3.0


보다시피 y = a * b + c 형태로 코등하는게 가능해졌습니다. 계산 시 + 와  * 를 자유롭게 사용할 수 있게 된 것이죠, / 와 - 같은 다른 연산자도 같은 방식으로 구현할 수 있습니다. 그럼 다음 단계에서도 계속 연산자 오버로드를 살펴보겠습니다.