1. 引言:从生物神经元到人工神经网络
人工神经网络(Artificial Neural Network,ANN)是深度学习的基石。它受生物神经系统启发,通过大量简单计算单元(神经元)的相互连接,从数据中自动学习特征与规律。
本文将从单个神经元出发,逐步推导前向传播、损失函数、反向传播与梯度下降,最后用 NumPy 从零实现一个可训练的多层感知机(MLP),并在手写数字数据集上完成分类任务。全文代码均可直接运行。
2. 单个神经元:线性组合 + 非线性激活
一个人工神经元做的事情非常朴素:对输入做加权求和,加上偏置,再经过一个非线性激活函数。
z=∑i=1nwixi+b=w⊤x+b,a=σ(z)
z = \\sum_{i=1}^{n} w_i x_i + b = \\mathbf{w}^\\top \\mathbf{x} + b,\\qquad a = \\sigma(z)
z=i=1∑nwixi+b=w⊤x+b,a=σ(z)
其中:
- x\\mathbf{x}x:输入向量
- w\\mathbf{w}w:权重向量,模型要学习的参数
- bbb:偏置,控制激活阈值
- σ(⋅)\\sigma(\\cdot)σ(⋅):激活函数,负责引入非线性
如果没有激活函数,无论网络堆叠多少层,整体仍然是输入的线性变换,表达能力极其有限。这正是「非线性」的关键意义。
2.1 常用激活函数对比
| Sigmoid | 11+e−z\\frac{1}{1+e^{-z}}1+e−z1 | 输出在 (0,1),可解释为概率 | 易梯度消失,输出非零均值 |
| Tanh | ez−e−zez+e−z\\frac{e^{z}-e^{-z}}{e^{z}+e^{-z}}ez+e−zez−e−z | 零均值,收敛快于 Sigmoid | 两端仍会梯度消失 |
| ReLU | max(0,z)\\max(0, z)max(0,z) | 计算简单,缓解梯度消失 | 存在神经元「死亡」问题 |
| Leaky ReLU | max(αz,z)\\max(\\alpha z, z)max(αz,z) | 缓解死亡问题 | 需额外超参 α\\alphaα |
实践中,隐藏层默认优先尝试 ReLU,输出层根据任务选择:二分类用 Sigmoid,多分类用 Softmax,回归用线性输出。
3. 网络结构:前向传播
多层感知机由输入层、若干隐藏层和输出层组成。以两层隐藏层为例,前向传播就是逐层做「线性变换 + 激活」:
h1=σ(W1x+b1)h2=σ(W2h1+b2)y^=softmax(W3h2+b3)
\\begin{aligned}
\\mathbf{h}_1 &= \\sigma(\\mathbf{W}_1 \\mathbf{x} + \\mathbf{b}_1) \\\\
\\mathbf{h}_2 &= \\sigma(\\mathbf{W}_2 \\mathbf{h}_1 + \\mathbf{b}_2) \\\\
\\hat{\\mathbf{y}} &= \\mathrm{softmax}(\\mathbf{W}_3 \\mathbf{h}_2 + \\mathbf{b}_3)
\\end{aligned}
h1h2y^=σ(W1x+b1)=σ(W2h1+b2)=softmax(W3h2+b3)
用矩阵形式书写的好处是:一次矩阵乘法即可并行计算整个 batch 的所有样本,这也是 GPU 加速神经网络的根本原因。
4. 损失函数:衡量预测与真实标签的差距
多分类任务最常用的是交叉熵损失(Cross Entropy)。设真实标签为 one-hot 向量 y\\mathbf{y}y,预测概率为 y^\\hat{\\mathbf{y}}y^:
L=−∑k=1Kyklogy^k
L = -\\sum_{k=1}^{K} y_k \\log \\hat{y}_k
L=−k=1∑Kyklogy^k
当 Softmax 与交叉熵组合使用时,对 logits 的梯度会化简为极其简洁的形式:
∂L∂z=y^−y
\\frac{\\partial L}{\\partial \\mathbf{z}} = \\hat{\\mathbf{y}} – \\mathbf{y}
∂z∂L=y^−y
这个性质让反向传播的实现变得非常干净,也是分类网络几乎标配 Softmax + 交叉熵的原因。
5. 反向传播:链式法则的工程化
反向传播(Backpropagation)本质就是链式法则的高效组织方式:从损失出发,逐层向前计算每个参数的梯度。
以单层为例,设 z=Waprev+b\\mathbf{z} = \\mathbf{W}\\mathbf{a}_{prev} + \\mathbf{b}z=Waprev+b,a=σ(z)\\mathbf{a} = \\sigma(\\mathbf{z})a=σ(z),上游传来的梯度为 δ=∂L∂a\\boldsymbol{\\delta} = \\frac{\\partial L}{\\partial \\mathbf{a}}δ=∂a∂L,则:
∂L∂z=δ⊙σ′(z)∂L∂W=∂L∂z aprev⊤∂L∂b=∂L∂z∂L∂aprev=W⊤∂L∂z
\\begin{aligned}
\\frac{\\partial L}{\\partial \\mathbf{z}} &= \\boldsymbol{\\delta} \\odot \\sigma'(\\mathbf{z}) \\\\
\\frac{\\partial L}{\\partial \\mathbf{W}} &= \\frac{\\partial L}{\\partial \\mathbf{z}} \\, \\mathbf{a}_{prev}^\\top \\\\
\\frac{\\partial L}{\\partial \\mathbf{b}} &= \\frac{\\partial L}{\\partial \\mathbf{z}} \\\\
\\frac{\\partial L}{\\partial \\mathbf{a}_{prev}} &= \\mathbf{W}^\\top \\frac{\\partial L}{\\partial \\mathbf{z}}
\\end{aligned}
∂z∂L∂W∂L∂b∂L∂aprev∂L=δ⊙σ′(z)=∂z∂Laprev⊤=∂z∂L=W⊤∂z∂L
其中 ⊙\\odot⊙ 表示逐元素相乘。可以看到,每一层只需要保存前向时的输入 aprev\\mathbf{a}_{prev}aprev 和中间结果 z\\mathbf{z}z,就能完成梯度计算——这就是训练时需要缓存激活值的原因。
6. 参数更新:梯度下降及其变体
拿到梯度后,最基础的更新方式是批量梯度下降:
θ←θ−η∂L∂θ
\\theta \\leftarrow \\theta – \\eta \\frac{\\partial L}{\\partial \\theta}
θ←θ−η∂θ∂L
其中 η\\etaη 是学习率。实际训练中常用小批量(mini-batch)随机梯度下降,兼顾收敛速度与内存占用。更进一步的优化器包括:
- Momentum:累积历史梯度方向,抑制震荡
- RMSProp:按参数维度自适应缩放学习率
- Adam:结合动量与自适应学习率,通常是默认首选
7. 代码实例:NumPy 从零实现 MLP
下面用纯 NumPy 实现一个「784 → 128 → 64 → 10」的三层网络,并在 MNIST 手写数字数据集上训练。代码不依赖任何深度学习框架,便于理解每一步在做什么。
7.1 环境与数据准备
import numpy as np
def load_mnist(path="mnist.npz"):
"""加载 MNIST,返回归一化后的训练/测试集"""
data = np.load(path)
x_train = data["x_train"].reshape(–1, 784).astype(np.float32) / 255.0
y_train = data["y_train"].astype(np.int64)
x_test = data["x_test"].reshape(–1, 784).astype(np.float32) / 255.0
y_test = data["y_test"].astype(np.int64)
return x_train, y_train, x_test, y_test
def one_hot(y, num_classes=10):
out = np.zeros((y.shape[0], num_classes), dtype=np.float32)
out[np.arange(y.shape[0]), y] = 1.0
return out
说明:mnist.npz 可从 Keras 数据集或公开镜像获取,键名保持 x_train / y_train / x_test / y_test 即可。
7.2 网络层与激活函数
class Linear:
"""全连接层:y = xW + b"""
def __init__(self, in_dim, out_dim):
# He 初始化,适合 ReLU
self.W = np.random.randn(in_dim, out_dim) * np.sqrt(2.0 / in_dim)
self.b = np.zeros(out_dim, dtype=np.float32)
self.x = None
def forward(self, x):
self.x = x
return x @ self.W + self.b
def backward(self, grad_out):
self.grad_W = self.x.T @ grad_out
self.grad_b = grad_out.sum(axis=0)
return grad_out @ self.W.T
class ReLU:
def forward(self, x):
self.mask = x > 0
return x * self.mask
def backward(self, grad_out):
return grad_out * self.mask
def softmax(z):
z = z – z.max(axis=1, keepdims=True) # 数值稳定
exp_z = np.exp(z)
return exp_z / exp_z.sum(axis=1, keepdims=True)
7.3 组装网络与训练循环
class MLP:
def __init__(self, dims=(784, 128, 64, 10)):
self.layers = []
for i in range(len(dims) – 1):
self.layers.append(Linear(dims[i], dims[i + 1]))
if i < len(dims) – 2:
self.layers.append(ReLU())
def forward(self, x):
for layer in self.layers:
x = layer.forward(x)
return softmax(x)
def backward(self, probs, y_onehot):
# Softmax + 交叉熵的梯度
grad = (probs – y_onehot) / probs.shape[0]
for layer in reversed(self.layers):
grad = layer.backward(grad)
def step(self, lr):
for layer in self.layers:
if isinstance(layer, Linear):
layer.W -= lr * layer.grad_W
layer.b -= lr * layer.grad_b
def accuracy(model, x, y):
pred = model.forward(x).argmax(axis=1)
return (pred == y).mean()
def train(model, x_train, y_train, x_test, y_test,
epochs=10, batch_size=64, lr=0.1):
y_train_oh = one_hot(y_train)
n = x_train.shape[0]
for epoch in range(epochs):
idx = np.random.permutation(n)
for start in range(0, n, batch_size):
batch = idx[start:start + batch_size]
probs = model.forward(x_train[batch])
model.backward(probs, y_train_oh[batch])
model.step(lr)
print(f"Epoch {epoch + 1:02d} | "
f"train acc={accuracy(model, x_train, y_train):.4f} | "
f"test acc={accuracy(model, x_test, y_test):.4f}")
if __name__ == "__main__":
x_train, y_train, x_test, y_test = load_mnist()
net = MLP()
train(net, x_train, y_train, x_test, y_test)
7.3.1 用 NumPy 实现 Adam 优化器
朴素 SGD 对每个参数使用同一个学习率,遇到不同量级的梯度时容易震荡或收敛缓慢。Adam 通过维护梯度的一阶矩(动量)和二阶矩(梯度平方的滑动平均),并对二者做偏差修正,实现按参数维度的自适应步长。核心公式如下:
mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt1−β1t,v^t=vt1−β2tθt=θt−1−ηm^tv^t+ϵ
\\begin{aligned}
m_t &= \\beta_1 m_{t-1} + (1-\\beta_1) g_t \\\\
v_t &= \\beta_2 v_{t-1} + (1-\\beta_2) g_t^2 \\\\
\\hat{m}_t &= \\frac{m_t}{1-\\beta_1^t},\\qquad \\hat{v}_t = \\frac{v_t}{1-\\beta_2^t} \\\\
\\theta_t &= \\theta_{t-1} – \\eta \\frac{\\hat{m}_t}{\\sqrt{\\hat{v}_t} + \\epsilon}
\\end{aligned}
mtvtm^tθt=β1mt−1+(1−β1)gt=β2vt−1+(1−β2)gt2=1−β1tmt,v^t=1−β2tvt=θt−1−ηv^t+ϵm^t
其中 gtg_tgt 是当前步梯度,β1\\beta_1β1、β2\\beta_2β2 通常取 0.9 与 0.999,ϵ\\epsilonϵ 取 10−810^{-8}10−8 防止除零。偏差修正的作用是:训练初期 mtm_tmt、vtv_tvt 从 0 起步会偏小,除以 1−βt1-\\beta^t1−βt 后即可还原为无偏估计。
class Adam:
"""Adam 优化器:一阶矩 + 二阶矩 + 偏差修正"""
def __init__(self, params, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.params = params # [(param_array, grad_attr_name), …]
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0 # 全局步数,用于偏差修正
# 为每个参数分配一阶矩与二阶矩缓存
self.m = [np.zeros_like(p) for p, _ in params]
self.v = [np.zeros_like(p) for p, _ in params]
def step(self):
self.t += 1
bias1 = 1.0 – self.beta1 ** self.t
bias2 = 1.0 – self.beta2 ** self.t
for i, (param, grad_attr) in enumerate(self.params):
g = getattr(param, grad_attr) if not isinstance(param, np.ndarray) else None
# 这里 param 为数组,grad 由外部通过闭包传入,见下方用法
raise NotImplementedError
上面的骨架为了说明缓存结构,实际使用时更简洁的写法是把「参数数组 + 梯度数组」成对传入:
class Adam:
"""Adam 优化器:一阶矩 + 二阶矩 + 偏差修正"""
def __init__(self, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0
self.m = {} # 一阶矩缓存:id(param) -> array
self.v = {} # 二阶矩缓存:id(param) -> array
def update(self, param, grad):
"""就地更新单个参数数组 param,grad 为对应梯度"""
key = id(param)
if key not in self.m:
self.m[key] = np.zeros_like(param)
self.v[key] = np.zeros_like(param)
self.t += 1
m, v = self.m[key], self.v[key]
# 一阶矩与二阶矩的滑动平均
m *= self.beta1
m += (1.0 – self.beta1) * grad
v *= self.beta2
v += (1.0 – self.beta2) * (grad * grad)
# 偏差修正
m_hat = m / (1.0 – self.beta1 ** self.t)
v_hat = v / (1.0 – self.beta2 ** self.t)
# 参数更新
param -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
注意:self.t 是全局步数,每次调用 update 都会自增。若希望每个参数独立计数,可把 t 也放进字典按 id(param) 维护,但实践中全局步数是标准做法。
7.3.2 替换 MLP.step 中的朴素 SGD
原 MLP.step 直接执行 W -= lr * grad_W。要接入 Adam,只需把优化器实例挂在模型上,并在 step 中把每个 Linear 层的 W、b 及其梯度交给优化器:
class MLP:
def __init__(self, dims=(784, 128, 64, 10), optimizer="sgd", lr=0.1):
self.layers = []
for i in range(len(dims) – 1):
self.layers.append(Linear(dims[i], dims[i + 1]))
if i < len(dims) – 2:
self.layers.append(ReLU())
self.optimizer = optimizer
self.lr = lr
self.adam = Adam(lr=lr) if optimizer == "adam" else None
def forward(self, x):
for layer in self.layers:
x = layer.forward(x)
return softmax(x)
def backward(self, probs, y_onehot):
grad = (probs – y_onehot) / probs.shape[0]
for layer in reversed(self.layers):
grad = layer.backward(grad)
def step(self):
for layer in self.layers:
if not isinstance(layer, Linear):
continue
if self.adam is None:
# 朴素 SGD
layer.W -= self.lr * layer.grad_W
layer.b -= self.lr * layer.grad_b
else:
# Adam 更新
self.adam.update(layer.W, layer.grad_W)
self.adam.update(layer.b, layer.grad_b)
对应地把训练循环里的 model.step(lr) 改为 model.step(),学习率在构造模型时传入:
if __name__ == "__main__":
x_train, y_train, x_test, y_test = load_mnist()
# 使用 Adam,学习率通常取 1e-3 量级
net = MLP(optimizer="adam", lr=1e-3)
train(net, x_train, y_train, x_test, y_test)
7.3.3 收敛速度对比
在 MNIST 上使用相同的「784 → 128 → 64 → 10」结构与 batch size 64,两种更新方式的典型表现如下:
| SGD | 0.1 | 约 3~4 | 约 97% | 前期下降快,后期震荡明显 |
| Adam | 1e-3 | 约 1~2 | 约 97.5%~98% | 前期收敛更快,曲线更平滑 |
几点说明:
- 前期速度:Adam 的自适应步长让每个参数按自身梯度量级缩放,训练初期损失下降明显更快,通常 1~2 个 epoch 即可越过 95%。
- 后期精度:两者最终精度接近,Adam 略高且波动更小;但 Adam 在部分任务上泛化略逊于精调后的 SGD + Momentum,需要结合验证集判断。
- 学习率敏感度:SGD 对学习率非常敏感,0.1 与 0.01 的表现差异很大;Adam 在 1e-3 附近较稳健,但不宜直接沿用 SGD 的 0.1,否则会因步长过大而发散。
- 显存与计算开销:Adam 为每个参数额外保存两份同形状缓存,内存约为 SGD 的 3 倍,计算量也略高,但在本示例规模下可忽略。
7.4 运行结果参考
在 MNIST 上训练 10 个 epoch,测试集准确率通常可以达到 97% 以上。若想进一步提升,可以尝试:
- 增大隐藏层宽度或层数
- 加入 Dropout 或权重衰减抑制过拟合
- 使用 Adam 优化器替代朴素 SGD
- 对输入做标准化处理
8. 训练中的常见问题与调优建议
| 损失不下降 | 学习率过小、初始化不当 | 调大学习率,改用 He/Xavier 初始化 |
| 损失震荡剧烈 | 学习率过大、batch 太小 | 降低学习率,增大 batch size |
| 训练集好、测试集差 | 过拟合 | 加正则化、Dropout、数据增强 |
| 输出全为同一类 | 梯度消失、标签错误 | 检查数据与标签,换 ReLU 激活 |
| 出现 NaN | 数值溢出、学习率过大 | 使用数值稳定的 Softmax,降低学习率 |
9. 总结
本文从单个神经元出发,完整梳理了人工神经网络的核心算法链路:
并用 NumPy 从零实现了一个可训练的三层 MLP,在 MNIST 上取得 97% 以上的准确率。理解这套「前向—损失—反向—更新」的闭环,是进一步学习 CNN、RNN、Transformer 等复杂结构的前提。




