[밑시딥 1] Chapter 5. 오차역전파법

Seong Woong Kim·2023년 11월 20일

신경망의 가중치 매개변수에 대한 손실 함수의 기울기를 수치 미분을 사용해 구했다.

수치 미분은 단순하고 구현하기 쉽지만, 계산 시간이 오래 걸린다는 점이 단점이다.

이번 챕터에서는 가중치 매개변수의 기울기를 효율적으로 계산하기 오차역전파backpropagation^{backpropagation}을 다뤄본다.

5.1 계산 그래프

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

  1. 수식을 통한 것이고
  2. 다른 하나는 계산 그래프를 통한 것이다.

계산 그래프computational  graph^{computational\;graph}는 계산 과정을 그래프로 나타낸 것이다.

  • 여기서 그래프는 그래프 자료구조로, 복수의 노드node^{node}에지edge^{edge}로 표현됨

5.1.1 계산 그래프로 풀다

계산 그래프 예제

현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다.

이때 지불 금액을 구하자.

단, 소비세가 10% 부과된다.

답 : 사과의 개수와 소비세를 변수로 취급해 원 밖에 표기

문제 2

현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다.

사과는 1개에 100원, 귤을 1개에 150원이다.

소비세가 10% 일 때 지불 금액을 고르자.

답: 계산 그래프로 풀어본 문제 2의 답


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

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

2번에 해당하는 단계를 순전파forward  propagation^{forward\;propagation}라고 한다.

  • 순전파는 계산 그래프의 출발점부터 종착점으로의 전파이다.

이것에 반대되는 계산 방법을 역전파라고 한다.

  • 역전파는 이후에 미분을 계산할 때 중요한 역할을 합니다.

5.1.2 국소적 계산

계산 그래프의 특징은 '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있다.

  • 국소적이란 자신과 직접 관계된 작은 범위라는 뜻

국소적 계산은 결국, 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.

계산 그래프의 각 노드에서의 계산은 국소적이다.

계산 그래프는 국소적 계산에 집중한다.

전체 계산이 아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 국소적 계산이다.

국소적인 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 할 수 있다.


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

계산 그래프의 이점

  1. 국소적 계산
  2. 계산 그래프는 중간 계산 결과를 모두 보관할 수 있다.

계산 그래프를 사용하는 가장 큰 이유는 역전파를 통해 '미분'을 효율적으로 계산할 수 있다는 점에 있다.

다음 그림처럼 계산 그래프 상의 역전파에 의해서 미분을 구할 수 있다.

역전파는 순전파와는 반대 방향의 굵은 화살표로 그린다.

  • 이 전파는 국소적 미분을 전달하고, 그 미분 값은 화살표의 아래에 적는다.

이 결과로부터 사과 가격에 대한 지불 금액의 미분 값은 2.2라 할 수 있다.

  • 사과가 1원 오르면 최종 금액은 2.2원 오른다는 의미이다.

여기에서는 사과 가격에 대한 미분만 구했지만, 소비세에 대한 지불 금액의 미분이나, 사과 개수에 대한 지불 금액의 미분도 같은 순서로 구할 수 있다.

그 때는 중간까지 구한 미분 결과를 공유할 수 있어서 다수의 미분을 효율적으로 계산할 수 있다.

이처럼 계산 그래프의 이점은, 순전파와 역전파를 활용해서 각 변수의 미분을 효율적으로 구할 수 있다는 것이다.



5.2 연쇄법칙

국소적 미분을 전달하는 원리는 연쇄법칙chain  rule^{chain\;rule}에 따른 것이다.


5.2.1 계산 그래프의 역전파

다음 그림은 y=f(x)y = f(x)라는 계산의 역전파이다.

  • 역전파의 계산 절차는 신호 EE에 노드의 국소적 미분(yx)(\frac{\partial y}{\partial x})을 곱한 후 다음 노드에 전달하는 것이다.
    • 여기서 말하는 국소적 미분은 순전파 때의 y=f(x)y = f(x) 계산의 미분을 구한다는 것이며, 이는 xx에 대한 yy의 미분 (yx)(\frac{\partial y}{\partial x}) 을 구한다는 뜻이다.
  • 국소적인 미분을 상류에서 전달된 값에 곱해 앞쪽 노드로 전달하는 것
  • 이것이 역전파의 계산순서이다. 왜 그런 일이 가능한지는 연쇄 법칙으로 설명이 가능하다.

5.2.2 연쇄법칙이란?

연쇄법칙을 합성 함수의 개념이 필요하다.

  • 합성 함수란 여러 함수로 구성된 함수이다.

