5.1 개요(Introduction)

5.1.1 동기(Motivation)

이번 장에서는 연쇄법칙(chain rule)을 반복적으로 적용하여 표현식의 그래디언트를 계산하는 방법인, 역전파(backpropagation)에 대한 직관적 이해를 기를것입니다. Neural Networks를 이해하고 효율적으로 구현하며 디버깅을 하는데 있어서 이 과정을 이해하는 것은 매우 중요합니다.

5.1.2 문제 진술(Problem statement)

이번 장에서 다루는 핵심 문제는 다음과 같습니다: 입력 벡터 \(x\)에 대해 함수 \(f(x)\)가 주어질 때, \(x\)에서 \(f\)의 그래디언트 (\(\nabla f(x)\))를 계산하고자 합니다.

5.1.3 동기(Motivation)

이 문제에 관심을 가지는 주된 이유는 특정 Neural Networks에서 \(f\)는 loss function ( \(L\) )에 해당하고 입력 \(x\)는 트레이닝 데이터와 neural network의 weight에 해당하기 때문입니다. 예를 들어, loss는 SVM loss function이고, 입력은 트레이닝 데이터 \((x_i,y_i), i=1 \ldots N\)와 weight 및 bias \(W,b\)에 해당합니다. 여기서도 일반적인 머신러닝의 경우와 같이, 고정된 트레이닝 데이터가 주어지며, weight를 변수처럼 조정할 수 있습니다. 따라서, backpropagation을 이용하면 입력 \(x_i\)의 그래디언트를 쉽게 계산할 수 있지만, 실전에서는 대개 (\(W,b\))와 같은 파라미터의 그래디언트만을 구하고 이를 파라미터 업데이트에 이용합니다. 하지만, 시각화의 목적이나 Neural Network가 무엇을 하는지에 대해 해석할 때 \(x_i\)는 유용하게 쓰일 수 있습니다.

체인룰을 이용하여 그래디언트를 계산하는것이 익숙하다면 이번 장을 건너뛰어도 좋습니다. 간단한 회로 내의 실제 값들을 가지고 backward flow를 따라감으로써 backpropagation에 대한 통찰을 얻을 수 있을 것입니다.

5.2 그래디언트의 간단한 식과 해석(Simple expressions and interpretation of the gradient)

더 복잡한 식의 표기법(notation)이나 규칙(convention)들로 넘어가기 전에 간단한것부터 시작해봅시다. 두 수의 곱으로 표현된 간단한 함수 \(f(x,y) = x y\)가 있습니다. x와 y 둘 중 어느 쪽의 입력에 대해서도 편미분을 간단하게 구할 수 있습니다:

\[f(x,y) = x y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = y \hspace{0.5in} \frac{\partial f}{\partial y} = x\]

5.2.1 해석(Interpretation)

미분(derivative)은 특정 지점에서 매우 작은(infinitesimally) 영역을 가지는 변수에 대한 함수의 변화율을 의미합니다:

\[\frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h}\]

왼쪽 항에서 나눗셈은 오른쪽 항의 나눗셈과는 다르게 사실 나눗셈이 아닙니다. 이 표기는 \( \frac{d}{dx} \)가 함수 \(f\)에 적용되며 새로운 함수(미분)를 반환하는 것을 의미합니다. 위 식에서 \(h\)가 매우 작을 때, 함수는 거의 직선으로 근사되고 미분은 기울기를 의미하게 됩니다. 다시 말해, 각 변수에 대한 미분은 그 값에 대한 전체 식의 민감도(sensitivity)를 의미합니다. 가령, \(x = 4, y = -3\)이고 \(f(x,y) = -12\)일 때, \(x\)에 대한 미분은 \(\frac{\partial f}{\partial x} = -3\)이 됩니다. 이는 \(x\)를 아주 조금 증가시켰을 때, 전체 식에 대한 영향은 그 값의 3배만큼 감소함(- 부호에 의해)을 의미합니다. 위의 식을 ( \( f(x + h) = f(x) + h \frac{df(x)}{dx} \) )와 같이 고침으로써 확인할 수 있습니다. 이와 유사하게, \(\frac{\partial f}{\partial y} = 4\)이므로 \(y\)를 아주 작은 값 \(h\)만큼 증가시켰을 때, 함수의 출력은 \(4h\)만큼 증가(+ 부호에 의해)합니다.

