ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从交叉熵到Focal Loss:代价函数实战避坑指南

从交叉熵到Focal Loss:代价函数实战避坑指南

调模型的人,迟早都要跟“代价函数”正面交锋。你可以把数据集准备好、把网络结构搭得再花哨,但最后告诉模型“错得多离谱”的,永远是代价函数。我记得自己第一次正经接触这个概念时,导师给了一句很朴素的定义:代价函数就是用来衡量模型预测值和真实值之间差距的函数,训练的本质就是不断缩小这个差距。后来自己带的项目多了,才发现这句话背后藏着大量细节——同样是衡量差距,选错函数,模型可能三天三夜不收敛,或者收敛到一堆完全没用的局部最优解上。

这篇文章不打算做学术式综述,我想站在实际建模的角度,把这几年在项目里反复踩过的坑、验证过有效的做法,以及各个代价函数背后的设计动机,一次讲清楚。不管你是刚入门的数据分析,还是已经在跑模型但总感觉训练不稳的从业者,应该都能从这里找到一些可落地的参考。

1. 代价函数为什么是模型优化的起点

1.1 训练的本质是一个数值优化问题

模型训练说白了是在解一道优化题:我们要找一组参数θ,让某个目标函数的值尽可能小。这个目标函数里,通常包含两项——一项是损失项,衡量模型在训练样本上的预测误差;另一项是正则项,对参数复杂度施加惩罚。两者合在一起,就是我理解的“代价函数”。

为什么非得这样搭配?我在早期做过一个极端实验:只保留损失项,去掉正则项。结果模型在训练集上确实跑到了几乎零误差,但换到新数据上立刻崩溃。原因不复杂,模型把训练数据里的噪声也当成了规律。加上了正则项,相当于给参数的取值空间划了一条边界,让模型不能太“任性”。

提示:如果你后续看论文、看开源代码,会让正则项占据较大权重导致模型欠拟合的,那不是一个成熟的配置。正常比例应当是先确保损失项能下降,再逐步增大正则权重去缓解过拟合。

1.2 代价函数直接决定了模型的收敛行为

同一个任务、同一份数据、同一个网络结构,只把代价函数从均方误差换成交叉熵,训练曲线会变得完全不一样。原因在于不同函数的梯度形态完全不同。

拿回归任务里最常用的均方误差来说,它对预测偏差大的样本会给出指数级增大的惩罚和梯度。模型在前几个Epoch会拼命去处理那些离群点,把正常的样本反而忽略了。交叉熵则不同,它天生对“极度自信的错误预测”反应激烈,但对“已经比较接近的预测”会逐步放松调整幅度,这种梯度舒缓特性让分类训练稳定得多。

我习惯在项目启动时先画一下候选代价函数的理论梯度曲线,再去选择,这个习惯帮我避开了很多无效训练时间。

1.3 代价函数与模型评估要分开看

另外要强调一个新手容易混淆的点:代价函数和评估指标不是一回事。代价函数是训练过程中的优化目标,必须可微、数值稳定;评估指标是最终业务上关心的数字,比如精确率、召回率、AUC。两者经常不一致,比如你业务上要的是低误报率,但如果直接把这个做成代价函数,它可能是不可导的,没法做梯度优化。

正确的思路是:用可导的代理函数去逼近这个业务目标,训练结束后再用真正的业务指标评估模型。关于这个错位问题,我在后面第5部分会用真实项目再展开。

2. 五种基础代价函数的应用边界:从均方误差到交叉熵

2.1 回归任务首选:L1、L2 与 Huber

先看回归任务,这是新手最早接触代价函数的场景。L2损失(均方误差)对偏差取平方,数学性质优秀,处处可导,梯度在接近零点时平滑变小。但它对离群样本的惩罚太重,一个异常值就能拖拽整个模型的方向,这就是为什么真实业务里单独用L2经常出事。

L1损失(平均绝对误差)对每个样本的惩罚是线性的,对离群点稳健得多。但它在误差为零处不可导,且梯度恒定,导致训练后期在最优值附近震荡,收敛精度打折。

