4.1 개요(Introduction)

이전 장에서 이미지 분류 문제의 맥락에서 두 가지 중요한 요소를 다루었습니다:

  1. (parameterized) score function. 가령, linear function은 raw 이미지 픽셀을 클래스 점수로 mapping 합니다.
  2. loss function는 특정 파라미터셋의 quality를 유도된 점수와 트레이닝셋의 ground truth labels가 얼마만큼 일치(agrred with)하는지에 따라 수치화 합니다. 여기에는 Softmax/SVM 등 여러 버전이 있습니다.

구체적으로, 선형 함수는 \( f(x_i, W) = W x_i \) 형태를 가지고 있으며, SVM의 형태는 다음과 같습니다:

\[L = \frac{1}{N} \sum_i \sum_{j\neq y_i} \left[ \max(0, f(x_i; W)_j - f(x_i; W)_{y_i} + 1) \right] + \alpha R(W)\]

예제 \(x_i\)에 대한 예측이 ground truth label \(y_i\)와 일치하게 만든 파라미터 \(W\)의 설정 또한 매우 낮은 loss \(L\)를 가집니다. 이제 세 번째이자 마지막 핵심 요소인 최적화(Optimization)에 대해 알아보겠습니다. Optimization은 loss function을 최소화하는 파라미터셋 \(W\)을 찾는 과정입니다.

4.1.1 전조(Foreshadowing)

이 세 가지 핵심 요소들이 어떻게 상호작용하는지를 이해하게 되면, 첫 번째 요소(parameterized function mapping)를 다시 찾아가 linear mapping보다 훨씬 더 복잡한 함수로 확장시킬 것입니다: 우선 전체 Neural Networks, 그 후에 Convolutional Neural Networks로 확장됩니다. loss function과 optimization은 다시 건드리지 않고 유지됩니다.

4.2 손실함수 시각화(Visualizing the loss function)

이 장에서 살펴볼 loss function은 대개 고차원 공간(가령, CIFAR-10에서 linear classifier의 weight 행렬은 [10 x 3073] 크기이며 총 30,730개의 파라미터를 가집니다)에서 정의되어 있어 시각화하기 어렵습니다. 그러나 여전히 고차원 공간을 선(1차원) 또는 평면(2차원)으로 자르는 것으로도 한 가지 직관을 얻을 수 있습니다. 예를 들어, 무작위의 weight 행렬 \(W\)를 생성한 후(공간 내의 단일 점에 해당), 선(ray)를 따라가면서 loss function 값을 기록할 수 있습니다. 즉, 임의의 방향 \(W_1\)을 생성하고, 서로 다른 \(a\)의 값으로 \(L(W + a W_1)\)를 구하면 이 방향에 따른 loss를 계산할 수 있습니다. 이 과정은 \(a\)값을 x축으로 하고, loss function 값을 y축으로 하는 단순한 좌표(plot)를 구성합니다. 또한 \(a, b\)를 변화시키면서 loss \( L(W + a W_1 + b W_2) \)를 구하면 2차원 상의 동일한 절차를 수행할 수 있습니다. 좌표(plot)에서 \(a, b\)는 x축과 y축에 해당하고, loss function의 값은 다음과 같이 색상으로써 시각화됩니다:

svm

하나의 예시(왼쪽, 중간)에 대한 (regularization이 없는) Multiclass SVM의 loss function landscape과 CIFAR-10에서 100개의 예시(오른쪽)에 대한 loss function landscape를 보여줍니다. 왼쪽: a만 바꾸어가며 본 1차원 loss. 중간, 오른쪽: loss를 2차원 단면으로 자른 것. 파랑 = 낮은 loss. 빨강 = 높은 loss. loss function의 비선형(piecewise-linear) 구조에 주목합니다. 여러 예시를 거치면서 loss는 평균과 합쳐지기 때문에, 오른쪽의 그릇 모양은 여러 비선형(piece-wise linear)그릇(가운데에 그림과 같이)모양의 평균 형태가 됩니다.

수학적으로 loss function의 piecewise-linear 구조를 설명할 수 있습니다. 아래는 하나의 예시입니다:

\[L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + 1) \right]\]

