[PyTorch] Optimizer & LR Scheduler 정리

2024. 8. 17. 22:32Developers 공간 [Basic]/Software Basic

728x90
반응형

이번 글에서는 학습에 활용되는 OptimizerLearning Rate Scheduler를 살펴보고자 합니다.

 

보통 PyTorch를 활용해 학습을 하는 경우 epoch와 step에 따라서 아래 코드와 같이 구현을 하곤합니다.

** Pytorch Tutorial : https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate

** Pytorch Lightning : https://lightning.ai/docs/pytorch/stable/common/optimization.html#automatic-optimization

from torch import optim
from torch.optim.lr_scheduler import ExponentialLR

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = ExponentialLR(optimizer, gamma=0.9)

for epoch in range(20):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler.step()

 

즉, Optimizer와 "optimizer 기반의 Learning Rate Scheduler"를 선언해주고 난 뒤에, step이 끝날 때마다 optimizer.step()을 활용해 weight를 update하고, epoch가 끝날 때마다 scheduler.step()을 활용해 learning rate를 변경해줍니다.

** 하지만 선택에 따라 epoch마다 업데이트 하는 것이 아닌 step마다 scheduler.step()을 통해 Learning Rate를 업데이트를 하기도 합니다.

 

따라서 이번 글에서는 다양한 Optimizer에 대해 살펴보고, Learning Rate Scheduler까지 살펴보려고 합니다.

<구성>
1. Optimizers 
    a. Gradient Descent
    b. Momentum & Adaptive
    c. Adam 계열
2. Learning Rate Schedulers
    a. Terminology
    b. PyTorch Schedulers
    c. Advanced

글효과 분류1 : 코드

글효과 분류2 : 폴더/파일

글효과 분류3 : 용어설명

글효과 분류4 : 글 내 참조

글효과 분류5 : 글 내 참조2

글효과 분류6 : 글 내 참조3


1. Optimizers

 

Optimizer모델을 학습하기 위해 최적화를 진행하는 "객체"입니다.

 

즉, Optimizer를 통해 weight를 얼마나 update할지를 얻어낼 수 있기 때문에 이에 대해 먼저 살펴보려고 합니다.


a. Gradient Descent

 

특정 Cost function(Loss function)을 최소화하는 방향으로 파라미터를 iterative하게 적응시키는 최적화 방법을 GD(Gradient Descent)라고 합니다.

 

즉, cost의 minimum point를 찾기 위해 미분 값(gradient)을 활용해 작은 움직임으로 나아가겠다는 것이고, 그 미분 값을 얼마나 적용할지를 결정하는 것이 Learning Rate입니다.

$$\underbrace{W^{t+1}}_{\text{next params}}=\underbrace{W^t}_{\text{current params}}-\underbrace{{\color{red}\underbrace{\eta}_{\text{learning rate}}}\underbrace{\nabla f(W^t)}_{\text{gradient of cost function}}}_{\text{step}}$$

 

가장 기본적인 Batch GD(=Vanila GD)는 한 epoch에 전체 데이터셋의 gradient 평균을 구하고 한번에 update를 진행하는 방법입니다. 하지만 매 epoch마다 전체 데이터의 gradient를 계산해야하기 때문에 업데이트가 느리고, suboptimal에 빠질 위험이 있어 Stochastic GD나 Mini-batch GD를 사용합니다.

  • Stochastic GD : 하나의 데이터셋에 대한 gradient로 update를 진행합니다.
  • Mini-batch GD : batch에 대한 gradient 평균으로 update를 진행합니다.

보통은 mini-batch GD를 사용하며 pyTorchSGD()는 실제로는 이름과 다르게 Mini Batch GD로도 동작합니다.

** batch와 관련된 용어를 자세히 알고 싶으시면 아래 더보기를 참조하세요

더보기

------------------------------------------------------------------------------------------

 <Batch와 관련된 용어 정리>

 

데이터를 학습하는 과정에서 training data를 선택하고 나면 mini batch 여러개로 나누어 학습을 진행합니다. 아래 그림과 같이 전체 데이터를 한번 학습하고 나면 1epoch가 끝났다고 볼 수 있습니다.

** 특정 batch 사이즈를 multi-GPU로 동작하는 경우, torch.utils.data.DataLoader()에 넣어주는 batch 사이즈는 "GPU 1개당 batch 사이즈"로 실제 effective batch size(GPU개수 x batch 사이즈)입니다.

단, 이런 분산환경의 경우 torch.utils.data.distributed.DistributedSampler를 sampler로 넣어주어야 데이터가 split되어 잘 들어갑니다. (pytorch-lightning에서는 자체적으로 할당을 해줍니다.)

[학습 과정의 batch 관련 용어]

 

