8.1 학습(Learning)

이전 장에서는 네트워크의 연결(connectivity), 데이터, loss function을 어떻게 설정할 것인지 등 Neural Network의 정적인(static) 부분에 대해 알아보았습니다. 이번 장에서는 역학(dynamics)에 관한 것입니다. 즉, 파라미터를 학습하고 좋은 하이퍼파라미터를 찾는 과정을 알아보겠습니다.

8.1.1 그래디언트 체크(Gradient Checks)

이론상으로, 그래디언트 체크(gradient check)는 단순히 분석적(analytic) 그래디언트와 수치적(numerical) 그래디언트를 비교하는 것을 말합니다. 하지만 실전에서 과정은 더욱 복잡하고(involved) 오차가 발생하기 쉽습니다(error prone). 이에 관한 팁, 트릭, 이슈에 대해 다루어보겠습니다:

중심 차분 공식을 사용하라(Use the centered formula) 수치적 그래디언트(numerical gradient)를 계산할 때 보았던 유한 차분 근사(finite difference approximation)에 대한 공식은 다음과 같습니다:

\[\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} \hspace{0.1in} \text{(bad, do not use)}\]

여기서 \(h\)는 매우 작은 수이며, 실전에서는 대략 1e-5 정도로 사용합니다. 실전에서, 중심(centered) 차분 공식을 이용하는 것이 더 좋다고 알려져있습니다:

\[\frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} \hspace{0.1in} \text{(use instead)}\]

이는 그래디언트의 모든 차원을 하나씩 체크하기 위해, loss function을 두 번 계산(고로 2배의 연산량을 요구)해야 하지만, 그래디언트 근사(gradient approximation)가 더 정확(precise)합니다. 이를 확인하기 위해, \(f(x+h)\)와 \(f(x-h)\)의 테일러 전개식(Taylor expansion)을 사용하면, 첫 번째 공식이 \(O(h)\만큼의 오차(error)를 가지는 반면, 두 번째 공식은 \(O(h^2)\)만큼의 오차만을 가지는 것을 증명할 수 있습니다. 이는 이계도 근사(second order approximation)이기 때문입니다.

비교를 할 때 상대적 오차를 사용하라(Use relative error for the comparison) 수치적 그래디언트(numerical gradient) \(f’_n\)과 분석적 그래디언트(analytic gradient) \(f’_a\)를 비교하는 기준이 무엇일까요? 즉, 일치하는지(compatible) 또는 일치하지 않은지 어떻게 알까요? 차이(difference) \(\mid f’_a - f’_n \mid \) 혹은 그 제곱값을 계속 확인하고, 그 차이가 임계값(threshold)을 넘어선다면 그래디언트 체크(gradient check)가 실패한것으로 정의내리고 싶을지도 모릅니다. 하지만, 이는 문제가 있습니다. 예를 들어, 차이가 1e-4인 경우를 생각해봅시다. 이는 두 그래디언트가 약 1.0일 때 매우 적절한 차이로 보입니다. 그래서 두 그래디언트가 동일하다고(match) 간주할 수 있습니다. 그러나 두 그래디언트가 1e-5 또는 그보다 작다면, 1e-4는 어마어마한 차이이고 실패로 간주할 것입니다. 따라서, 모든 경우에 다음과 같은 상대적 오차(relative error)로 계산하는 것이 바람직합니다:

\[\frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)}\]

이는 두 그래디언트의 절대값과 그 차이에 대한 비율을 계산합니다. 일반적으로 상대적 오차(relative error) 공식은 두 항 중 하나만 포함하지만, max(또는 add)를 사용하면 대칭성(symmetric)을 가지며 두 항 중 하나가 0인 경우(특히, ReLU에서 종종 발생할 수 있는 경우) 0으로 나누는 것을 방지할 수 있습니다. 단, 둘 다 0인 경우를 확실히 확인해야 하며, 이러한 극단적인 경우에 그래디언트 체크를 통과(pass)할 수 있어야 합니다. 실전에서:

  • 상대적 오차(relative error) > 1e-2, 대부분 그래디언트에 문제가 있는 경우입니다.
  • 1e-2 > relative error > 1e-4, 만족하기에는 애매한 경우입니다.
  • 1e-4 > relative error, 꺾임(kink)이 있는 목적함수(objective)의 경우 대부분 괜찮습니다. 그러나 만약 kink가 없다면(tanh 또는 softmax를 사용한 경우) 1e-4는 너무 높은 값입니다.
  • 1e-7 또는 이보다 작은 경우, 당신은 기뻐해야할 것입니다

네트워크가 깊어질수록, 상대적 오차(relative error)는 더 커질 것입니다. 따라서 10 레이어 네트워크의 입력 데이터에 대해 그래디언트 체크를 하는 경우, 오차는 레이어를 거치면서 누적되므로 1e-2 정도의 상대적 오차는 괜찮습니다. 반대로, 단일 미분 함수에서 1e-2의 오차는 그래디언트가 부정확함을 의미할것입니다.

2배 정밀도(double precision)를 사용하라 흔히 빠지기 쉬운 함정(common pitfall)은 그래디언트 체크를 할 때 단일 정밀도(single precision)의 부동 소수점(floating point)을 사용한다는 것입니다. 이는 그래디언트를 올바르게 구현했음에도 1e-2와 같이 높은 상대적 오차를 가질 때 종종하는 실수입니다. 간혹 2배 정밀도(double precision)을 사용하면 상대적 오차가 1e-2에서 1e-8까지 떨어지는 경우도 있습니다.

