PyTorch 自定义 Dataset 与 DataLoader 详解:从 TXT 标注文件读取图像分类数据
前言:为什么不能一直依赖 ImageFolder?
在 PyTorch 入门阶段,我们经常使用自带的 torchvision.datasets.ImageFolder 来加载数据。但它有一个极其严苛的前提条件:数据必须按类别分好子文件夹存放。
但在实际的工程项目或开源数据集中(如图像分类、目标检测等),数据往往是**“大杂烩”**形式的:几千上万张图片全部混放在一个大文件夹中,另附一个标注文件(如 train.txt 或 val.json)。
面对这种“一图一表”的结构,ImageFolder 就不再适用了。此时,我们需要根据标注文件手写自己的 Dataset。本文将以花卉分类任务为例,带你从零手写自定义的 Dataset 与 DataLoader。
一、 数据集目录结构与标注格式
在开始写代码前,我们先明确一下常见的数据集摆放格式。假设我们的花卉数据集(flower_data)结构如下:
flower_data/
├── train_images/ # 所有训练集图片都混放在这里
│ ├── image_001.jpg
│ ├── image_002.jpg
│ └── ...
├── train.txt # 训练集标注文件
└── val.txt # 验证集标注文件
打开 train.txt,里面记录了每张图片的名称和它对应的类别 ID(以空格分隔),格式如下:
image_001.jpg 0
image_002.jpg 1
image_003.jpg 2
...
我们接下来的目标,就是让程序自动解析这个 TXT 文件,并把图片送入深度学习的流水线中。

二、 DataLoader 的核心运转逻辑

在写代码之前,我们需要先理解 DataLoader 是如何工作的。可以将它想象成一个“大型京东物流仓库”:
-
洗牌(Shuffle):训练开始前,先把所有货品(数据序列)的顺序打乱。
-
按需打包(Batching):处于待命状态。当 GPU 算完一轮需要新数据时,它能迅速按设定的批次大小(如 Batch Size = 64),打包好 64 个数据送过去。
-
多进程提速(num_workers):GPU 计算非常快,如果每次都用单线程读取图片,就会出现“算力等硬盘”的尴尬局面。DataLoader 通过开启多个子进程预加载数据,完美解决了这个问题。
三、 第一步:解析标注文件,构建路径与标签 List
不论标注是 TXT 还是 JSON,进入 DataLoader 之前的终极目标,是在内存中构建出两个长度相等的 List(列表):
-
图片路径 List:存有每张图片的完整路径。
-
标签 List:存有每张图片对应的类别 ID。
代码实现:解析 TXT 标注
为了保持顺序并让逻辑最清晰,我们直接逐行读取 TXT,并将路径和标签追加到列表中:
import os
def load_annotations(txt_path, root_dir):
"""
解析 TXT 标注文件并返回图片完整路径列表和标签列表
TXT 格式示例: image_001.jpg 2
"""
img_paths =[]
labels =[]
with open(txt_path, 'r', encoding='utf-8') as f:
for line in f:
# 去除换行符并按空格切分
parts = line.strip().split()
if len(parts) != 2:
continue
img_name = parts[0]
label = int(parts[1])
# 补全实际路径 (关键易错点)
# TXT 中只有名字,程序无法直接读取,必须拼接前面的文件夹路径
img_path = os.path.join(root_dir, img_name)
img_paths.append(img_path)
labels.append(label)
return img_paths, labels
四、 第二步:重写 Dataset 核心类

