Fancier Optimization
Optimization: Problems with SGD
기존의 SGD를 사용하게 되면 몇 가지 문제가 발생한다. 첫번째로 Poor Conditioning 문제가 있다.
가령 우리의 손실 함수가 위와 같이 생겼다고 생각해 보자.
수평 축의 가중치는 업데이트를 해도 손실 함수가 아주 느리게 변할 것이다. 즉, Loss는 수직 방향의 가중치 변화에 훨씬 더 민감하게 반응할 것이다.
이는 현재 지점에서 Loss는 bad condition number를 지니고 있다고 말할 수 있으며, 이 지점의 Hessian 행렬(이차 미분 행렬)의 최대/최소 singular value들의 값의 비율이 안 좋다는 것을 의미한다.
결국 손실 함수가 SGD를 통해 최적화를 수행하면, 위와 같이 지그재그를 그리는 형태가 나타나게 되고, 이는 바람직하지 못한 행동이 된다.
SGD에서 발생하는 다른 문제는 local minima와 saddle point이다.
위의 그래프에서 휘어진 손힐 함수는 중간에 “valley”(local minima)가 하나 있다. 이 경우 gradient가 0이 되기 때문에 SGD는 멈추게 된다.
또 다른 문제는 saddle point에 관한 것으로, saddle point는 한쪽 방향으로는 증가하고 있고 다른 한쪽 방향으로는 감소하고 있는 지역을 말한다. 여기서도 gradient는 0이 되므로 SGD가 멈추게 된다.
1차원의 예제에서는 local minima가 더 심각한 문제 같아 보이지만, 실제로 고차원의 예제에서는 saddle point가 더 빈번하게 발생한다.
SGD의 또 다른 문제는 모든 training set이 아닌 mini-batch의 데이터들만 가지고 실제 Loss를 추정한다는 것이다.
이는 매번 정확한 gradient를 얻을 수 없다는걸 의미한다. (즉, noise 발생)
SGD + Momentum
위의 문제들의 대다수를 해결할 수 있는 아주 간단한 방법이 하나 있다. 바로 SGD에 momentum term을 추가하는 것이다.
SGD + Momentum 방식은 gradient를 계산할 때 velocity(속도)를 이용한다. 즉, 미니배치의 gradient 방향과 velocity를 같이 고려한다.
Velocity에 일정 비율 ρ(rho)를 곱해주고 현재 gradient를 더한다. 보통 rho는 0.9와 같은 높은 값으로 맞춰준다.
엄청 간단한 방법이지만 지금까지 말했던 문제들을 해결하는 데 많은 도움을 줄 수 있다.
- Local minima와 saddle point 문제를 생각해보면 물리적으로 공이 굴러내려 오는 것을 상상해 볼 수 있다. 따라서, gradient가 0이여도 여전히 velocity를 가지고 있기 때문에 움직일 수 있다.
- 업데이트가 잘 안 되는 경우(poor conditioning), 즉, 지그재그로 움직이는 상황이라면 momentum이 이 변동을 서로 상쇄시켜 버린다. 이를 통해 loss에 민감한 수직 방향의 변동은 줄여주고 수평 방향의 움직임은 점차 가속화 된다.
- 오른쪽을 보면 검은색이 일반 SGD이고, 파란색이 momentum SGD이다. Momentum을 추가해서 velocity가 생기면 결국 noise가 평균화되는 효과가 발생한다. 보통의 SGD가 구불구불 움직이는 것에 비해서 momentum은 minima를 향해 더 부드럽게 움직인다.
Nesterov Momentum
Nesterov momentum(혹은 Nesterov acclerated gradient라고도 함)은 momentum의 변형으로, 계산하는 순서를 조금 바꾼 것이다.
Nesterov는 빨간 점에서 시작해 우선 velocity의 방향으로 움직여 그 지점에서의 gradient를 계산하고, 다시 원점으로 돌아가 둘을 합친다.
완벽한 설명은 아니지만 이는 두 정보를 약간 더 섞어준다고 생각해 볼 수 있으며, velocity의 방향이 잘못되었을 경우에 현재 gradient의 방향을 좀 더 활용할 수 있도록 해준다.
Nesterov는 convex optimization 문제에서는 뛰어난 성능을 보이지만, neural network와 같은 non-convex 문제에서는 성능이 보장되지는 않는다.
Nesterov의 수식은 위와 같다. velocity를 업데이트하기 위해 이전의 velocity와 에서의 gradient를 계산한다.
그리고 step update는 앞서 계산한 velocity를 이용해서 구해준다.
Nesterov의 공식을 보면 다소 까다롭게 생겼다. 기존에는 loss와 gradient를 같은 점()에서 구하였지만 Nesterov는 그렇지 않다.
이를 해결하기 위해 변형 공식을 사용한다. 변수들을 적절히 잘 바꿔주면 Nesterov를 조금 다르게 표현할 수 있으며, loss와 gradient를 같은 점에서 계산할 수 있게 된다.
그리고 수정된 수식을 통해 우리는 nesterov를 다시 이해할 수 있다. 첫 번째 수식은 기존의 momentum과 같이 velocity와 계산한 gradient를 일정 비율로 섞어준다.
두 번째 사각형의 맨 밑의 수식을 보면, 현재 점(와 velocity()를 더해준다. 거기에 “현재 velocity - 이전 velocity”를 계산해서 일정 비율(rho)를 곱하고 더해준다.
즉, Nesterov momentum은 현재/이전의 velocity 간의 에러 보정(error-correcting term)이 추가되었다.
SGD, Momentum, Nesterov의 예시를 살펴보면 위와 같다.
Nesterov는 추가된 수식 때문에 momentum과는 조금 다르게 움직인다. 일반 momentum에 비해서 overshooting이 덜 한 것을 알 수 있다. 그러나 NN에서 사용하지는 않는다.
AdaGrad
최적화 방법 중에는 AdaGrad도 있다. AdaGrad는 훈련 도중 계산되는 gradients를 활용하는 방법이다.
AdaGrad는 velocity term 대신 grad squared term을 사용한다. 그리고 학습 도중에 계산되는 gradient에 제곱을 해서 계속 더해준다.
그리고 update를 할 때 update term을 앞서 계산한 grad squared term으로 나눠준다.
Condition number인 경우 빨간색 박스 안의 값은 gradient가 큰 차원은 더 큰 제곱 값이 나눠지기 때문에 속도가 줄어들고, gradient가 작은 차원은 더 작은 제곱 값으로 나눠지기 때문에 가속도가 붙게 된다.
AdaGrad에는 문제가 하나 있다. AdaGrad는 step을 진행할수록 값이 점점 작아진다.
update 동안 gradient의 제곱이 계속해서 더해지기 때문에 이는 step size를 더 작은 값이 되게 한다.
RMSProp
RMSProp은 AdaGrad의 변형으로, 앞서 언급한 문제를 개선시킨 방법이다.
RMSProp은 기존 AdaGrad의 제곱 항을 그대로 사용하지만, 이 값들을 그저 누적만 시키는 것이 아니라 기존의 누적 값에 decay_rate를 곱해준다.
decay_rate는 보통 0.9 또는 0.99 정도로 하면 된다. 그리고 현재 gradient의 제곱은 (1 - decay_rate)를 곱해줘서 더해준다.
RMSProp은 gradient 제곱을 계속 나눠준다는 점에서 AdaGrad와 유사하지만, 위와 같은 특징으로 점점 속도가 줄어드는 문제를 해결할 수 있다.
RMSProp이나 momentum은 위 사진에서 보는 바와 같이 기본 SGD보다는 훨씬 좋다. 하지만 이 둘의 행동 양상은 조금 다르다.
Momentum의 경우에는 overshooting한 뒤에 minima로 돌아오나 RMSProp은 각 차원마다의 상황에 맞도록 적절하게 궤적(trajectory)을 수정시킨다.
Adam (almost)
우리는 지금까지 Momentum과 AdaGrad, RMSProp에 대해 알아봤다. 이 둘(Momentum 계열 vs Ada 계열)은 모두 괜찮은 아이디어인 듯 보인다.
그렇다면 이 두 방법을 조합해보면 어떨까? 아마 훨씬 더 좋은 방법이 될 것이다. Adam의 현재 식이 나오게 된 과정을 먼저 살펴보자.
위 사진은 Adam과 “유사한” 알고리즘이다. Adam은 first moment와 second moment를 이용해서 이전의 정보(estimate)를 유지시킨다.
First moment는 gradient의 가중 합이다. 그리고 second moment는 AdaGrad나 RMSProp처럼 gradients의 제곱을 이용하는 방법이다.
Adam으로 update를 진행하게 되면, 우선 first moment는 velocity를 담당한다. 그리고 sqrt(second moment)를 나눠주는데 second moment는 gradient의 제곱 항이다.
Adam은 마치 momentum + RMSProp 같아 보인다. 하지만 여기에도 문제가 하나 있다. 초기 step에서는 어떤 일이 발생할까?
Second moment를 1회 업데이트하면, beta2는 decay_rate(0.9 또는 0.99)로 1에 가까운 값이다. 따라서 1회 업데이트 후에도 second moment는 여전히 0에 가깝다.
Update step에서 second moment로 나누면 나눠주는 값이 크기 때문에 초기 step이 엄청나게 커진다.
중요한 것은 이 step이 실제로 손실 함수가 가파르기 때문이 아니라 second moment를 0으로 초기화시켰기 때문에 발생하는 ‘인공적인’ 현상이라는 것이다.
Adam (full form)
Adam은 초기 step이 엄청 커져버릴 수 있고 이로 인해 잘못될 수도 있다고 말했다.
Adam은 이를 해결하기 위해 보정하는 항(bias correction term)을 추가한다.
동일한 환경에서 SGD, Momentum, RMSProp, Adam을 비교해보자. Adam은 momentum과 RMSProp을 합쳐놓은 모습이다.
Adam은 momentum처럼 overshoot하지만 momentum만큼 심하지는 않다. 또한 Adam은 RMSProp처럼 각 차원의 상황을 따로 고려해서 step을 이동한다.
Learning rate를 고르는 과정은 까다롭다. 따라서 각각의 learning rate의 특성을 적절히 이용하여 learning rate decay 전략을 사용해 볼 수 있다.
Step decay 방법에서는 몇 개의 epoch마다 learning rate를 낮출 수 있다. Exponential decay 방법으로는 학습 과정 동안에 꾸준히 learning rate를 낮출 수도 있다. 그 외에도 다양한 전략이 있다.
오른쪽의 그림은 ResNet 논문에 있는 그림이다. Loss가 계속 내려가다가 어느순간 평평해지더니 다시 또 내려가는 것을 반복한다.
ResNet 논문에서는 step decay learning rate 전략을 사용한 것으로, 갑자기 내려가는 구간은 learning rate를 낮추는 구간이다.
수렴을 잘 하고 있는 상황에서 gradient가 점점 작아지면 learning rate가 높기 때문에 더 깊게 들어가지 못한다.
이 상황에서 learning rate를 낮추게 되면 속도가 줄어들 것이고 지속해서 loss를 내려갈 수 있는 것이다.
Second-Order Optimization
지금까지 배운 optimization 알고리즘들은 모두 1차 미분을 활용한(first-order) 방법이다.
위의 그림처럼 빨간색 점에서 손실 함수의 gradient를 구한 후 이 정보를 활용해 손실 함수를 선형 함수로 근사(1차 테일러 근사)시킨다.
이 1차 근사 함수를 실제 손실 함수라고 가정하고 step을 내려가지만 이 근사 함수로는 멀리갈 수 없다.
따라서 2차 근사(second-order approximation)의 정보를 추가적으로 활용하는 방법이 있다.
이는 2차 테일러 근사 함수가 될 것이고 이 함수는 2차 함수의 모양이다.
2차 근사에서는 기울기 뿐만 아니라 곡률 정보(Hessian)를 사용하여 함수의 형태를 더 잘 이해할 수 있으므로, minima로 더 빠르게 이동할 수 있다.
위의 예시를 다차원으로 확장시킨 것이 ‘Newton step’이다. 2차 미분값들로 된 행렬인 Hessian matrix를 계산하고, Hessian matrix의 역행렬을 이용하면 실제 손실 함수의 2차 근사를 이용해 minima로 바로 이동할 수 있다.
이론적으로 “Newton’s method”에서는 learning rate는 필요없고, 매 step마다 항상 minima를 향해 이동한다.
그러나 실제로는 2차 근사도 완벽하지 않기 때문에 learning rate가 필요하다. Newton’s method에서는 minima로 이동하는게 아니라 ‘minima의 방향’으로 이동하기 때문이다.
그러나 Newton’s method는 Deep learning에서 사용할 수 없다. 왜냐하면 Hessian matrix는 N x N 행렬(N: Network의 파라미터 수)이므로 N이 1억이면 1억의 제곱만큼 존재한다.
이를 메모리에 저장할 방법도 없고, 역행렬 계산도 불가능하다.
따라서 실제로는 ‘Quasi-Newton methods’를 이용한다. Quasi-Newton methods는 Full Hessian을 그대로 사용하기 보다는 근사시킨다. (Low-rank approximations)
L-BFGS도 Hessian을 근사시켜서 사용하는 방법인데, DNN에서는 잘 사용하지 않는다.
왜냐하면 L-BFGS에서 2차 근사가 stochastic case에서는 잘 동작하지 않고, non-convex 문제에도 적합하지 않기 때문이다.
그러나 style transfer와 같은 stochasticity(무작위성)와 파라미터가 적은 경우에는 사용하기도 한다.
Beyond Training Error
지금까지 이야기했던 모든 것들은 전부 training error를 줄이기 위한 방법들이다.
하지만 사실 우리는 training error가 아닌 “한번도 보지 못한 데이터”에 대한 성능이 더 중요하다. 즉, 우리가 원하는 것은 train/test error의 격차를 줄이는 것이다.
Model Ensembles
Train/test error의 격차를 줄이는 가장 빠르고 쉬운 길은 바로 Model Ensembles다. 모델 앙상블은 n개의 모델을 독립적으로 학습시키고 n개 모델 결과의 평균을 이용한다.
모델의 수가 늘어날수록 overfitting이 줄고 성능이 조금(보통 2% 정도) 향상된다.
Regularization: Dropout
Dropout은 forward pass 과정에서 임의로 일부 뉴런들을 0으로 만든다.
한 레이어의 출력(activation function을 통과한)을 전부 구하고 일부 뉴런들을 0으로 만든 후 다음 레이어로 넘아간다.
Dropout은 특징들 간 상호작용을 방지하기 때문에, 즉 네트워크가 일부의 feature들에만 의존하지 못하게 하기 때문에 더 좋은 성능을 낸다.
Dropout: Test time
Dropout을 test time에 사용할 때는 복잡한 적분식을 근사하기 위해, 네트워크 출력에 dropout probability(p=0.5)를 곱하는 방식을 사용한다.
그러나, test time에는 계산 효율이 중요하므로 연산을 하나 추가하는 것은 좋지 않다. 따라서 test time에는 기존의 연산을 그대로 사용하고 대신 train time에서 p를 나눠주는 방식을 사용한다.
Regularization: Data Augmentation
Regularization의 다른 예로는 Data Augmentation이 있다. Data Augmentation은 train data에 무작위로 변환을 주는 방식으로 작동하며 아래와 같은 방식들을 사용한다.
- 사진 좌우반전
- 사진을 랜덤하게 자르기
- 사진 크기 바꾸기
- Color jittering
Regularization: A common pattern
Regularization에는 그 외에도 DropConnect, Fractional Max Pooling 등이 있다.
DropConnect는 weight matrix를 임의로 0으로 만들어준다. Fractional Max Pooling은 pooling 연산을 적용하는 지역이 임의로 선정된다.
Stochastic Depth는 아주 깊은 네트워크에서 train time에 일부 레이어들을 임의로 제거하고, 일부만 사용해 학습하는 방식이다. Test time에는 전체 네트워크를 다 사용한다.
Transfer Learning
충분한 데이터가 없을 경우, Transfer learning을 사용해볼 수도 있다.
Transfer learning은 기존에 학습된 모델을 가지고 와서 마지막 레이어만 초기화시켜 작은 데이터로 학습시킴으로써 모델이 잘 동작하게 만든다.
만약 데이터가 조금 더 있다면 네트워크의 더 많은 부분을 학습/업데이트 시킬 수 있다. 이 과정을 네트워크의 fine tuning이라고 부른다.
그러나 기존 learning rate보다는 낮춰서 학습해야 한다. 그 이유는 기존의 가중치들이 이미 ImageNet으로 잘 학습되어 있기 때문에 잘 동작하기 때문이다. 따라서, 우리가 가진 데이터셋의 성능을 높이려면 이미 학습된 가중치를 조금씩만 수정하면 된다.
Transfer learning에서는 위와 같이 4가지 사례를 살펴볼 수 있다.