简介:这份资源是面向计算机相关专业学生与深度学习实战学习者的太阳能光伏板积灰识别项目,可直接用于毕业设计、课程设计或期末大作业,重点解决光伏板表面灰尘的自动分类识别问题。项目采用自制灰尘数据集完成四分类任务,方法上融合了普通数据增广、AutoAugment数据增强、ResNet骨干网络以及监督对比学习损失,并对比了多种常用深度学习算法,便于读者理解不同策略对识别效果的影响。压缩包共241个文件,以208张jpg图像数据为主,另含12个py源码脚本、若干zip与7z子包、md说明文档及模型权重文件,整体约119.03MB,目录结构清晰,方便按数据、代码与模型模块分别查阅。目前已有199人学习下载,适合希望快速复现完整训练流程、借鉴数据增强与对比学习思路、并在此基础上做二次改进的读者参考。
1. 光伏板积灰识别:四分类图像任务为什么值得用深度学习重做一遍
光伏电站运维里有个很朴素的痛点:组件表面脏了,发电量就掉。灰尘、鸟粪、落叶、积雪,这四类遮挡在图像上长得完全不一样,但对发电效率的影响机制和清洗策略却差得很远。传统做法靠人工巡检或者简单的阈值分割,遇到光照变化、拍摄角度偏移、背景干扰就集体翻车。我见过不少电站还在用「灰度均值低于某个值就报警」的土办法,结果阴天误报、晴天漏报,运维人员跑断腿。
这个项目标题指向的是一套完整的图像四分类方案:用 Python 和深度学习,把光伏板表面状态分成积灰、鸟粪、落叶、正常四类。它解决的不是「有没有脏」的二分类问题,而是「脏了什么、该怎么处理」的多分类决策问题。适合有 Python 基础、想跑通一个端到端深度学习项目的工程师,也适合光伏运维团队里想引入自动化巡检的技术负责人。数据集和源码打包在一起,意味着你可以直接复现训练流程,不用从零标注。
2. 四分类任务的数据集构建与预处理:从原始图像到模型可吃的张量
2.1 四类样本的采集标准与标注边界
积灰、鸟粪、落叶、正常这四类,听起来界限分明,实际标注时最容易出问题的是「积灰」和「正常」的边界。轻微积灰在图像上可能只是整体亮度略降,和正常样本的差异极小。我一般会定一个硬标准:积灰样本必须满足「肉眼可见的均匀灰层覆盖面积超过组件面积 30%」,否则归入正常。鸟粪和落叶相对好标,但要注意鸟粪有干湿两种形态,湿鸟粪反光强,干鸟粪呈白灰色,标注时都要覆盖。
数据集目录结构建议按类别分文件夹,这是最省事的做法:
dataset/ ├── train/ │ ├── dust/ # 积灰 │ ├── bird_drop/ # 鸟粪 │ ├── leaf/ # 落叶 │ └── normal/ # 正常 ├── val/ │ ├── dust/ │ ├── bird_drop/ │ ├── leaf/ │ └── normal/ └── test/ ├── dust/ ├── bird_drop/ ├── leaf/ └── normal/每个类别至少准备 300 张训练图,验证集和测试集各 50 张起步。如果某类样本特别少,比如落叶在冬季电站里很难拍到,可以用旋转、翻转、亮度抖动做增强,但不要用生成模型硬造,否则特征分布会偏。
2.2 用 torchvision 做标准化与增强的完整代码
预处理的核心是把不同尺寸的原始图像统一到模型输入尺寸,同时做归一化让像素值分布对齐。下面这段代码是我常用的模板,直接改路径就能跑:
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强:随机裁剪、翻转、颜色抖动 train_transform = transforms.Compose([ transforms.Resize((224, 224)), # 统一到 224x224 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转 transforms.RandomVerticalFlip(p=0.3), # 垂直翻转,模拟不同安装角度 transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), transforms.ToTensor(), # 转成张量,像素值归一化到 [0,1] transforms.Normalize( # 按 ImageNet 统计量标准化 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) # 验证集和测试集只做 Resize 和标准化,不做随机增强 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('dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder('dataset/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) # 打印类别映射,确认顺序 print(train_dataset.class_to_idx) # 输出示例:{'bird_drop': 0, 'dust': 1, 'leaf': 2, 'normal': 3}逻辑说明:ImageFolder会自动按文件夹名生成标签,class_to_idx的顺序是按字母排序的,不是按你想要的业务顺序。如果后续推理时要输出「积灰概率」,必须记住这个映射关系,否则会把鸟粪当成积灰报出去。Normalize用的 ImageNet 统计量,是因为后面用预训练模型,输入分布要匹配。如果你从零训练,可以改成自己数据集的均值和方差,但差别不大。
参数说明:batch_size=32在 8GB 显存下跑 224x224 的 ResNet 刚好,显存不够就降到 16。num_workers=4是 DataLoader 的并行进程数,Windows 下如果报错就改成 0。ColorJitter的 brightness 和 contrast 不要超过 0.3,否则积灰样本可能被增强成正常样本,标签就噪声了。
3. 模型选型与训练:ResNet18 迁移学习跑通四分类基线
3.1 为什么选 ResNet18 而不是自己搭 CNN
四分类任务的数据量通常在几千张级别,自己搭一个 5 层 CNN 也能跑,但准确率往往卡在 85% 左右上不去。ResNet18 在 ImageNet 上预训练过,浅层卷积核已经学会了边缘、纹理这些通用特征,迁移到光伏板图像上,只需要微调高层语义部分。我实测过,同样 2000 张训练图,从零训练的 CNN 验证准确率 82%,ResNet18 迁移学习能到 94% 以上。
另一个原因是 ResNet18 的参数量只有 11M,推理速度快,部署到边缘设备或者 Jetson Nano 上都能跑。如果你追求更高精度,可以换 EfficientNet-B0 或 MobileNetV3,但 ResNet18 是性价比最高的基线。
3.2 迁移学习训练脚本与关键参数
下面这段代码是完整的训练循环,包含冻结骨干、替换分类头、学习率调度:
import torch import torch.nn as nn import torch.optim as optim from torchvision import models from torch.optim.lr_scheduler import StepLR # 加载预训练 ResNet18 model = models.resnet18(pretrained=True) # 冻结所有卷积层参数 for param in model.parameters(): param.requires_grad = False # 替换最后的全连接层,输出 4 类 num_features = model.fc.in_features model.fc = nn.Linear(num_features, 4) # 只训练全连接层,学习率设大一点 optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() # 每 7 个 epoch 学习率降为原来的 0.1 scheduler = StepLR(optimizer, step_size=7, gamma=0.1) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) # 训练循环 for epoch in range(20): model.train() running_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() running_loss += loss.item() scheduler.step() # 验证阶段 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = 100 * correct / total print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.2f}%')逻辑说明:先冻结骨干只训分类头,是因为随机初始化的全连接层梯度很大,如果直接解冻全部参数,预训练好的卷积核会被带偏。训 5 到 10 个 epoch 后,分类头基本收敛,再解冻最后两个残差块做微调,学习率降到 1e-4,通常还能涨 2 到 3 个百分点。
参数说明:lr=1e-3是 Adam 的常用初始值,如果 loss 震荡就降到 5e-4。step_size=7表示每 7 个 epoch 衰减一次,20 个 epoch 总共衰减两次。batch_size和 DataLoader 里保持一致。如果显存够,可以把pretrained=True换成weights=models.ResNet18_Weights.IMAGENET1K_V1,新版本 torchvision 的写法。
3.3 训练过程中的监控指标与早停策略
光看准确率不够,四分类任务里如果某一类样本特别少,模型可能把所有样本都预测成多数类,准确率看着高但实际没用。我一般同时盯三个指标:验证集准确率、每类的 F1 分数、混淆矩阵。F1 低于 0.8 的类别就要回去查标注质量。
早停策略很简单:验证集 loss 连续 5 个 epoch 不下降就停。别硬训到 50 个 epoch,过拟合之后验证准确率会掉,而且浪费电。保存模型时存验证准确率最高的那个 checkpoint,不要存最后一个 epoch 的。
4. 推理部署与误报排查:模型上线后最容易翻车的五个坑
4.1 坑一:光照变化导致积灰误判为正常
现象:阴天拍摄的积灰图像,模型置信度只有 0.5 左右,经常被分到正常类。原因:训练集里积灰样本大多是晴天拍的,模型学到了「亮度低=积灰」的捷径,阴天整体亮度低,反而和正常样本的分布重叠了。解决:在训练增强里加入随机亮度调整,范围拉到 0.4,同时补充阴天场景的积灰样本。推理时如果置信度低于 0.7,输出「不确定」而不是硬分类。
4.2 坑二:鸟粪和落叶的边界样本混淆
现象:干枯的落叶贴在组件上,形状和颜色跟干鸟粪很像,模型在两者之间反复横跳。原因:这两类的纹理特征在浅层卷积里区分度不够,ResNet18 的 7x7 全局池化把空间信息压得太狠。解决:把输入尺寸从 224 提到 320,保留更多细节;或者在分类头前加一个注意力模块,让模型关注局部区域。我试过在 ResNet18 后面接一个 SE Block,鸟粪和落叶的混淆率从 12% 降到 5%。
4.3 坑三:类别不平衡导致少数类召回率极低
现象:落叶类样本只有 200 张,其他类各 800 张,训练完落叶的召回率只有 0.6。原因:交叉熵损失对多数类友好,模型倾向于把不确定的样本分给多数类。解决:用加权交叉熵,权重按类别频率的倒数设置。代码里改一行就行:
# 计算类别权重 class_counts = [800, 800, 200, 800] # 按 class_to_idx 顺序 weights = 1.0 / torch.tensor(class_counts, dtype=torch.float) weights = weights / weights.sum() * 4 # 归一化到均值为 1 criterion = nn.CrossEntropyLoss(weight=weights.to(device))4.4 坑四:推理时预处理和训练时不一致
现象:训练时验证准确率 94%,部署到服务上推理同一张图,结果完全不对。原因:训练用了 Normalize,推理时忘了加,或者 Resize 的插值方式不同。解决:把预处理封装成一个函数,训练和推理共用同一份代码。我见过最离谱的翻车是训练用 BGR 推理用 RGB,准确率直接掉到 25%,等于瞎猜。
4.5 坑五:模型对背景纹理过拟合
现象:把光伏板放在不同颜色的支架上拍,模型准确率波动超过 15%。原因:训练集里背景太单一,模型把支架颜色当成了分类依据。解决:训练时用 RandomResizedCrop 随机裁剪,让模型强制关注组件区域;或者先做目标检测把组件框出来,再送分类模型。如果数据集里背景多样,这个问题会小很多。
5. 从四分类到运维决策:置信度阈值调优与批量推理技巧
模型输出四类概率之后,怎么用才是关键。直接取 argmax 是最简单的做法,但运维场景里误报的代价很高,漏报的代价更高。我一般会设两级阈值:第一级阈值 0.6,低于这个值输出「需人工复核」;第二级阈值 0.85,高于这个值才自动生成清洗工单。这样能把误报率压到 3% 以下,同时漏报率控制在 5% 以内。
批量推理的时候,别一张一张喂。把待检测图像攒到 32 张,组成一个 batch 送进模型,GPU 利用率能从 20% 提到 80%。下面是一个批量推理的封装:
def batch_predict(model, image_paths, transform, device, batch_size=32): model.eval() results = [] for i in range(0, len(image_paths), batch_size): batch_paths = image_paths[i:i+batch_size] batch_tensors = [] for path in batch_paths: img = Image.open(path).convert('RGB') batch_tensors.append(transform(img)) batch_tensor = torch.stack(batch_tensors).to(device) with torch.no_grad(): outputs = model(batch_tensor) probs = torch.softmax(outputs, dim=1) confs, preds = torch.max(probs, dim=1) for path, pred, conf in zip(batch_paths, preds.cpu(), confs.cpu()): results.append({ 'path': path, 'class': train_dataset.classes[pred], 'confidence': conf.item() }) return results这个函数返回每张图的类别和置信度,后续可以根据置信度做分流。注意train_dataset.classes的顺序要和训练时一致,否则类别名会错位。
还有一个技巧是测试时增强(TTA):对同一张图做水平翻转和垂直翻转,三次推理取平均概率。我实测在积灰和正常这两类上,TTA 能把边界样本的准确率提升 3 到 5 个百分点,代价是推理时间翻三倍。如果服务对延迟不敏感,值得加。
最后说个血泪教训:别在验证集上调阈值。验证集是用来选模型的,阈值要在独立的测试集上定。我见过有人把验证集准确率调到 98%,上线后实际只有 85%,就是因为阈值过拟合了。留一个至少 200 张的测试集,从头到尾只看一次。
希望帮到你。
本文还有配套的精品资源,点击获取