본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝

5. 오차역전파법

728x90

밑바닥부터 시작하는 딥러닝을 바탕으로 제작하였습니다.

 

5.1. 계산 그래프

 계산 그래프는 계산 과정을 그래프로 나타낸 것입니다. 여기에서 그래프는 복수의 노드와 에지로 표현됩니다. 간단한 문제를 예시로 보겠습니다.

문제: 현빈 군은 슈퍼에서 1개의 100원인 사과를 2개 샀습니다. 이때 지불 금액을 구하세요. 단, 소비세가 10% 부과됩니다.

이 문제를 계산 그래프로 표현을 해보면

이와 같이 볼 수 있습니다. 처음에 사과의 100원이 'x2'노드로 흐르고 200원이 되어 다음 'x1.1'노드로 전달되어 220원이 됩니다. 이를 노드 안에 곱셈인 'x'만을 연산으로 생각하는 방법으로 새로 표현해보겠습니다.

이와 같이 노드 안에 곱셈 연산을 넣어 표현할 수 있습니다. 이런 방법으로 새로운 문제를 표현해보겠습니다.

문제: 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀습니다. 사과는 1개에 100원, 귤은 1개 150원입니다. 소비세가 10% 일 때 지불 금액을 구하세요.

 

이와 같이 그래프를 그려 지불 금액을 구할 수 있습니다. 이 문제에는 덧셈 노드인 '+'가 새로 등장하여 사과와 귤 금액을 합산했습니다. 그래프는 왼쪽에서 오른쪽으로 진행되는데 이를 순전파(forward propagation)라고 합니다. 그럼 순전파와 반대 순서로 진행되는 전파는 역전파(back propagation)이라 할 수 있습니다. 역전파는 미분을 계산할 때 중요한 역할을 합니다.

 

 ■ 국소적 계산

계산 그래프이 특징은 '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있습니다. 국소적이란 '자신과 직접 관계된 작은 범위'라는 뜻입니다. 국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것입니다. 그래프에서 각 노드는 자신과 관련한 계산 외에는 아무것도 신경 쓰지 않고 국소적 계산에 집중합니다. 이러한 방법을 통해 전체 계산이 복작하더라도 각 단계에서 하는 일은 해당 노드의 국소적 계산입니다. 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 할 수 있습니다.

 

5.2. 연쇄법칙

역전파는 국소적인 미분을 순방향과는 반대인 오른쪽에서 왼쪽으로 전달합니다. 이렇게 국소적 미분을 전달하는 원리는 연쇄법칙에 따른 것입니다.

 

 ■ 계산 그래프의 역전파

예로 y = f(x)라는 계산의 역전파를 보면

이와 같이 표현할 수 있습니다. 신호 E에 노드의 국소적 미분을 곱한 후 다음 노드로 전달하는 그림입니다. 순방향과는 반대 방향으로 국소적 미분을 곱합니다.

이것이 역전파의 계산 순서인데, 이러한 방식을 따르면 목표로 하는 미분 값을 효율적으로 구할 수 있습니다.

 

 

 

 ■ 연쇄법칙이란?

우선 합성함수는 여러 함수로 구성된 함수입니다. 예를 들어 z = (x + y)^2라는 식은 z = t^2, t = x + y라는 두 식으로 구성됩니다. 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있습니다. 이것이 연쇄법칙의 원리입니다.

z = (x + y)^2라는 식을 미분하면 이렇게 표현할 수 있습니다. 이 식을 연쇄법칙을 이용해 표현하면

이렇게 미분 값 구한 후

대입을 하면 오른쪽 식과 같이 표현할 수 있습니다. 결국 우리가 구한 식은 두 미분을 곱해 계산되었습니다.

 

 

 

 ■ 연쇄법칙과 계산 그래프

연쇄법칙을 그래프로 그려보면

이와 같이 그릴 수 있습니다. 역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 국소적 미분(편미분)을 곱한 후 다음 노드로 전달합니다. 이러한 방식으로 위 식을 그래프로 그려보면

이와 같이 그릴 수 있고 2(x + y)라는 식은 z를 x에 대해서 미분한 식과 같다는 것을 볼 수 있습니다.

 

5.3. 역전파

 ■ 덧셈 노드의 역전파

z = x + y라는 식을 대상으로 역전파를 살펴보겠습니다. z = x + y의 미분은

이렇게 구할 수 있습니다. 이제 이를 이용해 계산 그래프로 표현하면

 

 

 

