Search

5. 오차역전파법

[5.1] 계산그래프

그래프(graph)
연결된 객체 간의 관계를 표현하는 자료구조
노드(N, node)와 노드를 연결하는 엣지(E, edge)로 구성
G = (V,E)로 표시
V(G) : 그래프 G의 노드들의 집합 (=node, vertex)
E(G) : 그래프 G의 에지들의 집합 (=edge, link)
계산 그래프(computational graph)
: 계산 과정을 그래프로 나타낸 것
노드와 화살표(edge)로 표현
계산 그래프의 장점
국소적 계산: 복잡한 문제도 간단한 단위(노드)로 분할해 문제를 단순화하여 해결 가능
국소적 계산 : 계산 그래프의 노드를 독립적으로 계산하는 방식
중간 결과 보관 : 단계별 계산 결과 보관 가능
순전파와 역전파를 통해 각 변수의 미분을 효율적으로 계산
예를 들어, 사과(xx) 가격에 대한 지불 금액(LL)의 미분(변화율)은 Lx\partial L \over \partial x
계산 그래프의 연산 진행 방향
순전파(forward propagation) : 왼쪽에서 오른쪽으로 계산 (출발점→종착점)
순방향의 화살표로 표현
화살표 위에 결과를 적음
역전파(backward propagation) : 오른쪽에서 왼쪽으로 계산 (종착점→출발점)
순전파와 반대되는 방향(역방향)으로 굵은 선 화살표로 표현
화살표 아래에 미분값을 적음
계산 그래프를 이용해 문제 해결하기
1개에 100원인 사과 2개를 샀을 때의 지불 금액 구하기(단, 소비세 10%가 부과됨)
×(곱셈)만을 하나의 연산으로 취급한다면?
1개에 100원인 사과 2개와 1개에 150원인 귤 3개를 샀을 때의 지불 금액 구하기(단, 소비세 10%가 부과됨)
사과 2개와 여러 식품의 지불 금액 구하기
사과 가격에 대한 지불 금액의 미분 구하기

Reference

그래프의 개념과 종류, 구현, tistory (link)
그래프의 용어, titory (link)
그래프, tistory (link)

[5.2] 연쇄법칙

