Skip to content

第 9 章 数据集与 DataLoader

学习目标

  • 理解 Dataset:数据集的统一接口
  • 掌握自定义 Dataset 的三个方法:__init____len____getitem__
  • 掌握 DataLoaderbatch_sizeshuffle
  • 掌握 TensorDataset 与遍历 loader 得到 batch
  • 了解 transforms(ToTensor、Normalize)与图像张量的 CHW 形状

9.1 为什么需要 Dataset 和 DataLoader

第 8 章训练时把全部数据一次性塞给模型。真实场景数据大得多,两个问题随之而来:

  1. 内存装不下:需要按批(batch)分批喂给模型;
  2. 顺序不能固定:每一轮(epoch)应打乱顺序,避免模型学到样本顺序的假规律。

PyTorch 用两个组件解决:Dataset(数据集)负责「按编号取样本」,DataLoader(数据加载器)负责「分批、打乱、自动收集成张量」。

9.2 自定义 Dataset:三个方法

Dataset 是一个抽象接口,你写子类,实现三个方法:

  • __init__:存数据(通常转成张量);
  • __len__:返回样本总数,让 len(ds) 可用;
  • __getitem__(idx):返回第 idx 个样本,通常是一个 (特征, 标签) 元组。
python
import torch
from torch.utils.data import Dataset

class WeatherDataset(Dataset):
    """温度样本:特征 t,标签 2t。"""
    def __init__(self, temps):
        self.temps = torch.tensor(temps, dtype=torch.float32)

    def __len__(self):
        return len(self.temps)

    def __getitem__(self, idx):
        t = self.temps[idx]
        return t, t * 2.0

ds = WeatherDataset([15.0, 18.0, 22.0, 25.0, 20.0])
print("样本数:", len(ds))
print("第 0 个样本:", ds[0])

输出:

样本数: 5
第 0 个样本: (tensor(15.), tensor(30.))

ds[0] 返回元组 (特征, 标签),特征 15、标签 30。

9.3 DataLoader:分批与打乱

DataLoader 接收一个 Dataset,按参数自动组织数据:

python
import torch
from torch.utils.data import DataLoader

loader = DataLoader(ds, batch_size=2, shuffle=False)
for batch_x, batch_y in loader:
    print("特征批:", batch_x)
    print("标签批:", batch_y)

输出:

特征批: tensor([15., 18.])
标签批: tensor([30., 36.])
特征批: tensor([22., 25.])
标签批: tensor([44., 50.])
特征批: tensor([20.])
标签批: tensor([40.])

解读:

  • batch_size=2:每批 2 个样本;最后一批只剩 1 个,不会丢弃;
  • 单个样本是标量张量,批是形状 (2,) 的张量——DataLoader 自动把同形状样本「摞」成批;
  • shuffle=False:按原顺序;训练时改成 True,每个 epoch 顺序不同。

打乱后顺序由随机种子决定,设置种子可复现:

python
torch.manual_seed(0)
loader = DataLoader(ds, batch_size=2, shuffle=True)
for batch_x, batch_y in loader:
    print(batch_x)

输出(与随机种子对应,不同版本可能有差异):

tensor([18., 20.])
tensor([22., 25.])
tensor([15.])

9.4 TensorDataset:现成的张量数据集

数据已经在两个张量里(x 特征、y 标签)时,不必写子类,用 TensorDataset:

python
import torch
from torch.utils.data import TensorDataset, DataLoader

torch.manual_seed(0)
x = torch.linspace(0, 10, 100).reshape(-1, 1)
y = 2 * x + 1
ds = TensorDataset(x, y)
loader = DataLoader(ds, batch_size=10, shuffle=True)

print("批数:", len(loader))
batch_x, batch_y = next(iter(loader))
print("特征批形状:", batch_x.shape)
print("标签批形状:", batch_y.shape)
print("第一批特征:", batch_x.reshape(-1)[:5])

输出:

批数: 10
特征批形状: torch.Size([10, 1])
标签批形状: torch.Size([10, 1])
第一批特征: tensor([2.6263, 8.8889, 5.9596, 5.8586, 7.3737])

100 个样本、每批 10 个 → 10 批。next(iter(loader)) 是「取第一批」的常用写法。每批特征形状 (10, 1),正好喂给第 8 章的 Linear(1, 1)

9.5 数据变换 transforms

真实数据常常需要预处理(归一化、转张量、增强)。torchvision.transforms 提供常用变换,重点掌握两个:

ToTensor:把 PIL 图片或 NumPy 数组(形状 H×W×C,值 0~255)转成形状 C×H×W、值 0~1 的 float32 张量:

python
import numpy as np
from torchvision import transforms

img = np.random.randint(0, 256, (32, 32, 3), dtype=np.uint8)
to_tensor = transforms.ToTensor()
t = to_tensor(img)
print("形状:", t.shape, "dtype:", t.dtype)
print("范围:", t.min().item(), "~", t.max().item())

输出:

形状: torch.Size([3, 32, 32]) dtype: torch.float32
范围: 0.0 ~ 1.0

注意通道维跑到最前面:图像张量统一用 C×H×W(通道、高、宽),深度学习约定俗成;NumPy 图片是 H×W×C。这个区别在卷积章节(第 10 章)会反复出现。

Normalize(mean, std):逐通道做 (x - mean) / std,把数据中心化:

python
norm = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
tn = norm(t)
print("归一化后范围:", round(tn.min().item(), 4), "~", round(tn.max().item(), 4))

输出:

归一化后范围: -1.0 ~ 1.0

数据从 [0, 1] 变成 [-1, 1],数值分布更利于训练。

