이전 포스팅에서는 신경망 학습에 대해서 간략히 설명을 하였다.
신경망의 가중치 매개변수의 기울기(가중치 매개변수에 대한 손실 함수의 기울기)는 수치 미분을 활용을 하였다. 그러나, 수치 미분은 단순하고 구현하기도 쉽지만, 계산 시간이 오래 걸린다는 단점이 있다. 그래서 이번 장에서는 좀 더 효율적으로 구하기 위해 오차역전파법(backpropagation)에 대해서 설명을 할 것이다.
오차역전파법은 수치 미분보다 좀 더 효율적으로 기울기를 구할 수 있다. 구하는 방법이 두 가지가 있는데, 하나는 수식을 통한 것, 다른 하나는 계산 그래프를 통한 것이다. 수식을 통한 것이 가장 일반적이지만, 이 책에서는 시각적으로 이해를 돕기 위해 계산 그래프를 사용하였다.
계산 그래프(computational graph)란 계산 과정을 그래프로 나타낸 것입니다. 그래프는 노드(node)와 에지(edge)로 표현됩니다. 에지는 노드 사이의 직선을 나타냅니다.
문제 1 : 슈퍼에서 1개에 100원인 사과를 2개 샀다. 이때 지불 금액을 구해라. 단, 소비세가 10%이다.
위 문제를 계산그래프로 나타내면 아래 그림과 같다.
문제 2 : 슈퍼에서 사과를 2개, 귤을 3개 샀다. 사과는 1개에 100원, 귤은 1개에 150원이다. 소비세가 10%일 때 지불 금액을 구하시오.
위 문제를 계산그래프로 풀면 아래 그림과 같다.
계산 그래프를 이용한 문제풀이는 아래와 같은 흐름으로 진행된다.
1.계산 그래프를 구성한다.
2.그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.
순전파(propagation) : 여기서는 왼쪽에서 오른쪽으로 진행
역전파(back propagation): 오른쪽에서 왼쪽으로 진행
국소적 계산 : 전체에서 어떤일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.
합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.
역전파는 오른쪽에서 왼쪽으로 신호를 보낸다. 역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 편미분을 곱한 후 다음 노드로 전파를 한다. **2 노드에서 입력은 이며, 국소적 미분인 를 곱하고 다음 노드에 넘긴다. 중요한건 맨 왼쪽 노드이다.은 연쇄법칙에 의해 성립이 되어 'x에 대한 z의 미분'이 된다. 즉, 역전파가 하는 일은 연쇄법칙의 원리와 같다는 것이다.
z = x + y일 때,
위의 식을 계산그래프로 나타내면
이렇게 되면 그냥 상류로부터 전해진 그값을 그대로 하류로 흘려내려준다고 생각을 하면 된다.
z = xy일 때,
위의 그림을 보면, 곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 '서로 바꾼 값'을 곱해서 하류로 보낸다. 그러므로, 곱셈의 역전파는 순방향 입력 신호의 값이 필요하기에, 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해 두어야한다.
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
논문구현에 나오는 코드들을 보면 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 초기화함수에서 순전파 때 썼던 x, y를 쓰지않기에 그냥 pass로 두었다.
위의 식에서 x에 대한 y의 미분은
위의 사진이 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, 그 외(0보다 큰 원소)는 False로 유지한다. 순전파 때의 입력 값이 0 이하면 역전파 때의 값은 0이 돼야 한다. 그래서 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정한다.
class Sigmoid:
def __init__(self):
self.out = None
def format(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
이 구현에서는 순전파의 출력을 인스턴스 변수 out에 보관했다가, 역전파 계산 때 그 값을 사용합니다.
신경망의 순전파 때 수행하는 행렬의 곱은 기하학에서는 어파인변환(Affine transformation)이라고 한다.
위 사진은 단순한 계산 그래프이다. 지금까지는 스칼라 값이 흘렀는데, 이제부터는 행렬이 흐른다.
위 식은 역전파할 때의 흘러가는 값이고, 아래는 역전파에서의 계산 그래프이다.
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 backword(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
이 코드에서 주의 깊게 보아야 할 것은 sum(dout, axis=0)이 부분이다. 이 코드는 각 열들의 합을 모아 하나의 행렬로 나타낸다는 것이다.
소프트맥스 함수는 입력 값을 정규화하여 출력을 한다.
위 그림과 같이 Softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력한다. 예를 들어, 손글씨 숫자가 10개이므로 Softmax 계층의 입력은 10개가 된다.
신경망에서 수행하는 작업은 학습과 추론 두 가지이다. 추론할 때는 일반적으로 Softmax 계층을 사용하지 않는다. 마지막 Affine 계층의 출력을 인식 결과로 이용한다.또한, 신경망에서 정규화하지 않는 출력결과에서는 Softmax 앞의 Affine 계층을 점수라고 한다. 즉, 신경망 추론에서 답을 하나만 내는 경우에는 가장 높은 점수만 알면되니 Softmax 계층은 필요없다.
반면, 신경망을 학습할 때는 Softmax 계층이 필요하다.
'소프트 맥스 함수'의 손실 함수로 '교차 엔트로피 오차'를 사용하니 역전파가 ()로 말끔히 떨어진다. 이건 우연이 아니라 교차 엔트로피 함수가 그렇게 설계되었기 때문이다.또, 회귀의 출력층에서 사용하는 '항등 함수'의 손실 함수로 '오차제곱합'을 이용하는 이유도 이와 같다. 즉, '항등 함수'의 손실 함수로 '오차제곱합'을 사용하면 역전파의 결과가 () 로 말끔히 떨어진다.
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 손실
self.y = None # Softmax의 출력
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