본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

22. 다중회귀-소프트맥스 함수 역전파(고급, 쉬운 방법)

by Majestyblue 2022. 2. 10.

이번 시간엔 소프트맥스 함수 역전파를 고급지게(?) 다뤄볼 것이다. 왜냐하면 기존에 다룬 방법(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) 행렬

크로네커 델타에 대해서는 인터넷 검색으로 참고하시라.

 

https://stackoverflow.com/questions/33541930/how-to-implement-the-softmax-derivative-independently-from-any-loss-function

 

# 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
"""