이와 같이 표현할 수 있습니다. 역전파 때는 상류에서 전해진 미분에 1을 곱하여 하류로 흘립니다. 즉 덧셈 노드의 역전파는 1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 전달합니다.

 

 ■ 곱셉 노드의 역전파

z = xy라는 식을 대상으로 생각을 해보겠습니다. 이 식을 미분하면

이와 같이 값을 구할 수 있습니다. 이 식을 이용해 계산 그래프를 그리면

 

 

 

 

이와 같이 표현할 수 있습니다. 곱셈의 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보냅니다. 서로 바꾼 값이란 순전파 때 x였다면 역전파에서는 y, 순전파 때 y였다면 역전파에서는 x로 바꾼다는 의미입니다.

 

5.4. 단순한 계층 구현하기

 ■ 곱셈 계층

forward()는 순전파, backward()는 역전파를 처리하는 메서드를 갖도록 구현할 것이고 곱셈 계층은 MulLayer라는 이름의 클래스로 구현하겠습니다.

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

__init__()에서는 인스턴스 변수인 x와 y를 초기화합니다. 이 두 변수는 순전파 시의 입력 값을 유지하기 위해서 사용됩니다. forward()에서는 x와 y를 인수로 받고 두 값을 곱해서 반환합니다. backward()에서는 상류에서 넘어온 미분(dout)에 순전파 때의 값을 서로 바꿔 곱한 후 하류로 흘립니다.

 

 ■ 덧셈 계층

덧셈 계층을 구현하겠습니다.

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

 덧셈 계층에서는 초기화가 필요 없으니 __init()__에서는 아무 일도 하지 않습니다. forward()에서는 입력받은 두 인수 x, y를 더해서 반환합니다. backward()에서는 상류에서 내려온 미분(dout)을 그대로 하류로 흘릴 뿐입니다.

 

5.5. 활성화 함수 계층 구현하기

 ■ ReLU 계층

활성화 함수로 ReLU 수식을 사용하는 경우입니다.

ReLU함수는 이와 같은 수식으로 표현할 수 있습니다. 이 식을 미분한 식은

 

 

 

이와 같은 식이 나옵니다. 순전파 떄의 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 흘립니다. 반면, 순전파 때 x가 0 이하인 경우 역전파 때는 하류로 신호를 보내지 않습니다. 이를 계산 그래프로 표현하면

이와 같이 그릴 수 있습니다. 이제 이 ReLU 계층을 구현하겠습니다. 단, 신경망 계층의 forward와 backward함수는 numpy array를 인수로 받는다고 가정하겠습니다.

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

ReLU 클래스는 mask라는 인스턴스 변수를 가집니다. mask는 True/False로 구성된 넘파이 배열로, 수전파의 입력인 x의 원소 값이 0 이하인 인덱스는 True, 그 외는 False로 유지합니다.

순전파 때의 입력 값이 0 이하면 역전파 때의 값은 0이 돼야 합니다. 그래서 역전파 때는 순저파 때 만들어준 mask를 사용해 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정합니다.

 

 ■ Sigmoid 계층

sigmoid 함수는

이러한 식을 의미하는 함수입니다. 이 식을 계산 그래프로 표현하면

 

 

 

 

이와 같이 그릴 수 있습니다. exp 노드는 exp(x) 계산을 수행하고 '/'노드는 y = 1/x 계산을 수행합니다. 이제 역전파를 한 단계씩 짚어보겠습니다.

 

  •  1단계

'/'노드, 즉 y = 1/x을 미분하면 -y^2가 됩니다. 즉 역전파 때는 상류에서 흘러온 값에 -y^2(순전파의 출력을 제곱해서 마이너스를 곱한 값)을 곱해서 하류로 전달합니다.

  • 2단계

'+'노드는 상류의 값을 여과 없이 하류로 내보내는 역할을 합니다.

  • 3단계

'exp'노드는 y - exp(x)연산을 수행하며, 그 미분은 exp(x)입니다. 즉 상류의 값에 순전파 때의 출력을 곱해 하류로 전파합니다.

  • 4단계

'x'노드는 순전파 때의 값을 서로 바꿔 곱합니다. 이 예에서는 -1을 곱하면 됩니다. 

이제 이러한 과정을 거쳐 그래프로 표현하면

이와 같이 그릴 수 있습니다. 이렇게 Sigmoid 계층의 역전파를 계산 그래프로 완성했습니다. 이 값을 정리하면

이와 같이 순전파의 출력(y)로만 계산할 수 있습니다. 이제 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

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

 

5.6. Affine/Softmax 계층 구현하기

 ■ Affine 계층

