简介:面向计算机视觉入门与天气识别任务的中型图像分类数据集,覆盖大雾、暴雨、沙尘暴、暴雪四类真实恶劣天气场景,约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", encoding="utf-8") as f: ann = json.load(f) print(list(ann.keys())) print(ann["categories"]) print(ann["images"][:3])运行后一般能看到类别列表和图片条目。categories是类别定义列表,每个元素里有id和name,name就是“雾”“暴雨”“沙尘暴”“暴雪”或者对应的英文名。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, rows=2, cols=4): with open(json_path, "r", encoding="utf-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决定画布多大多密。如果你有五个类别,rows=2, cols=4就够用了,因为二维画布能容纳rows * cols个子图,超过也没关系,会空着不显示。
3.3 可视化检查清单:哪些问题必须在训练前发现
跑完可视化脚本,别只看一眼“哦,四类都有”就收工。天气识别这种任务,图像特征不像猫狗那么直观,容易出现三类问题:第一类是错标,比如小雨被归进雾类,因为两种天气的视觉特征确实接近;第二类是混入无关场景,比如某些沙尘暴图片里出现了明显的建筑特写,这类样本会让模型学到背景特征而不是天气特征;第三类是光照风格差异太大,同属雾天,有的图是清晨薄雾,有的是浓雾,风格差异会影响模型收敛。
检查的时候重点看每个类别的“边界样本”,也就是你第一眼拿不准应该归哪一类的图片。这些样本不是数据集的错误,它们恰恰是天然存在的类间模糊地带,四分类本身就是一种粗糙划分,雾和暴雨在视觉上确实存在过渡。看到这类样本你不用惊讶,只需要记住:它们是正常存在的,问题只在于你希望模型怎么处理它们——是强行二选一,还是允许一定程度的不确定性。
4. 分类网络选型与训练参数:预训练权重和增强策略怎么搭
4.1 模型选型:1000张数据不配用 Transformer
四分类、每类两百多张,这个数据量决定了模型选择的空间有限。Vit 类模型在小数据集上的表现并不比 CNN 好,除非用大规模预训练权重,而大部分从业者手头没有那套计算资源。我推荐从卷积网络起步,首选 ResNet 系列。
| 模型 | 参数量 | 单张推理耗时(CPU,约) | 适合场景 |
|---|---|---|---|
| ResNet18 | 约 11M | 20ms 级别 | 通用首选,精度与速度均衡 |
| ResNet34 | 约 21M | 30ms 级别 | 数据量稍充足时可尝试 |
| MobileNetV3-Small | 约 2.5M | 5ms 级别 | 边缘设备部署 |
| EfficientNet-B0 | 约 5.3M | 12ms 级别 | 精度优先且算力有限 |
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_SIZE=30表示在第 30 个 epoch 时把学习率乘以GAMMA=0.1,从 1e-3 降到 1e-4 做精细收敛。
优化器选 Adam,配合WEIGHT_DECAY=1e-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(brightness=0.3, contrast=0.3, saturation=0.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( weights=sample_weights, num_samples=len(sample_weights), replacement=True )逻辑说明:label_counts统计每个类别出现次数,取倒数作为权重,样本少的类别权重更大;sample_weights是按每个样本的标签查表得到的权重数组;replacement=True允许同一张图在一个 epoch 里被重复采样,从而让少样本类别在多轮迭代里被反复“复习”。注意num_samples设置为训练集样本总数即可,不设过大会拖慢 epoch 速度。
5.2 训练过程里的三个坑
坑三:过拟合,训练准确率95%但验证集只有70%
现象:前 15 个 epoch 训练和验证准确率同步上涨,到第 20 轮之后训练准确率继续爬升,验证准确率开始掉头向下。
原因:1000 张数据对四分类来说还是偏少,模型参数量超过数据承载能力,浅层学到普适特征之后,深层开始记忆训练集里的具体图像噪声。
解决:第一优先是换用 ImageNet 预训练权重并冻结前几层,只微调最后几层,这样可以显著减少需要学习的参数数量。其次把增强强度往上调,RandomResizedCrop的 scale 下限从 0.8 降到 0.6,ColorJitter的 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(f"repairing {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(figsize=figsize) im = ax.imshow(cm, interpolation="nearest", cmap=plt.cm.Blues) ax.set_xticks(range(len(classes)), classes, rotation=45) 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"), ha="center", va="center", color="white" 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, k=10): model.eval() wrong = [] for imgs, labels in loader: preds = model(imgs).argmax(dim=1) 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 错误样本分析,多花十分钟,但能省下后面整整一周的盲目调参,希望帮到你。
本文还有配套的精品资源,点击获取