본문 바로가기
딥러닝&머신러닝/파이토치 모델 구현

Pytorch Custom CosineAnnealingWarmRestarts 정리

by David.Ho 2023. 2. 15.
728x90
반응형

CosineAnnealingWarmRestarts

 

  • 코드 : https://github.com/pytorch/pytorch/blob/v1.1.0/torch/optim/lr_scheduler.py#L655
  • CosineAnnealingWarmRestarts에 대하여 다루어 보겠습니다.
    (개인적으로 이 스케쥴러는 아쉽게 구현이 되어있습니다. 왜냐하면 warmup start가 구현되어 있지 않고 learning rate 최댓값이 감소하는 방법이 구현되어 있지 않기 때문입니다. 따라서 아래 따로 구현한 Custom  CosineAnnealingWarmRestarts을 사용하길 바랍니다.)
  • 사용할 파라미터는 optimizer 외에 T_0, T_mult 그리고 eta_min이 있습니다.
- T_0는 최초 주기값 입니다.
- T_mult는 주기가 반복되면서 최초 주기값에 비해 얼만큼 주기를 늘려나갈 것인지 스케일 값에 해당합니다. 
- eta_min은 learning rate의 최소값에 해당합니다.

 

scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=50, T_mult=2, eta_min=0.001)

 

 

  • 위 코드 예제는 50 epoch 주기를 초깃값으로 가지되 반복될수록 주기를 2배씩 늘리는 방법입니다.
  • 앞에서 언급하였듯이 warmup start나 max값이 감소되는 기능은 없습니다.

 


 

Custom CosineAnnealingWarmRestarts

  • 이번에 다룰 스케쥴러는 많은 논문에서 사용 중이고 SGDR로 알려져 있으며 특히 bag of tricks for image classification에서 사용한 최적화 방법으로 좋은 성능을 보입니다.
