오답노트

[밑딥] 오차역전파법 본문

Python/DL

[밑딥] 오차역전파법

권멋져 2023. 2. 27. 21:44

수치 미분은 단순하고 구현하기도 쉽지만 계산 시간이 오래 걸린다는게 단점이다. 이번엔 가중치 매개변수의 기울기를 효과적으로 계산하는 '오차역전파법(backpropagation)'을 배워보자.

오차역전파법을 제대로 이해하는 방법으로는 수식을 통한 것과 계산 그래프를 통한 것이 있다. 전자 쪽이 일반적인 방법으로, 특히 기계학습을 다루는 책 대부분은 수식을 중심으로 이야기를 전개한다. 하지만 계산 그래프를 사용하면 시각적으로 이해할 수 있다.

 

계산 그래프

계산 그래프는 계산 과정을 그래프로 나타낸것이다. 여기서 그래프는 복수의 노드와 엣지(노드 사이의 직선)로 표현된다.

 

계산 그래프로 풀다

계산 그래프는 계산 과정을 노드와 화살표로 표현한다. 노드는 원으로 표기하고 원 안에 연산 내용을 적는다. 또, 계산 결과를 화살표 위에 적어 각 노드의 계산 결과가 왼쪽에서 오른쪽으로 전해지게 한다.

  1. 계산 그래프를 구성한다.
  2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.

여기서 2번째 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파라고 한다. 순전파는 계산 그래프의 출발점부터 종착점으로의 전파다. 역전파는 이것을 반대 방향의 전파이다. 역전파는 이후에 미분을 계산할 떄 중요한 역할을 한다.

 

국소적 계산

계산 그래프의 특징은 '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점이다. 국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다.

계산 그래프로 푸는 이유

계산 그래프의 이점으로는 국소적 계산이다. 그리고 다른 이점으로, 계산 그래프는 중간 계산 결과를 모두 보관할 수 있다. 그리고 가장 큰 이유는 역전파를 통해 미분을 효율적으로 계산할 수 있는 점이다.

연쇄법칙

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

 

계산 그래프의 역전파

국소적 미분은 순전파 떄의 y=f(x) 계산의 미분을 구한다는 것이며, 이는 x에 대한 y의 미분을 구한다는 뜻이다. 이 국소적인 미분을 상류에서 전달된 값에 곱해 앞쪽 노드로 전달하는 것이다.

이러한 방식을 따르면 목표로하는 미분 값을 효율적으로 구할 수 있다는 것이 전파의 핵심이다.

연쇄법칙이란?

함성 함수란 여러 함수로 구성된 함수다. 함성 함수의 미분은 합성 함수를 구하성하는 각 함수의 미분의 곱으로 나타낼 수 있다.

 

연쇄법칙과 계산 그래프

역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 국소적 미분을 곱한 후 다음 노드로 전달한다.

 

역전파

덧셈 노드의 역전파

왼쪽이 순전파 오른쪽이 역전파 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.

 

곱셈 노드의 역전파

곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 '서로 바꾼 값'을 곱해서 하류로 보낸다.

 

단순한 계층 구현하기

곱셈 계층

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
    dy = dout * self.x

    return dx, dy

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)

dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple,dapple_num = mul_apple_layer.backward(dapple_price)

print(dapple, dapple_num, dtax)

덧셈 계층

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

활성화 함수 계층 구현하기

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

Sigmoid 계층

import numpy as np

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

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

 

오차역전파법 구현하기

신경망 학습의 전체 그림

  • 전제 : 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 학습이라 한다.
  • 1단계 [미니배치] : 훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함수 값을 줄이는 것이 목표다.
  • 2단계 [기울기 산출] : 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.
  • 3단계 [매개변수 갱신] : 가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.
  • 4단계 [반복] : 1~3단계를 반복합니다.
from layers import *
from gradient import *
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['Aff1'] = Affine(self.params['W1'],self.params['b1'])
    self.layers['Relu1'] = Relu()
    self.layers['Aff2'] = 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
  
  def loss(self, x, t):
    y = self.predict(x)
    return self.lastLayer.forward(y, t)
  
  def accuaracy(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
  
  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['Aff1'].dW
    grads['b1'] = self.layers['Aff1'].db
    grads['W2'] = self.layers['Aff2'].dW
    grads['b2'] = self.layers['Aff2'].db

    return grads

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

수치 미분은 느리지만 오차역전파법을 정확히 구현했는지 확인하기 위해 필요하다. 수치 미분과 오차역전파법으로 구한 기울기가 거의 같음을 확인하는 작업을 기울기 확인이라고 한다.

 

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False, cache=False)

X = mnist.data.astype('float32')
y = mnist.target.astype('int64')

network = TwoLayerNet(784,50,10)
x_batch = X[:3]
t_batch = y[: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))

 

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

iters_num = 10000
train_size = 100
batch_size = 10
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[batch_mask]
  t_batch = y[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)

 

정리

  • 계산 그래프를 이용하면 계산 과정을 시각적으로 파악할 수 있다.
  • 계산 그래프의 노드는 국소적 계산으로 구성된다. 국소적 계산을 조합해 전체 계산을 구성한다.
  • 계산 그래프의 순전파는 통상의 계산을 수행한다. 한편, 계산 그래프의 역전파로는 각 노드의 미분을 구할 수 있다.
  • 신경망의 구성 요소를 계층으로 구현하여 기울기를 효율적으로 계산할 수 있다. (오차역전파법)
  • 수치 미분과 오차 역전파법의 결과를 비교하면 오차 역전파법의 구현에 잘못이 없는지 확인 할 수 있다. (기울기 확인)

'Python > DL' 카테고리의 다른 글

[밑딥] 합성곱 신경망 (CNN)  (0) 2023.02.27
[밑딥] 학습 관련 기술들  (0) 2023.02.27
[밑딥] 신경망 학습  (0) 2023.02.27
[밑딥] 신경망  (0) 2023.01.21
[밑딥] 퍼셉트론  (1) 2023.01.21