优化器与损失函数#

优化器和损失函数是训练神经网络的关键组件。本章将详细介绍 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

回归任务#

  • 一般回归MSELossL1Loss
  • 异常值敏感SmoothL1Loss
  • 需要平滑MSELoss
  • 需要鲁棒性L1Loss

特殊任务#

  • 分割任务DiceLoss + BCEWithLogitsLoss
  • 度量学习TripletMarginLossContrastiveLoss
  • 知识蒸馏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}')

练习#

  1. 尝试不同的优化器,比较训练效果
  2. 实现一个自定义损失函数
  3. 使用学习率调度器优化训练过程
  4. 实现梯度累积和梯度裁剪