简介:面向图像分类任务,这份数据集提供已标注的常见果蔬图像,覆盖香蕉、苹果、梨、葡萄、橙子、黄瓜、胡萝卜、辣椒、洋葱、土豆等36个类别,共约4200张图片。json文件保存了36个类别的名称对应关系,图片已预处理,可直接作为分类网络的输入。资源包共2000个文件,包含1998张jpg图像、1个可视化脚本和1个json配置文件,压缩包大小约364.87MB。训练集、测试集、验证集按类别分目录存放,同一类图片集中在一个文件夹,可被常见深度学习框架的数据加载接口直接读取;运行可视化脚本可批量展示样本,快速检查图片质量与标注情况。目前已有119人浏览学习,特别适合从事果蔬识别、图像分类网络改进或模型对比实验的学生和研究人员。借助划分好的数据子集与可视化脚本,使用者可省去大量数据整理时间,把精力集中在网络结构与训练调优上,对快速验证算法效果很有帮助。
1. 4200 张已标注果蔬图,够不够一个图像分类模型的起点
做图像分类的人,第一脚大多踩在公开数据集上。常见那几个动辄几十万张,下载要半天,训练要整夜,跑完才发现自己连数据长什么样都没看清。这个标题里的数据集走的是另一个路子:常见果蔬、多类别、约 4200 张、已标注。规模不大,但胜在干净、聚焦、能快速闭环。对刚接触图像分类的开发者、做毕设的学生、以及想验证一个分类模型思路的工程师来说,它最大的价值不是刷精度,而是让你在一天之内走完“数据读取—模型训练—结果评估”的全流程,把图像分类那套东西真正跑通。这篇笔记我就按这个数据集的体量,把数据怎么组织、模型怎么训、参数怎么设、坑在哪一次说清楚。
2. 先说清楚数据集里到底有什么:类别构成与标注格式决定你后面的每一步
很多人拿到图像分类数据集,第一件事就是解压、开训,结果训到一半发现类别对不上、图片打不开、标注格式和模型不匹配。这些问题根子都在于没先搞懂数据集的内部结构。图像分类数据集虽然看起来简单,就是一堆图片加标签,但它的组织方式直接决定你后面能不能用 PyTorch 或 YOLO 系工具顺利跑起来。4200 张的规模意味着什么?先说结论:这个量级足够训练一个从零开始的浅层 CNN,也足够微调一个预训练的 ResNet 或 MobileNet,但不足以让你从头训出一个大模型。
2.1 类别构成:26 类左右是多类别分类的合理区间,每类 150 张上下是常见分布
“多类别”这个描述在图像分类里通常指 10 类以上、100 类以下。常见果蔬数据集的类别设计一般围绕日常生活,比如苹果、香蕉、橙子、番茄、黄瓜、胡萝卜、土豆、洋葱、青椒、生菜这类高频果蔬,也可能细化到苹果的不同品种。4200 张如果分到 26 类左右,每类大约 150 张出头,这是一个很典型的分布——既不会因为单类图片过少导致模型完全学不到特征,也不会因为类别太杂导致标注成本失控。
这个量级的类别数对模型选型有一个直接影响:如果你的分类目标在 10 到 50 类之间,那 ResNet18、ResNet50、MobileNetV3 这些经典图像分类模型的输出层改一下就能用,完全不需要上 Transformer 结构。如果你拿到手的实际类别数和这个假设差很远,比如只有 5 类或者多达 80 类,那数据增强策略和模型容量都要跟着调,这点在第四章我会展开讲。
拿到数据集后第一件事不是写代码,而是先做一次抽样摸底。我建议你用下面这个命令把目录结构打出来,看看图片文件是不是按类别分文件夹存放的,这是图像分类数据集最常见的组织方式:
# 假设数据集解压在 ./fruits_veg_dataset 目录下 find ./fruits_veg_dataset -maxdepth 2 -type d | sort | head -50 # 统计每个类别文件夹下的图片数量 for dir in ./fruits_veg_dataset/*/; do dir=${dir%*/} echo "$(basename "$dir"): $(find "$dir" -type f | wc -l)" done逻辑说明:第一个 find 命令只看两层目录,确定数据集的根目录下是否直接就是类别文件夹;第二个循环对每个类别文件夹计数图片数量。这两个命令跑完,你心里就有数了——哪些类别图片多、哪些类别图片少,后续在训练时是否需要对少数类做额外处理一目了然。
参数说明:maxdepth 2 是因为标准结构是“根目录/类别名/图片.jpg”,深度为 2。如果你的数据集是“根目录/子目录/类别名/图片.jpg”,改成 maxdepth 3。计数时我用的 find 加 wc -l,没有用 ls -l 是因为图片文件多的时候 ls 会带出额外信息,管道处理也慢。
2.2 标注格式:文件夹命名分类与 CSV 映射是两种主流方案,读取方式完全不同
整理得好的图像分类数据集通常有两种标注格式。第一种是文件夹命名即标签,也就是 2.1 里说的结构,每张图片的位置就隐含了它的类别,这种格式在 PyTorch 里用 torchvision.datasets.ImageFolder 一行就能读进来。第二种是图片文件在一个平铺目录里,另附一个 CSV 或 JSON 文件记录每张图片的文件名和类别 ID,这种格式常见于从数据标注平台导出的结果,需要你自己写读取逻辑做映射。
数据集标题里提到“已标注”,但没有细说标注格式。这个不确定性是你要在项目一开始就确认的:先解压看目录结构,如果打开就是一堆子文件夹,那就是 ImageFolder 风格;如果一堆图片加一个 labels.csv,那就是映射风格。如果你是从标注平台拿到的原始导出,还可能出现每张图对应一个同名 txt 文件的情况,那是检测或分割的格式,稍作转换也能用在分类上。
我一般会建议刚接触图像分类的人优先掌握 CSV 映射格式的处理方式,因为实际工程项目里从标注工具导出的数据多数长这样,而且它对类别的增删改更灵活:
import pandas as pd from PIL import Image import os # 假设 labels.csv 有两列:filename, label df = pd.read_csv("./fruits_veg_dataset/labels.csv") print(df.head()) # 先看一眼列名和内容,避免后续读错列 print(df["label"].value_counts()) # 统计每个类别的样本数量 # 检查前 5 张图片能否正常打开,这一步能提前发现损坏文件 for fname in df["filename"].head(5): path = os.path.join("./fruits_veg_dataset/images", fname) img = Image.open(path) print(fname, img.size, img.mode)逻辑说明:这段代码先用 pandas 读入标注文件并输出类别分布,确认数据是否均衡;然后遍历前几张图片确认路径拼接正确、文件能打开、图像尺寸和通道模式正常。损坏的图片在训练时会导致 DataLoader 报错或产生无效梯度,提前检查能省下不少排查时间。
参数说明:Image.open 之后最好调用 img.verify() 方法做完整校验,因为 open 本身是惰性的,只读文件头,不真正解码像素数据。如果图片文件只有几百字节的残留,open 可能不报错,verify 才报。另外注意 path 拼接用的是 os.path.join,而不是字符串加号,因为在 Linux 和 Windows 下分隔符不同,join 会自适应。
2.3 4200 张数据的数学账:训练集能分到多少、模型容量多大合适
4200 张的总量,按常见的 8:1:1 划分,训练集约 3360 张,验证集和测试集各约 420 张。3360 张训练图去拟合一个 1000 类的 ImageNet 预训练模型,直接全量微调会很容易过拟合。但如果你把预训练模型当作特征提取器,只训练最后的全连接分类层,这个数据量又显得绰绰有余。这是这个规模的数据集最核心的定位:它适合做迁移学习微调,不适合从零训练。
模型选型上,我强烈建议以 ResNet18 或 MobileNetV3 为起点,而不是一上来就上 ResNet50 或 EfficientNet-B4。原因很朴素:数据量越小,模型容量越要克制。ResNet18 参数量约 1100 万,ResNet50 约 2500 万,在 3000 多张训练图上,ResNet50 的过拟合风险明显更高,但精度提升却有限。先用小模型跑通流程,再逐步加大,这是图像分类项目里最稳妥的路径。
如果将来自动化标注工具或采集渠道加入,数据量扩充到 1 万张以上,再换 ResNet50 也不迟。数据集里如果包含同一类果蔬的多个品种或不同成熟度,模型需要学习的特征维度会更高,那时候 4200 张对某些难分对来说就略显紧张,需要靠数据增强和类别权重来弥补。记住一点:在数据量不动的前提下,调模型结构的边际收益远低于调数据处理和训练策略的收益。
3. 用 PyTorch 把 4200 张果蔬图跑进训练管线:迁移学习脚本与参数设定
数据摸清了,接下来就是把数据集真正送进模型训练。这一步我不会给你贴一个几百行的完整工程代码,而是拆成“数据加载—模型改造—训练循环—参数配置”四个环节。每个环节单独聚焦,你可以直接复制拼起来用。环境默认是 Python 3.9 以上、PyTorch 2.x、GPU 显存 6GB 以上。如果你的机器没有 GPU,也能跑,只是时间会长很多,后面参数部分我会给对应的调整方案。
3.1 数据读取与划分:用 ImageFolder 搭一个可复用的 DataLoader
PyTorch 的 torchvision.datasets.ImageFolder 是图像分类数据加载的首选工具,前提是你的数据集是“根目录/类别名/图片.jpg”结构。如果你的数据集是 CSV 映射格式,先做一个预处理脚本把图片按类别移动进对应文件夹,或者直接写一个自定义 Dataset 类。对大多数情况来说,ImageFolder 足够用,而且自带类别名到索引的映射,少写不少代码:
import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms import os data_dir = "./fruits_veg_dataset" # 三个关键操作:缩放、数据增强、转张量 transform_train = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转增强 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 亮度/对比度扰动 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_val = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) dataset = datasets.ImageFolder(root=data_dir, transform=transform_train) train_size = int(0.8 * len(dataset)) val_size = int(0.1 * len(dataset)) test_size = len(dataset) - train_size - val_size train_ds, val_ds, test_ds = random_split( dataset, [train_size, val_size, test_size] ) # 注意:val 和 test 也要套用验证集 transform val_ds.dataset.transform = transform_val test_ds.dataset.transform = transform_val train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4) print(f"类别数: {len(dataset.classes)}") print(f"训练集: {len(train_ds)}, 验证集: {len(val_ds)}, 测试集: {len(test_ds)}")逻辑说明:transform_train 里的 RandomHorizontalFlip 和 ColorJitter 属于数据增强,它们在每次训练迭代时对图片做随机变换,相当于帮模型多看一些变体,抑制过拟合。Resize 到 224x224 是 ResNet 系列标准输入尺寸,Normalize 用的均值方差是 ImageNet 预训练模型的统计值,迁移学习时不要改。random_split 是随机切分,但切分后要手动把 transform 改成验证集用的 transform,否则验证时也会做随机增强,导致评估指标不稳定。
参数说明:batch_size=32 是一个比较稳妥的起点,显存 6GB 的卡能跑,10GB 以上可以考虑 64。num_workers=4 在 Windows 上如果报多进程错误,改成 2 或 0,Windows 的 DataLoader 多进程支持不如 Linux 稳定,这是踩出来的经验。随机增强的 p=0.5 表示一半概率翻转,不要设成 1.0,否则所有图片都翻转过,模型会误以为“朝左”是某种类别的固定特征。
3.2 模型改造:把 ResNet18 的分类头换成你的果蔬类别数
迁移学习的思路很简单:在 ImageNet 上训练好的模型已经学会了通用的纹理、边缘、形状特征,你要做的是把最后一层分类输出换成自己的类别数量,然后选择性地训练所有层或只训练最后一层。4200 张这个量级,我建议两个策略都试一下:先只训练分类头,看基线准确率;再解锁全部层做微调,看能提升多少。后者时间成本高,但往往能带来 5 到 10 个百分点的提升。
import torch.nn as nn import torchvision.models as models def build_model(num_classes, pretrained=True, freeze_backbone=True): # 使用 ResNet18 作为骨干网络 model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT if pretrained else None) in_features = model.fc.in_features # ResNet18 这里是 512 # 替换最后一层全连接 model.fc = nn.Sequential( nn.Dropout(p=0.2), nn.Linear(in_features, num_classes) ) if freeze_backbone: # 冻结所有卷积层参数,只训练 fc 层 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True return model model = build_model(num_classes=26, freeze_backbone=True) print(model.fc) # 确认分类头结构 # 统计可训练参数量,验证冻结是否生效 trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) total = sum(p.numel() for p in model.parameters()) print(f"可训练参数: {trainable / 1e6:.2f}M / 总参数: {total / 1e6:.2f}M")逻辑说明:冻结骨干网络后,反向传播只更新最后两层,训练速度快、需要的数据量少,适合第一轮跑基线。Dropout 加在分类头里能进一步抑制过拟合,p=0.2 是相对保守的设定,因为数据量本来就不大。pretrained=True 时 PyTorch 新版本要求用 weights 参数而不是 pretrained 参数,ResNet18_Weights.DEFAULT 会下载官方在 ImageNet 上的权重。
参数说明:freeze_backbone 设为 False 就是全量微调。全量微调时学习率要调低,1e-4 到 3e-4 之间,比只训练分类头用到的 1e-3 小一个量级。原因是预训练权重已经在一个很好的位置,学习率太大会把学到的通用特征破坏掉,这在迁移学习里叫灾难性遗忘。Dropout 的 p 值不建议在冻结模式下调太高,因为分类头本身参数少,p=0.5 反而可能欠拟合。
3.3 训练循环:选对损失函数和学习率,前面一半功夫不白费
多类别图像分类的损失函数没有悬念,就是交叉熵。PyTorch 里 CrossEntropyLoss 会自己处理 Softmax,你只需要把模型的原始输出和整数标签喂进去。优化器我习惯用 AdamW,在图像分类任务上比普通 SGD 收敛更平稳,对学习率的敏感度也低一些。学习率调度器用一个简单的 CosineAnnealingLR 就很好,它会自动让学习率在训练后期逐步降低,帮助收敛到更平稳的极值点。
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-6) epochs = 20 best_acc = 0.0 for epoch in range(epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_loss = running_loss / total train_acc = correct / total # 验证集评估 model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_acc = val_correct / val_total scheduler.step() if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_fruit_model.pth") print(f"Epoch {epoch+1}/{epochs} | " f"Loss: {train_loss:.4f} | " f"Train Acc: {train_acc:.4f} | " f"Val Acc: {val_acc:.4f} | " f"LR: {optimizer.param_groups[0]['lr']:.2e}")逻辑说明:训练循环里有两个细节容易被忽略。第一,模型在训练和验证之间要切换 model.train() 和 model.eval(),因为 BatchNorm 和 Dropout 在两种模式下的行为不同,不切换会导致验证集结果不稳定。第二,验证阶段用 torch.no_grad() 关闭梯度计算,内存占用小、速度快,反向传播在验证时不需要。best_acc 逻辑记录验证集上表现最好的权重,后面测试集和实际推理都用它。
参数说明:T_max=20 要和 epochs 保持一致,这样学习率从初始值余弦下降到 eta_min。weight_decay=1e-4 是 L2 正则化,防止权重过大导致过拟合,但如果你的分类头只有两层,这个值可以调大一点到 1e-3。如果你没有 GPU 或者显存只有 4GB,把 batch_size 降到 16,epochs 增加到 30,学习率降到 5e-4,也能收敛,只是慢一些。
3.4 训练结果的判断基准:4200 张数据集跑到什么程度算及格
训练结束后,你需要一个客观的参照系。纯随机猜是 26 类的 1/26,约 3.8%。只训练分类头的迁移学习,通常能跑到 85% 以上的验证准确率。全量微调后,正常应该能到 92% 到 96%。如果你的验证准确率低于 80%,问题大概率不在模型,而在数据处理或者标注质量上,直接跳到第四章排查。如果到了 96% 以上,先别高兴太早,要检查是不是数据划分出了问题,比如同一个来源的图片被同时分进了训练集和验证集,导致评估虚高——这个坑在 4.4 里我详细说。
4. 多类别图像分类的避坑清单:5 个让新手翻车的典型问题
这一章我按“现象 → 原因 → 解决”的顺序写几条最常踩的坑,每一条都是自己或身边人真实遇到过的。这些坑对几万张的大数据集影响相对小,但在 4200 张这个量级上,每一条都可能直接决定你的项目是顺利完成还是卡死两周。
4.1 验证集准确率很高,但测试集上表现崩塌:问题出在随机划分
现象:验证集准确率到了 94%,你高高兴兴去跑测试集,结果只有 70%。差别大得离谱。原因:随机划分数据时,同一个拍摄批次、同一角度、同一光照条件下的图片被同时分到了训练集和验证集,验证集参与了训练分布,评估结果虚高。这个问题在果蔬数据集上特别明显,因为采集时往往是同一批果蔬一起拍摄的。解决:按拍摄批次或场景做划分,而不是完全随机。如果数据集没有批次信息,一个折中办法是先用聚类特征对图片分组,再按组划分。
import numpy as np from sklearn.cluster import KMeans from PIL import Image # 用简单颜色直方图做分组,避免同源图片跨集合 def image_signature(path, bins=32): img = Image.open(path).convert("RGB").resize((64, 64)) arr = np.array(img) # 分通道计算颜色直方图,拼成一个特征向量 hist = [np.histogram(arr[:, :, c], bins=bins, range=(0, 256))[0] for c in range(3)] return np.concatenate(hist) # 给每张图片生成签名向量(示例:只取前 50 张,实际要全量) sample_files = ... # 你的图片路径列表 signatures = np.array([image_signature(f) for f in sample_files[:50]]) kmeans = KMeans(n_clusters=5, random_state=42) groups = kmeans.fit_predict(signatures)逻辑说明:这个思路是把相似的图片聚到同一组,训练集和验证集分别从不同的组里取,降低数据泄漏风险。颜色直方图只是最简单的特征表达,实际使用时可以用神经网络中间层特征替代,效果更好,但复杂度也高。对 4200 张图来说,颜色直方图基本够用,果蔬类别的背景和主色调差异较大,聚类出来的分组和拍摄批次往往接近。参数说明:bins=32 是颜色直方图的粒度,越大特征越细但抗噪能力越差;n_clusters=5 是假设数据约有 5 个拍摄场景,你根据实际情况调整。KMeans 的 random_state 固定下来,保证结果可复现。
4.2 训练损失不降反升,伴随大量 NaN:学习率太大和坏图片在作怪
现象:训练到第三个 batch,loss 变成 nan,然后一路 nan 到底,准确率也变成 0。原因一般有两个,一是学习率太高导致梯度爆炸,二是数据里混入了损坏的图片文件。我在 2.2 里强调过用 verify 检查图片,就是为了堵住这个坑。解决:先降低学习率试一次,如果还是 nan,批量检查 DataLoader 里有没有坏图片。
from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True # 允许加载截断的图片 # 遍历所有图片做完整解码校验 import os for root, dirs, files in os.walk("./fruits_veg_dataset"): for fname in files: if fname.lower().endswith((".jpg", ".jpeg", ".png")): path = os.path.join(root, fname) try: img = Image.open(path) img.verify() # 验证文件完整性 except Exception as e: print(f"损坏文件: {path} -> {e}")逻辑说明:ImageFile.LOAD_TRUNCATED_IMAGES = True 是让 PIL 在遇到文件截断时尝试尽力解码,避免直接抛异常中断训练。verify 方法会真正读取文件内部结构,发现文件头正常但数据损坏的图片。这段遍历脚本会打印所有问题文件路径,你核对后决定是删除、替换还是保留。参数说明:遍历时用 os.walk 而不是 glob,是因为它能递归处理任意深度的目录,和 2.1 里的 find 命令互补,一个在命令行快速摸底,一个在 Python 里彻底检查。
4.3 训练集准确率 99%,验证集只有 75%:经典过拟合,增强没到位
现象:训练集准确率一路飙升到 99%,验证集卡在 75% 上下不再动。原因:4200 张的数据量不大,模型把训练集的背景、颜色分布都背下来了。解决分两步,第一步增强数据变换力度,第二步给模型结构做瘦身或加强正则化。
增强手段我常用的顺序是:随机旋转(±15 度)、随机裁剪(ResizedCrop 到 224 再缩放)、随机光照扰动。果蔬分类的场景里,旋转和裁剪是最贴近真实分布的增强方式,因为水果放在桌面上的角度本来就不固定。要注意增强不能过度,比如随机旋转到 90 度以上,会把苹果和橙子这种圆形水果变成很难辨认的倾斜状态,反而增加学习难度。一个常见的配置是 rotation=15,这种小角度旋转对绝大多数果蔬类别都是安全的。
正则化方面,除了 3.3 里的 weight_decay 和 Dropout,还可以在训练中引入标签平滑(label smoothing)。原理是把 one-hot 标签中的 1 改成 1-ε,剩余 ε 均匀分给所有类别。这样模型不会对训练集的某个样本过于自信,等于变相给数据加了噪声,对抑制过拟合有实际帮助。PyTorch 里 CrossEntropyLoss 直接支持 label_smoothing 参数,设成 0.1 就行。
4.4 某些类别准确率异常高、另一些始终为 0:类别不平衡和标注噪声
现象:训练完看分类报告,苹果、香蕉这些大类的 F1 分数很高,但某个小众类别,比如猕猴桃,准确率和召回率全是 0。原因可能是这个类别的图片数量太少,模型没见过足够的正样本;也可能是这个类别的标注本身有问题,比如标签错了,模型学到的是错误映射。解决:先看类别计数,如果某类少于 50 张,就要考虑用类别权重补偿或做过采样。
from sklearn.utils.class_weight import compute_class_weight import numpy as np # 获取所有样本的标签索引 all_labels = [dataset.targets[i] for i in range(len(dataset))] # 这行代码假设 dataset.targets 存在,ImageFolder 会自动生成 class_weights = compute_class_weight( class_weight="balanced", classes=np.unique(all_labels), y=all_labels ) weight_tensor = torch.tensor(class_weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=weight_tensor)逻辑说明:compute_class_weight 会把样本数少的类别权重调高,样本数多的类别权重调低,这样损失函数对少数类的分类错误更敏感,模型在更新参数时会更关注少数类。class_weight="balanced" 是自动计算方式,它的公式是样本总数除以类别数再除以该类样本数,不需要手动指定。参数说明:如果你的某个类别样本数极少,比如只有 20 张,权重补偿的效果也有限,这时候最有效的手段是去补充数据,或者用这个类别的所有图片做基础变换扩增到至少 100 张。
4.5 使用标注工具时踩的坑:LabelImg 画完矩形框却导不出分类标签
现象:你想用标注工具先清洗或扩展这个数据集,结果发现某些标注工具导出的是目标检测格式(VOC XML、YOLO txt),不是分类的文件夹结构。原因:图像分类数据集的标注和检测标注是两回事,检测需要画框,分类只需要给整张图打一个标签。解决:分类数据集的标注工作不建议用画框工具,直接按文件夹整理图片是最快的,配合 labelstudio 或 x-anylabeling 这类支持分类标注的工具逐张打标。
提示:如果后续你想把这份果蔬分类数据集升级成目标检测数据集,再考虑用 LabelImg 或 CVAT 对图片画框。分类和检测是两条路线,训练时的损失函数和数据加载方式完全不同,不要混着做。
5. 模型训完怎么验证才算数:混淆矩阵、置信度抽查与少数类复查
训练跑完、准确率看着不错,别急着收工。图像分类模型的验证不只是看一个总体准确率数字,还要搞清楚它在哪里犯错、犯什么错、错误是否集中在某些特定类别上。对 4200 张的果蔬数据集来说,最终交付的模型往往用在自动化分拣、商品识别这类场景,一个系统性的错误(比如把绿番茄全认成青椒)比随机几个零星错误严重得多。做两类检查:一是汇总层面的混淆矩阵,二是个体层面的置信度抽查。
import matplotlib.pyplot as plt import itertools from sklearn.metrics import confusion_matrix def plot_confusion_matrix(model, loader, class_names, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) plt.figure(figsize=(12, 10)) plt.imshow(cm, interpolation="nearest", cmap="Blues") plt.title("Confusion Matrix") plt.colorbar() tick_marks = range(len(class_names)) plt.xticks(tick_marks, class_names, rotation=45, ha="right") plt.yticks(tick_marks, class_names) plt.xlabel("Predicted") plt.ylabel("True") plt.tight_layout() plt.show() return cm逻辑说明:混淆矩阵展示的是“真实类别”和“预测类别”的交叉统计。对角线上的数值越大说明这个类别的识别越可靠;对角线外的非零块说明模型在哪些类别之间产生了系统性混淆。果蔬分类里最常见的混淆是外观相近的品类,比如番茄和圣女果、青椒和尖椒、橘子和橙子。这类混淆如果占比高,可能说明类别定义本身对模型来说区分度不足,需要考虑合并类别或者在标签时引入更细的子类层级。
参数说明:cmap="Blues" 只是配色方案,换成 "YlOrRd" 或 "Greens" 都可以,不影响数据。rotation=45 保证类别名显示不重叠,类别名过长时可以改成 90。confusion_matrix 返回的是一个二维 numpy 数组,你可以在这个基础上计算每个类别的召回率,代码里用 cm[i, i] / cm[i, :].sum() 就能得到第 i 类的召回率,那个值比总体准确率更能反映少数类情况。
接着做置信度抽查。随机选一批图片,让模型给出每个类别的预测概率,找到那些预测置信度最高但预测错误的样本,这些往往是标注噪声或者图片本身模糊导致的。你把这几个样本拿出来人工看一眼,如果图片本身没问题但模型就是信誓旦旦地分错,说明这个类别的训练特征没有学到本质,需要补充该类的训练图片而不是调模型。这个过程不需要额外写复杂代码,在测试循环里把 top-1 概率和真实标签一起保留下来,然后用排序找出置信度最高且预测错误的样本即可。
至于最后的落地部署,4200 张训练出来的模型建议导出成 TorchScript 或 ONNX 格式,比直接保存 PyTorch state_dict 更稳定,不依赖 Python 环境。验证模型是否真正可用,拿几张训练集之外、真实场景里拍的图片跑一次推理,看输出概率分布是否合理。这一步能绕过所有可视化指标,直接暴露模型在实战中的表现。我自己的习惯是每轮训练完都会保留一份包含验证集上错误样本的 CSV 文件,里面记录文件名、真实标签、预测标签和置信度。下次不管是调整数据还是调模型,翻开这个文件就能看到上次的问题在哪,不用重新跑一遍推理。这算是我做图像分类项目最值得推荐的一个习惯,希望帮到你。
本文还有配套的精品资源,点击获取