简介这份天气分类数据集面向计算机视觉入门与进阶学习者以及需要开展图像分类实验的学生和开发者可用于CNN模型训练、迁移学习对比与数据增强等场景。资源共包含2000个文件以7987张jpg图像为主体另附1个py脚本与1个png图片压缩包约676.81MB训练集7180张、测试集808张覆盖cloudy、dew、fogsmog、frost、glaze、hail、lightning、rain、rainbow、rime、sandstorm、shine、snow、sunrise共14个类别目录按train_data与test_data划分便于直接加载训练与评估。已有1130人学习下载作者使用CNN模型取得96.3%的最好结果说明数据质量与类别区分度较好。读者可基于该数据集复现分类流程、调整网络结构与超参数并借助脚本快速搭建训练入口适合作为课程作业、竞赛练习或论文实验的基准数据。1. 天气分类数据集共9000个样本从零训练一个能用的天气识别模型手上有个天气分类数据集共9000个样本四个类别——多云、雨天、晴天、日出。这个量级不大不小刚好卡在一个尴尬的位置从头训练容易过拟合直接套用预训练模型又觉得杀鸡用牛刀。但实际工程里这恰恰是最常见的场景——你不可能每次都拿到百万级数据更多时候就是几千到一万张图要求你在有限资源下把准确率做到能上线的水平。这篇文章要解决的问题很具体拿到这个9000样本的天气分类数据集后怎么划分、怎么增强、选什么模型、参数怎么调、训练过程中怎么判断模型是不是在假学。适合有基本PyTorch基础、想快速跑通一个完整图像分类流程的读者。不涉及任何复杂部署目标就是让你在单卡上把验证集准确率做到90%以上并且知道每一步为什么这么做。2. 数据划分与增强9000样本怎么切才不翻车2.1 按类别分层划分别用随机切分9000个样本如果按7:2:1划分训练集6300、验证集1800、测试集900。听起来合理但如果你直接random.shuffle然后切片很可能出现某个类别在验证集里只有几十张的情况。天气分类四个类别理论上应该均衡但实际数据集中晴天和多云往往偏多日出可能偏少。正确做法是按类别分层抽样。用sklearn.model_selection.train_test_split的stratify参数保证每个子集的类别比例和原始一致。import os import shutil from sklearn.model_selection import train_test_split # 假设数据结构data/多云/*.jpg, data/雨天/*.jpg, ... data_dir data classes [cloudy, rainy, sunny, sunrise] all_files, all_labels [], [] for idx, cls in enumerate(classes): cls_dir os.path.join(data_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .png, .jpeg)): all_files.append(os.path.join(cls_dir, fname)) all_labels.append(idx) # 先切出测试集再从剩余切验证集 X_train, X_test, y_train, y_test train_test_split( all_files, all_labels, test_size0.1, stratifyall_labels, random_state42 ) X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.2, stratifyy_train, random_state42 ) print(f训练集: {len(X_train)}, 验证集: {len(X_val)}, 测试集: {len(X_test)})这段代码的关键在stratifyall_labels它确保每个子集中四个类别的比例与原始数据集一致。random_state42是为了可复现换个数结果会不同但分布规律一致。注意先切测试集再切验证集避免测试集信息泄露到验证集。2.2 增强策略要匹配天气场景天气分类的图像增强不能照搬ImageNet那套。翻转、裁剪、颜色抖动都可以用但有几个细节水平翻转可以天气场景没有方向性垂直翻转不建议天空不会跑到下面颜色抖动慎用天气分类高度依赖颜色和亮度特征过度抖动会让晴天和多云混淆RandomResizedCrop可以用但scale范围别设太激进0.7到1.0比较稳from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])ColorJitter里hue只给了0.05因为色相变化太大会把日出的暖色调变成冷色调。Normalize用的是ImageNet统计量如果你用预训练模型就必须保持一致从头训练的话可以用数据集自身的均值和标准差但差别不大。提示增强只作用于训练集验证集和测试集只用ResizeCenterCropNormalize。这个坑每年都有人踩。3. 模型选型9000样本该用ResNet还是EfficientNet3.1 预训练权重几乎总是更好的起点9000样本从头训练一个ResNet-50训练集6300张每个epoch只能看到6300次样本。ResNet-50有2500万参数这个数据量根本喂不饱训练loss能降下去但验证loss很快反弹——典型的过拟合。用预训练权重是标准做法。ImageNet上预训练的模型已经学会了边缘、纹理、颜色分布等底层特征你只需要微调高层语义部分。实际测试中同样9000样本预训练ResNet-18比从头训练的ResNet-50验证准确率高8到12个百分点。import torch import torch.nn as nn from torchvision import models def build_model(num_classes4, model_nameresnet18, pretrainedTrue): if model_name resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif model_name efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT if pretrained else None) in_features model.classifier[1].in_features model.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model替换分类头时加了Dropout(0.3)这是小数据集上的常规操作。ResNet18_Weights.DEFAULT是新版torchvision的写法旧版用pretrainedTrue效果一样但新写法更明确。3.2 冻结策略先冻后解分阶段微调直接全量微调不是不行但小数据集上容易把预训练学到的特征冲掉。更稳的做法是分两阶段第一阶段冻结backbone只训练分类头。学习率设1e-3跑5个epoch。这一步让随机初始化的分类头先收敛到一个合理位置不会产生大梯度回传破坏预训练特征。第二阶段解冻全部参数用更小的学习率1e-4微调。跑15到20个epoch配合余弦退火调度。def set_backbone_grad(model, requires_grad): for name, param in model.named_parameters(): if fc not in name and classifier not in name: param.requires_grad requires_grad # 阶段一 model build_model() set_backbone_grad(model, False) optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 阶段二训练5个epoch后 set_backbone_grad(model, True) optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20)filter(lambda p: p.requires_grad, ...)这行很重要否则优化器会把冻结参数也加进去虽然不更新但浪费显存。阶段切换时重新构建优化器因为参数组的requires_grad状态变了。3.3 批次大小与学习率的搭配9000样本batch_size设32或64都行。32的话一个epoch约197步64约98步。小数据集建议用32梯度更新更频繁收敛更稳。学习率和batch_size的关系batch_size翻倍学习率可以适当放大1.5到2倍但微调场景下不建议超过1e-3。我一般用1e-4起步如果loss下降太慢再调到3e-4。4. 训练循环与验证怎么判断模型是不是在假学4.1 训练循环里必须记录的东西很多人训练时只看loss这是不够的。至少记录训练loss、训练准确率、验证loss、验证准确率、当前学习率。这四个指标能告诉你模型是在真学还是假学。import torch from torch.utils.data import DataLoader, Dataset from PIL import Image class WeatherDataset(Dataset): def __init__(self, file_list, label_list, transformNone): self.file_list file_list self.label_list label_list self.transform transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): img Image.open(self.file_list[idx]).convert(RGB) label self.label_list[idx] if self.transform: img self.transform(img) return img, label def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / totalmodel.train()和model.eval()必须成对出现前者启用Dropout和BatchNorm的更新后者固定它们。torch.no_grad()在验证时关闭梯度计算省显存也加速。4.2 过拟合与欠拟合的判断信号训练loss降、验证loss也降正常学习训练loss降、验证loss先降后升过拟合需要加正则或早停训练loss不降、验证loss不降欠拟合学习率太小或模型容量不够训练准确率很高、验证准确率很低且差距持续扩大严重过拟合9000样本用预训练ResNet-18正常情况下第3到5个epoch验证准确率就能到85%以上10个epoch内到90%。如果20个epoch还在80%徘徊检查数据标签有没有错、增强是不是太狠、学习率是不是设错了。注意验证集准确率波动2%以内是正常的别看到一次下降就调参。看趋势不看单点。4.3 早停与模型保存best_acc 0.0 patience 7 counter 0 for epoch in range(25): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1}: train_loss{train_loss:.4f} train_acc{train_acc:.4f} fval_loss{val_loss:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stopping at epoch {epoch1}) breakpatience7意味着验证准确率连续7个epoch没提升就停。这个值别设太小余弦退火过程中准确率可能先平后升。保存的是state_dict()而不是整个模型加载时先构建模型结构再load_state_dict。5. 避坑与排查9000样本训练中最容易翻车的5个地方5.1 验证准确率远高于测试准确率现象验证集92%测试集只有78%。原因验证集和测试集划分时没有分层或者验证集被重复用于调参导致信息泄露。更隐蔽的原因是数据集中存在同一场景的连拍图片随机划分时相似图片同时进入训练和验证集。解决按类别分层划分并且检查是否有重复或高度相似的图片。可以用感知哈希去重或者按拍摄时间/地点分组划分。如果数据集里同一场景有多张图用GroupShuffleSplit而不是普通train_test_split。5.2 训练loss正常但验证loss是NaN现象前几个epoch正常突然验证loss变成nan。原因验证集中有损坏图片或者某张图片尺寸异常导致预处理出错。也可能是学习率太大导致权重爆炸。解决在Dataset的__getitem__里加try-except跳过无法读取的图片。同时用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)做梯度裁剪。检查学习率是否超过1e-3。5.3 某个类别准确率特别低现象多云、雨天、晴天都在90%以上日出只有60%。原因日出样本量太少或者日出和多云在视觉上确实难分。也可能是标注错误把日出标成了多云。解决先看混淆矩阵确认是跟哪个类别混淆。如果是样本量问题对少数类做过采样或加类别权重。如果是标注问题人工抽查几十张。nn.CrossEntropyLoss(weightclass_weights)可以给少数类更高权重。5.4 模型在验证集上表现好但实际用起来很差现象验证集90%但拿手机拍的照片测试准确率掉到60%。原因训练数据都是专业相机拍摄的高质量图片手机照片的色调、分辨率、噪点分布完全不同。这是域偏移问题。解决在增强里加入模拟手机拍摄的变换——降低分辨率、加高斯噪声、调整白平衡。或者收集少量手机拍摄的图片做微调。没有这个条件的话至少把Resize的插值方式从默认的bilinear改成bicubic对低质量图片更友好。5.5 训练速度慢得离谱现象一个epoch要跑十几分钟。原因num_workers设成了0数据加载是单线程的。或者图片分辨率太大224的输入实际读的是4000x3000的原图。解决DataLoader设num_workers4或8pin_memoryTrue。在Dataset里先做一次离线Resize把原图缩到256x256存下来训练时直接读小图。这个操作能把训练速度提升3到5倍。train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue )drop_lastTrue在训练时丢弃最后一个不满batch的数据避免BatchNorm在只有一两张图时统计量不准。6. 进阶技巧用混淆矩阵和Grad-CAM定位模型到底在看哪里训练完模型准确率90%以上但你真的知道模型在学什么吗我习惯做两件事画混淆矩阵跑Grad-CAM。混淆矩阵能告诉你类别之间的混淆模式。比如多云和雨天互相混淆多说明模型对云层厚度和亮度的区分不够敏感。这时候可以针对性加强这两类的数据增强或者引入注意力机制。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclasses, yticklabelsclasses) plt.savefig(confusion_matrix.png) print(classification_report(all_labels, all_preds, target_namesclasses))Grad-CAM更直观它把模型最后卷积层的梯度回传到特征图上生成热力图叠加在原图上。如果模型判断晴天时关注的是天空区域说明学对了如果关注的是图片角落的水印那就翻车了。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers [model.layer4[-1]] # ResNet18的最后一个卷积块 cam GradCAM(modelmodel, target_layerstarget_layers) grayscale_cam cam(input_tensorimg_tensor.unsqueeze(0)) visualization show_cam_on_image(img_np, grayscale_cam[0], use_rgbTrue)跑完Grad-CAM后我发现一个血泪教训模型有时候会依赖图片的亮度均值来判断类别而不是真正的天气特征。这意味着如果测试图片整体偏暗模型会倾向预测雨天。解决办法是在增强里加入亮度归一化或者用更激进的ColorJitter让模型学会忽略全局亮度。最后一个习惯每次训练完把验证集里预测错误的图片单独存一个文件夹肉眼过一遍。十次里有八次能发现标注错误或者数据质量问题。这个动作花不了几分钟但比调参管用得多。希望帮到你。本文还有配套的精品资源点击获取