위의 식으로부터 각 예시에 대한 data loss는 \(W\)의 linear functions를 합한 것(\(\max(0,-)\) 함수에 의해 0의 임계치(threshold)를 가집니다)임을 알 수 있습니다. 또한 \(W\)의 각 행 (\(w_j\))에는 앞에 양수가 있을 때도 있고(예시와 다른 클래스에 해당하는 경우), 음수(예시와 일치하는 클래스에 해당하는 경우)가 있을 때도 있습니다. 이를 보다 명확하게 하기 위해, 3개의 1차원 점과 3개의 클래스를 포함하는 단순한 데이터셋을 생각해봅시다. (regularization이 없는) full SVM loss는 다음과 같습니다:

\[\begin{align} L_0 = & \max(0, w_1^Tx_0 - w_0^Tx_0 + 1) + \max(0, w_2^Tx_0 - w_0^Tx_0 + 1) \\\\ L_1 = & \max(0, w_0^Tx_1 - w_1^Tx_1 + 1) + \max(0, w_2^Tx_1 - w_1^Tx_1 + 1) \\\\ L_2 = & \max(0, w_0^Tx_2 - w_2^Tx_2 + 1) + \max(0, w_1^Tx_2 - w_2^Tx_2 + 1) \\\\ L = & (L_0 + L_1 + L_2)/3 \end{align}\]

위의 예시들은 1차원이기 때문에, 데이터 \(x_i\)와 weight \(w_j\)는 숫자로 이해할 수 있습니다. 예를 들어, \(w_0\)를 살펴봅시다. 위 식에서 몇몇 항은 \(w_0\)의 linear function이며, 각각 0에서 clamp됩니다. 이를 아래와 같이 시각화할 수 있습니다:

svmbowl

data loss을 1차원으로 표현한 그림. x축은 하나의 weight이고 y축은 loss를 의미합니다. data loss는 여러 항의 합으로 표현되고, 각 항은 특정 weight와는 독립적입니다. 또한 linear function은 0에서 threshold를 가집니다. full SVM data loss는 이 모양을 30,730차원의 버전으로 확장한것과 같습니다.

이와는 별도로, 그릇 모양을 보고 SVM cost function이 아래로 볼록한 함수(convex function) 중 하나라고 생각했을 것입니다.. 이러한 종류의 함수를 최소화하는 효율적인 방법을 찾는데 기여한 문헌들이 많으며, 스탠포드 대학의 강의 중에도 convex optimization이 있습니다. score function \(f\)를 Neural Networks의 영역으로 확장시키게 되면, objective function은 non-convex가 되며 시각화는 그릇 모양이 아닌 복잡하고 울퉁불퉁한 형태를 보일것 입니다.

미분 불가능한(Non-differentiable) loss functions 기술적인 참고사항으로, (max 연산으로 인해 생기는) loss function의 꺽임(kinks)은 loss function을 미분 불가능(non-differentiable)하게 만듭니다. 이러한 kinks에는 기울기(gradient)가 정의되지 않기 때문입니다. 그러나, subgradient는 여전히 존재하며, 일반적으로 이를 대체재로 사용합니다. 여기서는 맥락상 subgradientgradient를 같은 의미로 사용합니다.

4.3 최적화(Optimization)

loss function은 어떤 특정한 weight W의 품질(quality)을 정량화(quantify)할 수 있게 해줍니다. 최적화의 목표는 loss function을 최소화하는 W를 찾는 것입니다. 이제 loss function을 최적화하기 위한 접근방식을 서서히 발전시키고 동기부여를 할 것입니다. 결국 목표는 Neural Networks 최적화이며, 여기에 Convex Optimization 문헌에서 사용되는 도구들을 적용하기가 어렵다는 것입니다.

주어진 파라미터 W가 얼마나 좋은지를 확인하는 일은 매우 단순하기 때문에, 가장 먼저 떠오르는(매우 나쁜) 아이디어는 random weight를 시도해보고 어떤것이 가장 잘 동작하는지를 확인하는 것입니다. 절차는 다음과 같습니다:

# assume X_train is the data where each column is an example (e.g. 3073 x 50,000)
# assume Y_train are the labels (e.g. 1D array of 50,000)
# assume the function L evaluates the loss function

bestloss = float("inf") # Python assigns the highest possible float value
for num in xrange(1000):
  W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
  loss = L(X_train, Y_train, W) # get the loss over the entire training set
  if loss < bestloss: # keep track of the best solution
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# prints:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)