부동 소수점의 변동 범위에 머물러라(Stick around active range of floating point) “What Every Computer Scientist Should Know About Floating-Point Arithmetic”을 한 번 읽어보기를 권장합니다. 이는 오차에 대해 명쾌하게 설명하며, 코드를 더 신중하게 작성할 수 있도록 도와줍니다. 가령, neural nets에서는 일반적으로 배치(batch) 단위로 loss function을 정규화합니다. 하지만, 데이터포인트(datapoint)당 그래디언트가 매우 작다고 해서, 이를 추가적으로(addtionally) 데이터 포인트의 개수로 나누게 되면 매우 작은 숫자를 주기 시작할 것이고, 결국 이는 더 많은 수치적 문제를 발생시킬 것입니다. 이것이 수치적/분석적 그래디언트를 항상 출력(print)해보는 것을 권장하는 이유입니다(대략 절댓값이 1e-10보다 작은 경우에 주의해야 합니다). 만약 그렇다면, loss function을 일시적으로 상수(constant)배 만큼 확장(scale up)하여, 부동소수점(floats)이 더 밀집된 “더 좋은(nicer)” 범위로 가져오기를 원할 것입니다 - 이상적으로는 1.0에 가깝게, 여기서 부동소수점의 지수(float exponent)는 0입니다.

목적함수에서의 꺾임(Kinks in the objective) 그래디언트 체크를 할 때 알고 있어야 할 부정확한 이유 중 하나는 꺾임(kinks) 문제 때문에 발생합니다. kinks는 목적함수(objective function)의 미분 불가능한(non-differentiable) 영역을 의미하며, ReLU (\(max(0,x)\)), SVM loss, Maxout 뉴런 등에서 다루었습니다. 가령, ReLU 함수의 \(x = -1e6\)에서 그래디언트 체크를 할 때를 생각해봅시다. \(x < 0\)이면, 이 지점의 분석적 그래디언트(analytic gradient)는 정확히 0입니다. 그러나, 수치적 그래디언트(numerical gradient)에서는 \(f(x+h)\)가 kink를 건너뛰고(cross over)(\(h > 1e-6\)일 경우) 0이 아닌 결과(non-zero contribution)를 가져오기 때문에, 최종적으로 0이 아닌 그래디언트를 계산해냅니다. 이를 극단적인 경우(pathological case)라고 생각할 수도 있지만, 사실 이는 매우 흔한 일입니다. 예를 들어, CIFAR-10에 대한 SVM은 450,000개 의 \(max(0,x)\) 항을 가지고 있는데, 이는 50,000개의 예시와 각각의 예시가 목적함수(objective)에 대해 9개의 항을 만들기 때문입니다. 더군다나, SVM 분류기를 사용하는 Neural Netwrok는 ReLU에 의해 더 많은 kink를 가지게 됩니다.

만약 loss를 계산할 때 kink를 건너 뛰었다면(crossed) 이를 확인할 수 있습니다. 모든 \(max(x,y)\) 형태의 함수에서 “승자(winners)”가 누구인지를 계속 확인하면 됩니다; 즉, forward pass에서 x와 y 중에 무엇이 더 큰지 확인하는 것입니다. 만약 \(f(x+h)\)와 \(f(x-h)\)를 계산할 때, 적어도 하나의 승자(winne)가 바뀌면, kink를 건너 뛰었다는(crossed) 뜻이고 수치적 그래디언트는 정확하지 않을 것입니다.

적은 수의 데이터포인트만 사용하라(Use only few datapoints) 위의 kink 문제에 대한 한 가지 해결책은 더 적은 데이터포인트(datapoints)를 사용하는 것입니다. loss function이 kink를 포함하는 경우(예를 들어, ReLU 또는 margin losses 등을 사용할 때), 더 적은 데이터포인트를 사용하면 더 적은 kink를 가지기 때문에, 유한 차분 근사(infinite difference approximation)를 수행할 때 이를 건너뛸(cross) 가능성이 낮아지기 때문입니다. 게다가, 만약 2~3개 정도의 데이터포인트에 대해서만 그래디언트 체크(gradcheck)를 하더라도, 거의 확실히 전체 배치에 대해 그래디언트 체크(gradcheck)를 한 효과를 볼 수 있을것입니다. 또한 데이터포인트가 아주 적다면 그래디언트 체크를 더 빠르고 효율적으로 할 수 있습니다.

스텝 사이즈 h를 조심하라(Be careful with the step size h) \(h\)가 작다고 해서 꼭 좋은것만은 아닙니다. \(h\)가 작을수록 수치상의 정확도 문제들을 맞닦뜨릴 것입니다. 간혹 그래디언트가 잘 맞지 않는 경우, \(h\)를 1e-4 또는 1e-6으로 바꾸면 갑자기 그래디언트가 정확해지기도 합니다. wikipedia article에서 h의 값을 x축으로 두고, 수치적 그래디언트(numerical gradient)를 y축으로 하는 그래프를 확인할 수 있습니다.

그라디언트 검사가 파라미터 공간에서 특정 (일반적으로 무작위) 단일 지점에서 수행된다는 것을 인식하는 것이 중요하다. 그 자점에서 그래디언트 검사가 성공하더라도 그래디언트가 전역 적으로 올바르게 구현되었는지 확실하지 않다. 또한 무작위 초기화는 파라미터의 공간의 가장 “특징적인”점이 아닐 수 있으며, 실제로 그라디언트가 올바르게 구현 된 것으로 보이는 특정상황이 나오는 것 처럼 보이기도 하지만 그렇지 않는 경우도 있다. 예를 들어, 매우 작은 값으로 웨이트 초기화를 하는 경우, SVM은 모든 데이터 포인트에 거의 0을 할당 할 것이고, 그라디언트는 모든 데이터 포인트에서 특정 패턴을 나타낼 것이다. 그래디언트의 잘못된 구현은 여전히 이 패턴을 보이게 할 수 있다. 가장 안전한 방법은 번-인 (burn-in) 시간(로스가 감소하기 시작함)에 그라이던트 검사를 수행하는 것이다. 첫 번째 반복문에서 그라디언트 검사를 수행하는 경우 그라디언트의 잘못된 구현이 드러나지 않을 수도 있다.

