EECS 498-007 Lecture 4 | Optimization

shyoon·2025년 1월 15일
0

EECS 498-007

목록 보기
2/5
post-thumbnail

해당 시리즈 포스팅은 미시간 대학의 EECS 498-007 강의 내용을 정리한 글입니다. cs231n 강의와 유사하여 해당 시리즈 포스팅과 겹치는 부분이 많이 있을 수 있습니다.

Optimization

Loss function이 주어졌을 때, 이 loss를 만족시키는 가중치 행렬 W에 대한 값을 실제로 어떻게 찾을까?


우리가 가진 가중치 행렬 WW를 넣어서 얻어지는 loss값인 LL을 최소화하는 WW^*를 어떤 방식으로 찾을 수 있을까?


우리 현재 위치의 위도, 경도 값을 WW의 각 요소들로 생각하고, 해당 지점의 높이를 loss값으로 보면 이 과정은 마치 사진과 같은 절경에서 눈을 가리고 밑바닥을 찾아 걸어가는 것으로 생각할 수 있다.


Random Search

가중치 행렬의 많은 값을 무작위로 생성한 다음 각 케이스 별 loss를 평가하고 그 중 최적의 가중치 행렬을 선정할 수도 있다. 이는 생각보단 괜찮은 알고리즘일 수도 있지만, 요즘은 이보다 훨씬 좋은 접근 방식들이 있다.


Follow the slope

사람 여러 명을 랜덤 좌표에 올려놓고 가장 밑바닥에 있는 사람을 선정하는 Random search 대신, 한 사람을 랜덤 좌표에 올려놓고 경사 면을 따라 이동하게 하는 방식이 더 현명한 방법일 수도 있다. 발을 딛는 과정을 계속 반복하면서, 점점 더 낮은 지점을 향해 걸어가게 된다.


1차원에서 미분값을 통해 기울기를 알 수 있고, 그렇다면 다차원에서의 기울기는 각 차원에 따른 편미분들의 벡터가 된다.

특정 방향에서의 기울기는 해당 방향과 gradient의 내적이므로 양수 기울기는 증가, 음수 기울기는 감소 방향이 된다. 우리는 음수 기울기 방향을 택하면 최저점을 향해 걸어갈 수 있다.

그렇다면 특정 입력 벡터에 대한 loss function의 기울기를 어떻게 계산할까?


왼쪽은 우리에게 주어진 현재의 가중치 행렬 WW이고, 오른쪽은 WW에 대한 loss의 gradient를 가지는 행렬이다.

이를 얻으려면 WW에서의 loss값을 구한 다음, 특정 슬롯의 값을 미세하게 증가 시킨 후 새로운 loss를 구한 후 도함수의 극한 개념을 활용하여 해당 슬롯의 loss에 대한 gradient를 얻을 수 있다. 이 과정을 모든 슬롯에 대해서 반복한다.


이 방식을 Numeric Gradient라고 한다.

  • Numeric Gradient 단점
    • 대규모 신경망에서 쓰이게 되면 계산 비용이 너무 늘어나게 된다.
    • 위 예시에서 우리는 hh를 0.0001로 사용했기 때문에, 계산된 gradient는 결국 근사치에 불과하다.
  • Numeric Gradient 장점
    • Gradient를 계산할 때 실제로 무슨 일이 일어나고 있는지에 대해 알 수 있다.

그리하여 Analytic Gradient 방식을 통해 dL/dWdL/dW를 표현할 수 있는 함수를 유도하고, 이를 통해 WW에 대한 Loss의 gradient를 구할 수 있다. (6장에서 backpropagation을 통해 계산 예정)


그리하여 실제로는 Analytic gradient로 gradient 정보를 사용하여 loss를 최소화 하게 되지만, 이는 구현 과정이 복잡하고 계산 실수를 하기 쉽기(error-prone) 때문에 디버깅 도구로써 Numeric gradient로 gradient check를 할 수 있으면 좋다.


