BP神经网络计算入门:从数学原理到Python实现

1次阅读
没有评论

共计 2568 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

背景痛点:为什么反向传播让人头疼?

第一次接触 BP 神经网络时,我被三个问题卡住:

  • 矩阵维度魔术 :明明输入是(5,3) 的矩阵,经过隐藏层怎么就变成 (5,4) 了?
  • 梯度消失迷局:训练时 loss 死活不下降,原来是权重初始化不当导致梯度指数级衰减
  • 链式求导恐惧:看到 $\frac{\partial E}{\partial w} = \frac{\partial E}{\partial a}\frac{\partial a}{\partial z}\frac{\partial z}{\partial w}$ 就头皮发麻

这些问题其实都源于对计算过程的具象化不足。接下来我们像拆乐高一样,把 BP 网络拆成可理解的模块。

数学原理:链式求导的具象化理解

前向传播:数据流动的管道

假设网络结构为 输入层(3 节点) → 隐藏层(4 节点) → 输出层(1 节点),前向传播就是矩阵连续乘法:

  1. 隐藏层输入:$Z_1 = XW_1 + b_1$(X 形状(n,3),W1 形状(3,4))
  2. 激活输出:$A_1 = \sigma(Z_1)$(σ 是 sigmoid 函数)
  3. 最终输出:$Z_2 = A_1W_2 + b_2$(W2 形状(4,1))

BP 神经网络计算入门:从数学原理到 Python 实现
(示意图说明:展示矩阵乘法过程中各张量的形状变化)

反向传播:误差的溯源之旅

以输出层权重 $W_2$ 的梯度计算为例:

$$
\frac{\partial E}{\partial W_2} = \frac{\partial E}{\partial Z_2} \cdot \frac{\partial Z_2}{\partial W_2} = (A_2 – Y) \cdot A_1^T
$$

关键点:

  • $\frac{\partial E}{\partial Z_2}$ 就是预测值与真实值的差
  • $\frac{\partial Z_2}{\partial W_2}$ 就是上一层的激活值 $A_1$
  • 矩阵求导后需要转置 $A_1$ 才能保证维度匹配

Python 实现:从零搭建 BP 网络

1. 网络初始化

import numpy as np

class NeuralNetwork:
    def __init__(self, input_size, hidden_size):
        # Xavier 初始化:尺度保持输入输出方差一致
        self.W1 = np.random.randn(input_size, hidden_size) / np.sqrt(input_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, 1) / np.sqrt(hidden_size)
        self.b2 = np.zeros(1)

2. 激活函数与导数

# sigmoid 激活函数
def sigmoid(self, x):
    return 1 / (1 + np.exp(-x))

# sigmoid 导数(用于反向传播)def sigmoid_derivative(self, x):
    return x * (1 - x)  # 这里 x 应为 sigmoid 的输出值

3. 前向传播实现

def forward(self, X):
    self.z1 = np.dot(X, self.W1) + self.b1  # (n,3)×(3,4)=(n,4)
    self.a1 = self.sigmoid(self.z1)
    self.z2 = np.dot(self.a1, self.W2) + self.b2  # (n,4)×(4,1)=(n,1)
    return self.z2

4. 反向传播与参数更新

def backward(self, X, y, lr=0.01):
    m = X.shape[0]  # 样本数量

    # 输出层误差
    dz2 = self.z2 - y  # (n,1)
    dW2 = np.dot(self.a1.T, dz2) / m  # (4,n)×(n,1)=(4,1)

    # 隐藏层误差(注意链式法则)dz1 = np.dot(dz2, self.W2.T) * self.sigmoid_derivative(self.a1)  # (n,1)×(1,4)=(n,4)
    dW1 = np.dot(X.T, dz1) / m  # (3,n)×(n,4)=(3,4)

    # 参数更新
    self.W1 -= lr * dW1
    self.W2 -= lr * dW2
    self.b1 -= lr * np.mean(dz1, axis=0)
    self.b2 -= lr * np.mean(dz2, axis=0)

避坑指南:训练中的实用技巧

权重初始化:打破对称性

  • 错误做法:全零初始化会导致所有神经元同步更新
  • 推荐方案:Xavier 初始化(方差 =1/ 输入维度)或 He 初始化(ReLU 适用)

学习率调参:动态调整策略

# 学习率衰减示例
lr = initial_lr * (1 / (1 + decay_rate * epoch))

梯度裁剪:防止数值爆炸

# 限制梯度最大范数
grad_norm = np.linalg.norm(gradients)
if grad_norm > max_norm:
    gradients = gradients * (max_norm / grad_norm)

实战验证:鸢尾花二分类

from sklearn.datasets import load_iris

# 数据预处理
iris = load_iris()
X = iris.data[:100, :3]  # 取前两类,3 个特征
Y = iris.target[:100].reshape(-1,1)  # 二分类标签

# 训练循环
nn = NeuralNetwork(input_size=3, hidden_size=4)
for epoch in range(1000):
    pred = nn.forward(X)
    loss = np.mean((pred - Y)**2)
    nn.backward(X, Y, lr=0.1)

    if epoch % 100 == 0:
        print(f"Epoch {epoch}, Loss: {loss:.4f}")

(图示:损失函数随迭代次数下降的趋势)

思考题

当隐藏层使用 ReLU 激活函数时,反向传播公式需要做如下修改:

$$
\frac{\partial a}{\partial z} = \begin{cases}
1 & \text{if} z > 0 \
0 & \text{otherwise}
\end{cases}
$$

这会导致什么训练特性?与 sigmoid 相比有何优缺点?欢迎在评论区分享你的见解。


通过这次实践,我深刻体会到:BP 神经网络的本质就是通过矩阵运算实现复合函数的梯度下降。建议初学者一定要亲手实现一遍完整流程,这会比看十篇理论文章更有收获。

正文完
 0
评论(没有评论)