프로필사진

IT Anthology/encyclopedia

[밑러닝] 파이썬으로 다양한 계층(활성화함수, softmax)에서의 역전파 구현하기

다각 2020. 4. 8. 19:02

* 이 글은 <밑바닥부터 시작하는 딥러닝2 (저자: 사이토 고키)> 책을 읽으며 정리한 글입니다.
* 나중에라도 제가 참고하기 위해 정리해 두었으며, 모든 내용을 적은 것이 아닌,
필요하다고 생각되는 부분만 추려서 정리한 것임을 미리 밝힙니다.


목차

  1. ReLU에서의 역전파 구현
  2. Sigmoid에서의 역전파 구현
  3. Softmax-with-Loss에서의 역전파 구현

앞서 간단한 덧셈, 곱셈 노드에서의 역전파를 구현해보았다.

2020/03/29 - 파이썬으로 간단한 계층(덧셈 노드/곱셈 노드)에서의 역전파 구현하기

이제 신경망에서 자주 쓰이는 활성화함수에서의 역전파를 구현해보도록 하자.


1. ReLU에서의 역전파 구현

앞선 포스트에서 ReLU 함수에 대해 다룬 바가 있다. 리마인드를 위해 잠깐 언급하자면 다음과 같다.
ReLU(Rectified Linear Unit) 함수는 입력이 0을 넘지 않으면 다 신호를 죽이고, 0을 넘으면 입력 그대로를 출력하는 함수이다. 식은 다음과 같다.
$$
y=\begin{cases}
x\ (x > 0)\\
0\ (x \le 0)
\end{cases}
$$
위의 식을 미분하면 다음과 같다.
$$
\frac {\partial y}{\partial x} = \begin{cases}
1\ (x >0)\\
0\ (x \le 0)
\end{cases}
$$
즉, 입력값이 0보다 클 때는 뒤에서 오는 역전파를 '그대로' 흘려보낸다. 반면 입력값이 0보다 작거나 같을 때는 역전파를 흘려보내지 않는다. 이 정보를 바탕으로 파이썬으로 구현해보자.

class Relu:

    def __init__(self):
        self.mask = None    # 여기서 mask는 입력이 0 이하인 부분에서 True값을 반환하는 array로 쓰일 것이다.

    def forward(self, x):
        self.mask = (x<=0)
        out = x.copy()
        out[self.mask] = 0
        return out

    def backward(self, d_out):
        d_out[self.mask] = 0
        dx = d_out

렐루(ReLU) 클래스는 처음에 mask라는 변수를 생성하고, 입력들의 배열에서 0이하인 수를 모두 '0'으로 바꿔주는 역할을 수행한다.

2. Sigmoid에서의 역전파 구현

시그모이드 함수 역시 이전 포스트에서 구현한 바가 있다. 시그모이드는 모든 값을 0과 1 사이에 넣어주기 때문에, 확률 값처럼 나타내주는 효과가 있다. 시그모이드의 식은 아래와 같다.
$$
y = \frac {1}{1+\exp (-x)}
$$

위에서 살펴 본 ReLU 함수와는 다르게, y값을 구해내기 위해 많은 작업이 들어간다. x와 가장 가깝게 있는 계산식부터 살펴보며, 순전파 계산 그래프를 그려보자.

위의 그래프를 바탕으로 역전파를 그래프를 생각해보자. 제일 먼저 나눗셈 노드에서 $y = \frac {1}{x}$을 미분하면 다음과 같이 된다.
$$
\begin{aligned}
\frac {\partial y}{\partial x}&= - \frac {1}{x^2}
&=-y^2
\end{aligned}
$$
(입력값을 기억하는 것보다 뒤에서 흘러들어오는 값($y$)을 이용하는 것이 훨씬 경제적이므로 $y$로 나타낼 수 있는 식이면 바꿔서 나타내준다.)
또한 덧셈 노드에서는 뒤(상류)에서 흘러들어오는 값을 그대로 통과시키고, exp함수는 미분하면 자기 자신이 나오므로, $y = \exp (-x)$을 미분하면 다음과 같다.
$$
\frac {\partial y}{\partial x}= \exp (-x)
$$
또한 덧셈 노드에서는 앞선 입력값을 서로 바꾼 값이 들어가게 된다. 이를 종합하여 그래프 상에 나타내면 다음과 같다.

위의 모든 과정을 단순화시키면 다음과 같이 나타낼 수 있다.

한편, sigmoid의 역전파 결과인 $\frac {\partial L}{\partial y} y^2 \exp (-x)$의 변수를 줄여서 저장해야 하는 변수의 수를 줄여보자. 다음과 같이 $y$에 대한 식으로 정리할 수 있다.
$$
\begin{aligned}
\frac {\partial L}{\partial y} y^2 \exp (-x) & = \frac {\partial L}{\partial y} \frac{1}{(1+ \exp (-x))^2} \exp (-x) \\[2mm]
& = \frac {\partial L}{\partial y} \frac{1}{(1+ \exp (-x))} \frac {\exp (-x)}{(1+ \exp (-x))}\\[2mm]
& = \frac {\partial L}{\partial y} y(1-y)
\end{aligned}
위의 식을 반영해 sigmoid에서의 역전파를 다시 나타내면 다음과 같다.

위의 정보를 바탕으로 파이썬으로 sigmoid에서의 역전파를 구현해보도록 하자.

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

    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        self.out = out
        return out

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

위의 코드에서는 순전파의 출력 결과를 저장하기 위해 out 변수를 생성했다.

3. Softmax-with-Loss에서의 역전파 구현

조금 더 복잡한 예시인, softmax에서의 역전파를 생각해보자. 소프트맥스 함수는 교차 엔트로피 오차 층이 함께 있을 때 굉장히 단순한 역전파 결과는 내놓는다.

(Soft-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)

    def backward(self, d_out=1):
        batch_size = self.t.shape[0]
        dx = (self.y = self.t) / batch_size
        return dx

 


 

정리

지금까지 다양한 계층에서의 역전파에 대해서 알아보았다. 이러한 방식으로 오차역전파를 적용한 신경망을 구성할 수 있고, 오차역전파법을 이용하여 비교적 빠르게 기울기를 구할 수 있다.   

그렇다면 우리는 쓸모없는 수치 미분을 힘들여 배워버린 것일까...? 그렇지 않다!  

수치 미분과 비교해서 오차역전파법은 더 빠르지만, 구현하기 복잡하기 때문에 버그(라고 쓰고 실수라고 읽는다)가 발생할 수 있다. 따라서 구현하기 쉬운 수치 미분으로 검증을 해볼 수 있다.