STEP 13 / 18

학습률 스케줄과 학습 루프

rate: 논문의 학습률 공식이다. 원본 노트북이 "This part is very important"라고 강조한 부분이다.

$$lrate = d_{model}^{-0.5} \cdot \min(step^{-0.5},\ step \cdot warmup^{-1.5})$$

처음 warmup 스텝 동안은 학습률을 직선으로 올리고, 그 뒤로는 $1/\sqrt{step}$에 비례해 내린다. 초반에 큰 학습률로 시작하면 Adam의 통계가 덜 쌓인 상태에서 크게 튀기 때문에 천천히 올린다. step 0에서는 0의 음수 제곱이 되므로 1로 바꿔 둔다. PyTorch의 LambdaLR에 넘기면 optimizer의 기본 학습률(lr=1)에 이 값이 곱해진다.

run_epoch: 데이터를 한 바퀴 돈다. 배치마다 forward → 손실 → backward → (accum_iter번에 한 번) optimizer.step() → scheduler.step() 순서다. 40배치마다 손실과 처리 속도를 출력한다. mode="eval"이면 역전파 없이 손실만 잰다.

TrainState, DummyOptimizer/DummyScheduler: 진행 상황을 세는 상자와, 평가할 때 run_epoch에 넘길 "아무것도 안 하는" 옵티마이저다.

실행 결과에서 볼 것

원본의 example_learning_schedule 차트다. 모델 크기와 warmup에 따라 꼭짓점의 위치와 높이가 달라진다. d_model이 작을수록 학습률이 높다. (원본은 2만 스텝을 모두 그리지만 페이지 크기를 줄이려고 20스텝마다 하나씩만 그렸다.)

코드 변화

+93 −0 · 파일 전체 449줄

이전 단계의 transformer.py와 비교했다. 초록 줄이 새로 생긴 줄, 빨간 줄이 사라진 줄이다.

GitHub에서 보기 ↗
11import copy
22import math
3+import time
34
45import torch
56import torch.nn as nn
354355 / norm
355356 )
356357 return sloss.data * norm, sloss
358+
359+
360+def rate(step, model_size, factor, warmup):
361+ """
362+ we have to default the step to 1 for LambdaLR function
363+ to avoid zero raising to negative power.
364+ """
365+ if step == 0:
366+ step = 1
367+ return factor * (
368+ model_size ** (-0.5) * min(step ** (-0.5), step * warmup ** (-1.5))
369+ )
370+
371+
372+class TrainState:
373+ """Track number of steps, examples, and tokens processed"""
374+
375+ step: int = 0 # Steps in the current epoch
376+ accum_step: int = 0 # Number of gradient accumulation steps
377+ samples: int = 0 # total # of examples used
378+ tokens: int = 0 # total # of tokens processed
379+
380+
381+def run_epoch(
382+ data_iter,
383+ model,
384+ loss_compute,
385+ optimizer,
386+ scheduler,
387+ mode="train",
388+ accum_iter=1,
389+ train_state=TrainState(),
390+):
391+ """Train a single epoch"""
392+ start = time.time()
393+ total_tokens = 0
394+ total_loss = 0
395+ tokens = 0
396+ n_accum = 0
397+ for i, batch in enumerate(data_iter):
398+ out = model.forward(
399+ batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
400+ )
401+ loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
402+ # loss_node = loss_node / accum_iter
403+ if mode == "train" or mode == "train+log":
404+ loss_node.backward()
405+ train_state.step += 1
406+ train_state.samples += batch.src.shape[0]
407+ train_state.tokens += batch.ntokens
408+ if i % accum_iter == 0:
409+ optimizer.step()
410+ optimizer.zero_grad(set_to_none=True)
411+ n_accum += 1
412+ train_state.accum_step += 1
413+ scheduler.step()
414+
415+ total_loss += loss
416+ total_tokens += batch.ntokens
417+ tokens += batch.ntokens
418+ if i % 40 == 1 and (mode == "train" or mode == "train+log"):
419+ lr = optimizer.param_groups[0]["lr"]
420+ elapsed = time.time() - start
421+ print(
422+ (
423+ "Epoch Step: %6d | Accumulation Step: %3d | Loss: %6.2f "
424+ + "| Tokens / Sec: %7.1f | Learning Rate: %6.1e"
425+ )
426+ % (i, n_accum, loss / batch.ntokens, tokens / elapsed, lr)
427+ )
428+ start = time.time()
429+ tokens = 0
430+ del loss
431+ del loss_node
432+ return total_loss / total_tokens, train_state
433+
434+
435+class DummyOptimizer(torch.optim.Optimizer):
436+ def __init__(self):
437+ self.param_groups = [{"lr": 0}]
438+ None
439+
440+ def step(self):
441+ None
442+
443+ def zero_grad(self, set_to_none=False):
444+ None
445+
446+
447+class DummyScheduler:
448+ def step(self):
449+ None

