ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

半监督食物分类实战:从伪标签到细粒度图像识别的完整方案

半监督食物分类实战:从伪标签到细粒度图像识别的完整方案

半监督食物分类系统,听起来像是一门课的标题,但它解决的其实是特别接地气的问题:你有大量没标注的图片,却只有寥寥几张人工标签,怎么把分类模型训练出来?我自己手里有个美食内容整理项目,在某社区后台导出了六万多张菜品照片,需求不复杂——按菜系和食材自动分到四十多个类别里。可六万张图里只有不到两千张带规范标签,剩下的大多数要么没有标签,要么同一个菜被叫成七八种名字。真要按传统思路找人一张张标,成本高不说,光时间就要两三个月。恰好那段时间在跟一套深度学习系列课程,第五节课正好讲半监督学习,听完一拍大腿:这就是给食物分类这种场景准备的。这篇文章是那节课的学习笔记加我自己的实践心得,上半篇先讲清楚半监督食物分类系统的方法骨架、数据准备和模型选型,下篇再展开训练细节和部署。如果你也正被“标注太少、图片一大堆”卡住,这篇文章值得看完。

1. 为什么食物分类这种场景必须上半监督

半监督不是万灵药,它有自己的适用前提:有标签数据少但还算可靠,无标签数据多且与任务分布相关,类别边界不能完全模糊。食物分类恰好把这几条全占了,而且占得比很多通用分类场景更极端。

1.1 食物图片标注,比想象中麻烦得多

常规图像分类的标注,比如区分“猫”“狗”“汽车”,普通标注员扫一眼就能完成。食物不一样,它属于典型的细粒度分类问题。同样是面食,炸酱面和牛肉面的差别体现在酱料颜色、配菜种类、面汤清澈度这些非常细微的地方;同样是鸡肉,宫保鸡丁和辣子鸡丁如果不看花生米和干辣椒的分布,连专业厨师都可能认错。我一开始把类别体系定为四十类,实际试标了一遍才发现,规则必须写得非常细,比如“如果画面里同时出现米饭和菜,按主菜计入”“泡面算不算面食”这种边角问题多到崩溃。在这种情况下,让标注团队按高质量标准去标完几万张图,不仅单价高,质量波动也很大。

标注成本高还不是最麻烦的。食物图像的标准本身带有很强的地域和文化属性,同一道菜在不同地区做法不一样,外观差异也大。比如“鱼香肉丝”在川菜馆和家常菜馆,粗看根本是两道菜。标注员如果不熟悉这个菜品体系,很容易给出错误的标签。这样的错误标签一旦混进训练集,比没有标签还糟糕。我当时找了三个人试标,一致性只有八成出头,很多争议样本最后只能靠我逐张复核,效率极低。

1.2 无标签数据很多,但有标签数据很贵

另一个现实是,无标签的菜品图片在当下的互联网环境里几乎取之不尽。外卖平台的商品图、美食分享社区的用户晒图、社交平台上的探店照片,每张都是现成的训练素材。但这些数据有一个共同特点:分布很杂,质量参差不齐,可能包含外卖盒、餐桌、手指、水印等各种干扰信息。如果硬着头皮全量标注,除了成本爆炸,还得处理大量“根本不是一张干净菜品图”的样本,性价比极低。

这就形成了半监督学习最典型的场景:有标签数据是“小而精”的,无标签数据是“大而脏”的。半监督的思路,就是让模型先从少量干净标签里学会一类任务的基本判断,再利用大量无标签数据稳定自身的特征表达,不断扩大对数据分布的覆盖。对食物这种外观变化多、类别形态丰富的任务来说,无标签数据恰恰是最廉价的“增广素材库”。

1.3 细粒度分类天然依赖“见过足够多真实样本”

