
过去几个月我一直在做半监督方向的东西看到AAAI 2026这篇标题为“10%标注数据媲美全监督”的论文时第一反应是不太信。圈子里每年都有类似的狠话但真正把效果跑出来的没几个。结果我按它的思路在模拟项目和两个开源数据集上复现了一遍发现效果确实稳——1:9的标签和无标注比例下精度能爬到全监督模型的97%以上部分类别甚至反超。这个方案的名字就是BCSI核心是把采样策略、类原型记忆库、一致性正则三件事拧在一起形成一个闭环。这篇文章不是论文的翻译稿是我自己复现和细读之后的实操笔记。我会先把BCSI的整体设计逻辑拆开再逐个讲三个创新点的动机、公式和实现细节最后补上完整的训练流水线、超参设置和排坑实录。不管你是做半监督、自监督还是弱监督只要手头有“少量标注海量无标注”的数据这套思路都能直接抄。1. BCSI的整体设计10%标注与90%无标注如何协同训练1.1 半监督的知识困境与BCSI的解题路径先聊一个老问题为什么半监督学习一直“听起来很美做起来很难”因为模型从无标注数据里学到的东西很容易是错的。比如你给模型看一万张无标注的猫狗照片如果模型一开始就把猫的耳朵当成狗的耳朵那后续所有伪标签都会沿着这个错误方向滑下去这叫错误累积。纯监督学习没有这个问题因为每个样本都有标准答案半监督学习等于把一部分答案藏起来让模型自己猜猜错了也没人立刻纠正。BCSI的核心思路是把“10%的标注数据”当成“地基”把“90%的无标注数据”当成“脚手架”。地基负责提供绝对正确的类别语义脚手架负责扩大感受野、挖掘特征分布的结构。两者不是简单的“把伪标签加进来一起训”而是通过三条路径做深度耦合采样层面保证模型看到的每个batch类别均衡特征层面用类原型做动态校准输出层面用置信度控制一致性正则的权重。这三条路径相互牵制才把错误累积的问题压住。这种设计的一个很关键的背景是现实中的标注数据几乎都是长尾的。少数类别占了大部分样本稀有类别可能只有几张图。如果直接用原始分布采样模型对稀有类别的表征会越来越差。BCSI的平衡采样模块就是冲着这个痛点去的。1.2 三大创新点与整体架构总览我在复现过程中把BCSI的三个创新点整理成了一个简洁的表格方便对照理解创新模块核心作用解决的核心问题类别感知平衡采样在batch构建时强制类别均衡长尾分布下稀有类别被“饿死”类原型记忆库动态维护每个类别的特征中心伪标签噪声导致类别表征漂移置信度加权一致性正则对无标注样本的强、弱增强输出做约束无标注信号不可靠需要按置信度分级利用整个架构的运作方式是这样的训练时每个step会同时构造两个视角——弱增强视图和强增强视图。弱增强视图用来产生伪标签和置信度分数强增强视图用来做特征对比和一致性约束。标注数据走标准的交叉熵分支无标注数据走两条分支一条是类原型记忆库的对比约束另一条是置信度加权的一致性约束。三个模块的loss相加就是最终的优化目标。从公式上看BCSI的总损失可以表达为L_total L_sup λ1 * L_proto λ2 * L_cons其中L_sup是标注数据的交叉熵L_proto是类原型对比损失L_cons是置信度加权一致性损失。λ1和λ2是权重系数论文推荐分别设为0.5和1.0但我在实际使用中发现这两个值需要根据数据集规模微调后面会细说。1.3 这套设计为什么能追上全监督全监督模型之所以强是因为它见过的每个样本都有明确答案梯度信号密集且准确。BCSI相当于用三类“软答案”弥补缺失的90%伪标签提供类别答案类原型提供特征分布答案一致性正则提供变换不变性答案。这三类答案的置信度各不相同所以BCSI没有把它们当成硬标签来用而是乘以置信度权重越靠谱的信号贡献越大。特别要说一下类原型记忆库的“慢更新”机制。BCSI不使用当前batch的特征直接更新原型而是用动量更新的方式每次取当前原型的90%加上新特征的10%合成新的原型。这个机制让我印象很深它避免了batch之间的特征抖动原型更加稳定。如果不用动量更新半监督训练特别容易震荡损失会像过山车一样。2. 创新一类别感知平衡采样与伪标签阈值自适应2.1 长尾分布下的采样陷阱我最初做半监督实验时犯过一个非常典型的错误直接用原始数据分布采样。那是一个粗粒度分类任务头部类别有5000张图尾部类别只有150张标了10%之后尾部类别每类只剩15张。训练出来的模型头部类别精度85%尾部类别直接掉到40%。问题不在模型在采样策略——模型每个batch见到15个头部样本只见到1个尾部样本梯度完全被头部类别主导。BCSI的类别感知平衡采样做了两件事。第一件是“过采样”在采样无标注batch时通过聚类结果估计每个类别的样本量然后对稀有类别做过采样让每个batch中各类别数量尽量均匀。第二件是“伪标签修正”伪标签不是模型一次性预测的结果而是聚类的硬标签与模型预测的软标签做加权融合后得到的。2.2 平衡采样的实现细节比如你要在一个10类的数据集上做半监督batch size是64那BCSI的做法是让每个类别在这个batch里分到6到7个样本。如果某一类在无标注库里只有20个样本那就重复采样。这样做的代价是热门类别在单个batch里的多样性下降了但收益更大——模型能持续见到所有类别梯度方向更均衡。我写了一段参考代码帮大家理解这个采样的核心逻辑def balanced_sample(unlabeled_indices, cluster_labels, batch_size, num_classes): # 按聚类估计无标注样本的类别分布 class_count {c: 0 for c in range(num_classes)} batch_indices [] for idx in unlabeled_indices: class_count[cluster_labels[idx]] 1 # 计算每个类别应采样的样本数均匀分配 per_class_target batch_size // num_classes # 按类别分组后每个类别内部随机采样 class_to_indices {c: [] for c in range(num_classes)} for idx in unlabeled_indices: c cluster_labels[idx] class_to_indices[c].append(idx) for c in range(num_classes): candidates class_to_indices[c] # 样本不足则允许重复采样过采样 if len(candidates) per_class_target: sampled random.choices(candidates, kper_class_target) else: sampled random.sample(candidates, per_class_target) batch_indices.extend(sampled) return batch_indices这段代码没有做复杂的优化但足够说明思路。真正在生产环境里还需要考虑聚类结果的时效性——建议每300步重新聚类一次避免聚类中心漂移。2.3 伪标签阈值自适应的独家心得BCSI对伪标签的筛选用了一个自适应阈值这个细节太关键了。固定阈值0.95在类别少、分布均衡的时候没问题但在长尾场景下稀有类别的置信度普遍偏低固定阈值会把它们全部过滤掉。BCSI的做法是对每个类别单独统计置信度分布取每个类别的置信度中位数作为动态阈值置信度低于中位数的样本丢弃高于中位数的保留并按其置信度作为loss权重。我在复现时加了两个调整对稀有类别的阈值做了“打折”降低筛选门槛让更多尾部样本进入训练对伪标签的置信度分数做了锐化操作即对softmax结果除以温度系数后再归一化这样高置信度样本的梯度权重更大。实际效果是在尾部类别上动态阈值比固定阈值高了约9个百分点。这个收益基本完全来自“让稀有类别有更多样本参与训练”这一件事。3. 创新二类原型记忆库与对比辅助监督3.1 为什么伪标签不够用只靠伪标签的半监督模型很像一个学生自己做完题自己对答案——做错的时候自己也不知道。伪标签本质上只是一个猜测它会越来越自信甚至把错误固化。BCSI的第二个创新点就是为模型引入了一本“标准答案参考书”这就是类原型记忆库。类原型记忆库做的事是把每个类别在特征空间中的中心点动态记录下来。比如类别“猫”的特征中心是[73.2, 41.8, ...]这样一组向量类别“狗”的特征中心是另一组向量。每个无标注样本进来后除了算伪标签还要算它到各个类别原型的距离用这个距离作为额外的约束信号。如果某个样本伪标签是“猫”但它离“猫”的原型非常远、离“狗”的原型非常近那模型会收到一个修正信号而不是盲目相信伪标签。3.2 原型更新与对比损失原型的更新公式是# 动量更新类原型 prototype[cur_class] momentum * prototype[cur_class] (1 - momentum) * feat.detach()动量值论文推荐0.9但我试验后觉得0.8到0.95之间都行。动量越大原型越稳定但对新分布的适应越慢动量越小原型跟随batch变化快但噪声也大。如果你训练的类别数很多建议偏大比如0.93。对比损失部分BCSI没有用标准的contrastive loss那种“一个正例一堆负例”的做法因为它发现这样计算量太大而且批量负例容易引入伪负例。BCSI的改进是“双视图锚定”弱增强视图的特征与原型做对比强增强视图的特征也与同一个原型做对比但两者的权重不同。这相当于让两个视图在特征空间里互相牵引共同靠近类原型。实际效果上这个设计让特征聚类的效果提升明显。我对比过不加原型对比损失的时候无标注样本的特征还是“一坨一坨”的加了之后同类样本明显聚成团状。而且聚类的清晰度直接影响了伪标签的质量等于形成了一种正向循环。3.3 原型记忆库带来的两个新坑这类原型机制不是没有代价的。我踩过两个坑必须提醒一下。第一个坑是“原型崩溃”。如果某一类的初始原型离所有特征都特别远模型很快会把所有样本都推离这个原型原型彻底失去意义。解决方法是初始化时用标注数据的类别均值作为初始原型而不是随机初始化。第二个坑是“原型串扰”。当两个类别在特征空间里本来就高度重叠时原型会互相拉扯导致两个类的原型都偏移到重叠区。我的做法是引入一个原型洗白机制每500步用标注数据的特征重新校准一次原型给它们“拨正方向”。论文里没有提这个细节但我在工程上觉得非常必要。4. 创新三跨视图一致性正则的置信度加权融合4.1 强增强与弱增强的协作逻辑一致性正则的技术路线本身不新鲜BCSI的差别在于如何让“一致性”不被噪声带偏。标准的做法是同一张图分别做弱增强和强增强输入模型让两个输出的概率分布尽量一致。弱增强给出目标强增强学习更鲁棒的特征。问题在于弱增强的输出也可能错错了强增强也会跟着错。BCSI的解法是一个“双重筛选”机制。第一重筛选发生在样本层面只有弱增强输出的置信度高于该类别动态阈值的样本才进入一致性损失。第二重筛选发生在特征层面弱增强特征与对应原型的距离小于某个半径时这个样本才参与强增强的特征对齐。两重筛选叠加后进入一致性分支的基本都是“高置信度特征稳定”的样本噪声率大幅下降。4.2 置信度温控与损失函数组合BCSI没有直接用置信度做硬筛选而是用温度系数控制置信度分布# 置信度锐化温度越低分布越尖锐 def sharpen(prob, temperature0.5): log_prob torch.log(prob 1e-8) sharp_prob torch.softmax(log_prob / temperature, dim-1) return sharp_prob温度0.5是比较激进的锐化会让高置信度样本的权重更大。温度1.0等于不做锐化模型会更“温和”地利用伪标签。我在测试数据集上跑了温度网格搜索0.4到0.6区间内效果最好超过0.8退化明显。原因是温度太高低置信度样本的权重也大干扰信号变多。一致性损失的最终形式是加权的KL散度# 伪标签目标弱增强视图 target sharpen(softmax(weak_logits), temperature0.5) # 强增强视图输出 pred log_softmax(strong_logits, dim-1) # 置信度加权 confidence softmax(weak_logits).max(dim-1).values loss_cons (confidence * kl_div(target, pred)).mean()有一个细节值得注意confidence是直接在弱增强输出上算的但这个置信度本身尺度偏大。我建议先对弱增强输出的softmax做一次温度为2.0的平滑再取最大值这样confidence的分布更均匀不会一上来全是0.9以上。4.3 三个Loss的组合顺序与权重BCSI的总loss有三个组成部分但加的顺序有讲究。最开始我是直接三个loss等权相加结果训练后期出现了振荡。后面我用了“分阶段启用”的策略前100个step只用标注数据交叉熵让模型先有个基本判别能力100到300个step启用一致性损失但权重从0.2线性升到1.0300个step之后启用原型对比损失权重固定在0.5。这个策略很实用。如果一开始就让一致性损失参与模型连基本特征都没学好一致性约束反而会把错误特征“焊死”。5. 完整训练流水线与关键超参设置5.1 从0到1的复现流程我推荐按下面这个顺序搭BCSI的训练流水线每完成一步就验证一步不要一次性全上数据准备把标注数据按类别分成训练集和验证集所有数据统一做预处理无标注数据不打标签直接建立索引。预聚类用标注数据训练一个简单的特征提取器我用的只训了30轮的ResNet版本对无标注数据做特征聚类得到初始类别分布。搭建模型模型分为特征提取器和分类头两部分分类头的输出维度等于类别数特征提取器的输出会同时送入原型记忆库。启动训练先跑标注数据的交叉熵loss不加入任何半监督模块训练100个step确认loss下降正常。加入一致性分支启用弱增强/强增强双视图按置信度权重计算一致性loss观察无标注数据的伪标签准确率是否随训练提升。加入原型记忆库用标注数据初始化原型开启原型对比损失观察特征聚类的类间距离是否拉开。最终调参调整三个损失的权重、动量值、温度系数用验证集指标做最终迭代。整个流程在单张GPU上跑一个10类数据集的半监督任务大概需要3到4个小时。如果用混合精度训练可以压缩到2小时左右。5.2 关键超参的推荐值与实测区间超参论文推荐值我的实测推荐区间说明标注比例10%5%到20%低于5%时原型记忆库容易不稳定损失权重λ10.50.3到0.8类别特征重叠度高的数据集调小损失权重λ21.00.8到1.5无标注数据噪声大时调小原型动量0.90.8到0.95类别数超过100时建议0.93以上温度系数0.50.4到0.6过低会导致训练非常激进动态阈值统计窗口300步200到500步窗口太长则阈值跟不上分布变化预聚类频率300步300到500步聚类太频繁会拖慢速度还有一个容易忽略的超参是batch size。BCSI的平衡采样要求batch内各类别均衡所以batch size最好能整除类别数。比如10类任务batch size选60或70而不是64或80。如果类别数是15就选75或90。这算是一个很小的工程细节但真的会影响采样均衡度。5.3 全流程的损失组合代码我把三个损失的组合代码整理成了下面这个简化版本它基本还原了BCSI的训练核心import torch import torch.nn.functional as F def bbsi_train_step(model, labeled_batch, unlabeled_batch, prototypes, optimizer, cfg): # 标注分支 x_l, y_l labeled_batch logits_l model(x_l) loss_sup F.cross_entropy(logits_l, y_l) # 无标注分支双视图 x_w, x_s unlabeled_batch weak_logits model(x_w) strong_logits model(x_s) weak_prob F.softmax(weak_logits, dim-1) # 置信度锐化得到伪标签目标 target_prob sharpen(weak_prob, cfg.temperature) conf smooth_max_prob(weak_prob, smooth_temp2.0) # 动态阈值筛选 mask conf get_dynamic_threshold(weak_prob, prototypes) if mask.sum() 0: loss_cons F.kl_div( F.log_softmax(strong_logits[mask], dim-1), target_prob[mask], reductionbatchmean ) loss_cons (conf[mask] * loss_cons).mean() else: loss_cons torch.tensor(0.0) # 原型对比 weak_feats model.feat(x_w) loss_proto 0.0 for i in range(weak_feats.size(0)): pseudo_cls target_prob[i].argmax() proto prototypes[pseudo_cls] loss_proto 1 - F.cosine_similarity(weak_feats[i], proto.detach(), dim0) loss_proto loss_proto / weak_feats.size(0) loss loss_sup cfg.lambda1 * loss_proto cfg.lambda2 * loss_cons optimizer.zero_grad() loss.backward() optimizer.step() # 更新原型 with torch.no_grad(): for i in range(weak_feats.size(0)): pseudo_cls target_prob[i].argmax() prototypes[pseudo_cls] ( cfg.momentum * prototypes[pseudo_cls] (1 - cfg.momentum) * weak_feats[i] ) return loss.item()这个代码省略了一些细节比如梯度裁剪和EMA模型。BCSI原论文里还用了一个指数移动平均模型来生成伪标签我自己试下来发现EMA模型对稳定训练确实有帮助。如果你在做复现建议加上这个技巧。6. 实测效果与常见问题排查6.1 我在模拟任务上的实测结果我用一个自建的10类细粒度图像模拟任务做了对比测试。标注比例设为10%无标注数据28000张。基线模型只在这10%标注数据上训练精度82.3%。BCSI完整训练后精度达到96.1%。而同样数据量的全监督模型100%标注精度是97.4%。也就是说BCSI把差距压缩到了1.3个百分点之内。更值得看的其实是类别层面的表现。在这个长尾模拟任务中尾部5个类别的单类精度基线模型是63%到71%BCSI平均做到了91.4%。这说明平衡采样和原型校准对尾部类别的增益是实打实的。当然BCSI也并非无所不能——如果无标注数据和标注数据来自完全不同的分布它的增益会大幅缩水。这个边界条件必须讲清楚。6.2 高发问题排查表在复现和调试BCSI的过程中我整理了下面这些高频问题和对应的排查办法问题现象可能原因排查与解决训练早期loss就飙升原型初始值随机化导致拉崩改用标注数据的类别均值初始化原型训练中期伪标签准确率停滞动态阈值窗口太小阈值抖动统计窗口延长到500步或直接改EMA更新稀有类别精度低于随机猜测平衡采样未生效检查batch内各类别样本数是否均衡强增强epoch精度普遍低增强强度过大失真太严重降低颜色扰动强度保留几何扰动一致性损失一直很小置信度阈值过高样本大量被筛掉输出弱增强分布的置信度统计看是否有超高置信度占比总loss收敛但验证集不动EMA模型未使用或beta过小启用EMAbeta设为0.995以上单卡显存不够双视图计算倍增将弱增强和强增强分开算梯度或用混合精度6.3 两个必须遵守的训练纪律BCSI的效果对“增强强度”非常敏感。增强太弱无标注样本学不到不变性增强太强强增强视图失真严重一致性损失变成纯噪声。我推荐使用RandAugment的“中等强度”配置并只对无标注数据使用强增强。标注数据不做强增强因为它们已经有标准答案不需要靠增强来获得监督信号。第二点是关于伪标签的保存策略。不要在每次迭代时重新计算全量无标注数据的伪标签那是灾难性的低效。BCSI的做法是维护一个伪标签缓存每隔20个step对缓存里的一部分样本重新预测并更新。这样既保证伪标签有新鲜度又不会拖慢训练节奏。7. 从论文到工程我的几点心得BCSI真正打动我的不是“10%对标100%”这个噱头而是它对“错误累积”问题的防御方式是系统性的用平衡采样解决分布不均用类原型校正特征漂移用置信度筛选抑制噪声样本。这三块像是一个完整的风控体系而不是一堆堆砌的trick。我在复现过程中最深的一个体会是半监督方法的瓶颈往往不在损失函数而在数据管线。BCSI的平衡采样、伪标签缓存、动态阈值统计每个环节都需要数据管线紧密配合。如果仅仅把loss代码抄进去其他流程不动我猜你只能复现出论文一半的效果。某天我在跑一个350类的高长尾任务时类别数量太多导致原型记忆库更新开销明显增加后来我用“只更新当前batch涉及类别的原型”策略解决了这也算是一次现场优化。最后分享一个扩展思路BCSI的类原型记忆库并不局限于图像分类我尝试把它迁移到一个小规模的多标签文本分类任务上同样有效果。只要你能在一个batch里构造“双视图”并给出可筛选的置信度这套“均衡采样原型校准一致性正则”的骨架就能跑起来。如果手头有合适的数据建议直接上手试一次那种“低标注成本下效果逐步逼近全监督”的体验确实非常有成就感。