딥러닝

[Deep Learning from scratch] 5. 오차역전파법

PKkomi 2023. 5. 15. 16:20

앞 장에서는 신경망의 weight parameter에 대한 cost function의 기울기를 수치 미분을 사용했다.

수치 미분은 단순하고 구현하기 쉽지만, 연산이 오래 걸린다는 단점이 존재한다. 

오차역전파법(Backward propagation)은 비교적 복잡한 구조를 가지지만, 연산이 빠르고 효율적이다.

 

오차역전파법에 대한 자세한 설명은 책에 나와있다.

계산그래프를 통한 설명을 차근차근 따라가면 쉽게 이해할 수 있다.

간단하게 핵심만 정리해보면 다음과 같다.

1) 덧셈 노드의 역전파 : 입력 신호를 그대로 다음 노드로 전달한다.

2) 곱셈 노드의 역전파 : 순전파의 입력 신호를 서로 바꿔서 역전파의 입력 신호와 곱한다.

3) ReLu 계층과 Sigmoid 계층 구현 : 앞장에서 배운 것과 1,2를 토대로 구현할 수 있다. 책의 코드와 설명이 자세히 나와있으므로 생략한다.


 

 

중요한 부분은 여기서부터다. 

Affine 계층과 Softmax 계층을 구현할 수 있다.

 

Affine 계층부터 살펴보자.

신경망의 순전파에서는 Y = np.dot(X, W) + B로 뉴런의 가중치의 합을 계산한다.

그리고 Y를 활성화 함수로 변환하여 다음 층으로 전달한다.

이때, np.dot(X, W)는 행렬곱연산이므로 차원수를 맞춰주는 것이 매우 중요하다.

배치를 이용한 배치용 Affine 계층은 다음과 같이 구현된다.

 

1과 2에서 W와 X가 transpose된 것을 알 수 있는데, 역전파 계산에서 차원을 고려하여 원하는 결과를 얻을 수 있도록 설계된 것이다.

파이썬에서 코드로 구현한 것은 다음과 같다.

# 배치용 Affine 계층 구현
import numpy as np

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) # 그림의 1번
        self.dW = np.dot(self.x.T, dout) # 그림의 2번
        self.db = np.sum(dout, axis=0) # 그림의 3번
        
        return dx # x의 변화에 따른 Y의 변화율(우리가 원하는 값)

 

 

이제 Softmax-with-Loss 계층을 구현해보자.

먼저, 몇 가지 알면 좋은 것이 있다.

신경망에서는 학습과 추론, 두 가지를 수행한다. 추론할 때는 마지막 Affine 계층의 출력을 통해 가장 높은 값(점수)를 가진 값이 답이 된다(하나의 답만 출력하면 되는 경우). 하지만 신경망으로 학습을 하는 경우 Softmax 계층이 필요하다.
Softmax 계층에서 받는 입력은 마지막 Affine 계층의 출력들이다. Cross Entropy 계층은 Softmax 계층의 출력을 입력으로 받게 되며 그 출력은 데이터로부터 얻은 손실 L이 된다.
신경망 학습의 목적은 신경망의 출력(Softmax의 출력)이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것이다. Softmax-with-Loss 계층의 장점은 이것의 역전파가 (y1 - t1, y2 - t2, y3 - t3)(y는 신경망(softmax)의 출력, t는 정답 레이블)과 같이 딱 떨어진다는 것이다. 역전파가 오차이므로 항등함수의 cost function으로 평균 제곱 오차를 사용하면 위와 같은 깔끔한 오차를 얻을 수 있다. 이로 인해 빠르게 loss를 계산할 수 있어 오차역전파법을 사용한다.

 

Softmax-with-Loss 계층은 다음과 같이 구현된다.

