Skip to content

损失函数与优化器 ​

损失函数(Loss Function)和优化器(Optimizer)是深度学习模型训练的两大核心组件。损失函数衡量模型预测值与真实值之间的差异,优化器则根据损失函数计算的梯度来更新模型参数。PyTorch 在 torch.nn 和 torch.optim 中提供了丰富的损失函数和优化器实现。


损失函数详解 ​

回归损失 ​

回归任务的目标是预测连续值,常用的损失函数如下:

MSELoss(均方误差损失) ​

MSE(x,y)=1n∑i=1n(xi−yi)2
python
import torch
import torch.nn as nn

# 创建 MSE 损失
criterion = nn.MSELoss(reduction='mean')  # 'mean', 'sum', 'none'

# 示例
predictions = torch.tensor([2.5, 0.0, 2.0, 8.0], dtype=torch.float32)
targets = torch.tensor([3.0, -0.5, 2.0, 7.0], dtype=torch.float32)

loss = criterion(predictions, targets)
print(f"MSE Loss: {loss.item():.4f}")  # 0.3750

# reduction='none' 返回每个样本的损失
loss_none = nn.MSELoss(reduction='none')(predictions, targets)
print(f"逐样本损失: {loss_none}")  # tensor([0.2500, 0.2500, 0.0000, 1.0000])

# reduction='sum'
loss_sum = nn.MSELoss(reduction='sum')(predictions, targets)
print(f"损失总和: {loss_sum.item():.4f}")  # 1.5000

L1Loss(平均绝对误差损失) ​

L1(x,y)=1n∑i=1n|xi−yi|
python
criterion = nn.L1Loss(reduction='mean')

loss = criterion(predictions, targets)
print(f"L1 Loss: {loss.item():.4f}")  # 0.5000

SmoothL1Loss / HuberLoss ​

结合 MSE 和 L1 Loss 的优点,对异常值更鲁棒:

SmoothL1(x,y)={0.5(x−y)2if |x−y|<β|x−y|−0.5βotherwise
python
# SmoothL1Loss(默认 beta=1.0)
criterion_smooth = nn.SmoothL1Loss(beta=1.0)

# HuberLoss 是 SmoothL1Loss 的等价形式(参数名不同: delta 而非 beta)
criterion_huber = nn.HuberLoss(delta=1.0)

loss_smooth = criterion_smooth(predictions, targets)
loss_huber = criterion_huber(predictions, targets)
print(f"SmoothL1 Loss: {loss_smooth.item():.4f}")
print(f"Huber Loss: {loss_huber.item():.4f}")

SmoothL1Loss vs MSELoss:SmoothL1Loss 在误差较小时类似 MSE(二次,梯度平滑),误差较大时类似 L1(线性,梯度恒定),因此对离群值更鲁棒。常用于目标检测中的 bbox 回归。

回归损失函数对比 ​

损失函数公式特点对异常值敏感度典型应用
MSELoss二次误差高(平方放大)一般回归
L1Loss线性误差低稀疏回归
SmoothL1Loss小误差二次 + 大误差线性低目标检测 bbox 回归
HuberLoss同 SmoothL1Loss低同上

分类损失 ​

CrossEntropyLoss(交叉熵损失) ​

分类任务中最常用的损失函数,内部包含 LogSoftmax 和 NLLLoss:

CE(x,y)=−log⁡(exp⁡(xy)∑jexp⁡(xj))
python
import torch
import torch.nn as nn

# 多类分类
criterion = nn.CrossEntropyLoss()

# logits(未经 softmax 的原始输出)
# batch_size=3, num_classes=5
logits = torch.tensor([
    [2.0, 1.0, 0.1, 0.5, 0.3],
    [0.5, 2.5, 0.3, 0.1, 0.6],
    [0.1, 0.2, 3.0, 0.4, 0.3]
], dtype=torch.float32)

# 类别标签(整数形式)
labels = torch.tensor([0, 1, 2], dtype=torch.long)

loss = criterion(logits, labels)
print(f"CrossEntropy Loss: {loss.item():.4f}")

# 带类别权重(处理类别不平衡)
class_weights = torch.tensor([1.0, 2.0, 1.0, 1.5, 1.0])
criterion_weighted = nn.CrossEntropyLoss(weight=class_weights)

# 带 label smoothing
criterion_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)

