半监督图像分类实战:从FixMatch到PyTorch训练全流程
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但在复现阶段不鼓励一上来就上大模型——排查问题会非常痛苦。优化器建议使用SGDmomentummomentum设为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 数据增强策略弱增强与强增强的选择弱增强一般就是RandomCroppadding4 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, dim1) conf, pseudo probs.max(dim1) mask (conf threshold).float() logits_u_s model(x_u_s) # 强视图保留梯度 loss_unsup (CE(logits_u_s, pseudo, reductionnone) * 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到30m太小达不到一致性正则的效果m太大会让强视图已经无法辨认模型被迫去拟合噪声。我在实验中发现m在10附近时效果最好。你可以做一组m5、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 CutOutEMA 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和CPSCPS训练两个不同初始化的模型用一个模型对另一个模型的预测做一致性约束。如果你已经吃透了本文讲的核心流程迁移到那些场景并不会太费力。伪标签筛选、EMA teacher、一致性正则、置信度阈值、类别均衡这五个组件就是半监督视觉任务的通用工具箱。分类只是复杂度最低的一个入场项目。8. 个人体会与几个值得保留的调试习惯说到最后分享几条我在大量半监督实验里总结出来的个人体会希望能帮你少走弯路。第一把无监督损失的数值当作健康指标不要只盯着测试精度。如果你发现无监督损失快速下降但测试精度同步下滑几乎可以断定是伪标签污染了马上调高阈值或者检查warmup模型。第二保存每个epoch的伪标签可视化结果。直接把伪标签错误样本打印出来看比看任何曲线都直观。很多时候你会发现模型把混淆集中在几个语义相似的类别之间这时候最有效的操作不是调参而是给这几个类别多标一些样本。第三半监督方法的调参空间不小但绝大多数时候从FixMatch基线出发、只调阈值和ramp-up就能拿到八成以上的收益。不要为了追求SOTA而一上来堆砌各种模块先把一个简单框架调到稳定再考虑加对比分支或聚类约束。第四警惕把测试集精度当成模型真实能力的唯一标准。半监督模型在分布内测试集上的精度可能和全监督接近但在分布外数据上退化非常快。如果业务数据本身分布漂移频繁建议在验证时加入一个独立的分布外测试集。最后如果你打算在这个方向深入我建议把EMA teacher相关代码好好读一遍。它是半监督视觉任务里性价比最高的组件不光在分类里有用检测、分割、跟踪场景也大量使用。理解它的原理之后你会发现自己对半监督训练稳定性的理解会上一个台阶。