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

1次阅读
没有评论

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

image.webp

背景痛点:为什么反向传播这么难懂?

刚开始接触 BP 神经网络时,我被三个问题困扰得头疼:

  • 矩阵维度像俄罗斯套娃 :每次前向传播时矩阵形状都在变,一不留神就出现维度不匹配
  • 梯度像坐过山车 :有时候参数更新后 loss 不降反升,后来才知道是学习率设大了
  • 代码像天书 :看教材推导能明白,但一到用代码实现就不知道从哪下手

后来发现,只要抓住两个关键点就能破局:
1. 链式求导本质是误差的逆向分配
2. 矩阵运算是为了批量计算的效率

数学原理:拆解链式求导

假设我们有一个 3 层网络(输入层、隐藏层、输出层),用 sigmoid 激活函数。前向传播过程如下:

  1. 隐藏层输入:$z^{(2)} = W^{(1)}x + b^{(1)}$
  2. 隐藏层输出:$a^{(2)} = \sigma(z^{(2)})$
  3. 输出层结果:$\hat{y} = \sigma(W^{(2)}a^{(2)} + b^{(2)})$

损失函数用均方误差:$E = \frac{1}{2}(y – \hat{y})^2$

反向传播时,关键是要计算 $\frac{\partial E}{\partial W}$。以输出层权重为例:

$$
\frac{\partial E}{\partial W^{(2)}} =
\underbrace{\frac{\partial E}{\partial \hat{y}}}{-(y-\hat{y})} \cdot
\underbrace{\frac{\partial \hat{y}}{\partial z^{(3)}}}
\cdot
\underbrace{\frac{\partial z^{(3)}}{\partial W^{(2)}}}_{a^{(2)}}
$$})

这就是链式法则的直观体现——误差从输出层逐步向前分配。

BP 神经网络计算入门:从数学原理到 Python 实现
(示意图说明:输入向量 x 是 4 维,隐藏层 3 个神经元,输出层 1 个神经元时各矩阵的维度变换)

Python 实现:手把手编码

import numpy as np

class BPNN:
    def __init__(self, input_size, hidden_size):
        # Xavier 初始化(避坑点 1)self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1/input_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, 1) * np.sqrt(1/hidden_size)
        self.b2 = np.zeros(1)

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def sigmoid_derivative(self, x):
        return x * (1 - x)

    def forward(self, X):
        self.z1 = np.dot(X, self.W1) + self.b1  # 矩阵乘法
        self.a1 = self.sigmoid(self.z1)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        return self.sigmoid(self.z2)

    def backward(self, X, y, lr=0.01):
        # 输出层误差
        error = y - self.output
        delta_out = error * self.sigmoid_derivative(self.output)

        # 隐藏层误差(关键步骤)delta_hidden = np.dot(delta_out, self.W2.T) * \
                      self.sigmoid_derivative(self.a1)

        # 更新参数(避坑点 2:梯度裁剪)grad_W2 = np.dot(self.a1.T, delta_out)
        grad_W2 = np.clip(grad_W2, -1, 1)  # 防止梯度爆炸

        self.W2 += lr * grad_W2
        self.b2 += lr * np.sum(delta_out, axis=0)
        self.W1 += lr * np.dot(X.T, delta_hidden)
        self.b1 += lr * np.sum(delta_hidden, axis=0)

关键避坑指南

  1. 权重初始化
  2. 错误方法:直接用 np.random.rand(),会导致梯度消失
  3. 正确做法:Xavier 初始化,缩放因子为 $\sqrt{1/\text{ 输入维度}}$

  4. 学习率调参

  5. 先用 0.001 试跑,观察 loss 曲线
  6. 如果震荡剧烈(锯齿状),适当减小学习率
  7. 如果下降过慢,可尝试指数衰减策略

  8. 批量归一化

  9. 在隐藏层后添加 (a - np.mean(a))/np.std(a)
  10. 能使 loss 下降更平稳(效果见下图)

实战:鸢尾花分类

from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler

# 数据预处理
iris = load_iris()
X = iris.data[:100]  # 只用前两类
y = iris.target[:100].reshape(-1,1)
scaler = StandardScaler()
X = scaler.fit_transform(X)

# 训练网络
model = BPNN(input_size=4, hidden_size=5)
for epoch in range(1000):
    output = model.forward(X)
    model.backward(X, y, lr=0.1)
    if epoch % 100 == 0:
        loss = np.mean(0.5*(y-output)**2)
        print(f"Epoch {epoch}, Loss: {loss:.4f}")

思考题

当隐藏层使用 ReLU 激活函数时:
1. 反向传播中 $\sigma'(z)$ 需要替换成什么?
2. 在初始化权重时应该注意什么?
3. 可能会遇到什么新问题?(提示:死亡 ReLU 现象)

通过这个实现过程,我深刻体会到:理解反向传播最好的方式就是亲手推导公式 + 用代码实现。虽然第一次可能会写出满屏 bug 的程序,但每次 debug 都是对原理的加深理解。

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