# ignore_index: 忽略特定标签(常用于语义分割中的背景/填充值)
criterion_ignore = nn.CrossEntropyLoss(ignore_index=255)

注意:CrossEntropyLoss 的输入是原始 logits(未经 softmax),不是 softmax 后的概率。

NLLLoss(负对数似然损失) ​

NLL(x,y)=−xy
python
# NLLLoss 通常与 log_softmax 配合使用
log_probs = torch.log_softmax(logits, dim=1)  # 先手动 log_softmax
criterion_nll = nn.NLLLoss()
loss_nll = criterion_nll(log_probs, labels)
print(f"NLL Loss: {loss_nll.item():.4f}")

# CrossEntropyLoss(logits) == NLLLoss(log_softmax(logits))
# 两者数学上等价,但 CrossEntropyLoss 数值上更稳定

BCELoss(二元交叉熵损失) ​

用于二分类或多标签分类:

BCE(x,y)=−1n∑i=1n[yilog⁡(xi)+(1−yi)log⁡(1−xi)]
python
# 多标签分类:每个标签独立地做二分类
criterion_bce = nn.BCELoss()

# 输入必须是经过 sigmoid 的概率值
predictions = torch.sigmoid(torch.randn(4, 5))  # batch=4, 5个标签
targets = torch.tensor([
    [1, 0, 1, 0, 1],
    [0, 1, 0, 1, 0],
    [1, 1, 0, 0, 1],
    [0, 0, 1, 1, 0]
], dtype=torch.float32)

loss_bce = criterion_bce(predictions, targets)
print(f"BCE Loss: {loss_bce.item():.4f}")

BCEWithLogitsLoss ​

与 BCELoss 等价,但内部自动应用 sigmoid,数值更稳定:

python
# 推荐使用这个,而不是先 sigmoid 再 BCELoss
criterion_bce_logits = nn.BCEWithLogitsLoss()

# 输入是原始 logits
raw_logits = torch.randn(4, 5)
loss = criterion_bce_logits(raw_logits, targets)
print(f"BCEWithLogits Loss: {loss.item():.4f}")

# 同样支持 pos_weight 处理正负样本不均衡
pos_weight = torch.tensor([2.0, 2.0, 2.0, 2.0, 2.0])
criterion_weighted = nn.BCEWithLogitsLoss(pos_weight=pos_weight)

FocalLoss ​

Focal Loss 用于解决类别极度不平衡的问题(如目标检测中的前景/背景),通过降低易分类样本的权重来聚焦于难分类样本:

FL(pt)=−αt(1−pt)γlog⁡(pt)
python
import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    """Focal Loss 实现"""

    def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
        """
        Args:
            alpha: 平衡因子,正类权重
            gamma: 聚焦参数,越大越关注难分类样本
            reduction: 'mean', 'sum', 'none'
        """
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction

    def forward(self, inputs, targets):
        """
        Args:
            inputs: 模型输出 logits (N, C) 或 (N,)
            targets: 目标标签
        """
        BCE_loss = F.binary_cross_entropy_with_logits(
            inputs, targets, reduction='none'
        )
        pt = torch.exp(-BCE_loss)  # pt = p if y=1, 1-p if y=0
        focal_weight = self.alpha * (1 - pt) ** self.gamma
        focal_loss = focal_weight * BCE_loss

        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        return focal_loss


# 使用 Focal Loss
focal_criterion = FocalLoss(alpha=0.25, gamma=2.0)
focal_loss = focal_criterion(raw_logits, targets)
print(f"Focal Loss: {focal_loss.item():.4f}")

分类损失函数对比 ​

损失函数输入格式任务类型说明
CrossEntropyLoss原始 logits + 整数标签多类分类内含 softmax,最常用
NLLLosslog 概率 + 整数标签多类分类需手动 log_softmax
BCELosssigmoid 后概率 + float 标签二分类/多标签输入需先 sigmoid
BCEWithLogitsLoss原始 logits + float 标签二分类/多标签推荐替代 BCELoss
FocalLoss原始 logits + float 标签不平衡分类需自行实现

