🔊 해당 포스팅은 밑바닥부터 시작하는 딥러닝 1권의 교재 내용을 기반으로 딥러닝 신경망을 Tensorflow, Pytorch와 같은 딥러닝 프레임워크를 사용하지 않고 순수한 Numpy로 구현하면서 딥러닝의 기초를 탄탄히 하고자 하는 목적 하에 게시되는 포스팅입니다. 내용은 주로 필자가 중요하다고 생각되는 내용 위주로 작성되었음을 알려드립니다.
저번 포스팅에서 오차역전파 개념을 계산 그래프로 이해하고 간단한 곱셈, 덧셈 연산에 대한 오차역전파를 넘파이로 구현하는 방법에 대해 알아보았다. 이번 포스팅에서는 곱셈, 덧셈 연산을 넘어서서 나눗셈과 활성화 함수 연산에 대한 오차역전파가 어떻게 동작하는지 이해해보고 이를 넘파이로 구현하는 방법에 대해서도 알아보자.
신경망 모델은 덧셈, 곱셈 연산도 존재하지만 가장 핵심은 활성화 함수 연산이다. 활성화 함수 종류에 따라 오차역전파 방법이 차이가 있는데, 하나씩 살펴보자.
1. ReLU 계층
ReLU 라는 활성화 함수는 $x$가 0보다 작거나 같을 때 $y$를 모두 0으로 차단해버리는 활성함수이다. 수식은 아래와 같다.
$$y = \begin{cases} x & (x > 0) \\ 0 & (x \le 0) \end{cases}$$
위 ReLU 함수가 있을 때, $x$에 대한 $y$의 미분은 아래와 같다.
$${\partial y \over \partial x} = \begin{cases} 1 & (x > 0) \\ 0 & (x \le 0) \end{cases}$$
위 미분값들이 오차역전파를 수행할 때 의미하는 바는 다음과 같다. 순전파 시 입력 데이터인 $x$가 0보다 컸다면 이를 역전파 수행할 때 이전 노드에서 흘러들어온 국소적인 미분 값을 그대로 흘려보내는 것을 의미한다. 왜냐하면 $x$가 0보다 클 때의 미분값이 1이기 때문에 1을 곱해주는 것은 결국 이전 값을 그대로 전달하는 것과 마찬가지이다.
반대로 순전파 시 $x$가 0 이하였다면 이를 역전파 수행할 때 이전 노드에서 흘러들어온 국소적인 미분 값을 전달하지 않는 것을 의미한다. 왜냐하면 $x$가 0 이하일 때의 미분값이 0이기 때문에 이전 노드에서 어떤 값이 흘러나왔던 간에 0을 곱하면 0이 되기 때문이다.
이러한 특성을 기반으로 ReLU 함수를 사용한 오차역전파 계층을 넘파이로 구현하면 아래와 같다.
import numpy as np
# Relu 활성함수의 역전파 계층 만들기
class Relu:
def __init__(self):
self.mask = None
def forward(self, x: np.array):
self.mask = (x <= 0) # 배열 원소값이 0이하인 값들에 대한 Boolean 인덱싱
out = x.copy()
out[self.mask] = 0
return out
def backward(self, d_out):
d_out[self.mask] = 0 # 순전파 시 입력값이 0이하인 데이터에 대한 국소적인 미분값은 0으로 전달하도록 변환
dx = d_out
return dx
2. Sigmoid 계층
다음은 시그모이드 활성함수이다. 시그모이드 활성함수의 수식을 다시 상기시켜보자.
$$y = \frac{1}{1+exp(-x)}$$
시그모이드 함수 연산을 계산 그래프로 펼쳐보면 아래와 같아진다.
위 계산 그래프를 이제 오른쪽에서 왼쪽으로 진행하면서 각 연산(동그라미)에 대해 오차역전파를 수행하는 방법에 대해 알아보자. 먼저 / 인 '나누기' 연산에 대한 방법이다.
2-1단계: 나누기 연산
우선 나누기 연산의 결과값에서 $1+exp(-x)$를 새로운 변수 $t$로 정의해보자. 그러면 아래와 같이 수식이 변경된다.
$$y = \frac{1}{1+exp(-x)} = \frac{1}{t}$$
이제 $y = \frac{1}{t}$를 $t$에 대해서 $y$를 미분하면 아래와 같이 변형된다.
$${\partial y \over \partial t} = - \frac{1}{t^2}$$
그런데, 우리는 위에서 $y = \frac{1}{t}$라고 정의했으니 미분한 식의 $t$에 $y$를 집어넣으면 아래와 같이 된다.
$${\partial y \over \partial t} = - \frac{1}{t^2} = -{\left( \frac{1}{t} \right)}^2 =-y^2$$
이는 결국 나누기 연산에 대한 역전파를 수행할 때 다음으로 전달할 국소적인 미분값은 순전파 시 나누기 연산을 거치고 난 후 나온 계산결과 값인 $y$를 제곱하고 음수(-)를 붙여준 값(A라고 하자)에 이전 노드에서 흘러나오는 국소적인 미분값(B)을 곱하면 된다.(A x B) 도식화하면 아래와 같다.
2-2단계: 덧셈 연산
저번 포스팅에서 덧셈 연산에 대해서는 배웠다. 덧셈 연산은 이전에서 흘러들어온 국소적인 미분 값을 건드리지 않고 그대로 흘려보낸다. 따라서 덧셈연산에 대한 역전파를 수행한 후의 도식화는 아래와 같다.
2-3단계: exp(Exponential) 연산
exp 연산일 때의 미분은 해당 exp 연산으로 들어온 입력값을 exp 연산한 것이다. 예를 들어, $t$가 exp 연산으로 들어온 입력 데이터라고 가정하고 $y$는 exp 연산을 수행한 후의 결과값($y = exp(t)$)일 때, $t$에 대한 $y$의 미분은 아래와 같다.
$${\partial y \over \partial t} = exp(t)$$
따라서 아래 도식화 그림에 적용해보면 exp 연산에 $-x$라는 입력 데이터가 들어갔으므로 exp 연산에 대한 미분값을 계산할 때는 이전으로부터 흘러들어온 국소적인 미분값에 $exp(-x)$만 곱해주면 된다.
2-4단계: 곱셈 연산
이제 마지막 곱셈 연산이다. 이에 대해서도 저번 포스팅에서 배웠다. 곱셈 연산은 해당 연산에 순전파로 들어온 입력 데이터 2개를 역전파 시에는 서로 바꾸어 주어 곱해주면 된다.
따라서 최종적으로 입력 데이터 $x$에 대한 최종 결과값 $y$의 미분값은 아래와 같다.
$${\partial y \over \partial x} = {\partial L \over \partial y}y^2exp(-x)$$
그런데, 여기서 위 최종 미분값을 보면 입력 $x$와 최종 출력$y$의 값들로만 이루어진 것을 알 수 있다!(${\partial L \over \partial y}$ 값은 어차피 1이다)따라서 우리는 시그모이드 활섬함수 계층에서 입력 데이터 $x$에 대한 결과값 $y$의 미분값을 구하기 위해서는 위와 같이 모든 중간 연산 노드를 거치지 않고 $x$와 $y$ 값으로만으로도 미분값을 구할 수 있게 된다.(입력 데이터 $x$와 최종 아웃풋인 $y$는 항상 우리가 알 수 있는 값들이지 않은가!) 따라서 이러한 중간과정 생략으로 인해 오차역전파 방법이 효율적인 계산을 수행할 수 있게 된다.
심지어 $y$값 하나만 활용해서도 $x$에 대한 $y$의 최종 미분값을 정의할 수 있다. 시그모이드 함수 정의인 $y = \frac{1}{1+exp(-x)}$를 활용하면 아래처럼 수식이 더 간결화된다.
$$\begin{matrix} {\partial L \over \partial y}y^2exp(-x) &=& {\partial L \over \partial y} \frac{1}{(1+exp(-x))^2}exp(-x) \\ &=& {\partial L \over \partial y} \frac{1}{1+exp(-x)} \frac{exp(-x)}{1+exp(-x)} \\ &=& {\partial L \over \partial y} y(1-y) \end{matrix}$$
결국에는 시그모이드 함수 계층의 역전파는 순전파의 최종 출력인 $y$값으로만 계산할 수 있음을 알 수 있다! 이렇게 중간과정을 모두 생략한 후의 시그모이드 함수 계층의 역전파를 그린 도식화를 나타내면 아래와 같다.
이렇게 간소화된 시그모이드 역전파 방법을 기반으로 넘파이로 구현한 소스코드는 아래와 같다.
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x: np.array):
# sigmoid
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(self, dout):
dx = dout * self.out * (1.0 - self.out)
return dx
3. Affine 계층
Affine은 행렬의 곱을 의미한다. 즉, 신경망은 순전파를 수행할 때, 행렬의 곱을 수행한다. 예를 들어, $X * W + b$ 처럼 말이다. 어파인(Affine)은 기하학에서 행렬의 곱을 의미한다고 해서 여기서는 행렬의 곱 연산에 대한 순전파, 오차역전파를 수행하는 계층을 Affine 계층이라고 부르기로 한다.
지난 [밑시딥] 첫 포스팅에서 신경망을 직접 구현하기 위해 알아야 하는 필수 요소로서 행렬 곱을 수행할 때 형상(shape) 아다리를 맞춰주어야 한다고 했다. 행렬 곱 연산 시 형상을 맞춰주는 부분에 대해 잘 모른다면 해당 포스팅을 살펴보기로 하고 여기서는 간단하게 상기시키는 그림으로 설명을 대체한다. 아래의 빨간색 부분끼리 숫자를 맞춰주어야 함을 기억하자!
자, 이제 그렇다면 행렬 곱 연산이 다음과 같이 있다고 하자.
$$Y = X \cdot W + b$$
이 때, $X$의 shape은 (2,) $W$의 shape은 (2, 3)이며 $Y$의 결과 shape은 (3,) 이라고 하자. 이렇게 되면 $b$의 shape도 (3,)이 될 것이다. 그럴 때 계산 그래프는 아래와 같다.
이제 역전파를 수행할 때, 각 파라미터들($X$, $W$, $b$)의 최종 미분 값이 어떻게 되는지 살펴보자. 기본적인 역전파 과정은 그동안 배웠던 과정과 유사하다. 단, 그동안은 간선 위에 있는 값들이 스칼라 값이었지만 이제 입력 데이터들이 행렬이기 때문에 계산결과값들도 모두 '행렬'이라는 것을 인지하자.
우선 위 그림에서 노란색으로 되어 있는 부분은 잠깐 제쳐두고 빨간색 글씨로 되어있는 부분을 보자. 가장 오른쪽의 ${\partial L\over\partial Y}$를 보면 shape이 (3,) 인 것을 볼 수 있다. 왜그럴까? 당연히 순전파 시 출력값이 $Y$의 shape가 (3,)이기 때문에 이에 대한 변화량 shape은 동일하게 (3,) 이다.
이제 역전파의 첫 출발인 덧셈(+) 노드를 살펴보자. 덧셈 노드에 대한 역전파 방법은 이미 배웠다. 단순히 이전 노드로부터 흘러들어온 국소적인 미분 값을 그냥 전달하기만 하면 된다. 따라서 $X \cdot W$와 $b$에 대한 미분값도 그대로 ${\partial L\over\partial Y}$가 되며 이들의 shape도 (3,)로 동일하다.
다음은 dot 연산($X \cdot W$의 $ \cdot $을 의미)이다. dot 연산을 수행하기 이전에 한 가지 상기시켜야 할 사실이 있다. 오차역전파는 파라미터들의 변화량을 구함으로써 기존 파라미터 값에 더해주거나 빼주기 위함이 목적이다. 따라서 순전파 시 입력 파라미터였던 $X, W, b$들의 입장에서 생각했을 때, 오차역전파의 결과로 얻은 각 파라미터의 변화량이 담긴 행렬의 shape가 각 파라미터의 shape과 동일해야 한다. 예를 들어, $X$ 파라미터 shape가 (2,) 이다. 그런데 $X$ 파라미터를 변화시키려면 당연히 $X$에 대한 변화량이 담긴 행렬의 shape도 (2,) 여야 한다!
이 점을 인지하고 다시 위 그림의 ①을 살펴보자. ①은 현재 $X$ 파라미터에 대한 변화량이다. 따라서 ①의 shape도 무조건 (2,) 이어야 한다. 그렇다면 이전 덧셈노드를 통과해서 흘러들어온 shape가 (3, )인 ${\partial L\over\partial Y}$ 에다가 어떤 shape의 행렬을 곱해주어야 ①의 shape인 (2, )가 될까?
위 그림을 보면 빈칸에 들어가야 하는 shape가 (3, 2)가 되어야 하며 결국 $W$의 shape가 (2, 3)이기 때문에 $W$를 전치행렬 시켜준 $W^T$가 들어가게 된다.
이제 그렇다면 ②번에 대해서도 shape를 찾아내보자. 지금 예시에서는 데이터가 1개이기 때문에 $X$ shape가 (2,)로 되어 있지만 실질적으로 배치 사이즈가 1이라고 생각하면 (1, 2)가 된다. 아래 그림에서는 (1, 2)로 reshape 변환해주었다고 가정하고 보자.
위 그림에서도 알 수 있듯이 빈칸에는 $X^T$의 shape가 들어가야 함을 알 수 있다. 따라서 지금 찾아낸 shape 2개를 위에서 본 계산 그래프에 반영해서 그려보면 아래와 같아진다.
한 가지 주의해야 할 점은 $b$라는 편향에 대한 미분값을 적용할 때이다. 위에서는 데이터가 1개일 때의 계산 그래프이기 때문에 $b$ 변화량을 $b$에 더해줄 때 문제가 없어보이지만, 데이터가 N개(배치 사이즈가 N)일 때 계산 그래프로 확장해보면 다음과 같아진다.
그림의 초록색 글씨를 보면 두 개의 shape가 다른 것을 알 수 있다. 이럴 땐 어떻게 할까? 이 때는 shape가 (N, 3)인 ${\partial L\over\partial Y}$를 열 방향(axis=0)으로 원소를 더해주어 shape를 (3, )로 동일하게 만들어준다.(이는 추후에 넘파이로 구현할 때, np.sum(array, axis=0)을 왜 사용하는지 이해하는 데 결정적인 역할을 한다) 즉, 편향인 $b$의 미분값을 N개의 데이터마다 더해서 구하게 된다.
Affine 계층을 넘파이로 구현하는 소스코드는 아래와 같다. 해당 소스코드는 4차원 데이터의 경우를 고려한 것은 아니이다. 4차원 데이터까지 모두 고려한 Affine 계층 구현 코드는 저자 코드를 확인하자.
# Affine(행렬 곱) 계층 구현 -> 4차원인 경우 고려한 코드랑 차이가 있음!
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):
# out = x * W + b
self.x = x
out = np.matmul(x, self.W) + self.b
return out
def backward(self, d_out):
dx = np.matmul(d_out, self.W.T)
self.dW = np.matmul(self.x.T, d_out)
self.db = np.sum(d_out, axis=0)
return dx
4. Softmax-with-Loss 계층
마지막은 Softmax-with-Loss 계층이다. 여기서 Loss는 크로스 엔트로피(CEE)를 의미한다. 결국 Softmax와 CEE 함수가 함께 있는 계층의 역전파 방법을 알아보자. 소프트맥스는 이전에 알아본 것처럼 최종 출력값을 정규화(Normalization)시켜주어 0과 1사이의 일종의 확률 값으로 만들어 준다. 또 소프트맥스의 출력값의 합은 1이 된다.
번외로 잠깐 소프트맥스 함수에 대해 이야기하자면, 소프트맥스 계층은 보통 신경망 모델을 학습시킬 때만 사용하고 테스트 데이터에 대해 예측 즉, '추론'하는 단계에서는 보통 사용하지 않는다. 왜냐하면 소프트맥스 계층은 소프트맥스를 통과시켜 나온 예측 출력값과 실제 정답 간의 차이(손실 함수)를 0으로 만드는 파라미터 변화량을 찾는 즉, 학습할 때에 기여하기 때문이다. 다시 말해, 소프트맥스 함수는 파라미터 최적화하는 데 크게 기여한다는 것이다. 하지만 데이터를 예측하는 '추론' 단계에서는 단순히 출력값이 큰 것을 최종 출력으로 내뱉기만 하면 되므로 손실함수를 건드릴 필요가 없다. 따라서 소프트맥스를 추론 단계에서는 보통 사용하지 않는다.(심지어 소프트맥스가 지수함수로 이루어져 있기 때문에 불필요한 소프트맥스 함수 계산은 리소스 낭비를 초래할 수도 있다)
Softmax-with-Loss 계층의 구체적인 계산 그래프는 다른 것들보다 복잡하다. 책에서도 계산 그래프를 통해 구체적인 역전파 과정을 부록으로 다루고 있는데, 필자는 한 번 이해해보기 위해서 부록 내용을 읽고 이해한 내용 기반으로 포스팅하려고 한다.(혹시라도 부득이하게 이해가 안되는 설명이 있다면 답글 달아주시면 꼭 답변 드리겠습니다!)
먼저 이 과정의 결론인 Softmax-with-Loss 계층의 최종 미분값부터 한 번 보자.
위 그림에서 $t_i$는 정답 레이블, $y_i$는 출력값, $L$은 Loss(오차)를 의미한다. 이럴 때, Softmax-with-Loss 계층의 입력 데이터인 $a_1, a_2, a_3$에 대한 최종 미분값은 각각 $y_1-t_1, y_2-t_2, y_3-t_3$가 된다. 시그모이드 계층 때처럼 최종 미분값이 매우 간단한 형태로 나오는 것을 볼 수 있다. 출력값인 $y$와 정답인 $t$만 알면 구할 수 있다는 것! 그렇다면 어떤 과정을 거치길래 이렇게 간단하게 나올까?
4-1. 순전파
먼저 순전파를 수행하면서 계산 그래프를 살펴보자. 그 중에서도 Softmax 계층이 먼저 나오니 Softmax 계층부터 살펴보자. 계산 그래프는 아래와 같다. 소프트맥스 함수 수식은 아래와 같다.
$$y_k = \frac{exp(a_k)}{\sum_{i=1}^n exp(a_i)}$$
다음은 Cross-Entropy Error(CEE) 수식과 CEE 계층에 대한 순전파 계산 그래프이다.
$$L = - \sum_{k} t_klogy_k$$
위 CEE 계층의 시작 결과값은 위에서 살펴보았던 Softmax의 마지막 계산 결과값에 이어서 진행된다.
4-2. 역전파 - Cross Entropy Error 계층
자, 이제 역전파를 수행해보자. 그동안 배웠던 덧셈,곱셈,나누기,exp 등 각 연산에 대한 역전파를 수행했을 때 어떻게 했는지 상기시키면서 진행하자. 역전파는 오른쪽에서 왼쪽으로 진행하므로 CEE 계층 먼저 역전파를 수행해야 한다.
가장 먼저 오른쪽의 'x' 인 곱셈 연산이다. 곱셈 연산은 순전파를 수행할 때 들어간 입력 데이터를 서로 바꾸어주어 이전노드로부터 흘러들어온 국소적인 미분 값에 곱해준다는 것을 배웠다. 그러므로 곱셈 연산을 거칠 때의 역전파 결과는 아래와 같다.
다음은 '+'인 덧셈 연산이다. 덧셈 연산은 이전 연산노드로부터 흘러들어온 국소적인 미분값을 그대로 다음으로 전달한다고 했으므로 아래와 같이 된다.
다음은 3개의 'x' 곱셈 연산들이다. 이것도 마찬가지로 순전파를 수행할 때 들어간 입력 데이터를 서로 바꾸어주고 이전노드로부터 흘러들어온 국소적인 미분값에 곱해준다.
다음은 3개의 $log$ 연산에 대해 역전파를 수행할 차례이다. 지금까지 $log$ 연산에 대해 미분을 수행했을 때 어떻게 되는지에 대해서는 소개하지 않았으니 여기서 알아보겠다. 우선 $y = logx$라는 로그함수를 $x$에 대한 $y$값을 미분하게 되면 아래와 같이 된다.
$${\partial y \over \partial x} = \frac{1}{x}$$
결국, $log$ 함수를 미분한 값은 순전파 시 입력 데이터를 분모로 하고 분자는 1인 값이 된다. 이를 계산 그래프에 적용해 3개의 $log$ 연산에 대해 역전파를 수행하면 다음과 같이 CEE 계층에서의 최종 미분값이 나오게 된다.
이렇게 CEE 계층에서 나오는 $y_i$ 값들에 대한 최종적인 미분값은 $-\frac{t_1}{y_1}, -\frac{t_2}{y_2}, -\frac{t_3}{y_3}$가 된다. 이제 이 3개의 값들을 다시 시작점으로 하여 Softmax 계층 역전파를 수행해보자.
4-3. 역전파 - Softmax 계층
Softmax 계층 역전파는 위의 CEE 계층보다 약간 더 복잡하기에 단계로 목차를 나누어서 알아보자.
1단계
가장 먼저 CEE 계층의 마지막 역전파 미분값인 $-\frac{t_1}{y_1}, -\frac{t_2}{y_2}, -\frac{t_3}{y_3}$가 흘러들어오면서 아래의 빨간색 동그라미 부분의 'x' 곱셈 연산 노드가 시작된다. 곱셈 연산 노드에 대한 역전파는 어떻게 했는지 이제 자주 상기했기에 바로 기억할 것이다. 순전파 시 해당 연산 노드에 들어간 입력 데이터를 서로 바꾸어 주는 것! 아래처럼 말이다.
위 그림을 보면 총 두 종류의 미분 값이 갱신되었다. 바로 ①번으로 표시되어 있는 노란색 칸, ②번으로 표시되어 있는 초록색 칸이다. 이 2개 값이 어떻게 계산되었는지 하나씩 확인해보자. 먼저 ①번이다. ①번 값을 계산하기 위해서는 이전으로부터 흘러들어온 국소적인 미분값인 $-\frac{t_1}{y_1}$ 에다가 순전파 시 다른 방향으로 들어온 입력 데이터인 $exp(a_1)$을 곱해주어 아래와 같은 식이 된다.
$$-\frac{t_1}{y_1}exp(a_1)$$
그런데 위 수식에서 우리는 $y_1 = \frac{exp(a_1)}{S}$라는 것을 알고 있다. 이를 위 수식의 $y_1$에 대입하게 되면 아래와 같이 식이 전개된다.
$$\begin{matrix}-\frac{t_1}{y_1}exp(a_1) &=& -t_1\frac{S}{exp(a_1)}exp(a_1) \\ &=& -t_1S \end{matrix}$$
다음은 ②번이다. ②번값을 계산하기 위해서도 이전으로부터 흘러들어온 국소적인 미분값인 $-\frac{t_1}{y_1}$에다가 순전파 시 다른 방향으로 들어온 입력 데이터 즉, 이번에는 $\frac{1}{S}$를 곱해주어 $-\frac{t_1}{y_1} \cdot \frac{1}{S}$ 라는 식이 된다. 이 때, 또 우리는 $y_1 = \frac{exp(a_1)}{S}$를 알고있기 때문에 이를 활용하면 최종적으로 수식은 아래와 같이 전개된다.
$$\begin{matrix}-\frac{t_1}{y_1}\frac{1}{S} &=& -t_1\frac{S}{exp(a_1)}\frac{1}{S} \\ &=& -\frac{t_1}{exp(a_1)} \end{matrix}$$
S가 분모, 분자에 모두 있기 때문에 약분이 되어 S가 없어지고 결국 $-\frac{t_1}{exp(a_1)}$이 된다.
2단계
다음은 '/' 나누기 연산 차례이다. 그런데 나누기 연산 노드에서 순전파 시 3개의 계산결과로 갈라졌던 과거(?)가 있다. 그렇기 때문에 이 연산 노드에 대해 역전파를 수행할 때도 3개로 흘러들어오는 3개의 국소적인 미분값을 더해주어서 취합한 후에 역전파를 수행해준다.(이를 분기 노드의 역전파라고 하여 추후 포스팅에서 배운다)
나누기 연산에 대한 역전파를 수행하는 방법은 위에서도 알아보았듯이 순전파 시의 계산결과(출력값)을 제곱한 후 음수를 붙인 값에다가 이전으로부터 흘러들어오는 국소적인 미분값을 곱해준다.
위 그림에서 나누기 연산에 대한 순전파 시의 계산결과는 $\frac{1}{S}$이다. 그러면 이 값을 제곱한 후 음수를 붙이게 되면 $-\frac{1}{S^2}$(A라고 하자)이 된다. 다음으로 이전으로부터 흘러들어오는 국소적인 미분값은 3가지 갈래의 국소적인 미분값을 모두 더해주어야 하기 때문에 $(-t_1S) + (-t_2S) + (-t_3S)$ 이기 때문에 정리하게 되면 $-S(t_1 + t_2 + t_3)$(B라고 하자)가 된다. 그럼 이제 이 2개의 값(A와 B)을 서로 곱해주면 $-S(t_1 + t_2 + t_3) \cdot -\frac{1}{S^2} = \frac{1}{S}(t_1 + t_2 + t_3)$으로 나누기 연산에 대한 역전파 미분값이 계산된다.
그런데 이 때 중요한 점이 하나 있다. $t_1, t_2, t_3$는 현재 원-핫 인코딩된 형태이며 결국 $t_1, t_2, t_3$ 중에 1개만 1이고 나머지는 무조건 0이다. 따라서 $t_1 + t_2 + t_3 = 1$ 이되어서 최종 미분값은 $\frac{1}{S}$가 된다.
3단계
다음은 '+' 덧셈 연산에 대한 역전파이다. 덧셈은 이전노드로부터 흘러들어오는 국소적인 미분값을 그대로 전달한다고 했다. 따라서 아래와 같이 된다.(단, 이 때, 순전파 시, $exp(a_1), exp(a_2), exp(a_3)$을 더했었다. 이를 sum 연산이라고 하는데, 이 sum에 대해 역전파를 수행할 때는 국소적인 미분값을 그냥 복제(repeat)해서 전달해주면 된다. sum 연산에 대해서도 추후 포스팅에서 다룬다)
4단계
이제 마지막인 exp(Exponential) 연산이다. 이 exp 연산도 순전파 시 exp 연산을 통과하면서 2개로 갈라졌던 과거(?)가 있다. 따라서 역전파를 수행할 때도 갈라진 2개로부터 국소적인 미분값이 2개 들어오기 떄문에 이 2개를 취합해서 역전파를 수행해준다.(위에서 Softmax의 '/' 나누기 연산 역전파를 수행했을 때와 동일하게!)
exp 연산의 역전파는 순전파 시 exp 연산으로 들어온 입력 데이터를 exp 연산한 값이다. 위에서도 말했지만 까먹었을 수도 있으니.. 다시 설명하자면 exp 연산의 순전파 시 입력 데이터가 $x$라고 했었다면, 이 exp 연산의 역전파 미분값도 $x$를 exp 연산한 값이다. 즉, exp 연산의 역전파 값은 exp 연산을 순전파했을 때 계산결과 동일하다.
현재 2가지 갈래로 들어오고 있는 국소적인 미분값 2개는 각각 $\frac{1}{S}$ 와 $-\frac{t_1}{exp(a_1)}$이다. 따라서 이 2개를 더해주면 $\left( \frac{1}{S} -\frac{t_1}{exp(a_1)} \right)$가 된다. 그리고 이 더한 값에 exp 연산 미분값을 곱해주면 아래와 같이 수식이 전개된다.
$$\left( \frac{1}{S} -\frac{t_1}{exp(a_1)} \right) \cdot exp(a_1) = \frac{exp(a_1)}{S} - t_1$$
그런데 우리는 $y_1 = \frac{exp(a_1)}{S}$라는 것을 알고있다. 따라서 $y_1$으로 치환하게 되면 결국 아래와 같이 최종적인 미분값이 계산된다.
$$\begin{matrix} \left( \frac{1}{S} - \frac{t_1}{exp(a_1)} \right) \cdot exp(a_1) &=& \frac{exp(a_1)}{S} -t_1 \\ &=& y_1 - t_1 \end{matrix}$$
exp 연산 역전파를 끝으로 드디어 CEE 계층에서부터 Softmax 레이어까지의 모든 역전파가 끝이났다. 결국 Softmax-with-Loss 계층의 최종적인 미분값은 $y_i - t_i$라는 것이다. 이는 이 목차 처음에 결론부터 보자고 했던 그림 속의 미분값과 동일하다는 것을 알 수 있다. Softmax-with-Loss 계층의 전체적인 역전파 계산 그래프는 아래와 같다.
Softmax-with-Loss 계층도 최종적인 미분값은 $y$라는 예측값과 $t$라는 정답만을 갖고 계산할 수 있다. 그런데 이렇게 간단하게 최종 미분값이 계산될 수 있는 이유는 손실함수로 크로스-엔트로피(CEE) 함수를 사용하기 때문이라고 한다. CEE 함수가 그렇게 설계되었기 때문이라고 한다. 매우 놀랍다.. 다른 예시로는 회귀 문제에서 사용하는 항등 함수의 손실함수로 오차제곱합을 이용하는 것이 있다고 한다. 항등함수에서 오차제곱합을 손실함수로 사용하게 되면 이 때도 역전파의 최종 미분 결과값도 $y_i - t_i$로 말끔히 계산된다고 한다.
이제 Softmax-with-Loss 계층을 넘파이로 구현하면 아래와 같이 구할 수 있다. 아래 소스코드에서 softmax 메소드, cross_entropy_error 함수는 저번 포스팅에서 소개한 함수 또는 저자의 코드에 있는 함수를 이용했다.
# Sofmtax-with-Loss 계층 구현
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None # prediction
self.t = None # label
def softmax(x):
max_x = np.max(x)
exp_x = np.exp(x - max_x)
exp_x_sum = np.sum(exp_x)
return exp_x / exp_x_sum
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
# 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
if t.size == y.size:
t = t.argmax(axis=1)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
def forward(self, x, t):
self.t = t
self.y = self.softmax(x)
self.loss = self.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
'Data Science > 밑바닥부터시작하는딥러닝(1)' 카테고리의 다른 글
[밑시딥] 오직! Numpy로 학습관련 기술들 구현하기 (0) | 2021.11.16 |
---|---|
[밑시딥] 오직! Numpy로 오차역전파를 사용한 신경망 학습 구현하기 (0) | 2021.11.14 |
[밑시딥] 오직! Numpy와 계산 그래프를 활용해 오차역전파 이해하기 (0) | 2021.11.10 |
[밑시딥] 오직! Numpy와 수치 미분을 통해 신경망 학습시키기 (0) | 2021.11.06 |
[밑시딥] 오직! Numpy로 간단한 신경망 구현하기 (0) | 2021.11.04 |