Danh sách bài viết

Bài 25: Learning Rate Scheduling — giảm LR theo thời gian

Learning rate cố định ít khi tối ưu cho training dài. Bài này đi qua các schedule phổ biến (Step, MultiStep, Exponential, Cosine Annealing, Warm Restarts, ReduceLROnPlateau, Warmup, OneCycleLR), công thức cập nhật, API PyTorch (torch.optim.lr_scheduler), vị trí gọi scheduler.step(), cách chọn schedule theo task (tabular, CNN, Transformer, fine-tuning), LR finder và các pitfall thường gặp khi resume training.

24/05/2026
14 phút đọc
1 lượt xem
1

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ọi scheduler.step(), lấy LR hiện tại qua get_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).

2

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.

3

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.
4

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ự).

5

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ả.
6

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.

7

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.

8

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.

9

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.
10

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)
11

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_steps phả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.
12

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ớ:

  1. Khởi tạo scheduler sau optimizer; truyền optimizer vào constructor.
  2. Gọi scheduler.step() trong mỗi vòng lặp (đa số sau epoch, một số sau batch — xem mục 13).
  3. Đọ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.

13

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ặc scheduler.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.

14

Chọn schedule theo task

Heuristic dựa trên thực tế codebase và recipe phổ biến:

  • Tabular / small data: ReduceLROnPlateau hoặc StepLR. Số epoch thay đổi mạnh theo seed → reactive an toàn hơn.
  • CNN ImageNet: MultiStepLR (recipe ResNet kinh điển) hoặc CosineAnnealingLR (recipe modern). Cosine thường nhỉnh hơn ~0.2%.
  • CNN CIFAR / small dataset: OneCycleLR cho 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ể.

15

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:

  1. 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 \)).
  2. Plot loss theo LR (trục \( x \) là LR log scale).
  3. 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ế.

16

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_steps và raise. Ngược lại gọi CosineAnnealingLR sau mỗi batch với T_max=epochs thì LR giảm cực nhanh (giảm trong epochs batch thay vì epochs epoch).
  • ReduceLROnPlateau không truyền metric: scheduler.step() không có argument sẽ raise TypeError.
  • 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_max quá 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 ChainedScheduler hoặc SequentialLR, kiểm tra scheduler.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.
17

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: patience counter 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).

18

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 LambdaLR vớ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
)
19

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.

20

Bài tập

  1. Train MLP iris với CosineAnnealingLR(T_max=100, eta_min=1e-5) và optimizer Adam lr=1e-2. Plot LR và train/val loss theo epoch. Quan sát LR có giảm mượt theo cosine không.
  2. 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?
  3. 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.
  4. Train với ReduceLROnPlateau(patience=5, factor=0.5). Mở rộng training tới khi LR chạm min_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?
  5. Demo pitfall: dùng CosineAnnealingLR nhưng gọi scheduler.step() trong vòng batch thay vì sau epoch. Quan sát LR teo sau bao nhiêu batch.
  6. 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?
  7. Lặp lại câu 6 nhưng save scheduler state. Loss curve có khớp với baseline liền mạch?
  8. Dùng OneCycleLR(max_lr=0.1, total_steps=100, pct_start=0.3) — gọi scheduler.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?
  9. Implement LR finder thủ công: train 200 step với LR tăng exponential từ \( 10^{-6} \) lên \( 1 \) qua LambdaLR với lr_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
  1. LR giảm theo đường cosine từ 1e-2 về 1e-5 qua 100 epoch.
  2. 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.
  3. LR tăng tuyến tính 0 → target trong 10 epoch, sau đó cosine về gần 0.
  4. Mỗi lần LR giảm phải trùng với một plateau \( \geq \) patience epoch không cải thiện val_loss.
  5. 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_min rất sớm.
  6. Có "vết nẩy" rõ tại epoch 50 — LR reset về peak.
  7. Loss curve trùng với baseline (chênh lệch ở mức sai số float).
  8. LR vẽ thành tam giác / cong tăng-giảm với đỉnh ở epoch 30.
  9. 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).
21

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.