【机器学习】二分类神经网络

时间:2024-10-29 09:23:16

本教程旨在帮助初学者理解神经网络的基本原理和实现,特别针对二分类任务,深入解析其正向传播和反向传播的数学推导,并逐步用 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)

正向传播是指数据从输入层流向输出层的计算过程。

  1. 输入层到隐藏层:计算 Z1 = X * W1 + b1
  2. 激活函数:在隐藏层使用 ReLU 函数。
  3. 隐藏层到输出层:计算 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=1m(y(i)log(y^(i))+(1y(i))log(1y^(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)

反向传播通过链式法则计算损失函数对每个参数的梯度,从而更新参数以最小化损失。具体步骤如下:

  1. 计算输出层的梯度。
  2. 通过输出层的梯度,进一步计算隐藏层的梯度。
  3. 使用梯度下降算法更新参数。
# 反向传播函数
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()

总结

通过本教程,我们从原理到实现