资讯动态

深度学习损失函数怎么选?从MSE到交叉熵、Focal Loss的实战避坑指南

发布时间:2026/10/14 23:37:26 来源:尧图企业网站定制
最近帮一位开发者排查一个多分类任务的训练问题验证准确率卡在86%不再往上涨训练日志里的loss从头到尾都在5.4附近晃来晃去。我把代码翻出来一看才发现他用均方误差MSE直接去优化多分类输出。这个案例不是孤例很多刚开始接触深度学习的同学都会在损失函数这一步走歪。损失函数是深度学习中定义“模型当前错得有多离谱”的量化标准它决定了梯度回传的方向和大小也直接决定了模型最终学到什么样的决策边界。文章开头先说结论这篇文章不堆公式只讲清楚常用的损失函数为什么长这样、各自适合什么场景、实际训练里会踩到哪些坑以及我怎么从loss曲线的表现反推问题根源。适合正在入门深度学习的学生、转岗工程师以及那些一直抄别人代码但没认真想过损失函数内部逻辑的人。1. 损失函数在训练中的角色它就是给模型划线的裁判1.1 损失函数在训练循环里到底做什么随便打开一份深度学习的训练代码核心循环无非几步把一批样本喂给模型得到预测结果计算预测结果和真实标签之间的差距也就是loss反向传播求梯度优化器沿着梯度方向更新参数。这里的loss计算就是整个训练过程的“指挥棒”。这个裁判怎么判罚决定了运动员模型接下来怎么调整动作。举个最直白的例子。裁判如果只告诉你“错了”或者“对了”模型在连续参数空间里拿不到任何有用的方向。但如果裁判告诉你“这次错得离谱离谱程度是5.8那次错得轻一点程度是1.2”模型就能知道往哪个方向调整、调整幅度应该多大。损失函数的核心作用就是把“模型有多差”从一个离散的对错判断转换成一个连续可导的实数并且让这个实数的梯度方向指向让误差更小的参数区域。我自己一直有一个理解方式损失函数做的事情是“把你的任务要求翻译成梯度语言”。任务要求可能是“分类准确率高一点”“边框回归准一点”“推荐排序正一点”但优化器只懂得最小化某个可微函数。损失函数就是这个翻译器。任务目标是否被准确翻译直接决定模型最终学到的东西是否符合预期。这里还要多说一句损失函数数值本身的大小没有绝对意义。交叉熵算出来0.3回归MSE算出来0.02不同任务之间这个数字完全不能横向比较哪怕同一个任务换一种batch组织方式、换一个损失函数实现loss的绝对数值也会不一样。所以训练过程中该关注的不是“loss数值是不是足够小”而是“loss有没有持续下降”“验证指标有没有跟着变好”“loss的下降形态是否正常”。1.2 准确率这么好懂为什么不能直接拿它当优化目标初学者经常问分类任务里我直接让准确率最大化不行吗准确率计算起来多简单测试时大家也都用它评估。问题在于准确率是一个分段常数函数。参数只要让某个样本仍然分类正确准确率就不变对应的梯度就是0只有当某个样本恰好跨越分类边界时准确率才会发生跳变而这个跳变处要么导数不存在要么就是0。模型完全不知道往哪个方向走能提高准确率梯度自然就给不出任何有效信号。交叉熵这类损失函数就不一样。它通过软概率输出错得很离谱的样本给大梯度错得不严重的样本给小梯度连续地为参数提供改进信号。这就是为什么几乎所有监督学习场景下我们都在用可微的代理损失去逼近不可微的评估指标。理解了这一点你就能明白“损失函数的选择”为什么在整个训练流程里排在相当靠前的位置。2. 回归任务三件套MSE、MAE、Smooth L1的脾气与选型回归任务里损失函数本质上是在度量“预测值和真实值之间的差距量级”。最常用的是MSE均方误差、MAE平均绝对误差和Smooth L1平滑L1也叫Huber Loss。这三个函数看着差不多实际脾气差别很大。2.1 MSE惩罚平方的“老实人”MSE的定义是L (1/N) Σ (y_i - y_pred_i)^2。它天然对所有样本一视同仁按平方放大误差。预测值和真值差0.1惩罚是0.01差1.0惩罚是1.0差2.0惩罚是4.0。误差越大惩罚加得越狠模型会优先去修那些错得离谱的样本。从概率角度看如果你假设观测噪声服从高斯分布最大化高斯似然等价于最小化MSE。当数据噪声比较稳定、离群点很少时MSE是非常自然的选择。但它有一个众所周知的弱点对异常值极度敏感。数据里只要混入几个和总体趋势完全不符的点MSE的梯度会严重偏向这些离群点模型宁可牺牲大部分样本去迁就它们。我之前做过一个室温预测实验某一天传感器故障输出了一个60度的异常值MSE直接让整条拟合曲线在一个小范围内硬生生拉出一个尖峰。2.2 MAE抗异常值但收敛不省心MAE的定义是L (1/N) Σ |y_i - y_pred_i|。它的惩罚是线性的差0.1罚0.1差1.0罚1.0没有平方放大效应所以对异常值的敏感度低很多。问题也出在这里它的梯度绝对值恒为1在最佳点附近会来回振荡。具体来说当预测值和真值已经很接近时MSE的梯度是2*(y_i - y_pred_i)会随着误差变小而趋近0模型可以稳稳“落”进最优点MAE的梯度永远是±1接近最优点时依然给你一个固定大小的推力很难精准收敛。实战里单独用MAE做回归经常看到loss收敛到一定程度后开始小幅震荡很难进一步压低。还有一个容易被忽略的点MAE在0处不可导。深度学习框架通常用次梯度来处理这个位置实际训练中影响不大但从数学上看确实不是处处光滑的函数。2.3 Smooth L1实战里最省心的折中Smooth L1正是冲着MAE和MSE的痛点去的它的分段定义是当 |y - y_pred| δ 时取 0.5 * (y - y_pred)^2 / δ当 |y - y_pred| ≥ δ 时取 |y - y_pred| - 0.5 * δ小误差时按平方惩罚梯度小、收敛稳大误差时按线性惩罚梯度被限制住不会因为个别异常样本导致梯度爆炸。这也是它长期在目标检测边框回归分支里成为默认选择的原因。检测框的初始预测往往离标注框很远如果直接用MSE早期几个大误差样本会把梯度冲得非常猛训练过程极不稳定Smooth L1起到的作用就是“削峰填谷”。我自己的习惯是数据清洗得比较干净、误差分布接近高斯时用MSE数据来源杂、偶尔有脏样本时用Smooth L1纯MAE用得最少基本只在某些对异常值容忍度要求极高的场景里作为对比基线出现。2.4 一张表说清三者的差异损失函数大误差惩罚量级梯度大小对异常值敏感度典型场景MSEO(e^2)随误差增长高数据干净、误差分布对称MAEO(e)恒为±1低强异常值场景、稳健估计Smooth L1大误差O(e)小误差O(e^2)大误差时饱和中目标检测回归、含噪声回归这三个之外MAPE平均绝对百分比误差在价格预测、销售预测里也经常出现因为相对误差更符合商业直觉但真值接近0时函数会直接趋于无穷大使用时要人为设置下限保护。还有用于分位数回归的Pinball Loss适合预测区间类任务这里就不展开了。3. 分类任务为什么绕不开交叉熵3.1 分类用MSE会踩到什么坑不少新手会把MSE直接搬到分类任务里。多分类输出经过softmax之后是一个概率分布用MSE去比较两个分布看起来似乎没什么问题实际梯度形态大有问题。以二分类为例。模型输出为z经过sigmoid得到概率p σ(z)。MSE对z的梯度是dL/dz (p - y) * p * (1 - p)注意最后多乘了一个p(1-p)这是sigmoid的导数。当预测明显错误时p会趋近1或者0此时p(1-p)趋近0梯度被压没了。也就是说模型错得越离谱梯度反而越小在饱和区域里模型几乎学不动。这完全和直觉相反——该给强学习信号的地方梯度消失了。交叉熵的梯度就不一样。以Binary Cross Entropy为例L -[ y * log(p) (1-y) * log(1-p) ]对z求导的结果是dL/dz p - y预测概率和真实标签直接相减预测越离谱p和y差距越大梯度越大。这才是“错误越大学得越狠”的正确打开方式。所以分类任务里交叉熵几乎是无脑首选MSE出现在分类场景里基本可以视为bug。3.2 softmax和交叉熵的梯度为什么这么顺手多分类通常用softmax把logits转成概率分布p_i exp(z_i) / Σ_j exp(z_j)配合交叉熵L - Σ_i y_i * log(p_i)考察对logit z_k的梯度结果同样是dL/dz_k p_k - y_k在真实类别k上梯度是p_k - 1为负值说明要增大该类的logit在其他类别上梯度是p_j为正值说明要压低这些类的logit。这个梯度表达非常直观每一类该往哪个方向推一目了然debug的时候甚至能直接拿来分析。Softmax和交叉熵在数学上是“天生一对”信息论角度来说它等价于预测分布和真实分布之间的KL散度。为什么梯度可以这么简洁因为softmax的雅可比矩阵有特殊的结构交叉熵的log求导恰好把softmax分母上的求和项抵消掉了。这个抵消不是巧合是两者配合之后自然产生的结果。实际工作中不需要每次都手动推导但理解这个原理对你后续自己实现自定义损失函数会有很大帮助。3.3 数值稳定实现别手动把softmax喂给loss写代码时最常见的坑是手动做softmax再把概率送进对数损失。假如某个样本的第k类logit特别大softmax算出来的概率可能直接变成0.0浮点下溢然后交给log就成了log(0) -infloss直接变成NaN整个训练直接报废。正确做法是利用框架里现成的融合算子。PyTorch里就是CrossEntropyLoss内部先算log_softmax再和标签做负对数似然。log_softmax的实现会做减去最大值、logsumexp等数值稳定技巧等价于log(p_i)但不会真的先算概率再取对数。二分类则用BCEWithLogitsLoss把sigmoid和BCE合成一步。这也是为什么总有人提醒“别在loss里手动加sigmoid或softmax”这不是玄学是实打实的数值稳定性问题。我自己做模型时的默认写法是模型最后一层直接输出logits不加激活函数loss用框架自带的CrossEntropyLoss或BCEWithLogitsLoss推理阶段再对logits手动做softmax得到概率。这套流程几乎不会在数值层面翻车。4. 类别不平衡与难易样本加权、Focal Loss和标签平滑真实业务场景里很少会遇到完美均衡的数据集。二分类中正样本可能只占1%多分类中头部类别可能占掉90%。这种情况下普通交叉熵会被多数类主导模型很容易学会“全都预测成多数类”——loss会显得很低但对你真正关心的少数类没有任何分辨能力。4.1 最简单的加权交叉熵最朴素的做法是给每个类别一个权重。比如正样本占比1%、负样本占比99%在BCE里把正样本这一项乘以99负样本保持1相当于强制模型重视少数类。PyTorch的BCEWithLogitsLoss自带pos_weight参数多分类的CrossEntropyLoss也支持weight参数操作起来非常方便。加权交叉熵简单直接适合类别比例相对固定、不涉及难易样本区分的场景。但有个问题如果少数类里既有很好辨别的样本又有特别难辨别的样本加权交叉熵会把所有少数类样本统一放大易样本和难样本获得同样的权重。那些已经学得很好的易样本还在持续贡献loss反而挤占了难样本的学习空间。4.2 Focal Loss如何压制易样本Focal Loss最早在目标检测里为了解决“绝大多数候选框都是容易背景”的问题而提出后来被广泛借鉴到各类不平衡任务中。定义是FL(p_t) - α * (1 - p_t)^γ * log(p_t)其中p_t是真实类别对应的预测概率γ是聚焦参数α是类别权重。核心思路是用(1-p_t)^γ作为调制因子当某个样本已经被预测得很好时p_t接近1(1-p_t)^γ趋近0该样本的loss被压低当样本预测很差时p_t很小调制因子接近1loss被完整保留。训练重心自然转向难样本和没学好的样本。拿数字举个例子。某个易样本的真实类概率是0.9普通交叉熵的loss是0.105某个难样本的真实类概率是0.5普通交叉熵的loss是0.693两者只差约6.6倍。加γ2的调制后易样本的loss变成0.105 × 0.01 0.00105难样本变成0.693 × 0.25 0.173差距拉大到165倍。这个对比非常直观。调参这块说点我自己的经验γ从2.0开始试α在二分类场景里可以取0.25正样本少时倾向给正样本更高权重。γ太大会让所有样本的loss都变得很小模型容易欠拟合α和γ是耦合的实际使用中建议固定一个、调另一个。Focal Loss在调参过程中有不少反直觉现象比如数据量充足且训练充分时γ1的效果可能比γ2更好所以千万别拿论文里的默认值直接抄要按自己数据的难度分布来试。4.3 标签平滑给模型留一点怀疑空间交叉熵的优化目标会把真实类别的概率挤向1、其他类别概率挤向0这会鼓励模型输出非常大且极端的logits长期来看容易过拟合对含噪声的数据尤其不友好。标签平滑的做法是修改训练时用的“目标标签”把one-hot里的1换成1-ε把0换成ε/(K-1)K是类别数。PyTorch的CrossEntropyLoss自带label_smoothing参数一般设置0.1。这样模型不会被逼迫成完全自信的预测logits不会发散到很大泛化能力通常更好。这套机制的理解在我看来是给训练过程加了一层“别太极端”的正则化。有人会问平滑之后的标签还是正确标签吗它不再是严格one-hot了但它是在用软标签告诉你“这个样本大概率属于这一类但边缘情况也不是完全不可能”。数据质量不确定的任务我基本都会顺手开0.1的标签平滑。分割任务里还常见Dice Loss或者结合BCE的混合损失本质是处理类别像素比例失衡NLP文本分类也常用Focal Loss处理长尾类别思路都是通用的这里就不展开细说了。5. Loss曲线之外的排错经验5.1 Loss变成NaN时的排查链路Loss跑到NaN我在项目里遇到过几种原因按排查顺序列一下。第一数值问题手动softmax导致log(0)、自定义loss出现除零、sqrt里出现负数。第二学习率过大梯度在更新过程中撞上了数值溢出。第三数据问题标签里有NaN、特征里有inf、归一化做错导致输入极其巨大。第四模型内部存在除零点或者使用了不稳定的exp运算。排查的时候我一般先把学习率降到原来的十分之一跑几个step。如果不再NaN大概率是学习率或梯度爆炸配合梯度裁剪可以稳住。如果降学习率之后还是NaN就去检查输入batch里有没有异常值用一个简单的断言检查数据和标签。最后再回到loss函数本身逐个检查有没有手动实现的地方。这里最值得强调的还是那句话能直接用框架的标准loss就不要自己造轮子。5.2 Loss不降要么优化坏了要么目标错了loss不降不一定就是损失函数的问题。学习率太大会震荡或发散太小则下降缓慢batch size太小loss曲线会非常毛糙权重初始化不当某些层输出饱和梯度根本传不回去。排错时先看训练曲线形态如果loss在前几十个step断崖式下降之后长期横盘可能是学习率进入了平台期或者任务本身难度就是这样如果loss从一开始就纹丝不动那多半是优化根本动不了优先查梯度消失或者梯度爆炸。还有一种隐蔽情况就是优化目标和评估指标不一致。文章开头那个案例正是如此多分类任务用MSEsoftmax概率被当成回归目标来拟合模型确实在“减少平方误差”但它和“让正确类概率最大”不是同一个方向所以分类准确率上不去。这种属于典型的优化目标错位。对比训练loss和验证指标能明显发现loss在正常下降指标却长时间原地踏步。发现这种情况果断换成交叉熵基本一两个epoch就能看到指标起飞。5.3 自定义损失函数时的三个坏习惯我见过不少同学喜欢把损失函数写成自定义形式觉得这样“灵活”。自定义本身没问题但有三个坏习惯非常常见。第一个是不注意维度。模型输出是[B, C]或者[B, T, C]标签形状没对齐最后直接对整个矩阵做mean压成一个标量梯度方向全乱了。建议在每一步操作前打印一下shape确认是在计算per-sample loss再做最终的reduction。第二个是把不该求梯度的操作包进去。比如在loss里做detach、做索引赋值导致某些分支没有梯度模型学一段时间之后发现一半参数完全不动。这类bug很隐蔽因为loss数值看起来正常但部分网络已经停止更新。第三个是忽略数值稳定性。自己手写log、exp、除法的组合没有做溢出保护一旦中间结果跑到正负无穷整个训练就崩了。自定义损失的检查方式也有小技巧用一个小batch跑一步看看loss是否符合合理范围再用梯度检查或者直接比较反向传播前后的参数变化量确认所有需要更新的模块都收到了梯度。说得更具体一点训练脚本里加一段代码对每个参数打印梯度范数如果发现某块参数一直是0十有八九就是loss没有正确连到它。最后分享一个我养成的习惯损失函数最开始一定要选主流、经过验证的方案所有花活都等基线跑通之后再上。基线模型用标准交叉熵或Smooth L1先让训练曲线符合直觉再逐项换新的损失函数做对比。这样即使某一项实验效果不理想你也知道是损失函数换错了而不是整个训练流程出了毛病。损失函数决定优化目标优化目标决定模型行为这笔账在动手之前算清楚能省下大半排错时间。

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

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

免费获取报价 →
↑