news 2026/10/3 13:06:49

六种水果分级数据集构建:从分级标准到模型部署全流程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
六种水果分级数据集构建:从分级标准到模型部署全流程实践

上个月在给一条果蔬分拣线做视觉质检方案时,我遇到一个很现实的问题:项目需要按品质等级对六种水果进行图像分类,但翻了半天公开数据集,能下载到的水果分类数据大多按“品种”区分,比如苹果有富士、嘎啦、蛇果,或者按“状态”区分,比如好果和烂果。真正面向“同一种水果的A级/B级/C级”这种细粒度区分的六种水果分级-图像分类数据集,几乎找不到。没办法,只能自己动手构建一套,顺便把从分级标准制定、采集清洗、标注质检、数据增强、模型训练到真机部署的完整流程都趟了一遍。这篇文章就记录这一整套做法和踩过的坑,给同样在做农业视觉项目、需要自建图像分类数据集的朋友做个参考。

1. 为什么我决定自己做一套六种水果的分级数据集

1.1 公开数据集解决不了“等级”问题

Fruits-360这样的公开数据集确实很经典,里面收录了大量水果品种的实拍图,类别标签也很干净。但它解决的是“这是什么水果”的问题,而不是“这个水果属于哪个等级”的问题。实际分拣线上,我们更关心的是:这个苹果能不能按一级果出售,这个橙子是否有足够的甜度和色泽进入商超渠道,这批草莓里有没有畸形果需要挑出去。简单说,客户要的不是物种识别,而是质量分级。

另一个现状是,工业缺陷数据集虽然有不少,比如钢材表面缺陷、木材瑕疵、纺织物破损,但农产品的分级逻辑和工业缺陷完全不同。一颗苹果有0.5厘米的擦伤,可能只是B级,但如果是轮纹病留下的病斑,哪怕很小也要直接降为C级。这种“缺陷类型+面积阈值+位置因素”的组合,在通用公开数据集中很难找到。更别提六种水果各自的分级维度还不一样,香蕉看长度和黑斑,猕猴桃看软硬和绒毛,葡萄看脱粒和白霜。硬套公开数据集,模型上线后正确率会非常难看。

1.2 数据集的规格设计与命名

既然决定自己做,第一步就是把数据集的形态定义清楚。我选了苹果、香蕉、橙子、草莓、猕猴桃、葡萄这六种常见水果,每种水果设置A级、B级、C级三个品质等级,总共18个分类。为什么是三个等级?因为再细的级别(比如特级、一级、二级、三级)在图像上差异非常模糊,人工标注都很难稳定区分,模型学起来更会一团浆糊。

类别命名采用英文小写加等级后缀的方式:

apple_A, apple_B, apple_C banana_A, banana_B, banana_C orange_A, orange_B, orange_C strawberry_A, strawberry_B, strawberry_C kiwi_A, kiwi_B, kiwi_C grape_A, grape_B, grape_C

目录结构也设计成图像分类任务最常见的按类文件夹摆放:

dataset/ ├── train/ │ ├── apple_A/ │ │ ├── apple_041_0032.jpg │ │ └── ... │ ├── apple_B/ │ ├── apple_C/ │ ├── banana_A/ │ ... │ └── grape_C/ ├── val/ └── test/

每类目标600张,18类就是10800张;额外增加300张“背景/杂质”图像放到单独类别,用于后面让模型学会拒识,最终数据集约11100张。划分比例按训练70%、验证15%、测试15%,但划分必须基于“水果个体”分组切分,而不是简单随机切分,这一点后面会详细讲。命名里的apple_041表示第41个苹果个体,0032表示该个体的第32帧,这样的命名规则对后续防数据泄漏起到了关键作用。

2. 分级标准是实现可复现标注的第一步

2.1 把质检经验翻译成可量化的规则

一开始我以为分级标准很简单,不就是好果、次果、坏果吗?真到制定规则的时候才发现,质检师傅嘴里的“这个果面要干净”“这个太小了”根本没法直接落成标注指令。所谓“干净”,是完全没有斑点还是允许少量果锈?所谓“太小”,直径小于多少毫米才算小?

后来我们拉着质检师傅一起,把每个等级的关键指标拆出来,做成量化表。以苹果为例:

等级直径着色面积瑕疵面积形状
A级≥80mm红色占比≥70%无可见瑕疵端正,无畸形
B级≥70mm红色占比≥40%擦伤/斑点面积≤5%允许轻微不对称
C级其余红色占比不足或分布不均瑕疵面积>5%,或存在腐烂、破损畸形明显,不可接受

