1. 为什么我盯上了半监督图像分类
训练集标注成本,迟早会成为一个项目的天花板。做计算机视觉的朋友应该都有这种感觉:公开数据集刷精度挺顺手,一到自己的业务场景就开始头疼。业务数据的标注费用高、周期长、隐私问题多,尤其在医疗影像、遥感图像、工业质检这些领域,找懂行的标注人员比找算法工程师还难。我手头有一个某场景的图像分类项目,前后攒了十几万张图像数据,但真正带标签的只有几千张。请人标剩下十万张,按每张几毛钱算也得大几万块,时间还要耗掉好几周。
后来我转向了半监督图像分类这条路:用少量标注数据 + 大量未标注数据一起训练模型,在几乎不增加人工成本的前提下拿到接近全监督的效果。这个方向在近两年其实已经相当成熟,各种SOTA方法层出不穷,而且很多核心技巧并不复杂,用PyTorch就能自己复现。这篇文章不打算罗列论文,而是从我实际跑通实验的角度,把半监督图像分类的常用方案、关键配置、踩坑点和性能评估逻辑完整讲一遍。
我假设你已经会PyTorch基础训练流程。如果纯粹是新手,前半部分理论内容也能帮你建立全局视野。提醒一下:即使是最新的半监督方法,也没有脱离“用未标注数据帮助模型”的本质。先把这一点看透,后面所有技术细节都围绕它展开。
2. 半监督学习的核心逻辑:一张未标注图到底能帮上什么忙
很多人第一次接触半监督图像分类时,都会问一个很朴素的问题:未标注的数据又没有标签,模型从它们身上能学到什么?这个问题值得认真回答。
2.1 三个基本假设:平滑性、聚类、低密度分离
半监督学习能起作用,依赖的是数据本身的分布规律。确切地说,有三个基本假设支撑着几乎所有算法设计。
第一个叫平滑性假设:相近的输入应当有相近的输出。如果两张图像在特征空间里距离很近,那它们的类别大概率相同。把未标注图像放在模型面前,它虽然不知道这张图的真实标签,但只要能把周围已标注样本的决策边界“顺滑”地推过去,就能对未标注区域做出合理推断。这本质上是一种空间先验:类别信号在流形上是连续的,不会突变。
第二个叫聚类假设:数据在特征空间里会形成簇,同一簇内的样本大概率属于同一类。这给了未标注数据一个很自然的使用方式:判断某张未标注图落在哪个簇里,把它拉向该簇的类别中心。许多方法里的“伪标签”“类中心对齐”本质都是在利用聚类结构。
第三个叫低密度分离假设:分类决策边界最好穿过特征空间的低密度区域,而不是高密度区域。直观理解就是,两个类别之间的分界线应该尽量远离所有样本点,避免把聚集在一起的样本从中间劈开。半监督方法通过让模型对未标注数据的预测更加自信(低熵),不断把决策边界推向样本稀疏的地方。
这三条假设是半监督学习的“地基”。后面所有方法——不管是伪标签、一致性正则、对比学习还是图方法——最终都在变着法子利用这三个假设,只是切入角度不同。
2.2 全监督、半监督、自监督和弱监督的边界在哪里
很多人容易把半监督、自监督、弱监督混在一起,这里我做个简单的区隔,方便后面讨论时不乱。
全监督是每个训练样本都有标签。半监督是少量有标签、大量无标签。自监督则是完全没有标签,靠构造辅助任务(如旋转预测、对比学习)学特征表示。弱监督通常指标签很粗糙,比如只有图像级标签,没有目标位置框,或者标签带有大量噪声。
半监督和自监督在实践中经常搭配使用:先用自监督在大规模未标注数据上做预训练,然后再用半监督方法微调。这两种思路并不冲突,甚至互补。很多半监督SOTA流程里,预训练表示的质量直接决定了后续半监督性能的上限。
2.3 半监督为什么不是“玄学”:一张未标注图的信息量
回到刚才那个问题。一张未标注图像确实没有提供“类别答案”,但它提供了“结构信息”。模型看到十万张未标注图像后,能学到特征空间中哪些区域密集、哪些区域稀疏、哪些维度能够区分不同语义内容。这种结构信息在少量标注样本的条件下,足以帮助模型避免大量低级的过拟合。
更实际地讲,有标注数据告诉你“什么是什么”,未标注数据告诉你“什么是相似的”。分类模型既要辨认类别,也要学会特征抽取。后者其实占据模型能力的一大部分,而这部分完全可以靠未标注数据训练。把分类任务拆成“找特征”和“分边界”两步之后,半监督的价值就非常清晰了。
3. 半监督图像分类的主流方法演进:从伪标签到统一框架
理解了底层逻辑,再看具体方法会顺很多。半监督图像分类近五年经历了几个阶段,每个阶段都在解决上一代方法的核心短板。我从实用的角度把它们分为四类,并给出它们的适用场景。
3.1 自训练与伪标签方法:最朴素也最实用
自训练的思路非常简单:先用带标签数据训练一个模型,然后对未标注数据做预测,把高置信度的预测结果当作伪标签加入训练集,迭代训练。
早期做这个事的人很快发现一个问题:模型会越来越自信,错误的高置信度预测会固化下来,越训越偏。但后来大家找到了补救方法,就是置信度阈值。只有预测概率超过某个阈值的未标注样本才能进入训练集。比如阈值设为0.95,那么模型输出概率低于0.95的统统丢弃。这个简单的过滤机制让伪标签方法从“容易翻车”变成了“稳定可用”。
伪标签方法的优势是简单直接,没有任何花哨的约束项,容易工程化。缺点是过度依赖阈值选择,且模型本身的自信心容易虚高,尤其是面对分布外样本时。你的未标注数据如果和标注数据分布差异很大,伪标签可靠性就会大幅下降。
3.2 一致性正则方法:在扰动中寻找不变性
一致性正则的核心思想是:对同一张图施加不同扰动,模型给出的预测应当保持一致。这个思想源于一个朴素的观察——一个合格的分类器不应该因为图像的轻微平移、旋转、加噪声、裁剪就改变判断。
具体做法通常是:对未标注图像做一次弱增强(如随机裁剪+翻转)得到弱视图,再做强增强(如AutoAugment、RandAugment)得到强视图。模型对弱视图和强视图的预测分布应该接近,于是可以用一致性损失(如KLD散度、MSE)约束模型。最常见的一致性正则实现有Π-Model、Temporal Ensembling、Mean Teacher。
Pi-Model是每次迭代都对同一个未标注样本做两次随机增强,拉近两次预测结果。Temporal Ensembling是对每个样本维护一个历史预测的滑动平均,让当前预测向历史平均靠拢。Mean Teacher则维护一个教师模型的权重(学生模型权重的指数移动平均),用教师模型的预测充当监督信号。
一致性正则的哲学是“决策边界必须对扰动鲁棒”。如果你对一张猫图做调整后模型却说是狗,说明决策边界离数据太近了,不稳定。一致性正则恰好把边界推远。
3.3 对比学习加入战局:表示学习与半监督的融合
前面两类方法更多是在利用现成的特征,而未标注数据最擅长的事情其实是学特征。对比学习在这时候加入了战局:通过拉近同一图像不同增强视图的表示、推远不同图像之间的表示,让模型在没有任何标签的情况下学到高质量特征。
半监督方法引入对比学习后,一个常见做法是把SimCLR、MoCo之类的对比损失加进整体训练目标。对未标注数据,对比损失负责学表示;对标注数据,交叉熵损失负责学分类。特征质量和分类边界同时优化,效果往往比单独使用伪标签或一致性正则更好。
不过对比学习的代价是训练开销激增,因为需要构造正负样本对,负样本数量直接影响表示质量,显存和时间消耗都不可忽视。如果你的算力有限,我的建议是优先用带对比分支的方法来评估收益,再决定是否全量加入。
3.4 FixMatch与主流统一框架:阈值、强增强与动量更新
当前半监督图像分类的SOTA方法,基本上走的是整合路线。FixMatch是其中的代表:它把伪标签方法和一致性正则结合在同一个框架里。
FixMatch的流程可以概括为三步:第一,对未标注图像做弱增强,得到模型预测,如果最大预测概率超过阈值(比如0.95),就把它转为伪标签;第二,对同一张图做强增强,得到强视图;第三,让强视图的预测去匹配弱视图的伪标签。这等于把伪标签方法的“高置信度过滤”和一致性正则的“扰动鲁棒性”合并成了一个损失项。
这种设计的好处是:伪标签筛选保留了可靠监督信号,强增强约束提高了模型的泛化能力,两个机制互为补充。后面出现的FlexMatch、CoMatch、UniMatch等,大多是在FixMatch的框架上做细分优化——或者调整阈值策略,或者引入对比分支,或者允许多视图参与。
我拿FixMatch做了大量实验,强烈建议入门半监督图像分类的朋友直接以FixMatch为基线。理由有三个:实现简单,不依赖太多额外组件;性能上限很高,和许多SOTA只差几个点;可扩展性强,想加什么模块都能方便地往上接。
4. 手把手复现一个可用的半监督图像分类训练流程
我这里的复现流程不是把论文代码搬进来直接跑,而是从头搭建一个能稳定work的最小可用系统。我会把数据划分、模型选择、训练细节、代码逻辑全部讲清楚,每一步都有具体参数和理由。建议你按我的顺序搭建,跑通后再自行调整。
4.1 实验设计与数据划分
首先明确实验设计。我采用CIFAR-10作为模拟数据集(你也可以换成自己的业务数据),标注样本数设为4000,未标注样本数设为46000,测试集保持标准的10000张。这是一个比较经典的半监督配置,效果好坏容易被感知。
数据划分要特别注意:标注集和未标注集必须从同一个分布中抽取,类别比例尽量保持一致,不能出现未标注集里某类样本特别多、标注集里特别少的情况。实际业务里分布失衡是常有的事,但实验起步阶段先按均衡分布做,否则变量太多不好排查。
半监督训练通常分两个阶段:先在标注集上用普通交叉熵训练一个初始模型(也叫warmup阶段),然后再加入未标注数据做半监督训练。warmup的epoch数不用太多,一般几十个epoch即可。它的作用是让模型具备一个基本可用的预测能力,否则伪标签和一致性约束在早期都是噪声,反而干扰训练。
4.2 模型选择与优化器配置
模型方面,我推荐ResNet系列,若资源受限可用ResNet-18,条件允许直接用ResNet-50或Wide-ResNet。原因是这些模型稳定性好、没有任何花哨的模块,适合作为算法验证主体。如果你使用更大规模的SOTA方法,可以考虑加注意力模块或Vision Transformer,但在复现阶段不鼓励一上来就上大模型——排查问题会非常痛苦。
优化器建议使用SGD+momentum(momentum设为0.9),权重衰减设5e-4。在CIFAR-10这种小规模数据集上,SGD的效果往往比Adam更好,尤其在半监督场景下,Adam容易把模型推到锐利极小值,泛化能力反而不如SGD。你可能想用AdamW,但实测下来SGD依然是这个场景更稳的选择。
学习率用余弦退火,初始学习率0.03,最小学习率0.0001。单个batch的标注数据部分设置为64,未标注数据部分设置为64。整个训练跑200个epoch,如果显存紧张可以把未标注batch减小,但要保证每个step里未标注样本数量不要少于标注样本数量的一半。
4.3 损失函数设计与动态权重
总损失由三部分组成:有监督交叉熵损失、无监督一致性损失(或伪标签交叉熵损失)、以及可选的对比学习损失。
有监督部分就是标准交叉熵,只对有标注的batch计算:
L_sup = CrossEntropy(model(x_weak_l), y)
无监督部分按FixMatch的方式实现:对未标注图像做弱增强得到弱视图,送入模型得到概率分布;取最大概率作为伪标签,如果最大概率大于阈值则保留该样本;再对同一张图做强增强得到强视图,计算强视图预测与伪标签之间的交叉熵:
L_unsup = CE(model(x_strong_u), pseudo_label) * mask(conf > threshold)
阈值通常取0.95,但这不是绝对的。标注数据越少,阈值应该越高,因为伪标签的可靠性更关键;标注数据较多或类别容易区分时,可以适当降低阈值到0.8~0.9,让更多未标注样本参与训练。
对比损失部分如果加,我会在后面单独说明何时加、怎么加。FixMatch原版并没有对比损失,所以先把基础版跑通再考虑扩展。
4.4 数据增强策略:弱增强与强增强的选择
弱增强一般就是RandomCrop(padding=4)+ RandomHorizontalFlip,这种程度的增强不会显著改变图像语义,适合用来产生稳定的伪标签。
强增强我试过AutoAugment、RandAugment、CutOut、Mixup等。比较推荐RandAugment,它在CIFAR-10上效果稳定,而且不需要在训练前搜索增强策略。CutOut可以和RandAugment叠加使用。
这里有一个值得注意的细节:模型对强增强视图的预测天然会更不稳定,这其实是好事。强视图相当于一个挑战样本,模型只有真正学到了鲁棒特征,才能在强扰动下保持正确预测。如果强增强切换得太早,模型还在warmup阶段就被强视图带偏了,所以无监督损失可以逐步升温(ramp-up),例如前20个epoch把无监督损失的权重从0线性升到1。这个技巧非常有效,尤其是从头训练时。
4.5 核心代码逻辑
我整理了一个去掉工程噪声的最小PyTorch实现逻辑,方便你抓住训练循环的核心:
for epoch in range(start_epoch, total_epochs): for x_l, y_l, x_u_w, x_u_s in loader: # 有监督损失 logits_l = model(x_l) loss_sup = CE(logits_l, y_l) # 无监督损失 with torch.no_grad(): logits_u_w = model(x_u_w) probs = torch.softmax(logits_u_w, dim=1) conf, pseudo = probs.max(dim=1) mask = (conf >= threshold).float() logits_u_s = model(x_u_s) # 强视图,保留梯度 loss_unsup = (CE(logits_u_s, pseudo, reduction="none") * mask).mean() # 总损失 loss = loss_sup + unsup_w * loss_unsup optimizer.zero_grad() loss.backward() optimizer.step()这个流程里,伪标签是在梯度停止的条件下生成的,否则模型会通过强视图的“自我确认”路径退化成一种捷径学习,模型不再真的分辨图像,而是直接模仿自己的高置信度输出。这个细节非常关键,忘了stop_gradient会让整个训练体系崩掉。
5. 半监督训练过程中我踩过的那些坑
这个部分我想重点讲一些坑。它们的共性是:每一步单看都合理,组合起来却会导致训练失效。不把这些讲清楚,你照着paper写的代码可能跑出非常差的结果,却完全不知道问题出在哪。
5.1 阈值太低的伪标签污染:模型自信与真实精度的偏离
伪标签方法最大的敌人是“过自信”。模型在训练早期对未标注数据的预测看起来非常自信,但真实精度并不高。这时候如果阈值设得太低,大量错误伪标签会进训练集,模型会把这些错误当成正确答案去拟合,形成“自我污染”。
举个真实例子:我在warmup后的CIFAR-10模型上统计过,预测置信度大于0.7的未标注样本中,实际正确率大约只有85%;当阈值提升到0.95时,正确率可达95%以上。但提升阈值意味着可用样本急剧减少,训练后期又会陷入样本不足的问题。
实践上更好的做法是两段式阈值:前一半训练epoch用0.9,后一半用0.97。或者采用动态阈值策略——FlexMatch的做法是根据每个类别的学习进度动态调整阈值,实现上更复杂但确实有效。当你发现损失下降但验证集精度停滞时,优先检查伪标签错误率,它往往已经悄悄飙升了。
5.2 类别不均衡让伪标签彻底失衡
半监督场景下类别不均衡比全监督更致命。假设某个类别在未标注数据中只占5%,但模型初始预测它占15%,伪标签会进一步放大这种偏差,导致这个类别的预测概率被严重高估。
这个问题有几个补救手段。一是为无监督损失加上类别权重,通过收集当前epoch的伪标签统计分布,对每个类别的损失做反频率加权。二是使用基于分布的伪标签阈值:如果一个类别的预测比例远高于先验分布,提高该类别伪标签阈值,反之降低。三是在采样时引入类别均衡策略,尽量让每个batch里各标注类别的数量接近。
我当时跑业务数据时发现某个类别直接崩到0%准确率,排查半天才意识到未标注集里该类样本极少,且warmup模型对它的识别能力本来就弱。后来我按SoftMatch里的方法做了自适应阈值调整,情况才明显改善。
5.3 批次归一化统计量错乱问题
这个问题出现的概率很高,但很少有人第一时间想到。半监督训练中,未标注batch和标注batch会混在一起过同一个模型。BatchNorm层会统计当前batch的均值和方差。如果你的batch构成不稳定(比如有时全标注、有时全未标注),模型的归一化统计量就会剧烈抖动。
更隐蔽的问题是:在计算弱视图伪标签时用了torch.no_grad(),此时BN的running statistics仍然会更新,但强视图计算又更新了一次。两个视图各自更新,结果归一化统计量被双倍推进,导致模型对单样本预测时的表现非常不稳定。
解决办法有两个方向:要么在计算伪标签时显式设置model.eval()来冻结BN统计量,计算完恢复train模式;要么放弃BN,使用GroupNorm或LayerNorm。我最终选择了后者,因为GN在batch大小波动时更鲁棒。如果你的显存可以撑住固定batch大小,那继续用BN也没问题。
5.4 强增强过头:伪标签完全失去意义
增强强度是另一个调参重灾区。RandAugment的强度参数m从0到30,m太小达不到一致性正则的效果,m太大会让强视图已经无法辨认,模型被迫去拟合噪声。
我在实验中发现,m在10附近时效果最好。你可以做一组m=5、10、15、20的对照实验,观察无监督损失和验证集精度的变化趋势。还有一种处理方法是EMANet里那种做法——让弱视图的预测从指数滑动平均模型中产生,这个教师模型天然更稳定,生成的伪标签质量也更高。我在自己的框架里保留了EMA teacher,实测在多个数据集上都能稳定提升半个点到两个点。
6. 性能评估:你的半监督模型到底行不行
很多人训练完半监督模型后,只用测试集精度一个指标做评估,这是不够的。我建议你至少建立一套自己的评估方案,包括以下几个方面。
6.1 诊断曲线:只看测试精度远远不够
训练过程中除了记录loss和测试精度,我强烈建议保存以下曲线:标注集交叉熵、未标注集伪标签精度(即被mask选中的样本中伪标签正确的比例)、未标注集覆盖率(被mask选中的样本比例)、每个类别的预测分布随时间的变化。
伪标签精度和覆盖率是我最依赖的两个指标。伪标签精度高而覆盖率低,说明阈值太严,未标注数据没被充分利用;覆盖率高的同时伪标签精度下降,说明模型已经开始自我污染。理想状态是伪标签精度能维持在95%上下,且覆盖率随训练逐渐走高。
另一个值得记录的是置信度分布曲线。如果大量未标注样本的预测置信度集中在0.9~1.0区间,但伪标签精度只有80%,说明模型过度自信,可能需要更强的约束或者更高的阈值。如果置信度普遍低于0.6,说明模型本身能力不足,warmup阶段就应该加强。
6.2 消融实验的清单:哪些模块真正起效
如果你想在业务场景里上线半监督方案,我强烈建议按下面的清单做消融实验,每一项都单独开合关:
- 无监督损失有无(看半监督相比纯全监督的收益)
- 阈值高低(0.85 / 0.90 / 0.95)
- 弱增强与强增强的选择(RandAugment vs AutoAugment vs CutOut)
- EMA teacher 有无
- 无监督损失ramp-up有无
- 对比损失分支有无
- BN vs GroupNorm
每一组实验不需要跑满200个epoch,跑60个epoch看趋势就足够筛选方向了。我在多个实验里发现,阈值和ramp-up的影响通常最大,EMA teacher次之,对比损失分支有时候反而会掉点。如果你时间有限,优先调前三个。
6.3 与弱监督/全监督基线的对比逻辑
比较半监督效果时,最容易犯的错是只跟最好的全监督结果比。正确的评价逻辑应该同时给出几条基线:
全监督下限:只使用现有标注数据训练(不做半监督);全监督上限:假设所有未标注数据都有标签,训练同一个模型;半监督方法:使用现有标注 + 未标注数据训练。
只有把这三条线放在一起,才能判断你的半监督方法到底有没有真正榨干未标注数据的价值。如果半监督结果只比全监督下限高不到一个点,说明你的未标注数据大概率没有被有效利用,问题多半出在伪标签质量或者数据分布上。
7. 进阶思路:不同数据规模与场景下的策略调整
基础流程跑通后,你一定会遇到自己的业务场景和标准benchmark差异很大的情况。这时候需要根据数据规模、类别数和分布特点做针对性调整。
7.1 标注数据极少:从初始模型到伪标签保护的梯度
当标注数据只有几百张甚至几十张时,warmup模型几乎没有任何区分能力,整个训练过程会变得很不稳定。我的建议是三点:先做无监督预训练(比如用SimSiam或MAE在未标注数据上学表示),再加载预训练权重做warmup;把固定阈值改成每个类别动态调整;无监督损失的权重从非常小的值(比如0.1)开始,让模型先稳定在标注数据附近。
我试过在只有每类10张标注图像的情况下,先用MoCo v3在未标注数据上预训练200个epoch,再接入半监督流程,效果比直接FixMatch高出不少。本质上预训练阶段已经把聚类结构学到了模型里,后续的半监督训练只是在精调边界而已。
7.2 类别很多且语义相近:伪标签的置信度陷阱
20个类别以内的分类任务,伪标签置信度通常还算可靠。但当类别数超过100,且类别之间语义相近时(比如不同品种的昆虫、不同类型的瑕疵),模型输出的概率分布往往在几个相似类别间摇摆,最大概率可能虚高。
这种情况下,我更建议使用基于特征距离的伪标签方法,而不是直接拿softmax概率做阈值筛选。具体做法是:记录已标注样本在当前特征空间中的嵌入向量,计算未标注样本与各类别中心(EMA维护)的余弦相似度,把距离最近且相似度足够高的类别作为伪标签。这比softmax概率更稳定,因为它引入了原型约束,降低了对分类头输出校准的依赖。
如果你的半监督任务从图像分类转向目标检测或分割,思路其实也相通:检测器可以先在标注数据上做初始训练,然后在未标注数据上生成高置信度的伪框,再配合一致性正则进行自训练。分割任务则可以把伪标签置信度筛选改成基于区域大小的过滤,小面积的高置信度片段经常是误检。
7.3 从分类到检测和分割:迁移思考路径
这篇文章虽然讲图像分类,但国内做业务的朋友很多会延伸到检测和分割。半监督在这两个方向的经典做法其实也基于相同的逻辑:检测里常用Unbiased Teacher,用EMA teacher生成伪框,并用分类和回归的置信度联合筛选伪框;分割里常用ST++和CPS,CPS训练两个不同初始化的模型,用一个模型对另一个模型的预测做一致性约束。
如果你已经吃透了本文讲的核心流程,迁移到那些场景并不会太费力。伪标签筛选、EMA teacher、一致性正则、置信度阈值、类别均衡,这五个组件就是半监督视觉任务的通用工具箱。分类只是复杂度最低的一个入场项目。
8. 个人体会与几个值得保留的调试习惯
说到最后,分享几条我在大量半监督实验里总结出来的个人体会,希望能帮你少走弯路。
第一,把无监督损失的数值当作健康指标,不要只盯着测试精度。如果你发现无监督损失快速下降但测试精度同步下滑,几乎可以断定是伪标签污染了,马上调高阈值或者检查warmup模型。
第二,保存每个epoch的伪标签可视化结果。直接把伪标签错误样本打印出来看,比看任何曲线都直观。很多时候你会发现模型把混淆集中在几个语义相似的类别之间,这时候最有效的操作不是调参,而是给这几个类别多标一些样本。
第三,半监督方法的调参空间不小,但绝大多数时候,从FixMatch基线出发、只调阈值和ramp-up,就能拿到八成以上的收益。不要为了追求SOTA而一上来堆砌各种模块,先把一个简单框架调到稳定,再考虑加对比分支或聚类约束。
第四,警惕把测试集精度当成模型真实能力的唯一标准。半监督模型在分布内测试集上的精度可能和全监督接近,但在分布外数据上退化非常快。如果业务数据本身分布漂移频繁,建议在验证时加入一个独立的分布外测试集。
最后,如果你打算在这个方向深入,我建议把EMA teacher相关代码好好读一遍。它是半监督视觉任务里性价比最高的组件,不光在分类里有用,检测、分割、跟踪场景也大量使用。理解它的原理之后,你会发现自己对半监督训练稳定性的理解会上一个台阶。