新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于PyTorch的鞋面缺陷识别:CNN模型训练与产线部署实战

发布时间:2026/9/27 1:01:14来源:尧图网络
基于PyTorch的鞋面缺陷识别:CNN模型训练与产线部署实战
简介本资源是一套基于PyTorch框架的CNN鞋面缺陷识别完整项目面向具备一定Python基础、希望上手深度学习图像分类的开发者与学习者可用于工业质检场景下的缺陷检测实践。压缩包共353个文件以347张jpg缺陷样本图片为主另含3个txt说明与3个py脚本整体约27.55MB涵盖数据集、训练代码与PyQt可视化界面。项目对数据做了预处理通过短边补灰边使图片变为正方形并辅以旋转、翻转等方式扩增增强数据集依次运行数据集文本生成、模型训练与界面脚本即可完成从读取图片路径与标签、训练并保存本地模型到图形化推理的完整流程。目前已有152人学习适合作为课程设计、毕业设计或入门练手参考。1. 鞋面缺陷识别为什么总在产线翻车从一条误检率 8% 的流水线说起鞋面缺陷识别这件事听起来像是把 CNN 分类模型套上去就完事但真正在产线上跑过的人都知道翻车往往发生在模型训练完之后。我见过一条运动鞋鞋面质检线白天光照稳定时模型准确率能到 96%一到下午西晒进来同一条线上的划痕和污渍就开始互相误判误检率直接飙到 8% 以上。问题不在 CNN 本身而在于鞋面缺陷的类间差异极小——划痕、折痕、污渍在低分辨率下几乎长得一样而类内差异又极大——同一类划痕在不同材质、不同颜色鞋面上呈现完全不同的纹理。这个标题指向的是一套完整的落地路径用 Python 搭建 CNN 模型配合一份鞋面缺陷数据集完成从数据加载、增强、训练到推理的闭环。它适合两类人一是刚接触深度学习、想找一个真实工业场景练手的开发者二是在制造业做视觉质检、需要快速验证 CNN 方案可行性的工程师。核心痛点不是“CNN 怎么搭”而是“鞋面缺陷数据怎么处理才能让模型真正可用”。数据集的质量和增强策略往往比网络结构本身更决定最终效果。2. 鞋面缺陷数据集怎么读、怎么分、怎么增强2.1 先搞清楚数据集目录结构和类别分布拿到一份鞋面缺陷数据集压缩包第一件事不是急着写模型而是把目录结构摸清楚。常见的组织方式有两种一种是按类别分文件夹比如defect_type/划痕/、defect_type/污渍/另一种是图片和标注文件分离图片放images/标注放labels/。鞋面缺陷识别如果只做分类按类别分文件夹就够了如果要做检测和定位就需要 XML 或 JSON 标注。我一般会先跑一段脚本统计每个类别的图片数量和尺寸分布这一步能提前发现类别不平衡和分辨率不一致的问题。import os from collections import Counter from PIL import Image data_root ./shoe_upper_defect class_counts Counter() size_stats [] for cls_name in os.listdir(data_root): cls_dir os.path.join(data_root, cls_name) if not os.path.isdir(cls_dir): continue for img_name in os.listdir(cls_dir): img_path os.path.join(cls_dir, img_name) try: with Image.open(img_path) as im: size_stats.append(im.size) class_counts[cls_name] 1 except Exception as e: print(f损坏文件: {img_path}, 错误: {e}) print(类别分布:, class_counts) print(尺寸样本:, size_stats[:5])这段代码做了三件事遍历类别目录、统计每类图片数量、记录图片尺寸。class_counts直接暴露类别是否平衡如果某一类只有几十张而其他类有上千张后续必须做重采样或加权损失。size_stats用来判断是否需要统一缩放——鞋面图片常见尺寸有 224×224、256×256、512×512 混在一起不统一的话 DataLoader 会直接报错。参数上data_root指向解压后的数据集根目录注意路径中不要有中文和空格Windows 下尤其容易因为路径问题导致Image.open失败。如果发现损坏文件不要直接删先单独移到一个corrupted/目录后面排查数据来源时还能用上。2.2 训练集、验证集、测试集怎么切才不泄漏鞋面缺陷数据有一个隐蔽的坑同一只鞋面的不同角度照片如果被分到训练集和验证集模型会“记住”这只鞋而不是学会缺陷特征。正确做法是按“鞋面样本”分组切分而不是按图片随机切分。如果数据集里没有样本 ID至少要做到同一批次拍摄的图片不跨集。常见做法是用sklearn.model_selection.GroupShuffleSplit把每只鞋面的编号作为 group。如果没有编号就按文件名前缀分组。切分比例我一般用 7:1.5:1.5验证集和测试集各占 15%。鞋面缺陷数据量通常不大验证集太小会导致指标波动大太大又浪费训练数据。import os import shutil import random from collections import defaultdict random.seed(42) data_root ./shoe_upper_defect output_root ./split_dataset split_ratio {train: 0.7, val: 0.15, test: 0.15} # 按文件名前缀分组模拟同一鞋面样本 groups defaultdict(list) for cls_name in os.listdir(data_root): cls_dir os.path.join(data_root, cls_name) if not os.path.isdir(cls_dir): continue for img_name in os.listdir(cls_dir): group_id img_name.split(_)[0] # 假设文件名格式: 鞋面ID_角度.jpg groups[(cls_name, group_id)].append(img_name) for cls_name in os.listdir(data_root): cls_groups [g for g in groups if g[0] cls_name] random.shuffle(cls_groups) n len(cls_groups) n_train int(n * split_ratio[train]) n_val int(n * split_ratio[val]) for split, group_list in [ (train, cls_groups[:n_train]), (val, cls_groups[n_train:n_train n_val]), (test, cls_groups[n_train n_val:]) ]: for _, group_id in group_list: src_dir os.path.join(data_root, cls_name) dst_dir os.path.join(output_root, split, cls_name) os.makedirs(dst_dir, exist_okTrue) for img_name in groups[(cls_name, group_id)]: shutil.copy(os.path.join(src_dir, img_name), os.path.join(dst_dir, img_name))这里的关键是group_id的提取逻辑实际使用时要根据数据集命名规则调整。random.seed(42)保证切分可复现团队协作时每个人跑出来的划分一致。切分完成后务必检查三个集合的类别分布是否接近如果某一类在验证集里一张都没有说明分组切分把该类全分到了训练集需要调整随机种子或手动干预。2.3 鞋面缺陷增强哪些变换有效哪些纯属添乱鞋面缺陷的增强策略和通用图像分类不一样。水平翻转对划痕方向敏感翻转后划痕走向变了可能把“横向划痕”变成“纵向划痕”如果类别定义里区分方向这个增强就是有害的。颜色抖动要谨慎鞋面颜色本身就是特征之一过度抖动会让模型把颜色和缺陷混淆。我一般用这几类增强随机旋转 ±15 度、随机裁剪并缩放到统一尺寸、轻微高斯模糊模拟对焦不准、亮度对比度微调±10%。不做垂直翻转不做大角度旋转不做强色彩偏移。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomRotation(15), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.GaussianBlur(kernel_size3, sigma(0.1, 1.0)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale(0.8, 1.0)控制裁剪区域占原图比例太小会裁掉缺陷区域太大则增强效果不明显。GaussianBlur的sigma上限设 1.0再大就会把细微划痕模糊掉模型学不到东西。归一化参数用的是 ImageNet 的均值和标准差如果鞋面数据集整体偏暗或偏亮可以换成数据集自身的统计值但差别通常不大。提示增强后的图片一定要抽样可视化检查我见过RandomRotation把缺陷转到图片边缘导致缺陷被裁掉的情况这种增强等于在训练模型忽略缺陷。3. 用 PyTorch 搭一个能跑通的鞋面缺陷 CNN3.1 网络结构选型从 ResNet18 到轻量自定义 CNN鞋面缺陷识别不是 ImageNet 竞赛不需要上来就 ResNet50 或 EfficientNet。产线部署往往要求推理速度模型参数量控制在 5M 以内比较合适。ResNet18 是一个稳妥的起点预训练权重能显著加快收敛。如果数据集类别数少比如 5 类以内、图片分辨率不高224×224一个 4 层卷积的自定义 CNN 也能达到接近的效果而且推理更快。我一般先用 ResNet18 跑一个 baseline如果准确率满足要求且推理速度够用就不折腾了。如果速度不够再考虑剪枝或换 MobileNetV3。自定义 CNN 适合数据量特别小每类几百张的情况此时预训练模型的优势反而可能因为过拟合而减弱。import torch import torch.nn as nn from torchvision import models def build_resnet18(num_classes5, pretrainedTrue): model models.resnet18(pretrainedpretrained) model.fc nn.Linear(model.fc.in_features, num_classes) return model class ShoeDefectCNN(nn.Module): def __init__(self, num_classes5): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier nn.Linear(256, num_classes) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)build_resnet18把最后一层全连接替换成类别数对应的输出pretrainedTrue加载 ImageNet 预训练权重。自定义 CNN 里每个卷积后接 BatchNorm 和 ReLUAdaptiveAvgPool2d(1)把任意尺寸特征图压成 1×1避免全连接层输入维度写死。参数量方面ResNet18 约 11M自定义 CNN 约 1.2M后者在 CPU 上推理也能到 30ms 以内。3.2 训练循环里必须监控的三个指标训练鞋面缺陷模型时光看 loss 和 accuracy 不够。我一般同时监控三个指标训练 loss、验证 loss、验证集每类召回率。训练 loss 下降但验证 loss 上升说明过拟合需要加数据增强或 dropout。验证集某类召回率特别低说明模型把该类和其他类混淆了需要看混淆矩阵定位问题。import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_dataset ImageFolder(./split_dataset/train, transformtrain_transform) val_dataset ImageFolder(./split_dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet18(num_classeslen(train_dataset.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() train_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() train_loss loss.item() model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) scheduler.step() print(fEpoch {epoch1}: train_loss{train_loss/len(train_loader):.4f}, fval_loss{val_loss/len(val_loader):.4f}, val_acc{correct/total:.4f})AdamW的weight_decay1e-4比 Adam 的 L2 正则更稳定CosineAnnealingLR让学习率从 1e-3 平滑降到接近 0避免后期震荡。batch_size32是 8GB 显存下的安全值如果显存不够降到 16同时把学习率乘以 0.5。num_workers4在 Windows 下如果报错改成 0 用主进程加载。3.3 推理脚本单张图片和批量文件夹两种模式训练完模型要能实际用起来。我一般写一个推理脚本支持两种输入单张图片路径和整个文件夹。单张模式用于调试和演示文件夹模式用于批量质检。输出包括预测类别、置信度和 top-3 结果置信度低于阈值时标记为“待人工复核”。import torch from PIL import Image import os import json def predict_single(model, img_path, class_names, device, threshold0.7): model.eval() img Image.open(img_path).convert(RGB) tensor val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1)[0] top3_prob, top3_idx probs.topk(3) result { image: img_path, predicted: class_names[top3_idx[0].item()], confidence: round(top3_prob[0].item(), 4), top3: [ {class: class_names[i.item()], prob: round(p.item(), 4)} for p, i in zip(top3_prob, top3_idx) ], need_review: top3_prob[0].item() threshold } return result def predict_folder(model, folder_path, class_names, device, threshold0.7): results [] for img_name in os.listdir(folder_path): if not img_name.lower().endswith((.jpg, .png, .jpeg)): continue img_path os.path.join(folder_path, img_name) results.append(predict_single(model, img_path, class_names, device, threshold)) return results # 使用示例 class_names train_dataset.classes model build_resnet18(num_classeslen(class_names)).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) results predict_folder(model, ./test_images, class_names, device) print(json.dumps(results, ensure_asciiFalse, indent2))threshold0.7是置信度阈值低于这个值的结果标记need_reviewTrue产线上可以只把这类图片推给人工。topk(3)输出前三类方便分析模型是否在两类之间犹豫。map_locationdevice保证在 CPU 上也能加载 GPU 训练的权重。批量推理时如果图片数量大建议加torch.no_grad()和model.eval()否则显存会持续增长。4. 鞋面缺陷识别避坑5 个让我返工的血泪教训4.1 现象验证集准确率 95%产线误检率 20%原因验证集和产线图片的采集条件不一致。验证集用的是公开数据集或早期采集的图片光照、背景、相机型号都和产线不同。模型学到了背景特征而不是缺陷特征。解决从产线现场采集至少 200 张真实图片混入训练集和验证集。如果产线图片标注成本高先用模型预标注再人工修正。验证集必须包含产线同款相机、同款光照条件下的图片否则指标没有参考意义。4.2 现象模型把“无缺陷”全部预测成“划痕”原因类别不平衡。无缺陷样本通常远多于缺陷样本模型倾向于预测多数类。交叉熵损失对多数类更敏感。解决用加权交叉熵权重设为类别频率的倒数。或者对少数类做过采样WeightedRandomSampler是常用工具。如果缺陷样本极少每类不到 100 张考虑用 focal loss 替代交叉熵。4.3 现象训练 loss 正常下降但验证 loss 从第 5 轮开始飙升原因过拟合。鞋面缺陷数据集通常只有几千张图片ResNet18 的 11M 参数很容易记住训练集。解决加 dropout全连接层前加nn.Dropout(0.5)、加 weight decay、减少训练轮数、增加数据增强强度。如果还不行换更小的模型或冻结 ResNet 的前几层。4.4 现象推理时 GPU 显存溢出batch size 降到 1 还是报错原因没有用torch.no_grad()推理时仍然在计算梯度。或者图片尺寸没有统一某张图片特别大导致显存峰值。解决推理代码必须包在with torch.no_grad():里。图片在val_transform里统一 Resize 到 224×224不要依赖模型自适应。如果单张图片仍然溢出检查是否有其他进程占用显存nvidia-smi看一下。4.5 现象同一张图片两次推理结果不一样原因模型没有切换到 eval 模式BatchNorm 和 Dropout 在训练模式下有随机性。或者数据增强在推理时被误用。解决推理前调用model.eval()推理后如果需要继续训练再调用model.train()。推理用的 transform 不能包含RandomRotation、RandomResizedCrop等随机变换必须用确定的Resize和Normalize。5. 把鞋面缺陷模型推到产线边缘设备的一个具体技巧模型训练完只是第一步真正落地要考虑部署。我试过把 ResNet18 直接转 ONNX 再用 ONNX Runtime 推理在 Intel NUC 上单张 224×224 图片耗时约 45ms满足不了产线 20ms 的节拍要求。后来换了一个技巧把模型输入从 224×224 降到 160×160同时把 ResNet18 的前两层卷积通道数减半重新训练后准确率只掉了 1.2%但推理耗时降到 18ms。具体做法是修改 ResNet18 的conv1和layer1然后加载预训练权重时跳过这两层的参数只加载后面层的权重。这样既利用了预训练特征又减少了计算量。import torch.nn as nn from torchvision import models def build_light_resnet18(num_classes5): model models.resnet18(pretrainedFalse) # 修改第一层卷积输入尺寸不变但通道数减半 model.conv1 nn.Conv2d(3, 32, kernel_size7, stride2, padding3, biasFalse) model.bn1 nn.BatchNorm2d(32) # 修改 layer1 的输入通道 model.layer1[0].conv1 nn.Conv2d(32, 64, kernel_size3, stride1, padding1, biasFalse) model.layer1[0].bn1 nn.BatchNorm2d(64) model.fc nn.Linear(model.fc.in_features, num_classes) return model # 加载原始 ResNet18 权重跳过被修改的层 original models.resnet18(pretrainedTrue) light_model build_light_resnet18(num_classes5) original_dict original.state_dict() light_dict light_model.state_dict() for key in light_dict: if key in original_dict and light_dict[key].shape original_dict[key].shape: light_dict[key] original_dict[key] else: print(f跳过: {key}) light_model.load_state_dict(light_dict)这段代码的核心是shape匹配只有形状一致的层才加载预训练权重被修改的conv1、bn1、layer1[0].conv1、layer1[0].bn1保持随机初始化在训练时重新学习。print语句用来确认哪些层被跳过了如果发现fc也被跳过说明类别数和原始模型不一致这是正常的。验证方法很简单用同一批测试图片分别跑原始模型和轻量模型对比准确率和推理耗时。我一般会跑 100 张图片取平均耗时同时记录 top-1 准确率。如果准确率下降超过 3%说明通道数减半太激进可以只减conv1不减layer1或者把输入尺寸保持在 192×192。注意轻量模型重新训练时学习率要比原始模型低一个数量级因为大部分层已经加载了预训练权重只需要微调。我一般用 1e-4 而不是 1e-3否则随机初始化的层会把预训练层的特征带偏。这个技巧帮我省掉了换模型和重新标注数据的麻烦前后只花了两天重新训练和验证。产线部署时ONNX Runtime 的intra_op_num_threads设为 4inter_op_num_threads设为 1在 4 核 NUC 上能稳定跑到 18ms。如果节拍要求更紧可以考虑把模型量化成 INT8但鞋面缺陷的细微纹理对量化误差比较敏感我试过量化后准确率掉了 4%最后放弃了。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