신경망의 순전파에서는 가중치 신호의 총합을 계산하기 때문에 행렬의 곱을 사용했습니다. 그리고 행렬의 곱 계산은 대응하는 차원의 원소 수를 일치시키는 게 핵심입니다. 그럼 행렬의 곱과 편향의 합을 계산 그래프로 그려보겠습니다.

이와 같이 표현할 수 있습니다. 이 그래프는 각 노드에 스칼라 값이 아닌 행렬이 흐르고 있습니다. 이제 이 식의 역전파를 생각해보겠습니다. 행렬을 사용한 역전파도 행렬의 원소마다 전개해보면 스칼라값을 사용한 지금까지의 계산 그래프와 같은 순서로 생각할 수 있습니다.

W^T는 W의 전치행렬을 뜻합니다. 전치행렬은 W의 (i, j) 위치의 원소를 (j, i) 위치로 바꾼 것을 말합니다. 수식으로는 오른쪽과 같이 작성할 수 있습니다. 전치는 대응하는 차원의 원소 수를 일치시키기 위해서 사용합니다. 그럼 이제 계산 그래프의 역전파를 구해보겠습니다.

 

 

 

계산 그래프에서 각 변수의 형상에 주의해야 합니다.

 

 ■ 배치용 Affine 계층

지금까지 설명한 Affine 계층은 입력 데이터로 X 하나만을 고려한 것입니다. 이번에는 데이터 N개를 묶어 순전파하는 경우, 즉 배치용 Affine 계층을 생각해보겠습니다.

기존과 다른 부분은 X의 형상이 (N, 2)가 된 것뿐입니다. 이제 Affine 계층을 구현해보겠습니다.

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        # 가중치와 편향 매개변수의 미분
        self.dW = None
        self.db = None

    def forward(self, x):
        # 텐서 대응
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.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)
        
        dx = dx.reshape(*self.original_x_shape)  # 입력 데이터 모양 변경(텐서 대응)
        return dx

이와 같이 구현할 수 있습니다.

 

 ■ Softmax-with-Loss 계층

마지막으로 출력층에서 사용하는 softmax 함수입니다. softmax 함수는 입력 값을 정규화하여 출력합니다. 예를 들어 손글씨 숫자 인식에서는 0부터 9 사이의 값으로 정규화(출력의 합이 1이 되도록 변형)합니다. 앞으로 softmax 함수와 손실함수인 cross entropy error도 포함하여 softmax-with-loss 계층을 구현하겠습니다.

일단 간소화한 계산 그래프를 그려보겠습니다. 여기에서는 3 클래스 분류를 가정했습니다. 그래서 이전 계층에서 3개의 입력을 받습니다. softmax에서 값을 정규화해 cross entropy error에 전달합니다. cross entropy error에서는 softmax의 출력(y)과 정답 레이블(t)을 받고 이 데이터로부터 손실 L을 출력합니다. 이 계산 그래프에서 역전파가 y-t로 나왔습니다. 즉 softmax 계층의 출력과 정답 레이블의 차분입니다. 즉 현재 출력과 정답 레이블의 오차를 있는 그대로 드러냅니다. 그럼 softmax-with-loss 계층을 구현하겠습니다.

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]
        if self.t.size == self.y.size: # 정답 레이블이 원-핫 인코딩 형태일 때
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

 

5.7. 오차역전파법 구현하기

신경망 학습의 순서를 보겠습니다.

  • 전제: 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 합니다.

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

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

3. 매개변수 갱신: 가중치 매개변수를 기울기 방향으로 아주 조금 갱신합니다.

4. 반복: 1~3단계를 반복합니다

지금까지 설명한 오차역전파법이 등장하는 단계는 두 번째인 기울기 산출입니다.

먼저 2층 신경망을 TwoLayerNet 클래스로 구현합니다.

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

OrderedDict()를 사용해 딕셔너리 순서를 기억하게 했습니다. 그래서 순전파 때는 추가한 순서대로 각 계층의 forward() 메서드를 호출하기만 하면 처리가 됩니다. 이제 오차역전파법을 사용한 신경망 학습을 구현해보겠습니다.

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

실행결과

이렇게 학습이 진행하고 학습 때와 test 때의 정확도를 잘 출력하는 것을 볼 수 있습니다.

'Deep Learning(강의 및 책) > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

7. 합성곱 신경망(CNN)  (0) 2022.02.18
6. 학습 관련 기술들  (0) 2022.02.15
4. 신경망 학습  (0) 2022.02.07
3. 신경망  (0) 2022.02.07
2. 퍼셉트론(perceptron)  (0) 2022.02.06