虽然现在有了AI生成工具,但是我们是研究者,还是需要了解一个基本的神经网络是如何构建的。
接下来以pytorch框架的一个简单网络为例,来说明神经网络的构建过程。

整体结构预览

一般来说,一个神经网络的代码结构大致如下所示(以”从零到可训练”为主线,把数据、模型、训练、评估、配置拆开):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
project/
README.md
requirements.txt
configs/
mnist_mlp.yaml
data/
(数据会下载到这里,或放置本地数据)
src/
__init__.py
datasets.py # 数据集与DataLoader
model.py # 网络结构
train.py # 训练循环
eval.py # 验证/测试
utils.py # 通用工具:seed、日志、指标、保存/加载
scripts/
run_train.sh
run_eval.sh
outputs/
mnist_mlp/
ckpt_last.pt
ckpt_best.pt
train.log
curves.json

上面的结构并非唯一,但它体现了研究代码里常见的分层:数据模型训练评估配置与复现

从一个最小可训练例子开始

为了让”构建神经网络”这件事更具体,我们先用 MNIST 的一个 MLP(多层感知机)跑通训练与评估。这个例子足够简单,但覆盖了工程里最核心的模块。

任务设定:分类问题

MNIST 是手写数字分类数据集,输入是一张 $28\times 28$ 的灰度图(可视为向量 $\mathbf{x}\in \mathbb{R}^{784}$),输出类别 $y\in{0,\dots,9}$。

我们的目标是学习一个函数

$$
f_\theta:\mathbb{R}^{784}\rightarrow \mathbb{R}^{10},
$$

输出 10 类的 logits(未归一化分数),再用 softmax 得到概率:

$$
p_\theta(y=k\mid \mathbf{x})=\frac{\exp(f_\theta(\mathbf{x})k)}{\sum{j=0}^{9}\exp(f_\theta(\mathbf{x})_j)}.
$$

损失函数:交叉熵

对单样本 $(\mathbf{x},y)$,分类常用交叉熵损失:

$$
\mathcal{L}(\theta;\mathbf{x},y)=-\log p_\theta(y\mid \mathbf{x}).
$$

对一个 batch ${(\mathbf{x}i,y_i)}{i=1}^{B}$,取平均:

$$
\mathcal{L}{\text{batch}}(\theta)=\frac{1}{B}\sum{i=1}^{B}-\log p_\theta(y_i\mid \mathbf{x}_i).
$$

模块 1:数据(Dataset & DataLoader)

为什么要拆出数据模块?

数据模块负责:

  • 读取与预处理:标准化、数据增强、padding/裁剪等;
  • 批处理:构造 mini-batch,支持 shuffle;
  • 复现性:随机种子与 worker 初始化。

例子:MNIST 的输入输出形状

以 MNIST 为例,DataLoader 每次返回一个 batch:

$$
\mathbf{X}\in \mathbb{R}^{B\times 1\times 28\times 28},\qquad
\mathbf{y}\in {0,\dots,9}^{B}.
$$

如果我们使用 MLP,需要把图像摊平成向量:

$$
\text{flatten}(\mathbf{X})\rightarrow \mathbb{R}^{B\times 784}.
$$

常见坑:dtype 与归一化

  • 图像输入通常是 float32;标签是 int64(分类损失常要求 long)。
  • 归一化:像素 $\in[0,1]$,可再做标准化 $\frac{x-\mu}{\sigma}$。

模块 2:模型(nn.Module 的最小闭环)

一个最简 MLP

MLP 可以写成两层线性变换加非线性:

$$
\mathbf{h}=\phi(\mathbf{W}_1\mathbf{x}+\mathbf{b}_1),\qquad
\mathbf{z}=\mathbf{W}_2\mathbf{h}+\mathbf{b}_2,
$$

其中 $\phi$ 可以选 ReLU:$\phi(t)=\max(0,t)$。

设隐藏层维度为 $d$,则参数维度为:

$$
\mathbf{W}_1\in\mathbb{R}^{d\times 784},\ \mathbf{b}_1\in\mathbb{R}^{d},\
\mathbf{W}_2\in\mathbb{R}^{10\times d},\ \mathbf{b}_2\in\mathbb{R}^{10}.
$$

例子:形状流动(shape flow)

假设 batch size $B=64$,隐藏维度 $d=256$:

$$
\mathbf{X}\in\mathbb{R}^{64\times 1\times 28\times 28}
\rightarrow
\mathbb{R}^{64\times 784}
\rightarrow
\mathbb{R}^{64\times 256}
\rightarrow
\mathbb{R}^{64\times 10}.
$$

这一步”形状检查”是排错最有效的方法之一:一旦 shape 不对,后续损失与梯度都不可信。

