光伏板积灰四分类实战:从数据增强到监督对比损失

发布时间:2026/9/14 17:40:00
光伏板积灰四分类实战:从数据增强到监督对比损失 简介一套基于深度学习的太阳能光伏板积灰识别项目源码与自制数据集主要面向计算机相关专业正在做毕业设计的学生以及需要项目实战练习的进阶学习者也可作为课程设计或期末大作业的参考。项目聚焦光伏板灰尘识别这一图像四分类问题采用自制灰尘数据集整合了普通数据增广、AutoAugment数据增强、ResNet模型、监督对比学习损失及多种常用深度学习算法能够帮助读者系统完成从数据准备、模型训练到效果评估的完整流程其中AutoAugment可自动搜索较优增强策略监督对比学习损失则有助于提升特征判别力适合做算法对比与消融实验。该项目经导师指导并认可通过评审分98分整体代码组织与文档说明较为规范便于二次开发。资源包共241个文件以208张jpg图像数据、12个Python源码文件、5个zip与2个7z压缩包、2个md说明文档为主另含DensNet121-torch预训练模型与相关配置文件压缩包大小约119.03MB目录结构清晰便于按数据集、模型与说明文档分模块查阅。目前已有199人学习下载适合需要一套可直接运行参考的完整深度学习项目、希望理解灰尘识别任务与对比学习思路的读者。1. 太阳能光伏板积灰识别项目拆解四分类比二分类更接近运维现场光伏板表面积灰不是“有”或“没有”的零一问题灰尘覆盖面积不同发电损失和清洗策略差别很大二分类会让运维人员没法决定什么时候清洗。这个项目直接做四分类正常、轻度积灰、中度积灰、重度积灰用自制数据集训练 ResNet/DenseNet 分类网络再配合普通数据增广、AutoAugment 和监督对比学习损失。源码基于 PyTorch带图像数据集和四分类标注适合毕设、课程设计和想完整走一遍图像分类流程的初学者。对有经验的人这里更值钱的是数据增强、骨干选型和损失改造的完整链路换到玻璃、陶瓷表面缺陷任务也能复用。2. 灰尘四分类任务的数据标注、增广与验证集切分在分类任务里标签质量决定模型上限。太阳能光伏板表面灰尘与阴影、水渍、鸟粪的边界很容易混如果只凭肉眼把“看起来有点脏”标成同一类模型后期会一直在类别边界附近震荡。我一般会把类别先定义成可执行的等级无灰代表灰尘覆盖面积很低、电池片纹理清楚轻度积灰时仍能看到完整栅线中度积灰后部分电池片纹理被遮挡重度积灰则基本看不到电池片走向。这个标准比直接写“脏、很脏、特别脏”更容易保持标注一致性也方便后续按类别制定清洗策略。2.1 类别标签如何落到图像上四分类的类别数不能拍脑袋定。光伏板在倾斜安装、雨天自清洁后会自然分布在中等覆盖度灰尘层的透光率又受颗粒成分影响所以类别边界要配合采样统计。实际操作时我会先统计每张图的人工标注覆盖面积画一张直方图然后用分位数把样本切到四个桶里而不是手工逐个打标。这样做的优势是类内差异可控类别不平衡现象能从源头缓解。类别灰尘覆盖面积运维动作模型输出0无灰/基本清洁不触发清洗normal1轻度积灰纳入例行巡检light2中度积灰建议一周内清洗medium3重度积灰立即清洗并检查板面heavy这里的运维动作不是模型自动下发的而是给现场人员看的建议。模型输出到heavy时如果连续多个巡检周期都触发往往意味着光伏板所在区域存在固定扬尘源比如靠近土路或施工场地需要从环境侧考虑抑尘。2.2 普通数据增广与 AutoAugment 的取舍普通数据增广要解决的主要是光伏板图像的拍摄角度、光照和尺度变化。常见做法是训练时做 RandomResizedCrop、RandomHorizontalFlip、ColorJitter、Rotate验证集只做 Resize 和 Normalize绝对不把增强用到验证上。import torchvision.transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.6, 1.0)), T.RandomHorizontalFlip(p0.5), T.RandomRotation(10), T.ColorJitter(brightness0.3, contrast0.3), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale控制裁剪面积比例0.6是为了模拟无人机巡检时远近不同的视角ColorJitter的brightness和contrast不要同时调太大否则灰尘和阴影的反差会被抹平。这里用 ImageNet 的 mean/std 做归一化因为后面会加载 ImageNet 预训练权重保持相同的输入分布能提高迁移效果。AutoAugment 是策略化的数据增强PyTorch 里封装成torchvision.transforms.AutoAugment。它从标注数据集上搜索出一组子策略再随机挑选作用于每张图。光伏灰尘数据比较特殊AutoAugment 的原始策略来自 ImageNet其中包含大量对自然图像有效的几何与颜色变换但用到光伏板这种重复性纹理场景时过强的平移、剪切可能把电池片栅线搞乱模型就容易学到“变形后的假特征”。维度普通数据增广AutoAugment策略来源人工设计搜索/预训练策略计算开销低略高但多数在 CPU 上处理对灰尘任务可控性好容易定位问题可能过强需要限制 magnitude适合阶段小数据集、刚开始跑模型欠拟合、数据量够大时我的建议是先固定普通增广把基线上来再叠加 AutoAugment 做横向比较。项目源码同时保留两种入口“普通增广”和“AutoAugment”可以一键切换适合做消融实验。2.3 自制数据集的目录组织与验证集切分下载包解压后能看到DensNet121-torch目录和一批train*.jpg命名图片这符合 PyTorch 工程常见结构。我更推荐把目录整理成可直接给ImageFolder使用的形式data/ train/ 0_normal/ 001.jpg 002.jpg 1_light/ 2_medium/ 3_heavy/ val/ 0_normal/ ...from torchvision import datasets from torch.utils.data import DataLoader train_set datasets.ImageFolder(data/train, transformtrain_transform) val_set datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_set, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue)ImageFolder的标签从子目录名按字母序生成0_normal对应 01_light对应 1以此类推。训练时要注意验证集与训练集必须使用同一套 normalize 参数否则模型在推理阶段看到的输入分布不一致指标就不可比。这里有个很容易被忽略的坑同一次巡检拍摄的连续帧不允许 split 到 train 和 val 两侧。光伏板图像有很强的时序相关性如果连续拍摄的几十张图被随机切分验证集会“记住”背景而不是灰尘调参时会给你虚假的高分。数据准备时按照片时间戳或文件夹 ID 分组再在组级别做 train/val 切分。3. ResNet 与 DenseNet121 训练从交叉熵到监督对比损失对于光伏板积灰识别这类细粒度纹理分类模型第一层学习到的往往是边缘、角点深层才组合出“灰尘覆盖区域”这种语义。VGG 堆叠卷积层参数多且容易梯度消失ResNet 用残差连接让梯度直通DenseNet 更进一步把每层输出拼接到后续层特征复用性强。摘要里写到的DensNet121-torch应该就是 DenseNet121 的 PyTorch 版本这类网络在只有几千张图的自制数据集上比直接上大模型更稳。3.1 骨干网络选型感受野、参数量与迁移学习我在这个小项目里会优先试 ResNet50不只因为准确率而是训练稳定性好。ResNet 的 shortcut 可以在预训练权重基础上只更新部分层DenseNet121 参数量小但显存占用因为特征拼接反而偏高。三个网络方案的对比网络参数量输入尺寸特征复用灰尘任务适用点ResNet18约 11M224残差学习训练快作为消融基线ResNet50约 25M224残差学习梯度稳定预训练权重成熟DenseNet121约 8M224密集拼接特征重用适合小数据集提示不是参数越少越好。DenseNet 对实现细节敏感显存占用和 BN 统计量都比 ResNet 难调如果主线要放在损失函数上先以 ResNet50 为主。3.2 预训练权重的加载与两种微调策略torchvision.models里的resnet50(pretrainedTrue)权重来自 ImageNet用到光伏图上有两种常见方式。第一种冻结前几层只训练最后几个 block 和分类头第二种全部参与训练但给 backbone 一个较小的学习率。我用得比较多的是第二种因为灰尘纹理和 ImageNet 的自然图像差异较大冻结太狠会让底层特征无法适应板面材质。import torch import torchvision.models as models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_classes 4 in_features model.fc.in_features model.fc torch.nn.Linear(in_features, num_classes) backbone_params [ model.conv1.parameters(), model.layer1.parameters(), model.layer2.parameters(), model.layer3.parameters(), model.layer4.parameters(), ] optimizer torch.optim.AdamW([ {params: model.fc.parameters(), lr: 3e-4}, {params: backbone_params, lr: 1e-5}, ], weight_decay1e-4)新换的fc层是随机初始化的所以给它一个更大的学习率backbone 使用预训练权重学习率压到1e-5这样前几个 epoch 不会把预训练特征冲乱。如果不想手动拆参数组可以先冻结 backbone 训练fc几个 epoch再解冻 backbone 统一微调这种两阶段策略在自制数据集中也很常见。3.3 训练循环优化器、余弦退火和评估口径from torch.optim.lr_scheduler import CosineAnnealingLR epochs 60 scheduler CosineAnnealingLR(optimizer, T_maxepochs) for epoch in range(epochs): model.train() total_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss torch.nn.functional.cross_entropy(out, y) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() model.eval() # 这里记录 val loss、accuracy、macro-F1CosineAnnealingLR的T_maxepochs表示学习率从初始值余弦下降到接近 0。我建议训练 60 个 epoch前 10 个 epoch 观察 loss 是否正常下降后 30 个 epoch 主要靠余弦退火带来的细粒度收敛。如果显存不够batch size 从 16 起步学习率也要按比例缩放AdamW配合1e-4的weight_decay比普通 Adam 更稳。3.4 监督对比学习损失把同类灰尘样本拉近交叉熵单独使用时它只关心“这张图是不是重灰”不会显式构造样本之间的相似度。重度积灰和轻度积灰在特征空间中可能距离很远但在分类边界上的中度灰却很难分。监督对比损失SupConLoss把同一类别的样本作为正样本对不同类别的作为负样本对让特征空间更紧凑。这个项目把它作为补充损失正好匹配四分类的细粒度判别。import torch import torch.nn as nn import torch.nn.functional as F class SupConLoss(nn.Module): def __init__(self, temperature0.1): super().__init__() self.temperature temperature def forward(self, features, labels): # features: [batch, dim]labels: [batch] features F.normalize(features, dim1) sim features features.T / self.temperature mask (labels.reshape(-1, 1) labels.reshape(1, -1)).float() mask.fill_diagonal_(0.0) exp_sim torch.exp(sim) * mask log_prob sim - torch.log(exp_sim.sum(dim1, keepdimTrue) 1e-8) loss -(mask * log_prob).sum(dim1) / mask.sum(dim1) return loss.mean()temperature控制分布平滑度。温度越小模型对负样本越严格灰尘对比度低的时候我会用0.1如果训练震荡就调到0.2。需要注意SupConLoss的输入不是分类头的 logits而是 backbone 输出的 embedding实际使用时常在fc前拉一个投影头训练结束后再丢掉投影头。4. 复现时的高频故障与调优类别不平衡、增强泄漏和收敛问题这类项目在别人电脑上跑得好换一个环境又是一堆毛病。复现时我会先盯三个地方验证集指标是不是虚高、损失是不是一直不动、混淆矩阵里错得最多的到底是哪一类。4.1 类别不平衡先看混淆矩阵再谈重采样用 sklearn 对验证集输出预测结果from sklearn.metrics import confusion_matrix, classification_report y_true val_labels y_pred [] model.eval() with torch.no_grad(): for x, y in val_loader: out model(x.to(device)) y_pred out.argmax(dim1).cpu().numpy().tolist() cm confusion_matrix(y_true, y_pred) print(cm) print(classification_report( y_true, y_pred, target_names[normal, light, medium, heavy] ))打印出的对角线如果只有normal和heavy很高说明模型把大多数预测压到两个类别里中灰样本被当成了噪声。常见解决方法是加权采样让每个 epoch 里中灰样本出现的次数更接近重灰。from torch.utils.data import WeightedRandomSampler counts [500, 300, 120, 60] # 四个类别的实际样本数 weights [1.0 / c for c in counts] sample_weights [weights[label] for label in train_labels] sampler WeightedRandomSampler( sample_weights, num_sampleslen(train_labels), replacementTrue ) train_loader DataLoader(train_set, batch_size32, samplersampler)WeightedRandomSampler按类别逆频率过采样replacementTrue会重复抽到中灰样本。它不增加数据多样性但能避免梯度被多数类主导。另一种做法是直接用 Focal Loss它对易分类样本降权适合灰尘类别边界模糊的场景。4.2 AutoAugment 过强导致验证集分布偏移AutoAugment 是训练集专用策略验证集不要调用同样的 transform。我见过有人把 AutoAugment 加到训练后又顺手加进验证流程导致验证集样本被随机裁剪平移模型推理时看到的输入分布和训练不一致准确率掉 10 多个点。验证集只做Resize、CenterCrop、ToTensor、Normalize这是红线。训练损失低但验证损失异常高时先检查数据增强是否过强。比如ColorJitter的brightness上限超过0.4会让积灰区域的灰度特征被抹掉RandomRotation超过 15 度光伏板边框和电池片连接处的形态会被严重扭曲。遇到这种情况先降低增强幅度再考虑加正则。4.3 收敛失败学习率、BN 参数和预训练权重冻结现象可能原因排查/调整训练 loss 变成 NaN学习率过大 / 样本里有全黑图学习率降到 1e-4检查图像预处理验证准确率不进backbone 学习率太低把骨干学习率提到 3e-4或只训练分类头对比显存爆掉DenseNet 特征拼接用梯度累积调整实际 batch size收敛到 0.75 不再动类别不平衡换 SupConLoss或 WeightedRandomSamplertrain 和 val 指标都异常高验证集时间泄漏按时间段重新切分数据训练时如果使用pretrainedTrueBN 的running_mean和running_var会跟着微调前几个 epoch 会出现短暂震荡这是正常现象不要过早早停。复现时要固定随机种子同时给 DataLoader 设置shuffle的随机种子否则每跑一次结果都不一样很难判断改动是否有效。提示如果数据切分是“按文件夹分组再随机分裂”同步设置全局随机种子避免每组图片被顺序读进同一个 batch让梯度方向被连续相关性带偏。5. 用 Grad-CAM 验证模型关注的是灰尘区域还是栅线反光模型在验证集上拿到 98% 不代表它真的在“看灰尘”很可能学到了光伏板边缘的光晕或固定支架的纹理。验收这类识别项目时最后一定跑一次 Grad-CAM把模型最后一层卷积输出的梯度拿来看热力图。如果激活区域落在电池片之间没有灰尘的空白区就要回头检查数据增广或分类决策。# 如果没有安装先执行 pip install grad-cam from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image cam GradCAM(modelmodel, target_layers[model.layer4[-1]]) input_tensor val_transform(img).unsqueeze(0).to(device) grayscale_cam cam(input_tensorinput_tensor, targetsNone)[0] vis show_cam_on_image(img_rgb, grayscale_cam, use_rgbTrue)target_layers一般取 stage 最后一个残差块的输出。层选得太浅比如conv1热力图只反映边缘选到layer4[-1]时能捕捉到“积灰区域”这种高层语义。对 DenseNet121则把target_layers指向最后一个 DenseBlock 后的高层输出具体层名以打印model为准。判断时按类别看热力图分布重度积灰的热力图应该集中在灰块覆盖区域边缘弱轻度积灰的热力图应该稀疏地点在若干灰带上。如果热力图集中在太阳能板的高亮反射条纹说明模型学到的是反光而不是灰尘这时优先检查训练集里是否混入了大量阴天反光样本。最后把灰度热力图叠加图保存为 jpg与原图逐张对照确认模型关注区域和灰尘分布重合后再提交验收。本文还有配套的精品资源点击获取