상단의 코드에서 여러 random weight 벡터 W를 시도해보는것을 볼 수 있습니다. 그리고 어떤 값은 다른 값보다 잘 동작하는것을 알 수 있습니다. 이 search를 통해 최적의 weight W를 찾아 테스트셋에 대해 시도해봅니다:

# Assume X_test is [3073 x 10000], Y_test [10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# find the index with max score in each column (the predicted class)
Yte_predict = np.argmax(scores, axis = 0)
# and calculate accuracy (fraction of predictions that are correct)
np.mean(Yte_predict == Yte)
# returns 0.1555

최적의 W을 가지고도 정확도는 15.5%에 그칩니다. 무작위로 클래스를 유추할 때의 결과인 10%보다는 나은걸 보니, 무식한 random search치고 나쁘지 않은 결과인듯 합니다!

핵심 아이디어: 반복적인 정제(Core idea: iterative refinement) 당연히 이를 더 개선할 여지는 있습니다. 가장 최적의 weight W를 찾는 일은 매우 어렵거나 거의 불가능에 가까습니다(특히 W가 복잡한 neural networks 전체의 weight이기 때문에). 그러나 특정 weight W를 정제하는 문제의 경우 조금 덜 어려운 문제가 됩니다. 다시 말해, 무작위의 W부터 시작해서 반복적으로 이를 정제함으로써 매 반복마다 조금 더 나은 값을 가지도록 하는 것입니다.

무작위의 weight부터 시작해서 반복적으로 정제하여 더 작은 loss를 가지게끔 하는 전략입니다.

안대를 낀 등산가(Blindfolded hiker analogy) 앞으로 나아가는데 도움이 될 수 있는 한 가지 좋은 비유는 당신이 안대를 한 채 험준한 지형을 하이킹하며 바닥에 내려가려 한다고 생각하는 것입니다. CIFAR-10의 예에서 W의 차원은 10 x 3073이므로, 언덕은 30,730 차원이 됩니다. 언덕 위의 모든 지점에서 특정한 loss(지형의 높이)를 얻습니다.

먼저 무작위의 방향을 선택한 후 발을 뻗어보고, 그 방향이 아래쪽으로 향할 때만 발자국을 내딛는 전략입니다. 구체적으로는, 무작위의 \(W\)로 시작해서, 그 위치에서 무작위의 작은 변화(perturbation) \( \delta W \)를 발생시키고, 만약 변화한 지점 \(W + \delta W\)에서의 loss가 더 낮으면 업데이트를 진행합니다. 이 절차에 대한 코드는 다음과 같습니다:

W = np.random.randn(10, 3073) * 0.001 # generate random starting W
bestloss = float("inf")
for i in xrange(1000):
  step_size = 0.0001
  Wtry = W + np.random.randn(10, 3073) * step_size
  loss = L(Xtr_cols, Ytr, Wtry)
  if loss < bestloss:
    W = Wtry
    bestloss = loss
  print 'iter %d loss is %f' % (i, bestloss)

loss function을 구할 때 이전과 같은 숫자(1000)를 사용하였고, 테스트셋 분류 정확도 21.4%를 달성했습니다. 전보다는 더 나은 방법이긴 하지만, 여전히 연산적으로 낭비되고 값비쌉니다.

4.3.3 전략 #3: 기울기를 따라갑니다(Following the Gradient)

앞에서 weight 벡터를 개선하는(그리고 더 낮은 loss를 가져오는) weight-공간에서의 방향을 찾고자 했습니다. 좋은 방향을 찾기 위해 random search를 할 필요는 없다는 것을 알았습니다: 어느 방향으로 weight 벡터를 변화시키는 것이 수학적으로 가장 가파른 하강 방향인지를 알 수 있기 때문에 최선의(best) 방향을 계산하는것이 가능합니다(step size가 0의 한계에 다다르기 전까지는). 이 방향은 loss function의 기울기(gradient)와 관련이 있습니다. 이 접근은 우리 발 아래 언덕의 경사를 느끼고 가장 가파르게 느껴지는 방향으로 내려가는 하이킹 비유와 유사합니다.

