본문 바로가기
Tensorflow

[ANN] 11. 활성화함수's Back Propagation (Affine, Softmax)

by 청양호박이 2020. 5. 22.

지금까지 역전파(Back Propagation)에 대해서 많은 시간을 들여 알아보고 있습니다. Graph를 그려서 기본적인 흐름을 살펴보고, 실제로 활성화함수(Activation Function)에서 동작방식을 알아보았습니다. 그리고 최종적으로 python코드로 구현함으로써 내재화도 시켰습니다.

 

바로 전에는 활성화함수에 대해서 알아보다가... 너무 길어져서 2개로 나눴었습니다. ReLU와 Sigmoid는 살펴보았고, 이번에는 Affine과 Softmax에 대해서 알아보겠습니다.

 

  • 인공신경망(ANN)의 개요 및 퍼셉트론과의 차이점
  • 인공신경망(ANN)의 활성화함수(AF, Activation Function)
  • 인공신경망(ANN)의 출력층 설계 및 순전파(Forward Propagation)
  • 인공신경망(ANN)의 손실함수(Loss) 구현
  • 수치미분 및 편미분, 그리고 GDA
  • 인공신경망(ANN)의 역전파(Back Propagation) 개념
  • 인공신경망(ANN)의 역전파(Back Propagation) 구현

 

 

0. 지난시간 정리


[ReLU 역전파]

y = np.max(0, x)의 함수로, 미분을 하게되면

dx = 0 or 1 (if, x > 0 면 1 아니면 0)

 

따라서, 구현은 chk 변수를 하나 만들어서 조건 판단을 수행하는 것으로 구현합니다.

 

[Sigmoid 역전파]

y = 1 / (1 + np.exp(-x))의 함수로, 미분을 하고 치환을 해서 y의 함수로 만들면

dx = dL( )/dout * y * (1 - y)

 

 

1. Affine


신경망의 node는 아래와 같이 활성화함수를 진입하기 전에 input 값과 weight의 변수가 곱해진 후 summation되는 부분이 있습니다.

 

 