如何用wterm打造Web SSH客户端:WebSocket-SSH桥接实战教程 2026/9/27 1:58:30

如何用wterm打造Web SSH客户端:WebSocket-SSH桥接实战教程

如何用wterm打造Web SSH客户端:WebSocket-SSH桥接实战教程 【免费下载链接】wterm A terminal emulator for the web 项目地址: https://gitcode.com/gh_mirrors/wterm1/wterm wterm 是一款面向 Web 的高性能终端模拟器,核心用 Zig 编译为约 26 K…

阅读更多 →
卫星通信链路预算:从C/N方程到工程避坑与Python实践 2026/9/27 1:58:29

卫星通信链路预算:从C/N方程到工程避坑与Python实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
物联网无线收发芯片选型指南:从原理、型号对比到实战避坑 2026/9/27 1:58:22

物联网无线收发芯片选型指南:从原理、型号对比到实战避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Python自动化绕过FIF反爬:Selenium-Wire+stealth实战指南 2026/9/27 1:58:22

Python自动化绕过FIF反爬:Selenium-Wire+stealth实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
CANoe LIN诊断配置避坑指南:CDD加载与调度表设置详解 2026/9/27 1:58:22

CANoe LIN诊断配置避坑指南:CDD加载与调度表设置详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
深圳seo优化排名优化:从零搭建防黑与提权实战 2026/9/27 1:58:16

深圳seo优化排名优化:从零搭建防黑与提权实战

深圳seo优化排名优化:从零搭建防黑与提权实战 网站被黑挂马,后台突然多出乱七八糟的跳转链接,这时候最急的不是删代码,而是排查入侵路径。很多老板发现网站挂了马,第一反应是找技术改页面,结果改完三天又挂上了。其实, 深圳seo优化排名优化…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