신경망의 순전파(forward propagation)는 입력층에서 출력층까지 신호를 계산 및 전달하는 과정이다. 반면에, 역전파(backward propagation)는 거꾸로 출력층에서 입력층까지 “국소적 미분”을 계산 및 전달하는 과정이며, 여기서 국소적 미분 결과가 다음 노드로 전달되는 원리는 연쇄법칙(chain rule)에 따른 것이다.
계산 그래프의 역전파
신경망 학습의 실체는 오차역전파(backward propagation of errors)이며, 이는 매개변수 별로 ‘국소적 미분(편미분)’을 계산하고 연쇄법칙(chain rule)에 따라 출력층에서 입력층으로 전달하면서 매개변수를 연쇄적으로 갱신하는 과정이다.
계산 그래프로 표현된 함수 y=f(x)y=f(x)의 역전파
함수 y=f(x)y=f(x)를 예로 들면, 순전파 과정에서는 입력 신호 xx를 노드 ff가 처리하여 출력 신호 yy를 생성한다. 반면에, 역전파 과정에서는 입력 신호 EE(손실 함수에 의해 측정된 오차)에 xx에 대한 편미분 yx\frac{\partial y}{\partial x}을 곱하여 계산된 EyxE\frac{\partial y}{\partial x} 을 (왼쪽 방향의) 다음 노드로 전달한다.
만약, y=f(x)=x2y=f(x)=x^2 이라면, 미분계수는 yx=2x\frac{\partial y}{\partial x}=2x 이므로 다음 노드로 2xE2xE를 전달한다.
모든 매개변수 wijW\forall w_{ij}\in W 에 대해 편미분 fwij\frac{\partial f}{\partial w_{ij}} 갱신을 수행하는 수치 미분(numerical differentiation) 보다 오차역전파 방법이 연쇄법칙을 통해 미분을 계산 효율적으로 수행한다고 한다. 왜 그럴까?
연쇄법칙이란?
연쇄법칙을 이해하기 위해서는 합성 함수의 개념을 알아야 한다. 합성 함수란 여러 함수로 구성된 함수이다.
수학적 정의:
어떤 함수 ff의 공역f(x)f(x)가 다른 함수 gg의 정의역이 되는 경우 두 함수의 합성 함수(gf)(x)=g(f(x))(g \circ f)(x)=g(f(x))로 표현된다. 참고자료: Khan Academy
합성 함수의 개념
이와 관련하여, 오차역전파 과정에 적용되는 연쇄법칙합성 함수의 미분에 대한 성질이며, 다음과 같이 정의된다.
합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다. 예: [g(f(x))]=g(f(x))f(x)=dgdfdfdx=dgdx[g(f(x))]'=g'(f(x))\cdot f'(x)=\frac{dg}{df}\frac{df}{dx}=\frac{dg}{dx} 참고 자료: Khan Academy
예를 들어, z=(x+y)2z=(x+y)^2의 식은 다음과 같이 두 개의 식으로 구성된다.
z=t2t=x+yz=t^2\\ t=x+y
위의 식을 합성 함수 (zt)(x)(z \circ t)(x)와 같이 나타낼 수 있으며, 각 함수의 미분은 zt\frac{\partial z}{\partial t} (tt에 대한 zz의 미분)과 tx\frac{\partial t}{\partial x} (xx에 대한 tt의 미분)와 같다.
따라서, 연쇄 법칙을 통해 구하고자 하는 zx\frac{\partial z}{\partial x} (xx에 대한 zz의 미분)은 두 함수 zztt의 미분의 곱으로 나타낸다.
zx=zttx\frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\cdot \frac{\partial t}{\partial x}
두 함수의 미분은 zt=2t\frac{\partial z}{\partial t}=2t, tx=1\frac{\partial t}{\partial x}=1 이므로 최종적으로 zx\frac{\partial z}{\partial x}을 해석적으로 구한 결과는 다음과 같다.
zx=2t1=2(x+y)\frac{\partial z}{\partial x}=2t\cdot1=2(x+y)
연쇄법칙과 계산 그래프
위의 예제 z=(x+y)2z=(x+y)^2 의 역전파 과정을 계산 그래프로 나타내면 다음과 같다.
z=(x+y)2z=(x+y)^2의 역전파 과정. 각 노드의 편미분을 곱하여 전달한다. (**2 노드는 2 제곱 연산을 나타냄)
입력은 zz\frac{\partial z}{\partial z} 이며, 이에 zt\frac{\partial z}{\partial t}(tt에 대한 zz의 편미분)을 곱하고 다음 노드로 전달된다. 마지막으로 tx\frac{\partial t}{\partial x}(xx에 대한 tt의 편미분)가 곱해진다.
zzzttx=zx\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial x}
결과적으로 매개변수 xx에 대한 역전파 계산 결과는 zx\frac{\partial z}{\partial x} 이므로, ‘xx에 대한 zz의 편미분’ 과 같다. 즉, 역전파가 하는 일은 연쇄법칙의 원리와 같다는 것을 의미한다.
따라서, 각 단계에서의 편미분 결과를 대입하면 아래 그림과 같이 zx=2(x+y)\frac{\partial z}{\partial x}=2(x+y)임을 알 수 있다.
계산 그래프의 역전파 계산에 따라 zx\frac{\partial z}{\partial x}2(x+y)2(x+y)가 된다
오차역전파가 왜 더 효율적일까?
위의 예제에서 xx를 신경망을 구성하는 매개변수 중에 하나라고 했을 때, 수치 미분 방식을 적용하면 xx의 (아주 작은)변화량에 따른 손실 함수 ff의 변화량을 계산해야 한다.
즉, 편미분 fx\frac{\partial f}{\partial x} 을 수행하는데 이는 계산 비용이 매우 높다. 그 이유는 신경망이 선형 방정식으로 표현되는 단순한 함수가 아니기 때문이다. 오차 측정에 필요한 순전파 과정은 입력층에서 은닉층들을 거쳐 출력층까지 흘러가는 수많은 입력 신호와 매개변수간 곱셈과 총합, 그리고 활성화 함수 단계들을 포함한다.
그러므로, 수 백만개 이상의 매개변수로 구성되는 신경망에서 각 매개변수 별로 편미분 계산을 위해 순전파 과정을 반복하는 것은 굉장히 계산 비효율적이다.
반면에, 오차역전파는 출력층에서부터 역방향으로 편미분 결과를 전달하므로, 반복적으로 순전파를 수행할 필요가 없다. 위의 예제에 대입하면, 매개변수 xx의 편미분인 zx\frac{\partial z}{\partial x}는 역전파에 따라 다음과 같이 정의된다.
zzzttx=zx\color{gray}\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\color{blue}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial x}
여기서 zzzt\color{gray}\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}는 이미 이전 노드에서 계산된 결과이므로, 재계산 없이 사용된다. 결국, 매개변수 xx를 갱신하기 위해 필요한 편미분에는 tx\color{blue}\frac{\partial t}{\partial x}의 계산만이 남았다. 이를 신경망에 비유하면 출력층까지 순전파 계산을 수행하지 않고 매개변수 xx에 연결된 뉴런의 출력 값까지만 계산한다는 의미를 가진다.

