
【深度学习入门 Day 6】DataLoader 与 mini-batch 训练用 MLP 分类双月数据本文记录深度学习学习第 6 天的内容从前几天的 XOR 小实验升级到一个更接近真实训练流程的二维非线性分类任务。今天重点理解make_moons数据集、TensorDataset、DataLoader、batch_size、shuffle、epoch、mini-batch 训练以及如何用model.train()/model.eval()完成训练与评估。文章目录一、从 XOR 到真实一点的数据集二、生成 make_moons 双月数据三、把 NumPy 数据转成 PyTorch Tensor四、TensorDataset把 X 和 y 绑在一起五、DataLoader按 batch 喂给模型六、定义 MLP 分类器七、损失函数、优化器和 accuracy八、mini-batch 训练循环九、最终整体评估十、完整训练代码十一、今日总结十二、课后自测一、从 XOR 到真实一点的数据集前几天我们一直在训练 XOR[0, 0] - 0 [0, 1] - 1 [1, 0] - 1 [1, 1] - 0XOR 很适合理解非线性分类 隐藏层 反向传播 PyTorch 标准训练流程但它只有 4 个样本每次训练都是logitsmodel(X)losscriterion(logits,y)也就是一次性把全部数据喂给模型。真实训练通常不是这样。真实数据可能有几千、几万、几百万个样本不可能每次都全部喂进去。更常见的做法是把数据切成很多小批次 batch 每次用一个 batch 训练 一轮 epoch 看完整个数据集今天我们要从全量训练 XOR升级到DataLoader mini-batch 训练二维非线性分类器二、生成 make_moons 双月数据今天使用sklearn.datasets.make_moons生成二维二分类数据importtorchimporttorch.nnasnnfromsklearn.datasetsimportmake_moonsfromtorch.utils.dataimportTensorDataset,DataLoader X_np,y_npmake_moons(n_samples500,noise0.2,random_state42,)这里n_samples500表示生成 500 个样本。noise0.2表示给数据加一些噪声让它不要太完美更接近真实情况。random_state42表示固定随机种子方便每次运行都得到同一批数据。打印形状print(X_np shape:,X_np.shape)print(y_np shape:,y_np.shape)print(first 5 X:)print(X_np[:5])print(first 5 y:)print(y_np[:5])输出类似X_np shape: (500, 2) y_np shape: (500,) first 5 X: [[ 0.83085779 -0.33434228] [ 0.99170955 0.87899966] [ 1.1072453 -0.47034376] [-0.14089918 1.03314844] [ 0.40559217 1.32852876]] first 5 y: [1 0 1 0 0]含义是X_np每个点的二维坐标 y_np每个点的类别标签0 或 1make_moons不是随机乱贴标签而是生成两条“月牙形”的点云一条月牙上的点标为 0 另一条月牙上的点标为 1监督学习要学的就是从(X, y)的对应关系中学习输入特征和类别标签之间的规律。三、把 NumPy 数据转成 PyTorch Tensormake_moons生成的是 NumPy 数组。训练 PyTorch 模型前要转成 tensorXtorch.tensor(X_np,dtypetorch.float32)ytorch.tensor(y_np,dtypetorch.float32).view(-1,1)打印print(X shape:,X.shape)print(y shape:,y.shape)print(X dtype:,X.dtype)print(y dtype:,y.dtype)输出X shape: torch.Size([500, 2]) y shape: torch.Size([500, 1]) X dtype: torch.float32 y dtype: torch.float32这里有两个重点。第一训练神经网络一般使用浮点数dtypetorch.float32第二原来的标签形状是y_np.shape (500,)我们用.view(-1,1)改成y.shape (500, 1)这样就能和模型输出的形状保持一致logits.shape (batch_size, 1)四、TensorDataset把 X 和 y 绑在一起接下来创建数据集datasetTensorDataset(X,y)可以查看长度和第一个样本print(dataset length:,len(dataset))sample_x,sample_ydataset[0]print(sample_x:,sample_x)print(sample_y:,sample_y)print(sample_x shape:,sample_x.shape)print(sample_y shape:,sample_y.shape)输出类似sample_x: tensor([ 0.8309, -0.3343]) sample_y: tensor([1.]) sample_x shape: torch.Size([2]) sample_y shape: torch.Size([1])TensorDataset(X, y)的作用是把特征X和标签y按样本维度绑定在一起。它至少能回答两个问题这个数据集有多少个样本 len(dataset) 第 i 个样本和标签是什么 dataset[i]可以把TensorDataset理解成一个“样本仓库”。五、DataLoader按 batch 喂给模型有了 dataset 后再创建 DataLoaderloaderDataLoader(dataset,batch_size32,shuffleTrue,)取一个 batch 看看forbatch_X,batch_yinloader:print(batch_X shape:,batch_X.shape)print(batch_y shape:,batch_y.shape)print(first batch_X[:3]:)print(batch_X[:3])print(first batch_y[:3]:)print(batch_y[:3])break输出类似batch_X shape: torch.Size([32, 2]) batch_y shape: torch.Size([32, 1])这里batch_size32表示每次从数据集中取 32 个样本给模型训练。shuffleTrue表示每一轮训练前打乱数据顺序。为什么要打乱如果数据本身按类别或某种规律排序不打乱可能导致模型连续看到很多相似样本训练不稳定。打乱后每个 batch 更像整体数据的一小份随机样本。如果有 500 个样本batch_size32那么一轮 epoch 大约有ceil(500 / 32) 16 个 batch也就是说模型完整看完一遍数据大约会更新 16 次参数。六、定义 MLP 分类器今天的模型结构是2 - 16 - 16 - 1代码classMoonNet(nn.Module):def__init__(self):super().__init__()self.netnn.Sequential(nn.Linear(2,16),nn.ReLU(),nn.Linear(16,16),nn.ReLU(),nn.Linear(16,1),)defforward(self,x):returnself.net(x)modelMoonNet()print(model)输出MoonNet( (net): Sequential( (0): Linear(in_features2, out_features16, biasTrue) (1): ReLU() (2): Linear(in_features16, out_features16, biasTrue) (3): ReLU() (4): Linear(in_features16, out_features1, biasTrue) ) )这里用了nn.Sequential。它可以理解成一个“顺序容器”把层按顺序串起来输入 x ↓ Linear(2, 16) ↓ ReLU ↓ Linear(16, 16) ↓ ReLU ↓ Linear(16, 1) ↓ 输出 logits最后一层不加sigmoid因为损失函数会使用nn.BCEWithLogitsLoss()七、损失函数、优化器和 accuracy损失函数criterionnn.BCEWithLogitsLoss()优化器optimizertorch.optim.Adam(model.parameters(),lr0.01)今天使用Adam而不是SGD。原因是Adam 通常比 SGD 更省心 对学习率不那么敏感 在小型 MLP 上收敛更快再写一个 accuracy 函数defaccuracy_from_logits(logits,y):probtorch.sigmoid(logits)pred(prob0.5).float()acc(predy).float().mean()returnacc解释一下probtorch.sigmoid(logits)把 logits 转成属于 1 类的概率。pred(prob0.5).float()概率大于等于 0.5 判为 1否则判为 0。acc(predy).float().mean()预测对的位置是True预测错的位置是False。转成 float 后True - 1 False - 0取平均就是准确率。八、mini-batch 训练循环训练循环forepochinrange(101):model.train()total_loss0.0total_acc0.0num_batches0forbatch_X,batch_yinloader:optimizer.zero_grad()logitsmodel(batch_X)losscriterion(logits,batch_y)loss.backward()optimizer.step()accaccuracy_from_logits(logits,batch_y)total_lossloss.item()total_accacc.item()num_batches1avg_losstotal_loss/num_batches avg_acctotal_acc/num_batchesifepoch%100:print(fepoch{epoch:03d}, floss{avg_loss:.6f}, facc{avg_acc:.4f})这里有几个关键词。epoch模型完整看完一遍训练数据叫 1 个 epoch。batch每次从 DataLoader 里取出的一小批样本。model.train()model.train()表示进入训练模式。今天这个模型只有Linear和ReLU影响不大。但以后如果模型里有Dropout BatchNorm训练模式和评估模式的行为会不同所以从现在开始养成习惯。avg_loss 和 avg_acc一个 epoch 里有多个 batch每个 batch 都有一个 loss 和 accuracy。所以我们取平均avg_loss 所有 batch loss 的平均 avg_acc 所有 batch accuracy 的平均运行结果类似epoch010, loss0.095390, acc0.9637 epoch020, loss0.057364, acc0.9805 epoch030, loss0.054290, acc0.9832 epoch040, loss0.047844, acc0.9805 epoch050, loss0.039478, acc0.9824 epoch060, loss0.046938, acc0.9766 epoch070, loss0.060244, acc0.9691 epoch080, loss0.034879, acc0.9824 epoch090, loss0.046880, acc0.9812 epoch100, loss0.033519, acc0.9812可以看到 accuracy 已经达到 98% 左右。loss 和 accuracy 不一定每一轮都严格变好这是正常的。原因包括DataLoader 每轮 shuffleTrue Adam 优化器本身有动态更新 mini-batch 梯度只是整体梯度的近似所以训练曲线会有一点波动。九、最终整体评估训练结束后再在全部 500 个样本上做一次整体评估model.eval()withtorch.no_grad():logitsmodel(X)final_losscriterion(logits,y)final_accaccuracy_from_logits(logits,y)print(\nfinal evaluation:)print(floss{final_loss.item():.6f})print(facc{final_acc.item():.4f})这里model.eval()表示进入评估模式。withtorch.no_grad():表示评估时不记录计算图也不需要求梯度。最终输出final evaluation: loss0.031530 acc0.9840准确率98.40%说明这个小 MLP 已经很好地学会了双月数据的非线性分类边界。十、完整训练代码importtorchimporttorch.nnasnnfromsklearn.datasetsimportmake_moonsfromtorch.utils.dataimportTensorDataset,DataLoader X_np,y_npmake_moons(n_samples500,noise0.2,random_state42,)Xtorch.tensor(X_np,dtypetorch.float32)ytorch.tensor(y_np,dtypetorch.float32).view(-1,1)datasetTensorDataset(X,y)loaderDataLoader(dataset,batch_size32,shuffleTrue,)classMoonNet(nn.Module):def__init__(self):super().__init__()self.netnn.Sequential(nn.Linear(2,16),nn.ReLU(),nn.Linear(16,16),nn.ReLU(),nn.Linear(16,1),)defforward(self,x):returnself.net(x)defaccuracy_from_logits(logits,y):probtorch.sigmoid(logits)pred(prob0.5).float()acc(predy).float().mean()returnacc modelMoonNet()criterionnn.BCEWithLogitsLoss()optimizertorch.optim.Adam(model.parameters(),lr0.01)forepochinrange(101):model.train()total_loss0.0total_acc0.0num_batches0forbatch_X,batch_yinloader:optimizer.zero_grad()logitsmodel(batch_X)losscriterion(logits,batch_y)loss.backward()optimizer.step()accaccuracy_from_logits(logits,batch_y)total_lossloss.item()total_accacc.item()num_batches1avg_losstotal_loss/num_batches avg_acctotal_acc/num_batchesifepoch%100:print(fepoch{epoch:03d}, floss{avg_loss:.6f}, facc{avg_acc:.4f})model.eval()withtorch.no_grad():logitsmodel(X)final_losscriterion(logits,y)final_accaccuracy_from_logits(logits,y)print(\nfinal evaluation:)print(floss{final_loss.item():.6f})print(facc{final_acc.item():.4f})十一、今日总结今天的核心内容可以压缩成 8 点make_moons可以生成二维非线性二分类数据。TensorDataset用来把特征和标签绑定成样本集合。DataLoader用来按 batch 取数据并支持 shuffle。batch_size32表示每次训练使用 32 个样本。epoch表示模型完整看完一遍训练数据。mini-batch 训练的 loss 和 accuracy 会有波动这是正常现象。model.train()用于训练模式model.eval()用于评估模式。torch.no_grad()用于评估阶段避免记录计算图和浪费内存。最终要记住这句话从 Day6 开始我们不再只是在玩具 XOR 上训练模型而是在接近真实深度学习训练流程数据集、批量加载、模型、损失、优化器、训练循环和评估。十二、课后自测make_moons生成的X_np和y_np分别表示什么y_np的标签是任意的吗为什么为什么要把y从(500,)改成(500, 1)TensorDataset(X, y)的作用是什么DataLoader和Dataset有什么区别batch_size32表示什么为什么训练时通常要设置shuffleTrue什么是 epoch什么是 batch为什么 mini-batch 训练的 loss 不一定单调下降model.train()和model.eval()分别在什么时候使用评估时为什么要使用torch.no_grad()