본문 바로가기
Minding's Reading/밑바닥부터 시작하는 딥러닝

[밑바닥부터 시작하는 딥러닝] CH.5 오차역전파법

by Minding 2021. 5. 9.
728x90
반응형

- 데이터셋 다운로드

github.com/WegraLee/deep-learning-from-scratch

 

WegraLee/deep-learning-from-scratch

『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017). Contribute to WegraLee/deep-learning-from-scratch development by creating an account on GitHub.

github.com


5. 오차역전파법

  • 가중치 매개변수의 기울기를 효율적으로 계산

오차역전파법을 제대로 이해하는 두 가지 방법

  • 수식을 통해
  • 계산 그래프를 통해

5.1 계산 그래프

  • 계산그래프 (computational graph)는 계산 과정을 그래프로 나타낸 것
  • 그래프는 복수의 노드(node)와 에지(edge)로 표현된다.

5.1.1 계산 그래프로 풀다

  • 문제 1 : 현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다. 이 때 지불금액은? (단, 소비세 10% 부과됨)
  • 문제 2 : 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다. 사과는 1개 100원, 귤은 1개에 150원, 소비세 10%일때 지불금액은?

계산 그래프를 이용한 문제풀이는 다음 흐름으로 진행됨

  • 계산 그래프를 구성한다.
  • 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다. (순전파)
  • 순전파는 계산그래프의 출발점부터 종착점으로의 전파
  • 역전파 : 종착점(오른쪽)에서 출발점(왼쪽)으로의 전파 (미분계산시 중요역할)

5.1.2 국소적 계산

  • 계산 그래프의 특징은 국소적 계산을 전파함으로써 최종 결과를 얻음
  • 국소적 = 자신과 직접 관계된 작은 범위
  • 국소적 계산 = 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것
  • 계산 그래프는 각 노드에서 국소적계산을 하므로 전체 계산이 아무리 복잡하더라도 단순하게 처리가능

5.1.3 왜 계산 그래프로 푸는가?

  • 계산 그래프의 이점
  1. 국소적 계산
  2. 중간 계산 결과를 모두 보관할 수 있음.
  3. 계산 그래프는 '역전파'를 통해 미분을 효율적으로 계산할 수 있음

  • 문제1을 예시로 한 설명
    • 역전파는 순전파와 반대 방향의 굵은 화살표 그림
    • 역전파는 국소적 미분 전달하고, 미분 값은 화살표의 아래에 표시
    • 사과가 1원 오르면 최종 금액은 2.2원 오른다는 의미
    • 소비세에 대한 지불 금액의 미분이나 사과 개수에 대한 지불 금액의 미분도 같은 순서로 구할 수 있음
    • 중간까지 구한 미분 결과 공유 가능하여 다 수 미분 효율적 계산 가능
    • 계산 그래프의 이점은 순전파와 역전파를 활용해 각 변수의 미분을 효율적으로 구할 수 있음

 

5.2 연쇄법칙

  • 국소적 미분을 전달하는 원리는 연쇄법칙(chain rule)에 따른 것

5.2.1 계산 그래프의 역전파

  • y = f(x)의 계산 역전파
    • 역전파의 계산 절차는 신호 E에 노드의 국소적미분(ay/ax)을 곱한 후 다음노드로 전달
    • 국소적 미분은 순전파 때의 y = f(x) 계산의 미분을 구하는 것 즉, x에 대한 y의 미분을 구한다는 뜻
    • 국소적인 미분을 상류에서 전달된 값에 곱해 앞쪽 노드로 전달하는 것
    • 이 것이 역전파의 계산순서, 왜 그런 일이 가능한지는 연쇄 법칙으로 설명가능

5.2.2 연쇄법칙이란?

  • 합성 함수 : 여러함수로 구성된 함수
  • 연쇄법칙은 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의
    • 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.

5.2.3 연쇄법칙과 계산 그래프

 

5.3 역전파

  • 더하기, 곱하기 등의 연산을 예로 들어 역전파의 구조 살펴봄