其他五种水果也按类似思路拆解。香蕉按长度、弯度和果皮黑斑面积;橙子按颜色均匀度、粗皮比例和病斑;草莓按形状是否圆锥、红色覆盖比例和表面是否受损;猕猴桃按软硬程度、绒毛完整度和表面凹陷;葡萄按果粒密度、脱粒比例和白霜覆盖程度。

重点并不是规则写得多漂亮,而是要将规则细化到“看一眼就能判断”的程度。在我做的版本里,瑕疵面积的判定直接用透明网格纸叠加到屏幕上,用面积占比来估算。规则里必须有具体的数值阈值,比如“擦伤面积≤5%”而不是“轻微擦伤”。有了这样的文字规则,标注员才可能保持一致。

2.2 边界案例是统一标注手感的唯一办法

文字规则再细,依然会出现同一张图两个人给出不同等级的情况。一个典型的例子:苹果表面有一块6%的黄褐色晒斑,有人觉得“不明显嘛,算B级吧”,另一个人认为“超过5%就算C级”。这不是标注员不认真,而是规则对边界情况的解释还不够。

我的解决办法是建立一份“边界案例图卡”。每个等级准备8张标准图,再额外准备4张边界图。边界图专门收录那些卡在阈值附近的样本,并在图卡上标注这个样本“为什么被判为这个等级”。比如那张晒斑6%的苹果,明确标注为C级,理由是“瑕疵面积超过5%的硬性标准”。标注员正式开工前,需要先拿30张图试标,准确率达到100%才允许进入正式标注流程。

还有一个容易忽略的点:腐烂果。拍摄过程中难免碰到已经软烂的水果,这种如果直接扔进C级,会让C级里的特征非常发散——既有畸形果、又有病斑果、还有腐烂果。我在标注规范里单独标记了腐烂/严重破损样本,仍然放在C级,但要求图像占比不超过该类别样本量的15%,避免模型把“腐烂”当成C级的主要特征。

3. 采集与清洗:别让脏数据毁了模型

3.1 采集环境和相机参数

数据采集阶段最容易犯的错误是“把拍摄环境打理得太完美”。而实际上,真实产线的光照、背景、运动模糊都是不可控的。我在实验室搭了一套采集台,也用手机补拍了一些自然场景的图像。

工业相机采用海康MV-CA050-20GC,500万像素,配8mm焦距镜头,工作距离控制在40到50厘米。两个LED条形光源从左右45度方向打光,减少水果表面的大面积阴影。白平衡先通过灰卡校准一次。传送带速度设为0.2米每秒,用光电传感器触发相机拍照,保证每张图都是清晰的单果画面。

只靠工业相机拍出来的数据有一个严重问题:背景永远是传送带的黑色皮带,光源永远是固定位置。模型如果长期只在这种数据上训练,很容易把“黑色背景”当作辨别特征的背景。所以我又用手机在日光、暖白灯、荧光灯等不同光源下手持拍摄了约2000张图片,背景包括塑料筐、泡沫托盘、桌面、甚至手掌。拍摄角度也做了变化:正面、侧面、背面、俯视,水果在画面中占比约60%到90%,不要求完全居中。

3.2 数据清洗与去重

清洗环节我差不多删掉了快三分之一的数据。先是程序过滤:计算图像的拉普拉斯方差,小于阈值100的判定为模糊图,直接删除。这个阈值不是拍脑袋定的,而是先手动挑了几十张清楚和模糊的图,计算出来一个分界值。接着人工快速过一遍缩略图,删除过曝、欠曝、高光反射强烈以及水果被严重遮挡的图。

最容易被忽略的是连续帧去重。因为传送带触发拍照,同一个水果从进入视野到离开,可能拍了6到8张几乎一样的图。如果这些相似帧同时进入训练集和测试集,模型等于在“背答案”。我用感知哈希算法给每一帧计算dHash,然后两两比较汉明距离,距离小于5的视为重复帧,只保留其中一帧。这一步把约15000张原始图像压缩到了11500张左右。

清洗后的文件命名也很有讲究。我采用“水果种类+个体编号+帧号”的方式,比如apple_041_0032.jpg,这样后期做数据划分时,可以通过个体编号知道哪些图像来自同一个水果,从而避免数据泄漏。很多人在清洗后会忽略这一步,等到训练时才发现无法做分组切分,只能重新整理,非常浪费工时。

4. 标注一致性比标注本身更值得花时间

4.1 标注工具和流程设计

图像分类的标注工具其实很简单,我在Label Studio里配了一个分类标注模板,每个样本选择对应的18个类别之一。标注结果导出为CSV,包含image_id和label两列。如果你不想用工具,也可以直接把图像复制到对应类别的文件夹,但那样不利于后续的争议样本统计。