연산의 “특징적인” 모드에서 그래디언트 체크를 수행하라(Gradcheck during a “characteristic” mode of operation) 그래디언트 체크는 파라미터 공간에 있는 (일반적으로 임의의) 한 지점에서 수행됩니다. 그러므로 그 지점에서 그래디언트 체크에 성공한다고 해도, 그 그래디언트가 전 구간에서(globally) 정확하게 구현되었지는 확신할 수 없습니다. 또한 무작위 초기화(random initialization)를 통해 얻은 지점이 파라미터 공간에서 가장 “특징적인(characterisitc)” 지점이 아닐 수 있으며, 그래디언트가 정확히 구현된 것처럼 보이지만 실제로는 그렇지 않은 병적인(pathological) 상황에 처할 수도 있습니다. 가령, 매우 작은 weight로 초기화하는 경우, SVM은 모든 데이터포인트에 대해 거의 0에 가까운 점수를 부여할것이고, 그래디언트는 모든 데이터포인트에서 일정한 패턴을 보일것입니다. 그래디언트를 부정확하게 구현한 경우에도 이러한 패턴을 보이며, 연산의 특징적인 모드(characteristic mode)(점수의 분포가 들락날락하는 것)로 일반화되지 않을 수 있습니다. 따라서, loss가 내려하기 시작하는 시점에 잠깐 동안 버닝(burn-in) 타임을 사용하여 네트워크가 학습하고 그래디언트 체크를 수행할 수 있게하는 것이 안전합니다. 첫 번째 반복문(iteration)에서 이를 수행하면 병적이고(pathological) 극단적인 경우를 가져올 수 있고, 그래디언트의 부정확한 구현이 드러나지 않을 수 있다는(mask) 위험이 있습니다.

regularization이 data를 잡아먹게 두지마라(Don’t let the regularization overwhelm the data) 일반적으로 loss function은 data loss와 regularization loss(weight에 대한 L2 패널티)의 합으로 표현됩니다. 여기서 한 가지 주의해야 할 것은 regularization loss가 data loss보다 훨씬 클 수(overwhelm) 있다는 것입니다. 이렇게 되면 그래디언트의 대부분이 regularization term(보통 훨씬 간단한 그래디언트 식을 가짐)에서 비롯되고, data loss 그래디언트가 부정확하게 구현되어도 드러나지 않을 수도(mask) 있습니다. 따라서, 우선 regularization을 해제(turn off)한 채로 data loss 하나만 체크하고, 그 다음에 regularization term을 따로 확인하는 것이 좋습니다. 후자를 수행하는 한 가지 방법은 코드를 해킹(hack)하여 data loss의 영향(contribution)을 제거하는 것입니다. 또 다른 방법은 regularization strength를 높여서 그래디언트 체크에 있어 data loss 영향을 미미한 수준으로 만들고, 부정확한 구현을 발견(spotted)할 수 있도록 하는 것입니다.

드롭아웃/증강을 제거하는 것을 명심할것(Remember to turn off dropout/augmentations) 그래디언트 체크를 수행할 때, 드롭아웃(dropout), 무작위 데이터 증강(random data augmentations) 등 네트워크에 결정적이지 않은(non-deterministic) 효과를 제거(turn off)하는 것을 명심합니다. 그렇지 않는다면 이러한 것들은 수치적 그래디언트(numerical gradient)를 계산할 때 커다란 오차를 발생시킬 것입니다. 이러한 효과를 제거하면 이에 대한 그래디언트를 체크할 수 없다는 단점이 있습니다(가령, 드롭아웃을 정확하게 역으로 전파(backpropagate)하지 못할 수 있습니다). 따라서, 보다 더 나은 해결책은 \(f(x+h)\)와 \(f(x-h)\)를 계산하기 전, 그리고 분석적 그래디언트(analytic gradient)를 계산할 때, 특정 랜덤시드(random seed)를 가하는 것입니다.

몇몇 개의 차원만 체크하라(Check only few dimensions) 실전에서 그래디언트는 백만 단위의 파라미터를 가집니다. 때문에 그래디언트의 일부 차원만을 체크하고, 다른 것은 맞다고 가정하는 것이 현실적입니다. 주의(Be careful): 모든 개별 파라미터에 대해 몇몇 개의 차원만 그래디언트 체크해야 합니다. 간혹 편의상 여러 파라미터를 하나의 커다란 파라미터 벡터로 합치는 경우도 있습니다. 이러한 경우, bias는 전체 벡터에서 아주 적은 수의 파라미터만을 차지하기 때문에, 무작위로 추출(sample)하지 않고 이를 고려하여 모든 파라미터가 올바른 그래디언트를 받도록 체크하는것이 중요합니다.

8.1.2 학습전: 타당성 검사 꿀팁(Before learning: sanity checks Tips/Tricks)

