딥러닝(DL)/딥러닝 기초

[DL] 역전파 ( Backward-propagation )

Song 컴퓨터공학 2023. 8. 21. 01:25

 

 

다층 퍼셉트론(MLP) 같은 깊은 신경망이 등장한 후 선형으로 분류가 가능하지 않은 데이터들을 분류하는 것이 가능해졌습니다. 하지만 모델의 깊이(depth)가 깊어질수록, 즉 모델의 층이 많아질수록 모델의 가중치의 수는 기하급수적으로 많이 늘어납니다. 기존의 경사하강법(수치미분)은 기울기를 이용하여 가중치를 업데이트하는 방법을 사용하는데, 이 때 수치미분 방법으로 여러 층에 존재하는 가중치들을 모두 하나하나 업데이트하는 것은 매우 많은 연산량과 메모리를 요구합니다.

 

이를 해결하고자 고안된 알고리즘이 역전파 알고리즘(Backpropagation algorithm) 입니다.

 

역전파를 설명하는 대표적인 2가지 방법은 계산 그래프(Computational graph) 로 설명하는 방법과, 다변수 미분을 직접 수식적으로 전개해가며 연쇄 법칙(Chain Rule) 을 통해 이를 증명하는 방법이 있습니다. 오늘의 포스팅은 2번째 방법으로 직접 아주 간단한 신경망에 대해 직접 연산을 확인해볼 예정입니다.

 

 

앞서 다루었던 데이터가 입력으로 들어갔을 때 출력을 얻어내는 과정은 모두 입력층에서 출력층 방향(앞)으로 진행됩니다. 이런 형태의 신경망을 순방향 신경망(Feed Forward Neural Network, FFNN) 이라 합니다. 또한 이처럼 연산이 앞쪽으로 이동하는 과정을 순전파(Feed Forward) 라고 부릅니다.

순전파(Feed Forward)

 

순방향 신경망은 아래의 과정을 반복하며 학습을 합니다.

  • 입력층에 들어온 값을 순전파하여 예측값 계산. (이를 계산하는 층을 affine 계층이라고도 부름)
  • 타깃과 오차를 계산하여 역전파 알고리즘을 이용해 가중치를 업데이트

 

 

역전파 알고리즘은 입력층 방향(역방향)으로 오차를 전파시키며 각 층의 가중치를 업데이트합니다. 이를 이해하기 위해 은닉층이 1개, 입력층 노드 2개, 은닉층 노드 3개, 출력층 노드 2개, 바이어스는 생략한 간단한 신경망에 대해 역전파 과정을 확인해보도록 하겠습니다.

$z_i$와 $a_i$ 사이는 활성 함수를 뜻합니다. 이전과 동일한 표기로 선형 방정식을 통과한 값이 $z$, 활성 함수를 통과한 값이 $a$로 표기됩니다. 처음 입력값이 모델에 주입되면 순전파를 통해 각 노드의 값을 계산합니다.

 

이후 출력층의 노드에 있는 값과 타깃 $y_i$ 값의 차이를 이용하여 전체 손실 $L$을 구합니다. 각 $L$은 MSE로 계산됩니다.

 

 

 

 

윗 첨자는 층을 구분하고 아래 첨자는 노드를 구분합니다. 가중치의 아래 첨자는 방향을 연결되어 있는 노드 번호를 의미합니다. 순전파를 통해 L을 구하게 된다면 역전파법을 통해 가중치를 업데이트 하게 되는데, 이는 이전에 살펴봤듯 Gradient descent 로 이루어집니다.

 

[DL] 경사 하강법 ( Gradient Descent )

기계학습 문제는 학습의 목표가 최적의 매개변수를 찾는 것이고, 이는 곧 손실 함수가 최솟값이 될 때의 매개변수를 찾는 것과 동일합니다. 이 때 기울기를 사용해서 함수의 최솟값을 찾는 아이

songsite123.tistory.com

 

