본문 바로가기

Data Science/밑바닥부터시작하는딥러닝(1)

[밑시딥] 오직! Numpy와 계산 그래프를 활용해 오차역전파 이해하기

반응형

🔊 해당 포스팅은 밑바닥부터 시작하는 딥러닝 1권의 교재 내용을 기반으로 딥러닝 신경망을 Tensorflow, Pytorch와 같은 딥러닝 프레임워크를 사용하지 않고 순수한 Numpy로 구현하면서 딥러닝의 기초를 탄탄히 하고자 하는 목적 하에 게시되는 포스팅입니다. 내용은 주로 필자가 중요하다고 생각되는 내용 위주로 작성되었음을 알려드립니다.

 

밑바닥부터 시작하는 딥러닝


저번 포스팅까지 파라미터의 기울기를 구하는 방법으로서 수치 미분이 무엇인지 알아보고 넘파이로 구현하는 방법에 대해서도 알아보았다. 그리고 그를 기반으로 신경망을 학습시키는 것도 구현해보았다. 

 

그런데 수치미분을 활용해서 파라미터의 기울기를 구하는 방법은 단순한 계산이지만 시간이 오래걸린다는 단점이 존재했다. 왜냐하면 저번 포스팅의 소스코드를 살펴보면 알겠지만, 하나의 가중치에 대한 수치미분을 계산하기 위해서 모든 데이터를 매번 계산해야 했기 때문이었다.

 

이번 포스팅에서는 이해하는 과정에서 계산이 보다 복잡하지만 결과적으로는 기울기 계산을 효율적으로 수행해 속도가 빠른 오차역전파 방법에 대해 알아보자. 그리고 이를 넘파이로도 구현해보도록 하자.

1. 계산 그래프를 활용하자!

오차역전파법을 넘파이로 구현하기 전에 개념부터 정립할 필요가 있다. 오차역전파법을 이해하기 위한 방법으로는 크게 2가지가 존재하는데, 이 책에서는 계산 그래프(Computational Graph)를 활용해서 오차역전파법을 이해한다. 먼저 계산 그래프라는 것은 노드와 간선(edge)로 계산을 표현할 수 있는 그래프를 의미한다. 

 

계산 그래프 개념에 대해 이해해보자. 한 박스에 100원 하는 사과박스가 있는데, 이 사과박스를 2개 구입하고 소비세로 10%를 추가로 내야 한다고 하자. 그렇다면 지불해야 하는 총 금액은 얼마일까? 이에 대해 계산 그래프로 나타내면 아래와 같다.

 

계산 그래프

 

위 계산 그래프에서 동그라미 모형의 노드는 하나의 '연산'을 의미하고 화살표인 간선 위에 있는 숫자값은 계산 결과를 의미하다. 이 계산 결과는 다음 노드로 전달된다. 그렇다고 할 때 위 그래프를 다시 살펴보자. 우선 사과박스가 1개 당 100원이라고 했다. 그래서 첫 번째 간선 위의 숫자인 계산 결과값은 100이 된다. 그리고 우리는 사과박스를 2개 구입한다고 했다. 그러므로 동그라미 연산 노드에서 곱하기 2라는 연산을 수행해준다. 100 x 2를 수행한 계산 결과인 200을 간선에 담아서 다음 노드로 전달해준다. 이렇게 함으로써 우리가 내야할 총 지불 금액은 마지막 간선 위에 있는 220원이다.

 

그런데 위 계산 그래프 도형에서 살짝 변환을 해주어야 한다. 왜냐하면 연산을 가리키는 동그라미 노드안에는 정확히는 '연산'만 담겨야 하기 때문이다. 따라서 위 그래프에서 아래처럼 계산 그래프를 변환해줄 수 있다.

 

변환한 계산 그래프

 

달라진 점은 '사과 개수' 와 '소비세'라는 변수들을 밖으로 빼내었다는 것이다. 첫 번째 동그라미 연산 노드를 보면 입력 값으로 사과박스 1개당 값인 100원과 사과 개수인 2(개)를 입력받아서 곱하기 연산을 취하는 것을 볼 수 있다. 이런식으로 다음 동그라미 연산 노드도 동일하게 적용해준다. 

 

이렇게 위와 같이 왼쪽에서 오른쪽으로 진행하는 단계를 순전파(Forward Propagation)이라고 한다. 역전파는 이 반대방향인 오른쪽에서 왼쪽으로 진행하는 단계를 의미한다. 이 역전파는 추후에 소개할 미분을 계산할 때 중요한 역할을 하게 된다.

2. 국소적 계산

