Epoch、Batch 与 DataLoader

很多刚开始阅读 PyTorch 推荐系统训练代码的同学,都会卡在一组名词:

epochbatchDataLoadershuffleloss.backward()optimizer.step()testHRNDCG

单独看每个概念不难,但放进训练循环里,很容易分不清整条流水线:

数据从哪里读取?如何切分成 batch?模型什么时候更新参数?测试阶段会不会偷偷修改权重?

本文用最简单的数字样本,完整拆解整条训练链路。

✅ 核心结论

  1. 一个 epoch = 完整遍历一遍全部训练数据集。
  2. 单个 epoch 内部,循环读取多个 batch
  3. 训练阶段 ,每拿到一个 batch,标准流程:
    前向传播 → 计算损失 → 清空梯度 → 反向传播 → 更新模型参数。
  4. verbose 用来控制:每隔多少个 epoch,执行日志打印/离线评估。
  5. 测试评估阶段 :仅做预测、计算指标(HR@K、NDCG@K),不会更新模型参数
  6. shuffle=True 只改变样本读取顺序,不会修改 Dataset 内部原始数据。
  7. shuffle=True每一轮epoch开始,重新生成随机读取顺序
  8. shuffle=False:所有epoch读取样本顺序完全固定。

一句话概括分工:

Dataset 管理「有哪些数据」;DataLoader 管理「以什么顺序、多少条一组取出数据」;训练循环控制「拿到batch后如何迭代优化模型」。


🔢 极简数字数据集演示

假设我们一共有10条训练样本,使用索引编号代表样本:

复制代码
原始数据集索引:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

参数设置:

python 复制代码
batch_size = 4

含义:一次性取出4条样本,封装为1个batch。

场景1:shuffle=False(不打乱顺序)

数据会按原生索引切分batch:

复制代码
第1个 batch: [0, 1, 2, 3]
第2个 batch: [4, 5, 6, 7]
第3个 batch: [8, 9]

总数10无法被4整除,最后一组为不完整batch。

如果开启 drop_last=True,最后这个不完整batch [8, 9] 会直接丢弃。

shuffle=False:所有epoch顺序固定

读取索引永远不变,连续两轮epoch批次完全相同:

复制代码
epoch 1:
batch 1: [0, 1, 2, 3]
batch 2: [4, 5, 6, 7]
batch 3: [8, 9]

epoch 2:
batch 1: [0, 1, 2, 3]
batch 2: [4, 5, 6, 7]
batch 3: [8, 9]

场景2:shuffle=True(开启打乱)

❗重点:打乱的是索引读取顺序 ,原始Dataset内的数据本身保持不变。

原始数据依旧是 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

关键点总结:

shuffle=True ≠ 永久修改数据;

每一轮epoch启动时,DataLoader重新规划本轮样本读取次序。


📦 DataLoader 到底如何生成一个 batch?

这是新手最容易混淆的环节。我们拆成完整5步理解。

自定义数据集模板:

python 复制代码
class NumberDataset(torch.utils.data.Dataset):
    def __init__(self):
        self.samples = [
            {"idx": 0, "feature": [0.0, 0.5], "label": 0},
            {"idx": 1, "feature": [1.0, 1.5], "label": 1},
            {"idx": 2, "feature": [2.0, 2.5], "label": 0},
            # ...更多样本
        ]

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

    def __getitem__(self, index):
        # 根据索引返回【单条样本】
        return self.samples[index]

训练循环代码:

python 复制代码
for batch in data_loader:
    ...

循环背后完整流程:

第1步:生成索引顺序

  • shuffle=False:原生索引 [0,1,2,3,...]
  • shuffle=True:随机打乱索引序列

注意:数字只是样本索引,不是样本内容。后续会调用 dataset[index] 获取单条数据。

第2步:按batch_size分组索引

索引序列 [3, 7, 1, 9, 0, 6, 2, 8, 4, 5]

batch_size=4 → 分组:

[3,7,1,9][0,6,2,8][4,5]

当前仅规划索引,还没有读取真实样本

第3步:逐条调用 Dataset.__getitem__

取索引组 [3,7,1,9]

依次执行:

python 复制代码
sample_1 = dataset[3]
sample_2 = dataset[7]
sample_3 = dataset[1]
sample_4 = dataset[9]

得到4条独立样本字典:

python 复制代码
[
    {"idx": 3, "feature": [3.0, 3.5], "label": 1},
    {"idx": 7, "feature": [7.0, 7.5], "label": 1},
    {"idx": 1, "feature": [1.0, 1.5], "label": 1},
    {"idx": 9, "feature": [9.0, 9.5], "label": 1},
]

第4步:collate_fn 打包,拼接成batch张量

默认 default_collate 负责打包:把多条样本同字段堆叠

打包完成后:

python 复制代码
{
    "idx": tensor([3, 7, 1, 9]),
    "feature": tensor([
        [3.0, 3.5],
        [7.0, 7.5],
        [1.0, 1.5],
        [9.0, 9.5],
    ]),
    "label": tensor([1, 1, 1, 1]),
}

这就是训练循环拿到的 batch。数据类型本质上是:

python 复制代码
dict[str, torch.Tensor]

通俗理解:

Dataset 产出一条条独立样本;collate_fn 打包员,把多条样本组装成一个batch张量。

第5步:训练循环接收batch,送入模型

外部代码直接使用:

python 复制代码
for batch in train_loader:
    features = batch["feature"]
    labels = batch["label"]
    scores = model(features)

整条链路简化:

生成索引顺序 → 索引分组 → __getitem__逐条读取样本 → collate_fn拼接batch → 返回循环


🔁 完整训练循环执行流程

示例标准训练代码:

python 复制代码
epochs = 3
batch_size = 4
verbose = 2

for epoch in range(1, epochs + 1):
    model.train()
    # 内层循环遍历所有batch
    for batch in train_loader:
        features = batch["feature"]
        labels = batch["label"]
        scores = model(features)
        loss = loss_fn(scores, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    # 间隔verbose个epoch执行测试
    if epoch % verbose == 0:
        evaluate(model, test_loader)

流程翻译:

  • Epoch 1:完整遍历训练集,所有batch更新参数;1%2≠0不测试
  • Epoch 2:完整遍历训练集,所有batch更新参数;2%2=0执行测试评估
  • Epoch 3:完整遍历训练集,所有batch更新参数;3%2≠0不测试

单个batch内部标准训练闭环

python 复制代码
model.train()          # 切换训练模式(Dropout/BatchNorm生效)
scores = model(features)# 前向传播,得到预测值
loss = loss_fn(scores, labels) # 计算损失
optimizer.zero_grad()  # 清空上一轮梯度
loss.backward()        # 反向传播,计算参数梯度
optimizer.step()       # 使用梯度更新模型权重

⚖️ 训练阶段 VS 测试评估阶段

阶段 是否计算梯度 是否反向传播 是否更新参数 核心目的
训练阶段 ✅ 是 ✅ 是 ✅ 是 迭代优化模型
测试评估 ❌ 否 ❌ 否 ❌ 否 观测模型泛化效果

训练代码模板

python 复制代码
model.train()
for batch in train_loader:
    scores = model(batch["feature"])
    loss = loss_fn(scores, batch["label"])
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

测试评估模板

python 复制代码
model.eval()
with torch.no_grad(): # 关闭梯度计算,节省显存
    for batch in test_loader:
        scores = model(batch["feature"])
        # 根据预测分数计算HR、NDCG指标

torch.no_grad():告知PyTorch仅推理,不需要构建梯度计算图。


📊 推荐系统指标:HR@K 和 NDCG@K

推荐任务离线评估最常用两个指标:

HR@K(Hit Ratio,命中率)

含义:给用户推荐Top-K物品,真实交互物品是否出现在推荐列表内

  • 命中:HR=1
  • 未命中:HR=0

示例:

推荐Top5列表:[item_8, item_2, item_6, item_1, item_9]

用户真实喜爱物品:item_6

item_6 在列表中 → HR@5 = 1。

NDCG@K

HR只关心「有没有命中」;NDCG额外关注命中物品的排序位置

案例:真实物品为 item_6

  • 推荐A:[item_6, item_2, item_8, item_1, item_9](命中排在第1位)
  • 推荐B:[item_8, item_2, item_1, item_9, item_6](命中排在第5位)

两者HR都等于1,但推荐A的NDCG更高。

简单理解:

HR:是否猜中;NDCG:猜中之后,排得够不够靠前。


🛠️ 可直接运行的演示代码

复制运行,直观观察 __getitem__ 的调用时机:

python 复制代码
import torch
from torch.utils.data import Dataset, DataLoader


class NumberDataset(Dataset):
    def __init__(self):
        self.samples = []
        for idx in range(10):
            self.samples.append({
                "idx": idx,
                "feature": torch.tensor([float(idx), float(idx) + 0.5]),
                "label": torch.tensor(idx % 2, dtype=torch.float32),
            })

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

    def __getitem__(self, index):
        print(f"  __getitem__ 被调用,index={index}")
        return self.samples[index]


dataset = NumberDataset()
loader = DataLoader(dataset, batch_size=4, shuffle=False)

for batch_id, batch in enumerate(loader, start=1):
    print(f"\n第 {batch_id} 个 batch")
    print("idx:", batch["idx"])
    print("feature:", batch["feature"])
    print("label:", batch["label"])

运行输出可以验证:

  1. DataLoader 逐个调用 __getitem__ 获取单条样本
  2. 收集足够样本后,collate_fn自动合并为batch张量
  3. 训练循环拿到的是封装完成的批量数据,而非单样本

💡 收尾总结

看到推荐系统经典双层循环代码:

python 复制代码
for epoch in range(epochs):
    for batch in train_loader:
        ...

脑海中自动翻译:

外层epoch循环:控制一共完整训练多少轮;

内层batch循环:一轮训练中分批读取数据;

每一个训练batch都会更新模型参数;

到达指定epoch间隔,进入评估模式,只预测、计算指标,不更新权重。

打通这套流程后,再阅读 BPR、NeuMF、LightGCN 等推荐模型训练代码,理解门槛会大幅降低。

相关推荐
Cachel wood1 小时前
hands-on-modern-rl:动手学强化学习策略梯度reinforce
开发语言·python
蓝斯4972 小时前
一碰即传,重构跨设备文件分享体验
开发语言·python·重构
宁风NF2 小时前
JavaScript:内存、垃圾回收、性能优化
开发语言·前端·javascript·学习·性能优化·es6
ShuiShenHuoLe2 小时前
Go html/template 使用入门
开发语言·golang·html
geovindu2 小时前
java: Gale-Shapley Algorithm
java·开发语言·后端·算法
tangjunjun-owen2 小时前
YOLOv6 五大核心创新点深度解读:从训练到推理的完整剖析
人工智能·深度学习·yolov6
冻柠檬飞冰走茶3 小时前
PTA基础编程题目集 7-34 通讯录的录入与显示(C语言实现)
c语言·开发语言·数据结构·算法
星核0penstarry3 小时前
DeepSeek-V4-Flash 正式公测:大模型行业进入「极速平价普惠时代」
java·开发语言·人工智能
老洋葱Mr_Onion3 小时前
【C++】高精度模板
开发语言·c++·算法