深度学习损失函数完全指南:理论、实现与应用

71 阅读5分钟

深度学习损失函数完全指南:理论、实现与应用

Comprehensive Loss Functions in Deep Learning: Theory, Implementation, and Applications


摘要:损失函数是深度学习模型优化的核心。本教程系统性地介绍了从基础分类损失到前沿对比学习损失的完整理论体系,结合PyTorch代码实现和可视化分析,帮助读者深入理解各类损失函数的数学原理、适用场景和最佳实践。

适用读者:具备机器学习基础的研究者、工程师和高年级学生

前置知识

  • Python与PyTorch编程基础
  • 概率论与数理统计(概率分布、期望、方差)
  • 微积分(偏导数、梯度)
  • 线性代数(向量、矩阵运算)

目录

  1. 引言与分类学
  2. 数学基础
  3. 分类损失函数
  4. 回归损失函数
  5. 距离与排序损失
  6. 现代架构的高级损失
  7. 设计原则与最佳实践
  8. 综合对比与总结

1. 引言与损失函数分类学

Introduction and Taxonomy of Loss Functions


1.1 什么是损失函数?

损失函数(Loss Function) 是衡量模型预测结果与真实标签之间差异的数学函数,也称为代价函数(Cost Function)或目标函数(Objective Function)。

在监督学习中,我们的核心目标是找到最优参数 θ\theta^*,使得损失函数最小化:

θ=argminθ1Ni=1NL(fθ(xi),yi)\theta^* = \arg\min_{\theta} \frac{1}{N}\sum_{i=1}^{N} \mathcal{L}(f_{\theta}(x_i), y_i)

其中:

  • L(,)\mathcal{L}(\cdot, \cdot) 是损失函数
  • fθ(xi)f_{\theta}(x_i) 是参数为 θ\theta 的模型对输入 xix_i 的预测
  • yiy_i 是真实标签
  • NN 是训练样本数量

1.2 经验风险最小化(ERM)原理

根据统计学习理论,模型的**真实风险(True Risk)**定义为:

Rtrue(θ)=E(x,y)Pdata[L(fθ(x),y)]R_{true}(\theta) = \mathbb{E}_{(x,y)\sim P_{data}}[\mathcal{L}(f_{\theta}(x), y)]

由于真实数据分布 PdataP_{data} 未知,我们使用**经验风险(Empirical Risk)**作为代理:

Remp(θ)=1Ni=1NL(fθ(xi),yi)R_{emp}(\theta) = \frac{1}{N}\sum_{i=1}^{N} \mathcal{L}(f_{\theta}(x_i), y_i)

这就是**经验风险最小化(Empirical Risk Minimization, ERM)**原理的核心思想。


1.3 损失函数的分类体系

损失函数分类学

Figure 1: 深度学习损失函数的层次分类体系

损失函数可以从多个维度进行分类:

分类维度类别典型代表
任务类型分类Cross-Entropy, Focal Loss
回归MSE, Huber Loss
排序/度量Triplet Loss, ArcFace
数学性质凸函数MSE, Cross-Entropy
非凸函数大多数深度学习损失
鲁棒性对异常值敏感MSE
对异常值鲁棒MAE, Huber
应用领域计算机视觉Focal Loss, Dice Loss
自然语言处理Cross-Entropy, CTC Loss
度量学习Triplet Loss, Contrastive Loss

2. 数学基础:从信息论到损失函数

Mathematical Foundations: From Information Theory to Loss Functions


2.1 信息论基础

损失函数的设计与信息论有着深刻的联系。理解以下概念对于深入理解损失函数至关重要。

2.1.1 信息熵(Entropy)

香农熵衡量随机变量的不确定性:

H(X)=xp(x)logp(x)=E[logp(X)]H(X) = -\sum_{x} p(x) \log p(x) = \mathbb{E}[-\log p(X)]

性质

  • 熵越大,不确定性越高
  • 对于离散均匀分布,熵取最大值
  • 对于确定性分布(one-hot),熵为0
2.1.2 交叉熵(Cross-Entropy)

衡量用分布 qq 来编码服从分布 pp 的数据时的平均编码长度:

H(p,q)=xp(x)logq(x)=Ep[logq(X)]H(p, q) = -\sum_{x} p(x) \log q(x) = \mathbb{E}_{p}[-\log q(X)]
2.1.3 KL散度(Kullback-Leibler Divergence)

衡量两个分布之间的"距离"(非对称):

DKL(pq)=xp(x)logp(x)q(x)=H(p,q)H(p)D_{KL}(p \| q) = \sum_{x} p(x) \log \frac{p(x)}{q(x)} = H(p, q) - H(p)

关键洞察:当 pp 是one-hot编码的真实标签时,H(p)=0H(p) = 0,因此:

LCE=H(p,q)=DKL(pq)\mathcal{L}_{CE} = H(p, q) = D_{KL}(p \| q)

这解释了为什么交叉熵损失在分类任务中如此有效——最小化交叉熵等价于最小化预测分布与真实分布之间的KL散度


2.2 最大似然估计视角

数学框架

Figure 2: 从信息论推导损失函数的数学框架

从概率角度,模型训练可以视为最大似然估计(Maximum Likelihood Estimation, MLE)

θ=argmaxθi=1Npθ(yixi)=argmaxθi=1Nlogpθ(yixi)\theta^* = \arg\max_{\theta} \prod_{i=1}^{N} p_{\theta}(y_i | x_i) = \arg\max_{\theta} \sum_{i=1}^{N} \log p_{\theta}(y_i | x_i)

取负对数后:

θ=argminθi=1Nlogpθ(yixi)\theta^* = \arg\min_{\theta} -\sum_{i=1}^{N} \log p_{\theta}(y_i | x_i)

这正是负对数似然损失(Negative Log-Likelihood, NLL),在分类任务中等价于交叉熵损失。


2.3 梯度特性与优化

损失函数的梯度特性直接影响优化效率:

损失函数梯度形式特点
MSEL=2(yy^)\nabla L = 2(y - \hat{y})误差越大梯度越大
MAEL=sign(yy^)\nabla L = \text{sign}(y - \hat{y})梯度恒定为±1
Cross-EntropyL=p^p\nabla L = \hat{p} - p与预测误差成正比
Focal LossL(1pt)γ\nabla L \propto (1-p_t)^{\gamma}自适应加权

好的损失函数应该具备

  1. 数值稳定性:避免数值溢出或下溢
  2. 合适的梯度范围:既不消失也不爆炸
  3. 与任务目标一致:优化损失等价于优化评价指标

3. 分类损失函数

Classification Losses: From Cross-Entropy to Focal Loss


3.1 交叉熵损失(Cross-Entropy Loss)

交叉熵是分类任务中最基础、最重要的损失函数。

3.1.1 多分类交叉熵

对于 CC 类分类问题,交叉熵损失定义为:

LCE=c=1Cyclog(p^c)\mathcal{L}_{CE} = -\sum_{c=1}^{C} y_c \log(\hat{p}_c)

其中 yy 是one-hot编码的真实标签,p^\hat{p} 是softmax输出的预测概率。

当真实类别为 kk 时,简化为:

LCE=log(p^k)=log(ezkj=1Cezj)\mathcal{L}_{CE} = -\log(\hat{p}_k) = -\log\left(\frac{e^{z_k}}{\sum_{j=1}^{C} e^{z_j}}\right)

其中 zkz_k 是类别 kk 的logit(未经softmax的输出)。

3.1.2 梯度推导

对logit zkz_k 求偏导:

LCEzi=p^iyi\frac{\partial \mathcal{L}_{CE}}{\partial z_i} = \hat{p}_i - y_i

这个简洁的梯度形式是交叉熵损失如此流行的原因之一:

  • 误差驱动:梯度与预测误差成正比
  • 自动归一化:softmax确保概率和为1
  • 数值稳定:PyTorch的实现结合log-softmax避免数值问题
3.1.3 二分类交叉熵(BCE)

对于二分类问题:

LBCE=[ylog(p^)+(1y)log(1p^)]\mathcal{L}_{BCE} = -[y \log(\hat{p}) + (1-y) \log(1-\hat{p})]

