5. 오차역전파법 [2/2]

정우일·2021년 10월 23일
1

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

5.5.2 Sigmoid 계층


Sigmoid 계층의 계산 그래프 (순전파)


1단계

' / ' 노드, y=1xy= \cfrac {1}{x} 를 미분하면, yx=1x2=y2\cfrac{\partial y}{\partial x} = - \cfrac{1}{x^2} = - y^2 : 상류에서 흘러온 값에 y2-y^2 곱해서 하류로 전달

2단계

' + ' 노드, 상류의 값 여과 없이 하류로 전달

3단계

' exp ' 노드, y=exp(x)y= exp(x) (y=exy = e^x) 를 미분하면, yx=exp(x)\cfrac{\partial y}{\partial x} = exp(x) : 상류의 값에 순전파의 출력 exp(x)exp(-x) 곱해 하류로 전달

4단계

' ×\times ' 노드, 순전파 때의 값 '서로 바꿔' 곱 : 상류의 값 ×(1)\times (-1) 해서 전달


역전파 최종 출력

하류 노드로 최종적으로 전달되는 Ly y2 exp(x)\cfrac{\partial L}{\partial y}\ y^2 \ exp(-x) 값은 입력 xx,

출력 yy 만으로 계산이 가능하여, 'sigmoid' 노드 하나로 대체가 가능하다.

또한, Ly y2 exp(x)\cfrac{\partial L}{\partial y}\ y^2 \ exp(-x)는 다음과 같이 정리된다.

 

Ly y2 exp(x)\cfrac{\partial L}{\partial y}\ y^2 \ exp(-x)

=Ly 1(1+exp(x))2 exp(x)= \cfrac{\partial L}{\partial y} \ \cfrac{1}{(1 + exp(-x))^2}\ exp(-x)\\

=Ly 11+exp(x) exp(x)1+exp(x)= \cfrac{\partial L}{\partial y} \ \cfrac{1}{1 + exp(-x)}\ \cfrac{exp(-x)}{1 + exp(-x)}\\

=Ly 11+exp(x) (1+exp(x))11+exp(x)= \cfrac{\partial L}{\partial y} \ \cfrac{1}{1 + exp(-x)}\ \cfrac{(1+exp(-x))-1}{1 + exp(-x)}\\