1차원 함수에서 기울기(slope)는 한 지점에서 함수의 순간(instantaneous) 변화율입니다. 그래디언트는 숫자 하나에 대해서가 아닌 숫자들의 벡터를 가지는 함수에 대한 기울기의 일반화입니다. 또한 그래디언트는 입력 공간에서 각 차원에 대한 기울기 벡터(일반적으로는 미분(derivatives)이라고 합니다)를 의미합니다. 입력과 관련된 1차원 함수의 미분에 대한 수학적 표현은 다음과 같습니다:

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

함수가 숫자 하나가 아닌 숫자들의 벡터를 가질 때, 미분(derivative)을 편미분(partial derivatives)이라고 부릅니다. 또한 그래디언트는 단순히 각 차원에 대한 편미분의 벡터입니다.

4.4 그래디언트 연산(Computing the gradient)

그래디언트를 계산하는 두 가지 방법이 있습니다: 느리고 정확하진 않지만 쉬운 방법(numerical gradient)과 빠르고 정확하지만 미적분(calculus)이 필요하며 오류가 발생하기 쉬운(error-prone) 방법(analytic gradient). 이 둘을 살펴보겠습니다.

4.4.1 Computing the gradient numerically with finite differences

위에서 주어진 수식을 통해 그래디언트를 수치적으로 계산할 수 있습니다. 아래는 함수 f와 벡터 x를 가지고 x에서 f의 그래디언트를 구하는 generic function을 보여줍니다:

def eval_numerical_gradient(f, x):
  """ 
  a naive implementation of numerical gradient of f at x 
  - f should be a function that takes a single argument
  - x is the point (numpy array) to evaluate the gradient at
  """ 

  fx = f(x) # evaluate function value at original point
  grad = np.zeros(x.shape)
  h = 0.00001

  # iterate over all indexes in x
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # evaluate function at x+h
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # increment by h
    fxh = f(x) # evalute f(x + h)
    x[ix] = old_value # restore to previous value (very important!)

    # compute the partial derivative
    grad[ix] = (fxh - fx) / h # the slope
    it.iternext() # step to next dimension

  return grad

주어진 그래디언트 공식에 따라, 위의 코드는 모든 차원을 일일이 반복하고 그 차원에 대해 작은 편차 h를 만들며 그 차원에서 함수가 얼마나 많이 바뀌었는지를 관찰함으로써 loss function의 편미분을 계산합니다. 결국 변수 grad는 전체 그래디언트와 같습니다.

4.4.2 현실적 고려(Practical considerations)

수학적으로 그래디언트는 h 가 0의 극한일 때 정의 되지만, 실전에서는 매우 작은 값(예시와 같이 1e-5)을 사용하는 것으로 충분 합니다. 이상적으로는 연산상의 문제(numerical issues)가 일어나지 않는 선에서 가장 작은 step size를 하는것이 좋습니다. 또한 centered difference formula: \( [f(x+h) - f(x-h)] / 2 h \)을 사용하여 numeric gradient를 계산하는 것이 더 효과적일 때도 있습니다. 자세한 내용은 wiki를 참고합니다.

위에서 주어진 함수를 사용하면 모든 점과 함수에서 그래디언트를 계산할 수 있습니다. 아래는 weight space 내의 임의의 점에서 CIFAR-10 loss function에 대한 그래디언트를 계산합니다:


# to use the generic code above we want a function that takes a single argument
# (the weights in our case) so we close over X_train and Y_train
def CIFAR10_loss_fun(W):
  return L(X_train, Y_train, W)

W = np.random.rand(10, 3073) * 0.001 # random weight vector
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # get the gradient

그래디언트는 모든 차원에 대한 loss function의 기울기를 알려주며, 이는 업데이트에 사용될 수 있습니다:

loss_original = CIFAR10_loss_fun(W) # the original loss
print 'original loss: %f' % (loss_original, )

# lets see the effect of multiple step sizes
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
  step_size = 10 ** step_size_log
  W_new = W - step_size * df # new position in the weight space
  loss_new = CIFAR10_loss_fun(W_new)
  print 'for step size %f new loss: %f' % (step_size, loss_new)

# prints:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036

4.4.3 음의 gradeint 방향으로의 업데이트(Update in negative gradient direction)