PyTorch에서는 gradcheck이라는 함수로 gradient check이 가능하다.


2차 미분 값이 제대로 적용되었는지 확인할 때는 gradgradcheck 함수를 사용하면 된다.


Gradient Descent

임의의 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 # 음의 방향으로 업데이트 진행

하이퍼파라미터는 아래와 같다.

  • Weight initialization method
    • 단순히 랜덤 난수로 초기화할 수도 있지만, 실제론 정확한 매커니즘이나 값의 분포가 매우 큰 영향을 미칠 수 있기 때문에 해당 방법들을 사용하는 것이 좋다(뒤 강의에서 다룰 예정).
  • Number of steps
    • 단순히 일정 횟수만큼 반복하는 간단한 방법을 사용할 수도 있다.
    • 일반적으로 많이 반복할수록 잘 작동하는 경우가 많지만, 일반적으로 계산 비용과 시간에 따라 제한된다.
  • Learning rate
    • Gradient를 계산해서 음의 gradient를 얻은 시점에서 해당 방향으로 얼마나 이동 시킬지 정한다.
    • 값이 클수록 step size가 커서 학습은 빠르게 수렴하지만, overshoot이 발생할 수 있다.

하지만 만약 손실 함수가 타원형을 띄게 된다면 gradient 방향이 최저점으로 가는 방향과는 약간의 차이가 있고, 그 결과 위처럼 한 번 휘어서 최적점에 도달하게 된다.

또한, 학습이 진행되어 최저점에 가까워질수록 주위가 점점 평평해져서 gradient 크기가 작아지고, 자연스레 보폭이 줄어들게 된다.


여태 살펴본 Gradient Descent를 Batch Gradient Descent 또는 Full Batch Gradient Descent라고 한다. Batch Gradient Descent는 모든 학습 데이터의 loss를 합친 전체 loss를 쓰기 때문에, 학습 데이터 수가 많다면 매우 오래 걸릴 수 있다.


Stochastic Gradient Descent

따라서, 전체 학습 데이터셋에 대한 합계를 계산하는 대신, 미니배치를 이용해 loss 함수를 근사하고, gradient를 근사하는 방법이다.

미니배치의 일반적인 크기는 32, 64, 128 등을 사용하는 경우가 많다.

하이퍼파라미터는 아래와 같다.

  • Weight initialization method
  • Number of steps
  • Learning rate

위 세가지는 Full batch gradient descent와 같고, 아래 추가 항목들이 있다.

  • Batch size
    • 덜 민감한 편이며, 가장 일반적인 방법은 본인의 GPU 메모리 크기에 맞춰서 가능한 크게 사용하면 된다.
  • Data sampling
    • 데이터를 무작위로 섞어주거나 반복의 시작마다 데이터를 섞어주는 방법 등이 있다. 이 또한 이미지 분류의 경우 영향이 큰 편은 아니다.

사실 위 설명 방식은 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)을 사용할 지 정해주어야 한다.


Problems with SGD

High condition number

만약 손실함수가 위처럼 생겼고, 가운데 최저점에 도달하려 한다면, 수평 축의 가중치는 변해도 loss가 아주 천천히 줄어들고, 수직 방향의 가중치 변화에 훨씬 더 민감하게 된다. 이런 경우 loss는 bad condition number를 갖고 있다고 하고, 이는 이계도함수를 행렬로 표현한 Hessian matrix의 최대/최소 singular values 값이 비율이 매우 안 좋다는 것이다.


이런 경우 step size가 너무 크다면 loss의 gradient 방향이 고르지 못하여 지그재그 형태를 띠면서 가중치 업데이트가 이루어지게 되고, 이렇게 되면 최적화에 시간이 오래 걸린다. Step size가 작은 경우엔 매우 느리게 수렴하여 최저점에 도달하기 어려워진다.


local minimum / saddle point

local minima

  • 위 그림처럼 손실 함수에서 local minima를 만나게 되면 gradient는 0이 되고, 학습은 멈추게 된다.

