오차 역전파( Error Backpropagation) 란 말그대로 오차(error)를 역방향으로 보내는것이다.
앞에서는 입력층-> 은닉층 -> 출력 순으로 가중치를 업데이트 했었다. 이를 순전파라고 한다.
오차 역전파 방법은 순전파에서 생기는 결과값의 오차를 다시 역순으로 보낸 후, 가중치를 재계산하여 오차를 줄여나간다.
더 정확히 얘기하면, 기존의 방식은 수치 미분을 통해 기울기를 구하지만, 계산 시간이 오래걸린다는 단점이 있다.
하지만 오차 역전파 방식은 손실함수의 기울기를 더 효율적으로 계산하기 위해 쓰인다고 볼 수 있다.
(가중치 매개변수에 대한 손실함수의 기울기)
오차 역전파 방식을 그림으로 이해해보자.!
5.1.1 계산 그래프
문제 1) 재환이는 슈퍼에서 개당 100원인 사과를 두 개 샀다. 이 때 지불금액을 구해라. 소비세는 10%
문제 2)재환이는 슈퍼에서 개당 100원인 사과를 두 개, 개당 150원인 귤을 세 샀다. 이 때 지불금액을 구해라.
소비세는 10%
문제 1과 2을 계산 그래프로 푼다면 다음 그림과 같다.
이렇게 계산 방향이 왼쪽 -> 오른쪽으로 전달되어 진행되는 과정을 순전파라고한다.
역전파는 물론 순전파의 반대 개념이다.!!
그 전에, 더 알아볼 개념이 있다.
5.1.2 국소적 계산
계산 그래프의 특징은 '국소적 계산'을 전파함으로 최종 결과를 얻는다는 점에 있다.
'국소적' 이란 '자신과 직접 관계 되있는 작은 범위'를 말한다.
국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.
구체적인 예시를 통해 알아보자.
가령 슈퍼에서 사과를 포함한 여러 식품을 구매한다고 했을 때는 아래 그림 같다.
그림에서, 여러 식품의 구매가격이 4,000원이 나왔다. 여기서 핵심은 각 노드의 계산이 국소적이라는 것이다.
즉, 무엇을 얼마나 샀는지 (계산이 얼마나 복잡한 지)와 상관없이 두 숫자만 더하면 된다는것( 4,000 + 200)
이처럼 계산 그래프는 국소적 계산에만 집중하면 된다.
5.1.3 왜 계산 그래프로 푸는가?
첫 번째 답은 국소적 계산의 이점 때문이다.
즉, 아무리 계산 과정, 방법이 복잡해도 노드 계산에만 집중해서 문제를 해결할 수 있다는 점이다.
두 번째 답은 역전파를 통해 '미분'을 효율적으로 계산할 수 있기 때문이다.
이제 역전파를 이해하기 쉽게 위 문제를 다시 보자.
문제 1은 사과 두 개 사서 최종 가격을 구하는 것이였다.
여기서 만약 사과 가격이 오르면 최종 금액이 얼마나 변할 지 알고 싶다고 해보자.
이는 사과 가격에 대한 지불금액의 미분값을 구하는 것과 같다.!!
기호로 나타낸다면 ∂L/∂x ( L = 최종금액, x = 사과 가격)
문제를 풀어본다면 다음 그림과 같다.
- 문제1을 예시로 한 설명
- 역전파는 순전파와 반대 방향의 굵은 화살표 그림
- 역전파는 국소적 미분 전달하고, 미분 값은 화살표의 아래에 표시 ( 위 그림에서는 빨간 글씨 )
- ★ 사과가 1원 오르면 최종 금액은 2.2원 오른다는 의미 ★
- 소비세에 대한 지불 금액의 미분이나 사과 개수에 대한 지불 금액의 미분도 같은 순서로 구할 수 있음
- 중간까지 구한 미분 결과가 공유가 가능하기 때문에 다수의 미분을 효율적 계산이 가능함
- 계산 그래프의 이점은 순전파와 역전파를 활용해 각 변수의 미분을 효율적으로 구할 수 있음
5.2 연쇄법칙
위의 역전파 방식은 국소적 미분을 전달하는 것이고, 전달하는 원리는 연쇄법칙을 따른 것이다.
5.2.1 계산 그래프의 역전파
위 그림과 같이 역전파의 계산 절차는 신호 E에 노드의 국소적 미분(∂y/∂x)를 곱한 후 다음 노드로 전달하는 것이다.
방금 말한 국소적 미분은 순전파 때의 y=f(x) 계산의 미분을 구한다는 뜻이며, 이는 x에 대한 y의 미분을 구한다는 것이다.
이러한 방식이 역전파의 계산 순서인데, 이 방식이 목표로 하는 미분값을 효율적으로 구할 수 있다는것이 역전파의 핵심임.
5.2.2 연쇄법칙이란?
연쇄 법칙은 합성 함수부터 시작해야 한다.
합성 함수란 여러 함수로 구성된 함수로, 연쇄법칙은 합성 함수의 미분에 대한 성질과 같다.
또한 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.
이게 연쇄법칙의 원리이다.
ex) z = (x + y)^2라는 식은 z=t^2 과 t=x+y 두 개의 식으로 구성된다.
이 두 개의 식을 각각 t에 대해서 미분을 하면 2t, 1이다. 그리고 이 둘을 곱하면 아래 식(그림)과 같다.
위 그림을 보자.
계산 그래프의 역전파는 오른쪽에서 왼쪽으로 진행한다고 했었다.
역전파의 계산 절차는 노드로 들어온 입력 신호에 그 노드의 국소 미분(편미분)을 곱한 후 다음 노드로 전달한다.
위에서 **2 노드에서 역전파를 살펴보자. 입력은 ∂z/∂z이며 이에 국소적 미분인 ∂z/∂t를 곱한 후 다음 노드로 넘긴다.
주목할 것은 맨 왼쪽 역전파이다. 이 계산은 연쇄법칙에 따르면 'x에 대한 z의 미분'이 된다.
즉, 역전파는 연쇄법칙의 원리와 같다고 볼 수 있다.
5.3.1 덧셈 노드의 역전파
z = x+ y 라는 식을 대상으로 역전파를 살펴보면, ∂z/∂x와 ∂z/∂y 모두 1이다 (분자 z를 각각 분모인 x,y로 미분하니까)
결론만 말하자면 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.
5.3.2 곱셈 노드의 역전파
z = x * y 라는 식을 가정해보자. 이 식의 미분은 ∂z/∂x = y , ∂z/∂y = x이다.
곱셈노드의 역전파는 상류의 값에 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다.
(서로 바꾼 값이란 순전파에서는 x였다면 y를, y였다면 x로 바꾼다는 의미)
아래 그림을 통해 이해
5.3.3 사과 쇼핑의 예
정리할 겸, 아래 그림을 통해 빈칸에 들어갈 값을 구해보자.
5.4 계층 구현하기
5.4.1 곱셈 계층
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 # x와 y를 바꾼다
dy = dout * self.x
return dx, dy
__init__에는 인스턴스 변수인 x와 y를 초기화한다. 이 두 변수는 순전파 시의 입력값을 유지하기 위해 사용!!!
MulLayer를 통해 순전파를 다음과 같이 구현할 수 있다.
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.00000000000003
"""각 변수에 대한 미분 - 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.00000000000001 200
5.4.2 덧셈 계층
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
# 덧셈 계층과 곱셈 계층 활용해 사과문제 풀이
# from layer_naive import *
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# 계층
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# 순전파
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)
# 역전파
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)
>>>
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650
5.5 활성화 함수 계층 구현하기
이제 계산그래프를 신경망 학습에 적용시켜 보자.
5.5.1 ReLu 계층
활성화 함수로 사용되는 ReLu의 수식은 0보다 클때는 그 값, 0보다 작거나 같은 경우는 0을 출력한다.
또한 미분은 다음과 같다.
역전파의 개념으로 본다면, 순전파 때 입력값이 0보다 크면 상류의 값을 그대로 흘려보내고,
0이하면 하류로 신호를 보내지 않는다. ( = 0 을 보낸다.)
이제 코드로 구현해보자.
# ReLU 계층 코드 구현
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
- Relu 클래스는 mask라는 인스턴스 변수 가짐
- mask는 True / False로 구성된 넘파이 배열
- 순전파 입력인 x의 원소값이 0 이하면 True, 아니면 False
- 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정
import numpy as np
x = np.array( [[1.0, -0.5], [-2.0, 3.0]])
print(x)
mask = (x <= 0)
print(mask)
>>>
[[ 1. -0.5]
[-2. 3. ]]
[[False True]
[ True False]]
5.5.2 Sigmoid 계층
시그모이드 함수에는 곱셈 , 덧셈 말고도 exp와 나눗셈이 등장한다.
단계별로 진행하자.
1단계
나눗셈 '/' 미분
y=1/x를 미분해보면 (∂y/∂x) -> -(x^-2) -> - (y^-2) 이다.
따라서 역전파 때는 상류에서 흘러온 값에 -y^2을 곱해서 하류로 전달한다. (순전파의 출력을 제곱한 마이너스 붙인값)
2단계
'+'노드는 그대로 전달하기
3단계
'exp'노드는 y=exp(x)연산을 수행하며 미분은 다음과 같다. ∂y/∂x=exp(x)
계산 그래프에서는 상류의 값에 순전파 때의 출력(여기서는 exp(-x)을 곱해 하류로 전파한다.!!
4단계
'x'노드는 순전파 때의 값을 서로 바꿔 곱한다! ( 여기서는 -1)
위 단계를 아래 그림과 같이 이해해보자.
파이썬 코드로 구현해보면 다음과 같다.
# sigmoid 파이썬 구현
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = sigmoid(x)
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
위 구현에서는 순전파의 출력을 인스턴스 변수인 out에 보관했다가, 역전파 계산 시에 다시 사용한다.
'밑바닥 딥러닝' 카테고리의 다른 글
Ch 6-1 학습 관련 기술들 (0) | 2023.05.29 |
---|---|
Ch 5-2 오차 역전파 (Error BackPropagation) (0) | 2023.05.27 |
Ch4-2 신경망 학습 (0) | 2023.05.26 |
Ch4-1 신경망 학습 (0) | 2023.05.17 |
Ch3-2 출력층 설계, 실습 (0) | 2023.05.17 |