이번 시간엔 소프트맥스 함수 역전파를 고급지게(?) 다뤄볼 것이다. 왜냐하면 기존에 다룬 방법(8.다중분류 구현하기(기초실습), 9.다중분류 구현하기(심화실습) ) 은 너무 비효율적이기 때문이다. (그래도 고급을 이해하기 위해서 읽고 오시라, 기본 수학적 배경 없으면 읽기 힘들 수 있다.)
일일이 소프트맥스 미분 행렬을 만드는 것은 너무나도 귀찮은(...) 일이다. 아래는 IRIS 꽃 분류에 사용하였던 소프트맥스 미분 행렬이다. 3개 출력이므로 (3, 3) Size의 행렬이 된다.
dsmax_dg_matrix = np.array([[(smax_WXB1*(1-smax_WXB1))[0], -(smax_WXB1*smax_WXB2)[0], -(smax_WXB1*smax_WXB3)[0]],
[-(smax_WXB1*smax_WXB2)[0], (smax_WXB2*(1-smax_WXB1))[0], -(smax_WXB2*smax_WXB3)[0]],
[-(smax_WXB1*smax_WXB3)[0], -(smax_WXB2*smax_WXB3)[0], (smax_WXB2*(1-smax_WXB1))[0]]])
MNIST 손글씨 분류에서 출력은 0~9 즉 10개 이므로 소프트맥스 미분 행렬은 (10, 10)의 Size가 되어야 한다. 이를 일일이 코드로 작성하는 것은 가독성을 떨어뜨리며 오류발생확률이 높다. 이번 시간엔 좀 더 일반화된 소프트맥스 미분 함수를 만들어 볼 것이며 이를 이용해 추후 다중 분류를 진행해 보도록 하겠다.
1. 순방향 출력 정의 (위의 링크 글 처럼 단층 퍼셉트론으로 다중 분류하기)
2. 기본 방법
1) batch의 요소 단위로 실행(for문)
(1) 소프트맥스 출력 행렬(S)에 대한 미분 행렬을 Kronecker delta를 이용하여 작성 → (3, 3) 행렬
크로네커 델타에 대해서는 인터넷 검색으로 참고하시라.
# create softmax derivate matrix
grads = []
for s in np.transpose(S, (1, 0)):
grad_matrix = np.zeros((s.size, s.size))
for i in range(len(S)):
for j in range(len(s)):
if i == j:
grad_matrix[i][i] = s[i]*(1-s[i])
else:
grad_matrix[i][j] = - s[i]*s[j]
(2) 위 행렬을 배열(grads)에 저장하기 → grads.shape = (batch, 3, 3)
grads.append(grad_matrix.tolist()) # (batch, 3, 3)
2) target값과 소프트맥스 행렬의 요소 곱을 위해 target값을 3차원으로 변경 (batch, 3) → (batch, 1, 3)
grads = np.array(grads)
Y_target = np.expand_dims(target, axis=1) # (batch, 1, 3)
3) batch의 요소 단위로 실행(for문)
(1) 계산결과를 저장할 배열 선언 dS_dG = [ ]
(2) grads의 요소와 target값의 요소의 전치와 행렬곱을 수행한다. 그러면 각 요소에 대해 ∂S/∂G를 구한것이다.
grads shape → (3, 3)
target shape → (1, 3) → 전치 → (3, 1)
행렬곱을 수행하면 (3, 1)
np.dot(grads[i], np.transpose(Y_target[i], (1, 0))) # (3, 1)
(3) 연산 결과를 배열에 쌓기 위해 다시 전치하고 배열에 추가
value = np.transpose(value, (1, 0)) # (1, 3)
dS_dG.append(value.tolist()) # (batch, 1, 3)
(4) 최종 출력을 위해 차원을 줄이고 다시 전치해준다. 이렇게 하면 체인룰을 이용한 역전파 계산이 쉬워진다.
dS_dG = np.array(dS_dG) # (batch, 1, 3)
dS_dG = dS_dG.reshape(len(Y_target), -1) # (batch, 3)
dS_dG = np.transpose(dS_dG, (1, 0)) # (3, batch)
3. 전체 코드와 결과 확인
import numpy as np
inputs = np.array([[0.1, 0.2, 0.3, 0.4],
[2., 3., 4., 5., ],
[10., 11., 12., 13.],
[15., 16., 17., 18.]], dtype=np.float32) * 0.01
targets = np.array ([[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[0, 0, 1]], dtype=np.float32)
np.random.seed(220209)
W = np.random.randn(3, 4)
B = np.random.randn(3, 1)
learning_rate = 0.25
def forward(input, target, W, B):
X = np.transpose(input, (1, 0)) # (4, batch)
G = np.dot(W, X) + B # (3, batch)
exp = np.exp(G) # (3, batch)
sum_exp = np.sum(exp, axis=0, keepdims=True) #(1, batch)
S = exp/sum_exp # (3, batch)
pred = np.transpose(S, (1, 0)) # (batch, 3)
target_Y = np.sum(pred * target, axis=1, keepdims=True) # (batch, 1)
losses = np.sum(-np.log(target_Y)) # (1, 1)
return G, S, target_Y, pred, losses
def backward(G, S, target_Y, input, target):
dL_dS = -1/(np.transpose(target_Y, (1, 0))) # (1, batch)
# create softmax derivate matrix
grads = []
for s in np.transpose(S, (1, 0)):
grad_matrix = np.zeros((s.size, s.size))
for i in range(len(S)):
for j in range(len(s)):
if i == j:
grad_matrix[i][i] = s[i]*(1-s[i])
else:
grad_matrix[i][j] = - s[i]*s[j]
grads.append(grad_matrix.tolist()) # (batch, 3, 3)
# calculate dS/dG
grads = np.array(grads)
Y_target = np.expand_dims(target, axis=1) # (batch, 1, 3)
dS_dG = []
for i in range(len(Y_target)):
value = np.dot(grads[i], np.transpose(Y_target[i], (1, 0))) # (3, 1)
value = np.transpose(value, (1, 0)) # (1, 3)
dS_dG.append(value.tolist()) # (batch, 1, 3)
dS_dG = np.array(dS_dG) # (batch, 1, 3)
dS_dG = dS_dG.reshape(len(Y_target), -1) # (batch, 3)
dS_dG = np.transpose(dS_dG, (1, 0)) # (3, batch)
dG_dW = input # (batch, 4)
dG_dB = np.ones_like(G) # (3, batch)
# ∂L/∂W
dL_dG = dL_dS * dS_dG # (3, batch)
dL_dW = np.dot(dL_dG, dG_dW) # (3, 4)
dL_dB = np.sum(dL_dG * dG_dB, axis=1, keepdims=True) # (3, 1)
return dL_dW, dL_dB
_, _, _, pred, losses = forward(inputs, targets, W, B)
print('before pred', pred)
print('before loss', losses)
"""
before pred [[0.10265982 0.29645386 0.60088631]
[0.10900662 0.29600653 0.59498684]
[0.13907667 0.29382167 0.56710166]
[0.16118327 0.29108023 0.5477365 ]]
before loss 4.662885829078962
"""
for i in range(1000):
G1, S, target_Y, pred, losses = forward(inputs, targets, W, B)
dL_dW, dL_dB = backward(G1, S, target_Y, inputs, targets)
W = W + -1*learning_rate * dL_dW
B = B + -1*learning_rate * dL_dB
_, _, _, pred, losses = forward(inputs, targets, W, B)
print('after pred', pred)
print('after loss', losses)
"""
after pred [[0.59364373 0.37807008 0.02828618]
[0.37980758 0.4856894 0.13450303]
[0.02272509 0.1177337 0.85954121]
[0.00140001 0.01738906 0.98121093]]
after loss 1.413986211117531
"""
3. 훨씬 쉬운 방법(23년 9월 23일 추가)
더 쉬운 방법을 발견했다! (발견했다기 보단 늦게 깨달았다 ㅜㅜ)
Deep Learning from Scratch(세스 와이드먼)이라는 책을 참고하여 더 쉬운 softmax 역전파 방법을 안내하겠다.
위에서처럼 소프트맥스 미분 행렬을 Kronecker delta로 만드는 것이 어려울 수 있다. 더 쉬운 직관력을 발휘해 보자.
우리의 목적은 4개의 입력을 3개의 확률 분포로 나타내야 한다. 소프트맥스 출력 S = [s_1, s_2, s_3]이고 선형 연산 출력
G = [g_1, g_2, g_3] 일 때 아래와 같이 s_1과 g_1에 대해 성립함을 계산할 수 있다. 참고로 목표값 y_1은 s_1에 대해 1인 값이다.
결론적으로, 활성화 함수가 softmax일 때 선형 연산(G)에 대한 오차(E) 기울기는
소프트맥스 통과값에서 목표값 Y를 뺀 것(S - Y)과 같다는 것이다. 미련하게 소프트맥스 미분 행렬을 구하지 않아도 된다 ㅜㅜ
아래는 이를 증명한 코드이다. 훨씬 더 간단함에도 불구하고, 똑같은 결과가 나온다!
import numpy as np
inputs = np.array([[0.1, 0.2, 0.3, 0.4],
[2., 3., 4., 5., ],
[10., 11., 12., 13.],
[15., 16., 17., 18.]], dtype=np.float32) * 0.01
targets = np.array ([[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[0, 0, 1]], dtype=np.float32)
np.random.seed(220209)
W = np.random.randn(3, 4)
B = np.random.randn(3, 1)
learning_rate = 0.25
def forward(input, target, W, B):
X = np.transpose(input, (1, 0)) # (4, batch)
G = np.dot(W, X) + B # (3, batch)
G_T = np.transpose(G, (1, 0)) # 올바른 softmax 통과를 위해 shape 변경(batch, 3)
S = np.exp(G_T)/np.sum(np.exp(G_T), axis=1, keepdims=True) # (batch, 3)
losses = np.sum(-target*np.log(S)) # (1, 1)
return G, S, pred, losses
def backward(G, S_T, input, target):
dL_dG = S - target # (batch, 3)
dL_dG = np.transpose(dL_dG, (1, 0)) # (3, batch)
dG_dW = input # (batch, 4)
dL_dW = np.dot(dL_dG, dG_dW) # (3, 4)
dL_dB = np.sum(dL_dG, axis=1, keepdims=True) # (3, 1)
return dL_dW, dL_dB
_, _, pred, losses = forward(inputs, targets, W, B)
print('before pred', pred)
print('before loss', losses)
"""
before pred [[0.02620253 0.07566585 0.15336812]
[0.02818704 0.07654166 0.15385229]
[0.03424511 0.07234827 0.13963852]
[0.03867602 0.06984488 0.13142971]]
before loss 4.662885829078962
"""
for i in range(1000):
G1, S, pred, losses = forward(inputs, targets, W, B)
dL_dW, dL_dB = backward(G1, S, inputs, targets)
W = W + -1*learning_rate * dL_dW
B = B + -1*learning_rate * dL_dB
_, _, pred, losses = forward(inputs, targets, W, B)
print('after pred', pred)
print('after loss', losses)
"""
after pred [[0.02620253 0.07566585 0.15336812]
[0.02818704 0.07654166 0.15385229]
[0.03424511 0.07234827 0.13963852]
[0.03867602 0.06984488 0.13142971]]
after loss 1.4139862111175314
"""
'파이썬 프로그래밍 > Numpy 딥러닝' 카테고리의 다른 글
24. 딥러닝에서 데이터 표준화, 정규화가 필요한 이유 (0) | 2022.04.19 |
---|---|
23 - 학습 성능 개선 : Mini batch & Shuffle 구현하기 (0) | 2022.02.11 |
21. 경사하강법의 개선 - Adam (0) | 2022.02.05 |
20. 경사하강법의 개선 - Momentum, RMSprop (0) | 2022.02.04 |
19. 경사하강법과 단순 경사하강법의 문제점 (0) | 2022.01.28 |