각 변수에 대한 미분은 그 값에 대한 전체 식의 민감도(sensitivity)를 의미합니다.

앞서 말한 바와 같이, 그래디언트 \(\nabla f\)는 편미분 벡터입니다. 따라서 \(\nabla f = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [y, x]\)가 됩니다. 기술적 측면에서 그래디언트는 벡터이기 때문에 “x에 대한 편미분(the partial derivative on x)”라고 하는 것이 정확한 표현이지만, 이를 간단히 “x에 대한 그래디언트(the gradient on x)”라고 사용할 것입니다.

덧셈 연산에 대해서도 미분을 유도할 수 있습니다:

\[f(x,y) = x + y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = 1 \hspace{0.5in} \frac{\partial f}{\partial y} = 1\]

즉, \(x,y\)에 대한 그래디언트는 \(x,y\)의 값에 관계없이 1이 됩니다. \(x,y\) 둘 중 하나를 증가시키는것이 출력 \(f\)를 증가시키고, (곱셈 연산과는 달리) 증가율이 \(x,y\)의 실제 값에 영향을 받지 않기(independent) 때문에 가능합니다. 앞으로 자주 등장할 함수는 max 연산입니다:

\[f(x,y) = \max(x, y) \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = \mathbb{1}(x >= y) \hspace{0.5in} \frac{\partial f}{\partial y} = \mathbb{1}(y >= x)\]

즉, 더 큰 입력의 (sub)gradient는 1이고 작은 입력은 0이 됩니다. 쉽게 말해, 입력이 \(x = 4,y = 2\)일 때 max는 4가 되고 \(x\)에 대한 (sub)gradient는 1, \(y\)는 0(함수가 \(y\)에 민감하지(sensitive) 않음을 의미)이 됩니다. 즉, 아주 작은 \(h\)만큼을 증가시켰을 때, 함수는 계속 4를 출력할 것이고 그래디언트는 0가 됩니다: 아무런 영향이 없음을 의미합니다. 물론 \(y\)가 크게 변화하는 경우(2 이상), \(f\)의 값은 바뀔것입니다. 그러나 미분에서는 함수 입력이 크게 변화할 때의 영향을 이야기하지 않습니다; 미분은 \(\lim_{h \rightarrow 0}\)의 정의에서 알 수 있듯이 입력에서 아주 미세하고, 극도로(infinitesimally) 작은 변화에 대한 정보를 가집니다.

5.3 복잡한 표현식과 체인룰(Compound expressions with chain rule)

이제 \(f(x,y,z) = (x + y) z\)와 같이 여러 함수로 구성된 보다 복잡한 표현식을 살펴보겠습니다. 이 표현식은 여전히 ​​직접 미분할 수 있을 정도로 간단하지만, backpropagation에 대한 직관적인 이해를 주는 방식으로 접근해보겠습니다. 이 표현식은 두 개의 표현식으로 쪼갤 수 있습니다:\(q = x + y\)와 \(f = q z\). 게다가 각각의 표현식에 대한 미분을 계산하는 방법을 알고 있습니다. \(f\)는 \(q\)와 \(z\)의 곱이므로 \(\frac{\partial f}{\partial q} = z, \frac{\partial f}{\partial z} = q\)이며, \(q\)는 \(x\)와 \(y\)의 합이므로 \( \frac{\partial q}{\partial x} = 1, \frac{\partial q}{\partial y} = 1 \)입니다. 하지만, 중간값 \(q\)에 대한 그래디언트는 신경쓸 필요가 없기 때문에 \(\frac{\partial f}{\partial q}\)는 불필요합니다. 결국 입력 \(x,y,z\)에 대한 \(f\)의 그래디언트를 알고 싶을 뿐입니다. chain rule은 그래디언트 표현식들을 곱셈을 통해서 “chain”합니다. \(\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x} \)는 두 개의 그래디언트를 나타내는 두 수의 간단한 곱입니다. 다음 예를 살펴봅시다:

