Skip to content

MIT 6.7960 L03:優化總覽——SGD、Adam、學習率排程與縮放規則

2026年8月30日 1 分鐘
TL;DR 從 SGD 到 Adam,用縮放規則一次搞懂深度學習優化器怎麼選、學習率怎麼調
目錄
  1. 優化器的譜系:從 SGD 到 Adam
  2. 縮放規則:大批次怎麼調學習率
  3. 學習率排程:Warmup → Cosine → Decay
  4. 實戰決策樹:新專案怎麼選優化器與超參數
  5. PyTorch 完整可跑範例:優化器對比實驗
  6. 常見坑與避雷指南
  7. 參考資料

🌏 English version

MIT 6.7960 Fall 2024 OCW 第 7 講 Scaling Rules for Optimization(縮放規則與優化)由 Jeremy Bernstein 授課。這講不只是列優化器清單,而是從梯度下降的動力學出發,推導為什麼大批次需要大學習率、為什麼 Adam 在某些情況下會失效、以及怎麼用「縮放規則」把小批次實驗的超參數轉移到大規模訓練。這篇文章把講義重點重組成可直接套用的決策框架,並附上可跑的 PyTorch 程式碼。

優化器的譜系:從 SGD 到 Adam

深度學習優化器演進的主線是:怎麼在高維非凸地形裡,用最少的超參數調整,穩定收斂到好解

優化器核心思想適用情境缺點
SGD純梯度下降,可加動量小模型、凸問題、要理論保證時深層網路收斂慢、需精心調 LR
SGD + Momentum累積歷史梯度方向,穿越鞍點影像分類、ResNet 類架構仍需手調 LR、對 ill-conditioned 敏感
Adam一階動量 + 二階自適應學習率NLP、Transformer、快速原型大批次易發散、weight decay 需分離
AdamWAdam + 解耦 weight decay現代 LLM/ViT 訓練預設同 Adam,但正則化更正確
Lion / Sophia符號梯度 / 二階近似大模型預訓練嘗試生態較新、超參數敏感

關鍵洞見:Bernstein 在講義中強調,優化器選擇不如「縮放規則」重要——同樣的優化器,學習率、批次大小、權重衰減若不按規則縮放,大模型照樣訓不動。

縮放規則:大批次怎麼調學習率

講義推導的核心公式(Linear Scaling Rule):

lr_new = lr_base × (batch_size_new / batch_size_base)

前提條件:

  • 使用 SGD + MomentumAdamW(自適應優化器近似成立)
  • 學習率在「穩定區間」內(太大會發散、太小收斂慢)
  • Warmup 步數同比例增長:warmup_steps_new = warmup_base × (batch_size_new / batch_size_base)

為什麼有效:大批次梯度方差下降 ∝ 1/√B,信噪比提升,可以承受更大步長。但超過「critical batch size」後,收益遞減甚至發散。

實務上對 Transformer 採用 sqrt scaling(學習率 ∝ √B)更穩健,見 Kaplan et al. 2020Chinchilla 的實驗。

學習率排程:Warmup → Cosine → Decay

現代標準排程(Warmup + Cosine Annealing):

import torch
from torch.optim.lr_scheduler import LambdaLR, CosineAnnealingLR, SequentialLR

def get_lr_scheduler(optimizer, warmup_steps, total_steps, min_lr_ratio=0.1):
    """Warmup + Cosine decay,回傳可直接 step() 的 scheduler"""
    def warmup_lambda(step):
        return min(1.0, step / warmup_steps)
    
    warmup_scheduler = LambdaLR(optimizer, warmup_lambda)
    cosine_scheduler = CosineAnnealingLR(
        optimizer, 
        T_max=total_steps - warmup_steps,
        eta_min=optimizer.param_groups[0]['lr'] * min_lr_ratio
    )
    return SequentialLR(optimizer, [warmup_scheduler, cosine_scheduler], [warmup_steps])

# 使用範例
model = torch.nn.Linear(512, 10)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
scheduler = get_lr_scheduler(optimizer, warmup_steps=2000, total_steps=100_000)

for step in range(100_000):
    loss = model(torch.randn(32, 512)).sum()
    loss.backward()
    optimizer.step()
    scheduler.step()
    optimizer.zero_grad()

