티스토리 뷰

머신러닝

[ML] 6. Back Propagation

hezma 2020. 8. 8. 16:23
  • 이 글은 글 하단의 Reference에 있는 강의, 교재를 보고 정리한 것입니다.

6.0 Intro

앞장에서는 신경망 학습에 대해서 배웠다. 그 때 기울기 산출은 수치 미분을 사용해 구했다. 수치 미분은 단순해서 구현하기 쉽지만 시간이 오래 걸린다는 단점이 있다. 그래서 이 장에서는 가중치 매개변수의 기울기를 효율적으로 계산하는 오차역전파법(Back propagation)에 대해 다룬다.

오차역전파법에 대해 이해하는 방법은 크게 두가지가 있다. 하나는 수식을 통한 것, 다른 하나는 그래프를 통한 것이다. 이 장에서는 그래프를 중심으로 다룬다.

오차역전파법을 그래프로 설명한다는 아이디어는 Stanford Univ open course CS231n의 Fei-Fei Li 교수에게서 참고했다고 한다.

다음의 오차 역전파법을 배우게 된 흐름.

  1. Neural Network의 training에서 각 매개변수의 갱신은 Gradient Descent를 통해 이루어진다.
  2. Gradient Descent를 하기 위해서는 Loss function을 해당 매개변수로 미분한 값을 계산해야 한다.
  3. 이 값은 수치 미분 or 이 장에서 설명할 오차역전파법을 통해 구한다.

6.1 Computational Graph(계산 그래프)

계산 그래프(computational graph)는 계산 과정을 그래프로 나타낸 것으로, 그래프는 Node들이 Edge로 연결되어있는 자료구조를 말한다. 다음<그림1>과 같은 구조를 말한다.

<그림1: 그래프 구조>

그래프를 통해 계산 과정을 나타낸다는 아이디어가 있는데, 각 사칙 연산과 항들을 Node로 표현하여 계산 순서에 따라 Edge로 연결하는 방법이다. 다음의 예제에서 생각해보자.

6.1.1 Represent computing in graph

예제를 통해 계산 과정을 그래프로 표현하는 방법을 알아보자. 문제는 다음과 같다. 슈퍼에서 100원짜리 사과 2개와 150원짜리 귤 3개를 샀다. 이 때 소비세는 10%다. 총 가격은?

문제를 푸는 과정을 사칙 연산으로 표현해보면 {(100x2)+(150x3)}*1.1 이 될것이다. 이를 계산 그래프로 표현해보면 다음 <그림2>와 같다.

<그림2: 계산 그래프를 이용한 계산 과정>

이 때 왼쪽에서 오른쪽으로 계산의 흐름이 진행되며 이를 순전파(Forward propagation)라 한다. 우리가 궁극적으로 원하는 미분값은 순전파 진행 후 역전파를 통해 계산된다.

6.1.2 Local computing

계산 그래프의 특징은 '국소적 계산'을 왼쪽에서 오른쪽으로 전파함으로써 최종 결과를 얻는다는 점이다. 즉, 하나의 노드는 전체와 관계없이 자신과 관련된 input만 가지고도 결과를 출력할 수 있다. 즉, 자신과 관계없는 연산에 대해서는 신경쓸 필요가 없다.

6.1.3 Why Graph?

그럼 왜 그래프로 이런 문제를 풀어야할까? 그래프에는 다음과 같은 이점들이 있다.

  • 각 부분적 계산들은 단순하다.
  • 중간 계산 결과를 Node들에 보관할 수 있다.
  • 역전파를 통해 '미분'을 효율적으로 계산할 수 있다.

마지막 점을 구체적으로 설명해보기 위해 위의 예제에서 구할 수 있는 미분값들을 생각해보자.

  • '사과 가격 변화에 따른 총 가격의 변화'
  • '오렌지 개수 변화에 따른 총 가격의 변화'

등을 구해볼 수 있을 것이다. 이런 것들을 역전파로 쉽게 구할 수 있다.

6.2 Chain Rule(연쇄 법칙)

역전파는 순전파와 달리 '국소적인 미분'을 오른쪽에서 왼쪽으로 전달하는 과정이다. 또한 이 미분을 전달하는 과정의 근거는 연쇄법칙에 따른 것이다. 이 절에서는 연쇄 법칙에 대한 간략한 설명과 그것이 역전파와 같다는 사실을 보인다.

