[CS231n] Training Neural Networks Part 2 - SGD, Momentum, Adam, Dropout

2024. 10. 15. 21:13카테고리 없음

 

 

https://www.youtube.com/watch?v=vT1JzLTH4G4&list=PLC1qU-LWwrF64f4QKQT-Vg5Wr4qEE1Zxk&index=3

스탠포드 대학교의 유명한 컴퓨터 비전 강의를 보고 정리한 글입니다.

https://www.youtube.com/playlist?list=PLetSlH8YjIfXMONyPC1t3uuDlc1Mc5F1A

CS231n 강의와 서울대학교 DSBA 연구실의 강의를 참고하여 공부한 부분을 정리하였습니다.

이번 강의를 들으면서 처음 AI 공부를 시작했고, 그만큼 틀린 부분 해석이 모호한 부분이 있을 수 있습니다.


 

6. Training Neural Networks Part 2

1. Mini-batch SGD

딥러닝 모델을 학습할 때 사용되는 최적화 알고리즘은 크게 세 개 존재한다.

Gradient Descent는 전체 데이터를 사용하여 손실함수의 기울기를 계산하고 가중치를 업데이트 하는 방식이고, Stochastic Gradient Descent는 하나의 데이터 샘플을 사용한다.

Mini-batch Stochastic Gradient Descent, 줄여서 Mini-batch SGD는 mini-batch를 사용하여 손실 함수의 기울기를 계산하고 가중치를 업데이트 하는 방식이다.

Mini-batch SGD는 다음과 같은 Loop를 가진다.

  1. batch of data의 sample을 구한다.
  2. graph의 forward prop을 통해 loss를 구한다.
  3. 역전파 과정, 즉 loss를 가지고 각각 파라미터에 대해 local gradeint를 학습할 수 있다.
  4. 각각 파라미터를 gradient를 사용해서 업데이트한다.

간단하게 위와 같은 코드를 생각해볼 수 있다. data_batch를 구하고 forward() 함수에서 loss를 구한 뒤 backward() 함수에서 x의 gradient를 구한다. 그리고 x를 learning_rate 와 곱해서 업데이트하게 된다.

업데이트하는 부분을 생각해보면 learning_rate 하나만을 가지고 굉장히 많은 파라미터를 업데이트 하게 된다. 파라미터는 굉장히 많은데 learning_rate 하나만으로는 충분하지 않을 수 있다.


2. Parameter Updates

Gradient Descent

 

위의 그래프는 3차원 함수의 등고선이다. 이 함수는 최적화 문제의 손실 함수를 나타낸다.

그래프의 x, y 축은 2차원 파라미터 공간을 나타낸다. 그래프의 각 점은 특정 파라미터 값의 조합을 나타낸다. 참고로 여기서 파라미터는 input data가 아니라 모델이 학습하는 가중치와 편향을 말한다.

최적화 알고리즘의 목표는 이 파라미터 공간에서 최적의 파라미터를 찾는 것이고, z축은 각 파라미터 조합에 대한 손실값을 나타낸다.

그림에서 Long valley는 최적의 파라미터 값이 좁고 긴 valley 영역에 분포해있음을 나타낸다. 이러한 지형에서는 SGD와 같은 일부 알고리즘이 최적점을 찾는데 어려움을 겪을 수 있다.

Momentum (관성)

GD 방법론은 다음과 같은 방식으로 업데이트가 이루어 졌다.

x += - learning_rate * dx

Momentum 방법론은 mu라는 hyperparameter를 하나 더 써서 관성을 주는 방법이다.

전 iteration에서 움직인만큼의 방향과 크기에 대해 mu 값을 보존을 하고 그 다음 iteration 업데이트에 반영을 하게 된다.

수식은 다음과 같다.

v = mu * v - learning_rate * dx
x += v

v의 값이 처음에는 0으로 초기화된다. 따라서 처음 iteration 에서는 GD와 똑같이 파라미터를 업데이트 하고, 그 다음 iteration에서 v의 값이 살아있기에 그 전 iteration의 움직임에 대해 mu라는 hyperparamer 만큼 곱해져서 움직이게 된다.

 

위의 그래프를 보면, momentum 같은 경우는 optimum point 보다 뒤로 갔다가 다시 돌아온다. 관성 때문이다.

아래의 그래프를 보면 SGD보다 momentum이 초반 target에 접근하는 속도가 더 빠르다. SGD의 약점을 보완한 방식인 것이다.

Nesterov Accelerated Gradient

Momentum은 두 벡터의 합을 이용해서 구했다. Momemtum에서는 현재 위치에서 gradient를 계산하고 이전 momentum과 local gradient를 조합하여 다음 step을 계산하였다.

순서가 local gradient 계산 → momentum 적용 → 이동인 것이다.

 