通常配合sigmoid激活函数使用:p^=σ(z)=11+ez\hat{p} = \sigma(z) = \frac{1}{1+e^{-z}}

# 导入必要的库
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt

# 设置中文字体支持和绘图风格
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.style.use('seaborn-v0_8-whitegrid')

# 设置随机种子保证可复现性
torch.manual_seed(42)
np.random.seed(42)

print("=" * 70)
print("3.1 交叉熵损失(Cross-Entropy Loss)演示")
print("=" * 70)

# 创建示例数据:4个样本,3个类别
batch_size = 4
num_classes = 3

# 模型输出的logits(未经softmax)
logits = torch.tensor([
    [2.0, 1.0, 0.1],   # 样本1:倾向于类别0
    [0.5, 2.5, 0.3],   # 样本2:倾向于类别1
    [0.2, 0.3, 3.0],   # 样本3:倾向于类别2
    [1.5, 1.5, 0.1]    # 样本4:类别0和1概率相近
], requires_grad=True)

# 真实标签
labels = torch.tensor([0, 1, 2, 0])

print("\n【输入数据】")
print(f"Logits形状: {logits.shape}")
print(f"Logits:\n{logits.detach().numpy()}")
print(f"\n真实标签: {labels.numpy()}")

# 方法1:使用PyTorch内置CrossEntropyLoss(推荐,数值最稳定)
ce_loss_fn = nn.CrossEntropyLoss()
loss_pytorch = ce_loss_fn(logits, labels)

print("\n【PyTorch CrossEntropyLoss】")
print(f"损失值: {loss_pytorch.item():.6f}")

# 方法2:手动实现交叉熵,展示计算过程
def manual_cross_entropy(logits, labels):
    """手动计算交叉熵损失,展示计算细节"""
    # Step 1: 计算softmax概率
    probs = F.softmax(logits, dim=1)
    
    # Step 2: 选取真实类别对应的概率
    batch_indices = torch.arange(logits.size(0))
    correct_probs = probs[batch_indices, labels]
    
    # Step 3: 计算负对数
    neg_log_probs = -torch.log(correct_probs)
    
    # Step 4: 求平均
    loss = neg_log_probs.mean()
    
    return loss, probs, correct_probs, neg_log_probs

loss_manual, probs, correct_probs, per_sample_loss = manual_cross_entropy(logits.detach(), labels)

print("\n【手动计算过程】")
print(f"Step 1 - Softmax概率:\n{probs.numpy().round(4)}")
print(f"\nStep 2 - 真实类别概率: {correct_probs.numpy().round(4)}")
print(f"Step 3 - 负对数损失: {per_sample_loss.numpy().round(4)}")
print(f"Step 4 - 平均损失: {loss_manual.item():.6f}")

# 验证两种方法结果一致
print(f"\n✓ 验证:PyTorch结果 ≈ 手动计算结果: {torch.isclose(loss_pytorch, loss_manual)}")
======================================================================
3.1 交叉熵损失(Cross-Entropy Loss)演示
======================================================================

【输入数据】
Logits形状: torch.Size([4, 3])
Logits:
[[2.  1.  0.1]
 [0.5 2.5 0.3]
 [0.2 0.3 3. ]
 [1.5 1.5 0.1]]

真实标签: [0 1 2 0]

【PyTorch CrossEntropyLoss】
损失值: 0.391739

【手动计算过程】
Step 1 - Softmax概率:
[[0.659  0.2424 0.0986]
 [0.1086 0.8025 0.0889]
 [0.0539 0.0596 0.8865]
 [0.4451 0.4451 0.1098]]

Step 2 - 真实类别概率: [0.659  0.8025 0.8865 0.4451]
Step 3 - 负对数损失: [0.417  0.22   0.1205 0.8094]
Step 4 - 平均损失: 0.391739

✓ 验证:PyTorch结果 ≈ 手动计算结果: True

3.2 Focal Loss:解决类别不平衡问题

在目标检测(如RetinaNet)和医学影像分析等场景中,正负样本比例可能达到1:1000甚至更极端。标准交叉熵损失会被大量简单负样本主导,导致模型学习效率低下。

Focal Loss(Lin et al., ICCV 2017)通过动态调整样本权重解决这一问题。

3.2.1 数学定义
LFL(pt)=αt(1pt)γlog(pt)\mathcal{L}_{FL}(p_t) = -\alpha_t (1-p_t)^{\gamma} \log(p_t)

其中:

  • ptp_t 是模型对真实类别的预测概率
  • γ0\gamma \geq 0焦点参数(focusing parameter),控制对困难样本的关注程度
  • αt(0,1)\alpha_t \in (0,1)平衡因子,处理正负样本数量不平衡
3.2.2 核心机制

调制因子 (1pt)γ(1-p_t)^{\gamma} 的作用:

ptp_t(预测正确的置信度)(1pt)2(1-p_t)^2γ=2\gamma=2时的权重)效果
0.9(容易样本)0.01损失降低100倍
0.5(中等难度)0.25损失降低4倍
0.1(困难样本)0.81损失基本保持

直观理解:模型会将更多注意力放在那些"还没学好"的困难样本上,而不会被大量已经学会的简单样本淹没。

3.2.3 参数选择建议

根据原论文的消融实验:

  • γ=2\gamma = 2:在COCO数据集上表现最优
  • α=0.25\alpha = 0.25:对于前景类(当前景:背景≈1:3时)
print("=" * 70)
print("3.2 Focal Loss 实现与分析")
print("=" * 70)

class FocalLoss(nn.Module):
    """Focal Loss的PyTorch实现
    
    论文: "Focal Loss for Dense Object Detection" (Lin et al., ICCV 2017)
    
    参数:
        alpha: 类别权重因子,用于处理类别不平衡
        gamma: 焦点参数,控制对困难样本的关注程度
        reduction: 损失聚合方式 ('mean', 'sum', 'none')
    """
    def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction
    
    def forward(self, logits, targets):
        # 计算标准交叉熵(不聚合)
        ce_loss = F.cross_entropy(logits, targets, reduction='none')
        
        # 计算p_t:真实类别的预测概率
        probs = F.softmax(logits, dim=1)
        batch_indices = torch.arange(logits.size(0))
        p_t = probs[batch_indices, targets]
        
        # 计算focal权重
        focal_weight = (1 - p_t) ** self.gamma
        
        # 组合得到focal loss
        focal_loss = self.alpha * focal_weight * ce_loss
        
        # 聚合
        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        return focal_loss

# 对比实验:容易样本 vs 困难样本
print("\n【实验1:容易样本 vs 困难样本的损失对比】")

# 容易样本:模型预测置信度高
easy_logits = torch.tensor([
    [5.0, -1.0, -1.0],  # 类别0概率≈0.99
    [-1.0, 5.0, -1.0],  # 类别1概率≈0.99
])
easy_labels = torch.tensor([0, 1])

# 困难样本:模型预测置信度低
hard_logits = torch.tensor([
    [0.5, 0.3, 0.2],   # 类别0概率≈0.39
    [0.3, 0.5, 0.2],   # 类别1概率≈0.39
])
hard_labels = torch.tensor([0, 1])

ce_loss_fn = nn.CrossEntropyLoss()
focal_loss_fn = FocalLoss(alpha=1.0, gamma=2.0)

print("\n样本类型      | CE Loss | Focal Loss (γ=2) | 降低比例")
print("-" * 60)

ce_easy = ce_loss_fn(easy_logits, easy_labels).item()
focal_easy = focal_loss_fn(easy_logits, easy_labels).item()
print(f"容易样本       | {ce_easy:7.4f} | {focal_easy:16.4f} | {(1-focal_easy/ce_easy)*100:5.1f}%")

ce_hard = ce_loss_fn(hard_logits, hard_labels).item()
focal_hard = focal_loss_fn(hard_logits, hard_labels).item()
print(f"困难样本       | {ce_hard:7.4f} | {focal_hard:16.4f} | {(1-focal_hard/ce_hard)*100:5.1f}%")

