共计 2965 个字符,预计需要花费 8 分钟才能阅读完成。
神经网络基础与问题定义
当我们构建一个最简单的三层神经网络(输入层、隐藏层、输出层)时,前向传播过程可以表示为:

$$
\begin{aligned}
z^{(2)} &= W^{(1)}x + b^{(1)} \
a^{(2)} &= \sigma(z^{(2)}) \
z^{(3)} &= W^{(2)}a^{(2)} + b^{(2)} \
a^{(3)} &= \sigma(z^{(3)})
\end{aligned}
$$
其中 $\sigma(\cdot)$ 是 sigmoid 激活函数,$W$ 和 $b$ 分别代表权重矩阵和偏置项。我们需要定义一个损失函数(这里采用均方误差):
$$
J = \frac{1}{2}(y – a^{(3)})^2
$$
链式法则的逐层应用
输出层梯度计算
首先计算输出层权重 $W^{(2)}$ 的梯度:
$$
\frac{\partial J}{\partial W^{(2)}} = \frac{\partial J}{\partial a^{(3)}} \cdot \frac{\partial a^{(3)}}{\partial z^{(3)}} \cdot \frac{\partial z^{(3)}}{\partial W^{(2)}}
$$
展开各项导数:
- $\frac{\partial J}{\partial a^{(3)}} = -(y – a^{(3)})$
- $\frac{\partial a^{(3)}}{\partial z^{(3)}} = \sigma'(z^{(3)}) = a^{(3)}(1-a^{(3)})$
- $\frac{\partial z^{(3)}}{\partial W^{(2)}} = a^{(2)}$
最终得到:
$$
\delta^{(3)} = (a^{(3)} – y) \cdot a^{(3)}(1-a^{(3)})
$$
$$
\frac{\partial J}{\partial W^{(2)}} = \delta^{(3)} a^{(2)T}
$$
隐藏层梯度计算
对于隐藏层权重 $W^{(1)}$,梯度计算需要继续反向传播:
$$
\frac{\partial J}{\partial W^{(1)}} = \frac{\partial J}{\partial a^{(3)}} \cdot \frac{\partial a^{(3)}}{\partial z^{(3)}} \cdot \frac{\partial z^{(3)}}{\partial a^{(2)}} \cdot \frac{\partial a^{(2)}}{\partial z^{(2)}} \cdot \frac{\partial z^{(2)}}{\partial W^{(1)}}
$$
其中新增的导数为:
- $\frac{\partial z^{(3)}}{\partial a^{(2)}} = W^{(2)}$
- $\frac{\partial a^{(2)}}{\partial z^{(2)}} = \sigma'(z^{(2)}) = a^{(2)}(1-a^{(2)})$
- $\frac{\partial z^{(2)}}{\partial W^{(1)}} = x$
因此隐藏层误差项为:
$$
\delta^{(2)} = (W^{(2)T} \delta^{(3)}) \odot a^{(2)}(1-a^{(2)})
$$
Python 完整实现
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# 权重初始化(使用较小随机值)self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros((1, output_size))
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(self, z):
s = self.sigmoid(z)
return s * (1 - s)
def forward(self, X):
self.z2 = np.dot(X, self.W1) + self.b1
self.a2 = self.sigmoid(self.z2)
self.z3 = np.dot(self.a2, self.W2) + self.b2
self.a3 = self.sigmoid(self.z3)
return self.a3
def backward(self, X, y, learning_rate):
m = X.shape[0] # 样本数量
# 输出层误差
delta3 = (self.a3 - y) * self.sigmoid_derivative(self.z3)
dW2 = np.dot(self.a2.T, delta3)
db2 = np.sum(delta3, axis=0, keepdims=True)
# 隐藏层误差
delta2 = np.dot(delta3, self.W2.T) * self.sigmoid_derivative(self.z2)
dW1 = np.dot(X.T, delta2)
db1 = np.sum(delta2, axis=0)
# 参数更新
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
def train(self, X, y, epochs, learning_rate):
for i in range(epochs):
output = self.forward(X)
self.backward(X, y, learning_rate)
避坑指南
梯度消失问题
当使用 sigmoid 激活函数时,其导数的最大值仅为 0.25(在输入为 0 时)。这意味着在深层网络中,梯度会随着反向传播层层衰减,导致靠前的层几乎无法更新,这种现象称为梯度消失。解决方案包括:
- 使用 ReLU 等具有恒定梯度的激活函数
- 采用残差连接(ResNet)
- 使用 Batch Normalization
学习率选择
学习率过大容易震荡不收敛,过小则训练缓慢。建议策略:
- 初始尝试 0.01 或 0.001
- 使用学习率衰减策略
- 考虑自适应优化器(Adam 等)
权重初始化
全零初始化会导致神经元对称性问题。推荐方法:
- Xavier 初始化:$W \sim N(0, \sqrt{2/(n_{in} + n_{out})})$
- He 初始化(ReLU 适用):$W \sim N(0, \sqrt{2/n_{in}})$
思考题
- 如何修改代码实现 ReLU 激活函数?需要注意哪些问题?
- 批量训练(Batch)与在线训练(Online)在实现上有什么区别?各自的优缺点是什么?
- 二阶优化算法(如 L -BFGS)相比梯度下降有什么理论优势?为什么在深度学习中不常见?
总结
通过本文的推导和实现,我们完成了以下目标:
1. 从数学原理层面理解了反向传播的链式法则应用
2. 实现了完整的神经网络训练流程
3. 掌握了实践中常见的调参技巧
建议读者尝试用不同数据集测试代码,观察不同超参数对训练过程的影响,这是理解算法行为的最佳方式。
