앞 장에서 신경망 학습에 대해 배웠다. 가중치 매개변수에 대한 손실 함수의 기울기는 수치 미분을 통해 구했다. 하지만 수치 미분은 단순하고 구현하기도 쉽지만 계산이 오래걸린다는 단점이 있다. 이에 효율적으로 계산하는 오차역전파법(backpropagation)을 알아보자.
5.1 계산그래프
계산 그래프는 계산 과정을 그래프로 나타낸 것이다. 이는 복수의 노드와 에지로 표현된다.
다음 문제를 확인해보자.
문제1: 현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다. 이때 지불 금액을 구하라. 단, 소비세가 10%부과된다.
위 그림과 같이 처음 사과의 100원이 '*2'노드로 흐르고 200원이 되어 다음 노드로 전달된다. 이제 200원이 '*1.1' 노드를 거쳐 220원이 된다. 그러므로 최종 답은 220원이 된다.
다른 계산 그래프를 살펴보자.
위 그림은 사칙기호만을 노드에 넣고 나머지는 에지를 통해 들어가는 모습이다.
문제2: 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다. 사과는 1개에 100원, 귤은 1개 150원이다. 소비세가 10%일 때 지불 금액을 구하라.
이 문제에는 덧셈 노드인 '+'가 새로 등장하여 사과와 귤의 금액을 합산한다.
계산 그래프를 이용한 문제 풀이는 다음 흐름으로 진행된다.
1. 계산 그래프를 구성한다.
2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.
여기서 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파(forward propagation)
반대 방향의 전파를 역전파(backward propagation)이라 부른다.
5.1.2 국소적 계산
계산 그래프의 특징: '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있다.
국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과룰 출력할 수 있다는 것이다.
위 그림의 핵심은 각 노드에서의 계싼은 국소적 계산이라는 점이다. 즉, 노드는 자신과 관련한 계산 외에는 아무것도 신경 쓸게 없다. 전체 계산이 제 아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 '국소적 계산'이며 국소적 계산은 단순하지만, 전체를 구성하는 복잡한 계산을 해낼 수 있다.
5.1.3 왜 계산 그래프로 푸는가?
계산 그래프의 이점은 다음과 같다.
1. 국소적 계산
2. 계산 그래프는 중간 계산 결과를 모두 보관할 수 있다.
실제 계산 그래프르 사용하는 가장 큰 이유는 역전파를 통해 미분을 효율적으로 계산할 수 있는 점에 있다.
5.2 연쇄법칙
그동안 해온 계산 그래프의 순전파는 계산 결과를 왼쪽에서 오른쪽으로 전달했다. 한편 역전파는 '국소적이 미분'을 순방향과는 반대인 오른쪽에서 왼쪽으로 전달한다. 또한 이 '국소적 미분'을 전달하는 원리는 연쇄법칙(chain rule)에 따른 것이다.
5.2.1 계산 그래프의 역전파
생략)
5.2.2 연쇄법칙이란?
연쇄법칙은 합성 함수의 미분에 대한 성질이며 다음과 같이 정의된다.
합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱을 나타낼 수 있다.
5.2.3 연쇄법칙과 계산 그래프
위 식 5.4의 연쇄법칙 계산을 계산 그래프로 나타내보자.
위 그림과 같이 계산 그래프의 역전파는 오른쪽에서 왼쪽으로 신호를 전파한다.
역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 국소적 미분(편미분)을 곱한 후 다음 노드로 전달한다.
즉, 역전파가 하는 일은 연쇄 법칙의 원리와 같다.
5.3 역전파
연산을 예로 들어 역전파의 구조를 살펴보자.
덧셈 노드의 역전파
z=x+y라는 식을 대상으로 살펴보자.
계산 그래프를 살펴보면,
곱셈 노드의 역전파
z=xy라는 식을 생각해보자.
계산 그래프로 나타내면,
곱셈 또한 덧셈과 크게 다를 것이 없다.
5.4 단순한 계층 구현하기
5.4.1 곱셈 계층
모든 계층은 forward() 와 backward()라는 공통의 메서드(인터페이스)를 갖도록 구현할 것이다. 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
dy=dout*self.x
return dx,dy
__init__에서 인스턴스 변수 x,y 초기화. 이는 순전파 시의 입력 값을 유지하기 위해서 사용한다.
forward()에선 x,y를 인수로 받아 두값을 곱해 반환.
backward()에선 미분에 순전파 때의 값을 '서로 바꿔' 곱한 후 넘긴다.
사과예를 살펴보자.
<순전파>
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
<역전파>
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
이 계산 그래프를 코딩으로 구현하면,
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)
orange_price=mul_orange_layer.forward(orange,orange_num)
all_price=add_apple_orange_layer.forward(apple_price,orange_price)
price=mul_tax_layer.forward(all_price,tax)
dprice=1
dall_price,dtax=mul_tax_layer.backward(dprice)
dapple_price,dorange_price=add_apple_orange_layer.backward(dall_price)
dorange,dorange_num=mul_orange_layer.backward(dorange_price)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)
print(price)
print(dapple_num,dapple,dorange,dorange_num,dtax)
715.0000000000001
110.00000000000001 2.2 3.3000000000000003 165.0 650
5.5 활성화 함수 계층 구현하기
활성화 함수에 관련하여 앞선 포스트에 세밀하게 해놓았다. 그러므로 이번은 코딩으로 구현하는 것만 확인하겠다.
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
x=np.array([[1.0,-.5],[-2.0,3.0]])
print(x)
[[ 1. -0.5]
[-2. 3. ]]
mask=(x<=0)
print(mask)
[[False True]
[ True False]]
Sigmoid
class Sigmoid:
def __init__(self):
self.out=None
def forward(self,x):
out=1/(1+np.exp(-x))
self.out=out
return out
def backward(self,dout):
dx=dout*(1.0-self.out)*self.out
return dx
Affine
class Affine:
def __init__(self,W,b):
self.W=W
self.b=b
self.x=None
self.dW=None
self.db=None
def forward(self,x):
self.x=x
out=np.dot(x,self.W)+self.b
return out
def backward(self,dout):
dx=np.dot(dout,self.W.T)
self.dW=np.dot(self.x.T,dout)
self.db=np.sum(dout,axis=0)
return dx
Softmax-with-Loss
class SoftmaxWithLoss:
def __init__(self):
self.loss=None
self.y=None
self.t=None
def forward(self,x,t):
self.t=t
self.y=softmax(x)
self.loss=cross_entropy_error(self.y,self.t)
return self.loss
def backward(self,dout=1):
batch_size=self.t.shape[0]
dx=(self.y-self.t)/batch_size
return dx
'Deep Learning > 밑바닥부터 시작하는 딥러닝(1)' 카테고리의 다른 글
Chapter 6. 학습 관련 기술들(2) (0) | 2021.03.03 |
---|---|
Chapter 6. 학습 관련 기술들(1) (0) | 2021.03.02 |
Chapter 4. 신경망 학습 (0) | 2021.01.23 |
Chapter 3-2 다차원 배열의 계산 (0) | 2021.01.18 |
Chapter 3-1 신경망 (0) | 2021.01.17 |