BP神经网络实战:从Softmax到交叉熵损失函数的完整实现与优化

1次阅读
没有评论

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

image.webp

BP 神经网络实战:从 Softmax 到交叉熵损失函数的完整实现与优化

背景介绍

BP 神经网络是深度学习中最基础也最重要的模型之一,尤其在分类任务中表现出色。而 Softmax 函数和交叉熵损失函数则是 BP 神经网络分类任务中的黄金搭档。

BP 神经网络实战:从 Softmax 到交叉熵损失函数的完整实现与优化

  • Softmax 函数 :将神经网络的输出转化为概率分布,使每个类别的预测值介于 0 到 1 之间,且所有类别概率之和为 1。
  • 交叉熵损失函数 :衡量预测概率分布与真实概率分布之间的差异,是分类任务中最常用的损失函数。

理解并正确实现这两个组件,是每个深度学习初学者的必修课。

数学原理

Softmax 函数

Softmax 函数的数学表达式如下:

$$
\text{Softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}}
$$

其中,$z_i$ 是第 $i$ 个类别的网络输出,$K$ 是类别总数。

交叉熵损失函数

对于单个样本,交叉熵损失函数的定义为:

$$
L = -\sum_{i=1}^K y_i \log(p_i)
$$

其中,$y_i$ 是真实标签的 one-hot 编码,$p_i$ 是 Softmax 输出的预测概率。

代码实现

Softmax 函数的向量化实现

import numpy as np

def softmax(z):
    """
    计算 Softmax 函数
    :param z: 输入向量,形状为 (n_samples, n_classes)
    :return: Softmax 输出,形状与输入相同
    """
    # 减去最大值提高数值稳定性
    e_z = np.exp(z - np.max(z, axis=1, keepdims=True))
    return e_z / np.sum(e_z, axis=1, keepdims=True)

交叉熵损失函数

def cross_entropy_loss(y_true, y_pred):
    """
    计算交叉熵损失
    :param y_true: 真实标签的 one-hot 编码,形状为 (n_samples, n_classes)
    :param y_pred: 预测概率,形状与 y_true 相同
    :return: 平均交叉熵损失
    """
    # 避免 log(0) 的情况
    epsilon = 1e-15
    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)

    # 计算损失
    loss = -np.sum(y_true * np.log(y_pred)) / y_true.shape[0]
    return loss

反向传播梯度计算

def softmax_cross_entropy_backward(y_true, y_pred):
    """
    计算 Softmax + 交叉熵的反向传播梯度
    :param y_true: 真实标签的 one-hot 编码,形状为 (n_samples, n_classes)
    :param y_pred: 预测概率,形状与 y_true 相同
    :return: 梯度,形状与输入相同
    """
    return (y_pred - y_true) / y_true.shape[0]

常见问题

数值稳定性问题

在实现 Softmax 时,直接计算 $e^{z_i}$ 可能会导致数值溢出(当 $z_i$ 很大时)或数值下溢(当 $z_i$ 很小时)。

解决方案 :

  1. 在计算 Softmax 前,先减去输入向量中的最大值
  2. 在计算对数时,给预测概率加上一个小常数(如 1e-15)避免 log(0)

性能优化

  1. 向量化操作 :使用 NumPy 的向量化操作替代循环,可以显著提高计算速度
  2. 批处理 :一次性处理多个样本比逐个处理更高效
  3. 内存布局 :确保数据在内存中是连续存储的,可以提高缓存命中率

避坑指南

  • 梯度消失 / 爆炸 :如果网络层数较深,可能需要使用更先进的优化技术,如批归一化或残差连接
  • 学习率设置不当 :过大的学习率可能导致模型无法收敛,过小的学习率则训练速度太慢
  • 类别不平衡 :当各类别样本数量差异较大时,可以考虑使用加权交叉熵损失

实践建议

下面是一个简单的分类任务示例,使用鸢尾花数据集:

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder

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

# 转换为 one-hot 编码
encoder = OneHotEncoder(sparse=False)
y_onehot = encoder.fit_transform(y)

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

# 简单的神经网络实现
class SimpleNN:
    def __init__(self, input_size, hidden_size, output_size):
        # 初始化权重
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))

    def forward(self, X):
        # 前向传播
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = np.tanh(self.z1)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        self.a2 = softmax(self.z2)
        return self.a2

    def backward(self, X, y, learning_rate=0.01):
        # 反向传播
        m = X.shape[0]

        # 输出层梯度
        dz2 = softmax_cross_entropy_backward(y, self.a2)
        dw2 = np.dot(self.a1.T, dz2)
        db2 = np.sum(dz2, axis=0, keepdims=True)

        # 隐藏层梯度
        da1 = np.dot(dz2, self.W2.T)
        dz1 = da1 * (1 - np.power(self.a1, 2))  # tanh 导数
        dw1 = np.dot(X.T, dz1)
        db1 = np.sum(dz1, axis=0, keepdims=True)

        # 更新参数
        self.W1 -= learning_rate * dw1
        self.b1 -= learning_rate * db1
        self.W2 -= learning_rate * dw2
        self.b2 -= learning_rate * db2

    def train(self, X, y, epochs=1000, learning_rate=0.01):
        for epoch in range(epochs):
            # 前向传播
            y_pred = self.forward(X)

            # 计算损失
            loss = cross_entropy_loss(y, y_pred)

            # 反向传播
            self.backward(X, y, learning_rate)

            if epoch % 100 == 0:
                print(f"Epoch {epoch}, Loss: {loss:.4f}")

# 训练模型
input_size = X_train.shape[1]
hidden_size = 5
output_size = y_train.shape[1]

model = SimpleNN(input_size, hidden_size, output_size)
model.train(X_train, y_train, epochs=1000, learning_rate=0.01)

# 测试模型
y_pred_test = model.forward(X_test)
predictions = np.argmax(y_pred_test, axis=1)
true_labels = np.argmax(y_test, axis=1)
accuracy = np.mean(predictions == true_labels)
print(f"Test Accuracy: {accuracy:.4f}")

通过这个完整的示例,你可以看到 BP 神经网络、Softmax 和交叉熵损失函数是如何协同工作的。建议读者动手实践这个代码,并通过调整网络结构、学习率等参数来观察模型性能的变化。

希望这篇文章能帮助你理解 BP 神经网络中这两个关键组件的实现细节。深度学习是一个实践性很强的领域,理解原理固然重要,但更重要的是通过实际编码来加深理解。祝你学习愉快!

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