Back Propagation

3 minute read

역전파

신경망을 학습하거나 테스트를 할 때에는
연산이 입력층에서 출력층으로 향했다.
이를 순전파라고 한다.

이와 반대로 출력층 쪽에서 입력층으로 값이 이동하는 것을
역전파(back propagation)라고 한다.

역전파는 해당 연산 단계에서 각 입력에 따른 출력의 변화량
즉 당시의 미분 결과를 구한다.

출력 후 출력층 쪽에서 가중치에 대한 손실함수, 그리고 입력층까지의 모든 변환을 다 수치 미분하면 시간이 오래 걸린다.
이 대신 역전파를 통하여 미분 값만을 구하게 되면 보다 더 효율적으로 기울기를 구할 수 있다.

예를 들어 다음과 같은 노드가 있다.

x1 \    
    + -> y  
x2 /   

y = x1 + x2 라는 순전파를 그래프로 그린 것이다.
이때 y 쪽에서 1 이라는 값을 넣으면

  • 노드에서 미분을 하게 된다.

y = x1 + x2 라는 식을 x1 에 대해 미분하면 1 이 나온다.
x2 에 대해 미분하면 역시 1 이 나온다.

출력측의 역전파로 x1, x2 는 모두 1이라는 값을 얻게 된다.
이는 x1이 1 증가할 시 y 가 1 증가함을 의미한다.

x1 \    
    X -> y  
x2 /   

이번엔 곱셈을 해보자.
y 측에서 1을 넣으면 X (곱하기) 측에서 미분을 하여
x1 에는 x2가, x2에는 x1 이 들어갈 것이다.

이번엔 역전파로 x1 은 x2, x2 는 x1 이라는 값을 얻게 된다.
x1 이 1 증가하면 y 는 x2 만큼 증가함을 의미한다.

x1 \
    X ㅡㅡㅡ X -> y
x2 /   5 /

이번에는 합성미분을 해보자.
y 측에서 1을 넣으면
위쪽에는 5가 들어갈 것이다.
그리고 첫번째 곱셈 쪽에서 또 미분을 하여
최종적으로 x1 쪽에는 5 * x2 가,
x2 쪽에는 5 * x1 이 들어간다.
( f(t), t = d(x) 일 때 f’(t)의 미분은 f’(t) * d’(x) 이다. )

이렇듯 역전파로 실제 미분을 하지 않아도 기울기를 구할 수 있다.

예시로는 덧셈과 곱셈을 들었지만
신경망 학습에는 ReLU나 Sigmoid 같은 활성화 함수, Softmax 같은 출력 함수 등이 있다.
이에 대한 역전파 함수는 다음과 같다.

ReLU 역전파

ReLU 함수는 0 이하이면 0이고 0보다 크면 그대로 출력한다.
이를 미분하면 (입력층에서 오는) 입력값이 0 이하일 때 0 이고, 0보다 크면 1이 된다.

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

순전파를 하면서 그때의 결과(어디가 0보다 큰지)를 클래스 변수에 저장하고
역전파를 하게 되면 그때 들어오는 입력 (이도 오른쪽에서 온 변화량이다.)에서 순전파할 때의 결과를 토대로 미분값을 출력한다.
순전파할 때 입력값이 0 이하였다면 그때의 출력도 0이었을 것이므로,
역전파할 때의 출력도 0 이 되어야 한다.
반면 0 초과였다면 역전파할 때의 출력은 그대로 간다. (사실은 ReLU 함수의 미분값인 1을 곱한 것이다.)

Sigmoid 역전파

sigmoid 함수는 1 + e^(-x) 의 역수이다.
이를 미분하면 출력값을 y 라 했을 때 y(1-y) 라는 결과를 얻는다.
순전파할 때의 y 값을 저장해두면 역전파할 때는 그 값을 그대로 가져다 쓰기만 하면 된다.
수치 미분을 했더라면 저 함수를 또 두 번씩이나 구했을 것이다.

class Sigmoid:
  def __init__(self):
    self.y = None
  
  def forward(self, x):
    self.out = 1 / (1 + np.exp(-x))
    return self.out
    
  def backward(self, dOut):
    return dOut * (1.0 - self.out) * self.out

Affine 역전파

신경망은 노드 * 가중치 + 편향을 계속 다음 층에 전달한다.
이때 이 작업을 Affine 계층이라고 한다.

Affine 계층은 다른 계층과 달리 행렬의 곱을(np.dot(a, b)) 필요로 한다.

이때 행렬의 곱 미분은
y = W * X 라고 할 때 dy/dW = X^T, dy/dX = W^T 이다.

단 이것이 합성되었을 경우 곱하는 순서를 맞춰주어야 한다.
예를 들어 오른쪽에서 dL/dy 이라는 값이 들어오면 dL/dW 는 dL/dy * X^T이고
dL/dX 는 W^T * dL/dy 이다.

편향은 덧셈이므로 그냥 보내면 된다.
단 행렬 입력이므로 각 행들의 값을 더해야 한다.

Softmax 와 Cross Entropy Error

출력 함수로 softmax 를 구하는 경우 (분류)
손실 함수로 cross entropy error 를 이용한다.

역전파는
출력값 -> 손실함수 -> 소프트맥스 -> 활성화n -> 어파인n -> 활성화n-1 -> 어파인n-1 …
의 단계를 거치게 되는데
이때 소프트맥스와 활성화 n 번째 사이에서의 값이
yk - tk 가 된다.

반대로 출력 함수로 항등 함수, 손실 함수로 평균 제곱 오차 함수를 쓰게 되어도
같은 결과를 볼 수 있다.

정리

  1. 신경망의 각 층을 넘어갈 때 노드들과 각 가중치의 곱 + 편향의 값을 구한다. (Affine)
  2. 해당 값으로 활성화 함수를 구하여 다음 노드의 값을 정한다. (ReLU, Sigmoid)
  3. 위 과정을 반복 후 출력층 함수를 통하여 출력한다. (Softmax, 항등)
  4. 첫 가중치로 인한 출력과 정답을 비교하는 손실 함수의 기울기를 구한다. (Mean Squared / Cross Entropy Error)
  5. 구해진 기울기와 학습률을 반영하여 가중치를 수정한다. (경사법)
  6. 위 과정을 학습 데이터 / 배치 크기만큼 반복한다.

5번째 과정에서 우리는 수치 미분이 아니라 역전파로 보다 효율적으로 기울기를 계산할 수 있게 되었다.