$$ W := W - \alpha \dfrac{\partial L}{\partial W} $$

 

$L$, $a$, $z$의 값은 각각 다음과 같습니다.

 

$$ \begin{align*} & L_1 = \dfrac{1}{2} (y_1 - a_1^{(2)})^2 \\ & a_1^{(2)} = \text{activation}(z_1^{(2)}) \\ & z_1^{(2)} = w_{11}^{(1)}a_1^{(1)} + w_{21}^{(1)}a_2^{(1)} + w_{31}^{(1)}a_3^{(1)} \end{align*}$$

 

수식은 복잡해보이지만 보면 $z$는 단순히 가중치와 노드들의 행렬곱으로 계산된 결과이고, $a$는 $z$를 활성함수에 넣고 나온 값이며, $L$은 MSE를 통해 구해낸 손실입니다. 이 때, $L_1$을 결정하는데 관여했던 가중치들은 각각 위 첨자 (1)을 달고 있는 $w_{11}^{(1)}$, $w_{21}^{(1)}$, $w_{31}^{(1)}$ 입니다. 따라서 $L_1$은 위 첨자 $(1)$을 달고 있는 가중치들을 업데이트 합니다. 기울기를 구할 때는 연쇄 법칙(chain rule) 이 적용되어 아래와 같이 표현할 수 있습니다.

 

$$ \dfrac{\partial L_1}{\partial w_{i1}^{(1)}}= \dfrac{\partial L_1}{\partial a_1^{(2)}} \dfrac{\partial a_1^{(2)}}{\partial z_1^{(2)}} \dfrac{\partial z_1^{(2)}}{\partial w_{i1}^{(1)}} $$

 

수식이 복잡해보이지만 사실 정말 간단한 의미를 가집니다. 아래 그림의 빨간 선을 확인해봅시다.

$L_1$을 가중치 중 윗첨자 $(1)$을 가지는 값들에 대해 편미분하고 싶은데, 먼저 $a_1^{(2)}$에 대해 편미분하고, 그것을 $z_1^{(2)}$에 대해 편미분하고, 그것을 각 $w_{i1}$ 에 대해 편미분한 것을 의미합니다. 합성함수의 미분법과 굉장히 유사한 과정을 거치죠.

 

위 식을 조금 수정해서 간단히 나타내도록 하겠습니다.

 

$$ \begin{align*} \dfrac{\partial L_1}{\partial w_{i1}^{(1)}} &= \dfrac{\partial L_1}{\partial a_1^{(2)}} \dfrac{\partial a_1^{(2)}}{\partial z_1^{(2)}} \dfrac{\partial z_1^{(2)}}{\partial w_{i1}^{(1)}} \\ &= \beta_1^{(2)} \times a_i^{(1)} \\ & where \quad \beta_1^{(2)} = \dfrac{\partial L_1}{\partial a_1^{(2)}} \dfrac{\partial a_1^{(2)}}{\partial z_1^{(2)}} \end{align*} $$

 

위 수식에서 구한 값에 학습률(learning rate)를 곱한 값을 원래 가중치 값에서 빼서 가중치를 갱신할 수 있습니다.

 

$L_2$에 대해서도 위와 완전 동일한 과정으로 가중치를 갱신합니다.

$$ \begin{align*} \dfrac{\partial L_2}{\partial w_{i2}^{(1)}} &= \dfrac{\partial L_2}{\partial a_2^{(2)}} \dfrac{\partial a_2^{(2)}}{\partial z_2^{(2)}} \dfrac{\partial z_2^{(2)}}{\partial w_{i2}^{(1)}} \\ &= \beta_2^{(2)} \times a_i^{(1)} \\ & where \quad \beta_2^{(2)} = \dfrac{\partial L_2}{\partial a_2^{(2)}} \dfrac{\partial a_2^{(2)}}{\partial z_2^{(2)}} \end{align*} $$

 

 