影片時間戳

  • 0:00–12:00 SGD 動量與 Nesterov 加速推導
  • 12:00–28:00 Adam/AdamW 內部機制與 weight decay 解耦
  • 28:00–42:00 縮放規則推導與 critical batch size
  • 42:00–55:00 學習率排程實務(warmup、cosine、constant、reduce-on-plateau)
  • 55:00–1:10:00 實驗展示:不同批次大小下的收斂曲線對比

實戰決策樹:新專案怎麼選優化器與超參數

START: 新模型、新資料集

├─ 是不是 Transformer / LLM / ViT 大模型?
│   ├─ 是 → AdamW (lr=3e-4, wd=0.1, β=(0.9,0.95)) + Warmup+Cosine
│   └─ 否 → 是 CNN (ResNet/EfficientNet)?
│       ├─ 是 → SGD + Momentum (lr=0.1, momentum=0.9, wd=1e-4) + Cosine
│       └─ 否 → 先試 AdamW (lr=1e-3, wd=0.01),觀察 loss curve

├─ 批次大小要放大?
│   ├─ 是 → 線性縮放 lr、同比例增 warmup、監控 gradient norm
│   └─ 否 → 維持 base config

└─ 觀察訓練前 1000 steps:
    ├─ Loss 爆炸 → lr 除 10、加 gradient clipping (1.0)
    ├─ Loss 震盪不下降 → lr 除 3、延長 warmup
    └─ Loss 平滑下降 → 繼續跑、記錄 best checkpoint

PyTorch 完整可跑範例:優化器對比實驗

"""優化器對比:SGD vs AdamW 在 MLP 上的收斂行為"""
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

class MLP(nn.Module):
    def __init__(self, dim=256, depth=4):
        super().__init__()
        layers = []
        for _ in range(depth):
            layers += [nn.Linear(dim, dim), nn.ReLU()]
        layers.append(nn.Linear(dim, 10))
        self.net = nn.Sequential(*layers)
    
    def forward(self, x):
        return self.net(x)

def train_one_epoch(model, opt, scheduler, loader, device):
    model.train()
    total_loss = 0
    for x, y in loader:
        x, y = x.to(device), y.to(device)
        opt.zero_grad()
        loss = nn.functional.cross_entropy(model(x), y)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        opt.step()
        if scheduler:
            scheduler.step()
        total_loss += loss.item()
    return total_loss / len(loader)

# 合成資料
torch.manual_seed(42)
train_data = torch.utils.data.TensorDataset(
    torch.randn(5000, 256), torch.randint(0, 10, (5000,))
)
loader = torch.utils.data.DataLoader(train_data, batch_size=128, shuffle=True)
device = 'cuda' if torch.cuda.is_available() else 'cpu'

# 三組優化器設定
configs = {
    'SGD+Momentum': dict(lr=0.1, momentum=0.9, weight_decay=1e-4, opt_fn=torch.optim.SGD),
    'Adam': dict(lr=3e-4, betas=(0.9, 0.999), weight_decay=0.1, opt_fn=torch.optim.Adam),
    'AdamW': dict(lr=3e-4, betas=(0.9, 0.95), weight_decay=0.1, opt_fn=torch.optim.AdamW),
}

results = {}
for name, cfg in configs.items():
    opt_fn = cfg.pop('opt_fn')
    model = MLP().to(device)
    opt = opt_fn(model.parameters(), **cfg)
    scheduler = get_lr_scheduler(opt, warmup_steps=50, total_steps=500)
    
    losses = []
    for epoch in range(20):
        loss = train_one_epoch(model, opt, scheduler, loader, device)
        losses.append(loss)
    results[name] = losses
    print(f"{name}: final loss = {losses[-1]:.4f}")

# 繪圖
plt.figure(figsize=(8, 5))
for name, losses in results.items():
    plt.plot(losses, label=name, marker='o')
plt.yscale('log')
plt.xlabel('Epoch')
plt.ylabel('Loss (log scale)')
plt.title('Optimizer Comparison on Synthetic MLP Task')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150)
print("Saved plot to optimizer_comparison.png")

常見坑與避雷指南

症狀可能原因修正
Loss 前幾步就變 NaNlr 太大、無 gradient clippinglr ÷ 10、加 clip_grad_norm_(1.0)
驗證 loss 不降、訓練 loss 降過擬合、weight decay 太小調大 wd、加 dropout、早停
大批次訓練不收斂Linear scaling 失效改 sqrt scaling、延長 warmup、檢查 batch norm 統計量
AdamW 權重衰減無效用了 weight_decay 參數但優化器是 Adam改用 torch.optim.AdamW(解耦)

參考資料