# 实验2:不同gamma值的影响
print("\n【实验2:γ参数对损失的影响】")
print("\np_t    | γ=0(CE) | γ=0.5  | γ=1    | γ=2    | γ=5")
print("-" * 60)

p_t_values = [0.1, 0.3, 0.5, 0.7, 0.9, 0.95]
gamma_values = [0, 0.5, 1, 2, 5]

for p_t in p_t_values:
    row = f"{p_t:.2f}   |"
    for gamma in gamma_values:
        loss = -((1 - p_t) ** gamma) * np.log(p_t)
        row += f" {loss:6.3f} |"
    print(row)
======================================================================
3.2 Focal Loss 实现与分析
======================================================================

【实验1:容易样本 vs 困难样本的损失对比】

样本类型      | CE Loss | Focal Loss (γ=2) | 降低比例
------------------------------------------------------------
容易样本       |  0.0049 |           0.0000 | 100.0%
困难样本       |  0.9398 |           0.3489 |  62.9%

【实验2:γ参数对损失的影响】

p_t    | γ=0(CE) | γ=0.5  | γ=1    | γ=2    | γ=5
------------------------------------------------------------
0.10   |  2.303 |  2.184 |  2.072 |  1.865 |  1.360 |
0.30   |  1.204 |  1.007 |  0.843 |  0.590 |  0.202 |
0.50   |  0.693 |  0.490 |  0.347 |  0.173 |  0.022 |
0.70   |  0.357 |  0.195 |  0.107 |  0.032 |  0.001 |
0.90   |  0.105 |  0.033 |  0.011 |  0.001 |  0.000 |
0.95   |  0.051 |  0.011 |  0.003 |  0.000 |  0.000 |
# 可视化:Focal Loss曲线
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 图1:不同gamma值下的Focal Loss曲线
p_range = np.linspace(0.01, 0.99, 200)
ce_curve = -np.log(p_range)

ax1 = axes[0]
ax1.plot(p_range, ce_curve, 'k-', linewidth=2.5, label='CE (γ=0)')
colors = ['#e74c3c', '#3498db', '#2ecc71', '#9b59b6']
for gamma, color in zip([0.5, 1, 2, 5], colors):
    focal_curve = -((1 - p_range) ** gamma) * np.log(p_range)
    ax1.plot(p_range, focal_curve, color=color, linewidth=2, label=f'Focal (γ={gamma})')

ax1.set_xlabel('预测概率 $p_t$(真实类别)', fontsize=12)
ax1.set_ylabel('损失值', fontsize=12)
ax1.set_title('Focal Loss vs Cross-Entropy: 随γ增大,容易样本损失被压缩', fontsize=13)
ax1.legend(loc='upper right', fontsize=10)
ax1.set_xlim([0, 1])
ax1.set_ylim([0, 5])
ax1.axvline(x=0.5, color='gray', linestyle='--', alpha=0.5)
ax1.annotate('困难样本区域', xy=(0.2, 4), fontsize=10, color='gray')
ax1.annotate('容易样本区域', xy=(0.75, 4), fontsize=10, color='gray')

# 图2:损失降低比例(相对于标准CE)
ax2 = axes[1]
gammas = [0.5, 1, 2, 5]
p_values = [0.9, 0.7, 0.5, 0.3, 0.1]
x = np.arange(len(p_values))
width = 0.18

for i, gamma in enumerate(gammas):
    reductions = []
    for p in p_values:
        ce = -np.log(p)
        focal = -((1-p)**gamma) * np.log(p)
        reduction = (1 - focal/ce) * 100
        reductions.append(reduction)
    ax2.bar(x + i*width, reductions, width, label=f'γ={gamma}', alpha=0.8)

ax2.set_xlabel('预测概率 $p_t$', fontsize=12)
ax2.set_ylabel('损失降低比例 (%)', fontsize=12)
ax2.set_title('Focal Loss相对于CE的损失降低比例', fontsize=13)
ax2.set_xticks(x + width * 1.5)
ax2.set_xticklabels([f'{p}' for p in p_values])
ax2.legend(loc='upper left')
ax2.set_ylim([0, 100])