빨간 선의 과정을 위 수식으로 나타낼 수 있습니다. 이제 진짜 중요한 역전파의 의미가 나오는 부분을 확인해보겠습니다.

은닉층과 출력층 사이가 아닌, 은닉층과 입력층 사이의 가중치가 갱신되는 과정을 살펴보겠습니다.

 

 

입력층과 은닉층 사이의 가중치들은 윗 첨자로 $(0)$을 가지고 있는 가중치들을 의미합니다. 그 중에서도 먼저 $w_{11}^{0}$ 이 갱신되는 과정을 살펴봅시다. $w_{11}^{0}$ 을 갱신하기 위해서는 $w_{11}^{0}$ 에 대한 손실함수의 기울기를 알아야 합니다.

 

$$ \begin{align*} \dfrac{\partial L}{\partial w_{11}^{(0)}} &= \dfrac{\partial L}{\partial a_1^{(1)}} \dfrac{\partial a_1^{(1)}}{\partial z_1^{(1)}} \dfrac{\partial z_1^{(1)}}{\partial w_{11}^{(0)}} \\ &= \left( \textcolor{#ff0010}{\dfrac{\partial L_1}{\partial a_1^{(1)}} + \dfrac{\partial L_2}{\partial a_1^{(1)}}} \right) \dfrac{\partial a_1^{(1)}}{\partial z_1^{(1)}} \dfrac{\partial z_1^{(1)}}{\partial w_{11}^{(0)}} \end{align*} $$

 

이 식도 아래 그림과 함께 보면 그 의미를 파악하기가 쉽습니다.

$(0)$ 층의 가중치 $w_{11}^{(0)}$ 은 $L_1$과 $L_2$가 모두 오차에 관여하고 있기 때문에 위처럼 나눠 생각해야합니다. 여기서 익숙한 식이 보이는데요, 바로 앞선 윗첨자가 $(1)$인 가중치들을 구할 때 사용했던 값이 그대로 사용된다는 것입니다.

 

$$ \begin{align*} \dfrac{\partial L_1}{\partial a_1^{(1)}} &= \dfrac{\partial L_1}{\partial a_1^{(2)}} \dfrac{\partial a_1^{(2)}}{\partial z_1^{(2)}} \dfrac{\partial z_1^{(2)}}{\partial a_1^{(1)}} \\ &= \beta_1^{(2)} \times w_{11}^{(1)} \\ \dfrac{\partial L_2}{\partial a_1^{(1)}} &= \dfrac{\partial L_2}{\partial a_2^{(2)}} \dfrac{\partial a_2^{(2)}}{\partial z_2^{(2)}} \dfrac{\partial z_2^{(2)}}{\partial a_1^{(1)}} \\ &= \beta_2^{(2)} \times w_{12}^{(1)} \end{align*} $$

 

따라서 처음 식에 대입하고 정리해보면 다음과 같은 결론이 나옵니다.

 

$$ \begin{align*} \dfrac{\partial L}{\partial w_{11}^{(0)}} &= \left( \textcolor{#ff0010}{\dfrac{\partial L_1}{\partial a_1^{(1)}} + \dfrac{\partial L_2}{\partial a_1^{(1)}}} \right) \dfrac{\partial a_1^{(1)}}{\partial z_1^{(1)}} \dfrac{\partial z_1^{(1)}}{\partial w_{11}^{(0)}} \\ &= ( \textcolor{#ff0010}{\beta_1^{(2)}w_{11}^{(1)} + \beta_2^{(2)}w_{12}^{(1)}} ) \dfrac{\partial a_1^{(1)}}{\partial z_1^{(1)}} \dfrac{\partial z_1^{(1)}}{\partial w_{11}^{(0)}} \\ &= \beta_{11}^{(1)} x_1 \\ & \text{where} \quad \beta_{11}^{(1)} = ( \beta_1^{(2)}w_{11}^{(1)} + \beta_2^{(2)}w_{12}^{(1)} ) \dfrac{\partial a_1^{(1)}}{\partial z_1^{(1)}} \end{align*} $$

 

마찬가지로 이 값에 lr 을 곱해서 가중치를 갱신할 수 있습니다. 여기서 수식은 복잡해보이지만 요점은 이미 계산되었던 값을 또 사용한다 는 사실입니다. 알고리즘으로 보면 DP랑 굉장히 비슷한 개념이죠. 따라서 앞서 배웠던 수치미분으로 모든 가중치 값을 일일히 계산하는 것보다 역전파를 통해 Gradient descent 를 적용하는 것이 훨씬 빠를 수 밖에 없습니다. 지금은 아주 간단한 신경망에 대해 이를 확인해봤지만 신경망이 깊어질수록 기하급수적으로 계산량이 많아지기 때문에 역전파 법은 수치미분법과 비교할 수 없을 정도의 속도 차이가 나게 됩니다.

 

 

즉 결론적으로 역전파가 일어날 때, 이전 층에서 계산한 기울기(오차)가 다음 층에도 그대로 사용됩니다. 그렇기 때문에 오차가 역방향으로 전파되는 것처럼 보입니다. 아래에서 코드를 보면서 이해하겠지만, 실제 역전파가 하는 일은 연쇄법칙의 원리와 완전히 동일하다는 것을 알 수 있습니다. Local Gradient 를 역방향으로 계속 노드단에 전달하는 것이죠.

 

 

역전파 알고리즘(backpropagation algrithm)은 깊은 층을 갖는 신경망의 가중치를 효과적으로 업데이트하는 알고리즘입니다. 하지만 역전파 알고리즘 또한 경사 하강법의 기울기를 이용하여 오차를 줄이는 방법을 따르므로 항상 global minimum으로 수렴할 것이라는 보장이 없습니다. 또한 기울기를 이용하는 방법은 모델의 층이 깊어질수록 기울기가 사라지는(0이 되는) 기울기 소실(Gradient vanishing) 현상을 보입니다. 따라서 깊은 층을 가진 모델에서 역전파 알고리즘을 적용할 때 위와 같은 문제들을 인지해야 합니다.  

 

 

 

 

 affine 계층 역전파 구현

 

affine 계층의 역전파를 구현하기 전 아주 간단한 예시를 통해 계산 그래프로 역전파를 다시 이해해보도록 하겠습니다. affine 계층은 행렬곱을 수행하는 계층을 말하는데, 행렬곱에서는 오직 덧셈과 곱셈 연산만으로 모두 계산이 가능합니다. 따라서 덧셈과 곱셈에 대한 역전파만 만들 수 있다면 affine 계층의 역전파까지 구현이 가능합니다. 먼저 덧셈을 예시로 $z = x + y$ 라는 아주 간단한 수식의 역전파에 대해 알아보겠습니다. 각 변수에 대한 편미분은 다음과 같습니다.

 

$$ \dfrac{\partial z}{\partial x} = 1 \quad , \quad  \dfrac{\partial z}{\partial y} = 1 $$

 

아래는 이를 계산 그래프로 나타낸 모습입니다.

만약 원래 연산이 $10+5=15$였고 상류(Upstream gradient, 이전에 계산되어서 넘어온 값, 위 수식 전개에서 $\beta$를 의미) 가 $1.3$ 이라면 전달되는 오차는 아래와 같습니다.

 

 

$z=xy$ 라는 수식의 역전파에 대해 확인해보겠습니다. 이 식의 편미분은 다음과 같습니다.. 

$$ \dfrac{\partial z}{\partial x} = y \quad , \quad  \dfrac{\partial z}{\partial y} = x $$

예를 들어 $10\times 5 = 50$이라는 계산에 대해 Upstream gradient 가 $1.3$이라고 하면 계산 그래프는 아래와 같이 표현됩니다.

 

위처럼 단순한 곱셈과 덧셈 노드를 각각 $\text{MulLayer}$, $\text{AddLayer}$ 라는 이름의 클래스로 구현합니다. 모든 계층은 순전파 $\text{forward()}$, 역전파 $\text{backward()}$ 라는 공통 메소드를 가집니다. 순전파는 연산결과를 반환하고 역전파 메소드는 앞에서 계산된 미분을 이용해 연산값들을 반환합니다.

class MulLayer:
  def __init__(self):
    self.x = None
    self.y = None

  def forward(self, x, y):
    self.x = x
    self.y = y
    out = x * y
    return out

  def backward(self, dout):
    dx = dout * self.y
    dy = dout * self.x
    return dx, dy

forward() 는 x, y를 인수로 받아 곱한 값을 반환하고, backward() 는 앞에서 넘어오 미분 dout 에 순전파 때의 값을 서로 바꿔 곱한 후 반환합니다.

 

다음은 덧셈 노드에 대한 구현입니다.

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

 

덧셈과 곱셈 계층을 이용하여 사과 2개와 귤 3개를 구매하는 상황을 구현합니다.

 

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_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)

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)
print(dapple_num, dapple, dorange, dorange_num, dtax)

# 715.0000000000001
# 110.00000000000001 2.2 3.3000000000000003 165.0 650

 

그러나 위의 구현에서 곱셈은 벡터 곱셈이 아닌 스칼라 곱셈이기 때문에 실제 구현에서는 행렬곱을 위해 형상 (Shape)를 일치시켜주기 위해 전치 행렬을 사용합니다. Affine 계층에서 행렬 곱에 대한 역전파는, 순전파시 입력 신호를 서로 Switch 하여 곱해줍니다. 

 

또한 뉴럴 네트워크를 학습시킬 때는 보통 데이터를 하나씩 넣는 것이 아닌 묶어서 배치 단위로 처리합니다. 

이 때 바이어스의 차원 부분이 안 맞는 것을 확인하실 수 있는데, 이는 Repeat 노드와 Sum 노드로 설명할 수 있습니다. 이에 대한 자세한 내용은 다른 포스팅에서 다루고 링크를 추가하도록 하겠습니다.

# Affine 계층 구현 하기 

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 # Affine 변환, Y = XW + B
    # 바이어스에 대한 Repeat 노드
    

    return out 

  def backward(self, dout):  
    dx = np.dot(dout, self.W.T) # Shape을 고려, 역전파의 이전 입력신호 dout과 W의 전치행렬을 내적한다. 
    
    self.dW = np.dot(self.x.T, dout) # Shape을 고려, x의 전치행렬과 역전파의 이전 입력신호 dout을 내적한다. 
    
    self.db = np.sum(dout, axis = 0) 
    # Bias는 역전파의 입력신호를 순전파의 입력신호의 형상과 일치시키기위해 Sum을 통해 원소수를 일치시킨다.   
    # 바이어스에 대한 Sum 노드
    
    return dx

 

 

 sigmoid & ReLU 계층 역전파 구현

 

 

[DL] 활성 함수 ( Activation function )

활성 함수란? ( Heaviside, Sigmoid, ReLU ) 지난 포스팅에서 살펴본 퍼셉트론의 구조에 대한 그림을 다시 살펴보면 오른쪽에 활성 함수(Activation Function) 이라는 것이 있습니다. 다시 한 번 퍼셉트론을 나

songsite123.tistory.com

시그모이드 함수는 아래와 같습니다. 

$$ y = \dfrac{1}{1+e^{-x}} $$

이 함수를 미분하면 이와 같은 결론이 나온다는 것도 위 포스팅을 통해 확인해 보았습니다. 위 식을 통해 미분 문제를 미분이 아닌 단순 사칙연산 문제로 바꿔서 풀 수 있습니다. 시그모이드 함수를 계산그래프로 나타내면 아래와 같습니다.

계산 그래프로 함수를 이해한다는 것은 위처럼 하나하나 전부 쪼개서 생각한다는 것입니다. 이렇게 생각해야 역전파를 적용시키기가 훨씬 쉽습니다. 그리고 우선 위에서 우리가 했던 결론에 의해 시그모이드의 역전파는 아래와 같이 생각할 수도 있습니다.

위처럼 $y(1-y)$ 형태로 바뀌는 것은 알겠는데, 이 과정이 어떻게 나왔는지 계산 그래프에서 한 단계씩 확인해보도록 하겠습니다.

 

 

역전파에서 맨 처음 나눗셈 노드를 역전파하는 과정이 왜 $-y^2$이 곱해지는지는 치환을해서 생각해보면 쉽습니다.

 

나눗셈 노드의 순전파에 $t$ 라는 입력값이 들어갔다고 생각해보면, $y$ 는 $t$의 역수이기 때문에 $y=\dfrac{1}{t}$ 가 성립합니다. 이 상황에서 $\dfrac{\partial L}{\partial y} $ 를 구해야 하므로, 결국 $L(y) = L(\dfrac{1}{t})$ 라는 함수를 미분해야 합니다.

 

$\dfrac{d}{dt} \left( L(\dfrac{1}{t}) \right)  =  L'(\dfrac{1}{t}) \times (-\dfrac{1}{t^2})$ 이 성립하고, $y = \dfrac{1}{t} $ 를 왼쪽의 식에 대입하면 결론적으로 $-y^2$ 을 흘러들어왔던 기울기에 그대로 곱해주는 것과 같은 결과가 나타납니다.

 

더하기는 그대로, 지수 함수도 이와 비슷한 방식으로 합성함수의 미분처럼 생각하여 구하면 결론적으로 나타나는 식은 $ \dfrac{\partial L}{\partial y} y^2 \text{exp} (-x) $ 입니다. 이 식을 정리해보면

 

$$ \begin{align*} \dfrac{\partial L}{\partial y} y^2 \text{exp} (-x) &= \dfrac{\partial L}{\partial y} \dfrac{1}{(1+\text{exp} (-x))^2} \text{exp} (-x) \\ &= \dfrac{\partial L}{\partial y} \dfrac{1}{1 + \text{exp} (-x)} \dfrac{\text{exp} (-x)}{1+\text{exp} (-x)} \\ &= \dfrac{\partial L}{\partial y} y(1-y) \end{align*} $$

 

 

이렇게 구한 결론 또한 똑같이 나오게 되는 것이죠. 코드는 매우 간단합니다.

class Sigmoid:
  def __init__(self):
      self.out = None

  def forward(self, x):
      out = sigmoid(x)
      self.out = out
      return out

  def backward(self, dout):
      dx = dout * (1.0 - self.out) * self.out

      return dx

순전파는 데이터가 들어오게 되면 시그모이드를 통과한 출력을 반환합니다. 역전파는 위에서 보았듯 미분 과정이 사칙연산 과정으로 대체 되기 때문에 흘러들어온 값에 $(1-y)y$ 를 곱한 값을 반환합니다. self.out 을 $y$라고 생각하고, dout을 $ \dfrac{\partial L}{ \partial y} $ 라고 생각하시면 됩니다.

 

 

 

그런데 sigmoid 함수는 gradient vasnishing 문제가 있기 때문에 상대적으로 잘 안쓰입니다. ReLU 함수가 더 많이 쓰이죠. ReLU 함수와 그 도함수는 아래와 같습니다.

 

$$ y=\begin{cases}0,\ \ \ \ x\le 0\\x,\ \ \ \ x>0\end{cases} \quad , \quad \dfrac{\partial y}{\partial x} =\begin{cases}0,\ \ \ \ x\le 0\\1,\ \ \ \ x>0\end{cases} $$

 

 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

self.mask 는 논리식으로 이루어져 있기 때문에 x가 행렬이라면, self.mask 또한 T, F로 이루어진 행렬이 됩니다. 그 다음으로 들어왔던 x를 out에 copy 하고, out의 인덱스에 self.mask 가 들어간다는 의미는 곧 F로 구성된 인덱스는 모두 0으로 만들고, T로 구성된 인덱스는 모두 살리는, 즉 들어온 x의 원소 하나하나에 대해 0보다 작은 것을 모두 0 값으로 만드는 masking을 수행하는 것입니다. ReLU 의 의미 그대로이죠.

 

그 다음으로 역전파입니다. 역전파에서 0을 보낼지, $\dfrac{\partial L}{ \partial y} $ 를 보낼지는 흘러들어온 기울기의 부호가 결정하는 것이 아닌, 순전파시 사용했던 $x$의 부호에 따라 결정됩니다. 이번에도 아까 self.mask 를 사용해 흘러들어온 dout 에 대해 masking을 수행합니다.

 

 

 

 Softmax with loss 계층 역전파 구현

 

이제 남은 층은 일반적인 벡터를 확률 벡터로 바꾸어주는 소프트맥스 계층과  소프트맥스에서 추출된 확률 벡터와 라벨을 원핫 인코딩 시켜 만든 확률 벡터와의 거리를 재는 크로스 엔트로피 계층을 확인해보면 됩니다.

그런데 특이하게도 이 2개의 계층은 각각의 도함수보다 이 둘을 합성한 후의 도함수가 훨씬 간단합니다. 따라서 이 두 개의 계층을 합쳐서 역전파를 생각할 것이고, 그 계층을 Softmax with Loss 계층 이라고 부르도록 하겠습니다.

 

3차원 벡터를 내보내는 예를 통해서 직접 $L$을 Softmax의 입력으로 표현해봅시다. 가장 첫줄은 크로스 엔트로피의 정의에 의해 수식을 풀어주고, 두 번째 줄은 소프트맥스의 정의대로 수식을 전개합니다.

 

3번째 줄에서는 로그의 나눗셈 형태들을 전부 뺄셈과 덧셈으로 나누어줍니다. 4번째 줄은 수식을 정리합니다.

 

이 함수에 대해서 입력 $a_i$에 대해 편미분을 해보면 결론적으로 Softmax with Loss 층의 편미분은 뺄셈 으로 구현된다. 라는 결론이 나옵니다. 시그모이드에서 미분이 곱셈과 덧셈 등으로 바뀐것처럼 Softmax with Loss 층의 미분 또한 사칙연산으로 쉽게 구현됩니다.

 

계산 그래프로도 표현하면 아래와 같습니다. 이 둘을 합친 층의 역전파는 뺄셈으로 아주 간단히 나타난다는 것이죠.

 

따라서 이의 구현도 매우 쉽습니다. 역전파 때 전파하는 값을 배치로 나눠서 개당 오차를 앞 계층에 전파하는 것을 짚고 넘어가시면 될 것 같습니다.

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.t.size == self.y.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

 

 

지금까지는 이 둘을 합친 계층에 대해서만 확인을 해보았는데, 각 계층에서 어떤 식으로 역전파가 진행되는지도 확인해보겠습니다.

각각 소프트 맥스와 크로스 엔트로피의 순전파 계층은 위 사진의 왼쪽 / 오른쪽으로 구분됩니다.

 

 

크로스 엔트로피 역전파
소프트맥스 역전파

 

 

 

Reference :

밑바닥부터 시작하는 딥러닝 (저자 : 사이토 고키 / 번역 : 이복연 / 출판사 : 한빛미디어)

https://github.com/WegraLee/deep-learning-from-scratch

https://towardsdatascience.com/understanding-backpropagation-algorithm-7bb3aa2f95fd

https://blog.naver.com/samsjang/221033626685

https://yhyun225.tistory.com/

https://cumulu-s.tistory.com/