5.3.1 덧셈 노드의 역전파

  • 덧셈노드의 역전파는 입력값을 그대로 흘려보냄

  • 상류에서 전해진 미분값이 aL/az인 이유는 최종적으로 L이라는 값을 출력하는 큰 계산 그래프를 가정하기 때문
  • 위의 z = x + y 계산은 큰 계산 그래프의 중간 어딘가에 존재한다고 가정했기 때문에, 이 계산 그래프 상류에서부터 aL/az가 전해졌다고 가정

5.3.2 곱셈 노드의 역전파

  • 곱셈노드 역전파는 상류의 값에 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보냄
  • 덧셈과 달리 곱셈 노드 구현시에는 순전파의 입력 신호를 변수에 저장해 둠

5.3.3 사과 쇼핑의 예

  • 사과가격, 사과개수, 소비세라는 세 변수 각각이 최종 금액에 어떻게 영향을 주느냐
  • 사과가격에 대한 지불 금액의 미분 등 3가지 변수에 대한 지불금액의 미분 구하기
  • 사과가격 미분 2.2 / 사과 개수 미분 110 / 소비세 미분 200 만큼 최종금액에 영향
  • 사과 가격과 소비세의 단위가 다르므로 주의해야함

5.4 단순한 계층 구현하기

  • 사과쇼핑 파이썬 구현
  • 곱셈노드 'MulLayer' / 덧셈 노드 'AddLayer'

5.4.1 곱셈 계층

# 곱셈 계층 구현
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

 

  • MulLayer 사용하여 사과구매 순전파 구현
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(price)

>>> 220.00000000000003
# 각 변수에 대한 미분 - backward()에서 구할 수 있음
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print(dapple, dapple_num, dtax)

>>> 2.2 110.00000000000001 200

 

5.4.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
# 덧셈 계층과 곱셈 계층 활용해 사과문제 풀이
# from layer_naive import *

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)

>>>
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650

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

  • 활성화함수 ReLU와 Sigmoid 구현

5.5.1 ReLU 계층

  • ReLU 계층의 식

  • x에 대한 y의 미분의 식

 

  • 순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그래돌 하류로 흘림
  • 순전파 때 x가 0 이하면 역전파 때는 하류로 신호를 보내지 않음 (0을 보냄)
  • ReLU 계층의 계산그래프

# 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
  • Relu 클래스는 mask라는 인스턴스 변수 가짐
  • mask는 True / False로 구성된 넘파이 배열
  • 순전파 입력인 x의 원소값이 0 이하면 True, 아니면 False
  • 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정
import numpy as np

x = np.array( [[1.0, -0.5], [-2.0, 3.0]])
print(x)
mask = (x <= 0)
print(mask)

>>>
[[ 1.  -0.5]
 [-2.   3. ]]
[[False  True]
 [ True False]]

5.5.2 Sigmoid 계층

  • 시그모이드 함수의 식

  • 시그모이드 함수 계산그래프 
  • exp 노드는 y = exp(x) 계산 수행
  • / 노드는 y = 1/x 계산 수행

sigmoid 계층 역전파 한단계씩 짚어보기

  1. /노드 (y = 1/x)를 미분
  • ay/ax = -(1/x^2) = -y^2
  • 역전파 때는 상류에서 흘러온 값에 -y^2을 곱함
  • -y^2 = 순전파의 출력을 제곱한 후 마이너스를 붙인 값

    2. +노드 : 상류값 여과없이 하류로 보냄

    3. exp노드는 exp(x) 연산 수행

  • 미분 : ay/ax = exp(x)
  • 상류의 값에 순전파 때의 출력 (계산그래프에서는 exp(-x))을 곱해 하류로 전파

    4. X 노드 : 순전파 때의 값을 서로 바꿔 곱

  • 예시에서는 -1 곱
  • 역전파의 최종출력인 (aL/ay)*y^2exp(-x)가 하류 노드로 전파
  • 역전파 최종출력은 순전파의 입력 x와 출력 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 계층 구현하기

5.6.1 Affine 계층

  • 신경망의 순전파때 수행하는 행렬곱은 기하학에서 어파인 변환이라고 함 = Affine계층으로 구현
  • Affine 계층의 계산 그래프

