[딥러닝 기초] learning rate scheduler
1. Learning rate scheduler란?
2. 코드와 함께 실험해보기
2.1. No Scheduler
2.2. Linear warmup with linear decay
2.3. Cosine Decay
2.4. Cosine Annealing (SGDR)
2.5. CyclicLR
2.6. ReduceLROnPlateau
2.7. InverseSquareRoot Scheduler
3. cifar10 실험 결과
1. Learning rate scheduler란?
오늘은 learning rate scheduler에 대해 알아보겠습니다. learning rate는 학습에 가장 큰 영향을 미치는 하이퍼파라미터 중 하나입니다. learning rate가 너무 크면 optimal 지점을 지나치기 쉽고, 너무 작으면 학습이 느리고 잘못된 optimal 지점에 빠지기 쉽기 때문입니다. Gradient에 직접 영향을 미치는 요소인만큼 learning rate 값을 고정하기보다는 수치를 미세하게 조정해주는 것이 더 좋습니다. Learning rate scheduler는 학습에 따라 learning rate를 조절하는 방법입니다.
paperswithcode를 보면 논문에서 자주 사용된 learning rate scheduler의 통계가 나와 있습니다.
가장 자주 사용되는 12개의 scheduler가 사용횟수 순으로 표로 나타나 있는데 다행히 보면 거의 사용되는 것만 사용되고 있다는 것을 확인할 수 있습니다. 더불어 각 method 링크를 클릭하면 설명과 함께 언제 어디에 얼마나 사용되었는지에 대한 통계도 나와 있으니 참고하면 좋을 것 같습니다.
위 그래프를 보면 전체적으로 Linear Warmup With Linear Decay가 자주 사용되다가 최근에 들어서는 Linear Warmup Witch Cosine Annealing이 자주 사용되고 있다는 것을 확인할 수 있습니다.
2. 코드와 함께 실험해보기
이번 시간엔 PyTorch로 learning scheduler들을 사용해 보고 learning rate scheduler가 어떤 식으로 작동하고, 학습에 어떤 영향을 미치는지 직접 코드를 실행해보며 확인해 볼 것입니다. 실험은 CIFAR10의 분류 작업으로 테스트 할겁니다.
colab 코드 : https://colab.research.google.com/drive/1ZD0okOJ5lcqucYG2HPzry3S6blIYpUwc?usp=sharing
훈련 step은 적당히 과적합이 나타나는 25,000 step으로 지정했고 결과를 좀 더 세밀하게 확인하기 위해 500step마다 검증셋으로 테스트를 했습니다. 배치 크기는 16으로 했고 optimizer는 Adam을 사용했으며, 기본 learning rate는 0.001로 지정했습니다.
2.1. No Scheduler
우선 learning rate scheduler를 사용하지 않은 학습 결과를 살펴보겠습니다.
가장 낮은 eval loss는 0.82정도가 나왔고, eval accuracy는 72.62%가 나왔습니다. scheduler를 사용하지 않았기 때문에 learning rate가 학습 내내 0.001로 고정되어 있는 것을 확인할 수 있습니다.
2.2. Linear warmup with linear decay
다음으론 linear warmup with linear decay scheduler를 구현해서 적용해 보겠습니다.
학습 초기에는 모델의 파라미터가 랜덤으로 초기화되어 있기 때문에 학습이 매우 불안할 것입니다. 그렇기 때문에 처음부터 큰 learning rate로 학습될 경우 모델이 잘못된 방향으로 학습될 수도 있습니다. linear warmup은 이를 방지하기 위해 학습 초기에는 learning rate를 일정 step까지 아주 작은 값부터 천천히 선형적으로 증가시키는 방법입니다.
반대로 학습이 어느정도 된 순간부터는 learning rate가 클 경우 optimal 지점을 벗어날 수 있습니다. 그렇기 때문에 학습이 어느정도 된 후에는 learning rate를 차차 줄여주는 것이 학습을 안정적으로 할 수 있게 됩니다. 그래서 일정 step 이후에는 learning rate를 선형적으로 감소시킵니다.
from torch.optim import lr_scheduler
class LinearWarmup(lr_scheduler.LambdaLR):
def __init__(self, optimizer, warmup_steps, training_steps, last_epoch=-1):
'''
optimizer : target optimizer to apply learning rate scheduler
warmup_steps : step size of warming up the learning rate
training_steps : total training step
last_epoch : last epoch of training
'''
def lr_lambda(cur_step):
if cur_step < warmup_steps:
return float(cur_step) / float(max(1.0, warmup_steps))
return max(
0.0,
float(training_steps - cur_step) / float(max(1, training_steps - warmup_steps))
)
super().__init__(optimizer, lr_lambda, last_epoch=last_epoch)
scheduler = LinearWarmup(optimizer, 1000, 25000)
learning rate scheduler의 구현은 위와 같이 torch.optim.lr_scheduler의 LambdaLR을 상속받아서 구현할 수 있습니다. 이렇게 구현한 scheduler는 선언한 뒤에 학습 loop에서 .step() 함수를 통해 learning rate를 조절할 수 있습니다.
for step in tqdm(range(total_steps)):
data = next(train_iter)
data = {k: v.to(device) for k, v in data.items()}
output = model(data["images"])
loss = criterion(output, data["labels"])
train_loss.append(loss.detach().cpu().item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
if scheduler != None and "Plateau" not in str(scheduler):
# 학습 loop 안에서 scheduler.step()으로 learning rate를 조절
scheduler.step()
lr_lambda 함수에 linear warmup의 기능이 구현되어 있는데, 코드보단 learning rate가 학습 step에 따라 어떻게 변화하는지 직접 확인하는 것이 더 이해하기 쉽습니다.
warmup step은 적당히 1000 step 정도로 지정했습니다. 결과를 보면 Eval Loss가 scheduler를 적용하지 않은 것보다 훨씬 더 작은 값을 찍은 것을 볼 수 있습니다. Eval accuracy 또한 더 높아진 것을 확인할 수 있습니다. 구체적인 수치로 보자면 적용하기 전보다 최저 eval loss가 0.11 가량 감소(0.84->0.73)했으며, eval accuracy는 5%가량 증가(72.62%->76.94%)했습니다. Scheduler의 효과가 생각보다 크죠?
2.3. Cosine Decay
다음은 cosine annealing scheduler에 대해 알아보겠습니다. Cosine annealing은 Stochastic Gradient Descent with Warm restarts(SGDR)에서 등장한 방법입니다. Linear decay가 learning rate를 선형적으로 감소시키는 반면에 cosine annealing은 cosine 그래프 형태로 감소시키는 겁니다.
class CosineWithWarmup(lr_scheduler.LambdaLR):
def __init__(self, optimizer, warmup_steps, training_steps, num_cycles, last_epoch=-1):
'''
optimizer : target optimizer to apply learning rate scheduler
warmup_steps : step size of warming up the learning rate
training_steps : total training step
last_epoch : last epoch of training
'''
def lr_lambda(cur_step):
if cur_step < warmup_steps:
return float(cur_step) / float(max(1.0, warmup_steps))
progress = float(cur_step - warmup_steps) / float(max(1, training_steps - warmup_steps))
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress)))
super().__init__(optimizer, lr_lambda, last_epoch=last_epoch)
코드를 보면 linear warmup은 똑같이 수행한 뒤, cosine decay를 적용한 것을 알 수 있습니다.
위 학습결과 그래프를 보면 linear decay와 거의 유사한 성능을 보이는 것을 확인할 수 있습니다. 수치로 비교하면 다음과 같습니다.
No scheduling | Linear decay | Cosine decay | |
best eval loss | 0.8268 | 0.7378 | 0.7449 |
best eval acc | 72.62% | 76.94% | 77.03% |
최소 eval loss는 Cosine decay를 적용한 것이 조금 더 크지만, 최고 eval accuracy는 cosine decay가 조금 더 높은 것을 확인할 수 있습니다.
2.4. Cosine Annealing (SGDR)
Cosine annealing scheduler에 관해서 검색하다보면 Cosine decay와 SGDR의 개념이 혼용되어 사용되고 있는 것 같습니다. 그래서 개인적으로 검색하면서 많이 헷갈렸는데요, 둘이 비슷하지만 조금 다른 부분이 있는 것 같습니다.
SGDR은 cosine decay를 적용하는 건 맞지만, 여기에 더해 학습을 restart하는 것까지를 말합니다. 어떻게 보면 학습을 여러번 수행하고 가장 좋은 모델 결과를 사용하는 것과 같은 원리입니다. 학습을 restart하는 이유는 모델이 잘못 학습되지 않도록 여러번 학습한 뒤에 가장 좋은 평균의 결과를 사용하도록 하고 싶기 때문입니다. 단, 여기서 restart를 할 때 완전히 새로운 파라미터를 가지고 학습을 다시 시작하는 것이 아니라, 이미 학습된 parameter를 초기값으로 설정하여 학습을 다시 하는 것입니다.
scheduler = lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=12500, T_mult=1, eta_min=0)
SGDR은 PyTorch에 기본적으로 구현되어 있습니다. T_0는 restart를 수행할 step을 나타내고, T_mult는 restart의 주기 배율을 나타냅니다. 위 코드에 따르면, 12500step에서 처음 restart를 수행한 뒤, 그 뒤로 12500step 마다 restart를 수행하게 됩니다. 만약 T_mult가 2라면, 12500step, 12500+(12500x2)step, 37500+(12500x3)step마다 restart를 수행하게 됩니다. eta_min은 learning rate의 최소값을 말합니다.
위 그래프는 SGDR을 적용하여 12,500 step마다 warm restart를 적용하여 총 4번 실험한 것과 같은 결과를 나타냅니다. 그래프를 보면 랜덤으로 초기화된 파라미터에서 학습한 1번째 모델(12,500 step까지의 결과)보다 2번째 모델(1번째 모델의 파라미터를 이어 받아 다시 학습을 시작한 12,500step~25,000step까지의 결과)의 성능이 더 높게 나타난 것을 확인할 수 있습니다. 3번째와 4번째 모델의 eval loss는 계속 증가하는 모습을 보이지만 eval accuracy는 그래도 1번째 모델보다 높은 것을 확인할 수 있습니다. 이런 식으로 학습을 여러번하면서 가장 좋은 결과를 얻고자 하는 것이 SGDR 방식입니다.
2.5. CyclicLR
다음은 PyTorch에 있는 CyclicLR입니다. CyclicLR은 SGDR과 같이 restart를 적용하는 방법입니다. 다만 그래프의 형태가 cosine 함수와 다른 모양을 띱니다.
scheduler = lr_scheduler.CyclicLR(
optimizer, base_lr=0.00001, max_lr=0.001,
step_size_up=1000, step_size_down=11500, mode='triangular', cycle_momentum=False
)
scheduler = lr_scheduler.CyclicLR(
optimizer, base_lr=0.00001, max_lr=0.001,
step_size_up=6250, step_size_down=None, mode='triangular2', cycle_momentum=False
)
scheduler = lr_scheduler.CyclicLR(
optimizer, base_lr=0.00001, max_lr=0.001,
step_size_up=6250, step_size_down=None, mode='exp_range', gamma=0.95, cycle_momentum=False
)
- base_lr : 최소 learning rate값
- max_lr : 최대 learning rate값
- step_size_up : warmup step
- step_size_down : learning rate decay의 step수입니다. None으로 설정할 경우 warmup step과 같은 값을 가집니다.
- mode : 'triangular', 'triangular2', 'exp_range' 3가지가 있으며 각각 decay 그래프의 형태가 다르게 나타납니다.
- gamma : 만약 mode가 'exp_range'일 경우, gamma 값을 설정할 수 있습니다. exp_range는 지수 함수의 형태를 띱니다. ($\text{gamma}^{cycle}$) gamma 값은 이 지수의 밑을 결정하는 하이퍼파라미터입니다.
triangular와 triangular2는 직선 형태 그래프로 warmup과 decay가 진행됩니다. 다만 triangular2의 경우 각 restart마다 최대 learning rate가 절반씩 감소합니다. exp_range는 triangular와 유사하지만 지수 함수의 형태로 learning rate의 증감이 이뤄집니다.
똑같이 restart를 수행하는 cosine annealing과 cyclicLR scheduler끼리 결과를 비교해봤습니다. 보면 triangular2가 다른 그래프들에 비해 성능이 꽤 좋은 것을 알 수 있습니다. Restart를 하는 과정에서 max learning rate를 이전보다 줄인 것이 학습의 안정성을 높여서 나타난 결과가 아닌가 싶습니다. 수치적으로 비교하자면 다음과 같습니다.
cosine annealing | triangular | triangular2 | exp_range | |
best eval loss | 0.766 | 0.666 | 0.6486 | 0.6664 |
best acc | 75.94% | 77.93% | 78.86% | 77.91% |
2.6. ReduceLROnPlateau
다음은 PyTorch에 있는 ReduceLROnPlateau입니다. 이 방식은 eval loss가 감소하지 않고 증가할 때마다(모델의 과적합이 시작될 때마다) learning rate를 일정 비율로 감소 시키는 방식입니다.
scheduler = lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.1, patience=5, min_lr=0
)
for step in range(total_steps):
...
scheduler.step(eval_loss)
위 코드는 만약 eval_loss가 'minimum'(mode)값에 도달했을 경우, 5(patience) step만큼 기다려보고, 그래도 eval_loss가 감소하지 않으면 learning rate에 0.1(factor)을 곱하겠다는 뜻입니다. 만약 accuracy가 더 중요하다면 mode='max'로 수정하고 scheduler.step(eval_acc) 와 같이 코드를 수정해 주면 되겠죠.
25,000step을 학습시켜본 결과 총 4번 learning rate가 감소했습니다. Eval Loss Graph를 보면 learning rate가 감소하면서 과적합 현상이 나타나지 않는 것을 확인할 수 있습니다.
2.7. InverseSquareRoot Scheduler
마지막으로 InverseSquareRoot(ISR) Scheduler를 살펴보겠습니다. ISR scheduler는 아래 수식을 따라 learning rate가 감소합니다.
$${lr}\cdot{1\over\sqrt{\text{current_step}/\text{warmup_step}}}$$
class InverseSquareRootScheduler(lr_scheduler.LambdaLR):
def __init__(self, optimizer, warmup_steps, last_epoch=-1):
'''
optimizer : target optimizer to apply learning rate scheduler
warmup_steps : step size of warming up the learning rate
training_steps : total training step
last_epoch : last epoch of training
'''
def lr_lambda(cur_step):
if cur_step < warmup_steps:
return float(cur_step) / float(max(1.0, warmup_steps))
return 1.0 / math.sqrt((cur_step) / warmup_steps)
super().__init__(optimizer, lr_lambda, last_epoch=last_epoch)
linear warmup을 적용한 뒤, decay에 ISR scheduler를 적용해봤습니다.
최저 loss는 0.7356이 나왔고, eval accuracy는 76.14%가 나왔습니다.
3. cifar10 실험 결과
마지막으로 실험한 모든 scheduler들의 결과를 확인해 보는 것으로 마무리 하겠습니다. Restart를 사용하는 scheduler와 아닌 scheduler는 서로 학습 step 수도 다르고 하기 때문에 따로 비교합니다.
cosine annealing | triangular | triangular2 | exp_range | |
best eval loss | 0.766 | 0.666 | 0.6486 | 0.6664 |
best eval acc | 75.94% | 77.93% | 78.86% | 77.91% |
no scheduler | linear decay | cosine decay | plateau | ISR | |
best eval loss | 0.8268 | 0.7378 | 0.7449 | 0.8419 | 0.7356 |
best eval acc | 72.62% | 76.94% | 77.03% | 72.3% | 76.14% |
결과는 위와 같이 나왔지만 하이퍼 파라미터들을 어떻게 설정하느냐에 따라서, 어떤 task냐에 따라서 결과는 달라질 수 있습니다. Learning rate scheduler 역시 일종의 학습의 하이퍼 파라미터입니다. 여러가지 사용해보고 가장 성능이 잘 나오는 scheduler를 선택하면 되겠죠. 학습을 자주 돌리기 부담스러우면 cosine decay나 linear decay를 통상적으로 많이 사용하니 이 정도만 실험해봐도 좋은 성능을 보일 수 있을 겁니다.