오뚝이개발자

[CH5] 오차역전파법 본문

AI/밑바닥딥러닝1

[CH5] 오차역전파법

땅어 2020. 6. 20. 18:40
728x90
300x250

오차역전파법(backpropagation)


이전까진 신경망의 학습에서 가중치 매개변수의 기울기를 수치미분을 사용해 구했다. 수치미분은 단순해서 구현이 쉽지만 계산이 오래 걸린다는 단점이 있다. 오차역전파법은 이러한 단점을 개선해 가중치 매개변수의 기울기를 효율적으로 계산하도록 도와준다.

오차 역전파법을 이해하기 위해 먼저 그래프로 나타낸 계산 그래프를 살펴보자.

 

계산 그래프


문제 : 슈퍼에서 사과 2개, 귤 3개를 샀다. 사과와 귤을 개당 100원, 150원이다. 소비세가 10%일 때 지불금액은?

위의 문제를 계산 그래프로 나타내보면 아래와 같다.

계산 그래프 풀이(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

위의 계산그래프의 흐름은 왼쪽에서 오른쪽이다. 이러한 방향의 진행을 순전파(forward propagation), 반대 방향의 진행을 역전파(backward propagation)이라 한다.

그렇다면 계산 그래프로 푸는 것의 이점이 무엇일까?

  • 국소적 계산 : 전체가 복잡해도 각 노드에선 단순한 계산에 집중해 풀 수 있다.
  • 미분의 효율적 계산 : 역전파를 통해 국소적 미분을 전달해 효율적으로 계산 가능.

 

연쇄법칙(chain rule)


계산 그래프에서 역전파를 통해 국소적 미분을 전달하는 원리는 연쇄법칙에 따른 것이다. 연쇄법칙이란 합성함수의 미분은 함성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다는 것이다.

예컨대, z = (x + y)^2라는 함수는 [z = t^2, t = x+y]라는 두식으로 나타낼 수 있고, z의 x에 대한 미분은 아래와 같이 구한다.

 

위의 식을 계산그래프에 국소적미분과 함께 표현하면 아래와 같다.

국소적 미분을 포함한 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

위의 그래프에서 맨 왼쪽의 식은 상쇄되어 최종적으로 dz/dx가 된다. 이는 z의 x에 대한 미분이 되어 결국 역전파가 하는 일이 연쇄법칙의 원리와 같다. 위의 그래프에 계산 결과를 대입하면 아래와 같다.

계산결과 대입한 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

 

덧셈, 곱셈 노드의 역전파


1. 덧셈 노드의 역전파

z = x + y라는 식을 생각해보자. 그럼 dz/dx = 1, dz/dy = 1이다. 이는 계산그래프로 아래와 같이 나타낼 수 있다.

덧셈 노드의 역전파 그래프 (이미지 출처 : 밑바닥부터 시작하는 딥러닝)

즉, 덧셈 노드 역전파는 상류로부터의 입력신호를 다음 노드로 그대로 전달한다. 예를 하나 들자면, 아래와 같다.

덧셈 노드 역전파 예시(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

 

2. 곱셈 노드의 역전파 

z = xy라는 식을 생각해보자. 그럼 dz/dx=y, dz/dx=x이다. 계산그래프는 다음과 같다.

곱셈 노드의 역전파 그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

즉, 곱셈노드의 역전파는 상류로부터 전달받은 값에 순전파 때의 입력신호들을 서로 바꾼 값을 곱해 하류로 보낸다. 예시는 아래와 같다.

곱셈 노드 역전파 예시(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

3. 사과쇼핑의 역전파 예

위의 개념들을 가지고 사과쇼핑을 역전파로 나타낸 그래프는 아래와 같다. 가장 왼쪽의 세 수 2.2, 110, 200은 각각 '사과 가격에 대한 지불금액의 미분', '사과 갯수에 대한 지불금액의 미분', '소비세에 대한 지불금액의 미분' 값이다. 해석하자면 소비세와 사과 가격이 같은 양만큼 오르면 최종금액에 소비세가 200의 크기로, 사과가격이 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를 바꾼다.
        dy = dout * self.x
        return dx, dy

# 덧셈 계층
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

# 계층들
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# 순전파
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)

# 역전파
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
print(dapple_num, dapple, dorange_num, dorange, dtax)   # 110 2.2 165 3.3 650

 

 

활성화 함수 계층 구현하기


1. ReLU 계층

ReLU의 수식과 미분은 아래와 같다.

즉, 순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 보낸다. 반면, 순전파 때 x가 0이하면 역전파 때 하류로 신호를 보내지 않는다.(0을 보낸다.) 이를 간단히 나타내면 아래와 같다.

ReLU 계층 역전파 그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

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

 

2. Sigmoid 계층

sigmoid의 수식과 이를 그래프로 나타내면 아래와 같다.

sigmoid 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

/와 exp 노드가 새로 등장하였다. 이들에 대한 미분을 알아보자.

2-1. / 노드

y = 1/x를 미분하면 다음과 같다.

 

2-2. exp 노드

y = exp(x)를 미분하면 다음과 같다.

이제 전체적으로 나타내보면 다음과 같다.

sigmoid 계층의 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)
sigmoid 계층의 계산그래프 간소화 버전(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

여기서 식을 정리하면 최종적으로 아래와 같이 순전파의 출력 y만으로 표현 가능하다.

sigmoid 계층의 계산그래프 최종(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

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 계층 구현하기


순전파 때 수행하는 행렬의 내적을 기하학에서는 어파인 변환(affine transformation)이라 한다. 이 연산을 수행하는 처리를 Affine 계층이란 이름으로 구현하도록 한다. 지금까지의 계산그래프는 노드 사이에 '스칼라값'이 흘렀는데, 여기선 '행렬'이 흐르고 있다는 것을 유념하자. (각 행렬의 형상을 변수명 위에 표기)

Affine 계층 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

여기서 역전파로 미분값을 구해보면 아래와 같은 식이 나온다.(과정은 생략) T는 전치행렬이다.

계산 그래프로 나타내면 다음과 같다. 특히, X와 dL/dX가 같은 형상이고, W와 dL/dW가 같은 형상이란 것을 눈여겨보자. 행렬 계산에서 이 형상이 유지되도록 생각해보면 위의 식을 유도하는 것이 가능하다.

Affine 계층 역전파(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

데이터 N개를 묶어 순전파하는 배치용 Affine 계층의 경우 아래와 같이 나타낼 수 있다. 순전파의 편향 덧셈은 각각의 데이터에 더해진다. 그래서 역전파 땐 각 데이터의 역전파 값이 편향의 원소에 모여야 한다.(열방향의 합)

배치용 Affine 계층(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

# 배치용 Affine
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-with-Loss 계층 구현하기


여기에선 손실함수인 교차 엔트로피 오차도 포함하여 소프트맥스 계층을 구현한다. 

Softmax-with-Loss 계층의 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

다소 복잡하므로 결과만 제시한다. 위의 계산그래프는 역전파를 표시하여 아래와 같이 간소화 할 수 있다.

Softmax-with-Loss 계층 간소화 계산그래프(이미지 출처 : 밑바닥부터 시작하는 딥러닝)

y는 출력, t는 정답레이블이다. softmax 계층의 역전파는 (y1-t1, y2-t2, y3-t3)라는 말끔한 결과를 내놓고 있다. 이는 우연이 아니라 소프트맥스 함수의 손실함수로 교차 엔트로피 오차를 사용하였을 때 그렇게 나오도록 설계되었기 때문이다. 마찬가지로 항등함수의 손실함수로 평균제곱오차를 사용하면 역전파의 결과가 동일하게 나온다.

구체적인 예를 들어보자. 가령 정답 레이블이 (0, 1, 0)일 때 Softmax 계층이 (0.3, 0.2, 0.5)를 출력했다 하자. 정답의 인덱스는 1인데 출력에서 이 때의 확률이 겨우 0.2이므로 이 시점의 신경망은 올바른 인식을 못하고 있는 것이다. 이 경우 Softmax 계층의 역전파는 (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_entory_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

 

신경망 학습의 전체 그림


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

1단계-미니배치 : 훈련 데이터 중 일부를 무작위로 가져온다. 선별한 데이터를 미니배치라 하며, 미니배치의 손실 함수 값을 줄이는 것을 목표로 한다.

2단계-기울기 산출 : 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실함수의 값을 가장 작게 하는 방향을 제시한다.

3단계-매개변수 갱신 : 가중치 매개변수의 기울기 방향으로 조금 갱신한다.

4단계-반복 : 1~3단계 반복

여기서 오차역전파법이 등장하는 단계는 두번째 기울기 산출이다.

 

오차역전파를 적용한 신경망 구현


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

 

오차역전파법으로 구한 기울기 검증


오차역전파법은 수치미분보다 효율적으로 계산할 수 있다고 하였다. 그럼 수치 미분은 필요가 없는 것일까? 사실 수치 미분은 오차역전파법을 정확히 구현했는지 확인하기 위해 필요하다. 수치 미분은 상대적으로 구현이 쉽기 때문이다. 그에 반해 오차역전파법의 경우 구현이 복잡해 종종 실수를 해 버그가 생기기도 한다. 이러한 작업을 기울기 확인(gradient check)라고 한다. 코드의 실행결과들이 0에 가까운 수이면 실수 없이 구현했다고 보아도 된다.

# coding: utf-8
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))

 

오차역전파법을 사용한 학습 구현


# coding: utf-8
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)

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]
    
    # 기울기 계산
    #grad = network.numerical_gradient(x_batch, t_batch) # 수치 미분 방식
    grad = network.gradient(x_batch, t_batch) # 오차역전파법 방식(훨씬 빠르다)
    
    # 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[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(train_acc, test_acc)

 

 

 

 

728x90
300x250

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

[CH7] 합성곱 신경망(CNN)  (0) 2020.06.23
[CH6] 학습 관련 기술들  (0) 2020.06.21
[CH4] 신경망 학습  (0) 2020.06.19
[CH3] 신경망  (0) 2020.06.13
[CH2] 퍼셉트론(Perceptron)  (0) 2020.06.10
Comments