saddle point

  • local minima는 아니지만, 아래 그림처럼 평탄한 지역에 빠지게 된다면, 이런 saddle point에서도 gradient 값이 0이 되어 학습이 멈추는 경우가 발생할 수 있다.
  • 차원이 훨씬 더 증가하게 된다면, 이 Saddle point는 local minima보다 훨씬 빈번하게 발생하여 Neural Network를 취약하게 만드는 공신이다.
  • 그리고 이는 saddle point 뿐 아니라, 그 근처에서도 기울기가 매우 작기 때문에 업데이트가 매우 느리게 진행된다.

Noisy gradients

데이터 셋 전체의 loss를 계산하려면 시간이 너무 오래 걸리기 때문에 미니배치의 데이터로 실제 loss를 추정하기만 하는데, 이는 gradient의 부정확한 추정값(noise estimate)을 구하는 것이 되고, 이로 인해 학습 방향이 자주 왜곡될 수 있다. 이로 인해 학습 시간이 늘어나게 된다.


SGD + Momentum

SGD에서 현재 미니 배치의 gradient 방향만 고려하는 것이 아니라 velocity(속도)도 같이 고려하는 것. ρ 라는 하이퍼파라미터를 추가하여 이에 대한 비율도 조절이 가능하다. (보통 0.9나 0.99의 높은 값으로 설정) exponential moving average 방식

velocity의 비율(ρ)을 곱해주고 현재 gradient를 더함. 따라서 gradient vector 그대로의 방향이 아닌 velocity vector의 방향으로 나아가게 된다.

  • 실제로 경사면에서 공을 굴릴 때를 대입해서 생각해보면, 이전 예시와 같은 local minima와 saddle point의 경우에 도달해도 위에서 굴러오던 속도가 아직 남아있고, 따라서 gradient는 0이어도 아직 속도 값이 남아서 learning rate와 곱한 뒤 xx에 더해지기 때문에 학습이 바로 멈추진 않게 된다. → local minima, saddle points 해결
  • Momentum이 지그재그 변동을 서로 상쇄 시켜주고, 이를 통해 loss에 민감한 수직 방향의 변동은 줄여주고 수평 방향의 움직임은 가속화 된다. → Poor Conditioning 해결
  • velocity가 생기게 되면 결국 noise가 평균화 되어 SGD보다 훨씬 부드럽게 minima를 향해 움직인다. → Noisy gradients 해결

두 코드 모두 같은 SGD + Momentum 코드이다.


Nesterov Momentum

SGD + Momentum과 유사하지만, 계산 순서만 바뀌었다. 기본 SGD + Momentum은 현재 지점에서의 gradient를 계산한 뒤 velocity와 합해주지만, Nesterov Momenum은 우선 velocity 방향으로 움직여준 후, 그 지점에서의 gradient를 계산한다. 그리고 다시 원점에서 그 둘을 합해준다.

velocity와 gradient 두 정보를 약간 더 섞어준다고 생각할 수 있고, velocity의 방향이 잘못되었을 경우 현재 gradient의 방향을 좀 더 활용할 수 있도록 해준다.


velocity를 업데이트 할 때 이전 시점의 velocity와 x+ρvx + \rho v 에서의 gradient를 계산하고, step update는 방금 구한 vv를 활용해서 구한다.


이제 xt~\tilde{x_t}xt+ρvtx_t + \rho v_t 로 치환해서 수식을 조금 변경해보면 아래와 같이 나타낼 수 있다. 유도 과정은 아래와 같다.


기존 SGD 방식은 속도가 느린 편이고, Momentum 계열은 이전 velocity의 영향을 받아 minima를 그냥 지나쳐 버리는 경향이 있기는 하지만, 결국 다시 돌아와서 minima에 수렴하는 경향이 있다.