첨언으로, 위 그림 중 가장 위에 Training Data와 Validation Data를 분리하는 과정이 있는데, 아래와 같은 방법들이 있습니다.

  • Hold-Out : 임의로 특정 validation data를 직접 분리해 정해놓는 방법입니다. 하지만 이는 validation set에만 overfitting되는 하이퍼파라미터를 선택하게 될 수 있습니다.
  • Cross Validation : overfitting을 피하기 위해 valdiation set을 여러가지로 나누는 방법입니다.
    • K-Fold : k가지의 (1/k 전체데이터)를 준비해, 모두에 대해 validation을 진행합니다. 가장 일반적으로 많이 사용됩니다.
      ** Stratified K-Fold : data balance를 고려해 k-fold를 만들어 줍니다.
      ** Repeated K-Fold : k-fold를 여러번 반복하는 방법입니다.
    • Leave-One-Out : K-Fold의 K가 전체 데이터 개수와 같을 때의 경우로, 단 1개의 데이터로 돌아가면서 validation을 진행하는 경우입니다.
    • Repeated Random Sub Sampling : 랜덤하게 선택된 데이터들을 정해진 n번의 iteration을 통해 valdiation을 진행합니다.
    • TimeSeries : 시계열 데이터의 검증에 적합하게, 해당 epoch의 train dataset의 학습 상황에 맞도록 시간적으로 이후에 나와야할 데이터로 검증을 진행합니다.

------------------------------------------------------------------------------------------

 

이제 학습을 진행하면되는데, 딥러닝에는 아시다시피 아래와 같은 다양한 문제가 존재합니다.

  • Not Optimized for Cost Function : 아래 그림과 같이 Cost function의 global minimum을 찾지 못하는 문제입니다.
    • local minimum : gradient가 0 이지만, 전체적으로 보면 최적의 point는 아닙니다.
    • saddle point : gadient가 0 이지만, 다른 plane에서는 최적의 point는 아닙니다.
  • Not Optimized for Data : 학습 중 데이터에 잘못 adapt되거나, 데이터 자체의 문제가 있는 경우 입니다.
    • overfitting : generalized되지 않고 학습데이터에 너무 치우치게 학습이 된 경우입니다. 보통 학습이 너무 길거나 데이터에 비해 파라미터가 너무 많아 일어납니다.
      ** Regularization, Dropout, Early Stopping, Data Augmentation등의 방법으로 해결합니다. Regularization에 대해 아래에서 다루겠습니다.
    • underfitting : data의 정보를 확실히 못 얻어낸 경우입니다. 보통 너무 모델이 단순하거나 학습이 충분하지 않아 일어납니다.
      ** model complexity를 증가시키거나, 학습을 길게하거나, 더 적합한 optimizer를 통해 해결합니다. 
    • Insufficient & Imbalanced Data : 데이터가 너무 적거나, 한쪽에 치중된 데이터를 활용하는 경우입니다.
  • Network Related : 학습할 네트워크의 구조적 문제로 학습이 안된 문제입니다.
    • Vanishing Gradients : backprogation 상황에서 gradient가 사라지는 경우로, 보통 모델이 너무 크거나 중간 feature의 범위가 너무 넓어 일어납니다.
      ** weight initialization 방법을 바꿔주거나, activation function과 batch normalization layer를 활용해 consistent한 중간 값을 만들어주어 해결합니다.
    • Exploding Gradients : backprogation 상황에서 gradient가 너무 커지는 경우 입니다. 
  • Performance Related : 학습 속도가 너무 느린 문제입니다.
    • Bulky data : 데이터가 너무 많아 학습에 시간이 오래걸리는 경우입니다.
      ** mini batch를 활용하거나 병렬화를 활용해 학습합니다.
    • Convergence Speed : 학습은 빠르게 진행되지만 모델이 converge하는데 시간이 너무 오래 걸리는 경우입니다.

