从零开始学习大模型(十一):用 PyTorch 训练第一个神经网络

掌握前向与反向计算后,就可以使用框架组织训练。本文构建一个两输入、单隐藏层的分类模型,用独立生成的验证数据观察效果,不下载数据集,也不调用外部服务。

示例使用 Python 3.12、PyTorch 2.6.0 的 CPU 环境。可在独立虚拟环境安装 torch==2.6.0;本文使用 CPU wheel,不要求显卡。不同平台的安装方式以官方说明为准。

1. 明确任务和数据

输入是二维坐标,两个分量乘积大于零时标为正类,否则为负类。这形成四个区域,单个线性边界不能正确描述全部区域。

训练集与验证集分别包含 256 和 128 个合成样本,使用固定随机种子生成。标签由明确函数计算,因此样本没有真实业务含义;这个实验只检验训练流程,不证明模型适用于生产任务。

对象 形状 作用
输入 (B, 2) 每个样本两个分量
隐藏层 (B, 16) 学习非线性组合
输出 logits (B, 1) 对应正类的未归一化分数
标签 (B, 1) 与输出形状一致的浮点 0/1

Tensor 负责数值运算,Dataset 描述样本集合,DataLoader 按批提供数据。小数据可以一次放入内存,但训练过程仍应保留明确的批次与数据边界。

数据批次经过模型、损失、反向传播和优化器形成训练循环的示意

图 1:训练循环中的数据与参数流向。验证阶段不执行反向传播和更新。

2. 完整训练代码

BCEWithLogitsLoss 内部组合了适合二分类的概率变换与稳定损失计算,因此训练前不再额外应用 Sigmoid。

import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader

torch.manual_seed(7)
torch.set_num_threads(1)
train_x = torch.randn(256, 2)
valid_x = torch.randn(128, 2)
train_y = (train_x[:, 0] * train_x[:, 1] > 0).float().unsqueeze(1)
valid_y = (valid_x[:, 0] * valid_x[:, 1] > 0).float().unsqueeze(1)
loader = DataLoader(TensorDataset(train_x, train_y), batch_size=32, shuffle=True)
model = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 1))
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)

model.eval()
with torch.no_grad():
    initial = criterion(model(valid_x), valid_y).item()
for epoch in range(100):
    model.train()
    for inputs, targets in loader:
        optimizer.zero_grad(set_to_none=True)
        loss = criterion(model(inputs), targets)
        loss.backward()
        optimizer.step()

model.eval()
with torch.no_grad():
    logits = model(valid_x)
    final = criterion(logits, valid_y).item()
    accuracy = ((logits >= 0) == valid_y.bool()).float().mean().item()
assert final < initial
print(round(initial, 4), round(final, 4), round(accuracy, 4))

代码只要求验证损失低于初始值,不把某个固定准确率当作跨版本保证。随机种子有助于复现,但不保证不同硬件、软件版本或算法实现逐位相同。本次验收(2026-10-28,PyTorch 2.6.0 CPU)中,验证损失从约 0.7286 降至 0.0290,128 个合成验证样本的准确率为 1.0。这只反映该受控实验,不能作为真实任务效果或跨环境结果的保证;完整环境和输出保存在本地验收记录中。

3. 训练与验证需要分别设置什么

操作 训练阶段 验证阶段
模型模式 model.train() model.eval()
梯度记录 保留用于反向传播 常使用 no_grad()
参数更新 调用优化器 不更新
指标用途 观察拟合与优化 观察泛化、选择方案

eval() 会影响 Dropout、BatchNorm 等模块的行为,但本身不关闭自动微分;no_grad() 关闭该上下文内的梯度记录,但不自动切换模型模式。本例虽未使用这些特殊模块,仍保留正确结构。

这里使用 logits 的零阈值分类,等价于 Sigmoid 概率的 0.5 阈值。是否调整阈值,应由独立的验证依据决定,不能反复用最终测试集选择。

4. 如何观察训练是否正常

应记录多个批次或整轮平均损失,而不是只报告最后一个小批次。检查标签形状、类别分布、梯度是否有限,以及验证损失是否异常,是比单看训练轮数更有效的诊断方式。

示例的 100 轮为预先指定的教学设置,没有根据验证结果反复挑选网络。真实实验可使用验证集早停,但最终还需要未参与选择的测试集。

训练完成可保存 state_dict,并记录网络结构、预处理和版本。若需要恢复训练,还应保存优化器、训练进度及必要的随机状态。加载来源不明的模型文件前应核查来源和加载方式;可优先采用适当的仅权重加载接口。

5. 小结

一次完整训练包含清理梯度、前向计算、计算损失、反向传播和参数更新;验证则使用固定参数计算独立数据的表现。这个流程会继续用于 CNN、RNN 与 Transformer。

下一篇介绍卷积如何利用图像的局部结构。接口解释可参考 PyTorch 官方训练教程。

本阶段目录

  1. 神经网络为什么需要非线性?
  2. 手算一个神经网络的反向传播
  3. 用 PyTorch 训练第一个神经网络
  4. CNN 如何通过卷积提取图像特征?
  5. 从经典 CNN 到 ResNet,深层网络如何训练?

系列学习路线

上一篇:手算一个神经网络的反向传播

下一篇:CNN 如何通过卷积提取图像特征?