딥러닝

고속 옵티마이저

잔잔한 흐름 2025. 5. 13. 22:53

이 글은 <핸즈온 머신러닝 2판>을 참고하여 만들어졌습니다.

11.3 고속 옵티마이저

아주 큰 신경망의 훈련 속도는 심각하게 느릴 수 있습니다.

지금까지 훈련 속도를 높이는 네 가지 방법에 대해 살펴봤습니다.

  • 좋은 초기화 전략 적용하기
  • 좋은 활성화 함수 사용하기
  • 배치 정규화 사용하기
  • 사전 훈련된 네트워크의 일부 재사용하기

훈련 속도를 크게 높일 수 있는 방법은 더 빠른 옵티마이저를 사용하는 것입니다.

ex. 모멘텀 최적화(momentum optimization), 네스테로프 가속 경사(Nesterov accelerated gradient), AdaGrad, RMSProp, Adam, Nadam 옵티마이저

 

 

11.3.1 모멘텀 최적화

공을 굴릴때 처음에는 느리지만 종단속도(저항 때문에 등속도 운동을 하게 될때의 속도)에 도달할 때까지 빠르게 가속될 것입니다.

<--> 표준적인 경사 하강법은 일정한 크기의 스텝으로 내려갑니다.

 

경사 하강법은 가중치에 대한 비용 함수 J(Θ)의 그레이디언트(∇J(Θ)에 학습률 n을 곱한 것을 차감 => 가중치 Θ를 갱신

( 이전에 그레이디언트가 얼마였는지 고려x)  

 

그러나 모멘텀 최적화는 이전 그레이디언트가 얼마였는지 상당히 중요합니다.

매 반복에서 현재 그레이디언트를 (학습률 n를 곱한 후) 모멘텀 벡터(momentum vector) m에 더하고 이 값을 빼는 방식

다시 말해 가속도를 사용 => "매 스텝에서 속도 벡터(m)"를 누적

but,  모멘텀이 너무 커지는 것을 막기 위해 모멘텀(momentum)이라는 새로운 하이퍼파라미터 β가 등장합니다.

( β는 0과 1 사이, 기본값=0.9)

 

  • 그레이디언트가 일정하다면 종단속도 = n∇J(Θ)  x 1 / 1 - β
  • 모멘텀 최적화는 경사 하강법보다 더 빠르게 평편한 지역을 탈출하게 도와줍니다. (관성 효과 + 기울기 누적)
  • 경사 하강법은 가파른 경사는 꽤 빠르게 내려가지만 좁고 긴 골짜기에서는 오랜 시간이 걸립니다. 
  • 모멘텀 최적화는 바닥(최적점)에 도달할 때까지 점점 더 빠르게 내려갑니다.
  • 배치 정규화를 사용하지 않는 심층 신경망에서 상위층은 종종 스케일이 매우 다른 입력을 받게 됩니다.
    => 하위층의 가중치와 활성화 변화가 누적되면서 상위층 입력의 분포가 점점 불안정해지기 때문입니다.
  • 지역 최적점(local optima)을 건너뛰는데 도움이 됩니다.

※ 모멘텀 때문에 옵티마이저가 최적값에 안정되기 전까지 왔다갔다할 수 있습니다. (시스템에 마찰저항이 있는 것은 좋다)

     진동을 없애고 빠르게 수렴가능하기 때문입니다.

 

구현은 어떻게 할까?

optimizer = keras.optimizers.SGD(lr=0.01, momentum=0.9)

 

 

11.3.2  네스테로프 가속 경사

이 기법은 모멘텀 최적화보다 거의 항상 빠릅니다.

네스테로프 모멘텀 최적화(Nesterov momentum optimization)이란?

: 현재 위치가 Θ가 아니라 모멘텀의 방향으로 조금 앞선 Θ + βm에서 비용 함수 그레이디언트를 계산

  • 모멘텀 벡터는 최적점을 향하는 방향을 가리킬 것이므로 이런 변경이 가능합니다.
  • 현재 위치보다 조금 더 나아가서 측정한 그레이디언트가 조금 더 정확할 것입니다. (다음 그림 참조)
  • 시간이 조금 지나면 이 작은 개선이 쌓여서 NAG가 기본 모멘텀보다 확연이 빨라진다.

<일반 최적화와 네스테로프 모멘텀 최적화>

 

  • 일반 최적화는 모멘텀 단계 이전에 계산한 그레이디언트를 적용
  • 네스테로프 모멘텀 최적화는 그 이후에 계산한 모멘텀을 적용
  • ∇1은 시작점 Θ에서 측정한 비용 함수의 그레이디언트, ∇2는 Θ + βm에서 측정한 그레이디언트
  • 모멘텀이 골짜기를 가로지르도록 가중치에 힘을 가할 때 ∇1은 골짜기를 더 가로지르려고 하고, ∇2는 계곡의 아래쪽으로 잡아당기게 됩니다.

그럼 구현은?

optimizer = keras.optimizers.SGD(lr=0.001, momentum=0.9, nesterov=True)

 

11.3.3  AdaGrad

경사 하강법은 전역 최적점 방향으로 곧장 향하지 않고 가장 가파른 경사를 따라 빠르게 내려가기 시작해서 골짜기 아래로 느리게 이동합니다.

AdaGrad 알고리즘은 가장 가파른 차원을 따라 그레이디언트 벡터의 스케일을 감소시켜 이 문제를 해결합니다.

  1. 그레이디언트의 제곱을 벡터 s에 누적합니다. 
    - 이 벡터화된 식은 벡터 s의 각 원소 s1마다 s1 <- s1 +  (∂J(Θ)/∂(Θ1)^2을 계산하는 것과 동일합니다.
    - si는 파라미터 Θi에 대한 비용 함수의 편미분을 제곱하여 누적합니다.
    - 비용 함수가 i번째 차원을 따라 가파르다면 si는 반복이 진행됨에 따라 더욱 커질 것입니다.
  2. 경사 하강법과 같습니다. 
    한 가지 큰 차이는 그레이디언트 벡터를 √s+ ε으로 나누어 스케일을 조정하는 것입니다. ( ∅ 기호는 원소별 나눗셈을 나타내고, ε은 0으로 나누는 것을 막기 위한 값)

=> 이 알고리즘은 학습률을 감소시키지만 가파른 차원에 대해 더 빠르게 감소됩니다. (=적응적 학습률(adaptive learning rate))

     학습률 하이퍼파라미터 n을 덜 튜닝해도 되는 점이 장점입니다.

 

<AdaGrad와 경사하강법: AdaGrad는 최적점을 향해 일찍 방향을 바꿀 수 있습니다.>

 

  • AdaGrad는 훈련할 때 너무 일찍 멈추는 경우가 종종 있습니다.
  • 학습률이 너무 감소되어 전역 최적점에 도달하기 전에 알고리즘이 완전히 멈춥니다.
  • 심층 신경망에서는 사용 x. 

 

11.3.4  RMSProp

AdaGrad는 너무 빨리 느려져서 전역 최적점에 수렴하지 못하는 위험이 있습니다.

RMSProp 알고리즘은 가장 최근 반복에서 비롯된 그레이디언트만 누적하여 문제를 해결했습니다.

다음 그림과 같이 알고리즘의 첫 번째 단계에서 지수 감소를 사용합니다.

  • 보통 감쇠율 β는 0.9로 설정합니다.
  • 튜닝할 필요 x(기본값이 잘 작동하기 때문)

어떻게 구현할까?

optimizer = keras.optimizers.RMSprop(lr=0.001, rho=0.9)

 

rho 매개변수는 β에 해당합니다.

아주 간단한 문제를 제외하고 이 옵티마이저가 AdaGrad보다 훨씬 더 성능이 좋습니다.

 

11.3.5  Adam과 Nadam 최적화

적응적 모멘트 추정(adaptive moment estimation)을 의미하는 Adam은 

모멘텀 최적화와 RMSProp의 아이디어를 합친 것입니다.

=> 모멘텀 최적화처럼 지난 그레이디언트의 지수 감소 평균(exponetial decaying average)을 따르고 RMSProp처럼 

     지난 그레이디언트 제곱의 지수 감소된 평균을 따릅니다.
     (이는 그레이디언트의 평균과 분산에 대한 예측입니다. 이 평균을 첫 번째 모멘트(first moment)라 부르고 분산은 두 번째 모멘          트(second moment)라 불러 이를 적응적 모멘트 추정이라고 합니다. )

 

<Adam 알고리즘>

  • t는 (1부터 시작하는) 반복 횟수를 나타냅니다.
  • 단계 1,2,5를 보시면 Adam이 모멘텀 최적화, RMSProp과 아주 비슷하다는 것을 알 수 있습니다.
  • 차이 나는 점은 단계 1에서 지수 감소 합 대신 지수 감소 평균을 계산한 것입니다. (지수 감소 평균은 지수 감소 합의 1 - β1배)
  • 단계 3과 4는 기술적인 설명이 필요합니다. (m과 s가 0으로 초기화, 훈련 초기에는 0으로 치우침) => 증폭시키는 데 도움

모멘텀 감쇠 하이퍼파라미터 β1은 0.9로 초기화

스케일 감쇠 하이퍼파라미터 β2는 0.999로 초기화

=> ε은 아주 작은 수로 초기화(Adam 클래스의 기본값)

 

그럼 어떻게 Adam 옵티마이저를 어떻게 만들까?

optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999)

=> Adam이 (AdaGrad나 RMSProp처럼) 적응적 학습률 알고리즘(하이퍼파라미터 튜닝할 필요가 적음)

     (대부분 문제에서 안정적인 학습을 보장해주기 때문에)

 

다음은 Adam의 두 가지 변종에 대해 알아보겠습니다.

AdaMax

  • 위의 Adam 알고리즘 식의 2단계를 보면 s에 그레이디언트의 제곱을 누적합니다.
  • 또한 5단계에서 ε과 단계 3,4를 무시하면 Adam은 s의 제곱근으로 파라미터 업데이트의 스케일을 낮춥니다.
  • Adam은 시간에 따라 감쇠된 그레이디언트의 l2 노름으로 파라미터 업데이트의 스케일을 낮춥니다.
  • AdaMax는 l2 노름을 l∞ 노름으로 바꿉니다.
  • 식을 관찰했을때 단계 2를 s <- max로 바꾸고 단계 4를 삭제합니다.
  • 단계 5에서 s에 비례하여 그레이디언트 업데이트의 스케일을 낮춥니다. (실제 Adam 성능이 낮아짐)

Nadam

  • Adam 옵티마이저 + 네스테로프 기법
  • 종종 Adam보다 더 빠르게 수렴(일반적으로 Adam보다 성능이 좋았지만 RMSProp이 나올 때도 있었음)

 

지금까지는 1차 편미분에만 의존했지만 최적화 이론에는 2차 편미분을 기반으로 한 뛰어난 알고리즘들이 존재합니다.

 

불행히도 이런 알고리즘은 심층 신경망에 적용하기가 매우 어렵습니다.

=> 이런 알고리즘은 하나의 출력마다 n개의 1차 편미분이 아니라 n개의 2차 편미분을 계산해야 되기 때문입니다.

 

DNN은 전형적으로 수만 개의 파라미터를 가지므로 2차 편미분 최적화 알고리즘은 메모리 용량을 넘어서는 경우도 많고 해시안 계산은 매우 느립니다.

 

 

최소 모델 훈련

  • 모든 최적화 알고리즘은 대부분의 파라미터가 0이 아닌 밀집(dense) 모델을 만듭니다.
  • 매우 빠른 모델이 필요하거나 메모리를 적게 차지하는 모델이 필요하면 희소(sparse) 모델을 만듭니다.
  • 간단한 방법은 작은 값의 가중치를 제거(모델의 성능을 낮추기)
  • l1 규제를 강하게 적용(옵티마이저가 가능한 한 많은 가중치를 0으로 강제)
  • 텐서플로 모델화 툴킷(TF-MOT)

 

11.3.6  학습률 스케줄링

좋은 학습률을 찾는 것이 매우 중요합니다. 

=> 너무 크면 훈련이 발산, 너무 작으면 최적점에 수렴

만약 조금 높게 잡으면 처음에는 매우 빠르게 진행하지만, 최적점 근처에는 요동이 심해 수렴하지 못합니다.

  • 컴퓨터 자원이 한정적이라면 차선의 솔루션을 만들기 위해 완전히 수렴하기 전에 훈련을 멈추어야 합니다.
  • 매우 작은 값에서 매우 큰 값까지 지수적으로 학습률을 증가시키면서 모델 훈련을 수백 번 반복하여 좋은 학습률을 찾을 수 잇습니다
  • 다음 학습 곡선을 살펴보고 다시 상승하는 곡선보다 조금 더 작은 학습률을 선택합니다.
  • 모델을 다시 초기화하고 이 학습률로 훈련합니다
  • 큰 학습률로 시작하고 학습 속도가 느려질때 학습률을 낮추면 최적의 고정 학습률보다 좋은 솔루션을 빨리 발견할 수 있습니다.
  • 훈련하는 동안 학습률을 감소시키는 전략 : 학습 스케줄(learning schedule)

학습 스케줄에 어떤 것들이 있을까?

  • 거듭제곱 기반 스케줄링(power scheduling)
    • 학습률을 반복 횟수 t에 대한 함수 n(t) = n0 / (1 + t/s)^c로 지정합니다.
    • 초기 학습률 n0 거듭제곱 수 c(일반적으로 1로 지정).
    • 스텝 횟수 s는 하이퍼파라미터
    • 학습률은 각 스텝마다 감소 (s번 스텝 뒤에 학습률은 n0/2로 줄어듬)
    • 처음에는 빠르게 감소하다가 점점 더 느리게 감소
    • n0과 s를 (아마 c도) 튜닝
  • 지수 기반 스케줄링(exponetial scheduling)
    • 학습률을 n(t) = n0 0.1 ^ t/s로 설정
    • 학습률이 s 스텝마다 10배씩 점차 줄어듬
    • s번 스텝마다 계속 10배씩 감소
  • 구간별 고정 스케줄링(piecewise constant scheduling)
    • 일정 횟수의 에포크 동안 일정한 학습률을 사용하고 그다음 또 다른 횟수의 에포크 동안 작은 학습률을 사용하는 식
  • 성능 기반 스케줄링(performance scheduling)
    • 매 N 스텝마다 (조기 종료처럼) 검증 오차를 측정하고 오차가 줄어들지 않으면 λ배만큼 학습률을 감소시킵니다.
  • 1사이클 스케줄링(1cycle scheduling)
    • 1사이클(1cycle)은 훈련 절반 동안 초기 학습률 n0을 선형적으로 n1까지 증가시킵니다.
    • 그다음 절반 동안 선형적으로 학습률을 n0까지 다시 줄입니다.
    • 마지막 몇 번의 에포크는 학습률을 소수점 몇 째 자리까지 줄입니다.
    • 최대 학습률 n1은 최적의 학습률을 찾을 때와 같은 방식을 사용해 선택하고 초기 학습률 n0은 대략 10배 정도 낮은 값을 선택합니다.
    • 모멘텀을 사용할 때는 처음에 높은 모멘텀으로 시작해서 훈련의 처음 절반 동안 낮은 모멘텀으로 줄어듭니다.
    • 그다음 훈련 절반 동안 최댓값으로 되돌립니다. 마지막 에포크는 최댓값으로 진행

=> 결국 튜닝이 쉽고 최적점에 조금 더 빨리 수렴하는 지수 기반 스케줄링은 선호됩니다.

 

그럼 거듭제곱 기반 스케줄링 구현은?

옵티마이저를 만들 때 decay 매개변수만 지정하면 됩니다.

from tensorflow import keras
optimizer = keras.optimizers.SGD(lr=0.01, decay=1e-4)

 

=> decay는 s(학습률을 나누기 위해 수행할 스텝 수)의 역수 (케라스는 c를 1로 가정)

 

먼저 현재 에포크를 받아 학습률을 반환하는 함수를 정의해야 합니다.

def exponetial_decay_fn(epoch):
    return 0.01*0.1*(epoch/20)

 

n0와 s를 하드코딩하고 싶지 않다면 이 변수를 설정한 클로저(closure)를 반환하는 함수를 만들 수 있습니다.

def exponetial_decay(lr0, s):
    def exponetial_decay_fn(epoch):
        return lr0*0.1**(epoch / s)
    return exponetial_decay_fn

exponetial_decay_fn = exponetial_decay(lr0=0.01, s=20)

 

그 다음 이 스케줄링 함수를 전달하여 LearningRateScheduler 콜백을 만듭니다. 그리고 이 콜백을 fit() 메서드에 전달합니다.

lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])

 

  • LearningRateScheduler는 에포크를 시작할 때마다 옵티마이저의 learning_rate 속성을 업데이트합니다.
  • 에포크마다 한 번씩 스케줄을 업데이트해도 충분합니다. 더 자주 업데이트하고 싶으면 사용자 정의 콜백을 만들 수 있습니다.
  • 스텝이 많다면 스텝마다 학습률을 업데이트하는 것이 좋습니다.