→ 만일 minima가 좁고 깊다면 minima를 지나쳐서 다시 돌아오지 못하는 경우가 발생할 수도 있다. 하지만, 이런 좁고 깊은 minima는 심한 overfitting 문제를 가져올 수 있기 때문에 애초에 선호되지 않는 minima이고(평평한 minima가 일반화를 잘 함), 데이터의 크기가 커질수록 이런 민감한 유형의 minima는 줄어든다고 한다.


AdaGrad

  • 훈련 도중 계산되는 gradient를 제곱하여 gradient squared term에 계속해서 더한다.
  • 그리고 업데이트 시에는 이 gradient squared term의 제곱근을 update term에 나눈 값으로 업데이트 해준다.

Poor Conditioning이 존재하는 경우, 작은 차원에서는 gradient 제곱을 합한 값이 작아지게 되고, update term에 그만큼 작은 값을 나눠주기 때문에 비교적 업데이트가 크게 이루어지고, 반대로 큰 차원에서는 update term에 큰 값을 나눠주기 때문에 업데이트가 작게 된다.

하지만 학습 횟수가 증가하게 된다면 gradient squared term의 값은 점점 커지기 때문에, step size는 점점 작아지게 된다. 이는 Convex(볼록)한 경우에는 장점이 될 수 있지만, saddle point 같은 지점이 존재한다면 학습이 멈추게 될 수 있다. 이는 RMSProp에서 개선되었다.


RMSProp

AdaGrad에서 gradient squared term을 그대로 사용하지만, 단순한 gradient 제곱의 누적이 아니라, 기존 누적 값에 decay_rate를 곱한 값에 현재 gradient 제곱에 1-decay_rate를 곱한 값을 더한다. SGD + Momentum 방식에서도 velocity를 decay하는 하이퍼파라미터 ρ\rho가 있었듯, 비슷한 텀으로 decay_rate를 넣어준다고 보면 된다. 이를 통해 Momentum처럼 exponential moving average 방식을 사용하게 된다.

decay_rate는 보통 0.9 또는 0.99를 사용한다.


Adam

여태까지 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 알고리즘들의 특징을 보기 쉽게 표로 정리한 것.


AdamW

2019년 강의에서는 나오지 않지만, 2020년도 슬라이드에는 Adam에 weight decay를 적용한 AdamW에 대해서도 다룬다.


PRML 책에 있는 그림인데, polynomial regression을 적용할 때 차수가 커지면 각 weight 값이 굉장히 큼직큼직하게 결정되는 것을 확인할 수 있는데, 이로 인해 데이터에 굉장히 민감해지고 오버피팅이 발생하게 된다. 따라서 weight 업데이트를 할 때, 이전 weight의 크기를 일정 비율 감소시키는 기법이 weight decay이다.


First-Order Optimization

여태까지 살펴본 알고리즘들은 모두 loss의 일차 미분 값으로 gradient step을 밟는 First-order optimization이다.


Second-Order Optimization

Second-Order optimization도 존재하는데, 이는 gradient에다가 hessian 정보까지 활용하여 step size를 adaptive하게 결정한다.


위처럼 이차 미분을 통해 얻은 곡률이 낮은 경우에는 step size를 더 크게 적용해줄 수 있다. 이처럼 learning_rate를 robust하게 세팅할 수 있는 아주 좋은 아이디어이다.


하지만 gradient 행렬은 weight 행렬과 같은 크기를 같지만, 헤시안 행렬은 그 제곱 크기를 가지게 되고, 심지어 헤시안 행렬의 역행렬을 사용하려면 O(N3)O(N^3)의 계산량이 필요하게 돼서 실용적이진 못하다.


In practice

보통 실제로 실험을 하게 되면, 일반적으로 Adam을 가장 먼저 써보고, 때때로 Momentum이 더 좋은 결과를 보일 수도 있다.

만약 데이터의 dimension이 크지 않아 full batch update가 가능하다면 L-BFGS(전체 헤시안 역행렬을 이용하는 BFGS에서 개선된 알고리즘) 를 써보자.


References

유튜브 강의: 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

profile
큰 사람이 되겠어요

0개의 댓글