연쇄법칙은 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의된다.

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

  • 이것이 연쇄법칙의 원리이다.
  • zx\frac{\partial z}{\partial x} (xx에 대한 zz의 미분)은 zt\frac{\partial z}{\partial t} (tt에 대한 zz의 미분)과 tx\frac{\partial t}{\partial x} (xx에 대한 tt의 미분)의 곱으로 표현할 수 있다.

5.2.3 연쇄법칙과 계산 그래프

위 식의 연쇄법칙 계산을 계산 그래프로 나타내면 다음과 같다.

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

왼쪽 그림에서 맨 왼쪽 역전파 수식을 보면

zzzttx=zttx=zx\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x} = \frac{\partial z}{\partial x}

가 성립되어 x에 대한 z에 대한 미분이 된다.

즉, 역전파가 하는 일은 연쇄 법칙의 원리와 같다.



5.3 역전파

앞에서 계산 그래프의 역전파가 연쇄법칙에 따라 진행되는 모습을 설명했다.

이번 절에서는 +, x 등의 연산을 예로 들어 역전파의 구조를 설명한다.


5.3.1 덧셈 노드의 역전파

z=x+yz = x + y라는 식을 대상으로 역전파를 살펴본다.

  • z=x+yz = x + y의 미분은 다음과 같이 해석할 수 있다.
zx=1,zy=1\frac{\partial z}{\partial x} = 1,\quad \frac{\partial z}{\partial y} = 1

위 식에서와 같이 모두 1이 된다.

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

상류에서 전해진 미분값이 Lz\frac{\partial L}{\partial z}인 이유는 최종적으로 LL이라는 값을 출력하는 큰 계산 그래프를 가정하기 때문이다.

  • 위의 z=x+yz = x + y 계산은 큰 계산 그래프의 중간 어딘가에 존재한다고 가정했기 때문에, 이 계산 그래프 상류에서부터 Lz\frac{\partial L}{\partial z}가 전해졌다고 가정

구체적인 예는 다음과 같다.


5.3.2 곱셉 노드의 역전파

z=xyz = xy라는 식을 생각해볼 때, 이 식의 미분은 다음과 같다.

zx=y,zy=x\frac{\partial z}{\partial x} = y,\quad \frac{\partial z}{\partial y} = x

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

그 구체적인 예를 살펴보면 다음과 같다.

덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값은 필요하지 않았다.

곱셈의 역전파는 순방향 입력 신호의 값이 필요하다. 그렇기 때문에 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해둔다.


5.3.3 사과 쇼핑의 예

사과가격, 사과개수, 소비세라는 세 변수 각각이 최종 금액에 어떻게 영향을 주는가??

사과가격에 대한 지불 금액의 미분 등 3가지 변수에 대한 지불금액의 미분 구하기

사과가격 미분 2.2 / 사과 개수 미분 110 / 소비세 미분 200 만큼 최종금액에 영향을 미친다.

  • 사과 가격과 소비세의 단위가 다르므로 주의해야함


5.4 단순한 계층 구현하기

위 사과쇼핑 예제를 구현한다.

  • 곱셈노드 'MulLayer' / 덧셈 노드 'AddLayer'

💡 신경망을 구성하는 '계층' 가각을 하나의 클래스로 구현한다.

  • 계층이란 신경망의 기능 단위

5.4.1 곱셈 계층

모든 계층은 forward()와 backward()라는 공통의 메서드를 갖는다.

  • forward()는 순전파, backward()는 역전파를 처리한다.
# 곱셈 계층 구현
class MulLayer:
  # init()을 통해 변수 x,y를 초기화 한다. 
  # 이 변수들은 순전파 시의 입력값을 유지하기 위해 사용한다
  def __init__(self):
    self.x = None
    self.y = None
 
  # 순전파
  # x,y를 입력받고 두 값을 곱해서 반환
  def forward(self, x, y):
    self.x = x
    self.y = y
    out = x * y
 
    return out
 
  # 역전파
  def backward(self, dout):
  	# 상류에서 넘어온 미분(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

여기에서 주의할 점은 backward()의 호출 순서는 forward() 때와는 반대이다.

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


5.4.2 덧셈 계층

class AddLayer:
    def __init__(self):
        pass  # 덧셈 계층에는 초기화 필요없음

	# 입력받은 두 인수 x,y를 더해서 반환
    def forward(self, x, y):
        out = x + y
        return out

	# 상류에서 내려온 미분(dout)을 그대로 하류로 흘린다
    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1
        return dx, dy

