ARTICLE DETAIL

资讯详情

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

ShuffleNet+CNN电力巡检缺陷分类:从模型到PyQt界面

ShuffleNet+CNN电力巡检缺陷分类:从模型到PyQt界面 简介这份资源面向深度学习入门者与电力巡检图像分析方向的开发者提供一套基于PyTorch的ShuffleNet卷积神经网络缺陷分类识别代码用于解决杆塔倾斜、导线脱落、绝缘子破损等巡检图像的多类别识别问题。压缩包共8个文件约215KB包含3个Python脚本、3张示例图片、1份说明文档和1份环境依赖文本脚本分别负责数据列表生成、CNN模型训练及PyQt可视化界面结构精简、上手门槛低。代码逐行附带中文注释并配有说明文档便于理解网络搭建、数据加载与训练流程。数据集图片需自行搜集按类别放入对应文件夹即可开始训练类别可灵活增删。目前已有104人学习适合希望快速跑通电力缺陷分类任务、掌握ShuffleNet实战用法的读者参考。1. 电力巡检图像缺陷分类为什么 ShuffleNet 是那个被低估的起点输电线路巡检从人工登塔拍照转向无人机自动巡检之后真正的瓶颈早就不是拍不拍得到而是拍回来几千张图谁来判。绝缘子自爆、防震锤滑移、销钉缺失、均压环歪斜这几类缺陷在整张巡检图里往往只占几十个像素背景却是天空、杆塔、植被混杂的大场景。把 CNN 卷积神经网络直接套上去做缺陷分类识别第一反应通常是上 ResNet50 或者 VGG16但真到部署环节——巡检终端算力有限、要现场出结果、甚至要塞进嵌入式盒子——参数量和推理延迟立刻变成硬约束。ShuffleNet 系列就是在这个夹缝里被反复翻出来的方案用分组卷积加通道混洗把计算量压到 MobileNet 量级甚至更低同时保留足够的特征表达能力。这套ShuffleNet 模型 CNN 缺陷分类 PyQt 界面的组合适合两类人一类是手里有巡检图像、想跑通一个能演示能落地的分类 pipeline 的学生和初级算法工程师另一类是做电力智能巡检产品、需要在边缘端验证模型可行性的工程人员。它不含数据集图片意味着你得自己准备数据但逐行注释和说明文档把网络结构、训练流程、界面调用这条链路讲透了反而比塞一堆图更实用。2. ShuffleNet 的通道混洗到底解决了什么问题2.1 从分组卷积的副作用说起要理解 ShuffleNet得先接受一个前提在移动端做 CNN 卷积神经网络1x1 卷积才是计算量的大头。标准卷积里 3x3 卷积看着参数多但现代网络大量堆叠 1x1 做通道升降维导致 1x1 的 FLOPs 占比经常超过 3x3。MobileNet 用深度可分离卷积砍掉了 3x3 的空间冗余可 1x1 那部分没动。ShuffleNet 的思路更激进既然 1x1 贵那就把 1x1 也做成分组卷积。分组之后每个组只处理一部分通道计算量按组数线性下降。但分组卷积有个副作用——组与组之间的通道信息不流通。第 1 组永远只看到第 1 组输入通道的特征第 2 组永远只看到第 2 组堆叠几层之后网络相当于几个互不相干的小网络并联特征表达被割裂。这就是通道混洗Channel Shuffle要补的洞在分组卷积之后把输出通道打散重排让下一层的每个组都能拿到来自上一层所有组的通道。操作本身不涉及任何参数纯靠张量 reshape 和 transpose 完成零计算成本。我一般会这样跟人解释分组卷积是分头干活省力气通道混洗是干完活交换情报两者必须成对出现缺了混洗的分组卷积就是自废武功。2.2 ShuffleNet 单元的结构拆解一个标准 ShuffleNet 基本单元stride1的顺序是1x1 分组卷积 → 通道混洗 → 3x3 深度可分离卷积 → 1x1 分组卷积。注意这里第一个 1x1 是分组卷积第二个 1x1 也是分组卷积中间夹一个深度卷积负责空间特征。stride2 的下采样单元则多一条分支主分支走 3x3 深度卷积stride2后接 1x1 分组卷积旁路分支用 3x3 平均池化stride2后接 1x1 分组卷积最后两路 concat。这个设计让下采样时通道数翻倍而计算量不爆炸。下面是用 PyTorch 复现通道混洗的核心代码也是整个网络里最容易写错的一段import torch import torch.nn as nn def channel_shuffle(x, groups): # x 形状: [batch, channels, height, width] batch, channels, height, width x.size() # 断言通道数能被组数整除否则 reshape 会报错 assert channels % groups 0, channels must be divisible by groups channels_per_group channels // groups # 第一步把通道维度拆成 (groups, channels_per_group) x x.view(batch, groups, channels_per_group, height, width) # 第二步转置让组间通道交错这是混洗的关键 x torch.transpose(x, 1, 2).contiguous() # 第三步展平回原始通道维度 x x.view(batch, -1, height, width) return x逻辑说明view把通道切成 groups 份transpose(1, 2)交换组和组内通道两个维度效果就是原来第 0 组的第 0 个通道、第 1 组的第 0 个通道、第 2 组的第 0 个通道……被排到了一起。contiguous()不能省transpose 之后内存不连续直接 view 会抛错这是新手最常见的翻车点。参数说明groups一般取 2 到 8ShuffleNet 原论文在 0.5x 到 2x 宽度下用 3 或 8。组数越大省算力越狠但混洗后特征越碎分类精度会掉。电力缺陷分类这种细粒度任务我建议 groups2 或 3 起步别一上来就 8。2.3 为什么不用 ResNet 直接上有人会问巡检缺陷分类数据量不大ResNet18 微调一下不香吗。香但要看部署目标。ResNet18 约 11M 参数、1.8 GFLOPsShuffleNet 0.5x 约 0.35M 参数、40 MFLOPs差了四十多倍计算量。在服务器上跑推理这点差距无所谓但要在巡检手持终端或者 Jetson 这类边缘设备上实时出结果ShuffleNet 能跑到 30 FPS 以上ResNet18 可能只有个位数。缺陷分类识别本身不是 ImageNet 那种千类任务电力巡检常见缺陷类别也就十到二十类ShuffleNet 的表达能力完全够用。选型不是比谁精度高零点几个点是比谁在约束条件下能跑起来。3. 从零搭一个能训练的 ShuffleNet 缺陷分类网络3.1 网络主体与分类头改造ShuffleNet 原版是为 ImageNet 1000 类设计的最后一层全连接输出 1000。做电力缺陷分类要把分类头换成自己的类别数。下面给出一个精简版 ShuffleNet 主干加分类头的实现import torch import torch.nn as nn class ShuffleNetUnit(nn.Module): def __init__(self, in_channels, out_channels, stride, groups): super().__init__() mid_channels out_channels // 4 # 瓶颈比例原论文用 1/4 self.stride stride self.groups groups # 第一个 1x1 分组卷积负责降维 self.conv1 nn.Conv2d(in_channels, mid_channels, 1, groupsgroups, biasFalse) self.bn1 nn.BatchNorm2d(mid_channels) # 3x3 深度可分离卷积groupsmid_channels 即逐通道卷积 self.conv2 nn.Conv2d(mid_channels, mid_channels, 3, stridestride, padding1, groupsmid_channels, biasFalse) self.bn2 nn.BatchNorm2d(mid_channels) # 第二个 1x1 分组卷积负责升维 self.conv3 nn.Conv2d(mid_channels, out_channels, 1, groupsgroups, biasFalse) self.bn3 nn.BatchNorm2d(out_channels) # stride2 时旁路需要下采样对齐尺寸 if stride 2: self.shortcut nn.Sequential( nn.AvgPool2d(3, stride2, padding1), nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels) ) else: self.shortcut nn.Identity() def forward(self, x): out torch.relu(self.bn1(self.conv1(x))) out channel_shuffle(out, self.groups) # 混洗在第一个 1x1 之后 out torch.relu(self.bn2(self.conv2(out))) out self.bn3(self.conv3(out)) # stride2 时两路 concat否则相加 if self.stride 2: out torch.cat([out, self.shortcut(x)], dim1) else: out out self.shortcut(x) return torch.relu(out)逻辑说明mid_channels out_channels // 4是瓶颈结构先降到 1/4 再升回去减少 3x3 卷积的通道负担。混洗放在第一个 1x1 之后、3x3 之前这是原论文的位置别乱挪。stride2 时旁路用平均池化加 1x1 卷积而不是直接跳连因为尺寸对不上。参数说明groups要和channel_shuffle里的 groups 保持一致否则混洗的组数和卷积的组数错位特征就乱了。biasFalse是因为后面跟了 BatchNorm偏置会被 BN 吸收加了纯属浪费。3.2 数据加载与缺陷类别映射标题明确说不含数据集图片所以数据得自己组织。电力巡检缺陷分类的目录结构我一般这样定dataset/ ├── train/ │ ├── insulator_broken/ # 绝缘子自爆 │ ├── damper_slip/ # 防震锤滑移 │ ├── pin_missing/ # 销钉缺失 │ └── normal/ # 正常 └── val/ ├── insulator_broken/ ├── damper_slip/ ├── pin_missing/ └── normal/用torchvision.datasets.ImageFolder直接读类别按文件夹名字母序自动映射成 0、1、2、3。这里有个坑训练集和验证集的文件夹顺序必须完全一致否则标签错位训练 loss 会诡异地震荡。我习惯在训练脚本开头打印train_dataset.class_to_idx和val_dataset.class_to_idx肉眼确认一遍再开跑。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((224, 224)), # ShuffleNet 常用输入尺寸 transforms.RandomHorizontalFlip(), # 巡检图水平翻转合理 transforms.RandomRotation(15), # 小角度旋转增强 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ]) train_set datasets.ImageFolder(dataset/train, transformtrain_tf) val_set datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4)逻辑说明训练集做翻转和旋转增强验证集只做 resize 和归一化这是铁律验证集加增强会让评估结果不可信。Normalize用的 ImageNet 均值方差因为主干是在 ImageNet 上预训练的输入分布要对齐。参数说明batch_size32在 8G 显存上跑 224x224 的 ShuffleNet 绰绰有余显存小就降到 16。num_workers在 Windows 上设 0 更稳Linux 上设 4 到 8。RandomRotation(15)别开太大巡检图旋转超过 30 度会出现大量黑边反而引入噪声。3.3 训练循环与学习率策略import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model ShuffleNet(num_classeslen(train_set.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): 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() scheduler.step() # 每个 epoch 后在验证集上评估 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 outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fEpoch {epoch1}, Loss {running_loss/len(train_loader):.4f}, fVal Acc {correct/total:.4f})逻辑说明CosineAnnealingLR让学习率按余弦曲线从 1e-3 降到接近 0比固定学习率收敛更稳尤其适合小数据集。每个 epoch 结束切model.eval()并关掉梯度验证完再切回train()忘了切回会导致 BN 统计量在训练时被污染。参数说明weight_decay1e-4是轻量正则缺陷分类数据少容易过拟合这个值能压一压。T_max50要和总 epoch 数一致设错了学习率曲线会提前躺平。如果验证准确率连续 10 个 epoch 不涨基本可以停了再训就是过拟合。4. 用 PyQt 把模型包成一个能演示的巡检分类工具4.1 界面布局与推理线程分离模型训完只是半成品要给人演示或者现场用得有个界面。PyQt 是这套方案里最顺手的选择但有个血泪经验推理千万别放在主线程里。主线程负责刷新界面推理一卡几百毫秒界面直接假死用户以为程序崩了。正确做法是把推理塞进QThread通过信号槽把结果传回界面。from PyQt5.QtCore import QThread, pyqtSignal import torch from torchvision import transforms from PIL import Image class InferenceThread(QThread): # 定义信号传出类别名和置信度 result_ready pyqtSignal(str, float) def __init__(self, model, img_path, class_names, device): super().__init__() self.model model self.img_path img_path self.class_names class_names self.device device self.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]) ]) def run(self): img Image.open(self.img_path).convert(RGB) tensor self.tf(img).unsqueeze(0).to(self.device) self.model.eval() with torch.no_grad(): output self.model(tensor) prob torch.softmax(output, dim1) conf, idx prob.max(dim1) self.result_ready.emit(self.class_names[idx.item()], conf.item())逻辑说明pyqtSignal定义了两个参数字符串传类别名浮点传置信度。run()里做完整的预处理加前向推理unsqueeze(0)是给单张图补上 batch 维度。softmax把 logits 转成概率max同时拿到最大概率和对应索引。参数说明convert(RGB)不能省巡检图有时候是灰度或者带 alpha 通道不转 RGB 送进网络会报通道数不匹配。self.device要和模型所在设备一致模型在 GPU 上而输入在 CPU 上直接抛错。4.2 主窗口与文件选择逻辑from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget, QFileDialog) import sys class MainWindow(QMainWindow): def __init__(self, model, class_names, device): super().__init__() self.model model self.class_names class_names self.device device self.setWindowTitle(电力巡检缺陷分类识别) self.resize(500, 400) self.btn QPushButton(选择巡检图像) self.label QLabel(等待选择图像...) self.btn.clicked.connect(self.select_image) layout QVBoxLayout() layout.addWidget(self.btn) layout.addWidget(self.label) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def select_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图像, , Images (*.png *.jpg *.jpeg)) if not path: return self.label.setText(推理中...) # 启动子线程避免阻塞界面 self.thread InferenceThread(self.model, path, self.class_names, self.device) self.thread.result_ready.connect(self.show_result) self.thread.start() def show_result(self, class_name, confidence): self.label.setText(f缺陷类别: {class_name}\n置信度: {confidence:.4f}) if __name__ __main__: app QApplication(sys.argv) # 加载训练好的权重 model ShuffleNet(num_classes4) model.load_state_dict(torch.load(best_shufflenet.pth, map_locationcpu)) class_names [damper_slip, insulator_broken, normal, pin_missing] window MainWindow(model, class_names, torch.device(cpu)) window.show() sys.exit(app.exec_())逻辑说明class_names的顺序必须和训练时ImageFolder的class_to_idx完全一致这里按字母序排。map_locationcpu是为了在没有 GPU 的机器上也能加载 GPU 训出来的权重不加会报设备不匹配。参数说明QFileDialog的过滤器写成*.png *.jpg *.jpeg覆盖巡检图常见格式。resize(500, 400)是演示窗口的舒适尺寸实际部署可以再调。5. 缺陷分类落地时最容易翻车的几个地方5.1 类别极度不平衡导致模型只会猜正常现象训练 loss 一路下降验证准确率看着有 90% 多但一测缺陷图全判成 normal。原因巡检图里正常样本占绝大多数缺陷样本可能只占 5%模型学到全猜正常就能拿高准确率。解决用加权交叉熵权重按类别频率的倒数设或者对缺陷类做过采样。我一般先算一遍各类样本数把weight传进CrossEntropyLoss。5.2 通道混洗的 groups 和卷积 groups 不一致现象训练直接报 reshape 维度错误或者不报错但精度死活上不去。原因channel_shuffle里的 groups 和nn.Conv2d里的 groups 设成了不同值混洗打乱的组数和卷积分组的组数对不上。解决把 groups 抽成一个变量在单元初始化和混洗调用处共用同一个值别两处各写各的。5.3 验证集做了数据增强现象验证准确率比测试时实际表现高出一截上线就掉点。原因验证集的 transform 里混进了RandomHorizontalFlip或RandomRotation每次评估看到的图都不一样指标虚高。解决验证和测试的 transform 只保留 resize、ToTensor、Normalize 三步任何随机操作都不能有。5.4 PyQt 推理卡在主线程现象点完选择图像按钮界面转圈假死几秒后才出结果连续点还会崩。原因推理直接在按钮回调里同步执行阻塞了 Qt 事件循环。解决所有推理走QThread通过信号槽回传结果主线程只负责更新界面。5.5 输入尺寸和预训练权重不匹配现象加载 ImageNet 预训练权重时报形状错误或者不报错但精度极差。原因ShuffleNet 预训练权重是按 224x224 训的如果 resize 成 112x112浅层特征分布对不上。解决要么保持 224要么改小输入时重新训几轮微调别指望直接套。6. 把 ShuffleNet 缺陷分类推到能用的程度几个进阶技巧模型能跑通和能交付之间还差几件事。第一件是置信度阈值。缺陷分类识别在巡检场景里宁可漏报也别误报太多因为误报会让运维人员白跑一趟。我一般会在 PyQt 界面里加一个阈值滑块置信度低于阈值就输出疑似缺陷建议人工复核而不是硬判一个类别。这个阈值在验证集上按 F1 或者召回率调别拍脑袋定。第二件是模型量化。ShuffleNet 本身已经很小但用torch.quantization做动态量化之后CPU 推理还能再快 2 到 3 倍模型体积压到原来的四分之一。对边缘盒子部署来说这一步收益很直接import torch.quantization # 动态量化只量化 Linear 和 Conv适合 CPU 推理 quantized_model torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) torch.save(quantized_model.state_dict(), shufflenet_quantized.pth)逻辑说明动态量化在推理时把权重转成 int8激活值保持浮点不需要校准数据集改完直接能用。参数说明dtypetorch.qint8是 8 位整型精度损失通常在 1 个点以内缺陷分类这种任务完全能接受。第三件是 Grad-CAM 可视化。巡检缺陷分类最怕的是模型学错了特征——比如把杆塔背景当成缺陷线索。用 Grad-CAM 把热力图叠回原图看一眼模型到底在看哪里比盯着准确率数字有用得多。如果热力图集中在绝缘子串上说明模型学对了如果散在天空或者植被上那准确率再高也是玄学换个数据集就崩。优化手段推理速度提升精度影响适用场景动态量化2-3 倍掉 0.5-1 个点CPU 边缘部署输入降到 160约 2 倍掉 1-2 个点算力极受限groups 调到 4约 1.3 倍掉 0.5 个点追求极致轻量剪枝 30%约 1.5 倍掉 1 个点有微调数据最后说个习惯每次改完网络结构或者训练参数我都会固定一个随机种子把验证集指标记在一个表格里改一次记一行。不记的话改到后面根本分不清哪个配置是哪个纯靠记忆就是自找翻车。这套 ShuffleNet 加 PyQt 的方案真正花时间的不是写网络是数据清洗和阈值调优网络结构本身反而是最确定的部分。希望帮到你。本文还有配套的精品资源点击获取
返回列表