其他损失函数 ​

KLDivLoss(KL 散度损失) ​

衡量两个概率分布之间的差异,常用于知识蒸馏:

python
criterion_kl = nn.KLDivLoss(reduction='batchmean')

# 输入是 log 概率,目标是概率分布
log_probs = torch.log_softmax(logits, dim=1)
target_probs = torch.softmax(torch.randn_like(logits), dim=1)

kl_loss = criterion_kl(log_probs, target_probs)
print(f"KL Divergence Loss: {kl_loss.item():.4f}")

# 知识蒸馏中的用法
# student_logits: 学生模型输出
# teacher_logits: 教师模型输出(soft labels)
def distillation_loss(student_logits, teacher_logits, labels,
                      temperature=4.0, alpha=0.7):
    soft_loss = nn.KLDivLoss(reduction='batchmean')(
        F.log_softmax(student_logits / temperature, dim=1),
        F.softmax(teacher_logits / temperature, dim=1)
    ) * (temperature ** 2)

    hard_loss = F.cross_entropy(student_logits, labels)

    return alpha * soft_loss + (1 - alpha) * hard_loss

CosineEmbeddingLoss ​

用于学习嵌入空间中的相似度,常用与度量学习(Metric Learning):

python
criterion_cos = nn.CosineEmbeddingLoss(margin=0.5)

# 两个嵌入向量
embedding1 = torch.randn(4, 128)
embedding2 = torch.randn(4, 128)

# 标签: 1 表示相似,-1 表示不相似
similar_labels = torch.tensor([1, -1, 1, -1], dtype=torch.float32)

loss = criterion_cos(embedding1, embedding2, similar_labels)
print(f"Cosine Embedding Loss: {loss.item():.4f}")

TripletMarginLoss(三元组损失) ​

常用于人脸识别、图像检索等度量学习任务:

python
criterion_triplet = nn.TripletMarginLoss(margin=1.0, p=2)

anchor = torch.randn(4, 128)    # 锚点样本
positive = torch.randn(4, 128)  # 正样本(同类)
negative = torch.randn(4, 128)  # 负样本(异类)

loss = criterion_triplet(anchor, positive, negative)
print(f"Triplet Margin Loss: {loss.item():.4f}")

HingeEmbeddingLoss ​

用于半监督学习或学习相似性:

python
criterion_hinge = nn.HingeEmbeddingLoss(margin=1.0)

x = torch.randn(4, 128)
y = torch.tensor([1, -1, 1, -1], dtype=torch.float32)

loss = criterion_hinge(x, y)
print(f"Hinge Embedding Loss: {loss.item():.4f}")

损失函数选择指南 ​

任务类型推荐损失函数说明
一般回归MSELoss最标准的回归损失
鲁棒回归(有异常值)SmoothL1Loss / HuberLoss对离群值不敏感
多类分类CrossEntropyLoss最常用,内置 softmax
二分类BCEWithLogitsLoss比 BCELoss 数值稳定
多标签分类BCEWithLogitsLoss每个标签独立二分类
类别极度不平衡FocalLoss需自行实现
知识蒸馏KLDivLoss + CrossEntropyLosssoft loss + hard loss
度量学习/人脸识别TripletMarginLoss / CosineEmbeddingLoss学习嵌入空间距离
语义分割CrossEntropyLoss(ignore_index=255)忽略无标注像素
生成对抗网络BCEWithLogitsLoss 或自定义判别器损失
目标检测SmoothL1Loss(bbox) + CrossEntropyLoss(cls) + BCEWithLogitsLoss(obj)组合使用

label_smoothing 参数 ​

标签平滑(Label Smoothing)是一种正则化技术,防止模型对训练标签过度自信:

python
# 标签平滑: 将 one-hot 标签 [0, 0, 1, 0, 0] 变为 [0.02, 0.02, 0.92, 0.02, 0.02]
# 公式: y_smooth = (1 - epsilon) * y_hot + epsilon / K
# epsilon: label_smoothing 参数值, K: 类别数

criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

# 对比有无 label smoothing
logits = torch.tensor([[5.0, 0.1, 0.1, 0.1, 0.1]])
labels = torch.tensor([0])

loss_no_smooth = nn.CrossEntropyLoss(label_smoothing=0.0)(logits, labels)
loss_with_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)(logits, labels)
print(f"无 label smoothing: {loss_no_smooth.item():.4f}")
print(f"有 label smoothing: {loss_with_smooth.item():.4f}")
# label smoothing 会略微增大损失,防止模型过度自信

优化器详解 ​

PyTorch 在 torch.optim 中提供了多种优化算法的实现。

SGD(随机梯度下降) ​

SGD 是最基础的优化算法。PyTorch 实现支持动量(Momentum)和 Nesterov 加速:

vt+1=μvt+gtpt+1=pt−ηvt+1
python
import torch.optim as optim

# 基础 SGD(无动量)
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 带动量的 SGD
optimizer = optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,          # 动量系数
    dampening=0,           # 动量的阻尼系数(通常为 0)
    weight_decay=1e-4,     # L2 正则化系数
    nesterov=False         # 是否使用 Nesterov 动量
)

# Nesterov 动量 SGD
optimizer_nesterov = optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    nesterov=True          # Nesterov 加速梯度
)

Nesterov 动量 vs 标准动量:Nesterov 动量先用当前动量"预看"一步,再在预看位置计算梯度,收敛通常更快更稳定。

Adam(自适应矩估计) ​

Adam 结合了动量和自适应学习率的优点,是目前最广泛使用的优化器之一:

python
# 基础 Adam
optimizer = optim.Adam(
    model.parameters(),
    lr=1e-3,
    betas=(0.9, 0.999),    # 一阶/二阶矩估计的指数衰减率
    eps=1e-8,              # 数值稳定性参数
    weight_decay=0,        # L2 正则化(注意:不是真正的权重衰减)
    amsgrad=False          # 是否使用 AMSGrad 变体
)

# AMSGrad 变体(保证学习率不增加)
optimizer_amsgrad = optim.Adam(
    model.parameters(),
    lr=1e-3,
    amsgrad=True
)

AdamW(解耦权重衰减的 Adam) ​

AdamW 将权重衰减与梯度更新解耦,是目前推荐使用的优化器:

python
optimizer = optim.AdamW(
    model.parameters(),
    lr=1e-3,
    betas=(0.9, 0.999),
    eps=1e-8,
    weight_decay=0.01,     # 真正的权重衰减
    amsgrad=False
)

Adam vs AdamW 中 weight_decay 的区别 ​

python
# Adam 的 weight_decay 实际上是 L2 正则化
# 将权重衰减加到梯度上: grad = grad + weight_decay * param
# 这与 L2 正则化在 Adam 中的效果相同,但与自适应学习率耦合

# AdamW 的 weight_decay 是解耦的
# 在参数更新后直接衰减: param = param - lr * weight_decay * param
# 不受自适应学习率缩放影响

# 推荐使用 AdamW,尤其在使用学习率调度器时
optimizer_adam = optim.Adam(model.parameters(), lr=1e-3, weight_decay=0.01)
optimizer_adamw = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)

RMSprop ​

自适应学习率优化器,适合处理非平稳目标:

python
optimizer = optim.RMSprop(
    model.parameters(),
    lr=1e-3,
    alpha=0.99,            # 平滑常数
    eps=1e-8,
    weight_decay=0,        # L2 正则化
    momentum=0,            # 动量
    centered=False         # 是否使用中心化的 RMSprop
)

Adagrad ​

自适应调整每个参数的学习率,适合稀疏数据:

python
optimizer = optim.Adagrad(
    model.parameters(),
    lr=1e-2,
    lr_decay=0,            # 学习率衰减
    weight_decay=0,
    eps=1e-10
)
# 注意:Adagrad 的学习率会单调递减,可能导致训练后期学习率过小