참고 자료

합성함수, Khan Academy (link)
합성 함수의 미분법, Khan Academy (link)

[5.3] 역전파

5.3.1 덧셈 노드의 역전파

덧셈 노드의 역전파를 계산 그래프에서 한 단계 나아가려면 1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 보내면 된다.

5.3.2 곱셈 노드의 역전파

곱셈 노드 역전파는 순전파 때의 입력 신호들을 서로 바꿔준 값을 곱해서 다음 노드로 보내면 된다.
덧셈 노드와 곱셈 노드에서 다음 노드로 보낼 때 곱해주는 값을 편미분으로 나타내보면,
결국 덧셈과 곱셈에서의 역전파 원리는 동일하다.

5.3.3 사과 쇼핑의 예

덧셈 노드의 역전파 & 곱셈 노드의 역전파 실제 예제에 적용해보기

[5.4] 단순한 계층 구현하기

곱셈 계층 구현하기
사과 2개 구입하기
class MulLayer: def __init__(self): self.x = None # 초기화를 위해 None을 넣음 self.y = None def forward(self, x, y): # forward propagation self.x = x self.y = y out = x * y return out def backward(self, dout): # backward propagation dx = dout * self.y dy = dout * self.x return dx, dy
Python
복사
__init__(self) : 기본값 설정(초기화)
forward(self, x, y) : 순전파 처리
backward(self, dout) : 역전파 처리 - 미분 특성에 따라 dx, dy는 x값과 y값을 바꾼 것과 같음
dx, dy : 현재 층의 역전파 값(gradient)
가장 최근에 업데이트 된 이전 계층의 역전파 값(dout)과 현재 층의 출력값의 미분 값을 곱함
곱셈 계층에서의 출력값의 미분은 다른 변수의 값을 곱한 것과 같음
f(x, y)의 x(y)에 대한 편도함수는 y(x)
f(x, y, z)의 x(y)(z)에 대한 편도함수는 y*z(x*z)(x*y)
덧셈 계층 구현하기
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
Python
복사
__init__(self) : 초기화 없이 변수의 값 그대로 유지
forward(self, x, y) : 순전파
backward(self, doub) : 역전파 - 미분 특성에 따라 dx, dy는 값이 그대로 내려옴
덧셈과 곱셈 계층 구현하기
사과 2개와 귤 3개 구입하기
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()
Python
복사
순전파
# forward propagation apple_price = mul_apple_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)
Python
복사
print("price:", int(price)) print("Apple:", apple) print("Apple_num:", int(apple_num)) print("Orange:", orange) print("Orange_num:", int(orange_num)) print("Tax:", tax)
Python
복사
price: 715 Apple: 100 Apple_num: 2 Orange: 150 Orange_num: 3 Tax: 1.1
Python
복사
역전파
# backward propagation 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)
Python
복사
print("price:", int(price)) # 역전파 초기값(순전파 결과값) print("dApple:", dapple) # (100*2)를 편미분한 후 *1.1 print("dApple_num:", int(dapple_num)) # (100*2)를 편미분한 후 *1.1 print("dOrange:", dorange) # (100*2)를 편미분한 후 *1.1 print("dOrange_num:", int(dorange_num)) # (100*2)를 편미분한 후 *1.1 print("dTax:", dtax) # (650*1.1)을 편미분한 후 *1
Python
복사
price: 715 dApple: 2.2 dApple_num: 110 dOrange: 3.3000000000000003 dOrange_num: 165 dTax: 650
Python
복사

Reference

역전파 알고리즘(backpropagation algorithm), velog (link)

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

5.5.1 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 x = np.array( [ [1.0, -0.5], [-2.0, 3.0] ] ) test_Relu = Relu() test_Relu.mask = (x <= 0)
Python
복사
순전파 때의 입력이 0보다 크면 → 역전파는 그대로 다음 노드로 보냄.
순전파 때의 입력이 0 이하 → 역전파는 신호를 보내지 않는다.

