본문 바로가기

AI

Chapter 5. 오차역전파법

본 글은 "바닥부터 시작하는 딥러닝" 책을 바탕으로 작성되었습니다.


- Chapter4에서 신경망의 가중치 매개변수의 기울기 구할 때, 수치 미분을 사용하였다.

- 수치 미분은 구현이 쉽지만 계산 시간이 오래 거리는 단점이 있으므로,
   이번 Chapter5에서는 효율적으로 계산할 수 있는 '오차역전파법'에 대해서 학습한다.

5.1 계산 그래프

- 계산 그래프와 같이, 계산을 왼쪽에서 오른쪽으로 전파하는 단계를 "순전파"라고 한다.

- 순전파의 반대 방향의 단계를 "역전파"라고 한다.

- 계산 그래프의 이점은 순전파와 역전파를 활용하여, 각 변수의 미분을 효율적으로 구할 수 있다.

 

5.2 연쇄법칙

- 연전파는 '국소적인 미분'을 순반향과는 반대로 전달한다.
  (국소적: 자신과 직접 관계된 작은 범위)

- 국소적 미분을 전달하는 원리는 "연쇄법칙'이라고 한다.

- 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다. 이는 "연쇄법칙의 원리"이다.

  (합성함수: 여러 함수로 구성된 함수)

5.3 단순한 계층 구현하기

5.3.1 곱셈 계층

- 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  #x와 y를 바꾼다.
        dy = dout * self.x

        return dx, dy

- 아래는 사과 100원짜리 2개를 구매한 가격을 구하는 예제이다. (구매가격에는 부가세 10% 포함)

- backward()가 받는 인수는 '순전파의 출력에 대한 미분'이다.

apple = 100
apple_num = 2
tax = 1.1

#계층들
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

#순전파
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

print(f"forward: {price}")

#역전파
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print(f"backwoard: {dapple}, {dapple_num}, {dtax}")

그림 1. 곱셈계층 예제 결과

 

5.3.2 덧셈 계층

- 덧셈 계층을 구현하면 아래와 같다.

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

- 아래는 위에서 구현한 덧셈 계층의 코드를 이용하여, 사과 100원짜리 2개와 귤 150원짜리 3개의 값을 추출하는 코드이다

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)
print(dapple_num, dapple, dorange, dorange_num, dtax)

그림2. 덧셈계층 예시 결과

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

5.4.1 ReLU 계층

- 순전파 때 입력 값 x가 0보다 크면, 역전파는 상류 값을 그대로 하류로 보낸다.

- 순전파 때 입력 값 x가 0이하면, 역전파 때는 하류에 신호를 보내지 않고 0을 반환한다.

- mask라는 인스턴스 변수는 True/False로 구성된 넘파이 배열이다.

  순전파 입력인 x의 원소 값이 0 이하인 인덱스는 True, 그 외 0보다 큰 원소는 False로 유지한다.

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

- 아래는 mask변수에 대해 이해하기 쉽게 확인한 부분이다.

- 아래 결과와 같이, x가 0이하면 True값으로 설정되는 것을 알 수 있다.

- 이를 통해, mask 원소가 True인 곳에는 0으로 설정한다.

그림3. mask 변수에 대한 결과

5.4.2 Sigmoid 계층

- 시그모이드 계층의 역전파는 순전파의 출력 y로만 계산할 수 있다. (책 참고)

- 시그모이드 계층을 파이썬으로 구현하면 아래와 같다.

- 아래 구현은 순전파의 출력을 인스턴스 변수 out에 보관했다가, 역전파 계산 때, 그 값을 사용한다.

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

 

5.5 Affine/Softmax 계층 구현하기

5.5.1 Affine 계층

- 순전파 편향 덧셈은 각각의 데이터에 더해진다. 이에 대한 예제는 아래와 같다.

그림 4. 순전파 편향 예제

- 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여져야되는데, 이는 아래와 같다.

- 편향의 역전파는 데이터에 대한 미분을 각 데이터마다 더해서 구한다.

그림5. 역전파 편향 예제

 - 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.5.2 Softmax-with-Loss 계층

- softmax-with-loss 계층은 소프트맥스 계정을 구현하고, 손실함수인 교차 엔트로피 오차도 포함하여 구현한다는 의미이다

- 역전파 때는 전파하는 값을 배치 수로 나눠, 데이터 1개당 오차를 앞 계층으로 전파한다.

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

5.6 오차역전파법 구현하기

- 지금까지 구현한 계층을 조합하여, 신경망을 구축한다.

5.6.1 신경망 학습의 전체 그림

- 전제 -> 미니배치 -> 기울기 산출 -> 매개변수 갱신 -> 반복

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

- OrderedDict에 신경망의 계층을 보관하는데, 이는 순서 있는 딕셔너리이다.

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

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

- 지금까지 기울기는 구하는 방법은  수치 미분을 사용하는 방법 또는 해석적으로 수식을 풀어 구하는 방법이 있었다.

- 해석적 방법은 오차역전파법을 이용하여, 매개변수가 많아도 효율적으로 계산할 수 있다.

- 수치 미분으로 느리지만, 오차역전파법이 제대로 구현되었는지 확인하기 위해서는 필요하다.

   (수치 미분은 구현이 쉽지만, 오차역전파법은 복잡해서 종종 실수가 발생한다.)

- 수치 미분 결과와 오차역전파법의 결과를 비교해서, 기울기가 일치하는지 확인하는 작업을 "기울기 확인"이라고 한다.

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

그림 6. 기울기 검증 결과

 

5.6.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.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)

그림 7.오차역전파법 학습 결과

'AI' 카테고리의 다른 글

Building Systems with the ChatGPT API  (0) 2024.08.03
Chapter 6. 학습 관련 기술들  (0) 2024.07.13
Chapter 4. 신경망 학습  (0) 2024.06.23
Chapter 3. 신경망  (0) 2024.06.18
Chapter2. 퍼셉트론  (1) 2024.06.17