Adadelta ​

Adagrad 的改进版,使用滑动窗口代替累积梯度平方:

python
optimizer = optim.Adadelta(
    model.parameters(),
    lr=1.0,                # 默认 lr=1.0
    rho=0.9,               # 滑动平均系数
    eps=1e-6,
    weight_decay=0
)

优化器参数对比 ​

优化器关键参数自适应学习率适用场景训练速度
SGDlr, momentum, nesterov否CV 任务、精调慢
SGD + Momentumlr, momentum=0.9否ResNet 等经典模型中
Adamlr, betas, eps是通用,NLP、Transformer快
AdamWlr, betas, weight_decay是Transformer、大规模预训练快
RMSproplr, alpha, momentum是RNN、非平稳目标快
Adagradlr, lr_decay是稀疏数据、NLP中
Adadeltalr, rho是不需要手动设 lr 的场景中

学习率调度器 ​

学习率调度器(Learning Rate Scheduler)根据预设策略在训练过程中动态调整学习率。PyTorch 在 torch.optim.lr_scheduler 中提供了多种调度策略。

StepLR 与 MultiStepLR ​

python
import torch.optim.lr_scheduler as lr_scheduler

# StepLR: 每隔 step_size 个 epoch,学习率乘以 gamma
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
# epoch 0-29: lr=0.1
# epoch 30-59: lr=0.01
# epoch 60-89: lr=0.001
# ...

# MultiStepLR: 在指定的 epoch 列表处衰减学习率
scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[30, 60, 90], gamma=0.1)
# epoch 0-29: lr=0.1
# epoch 30-59: lr=0.01
# epoch 60-89: lr=0.001
# epoch 90+: lr=0.0001
python
# 训练循环中使用 scheduler
for epoch in range(num_epochs):
    train(model, train_loader, optimizer)
    validate(model, val_loader)

    # 在每个 epoch 结束后调用(StepLR, MultiStepLR 等)
    scheduler.step()
    print(f"Epoch {epoch}: lr = {scheduler.get_last_lr()[0]:.6f}")

ExponentialLR ​

python
# 每个 epoch 学习率乘以 gamma
scheduler = lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
# epoch 0: lr=0.1
# epoch 1: lr=0.095
# epoch 2: lr=0.09025
# ... 指数衰减

CosineAnnealingLR ​

学习率按余弦函数从初始值衰减到最小值,常用于训练后期的微调:

python
# T_max: 半周期的 epoch 数
# eta_min: 最小学习率
scheduler = lr_scheduler.CosineAnnealingLR(
    optimizer,
    T_max=200,       # 半个余弦周期的 epoch 数
    eta_min=1e-6     # 最小学习率
)

CosineAnnealingWarmRestarts ​

带有"热重启"的余弦退火,每个周期结束后学习率重新回到初始值:

python
scheduler = lr_scheduler.CosineAnnealingWarmRestarts(
    optimizer,
    T_0=10,          # 第一个重启周期的 epoch 数
    T_mult=2,        # 每次重启后周期长度的倍数
    eta_min=1e-6
)
# T_0=10, T_mult=2 时:
# 第 1 周期: epoch 0-9 (10 epochs)
# 第 2 周期: epoch 10-29 (20 epochs)
# 第 3 周期: epoch 30-69 (40 epochs)

ReduceLROnPlateau ​

当监控指标停止改善时自动降低学习率,是最实用的调度器之一:

python
scheduler = lr_scheduler.ReduceLROnPlateau(
    optimizer,
    mode='min',          # 'min' 表示指标越小越好,'max' 表示越大越好
    factor=0.1,          # 学习率衰减因子
    patience=10,         # 等待多少个 epoch 指标无改善
    threshold=1e-4,      # 判定改善的最小变化量
    min_lr=1e-7,         # 学习率下限
    verbose=True         # 学习率变化时打印信息
)

# 使用时需要传入监控指标的值
for epoch in range(num_epochs):
    train_loss = train(model, train_loader, optimizer)
    val_loss = validate(model, val_loader)

    # 注意:ReduceLROnPlateau 在 step 时传入监控值
    scheduler.step(val_loss)

