ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

L1、L2与Smooth L1损失函数深度对比:从数学原理到工程选型

L1、L2与Smooth L1损失函数深度对比:从数学原理到工程选型 先聊点实际的。我在做目标检测和回归项目的时候被损失函数坑过不止一次。最典型的一幕是模型训练到一半loss 曲线突然拉出一条尖刺然后整条曲线都回不去了还有一种情况是 loss 看起来降得很平稳但验证集上的指标就是纹丝不动。后来我才意识到很多问题的根源不在网络结构而在那个每天都会见到、却常常被随手选中的损失函数上。L1 loss、L2 loss、smooth L1 loss这三个名字在回归任务里几乎是绕不开的。无论是做人脸关键点检测、车辆速度预测、深度估计还是目标检测里的边界框回归你都要从它们中间挑一个当模型的“裁判”。这篇博客就把这三兄弟彻底讲透它们的数学本质、梯度行为、对异常值的敏感度以及在真实项目里该怎么选、怎么调、有哪些坑。这篇文章适合刚入门的同学理解基础也适合已经跑过几个项目、想回头搞清楚“为什么当时用A不用B”的朋友。1. 损失函数在回归任务中的角色它不是公式是模型的指挥棒1.1 回归任务到底在优化什么回归任务的目标很简单给定输入预测一个连续值。比如根据房屋面积预测房价根据驾驶画面预测转向角度根据人脸图片预测关键点坐标。模型输出的不是一个类别标签而是一个实数或者一组实数。那模型怎么知道自己的预测好不好这时候损失函数就上场了。它的作用用一个不恰当但很贴切的比喻来说它就是老师手里的红笔。模型每次做出一个预测损失函数就在预测值和真实值之间画一个“差距分数”。分数越低说明预测越接近真实分数越高说明错得越离谱。训练过程本质上就是不断调整网络参数让这个分数越来越小。这句话听起来好像不值钱但我想强调的是其中容易被忽略的一点损失函数的选择直接决定了模型会学到什么、忽略什么。你用 L2 loss模型就会优先处理那些误差特别大的样本你用 L1 loss模型对所有样本的“重视程度”是一样的。这就像同一个班级有的老师喜欢抓尖子生冲刺满分有的老师喜欢照顾整体平均分最后教出来的学生画像完全不一样。1.2 回归和分类的损失函数为什么不能混用很多新手会问为什么不直接拿分类任务里的交叉熵损失来训练回归模型简单说交叉熵衡量的是两个概率分布之间的差异它要求模型的输出能够被解释成一个概率分布。回归任务里我们预测的是一个连续数值不是概率硬套交叉熵在数学上说不通实践中也没法训练。在深度学习里损失函数的选择跟任务类型强绑定。分类任务看交叉熵回归任务看 L1/L2 这类基于距离的损失。这与 LLM 预训练阶段的损失函数选择逻辑也是一致的——语言模型本质上是在预测下一个 token 的概率所以用的是交叉熵而不是用 L1 或 L2 去算生成文本和真实文本的“距离”。有些做 NLP 的朋友在尝试用回归损失做生成任务时发现效果很差原因就在这里你对齐的“度量方式”本身就不匹配任务的目标。那回归任务里的 L1、L2、smooth L1 到底有什么区别别急下面逐个拆开看。2. L1 lossMAE简单粗暴但对谁都一视同仁2.1 数学定义与最直观的理解L1 loss 的公式是[ L |y - \hat{y}| ]其中 (y) 是真实值(\hat{y}) 是模型的预测值。如果你接触过统计或者机器学习可能更熟悉它的另一个名字MAEMean Absolute Error平均绝对误差。严格来说MAE 是在整个数据集上算平均而 L1 loss 通常指单个样本的绝对误差但日常交流里大家经常混着用意思很明确。直觉上L1 loss 就是测“你和目标之间差多远”不关心方向只看距离。比如真实温度是 25 度模型预测了 23 度误差是 2预测了 30 度误差是 5。这两个误差的“惩罚”就是 2 和 5没有任何放大或者缩小。在 PyTorch 里它对应的接口是torch.nn.L1Loss。手写的话就一行def l1_loss(pred, target): return torch.abs(pred - target).mean()2.2 L1 loss 的梯度行为恒定不变L1 loss 的梯度非常有意思。对参数求梯度的时候误差的导数会传播回来而 (|x|) 这个函数的导数在非零点是一个常数(x 0) 时为 1(x 0) 时为 -1。换句话说无论预测值和真实值之间的差距是一万还是零点零零一L1 loss 传回来的梯度大小都恒定为 1。这带来两个非常直观的后果。第一个后果是它对异常值不敏感。假设一批样本里有一个极端离群点真实值是 100模型预测值是 5误差 95另一个正常样本误差是 3。在 L1 loss 看来前者的惩罚只是后者的三十多倍不会出现“一个离群点碾压所有正常样本”的情况。第二个后果是它在训练后期可能会抖。当预测值已经很接近真实值误差只有 0.01 的时候L1 loss 的梯度依然还是 ±1而不是 0.01。这意味着模型在收敛到最优解附近时每次更新的步长不会因为误差变小而自动缩小容易在最优解附近来回震荡。2.3 从统计视角看 L1它是在拟合中位数还有个可能颠覆认知的事实用 L1 loss 训练回归模型等价于在逼近条件中位数而不是均值。这个在统计学里是有严格结论的——最小化绝对误差和得到的是样本中位数。这在实践中的含义很具体如果你的数据里有大量噪声和异常值L1 loss 会让模型学到一个“中间偏稳健”的预测结果不会因为少数极端值而把整个预测曲线拽偏。反过来说如果数据本身是干净的高斯分布L1 loss 的收敛速度和最终精度通常不如 L2 loss。3. L2 lossMSE回归任务的默认选择但它不是万能的3.1 数学定义与直觉理解L2 loss 的公式是[ L (y - \hat{y})^2 ]也就是预测值和真实值差值的平方。如果对数据集里所有样本取平均就得到了 MSEMean Squared Error均方误差。注意L2 loss 在实现的时候到底除不除以 2取决于框架定义。PyTorch 的torch.nn.MSELoss默认是 ( (y - \hat{y})^2 ) 再求平均没有除以 2很多教材和推导里为了让导数是 ( \hat{y} - y ) 而不是 ( 2(\hat{y} - y) )会写成 ( \frac{1}{2}(y - \hat{y})^2 )。这一半的差异不会影响优化方向只会影响学习率对梯度的缩放倍数但如果你在调参时换过 loss 的实现可能就需要相应调整学习率。直觉上L2 loss 是“差的平方”。这个平方操作带来一个关键变化误差越大被放大得越厉害。预测值和真实值差 1 时惩罚是 1差 3 时惩罚是 9差 5 时惩罚是 25。误差翻 5 倍惩罚翻 25 倍。PyTorch 里的用法import torch.nn as nn mse_loss nn.MSELoss() loss mse_loss(pred, target)或者手写def l2_loss(pred, target): return ((pred - target) ** 2).mean()3.2 L2 loss 的梯度行为误差越大梯度越猛L2 loss 的梯度是[ \frac{\partial L}{\partial \hat{y}} 2(\hat{y} - y) ]梯度的大小和误差成正比。误差为 10 时梯度是 20误差为 0.1 时梯度是 0.2。这意味着两件事。第一在训练初期如果模型初始化得不好预测值和真实值差距很大梯度会非常大可能导致参数更新过猛loss 直接飞掉。我在训练一个速度预测模型时踩过这个坑输入特征没有做归一化真实值范围在 0 到 100 之间结果训练刚开始 loss 就冲到了几千然后梯度爆炸整个模型废掉。后来把标签做了标准化误差从 100 级别降到了个位数级别训练才稳定下来。第二在训练后期当预测已经很接近真实值误差只有 0.01 时梯度也只有 0.02 左右。这时候网络参数更新非常缓慢收敛过程会变得细碎。好处是不容易震荡坏处是如果学习率不够大最后那一段收敛可能很磨人。3.3 从统计视角看 L2它是在拟合均值L2 loss 在统计上对应的是均值回归。也就是说在最小化平方误差的模型下给定输入时模型的最优预测是训练数据中该输入对应输出的条件均值。这是最大似然估计在高斯噪声假设下的自然结果——当你假设误差服从均值为零的高斯分布时最大化似然等价于最小化平方误差。这带来一个无法回避的问题均值对异常值极其敏感。比如 5 个样本的真实误差分别是 1、1、1、1、100那么均值是 20.8中位数是 1。用 L2 loss模型会被那个 100 的离群点强烈影响用 L1 loss模型几乎不会在意它。在数据质量较差的场景下直接用 MSE 很容易训练出一个被少数坏数据带偏的模型。4. 正面硬刚L1 和 L2 的对比连梯度带鲁棒性全摆出来前两节分别说了 L1 和 L2 的特点下面把它们放在一起做一个系统性对比。这里引用一个在业内流传甚广的图横轴是预测值与真实值的差 (\delta)纵轴是损失值。L2 是一条抛物线L1 是两条直线组成的 V 形。两条曲线在 (\delta 0) 处相交但离开零点后迅速分道扬镳。这张图可以说一目了然但背后的信息值得逐条展开。4.1 损失值对比离群点的影响差异到底有多大误差 (\delta)L1 lossL2 loss0.10.10.0111133955251010100这个表很直观误差小于 1 时L2 loss 的惩罚反而比 L1 小因为它把误差“缩小”了误差大于 1 时L2 loss 的惩罚迅速超过 L1而且是平方级增长。到了误差为 10 的时候L2 的惩罚是 L1 的 10 倍。这直接导致了两种损失函数对异常值截然不同的态度。如果一批正常样本的误差在 0.5 到 2 之间有一个异常样本的误差是 10那么在 L2 loss 的视角里这个异常样本的 loss 可能占整体 loss 的 80% 以上模型会把绝大部分“注意力”放在修正这个异常样本上其他正常样本的学习被压制。而在 L1 loss 的视角里异常样本只是比正常样本大几倍而已模型还是会把大量精力花在所有样本上。4.2 梯度对比收敛过程的速度与稳定性差异误差 (\delta)L1 梯度L2 梯度0.110.211231610120注意一个小细节L1 的梯度在 (\delta 0) 处未定义这也解释了为什么 L1 loss 在实现时通常会加一个极小值或者采用次梯度近似。不过在实际训练中预测值精确等于真实值的情况几乎不会发生所以这个不可导点的影响其实很有限。真正影响训练效果的是梯度随误差的变化趋势误差大的时候L2 的梯度远大于 L1所以训练初期 L2 收敛得更快。这也是很多人默认用 MSE 的原因模型能在前面几轮快速把大误差压下来看起来 loss 下降得很漂亮。误差小的时候L2 的梯度远小于 L1所以训练后期 L2 的收敛速度会明显放缓。L1 则会以恒定步长继续“磨”但可能在最优点附近反复横跳无法精细收敛。误差刚好在 1 附近的时候两者的梯度几乎一样这也是为什么很多任务把误差归一化到接近 1 的量级后再算损失。正因为 L1 和 L2 都有各自的硬伤研究者们才会琢磨能不能搞一个损失函数前期像 L2 一样快速收敛后期像 L1 一样不受异常值干扰这就引出了 smooth L1 loss。5. Smooth L1 Loss回归任务里的“取长补短”选手5.1 它从哪来为什么被发明出来Smooth L1 loss 最早在 Fast R-CNN 这篇论文里被明确提出用来做目标检测里的边界框回归。为什么要专门设计一个新的损失函数因为目标检测的边界框回归有一个特点训练初期如果 anchor候选框和真实框差距很大误差可能到几十甚至上百个像素训练后期框已经对齐得差不多了误差只有零点几。如果只用 L2 loss前期梯度爆炸风险极高如果只用 L1 loss后期收敛不精细。Smooth L1 就是专门针对这种“前期误差大、后期误差小”的场景设计的。后来YOLO 系列等目标检测算法也纷纷在边界框回归部分使用 smooth L1 loss。可以说只要涉及目标检测的回归分支smooth L1 几乎成了默认选项。5.2 数学定义一个分段函数Smooth L1 loss 的定义是[ \text{smooth}_{L1}(\delta) \begin{cases} 0.5 \delta^2, \text{if } |\delta| 1 \ |\delta| - 0.5, \text{otherwise} \end{cases} ]其中 (\delta y - \hat{y})。这个分段函数有两个“零件”当误差绝对值小于 1 时使用二次函数 (0.5\delta^2)。注意这个 0.5 的系数不是随便写的是为了让分段点在 (\delta \pm 1) 处两段函数的取值和导数都连续。在 (|\delta| 1) 时二次函数的值是 0.5一次函数 (|\delta| - 0.5) 的值也是 0.5二次函数的导数是 (\delta)在 (\delta 1) 时为 1一次函数的导数是 (\pm 1)两边刚好衔接。这个设计非常精巧避免了一个参数更新时因损失函数在不同区域的突变而导致的不稳定。当误差绝对值大于等于 1 时使用线性函数 (|\delta| - 0.5)。此时梯度恒定为 ±1不会因为误差增大而梯度爆炸。5.3 三种损失放在一起对比特性L1 lossL2 lossSmooth L1 loss误差大时梯度恒为 1与误差成正比可能爆炸恒为 1误差小时梯度恒为 1易震荡与误差成正比平缓收敛与误差成正比平缓收敛对异常值敏感度不敏感非常敏感较不敏感可导性(\delta0) 处不可导处处可导处处可导典型应用鲁棒回归、关键点检测一般回归、信号处理目标检测边框回归、关键点检测从表格能看出smooth L1 loss 本质上是在“大误差时学习 L1 的稳健”在“小误差时学习 L2 的精确”。它自己恰好躲开了 L1 在零点不可导的毛病也躲开了 L2 对异常值平方放大的毛病。这种“缝合怪”思路现在看可能觉得理所当然但在当年Fast R-CNN 的作者能做出这个选择确实是基于工程实践的深入思考而不是拍脑袋想出来的。5.4 Smooth L1 其实可以看得更深一点如果把 smooth L1 loss 的分段阈值从 1 换成一个任意值 (a)就变成了 Huber loss 的一种形式。Huber loss 的定义是[ L_a(\delta) \begin{cases} \frac{1}{2}\delta^2, \text{if } |\delta| \le a \ a(|\delta| - \frac{1}{2}a), \text{otherwise} \end{cases} ]超参数 (a) 控制了“二次区”的范围。(a) 越大损失函数越像 L2 loss(a) 越小越像 L1 loss。Smooth L1 loss 本质上是 (a1) 的 Huber loss。这个视角有什么用如果项目里发现 smooth L1 loss 在某个数据集上表现不够好可以试着调整这个阈值 (a)而不是直接换损失函数。比如当数据里的异常值很多时把阈值调小让损失函数更快进入线性区增强鲁棒性当数据比较干净时把阈值调大让模型在更广的范围内享受 L2 的平滑梯度。6. 实操环节用 PyTorch 复现三种损失并做实验对比6.1 内置接口与手写实现PyTorch 里三个损失函数都有现成接口import torch import torch.nn as nn # 预测值和真实值 pred torch.tensor([2.5, 0.0, 3.1], requires_gradTrue) target torch.tensor([3.0, 0.2, 2.0]) l1_loss nn.L1Loss() mse_loss nn.MSELoss() smooth_l1_loss nn.SmoothL1Loss(beta1.0) # 注意 PyTorch 里参数名是 beta默认 1.0 print(l1_loss(pred, target)) # L1 loss print(mse_loss(pred, target)) # L2 loss print(smooth_l1_loss(pred, target)) # Smooth L1 lossPyTorch 的SmoothL1Loss有一个参数beta也就是公式里的那个分段阈值默认值是 1.0。beta的物理意义是“误差达到多大时从二次函数切换到线性函数”。在某些版本里PyTorch 还提供了一个小的数值稳定处理避免梯度在 beta 附近出现奇异行为。实际调参时beta这个参数值得试几个不同的值它比换一个损失函数的“动静”要小得多但效果可能非常明显。手写实现也不难def l1_loss(pred, target): return torch.abs(pred - target).mean() def l2_loss(pred, target): return ((pred - target) ** 2).mean() def smooth_l1_loss(pred, target, beta1.0): diff torch.abs(pred - target) loss torch.where(diff beta, 0.5 * (diff ** 2) / beta, diff - 0.5 * beta) return loss.mean()注意PyTorch 官方实现里在beta参数存在的情况下二次项是 (0.5 \times \delta^2 / \beta) 而不是 (0.5 \times \delta^2)。这是为了确保分段点在 (\delta \beta) 处导数连续。默认beta1.0时两者等价。6.2 带离群点的回归实验为了让对比更直观可以自己构造一个小实验。生成一批符合近似线性关系的数据加上一个高斯噪声然后人为加入几个离群点。import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 生成数据 np.random.seed(42) X np.linspace(-3, 3, 100) y_true 2 * X 1 noise np.random.normal(0, 0.5, sizeX.shape) y y_true noise # 人为加入 5 个离群点 outlier_idx np.random.choice(len(X), size5, replaceFalse) y[outlier_idx] y[outlier_idx] np.random.normal(0, 10, size5) X_t torch.tensor(X, dtypetorch.float32).view(-1, 1) y_t torch.tensor(y, dtypetorch.float32).view(-1, 1) dataset TensorDataset(X_t, y_t) dataloader DataLoader(dataset, batch_size16, shuffleTrue)然后定义一个非常简单的单层线性模型分别用三种 loss 训练足够的轮次def train_with_loss(loss_fn, epochs200, lr0.01): model nn.Linear(1, 1) optimizer torch.optim.SGD(model.parameters(), lrlr) losses [] for epoch in range(epochs): epoch_loss 0.0 for xb, yb in dataloader: optimizer.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() optimizer.step() epoch_loss loss.item() * len(xb) losses.append(epoch_loss / len(dataset)) return model, losses model_l1, losses_l1 train_with_loss(nn.L1Loss()) model_l2, losses_l2 train_with_loss(nn.MSELoss()) model_sl1, losses_sl1 train_with_loss(nn.SmoothL1Loss(beta1.0))把三条 loss 曲线画出来你会发现 L2 loss 的训练曲线在初期可能冲得很高之后降得快但最终学到的直线会明显被离群点“拽”歪L1 loss 和 smooth L1 loss 学到的直线会更接近真实的那条线y 2X 1。6.3 怎么画一条“能看”的损失曲线现在很多人做 YOLOv8 或者其他模型训练时会画损失函数曲线图来观察训练状态。这里有一个经验性的建议不要只画训练集上的 loss。训练 loss 会疯狂下降但这只能说明模型在死记硬背。真正值得画的是验证集上的 loss 曲线它才能反映泛化能力。如果训练 loss 下降但验证 loss 先降后升说明过拟合了如果训练 loss 从一开始就不降说明大概率是学习率没调好、数据没归一化、或者损失函数选错了量级。画曲线时还有一个实用技巧如果曲线太抖可以做一个滑动平均也叫 EMA 平滑。比如每隔 5 个 iteration 记录一次 loss然后对这些记录做指数滑动平均。这个操作不会改变曲线的趋势但会让图看起来干净很多也更容易判断模型是否真的收敛了。7. 回归任务损失函数选型给个能直接抄的作业7.1 看数据噪声和离群点多不多这可能是最粗粒度但最有效的一个判断维度。数据干净噪声接近高斯分布优先用 MSEL2 loss。它收敛快、精度高而且和最大似然估计的理论完全契合。数据有离群点或者标签偶尔标错优先用 L1 loss 或者 smooth L1 loss。它们对异常值不敏感不会因为少数坏数据把整个模型带偏。不确定数据质量想找一个稳妥的默认选项smooth L1 loss 是很好的默认选择。它的表现通常不会比 MSE 差太多但在异常值场景下会稳健得多。7.2 看任务目标检测、关键点检测、深度估计各有偏好在目标检测的边界框回归里smooth L1 loss 几乎是标准配置。YOLO 系列在回归分支用的大多是 smooth L1原因在于边界框回归的误差绝对值变化范围实在太大从几个像素到几百像素都有。L2 loss 在这种场景下很容易梯度爆炸L1 loss 收敛精度又不够。在人脸关键点检测、人体姿态估计这类任务里常会对坐标做归一化处理比如把像素坐标缩放到 [0, 1] 区间然后用 L1 loss 或者 smooth L1 loss。这里使用 L1 的动机同样是对离群点不敏感因为关键点标注本身就带有一定的主观误差和标注噪声。在深度估计任务中L2 loss 依然是常见选项因为深度值的分布比较连续误差通常不会被极端离群点主导。不过也会有工作直接用 smooth L1 或者结合两者的复合损失说明损失函数的选择和任务的数据分布高度相关。7.3 对损失函数做加权组合也是一种常见解法现实中不一定非要三选一。很多项目会把损失函数做加权组合。比如目标检测里分类分支用交叉熵损失回归分支用 smooth L1 损失然后把两项加起来( L L_{\text{cls}} \lambda L_{\text{reg}} )。这里的 (\lambda) 是超参控制回归损失在总损失里的权重。YOLOv8 在损失函数设计上也保持了这种复合思路分类和回归分别用不同的损失函数再配合一些额外的结构使得训练更平滑。多任务学习里更常见一个网络同时预测分类和回归每个任务各带一个损失然后加权求和。这种情况下损失函数本身是否匹配每个子任务的特性就非常重要。如果你在回归分支用了 L2 loss但数据里异常值多那整个多任务训练的梯度都可能被回归分支的异常值带偏。7.4 补充几个变种Huber loss、Log-Cosh、Fair loss除了上面三个还有一些衍生损失函数在一些比赛和特定任务中出现频率不低。Huber loss前面提过其实就是带可调阈值 (a) 的 smooth L1。scikit-learn和XGBoost里都有相关实现常用于带离群点的回归。Log-Cosh loss( \log(\cosh(\delta)) ) 的一种做法是先用一个较小的近似公式实现计算量略高好处是处处二阶可导梯度平滑程度比 smooth L1 更好。效果上和 Huber loss 差不多。Fair loss这个在 LightGBM 等框架里比较常见。它有一个平滑系数 (c)当 (c) 较小时行为接近 L1较大时行为接近 L2。这些变种万变不离其宗都是在 L1 的鲁棒性和 L2 的平滑收敛之间找平衡。理解了 L1、L2、smooth L1 三者的本质区别这些变种的核心思想和调参方向你大约能猜到一半。7.5 一个很重要的前提先归一化数据再谈损失函数这一点必须提到前面来。损失函数的数值对数据的量级极其敏感尤其是 L2 loss。如果真实值的范围是 0 到 100000而网络输出的初始化范围在 0 到 1 左右那么误差一开始可能就要 10 万量级平方之后是 100 亿量级梯度大得离谱。这不是损失函数的问题是数据没有归一化的问题。常规做法是回归标签做标准化或者缩放到 [0, 1] 区间。对关键点坐标缩放到 [0, 1] 或者按照图片宽高归一化对深度值可以取对数再回归对速度、角度等其他连续值做 z-score 标准化。做完归一化之后三个损失函数的梯度都在一个合理范围内学习率也好调了训练也更稳定。8. 常见问题与排查技巧实录8.1 训练 loss 直接变成 NaN怎么办这个情况我在头几次训练里遇到时第一反应是去调学习率其实更可能是损失函数和数据量级不匹配。如果用 L2 loss 且标签量级非常大误差的平方反馈回来的梯度过大参数瞬间更新出 NaN。解决办法先把标签做归一化再看训练是否稳定如果归一化后还是 NaN检查一下网络里有没有除以零或者 log 0 的操作。8.2 训练初期 loss 下降很快后期纹丝不动如果用的是 MSE很可能是因为误差变小后梯度也跟着变得很小导致收敛停滞。这时候可以换用 smooth L1 loss在误差小的区域依然保持线性梯度帮助模型继续“磨”到更优。也可以尝试把学习率调大一点或者对梯度做 clipping。8.3 训练后期 loss 反复震荡不收敛如果用的是 L1 loss这是常见现象。因为 L1 的梯度恒为 1误差很小的时候更新步长还是那么大自然容易在最优点附近震荡。解决办法是把部分 epoch 的学习率调低或者切换成 smooth L1 loss让模型在后期进入更精细的收敛阶段。8.4 验证集 loss 上升训练集 loss 一直降这不是损失函数的问题是过拟合。可以从数据增强、正则化、模型容量这些方向去解决。损失函数能调整的只是在“怎么衡量误差”这个层面的行为它没法解决泛化问题。8.5 画损失函数曲线时不收敛或者很抖可以试试这几个操作在 x 轴上用 epoch 而不是 iteration 作为单位对原始 loss 序列做 EMA 平滑把不同 loss 分开画子图避免数值范围差异太大导致某条线被“压扁”。用 matplotlib 画的时候两个循环epoch 和 batch里记录的点位要保持一致不要混着记。我在实际项目中踩过的一个比较深的坑是训练时在一个 batch 内做了多次梯度累积但画曲线时只记录了每次 optimizer.step() 之前的 loss 值导致曲线忽高忽低根本看不出趋势。后来我改成每次记录 loss 之前先对当前 epoch 的所有 batch 求平均曲线一下子就清晰了。最后再分享一下我个人的体会对我自己来说最早的阶段是无脑用 MSE因为教程里就是这么教的。后来做目标检测接触到了 smooth L1才慢慢意识到损失函数的设计本身就是一门学问。现在我的习惯是拿到一个回归任务先看数据分布再决定默认损失函数。如果数据量不大、离群点较多我大概率会直接选 smooth L1然后把 beta 或阈值作为超参一起调。如果时间充裕我会把 L1、L2、smooth L1 各跑一版对比验证集上的表现。这种对比的成本很低但收益很直观。另外一个小技巧如果你在预训练模型上继续微调回归头部损失函数的数值量级会直接影响微调时的学习率。之前我做过一个关键点检测项目加载预训练权重后模型输出的数值范围和随机初始化时差别很大导致用同一个学习率微调时 loss 反而下降了但指标烂得不忍直视。后来我把回归分支单独设置了一个更低的学习率、用 smooth L1 loss才把问题解决。损失函数从来不是一个孤立的公式它和学习率、数据分布、网络结构都纠缠在一起。理解了这一点你调参的底气会足很多。
返回列表