# set some inputs
x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1

최종적으로, f에 대한 변수 x,y,z의 민감도(sensitivity)를 의미하는 그래디언트 [dfdx,dfdy,dfdz]를 알 수 있습니다. 이는 backpropagation의 가장 간단한 예시입니다. 앞으로는 df를 없애고 보다 간결한 표현을 사용하고자 합니다. 즉, dfdq 를 단순하게 dq로 표현할것 이고, 이를 최종 출력에 대한 그래디언트라고 간주합니다.

이 연산 또한 회로도(circuit diagram)로 시각화 할 수 있습니다:

-2-4x5-4y-43z3-4q+-121f*

왼쪽의 “회로(circuit)”는 연산 과정을 실제값을 이용하여 시각적으로 표현합니다. 초록색으로 표시된 forward pass는 입력에 대한 출력 값을 계산합니다. 이후 빨강색으로 표시된 backward pass는 끝에서 부터 반복적으로(recursively) 체인룰을 적용함으로써 미분을 계산하는 backpropagation을 수행합니다. 그래디언트는 회로를 타고 거꾸로 흐른다고(flowing backwards) 생각할 수 있습니다.

5.4 역전파에 대한 직관적 이해(Intuitive understanding of backpropagation)

역전파는 아름다운 로컬 프로세스입니다. 회로도의 모든 게이트에서는는 입력을 가지고 두 가지를 바로 계산할 수 있습니다: 1. 출력 값 2. 출력값에 대한 입력의 로컬(local) 그래디언트. 이는 게이트가 속한 전체 회로의 세부사항을 모르고도 완전히 독립적으로 수행 됩니다. 하지만, forward pass가 끝나는 순간, 결국 게이트는 backpropagation을 통해 전체 회로의 최종 출력에서 출력값의 그래디언트를 학습하게 됩니다. chain rule에 따라 각 게이트에서는 그 그래디언트를 가지고 게이트의 모든 입력이 되는 그래디언트를 구합니다.

chain rule에서 (각 입력에 대한) 추가적인 곱(multiplication) 연산은, 쓸모없던 하나의 게이트를, neural network같이 복잡한 회로에서 쓸모있는 톱니(cog)로써 역할하게 합니다.

위 예시를 다시보며 작동하는 방식에 대한 직관을 얻어보도록 합시다. add 게이트는 입력 [-2, 5]을 받아 3을 출력하였습니다. 게이트가 합 연산을 수행하기 때문에, 두 입력에 대한 로컬 그래디언트는 +1이 됩니다. 회로의 나머지 부분에서는 최종값 -12를 계산했습니다. 회로를 통해 chain rule이 반복적으로(recursively) 적용되는 backward pass에서, (mul 게이트의 입력이 되는) add 게이트는 출력에 대한 그래디언트가 -4임을 학습합니다. 회로는 높은 출력 값을 갖길 위해서, add 게이트의 출력이 4의 강도(force)로 낮아지기를(- 부호로 인해) “원한다고(wanting)” 생각할 수 있습니다. 그래디언트를 chain하는 과정을 반복하기 위해서, add게이트는 그 그래디언트를 가지고 그 입력에 대한 모든 로컬 그래디언트에 곱합니다(xy의 그래디언트를 1 * -4 = -4로 만듦). 이는 다음과 같은 기대효과가 있습니다: 만약 x, y가 감소하는 경우(음의 그래디언트 때문에), add게이트의 출력은 감소할 것이고 이는 결과적으로 mul게이트의 출력의 증가로 이어집니다.

따라서 backpropagation은 그래디언트 신호를 통해 출력이 얼마만큼 증가하거나 감소하길 원하는지에 대한 게이트 간의 통신으로 이해할 수 있습니다. 결국 이를 통해 최종 출력값이 높아지도록 만듭니다.

5.5 모듈성: 시그모이드(Modularity: Sigmoid example)

앞서 언급한 게이트들은 다소 임의적이었습니다. 미분가능한 어떤 함수라도 게이트 역할을 할 수 있으며, 여러 게이트를 단일 게이트로 그룹화하거나, 편의에 따라 함수를 여러 게이트로 분해 할 수도 있습니다. 아래의 표현식을 살펴보겠습니다:

\[f(w,x) = \frac{1}{1+e^{-(w_0x_0 + w_1x_1 + w_2)}}\]

위 식은 시그모이드 활성화(sigmoid activation) 함수를 사용하는 2차원 뉴런(입력 x 및 weight w)을 의미합니다. 지금은 이를 단순히 입력 w, x에서 하나의 숫자를 출력하는 함수 정도로 생각합니다. 함수는 여러 개의 게이트로 구성됩니다. 앞서 설명한 게이트(add, mul, max) 외에도 네 가지가 더 있습니다:

\[f(x) = \frac{1}{x} \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = -1/x^2 \\\\ f_c(x) = c + x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = 1 \\\\ f(x) = e^x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = e^x \\\\ f_a(x) = ax \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = a\]

\(f_c, f_a\)는 각각 입력을 상수 \(c\)만큼 평행이동(translate)하고, 상수 \(a\)만큼 크기를 바꾸는(scale) 함수입니다. 이는 합과 곱에 해당하긴 하지만, 상수 \(c,a\)에 대한 그래디언트도 필요하기 때문에 여기서는 (새로운) unary 게이트로써 정의합니다. 전체 회로는 다음과 같습니다:

2.00-0.20w0-1.000.39x0-3.00-0.39w1-2.00-0.59x1-3.000.20w2-2.000.20*6.000.20*4.000.20+1.000.20+-1.00-0.20*-10.37-0.53exp1.37-0.53+10.731.001/x

시그모이드 활성화 함수를 포함하는 2차원 뉴런 회로도. 입력은 [x0,x1]이고 학습가능한(learnable) 뉴런의 weights는 [w0,w1,w2]입니다. 뉴런은 입력과의 내적(dot product)을 연산하고 그 활성(activation)은 시그모이드 함수에 의해 0과 1사이의 값으로 부드럽게 뭉개집니다(squashed).

위 예시에서, w과 x의 내적에 대한 결과를 보여주는 함수들의 chain을 볼 수 있습니다. 이러한 연산을 적용한 함수를 시그모이드 함수(sigmoid function) \(\sigma(x)\)이라고 부릅니다. 여기서 분자에 1을 더하고 빼는 트릭을 적용한 이후에 미분을 하면 입력에 대한 시그모이드 함수의 미분이 매우 간단해진다는 사실을 알 수 있습니다:

\[\sigma(x) = \frac{1}{1+e^{-x}} \\\\ \rightarrow \hspace{0.3in} \frac{d\sigma(x)}{dx} = \frac{e^{-x}}{(1+e^{-x})^2} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1+e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x)\]

위의 과정을 통해 그래디언트는 아주 간단해집니다. 가령, 시그모이드 표현식은 입력 1.0을 받아 forward pass를 통해 0.73을 출력합니다. 위에서 로컬(local) 그래디언트는 이전에 계산한 값과 동일하게 (1 - 0.73) * 0.73 ~= 0.2가 됩니다. 또한 하나의 간단하고 효율적인(또한 수적 연산이 덜한) 표현식이 됩니다. 따라서 실제 구현에서 이러한 연산들을 하나의 게이트로 그룹화하는것은 매우 유용하게 사용됩니다. 코드를 통해 이 뉴런에 대한 backprop을 살펴봅시다:

w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]

# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit

5.5.1 구현 꿀팁: 단계별 역전파(Implementation protip: staged backpropagation)

위의 코드에서 볼 수 있듯이, 실전에서는 forward pass를 여러 단계로 쪼개서(break down) backprop을 하기 쉽게 만드는것이 좋습니다. 여기서는 wx의 내적값을 가지는 중간 변수 dot을 두었습니다. backward pass에서는 wx의 그래디언트를 갖는 변수들(ddot, 최종적으로는 dw, dx)을 반대 방향으로 거슬러 올라가며 계산합니다.

이 장의 요는 backpropagation의 동작에 대한 세부사항이나 forward function의 어느 부분이 게이트에 해당하는지 같은 것들은 편의상의 문제라는 것입니다. 이는 표현식의 어느 부분이 간단한 로컬 그래디언트를 가지는지를 파악할 수 있게 하고, 최소한의 코드와 노력만으로도 이들을 서로 연결(chained)시킬 수 있게 도와줍니다.

5.6 실전에서의 역전파: 단계별 연산(Backprop in practice: Staged computation)

다른 예시를 봅시다. 아래와 같은 형태의 함수가 있다고 가정합니다:

\(f(x,y) = \frac{x + \sigma(y)}{\sigma(x) + (x+y)^2}\) 분명한 것은, 실전에서의 backpropagation에 대한 좋은 예시라는 것 외에는 쓸모없으며 그래디언트를 왜 구하려는지도 모를것입니다. \(x\)나 \(y\)에 대한 미분을 수행하기 시작한다면, 매우 크고 복잡한 표현으로 얻을 수 있을 것입니다. 그러나 그래디언트를 구하는 명확한 함수가 필요하지 않기 때문에 이러한 과정 또한 전혀 필요하지 않습니다. 단지 이를 계산하는 방법만 알면 됩니다. 아래는 표현식의 forward pass를 코드로 구현한 것입니다:

x = 3 # example values
y = -4

# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator   #(1)
num = x + sigy # numerator                               #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # denominator                        #(6)
invden = 1.0 / den                                       #(7)
f = num * invden # done!                                 #(8)

휴, forward pass를 끝까지 다 계산했습니다. 코드는 여러 개의 중간 변수를 포함하는 방식으로 구현하였고, 각 변수는 로컬 그래디언트에 대한 단순한 표현입니다. 따라서 backprop pass를 계산하는 것은 간단합니다: 뒤에서 거슬러가는 동안 등장하는 모든 변수들(sigy, num, sigx, xpy, xpysqr, den, invden)은 forward pass와 동일한 변수를 가집니다. 하지만 회로 출력의 그래디언트를 가지는 den은 다릅니다. 또한, backprop의 모든 부분은 그 표현식에 대한 로컬 그래디언트를 연산하고 이를 곱셈 연산을 통해 chaining하는 것을 포함합니다. 아래의 코드에서는 각 줄마다, forward pass의 어느 부분에 해당하는지 표시 해두었습니다:

# backprop f = num * invden
dnum = invden # gradient on numerator                             #(8)
dinvden = num                                                     #(8)
# backprop invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# backprop xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# backprop num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# done! phew

몇 가지 유의사항이 있습니다:

Cache forward pass variables.

5.6.1 순전파 변수들을 저장합니다(Cache forward pass variables)

backward pass를 계산할 때 forward pass에서 계산한 변수들을 재사용하는 것이 좋습니다. 실전에서는, 이러한 변수를 저장(cache)해서 backpropagation 중에 사용할 수 있도록 코드를 설계합니다. 만약 이것이 너무 어렵다면, (낭비가 생기지만) 다시 계산하는 것도 가능합니다.

5.6.2 분기점에서 그래디언트를 합합니다(Gradients add up at forks)