6.2.1 Backward Propagation in Computational Graph

계산 그래프를 통해 역전파를 계산하는 방법은 다음 <그림3>과 같다.

<그림3: 계산 그래프를 통한 역전파 계산>

위의 그림과 같이 역전파는 특정 노드에서 오른쪽에서 받은 신호 E에 그 노드의 국소적 미분값을 곱해 왼쪽으로 전달하는 과정의 반복이다. 여기서 말하는 국소적 미분은 y=f(x)의 미분을 구한다는 것이다. 이런 계산이 가능한 것은 연쇄 법칙 때문이다.

6.2.2 What is Chain Rule?

연쇄 법칙은 합성 함수에서 미분 값을 구할 때 사용되는 원리다. 예를 들어 z = (x+y)2 같은 합성함수를 생각해보자. 이는 다음 두 함수의 합성으로 볼 수 있다.

이 때 연쇄법칙에 따라 z에 대한 x의 미분값은 다음과 같이 두 미분값의 곱으로 계산할 수 있다.
$$
\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\times \frac{\partial t}{\partial x}=2t \times 1 = 2(x+y)
$$
즉, 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다는 사실이 연쇄법칙이다. 이는 비단 두 함수의 합성뿐만이 아니라 임의의 N개 함수가 합성된 합성 함수에서도 성립하는 법칙이다.

6.2.3 Chain Rule & Computational Graph

이제 방금의 합성함수 예제를 계산 그래프로 나타내보면 다음 <그림4>와 같다.

<그림4: 합성함수 예제 계산그래프>

역전파 과정은 <그림3>과 같이 미분값을 곱해서 넘기는 것이었으므로, 이는 결국 <그림4>의 연산에서 볼 수 있듯이 Chain Rule을 한 것과 마찬가지가 된다. 즉, 역전파가 하는 일은 연쇄법칙에서 각 함수의 미분값을 곱해나가는 것과 완전히 같게 된다.

6.3 Back Propagation(역전파)

각 기본적인 연산 노드들에서 back prop이 어떻게 진행되는지 알아보고 이를 클래스로 구현해본다.

6.3.1 덧셈 노드의 역전파

우선 덧셈 노드에서는 두 input x,y에 대해 output z가 z=x+y로 정의된다. 이때 x에 대한 z의 미분, y에 대한 z의 미분은 모두 1이므로 backward prop은 다음 <그림5>와 같이 오른쪽에서 들어온 error signal이 그대로 전파된다.

<그림5: 덧셈 노드의 역전파 계산>

6.3.2 곱셈 노드의 역전파

두 input x,y에 대해 output z = xy로 정의되는 전형적인 곱셈노드를 생각해보면 다음의 사실을 알 수 있다.
$$
\frac{\partial z}{\partial x} = y \quad, \frac{\partial z}{\partial y} = x
$$
따라서 backward prop은 다음 <그림6>과 같이 오른쪽에서 들어온 error signal에 대해 두 input을 바꾸어 곱해주는 꼴이 된다.

<그림6: 곱셈 노드의 역전파 계산>

6.4 Implementing Simple Layer

이 장의 맨 처음에서 소개했던 다음 예제를 파이썬으로 구현하여 계산해보자. 다음 <그림7>은 <그림2>를 그대로 가져온 것이다.

<그림7: 처음 예제>

6.4.1 MulLayer(곱셈 계층)

모든 layer는 순전파, 역전파를 수행하는 forward(), backward()의 공통 메서드를 갖도록 구현한다. 이 때 곱셈 계층은 다음과 같이 구현할 수 있다.

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

forward() 메서드는 두 값을 곱해서 출력해주는 당연한 연산을 수행한다. 이 때 클래스의 인스턴스 변수 x와 y를 기록해놓는다. 이 기록해놓은 x와 y를 backward시 오른쪽에서 받은 dout에 곱해서 흘려주면 된다.

6.4.2 AddLayer(덧셈 계층)

덧셈 계층은클래스 인스턴스가 필요 없으니 다음과 같이 정의한다.

class AddLayer:
  def __init__(self):
    pass
  def forward(self,x,y):
    self.x = x
    self.y = y
    return (x+y)
  def backward(self,dout):
    dx = dout*1
    dy = dout*1
    return (dx,dy) # dout을 그대로 backward전파