还有一点是细粒度分类本身的特性决定的。像“猫狗分类”这种粗粒度任务,几千张有标签图就能让模型学得不错,因为每类的类内变化相对有限。但食物分类里,同一个“西红柿炒蛋”,不同人做的版本,鸡蛋的嫩老程度、西红柿的出汁量、是否加葱花、盛在什么盘子里,都会显著影响图像特征。如果每类只有三四十张标签图,模型根本没法覆盖这种类内多样性,很容易把“摆盘方式”当成“菜品类别”来学。

半监督的价值就在这里:大量无标签样本虽然没人给它们打标签,但它们覆盖了同一个菜品在不同角度、不同光线、不同食材配比、不同摆盘下的真实分布。模型通过伪标签或一致性约束反复“看”这些样本,相当于在没有人工标注的情况下,把类内特征的覆盖面撑大了好几倍。这是我实践下来最直观的感受:没有大量无标签数据做支撑,细粒度分类的小标签模型很快就到瓶颈,加了伪标签之后,模型对同菜不同形态的鲁棒性明显提升。

2. 半监督在食物图像上的三条主线:伪标签、一致性正则与混合增强

半监督学习的方法很多,但抛开各种噱头,核心思路基本可以归成三条线:伪标签、一致性正则,以及把前两者结合起来的混合增强框架。食物图像场景下,三条线各有自己的适用半径,我一个个说。

2.1 伪标签路线:让模型先自评,再自证

伪标签是最直觉的半监督做法,流程一句话就能说完:先用有标签数据训练一个基础模型,然后让这个模型预测无标签数据的类别,把预测置信度高的样本直接当成有标签数据加入训练集,重新训练,迭代若干轮。它的底层依据是聚类假设:在特征空间里,同一簇内的样本大概率属于同一类别,模型对某个无标签样本给出高置信度预测,说明它距离某个类中心很近,判断可信度较高。

食物图像用伪标签有一个天然优势:很多菜品的外观特征非常鲜明。比如红烧类的红亮酱色、油炸类的金黄质感、清蒸类的半透明汤汁,这些视觉特征在特征空间里聚得比较紧,模型对它们的高置信度预测确实比较可靠。我自己实验下来,第一批伪标签里准确率能达到九成以上。

但伪标签最大的坑叫确认偏差。模型在早期可能对某些样本判断错误,如果这个错误预测被当成“正确答案”写进训练集,模型会在下一轮训练里进一步强化这个错误方向,越错越固执。这在细粒度分类里尤其致命:红烧肉和红烧排骨,如果模型一开始把某张红烧排骨图错标成红烧肉,后续它可能把更多偏瘦的红烧类样本统统归到红烧肉这个类里,两个类之间的边界直接被污染。缓解办法我后面专门讲,核心就一句话:阈值宁高勿低,迭代宁慢勿快。

2.2 一致性正则路线:预测稳定比预测正确更早出现

一致性正则是另一条完全不同的思路,它不要求模型猜出无标签样本的准确标签,而是要求模型的预测对输入扰动保持稳定。通俗地说,同一张食物图,水平翻转一下、轻微裁剪一点、饱和度稍作调整,模型输出的概率分布不应该发生剧烈变化。如果模型能做到这一点,说明它学到的是菜品本身的稳定特征,而不是对某张具体图像的过拟合。

这个思路利用的是平滑性假设:真实的决策边界应该落在数据分布的低密度区域,而不会把两个距离很近的真实样本硬生生劈开。对食物图像来说,这个假设大体成立——同一个菜品在不同拍摄条件下的变化是连续的,模型的输出也应该连续变化。实践上,一致性正则一般通过额外的一致性损失来实现,比如对弱增强样本和强增强样本的预测结果计算均方误差或KL散度,让两者不断靠近。

我自己的感受是,一致性正则单独用在小规模食物数据上收敛偏慢,训练过程也不太直观,因为它不像伪标签那样能直接看到“模型学会了多少新样本”。但它有一个伪标签比不了的好处:对尾部冷门类别特别友好。冷门类别的有标签样本太少,伪标签根本筛不出几个高置信度样本,而一致性正则可以靠着无标签样本之间的互相约束,把这类别的决策边界慢慢撑开。所以后面我实际采用的是混合方案,而不是单押某一条线。

