// 아래 내용은 [밑바닥부터 시작하는 딥러닝]으로 학습한 내용을 정리하였다.
오차역전파법(backpropagation)을 이용하면 가중치 매개변수의 기울기를 효율적으로 계산이 가능하다.
오차역전파법을 제대로 이해하는 두 가지 방법은 ① 수식을 통한 것과 ② 계산 그래프를 통한 것이다.
계산 그래프
계산 그래프(computational graph): 계산 과정을 그래프로 나타낸 것이다. 여기서 그래프는 그래프 자료구조로, 복수의 노드(node)와 에지(edge)로 표현된다.
계산 그래프로 계산
계산 그래프 표현하기: 계산 그래프는 계산 과정을 노드와 화살표로 표현한다. 노드는 원으로 표기하고, 원 안에는 연산 내용을 적는다. 계산 결과를 화살표 위에 적어 각 노드의 계산 결과가 왼 -> 오로 전해지도록 한다.
예제를 이용해 계산 그래프를 이해하기
문제 1) 현빈은 슈퍼에서 1개에 100원인 사과 2개를 샀다. 이때의 지불 금액은 얼마인가? ( 단, 소비세가 10% 부과된다.)
풀이)
계산 그래프로 풀면 다음과 같다.

위의 계산 그래프는 'x2'와 'x1.1'을 각각 하나의 연산으로 취급해 원 안에 표기했지만, 곱셈인 'x'만을 연산으로 생각할 수도 있다. 아래처럼 '2'와 '1.1'은 각각 '사과의 개수'와 '소비세' 변수가 되어 원 밖에 표기하게 된다.

문제 2) 현빈은 슈퍼에서 사과 2개, 귤 3개를 샀다. 사과는 1개에 100원, 귤은 1개에 150원이다. 소비세가 10%일 때 지불 금액은 얼마인가?
풀이)
계산 그래프로 풀면 다음과 같다.

여기서는 덧셈 노드인 '+'가 새로 등장해 사과와 귤의 금액을 환산한다.
이 두 가지 예를 통해 계산 그래프를 이용한 문제 풀이의 흐름을 정리할 수 있다.
① 계산 그래프를 구성한다.
② 그래프에서 계산을 왼 -> 오로 진행한다. => 순전파(forward propagation)
(이와 반대로 계산을 오 -> 왼으로 진행하면 역전파(backward propagation)이라고 하며 미분 계산 시에 중요한 역할을 한다.)
국소적 계산
계산 그래프의 특징은 국소적 계산을 전파함으로써 최종 결과를 얻는 다는 점이다.
국소적 계산: 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 출력이 가능하다.
계산 그래프에서 각 노드는 자신과 관련한 계산 이외에는 아무 것도 신경 쓸 것이 없다. 전체 계산이 아무리 복잡해도 각 단계에서 하는 일은 해당 노드의 국소적 계산이며, 이는 단순하지만 그 결과를 전달함으로써 복잡한 전체 계산을 가능하게 한다.
왜 계산 그래프인가
계산 그래프의 이점은 다음과 같으며, 이와 같은 이유로 계산 그래프를 사용한다.
① 국소적 계산을 이용한다.
② 중간 계산 결과를 모두 보관 및 공유가 가능하기 때문에 다수의 미분을 효율적으로 계산할 수 있다.
③ 순전파와 역전파를 통해 각 변수의 미분을 효과적으로 계산할 수 있다.
연쇄법칙
역전파에서 국소적 미분을 순방향과 반대로 전달하는 원리는 연쇄법칙(chain rule)에 따른 것이다.
계산 그래프의 역전파

y = f(x)라는계산 그래프의 역전파를 그림으로 표현하면 다음과 같다.