OneCycleLR ​

Super-convergence 策略,学习率先增后减,可显著加速训练:

python
# OneCycleLR: 在一个训练周期内学习率先增大后减小
scheduler = lr_scheduler.OneCycleLR(
    optimizer,
    max_lr=0.01,              # 最大学习率
    total_steps=1000,         # 总训练步数
    epochs=None,              # 与 total_steps 二选一
    steps_per_epoch=None,     # 配合 epochs 使用
    pct_start=0.3,            # 学习率上升阶段占总步数的比例
    anneal_strategy='cos',    # 退火策略: 'cos' 或 'linear'
    div_factor=25.0,          # 初始学习率 = max_lr / div_factor
    final_div_factor=1e4      # 最终学习率 = 初始学习率 / final_div_factor
)

# OneCycleLR 需要在每个 batch 后调用 step()
for epoch in range(num_epochs):
    for batch in train_loader:
        optimizer.zero_grad()
        loss = compute_loss(model, batch)
        loss.backward()
        optimizer.step()
        scheduler.step()  # 每个 batch 后调用!

LambdaLR 与 SequentialLR ​

python
# LambdaLR: 使用自定义函数控制学习率
# warmup + 衰减的学习率策略
def warmup_cosine_fn(epoch):
    warmup_epochs = 5
    total_epochs = 100
    if epoch < warmup_epochs:
        return epoch / warmup_epochs  # 线性预热
    else:
        progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)
        return 0.5 * (1 + torch.cos(torch.tensor(progress * 3.14159)).item())

scheduler_lambda = lr_scheduler.LambdaLR(optimizer, lr_lambda=warmup_cosine_fn)

# SequentialLR: 按顺序串联多个调度器
scheduler1 = lr_scheduler.LinearLR(optimizer, start_factor=0.1, total_iters=5)  # warmup
scheduler2 = lr_scheduler.CosineAnnealingLR(optimizer, T_max=95)                # 余弦退火

scheduler_seq = lr_scheduler.SequentialLR(
    optimizer,
    schedulers=[scheduler1, scheduler2],
    milestones=[5]  # 在第 5 个 epoch 切换
)

学习率调度器使用总结 ​

python
# 类型一: epoch-based 调度器 —— 在每个 epoch 结束后调用 step()
# StepLR, MultiStepLR, ExponentialLR, CosineAnnealingLR,
# CosineAnnealingWarmRestarts, LambdaLR, SequentialLR
for epoch in range(num_epochs):
    train(...)
    scheduler.step()

# 类型二: metric-based 调度器 —— 在验证后传入指标调用 step(metric)
# ReduceLROnPlateau
for epoch in range(num_epochs):
    train(...)
    val_loss = validate(...)
    scheduler.step(val_loss)

# 类型三: batch-based 调度器 —— 在每个 batch 后调用 step()
# OneCycleLR, CyclicLR, CosineAnnealingWarmRestarts (with T_mult)
for epoch in range(num_epochs):
    for batch in train_loader:
        optimizer.zero_grad()
        loss = ...
        loss.backward()
        optimizer.step()
        scheduler.step()

学习率调度器对比表 ​

调度器调用时机核心参数特点
StepLR每 epochstep_size, gamma简单阶梯衰减
MultiStepLR每 epochmilestones, gamma在指定 epoch 衰减
ExponentialLR每 epochgamma指数衰减
CosineAnnealingLR每 epochT_max, eta_min余弦退火
CosineAnnealingWarmRestarts每 epochT_0, T_mult带重启的余弦退火
ReduceLROnPlateau验证后patience, factor自适应衰减
OneCycleLR每 batchmax_lr, total_steps超级收敛策略
LambdaLR每 epochlr_lambda自定义策略
SequentialLR取决于子调度器schedulers, milestones串联多个调度器
CyclicLR每 batchbase_lr, max_lr循环学习率
LinearLR每 epochstart_factor, total_iters线性缩放(常用于 warmup)

梯度裁剪 ​

