24类商品图像数据集实战:PyTorch ResNet18图像分类避坑指南 简介面向图像分类任务的中文二十四类商品图片数据集覆盖手机、化妆品、酒水等常见商品类别为初学者与中级开发者提供可直接投入训练的图像样本适用于模型训练、效果评估及相关分类项目实践。资源包共含两千个文件以一千九百九十八张商品图片为主体另附带一个可视化脚本与一份配置文件压缩包大小约六百二十四兆解压后数据总量约九百四十五兆目前已有二百三十七人浏览学习。数据已完成训练集与测试集划分训练集包含两万七千五百六十六张图片测试集包含六千八百八十一张图片各商品类别按文件夹独立存放无需额外处理即可借助常规图像加载工具直接读取。附带脚本无需修改即可运行随机传入一张图片即可展示样本并保存至当前目录便于快速核对各品类图像质量、降低数据检查成本。整体目录结构清晰是一份可直接用于商品识别与图像分类任务的高质量数据集素材。1. 24 类商品图像数据集划分好的分类数据才是真正能直接开工的数据做图像分类的人都有过这种经历从某个渠道找到一组商品图内容合适但标签乱、数量不均最麻烦的是训练集和验证集混在一起得自己花半天去重、去补标签、再按比例切分。所以当我看到一个已经完成数据集划分的 24 种商品图像数据集时第一反应不是“图片清不清楚”而是“划分是否可靠”。本篇文章围绕的就是这类数据集24 类常见商品的图像集合按类别整理、已完成训练/验证/测试划分可直接交给分类模型使用。适合正在做商品识别、货架巡检或零售场景分类的开发者也适合想跑通一套标准图像分类流程的初学者。先别急着训练把数据集的划分逻辑、目录结构和隐藏的坑摸清楚后面能少走很多弯路。2. 先看清 24 类商品是什么类别构成、命名规则与划分比例2.1 24 类商品的类别清单与文件夹命名拿到这类数据集第一件事不是看图片而是看顶层目录里有哪些文件夹。文件夹名基本就是类别名每个文件夹对应一类商品。常见的一种组织方式是类别文件夹直接放在 train / val / test 下面也就是“训练集/验证集/测试集 各自下辖 24 个类别文件夹”。我按这类数据集的常见构成整理一份类别清单供对照编号类别名示例商品说明00water瓶装水01cola碳酸饮料02chips薯片03instant_noodles方便面04biscuit饼干05chocolate巧克力06yogurt酸奶07juice果汁饮料08laundry_detergent洗衣液09dish_washing_liquid洗洁精10shampoo洗发水11body_wash沐浴露12toothpaste牙膏13toothbrush牙刷14facial_tissue抽纸15toilet_paper卷纸16adhesive_tape透明胶带17folder文件夹18notebook笔记本19gel_pen中性笔20battery电池21led_bulbLED 灯泡22data_cable数据线23charger充电器注意我用的类别名是英文小写加下划线这是图像分类数据集里最常见的做法。为什么不直接用中文因为多数深度学习框架的 ImageFolder 类、以及 Linux 环境下的路径处理对中文路径支持虽然不至于报错但在跨平台复制、打包、部署时容易出编码问题。文件夹名即类别名读取时再映射成中文展示这是最稳的方案。每个文件夹里是同一类商品在不同光照、不同背景、不同角度下的图片。图片格式以 jpg 为主也有少量 png。这类数据集通常不做统一分辨率常见边长在 200 到 600 像素之间训练时用 DataLoader 统一 resize 即可。至于每类样本量不要只看说明文档写“每类约几百张”拿到手先自己统计。2.2 划分比例与划分方式8:1:1 还是 7:2:1“已做数据集划分”是这个标题的价值核心。划分逻辑通常是按比例对每一类单独切分保证三个子集里 24 个类别都存在。常见比例有两种8:1:1 和 7:2:1。前者训练数据更多适合底子薄、想多喂数据的情况后者验证集更大指标更稳适合用来调参和早期判断是否过拟合。如果数据集里附带划分脚本或说明文件以实际标注为准没有的话按时默认按 8:1:1 理解即可。这里有一个容易忽略的细节划分的粒度是“按文件划分”还是“按图片划分”。两者看似一样实际上如果同一个商品实体被拍摄了多张连续帧按文件随机划分可能把同一实体的多张图同时放进训练集和验证集造成验证集指标虚高。严谨的做法是按“商品实例”或“拍摄批次”划分但多数公开数据集不会做到这步。另外要注意一个比例陷阱类别不均衡。24 类商品里通常有几类特别好拍、数量特别多比如瓶装水有类别数量特别少比如数据线。总量按 8:1:1 切完之后少数类在验证集里可能只有 20 张一个 batch 就能覆盖评估时波动极大。后面我会专门讲这个坑的处理方式。2.3 划分的意义省掉的不仅是时间还有随机切分的偏差很多人觉得“自己用 sklearn 切一下就完事”这个想法在数据量大时问题不大但在商品图这种单类样本可能只有几百张的数据集上会有明显偏差。随机切分不按类别分层的话很可能出现某个类别在训练集有 180 张、验证集只有 5 张的情况更严重的是如果没有先做去重同一个商品在不同文件名的图片会被分到两边训练时模型见过的那张图又在验证集里出现评估结果虚高却不自知。所以这个数据集最大的价值是划分已经完成而且划分的基本单位是“已经在整理时做过去重的图片集”。我能做的第一件事不是质疑它的划分方式而是先用脚本验证一遍每个子集是否包含完整的 24 个类别、每类数量和整体比例是否合理、是否存在空目录。这一步既是对数据集的体检也是后续所有实验的基准。3. 把划分好的数据集读进来ImageFolder 加载、统计与三项自检3.1 目录树结构与路径约定一个典型的数据集目录结构如下24class_goods/ ├── train/ │ ├── water/ │ ├── cola/ │ ├── chips/ │ └── ...其余类别 ├── val/ │ ├── water/ │ ├── cola/ │ └── ... └── test/ ├── water/ ├── cola/ └── ...这种结构是 PyTorch 里 ImageFolder 能直接读的标准形态。train/val/test 三个目录下各自维护一份完整的类别子目录不需要额外的 CSV 映射文件。如果数据集里附带一个 label.txt 或 classes.txt那一般是说明类别顺序的读取时要和文件夹实际名字做交叉验证。我一般会把数据集路径放到一个单独的配置变量里不要散落在代码各处。后续所有脚本都用这个变量避免路径写错反复返工。另外macOS 的 .DS_Store、Windows 的 Thumbs.db 这类系统文件偶尔混进文件夹加载前最好过滤掉。3.2 用脚本统计每个子集的类别数量与分布拿到数据集先跑一段统计脚本这是最值得花的三分钟。脚本输出每个子集的类别数、总图片数、每类数量以及最小类和最大类的倍数差。import os from collections import Counter data_root ./24class_goods subsets [train, val, test] for subset in subsets: subset_path os.path.join(data_root, subset) if not os.path.exists(subset_path): print(f[缺失] {subset} 目录不存在) continue class_counter Counter() total 0 for class_name in os.listdir(subset_path): class_dir os.path.join(subset_path, class_name) if not os.path.isdir(class_dir): continue # 只统计 jpg/png/jpeg过滤系统文件 valid_exts (.jpg, .jpeg, .png) count sum(1 for f in os.listdir(class_dir) if f.lower().endswith(valid_exts)) class_counter[class_name] count total count print(f[{subset}] 类别数: {len(class_counter)}图片总数: {total}) for class_name, count in class_counter.most_common(): print(f {class_name}: {count}) if class_counter: max_count max(class_counter.values()) min_count min(class_counter.values()) print(f 最大类/最小类倍数: {max_count / min_count:.2f})这段脚本的作用有四个确认 24 个类别是否齐全、确认每个子集都有数据、确认类别不均衡的程度、发现可能混入的非图片文件。逻辑上先遍历子集目录再用扩展名过滤图片最后统计分布。看结果时重点关注两点一是某个类别是否在某个子集中缺失二是最大类和最小类的倍数是否超过 5 倍。这两点直接决定后面训练策略。3.3 三项自检类别完整性、图片可读性、路径一致性统计脚本只能说明“文件存在”不能说明“文件能读”。图片分类训练中最隐蔽的错误之一就是数据里混入损坏的 JPEG文件存在且大小正常但解码时报错训练跑到一半中断。所以我会对数据集做三项自检。第一项是类别完整性检查直接用 Python 集合比较 train、val、test 三者的类别集合是否完全一致。任何不一致都说明划分出了问题模型训练时类别数和评估时对不上。第二项是图片可读性检查用 PIL 逐个打开图片并验证尺寸是否大于零。遇到打不开的文件记录下来并从统计中剔除。这一步在小数据集上只需要几十秒但能避免训练中途因坏图崩溃。from PIL import Image def verify_image(path): try: img Image.open(path) img.verify() # 只校验格式不加载全图 return True except Exception as e: print(f[坏图] {path}: {e}) return False # 用法示例对 train 下的每个类、每张图执行 verify_image第三项是路径一致性检查确认没有图片在 train 和 val 中重复出现。做法是对每张图计算文件名的 md5或者更简单一点记录所有文件名的列表检查交集。文件名重复不等于内容重复但出现重复文件名至少说明划分时没有做重命名处理需要进一步抽查。4. 用 PyTorch 跑通基线ResNet18 微调的参数与训练循环4.1 模型选型从 ResNet18 起步的理由24 类商品分类是小规模图像分类任务这个规模下没必要上来就上大模型。ViT、大卷积网络在这个量级上优势不明显反而更容易过拟合训练时间却成倍增加。我一般从 ResNet18 或 ResNet34 起步。商品图的特征比较明确颜色、形状、包装上的文字和 logo这些中低层视觉特征 ResNet18 完全能捕捉。更关键的是预训练权重。ImageNet 上预训练的 ResNet18迁移到商品分类这类任务上哪怕数据量只有几千张也能快速收敛到可用的精度。商品图和 ImageNet 的自然图像分布不完全一致但底层的边缘、纹理、颜色特征是可复用的。如果从零训练24 类几千张图通常不够除非你另外做高强度数据增强。也可以考虑 EfficientNet-B0 作为对比选项它的参数量和精度在同等资源下比 ResNet18 略好但对输入分辨率更敏感训练时容易因为分辨率不足而表现不佳。我的习惯是先跑 ResNet18把整个流程打通再对比 EfficientNet-B0花的时间不多但能确认自己数据的特性。4.2 最小训练脚本数据加载、增强与训练循环有了划分好的目录结构训练脚本写起来非常短。这里给出一份能直接跑的脚本数据集路径换成你自己的即可。import torch import torch.nn as nn # 网络层、损失函数和优化器入口 from torch.utils.data import DataLoader from torchvision import datasets, transforms, models data_root ./24class_goods batch_size 32 num_epochs 15 num_classes 24 device cuda if torch.cuda.is_available() else cpu # 训练集加数据增强验证集和测试集只做尺寸调整 train_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder( rootf{data_root}/train, transformtrain_transforms) val_dataset datasets.ImageFolder( rootf{data_root}/val, transformval_transforms) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers4) # 加载预训练权重替换最后一层全连接 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(num_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 每个 epoch 结束后在验证集上评估一次 model.eval() correct 0 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) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / len(train_dataset) val_acc correct / total print(fEpoch {epoch1}/{num_epochs} ftrain_loss{train_loss:.4f} val_acc{val_acc:.4f})这段脚本里有几个参数需要注意。batch_size 取 32商品图比较规整、ResNet18 在 224×224 输入下显存需求不大大部分显卡都能跑如果你的显存只有 4G改成 16。学习率用 1e-4这是微调预训练模型的常见起步值不要直接用 1e-3容易把预训练权重冲坏。RandomCrop 和 ColorJitter 是商品图分类最有效的两个增强前者模拟不同视角后者模拟光照变化注意验证集不要加入任何随机增强保证评估稳定。4.3 关键参数怎么调冻结层、学习率与早停策略这套脚本跑通之后正确率通常已经能到一个“能用的基线水平”但还有三个参数值得细调。第一是冻结层的选择。上面脚本没有冻结任何层全部参数都在更新。如果数据量很少可以只训练最后一层全连接把前面所有卷积层冻结这时学习率可以用 1e-3。做法是把卷积层参数的 requires_grad 设为 False只让 fc 层可训练。这种方式适合数据量不到 5000 张的情况能有效防止过拟合。第二是学习率的衰减策略。15 个 epoch 里全程用 1e-4后期往往会有波动。常见做法是用 StepLR 或 ReduceLROnPlateau在验证集正确率连续两个 epoch 不升时把学习率降到原来的五分之一。这会费一点时间但通常能再提升零点几个百分点。第三是早停。训练收敛后继续硬跑验证集指标不升反降是典型过拟合信号。我在验证脚本里会记录最佳验证集正确率连续 3 个 epoch 没有刷新就提前停止并保存当时那组权重。这个习惯能省时间也避免把“过拟合后的权重”当作最终模型。5. 避坑清单加载、归一化、shuffle 与标签错位导致的 5 个隐蔽问题5.1 类别标签顺序不固定sorted 排序与自定义类别表冲突现象训练时 loss 正常下降验证集正确率却一直在 4% 左右徘徊24 类随机水平排查代码没有问题数据也能正常加载。原因ImageFolder 按文件夹名的字符串排序生成类别索引。如果文件夹名是 “cola”“water” 这类按字母序排但数据集附带说明里标注的标签顺序是按品类逻辑排的比如先把饮料类放一起两者不一致模型输出的类别编号就和你的预期对不上评估脚本按错误映射计算正确率自然接近随机水平。解决以实际目录结构为准打印train_dataset.class_to_idx确认顺序不要相信外部文档写的标签序号。所有评估脚本使用同一个字典做映射。5.2 验证集用了训练增强结果虚高且每次结果不一致现象验证集正确率很高但把同一批图片再测一次正确率有波动而且每次跑出来不一样。原因为了“涨点”把 RandomHorizontalFlip、RandomCrop 等增强同时应用到了训练集和验证集。验证集每次随机变换后都产生新样本模型对不同变换的适应能力不同导致多次评估之间出现抖动。如果同时用 RandomResizedCrop还会出现验证集图片每次裁剪位置不同、信息量不同的情况指标根本不具备可比性。解决训练集随机增强、验证集只做 Resize 和 CenterCrop。评估必须确定性可复现不能有随机因素。这是对比实验和调参的基本原则。5.3 归一化参数和预训练权重不匹配平均亮度被扭曲现象用预训练 ResNet18 加载自己的图片训练一开始 loss 异常高训练很久都不降。原因预训练模型默认输入是 ImageNet 的通道顺序RGB和归一化参数mean 和 std如果数据增强流程里漏写transforms.Normalize或者把 mean/std 写错、通道顺序搞错比如把 BGR 当 RGB预训练权重的第一层统计分布和实际输入就对不上模型看到一个“被扭曲”的输入分布。解决使用预训练权重时归一化参数严格采用Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])。另外商品图里很多是白底、浅色底色图片归一化之后均值确实是偏低的这属于正常现象不要因此去改归一化参数。5.4 类别不平衡导致整体正确率虚高现象训练结束后整体正确率 94%看混淆矩阵却发现电池这类只有 60%矿泉水类却接近 100%。整体指标被样本量大的类别“稀释”了。原因这个数据集里常见的类别不均衡个别类样本数量可能是另一些类的五倍以上。模型训练时学到的是让整体 loss 最小天然偏向多数类。最终评估时多数类占比大即使少数类全错整体正确率也不会太低。解决看指标时同时输出每个类别的召回率和混淆矩阵不要只看整体正确率。训练时还可以给少数类加权把CrossEntropyLoss的weight参数设为样本量倒数的归一化结果或者直接用pytorch的WeightedRandomSampler重采样训练数据。5.5 坏图与异常通道数中途崩溃或通道数对不上现象训练中途dataloader报ImageOpenError或valueError在某个 epoch 的某个 batch 处反复崩换不同 batch_size 时崩溃位置不同。原因数据集中混有损坏图片文件头不完整、尺寸为 0还有部分是 RGBA 四通道 PNG标准化到 RGB 三通道时出错。另有一种少见情况是黑白图单通道灰度图在ToTensor之后变成 1 通道与模型输入要求不符。解决在训练前统一跑一遍预处理脚本遇到打不开的文件或通道数不等于 3 的图片复制到一个单独目录留档并从原目录移除RGBA 图统一转成 RGB灰度图转成三通道复制。这个脚本每份数据集只跑一次花费的时间远小于训练中途排查问题的成本。6. 进一步榨精度混淆矩阵定位分不清的商品再决定调模型还是调数据基线跑通之后下一步不是盲目堆数据和调参而是先搞清楚模型到底在哪些类别上犯错。我会生成混淆矩阵按类别统计每个真实类被预测成了哪些其他类。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().tolist()) all_labels.extend(labels.tolist()) class_names list(val_dataset.class_to_idx.keys()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namesclass_names, digits3)) # 找出被误分次数最多的前 5 对类别 error_pairs [] for true_idx in range(len(class_names)): for pred_idx in range(len(class_names)): if true_idx ! pred_idx and cm[true_idx][pred_idx] 0: error_pairs.append((class_names[true_idx], class_names[pred_idx], cm[true_idx][pred_idx])) error_pairs.sort(keylambda x: -x[2]) for true_name, pred_name, count in error_pairs[:5]: print(f{true_name} - {pred_name}: {count} 张)这段代码的逻辑是先收集所有验证集预测结果再生成混淆矩阵最后找出错误最多的类别对。看结果时有两个重点一是看哪些类别对彼此混淆二是被误分类的图到底是模型学不到特征还是标注本身有问题。商品图里最容易出现混淆的是外观接近的品类比如瓶装水、可乐、果汁都是透明瓶身加彩色标签若背景又是同一货架模型主要靠标签颜色区分这种错误通过加数据很难根治。对于外观确实相近的商品类别我的处理习惯是先随机抽 20 张被误分的图逐一查看确认是人眼也难分还是模型失误。如果人眼一看就分得清说明模型特征学习不到位继续增强如果人眼也分不清就该考虑是否合并类别把任务从“24 类”改成“20 类”交换精度和细粒度。至于要不要按误分结果重新调整数据划分我一般不看单次误分就重划而是先确认误分是否集中在某个拍摄批次或某种光照条件下确认后再从训练集中补充对应负样本。这条路走完你的分类系统才算真正从“跑通”进到“能用”。这对我来说已经成为一个固定习惯先跑混淆矩阵再谈调参。希望帮到你。本文还有配套的精品资源点击获取