본문 바로가기
밑바닥 딥러닝

Chapter5 - Backpropogation

by winston1214 2021. 7. 3.
반응형

## 계산 그래프

오차역전파법을 이해하기 위해 간단한 그림을 통해 이해해보겠다.

계산하는 과정을 그림으로 나타낸 것을 계산 그래프라고 칭한다. 계산 그래프는 node와 edge로 구성된다.

하나의 예시를 들어 설명하겠다.

문제 : 철수는 사과 2개와 귤 3개를 샀다. 사과는 100원, 귤은 150원이다. 소비세가 10%일 때 지불금액은?

이를 계산 그래프로 풀면 다음과 같은 그림이 나온다.

문제에 대한 계산그래프

소비세가 10% 이므로 1.1이 나중에 곱해지는 것을 볼 수 있다. 이 그림과 같이 왼쪽에서 오른쪽으로 가는 것을 순전파(feedforward)라고 한다. 그리고 이를 오른쪽에서 왼쪽으로 진행하는 것을 역전파(Backpropogation) 이라 한다.

 

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

계산 그래프로 푸는 이유는 여러가지가 있지만 첫번째로 국소적 계산을 하기 위해서이다.

국소적 계산이란 자신과 직접 관계된 작은 범위이다. 국소적 계산을 통해 이를 전파하면서 계산을 이뤄낸다. 따라서 전체 네트워크가 아무리 복잡해도 각 노드의 간단한 계산 문제로 접근할 수 있다는 장점이 있다.

두번째로 중간 계산 결과를 모두 보관할 수 있다는 것이다. 위 그림에서 소비세를 적용하기 전에 값을 쉽게 알 수 있는 것을 생각해보면 바로 이해할 수 있을 것이다.

세번째로 효율적인 미분이 가능하다는 것이다. 다음과 같은 그림을 통해 쉽게 설명이 가능하다.

역전파 계산 그래프

처음엔 자기 자신을 미분하여 1을 반환하게 되고 소비세를 곱해준 값 즉, 소비세 X 지불금액을 소비세로 미분한 값을 쉽게 얻을 수 있다는 것이다. 따라서 1.1을 반환하는 결과를 다음과 같은 그래프를 통해 쉽게 도출해낼 수 있다.

이러한 계산 그래프의 개념을 통해 역전파에 대해서 좀 더 자세히 알아보겠다.

## 연쇄법칙

연쇄법칙(Chain Rule)은 어려워보이지만 고등학교 미분에서 나온 개념이다. 합성 함수의 미분 방법이라고 생각하면 편하다.

예를 들어 z = (x+y)^2 이라는 식이 있다고 하자.

이는 다음과 같이 풀어쓸 수 있다. z = t^2, t = x+y

여기서 z에 대한 x의 미분을 어떻게 할까? 이를 연쇄법칙으로 풀면 다음과 같다.

chain rule example

바로 z를 x에 대하여 미분하는 것이 아니라 t라는 매개체를 거쳐서 미분한다.

그러면 위 식에서 z를 t에 대해 미분한 식은 2t가 되고 t를 x에 대한 미분한 식은 y가 되서 최종적으로는 2t가 도출되고 이는 2(x+y)가 된다.

이러한 과정을 계산그래프로 표현하면 다음과 같다.

chain rule 계산 그래프

### 덧셈의 역전파

z = x+y -> z에 대한 x의 미분 = 1, z에 대한 y의 미분 1  -> 따라서 값 그대로 전달

### 곱셈의 역전파

z = xy -> z에 대한 x의 미분 = y, z에 대한 y의 미분 x  -> 순방향의 입력 신호 값을 전달

 

이러한 연쇄 법칙과 역전파 알고리즘을 잘 이해했는지 예시 문제를 풀어보겠다.

문제

우선 첫번째로 풀어야할 곳은 지불금액이다.지불금액 부분이 최종적인 output이기 때문에 그 부분은 1로 시작한다.

그리고 기호를 봐야한다. 다음 기호는 곱셈이기 때문에 곱해진 값 1.1을 그대로 적어준다

이를 z = xy의 식으로 보면 z에 대한 x의 미분값은 y 이다. 이를 똑같이 적용해서 입력신호 값을 그대로 적어주면 된다. 즉, 숫자 650 밑에 네모칸에는 1.1의 신호를 받았으니 1.1, 그리고 1.1 밑에 값은 650x1.1 이니 이에 대한 신호값 650 을 적으면 된다.

그리고 다음으로 덧셈기호이다. 덧셈 기호는 그대로 전파 되기 때문에 1.1이 그대로 전파된다. 

다음으로 곱셈도 전에 했던 방식과 같이 계산한다. 따라서 왼쪽 맨 위의 네모칸은 100이라는 신호값이 있고, 그 전 노드에서 계산되었던 1.1이 곱해져서 100 X 1.1 = 110이 된다. 그 밑에는 자동적으로 2 X 1.1 = 2.2 가 된다. 최종적인 답은 다음과 같다.

문제 답

 

이러한 역전파를 코드로 구현해보겠다.

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 # x,y = y,x
        dy = dout*self.x
        return dx,dy

x와 y를 초기화 시키고 순전파는 곱셈으로 정의한다. 그리고 역전파는 xy의 미분값을 곱해줌으로써 진행을 한다. 위와 같은 문제를 간단하게 순전파로 구현한 코드이다.

apple = 100
apple_num = 2
tax = 1.1

# layers
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

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

print(price)
# 220

위 코드는 맨 처음의 문제를 순전파로 푸는 코드이다. 이를 역전파 구하는 코드는 다음과 같다.

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

print(dapple,dapple_num,dtax)
# 2.2 110.00000000000001 200

마지막으로 역전파를 계산한 손수 계산한 것을 코드로 나타내는 방법이다.

그 전에 덧셈에 대한 역전파 계산 코드를 짠다.

class AddLayer: # 덧셈 연산
    def __init__(self):
        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)
orange_price = mul_orange_layer.forward(orange,orange_num)
all_price = add_apple_orange_layer.forward(apple_price,orange_price)
price = mul_tax_layer.forward(all_price,tax)

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

print(price) # 715.0000000000001
print(dapple_num,dapple,dorange,dorange_num,dtax)
# 110.00000000000001 2.2 3.3000000000000003 165.0 650

 

## 활성화 함수의 코드 구현

먼저 ReLU 함수를 구현한다. ReLU 함수는 x>0이면 y=x이고, x<=0 이면 y = 0이다. 이에 대한 미분은 암산으로 가능하니 생략한다.

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

순전파에서 x가 0이하의 값이면 0이하인 numpy array 값만 0이 되는 구조이다. 또한, 이 마스크 값을 이용하여 0이하인 값만 미분값인 0을 삽입하고 그 외이면 dout 의 값을 그대로 출력한다.

예시를 들면 다음과 같다.

x = np.array([[1.0,-0.5],[-2.0,3.0]])
mask = (x<=0)
print(mask)
'''
[[False  True]
 [ True False]]
 '''

 

다음은 sigmoid function이다.

sigmoid function = 1/(1+(exp(-x))) 이다. sigmoid function을 미분하면 sigmoid(x)(1sigmoid(x)) 값이 된다.

sigmoid 함수를 미분한 그래프는 다음과 같다.

sigmoid 미분 결과 plot

이를 python code로 나타내보자

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.0-self.out) * self.out
        return dx

 

## Affine Transform & Softmax 계층 구현

순전파에서 행렬곱을 Affine Transform이라고 한다. 이는 기존의 계산과 다른 방식인데, 스칼라 값이 아닌 Matrix 형태로 전달되는 것이다.

순전파를 계산 그래프로 그렸을 땐 다음과 같이 그려진다.

Affine Transform feedfoward propogation

다음 그림은 Y = XW+B를 나타낸 것이다. 위의 숫자는 matrix의 shape을 나타낸다.

위 그림에서 역전파를 구하고 그림을 표현하면 다음과 같다.

Affine transform backpropogation

전치를 곱해주면서 X에 대한 미분을 계산해낸다. 따라서 1번 처럼 W의 Transpose한 값을 dot 해주면서 값이 도출된다.

이를 배치형태로 나타내면 X의 shape은 (N,2)가 되고 이 N이 그대로 전파되서 X dot W는 (N,3), Y는 (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)
        return dx

단순히 위의 식을 코드로 나타낸 것이다.

 

Softmax는 입력 값을 확률 형태로 바꾸어 나타내주는 층이다. 다음은 기본 네트워크의 구조이다.

Network Architecture

softmax의 식은 다음과 같다.

 

softmax

Softmax에 대한 자세한 설명은 https://wikidocs.net/35476 를 참고하면 된다.

이 코드는 다음과 같다.

def softmax(a) : 
    c = np.max(a) # 최댓값
    exp_a = np.exp(a-c)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y
def cross_entropy_error(y,t):
    delta = 1e-7 # 절대 0이 되지 않도록 만듦
    return -np.sum(t*np.log(y+delta))

앞장에서도 설명한 softmax와 cross entropy function이다.

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 손실
        self.y = None # softmax 출력
        self.t = None # 정답 레이블(onehotlabel)
        
    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

순전파에선 예측값과 실제 값의 cross entropy error로 loss를 계산하고 역전파에선 이 에러를 배치사이즈만큼 나눈 것으로 업데이트를 한다.

네트워크와 다른 모든 함수들의 코드는 github에 있다.

스타는 사랑입니다..!ㅎㅎㅎ

https://github.com/winston1214/DeepLearning-from-scratch/blob/master/Chapter5-Backward_propagation/Chapter5-Backward_propagation.ipynb

 

winston1214/DeepLearning-from-scratch

밑바닥 딥러닝 공부. Contribute to winston1214/DeepLearning-from-scratch development by creating an account on GitHub.

github.com

 

반응형

'밑바닥 딥러닝' 카테고리의 다른 글

Chapter4 - Training Neural Network(1)  (0) 2021.02.02
Chapter3 - Neural Network(3)  (0) 2021.01.10
Chapter3 - Neural Network(2)  (0) 2021.01.10
Chapter3 - Neural Network(1)  (0) 2021.01.10
Chapter2 - Perceptron  (0) 2020.12.28

댓글