损失函数与优化器
损失函数(Loss Function)和优化器(Optimizer)是深度学习模型训练的两大核心组件。损失函数衡量模型预测值与真实值之间的差异,优化器则根据损失函数计算的梯度来更新模型参数。PyTorch 在 torch.nn 和 torch.optim 中提供了丰富的损失函数和优化器实现。
损失函数详解
回归损失
回归任务的目标是预测连续值,常用的损失函数如下:
MSELoss(均方误差损失)
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.5000L1Loss(平均绝对误差损失)
criterion = nn.L1Loss(reduction='mean')
loss = criterion(predictions, targets)
print(f"L1 Loss: {loss.item():.4f}") # 0.5000SmoothL1Loss / HuberLoss
结合 MSE 和 L1 Loss 的优点,对异常值更鲁棒:
# 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:
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(负对数似然损失)
# 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(二元交叉熵损失)
用于二分类或多标签分类:
# 多标签分类:每个标签独立地做二分类
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,数值更稳定:
# 推荐使用这个,而不是先 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 用于解决类别极度不平衡的问题(如目标检测中的前景/背景),通过降低易分类样本的权重来聚焦于难分类样本:
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,最常用 |
NLLLoss | log 概率 + 整数标签 | 多类分类 | 需手动 log_softmax |
BCELoss | sigmoid 后概率 + float 标签 | 二分类/多标签 | 输入需先 sigmoid |
BCEWithLogitsLoss | 原始 logits + float 标签 | 二分类/多标签 | 推荐替代 BCELoss |
FocalLoss | 原始 logits + float 标签 | 不平衡分类 | 需自行实现 |
其他损失函数
KLDivLoss(KL 散度损失)
衡量两个概率分布之间的差异,常用于知识蒸馏:
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_lossCosineEmbeddingLoss
用于学习嵌入空间中的相似度,常用与度量学习(Metric Learning):
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(三元组损失)
常用于人脸识别、图像检索等度量学习任务:
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
用于半监督学习或学习相似性:
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 + CrossEntropyLoss | soft loss + hard loss |
| 度量学习/人脸识别 | TripletMarginLoss / CosineEmbeddingLoss | 学习嵌入空间距离 |
| 语义分割 | CrossEntropyLoss(ignore_index=255) | 忽略无标注像素 |
| 生成对抗网络 | BCEWithLogitsLoss 或自定义 | 判别器损失 |
| 目标检测 | SmoothL1Loss(bbox) + CrossEntropyLoss(cls) + BCEWithLogitsLoss(obj) | 组合使用 |
label_smoothing 参数
标签平滑(Label Smoothing)是一种正则化技术,防止模型对训练标签过度自信:
# 标签平滑: 将 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 加速:
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 结合了动量和自适应学习率的优点,是目前最广泛使用的优化器之一:
# 基础 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 将权重衰减与梯度更新解耦,是目前推荐使用的优化器:
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 的区别
# 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
自适应学习率优化器,适合处理非平稳目标:
optimizer = optim.RMSprop(
model.parameters(),
lr=1e-3,
alpha=0.99, # 平滑常数
eps=1e-8,
weight_decay=0, # L2 正则化
momentum=0, # 动量
centered=False # 是否使用中心化的 RMSprop
)Adagrad
自适应调整每个参数的学习率,适合稀疏数据:
optimizer = optim.Adagrad(
model.parameters(),
lr=1e-2,
lr_decay=0, # 学习率衰减
weight_decay=0,
eps=1e-10
)
# 注意:Adagrad 的学习率会单调递减,可能导致训练后期学习率过小Adadelta
Adagrad 的改进版,使用滑动窗口代替累积梯度平方:
optimizer = optim.Adadelta(
model.parameters(),
lr=1.0, # 默认 lr=1.0
rho=0.9, # 滑动平均系数
eps=1e-6,
weight_decay=0
)优化器参数对比
| 优化器 | 关键参数 | 自适应学习率 | 适用场景 | 训练速度 |
|---|---|---|---|---|
| SGD | lr, momentum, nesterov | 否 | CV 任务、精调 | 慢 |
| SGD + Momentum | lr, momentum=0.9 | 否 | ResNet 等经典模型 | 中 |
| Adam | lr, betas, eps | 是 | 通用,NLP、Transformer | 快 |
| AdamW | lr, betas, weight_decay | 是 | Transformer、大规模预训练 | 快 |
| RMSprop | lr, alpha, momentum | 是 | RNN、非平稳目标 | 快 |
| Adagrad | lr, lr_decay | 是 | 稀疏数据、NLP | 中 |
| Adadelta | lr, rho | 是 | 不需要手动设 lr 的场景 | 中 |
学习率调度器
学习率调度器(Learning Rate Scheduler)根据预设策略在训练过程中动态调整学习率。PyTorch 在 torch.optim.lr_scheduler 中提供了多种调度策略。
StepLR 与 MultiStepLR
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# 训练循环中使用 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
# 每个 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
学习率按余弦函数从初始值衰减到最小值,常用于训练后期的微调:
# T_max: 半周期的 epoch 数
# eta_min: 最小学习率
scheduler = lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=200, # 半个余弦周期的 epoch 数
eta_min=1e-6 # 最小学习率
)CosineAnnealingWarmRestarts
带有"热重启"的余弦退火,每个周期结束后学习率重新回到初始值:
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
当监控指标停止改善时自动降低学习率,是最实用的调度器之一:
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 策略,学习率先增后减,可显著加速训练:
# 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
# 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 切换
)学习率调度器使用总结
# 类型一: 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 | 每 epoch | step_size, gamma | 简单阶梯衰减 |
MultiStepLR | 每 epoch | milestones, gamma | 在指定 epoch 衰减 |
ExponentialLR | 每 epoch | gamma | 指数衰减 |
CosineAnnealingLR | 每 epoch | T_max, eta_min | 余弦退火 |
CosineAnnealingWarmRestarts | 每 epoch | T_0, T_mult | 带重启的余弦退火 |
ReduceLROnPlateau | 验证后 | patience, factor | 自适应衰减 |
OneCycleLR | 每 batch | max_lr, total_steps | 超级收敛策略 |
LambdaLR | 每 epoch | lr_lambda | 自定义策略 |
SequentialLR | 取决于子调度器 | schedulers, milestones | 串联多个调度器 |
CyclicLR | 每 batch | base_lr, max_lr | 循环学习率 |
LinearLR | 每 epoch | start_factor, total_iters | 线性缩放(常用于 warmup) |
梯度裁剪
梯度裁剪(Gradient Clipping)用于防止梯度爆炸,是训练 RNN、Transformer 等模型的常见技巧。
clip_grad_norm_(按范数裁剪)
将所有参数的梯度视为一个向量,若其范数超过阈值则等比缩放:
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_(按值裁剪)
将每个梯度值限制在指定范围内:
# 将所有梯度值限制在 [-0.5, 0.5] 范围内
nn_utils.clip_grad_value_(model.parameters(), clip_value=0.5)梯度裁剪对比
| 方法 | 说明 | 适用场景 |
|---|---|---|
clip_grad_norm_ | 等比缩放所有梯度,保持梯度方向 | RNN、Transformer、一般模型 |
clip_grad_value_ | 硬截断每个梯度值 | 简单但可能改变梯度方向 |
# 常见的 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 正则化 = 权重衰减
# 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 正则化 ≠ 权重衰减
# 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 允许为模型的不同层设置不同的学习率和优化参数,这在迁移学习和微调中非常有用。
创建参数组
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 微调
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)查看和修改参数组
# 查看所有参数组
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
)综合训练示例
将损失函数、优化器、学习率调度器、梯度裁剪结合使用:
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}%')