【深度学习入门 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:
text
[0, 0] -> 0
[0, 1] -> 1
[1, 0] -> 1
[1, 1] -> 0
XOR 很适合理解:
text
非线性分类
隐藏层
反向传播
PyTorch 标准训练流程
但它只有 4 个样本,每次训练都是:
python
logits = model(X)
loss = criterion(logits, y)
也就是一次性把全部数据喂给模型。
真实训练通常不是这样。真实数据可能有几千、几万、几百万个样本,不可能每次都全部喂进去。更常见的做法是:
text
把数据切成很多小批次 batch
每次用一个 batch 训练
一轮 epoch 看完整个数据集
今天我们要从:
text
全量训练 XOR
升级到:
text
DataLoader mini-batch 训练二维非线性分类器
二、生成 make_moons 双月数据
今天使用 sklearn.datasets.make_moons 生成二维二分类数据:
python
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import TensorDataset, DataLoader
X_np, y_np = make_moons(
n_samples=500,
noise=0.2,
random_state=42,
)
这里:
python
n_samples=500
表示生成 500 个样本。
python
noise=0.2
表示给数据加一些噪声,让它不要太完美,更接近真实情况。
python
random_state=42
表示固定随机种子,方便每次运行都得到同一批数据。
打印形状:
python
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])
输出类似:
text
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]
含义是:
text
X_np:每个点的二维坐标
y_np:每个点的类别标签,0 或 1
make_moons 不是随机乱贴标签,而是生成两条"月牙形"的点云:
text
一条月牙上的点标为 0
另一条月牙上的点标为 1
监督学习要学的就是:
从
(X, y)的对应关系中,学习输入特征和类别标签之间的规律。
三、把 NumPy 数据转成 PyTorch Tensor
make_moons 生成的是 NumPy 数组。训练 PyTorch 模型前,要转成 tensor:
python
X = torch.tensor(X_np, dtype=torch.float32)
y = torch.tensor(y_np, dtype=torch.float32).view(-1, 1)
打印:
python
print("X shape:", X.shape)
print("y shape:", y.shape)
print("X dtype:", X.dtype)
print("y dtype:", y.dtype)
输出:
text
X shape: torch.Size([500, 2])
y shape: torch.Size([500, 1])
X dtype: torch.float32
y dtype: torch.float32
这里有两个重点。
第一,训练神经网络一般使用浮点数:
python
dtype=torch.float32
第二,原来的标签形状是:
text
y_np.shape = (500,)
我们用:
python
.view(-1, 1)
改成:
text
y.shape = (500, 1)
这样就能和模型输出的形状保持一致:
text
logits.shape = (batch_size, 1)
四、TensorDataset:把 X 和 y 绑在一起
接下来创建数据集:
python
dataset = TensorDataset(X, y)
可以查看长度和第一个样本:
python
print("dataset length:", len(dataset))
sample_x, sample_y = dataset[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)
输出类似:
text
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按样本维度绑定在一起。
它至少能回答两个问题:
text
这个数据集有多少个样本? len(dataset)
第 i 个样本和标签是什么? dataset[i]
可以把 TensorDataset 理解成一个"样本仓库"。
五、DataLoader:按 batch 喂给模型
有了 dataset 后,再创建 DataLoader:
python
loader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
)
取一个 batch 看看:
python
for batch_X, batch_y in loader:
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
输出类似:
text
batch_X shape: torch.Size([32, 2])
batch_y shape: torch.Size([32, 1])
这里:
python
batch_size=32
表示每次从数据集中取 32 个样本给模型训练。
python
shuffle=True
表示每一轮训练前打乱数据顺序。
为什么要打乱?
如果数据本身按类别或某种规律排序,不打乱可能导致模型连续看到很多相似样本,训练不稳定。打乱后,每个 batch 更像整体数据的一小份随机样本。
如果有 500 个样本,batch_size=32,那么一轮 epoch 大约有:
text
ceil(500 / 32) = 16 个 batch
也就是说:
模型完整看完一遍数据,大约会更新 16 次参数。
六、定义 MLP 分类器
今天的模型结构是:
text
2 -> 16 -> 16 -> 1
代码:
python
class MoonNet(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 16),
nn.ReLU(),
nn.Linear(16, 1),
)
def forward(self, x):
return self.net(x)
model = MoonNet()
print(model)
输出:
text
MoonNet(
(net): Sequential(
(0): Linear(in_features=2, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
)
)
这里用了 nn.Sequential。
它可以理解成一个"顺序容器",把层按顺序串起来:
text
输入 x
↓
Linear(2, 16)
↓
ReLU
↓
Linear(16, 16)
↓
ReLU
↓
Linear(16, 1)
↓
输出 logits
最后一层不加 sigmoid,因为损失函数会使用:
python
nn.BCEWithLogitsLoss()
七、损失函数、优化器和 accuracy
损失函数:
python
criterion = nn.BCEWithLogitsLoss()
优化器:
python
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
今天使用 Adam,而不是 SGD。
原因是:
text
Adam 通常比 SGD 更省心
对学习率不那么敏感
在小型 MLP 上收敛更快
再写一个 accuracy 函数:
python
def accuracy_from_logits(logits, y):
prob = torch.sigmoid(logits)
pred = (prob >= 0.5).float()
acc = (pred == y).float().mean()
return acc
解释一下:
python
prob = torch.sigmoid(logits)
把 logits 转成属于 1 类的概率。
python
pred = (prob >= 0.5).float()
概率大于等于 0.5 判为 1,否则判为 0。
python
acc = (pred == y).float().mean()
预测对的位置是 True,预测错的位置是 False。转成 float 后:
text
True -> 1
False -> 0
取平均就是准确率。
八、mini-batch 训练循环
训练循环:
python
for epoch in range(101):
model.train()
total_loss = 0.0
total_acc = 0.0
num_batches = 0
for batch_X, batch_y in loader:
optimizer.zero_grad()
logits = model(batch_X)
loss = criterion(logits, batch_y)
loss.backward()
optimizer.step()
acc = accuracy_from_logits(logits, batch_y)
total_loss += loss.item()
total_acc += acc.item()
num_batches += 1
avg_loss = total_loss / num_batches
avg_acc = total_acc / num_batches
if epoch % 10 == 0:
print(
f"epoch={epoch:03d}, "
f"loss={avg_loss:.6f}, "
f"acc={avg_acc:.4f}"
)
这里有几个关键词。
epoch
text
模型完整看完一遍训练数据,叫 1 个 epoch。
batch
text
每次从 DataLoader 里取出的一小批样本。
model.train()
python
model.train()
表示进入训练模式。
今天这个模型只有 Linear 和 ReLU,影响不大。但以后如果模型里有:
text
Dropout
BatchNorm
训练模式和评估模式的行为会不同,所以从现在开始养成习惯。
avg_loss 和 avg_acc
一个 epoch 里有多个 batch,每个 batch 都有一个 loss 和 accuracy。
所以我们取平均:
text
avg_loss = 所有 batch loss 的平均
avg_acc = 所有 batch accuracy 的平均
运行结果类似:
text
epoch=010, loss=0.095390, acc=0.9637
epoch=020, loss=0.057364, acc=0.9805
epoch=030, loss=0.054290, acc=0.9832
epoch=040, loss=0.047844, acc=0.9805
epoch=050, loss=0.039478, acc=0.9824
epoch=060, loss=0.046938, acc=0.9766
epoch=070, loss=0.060244, acc=0.9691
epoch=080, loss=0.034879, acc=0.9824
epoch=090, loss=0.046880, acc=0.9812
epoch=100, loss=0.033519, acc=0.9812
可以看到 accuracy 已经达到 98% 左右。
loss 和 accuracy 不一定每一轮都严格变好,这是正常的。原因包括:
text
DataLoader 每轮 shuffle=True
Adam 优化器本身有动态更新
mini-batch 梯度只是整体梯度的近似
所以训练曲线会有一点波动。
九、最终整体评估
训练结束后,再在全部 500 个样本上做一次整体评估:
python
model.eval()
with torch.no_grad():
logits = model(X)
final_loss = criterion(logits, y)
final_acc = accuracy_from_logits(logits, y)
print("\nfinal evaluation:")
print(f"loss={final_loss.item():.6f}")
print(f"acc={final_acc.item():.4f}")
这里:
python
model.eval()
表示进入评估模式。
python
with torch.no_grad():
表示评估时不记录计算图,也不需要求梯度。
最终输出:
text
final evaluation:
loss=0.031530
acc=0.9840
准确率:
text
98.40%
说明这个小 MLP 已经很好地学会了双月数据的非线性分类边界。
十、完整训练代码
python
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import TensorDataset, DataLoader
X_np, y_np = make_moons(
n_samples=500,
noise=0.2,
random_state=42,
)
X = torch.tensor(X_np, dtype=torch.float32)
y = torch.tensor(y_np, dtype=torch.float32).view(-1, 1)
dataset = TensorDataset(X, y)
loader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
)
class MoonNet(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 16),
nn.ReLU(),
nn.Linear(16, 1),
)
def forward(self, x):
return self.net(x)
def accuracy_from_logits(logits, y):
prob = torch.sigmoid(logits)
pred = (prob >= 0.5).float()
acc = (pred == y).float().mean()
return acc
model = MoonNet()
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(101):
model.train()
total_loss = 0.0
total_acc = 0.0
num_batches = 0
for batch_X, batch_y in loader:
optimizer.zero_grad()
logits = model(batch_X)
loss = criterion(logits, batch_y)
loss.backward()
optimizer.step()
acc = accuracy_from_logits(logits, batch_y)
total_loss += loss.item()
total_acc += acc.item()
num_batches += 1
avg_loss = total_loss / num_batches
avg_acc = total_acc / num_batches
if epoch % 10 == 0:
print(
f"epoch={epoch:03d}, "
f"loss={avg_loss:.6f}, "
f"acc={avg_acc:.4f}"
)
model.eval()
with torch.no_grad():
logits = model(X)
final_loss = criterion(logits, y)
final_acc = accuracy_from_logits(logits, y)
print("\nfinal evaluation:")
print(f"loss={final_loss.item():.6f}")
print(f"acc={final_acc.item():.4f}")
十一、今日总结
今天的核心内容可以压缩成 8 点:
make_moons可以生成二维非线性二分类数据。TensorDataset用来把特征和标签绑定成样本集合。DataLoader用来按 batch 取数据,并支持 shuffle。batch_size=32表示每次训练使用 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_size=32表示什么?- 为什么训练时通常要设置
shuffle=True? - 什么是 epoch?什么是 batch?
- 为什么 mini-batch 训练的 loss 不一定单调下降?
model.train()和model.eval()分别在什么时候使用?- 评估时为什么要使用
torch.no_grad()?