流程上,我采用了“初标-复标-仲裁”三段式。初级标注员先按类别初标全部数据;然后由一位质检主管随机抽取30%的已标数据进行复核,发现不一致就退回重标;被退回的不一致样本进入仲裁池,最后由更懂水果的质检师傅逐张拍板。这套流程看起来繁琐,但非常必要,特别是对于B级和C级边界模糊的样本,避免了个体主观性影响整体标注质量。

另一个容易踩的坑是“标注漂移”。一个人从第1张标到第500张,对标准的掌握会慢慢变化——前期觉得严重的瑕疵,后期可能觉得不严重。所以我在每标完250张后,会抽50张与之前标注过的图进行对比,检查是否存在系统性松紧偏移。如果发现标准变了,就整个重新校准一次。

4.2 用Kappa系数把主观分歧量化出来

标注质量不能光靠“感觉大家意见差不多”,我用Cohen's Kappa系数来量化不同标注员之间的一致性。Kappa的计算公式是:

kappa = (Po - Pe) / (1 - Pe)

其中Po是两人标注结果的实际一致率,Pe是假设完全随机标注时可能达到的一致率。kappa值大于0.8可以认为标注一致性良好。

第一次试标时,我随机抽取了300张图,让两位资深标注员独立标注,算出的kappa只有0.73。翻看分歧样本,几乎所有问题都集中在“擦伤面积是否超过5%”和“轻微畸形是否算畸形”这两类边界判断上。于是我把透明网格辅助工具引入标注界面,让标注员可以通过叠加网格估算瑕疵面积,同时把新增的边界案例图发给每个人。第二次测量,kappa提升到了0.86,达到了可接受的门槛。

4.3 类别平衡与样本控制

经过清洗和标注后,各类别数量并不均匀。苹果因为采购的是分级较好的货源,C级特别少;草莓恰好相反,A级好果难找,B/C级特别多。我的目标是把每个类别控制在600张左右,上下浮动不超过50张。

对于数量不足的类别,我采取定向补拍,而不是用合成样本硬凑。比如苹果C级缺了120张,我就专门去批发市场挑了一批病斑果、畸形果和部分有轻微腐烂的果子拍。对于超过700张的类别,随机删除多余的,尽量保证所有类别数量在同一量级。不要用SMOTE之类的过采样方法生成图像,分类模型尤其吃真实分布,合成的伪图像会让训练集分布偏离真实场景。

5. 数据增强和切分时最容易踩的坑

5.1 用消融实验决定用哪些增强

图像分类数据增强我用了很多年,但每次面对新数据集,我还是会做一轮消融实验,而不是直接把所有增强都堆上去。这次我用了Albumentations库,对比了不同增强组合对验证集准确率的影响。

最终保留下来的增强配置是:

  • 随机裁剪缩放RandomResizedCrop,scale范围0.7到1.0,ratio范围0.8到1.2
  • 水平翻转,概率0.5
  • 亮度对比度调整,brightness_limit和contrast_limit都设为0.2
  • HSV抖动,hue_limit=10, sat_limit=20, val_limit=20
  • 随机旋转±20度

这个组合在验证集上表现最稳。我测试过额外增加CoarseDropout随机擦除,验证集准确率反而掉了0.6个百分点。后来分析发现,水果图像中的瑕疵区域往往面积不大,随机擦除很容易把关键缺陷区域直接抹掉,导致模型学到错误的特征。类似地,高斯模糊强度过大会让训练集和真实场景的锐利细节不匹配,影响泛化。

5.2 GroupSplit按个体分组切分,避免数据泄漏

这一小节是整个数据集制作中最值得重视的问题。最初我用train_test_split(X, test_size=0.15, random_state=42)做随机切分,训练出来的模型测试准确率高达98%,当时觉得模型已经成了。结果拿到真实场景一测,准确率直接跌到80%以下,完全没法用。

问题出在哪?因为同一个苹果的连续帧里有大量高度相似的图像,随机切分时,同一个苹果的A帧可能在训练集,B帧可能在测试集。模型在训练时已经见过这个苹果的“长相”,到了测试阶段,它做的是图像检索而不是等级分类。这是典型的数据泄漏,会让验证指标完全失真。

解决方法是按“水果个体”分组切分。我们命名时保留了个体编号,比如apple_041的所有帧只能出现在同一个集合里。使用sklearn的GroupShuffleSplit实现:

from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.30, random_state=42) train_idx, val_test_idx = next(gss.split(X, y, groups=groups))

