ARTICLE DETAIL

资讯详情

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

基于ResNet的恶劣天气图像分类:1000张小样本数据集实战指南

基于ResNet的恶劣天气图像分类:1000张小样本数据集实战指南 简介面向恶劣天气识别与图像分类任务的已标注数据集覆盖大雾、暴雨、沙尘暴、暴雪四类天气样本约1000张适合深度学习入门、CNN分类课程设计、算法复现以及多类别天气识别场景研究也可用于道路能见度预警等应用探索。整个压缩包含1030个文件以1028张JPG图片为主体配有一个JSON标注文件用于查看四类标签另附Python可视化脚本可快速预览样本并检查数据质量压缩包大小约132MB文件命名规范类别目录清晰。已有667人学习下载。资源已划分好训练集与测试集目录按类别存放直接可用于模型训练、评估与调参也便于在Jupyter Notebook等环境中自行做数据增强与对比实验对需要自建天气数据集的用户来说可显著节省爬取、清洗和标注的时间同时可作天气分类基准数据使用。1. 大雾、暴雨、沙尘暴、暴雪图像分类1,000张已经够做八成的天气过滤任务做户外监控、自动驾驶数据预筛、或者电厂巡检告警时你大概率遇到同一个尴尬摄像头一拍到坏天气后续的目标检测、语义分割模型就开始集体“失明”。与其让主模型硬扛更务实的做法是在前面挂一个天气图像分类器把大雾、暴雨、沙尘暴、暴雪的图片先分流出来单独处理。这个标题给的就是一份已标注的四种恶劣天气图像分类数据集约1,000张。反直觉的结论是1,000张在四分类任务上够用甚至能跑到85%以上的验证准确率。原因是大雾、暴雨、沙尘暴、暴雪在视觉上是颜色分布和纹理结构差异极大的四类场景类别之间的特征距离很宽不是“猫和狗”那种细粒度差异再加上预训练模型迁移学习等效样本量远大于1,000张。但前提有两件事必须做对一是数据本身要仔细校验二是训练时要防过拟合。这套方案适合两类人——刚入门图像分类、需要一份干净数据练手的学生以及要在真实业务里快速搭一个天气前置过滤器的算法工程师。2. 拿到手先别训模型1,000张已标注数据的目录组织与标签校验“已标注”三个字是起点不是终点。标注文件里标签写的是“暴雨”不代表图片内容真的是暴雨——有的是文件名带“rain”但内容是雨后的地面有的是大雾里夹着雨丝被标成了暴雨还有的是沙尘暴里的扬沙被标成了雾。这种噪声在1,000张的小数据集里占比哪怕5%就会让模型学会抄捷径而不是学天气特征。所以动手训练前的第一步永远是数据盘点。2.1 拿到数据先做四项体检数量、尺寸、格式、标签分布用一段脚本把这四件事全查清楚。别用眼睛一个个点开看效率太低。import glob import os from collections import Counter from PIL import Image img_paths glob.glob(dataset/**/*.jpg, recursiveTrue) \ glob.glob(dataset/**/*.png, recursiveTrue) formats Counter() sizes [] labels [] for path in img_paths: ext os.path.splitext(path)[1].lower() formats[ext] 1 with Image.open(path) as im: sizes.append(im.size) labels.append(os.path.basename(os.path.dirname(path))) print(图片总数:, len(img_paths)) print(格式分布:, formats) print(标签分布:, Counter(labels)) print(尺寸范围:, min(w for w, h in sizes), min(h for w, h in sizes), -, max(w for w, h in sizes), max(h for w, h in sizes))这段脚本能同时回答四个关键问题数量是否接近1,000张、四种天气的类别是否均衡、尺寸是不是有极端超宽图或分辨率极低的小图、格式是否混杂。如果发现某类只有180张而另一类有320张后面训练时就要处理类别不平衡如果发现图片里混着webp或者bmp统一转成jpg避免DataLoader读取时踩格式兼容的坑。格式统一这一步我一般用下面的代码批量转掉顺便把中文文件名和目录名全部改成英文。Windows环境下Python读取中文路径容易出编码问题改成英文目录名后训练脚本和部署脚本都不会被路径坑到。import os from PIL import Image root dataset mapping {大雾: fog, 暴雨: rain, 沙尘暴: sandstorm, 暴雪: snow} for ch_name, en_name in mapping.items(): ch_dir os.path.join(root, ch_name) en_dir os.path.join(root, en_name) os.makedirs(en_dir, exist_okTrue) for fname in os.listdir(ch_dir): src os.path.join(ch_dir, fname) if not os.path.isfile(src): continue im Image.open(src).convert(RGB) en_path os.path.join(en_dir, os.path.splitext(fname)[0] .jpg) im.save(en_path, quality95) print(完成转换:, ch_name, -, en_name)2.2 用ImageFolder按类别组织目录整理脚本与重命名规则torchvision的datasets.ImageFolder要求数据按“根目录/类别名/图片文件”的层级存放目录名就是标签名。好处是零配置文件训练代码里三行就能加载坏处是目录名是唯一的标签来源一旦放错文件夹标签就是错的。我建议的顶层结构是这样的dataset/ ├── fog/ │ ├── fog_001.jpg │ ├── fog_002.jpg │ └── ... ├── rain/ │ ├── rain_001.jpg │ ├── rain_002.jpg │ └── ... ├── sandstorm/ └── snow/文件名统一用类别_序号.jpg不要保留原始文件名。原始文件名经常带着来源网站的水印痕迹、时间戳甚至拍摄者信息训练时模型如果通过文件名“作弊”判断类别部署后文件名一变模型就废了。重命名脚本很简单遍历每个类别目录按序号补零重命名即可。2.3 不迷信“已标注”用快速可视化抽检代替逐张人工复核1,000张图逐张看大概要半小时到一小时对工程师来说太奢侈。更快的方式是从每个类别随机抽9到16张拼成一个九宫格扫一眼就能发现大部分标注问题。import matplotlib.pyplot as plt from torchvision import datasets, transforms dataset datasets.ImageFolder( dataset, transformtransforms.Resize((224, 224)), ) fig, axes plt.subplots(4, 8, figsize(16, 8)) for cls_idx in range(4): cls_images [i for i, (_, label) in enumerate(dataset.samples) if label cls_idx] for row in range(8): img, _ dataset[cls_images[row]] axes[cls_idx, row].imshow(img) axes[cls_idx, row].axis(off) axes[cls_idx, 0].set_ylabel(dataset.classes[cls_idx], fontsize12) plt.tight_layout() plt.savefig(check_sample_grid.png, dpi120)重点看两类问题一是混入了完全不属于该类天气的图比如晴天傍晚被标成“沙尘暴”二是类别定义本身冲突比如“雨雾同时出现”到底归哪个类。这个抽检图我也会保留一份作为项目记录后面训练完回来复盘时能对比标注版本差异。3. 用预训练ResNet快速跑通基线最小训练脚本与超参数设定让1,000张数据发挥价值最快路径不是从零训练一个卷积网络而是拿在ImageNet上预训练好的模型做迁移学习。常见做法是把ResNet34的最后一层全连接从1000类换成4类然后整个网络微调。速度比从零训练快一个数量级准确率还能高十个百分点以上。3.1 为什么选ResNet而不是从零训练的卷积网络或Vision Transformer从零训练一个ResNet或者ViT在1,000张图上基本是灾难。机器学习的冷启动规律在小数据集上体现得最残酷没有预训练权重模型前几十个epoch都在学边缘和纹理四分类任务还没学到“雾是低对比度灰白色”这个层面就已经过拟合训练集了。ViT更挑数据1,000张连它的位置编码都喂不饱即便用预训练ViT微调效果也不会明显优于ResNet34反而推理和训练都更慢。所以这里的选型逻辑是ResNet34配合ImageNet预训练权重是“小样本四分类需要快速出基线”的最稳组合。ResNet18更轻适合测试环境ResNet50精度略高但小数据集上容易过拟合需要更重一点的数据增强来压制。我一般先跑ResNet34。3.2 最小训练代码加载、随机增强、SGD和早停这份代码可以直接存成train.py跑起来。为了让你能复现我把所有超参数集中在args里关键的改动我都加了注释。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models import numpy as np from sklearn.metrics import classification_report, confusion_matrix BATCH_SIZE 16 EPOCHS 30 LR 1e-3 WEIGHT_DECAY 1e-4 NUM_CLASSES 4 DEVICE cuda if torch.cuda.is_available() else cpu train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(dataset/train, transformtrain_transform) val_ds datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_ds, batch_sizeBATCH_SIZE, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_sizeBATCH_SIZE, shuffleFalse, num_workers4) model models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, NUM_CLASSES) model model.to(DEVICE) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lrLR, momentum0.9, weight_decayWEIGHT_DECAY) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) best_acc 0.0 for epoch in range(EPOCHS): model.train() train_loss_sum, train_correct 0.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_sum loss.item() * images.size(0) train_correct (outputs.argmax(1) labels).sum().item() model.eval() val_correct, val_loss_sum 0, 0.0 all_preds, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(DEVICE), labels.to(DEVICE) outputs model(images) loss criterion(outputs, labels) val_loss_sum loss.item() * images.size(0) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.cpu().numpy()) train_acc train_correct / len(train_ds) val_acc np.mean(np.array(all_preds) np.array(all_labels)) val_loss val_loss_sum / len(val_ds) scheduler.step(val_loss) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), weather_cls_best.pt) print(fEpoch {epoch1:02d} | train_acc {train_acc:.3f} | fval_acc {val_acc:.3f} | val_loss {val_loss:.3f}) print(Best val acc:, best_acc) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_namestrain_ds.classes, digits3))几个必须说清楚的参数选择批大小16不要用64。1,000张级别的数据大batch会加速过拟合小batch带来的梯度噪声反而有正则化效果。优化器用SGD而不是Adam。迁移学习微调阶段SGDmomentum的泛化能力通常比Adam好特别在样本量小的场景下差距更明显。如果换用AdamW学习率建议降到3e-4。ReduceLROnPlateau设patience3连续三个epoch验证损失不降就把学习率减半。这比固定epoch衰减更贴合训练动态。必须保存best_acc对应的权重而不是最后一轮的权重。小数据集的训练后期往往在震荡最后一轮通常不是最优的。3.3 评估指标看混淆矩阵而不是只看准确率四分类准确率90%听起来不错但要看具体错在哪。沙尘暴被误判成雾、暴雨被误判成雪这两种错误的业务代价完全不同。所以我在训练脚本最后打印了混淆矩阵和classification_report重点看每一类的precision和recall。如果某类的recall特别低说明这类被系统性误判需要回看训练集该类图片是否有标签噪声或者该类样本数过少。此时优先检查数据不要急着换模型。模型层面的调整建议放在第四章的数据增强和类别平衡之后再做。4. 把1,000张样本用到极限按天气特征定制的增强与交叉验证在1,000张图上数据增强不是“锦上添花”而是“救命稻草”。但增强不是越多越好而是要顺着四种天气的物理特征来设计。瞎用增强会引入和天气无关的伪规律比如把雾天图裁剪成局部特写模型学到的可能是一堵墙而不是雾。4.1 增强策略按恶劣天气视觉特征来定亮度、对比度、噪声和色偏四种天气在视觉上有清晰特征雾天整体低对比度、亮部发白暴雨的画面亮度和对比度变化剧烈暗部细节丢失沙尘暴图像整体偏黄饱和度偏低暴雪的画面有大面积高亮区域边缘纹理被雪覆盖。针对这些特征我把增强分成四组。train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter( brightness0.4, contrast0.4, saturation0.3, hue0.05, ), transforms.GaussianBlur(kernel_size3, sigma(0.1, 1.5)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])RandomResizedCrop(scale(0.7, 1.0))模拟摄像头抓拍时画面里天气区域的占比变化。不要用(0.08, 1.0)那种ImageNet默认值裁得太碎会把远处一座山当成天气特征。ColorJitter的brightness和contrast各给到0.4重点模拟暴雨天气下光线骤变和雾天能见度变化的场景。saturation给0.3、hue只给0.05。hue调太大会制造出物理上不存在的颜色比如把沙尘暴的黄色调成蓝绿色模型学到的是“偏色的图就是沙尘暴”这是要避开的坑。加了GaussianBlur模拟镜头上的水雾和沙尘颗粒。不需要每次生效PyTorch的GaussianBlur以50%概率随机生效。另外一个细节不要做垂直翻转。户外监控场景里天空永远在画面上方、地面在下方垂直翻转会把天空和地面的物理位置颠倒模型会学习“上暗下亮的是暴雨”这种错误规律。水平翻转不受影响因为从左来风还是从右来风不改变天气类别。如果1,000张里某一类特别少我还会用torchvision.transforms.RandomErasing做随机遮挡强迫模型不要依赖单一区域的特征。比如沙尘暴类只有180张模型很容易靠某张图右下角的黄沙色调取巧RandomErasing可以逼它看整个画面。4.2 类别不平衡加权采样与焦点损失二选一四类样本数量大概率不会完全相等。假如雾天300张、暴雨280张、沙尘暴200张、暴雪220张直接训练loss会被大头类别主导。最简单的解决办法是用WeightedRandomSampler让每个类别在每轮epoch中的采样次数相等。from torch.utils.data import WeightedRandomSampler labels np.array([s[1] for s in train_ds.samples]) class_counts np.bincount(labels) sample_weights 1.0 / class_counts[labels] weights torch.from_numpy(sample_weights.astype(np.float32)) sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) train_loader DataLoader(train_ds, batch_size16, samplersampler)1.0 / class_counts[labels]的含义是样本越多的类别每个样本被抽到的权重越小反向拉平各类别在训练中的出现次数。num_sampleslen(weights)表示每轮仍然抽1000次但每次抽取时按权重采样类别少的那一类会被重复抽到。注意这种采样会让模型看到重复样本配合上一节的增强一起用才有效果。如果你的项目里类别数量悬殊极大比如暴雨400张、沙尘暴100张加权采样还不够可以换Focal Loss。它的原理是让模型更关注难分类的样本但需要额外调节gamma参数默认gamma2比较合适。在小数据集上我通常先试加权采样效果不明显再上Focal Loss不做无谓的复杂度叠加。4.3 小样本评估5折交叉验证算可信度区间1,000张数据如果只划分一次train/val结果会强烈依赖随机划分。运气好点验证集准确率92%运气差点只有85%你很难判断这是数据问题还是模型问题。常见做法是直接做5折交叉验证用五轮结果的平均值和方差来说明模型稳定性。from sklearn.model_selection import StratifiedKFold train_ds datasets.ImageFolder(dataset, transformtrain_transform) kf StratifiedKFold(n_splits5, shuffleTrue, random_state42) samples np.array(train_ds.samples) fold_results [] for fold, (train_idx, val_idx) in enumerate(kf.split(samples, samples[:, 1])): fold_train torch.utils.data.Subset(train_ds, train_idx) fold_val torch.utils.data.Subset(train_ds, val_idx) # 在每一折中重新训练模型并记录验证准确率 # fold_results.append(val_acc)StratifiedKFold在划分时保留各类别的样本比例避免某一折里完全没分到沙尘暴图片导致评估失效。每一折都从头训练一次五折跑完耗时大约十五分钟但你能得到一个准确的估计如果所有折的准确率都在85%-88%之间波动说明模型稳定如果第3折突然掉到70%说明那一折的训练集里存在标签噪声或某类样本特别少值得翻回去查数据。最终的模型可以用全部数据重新训练一份验证集只是一个评估手段不是模型的一部分。交叉验证会暴露单个划分掩盖的过拟合问题这是小数据集项目最值得花的十五分钟。5. 避坑指南恶劣天气分类最容易翻车的5个细节与排查路径这部分是我实际做类似数据集项目时遇到的真实坑每一条都按“现象→原因→解决”写。别人踩过的坑你不用再踩一遍。5.1 误差不均衡混淆矩阵里“暴雨”列混进了大量“雾”图现象整体准确率82%但看混淆矩阵实际是雾天的图片里有12%被预测成暴雨而暴雨被预测成雾的只有3%。原因标注阶段把“起雾时下了小雨”的场景归入了暴雨类模型看到雾和雨同时出现的图片在两类之间左右摇摆。小数据集中10%左右的不一致标注就足以造成局部预测系统性偏移。解决翻出第二章保存的抽检九宫格图重点看雨雾两类把标注为暴雨但视觉上雾占主导的图重新归类如果标注标准实在难以统一干脆把这两类合并成“雾雨”类四分类改三分类。模型结构和代码都不用改直接改目录名即可。5.2 训练准确率99%验证集准确率只有72%典型的过拟合现象第10轮开始训练集准确率冲上99%验证集准确率卡在72%的水平再不动。原因模型容量太大或者增强强度不足。ResNet34在1,000张图上完全有能力“背下来”全部训练样本如果没有足够强的随机扰动模型直接在训练集上做记忆验证集自然拉胯。解决先增强RandomResizedCrop的scale下限到0.7打开GaussianBlur再给loss加label smoothingnn.CrossEntropyLoss(label_smoothing0.1)。PyTorch从1.10开始原生支持label_smoothing参数不用额外实现。如果整轮加强后验证集仍然在75%以下回查训练集是否混入了大量错标图因为增强只能压制过拟合救不了标签噪声。5.3 DataLoader读取图片报错中文文件名和EXIF方向现象ImageFolder在加载时偶发PIL.UnidentifiedImageError或者图片被旋转90度显示训练中途崩溃。原因原始数据集里文件名含中文或者拍摄设备在EXIF信息里写了旋转方向。Windows下中文路径被Python读取时编码不一致容易出现打不开文件的问题而手机拍摄的图片若不校正EXIF旋转模型看到的图像是横着的这属于与天气无关的伪特征。解决在第二章的统一转码环节就把文件名改成英文并且用ImageOps.exif_transpose处理方向。下面的代码可以在读取图片时自动校正EXIFfrom PIL import ImageOps with Image.open(path) as im: im ImageOps.exif_transpose(im) im im.convert(RGB)在datasets.ImageFolder里也可以写一个自定义loader传入替换默认的PIL读取逻辑。5.4 沙尘暴和雾天边界模糊类别定义本身就是噪声现象沙尘暴类的分类报告里精确率很高、召回率很低大量沙尘暴图片被预测成雾天。人工回看发现确实有一部分沙尘暴图片颜色偏灰白与浓雾照片几乎无法区分而黄色偏色明显的图模型分得很好。原因恶劣天气是连续光谱不是离散标签。轻度沙尘暴和中度雾的视觉效果重叠度很高图像分类模型对这个边界的处理能力有限这是任务本身的性质决定的不是模型调参能彻底解决的。解决第一选择是人为放宽类别定义强调沙尘暴必须是肉眼可见的“黄色调风沙纹理”把偏灰白的样本归为雾第二选择是训练一个辅助任务统计图像的R/B通道比值沙尘暴图像R通道明显高于B通道而雾天R/G/B三个通道接近。这个比值可以作为后处理规则当模型概率输出在两类中间时用颜色比值破平局。5.5 部署图片和训练图片风格不一致白平衡和HDR带来特征偏移现象用网络下载的公开图片训练模型在验证集上表现不错一部署到实际摄像头准确率掉了15%以上。原因训练集里的图片多数是摄影师拍的经过了白平衡校正、HDR调色甚至后期滤镜摄像头直出的原始图像色调偏灰、对比度低、动态范围宽。模型学到的“沙尘暴黄色高饱和”可能在摄像头画面中是“灰黄色低饱和”特征分布发生了偏移。解决收集目标场景的真实抓拍图哪怕是几百张也要加入训练集做finetune。如果暂时拿不到现场图先做一个预处理层对所有输入图执行灰度世界白平衡import numpy as np def gray_world_balance(img): img np.array(img).astype(np.float32) mean_r img[..., 0].mean() mean_g img[..., 1].mean() mean_b img[..., 2].mean() avg (mean_r mean_g mean_b) / 3.0 img[..., 0] * avg / mean_r img[..., 1] * avg / mean_g img[..., 2] * avg / mean_b return np.clip(img, 0, 255).astype(np.uint8)这个预处理能抹掉大部分由于不同设备白平衡算法造成的色偏差异让模型更关注天气的结构特征而不是绝对色调。6. 从四分类到多标签与检测迁移让这套数据发挥剩余价值在真实业务里“一张图只有一种天气”这个假设经常不成立。暴雨中裹着雾、沙尘暴来之前先起风扬尘这些场景都带有“兼类”属性。把四分类改造成多标签只需要改动两个地方损失函数从CrossEntropyLoss换成BCEWithLogitsLoss最后一层全连接保持4个神经元每个神经元独立输出一个0到1之间的概率。criterion nn.BCEWithLogitsLoss() # 每个样本的标签是长度为4的01向量例如 [1, 1, 0, 0] 表示既有雾又有雨 labels_multihot torch.tensor([[1.0 if fog in tags else 0.0 for tags in sample_tags]])多标签模型在推理时你可以遍历验证集找出最优概率阈值。比如当概率大于0.5算命中关闭模型已无法识别如果大于0.65才提醒人工抽检误报率会降低但漏报率上升按业务容忍度调即可。另外一个小技巧这1,000张已标注数据也可以作为更下游任务的数据基础。图像分类只是最浅层的用法再做目标检测时你可以用cvat或labelimg在原有图片上框出雨丝、雾区、沙尘区域把分类升级为检测。cvat适合团队协作labelme适合单人快速打点labelimg则是目标检测框标注的老牌选择三者导出的格式不一样后续要接YOLO系列的话labelimg的YOLO格式输出最省事。我个人的习惯是分类模型先上线留作备用同时用同批数据扩充检测样本这样一套恶劣天气数据集能支撑两条算法线。每个项目我都留一份增强后的可视化样本和混淆矩阵截图回看归档才能知道哪些天气类别在什么阶段被系统性地分错。希望帮到你。本文还有配套的精品资源点击获取
返回列表