ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

四类恶劣天气图像分类:数据集结构与ResNet训练指南

四类恶劣天气图像分类:数据集结构与ResNet训练指南 简介面向计算机视觉入门与天气识别任务的中型图像分类数据集覆盖大雾、暴雨、沙尘暴、暴雪四类真实恶劣天气场景约1000张图片均已完成类别标注类别编号可在JSON标签文件中直接查看。压缩包共1030个文件其中1028张jpg图片已按训练集、测试集分目录存放免去自行切分样本的环节可直接用于CNN等分类模型训练与评估另含1个Python可视化脚本和1个JSON标签文件脚本可帮助随机展示各类样本JSON则记录类别与文件的对应关系整体132.04MB结构清晰、下载后即可展开实验。已有667人学习下载适合需要规范数据进行天气分类练手、复现网络改进、完成课程设计或毕业设计的开发者、研究初学者。借助自带show脚本能快速核对标注质量与样本分布极大节省数据清洗时间帮助轻松搭建完整的气象图像分类流程。1. 恶劣天气图像分类数据集1000张四类标注够不够用做监控图像处理做久了会发现天气其实不是“环境变量”而是第一个要过的坎。雾天和沙尘暴天里同一个检测模型的表现能掉一大截这时候最直接的解决办法不是改网络而是先给图像做一个天气分类把雾、暴雨、沙尘暴、暴雪各自分流再走不同的预处理管线。这份恶劣天气图像分类数据集就是干这个用的约1000张已标注图片四个类别——雾、暴雨、沙尘暴、暴雪自带训练集和测试集划分还附带一个 show 脚本解压后跑一下就能预览每类样本。适合两类人来用一类是刚开始做图像分类想找一份干净、类别少、干扰小的数据集练手另一类是已经在做检测或分割的工程师拿来作为图片前置分流模块的数据基础。2. 数据集结构与标注逻辑从文件名前缀和 JSON 里读出真实类别2.1 目录到底长什么样拿到数据集解压后先别急着写训练代码花两分钟把目录结构摸清楚。摘要里写得很明确已经划分了训练集和测试集各自存放同一类数据图片。常见的组织方式是这样的weather_dataset/ ├── train/ │ ├── fog/ │ ├── rain_storm/ │ ├── sand_storm/ │ └── snow_storm/ ├── test/ │ ├── fog/ │ ├── rain_storm/ │ ├── sand_storm/ │ └── snow_storm/ └── annotation.jsontrain 和 test 是按类别目录分开存放的这种结构最大的好处是省事PyTorch 的torchvision.datasets.ImageFolder和 Keras 的flow_from_directory都能直接吃这种目录结构不需要额外写样本到标签的映射。你只需要确认一件事——解压后四个类别的文件夹名是不是和上面一致如果叫 mist、storm 这类缩写自己心里先记一下。2.2 JSON 标注文件类别和文件对应关系的唯一依据摘要里特意加了一句“具体查看 json 文件”这说明数据集里除了目录结构还提供了标注文件。一般这类数据集的 JSON 长这样顶层有两个关键字段一个叫categories一个叫images。# 先看 JSON 的顶层 key确认结构 import json with open(weather_dataset/annotation.json, r, encodingutf-8) as f: ann json.load(f) print(list(ann.keys())) print(ann[categories]) print(ann[images][:3])运行后一般能看到类别列表和图片条目。categories是类别定义列表每个元素里有id和namename就是“雾”“暴雨”“沙尘暴”“暴雪”或者对应的英文名。images是图片清单每条记录至少包含file_name和category_id有的还会附带宽高。这段代码的逻辑很简单先看顶层 key再打印类别定义最后打印前三条图片记录目的是快速判断 JSON 结构和预期是否一致。因为不同数据集作者的字段命名不完全统一有的用filename而不是file_name有的把类别信息嵌在路径里而不是 JSON 里先打印再解析能少踩很多坑。2.3 文件名前缀不等于类别a mist 和 foggy 可能是同一个类这次项目正文给了一组文件名示例值得掰开来看snow_storm-347.jpg snow_storm-060.jpg mist-101.jpg sand_storm-184.jpg mist-019.jpg sand_storm-115.jpg sand_storm_g2-992.jpg foggy-040.jpg snow_storm-018.jpg snow_storm-243.jpg注意看雾类出现了两种前缀mist和foggy它们都属于同一个“雾”类别只是采集时命名没统一。如果你拿到数据后直接按文件名前缀生成标签就会把雾类拆成两个类训练出来雾的分类里面全是乱的。我一般会先写一个统计脚本把前缀和 JSON 里的category_id比对一遍确认哪些前缀归哪个类别再决定标签策略。这种命名不统一的情况在真实数据集里非常常见尤其天气数据往往是多批次采集合并的文件名根本不能作为标签来源。唯一可信的标注来源是 JSON 文件里的category_id字段或者严格对应的目录名。对你来说第一步不是训练而是建立一个“文件名 → 类别”的映射表做出来之后所有后续工作才不容易翻车。2.4 约1000张数据对四分类意味着什么四分类、每类平均两百多张这个规模说大不大说小也够起步。训练集和测试集已经划分好了你不需要自己再做 split但有一个事必须做统计每个类别的数量分布。暴雨和沙尘暴这类天气采集难度不一样沙尘暴样本数明显少于雾天是正常的如果某类比平均数量少太多训练时就要考虑加权采样。最简单的方式是把训练集每个类别的图片数量列成一个表看一眼# 统计 train 目录下每个子文件夹的图片数量 for dir in train/*/; do count$(ls -1 $dir | wc -l) echo $dir : $count done这段命令会遍历 train 下的每个类别目录打印目录名和图片数量。注意for dir in train/*/最后那个斜杠只有匹配到目录才会进入循环避免把文件也统计进去。如果你用 Windows直接在资源管理器里看文件夹属性里的“包含文件数”也行效果一样。统计完你会发现类间数量差距明显这个观察结果直接决定第 4 章要不要做加权采样以及要不要用类别权重损失。3. 训练集/测试集划分与 show 脚本先把数据可视化再进入训练3.1 现成的划分为什么更省心这个数据集已经把训练集和测试集分开train 和 test 目录内各自按四个类别存放。这样做的好处不只是省去train_test_split那一步更重要的是它让数据分布变得更可控。天气图像本身受拍摄时间、地点、设备影响很大如果自己随机划分很容易让同一个时间段拍的雾天图片同时出现在训练集和测试集里导致验证结果虚高。数据集作者按目录划分至少保证了测试集与训练集在来源上有一定隔离。如果你想重新划分我建议保持按目录操作而不是按文件名操作。用sklearn的train_test_split时注意设置stratify参数按类别分层抽样否则随机划分在小样本上很容易出现某个类的训练集只有一百张、测试集却有五十张这种失衡情况。不过既然现成划分已经给出来了优先直接用等后面模型验证效果不满意再调整。3.2 show 脚本一句话跑起来看到四类样本摘要里明确提到资源里带了一个 show 脚本用来可视化数据集。解压后一般能找到一个 Python 脚本运行方式通常是python show.py --data_dir weather_dataset --show_num 5--data_dir指向数据集根目录--show_num控制每个类别显示几张。我手边没有项目里那个 show.py 的源文件但一般这种脚本内部就是读取 JSON 标注按类别抽几张图片用matplotlib拼一个网格出来。如果这个脚本报错多半是路径参数没对上。我一般也会自己写一个简化版逻辑更短更好排查# show_samples.py 简化版读取 JSON 并展示每个类别的样本 import json import random import matplotlib.pyplot as plt import matplotlib.image as mpimg def show_samples(json_path, root_dir, rows2, cols4): with open(json_path, r, encodingutf-8) as f: ann json.load(f) # 把图片按 category_id 分桶 buckets {} for item in ann[images]: cid item[category_id] buckets.setdefault(cid, []).append(item[file_name]) fig, axes plt.subplots(rows, cols, figsize(14, 7)) for idx, (cid, files) in enumerate(buckets.items()): sample_path f{root_dir}/{random.choice(files)} img mpimg.imread(sample_path) row, col divmod(idx, cols) axes[row][col].imshow(img) axes[row][col].set_title(ann[categories][cid][name]) axes[row][col].axis(off) plt.tight_layout() plt.show() show_samples(weather_dataset/annotation.json, weather_dataset)逻辑说明buckets以category_id为键把同一个类别的所有文件名收集到列表里遍历时用random.choice在每类里随机抽一张divmod(idx, cols)把序号转成画布上的行列位置。这样无论类别数量多少都能自动排版成网格。参数说明json_path是标注文件路径root_dir是图片根目录rows和cols决定画布多大多密。如果你有五个类别rows2, cols4就够用了因为二维画布能容纳rows * cols个子图超过也没关系会空着不显示。3.3 可视化检查清单哪些问题必须在训练前发现跑完可视化脚本别只看一眼“哦四类都有”就收工。天气识别这种任务图像特征不像猫狗那么直观容易出现三类问题第一类是错标比如小雨被归进雾类因为两种天气的视觉特征确实接近第二类是混入无关场景比如某些沙尘暴图片里出现了明显的建筑特写这类样本会让模型学到背景特征而不是天气特征第三类是光照风格差异太大同属雾天有的图是清晨薄雾有的是浓雾风格差异会影响模型收敛。检查的时候重点看每个类别的“边界样本”也就是你第一眼拿不准应该归哪一类的图片。这些样本不是数据集的错误它们恰恰是天然存在的类间模糊地带四分类本身就是一种粗糙划分雾和暴雨在视觉上确实存在过渡。看到这类样本你不用惊讶只需要记住它们是正常存在的问题只在于你希望模型怎么处理它们——是强行二选一还是允许一定程度的不确定性。4. 分类网络选型与训练参数预训练权重和增强策略怎么搭4.1 模型选型1000张数据不配用 Transformer四分类、每类两百多张这个数据量决定了模型选择的空间有限。Vit 类模型在小数据集上的表现并不比 CNN 好除非用大规模预训练权重而大部分从业者手头没有那套计算资源。我推荐从卷积网络起步首选 ResNet 系列。模型参数量单张推理耗时CPU约适合场景ResNet18约 11M20ms 级别通用首选精度与速度均衡ResNet34约 21M30ms 级别数据量稍充足时可尝试MobileNetV3-Small约 2.5M5ms 级别边缘设备部署EfficientNet-B0约 5.3M12ms 级别精度优先且算力有限ResNet18 是最稳妥的选择结构简单预训练权重好找1000张数据用它训练甚至不需要太长的调参周期。用 EfficientNet 也可以但它的缩放策略在小数据集上不一定发挥得出来。MobileNet 留给后期做部署时再考虑前期调通流程用 ResNet18 最省心。4.2 超参数配置一组能直接开跑的参数基于四分类和每类两百多张的规模我通常从这组参数起步EPOCHS 50 BATCH_SIZE 32 INIT_LR 1e-3 WEIGHT_DECAY 1e-4 STEP_SIZE 30 GAMMA 0.1逻辑说明EPOCHS取 50 而不是 200因为小数据集上模型在 30 到 40 轮左右就会收敛再多轮次只会放大过拟合风险BATCH_SIZE取 32对显存要求低INIT_LR用 1e-3配合预训练权重这个学习率属于偏大但可以让收敛更快配合后续衰减来抑制过拟合。STEP_SIZE30表示在第 30 个 epoch 时把学习率乘以GAMMA0.1从 1e-3 降到 1e-4 做精细收敛。优化器选 Adam配合WEIGHT_DECAY1e-4做 L2 正则。千万不要在这个数据量下用从头训练的 SGD 加动量收敛太慢而且对学习率的敏感度高。如果训练过程中验证损失在第 20 轮左右就回升把STEP_SIZE改成 15 或 20让学习率更早衰减。4.3 数据增强针对天气场景的增强不是随机裁剪那么简单暴雨和雾天的图像普遍对比度低沙尘暴图像有色调偏移暴雪图像有大面积白色区域。这些特征决定了通用数据增强套件需要一个重要的调整把颜色抖动加进来。from torchvision import transforms transforms_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])逻辑说明RandomResizedCrop的scale(0.8, 1.0)限制了裁剪尺度变化范围不会裁出太小的局部区域——对天气识别来说过小的裁剪容易丢掉全局天气信息ColorJitter的三个参数分别控制亮度、对比度、饱和度扰动对比度扰动对雾天和暴雨图片尤其关键因为这类图像对比度本身偏低模型容易依赖对比度特征做判断加入扰动可以防止模型学到“低对比度 雾”这种过于简单的映射。验证集不要用增强只做Resize、CenterCrop和Normalize保证评估结果和训练数据分布一致。测试集同理。注意Normalize的均值方差直接用 ImageNet 标准值即可因为用的是 ImageNet 预训练权重输入分布必须和预训练分布对齐不然前面训练好的浅层特征就白预训练了。训练过程中的实时监控也很重要。每完成一个 epoch记录训练准确率和验证准确率如果训练准确率已经超过 95% 而验证准确率还在 80% 徘徊说明模型开始背书了这时候看第 5 章里过拟合的处理方式。5. 常见问题与排查五条真实踩坑记录5.1 标签组织上的两个坑坑一把文件名前缀当成类别标签导致类别数量膨胀现象直接用snow_storm、mist、sand_storm、foggy做标签模型训练时准确率看起来不错但输出层类别数变成 5 个实际只应该有 4 个。原因雾类图片在采集时用了mist和foggy两个前缀暴雪用了snow_storm沙尘暴用了sand_storm前缀比真实类别多。文件名前缀只是采集时的标记不是标注。解决以 JSON 文件的category_id为准。如果 JSON 里已经把 foggy 和 mist 归到同一个类直接读 JSON如果 JSON 里类别字段也有多种写法就自己写个映射表把同义词统一成四个标准类别。建议训练前输出一份标签统计表看到 “5 classes” 这类结果就说明前缀映射出问题了。坑二类别数量不均衡导致模型偏向多数类现象训练完以后验证集上雾类的 F1 值明显高于沙尘暴而且沙尘暴的测试图像经常被误判成暴雪或雾。原因雾、暴雨样本数量多模型对它们的特征建模更充分沙尘暴样本数量少模型倾向于把不熟悉的样本推向先验概率高的类。解决先统计训练集中各类别的样本数如果沙尘暴确实明显少于其他三类用WeightedRandomSampler做采样加权让每个 epoch 里每个类被抽到的期望次数接近。下面的代码展示了采样器的常见用法from torch.utils.data import WeightedRandomSampler # labels 是训练集每个样本的类别 id长度等于样本总数 label_counts torch.bincount(torch.tensor(labels)) weights 1.0 / label_counts.float() sample_weights weights[labels] sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(sample_weights), replacementTrue )逻辑说明label_counts统计每个类别出现次数取倒数作为权重样本少的类别权重更大sample_weights是按每个样本的标签查表得到的权重数组replacementTrue允许同一张图在一个 epoch 里被重复采样从而让少样本类别在多轮迭代里被反复“复习”。注意num_samples设置为训练集样本总数即可不设过大会拖慢 epoch 速度。5.2 训练过程里的三个坑坑三过拟合训练准确率95%但验证集只有70%现象前 15 个 epoch 训练和验证准确率同步上涨到第 20 轮之后训练准确率继续爬升验证准确率开始掉头向下。原因1000 张数据对四分类来说还是偏少模型参数量超过数据承载能力浅层学到普适特征之后深层开始记忆训练集里的具体图像噪声。解决第一优先是换用 ImageNet 预训练权重并冻结前几层只微调最后几层这样可以显著减少需要学习的参数数量。其次把增强强度往上调RandomResizedCrop的 scale 下限从 0.8 降到 0.6ColorJitter的 brightness 加到 0.4。第三步是在模型结构上做减法比如把 ResNet18 最后的全连接层从 512 维降到 128 维减少分类头的拟合能力。坑四雾类和暴雨类交叉误判严重现象混淆矩阵显示雾类有 15% 到 20% 的样本被预测成暴雨暴雨类也有相似比例的样本被预测成雾。原因两类天气在视觉上有真实的重叠区域——薄雾、小雨、雨后水汽边界本身就是模糊的。加上 224×224 的输入分辨率下降后远处背景纹理细节丢失模型更分不清。解决这类混淆属于数据本身的固有噪声先确认标注本身没有错标如果标注正确就接受这个混淆程度。想要压低混淆率可以尝试把输入分辨率从 224 提到 320代价是训练速度下降明显也可以把问题从四分类改造成“先二分类低可见度 vs 高可见度再细分”的层级结构让模型先学习大气状况再学降水形态但这会引入额外工程复杂度数据量有限时不建议新手一上来就做。坑五show 脚本在 Windows 下报路径错误现象解压后运行python show.py直接报FileNotFoundError或者OSError: image file is truncated。原因Windows 的路径分隔符是反斜杠而 JSON 里的file_name可能用的是斜杠另外部分图片文件本身可能是通过压缩包转换导致文件头损坏。解决脚本里所有路径拼接统一用os.path.join不要手写字符串拼接。遇到image file is truncated时用 PIL 打开图片并重新保存能修复大部分损坏的 JPEG 文件头。修复脚本可以这么写from PIL import Image import os def repair_images(root_dir): for path in os.listdir(root_dir): full os.path.join(root_dir, path) if not full.lower().endswith(.jpg): continue try: img Image.open(full) img.load() except Exception as e: print(frepairing {full}: {e}) # 忽略损坏头并重新保存保持原尺寸 with open(full, rb) as f: data f.read() # 找到 JPEG 数据实际起点 start data.find(b\xff\xd8) if start -1: continue Image.open(io.BytesIO(data[start:])).save(full)逻辑说明img.load()会真的读到像素数据如果文件头有损坏这一句就会抛异常捕获异常后按字节读取原文件找到 JPEG 的起始标记\xff\xd8截掉前面多余内容后重新保存。这个操作对带有缩略图或尾部附加数据的 JPEG 也有效。注意这张图如果本身后半段字节全丢了重新保存后照样打不开那就只能删除或重新从源头获取。6. 验证环节再拷打混淆矩阵和 top-K 错误样本定位模型短板训练完模型验证准确率是 85% 还是 90%这只是第一层信息。真正决定模型能不能部署到业务里的是错误结构——错在哪些类、错得有没有规律。天气识别尤其要看这个因为不同错误类型的代价完全不一样。把暴雨预测成雾和把暴雪预测成沙尘暴前者可能只是预处理流程切换错误后者会让后续的图像处理管线完全失效。from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import numpy as np def plot_confusion_matrix(cm, classes, figsize(6, 5)): fig, ax plt.subplots(figsizefigsize) im ax.imshow(cm, interpolationnearest, cmapplt.cm.Blues) ax.set_xticks(range(len(classes)), classes, rotation45) ax.set_yticks(range(len(classes)), classes) thresh cm.max() / 2. for i in range(cm.shape[0]): for j in range(cm.shape[1]): ax.text(j, i, format(cm[i, j], d), hacenter, vacenter, colorwhite if cm[i, j] thresh else black) ax.set_ylabel(True Label) ax.set_xlabel(Predicted Label) plt.tight_layout() plt.show() def get_top_misclassified(model, loader, k10): model.eval() wrong [] for imgs, labels in loader: preds model(imgs).argmax(dim1) for i, (pred, label) in enumerate(zip(preds, labels)): if pred ! label: wrong.append((pred.item(), label.item(), imgs[i])) return wrong[:k]逻辑说明plot_confusion_matrix里thresh cm.max() / 2用来控制格子内文字颜色超过半最大值的格子用白色文字否则用黑色保证可读性get_top_misclassified遍历整个测试集收集预测和真实标签不一致的样本返回前 k 个。拿到混淆矩阵后回答三个问题哪两个类的混淆最严重最严重的那对混淆里A 错成 B 多还是 B 错成 A 多错误样本里有没有统一的视觉特征——比如颜色偏亮、饱和度偏低、或者有遮挡物这三个问题会指向三个不同的调整方向混淆严重的类对要考虑数据增强或者类别层级结构调整单向混淆偏多说明两个类的特征区分度不够统一视觉特征则说明训练集缺了这一类场景的样本。回答完这三个问题模型还有没有提升空间、该往哪个方向提升心里就有数了。从那以后我每次拿到新数据集都强制自己走一遍准确率 混淆矩阵 top-K 错误样本分析多花十分钟但能省下后面整整一周的盲目调参希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进