本教程旨在帮助初学者理解神经网络的基本原理和实现,特别针对二分类任务,深入解析其正向传播和反向传播的数学推导,并逐步用 numpy
实现完整的神经网络模型,最终使用 PyTorch
简化实现。
神经网络基本概念
神经网络是一种**基于神经元(节点)构建的机器学习模型。每个节点接收输入、执行计算并输出结果。通过多个层(layer)**的堆叠和复杂的计算结构,神经网络可以逼近任意的非线性关系。
数据准备
我们首先生成一个简单的二分类数据集。每个数据点有两个特征,我们希望训练一个神经网络模型预测其类别(0 或 1)。
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
np.random.seed(0)
num_points = 200
X = np.random.randn(num_points, 2)
y = (X[:, 0] * X[:, 1] > 0).astype(int) # 创建一个简单的二分类数据集
# 数据可视化
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='red', label='Class 0')
plt.scatter(X[y == 1, 0], X[y == 1, 1], color='blue', label='Class 1')
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.title("Binary Classification Data")
plt.legend()
plt.show()
构建神经网络的基本结构
我们构建一个简单的神经网络结构,包含:
- 输入层:接受数据输入。
- 隐藏层:使用ReLU激活函数进行非线性变换。
- 输出层:通过sigmoid激活函数输出0到1之间的概率,用于二分类。
正向传播(Forward Propagation)
正向传播是指数据从输入层流向输出层的计算过程。
-
输入层到隐藏层:计算
Z1 = X * W1 + b1
。 - 激活函数:在隐藏层使用 ReLU 函数。
-
隐藏层到输出层:计算
Z2 = A1 * W2 + b2
,然后使用 sigmoid 激活函数获得输出概率。
# 初始化参数函数
def initialize_parameters(input_dim, hidden_dim, output_dim):
"""
初始化神经网络的权重和偏置参数。
参数:
- input_dim: 输入层神经元数量(即特征数量)
- hidden_dim: 隐藏层神经元数量
- output_dim: 输出层神经元数量(对于二分类输出为1)
返回:
- W1: 输入层到隐藏层的权重矩阵,形状为 (input_dim, hidden_dim)
- b1: 隐藏层的偏置项,形状为 (1, hidden_dim)
- W2: 隐藏层到输出层的权重矩阵,形状为 (hidden_dim, output_dim)
- b2: 输出层的偏置项,形状为 (1, output_dim)
"""
np.random.seed(1) # 固定随机种子,保证每次初始化相同
# 初始化 W1,权重较小以防止初始值过大影响训练
W1 = np.random.randn(input_dim, hidden_dim) * 0.01
b1 = np.zeros((1, hidden_dim)) # 初始化 b1 为零,避免对初始输出的影响
W2 = np.random.randn(hidden_dim, output_dim) * 0.01
b2 = np.zeros((1, output_dim))
return W1, b1, W2, b2
# ReLU 激活函数
def relu(Z):
"""
ReLU(线性整流)激活函数,返回输入中每个值的最大值和 0 的较大值。
参数:
- Z: 输入数组,可以是任意形状
返回:
- A: ReLU 激活后的输出,与 Z 形状相同
"""
return np.maximum(0, Z)
# Sigmoid 激活函数
def sigmoid(Z):
"""
Sigmoid 激活函数,将输入值映射到 0 到 1 之间的范围,用于二分类问题。
参数:
- Z: 输入数组,可以是任意形状
返回:
- A: Sigmoid 激活后的输出,与 Z 形状相同
"""
return 1 / (1 + np.exp(-Z))
# 正向传播函数
def forward_propagation(X, W1, b1, W2, b2):
"""
执行神经网络的正向传播过程。
参数:
- X: 输入数据矩阵,形状为 (样本数, 输入层神经元数)
- W1, b1: 输入层到隐藏层的权重和偏置
- W2, b2: 隐藏层到输出层的权重和偏置
返回:
- A2: 输出层激活值,形状为 (样本数, 输出层神经元数)
- cache: 包含正向传播中间计算结果的字典(供反向传播使用)
"""
# 计算隐藏层的线性组合 Z1 = X * W1 + b1
Z1 = np.dot(X, W1) + b1
# 通过 ReLU 激活函数得到 A1
A1 = relu(Z1)
# 计算输出层的线性组合 Z2 = A1 * W2 + b2
Z2 = np.dot(A1, W2) + b2
# 通过 Sigmoid 激活函数得到最终输出 A2
A2 = sigmoid(Z2)
# 将所有计算结果缓存,以便反向传播时使用
cache = (Z1, A1, W1, b1, Z2, A2, W2, b2)
return A2, cache
损失函数(Loss Function)
对于二分类任务,常用的损失函数是二元交叉熵损失(binary cross-entropy loss),其定义为:
Loss
=
−
1
m
∑
i
=
1
m
(
y
(
i
)
log
(
y
^
(
i
)
)
+
(
1
−
y
(
i
)
)
log
(
1
−
y
^
(
i
)
)
)
\text{Loss} = -\frac{1}{m} \sum_{i=1}^m (y^{(i)} \log(\hat{y}^{(i)}) + (1 - y^{(i)}) \log(1 - \hat{y}^{(i)}))
Loss=−m1i=1∑m(y(i)log(y^(i))+(1−y(i))log(1−y^(i)))
# 定义损失函数
def compute_loss(A2, Y):
"""
计算二分类问题的交叉熵损失函数。
参数:
- A2: 模型的预测输出,形状为 (样本数, 1),值在 0 到 1 之间
- Y: 实际标签,形状为 (样本数, 1),值为 0 或 1
返回:
- loss: 交叉熵损失的平均值,标量
"""
m = Y.shape[0] # 获取样本数
# 计算交叉熵损失,每个样本的损失为 -[y*log(a) + (1-y)*log(1-a)]
loss = -np.mean(Y * np.log(A2) + (1 - Y) * np.log(1 - A2))
return loss
反向传播(Backward Propagation)
反向传播通过链式法则计算损失函数对每个参数的梯度,从而更新参数以最小化损失。具体步骤如下:
- 计算输出层的梯度。
- 通过输出层的梯度,进一步计算隐藏层的梯度。
- 使用梯度下降算法更新参数。
# 反向传播函数
def backward_propagation(X, Y, cache):
"""
执行神经网络的反向传播计算梯度,以用于更新参数。
参数:
- X: 输入数据矩阵,形状为 (样本数, 输入层神经元数)
- Y: 实际标签,形状为 (样本数, 1)
- cache: 包含正向传播中间结果的字典,供反向传播计算使用
返回:
- gradients: 包含各层参数梯度的字典,供梯度下降法更新参数
"""
# 解包缓存的中间结果
Z1, A1, W1, b1, Z2, A2, W2, b2 = cache
m = X.shape[0] # 获取样本数量
# 计算输出层的梯度
dZ2 = A2 - Y # A2 是模型预测值,Y 是实际值,计算损失对 Z2 的梯度
dW2 = (1 / m) * np.dot(A1.T, dZ2) # 损失对 W2 的梯度
db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True) # 损失对 b2 的梯度
# 计算隐藏层的梯度
dA1 = np.dot(dZ2, W2.T) # 反向传播 dZ2,通过 W2 获得 dA1
dZ1 = dA1 * (Z1 > 0) # ReLU 导数,当 Z1 > 0 时导数为 1,否则为 0
dW1 = (1 / m) * np.dot(X.T, dZ1) # 损失对 W1 的梯度
db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True) # 损失对 b1 的梯度
# 将各梯度存入字典,便于更新
gradients = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}
return gradients
参数更新
通过反向传播获得梯度后,我们使用梯度下降算法更新参数。更新公式如下:
W = W − α ⋅ d W b = b − α ⋅ d b W = W - \alpha \cdot dW\\ b = b - \alpha \cdot db W=W−α⋅dWb=b−α⋅db
# 参数更新函数
def update_parameters(W1, b1, W2, b2, gradients, learning_rate=0.01):
"""
使用梯度下降法更新模型参数。
参数:
- W1, b1, W2, b2: 当前神经网络的权重和偏置参数
- gradients: 包含各参数梯度的字典(从反向传播计算得出)
- learning_rate: 学习率,控制更新步长,默认为 0.01
返回:
- W1, b1, W2, b2: 更新后的参数
"""
# 使用学习率调整各参数的梯度,更新 W1
W1 -= learning_rate * gradients["dW1"]
# 更新 b1
b1 -= learning_rate * gradients["db1"]
# 更新 W2
W2 -= learning_rate * gradients["dW2"]
# 更新 b2
b2 -= learning_rate * gradients["db2"]
return W1, b1, W2, b2
训练神经网络
我们将上述步骤整合到一个完整的训练过程中,循环执行正向传播、损失计算、反向传播和参数更新。
# 定义训练神经网络的函数
def train_neural_network(X, Y, input_dim, hidden_dim, output_dim, epochs=1000, learning_rate=0.01):
"""
训练神经网络模型,使用梯度下降法优化参数。
参数:
- X: 输入数据矩阵,形状为 (样本数, 输入层神经元数)
- Y: 实际标签矩阵,形状为 (样本数, 1)
- input_dim: 输入层的神经元数量
- hidden_dim: 隐藏层的神经元数量
- output_dim: 输出层的神经元数量
- epochs: 训练迭代次数,默认为 1000
- learning_rate: 学习率,控制每次参数更新的步长,默认为 0.01
返回:
- W1, b1, W2, b2: 训练好的参数
- losses: 每个 epoch 的损失值列表,用于可视化
"""
# 初始化参数
W1, b1, W2, b2 = initialize_parameters(input_dim, hidden_dim, output_dim)
losses = [] # 存储每个 epoch 的损失值,用于后续绘图
# 迭代训练
for epoch in range(epochs):
# 正向传播
A2, cache = forward_propagation(X, W1, b1, W2, b2)
# 计算损失
loss = compute_loss(A2, Y)
losses.append(loss) # 保存当前损失值
# 反向传播
gradients = backward_propagation(X, Y, cache)
# 更新参数
W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, gradients, learning_rate)
# 每 100 个 epoch 打印一次损失
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss}")
return W1, b1, W2, b2, losses
# 使用数据训练神经网络
Y = y.reshape(-1, 1) # 确保标签是列向量,符合网络输入要求
W1, b1, W2, b2, losses = train_neural_network(X, Y, input_dim=2, hidden_dim=4, output_dim=1, epochs=1000, learning_rate=0.1)
# 可视化训练过程中的损失变化
import matplotlib.pyplot as plt
plt.plot(losses)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss")
plt.show()
模型预测
模型训练后,我们可以使用训练得到的参数进行预测。
# 定义预测函数
def predict(X, W1, b1, W2, b2):
A2, _ = forward_propagation(X, W1, b1, W2, b2)
predictions = (A2 > 0.5).astype(int)
return predictions
# 预测并可视化分类结果
predictions = predict(X, W1, b1, W2, b2)
# 绘制预测结果
plt.scatter(X[predictions.flatten() == 0, 0], X[predictions.flatten() == 0, 1], color='red', label='Class 0')
plt.scatter(X[predictions.flatten() == 1, 0], X[predictions.flatten() == 1, 1], color='blue', label='Class 1
')
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.title("Prediction Results")
plt.legend()
plt.show()
使用PyTorch简化实现
在实际应用中,可以使用深度学习库如 PyTorch
快速实现同样的网络结构。
# 导入必要库
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 将数据转换为 PyTorch tensor
X_tensor = torch.FloatTensor(X) # 将数据 X 转换为浮点数 tensor
y_tensor = torch.FloatTensor(Y) # 将标签 y 转换为浮点数 tensor
# 定义简单的二分类神经网络
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.hidden = nn.Linear(2, 4) # 隐藏层
self.output = nn.Linear(4, 1) # 输出层
def forward(self, x):
x = torch.relu(self.hidden(x)) # 使用 ReLU 激活函数
x = torch.sigmoid(self.output(x)) # 使用 Sigmoid 激活函数
return x
# 创建模型实例、定义损失函数和优化器
model = SimpleNN()
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 训练模型并记录损失
epochs = 1000
losses = []
for epoch in range(epochs):
optimizer.zero_grad()
output = model(X_tensor)
loss = criterion(output, y_tensor)
loss.backward()
optimizer.step()
losses.append(loss.item())
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss.item()}")
# 绘制训练过程中损失值变化
plt.plot(losses)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss over Epochs")
plt.show()
# 使用训练好的模型进行预测
with torch.no_grad(): # 禁用梯度计算,提高效率
predictions = model(X_tensor) # 获取预测值
predicted_labels = (predictions > 0.5).float() # 0.5 阈值将预测值转为二分类
# 绘制预测结果
plt.figure(figsize=(8, 6))
# 绘制预测为类别 1 的点
plt.scatter(X[predicted_labels.squeeze() == 1, 0], X[predicted_labels.squeeze() == 1, 1], color="blue", label="Predicted Class 1")
# 绘制预测为类别 0 的点
plt.scatter(X[predicted_labels.squeeze() == 0, 0], X[predicted_labels.squeeze() == 0, 1], color="red", label="Predicted Class 0")
# 绘制原始数据的分布
plt.scatter(X[Y.squeeze() == 1, 0], X[Y.squeeze() == 1, 1], color="cyan", marker="x", label="Actual Class 1")
plt.scatter(X[Y.squeeze() == 0, 0], X[Y.squeeze() == 0, 1], color="orange", marker="x", label="Actual Class 0")
# 添加图例和标签
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.title("Prediction Results vs Actual Data")
plt.legend()
plt.show()
总结
通过本教程,我们从原理到实现