BP神经网络实战:从零实现鸢尾花分类的简明指南

1次阅读
没有评论

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

image.webp

背景与痛点

鸢尾花数据集(Iris Dataset)是机器学习领域的经典入门数据集,包含 150 个样本,每个样本有 4 个特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度)和 3 个类别标签(Setosa、Versicolour、Virginica)。这个数据集非常适合用来学习分类算法,因为它的特征维度适中,类别区分度明显。

BP 神经网络实战:从零实现鸢尾花分类的简明指南

然而,初学者在使用 BP 神经网络实现鸢尾花分类时,常常会遇到以下问题:

  • 梯度消失:当网络层数较多时,梯度在反向传播过程中会逐渐变小,导致权重更新缓慢甚至停滞。
  • 过拟合:模型在训练集上表现很好,但在测试集上表现不佳,泛化能力差。
  • 调参困难:学习率、隐藏层节点数等超参数的选择对模型性能影响很大,但缺乏明确的指导原则。

技术实现

网络结构设计

  1. 输入层:鸢尾花数据集有 4 个特征,因此输入层节点数设为 4。
  2. 隐藏层:选择 1 个隐藏层,节点数设为 10(经验法则:隐藏层节点数可以是输入层节点数的 2 - 3 倍)。
  3. 输出层:有 3 个类别,因此输出层节点数设为 3,使用 Softmax 函数输出概率分布。

激活函数选择

Sigmoid 函数是 BP 神经网络中常用的激活函数,其公式为:

$$
\sigma(x) = \frac{1}{1 + e^{-x}}
$$

选择 Sigmoid 的原因:

  • 输出范围在 (0,1) 之间,适合概率输出。
  • 导数容易计算:(\sigma'(x) = \sigma(x)(1 – \sigma(x)))。

反向传播过程

反向传播的核心是通过链式法则计算损失函数对权重的梯度。以下是关键步骤:

  1. 计算输出层误差:
    $$
    \delta^{(L)} = (y – \hat{y}) \odot \sigma'(z^{(L)})
    $$
  2. 计算隐藏层误差:
    $$
    \delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot \sigma'(z^{(l)})
    $$
  3. 更新权重:
    $$
    W^{(l)} \leftarrow W^{(l)} – \eta \delta^{(l)} (a^{(l-1)})^T
    $$

代码示例

import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target

# 数据标准化
scaler = StandardScaler()
X = scaler.fit_transform(X)

# 将标签转换为 one-hot 编码
def to_one_hot(y, num_classes):
    return np.eye(num_classes)[y]

y_one_hot = to_one_hot(y, 3)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y_one_hot, test_size=0.2, random_state=42)

# 定义网络结构
input_size = 4
hidden_size = 10
output_size = 3

# 初始化权重
def initialize_weights(input_size, hidden_size, output_size):
    W1 = np.random.randn(input_size, hidden_size) * 0.01
    b1 = np.zeros((1, hidden_size))
    W2 = np.random.randn(hidden_size, output_size) * 0.01
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

W1, b1, W2, b2 = initialize_weights(input_size, hidden_size, output_size)

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

def sigmoid_derivative(x):
    return sigmoid(x) * (1 - sigmoid(x))

# 定义损失函数(交叉熵)def cross_entropy_loss(y_true, y_pred):
    m = y_true.shape[0]
    return -np.sum(y_true * np.log(y_pred + 1e-15)) / m

# 前向传播
def forward_propagation(X, W1, b1, W2, b2):
    z1 = np.dot(X, W1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, W2) + b2
    a2 = np.exp(z2) / np.sum(np.exp(z2), axis=1, keepdims=True)
    return a1, a2

# 反向传播
def backward_propagation(X, y, a1, a2, W2):
    m = X.shape[0]
    dz2 = a2 - y
    dW2 = np.dot(a1.T, dz2) / m
    db2 = np.sum(dz2, axis=0, keepdims=True) / m
    dz1 = np.dot(dz2, W2.T) * sigmoid_derivative(a1)
    dW1 = np.dot(X.T, dz1) / m
    db1 = np.sum(dz1, axis=0, keepdims=True) / m
    return dW1, db1, dW2, db2

# 训练模型
def train(X, y, W1, b1, W2, b2, learning_rate=0.1, epochs=1000):
    losses = []
    for epoch in range(epochs):
        a1, a2 = forward_propagation(X, W1, b1, W2, b2)
        loss = cross_entropy_loss(y, a2)
        losses.append(loss)
        dW1, db1, dW2, db2 = backward_propagation(X, y, a1, a2, W2)
        W1 -= learning_rate * dW1
        b1 -= learning_rate * db1
        W2 -= learning_rate * dW2
        b2 -= learning_rate * db2
        if epoch % 100 == 0:
            print(f'Epoch {epoch}, Loss: {loss}')
    return W1, b1, W2, b2, losses

# 训练模型
W1, b1, W2, b2, losses = train(X_train, y_train, W1, b1, W2, b2, learning_rate=0.1, epochs=1000)

# 预测函数
def predict(X, W1, b1, W2, b2):
    _, a2 = forward_propagation(X, W1, b1, W2, b2)
    return np.argmax(a2, axis=1)

# 评估模型
y_pred = predict(X_test, W1, b1, W2, b2)
y_true = np.argmax(y_test, axis=1)
accuracy = np.mean(y_pred == y_true)
print(f'Test Accuracy: {accuracy * 100:.2f}%')

优化实践

学习率调整

学习率是影响模型收敛速度的关键参数。以下是不同学习率的对比:

  • 学习率 =0.01:收敛慢,可能需要更多 epoch。
  • 学习率 =0.1:通常是一个较好的初始选择。
  • 学习率 =1.0:可能导致震荡甚至发散。

L2 正则化

为了防止过拟合,可以在损失函数中加入 L2 正则化项:

def cross_entropy_loss_with_l2(y_true, y_pred, W1, W2, lambda_val=0.01):
    m = y_true.shape[0]
    cross_entropy = -np.sum(y_true * np.log(y_pred + 1e-15)) / m
    l2_penalty = (np.sum(W1 ** 2) + np.sum(W2 ** 2)) * lambda_val / (2 * m)
    return cross_entropy + l2_penalty

模型评估指标

除了准确率,还可以使用混淆矩阵来更详细地评估模型性能:

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_true, y_pred)
print('Confusion Matrix:')
print(cm)

避坑指南

  1. 特征缩放:输入特征的范围差异较大时,务必进行标准化或归一化,否则可能导致梯度下降缓慢或不收敛。
  2. 隐藏层节点数:通常设置为输入层节点数的 2 - 3 倍,但可以通过交叉验证来调整。
  3. 训练终止条件:可以设置早停(early stopping)机制,当验证集上的损失不再下降时停止训练。

延伸思考

  1. 激活函数改进:可以尝试 ReLU 激活函数,它在深层网络中表现更好,能缓解梯度消失问题。
  2. 可视化权重:通过绘制权重分布图,可以直观地了解模型的学习情况。
  3. 增加隐藏层:对于更复杂的数据集,可以尝试增加隐藏层数,构建更深的网络。

总结

本文详细介绍了如何使用 BP 神经网络实现鸢尾花分类,从网络结构设计到代码实现,再到优化和评估,涵盖了整个流程的关键步骤。希望这篇指南能帮助初学者快速上手 BP 神经网络,并将其应用到实际项目中。

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