损失函数这东西,刚入坑深度学习的时候总觉得它就是“算个误差”,选哪个都差不多。可真到调模型的时候才发现,损失函数选得好不好,直接决定你能不能收敛、收敛到什么精度、甚至在类别不均衡和数据有噪声的时候能不能稳住。我踩过不少坑之后,现在基本把它当成整个训练流程里最先要拍板的一件事。这篇就把我常用的损失函数按场景梳理一遍,从原理到选型,再到实际训练里的那些细节,一次说清楚。
这里主要适合刚学完神经网络基础、准备开始做分类和回归任务的人,也适合已经跑通了一些模型、但觉得loss曲线总是不对劲、想回头补补底层逻辑的同学。损失函数本身不难,难点在于知道什么场景用它、为什么用它,以及它失效的时候到底是哪一环出了问题。
1. 先搞清楚损失函数在训练里的真实位置
1.1 损失函数不是“算个差”那么简单
很多人把损失函数理解成“预测值和真实值差多少”,这个说法对但也不全对。差多少只是表面,真正重要的是这个“差”要变成一个可以求导的、处处光滑的数值,然后反向传播才能算出梯度,梯度才能告诉模型参数往哪个方向更新。
所以损失函数要满足两个硬性条件:一是能反映模型预测好坏,二是可导。严格来说还得是处处可导或者至少几乎处处可导,不然反向传播到某个点直接断掉,梯度就传不下去了。
另一个很多人容易忽略的点:损失函数本质上是给优化问题定义了“什么是好”。同样的任务、同样的模型结构,换一个损失函数,模型学到的特征分布完全不同。用MSE做分类不是不能跑,但你会看到收敛慢、边界模糊、概率输出还很奇怪;用交叉熵做回归,模型根本不知道怎么优化,因为它的梯度逻辑跟回归目标不匹配。这不是模型的问题,是损失函数和目标任务的匹配问题。
1.2 训练过程中损失函数如何影响梯度
顺着损失函数往回传梯度,每一层的参数更新量其实是损失对那一层输入的导数。这里有个关键逻辑:不同的损失函数,梯度的大小和方向特征是完全不同的。
拿分类任务举例。softmax交叉熵的梯度形式是“预测概率减去真实one-hot向量”,这个形式有个好处,预测越错梯度越大,预测越对梯度越接近零,整个更新过程是自适应的。而MSE配合sigmoid用在分类上,梯度里会多乘一个sigmoid的导数项,sigmoid在两端饱和区导数几乎为零,于是模型一上来就陷在“预测很错但梯度很小”的困境里。
这其实解释了为什么某些损失函数组合在理论上看似乎可行、一跑就废。所以后面讲每个函数的时候,我都会把梯度特征也一起讲清楚,这才是选型的真正依据。
2. 分类任务里绕不开的交叉熵家族
2.1 softmax交叉熵:多分类任务默认选项
softmax交叉熵几乎是多分类任务里最稳妥、最常用的损失函数,它的完整形式是给logits做softmax得到概率分布,然后计算概率分布和真实标签的交叉熵。
公式上其实不用死记,核心要理解两点:第一,softmax把logits转成概率分布,并且带有“相对比较”的性质,logits大的类别概率就大,而且是拉大差距的那种指数放大;第二,交叉熵在计算时只关心真实标签那一维的概率值,其他维度不管,直接取对数取负号。
这样设计推导出来的梯度形式非常漂亮:反向传播到logits层的梯度等于预测概率减去one-hot真实标签。这个形式意味着什么呢?如果模型对正确类别的置信度是0.8,梯度那一维就是-0.2,会温和地推着参数增加该类的logit;如果置信度才0.2,梯度就是-0.8,推力非常大。整个学习过程天然是“错得越多、学得越狠”,这也是它收敛效率高的根本原因。
实际用的时候有几点经验:
- 输入给softmax交叉熵的logits不需要手动过softmax,很多框架的损失函数层内部会合并softmax和交叉熵的计算,这样数值上更稳定。
- 如果做二分类,用sigmoid加BCE,不要用双分类的softmax。本质上两者数学等价,但BCE的数值稳定性更好,实现也更简洁。
- 标签做了平滑的话,梯度形式会从“预测减one-hot”变成“预测减平滑标签”,极端情况下模型输出概率会偏向平滑值而不是0和1,这对防止过拟合有奇效。
2.2 二分类场景的BCE与logits技巧
二分类任务里最常用的是BCE,本质上是一个只有两个类别的交叉熵变体。它的做法是先对logits做sigmoid得到0到1之间的概率,再拿这个概率跟目标值计算二值交叉熵。
BCE的数值实现上有个大坑:不要自己先算sigmoid再算log,直接在损失函数里传入原始logits,并且使用带logits的BCE接口。原因是sigmoid在输入绝对值很大的时候,概率会趋于0或1,直接算log(0)会得到负无穷,NaN就这样出现了。框架内置的融合实现会用log-sum-exp之类的技巧做数值稳定,我刚开始用原生代码复现的时候在这个上面卡了好几天。
还有一个容易被忽略的细节:BCE的目标值并不强制要求是0或1。如果你想做标签平滑,完全可以把目标值设成0.1或0.9。这时模型输出的概率会被约束在中间区间,不会过分自信,泛化能力通常更好,这个方法在有些比赛里被用来压过拟合。
2.3 Focal Loss:类别不均衡时的强力武器
标准交叉熵在类别不均衡场景下有个明显问题:模型天然倾向于把样本都预测为多数类,因为这样整体loss比较小。Focal Loss针对这个问题做了两件事:一是加入类别权重,让少数类的loss被放大;二是加一个调节因子,让模型更关注难分样本。
核心思想是:如果某个样本已经预测得很准了,也就是概率接近1,那么它的loss应该被压得很低;反之如果样本总是被预测错,概率很小,那它的loss要保持一个大值。这样模型不会花太多精力在已经学会的简单样本上,而是集中火力处理那些区分不开的难样本。
实际调Focal Loss的时候,gamma参数一般从2.0开始,alpha用来平衡类别在0.25到0.75之间调。需要注意的是,Focal Loss对超参比较敏感,如果gamma太大,模型会过度关注那些离群点甚至噪声样本,导致训练不稳定。如果数据不均衡没到特别夸张的程度,比如正负比在1比10以内,先用类别权重加普通交叉熵通常就够用了,不一定非要上Focal Loss。
3. 回归任务里的MSE、MAE与Smooth L1
3.1 MSE的梯度放大效应
回归任务里最直观的损失函数是MSE,也就是均方误差。它对“大误差”极其敏感,因为误差先平方再取平均,这个平方操作会把离群点的影响放大得非常强。
梯度也很有意思:MSE对预测值的梯度是2乘以预测和真实值的差。也就是说,差的越大,梯度越大,模型会被推着快速修正错误。这个特性在模型初期阶段非常好用,前期误差大,梯度也大,更新速度快。但到后期接近收敛的时候,梯度会变得很小,容易出现“慢悠悠地磨”的情况。更麻烦的是,如果样本里有几个明显的离群点,MSE会把大量梯度分配给这些点,模型参数被拖着走,整体预测质量反而下降。
我自己做预测量级跨度很大的任务时,比如房价预测或用户时长预测,直接上MSE经常被极大值带偏。这种时候要么对目标值做变换,要么换MAE,要么用Smooth L1,根据实际数据情况选择。
3.2 MAE的鲁棒性与训练不稳定的矛盾
MAE用的是绝对误差,它对离群点的敏感度比MSE低很多。因为它的梯度是固定的,绝对值等于正负1,不会因为误差变大而梯度无限增大。从鲁棒性的角度来说,MAE在数据有脏标签或者明显离群点的场景下表现更好。
但MAE有个让人头疼的缺点:它在误差为0的那个点是一个折角,导数不存在。实际训练中虽然很少正好落在0点,但这个性质导致它在收敛阶段的表现不够平滑。还有一个更实际的问题:MAE的梯度大小恒定不变,不管误差多小都是1,这会导致模型在收敛到最优解附近时,很难做精细的微调,loss会在一个范围内来回震荡,精度的上限受到限制。
所以MAE直接用的场景其实有限,更多时候大家用Smooth L1或者Huber Loss来做折中。
3.3 Smooth L1与Huber Loss的折中设计
Smooth L1结合了MSE和MAE的优点:在误差比较小的时候,梯度随误差缩小而缩小,行为接近MSE,适合精细收敛;在误差比较大的时候,梯度限制在一个常数上,行为接近MAE,不会出现梯度爆炸。这就解决了回归任务里离群点和收敛精度不能兼得的矛盾。
目标检测里的边界框回归几乎默认用Smooth L1,原因就在这里。检测框的回归任务里,初期预测框跟真实框差值很大,用MSE容易被极端偏差带飞,用MAE收敛又不够精细,Smooth L1两边都占了。我实际用下来,Smooth L1对超参不敏感,基本不用怎么调就能出稳定结果,很适合当回归任务的默认选项。
Huber Loss和Smooth L1本质是同一个思想,只是对“小误差”和“大误差”的分界线可以自己调delta参数。delta越小,越接近MAE的鲁棒性;delta越大,越接近MSE的精度特性。有精力的话可以像调学习率一样去搜索一下delta,效果往往比无脑默认参数要好。
4. 多标签场景与度量学习中的损失函数
4.1 多标签分类为什么优先BCE
多标签任务和多分类任务有本质区别:多分类的标签是互斥的,一个样本只能是五个类别中的一个;多标签的标签是独立的,一个样本同时属于多个类别。所以多分类用的softmax交叉熵在这里并不合适,它会强制所有类别概率加起来等于1,反而破坏标签之间的独立性。
多标签分类的正解是BCE,对每个类别单独计算二分类损失,然后再把所有类别的损失累加或取平均。每个类别都有自己独立的输出概率,互不干扰。训练时模型要去学习“这个样本有没有包含猫”、“有没有包含狗”、“有没有包含树”这几个独立任务。
实操上有个细节:类别不均衡在多标签场景里比单标签更常见,因为每个类别的正样本比例可能差异巨大。这种情况下要按类别设置不同的权重,给少见的类别更高权重,否则模型对所有类别的预测都会偏向负样本那边。权重设置可以简单根据正样本数量的倒数和类别总数做归一化来得到。
4.2 对比学习里的温度系数与InfoNCE
对比学习这几年非常火,它用的损失函数本质上是一种带温度系数的softmax交叉熵变体。核心思路是构造正样本对和负样本对,然后拉近正样本的表示距离,推远负样本的表示距离。
InfoNCE这个损失函数做过很多实验之后,给我的直接感受是:温度系数的影响极大,可以说比学习率还敏感。温度系数设大了,所有样本的相似度都被抹平,模型分不清正负样本,学出来的表示缺乏区分度;设小了,模型只关注最难的负样本,容易被离群点带偏,训练很不稳定。经典的对比学习实验里,温度系数大多是0.07附近,但实际使用时还得根据自己数据的分布和batch大小重新搜索。
对比学习里batch size也很关键,因为负样本来自同一个batch里的其他样本。batch越大,负样本越丰富,模型学到的表示通常越好。很多人以为自己的模型效果差是网络结构或数据增强的问题,结果把batch size从64提到256之后效果直接涨了一大截,这在对比学习里是很常见的事。
4.3 Triplet Loss的样本构造难点
Triplet Loss在度量学习、人脸识别、行人重识别这类任务里用得很多。它的损失构造是拿锚点样本和正样本的距离减去锚点样本和负样本的距离,然后加一个margin,希望这个差值为负。
这个损失在原理上不复杂,真正难的是样本挖掘策略。如果随机选三元组,你会发现大部分三元组都是“简单三元组”,也就是锚点和正样本已经很近、和负样本已经很远了,loss基本为零,梯度也没多少,训练效率极低。所以实际工程里要做难样本挖掘,专门挑那些“正样本远、负样本近”的困难三元组来训练。
半难样本挖掘是实战里最常用的方案:在一个batch内部,对每个锚点随机选正样本,然后只挑那些距离锚点比正样本更近的负样本作为难负样本。这样既能保证训练效率,又不会像全难样本挖掘那样让模型崩溃。Triplet Loss的批量构造是个经验活,batch内部样本的类别构成、采样方式都会直接影响最终效果。
5. 实际训练中的调参经验与避坑指南
5.1 损失函数选型对照表
| 任务类型 | 推荐损失函数 | 主要特点 | 特别注意 |
|---|---|---|---|
| 多分类 | softmax交叉熵 | 梯度自适应,收敛稳定 | 输入logits,不要手动过softmax |
| 二分类 | BCE | 简单稳定,支持标签平滑 | 使用带logits的融合实现 |
| 多标签分类 | BCE(逐类别) | 保持标签独立性 | 按类别设置权重缓解不均衡 |
| 类别不均衡分类 | Focal Loss | 聚焦难样本,压制易分样本 | gamma和alpha需仔细调节 |
| 回归-一般场景 | Smooth L1 / Huber | 兼顾鲁棒性和收敛精度 | delta可调,默认值也能用 |
| 回归-离群点多 | MAE | 对离群点不敏感 | 收敛阶段可能震荡 |
| 回归-要求高精度 | MSE | 收敛精度高 | 警惕梯度爆炸,需要清洗数据 |
| 度量学习 | Triplet Loss / InfoNCE | 学习特征表示 | 难样本挖掘是关键 |
| 序数回归/打分 | 排序损失(如Pairwise) | 关注相对顺序 | 标签需要构造对比关系 |
这个表不是死的,比如回归任务数据很干净并且量级合适,MSE依然非常好用;类别比较均衡时,Focal Loss带来的收益也不明显。选型的原则永远是“先理解任务的本质,再考虑损失函数的梯度行为”。
5.2 训练中loss不下降该怎么排查
训练时loss不下降的情况我碰到过很多次,每次原因可能都不一样,但排查顺序是有规律可循的。
我先看loss的初始值是否正常。以多分类交叉熵为例,如果类别数是10,随机初始化模型的loss初始值应该在ln(10)左右,也就是大约2.3。如果偏离这个值很远,基本能确定数据预处理或者模型初始化出了问题。二分类BCE的初始期望是ln(2),大约0.69。这个“初始loss合理性检查”是判断问题出在哪个环节的最好抓手。
再看数据进到模型里是否正常。检查有没有NaN、标签有没有错位、特征有没有做归一化。有一个很隐蔽的坑:标签没有从0开始编号,或者类别索引越界,很多框架的交叉熵接口对这类错误检查并不严格,跑起来loss直接变成NaN或者在很小的范围内震荡,排查起来特别费劲。
然后检查学习率。学习率过大时,loss曲线通常表现为前期下降很快然后突然卡死或飙升;学习率过小时,loss下降非常缓慢,像一个老年人挪步。如果你发现loss在几百步内纹丝不动,试着把学习率调大10倍。如果你发现loss一开始就飞了,先把学习率调小10倍再观察。
5.3 关于自定义损失函数的一些心得
工程里经常会遇到现成损失函数不能满足需求的情况,这种时候就得自己设计或组合损失函数。我一般遵循几条原则。
第一,保持可导性。所有参与计算的操作,包括自定义的权重函数、mask操作,都必须保证在有效域内可导,或者在不可导点做好平滑处理。用Python的if做分支判断然后直接返回一个常数,听起来没问题,但反向传播时梯度就断掉了。
第二,组合损失时注意量纲。比如总损失是分类损失加边界框回归损失,如果回归损失的数值范围是分类损失的几百倍,梯度会被回归项完全主导,分类任务实际上没学到东西。这种情况要给每个子损失配一个权重系数,或者对着两个loss分别做归一化。
第三,写自定义损失一定要做梯度检查。用框架自带的gradcheck工具,用数值梯度对比解析梯度,误差在可接受范围内才算写对了。这能省掉后面Debug时的一大堆烦恼。很多人觉得写个损失函数很简单,不检查直接开始训练,结果模型效果怎么都不对,回头查才发现是梯度数值有问题,白跑了一堆实验。
5.4 多损失组合时如何平衡各分量
多任务学习里,损失函数往往是多个分量的加权和。权重怎么设,本质上是“模型资源的分配比例”。一个分类任务加一个回归任务,分类loss权重给1.0,回归loss权重给0.1,意思就是模型大头的学习能力都放在分类上,回归只要“差不多对就行”。
有一种更聪明的做法是让权重自动调节,不确定性加权就是其中之一。它把每个子任务的不确定性作为可学习的参数,让模型在训练过程中自动平衡各任务的权重。这个方法的直觉是:任务噪声越大,它对总损失的贡献就应该越小。在实际项目里,这种做法比手动调权重要省事很多,往往还有更好的收敛效果。
如果你不想引入额外参数,也可以先固定权重跑几个step,观察每个子loss的数值量级和梯度范数,然后反过来调整权重的比例。场景是:分类loss是1.2,回归loss是0.003,直接把回归权重调到几十或者几百,让两个loss对总梯度的贡献大致处于同一个量级,这样模型才不会偏科。
6. 从loss曲线诊断训练状态
6.1 常见曲线形态与对应问题
训练过程中的loss曲线包含大量信息,积累多了之后,基本看一眼曲线形态就能猜出问题大概出在哪。
曲线正常下降然后趋于平缓,这是健康的状态,说明模型在收敛,这时候可以开始考虑验证集效果,决定要不要加正则化或提前停止。
曲线下降一段后开始反弹,大概率是学习率太大,模型越过最优点在附近震荡。这时要么降低学习率,要么加入学习率衰减策略。
曲线从一开始就保持很高且没有任何下降迹象,往往代表梯度传不回去,或者数据本身和目标完全不匹配。检查一下模型输出层有没有加对激活函数,比如回归任务输出层不应该有sigmoid,多分类的logits输出后不需要额外激活。
曲线锯齿状震荡但总体趋势缓慢下降,通常是batch size太小,或者数据顺序存在某种周期性。可以试试增大batch size,或者打乱数据的顺序。
曲线出现突然的NaN,这个场景我专门说一下子。
6.2 训练中出现NaN的排查顺序
NaN问题几乎是每个深度学习者都撞过的墙,它的出现一般有几个固定来源。
最常见的是loss计算阶段出现数值不稳定,比如把概率值直接取log,遇到0概率,或者用某些框架的softmax实现不当,数值溢出。解决方式是使用框架内置的数值稳定版本实现,不要自己拼装中间过程。
其次是学习率过大,导致梯度更新后参数变成NaN。这种情况把学习率往回调就能解决,一般从当前值除以10开始试。
还有可能是数据里有NaN或者无穷值。有些数据集预处理不干净,特征里藏着NaN,训练到那个样本时梯度直接变NaN。这种最阴,因为不是一开始就出现,而是跑到某一步才爆掉。排查方法是在数据加载后做一次数值检查,确认没有NaN和Inf。
最后一类是梯度爆炸,尤其在深层网络或者RNN这类结构里常见。解决手段有梯度裁剪、更好的初始化方式、调整网络结构。我的习惯是看到NaN先降学习率,不行就加梯度裁剪,再不行就检查数据和loss实现,基本能覆盖90%的情况。
6.3 验证集loss和训练集loss差距过大怎么办
训练loss持续下降,验证loss不降反升,这是过拟合的信号。处理思路一般从这几个方向入手。
最简单的办法是加正则化,权重衰减是默认选项,我一般从小量级开始试,根据验证集的表现调整。Dropout对全连接网络很有效,但卷积网络和Transformer结构里它位置和比例很重要,加不好反而损伤精度。
更根本的方式是增加数据增强。数据增强本质上是在扩充训练分布,让模型很难死记硬背训练数据。图像里随机翻转、裁剪、色彩抖动,文本里有随机掩码、同义词替换,不同模态有各自默认的增强套路。增强幅度太大也会导致分布偏移过大,模型学不到有效信息,需要注意把握度。
标签平滑也很有用,它让模型不要那么确信自己的预测,从而不容易对训练集过度拟合。我自己做分类任务时,不管有没有过拟合迹象,都倾向于加一个0.05或0.1的标签平滑,缺点是会对最终准确率有一点微小的影响,但通常换来的泛化能力提升更划算。
还有一个容易忽略的方向是检查验证集和训练集的分布差异。有时候并不是过拟合,而是验证集本身采样有问题,比如某些类别在验证集中特别少,导致验证loss波动很大。这种情况下要回头检查数据划分方式,而不是盲目调模型。
7. 一些实际工程中的补充经验
7.1 对梯度裁剪的重新认识
很多人对梯度裁剪的理解就是“防止梯度爆炸”,但它在损失函数层面的意义其实更深。当损失函数在某个区域的梯度极大时,比如MSE遇到极端离群点,或者Focal Loss遇到极难样本,梯度裁剪能限制单步更新的最大幅度,让训练过程更平滑。
实际操作上,global norm clipping比value clipping更常用。前者把所有参数的梯度拼成一个全局向量,限制这个向量的范数大小,如果超过阈值就等比缩放,这样既能保留各参数梯度的相对关系,又能控制总步长。一般阈值设为1.0到5.0之间,1.0通常是可靠的起点。
我印象很深的一次是训练一个带注意力机制的文本分类模型,loss在某个batch上突然冲高,但整体没有变成NaN,只是训练波动很大导致最终效果不好。加上梯度裁剪之后,整个训练过程平稳了许多,最终效果也明显提升。从那之后我养成了习惯,但凡用MSE或Focal Loss,基本都会顺手加一层梯度裁剪保平安。
7.2 损失函数与数据增强的协同作用
损失函数并不是孤立的,它和数据增强策略之间有很强的耦合关系。数据增强改变了样本的分布,模型看到的训练数据形态变了,它对损失的响应也会跟着变。
举一个有代表性的例子:对比学习里,数据增强强度直接影响InfoNCE损失的难度分布。增强太弱,正样本对太容易区分,损失很快就降到很低,特征提取器学不到什么有区分度的表示;增强太强,正样本对被搞得差异过大,模型很难学出稳定的特征,损失下不去。所以对比学习项目里,调数据增强策略和调温度系数基本上是一起做的。
另一个例子是图像分类里用Mixup或CutMix这类增强方式时,标签不再是干净的one-hot,而是混合比例下的软标签。这种时候如果继续用传统交叉熵,模型会学得很别扭,因为它的预期是硬标签。但如果配合适当标签平滑,或者直接用针对软标签设计的损失形式,模型就能平滑地适应这种混合分布。
7.3 从损失函数视角看模型评估
最后说一个经常被忽略的点:训练用的损失函数和评估用的指标往往不是一回事。分类任务训练用交叉熵,评估用准确率或F1;回归任务训练用MSE,评估可能用平均绝对百分比误差或R方。
从损失函数的视角来看,评估指标的选择本质上也是在定义“这个任务里什么最重要”。交叉熵低不代表F1就高,尤其类别不均衡的时候,模型把多数类都预测对,交叉熵也能很低,但少数类一个都认不出来,F1直接崩盘。这种时候要么在损失函数层面下功夫,要么在评估指标上重新定义,甚至两者一起改。
回归任务里,如果你更关心小误差而不是大误差,训练用MAE可能比MSE更贴合最终评估指标,哪怕MSE在数值上更好看。反过来,如果你希望模型在极端值上的表现也要好,那就得接受MSE对离群点的敏感性,同时想办法清洗数据。模型什么时候该换损失函数,不只看训练loss降不降,更要看最终任务指标有没有跟上。
就我自己实际训练的经验来说,损失函数的选型和调节确实会直接影响模型的天花板,但它也不是万能的。数据质量、模型结构、学习率调度、训练时长,每一个因素都在共同决定最终结果。建议大家新开一个任务时,先从最成熟的默认损失函数开始跑通全流程,再根据瓶颈一步一步替换和调整,这样既省时间,也能真正看到每个改动带来的真实影响。损失函数这个环节,值得反复打磨,但别指望光靠它解决所有问题。