BP神经网络之BP算法手写推导:从数学原理到代码实现

1次阅读
没有评论

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

image.webp

理解 BP 算法的数学本质

反向传播(Backpropagation)是神经网络训练的核心算法,其本质是复合函数的链式求导法则。我们先从一个简单的 3 层神经网络(输入层、隐藏层、输出层)入手推导。

BP 神经网络之 BP 算法手写推导:从数学原理到代码实现

设神经网络有以下参数:
– 输入层到隐藏层权重矩阵 (W^{(1)})
– 隐藏层到输出层权重矩阵 (W^{(2)})
– 隐藏层激活函数 (f(\cdot))
– 输出层激活函数 (g(\cdot))
– 损失函数 (L(y, \hat{y}))

前向传播过程

  1. 隐藏层输入:
    [z^{(1)} = xW^{(1)} ]
  2. 隐藏层输出:
    [a^{(1)} = f(z^{(1)}) ]
  3. 输出层输入:
    [z^{(2)} = a^{(1)}W^{(2)} ]
  4. 最终输出:
    [\hat{y} = g(z^{(2)}) ]

反向传播推导关键步骤

我们需要计算损失函数对各个权重的偏导数。以输出层权重 (W^{(2)}) 为例:

  1. 计算损失函数对输出的偏导:
    [\frac{\partial L}{\partial \hat{y}} ]
  2. 计算输出对 (z^{(2)}) 的偏导(取决于输出层激活函数):
    [\frac{\partial \hat{y}}{\partial z^{(2)}} = g'(z^{(2)}) ]
  3. 计算 (z^{(2)}) 对 (W^{(2)}) 的偏导:
    [\frac{\partial z^{(2)}}{\partial W^{(2)}} = a^{(1)T} ]
  4. 组合得到权重梯度:
    [\frac{\partial L}{\partial W^{(2)}} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z^{(2)}} \cdot \frac{\partial z^{(2)}}{\partial W^{(2)}} ]

同理可以推导隐藏层权重 (W^{(1)}) 的梯度。这个链式求导过程会从输出层逐层反向传播到输入层。

Python 实现 BP 神经网络

下面我们实现一个简单的全连接神经网络类:

import numpy as np

class SimpleNN:
    def __init__(self, input_size, hidden_size, output_size):
        # 初始化权重(使用 Xavier 初始化)self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1/input_size)
        self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1/hidden_size)

    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.a1 = self.sigmoid(self.z1)
        self.z2 = np.dot(self.a1, self.W2)
        y_hat = self.sigmoid(self.z2)
        return y_hat

    def backward(self, X, y, y_hat, learning_rate):
        # 输出层误差
        error = y_hat - y
        d_output = error * self.sigmoid_derivative(y_hat)

        # 隐藏层误差
        error_hidden = np.dot(d_output, self.W2.T)
        d_hidden = error_hidden * self.sigmoid_derivative(self.a1)

        # 更新权重
        self.W2 -= learning_rate * np.dot(self.a1.T, d_output)
        self.W1 -= learning_rate * np.dot(X.T, d_hidden)

    def train(self, X, y, epochs, learning_rate):
        for i in range(epochs):
            y_hat = self.forward(X)
            self.backward(X, y, y_hat, learning_rate)

            # 每 100 次迭代打印损失
            if i % 100 == 0:
                loss = np.mean(np.square(y - y_hat))
                print(f'Epoch {i}, Loss: {loss:.4f}')

代码关键点说明

  1. 前向传播 :
  2. 计算每一层的加权输入和激活输出
  3. 使用 sigmoid 作为激活函数

  4. 反向传播 :

  5. 从输出层开始计算误差
  6. 通过链式法则逐层计算各权重的梯度
  7. 使用梯度下降更新权重

  8. 训练过程 :

  9. 多次迭代前向传播和反向传播
  10. 监控损失函数的变化

验证与自动微分对比

为了验证我们的手动实现是否正确,可以使用 PyTorch 的自动微分进行对比验证:

import torch
import torch.nn as nn

# 使用相同参数初始化 PyTorch 模型
torch_model = nn.Sequential(nn.Linear(input_size, hidden_size),
    nn.Sigmoid(),
    nn.Linear(hidden_size, output_size),
    nn.Sigmoid())

# 复制权重
with torch.no_grad():
    torch_model[0].weight.copy_(torch.FloatTensor(self.W1.T))
    torch_model[2].weight.copy_(torch.FloatTensor(self.W2.T))

# 使用相同输入计算梯度
...

通过比较手动计算梯度和自动微分结果,可以验证我们推导的正确性。

常见问题与优化建议

  1. 梯度爆炸与消失 :
  2. 使用合适的权重初始化(如 Xavier 初始化)
  3. 添加梯度裁剪(gradient clipping)
  4. 考虑使用 ReLU 等缓解梯度消失的激活函数

  5. 学习率选择 :

  6. 初始学习率建议设置较小值(如 0.01)
  7. 实现学习率衰减策略

  8. 批量训练优化 :

  9. 计算小批量数据的平均梯度
  10. 使用动量(momentum)加速训练

延伸思考方向

  1. 向量化优化 :
  2. 当前实现已经向量化处理整个批量的数据
  3. 可以进一步优化矩阵运算顺序减少临时变量

  4. 高级优化器 :

  5. 实现 Adam 等自适应学习率优化器
  6. 比较不同优化器在相同网络上的表现

  7. 正则化技术 :

  8. 添加 L2 权重衰减(正则化)
  9. 实现 dropout 层防止过拟合

通过这次手写推导和代码实现,我们对 BP 算法有了更深入的理解。建议读者可以尝试扩展这个基础实现,比如添加更多隐藏层、实现不同的激活函数,或者尝试在其他数据集上训练。

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