有了这两个 List,我们需要继承 torch.utils.data.Dataset,并重写三个必备的魔法方法。这就相当于建立一个属于我们自己的“取货逻辑”。
-
__init__:接收目录、标注路径和预处理操作。在这里调用刚才的解析函数。
-
__len__:返回数据集总大小。
-
__getitem__:最核心的函数。每次 DataLoader 索要数据时,会传进一个随机的索引(如 idx=761),我们依据索引去读图、取标签、做预处理。
代码实现:自定义 Dataset
import torch
from torch.utils.data import Dataset
from PIL import Image
class CustomFlowerDataset(Dataset):
def __init__(self, root_dir, txt_path, transform=None):
self.root_dir = root_dir
self.transform = transform
# 调用辅助函数,生成两个核心 List
self.img_paths, self.labels = load_annotations(txt_path, root_dir)
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
# 1. 拿着索引去找图片的完整路径
img_path = self.img_paths[idx]
# 2. 用 PIL 读取图像读入内存
image = Image.open(img_path).convert('RGB')
# 3. 拿着相同的索引去取标签
label = self.labels[idx]
# 4. 执行数据增强与预处理 (Transforms)
if self.transform is not None:
image = self.transform(image)
# 5. 将标签转换为 LongTensor
label = torch.tensor(label, dtype=torch.long)
# 返回处理干净的一对数据 (图像张量, 标签张量)
return image, label
五、 第三步:封装 DataLoader 与可视化校验
代码写完绝对不能直接丢进模型训练,因为数据加载极易出现维度错误或预处理 Bug。必须先抽取一个 Batch 进行数据可视化校验!
⚠️ 避坑指南:我们在这里引入了 num_workers=4 以开启多进程加载。
如果在 Windows 系统下运行脚本,必须将 DataLoader 的执行与迭代代码放在 if __name__ == '__main__': 保护块下,否则会引发多进程递归报错。
如果电脑配置较低,或者运行环境是 Jupyter Notebook,建议先把 num_workers 设置为 0,确认代码无误后再逐步调大。
代码实现:实例化与测试校验(完整可运行版)
为了方便大家复现,这里提供整合后的完整版本,大家可以直接复制运行(确保准备好了对应的测试图片和 TXT 文件):
import os
import torch
from torch.utils.data import Dataset, DataLoader
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import numpy as np
# ================= 1. 辅助函数 =================
def load_annotations(txt_path, root_dir):
img_paths = []
labels =[]
with open(txt_path, 'r', encoding='utf-8') as f:
for line in f:
parts = line.strip().split()
if len(parts) != 2:
continue
img_name = parts[0]
label = int(parts[1])
img_path = os.path.join(root_dir, img_name)
img_paths.append(img_path)
labels.append(label)
return img_paths, labels
# ================= 2. Dataset 类 =================
class CustomFlowerDataset(Dataset):
def __init__(self, root_dir, txt_path, transform=None):
self.root_dir = root_dir
self.transform = transform
self.img_paths, self.labels = load_annotations(txt_path, root_dir)
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
img_path = self.img_paths[idx]
image = Image.open(img_path).convert('RGB')
label = self.labels[idx]
if self.transform is not None:
image = self.transform(image)
label = torch.tensor(label, dtype=torch.long)
return image, label
# ================= 3. 执行与校验 =================
if __name__ == '__main__':
# 定义预处理: Resize、ToTensor()转为[0,1]、Normalize()做ImageNet标准化
data_transforms = transforms.Compose([
transforms.Resize((64, 64)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
# 实例化 Dataset
train_dataset = CustomFlowerDataset(
root_dir='./flower_data/train_images',
txt_path='./flower_data/train.txt',
transform=data_transforms
)
# 实例化 DataLoader (Windows 下必须在 __main__ 中运行)
train_loader = DataLoader(
train_dataset,
batch_size=64,
shuffle=True,
num_workers=4 # 开启 4 个子进程预加载数据
)
# 抽取一个 Batch 的数据进行校验
inputs, labels = next(iter(train_loader))
print(f"Batch 数据形状: {inputs.shape}") # 预期:[64, 3, 64, 64]
print(f"Batch 标签形状: {labels.shape}") # 预期:[64]
# 取出第一张图片进行反向处理展示
img_tensor = inputs[0]
# 维度变换: PyTorch [C, H, W] -> matplotlib[H, W, C]
img_np = img_tensor.cpu().numpy().transpose((1, 2, 0))
# 逆标准化: X_原图 = X_tensor * std + mean
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
img_np = std * img_np + mean
img_np = np.clip(img_np, 0, 1) # 限制像素范围在 0-1 之间
# 画图与标签核对
plt.imshow(img_np)
plt.title(f"Label ID: {labels[0].item()}")
plt.axis('off')
plt.show()
总结
通过以上步骤,我们彻底打通了自定义数据集读取的任督二脉。本质上,构建自定义数据流就是完成了一次数据解耦:
-
解析函数:负责地址与标签映射。
-
Dataset:负责单张读取与预处理。
-
DataLoader:负责多进程打包与分发。
掌握了这一套完整模板,以后无论面对的是 JSON 格式的检测框、PNG 格式的语义分割掩码,还是文本序列数据,只要修改解析函数提取正确的标签,就能写出适配复杂项目的数据流水线!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)