ARTICLE DETAIL

资讯详情

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

恶劣天气图像分类实战:1000张数据集的训练技巧与避坑指南

恶劣天气图像分类实战:1000张数据集的训练技巧与避坑指南 简介这套图像分类数据集聚焦大雾、暴雨、沙尘暴、暴雪四种恶劣天气场景共约1000张已标注图片适合深度学习、计算机视觉方向的学习者用于图像分类实践、模型训练与算法验证。数据按4个类别划分并提供明确的JSON标注文件同时已划分训练集和测试集各类别图片单独存放便于直接训练与评估。资源包共1030个文件以1028张JPG图片为主体另含1个Python脚本和1个JSON文件其中show脚本可一键可视化数据集辅助快速检查样本质量与类别分布。压缩包整体约132.04MB下载后解压即可使用无需额外整理。目前已有667人学习浏览对有天气识别、恶劣场景分类或YOLO改进等需求的读者而言是一份标注完整、结构清晰的基础数据资源。1. 恶劣天气图像分类数据集入门只有1000张图为什么比十万张通用图更值得先跑通做图像分类的同行看到「约1,000张」这个规模第一反应多半是“这能训出什么”。但如果你做过安防、交通、电网或农业场景的落地项目就会知道真实业务里最缺的恰恰是这种带标注的恶劣天气样本。大雾、暴雨、沙尘暴、暴雪这四类天气在公开的ImageNet、COCO里几乎找不到成建制的分类集合而它们恰恰是户外视觉系统误报率最高的来源。一个能精准区分“镜头起雾”和“场外真起雾”的小模型比一个在晴天数据上刷到99%的大模型更值钱。这个数据集的意义不在体量在于它把四类最影响现场判断的天气形态收敛到了可标注、可训练、可评估的范围内。约1000张图做四分类每类250张上下这正好是迁移学习的甜区——不需要从零训练用预训练backbone做微调一台消费级GPU就能在半小时内跑完整轮实验。适合谁适合刚入门图像分类、想用一份干净数据练手的学生也适合要快速验证恶劣天气识别可行性的工程师。接下来我按拿到数据后的实际顺序把数据校验、训练脚本、增强策略、踩坑记录和进阶玩法一次讲透。2. 目录组织与标注校验先把数据摸清再谈训练效果2.1 拿到数据集后第一件事统计类别分布和检查图片完整性任何“已标注”的数据集拿到手都不能直接扔进训练脚本。标注不等于没毛病文件损坏、类别名拼写不一致、图片尺寸异常这些都会在训练中途以莫名其妙的报错形式冒出来。我一般习惯写一个独立的检查脚本先把家底盘清楚。import os from collections import Counter from PIL import Image data_root weather_dataset # 数据集的根目录 # 常见目录结构: weather_dataset/train/大雾/*.jpg, weather_dataset/val/暴雨/*.jpg for split in [train, val, test]: split_dir os.path.join(data_root, split) if not os.path.exists(split_dir): print(f[跳过] 找不到 {split} 目录) continue class_counter Counter() corrupt_list [] size_counter Counter() for class_name in os.listdir(split_dir): class_dir os.path.join(split_dir, class_name) if not os.path.isdir(class_dir): continue for img_name in os.listdir(class_dir): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(class_dir, img_name) class_counter[class_name] 1 try: with Image.open(img_path) as im: im.verify() # 只校验文件头不加载完整像素 size_counter[im.size] 1 except Exception: corrupt_list.append(img_path) print(f {split} 集 ) for cls, num in class_counter.most_common(): print(f {cls}: {num} 张) print(f 图片尺寸分布: {dict(size_counter)}) if corrupt_list: print(f [警告] {len(corrupt_list)} 张损坏图片:) for p in corrupt_list[:10]: print(f {p})这段脚本的核心是Image.verify()它只解析文件头并校验完整性不会把整张图片解码进内存速度很快1000张图几秒钟就能跑完。统计类别分布能立刻暴露两个问题某类样本是否过少、某类是否被重复拷贝。图片尺寸分布则直接决定后续训练时要不要做resize——如果四种天气的原始分辨率差异过大比如暴雨图是1080p、沙尘暴图是640x480统一resize到224x224时信息损失程度会不一致。提示如果发现损坏图片不要直接删了重下。先看损坏文件名的规律很多数据集是爬虫采集的损坏文件往往集中在同一个来源站点整批剔除比逐张删除更干净。2.2 标注文件的读法与格式转换把CSV/JSON转成训练目录这份数据集标注的具体格式不同渠道拿到的版本可能有差异。比较常见的是两种要么是图片文件名 类别标签的CSV或JSON映射表要么是已经按类别分好文件夹的ImageFolder结构。如果是前者需要先转成目录结构因为PyTorch的torchvision.datasets.ImageFolder和Keras的flow_from_directory都直接认目录。import csv import json import os import shutil # 假设标注文件是 CSV: image_path,label # 假设标注文件是 JSON: {大雾/001.jpg: 雾, 暴雨/002.jpg: 雨} def csv_to_folder(csv_path, src_root, dst_root): os.makedirs(dst_root, exist_okTrue) with open(csv_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: # 兼容不同列名写法 img_rel row.get(image_path) or row.get(filename) or row.get(img) label row.get(label) or row.get(class) or row.get(category) if not img_rel or not label: print(f[跳过] 不完整行: {row}) continue src os.path.join(src_root, img_rel) if not os.path.exists(src): print(f[跳过] 源文件不存在: {src}) continue label_dir os.path.join(dst_root, label) os.makedirs(label_dir, exist_okTrue) dst os.path.join(label_dir, os.path.basename(img_rel)) shutil.copy2(src, dst) # copy2 保留元信息便于溯源 # 调用示例 csv_to_folder(labels.csv, weather_raw_images, weather_dataset/train)这段脚本做了两层防御utf-8-sig编码解决Windows下CSV常见的BOM头乱码问题列名兼容处理解决不同版本标注文件字段名不一致的问题。图片不存在时跳过而不是中断因为一份1000张的标注里偶尔漏掉几张源图很常见中断会让你反复排查。转换完成后记得用2.1节的检查脚本再跑一遍确认每个类别的图片数量与标注文件里的统计一致。2.3 类别不均衡的量化判断别等训练完才发现某类只有80张四类天气各250张是最理想的情况但实际拿到的数据经常是雾天样本多、沙尘暴样本少因为雾天在南方城市太常见沙尘暴只在特定地区才有。类别不均衡不是必须做处理但你必须知道它不均衡到了什么程度。一个简单的判断标准最大类样本数除以最小类样本数比值超过3就该干预。import numpy as np class_counts { 雾: 260, 暴雨: 240, 沙尘暴: 90, 暴雪: 210 } # 示意数据按实际统计结果替换 counts np.array(list(class_counts.values())) imbalance_ratio counts.max() / counts.min() print(f最大类/最小类 {imbalance_ratio:.2f}) if imbalance_ratio 3: print(建议做类别均衡采样或数据增强补样本) elif imbalance_ratio 1.5: print(轻微不均衡损失函数里加class weight即可) else: print(类间分布健康正常训练)计算不均衡比只是开始关键是后续策略。常见做法有三种一是在DataLoader里用WeightedRandomSampler做加权采样让每个epoch里各类别出现的次数大致相同二是在损失函数里给样本数少的类更高的权重三是只对少样本类做更强的数据增强。前两种改代码成本低第三种保留原始数据的真实分布各有取舍。我的习惯是先用加权采样因为它的概率语义最直观每类样本被抽中的概率与样本数成反比。3. 用ResNet做恶劣天气四分类最小可复现的训练脚本3.1 为什么是ResNet而不是ViT1000张图撑不起从零训练的Transformer图像分类算法这两年最热的话题是Transformer架构ViT、Swin在各大数据集上把CNN打得没什么还手之力。但那是建立在千万级数据预训练的前提下的。1000张图微调ViT结果大概率是灾难——Transformer没有CNN那种内建的局部先验数据量不足时收敛慢、过拟合快。训练中你会发现ViT的验证精度在半程后开始剧烈震荡这就是典型的“数据撑不住模型容量”。ResNet18是这类小数据集的平衡点预训练权重容易获取参数量只有1170万左右即便在CPU上做推理单张图也在毫秒级。更关键的是它的残差连接天然缓解了深层网络的优化困难微调时即使学习率设得稍微激进一点也不容易彻底翻车。如果你实在想试试ViT我建议把位置放后面一点——先用ResNet把baseline跑出来再用TinyViT或蒸馏版ViT做对比不要一上来就赌大的。3.2 最小可用训练脚本数据加载、模型初始化与训练循环import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 训练集增强与归一化 train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), 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]), ]) train_dataset datasets.ImageFolder(weather_dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(weather_dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 4) # 把最后一层换成4分类 model model.to(device) # 损失函数与优化器 criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(1, 31): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) scheduler.step() val_acc 0.0 model.eval() with torch.no_grad(): v_correct, v_total 0, 0 for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) v_correct (preds labels).sum().item() v_total labels.size(0) val_acc v_correct / v_total print(fEpoch {epoch:2d} | Loss {total_loss/total:.4f} | fTrain Acc {correct/total:.4f} | Val Acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_weather_model.pth) print(f - 已保存最佳模型, Val Acc {best_acc:.4f})这套脚本最大的特点是把“能跑”和“能复现”放在一起。ResNet18_Weights.IMAGENET1K_V1显式指定预训练权重版本避免未来torchvision更新导致权重接口变动AdamW配合weight_decay1e-4比SGD对新手友好收敛稳定CosineAnnealingLR让学习率在30个epoch内从1e-4平滑降到接近0省去手动调整学习率衰减节点的麻烦。batch_size32在8G显存的显卡上刚好跑满如果显存不足降到16记得学习率也相应降到5e-5。注意用ImageNet预训练权重微调时最后那个fc层是随机初始化的。前几个epoch它的梯度会很大所以全模型微调时学习率别超过1e-3否则前面层预训练的特征会被冲坏。这也是我把初始学习率压在1e-4的原因。3.3 训练参数设定的逻辑学习率、batch size与epoch的联动关系训练参数不是随手填的它们之间有明确的联动关系。学习率决定参数更新的步长batch size决定每次更新时的梯度估计质量epoch数决定模型看到数据的遍数。小数据集上最常见的错误是照搬ImageNet的batch size和epoch设置——那是百万级数据的配置拿过来直接会过拟合。以1000张图、四分类为例我推荐的组合是batch size 32、学习率1e-4、epoch 30。选择依据很直接1000张图每张被看到约30遍对于预训练模型的微调来说足够如果你发现验证精度在某个epoch后开始不升反降那就是过拟合信号需要把epoch减半或者加大数据增强。一个实用的判断方法记录每个epoch的训练精度和验证精度两者差距超过10个百分点就该考虑正则化或提前停止。4. 数据增强与类间混淆四种天气为什么总被“晴天阴天”带偏4.1 先跑一轮不加增强的baseline看混淆矩阵再决定策略很多人拿到数据的第一反应是堆增强。我的建议恰恰相反——先裸跑一轮不加任何随机变换只做resize和归一化。这轮baseline的价值在于暴露问题如果你的验证精度已经到95%以上说明数据本身的区分度很高增强只需要保守的翻转和裁剪如果精度在80%以下徘徊你再看混淆矩阵会发现“雾”和“暴雪”永远互相误判。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay # 推理阶段收集所有预测结果 all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) class_names train_dataset.classes # 按目录字母序排序的类别名 cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(cm, display_labelsclass_names) disp.plot(cmapBlues) plt.xticks(rotation30) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)混淆矩阵是分类任务的黑暗角落探照灯。总精度只告诉你“有没有错”混淆矩阵告诉你“错在哪里”。比如雾和暴雪互相误判率高直观上说得通暴雪天气里远处的景物也是白茫茫一片低对比度和雾天高度相似。这时候单纯增加训练数据量作用有限要针对性地增强两者的区分特征。4.2 针对恶劣天气的增强组合亮度、对比度与随机擦除四种天气各有各的视觉退化模式雾天是低对比度加去饱和暴雨是画面偏暗加动态模糊沙尘暴是整体偏黄褐色暴雪是白色区域占比高加局部遮挡。通用的随机裁剪和翻转并不能模拟这些退化需要针对性增强。import random from torchvision import transforms import torchvision.transforms.functional as F class WeatherJitter: 针对恶劣天气的亮度/对比度/饱和度扰动 def __init__(self, brightness0.4, contrast0.4, saturation0.4): self.brightness brightness self.contrast contrast self.saturation saturation def __call__(self, img): # 随机应用亮度扰动: 模拟不同时间段的日照差异 if random.random() 0.5: factor 1.0 random.uniform(-self.brightness, self.brightness) img F.adjust_brightness(img, factor) # 随机应用对比度扰动: 模拟雾和沙尘对通透度的影响 if random.random() 0.5: factor 1.0 random.uniform(-self.contrast, self.contrast) img F.adjust_contrast(img, factor) # 随机应用饱和度扰动: 模拟雨后和沙尘天的色彩退化 if random.random() 0.5: factor 1.0 random.uniform(-self.saturation, self.saturation) img F.adjust_saturation(img, factor) return img # 增强后的训练transform strong_train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), WeatherJitter(brightness0.4, contrast0.4, saturation0.4), transforms.RandomErasing(p0.25, scale(0.02, 0.15)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomErasing在0.02到0.15的遮挡比例下随机擦除小区域模拟暴雪中雪花遮挡和暴雨中雨滴遮挡物体的效果。WeatherJitter里的三个操作各以50%概率独立触发比全部概率触发更稳因为真实天气不是同时在所有维度退化的。对比度扰动对雾天尤其重要——雾的本质是能见度降低导致的对比度下降手动调低对比度等于把雾的训练分布往更雾的方向外推。沙尘暴的偏色可以用饱和度扰动配合色相扰动模拟但色相扰动幅度要控制在0.1以内否则会把蓝天变成紫天引入不存在的分布。4.3 增强还会翻车从过拟合到欠拟合的弹道修正增强不是越强越好。我见过有人把增强拉到随机擦除50%概率、亮度扰动0.8结果训练loss降不下去验证精度反而比baseline还低。这是增强过度的典型症状图片被破坏到保留不下原始天气语义模型学到的全是噪声。怎么判断增强的强度是否合适一个有效方法是对增强后的样本做人工抽检。每轮增强配置跑完训练后把训练集里随机抽9张图拼一张网格保存下来肉眼扫一遍看看增强后的图是否还能让一个人一眼认出天气。如果人看着都费劲模型大概率也学不出来。另一个经验值是增强引入的精度损失不应超过baseline的2到3个百分点。如果增强前后验证精度差距拉大到5个点以上说明增强破坏了太多语义信息需要回调强度。5. 恶劣天气分类的四个常见坑切分方式、特征泄漏与部署一致性5.1 坑一训练集和验证集切分太随意同源图片泄漏进场现象训练精度和验证精度都很高但把模型拿到真实场景里一测精度直接掉到60%。原因数据集里的图片很多是同一次拍摄事件的多帧连拍比如同一场雾的5张连拍。如果按文件名顺序前80%做训练、后20%做验证同一个场景的帧被分到两边。模型学到的是“这个具体场景的特征”而不是“雾的通用特征”。解决按拍摄事件或时间戳分组切分。如果标注信息里没有时间戳可以用图片的感知哈希做去重把内容高度相似的多帧归到同一个组。分组后再按组切分保证同一场景的帧不会同时出现在训练与验证集中。操作上我用一个简单的规则sorted_images[::5]做验证集抽帧这样每隔4帧取一帧同场景连拍最多验证集只有一帧基本能打破这种泄漏。注意这个坑在1000张的小数据集上尤其致命。因为数据量小模型拟合场景特征比拟合天气特征更容易泄漏的破坏力会被放大。5.2 坑二雾和暴雪互相误判区分特征被增强淹没现象其他类别的精度都到了90%以上只有雾和暴雪的互认率还在40%左右徘徊。原因两类在低对比度、低纹理区域的视觉表现非常接近。暴雪时远近景物被雪覆盖画面变成白茫茫一片大雾天气同样是白茫茫一片。如果增强配置里没有强化二者的差异特征模型就学不到区分的着力点。解决给暴雪类单独加一组增强——提升白色像素的占比和对比度让画面更接近“雪的物质感”同时给雾类加轻微去饱和增强拉低它的色彩丰富度。具体做法是把暴雪类数据额外做一次ColorJitter的偏白处理或者在loss层面给两类错误互判的样本提高权重。我实际用到最好用的是给暴雪类的随机裁剪比例调大比如RandomResizedCrop的scale从0.08提至0.2让模型更多看到雪面纹理而不只是整片白。5.3 坑三沙尘暴过拟合严重训练精度99%但验证精度只有70%现象沙尘暴类样本少训练精度暴涨验证精度却一直跟不上。原因某类样本少且画风单一比如所有沙尘暴图都来自同一个月份的同一区域模型把背景色和天气语义捆绑在一起了。沙尘暴的黄色调变成了背景色特征而不是天气过程特征。解决一是用2.3节的WeightedRandomSampler给沙尘暴类提高采样权重等效于每轮多看几遍二是专门给该类做色相扰动打破黄色调与具体场景的强绑定。另外可以对沙尘暴类做Mixup把沙尘暴图和雾图按0.7/0.3混合标签平滑强制模型去关注沙尘暴的结构特征而不是整体色调。5.4 坑四训练时归一化正常部署时忘了带预处理管线现象离线测试精度95%模型部署到摄像头后晴天画面被识别成暴雨。原因训练时数据经过Normalize(mean[0.485, 0.456, 0.406], std...)模型学到的分布是在这个标准化空间里的。部署端的推理代码如果直接喂原始图像给模型输入张量的数值范围完全不同输出自然乱套。解决部署时把预处理管线完整迁移——Resize((224, 224))、ToTensor()、Normalize三件套一个不能少。建议把预处理封装成一个函数放进推理服务里不要散写在各个调用点。def preprocess_for_inference(img_pil): 部署端必须与训练端保持完全一致的预处理 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]), ]) return transform(img_pil).unsqueeze(0) # 增加batch维度6. 把1000张用出5000张的效果蒸馏、伪标签与均衡采样的组合拳这个数据集让人最纠结的永远是“只有1000张”。前面说的都是怎么把这1000张训练好但如果你需要更高的精度还有三条不增加标注成本的扩展路径按性价比排序分别是知识蒸馏、半监督伪标签、类别均衡采样深调。知识蒸馏的思路是用一个更强的教师模型在训练集上生成软标签让学生模型学“教师怎么想”而不是学“硬标签是什么”。教师可以用ImageNet预训练的ResNet50或Swin-T因为四个类别差异大但类间有相似性教师的软标签里蕴含着“雾和暴雪相似度0.3但和晴天只有0.05”这类精细信息。蒸馏损失用KLDivLoss配上温度系数T3软化分布。实际操作里我会先训教师模型然后把教师对训练集所有图片的输出logits保存下来训练学生模型时同时算交叉熵和蒸馏损失loss alpha * CE(student_output, hard_label) (1 - alpha) * KL(student_logits / T, teacher_logits / T) * T^2alpha取0.5让学生既尊重硬标签又吸收教师对类间相似度的判断。在1000张这种规模上蒸馏通常能给2到4个点的提升。伪标签要谨慎。做法是拿已训好的模型对无标注的外部天气图片做预测把置信度超过0.95的样本打上预测标签加入训练集。这个做法在跨domain时风险很高——外部图片的拍摄条件跟训练集差异大高置信度往往意味着“模型见过的某种模式”而非“真是这个类”。用的时候只挑置信度在0.95以上的并且每轮加入的伪标签数量不超过原数据集的30%防止模型被自己带偏。最后一招是类别均衡采样的深调版本。前面用的WeightedRandomSampler只能做到“各类图片被抽到的概率相近”但如果某个类只有80张它每轮都在重复同一批样本反而加深过拟合。我一般会配合Mixup做类间插值或者对少样本类单独加大增强强度。这三招可以叠加使用叠加后这个1000张的模型在验证集上跑到93%到95%并不夸张关键是每一步都要用小验证集盯住精度变化不要叠加完才统一看效果。我个人的习惯是每加一个模块就做一组消融实验把“baseline→蒸馏→伪标签→均衡采样”的精度变化记录成一张表。这个习惯帮我挡掉过很多次“不知道是哪一步变好了还是变坏了”的情况。而且在这个项目的后续迭代里这张表能直接告诉你要不要花更多成本去扩数据——如果蒸馏只带来0.5个百分点提升优先级就得让给标注更多样本如果带来了4个点那当前数据还没有被榨干再加训练技巧空间反而更大。用这套方式1000张图也能跑出一条可靠、可持续迭代的恶劣天气分类基线希望帮到你。本文还有配套的精品资源点击获取
返回列表