상단의 코드에서 W_new를 계산할 때, 그래디언트 df의 마이너스 방향으로 업데이트하는 것을 볼 수 있습니다. 이는 loss function이 증가하는것이 아닌 감소하길 원하기 때문입니다.

4.4.4 step size의 효과(Effect of step size)

그래디언트는 함수가 가장 급격히 증가하는 지점에 대한 방향을 알려주지만, 이 방향을 따라 얼마만큼 발자국을 내딛어야 할지(step)는 알려주지 않습니다. 이후에 알 수 있겠지만, step size(또는 학습률(learning rate))를 결정하는 것은 neural networks의 학습에 있어 가장 중요하면서도 골치 아픈 하이퍼 파라미터 설정 중 하나가 될 것입니다. 안대를 낀 채 언덕을 내려가는 비유에서, 우리 발 아래의 언덕이 어떤 방향으로 기울어졌는지 느낄 수 있지만, 얼마만큼 발을 뻗어야 할지는 불확실합니다. 만약 발을 신중히 내딛으면 아주 작지만 일관된 전진을 할 수 있습니다(이는 작은 step size와 같습니다). 반면, 빠르게 내려가기 위해 큰 step size을 선택할 수도 있지만 딱히 성과가 없을 수도 있습니다. 상단의 코드에서 볼 수 있듯이, 어떤 지점에서 큰 step size를 선택하면 오히려 loss가 증가하는 “overstep”이 발생할 수도 있습니다.

stepsize

step size의 효과를 시각화한 것입니다. 특정 지점 W에서 시작해서 loss function의 가장 급격한 감소 방향을 나타내는 그래디언트(혹은 흰색화살표가 나타내는 음의 그래디언트)를 구합니다. 작은 step은 느리지만 일관성있는 전진으로 이어지고, 큰 step은 위험하지만 더 빠르게 전진할 수 있습니다. 큰 step size의 경우 overshoot로 인해 loss가 더 나빠질 수도 있습니다. step size(또는 learning rate)는 신중히 조정해야하는 가장 중요한 하이퍼파라미터 중 하나입니다.

4.4.5 효율성의 문제(A problem of efficiency)

numerical gradient를 구하는 과정에서 파라미터의 수는 선형적 복잡도(complexity linear)를 가집니다. 예시에서 파라미터의 개수는 총 30,730개 이며, 이로 인해 그래디언트를 계산하고 한 번의 파라미터 업데이트를 하기 위해서는 30,731번의 loss function 계산을 해야합니다. 최신의 neural networks로 올수록 파라미터의 수가 수천만을 훌쩍 넘기 때문에 이 문제는 더욱 심각해집니다. 분명히 이 전략은 확장성(scalable)이 없고 더 나은 무언가가 필요합니다.

4.4.6 Computing the gradient analytically with Calculus

numerical gradient는 finite difference approximation을 이용해서 계산이 매우 간단하지만, 근사치(진짜 그래디언트는 h가 0의 극한으로 정의되지만, 여기서는 매우 작은 h를 사용하기 때문에)이며, 연산량이 매우 큽니다. 그래디언트를 계산하는 두 번째 방법은 미적분을 분석적으로(analytically) 사용하는 것입니다. 이를 이용하면 연산속도도 매우 빠르며 그래디언트에 대한 (근사치가 아닌) 정확한 공식을 유도할 수 있습니다. 그러나 numerical gradient와는 달리 구현하는 과정에서 쉽게 오류가 날 수 있습니다. 따라서 실전에서는 anlaytic gradient를 계산한 후 이를 numerical gradient와 비교하여 구현이 정확한지를 확인하는 것이 일반적입니다. 이를 gradient check라고 부릅니다.

아래에서는 하나의 datapoint에 대해 SVM loss function을 적용하였습니다:

\[L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + \Delta) \right]\]

함수를 weight에 관해서 미분할 수 있습니다. 가령, \(w_{y_i}\)에 대한 그래디언트는 다음과 같습니다:

\[\nabla_{w_{y_i}} L_i = - \left( \sum_{j\neq y_i} \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) \right) x_i\]

