共计 3498 个字符,预计需要花费 9 分钟才能阅读完成。
BP 神经网络实战:从 Softmax 到交叉熵损失函数的完整实现与优化
背景介绍
BP 神经网络是深度学习中最基础也最重要的模型之一,尤其在分类任务中表现出色。而 Softmax 函数和交叉熵损失函数则是 BP 神经网络分类任务中的黄金搭档。

- 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$ 很小时)。
解决方案 :
- 在计算 Softmax 前,先减去输入向量中的最大值
- 在计算对数时,给预测概率加上一个小常数(如 1e-15)避免 log(0)
性能优化
- 向量化操作 :使用 NumPy 的向量化操作替代循环,可以显著提高计算速度
- 批处理 :一次性处理多个样本比逐个处理更高效
- 内存布局 :确保数据在内存中是连续存储的,可以提高缓存命中率
避坑指南
- 梯度消失 / 爆炸 :如果网络层数较深,可能需要使用更先进的优化技术,如批归一化或残差连接
- 学习率设置不当 :过大的学习率可能导致模型无法收敛,过小的学习率则训练速度太慢
- 类别不平衡 :当各类别样本数量差异较大时,可以考虑使用加权交叉熵损失
实践建议
下面是一个简单的分类任务示例,使用鸢尾花数据集:
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 神经网络中这两个关键组件的实现细节。深度学习是一个实践性很强的领域,理解原理固然重要,但更重要的是通过实际编码来加深理解。祝你学习愉快!
正文完