2.3 混合增强思路:把前两条合起来的FixMatch

FixMatch这类方法之所以在近年成为半监督的主流,就是因为它用极简洁的方式把两条线缝在了一起。具体做法分三步:对无标签样本做弱增强,得到一个预测;如果预测置信度超过阈值,就用模型对这个类别的高置信度判断作为伪标签;然后取该样本的强增强版本,用强增强版本的预测结果和伪标签算监督损失。弱增强通常指随机水平翻转、小范围平移这类轻微变换,强增强则用RandAugment之类的大扰动。

这套设计的精妙之处在于:伪标签负责“挑选值得学习的样本”,强增强负责“迫使模型从被破坏的图像里还原出关键特征”。弱增强生成的伪标签置信度更高,强增强参与训练的样本对模型能力要求更高,两者互补。对食物图像来讲,唯一要注意的是强增强的幅度必须受控。食物图的语义很大程度建立在颜色和形状上,红烧肉的酱色、糖醋里脊的挂糊形态,一旦被过度扭曲,模型就会开始学习“颜色块统计”而不是“菜品特征”。

下面这个表是我整理的三条线在食物分类场景下的对比,方便快速选型:

方法线信号来源核心假设食物场景优势主要风险
伪标签模型自评的高置信度预测聚类假设外观鲜明类易获得可靠伪标签确认偏差导致错误累积
一致性正则无标签样本的扰动不变性平滑性假设对冷门类别的边界撑开有帮助单独用收敛慢、调参繁琐
FixMatch式混合弱增强伪标签 + 强增强训练两条假设叠加综合稳定性和样本扩展能力强增强幅度控制难度高

3. 数据集准备与模型选择的几个关键决定

半监督模型的表现上限,很大程度在进入模型之前就已经被决定了。模型只负责在给定数据分布里优化目标,如果你喂给它的类别定义是模糊的、数据是大量重复的、预训练特征是错位的,再好的半监督算法也救不回来。

3.1 类别体系设计:先定什么是“一个类”

食物分类系统失败的首要原因,往往不是模型不行,而是类别定义不清晰。我在项目最开始的阶段,直接按菜名分了五十多个类,结果“酸辣土豆丝”和“醋溜土豆丝”在视觉上几乎无法区分,而“大盘鸡”和“黄焖鸡”反而挺好区分。这种粒度不一的分类体系,会让半监督的置信度阈值完全没法统一设置:有些类模型轻松打出0.99的概率,有些类模型永远在0.8徘徊。

后来我重新设计了类别体系,采用了“主食材+烹饪方式”的二级结构。顶层先分畜肉、禽肉、水产、蔬菜、主食、汤羹等大类,每个大类下再细化具体菜品。这一改,类间差异变大了,伪标签的可靠性明显上升。这个教训说明一个道理:半监督的类别体系必须优先考虑“视觉可区分性”,而不是文本语义上的精确性。你可以在检索产品里用更细的菜名标签,但训练系统的分类粒度一定要落在模型视觉上“跳得开”的地方。

3.2 清洗与去重:半监督最怕自欺欺人

无标签数据不是越多越好,这是我在第一轮实验里被现实教育出来的。当时觉得既然半监督能利用无标签数据,那就把导出的六万张图全部扔进去,结果训练一轮下来,验证集准确率反而比只用有标签数据还低了一点。排查之后发现问题出在重复上:很多用户图片是同一道菜连拍了好几张,或者同一个博主在不同文章里发过相似度极高的摆盘图。感知哈希去重一跑,六万张图直接去掉了一万五千张。