기존에 해당부분은 결국 행렬(matrix)로 구성되기 때문에 행렬의 곱인 matrix multiply로 구현했습니다. 이는 결국 numpy의 np.dot( )인데 이를 아주 유식한 말로 행렬의 곱(Affine transfomation이라고 합니다.  

 

예를들어서 알아보면... x * w + b에 해당하는 아래의 그래프가 있다고 가정하겠습니다.

 

input으로 1 x 5형태의 np.array가 들어오고, output으로 1 x 10형태의 np.array가 나오는 구조입니다. 이를 위해서 weight는 중간에 5 x 10형태로 구성이 됩니다. 

 

방금 원하는 input과 output의 구조를 맞추기 위해서 weight의 모양을 수동으로 설정을 해줬습니다. 역전파(Back Propagation)도 마찬가지 입니다. dL( ) / dout을 통해서 넘어온 배열 모양을 기준으로 역전파를 통해서 구성해야 하는 모양을 바꾸기 위해서 적합한 모양을 찾는 것 입니다. 

 

Affine의 역전파는!! 배열의 모양을 만들기위한 블럭 찾기이다!!

 

그럼 실제 위의 예시를 기준으로 알아보겠습니다. dL( ) / dout을 통해서 역전파로 input된 배열의 모양은 out의 모양과 동일합니다. 왜냐하면, 각 원소들은 Loss Function에 입력값이 되고 broadcasting으로 계산이 되기 때문입니다. 

 

(1) 결국 dL( ) / dout의 모양은 1 x 10이 됩니다.

 

그렇다면, 다음으로 dL( ) / dx에 대해서 알아보겠습니다. x의 모양은 1 x 5입니다. 그렇다면 1 x 10을 가지고 어떻게 1 x 5를 만들 수 있을까요??

1 x 10 || 10 x 5 = 1 x 5 가 됩니다. 

10 x 5는 어디서 많이 보지 않으셨나요?? 맞습니다. 바로 weight의 전치행렬(Transpose of Matrix)입니다. 

 

(2) 결국 dL( ) / dx의 모양은, dL( ) / dout * wT로 구할 수 있습니다.

 

마지막으로, dL( ) / dw에 대해서 알아보겠습니다. w의 모양은 5 x 10으로 dL( ) / dout의 모양인 1 x 10으로 만들어야 합니다. 

5 x 1 || 1 x 10 = 5 x 10 가 됩니다.

여기서 5 x 1은 input인 x의 전치행렬(Transpose of Matrix)입니다. 

 

(3) 결국 dL( ) / dw의 모양은, xT * dL( ) / dout으로 구할 수 있습니다.

 

이 결과를 그림으로 알아보면 아래와 같습니다. 

 

- out에 대한 Loss Function 미분값은 모양 그대로 들어옴 : dL( ) / dout = (10, ) = 1 x 10

- '+' node를 통해서 동일한 값이 양쪽으로 그대로 배분됨

- 'dot' node를 통해서 입력된 전파값을 양쪽 matrix shape에 맞게 가공해서 전달함

   (1) dL( ) / dx = dL( ) / dout * wT = 1 x 5

   (2) dL( ) / dw = xT * dL( ) / dout = 5 x 10

 

 

2. Affine for mini-batch


이번에는 단일값이 아닌, 여러개의 입력값이 한번에 들어오는 mini-batch상황에서의 Affine을 알아보겠습니다.

 

여기서 바뀐 부분은, input의 x 모양이 기존 1 x 5에서 N x 5로 바뀌고... output의 모양도 기존 1 x 10에서 N x 10으로 바뀌었습니다.

 

하지만 weight와 bias에 해당하는 변수에는 전혀 변화가 없습니다. 이 경우, bias는 각 입력에 broadcasting으로 더해지게 됩니다.

 

오랫만에 broadcasting에 대해서 예를 들어서 알아보겠습니다.

import numpy as np

xdotw = np.array([[1,2,3],[4,5,6]])
b = np.array([1,1,1])

print(xdotw + b)
====================================

[[2 3 4]
 [5 6 7]]

다음과 같이 한쪽은 2 x 3 행렬이고, 나머지는 1 x 3 행렬일 경우... 각 행에 1 x 3 행렬이 투입되서 '+' 연산을 수행하게 됩니다.

 

그럼 다시 본론으로 돌아와서, mini-batch일 경우에 Affine의 역전파가 수행되는 부분은 거의 동일하며, 한가지 차이점은... bias의 '+' node에서 dL( ) / dout인 N x 10 배열이 넘어오게 되는데, 특성상 그 값을 양쪽에 그대로 전달해줍니다. 결국, bias는 1 x 10의 형태임에도 불구하고 N x 10을 전달 받게되어 문제가 발생합니다.

 

따라서, 단일입력일때와의 차이는 N x 10을 강제로 np.sum(dout, axis=0)을 통해서 1 x 10으로 변경 시켜야 합니다.

 

np.sum( axis=0)의 결과는 예를들어보면 아래와 같습니다.

dout = np.array([[1,2,3],[4,5,6]])
print(np.sum(dout, axis=0))
===================================

[5 7 9]

그럼 이제 마지막으로 Affine Node에 대한 전체 코드는 아래와 같습니다.

 

[Affine Node]

class Affine:
    def __init__(self, w, b):
        self.x = None
        self.w = w
        self.b = b

    def forward(self, x):
        self.x = x
        return np.dot(x, self.w) + self.b

    def backward(self, dout):
        db = np.sum(dout, axis=0)
        dx = np.dot(dout, self.w.T)
        dw = np.dot(self.x.T, dout)
        return db, dx, dw

[Test]

x = np.array([[1,2,3,4,5],
              [1,3,5,7,9],
              [2,4,6,8,10]])
w = np.array([[1,1,1,1,1,1,1,1,1,1],
              [2,2,2,2,2,2,2,2,2,2],
              [1,1,1,1,1,1,1,1,1,1],
              [2,2,2,2,2,2,2,2,2,2],
              [1,1,1,1,1,1,1,1,1,1]])
b = np.array([1,2,1,2,1,2,1,2,1,2])

aff = Affine(w, b)
out = aff.forward(x)
print(out)

dout = np.array([[1,2,3,4,5,1,2,3,4,5],
                 [5,4,3,2,1,5,4,3,2,1],
                 [1,2,1,2,1,2,1,2,1,2]])

db, dx, dw = aff.backward(dout)
print(db)
print(dx)
print(dw)
=========================================

[[22 23 22 23 22 23 22 23 22 23]
 [36 37 36 37 36 37 36 37 36 37]
 [43 44 43 44 43 44 43 44 43 44]]
 
[7 8 7 8 7 8 7 8 7 8]
[[30 60 30 60 30]
 [30 60 30 60 30]
 [15 30 15 30 15]]
[[ 8 10  8 10  8 10  8 10  8 10]
 [21 24 19 22 17 25 20 23 18 21]
 [34 38 30 34 26 40 32 36 28 32]
 [47 52 41 46 35 55 44 49 38 43]
 [60 66 52 58 44 70 56 62 48 54]]

 

 

3. Softmax And CEE


이미 이전에 softmax function에 대해서 알아보았습니다. 해당 함수는 각 output에 대한 출현확률을 나타내 주었고, 도출되는 값은 모두 0 ~ 1사이였습니다. 왜냐하면, 정규화(Normalize)를 거치기 때문이빈다. 마지막으로, 전체 확률의 합은 1 (=100%)이 되는 로직의 주인공입니다. 

 

이 확률의 목적은, 모델을 통해서 나온 결과와 실제 정답인 target이 가까워지게 하기 위함입니다. 따라서 학습할때는 마지막 node의 활성화함수에 Softmax를 사용하고, predict단계에서는 연산작업의 최소화를 통해서... 그리고 출력되는 결과가 가장 큰 것만 선택하면 되기 때문에, 구지 Softmax를 사용하지 않아도 된다고 했었습니다. 

 

Softmax는 3항이상의 분류를 수행할때 사용하는 함수이며, 분류의 경우 Logistic Regression에서 알아봤듯이 보통 Cross-Entropy Error를 Losss Funciton으로 사용합니다. 그래서 나온 로직이 위의 그래프 입니다.

 

input으로 x를 받아서 softmax를 수행하고, 그 결과와 target (정답지)을 기준으로 CEE(크로스엔트로피)를 수행하여 Loss를 구합니다.

 

이의 역전파는 증명에는 좀 길어서 생략을 하고, 결과적으로 softmax의 출력인 y와 정답지 t의 차이를 앞으로 전달하게 됩니다. 

역전파는 yi - ti 입니다!!!

 

예를들어, 정답이 (0, 0, 1, 0, 0) 일때...

 

(1) softmax출력이 (0.5, 0.3, 0.1, 0.1, 0) 이라면, 정답으로 선택될 확률이 10%로 학습이 많이 필요한 상황입니다.

    그의 역전파를 보면... (0.5, 0.3, -0.9, 0.1, 0)으로 -0.9인 부분이 크게 학습이 될 것 입니다.

(2) softmax출력이 (0.01, 0.01, 0.98, 0, 0)이라면, 정답으로 선택될 확률이 98%로 학습이 거의 필요가 없습니다.

    그의 역전파를 보면... (0.01, 0.01, -0.02, 0, 0)으로 모든 부분이 크게 학습이 진행되지 않을 것 입니다.

 

그렇다면, 다음으로 Softmax 및 CEE Node에 대한 전체 코드는 아래와 같습니다.

 

[Softmax and CEE Node]

def softmax(x):
    if x.ndim == 1:
        return softmax_inner(x)
    else:
        res = np.zeros_like(x)
        for i in range(x.shape[0]):
            res[i] = softmax_inner(x[i])
        return res

def softmax_inner(x):
    x_max = np.max(x)
    x_max_exp = np.exp(x - x_max)
    x_max_exp_sum = np.sum(x_max_exp)
    return x_max_exp / x_max_exp_sum

def CEE(y, t):
    alpha = 1e-7
    if t.ndim == 1:
        y = y.reshape(1, y.size)
        t = t.reshape(1, t.size)
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + alpha)) / batch_size