값비싼 최적화 연산을 수행하기 전에 몇개의 타당성 검사(sanity check)를 고려해볼 필요가 있습니다:

  • chance performance를 통해 정확한 loss를 확인하라 작은 파라미터로 초기화하면서 loss가 예상되는 값인지 확인합니다. data loss 하나만을 체크하는 것이 가장 좋습니다 (따라서, regularization strength를 0으로 설정합니다). 예를 들어, Softmax 분류기로 CIFAR-10을 분류하는 경우, 초기 loss를 2.302로 예상합니다. 이는 (클래스가 10개 이므로) 0.1의 퍼져있는(diffuse) 확률값을 가지며, Softmax loss가 올바른 클래스의 음의 로그 확률(negative log probability)을 의미하기 때문에, 초기 loss가 -ln(0.1) = 2.302이 됩니다. Weston Watkins SVM의 경우, 원하는 모든 margin을 violated로 예상하기 때문에(모든 점수가 0에 가깝기 때문에), 9의 loss를 예상합니다(각각의 틀린 클래스에 대해 margin이 1이 되기 때문). 만약 이러한 loss를 확인하지 못한다면, 초기화에 문제가 있다고 판단할 수 있습니다.

  • 두 번째 타당성 검사(sanity check)로, regularization strength를 키울 때 loss가 증가함을 확인합니다.
  • 데이터의 작은 일부(subset)를 과적합(Overfit)시켜라 마지막으로 가장 중요한 것은, 전체 데이터셋을 학습시키기 전에 데이터의 아주 작은 일부(가령, 20개 정도)만을 학습시켜 보고, 비용함수(cost)가 0을 달성하는지를 확인합니다. 이 실험에서도 regularization을 0으로 설정하는 것이 가장 좋습니다. 그렇지 않은 경우에는 0의 cost를 얻지 못할 수도 있습니다. 작은 데이터셋으로 이 타당성 검사(sanity check)를 통과하지 못한다면, 이는 전체 데이터셋에 대해서도 진행할 필요가 없다는 것입니다. 하지만 매우 작은 데이터셋을 과적합(overfit)한다고 하더라도 여전히 부정확한 구현이 있을 수 있습니다. 예를 들어, 어떠한 버그로 인해 데이터포인트의 피쳐(feature)가 무작위의 값(random)을 가지는 경우, 작은 트레이닝셋을 과적합(overfit)하는 것은 가능하지만, 이를 전체 데이터셋에 적용했을 때 일반화(generalization)되지는 않습니다.

8.1.3 학습 과정 돌보기(Babysitting the learning process)

Neural Network의 학습과정에서 확인해야 할 여러 유용한 값(quantities)들이 있습니다. 이 그래프들은 학습과정을 들여다 보는 창문(window)이며 다양한 하이퍼파라미터 설정과 더 효과적인 학습을 위해 이것을 어떻게 바꾸어야 하는지에 대한 직관을 얻기 위해 활용할 수 있습니다.

그래프의 x축은 에폭(epochs)이며, 이는 학습과정에서 모든 예시를 몇 번 보았는가를 측정하는 단위입니다(1 epoch는 모든 예시를 한 번 봤다는 것을 의미합니다). iteration는 임의의 batch size에 의존하기 때문에, iteration 대신 epochs를 사용하는 것이 선호됩니다.

8.1.3.1 손실함수(Loss function)

학습과정에서 확인해야 할 유용한 값(quantity) 중 첫 번째는 loss입니다. 이는 forward pass 과정 중 각각의 배치(batches)에서 구해지기 때문입니다. 아래 그림은 시간에 따른 loss를 보여주고, 특히 학습속도(learning rate)에 관해 보여줍니다:

왼쪽: 그림은 학습속도(learning rate)의 변화에 따른 영향을 보여줍니다. 낮은 학습속도는 loss가 선형(linear)으로 개선됩니다. 높은 학습속도에서는 이것이 보다 지수적으로(exponential) 바뀝니다. 더 높은 학습속도는 loss를 더 빠르게 감소시키지만(decay), 더 나쁜 loss값에 갇히게 됩니다 (초록색 선). 이는 최적화에 지나치게 많은 “에너지(energy)”를 쏟아부운 나머지 파라미터가 무질서하게(chaotically) 요동치면서 최종적으로 좋은 장소에 다다르지 못하기 때문입니다. 오른쪽: 시간에 따른 손실함수(loss function)의 일반적인 형태로, 작은 네트워크를 CIFAR-10 데이터셋으로 학습시켰습니다. 이 loss function은 합리적으로 보이며(감소하는 속도로 보아 학습속도가 너무 작은 것 처럼 보이지만, 이는 확실히 말하기 어렵습니다), 그림상으로는 cost의 노이즈가 굉장히 많아(noisy) 보이기 때문에 배치 사이즈(batch size)가 많이 작다고 판단할 수 있습니다

loss에서의 “흔들림(wiggle)”의 정도(amount)는 batch size과 관련이 있습니다. batch size가 1이라면, wiggle은 상대적으로 많을 것입니다. 반면, 전체 데이터셋을 batch size로 설정한다면, 모든 그래디언트 업데이트가 loss function을 단조(monotonically) 감소시키기 때문에 wiggle은 최소화될 것입니다(학습 속도가 지나치게 높게 설정되지 않는다면).

간혹 loss function을 log 도메인에 그리는 것을 선호하는 사람들이 있습니다. 일반적으로 학습과정은 하키스틱(hockey stick)과 같이 지수적인(exponenetial) 형태를 띄기 때문에, 이렇게 하면 그래프를 조금 더 해석하기 용이한 직선의 형태로 표현할 수 있게 됩니다. 또한, 여러 cross-validated 모델들을 하나의 loss 그래프에 그려 넣을 때, 그 차이를 더 명확히 볼 수 있습니다.

