半监督食物图像分类实战:伪标签、FixMatch与动态阈值调优全解析

发布时间:2026/10/22 14:36:58
半监督食物图像分类实战:伪标签、FixMatch与动态阈值调优全解析 先说个真实感受半监督学习这东西理论看了好几遍都觉得自己懂了真到自己动手做一个半监督食物图像分类项目时才意识到坑全在细节里。尤其是食物图像这种类内差异大、类间又容易混淆的数据用半监督去搞稍不注意就会陷入伪标签越滚越脏、模型越训越歪的循环。这篇文章我把整个项目从数据准备、方案选型、训练循环到排错实录完整拆开讲适合已经跑通基础图像分类、想尝试半监督方向但还没系统性入手的开发者参考。1. 需求复盘为什么食物图像分类偏要走半监督这条路1.1 先说我最初的真实需求项目起始点其实很简单手头有一批食物图像覆盖了大概40个常见类别包括米饭类、面食类、菜品类和水果类。数量并不少累计有将近3万张图但实际标注情况非常尴尬——只有大约4000张有干净的人工标签剩下26000多张都只有模糊的所属分组信息甚至有些组的边界本身就是错的。当时我面临两个选择第一把26000多张全部重新人工标注按照每张图平均需要3到5秒的标注时间、再加上质量复核这至少要两到三周的工作量第二尝试用半监督的方式让模型在少量标注数据之上利用大量无标注数据来提升效果。我选了后者。原因是这4000张标注数据虽然少但基本覆盖了全部40个类别每类都有接近100张的样本。这个基础决定了半监督学习里的伪标签生成、一致性正则这些技术有机会生效因为模型至少能先在每个类上学到一个不算离谱的初始特征分布。1.2 食物图像分类和通用图像分类的重要差异如果拿食物图像分类和CIFAR、ImageNet这类通用图像分类对比最明显的差异是类内差异极大且类间共享大量视觉特征。同样的“红烧肉”不同餐馆做的色泽、摆盘、光线完全不同而“糖醋里脊”和“锅包肉”在很多角度下对模型来说就是同一类炸肉块配酱汁。这种模糊边界会让全监督模型都容易犯错在半监督场景下问题更严重——一个错误的伪标签一旦以高置信度被选入训练集模型会把它当成真理继续强化。所以在数据策略、增强方案、伪标签阈值设计上都不能直接套用通用图像分类的方案必须针对食物图像的特性做调整。这也是我在项目里踩坑最深的地方。1.3 一句话定义这个项目的目标这个项目的目标可以概括成仅使用4000张人工标注食物图像配合26000余张无标签图像通过半监督学习训练出一个在40类食物分类任务上不输于全量标注监督学习方案的模型。这里的“不输”我用一个更具体的指标来衡量——在独立测试集上半监督模型的Top-1准确率至少要超过只用4000张标注数据训练出的全监督基线5个百分点以上同时逼近使用全量3万张标注训练的模型约2个百分点以内。这个目标不算激进但也不轻松因为半监督提升并不是线性的尤其当无标签数据的分布和标注数据分布存在偏差时效果可能远低于理论预期。2. 数据准备阶段标签体系、去重和分布摸底比模型更重要2.1 标签体系的重建从粗糙分组到干净标注项目第一步不是找模型而是重新整理那4000张已标注数据。因为原始的人工标签是在不同时间、由不同人标注的类别名称本身就存在同物异名的问题比如“番茄炒蛋”和“西红柿鸡蛋”其实是同一道菜“白米饭”和“米饭”也被分成了两个组。我先做了一层映射把同类目不同叫法合并成一个标准名称最终确定40个类别。这一步看似和半监督无关但直接影响后面伪标签的置信度计算——如果类别定义本身混乱模型输出的类别概率分布也会混乱阈值筛选就会失真。提示在半监督食物图像分类项目里宁可多花两天整理标签体系也不要急着训练模型。伪标签方案对标签噪声极其敏感初始标注的干净程度决定了整个训练过程的信噪比。2.2 无标签数据的去除和去重先减肥再训练26000张无标签图像听起来很多但实际质量参差不齐。我在初筛时做了三件事模糊去重、低分辨率过滤、纯背景或文字图像过滤。模糊去重用的是感知哈希算法把每张图缩放到8x8灰度图后计算哈希值汉明距离小于5的当成重复图像处理。这个操作去掉了接近3000张高度重复的图片比如同一个盘子不同角度的连拍、同一道菜在相同背景下拍的十几张照片。低分辨率过滤更简单把所有短边小于200像素的图直接剔除因为这种图即便模型学出了特征在测试阶段也很难泛化。文字图像过滤主要针对一些从网页上抓下来的包含大面积标题文字的食物图这类图像会让模型学到“文字纹理”而不是“食物纹理”在部署时会变成非常隐蔽的偏差来源。筛选后无标签数据稳定在约22000张质量上比原始数据干净很多。2.3 标注集划分为三个子集训练、验证、测试的隔离原则4000张标注数据不能全部拿去当训练集。我的划分方案是子集数量用途有标签训练集3200张监督损失来源用于模型训练验证集400张用于阈值调整、模型选择、早停判断测试集400张最终效果评估全程不参与任何决策验证集和测试集的隔离非常重要尤其是使用半监督算法时伪标签的置信度阈值、协同训练的轮次、增强强度等超参数都要在验证集上调不能让测试集信息渗入超参数选择。很多半监督项目效果虚高就是因为验证集和测试集没有严格隔离超参数实际上偷看了测试集。还有一个容易忽略的细节划分时要按类别分层抽样确保每个类别在训练、验证、测试三部分中的比例一致。否则有些类别可能在验证集里只有一两张图评估指标会有非常大的随机波动导致过早停止或错误选型。2.4 食物图像专属的数据增强策略数据增强在半监督食物图像分类里扮演的特殊角色比全监督场景重要得多。因为一致性正则的核心假设是一张图经过不同增强后模型对它的预测应该保持一致。增强策略越合理这个约束就越有效增强策略越盲目模型学到的可能是对颜色、旋转这些无关维度的不变性而对食物本身的类别特征不够敏感。我用的增强分成两支弱增强分支Resize到256x256后随机裁剪到224x224、随机水平翻转。这是标准的ImageNet风格增强修改幅度很小保证模型看到的图像和真实分布接近。强增强分支在弱增强基础上增加了随机颜色扰动、高斯模糊、随机擦除Random Erasing以及轻微的仿射变换。随机擦除对食物图和通用物体图有不同的效果比如擦除饭碗中央的饭粒区域模型会被迫依赖碗沿、桌面纹理等环境特征来判断。这对泛化提升有一定作用但也要注意如果擦除区域过大可能把关键判别特征完全抹掉导致伪标签变成纯随机猜测。注意强增强的强度参数需要专门针对食物图像调优不能直接使用开源项目默认值。我在实验中发现默认的颜色扰动幅度对食物图来说偏大因为食物本身色彩饱和度较高稍微调一下色相就容易让一道菜从“红烧”变成“酱烧”类别感直接错乱。3. 方案选型几种主流半监督方法的对比与取舍3.1 伪标签、一致性正则、以及它们的历史问题半监督图像分类的方法论大体上有几条路线早期的伪标签法Pseudo-Labeling直接用模型自己的高置信度预测当作标签回填训练后来的Π-Model、Temporal Ensembling引入了一致性正则的概念强调同一输入在不同扰动下输出稳定再后来MixMatch、FixMatch、FlexMatch、FreeMatch这些方法把伪标签和一致性正则结合在一起形成了现在的主流范式。伪标签法的核心矛盾在于模型尚未成熟时它的高置信度预测往往集中在一小部分类别上长期回填会让某些类别样本越积越多、其他类别越学越弱最终出现类别塌缩。一致性正则方法的优势在于不直接依赖预测置信度而是约束同一图像不同视角下的输出一致性相对温和一些但如果模型很快对某种错误的特征组合达成一致这个约束会反过来巩固错误。3.2 我的选择FixMatch变体加动态阈值调整这轮项目里我最终选择了FixMatch作为基础框架。FixMatch的逻辑很清晰对无标签样本分别做弱增强和强增强弱增强分支的输出经过argmax得到伪标签同时记录对应的最大预测概率只有当最大预测概率超过固定阈值默认0.95时才把伪标签和强增强分支的预测结果放进一致性损失里计算。这个方案训练成本低、实现简洁实测效果通常也不错。但FixMatch的固定阈值是它最大的隐患——训练初期模型整体置信度低0.95的阈值会让有效伪标签数量极少训练后期某些容易识别的类别大量越过阈值而难识别类别始终无法贡献伪标签形成类别间的不平衡。因此我在FixMatch基础上引入了阈值自动调整机制不再使用固定0.95而是让每个类别的阈值随训练动态变化。具体来说我会用一个滑动窗口统计每个类别近几个epoch的高置信度样本数量如果某类别伪标签过少就将它的阈值降低如果某类别伪标签泛滥则适当提高阈值。这个思路和FlexMatch类似但实现更轻量更适合快速迭代。3.3 模型骨架的选择ResNet50还是EfficientNet模型骨架方面我在ResNet50和EfficientNet-B3之间做了对比。ResNet50的优势是稳定、训练技巧成熟、对超参数不敏感适合项目初期的快速验证EfficientNet-B3的理论FLOPS更低在图像分类榜单上表现更好但对训练策略和增强参数更敏感在没有充分调参的情况下效果可能反而不如ResNet50。我的建议是在半监督项目中优先使用自己熟悉、可靠度更高的骨架而不是追求新结构。半监督的训练信号已经比全监督弱如果骨架本身对增强策略过于敏感很难判断性能下降到底是半监督机制的问题还是骨架的问题。最终我选定了ResNet50预训练权重使用ImageNet上的版本在实验阶段有效减少了训练初期的冷启动问题。4. 训练循环实现从数据加载到损失拼接的完整代码4.1 数据加载器设计一个batch里混合有标签和无标签数据训练时每个batch由两部分组成一部分是有标签数据正常计算交叉熵损失另一部分是无标签数据弱增强和强增强各生成一份计算一致性损失。数据加载器我用PyTorch实现两个Dataset分别走不同的transform再用自定义collate拼到一起。class SemiDataLoader: def __init__(self, labeled_dataset, unlabeled_dataset, batch_size): self.labeled_loader DataLoader( labeled_dataset, batch_sizebatch_size // 2, shuffleTrue, num_workers4, drop_lastTrue ) self.unlabeled_loader DataLoader( unlabeled_dataset, batch_sizebatch_size // 2, shuffleTrue, num_workers4, drop_lastTrue ) def __iter__(self): for (img_l, label), (img_w, img_s) in zip(self.labeled_loader, self.unlabeled_loader): yield { labeled: (img_l, label), unlabeled: (img_w, img_s), }有标签数据的transform只包含随机裁剪和翻转不做强增强避免破坏有限的干净信号无标签数据的弱增强和强增强则按前面描述的两套策略配置。4.2 损失函数拼接监督损失与半监督损失的比例总损失可以写成[ L L_{sup} \lambda_{u} \cdot L_{unsup} ]其中监督部分是标准交叉熵无监督部分也是交叉熵但输入是伪标签和强增强分支的预测概率class SemiLoss(torch.nn.Module): def forward(self, logits_l, labels_l, logits_w, logits_s, threshold, temperature1.0): loss_l F.cross_entropy(logits_l, labels_l) prob_w F.softmax(logits_w / temperature, dim1) max_prob, pseudo_label prob_w.max(dim1) mask (max_prob threshold).float() loss_u F.cross_entropy(logits_s, pseudo_label, reductionnone) loss_u (loss_u * mask).mean() return loss_l self.lambda_u * loss_u无监督损失只在mask为1的位置上计算也就是说只有当弱增强分支对无标签样本的预测置信度超过阈值时这个样本的强增强预测结果才会被拉向伪标签。这个设计本质上是在给无标签样本做课程学习——模型先学简单的、置信度高的再逐步学习难的。4.3 类别的动态阈值实现细节动态阈值模块我单独写成一个类每个类别维护两个统计量当前阈值和近期过线样本数。每个epoch结束后根据近期统计量做一次更新。class DynamicThreshold: def __init__(self, num_classes, init_threshold0.9, min_threshold0.8): self.num_classes num_classes self.thresholds torch.full((num_classes,), init_threshold) self.min_threshold min_threshold self.class_hit_count torch.zeros(num_classes) self.class_total_count torch.zeros(num_classes) def collect(self, probs, pseudo_labels, mask): max_probs, preds probs.max(dim1) for cls in range(self.num_classes): cls_mask (pseudo_labels cls) hit_mask (max_probs self.thresholds[cls]) cls_mask self.class_hit_count[cls] hit_mask.sum().item() self.class_total_count[cls] cls_mask.sum().item() def update(self): for cls in range(self.num_classes): if self.class_total_count[cls] 10: # 样本太少阈值保持不动不冒险 continue hit_ratio self.class_hit_count[cls] / self.class_total_count[cls] if hit_ratio 0.05: self.thresholds[cls] max(self.thresholds[cls] - 0.05, self.min_threshold) elif hit_ratio 0.3: self.thresholds[cls] self.thresholds[cls] 0.05阈值调整的核心逻辑如果一个类别的伪标签命中率过低就降低阈值给这个类别更多出伪标签的机会如果一个类别的命中率过高就提高阈值防止模型在这个类别上过度自信。4.4 优化器、学习率和训练轮次设置优化器使用SGD动量0.9权重衰减5e-4初始学习率0.03配合Cosine Annealing学习率衰减。batch size设64其中32张有标签、32张无标签一共训练120个epoch。关于学习率有个细节要提半监督训练里学习率过大时有标签数据的监督损失会迅速收敛但无标签数据的损失可能会出现来回震荡原因是伪标签变化幅度跟不上模型参数更新幅度学习率过小又会让无标签数据的贡献变得非常微弱。0.03配合Cosine Annealing在ResNet50上是我实测比较稳的组合。5. 训练实录三个隐蔽的坑与完整排查链路5.1 类别塌缩出现时模型表现出的反常特征第一个坑是类别塌缩。训练到大约第30个epoch时我在验证集上发现整体准确率还不错但查看分类别准确率时发现一个反常现象——“蔬菜沙拉”这个类别的准确率从78%骤降到23%而“水果拼盘”的准确率从82%涨到了91%。查了日志之后发现这两类在无标签数据里的视觉特征高度相似模型最开始把不少“蔬菜沙拉”误判为“水果拼盘”由于误判置信度很高、过了阈值这些错误标签就被当成高质量伪标签回填进训练集。越往后模型越倾向于把所有颜色鲜艳、摆盘整齐的混合食物一律判定为“水果拼盘”。排查链路是这样的先确认伪标签分布把选中的无标签样本按伪标签类别统计数量发现“水果拼盘”的伪标签数量在5个epoch内翻了4倍而“蔬菜沙拉”几乎没有新伪标签然后人工抽检了部分高置信度的“水果拼盘”伪标签图像发现里面确实混入了大量沙拉图。修复方案是把动态阈值模块的初始阈值从0.95调更低到0.9并增加了对称性约束同一批次中类别伪标签数量占比低于整体1%的类别强制把阈值降低0.1。这样一来“蔬菜沙拉”这类冷门类别也能快速出伪标签模型对它的学习信号不再断供。5.2 强增强过猛导致的伪标签无效化第二个坑来自强增强本身。有一段时间训练loss降得很漂亮但测试集准确率停滞在基线附近几乎没有提升空间。排查时我先怀疑模型或者超参数但逐一对照之后发现训练损失下降的主要来源是监督损失无监督损失几乎没在变化。然后我做了更进一步的诊断把无标签batch的强增强图像单独拿出来喂给模型看预测结果和弱增强分支的一致率发现一致率只有41%远低于正常水平。这说明问题不在监督信号而在于强增强已经破坏了太多原始图像信息模型从强增强图里提取不到稳定的类别特征自然也没法为伪标签提供有效梯度。定位到问题之后就做了一组小实验分别把随机擦除概率从0.5降到0.2、高斯模糊半径从3降到1.5、颜色扰动强度从原来的默认值乘了0.5单独控制变量跑30个epoch。最终把擦除概率设为0.2、模糊半径设为1.5颜色扰动减半无标签分支的一致率回到了77%以上。5.3 无标签数据分布偏移导致的伪标签污染第三个坑比较隐蔽无标签数据中有大量图像是在餐厅环境下拍摄的强光、暖色调、蒸汽非常普遍而有标签数据主要是厨房或白底环境下拍摄的。模型在有标签数据上学到的特征对餐厅环境很陌生但它又在餐厅图像上强行生成高置信度的伪标签。这些伪标签本身可能是对的但特征分布差距大模型一直接在一种独特的光线纹理上拟合无法泛化到测试集。排查时我画了特征分布散点图用的是倒数第二层特征做t-SNE降维发现无标签数据的特征点和有标签数据明显分成了两个簇。这提示数据分布不一致而不是模型学习能力问题。修复方式不是换模型而是做数据层面的匹配把有标签数据中也加入少量餐厅场景图像从无标签数据里人工抽了400张补标注进去让源域分布向目标域靠拢同时在训练初期冻结了骨干网络的前几层只训练高层分类层降低了模型对环境纹理的依赖。6. 效果复盘与评估细节怎么看半监督带来的真实增益6.1 三条对照路线的最终数据项目最终按三条路线做了对比路线训练数据Top-1准确率全监督基线3200张有标签68.4%半监督方案3200张有标签 22000张无标签76.8%全标注上限25000张全标注参考79.1%这个结果基本达到了项目目标半监督相对全监督基线提升了8.4个百分点距全标注上限模型只差2.3个百分点。考虑到标注成本差了6倍以上这个tradeoff是划算的。6.2 除了Top-1准确率之外我还看了什么只看Top-1准确率很容易被误导。我额外做了两个维度的评估第一是逐类别准确率矩阵观察哪些类别在引入半监督后反而下降了结果显示“拔丝地瓜”和“糖醋里脊”这两个高难度类别下降了约4个百分点原因和5.1里提到的类别塌缩机制类似这类样本在无标签集中的视觉分布太过极端导致伪标签始终集中在少数几种形态上。第二是置信度校准曲线。半监督模型普遍存在的风险是过度自信尤其是在伪标签大量覆盖的类别上。我用验证集画了可靠性图发现半监督模型在高置信区间0.95以上的实际正确率只有82%而全监督模型在同区间的正确率是86%。这说明固定阈值不能代表真实置信度如果后续要做主动学习或者人机协同审核必须重新校准。6.3 部署时的两点额外提醒我把模型最终导出为ONNX格式部署到服务端。这里有两个额外提醒要给到同样做半监督食物分类项目的同学。第一半监督模型在训练时使用的图像预处理参数归一化均值、标准差、裁剪尺寸必须和部署环境完全一致否则很小的像素分布偏移也会被模型当作特征变化预测结果会整体漂移。这类问题在全监督模型上也有但半监督模型的漂移会更明显因为它本身就是在增强视图和原始视图间学习一致性约束的。第二如果上线后持续收集到新的无标签数据不要直接把它们加进半监督训练集重新训练。新的无标签数据分布如果发生偏移、而你又没有足够的新有标签数据来校准模型会被新数据里的噪声带偏一种更安全的方式是先在少量新有标签数据上做验证确认特征分布没有显著变化后再增量更新。7. 写在最后的个人体会做完这个半监督食物图像分类项目我最大感受是半监督学习的上限取决于你对数据分布的理解而不只是算法本身的强弱。FixMatch系方法把问题形式化得很好但真正决定8个点还是3个点提升的往往是你有没有发现类别塌缩、有没有调整好强增强的强度、有没有识别出无标签数据和目标域之间的分布偏移。另一个实际的体会是半监督带来的效果增益并不均匀。简单类别上模型几乎不需要无标签数据就能学得很好真正吃红利的是那些样本形态丰富、单张有标签图不足以覆盖类内多样性的类别。所以如果要做类似项目建议先按类别统计一下有标签样本的数量分布把那些样本少但视觉形态多的类别标记出来后续调阈值时直接往这些类别倾斜。最后再分享一个团队项目里可以用上的小技巧把动态阈值的调整记录、伪标签命中率、每类伪标签数量这些日志全部可视化。表面看这只是排查工具但实际训练时它能让你在模型完全跑偏之前就发现异常省掉好几个白跑的版本。毕竟在半监督训练里模型自己觉得自己学得很好和它真的学得很好是两件可能完全不相关的事情。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询