목차
- 오차역전법(backpropagation)(=‘역전파법’,‘역전파’)
- 계산그래프 computational graph
- 활성화 함수 계층 구현
- Affine/Softmax 계층 구현하기
- 오차역전파법으로 구한 기울기 검증
- 정리
👀, 🤷♀️ , 📜
이 아이콘들을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
전체 딥러닝 학습과정
이번 포스트에선 어딜 배울까?
전의 포스트 경사하강법에선 오차값에 따라 매개변수를 오차가 없는 방향으로 줄여주는 기본적인 메서드를 봤다.
그럼 이제 이 경사학습법을 이용하여 딥러닝 모델의 구현법인 오차역전법(backpropagation)을 볼 것이다
학습 방식
매개변수의 조정 방식(위의 핑크색 부분)은 다음의 과정을 통해 진행된다.
1️⃣ 먼저 임의의 초기 가중치(W) 를 준 뒤 순전파를 통한 결과 값(output)(Y)을 얻는다
2️⃣ 계산 결과와 우리가 원하는 값 사이의 오차를 구한다
3️⃣ 경사 하강법을 이용해 바로 앞 가중치를 오차가 작아지는 방향으로 업데이트한다
4️⃣ 더이상 오차가 줄어들지 않을 때 까지 위의 과정을 반복한다
이 포스트에서는 3️⃣을 구현하기 위해 쓰이는 수단인 경사 하강법을 이용하여 backpropagation하는 방법을 설명해 볼 것이다
INTRO
이전 포스트에서 Input에서 Output으로 가중치를 업데이트하면서 활성화 함수를 통해서 결과값을 가져오는 것까지 배웠다. ➡ 신경망의 구성(활성화 함수)
이렇게 쭉 오는 것을 순전파(foward) 라고 하며 말 그대로 앞쪽으로 input 값을 전파, 보내는 것이라고 보면 된다.
⛔ PROBLEM
하지만 우리가 임의로 한 번 순전파 했다고 출력 값이 정확하지는 않을 것이다.
초기 가중치는 그 데이터의 특징에 맞는 가중치가 아닌 우리가 임의로 설정한 랜덤 가중치 였기 때문이다.
우리가 임의로 설정한 가중치 값이 input에 의해서 한 번 업데이트 되긴 했지만 많은 문제가 있을 수 있다.
⭕ SOLUTION
역전파 방법은 결과 값을 통해서 다시 역으로 input 방향으로 오차를 다시 보내며 가중치를 재업데이트 하는 것이다.
결과에 영향을 많이 미친 노드(뉴런)에 더 많은 오차를 돌려줄 것이다.
Before backpropagation
그럼 이를 설명하기 위해서 전체 학습 과정을 계산그래프로 나타내보자
➡ 국소적 계산에 집중하기 좋기 때문이다
국소적 계산에 집중
전체 계산이 복잡하더라도 각 단계에서 하는 일은 해당 노드의 ‘국소적 계산’.
국소적인 계산은 단순하지만, 그 결과를 전달함으로서 전체를 구성하는 복잡한 계산을 해낼 수 있음
즉 각 노드가 자신의 노드만 신경쓴다는 뜻이다.
[계산그래프로 풀기]
계산 결과를 위에(에지)에 적음
[계산그래프의 흐름]
1. 계산 그래프를 구성
2. 순전파(forward propagation)
- 그래프에서 계산을 왼쪽에서 오른쪽으로 진행
- 계산 그래프의 출발점부터 종착점으로의 전파
- cf) 역전파 backward propagation: 반대 방향(그림에서 말하면 오른쪽에서 왼쪽)의 전파-> 미분을 계산할 때 중요한 역할
📜 계산그래프 computational graph
계산 과정을 그래프(수의 노드 node와 에지edge 표현)로 나타낸 것
노드와 에지?
[계산그래프 사용 이유]
전체가 아무리 복잡해도 각 노드에서는 단순한 계산에 집중하여 문제 단순화 가능
중간 계산 결과를 모두 보관가능
역전파를 통해 ‘미분’의 효율적 계산
📜 국소적 계산
국소적: ‘자신과 직접 관계된 작은 범위’
국소적 계산: 전체에서 어떤 일이 벌어지든 상관X
- 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것
각 노드에서의 계산은 국소적 계산
각 노드는 자신만 신경 씀
오차역전법 (backpropagation)
=‘역전파법’, ‘역전파’
그럼 계산 그래프로 순전파까지 구현을 해뒀으니 역전파를 해보자
이 역전파를 통해서 출력과 정답의 오차를 input 노드까지 전달해가며 각 가중치를 업데이트 해나갈 것이다.
그럼 역전파를 하며 기울기를 구하는 법은 무엇일까?
순전파 시 각 계산에 따라 다르다 아래에서 확인해보자
연쇄법칙 chain rule
정의: 합성함수의 미분법
‘국소적 미분’을 전달하는 원리는 연쇄법칙 에 따른 것이다
[연쇄법칙(겉미분 속미분)]
연쇄법칙: 합성 함수의 미분에 대한 성질
- 합성함수: 여러 함수로 구성된 함수
- Ex) \(z = (x + y)^2 -> z = t^2 and t = x + y\)
[계산 그래프의 역전파]
\(y = f (x)\)라는 계산의 역전파
- 신호 E에 노드의 국소적 미분 \(ay/ax\)를 곱한 후 다음노드로 전달
- 이것이 가능한 이유는 연쇄적법칙 때문
역전파
덧셈 노드의 역전파
그대로 출력
-
1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 보내게 됨
- \(L\): 순전파 시 최종적인 출력 값
- \(z\),
- \(x + y\) 계산결과로, 그 큰 계산 그래프의 중간 어딘가에 존재
- 상류로부터 값이 전해진 것
- 하류로는 \(∂L/∂x\)과 \(∂L/∂y\)값을 전달하는 것
👀 덧셈 노드 코드 보기
[단순한 계층 구현하기]
밑의 기능을 계층 단위로 구현
- 덧셈 노드를 ‘AddLayer’
- 시그모이드 함수를 Sigmoid
- 행렬 곱을 Affine
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
곱셈 노드의 역전파
서로 바꾼값 곱하기
- \(∂L/∂x=y\).
- \(∂L/∂y=x\)
[ex]
- \(1.3 * 5 = 6.5\).
- \(1.3 * 10 = 13\).
👀 곱셈노드 코드 보기
[단순한 계층 구현하기]
밑의 기능을 계층 단위로 구현
- 곱셈 노드를 ‘MulLayer’
- 시그모이드 함수를 Sigmoid
- 행렬 곱을 Affine
[곱셈 계층 구현]
모든 계층은 forward ( )
순전파와 backward ( )
역전파라는 공통의 메서드(인터페이스)를 갖도록 구현할 것
class MulLayer:
# 인스턴스 변수인 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
#상류에서 넘어온 미분(dout)에 순전파 때의 값을 ‘서로 바꿔’ 곱한 후 하류로
def backward(self, dout):
dx = dout * self.y # x 와 y 를 바꾼다.
dy = dout * self.x
return dx, dy
예시 적용
[예시 설명]
소비세와 사과 가격이 같은 양만큼 오를 때 영향
- 최종 금액에는 소비세가 200의 크기로,
- 사과 가격이 2.2 크기로 영향을 줌
주의) 이 예에서 소비세와 사과 가격은 단위가 다름
- 소비세 1은 100%
- 사과 가격 1은 1원
[예시 적용 1]
👀 예시 적용 코드 보기
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
# 역전파
dprice = 1
## dapple_price로 dapple, dapple_num에 앞의 역전파 결과 전달
## dapple_price = 1.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 ( )가 받는 인수는 ‘순전파의 출력에 대한 미분’
[예시 적용 2]
이번엔 앞에서 배운 덧셈노드까지
👀코드 보기
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_appl _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) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax)
# 110 2.2 3.3 165 650
활성화 함수 계층 역전파 구현
활성화 함수란?
계산 그래프를 신경망에 적용
ReLU 계층
[역전파]
- 순전파 시 \(x> 0\): 역전파는 상류의 값을 그대로 하류로 흘림
- 순전파 시 \(x<= 0\): 역전파 때는 하류로 신호를 보내지X (0을 보냄)
👀 코드 보기
class Relu:
# mask라는 인스턴스 변수를 가집니다
# mask는 True/False로 구성된 넘파이 배열
# 순전파의 입력인 x의 원소 값이 0 이하인 인덱스는 True,
# 그 외(0보다 큰 원소)는 False로 유지
def __init__(self):
self.mask = None
def forward(self, x):
# "순전파 때의 입력 값이 0 이하면" 순전파 시의 값은 0이 돼야 함.
self.mask = (x < = 0)
# 0이상 이면 그냥 똑같은거 보냄
out = x.copy()
# 순전파 때의 입력 값이 0 이하면 "순전파 시의 값은 0이 돼야 함".
out[self.mask] = 0
return out
def backward(self, dout):
# "순전파 때의 입력 값이 0 이하면" 역전파 때의 값은 0이 돼야 함.
## 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는
## 상류에서 전파된 dout을 0으로 설정
dout[self.mask] = 0
dx = dout
return dx
Sigmoid 계층
[역전파]
[순전파]
[역전파]
[1단계]
‘/’ 노드, 즉 \(y= 1/x\) 미분
- 역으로 바꾼 것, 즉 \(* x^{-1}\)한 것 미분
- 역전파 때는 상류에서 흘러온 값에 \(-y^2\)을 곱해서 하류로 전달
\(ay/ax= -1/x^{2} = -y^{2}\).
[2단계]
‘+’ 노드, 상류의 값을 여과 없이 하류로 내보냄.
[3단계]
‘exp’ 노드, \(y = exp (x)\) 연산을 수행
&&ay/ax= exp(x) = exp(-x) &&
[4단계]
‘×’ 노드, 순전파 때의 값을 ‘서로 바꿔’ 곱함.
-
이 예에서는 -1을 곱하면 됨
-
이를 하나의 노드로 표현하면
-
위를 전개한 식은
-
그럼 최종적으로 아래와 같은 역전파 결과 나오게 된다
👀코드 보기
class Sigmoid:
def __init __(self):
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x))
# 순전파의 출력을 인스턴스 변수 out에 보관
## 역전파 계산 때 그 값을 사용합니다
self.out = out
return out
# self.out = y
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
Affine/Softmax 계층 역전파 구현하기
Affine 계층
신경망의 순전파에서는 가중치 신호의 총합을 계산하기 때문에 행렬의 곱(넘파이에서는 np.dot ( ) )을 사용
(신경망)순전파의 흐름
- \(Y = np.dot (X, W ) + B\)처럼 계산.
- Y를 활성화 함수로 변환해 다음 층으로 전파하는 것
어파인 변환 (affine transformation): 신경망의 순전파 때 수행하는 행렬의 곱
- Affine 계층: 어파인 변환을 수행하는 처리
- 노드계산시 각변수의 이름 위에 그 변수의 형상도 표기
- 지금까지의 계산 그래프: 노드 사이에 ‘스칼라값’이 흐름
- 어파인 변환(아핀 변환):‘행렬’이 흐름
[역전파]
곱하기와 같음
곱할 때 다른거 곱(하지만 전치(T)필요!!)
[1단계]
[2단계]
배치용 Affine 계층
지금까진 입력 데이터로 X 하나만을 고려
배치-묶은 데이터: 데이터 N개를 묶어 순전파하는 경우
- 기존과 다른 부분은 입력인 X의 형상이 (N, 2)가 된 것
👀코드 보기
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)
#[3]구현 얘는 열로 더해버려
self.db = np.sum(dout, axis = 0)
return dx
Softmax-with-Loss 계층
출력층에서 사용하는 소프트맥스 함수
-
소프트맥스 함수는 입력 값을 정규화하여 출력(확률로 출력)
1) 입력 이미지가 Affine 계층과 ReLU 계층을 통과하며 변환되고,
2) 마지막 Softmax 계층에 의해서 10개의 입력이 정규화 - 소프트맥스 함수의 이용: 신경망에서 학습과 추론중 학습에 주로 이용
- 추론에는 제일 높은 값만 필요하기 때문
- 점수(score): 신경망에서
정규화하지 않는출력 결과(그림에선 Softmax 앞 Affine 계층의 출력)- 주로 ‘학습’에 이용
[Softmaxwith-Loss 계층] 손실 함수인 교차 엔트로피 오차도 포함
‘Softmax’ 계층+‘Cross Entropy Error’
(간소화)
- 입력: \((a_1 , a_2 , a_3 )\)
- Softmax 계층 출력: \((y_1 , y_2 , y_3)\)
- 정답 레이블: \((t_1 , t_2 , t_3 )\)
- Cross Entropy Error 출력: 손실 L
- Softmax-with-Loss 계층 역전파 결과: \((y_1 - t_1 , y_2 -t_2 , y_3 - t_3 )\)
(구체화)
[1 순전파]
(1) Softmax 계층
- 최종 출력은 \((y_1 , y_2 , y_3)\)
(2) Cross Entropy Error 계층
[2 역전파]
(1) Cross Entropy Error 계층
주의
- 역전파의 초깃값, 즉 가장 오른쪽 역전파의 값은 \(1(aL/aL =1)\)
- ‘×’ 노드의 역전파는 순전파 시의 입력들의 값을 ‘서로 바꿔’ 상류의 미분에 곱하고 하류로 흘림
- ‘+’ 노드에서는 상류에서 전해지는 미분을 그대로 흘림
- ‘log’ 노드의 역전파는 다음 식을 따름
- \(y- log x\),
- \(∂y/∂x = 1/x\).
(2) Softmax 계층 (1단계) (2단계)
‘/’ 노드의 역전파:‘상류에서 흘러온 값’
x‘순전파 때의 출력을 제곱한 후 마이너스를 붙인 값’
을 하류 전달.
‘상류에서 흘러온 값’
: \((-t_1 S) + (-t_2 S) + (-t_3 S)\) \(= -S(t_1 + t_2 + t_3 )\)‘순전파 때의 출력’
: \(1/S\) 역전파의 출력- \(-S(t_1 + t_2 + t_3 ) * (-1/S)^2\).
- \(= (t_1 + t_2 + t_3 ) * (1/S)\).
- (3단계)
‘x’는 바꿔서 연산 (4단계)
‘exp’ 노드 역전파: 두 갈래의 입력의 합에 \(exp(a_1 )\)을 곱한 수치
[정리]
- 신경망의 역전파: 이 차이인 오차가 앞 계층에 전해지는 것
- \((y_1 - t_1 , y_2 - t_2 , y_3 - t_3 )\): Softmax 계층의 출력과 정답 레이블의 차.
- 신경망 학습의 목적: 신경망의 출력(Softmax의 출력)이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것
- 그래서 신경망의 출력과 정답 레이블의 오차를 효율적으로 앞 계층에 전달
ex) 구체적인 예
정답 레이블이 (0, 1, 0 )일 때, Softmax 계층이 (0.3, 0.2, 0.5 )를 출력
- 정답 레이블을 보면 정답의 인덱스는 1.
- 그런데 출력에서는 이때의 확률 겨우 0.2 (20%)라서, 이 시점의 신경망은 제대로 인식X
- 이 경우 Softmax 계층의 역전파는 (0.3, -0.8, 0.5 )라는 커다란 오차를 전파
- 결과적으로 Softmax 계층의 앞 계층들은 그 큰 오차로부터 큰 깨달음을 얻게 됨
👀코드 보기
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
# 역전파 때는 전파하는 값을 배치의 수(batch_size) 로 나눠서
# 데이터 1개당 오차를 앞 계층으로 전파하는 점에 주의
def backward(self, dout = 1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
오차역전파법 구현
신경망 학습의 전체 그림(4단계)의 [2단계 기울기 산출]에서 오래걸리는 수치미분 대신 기울기를 효율적이고 빠르게구할 수 있음
- 구현: 2층 신경망을 TwoLayerNet 클래스로 구현
📜 인스턴스 변수 보기
params
: 딕셔너리 변수로, 신경망의 매개변수를 보관
params['W1']
: 1번째 층의 가중치, params[‘b1’]은 1번째 층의 편향params['W2']
: 2번째 층의 가중치, params[‘b2’]는 2번째 층의 편향
layers
: 순서가 있는 딕셔너리 변수로, 신경망의 계층을 보관 layers[‘Affine1’], layers[‘Relu1’], layers['Affine2']
와 같이 각 계층을 순서대로 유지
lastLayer
: 신경망의 마지막 계층
- 이 예에서는 SoftmaxWithLoss 계층
📜 매서드 설명 보기
__init__(self,input_size,hidden_ size,output_size,weight_init_ std)
: 초기화를 수행
input_size
: 입력층 뉴런 수hidden_ size
: 은닉층 뉴런 수output_size
: 출력층 뉴런 수weight_init_ std
: 가중치 초기화 시 정규분포의 스케일
predict(self, x)
: 예측(추론)을 수행
x
: 이미지 데이터
loss(self, x, t)
: 손실 함수의 값을 구함
x
: 이미지 데이터t
: 정답 레이블
accuracy(self, x, t)
: 정확도를 구함
numerical_gradient(self, x, t)
: 가중치 매개변수의 기울기를 수치미분방식으로 구함
gradient(self, x, t)
: 가중치 매개변수의 기울기를 오차역전파법으로 구함
[요약]
(predict ( ) )
: 계층을 사용함으로써 인식 결과를 얻는 처리와(gradient ( ) )
: 기울기를 구하는 처리
이 두 계층의 전파만 으로 동작이 이루어지는 것
👀코드 보기
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical _gradient
# 입력된 아이템들(items)의 순서를 기억하는 Dictionary 클래스
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
📜 코드 설명 보기
OrderedDict에 보관하는 점이 중요
- OrderedDict: 순서가 있는 딕셔너리
순전파: 추가한 순서대로 각 계층의 forward ( ) 메서드를 호출하기만 하면 처리 완료.(OrderedDict 덕분)
역전파: 계층을 반대 순서로 호출하기만 하면 됨
Affine 계층과 ReLU 계층이 각자의 내부에서 순전파와 역전파를 제대로 처리하고 있으니,
- 여기에서는 그냥 계층을 올바른 순서로 연결한 다음 순서대로 (혹은 역순으로) 호출해주면 끝
오차역전파법으로 구한 기울기 검증
수치 미분은 오차역전파법을 정확히 구현했는지 확인하기 위해 필요
[gradient check]
두 방식으로 구한 기울기가 일치함(엄밀히 말하면 거의 같음)을 확인하는 작업을 기울기 확인
[크기 비교 유의점]
수치 미분과 오차역전파법의 결과 오차가 0이 되는 일은 드뭄 (이는 컴퓨터가 할 수 있는 계산의 정밀도가 유한하기 때문)
이 정밀도의 한계 때문에 오차는 대부분 0이 되지는 않지만, 올바르게 구현했다면 0에 아주 가까운 작은 값이 됨
만약 그 값이 크면 오차역전파법을 잘못 구현했다고 의심 필요
정리
이번 장에서 배운 내용
● 계산 그래프를 이용하면 계산 과정을 시각적으로 파악 가능.
● 계산 그래프의 노드는 국소적 계산으로 구성. 국소적 계산을 조합해 전체 계산을 구성.
● 계산 그래프의 순전파는 통상의 계산을 수행. 한편, 계산 그래프의 역전파로는 각 노드의 미분을 구할수 있다.
● 신경망의 구성 요소를 계층으로 구현하여 기울기를 효율적으로 계산할 수 있다(오차역전파법).
● 수치 미분과 오차역전파법의 결과를 비교하면 오차역전파법의 구현에 잘못이 없는지 확인할 수 있다(기울기 확인).
사진 출처: 밑바닥부터 시작하는 딥러닝 1