우습게 생긴 loss function들도 있습니다. lossfunctions.tumblr.com

8.1.3.2 트레이닝/검증 정확도(Train/Val accuracy)

분류기의 학습과정에서 확인해야 할 두 번째 유용한 값(quantity)은 트레이닝/검증(train/val) 정확도 입니다. 다음 그림을 통해 모델의 오버피팅(overfitting)의 정도에 대한 직관을 얻을 수 있습니다:

트레이닝과 검증 정확도의 차이는 오버피팅(overfitting)의 정도를 나타냅니다. 왼쪽의 그림에서 가능한 두 가지 경우를 보여줍니다. 파랑색 곡선은 트레이닝 정확도(training accuracy)에 비해 비교적 낮은 검증 정확도(validation accuracy)를 보여줍니다. 이는 강한 오버피팅을 의미합니다(validation accuracy가 어느 순간을 지나면 오히려 떨어지는 경우도 있습니다). 만약 실전에서 이러한 그래프를 목격한다면 regularization(더 강력한 L2 weight penalty, dropout 등)을 높이거나 더 많은 데이터를 수집해야 합니다. validation accuracy가 training accuracy를 상당히 잘 따라 가는 경우도 있습니다. 이 경우 모델 용량(model capacity)이 충분히 크지 않다는 것을 의미합니다: 파라미터 수를 늘려 모델을 더 크게 만들 수도 있습니다.

8.1.3.3 가중치:업데이트 비율(Ratio of weights:updates)

마지막으로 확인해야할 값(quantity)은 업데이트의 크기(magnitude)와 weight의 크기의 비율(ratio)입니다. 단순히 그래디언트가 아닌, 업데이트(updates)임에 유의합니다(가령, 기본 sgd에서 이는 그래디언트와 학습속도를 곱한 값을 의미합니다). 모든 파라미터 집합에 대해 개별적으로 이 비율을 구하고 확인해야 합니다. 경험적으로(heuristic) 이 비율은 대략 1e-3 내외 입니다. 만약 이보다 작다면 학습속도(learning rate)가 너무 작다는 것을 의미하고, 반대로 이보다 크면 학습속도가 너무 높다는 것을 의미합니다. 다음은 구체적인 예시를 보여줍니다:

# assume parameter vector W and its gradient vector dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate*dW # simple SGD update
update_scale = np.linalg.norm(update.ravel())
W += update # the actual update
print update_scale / param_scale # want ~1e-3

최소 또는 최댓값을 확인하는 대신, 그래디언트 혹은 업데이트의 norm을 계산하고 확인할 수도 있습니다. 이러한 지표(metrics)들은 대개 서로 관련이 있으며 대략적으로 비슷한 결과를 보입니다.

8.1.3.4 활성화 / 레이어 당 그래디언트 분포(Activation / Gradient distributions per layer)

An incorrect initialization can slow down or even completely stall the learning process. Luckily, this issue can be diagnosed relatively easily. One way to do so is to plot activation/gradient histograms for all layers of the network. Intuitively, it is not a good sign to see any strange distributions - e.g. with tanh neurons we would like to see a distribution of neuron activations between the full range of [-1,1], instead of seeing all neurons outputting zero, or all neurons being completely saturated at either -1 or 1.

8.1.3.5 첫 번째 레이어 시각화(First-layer Visualizations)

Lastly, when one is working with image pixels it can be helpful and satisfying to plot the first-layer features visually:

Examples of visualized weights for the first layer of a neural network. Left: Noisy features indicate could be a symptom: Unconverged network, improperly set learning rate, very low weight regularization penalty. Right: Nice, smooth, clean and diverse features are a good indication that the training is proceeding well.

8.1.4 파라미터 업데이트(Parameter updates)

Once the analytic gradient is computed with backpropagation, the gradients are used to perform a parameter update. There are several approaches for performing the update, which we discuss next.

We note that optimization for deep networks is currently a very active area of research. In this section we highlight some established and common techniques you may see in practice, briefly describe their intuition, but leave a detailed analysis outside of the scope of the class. We provide some further pointers for an interested reader.

8.1.4.1 SGD와 부가기능(SGD and bells and whistles)

Vanilla update. The simplest form of update is to change the parameters along the negative gradient direction (since the gradient indicates the direction of increase, but we usually wish to minimize a loss function). Assuming a vector of parameters x and the gradient dx, the simplest update has the form:

# Vanilla update
x += - learning_rate * dx

where learning_rate is a hyperparameter - a fixed constant. When evaluated on the full dataset, and when the learning rate is low enough, this is guaranteed to make non-negative progress on the loss function.

Momentum update is another approach that almost always enjoys better converge rates on deep networks. This update can be motivated from a physical perspective of the optimization problem. In particular, the loss can be interpreted as the height of a hilly terrain (and therefore also to the potential energy since \(U = mgh\) and therefore \( U \propto h \) ). Initializing the parameters with random numbers is equivalent to setting a particle with zero initial velocity at some location. The optimization process can then be seen as equivalent to the process of simulating the parameter vector (i.e. a particle) as rolling on the landscape.

Since the force on the particle is related to the gradient of potential energy (i.e. \(F = - \nabla U \) ), the force felt by the particle is precisely the (negative) gradient of the loss function. Moreover, \(F = ma \) so the (negative) gradient is in this view proportional to the acceleration of the particle. Note that this is different from the SGD update shown above, where the gradient directly integrates the position. Instead, the physics view suggests an update in which the gradient only directly influences the velocity, which in turn has an effect on the position:

# Momentum update
v = mu * v - learning_rate * dx # integrate velocity
x += v # integrate position

