1. Tiny-ImageNet数据集初探第一次接触Tiny-ImageNet数据集时我完全被它混乱的目录结构搞懵了。这个由斯坦福CS231N课程推出的经典数据集虽然只有ImageNet的迷你版200个类别每类500张训练图片但在实际使用中却比想象中麻烦得多。最让人头疼的是它的训练集和验证集采用了完全不同的存储方式——训练集按类别分文件夹存放验证集却把所有图片堆在一个目录里这种设计让直接用PyTorch的ImageLoader变得不可能。记得当时为了赶项目进度我差点就放弃使用这个数据集了。好在经过一番折腾终于摸索出一套完整的解决方案。现在回想起来其实只要理解了数据组织逻辑再配合PyTorch的Dataset类进行定制化处理整个过程并没有那么可怕。下面我就把踩过的坑和最终方案完整分享给大家。2. 数据下载与目录解构2.1 获取数据集官方下载链接http://cs231n.stanford.edu/tiny-imagenet-200.zip依然有效不过我发现用wget命令下载会更稳定wget http://cs231n.stanford.edu/tiny-imagenet-200.zip -P ./data unzip ./data/tiny-imagenet-200.zip -d ./data解压后的目录结构是这样的tiny-imagenet-200/ ├── train/ │ ├── n01443537/ # 类别文件夹 │ │ ├── images/ # 实际图片存放处 │ │ │ ├── n01443537_0.JPEG │ │ │ └── ... │ │ └── n01443537_boxes.txt # 边界框标注 │ └── ... # 共200个这样的文件夹 ├── val/ │ ├── images/ # 所有验证图片混在一起 │ │ ├── val_0.JPEG │ │ └── ... │ └── val_annotations.txt # 验证集标注文件 ├── test/ # 无标签通常不用 │ └── images/ │ ├── test_0.JPEG │ └── ... ├── wnids.txt # 所有类别ID列表 └── words.txt # 类别ID到名称的映射2.2 关键文件解析wnids.txt包含200个WordNet ID如n01443537每行一个words.txt将WordNet ID映射到人类可读标签如goldfishval_annotations.txt验证集的图片与类别对应关系格式为val_0.JPEG n01443537 0 0 64 64各列分别是文件名、类别ID、边界框坐标(x,y,w,h)3. 自定义Dataset类实现3.1 基础框架搭建PyTorch的Dataset类需要实现三个核心方法__init__、__len__和__getitem__。我们先搭建骨架from torch.utils.data import Dataset from PIL import Image import os class TinyImageNetDataset(Dataset): def __init__(self, root_dir, trainTrue, transformNone): self.root_dir root_dir self.train train self.transform transform self.image_paths [] self.labels [] # 后续填充具体实现 self._prepare_data() def _prepare_data(self): 根据train/val模式准备数据路径和标签 pass def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] label self.labels[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label3.2 训练集处理实现训练集的处理相对简单因为目录结构清晰。我们需要遍历每个类别文件夹收集所有图片路径根据文件夹名确定类别标签def _prepare_data(self): if self.train: train_dir os.path.join(self.root_dir, train) # 获取所有类别文件夹 class_dirs [d for d in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, d))] class_dirs.sort() # 确保顺序一致 # 创建类别到索引的映射 self.class_to_idx {cls: i for i, cls in enumerate(class_dirs)} # 收集所有图片路径和标签 for cls in class_dirs: cls_dir os.path.join(train_dir, cls, images) for img_name in os.listdir(cls_dir): if img_name.endswith(.JPEG): self.image_paths.append(os.path.join(cls_dir, img_name)) self.labels.append(self.class_to_idx[cls])3.3 验证集处理实现验证集需要解析val_annotations.txt文件来建立图片到类别的映射else: # 验证集模式 val_dir os.path.join(self.root_dir, val) annotations_file os.path.join(val_dir, val_annotations.txt) # 先读取所有类别 with open(os.path.join(self.root_dir, wnids.txt), r) as f: classes [line.strip() for line in f] self.class_to_idx {cls: i for i, cls in enumerate(classes)} # 解析标注文件 img_to_class {} with open(annotations_file, r) as f: for line in f: parts line.strip().split(\t) img_to_class[parts[0]] parts[1] # 收集图片和标签 images_dir os.path.join(val_dir, images) for img_name in os.listdir(images_dir): if img_name.endswith(.JPEG) and img_name in img_to_class: self.image_paths.append(os.path.join(images_dir, img_name)) self.labels.append(self.class_to_idx[img_to_class[img_name]])4. 数据增强与标准化4.1 标准预处理流程对于ImageNet类数据集通常采用以下标准化参数from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(64), # Tiny-ImageNet图片尺寸 transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(64), transforms.CenterCrop(64), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])4.2 可视化检查技巧在正式训练前我强烈建议先可视化检查数据加载是否正确import matplotlib.pyplot as plt import numpy as np def imshow(inp, titleNone): 反标准化并显示图像 inp inp.numpy().transpose((1, 2, 0)) mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) inp std * inp mean inp np.clip(inp, 0, 1) plt.imshow(inp) if title: plt.title(title) plt.pause(0.001) # 测试显示 dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainTrue, transformtrain_transform) img, label dataset[0] imshow(img, titlefLabel: {label})5. 完整使用示例5.1 创建DataLoaderfrom torch.utils.data import DataLoader train_dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainTrue, transformtrain_transform) val_dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainFalse, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4)5.2 与模型训练集成import torch import torch.nn as nn import torch.optim as optim model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 200) # 200个类别 criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9) for epoch in range(10): model.train() for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() val_loss 0.0 correct 0 with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() _, preds torch.max(outputs, 1) correct torch.sum(preds labels.data) print(fEpoch {epoch}: Val Loss {val_loss/len(val_loader):.4f}, fAcc {correct.double()/len(val_dataset):.4f})6. 性能优化技巧在实际项目中我发现以下几个优化点能显著提升数据加载效率预读取标签在__init__中一次性读取所有标签避免每次__getitem__都访问文件系统缓存机制对已经加载的图像进行缓存特别当使用复杂变换时使用内存映射对于超大batch size考虑使用torch.utils.data.Dataset的pin_memory选项并行加载合理设置num_workers通常为CPU核心数的2-4倍修改后的__init__方法可以加入缓存支持def __init__(self, root_dir, trainTrue, transformNone, cacheFalse): self.cache cache self.cached_images {} # 缓存字典 def __getitem__(self, idx): if idx in self.cached_images: return self.cached_images[idx] img_path self.image_paths[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) if self.cache: self.cached_images[idx] (image, self.labels[idx]) return image, self.labels[idx]7. 常见问题排查在实现过程中我遇到过几个典型问题标签不对齐验证集的标签顺序必须与训练集完全一致。解决方案是在处理验证集时使用与训练集相同的class_to_idx映射图片损坏极少数JPEG文件可能损坏。可以增加错误处理def __getitem__(self, idx): try: image Image.open(self.image_paths[idx]).convert(RGB) except: # 返回随机图像或跳过 return self[(idx 1) % len(self)]内存不足当使用过大batch size时可能出现。可以减小batch size使用梯度累积尝试更小的图像尺寸数据不均衡某些类别样本可能偏少。可以通过采样策略解决from torch.utils.data.sampler import WeightedRandomSampler class_counts np.bincount(train_dataset.labels) weights 1. / class_counts[train_dataset.labels] sampler WeightedRandomSampler(weights, len(weights)) balanced_loader DataLoader(train_dataset, batch_size64, samplersampler, num_workers4)这套方案在我参与的多个图像分类项目中表现稳定特别是在资源有限需要快速迭代的场景下Tiny-ImageNet相比完整版ImageNet能节省大量训练时间。虽然初始的数据加载需要一些定制化工作但一旦封装好Dataset类后续使用就和标准数据集一样方便了。
数据集实战指南(二)——Tiny-ImageNet自定义加载与预处理
1. Tiny-ImageNet数据集初探第一次接触Tiny-ImageNet数据集时我完全被它混乱的目录结构搞懵了。这个由斯坦福CS231N课程推出的经典数据集虽然只有ImageNet的迷你版200个类别每类500张训练图片但在实际使用中却比想象中麻烦得多。最让人头疼的是它的训练集和验证集采用了完全不同的存储方式——训练集按类别分文件夹存放验证集却把所有图片堆在一个目录里这种设计让直接用PyTorch的ImageLoader变得不可能。记得当时为了赶项目进度我差点就放弃使用这个数据集了。好在经过一番折腾终于摸索出一套完整的解决方案。现在回想起来其实只要理解了数据组织逻辑再配合PyTorch的Dataset类进行定制化处理整个过程并没有那么可怕。下面我就把踩过的坑和最终方案完整分享给大家。2. 数据下载与目录解构2.1 获取数据集官方下载链接http://cs231n.stanford.edu/tiny-imagenet-200.zip依然有效不过我发现用wget命令下载会更稳定wget http://cs231n.stanford.edu/tiny-imagenet-200.zip -P ./data unzip ./data/tiny-imagenet-200.zip -d ./data解压后的目录结构是这样的tiny-imagenet-200/ ├── train/ │ ├── n01443537/ # 类别文件夹 │ │ ├── images/ # 实际图片存放处 │ │ │ ├── n01443537_0.JPEG │ │ │ └── ... │ │ └── n01443537_boxes.txt # 边界框标注 │ └── ... # 共200个这样的文件夹 ├── val/ │ ├── images/ # 所有验证图片混在一起 │ │ ├── val_0.JPEG │ │ └── ... │ └── val_annotations.txt # 验证集标注文件 ├── test/ # 无标签通常不用 │ └── images/ │ ├── test_0.JPEG │ └── ... ├── wnids.txt # 所有类别ID列表 └── words.txt # 类别ID到名称的映射2.2 关键文件解析wnids.txt包含200个WordNet ID如n01443537每行一个words.txt将WordNet ID映射到人类可读标签如goldfishval_annotations.txt验证集的图片与类别对应关系格式为val_0.JPEG n01443537 0 0 64 64各列分别是文件名、类别ID、边界框坐标(x,y,w,h)3. 自定义Dataset类实现3.1 基础框架搭建PyTorch的Dataset类需要实现三个核心方法__init__、__len__和__getitem__。我们先搭建骨架from torch.utils.data import Dataset from PIL import Image import os class TinyImageNetDataset(Dataset): def __init__(self, root_dir, trainTrue, transformNone): self.root_dir root_dir self.train train self.transform transform self.image_paths [] self.labels [] # 后续填充具体实现 self._prepare_data() def _prepare_data(self): 根据train/val模式准备数据路径和标签 pass def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] label self.labels[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label3.2 训练集处理实现训练集的处理相对简单因为目录结构清晰。我们需要遍历每个类别文件夹收集所有图片路径根据文件夹名确定类别标签def _prepare_data(self): if self.train: train_dir os.path.join(self.root_dir, train) # 获取所有类别文件夹 class_dirs [d for d in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, d))] class_dirs.sort() # 确保顺序一致 # 创建类别到索引的映射 self.class_to_idx {cls: i for i, cls in enumerate(class_dirs)} # 收集所有图片路径和标签 for cls in class_dirs: cls_dir os.path.join(train_dir, cls, images) for img_name in os.listdir(cls_dir): if img_name.endswith(.JPEG): self.image_paths.append(os.path.join(cls_dir, img_name)) self.labels.append(self.class_to_idx[cls])3.3 验证集处理实现验证集需要解析val_annotations.txt文件来建立图片到类别的映射else: # 验证集模式 val_dir os.path.join(self.root_dir, val) annotations_file os.path.join(val_dir, val_annotations.txt) # 先读取所有类别 with open(os.path.join(self.root_dir, wnids.txt), r) as f: classes [line.strip() for line in f] self.class_to_idx {cls: i for i, cls in enumerate(classes)} # 解析标注文件 img_to_class {} with open(annotations_file, r) as f: for line in f: parts line.strip().split(\t) img_to_class[parts[0]] parts[1] # 收集图片和标签 images_dir os.path.join(val_dir, images) for img_name in os.listdir(images_dir): if img_name.endswith(.JPEG) and img_name in img_to_class: self.image_paths.append(os.path.join(images_dir, img_name)) self.labels.append(self.class_to_idx[img_to_class[img_name]])4. 数据增强与标准化4.1 标准预处理流程对于ImageNet类数据集通常采用以下标准化参数from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(64), # Tiny-ImageNet图片尺寸 transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(64), transforms.CenterCrop(64), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])4.2 可视化检查技巧在正式训练前我强烈建议先可视化检查数据加载是否正确import matplotlib.pyplot as plt import numpy as np def imshow(inp, titleNone): 反标准化并显示图像 inp inp.numpy().transpose((1, 2, 0)) mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) inp std * inp mean inp np.clip(inp, 0, 1) plt.imshow(inp) if title: plt.title(title) plt.pause(0.001) # 测试显示 dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainTrue, transformtrain_transform) img, label dataset[0] imshow(img, titlefLabel: {label})5. 完整使用示例5.1 创建DataLoaderfrom torch.utils.data import DataLoader train_dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainTrue, transformtrain_transform) val_dataset TinyImageNetDataset(./data/tiny-imagenet-200, trainFalse, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4)5.2 与模型训练集成import torch import torch.nn as nn import torch.optim as optim model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 200) # 200个类别 criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9) for epoch in range(10): model.train() for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() val_loss 0.0 correct 0 with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() _, preds torch.max(outputs, 1) correct torch.sum(preds labels.data) print(fEpoch {epoch}: Val Loss {val_loss/len(val_loader):.4f}, fAcc {correct.double()/len(val_dataset):.4f})6. 性能优化技巧在实际项目中我发现以下几个优化点能显著提升数据加载效率预读取标签在__init__中一次性读取所有标签避免每次__getitem__都访问文件系统缓存机制对已经加载的图像进行缓存特别当使用复杂变换时使用内存映射对于超大batch size考虑使用torch.utils.data.Dataset的pin_memory选项并行加载合理设置num_workers通常为CPU核心数的2-4倍修改后的__init__方法可以加入缓存支持def __init__(self, root_dir, trainTrue, transformNone, cacheFalse): self.cache cache self.cached_images {} # 缓存字典 def __getitem__(self, idx): if idx in self.cached_images: return self.cached_images[idx] img_path self.image_paths[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) if self.cache: self.cached_images[idx] (image, self.labels[idx]) return image, self.labels[idx]7. 常见问题排查在实现过程中我遇到过几个典型问题标签不对齐验证集的标签顺序必须与训练集完全一致。解决方案是在处理验证集时使用与训练集相同的class_to_idx映射图片损坏极少数JPEG文件可能损坏。可以增加错误处理def __getitem__(self, idx): try: image Image.open(self.image_paths[idx]).convert(RGB) except: # 返回随机图像或跳过 return self[(idx 1) % len(self)]内存不足当使用过大batch size时可能出现。可以减小batch size使用梯度累积尝试更小的图像尺寸数据不均衡某些类别样本可能偏少。可以通过采样策略解决from torch.utils.data.sampler import WeightedRandomSampler class_counts np.bincount(train_dataset.labels) weights 1. / class_counts[train_dataset.labels] sampler WeightedRandomSampler(weights, len(weights)) balanced_loader DataLoader(train_dataset, batch_size64, samplersampler, num_workers4)这套方案在我参与的多个图像分类项目中表现稳定特别是在资源有限需要快速迭代的场景下Tiny-ImageNet相比完整版ImageNet能节省大量训练时间。虽然初始的数据加载需要一些定制化工作但一旦封装好Dataset类后续使用就和标准数据集一样方便了。