6.1.4 모멘텀

 

v라는 새로운 변수가 있는데 이는 물리에서의 velocity에 해당한다. 위의 식 (v ← ...)은 기울기 방향으로 힘을 받아 물체가 가속되는 물리 법칙을 나타낸다. 모멘텀은 그림과 같이 공이 그릇의 바닥을 구르는 듯한 움직임을 보여준다. 

#솔직히 말해서, 모멘텀 (운동량)과 속도에 대한 물리식들을 살펴보았지만 연관성을 잘 모르겠다.

αv항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다. 

 

class Momentum:

    """모멘텀 SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

momentum이 α 에 해당한다. v는 초기화 때는 아무 값도 담지 않고, 대신 update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장한다. 

##for key, val in params.items():                                
                self.v[key] = np.zeros_like(val) 이 부분에 대한 설명으로 새로운 게시글 링크를 남긴다.

https://codingneedsinsanity.tistory.com/30

 

 

모멘텀에 의한 최적화 갱신 경로

모멘텀의 갱신 경로는 공이 구르듯 움직인다. 굉장히 직관적인 표현을 쓰자면 'SGD에 비해서 지그재그 정도가 적다' 

정리하자면

  • x축의 힘은 아주 작지만 방향은 변하지 않아 한 방향으로 일정하게 가속
  • y축의 힘은 크지만 위아래로 번갈아 가며 상충하여 속도는 안정적이지 않음
  • SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 적음

 

6.1.5 AdaGrad

 

신경망 학습에서는 학습률(η)값이 중요하다. 값이 작으면 학습 시간이 너무 길어지고, 너무 크면 발산하여 학습이 제대로 이루어지지 않는다.

 

이 학습률을 정하는 효과적 기술로 학습률 감소(learning rate decay)가 있다. 학습을 진행하면서 학습률 자체를 점차 줄여나가는 방법이다. 학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 학습률 값을 일괄적으로 낮추는 것이다. 이를 더욱 발전시킨 것이 AdaGrad이다. AdaGrad는 '각각의 ' 매개변수에 '맞춤형'값을 만들어준다. 

AdaGrad는 개별 매개변수에 적응적으로 학습률을 조정하면서 학습을 진행한다.

 

h는 기존 기울기값을 제곱하여 계속 더해준다. (기호는 행렬의 원소별 곱셈을 의미) 그리고 매개변수를 갱신할 때 sqrt(h)의 역수값을 곱해 학습률을 조정한다. 매개변수의 원소 중에서 크게 갱신된 원소의 학습률이 낮아진다는 뜻인데, 학습률 감소가 매개변수의 원소마다 다르게 적용됨을 뜻한다. 

 

##AdaGrad는 학습을 진행할수록 갱신 강도가 약해져, 어느 순간 갱신량이 0이 되어 전혀 갱신되지 않는다. 이 문제를 해결하기 위해서 RMSProp이라는 방법이 있는데, 이는 먼 과거의 기울기는 잊고, 새로운 기울기 정보를 크게 반영하는 식이다. 이를 지수이동평균 (Exponential Moving Average, EMA)이라 하여, 과거 기울기 반영 규모를 기하급수적으로 감소시킨다.

 

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key]=np.zeros_like(val)
        
        for key in params.keys():
            self.h[key] += grads[key]*grads[key]
            params[key] -= self.lr*grads[key]/(np.sqrt(self.h[key])+1e-7)

** 마지막에 1e-7이 들어가는 이유는 divide by zero를 막기 위함이다. 

처음에 설명한 것처럼, 초기에는 크게 움직이고 점점 갱신 정도가 작아지는 것을 확인할 수 있다.

 

 

6.1.6 Adam

 

Adam은 위 모멘텀과 AdaGrad의 두 기법을 융합한 것이다. 이론은 복잡하지만 직관적으로는 이 둘을 융합한 모습이다. 매개변수 공간을 효율적으로 탐색해주고, 하이퍼파라미터의 '편향 보정'이 진행된다. 책에 서술된대로 여기서 설명을 마치고 자세한 내용은 다음 링크에 남긴다.

https://medium.com/@nishantnikhil/adam-optimizer-notes-ddac4fd7218

 

Everything you need to know about Adam Optimizer

Paper : Adam: A Method for Stochastic Optimization

medium.com

 

6.1.7 어느 갱신 방법을 이용할 것인가?

 

 

그림만 봐서는 AdaGrad가 가장 좋은 것 같지만, 결론만 이야기하면 아니다. 각 방법이 각각 장단이 있어 잘 푸는 문제가 있고 아닌 문제가 있다. 책에서는 요즘에는 많은 사람들이 Adam을 사용한다고 한다. (2017년 기준) (위 교재에서는 Adam과 SGD를 사용한다.)

 

 

6.1.8 MNIST 데이터셋으로 본 갱신 방법 비교

 

위 실험에서는 각 층이 100개의 뉴런으로 구성된 5층 신경망에서 ReLU를 활성화 함수로 사용해 측정했다. 

일반적으로 SGD보다 다른 세 기법이 빠르게 학습하고, 때로는 최종 정확도 역시 높게 나타난다고 한다.

 

for key, val in params.items():                                 
    self.v[key] = np.zeros_like(val) 

이 게시글은 이 코드를 이해하기 위해서 작성되었다.

 

1. 반복문과 딕셔너리의 이해

 

먼저 NumPy를 사용하지 않은 코드를 먼저 살펴보자.

dic1 = {'a':0, 'b': 1, 'c':2}
dic2 = {}

for key, val in dic1.items():
    dic2[key] = val+1

심플하게 생각해보면, dic1의 value에 1을 더해 dic2에 저장한다는 느낌이다. 실제로 dic2를 찍어보면 

{'a': 1, 'b': 2, 'c': 3} 과 같은 결과를 얻을 수 있다. 

 

그렇다면 대체 items() 메소드는 무슨 역할을 하는걸까. 

책 '점프 투 파이썬' 에 따르면 

items 함수는 Key와 Value의 쌍을 튜플로 묶은 값을 dict_items 객체로 돌려준다. dict_values 객체와 dict_items 객체 역시 dict_keys 객체와 마찬가지로 리스트를 사용하는 것과 동일하게 사용할 수 있다.

라고 한다. 

 

실행 후에 key와 val을 찍어보면 

key
Out[30]: 'c'

val
Out[31]: 2

다음과 같은 결과를 얻을 수 있다. 이 두 값은 dic1 딕셔너리 변수의 마지막 key와 value 값이다. 

과연 Python은 어떻게 이 반복문을 작동시키는 것일까?

 

디버그를 통해서 확인해보면 key와 val에는 순서대로 (a, b, c) (1, 2, 3) 이 저장된 것을 확인할 수 있다. 

 

즉 Python의 반복문에서는 변수를 2개 이상 받을 수 있으며, 이를 차례대로 넘겨줌을 알 수 있다. 

 

 

2. np.zeros_like

 

arr1 = np.array([[1,2],[3,4]])

먼저 다음과 같은 2차원 NumPy 행렬을 만들었다.

arr2 = np.zeros_like(arr1)

arr2
Out[53]: 
array([[0, 0],
       [0, 0]])

다음과 같이 np.zeros_like() 를 실행하였다. 그 결과 모양과 차원이 동일하지만, 내부 값이 0인 행렬이 나오는 것을 확인할 수 있다.

 

 

 

마지막으로 원본 코드를 다시 살펴보자

for key, val in params.items():                                 
    self.v[key] = np.zeros_like(val) 

  즉 params라는 dict변수에는 특정한 NumPy행렬이 저장되어 있을 것이며, 형태는 유지시키고 값은 0으로 하여 v라는 dict변수에 key값을 그대로 하여 복사한 것이다. 

'머신 러닝 및 파이썬 > Numpy 공부' 카테고리의 다른 글

행렬 곱셈, 내적  (0) 2020.03.04
Numpy의 Shape  (0) 2020.03.04
Numpy의 log  (0) 2020.02.27
Numpy의 Dimension  (0) 2020.02.27

6.1 매개변수 갱신

 

신경망 학습의 목적은 '손실 함수의 값을 최대한 낮추는 매개변수를 찾는 것'이다. 이를 위해서 매개변수의 기울기를 이용하였고, 갱신한 매개변수 값으로 다시 갱신하고, 갱신하는 방법을 취했다. 이를 SGD, 확률적 경사 하강법이라고 하였다. 

 

 

6.1.2 확률적 경사 하강법(SGD)

 

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

 

6.1.3 SGD의 단점

 

SGD는 단순하고 구현도 쉽지만 문제가 있다. 다음 예시를 보자

기울기는 다음과 같다.

위 식이 최솟값이 되는 장소는 (x, y) = (0, 0)인데, 기울기의 대부분은 (0, 0) 방향을 가리키고 있지 않다.

초기값 (x, y) = (-7.0, 2.0)에서 SGD를 적용하면 다음 그림의 경로를 따른다.

지그재그로 한참을 가게되는데, 굉장히 비효율적임을 볼 수 있다.

 

책에서는 이러한 단점을 해결하기 위한 방법으로 모멘텀, AdaGrad, Adam을 소개한다.

5.6.1 Affine 계층

 

신경망의 순전파에서 가중치 신호의 총합을 계산하기 위해서 행렬의 내적을 사용하였다. 이를 Computational Graph로 표현한다면

다음 과 같다. 지금까지의 그래프는 노드 사이에 '스칼라값'이 흘렀는데 반해, 이 예에서는 '행렬'이 흐르고 있다.

 

역전파에 대해서 구해보면 

다음과 같은데, 행렬 혹은 벡터에 대한 미분에 대한 증명으로

https://nbviewer.jupyter.org/github/metamath1/ml-simple-works/blob/master/fitting/matrix-derivative.ipynb

 

Jupyter Notebook Viewer

$$ \frac{\partial \, \mathbf{Y}}{\partial \, \mathbf{Z}} = \frac{\partial}{\partial \, \mathbf{Z}} \otimes \mathbf{Y} = \begin{bmatrix} \dfrac{\partial \, Y_1}{\partial \, Z_1} \\ \dfrac{\partial \, Y_1}{\partial \, Z_2} \\ \vdots \\ \dfrac{\partial \, Y_1

nbviewer.jupyter.org

다음 게시글을 참고하며 정리해보았다. 대충 증명에 대한 것이므로, 읽지 않아도 무관하다.

 

5.6.2 배치용 Affine 계층

 

위 예시에서는 입력 데이터로 X하나만 고려하였다. 그렇다면 데이터 N개를 묶어 순전파 하는 경우는 어떻게 될까?

**3번 방향의 역전파를 보면 행렬 -> 벡터 가 된 것을 볼 수 있다. 이는 값을 합쳐주면 된다.

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None
        
    def forward(self, x):
        self.x = x
        out = np.dot(x, self.W)+self.b
        
        return out
    
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

 

5.6.3 Softmax-with-Loss 계층

 

소프트맥스 함수는 입력 값을 정규화하여 출력한다.

이 책에서는 손글씨 숫자 인식(Mnist)을 예시로 들고 있다.

Softmax 계층은 입력 값을 정규화 (출력의 합이 1이 되도록 변형)하여 출력한다. 

 

Softmax-with-Loss는 손실 함수인 교차 엔트로피 오차까지 포함하는 계층이다. Computational Graph와 간소화된 버전 두 개를 살펴보자

Computational Graph of the Softmax-with-Loss layer
간소화된 버전

여기에서는 3클래스 분류를 가정하고 이전 계층에서 3개의 입력(점수)를 받는다.

  • Softmax 계층은 입력 (a1, a2, a3)를 정규화하여 (y1, y2, y3)를 출력한다.
  • Cross Entropy Error 계층은 Softmax의 출력 (y1, y2, y3)와 정답 레이블 (t1, t2, t3)를 받고, 손실 L을 출력한다.

여기서 주목해야 할 것은 역전파의 결과이다. Softmax 계층의 역전파는 (y1 - t1, y2 - t2, y3 - t3)라는 말끔한 결과를 내놓고 있다. 신경망의 역전파에서 이 차이는 오차가 앞 계층에 전해진다는 것이다. 이는 신경망 학습의 중요한 성질이다.

 

신경망 학습의 목적은 신경망의 출력이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것이다. 그렇기 때문에 신경망의 출력과 정답 레이블의 오차를 효율적으로 앞 계층에 전달하는 것이다. (교차 엔트로피 오차 함수가 다음과 같은 말끔한 결과를 내는 이유는 그렇게 설계되었기 때문이다.)

 

이해가 조금 되지 않는데 예시를 하나 보자

Softmax 계층이 (0.3, 0.2, 0.5)를 출력했으며, 정답 레이블이 (0, 1, 0)이라고 가정하자. 그렇다면 역전파는 (0.3, -0.8, 0.5)이다. 결과적으로 Softmax 계층의 앞 계층들은 이런 오차로부터 큰 깨달음을 얻게 된다. (학습이 많이 필요하다는 뜻)

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None #softmax 출력
        self.t = None #정답 레이블
        
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss
    
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        
        return dx

 

[벡터와 벡터의 곱셈]

 

먼저 1차원인 Vector 끼리의 곱셈 먼저 알아보자.

내적 (inner product)에 대해서 알아 볼 것인데, 내적은 사실 여러가지 표현법이 있다. 

번역과 표기 덕에 책을 공부하면서 헷갈렸던 부분이다....

 

<x,y>와 같은 기호로도 표기하고, dot product라고 하기도 한다. 단순히 나열하는 식으로 쓰기 위해서는 행렬의 전치, 

Transpose가 필요하다.

NumPy코드 예시를 하나 보자

x = np.array([1,2,3])

y = np.array([3,2,1])

np.dot(x,y)
Out[66]: 10

np.dot()이라는 메소드로 계산할 수 있는데, y대신 y.T를 넣어도 된다. 하지만 vector 꼴이기 때문에, Transpose하지 않아도 계산된다.

 

그렇다면 NumPy의 matrix 꼴에서는 어떤 결과가 나올까?

x=np.array([[1,2,3]])

y=np.array([[3,2,1]])

x.shape
Out[74]: (1, 3)

y.shape
Out[75]: (1, 3)

다음과 같이 1X3 행렬을 두 개 만들었다.

np.dot(x,y)
Traceback (most recent call last):

  File "<ipython-input-76-6849a5f7ad6c>", line 1, in <module>
    np.dot(x,y)

ValueError: shapes (1,3) and (1,3) not aligned: 3 (dim 1) != 1 (dim 0)

두 개를 dot product 한 결과 불가능하다고 나온다. 

np.dot(x,y.T)
Out[77]: array([[10]])

앞서 서술한대로 Transpose한 경우에는 성립한다.

 

 

[행렬의 내적과 곱셈]

 

가볍게 Vector의 곱셈에 대해서 파악해보았으니 Matrix 단에서 알아보자

행렬곱은 기본적으로 모양이 맞아야 가능하다.

간단하게 말해서 위 예시 처럼 두 행렬을 곱하고 싶다면 A(m x k) B(k x n) 처럼 앞 부분의 col과 뒷 부분의 row가 같아야한다.  

(m x k) · (k x n) = (m x n)

이 경우에만 행렬곱이 이루어질 수 있다. 한 가지 예시를 보자

 

마지막으로 코드를 통한 예시를 하나 보자

np.dot(x,y.T)
Out[77]: array([[10]])

A = np.array([[1,2,3],[4,5,6]])

B = np.array([[1,2],[3,4],[5,6]])

A.shape
Out[80]: (2, 3)

B.shape
Out[81]: (3, 2)

np.dot(A,B)
Out[82]: 
array([[22, 28],
       [49, 64]])

np.dot(A,B).shape
Out[83]: (2, 2)

 

 

다음 두 글을 참고하여 작성하였다.

 

https://mathbang.net/562

 

행렬의 곱셈, 행렬의 거듭제곱

행렬의 곱셈은 행렬의 실수배에 비하면 훨씬 어려워요. 행렬을 곱할 수 있는 조건이 있어 이 조건을 만족하지 않으면 곱셈을 하지 못하는 경우도 있어요. 게다가 계산방식도 매우 까다롭죠. 도형 문제처럼 행렬을..

mathbang.net

https://datascienceschool.net/view-notebook/3f44cfdda2874080a9aa6b034c71d5ec/

'머신 러닝 및 파이썬 > Numpy 공부' 카테고리의 다른 글

Dictionary 변수 데이터 옮기기  (0) 2020.03.10
Numpy의 Shape  (0) 2020.03.04
Numpy의 log  (0) 2020.02.27
Numpy의 Dimension  (0) 2020.02.27

Numpy matrix를 만들게 되면 shape 메소드를 통해서 그 matrix의 모형을 알아낼 수 있다.

 

x = np.array([1,2,3])

x.shape
Out[49]: (3,)

y = np.array([[1,2],[3,4]])

y.shape
Out[51]: (2, 2)

다음과 같이 1X3 행렬과, 2X2 행렬을 만들었다. 그런데 뭔가 이상하지 않은가? 다음 코드를 보고 판단해보자

x = np.array([1,2,3])

x2 = np.array([[1,2,3]])

x.shape
Out[55]: (3,)

x2.shape
Out[54]: (1, 3)

두 행렬의 요소는 동일할지언정, 표기법이 다르다. 이는 Numpy matrix의 표기 방식 때문이라고 추정한다.

https://stackoverflow.com/questions/22053050/difference-between-numpy-array-shape-r-1-and-r

 

Difference between numpy.array shape (R, 1) and (R,)

In numpy, some of the operations return in shape (R, 1) but some return (R,). This will make matrix multiplication more tedious since explicit reshape is required. For example, given a matrix M, if...

stackoverflow.com

역시나! Stackoverflow에 비슷한 질문이 있었고, 이 중 충분한 답변이 될만한 부분을 같이 적는다.

 

The shape is a tuple. If there is only 1 dimension the shape will be one number and just blank after a comma. For 2+ dimensions, there will be a number after all the commas.

(모양은 튜플입니다. 만약 1차원이라면 숫자 하나와 콤마 뒤의 빈칸으로 나타나고, 2차원 이상에서는 콤마 뒤에 숫자가 옵니다.)

 

x.ndim
Out[56]: 1

x2.ndim
Out[57]: 2

차원 확인으로 다시 한번 확인해볼 수 있다.

'머신 러닝 및 파이썬 > Numpy 공부' 카테고리의 다른 글

Dictionary 변수 데이터 옮기기  (0) 2020.03.10
행렬 곱셈, 내적  (0) 2020.03.04
Numpy의 log  (0) 2020.02.27
Numpy의 Dimension  (0) 2020.02.27

5.5.1 ReLU 계층

 

먼저 활성화 함수로 사용되는 ReLU의 수식이다.

ReLU의 수식
ReLU의 미분

순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 흘린다, 반면 x가 0보다 작은 경우엔 신호를 보내지 않는다. 

 

Computational Graph로는 다음과 같다.

이제 코드로 옮겨보자

class ReLU:
    def __init__(self):
        self.mask = None
        
    def forward(self, x):
        self.mask = (x<=0)
        out =x.copy()
        out[self.mask] = 0
        
        return out
    
    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout
        
        return dx

여기서 mask 의 정체에 대해서 알기 어려웠지만 예시를 통한다면 쉽게 파악할 수 있다.

>>>x=np.array([[1.0,-0.5],[-2.0,3.0]])

>>>print(x)
[[ 1.  -0.5]
 [-2.   3. ]]

>>>mask = (x<=0)

>>>print(mask)
[[False  True]
 [ True False]]
 
>>>type(mask)
numpy.ndarray

>>>mask.dtype
dtype('bool')

기본적으로 x를 numpy array로 선언하기 때문에 mask 역시 이에 따라서 생성된다. 확인 결과 mask는 bool type의 ndarray임을 알 수가 있다.

>>relu = ReLU()
>>relu.forward(x)
array([[1., 0.],
       [0., 3.]])

0보다 작은 경우엔 mask가 True가 되어, out의 값을 0으로 만들어버렸다. 

>>>out[mask]
array([-0.5, -2. ])

>>>out[mask]=0
>>>out
array([[1., 0.],
       [0., 3.]])

다음과 같음을 볼 수 있다.

 

 

5.5.2 Sigmoid 계층

 

시그모이드 함수

이를 computational graph로 나타내면 다음과 같다.

이제 역전파의 흐름대로 한 단계 씩 알아보자. 

 

1단계

'/' 노드를 즉 y = 1/x를 미분하면 다음과 같은 결과를 얻을 수 있다. 

해석 하자면, 역전파 때는 순전파의 출력을 제곱한 후 음수값으로 하류로 전달한다.

 

2단계

'+' 노드는 단순히 값을 하류로 보낸다.

 

3단계

'exp'노드는 y=exp(x)를 수행하고, 미분과 원함수가 동일하다.

4단계

'x' 노드는 순전파 때의 값을 '서로 바꿔' 곱한다.

 

 

이제 전체의 결과를 보자

 

Sigmoid 계층의 Computational Graph (간소화)

 

식을 한 번 정리해보자 

굉장히 간단해졌다! Sigmoid 계층의 역전파는 순전파의 출력 (y) 만으로 계산할 수 있다.

 

마지막으로 간단하게 코드로 구현해보자

class Sigmoid:
    def __init__(self):
        self.out = None
        
    def forward(self, x):
        out = 1/(1+np.exp(-x))
        self.out = out
        
        return out
    
    def backward(self, dout):
        dx = dout * (1-self.out)*self.out
        
        return dx

이 구현에서는 순전파의 출력을 인스턴스 변수 out에 저장했다가, 역전파 계산 때 그 값을 사용한다.

앞 장에서 가중치 매개변수 에 대한 손실 함수의 기울기를 수치 미분을 사용해 구했다. 하지만 수치 미분은 계산 시간이 오래 걸린다.

하지만 오차역전파법(backpropagation)은 이를 효율적으로 계산할 수 있다.

 

5.1 계산 그래프 (computational graph)

 

계산 그래프는 계산 과정을 그래프로 나타낸 것이다. 

 

1. 계산 그래프를 구성한다.

2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.

 

'계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파(forward propagation), 그 반대를 역전파(backward propagation)

 

 

5.1.3 왜 계산 그래프로 푸는가?

 

  • '국소적 계산'에 집중하여 문제를 단순화할 수 있다.
  • 중간 계산 결과를 보관할 수 있다. 
  • 역전파를 통해 '미분'을 효율적으로 계산할 수 있다.

역전파에 의한 미분값의 전달

 

5.2 연쇄법칙

 

'국소적 미분'을 전달하는 원리는 연쇄법칙(Chain rule)에 따른 것이다. 다음 내용부터 연쇄법칙을 설명하고 그것이 계산 그래프 상의 역전파와 같다는 사실을 밝힐 것이다.

##앞으로 계산그래프는 computational graph, 연쇄법칙은 Chain rule로 서술하겠습니다. 특히나 Chain rule 같은 경우는 미분적분학에 나오는 부분인 만큼 원어를 사용할 것입니다.

 

5.2.1 Computational graph의 역전파

역전파의 계산 절차는 신호 에 노드의 국소적 미분(편미분)을 곱한 후에 다음 노드로 전달하는 것이다. 

 

 

5.2.2 Chain rule 이란?

 

미분적분학의 내용으로 Chain rule을 검색해서 알아보자. 아주 쉽다.

 

 

5.2.3 Chain rule과 Computational Graph

 

그림1

 

5.3 역전파 

 

5.3.1 덧셈 노드의 역전파

 

그림2
그림3

t = x+y의 x에 대한 t의 편미분을 생각해보자, x는 1차항이기 때문에 편미분 값은 1이 나오게 된다. 그렇기 때문에 [그림2]에서 편미분값이 그대로 전달되는 것이다. ([그림1]에서의 dt/dx 가 1임)

 

 

5.3.2 곱셈 노드의 역전파

 

 

곱셈 노드 역전파는 상류의 값의 순전파 때 입력 신호들을 '서로 바꾼 값'을 곱해서 하류로 보낸다. 

**암기하려고 하지말자, 단순한 수학일 뿐이다!!

 

곱셈 역전파의 예시

 

5.4 단순한 계층 구현하기

 

5.4.1 곱셈 계층

 

class MulLayer:
    def __init__(self):
        self.x=None
        self.y=None

    def forward(self, x, y): #순전파
        self.x=x
        self.y=y
        out = x*y
        
        return out
    
    def backward(self, dout): #역전파
        dx = dout * self.y
        dy = dout * self.x
        
        return dx, dy

dx, dy는 실제로 dz/dx, dz/dy 를 의미한다. 다음 그림을 내용을 코드로 옮겨보자

apple = 100
apple_num = 2
tax = 1.1 

mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

print(price) #순전파 결과

#역전파
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print(dapple, dapple_num, dtax)

 

5.4.2 덧셈 계층

class AddLayer:
    def __init__(self, x,y):
        pass #실행할 코드가 없음을 의미
        
    def forward(self, x, y):
        out = x+y
        return out
    
    def backward(self, dout):
        dx = dout *1
        dy = dout *1
        return dx, dy

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650

+ Recent posts