Here we see an introduction of a v variable that is initialized at zero, and an additional hyperparameter (mu). As an unfortunate misnomer, this variable is in optimization referred to as momentum (its typical value is about 0.9), but its physical meaning is more consistent with the coefficient of friction. Effectively, this variable damps the velocity and reduces the kinetic energy of the system, or otherwise the particle would never come to a stop at the bottom of a hill. When cross-validated, this parameter is usually set to values such as [0.5, 0.9, 0.95, 0.99]. Similar to annealing schedules for learning rates (discussed later, below), optimization can sometimes benefit a little from momentum schedules, where the momentum is increased in later stages of learning. A typical setting is to start with momentum of about 0.5 and anneal it to 0.99 or so over multiple epochs.

With Momentum update, the parameter vector will build up velocity in any direction that has consistent gradient.

Nesterov Momentum is a slightly different version of the momentum update that has recently been gaining popularity. It enjoys stronger theoretical converge guarantees for convex functions and in practice it also consistenly works slightly better than standard momentum.

The core idea behind Nesterov momentum is that when the current parameter vector is at some position x, then looking at the momentum update above, we know that the momentum term alone (i.e. ignoring the second term with the gradient) is about to nudge the parameter vector by mu * v. Therefore, if we are about to compute the gradient, we can treat the future approximate position x + mu * v as a “lookahead” - this is a point in the vicinity of where we are soon going to end up. Hence, it makes sense to compute the gradient at x + mu * v instead of at the “old/stale” position x.

Nesterov momentum. Instead of evaluating gradient at the current position (red circle), we know that our momentum is about to carry us to the tip of the green arrow. With Nesterov momentum we therefore instead evaluate the gradient at this “looked-ahead” position.

That is, in a slightly awkward notation, we would like to do the following:

x_ahead = x + mu * v
# evaluate dx_ahead (the gradient at x_ahead instead of at x)
v = mu * v - learning_rate * dx_ahead
x += v

However, in practice people prefer to express the update to look as similar to vanilla SGD or to the previous momentum update as possible. This is possible to achieve by manipulating the update above with a variable transform x_ahead = x + mu * v, and then expressing the update in terms of x_ahead instead of x. That is, the parameter vector we are actually storing is always the ahead version. The equations in terms of x_ahead (but renaming it back to x) then become:

v_prev = v # back this up
v = mu * v - learning_rate * dx # velocity update stays the same
x += -mu * v_prev + (1 + mu) * v # position update changes form

We recommend this further reading to understand the source of these equations and the mathematical formulation of Nesterov’s Accelerated Momentum (NAG):

8.1.4.2 학습속도 어닐링(Annealing the learning rate)

In training deep networks, it is usually helpful to anneal the learning rate over time. Good intuition to have in mind is that with a high learning rate, the system contains too much kinetic energy and the parameter vector bounces around chaotically, unable to settle down into deeper, but narrower parts of the loss function. Knowing when to decay the learning rate can be tricky: Decay it slowly and you’ll be wasting computation bouncing around chaotically with little improvement for a long time. But decay it too aggressively and the system will cool too quickly, unable to reach the best position it can. There are three common types of implementing the learning rate decay:

  • Step decay: Reduce the learning rate by some factor every few epochs. Typical values might be reducing the learning rate by a half every 5 epochs, or by 0.1 every 20 epochs. These numbers depend heavily on the type of problem and the model. One heuristic you may see in practice is to watch the validation error while training with a fixed learning rate, and reduce the learning rate by a constant (e.g. 0.5) whenever the validation error stops improving.
  • Exponential decay. has the mathematical form \(\alpha = \alpha_0 e^{-k t}\), where \(\alpha_0, k\) are hyperparameters and \(t\) is the iteration number (but you can also use units of epochs).
  • 1/t decay has the mathematical form \(\alpha = \alpha_0 / (1 + k t )\) where \(a_0, k\) are hyperparameters and \(t\) is the iteration number.

In practice, we find that the step decay is slightly preferable because the hyperparameters it involves (the fraction of decay and the step timings in units of epochs) are more interpretable than the hyperparameter \(k\). Lastly, if you can afford the computational budget, err on the side of slower decay and train for a longer time.

8.1.4.3 2등 방법(Second order methods)

A second, popular group of methods for optimization in context of deep learning is based on Newton’s method, which iterates the following update:

\[x \leftarrow x - [H f(x)]^{-1} \nabla f(x)\]

Here, \(H f(x)\) is the Hessian matrix, which is a square matrix of second-order partial derivatives of the function. The term \(\nabla f(x)\) is the gradient vector, as seen in Gradient Descent. Intuitively, the Hessian describes the local curvature of the loss function, which allows us to perform a more efficient update. In particular, multiplying by the inverse Hessian leads the optimization to take more aggressive steps in directions of shallow curvature and shorter steps in directions of steep curvature. Note, crucially, the absence of any learning rate hyperparameters in the update formula, which the proponents of these methods cite this as a large advantage over first-order methods.

However, the update above is impractical for most deep learning applications because computing (and inverting) the Hessian in its explicit form is a very costly process in both space and time. For instance, a Neural Network with one million parameters would have a Hessian matrix of size [1,000,000 x 1,000,000], occupying approximately 3725 gigabytes of RAM. Hence, a large variety of quasi-Newton methods have been developed that seek to approximate the inverse Hessian. Among these, the most popular is L-BFGS, which uses the information in the gradients over time to form the approximation implicitly (i.e. the full matrix is never computed).

