BP神经网络分类实战:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

背景:为什么选择 BP 神经网络

BP 神经网络(Backpropagation Neural Network)是解决分类问题的利器,尤其适合处理复杂的非线性边界。与 SVM、决策树相比,它的优势在于:

BP 神经网络分类实战:从数学原理到 Python 实现

  • 自动学习特征组合,无需手动设计特征
  • 通过增加隐藏层可以逼近任意连续函数
  • 对高维数据表现稳定(比如图像分类)

典型应用场景包括:
– 银行信用评分(二分类)
– 手写数字识别(多分类)
– 医疗影像分析

数学原理:反向传播到底在传播什么

核心是链式求导法则。假设网络有 1 个隐藏层,输出层使用 sigmoid 激活函数:

  1. 前向传播:
    $$
    \begin{aligned}
    z^{[1]} &= W^{[1]}X + b^{[1]} \
    a^{[1]} &= \text{ReLU}(z^{[1]}) \
    z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} \
    \hat{y} &= \sigma(z^{[2]})
    \end{aligned}
    $$

  2. 损失函数(交叉熵):
    $$ L = -\frac{1}{m}\sum_{i=1}^m [y^{(i)}\log(\hat{y}^{(i)}) + (1-y^{(i)})\log(1-\hat{y}^{(i)})] $$

  3. 反向传播 关键导数:
    $$
    \begin{aligned}
    dz^{[2]} &= \hat{y} – y \
    dW^{[2]} &= \frac{1}{m}dz^{[2]}a^{[1]T} \
    db^{[2]} &= \frac{1}{m}\sum dz^{[2]} \
    dz^{[1]} &= W^{[2]T}dz^{[2]} \odot \text{ReLU}'(z^{[1]})
    \end{aligned}
    $$

Python 实现:手把手搭建网络

import numpy as np
from sklearn.datasets import make_moons

# 数据准备
X, y = make_moons(n_samples=1000, noise=0.2)
y = y.reshape(-1, 1)

class BPNet:
    def __init__(self, layer_dims):
        self.params = {}
        for l in range(1, len(layer_dims)):
            # He 初始化应对 ReLU
            self.params[f'W{l}'] = np.random.randn(layer_dims[l], layer_dims[l-1]) * np.sqrt(2/layer_dims[l-1])
            self.params[f'b{l}'] = np.zeros((layer_dims[l], 1))

    def forward(self, X):
        cache = {'A0': X}
        L = len(self.params) // 2
        for l in range(1, L):
            cache[f'Z{l}'] = np.dot(self.params[f'W{l}'], cache[f'A{l-1}']) + self.params[f'b{l}']
            cache[f'A{l}'] = np.maximum(0, cache[f'Z{l}'])  # ReLU
        # 输出层用 sigmoid
        cache[f'Z{L}'] = np.dot(self.params[f'W{L}'], cache[f'A{L-1}']) + self.params[f'b{L}']
        cache[f'A{L}'] = 1/(1+np.exp(-cache[f'Z{L}']))
        return cache

    def compute_loss(self, AL, y, lambd=0.1):
        m = y.shape[1]
        # 交叉熵损失 + L2 正则化
        cross_entropy = -np.mean(y*np.log(AL) + (1-y)*np.log(1-AL))
        l2_penalty = sum(np.sum(np.square(W)) for W in self.params.values() if W.ndim > 1)
        return cross_entropy + (lambd/(2*m)) * l2_penalty

    def backward(self, cache, y, lambd=0.1):
        grads = {}
        m = y.shape[1]
        L = len(self.params) // 2
        # 输出层梯度
        dZL = cache[f'A{L}'] - y
        grads[f'dW{L}'] = np.dot(dZL, cache[f'A{L-1}'].T)/m + (lambd/m)*self.params[f'W{L}']
        grads[f'db{L}'] = np.sum(dZL, axis=1, keepdims=True)/m
        # 隐藏层梯度
        for l in reversed(range(1, L)):
            dAl = np.dot(self.params[f'W{l+1}'].T, dZL)
            dZl = dAl * (cache[f'Z{l}'] > 0)  # ReLU 导数
            grads[f'dW{l}'] = np.dot(dZl, cache[f'A{l-1}'].T)/m + (lambd/m)*self.params[f'W{l}']
            grads[f'db{l}'] = np.sum(dZl, axis=1, keepdims=True)/m
            dZL = dZl
        return grads

    def update_params(self, grads, learning_rate):
        for key in self.params:
            self.params[key] -= learning_rate * grads[f'd{key}']

# 训练示例
net = BPNet([2, 4, 1])  # 输入层 2 维,隐藏层 4 神经元,输出 1 维
for epoch in range(1000):
    cache = net.forward(X.T)
    loss = net.compute_loss(cache['A2'], y.T)
    grads = net.backward(cache, y.T)
    net.update_params(grads, learning_rate=0.01*(1-epoch/1000))  # 学习率衰减

调优实战技巧

  1. 隐藏层设计:
  2. 通常从 [输入维度, 输出维度] 之间的数值开始尝试
  3. 示例:对于 MNIST 分类(784 输入,10 输出),可先试[784, 256, 10]

  4. 激活函数对比:

    # Sigmoid vs ReLU 实验
    sigmoid_loss = []
    relu_loss = []
    for _ in range(10):
        # sigmoid 版本会梯度消失
        sigmoid_loss.append(train_with_activation('sigmoid'))
        relu_loss.append(train_with_activation('relu'))

  5. 早停法实现:

    best_loss = float('inf')
    patience = 5
    for epoch in range(100):
        # ... 训练代码...
        if loss < best_loss:
            best_loss = loss
            counter = 0
        else:
            counter += 1
            if counter >= patience:
                break

扩展思考

  1. 与传统算法对比:
  2. 决策树:训练更快,但难以处理细微特征差异
  3. SVM:在小数据集上可能更优,但核函数选择困难

  4. 批归一化改进:

    # 在前向传播中加入
    Z = np.dot(W, A_prev) + b
    Z_norm = (Z - np.mean(Z))/np.std(Z)
    Z_tilde = gamma * Z_norm + beta  # 可学习参数

结语

通过这次实践,我深刻体会到 BP 神经网络就像 ” 可编程的数学函数 ”——通过调整网络结构,它能自动学习到最适合当前任务的变换方式。建议初学者多尝试可视化权重矩阵(如plt.imshow(W1)),这能直观理解网络学到了什么特征。

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