先把数据按7:3分成训练集和验证测试集,再把验证测试集按同样的逻辑分成验证集和测试集。改进后,测试准确率从虚高的98%降到了更真实的91%。表面上分数降了,但真机实测准确率反而回升到了88%左右,这才是可靠的结果。如果你做的是视频流采集的数据集,强烈建议从一开始就保留个体ID,否则之后想修都难。

6. 基于该数据集的分类模型训练实测

6.1 ResNet50迁移学习基线

我先用ResNet50做基线模型。加载ImageNet预训练权重后,把最后一层全连接替换成18类输出。输入图像尺寸统一为224×224。

训练超参数如下:

  • 优化器:SGD,momentum=0.9,weight_decay=1e-4
  • 初始学习率:0.01
  • 学习率策略:前5轮warmup,之后余弦退火到1e-5
  • 轮数:40
  • 批大小:64
  • 损失函数:CrossEntropyLoss,label_smoothing=0.1

为什么不用Adam而用SGD?迁移学习场景下,SGD配合余弦退火在中小数据集上通常更稳,不容易在训练初期把预训练特征破坏掉。label_smoothing是为了避免模型对18类的预测过于自信,尤其是相邻等级特征本身接近,过度自信会让混淆更严重。训练代码很简单,但有几个细节要记住:

import torch import torch.nn as nn model = torch.hub.load('pytorch/vision:v0.14.0', 'resnet50', weights='ResNet50_Weights.IMAGENET1K_V1') model.fc = nn.Linear(model.fc.in_features, 18)

在单张RTX 3090上训练约45分钟,测试集准确率95.8%。

6.2 多模型对比:ResNet、EfficientNet、YOLOv8-cls

基线模型跑通后,我又对比了几种常见图像分类模型,统一使用同样的训练配置。这里不是要“吹”某个框架,而是给你一个参考路径。

模型参数量测试准确率推理耗时(Jetson Nano FP16)
ResNet5025.6M95.8%8.2ms
EfficientNet-B312.3M96.4%9.1ms
MobileNetV3-Large5.5M94.2%2.3ms
YOLOv8s-cls11.1M96.7%6.0ms

最终我选了YOLOv8s-cls作为线上模型。原因有三:准确率最高;推理速度在Jetson Nano上够用;同一个框架后续如果要扩展到水果目标检测,可以复用部署流程和推理代码。MobileNetV3虽然更快,但准确率掉了2个多点,对于分拣线来说,误判成本远高于算力成本。

6.3 混淆矩阵暴露的等级边界问题

画混淆矩阵的时候能看到一个很明显的规律:绝大多数错判发生在同类水果的相邻等级之间,也就是A级被当成B级、B级被当成C级,跨品种的混扰极少。这说明模型确实学到了“品种”和“等级”两类特征,但等级边界的可分性还是不够。

最典型的例子是草莓。A级要求“外形近似圆锥形,红色覆盖90%以上”,B级允许轻微畸形。但在图像上,轻微畸形和正常形状之间的差异非常细微,人工都容易看走眼,模型自然也会犯错。另一个难分的是葡萄的B/C级,因为C级的脱粒和软果特征往往集中在果梗处,单张俯视图不一定能拍到。

针对这个问题,我做了两件事:一是对C级样本增加重采样权重,让模型在训练时多看C级,把C级的F1从87.2%提升到了89.6%;二是在训练时将C级内部图像随机裁剪放大,让模型多关注局部瑕疵。整体准确率提升有限,但关键等级的召回更稳了。

7. 部署到实际场景后的泛化教训

7.1 相机变化导致精度下跌,微调救场

模型在测试集上准确率96%,移植到客户工厂的监控相机后,准确率直接跌到82%。原因并不意外:不同厂家、不同型号的相机在白平衡、锐度、色彩饱和度上差异很大。实验室的工业相机成像偏冷,客户现场的监控相机偏暖,加上现场有自然光从窗户照进来,整个画面的色调完全变了。

解决办法分两步。第一步,收集现场约2000张不带标注的真实图像,用现有模型推理,把置信度超过0.9的样本当作伪标签加入训练集,然后进行两轮微调。第二步,将原本纯实验室数据中的背景、光照比例降低,补充更多现场风格的图像。微调后准确率回升到93%。

更早更省事的做法是采集阶段就主动引入多相机、多光源、多背景。我现在做数据集,都会在采集计划里强制加入10%以上的异源图像,比如手机拍摄、日光灯下拍摄、隔着透明薄膜拍摄等。这个比例不是拍脑袋定的,是根据部署现场可能出现的环境变化度估算出来的。

7.2 给分类器加一个“拒识”选项

