共计 3228 个字符,预计需要花费 9 分钟才能阅读完成。
为什么需要神经网络?
传统机器学习方法(如 SVM、随机森林)依赖手工设计特征,但面临两大瓶颈:

- 特征工程成本高:图像、语音等原始数据维度高,人工提取有效特征需要领域专业知识
- 表达能力有限:线性模型难以捕捉数据中的非线性关系,例如图像中的边缘、纹理组合
神经网络通过多层非线性变换自动学习特征表示。以 MNIST 手写数字识别为例:
- 原始输入是 784 维像素值(28×28 图像展平)
- 第一层隐层可学习到边缘、角点等低级特征
- 后续层逐步组合出数字部件(如弧线、横笔)
- 输出层对应数字类别的概率分布
前向传播与损失函数
定义 3 层网络结构(输入层 $h_0$,隐层 $h_1$, $h_2$,输出层 $h_3$):
$$
\begin{aligned}
h_1 &= \sigma(W_1 h_0 + b_1) \
h_2 &= \sigma(W_2 h_1 + b_2) \
h_3 &= \text{softmax}(W_3 h_2 + b_3)
\end{aligned}
$$
其中 $\sigma$ 为 sigmoid 激活函数:
$$
\sigma(z) = \frac{1}{1+e^{-z}}
$$
使用交叉熵损失函数衡量预测 $h_3$ 与真实标签 $y$ 的差异:
$$
L = -\sum_{k=1}^{10} y_k \log h_3^{(k)}
$$
反向传播推导
核心是通过链式法则计算损失对参数的偏导。以输出层权重 $W_3$ 为例:
$$
\frac{\partial L}{\partial W_3} = \frac{\partial L}{\partial h_3} \frac{\partial h_3}{\partial z_3} \frac{\partial z_3}{\partial W_3}
$$
具体分步计算:
- $\frac{\partial L}{\partial h_3} = -\frac{y}{h_3}$(交叉熵梯度)
- $\frac{\partial h_3}{\partial z_3} = h_3 \circ (1-h_3)$(softmax 梯度)
- $\frac{\partial z_3}{\partial W_3} = h_2^T$(线性变换梯度)
最终得到:
$$
\frac{\partial L}{\partial W_3} = (h_3 – y) h_2^T
$$
隐层参数梯度需继续反向传播。以 $W_2$ 为例:
$$
\frac{\partial L}{\partial W_2} = \left(\frac{\partial L}{\partial h_2} \circ \sigma'(z_2) \right) h_1^T
$$
其中 $\frac{\partial L}{\partial h_2} = W_3^T (h_3 – y)$,$\sigma'(z_2)$ 为 sigmoid 导数。
Python 实现
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import OneHotEncoder
# 网络初始化
class NeuralNetwork:
def __init__(self, input_size, hidden_sizes, output_size):
self.weights = [np.random.randn(input_size, hidden_sizes[0]) * np.sqrt(2./input_size), # Xavier 初始化
np.random.randn(hidden_sizes[0], hidden_sizes[1]) * np.sqrt(2./hidden_sizes[0]),
np.random.randn(hidden_sizes[1], output_size) * np.sqrt(2./hidden_sizes[1])
]
self.biases = [np.zeros((1, size)) for size in hidden_sizes + [output_size]]
def forward(self, x):
self.z1 = x @ self.weights[0] + self.biases[0]
self.h1 = 1 / (1 + np.exp(-self.z1)) # sigmoid
self.z2 = self.h1 @ self.weights[1] + self.biases[1]
self.h2 = 1 / (1 + np.exp(-self.z2))
self.z3 = self.h2 @ self.weights[2] + self.biases[2]
exp_scores = np.exp(self.z3 - np.max(self.z3, axis=1, keepdims=True))
self.h3 = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # softmax
return self.h3
def backward(self, x, y, l2_lambda=0.01):
m = x.shape[0]
# 输出层梯度
dz3 = self.h3 - y
dw3 = (self.h2.T @ dz3) / m + l2_lambda * self.weights[2] # L2 正则化
db3 = np.sum(dz3, axis=0, keepdims=True) / m
# 第二隐层梯度
dh2 = dz3 @ self.weights[2].T
dz2 = dh2 * (self.h2 * (1 - self.h2)) # sigmoid 导数
dw2 = (self.h1.T @ dz2) / m + l2_lambda * self.weights[1]
db2 = np.sum(dz2, axis=0, keepdims=True) / m
# 第一隐层梯度
dh1 = dz2 @ self.weights[1].T
dz1 = dh1 * (self.h1 * (1 - self.h1))
dw1 = (x.T @ dz1) / m + l2_lambda * self.weights[0]
db1 = np.sum(dz1, axis=0, keepdims=True) / m
return dw1, db1, dw2, db2, dw3, db3
训练技巧与避坑指南
梯度消失 / 爆炸
- 现象 :深层网络在反向传播时,梯度呈指数级减小(消失)或增大(爆炸)
- 解决方案 :
- 使用 Xavier/Glorot 初始化:权重方差 $\text{Var}(W) = \frac{2}{n_{in} + n_{out}}$
- 改用 ReLU 激活函数:$\text{ReLU}(z) = \max(0,z)$,导数为 0 或 1,缓解梯度消失
学习率设置
- 初始学习率通常取 0.01~0.1
- 采用指数衰减策略:
learning_rate = initial_lr * (0.95 ** epoch)
批量归一化(BatchNorm)
在激活函数前插入:
# 前向传播
self.z1 = x @ self.weights[0] + self.biases[0]
self.z1 = (self.z1 - np.mean(self.z1, axis=0)) / np.std(self.z1, axis=0) # BN
self.h1 = np.maximum(0, self.z1) # ReLU
MNIST 实验对比
设置相同超参数(隐层 256 单元,学习率 0.05,迭代 50 轮):
| 激活函数 | 测试准确率 | 训练时间 |
|---|---|---|
| Sigmoid | 92.3% | 38s |
| ReLU | 97.6% | 29s |
ReLU 优势明显:
- 计算更快(无需指数运算)
- 缓解梯度消失(正区间导数为 1)
- 带来稀疏激活(约 50% 神经元输出为 0)
下一步挑战
尝试实现卷积层的反向传播:
- 将全连接层的矩阵乘法替换为卷积操作
- 计算卷积核权重的梯度时需进行 im2col 转换
- 池化层反向传播需记录最大值位置(最大池化)
关键公式:
$$
\frac{\partial L}{\partial W_{conv}} = \text{rot180}(\frac{\partial L}{\partial Z} * X)
$$
其中 $*$ 表示卷积操作,rot180 表示旋转 180 度。
通过本文的实现,你已经掌握了 bp 算法的核心思想。建议尝试在 CIFAR-10 数据集上测试卷积网络,观察特征学习的效果提升。
