✍ 해당 시리즈 포스팅은 미시간 대학의 EECS 498-007 강의 내용을 정리한 글입니다. cs231n 강의와 유사하여 해당 시리즈 포스팅과 겹치는 부분이 많이 있을 수 있습니다.
Loss function이 주어졌을 때, 이 loss를 만족시키는 가중치 행렬 W에 대한 값을 실제로 어떻게 찾을까?
우리가 가진 가중치 행렬 를 넣어서 얻어지는 loss값인 을 최소화하는 를 어떤 방식으로 찾을 수 있을까?
우리 현재 위치의 위도, 경도 값을 의 각 요소들로 생각하고, 해당 지점의 높이를 loss값으로 보면 이 과정은 마치 사진과 같은 절경에서 눈을 가리고 밑바닥을 찾아 걸어가는 것으로 생각할 수 있다.
Random Search
가중치 행렬의 많은 값을 무작위로 생성한 다음 각 케이스 별 loss를 평가하고 그 중 최적의 가중치 행렬을 선정할 수도 있다. 이는 생각보단 괜찮은 알고리즘일 수도 있지만, 요즘은 이보다 훨씬 좋은 접근 방식들이 있다.
Follow the slope
사람 여러 명을 랜덤 좌표에 올려놓고 가장 밑바닥에 있는 사람을 선정하는 Random search 대신, 한 사람을 랜덤 좌표에 올려놓고 경사 면을 따라 이동하게 하는 방식이 더 현명한 방법일 수도 있다. 발을 딛는 과정을 계속 반복하면서, 점점 더 낮은 지점을 향해 걸어가게 된다.
1차원에서 미분값을 통해 기울기를 알 수 있고, 그렇다면 다차원에서의 기울기는 각 차원에 따른 편미분들의 벡터가 된다.
특정 방향에서의 기울기는 해당 방향과 gradient의 내적이므로 양수 기울기는 증가, 음수 기울기는 감소 방향이 된다. 우리는 음수 기울기 방향을 택하면 최저점을 향해 걸어갈 수 있다.
그렇다면 특정 입력 벡터에 대한 loss function의 기울기를 어떻게 계산할까?
왼쪽은 우리에게 주어진 현재의 가중치 행렬 이고, 오른쪽은 에 대한 loss의 gradient를 가지는 행렬이다.
이를 얻으려면 에서의 loss값을 구한 다음, 특정 슬롯의 값을 미세하게 증가 시킨 후 새로운 loss를 구한 후 도함수의 극한 개념을 활용하여 해당 슬롯의 loss에 대한 gradient를 얻을 수 있다. 이 과정을 모든 슬롯에 대해서 반복한다.
이 방식을 Numeric Gradient라고 한다.
그리하여 Analytic Gradient 방식을 통해 를 표현할 수 있는 함수를 유도하고, 이를 통해 에 대한 Loss의 gradient를 구할 수 있다. (6장에서 backpropagation을 통해 계산 예정)
그리하여 실제로는 Analytic gradient로 gradient 정보를 사용하여 loss를 최소화 하게 되지만, 이는 구현 과정이 복잡하고 계산 실수를 하기 쉽기(error-prone) 때문에 디버깅 도구로써 Numeric gradient로 gradient check를 할 수 있으면 좋다.
PyTorch에서는 gradcheck이라는 함수로 gradient check이 가능하다.
2차 미분 값이 제대로 적용되었는지 확인할 때는 gradgradcheck 함수를 사용하면 된다.
임의의 weight를 초기화한 후 local gradient를 계산하고, 그 방향으로 업데이트를 반복한다.
# Vanilla gradient descent
w = initialize_weights() # 가중치 초기화
for t in range(num_steps): # num_steps 만큼 반복
dw = compute_gradient(loss_fn, data, w) # gradient 계산
w -= learning_rate * dw # 음의 방향으로 업데이트 진행
하이퍼파라미터는 아래와 같다.
하지만 만약 손실 함수가 타원형을 띄게 된다면 gradient 방향이 최저점으로 가는 방향과는 약간의 차이가 있고, 그 결과 위처럼 한 번 휘어서 최적점에 도달하게 된다.
또한, 학습이 진행되어 최저점에 가까워질수록 주위가 점점 평평해져서 gradient 크기가 작아지고, 자연스레 보폭이 줄어들게 된다.
여태 살펴본 Gradient Descent를 Batch Gradient Descent 또는 Full Batch Gradient Descent라고 한다. Batch Gradient Descent는 모든 학습 데이터의 loss를 합친 전체 loss를 쓰기 때문에, 학습 데이터 수가 많다면 매우 오래 걸릴 수 있다.
따라서, 전체 학습 데이터셋에 대한 합계를 계산하는 대신, 미니배치를 이용해 loss 함수를 근사하고, gradient를 근사하는 방법이다.
미니배치의 일반적인 크기는 32, 64, 128 등을 사용하는 경우가 많다.
하이퍼파라미터는 아래와 같다.
위 세가지는 Full batch gradient descent와 같고, 아래 추가 항목들이 있다.
사실 위 설명 방식은 Stochastic Gradient Descent보다는 Mini Batch Gradient Descent에 가까운 설명이다. 흔히 말하는 Stochastic Gradient Descent는 Mini Batch Gradient Descent에서 mini batch 크기를 1(단일 샘플)로 설정한 경우라고 생각하면 될 것 같다.
Stochastic Gradient Descent(확률적 경사 하강법)으로 불리는 이유는, 샘플링 된 데이터 x와 y에 대한 loss 함수를 전체 데이터 분포의 loss에 대한 기댓값으로 볼 수 있다. 그리고 모든 데이터셋 샘플에 대한 loss 함수의 평균은 전체 확률 분포에 대한 몬테카를로 추정이 되기 때문이다. (loss 함수를 확률적으로 생각)
그리고 이는 gradient에 대해서도 마찬가지이고, 손실 함수에 대해 전체 기댓값을 근사화 하기위해 얼마나 많은 샘플(batch size)을 사용할 지 정해주어야 한다.
High condition number
만약 손실함수가 위처럼 생겼고, 가운데 최저점에 도달하려 한다면, 수평 축의 가중치는 변해도 loss가 아주 천천히 줄어들고, 수직 방향의 가중치 변화에 훨씬 더 민감하게 된다. 이런 경우 loss는 bad condition number를 갖고 있다고 하고, 이는 이계도함수를 행렬로 표현한 Hessian matrix의 최대/최소 singular values 값이 비율이 매우 안 좋다는 것이다.
이런 경우 step size가 너무 크다면 loss의 gradient 방향이 고르지 못하여 지그재그 형태를 띠면서 가중치 업데이트가 이루어지게 되고, 이렇게 되면 최적화에 시간이 오래 걸린다. Step size가 작은 경우엔 매우 느리게 수렴하여 최저점에 도달하기 어려워진다.
local minimum / saddle point
local minima
saddle point
Noisy gradients
데이터 셋 전체의 loss를 계산하려면 시간이 너무 오래 걸리기 때문에 미니배치의 데이터로 실제 loss를 추정하기만 하는데, 이는 gradient의 부정확한 추정값(noise estimate)을 구하는 것이 되고, 이로 인해 학습 방향이 자주 왜곡될 수 있다. 이로 인해 학습 시간이 늘어나게 된다.
SGD에서 현재 미니 배치의 gradient 방향만 고려하는 것이 아니라 velocity(속도)도 같이 고려하는 것. ρ 라는 하이퍼파라미터를 추가하여 이에 대한 비율도 조절이 가능하다. (보통 0.9나 0.99의 높은 값으로 설정) exponential moving average 방식
velocity의 비율(ρ)을 곱해주고 현재 gradient를 더함. 따라서 gradient vector 그대로의 방향이 아닌 velocity vector의 방향으로 나아가게 된다.
두 코드 모두 같은 SGD + Momentum 코드이다.
SGD + Momentum과 유사하지만, 계산 순서만 바뀌었다. 기본 SGD + Momentum은 현재 지점에서의 gradient를 계산한 뒤 velocity와 합해주지만, Nesterov Momenum은 우선 velocity 방향으로 움직여준 후, 그 지점에서의 gradient를 계산한다. 그리고 다시 원점에서 그 둘을 합해준다.
velocity와 gradient 두 정보를 약간 더 섞어준다고 생각할 수 있고, velocity의 방향이 잘못되었을 경우 현재 gradient의 방향을 좀 더 활용할 수 있도록 해준다.
velocity를 업데이트 할 때 이전 시점의 velocity와 에서의 gradient를 계산하고, step update는 방금 구한 를 활용해서 구한다.
이제 를 로 치환해서 수식을 조금 변경해보면 아래와 같이 나타낼 수 있다. 유도 과정은 아래와 같다.
기존 SGD 방식은 속도가 느린 편이고, Momentum 계열은 이전 velocity의 영향을 받아 minima를 그냥 지나쳐 버리는 경향이 있기는 하지만, 결국 다시 돌아와서 minima에 수렴하는 경향이 있다.
→ 만일 minima가 좁고 깊다면 minima를 지나쳐서 다시 돌아오지 못하는 경우가 발생할 수도 있다. 하지만, 이런 좁고 깊은 minima는 심한 overfitting 문제를 가져올 수 있기 때문에 애초에 선호되지 않는 minima이고(평평한 minima가 일반화를 잘 함), 데이터의 크기가 커질수록 이런 민감한 유형의 minima는 줄어든다고 한다.
Poor Conditioning이 존재하는 경우, 작은 차원에서는 gradient 제곱을 합한 값이 작아지게 되고, update term에 그만큼 작은 값을 나눠주기 때문에 비교적 업데이트가 크게 이루어지고, 반대로 큰 차원에서는 update term에 큰 값을 나눠주기 때문에 업데이트가 작게 된다.
하지만 학습 횟수가 증가하게 된다면 gradient squared term의 값은 점점 커지기 때문에, step size는 점점 작아지게 된다. 이는 Convex(볼록)한 경우에는 장점이 될 수 있지만, saddle point 같은 지점이 존재한다면 학습이 멈추게 될 수 있다. 이는 RMSProp에서 개선되었다.
AdaGrad에서 gradient squared term을 그대로 사용하지만, 단순한 gradient 제곱의 누적이 아니라, 기존 누적 값에 decay_rate를 곱한 값에 현재 gradient 제곱에 1-decay_rate를 곱한 값을 더한다. SGD + Momentum 방식에서도 velocity를 decay하는 하이퍼파라미터 가 있었듯, 비슷한 텀으로 decay_rate를 넣어준다고 보면 된다. 이를 통해 Momentum처럼 exponential moving average 방식을 사용하게 된다.
decay_rate는 보통 0.9 또는 0.99를 사용한다.
여태까지 Momentum 계열과 Ada 계열의 방식들을 살펴보았는데, SGD + Momentum과 RMSProp의 장점을 잘 조합한 알고리즘이 바로 Adam 이다.
앞서 보았던 Momentum과 RMSProp의 구현부를 다시 살펴보고, 이를 조합한 Adam 방식을 살펴보자.
빨간 박스는 Momentum의 아이디어이고, 파란 박스는 RMSProp의 아이디어 부분이라고 생각하면 된다. 따라서 moment1 부분은 velocity를 고려하게 되고, moment2 부분은 squared gradient(exponential moving average를 포함한..)를 고려하게 된다.
beta1 과 beta2 는 각각 하이퍼파라미터로 각 moment의 decay_rate라고 생각하면 된다.
하지만 만약 time step이 0인 시점에서 beta2값이 0.999로 가정된다면 moment2의값은 맨 처음 0으로 initialized되고, 첫 번째 for문을 돌게 되면 거의 0에 가까운 값이 저장된 후 제곱근이 learning_rate * moment1에 나눠지기 때문에, 이는 매우 매우 큰 값이 돼서 결국 첫 번째 step의 step size가 너무 커지게 될 것이다.
따라서 위 슬라이드처럼 Bias correction term을 추가해서 moment2가 초기에 0에 가까운 값으로 편향되는 것을 막는다.
보통 beta1은 0.9, beta2는 0.999, learning_rate는 1e-3, 5e-4, 1e-4 정도면 웬만하면 잘 동작하고, 2015년 나온 논문이지만 포스팅을 작성하는 현재까지도 매우 많이 사용되는 최적화 알고리즘임.
오늘 학습한 Optimization 알고리즘들의 특징을 보기 쉽게 표로 정리한 것.
2019년 강의에서는 나오지 않지만, 2020년도 슬라이드에는 Adam에 weight decay를 적용한 AdamW에 대해서도 다룬다.
PRML 책에 있는 그림인데, polynomial regression을 적용할 때 차수가 커지면 각 weight 값이 굉장히 큼직큼직하게 결정되는 것을 확인할 수 있는데, 이로 인해 데이터에 굉장히 민감해지고 오버피팅이 발생하게 된다. 따라서 weight 업데이트를 할 때, 이전 weight의 크기를 일정 비율 감소시키는 기법이 weight decay이다.
여태까지 살펴본 알고리즘들은 모두 loss의 일차 미분 값으로 gradient step을 밟는 First-order optimization이다.
Second-Order optimization도 존재하는데, 이는 gradient에다가 hessian 정보까지 활용하여 step size를 adaptive하게 결정한다.
위처럼 이차 미분을 통해 얻은 곡률이 낮은 경우에는 step size를 더 크게 적용해줄 수 있다. 이처럼 learning_rate를 robust하게 세팅할 수 있는 아주 좋은 아이디어이다.
하지만 gradient 행렬은 weight 행렬과 같은 크기를 같지만, 헤시안 행렬은 그 제곱 크기를 가지게 되고, 심지어 헤시안 행렬의 역행렬을 사용하려면 의 계산량이 필요하게 돼서 실용적이진 못하다.
보통 실제로 실험을 하게 되면, 일반적으로 Adam을 가장 먼저 써보고, 때때로 Momentum이 더 좋은 결과를 보일 수도 있다.
만약 데이터의 dimension이 크지 않아 full batch update가 가능하다면 L-BFGS(전체 헤시안 역행렬을 이용하는 BFGS에서 개선된 알고리즘) 를 써보자.
유튜브 강의: https://www.youtube.com/watch?v=qcSEP17uKKY&list=PL5-TkQAfAZFbzxjBHtzdVCWE0Zbhomg7r&index=4
PDF: https://web.eecs.umich.edu/~justincj/slides/eecs498/FA2020/598_FA2020_lecture04.pdf