NAG는 먼저 momentum 방향으로 이동시켰을 때 위치를 예측하고, 예측 위치에서 local gradient를 구한다. 그리고 계산된 gradient와 momentum을 조합하여 최종 업데이트를 수행하게 된다.

순서가 momentum 적용 → local gradient 계산 → 최종 이동인 것이다.

 

수식은 다음과 같다.

$$ v_t = \mu v_{t-1} - \epsilon \nabla f(\theta {t-1} + \mu v{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

NAG는 long valley 상황에서 더 효과적이고 잘못된 방향으로 가고 있을 때 NAG가 더 빠르게 교정할 수 있다.

 

Momentum은 그래프상에서 약간 더 관성이 있는 움직임을 보이고 NAG는 더 직접적으로 최소점을 향해 가는 것을 볼 수 있다.

AdaGrad

각각의 iteration에서 모든 parameter에 대한 gradient가 계산된다.

각 파라미터에 대해, 지금까지 계산된 모든 gradient의 제곱을 cache에 적재한다.

cache를 분모 term에 넣고 루트를 씌워 다시 local gradient와 learning_rate를 곱해서 업데이트 한다. 1e-7은 epsilon의 역할로, 분모가 0이 되지 않도록하는 역할을 한다.

 

수식은 다음과 같다.

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

Adagrad는 파라미터마다 다른 학습률을 적용하여 자주 업데이트되는 파라미터는 작은 학습률을, 드물게 업데이트되는 파라미터는 큰 학습률을 사용하게 한다.

수식을 보면 cache는 각 파라미터마다 별도로 유지되는 값이다. 자주 업데이트되는 파라미터는 dx가 cache가 자주 계산되어서 커진다는 뜻이고, learning_rate에 나눠지는 값이 커져 작은 학습률을 사용하는 것과 똑같다는 말이다.

주요 특징은 시간이 지남에 따라 학습률이 자연스레 감소하고 각 파라미터 차원에 대해 개별적으로 학습률을 조정할 수 있다.

그런데 여기서 step size가 굉장히 커진다면 어떻게 될까? 즉, step size = learning_rate * dx / (np.sqrt(cache) + 1e-7) 가 굉장히 커진다면 어떻게 될까?

cache object를 생각해보면 증가함수로 꼴로 되어있어 step size가 무조건 점점 작아지게 되어서 학습이 거의 멈출 수 있고 새로운 패턴이나 변화에 적응하기 어렵다는 문제가 있다.

RMSProp

AdaGrad의 문제를 해결하기 위해 고안되었다.

iteration이 많아지면 분모텀이 많아져서 점점 학습률이 작아지고, 무조건적으로 cache가 작아지는 것을 막기 위해 batch-normalization에서도 사용하였던 Exponential Moving Average, EMA를 사용한다.

 

수식을 살펴보자.

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

decay_rate는 hyperparameter로 보통 0.99로 설정한다. decay_rate를 크게 할수록 window를 크게 사용하는 것과 일맥상통하다. 즉, dacay_rate가 클수록 과거 데이터가 영향을 더 미치는 것이다.

참고로 여기서 window는 현재 시점에서 계산이나 분석에 사용되는 데이터의 범위를 의미한다. 보통 두 가지 유형이 존재한다.

  1. 고정 윈도우
    • 정해진 수의 가장 최근 데이터 포인터만 고려한다.
    • 최근 10일간 주가 데이터라면 window 크기는 10이 된다.
  2. 가중 윈도우
    • 모든 과거 데이터를 고려하지만, 각 데이터 포인트에 다른 가중치를 부여한다.
    • RMSprop에서도 가중 윈도우가 사용되고 decay_rate가 그 역할이다.

dacay_rate가 크다면 과거 데이터인 cache가 더 크게 더해지고, 현재 데이터인 dx의 제곱이 더 작게 더해진다.

한 가지 고려할 수 있는 사항이 있는데 Exponential Moving Average이 아니라 고정된 크기의 window로 moving window 기법을 사용할 수는 없을까?

  • 파라미터 자체가 보통 몇천만개의 단위가 된다. 그럼 3번의 iteration만 일어나도 굉장히 큰 메모리를 차지하게 되고, 따라서 실전에서 사용하지 않는 것이다.
  • 고정 크기 윈도우를 사용한다면 각 파라미터에 대해 window 크기만큼 값을 저장해야 한다. 하지만 RMSProp 방식은 파라미터별로 하나의 값만 저장하면 된다.

Adam

요즘 가장 많이 사용되는 Optimization 방법이다. momentum과 RMSProp를 같이 사용하는 방법이다.

 

수식을 바로 보자.

m, v = #... initialize caches to zeros
for t in xrange(1, big_number):
	dx = #... evaluate gradient
	m = beta1 * m + (1 - beta1) * dx # update first moment
	v = beta2 * v + (1 - beta2) * (dx ** 2) # update second moment
	mb = m/(1-beta1**t) # correct bias
	vb = v/(1-beta2**t) # correct bias
	x += - learning_rate * mb / (np.sqrt(vb) + 1e-7)

m = beta1 * m + (1 - beta1) * dx # update first moment

  • momentum 계산

v = bets2 * v + (1 - beta2) * (dx ** 2) # update second moment

  • RMSProp 계산

mb = m/(1-beta1**t) # correct bias vb = v/(1-beta2**t) # correct bias

  • m, v는 지수 이동 평균을 사용한다. 학습 초기에는 m, v가 0으로 초기화되어있어 초기 몇 번의 iteration에서 m과 v가 실제 값보다 적게 추정된다.
  • 따라서 iteration이 작을 때 m, v 값을 큰 값으로 조정해주고 iteration이 진행될수록 보정 값을 줄인다.

각 hyperparameter를 다시 정리해보자.

 

beta1

  • 0.9로 초기화한다.
  • beta1 값이 클수록 과거 정보를 더 많이 반영한다. 그만큼 더 안정적이지만 느리다.

beta2

  • 0.999로 초기화한다.
  • beta2 값이 클수록 과거 정보를 더 많이 기억한다.

learning_rate

  • 0.001로 초기화한다. 클수록 파라미터가 더 크게 업데이트 된다.

Learning Rate Optimization

epoch가 진행될수록 Loss가 내려가고, Loss가 내려가다 보면 업데이트가 되는 구간이 좀 더 세밀해져야 한다.

따라서 여러 방법으로 learning rate를 조정해야 하고 대표적인 방법으로 exponential decay가 있다. 

 

수식은 다음과 같다.

$$ \alpha = \alpha_0e^{-kt} $$

 

어떤 decay 방법을 사용하더라도 끝에 가서 담금질 역할을 하게 된다. 처음에는 Adam으로 빠르게 Loss 값을 감소시키고, 끝에는 Gradient Descent 방법으로 천천히 담금질을 하는 과정을 사용하기도 한다.

왜냐하면 우리가 사용하는 parameter는 특정 iteration 시점에서의 parameter 이기에 이 담금질, dacay 하는 과정이 상당히 중요하다.


3. Model Ensembles

iteration 별로 학습을 해서 가는 경우, check point를 설정해서 weight를 저장해두고 사용할 수 있다.

여기서 강조하는 부분도 여러 point를 save해두고 ensemble을 하자는 것이다.

여러 모델의 예측을 결합하여 개별 모델의 과적합 위험을 감소시키는 장점이 있다. 예를 들어 세 개의 point가 같은 loss를 가진다면, 그 point가 가지는 error point는 다르므로 ensemble 해서 사용하면 좋다.


4. Dropout

왼쪽 그림이 표준 신경망인데, Dropout을 사용하게 되면 layer 단위로 node의 percentage를 제한하여 사용하게 된다.

Dropout은 신경망의 과적합을 방지하기 위한 정규화 기법이다. 학습 과정에서 무작위로 일부 뉴런을 drop out 시킨다. 이는 네트워크가 특정 뉴런에 과도하게 의존하는 것을 방지한다.

 

코드로 보자.

p = 0.5 # probability of keeping a unit active, higher = less dropout

def train_step(X)
	""" X contaions the data """
	H1 = np.maximum(0, np.dot(W1, X) + b1)
	U1 = np.random.rand(*H1.shape) < p # first dropout mask
	H1 *= U1 # drop!
	H2 = np.maximum(0, np.dot(W2, H1) + b2)
	U2 = np.random.rand(*H2.shape) < p # second dropout mask
	H2 *= U2 # drop!
	out - np.dot(W3, H2) + b3

p가 전체 노드 중 몇 퍼센트를 masking 할지 결정하는 hyperparameter이다.

 

H1 = np.maximum(0, np.dot(W1, X) + b1)

  • 가중치 행렬 W1과 입력 X의 행렬 곱 계산

U1 = np.random.rand(*H1.shape) < p

  • H1과 같은 크기의 0~1 사이 무작위 값을 생성하고, p 보다 작은 값은 True, 그렇지 않으면 False인 boolean mask 생성

Dropout은 학습 시에만 적용되고 추론 시에는 사용되지 않는다.

 

Dropout의 중요한 특성 중 하나로 앙상블 효과가 있다.

  1. 서브네트워크 생성
    • 매 학습 단계마다 일부 뉴런이 무작위로 비활성화되어 서브네트워크를 만드는 효과가 있다.
  2. 다양한 모델 학습
    • 각 학습 단계는 서로 다른 서브네트워크로 이루어져 하나의 네트워크로 여러 다른 모델을 동시에 학습하는 효과가 있다.
  3. 평균화 효과
    • 추론 시에는 모든 뉴런을 사용하여 학습된 여러 서브네트워크의 평균적인 예측을 얻는 것과 유사하다.