ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

11类食物分类数据集实战:从数据划分到迁移学习模型选型

11类食物分类数据集实战:从数据划分到迁移学习模型选型 简介这是一份面向图像分类初学者与算法实践者的常见食物图像数据集覆盖粥、甜点、牛排、pie等11个类别适合用于课程作业、模型训练入门与分类算法对比实验。数据已按文件夹完成训练集与测试集划分可直接通过ImageFolder加载无需额外清洗或重组。资源包共2000个文件以1999张jpg图像为主另附1个可视化py脚本压缩包约239.88MB其中train目录含9866张图片test目录含3430张图片类别结构清晰便于快速构建数据管道。附带的py脚本可随机读取一张图片并展示结果保存在当前目录无需修改即可运行方便检查数据质量与类别分布。目前已有585人学习适合希望跳过数据整理、直接投入建模与调参的读者使用。1. 食物分类数据集怎么选11 类已划分图像集的真实落地价值拿到一个「11 种常见食物分类图像数据集已做数据集划分」的标题很多人第一反应是去找下载链接但真正决定这个数据集能不能用的是划分方式、类别均衡度和图像来源。食物分类是图片分类里最容易被低估的方向——它不像 CIFAR-10 那样干净也不像 ImageNet 那样庞大但它的类间相似度极高包子、饺子、烧麦在低分辨率下几乎是一个东西蛋糕和面包在颜色直方图上高度重叠。这意味着你拿这个数据集跑一个 ResNet-18准确率可能停在 85% 上不去不是模型不行是数据本身的类间边界模糊。这个数据集的核心价值在于「已做数据集划分」这五个字。做过图片分类的人都知道自己切分训练集/验证集/测试集时最容易翻车的地方是数据泄漏——同一张图的不同增强版本被分到训练和验证两边验证准确率虚高上线就崩。一个已经划分好的食物分类图像数据集省掉的不只是写train_test_split的时间更是帮你避开了按文件名随机切分时同源图片跨集分布的坑。它适合三类人想快速验证迁移学习效果的算法工程师、需要做菜品识别原型的移动端开发者、以及拿食物分类当教学案例的高校教师。11 个类别这个数字也值得说一句。少于 10 类分类任务的决策边界太简单模型学不到细粒度特征多于 20 类在中小规模数据集上每类样本数会被摊薄长尾问题立刻暴露。11 类是一个刚好能体现细粒度分类难度、又不至于让数据量失控的区间。常见做法是每类 500 到 1500 张总量在 6000 到 15000 张之间配合 7:1.5:1.5 或 8:1:1 的划分比例。如果你拿到的数据集每类只有一两百张那就要认真考虑数据增强和冻结层策略了否则过拟合是必然的。2. 从目录结构到 DataLoader把已划分数据集接进训练管线2.1 先看清目录长什么样再决定用 ImageFolder 还是自定义 Dataset已划分的数据集通常有两种组织方式。第一种是按 split 分目录每个 split 下再按类别分子目录food11/ ├── train/ │ ├── baozi/ │ ├── dumpling/ │ ├── ... ├── val/ │ ├── baozi/ │ ├── ... └── test/ ├── baozi/ └── ...第二种是每个类别一个目录划分信息放在单独的 CSV 或 JSON 里。第一种直接用torchvision.datasets.ImageFolder就能读第二种必须写自定义 Dataset。我一般会先跑一段脚本确认结构别凭感觉写路径import os from pathlib import Path root Path(food11) for split in [train, val, test]: split_dir root / split if not split_dir.exists(): print(f[跳过] {split} 不存在) continue classes sorted([d.name for d in split_dir.iterdir() if d.is_dir()]) counts {c: len(list((split_dir / c).glob(*))) for c in classes} total sum(counts.values()) print(f{split}: {len(classes)} 类, {total} 张) for c, n in counts.items(): print(f {c}: {n})这段脚本做三件事确认 split 目录是否存在、列出类别名并排序、统计每类图片数。排序很重要因为ImageFolder按字母序分配标签索引如果你自己写 Dataset 时用了不同的顺序训练和推理的类别映射就会错位这种 bug 不会报错只会让模型输出看起来「还行但总差一点」。统计每类数量是为了检查类别均衡度如果某一类只有其他类的三分之一后面采样策略就要调整。2.2 用 ImageFolder 三行接进训练但变换参数别照抄确认结构后接进 PyTorch 训练管线是最短路径from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), 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_tf 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(food11/train, transformtrain_tf) val_ds datasets.ImageFolder(food11/val, transformeval_tf) test_ds datasets.ImageFolder(food11/test, transformeval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)RandomResizedCrop的scale(0.7, 1.0)是食物分类的关键参数。默认的(0.08, 1.0)会把图片裁得太狠食物图像的主体通常占据画面中央大部分区域裁到 8% 面积时可能只剩一块桌布或盘子边缘模型学到的是背景噪声。0.7 的下限保证每次裁剪至少保留七成画面食物主体不会丢。ColorJitter的强度也别开太大食物分类里颜色是重要判别特征——把红烧肉调成灰色模型就分不清它和酱牛肉了。Normalize用的 ImageNet 均值方差是迁移学习的标准做法如果你从零训练可以换成这个数据集自己的统计值但用预训练权重时不要改。2.3 类别不均衡时用 WeightedRandomSampler别硬调 loss 权重如果统计发现某类样本明显偏少有两种处理方式调 loss 的 class weight或者用 WeightedRandomSampler 过采样。我一般优先用采样器因为它直接改变每个 batch 的类别分布效果比在 loss 上乘系数更直观import numpy as np from torch.utils.data import WeightedRandomSampler targets [s[1] for s in train_ds.samples] class_count np.bincount(targets) class_weight 1.0 / class_count sample_weight [class_weight[t] for t in targets] sampler WeightedRandomSampler( weightssample_weight, num_sampleslen(sample_weight), replacementTrue ) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4, pin_memoryTrue)class_weight 1.0 / class_count给少数类更高的采样概率replacementTrue允许同一张图在一个 epoch 内被抽到多次。注意用了 sampler 之后shuffle必须去掉两者互斥同时设会直接报错。num_samples设成总样本数保证每个 epoch 的迭代次数和原来一致。这个方案在类别比例不超过 1:5 时效果很好如果差距到 1:20 以上光靠过采样会导致少数类严重过拟合那就得配合数据增强或考虑收集更多数据了。3. 迁移学习选型ResNet、EfficientNet 还是 ViT食物分类上谁更稳3.1 小数据集上 ResNet-18 仍然是最不容易翻车的基线食物分类数据集通常在一万张量级这个规模下 ResNet-18 配合 ImageNet 预训练权重微调 20 到 30 个 epoch 就能到 85% 以上的准确率。它的优势不是精度上限高而是训练稳定、显存占用低、调参经验成熟。我一般会先跑一个 ResNet-18 基线确认数据管线没问题、准确率在合理区间再换更大的模型做对比。import torch.nn as nn from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 11) # 11 类输出 # 先冻结 backbone只训分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4 )冻结 backbone 先训 5 个 epoch 分类头再解冻全部参数用 1e-4 的学习率微调这个两阶段策略在小数据集上比直接全量微调更稳。直接全量微调时预训练权重的特征提取能力容易被随机初始化的分类头产生的大梯度破坏尤其是 batch size 较小时。AdamW的weight_decay1e-4是 Transformer 时代之后被验证过的默认值比 SGD 的 5e-4 更不容易过拟合。3.2 EfficientNet-B0 在食物细粒度上通常比 ResNet 高 2 到 4 个点EfficientNet 的复合缩放策略让它在同等参数量下感受野和通道数更均衡食物分类这种需要同时关注纹理米饭的颗粒感和形状披萨的圆形的任务EfficientNet-B0 的表现一般比 ResNet-18 好。代价是训练更慢输入分辨率通常要上到 224 或 256显存占用也更高。model models.efficientnet_b0( weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1 ) model.classifier[1] nn.Linear(model.classifier[1].in_features, 11)EfficientNet 的classifier是一个 Sequential索引 1 才是 Linear 层直接替换model.classifier会报错。这个细节在 torchvision 不同版本间有差异写之前先print(model)看一眼结构比查文档快。3.3 ViT 不是不能用但你的数据量可能撑不住Vision Transformer 在 ImageNet 级别数据上碾压 CNN但在万张量级的数据集上从预训练权重微调 ViT-B/16 的效果往往不如 EfficientNet。原因是 ViT 缺少 CNN 的归纳偏置平移不变性、局部性小数据下更容易过拟合。如果你一定要用建议用 DeiT 的蒸馏版本或者把 patch size 调大、层数减少。我试过在 8000 张食物图上微调 ViT-B/16验证准确率比 EfficientNet-B0 低了 3 个点训练时间翻了三倍显存占用多了四倍。除非你有十万张以上的食物图像否则这个方向性价比不高。模型参数量输入尺寸预期准确率训练时间单卡ResNet-1811M22485-88%15 minEfficientNet-B05.3M22488-91%25 minViT-B/1686M22484-87%60 min提示上表的准确率区间基于每类 800 张左右的均衡数据集实际值受图像质量、类别相似度影响很大仅作选型参考。4. 训练中必调的 4 个参数与验证集上的三个危险信号4.1 学习率、batch size、weight decay、warmup 的联动关系这四个参数不是独立的。学习率跟 batch size 大致成正比batch size 翻倍时学习率可以乘 1.5 到 2但不要线性放大。weight decay 跟学习率也有关联学习率调小时 weight decay 要相应减小否则正则化过强会导致欠拟合。warmup 在微调预训练模型时几乎是必须的前 500 到 1000 步线性升温避免初始大梯度破坏预训练权重。from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR warmup LinearLR(optimizer, start_factor0.1, total_iters500) cosine CosineAnnealingLR(optimizer, T_max5000, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[500])start_factor0.1表示 warmup 开始时学习率是设定值的 10%线性升到 100% 后切换余弦退火。eta_min1e-6是退火下限别设成 0否则最后阶段模型几乎不更新。这个组合在食物分类微调任务上基本不需要再调直接抄。4.2 验证 loss 不降反升、准确率震荡、类别预测坍缩训练过程中盯着验证集看三个信号。第一验证 loss 在训练 loss 还在降的时候开始上升这是过拟合的典型标志解决办法是加数据增强、加 dropout、或者早停。第二验证准确率在两个值之间反复跳比如 0.82 和 0.86 来回震荡通常是学习率太大或者 batch size 太小导致梯度噪声过大把学习率降一半试试。第三混淆矩阵里某一类的预测数量远低于真实数量比如「饺子」类几乎全被预测成「包子」这是类别坍缩说明模型没有学到区分这两类的特征需要检查这两类的图像在视觉上是否真的可区分或者用 focal loss 加大难分样本的权重。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.cuda() preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsval_ds.classes, yticklabelsval_ds.classes) plt.show()混淆矩阵是食物分类最有用的诊断工具没有之一。准确率只告诉你「错了多少」混淆矩阵告诉你「错在哪两类之间」。如果错误集中在少数几对类别上针对性补充这些类别的训练数据比盲目加数据更有效。4.3 测试集只在最后跑一次别拿它调参这是血泪经验。很多人习惯每个 epoch 都跑一遍测试集看准确率然后根据测试集表现调超参这等于把测试集当验证集用最终报告的指标没有意义。正确做法是训练和调参只看验证集测试集在模型完全确定后跑一次那个数字才是你能对外说的。如果验证集和测试集准确率差距超过 3 个点说明验证集划分可能有问题或者数据分布不一致需要回头检查划分脚本。5. 避坑与排查食物分类数据集上最容易翻车的 5 个地方5.1 验证准确率 95% 但测试只有 70%数据泄漏现象训练时验证集准确率一路涨到 95% 以上测试集一跑只有 70% 出头。原因同一张原始图片经过不同增强后分别进入了训练集和验证集或者同一道菜的多张连拍被随机分到了不同 split。解决按图片的原始来源比如同一家餐厅、同一次拍摄做分组划分而不是按单张图片随机划分。如果数据集已经划分好了检查一下 train 和 val 里有没有文件名高度相似的图片。5.2 训练 loss 正常下降但准确率不动标签映射错位现象loss 从 2.3 降到 0.5但准确率始终在 9% 左右11 类随机猜的水平。原因自定义 Dataset 返回的标签索引和模型输出层的类别顺序不一致或者ImageFolder的类别排序和你以为的不一样。解决打印train_ds.class_to_idx确认映射关系用train_ds.classes检查排序确保推理时用同一套映射。5.3 显存溢出但 batch size 已经调到 1输入分辨率没对齐现象batch size 降到 1 还是 OOM。原因EfficientNet 或 ViT 的默认输入分辨率可能不是 224或者RandomResizedCrop的输出尺寸设成了 512 而模型期望 224。解决在transforms里显式指定Resize和CenterCrop的尺寸训练和验证用同一套空间尺寸。用torch.cuda.memory_summary()看显存分配在哪一层。5.4 模型把所有图都预测成同一类学习率太大导致坍缩现象混淆矩阵里某一列全是预测值其他列全为零。原因初始学习率太大分类头的随机权重产生巨大梯度把 backbone 的特征提取能力直接打崩。解决先冻结 backbone 用 1e-3 训分类头再解冻用 1e-4 微调。如果已经坍缩了重新加载预训练权重从头来别在坍缩的模型上继续训。5.5 推理时单张图片预测结果和验证集不一致预处理没对齐现象验证集准确率 88%但拿单张图片推理时结果乱七八糟。原因推理时的预处理和验证集不一致常见的是忘了Normalize、用了不同的Resize尺寸、或者 PIL 读图和 OpenCV 读图的通道顺序不同。解决把验证集的eval_tf单独保存成一个函数推理时直接调用同一个函数不要重新写一遍。注意以上五个坑里数据泄漏和标签映射错位是最隐蔽的因为它们不会报错只会让指标看起来「还行但不够好」很容易被误判成模型能力问题。6. 把 11 类食物分类推到 93% 的三个进阶技巧第一个技巧是测试时增强TTA。对同一张验证图片做多次不同的裁剪和翻转把多次预测的概率平均后取 argmax。这个操作不需要重新训练推理时间乘以增强次数通常能涨 1 到 2 个点。我一般用中心裁剪加四个角裁剪共 5 次水平翻转再翻倍到 10 次再多了收益递减。def tta_predict(model, img_path, n_crops5): img Image.open(img_path).convert(RGB) w, h img.size crops [] # 中心 四角 positions [(0, 0), (w//4, h//4), (w//2, h//2), (w//4, h//4), (w//4, h//4)] # 实际实现用 transforms 组合更简洁 tf transforms.Compose([ transforms.Resize(256), transforms.FiveCrop(224), transforms.Lambda(lambda crops: torch.stack( [eval_tf(c) for c in crops])), ]) ...第二个技巧是标签平滑label smoothing。把硬标签 0/1 换成 0.1/0.9让模型不要对某一类过度自信。食物分类里类间相似度高标签平滑能显著改善模型的泛化能力通常涨 0.5 到 1.5 个点。在CrossEntropyLoss里直接设label_smoothing0.1就行一行代码的事。第三个技巧是模型集成。把 ResNet-18 和 EfficientNet-B0 的预测概率平均两个模型架构差异越大集成收益越明显。我试过在食物数据集上集成这两个模型比单模型最好的结果高了 2.3 个点。代价是推理时要同时跑两个模型移动端部署可能吃不消服务端就没问题。最后一个习惯每次跑完实验把配置文件、随机种子、验证集准确率、测试集准确率记到一个 CSV 里。食物分类这种任务你会在不同 backbone、不同增强策略、不同学习率之间反复横跳没有记录的话两周后你根本想不起来哪个组合是最好的。我吃过这个亏现在每次实验完第一件事就是写日志后悔药没地方买。希望帮到你。本文还有配套的精品资源点击获取
返回列表