分类模型的softmax输出天然有一个缺陷:无论输入什么图像,它都会强制把概率分配给已定义的类别。如果递给模型一张手套的照片,它可能会以非常高的置信度把它识别成“草莓A级”。在分拣产线上,这种错误是不可接受的,因为杂物混入水果包装会直接引发客诉。

所以我给数据集增加了一个background类,专门收录空背景、人手、手套、碎叶、包装袋等杂物图像,共300张。同时在推理阶段加了一个置信度阈值判断:取softmax最大概率p,如果p小于0.75,就输出unknown,不进分拣流程。阈值怎么定?在验证集上做阈值扫描,画出误触发率和准确率的曲线,选择“每1000个正常样品误触发次数小于1次”时的最大值。落地后,杂质误判率从原来的每千次几十次降到了不足1次。

7.3 ONNX/TensorRT部署与量化校准

模型落地用的是Jetson Nano,所以要先把PyTorch模型转成ONNX,再转成TensorRT engine。ONNX导出时要注意,必须固定输入尺寸,否则动态尺寸会带来额外的延迟:

torch.onnx.export( model, dummy_input, "model.onnx", opset_version=11, input_names=["input"], output_names=["output"], dynamic_axes=None )

TensorRT量化时,我的经验是校准集不能用训练集,而要用单独的1000张真实部署环境采集的图片。训练集和模型分布过于接近,校准出来的量化阈值偏向训练分布,实际部署时精度损失会更大。INT8量化后,准确率相比FP32下降了约1.2个百分点,但推理速度从FP16的6毫秒提升到了约4毫秒。对于单帧处理的产线,4毫秒完全够用,甚至还能冗余一些算力给后续的检测模型。

这次项目下来,我的核心体会是:数据集的质量直接决定分类模型的上限,模型结构和调参只是把数据集的能量释放出来。六种水果分级听起来简单,真正难的是把“看起来差不多”的果按照同一把尺子分到不同等级,并且保证切分时不让同一个果出现在训练集和测试集里。如果你也在做类似的分级数据集,建议先把分级规则写在一页纸以内,再花时间把边界案例图拍好,让标注员试标一轮,确认Kappa值合格后再放开标注。最后分享一个小技巧:发布数据集的时候,附一张在测试集上的混淆矩阵和几张典型的边界错判示例,不仅能省去大量答疑时间,也能让使用者更清楚你的类别定义边界在哪里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:04:35

2026云栖大会AI原生技术落地实战:Agent云架构部署与产业落地全指南

摘要:2026年云栖大会彻底终结了大模型参数竞赛的行业旧范式,正式确立AI原生产业规模化落地的全新赛道。本文基于第一性原理拆解本次大会全部核心技术成果,从底层芯片算力、云端基础设施、大模型迭代、智能体架构到千行百业实战落地&#xff0…

作者头像 李华
网站建设 2026/10/3 13:04:27

Fourier-Galerkin谱方法求解二维Navier-Stokes方程的MATLAB实现与全流程解析

简介:一个将Fourier-Galerkin谱方法与MATLAB实现完整结合的项目,面向需要求解二维不可压缩Navier-Stokes方程的研究者、研究生和工程师。资源提供从预处理、主程序到结果展示的整套计算流程,涵盖时间积分、右侧项构建以及多种典型流场设置&am…

作者头像 李华
网站建设 2026/10/3 13:03:51

微信聊天记录导出实战攻略:从准备到归档,一次讲透

前言微信聊天记录,可能是你手机里最值钱、又最没人管的数据。它里面有跟家人的日常、跟客户的沟通、跟对象的情话、转账的凭证、工作的交接……但绝大多数人,从来没有完整导出过一次。等到手机坏了、换机失败、要打官司、要交接工作的时候,才…

作者头像 李华
网站建设 2026/10/3 13:03:50

wsl下使用npm,错误识别到了windows下的npm

这里写自定义目录标题报错问题的根本原因:下载并安装 nvm (以 v0.40.1 为例)让环境变量立即生效 (如果你用的是 zsh,请将 bashrc 换成 zshrc)安装 Node 20切换到 Node 20将 Node 20 设置为 WSL 默认的 Node 版本(每次打开终端自动使用&#x…

作者头像 李华
网站建设 2026/10/3 13:02:24

TCP 协议:报文头、连接管理、可靠传输与窗口

TCP 协议:报文头、连接管理、可靠传输与窗口 课程:尚硅谷《嵌入式 Linux 应用层开发》第 6 章 Socket 编程 依据:2026-09-29 20:51 录音转写;课程 PDF 第 254—264 页。 本节边界:讲 TCP 原理和窗口机制;第…

作者头像 李华