第 9 章 数据集与 DataLoader
学习目标
- 理解
Dataset:数据集的统一接口 - 掌握自定义
Dataset的三个方法:__init__、__len__、__getitem__ - 掌握
DataLoader的batch_size与shuffle - 掌握
TensorDataset与遍历 loader 得到 batch - 了解
transforms(ToTensor、Normalize)与图像张量的 CHW 形状
9.1 为什么需要 Dataset 和 DataLoader
第 8 章训练时把全部数据一次性塞给模型。真实场景数据大得多,两个问题随之而来:
- 内存装不下:需要按批(batch)分批喂给模型;
- 顺序不能固定:每一轮(epoch)应打乱顺序,避免模型学到样本顺序的假规律。
PyTorch 用两个组件解决:Dataset(数据集)负责「按编号取样本」,DataLoader(数据加载器)负责「分批、打乱、自动收集成张量」。
9.2 自定义 Dataset:三个方法
Dataset 是一个抽象接口,你写子类,实现三个方法:
__init__:存数据(通常转成张量);__len__:返回样本总数,让len(ds)可用;__getitem__(idx):返回第idx个样本,通常是一个(特征, 标签)元组。
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,按参数自动组织数据:
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 顺序不同。
打乱后顺序由随机种子决定,设置种子可复现:
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:
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 张量:
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,把数据中心化:
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 章循环里的「一次性喂全部数据」:
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 的损失取平均。这就是现代深度学习训练的标准结构。
动手实践
- 自定义一个
ScoresDataset,数据是 8 个成绩,__getitem__返回(成绩, 是否及格)(≥60 为 1),实现三个方法并打印len与ds[0]。 - 把上面的数据集放进
DataLoader(batch_size=3, shuffle=False),打印每批的形状;验证最后一批样本数小于 3。 - 用
TensorDataset装x = torch.arange(20).reshape(-1, 1)和y = x * 3,batch_size=5、shuffle=True,打印批数与前两批的标签。 - 用
ToTensor把一张(64, 64, 3)的 NumPy 图片转成张量,验证形状是(3, 64, 64)且值在[0, 1]。 - 运行 9.6 的完整训练,把
batch_size改成 100(等于全量)和 5,对比每个 epoch 耗时与损失变化。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
__getitem__ 返回形状不一致的样本 | RuntimeError: stack expects each tensor to be equal size | DataLoader 要按批摞起来,同批样本形状必须一致 |
忘了 __len__ | TypeError 或 len() 报错 | 三方法缺一不可 |
训练时 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 ndarray | ToTensor 接收 PIL/NumPy,不接收 torch 张量 |
章末练习
基础
- 实现
NumbersDataset:装torch.arange(10),__getitem__返回(n, n ** 2),打印len(ds)和ds[3]。 - 用
DataLoader(batch_size=4, shuffle=False)加载上面的数据集,打印批数(应为 3)与每批形状。 - 用
TensorDataset+DataLoader把 50 个样本分成每批 8 个,打印批数(应为 7)。
提高
- 解释
Dataset与DataLoader的分工,并说明为什么训练要shuffle=True而验证不需要。 - 写一个
DataLoader循环,统计一个 epoch 内「最后一批的样本数」,并用batch_size=7、样本数 50 验证你的计算。
挑战
- 自定义一个带变换的
Dataset:__getitem__里把样本归一化到[0, 1]再返回(自己实现(x - min) / (max - min)),用DataLoader加载并验证所有 batch 的值域。 - 不运行代码,预测:
DataLoader(ds, batch_size=10, shuffle=True)配合torch.manual_seed(0)时,第一个 batch 与第二个 batch 是否有交集?运行后解释 shuffle 的机制(提示:每个 epoch 重新洗牌)。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 自定义
Dataset必须实现哪三个方法?- A.
__init__、__iter__、__next__ - B.
__init__、__len__、__getitem__ - C.
__load__、__batch__、__save__ - D.
__init__、__size__、__data__
- A.
len(ds)依赖哪个方法?- A.
__getitem__ - B.
__len__ - C.
__iter__ - D.
__size__
- A.
DataLoader(ds, batch_size=4)处理 10 个样本,会产生几批?- A. 2
- B. 3
- C. 4
- D. 10
- 训练时
DataLoader应设置?- A.
shuffle=True - B.
shuffle=False - C.
shuffle无影响 - D. 与验证集相同
- A.
next(iter(loader))返回的是?- A. 全部数据
- B. 第一批数据
- C. 迭代器本身
- D. 最后一个 batch
- 图像张量的标准形状约定是?
- A.
H×W×C - B.
C×H×W - C.
W×H×C - D.
H×C×W
- A.
ToTensor对[0, 255]的 uint8 图片做了什么?- A. 转成 float32 并缩放到
[0, 1] - B. 保持整数不变
- C. 转成 float64
- D. 直接报错
- A. 转成 float32 并缩放到
Normalize(mean, std)的计算是?- A.
x * mean + std - B.
(x - mean) / std - C.
x / mean - D.
x - std
- A.
TensorDataset(x, y)要求?- A. x、y 形状完全一样
- B. x、y 第一维长度相同
- C. x、y 都是列表
- D. 任意形状
- 训练循环中,内层 for 遍历的是?
- A. epoch
- B. batch
- C. 样本特征
- D. 损失函数