重复图在半监督里危害很大,因为它们会让伪标签在训练集里反复出现,模型相当于翻来覆去读同一页书,还误以为读了很多不同的知识点。除重之外,我还加了简单的背景过滤:计算图片边缘密度和主色分布,把那些明显拍的是整个餐桌、包含大量餐具和桌布纹理的图挑出来做单独处理。这一步虽然没有严格的标准,但对伪标签质量的提升立竿见影。

3.3 骨干网络选型:伪标签质量看上界,只能靠预训练兜底

半监督训练里,基座模型的质量几乎决定了所有后续操作的上限。伪标签是从基座模型的预测里“捡”出来的,基座模型在某个类上本身就是糊涂的,那后面的伪标签也必然是糊涂的,这不以你的阈值调得多高为转移。

我当时用ResNet-50做基线网络,主要考虑是它和大规模预训练权重的结合最成熟,训练稳定性好,显存占用也友好。后来换过更重的骨干结构,收益并不明显,训练时间反而拉长了一大截。核心原因在于:半监督在小标签场景下最缺的不是模型容量,而是特征质量的稳定来源。预训练权重让骨干网络在前几轮就具备不错的特征提取能力,这比单纯堆参数有用得多。迁移到食物域时,我的做法是保持骨干网络先冻结几个epoch,让新加的分类头先适应任务,再解冻全部参数用低学习率微调。这个细节帮我把早期伪标签的噪声压下去不少。

3.4 增强策略要克制:食物图像经不起过度折腾

数据增强在半监督里的地位比全监督高得多,因为无标签样本的增强直接参与伪标签生成和一致性损失计算。但食物图像本身有自己的“物理常识”:菜品通常是正面拍摄的,有明确的上菜方向,过度随机旋转会出现倒挂的菜盘;颜色是菜品的重要身份信息,红烧的酱色和清蒸的本色差之毫厘,谬以千里,饱和度和色相扰动幅度稍大,模型就分不清这两种做法了。

我实际用的增强配置分两档。弱增强只做了随机水平翻转、小范围平移和很轻微的亮度微调,保证伪标签生成时图像语义不改变。强增强从RandAugment里挑了对比度、饱和度、锐度和几何裁剪四种操作,幅度控制在8以内,坚决不用旋转和翻转叠加的极端组合。这个配置是试出来的:一开始我把幅度拉到10,伪标签训练后验证集不升反降,后期复盘发现模型已经学会了“通过色块分布猜类别”,跟真实菜品特征完全脱节了。记住一个原则:增强的强度上限,是保证图片仍能被人类一眼认出是什么菜。

4. 伪标签迭代的完整流程:从基座模型到第二轮训练

方法讲完,落到具体执行层面。这一章我按操作顺序,把从基座模型训练到伪标签筛选再到迭代停止的完整流程过一遍,所有参数都是我实际跑过的值,供你参考。

4.1 基座模型训练:分类头要稳,backbone可以换

第一步是在有标签数据上训练一个尽可能稳的基座模型。这里强调“稳”而不是“准”,是因为基座模型的预测置信度分布直接决定后面伪标签筛选的质量。我当时的初始标签一共1800张左右,分布很不均衡,头部几类占了快一半。处理方式是做类别均衡采样,把尾部类别的样本在同一个epoch里多重复几次,同时用Label Smoothing防止模型在头部类别上过于自信。

训练轮次拉到60个epoch,比常规监督训练长了一些,目的就是让分类头在少量样本上充分收敛。分类头用最简单的Linear层就够,不需要任何花哨设计。训练结束时,基座模型在独立验证集上的准确率大约74%。这个水平不算高,但对于后续伪标签筛选来说已经够用,因为挑选高置信度样本本身就相当于在模型比较有把握的区域里采集新数据,并不要求模型完胜所有类别。

4.2 置信度阈值与噪声控制:宁可少收,不可错收