forward 표현식은 변수 x, y를 여러번 포함하므로, backpropagation을 수행할 때 +=를 사용할 경우(변수에 그래디언트를 누적시키기 위해), =를 잘못 써서 덮어쓰지 않도록 주의해야 합니다. 이는 미적분학의 multivariable chain rule을 따릅니다. 만약 변수가 회로의 다른 부분으로 분기(braches out)한다면, 그 분기점에서 거꾸로 전달(flow back)되는 그래디언트도 합해주어야 합니다.

5.7 역전파에서의 패턴(Patterns in backward flow)

많은 경우에 거꾸로 흐르는(backward-flowing) 그래디언트를 직관적인 수준에서 해석할 수 있습니다. 가령, neural networks에서 가장 일반적으로 사용되는 세 개의 게이트(add,mul,max) 모두 backpropagation 중에 어떻게 작동하는지를 매우 간단히 해석할 수 있습니다. 아래의 예시를 살펴봅시다:

3.00-8.00x-4.006.00y2.002.00z-1.000.00w-12.002.00*2.002.00max-10.002.00+-20.001.00*2

입력에 대한 그래디언트를 계산하기 위한 backward pass에서 backpropagation이 어떻게 작동하는지에 대한 직관을 보여줍니다. Sum 게이트는 모든 입력에 그래디언트를 균등하게 분배합니다. Max 게이트는 더 높은 입력에 그래디언트를 라우팅(route)한다. Mul 게이트는 입력 활성화(input activations)를 구하고 swap하여 그래디언트와 곱합니다.

위의 그림을 통해 다음과 같은 것을 알 수 있습니다:

add 게이트는 항상 출력의 그래디언트을 (forward pass에서 어떤 값이 입력이 되었든간에) 모든 입력에 균등하게 분배합니다. add 연산의 로컬 그래디언트가 +1.0이기 때문에, 모든 입력의 그래디언트에 x1.0을 곱하게 되어서 출력의 그래디언트와 정확히 동일합니다. 위의 예에서 + 게이트는 두 입력 모두에 2의 그래디언트를 공평하고(equally) 동일하게(unchanged) 라우팅(route)합니다.

max 게이트는 그래디언트를 라우팅(route)합니다. 모든 입력에 동일한 그래디언트를 분배하는 add 게이트와는 달리, max 게이트는 (동일한) 그래디언트를 하나의 입력에만(forward pass 동안 가장 높은 값을 가진 입력) 분배합니다. max 게이트의 로컬 그래디언트는 가장 높은 값에만 1.0을 주고, 그 밖의 모든 값은 0.0이기 때문입니다. 위의 예에서 max 게이트는 w보다 값이 높은 z 변수에 2.00의 그래디언트를 라우팅하고, w의 그래디언트는 0으로 유지됩니다.

mul 게이트는 해석하기 조금 까다롭습니다. 로컬 그래디언트는 입력 값(스위치된 것을 제외하고)이며, 이는 chain rule이 적용되는 동안 그 출력의 그래디언트과 곱해집니다. 위의 예에서 x의 그래디언트는 -4.00 x 2.00 = -8.00입니다.

직관적이지 않은 효과와 그 결과들(Unintuitive effects and their consequences) mul 게이트의 입력 중 하나가 매우 작고 다른 하나는 매우 크다면, mul 게이트는 약간 직관적이지 않은 작업을 합니다: 작은 입력에 상대적으로 큰 그래디언트와 큰 입력에 작은 그래디언트를 부여합니다. linear classifier에서 weight가 입력과 내적된다면(곱해진다면) \(w^Tx_i\), 이는 데이터의 양(scale)이 weihgt의 그래디언트의 크기(magnitude)에 영향을 미친다는 것을 의미합니다. 가령, 전처리 중에 모든 입력 데이터 \(x_i\)에 1000을 곱하면, weight의 그래디언트는 1000배 더 커지며, 이를 보상(compensate)하기 위해 학습률(learning rate)을 그만큼 낮춰야 합니다. 이것이 바로 전처리가 중요한 이유입니다! 또한 그래디언트가 어떻게 흘러가는지(flow)에 대한 직관적인 이해는 디버깅하는 데 도움을 줄 수 있습니다.