역전파의 계산 절차는 신호 E에 국소적 미분을 곱한 후, 다음 노드로 전달하는 것이다.
여기서 역전파의 핵심은 위의 방식을 따르면 목표로 하는 미분 값을 효율적을 구할 수 있다는 점이다.
연쇄법칙이란
합성함수: 여러 함수로 구성된 함수
연쇄법칙은 합성함수의 미분에 대한 성질로, 합성함수를 구성하는 각 함수의 미분의 곱으로 표현이 가능하다.
연쇄 법칙의 원리의 예를 다음과 같은 식의 전개로 이해할 수 있다.

연쇄법칙과 계산 그래프
위에서 구한 국소적 미분 식의 연쇄법칙 계산을 계산 그래프로 나타내면 아래와 같다.

- 계산 그래프의 역전파는 오 -> 왼으로 신호를 전파한다.
- 노드로 들어온 입력 신호에 그 노드의 국소적 미분(편미분)을 곱한 후, 다음 노드로 전달한다.

- 맨 왼쪽 역전파를 통해 역전파가 하는 일 = 연쇄법칙의 원리 임을 알 수 있다.
위의 계산 그래프에 위에서 구한 국소적 미분(식 5.3)의 결과를 대입하면 아래와 같다.

역전파
1) 덧셈 노드의 역전파
ex) z = x + y를 예로 역전파를 설명한다
① z = x + y의 미분 계산

② 계산 그래프로 표현


이와 같이 역전파 때는 상류에서 전해진 미분에 1을 곱해 하류로 흘린다. 즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐 입력된 값을 그대로 다음 노드로 보내게 되는 것이다.

2) 곱셈 노드의 역전파
ex) z = xy를 예로 역전파를 설명한다
① z = xy의 미분 계산

② 계산 그래프로 표현

곱셈 노드 역전파는 상류 값에 순전파 떄의 입력 신호들을 서로 바꾼 값을 곱해 하류로 보낸다. 여기서 서로 바꾼 값이란 순전파 때 x 였다면 역전파에서는 y, 순전파 때 y였다면 역전파에서는 x로 바꾼다는 의미이다.
3) 사과 쇼핑의 예
풀이 point!
- 사과의 가격, 사과의 개수, 소비세라는 세 변수 각각이 최종 금액에 어떻게 영향을 주는 가?
풀이) 계산 그래프의 역전파 사용

* 사과 가격의 미분 = 2.2
* 사과 개수의 미분 = 110
* 소비세의 미분 = 200
=> 소비세와 사과 가격이 같은 양 만큼 오르면 최종 금액에는 소비세가 200의 크기로, 사과 가격이 2.2 크기로 영향을 준다고 해석할 수 있다. (단, 이 예에서 소비세와 사과 가격은 단위가 다르니 주의!)
단순한 계층 구현(사과 쇼핑의 예를 파이썬으로 구현)
곱셈 노드: MulLayer / 덧셈 노드: AddLayer
신경망을 구성하는 계층 각각을 하나의 클래스로 구현한다.
1) 곱셈 계층: MulLayer
모든 계층은 (순전파 처리)forward()와 (역전파 처리)backward()라는 공통의 메서드(인터페이스)를 갖도록 구현할 것이다.
class MulLayer:
def __init__(self): # 인스턴스 변수 x와 y 초기화
self.x = None
self.y = None
def forward(self, x, y): # 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
각 변수에 대한 미분은 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 200
backward()의 호출 순서는 forward()와는 반대이며, backward()가 받는 인수는 순전파의 출력에 대한 미분임을 주의하자. mul_apple_layer라는 곱셈 계층은 순전파 때는 apple_price를, 역전파 때는 그 미분값인 dapple_price를 인수로 받는다.
2) 덧셈 계층: AddLayer
class AddLayer:
def __init__(self): # 초기화가 필요 없으니 여기서는 아무 일도 하지 않음
pass # 아무 것도 하지 말라는 명령
def forward(self, x, y): # 입력받은 두 인수 x, y를 더해서 반환
out = x + y
return out
def backward(self, dout): # 상류에서 내려온 미분(dout)을 그대로 하류로 흘림
dx = dout * 1
dy = dout * 1
return dx, dy
덧셈 계층과 곱셈 계층을 사용해 사과 2개와 귤 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()
# 순전파_순전파 메서드인 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) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
계산 그래프에서의 계층은 쉽게 구현이 가능하며, 이를 사용해 복잡한 미분도 계산할 수 있다.
활성화 함수 계층 구현
1) ReLU 계층
활성화 함수로 사용되는 ReLU의 수식은 아래와 같다.

