Skip to content

MIT 6.7960 L04:正則化實戰——Weight Decay、Dropout、Batch Norm 與標籤平滑

2026年8月30日 1 分鐘
TL;DR 正則化不只是防過擬合——Weight Decay、Dropout、BN、Label Smoothing 的機制與組合策略一次看懂
目錄
  1. 正則化四大支柱:機制、效果、適用場景
  2. Weight Decay:Adam vs AdamW 的關鍵差異
  3. Dropout:訓練 vs 推論的縮放陷阱
  4. Batch Norm:訓練/推模式的統計量切換
  5. Label Smoothing & Mixup:標籤端正則化
  6. 現代架構的正則化組合慣例
  7. 影片時間戳
  8. PyTorch 完整可跑範例:正則化消融實驗
  9. 常見坑與避雷指南
  10. 參考資料

🌏 English version

MIT 6.7960 Fall 2024 OCW 第 9 講 Hacker's Guide to Deep Learning 由 Sara Beery 授課。這講把正則化視為「工程工具箱」而非純理論:每種技巧解決什麼具體問題、怎麼正確用、怎麼跟其他技巧組合。這篇文章把講義重點整理成可直接套用的正則化決策表,並附上可跑的 PyTorch 程式碼。

正則化四大支柱:機制、效果、適用場景

技巧核心機制解決的問題現代預設值
Weight Decay (L2)權重向原點收縮,等價於高斯先驗大權重導致的數值不穩、泛化間隙AdamW: 0.1, SGD: 1e-4
Dropout訓練時隨機歸零神經元,推論時縮放共適應、集成效應近似0.1–0.3 (Transformer), 0.5 (MLP)
Batch Norm批次統計量標準化 + 可學習縮放平移內部協變量偏移、梯度消失/爆炸、隱性正則化momentum=0.1, eps=1e-5
Label Smoothing硬標籤 → 軟分布 (1-ε, ε/(K-1))過度自信、校準誤差、知識蒸餾基礎ε=0.1 (分類), 0.0 (蒸餾教師)

關鍵觀念:Beery 強調正則化不是「越強越好」——目標是把有效容量留給資料要學的模式,把雜訊容量壓掉。過度正則化會導致 underfitting,特別是在大模型、大資料下。

Weight Decay:Adam vs AdamW 的關鍵差異

Adam 的 weight decay 有 bug:原始 Adam 把 L2 懲罰項直接加在梯度上 g ← g + λw,但自適應學習率會把這項也縮放,導致大梯度參數的 weight decay 效果被稀釋。

AdamW 解耦

# Adam (錯誤做法)
g = grad + λ * w
m = β1*m + (1-β1)*g
v = β2*v + (1-β2)*
w = w - lr * m / (√v + ε)

# AdamW (正確做法)
m = β1*m + (1-β1)*grad
v = β2*v + (1-β2)*grad²
w = w - lr * (m / (√v + ε) + λ * w)  # weight decay 直接作用在權重上

實務上永遠用 torch.optim.AdamW,不要用 Adamweight_decay 參數。

Dropout:訓練 vs 推論的縮放陷阱

標準 Dropout(Inverted Dropout):

  • 訓練:x * mask / (1-p) 其中 mask ~ Bernoulli(1-p)
  • 推論:x(不乘 mask,也不縮放,因為訓練已經期望值校正)
# PyTorch nn.Dropout 已內建 inverted scaling
dropout = nn.Dropout(p=0.1)  # Transformer 注意力後常用 0.1

# 手寫版本(教學用)
def dropout_forward(x, p, training):
    if not training:
        return x
    mask = (torch.rand_like(x) > p).float()
    return x * mask / (1 - p)

常見錯誤:推論忘記 model.eval(),導致 Dropout 仍在隨機歸零,輸出不穩定。

Batch Norm:訓練/推模式的統計量切換

Batch Norm 維護 running statistics:

  • 訓練:用當前 batch 的 mean, var 正規化,同時更新 running_mean, running_var(動量更新)
  • 推論:用累積的 running_mean, running_var 正規化
bn = nn.BatchNorm1d(256, momentum=0.1, eps=1e-5)

# 訓練模式
model.train()
out = bn(x)  # 用 batch 統計量

# 推論模式
model.eval()
out = bn(x)  # 用 running 統計量

關鍵細節

  • 小批次(< 16)時 BN 統計量噪聲大 → 改用 Group NormLayer Norm
  • 預訓練模型微調時:凍結 BN 統計量model.eval() 只對 BN 層)可防災難性遺忘
  • SyncBN(多 GPU 同步統計量)對大批次分佈式訓練必要

Label Smoothing & Mixup:標籤端正則化

Label Smoothing

def label_smoothing_loss(logits, targets, epsilon=0.1):
    """logits: [B, C], targets: [B] (class indices)"""
    log_probs = torch.log_softmax(logits, dim=-1)
    n_classes = logits.size(-1)
    # one-hot -> smoothed
    true_dist = torch.zeros_like(log_probs).scatter_(1, targets.unsqueeze(1), 1.0)
    true_dist = true_dist * (1 - epsilon) + epsilon / n_classes
    return torch.mean(torch.sum(-true_dist * log_probs, dim=-1))