6.4.3 예제 전체 구현

위에서 구현한 class들을 기반으로 forward,backward prop을 모두 진행해보면 아래 코드와 같다. 맨 오른쪽에서 들어가는 error signal은 1이다.

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

# make 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)

6.5 Implementing Layers in Neural Network

간단한 덧셈 노드, 곱셈 노드에 대해서 어떻게 backward prop이 적용되는지 알아봤으니, 이제 신경망을 구성하는 비교적 복잡한 함수들에 대해서도 위에서 한 것들을 똑같이 구현해봐야 할 것이다. 이 절에서는 신경망을 구성하는 층 각각을 하나의 클래스로 구현해보자.

아래에서 다룰 함수들 중 ReLU와 Sigmoid의 정의에 대해서는 아래 링크에서 자세히 다루었다.

ReLU & Sigmoid: https://hezma.tistory.com/93

6.5.1 ReLU Layer

활성화 함수로 사용되는 ReLU는 x>0일 때 x, x<=0 일 때 0을 출력하는 함수다. 이 함수 y를 x에 대해 미분해보면 다음과 같다.
$$
\frac{\partial y}{\partial x} = 1 (x>0),0(x<0)
$$
계산 그래프로는 다음 <그림8>과 같이 나타낼 수 있다.

<그림8: ReLU의 역전파>

이를 코드로 구현해보면 다음과 같다.

class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0) # x 배열의 각 원소가 0보다 작은지 판단하는 논리배열
        out = x.copy() # shallow copy
        out[self.mask] = 0 # mask위치에 있는 원소만 0으로
        return out

    def backward(self, dout):
        dout[self.mask] = 0 #0보다 작은 원소에 대해서는 backward error signal도 0
        dx = dout
        return dx

6.5.2 Sigmoid Layer

sigmoid는 다음과 같이 계산되는 함수였다.
$$
y=\frac{1}{1+e^{-x}}
$$
이 때 y를 x로 미분한 값은 다음과 같이 계산하여 정리할 수 있다.
$$
\frac{\partial y}{\partial x}=\frac{e^{-x}}{(1+e^{-x})^2}=y(1-y)
$$
따라서 sigmoid layer의 backward를 그림으로 표현해보면 다음 <그림9>와 같이 표현될 것이다.

<그림9: sigmoid의 역전파>

이를 코드로 표현해보면 다음과 같다.

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x): # 그냥 시그모이드 계산
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx

6.5.3 Affine Layer

NN의 forward propagation에서는 신호의 총합을 계산하기 위해 행렬의 곱 연산을 사용했다. 이 연산을 하는 계층을 Affine Layer라 하고, 이는 다음과 같은 연산을 생각해보면 된다.
$$
Y=np.dot(X,W)+B
$$
(B를 W에 한 줄로 포함시키고 X에 1이라는 bias unit을 넣어 Y=XW로 계산해도 된다.)

이 연산을 계산 그래프로 표현해보면 다음 <그림10>과 같다.

<그림 10: Affine Layer의 역전파>

이는 간단한 계산그래프지만 위의 그래프들과 달리 흐르는 값이 단순 스칼라가 아니라 행렬이라는 점에 주의해야 한다. 각 원소가 행렬이므로 backward에서의 미분도 행렬의 행렬에 대한 미분값이 되어야 한다. 계산 과정은 생략하고 결과만 나타내보면 다음과 같다.
$$
\frac{\partial L}{\partial \textbf{X}}=\frac{\partial L}{\partial \textbf{Y}}W^T
$$

$$
\frac{\partial L}{\partial \textbf{W}}=X^T\frac{\partial L}{\partial \textbf{Y}}
$$

6.5.4 Affine Layer with batch

6.5.3에서는 하나의 데이터 정보만이 포함된 X에 대해서 생각했다. 그러나 우리가 이를 batch로 처리할 시에는 X가 N개의 data를 포함하고 있으므로 X가 (N,x)꼴의 행렬인 경우에 대해 생각해봐야 할 것이다. 즉, 배치용 Affine 계층에 대해 알아본다. 미분 식은 6.5.3의 그것과 동일하므로 간단한 차이점만 그림으로 표현해보면 다음 <그림11>과 같다.