伪标签筛选最核心的操作是设定置信度阈值。阈值太高,能收入的新样本太少,迭代跟没做一样;阈值太低,噪声样本涌进来,模型很快被带偏。我在实验里从0.9测到0.98,最后的结论是:固定阈值选0.95比较稳,但要配合两个附加机制。

第一个机制是每类数量下限。有些类别在伪标签预测里始终处于弱势,比如冷门的汤羹类,全局0.95阈值下可能一个样本都选不进来。我给每个类别设了一个最低入选数量,如果某类低于20张,就把该类的阈值动态放宽到0.90,优先保证每个类都有新样本进入。第二个机制是单类贡献上限,每个类别补充的伪标签量不超过该类别有标签样本数量的两倍,防止头部类在训练集里迅速膨胀,把模型注意力全吸过去。

下面这段是伪标签筛选的示意代码,PyTorch风格,里面包含了按类别动态调整阈值的基本逻辑:

# 伪标签筛选示意:高置信度 + 每类数量下限 base_threshold = 0.95 min_samples_per_class = 20 class_thr = {cls: base_threshold for cls in range(num_classes)} pseudo_images, pseudo_labels = [], [] for images in unlabeled_loader: with torch.no_grad(): prob = torch.softmax(model(images), dim=-1) max_prob, pred = prob.max(dim=-1) keep = max_prob >= class_thr[pred] keep_images = images[keep] keep_labels = pred[keep] # 统计当前轮次各个类别的入选数量 for cls in range(num_classes): if count[cls] < min_samples_per_class: class_thr[cls] = 0.90 # 冷门类型放宽阈值 else: class_thr[cls] = base_threshold pseudo_images.append(keep_images) pseudo_labels.append(keep_labels) pseudo_images = torch.cat(pseudo_images) pseudo_labels = torch.cat(pseudo_labels)

还要提醒一句:伪标签并不是全量加入训练集就完事了,最好做一次相似度去重。我第一轮筛出来的一千多张伪标签里,有五分之一来自同一菜品不同角度的连拍,简单跑一下特征相似度聚类,把这些重复项压到只留一两张,训练效果反而更好,因为模型接触到了更多元的形态,而不是反复复习同一盘菜。

4.3 迭代节奏安排:三轮训练的实操记录

伪标签迭代我做了三轮,每一步都从零开始重新训练,而不是在上一轮模型上继续训练。这一点很重要,继续训练会加剧确认偏差,错误的伪标签会像滚雪球一样被续传下去。三轮的具体情况是:第一轮筛选出约1500张高置信度伪标签,加入有标签数据后重新训练,验证集准确率提升约4个百分点;第二轮新增约3000张,提升约2个百分点;第三轮只新增了600张就接近饱和,继续迭代已经开始有轻微过拟合迹象,到这里就止损停住。

判断迭代是否该停,我自己的经验是观察每轮新增伪标签的数量变化。如果一轮结束后,能通过高阈值筛选的新样本数量比上一轮锐减,说明无标签数据里“模型有把握”的部分已经被挖掘得差不多了。此时再强行拉低阈值去搜刮更多伪标签,收进来的大概率是错误样本。第三轮结束后我也试过一次把阈值从0.95降到0.90,结果验证集准确率不升反降,这就是典型的“错收”代价。

5. 评估设计与实验复盘:半监督到底值不值

训练做完只是第一步,最关键的事情是想清楚怎么验证它真的有用。半监督提升最容易被“看起来涨了点”这种模糊结论糊弄过去,所以这一章设计了一套相对完整的评估方案,以及从实验结果里挖出来的几条反直觉规律。

5.1 对比实验:有监督基线、半监督和全监督上界

我搭了三组对照:全监督上界使用全部有标签数据外加独立测试集;有监督基线只使用初始有标签数据的80%;半监督则在初始80%标签的基础上加入伪标签数据。同时把标签比例切成5%、10%、20%三档,观察半监督在不同标签稀缺程度下的表现。