5.8 벡터화 연산을 위한 그래디언트(Gradients for vectorized operations)

앞에서는 변수 하나에 관한 것이었지만, 모든 개념은 매트릭스와 벡터 연산으로 쉽게 확장됩니다. 여기서는 차원와 전치(transpose) 연산에 더 많은 주의를 기울여야 합니다.

5.8.1 행렬-행렬 곱셈 그래디언트(Matrix-Matrix multiply gradient)

아마도 가장 까다로운 연산은 행렬-행렬 곱셈(모든 행렬-벡터와 벡터-벡터 연산의 일반적인 형태인)일 것 입니다:

# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)

팁:차원 분석(dimension analysis)을 이용하라! dWdX의 표현은 기억할 필요는 없습니다. 이는 차원을 이용하여 다시 유도하기(re-derive) 쉽기 때문입니다. 가령, XdD의 행렬 곱셈으로 구해지는 weight의 그래디언트 dWW의 크기와 같아야 합니다. 행렬의 차원을 일치하게 만드는 방법은 한가지 뿐입니다. 예를 들어, X는 [10 x 3]의 크기를 가지고 dD는 [5 x 3]의 크기를 가질 때, dWW가 [5 x 10]의 크기를 가지려면, 위와 같이 dD.dot(X.T)을 사용해야만 합니다.

5.8.2 작고 명확한 데이터로 작업하기(Work with small, explicit examples)

처음에는 벡터화된 표현에 대한 그래디언트 업데이트를 유도하는 것이 어렵다고 생각할 수 있습니다. 종이에 벡터화된 예시의 그래디언트를 유도해보고, 그 패턴을 효과적이고 벡터화된 패턴으로 일반화해보기를 권장합니다.

Erik Learned-Miller가 작성한, 행렬/벡터 미분에 관련하여 도움이 될 만한 문서입니다.

다음 섹션에서는 신경망을 정의 할 것이고, 역전파를 통해 손실 함수에 따른 출력에 대한 그라디언트를 신경망의 연결을 따라 효율적으로 계산할 것이다. 즉, 이제 우리는 신경망을 훈련할 준비가 되었다. 이 수업에서 개념적으로 이해하기 가장 어려운 부분이 다음 섹션에 있다! 그리고 ConvNets으로 한 발짝 내딛을 것이다.

5.9 요약(Summary)

  • 그래디언트의 의미와, 회로 내에서 backward flow가 어떻게 흘러가는지, 최종 출력을 더 크게 만들기 위한 상호작용(회로의 어느 부분을 얼마만큼 더 증가하거나 감소해야 하는지)을 어떻게 하는지 알아보았습니다.
  • backpropagation의 실제 구현을 위한 단계별 연산(staged computation)을 알아보았습니다. 그리고 함수를 모듈로 분해하여 로컬 그래디언트를 구하기 쉽게 만든 후 chain rule을 적용하여 이를 chain합니다. 결정적으로, 종이에 표현식들을 쓰고 전부 다 미분하고 싶지 않을 것입니다. 입력 변수의 그래디언트를 위한 식이 필요하지 않기 때문입니다. 따라서 표현식들을 단계별로(여기서 단계는 행렬 벡터 곱, max연산, sum 연산 등이 됩니다) 분해하여 미분하고 변수 하나에 한번씩 backprop을 진행합니다.

다음 장에서는 Neural Networks를 정의합니다. 그리고 backpropagatin을 통해 (loss function에 관련이 있는) neural networks의 그래디언트를 효율적으로 계산할 수 있습니다. 다시 말해, 이제 Neural Nets를 훈련시킬 준비가 되었고, 개념적으로 가장 어려운 부분이 등장합니다! ConvNets가 가까워졌습니다.

5.9.1 참고(References)