
简介面向图像分类与计算机视觉学习者的11种常见食物分类数据集覆盖粥、甜点、牛排、派等类别已完成训练集/测试集划分。训练集含9866张图片测试集3430张全部为jpg格式并按类别存放可直接通过ImageFolder读取无需额外预处理。资源包共2000个文件包含1999张jpg图像和1个Python可视化脚本脚本可随机选取图片展示并保存到当前目录方便快速检查数据质量。压缩包大小约239.88MB数据总量246MB目录结构清晰适合作为图像分类项目的数据输入也可用于教学演示或算法对比实验。目前已有586人学习使用能显著节省数据采集与整理时间是构建食物识别模型的高质量现成资源。1. 图片分类入门首选11种常见食物分类图像数据集到底替你省了多少事图片分类数据集这个关键词每天都有人搜但真正上手做图像分类的人都清楚分类模型早已成熟最难的部分不在模型结构而在数据。11种常见食物分类图像数据集已做数据集划分想解决的就是新手起步最头疼的事——图片往哪放、训练集和验证集怎么切、切完会不会重叠。它把目录、类别映射、训练/验证/测试三份子集提前定好拿到手就能直接喂给PyTorch或TensorFlow。已做划分这四个字省掉的是一套极易翻车的流程手动切分随机性大、类别分布不均、验证集混入训练集同源图片。这个数据集的价值就是让你把精力全放在训练和调参上而不是花一晚上跟文件路径搏斗。适合刚学图像分类的学生、要快速验证想法的算法工程师和需要干净数据做课程设计的开发者。2. 划分好的数据集长什么样目录结构、划分比例与第一个必做检查拿到数据集第一件事不是急着写模型而是先看清目录组织。图像分类数据集最常用的组织方式就是 ImageFolder 格式根目录下按类别建子文件夹每个文件夹里放该类别的图片文件夹名就是类别标签。这个已做划分的 11 类食物数据集常见结构长这样data/ ├── train/ │ ├── apple/ │ ├── banana/ │ ├── bread/ │ ├── carrot/ │ ├── egg/ │ ├── meat/ │ ├── milk/ │ ├── potato/ │ ├── rice/ │ ├── soup/ │ └── tomato/ ├── val/ │ ├── apple/ │ ├── banana/ │ └── ... └── test/ ├── apple/ └── ...类别名以你实际拿到的目录为准这里只是示意。关键在于类别不靠单独的 CSV 或 JSON 标注文件记录靠文件夹结构本身。torchvision.datasets.ImageFolder扫描 train 根目录后会自动生成class_to_idx映射把类别名映射成 0 到 10 的整数。也就是说train、val、test 三个根目录下必须有完全一致的一组类别文件夹否则验证集的类别索引索引会对不上。2.1 用目录结构一眼判断数据集质量先跑一遍 tree 命令确认结构完整再写三行 Python 统计每个类别下的图片数量import os from collections import Counter for split in [train, val, test]: root fdata/{split} count Counter() for cls in os.listdir(root): cls_path os.path.join(root, cls) if os.path.isdir(cls_path): count[cls] len(os.listdir(cls_path)) print(split, dict(count))这段代码把每个子集的类别分布打出来一眼能看出两个问题三个 split 的类别是否完全一致以及类别之间样本数是否严重失衡。正常情况下训练集每类几千张、验证集和测试集每类几百张是最常见的配置。如果某个类别在某个 split 里数量只有个位数后续训练时这个类几乎必然被模型忽略。这类数据和 COCO、VOC 那些目标检测数据集有本质差别检测数据的标注是几万行的 JSON 或 XML需要解析 bounding box而纯分类数据集把标签编码进路径少了整个标注解析环节代码量少了一个量级。所以判断一个图片分类数据集能不能直接用看目录结构就够了——干净、对称、每类有足够样本就是好数据。2.2 划分比例和划分方式比样本总量更能决定模型上限为什么分类任务里已做数据集划分这么值钱因为划分方式直接决定验证集指标可不可信。常见划分比例是训练集 70%80%、验证集 10%15%、测试集 10%15%三份子集各司其职子集比例范围作用能不能参与训练train70%80%更新模型参数能val10%15%调超参、选模型、看收敛曲线不能test10%15%最终无偏评估绝对不能训练集负责让模型学规律验证集用来判断什么时候该停、学习率怎么调测试集只在全部定稿后跑一次。很多人为了省事只切 train 和 test 两份用 test 来调参最后测试集指标已经过拟合到超参上模型真实性能无从得知。这个数据集把三份都切好就是在逼你走正确的评估流程。比比例更重要的是划分方式。最怕的是这种操作把文件名按字典序排序取前 80% 当训练集、后 20% 当测试集。同一批拍摄、同一光照条件下拍的香蕉照片会全部挤进训练集另一天拍的则全部落在测试集里——模型看到的训练分布和测试分布偏差巨大这就是数据划分泄漏的典型形态。规范做法是按类别做分层随机划分stratified split让每个类别在三个子集里都按同样比例出现。标题里的已做数据集划分通常就意味着这种分层随机已经替你做好了。2.3 第一个必做检查跨子集重复图片查重这是一个经常被忽略的环节。下载完数据集先做一次图片级查重确认 train 和 val、train 和 test 之间不存在重复或近乎重复的图片。食物图片由于拍摄场景有限同一张图被复制到多个子集的情况并不少见。一旦发生验证集指标会虚高模型上线后效果骤降。查重用感知哈希比较可行pip install pillow imagehashimport os import imagehash from PIL import Image from collections import defaultdict hash_map defaultdict(list) for split in [train, val, test]: root fdata/{split} for cls in os.listdir(root): cls_path os.path.join(root, cls) if not os.path.isdir(cls_path): continue for name in os.listdir(cls_path): path os.path.join(cls_path, name) h imagehash.phash(Image.open(path)) hash_map[str(h)].append(path) for h, paths in hash_map.items(): if len(paths) 1: print(重复或近似图片:, paths)这段代码计算每张图片的感知哈希值pHash哈希值一样的说明在感知层面几乎相同。输出里如果出现跨子集的重复项需要手动从 train 里剔除重复图保留 val 和 test 的版本。这个检查花不了五分钟却能在训练开始前就排掉一个大问题属于血泪经验换来的必做步骤。3. 用 PyTorch 跑通基础图像分类DataLoader 写法、数据增强和参考基线目录和划分确认无误后就进入正式训练环节。我一般先用 PyTorch 搭一套最朴素的流程拿到参考基线ImageFolder 加载数据、一套标配增强、一个轻量 CNN。这一步目的不是追求精度而是确认数据链路是通的、模型能正常收敛、类别数对不对。基线跑通了后面换预训练模型才有意义。3.1 三步完成数据加载ImageFolder、transform、DataLoader数据加载的代码可以浓缩成三块先定义 transform再构建 Dataset最后包进 DataLoader。训练集和验证集必须用两套 transform这是这章最重要的规矩。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强随机裁剪到224、随机水平翻转、颜色抖动 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集/测试集只做缩放和中心裁剪不做任何随机增强 eval_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_transform) val_ds datasets.ImageFolder(data/val, transformeval_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(train_ds.classes) # 类别名列表 print(train_ds.class_to_idx) # 类别名 - 索引 print(len(train_ds.classes)) # 类别数应该等于11逻辑说明ImageFolder 扫描data/train下的子文件夹把每个文件夹当作一个类别。class_to_idx按字母顺序生成所以类别是英文名时索引顺序和直觉一致。训练集做了随机裁剪和翻转等价于在有限样本上制造更多变体验证集不做任何随机操作保证每次评估的是同一批确定性输入。参数说明batch_size32适合显存 8G 以上的显卡shuffleTrue只在训练集上开用来打乱样本顺序避免同一类图片在连续 batch 里扎堆num_workers4是数据预处理并行进程数Windows 下如果报错就改成 0pin_memoryTrue配合 GPU 训练能略微减少数据搬运时间纯 CPU 跑可以关掉。Normalize 里的 mean 和 std 用的是 ImageNet 的统计值在迁移学习场景下是标准配置从零训练时也可以换成自己数据集的统计值但通常没必要。3.2 数据增强的分寸训练集加噪声验证集求稳定数据增强是图像分类里性价比最高的一步也是最容易做过头的一步。上面代码里RandomResizedCrop(224)会随机裁剪图片的一部分再缩放到 224x224相当于让模型学会在食物被盘子、其他食材遮挡一部分时依然认得出来ColorJitter抖动亮度、对比度、饱和度模拟不同餐厅灯光下的色差——这个对食物分类尤其重要因为同一道菜在不同光线环境下的颜色差异极大。验证集和测试集永远不要加随机增强。一个常见翻车是图省事把训练用的 transform 直接套到 val 上结果验证集每次评估时输入都在随机变化指标曲线抖动得像心电图你根本分不清是模型在变好还是运气在变好。验证集要的是稳定复现的输入只有这样才能和训练集的 loss 做对比。如果你发现训练集准确率刷到 95% 以上、验证集却一直停在 70%先检查两件事val 有没有误用带 Random 前缀的增强模型有没有在训练时把 Dropout 或 BatchNorm 跑成 train 模式。这两个点后面避坑章节会专门展开。提示val 和 test 的 transform 里禁止出现任何 Random 开头的方法这是图像分类实验里出现频率最高的约定。3.3 从零训练的轻量 CNN一份不算漂亮但可靠的参考基线第一次跑通流程不需要上预训练模型先写一个三层卷积的小网络当基线。它的意义在于验证数据本身有没有问题如果这么简单的网络都能在验证集上明显超过随机猜测11 类随机猜测准确率约 9%说明数据标注、加载、类别映射全是通的如果连这个都学不动再好的预训练模型也白搭。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))模型结构很简单三个卷积层分别输出 32、64、128 个特征图每层后接 ReLU 和 2x2 最大池化最后用自适应平均池化把特征压成 1x1 再接全连接层。AdaptiveAvgPool2d(1)的好处是输入尺寸变了进入全连接层的特征维度也固定是 128以后换分辨率不用改网络。训练循环要同时处理训练和验证两个阶段注意 train 和 eval 模式切换model SimpleCNN(num_classeslen(train_ds.classes)) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(20): model.train() train_loss 0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fepoch {epoch 1:02d} | train_loss {train_loss / len(train_loader):.4f} f| val_acc {correct / total:.3f})逻辑说明训练阶段每个 batch 算 loss、回传梯度、更新参数验证阶段用torch.no_grad()关掉梯度计算只做前向推理和准确率统计。model.train()和model.eval()切换在 CNN 里影响 BatchNorm 和 Dropout 的行为忘记切换会让验证集指标出现诡异波动。参数说明lr1e-3是 Adam 最常用的起点epoch20对一个小网络和几千张图来说通常足够看到收敛趋势。跑完如果 val_acc 稳定在 50% 以上说明数据链路没问题可以进入下一章换预训练模型如果 val_acc 一直贴着 9% 上下回头查上一章的目录结构和查重步骤大概率是类别映射错位。4. 迁移学习ResNet18 微调的完整流程与 5 个必调参数基线跑通之后就到了真正提升精度的环节。对 11 类食物这种体量的数据集我不建议从零训练大网络也不建议一上来就上最新的图像分类模型。最新的图像分类模型里 Vision Transformer 系列在 ImageNet 上指标很漂亮但 Transformer 系模型极度吃数据量几万张图片以下很容易陷入欠拟合。ResNet18 这个 2015 年的结构在今天依然是中小型分类数据集上最稳的起点。4.1 为什么要用 ImageNet 预训练权重做食物分类食物分类的难点和通用物体分类不太一样同类食物之间存在巨大的形态差异比如同一道西红柿炒蛋有人拍成糊状有人拍出完整西红柿块但跨类之间可能只差一个颜色特征。ImageNet 预训练权重在千万级图片上学过边缘、纹理、颜色渐变等通用视觉特征这些特征对食物是直接可迁移的——模型不需要从头学什么是边缘只需要在已有特征基础上重新组合出食物专属的高层语义。具体做法是保留 ResNet18 的卷积骨架把最后一层全连接换成输出为 11 的新分类头。骨架部分加载在 ImageNet 上训练好的权重分类头随机初始化。训练时有两种策略只训练分类头或者全量微调整个网络。对几万张以内的数据量我一般先冻结骨架训练几个 epoch 让分类头稳定下来再解冻骨架用更低的学习率全量微调这样既快又不容易破坏预训练特征。4.2 完整微调代码冻结、替换分类头、学习率分层from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(train_ds.classes) # 替换最后一层全连接输出为11类 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 第一轮冻结卷积骨架只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 跑5个epoch后再解冻骨架 for param in model.parameters(): param.requires_grad True # 骨架用更小的学习率分类头保持原学习率 optimizer torch.optim.Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.parameters(), lr: 1e-4}, ], weight_decay1e-4)逻辑说明weightsmodels.ResNet18_Weights.IMAGENET1K_V1是 torchvision 当前推荐的预训练权重加载方式比直接写pretrainedTrue更明确权重版本。冻结骨架后反向传播不会更新骨架参数训练速度和显存占用都更友好。解冻后采用分组优化器分类头用 1e-3 保持较快收敛骨架用 1e-4 做精细调整避免大学习率把预训练特征冲掉。参数说明weight_decay1e-4是 L2 正则对抑制过拟合有帮助Adam 自带自适应学习率但分组设置 lr 依然必要因为骨架和分类头的收敛进度完全不同。训练 epoch 可以这样安排冻结阶段 5 个 epoch解冻后 1525 个 epoch全部约 2030 个 epoch。验证集指标一般在解冻后的第 3 到第 5 个 epoch 快速上升如果没出现这个现象说明学习率分层不合适。4.3 5 个必调参数一张表说清影响方向训练图像分类模型与其玄学调参不如理解每个参数影响的维度。以下是我在类似数据集上常用的参数范围和调整逻辑参数常见范围影响调整逻辑学习率 lr1e-4 ~ 1e-3收敛速度与稳定性冻结阶段 1e-3解冻骨架用 1e-4batch_size16 ~ 64梯度噪声与显存占用显存够就大一点不足 32 就先保 32weight_decay1e-5 ~ 1e-4抑制过拟合验证集掉点明显时往上加训练轮数20 ~ 30模型容量利用程度看 val_acc 是否还在涨不涨就停是否冻结骨架冻结/微调泛化能力与训练速度数据少先冻结数据够再全量一个血泪经验学习率是这里面最敏感的。Adam 不是万能药1e-2 这种偏高的学习率在微调预训练模型时经常发生 loss 剧烈震荡表现为训练集 loss 先降后跳、验证集指标始终上不去。遇到这种情况先降学习率不要急着换模型结构。另外提一个实用的收敛判断方法每跑完一个 epoch 打印 val_acc如果连续 5 个 epoch 不涨要么降低学习率继续跑要么提前停。训练集 loss 和验证集 acc 趋势背离时以验证集为准——训练集 loss 可以无限趋近于 0那只是模型在背答案。5. 避坑划分泄漏、类别失衡和路径编码四条血泪排查记录这个数据集拿来做图像分类前面几章的流程能覆盖大部分正常情况但有几个坑几乎每个人都会踩一遍。按现象、原因、解决的顺序记下来遇到了直接对照排查。5.1 验证集指标虚高测试集却打回原形现象训练时验证集准确率一路飙到 90% 以上心里已经盘算着上线结果在测试集上一跑只剩 70%整个实验像被泼了冷水。原因验证集和测试集之间存在同源图片或者划分时没按类别分层导致某一类在验证集里覆盖率偏高。还有一种隐蔽情况是查重没做——train 里和 val 里出现了几乎相同的图片模型在训练时已经把验证集内容背下来了。解决回到 2.3 节做一遍感知哈希查重把跨子集的重复图片从 train 里剔除。然后检查每个类别的分层比例是否一致用 2.1 节的统计脚本把三个 split 的类别直方图打出来逐类对比。这个检查一定要在训练前做训练后发现问题重新划分之前所有实验都要作废。5.2 验证集指标像心电图一样抖现象训练 loss 稳步下降val_acc 却忽高忽低昨天跑出 85%今天同样代码只剩 72%完全无法判断模型是不是在变好。原因验证阶段没有固定输入。最常见的是把带数据增强的 transform 用在了 val 上RandomResizedCrop每次随机裁剪的位置不一样模型看到的验证图每次都不同。其次是忘了model.eval()BatchNorm 还在用 batch 内统计量输出不稳定。解决确认 val_loader 绑定的是eval_transform只有 Resize、CenterCrop、Normalize训练循环里验证前加上model.eval()验证完回到训练时再切回model.train()。修完这两处val_acc 的波动会显著减小如果还有波动检查验证集 DataLoader 的 shuffle 是不是没关必须为 False。5.3 预测结果里某个类别永远不出现现象11 类分类任务训练完成后看预测统计发现模型几乎从不预测苹果这个类别查训练集却发现苹果图片数量其实不少。原因类别不平衡。如果某个类别在训练集里只有几百张其他类别却各有几千张模型学到的最优策略是忽略小类别以降低整体 loss。另一个原因是类别名在class_to_idx里的索引和你预期不一致预测输出和真实标签错位。解决先用 2.1 节的统计脚本确认各类别样本数分布。如果是失衡给数据加载加上WeightedRandomSampler让小类别被采样到的概率提高from torch.utils.data import WeightedRandomSampler class_counts [train_ds.targets.count(i) for i in range(num_classes)] sample_weights [1.0 / class_counts[label] for label in train_ds.targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(train_ds.targets), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4)这段代码按类别样本数的倒数给每个样本赋权样本少的类别权重高采样时更容易被抽到。replacementTrue允许同一张图在一个 epoch 里被多次抽中这是故意用来平衡分布的。注意使用 sampler 后DataLoader 的shuffle参数要删掉两者会冲突。5.4 训练集准确率 99%验证集只有 60%现象训练集上 loss 逼近 0val_acc 挣扎在 60% 上下两者之间像隔了一堵墙。原因过拟合模型把训练集里的噪声、背景纹理背了下来。食物数据尤其容易过拟合因为同类图片的背景高度相似盘子、桌面、餐厅环境模型可能不学食物本身转而去学背景特征。解决先确认model.eval()没漏。然后增强数据增强强度把ColorJitter幅度调大加入RandomRotation(10)或RandomAffine让模型被迫关注食物区域同时给优化器加weight_decay1e-4必要时在分类头前加一层Dropout(0.3)。如果增强已经很足还过拟合说明数据量确实太少可以用第 6 章的重划分脚本把训练集比例调大一点。5.5 Windows 下加载图片报 UnicodeDecodeError现象在 Windows 上跑训练脚本DataLoader 的 worker 进程报错提示UnicodeDecodeError或FileNotFoundError路径里带着中文或者奇怪字符。原因图片文件名或所在路径包含中文Python 在 Windows 下默认编码不是 UTF-8worker 进程读取路径时解码失败。这个问题在 Linux 上几乎不出现Windows 上只要数据集路径带中文就很容易触发。解决最省事的办法是把整个数据集放到纯英文路径下比如D:\food_data\train。如果文件名本身带中文训练前写个脚本把文件名批量改成英文。以后凡是处理公开数据集第一步就把路径统一成英文小写加下划线这是 Windows 用户用血泪换来的习惯。6. 验证模型不是碰运气混淆矩阵、按需重划分与迁移到自有数据集的技巧只看整体 val_acc 容易漏掉局部问题。11 类食物里如果胡萝卜和土豆总是互相误判准确率数字根本看不出来。验证模型质量我习惯先上混淆矩阵。6.1 用混淆矩阵定位最容易混淆的类别对先跑一次完整验证集收集预测和真实标签再画混淆矩阵from sklearn.metrics import confusion_matrix import seaborn as sns all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: outputs model(images) all_preds.extend(outputs.argmax(dim1).cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelstrain_ds.classes, yticklabelstrain_ds.classes) plt.show()这段代码把每个真实类被预测成哪个类的情况可视化成矩阵。先看对角线哪一类自召回率最低说明它是主要短板再看横向最亮的格子真实类别 X 被误判成了哪些类别 Y就是最容易混淆的类别对。针对这对类别补样本或调权重比盲目换模型有效。6.2 按需重新划分当你想加大训练集或做 K 折验证时数据集自带的划分不一定适合所有场景。做交叉验证、或想提高训练集占比时用分层重划分脚本from sklearn.model_selection import train_test_split all_images, all_labels [], [] for cls in os.listdir(data/train): for name in os.listdir(fdata/train/{cls}): all_images.append(fdata/train/{cls}/{name}) all_labels.append(cls) train_imgs, test_imgs, train_lbls, test_lbls train_test_split( all_images, all_labels, test_size0.2, stratifyall_labels, random_state42 )stratifyall_labels保证每个类在 train 和 test 中的占比和原始数据一致避免某一类全被切进测试集。random_state42固定随机序列多次运行结果完全一致。重划分后务必跑一遍 2.1 节的统计脚本核对类别分布划分完不检查等于白做。6.3 把食物数据集流程沉淀成自己的模板这套流程可以原样迁移到任何图片分类项目整理成 train/val/test 目录类别用英文小写命名改掉 num_classes 和数据路径剩下的数据加载、增强、迁移学习、混淆矩阵代码全部复用。我自己的习惯是首次在新数据上跑通时先观察基线 CNN 能否明显超过随机猜测——能超过说明数据没问题再上预训练模型超过不了就先查标注和划分。先让最笨的模型学会再去追求最聪明的模型。这个顺序帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取