下面是实验结果的一个典型呈现形式,数据来自我项目里的一次完整对照:

标签比例有监督基线半监督全监督上界
5%58.2%65.4%71.9%
10%66.1%72.8%76.3%
20%70.7%74.2%78.5%

从表格可以看得很清楚,标签越少,半监督的相对收益越大。5%标签比例下,半监督直接把准确率拉高了7个多点,逼近了全监督上界的九成水平;等到20%标签比例时,半监督的提升就只有3.5个点了。这符合半监督理论的基本判断:有监督信息越稀薄,利用无标签分布的价值就越突出。如果你手里的标签数量已经不少,半监督更像是一个锦上添花的优化手段,不要指望奇迹。

5.2 混淆矩阵里的“偏科”现象:哪些菜被救活了,哪些菜根本没人管

整体准确率提高不代表所有类别都在进步。我特意去翻了半监督训练前后的混淆矩阵,发现提升并不是均匀分布的。外观特征强、颜色纹理特点明确的类别,比如红烧类、油炸类、烧烤类,提升幅度最大,伪标签确实帮模型进一步明确了这些类的决策边界。而另外一些类别基本没动,最典型的是汤面类和凉拌菜类。这些菜品的区分更多依赖碗具形态、汤底浓度、配菜布局这类细节,有时甚至需要依赖上下文信息,单靠一张图很难在特征空间里形成类似红烧肉那样紧凑的簇。

还有一个更扎心的现象:个别尾部类别从头到尾没有一张伪标签入选。原因很简单,基座模型在这些类上置信度始终不高,阈值一卡就全被过滤了。这也暴露了半监督的一个天然盲区——它对模型已经“有点会”的类别能起到显著增强作用,对模型完全不会的类别几乎是束手无策。尾部类别的覆盖问题只能靠主动学习、少量人工补标或者辅助信息来补,半监督不能包打天下。

5.3 踩坑最深的三个细节

第一个坑是强增强力度失控。有一轮我把RandAugment的幅度从8调到10,本来只是想看看更强的扰动能不能带来更强的鲁棒性,结果训练结束后验证集准确率掉了将近一个点。翻了几张增强后的样本才明白,幅度超过10之后,很多菜品图在视觉上已经变成了“抽象色块”,模型被迫在噪声里强行找规律,学了一堆和真实菜品无关的统计特征。此后我对增强幅度的调整都保持一次只动一个数值,并且每次都用肉眼抽查增强结果。

第二个坑是伪标签重复样本的隐性污染。前面提到我用相似度去重过滤了一万五千张无标签图,但由于第一轮筛选伪标签时没有对筛选结果再次去重,导致某些连拍图被当成多个独立样本进入训练集,单类数据量虚高。一次意外中断让我不得不重新审视伪标签集合,才发现这个问题。之后我把去重逻辑加到了每一轮筛选之后,而不是只作用于原始无标签数据。

第三个坑是阈值“从一而终”的惰性思维。固定0.95在第一轮好使,不代表第三轮也好使。到第三轮时,模型对高置信度样本的把握已经很高,但无标签数据集里能被0.95阈值“捡走”的样本已经枯竭,这时候继续用这个阈值等于让迭代空转。改成分类别动态阈值之后,才勉强从剩余样本里又挖出几百条可用数据,虽然提升有限,但至少没有浪费时间。

我自己的体会是,半监督带来的真实增益并不来自算法本身的玄妙,而是来自每个环节里对噪声的严防死守。模型也从不会替你把垃圾数据变成金子,它只会把垃圾数据变成更固执的垃圾模型。下篇我会把这套流程搬到一个更大的无标签数据集上,重点处理尾部类别覆盖不足的问题,同时把推理阶段的优化方案整理出来。如果你也在做类似的食物分类或者其他细粒度分类项目,非常欢迎拿实验数据过来一起对效果,这些坑我一个人踩过了,你就不用再踩一遍。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进