简介:面向计算机视觉入门与图像分类任务实践,提供一套已划分好的4种自然天气图像数据集,覆盖晴朗、多云等四类场景,可直接用于训练与测试。数据按ImageFolder格式组织,train与test目录分别存放901张和224张图片,无需额外预处理即可接入PyTorch等框架;目录命名直观,类别标签与类型一一对应。压缩包共1128个文件,以jpg图片为主体,另含少量jpeg、png及一个可视化py脚本、一个json配置文件;py脚本支持随机读取图片并保存展示结果,方便快速检查数据质量,json文件则可用于记录类别映射。整体大小约91.93MB,轻量易下载,目前已有172人学习使用。数据集类别均衡、划分明确,适合课程作业、算法对比、天气识别项目起步;自带可视化工具进一步降低上手门槛,是一份可直接落地的分类数据集。
1. 自然天气图片分类数据集到手后:别急着写网络,先花十分钟验收划分
做图像分类的人最容易在数据集上翻车,尤其是花了大把时间写模型,最后发现准确率上不去的“元凶”是训练集和测试集根本不在同一个频道上。这份自然天气分类数据集一共 93MB,训练集 901 张、测试集 224 张,4 个天气类别已经按文件夹分好,不用你手动拆分。它最大的价值并不是“图多”,而是“划分干净”:data/train、data/test 各自带着子目录,直接用 PyTorch 的 ImageFolder 就能打开,省掉了写自定义 Dataset 和拆分脚本的过程。如果你是刚入门图像分类、要做课程设计,或者只是缺一份能快速验证训练流程的天气图片数据,这套资源很合适。但有一点必须说在前面:划分做完了不代表划分合理,训练之前先验收。
2. 先看懂 93MB 数据怎么组织的:目录结构、类别映射与数量校验
2.1 目录树里藏着答案:父目录名才是真正的标签
解压这份数据后,你会看到 data 目录,下面只有 train 和 test 两个子目录。train 里面的每一个子文件夹代表一个天气类别,文件夹里放的是这个类别的图片,test 目录结构完全一样,只是图片数量更少。这种组织方式不是随便拍的,它是 torchvision ImageFolder 能直接读取的基础前提。
data/ ├── train/ │ ├── rain/ │ │ ├── rain22.jpeg │ │ ├── rain24.jpeg │ │ └── ... │ ├── sunrise/ │ │ ├── sunrise89.jpeg │ │ ├── sunrise7.jpg │ │ └── ... │ ├── cloudy/ │ │ └── ... │ └── shine/ │ └── ... └── test/ ├── rain/ ├── sunrise/ ├── cloudy/ └── shine/这里有个容易让新手迷惑的点:文件名里有 rain、sunrise 这样的前缀,但文件名本身不是标签。哪怕某张图叫 rain22.jpeg,程序真正读到的类别也是它所在的父目录名。我之前见过有人拿文件名做 one-hot 编码,结果下载回来图片名被批量重命名过,标签全乱了。这个数据集的正确姿势是只看父目录名,不要自己去 parse filename,ImageFolder 在构造时就会把子目录名字按字母排序映射成 label。
2.2 数量分布核查:901 和 224 不是随便写的数字
train 901 张、test 224 张,加起来 1125 张,测试集占比大概是 19.9%,接近常见的 8:2 分割。但光看总数不够,你得确认每个类别在 train 和 test 里都出现了,而且数量没有严重失衡。尤其是天气数据,雨天和晴天的数量经常差好几倍,直接训练会让模型偏向样本多的类。
import os from collections import Counter for split in ['train', 'test']: root = os.path.join('data', split) counter = Counter() for cls in os.listdir(root): cls_path = os.path.join(root, cls) if os.path.isdir(cls_path): counter[cls] = len(os.listdir(cls_path)) total = sum(counter.values()) print(f'[{split}] 总数: {total}') for cls, count in counter.items(): print(f' {cls}: {count} ({count / total:.1%})')这段脚本做的事情就是遍历 data/train 和 data/test 下的每一个类别文件夹,统计每个文件夹里的图片文件数量。Counter 用来按类别计数,total 是当前 split 下的样本总数。如果输出里某个类别在 test 中数量为 0,说明这个类只有训练样本没有测试样本,后续做评估时该类的准确率根本算不出来。org 目录结构有问题的时候,os.path.isdir 会帮我们跳过非目录文件,避免把 Mac 上的 .DS_Store 之类的隐藏文件误当成类别。
2.3 为什么“已做数据集划分”能省掉一大半踩坑
手动划分数据集的坑是很隐蔽的:需要先收集所有图片路径,做 shuffle,再按比例切开,还要保证每个类别在两个集合里的比例接近原分布。如果直接在全局文件列表上切一刀,很可能某个类别全部进了训练集,测试集里根本见不到它。这份数据已经帮你完成了 train/test 划分,所以你至少不用写那套 split 脚本。但“已经划分”不等于“划分合理”,你还需要做一道最基础的验收:确认训练集和测试集的类别映射是否一致。
from torchvision import datasets train_ds = datasets.ImageFolder('data/train') test_ds = datasets.ImageFolder('data/test') print('train classes:', train_ds.classes) print('test classes:', test_ds.classes) print('train index:', train_ds.class_to_idx) print('test index:', test_ds.class_to_idx) assert train_ds.class_to_idx == test_ds.class_to_idx, '类别映射不一致'ImageFolder 在初始化的时候会扫描 root 下的子目录,自动生成 classes 列表和 class_to_idx 字典,不需要手动读一遍文件名。这里直接实例化两个数据集,做一个断言,就能知道 train 和 test 的映射是否对齐。之所以这一步重要,是因为后面训练时模型输出的是索引,如果你用 train 的顺序去解释 test 的预测结果,而两边字母排序不一致,结果会完全错位。这种做法对作物病害图像、工业质检图像这类按文件夹组织的图像数据集同样适用,目录结构一规范,后面的流程就统一了。
3. ImageFolder 加载与图片可视化:两行代码背后的三个约定
3.1 ImageFolder 运行前提:子目录、扩展名、类别排序
torchvision 的 datasets.ImageFolder 功能其实很薄,它本质上是一个读取文件夹的 Dataset。它有三个约定需要你心里有数。第一,root 下面必须直接是类别子目录,不能中间再套一层;第二,图片后缀得是 ImageFolder 默认支持的 .jpg、.jpeg、.png 等,如果你有一批大写的 .JPG 文件,它不会自动识别;第三,类别索引不是按你写代码的顺序排列,而是按文件夹名字母升序排列。这个排序是很多人第一批踩坑的地方,我在第 5 章会展开讲。
3.2 直接打开 train 并随机展示一张图片
这份资源里自带一个可视化 py 文件,作用就是随机抽一张图片展示并保存。如果你自己写,核心逻辑是这样的:
import random import matplotlib.pyplot as plt from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) train_dataset = datasets.ImageFolder(root='data/train', transform=transform) print('类别索引:', train_dataset.class_to_idx) print('样本总数:', len(train_dataset)) idx = random.randint(0, len(train_dataset) - 1) img_tensor, label = train_dataset[idx] print('随机索引:', idx, '标签索引:', label) # tensor 是 CHW 顺序,matplotlib 需要 HWC plt.imshow(img_tensor.permute(1, 2, 0)) plt.axis('off') plt.savefig('preview.png', bbox_inches='tight')逻辑说明:先定义 transform,Resize 把输入统一到 224×224,ToTensor 把 PIL Image 转成浮点张量,并将通道从 HWC 调整成 CHW,同时把像素值从 0-255 缩放到 0-1。ImageFolder 构造完成后,train_dataset[i] 会返回一个图像张量和一个整数标签。最后 plt.imshow 需要的是 HWC 布局,所以要用 permute(1, 2, 0) 把通道维度挪回最后。这里有个容易忽略的参数:Resize 的尺寸。如果你后续要接 ResNet 这类网络,224 是最常见输入尺寸;如果只是想看原图,建议直接不传 transform 读原图保存,避免被压缩变形干扰判断。
3.3 把单图预览升级成逐类网格预览
随机抽一张图只能证明数据能读,不能证明每个文件夹里的图确实对应它的类名。更好的做法是每个类别各抽一张,拼成一张网格图,一眼就能看出有没有放错。
import os import matplotlib.pyplot as plt from torchvision import datasets train_dataset = datasets.ImageFolder(root='data/train', transform=None) fig, axes = plt.subplots(1, len(train_dataset.classes), figsize=(12, 3)) for ax, cls in zip(axes, train_dataset.classes): cls_dir = os.path.join(train_dataset.root, cls) img_file = os.listdir(cls_dir)[0] img = plt.imread(os.path.join(cls_dir, img_file)) ax.imshow(img) ax.set_title(cls) ax.axis('off') fig.savefig('class_grid.png', bbox_inches='tight')这段代码的关键在于 transform=None,也就是说 ImageFolder 不会对图片做任何预处理,返回的仍然是最原始的样本。取每个类别文件夹的第一张图,用 plt.imread 读进来,直接画在子图里。figsize 里的 12 按类别数做了简单的宽度适配,四类天气图刚好合适。如果你发现某个类别里混入了明显不是对应天气的图片,别急着训练,先把目录里的数据清理掉再跑实验。这个网格预览脚本也适合其他图像数据集,比如病害叶片或者工业缺陷样本,原则都一样:先看图,再看数。
4. 训练前先验收:重复图片、类别一致性与损坏图片的三类检查
4.1 重复图片检查:给所有文件算 SHA256,训练与测试不应有交集
数据泄漏是图像分类里最让人难受的问题。训练集和测试集里出现同一张图,或者同一张图的不同压缩版本,测试准确率会高得离谱,一旦换成真实新图立刻现原形。这种问题用肉眼看目录很难发现,最快的方式是对每个文件算哈希,然后比较 train 和 test 的哈希集合。
import os import hashlib def file_sha256(path, chunk_size=1 << 20): h = hashlib.sha256() with open(path, 'rb') as f: while chunk := f.read(chunk_size): h.update(chunk) return h.hexdigest() def build_index(split): index = {} root = os.path.join('data', split) for cls in os.listdir(root): cls_path = os.path.join(root, cls) if not os.path.isdir(cls_path): continue for fn in os.listdir(cls_path): fp = os.path.join(cls_path, fn) index[file_sha256(fp)] = (split, cls, fn) return index train_idx = build_index('train') test_idx = build_index('test') dup = set(train_idx.keys()) & set(test_idx.keys()) print('重复图片数:', len(dup)) for h in list(dup)[:5]: print(train_idx[h], '<=>', test_idx[h])逻辑说明:file_sha256 使用分块读取,chunk_size 设成 1MB,对 93MB 的数据集来说根本不用等。build_index 遍历某一个 split,以文件哈希为 key,记录它的 split、类别和文件名。最后用集合取交集,找出两个 split 完全相同的图片。这里有个参数需要根据实际情况调:如果图片很大,chunk_size 可以调大减少读盘次数;如果图片都很小,直接一次读完也行。找到重复后,处理原则只有一个,那就是从一个集合里删掉重复项,绝不能让同一张图同时出现在 train 和 test。
4.2 类别索引一致性:测试集的 class_to_idx 必须与训练集一致
我在第 2 章已经提过一次断言,但这里值得再强调一遍。ImageFolder 的类别排序是自动按字母升序生成的,如果你某个类别的文件夹名有细微差别,比如一个是sunrise,另一个是sun_rise,两边的 class_to_idx 顺序就会错开。一旦错开,模型预测出的 0、1、2、3 和你想的类别对不上,整个结果就成了黑匣子。
from torchvision import datasets train_ds = datasets.ImageFolder('data/train') test_ds = datasets.ImageFolder('data/test') assert train_ds.class_to_idx == test_ds.class_to_idx, '训练集测试集类别映射不一致' # 输出映射方便肉眼确认 for cls, idx in train_ds.class_to_idx.items(): print(f'{idx}: {cls}')这段代码最重要的一行就是断言,失败时直接报错,成功时打印出映射。如果两边类别数量一致但顺序不一致,也可以手动重命名文件夹,比如在类名前加0_、1_、2_前缀,让字母排序直接变成你想要的排序。这个做法能避免后续每次都要去查 class_to_idx 的麻烦。
4.3 损坏图片与尺寸异常:PIL 能打开、能 load 才算过关
jpg 文件并不是一定能把像素读出来。有时候下载过程丢字节,文件头正常但中间数据损坏;有时候文件后缀是 .jpg,内容却是 PNG。ImageFolder 能扫描到它,但训练时读取它就会报错或者产生奇怪的数据。最直接的检查方式是用 Pillow 打开并触发 load 解码。
from PIL import Image import os bad = [] sizes = set() for split in ['train', 'test']: root = os.path.join('data', split) for cls in os.listdir(root): cls_path = os.path.join(root, cls) if not os.path.isdir(cls_path): continue for fn in os.listdir(cls_path): fp = os.path.join(cls_path, fn) try: with Image.open(fp) as im: im.load() sizes.add(im.size) except Exception as e: bad.append((fp, repr(e))) print('损坏图片数:', len(bad)) print('出现过的尺寸:', sizes) for item in bad[:5]: print(item)看代码时注意两点。第一,Image.open 默认是惰性的,只有执行 im.load() 才会真正解压像素数据,所以异常要在 load 时才暴露。第二,sizes 集合记录了整个数据集出现过哪些尺寸,如果输出结果里尺寸种类特别多,说明这批图是各种来源拼凑的。尺寸不统一本身不是致命的,后面 transform 加 Resize 和 CenterCrop 就能解决,但如果出现大量非 4 通道图或损坏图,就要考虑清洗数据了。
5. 避坑指南:天气图像数据集训练前最常见的五个坑
5.1 坑一:类别索引被字母顺序打乱,导致预测标签错位
现象:训练过程一切正常,但打印预测结果时发现 sunny 对应的数字不是 0 也不是 3,和数据集介绍里给的顺序完全不一致。
原因:torchvision 的 ImageFolder 内部用sorted(os.listdir(root))生成类别列表,文件夹叫什么名字,索引顺序就按名字排序。你脑子里的顺序是 sunny、cloudy、rain、snow,但程序看到的顺序是 cloudy、rain、snow、sunny。
解决:第一次加载数据的时候立刻打印 class_to_idx,把映射存下来。更稳妥的做法是给文件夹重命名,加上数字前缀,比如0_cloudy、1_rain、2_snow、3_sunny,这样字母排序和语义顺序一致。从那以后我只要换新数据集,第一步永远是打印映射,而不是直接开训。
5.2 坑二:训练集和测试集重复图片,验证指标虚高
现象:测试集准确率轻松到 95%,但把几张拍好的新照片丢进去预测,准确率直接掉到 60%。这种漏洞最常见于从网上下载的图片集,因为整理数据时从同一个来源复制了多次。
原因:数据划分时没有做去重,同一张图同时出现在 train 和 test 中。模型在测试时相当于开卷考试,它已经见过这张图的精确像素。
解决:用第 4.1 节的哈希脚本跑一遍,找出交集,从训练集里删除重复项。这里尤其要小心那些做了缩放或加噪的“半重复”图片,肉眼觉得不一样,但语义内容一致,这类问题哈希查不出来,需要靠感知哈希或者人工抽检。对 93MB 的自然天气数据,我是建议直接多花几分钟把所有图片全部过一遍,因为图量不大。
5.3 坑三:matplotlib 中文标签变成方块
现象:用可视化脚本保存类别预览图,图片上每个类别的 title 都是方块或者问号,完全看不出写的是什么。
原因:matplotlib 默认字体里没有中文字形,Windows 上的 SimHei、Linux 上的 WenQuanYi 都需要手动指定才能生效。这个坑不会让训练失败,但会浪费你排查可视化结果的时间。
解决:在绘图前加两行配置,plt.rcParams['font.sans-serif'] = ['SimHei'],同时把axes.unicode_minus设为 False 防止负号异常。如果运行环境是 Linux 服务器,没有 SimHei,可以直接用英文类名画图,或者把类名映射先翻译成英文字母再展示。我的习惯是可视化代码默认用英文,因为训练脚本跑在服务器上的概率很大。
5.4 坑四:Batch 训练时尺寸不一致,报 stack 错误
现象:训练第一个 batch 没事,第二个 batch 突然报错,类似stack expects each tensor to be equal size。
原因:ImageFolder 本身不做任何尺寸统一,它只是把图片读出来交给 transform。如果你 transform 里只写了 ToTensor,没有 Resize,每张图的宽高就是原始尺寸,一个 batch 里存在不同 shape 的张量,DataLoader 无法把它们堆成 batch。
解决:在 transform 里统一加入Resize((224, 224)),如果想让更自然的裁剪,可以Resize((256, 256))再加CenterCrop(224)。还有一个小技巧:可以用default_collate之前做一个打印,输出每个 sample 的尺寸,快速确认问题源的路径。现在的主流网络对输入尺寸并不敏感,只要固定一个值就行,关键是别让尺寸不一致拖垮训练。
5.5 坑五:训练集和测试集用了两套 transform,归一化对不上
现象:训练损失正常下降,测试集上 Loss 异常高或者预测全是一个类。仔细检查后发现测试集 transform 里漏了 Normalize,模型输入的分布和训练时完全不一样。
原因:写代码时图省事,给 test set 单独写了一个 transform,只做了 ToTensor,没做 Normalize,还把随机翻转写进了 test 流程。测试阶段任何随机变换都会让结果不稳定。
解决:定义一个函数统一生成 transform,用train=True控制是否加入随机增强,保证两套 transform 的基础部分一直同步。基础部分至少包含 ToTensor 和一样的 Normalize,随机翻转、随机擦除只允许出现在 train。我自己现在复制代码时都会单独确认 test transform 里没有任何 random 操作,宁可多写几行也不想在指标上吃亏。
6. 进阶用法:用迁移学习把 ImageFolder 数据集一次性跑成可复现的指标
6.1 固定随机种子与统一数据流
自然天气数据集不大,1125 张图撑不起从零训练一个深度网络。常见做法是加载 ImageNet 预训练的 ResNet18,把最后一层全连接换掉,只微调分类头。为了让结果可复现,训练前要固定 PyTorch、NumPy、Python random 三个随机种子;如果用了 GPU,还要设置torch.cuda.manual_seed_all。
import torch from torchvision import datasets, models, transforms from torch.utils.data import DataLoader 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_ds = datasets.ImageFolder('data/train', transform=transform) test_ds = datasets.ImageFolder('data/test', transform=transform) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False) model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = torch.nn.Linear(model.fc.in_features, len(train_ds.classes))代码逻辑是把整个数据流串成一条线:ImageFolder 负责读图,transform 负责统一尺寸和归一化,DataLoader 负责 batch 化。Normalize 的 mean 和 std 用的是 ImageNet 预训练统计值,因为 ResNet18 的预训练权重就是这么做的。最后一行的len(train_ds.classes)会得到 4,正好替换成天气分类输出维度。
6.2 用五个 epoch 验证数据集划分是否真的合理
迁移学习不需要训练很久,就能看出数据划分有没有问题。设置五个 epoch,只在全连接层做训练,前面的卷积冻结住,这样几分钟就能跑完。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001) criterion = torch.nn.CrossEntropyLoss() for epoch in range(5): model.train() for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(images), labels) loss.backward() optimizer.step() model.eval() correct = total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) preds = model(images).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'epoch {epoch + 1}, test acc: {correct / total:.3f}')这里注意一个细节:optimizer 只传入了model.fc.parameters(),也就是只更新最后那个新的线性层。这么做的原因是天气图片和 ImageNet 的场景差异不算特别大,冻结卷积特征可以在小数据量下防止过拟合。如果你发现五个 epoch 后测试准确率还在 0.5 左右徘徊,先别怀疑模型,回头检查哈希重复和类别映射;如果准确率能到 0.8 左右,说明这个数据集的划分基本可靠。
从那以后,我每次拿到新的图片分类数据集,都强制把“哈希去重、class_to_idx 断言、统一 transform”这三步走一遍,确认没问题才把代码交给训练脚本。数据划分这类问题最隐蔽,它不会像维度报错那样打断你,只会让最终指标看起来不错,却扛不住真实场景。希望这套流程能帮你在自然天气分类以及其它图像数据集上少走几个弯路。
本文还有配套的精品资源,点击获取