# PyTorch 內建 (>= 1.10)
loss_fn = nn.CrossEntropyLoss(label_smoothing=0.1)

Mixup:兩樣本線性插值

def mixup_data(x, y, alpha=0.2):
    lam = np.random.beta(alpha, alpha)
    index = torch.randperm(x.size(0))
    mixed_x = lam * x + (1 - lam) * x[index]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

def mixup_loss(criterion, pred, y_a, y_b, lam):
    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)

現代架構的正則化組合慣例

架構Weight DecayDropoutBatch/Layer NormLabel SmoothingMixup/CutMix
ResNet (ImageNet)1e-4 (SGD)BN0.1CutMix α=1.0
ViT / DeiT0.1 (AdamW)0.1 (attn + MLP)LN0.1Mixup α=0.8
BERT / GPT0.1 (AdamW)0.1 (residual)LN無 (MLM 用整詞遮蔽)
EfficientNet1e-5 (RMSProp)0.2 (stochastic depth)BN0.1Mixup α=0.2

Stochastic Depth(DropPath)是現代 CNN/ViT 的隱藏正則化:

def drop_path(x, drop_prob=0.1, training=True):
    if not training or drop_prob == 0.:
        return x
    keep_prob = 1 - drop_prob
    shape = (x.shape[0],) + (1,) * (x.ndim - 1)
    random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
    random_tensor.floor_()
    return x.div(keep_prob) * random_tensor

影片時間戳

  • 0:00–15:00 Weight Decay 理論與 AdamW 解耦推導
  • 15:00–30:00 Dropout 機制、inverted scaling、推論模式
  • 30:00–45:00 Batch Norm 訓練/推論統計量、SyncBN、小批次替代方案
  • 45:00–58:00 Label Smoothing、Mixup、CutMix、知識蒸餾連結
  • 58:00–1:10:00 實戰清單:新專案正則化配置檢查表

PyTorch 完整可跑範例:正則化消融實驗

"""正則化消融:在 CIFAR-10 上測試各組合效果"""
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

class SimpleCNN(nn.Module):
    def __init__(self, dropout=0.0, use_bn=True, num_classes=10):
        super().__init__()
        self.use_bn = use_bn
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(32) if use_bn else nn.Identity()
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(64) if use_bn else nn.Identity()
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout = nn.Dropout(dropout)
        self.fc = nn.Linear(64 * 8 * 8, num_classes)
    
    def forward(self, x):
        x = self.pool(torch.relu(self.bn1(self.conv1(x))))
        x = self.pool(torch.relu(self.bn2(self.conv2(x))))
        x = x.view(x.size(0), -1)
        x = self.dropout(x)
        return self.fc(x)

def train_eval(config, epochs=5):
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    
    # 資料
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
    ])
    train_set = datasets.CIFAR10('./data', train=True, download=True, transform=transform)
    test_set = datasets.CIFAR10('./data', train=False, download=True, transform=transform)
    train_loader = DataLoader(train_set, batch_size=128, shuffle=True)
    test_loader = DataLoader(test_set, batch_size=256, shuffle=False)
    
    # 模型與優化器
    model = SimpleCNN(dropout=config['dropout'], use_bn=config['bn']).to(device)
    opt = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=config['wd'])
    criterion = nn.CrossEntropyLoss(label_smoothing=config['label_smooth'])
    
    # 訓練
    for epoch in range(epochs):
        model.train()
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            loss = criterion(model(x), y)
            loss.backward()
            opt.step()
    
    # 評估
    model.eval()
    correct = 0
    with torch.no_grad():
        for x, y in test_loader:
            x, y = x.to(device), y.to(device)
            pred = model(x).argmax(1)
            correct += (pred == y).sum().item()
    acc = correct / len(test_set)
    return acc

# 消融配置
configs = {
    'Baseline': dict(dropout=0.0, bn=True, wd=0.0, label_smooth=0.0),
    '+WeightDecay': dict(dropout=0.0, bn=True, wd=1e-4, label_smooth=0.0),
    '+Dropout': dict(dropout=0.2, bn=True, wd=1e-4, label_smooth=0.0),
    '+LabelSmooth': dict(dropout=0.2, bn=True, wd=1e-4, label_smooth=0.1),
    'NoBN+GroupNorm': dict(dropout=0.2, bn=False, wd=1e-4, label_smooth=0.1),  # 需改模型
}

print("Regularization Ablation on CIFAR-10 (5 epochs)")
for name, cfg in configs.items():
    if name == 'NoBN+GroupNorm':
        continue  # 略過需改架構的配置
    acc = train_eval(cfg)
    print(f"{name:20s}: Test Acc = {acc*100:.2f}%")

常見坑與避雷指南

症狀可能原因修正
訓練 loss 正常、驗證 loss 高且不降正則化太強、或模型容量不足減小 wd、dropout、label_smooth;增加模型寬度
BN 層推論結果每次不同忘記 model.eval()推論前必呼叫 model.eval()
AdamW weight decay 不生效用了 optim.Adam(weight_decay=...)改用 optim.AdamW(weight_decay=...)
Mixup 後 loss 計算錯誤直接用混合標籤算 CEmixup_loss 線性組合兩個 CE
小批次 BN 統計量不穩Batch size < 16改 GroupNorm(32) 或 LayerNorm

參考資料