5.5.2 Sigmoid 계층

Q: 혹시 Sigmoid 도함수도 Sigmoid 함수 자신의 형태로 나타낼 수 있지 않을까?
1) 합성함수의 미분으로 정리해보기
2) 몫의 미분법으로 접근해보기
몫의 미분법 공식
몫의 미분법으로 도함수 접근
따라서 Sigmoid 함수(f(x))의 도함수는 f(x) { 1 - f(x) } 라고 나타낼 수 있다.
그렇다면 이를 계산 그래프로 나타내보자.
Sigmoid 계층의 순전파 계산 그래프
Sigmoid 계층의 계산 그래프

[5.6] Affine/Softmax 계층 구현하기

Affine 계층
Affine 변환을 통해 입력 데이터에 선형 변환을 적용하는 계층
Affine 변환
입력 데이터에 선형 변환을 적용하는 연산
선형 변환(linear transformation) : 가중치를 곱하고 편향을 더하는 연산
선형성(linearity) : 중첩의 원리를 만족하는 함수 또는 연산 ⇒ f(a+b)=f(a)+f(b)f(a+b) = f(a) + f(b)
입력 데이터의 구조와 관계를 유지하며 변환 수행
Affine 계층의 수식 표현
y=Wx+by = \mathit Wx + b
W\mathit W : 가중치 행렬 → 입력 데이터 x\mathrm x와 곱해져 선형 변환 수행
xx : 입력 데이터 (벡터 or 행렬)
bb : 편향 (벡터 or 행렬)
yy : 출력 (벡터 or 행렬)
Affine 계층의 계산 그래프
Affine 계층의 계산 그래프
변수 X,W,B \mathrm {X, W,B}는 모두 행렬 → 곱의 교환 법칙이 성립하지 않음 (ABBA)AB \neq BA)
X\mathrm XLX{\partial{L} \over \partial {X}}, WWLW\partial L \over \partial W은 같은 형상을 가짐
X=(x0,x1,,xn)LX=(Lx0,Lx1,Lxn)\mathrm X = (x_0, x_1, \cdots , x_n) \\ {\partial L \over \partial {\mathrm X}} = \left ( {\partial L \over \partial x_0},{\partial L \over \partial x_1}, \cdots{\partial L \over \partial x_n} \right )
Affine 계층의 곱셈 노드의 gradient(역전파 값) 구하는 법
최근 갱신된 gradient와 순전파의 입력 신호들을 서로 바꿔 전치한 값을 곱해서 계산
LX=LYWT LW=XTLY{\partial L \over \partial X} ={\partial L \over \partial Y } \cdot \mathrm W^T \ \qquad {\partial L \over \partial W} = \mathrm X^T \cdot {\partial L \over \partial Y}
전치(Transpose, ATA^T) : 행렬의 행을 열로, 열을 행으로 바꾸는 것 (m×n n×m)\left ( m\times n \ \Rightarrow n \times m \right )
W=(w11w12w13w21w22w23)WT=(w11w21w12w22w13w23)\mathrm W = \begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} &w_{22}& w_{23} \end{pmatrix} \qquad \mathrm W^T = \begin{pmatrix} w_{11} & w_{21} \\ w_{12} & w_{22} \\ w_{13}& w_{23} \end{pmatrix}
배치용 Affine 계층
데이터를 여러 개로 묶어 배치 처리 (1개 → n개)
배치용 Affine 계층의 계산 그래프
코드 구현
순전파(Forward propagation)
X_dot_W = np.array([[0, 0, 0], [10, 10, 10]]) B = np.array([1, 2, 3])
Python
복사
X_dot_W + B """ array([[ 1, 2, 3], [11, 12, 13]]) """
Python
복사
X_dot_W : 입력 데이터 X와 가중치 행렬 W를 곱한 출력값 (입력 데이터 2개, N=2)
B : 편향 행렬
역전파(Backward propagation)
dY = np.array([[1, 2, 3], [4, 5, 6]]) dB = np.sum(dY, axis=0)
Python
복사
dB """ array([5, 7, 9]) """
Python
복사
dY : 출력 Y의 gradient
dB : 편향 B의 gradient
편향의 gradient는 입력 데이터에 대한 미분을 데이터마다 더해서 구함
순전파의 편향 덧셈(각각의 데이터에 대해 수행)
→ 역전파에서는 각 데이터의 역전파 값이 편향의 원소에 모여야 함(모든 입력 데이터에 대한 영향을 반영, 손실값 계산의 편리성, outlier에 강함)
np.sum()의 axis=0 옵션을 통해 배치 내의 데이터에 대한 gradient 합산
참고 그림
Affine 계층
class Affine: def __init__(self, W, b): self.W = W self.b = b self.x = None # 입력값 초기화 self.original_x_shape = None # 가중치와 편향 매개변수의 미분 self.dW = None self.db = None def forward(self, x): # 텐서 대응 self.original_x_shape = x.shape x = x.reshape(x.shape[0], -1) self.x = x out = np.dot(self.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) dx = dx.reshape(*self.original_x_shape) # 입력 데이터 모양 변경(텐서 대응) return dx
Python
복사
<신경망 학습의 목적>
신경망의 출력이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것
신경망의 출력과 정답 레이블의 오차를 앞 계층에 효율적으로 전달해야 함
Softmax-with-Loss
[ Remind ] Softmax
Cross Entropy Error(교차 엔트로피 오차) - Loss function(손실함수)
E=ktklog(yk)E = -\sum_k t_klog(y_k)
tkt_k : Ont-Hot 인코딩 된 정답 레이블
yky_k : 신경망의 출력
loglog : 밑이 ee인 자연 로그
kk : 데이터의 차원
정답에 해당하는 인덱스의 원소만 1이고 나머지는 0이므로(one-hot encoding), 정답일 때의 추정되는 자연 로그를 계산하는 식이 됨
로그 함수를 사용함으로써 확률의 곱셈이 덧셈으로 변환되어 계산이 안정적으로 이루어짐
확률값은 보통 0~1 사이의 작은 값으로, 이러한 값들에 곱셈 연산을 적용할 경우 수치적 불안정성이 발생할 수 있음
e.g.) 정답 레이블이 ‘3’이라면 t3=1t_3 = 1이고 k3k \neq 3일 때, tk=0t_k=0
신경망의 3에 해당하는 출력 값이 0.6이라면 (y3=0.6)y_3 = 0.6), 교차 엔트로피 오차는 log0.6=0.51-log0.6 = 0.51이 되며 정답이 아닌 나머지 tkt_k는 모두 0이기 때문에 tklogyk=0t_k * log_{y_k} =0이 됨.
Softmax와 Cross Entropy Error
Softmax-with-Loss 계층의 계산 그래프(casual)
순전파
Softmax 계층은 이전 계층으로부터 3개의 입력(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)라는 결과를 가짐
Softmax의 출력과 정답 레이블의 차분(difference)
(y1,y2,y3)(y_1, y_2, y_3) - (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-with-Loss 계층의 계산 그래프
순전파
Softmax 계층
입력값을 0~1 사이의 값으로 정규화하여 출력의 총합을 1로 만드는 함수
exp: 각 입력값에 exp(지수함수)를 취해 양수로 만들어줌
f(x)=exf(x) = e^x (ee는 자연상수, 약 2.718)
덧셈 노드 : exp를 취한 값을 모두 합해줌(SS)
곱셈 노드 : 역수를 취해 곱해 줌으로써 각 데이터의 확률분포를 만들어줌(y1,y2,y3)y_1, y_2, y_3)
Cross Entropy Error 계층
Softmax 계층의 출력값과 정답 레이블을 통해 loss 계산(손실 함수)
log노드 통과 → t값을 곱해줌(곱셈노드) → 전부 합해줌 → -1 곱해줌
역전파
Softmax 계층
덧셈 노드 : 입력값 그대로 출력
곱셈 노드 : 출력값의 미분값에 최근 갱신된 gradient 곱해서 출력
log 노드 : y=log xy = log\ x를 미분해서 1x1 \over x출력
e.g.
정답 레이블이 (0, 1, 0)일 때 Softmax 계층이 (0.3, 0.2, 0.5)를 출력
→ 이때(정답의 인덱스가 1)의 확률이 0.2(20%)로, 제대로 인식하지 못함.
→ Softmax 계층의 역전파가 (0.3, -0.8, 0.5)라는 큰 오차를 앞 계층으로 전파하여 학습시킴
정답 레이블이 (0, 1, 0)일 때 Softmax 계층이 (0.01, 0.99, 0)을 출력
→ 이때(정답의 인덱스가 1)의 확률이 0.99(99%)로, 제대로 인식함.
→ Softmax 계층의 역전파가 (0.01, -0.01, 0)이라는 작은 오차를 앞 계층으로 전파하여 학습시킴
Softmax-with-Loss 계층 코드 구현
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] dx = (self.y - self.t) / batch_size return dx
Python
복사