9.6 训练循环如何用 loader

DataLoader 直接替换第 8 章循环里的「一次性喂全部数据」:

python
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader

torch.manual_seed(0)
x = torch.linspace(0, 10, 100).reshape(-1, 1)
y = 2 * x + 1 + 0.1 * torch.randn_like(x)

ds = TensorDataset(x, y)
loader = DataLoader(ds, batch_size=10, shuffle=True)

model = nn.Linear(1, 1)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

for epoch in range(20):
    total_loss = 0.0
    for batch_x, batch_y in loader:
        pred = model(batch_x)
        loss = loss_fn(pred, batch_y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    if epoch % 5 == 0:
        print(f"epoch {epoch}: avg loss = {total_loss / len(loader):.6f}")

输出:

epoch 0: avg loss = 20.954975
epoch 5: avg loss = 0.064353
epoch 10: avg loss = 0.044226
epoch 15: avg loss = 0.030995

外层循环是 epoch(整轮),内层循环是 batch。每个 batch 都走一遍「五步」,一个 epoch 的损失取平均。这就是现代深度学习训练的标准结构。

动手实践

  1. 自定义一个 ScoresDataset,数据是 8 个成绩,__getitem__ 返回 (成绩, 是否及格)(≥60 为 1),实现三个方法并打印 lends[0]
  2. 把上面的数据集放进 DataLoader(batch_size=3, shuffle=False),打印每批的形状;验证最后一批样本数小于 3。
  3. TensorDatasetx = torch.arange(20).reshape(-1, 1)y = x * 3,batch_size=5shuffle=True,打印批数与前两批的标签。
  4. ToTensor 把一张 (64, 64, 3) 的 NumPy 图片转成张量,验证形状是 (3, 64, 64) 且值在 [0, 1]
  5. 运行 9.6 的完整训练,把 batch_size 改成 100(等于全量)和 5,对比每个 epoch 耗时与损失变化。

常见错误

错误写法现象原因
__getitem__ 返回形状不一致的样本RuntimeError: stack expects each tensor to be equal sizeDataLoader 要按批摞起来,同批样本形状必须一致
忘了 __len__TypeErrorlen() 报错三方法缺一不可
训练时 shuffle=False模型学到样本顺序的假规律,泛化差训练集 shuffle=True,验证/测试集才用 False
next(iter(loader)) 用法混淆拿到的是「数据迭代器」而不是数据iter(loader) 返回迭代器,next 才取第一批
H×W×C 的 NumPy 图片直接喂 CNN通道维在最后,形状不符图像张量约定 C×H×W,用 ToTensor 转换
ToTensor 直接传 torch 张量TypeError: pic should be PIL Image or ndarrayToTensor 接收 PIL/NumPy,不接收 torch 张量

章末练习

基础

  1. 实现 NumbersDataset:装 torch.arange(10),__getitem__ 返回 (n, n ** 2),打印 len(ds)ds[3]
  2. DataLoader(batch_size=4, shuffle=False) 加载上面的数据集,打印批数(应为 3)与每批形状。
  3. TensorDataset + DataLoader 把 50 个样本分成每批 8 个,打印批数(应为 7)。

提高

  1. 解释 DatasetDataLoader 的分工,并说明为什么训练要 shuffle=True 而验证不需要。
  2. 写一个 DataLoader 循环,统计一个 epoch 内「最后一批的样本数」,并用 batch_size=7、样本数 50 验证你的计算。

挑战

  1. 自定义一个带变换的 Dataset:__getitem__ 里把样本归一化到 [0, 1] 再返回(自己实现 (x - min) / (max - min)),用 DataLoader 加载并验证所有 batch 的值域。
  2. 不运行代码,预测:DataLoader(ds, batch_size=10, shuffle=True) 配合 torch.manual_seed(0) 时,第一个 batch 与第二个 batch 是否有交集?运行后解释 shuffle 的机制(提示:每个 epoch 重新洗牌)。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. 自定义 Dataset 必须实现哪三个方法?
    • A. __init____iter____next__
    • B. __init____len____getitem__
    • C. __load____batch____save__
    • D. __init____size____data__
  2. len(ds) 依赖哪个方法?
    • A. __getitem__
    • B. __len__
    • C. __iter__
    • D. __size__
  3. DataLoader(ds, batch_size=4) 处理 10 个样本,会产生几批?
    • A. 2
    • B. 3
    • C. 4
    • D. 10
  4. 训练时 DataLoader 应设置?
    • A. shuffle=True
    • B. shuffle=False
    • C. shuffle 无影响
    • D. 与验证集相同
  5. next(iter(loader)) 返回的是?
    • A. 全部数据
    • B. 第一批数据
    • C. 迭代器本身
    • D. 最后一个 batch
  6. 图像张量的标准形状约定是?
    • A. H×W×C
    • B. C×H×W
    • C. W×H×C
    • D. H×C×W
  7. ToTensor[0, 255] 的 uint8 图片做了什么?
    • A. 转成 float32 并缩放到 [0, 1]
    • B. 保持整数不变
    • C. 转成 float64
    • D. 直接报错
  8. Normalize(mean, std) 的计算是?
    • A. x * mean + std
    • B. (x - mean) / std
    • C. x / mean
    • D. x - std
  9. TensorDataset(x, y) 要求?
    • A. x、y 形状完全一样
    • B. x、y 第一维长度相同
    • C. x、y 都是列表
    • D. 任意形状
  10. 训练循环中,内层 for 遍历的是?
    • A. epoch
    • B. batch
    • C. 样本特征
    • D. 损失函数