我自己的经验是,如果数据比较干净、异常值可控,首选L2;一旦数据里异常值较多又来不及清洗,用Huber损失。它在误差较小时表现为L2,误差超过阈值时变为线性的L1,既保留了L2的收敛精度,又限制了离群值的破坏力。唯一代价是多了一个超参数δ,需要用小规模验证集调一下。

2.2 分类任务标配:交叉熵与它的数值配合

分类问题里,交叉熵几乎是默认选项。它衡量的是预测概率分布和真实标签分布之间的差异,配合Softmax输出层使用时,数值上表现出极好的稳定性。

为什么交叉熵在分类任务中比均方误差更合理?从梯度角度能看得很清楚。二分类交叉熵的梯度形式是预测值与真实值的差,线性而稳定;而把均方误差硬套到Softmax输出上,梯度会乘上概率梯度项,导致在模型“非常自信却预测错误”时,梯度反而极小——模型几乎没有动力去纠正错误,这在真实训练中会表现为损失长期悬停。

需要特别留意的是实现层面的数值稳定性。交叉熵公式里带有log项,当Softmax输出概率非常接近0时,直接计算log会得到负无穷,梯度就会变成NaN。工业级代码里通常会把Softmax和交叉熵融合进同一个函数,直接对logits做计算,避免中间概率出现过小值。

2.3 最大间隔思路:Hinge损失与SVM

如果你追求模型不仅分类正确、还要分类得有“余量”,可以了解Hinge损失。SVM之所以经典,一个关键因素就是它用Hinge损失来促使样本不仅被分到正确一侧,而且要离分类边界足够远。

深度学习里也能见到它的身影,某些小样本分类场景下,用Hinge配合特征提取网络效果不错。不过它对异常值同样敏感,且梯度在分类正确区域为0,会让模型在后期缺乏微调动力。一般我建议先跑交叉熵基线,再考虑Hinge。

损失函数基本公式核心优势主要劣势典型任务
L2 Loss(y - ŷ)²收敛平滑,数学友好对离群值敏感干净数据的回归
L1 Loss|y - ŷ|对离群值稳健零点不可导,后期震荡稳健回归
Huber分段兼顾稳健与收敛需调阈值δ含噪回归
Cross Entropy-Σy·log(p)梯度稳定,分类首选对标注噪声敏感多分类/二分类
Hingemax(0, 1 - y·f)支持最大间隔正确分区梯度为零SVM、少样本分类

2.4 语义分割中的特例:Dice系数的变体

还有一个有趣的变体是在图像分割领域,很多人直接用Dice Loss而不是逐个像素算交叉熵。原因是分割任务中前景和背景像素数极不均衡,交叉熵会倾向于把一切都预测为背景。Dice Loss衡量预测区域与真实区域的重叠度,对前景像素给予更高权重,但它的损失曲面非凸,用不好会震荡。

现实中更稳妥的做法是把Dice Loss与交叉熵按一定权重混合,用交叉熵保障训练的稳定收敛,用Dice Loss把优化方向拉向分割质量本身,两者互为补充。我的调参经验是先从0.5:0.5试起,再根据验证集表现调整混合比例。

3. 代价函数在真实训练中的坑:从数值稳定性到类别不平衡

3.1 log(0) 与下溢问题:一次NaN事故的复盘

有一回我在训练一个文本分类模型,跑到第300步,损失直接变成NaN。第一反应是学习率太高,调低了几轮仍复现;查数据发现某几个类别样本稀少,模型开始对它们输出接近0的概率,导致交叉熵里的log项直接下溢到负无穷。

这种情况在混合精度训练里尤其常见,因为FP16能表示的数值范围比FP32窄得多。解决办法也很机械:检查优化器是否开启了梯度裁剪,是否用了数值稳定的LogSoftmax实现,是否在损失计算前加了最小概率阈值。这个排查顺序是有讲究的,因为不同位置的下溢需要不同的修复手段。

注意:如果训练曲线下降正常但某一步突然跳出NaN,优先查数据里是否出现了极端值或空标签,第二查损失函数实现,第三才查学习率。很多人一上来就动学习率,往往是在错的方向上花时间。

3.2 类别不平衡:交叉熵失效的经典场景

