优化器与损失函数#
优化器和损失函数是训练神经网络的关键组件。本章将详细介绍 PyTorch 中常用的优化器和损失函数。
核心原理#
优化目标#
设损失函数为 $L(\theta)$ ,优化目标为:
$$ \theta^* = \arg\min_\theta L(\theta) $$梯度下降通过迭代更新参数:$\theta_{t+1} = \theta_t - \eta \cdot g_t$ ,其中 $g_t = \nabla_\theta L(\theta_t)$ 为梯度,$\eta$ 为学习率。
关键概念#
- 学习率:控制每步更新幅度,过大易震荡,过小收敛慢
- 动量:累积历史梯度,加速收敛、平滑更新
- 自适应学习率:根据梯度历史为每个参数单独调整步长
- 权重衰减:L2 正则化,防止过拟合
优化器(Optimizers)#
优化器用于更新模型参数,最小化损失函数。
SGD(随机梯度下降)#
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, # 动量系数
weight_decay=1e-4, # L2 正则化
nesterov=True # Nesterov 动量
)更新公式(带动量):
$$ v_t = \mu \cdot v_{t-1} + g_t, \quad \theta_{t+1} = \theta_t - \eta \cdot v_t $$特点:
- 简单稳定
- 需要仔细调整学习率
- 适合大规模数据集
Adam(自适应矩估计)#
optimizer = optim.Adam(
model.parameters(),
lr=0.001, # 学习率
betas=(0.9, 0.999), # 动量参数
eps=1e-8, # 数值稳定性
weight_decay=1e-4, # L2 正则化
amsgrad=False # 是否使用 AMSGrad 变体
)更新公式:
$$ m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t, \quad v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 $$$$ \theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} $$其中 $\hat{m}_t = m_t/(1-\beta_1^t)$ ,$\hat{v}_t = v_t/(1-\beta_2^t)$ 为偏差修正。
特点:
- 自适应学习率
- 收敛速度快
- 内存占用较大
- 最常用的优化器之一
AdamW(Adam with Weight Decay)#
optimizer = optim.AdamW(
model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
eps=1e-8,
weight_decay=1e-4
)特点:
- Adam 的改进版本
- 权重衰减更合理
- 通常比 Adam 表现更好
RMSprop#
optimizer = optim.RMSprop(
model.parameters(),
lr=0.01,
alpha=0.99, # 平滑常数
eps=1e-8,
weight_decay=1e-4,
momentum=0
)特点:
- 适合非平稳目标
- 常用于 RNN
Adagrad#
optimizer = optim.Adagrad(
model.parameters(),
lr=0.01,
lr_decay=0, # 学习率衰减
weight_decay=1e-4,
eps=1e-10
)特点:
- 自适应学习率
- 学习率会逐渐减小
- 适合稀疏梯度
其他优化器#
# Adadelta
optimizer = optim.Adadelta(model.parameters(), lr=1.0, rho=0.9)
# Adamax
optimizer = optim.Adamax(model.parameters(), lr=0.002)
# SparseAdam(用于稀疏梯度)
optimizer = optim.SparseAdam(model.parameters(), lr=0.001)学习率调度器(Learning Rate Schedulers)#
StepLR#
# 每 step_size 个 epoch 将学习率乘以 gamma
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
train(...)
scheduler.step() # 更新学习率MultiStepLR#
# 在指定的 milestones 处降低学习率
scheduler = optim.lr_scheduler.MultiStepLR(
optimizer,
milestones=[30, 60, 90],
gamma=0.1
)ExponentialLR#
# 每个 epoch 将学习率乘以 gamma
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)CosineAnnealingLR#
# 余弦退火
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 最大迭代次数
eta_min=0 # 最小学习率
)ReduceLROnPlateau#
# 当指标停止改善时降低学习率
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 'min' 或 'max'
factor=0.5, # 学习率衰减因子
patience=5, # 等待的 epoch 数
verbose=True
)
for epoch in range(100):
train(...)
val_loss = validate(...)
scheduler.step(val_loss) # 传入监控的指标OneCycleLR#
# 一个周期的学习率调度
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.01,
epochs=100,
steps_per_epoch=len(train_loader)
)
for epoch in range(100):
for batch in train_loader:
train_step(...)
scheduler.step() # 每个 batch 更新一次LambdaLR#
# 自定义学习率函数
lambda1 = lambda epoch: epoch // 30
lambda2 = lambda epoch: 0.95 ** epoch
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=[lambda1, lambda2])损失函数(Loss Functions)#
损失函数原理#
损失函数衡量预测与真实标签的差异,训练目标是最小化损失。常见形式:
- 分类:交叉熵 $L = -\sum_i y_i \log(p_i)$
- 回归:MSE $L = \frac{1}{N}\sum(x_i-y_i)^2$ 或 L1 $L = \frac{1}{N}\sum|x_i-y_i|$
分类任务#
CrossEntropyLoss#
import torch.nn as nn
# 用于多分类(内部含 LogSoftmax + NLLLoss)
# 数学形式:L = -log(exp(x[y])/sum(exp(x)))
criterion = nn.CrossEntropyLoss()
# 输入:未归一化的 logits (N, C)
# 目标:类别索引 (N,)
outputs = model(inputs) # (32, 10)
targets = torch.randint(0, 10, (32,))
loss = criterion(outputs, targets)NLLLoss(负对数似然)#
# 需要先应用 LogSoftmax
criterion = nn.NLLLoss()
log_probs = nn.LogSoftmax(dim=1)(outputs)
loss = criterion(log_probs, targets)BCELoss(二元交叉熵)#
# 用于二分类
criterion = nn.BCELoss()
outputs = torch.sigmoid(model(inputs)) # 需要先应用 sigmoid
targets = torch.randint(0, 2, (32, 1)).float()
loss = criterion(outputs, targets)BCEWithLogitsLoss#
# BCELoss + Sigmoid(数值稳定)
criterion = nn.BCEWithLogitsLoss()
outputs = model(inputs) # 不需要 sigmoid
targets = torch.randint(0, 2, (32, 1)).float()
loss = criterion(outputs, targets)回归任务#
MSELoss(均方误差)#
criterion = nn.MSELoss()
outputs = model(inputs) # (32, 1)
targets = torch.randn(32, 1)
loss = criterion(outputs, targets)L1Loss(平均绝对误差)#
criterion = nn.L1Loss()
loss = criterion(outputs, targets)SmoothL1Loss(Huber Loss)#
# 结合 L1 和 L2 的优点
criterion = nn.SmoothL1Loss()
loss = criterion(outputs, targets)其他损失函数#
KLDivLoss(KL 散度)#
# 用于分布之间的差异
criterion = nn.KLDivLoss(reduction='batchmean')
log_probs = nn.LogSoftmax(dim=1)(outputs)
target_probs = nn.Softmax(dim=1)(target_outputs)
loss = criterion(log_probs, target_probs)MarginRankingLoss#
# 用于排序任务
criterion = nn.MarginRankingLoss(margin=1.0)
input1 = torch.randn(32, 1)
input2 = torch.randn(32, 1)
target = torch.randint(0, 2, (32,)).float() * 2 - 1 # -1 或 1
loss = criterion(input1, input2, target)TripletMarginLoss#
# 用于度量学习
criterion = nn.TripletMarginLoss(margin=1.0)
anchor = torch.randn(32, 128)
positive = torch.randn(32, 128)
negative = torch.randn(32, 128)
loss = criterion(anchor, positive, negative)CosineEmbeddingLoss#
# 用于学习相似度
criterion = nn.CosineEmbeddingLoss(margin=0.5)
input1 = torch.randn(32, 128)
input2 = torch.randn(32, 128)
target = torch.randint(0, 2, (32,)).float() * 2 - 1
loss = criterion(input1, input2, target)自定义损失函数#
实现自定义损失函数#
class FocalLoss(nn.Module):
"""Focal Loss for handling class imbalance"""
def __init__(self, alpha=1, gamma=2):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, inputs, targets):
ce_loss = nn.CrossEntropyLoss(reduction='none')(inputs, targets)
pt = torch.exp(-ce_loss)
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
return focal_loss.mean()
criterion = FocalLoss(alpha=1, gamma=2)Dice Loss(用于分割任务)#
class DiceLoss(nn.Module):
def __init__(self, smooth=1.0):
super().__init__()
self.smooth = smooth
def forward(self, inputs, targets):
inputs = torch.sigmoid(inputs)
inputs_flat = inputs.view(-1)
targets_flat = targets.view(-1)
intersection = (inputs_flat * targets_flat).sum()
dice = (2. * intersection + self.smooth) / (
inputs_flat.sum() + targets_flat.sum() + self.smooth
)
return 1 - dice
criterion = DiceLoss()优化器使用技巧#
1. 不同参数组使用不同学习率#
# 预训练层使用较小学习率,新层使用较大学习率
optimizer = optim.SGD([
{'params': model.backbone.parameters(), 'lr': 0.001},
{'params': model.classifier.parameters(), 'lr': 0.01}
], lr=0.001, momentum=0.9)2. 冻结部分参数#
# 冻结某些层
for param in model.backbone.parameters():
param.requires_grad = False
# 只优化需要梯度的参数
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()))3. 梯度累积#
accumulation_steps = 4
optimizer.zero_grad()
for i, (data, labels) in enumerate(dataloader):
outputs = model(data)
loss = criterion(outputs, labels) / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()4. 梯度裁剪#
# 防止梯度爆炸
max_norm = 1.0
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)损失函数选择指南#
分类任务#
- 多分类:
CrossEntropyLoss(最常用) - 二分类:
BCEWithLogitsLoss - 类别不平衡:
FocalLoss或加权CrossEntropyLoss - 多标签分类:
BCEWithLogitsLoss
回归任务#
- 一般回归:
MSELoss或L1Loss - 异常值敏感:
SmoothL1Loss - 需要平滑:
MSELoss - 需要鲁棒性:
L1Loss
特殊任务#
- 分割任务:
DiceLoss+BCEWithLogitsLoss - 度量学习:
TripletMarginLoss或ContrastiveLoss - 知识蒸馏:
KLDivLoss
实践建议#
1. 优化器选择#
- 一般情况:Adam 或 AdamW
- 需要精调:SGD with momentum
- 大规模数据:SGD
- RNN:RMSprop 或 Adam
2. 学习率设置#
- Adam/AdamW:通常 0.001 或 0.0001
- SGD:通常 0.01 或 0.1(需要仔细调整)
- 使用学习率调度器:从较大学习率开始,逐渐减小
3. 权重衰减#
- 防止过拟合:1e-4 到 1e-5
- AdamW:可以使用较大的权重衰减(1e-2)
4. 损失函数#
- 从简单开始:先使用标准损失函数
- 根据任务调整:根据具体问题选择合适的损失函数
- 组合损失:可以组合多个损失函数
完整示例#
import torch
import torch.nn as nn
import torch.optim as optim
# 定义模型
model = YourModel()
# 选择优化器
optimizer = optim.AdamW(
model.parameters(),
lr=0.001,
weight_decay=1e-4
)
# 选择学习率调度器
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100,
eta_min=1e-6
)
# 选择损失函数
criterion = nn.CrossEntropyLoss()
# 训练循环
for epoch in range(100):
model.train()
for data, targets in train_loader:
optimizer.zero_grad()
outputs = model(data)
loss = criterion(outputs, targets)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
print(f'Epoch {epoch+1}, LR: {scheduler.get_last_lr()[0]:.6f}')练习#
- 尝试不同的优化器,比较训练效果
- 实现一个自定义损失函数
- 使用学习率调度器优化训练过程
- 实现梯度累积和梯度裁剪