ARTICLE DETAIL

资讯详情

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

基于对比学习的半监督分类:方法框架、实验与复现要点

基于对比学习的半监督分类:方法框架、实验与复现要点 我从去年开始密集地重读半监督分类方向的工作起因是自己手头某个分类项目只有不到千张标注图而标注成本高到老板批不下来。那段时间我试过 FixMatch、FlexMatch也试过给无标签数据打伪标签硬训效果都不太稳。直到系统地读了一遍基于对比学习的半监督分类这个方法线才逐渐把伪标签一致性训练和对比表征学习这两套思路在脑子里真正焊接到一起。这篇文章就是我整理的一份论文笔记里面不止有方法框架的拆解还有我复现时踩过的坑、调参的实际体会以及对这类方法适用边界的思考。这篇笔记适合两类人一类是刚接触半监督分类、想搞清楚对比学习为什么能帮上忙的初学者另一类是已经在用伪标签类方法、但总觉得训练过程不稳、想找一个更鲁棒框架的工程师。我会尽可能把方法的动机、每个模块的设计原因、消融实验的结论讲透最后补上我在 TensorFlow/PyTorch 下复现时遇到的几个真实问题以及对应的解决办法。1. 为什么读到这篇论文半监督分类的三个老大难1.1 标注少的时候模型到底缺什么半监督分类的问题说起来很简单给你一小撮有标签数据加一大堆无标签数据目标是让模型分类准确率尽量逼近全部有标签训练出来的上界。但实际跑起来就发现逼近上界这四个字的难度远超预期。我自己的经验是在 CIFAR-10 上只保留 250 张有标签图片每类 25 张时一个正常的 ResNet 用交叉熵硬训测试准确率大约只能在 70% 上下浮动。别小看这个数字它其实暴露了一个关键事实交叉熵损失的梯度信息量在标注数据极其稀疏时是严重不足的。为什么会这样因为交叉熵只告诉模型这个样本属于这一类它不告诉模型这个样本和那个样本在语义上是相近的。当每类只有 25 个样本时模型学到的类别边界是基于极少几个锚点划出来的边界附近稍微出现一个没见过视角的样本预测就很容易翻车。对比学习恰好补的就是这个缺口。它不依赖标签只依赖同一样本的不同视角应该被映射到相近位置不同样本应该被映射到较远位置这个自监督信号。这个信号可以让模型在完全没有标签的情况下先把特征空间里哪些区域是稠密的、哪些区域是稀疏的这个几何结构摸清楚。当有标签样本的数量再小它也只需要在这个已经合理的特征空间里做很少的调整而不是从零开始摸索类别边界。1.2 伪标签为什么会越滚越错很多半监督方法的核心是给无标签数据生成伪标签然后让模型去拟合这些伪标签。问题在于伪标签不是免费的午餐。早期我在跑一致性正则方法时发现一个非常典型的失败模式训练到中后期模型会进入一种自我确认的恶性循环。模型对某一类样本的判断出现了偏差产生了一批错误伪标签这些错误标签反过来强化了模型在这类样本上的错误判断于是错误量像滚雪球一样越滚越大。到最后模型把所有信心都集中到少数几个类别上其他类别的准确率直接崩盘。这正是所谓的确认偏差问题也是半监督分类里最难根治的顽疾之一。FixMatch 的思路是用高置信度阈值做筛选——伪标签的置信度低于 0.95 就丢弃。这个策略能挡住一部分噪声但阈值太高会损失大量无标签样本阈值太低又不够干净调参过程很痛苦。对比学习处理这个问题的方式更间接、也更优雅。它不直接跟伪标签的置信度纠缠而是通过特征层面的拉近和推远让同一个类别在特征空间里自然聚拢。当特征空间本身已经形成清晰的聚类结构后再用伪标签去给样本归类模型需要做出的决策从判断这个样本像猫还是像狗变成了把这个样本放到它本来就在的那个聚簇里去。决策难度的下降会直接表现为伪标签质量的提升。1.3 对比学习试图补上哪块短板对比学习在半监督场景里的核心作用可以概括成一句话它给特征空间提供了结构先验。具体来说有监督交叉熵只保证不同类别的特征均值距离尽量远但它不保证同类别内的特征紧凑。对比学习刚好相反它把每个样本的每种增广视图都看成独立实例要求同一实例的不同视图在归一化特征空间里尽量接近不同实例尽量远离。这个目标强制模型学到对增广扰动不敏感的特征比如物体形状、相对位置、颜色分布这些本质属性而不是被迫记忆训练集里的表面纹理。这个方法线的设计动机正是如此与其想方设法从少量标注里压榨监督信号不如先用对比学习把特征空间铺好让监督信号在这片已经开垦过的土地上更高效地工作。2. 方法主线一个预测拉近推远的统一框架2.1 骨架设计双分支与两种视图我读的这套方法整体是一个双分支框架结构上融合了 FixMatch 的一致性正则和 SimCLR 式的对比学习两个分支共享一个编码器。具体地说网络由三部分组成Encoder (f)负责把图片映射为特征向量通常用 ResNet-50 或 WideResNet-28-2。Projection head (g)接在 Encoder 之后是一个两层的 MLP把特征投影到一个归一化的低维空间输出维度一般是 128 或 256。这个只用于计算对比损失推理阶段会被扔掉。Classifier head (h)接在 Encoder 之后输出各类别的 logits用于最终分类。训练时每个 batch 里同时包含有标签样本和无标签样本。每个样本会做两次独立的数据增广得到弱增强视图和强增强视图。弱增强通常是随机裁剪 水平翻转这类保持语义的操作强增强则使用 RandAugment、Cutout 这类会大幅改变图像外观的增广策略。在训练过程中弱增强视图主要用于生成伪标签因为它保留了足够多的原始语义信息模型对它的预测相对可信强增强视图则用于计算一致性损失和对比损失它让模型不得不学会穿透表层干扰、看穿语义本质。视图设计本身不是随意安排的它直接决定了伪标签的可信度和特征表示的鲁棒性。2.2 四类损失如何各司其职整个训练过程的总体损失大致可以写成这样L L_sup λ_u · L_consistency λ_c · L_contrastive三个损失函数各管一段具体分工如下第一是有监督交叉熵损失 (L_sup)作用在有标签样本的弱增强视图上。它是最直接、最可信的监督信号但数量很少。它的任务不是独自把模型训好而是给模型提供一个可靠的种子确保特征空间里的类别锚点没有漂移。我之前试过只让有标签的强增强视图进分类损失放弃弱增强视图效果反而变差原因是强增强视图的表观变化太大在标签极少时会把边界学得过于弯曲。第二是无标签一致性损失 (L_consistency)。先对无标签样本的弱增强视图做预测生成伪标签分布 (q_w)然后通过温度系数做锐化尖锐化再用置信度阈值筛选只保留最大置信度超过阈值的样本。这些样本的伪标签会被当作目标去约束强增强视图的预测 (p_s)二者之间的交叉熵就是一致性损失。设计上采用弱监督强的方式因为弱增强视图的预测更容易得到高置信度、低噪声的分布。第三是对比损失 (L_contrastive)。它同时作用于有标签和无标签样本。计算方式是在 projection head 的输出空间里做 InfoNCE对于每一个样本它自身的另一个增广视图是正样本batch 内其他所有样本或 memory bank 里的其他样本都是负样本。目标函数减小正样本对之间的距离增大负样本对之间的距离。这个模块有效弥补了交叉熵和伪标签方法对特征空间结构约束不足的问题。第四是论文里常被忽略但非常关键的一个设计分类交叉熵和对比损失的梯度方向不完全一致因此需要 λ_u 和 λ_c 的调度策略。论文的典型做法是前期让对比损失的权重偏高让特征空间先铺开后期逐步降低 λ_c让分类头有更多的空间来精调边界。我按这个思路做实验时发现它确实能抑制后期训练震荡——这个后面会专门展开说。2.3 看待它与 SimCLR、FixMatch 的差异初学者最容易问的问题是这个方法和 SimCLR 到底差在哪里跟 FixMatch 又差在哪里先说 SimCLR。SimCLR 是纯自监督方法它完全不用标签。在这个框架里SimCLR 更像是被嵌入进来的一个部件——它负责产出表征结构但不负责给出分类结果。原始的 SimCLR 没有分类头也无法直接做半监督分类而这个方法在线性分类头上接了监督信号同时保留对比学习对特征几何结构的塑造能力。你可以理解为SimCLR 是基建工程这个方法在基建完成后还派了监理团队去盯细节质量。再说 FixMatch。FixMatch 是纯粹的一致性正则方法它只靠弱增强视图的伪标签来约束强增强视图的预测特征空间结构基本靠分类损失在背后支撑。问题在于当标注样本非常稀疏时FixMatch 的特征空间可能一开始就是病态的伪标签质量在训练早期会很糟糕。对比学习的引入相当于给特征空间装了稳定器让弱增强视图能产出更可信的伪标签。所以这个框架的关键洞察可以这样表达一致性损失负责把监督信号横向传播对比损失负责纵向加强特征的可区分性二者相互配合才构成一个完整的半监督学习闭环。3. 实验设计的门道与结果解读3.1 评测基准怎么选论文的实验安排里最让我印象深刻的不是它最终刷了多少分而是它对评测基准的处理方式。半监督分类领域的标准评测集是 CIFAR-10、CIFAR-100、SVHN 和一小部分 ImageNet。固定训练轮数和 batch size只改变有标签样本的数量考察模型在不同标注预算下的表现。典型设置如下数据集有标签样本数度量指标CIFAR-1040 / 250 / 4000Top-1 AccCIFAR-100400 / 2500 / 10000Top-1 AccSVHN40 / 250 / 1000Top-1 Acc这里面有一个每个做实验的人都要注意的细节CIFAR-10 的 40 标签设置对几乎所有方法来说都是一个断层式的难度跃迁。每类只有 4 张图弱增强视图的伪标签在早期几乎不可用。数据增强策略稍微弱一点伪标签就全是噪声增强策略稍微强一点模型可能连弱增强视图都认不出来。能在这个设置下稳住的模型往往才是真正把对比学习和半监督训练揉合好了的模型。我复现时发现如果不做对比损失只靠 FixMatch 那一套40 标签的 CIFAR-10 准确率约在 48% 左右标注随机种子不同会有波动。加入对比损失的完整版大约能拉到 65% 上下。这个差距不是小数目它说明在极端低标注场景下伪标签方法已经接近失效而对比学习提供的结构先验还能撑住大局。3.2 消融实验告诉我们的核心问题消融实验是这篇方法笔记里信息密度最高的部分。我把它拆成三个问题来看第一个问题是对比损失的增益到底来自哪里论文分别测试了去掉对比损失但保留一致性损失、去掉一致性损失但保留对比损失和两者都有三种配置。结论是两者单独使用时效果都远低于合体版本。一致性损失让特征边界清晰化对比损失让特征聚类紧密化它们的增益不是简单叠加而是互相放大。我从自己的实验里观察到去掉对比损失后特征空间中同类样本的类内方差明显变大伪标签的置信度分布也更平坦说明对比损失实实在在地压缩了类内距离。第二个问题是伪标签阈值应该设多少固定其他条件阈值从 0.90 调到 0.98最优区间基本落在 0.93 到 0.96 之间。阈值设得低0.85 左右伪标签数量多但噪声也大模型准确率波动明显设到 0.98伪标签干净了但无标签样本被利用得太少训练后期的提升曲线几乎走平。所以阈值的本质是在利用率和精度之间做一个平衡0.95 左右是经验上比较稳的中间值。第三个问题是投影头输出的维度对结果有没有影响论文比较了 64、128、256、512 四种维度。128 和 256 的性能接近512 略有下降64 明显更差。这个现象可以从两个角度解释维度过低时InfoNCE 需要区分样本的空间不够特征间阳极重叠严重维度过高时对比损失的目标空间过于稀疏语义相近的样本也全被推远反而破坏了聚类结构。我实际复现时喜欢用 128主要原因是省显存而且性能上和 256 没有明显差异。3.3 一个值得注意的结论负样本不是越多越好对比学习里有一个普遍认知负样本数量越多效果越好。这在 SimCLR 的原始设定里基本成立但在半监督分类的框架里这个结论会变得微妙。原因在于半监督分类里引入了一个有监督信号它会把相同类别的样本拉近。如果此时对比学习仍然把所有其他样本当作负样本无差别推远就会在特征的类别边界处产生拉锯战——交叉熵说这两个样本是同类请靠近对比损失却说这两个是不同实例请分开。当 batch 里恰好出现同类样本的概率较高时这种拉锯会造成训练后期的震荡。更好的做法是给对比损失加一个类别感知的开关当模型对无标签样本的伪标签置信度较高时可以直接把伪标签类别不同的样本作为负样本把伪标签类别相同的样本从负样本集合中剔除或者进一步在对比损失中加入类别的条件权重。这实际上就把通用的对比损失升级成了监督对比损失SupCon的一种变体能够让训练过程稳定下来。我在自己的一次实验中对比过无差别负样本和类别感知负样本两种配置后者的最终准确率大约高出 2 个百分点而且收敛曲线平滑得多。如果你的显存比较紧张或者训练不稳定这绝对是一个值得优先尝试的改进方向。4. 复现过程中的关键细节与踩坑记录4.1 温度系数的连锁反应对比学习的 InfoNCE 损失里有一个重要的超参数——温度系数 τ。它控制着相似度分布的锐利程度τ 越小softmax 输出的分布越尖锐模型对正样本对应拉近的力度越强τ 越大分布越平滑学习信号越柔和。我的复现经验是把 τ 设定为 0.05 到 0.1 之间效果比较稳定。设得太小比如 0.01模型会变得非常激进稍微有一点相似度差异就被放大早期容易引发特征空间的剧烈崩塌设得太大比如 0.5对比学习的信号就会被稀释特征聚类效果明显变差。这个参数还和 batch size 高度相关。大 batch 下负样本更多τ 可以适当调大一些避免过度尖锐小 batch 下负样本稀缺τ 要调小才能让有限的正负样本对产生足够的梯度。我刚开始复现时用了固定的 τ0.1但 batch size 从 256 改到 64 后照样崩后来才发现是负样本数量大幅减少导致的——把 τ 调成 0.07 后训练才恢复正常。值得强调的是伪标签锐化里的温度系数和对比损失里的温度系数要分开调。伪标签锐化的温度一般设置在 0.4 到 0.6 之间目标是把软标签分布挤向一个类别的置信度占主导的状态。如果这里把温度设成 0.1伪标签的分布会被压缩成 one-hot类别概率的信息量直接被破坏。4.2 伪标签阈值怎么设才稳前面消融那里提过阈值最优区间是 0.93 到 0.96但这里我想补充一个很多论文里不会写清楚的实操问题阈值的设定需要和训练阶段联动。一种更稳的做法是动态调整阈值。训练前 20 个 epoch模型对无标签数据的判断还不可靠可以把阈值设到 0.98 附近保证进入损失函数的伪标签尽可能干净训练中期模型逐渐稳定逐步把阈值降到 0.95后期再进一步降到 0.93 左右回收更多无标签样本的梯度贡献。这也就是 FlexMatch 提出阈值动态调整思路的动机。在我自己的实验里动态阈值比固定阈值普遍高出 1 到 1.5 个点。还有一个容易忽略的问题伪标签生成属于在线过程每个 epoch 生成的伪标签都可能变化如果模型在某个 epoch 突然发生了剧烈更新比如学习率过大伪标签分布会发生跳跃式变化对训练稳定性危害很大。我建议对伪标签做指数滑动平均EMA让伪标签的变化轨迹平滑化这会显著减少训练后期准确率的抖动。4.3 训练的稳定性EMA 和 batch size 的坑对比学习框架对 batch size 的敏感性远超纯分类模型。原因是 InfoNCE 的效果直接依赖负样本数量负样本太少时对比损失的方差极大训练会呈现一种间歇性震荡的状态。显存不够怎么办比较实用的方案是加一个 memory bank把过去若干 batch 里计算过的特征向量存下来训练当前 batch 时把 bank 里的旧特征也作为负样本参与计算。这个思路源自 MoCo 的设定好处是把有效 batch size 从物理上的 64 扩到几千的大小又不怎么增加显存开销。我在复现时还遇到一个很具体的问题当 backbone 使用批量归一化BN时小 batch 下 BN 的统计量不稳定直接导致对比学习的正负样本对特征均值漂移。解决方法是把 projection head 里的 BN 换成 Layer Normalization或者至少在整个 encoder 的核心层使用固定统计量的 BNrunning stats。这是一个只会在实际训练中碰到的坑论文数据里完全看不出来但一碰就非常头疼。另外EMA 不仅用于伪标签也可以用于模型的参数更新。很多这套框架的变体里会维护一个 teacher 模型的 EMA 版本用它来生成伪标签和提取特征student 模型则根据这些目标做对比学习和分类。这本质上是把训练目标的滞后性利用起来让伪标签的来源比当前模型更稳定。我在复现中设置 EMA 衰减系数为 0.999效果不错。如果衰减系数太小teacher 和 student 更新太快伪标签照样会震荡太大则更新过慢模型跟不上数据分布的变化。4.4 从代码层面看projection head 到底要不要参与分类训练这个细节在论文里往往一笔带过但它对最终准确率有实打实的影响。Projection head 的输出是对比损失专用的表示空间因为它强制要求实例区分空间里保留的是这个样本和其他样本差异有多大的信息而不是这个样本属于哪个语义类别的信息。如果你让 classifier head 也读取 projection head 的输出分类性能往往不升反降。正确的用法是分类头只从 encoder 的特征层读取输入projection head 的梯度也只在对比损失中回传。推理时projection head 会被完全抛弃只用 encoder classifier head 输出预测结果。这听起来像一个实现上的小细节但我见过有不止一个人把两个 head 共享了底层特征之后发现对比损失把分类边界搅成了浆糊然后回头怀疑方法的有效性。5. 这套方法给我的启发与落地思考5.1 什么时候应该用对比学习路径任何方法都有它的适用边界。读完备套框架之后我个人的判断是当你有很有限的有标签数据但无标签数据的分布足够丰富、覆盖了目标类别的绝大部分视觉形态时对比学习路径的价值最大。它特别适合那些类别少、但每个类别的视觉变化巨大的任务比如工业质检中的缺陷类型识别、医疗影像中的病变分类、自然影像中的细粒度分类等。反过来如果无标签数据的分布和你关心的目标任务偏差很大比如你有 10 万张日常照片但只想识别工业零件上的 3 种瑕疵这时候对比学习学到的特征结构可能和瑕疵分类几乎无关硬加上去反而会引入分布外噪声不如老老实实用一个高质量伪标签一致性方法。5.2 如何扩展到文本、多模态等其他场景对比学习加半监督分类的思路本质上跟模态绑定程度并不强。我刚接触这套方法时也在想如果换成文本任务这套框架还能不能成立答案是能但增强策略要换机制。在文本分类里弱增强可以是保留语义的句子片段丢弃强增强可以是回译或者同义词替换。在伪标签生成环节回译方式生成的增广视图由于语义噪声较大生成伪标签时置信度容易偏低此时可以适度降低阈值或者引入基于模型预测一致性的筛选机制。多模态场景的扩展也类似可以在图像和文本两个模态之间做跨模态对比学习正样本定义为同一语义内容的不同模态表达负样本定义为不同语义内容的一对样本。这类扩展目前已经有论文在做不过从工程落地角度看还不够成熟主要问题在于伪标签和对比损失之间的权重协调方式需要重新设计。5.3 仍然没有被解决的问题这套框架并非万能的。它有一个理论上的短板——对比学习本质上假设样本之间是相互排斥的而对于细粒度分类任务来说很多不同的类别在特征空间里天然是相邻的强行拉开距离会把边界推到错误的位置。在长尾分布的任务里尾部类别的样本数量极少对比学习会把这些稀有类别的样本当作负样本推走反而让它们更难聚类这个问题目前还没有特别好的解法。我自己的经验是这类问题可以通过提高尾部类别的采样权重或者把 tail class 的样本从负样本中剔除来缓解但这样做的代价是需要额外维护一个类别数量的统计模块实现复杂度会上升不少。不管怎么说这套方法至少打开了对比学习为半监督分类提供了特征结构先验这一思路的大门它给我最大的启发是半监督分类的真正难点从来都不是设计一个更花哨的损失函数而是怎么让模型在没有标签的情况下先理解世界的结构——对比学习恰好提供了一条可行的路径。
返回列表