plt.tight_layout()
plt.savefig('../output/diagrams/focal_loss_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

print("\n✓ 图表已保存至: output/diagrams/focal_loss_comparison.png")

png

✓ 图表已保存至: output/diagrams/focal_loss_comparison.png

3.3 标签平滑(Label Smoothing)

标签平滑(Szegedy et al., 2016)是一种简单有效的正则化技术,可以防止模型过度自信。

3.3.1 动机

使用硬标签(one-hot)训练时,模型被鼓励输出极端概率(接近0或1),这可能导致:

  • 过拟合:模型对训练数据过度自信
  • 校准问题:预测概率不能真实反映不确定性
  • 泛化性差:难以适应分布偏移
3.3.2 数学定义

将硬标签 yy 替换为软标签 yy'

yi={1ϵ+ϵKif i=y (正确类别)ϵKotherwise (其他类别)y'_i = \begin{cases} 1 - \epsilon + \frac{\epsilon}{K} & \text{if } i = y \text{ (正确类别)} \\ \frac{\epsilon}{K} & \text{otherwise (其他类别)} \end{cases}

其中:

  • ϵ\epsilon 是平滑参数(通常取0.1)
  • KK 是类别总数

直观理解:不再要求模型100%确定,而是允许"我90%确定是猫,但也可能是其他动物"。

print("=" * 70)
print("3.3 Label Smoothing 实现")
print("=" * 70)

class LabelSmoothingCrossEntropy(nn.Module):
    """带标签平滑的交叉熵损失
    
    参数:
        epsilon: 平滑参数,默认0.1
        reduction: 损失聚合方式
    """
    def __init__(self, epsilon=0.1, reduction='mean'):
        super().__init__()
        self.epsilon = epsilon
        self.reduction = reduction
    
    def forward(self, logits, targets):
        num_classes = logits.size(1)
        
        # 创建软标签
        soft_targets = torch.full_like(logits, self.epsilon / num_classes)
        soft_targets.scatter_(1, targets.unsqueeze(1), 1 - self.epsilon + self.epsilon / num_classes)
        
        # 计算交叉熵
        log_probs = F.log_softmax(logits, dim=1)
        loss = -(soft_targets * log_probs).sum(dim=1)
        
        if self.reduction == 'mean':
            return loss.mean()
        elif self.reduction == 'sum':
            return loss.sum()
        return loss

# 演示标签平滑效果
print("\n【标签平滑示例】")
num_classes = 5
epsilon = 0.1

# 硬标签(one-hot)
hard_label = torch.tensor([2])  # 类别2
one_hot = F.one_hot(hard_label, num_classes=num_classes).float()

# 软标签
soft_label = torch.full((1, num_classes), epsilon / num_classes)
soft_label[0, 2] = 1 - epsilon + epsilon / num_classes

print(f"真实类别: 2 (共{num_classes}类)")
print(f"硬标签 (one-hot):  {one_hot.numpy()[0]}")
print(f"软标签 (ε=0.1):   {soft_label.numpy()[0].round(4)}")

# 对比损失值
logits = torch.randn(4, 5)
labels = torch.tensor([0, 1, 2, 3])

ce_loss = nn.CrossEntropyLoss()
ls_loss = LabelSmoothingCrossEntropy(epsilon=0.1)

print(f"\n【损失对比】")
print(f"标准交叉熵损失: {ce_loss(logits, labels).item():.4f}")
print(f"标签平滑损失:   {ls_loss(logits, labels).item():.4f}")
print("\n说明:标签平滑损失通常略高,因为模型不再被要求100%确定")
======================================================================
3.3 Label Smoothing 实现
======================================================================

【标签平滑示例】
真实类别: 2 (共5类)
硬标签 (one-hot):  [0. 0. 1. 0. 0.]
软标签 (ε=0.1):   [0.02 0.02 0.92 0.02 0.02]

【损失对比】
标准交叉熵损失: 1.3171
标签平滑损失:   1.3780

说明:标签平滑损失通常略高,因为模型不再被要求100%确定

4. 回归损失函数

Regression Losses: From MSE to Robust Estimation


回归任务的目标是预测连续值。选择合适的损失函数需要考虑数据特性,特别是**异常值(outliers)**的存在。

4.1 均方误差(Mean Squared Error, MSE)

4.1.1 定义与性质
LMSE=1Ni=1N(yiy^i)2\mathcal{L}_{MSE} = \frac{1}{N}\sum_{i=1}^{N}(y_i - \hat{y}_i)^2

性质

  • 凸函数:存在唯一全局最优解
  • 处处可微:梯度 L=2(yy^)\nabla L = 2(y - \hat{y}),误差越大梯度越大
  • 对异常值敏感:平方项会放大大误差的影响

概率解释:假设残差服从高斯分布 ϵN(0,σ2)\epsilon \sim \mathcal{N}(0, \sigma^2),则最小化MSE等价于最大似然估计。


4.2 平均绝对误差(Mean Absolute Error, MAE)

LMAE=1Ni=1Nyiy^i\mathcal{L}_{MAE} = \frac{1}{N}\sum_{i=1}^{N}|y_i - \hat{y}_i|

与MSE对比

特性MSEMAE
对异常值敏感(平方放大)鲁棒(线性增长)
梯度2(yy^)2(y-\hat{y}),随误差变化±1\pm 1,恒定
在原点可微
收敛速度快(大误差时梯度大)慢(梯度恒定)

4.3 Huber Loss:最佳两全

Huber Loss(也称Smooth L1)结合了MSE和MAE的优点:

LHuber(e)={12e2if eδδ(eδ2)otherwise\mathcal{L}_{Huber}(e) = \begin{cases} \frac{1}{2}e^2 & \text{if } |e| \leq \delta \\ \delta(|e| - \frac{\delta}{2}) & \text{otherwise} \end{cases}

其中 e=yy^e = y - \hat{y} 是残差,δ\delta 是阈值参数。

特点

  • 小误差区域使用MSE(收敛快)
  • 大误差区域使用MAE(对异常值鲁棒)
  • 处处可微(在±δ\pm\delta处一阶导数连续)

4.4 分位数损失(Quantile Loss)

用于分位数回归,预测条件分布的特定分位数:

Lτ(y,y^)={τ(yy^)if yy^(1τ)(y^y)if y<y^\mathcal{L}_{\tau}(y, \hat{y}) = \begin{cases} \tau(y - \hat{y}) & \text{if } y \geq \hat{y} \\ (1-\tau)(\hat{y} - y) & \text{if } y < \hat{y} \end{cases}
  • τ=0.5\tau = 0.5:预测中位数(等价于MAE)
  • τ=0.9\tau = 0.9:预测90%分位数(上界)
  • τ=0.1\tau = 0.1:预测10%分位数(下界)

应用:时间序列预测区间估计、风险评估

print("=" * 70)
print("4. 回归损失函数实现与对比")
print("=" * 70)

# 创建包含异常值的回归数据
np.random.seed(42)
n_samples = 100

# 正常数据
y_true_normal = np.linspace(0, 10, n_samples)
y_pred_normal = y_true_normal + np.random.normal(0, 0.5, n_samples)

# 添加异常值
outlier_indices = [20, 50, 80]
y_true_with_outliers = y_true_normal.copy()
y_true_with_outliers[outlier_indices] = [15, 25, 20]  # 异常值

y_true = torch.tensor(y_true_with_outliers, dtype=torch.float32)
y_pred = torch.tensor(y_pred_normal, dtype=torch.float32)

print("\n【数据特征】")
print(f"样本数量: {n_samples}")
print(f"异常值索引: {outlier_indices}")
print(f"异常值大小: {[y_true_with_outliers[i] for i in outlier_indices]}")

# 计算各种损失
mse_loss = F.mse_loss(y_pred, y_true)
mae_loss = F.l1_loss(y_pred, y_true)
huber_loss = F.huber_loss(y_pred, y_true, delta=1.0)
smooth_l1_loss = F.smooth_l1_loss(y_pred, y_true)

print("\n【损失值对比(含异常值数据)】")
print(f"MSE Loss:       {mse_loss.item():.4f}")
print(f"MAE Loss:       {mae_loss.item():.4f}")
print(f"Huber Loss:     {huber_loss.item():.4f}")
print(f"Smooth L1 Loss: {smooth_l1_loss.item():.4f}")

# 分位数损失实现
def quantile_loss(y_true, y_pred, tau=0.5):
    """分位数损失函数"""
    errors = y_true - y_pred
    loss = torch.where(
        errors >= 0,
        tau * errors,
        (tau - 1) * errors
    )
    return loss.mean()

print("\n【分位数损失(不同τ值)】")
for tau in [0.1, 0.25, 0.5, 0.75, 0.9]:
    q_loss = quantile_loss(y_true, y_pred, tau=tau)
    print(f"τ = {tau:.2f}: {q_loss.item():.4f}")
======================================================================
4. 回归损失函数实现与对比
======================================================================

【数据特征】
样本数量: 100
异常值索引: [20, 50, 80]
异常值大小: [np.float64(15.0), np.float64(25.0), np.float64(20.0)]

【损失值对比(含异常值数据)】
MSE Loss:       7.0634
MAE Loss:       0.7920
Huber Loss:     0.5257
Smooth L1 Loss: 0.5257

【分位数损失(不同τ值)】
τ = 0.10: 0.1958
τ = 0.25: 0.2709
τ = 0.50: 0.3960
τ = 0.75: 0.5211
τ = 0.90: 0.5962
# 可视化:回归损失函数曲线对比
fig, axes = plt.subplots(1, 3, figsize=(16, 5))

errors = np.linspace(-5, 5, 300)

# 图1:损失函数曲线
ax1 = axes[0]
delta = 1.0

# MSE
mse_curve = errors ** 2
ax1.plot(errors, mse_curve, 'b-', linewidth=2, label='MSE (L2)')

# MAE
mae_curve = np.abs(errors)
ax1.plot(errors, mae_curve, 'r-', linewidth=2, label='MAE (L1)')

# Huber Loss
huber_curve = np.where(
    np.abs(errors) <= delta,
    0.5 * errors ** 2,
    delta * (np.abs(errors) - 0.5 * delta)
)
ax1.plot(errors, huber_curve, 'g-', linewidth=2, label=f'Huber (δ={delta})')

ax1.set_xlabel('残差 (y - ŷ)', fontsize=12)
ax1.set_ylabel('损失值', fontsize=12)
ax1.set_title('回归损失函数对比', fontsize=13)
ax1.legend(fontsize=10)
ax1.set_ylim([0, 10])
ax1.axvline(x=0, color='gray', linestyle='--', alpha=0.3)

# 图2:梯度曲线
ax2 = axes[1]
mse_grad = 2 * errors
mae_grad = np.sign(errors)
huber_grad = np.where(
    np.abs(errors) <= delta,
    errors,
    delta * np.sign(errors)
)

ax2.plot(errors, mse_grad, 'b-', linewidth=2, label='MSE梯度')
ax2.plot(errors, mae_grad, 'r-', linewidth=2, label='MAE梯度')
ax2.plot(errors, huber_grad, 'g-', linewidth=2, label='Huber梯度')

ax2.set_xlabel('残差 (y - ŷ)', fontsize=12)
ax2.set_ylabel('梯度', fontsize=12)
ax2.set_title('梯度特性对比:Huber在远离原点处梯度有界', fontsize=13)
ax2.legend(fontsize=10)
ax2.axhline(y=0, color='gray', linestyle='--', alpha=0.3)
ax2.axvline(x=0, color='gray', linestyle='--', alpha=0.3)

# 图3:分位数损失
ax3 = axes[2]
taus = [0.1, 0.25, 0.5, 0.75, 0.9]
colors_q = plt.cm.viridis(np.linspace(0.2, 0.8, len(taus)))

for tau, color in zip(taus, colors_q):
    quantile_curve = np.where(
        errors >= 0,
        tau * errors,
        (tau - 1) * errors
    )
    ax3.plot(errors, quantile_curve, color=color, linewidth=2, label=f'τ={tau}')

ax3.set_xlabel('残差 (y - ŷ)', fontsize=12)
ax3.set_ylabel('损失值', fontsize=12)
ax3.set_title('分位数损失:不对称惩罚过预测/欠预测', fontsize=13)
ax3.legend(fontsize=10)
ax3.axhline(y=0, color='gray', linestyle='--', alpha=0.3)
ax3.axvline(x=0, color='gray', linestyle='--', alpha=0.3)

plt.tight_layout()
plt.savefig('../output/diagrams/regression_loss_curves.png', dpi=150, bbox_inches='tight')
plt.show()

print("\n✓ 图表已保存至: output/diagrams/regression_loss_curves.png")

png

✓ 图表已保存至: output/diagrams/regression_loss_curves.png

5. 距离与排序损失:度量学习

Distance and Ranking Losses: Metric Learning


度量学习(Metric Learning) 的目标是学习一个嵌入空间,使得:

  • 相似样本在嵌入空间中距离近
  • 不相似样本在嵌入空间中距离远

应用场景:人脸识别、图像检索、推荐系统、few-shot学习


5.1 对比损失(Contrastive Loss)

用于样本对的学习,判断两个样本是否属于同一类别。

Lcontrastive=yD2+(1y)max(0,mD)2\mathcal{L}_{contrastive} = y \cdot D^2 + (1-y) \cdot \max(0, m - D)^2

其中:

  • y=1y = 1 表示正样本对(同类),y=0y = 0 表示负样本对(不同类)
  • D=f(x1)f(x2)2D = \|f(x_1) - f(x_2)\|_2 是嵌入向量的欧氏距离
  • mm 是**边距(margin)**参数

直觉

  • 对于正对:最小化距离(使其靠近)
  • 对于负对:推开到至少距离 mm

5.2 三元组损失(Triplet Loss)

由FaceNet(Schroff et al., CVPR 2015)提出,成为人脸识别的基础方法。

Ltriplet=max(0,f(a)f(p)2f(a)f(n)2+m)\mathcal{L}_{triplet} = \max(0, \|f(a) - f(p)\|^2 - \|f(a) - f(n)\|^2 + m)

其中:

  • aa锚点(anchor)
  • pp正样本(positive),与锚点同类
  • nn负样本(negative),与锚点不同类
  • mm 是边距参数

目标:确保 d(a,p)+m<d(a,n)d(a, p) + m < d(a, n),即正样本距离比负样本距离至少小 mm

5.2.1 三元组采样策略

三元组选择对训练效果至关重要:

策略描述特点
Easy Negativesd(a,n)>d(a,p)+md(a,n) > d(a,p) + m损失为0,无学习信号
Hard Negativesd(a,n)<d(a,p)d(a,n) < d(a,p)最困难,可能导致不稳定
Semi-Hard Negativesd(a,p)<d(a,n)<d(a,p)+md(a,p) < d(a,n) < d(a,p) + m平衡有效性和稳定性

5.3 ArcFace Loss:角度边距损失

ArcFace(Deng et al., CVPR 2019)在角度空间添加边距,是目前人脸识别的SOTA方法。

LArcFace=logescos(θy+m)escos(θy+m)+jyescosθj\mathcal{L}_{ArcFace} = -\log\frac{e^{s\cos(\theta_{y} + m)}}{e^{s\cos(\theta_{y} + m)} + \sum_{j \neq y} e^{s\cos\theta_j}}

其中:

  • θy\theta_y 是特征向量与真实类别权重向量的夹角
  • mm 是角度边距(通常0.5 rad)
  • ss 是缩放因子(通常30-64)

几何解释

  • 特征和权重都L2归一化后,投影到超球面
  • 在真实类别方向上添加角度惩罚
  • 迫使同类样本更紧凑、不同类样本更分离
print("=" * 70)
print("5. 度量学习损失函数实现")
print("=" * 70)

# 5.1 对比损失实现
class ContrastiveLoss(nn.Module):
    """对比损失函数"""
    def __init__(self, margin=1.0):
        super().__init__()
        self.margin = margin
    
    def forward(self, x1, x2, label):
        """
        x1, x2: 嵌入向量 [batch_size, embedding_dim]
        label: 1表示同类,0表示不同类 [batch_size]
        """
        distance = F.pairwise_distance(x1, x2)
        loss = label * distance.pow(2) + \
               (1 - label) * F.relu(self.margin - distance).pow(2)
        return loss.mean()

# 5.2 三元组损失实现
class TripletLoss(nn.Module):
    """三元组损失函数"""
    def __init__(self, margin=1.0):
        super().__init__()
        self.margin = margin
    
    def forward(self, anchor, positive, negative):
        """
        anchor: 锚点嵌入 [batch_size, embedding_dim]
        positive: 正样本嵌入 [batch_size, embedding_dim]
        negative: 负样本嵌入 [batch_size, embedding_dim]
        """
        pos_dist = F.pairwise_distance(anchor, positive)
        neg_dist = F.pairwise_distance(anchor, negative)
        loss = F.relu(pos_dist - neg_dist + self.margin)
        return loss.mean()

# 5.3 ArcFace损失实现
class ArcFaceLoss(nn.Module):
    """ArcFace: Additive Angular Margin Loss"""
    def __init__(self, in_features, out_features, s=30.0, m=0.5):
        super().__init__()
        self.s = s  # 缩放因子
        self.m = m  # 角度边距
        self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))
        nn.init.xavier_uniform_(self.weight)
        
        # 预计算
        self.cos_m = np.cos(m)
        self.sin_m = np.sin(m)
    
    def forward(self, features, labels):
        # L2归一化
        features = F.normalize(features, p=2, dim=1)
        weight = F.normalize(self.weight, p=2, dim=1)
        
        # 计算cos(theta)
        cosine = F.linear(features, weight)
        
        # 计算sin(theta)
        sine = torch.sqrt(1.0 - cosine.pow(2).clamp(0, 1))
        
        # cos(theta + m) = cos*cos_m - sin*sin_m
        phi = cosine * self.cos_m - sine * self.sin_m
        
        # 只对正确类别应用margin
        one_hot = F.one_hot(labels, num_classes=self.weight.size(0)).float()
        output = (one_hot * phi) + ((1.0 - one_hot) * cosine)
        output *= self.s
        
        return F.cross_entropy(output, labels)

# 演示
print("\n【演示数据设置】")
batch_size = 8
embedding_dim = 128
num_classes = 10

# 创建模拟嵌入
torch.manual_seed(42)
anchor = torch.randn(batch_size, embedding_dim)
positive = anchor + 0.1 * torch.randn(batch_size, embedding_dim)  # 正样本靠近anchor
negative = torch.randn(batch_size, embedding_dim)  # 负样本随机

print(f"Batch大小: {batch_size}")
print(f"嵌入维度: {embedding_dim}")

# 三元组损失
triplet_loss_fn = TripletLoss(margin=1.0)
triplet_loss = triplet_loss_fn(anchor, positive, negative)

print(f"\n【Triplet Loss】")
print(f"Anchor-Positive平均距离: {F.pairwise_distance(anchor, positive).mean().item():.4f}")
print(f"Anchor-Negative平均距离: {F.pairwise_distance(anchor, negative).mean().item():.4f}")
print(f"Triplet Loss: {triplet_loss.item():.4f}")

# ArcFace损失
features = torch.randn(batch_size, embedding_dim)
labels = torch.randint(0, num_classes, (batch_size,))

arcface_loss_fn = ArcFaceLoss(embedding_dim, num_classes, s=30.0, m=0.5)
arcface_loss = arcface_loss_fn(features, labels)

print(f"\n【ArcFace Loss】")
print(f"缩放因子s: 30.0, 角度边距m: 0.5 rad ({np.degrees(0.5):.1f}°)")
print(f"ArcFace Loss: {arcface_loss.item():.4f}")
======================================================================
5. 度量学习损失函数实现
======================================================================

【演示数据设置】
Batch大小: 8
嵌入维度: 128

【Triplet Loss】
Anchor-Positive平均距离: 1.0980
Anchor-Negative平均距离: 15.7465
Triplet Loss: 0.0000

【ArcFace Loss】
缩放因子s: 30.0, 角度边距m: 0.5 rad (28.6°)
ArcFace Loss: 18.6298

6. 现代架构的高级损失函数

Advanced Losses for Modern Architectures


6.1 NT-Xent Loss(对比学习)

NT-Xent(Normalized Temperature-scaled Cross Entropy)是SimCLR(Chen et al., ICML 2020)提出的自监督对比学习损失。

对于batch中的正样本对 (i,j)(i, j)

Li,j=logexp(sim(zi,zj)/τ)k=12N1kiexp(sim(zi,zk)/τ)\mathcal{L}_{i,j} = -\log\frac{\exp(\text{sim}(z_i, z_j)/\tau)}{\sum_{k=1}^{2N} \mathbb{1}_{k \neq i} \exp(\text{sim}(z_i, z_k)/\tau)}

其中:

  • zi,zjz_i, z_j 是同一图像两个增强视图的嵌入
  • sim(u,v)=uTvuv\text{sim}(u, v) = \frac{u^T v}{\|u\|\|v\|} 是余弦相似度
  • τ\tau温度参数(通常0.1-0.5)
  • NN 是batch大小

温度参数的作用

  • 低温度(如0.1):分布更尖锐,对困难负样本更敏感
  • 高温度(如1.0):分布更平滑,更均匀地对待所有负样本

6.2 VAE损失(变分自编码器)

变分自编码器的损失是Evidence Lower Bound(ELBO)的负值:

LVAE=Eq(zx)[logp(xz)]+DKL(q(zx)p(z))\mathcal{L}_{VAE} = -\mathbb{E}_{q(z|x)}[\log p(x|z)] + D_{KL}(q(z|x) \| p(z))

简化为:

LVAE=xx^2重构损失+βDKL(q(zx)N(0,I))KL散度正则化\mathcal{L}_{VAE} = \underbrace{\|x - \hat{x}\|^2}_{\text{重构损失}} + \beta \cdot \underbrace{D_{KL}(q(z|x) \| \mathcal{N}(0, I))}_{\text{KL散度正则化}}

其中KL散度项可以解析计算:

DKL=12j=1J(1+logσj2μj2σj2)D_{KL} = -\frac{1}{2}\sum_{j=1}^{J}(1 + \log\sigma_j^2 - \mu_j^2 - \sigma_j^2)

6.3 GAN损失

原始GAN损失(Goodfellow et al., 2014)
minGmaxDExpdata[logD(x)]+Ezpz[log(1D(G(z)))]\min_G \max_D \mathbb{E}_{x\sim p_{data}}[\log D(x)] + \mathbb{E}_{z\sim p_z}[\log(1-D(G(z)))]
WGAN损失(Wasserstein距离)
LWGAN=Expdata[D(x)]Ezpz[D(G(z))]\mathcal{L}_{WGAN} = \mathbb{E}_{x\sim p_{data}}[D(x)] - \mathbb{E}_{z\sim p_z}[D(G(z))]

优势:提供了有意义的距离度量,解决了原始GAN训练不稳定的问题。


6.4 知识蒸馏损失

用于将大模型(Teacher)的知识迁移到小模型(Student):

LKD=(1α)LCE(ps,y)+αT2DKL(pt(T)ps(T))\mathcal{L}_{KD} = (1-\alpha)\mathcal{L}_{CE}(p_s, y) + \alpha T^2 D_{KL}(p_t^{(T)} \| p_s^{(T)})

其中:

  • pt(T),ps(T)p_t^{(T)}, p_s^{(T)} 是temperature=T时的软标签
  • α\alpha 是权重因子
  • TT 是温度(通常4-20)
print("=" * 70)
print("6. 高级损失函数实现")
print("=" * 70)

# 6.1 NT-Xent Loss (SimCLR)
class NTXentLoss(nn.Module):
    """NT-Xent Loss for Contrastive Learning (SimCLR)"""
    def __init__(self, temperature=0.5):
        super().__init__()
        self.temperature = temperature
    
    def forward(self, z_i, z_j):
        """
        z_i, z_j: 同一batch的两个视图的嵌入 [batch_size, embedding_dim]
        """
        batch_size = z_i.shape[0]
        
        # L2归一化
        z_i = F.normalize(z_i, dim=1)
        z_j = F.normalize(z_j, dim=1)
        
        # 合并嵌入 [2*batch_size, embedding_dim]
        representations = torch.cat([z_i, z_j], dim=0)
        
        # 计算相似度矩阵 [2*batch_size, 2*batch_size]
        similarity = torch.mm(representations, representations.t()) / self.temperature
        
        # 创建mask,排除自身
        mask = torch.eye(2 * batch_size, dtype=torch.bool)
        similarity = similarity.masked_fill(mask, float('-inf'))
        
        # 正样本对的位置
        labels = torch.cat([torch.arange(batch_size) + batch_size,
                           torch.arange(batch_size)], dim=0)
        
        loss = F.cross_entropy(similarity, labels)
        return loss

# 6.2 VAE Loss
def vae_loss(recon_x, x, mu, logvar, beta=1.0):
    """VAE损失 = 重构损失 + β * KL散度"""
    # 重构损失(MSE)
    recon_loss = F.mse_loss(recon_x, x, reduction='sum')
    
    # KL散度
    kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    
    return recon_loss + beta * kl_loss, recon_loss, kl_loss

# 6.3 知识蒸馏损失
def knowledge_distillation_loss(student_logits, teacher_logits, labels, 
                                 temperature=4.0, alpha=0.7):
    """知识蒸馏损失"""
    # 硬标签损失
    hard_loss = F.cross_entropy(student_logits, labels)
    
    # 软标签损失
    soft_student = F.log_softmax(student_logits / temperature, dim=1)
    soft_teacher = F.softmax(teacher_logits / temperature, dim=1)
    soft_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean')
    
    # 组合损失
    total_loss = (1 - alpha) * hard_loss + alpha * (temperature ** 2) * soft_loss
    return total_loss, hard_loss, soft_loss

# 演示NT-Xent Loss
print("\n【6.1 NT-Xent Loss演示】")
batch_size = 32
embedding_dim = 128

z_i = torch.randn(batch_size, embedding_dim)
z_j = z_i + 0.1 * torch.randn(batch_size, embedding_dim)  # 正样本对

nt_xent = NTXentLoss(temperature=0.5)
loss_05 = nt_xent(z_i, z_j)

print("温度参数对NT-Xent Loss的影响:")
for temp in [0.1, 0.5, 1.0, 2.0]:
    nt_xent_temp = NTXentLoss(temperature=temp)
    loss = nt_xent_temp(z_i, z_j)
    print(f"  τ = {temp:.1f}: Loss = {loss.item():.4f}")

# 演示VAE Loss
print("\n【6.2 VAE Loss演示】")
x = torch.randn(16, 784)  # 原始数据
recon_x = x + 0.1 * torch.randn_like(x)  # 重构数据
mu = torch.randn(16, 32)  # 潜在均值
logvar = torch.zeros(16, 32)  # 潜在对数方差

total, recon, kl = vae_loss(recon_x, x, mu, logvar, beta=1.0)
print(f"总损失: {total.item():.2f}")
print(f"  - 重构损失: {recon.item():.2f}")
print(f"  - KL散度: {kl.item():.2f}")

# 演示知识蒸馏
print("\n【6.4 知识蒸馏损失演示】")
student_logits = torch.randn(8, 10)
teacher_logits = torch.randn(8, 10) * 2  # Teacher通常更confident
labels = torch.randint(0, 10, (8,))

total, hard, soft = knowledge_distillation_loss(
    student_logits, teacher_logits, labels, temperature=4.0, alpha=0.7
)
print(f"总损失: {total.item():.4f}")
print(f"  - 硬标签损失: {hard.item():.4f}")
print(f"  - 软标签损失: {soft.item():.4f}")
======================================================================
6. 高级损失函数实现
======================================================================

【6.1 NT-Xent Loss演示】
温度参数对NT-Xent Loss的影响:
  τ = 0.1: Loss = 0.0045
  τ = 0.5: Loss = 2.2682
  τ = 1.0: Loss = 3.1817
  τ = 2.0: Loss = 3.6584

【6.2 VAE Loss演示】
总损失: 394.36
  - 重构损失: 124.20
  - KL散度: 270.16

【6.4 知识蒸馏损失演示】
总损失: 2.6050
  - 硬标签损失: 3.3454
  - 软标签损失: 0.1430

7. 损失函数设计原则与最佳实践

Loss Function Design Principles and Best Practices


7.1 设计原则

7.1.1 从问题定义到损失函数

设计自定义损失函数的系统方法:

  1. 明确目标:你希望模型学会什么?
  2. 定义"好"的预测:什么样的输出是理想的?
  3. 量化误差:如何衡量预测与理想的差距?
  4. 确保可微:梯度下降需要可计算的梯度
  5. 考虑数值稳定性:避免log(0)、除零等问题
7.1.2 核心设计原则
原则说明示例
对齐性损失函数应与评价指标一致分类用CE,不用MSE
可微性确保梯度可以反向传播避免阶跃函数
有界性损失值应在合理范围内使用log防止爆炸
鲁棒性对异常值有容忍度Huber代替MSE
可解释性便于调试和分析分解为多个子项

7.2 梯度分析

监控梯度是调试损失函数的关键:

# 梯度监控示例
for name, param in model.named_parameters():
    if param.grad is not None:
        grad_norm = param.grad.norm()
        print(f"{name}: grad_norm = {grad_norm:.4f}")

常见问题与解决方案

问题症状解决方案
梯度消失梯度趋近于0使用ReLU、BatchNorm、残差连接
梯度爆炸梯度极大梯度裁剪、降低学习率
梯度不平衡某些参数梯度过大归一化损失项、调整权重

7.3 超参数敏感性

损失函数超参数对模型性能影响巨大:

Focal Loss的γ参数

  • γ=0:退化为标准CE
  • γ=2:适合中等不平衡(推荐起点)
  • γ=5+:仅适合极度不平衡

Triplet Loss的margin

  • margin过小:约束太弱,分离度不够
  • margin过大:难以满足,训练困难

7.4 调试技巧

  1. 从简单开始:先用标准损失验证模型
  2. 可视化损失曲线:观察收敛行为
  3. 检查梯度:确保梯度流动正常
  4. 消融实验:逐项测试损失组件
  5. 对比基线:与已知好的配置对比
print("=" * 70)
print("7. 损失函数工具箱与最佳实践")
print("=" * 70)

# 7.1 统一的损失函数工厂
class LossFactory:
    """损失函数工厂:提供统一接口创建各类损失函数"""
    
    CLASSIFICATION = {
        'ce': lambda: nn.CrossEntropyLoss(),
        'bce': lambda: nn.BCEWithLogitsLoss(),
        'focal': lambda alpha=0.25, gamma=2.0: FocalLoss(alpha, gamma),
        'label_smoothing': lambda eps=0.1: LabelSmoothingCrossEntropy(eps),
    }
    
    REGRESSION = {
        'mse': lambda: nn.MSELoss(),
        'mae': lambda: nn.L1Loss(),
        'huber': lambda delta=1.0: nn.HuberLoss(delta=delta),
        'smooth_l1': lambda: nn.SmoothL1Loss(),
    }
    
    METRIC = {
        'triplet': lambda margin=1.0: nn.TripletMarginLoss(margin=margin),
        'contrastive': lambda margin=1.0: ContrastiveLoss(margin),
    }
    
    @classmethod
    def get(cls, name, **kwargs):
        """根据名称获取损失函数"""
        all_losses = {**cls.CLASSIFICATION, **cls.REGRESSION, **cls.METRIC}
        if name.lower() not in all_losses:
            raise ValueError(f"Unknown loss: {name}. Available: {list(all_losses.keys())}")
        return all_losses[name.lower()](**kwargs)
    
    @classmethod
    def list_available(cls):
        """列出所有可用的损失函数"""
        print("【分类损失】:", list(cls.CLASSIFICATION.keys()))
        print("【回归损失】:", list(cls.REGRESSION.keys()))
        print("【度量损失】:", list(cls.METRIC.keys()))

print("\n【损失函数工厂演示】")
LossFactory.list_available()

# 7.2 梯度分析工具
def analyze_gradients(model, loss):
    """分析模型参数的梯度"""
    loss.backward()
    
    grad_stats = []
    for name, param in model.named_parameters():
        if param.grad is not None:
            grad = param.grad
            stats = {
                'name': name,
                'mean': grad.mean().item(),
                'std': grad.std().item(),
                'max': grad.max().item(),
                'min': grad.min().item(),
                'norm': grad.norm().item()
            }
            grad_stats.append(stats)
    return grad_stats

# 演示梯度分析
print("\n【梯度分析演示】")
simple_model = nn.Sequential(
    nn.Linear(10, 32),
    nn.ReLU(),
    nn.Linear(32, 3)
)

x = torch.randn(8, 10)
y = torch.randint(0, 3, (8,))
logits = simple_model(x)
loss = F.cross_entropy(logits, y)

grad_stats = analyze_gradients(simple_model, loss)
print(f"损失值: {loss.item():.4f}")
print("\n各层梯度统计:")
for stats in grad_stats:
    print(f"  {stats['name'][:20]:20s} | norm: {stats['norm']:.4f} | mean: {stats['mean']:.4f}")

# 7.3 多任务损失组合
class MultiTaskLoss(nn.Module):
    """多任务学习的自动权重损失"""
    def __init__(self, num_tasks):
        super().__init__()
        # 可学习的对数权重(确保权重为正)
        self.log_vars = nn.Parameter(torch.zeros(num_tasks))
    
    def forward(self, losses):
        """
        losses: 各任务的损失列表
        """
        total_loss = 0
        for i, loss in enumerate(losses):
            precision = torch.exp(-self.log_vars[i])
            total_loss += precision * loss + self.log_vars[i]
        return total_loss

print("\n【多任务损失演示】")
mt_loss = MultiTaskLoss(num_tasks=3)
task_losses = [torch.tensor(1.0), torch.tensor(0.5), torch.tensor(2.0)]
combined = mt_loss(task_losses)
print(f"各任务损失: {[l.item() for l in task_losses]}")
print(f"组合后损失: {combined.item():.4f}")
print(f"学习到的权重: {torch.exp(-mt_loss.log_vars).detach().numpy().round(4)}")
======================================================================
7. 损失函数工具箱与最佳实践
======================================================================

【损失函数工厂演示】
【分类损失】: ['ce', 'bce', 'focal', 'label_smoothing']
【回归损失】: ['mse', 'mae', 'huber', 'smooth_l1']
【度量损失】: ['triplet', 'contrastive']

【梯度分析演示】
损失值: 1.0901

各层梯度统计:
  0.weight             | norm: 0.3907 | mean: -0.0008
  0.bias               | norm: 0.1061 | mean: -0.0046
  2.weight             | norm: 0.6441 | mean: 0.0000
  2.bias               | norm: 0.0727 | mean: 0.0000

【多任务损失演示】
各任务损失: [1.0, 0.5, 2.0]
组合后损失: 3.5000
学习到的权重: [1. 1. 1.]

8. 综合对比与总结

Comprehensive Comparison and Conclusions


8.1 损失函数选择决策树

损失函数对比矩阵

Figure 3: 损失函数综合对比矩阵

你的任务是什么?
│
├─► 分类任务
│   ├─► 类别平衡? ──────────► Cross-Entropy Loss
│   ├─► 类别不平衡? ────────► Focal Loss (γ=2, α=0.25)
│   ├─► 需要置信度校准? ────► Label Smoothing (ε=0.1)
│   └─► 多标签分类? ────────► Binary Cross-Entropy
│
├─► 回归任务
│   ├─► 数据干净,无异常值? ► MSE
│   ├─► 存在异常值? ────────► Huber Loss 或 MAE
│   └─► 需要预测区间? ──────► Quantile Loss
│
├─► 度量学习/嵌入
│   ├─► 样本对学习? ────────► Contrastive Loss
│   ├─► 三元组可用? ────────► Triplet Loss
│   └─► 大规模人脸识别? ────► ArcFace (s=30, m=0.5)
│
└─► 自监督/生成
    ├─► 对比学习? ──────────► NT-Xent Loss (τ=0.5)
    ├─► 变分推断? ──────────► VAE ELBO
    └─► 图像生成? ──────────► WGAN-GP

8.2 损失函数综合对比表

损失函数任务类型关键特性适用场景主要超参数
Cross-Entropy分类信息论基础,数值稳定通用分类
Focal Loss分类自动困难样本挖掘类别不平衡γ, α
Label Smoothing分类正则化,防止过拟合需要校准ε
MSE回归凸函数,快速收敛干净数据
MAE回归对异常值鲁棒存在异常值
Huber回归平衡MSE和MAE通用回归δ
Triplet度量相对距离约束人脸/检索margin
ArcFace度量角度边距,SOTA大规模人脸s, m
NT-Xent自监督对比学习标准预训练τ

8.3 核心要点总结

分类任务
  1. 首选Cross-Entropy:数值稳定,理论基础扎实
  2. 类别不平衡用Focal Loss:自动关注困难样本
  3. 防止过拟合用Label Smoothing:改善校准性
回归任务
  1. 干净数据用MSE:收敛快,梯度随误差缩放
  2. 有异常值用Huber/MAE:鲁棒性更好
  3. 不确定性估计用Quantile Loss:预测分布分位数
度量学习
  1. Triplet Loss是基础:理解后再进阶
  2. ArcFace是人脸识别SOTA:角度边距效果最好
  3. 困难样本挖掘是关键:Semi-hard mining效果好
高级任务
  1. 对比学习用NT-Xent:温度参数很重要
  2. VAE用ELBO:重构+KL散度
  3. 多任务用不确定性加权:自动平衡各任务

8.4 参考文献

分类损失

  • Lin et al. "Focal Loss for Dense Object Detection." ICCV 2017.
  • Szegedy et al. "Rethinking the Inception Architecture for Computer Vision." CVPR 2016.

回归损失

  • Huber, P. J. "Robust Estimation of a Location Parameter." Annals of Statistics 1964.
  • Ren et al. "Faster R-CNN: Towards Real-Time Object Detection." NeurIPS 2015.

度量学习

  • Schroff et al. "FaceNet: A Unified Embedding for Face Recognition." CVPR 2015.
  • Deng et al. "ArcFace: Additive Angular Margin Loss for Deep Face Recognition." CVPR 2019.
  • Wang et al. "CosFace: Large Margin Cosine Loss for Deep Face Recognition." CVPR 2018.

对比学习与生成模型

  • Chen et al. "A Simple Framework for Contrastive Learning of Visual Representations." ICML 2020.
  • He et al. "Momentum Contrast for Unsupervised Visual Representation Learning." CVPR 2020.
  • Goodfellow et al. "Generative Adversarial Nets." NeurIPS 2014.
  • Kingma & Welling. "Auto-Encoding Variational Bayes." ICLR 2014.
  • Arjovsky et al. "Wasserstein GAN." ICML 2017.

多任务学习

  • Kendall et al. "Multi-Task Learning Using Uncertainty to Weigh Losses." CVPR 2018.

综述

  • Wang et al. "A Comprehensive Survey of Loss Functions in Machine Learning." 2022.
  • Terven & Cordova-Esparza. "Loss Functions and Metrics in Deep Learning." 2023.

8.5 结语

损失函数是深度学习的核心组件,选择合适的损失函数需要:

  1. 理解任务需求和数据特性
  2. 掌握各类损失函数的数学原理
  3. 通过实验验证和调参优化

本教程涵盖了从基础到前沿的损失函数知识,希望能帮助读者在实际项目中做出更好的选择。

# 最终综合对比可视化
print("=" * 70)
print("8. 损失函数综合对比可视化")
print("=" * 70)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 图1:分类损失对比
ax1 = axes[0, 0]
p_range = np.linspace(0.01, 0.99, 200)
ax1.plot(p_range, -np.log(p_range), 'b-', lw=2, label='Cross-Entropy')
ax1.plot(p_range, -((1-p_range)**2)*np.log(p_range), 'r-', lw=2, label='Focal (γ=2)')
ax1.set_xlabel('预测概率 $p_t$')
ax1.set_ylabel('损失值')
ax1.set_title('分类损失函数对比')
ax1.legend()
ax1.set_xlim([0, 1])
ax1.set_ylim([0, 5])
ax1.grid(True, alpha=0.3)

# 图2:回归损失对比
ax2 = axes[0, 1]
errors = np.linspace(-4, 4, 200)
ax2.plot(errors, errors**2, 'b-', lw=2, label='MSE')
ax2.plot(errors, np.abs(errors), 'r-', lw=2, label='MAE')
huber = np.where(np.abs(errors) <= 1, 0.5*errors**2, np.abs(errors)-0.5)
ax2.plot(errors, huber, 'g-', lw=2, label='Huber (δ=1)')
ax2.set_xlabel('残差')
ax2.set_ylabel('损失值')
ax2.set_title('回归损失函数对比')
ax2.legend()
ax2.set_ylim([0, 8])
ax2.grid(True, alpha=0.3)

# 图3:损失函数适用场景
ax3 = axes[1, 0]
categories = ['分类\n(平衡)', '分类\n(不平衡)', '回归\n(干净)', '回归\n(异常)', '度量\n学习', '对比\n学习']
losses = ['CE', 'Focal', 'MSE', 'Huber', 'Triplet', 'NT-Xent']
colors = ['#3498db', '#e74c3c', '#2ecc71', '#9b59b6', '#f39c12', '#1abc9c']

bars = ax3.bar(categories, [1]*6, color=colors, alpha=0.7, edgecolor='black')
for bar, loss in zip(bars, losses):
    height = bar.get_height()
    ax3.text(bar.get_x() + bar.get_width()/2., height/2,
             loss, ha='center', va='center', fontsize=11, fontweight='bold', color='white')

ax3.set_ylabel('推荐程度')
ax3.set_title('不同场景推荐的损失函数')
ax3.set_ylim([0, 1.2])
ax3.set_yticks([])

# 图4:超参数敏感性热力图
ax4 = axes[1, 1]
gammas = [0, 0.5, 1, 2, 5]
probs = [0.1, 0.3, 0.5, 0.7, 0.9]
loss_matrix = np.zeros((len(probs), len(gammas)))

for i, p in enumerate(probs):
    for j, g in enumerate(gammas):
        loss_matrix[i, j] = -((1-p)**g) * np.log(p)

im = ax4.imshow(loss_matrix, cmap='YlOrRd', aspect='auto')
ax4.set_xticks(range(len(gammas)))
ax4.set_xticklabels([f'γ={g}' for g in gammas])
ax4.set_yticks(range(len(probs)))
ax4.set_yticklabels([f'p={p}' for p in probs])
ax4.set_xlabel('Focal Loss γ参数')
ax4.set_ylabel('预测概率')
ax4.set_title('Focal Loss参数敏感性')

# 添加数值标注
for i in range(len(probs)):
    for j in range(len(gammas)):
        text = ax4.text(j, i, f'{loss_matrix[i, j]:.2f}',
                       ha='center', va='center', color='black', fontsize=9)

plt.colorbar(im, ax=ax4, label='损失值')
plt.tight_layout()
plt.savefig('../output/diagrams/loss_comprehensive_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

print("\n✓ 综合对比图已保存至: output/diagrams/loss_comprehensive_comparison.png")
print("\n" + "=" * 70)
print("教程完成!")
print("=" * 70)

png

✓ 综合对比图已保存至: output/diagrams/loss_comprehensive_comparison.png

======================================================================
教程完成!
======================================================================