논문 : SGDR, Stochastic Gradient Descent with Warm Restarts (https://arxiv.org/abs/1608.03983)
논문 : bag of tricks for image classification (https://arxiv.org/abs/1812.01187)

 

  • 간단하게 설명드리면 앞에서 다룬 CosineAnnealingWarmRestarts에 warm up start와 max 값의 감소 기능이 추가된 형태입니다.
  • 아래 코드는 Pytorch의 기존 CosineAnnealingWarmRestarts를 변경하여 사용되었습니다.
import math
from torch.optim.lr_scheduler import _LRScheduler

class CosineAnnealingWarmUpRestarts(_LRScheduler):
    
    
    def __init__(self, optimizer, T_0, T_mult=1, eta_max=0.1, T_up=0, gamma=1., last_epoch=-1):
        
        ## 예외처리
        #초기 주기값
        if T_0 <= 0 or not isinstance(T_0, int):
            raise ValueError("Expected positive integer T_0, but got {}".format(T_0))
        
        # 주기마다 배수할 값
        if T_mult < 1 or not isinstance(T_mult, int):
            raise ValueError("Expected integer T_mult >= 1, but got {}".format(T_mult))
        
        # Warm up 시 필요한 주기
        if T_up < 0 or not isinstance(T_up, int):
            raise ValueError("Expected positive integer T_up, but got {}".format(T_up))
        
        self.T_0 = T_0
        self.T_mult = T_mult
        self.base_eta_max = eta_max # 초기 learning rate
        self.eta_max = eta_max      # 갱신될 learning rate
        self.T_up = T_up
        self.T_i = T_0
        self.gamma = gamma
        self.cycle = 0
        self.T_cur = last_epoch
        super(CosineAnnealingWarmUpRestarts, self).__init__(optimizer, last_epoch)
    
    
    def get_lr(self):
        
        if self.T_cur == -1:
            return self.base_lrs # optimizer에서 가져온 base_lrs
        
        elif self.T_cur < self.T_up:
            return [(self.eta_max - base_lr)*self.T_cur/self.T_up + base_lr for base_lr in self.base_lrs]
        
        else:
            return [base_lr + (self.eta_max - base_lr) * (1 + math.cos(math.pi * (self.T_cur-self.T_up) / (self.T_i - self.T_up))) / 2
                    for base_lr in self.base_lrs]

    
    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 # 주기 조절
        
        # # TODO: 나중에 코드 분석
        # else:
        #     if epoch >= self.T_0:
                
        #         # 배수가 1일 때
        #         if self.T_mult == 1:
        #             self.T_cur = epoch % self.T_0
        #             self.cycle = epoch // self.T_0
                
        #         # 배수가 2이상인 경우
        #         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) # 최대 learning rate 조절
        self.last_epoch = math.floor(epoch)
        
        for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
            param_group['lr'] = lr
  • 먼저 warm up을 위하여 optimizer에 입력되는 learning rate = 0 또는 0에 가까운 아주 작은 값을 입력합니다.
  • 위 코드의 스케쥴러에서는 T_0, T_mult, eta_max 외에 T_up, gamma 값을 가집니다.
  • T_0, T_mult의 사용법은 pytorch 공식 CosineAnnealingWarmUpRestarts와 동일합니다.
  • eta_max는 learning rate의 최댓값을 뜻합니다. 
  • T_up은 Warm up 시 필요한 epoch 수를 지정하며 일반적으로 짧은 epoch 수를 지정합니다.
  •  gamma는 주기가 반복될수록 eta_max 곱해지는 스케일값 입니다.
Learning rate Warmup
요즘 코드들을 보고 있으면 Learning rate warm-up scheduler가 종종 보인다.
 
이는 논문 Bag of Tricks for Image Classification with Convolutional Neural Networks (2018)에 나온 학습 방법 중 하나이다.

논문 내용을 해석해보면

Training이 시작될 때, 모든 parameters들은 보통 random values(initialized)이므로,
 최종 solution에서 멀리 떨어져 있다.
 
이 때, 너무 큰 learning rate를 사용하면 numerical instability가 발생할 수 있기에,
 초기에 작은 learning rate를 사용하고, training과정이 안정되면 초기 learning rate로 전환하는 방법이다.

 

optimizer = optim.Adam(model.parameters(), lr = 0)
scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=150, T_mult=1, eta_max=0.1,  T_up=10, gamma=0.5)

 

 

  • 먼저 위 그래프의 연두색 선은 50 epoch을 나타냅니다. 따라서 T_0=150 epoch의 초기 주기값 후에 다시 0으로 줄어들게 됩니다. 이 때, T_up=10 epoch 만에 learning rate는 0 → eta_max 까지 증가하게 되고 다음 주기에는 gamma=0.5만큼 곱해진 eta_max * gamma 만큼 warm up start 하여 learning rate가 증가하게 됩니다.
  • 앞에서도 언급하였지만 주의할 점은 optimizer에서 시작할 learning rate를 일반적으로 사용하는 learning rate가 아닌 0에 가까운 아주 작은 값을 입력해야 합니다.

 

scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=50, T_mult=2, eta_max=0.1,  T_up=10, gamma=0.5)

 

 

  • 이번 예제에는 T_mult=2가 적용되었습니다. 따라서 주기가 반복될수록 T_0 * T_mult 만큼 주기가 늘어나게 됩니다. 따라서 위 예제와 같이 주기가 반복할수록 주기가 2배씩 늘어나는 것을 볼 수 있습니다.

 

scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=100, T_mult=1, eta_max=0.1,  T_up=10, gamma=0.5)

 

  • 만약 어떤 것을 사용해야 할 지 모르겠다면 마지막 Custom CosineAnnealingWarmUpRestarts을 선언 후 사용해 보시길 추천드립니다.

 

 


[코드 작성에 필요한 함수]

math.floor( )

floor 함수는 실수를 입력하면내림하여 정수를 반환하는 함수이다. floor은 바닥을 의미한다. 실수의 바닥의 수인 바로 아래 정수로 내림한다고 생각하면 기억하기가 좋다.

 

함수를 사용 할 때는 math 모듈을 import 하고서 math.floor(x) 형태로 사용한다. 괄호( ) 안의 수 x는 float타입의 소수점이 있는 실수를 입력한다. ceil 함수와 마찬가지로 정수를 입력하는 경우에는 정수를 그대로 반환한다.

 

math.floor 함수를 사용하면 반환되는 값은 정수인 int타입이다. 실수를 내림하는 코드를 작성한 사용 예시는 아래와 같다.

>>> math.floor(3.14)
3

>>> math.floor(-3.14)
-4

>>> math.floor(0.15)
0

>>> math.floor(-0.15)
-1

>>> math.floor(3)  # 정수는 그대로 정수로 반환
3

>>> math.floor(-3)
3

 

코사인 함수 : math.cos(x)

>>> a = math.pi / 6
     
>>> # returning the value of cosine of pi / 6 
>>> print ("The value of cosine of pi / 6 is : ", end ="") 
>>> print (math.cos(a))
The value of cosine of pi/6 is : 0.8660254037844387
728x90
반응형

댓글