简介:面向绝缘子表面污染物识别任务,这份已标注分类数据集可支撑深度学习图像分类模型训练与验证,适合电力巡检、计算机视觉方向的研究者和学习者使用。数据覆盖灰尘、脏污、正常等四种类别,约14400张图片,并预先划分好训练集与测试集,各类别图片分目录存放,json文件记录具体类别标签,便于直接开展分类实验。压缩包共2000个文件,以jpg图片为主体(1998个),另附1个python脚本和1个json标注文件;python脚本可对数据集进行可视化预览,json文件辅助解析标签与划分信息,整体包体约267.75MB。目前已有83人学习。借助博主的配套项目,还可与基于CNN的分类网络、图像分割以及yolov5改进等方案衔接,从单一分类任务扩展到检测、分割等更多应用场景,是一份兼顾基础实验与进阶扩展的数据资源。
1. 绝缘子污染物模拟图像分类数据集:我建议你先把它当测试台
绝缘子污染物模拟图像分类数据集,我建议你把它当作迁移学习和模型验证的“测试台”来用:约 14400 张已标注图像,灰尘、脏污、正常等四分类,训练集和测试集已经划分好,还附带 show 脚本。做输电线路巡检和电力设备视觉检测的人,总会遇到分类模型在公开数据集上达标、到自己绝缘子图片上就露馅的尴尬,这个数据集正好提供一个能快速复现的对照场景。但别急着解压跑训练,先确认四件事:json 里类别顺序、目录结构、每类样本量、图片尺寸。下面按我的习惯一步步拆开。
2. 四分类标注与目录结构:先把数据读透再谈训练
2.1 json 类别映射:先看标签再定损失函数
很多下载下来的分类数据集都带一个 annotations.json 或 label_map.json,这个数据集也不例外。json 里通常记录的是类别 id 和类别名的映射,以及可能每张图的标注信息。我的习惯是解压后第一件事就是把它读出来,而不是直接开跑训练脚本。
import json from collections import Counter with open("annotations.json", "r", encoding="utf-8") as f: ann = json.load(f) # 常见结构一:class_names + annotations 列表 class_names = ann.get("class_names", []) print("类别列表:", class_names) # 常见结构二:图片路径列表 + 类别id if "annotations" in ann: img_labels = [(item["image"], item["class_id"]) for item in ann["annotations"]] print("标注条数:", len(img_labels)) counter = Counter([label for _, label in img_labels]) print("每个类别的样本数:", counter)这段代码把最关键的类别顺序打印出来。为什么先看这个?因为后面用 ImageFolder 或者自己写 Dataset 时,类别 id 默认是按文件夹名称排序的,如果 json 里的 id 顺序和文件夹排序不一致,训练时标签就可能错位。比如 json 里“0=normal,1=dust,2=dirty,3=other”,而磁盘上文件夹排序是“dirty,dust,normal,other”,那么错误率会在 50% 以上,而且模型自己很难察觉。
从文件名前缀 isolador_vidro_semTextura_512x 也能看出,这批图是同一批玻璃绝缘子在不同污染模拟条件下的截图。因为文件名里带着 semTextura,说明图片是去掉表面纹理的简化模拟图,这对训练来说是一个重要信息——模型学到的可能是颜色和灰度分布,而不是纹理细节。后面做增强和迁移学习时需要考虑到这一点。
2.2 训练测试目录:确认 ImageFolder 能不能直接用
再看目录结构。摘要里说“划分了训练集、测试集,存放各自的同一类数据图片”,通常做法是:
dataset/ ├── train/ │ ├── normal/ │ ├── dust/ │ ├── dirty/ │ └── other/ └── test/ ├── normal/ ├── dust/ ├── dirty/ └── other/用下面的脚本验证实际落盘结构和样本量:
import os for split in ["train", "test"]: base = f"./dataset/{split}" subdirs = [d for d in os.listdir(base) if os.path.isdir(os.path.join(base, d))] print(split, "类别:", subdirs) total = 0 for cls in subdirs: files = os.listdir(os.path.join(base, cls)) print(" ", cls, len(files)) total += len(files) print(" total:", total)这一步可以顺带确认两个事:一是 train/test 里类别是否一致,二是每类的样本量是否均衡。如果发现 train 里有 normal 而 test 里没有,或者 test 里多了一个类,那你训练出来的模型在评估时一定会崩。我遇到过好几次类似的数据集——作者重新整理了目录,但忘了同步两边的类别集合,最后靠这个脚本才定位到问题。
2.3 show 脚本可视化:人眼先过一遍
show 脚本是这份资源里比较贴心的部分。它的作用就是按类别把图片铺成网格,让你一眼看到四类长什么样。常见用法是先看 help:
python show.py --help如果脚本写得规范,会看到类似下面这样的一组参数:
python show.py --data_dir ./dataset --mode grid --sample_per_class 8 --save_dir ./preview参数说明:--data_dir指向数据集根目录,--mode控制展示模式,grid表示按类别网格展示,--sample_per_class控制每个类别抽取几张,--save_dir把拼接好的预览图保存到指定目录。有些版本还会支持--show_label,把 json 里的类别名叠在图片上。
跑完会发现,normal 类就是干净的玻璃绝缘子,dirty 类表面有明显黑色或深色的积污条带,dust 类像盖了一层薄薄的灰。如果你发现某个类里混了明显不对的图,趁早自己决定是删掉还是手工纠正,否则后面训练出来的模型会把噪声当作特征。我一般会把预览图存下来,贴在实验记录里,之后每次分析 test set 错误样本时都回来对照。
3. 用 CNN 跑通绝缘子四分类:从 DataLoader 到训练闭环
3.1 transforms 与输入尺寸:512x512 怎么处理
这批原始图片来源是 512x512 左右的图。CNN 分类任务里,直接喂 512x512 也不是不行,但显存占用大、收敛慢。常见做法是先缩放到 256x256,再随机裁剪到 224x224,这样等于顺手做了一点数据增强。也可以用 Resize 到 224。如果你打算后面接 YOLOv8 做检测,那么保持大分辨率反而有价值,这里先讨论分类。
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) test_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])ColorJitter 的幅度要克制。这个数据集本质是模拟污染物,本身灰度差异和颜色分布是关键特征。你把对比度拉得太多,可能会把 normal 的图增强得像 dirty,模型会特别困惑。而且原始图已经是 semTextura——无纹理的模拟图,再叠加高斯模糊这类强增强会丢失仅剩的可分信息。所以我的建议是:轻量增强可以,重增强慎重。
3.2 模型选型:ResNet18 起步,Transformer 先等等
14400 张图在分类任务里算“小中型”数据集,预训练 ResNet18 是一个性价比很高的起点。它只有 11M 参数,单卡 V100 或 3060 上,batch 32 跑一个 epoch 大约几十秒到一两分钟。相比 ResNet50,ResNet50 精度可能高 1-2 个点,但训练时间翻倍。我一般先拿 ResNet18 定基线和排错,确认数据链路没问题后,再升级到 ResNet50 或 EfficientNet。
import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 4)如果要用 Transformer,比如 ViT-B/16,14400 张肯定能训,但需要更精细的增强、更长的 warmup 和 weight decay,收益不一定比 CNN 大。只有当你要和检测网络共享 backbone,或者你怀疑卷积的感受野不足以捕捉大面积积污条带时,再试 ViT。起步阶段别跟风,先把 ResNet18 这版跑通。
3.3 训练参数与主循环:batch、lr、loss 怎么配
建议训练参数:batch_size=32(显存不够就 16),lr=1e-4用 AdamW,weight_decay=0.01,训练 30 个 epoch,每隔 5 个 epoch 在 test 上评估一次,保存最优准确率的权重。这个数据集类别数少,用标准交叉熵即可。如果发现 dirty 和 dust 特别容易混淆,可以给 loss 加类别权重,但首先要排除边界问题——也就是图片本身在两者之间确实没有明显边界。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) train_ds = ImageFolder("./dataset/train", transform=train_transform) test_ds = ImageFolder("./dataset/test", transform=test_transform) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) best_acc = 0.0 for epoch in range(30): model.train() total_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() total_loss += loss.item() * images.size(0) scheduler.step() print(f"epoch {epoch+1}, loss: {total_loss/len(train_ds):.4f}")这段主循环省略了验证逻辑,但核心结构没问题。值得提醒的是,ImageFolder要求目录严格按类别排列,如果 json 里类别顺序和目录不一致,会出现上面提到的错位。还有一点,num_workers在 Windows 下建议设为 0,否则会报BrokenPipeError;Linux 下可以开 4 或 8,加快 IO。
4. 可视化与结果验证:不要只看准确率
4.1 show 脚本的扩展用法:把预测结果叠回预览图
show 脚本不只可以用来展示原始数据。很多版本允许传入一个预测结果文件,它会在原图上叠出预测类别和置信度。用法类似:
python show.py --data_dir ./dataset/test --csv predict_results.csv --mode overlay --sample_per_class 5这样你能最直观地看到哪些图被分错。特别是绝缘子模拟图里,dirty 和 dust 的边界非常主观,人眼看都不一定能分清。把预测叠在原图上,比翻一堆数字有效得多。
4.2 混淆矩阵:错误都集中在哪里
训练结束后,在 test 集上做一次完整推理,生成混淆矩阵和分类报告。这一步别省。准确率 90% 告诉你“还行”,混淆矩阵告诉你“哪里不行”。
from sklearn.metrics import confusion_matrix, classification_report import torch model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) preds = model(images).argmax(dim=1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) print(cm) print(classification_report(all_labels, all_preds, target_names=train_ds.classes))如果看到 normal 被错分成 dust,大概率是增强过重,或者积灰轻微的 normal 本来就和 dust 长得像。如果看到 dirty 被误分成 normal,就要怀疑是光照问题——模拟图里有些脏污颜色很浅,人工标注本身也存在主观性。这些结论需要回到 show 脚本的预览图里人工确认,不能直接调 loss 权重掩盖数据本身的模糊性。
4.3 Grad-CAM:模型到底在看哪里
绝缘子污染识别的关键是模型要看绝缘子表面,而不是背景的天空、导线或者其他杂物。用 Grad-CAM 能快速验证这一点。PyTorch 里可以用 torchcam 这个库,代码参考如下:
from torchcam.methods import GradCAM cam_extractor = GradCAM(model, target_layer="layer4") model.eval() with torch.no_grad(): out = model(img_tensor.unsqueeze(0)) act = cam_extractor(0, out) cam = act[0].squeeze(0).cpu().numpy()注意,target_layer需要根据实际模型名称调整,ResNet18 是layer4,如果换成 EfficientNet 或 ViT,层名完全不同。对 test 集抽几张图跑一下,把热力图叠加到原图上。如果热力点集中在绝缘子伞裙边缘,说明模型学到了结构特征;如果大面积覆盖背景,那你就要注意了——模型可能学到了“环境亮度”这类捷径。对于模拟图,背景相对干净,这个问题不严重,但一旦将来换到真实巡检图片,模型鲁棒性会明显下降。
5. 避坑指南:14400 张标注数据里最常翻车的五个位置
5.1 json 类别顺序与文件夹排序不一致
现象:训练时 loss 正常下降,但 test 准确率只有 20%~40%,重复几次都一样。原因:json 里的类别 id 和 ImageFolder 按名称排序后的类别顺序对不上。比如 json 里 0=dust、1=dirty、2=normal、3=other,而文件夹排序是 dirty、dust、normal、other,于是所有标签都偏移了一位。解决:训练前同时打印 json 里的 class_names 和ImageFolder自动生成的 class_to_idx,人工对齐后再喂给模型。如果你自己写 Dataset,干脆忽略 json 里的 id,统一用文件夹名作为标签来源,把 json 只作为类别注释。
5.2 train 和 test 里类别集合不一致
现象:训练正常,一跑 test 就报 KeyError 或准确率异常,还可能 classes 列表长度不同。原因:整理数据时,test 里漏拷了一个类文件夹,或者多了一个只有几张图的类。解决:用os.listdir对比两个 split 的类别集合。多出来的类删掉或移入对应类,缺失的类从 train 里按比例抽一部分补进去。这一步要趁早做,否则后面做迁移学习或生成 report 时全是坑。
5.3 文件名前缀一样,就默认为同类
现象:展示预览图时,每个类别文件夹里都看到同样的isolador_vidro_semTextura_512x_*.jpg前缀,于是怀疑数据集是不是没分对。原因:文件名是同一批原始图生成的编号,和类别无关。这个前缀只是在生成模拟图时保留的原始命名,不携带标签信息。解决:不要靠文件名判断类别,一切以文件夹和 json 为准。如果你有强迫症,可以写脚本把训练集按类别统一改名,但这不是必要的。
5.4 迁移学习时分类头改错
现象:把 ResNet18 的 fc 层改成 4 后训练,报维度不匹配的错,或者拿 ResNet50 的代码改到 ResNet18 上报错。原因:不同模型的分类头不一样,ResNet 是fc,EfficientNet 是classifier,ViT 是heads.head。总有人直接套模板。解决:打印 model 最后一层再改。代码写成num_ftrs = model.fc.in_features是通用做法,但换模型就必须同步换属性名。我的习惯是封装一个小函数,传入模型和类别数,内部判断分类头类型,避免每次都去翻模型结构。
5.5 ColorJitter 把类别边界调没了
现象:加了 ColorJitter 后,验证集准确率反而比不加低好几个点,尤其 dirty 类掉得厉害。原因:这个数据集靠灰度/颜色差异区分污染物。亮度对比度拉过头,会把轻度积污的图“洗”成 normal,或把 normal 增强成 dirty,造成了矛盾的标签。解决:把 brightness=0.1、contrast=0.1,或者干脆只保留水平翻转和随机裁剪。对语义极其依赖低层颜色统计特征的场景,增强策略要保守,不要套用 ImageNet 上那套强增强。
6. 进阶:把四分类数据集用足,再往前推一步
6.1 把分类输出变成检测任务的“半自动标注”
如果你下一步想做绝缘子目标检测,这个分类数据集不能直接用于 YOLO,但它可以帮你减少标注工作量。先用训练好的分类模型在真实巡检图上做滑窗推理,滑窗尺寸可以用 256x256、步长 64,把高置信度的绝缘子区域裁剪出来保存为伪标签。之后用标注工具人工修正边界框时,你只需要改框,不需要重新选类别。这套流程我实践下来,能把一张图的标注时间从 5 分钟压到 2 分钟,而且类别名称可以直接沿用这里的四分类。
6.2 用 ViT / Swin 做一次对比实验
等 ResNet18 基线稳定后,值得花半天时间试一次 ViT-B/16 或 Swin-T。针对大块积污区域,Transformer 的全局感受野有时会带来惊喜。微调时用 lr=2e-5 到 5e-5、batch_size=32、warmup_ratio=0.1、weight_decay=0.05,训练 15-20 个 epoch 就够。对比时要固定相同的 train/test 划分,并记录每次实验的混淆矩阵,不要只比较 Accuracy。调这种模型时不要随手改增强,变换了数据分布,模型对比就没意义了。
6.3 每次拿到数据集的三件套习惯
从那以后,我每次拿到这类带标注的视觉数据集,都强制自己走一遍三件套:先打印 json 和目录结构并存日志,再跑 10 个 epoch 的 ResNet18 基线,最后用 Grad-CAM 检查 3 张正确、3 张错误样本。这三个动作加起来不到一小时,却能规避绝大多数“数据没搞清就训模型”的翻车。尤其这个绝缘子数据集,看着注释很齐全,但类别顺序、目录差异和增强过度这三个坑,哪一个都能让你白跑一个下午。希望帮到你。
本文还有配套的精品资源,点击获取