ARTICLE DETAIL

资讯详情

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

基于CNN的鞋面缺陷识别:数据集处理与模型训练实战

基于CNN的鞋面缺陷识别:数据集处理与模型训练实战 简介本资源面向深度学习入门与计算机视觉实践者提供一套基于PyTorch的鞋面缺陷识别完整方案可用于工业质检场景的课程设计、毕业设计或算法练手。压缩包共353个文件以347张jpg缺陷样本图片为核心数据辅以3个txt说明与3个py脚本整体约27.55MB体积轻便易于本地运行。代码按流程拆分为数据集文本生成、模型训练与PyQt界面三个环节先读取各类别图片路径与标签再完成训练并保存本地模型最后通过图形界面演示识别效果。数据预处理阶段采用短边补灰边使图片变为正方形并叠加旋转角度进行增强有效扩充样本多样性。已有152人学习适合希望快速跑通CNN分类全流程、理解数据增强与界面部署的读者参考。1. 鞋面缺陷识别为什么值得用 CNN 重做一遍鞋面缺陷识别这件事放在工厂质检线上看本质是一个「小目标 弱对比 多类别」的视觉分类问题。鞋面材质有网布、超纤、真皮、飞织缺陷类型常见的有跳线、断纱、油污、色差、破洞、异物压痕这些缺陷在整幅图像里往往只占很小一块区域灰度差异也不大传统阈值分割和模板匹配在这种场景下经常翻车。基于 CNN 深度学习 python 的鞋面缺陷识别方案配合一份标注好的数据集正好能把这类问题从「靠老师傅肉眼盯」变成「模型批量过图 人工只复核可疑样本」。这套方案适合三类人一是做工业质检的算法工程师想找一个能直接跑通的缺陷分类 baseline二是刚学完 CNN 基础、想找一个真实数据集练手的学生或转行者三是产线上做自动化改造的工程师需要评估深度学习方案到底能不能落地。它解决的核心问题不是「识别率能不能到 99%」而是「在有限样本、有限算力下能不能稳定区分正常鞋面和几类高频缺陷」。数据集的存在让这件事从空谈变成可复现——你不需要自己去产线拍几千张图直接拿现成的标注数据就能把训练、验证、推理整条链路走一遍。2. 数据集怎么读、怎么切、怎么增强才不白干2.1 先搞清楚数据集目录结构和类别分布拿到一个鞋面缺陷数据集压缩包第一件事不是急着写模型而是把目录结构、图片数量、类别分布摸清楚。常见做法是train/val/test三个子目录每个子目录下按类别名建文件夹这是ImageFolder能直接吃的格式。但实际拿到的数据集经常是「一个大文件夹 一个 CSV 标注」或者「按缺陷编号命名」的形式这时候就得先写脚本转成标准结构。import os from collections import Counter from pathlib import Path # 假设数据集根目录下每个子文件夹是一个类别 root Path(shoe_upper_defect) class_counts Counter() for cls_dir in sorted(root.iterdir()): if cls_dir.is_dir(): imgs list(cls_dir.glob(*.jpg)) list(cls_dir.glob(*.png)) class_counts[cls_dir.name] len(imgs) for name, cnt in class_counts.items(): print(f{name}: {cnt}) total sum(class_counts.values()) print(ftotal: {total}) # 关键看最小类别和最大类别的比例超过 1:5 就要考虑重采样或加权这段脚本的作用是统计每个类别的图片数量。参数上没什么可调的但输出结果决定了后面几步的策略如果最小类别只有几十张而最大类别有上千张直接训练会让模型偏向多数类这时候要么对少数类做过采样要么在损失函数里加类别权重。我一般会先跑一遍这个统计把类别分布记下来后面调WeightedRandomSampler或者CrossEntropyLoss(weight...)时直接用。2.2 划分训练验证集时别让同一只鞋跨集鞋面缺陷数据集有一个很容易被忽略的坑同一只鞋、同一个拍摄工位可能拍了多张图这些图如果被随机分到训练集和验证集验证指标会虚高。正确做法是按「鞋号」或「拍摄批次」做分组划分保证同一只鞋的图只出现在一个集合里。如果数据集本身没提供分组信息至少要用文件名前缀或时间戳做粗分组。import random from pathlib import Path from sklearn.model_selection import GroupShuffleSplit # 假设文件名格式为 shoeID_xxx.jpg用下划线前部分作为分组键 files, labels, groups [], [], [] for cls_dir in sorted(Path(shoe_upper_defect).iterdir()): if cls_dir.is_dir(): for img in cls_dir.glob(*.jpg): files.append(str(img)) labels.append(cls_dir.name) groups.append(img.stem.split(_)[0]) # 分组键 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(files, labels, groups)) print(ftrain: {len(train_idx)}, val: {len(val_idx)}) # 检查两组的分组键有没有交集有交集说明划分失败 train_groups set(groups[i] for i in train_idx) val_groups set(groups[i] for i in val_idx) assert not (train_groups val_groups), 分组泄漏GroupShuffleSplit的groups参数就是分组键test_size0.2表示验证集占 20%。最后那个assert是后悔药——如果分组键有交集说明同一只鞋的图跨了集验证结果不可信。这个检查我每次划分完都会跑一遍比事后怀疑模型过拟合要省事得多。2.3 增强策略要贴着缺陷类型来设计鞋面缺陷的增强不能照搬 ImageNet 那套。水平翻转对跳线、断纱这类有方向性的缺陷会改变语义随机裁剪可能把本来就小的缺陷裁掉。比较稳的组合是轻度旋转±10°、亮度对比度微调、高斯噪声、以及小范围的随机缩放。颜色抖动要慎用因为色差本身就是一类缺陷你把颜色抖没了模型就学不到色差特征。import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf A.Compose([ A.Rotate(limit10, p0.5), # 小角度旋转保留方向语义 A.RandomBrightnessContrast(0.1, 0.1, p0.5), # 轻度亮度对比度扰动 A.GaussNoise(var_limit(5.0, 20.0), p0.3), # 模拟拍摄噪声 A.Resize(224, 224), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) val_tf A.Compose([ A.Resize(224, 224), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ])Rotate的limit10是血泪经验超过 15° 之后跳线缺陷的方向就乱了。GaussNoise的var_limit控制在 5 到 20 之间再大就会把细小缺陷淹没。验证集只做 resize 和归一化不做任何随机增强否则每次验证结果都在抖没法判断模型到底有没有进步。归一化参数用的是 ImageNet 的均值方差如果你从零训练而不是用预训练权重可以改成自己数据集的统计值但用预训练权重时就必须保持一致。3. 用 CNN 搭一个能跑通的鞋面缺陷分类器3.1 骨干网络选 ResNet18 还是自己搭轻量 CNN选型这件事取决于你的算力和数据量。如果数据集只有几千张图自己搭一个 4 层卷积 全局池化的小网络就够训练快、过拟合风险低。如果数据量上万或者你想用预训练权重加速收敛ResNet18 是性价比最高的选择——比 ResNet50 轻比 MobileNet 稳在 224×224 输入下显存占用不到 2GB。我一般会先用 ResNet18 跑一个 baseline如果验证集准确率卡在某个值上不去再考虑换骨干或加注意力模块。import torch import torch.nn as nn from torchvision import models def build_model(num_classes, pretrainedTrue): if pretrained: # 用 ImageNet 预训练权重加快收敛 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) else: model models.resnet18(weightsNone) # 替换最后的全连接层输出类别数改成缺陷类别数 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), # 防过拟合 nn.Linear(in_features, num_classes) ) return model model build_model(num_classes6) print(sum(p.numel() for p in model.parameters() if p.requires_grad)) # 输出可训练参数量ResNet18 大约 1100 万Dropout(0.3)加在全连接层前面是因为鞋面缺陷数据集通常不大分类头容易过拟合。pretrainedTrue时用的是 ImageNet 权重输入归一化必须和 ImageNet 一致否则预训练权重的优势会被抵消。参数量打印出来是为了心里有数——如果显存不够可以冻结前面的卷积层只训练分类头这样可训练参数会降到几万。3.2 训练循环里必须盯住的三个量训练脚本谁都会写但能不能跑出结果取决于你有没有盯对指标。我一般会在每个 epoch 记录三个量训练损失、验证损失、验证集上每个类别的召回率。训练损失降但验证损失升说明过拟合验证损失震荡说明学习率太大或 batch size 太小某个类别召回率特别低说明该类样本太少或特征不明显。import torch from torch.utils.data import DataLoader from sklearn.metrics import recall_score def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) return total_loss / len(loader.dataset) torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, all_preds, all_labels 0, [], [] for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) preds outputs.argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) avg_loss total_loss / len(loader.dataset) recalls recall_score(all_labels, all_preds, averageNone) return avg_loss, recalls # 训练主循环 device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes6).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): train_loss train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, recalls evaluate(model, val_loader, criterion, device) scheduler.step() print(fepoch {epoch}: train_loss{train_loss:.4f} fval_loss{val_loss:.4f} recalls{recalls})AdamW的weight_decay1e-4是解耦权重衰减比 Adam 自带的 L2 更稳。CosineAnnealingLR的T_max设成总 epoch 数让学习率从 1e-3 平滑降到接近 0。recall_score的averageNone会返回每个类别的召回率这样你能看到是哪个类别在拖后腿。如果某个类别召回率长期低于 0.6优先检查该类样本数量其次检查增强是否过度。3.3 推理阶段怎么把模型输出变成可复核的结果训练完模型只是第一步产线上要的是「这张图是什么缺陷、置信度多少、要不要人工复核」。推理脚本要输出结构化的结果而不是只打印一个类别名。我一般会输出 top-2 类别和对应置信度置信度低于阈值的自动标记为「待复核」这样人工只需要看一小部分图。import torch.nn.functional as F from PIL import Image torch.no_grad() def predict(image_path, model, transform, class_names, device, threshold0.7): model.eval() img Image.open(image_path).convert(RGB) tensor transform(image__import__(numpy).array(img))[image].unsqueeze(0).to(device) logits model(tensor) probs F.softmax(logits, dim1)[0] top2_prob, top2_idx probs.topk(2) result { top1: class_names[top2_idx[0]], top1_conf: round(top2_prob[0].item(), 4), top2: class_names[top2_idx[1]], top2_conf: round(top2_prob[1].item(), 4), need_review: top2_prob[0].item() threshold } return result # 示例输出 # {top1: 跳线, top1_conf: 0.92, top2: 断纱, top2_conf: 0.05, need_review: False}threshold0.7是复核阈值低于这个值就转人工。这个值不是拍脑袋定的要在验证集上跑一遍看不同阈值下「漏检率」和「复核率」的权衡。如果产线对漏检容忍度低阈值可以调到 0.5如果人工复核成本高阈值调到 0.85。top2输出的意义在于当 top1 和 top2 置信度接近时人工可以快速判断是不是混淆类别比如跳线和断纱在低分辨率下确实容易混。4. 鞋面缺陷识别里最容易翻车的几个地方4.1 验证集准确率很高但产线上完全不能用现象验证集准确率 95% 以上但拿产线新拍的图一测准确率掉到 60% 以下。原因通常是训练集和产线图的拍摄条件不一致——光照、焦距、背景、鞋面摆放角度都变了。解决方法是做域适应要么在产线图上重新标注一批做微调要么在训练时加入强光照扰动和背景替换增强。我一般会先用产线图跑一遍推理把置信度低的样本挑出来人工标注再拿这批数据微调最后几层。4.2 模型把「正常」样本也判成缺陷现象正常鞋面被大量误判为某类缺陷产线频繁报警。原因多半是正常类样本太少或者正常类内部差异太大不同材质、不同颜色的正常鞋面被当成一个类。解决办法是增加正常类样本的多样性或者在损失函数里给正常类更高的权重。另一个常见原因是阈值设太低把置信度 0.5 的预测也当结果输出调高阈值能立刻缓解。4.3 训练损失不下降模型像没学一样现象训练几个 epoch 后损失还在初始值附近震荡准确率跟随机猜差不多。原因可能是学习率太大导致梯度爆炸也可能是数据归一化没做对输入像素值还在 0 到 255 之间。检查方法是打印一个 batch 的输入张量看数值范围是不是在 0 附近。如果是学习率问题把 lr 从 1e-3 降到 1e-4 再试如果是归一化问题补上Normalize就行。4.4 显存不够batch size 只能设到 4现象训练时 CUDA out of memory只能把 batch size 降到很小但小 batch 导致训练不稳定。解决办法有三个一是冻结骨干网络的前几层减少反向传播的显存占用二是用混合精度训练显存能省将近一半三是把输入分辨率从 224 降到 160但要注意小缺陷可能因此看不清。我一般优先试混合精度改动最小。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() with autocast(): outputs model(imgs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast会自动把部分计算转成 float16GradScaler负责防止梯度下溢。这套组合在 ResNet18 上基本能省 40% 到 50% 显存代价是训练速度可能略慢但 batch size 能翻倍总体是划算的。4.5 类别不平衡导致少数类召回率极低现象整体准确率看着不错但某个缺陷类别的召回率只有 0.3等于漏检了七成。原因就是类别不平衡模型学会了「只要拿不准就猜多数类」。解决办法除了前面说的加权损失还可以用WeightedRandomSampler在采样阶段就让少数类被多抽到。from torch.utils.data import WeightedRandomSampler import numpy as np # class_counts 是前面统计出来的每类数量 class_weights 1.0 / np.array(list(class_counts.values())) sample_weights [class_weights[label] for label in train_labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)replacementTrue表示有放回采样少数类会被重复抽到。num_samples设成训练集大小保证每个 epoch 的样本量和原来一致。这个采样器和加权损失可以同时用但要注意别把权重调得太极端否则模型会对少数类过拟合。5. 把模型推到产线前我会先做这三件事第一件事是做一个「混淆矩阵 置信度分布」的联合分析。混淆矩阵告诉你哪些类别在互相混置信度分布告诉你模型在哪些样本上犹豫。我一般会把验证集里所有预测错误的样本按置信度排序看前 20 个错得最离谱的图往往能发现标注错误或者数据集里的脏数据。这一步比调参有用得多因为如果标注本身有问题模型再强也学不对。第二件事是固定一个「黄金测试集」这个集合的图不参与任何训练和调参只在最后评估时用一次。黄金测试集要从产线真实分布里抽覆盖不同材质、不同光照、不同缺陷严重程度。每次改完模型或增强策略先看验证集再看黄金测试集两个都涨才说明改动有效。只看验证集容易被过拟合骗到。第三件事是给推理脚本加一个「批量跑图 结果导出 CSV」的模式。产线验收时不会让你一张一张测他们会给你一个文件夹让你跑完输出每张图的预测结果和置信度。这个 CSV 就是验收依据也是后续做数据回流的基础——把置信度低的样本挑出来人工复核后加入训练集下一版模型就能覆盖更多边界情况。检查项合格标准常见不达标原因黄金测试集准确率与验证集差距小于 5%验证集泄漏或分布不一致少数类召回率不低于 0.7类别不平衡未处理单张推理耗时GPU 上低于 20ms输入分辨率过大或模型过重低置信度占比低于 15%阈值设太低或模型欠拟合这张表是我在产线验收前会逐项过的清单。黄金测试集和验证集差距大说明数据划分有问题少数类召回率低说明采样或损失权重没调好推理耗时超标考虑换轻量骨干或降分辨率低置信度占比高说明模型对当前数据分布还不适应需要补数据。最后说一个我自己的习惯每次训练完模型我都会把权重文件、训练日志、验证集混淆矩阵、黄金测试集结果放在同一个文件夹里用日期命名。过一个月回头看能清楚知道哪一版模型在什么数据上表现如何比只存一个.pth文件靠谱得多。鞋面缺陷识别这个方向模型结构不是门槛数据质量和评估流程才是真正拉开差距的地方。希望帮到你。本文还有配套的精品资源点击获取
返回列表