스케줄 함수는 두번째 매개변수로 현재 학습률을 받을 수 있습니다.

 

  • 모델을 저장할 때 옵티마이저와 학습률이 함께 저장됩니다. 
  • 새로운 스케줄 함수를 사용할 때도 문제 없이 훈련된 모델을 로드하여 중지된 지점부터 훈련을 계속 진행할 수 있습니다.
  • but, 스케줄 함수가 epoch 매개변수를 사용하면 문제가 복잡해집니다.
    • 에포크는 저장되지 않고 fit() 메서드를 호출할 때마다 0으로 초기화
    • 중지된 지점부터 모델 훈련을 이어가려 한다면 학습률이 너무 높아져 모델의 가중치를 망가뜨림
    • epoch에서 시작하도록 fit() 메서드의 inital_epoch 매개변수를 수동으로 지정

 

구간별 고정 스케줄링을 위해서는 다음과 같은 스케줄 함수를 수용할 수 있습니다.

 

def piecwise_constant_fn(epoch):
    if epoch < 5:
        return 0.01
    elif epoch < 15:
        return 0.005
    else:
        return 0.001

 

성능 기반 스케줄링을 위해서는 ReduceLROnPlatequ 콜백을 사용합니다.

ex. 다음 콜백을 fit() 메서드에 전달하면 최상의 검증 손실이 다섯 번의 연속적인 에포크 동안 향상되지 않을 때마다 학습률에 0.5를 곱합니다.

lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)

 

마지막으로 tf.keras는 학습률 스케줄링을 위한 또 다른 방법을 제공합니다.

keras.optimizers.schedules에 있는 스케줄 하나를 사용해 학습률을 정의 => 이 학습률을 옵티마이저에 전달

=> 매 스탭마다 학습률을 업데이트

 

또한 모델을 저장할 때 학습률과 (현재 상태를 포함한) 스케줄도 함께 저장합니다.

이 방식은 표준 Keras API는 아니며 tf.keras에서만 지원합니다.

 

결론적으로 1사이클 방식을 사용하기 위한 큰 어려움은 없고 매 반복마다 

학습률을 조정하는 사용자 정의 콜백을 만들면 됩니다!!!!