PyTorch手写BP神经网络:从零实现前向传播与反向传播

发布时间:2026/9/20 22:55:28
PyTorch手写BP神经网络:从零实现前向传播与反向传播 简介面向PyTorch初学者的BP神经网络回归实战资料提供基于完整Python工程的可运行代码与配套回归数据集帮助读者在真实数据上理解反向传播、梯度下降、优化器选择等关键训练环节。压缩包共15个文件以4个Python脚本为核心涵盖模型定义、训练入口与工具函数另含CSV格式的空气污染回归数据集、PyCharm工程配置文件及编译缓存文件整体仅33KB结构精简适合快速上手。目前已有356人学习使用。通过该工程读者可掌握用PyTorch构建BP网络解决连续值预测的完整流程包括网络层级设计、MSE损失计算、训练循环编写、模型测试等步骤同时可借助附带数据快速验证算法效果适合课程设计、算法入门与基础研究参考。 很多初学者学 BP 神经网络最痛苦的不是看不懂公式而是明明知道“前向传播算输出、反向传播算梯度”这句话打开编辑器还是一行代码都写不出来。网上的教程要么只讲理论推导要么直接甩一个调好包的模型中间那段“算法到底是怎么一步步落地成代码”的链路反而没人讲透。所以这次我用 PyTorch 手写一个完整的 BP 神经网络算法配一份经典的 MNIST 数据集从网络初始化、前向传播、反向传播到参数更新全程不借助 nn.Linear 这种封装好的层把算法底层的计算过程用张量运算一步步实现出来再把每一行代码都拆开讲清楚。这个项目适合刚刚学完 Python 和 PyTorch 基础语法、想真正搞懂神经网络内部原理的人也适合准备面试或者做课程设计时需要一个能跑、能讲、能改的底层实现作为参考。1. 项目定位与整体设计1.1 为什么选择用 PyTorch 手写 BP 神经网络你可能会问PyTorch 明明提供了 nn.Module、nn.Linear、optim.SGD 一整套高层API为什么还要像上世纪九十年代那样手写梯度原因说起来很简单自动求导帮我们省事的同时也把“算法本身”变成了一只黑箱。如果你的目标是快速做一个实际项目直接用 nn.Linear 和 loss.backward() 确实最高效但如果你是想把 BP 神经网络作为算法基础吃透就必须清楚地知道每一层权重是怎么更新的、链式法则在矩阵运算里到底怎么展开。手写一遍之后再回去看那些公式推导会有一种“哦原来书上写的就是这个”的通透感。另外从教学角度看手写一个两层的 BP 网络代码量并不大去掉注释也就七八十行但信息密度非常高。它几乎覆盖了神经网络入门阶段要理解的所有核心机制权重初始化、激活函数、前向传播、损失计算、梯度推导、参数更新、批量训练。整套逻辑跑通以后你再看那些用 PyTorch 高级API写的入门代码会非常清楚每一行背后发生了什么。1.2 项目结构与任务定义这个项目做的是一个标准的二分类任务从 MNIST 手写数字数据集中挑出标签为 0 和 1 两类图片训练一个两层 BP 神经网络判断一张图片是 0 还是 1。选择二分类而不是原始 MNIST 的十分类原因有两个一是二分类的输出和损失函数更简单直观新手容易看懂二是训练速度快CPU 上几秒钟就能跑完适合反复实验。网络结构我用的是最常见的单隐藏层结构输入层 784 个神经元28x28 的图片展开成一维向量隐藏层设置 16 个神经元输出层 1 个神经元。隐藏层和输出层都用 Sigmoid 作为激活函数损失函数用二分类交叉熵BCE Loss。整个项目不依赖 GPU一台普通笔记本跑 3000 轮迭代也就二十秒左右。1.3 学习这个项目能收获什么完成这个项目后你至少有四方面收获第一能独立手写出 BP 神经网络的正向和反向传播代码第二能手工推导出 Sigmoid 加二分类交叉熵下的每层梯度公式并且验证梯度计算的正确性第三理解学习率、权重初始化、数据归一化这些细节对训练效果的影响第四掌握用 PyTorch 的张量运算实现矩阵操作的思路而不是只会掉高级API。2. BP 神经网络算法核心原理2.1 前向传播矩阵运算如何完成一次预测BP 网络的前向传播本质上就是数据在网络中逐层做矩阵乘法、加上偏置、经过激活函数得到最终输出。你可以把它想象成一条装配流水线原材料输入向量经过第一道工序z1 XW1 b1再通过质检环节Sigmoid 激活变成半成品特征 a1然后进入第二道工序z2 a1W2 b2再一次激活后得到成品 a2也就是模型预测的概率。这里有一个很多人容易忽略的点为什么矩阵乘法之后一定要接激活函数因为如果只是线性组合的堆叠那不管网络多少层最终仍然等价于一层线性变换根本拟合不了非线性关系。Sigmoid 这个激活函数能把任意实数压缩到 0 到 1 之间既引入了非线性能力又让输出天然具有概率含义非常适合二分类场景。在实际代码里前向传播的矩阵运算会涉及到维度匹配问题。如果输入是一个批量大小为 m 的样本 X它的形状是 (m, 784)那么 W1 的形状必须是 (784, 16)这样 X W1 的结果才是 (m, 16)加上偏置 b1形状为 (1, 16)后广播机制会自动把偏置加到每一行上。这个设计不是为了省事而是为了保证一次前向传播能同时处理 m 个样本计算效率远高于 for 循环逐样本跑。2.2 损失与反向传播链式法则如何指导参数更新前向传播结束后我们得到一个预测值 a2接下来要计算它和真实标签 y 之间的差距。二分类交叉熵的公式是 L -[y·log(a2) (1-y)·log(1-a2)]它衡量的是预测概率分布和真实分布之间的差异。这个损失函数配合 Sigmoid 输出有一个特别优雅的性质最终的梯度居然可以化简成 (a2 - y) 的形式这意味着误差项就是“预测值减去真实值”非常便于手写实现也方便验证梯度计算是否正确。反向传播的原理是链式法则可以通俗地理解为“把最终损失的影响一层一层往前传”。我们最终要算的是损失 L 对每个权重 W1、b1、W2、b2 的偏导数但这些权重并不直接出现在损失函数里它们隔着好几层运算。链式法则就是沿着前向传播的路径一步一步倒推把每一层局部的导数乘起来得到总的导数。具体到我们这个结构反向传播大概分这么几步先算输出层的误差 dz2 a2 - y然后计算 W2 的梯度 dW2 a1.T dz2 / m接着把误差往隐藏层传即 dz1 (dz2 W2.T) * a1 * (1 - a1)这里面 a1*(1-a1) 是 Sigmoid 函数的导数最后算 W1 的梯度 dW1 X.T dz1 / m。整个过程里最需要注意的是对应关系——前向传播时是 X W1反向传播时就要转置成 W1.T 才能让维度对得上。2.3 学习率与参数更新逻辑拿到梯度后参数更新就非常简单了W W - lr * dW。这里的学习率 lr 控制的是每一步更新迈多大步子。学习率太大会导致 Loss 震荡甚至变成 NaN太小则训练速度极慢几百轮都看不到明显下降。我在这个项目里先用 lr 0.5这是二分类小网络比较稳妥的经验值。另外有一个细节值得注意权重初始化。PyTorch 的 randn 生成的是标准正态分布随机数如果直接初始化不缩放输入的特征值又比较大Sigmoid 很容易进入饱和区梯度趋近于 0训练基本失效。所以初始化时我习惯乘以 0.01 做缩放保证激活前的值集中在 0 附近让梯度能够正常传播。3. 环境准备与数据集说明3.1 PyTorch 环境搭建CPU 版实测步骤这个项目不需要 GPUCPU 版本就足够所以环境搭建非常简单。如果你电脑上已经装好了 Anaconda可以按下面几步操作conda create -n bp python3.9 conda activate bp pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu如果网络下载速度不理想可以加国内镜像源加速pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple装完以后务必验证一下版本避免后面跑课设时发现 torch 没装对import torch print(torch.__version__) print(torch.cuda.is_available())这里要提醒一个常见的坑不要一上来就去官网复制所谓“最新版”的命令尤其是 GPU 版本。如果你没有合适的显卡驱动和 CUDA 环境折腾半天很可能装完直接报错。CPU 版对于理解 BP 神经网络算法和跑通练习已经完全够用等真正需要大规模训练时再切 GPU 不迟。3.2 MNIST 数据集的选择与加载MNIST 是深度学习中最早使用的标准数据集之一包含 0 到 9 的手写数字灰度图片训练集 6 万张测试集 1 万张每张图片是 28x28 像素。因为它足够简单、下载方便、格式规范特别适合作为入门算法的实验数据。用 torchvision 直接下载即可代码非常简洁from torchvision import datasets train_set datasets.MNIST(./data, trainTrue, downloadTrue) test_set datasets.MNIST(./data, trainFalse, downloadTrue)如果你在下载这一步遇到网络不通或者下载到一半断掉的情况不用慌。MNIST 的原始数据文件很常见可以去网盘或者镜像站点手动下载四个 .gz 文件放到 ./data/MNIST/raw 目录下再把 download 参数改成 False 就能直接使用。这个“离线准备数据集”的技巧在复现其他项目时也很有用。3.3 数据预处理的几个细节原始数据是像素值为 0 到 255 的整数矩阵如果直接喂给网络数值范围太大会导致梯度计算极不稳定。这里有几个处理方式要注意第一一定要把数据变成浮点类型再参与矩阵运算。PyTorch 中整数张量不能和浮点权重做矩阵乘法这是新手最容易踩的 RuntimeError。第二归一化处理能显著提升训练稳定性。最简单有效的办法是直接除以 255.0把像素值压缩到 0 到 1 之间。这个区间恰好是 Sigmoid 输出所在的范围和损失函数也比较匹配。第三因为我们要做的是 0 和 1 的二分类需要先把全量 MNIST 中的样本筛选出来只保留标签为 0 或 1 的子集。同时要把标签转换成列向量的形式形状为 (m, 1)这样才能和预测值的形状保持一致计算损失时才不会出维度错误。我自己实测的效果是原始像素不归一化时Loss 下降慢且波动大准确率大概只能到 95% 左右归一化以后3000 轮训练结束时 Loss 能稳定在 0.02 上下测试集准确率可以超过 99%。这个差距足以说明数据预处理在深度学习中的重要性。4. BP 神经网络完整代码实现4.1 手写模型类与前向传播这里我特意没有使用 nn.Module因为我们的目标是算法基础要确保每一行矩阵运算都是显式的。完整代码如下import torch torch.manual_seed(42) # 加载并预处理数据 train_set datasets.MNIST(./data, trainTrue, downloadTrue) test_set datasets.MNIST(./data, trainFalse, downloadTrue) # 筛选标签为 0 和 1 的样本转换格式并归一化 def prepare_data(dataset): mask dataset.targets 1 images dataset.data[mask].float().view(-1, 784) / 255.0 labels dataset.targets[mask].float().unsqueeze(1) return images, labels train_images, train_labels prepare_data(train_set) test_images, test_labels prepare_data(test_set) # 定义激活函数 def sigmoid(x): return 1 / (1 torch.exp(-x)) # 初始化网络参数784 - 16 - 1 W1 torch.randn(784, 16, requires_gradFalse) * 0.01 b1 torch.zeros(1, 16) W2 torch.randn(16, 1, requires_gradFalse) * 0.01 b2 torch.zeros(1, 1) # 前向传播 def forward(X): z1 X W1 b1 a1 sigmoid(z1) z2 a1 W2 b2 a2 sigmoid(z2) return z1, a1, z2, a2初始化为什么用 randn 乘 0.01 而不是直接用 zeros因为如果所有权重都初始化为 0那么同一层的所有神经元在前向传播中得到完全相同的输出反向传播时梯度也一样网络就退化成了只有一个神经元的模型完全失去表达力。随机初始化是打破对称性的关键。forward 函数里有个小细节偏置 b1 和 b2 的形状分别设为 (1, 16) 和 (1, 1)这是为了让 PyTorch 的广播机制自动把偏置加到批量中每一行的计算结果上。如果你把 b1 写成 (16,) 也能跑通但显式声明为二维更符合张量运算的习惯后面反向传播时代码也更清晰。4.2 手写反向传播与训练循环反向传播的核心就是验证之前推导的梯度公式在代码里的形态。我把计算过程直接写在函数里并用注释标明每一步对应的数学含义# 二分类交叉熵损失 def bce_loss(y_pred, y_true, eps1e-8): return -torch.mean(y_true * torch.log(y_pred eps) (1 - y_true) * torch.log(1 - y_pred eps)) # 反向传播手动计算每个参数的梯度 def backward(X, y_true, z1, a1, z2, a2): m X.size(0) # 输出层误差dL/dz2 a2 - y dz2 a2 - y_true dW2 a1.T dz2 / m db2 dz2.mean(dim0, keepdimTrue) # 隐藏层误差先过输出层权重再过 Sigmoid 导数 da1 dz2 W2.T dz1 da1 * a1 * (1 - a1) dW1 X.T dz1 / m db1 dz1.mean(dim0, keepdimTrue) return dW1, db1, dW2, db2 # 训练循环 lr 0.5 epochs 3000 for epoch in range(epochs): z1, a1, z2, a2 forward(train_images) loss bce_loss(a2, train_labels) dW1, db1, dW2, db2 backward(train_images, train_labels, z1, a1, z2, a2) W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 if (epoch 1) % 300 0: print(fepoch {epoch 1}, loss: {loss.item():.6f})这段训练代码里值得反复理解的是 backward 中的维度变化。a1.T dz2 这一步a1 形状是 (m, 16)dz2 形状是 (m, 1)转置后变成 (16, m)矩阵乘法结果就是 (16, 1)正好是 W2 的维度。X.T dz1 同理。如果你在实现中把某个矩阵转置漏了运行时大概率会报维度不匹配这个错误提示反而能帮助我们确认公式的维度推导过程。因为手写反向传播的代码不多初学者很容易抄错某个符号所以我强烈建议你在正式训练之前先用数值梯度法验证一下手写梯度的正确性。做法很简单对某个参数加上一个极小的扰动 eps计算 (L(W eps) - L(W - eps)) / (2 * eps)把这个数值近似梯度与手写梯度对比。如果两者误差在 1e-6 量级说明反向传播实现没有问题。这个自查步骤非常值得做一次它能极大增强你对“梯度代码写对了”的信心。4.3 测试评估与结果分析训练结束后我们需要在测试集上评估模型的泛化能力。测试环节有一个关键点一定不能开梯度跟踪所以代码里要用 torch.no_grad() 包裹避免 PyTorch 自动记录计算图既省内存又提速。with torch.no_grad(): _, _, _, y_pred forward(test_images) preds (y_pred 0.5).float() acc (preds test_labels).float().mean().item() print(ftest accuracy: {acc * 100:.2f}%)我自己在 CPU 上跑这个完整流程训练 3000 轮后测试准确率大约在 99% 左右Loss 从最开始的 0.69 附近一路降到 0.02 上下。这个结果说明这个简单的两层 BP 网络已经完全掌握了 0 和 1 这两类手写数字的判别规律。如果你看到自己的准确率只有 95% 甚至更低不要急着怀疑代码大概率是前面提到的预处理或者学习率设置问题回到第 3 章和第 2 章逐项排查即可。5. 常见问题与排查技巧实录5.1 Loss 不下降或者变成 NaN在这个项目里Loss 不下降一般有三个原因学习率太大、权重初始化不合适、数据没有归一化。学习率太大时参数更新步子迈得过大Loss 可能剧烈震荡甚至变成 NaN初始化权重过大时Sigmoid 进入饱和区梯度消失Loss 几乎不动数据不归一化时梯度计算的值域太大训练极不稳定。我建议遇到这类问题时先把学习率调小到 0.01 或者 0.05 试一轮如果 Loss 开始缓慢下降说明原来的学习率确实偏大。如果 Loss 还是不动检查权重初始化是不是忘了乘 0.01。如果是在 MNIST 这类数据上直接判断一下你的输入像素是否还是 0 到 255 的原始值。5.2 Sigmoid 的梯度消失问题虽然这个项目用 Sigmoid 作为激活函数工作得很好但你必须知道它的局限性。Sigmoid 的导数最大值只有 0.25当网络层数增多时误差每向后传播一层就要乘一个小于 1 的数梯度很容易指数级衰减这就是梯度消失问题。所以现代网络结构中隐藏层激活函数基本都用 ReLU 而不是 Sigmoid。在代码里改成 ReLU 也非常简单把 forward 里的激活函数替换成 max(0, x)反向传播时隐藏层的梯度项从 a1*(1-a1) 改成 (z1 0).float()。你可以试试改成 ReLU 对比一下训练速度和效果这本身就是很好的实验练习。5.3 训练结果不可复现如果你每次运行代码得到的 Loss 曲线不一样大概率是随机种子没有固定。在代码最开始加上 torch.manual_seed(42) 就可以解决大多数情况下的随机性问题。如果追求严格可复现还可以在加载数据时设置 DataLoader 的随机数种子并限制 CPU 线程数量。不过对于这个入门项目固定最前面的种子已经足够。另外很多人问为什么要用 3000 轮而不是更少。其实你可以做一个非常直观的实验分别用 500、1000、2000 轮训练模型观察测试准确率的变化趋势。你会发现 2000 轮以后准确率的提升已经非常缓慢这可以帮助你建立起对训练轮数和收益之间关系的直觉以后做任何项目都知道“该先跑多少轮看效果”。5.4 常见问题速查表现象可能原因解决办法Loss 变成 NaN学习率过大降低学习率到 0.01Loss 几乎不下降权重初始化过大初始化乘 0.01 缩放训练完成后准确率低输入未归一化像素值除以 255.0矩阵乘法报维度错误反向传播漏了转置按 W 的维度反向检查每次运行结果不同未固定随机种子加 torch.manual_seed(42)MNIST 下载失败网络不通或源不稳定手动下载离线数据放对应目录最后再分享一个我在实际学习和教学中的体会手写 BP 神经网络最大的价值是帮你建立对深度学习框架的“信任感”。当你亲眼看到梯度公式化成的代码就能让 Loss 一步步下降准确率从 50% 提升到 99% 时你就不再需要担心框架里那些封装好的层是不是在变什么魔法。真正的项目开发里我百分之百会使用 nn.Linear 和 optimizer 来写模型但在那之前花半天时间手写一遍 BP这笔投入的收益远超你的想象。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询