分类任务里类别严重不平衡时,模型倾向于把一切预测为多数类,因为这样做整体损失最小。用一个简单例子说明:二分类负样本占99%,负样本预测对的概率是0.99,如果把正样本全部预测错,总误差也只有0.01水平,模型会觉得这个预测方案“很划算”。

应对策略需要在代价函数层面动手,我按实际效果排个序:

  • 加权交叉熵:给少数类样本的损失乘以大于1的权重,操作成本最低,是必试基线。
  • Focal Loss:在交叉熵基础上增加调制项(1-pt)γ,自动降低易分类样本的权重,迫使模型关注难分样本。γ取2时效果最好,但需要配合验证集调参。
  • 在损失里用类平衡采样:这不是直接改代价函数,而是让每个Batch中类别分布更均匀,效果和加权交叉熵互补。

3.3 训练指标和业务指标相悖:优化目标要分清主次

模型在验证集上AUC很高,但业务方反馈效果不佳,这种矛盾我见过很多次。问题往往出在优化目标和真实业务目标错位。AUC衡量的是排序质量,而不关心具体的概率校准程度。如果你用交叉熵优化,同时用AUC评估,模型可能在给出偏高或偏低的概率值但保持排序正确。

另一个典型例子是点击率预测。模型用交叉熵优化,业务方却更关心预测概率是否准确反映真实点击率。此时哪怕AUC高,概率偏差也会导致预算分配失误。解决办法是在代价函数基础上增加概率校准环节,如温度缩放、保序回归,或者直接在训练时添加校准相关的辅助损失。

4. 当教科书不够用时:如何设计适合你任务的代价函数

4.1 多目标冲突:显式的加权托底比隐式折中更可靠

实际项目里经常要同时优化几个目标,比如广告系统既要考虑点击率,又要控制单次花费,还要兼顾内容多样性。把这些目标简单相加,权重全靠拍脑袋,是一种风险很高的做法。

我一般会在项目早期花一个下午把这些目标量化出来,写成可微的代理函数,再做加权组合。加权的权重不是一次性定的,先定一个大概方向,跑小规模实验确定量级,再逐步调整。这里有个容易忽略的地方:不同损失项的量级差异是巨大的。一个交叉熵可能是0.5左右,一个L2正则可能是0.001左右,直接相加后梯度几乎被交叉熵淹没。必须先做梯度归一化或尺度均衡。

4.2 标签噪声场景:从交叉熵切换到更稳的损失

真实业务中标注数据不可能百分之百准确,而交叉熵对“干净标签”的强假设在噪声数据下会失效。模型一旦记忆了错误标签,损失就会持续下降,但泛化能力同步变差,这就是典型的“过拟合噪声”。

有限的样本下,我试验过的有效方案包括:把交叉熵换成Symmetrical Cross Entropy,用反向信息流弥补噪声影响;或者设定一个置信度阈值,把低置信度样本的损失截断掉;最朴素但稳定的是用MAE式损失做标签噪声鲁棒训练,不过收敛精度会打折。从实践投入产出比看,在数据侧做噪声过滤往往比换损失函数更直接有效。

4.3 对比学习里的InfoNCE:负样本如何塑造代价函数

如果你在跑自监督学习或对比学习模型,字符串相关项目里经常能见到InfoNCE。它本质上是让正样本对的相似度在负样本对中脱颖而出。这里有个关键细节:负样本数越多,代价函数越接近理想性质,对模型的需求也越高。

我在实验不同负样本规模时发现了明显的性能分水岭,从32提升到256时效果提升显著,再往上就平缓了。代价是显存暴涨。所以实际使用中要控制Batch内负样本数,或引入额外的类别采样策略。

4.4 误差分布视角:从正态假设到异方差加权

回归任务里,L2损失其实是假设误差服从正态分布且方差恒定。但真实数据往往不满足这个假设,比如不同价位的商品,预测误差的方差明显不同。此时单一损失函数会让模型过度关注高方差区间的样本。

一个更精细的做法是让模型同时预测均值与方差,利用负对数似然做代价函数。简单说就是在损失里引入一个方差缩放项,让不确定度大的样本天然获得较小的梯度权重。我在某个价格预测项目中把预测误差降低了约15%,代价是模型多一个输出头,训练时间略微增加。

5. 代价函数、评估指标与数据配比:我踩过的三次错位

