损失函数
是什么
- 定义模型预测与真实标签之间的差异
- 常见类型:交叉熵损失、均方误差损失、L1损失等
- 直接影响模型收敛方向和速度
交叉熵 transclusion
\( L = -\sum y_i \log(\hat{y}_i) \)
交叉熵(Cross-Entropy)是信息论中的概念,用于衡量两个概率分布之间的差异。在机器学习中,常用于分类任务的损失函数。
定义: 对于真实分布 \( P \) 和预测分布 \( Q \),交叉熵定义为:
特点:
- 当预测分布 \( Q \) 与真实分布 \( P \) 完全一致时,交叉熵最小。
- 在分类任务中,常用分类交叉熵(Categorical Cross-Entropy)作为损失函数,优化模型输出与真实标签的差距。
应用:
- 神经网络分类任务(如 softmax 输出 + 交叉熵损失)。
- 与 KL 散度密切相关:交叉熵 = 熵 + KL 散度。
MSE
均方误差
数学表示
\( \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \)
为什么线性回归使用 MSE 作为评估指标?
线性回归使用 MSE(均方误差)作为评估指标的主要原因:
数学性质优良
- MSE 是光滑可导的凸函数,便于梯度下降等优化算法求解
- 对异常值更敏感,能更好反映预测误差的严重程度
统计意义明确
- 最小化 MSE 等价于寻找条件期望 E[Y|X],具有最优无偏估计的性质
- 与高斯噪声假设一致:当误差服从正态分布时,MSE 给出最大似然估计
计算简便
- 平方运算使正负误差不会相互抵消
- 公式简单,易于理解和实现
实际考虑
- 大误差惩罚更重,符合实际应用中对小误差容忍、大误差重视的需求
虽然 MSE 对异常值敏感,但在数据质量较好时,其理论性质和优化特性使其成为线性回归的理想评估指标。
L1 损失
平均绝对误差
L1 损失是另一种常见的回归损失函数,计算预测值与真实值之间的绝对差平均值。
/定义/:
/特点/:
- 对异常值不敏感,因为绝对误差不会放大较大偏差。
- 梯度在零点不可导,优化时可能需要特殊处理(如次梯度方法)。
- 假设误差服从拉普拉斯分布时,MAE 是最大似然估计的自然选择。
/应用/:
- 对异常值鲁棒的回归任务。
- 计算机视觉中的图像重建。
DISTS
DISTS 是一种用于图像质量评估的感知损失函数,结合了结构相似性和纹理相似性。
/定义/: DISTS 通过深度特征提取器(如 VGG)计算多尺度特征之间的相似性,结合了结构信息和纹理信息。其公式为:
/特点/:
- 对图像的结构和纹理变化更敏感,与人眼感知更一致。
- 在图像超分辨率、风格迁移等任务中表现优异。
/应用/:
- 图像质量评估。
- 生成对抗网络(GAN)中的感知损失。
import torch
import torch.nn.functional as F
def cross_entropy_loss(logits, targets):
"""计算交叉熵损失"""
return F.cross_entropy(logits, targets)
def mse_loss(predictions, targets):
"""计算均方误差损失"""
return F.mse_loss(predictions, targets)
def mae_loss(predictions, targets):
"""计算L1损失"""
return F.l1_loss(predictions, targets)
# 示例使用
logits = torch.randn(4, 10) # 4个样本,10个类别
targets = torch.tensor([1, 3, 5, 7])
print(f"Cross Entropy Loss: {cross_entropy_loss(logits, targets):.4f}")
predictions = torch.randn(4, 1)
targets = torch.randn(4, 1)
print(f"MSE Loss: {mse_loss(predictions, targets):.4f}")
print(f"MAE Loss: {mae_loss(predictions, targets):.4f}")
DICE Loss
Dice Loss 是一种常用于图像分割任务的损失函数, 基于 Dice 系数(Sørensen–Dice 系数)构建,用于衡量两个样本集合的相似度。
/定义/: Dice 系数定义为:
/特点/:
- 对类别不平衡问题鲁棒,特别适用于前景像素远少于背景的分割任务。
- 直接优化分割区域的重叠度,与评估指标(如 IoU)高度相关。
- 梯度计算稳定,但需要预测值为概率形式(通常通过 sigmoid 或 softmax 获得)。
/应用/:
- 医学图像分割(如肿瘤、器官分割)。
- 任何需要处理类别不平衡的二值或多类分割任务。
import torch
import torch.nn.functional as F
def dice_loss(predictions, targets, smooth=1e-6):
"""计算 Dice Loss
Args:
predictions: 预测概率,形状 [N, C, H, W]
targets: 真实标签,形状 [N, C, H, W]
smooth: 平滑常数
"""
# 展平预测和标签
predictions = predictions.contiguous().view(predictions.shape[0], -1)
targets = targets.contiguous().view(targets.shape[0], -1)
# 计算交集和并集
intersection = (predictions * targets).sum(dim=1)
union = predictions.sum(dim=1) + targets.sum(dim=1)
# 计算 Dice 系数和损失
dice = (2. * intersection + smooth) / (union + smooth)
return 1 - dice.mean()
# 示例使用
predictions = torch.sigmoid(torch.randn(2, 1, 128, 128)) # 二值分割预测
targets = torch.randint(0, 2, (2, 1, 128, 128)).float() # 二值标签
print(f"Dice Loss: {dice_loss(predictions, targets):.4f}")
SSIM
SSIM(结构相似性指数)是一种基于人眼感知的图像质量评估指标,比 PSNR 更符合主观视觉感受。
测量恢复图像和干净图像之间的相似度
/公式/:
- \(\mu_x, \mu_y\):图像块均值(亮度)
- \(\sigma_x^2, \sigma_y^2\):方差(对比度)
- \(\sigma_{xy}\):协方差(结构相似性)
- \(C_1, C_2\):稳定常数
/特点/:
- 取值范围:[-1, 1],值越接近 1 表示质量越好。
- 综合评估亮度、对比度和结构信息。
- 通常按局部图像块计算后取平均值(MSSIM)。
结构相似性指数(Structural Similarity Index Measure, SSIM)是一种用于衡量两幅图 像感知质量的指标,它考虑了亮度、对比度和结构信息的相似性。
更通用的:
SSIM 的计算公式为:
- \( l(x, y) = \frac{2\mu_x\mu_y + C_1}{\mu_x^2 + \mu_y^2 + C_1} \) 为亮度比较
- \( c(x, y) = \frac{2\sigma_x\sigma_y + C_2}{\sigma_x^2 + \sigma_y^2 + C_2} \) 为对比度比较
- \( s(x, y) = \frac{\sigma_{xy} + C_3}{\sigma_x\sigma_y + C_3} \) 为结构比较
通常取 \( \alpha = \beta = \gamma = 1 \),且 \( C_3 = C_2/2 \),此时简化为:
LPIPS
Learned Perceptual Image Patch Similar ity (LPIPS) 是一种基于深度学习的图像相似 性度量方法,也称为"感知损失"。它通过预训练的深度神经网络来度量图像之间的感知相似 性。
定义
LPIPS 使用预训练的卷积神经网络(如 VGG、AlexNet 等)提取图像特征,然后在特征空间计算图像块之间的相似性。其公式为:
- \( x \) 和 \( x_0 \) 是待比较的图像
- \( l \) 表示网络层
- \( \hat{y}^l \) 和 \( \hat{y}_0^l \) 是第 \( l \) 层的激活值(经过通道归一化)
- \( w_l \) 是每层的可学习权重
- \( H_l, W_l \) 是特征图的尺寸
/特点/:
- 与人眼主观评价高度一致,比传统指标(如 PSNR、SSIM)更能反映图像质量
- 通过大规模人类标注数据学习得到,具有更好的泛化能力
- 对图像的纹理、结构等高级特征变化更敏感
/应用/:
- 图像质量评估
- 图像生成任务的质量监控
- 风格迁移、超分辨率等任务的损失函数
import torch
import torch.nn as nn
import lpips
def lpips_loss(image1, image2, net='vgg'):
"""计算 LPIPS 损失
Args:
image1: 图像1,形状 [N, C, H, W],值范围 [-1, 1] 或 [0,
1]
image2: 图像2,形状与 image1 相同
net: 使用的网络 backbone ('alex', 'vgg', 'squeeze')
"""
loss_fn = lpips.LPIPS(net=net)
return loss_fn(image1, image2)
# 示例使用
# 注意:LPIPS 要求输入图像在 [-1, 1] 或 [0, 1] 范围内
image1 = torch.rand(2, 3, 224, 224) * 2 - 1 # 范围 [-1, 1]
image2 = torch.rand(2, 3, 224, 224) * 2 - 1
print(f"LPIPS Loss: {lp
ips_loss(image1, image2):.4f}")
与其他损失函数的比较
与 MSE 的比较
- MSE 在像素级别比较,对逐像素变化敏感但可能不符合人类感知
- LPIPS 在特征空间比较,更能捕捉语义级别的差异
与 SSIM 的比较
- SSIM 基于手工设计的特征,考虑亮度、对比度、结构
- LPIPS 通过数据驱动学习,适应性更强,与人类评价相关性更高
适用场景
- 当需要与人眼感知一致的评估时,优先选择 LPIPS
- 对计算效率要求高时,可考虑 SSIM 或 MSE
- 在生成模型中,LPIPS 通常能产生更自然、更高质量的结果
torch 中的损失函数
使用 pytorch 模块进行模型训练时, loss 是怎么得到、怎么使用的? forward 和 backward 怎么使用?如何与 optimizer 联动?
在 PyTorch 中,损失函数、前向传播、反向传播和优化器共同构成了模型训练的核心循环。以下是它们如何协同工作的详细解释。
核心概念与流程
- 前向传播 (Forward Pass): 模型根据输入数据计算预测输出。
- 计算损失 (Compute Loss): 使用损失函数比较预测输出与真实标签,得到一个标量损失值。
- 反向传播 (Backward Pass): 自动计算损失相对于模型所有可训练参数的梯度。
- 参数更新 (Parameter Update): 优化器利用计算出的梯度来更新模型参数,以最小化损失。
详细步骤与代码示例
以下是一个完整的训练步骤示例,展示了各组件如何联动。
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 定义简单的模型、损失函数和优化器
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.linear = nn.Linear(10, 1) # 一个简单的线性层
def forward(self, x):
return self.linear(x)
model = SimpleModel()
criterion = nn.MSELoss() # 均方误差损失函数
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降优化器
# 2. 模拟一批训练数据
batch_size = 4
inputs = torch.randn(batch_size, 10) # 输入: (batch_size, input_features)
labels = torch.randn(batch_size, 1) # 真实标签
# 3. 训练循环中的一个迭代步骤
# 步骤 A: 前向传播
outputs = model(inputs) # 调用 model.forward(inputs)
print(f"模型输出形状: {outputs.shape}")
print(f"真实标签形状: {labels.shape}")
# 步骤 B: 计算损失
loss = criterion(outputs, labels)
print(f"损失值 (标量): {loss.item()}")
# 步骤 C: 反向传播
# 在计算梯度前,必须将优化器中存储的上一次迭代的梯度清零。
# 因为 PyTorch 默认会累积梯度 (.grad 属性会累加)。
optimizer.zero_grad()
# 调用 loss.backward() 计算损失相对于模型参数的梯度。
# 梯度被存储在模型每个参数的 .grad 属性中。
loss.backward()
print(f"线性层权重 w 的梯度形状: {model.linear.weight.grad.shape}")
print(f"线性层偏置 b 的梯度形状: {model.linear.bias.grad.shape}")
# 步骤 D: 参数更新
# 优化器根据参数的 .grad 属性和其自身的更新规则(如 SGD 的 w = w - lr * grad)来更新参数。
optimizer.step()
print("参数已由优化器更新。")
# 验证参数已更新(例如,检查权重是否变化)
old_weight = model.linear.weight.data.clone()
# 再进行一次前向-反向-更新循环
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
new_weight = model.linear.weight.data
print(f"权重在更新后是否变化: {not torch.allclose(old_weight, new_weight)}")
关键组件详解
损失函数 (Loss Function)
- 作用: 量化模型预测与真实目标之间的差异。它是一个标量值,值越小表示模型预测越好。
- 常见类型:
- 回归任务:
nn.MSELoss(均方误差),nn.L1Loss(平均绝对误差)。 - 分类任务:
nn.CrossEntropyLoss(交叉熵,常用于多分类),nn.BCELoss(二元交叉熵)。
- 回归任务:
- 得到与使用: 通过调用
criterion(predictions, targets)得到损失张量。
前向传播 (Forward Pass)
- 过程: 数据从输入层流经模型的各层,直到输出层,得到预测值。在 PyTorch 中,这通过调用模型实例(如
model(x)=)或直接调用 =forward方法完成。 - 关键:
forward方法定义了模型的计算图。PyTorch 会记录所有在forward中涉及张量的操作,为后续的自动微分做准备。
反向传播 (Backward Pass) 与 loss.backward()
- 原理: 基于链式法则,从损失值开始,反向计算损失相对于每个参数的梯度。
- 自动微分 (Autograd): PyTorch 的核心特性。当在张量上进行操作时,它会构建一个计算图。调用
loss.backward()会遍历这个图,计算并填充图中所有requires_grad=True的张量的.grad属性。 zero_grad()的重要性: 如果不将梯度清零,下一次loss.backward()计算的梯度会与已有的梯度累加,这通常不是我们想要的行为(除非在特定场景如梯度累加以模拟更大批次)。
优化器 (Optimizer)
- 作用: 根据梯度更新模型参数,以最小化损失函数。
- 初始化: 需要传入模型的可训练参数(=model.parameters()=)。
step()方法: 执行一次参数更新。其内部根据优化算法(如 SGD, Adam)使用存储的梯度来更新每个参数。- 与反向传播的联动:
optimizer.zero_grad(): 清空旧梯度。loss.backward(): 计算新梯度并存入 =.grad=。optimizer.step(): 利用.grad更新参数。
完整的训练循环模板
以下是一个更通用的训练循环模板,通常嵌套在 epoch 循环中。
def train_one_epoch(model, dataloader, criterion, optimizer, device):
model.train() # 将模型设置为训练模式(影响 Dropout, BatchNorm 等层)
running_loss = 0.0
for batch_idx, (inputs, labels) in enumerate(dataloader):
# 将数据移至指定设备(如 GPU)
inputs, labels = inputs.to(device), labels.to(device)
# 1. 前向传播
outputs = model(inputs)
# 2. 计算损失
loss = criterion(outputs, labels)
# 3. 反向传播
optimizer.zero_grad()
loss.backward()
# (可选)梯度裁剪,防止梯度爆炸
# torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 4. 参数更新
optimizer.step()
# 记录损失
running_loss += loss.item()
average_loss = running_loss / len(dataloader)
return average_loss
实例
def train_one_epoch(model, model_without_ddp, data_loader, optimizer, device, epoch, log_writer=None, args=None):
model.train(True)
metric_logger = misc.MetricLogger(delimiter=" ")
metric_logger.add_meter('lr', misc.SmoothedValue(window_size=1, fmt='{value:.6f}'))
header = 'Epoch: [{}]'.format(epoch)
print_freq = 20
optimizer.zero_grad()
if log_writer is not None:
print('log_dir: {}'.format(log_writer.log_dir))
for data_iter_step, (x, labels) in enumerate(metric_logger.log_every(data_loader, print_freq, header)):
# per iteration (instead of per epoch) lr scheduler
lr_sched.adjust_learning_rate(optimizer, data_iter_step / len(data_loader) + epoch, args)
# normalize image to [-1, 1]
x = x.to(device, non_blocking=True).to(torch.float32).div_(255)
x = x * 2.0 - 1.0
labels = labels.to(device, non_blocking=True)
with torch.amp.autocast('cuda', dtype=torch.bfloat16):
loss = model(x, labels)
loss_value = loss.item()
if not math.isfinite(loss_value):
print("Loss is {}, stopping training".format(loss_value))
sys.exit(1)
# Zero the gradients before backward to prevent accumulation from previous iterations.
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Synchronize the gradients to ensure they are correctly computed.
torch.cuda.synchronize()
# Update the EMA model parameters.
model_without_ddp.update_ema()
metric_logger.update(loss=loss_value)
lr = optimizer.param_groups[0]["lr"]
metric_logger.update(lr=lr)
# Reduce the loss value across all processes to get the average loss.
loss_value_reduce = misc.all_reduce_mean(loss_value)
if log_writer is not None:
# Use epoch_1000x as the x-axis in TensorBoard to calibrate curves.
# Log the loss and learning rate to TensorBoard.
epoch_1000x = int((data_iter_step / len(data_loader) + epoch) * 1000)
if data_iter_step % args.log_freq == 0:
log_writer.add_scalar('train_loss', loss_value_reduce, epoch_1000x)
log_writer.add_scalar('lr', lr, epoch_1000x)
数学原理简述
对于一个模型参数 \(\theta\),损失函数 \(L\),学习率 \(\eta\),一次梯度下降更新为:
其中 \(\nabla_{\theta} L\) 就是通过 loss.backward() 计算得到的梯度,optimizer.step() 负责执行上述更新操作(对于 SGD 而言,更复杂的优化器如 Adam 会有自适应学习率和动量项)。
总结
- 前向传播 (
model(inputs)): 得到预测,构建计算图。 - 计算损失 (
criterion(outputs, labels)): 得到衡量预测好坏的标量。 - 梯度清零 (
optimizer.zero_grad()): 防止梯度累积。 - 反向传播 (
loss.backward()): 自动计算梯度并存储。 - 参数更新 (
optimizer.step()): 利用梯度下降(或其变体)更新模型参数。个步骤是 PyTorch 模型训练迭代的核心,循环执行此过程,模型参数逐渐被优化,损失值逐渐降低。