[https://machinemindscape.com/understanding-optimization-algorithms-in-deep-learning/]

 

위와 관련된 여러가지 문제를 해결하기 위해 아래와 같은 다양한 Optimizer들이 등장합니다. 이들 중 PyTorch와 관련된 Optimizer를 다음 챕터부터 살펴보겠습니다.

[다양한 Optimizer의 진화]


b. Momentum & Adaptive 

 

먼저 위 그림의 오른쪽 빨간 화살표를 살펴보겠습니다.

 

기존 SGD는 local minimum나 saddle point에 빠질 위험이 있는데, 이를 해결하기 위해 step size를 늘려주자니 convergence하지 않고 발산할 확률이 높아집니다.

 

따라서 다른 방법으로 EMA(Exponential Moving Average)EWMA(Exponential Weighted Moving Average) 방법을 도입한 momentum 방법이 나옵니다.

  • Momentum : update를 할 때 “현재 weight에 대한 값만 반영"하는 것이 아니라,  “이전 iteration step에서 update했던 벡터(관성, velocity) $v_t$도 반영"하는 방법입니다.
    $$\begin{aligned}
    {\color{red}v_{t+1}}&={\color{red}\rho v_t}-\eta\nabla f(W^t)\\
    W^{t+1}&=W^t+{\color{red}v_{t+1}}
    \end{aligned}$$
  • NAG(Nesterov Accelerated Gradient) : velocity를 적용하는 gradient를 만들어내는 새로운 방식을  제안한 논문입니다.
    $$\begin{aligned}
    {v_{t+1}}&={\rho v_t}-\eta\nabla f(W^t-{\color{red}\rho v_t})\\
    W^{t+1}&=W^t+{v_{t+1}}
    \end{aligned}$$

 

다음으로 위 그림의 왼쪽 초록색 화살표를 살펴보겠습니다.

 

 “많이 가본 방향”보다 “안 가본 방향”의 step size를 조절하는 LR(Learning Rate) Decay 방법을 활용해 업데이트를 하는 Adaptive 방법이 등장합니다.

** 일반적으로 LR decay값이 클수록 빠르게 LR이 줄어듭니다.

  • AdaGrad(Adaptive Gradient): 여태 진행했던 gradient를 누적해 learning rate에 반영함으로써 줄어가는 learning rate의 방향 별로 그 양을 조절해주는 것입니다. 이 방법을 활용해 Loss function이 Convex(볼록)한 경우에는 효율적으로 사용할 수 있습니다.
    ** 이때 $\epsilon$은 0으로 나눠지는 것을 대비한 값입니다.
    $$\begin{aligned}
    {\color{red}G_{t+1}}&={\color{red}G_t}+(\nabla f(W^t))^2=\sum(\nabla f(W^t))^2\\
    W^{t+1}&=W^t-\frac{\eta}{\sqrt{{\color{red}G_{t+1}}+\epsilon}}\cdot\nabla f(W^t)
    \end{aligned}$$
  • RMSProp(Root Mean Square Propagation) : AdaGrad를 Non-convex한 loss function에 적용하는 경우 minima에 가까워질수록 느려지므로 실제로는 global minima가 아닌 saddle point에 갇힐 수 있습니다.
    이는 실제로 Learning rate가 빨리 줄어들어 step size가 굉장히 빠르게 작아지기 때문이고, RMSProp은 Gradient를 누적할 때 decay rate $\gamma$를 추가해 기존의 값을 decay해가면서 누적함으로써 이를 해결합니다.
    $$\begin{aligned}
    {G_{t+1}}&={\color{red}\gamma}{G_t}+{\color{red}(1-\gamma)}(\nabla f(W^t))^2\\
    W^{t+1}&=W^t-\frac{\eta}{\sqrt{{G_{t+1}}+\epsilon}}\cdot\nabla f(W^t)
    \end{aligned}$$
  • AdaDelta : 역시나 위와 같은 AdaGrad의 문제를 해결하기 위해 나왔으며, 분자와 부모의 단위를 맞춰주기 위해 Learning Rate 대신 파라미터의 변화량 제곱을 활용해 해결합니다.
    $$\begin{aligned}
    {\color{red}S_{t+1}}&={\color{red}\gamma S_t}+{\color{red}(1-\gamma)(\nabla W^t)^2}\\
    {G_{t+1}}&={\color{red}\gamma}{G_t}+{\color{red}(1-\gamma)}(\nabla f(W^t))^2\\
    W^{t+1}&=W^t-\frac{{\color{red}\sqrt{S_{t}+\epsilon}}}{\sqrt{{G_{t+1}}+\epsilon}}\cdot\nabla f(W^t)
    \end{aligned}$$

c. Adam 계열

 

이후에 momentum RMSProp이 합쳐진 Adam이라는 optimizer가 등장하며, 가장 많이 사용됩니다.

 

즉, Adam은 아래 식과 같이 momentum decay rate인 $\gamma1$와 sqaure gradient decay인 $\gamma2$를 활용해 합쳐줍니다. 이 둘을 합쳐 빠르게 converge하면서도 stable하게 학습할 수 있습니다. 
$$\begin{aligned}
{\color{red}v_{t+1}}&=\gamma_1 {\color{red}v_t}+(1-\gamma_1)\nabla f(W^t)\\
{G_{t+1}}&={\gamma_2}{G_t}+{(1-\gamma_2)}(\nabla f(W^t))^2\\
W^{t+1}&=W^t-\eta\frac{{\color{red}v_{t+1}}}{\sqrt{{G_{t+1}}+\epsilon}}
\end{aligned}$$

 

또한 추가적으로 아래와 같이 Bias Correction을 통해 Moving Average에 의한 smoothing결과를 보정해준 다음의 식을 보이기도 합니다.

$$\begin{aligned}
{v_{t+1}}&=\gamma_1 {v_t}+(1-\gamma_1)\nabla f(W^t)\\
{G_{t+1}}&={\gamma_2}{G_t}+{(1-\gamma_2)}(\nabla f(W^t))^2\\
{\color{red}\hat{v}_{t+1}}&=\frac{v_{t+1}}{1-\gamma^t_1}\\
{\color{red}\hat{G}_{t+1}}&=\frac{G_{t+1}}{1-\gamma^t_2}\\
W^{t+1}&=W^t-\eta\frac{\color{red}{\hat{v}_{t+1}}}{\sqrt{{\color{red}\hat{G}_{t+1}}+\epsilon}}
\end{aligned}$$

 

결과를 보기 위해 아래 인용한 동영상에는, loss 내의 momentum값이 빨간색 선과 같이 기존의 EWMA원리에 의해 $\gamma_1$과 $\gamma_2$가 0에서 1에 가까워질수록 데이터 포인트에 비해 낮게 나오는 것을 알 수 있습니다.

 

이에 Bias Correction을 적용하면 초록색 선과 같이 데이터에 더 적합하게 맞춰진 것을 볼 수 있습니다.

[https://angeloyeo.github.io/2020/09/26/gradient_descent_with_momentum.html]

 

이후에 Adam을 개선한 다양한 Adam 계열 Optimizer들이 등장합니다.

  • NAdam(Nesterov momentum into Adam) : Adam의 momentum 부분을, NAG momentum으로 대체한 방법입니다
  • AdamW(Adam with Weight Decay) 
    ** Decoupled Weight Decay Regularization (arxiv'17)
    regularization을 위해 (서로 동일하다고 취급되는) L2RegularizationWeight Decay기존의 SGD 업데이트 식에 둘 다 추가해보면 아래와 같이 될 것입니다.
    ** L2 Regularization과 Weight Decay에 대해 궁금하신 분들은 아래 더보기를 참조하세요
    $$\begin{aligned}
    W^{t+1}&={\color{red}(1-\lambda_1)}W^t-\eta(\nabla f(W^t)+{\color{red}\lambda_2W^t})\\
    W^{t+1}&=W^t-{\color{red}\underbrace{\lambda_1 W^t}_{\text{w-decay}}}-\eta(\nabla f(W^t)+)-{\color{red}\underbrace{\eta\lambda_2W^t}_{\text{L2 Reg}}}
    \end{aligned}$$
    하지만 위와 다르게 Adam은 momentum decay rate인 sqaure gradient decay에 대한 파라미터를 가지고 있어, 합쳐 간략하게 표현하면 아래와 같이 파란부분을 추가해 표현할 수 있습니다.
    $$\begin{aligned}
    W^{t+1}&={\color{red}(1-\lambda_1)}W^t-\eta{\color{blue}vG_{t+1}}(\nabla f(W^t)+{\color{red}\lambda_2W^t})\\
    W^{t+1}&=W^t-{\color{red}\lambda_1 W^t}-\eta{\color{blue}vG_{t+1}}(\nabla f(W^t)+)-{\color{red}\eta{\color{blue}vG_{t+1}}\lambda_2W^t}
    \end{aligned}$$
    즉, weight decay부분은 변함이 없지만 실제 L2 regularization의 파라미터 $\lambda_2$를 주어도 ${\color{blue}vG_{t+1}}$때문에 그 효과가 작게 적용됩니다.
    따라서 regularization 기능을 위해 L2 regularization만 추가하면 안되고 weight decay를 추가 해주어야 한다고 주장하고 이를 적용한 것이 AdamW입니다.
    ** 이후의 AdamWR(Adam with Warm Restart)는 Warm Restart LR Schedule을 추가해준 방법입니다. LR schedule에 대해서는 다음챕터에서 다룹니다.
더보기

----------------------------------------------------------

<L2 Regularization과 Weight Decay>

 

앞서 말씀드린바와 같이 학습 중 Overfitting을 방지하기 위해 Regularization을 사용하는데, Regularization에는 다양한 기법들이 있습니다.

  • Loss Term을 수정하는 방법 : L1/L2/Entropy Regularization
  • Data Sampling방법을 수정하는 방법 : Data Augmentation, Cross Validation
  • Training Algorithm을 수정하는 방법 : Dropout, Noise Injection, Early Stopping,
  • Model Architecture을 수정하는 방법 : Batch Normalization

이중에 L2 RegularizationWeight Decay를 살펴보겠습니다.

 

1. L2 Regularization (Ridge Regression)

 

아래 식과 같이 Loss Function에 weight의 L2 Norm을 penalty형태로 추가해주는 방법입니다.

** L1 Regularization은 Lasso Regression이라고 부릅니다.

** $\lambda$는 regularization 파라미터입니다.

$$f(W^t)=\underbrace{\frac{1}{N}\sum^N\left\|y-\hat{y}\right\|^2}_{\text{MSE Loss}}+{\color{red}\underbrace{\lambda\left\|W^t\right\|^2}_{\text{L2 regularization}}}$$

 

보통 weight가 굉장히 클수록 데이터가 조금만 달라져도 예측값이 민감하게 바뀌게 되고 이것이 overfitting을 일으킬 수도 있으므로, 이렇게 weight에 대한 항을 추가해주면 weight가 비상식적으로 커지는 것을 방지하면서 loss를 최소화 할 수 있습니다.

 

근데 혹자는 L2 RegularizationWeight Decay와 같다 라고도 합니다. 그럼 Weight Decay를 살펴보겠습니다.

 

2. Weight Decay

 

Weight decay는 아래 식과 같이 "현재 weight의 크기"를 일정비율 감소시키면서 다음 weight를 update함으로써 overfitting을 방지합니다.

$$W^{t+1}={\color{red}(1-\lambda)}W^t-\eta\nabla f(W^t)$$

 

이 또한 위와 같이 weight가 과하게 커지는 것을 방지함으로써 원하는 효과를 이뤄냅니다.

----------------------------------------------------------

  • RAdam(Rectified Adam) 
    ** On the Variance of the Adaptive Learning Rate and Beyond (ICLR'20)
    기존의 Adam을 포함한 Adaptive 방법들에서는, 학습 초기를 지나면 Learning Rate가 굉장히 작아져 Local Optima에 빠지는 경우가 발생했습니다.
    이는 초반의 Learning Rate가 커서 variance가 커지는 문제 때문이라고 지적하고, Adaptive warmup이 variance reduction 효과를 가져와 이 문제를 해결할수 있다고 합니다. 
    또한, 본 논문에서는 이 variance효과를 증명하기 위한 variance식을 역으로 이용해 variance를 consistent하게 만들 수 있는 rectification term을 알아내 이를 적용합니다.

 

여기까지 설명한 Optimizer들은 First-orderSecond-order로 구분 됩니다.

  • First-order Optimizer : weight의 gradient를 바로 적용하는 것으로, linear하게 이동하기 때문에 정확한 이동은 힘들지만 빠릅니다.
    앞서 설명한 대부분의 Optimizer(SGD, AdaGrad, RMSProp, Adam)들이 이에 속합니다.
  • Second-order Optimizer : weight의 high-order gradient를 적용하는 것으로, 고차함수 gradient를 활용하기 때문에 정확하지만 느립니다.

[https://say-young.tistory.com/entry/CS231n-Lecture-7-Optimizer]


2.  Learning Rate Schedulers

 

맨 처음에 살펴보았던 일반적인 학습 과정을 다시 살펴보겠습니다.

from torch import optim
from torch.optim.lr_scheduler import ExponentialLR

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = ExponentialLR(optimizer, gamma=0.9)

for epoch in range(20):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler.step()

 

Optimizer를 선언했지만 Learning Rate Scheduler를 따로 선언해주지 않으면 Optimizer에 주어진 Learning Rate는 변화하지 않고 Optimizer 함수에 반영이 됩니다.

 

따라서 적절한 Learning Rate Scheduler를 활용하는 것도 꼭 필요한 과정입니다.


a. Terminology

 

Learning Rate Scheduling에 대해 설명하기 전에, 방법들에서 사용되는 용어를 먼저 살펴보겠습니다.

 

1. Warm(up) Restart/Start

 

보통 learning rate를 schedule하면 지속적으로 감소하게 설계하는데, 이렇게 되면 이후에 Local minimum에 빠졌을 때 작은 learning rate를 활용하기 때문에 빠져나오기 어려울 수 있습니다.

 

따라서 학습 중간중간에 learning rate를 증가시켜 Local minimum에서 빠져나올 기회를 제공하는 것을 Warm Restart라고 합니다.

 

또한, 데이터를 random sampling할 때 초기에 사용하는 데이터가 강하게 bias된 데이터에 치우친 경우 Early Over Fitting하는 경향이 있기도 합니다.

 

이를 방지하기 위해 초반에 사용하는 데이터의 영향을 줄이는 방법을 Warm Start라고 합니다. 초반에 낮은 값에서 점차적으로 Learning Rate를 증가시켜 weight를 급격하게 변화시키는 unstable한 학습을 방지하는 것입니다.

 

방법은 예상하시다시피, warm-up period에서는 learning rate가 서서히 증가하도록 하는 것이며, 방법으로는 두가지가 있습니다.

** Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour (arxiv'17)

  • Constant : Base LR보다 낮은 learning rate를 상수로 사용하다가 Base LR로 돌아오는 것
  • Gradual : Base LR보다 낮은 learning rate에서 서서히 선형적/비선형적으로 Base LR로 돌아오는 것

[Warmup-Steady-Decay 과정]

 

Warmup start은 사실 "처음부터 높은 LR을 주는 것"보다 과연 좋을까라는 의문을 가지는 분도 있지만, 실제로 variance를 줄이는 학습에 유용하며 batch size가 굉장히 큰 경우 도움이 된다고 합니다.

 

또한 fine-tuning을 할 때도, 서서히 learning rate를 증가시켜 주면, pretrained의 weight를 급격하게 새로운 데이터에 맞게 업데이트하는 것을 방지할 수 있어 좋다고 합니다.

 

2. Learning Rate Annealing

 

Annealing(담금질)Learning Rate Decay와 같은 뜻으로, 시간이 지날수록 Learning Rate를 감소시키는 방법을 의미합니다. 즉,

  • 초기 learning rate를 상대적으로 크게 설정해 Local minimum에 보다 더 빠르게 다가갈 수 있게 만들어 준 뒤,
  • 이후 learning rate를 줄여가며 Local minimum에 보다 더 정확하게 수렴하는 방향으로 나아가도록 합니다.

Step Decay, Exponential Decay, Inverse Time Decay, Cosine Decay등이 이 방법에 해당되며, 대표적으로 Cosine Annealing은 i번째 epoch에서 아래 식과 같습니다.

**SGDR: Stochastic Gradient Descent with Warm Restarts(arxiv'16)

** ${i^*}$는 초기 learning rate를 복구한 후부터의 epoch 인덱스입니다. 따라서 이 값이 $T_i$가 되면 $\eta_{min}$이며, 이 값이 0이 되면 $\eta_{max}$입니다.

** $T_{i}$는 i번째의 cosine annealing에서의 $\eta_{max}$간의 epoch길이입니다.

$$\eta_t=\eta^i_{min}+\frac{1}{2}(\eta^i_{max}-\eta^i_{min})(1+cos(\frac{{i^*}}{T_i}\pi))$$


b. PyTorch Schedulers

 

그럼 PyTorch에 존재하는 Learning Rate Scheduler들에 대해 먼저 살펴보겠습니다.

** 아래 과정에서 plotting하는 함수에 대해 궁금하시면 아래 더보기를 참조하세요

더보기

-----------------------------------------------------------

<아래 Plotting에 사용된 코드> 

 

먼저 모델과 optimizer는 아래와 같이 선언해주었습니다.

import torch
import torch.nn as nn
from torch.optim import lr_scheduler


class NullModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1,10)
    

model = NullModule()
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, betas=[0.9,0.999], weight_decay=1e-3)

 

이후에 plotting은 아래 함수와 함께 진행했습니다.

import matplotlib.pyplot as plt

def plot_lr(scheduler, epochs=100, title=""):
    lrs = []
    for i in range(epochs):
        lr = optimizer.param_groups[0]['lr']
        scheduler.step()
        lrs.append(lr)

    plt.figure(figsize=(11,8))
    plt.plot(lrs, color="#e35f62", linewidth=3)
    plt.xlabel("Epoch", fontsize=13, fontweight='bold')
    plt.ylabel("Learning Rate", fontsize=13, fontweight='bold')
    plt.scatter(0, lrs[0], color='r')
    plt.annotate("{:.2e}".format(lrs[0]), (0.0, lrs[0]), fontsize=12, fontweight='bold')
    plt.title(title, fontsize=15, fontweight='bold')
    plt.show()

 

----------------------------------------------------------- 

 

1. Step 계열

 

특정 factor에 따라 step function을 활용해 줄여나가는 방법입니다. StepLRMultiStepLR은 Step을 정의하는 방법이 아래와 같이 다릅니다.

LR_SCHED = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
plot_lr(LR_SCHED, title="StepLR")

LR_SCHED = lr_scheduler.MultiStepLR(optimizer, milestones=[200,35], gamma=0.5)
plot_lr(LR_SCHED, 300, title="MultiStepLR")

 

[Step 계열 LR Scheduler 결과]

 

2. Exponential 계열

 

계속해서 특정 factor를 곱해나가며 Learning Rate를 줄여나가는 방법입니다.

  • MultiplicativeLR : $LR_{epoch}=LR_0\times{\text{lambda}}^{epoch-1}$
  • ExponentialLR : $LR_{epoch}=LR_0\times{\text{gamma}}^{epoch-1}$
LR_SCHED = lr_scheduler.MultiplicativeLR(optimizer, lr_lambda=lambda x:0.9)
plot_lr(LR_SCHED, title="MultiplicativeLR")

LR_SCHED = lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
plot_lr(LR_SCHED, title="ExponentialLR")

 

[Exponential 계열 LR Scheduler 결과]

 

3. Cycle 계열

 

Cycle에 따른 함수를 활용해 Learning Rate를 만들어냅니다. OneCycleLR은 한번의 Cycle을 가지는 schedule이며 CyclicLR에는 아래와 같은 세가지 버전(triangular, triangular2, exp-range)이 있습니다.

LR_SCHED = lr_scheduler.OneCycleLR(optimizer, max_lr=0.1, total_steps=10000, cycle_momentum=False)
plot_lr(LR_SCHED, 10000, title="OneCycleLR")

LR_SCHED = lr_scheduler.CyclicLR(optimizer, base_lr=0.001, max_lr=0.1, step_size_up=50, step_size_down=100, cycle_momentum=False, mode='triangular')
plot_lr(LR_SCHED, 500, title="CyclicLR-triangular")

LR_SCHED = lr_scheduler.CyclicLR(optimizer, base_lr=0.001, max_lr=0.1, step_size_up=50, step_size_down=None, cycle_momentum=False, mode='triangular2')
plot_lr(LR_SCHED, 500, title="CyclicLR-triangular2")

LR_SCHED = lr_scheduler.CyclicLR(optimizer, base_lr=0.001, max_lr=0.1, step_size_up=50, step_size_down=None, gamma=0.995, cycle_momentum=False, mode='exp_range')
plot_lr(LR_SCHED, 500, title="CyclicLR-exp_range")

 

[Cycle 계열 LR Scheduler 결과]

 

 

4. Annealing과 Warm Restart

 

cos함수를 따라 초기 learning rate까지 올라왔다가 Cosine Annealing 방법을 활용해 learing rate가 eta_min으로 내려가는 함수는 CosineAnnealingLR이며, 이에 learning rate에 올라갈 때 warm restart를 추가한 것이 아래 그림과 같은 CosineAnnealingWarmRestarts입니다.

[https://yongwookha.github.io/MachineLearning/2021-10-06-cosine-annealing-warm-up-restarts]

 

이들의 결과를 직접 살펴보면 아래와 같습니다.

LR_SCHED = lr_scheduler.CosineAnnealingLR(optimizer, T_max=10, eta_min=0)
plot_lr(LR_SCHED, 100, title="CosineAnnealingLR")

LR_SCHED = lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=1, eta_min=0)
plot_lr(LR_SCHED, 100, title="CosineAnnealingWarmRestarts-mult1")

LR_SCHED = lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=2, T_mult=2, eta_min=0)
plot_lr(LR_SCHED, 100, title="CosineAnnealingWarmRestarts-mult2")

[Annealing과 WarmRestart가 반영된 Cosine LR Scheduler 결과]


c. Advanced

 

이번엔 직접 Learning Rate Scheduler를 상황에 맞게 만들어보는 것을 보이겠습니다.

 

PyTorch에는 아래와 같은 LambdaLR을 통해 lambda함수 혹은 정의된 함수로 Learning Rate를 만들어낼 수 있는 방법을 제공합니다. 아래는 아래 식을 예시로 구현한 결과입니다.

$$LR_{epoch}=LR_0\times0.95^{epoch-1}$$

LR_SCHED = lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda epoch:0.95**epoch)
plot_lr(LR_SCHED, 200, title="LambdaLR")

[Lambda 함수로 만든 LR Scheduler 결과]

 

하지만 직접 구현해 사용하는 경우가 많은데, 이때는 torch.optim.lr_scheduler._LRScheduler를 상속해 구현합니다.

 

아래는 exponential warmup을 포함해 구현한 Inverse Decay Learning Rate Schedule을 예로 살펴보겠습니다. 즉, Inverse Learning Rate Decayexponential Warpup이 추가된 형태입니다. 필요한 파라미터는 아래와 같습니다.

  • inv_gamma : 아래의 power가 적용되기까지 필요한 steps/epochs의 수 입니다. 
  • power : 아래식을 수행하기 위한 power입니다. epochinv_gamma값에 가까울 정도로 커지면 $\frac{1}{2}^{\text{power}}$가 될 것입니다.
    $$LR_{epoch}=(\frac{\text{inv gamma}}{\text{inv gamma}+epoch})^{\text{power}}$$
  • warmup(0 <= warmup < 1) : 0이면 disable되며, exponential warmup을 결정하는 factor입니다.
  • final_lr : 끝날 때의 마지막 learning rate를 의미합니다. 0에 가까운 값으로 끝날 것이기 때문에 0으로 둡니다.
  • last_epoch : 가장 최근에 수행된 epoch를 의미합니다. -1로 셋팅하면 초기로 준 Learning Rate를 활용하게 됩니다.

예를 들어 inv_gamma(1000000), power(0.5), warmup(0.99), final_lr(0)일 때 아래 식과 같습니다.

$$\begin{aligned}
\text{warmup}&=1-0.99^{epoch+1}\\
\text{lr mult}&=(1+\frac{\text{epoch}}{1000000})^{-0.5}\\
\text{result}&=\left\{\begin{aligned}
&LR\times\text{warmup}\times \text{lr mult}&\text{when}\geq\text{0}\\
&0&\text{when}<\text{0}
\end{aligned}\right.
\end{aligned}$$

 

구현한 결과는 아래와 같습니다.

class InverseLR(torch.optim.lr_scheduler._LRScheduler):
    def __init__(self, optimizer, inv_gamma=1., power=1., warmup=0., final_lr=0.,
                 last_epoch=-1, verbose=False):
        self.inv_gamma = inv_gamma
        self.power = power
        if not 0. <= warmup < 1:
            raise ValueError('Invalid value for warmup')
        self.warmup = warmup
        self.final_lr = final_lr
        super().__init__(optimizer, last_epoch, verbose)

    def get_lr(self):
        if not self._get_lr_called_within_step:
            import warnings
            warnings.warn("To get the last learning rate computed by the scheduler, "
                          "please use `get_last_lr()`.")

        return self._get_closed_form_lr()

    def _get_closed_form_lr(self):
        warmup = 1 - self.warmup ** (self.last_epoch + 1)
        lr_mult = (1 + self.last_epoch / self.inv_gamma) ** -self.power
        result = [warmup * max(self.final_lr, base_lr * lr_mult) for base_lr in self.base_lrs]
        return result

 

위와 같이 get_lr()함수를 구현하면, 내장 구현된 step()함수가 scheduler.step()으로 실행되며 실제 Learning Rate를 셋팅하게 됩니다.

 

이 함수는 _LRScheduler를 상속해 구현하는 경우 NotImplementedError를 피하기 위해 무조건 구현해주어야 하며, 구현시 참조해야할 값들은 아래와 같습니다.

  • self.last_epoch : 가장 최근의 epoch
  • self.base_lr : optimizer에 설정된 초기 Learning Rate
  • return : 설정될 learning rate

이외에도 step()함수를 직접 구현해줌으로써 실제 step을 진행할 때 업데이트가 필요한 파라미터들을 구현해줄 수 있습니다. 이를 구현할 때 참조해야할 값들은 아래와 같습니다.

  • self.last_epoch : 가장 최근의 epoch
  • epoch (input) : 이번에 수행할 epoch로, self.last_epoch+1과 같아야 합니다.
  • self.optimizer.param_groups : learning rate를 설정하는데 사용합니다.
더보기

------------------------------------------------

<step()함수 예시>

 

step()함수를 직접 구현한 예시는 아래와 같습니다. 위 언급한 바와 같이 epochself.last_epoch를 활용하고, self.optimizer.param_groups를 통해 셋팅해준 것을 볼 수 있습니다.

    def step(self, epoch=None):
        if epoch is None:
            epoch = self.last_epoch + 1
            self.T_cur = self.T_cur + 1
            if self.T_cur >= self.T_i:
                self.cycle += 1
                self.T_cur = self.T_cur - self.T_i
                self.T_i = (self.T_i - self.T_up) * self.T_mult + self.T_up
        else:
            if epoch >= self.T_0:
                if self.T_mult == 1:
                    self.T_cur = epoch % self.T_0
                    self.cycle = epoch // self.T_0
                else:
                    n = int(math.log((epoch / self.T_0 * (self.T_mult - 1) + 1), self.T_mult))
                    self.cycle = n
                    self.T_cur = epoch - self.T_0 * (self.T_mult ** n - 1) / (self.T_mult - 1)
                    self.T_i = self.T_0 * self.T_mult ** (n)
            else:
                self.T_i = self.T_0
                self.T_cur = epoch
                
        self.eta_max = self.base_eta_max * (self.gamma**self.cycle)
        self.last_epoch = math.floor(epoch)
        
        # Setting Learning Rate with get_lr()
        for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
            param_group['lr'] = lr

 

위 코드와 같이 실제로 self.T_cur, self.T_mult, self.T_i, self.T_up 등 다음 step에서 활용할 파라미터 셋팅이 필요하지 않는 이상 step()함수는 따로 구현할 필요는 없습니다.

------------------------------------------------

 

그럼 실제 결과를 살펴보겠습니다.

LR_SCHED = InverseLR(optimizer, inv_gamma=1000000, power=0.5, warmup=0.99)
plot_lr(LR_SCHED, 1000, title="InverseLR")

LR_SCHED = InverseLR(optimizer, inv_gamma=1000000, power=0.5, warmup=0.99)
plot_lr(LR_SCHED, 100000, title="InverseLR")

[직접 만든 LR Scheduler의 결과, 0~1000(왼쪽), 0~1000000(오른쪽)]

 

위 Schedule이 어떤식으로 동작하는지 살펴보기 위해 위에서 보았던 식을 다시보겠습니다.

$$(1-0.99^{epoch+1})(1+\frac{epoch}{1000000}^{-0.5})$$

  • 위 식의 왼쪽 항 (아래 그림의 초록색 선) : exponential warmup으로, 0을 지나 계속 증가합니다.
  • 위 식의 오른쪽 항 (아래 그림의 빨간색 선) : Inverse LR decay로, 0을 지나 계속 감소합니다.

[식의 도식화, x축은 linear y축은 logarithmic]

결과적으로

  • (왼쪽 항의 힘으로) 초기 Learning Rate에 도달하기까지 warmup은 증가, lr_mult는 감소, result는 증가하며,
  • (오른쪽 항의 힘으로) 초기 Learning Rate에 도달 후에는 warmup은 감소, lr_mult는 증가, result는 감소합니다.

Optimizer 정리 논문

https://arxiv.org/abs/1609.04747

batch size

https://pytorch.org/tutorials/beginner/ddp_series_multigpu.html

Cross Validation

https://blog.naver.com/winddori2002/221850530979

Deep learning문제

https://medium.com/@lostandfound2654/common-problems-when-training-deep-learning-models-and-how-to-overcome-them-e37d0ac0a13b

PyTorch 공식 페이지 optim

https://pytorch.org/docs/stable/optim.html

Scheduler 참조
https://gaussian37.github.io/dl-pytorch-lr_scheduler/ 
https://ropiens.tistory.com/90

Adam

https://angeloyeo.github.io/2020/09/26/gradient_descent_with_momentum.html#google_vignette

Annealing, Warm Start
https://yongwookha.github.io/MachineLearning/2021-10-06-cosine-annealing-warm-up-restarts

728x90
반응형