However, even after we eliminate the memory concerns, a large downside of a naive application of L-BFGS is that it must be computed over the entire training set, which could contain millions of examples. Unlike mini-batch SGD, getting L-BFGS to work on mini-batches is more tricky and an active area of research.

In practice, it is currently not common to see L-BFGS or similar second-order methods applied to large-scale Deep Learning and Convolutional Neural Networks. Instead, SGD variants based on (Nesterov’s) momentum are more standard because they are simpler and scale more easily.

Additional references:

  • Large Scale Distributed Deep Networks is a paper from the Google Brain team, comparing L-BFGS and SGD variants in large-scale distributed optimization.
  • SFO algorithm strives to combine the advantages of SGD with advantages of L-BFGS.

8.1.4.4 파라미터 적응형 학습속도(Per-parameter adaptive learning rate methods)

All previous approaches we’ve discussed so far manipulated the learning rate globally and equally for all parameters. Tuning the learning rates is an expensive process, so much work has gone into devising methods that can adaptively tune the learning rates, and even do so per parameter. Many of these methods may still require other hyperparameter settings, but the argument is that they are well-behaved for a broader range of hyperparameter values than the raw learning rate. In this section we highlight some common adaptive methods you may encounter in practice:

Adagrad is an adaptive learning rate method originally proposed by Duchi et al..

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

Notice that the variable cache has size equal to the size of the gradient, and keeps track of per-parameter sum of squared gradients. This is then used to normalize the parameter update step, element-wise. Notice that the weights that receive high gradients will have their effective learning rate reduced, while weights that receive small or infrequent updates will have their effective learning rate increased. Amusingly, the square root operation turns out to be very important and without it the algorithm performs much worse. The smoothing term eps (usually set somewhere in range from 1e-4 to 1e-8) avoids division by zero. A downside of Adagrad is that in case of Deep Learning, the monotonic learning rate usually proves too aggressive and stops learning too early.

RMSprop. RMSprop is a very effective, but currently unpublished adaptive learning rate method. Amusingly, everyone who uses this method in their work currently cites slide 29 of Lecture 6 of Geoff Hinton’s Coursera class. The RMSProp update adjusts the Adagrad method in a very simple way in an attempt to reduce its aggressive, monotonically decreasing learning rate. In particular, it uses a moving average of squared gradients instead, giving:

cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

Here, decay_rate is a hyperparameter and typical values are [0.9, 0.99, 0.999]. Notice that the x+= update is identical to Adagrad, but the cache variable is a “leaky”. Hence, RMSProp still modulates the learning rate of each weight based on the magnitudes of its gradients, which has a beneficial equalizing effect, but unlike Adagrad the updates do not get monotonically smaller.

Adam. Adam is a recently proposed update that looks a bit like RMSProp with momentum. The (simplified) update looks as follows:

m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)

Notice that the update looks exactly as RMSProp update, except the “smooth” version of the gradient m is used instead of the raw (and perhaps noisy) gradient vector dx. Recommended values in the paper are eps = 1e-8, beta1 = 0.9, beta2 = 0.999. In practice Adam is currently recommended as the default algorithm to use, and often works slightly better than RMSProp. However, it is often also worth trying SGD+Nesterov Momentum as an alternative. The full Adam update also includes a bias correction mechanism, which compensates for the fact that in the first few time steps the vectors m,v are both initialized and therefore biased at zero, before they fully “warm up”. With the bias correction mechanism, the update looks as follows:

