Mục lục
- Mục tiêu bài học
- Vì sao LR cần thay đổi theo thời gian
- Learning rate schedule là gì
- Step Decay
- MultiStep Decay
- Exponential Decay
- Cosine Annealing
- Cosine Annealing với Warm Restarts
- ReduceLROnPlateau — reactive
- Warmup
- OneCycleLR
- API pattern trong PyTorch
- Vị trí gọi
scheduler.step() - Chọn schedule theo task
- LR Finder
- Pitfall thường gặp
- Save / load scheduler state
- Liên hệ với optimizer
- Code Python — 3 scheduler trên MLP
- Bài tập
- Tóm tắt
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Viết được công thức của Step, MultiStep, Exponential, Cosine Annealing, Warm Restarts, Warmup, OneCycleLR.
- Biết khi nào dùng schedule chủ động (Cosine, Step) và khi nào dùng reactive (ReduceLROnPlateau).
- Dùng đúng API
torch.optim.lr_scheduler: vị trí gọischeduler.step(), lấy LR hiện tại quaget_last_lr(). - Chọn schedule hợp lý theo task: tabular vs CNN vs Transformer vs fine-tuning.
- Hiểu nguyên lý LR finder và lý do warmup quan trọng cho Transformer.
- Tránh các pitfall: quên
scheduler.step(), sai vị trí (per-batch vs per-epoch), không load scheduler state khi resume.
Bài này nối tiếp Bài 24 — Early Stopping (cũng can thiệp vào training loop để kiểm soát hội tụ) và bổ sung cho Bài 17 — Optimizer (LR schedule áp lên LR khởi tạo của optimizer).
Vì sao LR cần thay đổi theo thời gian
Learning rate \( \eta \) điều khiển độ dài bước cập nhật parameter. Yêu cầu thay đổi theo giai đoạn:
- Đầu training: parameter ở vị trí ngẫu nhiên, loss surface phẳng tương đối, cần LR lớn để di chuyển nhanh và explore — tìm được "lưu vực" tốt thay vì kẹt cạnh điểm khởi tạo.
- Cuối training: đã ở gần minimum, gradient bé và dao động. LR lớn sẽ "nhảy" qua minimum; LR nhỏ giúp settle, hội tụ chính xác hơn.
LR cố định buộc phải chọn compromise: đủ nhỏ để không divergence ở cuối, đủ lớn để không chậm ở đầu. Schedule cho phép "ăn được cả hai" — LR lớn ở đầu, giảm dần về cuối.
Trên CNN ImageNet, recipe ResNet kinh điển (He et al. 2015) dùng SGD-momentum LR \( 0.1 \), chia 10 mỗi 30 epoch — riêng LR schedule đóng góp 1–2% accuracy so với LR cố định. Trên Transformer, không có warmup + decay thì pretraining gần như diverge.
Learning rate schedule là gì
LR schedule là một hàm \( \eta(t) \) biến đổi LR theo "thời gian" \( t \) — thường là số epoch hoặc số step. Bên trong PyTorch, scheduler không thay đổi optimizer nội bộ; nó chỉ overwrite param_groups[i]["lr"] theo công thức:
\[ \eta_t = \eta_0 \cdot f(t) \]
Với \( \eta_0 \) là LR khởi tạo của optimizer, \( f(t) \) là factor do schedule sinh ra. Các schedule chỉ khác nhau ở dạng của \( f \): bậc thang (Step), liên tục (Exponential, Cosine), reactive (Plateau), tăng-giảm (OneCycle).
Schedule chia làm hai kiểu:
- Open-loop (Step, Exponential, Cosine, OneCycle): \( \eta_t \) chỉ phụ thuộc \( t \), không nhìn loss.
- Closed-loop / reactive (ReduceLROnPlateau): nhìn metric validation và quyết định có giảm LR hay không.
Step Decay
Schedule đơn giản và lâu đời nhất: cứ \( s \) epoch nhân LR với một factor \( \gamma < 1 \).
\[ \eta_t = \eta_0 \cdot \gamma^{\lfloor t / s \rfloor} \]
Trong đó \( s \) là step size (vd: 30 epoch), \( \gamma \) là decay factor (thường \( 0.1 \) hoặc \( 0.5 \)). Ở mốc \( t = 30 \), LR thành \( 0.1 \, \eta_0 \); ở \( t = 60 \), thành \( 0.01 \, \eta_0 \)...
from torch.optim.lr_scheduler import StepLR
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
Ưu điểm: dễ tune, dễ giải thích, hoạt động tốt cho CNN classification. Nhược điểm: bậc thang gây "nẩy" loss ở mốc giảm — không mượt như Cosine. Phù hợp khi bạn đã biết khoảng nào cần giảm (qua kinh nghiệm bài tương tự).
MultiStep Decay
Variant của Step nhưng cho phép chỉ định các mốc cụ thể thay vì khoảng đều. Hữu ích khi recipe gốc của paper đã ghi rõ epoch giảm LR.
from torch.optim.lr_scheduler import MultiStepLR
scheduler = MultiStepLR(optimizer, milestones=[60, 120, 180], gamma=0.1)
Ở epoch 60, 120, 180 LR nhân với \( 0.1 \). Recipe ResNet kinh điển trên CIFAR-10/100 thường dùng pattern này — train 200 epoch, giảm tại epoch 100 và 150.
Khi nào MultiStep tốt hơn Step:
- Khoảng cách mốc không đều: giảm sớm để qua "plateau" ban đầu, rồi để LR giữ lâu hơn.
- Bám đúng recipe của paper để reproduce kết quả.
Exponential Decay
Nhân LR với \( \gamma \) mỗi epoch, không phải mỗi \( s \) epoch:
\[ \eta_t = \eta_0 \cdot \gamma^t \]
from torch.optim.lr_scheduler import ExponentialLR
scheduler = ExponentialLR(optimizer, gamma=0.95)
Với \( \gamma = 0.95 \), sau 100 epoch LR còn khoảng \( 0.6\% \) so với ban đầu. Đường giảm mượt, không có bậc.
Cảnh báo: nếu \( \gamma \) quá thấp (vd 0.9), LR teo cực nhanh — sau 50 epoch còn \( 0.5\% \) — model có thể "đóng băng" giữa chừng. Tinh chỉnh kỹ \( \gamma \) theo tổng số epoch.
Cosine Annealing
Loshchilov & Hutter (2016, arXiv:1608.03983) đề xuất giảm LR theo đường cosine từ \( \eta_0 \) về \( \eta_{\min} \) trong \( T \) bước:
\[ \eta_t = \eta_{\min} + \frac{1}{2} (\eta_0 - \eta_{\min}) \left( 1 + \cos\left( \frac{t}{T} \, \pi \right) \right) \]
Tại \( t = 0 \): \( \cos(0) = 1 \), \( \eta_t = \eta_0 \). Tại \( t = T \): \( \cos(\pi) = -1 \), \( \eta_t = \eta_{\min} \). Đường cong mượt, giảm chậm ở đầu (giữ LR lớn để explore) và chậm ở cuối (giữ LR nhỏ để settle), giảm nhanh ở giữa.
from torch.optim.lr_scheduler import CosineAnnealingLR
scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6)
Cosine Annealing là default thực tế cho:
- Image classification modern (ViT, EfficientNet, ConvNeXt).
- Transformer pretraining (kết hợp với warmup — mục 10).
- Fine-tuning LLM (cosine từ peak LR về 10% peak LR).
Trong thực hành Cosine thường nhỉnh hơn Step 0.1–0.3% accuracy mà không cần tune mốc giảm — phù hợp khi không có recipe sẵn.
Cosine Annealing với Warm Restarts
Sau khi LR chạm \( \eta_{\min} \), reset về \( \eta_0 \) và lặp lại — gọi là warm restart (SGDR). Mỗi lần restart, độ dài chu kỳ có thể nhân với \( T_{\text{mult}} \).
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)
# chu kỳ: 10, 20, 40, 80 epoch...
Trực giác: mỗi lần "đẩy" LR lên cao có thể giúp parameter thoát khỏi local minimum và tìm vùng tốt hơn. Bonus: có thể chụp ảnh model ở mỗi điểm \( \eta_{\min} \) như một thành viên ensemble ("snapshot ensemble", Huang et al. 2017).
Trade-off: training không hội tụ mượt — có thể bị "phá" nếu chu kỳ chọn không tốt. Cosine thường (không restart) là lựa chọn an toàn hơn cho hầu hết bài.
ReduceLROnPlateau — reactive
Khác với các schedule trên (chủ động theo lịch), ReduceLROnPlateau nhìn metric validation — chỉ giảm LR khi metric không cải thiện sau \( N \) epoch (patience).
from torch.optim.lr_scheduler import ReduceLROnPlateau
scheduler = ReduceLROnPlateau(
optimizer,
mode="min", # "min" cho loss, "max" cho accuracy
factor=0.5, # LR mới = factor * LR cũ
patience=10, # chờ 10 epoch
min_lr=1e-6,
)
Sử dụng:
for epoch in range(epochs):
train_epoch(...)
val_loss = validate(...)
scheduler.step(val_loss) # PHẢI truyền metric
Lưu ý: đây là scheduler duy nhất cần truyền metric vào step(). Quên truyền sẽ raise TypeError.
Khi nào dùng:
- Không biết trước tổng số epoch / khi nào nên giảm LR.
- Bài tabular hoặc fine-tuning với data nhỏ — số epoch thay đổi theo seed.
- Kết hợp với Early Stopping (B24) như "phòng tuyến": plateau → giảm LR; nếu vẫn không cải thiện → stop.
Warmup
Warmup ngược với decay: trong \( T_w \) step đầu, LR tăng tuyến tính từ 0 (hoặc giá trị rất nhỏ) lên LR target \( \eta_0 \).
\[ \eta_t = \eta_0 \cdot \min\left( 1, \, \frac{t}{T_w} \right) \]
Vì sao cần warmup:
- Đầu training, weight ở vị trí random, gradient có thể có scale rất lớn ở một vài layer. LR target ngay từ step đầu có thể làm parameter "nổ" — đặc biệt với Transformer có residual + LayerNorm chưa stable.
- Adam đầu training có \( m_t, v_t \) bị bias về 0 (mục 9 B17). Vài chục step đầu LR effective không chuẩn — warmup mua thời gian cho bias correction.
- Batch lớn (linear scaling rule, Goyal et al. 2017): tăng batch \( k \) lần đòi tăng LR \( k \) lần; warmup giảm rủi ro divergence khi LR target cao.
Recipe phổ biến: linear warmup + cosine decay. \( T_w \) thường là 1–10% tổng số step. BERT pretraining: 10 000 warmup step / 1M total. LLaMA: 2000 warmup step.
PyTorch core chưa có warmup riêng — dùng LambdaLR hoặc HuggingFace get_cosine_schedule_with_warmup:
from torch.optim.lr_scheduler import LambdaLR
import math
def lr_lambda(step):
if step < warmup_steps:
return step / warmup_steps
progress = (step - warmup_steps) / (total_steps - warmup_steps)
return 0.5 * (1.0 + math.cos(math.pi * progress))
scheduler = LambdaLR(optimizer, lr_lambda)
OneCycleLR
Smith (2017, arXiv:1708.07120) đề xuất chính sách 1-cycle: trong toàn bộ training, LR tăng từ max_lr/div_factor lên max_lr ở khoảng giữa, rồi giảm về một giá trị rất nhỏ ở cuối. Đồng thời momentum giảm khi LR tăng và ngược lại.
from torch.optim.lr_scheduler import OneCycleLR
scheduler = OneCycleLR(
optimizer,
max_lr=0.1,
total_steps=epochs * len(loader),
pct_start=0.3, # 30% step đầu tăng LR
anneal_strategy="cos",
)
Aggressive: max_lr thường cao hơn LR thông thường 3–10 lần. Smith gọi đây là "super-convergence" — train CIFAR-10 trong ~70 epoch đến accuracy ~94% (thay vì 200+ epoch theo recipe truyền thống).
Lưu ý quan trọng:
scheduler.step()phải gọi sau mỗi batch, không phải sau mỗi epoch.total_stepsphải đúng — nếu sai, scheduler raise lỗi khi vượt quá.- Tăng momentum khi LR thấp (đầu/cuối), giảm momentum khi LR cao (giữa) — pattern này được PyTorch implement sẵn.
API pattern trong PyTorch
Tất cả scheduler trong torch.optim.lr_scheduler theo cùng một pattern:
import torch.optim as optim
optimizer = optim.AdamW(model.parameters(), lr=1e-3)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
for epoch in range(epochs):
train_epoch(model, loader, optimizer, criterion)
val_loss = validate(model, val_loader, criterion)
scheduler.step() # cập nhật LR
print(f"epoch {epoch} lr = {scheduler.get_last_lr()[0]:.6f}")
Ba điểm cần nhớ:
- Khởi tạo scheduler sau optimizer; truyền optimizer vào constructor.
- Gọi
scheduler.step()trong mỗi vòng lặp (đa số sau epoch, một số sau batch — xem mục 13). - Đọc LR hiện tại qua
scheduler.get_last_lr()(list, một giá trị mỗi param group). Cách thay thế:optimizer.param_groups[0]["lr"].
Scheduler không thay optimizer — nó chỉ overwrite param_groups[i]["lr"]. Nhờ vậy có thể chain nhiều scheduler hoặc chuyển đổi giữa các schedule mà optimizer state (momentum, \( m_t, v_t \)) vẫn nguyên.
Vị trí gọi scheduler.step()
Vị trí gọi step() phụ thuộc vào loại schedule:
- Sau mỗi epoch (default cho hầu hết): StepLR, MultiStepLR, ExponentialLR, CosineAnnealingLR, ReduceLROnPlateau.
- Sau mỗi batch: OneCycleLR, CosineAnnealingWarmRestarts (nếu muốn smooth), warmup-based scheduler cho Transformer.
- ReduceLROnPlateau: phải truyền metric:
scheduler.step(val_loss)hoặcscheduler.step(val_acc)tuỳmode.
Per-epoch pattern:
for epoch in range(epochs):
for x, y in train_loader:
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
scheduler.step() # sau epoch
Per-batch pattern (OneCycleLR, warmup):
for epoch in range(epochs):
for x, y in train_loader:
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
scheduler.step() # sau batch
PyTorch từ 1.1+ có warning nếu gọi scheduler.step() trước optimizer.step() đầu tiên — đảm bảo thứ tự đúng: optimizer step → (optional) scheduler step.
Chọn schedule theo task
Heuristic dựa trên thực tế codebase và recipe phổ biến:
- Tabular / small data:
ReduceLROnPlateauhoặcStepLR. Số epoch thay đổi mạnh theo seed → reactive an toàn hơn. - CNN ImageNet:
MultiStepLR(recipe ResNet kinh điển) hoặcCosineAnnealingLR(recipe modern). Cosine thường nhỉnh hơn ~0.2%. - CNN CIFAR / small dataset:
OneCycleLRcho train nhanh, hoặc Cosine cho train chuẩn. - Transformer / LLM pretraining: linear warmup + cosine decay. Warmup là bắt buộc — không có thì pretraining diverge.
- Fine-tuning LLM:
ReduceLROnPlateau, linear decay, hoặc cosine từ peak về 10% peak. Warmup ngắn (vài trăm step). - RL: thường LR cố định hoặc decay rất nhẹ — RL nhạy với LR theo cách khác (policy thay đổi → distribution thay đổi).
Quy tắc khởi đầu cho người mới: thử Cosine trước. Đơn giản, ít hyperparameter (chỉ T_max), kết quả ổn cho hầu hết bài. Sau đó mới cân nhắc các schedule khác nếu có lý do cụ thể.
LR Finder
LR finder (Smith 2015) là kỹ thuật chọn LR ban đầu — không phải schedule nhưng liên quan chặt vì \( \eta_0 \) là input của mọi schedule.
Cách làm:
- Train một vài epoch (hoặc 100–500 step) với LR tăng exponential từ rất nhỏ (\( 10^{-7} \)) lên rất lớn (\( 10 \)).
- Plot loss theo LR (trục \( x \) là LR log scale).
- Tìm LR ở "vùng giảm dốc nhất" — không phải LR ở điểm loss thấp nhất, mà LR khoảng \( 10\times \) trước điểm loss bắt đầu tăng vọt.
Công cụ:
- PyTorch Lightning:
trainer.tuner.lr_find(model). - fastai:
learn.lr_find(). - torch-lr-finder (package độc lập):
from torch_lr_finder import LRFinder.
Output thường gợi ý một LR — nhưng đừng tin tuyệt đối. Coi nó như starting point, sau đó tinh chỉnh dựa trên loss curve thực tế.
Pitfall thường gặp
- Quên gọi
scheduler.step(): LR giữ nguyên cả training. Triệu chứng:scheduler.get_last_lr()trả về cùng giá trị mọi epoch. - Sai vị trí (per-batch vs per-epoch): gọi OneCycleLR sau mỗi epoch thì sau vài epoch sẽ vượt
total_stepsvà raise. Ngược lại gọi CosineAnnealingLR sau mỗi batch vớiT_max=epochsthì LR giảm cực nhanh (giảm trongepochsbatch thay vìepochsepoch). - ReduceLROnPlateau không truyền metric:
scheduler.step()không có argument sẽ raiseTypeError. - Resume checkpoint quên load scheduler state: scheduler reset về \( t = 0 \), LR nhảy lên giá trị ban đầu — "vết nẩy" ở điểm resume. Lưu cùng model và optimizer state (mục 17).
- LR giảm quá nhanh: \( \gamma \) quá thấp trong Exponential, hoặc
T_maxquá nhỏ trong Cosine, hoặc patience quá nhỏ trong Plateau → LR teo trước khi model học xong → training stuck. - Chain scheduler không cẩn thận: nếu dùng
ChainedSchedulerhoặcSequentialLR, kiểm trascheduler.get_last_lr()ở vài epoch đầu để chắc nó hoạt động đúng — debug nhanh hơn là plot xong mới phát hiện.
Save / load scheduler state
Scheduler có state nội tại — chủ yếu là counter last_epoch. Khi resume training, phải lưu và khôi phục cùng model + optimizer:
# Save
torch.save({
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"scheduler_state_dict": scheduler.state_dict(),
}, "checkpoint.pt")
# Load
ckpt = torch.load("checkpoint.pt")
model.load_state_dict(ckpt["model_state_dict"])
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
start_epoch = ckpt["epoch"] + 1
Nếu chỉ load model + optimizer mà bỏ scheduler:
- Cosine: reset về peak → một bước "nhảy" LR lên cao → loss tăng đột ngột.
- Step / MultiStep: counter mốc reset → giảm LR sai epoch.
- Plateau:
patiencecounter reset → có thể giảm LR muộn hoặc sớm so với run liền mạch.
Hệ quả thực tế: loss curve resume có "vết nẩy" rõ tại điểm khởi động lại; metric có thể giảm tạm thời. Verify bằng so sánh loss curve resume vs run liền mạch (xem bài tập).
Liên hệ với optimizer
Scheduler không sở hữu LR — nó chỉ điều khiển trường lr của optimizer.param_groups:
optimizer = optim.AdamW(model.parameters(), lr=1e-3)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)
print(optimizer.param_groups[0]["lr"]) # 0.001
scheduler.step()
print(optimizer.param_groups[0]["lr"]) # giảm theo cosine
Hệ quả khi dùng param groups (B17 mục 16):
- Mỗi group có LR riêng và scheduler scale tất cả theo cùng factor \( f(t) \). Nếu group 0 khởi tạo \( 10^{-5} \) và group 1 khởi tạo \( 10^{-3} \), sau cosine annealing cả hai cùng giảm về \( 10\% \) giá trị ban đầu — tỉ lệ \( 100\times \) giữa hai group được bảo toàn.
- Muốn schedule khác nhau cho từng group: dùng
LambdaLRvới list lambda — một lambda cho mỗi group.
scheduler = optim.lr_scheduler.LambdaLR(
optimizer,
lr_lambda=[lambda t: 0.95**t, # group 0: exponential
lambda t: 1.0], # group 1: giữ nguyên
)
Code Python — 3 scheduler trên MLP
So sánh StepLR, CosineAnnealingLR, ReduceLROnPlateau trên MLP iris. Plot LR và loss theo epoch.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import (
StepLR, CosineAnnealingLR, ReduceLROnPlateau,
)
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
torch.manual_seed(0)
X, y = load_iris(return_X_y=True)
X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.25, random_state=0)
scaler = StandardScaler().fit(X_tr)
X_tr = torch.tensor(scaler.transform(X_tr), dtype=torch.float32)
X_va = torch.tensor(scaler.transform(X_va), dtype=torch.float32)
y_tr = torch.tensor(y_tr, dtype=torch.long)
y_va = torch.tensor(y_va, dtype=torch.long)
def make_model():
torch.manual_seed(0)
return nn.Sequential(nn.Linear(4, 16), nn.ReLU(), nn.Linear(16, 3))
def train(scheduler_name, epochs=100):
model = make_model()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-2)
if scheduler_name == "step":
scheduler = StepLR(optimizer, step_size=30, gamma=0.5)
elif scheduler_name == "cosine":
scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-5)
elif scheduler_name == "plateau":
scheduler = ReduceLROnPlateau(
optimizer, mode="min", factor=0.5, patience=5, min_lr=1e-5,
)
lrs, train_losses, val_losses = [], [], []
for epoch in range(epochs):
optimizer.zero_grad()
out = model(X_tr)
loss = criterion(out, y_tr)
loss.backward()
optimizer.step()
with torch.no_grad():
val_loss = criterion(model(X_va), y_va).item()
lrs.append(optimizer.param_groups[0]["lr"])
train_losses.append(loss.item())
val_losses.append(val_loss)
if scheduler_name == "plateau":
scheduler.step(val_loss)
else:
scheduler.step()
return lrs, train_losses, val_losses
for name in ["step", "cosine", "plateau"]:
lrs, tr_loss, va_loss = train(name)
print(f"{name:8s} lr[0]={lrs[0]:.4f} lr[-1]={lrs[-1]:.6f}"
f" train_loss[-1]={tr_loss[-1]:.4f} val_loss[-1]={va_loss[-1]:.4f}")
Plot LR theo epoch (nếu có matplotlib):
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
for name in ["step", "cosine", "plateau"]:
lrs, tr_loss, _ = train(name)
ax[0].plot(lrs, label=name)
ax[1].plot(tr_loss, label=name)
ax[0].set_title("learning rate"); ax[0].set_xlabel("epoch"); ax[0].legend()
ax[1].set_title("train loss"); ax[1].set_xlabel("epoch"); ax[1].legend()
plt.tight_layout(); plt.show()
Kết quả điển hình: cả ba scheduler đạt val accuracy 95%+; Cosine cho đường LR mượt, Step có bậc thang, Plateau giảm LR khi val_loss không cải thiện. Trên iris (data nhỏ) khác biệt loss cuối là nhỏ — schedule phát huy rõ trên bài lớn hơn.
Bài tập
- Train MLP iris với
CosineAnnealingLR(T_max=100, eta_min=1e-5)và optimizer Adamlr=1e-2. Plot LR và train/val loss theo epoch. Quan sát LR có giảm mượt theo cosine không. - So sánh cosine với LR cố định (không dùng scheduler) trên cùng setup. Train 200 epoch, ghi val_loss tốt nhất. Cosine cải thiện bao nhiêu?
- Implement warmup + cosine decay bằng tay với
LambdaLR: 10 epoch warmup tuyến tính từ 0 lên LR target, 90 epoch cosine về 0. Plot LR. - Train với
ReduceLROnPlateau(patience=5, factor=0.5). Mở rộng training tới khi LR chạmmin_lr. Ghi lại các epoch mà LR bị giảm — chúng có khớp với điểm val_loss phẳng không? - Demo pitfall: dùng
CosineAnnealingLRnhưng gọischeduler.step()trong vòng batch thay vì sau epoch. Quan sát LR teo sau bao nhiêu batch. - Demo pitfall resume: train 50 epoch với Cosine, save model+optimizer (KHÔNG save scheduler). Load lại, khởi tạo scheduler mới, train tiếp 50 epoch. So sánh loss curve với baseline train liền mạch 100 epoch — có "vết nẩy" ở epoch 50 không?
- Lặp lại câu 6 nhưng có save scheduler state. Loss curve có khớp với baseline liền mạch?
- Dùng
OneCycleLR(max_lr=0.1, total_steps=100, pct_start=0.3)— gọischeduler.step()sau mỗi epoch (đúng vì ở đây 100 step = 100 epoch). Plot LR — có thấy LR tăng đến epoch 30 rồi giảm về cuối không? - Implement LR finder thủ công: train 200 step với LR tăng exponential từ \( 10^{-6} \) lên \( 1 \) qua
LambdaLRvớilr_lambda = lambda step: 10 ** (step / 200 * 6 - 6). Plot loss vs LR (log x). Chọn LR ở vùng giảm dốc nhất.
Đáp án ngắn
- LR giảm theo đường cosine từ
1e-2về1e-5qua 100 epoch. - Cosine thường cải thiện 0.1–0.5% accuracy hoặc giảm val_loss 5–15% trên iris; khác biệt rõ hơn trên CIFAR-10.
- LR tăng tuyến tính 0 → target trong 10 epoch, sau đó cosine về gần 0.
- Mỗi lần LR giảm phải trùng với một plateau \( \geq \)
patienceepoch không cải thiện val_loss. - Nếu có \( B \) batch / epoch, sau 1 epoch LR đã chạy \( B \) bước cosine — \( T_{\max} \) thực sự là \( T_{\max}/B \) epoch. LR teo về
eta_minrất sớm. - Có "vết nẩy" rõ tại epoch 50 — LR reset về peak.
- Loss curve trùng với baseline (chênh lệch ở mức sai số float).
- LR vẽ thành tam giác / cong tăng-giảm với đỉnh ở epoch 30.
- Loss giảm dần khi LR tăng, đến một điểm nó tăng vọt → chọn LR khoảng \( 10\times \) trước điểm đó (thường là \( 10^{-3} \)–\( 10^{-2} \) cho Adam).
Tóm tắt
- LR lớn ở đầu để explore, LR nhỏ ở cuối để settle. LR cố định là compromise — schedule cho phép cả hai.
- Scheduler overwrite
optimizer.param_groups[i]["lr"]theo công thức \( \eta_t = \eta_0 \cdot f(t) \). - Step: \( \eta_t = \eta_0 \, \gamma^{\lfloor t/s \rfloor} \). Đơn giản, dễ tune.
StepLR(step_size=30, gamma=0.1). - MultiStep: giảm tại các mốc cụ thể. Phù hợp khi bám recipe của paper.
- Exponential: \( \eta_t = \eta_0 \, \gamma^t \). Mượt, dễ teo nhanh nếu \( \gamma \) thấp.
- Cosine: \( \eta_t = \eta_{\min} + \frac{1}{2}(\eta_0 - \eta_{\min})(1 + \cos(t \pi / T)) \). Default modern.
- Warm Restarts: reset LR định kỳ, hữu ích cho snapshot ensemble.
- ReduceLROnPlateau: reactive, nhìn val metric, giảm khi không cải thiện. Phải truyền metric vào
step(). - Warmup: tăng LR từ 0 lên target trong \( T_w \) step đầu. Bắt buộc cho Transformer pretraining.
- OneCycleLR: LR tăng rồi giảm trong 1 cycle. Aggressive, train nhanh. Gọi
step()sau mỗi batch. - Vị trí
step(): per-epoch cho đa số, per-batch cho OneCycleLR và warmup; ReduceLROnPlateau cần metric. - Chọn schedule: Tabular → Plateau / Step. CNN ImageNet → Step / Cosine. Transformer → Warmup + Cosine. Fine-tuning → Plateau / Linear.
- LR Finder (Smith 2015): tăng LR exponential, plot loss vs LR, chọn vùng giảm dốc nhất.
- Pitfall: quên
step(); sai vị trí; quên truyền metric cho Plateau; resume mà không load scheduler state; LR teo quá nhanh. - Save/load: lưu
scheduler.state_dict()cùng model và optimizer khi checkpoint.
- PyTorch Docs - How to adjust learning rate
- PyTorch Docs - StepLR
- PyTorch Docs - MultiStepLR
- PyTorch Docs - ExponentialLR
- PyTorch Docs - CosineAnnealingLR
- PyTorch Docs - CosineAnnealingWarmRestarts
- PyTorch Docs - ReduceLROnPlateau
- PyTorch Docs - OneCycleLR
- PyTorch Docs - LambdaLR
- Loshchilov & Hutter (2016) - SGDR: Stochastic Gradient Descent with Warm Restarts
- Smith (2017) - Super-Convergence: Very Fast Training Using Large Learning Rates (OneCycle)
- Smith (2015) - Cyclical Learning Rates for Training Neural Networks (LR Finder)
- Goyal et al. (2017) - Accurate, Large Minibatch SGD (warmup, linear scaling)
- Huang et al. (2017) - Snapshot Ensembles: Train 1, Get M for Free
- HuggingFace Transformers - Optimizer schedules
