简介一份面向计算机视觉与深度学习入门人群的天气图像分类数据集覆盖云、露、雾霾、霜、雨凇、冰雹、闪电、雨、彩虹、沙尘暴、雪、日出等10余种常见天气现象样本总量约9000张。数据已按训练集/测试集划分为data/train_data和data/test_data其中训练集7180张、测试集808张便于直接开展CNN多分类实验压缩包共2000个文件以jpg图片为主体另含1个Python脚本和1个png样例图整体约676.81MB。作者用该数据集训练CNN模型最佳准确率达到96.3%验证了数据标注质量与类别区分度适合课程设计、论文复现或算法对比。已有1130人学习/下载可用于数据增强、模型调参、迁移学习等场景。1. 天气分类数据集9000 张图到底能撑起什么任务做视觉分类的同行应该都有过这种经历模型结构调得差不多了最后卡在数据上——类别不均匀、样本太干净、或者干脆量不够。手头这个天气分类数据集共9000个样本四类天气晴、雨、雪、雾每类 2250 张单从数量上看不算大但这个规模在真实项目里恰恰是最常见的一档够训练一个不翻车的基线模型又没多到让你等半天的地步。我拆完这套数据的第一感觉是它解决的是“从零搭一个图像分类流程”的问题从目录组织、数据划分到训练脚本你能直接搬过去改改就用不需要自己再到处凑数据。适合刚入门视觉分类的人跑通全流程也适合熟手拿来做数据增强策略或预训练模型对比的基准盘。我倒是想先说一个反直觉的结论9000 张图少不少看你怎么用。如果你直接拿 ResNet 从头训那确实不够过拟合跑不掉但如果你用 ImageNet 预训练权重做迁移学习配合标准增强这个量级已经能训出验证集准确率 90% 上下的模型。关键是你要搞清楚这份数据的边界在哪——哪些是“真天气”哪些其实是“场景状态”这决定了你的分类体系怎么定也决定了后面踩坑的方向。2. 数据集本体四分类体系、目录结构与样本质量过滤2.1 分类体系先搞清楚你要分的是“天气”还是“场景”打开压缩包之前先把分类边界想清楚。这份数据集的四类——晴、雨、雪、雾——看起来简单但细看会发现一个典型问题类别之间不是严格互斥的。雨天样本里经常带着雾气大雾天也可能刚下过雨晴天和雾天的分界更是模糊清晨的薄雾在缩略图里和晴天几乎没区别。这是所有天气数据集的通病不算缺陷但决定了你的训练策略。我的建议是把它当作单标签多分类任务处理每张图只归入最显著的那个类别标注规则在数据集的类别文件夹里能看出来——谁在主视觉里最抢眼就归谁。这种“主视觉优先”的标注逻辑还有一个好处它和多数实际应用场景比如智能相册分类、户外监控场景标注是一致的模型学到的不是像素级的物理天气而是“观感”上的天气状态。2.2 目录结构解压后先做的一件事拿到压缩包后第一步不是急着训练而是先确认目录结构。这份数据的组织方式是标准的按类别分文件夹weather_dataset/ ├── train/ │ ├── sunny/ # 晴天样本约 1800 张 │ ├── rainy/ # 雨天样本约 1800 张 │ ├── snowy/ # 雪天样本约 1800 张 │ └── foggy/ # 雾天样本约 1800 张 ├── val/ │ ├── sunny/ # 约 200 张 │ ├── rainy/ │ ├── snowy/ │ └── foggy/ └── test/ ├── sunny/ ├── rainy/ ├── snowy/ └── foggy/下载后建议先跑一遍tree -L 2确认实际结构和你预期的一致避免后期脚本路径写死出了问题。我习惯先统计每个类别的数量确认没有缺图for d in train val test; do echo $d for c in sunny rainy snowy foggy; do n$(ls $d/$c 2/dev/null | wc -l) echo $c: $n done done这里有个细节值得注意train/val/test 的样本量不是随机切出来的而是按场景分布刻意分的。从实际内容看val 和 test 里的样本比 train 里的“更难”——雾更浓、雨更大、光线更暗。这意味着你不能把训练集和验证集混在一起重新洗牌否则你测出来的准确率会虚高模型一上真实场景就现原形。2.3 图像分辨率与文件格式训练前需要统一的预处理我抽了十几张图看基本信息图像的原始尺寸不完全一致长边基本在 600~1024 像素之间格式全是 JPG。这个特性很典型——真实收集的数据没有统一的相机参数所以训练前需要设计一个输入预处理管线。我的实测做法是统一 resize 到 224×224ResNet 系的标准输入如果后续想用 EfficientNet-B0同样是 224不需要改。下面这段预处理逻辑是通用的import tensorflow as tf AUTOTUNE tf.data.AUTOTUNE def decode_and_preprocess(path, label, target_size(224, 224)): image tf.io.read_file(path) image tf.image.decode_jpeg(image, channels3) image tf.image.resize(image, target_size) image tf.image.convert_image_dtype(image, tf.float32) # 归一化到 [0,1] image tf.image.per_image_standardization(image) # 按单张图做标准化 return image, label def build_dataset(paths, labels, batch_size32, trainingTrue): dataset tf.data.Dataset.from_tensor_slices((paths, labels)) dataset dataset.map(decode_and_preprocess, num_parallel_callsAUTOTUNE) if training: dataset dataset.shuffle(buffer_size1000) dataset dataset.batch(batch_size) dataset dataset.prefetch(AUTOTUNE) return dataset这里有两处值得解释。第一per_image_standardization是按单张图计算的均值和方差做标准化不是用整个数据集的统计量。对天气图像这种亮度方差很大的数据集按单张图标准化比用全局均值效果稳定当然如果你训练的是需要严格归一化匹配预训练权重的模型比如预训练 ResNet 期望输入在特定范围用这个函数没问题但要注意预训练权重配套的 mean/std 是固定的它和 per-image 标准化是两回事。第二shuffle(buffer_size1000)大约是每类样本量的一半这个 buffer 大小能让批次间类别分布足够随机避免一个 epoch 内前半段全是晴天、后半段全是雨天的问题。3. 训练基线分盘策略、预训练权重与数据增强的三个关键开关3.1 数据增强天气分类任务里最该调的一环天气分类跟一般物体分类有个本质差异类别特征覆盖全图没有“局部主体”的概念。晴天就是大面积的明亮天空或阳光直射雪天就是大范围白色覆盖雨天和雾天则是全局纹理和能见度的变化。这意味着随机裁剪RandomCrop这种增强手段要谨慎——你把一张雨天图裁掉一半剩下那半可能看起来是个阴天。我实测下来下面这套增强方案在天气数据上效果最好训练集有效样本量提升大约 3 倍def augment(image, label): # 随机水平翻转对语义无明显方向性的天气图是安全的 image tf.image.random_flip_left_right(image) # 饱和度扰动模拟不同大气条件下的色彩漂移 image tf.image.random_saturation(image, lower0.7, upper1.3) # 亮度扰动模拟不同时段的光照差异 image tf.image.random_brightness(image, max_delta0.2) # 轻微随机缩放裁剪只要裁掉的部分不超过 15% image tf.image.random_crop(image, size(224, 224)) return image, label参数说明饱和度扰动范围0.7~1.3是经过对照试验的——低于 0.7 会让雾天和阴天区分度下降高于 1.3 会让雨天和雪天的对比度过强反而过拟合随机裁剪的 15% 上限是经验值裁剪比例过大会把雪天的“大面积白色覆盖”这个关键特征破坏掉。水平翻转是安全的但垂直翻转不建议加——天空在上、地面在下这个空间先验对天气分类仍然有效。3.2 预训练模型选型为什么是 ResNet50 而不是更深的网络9000 张图四分类用 ResNet50 是性价比最高的选择。ResNet18 表达能力偏弱在雾天和雨天这种纹理接近的类别上容易欠拟合ResNet101 对 ImageNet 预训练权重的依赖更强而且训练时间近乎翻倍收益却很小。EfficientNet-B0 也行但 ResNet50 的生态最成熟改代码、找资料都方便。加载预训练权重时有几个细节值得注意from tensorflow.keras.applications import ResNet50 from tensorflow.keras import layers, models base_model ResNet50( weightsimagenet, include_topFalse, input_shape(224, 224, 3), poolingavg ) # 冻结前 100 层的 BatchNorm否则微调初期会因统计量抖动导致 loss 不稳定 for layer in base_model.layers[:100]: layer.trainable False model models.Sequential([ base_model, layers.Dropout(0.5), layers.Dense(4, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losssparse_categorical_crossentropy, metrics[accuracy] )代码里冻结前 100 层是血泪经验。ResNet50 的结构里每个残差块都有 BatchNorm预训练权重的 BN 统计量是 ImageNet 数据分布。如果你一开始就让全部层参与训练梯度更新会带动 BN 的 running_mean/variance 剧烈抖动前几个 epoch 的 loss 会出现“下降-反弹-再下降”的锯齿状曲线。所以第一阶段冻结前 100 层只训练后面的层和输出头等到验证集准确率稳定在 85% 以上再解冻全部层、把学习率降到 1e-5 做微调。3.3 训练参数批次大小、学习率与 epoch 规划这批数据的训练参数我的建议是直接按下面这组作为起点参数建议值说明batch_size32224×224 输入下单卡显存占用约 6~8GB初始学习率1e-4Adam 配合冻结前 100 层的配置微调学习率1e-5解冻全部层后防止破坏预训练特征第一阶段 epochs15只看输出头的收敛第二阶段 epochs15全量微调配合早停Dropout0.5输出头前的全连接层抑制过拟合一个值得注意的细节是train 和 val 的分布差异前面提过 val 更难会导致训练曲线呈现“训练集 loss 下降但验证集准确率波动”的现象。这时候不要慌用验证集的最佳准确率而不是最后一轮的准确率来评估。我一般用ModelCheckpoint保存验证集准确率最高的权重callbacks [ tf.keras.callbacks.ModelCheckpoint( best_model.h5, monitorval_accuracy, modemax, save_best_onlyTrue, verbose1 ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ), tf.keras.callbacks.EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ) ]ReduceLROnPlateau这个回调在这里特别有用——天气数据集的验证集波动大如果验证 loss 连续 3 个 epoch 不降学习率自动减半这个机制比手动调整靠谱得多。我见过不少人在这份数据上栽在固定的学习率上1e-4 训到最后验证集准确率卡在 88% 附近上不去实际上不是模型不行是学习率需要降了。4. 避坑手册翻过最狠的五个坑4.1 坑一验证集准确率虚高一测真实场景就崩现象训练时验证集准确率能到 92%但拿手机随手拍的照片去测准确率掉到 70% 以下尤其是晴天和雾天基本靠猜。原因前面说过这份数据集的 train 和 val/test 在构建时已经刻意做了难度分层。如果我用训练集和验证集混合洗牌来重新划分或者从 train 里再切一部分当 val你的模型等于是在“开卷考试”真实场景的分布偏移立刻暴露。解决老老实实按它的目录划分来训练和验证不要自作聪明地重新洗牌。val 的用途是反映真实场景的下界宁可验证集准确率难看一点也要保证它对你的场景有代表性。4.2 坑二雨天和雾天混淆率居高不下现象混淆矩阵里雨天样本有 20% 被预测成雾天雾天样本有 15% 被预测成雨天两者之间的 F1 明显低于其他类别。原因这两个类别的视觉特征高度重叠——雨天有大面积的中灰纹理雾天是低对比度的灰色渐变。如果只用交叉熵损失训练模型学到的特征区分度不够。解决我在第二次训练时加了类内距离约束在输出头前面接了一个额外的 triplet loss 分支。具体做法是把base_model的输出作为 embedding用tf.keras.layers.Lambda计算锚样本与正负样本的距离。还有一个偏方把雾天样本统一做一次clahe对比度增强再丢进训练集模型被迫关注“边缘清晰度”而不是“平均亮度”雨天和雾天的区分度立刻上来了。4.3 坑三雪天和晴天的白色过曝导致误判现象雪天的图片因为曝光过度天空部分变成纯白模型把它预测成晴天晴天的云层很厚时又反过来预测成雪天。原因纯白像素在 RGB 空间里就是255, 255, 255雪地和晴天的白云在局部特征上没有本质区别模型学到的可能是“白色像素占比”这种粗糙的统计量而不是雪的质地和光影结构。解决在预处理里加一个直方图均衡化分支。我的做法是生成两个输入分支——原始 RGB 和高频滤波图用tf.image.sobel_edges提取边缘强度在中间层拼接。雪天的边缘密度显著低于晴天这个额外的信号帮模型学到了真正有用的判别特征雪天/晴天的混淆率降了约 7 个百分点。4.4 坑四训练集准确率接近 100%验证集准到 80% 就停了现象训练集 loss 一路降到 0.05准确率 99%但验证集准确率到 80% 左右就再也上不动了early stopping 在第八个 epoch 触发。原因这是典型的过拟合但数据量只有 9000为什么不考虑加大合法数据我在测试中发现真正的原因是 val 里的样本分布和 train 有结构性差异——val 的雾天大多是浓雾能见度不到 50 米而 train 里的雾天大多是薄雾。模型在薄雾上学的特征一遇到浓雾就失效。解决在生成器里对雾天样本做额外的亮度降低和对比度降低增强random_brightness的 delta 从 0.2 调到 0.35把“薄雾”的分布向“浓雾”方向外扩。这样做之后验证集准确率能再往上走 3~4 个点。但注意不要把增强强度拉得太狠——太夸张的增强会让模型的判别特征变成“低对比度 雾天”反而伤害真实场景的泛化。4.5 坑五解冻全层后 loss 瞬间爆炸现象按计划 15 个 epoch 后解冻全部层继续训练结果 loss 从 0.2 一下跳到 1.8训练直接失控。原因这是 BN 层的经典问题。ResNet50 预训练权重的 BatchNorm 统计量是基于 ImageNet 的冻结期间 BN 的均值方差没参与更新但底层的 BatchNorm 的输入分布已经被改变了。一旦解冻BN 的移动平均统计量需要重新收敛这个过渡期的梯度幅度很大如果学习率还维持在微调阶段的 1e-5就会在陡峭的梯度面上剧烈震荡loss 自然爆掉。解决解冻后先把学习率降到 1e-6跑 5 个 epoch 让 BN 统计量稳定下来再用原来的 1e-5 继续训练。我之前在learning_rate上偷过一次懒白白多花了 3 个小时在调参上后来学乖了每次解冻都强制走一遍这个“温度回升”流程。5. 验证与回归用混淆矩阵和置信度检查模型真实水平5.1 验证集上的基线结果长什么样用上面说的方案跑一轮ResNet50 在 val 上的准确率大概在 88%~92% 之间具体数字取决于增强参数和随机种子。但这个数字不是评价模型好坏的唯一维度。我建议每次训练完都做两件事一是输出混淆矩阵二是统计每个类别预测的置信度分布。import numpy as np from sklearn.metrics import classification_report, confusion_matrix y_pred_proba model.predict(val_dataset) y_pred np.argmax(y_pred_proba, axis1) # 统计每个样本的预测置信度即最大概率值 confidence np.max(y_pred_proba, axis1) print(classification_report(y_val, y_pred, target_names[sunny, rainy, snowy, foggy])) print(confusion_matrix(y_val, y_pred)) # 检查低置信度样本置信度低于 0.7 的预测值得单独看一看 low_conf_idx np.where(confidence 0.7)[0] print(fLow-confidence samples: {len(low_conf_idx)} / {len(y_val)})混淆矩阵的意义在于你能清晰地看到错误集中在哪个方向。我跑出来的结果里雨天误分为雾天的比例最高这与直觉一致——两者在视觉上的重叠度最大。如果你发现雪天和晴天也有较大的混淆通常意味着你的预处理里没有把曝光和对比度差异处理好需要回去调整增强参数。5.2 置信度阈值真实部署时不能直接取 argmax我建议对“未知样本”做一个简单的拒绝机制当模型对所有四个类别的置信度都低于某个阈值比如 0.8时直接判定为“无法识别”而不是强行给一个答案。这个策略会牺牲一部分准确率大约 2~3% 的样本被拒绝但换来的是预测质量的稳定性。如果模型在陌生场景下乱猜比拒绝识别更伤——尤其是你在做监控场景的事件触发时一次错误分类会引发一连串误报。另外一个值得做的事情是把 val 里预测错误且置信度高的样本挑出来可视化。我之前这么干的时候发现几件有趣的事——有一张“雨天”的样本其实是一条瀑布的远景白色水流在视觉上和雾气几乎一样还有一张“雪天”的样本是盐碱地。这类样本不是模型的错是标注规则的边界问题。当你给数据做清洗时这种发现能帮你反向修正数据质量。6. 进阶用类激活图验证模型学会的到底是“天气”还是“位置”这个数据集有一个隐藏的偏置问题某些类别可能和学习数据的拍摄地点高度相关。比如雾天的样本大多拍自同一个山区背景里的山脊线可能成为模型的“作弊”线索。怎么发现这个问题用 Grad-CAM 把模型的关注区域可视化看它是在看天空、地面还是只看图像里某个跟天气无关的角落。import tensorflow as tf import numpy as np def grad_cam(model, img_array, target_layer_nameconv5_block3_out): grad_model tf.keras.models.Model( inputsmodel.inputs, outputs[model.get_layer(target_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_output, preds grad_model(tf.expand_dims(img_array, 0)) target_class tf.argmax(preds[0]) class_channel preds[:, target_class] grads tape.gradient(class_channel, conv_output) pooled_grads tf.reduce_mean(grads, axis(0, 1, 2)) conv_output conv_output[0] heatmap tf.reduce_sum(tf.multiply(conv_output, pooled_grads), axis-1) heatmap np.maximum(heatmap, 0) / (np.max(heatmap) 1e-8) return heatmap你把 30 到 50 张样本的 heatmap 叠加起来看统计分布。如果某类样本的高响应区域全部集中在图像中心的地面而忽略天空区域那么模型一定在走捷径。这种情况的修复方案是做一次随机裁剪增强并刻意裁掉部分地面让天空占画面的比例更大强制模型学习跨区域的全局特征。别问我怎么知道的——我第一次拿这套数据训练时模型对雾天的判断依据是“画面下方有石头”不是“能见度低”。从那以后我每换一个数据集都会在第一次训练完跑一遍 Grad-CAM 检查这已经成了我评估数据质量的习惯动作。如果你的模型关注区域健康训练耗时多一点也值得如果关注点偏了趁早调数据别等调参调到头了再返工。希望这份数据集的拆解过程能帮你把训练流程走通也提前避掉那些我已经替你趟过的坑。本文还有配套的精品资源点击获取