资讯动态

深度学习损失函数选型实战:从MSE到Focal Loss的原理与踩坑

发布时间:2026/10/14 6:38:24 来源:尧图企业网站定制
损失函数这玩意儿说起来是深度学习里最基础的组件之一但恰恰是很多同学在实战里翻车最频繁的地方。我带项目的时候经常看到有人模型结构抄得挺像样学习率调度也配了一堆可训练出来的结果就是不对Loss 曲线跟心电图似的最后查来查去问题往往出在损失函数用错了回归任务拿交叉熵硬套分类任务用 MSE 去拟合概率或者多任务里几个 Loss 互踩权重配了一个月也没配明白。这篇就把我在实际项目中反复用到的损失函数系统整理一遍从回归到分类到对比学习讲清楚每个 Loss 的原理、适用场景、梯度特性和踩坑经验适合刚入门的同学也适合那些训练总是不收敛、想回头排查基础问题的读者。1. 损失函数在整个训练流程里的位置以及选型的核心逻辑1.1 损失函数到底在干什么很多人把损失函数简单理解成衡量预测和真实值差距的公式这个说法没错但不够。损失函数在训练流程里承担的角色远不止打分——它是整个反向传播梯度的源头。神经网络更新参数靠的是梯度而梯度是从损失函数对参数的偏导一路链式传回来的。换句话说损失函数不仅决定了模型学得好不好还决定了模型学得动不动。我习惯用一个类比来解释这件事损失函数就像开车时的导航路线终点。终点设错了你再怎么踩油门、调方向盘都没用只会离目标越来越远终点设对了但路线设计得不好比如非要走一条坑坑洼洼的小路那车跑起来又慢又颠。对应到模型上终点设错意味着损失函数衡量的目标和你的业务指标不一致模型收敛得再好业务上也是废的路线不好意味着损失函数的曲面性质很差梯度消失、梯度爆炸、局部极小值扎堆训练根本进行不下去。所以在选损失函数之前先想清楚两件事第一你这个任务的最终评估指标是什么第二这个损失函数在数学上是否便于优化。这两点经常被忽略结果就是一堆人拿着不匹配的损失函数在那里疯狂调超参白费力气。1.2 损失函数选型的基本逻辑选损失函数有一个基本框架我在实际项目里都是先按任务类型走一遍这个框架回归任务预测值是连续数值核心看误差的数值大小常用 MSE、MAE、Huber Loss以及它们的变体。二分类任务输出是 0/1 概率核心看预测概率分布和真实标签的差异常用 Binary Cross Entropy样本不均衡时加 Focal Loss 或者加权。多分类任务输出是多个类别的概率分布常用 Categorical Cross Entropy配合 Softmax 使用类别不均衡时考虑 Label Smoothing 或 Focal Loss 的变体。排序/检索任务关注的是样本之间的相对关系常用 Margin Ranking Loss、Contrastive Loss、Triplet Loss。生成任务涉及分布匹配常用 KL 散度、以及 GAN 里那一套基于判别器的损失扩散模型里还有简化后的噪声预测损失。目标检测任务分类分支用交叉熵回归分支用 Smooth L1 或 IoU 系列的损失。框架记住之后还要补一个底层意识损失函数是分梯度友好度的。有的损失函数虽然衡量误差很准但梯度性质极差训练不稳有的损失函数虽然衡量得不那么精确但梯度非常干净训练飞快。这两者之间的权衡就是损失函数这门学问最核心的部分。1.3 损失函数与评估指标的关系有一个高频问题为什么我用了正确的损失函数训练 Loss 很低但线上评估指标却不好看答案往往是损失函数和评估指标不一致。比如排序任务你关心的是 AUC 或者 NDCG这俩都是关于顺序的指标但如果你用 MSE 去拟合评分值可能评分回归得很准顺序反而错得离谱。再比如目标检测里的框回归业务上关心的是 IoU 达到某个阈值以上算正样本但如果损失函数只算 L1 距离两个框的中心点偏了和尺度差了在损失上可能等效IoU 却完全不同。所以我的建议是要么把损失函数设计成指标的代理surrogate要么在损失函数里叠加一个和指标更对齐的辅助项。交叉熵本质上就是 0/1 分类错误的平滑代理这就是它成为分类任务默认选择的原因。理解了这层关系你才能在遇到Loss 掉得挺好、指标不动的时候快速意识到问题出在匹配度上而不是傻乎乎地继续调学习率。2. 回归任务的损失函数MSE、MAE 与 Huber 的三方博弈2.1 MSE最常用的万金油但别把它当万能药MSE均方误差公式是 L (1/n) Σ(y_i - ŷ_i)²。它衡量的是预测值和真实值差值的平方平均。这个损失函数在深度学习中出场率极高几乎所有入门教程的第一个回归例子都用它。为什么大家都爱用因为它的数学性质太好了。首先它是处处可微的梯度计算干净其次它对误差的惩罚是平方级的误差越大梯度越大模型会优先修正那些偏差特别大的样本这听起来很合理。而且在高斯噪声假设下MSE 对应最大似然估计统计上是最优的。但它的致命缺点也同样来自这个平方惩罚对离群点outlier极度敏感。如果你的训练数据里有几个标注错误、或者本身就带重尾分布的噪声MSE 会把大量模型的容量花在硬拟合这几个异常点上导致正常样本反而学不好。我在处理传感器时序数据时经常遇到这种情况一个跳变的噪声点就能把整个拟合曲线拉歪。从梯度角度看MSE 的梯度是 2(y - ŷ)也就是说梯度大小和残差成正比。这带来一个实际问题如果初始预测值和真实值差得很远比如差 10 和差 20 的样本后者梯度是前者的两倍模型会疯狂地往差得最远的样本方向更新。如果你确定数据质量极高、噪声很小MSE 是好的但只要你怀疑数据里有脏点就要考虑其他选择。2.2 MAE抗噪更强却隐藏着优化的大坑MAE平均绝对误差L (1/n) Σ|y - ŷ|。它对误差的惩罚是线性的一个离群点只会产生和它残差同等量级的贡献不会像 MSE 那样被平方放大。所以 MAE 对离群点的鲁棒性明显更好。但 MAE 的梯度是常数等于 sign(y - ŷ)。这意味着不管预测离真实值多远每次更新的步长都一样。问题来了当预测值非常接近真实值时梯度依然是 ±1模型很难精确收敛会在最优解附近来回震荡最终结果往往比 MSE 差那么一点精度。另一个麻烦是 y - ŷ 0 这一点不可导虽然实际训练中极少恰好等于 0但理论上的不光滑性确实存在。所以 MAE 适合的场景很明确数据噪声大、离群点多且你对最终精度要求不是极度苛刻。比如我在做某些工业设备的状态预测时数据里经常混入传感器毛刺用 MAE 明显比 MSE 稳得多。但如果你需要毫米级的拟合精度MAE 可能让你在收敛末期非常难受。2.3 Huber Loss把两者的优点合并代价是多了个超参数Huber Loss也叫 Smooth L1是 MSE 和 MAE 的折中它的核心是一个分段函数当误差小于阈值 δ 时用二次函数像 MSE当误差大于 δ 时用线性函数像 MAE。公式长这样若 |y - ŷ| ≤ δ: L 0.5 * (y - ŷ)² / δ 若 |y - ŷ| δ: L |y - ŷ| - 0.5 * δ注不同框架里 δ 的写法略有差异PyTorch 的 SmoothL1Loss 默认没有除以 δFocal 风格实现里则有使用前看一眼文档这个设计的精妙之处在于当误差小的时候它像 MSE 一样给一个递减的梯度保证收敛精度当误差大的时候它像 MAE 一样梯度饱和在一个固定值避免离群点把梯度带飞。我在实践中非常喜欢用 Huber尤其是数据质量不可控、又想保持一定精度的时候。但代价也是实实在在的你需要调 δ 这个超参数。δ 太小它退化成接近 MAE收敛精度下降δ 太大它退化成接近 MSE抗噪能力下降。我的经验是 δ 取 1.0 作为一个不错的起点然后根据实际残差的量级调整。如果残差普遍在 0.1 级别δ 设 1.0 就太大了要缩小到 0.1 左右才有效果。2.4 实际回归项目里的损失函数选择建议根据我做过的回归项目给出一个经验性的选择表场景推荐损失理由数据干净、噪声接近高斯MSE数学性质好收敛精度高数据离群点多、以稳定为主MAE鲁棒性强不硬拟合异常离群点存在但不想放弃精度Huber / Smooth L1折中方案最省心预测目标跨数量级先 log 变换再配 MSE直接算 MSE 会被大值样本主导需要同时看相对误差MAPE 或自定义加权绝对误差在目标很大时没有意义这里多说一句 log 变换。很多初学者在目标值跨几个数量级时比如房价、流量、功率等直接上 MSE结果大值样本完全主导梯度小值样本几乎学不动。把目标值先取 log 再算 MSE本质是把相对误差变成绝对误差来优化是我非常推荐的一个操作。3. 分类任务的损失函数交叉熵家族与它们的应用场景3.1 为什么分类任务不用 MSE而用交叉熵这是面试和自学里最容易碰到的一个问题也是理解损失函数本质的绝佳案例。假设你做一个二分类模型输出经过 Sigmoid 得到一个 0~1 的概率 p真实标签是 y ∈ {0, 1}。如果拿 MSE 去拟合损失是 (p - y)²。看起来也没毛病但问题出在梯度上。对 Sigmoid 函数求导结果是 p(1-p)当 p 接近 0 或 1 时这个导数趋近于 0。如果模型预测得非常离谱比如真实是 1预测是 0.01按照直觉我们希望梯度能很大、狠狠修正一把但实际上由于 Sigmoid 的饱和区导数趋近于零梯度被压得非常小模型反而学不动。这就是所谓的梯度消失在分类问题中的体现。你可能会想那我不用 Sigmoid 用别的激活函数但只要输出被约束在 0~1 区间这类饱和问题就很难彻底绕开。交叉熵的梯度性质则完全不一样。二分类交叉熵公式是 L -[y log(p) (1-y) log(1-p)]。对它求导之后梯度恰好等于 p - y一个干净利落的线性梯度预测离真实越远梯度越大越接近梯度越小。完美贴合错误越大改得越多的直觉。这就是交叉熵取代 MSE 成为分类默认损失的根本原因不是因为它更像概率而是因为它的梯度性质让优化变得顺畅。3.2 二分类与多分类的代码细节PyTorch 里最常踩坑的地方在于二分类和多分类的损失函数写法是有陷阱的尤其是BCEWithLogitsLoss和CrossEntropyLoss这类吃 logits的损失。二分类推荐用BCEWithLogitsLoss它内部把 Sigmoid 和交叉熵融合在一起计算比先过 Sigmoid 再用BCELoss数值上更稳定。多分类则用CrossEntropyLoss它内部做了 Softmax 和 Log 的融合同样比用F.softmax再接NLLLoss更稳。import torch import torch.nn as nn # 二分类 logits torch.randn(64, 1) # 注意输出维度是 (B, 1)不是 (B,) labels torch.randint(0, 2, (64, 1)).float() loss_fn nn.BCEWithLogitsLoss() loss loss_fn(logits, labels) # 多分类 logits torch.randn(64, 10) # 10 个类别 labels torch.randint(0, 10, (64,)) loss_fn nn.CrossEntropyLoss() loss loss_fn(logits, labels)这里有个新手必踩的坑二分类输出维度。很多人习惯把二分类网络最后一层输出设为 1然后用BCEWithLogitsLoss这没问题但也有不少人初始化标签时用了torch.randint(0, 2, (64,))形状是(64,)和(64, 1)的 logits 对不上报错又看不出所以然其实就是维度问题。还有一种写法是要求 logits 的形状和 labels 完全一致所以要么都拉成(B, 1)要么用nn.BCEWithLogitsLoss()之前调用.view(-1, 1)。数值稳定性方面也值得多说一句不要在损失函数之前手动做log操作。如果你自己算torch.log(p)而 p 恰好因为数值误差变成 0log(0) 直接产出inf反向传播直接崩掉。所有成熟框架里的交叉熵实现都会在内部做 clamp 或者用 log-sum-exp 技巧来避免这个问题。所以我的建议是能用框架自带损失就用自带损失不要自己组合除非你完全清楚数值边界在哪。3.3 Focal Loss处理类别极度不均衡的利器交叉熵有一个隐含假设所有样本对损失的贡献是等权的。这在类别均衡的数据集上没什么问题但现实任务中正负样本比例经常悬殊到离谱比如目标检测里一张图有几百上千个背景框只有几个前景框比如广告点击率预测里正样本可能只有千分之一的比例。这种情况下大量易分类的负样本会把总损失占满模型训练出来的效果就是啥都预测成负样本精确率看着还行召回率惨不忍赌。Focal Loss 就是在交叉熵基础上做的改进公式看起来也不复杂FL -α (1 - p_t)^γ log(p_t)其中 p_t 是模型对正确类别的预测概率。关键在 (1 - p_t)^γ 这个调制因子如果样本已经被分类得很好p_t 接近 1调制因子接近 0损失被大幅降低如果样本分得很差p_t 很小调制因子接近 1损失基本不变。γ 控制调制强度常用 2.0。α 是类别权重用来进一步调整正负样本的比例。我的项目经验是γ 设 2、α 设 0.25 是一个在很多场景下都能用的默认值。但要注意Focal Loss 不是银弹它会把模型注意力从简单样本转向难样本如果数据里本身噪声很多、难样本其实是错标样本那 Focal Loss 会让模型硬学噪声效果反而更差。用之前先想清楚你的难样本是真的难还是错标了。3.4 标签平滑一个成本极低收益稳定的技巧标签平滑Label Smoothing解决的问题和硬标签有关。交叉熵在拟合硬标签时模型会被逼着把正确类别的概率推向 1这会导致两个问题一是过拟合模型对训练集的预测过于自信泛化变差二是由于 Softmax 的性质模型为了把正确类别的 logit 拉高会把其他类别的 logit 压得极低这会让模型对噪声标签的容忍度很差。标签平滑的做法很简单把 one-hot 的硬标签改成软标签正确类别是 1 - ε其余类别均分 ε/(K-1)。你可以手动做def smooth_one_hot(labels, classes, epsilon0.1): n labels.size(0) smoothed torch.full((n, classes), epsilon / (classes - 1)) smoothed.scatter_(1, labels.unsqueeze(1), 1 - epsilon) return smoothed也可以直接用 PyTorch 新版CrossEntropyLoss里自带的label_smoothing参数我一般直接设 0.1 就完事。这个操作成本几乎为零但对分类模型的泛化能力提升在很多任务里是肉眼可见的尤其是数据量不大或者类别之间有相似性的场景。做图像分类时我用标签平滑很频繁它等效于告诉模型类别之间不是非黑即白反而让特征表达更鲁棒。4. 进阶场景对比损失、KL 散度与自定义损失4.1 对比损失和三元组损失让相似样本靠近、不同样本远离很多任务的目标不是输出一个值而是让表示空间具有语义结构最典型的就是人脸识别、图像检索、推荐系统里的 embedding 学习。这类任务里交叉熵固然可以用但它只关心最终分类对不对不关心样本在表示空间里的相对距离。于是有了对比损失Contrastive Loss和三元组损失Triplet Loss。对比损失的核心思想是给定一对样本如果它们属于同一类则最小化它们在嵌入空间的距离如果属于不同类则让距离至少大于一个边距margin。公式是L y * d² (1 - y) * max(margin - d, 0)²这里面 d 是两个样本 embedding 的欧氏距离y1 表示同类y0 表示异类。注意异类的损失只在距离小于 margin 的时候才产生也就是说只要掰开超过 margin 就不管了没必要让所有异类都无限远这为优化留了余地。三元组损失更进一步它每次取三个样本锚点anchor、正样本positive、负样本negative目标是让锚点和正样本的距离加上 margin 之后仍然小于锚点和负样本的距离L max(d(a, p) - d(a, n) margin, 0)这个损失只关心相对顺序不关心绝对距离所以它对 embedding 的尺度不敏感训练起来比较稳。但三元组损失有个大坑三元组的采样方式直接影响训练效果。随机采样的话大部分三元组都是容易三元组正负样本差距太大损失为 0梯度为 0模型根本不学。所以要往难样本挖掘的方向走选那些负样本和正样本距离接近的三元组来训。实战里我一般先随机采样训练几个 epoch再用半难样本挖掘semi-hard mining策略效果比纯随机好非常多。4.2 KL 散度衡量两个分布的距离但不是对称的KL 散度在深度学习中大量出现尤其是生成模型VAE、知识蒸馏、多模态对齐这些场景。它的公式是KL(P || Q) Σ P(x) * log(P(x) / Q(x))直观理解是如果用 Q 来编码来自 P 的样本平均需要多花多少信息量。多说一次KL 散度不是对称的KL(P||Q) ≠ KL(Q||P)。这个不对称性在实践中有非常具体的表现。举一个我踩过的例子知识蒸馏时学生模型学习老师模型的输出分布。如果你用了 KL(学生 || 老师)也就是把学生当 P老师当 Q那损失会惩罚学生在老师概率很低的地方给出高概率的行为相当于强制学生不要输出老师没说过的东西。这个方向通常会让学生模型变得保守欠拟合老师的知识反过来 KL(老师 || 学生)惩罚的是老师概率很高的地方学生概率低这会逼着学生去覆盖老师的重点输出。我在做蒸馏时两种方向都试过效果差异很明显选错了方向蒸馏后模型精度掉得厉害。现在框架里一般直接提供KLDivLoss但用的时候务必要搞清楚参数里input和target谁该是 log 域上的那个这个细节我后面在问题排查里细说。4.3 自定义损失函数的正确姿势很多场景下现成的损失函数解决不了问题你需要写自己的损失函数。比如我的一个项目里需要同时惩罚预测误差和预测的变化率让输出曲线更平滑这时候就得自定义。在 PyTorch 里自定义损失的标准做法是继承nn.Module或者干脆写一个普通函数。核心要注意两点第一所有的运算必须走 PyTorch 的张量操作保证梯度能正确回传第二要小心数值稳定性尤其是涉及log、exp、sqrt这些操作的时候。class SmoothnessAwareLoss(nn.Module): def __init__(self, alpha0.5): super().__init__() self.alpha alpha def forward(self, pred, target): # 主损失Huber base_loss nn.functional.smooth_l1_loss(pred, target) # 辅助损失预测值的一阶差分鼓励平滑 diff_pred pred[1:] - pred[:-1] diff_target target[1:] - target[:-1] smooth_loss nn.functional.mse_loss(diff_pred, diff_target) return base_loss self.alpha * smooth_loss写自定义损失时我有个习惯先把主损失和辅助损失分开打印看看它们在训练过程中的量级比例。如果辅助损失比主损失大十倍alpha设 0.5 等于没设主损失完全被淹没了反之如果辅助损失比主损失小三个数量级那这个辅助项等于摆设。量级对齐是自定义多损失组合里最容易忽略、也最影响效果的点。4.4 多任务损失别再用拍脑袋的固定权重多任务学习里最常见的问题就是多个损失函数加在一起权重怎么配。很多人直接loss loss_a loss_b或者拍脑袋设0.5 * loss_a loss_b然后训练出来发现某个任务完全没学好。原因往往是两个任务的损失量级差得很远比如一个任务损失在 0.01 级别另一个在 10 级别简单的加法让后者统治了梯度。我在实际项目里一般用两种方式来配权重第一种是量级归一化。训练前先单独跑几十个 step记录每个任务损失的均值然后用倒数作为权重让各个任务初始对梯度的贡献大致相等。这个方法虽然粗糙但非常好用能快速让多任务训练进入一个正常轨道。第二种是不确定性加权。这是我从实际使用中非常推崇的一种做法让模型自己学习每个任务损失的不确定性噪声参数作为权重的分母。不确定性大的任务自动降低权重。实现上是在每个任务损失前面乘一个可学习的1/(2σ²)并加上log σ作为正则项。实践中我发现这个方案比手动调权重稳定得多几乎不需要额外超参。5. 常见问题与排查技巧实录5.1 损失完全不下降怎么办这是群里被问烂了的问题但多数人没意识到损失不下降的第一步排查应该是把损失函数本身拿出来验证。我的习惯是先用极小的数据集比如 16 个样本做一步过拟合测试如果模型连这 16 个样本都拟合不了那就不是损失函数的问题而是网络、学习率、数据预处理的问题如果过拟合测试通过了但全量数据上损失还是不动这时候才考虑损失函数选型是否合适。这轮排查里有一个很隐蔽的坑回传梯度被detach切断。比如自定义损失里某个张量操作不小心带了.detach()或者从 numpy 转过来时没有设置requires_grad梯度链就断了。用tensor.grad在反向传播后检查一下参数是否有梯度能很快定位。5.2 训练过程中损失变成 NaN 或 infNaN 问题的头号嫌疑是数值不稳定最常见的就是log(0)。如果你用了自己实现的交叉熵预防手段是给 log 里的概率加一个极小值1e-7或者直接用框架自带的LogSoftmaxNLLLoss/CrossEntropyLoss。第二个嫌疑是学习率过大导致梯度爆炸。这跟损失函数也有关系——如果你用 MSE 而且预测值非常大比如目标值在千量级初始损失可能上千梯度巨大直接一波带走。这种场景我一般建议先对目标值做标准化或 log 变换再配 Huber 这类梯度有上界的损失能明显减少爆炸概率。排查 NaN 时的定位方法在训练循环里逐层打印梯度找到第一个出现 NaN 的层或者把损失计算的每一步拆开在张量上检查是否有inf/NaN用torch.isnan快速定位。5.3 损失震荡不收敛或者收敛后指标不对损失震荡有很多原因但和损失函数直接相关的有两种。第一种是 Loss 曲面本身很崎岖比如 MAE 在零点不光滑或者三元组损失里 hard mining 过于激进导致梯度方向频繁变化。处理办法是降低学习率、加大 batch size、给梯度加 clip或者换一个更平滑的损失如 Huber 替代 MAE。第二种是标签有问题错标或噪声标签会让模型来回横跳。这时候可以考虑用标签平滑、或者在损失上对高损失样本做截断比如设定阈值超过这个阈值的样本损失直接忽略防止模型被脏标签带走。收敛后指标不对这个问题我在前面也提过核心是检查损失函数和评估指标是否对齐。跑通的工具人式排查法是在验证集上同时打印 loss 和你真正关心的指标accuracy、IoU、AUC观察它们的趋势是否同步。如果 loss 一直掉但指标不动十有八九是损失函数没有对准指标这时候要重新设计损失而不是继续加大训练轮数。5.4 数值稳定性相关的一个小技巧logits 域操作最后分享一个我习惯性的操作规范所有涉及概率的损失尽量在 logits 域计算不要先 Softmax 得到概率再取 log。理由很简单Softmax 内部做了归一化概率会被压缩到 0~1取 log 后你可能得到很大的负数再经过反向传播数值误差被放大。框架自带的CrossEntropyLoss、BCEWithLogitsLoss、KLDivLoss都是针对 logits 或者 log 概率做了融合优化的直接调用它们数值稳定性和精度都远好于自己拼装。我见过太多人为了看得懂而自己写softmax torch.log loss结果训练到一半出现 NaN百思不得其解。用框架融合实现不是偷懒是让数值底层去处理那些你容易忽略的边界情况。5.5 损失函数排查速查表现象首要怀疑方向排查动作损失不降学习率/梯度链断裂小数据过拟合测试检查梯度损失 NaNlog 操作/梯度爆炸换框架自带损失加 grad clip损失震荡损失不平滑/标签有噪声换 Huber调大 batch检查标签损失下降但指标不动损失与指标不对齐同时监控 loss 和指标趋势重新选损失多任务一个不学权重量级不匹配做量级归一化或不确定性加权正负样本失衡导致全预测负样本交叉熵默认均权改用 Focal Loss 或类别加权训练集过拟合严重硬标签过于自信加标签平滑再用 Dropout 等正则化手段做深度学习这几年我在损失函数上花的时间远比想象中多。一开始总觉得损失函数就是套公式后来才发现它才是模型训练的方向盘——同一个网络结构配上不同的损失函数训练出来的表示空间可以天差地别。我个人现在的习惯是不管任务多急动手训练之前先把损失函数在 16 个样本上跑通打印一次它的数值和梯度分布确认它符合预期再上全量数据。这个看似多花十分钟的步骤帮我避开了无数次训练三五天发现白跑的惨剧。如果你现在也被训练不收敛或者收敛了指标不对折腾不妨回到损失函数这里重新审视一遍多数时候答案就在其中。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