资讯动态

BCEWithLogitsLoss多标签分类:形状与pos_weight

发布时间:2026/10/26 3:59:53 来源:尧图企业网站定制
1. 为什么这个损失函数老是被用错BCEWithLogitsLoss在多标签分类任务里被误用的频率比很多人想象的要高。它位于 PyTorch 的torch.nn里名字已经说得很清楚Binary Cross EntropyWith Logits。但实际项目里我见过大量代码写成loss nn.BCEWithLogitsLoss()(torch.sigmoid(logits), labels)也见过标签是LongTensor直接崩在 shape 校验上更见过 logits 是[B, C]、标签是[B, 1]程序一路跑完、loss 曲线平滑下降、指标纹丝不动。多标签分类这四个字背后是每个类别独立做一次二分类判断而BCEWithLogitsLoss恰恰是最贴合的损失函数它的输入要求只有一个核心约束logits 与标签形状严格一致且标签必须是浮点型。这个约束看起来简单但踩过的人都知道PyTorch 的广播机制会在某些形状组合下默默替你修正数据让你在错误的路上一路狂奔几个 epoch。这篇内容写给三类人刚接触多标签任务、把CrossEntropyLoss的思路直接平移过来结果撞墙的新手模型能跑但指标死活上不去、怀疑损失函数写错了的中级选手以及需要在类别极度不平衡、标签带软值soft label、需要自定义阈值这些复杂场景下把损失函数用准的老手。我会把 logits 与标签的形状契约、内部数值实现、pos_weight的计算逻辑、阈值后处理、以及那些只会在真实训练日志里暴露的静默错误一条条拆开讲清楚代码全部可以直接抄走。1.1 从一次loss 在降但模型没学的排查说起几年前我接手一个多标签图像打标的任务八类标签每张图可能同时命中多个。训练脚本交接过来时 loss 从 0.69 平稳降到 0.31看起来非常健康但验证集的 mAP 一直卡在 0.12 上下几乎等于随机。我第一反应是学习率、数据增强、模型容量折腾了大半天没结果最后打印了logits.shape和labels.shape发现前者是[32, 8]后者是[32, 1]——标注流水线里为了兼容单标签任务把多热向量压成了一列索引标签处理脚本又做了一次unsqueeze最终形状变成[32, 1]的浮点数。PyTorch 没有报错。[32, 8]和[32, 1]广播成[32, 8]标签那一列被复制了八次模型学到的是八个类别共享同一个标签。loss 当然会降因为它确实在拟合一个可解的问题只不过这个问题不是我们要的那个问题。这个案例之后我在所有多标签训练脚本里加了第一行断言assert logits.shape labels.shape, f{logits.shape} vs {labels.shape} assert labels.dtype logits.dtype, f{labels.dtype} vs {logits.dtype}两行代码救过我至少三次。多标签分类任务里最贵的 bug 从来不是报错而是静默广播——它不会打断你的训练只会安静地浪费你的算力和时间。1.2 多标签与多分类差的不只是一个损失函数CrossEntropyLoss服务的是互斥分类一张图是猫就不能同时是狗概率在类别维上做 softmax总和为 1。BCEWithLogitsLoss服务的是独立二分类一张图可以同时有人车树每个类别单独问一句有没有各类别之间互不干扰。这个语义差异直接决定了输出层的激活函数多分类用 softmax多标签用 sigmoid且是逐元素的 sigmoid不是沿类别维的归一化。很多人从多分类迁移过来时会下意识地在模型最后一层加nn.Softmax(dim1)然后再接BCEWithLogitsLoss。这个组合的后果是每个样本的八类概率被强行压缩成和为 1 的分布模型失去了同时输出多个高置信度标签的能力loss 能算、梯度也有但表达力被砍掉了一大截。正确的做法是模型只输出裸 logits激活和损失全部交给BCEWithLogitsLoss内部完成推理阶段再单独调用torch.sigmoid转概率。顺便提一句数值稳定性这也是With Logits这个后缀的全部意义。标准 BCE 是 $-[y\log\hat{y} (1-y)\log(1-\hat{y})]$其中 $\hat{y} \sigma(x)$。当 $x$ 是很大的负数时$\sigma(x)$ 下溢到 0log(0)直接变成-inf反向传播就是 NaN。PyTorch 的实现改用log_sigmoid的稳定形式$$ \mathcal{L} -\left[, y \cdot \text{log_sigmoid}(x) (1-y) \cdot \text{log_sigmoid}(-x) ,\right] $$log_sigmoid(x)内部走的是 $-\log(1e^{-x})$ 的数值稳定分支大正大负都不会溢出。所以绝对不要在传入前自己算 sigmoid那等于把这个稳定性设计原样扔掉还额外叠了一层饱和梯度会更小。1.3 一次 sigmoid 还是两次梯度会告诉你答案nn.BCEWithLogitsLoss()和nn.BCELoss()的区别本质就是内部是否已包含 sigmoid。写成BCELoss()(torch.sigmoid(logits), y)在数学上等价于BCEWithLogitsLoss()(logits, y)但前者在logits绝对值较大时会出现梯度消失甚至 NaN后者不会。我做过一个很小的对照实验让 logits 取 ±20 这种极端值BCELoss路线 loss 直接报nanBCEWithLogitsLoss路线返回有限的 20 左右的 loss梯度正常。下面这张表是我整理的两者差异建议贴在自己的笔记里对比项nn.BCELossnn.BCEWithLogitsLoss输入要求已经过 sigmoid 的概率取值 (0,1)未激活的裸 logits任意实数数值稳定性差极端值易nan好内部用log_sigmoid稳定实现是否支持pos_weight支持支持且与稳定实现结合更好推荐场景几乎不推荐多标签、多任务二分类的默认选择推理时无需额外激活必须补torch.sigmoid一个务实的判断标准如果你在代码里看到sigmoid出现在损失函数参数位置附近基本可以判定写错了。正确的位置永远是推理阶段、计算指标之前。2. logits 与标签的形状约定最容易踩坑的地方形状问题之所以难缠是因为 PyTorch 的广播规则在大多数张量运算里是优点在损失函数里却成了隐患。BCEWithLogitsLoss的官方文档写得很克制input 和 target 应具有相同形状但如果形状不同且可广播它不会主动阻止你。这就形成了一个灰色地带一部分形状错误报错一部分形状错误静默通过还有一部分形状错误只在特定 batch size 下才会暴露。2.1 标准形状 [N, C] 和它的两个合法变体多标签分类的标准形状是[N, C]N是 batch sizeC是类别数。logits 是模型直接输出的实数张量标签是同形状的 0/1 浮点张量。所有其他形状都应该被视作可疑信号直到你能说清楚它为什么是那样。合法变体一是C 1的单标签二分类形状[N, 1]。这种情况下 logits 和标签都是[N, 1]一切正常。但要注意别写成[N][N]和[N, 1]在一起会广播成[N, N]那是一个灾难性的矩阵loss 值看起来还挺正常但梯度已经全乱了。合法变体二是高维输入比如逐像素分割式的多标签任务形状[N, C, H, W]。BCEWithLogitsLoss对任意形状都支持逐元素计算后按reduction聚合所以形状一致性要求同样适用。我在做多标签图像分割时就是直接用[B, C, H, W]的 logits 和同形状的 mask不需要 reshape 成二维。至于[N, C, 1]这种多出来一维的形状绝大多数情况是模型结构问题。常见的来源有两个nn.Conv2d输出没有 flatten或者在全连接层后面手贱加了一个unsqueeze(-1)。这种形状如果标签是[N, C]会触发广播[N, C, 1]和[N, C]对齐后变成[N, C, C]loss 数值翻了好几倍你还会以为是学习率设置问题。我的习惯是在损失计算前统一做一次logits logits.view(logits.size(0), -1)把除了 batch 维之外的所有维度压平成类别维代价几乎为零收益是形状永远可控。2.2 广播导致的静默错误三种典型组合我把实际遇到过的错误形状组合整理成了一张表按危险程度排序logits 形状标签形状实际发生危险等级[B, C][B, C]正确计算安全[B, C][B, 1]标签沿类别维复制 C 次极高静默[B, C][B]视 B、C 关系报错或错误广播极高[B, C][C]每个 batch 共用同一行标签极高静默[B, C][B, C, 1]广播出[B, C, C]高loss 异常大[B, C, 1][B, C]广播出[B, C, C]高loss 异常大[B][B]单标签二分类合法安全第二行[B, 1]是最阴险的因为它在数学上讲得通——每个样本一个标签值广播到所有类别等于告诉模型这八类共享同一个答案。训练会收敛只是收敛到一个错误的目标。第四行[C]更隐蔽在多卡训练或自定义 sampler 里偶尔出现效果是每个 batch 都用同一套标签模型学到的是一组与输入无关的常数先验。一个实操层面的检测手段训练开始后的前几个 step打印labels.mean(dim0)每类的正样本比例和logits.mean(dim0)。如果所有类别的统计量完全相同几乎可以断定是标签被广播复制了。这个方法比看 loss 曲线快得多。2.3 dtype 也必须对齐LongTensor是头号公敌形状对了dtype 错了照样崩。BCEWithLogitsLoss要求target与input同为浮点类型因为数学公式里的 $y$ 参与的是乘法而非索引。CrossEntropyLoss习惯了LongTensor标签的人迁移过来时最容易在这里翻车报错信息通常是RuntimeError: result type Float cant be cast to the desired output type Long修复方式非常简单labels labels.float()但要注意float()会新建张量在 DataLoader 的collate_fn里做一次比在训练循环里每次做要省不少开销。如果标签是从 CSV 或者 JSON 解析来的多热向量解析后直接.astype(float32)转成 numpy 再torch.from_numpy一条链路上只做一次类型转换。还有一个容易忽略的点混合精度训练时logits 是float16标签如果是float32PyTorch 会在损失内部做类型提升通常会成功但会带来额外开销偶尔在某些版本组合下报类型不匹配。我的做法是模型输出后立刻logits logits.float()再做损失计算显存代价很小稳定性提升明显。注意labels.float()之后不要再用.long()做索引类操作两个用途的标签建议在数据集里就分开存成labels_float和labels_idx避免来回转换。3. 从零搭一条多标签训练管线光讲形状容易抽象我按一个完整的最小可运行例子走一遍从数据集构建到损失配置、训练循环、推理阈值每一步都标注为什么这么写。任务设定八类标签每张样本可以同时命中多类类别分布不均匀最长尾的类正样本比例只有 0.6%。3.1 标签构建多热向量要在数据集层做干净数据集的核心职责是把原始标注转成[C]的 0/1 浮点向量并且保证维度与C严格一致。我见过的最常见问题是标签集合动态变化训练集里没出现过的类别在验证集里出现了导致C对不上模型输出 8 维、标签 9 维报错报在很晚的地方。解决办法是在数据集初始化时传入固定的class_list不在任何环节动态推断。import torch from torch.utils.data import Dataset class MultiLabelDataset(Dataset): def __init__(self, samples, class_list, transformNone): self.samples samples # List[Tuple[path, List[str]]] self.class_list class_list # 固定顺序的类别名列表 self.cls_to_idx {c: i for i, c in enumerate(class_list)} self.num_classes len(class_list) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, tag_names self.samples[idx] image self._load_image(path) if self.transform: image self.transform(image) label torch.zeros(self.num_classes, dtypetorch.float32) for name in tag_names: # 未知标签直接忽略并计数不要静默扩展类别维 if name in self.cls_to_idx: label[self.cls_to_idx[name]] 1.0 return image, label关键点有三个。dtypetorch.float32写死在构造时别指望后面.float()补救未知标签走忽略并计数的路径我一般会挂一个Counter统计训练结束后检查有没有大量标注被丢掉那往往意味着class_list漏了类num_classes作为属性暴露出来模型的输出维度直接读它两处用一个来源永远不会对不上。标签里还有一种特殊情况需要提前想清楚全零样本。一张图八个类别一个都不命中标签是[0,0,...,0]。BCEWithLogitsLoss对这种样本完全能算它的贡献就是八个负样本项教模型这些都没有。这类样本有没有价值取决于业务——如果标注流程是人工逐条打标全零说明确实什么都没有值得保留如果是弱标注或者关键词匹配全零很可能是漏标保留下来会让模型学会过度保守。我的习惯是抽样几十个全零样本人工核对再决定保留比例。3.2 模型输出层与损失配置模型这边只有一条铁律最后一层不加任何激活。不管前面用 ResNet、ViT 还是自己堆的 MLP输出都是[B, C]的裸 logits。import torch.nn as nn class MultiLabelModel(nn.Module): def __init__(self, backbone, num_classes, drop_rate0.2): super().__init__() self.backbone backbone feat_dim backbone.num_features self.head nn.Sequential( nn.Dropout(drop_rate), nn.Linear(feat_dim, num_classes), # 不加 sigmoid不加 softmax ) def forward(self, x): feat self.backbone(x) return self.head(feat) # shape: [B, C]损失函数的配置要跟着任务特性走把常用参数一次说清楚import torch class_list [fcls_{i} for i in range(8)] C len(class_list) # 训练集各类正样本数用于计算 pos_weight pos_counts torch.tensor([120, 340, 88, 1500, 60, 210, 95, 40], dtypetorch.float32) neg_counts 5000 - pos_counts pos_weight (neg_counts / pos_counts).clamp(max20.0) # 上限裁剪避免极端类主导 criterion torch.nn.BCEWithLogitsLoss( pos_weightpos_weight, # shape: [C] reductionmean, )pos_weight的形状必须是[C]和 logits 的最后一维对齐这点后面单独讲。clamp(max20.0)是经验做法正样本极少的类别理论pos_weight可能到 100 以上过大的权重会让模型对这类样本过度敏感产生大量假阳性同时梯度尺度失控。裁剪到 20 以内配合后面的阈值调优效果比一味加大权重稳定得多。3.3 训练循环一个小断言省下几小时训练循环本身没什么特别关键是加保护。我在每个 epoch 的第一个 step 打印一次形状信息和梯度的范数用来第一时间发现异常from torch.utils.data import DataLoader loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4) optimizer torch.optim.AdamW(model.parameters(), lr2e-4, weight_decay1e-4) scaler torch.amp.GradScaler(cuda) for epoch in range(num_epochs): model.train() for step, (images, labels) in enumerate(loader): images images.cuda(non_blockingTrue) labels labels.cuda(non_blockingTrue).float() with torch.amp.autocast(cuda, dtypetorch.float16): logits model(images) # 断言放在 autocast 外面保证形状与 dtype 检查基于 float32 logits logits.float() assert logits.shape labels.shape, fshape {logits.shape} vs {labels.shape} assert labels.dtype torch.float32 loss criterion(logits, labels) optimizer.zero_grad(set_to_noneTrue) scaler.scale(loss).backward() scaler.unscale_(optimizer) grad_norm torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) scaler.step(optimizer) scaler.update() if epoch 0 and step 0: print(flogits {logits.shape} labels {labels.shape} floss {loss.item():.4f} grad_norm {grad_norm:.4f}) print(fper-class pos rate: {labels.mean(dim0).cpu().numpy().round(3)})scaler.unscale_之后再算grad_norm是必须的否则拿到的是被缩放过的梯度范数判断阈值会失真。clip_grad_norm_在多标签任务里几乎必开因为pos_weight会放大某些类的梯度量级不加裁剪时偶发的 loss 尖峰会直接把模型带偏。推理阶段记得补 sigmoid且不要在训练时算概率存下来复用logits 才是损失函数的输入torch.no_grad() def predict_proba(model, images, batch_size64): model.eval() probs [] for i in range(0, len(images), batch_size): batch images[i:i batch_size].cuda(non_blockingTrue) logits model(batch).float() probs.append(torch.sigmoid(logits).cpu()) return torch.cat(probs, dim0) # shape: [N, C]取值 (0,1)torch.sigmoid放在no_grad里、放在 CPU 上算都是小优化但推理批量大时省下的显存和时间很可观。4. 类别不平衡与 pos_weight 的正确算法多标签任务的类别分布几乎从来不是均匀的头部类占比 30%尾部类占比 0.5%这在一张图上同时出现八个标签的场景里是常态。不加权的 BCE 会被头部类主导尾部类的梯度被淹没最终模型对所有样本都倾向于预测没有因为从期望损失看这是最安全的策略。4.1 pos_weight 的数学含义带pos_weight的 BCE 形式是$$ \mathcal{L} -\left[, w_p \cdot y \cdot \text{log_sigmoid}(x) (1-y) \cdot \text{log_sigmoid}(-x) ,\right] $$$w_p$ 只乘在正样本项上负样本项不动。这意味着pos_weight k等价于告诉优化器漏掉一个正样本的代价是误判一个负样本的 k 倍。直觉上这个 k 应该等于负正样本比例N_neg / N_pos让两类在损失里势均力敌。但理论值和实际最优值经常差很远。原因在于损失里的势均力敌不等于指标上的势均力敌。多标签任务常用的指标是 mAP 或者 F1它们对排序质量敏感对绝对概率标定不太敏感。我做过对比实验在八个类的任务上把pos_weight从精确比例值调到它的 0.5 倍mAP 反而涨了 2 个点因为过度加权会让尾部类的概率整体偏高排序上不再有区分度。所以我的建议是以理论值为起点在其 0.5x 到 2x 区间做小规模网格搜索不要迷信公式。4.2 从训练集统计到权重张量一步都别用手写pos_weight必须从训练集统计不能用验证集或全体数据否则会引入信息泄露。我给的标准流程是跑一遍 DataLoader把每类的pos和neg累计出来def compute_pos_weight(dataset, num_classes, cap20.0): counts torch.zeros(num_classes, dtypetorch.float64) for _, label in DataLoader(dataset, batch_size256, shuffleFalse, num_workers4): counts label.sum(dim0).double() total float(len(dataset)) neg total - counts weight (neg / counts.clamp(min1.0)).clamp(maxcap) return weight.float()几个细节。用float64累加是因为某些类的计数可能上万float32累加几百个 batch 后精度会漂clamp(min1.0)防止某类在训练集里一个正样本都没有导致除零这种情况应该先去检查数据而不是硬算cap和前面说的一样避免极端权重。算完之后我会把权重和对应的类名打出来看一眼如果某个类的权重大于 10基本可以肯定这类样本太少需要补数据或者改用更强的数据增强而不是靠权重硬撑。还有一个weight参数容易和pos_weight混淆。weight是和输入同形状的张量按元素加权整个损失项正负样本都受影响pos_weight是一维的[C]只影响正样本项。多标签的不平衡处理用pos_weight按样本难度加权才用weight。两个一起用的时候注意权重会相乘实际效果可能远超预期。4.3 阈值后处理0.5 往往不是最优解模型输出的概率过了 sigmoid 之后用什么阈值判定命中绝大多数人写 0.5然后抱怨 F1 上不去。在多标签场景里每个类的概率分布都不一样头部类可能集中在 0.8 以上尾部类可能全部在 0.3 以下一个全局 0.5 对谁都不友好。我的做法是在验证集上做逐类阈值搜索用 F1 或者业务更关心的指标作为目标import numpy as np from sklearn.metrics import f1_score def search_thresholds(probs, targets, gridnp.arange(0.05, 0.96, 0.01)): probs np.asarray(probs) targets np.asarray(targets) num_classes probs.shape[1] best_th np.full(num_classes, 0.5) best_f1 np.zeros(num_classes) for c in range(num_classes): for th in grid: pred (probs[:, c] th).astype(int) score f1_score(targets[:, c], pred, zero_division0) if score best_f1[c]: best_f1[c] score best_th[c] th return best_th, best_f1这套搜索在验证集上跑一次把best_th存进模型产物的配置里推理时逐类比较。注意两个坑验证集要足够大尾部类如果只有十几个正样本搜出来的阈值会过拟合这时候可以退化成全类统一阈值 0.5 或 0.3阈值要在验证集上搜、在测试集上验证别用同一份数据反复调那只是自欺欺人。顺带说一个指标层面的心得。多标签分类里f1_score的average参数选什么直接影响你看到的数字。macro让每个类等权尾部类表现差会明显拉低总分适合评估是否所有类都要能用micro按样本数加权头部类主导适合评估整体准确度。我在项目里一般两个都报macro用来发现长尾问题micro用来汇报总体水平。5. 常见问题排查速查表报错、静默错误、不收敛这一节是我这些年攒下来的问题清单按症状分类。遇到BCEWithLogitsLoss相关的怪事先在这张表里对一遍能省掉大量盲猜。5.1 报错类问题形状和 dtype 的正面冲突报错信息根因修复方式result type Float cant be cast to Long标签是LongTensorlabels labels.float()最好在collate_fn里做The size of tensor a (C) must match the size of tensor b (B)logits[B,C]对标签[B]且 B≠C统一成[B,C]用view显式压平Target size must be the same as input size形状不可广播检查模型输出去掉多余的unsqueezepos_weight相关维度报错权重张量长度不等于 C用compute_pos_weight从数据集统计生成Expected floating point type for target标签是 bool 或 int.to(torch.float32)第二行这条报错我遇到最多报错文案像是从别的算子借来的实际原因是[B, C]和[B]在最后一维对齐失败。修起来不难但报错信息不直观容易浪费时间在别处找。5.2 静默错误不会报错但模型学不到东西这一类才是真正耗时间的。我列了五个高频症状和对应的检查手段loss 平滑下降但验证指标接近随机。先查labels.shape是不是[B, 1]再看labels.mean(dim0)是否所有类相同两个检查加起来不到一分钟。loss 数值异常大是正常值的 C 倍左右。查 logits 是不是[B, C, 1]广播出[B, C, C]后元素数翻了 C 倍reductionmean也压不回来。某些类的预测概率永远是 0 或 1。查这一类的pos_weight是否被设得过大导致 sigmoid 饱和、梯度消失。把pos_weight裁剪到 20 以内通常能缓解。训练前几个 step loss 是 nan。查有没有重复 sigmoid或者 logits 里本身就有 inf后者通常是上游学习率过大导致参数爆炸。验证 loss 比训练 loss 低很多。这通常不是损失函数的问题而是验证集的正样本比例更高BCE 对先验敏感。检查两份数据的标签分布统计。关于最后一条展开说一句。BCEWithLogitsLoss在reductionmean时是所有元素损失的算术平均正负样本数量都会影响结果。训练集如果做了重采样、验证集没做两边的 loss 数值本来就不可比别拿它当早停的唯一依据用好 mAP 或者 F1。5.3 不收敛超参数与标签质量的双重怀疑真遇到 loss 抖动、指标上不去时我按这个顺序排查命中率比较高第一步看标签质量。抽 30 个样本把每类的标签图和原图/原文并排看一遍统计标注错误率。多标签任务的标注成本高错误率经常在 5% 以上标签噪声大过模型容量问题时调参是白费功夫。第二步看pos_weight的量级。打印每个类的权重和正样本数如果某类正样本少于 50 且权重被压到 20这类基本上学不出来要么补数据要么在评估里把它单独看。第三步看学习率和 batch size。多标签任务里BCEWithLogitsLoss的梯度尺度比CrossEntropyLoss大一些同样的学习率容易震荡。我一般从 2e-4 起步配合clip_grad_norm_(max_norm5.0)如果 loss 曲线前 200 step 有明显尖峰先降学习率而不是加 warmup。第四步看阈值。指标不涨可能是阈值选错了而不是模型不行用 4.3 的搜索脚本扫一遍经常能发现模型其实学到了只是 0.5 不适合。第五步才怀疑模型容量和特征质量。前面四步都排除之后再上更大的 backbone性价比更高。5.4 几个只有踩过才知道的细节软标签要保留浮点精度。做 label smoothing 时把 1 改成 0.9、0 改成 0.05BCEWithLogitsLoss完全支持因为公式里 y 参与乘法。但要注意别把软标签转成LongTensor那会把 0.9 截断成 0等于把正样本全标成了负样本。mixup 类增强依赖浮点标签。两个样本按比例混合后标签也是混合的形状仍为[B, C]值在 0 到 1 之间。这是BCEWithLogitsLoss相对CrossEntropyLoss的一个结构性优势后者处理软标签还得额外实现。空 batch 要防。某些自定义 sampler 在数据量除不尽时会产出空 batchlogits是[0, C]此时 loss 是nan。加一句if logits.numel() 0: continue就行。分布式训练下reductionmean的语义。DDP 里每个进程各自算 mean 再平均如果各卡的样本数或正负比例差异大等权平均会带来偏差。样本数不均衡时改用reductionsum再手动除以全局样本数结果更准确。保存模型时把阈值和pos_weight一起存。我见过太多模型文件在、配置丢了的情况推理时用默认 0.5 阈值效果和训练时差一大截。用一个config.json固定住class_list、pos_weight、best_thresholds、input_size四项部署环节能省掉一整轮排查。验证集指标用 logits 排序、用阈值判定两件事分开做。mAP 只关心排序跟阈值无关适合用来判断模型本身好不好F1 依赖阈值适合判断当前配置能不能上线。两个指标一起看能区分模型不行和阈值没调好这两种完全不同的情况。我个人在这些年的多标签项目里体会最深的一点是BCEWithLogitsLoss本身几乎不会出问题它稳定、简单、行为可预期出问题的永远是喂给它的东西——形状被广播悄悄改过的标签、忘记转换的 dtype、从多分类任务沿袭过来的 sigmoid、以及一个从来没被认真选过的 0.5 阈值。把这几处盯死多标签任务的训练稳定性会比想象中好很多。

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

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

免费获取报价 →
↑