资讯详情 从零开始搭建一个简单的两层神经网络实现逻辑回归
📅 2026/10/20 5:56:04
一、前言在之前的学习中我们使用了 NumPy 手动实现了逻辑回归那么针对同一个逻辑回归研究问题我们用MLP感知机一个简单的神经网络去解决效果会如何呢这次继续学习 PyTorch 的基础知识尝试编写一个简单的两层全连接神经网络Multilayer PerceptronMLP完成二维数据的二分类任务。与之前使用 NumPy 手动计算梯度不同本次实验使用 PyTorch 的自动微分机制计算梯度但不调用torch.optim中的优化器而是自行实现梯度下降更新参数的过程。本次实验的重点是理解神经网络的前向传播、反向传播以及模型参数的更新方式。二、生成实验数据首先使用 NumPy 生成1000个二维样本每个样本包含两个特征取值范围为 -33。np.random.seed(42) num_m 1000 x_np np.random.uniform(-3, 3, size(num_m, 2)) y_np (x_np[:, 0] x_np[:, 1] 1).astype(np.float32).reshape(-1, 1)np.random.seed(42)用于固定 NumPy 随机数生成过程。np.random.uniform()生成均匀分布的随机数据因此x_np的形状为(1000, 2)。接下来为样本生成标签分类规则为当 x₁ x₂ 1 时y 1。当 x₁ x₂ ≤ 1 时y 0。所以真实分类边界满足x₁ x₂ 1即一条斜率为 -1 的直线。其中x_np[:,0]提取第一个特征。x_np[:,1]提取第二个特征。.astype(np.float32)将布尔值转换为浮点数。.reshape(-1,1)将标签调整为(1000,1)的形状。最后将 NumPy 数组转换成 PyTorch Tensorx torch.tensor(x_np, dtypetorch.float32) y torch.tensor(y_np, dtypetorch.float32)转换后的数据维度为x.shape [1000, 2] y.shape [1000, 1]三、搭建两层全连接神经网络1. 网络结构本次网络包含两个全连接层中间使用 ReLU 激活函数输出层使用 Sigmoid。输入 X[1000, 2] | Linear(2, 5) | ReLU | Linear(5, 1) | Sigmoid | 输出 A[1000, 1]第一层有五个神经元。因为x是二维的输入所以是2,5的维度数第二层只有1个神经元。这里的“两层”指的是两个具有可训练参数的全连接层不包含输入层。2. 定义网络class net(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(2, 5) self.fc2 nn.Linear(5, 1) self.relu nn.ReLU() self.sigmoid nn.Sigmoid() def forward(self, x): x self.relu(self.fc1(x)) x self.sigmoid(self.fc2(x)) return xnn.Module是 PyTorch 神经网络模块的基类。__init__()用于定义网络结构forward()则规定数据在网络中的前向传播过程。第一层self.fc1 nn.Linear(2, 5)将2个输入特征映射为5个隐藏特征对应数学表达式Z₁ XW₁ᵀ b₁然后经过 ReLUA₁ ReLU(Z₁) max(0, Z₁)ReLU 将负数映射为0正数保持不变为网络引入非线性表达能力。第二层self.fc2 nn.Linear(5, 1)将5个隐藏特征映射为1个输出Z₂ A₁W₂ᵀ b₂最后通过 SigmoidA₂ Sigmoid(Z₂) 1 / (1 e⁻ᶻ²)得到每个样本属于类别1的预测概率。例如当某个样本的输出为0.85时表示模型预测它属于类别1的概率为0.85。四、损失函数与梯度下降1. 二元交叉熵损失函数本次使用 PyTorch 提供的二元交叉熵损失函数model net() criterion nn.BCELoss() lr 0.3二元交叉熵Binary Cross-Entropy Loss用于衡量预测概率与真实标签之间的差异。对于单个样本其表达式为对于整个训练集则将所有样本的损失取平均。其中y真实标签。a模型预测概率。L损失函数值。预测结果越符合真实标签损失通常越小。由于网络输出层已经使用 Sigmoid将结果转换为概率因此可以直接使用nn.BCELoss()。2. 自动微分与反向传播outputs model(x) loss criterion(outputs, y) model.zero_grad() loss.backward()这是训练过程中非常重要的几行代码。首先model(x)调用网络的forward()方法完成前向传播得到预测结果。接着使用criterion()计算损失值。model.zero_grad()负责清空上一轮累积的梯度。最后loss.backward()通过 PyTorch 的自动微分机制反向传播利用链式法则计算损失函数对各个网络参数的梯度。例如某个权重 w 的梯度可以表示为∂L / ∂w这个表达式表示损失函数 L 对权重 w 的偏导数反映参数变化对损失函数的局部影响。需要注意的是loss.backward()只负责计算梯度并不会直接更新参数。计算得到的梯度保存在对应参数的.grad属性中。3. 手动实现梯度下降这是本次实验最重要的一部分with torch.no_grad(): for param in model.parameters(): param - lr * param.grad在之前的 NumPy 逻辑回归实验中我们根据推导得到的梯度手动更新权重和偏置。本次则由 PyTorch 自动计算梯度再手动完成参数更新。梯度下降的数学表达式为θ_new θ_old − α × (∂L/∂θ)其中θ网络中的可训练参数包括权重和偏置。α学习率本次设置为0.3。∂L/∂θ损失函数对参数 θ 的梯度。model.parameters()用来遍历网络中所有需要训练的参数。param - lr * param.grad根据梯度下降公式更新参数。而torch.no_grad()的作用是临时关闭梯度记录避免参数更新操作本身被加入自动微分计算图。这一过程与 PyTorch SGD 优化器最基础的参数更新方式相同只不过我们直接通过代码完成了更新。五、模型训练与准确率本次训练循环如下for epoch in range(501): outputs model(x) loss criterion(outputs, y) model.zero_grad() loss.backward() with torch.no_grad(): for param in model.parameters(): param - lr * param.grad if (epoch % 10) 0: preds (outputs 0.5).float() acc (preds y).float().mean().item() print( f轮数{epoch}, floss:{loss.item():6f}, f准确率:{acc:6f} )本次设置501次参数更新每隔10轮输出一次训练结果。整个过程可以概括为前向传播 → 计算损失 → 清空梯度 → 反向传播 → 梯度下降更新参数。准确率的计算代码为preds (outputs 0.5).float() acc (preds y).float().mean().item()首先将预测概率以0.5为阈值转换成0或1再与真实标签比较统计预测正确的样本比例。例如1000个样本中有960个分类正确则准确率为96%。这里计算的是训练集准确率没有使用独立测试集。另外本次打印的loss和outputs是当前轮参数更新之前计算得到的结果因此输出指标对应的是本轮更新前的模型状态。这不影响正常训练。六、决策边界可视化训练结束后使用 Matplotlib 绘制分类结果。本次定义了plot_decision_boundary()函数主要完成三个步骤首先通过np.meshgrid()在二维特征空间中生成200×200个网格点。然后将这些网格点输入训练完成的网络计算每个位置属于类别1的预测概率。最后使用plt.contourf()绘制分类区域使用plt.scatter()绘制真实样本。图像中红色散点表示类别1x₁ x₂ 1。蓝色散点表示类别0x₁ x₂ ≤ 1。背景颜色表示模型预测的分类区域。理论上的分类边界为x₂ 1 − x₁如果模型学习效果较好图中不同预测区域之间的分界线就应该接近这条直线。七、完整实验代码以下代码是本次实验的完整实现。import torch import torch.nn as nn #import torch.optim as optim import numpy as np import matplotlib.pyplot as plt np.random.seed(42) num_m1000 x_np np.random.uniform(-3,3,size(num_m,2)) y_np (x_np[:,0] x_np[:,1] 1).astype(np.float32).reshape(-1,1) x torch.tensor(x_np, dtypetorch.float32) y torch.tensor(y_np, dtypetorch.float32) class net(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(2,5) self.fc2 nn.Linear(5,1) self.relu nn.ReLU() self.sigmoid nn.Sigmoid() def forward(self,x): x self.relu(self.fc1(x)) x self.sigmoid(self.fc2(x)) return x model net() criterion nn.BCELoss() lr 0.3 for epoch in range(501): outputs model(x) loss criterion(outputs,y) model.zero_grad() loss.backward() with torch.no_grad(): for param in model.parameters(): param - lr * param.grad if(epoch % 10) 0: preds (outputs 0.5).float() acc (preds y).float().mean().item() print(f轮数{epoch}, loss:{loss.item():6f}, 准确率:{acc:6f}) def plot_decision_boundary(model, X, y): x_min, x_max X[:, 0].min() - 0.5, X[:, 0].max() 0.5 y_min, y_max X[:, 1].min() - 0.5, X[:, 1].max() 0.5 xx, yy np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200)) grid torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtypetorch.float32) with torch.no_grad(): Z model(grid).numpy().reshape(xx.shape) plt.contourf(xx, yy, Z, levels[0, 0.5, 1], alpha0.3, cmapcoolwarm) plt.scatter(X[:, 0], X[:, 1], cy.squeeze(), edgecolorsk, cmapcoolwarm) plt.xlabel(x1) plt.ylabel(x2) plt.title(red: x1x21 blue: x1x21) plt.show() plot_decision_boundary(model, x_np, y_np)八、运行结果与分析本次训练使用1000个二维样本学习率设置为0.3共进行501次参数更新。运行结果如下由于数据具有明确的线性分类规律模型可以通过不断更新权重和偏置逐渐学习输入特征与标签之间的关系。可以看到在学习率lr比较大时模型收敛速度也比较快。训练过程中可以观察损失函数值以及准确率的变化以判断模型是否逐渐收敛。从可视化结果中可以观察红色与蓝色样本的分布以及模型对整个二维特征空间的分类情况。需要注意的是本次数据本身是线性可分的简单的逻辑回归同样能够完成这一任务。使用两层神经网络主要是为了学习 PyTorch 的网络定义和反向传播机制。九、学习总结通过本次实验我进一步熟悉了 PyTorch 中全连接神经网络的搭建以及前向传播、损失计算、自动微分和参数更新的完整流程。其中最重要的收获是理解了loss.backward()负责计算梯度而手动编写的梯度下降代码负责利用这些梯度更新参数。相比直接调用优化器这种方式能够更加直观地理解神经网络训练过程中各个参数是如何变化的。后续可以进一步使用 PyTorch 自带的 SGD、Adam 优化器进行对比并尝试使用非线性可分的数据集观察隐藏层和激活函数的实际作用。参考资料PyTorch 官方文档Redirecting…Andrew NgDeep Learning Specialization。NumPy 官方文档https://numpy.org/doc/Matplotlib 官方文档Matplotlib documentation — Matplotlib 3.11.2 documentation