위 식에서 x에 대한 y의 미분은 아래처럼 구한다.

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

ReLU 계층을 구현해보자. 신경망 계층의 forward()와 backward() 함수는 넘파이 배열을 인수로 받는다고 가정한다.
class ReLU: # mask라는 인스턴스 변수 가짐
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 인스턴수 변수: True / False로 구성된 넘파이 배열로, 순전파의 입력인 x의 원소값이 0이하인 인덱스는 True, 그 외는 False로 유지한다.
순전파 때의 입력값이 0이하면 역전파 때의 값은 0이 돼야한다. 그래서 역전파 때는 순전파 때는 만들어둔 mask를 써서 mask의 원소가 True인 곳에서는 상류에서 전파된 dout을 0으로 설정한다.
2) Sigmoid 계층
시그모이드 함수의 수식은 다음과 같다.

시그모이드 계층의 순전파 계산 그래프는 아래처럼 나타낼 수 있다.

다음으로는 역전파의 흐름을 오 -> 왼으로 한 단계씩 살펴보자.
1단계)

2단계)

3단계)

4단계)
x노드는 순전파 때의 값을 서로 바꿔서 곱한다(이 예에서는 -1을 곱한다)
시그모이드 계층의 계산 그래프는 아래와 같이 표현할 수 있다.

위 계산 그래프의 중간 과정을 묶어 단순한 sigmoid 노드 하나로 대체할 수 있다.

위의 두 결과는 동일하지만, 간소화 버전은 역전파 과정의 중간 계산을 생략할 수 있어 더 효율적이다. 또한, 노드를 그룹화해 Sigmoid 계층의 세세한 내용을 노출하지 않고 입력과 출력에만 집중할 수 있다는 것도 중요한 point!

이처럼 Sigmoid 계층의 역전파는 순전파의 출력(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
Affine / Softmax 계층 구현
1) Affine 계층
신경망의 순전파에서는 가중치 신호의 총합을 계산하기에 행렬의 내적을 사용한다.
예를 들어 각각 형상이 (2, ), (2, 3), (3, )인 다차원 배열 X, W, B를 사용할 때, 뉴런의 가중치 합은 Y = np.dot(X, W) + B처럼 계산한다. 이 Y를 활성화 함수로 변환해 다음 층으로 전파하는 거이 신경망 순전파의 흐름이다.
어파인 변환(affine transformation): 기하학에서의 신경망의 순전파 때 수행하는 행렬의 내적이다.
다음은 행렬의 내적과 편향의 합을 계산 그래프로 표현한 것이다. 내적을 계산하는 노드를 dot이라 할 때, np.dot(X, W) + B 계산은 아래처럼 그려진다.

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

그렇다면 계산 그래프의 역전파을 구해보자.

각 그래프에서 각 변수의 형상에 주의해야 한다.
Why? 행렬의 내적에서는 대응하는 차원의 원소 수를 일치시켜야 하므로 이를 위해선 위의 역전파 전개식을 동원해야할 수도 있기 때문이다.
2) 배치용 Affine 계층: 데이터 N개를 묶어 순전파하는 경우
배치용 Affine 계층 계산 그래프는 다음과 같다.

기존과 다른 부분은 입력인 X의 형상이 (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
3) Softmax-with-Loss 계층
소프트맥스 함수는 입력값을 정규화해서 출력한다.
예를 들어서 손글씨 숫자 인식에서의 Softmax 계층이 출력을 살펴보자.