加入一个正则化例子:Dropout

训练时 Dropout 对隐藏层随机置零:

$$
\tilde{\mathbf{h}}=\mathbf{m}\odot \mathbf{h},\qquad m_i\sim \text{Bernoulli}(1-p),
$$

推理时不再随机置零(或使用等效缩放)。它的直觉是减少 co-adaptation,提高泛化。

模块 3:训练循环(Training Loop)

训练循环的四个基本动作

一次迭代(一个 batch)通常包含:

  1. 前向传播:$\mathbf{z}=f_\theta(\mathbf{x})$;
  2. 计算损失:$\mathcal{L}(\theta)$;
  3. 反向传播:$\nabla_\theta \mathcal{L}$;
  4. 参数更新:$\theta\leftarrow \theta-\eta \nabla_\theta \mathcal{L}$(以 SGD 为例)。

例子:学习率对收敛的影响(直观版)

假设在某一步梯度范数较大 $|\nabla_\theta \mathcal{L}|$:

  • 学习率 $\eta$ 太大:更新步长过大,可能在最优点附近”来回震荡”甚至发散;
  • 学习率 $\eta$ 太小:每步更新很微弱,训练极慢,可能停在次优区域很久。

一个实用做法:先用较大学习率短跑(warmup / 试探),观察 loss 曲线是否单调下降,再微调。

例子:梯度爆炸与梯度裁剪

在某些任务(如 RNN 或非常深的网络)可能出现梯度爆炸:

$$
|\nabla_\theta \mathcal{L}|\rightarrow \infty.
$$

常用的梯度裁剪(按范数)是:

$$
\nabla_\theta \mathcal{L}\leftarrow \nabla_\theta \mathcal{L}\cdot
\min\left(1,\frac{c}{|\nabla_\theta \mathcal{L}|}\right),
$$

其中 $c$ 是阈值。这在训练不稳定时很常见、也很有效。

例子:训练/验证模式切换的重要性

很多层在训练与评估时行为不同:

  • Dropout:训练时随机置零,评估时关闭;
  • BatchNorm:训练时用 batch 统计量,评估时用滑动平均。

因此必须区分:

$$
\text{train mode}\quad vs\quad \text{eval mode}.
$$

模块 4:评估与指标(Metrics)

例子:分类准确率

最常见指标是 accuracy:

$$
\text{acc}=\frac{1}{N}\sum_{i=1}^{N}\mathbb{I}\left(\arg\max_k z_{i,k}=y_i\right).
$$

例子:混淆矩阵能告诉你什么?

如果模型总把”9”预测成”4”,accuracy 可能还行,但错误结构会很明显。混淆矩阵 $C\in\mathbb{R}^{10\times 10}$:

$$
C_{a,b}=#{i: y_i=a,\ \hat{y}_i=b}.
$$

它能帮助你定位:哪些类别之间最容易混淆、是否数据偏斜、是否需要更强的特征提取网络。

模块 5:保存、加载与复现

例子:为什么要保存 best checkpoint?

训练 loss 下降不代表泛化一定更好;验证集指标通常先升后降(过拟合)。因此常见策略:

  • 保存 last checkpoint:方便中断后继续训练;
  • 保存 best checkpoint:以验证集最优指标为准,用于最终测试/部署。

例子:复现性最容易忽略的点

即使固定随机种子,不同硬件/并行策略也可能导致差异。实践中建议记录:

  • 代码版本(git commit);
  • 超参数(lr, batch size, weight decay, scheduler);
  • 环境信息(CUDA/cuDNN/PyTorch 版本);
  • 数据划分方式与随机种子。

扩展练习:把 MLP 换成一个小 CNN

如果想让例子更贴近”神经网络真正擅长的视觉归纳偏置”,可以把 MLP 换成 CNN:

$$
\mathbf{X}\in\mathbb{R}^{B\times 1\times 28\times 28}
\rightarrow
\text{Conv} \rightarrow \text{ReLU} \rightarrow \text{Pool}
\rightarrow
\text{Conv} \rightarrow \text{ReLU} \rightarrow \text{Pool}
\rightarrow
\text{Flatten} \rightarrow \text{Linear} \rightarrow \mathbb{R}^{B\times 10}.
$$

思考题:

  • 为什么 CNN 通常比 MLP 在图像上更强?
  • 卷积核大小、通道数、池化会如何影响表达能力与计算量?

小结

一个”可训练”的神经网络工程最少需要:数据管道模型定义训练循环评估指标保存与复现
建议先用最小例子(MNIST MLP)跑通闭环,再逐步替换模块(CNN、优化器、调度器、增强策略),这样改动可控、结果可解释。