BP神经网络的正向传播与反向传播:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

神经网络基础回顾

BP 神经网络是一种多层前馈神经网络,通过误差反向传播算法进行训练。它由输入层、隐藏层和输出层组成,每层包含若干神经元。相邻层的神经元通过权重连接,每个神经元有自己的偏置。神经网络的学习过程就是不断调整这些权重和偏置,使得网络的输出尽可能接近期望的输出。

BP 神经网络的正向传播与反向传播:从数学原理到 Python 实现

正向传播的数学推导与实现

正向传播是指输入数据从输入层经过隐藏层最终到达输出层的过程。对于第 l 层的第 j 个神经元,其输入为:

$$z_j^{(l)} = \sum_{i} w_{ji}^{(l)} a_i^{(l-1)} + b_j^{(l)}$$

其中,$a_i^{(l-1)}$ 是第 l - 1 层第 i 个神经元的输出,$w_{ji}^{(l)}$ 是连接第 l - 1 层第 i 个神经元和第 l 层第 j 个神经元的权重,$b_j^{(l)}$ 是第 l 层第 j 个神经元的偏置。

神经元的输出通过激活函数 $\sigma$ 计算:

$$a_j^{(l)} = \sigma(z_j^{(l)})$$

常见的激活函数包括 sigmoid、ReLU 和 tanh 等。

反向传播的梯度计算详解

反向传播是通过链式法则计算损失函数对网络参数的梯度。假设使用均方误差作为损失函数:

$$L = \frac{1}{2}\sum_{k}(y_k – a_k^{(L)})^2$$

其中,$L$ 表示输出层,$y_k$ 是期望输出,$a_k^{(L)}$ 是实际输出。

对于输出层神经元,误差项为:

$$\delta_k^{(L)} = (a_k^{(L)} – y_k) \odot \sigma'(z_k^{(L)})$$

对于隐藏层神经元,误差项通过反向传播计算:

$$\delta_j^{(l)} = (\sum_{k} w_{kj}^{(l+1)} \delta_k^{(l+1)}) \odot \sigma'(z_j^{(l)})$$

最后,权重和偏置的梯度为:

$$\frac{\partial L}{\partial w_{ji}^{(l)}} = a_i^{(l-1)} \delta_j^{(l)}$$

$$\frac{\partial L}{\partial b_j^{(l)}} = \delta_j^{(l)}$$

完整 Python 代码实现

import numpy as np

class NeuralNetwork:
    def __init__(self, layers, learning_rate=0.01):
        self.layers = layers
        self.learning_rate = learning_rate
        self.weights = []
        self.biases = []

        # 初始化权重和偏置
        for i in range(len(layers)-1):
            # 使用 Xavier 初始化
            w = np.random.randn(layers[i+1], layers[i]) / np.sqrt(layers[i])
            b = np.zeros((layers[i+1], 1))
            self.weights.append(w)
            self.biases.append(b)

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

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

    def forward(self, x):
        a = x.T if x.ndim > 1 else x.reshape(-1, 1)
        activations = [a]
        zs = []

        for w, b in zip(self.weights, self.biases):
            z = np.dot(w, a) + b
            a = self.sigmoid(z)
            zs.append(z)
            activations.append(a)

        return activations[-1], activations, zs

    def backward(self, x, y, activations, zs):
        m = y.shape[0]
        y = y.T if y.ndim > 1 else y.reshape(-1, 1)

        # 计算输出层误差
        delta = (activations[-1] - y) * self.sigmoid_derivative(activations[-1])

        # 初始化梯度
        dw = [np.zeros(w.shape) for w in self.weights]
        db = [np.zeros(b.shape) for b in self.biases]

        # 输出层梯度
        dw[-1] = np.dot(delta, activations[-2].T) / m
        db[-1] = np.sum(delta, axis=1, keepdims=True) / m

        # 反向传播误差
        for l in range(2, len(self.layers)):
            delta = np.dot(self.weights[-l+1].T, delta) * self.sigmoid_derivative(activations[-l])
            dw[-l] = np.dot(delta, activations[-l-1].T) / m
            db[-l] = np.sum(delta, axis=1, keepdims=True) / m

        return dw, db

    def update_params(self, dw, db):
        for i in range(len(self.weights)):
            self.weights[i] -= self.learning_rate * dw[i]
            self.biases[i] -= self.learning_rate * db[i]

    def train(self, X, y, epochs=1000, batch_size=32):
        m = X.shape[0]

        for epoch in range(epochs):
            # 随机打乱数据
            permutation = np.random.permutation(m)
            X_shuffled = X[permutation]
            y_shuffled = y[permutation]

            for i in range(0, m, batch_size):
                # 获取当前 batch
                X_batch = X_shuffled[i:i+batch_size]
                y_batch = y_shuffled[i:i+batch_size]

                # 前向传播
                _, activations, zs = self.forward(X_batch)

                # 反向传播
                dw, db = self.backward(X_batch, y_batch, activations, zs)

                # 更新参数
                self.update_params(dw, db)

            # 打印训练进度
            if epoch % 100 == 0:
                output, _, _ = self.forward(X)
                loss = np.mean((output - y.T)**2)
                print(f'Epoch {epoch}, Loss: {loss:.4f}')

超参数调优建议

  1. 学习率:通常设置在 0.001 到 0.1 之间。太大会导致震荡,太小会收敛过慢。可以尝试学习率衰减策略。

  2. 批量大小:常见的批量大小有 32、64、128 等。较小的批量可以提供更多的梯度更新,但计算效率较低。较大的批量可以提供更稳定的梯度估计,但可能导致收敛到较差的局部最优。

  3. 网络结构:隐藏层数量和每层神经元数量需要根据任务复杂度调整。通常从较简单的网络开始,逐步增加复杂度。

  4. 激活函数:ReLU 通常比 sigmoid 和 tanh 有更好的表现,可以缓解梯度消失问题。

  5. 权重初始化:Xavier 初始化或 He 初始化通常比随机初始化表现更好。

常见错误与解决方案

  1. 梯度消失:当网络较深时,梯度可能在反向传播过程中变得非常小。解决方案包括使用 ReLU 激活函数、批归一化、残差连接等。

  2. 过拟合:网络在训练集上表现很好,但在测试集上表现差。解决方案包括增加训练数据、使用正则化(L1/L2)、Dropout 等。

  3. 学习率设置不当:学习率太大导致震荡,太小导致收敛慢。可以使用学习率衰减或自适应优化器(如 Adam)。

  4. 数据未归一化:输入数据范围差异大可能导致训练困难。应该对输入数据进行标准化或归一化处理。

  5. 权重初始化不当:全零初始化会导致所有神经元学习相同的特征。应该使用随机初始化或更高级的初始化方法。

延伸思考题

  1. 如何修改代码实现不同的激活函数(如 ReLU、Leaky ReLU 等)?不同的激活函数对反向传播有什么影响?

  2. 除了均方误差,还可以使用哪些损失函数?它们分别适用于什么场景?

  3. 如何实现动量(Momentum)或 Adam 优化器来加速训练?

  4. 批归一化(Batch Normalization)是如何工作的?如何在现有代码中实现?

  5. 如何可视化训练过程中的权重变化和损失函数变化?这对调参有什么帮助?

通过这篇文章的学习,你应该已经掌握了 BP 神经网络的核心原理和实现方法。接下来可以尝试在更复杂的数据集上应用这些知识,或者探索更先进的神经网络结构和训练技巧。

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