# t is your iteration counter going from 1 to infinity
m = beta1*m + (1-beta1)*dx
mt = m / (1-beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / (np.sqrt(vt) + eps)

Note that the update is now a function of the iteration as well as the other parameters. We refer the reader to the paper for the details, or the course slides where this is expanded on.

Additional References:

Animations that may help your intuitions about the learning process dynamics. Left: Contours of a loss surface and time evolution of different optimization algorithms. Notice the “overshooting” behavior of momentum-based methods, which make the optimization look like a ball rolling down the hill. Right: A visualization of a saddle point in the optimization landscape, where the curvature along different dimension has different signs (one dimension curves up and another down). Notice that SGD has a very hard time breaking symmetry and gets stuck on the top. Conversely, algorithms such as RMSprop will see very low gradients in the saddle direction. Due to the denominator term in the RMSprop update, this will increase the effective learning rate along this direction, helping RMSProp proceed. Images credit: Alec Radford.

8.1.5 하이퍼파라미터 최적화(Hyperparameter optimization)

As we’ve seen, training Neural Networks can involve many hyperparameter settings. The most common hyperparameters in context of Neural Networks include:

  • the initial learning rate
  • learning rate decay schedule (such as the decay constant)
  • regularization strength (L2 penalty, dropout strength)

But as we saw, there are many more relatively less sensitive hyperparameters, for example in per-parameter adaptive learning methods, the setting of momentum and its schedule, etc. In this section we describe some additional tips and tricks for performing the hyperparameter search:

Implementation Larger Neural Networks typically require a long time to train, so performing hyperparameter search can take many days/weeks. It is important to keep this in mind since it influences the design of your code base. One particular design is to have a worker that continuously samples random hyperparameters and performs the optimization. During the training, the worker will keep track of the validation performance after every epoch, and writes a model checkpoint (together with miscellaneous training statistics such as the loss over time) to a file, preferably on a shared file system. It is useful to include the validation performance directly in the filename, so that it is simple to inspect and sort the progress. Then there is a second program which we will call a master, which launches or kills workers across a computing cluster, and may additionally inspect the checkpoints written by workers and plot their training statistics, etc.

Prefer one validation fold to cross-validation In most cases a single validation set of respectable size substantially simplifies the code base, without the need for cross-validation with multiple folds. You’ll hear people say they “cross-validated” a parameter, but many times it is assumed that they still only used a single validation set.

Hyperparameter ranges Search for hyperparameters on log scale. For example, a typical sampling of the learning rate would look as follows: learning_rate = 10 ** uniform(-6, 1). That is, we are generating a random number from a uniform distribution, but then raising it to the power of 10. The same strategy should be used for the regularization strength. Intuitively, this is because learning rate and regularization strength have multiplicative effects on the training dynamics. For example, a fixed change of adding 0.01 to a learning rate has huge effects on the dynamics if the learning rate is 0.001, but nearly no effect if the learning rate when it is 10. This is because the learning rate multiplies the computed gradient in the update. Therefore, it is much more natural to consider a range of learning rate multiplied or divided by some value, than a range of learning rate added or subtracted to by some value. Some parameters (e.g. dropout) are instead usually searched in the original scale (e.g. dropout = uniform(0,1)).

Prefer random search to grid search As argued by Bergstra and Bengio in Random Search for Hyper-Parameter Optimization, “randomly chosen trials are more efficient for hyper-parameter optimization than trials on a grid”. As it turns out, this is also usually easier to implement.

Core illustration from Random Search for Hyper-Parameter Optimization by Bergstra and Bengio. It is very often the case that some of the hyperparameters matter much more than others (e.g. top hyperparam vs. left one in this figure). Performing random search rather than grid search allows you to much more precisely discover good values for the important ones.

Careful with best values on border Sometimes it can happen that you’re searching for a hyperparameter (e.g. learning rate) in a bad range. For example, suppose we use learning_rate = 10 ** uniform(-6, 1). Once we receive the results, it is important to double check that the final learning rate is not at the edge of this interval, or otherwise you may be missing more optimal hyperparameter setting beyond the interval.

Stage your search from coarse to fine In practice, it can be helpful to first search in coarse ranges (e.g. 10 ** [-6, 1]), and then depending on where the best results are turning up, narrow the range. Also, it can be helpful to perform the initial coarse search while only training for 1 epoch or even less, because many hyperparameter settings can lead the model to not learn at all, or immediately explode with infinite cost. The second stage could then perform a narrower search with 5 epochs, and the last stage could perform a detailed search in the final range for many more epochs (for example).

Bayesian Hyperparameter Optimization is a whole area of research devoted to coming up with algorithms that try to more efficiently navigate the space of hyperparameters. The core idea is to appropriately balance the exploration - exploitation trade-off when querying the performance at different hyperparameters. Multiple libraries have been developed based on these models as well, among some of the better known ones are Spearmint, SMAC, and Hyperopt. However, in practical settings with ConvNets it is still relatively difficult to beat random search in a carefully-chosen intervals. See some additional from-the-trenches discussion here.

8.2 평가(Evaluation)

8.2.1 모델 앙상블(Model Ensembles)

In practice, one reliable approach to improving the performance of Neural Networks by a few percent is to train multiple independent models, and at test time average their predictions. As the number of models in the ensemble increases, the performance typically monotonically improves (though with diminishing returns). Moreover, the improvements are more dramatic with higher model variety in the ensemble. There are a few approaches to forming an ensemble:

  • Same model, different initializations. Use cross-validation to determine the best hyperparameters, then train multiple models with the best set of hyperparameters but with different random initialization. The danger with this approach is that the variety is only due to initialization.
  • Top models discovered during cross-validation. Use cross-validation to determine the best hyperparameters, then pick the top few (e.g. 10) models to form the ensemble. This improves the variety of the ensemble but has the danger of including suboptimal models. In practice, this can be easier to perform since it doesn’t require additional retraining of models after cross-validation
  • Different checkpoints of a single model. If training is very expensive, some people have had limited success in taking different checkpoints of a single network over time (for example after every epoch) and using those to form an ensemble. Clearly, this suffers from some lack of variety, but can still work reasonably well in practice. The advantage of this approach is that is very cheap.
  • Running average of parameters during training. Related to the last point, a cheap way of almost always getting an extra percent or two of performance is to maintain a second copy of the network’s weights in memory that maintains an exponentially decaying sum of previous weights during training. This way you’re averaging the state of the network over last several iterations. You will find that this “smoothed” version of the weights over last few steps almost always achieves better validation error. The rough intuition to have in mind is that the objective is bowl-shaped and your network is jumping around the mode, so the average has a higher chance of being somewhere nearer the mode.

One disadvantage of model ensembles is that they take longer to evaluate on test example. An interested reader may find the recent work from Geoff Hinton on “Dark Knowledge” inspiring, where the idea is to “distill” a good ensemble back to a single model by incorporating the ensemble log likelihoods into a modified objective.

8.3 요약(Summary)

To train a Neural Network:

  • Gradient check your implementation with a small batch of data and be aware of the pitfalls.
  • As a sanity check, make sure your initial loss is reasonable, and that you can achieve 100% training accuracy on a very small portion of the data
  • During training, monitor the loss, the training/validation accuracy, and if you’re feeling fancier, the magnitude of updates in relation to parameter values (it should be ~1e-3), and when dealing with ConvNets, the first-layer weights.
  • The two recommended updates to use are either SGD+Nesterov Momentum or Adam.
  • Decay your learning rate over the period of the training. For example, halve the learning rate after a fixed number of epochs, or whenever the validation accuracy tops off.
  • Search for good hyperparameters with random search (not grid search). Stage your search from coarse (wide hyperparameter ranges, training only for 1-5 epochs), to fine (narrower rangers, training for many more epochs)
  • Form model ensembles for extra performance

8.3.1 참고(Additional References)