ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深度学习损失函数详解:从MSE、MAE到SmoothL1的选型与实战

2026/9/16 19:24:29 拓冰建站 浏览量
深度学习损失函数详解:从MSE、MAE到SmoothL1的选型与实战 我们直接用标题做引子深度学习_损失函数(MSE、MAE、SmoothL1_loss...)。说实话我当年刚开始碰深度学习的时候也是被“损失函数”这四个字搞得够呛。网上教程一抓一大把但大多是甩三个公式、画两张图然后告诉你“MSE适合回归SmoothL1适合检测”至于为什么没人细讲。后来自己动手做项目、调模型、啃论文才慢慢琢磨明白损失函数这东西看着简单实则是整个训练过程的“指挥棒”。它往哪个方向指梯度就往哪里更新模型最后就长成什么样。这篇文章我就把这个话题彻底拆开聊从MSE、MAE、SmoothL1_loss的原理到代码实现、选型思路、训练排障一次性讲透。适合刚入门深度学习、正在做回归或目标检测实战项目、以及被损失曲线折磨得睡不着觉的朋友照着抄就行。1. 损失函数到底是什么为什么它决定模型“学成什么样”1.1 用考试打分来类比损失函数就是判卷标准我们先抛开公式用一个特别直觉的类比来理解损失函数。假设你训练一个模型就像培养一个学生。这个学生每次做预测考试答题老师要给他打分分数越低代表答得越差分数越高代表答得越好。学生拿到分数之后会反思“我哪里做错了”然后调整自己的解题思路争取下次分数更高。这里的“打分标准”就是损失函数学生调整思路的过程就是反向传播更新参数。关键在于用什么标准打分学生就会往什么方向努力。如果老师要求“误差越平方化惩罚越重”学生就会拼了命去把那些“错得离谱”的题目改对哪怕牺牲掉一些本来差不多对的题目。如果老师只关心“绝对差值”那学生面对一道怎么都算不对的怪题时可能会干脆放弃它因为多错一点少错一点分数差别不大。这就是MSE和MAE给人带来最直观的不同。放到模型训练里损失函数 L(y, ŷ) 告诉我们预测值 ŷ 和真实值 y 之间到底差了多少。训练的目标就是找到一组参数 θ让这个差距在所有样本上平均下来最小。用数学语言说就是 θ* argmin L(θ)而梯度下降就是一步步朝这个最小值走的过程。1.2 从公式看本质一个度量“差距”的函数严格来说损失函数也叫代价函数或目标函数它衡量的是模型预测分布与真实分布之间的差异。常见的写法是对单个样本loss l(y, ŷ)对整个训练集Loss (1/N) Σ l(y_i, ŷ_i)这里的 l(·) 就是具体某个损失比如 MSE 就是平方差MAE 就是绝对差SmoothL1 是两者结合。你可能会问为什么不能用“准确率”这种人类能直接理解的指标当损失函数原因是准确率、精确率这些指标是离散的、不可导的。比如判断“预测值是否等于真实值”要么对要么错没有任何中间过渡梯度根本传不过去。而损失函数必须可导梯度才能从最后一层一路传回网络每一层告诉每个参数“往哪个方向调、调多少”。所以损失函数不仅是模型学习的“标尺”更是深度学习中唯一能让“反馈信号”流动起来的通道。这就解释了为什么选错损失函数模型会训不动、训歪甚至直接爆炸。1.3 损失函数的地形图梯度决定模型更新方向如果我们把网络的全部参数当作一个多维空间中的坐标那么损失函数就是定义在这个空间上的一个“地形”——有山峰、有山谷、有平原。训练过程就是在这片地形上寻找最低点的过程。不同损失函数等于同一份数据在不同“重力场”里的投影地形完全不同。MSE 的地形在山谷底部附近比较平缓但离谷底远的地方斜率会变得特别大MAE 的地形则到处斜率一致不会因为离得远就陡峭SmoothL1 则是最“温柔”的那个远近都有合理的坡度。这就是为什么同样的数据、同样的网络只换一个损失函数收敛速度和最终效果会差别很大。明白了这一点再看下面逐个拆解各个损失函数思路就顺了。2. 回归损失函数逐个拆解MSE、MAE、SmoothL12.1 MSE均方误差惩罚离群点最“狠”的选手MSE 的全称是 Mean Squared Error公式长这样L_MSE (1/N) Σ (y_i - ŷ_i)²它的特点非常鲜明误差被平方了。如果一个样本预测差 0.1平方后只有 0.01另一个样本差 5平方后变成 25。这个“放大效应”会让模型把绝大多数注意力放在那些错得离谱的样本上。优点也很明显处处可导数学性质好优化起来比较稳定。误差大时梯度也大前期收敛很快。它的最优解是均值所以在数据噪声是高斯分布时MSE 是统计意义上的最优选择。但缺点同样要命对离群点过于敏感。举个实际例子你在做房价预测大部分房子价格在 100 万到 500 万之间但有一两套豪宅成交价 5000 万。MSE 会让模型拼命去拟合那套豪宅结果普通房子的预测反而变得一团糟。因为你平方之后那套豪宅带来的误差占了总损失的绝大部分。它对应的梯度是 ∂L/∂ŷ -2(y - ŷ)。你发现没有误差越大梯度越大参数更新步长就越猛。如果样本里混入一个极端离群点梯度可能直接把这个批次的其他样本“带偏”。这也是为什么很多老手在用 MSE 之前都会先做离群点清洗或标签裁剪。2.2 MAE平均绝对误差对离群点“免疫”但收敛费劲MAE 的全称是 Mean Absolute Error公式L_MAE (1/N) Σ |y_i - ŷ_i|它用的是绝对值所以误差是线性增长的。离群点造成的误差再大它对总损失的贡献也没有被“放大”。这就让 MAE 对离群点非常鲁棒哪怕数据里混进几个极端值模型也不会被它们牵着鼻子走。但 MAE 的缺点藏在它的导数里∂L/∂ŷ -1如果 ŷ y∂L/∂ŷ 1如果 ŷ y0 点处不可导。也就是说无论误差多大梯度的大小始终恒定在 1。样本预测差 0.1 是 1差 100 也是 1。这在训练后期就会出问题——当模型已经很接近真实值、误差很小时梯度依旧是 1参数还是按固定步长更新导致很难收敛到更精细的位置容易在最优解附近来回震荡。另外还有一个实际问题MAE 在 0 点不可导反向传播时会用到次梯度某些框架里的实现会额外做处理。早年间有些优化器对 MAE 的支持不如 MSE 稳虽然现在主流框架都没问题了但理解这个“病根”还是必要的。2.3 SmoothL1MSE 和 MAE 的“中和体”SmoothL1也叫 Huber Loss 在 δ1 时的特殊形式就是冲着 MSE 和 MAE 的缺点来的。它的公式是分段的当 |x| 1 时L 0.5 x²当 |x| ≥ 1 时L |x| - 0.5这里 x y - ŷ。这公式妙在哪我拆开给你看当误差比较小|x| 1时它退化成 MSE梯度会随着误差减小而减小模型可以在最优点附近精细收敛。当误差比较大|x| ≥ 1时它退化成 MAE梯度恒定不会因为某个离群点误差爆炸导致梯度爆炸训练更稳定。换句话说SmoothL1 同时拿到了“MSE 的精细收敛能力”和“MAE 对离群点的鲁棒性”。它唯一的缺点就是公式稍微复杂一点调参时要多照顾一个阈值参数 δ在 PyTorch 里叫 beta。δ 越大靠近原点的二次区域越大行为越接近 MSEδ 越小越接近 MAE。当年 Faster R-CNN 把 SmoothL1 引入目标检测框回归就是为了解决早期检测模型在训练初期梯度爆炸的问题。后面 YOLO、SSD 这些检测模型也都沿用了这个设计可以说它是目标检测里面“默认标配”的回归损失。2.4 别忘了分类任务的主角交叉熵损失虽然标题里写的是 MSE、MAE、SmoothL1但既然是深度学习就不能不聊交叉熵。你在热词里也看到了“交叉熵损失函数”“yolo损失函数”“llm预训练损失函数”这些高频搜索它们底层都离不开交叉熵。分类任务里模型输出的是一个概率分布比如图片是猫的概率 0.7、狗 0.2、鸟 0.1。真实标签是“猫”用 one-hot 表示成 [1, 0, 0]。交叉熵度量的是预测分布和真实分布之间的差异公式是L_CE -Σ y_i log(ŷ_i)二分类时简化为L_BCE -[y log p (1-y) log(1-p)]交叉熵有一个很多人津津乐道的性质预测越自信且越错惩罚越狠。比如真实类别是“猫”模型预测“猫”的概率只有 0.001那 log(0.001) 约等于 -6.9损失非常大但如果模型预测概率是 0.9损失就很小。这种非线性的惩罚机制让模型很快学会“不要过度自信地犯错”。很多新手会问“分类问题能不能用 MSE”理论上可以但实践中效果很差。原因是分类输出层通常接 Softmax把 MSE 和 Softmax 组合在一起时梯度会被 Softmax 的分母项“稀释”导致收敛极慢。而交叉熵和 Softmax 组合时梯度形式非常漂亮直接变成 ŷ - y预测减真实这也是几乎所有深度学习框架都默认“分类用交叉熵”的根本原因。2.5 一张表看明白常见损失函数对比损失函数公式梯度特点对离群点收敛表现常见场景MSE(y - ŷ)²误差越大梯度越大极敏感前期快、后期容易震荡高斯噪声下的回归、图像重建MAE|y - ŷ|梯度恒定鲁棒全程稳、后期难精细带大量离群点的回归SmoothL1分段0.5x² 或 |x|-0.5小误差梯度小大误差梯度恒定较鲁棒兼顾前期稳定与后期精细目标检测框回归、多任务回归交叉熵-ylog(ŷ)预测越错越“推得狠”天然适配概率分布与Softmax搭配收敛快图像分类、语义分割、LLM预训练3. 动手实现与可视化把损失函数“看”明白3.1 基于 NumPy 手写三个损失函数要理解一个损失函数最好的方式就是亲手写一遍。我用 NumPy 写了一份最简版本代码量很少但逻辑完整你在 Jupyter Notebook 里直接就能跑import numpy as np def mse_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) def mae_loss(y_true, y_pred): return np.mean(np.abs(y_true - y_pred)) def smooth_l1_loss(y_true, y_pred, delta1.0): diff y_true - y_pred abs_diff np.abs(diff) quadratic np.where(abs_diff delta, 0.5 * diff ** 2, delta * (abs_diff - 0.5 * delta)) return np.mean(quadratic) # 试一下 y_true np.array([1.0, 2.0, 3.0]) y_pred np.array([1.2, 1.8, 5.0]) print(MSE:, mse_loss(y_true, y_pred)) print(MAE:, mae_loss(y_true, y_pred)) print(SmoothL1:, smooth_l1_loss(y_true, y_pred))运行结果大概是这样MSE 对那个差 2.0 的样本惩罚特别重MAE 相对平和SmoothL1 则介于两者之间。自己动手跑一次比看十遍公式都有用。3.2 画出损失曲线和导数曲线理解了公式和代码还不够直观。我建议你再画两张图一张是损失函数随误差变化的总曲线一张是它的导数曲线。通过图形你就能看到MSE 在误差大时梯度吓人MAE 梯度像条直线SmoothL1 在零点附近最平滑。import matplotlib.pyplot as plt x np.linspace(-3, 3, 400) y_mse 0.5 * x ** 2 y_mae np.abs(x) y_smooth np.where(np.abs(x) 1, 0.5 * x ** 2, np.abs(x) - 0.5) dy_mse x dy_mae np.sign(x) dy_smooth np.where(np.abs(x) 1, x, np.sign(x)) fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(x, y_mse, labelMSE) axes[0].plot(x, y_mae, labelMAE) axes[0].plot(x, y_smooth, labelSmoothL1) axes[0].set_title(Loss vs Error) axes[0].set_xlabel(Error (y - y_pred)) axes[0].set_ylabel(Loss) axes[0].legend() axes[0].grid(True) axes[1].plot(x, dy_mse, labelMSE derivative) axes[1].plot(x, dy_mae, labelMAE derivative) axes[1].plot(x, dy_smooth, labelSmoothL1 derivative) axes[1].set_title(Derivative vs Error) axes[1].set_xlabel(Error (y - y_pred)) axes[1].set_ylabel(Gradient) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show()看到导数曲线那一刻你就明白我在前面说的“SmoothL1是大误差稳定、小误差精细”是什么意思了误差从 0 到 1 之间它的导数从 0 平滑过渡到 1误差超过 1 之后导数恒为 1不再增长所以大误差不会带来大梯度训练稳如老狗。3.3 在 PyTorch 里实际使用这几个损失函数如果你是真要训练模型没必要自己实现PyTorch 已经内置得明明白白import torch import torch.nn as nn # 语法先预测量再目标值和手写版本顺序相反 loss_mse nn.MSELoss() loss_mae nn.L1Loss() loss_smooth_l1 nn.SmoothL1Loss(beta1.0) loss_huber nn.HuberLoss(delta1.0) # 和 SmoothL1Loss 本质一致 y_pred torch.tensor([1.2, 1.8, 5.0]) y_true torch.tensor([1.0, 2.0, 3.0]) print(loss_mse(y_pred, y_true)) print(loss_mae(y_pred, y_true)) print(loss_smooth_l1(y_pred, y_true))功能函数版本也有torch.nn.functional.mse_loss、torch.nn.functional.l1_loss、torch.nn.functional.smooth_l1_loss适合你在自定义训练循环里直接调用。还有一个细节SmoothL1Loss默认 beta1.0HuberLoss默认 delta1.0它俩在数学上是同一个东西只是命名不同。3.4 小实验给数据加一个离群点看模型怎么变为了让你更直观感受选错损失函数的代价我强烈建议做一个对比小实验。用一组带噪声的线性数据分别用 MSE 和 MAE 训练一个最简单的线性回归模型然后手动往数据里塞一个离群点。你会发现MSE 模型会被离群点“拽”过去拟合直线明显偏离大多数点的趋势。MAE 模型几乎不为所动拟合直线依然贴着大多数点。实验代码很长这里给个核心思路用两层nn.Linear(1, 1)训练 200 个 epoch分别记录每个 epoch 的损失最后打印出两条拟合直线和真实直线对比。我第一次跑完这个实验时才彻底理解为什么做数据清洗的人总说“离群点比脏数据更可怕”。4. 实战选型你的任务到底该选哪个损失函数4.1 回归任务先看数据再看精度要求如果你做的是纯回归任务比如预测房价、预测温度、预测销售额选型逻辑主要看两点数据里有没有离群点以及你对“小误差”还是“大误差”更敏感。数据比较干净误差近似高斯分布选MSE。这是统计理论里最大似然估计下的最优选择收敛也稳。数据里可能有极端值或者你不想让个别大误差样本主导训练选MAE或SmoothL1。MAE 更“铁血”SmoothL1 更“温柔”。绝大多数时候我会首推SmoothL1。它在前期不容易被离群点带崩后期又能精细收敛是一个“女孩子看了不会哭男孩子看了不会炸”的稳健选择。如果你做的任务评估指标是 MAE比如某些气象预测但训练用了 MSE那你就要留意了模型在训练时会拼命拟合大误差样本可你最后考核的是平均绝对误差这两者有时候会打架。最好的办法是训练和评估尽量用一致或至少同源的度量方式。4.2 目标检测为什么回归分支默认用 SmoothL1目标检测模型YOLO、Faster R-CNN、SSD 这类的损失函数通常是多任务组合分类分支用交叉熵回归分支用 SmoothL1。回归分支预测的是边界框的中心坐标、宽高偏移量这些值通常已经归一化处理过范围不会太大。但训练前期网络还没收敛预测框可能离真实框非常远误差很大。如果用 MSE一段极端错误预测会产生巨大的梯度把整个模型带得乱七八糟。SmoothL1 的“大误差梯度封顶”特性正好能在训练初期稳住船头。这也是为什么几乎每个检测框架的源码里你都能看到 smooth_l1_loss 的身影。后面 YOLOv5、YOLOv8 虽然换成了 CIOU 这类 IoU-based 损失但 SmoothL1 在通用回归分支里依然是默认选择之一。4.3 多任务学习和 LLM 预训练里的损失函数组合到了多任务场景事情会微妙起来。比如一个模型同时做目标检测和分类总损失往往是 L L_cls λ * L_reg。这里的 λ 是一个加权系数用来平衡两个任务的贡献。λ 设大了模型重点学回归设小了模型重点学分类。很多新手拿到别人代码不知道 λ 为什么要取 0.5、1.0 还是 5.0其实没有什么万能答案就是一个先粗调再细调的过程。我自己的习惯是先设成 1.0 跑一版看两个任务的损失谁降得慢再把权重往慢的那个方向偏一点。再看大语言模型领域GPT、LLaMA 这些模型在预训练的时候用的也是交叉熵损失只不过作用在 token 级别每个位置的输出都要预测下一个 token然后算平均交叉熵。你在训练记录里看到的 “loss” 就是所有 token 的平均损失。模型要想学会“接话”本质就是把每个词在上下文里的条件概率分布估准这依然是交叉熵的活儿。理解这一点你就明白为什么热词榜里“llm预训练损失函数”会长期霸榜——不管模型多大损失函数的基本原理没变。4.4 损失函数的加权、阈值与“降权”技巧关于损失函数的调优还有几个实战细节值得单独说样本不均衡时可以在损失函数里给每个样本加权重。PyTorch 的损失函数基本都有weight参数或者你在计算时手动乘权重。比如正负样本比例 1:99那把正样本的损失权重设成 99负样本设成 1模型才不会“学成个复读机只会输出多数类”。SmoothL1 的阈值 δbeta可以调。如果你想让它更贴 MAE就把 β 调小比如 0.5想让它更贴 MSE就调大比如 2.0。但多数场景下默认 1.0 已经够用。损失函数和评估指标不一致时要警惕“损失一直在降但指标不动”的假象。比如你训练用 MSE、评估用 R²MSE 降了 R² 可能还在原地踏步因为 R² 还受数据方差影响。这时候与其怀疑网络结构不如回头看一眼“是不是损失函数选错了”。5. 训练中常见的损失函数异常与排障记录5.1 损失变成 NaN原因和排查思路训练到一半loss 突然变成 None 或者 NaN这大概是每个深度学习玩家都经历过的噩梦。我把它最可能的原因和排查顺序总结成一张表现象最可能原因快速验证方法解决思路loss 变 NaN且有规律比如某个 epoch 后学习率过大梯度爆炸打印每一层梯度的范数降低学习率或加梯度裁剪clip_grad_norm_某几个 batch 后必现 NaN数据里混入 NaN 标签或特征检查输入数据np.isnan().sum()清洗数据补齐或过滤异常样本用 FP16 混合精度时出现 NaN动态损失缩放处理不当查看 GradScaler 的 scale 值更新 PyTorch 版本检查 loss scaling 策略MSE 配极端离群点导致 NaN平方后数值溢出打印单 batch 的 loss 数值换成 SmoothL1或做标签裁剪排查 NaN 有个笨但很有效的方法先固定随机种子再加一行所有网络输出的日志看 NaN 是从哪一层开始出现的。只要找到“第一现场”问题就解决了一半。5.2 损失一直不下降别急着调参先检查数据相比 NaN更折磨人的是损失“稳如死水”几十个 epoch 纹丝不动。这时候很多人第一反应是换模型、加层数、调学习率但我建议你先做三件事检查输入特征是否做了归一化。没归一化的特征会让梯度在不同维度上尺度差异巨大优化器很容易在原地打转。检查标签的分布。如果标签是几百个数值但网络输出被激活函数限制在 [0,1]那模型一开始就“够不着”目标损失自然降不了。在训练最开始打印一次“初始损失”。如果初始损失和“瞎猜”的损失差很远说明前向传播的数值范围就有问题这时候调整网络初始化或输出层激活函数才是关键。有一个非常实用的做法先用一个 batch 的数据试跑看看模型能不能过拟合这个 batch。如果连一个 batch 都过拟合不了说明网络结构和损失函数大概率有 bug如果能过拟合再去谈收敛慢、泛化差的问题。5.3 损失下降但指标不涨优化目标和评估指标在打架这个场景多见于做分类或检测的朋友。训练时交叉熵一直在降但验证集准确率、mAP 就是上不去。最常见的原因是训练损失和评估指标天然就不完全一致。比如目标检测总损失是 L L_cls 0.5 * L_reg等模型快收敛时分类损失还在降但它对 mAP 的提升已经微乎其微这时候损失曲线看着挺好实际性能早就进入平台期。又比如类别严重不均衡时模型学会“全预测为多数类”交叉熵也能降得很低但你关心的少数类指标惨不忍睹。解决办法也不复杂别只盯着总损失把分类损失、回归损失分开记录训练过程中每几个 epoch 就做一次完整评估以评估指标为准来决定是否早停或调整学习率。我现在做项目统一用 TensorBoard 或 Weights Biases 把 train loss、val loss、mAP 画在同一张图里问题往往一眼就能看出来。5.4 损失震荡得厉害学习率、batch size 和损失函数的关系损失曲线像心电图一样上下乱跳也是高频问题。原因通常是学习率太大参数在最优解附近反复横跳最典型的修复办法是使用学习率衰减或余弦退火。batch size 太小每个 batch 的数据分布差异大梯度方向不稳定。可以试试加大 batch size或者把梯度累计起来模拟大 batch。SmoothL1 的阈值和任务不匹配如果误差本身大部分都落在阈值附近反复横跳也会造成不稳定这时可以微调 beta 或切回 MSE。有些损失函数的梯度本身就比较“冲”比如 MAE 的恒定梯度在训练后期容易导致震荡换成 SmoothL1 往往能缓解。我个人解决震荡的顺序是先降学习率看效果不行再调 batch size最后才动损失函数。因为前两者更容易操作而且不会改变问题的数学定义。5.5 损失函数排障速查表症状优先检查项建议操作loss NaN梯度范数、数据是否含 NaN、FP16梯度裁剪、数据清洗、降学习率loss 不降归一化、标签范围、初始化归一化、改输出激活函数、预训练迁移loss 降但指标不动损失与指标定义是否一致分开记录各任务损失以评估指标为准loss 震荡学习率、batch size、阈值降学习率、加大 batch size、调整 beta收敛特慢损失函数梯度特性试试 SmoothL1 或更换优化器工具箱里常备一套排障脚本比任何模型结构都管用。我的做法是训练脚本里直接集成“NaN 检测 梯度检测 损失记录”一旦异常自动停止并保存断点。这套东西帮我在实际项目里省了无数时间。6. 写在最后我的几点实战体会说实话我对损失函数的重视是从一次训练事故开始的。当时做一个目标检测小模型图省事直接用了 MSE 当回归损失结果训练前两个 epoch 损失直接飞到了几万紧接着就是 NaN。后来换成 SmoothL1同一个模型、同一批数据损失稳步下降20 个 epoch 后 mAP 反而比之前精心调参的效果还好。那一次之后我彻底明白模型好不好网络结构只占一半另一半在损失函数和它背后代表的数据问题上。踩过这么多次坑之后我现在的习惯是拿到任何新任务先花半小时把数据分布摸清楚尤其看有没有离群点、标签范围是什么、噪声是什么形态然后再去定损失函数。数据是“食材”网络是“锅”损失函数是“火候”。食材不新鲜锅再好、火再旺也白搭火候不对好食材一样被炒糊。愿你在训练路上少走几次弯路。