국소적이라는 단어의 사전적 의미는 '자신과 직접 관계된 작은 범위'를 의미한다. 그래서 국소적 계산은 자신과 직접 관계된 작은 범위 내에서 이루어지는 계산을 의미한다. 위의 계산 그래프는 각 노드에서 국소적 계산이 이루어진다는 점이다. 여기서 국소적 계산을 좀 더 와닿게 이해한다고 하면, 각각 자신의 노드에서 이루어지는 연산은 자기 노드 이전 노드까지 어떤 복잡한 계산 과정을 하던 말던 자신의 노드에 있는 연산 계산만 신경쓰면 된다는 것이다. 예를 들어, 다음과 같은 그림처럼 말이다.

 

국소적 계산은 국소적인 부분만 신경쓴다!

 

이러한 국소적 계산은 복잡한 계산을 단순화하는 장점이 있다. 또한 중간 계산 결과를 캐싱 기능처럼 모두 보관할 수 있다.(이러한 점은 추후에 미분 계산 시 효율적으로 만들어주는 데 기여를 한다)

3. 역전파 시 제공하는 '미분'

이제 순전파의 반대 방향인 역전파를 수행할 때,  어떤 값을 다음 노드로 전달할까? 바로 순전파를 수행했을 때의 입력으로 들어온 값에 대해 입력 이후 계산 결과값이 얼마나 변화하는지에 대한 값, 바로 '미분' 값을 전달한다. 위에서 예시로 들었던 사과박스 2개를 구매했을 때, 경우를 다시 살펴보자. 아래 그림의 빨간 화살표'사과박스 가격'에 대한 '내야할 돈'의 미분값을 의미한다.

 

사과박스에 대한 내야할 돈의 미분값

 

사과박스 바로 옆에 있는 빨간색 화살표에 2.2라는 값이 있다. 이것이 결국 사과박스 가격에 대한 내야할 돈의 미분값인데, 좀 더 구체적으로 해석하면 "사과박스 가격이 아주 조금 변화했을 때, 내야할 돈이 기존보다 얼마나 변화했는가?" 를 의미한다. 즉, 변화량을 의미한다!

 

위 그림에서 사과박스 가격에 대한 내야할 돈의 미분 값은 동그라미인 연산 노드를 거치면서 업데이트 된다. 역전파는 중간중간에 존재하는 모든 연산 과정에 대해 미분(이를 국소적 미분이라고 함)을 수행해서 최종적으로 '사과 박스 가격'에 대한 내야할 돈의 최종 미분 값을 결정해준다. 그렇다면 위와 같이 연속적으로 연산 노드 마다 미분 값을 어떻게 구해줄까?

3. 연쇄법칙(Chain Rule)

연쇄법칙을 통해서 역전파 시 국소적인 미분을 전달할 수 있다. 역전파를 수행해줄 때는 이전 노드에서 흘러들어오는 값에 해당 노드의 국소적인 미분값을 곱한 후 다음 노드로 전달해준다. 이를 도식화하면 아래와 같다.

 

국소적 미분

 

검은색 화살표가 순전파, 빨간색 화살표가 역전파 방향이다. 역전파 방향을 보면, 이전 노드에서 흘러나온 계산 결과값인 $E$가 있다. 그리고 $f(x)$에 대한 국소적 미분 값을 구하기 위해서 $x$에 대한 $y$값의 미분값을 구해주고 이를 $E$와 곱해서 다음 노드로 넘겨준다.

 

이제 역전파 시 국소적 미분을 계산하는 방법에 대해 알아보았으니 이 국소적 미분을 활용하는 연쇄법칙에 대해 알아보자. 연쇄법칙은 합성함수의 미분에 대한 성질이다. 합성함수는 고등학교 때에도 많이 들어보았을 텐데, 대표적으로 아래와 같은 유형들의 함수들이다.

$$\begin{cases} z = t^2 \\ t = x + y\end{cases}$$

 

위와 같은 함수를 합성함수라고 한다. 연쇄법칙은 이러한 합성함수의 미분을 의미한다. 합성함수의 미분을 구하기 위해서는 합성함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다. 위 수식의 합성함수가 존재할 때, $x$에 대한 $z$의 미분값인 ${\partial z \over\partial x}$를 구하기 위해서는 아래와 같이 펼칠 수 있다.

$${\partial z \over \partial x} = {\partial z \over \partial t}{\partial t \over \partial x}$$

위 수식은 바로 편미분임을 알 수 있다. 따라서, 위와 같이 분해한 편미분을 각각 수행해줌으로써 ${\partial z \over\partial x}$ 값은 아래와 같아진다.

$${\partial z \over \partial x} = {\partial z \over \partial t}{\partial t \over \partial x} = 2t \cdot 1 = 2(x+y)$$

이러한 연쇄법칙을 계산 그래프로 표현하면 아래와 같아진다.

 

연쇄법칙을 계산 그래프로 나타내기

4. 덧셈과 곱셈 노드의 역전파