梯度裁剪(Gradient Clipping)用于防止梯度爆炸,是训练 RNN、Transformer 等模型的常见技巧。

clip_grad_norm_(按范数裁剪) ​

将所有参数的梯度视为一个向量,若其范数超过阈值则等比缩放:

python
import torch.nn.utils as nn_utils

# 训练循环中
optimizer.zero_grad()
loss.backward()

# 按范数裁剪(最常用)
# max_norm: 范数的上限
# norm_type: 范数类型,默认 L2 范数
nn_utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0,
    norm_type=2
)

optimizer.step()

# 获取裁剪前的梯度范数(用于监控)
total_norm = nn_utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
print(f"梯度范数: {total_norm:.4f}")

clip_grad_value_(按值裁剪) ​

将每个梯度值限制在指定范围内:

python
# 将所有梯度值限制在 [-0.5, 0.5] 范围内
nn_utils.clip_grad_value_(model.parameters(), clip_value=0.5)

梯度裁剪对比 ​

方法说明适用场景
clip_grad_norm_等比缩放所有梯度,保持梯度方向RNN、Transformer、一般模型
clip_grad_value_硬截断每个梯度值简单但可能改变梯度方向
python
# 常见的 Transformer 训练配置中的梯度裁剪
optimizer.zero_grad()
loss = compute_loss(...)
loss.backward()

# 典型值: max_norm=1.0
nn_utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

optimizer.step()
scheduler.step()

权重衰减与 L2 正则化的关系 ​

L2 正则化和权重衰减在标准 SGD 中是等价的,但在自适应学习率优化器(如 Adam)中两者并不等价。

标准 SGD 中:L2 正则化 = 权重衰减 ​

python
# L2 正则化:在损失函数中加入权重的 L2 范数
# loss_reg = loss + lambda/2 * ||w||^2
# 梯度: grad = grad + lambda * w
# 更新: w = w - lr * (grad + lambda * w) = (1 - lr*lambda) * w - lr * grad

# PyTorch SGD 中的 weight_decay 实现的就是这个效果
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)

Adam 中:L2 正则化 ≠ 权重衰减 ​

python
# Adam 的 L2 正则化(Adam with weight_decay, 即 AdamW 的前身实现)
# 将 L2 项加到梯度上: grad = grad + lambda * w
# 然后通过自适应学习率缩放: w = w - lr / sqrt(v) * (m + lambda * w)
# 问题: lambda * w 也被自适应缩放了,正则化效果被削弱

# AdamW: 解耦权重衰减
# 先正常更新: w = w - lr / sqrt(v) * m
# 再衰减: w = w - lr * lambda * w = (1 - lr*lambda) * w
# 权重衰减不被自适应学习率缩放,效果更好

# 对比
optimizer_adam_with_l2 = optim.Adam(
    model.parameters(), lr=1e-3, weight_decay=0.01
)
optimizer_adamw = optim.AdamW(
    model.parameters(), lr=1e-3, weight_decay=0.01
)

实践建议:使用 Adam/AdamW 时,推荐使用 AdamW 进行权重衰减。使用 SGD 时两者等价,直接用 weight_decay 参数即可。


参数组与差异化学习率 ​

torch.optim 允许为模型的不同层设置不同的学习率和优化参数,这在迁移学习和微调中非常有用。

创建参数组 ​

python
import torch.optim as optim

# 示例:对预训练的 ResNet 进行微调
# backbone 使用较小学习率,新添加的分类头使用较大学习率

# 方法一:手动指定参数组
optimizer = optim.AdamW([
    {
        'params': model.backbone.parameters(),
        'lr': 1e-5,              # backbone 使用小学习率
        'weight_decay': 0.01
    },
    {
        'params': model.classifier.parameters(),
        'lr': 1e-3,              # 分类头使用大学习率
        'weight_decay': 0.001
    }
])

# 方法二:为每一层设置递减的学习率
def get_layer_wise_lr_params(model, base_lr=1e-3, decay_factor=0.9):
    """为每一层设置递减的学习率"""
    params = []
    layers = list(model.named_parameters())
    num_layers = len(layers)

    for i, (name, param) in enumerate(layers):
        lr = base_lr * (decay_factor ** (num_layers - i - 1))
        params.append({
            'params': param,
            'lr': lr,
            'name': name
        })

    return params