실행 결과

실행한 코드 demo.py 보기
1import altair as alt
2import pandas as pd
3import torch
4from torch.optim.lr_scheduler import LambdaLR
5
6from show import show
7from transformer import rate
8
9print("rate(step, model_size=512, factor=1, warmup=4000)")
10for step in (1, 1000, 2000, 4000, 8000, 20000):
11 print(f" step {step:6d}: {rate(step, 512, 1, 4000):.2e}")
12print("→ 4000스텝까지 직선으로 오르고, 그 뒤로 1/√step으로 내려간다")
13print()
14
15
16# 원본 노트북의 example_learning_schedule
17def example_learning_schedule():
18 opts = [
19 [512, 1, 4000], # example 1
20 [512, 1, 8000], # example 2
21 [256, 1, 4000], # example 3
22 ]
23
24 dummy_model = torch.nn.Linear(1, 1)
25 learning_rates = []
26
27 # we have 3 examples in opts list.
28 for idx, example in enumerate(opts):
29 # run 20000 epoch for each example
30 optimizer = torch.optim.Adam(
31 dummy_model.parameters(), lr=1, betas=(0.9, 0.98), eps=1e-9
32 )
33 lr_scheduler = LambdaLR(
34 optimizer=optimizer, lr_lambda=lambda step: rate(step, *example)
35 )
36 tmp = []
37 # take 20K dummy training steps, save the learning rate at each step
38 for step in range(20000):
39 tmp.append(optimizer.param_groups[0]["lr"])
40 optimizer.step()
41 lr_scheduler.step()
42 learning_rates.append(tmp)
43
44 learning_rates = torch.tensor(learning_rates)
45
46 # Enable altair to handle more than 5000 rows
47 alt.data_transformers.disable_max_rows()
48
49 opts_data = pd.concat(
50 [
51 pd.DataFrame(
52 {
53 "Learning Rate": learning_rates[warmup_idx, :],
54 "model_size:warmup": ["512:4000", "512:8000", "256:4000"][
55 warmup_idx
56 ],
57 "step": range(20000),
58 }
59 )
60 for warmup_idx in [0, 1, 2]
61 ]
62 )
63
64 return (
65 alt.Chart(opts_data[opts_data.step % 20 == 0])
66 .mark_line()
67 .properties(width=600)
68 .encode(x="step", y="Learning Rate", color="model_size\\:warmup:N")
69 .interactive()
70 )
71
72
73show(example_learning_schedule(), "learning_rate")
rate(step, model_size=512, factor=1, warmup=4000)
  step      1: 1.75e-07
  step   1000: 1.75e-04
  step   2000: 3.49e-04
  step   4000: 6.99e-04
  step   8000: 4.94e-04
  step  20000: 3.13e-04
→ 4000스텝까지 직선으로 오르고, 그 뒤로 1/√step으로 내려간다
차트 불러오는 중…