共计 2610 个字符,预计需要花费 7 分钟才能阅读完成。
为什么 BP 神经网络让初学者头疼?
刚开始接触 BP 神经网络时,我被三个问题困扰了很久:

- 数学推导抽象:反向传播的链式求导过程像俄罗斯套娃,稍不留神就会迷失在偏导数的符号里
- 维度匹配混乱 :权重矩阵的 shape 变化规律不明显,经常出现
ValueError: shapes not aligned报错 - 超参数玄学:学习率设 0.1 还是 0.01?用 Sigmoid 还是 ReLU?这些问题没有标准答案
用思维导图拆解黑箱
这是我整理的算法流程图:
graph LR
A[输入层] -->| 权重 W1| B[隐藏层 1]
B -->| 激活函数 | C[隐藏层 1 输出]
C -->| 权重 W2| D[输出层]
D -->| 损失函数 | E[误差计算]
E --> F[反向传播]
F -->| 更新 W2| D
F -->| 更新 W1| A
关键步骤说明:
- 前向传播:数据像流水线一样层层传递
- 隐藏层计算:$Z^{[1]} = W^{[1]}X + b^{[1]}$
-
激活变换:$A^{[1]} = g(Z^{[1]})$
-
损失计算:衡量预测与真实的差距
-
二分类常用交叉熵:$L = -\frac{1}{m}\sum(y\log(a) + (1-y)\log(1-a))$
-
反向传播:误差从后往前传递
- 输出层误差:$dZ^{[2]} = A^{[2]} – Y$
- 隐藏层误差:$dZ^{[1]} = W^{[2]T}dZ^{[2]} \odot g'(Z^{[1]})$
Python 实现核心代码
import numpy as np
class NeuralNetwork:
def __init__(self, layer_dims):
# 初始化权重矩阵
self.params = {}
for l in range(1, len(layer_dims)):
self.params[f'W{l}'] = np.random.randn(layer_dims[l], layer_dims[l-1]) * 0.01
self.params[f'b{l}'] = np.zeros((layer_dims[l], 1))
def sigmoid(self, z):
return 1/(1+np.exp(-z))
def forward_prop(self, X):
# 维度检查(重要!)assert X.shape[0] == self.params['W1'].shape[1], \
f"输入特征数 {X.shape[0]} 与权重维度 {self.params['W1'].shape[1]} 不匹配"
caches = []
A_prev = X
# 前向传播计算
for l in range(1, len(self.params)//2 + 1):
Z = np.dot(self.params[f'W{l}'], A_prev) + self.params[f'b{l}']
A = self.sigmoid(Z) if l == len(self.params)//2 else np.maximum(0, Z) # 输出层用 sigmoid
caches.append((A_prev, self.params[f'W{l}'], self.params[f'b{l}'], Z))
A_prev = A
return A, caches
def backward_prop(self, AL, Y, caches):
grads = {}
m = Y.shape[1]
# 初始化反向传播
dZL = AL - Y
for l in reversed(range(1, len(caches)+1)):
A_prev, W, b, Z = caches[l-1]
if l == len(caches): # 输出层
dZ = dZL
else: # 隐藏层
dA = np.dot(self.params[f'W{l+1}'].T, dZ)
dZ = np.multiply(dA, np.where(Z > 0, 1, 0)) # ReLU 导数
grads[f'dW{l}'] = np.dot(dZ, A_prev.T) / m
grads[f'db{l}'] = np.sum(dZ, axis=1, keepdims=True) / m
# 梯度裁剪(防止爆炸)grads[f'dW{l}'] = np.clip(grads[f'dW{l}'], -5, 5)
return grads
实践中的经验法则
激活函数选择
- Sigmoid:输出层二分类首选,但隐藏层使用会导致梯度消失
- 梯度表达式:$\sigma'(z) = \sigma(z)(1-\sigma(z))$
-
当 $z$ 很大或很小时,梯度趋近于 0
-
ReLU:隐藏层默认选择,计算简单且缓解梯度消失
- 死亡 ReLU 问题:负值区域梯度为 0,可通过 LeakyReLU 改善
网络结构设计
- 隐藏层神经元数量:通常取输入特征数的 2 / 3 到 2 倍之间
- 学习率设置:先用 0.001 测试,观察损失曲线调整
- 损失震荡→降低学习率
- 下降过慢→适当增大
效果验证与可视化
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt
# 生成测试数据
X, y = make_moons(n_samples=1000, noise=0.2)
y = y.reshape(1, -1)
# 训练网络
nn = NeuralNetwork([2, 4, 1])
losses = []
for i in range(1000):
AL, caches = nn.forward_prop(X.T)
loss = -np.mean(y*np.log(AL) + (1-y)*np.log(1-AL))
losses.append(loss)
grads = nn.backward_prop(AL, y, caches)
# 带学习率衰减的权重更新
learning_rate = 0.1 * (0.95 ** (i//100))
for l in range(1, len(nn.params)//2 + 1):
nn.params[f'W{l}'] -= learning_rate * grads[f'dW{l}']
nn.params[f'b{l}'] -= learning_rate * grads[f'db{l}']
# 绘制损失曲线
plt.plot(losses)
plt.xlabel('Iterations')
plt.ylabel('Loss')
plt.show()
思考与进阶
当面对高维小样本数据(比如 1000 个特征但只有 100 个样本)时:
- 网络结构应该调深(增加隐藏层)还是调宽(增加神经元数)?
- 是否需要引入 Dropout 等正则化手段?
- 如何利用自动编码器进行特征降维?
这些问题的答案可能需要通过实验来验证,欢迎在评论区分享你的见解。
正文完