5.6.2 배치용 Affine 계층

  • 데이터 N개를 묶어 순전파 하는 경우
  • 배치용 Affine 계층 계산그래프

  • 기존과 다른 부분은 X의 형상이 (N,2)로 변한 것
  • 편향 덧셈 주의 : 순전파 때의 편향 덧셈은 X*W에 대한 편향이 각 데이터에 더해짐
  • ex) N = 2일때, 편향은 그 두 데이터 각각의 계산결과에 더해짐
import numpy as np

# 순전파 때의 편향 덧셈
X_dot_W = np.array([[0,0,0], [10,10,10]])
B = np.array([1,2,3])

print(X_dot_W)
print(X_dot_W + B)

>>>
[[ 0  0  0]
 [10 10 10]]
[[ 1  2  3]
 [11 12 13]]
  • 순전파의 편향 덧셈이 각각의 데이터에 더해지기 때문에 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 함
# 역전파 때의 편향 덧셈
dY = np.array([[1,2,3],[4,5,6]])
print(dY)
dB = np.sum(dY, axis=0)
print(dB)

>>>
[[1 2 3]
 [4 5 6]]
[5 7 9]
  • 예시에서는 데이터가 2개(N=2)
  • 편향의 역전파는 그 두 데이터에 대한 미분을 데이터마다 더해서 구함
# 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 계층(layer)을 사용하지 않는다. Softmax 계층 앞의 Affine 계층의 출력을 점수(score)라고 하는데, 딥러닝의 추론에서는 답을 하나만 예측하는 경우에는 가장 높은 점수만 알면 되므로 Softmax 계층이 필요없다. 반면, 딥러닝을 학습할 때는 Softmax 계층이 필요하다.
  • softmax 계층의 출력 그림

  • softmax 계층은 입력 값을 정규화 (출력의 합이 1이 되도록 변형)
  • softmax 계층과 손실함수 교차 엔트로피 포함하여 softmax-with-loss계층 구현
  • softmax-with-loss 계층의 계산 그래프 
# Softmax-with-Loss 계층 구현
# common / layer.py

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 오차역전파법 구현하기

5.7.1 신경망 학습의 전체 그림

  • 전제 : 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라고 함
  1. 미니배치 : 훈련 데이터 중 일부를 무작위로 가져옴 (= 미니배치)
  2. 기울기 산출 : 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구함
  3. 매개변수 갱신 : 가중치 매개변수를 기울기 방향으로 아주 조금 갱신
  4. 반복 : 1~3단계를 반복

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

import sys, os
sys.path.append('/deep-learning-from-scratch')
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)

        # 계층 생성
        # OrderedDict = 순서가 있는 딕셔너리, 순서 기억
        # 순전파 때는 계층을 추가한 순서대로 / 역전파 때는 계층 반대 순서로 호출
        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

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

  • 느린 수치 미분보다 오차역전파법을 사용한 해석적 방법이 효율적 계산 가능
  • 수치 미분은 오차역전파법을 정확히 구현했는지 확인위해 필요
  • 수치 미분은 구현하기 쉬우나, 오차역전파법은 구현 복잡해 실수가 있을 수 있음
  • 기울기 확인(gradient check) : 두 방식으로 구한 기울기가 일치(거의 같음)함을 확인 하는 작업
# 기울기 확인 (gradient check)
import sys, os
sys.path.append('/deep-learning-from-scratch')
import numpy as np
from dataset.mnist import load_mnist
from ch05.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.1821647831167817e-10
b1:2.534937764494963e-09
W2:5.183343681548899e-09
b2:1.4008996131881224e-07

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

import sys, os
sys.path.append('/content/drive/MyDrive/deep-learning-from-scratch')

import numpy as np
from dataset.mnist import load_mnist
from ch05.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)
        
>>>
0.16111666666666666 0.1669
0.9040833333333333 0.9045
0.9236666666666666 0.9269
0.93625 0.9373
0.94525 0.944
0.9503833333333334 0.948
0.9547166666666667 0.951
0.9602166666666667 0.9569
0.9626333333333333 0.9588
0.9652166666666666 0.9598
0.9688 0.9619
0.9709833333333333 0.9641
0.9729 0.9653
0.9746166666666667 0.9667
0.97505 0.9663
0.97645 0.967
0.9784833333333334 0.9692
728x90
반응형

댓글