做图像分类做到一定阶段,大家都会撞上同一堵墙:标注成本。我早期做商品图分类时,一批数据动辄上万张,雇人打标一张几毛钱,还得反复质检,一个类目下来几万块没了。后来我把目光转向了图像分类半监督学习,才意识到过去扔掉的那些无标注图片,其实才是真正的富矿。
这篇文章不是论文复现笔记,也不是纯理论科普,而是我从一个做落地项目的工程师视角,把半监督学习从原理到实操、再到工程化过程中踩过的坑和总结的经验完整梳理一遍。适合手里有大量无标注图片、但只有少量人力打标预算的团队参考,也适合想系统理解半监督学习的同学。我会尽量把每个选择背后的"为什么"讲清楚,而不是只丢一堆配置参数。
1. 标注成本困局:半监督学习解决的到底是哪个问题
1.1 我被一张标注报价单敲醒的经历
有一次某项目要做服饰类目识别,建模之前先看了下数据现状:服务平台有60万张商品主图,但只有之前人工打过的3000张有效标签。按照供应商的报价,按类目标签10元/100张算,60万张全量打标要6万元,还不算二次质检的时间。项目经理当时问我,能不能用3000张标注图先把模型训练出来,后续按需增量打标。
放在两年前我大概率会摇头,因为常规经验是:数据不够,模型难产。但那次我刚好在调研半监督学习,于是决定赌一把——先拿3000张标注加5万张无标注图做一次半监督训练,看看能到什么水平。
结果让我意外:同样的测试集上,只用3000张标注做全监督训练,准确率约82%;而加入5万张无标注图做半监督训练,准确率冲到了89.6%。虽然还比不上全量打标的93%,但成本差距是数量级的。从那时起,我就把半监督学习当作分类项目中性价比最高的一条技术路线。
1.2 半监督学习的基本设定与边界
所谓半监督,就是指训练数据被分成两部分:一部分是有标注样本,一部分是没有标注的样本。模型在训练时同时利用这两部分信息,让无标注数据用某种方式"帮上忙"。
它适合解决的问题有三个非常明显的特征:标注成本高,需要专家、需要长时间、需要反复质检;无标注数据量大,原始图片容易获取,只是没人打标签;类别体系相对固定,分类任务的目标类别在训练前已经确定,不是开放式目标。
如果这三点都满足,半监督的收益通常会很大。反过来,如果你的类别本身不稳定,随时可能加新的类,或者你的无标注数据来源混乱、和标注数据分布严重不一致,那半监督带来的风险可能比收益更大。这部分的判断很重要,我后面会详细讲。
1.3 为什么传统"数据增强加预训练"替代不了半监督
这里我经常被问到一个问题:我已经用了ImageNet预训练,再加数据增强,是不是就够用了?我觉得要看清楚一点:预训练解决的是模型初始化的问题,它的收益在样本量不足时确实显著,但它无法把"无标注数据中蕴含的类别结构"显式传给模型。
打个比方,预训练相当于给一个学生请了一个基础科目的老师,让他先打好了基本功;但半监督相当于让这个学生在大量没批改过的作业里自己反复做题,做完再找老师批改一小部分,通过反馈一点点矫正。后者能学到的东西,和前者是不一样的。
数据增强同样如此。增强是让模型对同一张图的变换版本保持稳定,但它只在单样本层面起作用;半监督的一致性正则则把"同一图像的不同增强版本"与无标注数据有机结合,本质上是在无标注数据上构造了一种自训练的信号。预训练和数据增强都能和半监督叠加使用,但它们之间的分工并不重叠。
2. 一致性正则化:半监督里最值得先吃透的思想
2.1 换个问法,答案应该一样
我开始理解一致性正则化时,脑海里浮现的是这么个场景:数学辅导课上,老师把同一道应用题换了两种表述方式问学生,学生给出的最终答案应该是同一个。如果学生一种表述下答对、另一种表述下答错,说明他对这个知识点的理解是有漏洞的。
图像分类也是一回事。给模型看一张猫的照片,水平翻转一下、轻微平移一下、调一点颜色,只要这些变换不改变语义,模型的预测结果应该基本保持一致。如果模型在变换前后输出的类别概率差别很大,说明它对这张图的判断不够鲁棒,还有可以学习的空间。
这就是一致性正则化的核心思想:对无标注图像施加扰动后,模型在不同扰动版本上的预测分布应当尽量一致。在实现上,通常把这种一致性约束写作损失项,叠加在有监督交叉熵损失之上。早期的方法比如Mean Teacher、Π-model用的都是这个思路,后来MixMatch、FixMatch等一批方法,也都是围绕这个核心思想在演化。
2.2 弱增强与强增强的分工逻辑
具体操作中需要区分两种增强:弱增强包括随机水平翻转、小幅平移、缩放这类"温和"的变换,尽量保留原始图像的完整语义信息;强增强是RandAugment这类更激进的变换,比如随机改变颜色饱和度、对比度、亮度、旋转、切割等,使图像在保持类别信息的情况下产生较明显的视觉变化。
这里的分工不是随意的:弱增强版本会被用来生成伪标签,目的是让伪标签尽量可靠;强增强版本则作为"学生输入",逼迫模型在更大的扰动范围内依然保持稳定。换句话说,弱增强负责"提出问题",强增强负责"挑战模型",两相结合,帮助模型学到更本质的特征而不是纹理表面的偶合。
我在实践中最常采用的是FixMatch提出的框架:先对同一张无标注图做弱增强,得到模型的预测概率,当最大置信度超过阈值τ时,将其取argmax作为伪标签;再对同一张图做强增强,用模型对强增强结果的预测与该伪标签计算交叉熵损失。整个过程不需要额外的复杂结构,实现成本很低,但效果却非常好。
2.3 阈值τ:模型自信心的一个门禁
FixMatch里的阈值τ默认是0.95,意思是对无标注样本,只有模型预测置信度达到95%以上才把它当作有效伪标签参与训练。这个门禁设计得非常高,我最初觉得是不是太严了,会浪费大量样本。
实际跑下来才发现,0.95的阈值在大多数分类数据集上都行之有效,原因有两层:第一,如果阈值太低,错误伪标签的比例会快速上升,模型会在错误的监督信号上自我强化,出现所谓"确认偏误";第二,高阈值下虽然单步样本利用率低,但随着训练推进,模型对越来越多无标注样本都能达到高置信度,整体利用率是逐步提高的。
用一句大白话总结:宁可错过一些可能有效的样本,也不要放那些不靠谱的伪标签进来。很多半监督方法后来做的改进,本质上都还是在"如何让伪标签更可靠、如何让可靠性判断更灵活"上下文章,这说明这个门禁确实是整个方案的命门。
3. 伪标签的置信度阈值与动态调整:我踩过的几个坑
3.1 第一个坑:0.8阈值下的"自我欺骗"
最早做实验时,我把τ从0.95降到0.8,本意是想让更多无标注数据参与训练,提升样本利用率。刚开始的十几个epoch,loss下降很快,验证集指标也在涨,我一度以为发现了"更高效"的配置。
但跑到第30个epoch,问题爆发了:测试准确率开始缓慢下降,而且训练loss继续降低、测试loss却反弹,典型过拟合症状。后来分析发现,错误根源不在模型结构,而在伪标签本身——0.8置信度的样本中有不少是易混淆类别,比如"针织衫"和"卫衣"在商品图上非常接近,模型给了一个置信度0.82的错误预测。这批错误标签进入训练后,模型开始系统性地把某些类别拉偏。
所以我的经验教训是:伪标签质量优先于数量,置信度阈值宁可设高,也不要为了利用率牺牲可靠性。对于类别之间高度相似的场景,阈值甚至要提高到0.98以上才安全。
3.2 第二个坑:训练初期的高置信度样本并不总是可信
还有一个容易忽视的问题:训练初期模型的置信度整体偏低,但即便如此,它仍然会对一些样本输出接近1.0的"过度自信"预测。这类高置信度往往不是因为它真的认对了,而是因为模型还没见过足够多样的真实样本,把输入中的一些偶然特征当成了决定性特征。
比如我有一批背景颜色非常统一的产品图,训练初期模型会倾向把"红色背景"和"某个类别"强关联,产生看起来置信度很高、本质却错误的伪标签。如果这时候让这些样本进入半监督loss,错误信号就会在后续训练中持续放大。
解决思路是在训练初期加入一个预热阶段,前几个epoch只使用有标注数据计算有监督损失,不启用无标注损失,让模型的特征表达先达到一个基本可用水平,再逐渐放开无标注信号的权重。我用5到10个epoch的预热期后,稳定性明显提升。
3.3 第三个坑:类别不平衡导致伪标签越来越不平衡
实际项目中,无标注数据的类别分布几乎不可能是均匀的。常见的情形是"头部类别占70%,长尾类别每个占不到1%"。半监督训练在这个场景下天然有马太效应:模型对头部类别置信度高,产生的伪标签多;长尾类别置信度普遍低,伪标签少;于是头部类别越训越强,长尾类别越训越弱。
对此我试过两种有效的调整手段:对无标注损失按类别做加权,统计当前批次伪标签的类别频率,给低频类别的样本更高权重;对置信度阈值做类别自适应,长尾类别的阈值适当降低,头部类别的阈值适当提高,让模型更愿意学习少见类。
这两种方法都涉及"伪标签分布与真实分布的一致性假设",如果完全不知道真实分布,可以先用有标注数据上的类别频率作为先验估计。注意不要做太激进,否则会把噪声放大。
3.4 阈值动态调整:从固定阈值到自适应策略
坦白说,固定阈值0.95在简单数据集上足够好用,但是在长尾数据集或小标注集场景下还是有优化空间。现在很多改进工作都围绕"动态阈值"展开,我实验下来稳定有效的做法是:随着训练推进,逐步降低阈值下限。
具体逻辑是:训练初期模型弱,阈值要高,只接受最可靠的样本;训练中后期模型逐渐强壮,阈值可以逐步放宽,让更多样本进入训练。比如从0.98线性降到0.90,或者按照验证集上的无监督损失曲线决定何时放宽。
这个动态策略不一定在每个数据集上都带来很大提升,但基本不会造成明显退步。我在一个模拟的长尾商品数据集上试过,最终准确率比固定0.95高1.2个百分点,代价是超参数多了一个。如果不追求极限性能,固定阈值已经完全够用。
4. 一个可复现的半监督图像分类实践流程
4.1 数据准备与划分策略
先说数据划分。半监督训练里有一个很容易被忽略的原则:验证集一定要是有标注的、且不能参与任何无监督训练过程。我见过有人把验证集里的图片也顺便拿去给模型做一致性正则,短期看不出问题,实际上会污染验证结果,导致模型选择失真。
以我常用的配置为例,假设总共只有4000张有标注样本、5万张无标注样本。我会这样划分:
| 数据集 | 数量 | 用途 |
|---|---|---|
| 有标注训练集 | 3000 | 计算有监督损失,也参与无标注损失计算 |
| 有标注验证集 | 1000 | 模型选择、早停、超参数调整 |
| 无标注训练集 | 50000 | 仅参与无标注一致性损失计算 |
这里"有标注训练集也参与无标注损失计算"是常见做法,也就是对这部分样本既可以算交叉熵也可以算一致性损失,相当于多了一层正则。
4.2 训练流程与核心代码框架
整个训练流程可以归纳为几个步骤:构造有标签和无标签数据加载器、定义弱增强和强增强变换、计算有监督损失与无监督损失、按比例加权求和、反向传播更新。
我给出一个简化但能直接运行的PyTorch核心框架,方便直观理解训练过程:
# model、optimizer、criterion 省略,重点关注训练逻辑 for epoch in range(epochs): for batch_l, batch_u in zip(labeled_loader, unlabeled_loader): x_l, y_l = batch_l x_u_w, x_u_s = batch_u # 无标注的弱增强/强增强版本 # 有监督部分 pred_l = model(x_l) loss_sup = criterion(pred_l, y_l) # 用弱增强版本生成伪标签 with torch.no_grad(): pred_u_w = model(x_u_w) prob, pseudo_label = pred_u_w.max(dim=1) mask = prob.ge(threshold).float() # 置信度门控 # 无监督部分:强增强版本与伪标签计算损失 pred_u_s = model(x_u_s) loss_unsup = criterion(pred_u_s, pseudo_label, reduction='none') loss_unsup = (loss_unsup * mask).mean() loss = loss_sup + lambda_u * loss_unsup optimizer.zero_grad() loss.backward() optimizer.step()这个框架虽然简单,但已经是FixMatch的核心结构。实际的代码还需要处理分布式采样、批次大小差异、EMA模型等一系列细节。如果只是验证思路,上面的代码已经足够跑起来看趋势。
4.3 关键超参数与优化细节
下面几个超参数是半监督训练里影响最大的,我列出实践中的常用配置和选择理由:
| 超参数 | 常用值 | 理由 |
|---|---|---|
| 置信度阈值τ | 0.95 | 控制伪标签质量,太低会引入噪声 |
| 无监督损失权重λ_u | 1.0 | 与有监督损失同量级,多数任务上比较稳 |
| 无标注batch比例μ | 5或7 | 每个有标注batch对应5到7倍无标注样本 |
| 优化器 | SGD + Momentum 0.9 | 半监督中SGD普遍优于Adam,收敛更稳 |
| 权重衰减 | 5e-4 | 防止小样本下过拟合 |
| 学习率调度 | Cosine | 前期不要太大,后期平滑下降 |
关于优化器的选择,我在多个实验里对比过:Adam训练前期收敛快,但后期容易在无标注样本上出现过拟合式的震荡;SGD配合动量虽然收敛慢一些,但最终泛化更稳定。如果数据集规模特别小,可以用Adam做短训练快速验证,正式跑长训练还是建议SGD。
4.4 我的实验过程与指标变化
拿一个模拟项目举例:有标注3000张、无标注50000张、12个类别,ResNet-50作为骨干网络。实验设置三组对照:全监督3000张、半监督3000加5万、全监督5万张。
训练过程中的典型变化是:前半段,前20个epoch,半监督模型的准确率和全监督基本持平,甚至略低,因为无标注损失还在"拖后腿";从中段开始,半监督逐步超过全监督,到80个epoch时拉开明显差距;到训练后期,差距继续扩大但增速变缓。
最终测试集结果大致如下:
- 全监督3000张:82.1%
- 半监督3000加5万:89.5%
- 全监督5万张:93.0%
这个结果说明两件事:第一,无标注数据确实能弥补标注数据的不足,近8个百分点的提升非常可观;第二,半监督的上限仍然低于全量有监督,它解决的是成本问题而不是魔法,期望值要合理。如果标注预算允许,优先还是补标注;预算受限时,半监督是最值得投入的方向。
5. 从实验到工程化:半监督落地时必须想清楚的几件事
5.1 训练时长和算力预算要按3到5倍准备
半监督学习一个很现实的成本是训练时间。我最初以为多加5万张无标注图只是多了一些数据,没想到训练时长是纯有监督的好几倍。原因在于:无标注数据量通常远大于有标注数据量,而且每个epoch都要做两组增强计算,前向传播的压力天然翻倍。
在我自己参与的项目里,一个纯有监督ResNet-50跑150个epoch,单卡V100大约需要10小时;加上5万张无标注图后,同样条件下跑150个epoch,大约要25到35小时。如果使用更强的增强策略或者更大的无标注batch比例,时间还会往上走。
所以立项阶段就要把算力预算按纯有监督的3到5倍预估,否则容易在训练中途遇到"时间不够只能砍epoch"的窘境。另外,多卡并行时的数据采样也要注意,尽量保证每个设备上的无标注数据分布接近,否则BatchNorm统计量会出问题。
5.2 伪标签服务化:不是训练时算一次就完事
很多初学者以为伪标签只在训练循环里生成一次,训练完就扔了。但实际上在真实业务里,模型上线后每天都会有新图片进来,无标注池是动态的。如果我们希望把半监督的收益持续积累,就需要一个伪标签服务,定期对新增无标注图生成伪标签,并筛选高置信度样本补充到训练集里。
我在某项目中就是这么做的:每天离线任务扫描当天的无标注图,得到置信度大于0.95的伪标签,进入候选池;当候选池积累到一定数量后,触发一次增量训练;增量训练后的新模型先在验证集上跑一遍,如果没有下降再发布。
这套流程本质上把半监督训练变成了一种"自动数据飞轮"。它带来的收益不是一次性提升,而是让模型能持续吸收新数据中的分布变化,对线上图片风格漂移的鲁棒性也更好。但代价是工程复杂度上了一个台阶,需要专门处理版本管理、样本去重、标签冲突等问题。
5.3 无标注数据的质量清洗不能省
半监督对无标注数据的质量同样有要求。如果无标注池里混入大量与任务无关的图片,哪怕它们本身是清晰的,模型也可能学到无关的特征,拉低分类效果。
我做过一个测试:在无标注池中混入20%完全不相关的场景图,半监督最终准确率比干净无标注池低了近4个百分点。后来我在数据进入半监督训练前,增加了一道简单的前置过滤:先用现有的有监督模型对无标注图做一次粗筛,剔除那些置信度极低的"离群样本"。这个操作成本很低,但对稳定性的提升非常明显。
需要注意的是,粗筛不要做得太狠,否则会把长尾类别里的困难样本也一起过滤掉,导致类别失衡加剧。过滤的目标是删掉"根本不属于当前任务范畴"的图像,而不是删掉"模型暂时认不出的样本"。
5.4 模型更新与灰度发布的节奏
半监督模型训练完成后,最常见的问题是如何安全上线。分类模型一旦误判,在业务端可能直接造成客诉,所以我的习惯是先走灰度发布:把新模型和旧模型同时部署,对相同的线上请求做预测,实时比对分歧样本,人工抽检后再决定是否全量切换。
这是因为半监督模型在某些"伪标签污染"的类别上可能表现得很差,但平均指标看起来还可以。只盯着总准确率做决策,容易漏掉局部风险。我在具体操作中还会留一个"可回滚"的口子:灰度期保留旧模型一段时间,一旦发现问题可以快速切回,不至于影响线上稳定性。
最后再分享一个我调整心态上的小变化:以前我总觉得"只有标注数据才是数据",现在我会把无标注数据看成更基础的资产。半监督学习的价值不在于取代标注,而在于把标注预算花在刀刃上——用一小部分高质量的标注,撬动大规模无标注数据中的结构信息。如果你手头正好有一批分类图片,标注成本又卡得紧,很值得先用FixMatch这套流程跑一版基线。哪怕最终只提升了两三个点,省下的标注预算也足够覆盖几个季度的迭代成本了。