资讯动态

PyTorch损失函数:L1/L2范数与MAE/MSE/RMSE避坑指南

发布时间:2026/10/1 16:30:45 来源:尧图企业网站定制
训练模型的时候我把损失函数从 MSE 换成 MAE验证集指标立刻好看了一截追了半天才发现数据里有几个手工标注错得离谱的异常点。这类事在项目里遇到不止一次而每次翻车的姿势都不一样有人把 RMSE 写成mse ** 2有人在reductionnone之后忘了自己还得再求一次均值有人用torch.linalg.norm算二维张量结果拿到的是矩阵谱范数还浑然不觉。L1范数、L2范数、MAE、MSE、RMSE 这几个词写在论文和 PPT 里谁都认识落到 PyTorch 的代码里真正能把定义、维度、量纲、梯度四件事同时说清楚的人并不多。这篇内容从我自己的使用习惯出发把这五个概念从头捋一遍它们各自的数学定义是什么、为什么公式长这样、在 PyTorch 里对应的 API 有哪几种写法、每种写法在什么情况下会坑人以及回归、图像重建、Embedding 归一化这些具体场景里该怎么选。文中所有代码在 CPU 和 GPU 上都能直接跑不需要额外依赖PyTorch 1.9 以后的版本基本通用。1. 先把五个词的身份分清范数是向量的度量MAE/MSE/RMSE 是样本上的平均很多人第一次接触这几个概念时脑海里会糊成一团根本原因是把两个不同层面的东西混在一起了。范数是一个向量自身的属性衡量的是这个向量有多长MAE、MSE、RMSE 是一组样本误差的统计量衡量的是预测和真值差得有多远。前者是几何概念后者是统计概念它们之间通过误差向量这座桥连起来但绝不是同一个东西。1.1 L1 与 L2 范数同一个向量两种长度对一个 n 维向量 x (x₁, x₂, …, xₙ)L1 范数和 L2 范数的定义分别是L1 范数‖x‖₁ Σ |x|所有分量绝对值之和L2 范数‖x‖₂ √(Σ xᵢ²)所有分量平方和再开方更一般地p 范数写作 ‖x‖ₚ (Σ |xᵢ|ᵖ)^(1/p)L1 和 L2 只是 p1 和 p2 的两个特例。当 p 趋于无穷时得到最大范数也就是取绝对值最大的那个分量这个在梯度裁剪的参数里偶尔会用到。L1 范数有个很直观的几何解释在二维平面上到原点 L1 距离等于 1 的点集是一个菱形L2 距离等于 1 的点集是一个圆。菱形在坐标轴上有尖角这个尖角正是 L1 能产生稀疏解的根本原因——损失等高线和约束区域的交点大概率落在坐标轴上于是某些权重直接变成 0。L2 的圆形边界没有尖角交点一般不在坐标轴上所以 L2 正则只会把权重压小不会压成 0。这件事在做特征选择或者想让模型变轻的时候非常关键。1.2 MAE、MSE、RMSE把范数搬到每个样本上假设有一批样本预测值 ŷᵢ真值 yᵢ误差 eᵢ ŷᵢ − yᵢ样本数 n。三个指标的定义是MAE (1/n) Σ |eᵢ|也叫 L1 损失MSE (1/n) Σᵢ e²也叫 L2 损失严格说叫平方 L2 损失RMSE √MSE看出来了没有MAE 就是误差向量的 L1 范数除以 nMSE 是误差向量 L2 范数的平方除以 nRMSE 是 MSE 开方。所以MAE 等于 L1 损失这句话是对的MSE 等于 L2 损失这句话在工程语境里也基本对但严格讲 MSE 少了最后那次开方它是 L2 范数的平方再平均。这个细节在推导梯度的时候会咬人。用三个样本来算一遍感受一下数量级。预测[1.0, 2.0, 3.0]真值[1.5, 2.0, 2.0]误差是[-0.5, 0, 1.0]指标计算过程结果误差的 L1 范数0.5 0 1.01.5000误差的 L2 范数√(0.25 0 1.0)1.1180MAE1.5 / 30.5000MSE1.25 / 30.4167RMSE√0.41670.6455注意 MAE0.5比 RMSE0.6455小这是普遍规律MSE 因为平方放大了大误差RMSE 开方之后仍然带着这种放大的痕迹所以 RMSE 永远大于等于 MAE两者相等当且仅当所有误差的绝对值都相同。1.3 一张表把定义、量纲、敏感度放一起对比做技术选型时我最常看的四个维度是量纲、对离群值的敏感度、梯度特性、典型用途。整理成表贴在这里比记公式有用得多。指标公式量纲异常值敏感度零点梯度典型用途L1 范数Σ|xᵢ|与分量同中线性增长次梯度 ±1稀疏正则、稀疏编码L2 范数√Σxᵢ²与分量同高平方增长0线性趋近权重衰减、梯度裁剪、距离度量MAEmean|eᵢ|与标签同低线性±1/n带噪标签回归、图像重建MSEmean eᵢ²标签的平方很高平方0常规回归、需要大误差快速下降RMSE√MSE与标签同很高平方0对外汇报指标、跨数据集比较量纲这一列值得单独强调。假设你在预测房价单位是万元。MAE 算出来是 8意思是平均每套房预测偏差 8 万元可以直接讲给人听。MSE 算出来是 64这个 64 的单位是万元的平方没有任何物理意义你没法对业务方说我们模型误差 64。RMSE 开方之后又回到 8 万元恢复了可解释性。这就是 RMSE 存在的主要理由——它把 MSE 的量纲拉回原空间让人能读懂。2. 为什么 L2 要平方、L1 要取绝对值从分布假设到梯度行为公式记住了不代表理解了。真要理解为什么有两种损失得从两个角度看一个是概率统计视角一个是优化梯度视角。前者告诉你什么数据该用哪个损失后者告诉你训练过程会有什么不同。2.1 概率视角高斯噪声对应 MSE拉普拉斯噪声对应 MAE假设真值和预测之间的关系是 y f(x) ε噪声 ε 服从某种分布。如果我们假设 ε 服从均值为 0、方差为 σ² 的高斯分布那么单个样本的似然是 (1/√(2πσ²))·exp(−ε²/(2σ²))。取负对数之后常数项丢掉剩下的正比于 ε²。把整批样本的负对数似然加起来就得到 MSE。所以最小化 MSE 等价于在独立高斯噪声假设下做最大似然估计。换成拉普拉斯分布密度函数是 (1/2b)·exp(−|ε|/b)取负对数剩下的是 |ε|加起来就是 MAE。所以MAE 对应的是拉普拉斯噪声假设。拉普拉斯分布比高斯分布尖峰厚尾大量样本集中在 0 附近少数样本可以跑得很远。这正好对应现实中的重尾噪声——大部分标注很准个别标注错得离谱。这解释了为什么带脏标签的数据集上 MAE 往往表现更稳。如果噪声既不是纯高斯也不是纯拉普拉斯介于两者之间那对应的就是 Huber 损失误差小于 δ 时用平方像 MSE大于 δ 时用线性像 MAE。这是工程上非常实用的折中nn.HuberLoss和nn.SmoothL1Loss都是这个思路超分和检测框回归里用得很频繁。2.2 梯度视角常数梯度与线性增长梯度的差别只谈分布太抽象看梯度最直接。对单个样本的误差 eMSE 对 e 的导数是 2e误差越大梯度越大呈线性增长MAE 对 e 的导数是 sign(e)误差大小无关永远是 ±1这个差别会直接改变训练动力学。用 MSE 训练时一个误差是 10 的样本产生的梯度是一个误差是 1 的样本的 10 倍模型会被少数几个大误差样本牵着走。如果这几个样本恰好是标错的模型就跑偏了这在第 6 节的实验里能看得很清楚。用 MAE 训练时所有样本的梯度幅度一样谁也不特别有话语权鲁棒性好但代价是收敛慢——误差从 10 降到 1 的过程中梯度一直是 1没有越接近越慢的自然衰减到了最优点附近容易来回震荡。还有一个容易被忽略的点MAE 在 e0 处不可导只有次梯度。PyTorch 里torch.abs在 0 处的反向传播会返回 0这不是数学上的次梯度选择而是框架的实现约定。绝大多数情况下没影响但如果你在做对抗样本、梯度分析这类对梯度精度敏感的工作得留意一下。实操上还有一个常被忽视的细节MSE 的梯度随误差线性增长意味着如果不做梯度裁剪遇到离群样本时容易发生梯度爆炸。我在一个销量预测任务上就吃过这个亏某个门店的数据录入多打了一个 0MSE 损失瞬间冲到四位数一步更新就把前面几万步的训练全毁了。换成 Huber 或者加上clip_grad_norm_之后就没事了。2.3 量纲这件事RMSE 存在的唯一理由前面提过量纲这里补一个实际会踩的坑在多个不同量纲的目标上做多任务学习时不能直接对 MSE 求和。比如同时预测价格万元级数值 10²和折扣率0 到 1 之间。价格的平方误差动辄上百折扣率的平方误差是小数点后两位简单相加等于只有价格在起作用。正确做法是各自归一化或者用各自的 RMSE/MAE 再按任务权重加权。我在做多任务模型时习惯把所有回归目标先标准化到均值 0、方差 1训练时用 MSE评估时再反标准化回原空间算 MAE 和 RMSE这样既能享受 MSE 的优化效率又能用可解释的指标汇报。还有一种情况是不同 batch 之间 loss 尺度的比较。如果你用reductionsum最后一个 batch 数量不满的时候loss 数值会明显比前几个 batch 小打印日志的时候看着像训练突然变好了其实是 batch size 变了。这是新手最容易误判的现象之一后面第 3 节会展开。3. PyTorch 里的实现路径从 torch.norm 到 nn.L1Loss 的三种写法PyTorch 里算这几个量至少有三种路径底层张量 APItorch.linalg系列、损失模块nn.MSELoss等、以及函数式接口torch.nn.functional。三条路都能算对但适用场景和坑点完全不同。3.1 torch.norm 与 torch.linalg 系列ord 参数最容易搞错先说范数本身。老版本里大家习惯写torch.norm(x, p2)新版 PyTorch 已经把它标记为不推荐官方建议迁移到torch.linalg系列。迁移之后最反直觉的一点来了import torch x torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 按向量理解把整个张量展平后求 2 范数 sqrt(14916) 5.4772 print(torch.norm(x, p2)) # tensor(5.4772) print(torch.linalg.vector_norm(x)) # tensor(5.4772) # 按矩阵理解ord2 是矩阵的谱范数最大奇异值不是 Frobenius 范数 print(torch.linalg.norm(x, ord2)) # tensor(5.4650) 大约 print(torch.linalg.norm(x, ordfro)) # tensor(5.4772)torch.linalg.norm(x, ord2)对二维输入返回的是最大奇异值跟展平求 2 范数不是一个数。很多人从torch.norm迁过来的时候直接改了个名字结果莫名其妙变了排查半天。我的习惯是永远显式写清楚用哪个函数要向量范数就用torch.linalg.vector_norm(x, ord1/2, dim...)要矩阵范数就用torch.linalg.matrix_norm(x, ordfro/nuc)绝不依赖torch.linalg.norm的自动推断。还有一个细节是dim参数。torch.linalg.vector_norm(x, dim1)表示沿第 1 维求范数输出会把这个维度消掉。在序列任务里对最后一维做归一化是很常见的操作这时候写dim-1最安全不用关心前面有几个维度。3.2 nn.L1Loss / nn.MSELossreduction 决定了你把什么平均掉nn.L1Loss、nn.MSELoss、nn.SmoothL1Loss、nn.HuberLoss都有reduction参数三个取值mean默认对所有元素求平均sum对所有元素求和none不做任何聚合返回和输入同形状的张量听起来很简单但这里有几个必须记住的点。第一mean是对所有元素求平均不是对样本求平均。如果你的输出是 (B, C, H, W)那nn.MSELoss()除的是 B×C×H×W不是 B。在图像任务里这通常是你想要的但如果你在做逐像素加权或者每个样本的有效像素数不一样比如变长序列 padding直接 mean 就会把 padding 位置也算进去结果偏小。这时候必须用reductionnone加掩码自己算。第二reductionsum会让 loss 尺度随 batch size 变化。假设每个样本的损失平均是 1batch size 是 64sum 出来的 loss 是 64最后一个 batch 只有 10 个样本loss 就变成 10。日志上看着像断崖式下降其实是错觉。除非你在实现论文里的特定公式有些论文确实用 sum 再除以总 token 数否则训练时统一用 mean。变长序列的处理我通常这样写import torch import torch.nn.functional as F pred torch.randn(8, 20, 1) # batch8, 序列长 20, 特征 1 target torch.randn(8, 20, 1) mask torch.rand(8, 20, 1) 0.3 # True 表示有效位置 # 错误做法padding 位置的误差被算进了分母 loss_bad F.mse_loss(pred, target) # 正确做法只在有效位置求均值 loss_raw F.mse_loss(pred, target, reductionnone) loss_ok (loss_raw * mask).sum() / mask.sum().clamp(min1)那个clamp(min1)是防止整个 batch 的 mask 全为 False 时除零出 NaN这种边界情况在真实数据里真的会碰到尤其是短序列任务。3.3 手写实现广播与 dtype 两个隐形炸弹自己手写((pred - target) ** 2).mean()在简单场景下没问题但有两个坑。广播陷阱如果pred的形状是 (B, 1)target的形状是 (B,)相减会广播成 (B, B)loss 完全算错而且不报错。这类问题在加了keepdim不一致的时候特别容易出现。我的防御性写法是显式断言assert pred.shape target.shape, f{pred.shape} vs {target.shape} err pred - target mse torch.square(err).mean()dtype 陷阱整数张量不能直接算 MSE。torch.tensor([1, 2]) - torch.tensor([1, 3])是整型平方之后还是整型mean()会报错或者返回整型截断结果。真实场景里常见的是从数据加载器里拿到的标签是torch.long比如分类索引直接跟浮点预测相减就会出问题。养成习惯进损失函数之前先.float()。另外torch.square(x)比x ** 2稍微快一点点在超大张量上能看出差别因为前者少走一次通用幂运算的分发。这种优化属于锦上添花但顺手改掉没坏处。顺便说一句性能nn.MSELoss和手写torch.square(err).mean()在绝大多数规模下速度差异可以忽略我在 1080p 图像重建任务上实测过一整个 epoch 的差距不到 2%。所以选哪个主要看可读性和是否需要reductionnone不用为了性能纠结。4. 数值稳定性、维度语义和梯度裁剪里踩过的坑定义和 API 都清楚了接下来是真正会让人加班的部分。这一节的三个问题我在不同项目里都栽过。4.1 RMSE 里的 sqrt 在零点会给 NaN 梯度这是我最想强调的一条。RMSE 的定义是 √MSE所以很多人直接写rmse torch.sqrt(F.mse_loss(pred, target))问题在于当 MSE 恰好等于 0完美预测torch.sqrt在 0 点的导数是无穷大反向传播会得到 NaN然后污染整个网络的参数。在训练早期或者做单元测试的时候模型可能一瞬间就完美拟合某个 batch然后整条训练曲线变成 NaN。修法很简单加一个极小常数rmse torch.sqrt(F.mse_loss(pred, target) 1e-8)1e-8 用的是 float32 的精度极限附近的值对结果精度的影响可以忽略但足以让梯度保持有限。如果是 float64可以用 1e-12。这个技巧不只用在 RMSE 上任何开方后还要反传的场景都适用比如计算欧氏距离、标准化时的标准差。还有一点如果 RMSE 只是用来打日志、不参与反传那就不用加 eps加了反而让数值略微失真。判断标准很简单看这个量有没有进入loss.backward()的路径。我习惯写两个函数一个带 eps 用于训练一个不带用于评估避免混淆。4.2 混合精度下 MSE 的溢出与下溢用 AMP自动混合精度训练时激活值和损失可能被降到 float16。float16 能表示的最大值大约是 65504最小正规格化数约 6e-5。MSE 涉及平方误差是 100 的时候平方就是 10000还在范围内误差到 300平方就是 90000直接溢出成 inf。而 float16 的下溢更容易被忽略误差是 0.001 的时候平方是 1e-6属于次规格化数精度损失严重几百个这样的项加起来的均值可能变成 0梯度就没了。我的处理习惯是损失计算始终在 float32 里做。with torch.autocast(device_typecuda, dtypetorch.float16): pred model(batch) loss F.mse_loss(pred.float(), target.float())请注意pred.float()放在 autocast 块外面否则它又会被 autocast 转回去。另外 PyTorch 内置的nn.MSELoss在 autocast 下有专门的数值稳定处理多数情况下比自己手写更安全这也是我倾向用官方模块的一个理由。如果确实需要在 float16 下算可以先把误差缩放一下比如err (pred - target) / scale算完再乘回去。不过说实话除了显存实在不够的情况我不推荐这么折腾。4.3 clip_grad_norm_ 用的是哪种范数torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)是训练标配但它内部算的是什么范数答案是把模型所有参数的梯度张量展平拼接成一个超长向量然后算这个向量的 p 范数默认 p2。不是每个参数张量单独裁剪也不是逐层的范数是全局的。这一点在做梯度分析的时候很重要。如果你想让裁剪更温和一些可以设norm_type1用 L1 范数裁剪效果是限制总梯度绝对值之和对少数特别大的梯度分量惩罚更狠norm_typefloat(inf)则只限制最大单分量最宽松。我在训练 Transformer 类模型时一般用默认的 2训练带稀疏梯度的 embedding 时偶尔用 1。还有个容易混淆的是clip_grad_value_它是逐元素按值裁剪把每个梯度分量夹到 [−v, v]跟范数裁剪完全不是一回事。范数裁剪保持了梯度方向不变只改长度值裁剪会改变方向。这个区别在调试训练不稳定问题时很关键——如果你发现裁剪后训练反而变差了很可能是方向被改变导致的。5. 不同任务里怎么选回归、图像重建、Embedding 归一化理论讲完落到选型。我的经验是选损失函数不是看哪个更先进而是看你的数据长什么样、你要的输出长什么样。5.1 回归任务先看异常值再决定拿到一个回归任务我会先做两件事画误差分布的直方图看尾部有多厚统计标签里有多少个明显偏离的点。判断标准大致是这样数据特征推荐损失理由噪声接近高斯、无明显离群点MSE梯度随误差增长收敛快存在少量严重离群点MAE 或 Huber线性增长不会被单点主导离群点较多但也要收敛速度Huberδ 取 1.0 左右兼顾鲁棒性和收敛目标是分位数而非均值Pinball / Quantile LossMSE、MAE 都只能给条件均值或中位数Huber 的 δ 参数需要根据误差量级调。我的做法是先跑一遍 MSE统计误差的绝对值的 90 分位数把 δ 设成这个值附近。这样 90% 的样本走平方分支享受快速收敛剩下 10% 走线性分支不被带偏。关于 MAE 收敛慢还有一个实用的补偿办法先用 MSE 预热再切到 MAE 微调。我在一个销量预测项目里用这个策略前 70% 的 epoch 用 MSE 快速把模型拉到合理区域后 30% 切 MAE 精调最终 MAE 比全程用 MAE 低大约 3%而且训练时间少了一半。切换的时候记得把学习率降一个数量级因为两种损失的梯度尺度不一样。5.2 图像重建与超分为什么 L1 常常比 L2 好看做过超分或者去噪的人都知道用 MSE 训出来的图偏糊用 L1 训出来的锐利很多。原因在于 MSE 对大误差的惩罚是平方级的模型为了不让任何一个像素错太多倾向于输出一堆像素的平均值——平均值就是糊的数学本质。L1 的惩罚是线性的模型不需要为了压住个别大误差而牺牲整体锐度边缘的高频信息更容易保留下来。不过 L1 也不是万能的。它会让输出产生一些块状伪影因为在平坦区域它没有强烈的平滑驱动力。所以现在大多数超分方案是 L1 加感知损失perceptual loss加对抗损失组合L1 负责像素级对齐感知损失负责纹理对抗损失负责真实感。纯 L1 只适合做基线。还有一个实操细节图像任务的 loss 计算前要确认数值范围。有的数据集把像素归一化到 [0, 1]有的到 [−1, 1]有的保持 [0, 255]。范围不同同一个误差值对应的 loss 数值差几个数量级超参数完全不能迁移。我在复现别人代码时第一件事就是看他的输入归一化方式这比看网络结构重要得多。如果要在 [0, 1] 范围上算 RMSE得到的数值会非常小0.0x 级别打印日志时容易被当成噪音忽略。这时候可以直接用 MSE或者把结果乘以 255 换算到像素级再报告。业界论文里报的 PSNR 其实就是 RMSE 的对数变换PSNR 20·log₁₀(MAX/RMSE)MAX 通常取 255 或 1所以本质上还是在用 RMSE。5.3 Embedding 归一化与距离度量F.normalize 的正确姿势在检索、聚类、对比学习里经常要把 embedding 归一化到单位球面上然后算余弦相似度。PyTorch 的对应接口是import torch.nn.functional as F emb torch.randn(32, 128) # 32 个样本128 维 emb_norm F.normalize(emb, p2, dim-1) # 每行 L2 范数变成 1 cos_sim emb_norm emb_norm.t() # 直接内积就是余弦相似度三个要点。第一dim-1必须写默认也是 -1 但显式写出来更清楚如果你在 (B, T, D) 的张量上操作dim-1归一化的是特征维dim1归一化的是序列维语义完全不一样。第二F.normalize内部有eps参数默认 1e-12防止零向量除零。如果你手动写x / x.norm(dim-1, keepdimTrue)零向量会产生 NaN而F.normalize会返回 0。这个差异在稀疏输入下会显现出来。第三归一化之后 L2 范数是 1L1 范数一般是 √D 到 D 之间的某个值取决于分布两者不再有简单关系不要混用。另外提一句在对比学习中经常需要在归一化后的向量上算 L2 距离其实归一化之后 L2 距离和余弦相似度是一一对应的‖a−b‖² 2 − 2·cos(a,b)。所以代码里用哪个都行选实现更简洁的那个。5.4 正则化里的 L1/L2稀疏与权重衰减最后回到 L1/L2 范数在模型层面的用法。把权重向量的 L1 范数加进总损失就是 Lasso 正则效果是让一部分权重精确变成 0可以用来做特征筛选。把 L2 范数加进去是 Ridge 正则让权重整体变小但很少为 0。现代深度学习里L2 正则基本都以权重衰减的形式实现而不是显式地在 loss 里加范数。两种写法在纯 SGD 下等价但在 Adam 下不等价——因为 Adam 的梯度缩放会破坏这种等价性所以有了 AdamW把权重衰减解耦出去。如果你在用 Adam 并且手动在 loss 里加了weight_decay * sum(p.norm() ** 2)建议换成优化器的weight_decay参数配 AdamW。有个细节是手写 L2 正则时有人写成p.norm()而不是p.norm() ** 2这两个的梯度完全不同前者梯度是 p / ‖p‖后者是 2p。论文里说 weight decay 时指的是后者。这个错我在一个开源项目里见过作者自己都没意识到直到有人发现正则强度调不动。6. 一份可直接跑的对照脚本与结果解读前面讲了不少理论最后给一份能直接跑的代码把五个指标一次算清楚再用一个带离群点的小实验验证前面说的鲁棒性到底长什么样。6.1 六个指标同时算一遍import torch import torch.nn.functional as F def all_metrics(pred, target, dimNone): 一次算齐 L1/L2 范数、MAE/MSE/RMSE。 dimNone 时按全局展平计算dim 指定时沿该维度计算。 pred pred.float() target target.float() err pred - target if dim is None: l1_norm torch.linalg.vector_norm(err, ord1) l2_norm torch.linalg.vector_norm(err, ord2) n err.numel() else: l1_norm torch.linalg.vector_norm(err, ord1, dimdim) l2_norm torch.linalg.vector_norm(err, ord2, dimdim) n err.shape[dim] mae err.abs().mean() if dim is None else err.abs().mean(dimdim) mse torch.square(err).mean() if dim is None else torch.square(err).mean(dimdim) rmse torch.sqrt(mse 1e-8) return { L1_norm: l1_norm, L2_norm: l2_norm, MAE: mae, MSE: mse, RMSE: rmse, } torch.manual_seed(42) pred torch.tensor([1.0, 2.0, 3.0]) target torch.tensor([1.5, 2.0, 2.0]) print(all_metrics(pred, target))跑出来 L1 范数是 1.5L2 范数是 1.1180MAE 是 0.5MSE 是 0.4167RMSE 是 0.6455跟手算完全对得上。注意 MAE 乘以样本数等于 L1 范数MSE 乘以样本数再开方等于 L2 范数这两条恒等式可以拿来验证自己的实现有没有写错。和官方实现对拍是我写完自定义损失后的固定动作mine torch.square(pred - target).mean() official F.mse_loss(pred, target) assert torch.allclose(mine, official, atol1e-6), f{mine} vs {official}torch.allclose里一定要设atol不要用比浮点数。默认atol1e-8有时都嫌严因为不同实现的求和顺序不同最后几位可能差一点点。我一般用 1e-6 到 1e-5 之间。6.2 带离群点的线性拟合实验现在验证鲁棒性。构造 200 个点y 2x 1 加上一点高斯噪声然后人为把其中一个点改成 (5, 80)模拟标注错误。import torch torch.manual_seed(0) N 200 x torch.rand(N, 1) * 10 y 2.0 * x 1.0 0.5 * torch.randn(N, 1) # 插入一个标错的离群点 x[0] 5.0 y[0] 80.0 def fit(loss_fn, steps3000, lr0.02): w torch.zeros(1, 1, requires_gradTrue) b torch.zeros(1, requires_gradTrue) opt torch.optim.Adam([w, b], lrlr) for _ in range(steps): opt.zero_grad() loss loss_fn(w * x b, y) loss.backward() opt.step() return w.item(), b.item() print(MSE 拟合:, fit(lambda p, t: F.mse_loss(p, t))) print(MAE 拟合:, fit(lambda p, t: F.l1_loss(p, t)))跑完你会看到一个很直观的现象用 MSE 拟合出来的斜率明显偏离 2.0偏大不少因为那个离群点产生了几千倍于普通样本的梯度硬生生把直线拽向自己。换成 L1 之后斜率基本落在 2.0 附近截距也接近 1.0那个离群点只是让直线整体略微上移没有扭转趋势。这就是 2.1 节里分布假设的实证MSE 假设高斯噪声遇到重尾它就崩MAE 对应拉普拉斯噪声容忍厚尾。把实验里的离群点去掉两种损失拟合出的参数几乎一样——MAE 的鲁棒性只在有离群点时才是优势没有离群点时它反而更慢。这一点挺重要很多人看到MAE 更鲁棒就无脑换结果在干净数据上白白多花训练时间。6.3 几个我踩过之后固定下来的小习惯写到这里把几个我固定下来的编码习惯列一下都是真金白银换来的一是每个损失函数旁边都写一行注释说明 reduction 是什么、除以的是哪个维度。代码过两个月自己都看不懂这一行注释能省很多时间。二是训练和评估用不同的指标集合。训练看 loss可能是 MSE评估输出 MAE 和 RMSE 两个真实量纲的数。只看 loss 容易失去对模型实际表现的判断。三是任何涉及开方、除法、对数的计算先想一遍边界情况。零、负数、极大值各会怎样能不能加 eps会不会破坏单调性。四是换损失函数时一定要重调学习率。MAE 的梯度幅度恒定MSE 的梯度随误差变化两者在同一个学习率下的有效步长完全不同。我从 MSE 切 MAE 时会把学习率降一半左右从 MAE 切 MSE 时反而可以稍微调高但一定要加梯度裁剪兜底。五是记录每个 epoch 的误差分位数不只看均值。我习惯记 P50、P90、P99因为只看 MAE 会掩盖尾部问题。如果 P99 和 P50 差了几十倍说明数据里还有离群点没处理干净这时候不管用哪种损失都很难训好得回去查数据。这个习惯帮我抓到过好几次标注流程的问题比调损失函数有用得多。还有个小技巧值得分享如果你不确定该用 MAE 还是 MSE先画一张误差的 QQ 图或者直接用torch.quantile(err.abs(), 0.99) / err.abs().median()算个简单的尾部比值。这个比值接近 2 到 3说明接近高斯超过 10八成有离群点优先考虑 Huber 或 MAE。用一行代码就能做的数据诊断比事后调参划算太多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