
简介面向咖啡豆品质检测与图像分类任务数据集包含Arabica、Debris_Shell、Endosperm三类咖啡豆缺陷图像已完成训练集与测试集划分训练集一千零五十一张测试集二百六十二张按类别存放于独立文件夹可直接配合ImageFolder使用也可作为yolov5分类数据免去数据整理流程。资源包为zip压缩格式约三十四点五五兆共一千三百一十六个文件主体为一千三百一十三张JPG图片另含分类字典json、可视化py脚本及示例图片脚本可随机展示一张图片的所属类别无需改动即可运行便于快速查看数据分布。目录结构清晰训练、测试文件夹各自独立类别一目了然辅助json字典可帮助理解不同缺陷类别的映射关系整体使用门槛低适合直接接入模型训练流程。目前已有一百六十二人学习使用适合缺陷检测、图像分类入门或需现成数据集验证算法的开发者对于教学演示、课题实验或快速构建分类基线都很适用。1. 引言咖啡豆缺陷图像分类数据集先看它解决什么问题做工业视觉的同学都知道缺陷检测里最难的不是模型选型而是标注数据怎么来。咖啡豆这种农产品缺陷检测尤其典型生豆在筛选线高速过料表面裂纹、虫蛀、霉变特征差异很大肉眼质检员一天看下来疲劳度极高。这类场景最适合先用一个「已划分好」的图像分类数据集跑通基线验证可行性再决定要不要上目标检测甚至语义分割。这份咖啡豆缺陷图像分类数据集3类、已做划分的价值就在这里——省掉你整理目录结构、手动切训练集和测试集的时间直接喂给图像分类算法就能开工。适合的人群是正在做农产品外观质检、想快速跑通一个三分类图像识别任务的算法工程师或学生尤其是第一次接触缺陷分类的人。2. 数据集的真实构成三类缺陷标签与目录结构2.1 三分类任务在缺陷检测里意味着什么先拆解「3类」这个信息。缺陷检测的分类任务通常比通用分类简单但这里的难点在于缺陷和正常样本之间的边界模糊。以咖啡豆为例生豆从采收、水洗、日晒到烘焙的过程中外观会出现裂纹、虫蛀洞、局部霉变、破损等不同表现。一个三分类数据集最典型的划分逻辑是「正常豆 / 常见物理缺陷 / 变异性缺陷」的宏观分类或者「完整豆 / 表面裂纹 / 虫蛀或霉变」这样贴近产线分拣口的细分类。这里有一个值得关注的点为什么不是二分类正常 / 缺陷因为实际产线上处理不同缺陷的机制不一样。机械筛选能滤掉裂纹豆色选机能剔除霉变豆虫蛀豆可能需要单独的风选环节。所以三分类的意义不在于「识别率看起来高」而在于「不同类别的误判代价不同」。你把霉变豆判成正常豆和把裂纹豆判成正常豆后果不同把正常豆误判成缺陷豆等于直接造成了合格品的损耗。这个数据集既然已经做了划分说明它按照训练、验证、测试三份交付不是把所有图堆在一个文件夹里让你自己切。如果你的下载包里是「有多个子目录、按类别分好」的结构大概率遵循的是标准 ImageNet 式布局train 下每个类别一个文件夹val 和 test 同理。这种结构的好处是PyTorch 的 torchvision.datasets.ImageFolder、Keras 的 flow_from_directory 都能直接读取不需要写自定义的 DataLoader。2.2 文件组织与读取方式的预期一个合格的三分类缺陷数据集目录结构通常长这样coffee_defect/ ├── train/ │ ├── normal/ │ │ ├── img_001.jpg │ │ └── ... │ ├── crack/ │ │ └── ... │ └── mold/ │ └── ... ├── val/ └── test/用 ImageFolder 读取时的关键参数是is_valid_file和transform。实际使用中很多人会忽略验证集里如果混入了不同光线条件下的图片会直接影响早停Early Stopping的判断。我一般会在加载后先做一次类别数量核对from torchvision import datasets, transforms transform_base transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(rootcoffee_defect/train, transformtransform_base) val_data datasets.ImageFolder(rootcoffee_defect/val, transformtransform_base) test_data datasets.ImageFolder(rootcoffee_defect/test, transformtransform_base) print(训练集类别映射:, train_data.class_to_idx) print(训练集样本数:, len(train_data)) print(验证集样本数:, len(val_data)) print(测试集样本数:, len(test_data))这段代码的逻辑是先走一遍标准预处理缩放、归一化再检查类别映射和样本数。class_to_idx字典是这个数据集的「翻译官」它决定了模型最终输出节点的顺序。很多做图像分类的同学训练完了才发现模型输出的 id 跟实际类别对应反了原因就是没看这个字典。需要留意的边界问题如果下载包里 test 目录缺失或者 val 和 test 都是空的那么「已划分」的质量就要打折扣。此时我建议用 val 当 test 用把 train 再切出 10% 当新 val这个操作我后面给出脚本。还有一点咖啡豆图像的背景千差万别——黑背景、白背景、传送带纹理如果这个数据集只包含单一底色模型在真实产线上会很容易翻车这是要记住的坑。3. 划分策略复盘为什么「已划分」也要自己检查一遍3.1 划分合理性随机种子、类别比例与数据泄漏拿到「已做数据集划分」的资源最忌讳的心态是盲信结果。我复盘过的很多分类项目里划分阶段埋了雷训练时根本发现不了。最常见的三类问题是随机性未固定、类别比例不均、跨验证集泄漏。先说随机种子。如果划分用的是随机采样但没固定种子别人跑出来是 0.92 的准确率你复现却只有 0.88通常不是模型写错了而是 train/val 分布不一致。正常做法是把种子写进划分代码并注释清楚import random, os, shutil from glob import glob from sklearn.model_selection import train_test_split random.seed(42) normal_imgs glob(raw/normal/*.jpg) crack_imgs glob(raw/crack/*.jpg) mold_imgs glob(raw/mold/*.jpg) def split_with_ratio(imgs, ratio(0.7, 0.15, 0.15)): train, tmp train_test_split(imgs, test_sizeratio[1] ratio[2], random_state42) val, test train_test_split(tmp, test_sizeratio[2] / (ratio[1] ratio[2]), random_state42) return train, val, test train_normal, val_normal, test_normal split_with_ratio(normal_imgs) # 其余类别同理这里的核心是random_state42它在train_test_split的两次拆分中保持一致。拆分的次序也有讲究先把训练集整个切出去再把剩下的临时集二次划分为 val 和 test。如果反过来先切 test 再切 train/val数据分布会偏得更厉害。再强调一遍各类别比例。如果你发现某个类别只有几十张图而其他两类有几百张那 F1-Score 而不是 accuracy 才是考核指标。这个数据集的类别比例如果失衡训练时的解决方案是加权采样WeightedRandomSampler我在第 4 章给出实现。数据泄漏是划分中最隐蔽的坑。典型场景是同一个咖啡豆的连续多张照片被同时分进了 train 和 val。这种图片不能算完全独立样本因为光照、角度几乎一致模型在验证集上的表现会被虚高。要检查是否有泄漏一个笨办法是计算图像的感知哈希相似度import hashlib from PIL import Image def image_hash(path, resize(32, 32)): img Image.open(path).convert(L).resize(resize) return hashlib.md5(img.tobytes()).hexdigest() all_imgs glob(coffee_defect/**/*.jpg, recursiveTrue) hash_map {} for p in all_imgs: h image_hash(p) if h in hash_map and os.path.dirname(p) ! os.path.dirname(hash_map[h]): print(f疑似重复图片: {p} 与 {hash_map[h]}) hash_map[h] p这段代码把每张图缩到 32×32 的灰度图再做 MD5 哈希。它的原理不是图像级比对而是「近似感知内容」的粗筛。如果两份不同的文件有完全相同的内容哈希会碰撞。跑一遍如果输出很多跨目录的重复项说明划分的时候根本没有考虑去重测试结果可信度要打折扣。3.2 划分后的三个强制检查项拿到手的第一时间我不建议直接开训练。先做三项检查检查项方法通过标准类别文件夹是否齐全os.listdir对比标注文件里的类别枚举与标注完全一致图片能否被正常解码逐个用 PIL 打开记录异常解码失败率低于 0.5%尺寸一致性Image.size统计分布大多数在合理区间如 224×224 或更大图片能否被正常解码这是最容易踩的低级坑。下载包里如果混入了损坏的 JPEG 文件ImageFolder加载到那张图时会直接抛异常而且是在训练中途之前所有的进度全部白费。处理这个问题我习惯在训练前先做一次批量体检from PIL import Image from tqdm import tqdm import os broken [] for root, dirs, files in os.walk(coffee_defect): for f in files: if not f.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(root, f) try: with Image.open(path) as im: im.load() if im.size[0] 10 or im.size[1] 10: broken.append((path, 尺寸过小)) except Exception as e: broken.append((path, str(e))) for b in broken: print(异常文件:, b[0], b[1]) print(f共发现 {len(broken)} 个异常文件)这段代码的检查粒度很细im.load()不是打开就完事而是真正把像素加载进内存能够触发绝大部分解码报错。size 10是额外保险防止单像素图片这种「能打开但不能用」的情况。如果一个号称已划分的数据集里异常文件数超过总样本数的 1%我会要求先做清洗再训练否则训练曲线会被这些噪声完全带偏。4. 训练落地ResNet18 微调三分类识别模型4.1 为什么选 ResNet18 而不是更大模型三分类图像任务样本量通常不大特别是缺陷类数据集单类几百张图已经算不错了。这时候如果直接上 ResNet50 甚至 EfficientNet-B4不仅训练时间变长而且因为样本不足以支撑深层网络的参数拟合反而容易过拟合。我一般会先用 ResNet18 跑通基线当准确率在验证集上达到 85% 以上再试 ResNet34 或者加数据增强这样排障路径清晰。ResNet18 的另一个优势是它对预训练权重的依赖度低。如果你的机器没法联网下载 ImageNet 预训练权重从零开始训练 ResNet18 在 3 分类任务上也能有不错表现。而 ResNet50 从零开始训练很容易陷入不收敛的泥潭。对缺陷检测场景「快速跑通、快速迭代」优先级高于「极限精度」。4.2 微调完整代码与参数语义贴一段可以直接跑的微调代码注意注释里包含了我踩过坑的参数import torch import torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader, WeightedRandomSampler import numpy as np device torch.device(cuda if torch.cuda.is_available() else cpu) transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) transform_eval transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(coffee_defect/train, transformtransform_train) val_data datasets.ImageFolder(coffee_defect/val, transformtransform_eval) class_counts [len(train_data.targets), ] # 实际应统计每个类别的数量 targets train_data.targets counts np.bincount(targets) class_weights 1.0 / (counts 1e-6) sample_weights [class_weights[t] for t in targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_data, batch_size32, samplersampler, num_workers4) val_loader DataLoader(val_data, batch_size32, shuffleFalse, num_workers4) model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 3) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) best_val_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * imgs.size(0) scheduler.step() model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total print(fEpoch {epoch1}: loss{running_loss/len(train_data):.4f}, val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_coffee_model.pth)拆一下关键参数的含义RandomCrop(224)配合Resize((256, 256))先放大再随机裁剪这是缺陷检测里比直接 Resize 到 224 更好的数据增强方案因为裁剪出来的局部细节更丰富模型能学到缺陷的局部特征。ColorJitter的三个参数亮度、对比度、饱和度。咖啡豆的表面颜色是判断霉变的重要线索但不同产线和批次的光照不同这个增强能提升跨场景泛化能力。幅度不宜太大saturation0.1是我调过的稳定值。WeightedRandomSampler当三个类别样本数相差大时这个采样器让每个 epoch 里小类别的图片被抽中概率更高等价于「过采样」。注意replacementTrue是允许同一张图在一个 epoch 中被重复抽到。AdamW是 Adam 的权重衰减修正版配合weight_decay1e-4能在小数据集上有效抑制过拟合。lr1e-4而不是默认的 1e-3因为 ResNet18 的DEFAULT预训练权重已经在 ImageNet 上学到了很好的低层特征用太激进的学习率会破坏这些特征。4.3 训练日志怎么读训练过程中最需要关注的不是 loss 下降速度而是验证集准确率是否在真实提升。我见过很多案例loss 一路降到 0.2 以下验证集准确率却原地不动甚至往下掉——大概率是过拟合了。此时优先检查的事情只有一件训练集的数量级。如果模型把训练集里的背景、光线、托盘颜色都学进去了而验证集和它风格不同准确率就会断崖。训练跑完之后务必保存的不是最后一个 epoch 的权重而是val_acc最高的那个 checkpoint。上面的代码已经做了这件事——best_coffee_model.pth对应的就是验证集最优权重。测试时加载这个权重而不是model.state_dict()那是最后一轮的可能已经是过拟合状态。5. 避坑指南数据、标签与训练环节的五个常见坑5.1 现象一训练集准确率 99%验证集只有 75%这个现象几乎每个人都会遇到一次。原因大概率是数据增强太弱模型把训练集的背景当成了分类线索。在咖啡豆这种目标区域占比不是很大的图片里背景几乎是「免费特征」。解决思路把RandomCrop从(224, 224)调成(160, 160)让裁剪区域更聚焦在目标上如果图片里的咖啡豆本身只占中间一半再加上RandomResizedCrop替代RandomCrop。另一个有效手段是 MixUp 增强它把两张训练图按比例混合强行打断模型对背景特征的依赖代码只有几行# 在训练循环中额外使用 lam np.random.beta(0.8, 0.8) imgs_mix lam * imgs (1 - lam) * imgs.flip(0) labels_mix lam * labels (1 - lam) * labels.flip(0)MixUp 的数学原理是线性插值。它的坑在于标签也要跟着插值labels变成软标签soft label后CrossEntropyLoss 不能直接用需要换成KLDivLoss或者手动实现混合损失。如果你不想引入复杂度先调增强强度MixUp 留到第二梯队。5.2 现象二测试集准确率比验证集低 10 个百分点以上原因几乎永远是数据泄漏——验证集和训练集之间高度相似验证时分数虚高。解决方法是回到第 3 章的哈希查重把整组数据重新划分。如果这份数据集里明确标注了「来自不同批次」那测试集应该是独立的另一批拍摄数据验证集和测试集之间不能有图像级别的重复。5.3 现象三训练到第 5 个 epoch 时 loss 变成 NaN这个坑我都遇过好多次尤其是在输入图片没有归一化、像素范围是 0-255 的情况下。ToTensor()会把像素自动缩放到 0-1但如果你手动读图后忘了归一化AdamW 会瞬间把权重推到无穷大。排查手段是加一组断言assert imgs.min() -0.5 and imgs.max() 1.5, 输入范围异常只要这行断言在训练循环里NaN 的根源会被立刻定位。如果断言过了还是 NaN那就去检查学习率1e-3 会对小数据集表现出不稳定降到 3e-4 到 7e-4 的区间再试。5.4 现象四类别映射错位导致准确率被拉低train_data.class_to_idx的输出是{crack: 0, mold: 1, normal: 2}这样一个字典顺序取决于文件夹名的字母排序不是你的直觉顺序。如果测试时用idx_to_class反查把 0 当成了 normal准确率就会乱掉。解决方法是制作一个明确的映射表idx_to_class {v: k for k, v in train_data.class_to_idx.items()} print(idx_to_class)一定要把这张表存下来最好放进模型的训练日志里。我见过有人训练完代码重跑一遍类别顺序变了结果整个评估体系崩塌。5.5 现象五图片里有 EXIF 方向信息导致训练图被旋转手机拍摄的咖啡豆照片经常会带上 EXIF 的 orientation 标签有些环境下读取时被旋转 90°导致缺陷方向特征失效。解决方法是读取图片时都做一次统一处理from PIL import ImageOps def load_fixed(path): img Image.open(path) img ImageOps.exif_transpose(img) return img.convert(RGB)ImageOps.exif_transpose会根据 EXIF 信息自动旋转。这一步要应用到 DataLoader 的loader参数里不是 transform 里因为它发生在 Pillow 读图阶段来不及走ToTensor。这个坑在 coffee 这类用手机或数码相机拍摄的数据集中出现概率极高值得留个心。6. 进阶从三分类到缺陷定位的扩展路径三分类模型解决了「图片是否含缺陷、是哪类缺陷」这个粗粒度问题但产线上往往还要知道「缺陷在哪」。想更进一步有两种常见路径。第一种是热力图可视化。用 Grad-CAM 查看模型注意力放在图片的哪个区域能在不重新标注的前提下快速判断模型是否真的聚焦在缺陷特征上。这里我给一段用torchcam库的实现思路from torchcam.methods import SmoothGradCAMpp cam_extractor SmoothGradCAMpp(model, target_layermodel.layer4[-1]) model.eval() with torch.no_grad(): img_tensor test_imgs[0].unsqueeze(0).to(device) output model(img_tensor) cam cam_extractor(0, output)target_layer选layer4的最后一层卷积因为这是特征图空间分辨率最低但语义最强的位置。如果热力图集中在咖啡豆的裂纹边缘而非背景说明模型的决策依据是合理的如果热力图散落在整张图说明模型没有找到有效特征。第二种路径是往目标检测迁移。如果手头这份数据集的格式是「每张图一个类别标签」而没有 bounding box无法直接训练 YOLO 系检测器。此时有两个做法。一是手动标注缺陷区域用 LabelImg 或 X-AnyLabeling 框出位置后转为 YOLO 格式二是如果只想保留分类能力但增强对局部特征的敏感性可以用注意力模块如 SE Block替换 ResNet 的残差块。显然后者改动成本更低前者则真正解决定位问题。以我自己的经验从三分类出发做缺陷项目最值得先做的一步是「用梯度类激活图看模型判断依据」。它不需要额外标注不增加训练时长直接告诉你这个数据集能不能在真实产线站住脚。从那以后我每次加载一个分类数据集第一件事不是看准确率而是先跑十来张图的热力图确认模型关注的位置和人的判断一致再决定要不要继续往下调参。这个习惯帮我挡掉过至少两批次「看似准确率高、实际泛化稀烂」的数据集希望帮你省掉同样的曲折。本文还有配套的精品资源点击获取