optimizer = optim.AdamW(
    get_layer_wise_lr_params(model, base_lr=1e-3, decay_factor=0.95),
    weight_decay=0.01
)

差异化学习率实战:BERT 微调 ​

python
from transformers import BertModel

model = BertModel.from_pretrained('bert-base-uncased')

# BERT 不同层使用不同学习率
optimizer_grouped_parameters = [
    {
        'params': model.embeddings.parameters(),
        'lr': 1e-5
    },
    # 中间层使用逐渐增大的学习率
    *[{
        'params': model.encoder.layer[i].parameters(),
        'lr': 1e-5 + (1e-4 - 1e-5) * i / 11
    } for i in range(12)],
    {
        'params': model.pooler.parameters(),
        'lr': 1e-4
    }
]

optimizer = optim.AdamW(optimizer_grouped_parameters, weight_decay=0.01)

查看和修改参数组 ​

python
# 查看所有参数组
for i, param_group in enumerate(optimizer.param_groups):
    print(f"参数组 {i}: lr={param_group['lr']}, "
          f"weight_decay={param_group.get('weight_decay', 0)}")

# 动态修改某个参数组的学习率
optimizer.param_groups[0]['lr'] = 5e-6
optimizer.param_groups[1]['lr'] = 5e-4

# 冻结 backbone 的参数(不计算梯度)
for param in model.backbone.parameters():
    param.requires_grad = False

# 只优化需要梯度的参数
optimizer = optim.AdamW(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=1e-3
)

综合训练示例 ​

将损失函数、优化器、学习率调度器、梯度裁剪结合使用:

python
import torch
import torch.nn as nn
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
import torch.nn.utils as nn_utils

# 1. 定义模型
model = MyModel(num_classes=10)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)

# 2. 定义损失函数
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

# 3. 定义优化器(AdamW,差异化学习率)
optimizer = optim.AdamW([
    {'params': model.backbone.parameters(), 'lr': 1e-5, 'weight_decay': 0.01},
    {'params': model.head.parameters(), 'lr': 1e-3, 'weight_decay': 0.01}
])

# 4. 定义学习率调度器
# warmup + cosine annealing
warmup_epochs = 5
total_epochs = 100

scheduler_warmup = lr_scheduler.LinearLR(
    optimizer, start_factor=0.1, total_iters=warmup_epochs
)
scheduler_cosine = lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=total_epochs - warmup_epochs, eta_min=1e-7
)
scheduler = lr_scheduler.SequentialLR(
    optimizer,
    schedulers=[scheduler_warmup, scheduler_cosine],
    milestones=[warmup_epochs]
)

# 5. 训练循环
max_grad_norm = 1.0

for epoch in range(total_epochs):
    model.train()
    total_loss = 0.0

    for batch_idx, (inputs, targets) in enumerate(train_loader):
        inputs = inputs.to(device, non_blocking=True)
        targets = targets.to(device, non_blocking=True)

        # 前向传播
        outputs = model(inputs)
        loss = criterion(outputs, targets)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()

        # 梯度裁剪
        grad_norm = nn_utils.clip_grad_norm_(
            model.parameters(), max_norm=max_grad_norm
        )

        # 参数更新
        optimizer.step()

        total_loss += loss.item()

        # 监控梯度
        if batch_idx % 100 == 0:
            print(f'  Batch {batch_idx}, Loss: {loss.item():.4f}, '
                  f'Grad Norm: {grad_norm:.4f}')

    # 更新学习率(epoch-based)
    scheduler.step()
    current_lr = scheduler.get_last_lr()
    avg_loss = total_loss / len(train_loader)

    print(f'Epoch [{epoch+1}/{total_epochs}], '
          f'Loss: {avg_loss:.4f}, '
          f'LR: {current_lr}')

    # 验证
    val_loss, val_acc = validate(model, val_loader, criterion, device)
    print(f'  Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%')

参考资料 ​