简介面向计算机视觉入门与图像分类实战人群这套西瓜成熟度图像分类数据集适用于果蔬成熟度识别、CNN分类模型训练等场景。数据集成类别标注共划分成熟、半熟、未熟三个类别具体标签定义可查看json配置文件。包内共677个文件以675张jpg图片为主辅以1个Python脚本和1个json文件脚本用于快速可视化数据集json则记录分类映射整体约15.74MB轻量易用。资源已按训练集、测试集分类存放同一类别图片置于对应目录可直接用于模型训练与验证运行show脚本还能直观检查图片与标签是否匹配降低数据预处理门槛。目前已有134人学习下载适合需要快速获取带标注图像数据、开展分类网络改进实验的开发者使用。1. 600张已标注的西瓜成熟度数据集到底够不够用很多人拿到「约600张」第一反应是数量太小训练不了图像分类模型。实际上在图像分类算法里600张已经标注的图片足够跑通一条完整的成熟度识别流水线——前提是别从零训练改用迁移学习并且严格约束验证集划分。西瓜成熟度分类与常见的cnn花卉图像分类任务高度相似类别差异集中在果皮底色、网纹密度、光泽度和敲击音色对应的外观特征上模型要学的不是复杂语义而是有限的视觉模式。果实成熟度数据集普遍不大真正决定上限的不是样本量而是标注的一致性和验证方式的合理程度。这篇文章按「数据处理 → 基线模型 → 参数调优 → 阈值部署」的顺序展开覆盖从拿到目录到导出一个可调用模型的过程适合刚接触数据集的入门者也适合想确认小样本训练边界的从业者参考。2. 拆解西瓜成熟度图像分类数据集的目录结构与标注质量2.1 先确认标注载体文件夹命名还是独立标注文件已标注的图像分类数据集通常有两种组织方式类别文件夹语义标注或同一目录加 JSON/CSV 文件保存类别与文件名映射。前者最常见的结构是 train/val 下再按 maturity_level 分子目录例如unripe、ripe、overripe每个文件夹里的图片文件名就是样本编号后者则是每张图对应一行记录字段大致为filename,label或filename,category_id。拿到数据后第一件事是扫描一遍目录而不是急着训练。下面的命令把整个数据集的文件数量、扩展名分布和潜在的空目录列出来确认图片格式没有混用 JPEG 和 PNG避免后续 DataLoader 读图时类型不统一。find . -type f \( -name *.jpg -o -name *.jpeg -o -name *.png \) | \ awk -F. {print tolower($NF)} | sort | uniq -c for d in */; do echo $d : $(find $d -type f | wc -l) files done统计结果里如果出现异常的 0 文件目录优先排查是数据打包丢失还是原始采集时就遗漏。扩展名统计能直接发现混用了.JPG大写后缀的情况这类文件在 Linux 环境下按小写匹配时容易被跳过。类别分布信息则用一段短脚本读取输出每类的图片总数方便后续判断是否存在严重的类别不均衡。2.2 读图验证标注有效性找出坏图和错标目录结构正常不等于标注有效。约600张图片里如果混入几十张重复图、全黑图或拍摄对象完全不在画面中的图验证集一旦分到这些样本最终准确率看起来很高实际换一批新西瓜立即崩掉。对图像分类数据集做基本体检时我用 Python 逐张打开图片记录解码失败、尺寸异常和通道异常的文件并把分辨率统一记录成表格。from PIL import Image from pathlib import Path img_dir Path(data/watermelon) results [] for p in img_dir.rglob(*.jpg): try: img Image.open(p) img.load() w, h img.size if w 64 or h 64: results.append((p, too_small, w, h)) elif img.mode ! RGB: results.append((p, img.mode, w, h)) except Exception as exc: results.append((p, decode_error, 0, 0)) for item in results: print(item)这里img.load()是关键只有它被调用后损坏的截断文件才会在解码时抛出异常仅Image.open而不读取像素数据的话文件头正常但数据损坏的图不会被发现。尺寸低于 64×64 的样本对后续 ResNet 这类下采样网络没有意义因为输入会被强制插值成熟度纹理细节全部丢失。通道检查则用于识别灰度图或带透明通道的 RGBA 图。标注质量更隐蔽的问题是类别分错。以西瓜成熟度为例半熟和适熟在外观上的边界本身就模糊同一张图不同标注员可能给出不同结论。短期没有精力逐张复核时至少要做一次随机抽样审查从每个类别文件夹里随机抽 20 张拼成一张网格图人工过目确认类别内部没有明显混入异物。这一步不需要写复杂的工具任选一款看图软件按目录浏览即可。2.3 统一划分 train / val / test锁死验证集小数据集的划分比大模型更需要谨慎。常见做法是把可用样本按 7:2:1 切分为训练、验证和测试测试集一旦划分完成就冻结任何调参过程都不允许触碰。600张数据按比例切分后训练集约 420 张验证集约 120 张测试集约 60 张——验证集的每一次改动都可能让模型间接记住测试样本的分布。划分时要保证同一种拍摄场景的图片不要同时出现在训练和验证集中。西瓜成熟度数据集如果是按户采集、按天拍摄的同一个西瓜在多角度下的照片在像素层面高度相似随机划分会把同一瓜的不同角度分别放进训练集和验证集导致验证集的难度被严重低估。更稳妥的切分方式是按拍摄对象或采集批次分组用GroupShuffleSplit而不是普通的train_test_split。from sklearn.model_selection import GroupShuffleSplit # filenames: 所有图片路径 # group_ids: 同一来源图片的编号可按拍摄批次手工指定 # y: 成熟度标签0未熟1适熟2过熟 split GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, val_idx next(split.split(filenames, y, groupsgroup_ids))GroupShuffleSplit和train_test_split的本质差别在于groups参数拆分时把同一组的所有样本当作一个整体要么全部进训练要么全部进验证避免组内样本跨集泄漏。这一步对成熟度这类高度依赖光照和背景的数据尤其重要同一批次拍摄的西瓜在底色上通常高度相似泄漏会让验证准确率虚高十个百分点以上。数据预处理到这里告一段落。把划分结果落盘成三张 CSV文件名、标签、分组信息各一列后续所有实验只读这套划分不再重新随机。3. 用迁移学习建立西瓜成熟度图像分类的基线模型3.1 为什么成熟度分类优先选 ResNet 而不是直接上 Transformer图像分类模型选型的核心约束是训练集只有 420 张左右。ViT 等 transformer 图像分类模型在 ImageNet 上动辄训练数百万张迁移到小数据集时虽然也能微调但线性注意力机制对局部纹理的归纳偏置较弱需要更大数据量才能把细粒度差异学到手。ResNet 的卷积结构天然携带局部性和平移等变性对果皮网纹、底色渐变这类视觉特征更友好。我一般以 ResNet18 和 ResNet34 作为西瓜成熟度分类的起点两个模型在 ImageNet 上的预训练权重都稳定可得参数量分别为约 1100 万和 2100 万对显存要求低单张入门级显卡就能训练且推理速度快。EfficientNet-B0 也是可选项它在同样参数量下准确率略高但训练时长和显存开销都要多出不少在样本量只有几百的数据上多出来的那点模型容量往往得不到充分利用。训练时不建议冻结 backbone 只训分类头。成熟度的特征在 ImageNet 预训练的池化层输出里并不存在它依赖的是低层颜色、纹理特征的组合这部分在冻结状态下无法微调模型容易陷在「用 ImageNet 语义理解西瓜」的错误方向上。正确的做法是全部参数参与微调配上很小的学习率。3.2 构造适用于小数据集的图像增强管线成熟度分类的数据增强要针对拍摄环境设计。西瓜从采摘到拍摄光照条件几乎不可能恒定网纹的粗细、果皮的底色在不同品种间也有差异。增强策略里旋转和翻转是性价比最高的两种我通常加 ±15 度以内的随机旋转和水平翻转旋转角度超过 20 度会让西瓜的视觉重心出现明显偏移要求模型去学旋转无关性这在只有几百个样本时属于浪费模型容量。颜色抖动对小数据集有正反两面效果。适度的亮度扰动能模拟不同光照但过强的饱和度扰动会抹掉「底色由绿转黄」这个最关键的成熟度线索。用下面这组配置作为基线训练时再按验证集表现决定是否加大强度。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(15), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.2, saturation0.1, hue0.02), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize使用的均值和标准差直接沿用 ImageNet 统计值因为预训练权重就是在这组统计上训练的。验证变换里不加任何随机操作使用与训练一致的Resize((224, 224))避免因中心裁剪位置不同引入测量误差。ColorJitter的saturation参数压低到 0.1就是为了防止过度改变绿色到黄色的过渡区域。3.3 训练脚本主框架加载预训练权重与替换分类头PyTorch 里加载预训练模型后需要把最后一层全连接替换成符合当前类别数的输出。西瓜成熟度数据集按三档划分时输出维度是 3如果标注更细例如按采摘后天数分成 5 档则对应改成 5。import torch import torch.nn as nn from torchvision import models device cuda if torch.cuda.is_available() else cpu model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes 3 model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 )ResNet18_Weights.IMAGENET1K_V1是带版本标记的权重枚举比直接传字符串更像社区维护的规范写法。weight_decay设为 1e-4 属于小样本训练的常规值主要作用是约束权重范数防止模型在 420 张训练图上把足够多参数记忆得过于极端。优化器用 AdamW 而非 Adam两者的差异在于权重衰减和解耦方式AdamW 在微调场景下收敛更稳定。训练过程中记录每个 epoch 的训练损失和验证损失验证损失下降停滞时ReduceLROnPlateau自动把学习率减半等待损失继续下降。小数据集微调的前提是预训练权重已经把底层特征组织得足够好这一阶段的学习率不能大1e-4 起步是可靠的选择如果验证损失在头两个 epoch 内震荡剧烈再降到 5e-5。训练迭代数量参照验证损失而不是固定跑满多少个 epoch。当验证损失连续 8 个 epoch 没有低于历史最小值即可停止训练调用torch.save(model.state_dict(), watermelon_resnet18.pt)保存权重。对于约 600 张图片的数据集ResNet18 单卡训练一轮只需要几十秒整体训练时间非常可控。4. 数据增强、类别不均衡与训练参数调优4.1 类别不均衡的判断与损失函数加权采集西瓜成熟度数据时适熟西瓜往往最容易获取未熟和过熟的样本占比常低于预期。拿到上一章统计出的类别分布表后如果最少的类别数量不到最多的三分之一就需要干预。先直接看一下分布数值再决定策略。成熟度等级建议标注名称图像分类任务中的应用场景未熟unripe底色偏绿条纹紧密适熟ripe底色黄绿过渡条纹舒展过熟overripe底色偏黄表面光泽减弱对类别不均衡的处理有两个层面数据层面做重采样损失函数层面加类别权重。重采样会复制少数类样本虽然简单但会增加过拟合风险加权重则不改变样本数量只在计算损失时放大少数类的梯度贡献。成熟度数据集规模小我优先用类别权重。from sklearn.utils.class_weight import compute_class_weight classes np.array([0, 1, 2]) y_all np.array(labels) # 所有训练样本的标签 weights compute_class_weight(balanced, classesclasses, yy_all) class_weights torch.tensor(weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)compute_class_weight传入balanced时少数类会被赋予更大的权重计算公式是n_samples / (n_classes * bincount)最终各类权重和为类别数。如果训练集只有 120 张过熟样本、400 张适熟样本过熟类得到的权重约为适熟类的 3 倍以上模型在反向传播时会更重视过熟类的预测错误。4.2 三个最值得调整的训练参数小数据集的调参窗口很小改动一个参数影响的是整个损失景观。下面这张表总结了我对「西瓜成熟度图像分类」类任务的首选参数和调整理由参数首选用值调整方向与观察指标学习率1e-4验证损失震荡时减半连续不降时考虑降到 5e-5batch_size32显存允许时可降为 16过小会导致 BN 统计不稳定训练 epoch30~50以验证损失连续 8 轮不降为停止条件batch size 在图像分类里是关键中的关键。小数据集下 batch size 太小时批归一化层的均值和方差估计噪声大训练损失波动剧烈太大时每 epoch 梯度更新次数少420 张训练图一个 epoch 只更新几十次模型不容易收敛。32 对 ResNet18 是一个平衡点。学习率策略用ReduceLROnPlateau而不是固定 step 衰减因为成熟度数据集的验证损失曲线不是规则的下降过程提前设定第几轮衰减容易错过最佳区间。当调度器把学习率降到底部且验证损失仍然横盘时继续 training 只是浪费时间早停是这里最有效的正则化手段。4.3 过拟合信号与常见的错误操作小数据集过拟合来得比大模型更早典型信号是训练集准确率先升到 95% 以上验证集准确率停在一个较低的位置不再上涨。此时不要急着换更强的骨干网络先检查两个问题数据增强是否生效、训练集和验证集是否存在泄漏。增强是否生效的检查方法是把 train_transform 的输出直接保存成图片人工看一眼。RandomRotation(15)在小图上可能把西瓜的边缘旋出画幅生成大量黑色填充区域这类增强后的样本如果占比例过高模型会把注意力浪费在「黑色区域是否存在」这种伪特征上。类别不均衡与误标叠加的场景也值得警惕。过熟类样本少、外观又接近适熟少数被错标的样本在加权损失下会产生异常大的梯度拉偏整个模型的决策边界。遇到验证准确率出现反常下降时我通常先看混淆矩阵找出哪些样本被反复错分再回到原始图片人工确认标注是否正确。5. 把分类结果转化为可执行的成熟度判断5.1 用置信度阈值处理中间档样本经过调优的模型在测试集上通常能给出不错的准确率但实际部署时遇到的最大问题是中间态样本。适熟与过熟的边界在图像上是一条渐变的线模型在面对模棱两可的输入时softmax 输出的概率往往在图之间徘徊比如过熟 0.52、适熟 0.48。此时直接取 argmax 得到标签实际上是在用一个不可靠的预测强制落地决策。更稳的处理方式是引入置信度拒绝机制。设定一个阈值通常取 0.7 到 0.8只有当最高类别的概率超过阈值时才输出判定结果否则输出「不确定需要人工复核」。这个设计在西瓜收购场景里很实用模型拿不准的样本本来就是分拣员也会犹豫的强制二选一反而会打断分拣节奏。阈值搜索可以用下面的脚本在验证集上完成目标是在保证准确率不低于某个下限的前提下尽可能减少被拒绝的样本数。import torch import numpy as np def find_best_threshold(probabilities, true_labels, min_accuracy0.95): best_thr, best_reject 0.0, 1.0 for thr in np.arange(0.5, 0.96, 0.01): accept_mask probabilities.max(axis1) thr if accept_mask.sum() 0: continue acc (probabilities[accept_mask].argmax(axis1) true_labels[accept_mask]).mean() reject_rate 1 - accept_mask.sum() / len(true_labels) if acc min_accuracy and reject_rate best_reject: best_thr, best_reject thr, reject_rate return best_thr, best_reject这段脚本遍历 0.5 到 0.95 之间的阈值以验证集准确率不低于 95% 为硬约束寻找拒识率最低的那个阈值。准确率与拒识率是一对矛盾把阈值从 0.70 提到 0.85被拒绝的样本比例通常会上升几个百分点但剩余的自动判读结果会可靠很多。真实业务中到底偏向哪一端取决于复核成本和错误判定损失的相对大小。5.2 用混淆矩阵定位系统性问题阈值调完最后用测试集做一次全流程验证。把模型在测试集上的预测结果和真实标签生成一个 3×3 的混淆矩阵重点关注两个位置未熟被误判为适熟的比例以及过熟被误判为适熟的比例。前者在交易环节会导致西瓜未熟上市后者会导致西瓜过熟压价这两个方向上的误判代价是完全不同的。如果混淆矩阵显示某一对类别互相混淆的样本量异常集中建议去原始图片中按文件名把那批错分样本找出来逐张看一下。很多时候问题并不在模型参数而是训练集里某些样本的标注本身就违背了数据集定义。部署阶段的模型导出可以直接保存为 ONNX 格式方便用 ONNX Runtime 做推理而不依赖 PyTorch 环境dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export(model, dummy_input, watermelon_maturity.onnx, input_names[image], output_names[prob], dynamic_axes{image: {0: batch}}, opset_version17)dynamic_axes把 batch 维度声明为动态这样实际部署时可以一次传入一张或多张图片。导出后用onnxruntime加载模型测试一次推理输出确认与 PyTorch 原始输出一致再把置信度阈值逻辑写到同一份预测脚本里形成一个最小可用的判别接口。本文还有配套的精品资源点击获取