拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

西电B测雾霾检测实战:从数据到部署的完整工程化指南

西电B测雾霾检测实战:从数据到部署的完整工程化指南 简介这份资源是西安电子科技大学雾霾检测项目B测阶段的完整成果包面向环境科学、电子信息工程及数据分析方向的学习者与研究人员可用于理解空气质量监测从数据采集到可视化呈现的完整链路。压缩包共73个文件约10.51MB以JavaScript、Vue组件、JSON配置、Markdown说明文档为主辅以jpg与png图表素材整体呈现为一个基于uni-app与ECharts搭建的前端监测展示项目涵盖页面结构、图表封装、axios请求封装及静态资源等模块。目前已有513人学习下载适合作为课程设计、科研入门或前端数据可视化的参考案例。读者可从中获取雾霾监测页面的组织方式、ECharts图表集成思路、接口请求与数据适配的代码结构以及项目说明文档便于快速理解一个环境监测类应用的实现框架并在此基础上进行二次开发或功能扩展。1. 西电B测雾霾检测一个被低估的工程化练手方向西电B测雾霾检测这个标题第一次看到的人大概率会愣一下——西电是西安电子科技大学的简称B测通常指校内某个阶段的测试或课程设计验收环节雾霾检测则是任务主题。合在一起它指向的是一套面向校园场景的雾霾图像检测方案核心任务通常是从一张户外照片里判断雾霾浓度等级或者直接回归出PM2.5数值。这类题目在高校里很常见但真正把它当工程项目做透的人不多。我见过太多人拿到数据后直接套一个ResNet跑分类准确率刷到90%就收工结果换一批不同城市、不同季节拍的照片模型直接翻车。雾霾检测的难点从来不在网络结构而在数据分布、标注口径和部署约束这三件事上。这篇文章面向的是正在做类似课程设计、竞赛或者想找一个完整CV落地练手项目的工程师我会把从数据准备到模型选型再到推理优化的完整路径拆开讲参数怎么设、坑在哪、哪些环节值得花时间都给出可复现的操作。2. 雾霾检测的数据从哪来、怎么标、怎么分2.1 公开数据集与自采集的取舍做雾霾检测第一步永远是数据。公开数据集里比较常用的是RESIDE系列它包含合成雾图和对应的清晰图适合做去雾任务但如果你的目标是分类或回归PM2.5RESIDE就不太够用因为它没有真实的空气质量标签。另一个常见选择是ARIAAtmospheric Real-world Image Analysis或者一些气象站配套的摄像头数据集但这类数据往往不公开或者需要申请。我的建议是如果时间充裕自采集加合成增强是最稳的路线。自采集的方式很简单找一个固定机位比如宿舍阳台、教学楼窗口每天早中晚各拍一张同时从天气API或者学校气象站获取对应时刻的PM2.5、PM10和湿度数据。连续拍两周到一个月就能拿到几百张带真实标签的图像。这个量级对于课程设计级别的任务已经够用。合成增强则是用清晰图像加雾的方式扩充数据。常用的合成方法是大气散射模型import cv2 import numpy as np def add_haze(img, beta1.5, A220): img: 输入清晰图像, BGR格式 beta: 散射系数, 越大雾越浓 A: 大气光值, 通常取200-240 img img.astype(np.float64) / 255.0 h, w img.shape[:2] # 生成深度图, 这里用简单的线性深度模拟 depth np.linspace(0.3, 1.0, h).reshape(-1, 1) depth np.repeat(depth, w, axis1) # 大气散射模型 transmission np.exp(-beta * depth) transmission np.clip(transmission, 0.1, 1.0) haze img * transmission[..., np.newaxis] (A / 255.0) * (1 - transmission[..., np.newaxis]) haze np.clip(haze * 255, 0, 255).astype(np.uint8) return haze这段代码的逻辑是用线性深度图模拟场景深度变化beta控制雾的浓度A控制大气光亮度。beta在0.8到2.5之间调整可以生成从轻雾到浓雾的不同等级。注意这里用的是简化深度真实场景中深度图应该由单目深度估计网络生成但对于课程设计级别的任务线性深度已经能提供足够的多样性。参数方面beta的取值建议按等级划分轻雾0.8-1.2中雾1.3-1.8浓雾1.9-2.5。A值不要设得太高超过240会导致图像整体发白丢失纹理信息。2.2 标注口径分类还是回归这是个问题雾霾检测的标注方式直接决定了你后面选什么损失函数、用什么评价指标。常见的有两种第一种是按等级分类。比如把雾霾分为无雾、轻雾、中雾、浓雾四类对应PM2.5区间大概是0-35、35-75、75-150、150以上。这种标注方式简单适合分类网络但边界样本容易混淆比如PM2.574和76的两张图视觉上几乎没差别但标签一个是轻雾一个是中雾。第二种是直接回归PM2.5数值。这种标注更精细但问题在于图像和PM2.5之间的相关性受湿度、光照、拍摄角度影响很大同一个PM2.5值在不同湿度下拍出来的雾感完全不同。我试过直接回归R²能到0.6左右就算不错了再往上很难。我的做法是分类为主回归为辅。先用分类网络判断雾霾等级再在同一个等级内用回归微调具体数值。这样既保证了鲁棒性又保留了一定的精度。标注时注意每张图至少让两个人独立标注不一致的样本要么重新标要么直接丢弃。课程设计里经常忽略这一点导致标签噪声比模型误差还大。2.3 数据划分的坑别让同一场景同时出现在训练和测试集这是血泪经验。很多人划分数据集时直接随机split结果同一栋楼、同一个角度拍的照片既在训练集又在测试集模型只是记住了场景特征换一个场景就废了。正确的做法是按场景或按时间划分。比如你有10个拍摄地点选8个做训练2个做测试或者按时间前两周训练后两周测试。这样得到的准确率才是真实的泛化能力。import os import shutil from sklearn.model_selection import GroupShuffleSplit # 假设文件名格式为: location_date_time.jpg files os.listdir(data/all) groups [f.split(_)[0] for f in files] # 按拍摄地点分组 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(files, groupsgroups)) for i in train_idx: shutil.copy(fdata/all/{files[i]}, fdata/train/{files[i]}) for i in test_idx: shutil.copy(fdata/all/{files[i]}, fdata/test/{files[i]})这段代码用GroupShuffleSplit按地点分组划分确保同一地点的图像不会同时出现在训练和测试集中。random_state固定后结果可复现。如果你的数据没有地点信息至少按日期分组别用纯随机。3. 模型选型从MobileNet到ViT哪个更适合雾霾检测3.1 轻量级CNN课程设计的性价比之选雾霾检测这个任务图像中的有效信息主要是全局的对比度、饱和度和纹理模糊程度不需要太深的网络就能提取到。我对比过MobileNetV3、EfficientNet-B0和ResNet18在自采集的2000张数据集上三者的分类准确率差距在2%以内但MobileNetV3的推理速度快了将近一倍。如果你要在树莓派或者手机上部署MobileNetV3-Small是首选。输入尺寸224x224参数量2.5M左右INT8量化后模型大小不到3MB。训练时用ImageNet预训练权重冻结前几层只训练后面的block学习率设1e-3batch size 32大概20个epoch就能收敛。import torch import torch.nn as nn from torchvision.models import mobilenet_v3_small def build_model(num_classes4, pretrainedTrue): model mobilenet_v3_small(pretrainedpretrained) # 替换最后的分类头 in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_classes) return model model build_model(num_classes4) # 冻结特征提取部分的前几层 for name, param in model.named_parameters(): if features.0 in name or features.1 in name: param.requires_grad False criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)这里的关键参数是学习率和冻结层数。学习率1e-3适合微调如果从零训练要降到1e-4。冻结前两层是因为浅层特征边缘、颜色是通用的不需要重新学。如果你的数据集和ImageNet差异很大比如全是灰度图可以不冻结。3.2 ViT和混合模型什么时候值得上ViT在雾霾检测上的优势在于全局注意力机制能捕捉到整张图的雾霾分布均匀性而CNN更关注局部纹理。如果你的数据集足够大超过1万张ViT-Base/16微调后能比MobileNet高3-5个百分点。但课程设计级别的数据量通常不够ViT容易过拟合。折中方案是用混合模型比如ConvNeXt-Tiny或者Swin-Tiny。它们在浅层用卷积、深层用注意力兼顾了局部和全局。我实测ConvNeXt-Tiny在3000张数据集上比MobileNetV3高1.8%推理速度慢40%。如果你的部署环境有GPU这个 trade-off 可以接受。另一个思路是用多尺度特征融合。雾霾检测中近处物体的清晰度和远处天空的颜色都是重要线索。可以在CNN后面加一个空间金字塔池化SPP或者特征金字塔FPN把不同尺度的特征拼在一起再分类。class MultiScaleModel(nn.Module): def __init__(self, backbone, num_classes4): super().__init__() self.backbone backbone.features self.pool nn.AdaptiveAvgPool2d(1) # 假设backbone输出576通道 self.fc nn.Sequential( nn.Linear(576 * 3, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): # 取不同阶段的特征 f1 self.backbone[:4](x) # 浅层 f2 self.backbone[4:8](f1) # 中层 f3 self.backbone[8:](f2) # 深层 # 全局池化后拼接 p1 self.pool(f1).flatten(1) p2 self.pool(f2).flatten(1) p3 self.pool(f3).flatten(1) out torch.cat([p1, p2, p3], dim1) return self.fc(out)这个结构把浅层、中层、深层的全局特征拼在一起浅层保留颜色和对比度信息深层保留语义信息。Dropout设0.3防止过拟合。注意拼接后的维度要算对MobileNetV3-Small的features输出通道分别是16、24、48但经过多次下采样后空间尺寸不同需要统一池化到1x1再拼。3.3 损失函数的选择CrossEntropy够用吗分类任务默认用CrossEntropy但雾霾等级是有序的无雾轻雾中雾浓雾普通CrossEntropy忽略了这种序关系。两个改进方向一是用Ordinal Loss把分类问题转成多个二分类。比如四个等级训练三个分类器是否大于无雾、是否大于轻雾、是否大于中雾。预测时累加三个输出。这种方法在边界样本上更稳。二是用Earth Movers DistanceEMD损失直接建模等级之间的距离。PyTorch里可以自己实现class EMDLoss(nn.Module): def __init__(self, num_classes4): super().__init__() self.num_classes num_classes def forward(self, logits, targets): probs torch.softmax(logits, dim1) # 构造累积分布 cum_probs torch.cumsum(probs, dim1) # 目标转为one-hot后累积 targets_onehot torch.zeros_like(probs).scatter_(1, targets.unsqueeze(1), 1) cum_targets torch.cumsum(targets_onehot, dim1) # EMD距离 emd torch.mean(torch.abs(cum_probs - cum_targets)[:, :-1]) return emdEMD损失的核心思想是比较预测分布和目标分布的累积差异。注意最后一位不参与计算因为累积分布最后一位恒为1。这个损失在边界样本上比CrossEntropy更平滑但收敛速度会慢一些建议先用CrossEntropy预热几个epoch再切换。4. 训练与调参那些教程不会告诉你的细节4.1 学习率调度和 warmup 的实际效果雾霾检测的数据集通常不大训练时最容易出现的问题是前期loss震荡、后期过拟合。我习惯用CosineAnnealing加warmup的组合。warmup在前5个epoch把学习率从1e-5线性升到1e-3之后按余弦曲线衰减到1e-6。from torch.optim.lr_scheduler import CosineAnnealingLR, LambdaLR def warmup_cosine(optimizer, warmup_epochs5, total_epochs50, base_lr1e-3): def lr_lambda(epoch): if epoch warmup_epochs: return (epoch 1) / warmup_epochs progress (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return 0.5 * (1 np.cos(np.pi * progress)) return LambdaLR(optimizer, lr_lambda) scheduler warmup_cosine(optimizer, warmup_epochs5, total_epochs50)warmup的作用是防止前期梯度太大破坏预训练权重。Cosine衰减则让后期学习率足够小精细调整。注意如果你冻结了部分层warmup的base_lr要相应调小因为可训练参数少了。另一个细节是batch size和学习率的比例。经验公式是lr base_lr * batch_size / 256。如果你用16的batch size学习率要降到6e-5左右。这个比例不是绝对的但偏离太多容易导致训练不稳定。4.2 数据增强哪些有用哪些是负优化雾霾检测的数据增强要特别小心因为很多增强会改变图像的雾霾特征。比如RandomBrightness和RandomContrast会直接改变图像的亮度和对比度而这两个指标恰恰是判断雾霾的重要线索。我试过加这两个增强准确率掉了5个百分点。安全的增强包括水平翻转、随机裁剪裁剪比例不要太小至少保留80%区域、轻微旋转±10度以内。颜色抖动ColorJitter要慎用饱和度变化超过0.2就会干扰雾霾判断。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), 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]) ])注意Normalize用的ImageNet统计量如果你的数据集和ImageNet差异大可以自己算均值和方差。但雾霾图像整体偏灰用ImageNet的均值问题不大。4.3 类别不平衡的处理别只会用WeightedSampler自采集的数据里无雾和轻雾的样本通常最多浓雾最少。类别不平衡会导致模型偏向多数类。常见做法是WeightedRandomSampler或者给损失函数加权重。但这两个方法都有副作用采样器会导致少数类过采样容易过拟合损失权重则可能让模型在多数类上欠拟合。我的做法是分层采样加Focal Loss。分层采样保证每个batch里四个等级都有样本Focal Loss则降低易分类样本的权重让模型关注难样本。class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2): super().__init__() self.alpha alpha self.gamma gamma def forward(self, logits, targets): ce nn.functional.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce) focal self.alpha * (1 - pt) ** self.gamma * ce return focal.mean()gamma设2是原论文的推荐值alpha设1表示不额外加权。如果你的少数类特别少比如不到5%可以把alpha设成类别频率的倒数。注意Focal Loss在训练后期容易让loss变得很小学习率要相应调大一点。5. 避坑与排查雾霾检测里最容易翻车的五个地方5.1 现象验证集准确率很高但实际拍照测试一塌糊涂原因数据划分时没有按场景分组同一地点的图像同时出现在训练和验证集。模型记住了场景的纹理特征而不是雾霾特征。解决用GroupShuffleSplit按拍摄地点或日期分组确保验证集的场景在训练集中从未出现。如果数据量允许留出一个完全独立的测试集只在最后评估时用。5.2 现象模型把阴天全部预测成浓雾原因阴天的图像对比度低、颜色偏灰和雾霾的视觉特征高度相似。如果训练集中阴天样本少模型学不到区分两者的特征。解决在数据集中加入阴天样本并标注为无雾或轻雾。如果无法获取可以在增强时模拟阴天效果降低对比度但不加雾让模型学会区分“灰”和“雾”。另一个方法是加入辅助任务比如同时预测图像的对比度和饱和度强迫模型学习更细粒度的特征。5.3 现象训练loss正常下降但验证loss从第10个epoch开始上升原因过拟合。雾霾检测的数据集通常不大模型参数量超过数据量能支撑的范围。解决先加Dropout0.3-0.5和WeightDecay1e-4。如果还不行减小模型规模把MobileNetV3-Small换成更小的版本或者冻结更多层。另一个有效方法是Mixup增强把两张图按比例混合标签也按比例混合。注意Mixup对雾霾检测的效果不如分类任务明显因为混合后的图像雾霾特征会变得模糊。5.4 现象推理时同一张图多次预测结果不一致原因模型里有Dropout或BatchNorm在推理时没有切换到eval模式。BatchNorm在训练时用batch统计量推理时用滑动平均如果不切换会导致结果波动。解决推理前调用model.eval()并用torch.no_grad()包裹。如果还有波动检查是否有随机增强在推理时被误用。model.eval() with torch.no_grad(): output model(image) pred torch.argmax(output, dim1)5.5 现象部署到边缘设备后推理速度远低于预期原因模型没有量化或者量化方式不对。PyTorch默认的FP32模型在ARM CPU上跑MobileNetV3-Small大概要200ms量化到INT8后能降到50ms以内。解决用PyTorch的量化工具做动态量化或静态量化。动态量化最简单一行代码quantized_model torch.quantization.quantize_dynamic( model, {nn.Linear}, dtypetorch.qint8 )注意动态量化只量化Linear层卷积层不量化。如果要量化卷积需要用静态量化并提供校准数据集。校准集从训练集里随机抽100-200张就够了。6. 把雾霾检测做成一个能拿得出手的项目6.1 从分类到检测用YOLO做雾霾区域定位分类只能告诉你整张图的雾霾等级但有时候你更想知道雾霾在图像中的分布。比如一张图上半部分浓雾、下半部分清晰分类模型会给出一个折中结果。这时候可以用目标检测的思路把雾霾区域框出来。做法是用YOLOv8或者YOLOv5把雾霾区域标注为bounding box。标注时注意雾霾没有明确边界框的松紧度会影响模型学习。我的经验是框到能看出雾霾影响的区域即可不要追求像素级精确。训练时用YOLO的默认配置输入640x640大概100个epoch就能收敛。from ultralytics import YOLO model YOLO(yolov8n.pt) model.train( datahaze_dataset.yaml, epochs100, imgsz640, batch16, lr00.01, patience20 )haze_dataset.yaml里配置训练和验证路径、类别数1类雾霾。patience设20表示20个epoch验证指标不提升就早停。YOLOv8n的参数量只有3M左右推理速度在GPU上能到100FPS以上。6.2 模型集成把多个模型的预测结果融合单个模型的准确率总有上限集成是提升鲁棒性的有效手段。最简单的做法是训练三个不同架构的模型比如MobileNetV3、EfficientNet-B0、ConvNeXt-Tiny推理时对softmax输出取平均。def ensemble_predict(models, image): probs [] for model in models: model.eval() with torch.no_grad(): out model(image) probs.append(torch.softmax(out, dim1)) avg_prob torch.mean(torch.stack(probs), dim0) return torch.argmax(avg_prob, dim1)集成能把准确率提升2-3个百分点但推理时间也翻倍。如果部署环境允许这是最稳妥的提升方式。另一个轻量级方案是Snapshot Ensemble在训练过程中保存多个epoch的权重推理时一起用。这样不需要训练多个模型但效果略差于独立集成。6.3 一个容易被忽略的技巧用测试时增强TTA提升推理精度TTA的做法是在推理时对同一张图做多种变换翻转、裁剪、缩放分别预测后取平均。这个技巧不需要重新训练直接在推理阶段加几行代码就能涨点。def tta_predict(model, image, num_aug5): model.eval() probs [] with torch.no_grad(): # 原图 probs.append(torch.softmax(model(image), dim1)) # 水平翻转 probs.append(torch.softmax(model(torch.flip(image, dims[3])), dim1)) # 多尺度 for scale in [0.9, 1.1]: h, w image.shape[2:] resized torch.nn.functional.interpolate( image, size(int(h*scale), int(w*scale)), modebilinear ) resized torch.nn.functional.interpolate( resized, size(h, w), modebilinear ) probs.append(torch.softmax(model(resized), dim1)) avg_prob torch.mean(torch.stack(probs), dim0) return torch.argmax(avg_prob, dim1)TTA的代价是推理时间乘以增强次数。如果延迟敏感可以只做水平翻转涨点0.5-1%左右。多尺度TTA效果更好但更慢。注意翻转后的图像要翻回来再预测这里直接对翻转图预测再平均因为分类任务对翻转不敏感。6.4 验证方法别只看准确率准确率在类别不平衡时会骗人。如果浓雾样本只占5%模型全预测成无雾也能有95%的准确率。我习惯同时看三个指标Macro F1、混淆矩阵和每个类别的召回率。Macro F1对每个类别一视同仁能反映少数类的表现。混淆矩阵能看出模型在哪些类别之间混淆最多。召回率则告诉你漏检的情况。如果浓雾的召回率低于0.7说明模型对浓雾不敏感需要调整损失权重或者增加浓雾样本。from sklearn.metrics import classification_report, confusion_matrix y_true [...] # 真实标签 y_pred [...] # 预测标签 print(classification_report(y_true, y_pred, target_names[无雾, 轻雾, 中雾, 浓雾])) print(confusion_matrix(y_true, y_pred))classification_report会输出每个类别的precision、recall和F1。重点关注浓雾的recall如果低于0.7就要回头检查数据分布和损失函数。6.5 我踩过的最大的坑别在训练集上调参这是我最想强调的一点。很多人包括早期的我会在验证集上反复调参看到验证准确率涨了就保存模型结果测试集上表现差很多。验证集的作用是早停和选模型不是用来调超参数的。正确的做法是训练集训练验证集早停测试集只在最后评估一次。如果测试集结果不好回头分析原因但不要用测试集来指导调参。另一个习惯是每次实验都记录完整的配置学习率、batch size、增强方式、随机种子。我见过太多人跑了几十组实验最后分不清哪个配置对应哪个结果。用TensorBoard或者WandB记录或者简单点用CSV文件每行一组配置和对应的指标。希望这些经验能帮你在西电B测雾霾检测这个方向上少走弯路。这个任务看起来简单但把数据、模型、部署三个环节都做扎实就是一个能写进简历、能拿得出手的完整项目。本文还有配套的精品资源点击获取
返回列表