<그림11: Affine Layer with batch>

이 때 B와 XW의 차원이 달라서 덧셈이 되지 않는건가 생각할 수 있지만, X의 각 Data에 B가 동일하게 더해진다고 생각하면 된다. 예를 들어 N=3인 경우 3개의 Data 각각에 B가 더해지게 된다.(numpy상에서 이렇게 덧셈을 진행해준다.) 또한, 순전파의 편향 덧셈은 각각의 데이터에 더해지므로 역전파 때는 각 데이터의 역전파 값이 편향의 원소로 모여야 한다. 다음의 예를 보면 순전파, 역전파가 어떻게 되는지 이해할 수 있을 것이다. 다음 코드를 보자.

# 순전파시 편향의 덧셈
import numpy as np
X_dot_W = np.array([[0,0,0],[0,10,10]]) # 2*3
B= np.array([1,2,3])
print("X_dot_W+B")
print(X_dot_W+B)
# 역전파 계산 방법
dY = np.array([[1,2,3],[4,5,6]])
dB = np.sum(dY,axis=0)
print("dB")
print(dB)

이 때, 결과는 다음과 같이 출력된다.

X_dot_W+B
[[ 1  2  3]
 [ 1 12 13]]
dB
[5 7 9]

즉, B쪽으로 전해지는 역전파 error signal은 dY의 각 데이터별 error signal을 합한것이 된다. np.sum의 axis=0옵션은 0번축 원소들끼리 더한다는 것.

이제 이상의 Affine 계층을 python으로 옮겨보면 다음과 같다.

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)+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

6.5.5 Softmax-with-Loss Layer

이제 마지막으로 출력층에서 사용하는 Softmax함수에 대해 backward prop을 구현해본다. Softmax함수는 아래의 글 4.3.3에서 다루었다.

Softmax:https://hezma.tistory.com/93?category=789894

간략하게 설명하면 Softmax는 입력값을 정규화하여 출력값의 총합이 1이 되게 함수로 입력되는 여러개 원소의 대소관계는 그대로 유지된다는 특징을 갖는 함수다. 또한, 이 softmax는 추론에는 쓰이지 않고 학습때만 쓰이는 계층이다. 이 Softmax계층을 Cross-Entropy Error 계층과 함께묶어Softmax-with-Loss계층으로 구현해보자. 즉, 다음 <그림12>의 계층을 구현하는 것을 목표로 한다.

<그림12: Softmax with Loss>

이 때 역전파되는 error signal은 <그림12>에서 볼 수 있듯이 y-t다. (증명 생략) 이것을 파이썬 코드로 다음과 같이 구현할 수 있다.

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None # 정답 레이블 (one-hot-encoded)
    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 # error signal
        return dx

6.6 Implementing Backward Propagation

이제 앞서 만든 모든 layer class들을 동원해 Neural Network를 만들고 학습과 추론을 진행해보자.

6.6.1 신경망 학습의 전체 과정

신경망 학습의 전체 그림을 다시 한 번 살펴보면 다음과 같다. (Stochastic 기준)

  1. 미니배치: 훈련 데이터 중 일부를 무작위로 가져온다.
  2. 기울기 산출: 훈련데이터를 이용하여 산출한 Loss함수의 매개변수에 대한 기울기를 산출한다.
  3. 매개변수 갱신: 기울기 방향으로 매개변수를 움직인다.
  4. 종료 조건까지 반복.

6.6.2 오차역전파법을 이용한 신경망 구현

-1) 신경망 class 구현(TwoLayerNet)