=Ly y (1y)= \cfrac{\partial L}{\partial y} \ y\ (1-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
: 순전파의 출력을 인스턴스 변수 out에 보관했다가, 역전파 계산에 그 값을 사용

 

5.6 Affine / Softmax 계층 구현하기

5.6.1 Affine 계층

신경망의 순전파에서는 가중치의 신호의 총합을 계산하기 때문에 행렬의 곱 사용 (넘파이에서는 np.dot())

>>> X = np.random.rand(2) # 입력
>>> W = np.random.rand(2, 3) # 가중치
>>> B = np.random.rand(3) # 편향
>>> 
>>> X.shape # (2, )
>>> W.shape # (2, 3)
>>> B.shape # (3, )
>>> 
>>> Y = np.dot(X, W) + B

행렬의 곱 계산은 대응하는 차원의 원소 수를 일치시키는 것이 핵심이다.

 

💡 신경망의 순전파 때 수행하는 행렬의 곱은 기하학에서 어파인 변환(affine transformation) 이라고 한다.

Affile 계층의 계산 그래프 : 변수가 행렬임에 주의

(그래프의 노드 사이에는 '스칼라값'이 아닌 '행렬'이 흐르고 있다)

행렬의 역전파도 스칼라값을 사용한 계산 그래프와 같은 순서로 전개된다

 

실제로 행렬의 원소를 전개했을 때의 식

WTW^TWW의 전치행렬, (i, j)(i,\ j)위치를 (j, i)(j,\ i)위치로 바꾼 것


Affine 계산 그래프의 역전파

계산 그래프에서는 각 변수의 형상에 주의한다.(XXLX\cfrac {\partial L}{\partial X} , WWLW\cfrac {\partial L}{\partial W} 등은 같은 형상을 가진다)

행렬의 곱에서는 원소 수를 일치시켜야 하는데, LX=LY WT\cfrac {\partial L}{\partial X} = \cfrac {\partial L}{\partial Y}\ W^T와 같은 식을 사용해야 하는 경우가 생기기 때문이다.

따라서, 행렬 곱('dot' 노드)의 역전파는 행렬의 대응하는 차원의 원소 수 일치하도록 곱을 조립하여 구할 수 있다.

5.6.2 배치용 Affine 계층

: 데이터를 N개 묶어 순전파 하는 경우, 즉 배치(묶은 데이터)용 Affine 계층에서의 계산 그래프

기존의 형상 (2, )(2,\ )(N, 2)(N,\ 2)가 됐을 뿐, 지금까지와 같이 계산그래프 순서에 따라 행렬 계산을 한다.

편향을 더할 때 주의

>>> X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
>>> B = np.array([1, 2, 3])
>>> 
>>> X_dot_W
array([[ 0,  0,  0],
       [10, 10, 10]])
>>> X_dot_W + B
array([[ 1,  2,  3],
       [11, 12, 13]])

순전파의 편향 덧셈은 각각의 데이터 (1번째 데이터, 2번째 데이터, ...)에 더해진다

그래서 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 한다

>>> dY = np.array([[1, 2, 3], [4, 5, 6]])
>>> dY
array([[1, 2, 3],
       [4, 5, 6]])
>>> dB = np.sum(dY, axis=0)
>>> dB
array([5, 7, 9])

데이터가 2개 (N = 2)라고 가정할 때, 편향의 역전파는 두 데이터에 대한 미분을 데이터마다 더해서 구한다.

그래서 np.sum()에서 0번째 축 (데이터를 단위로 한 축)에 대해 (axis=0)의 총합을 구하는 것.

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

5.6.3 Softmax-with-Loss 계층

: 소프트맥스 함수는 입력 값을 정규화하여 출력한다.

손글씨 숫자 인식에서의 Softmax 계층 출력 예시

Softmax 계층은 그림과 같이 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력힌다.

💡 신경망에서 수행하는 작업은 학습추론 두 가지로, 추론할 때는 일반적으로 Softmax 계층을 사용하지 않는다.
또한, 신경망에서 정규화하지 않는 출력 결과를 점수(score)라고 한다.
신경만 추론에서 답을 하나만 내는 경우에는 Softmax 계층이 필요없지만, 신경망을 학습할 때는 필요하다.

손실 함수인 교차 엔트로피 오차도 포함한 'Softmax-with-Loss 계층'의 구현

다음의 계층 계산 그래프는 다소 복잡하여, 다음과 같이 간소화할 수 있다.

소프트맥스 함수는 'Softmax' 계층으로, 교차 엔트로피 오차는 'Cross Entropy Error' 계층으로 표기

Softmax 계층은 입력 (a1, a2, a3)(a_1,\ a_2,\ a_3)를 정규화하여 (y1, y2, y3)(y_1,\ y_2,\ y_3)를 출력한다.

Cross Entropy Error 계층은 Softmax의 출력 (y1, y2, y3)(y_1,\ y_2,\ y_3)와 정답 레이블 (t1, t2, t3)(t_1,\ t_2,\ t_3)로부터 손실 L을 출력한다.

 

역전파의 결과에서 Softmax 계층의 역전파는 (y1t1, y2t2, y3t3)(y_1-t_1,\ y_2-t_2,\ y_3-t_3)라는 결과를 내는데, 이는 Softmax 계층의 출력과 정답 레이블의 차분을 의미한다. 신경망의 역전파에서는 이 차이인 오차가 앞 계층으로 전해진다.

💡 '소프트맥스 함수'의 손실함수로 '교차 엔트로피 오차'를 사용했을 때, 역전파가 (y1t1, y2t2, y3t3)(y_1-t_1,\ y_2-t_2,\ y_3-t_3)로 말끔히 떨어지는 것은 교차 엔트로피 오차라는 함수가 그렇게 설계되었기 때문이다. 회귀의 출력층에서 사용하는 '항등 함수'의 손실 함수로 '오차제곱합'을 이용하는 이유도 이와 같다.

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]
        dx = (self.y -self.t) / batch_size

        return dx
: 역전파 때에는 전파하는 값을 배치의 수(batch_size)로 나눠서 데이터 1개당 오차를 앞 계층으로 전달한다.

 

5.7 오차역전파법 구현하기

5.7.1 신경망 학습의 전체 그림

전제

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

1단계 - 미니배치

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

2단계 - 기울기 산출

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

3단계 - 매개변수 갱신

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

4단계 - 반복

1~3 단계를 반복한다.

5.7.2 오차역전파법을 적용한 신경망 구현하기

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['b1'] = 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):
        # 순전파
        self.loss(x, t)

        # 역전파
        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'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db

        return grads
: 앞 장과 크게 다른 부분은 계층을 사용한다는 점인데, 계층을 사용함으로써 인식 결과 얻는 처리(predict())와 기울기 구하는 처리((gradient()) 계층의 전파만으로 동작이 이루어진다.

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

기울기를 구하는 방법은 두 가지가 있다.

  1. 수치 미분을 사용하는 방법 (느리지만 구현하기 쉽다)
  2. 해석적으로 수식을 풀어 구하는 방법 (빠르지만 구현하기 복잡하다)

수치 미분은 느리지만 구현하기 쉽다는 이점을 이용해, 수치 미분의 결과와 오차역전파법의 결과를 비교해 검증하는 작업을 기울기 확인(gradient check)이라고 한다.

기울기 확인의 구현

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.3546543286299815e-10
b1:2.7227992536863787e-09
W2:6.017825221733239e-09
b2:1.399838034071843e-07
: 수치 미분과 오차역전법으로 구한 기울기의 차이가 매우 작은 것을 확인할 수 있다.

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

오차역전파법을 사용한 신경망 학습의 구현

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.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)
0.16173333333333334 0.1512
0.9036166666666666 0.9061
0.92215 0.9242
0.9327666666666666 0.9359
0.9456833333333333 0.945
0.9533833333333334 0.9525
0.9588166666666667 0.9563
0.963 0.959
0.9646166666666667 0.9617
0.9686 0.9634
0.9692166666666666 0.9643
0.97285 0.9666
0.97315 0.965
0.97585 0.9669
0.97745 0.9687
0.97925 0.9687
0.9807333333333333 0.9697

0개의 댓글