ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PyTorch与TensorFlow2损失函数全解析:从原理到实战避坑指南

PyTorch与TensorFlow2损失函数全解析:从原理到实战避坑指南 损失函数这东西刚入门的时候觉得它就是个公式nn.CrossEntropyLoss()一行调用完事。但真正做过几个项目之后你会发现模型不收敛、梯度爆炸、训练到一半loss突然变NaN十有八九都跟损失函数的选择和使用方式有关。我见过太多人把模型结构调了又调最后发现是损失函数用错了——比如多分类任务用了二分类的损失或者回归任务忘了对输出做归一化就直接上MSE。这篇内容我打算把PyTorch和TensorFlow2两大框架下的损失函数做一个系统性的梳理和对照。不是那种列个API文档就完事的汇总而是从“为什么需要这个损失函数”出发讲清楚每个函数的数学直觉、适用场景、在两大框架里的具体写法差异以及我在实际项目中踩过的那些坑。无论你是刚接触深度学习的新手还是已经用过几个框架但想系统整理一下知识体系的老手应该都能从中找到有用的东西。1. 损失函数到底在做什么从直觉到数学1.1 损失函数的本质角色先把最核心的问题说清楚损失函数到底在干什么用一句话概括就是——它负责告诉模型“你现在的预测有多离谱”。模型训练的过程本质上就是不断调整参数让这个“离谱程度”越来越小。举个生活化的例子。你教一个小孩认猫和狗每次他指错了你会告诉他“错了”。但“错了”这个信息太模糊了——是差一点就对了还是完全搞反了损失函数的作用就是把这个“错了”量化成一个具体的数值。预测得越离谱数值越大预测得越准数值越接近零。然后模型根据这个数值计算梯度决定每个参数该往哪个方向调、调多少。这里有个关键点很多人一开始会忽略损失函数不只是衡量对错它还决定了梯度的形状。不同的损失函数即使面对同一个错误预测给出的梯度大小和方向可能完全不同。这就是为什么有些任务用MSE训练得很慢换成交叉熵就快很多——因为交叉熵对错误预测的惩罚更“陡峭”梯度更大参数更新更果断。1.2 从最大似然到损失函数如果只停留在“衡量对错”这个层面很多损失函数的设计看起来会很随意。但如果你从最大似然估计的角度去理解一切就顺理成章了。假设我们的模型输出的是一个概率分布我们希望这个分布尽可能接近真实分布。最大似然估计的思路是找到一组参数使得在这组参数下观测到当前训练数据的概率最大。把这个想法取负对数就变成了最小化负对数似然。而交叉熵损失本质上就是负对数似然。同理MSE可以从高斯分布的最大似然推导出来——假设预测误差服从高斯分布最大化似然函数等价于最小化均方误差。这就是为什么回归任务默认用MSE它隐含了“误差是高斯噪声”这个假设。理解这一层之后你选损失函数就不是凭感觉了。你的数据分布是什么样的就用对应的损失函数。数据里有大量离群点高斯假设不成立MSE就会被离群点带偏这时候该考虑MAE或者Huber。类别极度不平衡交叉熵会被多数类主导得考虑Focal Loss或者带权重的交叉熵。1.3 PyTorch与TensorFlow2的设计哲学差异两大框架在损失函数的设计上有明显的风格差异这个差异在后续每个具体函数里都会体现出来。PyTorch的风格是模块化和显式。torch.nn下的损失函数都是nn.Module的子类调用时需要先实例化再传入预测值和目标值。比如criterion nn.CrossEntropyLoss()然后loss criterion(output, target)。这种设计的好处是灵活——你可以在实例化时配置各种参数权重、忽略索引、标签平滑等而且自定义损失函数非常方便继承nn.Module重写forward就行。TensorFlow2的风格是函数式和集成化。tf.keras.losses下的损失函数大多可以直接调用也支持在model.compile()时用字符串指定。比如model.compile(losscategorical_crossentropy)。TF2更强调“开箱即用”很多损失函数会自动处理一些边界情况比如数值稳定性但自定义的灵活度相对低一些。实际使用中PyTorch的显式风格在调试时更有优势——你可以单独把损失函数拿出来测试传入构造的预测值和目标值看输出是否符合预期。TF2的集成风格在快速原型阶段更省事但出了问题排查起来链路更长。2. 分类任务的损失函数从二分类到多标签2.1 二分类BCE与BCEWithLogits二分类是最基础也最容易被低估的场景。PyTorch提供了两个版本nn.BCELoss和nn.BCEWithLogitsLoss。这两个的区别值得单独说清楚因为用错了会导致训练不稳定甚至完全训不动。nn.BCELoss要求输入已经经过Sigmoid激活值在0到1之间。如果你直接把logits传进去会得到一个负数或者大于1的值计算log的时候就会出现NaN。nn.BCEWithLogitsLoss则是在内部先做Sigmoid再做BCE而且用了log-sum-exp技巧保证数值稳定性。import torch import torch.nn as nn # 推荐写法直接传logits criterion nn.BCEWithLogitsLoss() logits torch.randn(4, 1) # 未经过Sigmoid target torch.tensor([[1.0], [0.0], [1.0], [0.0]]) loss criterion(logits, target) # 不推荐先Sigmoid再BCELoss数值稳定性差 sigmoid nn.Sigmoid() bce nn.BCELoss() loss2 bce(sigmoid(logits), target)实测下来BCEWithLogitsLoss在训练初期优势特别明显。因为Sigmoid在输入绝对值较大时梯度会饱和先Sigmoid再算BCE的话梯度会经过两层链式法则容易出现梯度消失。而合并后的版本在反向传播时梯度路径更短收敛更稳。TensorFlow2里对应的是tf.keras.losses.BinaryCrossentropy(from_logitsTrue)。注意from_logits这个参数默认是False意味着它期望输入已经过Sigmoid。如果你传的是logits但忘了设from_logitsTrue结果会完全错误。import tensorflow as tf # 推荐from_logitsTrue bce tf.keras.losses.BinaryCrossentropy(from_logitsTrue) loss bce(tf.constant([[1.0], [0.0]]), tf.constant([[2.0], [-1.0]])) # 如果输入已经过Sigmoid bce2 tf.keras.losses.BinaryCrossentropy(from_logitsFalse)2.2 多分类CrossEntropyLoss的输入格式陷阱多分类任务里nn.CrossEntropyLoss是使用频率最高的损失函数。但它有一个非常容易踩的坑它期望的输入是未经过Softmax的logits而且target是类别索引而不是one-hot编码。# 正确用法 criterion nn.CrossEntropyLoss() logits torch.randn(8, 10) # batch_size8, num_classes10 target torch.tensor([3, 7, 1, 0, 5, 2, 9, 4]) # 类别索引 loss criterion(logits, target) # 错误用法1传了one-hot编码 target_onehot torch.zeros(8, 10) target_onehot.scatter_(1, target.unsqueeze(1), 1) # loss criterion(logits, target_onehot) # 会报错或结果错误 # 错误用法2先Softmax再传进去 # loss criterion(torch.softmax(logits, dim1), target) # 相当于做了两次SoftmaxPyTorch的CrossEntropyLoss内部已经包含了LogSoftmax和NLLLoss两步。如果你在外面又做了一次Softmax相当于对概率分布再做一次Softmax输出会变得非常平滑梯度会变得很小训练几乎停滞。这个坑我在早期项目中踩过当时loss下降极慢排查了半天才发现是多了个Softmax。TensorFlow2的对应函数是tf.keras.losses.CategoricalCrossentropy和SparseCategoricalCrossentropy。前者期望one-hot标签后者期望整数索引标签。同样有from_logits参数需要设置。# one-hot标签 cce tf.keras.losses.CategoricalCrossentropy(from_logitsTrue) # 整数索引标签 scce tf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue)2.3 多标签分类BCEWithLogitsLoss的正确打开方式多标签分类一张图里同时有猫和狗和二分类的区别在于每个样本可以属于多个类别类别之间不互斥。这时候不能用Softmax因为Softmax会强制所有类别的概率和为1而多标签场景下每个类别应该是独立的Sigmoid。PyTorch里直接用nn.BCEWithLogitsLosstarget是形状和输出一样的0/1矩阵。TensorFlow2里用BinaryCrossentropy(from_logitsTrue)但要注意输出的最后一维应该是每个类别的独立logit。# PyTorch多标签 criterion nn.BCEWithLogitsLoss() logits torch.randn(4, 5) # 4个样本5个标签 target torch.tensor([[1,0,1,0,0], [0,1,0,1,0], [1,1,0,0,1], [0,0,1,1,0]], dtypetorch.float32) loss criterion(logits, target)这里有个经验多标签任务里如果某些标签的样本极少比如1000个样本里只有3个正例BCE会被负例主导模型倾向于全部预测为0。这时候需要给正例加权pos_weight参数就是干这个的。# 假设正例比例约为1:10 pos_weight torch.tensor([10.0, 10.0, 10.0, 10.0, 10.0]) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight)2.4 Focal Loss解决类别极度不平衡Focal Loss最初是在目标检测领域提出的用来解决正负样本极度不平衡的问题比如一张图里背景框几万个目标框可能只有几个。它的核心思想是降低容易分类样本的权重让模型聚焦在难分类样本上。公式是FL(p_t) -alpha * (1 - p_t)^gamma * log(p_t)。当gamma0时退化为标准交叉熵。gamma越大对易分类样本的压制越强。通常gamma2效果比较好。PyTorch没有内置Focal Loss需要自己实现class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2, reductionmean): super().__init__() self.alpha alpha self.gamma gamma self.reduction reduction def forward(self, inputs, targets): bce_loss nn.functional.binary_cross_entropy_with_logits( inputs, targets, reductionnone) pt torch.exp(-bce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * bce_loss if self.reduction mean: return focal_loss.mean() elif self.reduction sum: return focal_loss.sum() return focal_lossTensorFlow2里可以用tf.keras.losses.BinaryCrossentropy配合自定义包装来实现或者直接用tf.nn.sigmoid_focal_crossentropyTF 2.8 提供了内置实现。注意Focal Loss的alpha和gamma需要根据具体任务调。我试过在某个工业缺陷检测任务里gamma2配合alpha0.75效果最好但换到另一个数据集上gamma1反而更稳。没有万能参数得实验。3. 回归任务的损失函数MSE不是唯一选择3.1 MSE与MAE的取舍回归任务里MSE均方误差和MAE平均绝对误差是最常用的两个。MSE对大误差惩罚更重平方项MAE对所有误差一视同仁绝对值。# PyTorch mse nn.MSELoss() mae nn.L1Loss() # TensorFlow2 mse tf.keras.losses.MeanSquaredError() mae tf.keras.losses.MeanAbsoluteError()选择哪个取决于你的数据。如果数据里没有极端离群点MSE收敛更快梯度随误差线性增长误差大时梯度大。如果数据里有离群点MSE会被这些点带偏模型会牺牲整体精度去拟合离群点这时候MAE更鲁棒。但MAE有个问题在零点附近梯度不连续而且梯度大小恒定训练后期容易在最优解附近震荡。实际项目中我更多用Huber Loss或者Smooth L1 Loss来折中。3.2 Huber Loss与Smooth L1折中方案Huber Loss的思路是误差小的时候用平方保证梯度平滑误差大的时候用绝对值保证鲁棒性。有一个delta参数控制切换点。# PyTorch huber nn.HuberLoss(delta1.0) smooth_l1 nn.SmoothL1Loss(beta1.0) # TensorFlow2 huber tf.keras.losses.Huber(delta1.0)nn.SmoothL1Loss和nn.HuberLoss在PyTorch里几乎等价区别在于SmoothL1Loss的beta参数和HuberLoss的delta参数含义相同都是切换阈值。实测下来delta1.0在大多数回归任务里是个不错的起点。3.3 对数空间损失处理跨数量级的目标有些回归任务的目标值跨度极大比如房价预测几十万到几千万、销量预测个位数到几百万。直接在这些值上算MSE大值会主导损失小值几乎被忽略。一个常用的技巧是对目标做对数变换然后在变换后的空间里算MSE。这样相当于让模型学习相对误差而不是绝对误差。# 训练时对目标取log target_log torch.log1p(target) # log(1x)避免x0时的问题 loss nn.MSELoss()(pred, target_log) # 推理时记得还原 pred_original torch.expm1(pred)TensorFlow2里同样可以用tf.math.log1p和tf.math.expm1。这个技巧在Kaggle的很多回归比赛里是标配效果提升明显。4. 特殊场景下的损失函数选择4.1 目标检测IoU系列损失目标检测的损失函数比分类和回归复杂得多通常由分类损失和边界框回归损失两部分组成。边界框回归从最早的L1/L2损失发展到IoU Loss、GIoU Loss、DIoU Loss、CIoU Loss再到最近的Inner-IoU。IoU Loss直接优化预测框和真实框的交并比比L1/L2更符合检测的评价指标。但IoU在框不重叠时梯度为零GIoU通过引入最小闭包区域解决了这个问题。DIoU进一步考虑了中心点距离CIoU再加上长宽比。# 简化的IoU Loss实现 def iou_loss(pred_boxes, target_boxes): # pred_boxes, target_boxes: [x1, y1, x2, y2] inter_x1 torch.max(pred_boxes[:, 0], target_boxes[:, 0]) inter_y1 torch.max(pred_boxes[:, 1], target_boxes[:, 1]) inter_x2 torch.min(pred_boxes[:, 2], target_boxes[:, 2]) inter_y2 torch.min(pred_boxes[:, 3], target_boxes[:, 3]) inter_area torch.clamp(inter_x2 - inter_x1, min0) * \ torch.clamp(inter_y2 - inter_y1, min0) pred_area (pred_boxes[:, 2] - pred_boxes[:, 0]) * \ (pred_boxes[:, 3] - pred_boxes[:, 1]) target_area (target_boxes[:, 2] - target_boxes[:, 0]) * \ (target_boxes[:, 3] - target_boxes[:, 1]) union_area pred_area target_area - inter_area iou inter_area / (union_area 1e-7) return 1 - iouYOLOv8的损失函数就是CIoU Loss加上DFLDistribution Focal Loss和BCE分类损失的组合。如果你想画YOLOv8的损失曲线训练过程中会输出box_loss、cls_loss、dfl_loss三条曲线分别对应边界框回归、分类和分布焦点损失。4.2 对比学习InfoNCE LossInfoNCE Loss是对比学习的核心损失函数用在SimCLR、MoCo等自监督学习方法里。它的思路是给定一个锚点样本让它在嵌入空间里靠近正样本远离负样本。公式是L -log(exp(sim(z_i, z_j)/tau) / sum(exp(sim(z_i, z_k)/tau)))其中tau是温度系数控制分布的锐利程度。def info_nce_loss(features, temperature0.07): # features: [2*batch_size, dim]前一半和后一半互为增强视图 batch_size features.shape[0] // 2 labels torch.cat([torch.arange(batch_size) for _ in range(2)], dim0) labels (labels.unsqueeze(0) labels.unsqueeze(1)).float() features nn.functional.normalize(features, dim1) similarity_matrix torch.matmul(features, features.T) # 去掉对角线 mask torch.eye(labels.shape[0], dtypetorch.bool) labels labels[~mask].view(labels.shape[0], -1) similarity_matrix similarity_matrix[~mask].view(similarity_matrix.shape[0], -1) positives similarity_matrix[labels.bool()].view(labels.shape[0], -1) negatives similarity_matrix[~labels.bool()].view(similarity_matrix.shape[0], -1) logits torch.cat([positives, negatives], dim1) labels torch.zeros(logits.shape[0], dtypetorch.long) logits logits / temperature return nn.CrossEntropyLoss()(logits, labels)温度系数tau很关键。太小的话模型只关注最难的负样本训练不稳定太大的话所有负样本权重差不多学不到有区分度的特征。0.07是SimCLR里的默认值但实际任务里可以试试0.05到0.2之间的值。4.3 生成模型Wasserstein距离与对抗损失WGAN里用Wasserstein距离替代了原始GAN的JS散度解决了训练不稳定和模式坍塌的问题。Wasserstein距离的直观理解是“把一堆土搬到另一堆土所需的最小代价”比JS散度更平滑即使两个分布不重叠也能提供有意义的梯度。在WGAN-GP里损失函数加上了一个梯度惩罚项def gradient_penalty(critic, real, fake, device): batch_size real.shape[0] alpha torch.rand(batch_size, 1, 1, 1).to(device) interpolated alpha * real (1 - alpha) * fake interpolated.requires_grad_(True) critic_interpolated critic(interpolated) gradients torch.autograd.grad( outputscritic_interpolated, inputsinterpolated, grad_outputstorch.ones_like(critic_interpolated), create_graphTrue, retain_graphTrue )[0] gradients gradients.view(batch_size, -1) gp ((gradients.norm(2, dim1) - 1) ** 2).mean() return gp这个梯度惩罚项的作用是约束判别器的梯度范数接近1保证Wasserstein距离的Lipschitz连续性。实际训练时lambda_gp通常设10但也可以根据情况调整。5. 两大框架损失函数的对照与迁移5.1 常用损失函数对照表任务类型PyTorchTensorFlow2注意事项二分类nn.BCEWithLogitsLossBinaryCrossentropy(from_logitsTrue)不要提前做Sigmoid多分类nn.CrossEntropyLossSparseCategoricalCrossentropy(from_logitsTrue)target是索引不是one-hot多分类(one-hot)nn.CrossEntropyLossCategoricalCrossentropy(from_logitsTrue)PyTorch不直接支持one-hot多标签nn.BCEWithLogitsLossBinaryCrossentropy(from_logitsTrue)每个标签独立Sigmoid回归nn.MSELossMeanSquaredError注意离群点影响回归(鲁棒)nn.HuberLossHuberdelta/beta参数需调Focal Loss需自定义tf.nn.sigmoid_focal_crossentropygamma和alpha需实验5.2 从PyTorch迁移到TensorFlow2的注意事项如果你习惯了PyTorch的写法切到TensorFlow2时最容易犯的几个错误第一忘了设from_logitsTrue。PyTorch的CrossEntropyLoss默认就是接收logits而TF2的CategoricalCrossentropy默认from_logitsFalse。这个默认值的差异导致很多人迁移代码后loss异常。第二reduction参数的默认值不同。PyTorch的损失函数默认reductionmeanTF2的默认也是求平均但有些函数的行为不完全一致。比如TF2的BinaryCrossentropy在reductionSUM_OVER_BATCH_SIZE时如果batch size是动态的可能会有警告。第三自定义损失函数的写法差异。PyTorch继承nn.Module重写forwardTF2继承tf.keras.losses.Loss重写call。# PyTorch自定义 class MyLoss(nn.Module): def __init__(self): super().__init__() def forward(self, y_pred, y_true): return torch.mean((y_pred - y_true) ** 2) # TensorFlow2自定义 class MyLoss(tf.keras.losses.Loss): def __init__(self): super().__init__() def call(self, y_true, y_pred): return tf.reduce_mean((y_pred - y_true) ** 2)注意参数顺序PyTorch是(y_pred, y_true)TF2是(y_true, y_pred)。这个顺序搞反了不会报错但结果完全错误而且很难发现。5.3 损失函数与优化器的配合损失函数和优化器是搭档配合不好训练效果会大打折扣。几个经验Adam CrossEntropy最通用的组合大多数分类任务直接上这个就行。SGD Momentum CrossEntropy在图像分类任务里SGD往往能收敛到更好的局部最优但需要更仔细地调学习率和momentum。Adam MSE回归任务常用但要注意Adam的默认学习率1e-3对MSE可能偏大容易震荡。带weight_decay的AdamW 任何损失如果模型过拟合加weight_decay比换损失函数更有效。一个容易被忽略的点损失函数的scale会影响有效学习率。比如MSE的loss值可能是几百而交叉熵的loss值通常在个位数。如果你从MSE切换到Huberloss值会突然变小这时候如果学习率不变实际更新步长会变小训练变慢。所以换损失函数时最好重新扫一下学习率。6. 损失函数调试实战从NaN到收敛6.1 Loss变成NaN的排查链路Loss变NaN是最常见也最让人头疼的问题。我总结了一个排查顺序按这个链路走基本能定位到原因第一步检查输入数据。有没有NaN或Inf用torch.isnan(x).any()或tf.reduce_any(tf.math.is_nan(x))检查。数据预处理阶段的一个除零操作就可能导致整个训练崩溃。第二步检查损失函数的输入格式。是不是把logits和概率搞混了多分类任务里target是不是传成了one-hot这些格式错误往往不会报错但会产生NaN。第三步检查学习率。学习率太大导致参数更新过猛logits变得极大或极小经过Softmax后出现0或1log(0)就是负无穷。可以试着把学习率降10倍看看。第四步检查是否有log(0)或除零。自定义损失函数里经常出现torch.log(pred)而pred可能为0的情况。加一个极小值1e-7就能解决。第五步检查混合精度训练。AMP自动混合精度在FP16下容易出现溢出导致NaN。可以先用FP32跑一遍确认模型没问题再开AMP。6.2 Loss不下降的常见原因Loss不下降比NaN更隐蔽因为训练不会报错只是效果差。几个常见原因损失函数选错了回归任务用了交叉熵或者多分类用了BCE。这种错误loss会下降但降到一个很差的水平就停了。梯度消失网络太深或者激活函数选择不当。可以检查每层的梯度范数如果接近零就是梯度消失。学习率太小loss下降极慢曲线几乎水平。试着把学习率调大10倍。数据标签有问题标签噪声太大模型学不到有效模式。可以抽样检查一些样本的标签。batch size太小梯度估计噪声大loss震荡不下降。适当增大batch size或者用梯度累积。6.3 损失曲线怎么看训练时盯着loss曲线看能发现很多问题训练loss下降但验证loss上升过拟合。加正则化、Dropout、或者早停。训练loss和验证loss都下降但很慢学习率偏小或者模型容量不够。训练loss震荡剧烈学习率偏大或者batch size太小。训练loss突然跳变可能是数据里有异常样本或者学习率调度器在某个epoch调整了学习率。验证loss比训练loss低这种情况比较少见通常是因为训练时用了Dropout或数据增强而验证时没有。画YOLOv8的损失曲线时results.csv里会记录每个epoch的train/box_loss、train/cls_loss、train/dfl_loss以及对应的验证损失。用pandas读出来直接plot就行import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) df.columns df.columns.str.strip() plt.figure(figsize(12, 4)) plt.subplot(1, 3, 1) plt.plot(df[epoch], df[train/box_loss], labeltrain) plt.plot(df[epoch], df[val/box_loss], labelval) plt.legend() plt.title(Box Loss) plt.subplot(1, 3, 2) plt.plot(df[epoch], df[train/cls_loss], labeltrain) plt.plot(df[epoch], df[val/cls_loss], labelval) plt.legend() plt.title(Cls Loss) plt.subplot(1, 3, 3) plt.plot(df[epoch], df[train/dfl_loss], labeltrain) plt.plot(df[epoch], df[val/dfl_loss], labelval) plt.legend() plt.title(DFL Loss) plt.tight_layout() plt.show()7. 自定义损失函数的正确姿势7.1 PyTorch自定义损失函数PyTorch自定义损失函数有两种方式继承nn.Module或者直接写函数。继承nn.Module的好处是可以保存状态比如可学习的参数而且和nn.Sequential等容器兼容。class WeightedMSELoss(nn.Module): def __init__(self, weightNone): super().__init__() self.register_buffer(weight, weight) def forward(self, y_pred, y_true): loss (y_pred - y_true) ** 2 if self.weight is not None: loss loss * self.weight return loss.mean()注意register_buffer的用法。如果你有一个不需要梯度但需要随模型保存到state_dict的tensor比如样本权重用register_buffer比直接赋值更规范。7.2 TensorFlow2自定义损失函数TF2里自定义损失函数可以继承tf.keras.losses.Loss也可以直接写一个接收(y_true, y_pred)的函数。继承Loss类的好处是支持reduction参数和get_config序列化。class WeightedMSELoss(tf.keras.losses.Loss): def __init__(self, weight1.0, **kwargs): super().__init__(**kwargs) self.weight weight def call(self, y_true, y_pred): loss tf.square(y_pred - y_true) * self.weight return tf.reduce_mean(loss) def get_config(self): config super().get_config() config.update({weight: self.weight}) return configget_config方法在保存和加载模型时很重要。如果没实现这个方法用model.save()保存后重新加载会报错。7.3 自定义损失函数的梯度检查自定义损失函数写完后一定要做梯度检查。PyTorch提供了torch.autograd.gradcheck可以数值验证梯度的正确性。from torch.autograd import gradcheck def my_loss(pred, target): return ((pred - target) ** 2).mean() # 构造输入 pred torch.randn(5, 3, dtypetorch.float64, requires_gradTrue) target torch.randn(5, 3, dtypetorch.float64) # 梯度检查 result gradcheck(lambda x: my_loss(x, target), (pred,)) print(result) # True表示梯度正确注意gradcheck要求输入是float64因为数值梯度的精度需要双精度来保证。实际训练用float32没问题但检查梯度时要用双精度。一个实用技巧如果你的自定义损失函数在训练中表现异常loss不下降或NaN先别急着改模型结构用gradcheck验证一下损失函数的梯度是否正确。我遇到过好几次是自定义损失函数的梯度推导有误导致训练完全跑偏。8. 损失函数选择的决策框架8.1 按任务类型快速定位面对一个新任务怎么快速确定用哪个损失函数我整理了一个决策流程第一步确定任务类型。是分类、回归、还是其他检测、分割、生成第二步确定输出形式。分类任务输出是概率还是logits回归任务输出范围有没有限制第三步检查数据特点。类别是否平衡有没有离群点目标值跨度大不大第四步选择基础损失函数。分类用交叉熵回归用MSE/Huber检测用IoU系列。第五步根据数据特点调整。不平衡用Focal Loss或加权离群点多用Huber或MAE跨度大用对数空间。8.2 多任务学习的损失权重多任务学习里多个损失函数的加权方式很关键。最简单的做法是手动设权重但调起来很麻烦。常用的自适应方法有不确定性加权让模型自己学习每个任务的权重基于任务的不确定性。GradNorm动态调整权重让不同任务的梯度范数保持平衡。动态权重平均根据每个任务loss下降的速度来调整权重。# 不确定性加权的简化实现 class UncertaintyWeighting(nn.Module): def __init__(self, num_tasks): super().__init__() self.log_sigma nn.Parameter(torch.zeros(num_tasks)) def forward(self, losses): # losses: list of scalar tensors total 0 for i, loss in enumerate(losses): precision torch.exp(-self.log_sigma[i]) total precision * loss self.log_sigma[i] return total这个方法的直觉是如果一个任务的损失很难下降不确定性高就降低它的权重让模型先学好其他任务。8.3 损失函数的常见误区最后列几个我见过最多的误区误区一损失函数越复杂越好。不是的。大多数任务用标准交叉熵或MSE就够了。复杂的损失函数往往需要更仔细的调参反而容易出问题。误区二换了损失函数就能大幅提升效果。损失函数的影响通常没有数据质量和模型结构大。如果baseline很差先检查数据和模型别急着换损失。误区三忽略损失函数的数值范围。不同损失函数的输出范围差异很大这会影响有效学习率。换损失函数时记得重新调学习率。误区四训练和推理用不同的损失函数。训练时用带权重的损失推理时用不带权重的指标评估这本身没问题。但要注意如果训练损失和评估指标差距太大说明优化目标不对需要调整。误区五不检查损失函数的输入格式。PyTorch和TF2对输入格式的要求不同迁移代码时最容易在这里出错。养成习惯每次换框架或换损失函数先用一个小batch手动验证输出是否符合预期。在实际项目中我通常会先用一个简单的损失函数跑通整个流程确认数据管道、模型结构、训练循环都没问题然后再尝试更复杂的损失函数。这样出问题时排查范围小效率高很多。损失函数的选择和调优是一个迭代的过程没有一劳永逸的答案多实验、多记录、多对比慢慢就能建立起自己的直觉。
返回列表