AddLayer를 통해서 사과 문제를 풀면 다음과 같다.

# 덧셈 계층과 곱셈 계층 활용해 사과문제 풀이
# 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

필요한 계층을 만들어 순전파 메서드인 forward()를 적절한 순서로 호출한다.

그 다음 순전파와 반대 순서로 역전파 메서드인 backward()를 호출하면 원하는 미분을 얻을 수 있다.



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

여기에서는 신경망을 구성하는 층(계층) 각각을 크래스 하나로 구현한다.

  • ReLUSigmoid 구현

5.5.1 ReLU 계층

활성화 함수로 사용되는 ReLU의 수식과 이를 미분한 식은 다음과 같다.

위 식에서와 같이 순전파 때의 입력인 xx0보다 크면 역전파는 상류의 값을 그대로 하류로 흘린다.

반면, 순전파 때 xx0이라면 역전파 때는 하류로 신호를 보내지 않는다 (0을 보낸다.)

계산 그래프로는 다음과 같이 나타낼 수 있다.

# ReLU 계층 코드 구현
class Relu:
    def __init__(self):
    	# True/False로 구성된 넘파이 배열 / 인스턴스 변수
        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으로 설정한다.

import numpy as np
 
x = np.array( [[1.0, -0.5], [-2.0, 3.0]])
print(x)
>>> 
[[ 1.  -0.5]
 [-2.   3. ]]

mask = (x <= 0)
print(mask)
>>>
[[False  True]
 [ True False]]

5.5.2 Sigmoid 계층

시그모이드 함수를 식과 계산그래프로 나타내면 다음과 같다.

exp/ 노드가 새롭게 등장했다.

  • exp 노드는 y=exp(x)y=exp(x) 계산을 수행하고
  • / 노드는 y=1xy=\frac{1}{x} 계산을 수행한다.

시그모이드의 역전파 흐름을 오른쪽에서 왼쪽으로 한 단계씩 살펴보자

1단계

/ 노드, 즉 y=1xy=\frac{1}{x}를 미분하면 다음 식이 된다.

역전파 때는 상류에서 흘러온 값에 y2-y^2 (순전파의 출력을 제곱한 후 -를 붙인 값)을 곱해서 하류로 전달한다.

계산 그래프에서는 다음과 같다.

2단계

+ 노드는 상류의 값을 여과 없이 하류로 내보내는 것이 다이다.

3단계

exp 노드는 y=exp(x)y = exp(x) 연산을 수행하며, 그 미분은 다음과 같다.

yx=exp(x)\frac{\partial y}{\partial x} = exp(x)

계산 그래프에서는 상류의 값에 순전파 때의 출력(이 예에서는 exp(x)exp(-x)을 곱해 하류로 전달한다.

4단계

x 노드는 순전파 때의 값을 서로 바꿔서 곱한다.

이상으로 Sigmoid 계층의 역전파를 계산 그래프로 완성했다.

Lyy2exp(x)\frac{\partial L}{\partial y}y^2exp(-x)를 순전파의 입력 xx와 출력 yy만으로 계산할 수 있다.

간소화하면 다음과 같다.

Sigmoid 계층의 역전파는 순전파의 출력(y) 만으로 계산할 수 있다.

이 Sigmoid 계층을 구현하면 다음과 같다.

# sigmoid 파이썬 구현
class Sigmoid:
    def __init__(self):
        self.out = None
 
    def forward(self, x):
        out = sigmoid(x)  # 순전파의 출력을 out에 보관한 후 역전파 계산 때 그 값을 사용
        self.out = out
        return out
 
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
 
        return dx


5.6 Affine/Softmax 계층 구현하기

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

  • 이 책에서는 어파인 변환을 수행하는 처리를 'Affine 계층' 이라는 이름으로 구현한다.

Affine 계층의 계산 그래프는 다음과 같다.

행렬을 사용한 역전파도 행렬의 원소마다 전개해보면 스칼라값을 사용한 지금까지의 계산 그래프와 같은 순서로 생각할 수 있다.

Affine 계층의 역전파를 계산그래프로 나타내면 다음과 같다.

  • X=LXX = \frac{\partial L}{\partial X} 같은 형상, W=LWW = \frac{\partial L}{\partial W} 같은 형상임에 주의

5.6.2 배치용 Affine 계층

  • 데이터 NN개를 묶어 순전파하는 경우, 즉 배치용 Affine 계층을 알아보자.

기존과 다른 부분은 XX의 형상이 (N,2)로 변한 것이다.

편향 덧셈 주의 : 순전파 때의 편향 덧셈은 XWX\cdot W에 대한 편향이 각 데이터에 더해짐

  • ex) N=2N = 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]]

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

  • 그래서 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 한다.
# 역전파 때의 편향 덧셈
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개(NN=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 함수는 입력 값을 정규화 (출력의 합이 1이 되도록 변형)하여 출력한다.

예를 들어 손글씨 숫자 인식에서의 Softmax 계층의 출력은 다음과 같다.

💡
신경망에서 수행하는 작업은 학습추론 두 가지이다.

  • 추론할 때에는 일반적으로 Softmax 계층을 사용하지 않는다. 위 그림의 의 신경망은 추론할 때는 마지막 Affine 계층의 출력을 인식 결과로 이용한다.
  • 또한 신경망에서 정규화하지 않은 출력 결과(Softmax 앞의 Affine 계층의 출력)를 점수score^{score}라 한다.
    • 즉, 신경망 추론에서 답을 하나만 내는 경우에는 가장 높은 점수만 알면 되므로 Softmax 계층은 필요 없다는 것이다.
  • 반면 신경망을 학습할 때에는 Softmax 계층이 필요하게 된다.

Softmax-with-Loss 계층의 계산 그래프를 살펴보면 다음과 같다.

간소화한 계산 그래프는 다음과 같다.

그림과 같이 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)를 받고, 이 데이터들로부터 손실 LL을 출력한다.

여기서 주목할 것은 역전파의 결과이다.

Softmax 계층의 역전파는 (y1t1,  y2t2,  y3t3)(y_1-t_1,\; y_2-t_2, \;y_3-t_3)이라는 '말끔한' 결과를 내놓는다.

  • (y1,y2,y3)(y_1, y_2, y_3)는 Softmax 계층의 출력이고 (t1,t2,t3)(t_1, t_2, t_3)는 정답 레이블이므로 (y1t1,  y2t2,  y3t3)(y_1-t_1,\; y_2-t_2,\; y_3-t_3)는 Softmax 계층의 출력과 정답 레이블의 차분인 것이다.

  • 신경망의 역전파에서는 이 차이인 오차가 앞 계층에 전해지는 것이다.

  • 이는 신경망 학습의 중요한 성질이다.

그런데 신경망 학습의 목표는 신경망의 출력(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)로 깔끔히 떨어진다.

  • 우연이 아니라, 교차 엔트로피 오차라는 함수가 그렇게 설계된 것.

회귀의 출력층에서 사용하는 '항등 함수'의 손실 함수로 '오차제곱합'을 이용하는 것도 이와 같다.

  • (y1t1,  y2t2,  y3t3)(y_1-t_1,\; y_2-t_2,\; y_3-t_3)로 역전파의 결과가 깔끔히 떨어짐.

이 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
  • 역전파 때는 전파하는 값을 배치의 수 (batch_size)로 나눠서 데이터 1개당 오차를 앞 계층에 전파한다.


5.7 오차역전파법 구하기

5.7.1 신경망 학습의 전체 그림

신경망 학습의 순서는 다음과 같다.

전제

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

1단계-미니배치

  • 훈련 데이터 중 일부를 무작위로 가져오고 이를 미니배치라 한다.

  • 미니배치의 손실 함수 값을 줄이는 것이 목표이다.

2단계-기울기 산출

  • 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다.

  • 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.

3단계-매개변수 갱신

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

4단계-반복

  • 1~3단계를 반복한다

오차역전파법이 등장하는 단계는 2단계기울기 산출이다.

  • 수치 미분은 구현하기 쉽지만 계산이 오래 걸렸다.
  • 오차역전파법을 이용하면 느린 수치 미분과 달리 기울기를 효율적이고 빠르게 구할 수 있다.

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

2층 신경망을 TwoLayerNet 클래스로 구현하고 주요 인스턴스 변수와 메서드를 정리하면 다음과 같다.

코드로 구현하면 다음과 같다.


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 오차역전파법으로 구한 기울기 검증하기

지금까지 기울기를 구하는 방법을 두 가지 설명했다.

  1. 수치 미분을 써서 구하는 방법
  2. 해석적으로 수식을 풀어 구하는 방법

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

수치 미분의 이점은 구현하기 쉽고 오차가 적다. 오차역전파법은 구현하기 복잡해서 오차가 종종 나오기 때문에 둘을 비교하여 오차역전파법을 제대로 구현했는지 검증한다.

이렇게 두 방식으로 구한 기울기가 일치하는지 확인하는 작업을 기울기 확인gradient  check^{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


5.8 정리

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

0개의 댓글