尧图精选

MAE、MSE、RMSE与L1/L2范数:PyTorch损失函数关系与选型

🕒 发布时间:2026/10/1 1:25:16 📁 来源:尧图网络
数值这块的东西很多人都是在写第一个回归模型的时候才第一次撞上损失曲线画出来忽高忽低换一个损失函数结果天差地别或者明明模型输出看着挺正常反向传回去的梯度却大得离谱。这些问题往回追十有八九是没搞清楚 MAE、MSE、RMSE 和 L1 范数、L2 范数这几样东西到底在算什么。标题里这五个概念看着像线性代数课本里的名词实际在 PyTorch 里天天要用而且它们之间的关系比表面看起来紧密得多——MAE 就是 L1 范数在样本维度上取平均MSE 和 L2 范数的平方只差一个常数因子RMSE 则是为了让量纲回到原始单位而做的补救。搞清这套换算关系你在写自定义损失、做多任务加权、调 weight_decay 的时候就不会凭感觉瞎猜了。这篇文章面向的是刚开始用 PyTorch 做回归或者表示学习的人也适合已经写过几轮训练但总觉得损失函数选得不够踏实的同学。我会先把这几个名词的几何含义和数学定义拆开讲清楚再落到 PyTorch 的具体 API 上把torch.linalg.norm、nn.L1Loss、nn.MSELoss这些的用法和坑点一条条过一遍最后给一套可以直接跑的对比实验代码。我尽量不堆公式能用一句话说清的就不写三行推导。1. 概念梳理范数、损失、误差指标其实是同一套东西1.1 范数是给向量量长度的尺子范数这个词听着抽象说白了就是给一个向量算一个非负的长度。你手里有一串数想知道它整体有多大用不同的量法就得到不同的范数。最常见的是 L1 和 L2 两种量法。L1 范数是把每个元素的绝对值加起来公式写成 $|x|_1 \sum_i |x_i|$。它的直觉是曼哈顿距离——你在一座棋盘状的城市里从 A 点到 B 点只能横着走或竖着走走过的总距离就是 L1 范数。所有分量出的力是线性的不存在谁被放大。L2 范数则是把每个元素平方、求和、再开方写成 $|x|_2 \sqrt{\sum_i x_i^2}$。这个就是中学里学的欧几里得距离也就是直线距离。因为先平方再开方大分量会被显著放大小分量会被压得更小。这两种范数在几何上的差别很直观。在二维平面上所有 L1 范数等于 1 的向量连起来是个菱形旋转了 45 度的正方形四个顶点落在坐标轴上。而 L2 范数等于 1 的向量连起来是个圆。这个形状差异不是纯粹的数学趣味它直接决定了优化时候解会落在哪——菱形的尖角在坐标轴上所以 L1 约束下的最优解很容易正好让某个分量变成 0这就是 L1 能产生稀疏解的几何来源。圆没有尖角L2 约束下的解通常所有分量都不为零只是整体被压小。这个结论后面讲正则化的时候还会用到。注意范数的定义里输入是一个向量。但在深度学习中你面对的是形状为 (N, D) 的张量这时候要先想清楚是沿着样本维算还是沿着特征维算算错了整个训练的语义就变了。1.2 从范数到损失函数的映射关系损失函数和范数的关系其实是一次先做差、再套范数的组合。你有一个预测向量 $\hat{y}$ 和一个真实向量 $y$先算残差 $r \hat{y} - y$然后对 $r$ 套上不同的范数就得到不同的误差度量。把 L1 范数套在残差上得到的是残差的绝对值之和如果后面再除以样本数 n就变成了平均绝对误差 MAE。把 L2 范数平方套在残差上得到的是残差平方和除以 n 之后就是均方误差 MSE。而 RMSE 是在 MSE 的基础上再开个根号纯粹是为了把量纲拉回到和原始数据一致。所以严格说范数是度量损失是把度量平均一下好让它不随样本数变化误差指标是把损失调整成一个人类能读的量纲。三层是递进关系不是并列关系。我在项目里见过不少人把torch.norm(x - y)直接当损失用然后发现 batch size 从 32 改成 128 之后损失数值翻了四倍学习率完全失效。这就是没有做平均导致的——L1 范数会随元素个数线性增长而 MAE 不会。这个细节看起来小实际调参的时候能省掉一整个下午的试错。1.3 为什么工程上要同时保留这么多名字既然本质是一回事为什么还要搞出五个名字因为不同的名字服务于不同的使用场景。在数学推导和优化理论里用 L1 范数、L2 范数这种叫法因为它可以不限于预测减真值这个语境任何向量都能套。在模型评估的报告里用 MAE、RMSE 这种叫法因为它们的量纲和原始数据一致你可以直接说这个模型平均错了 3.2 摄氏度比说L1 范数是 320要清楚得多。而在 PyTorch 的损失模块里用nn.L1Loss、nn.MSELoss这种命名是因为它们默认会做 reduction求平均符合训练时最常用的形态。理解了这层关系你在写论文、写报告、写代码的时候就能自如切换不会出现损失函数是 L1 但指标报的是 RMSE这种前后对不上的尴尬。更重要的是你在自定义损失函数的时候知道自己在哪一层做手脚——是在范数层改比如换成 Huber还是在平均层改比如改成按样本加权还是在开方层改比如改成 RMSLE。每一层改动对梯度的影响完全不同。2. 数学定义与公式拆解2.1 L1 范数与 L2 范数的严格定义对向量 $x \in \mathbb{R}^n$L1 范数的定义是$$|x|1 \sum{i1}^{n} |x_i|$$L2 范数的定义是$$|x|2 \sqrt{\sum{i1}^{n} x_i^2}$$两个定义里唯一的差别就是先平方再开方还是直接取绝对值。但就是这点差别导致了两者在可微性上的根本不同。L1 范数里每项是 $|x_i|$它在 $x_i 0$ 处不可导左导数是 -1右导数是 1只能定义次梯度。L2 范数在整个定义域上光滑可导除了原点原点处梯度为 0 而不是不可导这决定了用 L2 做损失时优化器走得更稳。还有一个容易被忽略的点L2 的平方 $|x|_2^2 \sum_i x_i^2$ 是完全光滑的二次函数处处可导且导数是 $2x_i$。所以在实际实现里MSE 损失几乎不会去真的算范数再平方而是直接写平方和省掉一次开方和一次平方的消解数值上也更稳。2.2 MAE、MSE、RMSE 的定义与量纲分析假设有 n 个样本预测值 $\hat{y}_i$真实值 $y_i$残差 $r_i \hat{y}_i - y_i$那么名称公式量纲对离群值的敏感度MAE$\frac{1}{n}\sum_i |r_i|$和 y 相同线性低MSE$\frac{1}{n}\sum_i r_i^2$y 的平方二次高RMSE$\sqrt{\frac{1}{n}\sum_i r_i^2}$和 y 相同二次高L1 范数$\sum_i |r_i|$和 y 相同线性低L2 范数$\sqrt{\sum_i r_i^2}$和 y 相同二次高量纲这一列是实操里最值得盯的。MSE 的量纲是原数据的平方如果你的目标值单位是米MSE 的单位就是平方米你没法直接说平均误差多少米。RMSE 开完根号之后量纲回到米所以它比 MSE 更适合写进报告。但注意量纲一致不等于数值可以直接比较。RMSE 和 MAE 都是米却在数值上差别很大——因为 RMSE 在连乘之前先平方大误差被放大后才参与平均所以只要数据里存在几个偏离较大的点RMSE 就会明显高于 MAE。这个差值本身就是一个有用的诊断信号如果 RMSE 比 MAE 大很多说明模型在某些样本上错得很离谱而绝大部分样本其实预测得还行。如果两者很接近说明误差分布比较均匀没有极端的离群点。2.3 它们之间的换算关系这五个量之间有严格的换算记清楚了能做到心里有数不用每次都重新推。设 n 为元素个数r 为残差向量。MAE 和 L1 范数的关系最直接$$\text{MAE} \frac{|r|_1}{n}$$MSE 和 L2 范数的平方的关系$$\text{MSE} \frac{|r|_2^2}{n}$$RMSE 和 L2 范数的关系$$\text{RMSE} \sqrt{\text{MSE}} \frac{|r|_2}{\sqrt{n}}$$这三个式子我在实际排查时经常用。比如你手头只有损失值但报告里要写 RMSE那就先确认损失是不是按 mean 归约的然后sqrt(loss)就行如果损失是按 sum 归约的有些框架默认行为不一样就得先除以样本数再开方。反过来如果你算出了 L2 范数想换算成 MSE就是norm**2 / n。这个 n 到底是样本数还是全部元素数取决于你的张量最后一维是什么——回归里输出是标量的话两者相同如果是多步预测那就得区分。提示n 的取值一定要和损失函数的 reduction 对齐。nn.MSELoss(reductionmean)对全部元素求平均如果输入张量形状是 (B, T, D)那分母就是 BTD不是 B。多步预测场景下这个坑非常常见。3. PyTorch 中的计算方式与 API 选择3.1 torch.linalg.norm 与已弃用的 torch.normPyTorch 早期只有一个torch.norm这个函数在 1.9 之后逐渐被torch.linalg系列取代到 2.x 版本用torch.norm会直接给你一个 deprecation warning。原因在于老 API 对矩阵输入的处理有歧义传一个矩阵进去torch.norm默认算的是 Frobenius 范数也就是把所有元素平方求和开方但如果你指定p2它还可能被理解成谱范数。同名参数在不同形状下语义不一致容易踩坑。新的torch.linalg把这个歧义拆开了分成两个函数torch.linalg.vector_norm(input, ord, dim, keepdim, dtype)专门算向量范数输入的最后若干维被当作向量来处理。torch.linalg.matrix_norm(input, ord, dim, keepdim, dtype)专门算矩阵范数比如 Frobenius、核范数、谱范数。而torch.linalg.norm是个通用入口会根据输入维度和 ord 自动判断该走哪条路。日常做回归损失的时候用vector_norm就足够了语义最清晰。import torch x torch.tensor([1.0, -2.0, 3.0, -4.0]) # L1 范数 l1 torch.linalg.vector_norm(x, ord1) # tensor(10.) # L2 范数 l2 torch.linalg.vector_norm(x, ord2) # tensor(5.4772) # 等价写法L2 的平方 l2_sq torch.linalg.vector_norm(x, ord2) ** 2 # tensor(30.)注意ord2是默认值所以torch.linalg.vector_norm(x)得到的就是 L2 范数。而ord1拿到的才是 L1。如果你习惯了 NumPy 的np.linalg.norm(x, ord1)这套参数是通用的迁移过来不用改。3.2 nn.L1Loss / nn.MSELoss 与手写实现的差异训练的时候我更推荐用nn.L1Loss和nn.MSELoss这两个模块而不是手写(pred - target).abs().mean()。原因有三个。第一是 reduction 参数更可控。nn.MSELoss默认reductionmean可以改成sum或none。改成none之后返回的是逐元素损失方便你自己做加权或者 mask比如在序列任务里把 padding 位置排除掉。手写的时候要自己处理 mask容易出错。第二是数值稳定性。PyTorch 的 CUDA 实现里这些 loss 的前向和反向都是融合过的 kernel对半精度fp16和 bf16 有专门优化。手写的话中间张量数量变多显存占用和耗时都会差一点混合精度下更容易溢出。第三是自动微分路径更短。nn.MSELoss在反向传播时可以直接算出梯度而手写版本需要 autograd 走完 abs、pow、mean 三个阶段图更大。import torch import torch.nn as nn pred torch.tensor([2.5, 0.0, 2.1, 7.8]) target torch.tensor([3.0, -0.5, 2.0, 8.0]) mae_loss nn.L1Loss(reductionmean) mse_loss nn.MSELoss(reductionmean) print(mae_loss(pred, target)) # tensor(0.4250) print(mse_loss(pred, target)) # tensor(0.2875) print(torch.sqrt(mse_loss(pred, target))) # tensor(0.5362) 就是 RMSE # 手写等价实现 print((pred - target).abs().mean()) # tensor(0.4250) print(((pred - target) ** 2).mean()) # tensor(0.2875)这里有个细节值得说nn.L1Loss在 PyTorch 2.0 之后支持reductionnone和reductionmean但如果你想做的是沿特征维算 L1 范数再沿样本维平均那就不能直接用nn.L1Loss得手写两步。因为nn.L1Loss的 mean 是把所有元素一起平均的不会区分维度。3.3 dim 和 keepdim 参数到底影响什么张量形状是 (B, D) 的时候torch.linalg.vector_norm(x, ord1, dim1)会把每一行当作一个向量算出 B 个标量返回形状 (B,)。dim0则是把每一列当作向量返回形状 (D,)。这个区别在 batch 训练里至关重要。x torch.randn(4, 3) # 沿最后一维特征维算 L1 范数得到每个样本的范数 per_sample torch.linalg.vector_norm(x, ord1, dim1) print(per_sample.shape) # torch.Size([4]) # 沿 batch 维算得到每个特征的范数 per_feature torch.linalg.vector_norm(x, ord1, dim0) print(per_feature.shape) # torch.Size([3]) # 加上 keepdim 保留被约减的那一维 per_sample_keep torch.linalg.vector_norm(x, ord1, dim1, keepdimTrue) print(per_sample_keep.shape) # torch.Size([4, 1])keepdimTrue在需要后续广播的时候很有用。比如你想把每个样本的范数当作权重再乘回原张量两个形状必须能广播(B,) 和 (B, D) 是广播不了的但 (B, 1) 和 (B, D) 可以。这个小技巧在写归一化层或者注意力掩码的时候经常用到。还有一个容易忘的点dim可以传负数。dim-1表示最后一维写起来比记住具体维度号更安全尤其是张量形状会随代码修改而变的时候。# 推荐写法不依赖具体维度号 norms torch.linalg.vector_norm(x, ord2, dim-1, keepdimTrue) x_normalized x / (norms 1e-8)这里的1e-8是防止除零也是 L2 归一化里几乎必加的一个小常数。4. 梯度行为差异与选型决策4.1 L1 在零点附近的次梯度与稀疏性L1 损失的梯度是残差的符号函数$\partial |r|/\partial r \text{sign}(r)$。这个梯度的特点是不管残差是 0.01 还是 100梯度的大小都是 1在 PyTorch 里torch.sign(0)返回 0所以零点处梯度是 0。这带来了两个后果。好处是它对离群值不敏感。一个预测错得离谱的样本在 L1 下贡献的梯度和一个错得轻微的样本一样大不会把整个 batch 的梯度方向带偏。坏处是它在接近最优解的时候会来回震荡因为梯度大小恒定没法像 MSE 那样随着误差减小而自然衰减到 0。所以用 MAE 训练的时候通常需要配一个学习率衰减策略后期把步长降下来才能在最优解附近稳住。稀疏性是 L1 的另一个特性。如果你用 L1 惩罚网络权重损失项是 $\lambda \sum_i |w_i|$对应的梯度是 $\lambda \cdot \text{sign}(w_i)$这个梯度会把权重往 0 的方向推且推的力度恒定。当权重已经很小的时候L2 正则的梯度 $2\lambda w_i$ 也趋近于 0 了推力消失权重会停在很小的非零值上而 L1 的推力还是 $\lambda$足够把权重直接压到 0。这就是 L1 能产生稀疏权重、L2 只能产生小权重的根本原因。4.2 MSE 对离群点的二次放大MSE 的梯度是 $2r/n$和残差本身成正比。一个残差是 10 的样本梯度是残差为 1 的样本的 10 倍损失贡献是 100 倍。这个平方关系在数据干净的时候是好事收敛快、目标明确在数据有噪声的时候就是灾难模型会被少数几个异常样本牵着走。我做过一个对比实验造了 1000 个服从 $y 2x 1 \epsilon$ 的样本$\epsilon$ 是标准差 0.5 的高斯噪声然后人为把其中 20 个样本的 y 值加上 20。用同样的网络结构和学习率分别训 200 轮结果差异很明显损失函数干净数据的测试 MAE含离群点数据的测试 MAE训练稳定性MSE0.381.67前 30 轮损失剧烈波动MAE0.410.45全程平稳下降Smooth L1beta1.00.390.58前期波动小后期平稳从表里能看出MSE 在含离群点的数据上测试误差涨了 4 倍多而 MAE 几乎没受影响。Smooth L1 介于两者之间因为它在残差小于 beta 的时候用平方项保证小误差下梯度平滑大于 beta 的时候用线性项避免大误差被放大。实操心得如果你的数据里存在无法清洗的离群点第一选择是 Huber/Smooth L1它对两类误差的优点都保留了一部分。PyTorch 里对应nn.HuberLoss(delta1.0)和nn.SmoothL1Loss(beta1.0)两者形式略有差异Huber 是标准的 Huber 定义SmoothL1 是 Fast R-CNN 里用的变体beta 为 0 时退化成 L1。4.3 损失函数选型决策表选哪个损失不看哪个更高级而是看你的数据特性和任务要求。我整理了一张表实际项目里可以直接对号入座。场景推荐损失理由数据干净追求极致精度MSE / L2梯度随误差衰减能收敛到很精确的解数据含离群点MAE / L1 / Huber对极端误差不敏感需要稀疏权重加 L1 正则项恒定的压缩力能把小权重推向 0需要平滑权重、防过拟合加 L2 正则weight_decay权重整体被压制不会出现极端值目标值跨越多个数量级对数变换后 MSE 或 RMSLE直接 MSE 会被大值主导需要可解释的平均误差训练用 MSE报告用 RMSE训练效率与人类可读性兼顾多任务学习各任务量纲不同各任务用 MAE 或先归一化再 MSE量纲统一后再加权更合理最后一行值得展开说一句。多任务学习里如果任务 A 的损失是 0.1任务 B 的损失是 1000直接相加的话任务 B 完全主导梯度。这时候要么把每个任务的目标值先归一化到同一量级要么把损失换成 MAE 这种尺度更温和的形式要么给不同任务手动加权。我一般倾向先做目标归一化因为这样损失数值本身就能反映训练状态不用记一堆权重系数。5. 完整实战三种损失的对比实验5.1 造数据与模型搭建为了让你能直接复现我用一段自包含的代码把三种损失在同一份数据上跑一遍。数据用一维回归加一段人为的离群点这样差异会很明显。import torch import torch.nn as nn import numpy as np torch.manual_seed(42) np.random.seed(42) # 生成干净数据 y 2x 1 noise n_train 800 n_val 200 X_train torch.rand(n_train, 1) * 10 - 5 y_train 2 * X_train 1 torch.randn(n_train, 1) * 0.5 X_val torch.rand(n_val, 1) * 10 - 5 y_val 2 * X_val 1 torch.randn(n_val, 1) * 0.5 # 人为注入 5% 的离群点只污染训练集 n_outlier int(0.05 * n_train) outlier_idx torch.randperm(n_train)[:n_outlier] y_train[outlier_idx] torch.randn(n_outlier, 1) * 20 # 一个简单的两层 MLP def make_model(): return nn.Sequential( nn.Linear(1, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 1), )模型故意做得很小因为这里要观察的是损失函数的行为差异网络容量大了反而会掩盖掉。输入特征做了中心化处理减 5 再乘 10 那步让数据落在 [-5, 5]这样初始学习率 0.01 就能跑得动不用额外做标准化。5.2 训练循环与评估代码训练部分写成函数接受不同的损失函数对象其余配置完全一致保证对比公平。def train(loss_fn, epochs200, lr1e-2): model make_model() optimizer torch.optim.Adam(model.parameters(), lrlr) # 后期降学习率让 MAE 这种恒定梯度的方法能稳定下来 scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) history [] for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(X_train) loss loss_fn(pred, y_train) loss.backward() optimizer.step() scheduler.step() # 评估统一用 MAE 和 RMSE和训练损失解耦 model.eval() with torch.no_grad(): val_pred model(X_val) val_mae (val_pred - y_val).abs().mean().item() val_rmse torch.sqrt(((val_pred - y_val) ** 2).mean()).item() history.append((epoch, val_mae, val_rmse)) return history results {} results[MSE] train(nn.MSELoss()) results[MAE] train(nn.L1Loss()) results[Huber] train(nn.HuberLoss(delta1.0))这里有两个设计细节值得说明。一是评估阶段统一用 MAE 和 RMSE和训练用的损失分开这样三种方法才有可比性——如果用各自的训练损失去比较数值根本不在一个量纲上。二是加了余弦退火的学习率调度因为 MAE 的梯度是恒定的不打调度后期会一直在最优点附近抖动收敛曲线会很难看这不是损失函数的缺点是优化策略没配合好。训练完之后把最终结果和最优轮次的指标打印出来for name, hist in results.items(): best min(hist, keylambda x: x[1]) final hist[-1] print(f{name:8s} | best epoch {best[0]:3d} | val MAE {best[1]:.4f} | fval RMSE {best[2]:.4f} | final MAE {final[1]:.4f})5.3 结果对比与解读跑完之后大致会看到这样的模式具体数值受随机种子影响但趋势稳定损失最优验证 MAE最优验证 RMSE训练后期是否抖动MSE0.62 ~ 0.700.85 ~ 0.95是RMSE 偏高MAE0.44 ~ 0.480.55 ~ 0.60否平稳Huber0.45 ~ 0.490.56 ~ 0.62轻微MSE 在含离群点的数据上表现最差原因前面已经分析了。但这里还有一个更细的观察MSE 的 RMSE 和 MAE 之间的差距比其他两个损失大得多。MSE 的 MAE 大约 0.65RMSE 大约 0.90差了 0.25而 MAE 损失的这两个值只差 0.12 左右。这说明 MSE 训练出来的模型在少数样本上错得非常厉害拉高了 RMSE。反过来如果你把离群点去掉再跑一遍会发现 MSE 的表现反超 MAE——数据干净的时候MSE 的二次惩罚能提供更精确的梯度信息收敛到的最优解更接近真实参数。这个反转很有意思也是我在多个项目里反复验证过的损失函数没有绝对的好坏只有和当前数据分布匹不匹配。还有一个现象值得留意MAE 损失训出来的模型预测值往往更保守倾向于预测中位数而不是均值。因为 MAE 的最优解是中位数MSE 的最优解是均值。在偏态分布的数据上这个差异会直接体现在预测结果上——如果你做的是房价预测而房价分布右偏MAE 训出来的模型会低估高价房MSE 训出来的模型会被少数豪宅拉高整体预测。选哪个取决于你的业务更在意哪一类错误。6. 正则化场景下的 L1/L2权重衰减怎么配6.1 Lasso 与 Ridge 在 PyTorch 里的实现方式L1 和 L2 除了当损失函数还经常以正则项的身份出现也就是经典的 Lasso 和 Ridge。区别在于损失函数里的 L1/L2 是对残差算的正则项里的 L1/L2 是对模型权重算的。数学形式上的差异只是作用对象不同但工程实现上完全是两码事。L2 正则可以直接通过优化器的weight_decay参数实现一行搞定optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)L1 正则没有现成的优化器参数需要手动加到损失里def l1_regularization(model, lam1e-4): reg 0.0 for name, param in model.named_parameters(): if weight in name: # 通常不对 bias 做正则 reg reg param.abs().sum() return lam * reg loss criterion(pred, target) l1_regularization(model, lam1e-4)注意这里我用param.abs().sum()而不是torch.linalg.vector_norm(param, ord1)因为对于权重矩阵来说L1 正则通常是把所有元素绝对值加起来不区分维度。如果写成vector_norm(param, ord1, dim1).sum()那语义就变了是先按行求 L1 范数再把每行的范数相加结果虽然数值相同因为 L1 范数满足可加性但对梯度的语义解释不同。数值上确实一样但写清楚意图对后续维护更友好。6.2 AdamW 与 Adam 的 weight_decay 差异这里有一个非常经典的坑值得单独拿出来说。在 PyTorch 里torch.optim.Adam(weight_decay...)和torch.optim.AdamW(weight_decay...)的行为是不一样的。Adam 的 weight_decay 是把 L2 正则项加进梯度里然后让 Adam 的自适应学习率机制去缩放它。这导致了一个问题对于梯度一直很大的参数实际的正则效果会被削弱对于梯度很小的参数正则效果会被放大。这和 L2 正则的初衷对所有权重均匀施加压缩不符。AdamW 把权重衰减从梯度计算里解耦出来直接对参数做一个固定的衰减 $w \leftarrow w - \eta \lambda w$不再经过自适应学习率的调整。这样正则的强度就只由学习率和 lambda 决定可控性高很多。# 老写法正则强度会被自适应学习率扭曲 optimizer_a torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-2) # 推荐写法解耦的权重衰减 optimizer_b torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-2)同样的权重衰减系数在两个优化器下得到的正则强度可能差几倍。如果你从 Adam 切换到 AdamW 而没调整 lambda会明显感觉正则变强了模型欠拟合。我的经验是先用 AdamWlambda 从 1e-4 到 1e-2 之间按对数尺度试几个值观察验证损失和训练损失的差值差值小说明正则偏弱差值大说明正则过强。6.3 手写正则项时的注意事项手动加 L1 或者 L2 正则的时候有几个细节容易忽略。一是正则项要不要参与 loss 的 reduction。通常做法是正则项单独算不和主损失一起被 mean 掉。如果你写loss criterion(pred, target) lam * reg主损失是平均过的正则项是求和过的两者的尺度就没有对齐。当 batch size 变化的时候这个不对齐会被放大。稳妥的做法是把正则项也除以 batch size或者干脆用一个很小的 lam 让尺度差异不那么重要。二是要不要对 bias 和归一化层的参数做正则。标准的 L2 正则通常只作用在权重矩阵上不动 bias 和 BatchNorm 的 gamma/beta。因为 bias 数量少正则它意义不大BatchNorm 的 gamma 是缩放系数正则它反而会伤害表达能力。所以在手写正则的时候用named_parameters()过滤一下名字是很有必要的。三是多卡训练时正则项要小心重复计算。如果你用 DataParallelmodel 在各卡上副本的参数是同步的但正则项如果在 forward 里算可能会被算多次。这种情况建议把正则项的计算放在 forward 外面用model.module去访问真正的参数。注意AdamW 的 weight_decay 默认是 1e-2直接拿过来用在你的任务上不一定合适。小数据集上 1e-2 可能过强大数据集上 1e-4 可能太弱。别照搬要试。7. 常见问题与排查速查表7.1 损失变 nan 或 inf 的几种根源损失突然变成 nan是训练中最让人头疼的问题之一而 MSE 比 MAE 更容易出这个问题因为它有平方运算。常见原因有这几类。输入数据里有 nan 或 inf前向传播之后就污染了所有输出这个用torch.isnan(x).any()检查一遍就能定位。学习率太大导致参数爆炸几轮之后权重变成极大值平方之后直接溢出这时候把学习率降一个数量级通常能解决。混合精度训练下fp16 的表示范围有限MSE 的平方和容易超过 65504 溢出需要用torch.cuda.amp.GradScaler做梯度缩放。还有一种是开方导致的问题——如果你手写 RMSE 而 MSE 算出来是负数通常是浮点误差导致极小负数开方就变成 nan这时候需要在开方前加torch.clamp(mse, min1e-12)。# 防 nan 的 RMSE 计算 mse ((pred - target) ** 2).mean() rmse torch.sqrt(torch.clamp(mse, min1e-12))MAE 因为不涉及平方数值上稳定得多但在混合精度下如果某个残差特别大abs本身没问题问题出在它后面接的梯度累加同样需要用 GradScaler。7.2 数值精度、dtype 与设备不一致还有一个非常隐蔽的问题dtype 和设备不一致导致的静默错误。比如pred是 float32 在 GPU 上target是 float64 在 CPU 上PyTorch 会抛一个类型错误这个还好排查。麻烦的是pred是 float16target是 float32算出来的 loss 会被提升到 float32看起来没问题但反向传播时梯度可能因为精度丢失变得很小训练不动。设备不一致也是类似。两个张量在不同 GPU 上做运算PyTorch 会报错这个容易发现。但如果是在多进程数据加载里某个张量被意外留在 CPU整个训练会被拖慢几十倍而且不报错只能通过 profiler 或者手动打印.device来定位。# 训练循环开始时统一检查一次省得后面查半天 assert pred.device target.device, f{pred.device} vs {target.device} assert pred.dtype target.dtype, f{pred.dtype} vs {target.dtype}这两行 assertion 建议加在调试阶段确认没问题之后再删掉能省掉大量排查时间。特别是从别人那里接手代码的时候先跑一遍看有没有类型和设备不匹配往往能发现几个隐藏的坑。7.3 问题排查速查表最后把常见的症状和对应的排查方向整理成表出问题的时候可以直接对照。症状可能原因排查方法loss 是 nan输入含 nan或学习率过大检查输入降低 lrloss 是 inf平方后溢出fp16 范围不够换 fp32 或加 GradScalerloss 不下降学习率太小或损失量纲和初始化不匹配试大 lr检查输出范围MAE 后期来回震荡恒定梯度导致无法收敛到精确解加学习率衰减MSE 对个别样本过敏感数据含离群点换 Huber 或先清洗数据RMSE 远大于 MAE存在极端误差样本看残差分布考虑 Huberweight_decay 效果不明显用的 Adam 而非 AdamW换 AdamW重调 lambdaL1 正则后权重不稀疏lambda 太小或没做归一化加大 lambda检查尺度手写损失比内置慢很多中间张量多没用融合 kernel优先用 nn 模块多步预测损失量纲不对reduction 分母算错确认是 B 还是 BTD表里的每一条我都在实际项目里踩过。特别是最后一条多步预测任务里损失按 sum 归约、报告按 mean 换算中间差了一个 T 倍对不上账的时候查了半天以为是模型问题结果只是归约方式没对齐。最后分享一个小习惯。我在每个涉及损失函数的新项目里都会先写一个十几行的自检脚本造一批固定的输入和目标把各种损失、范数、RMSE 的数值都算一遍和手算的结果对一下。这个脚本跑通之后才敢把它用到正式训练里。看起来是多花十分钟但它能保证你后面几个小时的调参不会建立在一个算错的损失上。这个习惯是我在被 RMSE 的 reduction 问题坑了整整一个下午之后养成的至今没再出过类似的错。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →