深度学习损失函数全解析:从MSE、交叉熵到Focal Loss

发布时间:2026/10/19 6:45:35
深度学习损失函数全解析:从MSE、交叉熵到Focal Loss 如果你刚入深度学习的坑花了大半天搭好网络结构数据加载、优化器、训练循环也都照着教程写完了结果一跑起来loss要么疯了一样往上跳要么稳如一条直线验证集上的效果更是惨不忍睹——那我敢说七成问题出在你对损失函数的理解上。损失函数这个东西代码里往往就一行调用看着不起眼但实际上它定义了“什么是对、什么是错”决定了网络最终能学到什么。选错了损失函数后面调再多的学习率、换再强的网络骨架都像是在用错的尺子量身高怎么量都不对。这篇文章我打算从原理讲到实践不绕弯子。先把损失函数为什么是深度学习训练里的“指挥棒”讲清楚再深入回归和分类两个最常用的场景把MSE、MAE、Huber、交叉熵、Focal Loss这些反复出现的家伙一个个掰开看。最后聊一聊真实项目里怎么调试损失、怎么写自定义损失函数。适合刚入门想搞懂底层逻辑的同学也适合已经写了不少代码、但总感觉“调参全靠运气”的从业者。1. 损失函数是深度学习训练的“指挥棒”1.1 为什么需要一把“尺子”来量化偏差很多同学刚开始学的时候会把损失函数理解成“一个用来算difference的公式”然后照着教程用MSE或者CrossEntropy从不深想为什么非得是它。我的理解是损失函数本质上是一把尺子用来量模型预测值和真实答案之间的差距。但这个尺子不是随便拿把直尺就行因为不同任务对“差距大小”的定义不一样。拿价格预测举例。如果预测值比真实价格贵了10元和贵了1000元在业务上完全是两回事。但在MSE这种损失函数里这两种偏差会被平方放大最后算出来的梯度大小也完全不同。换句话说损失函数不仅告诉我们“错多远”还告诉优化器“哪个方向、用多大力气去修正参数”。再换个更直观的角度深度学习的训练过程本质上是在一个巨大的参数空间里搜索一组参数使得损失函数的值尽可能小。模型的结构决定了这个空间长什么样但损失函数决定了“空间里哪个点算好点”。同一个网络结构换一个损失函数最后收敛到的解往往也不一样。这就是为什么我们常说损失函数是模型训练的“指挥棒”——它指向哪里模型就朝着哪里使劲。这里顺便说一句很多人会问我“损失函数的数值越小是不是就代表模型越好”答案是不一定。损失值的大小和任务本身、数据量、是否加了正则项、预测目标任务难度都密切相关。比如同样一个回归模型MSE的数值天然比MAE大因为误差被平方了加了L2正则化之后损失也会变大。因此比较模型好坏不能只看loss绝对值要看同一份验证集、同一种损失函数配置下的相对变化。1.2 损失函数如何驱动梯度下降深度学习训练是一个闭环把输入喂给网络做前向传播得到预测结果把预测结果和真实标签放进损失函数算出一个标量loss然后从loss出发做反向传播利用链式法则逐层算出每个参数的梯度最后优化器拿着这些梯度去更新参数。整个链条里损失函数起到了“标尺信标”的双重作用。它先是给出一个数值告诉你当前模型有多差又通过导数告诉你在当前位置向哪个方向调整参数能更快降低误差。可以把它理解成爬山时手里的高度计每走一步看一眼读数然后朝下坡方向迈步子。如果这个高度计刻度设计得不合理——比如对某些方向的读数不敏感或者干脆在某些区域读数不变化——那你就会在山坡上原地打转甚至越走越偏。正因为梯度来自损失函数对预测值的导数损失函数的光滑性和梯度特性就非常关键。有的损失函数处处可导有的在某个点不可导有的对异常值非常激进有的又过于保守。这些看似微小的数学性质落到实际训练里就是收敛速度、稳定性、最终精度上的巨大差异。后面几个部分我会结合具体任务把每种常用损失函数的特点逐一讲清楚。1.3 先避开一个常见误区损失值越小不等于模型越好写代码的时候试过把训练集精度刷到很高、损失压到特别小结果拿到测试集上一测效果反而更差。这种情况几乎每个人都会遇到。原因是模型太“死记硬背”训练集了没有学到能泛化的规律这也就是过拟合。所以在实际项目里我从不会只盯训练loss。通常会把训练集和验证集的loss画在同一个图里一起看。如果训练loss一直降、验证loss从某个epoch开始反弹说明模型开始过拟合了这时候要么加正则化、要么提高数据增强强度、要么早停。如果训练loss和验证loss都卡在一个偏高的位置不动那首先怀疑的是学习率设置或者损失函数本身能不能表达任务目标。记住一句话损失函数是帮你“训练”模型的不是帮你“评价”模型的。评价模型还是要回到业务指标和测试集效果上这个观念会省掉后面很多弯路。2. 回归任务的损失函数MSE、MAE与Huber Loss2.1 MSE最常用但别忽略它对异常值的敏感回归问题里最经典的损失函数就是均方误差Mean Squared ErrorMSE公式长这样L (1 / N) * Σ (y_i - y_pred_i)^2也就是把每个样本的预测误差取平方再求平均。为什么用平方一个重要原因是为了处处可导且导数连续这个性质对梯度下降非常友好。另一个原因是平方会让大的误差受到更重的惩罚模型会优先把那些偏差很大的样本修正过来。但这条“大误差重罚”的特性有个副作用对异常值极度敏感。比如预测一个商品价格数据里有一个样本的真实价格因为记录错误高了10倍MSE会给这个样本一个巨大的平方误差模型就会花大量精力去拟合这个错误样本把整体的预测曲线都带歪。换句话说MSE对离群点没有任何“宽容度”样本越偏它的注意力就越集中在错误数据上。在实操层面MSE的梯度是 2 * (y_i - y_pred_i) 乘以上层回传梯度这意味着当某个样本误差特别大时梯度也可能非常大。如果恰好此时学习率设置得偏高参数更新一下就飞了训练直接发散。所以用MSE的时候我一般会配合梯度裁剪gradient clipping来兜底防止个别异常样本把训练搞崩。2.2 MAE更稳却可能卡在收敛路上平均绝对误差Mean Absolute ErrorMAE取的是误差的绝对值公式是L (1 / N) * Σ |y_i - y_pred_i|它的好处很直观无论误差有多大对损失的贡献都是线性增长不会像MSE那样被平方放大。因此在数据里有较多异常点的场景下MAE往往比MSE稳定得多模型不会被少数极端样本牵着走。不过MAE也带来两个新麻烦。第一它在误差等于0的位置不可导。数学上绝对值函数在0点是“尖角”梯度在这个点左右跳动理论上是没有定义的。实际常用的解决办法是在实现里把0点梯度设为0或者干脆忽略这一点——反正训练中误差恰好精确等于0的概率很低影响不大。第二它的梯度是一个常数正负1的方向不管当前误差是大是小更新力度都一样。这会导致模型在接近最优解的时候参数还在那个小范围内来回震荡收敛速度偏慢精度也比MSE稍微粗糙一些。所以MAE并不适合所有场景更不是“稳一点”就万能。它适合的是异常值明显、你希望模型对离群点不敏感的回归任务。如果数据比较干净、你也希望达到更精细的精度MAE反而不太合适。2.3 Huber Loss用超参数换取“两全其美”既然MSE敏感但精细MAE稳健但粗糙那能不能两者结合Huber Loss就是为此设计的。它的思路很简单误差较小时用平方误差来保证平滑和精细收敛误差较大时切换成线性损失来限制梯度爆炸。定义是分段函数当 |y_i - y_pred_i| ≤ δ 时损失为 0.5 * 误差^2否则为 δ * (|误差| - 0.5 * δ)。这里的δ是一个你需要手工调整的超参数。越小损失函数越接近MAE的强鲁棒性越大越接近MSE的二阶特性。PyTorch里的SmoothL1Loss其实就是Huber Loss的一种实现默认δ1在很多检测和回归任务里都是默认选择。我实际用下来的体验是Huber Loss是“你不想花太多时间纠结异常值时”的好帮手。尤其在一些回归任务中数据整体分布比较干净但偶尔冒出一个噪声样本用纯MSE会慌、用纯MAE又觉得太钝这时候把损失函数换成Huber基本不用怎么调δ模型就能稳定收敛到不错的结果。但这里也要提醒一下Huber Loss带有超参数δ又多了一个要调的东西。如果数据极其干净那MSE的收敛特性仍然更好如果你的任务就是要求对大量离群点完全不敏感那MAE也许更直接。Huber更适合的是“大多数时候干净、偶尔不干净”的中间地带。2.4 回归损失选择的实操建议我整理一下自己在实际项目里做选择的依据供你参考数据干净、噪声少追求收敛速度和精确拟合优先MSE。数据里有明显异常值或长尾分布优先Huber Loss保证训练稳定。异常值非常多并且你确信它们不该影响模型趋势考虑MAE或者干脆在数据预处理阶段把异常样本清洗掉。损失函数选择困难时可以先用Huber Loss跑通框架再切换到MSE和MAE做对比实验看验证集指标变化。最后说一个容易被忽略的点损失函数的“尺度”跟任务单位直接相关。同样是MSE如果预测目标的范围是0到1和预测目标的范围是0到10000算出来的loss量级差了几万倍但模型的“实际表现”可能差不多。所以在对比不同损失函数或者调学习率的时候别忘了把预测目标的取值范围考虑进去否则你看到的loss曲线形态会骗人。3. 分类任务的损失函数交叉熵与Focal Loss3.1 为什么分类问题很少直接用MSE有不少初学者会自然地想“分类不就是把结果映射成0和1吗那我把预测值和标签做MSE不就行了”理论上不是不行但实际效果会很差。核心原因有两点。第一分类问题的输出通常会过softmax或者sigmoid把原始logit压到0到1之间。这个函数在两端接近饱和导数趋近于0。如果配合MSE损失梯度计算里就会带上sigmoid/softmax导数的连乘项当预测值接近0或1时梯度会变得非常小造成“模型已经很确定但参数却几乎不动”的情况训练收敛极其缓慢。第二分类任务的标签是类别索引本身没有数值上的“距离”概念。比如类别“猫、狗、鸟”你编码成0、1、2但数值上1和2之间的差并不代表“狗和鸟比猫和狗更像”。用MSE去衡量这种数值差等于强行给无序类别安上一个自定义的度量结果容易误导模型。所以分类任务里主流做法是把类别输出转化为概率分布用交叉熵来衡量预测分布和真实分布之间的差距。它天然配合softmax/sigmoid来使用梯度更稳收敛更快。3.2 从最大似然理解交叉熵交叉熵Cross Entropy的公式是 H(p, q) -Σ p(x) * log(q(x))其中p是真实分布q是模型预测分布。在分类场景里真实标签通常用one-hot表示代入之后损失就等价于“对正确类别那一项的预测概率取负对数”。举个例子二分类任务里真实标签是1模型预测它为1的概率是0.9那么这一样本的loss就是 -log(0.9) ≈ 0.105。如果模型预测概率只有0.1loss就是 -log(0.1) ≈ 2.303。可以看到预测得越有把握且越正确loss越小预测得越自信却越错误loss会急剧变大。这种“指数级惩罚错误自信”的特性能快速把模型从错误判断里拉回来。从最大似然的角度更好理解。训练分类模型本质上就是调整参数使得所有样本属于其真实类别的联合概率最大。取对数后最大化似然就等价于最小化负对数似然负对数似然在分类问题里恰好就是交叉熵。这也是我更喜欢的一种理解路径——它让你明白交叉熵不是随便拍脑袋发明的而是从概率统计里自然导出的目标函数。在PyTorch里交叉熵最常用的是F.cross_entropy(logits, target)。注意它内部已经帮你把log_softmax和nll_loss合并好了直接传模型输出的原始logits就行不用自己再做一次softmax否则会重复计算并且可能在数值稳定性上出问题。这是一个很容易踩的坑。3.3 类别不平衡从加权交叉熵到Focal Loss实际项目里经常遇到类别极不平衡的情况比如缺陷检测里“正常”样本占了99%真实“缺陷”样本只有1%。如果直接用普通交叉熵模型只要学会“永远输出正常类”就能把损失压得很低但这样的模型毫无用处。第一种简单粗暴的解法是给不同类别加权重把多数类的loss权重调小少数类的loss权重调大。在PyTorch里可以给CrossEntropyLoss传入weight参数权重一般按类别样本数反比来设。这个方案有效但只是“硬性放大少数的梯度”没有改变模型对简单样本和困难样本的关注差异。Focal Loss是在交叉熵基础上做了进一步的创新最早用于目标检测领域专门解决“难例挖掘”问题。它的核心公式可以理解为普通交叉熵乘上一个调制因子 (1 - p_t)^γ其中p_t是模型对真实类别的预测概率γ是可调的聚焦参数。直觉是当模型对某样本已经预测得很好时p_t接近1调制因子 (1 - p_t)^γ 接近0这个样本的loss就被压得很低模型不再花太多精力去管它当模型对某样本预测得不好时p_t很小调制因子接近1loss保持在高位模型继续集中火力修正这些困难样本。配合上类别权重α就能同时解决“类别不均衡”和“难易样本不均”两个问题。我用Focal Loss做不平衡分类任务时γ一般取2α按类别逆频率设定。需要注意的是Focal Loss对γ很敏感γ过小和普通交叉熵区别不大γ过大模型可能完全忽略简单样本在早期训练时震荡得很厉害。建议先跑一版普通交叉熵作为基线再逐步调γ对比验证集表现。3.4 Label Smoothing给标签“松绑”还有一个经常和交叉熵搭配使用的技巧叫标签平滑Label Smoothing。one-hot标签在训练时非常“绝对”正确类别是1其他全是0。这会让模型为了把正确类别的概率推到1附近不断放大logit的差距容易造成过拟合也让模型的置信度过于极端。标签平滑的做法是把正确类别的目标从1改成 1 - ε同时把其余类别等分 ε/(K-1)K为类别数。ε一般取0.05到0.1。这样模型不再追求“非黑即白”的极端输出而是保留一定的不确定性泛化能力通常会更好。实现上也很简单PyTorch的CrossEntropyLoss在较新版本里直接支持label_smoothing参数不用自己手写标签变换。4. 实战进阶多任务、Masked Loss与自定义损失4.1 多任务学习的损失组合与权重平衡真实项目里很少只有一个输出。比如一个视觉模型既要检测目标框又要输出目标类别甚至还要预测一个属性分数。这种多任务场景下最常见的方式是把多个loss加权求和total_loss λ1 * L1 λ2 * L2 λ3 * L3听起来简单但λ怎么定是个让人头疼的问题。如果某个任务的loss天然就比其他任务大很多比如回归的loss是几十分类的loss是零点几那梯度就会被这个任务主导其他任务学不好。我最早做多任务时就只盯着总loss降后来发现某个子任务的指标一直不动一查才发现它的loss占比几乎可以忽略。比较土但有效的办法是先跑一两个epoch统计每个子任务loss的平均量级再按量级反比去设初始权重。更进阶一点的办法是用“不确定性加权”把每个任务都配上可训练的同方差不确定性参数σ然后用类似 L/(2σ^2) log(σ) 的方式来自动平衡权重。这样模型在训练过程中会自己调节各任务的梯度贡献省去了大量手工调参。不过也要提醒多任务loss的权重再平衡也只是“补偿手段”前提是任务之间确实共享底层特征。如果两个任务目标从根本上就矛盾比如一个任务要预测“是否成交”另一个任务要预测“是否点击”那再调权重也很难让两边同时变好这时候要考虑的是模型结构层面的设计而不是光盯着损失函数。4.2 序列任务中的Masked Loss在自然语言处理和序列预测任务里尤其要注意被填充padding的无效位置。比如一个batch里句子长度不一短的句子后面一般用0填充到batch内最大长度。这时候如果直接把所有位置的预测都算进loss填充位置的“预测”全是无意义的模型会花额外精力去拟合这些噪声目标拖慢训练。标准做法是Masked Loss先算出每个位置的单点loss然后乘以一个mask矩阵mask里有效位置为1、填充位置为0最后把mask位置求和后除以mask位置的总数得到当前batch的有效平均损失。这个细节看着小但在BERT类模型的预训练、机器翻译、语音识别里都会遇到。很多同学说自己的序列模型训练loss异常一查就是没做maskpadding位置疯狂贡献梯度。实现上不用自己造轮子PyTorch里有nn.CrossEntropyLoss的ignore_index参数专门用来忽略某个填充索引对应的标签位置。更复杂的场景也可以用F.cross_entropy(reductionnone)拿到逐元素loss再和mask相乘后手动取均值。4.3 自己写损失函数时的三条铁律有时候既要的业务指标没法直接用现成loss表达就得自己写一个。写自定义损失函数的时候我积累了几条铁律。第一尽量用框架自带的基础算子来组合不要在forward里写复杂的for循环或Python级别的逻辑。比如用torch.where代替if分支用矩阵运算代替逐元素循环。原因一是速度二是梯度能自动正确回传。你手动用Python加加减减实现可能逻辑没问题但显存占用和计算速度会差出几倍。第二注意数值稳定性。不要手动拆开softmax再算log因为当logit很大或很小时exp可能溢出得到inf而log(0)直接变成负无穷。应该优先用F.log_softmax或F.binary_cross_entropy_with_logits这类融合算子它们在内部已经做了数值稳定处理。第三写完之后一定要做梯度检查。可以先随机初始化一个小网络用单个样本跑一次反向传播再利用torch.autograd.gradcheck或者手动数值差分比较解析梯度和数值梯度是否一致。这个步骤能帮你抓住很多“看着loss在下降但其实梯度有错”的隐蔽bug。5. 损失函数的调试经验那些踩过的坑5.1 损失不下降问题出在哪损失曲线像一条水平线是初学者最常遇到的状况之一。这时候别急着换模型我一般按下面的顺序排查一遍先看数据对不对。标签是不是反了、有没有大量缺失值、数据标准化是否合理。如果数据本身就有问题损失函数再好也学不出来。再看损失函数计算范围。比如标签是2000个大类模型输出维度却写成了1000这种情况下PyTorch会直接报维度错误而如果输出维度大于或小于预期但没有精确匹配标签索引有时表现是“能跑但loss不降”。所以我会习惯性地打印一次logits和target的shape确认它们对得上。然后是梯度。给网络最前面几层的参数打开requires_grad并打印梯度范数如果发现梯度几乎为0可能是模型太深导致梯度消失或者前面某层被冻结了、输出没有连接到loss。如果梯度范数极大那就优先调低学习率或者做梯度裁剪。最后是初始化。有些人喜欢用复杂初始化但对初学者来说PyTorch默认初始化其实已经够用。如果网络很浅还出现loss不降先回到最简单的MLP加标准交叉熵验证pipeline是否正常再逐层加回复杂结构。5.2 损失变成NaN的常见原因NaN几乎人人都会碰到。我遇到最多的是三个原因学习率过大导致参数更新发散数据或标签里包含了无穷大值损失函数计算过程出现log(0)或0/0。学习率过大是最常见的解决方案很简单把学习率调低一到两个数量级或者用带warmup的调度器。指数发散往往有个前兆——loss在连续几个step里突然变成前沿数值然后猛增这时候去看梯度范数多半是天文数字。数据里的NaN或Inf容易被忽略。如果训练信号里混入一个NaN模型权重会迅速被污染后续所有输出全变NaN。建议在数据加载后加一步检查torch.isnan(data).any()和torch.isinf(data).any()能帮你节省不少排查时间。数值不稳定常见于自己手写损失函数时比如对softmax结果取log之前又手动做了一遍运算。解决办法我前面已经说过使用框架提供的稳定版本算子不要裸调log(softmax(x))。5.3 损失曲线正常但指标不动怎么办这种情况比loss不降更隐蔽训练loss在稳定下降精度、IOU、F1这些业务指标却基本不动。这时候最可能的解释是损失函数和业务指标之间存在“错位”。比如你用的是MSE但业务关心的是预测值是否落在某个区间内。MSE作为一个光滑的连续目标它更关注整体误差均值而业务指标更关心最终落在区间内的比例。模型可能在优化MSE时把大部分样本误差都压到了中间水平但边界附近的样本仍然被判错指标自然上不去。又比如分类任务里模型把多数类几乎全部分对loss下降主要来自少数类更精准但整体准确率被多数类主导看起来就是“loss降了acc没变”。这时候光看loss曲线是不够的要把混淆矩阵、每一类的指标都拉出来看。解决思路是把损失函数和业务目标拉近。既然业务关心F1分数那就试试用Focal Loss或者给少数类加权既然业务关心区间命中率那就试试量化分桶加辅助损失。损失函数不可能完美等价于业务指标但它能做得越靠近业务目标训练出来的模型越容易在最终评估时拿到好结果。5.4 一份可复用的损失函数排查速查表我把平时踩过的坑整理成了一张表遇到问题先对号入座能省不少时间。现象可能原因优先尝试的解法loss不下降训练精度也低学习率过小/过大、数据预处理问题打印梯度范数调学习率检查输入输出范围loss发散为NaN学习率过大、数据含NaN、数值不稳定调低学习率清洗数据改用稳定算子训练loss降但验证loss反弹模型过拟合加正则化、增强数据、Early Stoppingloss降但业务指标不动损失函数与业务目标错位分析混淆矩阵调整损失函数权重或类型验证loss正常但预测结果诡异标签编码不一致、类别索引错位检查标签映射和损失函数的class索引顺序多任务里某个子任务学不动该任务loss量级太小梯度被淹没按loss量级调整权重或改不确定性加权表格只解决表面定位真正要根治还得回到“损失函数定义是否合理”这个本源问题上去。最后再分享一个我自己的习惯不管用什么损失函数第一件事就是跑一个极小的batch比如4条样本关掉随机增强让模型过拟合这个batch看loss能不能降到接近0。如果不能说明损失函数代码或者梯度链路有问题这时候排查成本最低。等这步通过之后再打开数据增强跑完整训练集。这个习惯帮我挡掉过很多低级错误也推荐你试试。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询