5.1 第一次:用AUC当优化目标,结果模型原地踏步

有段时间我尝试直接用AUC的平滑近似作为代价函数,想着训练目标与评估指标一致总该没错。结果模型训练非常缓慢,损失值也不下降,原因是AUC的近似函数梯度过于平滑,在多数样本对没有排序差异时梯度接近0,模型缺乏推动力。后来换成交叉熵,再用AUC评估,反而效果更好。这说明代用代理损失本质上是在“绕路”,但绕路可以更高效地逼近目标。

5.2 第二次:类别权重配比与业务严重脱节

在某个风险识别项目中,业务方想要高召回。我按常规做法给正样本加了权重,提升了很多,但在召回提高的同时误报也涨了,业务方直接否了。问题出在我没有在代价函数里给出误报惩罚项。后来改成在损失里同时评估加权召回和误报率,并做成动态折中,模型表现才符合预期。

这段经历给我的教训是:调类别权重之前,先量化业务对漏报和误报的真实容忍度,再回去设计损失成分,而不是把召回率拉满就交差。

5.3 第三次:数据配比和代价函数一起改才见效果

有一次模型效果停滞,验证集损失平坦如线。我单独调损失函数、单独调采样权重,都没明显起色。后来我把数据采样策略和代价函数当成一个整体来改:重采样保证每个Batch类别均衡,同时配合Focal Loss聚焦难分样本,效果立刻拉开差距。这让我意识到,代价函数不是孤立组件,它和数据分布、采样策略是连在一起的一张网,单点优化强度有限。

5.4 实战中的评估方案建议

最后给一个可复制的实验配置。每次尝试新的代价函数,我会同时记录训练损失、验证损失和目标业务指标,不能只看一个数字。训练损失下降但验证损失上升,大概率过拟合;验证损失下降但业务指标不涨,代价函数和业务指标之间可能错位。这两层判断能帮你快速定位是模型问题、数据问题还是目标函数问题。

6. 一个可复现的调参实验:从损失函数曲线开始

这里提供一个简单的Python脚本框架,你可以拿来快速观察不同损失函数的行为差异。虽然画的是理论曲线,但几张图足够让你直观感受到梯度形态的差异。

import numpy as np import matplotlib.pyplot as plt def mse_loss(y_true, y_pred): return (y_true - y_pred) ** 2 def mae_loss(y_true, y_pred): return np.abs(y_true - y_pred) def huber_loss(y_true, y_pred, delta=1.0): diff = np.abs(y_true - y_pred) return np.where(diff <= delta, 0.5 * diff ** 2, delta * diff - 0.5 * delta ** 2) x = np.linspace(-3, 3, 400) for name, loss_func in [("MSE", mse_loss), ("MAE", mae_loss), ("Huber", huber_loss)]: y = loss_func(0, x) plt.plot(x, y, label=name) plt.legend() plt.xlabel("Error") plt.ylabel("Loss") plt.title("Loss Curves for Regression") plt.grid(True) plt.show()

这样一个10行不到的小图,能清楚解释为什么Huber在离群值场景下能保持稳定。多画几次、多对照几组任务后,你就和我一样会养成“先画曲线,再选函数”的习惯。

我个人在实际操作中的体会是,代价函数既没有绝对的“最优”,也没有一劳永逸的选择标准。它更像是你给模型立的一条纪律,这条纪律既要让模型知道什么叫“正确”,又要让它在面对复杂情况时能稳定前进。与其迷信某个论文里的高级损失,不如回到你自己的数据分布和业务目标上,把基础损失用扎实,再针对短板做自定义设计。那些能在公开排行榜排到前面的模型,往往不是输在损失函数的新颖上,而是能把最基础的代价函数调到恰到好处。

最后分享一个扩展方向。如果你在跑多任务学习,可以留意梯度冲突问题——不同任务的损失在反向传播时经常互相拉扯。现在有不少做法引入任务级不确定性来动态加权损失,思路类似于我前面提到的异方差加权,只是从样本级扩展到了任务级。每个项目的每一次改动,最终都指向同一个目标:让模型优化方向和真正解决问题的方向保持一致,这件事情,代价函数永远是那个源头。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进