引言
从零开始实现一个神经网络,是理解深度学习基础的最佳途径之一。本文将仅依赖 NumPy 和基础线性代数,手把手构建一个三层前馈神经网络(输入层 → 隐藏层 → 输出层),并在 MNIST 手写数字数据集上完成训练与评估。
全文分为两大部分:
理论篇:介绍三层网络的结构、前向传播机制、常用激活函数(sigmoid、softmax)、交叉熵损失,以及基于梯度下降的反向传播算法,并配合关键公式与直观解释。
实践篇:基于 NumPy 完成参数初始化、前向传播、损失计算、反向传播与参数更新,并在 MNIST 数据集上训练和测试模型(不依赖 TensorFlow / PyTorch 等高阶框架)。
读完本文,你不仅能运行一个可用的神经网络,还能清晰理解它为什么有效。
理论篇:三层神经网络的工作原理
本文会实现一个最简单的网络,只有三层:输入层 → 1 个隐藏层 → 输出层。虽然简单,但是已经涵盖了神经网络的核心和主要流程。
网络结构(输入层、隐藏层、输出层)
一张 28×28 的灰度图在展平后就是 784 维向量,作为输入层。隐藏层接收来自输入层的==加权和(再加偏置)==,经过非线性激活输出。输出层再对隐藏层输出进行线性变换与(任务相关的)激活,得到最终预测(多分类用 softmax 概率)。
- 输入层:仅承载特征,不做计算。
- 隐藏层:对输入做线性变换 + 偏置,再过激活函数(如 sigmoid),学习非线性特征。
- 输出层:对隐藏层输出做线性变换 + 偏置,再过适合任务的激活(多分类常用 softmax)。
- 偏置项:每个非输入层的神经元都会有一个可学习偏置,类似线性回归中的截距。
全连接(dense)意味着上一层的每个神经元都与下一层每个神经元相连,每条连接都有一个可学习权重。
记号与代数
设:
- 输入维度 $n_x$,隐藏层神经元数 $n_h$,输出类别数 $n_y$。
- 输入→隐藏权重 $W^{[1]} \in \mathbb{R}^{n_x \times n_h}$,偏置 $b^{[1]} \in \mathbb{R}^{n_h}$。
- 隐藏→输出权重 $W^{[2]} \in \mathbb{R}^{n_h \times n_y}$,偏置 $b^{[2]} \in \mathbb{R}^{n_y}$。
对单样本 $\mathbf{x} \in \mathbb{R}^{n_x}$: $\mathbf{z}^{[1]} = W^{[1]} \mathbf{x} + \mathbf{b}^{[1]}, \quad \mathbf{a}^{[1]} = \sigma(\mathbf{z}^{[1]}),$ $\mathbf{z}^{[2]} = W^{[2]} \mathbf{a}^{[1]} + \mathbf{b}^{[2]}, \quad \mathbf{a}^{[2]} = f(\mathbf{z}^{[2]}),$ 其中 $\sigma$ 为隐藏层激活函数(本文使用 sigmoid),$f$ 为输出层激活函数(本文使用 softmax)。
前向传播(Forward Propagation)
前向传播就是“线性变换 + 非线性激活”的层层堆叠:
- 输入→隐藏:$\mathbf{z}{[1]} = W{[1]} \mathbf{x} + \mathbf{b}{[1]}, \quad \mathbf{a}{[1]} = \sigma(\mathbf{z}_{[1]}).$
- 隐藏→输出: $\mathbf{z}{[2]} = W{[2]} \mathbf{a}{[1]} + \mathbf{b}{[2]}, \quad \mathbf{a}{[2]} = \text{softmax}(\mathbf{z}{[2]}).$
无非是矩阵乘法与逐元素非线性。==如果没有非线性激活,堆多少层还是线性模型,表达力不会增强==。
激活函数:Sigmoid 与 Softmax
- Sigmoid(逻辑函数): $\sigma(x) = \frac{1}{1+e^{-x}},\qquad \sigma’(x)=\sigma(x)\big(1-\sigma(x)\big).$
输出落在 $(0,1)$,历史上常用于隐藏层(现代更常用 ReLU,但本文用 sigmoid 便于推导与实现)。
- Softmax(多分类输出概率):\ 对 $\mathbf{z}\in\mathbb{R}^{n_y}$: $\text{softmax}(\mathbf{z})i=\frac{e^{z_i}}{\sum{k=1}^{n_y}e^{z_k}},$ 得到各类的概率分布(和为 1)。实现时常减去行最大值以稳定数值。
损失函数:交叉熵(Cross-Entropy)
多分类常用交叉熵损失。若真实标签 one-hot 为 $\mathbf{y}$,预测概率为 $\mathbf{p}$(softmax 输出): $L = -\sum_{i=1}^{n_y} y_i \log p_i.$ 若预测把正确类的概率压高,损失就小;反之损失大。训练时通常取样本平均。
反向传播(Backpropagation)与梯度下降(Gradient Descent)
==训练的核心是用反向传播计算梯度==,再用梯度下降更新参数,使损失下降。
输出层误差(softmax+交叉熵的便利结果): $\frac{\partial L}{\partial \mathbf{z}^{[2]}} = \mathbf{a}^{[2]} - \mathbf{y}.$
第二层梯度: $\frac{\partial L}{\partial W^{[2]}} = \mathbf{a}^{[1]}(\mathbf{a}^{[2]}-\mathbf{y})^\top,\quad \frac{\partial L}{\partial \mathbf{b}^{[2]}} = \mathbf{a}^{[2]}-\mathbf{y}.$
隐藏层误差: $\delta^{[1]}=\frac{\partial L}{\partial \mathbf{z}^{[1]}} = \left(W^{[2]\top}(\mathbf{a}^{[2]}-\mathbf{y})\right) \circ \sigma’(\mathbf{z}^{[1]}),$ 其中 $\circ$ 为逐元素乘。
第一层梯度: $\frac{\partial L}{\partial W^{[1]}}=\mathbf{x}(\delta^{[1]})^\top,\quad \frac{\partial L}{\partial \mathbf{b}^{[1]}}=\delta^{[1]}.$
梯度下降更新(学习率 $\alpha$): $W := W - \alpha \frac{\partial L}{\partial W},\quad b := b - \alpha \frac{\partial L}{\partial b}.$
权重初始化要随机且小:若全零初始化,隐藏单元梯度完全相同,永远学不出差异化特征(对称性无法打破)。偏置通常可初始化为 0。
实践篇:用 NumPy 从零实现并在 MNIST 上训练
我们按以下步骤实现:
加载数据(MNIST,70k 张 28×28 灰度图;60k 训练、10k 测试)。
初始化参数(随机小权重、零偏置)。
前向传播(含 sigmoid/softmax)。
损失计算(交叉熵)。
反向传播(矢量化计算梯度)。
参数更新(批量梯度下降)。
训练循环(多 epoch)。
测试集评估。
说明:数据加载我们用
scikit-learn
的fetch_openml
便捷获取 MNIST;模型计算全用 NumPy。若无 sklearn,可改为手动下载/解压 MNIST。
1) 加载 MNIST
kaggle 下载数据集:https://www.kaggle.com/datasets/aadeshkoirala/mnist-784/code
1 | import numpy as np |
2) 初始化权重与偏置
1 | # 维度设定 |
3) 前向传播实现
1 | def sigmoid(z): |
1 | # 训练集上的一次前向传播 |
初始损失应接近 $ln(10)≈2.302\ln(10)\approx 2.302$,准确率约 10%。
4) 反向传播实现(矢量化)
1 | # 反向传播 |
5) 参数更新(梯度下降)
1 | learning_rate = 0.1 |
6) 训练循环(多轮迭代)
1 | # 训练 |
正常情况下,损失会逐步下降、准确率提升。以 64 隐藏单元、10 个 epoch 为例,训练准确率大致能到 0.90 左右(因初始化与学习率不同会有差异)。
7) 测试集评估
1 | # 测试前向 |
你应能看到约 88%–92% 的测试准确率(取决于超参数与训练轮数)。这对一个仅一层隐藏层的纯 NumPy 实现来说已经相当不错。
小结与拓展
本文用 NumPy 从零实现了一个三层全连接神经网络,并在 MNIST 上完成训练与评估。我们:
将网络拆解为“线性($Wx+bW\mathbf{x}+b$)+ 非线性(激活)”的层级组合;
在隐藏层用 sigmoid,在输出层用 softmax;
用 交叉熵 衡量分类误差;
推导并实现了反向传播的向量化梯度;
用梯度下降迭代更新参数,并在测试集上做了评估。