简介:这份资源面向具备一定Python基础、希望入门工业视觉缺陷检测的开发者与高校学生,提供了一套基于PyTorch框架的CNN鞋面缺陷识别完整方案。压缩包共353个文件,以347张jpg缺陷样本图片为核心数据,辅以3个txt说明文本与3个py脚本,整体约27.55MB,体积轻便便于本地快速跑通。数据集已做预处理,通过短边补灰边统一为正方形并施加旋转角度来扩增样本,覆盖多类鞋面缺陷形态。代码按流程拆分为数据集文本生成、深度学习模型训练与PyQt可视化界面三个环节,训练完成后模型会保存至本地,方便后续推理与演示。目前已有152人学习,适合想快速搭建缺陷分类基线、理解数据增强与CNN训练全流程的读者参考借鉴。
1. 鞋面缺陷识别这套 CNN 源码,为什么我建议先跑通再改
鞋面缺陷识别在产线上是个挺典型的视觉检测场景:布料纹理本身有周期性,缺陷又分破洞、抽纱、污渍、色差好几类,传统阈值分割一遇到换批次就崩。这套「基于 CNN 深度学习 python 的鞋面缺陷识别-含数据集.zip」把 PyTorch 训练脚本、推理脚本和一份已经分好类的鞋面图像数据集打包在一起,拿到手就能直接训练,不用自己从零标注。它适合两类人:一类是想找一个真实工业缺陷数据集练手 CNN 的深度学习入门者,另一类是工厂里做质检自动化、想快速验证方案可行性的工程师。我拆包之后第一件事不是看模型结构,而是先把训练跑起来看 loss 曲线,因为数据集质量比网络结构更能决定这套东西能不能用。
2. 拆包先看数据:鞋面缺陷数据集的目录结构与类别分布
2.1 数据集长什么样,先数一遍再决定怎么读
拿到压缩包解压后,常见做法是先别急着写 Dataset,用几行脚本把目录结构和每类样本数摸清楚。鞋面缺陷这种工业数据集,类别不平衡是常态——破洞可能几百张,色差可能只有几十张,不先看清楚,训练时 accuracy 会骗你。
import os from collections import Counter root = "dataset" # 解压后的数据集根目录,按实际路径改 for split in ["train", "val", "test"]: split_dir = os.path.join(root, split) if not os.path.isdir(split_dir): continue counter = Counter() for cls in os.listdir(split_dir): cls_dir = os.path.join(split_dir, cls) if os.path.isdir(cls_dir): n = len([f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".png", ".jpeg", ".bmp"))]) counter[cls] = n print(split, dict(counter), "总计", sum(counter.values()))这段脚本做三件事:遍历 train/val/test 三个划分,统计每个类别下的图片数量,最后打印总数。参数上唯一要改的是root,指向你解压出来的数据集根目录。如果打印出来发现某个类在 train 里有 500 张、在 val 里只有 5 张,那验证集指标波动会非常大,后面调参时别被单次结果带偏。图片后缀我列了 jpg/png/jpeg/bmp 四种,工业相机导出的图常见就这几类,如果你的数据是 tif,自己补一个后缀。
2.2 类别不平衡时,ImageFolder 直接读会有什么问题
PyTorch 的torchvision.datasets.ImageFolder按子文件夹名当标签,读起来最省事,但它默认不做任何重采样。鞋面缺陷里如果「正常」样本远多于「破洞」,模型会倾向于全预测成正常,accuracy 看着有 90%,实际漏检一堆。
from torchvision import datasets, transforms from torch.utils.data import DataLoader, WeightedRandomSampler import torch train_tf = transforms.Compose([ transforms.Resize((224, 224)), # 统一到 CNN 常用输入尺寸 transforms.RandomHorizontalFlip(), # 鞋面左右翻转不改变缺陷性质 transforms.RandomRotation(10), # 小角度旋转增强,模拟摆放偏差 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet 统计量 ]) train_set = datasets.ImageFolder("dataset/train", transform=train_tf) # 按类别频次的反比给每个样本算权重,缓解不平衡 targets = [s[1] for s in train_set.samples] class_count = torch.bincount(torch.tensor(targets)) class_weight = 1.0 / class_count.float() sample_weight = class_weight[torch.tensor(targets)] sampler = WeightedRandomSampler(sample_weight, num_samples=len(sample_weight), replacement=True) train_loader = DataLoader(train_set, batch_size=32, sampler=sampler, num_workers=4)逻辑上,WeightedRandomSampler让少样本类别在每个 epoch 里被抽到的概率更高,等价于做了重采样。参数说明:Resize的 224 是给 ResNet 这类骨干用的,如果你自己搭的小 CNN,可以降到 128 省显存;RandomRotation(10)的 10 度是经验值,鞋面缺陷方向性不强可以再大点,但破洞这种有明确形状的别转太狠;Normalize用的 ImageNet 均值方差,是因为后面大概率要加载预训练权重,保持一致才不会让第一层输入分布错位。num_workers在 Windows 上如果报错就设成 0,这是血泪经验,多进程 DataLoader 在 Windows 下容易卡死。
3. 模型与训练:从零搭 CNN 还是拿预训练骨干微调
3.1 两种路线的取舍,别一上来就 ResNet50
这套资源里如果带了自定义 CNN 结构,通常是三到四个卷积块加全连接,参数量小、训练快,适合先验证数据集本身有没有问题。但鞋面缺陷的类间差异有时候很细微,从零训练的小网络容易欠拟合。我的建议是:先用小 CNN 跑通全流程,确认数据管道没问题,再换预训练骨干微调。
import torch.nn as nn import torchvision.models as models def build_model(num_classes, backbone="resnet18", pretrained=True): if backbone == "resnet18": model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT if pretrained else None) # 替换最后的全连接层,输出改成你的缺陷类别数 model.fc = nn.Linear(model.fc.in_features, num_classes) elif backbone == "custom": model = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes), ) return model model = build_model(num_classes=4, backbone="resnet18", pretrained=True)build_model里num_classes必须和你数据集的实际类别数一致,数错了训练时 loss 会直接报维度不匹配。pretrained=True会下载 ImageNet 权重,第一次跑需要联网,下不下来就改成 False 走从零训练。custom分支里我用了AdaptiveAvgPool2d(1)把任意尺寸特征图压成 1x1,这样输入图片尺寸不用严格固定,比直接 Flatten 更稳。选 ResNet18 而不是 50,是因为鞋面缺陷数据集通常就几千张,大骨干参数量远超样本量,过拟合风险高,18 层在精度和速度之间更平衡。
3.2 训练循环里必须盯的三个量
训练脚本本身不复杂,但有几个量不看就会翻车:训练 loss、验证 loss、验证集上每个类别的召回。只看总 accuracy,遇到不平衡数据等于闭眼开车。
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=30) 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() print(f"epoch {epoch} train_loss {running_loss / len(train_set):.4f} lr {scheduler.get_last_lr()[0]:.6f}")AdamW的weight_decay=1e-4是给权重加 L2 正则,抑制过拟合;CosineAnnealingLR让学习率按余弦曲线从 1e-3 降到接近 0,比固定学习率更容易收敛到好的局部解。T_max=30要和总 epoch 数一致,不一致的话学习率曲线会提前走完。每个 epoch 打印 train_loss 和当前学习率,如果 loss 在前几个 epoch 就卡住不降,先查数据标签有没有错、学习率是不是太大;如果 train_loss 一直降但验证指标不涨,那就是过拟合,该加数据增强或者减模型容量了。验证部分我一般单独写一个函数,用torch.no_grad()包起来,按类别算混淆矩阵,这样能看出模型到底把哪类缺陷认成了哪类。
4. 推理与部署:把训练好的权重接到实际图片上
4.1 单张图片推理脚本怎么写才不踩预处理坑
训练时用了 Resize、Normalize,推理时必须一模一样地走一遍,否则输入分布对不上,预测结果会莫名其妙。这是最常见的翻车点:训练准确率 95%,拿单张图一测全是错的,八成是预处理不一致。
from PIL import Image import torch.nn.functional as F def predict(image_path, model, class_names, device): model.eval() tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) img = Image.open(image_path).convert("RGB") # 强制三通道,防止灰度图报错 tensor = tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = F.softmax(logits, dim=1) conf, pred = probs.max(dim=1) return class_names[pred.item()], conf.item() class_names = ["normal", "hole", "stain", "color_diff"] # 顺序必须和训练时一致 label, conf = predict("test.jpg", model, class_names, device) print(label, round(conf, 4))关键点有三个:convert("RGB")防止灰度图或带 alpha 通道的图直接喂进去报维度错;unsqueeze(0)补上 batch 维度,因为模型期望输入是[N, C, H, W];class_names的顺序必须和ImageFolder按文件夹名排序后的顺序完全一致,顺序错了标签就全乱了。softmax之后的置信度只用来做参考,工业场景里低于某个阈值(比如 0.7)的样本最好转人工复核,别硬信模型输出。
4.2 批量推理和结果落盘
产线上不可能一张一张手动跑,常见做法是写个批量脚本遍历文件夹,把结果写进 CSV,方便后续统计和追溯。
import csv, os def batch_predict(folder, model, class_names, device, out_csv="result.csv"): rows = [] for fname in os.listdir(folder): if not fname.lower().endswith((".jpg", ".png", ".jpeg", ".bmp")): continue path = os.path.join(folder, fname) label, conf = predict(path, model, class_names, device) rows.append([fname, label, round(conf, 4)]) with open(out_csv, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["filename", "predicted_label", "confidence"]) writer.writerows(rows) print(f"写入 {len(rows)} 条到 {out_csv}") batch_predict("test_images", model, class_names, device)out_csv里保留了文件名、预测标签和置信度三列,产线追溯时按文件名就能定位到原图。置信度这一列别丢,后面做阈值筛选、挑出低置信样本人工复检都靠它。如果图片量很大,os.listdir一次性读进来内存吃紧,可以换成os.scandir迭代,但一般几千张以内没必要优化。
5. 避坑与排查:这套鞋面缺陷代码最容易翻车的五个地方
5.1 现象:训练 loss 正常下降,验证准确率始终在随机水平
原因通常是标签和图片没对上。ImageFolder按文件夹名映射标签,如果文件夹名有中文、空格或者顺序和你的class_names不一致,标签就错位了。另一个可能是验证集和训练集用了不同的 transform,验证集忘了 Normalize。
解决:先打印train_set.classes和train_set.class_to_idx,确认映射关系;再检查验证集的 transform 是否和训练集一致(除了数据增强部分)。我一般会在训练前抽几张图连同标签可视化一遍,肉眼确认没对错。
5.2 现象:Windows 上 DataLoader 报 BrokenPipeError 或直接卡死
原因:num_workers > 0时 PyTorch 在 Windows 下用 spawn 启动子进程,如果训练代码没有放在if __name__ == "__main__":保护块里,子进程会重复执行主模块导致崩溃。
解决:把训练入口包进if __name__ == "__main__":,或者干脆把num_workers设成 0。设 0 会慢一些,但调试阶段稳定优先,等跑通了再往上加。
5.3 现象:显存不够,报 CUDA out of memory
原因:batch_size太大,或者输入尺寸 224 对某些小显存卡来说偏高,又或者验证阶段忘了加torch.no_grad()导致计算图一直累积。
解决:先把batch_size降到 16 或 8;还不行就把输入尺寸降到 128;验证和推理代码务必用with torch.no_grad():包住。另外loss.item()要取标量,别把带梯度的 tensor 存进列表,那也会占显存。
5.4 现象:模型把所有样本都预测成样本最多的那一类
原因:类别不平衡加上没用重采样或加权 loss,模型发现全猜多数类就能拿到不错的 accuracy,于是躺平。
解决:用第 2 章里的WeightedRandomSampler,或者给CrossEntropyLoss传weight参数,按类别频次反比设置。同时把评估指标从 accuracy 换成每类召回率和 F1,这样少数类的表现才看得见。
5.5 现象:推理时单张图预测结果和训练时验证结果差很多
原因:预处理不一致,最常见的是推理时忘了 Normalize,或者Resize的插值方式不同,又或者图片通道数不对(灰度图没转 RGB)。
解决:把训练和推理的 transform 抽成同一个函数或同一个变量,别两处各写一遍。推理前统一convert("RGB")。如果还是对不上,把推理时的 tensor 反归一化后存成图片,和原图对比看是不是预处理阶段就变形了。
6. 把鞋面缺陷模型用起来:置信度阈值与误检复核的实操技巧
训练跑通只是第一步,真正上线要解决的是「模型说有问题,到底信不信」。鞋面缺陷检测里,漏检(把缺陷判成正常)和误检(把正常判成缺陷)的代价不一样,通常漏检更致命,所以阈值不能一刀切。我的做法是先把验证集所有样本的预测置信度导出来,按类别画一下分布,找到正常类和缺陷类置信度重叠的那段区间,把阈值定在重叠区偏缺陷一侧,宁可多报一点让人工复核。
import numpy as np # 假设 val_probs 是验证集所有样本的 softmax 输出, val_labels 是真实标签 # 找出缺陷类(非 normal)的置信度分布 defect_idx = [i for i, c in enumerate(class_names) if c != "normal"] defect_conf = val_probs[:, defect_idx].max(axis=1) normal_conf = val_probs[:, class_names.index("normal")] for th in np.arange(0.5, 0.95, 0.05): # 缺陷置信度高于阈值就判缺陷,否则判正常 pred = (defect_conf > th).astype(int) true = (val_labels != class_names.index("normal")).astype(int) tp = ((pred == 1) & (true == 1)).sum() fn = ((pred == 0) & (true == 1)).sum() fp = ((pred == 1) & (true == 0)).sum() recall = tp / (tp + fn + 1e-6) precision = tp / (tp + fp + 1e-6) print(f"th={th:.2f} recall={recall:.3f} precision={precision:.3f}")这段代码遍历 0.5 到 0.9 的阈值,每个阈值下算缺陷类的召回率和精确率。召回率高意味着漏检少,精确率低意味着误检多、人工复核量大。产线上根据人力配置选一个平衡点,比如要求召回率不低于 0.95,那就选满足这个条件里精确率最高的阈值。1e-6是防止除零,验证集里如果没有缺陷样本,分母会是 0。
另一个实用技巧是保存误检样本。每次批量推理后,把置信度在阈值附近(比如阈值上下 0.1 区间)的图片单独拷到一个文件夹,定期看这些图,你会发现模型反复在某一类纹理或某种光照条件下出错。把这些图补进训练集重新微调,比盲目加数据增强有效得多。我一般会保留一个hard_cases文件夹,每次迭代往里加,模型版本更新时优先用这批数据验证有没有改善。
从那以后我每次拿到新的缺陷数据集,都强制先跑一遍类别分布统计和预处理一致性检查,再动模型结构。这套鞋面缺陷识别的代码包把数据、训练、推理都串好了,省掉的是搭框架的时间,但数据本身的问题还是得自己盯。希望帮到你。
本文还有配套的精品资源,点击获取