共计 2693 个字符,预计需要花费 7 分钟才能阅读完成。
数学原理:链式求导详解
BP 神经网络的核心是反向传播算法,其本质是复合函数的链式求导。设网络有 L 层,第 l 层的权重为 $W^l$,偏置为 $b^l$,激活函数为 $\sigma(\cdot)$,则前向传播过程为:

$$
z^l = W^l a^{l-1} + b^l \
a^l = \sigma(z^l)
$$
对于损失函数 $J$,需要计算 $\frac{\partial J}{\partial W^l}$ 和 $\frac{\partial J}{\partial b^l}$。根据链式法则:
-
输出层误差:
$$
\delta^L = \frac{\partial J}{\partial a^L} \odot \sigma'(z^L)
$$ -
反向传播误差:
$$
\delta^l = (W^{l+1})^T \delta^{l+1} \odot \sigma'(z^l)
$$ -
参数梯度:
$$
\frac{\partial J}{\partial W^l} = \delta^l (a^{l-1})^T \
\frac{\partial J}{\partial b^l} = \delta^l
$$
激活函数对比
-
Sigmoid:
$$
\sigma(z) = \frac{1}{1+e^{-z}} \
\sigma'(z) = \sigma(z)(1-\sigma(z))
$$
易导致梯度消失,输出非零均值 -
Tanh:
$$
\tanh(z) = \frac{e^z – e^{-z}}{e^z + e^{-z}} \
\tanh'(z) = 1 – \tanh^2(z)
$$
梯度消失问题缓解,但计算量较大 -
ReLU:
$$
\text{ReLU}(z) = \max(0,z) \
\text{ReLU}'(z) = \begin{cases}
1 & z>0 \
0 & \text{otherwise}
\end{cases}
$$
计算高效,但需注意死亡 ReLU 问题
Python 实现核心代码
import numpy as np
class NeuralNetwork:
def __init__(self, layer_dims, activation='relu'):
self.params = {}
self.activation = activation
# He 初始化(ReLU 适用)for l in range(1, len(layer_dims)):
self.params[f'W{l}'] = np.random.randn(layer_dims[l], layer_dims[l-1]) * np.sqrt(2/layer_dims[l-1])
self.params[f'b{l}'] = np.zeros((layer_dims[l], 1))
def forward(self, X):
cache = {'A0': X}
# 前向传播
for l in range(1, len(self.params)//2 + 1):
Z = np.dot(self.params[f'W{l}'], cache[f'A{l-1}']) + self.params[f'b{l}']
if l == len(self.params)//2: # 输出层用 sigmoid
A = 1/(1+np.exp(-Z))
else: # 隐层用指定激活函数
if self.activation == 'relu':
A = np.maximum(0, Z)
elif self.activation == 'tanh':
A = np.tanh(Z)
cache[f'Z{l}'] = Z
cache[f'A{l}'] = A
return cache
def backward(self, cache, y):
grads = {}
m = y.shape[1]
L = len(self.params)//2
# 输出层误差
dZ = cache[f'A{L}'] - y
grads[f'dW{L}'] = np.dot(dZ, cache[f'A{L-1}'].T)/m
grads[f'db{L}'] = np.sum(dZ, axis=1, keepdims=True)/m
# 反向传播
for l in reversed(range(1, L)):
if self.activation == 'relu':
dA = np.dot(self.params[f'W{l+1}'].T, dZ)
dZ = dA * (cache[f'Z{l}'] > 0)
elif self.activation == 'tanh':
dA = np.dot(self.params[f'W{l+1}'].T, dZ)
dZ = dA * (1 - np.square(cache[f'A{l}']))
grads[f'dW{l}'] = np.dot(dZ, cache[f'A{l-1}'].T)/m
grads[f'db{l}'] = np.sum(dZ, axis=1, keepdims=True)/m
return grads
参数初始化策略
-
Xavier 初始化 :
适用于 Sigmoid/Tanh 激活函数
$$
W \sim \mathcal{N}(0, \sqrt{\frac{1}{n_{in}}})
$$ -
He 初始化 :
适用于 ReLU 系列激活函数
$$
W \sim \mathcal{N}(0, \sqrt{\frac{2}{n_{in}}})
$$
训练注意事项
- 学习率选择 :
- 典型初始值 0.01,观察损失曲线
- 出现震荡时降低学习率 10 倍
-
使用学习率衰减策略
-
梯度裁剪 :
max_norm = 5 for key in grads: norm = np.linalg.norm(grads[key]) if norm > max_norm: grads[key] = grads[key] * max_norm / norm -
隐层设计 :
- 首层神经元数建议为输入维度的 2 / 3 到 4 /3
- 后续每层减半
- 总层数不超过 $\log_2(n_{features})$ + 2
扩展思考
-
训练日志记录 :
def log_metrics(metric_names): def decorator(train_func): def wrapper(*args, **kwargs): results = train_func(*args, **kwargs) with open('training.log', 'a') as f: f.write(','.join(str(results[name]) for name in metric_names) + '\n') return results return wrapper return decorator -
批归一化影响 :
- 将每层的输入分布标准化为 $\mathcal{N}(0,1)$
- 允许使用更大的学习率
- 减少对初始化的敏感度
- 需在反向传播时增加 BN 层的梯度计算
实战建议
- 从单隐层网络开始调试
- 先在小数据集(如 MNIST)上验证正确性
- 使用 Adam 优化器替代 SGD
- 添加 L2 正则化防止过拟合
- 可视化每层的权重分布