위에서 \(\mathbb{1}\)은 indicator function으로 내부의 조건이 참이면 1, 거짓이면 0이 됩니다. 수식으로 보면 무시무시하게 보일 수 있지만, 코드 상에서는 단순히 margin을 충족하지 못하는(따라서 loss function에 기여하는) 클래스의 개수 만큼의 데이터 벡터 \(x_i\)를 그래디언트로 두면 됩니다. 이는 오로지 올바른 클래스에 해당하는 \(W\)의 행에 대한 그래디언트입니다. \(j \neq y_i \)일 때, 다른 행에 대한 그래디언트는 다음과 같습니다:

\[\nabla_{w_j} L_i = \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) x_i\]

일단 그래디언트에 대한 식을 구해두면, 식을 적용하거나 그래디언트 업데이트를 할 때 사용하기 수월합니다.

4.5 경사하강법(Gradient Descent)

이제 loss function의 그래디언트를 계산할 수 있습니다. 그래디언트를 반복적으로 계산하고 파라미터 업데이트를 수행하는 절차를 통틀어 경사하강법(Gradient Descent)이라고 합니다. 그 기본(vanilla) 버전은 다음과 같습니다:

# Vanilla Gradient Descent

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # perform parameter update

이 간단한 반복문은 모든 Neural Network 라이브러리의 핵심입니다. 최적화를 수행하는 다른 방법(가령, LBFGS)도 있지만, Gradient Descent는 현재까지 Neural Network의 loss function을 최적화하는 가장 보편적이고 정립된 방법입니다. 이후에도 이 반복문의 세부사항(업데이트 방정식의 세부적인 내용)에 대한 몇 가지 부가 설명이 있겠지만, 만족할만한 결과를 얻을때 까지 그래디언트를 계속 쫓아간다는 핵심 아이디어는 동일할 것입니다.

