摘要: 损失函数(Loss Function)是深度学习模型训练的核心组成部分,它衡量模型预测值与真实值之间的差异,并为模型参数优化提供梯度方向。本文系统梳理了回归任务中的MSE、MAE、Huber Loss、Smooth L1 Loss,分类任务中的Binary Cross-Entropy、Categorical Cross-Entropy、Focal Loss,以及度量学习和生成对抗网络中的Triplet Loss、Contrastive Loss、对抗损失等特殊损失函数。文中提供完整的NumPy纯实现与PyTorch调用示例,探讨各损失函数的数学原理、优缺点及适用场景,并给出任务驱动的损失函数选择指南。
关键词: 损失函数;均方误差;交叉熵;Focal Loss;Triplet Loss;PyTorch
1. 损失函数概述
1.1 什么是损失函数
损失函数(Loss Function)是深度学习中衡量模型预测结果与真实标签之间差距的函数。在训练过程中,模型的前向传播产生预测值,损失函数计算该预测值与真实值的误差,梯度反传(Backpropagation)将该误差信号回传给网络参数,驱动优化器(Optimizer)更新权重,使损失值逐步降低。
从数学角度看,设模型参数为 \\theta,输入为 x,真实标签为 y,模型预测为 \\hat{y} = f(x; \\theta),损失函数为 \\mathcal{L}(y, \\hat{y})。训练的目标是找到一组参数 \\theta\^\* 使得期望风险最小化:
\\theta\^\* = \\arg\\min*\\theta \\mathbb{E}*{(x,y)\\sim p_{data}} \[\\mathcal{L}(y, f(x; \\theta))\]
1.2 损失函数的分类
根据任务类型的不同,损失函数大致可分为以下几类:
| 类别 | 典型损失函数 | 适用任务 |
|---|---|---|
| 回归损失 | MSE、MAE、Huber Loss、Smooth L1 | 房价预测、年龄估计等 |
| 二分类损失 | Binary Cross-Entropy、BCE with Logits | 猫狗分类、垃圾邮件检测等 |
| 多分类损失 | Categorical Cross-Entropy、Softmax Loss | ImageNet分类、手写数字识别等 |
| 类别不平衡损失 | Focal Loss、Class-Balanced Loss | 目标检测、医学影像分析等 |
| 度量学习损失 | Triplet Loss、Contrastive Loss、Center Loss | 人脸识别、行人再识别、图像检索等 |
| 生成模型损失 | 对抗损失、重构损失、感知损失 | GAN、VAE、自编码器等 |
2. 回归损失函数
回归任务的目标是预测一个连续的数值输出。以下详细介绍四种经典的回归损失函数。
2.1 MSE(均方误差)
数学公式:
\\text{MSE} = \\frac{1}{n} \\sum*{i=1}\^{n} (y_i - \\hat{y}*i)\^2
其中 y_i 为真实值,\\hat{y}_i 为预测值,n 为样本数量。MSE计算的是预测误差的平方的均值。
特点:
-
优点:处处可导(对预测值的梯度为 -2(y - \\hat{y})),有利于梯度下降优化;对误差较大的样本惩罚更重(平方项),模型会优先修正大误差样本。
-
缺点:对异常值(Outlier)非常敏感,因为平方项会放大异常值的影响;当误差 \|y - \\hat{y}\| \> 1 时,梯度会变得很大,可能导致训练不稳定。
适用场景: 数据中异常值较少、误差分布接近高斯分布的回归任务,如房价预测、股票价格回归等。
NumPy实现:
import numpy as np
def mse_numpy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
计算均方误差(MSE)
参数:
y_true: 真实值数组,形状为 (n,)
y_pred: 预测值数组,形状为 (n,)
返回:
MSE值(标量)
"""
# 逐元素计算误差平方
squared_errors = (y_true - y_pred) ** 2
# 求均值
return np.mean(squared_errors)
def mse_gradient_numpy(y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
"""
计算MSE对预测值的梯度
梯度公式: d(MSE)/d(y_pred) = -2 * (y_true - y_pred) / n
参数:
y_true: 真实值数组
y_pred: 预测值数组
返回:
梯度数组,与y_pred形状相同
"""
n = len(y_true)
return -2 * (y_true - y_pred) / n
# 示例
if __name__ == "__main__":
y_true = np.array([3.0, -0.5, 2.0, 7.0])
y_pred = np.array([2.5, 0.0, 2.1, 8.0])
loss = mse_numpy(y_true, y_pred)
grad = mse_gradient_numpy(y_true, y_pred)
print(f"MSE = {loss:.4f}") # 输出: MSE = 0.3875
print(f"梯度 = {grad}") # 输出: 梯度 = [-0.25 0.25 -0.05 -0.5 ]
PyTorch实现:
import torch
import torch.nn as nn
# ------------------- MSE Loss -------------------
# 方法1:使用nn.MSELoss(默认返回所有样本的均值)
criterion_mse = nn.MSELoss()
y_true = torch.tensor([3.0, -0.5, 2.0, 7.0])
y_pred = torch.tensor([2.5, 0.0, 2.1, 8.0])
loss_mse = criterion_mse(y_pred, y_true)
print(f"MSE Loss = {loss_mse.item():.4f}") # 输出: MSE Loss = 0.3875
# 方法2:使用functional.mse_loss
from torch.nn.functional import mse_loss
loss_mse2 = mse_loss(y_pred, y_true)
print(f"MSE Loss (functional) = {loss_mse2.item():.4f}")
# 方法3:reduction参数控制聚合方式
# reduction='mean'(默认):返回均值
# reduction='sum':返回总和
# reduction='none':返回每个样本的损失
loss_sum = mse_loss(y_pred, y_true, reduction='sum')
loss_none = mse_loss(y_pred, y_true, reduction='none')
print(f"MSE Loss (sum) = {loss_sum.item():.4f}") # 输出: 1.5500
print(f"MSE Loss (per-sample) = {loss_none}") # 输出: tensor([0.2500, 0.2500, 0.0100, 1.0000])
2.2 MAE(平均绝对误差)
数学公式:
\\text{MAE} = \\frac{1}{n} \\sum*{i=1}\^{n} \|y_i - \\hat{y}*i\|
特点:
-
优点:对异常值鲁棒,因为绝对值函数不会放大误差;对所有样本的误差一视同仁,不会过度关注异常值。
-
缺点:在 y_i = \\hat{y}_i 处不可导(梯度是亚梯度,存在不唯一性),这给优化带来一定困难;在误差较大时,梯度大小保持不变,可能导致训练后期震荡。
适用场景: 数据中存在较多异常值的回归任务,如异常检测中的回归、鲁棒回归等。
NumPy实现:
def mae_numpy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
计算平均绝对误差(MAE)
参数:
y_true: 真实值数组
y_pred: 预测值数组
返回:
MAE值(标量)
"""
return np.mean(np.abs(y_true - y_pred))
def mae_gradient_numpy(y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
"""
计算MAE的次梯度(Subgradient)
当y_true > y_pred时,梯度为+1/n
当y_true < y_pred时,梯度为-1/n
当y_true == y_pred时,梯度为[-1/n, +1/n]区间内的任意值(此处返回0)
参数:
y_true: 真实值数组
y_pred: 预测值数组
返回:
次梯度数组
"""
n = len(y_true)
diff = y_true - y_pred
# 使用sign函数:正数返回+1,负数返回-1,零返回0
return np.sign(diff) / n
# 示例
if __name__ == "__main__":
y_true = np.array([3.0, -0.5, 2.0, 7.0])
y_pred = np.array([2.5, 0.0, 2.1, 8.0])
loss = mae_numpy(y_true, y_pred)
grad = mae_gradient_numpy(y_true, y_pred)
print(f"MAE = {loss:.4f}") # 输出: MAE = 0.4750
print(f"梯度 = {grad}") # 输出: 梯度 = [-0.25 0.25 -0.25 -0.25]
PyTorch实现:
import torch
import torch.nn as nn
# ------------------- L1 Loss -------------------
criterion_l1 = nn.L1Loss()
y_true = torch.tensor([3.0, -0.5, 2.0, 7.0])
y_pred = torch.tensor([2.5, 0.0, 2.1, 8.0])
loss_l1 = criterion_l1(y_pred, y_true)
print(f"MAE Loss = {loss_l1.item():.4f}") # 输出: MAE Loss = 0.4750
# reduction参数同样支持 'mean', 'sum', 'none'
2.3 Huber Loss
数学公式:
\\text{Huber}(y, \\hat{y}) = \\begin{cases} \\frac{1}{2}(y - \\hat{y})\^2 \& \\text{if } \|y - \\hat{y}\| \\leq \\delta \\ \\delta \\cdot \|y - \\hat{y}\| - \\frac{1}{2}\\delta\^2 \& \\text{if } \|y - \\hat{y}\| \> \\delta \\end{cases}
其中 \\delta 是阈值超参数,控制从二次损失切换到线性损失的过渡点。
特点:
-
优点:综合了MSE(在误差小时)和MAE(在误差大时)的优点;当误差较小(\< \\delta)时使用平方项,保证收敛速度;当误差较大(\> \\delta)时使用线性项,降低异常值的影响。
-
缺点:需要手动调优 \\delta 参数;函数在 \|y - \\hat{y}\| = \\delta 处不可导(二阶导数不连续)。
适用场景: 介于MSE和MAE之间的场景,既希望对异常值有一定鲁棒性,又希望在大部分正常样本上快速收敛。Huber Loss广泛应用于Kaggle竞赛中的回归任务,以及部分目标检测框架(如Faster R-CNN的回归分支)。
NumPy实现:
def huber_loss_numpy(y_true: np.ndarray, y_pred: np.ndarray, delta: float = 1.0) -> float:
"""
计算Huber Loss
参数:
y_true: 真实值数组
y_pred: 预测值数组
delta: 阈值参数,控制从二次损失切换到线性损失的过渡点
返回:
Huber Loss均值
"""
errors = y_true - y_pred
abs_errors = np.abs(errors)
# 使用mask区分小误差和大误差
small_error_mask = abs_errors <= delta
large_error_mask = abs_errors > delta
# 小误差区域:使用二次损失
loss = np.zeros_like(errors, dtype=np.float64)
loss[small_error_mask] = 0.5 * errors[small_error_mask] ** 2
# 大误差区域:使用线性损失(减去delta^2/2保证连续性)
loss[large_error_mask] = delta * abs_errors[large_error_mask] - 0.5 * delta ** 2
return np.mean(loss)
def huber_gradient_numpy(y_true: np.ndarray, y_pred: np.ndarray, delta: float = 1.0) -> np.ndarray:
"""
计算Huber Loss的梯度
"""
errors = y_true - y_pred
abs_errors = np.abs(errors)
gradient = np.zeros_like(errors)
small_error_mask = abs_errors <= delta
# 小误差:梯度为 -error(与MSE相同)
gradient[small_error_mask] = -errors[small_error_mask]
# 大误差:梯度为 -delta * sign(error)
gradient[~small_error_mask] = -delta * np.sign(errors[~small_error_mask])
return gradient
# 示例
if __name__ == "__main__":
y_true = np.array([3.0, -0.5, 2.0, 7.0])
y_pred = np.array([2.5, 0.0, 2.1, 8.0])
for delta in [0.5, 1.0, 2.0]:
loss = huber_loss_numpy(y_true, y_pred, delta=delta)
print(f"Huber Loss (delta={delta}) = {loss:.4f}")
# 输出:
# Huber Loss (delta=0.5) = 0.5417
# Huber Loss (delta=1.0) = 0.3925
# Huber Loss (delta=2.0) = 0.4250
PyTorch实现:
import torch
import torch.nn as nn
# ------------------- Huber Loss -------------------
criterion_huber = nn.HuberLoss(reduction='mean', delta=1.0)
y_true = torch.tensor([3.0, -0.5, 2.0, 7.0])
y_pred = torch.tensor([2.5, 0.0, 2.1, 8.0])
loss_huber = criterion_huber(y_pred, y_true)
print(f"Huber Loss = {loss_huber.item():.4f}") # 输出: Huber Loss = 0.3925
# SmoothL1Loss(也称Huber Loss,delta固定为1.0)
criterion_smooth_l1 = nn.SmoothL1Loss(reduction='mean', beta=1.0)
loss_smooth_l1 = criterion_smooth_l1(y_pred, y_true)
print(f"Smooth L1 Loss = {loss_smooth_l1.item():.4f}")
2.4 Smooth L1 Loss(平滑L1损失)
Smooth L1 Loss本质上是Huber Loss的一种特例,当 \\beta = 1.0 时即为Huber Loss。PyTorch中的nn.SmoothL1Loss默认\\beta=1.0,与TensorFlow中的tf.losses.huber_loss等价。
数学公式(PyTorch实现):
\\text{SmoothL1}(x) = \\begin{cases} 0.5 \\cdot x\^2 / \\beta \& \\text{if } \|x\| \< \\beta \\ \|x\| - 0.5 \\cdot \\beta \& \\text{otherwise} \\end{cases}
其中 x = y_i - \\hat{y}_i。
特点:
-
比L1 Loss更平滑(在零点处可导),收敛更稳定
-
比L2 Loss对异常值更鲁棒(当误差超过\\beta时,梯度被截断)
-
广泛用于目标检测中的边界框回归(如Faster R-CNN、SSD)
适用场景: 目标检测中的边界框回归(Box Regression)、3D检测中的位置回归等。
PyTorch示例:
import torch
import torch.nn as nn
# Smooth L1 Loss,beta参数控制过渡点
criterion_smooth_l1 = nn.SmoothL1Loss(reduction='mean', beta=1.0)
# 模拟预测框与真实框的偏移量
# 假设4个预测样本,每个样本4个回归值(x, y, w, h)
y_pred = torch.tensor([[1.2, 0.8, 2.0, 1.5],
[0.5, 0.3, 1.0, 0.8],
[3.0, 2.5, 1.5, 1.2],
[0.1, 0.1, 0.5, 0.5]])
y_true = torch.tensor([[1.0, 1.0, 2.0, 1.5],
[0.5, 0.5, 1.0, 1.0],
[2.0, 2.0, 1.5, 1.0],
[0.0, 0.0, 0.5, 0.5]])
loss = criterion_smooth_l1(y_pred, y_true)
print(f"Smooth L1 Loss = {loss.item():.4f}")
2.5 回归损失函数对比
| 损失函数 | 对异常值敏感性 | 收敛速度 | 可导性 | 梯度特征 |
|---|---|---|---|---|
| MSE | 非常敏感 | 快(误差>1时梯度大) | 处处可导 | 梯度随误差线性增长 |
| MAE | 鲁棒 | 较慢 | 零点不可导(亚梯度) | 梯度恒定 |
| Huber | 中等敏感 | 中等 | 截断点不可导 | 小误差时平方,大误差时线性 |
| Smooth L1 | 鲁棒 | 中等 | 零点不可导 | 小误差平方,大误差线性 |
3. 分类损失函数
分类任务的目标是预测离散的类别标签。以下详细介绍几种主流的分类损失函数。
3.1 Binary Cross-Entropy(二元交叉熵)
数学公式:
\\text{BCE} = -\\frac{1}{n} \\sum*{i=1}\^{n} \\left\[ y_i \\cdot \\log(\\hat{y}*i) + (1 - y_i) \\cdot \\log(1 - \\hat{y}_i) \\right\]
其中 y_i \\in {0, 1} 为真实标签,\\hat{y}_i \\in (0, 1) 为预测为正类的概率。
特点:
-
优点:当预测概率偏离真实标签时,损失值迅速增大;天然的概率解释,与sigmoid激活函数配合良好;梯度形式简洁,计算高效。
-
缺点:当 y=0 且 \\hat{y}\\to 1 或 y=1 且 \\hat{y}\\to 0 时,\\log(0) 导致数值不稳定(需要eps截断);对预测概率的校准依赖假设(正确分类时损失应趋于0)。
适用场景: 二分类任务,如图像二分类(猫/狗)、垃圾邮件检测、疾病诊断等。
NumPy实现:
def binary_cross_entropy_numpy(y_true: np.ndarray, y_pred: np.ndarray, eps: float = 1e-7) -> float:
"""
计算二元交叉熵损失
参数:
y_true: 真实标签数组(0或1)
y_pred: 预测概率数组(0到1之间)
eps: 防止log(0)的极小值
返回:
BCE Loss均值
"""
# 将预测值限制在[eps, 1-eps]范围内避免数值问题
y_pred = np.clip(y_pred, eps, 1 - eps)
# 逐元素计算交叉熵
bce = - (y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
return np.mean(bce)
def bce_gradient_numpy(y_true: np.ndarray, y_pred: np.ndarray, eps: float = 1e-7) -> np.ndarray:
"""
BCE对预测概率的梯度
d(BCE)/d(y_pred) = (y_pred - y_true) / (y_pred * (1 - y_pred))
参数:
y_true: 真实标签数组
y_pred: 预测概率数组
返回:
梯度数组
"""
y_pred = np.clip(y_pred, eps, 1 - eps)
return (y_pred - y_true) / (y_pred * (1 - y_pred))
# 示例
if __name__ == "__main__":
y_true = np.array([1, 0, 1, 1, 0])
y_pred = np.array([0.9, 0.1, 0.8, 0.7, 0.2])
loss = binary_cross_entropy_numpy(y_true, y_pred)
grad = bce_gradient_numpy(y_true, y_pred)
print(f"BCE = {loss:.4f}") # 输出: BCE = 0.1733
print(f"梯度 = {grad}")
PyTorch实现:
import torch
import torch.nn as nn
# ------------------- Binary Cross-Entropy -------------------
# 方法1:使用nn.BCELoss(需要预测值已经过sigmoid)
criterion_bce = nn.BCELoss(reduction='mean')
y_pred_sigmoid = torch.sigmoid(torch.tensor([2.0, -2.0, 1.0, 0.5, -1.0]))
y_true = torch.tensor([1.0, 0.0, 1.0, 1.0, 0.0])
loss_bce = criterion_bce(y_pred_sigmoid, y_true)
print(f"BCELoss = {loss_bce.item():.4f}")
# 方法2:使用nn.BCEWithLogitsLoss(预测值未过sigmoid,内部自动应用sigmoid)
# 推荐使用,比BCELoss更数值稳定
criterion_bce_logits = nn.BCEWithLogitsLoss(reduction='mean')
logits = torch.tensor([2.0, -2.0, 1.0, 0.5, -1.0]) # 未经过sigmoid的原始分数
loss_bce_logits = criterion_bce_logits(logits, y_true)
print(f"BCEWithLogitsLoss = {loss_bce_logits.item():.4f}")
# 两种方法结果应该相同
# 输出类似: BCEWithLogitsLoss = 0.3562
# reduction参数:'mean', 'sum', 'none'
loss_none = criterion_bce_logits(logits, y_true, reduction='none')
print(f"Per-sample BCE = {loss_none}")
3.2 Categorical Cross-Entropy(类别交叉熵/多分类交叉熵)
数学公式(单样本):
\\text{CE}(y, \\hat{p}) = -\\sum*{k=1}\^{K} y_k \\cdot \\log(\\hat{p}*k)
其中 K 为类别数,y_k 是真实标签的one-hot编码(y_k = 1表示属于第k类),\\hat{p}_k 是模型预测的第k类的概率。
由于 y 是one-hot向量,实际计算时只需要关注真实类别 c 对应的 \\log(\\hat{p}_c)。
特点:
-
优点:与softmax激活函数配合,输出天然满足概率分布(和为1,非负);梯度形式简洁,便于反向传播;在多分类任务中广泛应用。
-
缺点:当预测概率趋于均匀分布时,损失趋近于 \\log(K),收敛较慢;无法处理类别不平衡问题。
适用场景: 多分类任务,如ImageNet图像分类、手写数字识别(MNIST)、文本分类等。
NumPy实现(Softmax + Cross-Entropy):
def softmax_numpy(x: np.ndarray, axis: int = -1) -> np.ndarray:
"""
Softmax函数,将logits转换为概率分布
参数:
x: 输入logits数组
axis: 执行softmax的维度
返回:
概率数组,各元素和为1
"""
x_exp = np.exp(x - np.max(x, axis=axis, keepdims=True)) # 减去max防止数值溢出
return x_exp / np.sum(x_exp, axis=axis, keepdims=True)
def categorical_cross_entropy_numpy(y_true: np.ndarray, y_pred: np.ndarray, eps: float = 1e-7) -> float:
"""
计算类别交叉熵损失
参数:
y_true: 真实标签(整数索引)或one-hot编码数组
y_pred: 预测概率数组(各行和为1)
返回:
CE Loss均值
"""
# 如果y_true是整数索引,转换为one-hot
if len(y_true.shape) == 1:
n_classes = y_pred.shape[-1]
y_true_onehot = np.eye(n_classes)[y_true]
else:
y_true_onehot = y_true
# 限制预测概率范围
y_pred = np.clip(y_pred, eps, 1 - eps)
# 逐类别计算交叉熵并求和
ce = -np.sum(y_true_onehot * np.log(y_pred), axis=-1)
return np.mean(ce)
# 示例
if __name__ == "__main__":
# 假设3个样本,4个类别
# 真实标签:[2, 0, 3](整数索引)
y_true_idx = np.array([2, 0, 3])
# 模型输出的logits(未经过softmax)
logits = np.array([[1.0, 2.0, 5.0, 1.0],
[3.0, 1.0, 0.5, 1.0],
[0.5, 0.5, 1.0, 4.0]])
# 先softmax归一化
probs = softmax_numpy(logits)
print("预测概率:")
print(probs)
# 计算交叉熵
loss = categorical_cross_entropy_numpy(y_true_idx, probs)
print(f"\nCategorical CE = {loss:.4f}")
# 验证:手动计算第一个样本(真实类别为2)
# softmax(2) = exp(5) / sum = 较大值,p_2应该最大
print(f"\n样本0各类别概率: {probs[0]}, 真实类别2的概率: {probs[0][2]:.4f}")
PyTorch实现:
import torch
import torch.nn as nn
# ------------------- Cross-Entropy Loss -------------------
# PyTorch的nn.CrossEntropyLoss = LogSoftmax + NLLLoss
# 内部已经包含softmax操作,输入应该是原始logits(未经softmax)
criterion_ce = nn.CrossEntropyLoss(reduction='mean')
# 输入形状: (N, C) 其中N是批量大小,C是类别数
# 目标形状: (N,) 整数索引
logits = torch.tensor([[1.0, 2.0, 5.0, 1.0], # 样本0的真实类别是2
[3.0, 1.0, 0.5, 1.0], # 样本1的真实类别是0
[0.5, 0.5, 1.0, 4.0]]) # 样本2的真实类别是3
targets = torch.tensor([2, 0, 3])
loss_ce = criterion_ce(logits, targets)
print(f"CrossEntropyLoss = {loss_ce.item():.4f}")
# 如果已经有概率分布(经过softmax),使用NLLLoss
log_probs = torch.log_softmax(logits, dim=-1)
criterion_nll = nn.NLLLoss(reduction='mean')
loss_nll = criterion_nll(log_probs, targets)
print(f"NLLLoss (验证) = {loss_nll.item():.4f}") # 与上面的CE Loss应该相同
# 对于多标签分类(每个样本可属于多个类别),使用BCE
# 预测单个类别的概率
3.3 Focal Loss
背景: Focal Loss由Lin等人于2017年在《Focal Loss for Dense Object Detection》中提出,旨在解决目标检测中正负样本严重不平衡的问题(如FPN中背景候选框远多于前景目标)。
数学公式:
\\text{FL}(p_t) = -\\alpha_t (1 - p_t)\^\\gamma \\log(p_t)
其中:
-
p_t = p 当真实标签 y=1,p_t = 1-p 当 y=0
-
\\alpha \\in \[0, 1\] 是平衡正负样本的权重系数
-
\\gamma \\in \[0, 5\] 是聚焦参数(Focal Parameter),用于降低易分类样本的权重
物理意义:
-
当样本被正确分类且 p_t 很大时,(1-p_t)\^\\gamma 因子会大幅降低该样本的损失贡献
-
当样本被错误分类且 p_t 很小时,(1-p_t)\^\\gamma \\approx 1,损失基本不变
-
\\gamma = 0 时退化为普通交叉熵;\\gamma 越大,对易分类样本的抑制越强
特点:
-
优点:有效解决类别不平衡问题;通过聚焦参数动态调整难易样本的权重;不依赖启发式的hard negative mining。
-
缺点:需要调参 \\alpha 和 \\gamma;在小数据集上可能不如交叉熵稳定。
适用场景: 目标检测(如RetinaNet)、医学影像分割、细粒度分类、任意类别不平衡的分类任务。
PyTorch实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class FocalLoss(nn.Module):
"""
Focal Loss for binary classification
论文: Lin et al. "Focal Loss for Dense Object Detection" (ICCV 2017)
FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
"""
def __init__(self, alpha: float = 0.25, gamma: float = 2.0, reduction: str = 'mean'):
super().__init__()
self.alpha = alpha # 正负样本平衡权重
self.gamma = gamma # 聚焦参数
self.reduction = reduction
def forward(self, inputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
"""
参数:
inputs: 预测logits(未经过sigmoid),形状 (N,) 或 (N, C)
targets: 真实标签,形状 (N,) ,值为0或1
返回:
Focal Loss值
"""
# 计算sigmoid概率
p = torch.sigmoid(inputs)
# 计算二元交叉熵(不取均值,保留每个样本的损失)
bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')
# 计算p_t:当targets=1时p_t=p,当targets=0时p_t=1-p
p_t = p * targets + (1 - p) * (1 - targets)
# 计算聚焦因子 (1 - p_t)^gamma
focal_weight = (1 - p_t) ** self.gamma
# alpha平衡因子
alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets)
# 最终Focal Loss
focal_loss = alpha_t * focal_weight * bce_loss
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# ------------------- 多分类Focal Loss -------------------
class MultiClassFocalLoss(nn.Module):
"""
Focal Loss扩展到多分类
FL(p_t) = - (1 - p_t)^gamma * log(p_t)
"""
def __init__(self, gamma: float = 2.0, reduction: str = 'mean'):
super().__init__()
self.gamma = gamma
self.reduction = reduction
def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
"""
参数:
logits: 模型输出的未归一化分数,形状 (N, C)
targets: 目标类别索引,形状 (N,)
返回:
Focal Loss值
"""
# 计算类别概率
p = F.softmax(logits, dim=-1)
# 获取目标类别的概率 p_t
# gather: 根据targets索引,在dim=-1上收集对应类别的概率
p_t = p.gather(dim=-1, index=targets.unsqueeze(-1)).squeeze(-1)
# 计算交叉熵(负对数似然)
ce_loss = F.cross_entropy(logits, targets, reduction='none')
# 聚焦因子
focal_weight = (1 - p_t) ** self.gamma
focal_loss = focal_weight * ce_loss
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# ------------------- 示例 -------------------
if __name__ == "__main__":
# 二分类Focal Loss示例
print("=" * 50)
print("Binary Focal Loss 示例")
print("=" * 50)
focal_loss_fn = FocalLoss(alpha=0.25, gamma=2.0)
bce_loss_fn = nn.BCEWithLogitsLoss()
# 模拟数据:存在类别不平衡(正样本少)
logits = torch.tensor([3.0, 1.5, -2.0, -1.0, 0.5, -3.0])
targets = torch.tensor([1.0, 1.0, 0.0, 0.0, 0.0, 0.0])
loss_bce = bce_loss_fn(logits, targets)
loss_focal = focal_loss_fn(logits, targets)
print(f"BCE Loss: {loss_bce.item():.4f}")
print(f"Focal Loss: {loss_focal.item():.4f}")
print("\n解读: Focal Loss通过(1-p_t)^gamma降低了易分类负样本的权重,")
print(" 使模型更关注难分类的正样本。")
# 多分类Focal Loss示例
print("\n" + "=" * 50)
print("Multi-class Focal Loss 示例")
print("=" * 50)
criterion_focal = MultiClassFocalLoss(gamma=2.0)
criterion_ce = nn.CrossEntropyLoss()
# 模拟10个样本,5个类别
logits = torch.randn(10, 5)
targets = torch.randint(0, 5, (10,))
loss_ce = criterion_ce(logits, targets)
loss_focal = criterion_focal(logits, targets)
print(f"CrossEntropy Loss: {loss_ce.item():.4f}")
print(f"Multi-class Focal Loss: {loss_focal.item():.4f}")
4. 特殊损失函数
4.1 Triplet Loss(三元组损失)
背景: Triplet Loss由Schroff等人于2015年在FaceNet中提出,用于学习具有良好区分性的人脸特征嵌入。
核心思想: 拉近相同类别样本的距离,推开不同类别样本的距离。
数学公式:
\\mathcal{L} = \\max\\left(0, \\; \|f(a) - f(p)\|\^2 - \|f(a) - f(n)\|\^2 + \\alpha \\right)
其中:
-
a(Anchor):锚样本
-
p(Positive):与锚同类别的正样本
-
n(Negative):与锚不同类别的负样本
-
f(\\cdot):特征嵌入函数(通常为CNN)
-
\\alpha:间隔边界(margin),防止所有样本嵌入到同一空间
损失为0的条件: \|f(a) - f(p)\|\^2 + \\alpha \< \|f(a) - f(n)\|\^2(正样本对距离 + 间隔 < 负样本对距离)
适用场景: 人脸识别、行人再识别(ReID)、图像检索、度量学习。
PyTorch实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class TripletLoss(nn.Module):
"""
Triplet Loss with margin-based ranking loss
损失为零的条件: d(a,p) + margin < d(a,n)
即正样本对的距离比负样本对的距离至少小margin
"""
def __init__(self, margin: float = 0.3, reduction: str = 'mean'):
super().__init__()
self.margin = margin # 间隔边界,确保同类样本足够近,异类样本足够远
def forward(self, anchor: torch.Tensor, positive: torch.Tensor, negative: torch.Tensor) -> torch.Tensor:
"""
参数:
anchor: 锚样本特征,形状 (N, D)
positive: 正样本特征,形状 (N, D),与anchor同类
negative: 负样本特征,形状 (N, D),与anchor不同类
返回:
Triplet Loss值
"""
# 计算两两之间的欧氏距离平方
d_pos = F.pairwise_distance(anchor, positive, p=2) # 正样本对距离
d_neg = F.pairwise_distance(anchor, negative, p=2) # 负样本对距离
# 计算三元组损失
# 当 d_pos + margin > d_neg 时产生损失
losses = F.relu(d_pos - d_neg + self.margin)
if self.reduction == 'mean':
return losses.mean()
elif self.reduction == 'sum':
return losses.sum()
else:
return losses
# ------------------- 示例 -------------------
if __name__ == "__main__":
triplet_loss_fn = TripletLoss(margin=0.3)
# 模拟批次: batch_size=4, 嵌入维度=128
anchor = torch.randn(4, 128)
positive = torch.randn(4, 128) # 假设与anchor同类别
negative = torch.randn(4, 128) # 假设与anchor不同类别
loss = triplet_loss_fn(anchor, positive, negative)
print(f"Triplet Loss = {loss.item():.4f}")
# 测试"简单三元组"(正样本已够近或负样本已够远)
# 简单负样本:距离已经很大
d_pos_good = F.pairwise_distance(anchor, positive, p=2)
d_neg_good = torch.tensor([5.0, 6.0, 4.0, 5.0]) # 负样本距离已经很大
# 硬负样本挖掘:距离最近的那些负样本
d_neg_hard = anchor + 0.1 # 负样本距离很小(最难区分的情况)
print(f"\n正样本距离均值: {d_pos_good.mean().item():.4f}")
print(f"负样本距离均值(简单): {d_neg_good.mean().item():.4f}")
# 使用硬负样本计算损失
loss_hard = triplet_loss_fn(anchor, positive, negative)
print(f"Triplet Loss (hard negative) = {loss_hard.item():.4f}")
4.2 Contrastive Loss(对比损失)
核心思想: 拉近相似样本的嵌入,推开不相似样本的嵌入(与Triplet Loss不同,Contrastive Loss每次只处理一对样本)。
数学公式:
\\mathcal{L} = y \\cdot d\^2 + (1 - y) \\cdot \\max(0, \\alpha - d)\^2
其中:
-
y \\in {0, 1}:标签,1表示相似(同类),0表示不相似(异类)
-
d = \|f(x_1) - f(x_2)\|_2:两个样本嵌入的欧氏距离
-
\\alpha:间隔边界(异类样本的距离应大于 \\alpha)
特点:
-
需要成对标签(相似/不相似)
-
异类样本的距离必须大于间隔 \\alpha 才不会产生损失
-
适合Siamese Network训练
适用场景: 签名验证、人脸验证(判断两张图片是否为同一人)、图像检索的表示学习。
PyTorch实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class ContrastiveLoss(nn.Module):
"""
Contrastive Loss (对比损失)
相似对(y=1): 损失 = d^2,鼓励距离变小
不相似对(y=0): 损失 = max(0, margin - d)^2,鼓励距离变大
"""
def __init__(self, margin: float = 1.0, reduction: str = 'mean'):
super().__init__()
self.margin = margin
def forward(self, emb1: torch.Tensor, emb2: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
"""
参数:
emb1: 样本1的嵌入,形状 (N, D)
emb2: 样本2的嵌入,形状 (N, D)
labels: 成对标签,(N,),1表示相似,0表示不相似
返回:
Contrastive Loss值
"""
# 计算欧氏距离
d = F.pairwise_distance(emb1, emb2, p=2)
# 相似对损失: y * d^2
loss_sim = labels * (d ** 2)
# 不相似对损失: (1-y) * max(0, margin - d)^2
loss_dis = (1 - labels) * F.relu(self.margin - d) ** 2
loss = loss_sim + loss_dis
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else:
return loss
# ------------------- 示例 -------------------
if __name__ == "__main__":
contrastive_loss_fn = ContrastiveLoss(margin=1.0)
# 模拟4对样本
emb1 = torch.randn(4, 128)
emb2 = torch.randn(4, 128)
# 成对标签:前2对相似(1),后2对不相似(0)
labels = torch.tensor([1.0, 1.0, 0.0, 0.0])
loss = contrastive_loss_fn(emb1, emb2, labels)
print(f"Contrastive Loss = {loss.item():.4f}")
# 分析各对样本的损失
d = F.pairwise_distance(emb1, emb2, p=2)
print(f"\n各对距离: {d}")
print("相似对(标签=1): 损失 = 距离^2")
print("不相似对(标签=0): 损失 = max(0, margin - 距离)^2")
4.3 GAN的对抗损失
背景: 生成对抗网络(GAN)由Goodfellow等人于2014年提出,包含生成器(Generator)和判别器(Discriminator)两个网络,通过对抗训练进行学习。
核心思想:
-
判别器D:区分真实样本和生成样本,最大化 \\log(D(x)) + \\log(1 - D(G(z)))
-
生成器G:生成逼真的假样本欺骗判别器,最小化 \\log(1 - D(G(z)))
对抗损失(Minimax Game):
\\min_G \\max_D \\; \\mathcal{L}*{GAN}(D, G) = \\mathbb{E}* {x \\sim p*{data}}\[\\log D(x)\] + \\mathbb{E}*{z \\sim p_z}\[\\log(1 - D(G(z)))\]
特点:
-
训练不稳定(mode collapse、梯度消失/爆炸)
-
判别器过于强大时,生成器梯度消失;过于弱时,判别器失去判别能力
-
衍生出多种稳定训练的变体:WGAN、WGAN-GP、LSGAN等
适用场景: 图像生成(如GAN、BigGAN)、图像到图像翻译(如CycleGAN)、风格迁移、文本生成等。
PyTorch实现(vanilla GAN):
import torch
import torch.nn as nn
import torch.nn.functional as F
class Generator(nn.Module):
"""简单的生成器"""
def __init__(self, latent_dim: int = 100, img_shape: tuple = (1, 28, 28)):
super().__init__()
self.latent_dim = latent_dim
self.img_shape = img_shape
# 简单的多层感知机
self.model = nn.Sequential(
nn.Linear(latent_dim, 128),
nn.LeakyReLU(0.2),
nn.Linear(128, 256),
nn.BatchNorm1d(256),
nn.LeakyReLU(0.2),
nn.Linear(256, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, int(torch.prod(torch.tensor(img_shape)))),
nn.Tanh() # 输出范围 [-1, 1]
)
def forward(self, z: torch.Tensor) -> torch.Tensor:
img = self.model(z)
return img.view(img.size(0), *self.img_shape)
class Discriminator(nn.Module):
"""简单的判别器"""
def __init__(self, img_shape: tuple = (1, 28, 28)):
super().__init__()
self.img_shape = img_shape
img_size = int(torch.prod(torch.tensor(img_shape)))
self.model = nn.Sequential(
nn.Linear(img_size, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid() # 输出[0,1],表示真实样本的概率
)
def forward(self, img: torch.Tensor) -> torch.Tensor:
img_flat = img.view(img.size(0), -1)
return self.model(img_flat)
class GAN:
"""
完整的GAN训练器
包含生成器、判别器和训练循环
"""
def __init__(self, latent_dim: int = 100, img_shape: tuple = (1, 28, 28), lr: float = 0.0002):
self.latent_dim = latent_dim
self.img_shape = img_shape
# 初始化网络
self.generator = Generator(latent_dim, img_shape)
self.discriminator = Discriminator(img_shape)
# 优化器
self.opt_g = torch.optim.Adam(self.generator.parameters(), lr=lr, betas=(0.5, 0.999))
self.opt_d = torch.optim.Adam(self.discriminator.parameters(), lr=lr, betas=(0.5, 0.999))
# 损失函数:二元交叉熵
self.criterion = nn.BCELoss()
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.generator.to(self.device)
self.discriminator.to(self.device)
def train_step(self, real_imgs: torch.Tensor):
"""
单步训练:先更新判别器,再更新生成器
参数:
real_imgs: 真实图像批次
"""
batch_size = real_imgs.size(0)
real_labels = torch.ones(batch_size, 1).to(self.device)
fake_labels = torch.zeros(batch_size, 1).to(self.device)
# ---------------------
# 1. 训练判别器
# ---------------------
self.opt_d.zero_grad()
# 真实图像的损失:应输出1
real_imgs = real_imgs.to(self.device)
d_pred_real = self.discriminator(real_imgs)
loss_d_real = self.criterion(d_pred_real, real_labels)
# 生成图像的损失:应输出0
z = torch.randn(batch_size, self.latent_dim).to(self.device)
fake_imgs = self.generator(z).detach() # detach避免计算生成器梯度
d_pred_fake = self.discriminator(fake_imgs)
loss_d_fake = self.criterion(d_pred_fake, fake_labels)
# 判别器总损失
loss_d = (loss_d_real + loss_d_fake) / 2
loss_d.backward()
self.opt_d.step()
# ---------------------
# 2. 训练生成器
# ---------------------
self.opt_g.zero_grad()
# 生成图像,判别器应输出1(欺骗判别器)
z = torch.randn(batch_size, self.latent_dim).to(self.device)
fake_imgs = self.generator(z)
d_pred_fake = self.discriminator(fake_imgs)
loss_g = self.criterion(d_pred_fake, real_labels)
loss_g.backward()
self.opt_g.step()
return {'loss_d': loss_d.item(), 'loss_g': loss_g.item()}
def generate_images(self, n: int = 16) -> torch.Tensor:
"""生成图像"""
z = torch.randn(n, self.latent_dim).to(self.device)
with torch.no_grad():
return self.generator(z).cpu()
# ------------------- 示例 -------------------
if __name__ == "__main__":
# 简化示例:展示对抗损失的数学形式
print("=" * 50)
print("GAN对抗损失说明")
print("=" * 50)
print("""
判别器目标: max E[log(D(x))] + E[log(1 - D(G(z)))]
生成器目标: min E[log(1 - D(G(z)))]
(等价于 min -E[log(D(G(z)))],即最大化判别器对假样本的误判概率)
当使用BCE Loss实现时:
- 判别器真实样本损失: -log(D(x)),期望D(x)接近1
- 判别器生成样本损失: -log(1 - D(G(z))),期望D(G(z))接近0
- 生成器损失: -log(D(G(z))),期望生成器骗过判别器
""")
# 实际使用时只需调用GAN类
gan = GAN(latent_dim=100, img_shape=(1, 28, 28))
print(f"生成器参数数量: {sum(p.numel() for p in gan.generator.parameters()):,}")
print(f"判别器参数数量: {sum(p.numel() for p in gan.discriminator.parameters()):,}")
5. 损失函数选择指南
5.1 按任务类型选择
| 任务类型 | 推荐损失函数 | 说明 |
|---|---|---|
| 标准回归 | MSE | 默认选择,收敛速度快 |
| 有异常值的回归 | MAE 或 Huber Loss | MAE对异常值鲁棒;Huber是MSE和MAE的折中 |
| 边界框回归 | Smooth L1 Loss | 对大误差鲁棒,零点附近平滑 |
| 二分类 | BCEWithLogitsLoss | 数值稳定,无需手动sigmoid |
| 多分类 | CrossEntropyLoss | 默认选择,结合了softmax和NLL |
| 类别不平衡二分类 | Focal Loss | 降低易分类样本权重,关注难样本 |
| 类别不平衡多分类 | Class-Balanced Loss 或 Focal Loss | 可与CE结合使用 |
| 人脸识别/度量学习 | Triplet Loss 或 Contrastive Loss | 学习判别性嵌入 |
| 图像生成(GAN) | 对抗损失 + 重构损失 | 对抗损失提供对抗训练信号 |
| 图像分割 | Dice Loss 或 IoU Loss | 直接优化交并比,适合前景背景不平衡 |
5.2 实际建议
-
从标准损失开始:对于大多数任务,先使用标准的MSE(回归)或CrossEntropy(分类),验证整个流程是否正常工作。
-
根据数据特点选择:如果数据中存在明显的异常值,考虑使用MAE或Huber Loss;如果类别严重不平衡,考虑Focal Loss。
-
组合损失函数 :在许多复杂任务中,单一损失函数可能不够。例如,人脸检测常用
Softmax Loss + Triplet Loss的组合;分割任务常用CrossEntropy + Dice Loss。 -
损失函数与评价指标的关系 :训练时使用的损失函数与最终评价指标(如IoU、AP、mAP)可能不完全一致。如果评价指标是IoU,可以考虑直接优化IoU损失(
1 - IoU)。 -
数值稳定性 :优先使用带"Logits"后缀的损失函数(如
BCEWithLogitsLoss、CrossEntropyLoss),它们内部已经做了数值稳定化处理。
6. 总结
损失函数是连接模型预测与真实标签的桥梁,也是驱动模型学习的核心信号。不同任务需要不同类型的损失函数:
-
回归任务关注预测值与真实值的数值差异,MSE、MAE、Huber Loss各有优劣
-
分类任务关注预测概率与真实分布的交叉熵,Focal Loss解决了不平衡问题
-
度量学习通过Triplet Loss、Contrastive Loss学习具有判别性的嵌入空间
-
生成模型通过对抗训练让生成器和判别器相互博弈
理解每个损失函数的数学本质、适用场景和局限性,是在深度学习中做出正确建模决策的基础。希望本文的系统梳理和代码示例能够帮助你在实际项目中快速定位合适的损失函数,顺利完成模型训练。