CSDN博客-第6天-DataLoader与二维非线性分类

【深度学习入门 Day 6】DataLoader 与 mini-batch 训练:用 MLP 分类双月数据

本文记录深度学习学习第 6 天的内容:从前几天的 XOR 小实验,升级到一个更接近真实训练流程的二维非线性分类任务。今天重点理解 make_moons 数据集、TensorDatasetDataLoaderbatch_sizeshuffleepoch、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()

表示进入训练模式。

今天这个模型只有 LinearReLU,影响不大。但以后如果模型里有:

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 点:

  1. make_moons 可以生成二维非线性二分类数据。
  2. TensorDataset 用来把特征和标签绑定成样本集合。
  3. DataLoader 用来按 batch 取数据,并支持 shuffle。
  4. batch_size=32 表示每次训练使用 32 个样本。
  5. epoch 表示模型完整看完一遍训练数据。
  6. mini-batch 训练的 loss 和 accuracy 会有波动,这是正常现象。
  7. model.train() 用于训练模式,model.eval() 用于评估模式。
  8. torch.no_grad() 用于评估阶段,避免记录计算图和浪费内存。

最终要记住这句话:

从 Day6 开始,我们不再只是在玩具 XOR 上训练模型,而是在接近真实深度学习训练流程:数据集、批量加载、模型、损失、优化器、训练循环和评估。


十二、课后自测

  1. make_moons 生成的 X_npy_np 分别表示什么?
  2. y_np 的标签是任意的吗?为什么?
  3. 为什么要把 y(500,) 改成 (500, 1)
  4. TensorDataset(X, y) 的作用是什么?
  5. DataLoaderDataset 有什么区别?
  6. batch_size=32 表示什么?
  7. 为什么训练时通常要设置 shuffle=True
  8. 什么是 epoch?什么是 batch?
  9. 为什么 mini-batch 训练的 loss 不一定单调下降?
  10. model.train()model.eval() 分别在什么时候使用?
  11. 评估时为什么要使用 torch.no_grad()
相关推荐
Java小白笔记1 小时前
Codex config.toml配置实战指南
人工智能·算法·chatgpt·ai编程·集成学习
Goodbye1 小时前
AI 数据集:大模型智能的基石
人工智能
AKAMAI1 小时前
Akamai收购安全企业浏览器提供商LayerX
人工智能·云计算
用户2749329676611 小时前
Spring AI 手搓 RAG:文档上传 → 向量化 → 检索增强,全链路 Java 实现
人工智能
米小虾1 小时前
Claude Code Loop Engineering 深度解读:从写 Prompt 到设计 Loop
人工智能·agent
科技侃谈1 小时前
上海、安徽、浙江、江苏制造业智能仓储服务商选型指南:机器人零部件企业的破局之选
人工智能
重庆传粉科技2 小时前
传粉科技助力美国房产经纪企业Homequest开拓中国市场
大数据·人工智能
2501_930296992 小时前
模型驱动创作全链路梳理:2026上半年图像与视频生成AI模型全景盘点
人工智能·音视频