class SoftmaxAndCEE:
    def __init__(self):
        self.y = None
        self.t = None

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        return CEE(self.y, self.t)
        
    def backward(self, dout=1):
        if self.t.ndim == 1:
            batch_size = 1
        else:
            batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

[Test]

x = np.array([[1.0,2.0,3.0],
              [4.0,4.0,6.0],
              [7.0,9.0,10.0]])

t = np.array([[0,0,1],
              [1,0,0],
              [0,1,0]])

sac = SoftmaxAndCEE()
L = sac.forward(x, t)
print(L)

dx = sac.backward(1)
print(dx)
=================================

1.3320538242820614

[[ 0.03001019  0.08157616 -0.11158635]
 [-0.29783101  0.03550233  0.26232868]
 [ 0.01170634 -0.24683451  0.23512817]]

 

여기서 한가지 주의할 점은, mini-batch를 고려하여 구성하기 때문에, 역전파시 결과값에 batch size로 나누어줘야 합니다. 약간 평균의 느낌인 것이지요.

 


 

이렇게 오랜기간에 걸쳐서 역전파에 대해서 알아보았습니다. 사실 모델을 생성하고 학습하는 단계에 있어서 이렇게 함수를 다 구성하고 로직을 한땀한땀 만들어야 할 일은 매우 적습니다. 왜냐하면 이미 구현되어있는 어마어마한 것들이 존재하기 때문입니다.

 

이전에 제가 썼던 글에서도... Tensorflow를 통해서 구현하고, 더 나아가 High Level인 Keras를 통해서도 구현했었습니다. 

 

그때는 어땠나요?? 단순히 build, fit, evaluate, predict 등과같이 함수를 호출함으로써 모든것을 다 구현가능했습니다. 하지만.... 그래도 이렇게 오랜시간 Low Level에서 알아봄으로써 동작로직을 이해하고 , 앞으로 사용하는데 도움이 될 것이라고 믿어 의심치 않습니다.

 

- Ayotera Lab-

댓글