KoreanFoodie's Study
딥러닝 튜토리얼 5강 1부, 오차역전파법, 계산 그래프 - 밑바닥부터 시작하는 딥러닝 본문
딥러닝 튜토리얼 5강 1부, 오차역전파법, 계산 그래프 - 밑바닥부터 시작하는 딥러닝
GoldGiver 2019. 11. 22. 11:48
해당 포스팅은 한빛 미디어에서 출판한 '밑바닥부터 시작하는 딥러닝'이라는 교재의 내용을 따라가며 딥러닝 튜토리얼을 진행하고 있습니다. 관련 자료는 여기에서 찾거나 다운로드 받으실 수 있습니다.
오차역전파법
이전까지는 신경망의 가중치 매개변수의 기울기를 수치 미분을 사용해 구했다. 수치 미분은 단순하고 구현도 쉽지만 계산 시간이 오래 걸린다는 단점이 있다. 이번 강에서는 가중치 매개변수의 기울기를 효율적으로 계산하는 '오차역전파법(backpropagation)'을 배워보자.
오차역전파법을 제대로 이해하는 방법은 크게 두 가지가 있다. 하나는 수식을 통한 것이고, 다른 하나는 계산 그래프를 통한 것이다. 해당 포스트에서는 계산 그래프를 이용해 시각적으로 원리를 이해해 보자. 더 자세한 내용을 알고 싶다면, 스탠퍼드 대학교의 딥러닝 수업 CS231n을 참고해 보자!
계산 그래프
계산 그래프(computational graph)는 계산 과정을 그래프로 나타낸 것이다. 여기에서의 그래프는 자료구조에서 배우는 그래프로, 복수의 노드(node)와 에지(edge)를 가진다.
-
계산 그래프로 문제 풀기
간단한 문제를 계산 그래프로 풀어보자.
문제 1: 현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다. 이때 지불 금액을 구하라. 단, 소비세가 10% 부과된다.
계산 그래프는 노드를 원(O)으로 표기하고 원 안에 연산 내용을 적는다. 또, 계산 결과를 화살표 위에 적어 각 노드의 계산 결과가 왼쪽에서 오른쪽으로 전해지게 한다. 위 문제를 계싼 그래프로 풀면 다음과 같다.
위의 그림에서는 'x2'와 'x1.1'을 각각 하나의 연산으로 취급해 원 안에 표기했지만, 곱셈인 'x'만을 연산으로 생각할 수도 있다. 그 경우, 계산 그래프는 다음과 같이 표현할 수 있다.
다음 문제를 풀어보자!
문제 2 : 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다. 사과는 1개에 100원, 귤은 1개에 150원이다. 소비세가 10%일 때 지불 금액을 구하라
위 문제를 계산 그래프로 풀면, 다음과 같이 그래프를 그릴 수 있다.
지금까지 살펴본 것처럼, 계산 그래프를 이용한 문제풀이는 다음 흐름으로 진행된다.
1. 계산 그래프를 구성한다.
2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.
여기서 2번째 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파(forward propagation)라고 한다. 순전파는 계산 그래프의 출발점으로부터 종착점으로의 전파다. 반면, 오른쪽에서 왼쪽으로의 전파를 역전파(backward propagation)이라고 한다.
-
국소적 계산
계산 그래프의 특징은 '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있다. 국소적 계산은 결국 전체엣 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다. 예를 들어, 슈퍼에서 사과 2개를 포함한 여러 식품을 구입한다고 해 보자. 해당 계산 그래프는 다음과 같이 표현할 수 있다.
여기에서 핵심은 각 노드에서의 계산이 국소적 계산이라는 것이다. 가령 사과와 그 외의 물품 값을 더하는 계산은 4000이라는 숫자가 어떻게 계산되었느냐와는 상관없이, 단지 두 숫자를 더하면 된다는 것이다. 각 노드는 자신과 관련한 계산(이 예에서는 입력된 두 숫자의 덧셈) 외에는 아무것도 신경 쓸 게 없다!
이처럼 계산 그래프는 국소적 계산에 집중한다. 전체 계산이 제아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 '국소적 계산'이다. 국소적인 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 해낼 수 있다.
-
왜 계산 그래프로 푸는가?
계산 그래프의 이점이 뭘까? 하나는 방금 설명한 '국소적 계산'이다. 이는 전체가 복잡해도 각 노드에서 단순한 계산에 집중할 수 있게 해 문제를 단순화시킬 수 있다. 다른 이점으로, 계산 그래프는 중간 계산 결과를 모두 보관할 수 있다.
연쇄법칙
위의 계산 그래프에서, 각 그래프의 엣지(화살표)는 '국소적 미분'을 기억하고 있다. 이때, 이 국소적 미분을 전달하는 원리는 연쇄법칙(Chain rule)에 따른 것이다.
예를 들어, 다음과 같은 합성 함수가 있다고 가정해 보자. [ z = t^2, t = x + y ]. 합성 함수란 여러 함수로 구성된 함수이다.
연쇄법칙은 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의된다.
함성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.
매우 간단한 성질이다! 예를 들어, 위의 식은
이렇게 표현할 수 있고, dt의 소거도 가능하다! 국소적 미분은 다음과 같다.
그러므로, dz/dx는 위의 두 미분을 곱해 계산할 수 있다.
-
연쇄법칙과 계산 그래프
위의 식을 계산 그래프로 나타내 보자 2제곱 계산을 '**2' 노드로 나타내면 다음과 같이 그릴 수 있다.
위와 같이 계산 그래프의 역전파는 오른쪽에서 왼쪽으로 싢를 전파한다. 맨 왼쪽을 주목하자, 이때 dz와 dt는 전부 소거되어, 결국 남는 건 dz/dx가 된다. 이는 'x에 대한 z의 미분'을 의미한다!
위의 그래프에 미분 값들을 대입하면 dz/dx가 2(x+y)임을 알 수 있다.
역전파
앞 절에서는 계산 그래프의 역전파가 연괘법칙에 따라 진행되는 모습을 보았다. 이번에는 '+'와 'x'등의 연산을 예로 들어 역전파의 구조를 살펴보자.
-
덧셈 노드의 역전파
z = x + y 라는 식이 있다고 해 보자. 이때 미분은 다음과 같으 계산할 수 있다.
dz/dx = 1, dz/dy = 1
위 내용은 계산 그래프로 다음과 같이 표현 가능하다.
위와 같이 역전파 때는 상류에서 전해진 미분에 1을 곱하여 하류로 흘린다. 즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 보내게 된다.
dL/dz라는 표현을 쓴 이유는, 위 그림과 같이 최종적으로 L이라는 값을 출력하는 큰 계산 그래프를 가정했기 때문이다!
이제 구체적인 예를 하나 살펴보자. 가령 '10 + 5 = 15'라는 계산이 있고, 상류에서 1.3이라는 값이 흘러온다. 이를 계산 그래프로 그리면 다음 그림이 된다.
덧셈 노드 역전파는 입력 신호를 다음 노드로 출력할 뿐이므로 위 그림처럼 1.3을 그대로 다음 노드로 전달한다.
-
곱셈 노드의 역전파
이어서 곱셈 노드의 역전파를 살펴보자. z = xy라는 식이 있다고 가정하겠다. 이때, 계산 그래프는 다음과 같이 그려질 수 있다.
곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다. 서로 바꾼 값이란 위 그림처럼 순전파 때 x였다면 역전파에서는 y, 순전파 때 y였다면 역전파에서는 x로 바꾼다는 의미이다.
이제 구체적인 예를 하나 보자. Ex) '10 x 5 = 50'
덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값이 필요하지 않았지만, 곱셈의 역전파는 순방향 입력 신호의 값이 필요하다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해 둔다.
-
사과 쇼핑의 예
단순한 계층 구현하기
이제 위에서 예를 들었던 '사과 쇼핑'의 예를 파이썬으로 구현해 보자. 계산 그래프의 곱셈 노드를 'MulLayer', 덧셈 노드를 'AddLayer'라는 이름으로 구현했다.
신경망을 구성하는 '계층' 각각을 하나의 클래스로 구현하겠다. 여기에서 말하는 '계층'이란 신경망의 기능 단위이다!
-
곱셈 계층
모든 계층이 forward()와 backward()라는 공통의 메서드(인터페이스)를 갖도록 구현해보자. forward()는 순전파, backward()는 역전파를 처리한다.
먼저 곱셈 계층을 구현해 보자. (소스 코드는 ch05/layer_naive.py에 있다!)
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
def backward(self, dout):
dx = dout * self.y
dy = dout * self.x
return dx, dy
__init__()에서는 인스턴스 변수인 x와 y를 초기화한다. 이 두 변수는 순전파 시의 입력 값을 유지하기 위해서 사용한다. forward()에서는 x와 y를 인수로 받고 두 값을 곱해서 반환한다. 반면 forward()에서는 상류에서 넘어온 미분(dout)에 순전파 때의 값을 '서로 바꿔' 곱한 후 하류로 흘린다!
이제 실제 변수를 넣어 위의 그림을 구현해 보자! (소스 코드는 ch05/buy_apple.py에 있다)
apple = 100
apple_num = 2
tax = 1.1
#계층들
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
#순전파
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price) # 220
또, 각 변수에 대한 미분은 backward()에서 구할 수 있다.
#역전파
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax) # 2.2 110 200
backward() 호출 순서는 forward() 때와는 반대이다. 또, backward()가 받는 인수는 '순전파의 출력에 대한 미분'이다!
-
덧셈 계층
이어서 덧셈 계층을 구현해 보자.
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
덧셈 계층에서는 초기화가 필요 없으니 __init__()에서는 아무 일도 하지 않는다(pass가 '아무것도 하지 말라'는 명령이다). 덧셈 계층의 forward()에서는 입력받은 두 인수 x, y를 더해서 반환한다. backward()에서는 상류에서 내려온 미분(dout)을 그대로 하류로 흘린다!
이어서 덧셈 계층과 곱셈 계층을 사용하여 사과 2개와 귤 3개를 사는 상황을 구현해 보자!
# coding: utf-8
from layer_naive import *
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num) # (1)
orange_price = mul_orange_layer.forward(orange, orange_num) # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price) # (3)
price = mul_tax_layer.forward(all_price, tax) # (4)
# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) # (1)
print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
소스 코드는 ch05/buy_apple_orange.py)에서 확인할 수 있다. 다음 포스팅에서는 본격적으로 신경망에서 사용하는 계층을 구현해 보자!
'Deep Learning > 밑바닥부터 시작하는 딥러닝 1' 카테고리의 다른 글
딥러닝 튜토리얼 6-1강, SGD, 모멘텀, AdaGrad, Adam, 가중치 초기값 설정 - 밑바닥부터 시작하는 딥러닝 (4) | 2020.01.28 |
---|---|
딥러닝 튜토리얼 5강 2부, 활성화 함수 계층 구현, Affine/Softmax 계층 구현, 오차역전파법 구현 - 밑바닥부터 시작하는 딥러닝 (0) | 2019.12.26 |
딥러닝 튜토리얼 4강 2부, 수치 미분과 학습 알고리즘 - 밑바닥부터 시작하는 딥러닝 (0) | 2019.11.07 |
딥러닝 튜토리얼 4강 1부, 신경망 학습 - 밑바닥부터 시작하는 딥러닝 (7) | 2019.11.07 |
딥러닝 튜토리얼 3강 2부, 신경망 설계, 소프트 맥스 - 밑바닥부터 시작하는 딥러닝 (0) | 2019.11.05 |