Epoch、Batch 与 DataLoader 很多刚开始阅读 PyTorch 推荐系统训练代码的同学都会卡在一组名词epoch、batch、DataLoader、shuffle、loss.backward()、optimizer.step()、test、HR、NDCG。单独看每个概念不难但放进训练循环里很容易分不清整条流水线数据从哪里读取如何切分成 batch模型什么时候更新参数测试阶段会不会偷偷修改权重本文用最简单的数字样本完整拆解整条训练链路。✅ 核心结论一个epoch 完整遍历一遍全部训练数据集。单个epoch内部循环读取多个batch。训练阶段每拿到一个 batch标准流程前向传播 → 计算损失 → 清空梯度 → 反向传播 → 更新模型参数。verbose用来控制每隔多少个 epoch执行日志打印/离线评估。测试评估阶段仅做预测、计算指标HRK、NDCGK不会更新模型参数。shuffleTrue只改变样本读取顺序不会修改 Dataset 内部原始数据。shuffleTrue每一轮epoch开始重新生成随机读取顺序。shuffleFalse所有epoch读取样本顺序完全固定。一句话概括分工Dataset管理「有哪些数据」DataLoader管理「以什么顺序、多少条一组取出数据」训练循环控制「拿到batch后如何迭代优化模型」。 极简数字数据集演示假设我们一共有10条训练样本使用索引编号代表样本原始数据集索引[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]参数设置batch_size4含义一次性取出4条样本封装为1个batch。场景1shuffleFalse不打乱顺序数据会按原生索引切分batch第1个 batch: [0, 1, 2, 3] 第2个 batch: [4, 5, 6, 7] 第3个 batch: [8, 9]总数10无法被4整除最后一组为不完整batch。如果开启drop_lastTrue最后这个不完整batch[8, 9]会直接丢弃。shuffleFalse所有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]场景2shuffleTrue开启打乱❗重点打乱的是索引读取顺序原始Dataset内的数据本身保持不变。原始数据依旧是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]关键点总结shuffleTrue≠ 永久修改数据每一轮epoch启动时DataLoader重新规划本轮样本读取次序。 DataLoader 到底如何生成一个 batch这是新手最容易混淆的环节。我们拆成完整5步理解。自定义数据集模板classNumberDataset(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):returnlen(self.samples)def__getitem__(self,index):# 根据索引返回【单条样本】returnself.samples[index]训练循环代码forbatchindata_loader:...循环背后完整流程第1步生成索引顺序shuffleFalse原生索引[0,1,2,3,...]shuffleTrue随机打乱索引序列注意数字只是样本索引不是样本内容。后续会调用dataset[index]获取单条数据。第2步按batch_size分组索引索引序列[3, 7, 1, 9, 0, 6, 2, 8, 4, 5]batch_size4 → 分组[3,7,1,9]、[0,6,2,8]、[4,5]当前仅规划索引还没有读取真实样本。第3步逐条调用Dataset.__getitem__取索引组[3,7,1,9]依次执行sample_1dataset[3]sample_2dataset[7]sample_3dataset[1]sample_4dataset[9]得到4条独立样本字典[{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负责打包把多条样本同字段堆叠。打包完成后{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。数据类型本质上是dict[str,torch.Tensor]通俗理解Dataset产出一条条独立样本collate_fn打包员把多条样本组装成一个batch张量。第5步训练循环接收batch送入模型外部代码直接使用forbatchintrain_loader:featuresbatch[feature]labelsbatch[label]scoresmodel(features)整条链路简化生成索引顺序 → 索引分组 → __getitem__逐条读取样本 → collate_fn拼接batch → 返回循环 完整训练循环执行流程示例标准训练代码epochs3batch_size4verbose2forepochinrange(1,epochs1):model.train()# 内层循环遍历所有batchforbatchintrain_loader:featuresbatch[feature]labelsbatch[label]scoresmodel(features)lossloss_fn(scores,labels)optimizer.zero_grad()loss.backward()optimizer.step()# 间隔verbose个epoch执行测试ifepoch%verbose0:evaluate(model,test_loader)流程翻译Epoch 1完整遍历训练集所有batch更新参数1%2≠0不测试Epoch 2完整遍历训练集所有batch更新参数2%20执行测试评估Epoch 3完整遍历训练集所有batch更新参数3%2≠0不测试单个batch内部标准训练闭环model.train()# 切换训练模式(Dropout/BatchNorm生效)scoresmodel(features)# 前向传播得到预测值lossloss_fn(scores,labels)# 计算损失optimizer.zero_grad()# 清空上一轮梯度loss.backward()# 反向传播计算参数梯度optimizer.step()# 使用梯度更新模型权重⚖️ 训练阶段 VS 测试评估阶段阶段是否计算梯度是否反向传播是否更新参数核心目的训练阶段✅ 是✅ 是✅ 是迭代优化模型测试评估❌ 否❌ 否❌ 否观测模型泛化效果训练代码模板model.train()forbatchintrain_loader:scoresmodel(batch[feature])lossloss_fn(scores,batch[label])optimizer.zero_grad()loss.backward()optimizer.step()测试评估模板model.eval()withtorch.no_grad():# 关闭梯度计算节省显存forbatchintest_loader:scoresmodel(batch[feature])# 根据预测分数计算HR、NDCG指标torch.no_grad()告知PyTorch仅推理不需要构建梯度计算图。 推荐系统指标HRK 和 NDCGK推荐任务离线评估最常用两个指标HRKHit Ratio命中率含义给用户推荐Top-K物品真实交互物品是否出现在推荐列表内。命中HR1未命中HR0示例推荐Top5列表[item_8, item_2, item_6, item_1, item_9]用户真实喜爱物品item_6item_6 在列表中 → HR5 1。NDCGKHR只关心「有没有命中」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__的调用时机importtorchfromtorch.utils.dataimportDataset,DataLoaderclassNumberDataset(Dataset):def__init__(self):self.samples[]foridxinrange(10):self.samples.append({idx:idx,feature:torch.tensor([float(idx),float(idx)0.5]),label:torch.tensor(idx%2,dtypetorch.float32),})def__len__(self):returnlen(self.samples)def__getitem__(self,index):print(f __getitem__ 被调用index{index})returnself.samples[index]datasetNumberDataset()loaderDataLoader(dataset,batch_size4,shuffleFalse)forbatch_id,batchinenumerate(loader,start1):print(f\n第{batch_id}个 batch)print(idx:,batch[idx])print(feature:,batch[feature])print(label:,batch[label])运行输出可以验证DataLoader 逐个调用__getitem__获取单条样本收集足够样本后collate_fn自动合并为batch张量训练循环拿到的是封装完成的批量数据而非单样本 收尾总结看到推荐系统经典双层循环代码forepochinrange(epochs):forbatchintrain_loader:...脑海中自动翻译外层epoch循环控制一共完整训练多少轮内层batch循环一轮训练中分批读取数据每一个训练batch都会更新模型参数到达指定epoch间隔进入评估模式只预测、计算指标不更新权重。打通这套流程后再阅读 BPR、NeuMF、LightGCN 等推荐模型训练代码理解门槛会大幅降低。