이 그림에서 숫자 0의 점수는 5.3이며, Softmax 계층에 의해 0.008로 변환된다. 그림을 보면 입력 이미지가 Affine 계층과 ReLU 계층을 통과하며 변환되고, 마지막 Softmax 계층에 의해 10개의 입력이 정규화됨을 알 수 있다.
Softmax-with-Loss 계층의 계산 그래프는 다음과 같다.

위의 복잡한 계산 그래프는 아래처럼 간소화 가능하다.

여기서는 3 클래스 분류를 가정하고 이전 계층에서 3개의 입력을 받는다.
Softmax 계층은 입력 (a1, a2, a3)를 정규화해 (y1, y2, y3)를 출력한다.
Cross Entropy Error 계층은 Softmax의 출력 (y1, y2, y3)와 정답 레이블 (t1, t2, t3)를 받고, 이들 데이터로부터 손실 L을 출력한다.
Softmax 계층의 역전파는 (y1 - t1, y2 - t2, y3 - t3)라는 깔끔한 결과를 도출한다.
신경망 학습의 목적은 신경망의 출력이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것이므로, 신경망의 출력과 정답 레이블의 이 오차를 효율적으로 앞 계층에 전달해야한다.
오차역전파법 구현
오차역전파법을 이용하면 느린 수치 미분과 달리 기울기를 효율적이고 빠르게 구할 수 있다.
오차역전파법을 적용한 신경망 구현(2층 신경망을 TwoLayerNet 클래스로 구현)
TwoLayerNet 클래스의 인스턴스 변수와 메서드를 표로 간단히 정리하면 다음과 같다.


이 클래스의 구현에서는 계층을 사용함으로써 인식 결과를 얻는 처리(predict())와 기울기를 구하는 처리(gradient()) 계층의 전파만으로 동작이 이루어 진다.
import sys, os
sys.path.append(os.pardir)
import numpu 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'])
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
여기서 OrderedDict은 순서가 있는 딕셔너리는 딕셔너리에 추가한 순서를 기억한다. 그래서 순전파 때는 추가한 순서대로 각 계층의 forward() 메서드를 호출하기만 하면 처리가 완료되며, 역전파 때는 계층을 반대 순서로 호출하기만 하면 된다.
오차역전파법으로 구한 기울기 검증
오차역전파법을 제대로 구현해두면 기울기 구할 때 더이상 수치미분이 필요하지 않다. 대신 수치 미분은 오차역전파법을 정확히 구현했는 가를 확인할 때 필요하다.
수치 미분의 이점은 구현하기 쉽다는 것으로, 이와 달리 오차역전파법은 구현하기 복잡해 실수 할 수도 있다. 따라서 수치 미분의 결과와 오차역전파법의 결과를 비교해 오차역전파법을 제대로 구현했는가 검증을 해야한다.
기울기 확인(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))
# 결과값
# b1: 9.70418809871e-13
# W2: 8.41139039497e-13
# b2: 1.1945999745e-10
# W1: 2.2232446644e-13
코드에 대해 간단히 설명해보자면, 일단 가장 먼저 MNIST 데이터셋을 읽는다. 이후 훈련 데이터의 일부를 수치 미분으로 구한 기울기와 오차역전파법으로 구한 기울기의 오차를 확인한다. 여기서 각 매중치 매개변수의 차이의 절댓값을 평균한 값이 오차가 된다.
결과값을 보면 수치 미분과 오차역전파법으로 구한 기울기의 차이가 매우 작음을 알 수 있다.
오차역전파법을 사용한 학습 구현
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)
'AI > Deep Learning' 카테고리의 다른 글
| [Deep Learning for Scratch] 신경망 학습 (0) | 2021.05.25 |
|---|---|
| [Deep Learning for Scratch] 신경망 (0) | 2021.05.23 |
| [Deep Learning for Scratch] 퍼셉트론 (0) | 2021.05.23 |