수치 미분은 단순하고 구현하기도 쉽지만
계산 시간이 오래 걸린다는 게 단점이다.
가중치 매개변수의 기울기를 효율적으로 계산하는 오차역전파법 backpropagation 을 배워보자.
오차역전파법을 제대로 이해하는 방법은 두 가지가 있다.
계산 그래프 computational gragh 는 계산 과정을 그래프로 나타낸 것.
그래프는 복수의 노드 node 와 에지 edge 로 표현된다.
계산 그래프에 익숙해지자.
문제 1
현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다.
이때 지불 금액을 구하자.
단, 소비세가 10% 부과된다.
계산 그래프는 계산 과정을 노드와 화살표(에지)로 표현한다.
계산 그래프로 풀어본 문제 1의 답
[fig 5-2 계산 그래프로 풀어본 문제 1의 답 : 사과의 개수와 소비세를 변수로 취급해 원 밖에 표기
문제 2
현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다.
사과는 1개에 100원, 귤을 1개에 150원이다.
소비세가 10% 일 때 지불 금액을 고르자.
[fig 5-3] 계산 그래프로 풀어본 문제 2의 답
계산 그래프를 이용한 문제 풀이는 다음 흐름으로 진행된다.
여기서 계산을 왼쪽에서 오른쪽으로 진행하는 단계를 순전파 forward propagation 이라고 한다.
순전파는 계산 그래프의 출발점부터 종착점으로의 전파이다.
오르쪽에서 왼쪽으로의 전파도 있다. 역전파이다!
역전파는 이후 미분을 계산할 때 중요한 역할을 한다.
계산 그래프의 특징은 국소적 계산을 전파함으로써
최종 결과를 얻는다는 점에 있다.
국소적이란, 자신과 직접 관계된 작은 범위라는 뜻이다.
국소적 계산은 결국, 전체에서 어떤 일이 벌어지든 상관없이
자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.
계산 그래프의 각 노드에서의 계산은 국소적이다.
계산 그래프는 국소적 계산에 집중한다.
전체 계산이 아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 국소적 계산이다.
국소적인 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 할 수 있다.
계산 그래프의 이점.
실제 계산 그래프를 사용하는 가장 큰 이유는
역전파를 통해 미분을 효율적으로 계산할 수 있는 점에 있다.
문제 1은 사과를 2개 사서 소비세를 포함한 최종 금액을 구하는 것이다.
여기에서 만약 사과 가격이 오르면 최종 금액에 어떤 영향을 끼치는지 알고 싶다면,
이는 사과 가격에 대한 지불 금액의 미분을 구하는 문제에 해당한다.
계산 그래프 상의 역전파에 의해 미분을 구할 수 있다.
[그림5-5]
역전파는 순전파와는 반대 방향의 굵은 화살표로 그린다.
이 전파는 국소적 미분을 전달하고, 그 미분 값은 화살표의 아래에 적는다.
이 결과로부터 사과 가격에 대한 지불 금액의 미분 값은 2.2라 할 수 있다.
사과가 1원 오르면 최종 금액은 2.2원 오른다는 의미이다.
여기에서는 사과 가격에 대한 미분만 구했지만,
소비세에 대한 지불 금액의 미분이나, 사과 개수에 대한 지불 금액의 미분도 같은 순서로 구할 수 있다.
그리고 그 때는 중간까지 구한 미분 결과를 공유할 수 있어서
다수의 미분을 효율적으로 계산할 수 있다.
이처럼 계산 그래프의 이점은, 순전파와 역전파를 활용해서
각 변수의 미분을 효율적으로 구할 수 있다는 것이다.
국소적 미분을 전달하는 원리는 연쇄법칙 chain rule 에 따른 것이다.
[식 5-2,3,4]
식 5-4의 연쇄법칙 계산을 계산 그래프로 나타내보자.
[그림5-7]
역전파의 계산 절차에서
노드로 들어온 입력 신호에
그 노드의 국소적 미분:편미분을 곱한 후
다음 노드로 전달한다.
즉, 역전파가 하는 일은 연쇄 법칙의 원리와 같다.
[그림5-8]
앞에서 계산 그래프의 역전파가 연쇄법칙에 따라 진행되는 모습을 설명했다.
이번 절에서는 +,x 등의 연산을 예로 들어 역전파의 구조를 살펴보자.
z = x+y
라는 식을 대상으로 그 역전파를 살펴보자.
z = x+y
의 미분은 다음과 같이 해석적으로 계산할 수 있다.
식5-5
이를 계산 그래프로는 그림5-9 처럼 그릴 수 있다.
그림5-9
z = xy
라는 식을 생각해보자.
이 식의 미분은 다음과 같다.
위 식에서 계산 그래프는 다음과 같이 그릴 수 있다.
곱셈 노드의 역전파는
상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다.
구체적인 예를 들어보자.
10*5=50
이라는 계산이 있고,
역전파 때 상류에서 1.3 값이 흘러온다고 하자.
이를 계산 그래프로 그리면 다음과 같다.
덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값은 필요하지 않다.
하지만 곱셈의 역전파는 순방향 입력 신호의 값이 필요하다.
그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해둔다.
사과 쇼핑 예를 다시 살펴보자.
사과 쇼핑 예를 파이썬으로 구현해보자.
여기서는 계산 그래프의 곱셈 노드를 MulLayer, 덧셈 노드를 AddLayer 이름으로 구현한다.
다음 절에서는 신경망을 구성하는 계층 각각을 하나의 클래스로 구현한다.
여기서의 계층이란, 신경망의 기능 단위이다.
모든 계층은 forward():순전파 와 backward():역전파 라는 공통의 메서드:인터페이스를 갖도록 구현한다.
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
이 MulLayer 를 사용해서
사과 쇼핑을 구현해보자.
[그5-16]
MulLayer 를 사용해서 순전파를 구현해보자.
from layer_naive import *
apple = 100
apple_num = 2
tax = 1.1
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price)
각 변수에 대한 미분은 backward() 에서 구할 수 있다.
# 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
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
[그5-17]
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)
다음절에서는 신경망에서 사용하는 계층을 구현합니다.
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,
그 외 0보다 큰 원소는 False 로 유지한다.
Relu 계층은 전기 회로의 스위치에 비유할 수 있다.
순전파 때 전류가 흐르고 있으면 스위치를 On 으로 하고,
흐르지 않으면 Off 로 한다.
역전파 때는 스위치가 On 이라면 전류가 그대로 흐르고,
Off 이면 더 이상 흐르지 않는다.
시그모이드 함수.
식5-9
이를 계산 그래프로 그리면 다음과 같다.
그림5-19
exp
와 /
노드가 새롭게 등장했다.
exp 노드는 y=exp(x)
계산을 수행하고
/
노드는 y=1/x
계산을 수행한다.
이제 그림 5-19의 역전파를 알아보자.
1단계
/
노드를 미분하면 다음 식이 된다.
식5-10
계산 그래프에서는 다음과 같다.
2단계.
+
노드는 상류의 값을 여과 없이 하류로 내보다는 게 다이다.
계산 그래프에서는 다음과 같다.
3단계.
exp 노드의 미분은 다음과 같다.
식11
계산 그래프는 다음과 같다.
4단계.
x 노드는 순전파 때의 값을 서로 바꿔 곱한다.
이상으로 그림 5-10과 같이
Sigmoid 계층의 역전파를 계산 그래프로 완성했다.
그림20
그림5-20의 계산 그래프의 중간 과정을 모두 묶어 그림 5-21처럼 단순한 sigmoid 노드 하나로 대체할 수 있다.
그림21
계산 그래프의 간소화 버전은 역전파 과정의 중간 계산들을 생략할 수 있어 더 효율적인 계산이라 할 수 있다.
또 노드를 그룹화하여 Sigmoid 계층의 세세한 내용을 노출하지 않고
입력과 출력에만 집중할 수 있다는 것도 중요한 포인트이다.
또한 식을 다음과 같이 정리할 수 있다.
식12
이처럼 Sigmoid 계층의 역전파는 순전파의 출력 y 만으로 계산할 수 있다.
그22
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 에 보관했다가,
역전파 계산 때 그 값을 사용한다.