4.5.1 미니배치 경사하강법(Mini-batch gradient descent()

ILSVRC challenge와 같이 큰 규모의 대회에서는 수백만 개의 트레이닝데이터가 있습니다. 따라서 한 번의 파라미터 업데이트를 위해 전체 트레이닝셋에서 full loss function을 계산하는 것은 낭비입니다. 이 문제에 대한 가장 보편적인 해결책은 트레이닝 데이터에서 배치(batches)를 뽑아 그래디언트를 계산하는 것입니다. 가령, 최신의 ConvNets에서 하나의 batch는 120만 개의 전체 트레이닝 셋 중에서 256개 정도로 구성됩니다. 그리고 이 batch는 파라미터 업데이트에 사용됩니다:

# Vanilla Minibatch Gradient Descent

while True:
  data_batch = sample_training_data(data, 256) # sample 256 examples
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # perform parameter update

이 방법이 잘 작동하는 이유는 트레이닝 데이터의 각 데이터들이 상호연관성(correlated)이 있기 때문입니다. 극단적으로, ILSVRC의 120만 장의 모든 이미지가 사실은 1000장의 원본(unique) 이미지의 사본(duplicates)으로 구성되어 있다고 생각해봅시다(각 클래스당 한 장의 원본만을 가지는 경우, 다시 말해 이미지당 1200장의 동일한 복사본으로 구성된 경우). 그러면 1200장의 동일한 복사본에 대한 그래디언트의 계산 결과는 모두 동일할 것이고, 120만 장의 이미지에 대한 data loss의 평균은 1000장의 원본에서 구할 때와 정확히 동일한 값을 가질 것입니다. 물론 실전에서는 데이터셋에 사본 이미지를 포함하지 않으며, mini-batch의 그래디언트는 full objective의 그래디언트에 대한 좋은 근사치로 사용됩니다. 따라서 실전에서는 더 자주 파라미터를 업데이트하기 위헤 mini-batch gradient를 사용하며 이는 더 빠른 수렴(convergence)으로 이어집니다.

극단적인 경우 mini-batch가 하나의 데이터로만 구성될 수도 있습니다. 이를 확률적 경사하강법(Stochastic Gradient Descent)(SGD) (가끔은 on-line gradient descent)이라고 합니다. 이는 실전에서 자주 등장하지는 않습니다. 왜냐하면 vectorized code optimization으로 인해 데이터 1개에 대해 그래디언트를 100번 계산하는것 보다 데이터 100개에 대해 그래디언트를 1번 계산하는것이 연산적으로 훨씬 효율적이기 때문입니다. 기술적으로는 그래디언트를 구하기 위해 한 번에 데이터 하나를 사용하는 것을 SGD라고 하지만, 대개의 경우 SGD에서 mini-batch를 사용한다고 가정하고 mini-batch gradient descent를 SGD라고 부르기도 합니다(“Minibatch Gradient Descent”를 MGD라고 하거나 “Batch gradient descent”를 BGD라고 하는 것은 거의 보지 못했을 것입니다). mini-batch의 크기는 하이퍼파라미터이지만, cross-validate하는 것이 보편적이지는 않습니다. 보통 메모리의 제약이 있는 경우 메모리의 크기에 맞추거나, 32, 64 또는 128과 같이 2의 거듭제곱으로 설정합니다. 왜냐하면 많은 vectorized operation implementations는 입력이 2의 제곱의 크기일 때 더 빠르게 동작하기 때문입니다.

4.6 요약(Summary)

dataflow

전체적인 흐름(information flow)을 보여줍니다. 여러쌍의 고정된 데이터셋 (x,y)가 주어지고, 임의의 숫자로 초기화된 weight는 업데이트를 통해 점점 바뀝니다. forward pass에서 score function은 클래스 점수를 계산하고 벡터 f에 저장합니다. loss function은 두 가지 요소로 구성됩니다: data loss는 점수 f 와 라벨 y 간의 적합도(compatibility)를 계산하고, regularization loss는 weight만을 입력으로 하는 함수입니다. Gradient Descent를 통해 weight의 그래디언트를 계산할 수 있고(원한다면 데이터의 그래디언트도 계산할 수 있습니다), 이 그래디언트를 이용하여 파라미터를 업데이트할 수 있습니다.

이번 장에서는,

  • high-dimensional optimization landscape에서 바닥(bottom)에 도달하려는 시도를 통해 loss function에 대한 직관을 길렀습니다. 최적화 과정을 안대 찬 등산가가 바닥을 찾아가려는 것에 비유하여 이해하였습니다. 특히, SVM cost function이 비선형(piece-wise linear)이며 그릇 형태임을 보았습니다.
  • 반복적인 정제(iterative refinement)를 통해 loss function을 최적화하는 아이디어를 살펴보았습니다. 이는 weight를 임의의 값으로 초기화한 후 loss가 최소화될 때 까지 차근차근 정제하는 것입니다.
  • 함수의 그래디언트(gradient)는 가장 급격히 상승하는 방향을 알려줍니다. finite difference approximation(finite difference는 numerical gradient의 연산에서 사용되는 h를 의미)을 이용하여 수치적으로는 간단하지만 비효율적인 방법으로 gradient를 계산하였습니다.
  • 파라미터 업데이트는 step size (또는 learning rate)의 까다로운 설정이 필요합니다: 너무 낮으면 꾸준히 전진하기는 하지만 느립니다. 너무 크면 전진 속도는 빨라지지만, 더 위험해집니다. 이후에 이 적절한 tradeoff에 대해 더 자세히 다룰것 입니다.
  • numerical gradientanalytic gradient를 연산하는 것의 tradeoff를 다루었습니다. numerical gradient는 간단하지만 근사치를 계산하며, 계산 비용이 많이 듭니다. analytic gradient는 정확하고 빠른 계산이 가능하지만, 수학을 사용하여 그래디언트를 유도하기 때문에 오류가 발생하기 쉽습니다. 따라서 실전에서는 항상 analytic gradient를 사용한 후 그 결과를 numerical gradient와 비교하는 gradient check를 수행합니다.
  • 반복문(loop) 안에서 반복적으로 그래디언트를 계산하고 파라미터 업데이트를 수행하는 Gradient Descent 알고리즘을 알아 보았습니다.

4.6.1 Coming up

이번 장을 통해, neural networks를 설계, 학습, 이해하는데 필요한 (weight에 대해) loss function의 그라디언트를 계산하는 능력, 그리고 이를 직관적으로 이해하는 능력을 기를 수 있었습니다. 다음 장에서는 chain rule 또는 backpropagation이라고 불리는 것을 이용하여 그래디언트를 분석적으로(analytically) 계산해볼 것입니다. 이는 Convolutional Neural Networks를 포함한 모든 종류의 Neural Networks를 표현하는 임의의 loss function을 효율적으로 최적화할 수 있게 도와줍니다.