class TwoLayerNet:
  def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
    # initialize weights
    self.params = {}
    self.params['W1'] = weight_init_std * np.random.randn(input_size,hidden_size)
    self.params['b1'] = np.zeros(hidden_size)
    self.params['W2'] = weight_init_std * np.random.randn(hidden_size,output_size)
    self.params['b2'] = np.zeros(output_size)

    # make layers
    self.layers = OrderedDict()
    self.layers['Affine1'] = Affine(self.params['W1'],self.params['b1'])
    self.layers['Relu1'] = Relu()
    self.layers['Affine2'] = Affine(self.params['W2'],self.params['b2'])
    self.lastLayer = SoftmaxWithLoss()

  # FORWARD PROP.
  def predict(self,x):
    for layer in self.layers.values():
      x = layer.forward(x)
    return x

  def loss(self,x,t):
    y = self.predict(x)
    return self.lastLayer.forward(y,t)

  def accuracy(self,x,t):
    y = self.predict(x)
    y = np.argmax(y,axis=1)
    if t.ndim != 1 : t = np.argmax(x,axis=1) # t가 one-hot-encoding인 경우

    accuracy = np.sum(y==t)/float(x.shape[0])
    return accuracy

  def numerical_gradient(self,x,t):
    loss_W = lambda W : self.loss(x,t)

    grads={}
    grads['W1'] = numerical_gradient(loss_W,self.params['W1'])
    grads['b1'] = numerical_gradient(loss_W,self.params['b1'])
    grads['W2'] = numerical_gradient(loss_W,self.params['W2'])
    grads['b2'] = numerical_gradient(loss_W,self.params['b2'])
    return grads

  def gradient(self,x,t):
    # calculating gradient via backward prop.
    # forward prop.
    self.loss(x,t)
    # backward prop.
    dout = 1
    dout = self.lastLayer.backward(dout)
    layers = list(self.layers.values())
    layers.reverse() # backward prop 이니까 layer 순서를 거꾸로 바꿔야 함.
    # backward prop 연산
    for layer in layers:
      dout = layer.backward(dout)
    # save result
    grads ={}
    grads['W1'] = self.layers['Affine1'].dW
    grads['b1'] = self.layers['Affine1'].db
    grads['W2'] = self.layers['Affine2'].dW
    grads['b2'] = self.layers['Affine2'].db
    return grads

-2) 오차역전파법의 오차 검증

수치 미분으로 구한 기울기와 오차역전파법으로 구한 기울기가 큰 차이 없는지 검증.

# 오차 검증
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

#Read Data
(x_train,t_train),(x_test,t_test) = load_mnist(normalize=True,one_hot_label=True)
network = TwoLayerNet(input_size=784,hidden_size=50,output_size=10)
# 훈련 Data의 일부
x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch,t_batch)
grad_backprop = network.gradient(x_batch,t_batch)

for key in grad_numerical.keys():
  diff = np.average(np.abs(grad_backprop[key]-grad_numerical[key]))
  print(key + ": "+str(diff))

나의 실행 결과는 다음과 같았다.

W1: 3.2895494400827216e-10
b1: 1.7320099487548763e-09
W2: 4.1800207238947995e-09
b2: 1.4030532205777658e-07

-3) 학습 진행

(x_train,t_train),(x_test,t_test) = load_mnist(normalize=True,one_hot_label=True)
network = TwoLayerNet(input_size=784,hidden_size=50,output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list =[]
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size/batch_size,1)

for i in range(iters_num):
  batch_mask = np.random.choice(train_size,batch_size)
  x_batch = x_train[batch_mask]
  t_batch = t_train[batch_mask]

  grads = network.gradient(x_batch,t_batch)

  for key in ('W1','b1','W2','b2'):
    network.params[key] -= learning_rate*grads[key]

  loss = network.loss(x_batch,t_batch)
  train_loss_list.append(loss)

  if i % iter_per_epoch == 0:
    train_acc = network.accuracy(x_train,t_train)
    test_acc = network.accuracy(x_test,t_test)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)
    print(i,"th accuracy:",train_acc,test_acc)

-4) 최종 결과

train_data와 test_data에 대한 accuracy 를 학습 진행 횟수에 따라 plot해보면 다음 <그림13>과 같다.

<그림13: accuracy plot>

두 정확도가 거의 비슷한 추이로 변하고 있으므로 학습이 제대로 진행된다는 것을 알 수 있다.

Reference: Deep learning from scratch by Saito Goki,

All Codes by: https://github.com/WegraLee/deep-learning-from-scratch

'머신러닝' 카테고리의 다른 글

[ML/NLP] 8. Distributional Representation of Words  (0) 2020.08.25
[ML] 7. Convolutional Neural Network  (0) 2020.08.22
[ML] 5. Training Neural Network  (0) 2020.08.06
[ML] 4. Neural Network  (0) 2020.07.30
[ML] 3. Logistic Regression  (0) 2020.07.27
댓글