Reference

[5.7] 오차역전파법 구현하기

5.7.1 신경망 학습의 전체 그림

<신경망 학습 순서>
1단계 : 미니배치
훈련 데이터 중 일부를 무작위로 가져옴
2단계: 기울기 산출
각 가중치 매개변수의 기울기를 구하기
3단계: 매개변수 갱신
손실 함수의 값을 가장 작게 하는 방향으로 가중치 매개변수를 갱신함
(가중치 매개변수를 손실 함수의 값을 가장 작게 하는 기울기 방향으로 갱신)
<학습>
가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정

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

import sys, os sys.path.append(os.pardir) 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) 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.last_layer = 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 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 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'].dW return grads
Python
복사
# 코드 이해해보기
__init__ function
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.last_layer = SoftmaxWithLoss()
Python
복사
self.params 구조
self.layers 구조
#참고 class Affine: def__init__(self, W, b): self.W = w self.b = b self.x = None self.original_x_shape = None self.dW = None self.db = None def forward(self, x): self.original_x_shape = x.shape x = x.reshape(x.shape[0] , -1) self.x = x return np.dot(self.x, self.W) + self.b 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) dx = dx.reshape(* self.original_x_shape) return dx
Python
복사
#참고 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
Python
복사
predict function
def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x
Python
복사
self.layers를 다시 정리해보면,
{ ‘Affine1’ : Affine(self.params[’W1’], self.params[’b1’],
’Relu1 : Relu(),
‘Affine2’ : Affine(self.parmas[’W2’], self.params[’b2’],
}
가 되므로, layer는 각각
Affine(self.params[’W1’], self.params[’b1’])
Relu()
Affine(self.parmas[’W2’], self.params[’b2’]
가 된다.
즉, Affine과 Relu에 각각 forward가 존재하므로, x라는 변수에 forward 값을 저장해서 계속 진행해간다.
loss function
def loss(self, x, t): y = self.predict(x) return self.lastLayer.forward(y,t)
Python
복사
여기서는 self.lastLayer = SoftmaxWithLoss()임을 주의해야 한다.
#참고 class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None 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.y.size == self.t.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
Python
복사
결국 loss 함수는 cross_entropy_error의 값을 반환하는 함수이다.
accuracy function
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
Python
복사
확률 벡터 y와 라벨 t의 argmax의 값이 동일한 경우 accuracy++해서 최종적인 accuracy를 반환함
numerical_gradient function
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
Python
복사
수치미분으로 각각의 gradient 구하는 함수
gradient function
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'].dW return grads
Python
복사
self.layers.values() 였던
Affine(self.params[’W1’], self.params[’b1’])
Relu()
Affine(self.parmas[’W2’], self.params[’b2’]
이들을 reverse 시켜서 그 후, Affine과 Relu의 backward()를 수행시켜 dW와 db를 반환받아 이를 grads에 저장

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

import sys, os sys.path(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 ) # default weight_init_std : 0.01 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))
Python
복사
MNIST 데이터 셋에서 6만 개의 train data와 1만 개의 test data를 불러와서,
그 중 train data 중 3개만 선별해서 수치미분과 오차역전파법으로 구해본다.
grad_numerical에는 numerical_gradient(수치미분)을 통해 수치 미분 값을 구해보고,
grad_backprop에는 gradient(오차역전파)을 통해 gradient 값을 구해 본다.
grad_numerical.keys() 에는 W1, b1, W2, b2가 존재하는데,
각각의 값(key)에 대한 grad_backprop와 grad_numerical에 대한 차이의 평균을 diff로 저장한다.
Q: grad_backprop와 grad_numerical의 차이에 대한 평균을 구한다는 의미는?
예를 들어, MNIST의 W1은 784 * 50 개의 요소들이 존재하는데, 각각 784 * 50개의 값의 평균을 구해W1의 diff로 저장했다.

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

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] #60000 batch_size = 100 learing_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): # 0 ~ train_size -1 까지 batch_size개 random으로 choice 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) # 학습을 10000(iters_num)번 하는데, iter_per_epoch(60000 / 100 = 600) 번 마다 정확도 test 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)
Python
복사

[5.8] 정리

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