방금까지 살펴본 연쇄법칙은 노드 안에서 수행되는 연산 종류에 따라 국소적인 미분이 전달되는 방식에 약간 차이가 있다. 먼저 덧셈노드부터 살펴보자. $z = x + y$라는 식이 있다고 가정할 때,  $x$, $y$에 대한 $z$의 미분값은 아래와 같이 될 것이다.

$$\begin{cases} {\partial z \over \partial x} = 1 \\  {\partial z \over \partial y} = 1 \end{cases}$$

 

둘 다 미분 값이 모두 1이기 때문에 다음 노드로 전달하는 국소적인 미분 값은 이전 노드에서 흘러들어온 이전 노드의 국소적인 미분값을 그냥 전달하기만 하면 된다. 

 

덧셈 노드일 때 역전파

 

그렇다면 덧셈 노드일 때의 구체적인 예시를 살펴보자. 아래 그림은 $10 + 5 = 15$라는 연산이 있을 때, 역전파 시 이전 노드로부터 1.3이라는 국소적인 미분값이 흘러들어오고 있다고 가정했을 때이다.

덧셈 노드일 때 역전파 예시

 

위 그림을 보면 알겠지만 이전 노드에서 흘러들어온 국소적인 미분 값인 1.3 그대로 10과 5가 있는 방향으로 각각 전달하는 것을 알 수 있다.

 

다음은 곱셈 노드일 때의 역전파이다. 이번엔 $z = xy$라는 계산이 있을 때, $x$, $y$에 대한 미분값은 아래와 같아진다.

$$\begin{cases} {\partial z \over \partial x} = y \\  {\partial z \over \partial y} = x \end{cases}$$

 

아래 그림은 곱셈 노드일 때의 역전파 계산 그래프이다.

 

곱셈 노드일 때의 역전파

 

빨간색 화살표를 잘 보면 순전파 시 $x$가 들어온 방향으로의 역전파일 때는 $y$가 국소적인 미분에 곱해지고, 순전파 시 $y$가 들어온 방향으로의 역전파일 때는 $x$가 국소적인 미분에 곱해지는 것을 알 수 있다. 결국 곱셈 노드일 때는 순전파일 때 들어온 입력 신호를 역전파 방향일 때는 서로 바꾸어주어 국소적인 미분에 곱해주어 넘겨주면 된다.

5. 단순한 계층 구현하기

지금까지 알아본 개념들과 넘파이를 활용해 곱셈 계층과 덧셈 계층을 구현해보자.(여기서 '계층'은 신경망의 기능 단위를 의미한다. 하나의 레이어라고 보면 된다) 먼저 곱셈 계층이다. 계산 그래프에서 각 간선과 연산 노드들이 소스코드에서 어떤 변수를 설명하는지 도식화도 해놓았으니 그림과 소스코드를 보면서 이해하자.

 

계산 그래프와 소스코드 변수명 매핑

 

import numpy as np

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None
        
    def forward(self, x, y):
        self.x = x
        self.y = y
        return x * y
    
    def backward(self, d_out):
        # 순전파 시 입력을 서로 바꾸어서 곱해줌!
        dx = d_out * self.y
        dy = d_out * self.x
        return dx, dy
    
apple_box = 100
apple_box_num = 2
tax = 1.1

# 곱셈계층
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

# 순전파 수행
apple_box_price = mul_apple_layer.forward(apple_box, apple_box_num)
price = mul_tax_layer.forward(apple_box_price, tax)
print('순전파 수행 후 지불해야 할 최종 금액:', price)
print()

# 역전파 수행(순전파와 반대 순서로 호출)
d_price = 1
d_apple_box_price, d_tax = mul_tax_layer.backward(d_price)
d_apple_box, d_apple_box_num = mul_apple_layer.backward(d_apple_box_price)
print('역전파 수행 후 각 변수의 변화량 값')
print('사과 박스 가격:', d_apple_box)
print('사과 박스 개수:', d_apple_box_num)
print('소비자세:', d_tax)

 

다음은 덧셈계층이다. 국소적인 미분값을 그대로 넘겨주는 것을 볼수 있다. 아래의 덧셈 계층 소스코드를 구현한 계산식은 $z = x + y$로 가정했다.

 

class AddLayer:
    def __init__(self):
        pass
    
    def forward(self, x, y):
        return x + y
    
    def backward(self, d_out):
        dx = d_out * 1
        dy = d_out * 1
        return dx, dy

 

이제 곱셈, 덧셈 연산을 함께 결합한 계산 그래프에 대한 순전파, 역전파를 위 소스코드들을 기반으로 구현할 수 있다. 덧셈, 곱셈을 결합한 계산 그래프를 구현하는 구체적인 소스코드는 여기를 통해 확인하자.


다음 포스팅에서는 활성화 함수가 적용된 오차역전파법을 공부해보고 신경망 전체의 오차역전파를 넘파이로 구현하는 방법에 대해서도 알아보기로 하자.

반응형