关于AI损失函数的几点记录

AI损失函数相关的坑,多半出在边界条件上。

最近在做一个图像分割的项目,遇到了一个经典问题:明明训练准确率已经到 95% 以上,但实际用的时候总是差那么一点。

损失函数到底是什么

用最直白的话说,损失函数就是"如何衡量预测和真实之间的差距"。

但这里有个容易被忽视的点:损失函数不只是衡量差距,它还在定义"什么是重要的"。如果你在做一个回归任务,用 MSE 还是 MAE,模型关注的重点完全不同——MSE 会对大的错误更敏感,模型会优先避免那种"特别离谱"的预测;而 MAE 对所有错误一视同仁,模型会更均衡地处理各种情况。

# 简单的 MSE 和 MAE 对比
import numpy as np

def mse(y_true, y_pred):
    return np.mean((y_true - y_pred) ** 2)

def mae(y_true, y_pred):
    return np.mean(np.abs(y_true - y_pred))

# 真实值和两个预测
y_true = np.array([1.0, 1.0, 1.0, 10.0])
pred_mse_style = np.array([1.1, 0.9, 1.1, 11.0])  # 有一个较大的错误
pred_mae_style = np.array([1.3, 0.7, 1.3, 9.8])    # 多个较小的错误

print("MSE - pred_mse_style:", mse(y_true, pred_mse_style))
print("MSE - pred_mae_style:", mse(y_true, pred_mae_style))
print("MAE - pred_mse_style:", mae(y_true, pred_mse_style))
print("MAE - pred_mae_style:", mae(y_true, pred_mae_style))

上面的例子可以看出来:如果你的业务里更怕"特别离谱"的错误,用 MSE 会更合适;但如果你的数据里本来就有很多异常值,MAE 反而会更稳健。

常见问题:从分类到回归

分类任务:Cross-Entropy 还是 Focal Loss?

做分类任务时,Cross-Entropy 是最常见的选择。但实际中会遇到一个问题:类别不平衡时,模型容易偏向多数类。

上次做一个医疗影像分类项目,正样本只有 2%,模型哪怕全预测成负样本,准确率都有 98%。这时候用 Focal Loss 会有效果:

import torch
import torch.nn as nn

# 普通 Cross Entropy
ce_loss = nn.CrossEntropyLoss()

# Focal Loss 实现
class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2.0):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, inputs, targets):
        ce_loss = nn.functional.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-ce_loss)
        focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
        return focal_loss.mean()

# 实际使用中,alpha 和 gamma 需要根据数据分布调整
# alpha 处理正负样本权重,gamma 控制对难分样本的关注程度

Focal Loss 的核心思想是:把注意力放在"难分"的样本上,而不是被大量"容易分"的样本淹没。

Focal Loss 的调制机制:左侧展示不同 gamma 值下模型置信度如何影响权重,右侧说明难分样本和易分样本在不同 gamma 值下的权重差异

从图里可以看出来:当 gamma=0 时,Focal Loss 退化为普通 Cross Entropy,所有样本权重相同;随着 gamma 增大,模型对"容易分"的样本(高置信度)会自动降低权重,而对"难分"的样本(低置信度)保持较高的权重。但这里有个坑:如果数据本身就有噪声,过分关注难分样本反而会学到噪声。

回归任务:Huber Loss 的取舍

回归任务中,MSE 对异常值敏感,MAE 又对小的变化不够敏感。Huber Loss 试图兼顾两者:

def huber_loss(y_true, y_pred, delta=1.0):
    error = y_true - y_pred
    abs_error = np.abs(error)
    quadratic = np.minimum(abs_error, delta)
    linear = abs_error - quadratic
    return np.mean(0.5 * quadratic ** 2 + delta * linear)

但 Huber Loss 的问题是:需要手动调 delta 参数,而且不同数据分布下最佳值差别很大。如果 delta 选得太大,它就退化为 MSE;选得太小,它就接近 MAE。

不同损失函数的敏感度对比:左侧展示 MSE 与 MAE 的差异,右侧展示不同 delta 值下 Huber Loss 的行为变化

这个图清楚地说明了一个问题:MSE 在误差较大时快速上升(对异常值敏感),MAE 线性增长(对所有误差一视同仁),而 Huber Loss 则在两者之间找到平衡点——小误差时用 MSE 保证梯度足够大,大误差时用 MAE 避免被个别异常值主导。

实际项目中,如果不确定用哪个,可以先用 MSE,如果发现某些样本的预测偏差特别大,再考虑换 Huber Loss 或者 MAE。

多任务学习:如何平衡不同损失

做多任务学习时,不同任务的损失值量级可能完全不同。比如一个任务是分类,另一个是回归,把两个损失直接加起来,回归任务的损失值可能会完全主导梯度更新。

常见的做法有三种:

1. 手动调权重

# 直接根据经验调权重
total_loss = 1.0 * classification_loss + 0.01 * regression_loss

这种做法的缺点是:需要大量实验才能找到合适的权重,而且换了数据分布后又要重新调。

2. 自动调整权重

Uncertainty Weighting 是一个比较实用的方法:

