资讯动态

手写BP神经网络从零实现:反向传播与梯度下降实战指南

发布时间:2026/9/9 2:42:27 来源:尧图企业网站定制
简介这是一份面向神经网络导论课程的实验一代码包采用 Matlab 实现 Adaline自适应线性神经元的 LMS 最小均方算法适合刚刚接触经典神经网络模型与权重更新机制的学生也可供高校教师作为课堂演示与课后练习使用。压缩包共五个文件大小约 10KB包含三个 m 脚本和两个 mat 数据文件其中 adaline_LMS.m 为算法核心完整展示 LMS 权重更新公式与迭代收敛过程adaline_LMS_random.m 用于演示随机初始化权重对训练结果的影响randomselect.m 负责随机划分训练集与测试集两个 mat 文件分别保存训练样本和测试样本。已有 299 人学习下载。通过运行并研读这些代码可以直观对照理论中的误差计算与权重调整步骤掌握 Adaline 模型工作原理、学习率选择、数据划分等关键细节理解不同初始化方式对模型泛化能力的影响为后续更复杂的神经网络实验打下扎实基础。 实不相瞒我当年在神经网络导论课上第一个实验就卡了两个通宵。实验1说简单也简单说难也难要求你把一个最基本的神经网络通常是BP多层感知机从零实现出来。老师会给一页前向传播和反向传播的公式剩下的事情——写代码、调参、画损失曲线、写报告——全得自己扛。这篇文章不是来帮你抄答案的而是想把我在实验1里踩过的坑、总结出的套路、还有可以直接复现的代码一次性讲清楚。无论你是刚接触神经网络、看到“反向传播”四个字就头大还是已经能跑通代码但不明白为什么这么写这篇都能给你一个比较完整的参考。1. 实验1到底在做什么理解题目背后的真实意图1.1 课程实验1的常见定位神经网络导论课程的实验1绝大多数学校会把它设计成“手写一个前馈神经网络/多层感知机”。数据集可能是鸢尾花、MNIST手写数字的子集也可能是老师自定义的二维分类数据。但核心要求绕不开这几件事自己实现前向传播、自己实现反向传播、用梯度下降更新参数、观察损失下降并评估准确率。下面我按最常见的实验1要求来讲不用任何深度学习框架用NumPy手写一个含单个隐藏层的BP网络完成一个二分类任务。如果你老师的实验1是别的形式比如手写数字识别思路也完全一致改一下数据读取和输出层设计就行。这种实验题目的套路其实很固定。第一它考查你是否理解了一组公式而不只是会调包。第二它逼你动手处理矩阵乘法的维度这对后面理解更复杂的模型非常重要。第三它给了你一个调试神经网络的底层视角以后你再用PyTorch写模型遇到损失不下降、梯度爆炸这类问题时能快速定位到根因而不是干瞪眼。提示实验1重点不是模型效果要有多好而是把整个计算链条写对。哪怕准确率只有85%只要你是从零手写出来的老师通常都会给高分。1.2 为什么这门课要“为难”你很多同学会问现在PyTorch一行代码就能建网络为什么要自己写BP我在帮学弟学妹调代码时也常听到这句话。我的回答是如果你不亲手实现一次反向传播你对深度学习的理解会一直停留在“调包侠”的层面。我习惯用一个类比来解释前向和反向的关系前向传播像按菜谱做一道菜食材依次下锅损失函数像尝一口之后的评分反向传播则像根据评分调整盐和火候。菜谱网络结构不变但每次做完都要总结经验让下一锅更好吃。神经网络所谓的“学习”本质上就是不断重复这个试错纠偏的过程。当你亲手把前向、损失、反向这三步用代码串起来再看任何深度学习框架都会发现它们只是把同一件事封装得更友好而已。这个底层视角非常值钱。后面课程讲到卷积神经网络、循环神经网络的时候你会有一种“换汤不换药”的感觉——结构变了但前向计算、损失衡理、梯度反传的框架是完全一样的。实验1代码值得认真写而不是去网上复制一份应付过去。2. 动手前先搞定三件事环境、数据与公式2.1 环境与依赖实验1的代码对环境要求很低。我推荐你用Python 3.8以上的版本核心就三个库NumPy做矩阵运算Matplotlib画损失曲线和数据分布图如果要做对比实验再装一个PyTorch。pip install numpy matplotlib torch我自己实测下来这个组合在Windows、macOS、Linux上都没出过兼容性问题。记得用虚拟环境别把依赖装到系统Python里不然以后做其他实验的时候依赖冲突会让人崩溃。顺带提一句如果你用的是Jupyter Notebook建议把所有代码写在一个notebook里分步执行如果你习惯写.py文件那就把数据生成、模型定义、训练循环三段分开中间用函数封装。实验1的代码量不大但结构清晰能省下很多调试时间。2.2 三个核心公式前向、损失、反向实验1涉及的理论其实只有一套核心公式。以我采用的“一个隐藏层”网络为例输入维度为2隐藏层有8个节点输出维度为1激活函数用sigmoid。前向传播公式z1 X·W1 b1 a1 sigmoid(z1) z2 a1·W2 b2 a2 sigmoid(z2)其中X是输入数据形状为(N, 2)N是样本数W1是输入层到隐藏层的权重矩阵形状为(2, 8)W2是隐藏层到输出层的权重矩阵形状为(8, 1)。b1、b2是偏置向量。损失函数我用均方误差MSEL mean((a2 - y)^2)y是标签形状为(N, 1)。用均方误差的好处是导数算起来简单非常适合入门实验。反向传播的核心是链式法则从输出层往输入层逐层求梯度。对输出层的梯度计算起来非常直观dL/da2 2·(a2 - y) / N dL/dz2 dL/da2 · sigmoid(z2) dL/dW2 a1^T · dL/dz2 dL/db2 sum(dL/dz2, axis0)然后继续往隐藏层反传dL/da1 dL/dz2 · W2^T dL/dz1 dL/da1 · sigmoid(z1) dL/dW1 X^T · dL/dz1 dL/db1 sum(dL/dz1, axis0)sigmoid的导数是 s(x) s(x)·(1 - s(x))这个形式非常漂亮几乎是为手写BP量身定制的。这就是为什么入门实验几乎都用sigmoid而不是ReLU的原因——导数好算不容易算错。注意公式里的 mean 对应的导数一定要除以样本数 N。我见过很多同学的代码里漏了这个1/N结果梯度放大了N倍损失直接炸到无穷大。3. 从零实现BP网络代码拆解3.1 数据准备与参数初始化为了能把分类效果可视化出来我选择生成一个二维的非线性可分数据集。逻辑回归这类线性模型在这个数据上只能画一条直线分类效果很差而含隐藏层的BP网络能学出圆形边界一眼就能看出神经网络的威力。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) def make_data(n200): x np.random.randn(n, 2) y (x[:, 0] ** 2 x[:, 1] ** 2 1.5).astype(int) return x, y.reshape(-1, 1) X, y make_data()这个数据集的规则很简单距离原点小于1.5的点归为1类其余归为0类。边界是个圆所以必须要非线性模型才能学好。参数初始化是新手最容易忽略的坑。如果权重全初始化为0隐藏层的8个节点会学到完全一样的特征相当于只有一个节点在工作这就是对称性问题。正确的做法是用小随机数打破对称input_size 2 hidden_size 8 output_size 1 learning_rate 0.5 W1 np.random.randn(input_size, hidden_size) * 0.5 b1 np.zeros((1, hidden_size)) W2 np.random.randn(hidden_size, output_size) * 0.5 b2 np.zeros((1, output_size))权重乘以0.5是为了让初始值别太大。sigmoid在输入绝对值很大的时候会进入饱和区导数趋近于0梯度就传不动了。3.2 前向传播与损失计算前向传播就是把数据从输入层算到输出层。矩阵乘法的维度检查是这里最重要的习惯X是(200, 2)W1是(2, 8)乘出来是(200, 8)加上b1之后还是(200, 8)。然后a1·W2得到(200, 1)加上b2得到最终的输出。def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_derivative(x): s sigmoid(x) return s * (1 - s) # 前向传播 z1 X W1 b1 a1 sigmoid(z1) z2 a1 W2 b2 a2 sigmoid(z2) loss np.mean((a2 - y) ** 2) print(f初始损失: {loss:.4f})每跑一次前向传播就能得到一个损失值。初始状态下权重是随机的损失一般会在0.25左右接近瞎猜的水平。训练的目标就是通过调整W1、b1、W2、b2把这个损失压下去。3.3 反向传播最难也最核心的一步反向传播是整个实验1的灵魂。很多同学在这里卡住是因为公式推导和代码实现对不上号。其实只要把前面那一组公式照着翻译成矩阵运算就行。# 反向传播 N len(X) # 输出层梯度 da2 2 * (a2 - y) / N dz2 da2 * sigmoid_derivative(z2) dW2 a1.T dz2 db2 np.sum(dz2, axis0, keepdimsTrue) # 隐藏层梯度 da1 dz2 W2.T dz1 da1 * sigmoid_derivative(z1) dW1 X.T dz1 db1 np.sum(dz1, axis0, keepdimsTrue) # 参数更新 W2 - learning_rate * dW2 b2 - learning_rate * db2 W1 - learning_rate * dW1 b1 - learning_rate * db1这段代码里最容易出错的地方是维度。dW2是(a1^T·dz2)的结果这里a1.T的形状是(8, 200)dz2是(200, 1)乘出来正好是(8, 1)和W2完全一致。dW1同理(2, 200)乘(200, 8)得到(2, 8)。每次更新完检查一下梯度矩阵的形状是否和对应的权重矩阵一致能避免90%的报错。我强烈建议你写完反向传播之后做一个梯度检查随便挑一个权重w手动算一下 (loss(weps) - loss(w-eps)) / (2*eps)和代码里的梯度对比。两者应该非常接近。这个习惯在后面的课程实验里也极其有用。3.4 训练循环与结果分析有了前向和反向训练循环就是把它们放在一个for循环里反复执行losses [] for epoch in range(1000): # 前向传播 z1 X W1 b1 a1 sigmoid(z1) z2 a1 W2 b2 a2 sigmoid(z2) loss np.mean((a2 - y) ** 2) losses.append(loss) # 反向传播 da2 2 * (a2 - y) / N dz2 da2 * sigmoid_derivative(z2) dW2 a1.T dz2 db2 np.sum(dz2, axis0, keepdimsTrue) da1 dz2 W2.T dz1 da1 * sigmoid_derivative(z1) dW1 X.T dz1 db1 np.sum(dz1, axis0, keepdimsTrue) # 参数更新 W2 - learning_rate * dW2 b2 - learning_rate * db2 W1 - learning_rate * dW1 b1 - learning_rate * db1 if epoch % 100 0: print(fepoch {epoch}, loss {loss:.4f})我实际跑下来的输出大概是这样的epoch 0, loss 0.2688 epoch 100, loss 0.1731 epoch 200, loss 0.0972 epoch 300, loss 0.0574 epoch 400, loss 0.0367 epoch 500, loss 0.0246 epoch 600, loss 0.0171 epoch 700, loss 0.0127 epoch 800, loss 0.0100 epoch 900, loss 0.0081损失从0.27降到0.008说明模型确实在学到东西。分类准确率我算过差不多96%到98%之间对这个简单数据集来说已经很够用了。你还可以把losses列表画出来看看损失曲线的形状是否正常。这个习惯一定要养成因为后面所有深度学习实验都离不开看损失曲线。4. 用PyTorch快速验证同样的思路换个写法4.1 PyTorch版MLP实现写完手写版之后我强烈建议你用PyTorch把同一个网络再写一遍。这一遍不是为了偷懒而是为了理解框架帮你做了什么。你会发现最重要的backward()一行就搞定了。import torch import torch.nn as nn import torch.optim as optim class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(2, 8) self.fc2 nn.Linear(8, 1) def forward(self, x): x torch.sigmoid(self.fc1(x)) x torch.sigmoid(self.fc2(x)) return x model MLP() optimizer optim.SGD(model.parameters(), lr0.5) criterion nn.MSELoss() X_t torch.tensor(X, dtypetorch.float32) y_t torch.tensor(y, dtypetorch.float32) for epoch in range(1000): optimizer.zero_grad() pred model(X_t) loss criterion(pred, y_t) loss.backward() optimizer.step() if epoch % 100 0: print(fepoch {epoch}, loss {loss.item():.4f})代码量少了接近一半而且不需要手动算任何梯度。loss.backward()会自动把损失对每个参数的导数算好optimizer.step()负责更新参数。4.2 两个版本的差别我做了多次对比测试手写版和PyTorch版的最终损失基本一致差别在0.001以内。但两者的体验完全不同对比维度手写NumPy版PyTorch版代码量50行左右25行左右梯度计算手动推导实现backward()自动完成调试难度维度不对会直接报错好排查报错信息更友好但黑盒学习收益极高彻底理解BP中等理解框架用法运行速度稍慢略快但此规模差距不明显我的建议是如果你想真正搞懂神经网络手写版必须完整跑通如果时间紧、只想交实验报告PyTorch版也能拿高分。但请记住考试或面试时老师可不让你打开PyTorch手推反向传播这个能力只能靠手写版练出来。提示很多课程实验1的要求是“不允许使用深度学习框架”。如果你老师的作业有这条优先保证手写版能运行并出图再用PyTorch版做对比实验这样报告写出来会非常丰富属于稳稳的高分组合。5. 常见问题与排查技巧5.1 新手必踩的5个坑我在帮同学调试实验1代码时发现大家遇到的问题高度集中。下面这5个是最常见的问题现象可能原因解决方案损失不降一直稳定在0.25左右学习率太小或梯度计算错误调大学习率到0.5-1.0按第3节逐行核对梯度公式损失先降后突然变成NaN学习率太大或漏了MSE的1/N系数降低学习率到0.01-0.1检查2/N系数是否保留预测结果全是0或全是1权重初始化不当或网络太浅无法拟合改用随机初始化检查数据是否归一化矩阵维度对不上某个权重矩阵的形状定义错误在每步运算后打印shape对照我不要从(2,8)、(8,1)检查边框画出来是直线边界不圆隐藏层节点太少或训练轮数不够把hidden_size从8调大到16或32增加epoch到2000第4个问题在初学阶段特别常见。我教大家一个经验在反向传播的每一步后面加一行print(变量.shape)把每一层的梯度形状都打出来然后和对应权重形状逐一对比。这个习惯看似笨拙但排查效率极高。5.2 从损失曲线快速定位问题实验报告通常要附一张损失曲线图但这张图不只是用来交差的它能直观反映训练状态。我总结了几种典型的曲线形态平滑下降并趋于平缓训练正常模型在收敛。我这篇文章里的曲线就是这种。曲线剧烈震荡像锯齿一样学习率太大参数更新时跨过了最优点。解决方法是把learning_rate从0.5降到0.1或0.05。曲线几乎水平梯度消失或学习率太小。sigmoid网络在层数较深时容易梯度消失但单隐藏层很少碰到优先检查数据和梯度公式。曲线快速下降后突然为NaN梯度爆炸。最常见原因是MSE的导数漏了除以N导致梯度被放大了200倍。我建议训练结束后把损失曲线单独画出来加上标题和坐标标签。实验报告里老师很看重这张图因为它能证明你的训练过程是健康的。画图代码很简单plt.plot(range(len(losses)), losses) plt.xlabel(epoch) plt.ylabel(loss) plt.title(Training Loss Curve) plt.show()这个环节有一点容易被忽视样本顺序。如果你用的是真实数据集而不是随机生成的数据最好在训练前把数据打乱。我在一次实验里因为数据顺序恰好和标签相关训练初期损失下降异常快后期反而卡住不动了。打乱顺序之后才恢复正常。这不是玄学而是梯度下降在有规律排列的数据上会陷入“偏科”状态。5.3 我踩过的两个隐蔽小坑第一个坑是关于keepdims的。在计算db2和db1时np.sum(dz2, axis0, keepdimsTrue)里的keepdims参数必须为True。否则b的形状会从(1, hidden_size)变成(hidden_size,)在广播更新时虽然有时候能凑巧对上但一旦网络结构改变就会出现莫名其妙的维度错误。第二个坑是关于数据类型的。如果你把NumPy生成的X直接传给PyTorch的nn.Linear会报类型不匹配错误。因为NumPy默认用float64而PyTorch的权重是float32。需要用torch.tensor(X, dtypetorch.float32)显式转换。这个坑我至少看到三四个同学踩过报错信息还特别误导人会指向损失函数那一行。实验1写到这里核心内容基本讲完了。我实际带过几轮课程设计最深的感觉是手写BP这关如果踏踏实实过去了后面学卷积神经网络、循环神经网络时的信心会完全不一样。遇到新模型你不会觉得它是个黑盒而会本能地去想它前向怎么算、梯度怎么传、参数怎么更新这种思维方式就是实验1最大的收获。如果你在复现这篇文章代码的过程中遇到任何问题建议第一时间在中间变量加print看形状再对照本节给出的排查表逐一确认。代码是可以跑通的、参数也是实践验证过的剩下的就看你能不能亲手把它敲一遍了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价