
简介一份面向电力设备巡检与图像分类任务的绝缘子污染物模拟图像分类数据集。资源已标注约14400张图片划分灰尘、脏污、正常等4个类别具体标签对应关系见随附json文件并预先划分好训练集与测试集图片按类别存放于不同目录方便直接用于CNN分类模型训练、迁移学习或算法效果对比。压缩包共2000个文件以1998张jpg样本为主体另含1个Python可视化脚本可一键展示数据集样例并核对标注和1个json类别说明文件整体大小约267.75MB便于下载与快速启动实验。目前已有83人学习/浏览。除数据集本身外还可结合作者同系列公开的CNN分类网络项目、图像分割项目及基于YOLOv5的改进方案进行扩展研究适合电力视觉方向初学者与研究者作为实验基准数据。1. 绝缘子污染物模拟图像分类数据集14400张标注图能把模型训练到什么程度做输电线路巡检的人都知道绝缘子污秽闪络是停电事故的主要诱因之一。识别绝缘子表面有没有积污、积污到哪个程度靠人眼巡视频率太低靠红外测温又受天气影响大所以视觉分类成了最现实的方案。问题是真实绝缘子污秽图像太难得——无人机巡回来几千张真正带污秽标注的可能不到一百张训练一个像样的图像分类模型根本不够。这时候一套已标注、约14400张的绝缘子污染物模拟图像分类数据集就派上了用场它把样本量从“百张级”抬到“万张级”让深度学习模型在分类任务上第一次有了真正可用的训练规模。这篇笔记就围绕这套数据展开讲清楚它是什么、怎么用、参数怎么设、坑在哪以及最后怎么验证模型是真的学会了“污秽”这个概念而不是背下了图片背景。2. 读数据集与标签映射先弄清楚14400张图的真实分布拿到任何数据集第一件事不是训练而是把目录结构和标签文件读明白。这个数据集标注为“图像分类”意味着每张图对应一个类别标签而不是目标检测里的边界框。常见的组织方式有两种一种是按类别分文件夹存放训练时直接用torchvision.datasets.ImageFolder读取另一种是给出CSV或JSON映射文件由标注文件名对应标签。这两种方式我都处理过先讲怎么快速摸清底细再讲标签分布对训练的影响。2.1 标签分布检查先看有没有类别失衡约14400张数据说多不多说少不少但如果类别分布极其不均比如“正常”占12000张、“严重污秽”只有800张那么模型很容易学成一个“永远猜正常”的懒模型。拿到数据后我一般先用一段脚本统计每个类别的样本数顺便检查图片尺寸和格式是否统一。import os from collections import Counter from PIL import Image dataset_root insulator_dataset # 换成实际路径 folder_names [d for d in os.listdir(dataset_root) if os.path.isdir(os.path.join(dataset_root, d))] print(类别文件夹:, folder_names) counts Counter() for folder in folder_names: folder_path os.path.join(dataset_root, folder) img_files [f for f in os.listdir(folder_path) if f.lower().endswith((.jpg, .jpeg, .png))] counts[folder] len(img_files) # 抽查第一张图的尺寸和模式 if img_files: sample_path os.path.join(folder_path, img_files[0]) with Image.open(sample_path) as im: print(f{folder} 首图尺寸: {im.size}, 模式: {im.mode}) print(分布:, counts)逻辑上分三步列出子目录名、统计每个目录里的图片数量、抽查第一张图确认尺寸和色彩模式。这里最需要注意的是“图片格式”和“尺寸是否统一”。模拟图像如果是由程序批量生成的尺寸大概率统一但如果是实验室拍摄后整理的可能出现尺寸不一、带EXIF旋转信息的情况后续训练时Resize和Normalize都要统一处理。参数说明Image.open只是打开句柄不会把整图载入内存所以即使14400张图全扫一遍内存压力也不大。统计脚本的复杂度是O(N)一次性跑完即可。如果发现某个类别占比低于总数的10%后面做数据增强时要针对该类别多给几个增强策略或者采用加权采样而不是盲目按原始分布训练。2.2 模拟图像与真实图像的域差异为什么不能指望直接落地标题里“模拟图像”四个字是整篇笔记的核心。模拟图可能来自多种途径实验室在绝缘子上涂抹不同灰密/盐密后拍摄、用三维渲染引擎生成不同积污程度的模型图、或者在干净绝缘子图上做程序化纹理叠加。无论哪种它与无人机在野外拍到的真实照片之间存在“域差异”也就是我们常说的域偏移。模型在模拟图上跑出99%的准确率不意味着到了现场还能有这个数。我见过一个项目用模拟图训练的分类模型在测试集上准确率97%拿到现场实测直接掉到71%排查下来发现模拟图背景是均匀的实验室墙壁或纯色背景真实图片背景是天空、导线、塔材、草地模型实际是学了一堆背景特征。所以从第一天起就应该把模拟图当作“预训练素材”而非“最终素材”训练时用更强的数据增强去弱化背景影响并预留一小部分真实图像做最终验证。2.3 数据划分策略验证集要从“同一个来源”里抽14400张数据怎么划分常见做法是 7:2:1 或 8:1:1。但有一个细节容易翻车如果模拟图是按批次生成的同一批次的图可能共享了相同的纹理参数、相同的背景直接随机划分会让验证集和训练集“长得太像”导致验证指标虚高。更稳的划分方式是先按生成批次或拍摄条件分组再按组划分比如把批次A到M放进训练集批次N和O放进验证集批次P放进测试集。这样验证集才真正代表“没见过的数据”。如果数据集没有提供批次信息只能随机划分那至少要在训练日志里记录训练集和验证集的损失曲线差距训练损失和验证损失差距过大说明模型过拟合了模拟数据的特定分布就得靠增强和正则化来压。3. 用PyTorch跑通绝缘子污秽分类最小可复现的训练流程这部分直接给可复现的代码。我默认训练环境是单张NVIDIA GPUPyTorch 2.x。不要一上来就搬ResNet-152或ViT-Large14400张图训练一个分类模型ResNet-18或MobileNetV3这个量级的网络足够了训练速度快迭代调参也方便。3.1 加载图像与标签直接吃文件夹结构如果数据集是“按类别分文件夹”的结构用ImageFolder是最省事的路径。它会自动把文件夹名映射成整数标签并保留class_to_idx映射关系。以下是加载代码from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), 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]) ]) train_dataset datasets.ImageFolder(insulator_dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(insulator_dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别映射:, train_dataset.class_to_idx)代码逻辑很直白训练集和验证集各用一套transform差别在于训练集多了随机水平翻转和色彩抖动验证集不做随机增强。ImageFolder要求根目录下面按类别分子目录如果数据集的标注是CSV文件就需要自己写Dataset类按文件名去查标签这里先按最常见的文件夹结构来。关键参数要展开说Resize((224, 224))把所有图统一成224分辨率这是ResNet-18的标准输入。ColorJitter(brightness0.2, contrast0.2)是对模拟图像特别重要的增强项因为模拟图的光照往往很单一不加这个模型会把统一光照当成判断依据。batch_size32在单卡上兼顾显存和训练速度如果显存小于8G降到16。num_workers4是数据加载的并行进程数Windows系统上建议改成0否则偶尔会报错。3.2 定义模型与训练循环把训练过程抓在手里分类模型用ResNet-18关键改动是最后一层全连接层要把输出节点数改成实际类别数。训练循环里需要同时监控训练损失和验证准确率并且保存验证准确率最高的权重这就是我们的“后悔药”防止训练到后期模型过拟合。import torch import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes len(train_dataset.classes) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) 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_acc 0.0 epochs 30 for epoch in range(epochs): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) val_correct (preds labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total avg_train_loss train_loss / len(train_dataset) print(fEpoch {epoch1}/{epochs} | Train Loss {avg_train_loss:.4f} | Val Acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_insulator_model.pth) print(f Saved best model, val_acc{best_acc:.4f}) scheduler.step()这段代码里的几个参数值得逐一说清楚。weightsmodels.ResNet18_Weights.IMAGENET1K_V1表示加载ImageNet预训练权重这是14400张数据能训练出可用模型的关键不加载预训练权重从头训练小模型在这个数据量上很难收敛。学习率lr1e-4是微调阶段的常见起点如果加载预训练权重后直接用默认的1e-3很容易在头两轮就把预训练特征破坏掉。weight_decay1e-4给优化器加了L2正则对抗过拟合。CosineAnnealingLR让学习率在30个epoch内从1e-4按余弦曲线衰减到接近0后期自动变小步长做精细收敛。这里有一个训练策略上的选择浅层要不要冻结。常见做法有两种一是冻结所有卷积层只训练最后的全连接层跑5个epoch再解冻全部层二是像上面这样直接全量微调。14400张图像用第二种做法更合适因为数据量不算小全量微调能学到绝缘子特有的纹理细节而冻结浅层只适合几百张图的小样本场景。唯一的代价是需要更小的学习率这也是上面把lr压到1e-4的原因。4. 数据增强与训练参数调优让14400张模拟图练出能上线的模型模拟图数据量有14400张但视觉特征相对单一。这时候决定模型上限的就不是网络结构而是数据增强和训练参数。很多人在数据集上训练翻车问题不在模型在于增强做得太弱或太强。4.1 增强策略选择模拟图最需要的是“破坏背景一致性”模拟图的软肋是背景干净、光照统一、拍摄角度固定。如果增强只做水平翻转模型很快会注意到背景的特征而不是绝缘子本身的污秽纹理。正确思路是通过增强“打乱”背景和光照信息逼模型去学绝缘子本体。我常用的增强组合是随机旋转(±15度)、随机平移(±10%)、随机缩放(±10%)、随机亮度对比度扰动、随机高斯模糊、以及RandomErasing。其中RandomErasing对模拟图特别实用它随机遮挡图像的一小片区域让模型不能依赖任何一个局部背景特征这在域偏移场景下相当于一个弱正则化器。from torchvision import transforms import random class ErasingWithProb: def __init__(self, p0.5, scale(0.02, 0.15)): self.p p self.scale scale def __call__(self, img): if random.random() self.p: return transforms.RandomErasing(p1.0, scaleself.scale)(img) return img train_transform_strong transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop((224, 224), scale(0.7, 1.0)), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ErasingWithProb(p0.3, scale(0.02, 0.1)) ])这套增强比上一版多了RandomResizedCrop和RandomRotation。这里有个参数细节RandomResizedCrop的scale(0.7, 1.0)表示裁剪区域占原图面积的70%到100%比例范围控制得比较保守。如果你把scale下限降到0.3模型会看到很多局部特写虽然能增强鲁棒性但会让训练难度显著上升14400张图的数据量下0.7的下限是安全起点后续可以逐步降到0.5观察验证集变化。ErasingWithProb是一个包装类保证每次增强以30%概率执行擦除而不是每张图都擦。擦除面积控制在2%到10%之间太大会把绝缘子本体关键纹理抹掉模型直接学歪了。4.2 学习率与batch size调参顺序先固定结构再动超参很多新手一上来就同时调学习率、batch size、权重衰减结果模型反复横跳根本不知道是哪个参数起的作用。我的调参顺序是先固定batch size为32固定epoch为30只扫学习率从1e-4开始跑观察前5个epoch训练损失是否稳定下降如果损失震荡降到3e-5如果下降太慢升到3e-4。batch size的影响在于32时梯度噪声适中泛化通常好于64或128但如果你只有一张8G显存的卡batch size 32配合ResNet-18也会接近显存上限可以开启gradient_checkpointing或直接降到16。注意降低batch size时学习率也要按比例微调一般降一半batch size就把学习率降到原来的70%左右。权重衰减weight_decay我习惯固定在1e-4不跟其它参数同时调。它只在过拟合信号明显训练损失低、验证损失高时加大到5e-4否则不要动。4.3 早停与模型选择不要只盯“准确率”训练30个epoch验证准确率最高的那个模型不一定是最适合上线的。因为模拟图数据分布相对简单验证准确率可能在第10个epoch就到97%了但此时模型对真实场景的适应能力未必最好。我一般结合两个信号做判断一是训练损失和验证损失之间的差距二是验证集在“困难样本”上的表现差距。更实际的做法是保存每一轮的模型最后统一在预留的“测试集”上评估而不是在训练过程中就根据验证集反复挑选。测试集应该包含以下几种情况不同光照的图像、不同背景的图像、以及轻度到重度污秽的等级样本。这14400张模拟图如果覆盖了这些场景测试集就要按场景分层抽样保证每个场景都有。5. 绝缘子污染物分类避坑指南模拟图像落地时的五个血泪教训这个章节写的是我在类似项目里反复踩过的坑。每一条都按“现象→原因→解决”来写希望你能绕开。5.1 验证集准确率高测试现场准确率掉20个百分点现象模拟图验证集上准确率97%换成真实巡检图像直接掉到75%左右。原因模拟图像的背景、光照、拍摄角度都太规整了模型实际把大量权重分配给了背景特征。验证集也来自模拟图所以验证时发现不了这个薄弱点。解决训练时用4.1里的强增强策略特别是RandomResizedCrop和RandomErasing打散背景的稳定性。另外在训练集里混入少量真实绝缘子图像哪怕只有几百张也能明显拉近特征分布。如果真实图完全拿不到那就必须在部署时对输入图像做预处理用语义分割或目标检测先把绝缘子区域裁剪出来把分类器的输入限定在绝缘子本身。5.2 训练准确率上不去一直卡在85%左右现象模型训练到第10轮以后训练准确率和验证准确率双双停滞损失也不再下降。原因大概率是标签有噪声或者类别定义本身模糊。污染物严重程度的判定本来就带有主观性标注人员在“轻度”和“中度”之间可能产生分歧14400张图如果标注标准不统一模型学到的是“标注者的偏好”而不是“污染程度的本质”。解决先做一次标签清洗。把预测概率在0.4到0.6之间的样本单独抽出来人工复查一次如果类别边界确实模糊就修改损失函数用标签平滑label smoothing把硬标签改成软标签。比如smoothing0.1时正确类别的目标概率从1.0降为0.9其余0.1分散到所有类别这个改动能让模型对噪声标签的容忍度明显提高。5.3 数据增强加太猛反而把验证准确率压低了现象加了RandomRotation(degrees45)和RandomResizedCrop(scale(0.3, 1.0))之后训练损失一直比原来高验证准确率也下降了3个百分点。原因模拟图里的绝缘子大多是正立、居中、占画面比例较大的增强过猛生成了大量“现实中不会出现”的训练样本比如斜着超过45度的绝缘子、被裁剪到只剩一个边的绝缘子。模型被迫去拟合这些无效分布反而削弱了正常特征的表达。解决把旋转角度收回到±10度把RandomResizedCrop的scale下限从0.3调回0.6。记住了增强策略的目标是模拟真实场景的差异不是把训练集变成一个变形图片展览馆。5.4 显存溢出训练在第4轮就中断了现象batch_size32、ResNet-18、输入224分辨率按理说8G显存够用但实际训练到第4轮就报CUDA out of memory。原因大概率是num_workers开太多导致内存拷贝阻塞或者训练过程中开了太多中间变量没有释放。还有一种常见情况是验证阶段也保持了梯度计算。解决确认验证循环里加了torch.no_grad()。把num_workers从4降到2并设置pin_memoryFalse。如果仍然溢出把batch_size降到16训练速度损失不大但显存压力小很多。还有一个技巧是开启混合精度训练from torch.cuda.amp import autocast, GradScaler在forward和loss计算阶段用autocast()包裹能把显存占用降低约40%。5.5 训练好模型后无法复现结果现象同一份代码、同一份数据换一台机器重新训练验证准确率差了2个百分点以上。原因没有固定随机种子。模型初始化、数据增强的顺序、DataLoader的shuffle顺序都受随机性影响数据集大于一万张时这些随机扰动叠加在一起最终指标浮动几个点很正常。解决在训练脚本开头固定随机种子。PyTorch里至少需要设置三段random.seed(42)、numpy.random.seed(42)、torch.manual_seed(42)。如果使用GPU还要加上torch.cuda.manual_seed_all(42)。这虽然不能让结果完全一致但能把可复现误差压到0.2个百分点以内。6. 验证模型有没有“真学会”混淆矩阵、CAM热力图与测试集切分技巧训练完模型最后一步是验证它是不是真的在“看绝缘子”而不是“看运气”。准确率只是一个数字还不能告诉你模型在哪些类别上容易混淆。对绝缘子污染物分类来说最关心的是“轻度污染”和“中度污染”之间的边界以及“重度污染”和“正常”之间有没有误判。一条非常实用的验证思路是画出混淆矩阵。用测试集跑一遍模型统计真实标签和预测标签的交叉关系import numpy as np from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namestrain_dataset.classes)) plt.figure(figsize(8, 6)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xlabel(Predicted) plt.ylabel(True) plt.xticks(range(num_classes), train_dataset.classes, rotation45) plt.yticks(range(num_classes), train_dataset.classes) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)这段代码的作用是把验证集上的错误模式可视化。如果矩阵显示“轻度污秽”有大量样本被预测成“正常”说明模型对早期污秽不敏感需要补充轻度样本或者在损失函数里提高该类别权重。如果“重度污秽”频繁被预测成“中度”说明模型的区分力还停留在粗粒度上。比混淆矩阵更进一步的是CAM热力图它直接回答“模型看了图像的哪个区域”。对于绝缘子污秽分类热力图应该集中在绝缘子串的伞裙表面而不是背景或导线。如果热力图散落在整张图甚至背景上那前面的训练策略就有问题。ResNet-18可以用Hook机制提取最后一层卷积层的梯度生成CAM并叠加在原图上这个技巧在日常项目中价值极高能直观判断模型是否学到了绝缘子的纹理细节。最后的收尾习惯我一般会做三件事把最终模型导出为TorchScript或ONNX方便部署时脱离PyTorch环境在测试日志里记录每一类的精确率和召回率而不是只记总体准确率把训练时的随机种子、数据增强配置、学习率策略完整记录在项目README里。这套验证方法和记录习惯帮我避开了好几次“模型上线后才暴露问题”的尴尬也希望能帮到你。本文还有配套的精品资源点击获取