# Softmax-with-Loss 계층

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 손실(cost)
        self.y = None # softmax의 출력; 확률을 의미한다.
        self.t = None # 정답 레이블(one-hot encoded vector)
        
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t) # loss function이 cross_entropy_error function이다.
        
        return self.loss
    
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size # batch_size로 나눠줌으로써 데이터 1개당 오차를 다음 계층으로 전달한다
       
        
        return dx

 

 

이제 신경망 학습 전체를 구현할 준비가 끝났다.

 

전제

: 신경망에는 적응 가능한 가중치와 편향이 있고 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다. 신경망 학습은 다음의 4단계로 수행한다.

 

1단계 - 미니배치

:  훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 이 미니배치의 cost function 값을 줄이는 것이 목표다.

 

2단계 - 기울기 산출

: 미니배치의 cost function 값을 줄이기 위해 각 weight parameter의 gradient를 구한다. gradient는 cost function을 가장 작게 하는 방향을 제시한다.

 

3단계 - 매개변수 갱신

: 앞서 구한 gradient의 방향으로 weight parameter를 hyper parameter(학습률; learning rate)만큼 갱신시킨다. 이때 갱신되는 값은 아주 작다.

 

4단계 - 반복

: 1~3단계를 반복한다.

 

오차역전파법은 2단계인 기울기 산출에서 사용된다.

코드가 매우 길기 때문에 책에서 다음의 표를 가져왔다.

코드는 다음과 같다.

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        # 가중치 초기화
        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)

        # 계층 생성
        self.layers = OrderedDict() # 신경망의 계층을 OrderdDict에 보관한다.
        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()
        
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # x : 입력 데이터, t : 정답 레이블
    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(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x : 입력 데이터, t : 정답 레이블
    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):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

주목할 것은, OrderedDict에 신경망의 계층을 보관하여 역전파에서 그 값들을 호출하여 활용한다는 점이다.

 

 

오차역전파법으로 계층을 이렇게 표현하면 수치미분보다 훨씬 효율적(계산이 빠르다)이다.

또한 깊은 층의 신경망을 만들 때도 계층을 추가만 해주면 되므로 신경망을 구축하는 것이 생각보다 어렵지 않다.

하지만 위의 코드에서 알 수 있듯이 코드가 꽤 복잡해서 종종 실수를 할 수 있다. 이때 수치 미분을 통해 구한 값과 비교하여 결과를 검증할 수 있는데, 이를 기울기 확인(gradient check)이라 한다.

 

다음과 같은 코드를 통해서 각 weight(가중치)와 bias(편향)별로 오차의 평균을 구할 수 있다.

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 데이터 읽기
(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)

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:4.199090940814766e-10
b1:2.4143431933928204e-09
W2:4.665917479044934e-09
b2:1.4042813466369486e-07

0에 매우 근접한 값들을 가지는 것을 통해 오차역전파법으로 구한 기울기가 적절하다는 것을 확인할 수 있다.

 

 

마지막으로, 오차역전파법을 사용하여 신경망 학습을 구현해보자.

앞과 같지만 기울기를 오차역전파법으로 구현한다는 점만 다르므로 자세한 설명은 생략한다.

0.11565 0.1147
0.9056166666666666 0.9073
0.9235833333333333 0.9245
0.93495 0.934
0.9475333333333333 0.9438
0.9552166666666667 0.9513
0.9595833333333333 0.9586
0.96435 0.961
0.9667666666666667 0.963
0.9698833333333333 0.9659
0.9714833333333334 0.9658
0.9738 0.968
0.9750833333333333 0.9694
0.9763333333333334 0.9692
0.9776333333333334 0.9714
0.9786666666666667 0.9706
0.98015 0.971

 

 

지금까지 오차역전파법을 활용한 신경망을 살펴보았다.

모든 계층에서 forward와 backward라는 method를 구현하여 순전파와 역전파를 활용하였다.

순전파는 데이터를 순방향으로 전파하며 신경망의 출력값을 얻었고, backward는 역방향으로 전파하여 weight parameter의 기울기를 수치 미분보다 효율적으로 구하였다.