class UncertaintyWeightedLoss(nn.Module):
    def __init__(self, num_tasks):
        super().__init__()
        # 每个任务学习一个 log_sigma 参数
        self.log_sigmas = nn.Parameter(torch.zeros(num_tasks))

    def forward(self, losses):
        # losses 是每个任务的损失张量列表
        weighted_losses = []
        for i, loss in enumerate(losses):
            weighted_loss = loss / (2 * torch.exp(2 * self.log_sigmas[i])) + self.log_sigmas[i]
            weighted_losses.append(weighted_loss)
        return torch.stack(weighted_losses).sum()

这个方法的核心思想是:让模型自己学习每个任务的"不确定性",不确定性高的任务会自动降低权重。

3. Gradient Normalization

Gradient Normalization 则直接从梯度的角度出发,确保每个任务的梯度量级相近:

def gradient_normalization(losses, model):
    gradients = []
    for loss in losses:
        model.zero_grad()
        loss.backward(retain_graph=True)
        grad_norm = 0
        for param in model.parameters():
            if param.grad is not None:
                grad_norm += param.grad.norm() ** 2
        gradients.append(grad_norm.sqrt())
    
    # 调整权重使得梯度量级相近
    mean_grad = torch.mean(torch.stack(gradients))
    weights = [mean_grad / g for g in gradients]
    
    return sum(w * l for w, l in zip(weights, losses))

但这几种方法都有各自的坑:Uncertainty Weighting 在某些情况下会不稳定,Gradient Normalization 计算成本较高,手动调权重又太依赖经验。

实际踩过的坑

坑一:损失值下降但效果变差

有一次训练一个 GAN 生成模型,生成器的损失值一直在下降,但生成的图片质量越来越差。后来才发现:判别器训练得太快,生成器一直在"骗"判别器,而不是真正在生成好的样本。

graph LR G[生成器] -->|生成图片| D[判别器] D -->|真假判断| G D -->|反馈梯度| G D -->|自我优化| D style G fill:#f9f,stroke:#333,stroke-width:2px style D fill:#bbf,stroke:#333,stroke-width:2px

解决方法是控制判别器的学习节奏,或者用 Wasserstein Loss 替代普通的 GAN Loss。

坑二:数据不匹配导致损失失真

在做迁移学习时,如果预训练数据和当前任务的数据分布差异太大,损失函数可能会"误导"模型。比如用 ImageNet 预训练模型做医学影像分类,普通的分类损失可能不如 Dice Loss 有效。

def dice_loss(y_pred, y_true, smooth=1):
    y_pred = torch.sigmoid(y_pred)
    intersection = (y_pred * y_true).sum()
    union = y_pred.sum() + y_true.sum()
    return 1 - (2 * intersection + smooth) / (union + smooth)

Dice Loss 直接优化 IoU(交并比),更适合医学影像这种对重叠区域敏感的任务。

坑三:忽略了损失的边界条件

有一次做回归任务,预测值必须是非负的,但用 MSE 时模型偶尔会预测出负数。解决方法有两个:一是在模型输出层用 ReLU 限制范围,二是修改损失函数:

def constrained_mse(y_true, y_pred):
    error = y_pred - y_true
    # 对负预测值施加额外惩罚
    penalty = torch.where(y_pred < 0, torch.abs(y_pred) * 10, 0)
    return torch.mean(error ** 2) + penalty.mean()

实践中的选择建议

实际项目中,损失函数的选择更像是一个迭代过程:

第一步:先选最简单的

如果你刚开始做一个任务,不要一开始就上各种复杂的损失函数。分类用 Cross Entropy,回归用 MSE,多任务用简单的加权和。

先让模型跑起来,看看到底哪里有问题,然后再针对性地调整。

第二步:根据问题调整

  • 如果发现模型对某些样本"不够敏感",考虑用 Focal Loss 或加权损失
  • 如果异常值太多,考虑用 MAE 或 Huber Loss
  • 如果是多任务不平衡,考虑自动调权重或梯度平衡方法
  • 如果是特殊任务(医学影像、检测、分割),考虑领域特定的损失函数

第三步:持续监控

不要只看损失曲线,要看实际的业务指标。损失值下降不代表模型变好,要同步关注准确率、F1 分数、IoU 等直接反映业务效果的指标。

结语

损失函数本质上是把"想要什么"翻译成"能优化什么"的过程。选择对了,模型会朝你想要的方向前进;选择错了,模型可能在做表面功夫——损失值下降得很漂亮,但实际问题没有解决。

但也不是所有问题都能通过调损失函数解决。有时候数据质量更好、模型架构更合适、训练策略更合理,比纠结于损失函数的细节更重要。损失函数是工具,不是终点。

最后提醒一点:实际项目中,最有效的损失函数往往不是最复杂的,而是最贴合具体业务场景的。花时间理解问题的本质,比追逐最新的损失函数名词更值钱。

版权声明: 本文首发于 指尖魔法屋-关于AI损失函数的几点记录https://blog.thinkmoon.cn/post/336-ai-loss-function-target-optimization-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!