关于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 的核心思想是:把注意力放在"难分"的样本上,而不是被大量"容易分"的样本淹没。

从图里可以看出来:当 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 线性增长(对所有误差一视同仁),而 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 生成模型,生成器的损失值一直在下降,但生成的图片质量越来越差。后来才发现:判别器训练得太快,生成器一直在"骗"判别器,而不是真正在生成好的样本。
解决方法是控制判别器的学习节奏,或者用 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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。