지난 글을 통해 계산 그래프의 역전파가 연쇄 법칙에 따라서 진행되는 모습을 이야기해 보았습니다.
이번에는 덧셈 노드와 곱셈 노드의 역전파의 구조에 대하여 알아보도록 하겠습니다!
단순한 계층부터 구현하기
수치적인 오차역전파법, 계산 그래프를 이용한 역전파의 의미에 대해 살펴보았습니다. 본격적으로 파이썬으로 역전파를 구현하고, 이를 신경망에 집어넣어 실제 오차역전파를 구현해 보도록 하겠습니다. 일전에 수식을 이용해 오차역전파법을 공부했고, 계산 그래프를 이용해 쇼핑에 대한 역전파도 공부해 보았습니다. 신경망이라고 해서 별 다를 것이 없는 게, 신경망의 모든 계산은 덧셈과 곱셈을 이용해 수행됩니다. 따라서 아주 간단한 덧셈 계층과 곱셈 계층부터 구현해 보도록 하겠습니다. 모든 계층은 순전파와 역전파를 모두 처리할 수 있도록 공통의 메소드를 구현할 것입니다.
- forward : 순전파
- backward : 역전파
덧셈 노드의 역전파
먼저 𝑧=𝑥+𝑦라는 식을 대상으로 역전파를 살펴보도록 하겠습니다. 우선 𝑧=𝑥+𝑦의 미분은 다음과 같이 계산할 수 있을 것입니다.
𝑥, 𝑦에 대한 각각의 편미분 값이 모두 1이 되는 것을 확인할 수 있습니다. 계산 그래프로는 다음과 같이 표현이 가능합니다.
역전파 때는 상류에서 보내진 미분 값( 여기에서는 ∂𝐿∂𝑧 )에 1을 곱하여 하류로 흘려보냅니다. 즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐 입력된 값을 그대로 다음 노드로 보내게 됩니다. 위 예에서는 상류에서 전해진 미분 값을 ∂𝐿∂𝑧로 표현 하였는데 이는 최종적으로 𝐿이라는 값을 출력하는 큰 계산 그래프를 가정하였기 때문입니다.
즉, 국소적 미분이 가장 오른쪽의 출력에서 시작하여 노드를 타고 역방향(왼쪽)으로 전파된 것을 그렸다고 생각하면 됩니다. 덧셈 노드에 대한 역전파를 간단하게 살펴보겠습니다. 가령 10+5=15라는 계산이 있고, 상류에서 (오른쪽) 1.3이라는 값이 흘러나오면 다음과 같이 계산 그래프를 그려볼 수 있습니다.
다시 말해, 덧셈 노드 역전파는 입력 신호를 다음 노드로 출력할 뿐이므로 1.3을 그대로 다음 노드로 전파합니다. 바로 코드로 구현해 보도록 하겠습니다.
- 순전파 : 단순히 두 값을 더한다.
- 역전파 : 다음 노드로부터 들어온 미분값을 그대로 흘려보낸다.
- 순전파 시에 입력된 값을 저장하고 있을 필요는 없다.
import numpy as np
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
곱셈 노드의 역전파
이번엔 곱셈 노드의 역전파입니다.𝑧=𝑥𝑦 라는 식을 예로 들어 보겠습니다. 이 식의 미분은 다음과 같습니다.
계산 그래프로는 다음과 같이 그릴 수 있습니다.
곱셈 노드의 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보내고 있습니다. 즉, 순전파 때 𝑥였다면 역전파에서는 𝑦, 순전파 때 𝑦였으면 역전파에서는 𝑥로 바꾼다는 의미가 됩니다. 구체적인 예로 10⋅5=50이라는 계산이 있고, 역전파 때 상류에서 1.3 값이 흘러온다고 가정해 보겠습니다. 이를 계산 그래프로 그리면 다음과 같이 됩니다.
- 순전파 : 두 값을 곱한다
- 역전파 : 들어온 두 값에 미분값을 곱해서 반대로 전달한다.
- 곱할 값들을 저장하고 있어야 한다. 역전파시에 미분값을 곱한 다음 반대로 전달해야 하니까
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
곱셈의 역전파에서는 입력 신호를 바꾼 값을 곱하여야 하는1.3⋅5=6.5. 다른 하나는 1.3⋅10=13이 되는 것이 확인됩니다. 덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값은 필요하지 않았습니다만, 곱셈의 역전파에서는 순방향 입력신호의 값이 필요합니다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해 둡니다.
앞서 #10. 3에서 사과와 귤 계산 기억하시나요? 사과 쇼핑의 예를 다시 한번 이어가 보자면!
이 문제에서는 '사과의 가격', '사과의 개수', 소비세'라는 세 변수 각각이 최종 금액에 어떻게 영향을 주느냐를 풀고자 합니다. 이는
- 사과 가격에 대한 지불 금액의 미분
- 사과 개수에 대한 지불 금액의 미분
- 소비세에 대한 지불 금액의 미분을 구하는 것에 해당합니다. 이를 계산 그래프의 역전파를 사용해서 풀면 다음 그림과 같게 됩니다.
곱셈 노드의 역전파에서는 입력 신호를 서로 바꿔서 하류로 흘려보내는 것이 보입니다. 위 그림의 결과를 보면 사과 가격의 미분은 2.2, 사과 개수의 미분은 110, 소비세의 미분은 200입니다. 이를 해석해 보면
- 소비세와 사과 가격이 같은 양만큼 오른다면
- 최종 금액에는 소비세가 200의 크기로, 사과 가격이 2.2 크기로 영향을 준다고 할 수 있겠습니다.
- 단, 소비세 1은 100%, 사과 가격 1은 1원
정리할 겸 해서 '사과와 귤 쇼핑'의 역전파를 풀어보죠!
초기화 함수인 init에서는 단순히 인스턴스 변수 x, y를 초기화시킵니다. 이 두 변수는 순전파 시의 입력값을 유지하기 위해 사용됩니다. forward 메소드는 순전파로써, x, y 를 입력받아 그 값을 인스턴스 변수에 저장한 후 곱해서 반환 합니다. backward 메소드는 상류에서 넘어온 미분값(dout)을 입력받아 서로 바꿔 곱한 후 반환하여 하류로 흘려보냅니다. 사과 쇼핑의 예를 들어보면 다음처럼 그림으로 설명이 가능합니다. MulLayer를 사용하여 위 그림의 순전파를 다음과 같이 확인해 볼 수 있습니다.
# 사과만 테스트
apple = 100 # 사과 1개당 가격
apple_cnt = 2 # 사과 개수
tax = 1.1 # 소비세
# 계층은 2개가 필요함
# (apple * apple_cnt) * tax
# 레이어 준비
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# 순전파 먼저 수행
apple_price = mul_apple_layer.forward(apple, apple_cnt)
price = mul_tax_layer.forward(apple_price, tax)
print("최종 사과 가격 : {}".format(price))
각 변수에 대한 미분은 backward에서 구할 수 있습니다.
# 역전파 구현
dprice = 1 # d돈통 / d포스기 # 최종 가격에 대한 미분. d가격 / d가격 = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_cnt = mul_apple_layer.backward(dapple_price)
print("사과 가격 * 사과 개수에 대한 미분값 : {}".format(dapple_price))
print("사과 가격에 대한 미분값 : {}".format(dapple))
print("사과 개수에 대한 미분값 : {}".format(dapple_cnt))
print("소비세에 대한 미분값 : {}".format(dtax))
여기서 눈여겨보아야 할 것은 backward()의 호출 순서는 forward() 호출 순서의 반대라는 점입니다. 그리고 backward()가 받는 매개변수는 '순전파 출력에 대한 미분'임을 주의해야 합니다.
덧셈 계층 구현
두 번째로 구현할 계층은 덧셈 계층입니다.
# 사과와 귤 계산하기
apple = 100
apple_cnt = 2
orange = 150
orange_cnt = 3
tax = 1.1
1 계층
- 각 과일에 대한 개수 계산 ( 과일 가격 * 개수 )
mul_apple_layer = MulLayer() # 사과 개수 * 사과 가격
mul_orange_layer = MulLayer() # 귤 개수 * 귤 가격
2 계층
- 사과 총 가격 + 귤 총 가격
add_apple_orange_layer = AddLayer() # 사과 총 가격 + 오렌지 총 가격
3 계층
- 과일들의 총 가격 * 소비세
mul_tax_layer = MulLayer() # 과일 총 가격 * 소비세
연산 수행 (순전파)
# 1 계층
apple_price = mul_apple_layer.forward(apple, apple_cnt)
orange_price = mul_orange_layer.forward(orange, orange_cnt)
# 2 계층
total_price = add_apple_orange_layer.forward(apple_price, orange_price)
# 3 계층
price = mul_tax_layer.forward(total_price, tax)
print("최종 가격 : {}".format(int(price)))
연산 수행 (역전파)
dprice = 1 # d돈통 / d포스기
# dprice / dtotal_price, dprice / dtax
dtotal_price, dtax = mul_tax_layer.backward(dprice)
# d돈통 / dapple_price = ( d돈통 / d포스기 ) * (d돈통 / dtotal_price) * (dtotal_price / dapple_price)
dapple_price, dorange_price = add_apple_orange_layer.backward(dtotal_price)
dorange, dorange_cnt = mul_orange_layer.backward(dorange_price)
dapple, dapple_cnt = mul_apple_layer.backward(dapple_price)
print("사과 개수에 대한 미분값 : {}".format(dapple_cnt))
print("사과 가격에 대한 미분값 : {}".format(dapple))
print("귤 개수에 대한 미분값 : {}".format(dorange_cnt))
print("귤 가격에 대한 미분값 : {}".format(dorange))
지금까지 역전파의 덧셈 노드와 곱셈 노드를 알아보았습니다! 길고 긴 역전파의 마지막 종착지는 활성화 함수의 계층 구현으로 마무리하려 합니다. 다음 시간에는 신경망 레이어 만들기를 설명해 보도록 하겠습니다.
'Programming > Deep Learning' 카테고리의 다른 글
[Python/DeepLearning] #10.5. 역전파) 활성화 함수 계층 구현 (0) | 2024.03.01 |
---|---|
[Python/DeepLearning] #10.3. 역전파) 계산 그래프를 통한 역전파 이해 (1) | 2024.02.15 |
[Python/DeepLearning] #10.2. 역전파) 수식을 통한 오차역전파법 이해 (0) | 2024.02.08 |
[Python/DeepLearning] #10.1. 역전파) 합성함수의 미분과 연쇄법칙 (1) | 2024.02.06 |
[Python/DeepLearning] #9.3. MNIST 신경망 구현하기 (2) | 2024.02.02 |