资讯详情

损失函数怎么选?从分类、回归到多任务的原理与实战调参

📅 2026/10/21 21:56:24 | 华诺云谱 👁 阅读
损失函数怎么选?从分类、回归到多任务的原理与实战调参
做了这么多年模型训练我越来越觉得损失函数才是整套深度学习流程里最值得先想清楚、却经常最后才被想起的部分。很多人习惯把数据集准备好、网络结构搭好然后在训练脚本里顺手写一个CrossEntropyLoss或者MSELoss觉得“先跑通再说”结果模型不收敛、指标上不去、训练到一半loss炸掉最后排查一圈才发现问题出在损失函数的选择或实现上。损失函数不是训练脚本里可有可无的一行代码它直接决定了模型在优化时“什么是对的”“什么是错的”也决定了同样的网络结构在同样的数据上到底能学到什么程度。这篇文章我会从原理讲到实际调参包括分类任务和回归任务里最常用的损失函数分别适合什么场景、为什么适合以及我在实际项目中遇到的NaN排查、类别不平衡、多任务加权等真实问题。适合正在入门深度学习、想搞懂loss本质的读者也适合已经能跑通模型但总觉得训练过程不太可控的工程师参考。1. 损失函数在训练闭环里的真实角色1.1 从一次参数更新的完整链路看损失函数深度学习训练的本质可以用一句话概括通过反复调整网络参数让模型的预测结果不断接近我们期望的目标。这个“接近程度”用什么来衡量就是损失函数。每一次参数更新其实都经历了五步前向传播得到预测值用损失函数计算预测值和真实标签之间的差距根据这个差距计算梯度用梯度更新参数然后再重复整个过程。值得强调的是损失函数不只是拿来看的一个数值它决定了梯度的“方向”和“大小”。方向告诉我们每个参数该往哪个方向调大小告诉我们每次调多少。如果你的损失函数选得不对梯度方向可能完全是反的如果你的损失函数和评估指标不一致训练过程中loss一直在降但真正关心的指标可能纹丝不动。举个例子我在做“模拟项目X”的二分类模型时一开始天真地用了MSELoss去拟合0和1的标签。训练了半天训练集的MSE跌到了0.15左右看起来好像不错但实际准确率只有60%。原因后面会细讲核心就是因为MSE并不是为分类任务设计的损失函数它把分类问题当成回归问题来解梯度信号非常弱模型学出来的概率值缺少区分度。所以理解损失函数在训练闭环里到底扮演什么角色是避免这类问题的基础。1.2 损失函数和模型能力上限的关系很多人默认“只要有足够多的数据、足够深的网络模型就能学到任何东西”这个说法在一定条件下成立但有一个隐含前提损失函数要能量化出“模型离目标还有多远”。如果损失函数给不出有效信号网络再深、数据再多也是白搭。从统计学习的视角看损失函数反映的是我们对“误差分布”的假设。MSE隐含着“误差服从高斯分布”的假设交叉熵隐含着“预测分布与真实分布的KL散度”要被最小化。你选择了哪种损失函数本质上就是在告诉优化器什么样的预测结果是可接受的、什么样的结果是代价巨大的。模型的能力上限往往不是由网络结构决定的而是由“优化目标定义得好不好”决定的。很多调参问题本质上都是损失函数问题。比如梯度消失、训练不稳定、收敛到不理想的局部最优点这些表面现象背后可能都是损失函数的设计不合理导致梯度信号有问题。所以我把损失函数理解为整个训练过程的“指挥棒”它指向哪里模型就会往哪里走。2. 分类任务损失函数的选择逻辑2.1 交叉熵为什么能成为分类任务的默认选择分类任务里最常用的损失函数就是交叉熵Cross Entropy。它的公式长这样对于单个样本如果真实类别是i损失等于-log(模型预测该类的概率)。也就是说模型如果给正确类别打的概率越高损失越低如果给正确类别打了很低的概率损失会非常大。交叉熵之所以成为默认选择主要有三个原因第一它和Softmax天然搭配。Softmax把网络输出的logits转成概率分布交叉熵负责衡量这个分布和真实标签的差异两者的数学性质匹配得很好。第二它的梯度形式非常简洁。对于Softmax加交叉熵的组合梯度等于预测概率减去真实概率这个梯度信号在概率偏离较大时很强在接近正确时自然变小非常符合直觉也方便优化。第三它对应着最大似然估计和“模型输出概率分布”的建模视角完全一致。我之前遇到过一种情况有人嫌交叉熵在softmax之后计算会“数值不稳定”于是自己手动先算softmax再取log结果训练中经常出现NaN。正确的实践是使用框架里提供的CrossEntropyLoss它内部会把softmax和log合并起来做数值稳定性处理直接传入网络输出的logits就行框架会帮你处理溢出问题。2.2 二分类和多分类在损失实现上的细微差别二分类问题常见的实现有两种一种是用两个输出节点的Softmax然后用多分类交叉熵另一种是只有一个输出节点用Sigmoid得到概率然后算二分类交叉熵BCE。两种方式在理论上等价但实际使用中我更推荐用单节点的BCEWithLogitsLoss。这个函数把Sigmoid和BCE的计算合并在一起数值稳定性更好内存占用也更小。多分类任务里很多新手会犯一个错误手动对logits做softmax再算-torch.log(prob[target])作为loss。结果在训练后期有些样本的正确类别概率打到0.999以上log之后损失接近一个很小的数再乘学习率基本没有梯度模型几乎不动。倒不是说这样完全不对而是多此一举地引入了不必要的中间步骤也容易踩数值精度的坑。直接用框架提供的CrossEntropyLoss输入logits和整数标签即可干净利落。另外还有一种情况是多标签分类也就是一个样本可以同时属于多个类别。这时不能再用Softmax加交叉熵因为Softmax强制所有类别概率之和为1而多标签任务里多个类别可能同时为1。这种情况正确做法是每个类别独立算Sigmoid然后各自用BCE最后取平均。这里如果继续沿用多分类的交叉熵模型一定会学偏因为目标分布根本不符合“互斥”的假设。2.3 类别不平衡时加权交叉熵和Focal Loss各自怎么用真实项目里分类问题很少是完美平衡的。欺诈检测、异常识别、医学影像筛查正样本比例经常只有1%甚至更低。如果直接用普通交叉熵模型会倾向于把所有样本都预测为多数类因为这样可以把总loss压得很低但实际业务根本没法用。最简单的解法是加权交叉熵给少数类样本更高的权重相当于人为放大少数类的梯度贡献。权重的设置通常参考类别比例的倒数比如正样本占1%权重就设为100左右。实际操作中我不会直接用精确的比例倒数而是先用一个较小的倍数试比如10倍或者20倍观察模型在少数类上的召回率和精确率变化再逐步调整。因为按精确倒数调整往往会导致模型过分偏向少数类把多数类误伤得很严重。Focal Loss是另一个思路它不直接调整样本权重而是用一个调制因子降低“易分类样本”的损失贡献让模型把注意力集中到难分类的样本上。它的公式里有两个参数alpha控制类别平衡gamma控制难易样本的调节强度。gamma通常取2alpha根据正负样本比例来定。我在做某些高度不平衡的二分类任务时Focal Loss相比加权交叉熵在F1分数上能提升三到五个百分点尤其是在“难样本”集中在边界区域的数据集上。这里可以看一个简单的选择对比场景推荐损失函数说明类别大致平衡普通交叉熵简单可靠收敛稳定类别不平衡但难样本分布均匀加权交叉熵直接提高少数类梯度权重类别不平衡且难样本集中在边界Focal Loss降低易样本干扰聚焦难样本多标签分类多路BCE每个类别独立判断不走Softmax3. 回归任务的损失函数MSE的陷阱与替代方案3.1 为什么说MSE在回归里并不总是正确选择回归任务默认首选MSE均方误差把预测值和真实值之差的平方求平均。MSE在数学上很优雅处处可导梯度也比较简单但它有一个容易被忽略的毛病对离群值极其敏感。因为误差是平方的一个偏差为10的样本贡献的loss是偏差为1的样本的100倍哪怕整个数据集里只有一个这样的离群点它也可能主导整个训练过程。我在做一个价格预测的项目时遇到过这个情况训练集里绝大多数样本的误差都在1以内但有一个样本因为某种原因价格标错了误差达到7。结果模型训练了很久验证集上的表现一直很差后来画出预测值和真实值的散点图才发现模型为了照顾那个离群样本把整条拟合曲线都拉偏了。MSE在这种场景下会把模型“带偏”不是模型不行而是损失函数对误差的惩罚方式不适合这种带有异常值的数据。MAE平均绝对误差没有平方放大效应对离群值更鲁棒但它也有问题在误差接近0的位置梯度不连续、梯度绝对值恒定导致收敛到最优值附近时容易来回震荡难以精细逼近。实际项目中我很少直接用MAE除非数据里的噪声确实很大、且对精度要求不高。3.2 Huber LossMSE和MAE的折中方案Huber Loss可以说是为这类问题量身定做的。它在误差较小时表现为MSE在误差较大时表现为MAE中间通过一个delta参数来切换。当误差小于delta时梯度随误差减小而减小收敛精细当误差大于delta时梯度被限制在一个常数不会让离群点主导更新方向。选择delta的原则是把它设成你对“正常误差范围”的估计值。比如你的训练误差大多数落在0.3以内少部分落到2以上那delta设为0.5或1就比较合适。delta太大就退化成MSE太小就退化成MAE。我通常先观察一个MSE模型在验证集上的误差分布取大约75%或90%分位数的误差值作为delta的初始值再稍微调小一点。Smooth L1 Loss是Huber Loss在目标检测里的常用变体也被称为Huber的一种实现形式。Faster R-CNN系列的回归分支用的就是这个因为它同时具备MSE的收敛精度和MAE对离群点的容忍度特别适合处理边界框回归里面经常出现的“预测值和真实值偏差较大”的情况。3.3 目标数值跨度大时先考虑变换目标而不是换损失函数回归任务里还有一个很常见的坑目标值的量级跨度特别大。比如同一个数据集里既有几十块的日用品价格也有几十万的大件商品价格。直接用MSE训练模型会优先拟合那些数值大的样本因为它们的误差平方天然就大小数值样本的梯度几乎被淹没。处理这个问题有两个方向一是对目标值做对数变换先用log(y)作为学习目标预测完再指数还原二是把目标值归一化到类似0到1的区间。我通常更推荐对数变换因为它保留了数据的相对误差意义。对价格这类数据“预测1000块偏差100块”和“预测10块偏差1块”本质上是同样的相对误差但在MSE里前者的贡献是后者的1万倍这完全不合理。对数变换之后相对误差会变成绝对误差不同量级的样本对梯度的贡献就相对公平了。如果既不想变换目标又希望模型对不同量级都有感知可以考虑在损失函数里加入样本级别的权重归一化比如每个样本的loss除以它真实值的平方或绝对值。但这种方法需要额外写逻辑也容易引入新的问题不如直接对目标做变换来得干净。4. 梯度视角为什么同样的损失函数有人训得动、有人训不动4.1 梯度量级与学习率需要匹配损失函数设计得合理只解决了“方向对不对”的问题还有一个很重要的问题是“梯度量级是否合适”。学习率的本质是参数的更新步长它的选择必须和梯度量级匹配。如果你的损失函数天然会把梯度变得很小那么即使方向正确参数更新也会慢到让人崩溃反之梯度太大会导致训练震荡。一个经典例子是最小二乘问题。在特征尺度差异很大的数据集上MSE对不同特征的梯度量级可能相差几百倍这会让学习率很难选学习率小了大梯度特征不动学习率大了小梯度特征会震荡甚至发散。这也是为什么实际训练前要对特征做标准化让每个特征的尺度相近从而让梯度量级相对一致。从损失函数的角度看如果我们发现无论怎么调学习率训练都不太稳定可以先检查loss本身的尺度和梯度分布。比如在自定义损失函数时我给某些任务加了多个子损失直接相加结果总loss比单个子损失大了一个数量级。如果不做任何缩放原来能用的学习率突然就失效了。这种情况需要给总损失乘一个缩放系数或者给每个子损失配一个权重让梯度量级回到一个合理的范围。4.2 训练中出现NaN的完整排查链路训练中loss变成NaN是所有人都遇到过的问题我之前也排查过很多次现在逐步总结出了一套固定路线。第一条线是检查输入数据数据里有没有NaN标签有没有出现Inf特征有没有明显异常的数值第二条线是检查损失函数自身有没有出现log(0)、除以0、sqrt负数之类的情况第三条线是检查学习率和优化器第四条线是检查网络中间层有没有数值溢出。我印象最深的一次NaN排查是在某个自定义损失函数里计算-torch.log(prob)时某个样本的prob被softmax推到了0log(0)直接得到负无穷反向传播时梯度也变成NaN。这时再小的学习率也没用因为梯度本身就是NaN。找到原因之后我在计算里加了一个极小值epsilon做下界保护或者改用数值稳定的内置实现NaN问题立刻消失。如果是学习率过大导致的NaN现象通常是loss先从正常值跳跃式增长然后再变成NaN而不是一开始就是NaN。这时候把学习率从0.01降到0.001甚至更低一般能救回来。还有一个容易被忽略的地方如果模型用了BatchNorm并且batch size特别小统计量的方差太大也可能导致数值不稳定但这种情况比较少直接表现为NaN更常见的还是损失函数或数据的问题。下面这个表格是我自己用的排查顺序可以参考排查顺序检查项常见症状1输入数据含NaN/Inf训练一开始loss就不稳定2损失函数出现log(0)、除零某个特定情况下突然变NaN3学习率过大loss先增长后变NaN4网络中间层数值溢出通常在很深或很宽的网络上出现5混合精度训练精度不足小loss时梯度下溢为04.3 从损失曲线读出异常信号很多人在训练时只会盯着loss数字其实loss曲线本身包含了很多信息。我自己的习惯是同时记录训练集和验证集的loss并且每一轮都打印出来画成曲线。曲线如果下降平滑说明学习率合适如果下降之后又反弹再下降说明学习率可能偏大正在一些窄谷里震荡如果验证集loss降到某个位置后开始回升而训练集loss还在下降那基本可以判断是过拟合了不是损失函数的问题。还有一个不容易注意的信号训练集的loss一轮比一轮高但验证集的指标反而在提升。这种情况在早期学习中是有可能出现的尤其是用了正则化或者数据增强时。不要一看训练loss上升就着急改结构可以再等几轮看看验证集的表现。真正危险的是训练集loss不降这时大概率是梯度或学习率出了问题而不是损失函数本身选错了。如果loss曲线在某个数值附近反复震荡、怎么调学习率都不收敛我会回头检查样本标签是否存在质量问题。有些项目里标签本身就是错的模型在“学习一个不存在的规律”损失函数自然降不下去。这种情况下换再好的损失函数也没有用先清洗数据比调参更重要。5. 多任务组合损失与一次完整的模拟项目调参记录5.1 多任务损失加权的基本思路实际项目中很少只有一个目标。目标检测既要有分类损失又要有坐标回归损失推荐系统既要预测点击率又要预测转化率图像生成任务里既有对抗损失又有重构损失。多任务学习的第一步就是要把多个损失组合起来最简单的方式是加权求和总损失 w1 * 损失1 w2 * 损失2。但权重怎么定很多人会凭感觉设成1比1结果某些任务被忽略学不出来。我在实践中会用两种方式一是手动调整权重先让每个子任务单独跑一下观察各自loss的量级和收敛速度然后按量级比例设置初始权重二是不确定性加权这种方法让每个任务学习一个噪声参数自动调整权重在任务量级差异很大时比较省心。GradNorm这类方法更复杂想显式平衡不同任务的梯度幅度时可以考虑但实际项目里我很少一开始就用它因为引入的复杂度不一定值得。一个值得记住的原则是不同损失的量级要尽量对齐到同一水平。如果分类loss在0.5左右回归loss却在10左右那回归任务会主导整个训练分类任务基本学不到东西。可以先观察每个损失单独的数值范围再决定权重。5.2 模拟项目X的完整调参记录接下来分享一个具体的项目我就是用这个来自查调参流程的。模拟项目X的任务是一个二分类问题预测某类设备在未来一周内是否会发生故障正样本占比大约3%。数据集有20万条样本、80个特征输入是数值型特征和少量类别特征。评估指标用的是F1分数因为业务上既不想漏报故障也不想报太多假警。第一步我先用最简单的方案跑通一个两层的全连接网络隐藏层256和128ReLU激活用BCEWithLogitsLoss不加任何类别权重。跑了50轮之后验证集准确率高达96%因为绝大多数样本是负样本模型只要全部预测为负就能拿到很高的准确率。但F1几乎为0因为一个正样本都没预测出来。这个结果我预料到了目的只是为了建立baseline。第二步我在损失函数里加上正样本权重正样本的loss乘20。训练到30轮时验证集F1从0升到0.31但正样本的召回率很高、精确率很低模型把所有边界模糊的负样本都划成正样本了。我把概率阈值从默认的0.5调到0.7之后F1升到了0.4左右。这一步让我明白一个道理损失函数解决的是模型对样本的区分能力最终阈值是另一个自由参数两者需要一起调。第三步我换成了Focal Lossgamma取2alpha设为0.25。同样的网络结构、同样的训练轮数F1稳定在0.47比加权交叉熵有了明显提升。原因是这个数据集里的正样本和负样本在特征空间高度重叠难样本集中在决策边界附近普通加权交叉熵会让模型把注意力放在所有正样本上而Focal Loss天然会抑制那些已经能分类正确的样本让模型把精力放在真正难分的边界上。第四步我加入了Huber Loss来辅助某个手工构造的特征做重构任务形成多任务结构。这个辅助任务的本意是希望模型在分类的同时学到更稳健的特征表示。实验下来分类F1提升到了0.49接近0.5。加辅助任务不是必须的但在这个项目里确实帮助模型在正样本极少的情况下拿到了更平滑的决策边界。5.3 评估指标、损失函数和阈值三者的关系问题经过模拟项目X之后我对损失函数的理解又深了一层。损失函数和评估指标并不总是严格对应的。F1分数依赖阈值而阈值又和损失函数无关是我们自己做决策时定下来的一个截断点。交叉熵或Focal Loss只是让模型的概率输出更有判别力但最终把一个样本判成正样本还是负样本由阈值决定。实际使用中我会在验证集上做阈值扫描。比如从0.3到0.8每隔0.05计算一次F1选出最优阈值。这个阈值在deployment时还要根据业务场景重新校准因为上线后的数据分布很可能和训练集不一样。损失函数负责把模型学到“分辨正负的能力”阈值负责把这种能力转化成分数两者必须分开理解和调整。我还试过直接选用与评估指标完全一致的损失函数比如把F1本身变成可微的近似形式然后直接优化。但这类方法的优化稳定性不如交叉熵参数也更多调起来很花时间。在生产项目里我仍然更倾向于“交叉熵/Focal Loss用于训练F1用于评估阈值单独优化”这一套组合拳稳定、可控、容易排查问题。最后一个实际体会是损失函数的选择一定要和项目目标绑定而不是和数据竞赛里的排行榜绑定。同样的数据、同样的模型换一种损失函数结果可能差出一大截。这个“换”不是随机试而是要从数据分布、任务类型、数值稳定性、训练稳定性四个维度去考虑。我现在拿到一个训练任务会先花半小时把损失函数定好再开始搭网络结构这个时间一定比训练十条跑不通的曲线更值得花。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