简介:这是一份基于PyCharm环境、采用Python语言编写的误差反向传播(BP)神经网络工程,用于求解鸢尾花(Iris)三分类问题,适合机器学习初学者完成分类实验或课程设计。压缩包共四个文件,包括两个源代码文件(一个负责网络建模,一个负责数据加载与训练调用)、一份逗号分隔值格式的鸢尾花原始数据集,以及一份Word实验报告文档,整体仅342KB,轻量便于下载,解压即可使用。目前已有六百六十八人学习下载,可快速入手实践,代码结构清晰易读。实验报告细致记录了数据归一化、网络层数与节点设计、学习率与迭代次数调整等关键步骤,并附带训练过程中的准确率与损失变化分析;配合可运行代码,读者能直接复现分类结果,直观理解误差反向传播如何通过链式法则更新权重,为后续改进结构或迁移到其他数据集提供扎实参考,对完成同类型实验也有借鉴价值。
1. BP神经网络模型求解iris分类:这份资源到手后的第一印象
BP神经网络模型求解iris分类,这份资源拿到手的第一感觉是:麻雀虽小,五脏俱全。rar压缩包解开之后是pycharm工程加实验报告两条线:工程里从读取鸢尾花数据、划分训练集测试集,到搭建三层BP网络、反向传播更新权重,再到输出测试集准确率和损失曲线,一条main.py走完;实验报告把每轮训练的loss、最终acc和可视化图形全部对齐到了代码输出上。对正在补机器学习作业、需要交python实验报告,或者想用一个最短路径把BP网络完整跑通的人来说,它能直接把“理论懂了但代码跑不通”的卡点抹平。工程不大,但该有的环节一个不少,适合拿来复现、改参数和二次扩展。
2. 为什么iris适合当BP入门项目:数据、网络结构与三个前提
我拆过不少机器学习相关的小型工程,iris数据集出现在里面的频率高得惊人。原因不在于它有多难,而在于它的规模、维度、类别分布都刚好卡在“手写BP能看清每一步数值变化”的范围里。150条样本、4维特征、3个类别,既不臃肿到训练十分钟,也不简单到只剩一个线性可分问题。资源里的工程对这个数据集做了标准化、分层切分、三层网络训练和测试集评估,整体逻辑是标准的监督分类流程。
2.1 解压后先确认两件事:数据加载方式与特征值范围
打开工程后第一件事不是直接点运行,而是先看main.py开头的数据加载部分。这份资源里常见的加载写法有两种:一种是用sklearn自带的方法把数据读成numpy数组;另一种是把iris的csv文件放在工程目录下,用numpy的文本读取函数读进来。两种方式最终得到的矩阵结构一致,都是150行4列。
iris数据集的4个特征分别是花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位是厘米,但数值范围差异明显:
| 特征列 | 含义 | 数据范围(约) | 量级 |
|---|---|---|---|
| 第1列 | 花萼长度 | 4.3 - 7.9 | 大 |
| 第2列 | 花萼宽度 | 2.0 - 4.4 | 中 |
| 第3列 | 花瓣长度 | 1.0 - 6.9 | 中 |
| 第4列 | 花瓣宽度 | 0.1 - 2.5 | 小 |
标签列是3个类别,分别对应3种鸢尾花,工程里一般会转换成0、1、2这样的整数。这里有一个非常容易被忽略的点:花瓣宽度的最大值2.5和花萼长度的最大值7.9相差超过3倍,如果特征直接喂给BP网络,反向传播算出来的梯度会被数值大的特征带偏,导致权重更新不平衡。所以工程里会在训练前调用一次标准化,把每列数据压缩到均值为0、方差为1的分布里,这一步是后续loss能顺利下降的前提。
2.2 三层网络结构:4-6-3是怎么定出来的
这份工程里的BP网络是三层结构:输入层4个节点、隐藏层6个节点、输出层3个节点。这三个数字不是随手填的,背后各有一层逻辑。
输入层节点数由特征维度决定,iris是4列特征,所以输入层是4。输出层节点数由类别数决定,3类鸢尾花,所以输出层是3。真正需要动脑的是隐藏层节点数。隐藏层的作用是把输入特征重新组合成更高阶的抽象表示,节点太少拟合能力不足,节点太多又容易在150条样本这种小数据集上过拟合。工程里选6,是参考了一个工程上常用的经验估算公式:取输入层和输出层节点数的几何平均附近的值,再顺手加点余量。sqrt(4*3)约等于3.5,取6已经算相对保守偏宽的配置。
如果沿用这个公式去试,隐藏层取4到12之间都能得到一个可用的结果,只是收敛速度和最终准确率会有差异。节点数越多,每个epoch的计算量越大,但真正影响准确率的往往不是隐藏层宽度,而是学习率和迭代次数的配合。对于BP网络来说,隐藏层只要不是极端到1个节点或者100个节点,整个模型的表现其实相当稳定。
2.3 手写BP还是调库:这份工程的价值不在“最优”而在“可读”
很多接触过sklearn的人会问同一个问题:iris分类不是调一行库就能做吗?为什么还要手写BP网络。答案在于你拿这份资源的目的。如果只需要一个分类结果,sklearn确实可以更快完成;但如果要写python实验报告、要把反向传播的每个矩阵乘法和梯度更新都讲清楚,就必须有一份能一行行读下去的代码。
这份工程里的main.py没有用任何深度学习框架,权重初始化、前向传播、反向传播、梯度下降全是用numpy手写的。这样做的好处是每一条计算都能和理论公式对应上,比如隐藏层的输出是输入矩阵和权重矩阵做点积再经过激活函数,输出层的预测值是隐藏层输出和输出层权重做点积再经过softmax。实验报告里画的那条loss曲线,训练过程中每一个值都能在代码里找到对应的计算行,不存在黑匣子。
熟手拿这份工程当基线也很顺手。矩阵都是显式定义的,改成其他数据集时只需要替换输入维度、输出维度和数据加载部分。想换激活函数、加一个隐藏层、改成mini-batch训练,所有改动都是在一个函数内部完成,不涉及框架层面的重写。这也是这份资源值得下载的理由:它不追求性能极限,而是追求逻辑透明。
3. 在pycharm里复现iris训练:核心代码、参数表与画图脚本
这一章直接落到可执行层面。在pycharm里新建项目、把rar里的文件解压进去、配置好解释器环境之后,核心就是main.py。我拆工程的习惯是先跑一遍原始代码再动手改,这样基线数据在手,后面怎么调都有参照。
3.1 训练主流程:从load_iris到test acc
main.py里的训练流程可以分为数据准备、网络初始化、训练循环、测试评估四段。数据准备部分的关键在于标准化和分层抽样,网络初始化部分的关键在于权重范围,训练循环部分的关键在于梯度下降更新。下面的代码是这套流程的完整技术示范,和工程里main.py的结构基本一致:
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # ---------- 1. 数据准备 ---------- iris = load_iris() X = iris.data # (150, 4) 四维特征矩阵 y = iris.target # (150,) 三类标签 0/1/2 # 分层抽样:保证训练集和测试集里三类样本比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 标准化:只对训练集拟合,再用同一参数转换测试集 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # ---------- 2. 网络初始化 ---------- input_dim = 4 # 输入层节点数,由特征维度决定 hidden_dim = 6 # 隐藏层节点数,可在 4-12 之间调整 output_dim = 3 # 输出层节点数,由类别数决定 lr = 0.1 # 学习率 # 小随机初始化:randn 之后乘以 0.1,避免梯度爆炸 np.random.seed(0) W1 = np.random.randn(input_dim, hidden_dim) * 0.1 b1 = np.zeros(hidden_dim) W2 = np.random.randn(hidden_dim, output_dim) * 0.1 b2 = np.zeros(output_dim) def relu(x): return np.maximum(0, x) def relu_deriv(x): return (x > 0).astype(float) def softmax(x): exps = np.exp(x - np.max(x, axis=1, keepdims=True)) return exps / exps.sum(axis=1, keepdims=True) # ---------- 3. 训练循环 ---------- y_onehot = np.eye(output_dim)[y_train] # 标签转 one-hot epochs = 2000 losses = [] for epoch in range(epochs): # 前向传播 z1 = X_train.dot(W1) + b1 # (105, 4) x (4, 6) -> (105, 6) a1 = relu(z1) z2 = a1.dot(W2) + b2 # (105, 6) x (6, 3) -> (105, 3) a2 = softmax(z2) # 交叉熵损失 loss = -np.mean(np.sum(y_onehot * np.log(a2 + 1e-8), axis=1)) losses.append(loss) # 反向传播 dz2 = a2 - y_onehot # 输出层梯度 dW2 = a1.T.dot(dz2) / X_train.shape[0] db2 = dz2.mean(axis=0) da1 = dz2.dot(W2.T) dz1 = da1 * relu_deriv(z1) # relu 导数为 0 或 1 dW1 = X_train.T.dot(dz1) / X_train.shape[0] db1 = dz1.mean(axis=0) # 梯度下降更新 W1 -= lr * dW1 b1 -= lr * db1 W2 -= lr * dW2 b2 -= lr * db2 if epoch % 100 == 0: print(f"epoch {epoch:4d}, loss={loss:.4f}") # ---------- 4. 测试评估 ---------- z1 = X_test.dot(W1) + b1 a1 = relu(z1) z2 = a1.dot(W2) + b2 y_pred = np.argmax(z2, axis=1) acc = np.mean(y_pred == y_test) print(f"test acc={acc:.4f}")先看数据准备段的两个关键设计。分层抽样是这份工程里做对的一个细节,iris的三类样本数量均匀(每类50条),但如果不加stratify=y,随机切分后训练集和测试集里的类别比例可能失衡,直接导致测试集准确率出现波动。标准化这里有一个新手经常踩的隐蔽坑:StandardScaler只能对训练集fit,测试集只能transform。如果对全部数据一起fit_transform,测试集信息会泄漏到训练过程中,模型评估结果偏乐观,实验报告里的数据也不真实。
再看网络初始化。W1和W2用randn生成标准正态分布的随机值再乘以0.1,这个0.1不是玄学。标准正态分布产生的数值集中在-3到3之间,两三百个epoch下去,多层矩阵乘法会让梯度成倍放大,容易出现nan。乘以0.1把权重压缩到-0.3到0.3之间,梯度量级就温和得多。
训练循环里的核心是反向传播那段。sz2 = a2 - y_onehot是交叉熵损失对输出层预激活值的梯度,这个式子的简洁性来源于softmax和交叉熵的数学配合,换成其他激活函数和损失函数组合就没有这么直接了。da1和dz1两行把输出层的梯度一步步传回隐藏层,relu的导数只是把输入大于0的位置置1、小于等于0的位置置0,这意味着relu会在训练过程中把部分神经元的梯度直接置零,让网络自动稀疏化。这是relu相比sigmoid更常用于隐藏层的原因。
测试评估段的逻辑和训练前向完全一致,只是不需要反向传播。argmax拿到每个样本在3个输出节点中最大值对应的下标,也就是预测类别,再和真实标签比较算准确率。
3.2 参数怎么配:给一组能直接复现的默认值
工程运行时涉及5个核心参数,它们各自的作用范围和推荐初始值如下:
| 参数 | 推荐值 | 作用 | 调整方向 |
|---|---|---|---|
| 学习率 lr | 0.1 | 控制每次权重更新的步长 | 损失震荡时调小到0.01 |
| 隐藏层节点数 | 6 | 决定网络拟合能力 | 欠拟合加到10,过拟合减到4 |
| 训练轮数 epochs | 2000 | 梯度下降迭代次数 | 收敛慢时加到3000 |
| 随机种子 | 0 | 固定权重初始化和数据切分 | 复现结果时必须固定 |
| 测试集比例 | 0.3 | 划分训练与测试数据量 | 数据少时降到0.2 |
学习率是最敏感的参数。0.1在iris这个场景下配合2000轮迭代能顺利收敛,但如果把隐藏层节点数加到20,训练集上的loss可能在后半程开始震荡,这时候优先把学习率降到0.05或0.01,而不是减小epochs。我一般会先跑一遍原始参数拿到基准loss曲线,再单独调学习率,每次只动一个参数。判断依据很简单:loss在前500轮平滑下降、后面稳步走低,说明参数组合比较健康;如果loss前几轮就开始反弹,学习率大概率偏大。
3.3 画损失曲线:把训练过程存成图,实验报告才有说服力
实验报告里缺不了loss曲线。工程里通常会单独准备一段绘图脚本,也可以直接在main.py训练循环结束后追加以下代码:
import matplotlib.pyplot as plt plt.plot(range(len(losses)), losses, linewidth=1.5) plt.xlabel("epoch") plt.ylabel("loss") plt.title("BP Training Loss on iris") plt.grid(True, linestyle="--", alpha=0.6) plt.savefig("loss_curve.png", dpi=150)这段代码能把每一轮的交叉熵损失画成一条单调下降的曲线。在pycharm里运行时,如果plt.show()弹不出窗口,常见原因是matplotlib后端没配置,把show()换成savefig()直接存文件是最省事的做法。绘图前先确认losses列表是从第一个epoch开始记录,而不是从第100轮才开始,否则曲线会缺一段,实验报告里看起来像训练一开始loss就很低。
4. 避坑与常见问题:跑BP时最容易踩的五类坑
手写BP网络跑iris,代码量不大,但每个环节都有对应的翻车姿势。我把这份工程复现过程中最常见的五类问题按现象到原因到解决列出来,给后来者省点debug时间。
4.1 特征没归一化,loss卡在0.7附近下不去
现象:训练循环跑起来,loss从1.0左右降了一段之后卡在0.68-0.72之间不再明显下降,测试集准确率始终在70%上下,预测结果偏向某两类。
原因:iris特征的量级差异在第二节已经说过,花萼长度最大接近8,花瓣宽度最小只有0.1。BP前向传播时,数值大的特征在矩阵乘法中占据主导,反向传播算出的梯度也集中在大特征对应的权重上,小特征对应的权重几乎得不到有效更新,模型拟合能力被限制。
解决:在数据准备阶段加StandardScaler,先fit_transform训练集,再transform测试集。加完之后loss能继续降到0.1以下,准确率普遍能到95%以上。
4.2 epoch到一半输出nan:权重初始化那个0.1不是玄学
现象:训练到几百轮时打印的loss变成nan,或者loss突然从正常数值跳到一个巨大负数。
原因:权重初始化时用了np.random.randn但没有乘以缩放系数。randn产生的数值服从标准正态分布,大部分落在-3到3之间,经过隐藏层和输出层的两次矩阵乘法再叠加梯度更新,数值会指数级放大。
解决:把初始化改成np.random.randn(...) * 0.1,限制在-0.3到0.3之间。如果还出现nan,检查反向传播里有没有除以样本数,漏掉X_train.shape[0]会让梯度过大。另外软max里对exp的输入减去最大值,能避免指数运算溢出到inf。
4.3 三分类最后没走softmax,准确率一直在70%徘徊
现象:模型能训练,但测试准确率长期在0.6到0.75之间上下浮动,无论怎么调学习率都突破不了。
原因:输出层用了sigmoid作为激活函数。sigmoid把每个输出节点独立压缩到0-1之间,三个节点的值之和不一定等于1,模型输出的不是一个合法的概率分布。它对二分类问题没问题,但在三分类场景下,类别之间的竞争关系没有得到建模。
解决:三分类及以上输出层使用softmax,把3个输出节点的值转换成一个和为1的概率分布。配合交叉熵损失,模型会对正确类别施加更强的梯度信号。
4.4 训练完预测结果全是同一类
现象:测试集准确率在0.3左右,打印y_pred发现所有样本都被预测成同一个类别。
原因:有两种可能。一是train_test_split没有设置stratify,测试集里某一类样本占比过高,模型学到的是数据集的先验分布;二是学习率过大,权重更新跨度过大,模型在训练初期就陷入了一个使输出偏向某类的局部极小区域。
解决:切分数据时显式加上stratify=y;学习率从0.1降到0.05再重新训练。如果训练集本身就严重不平衡,考虑增加少数类的训练比重。iris原始数据是均衡的,所以主要问题几乎都出在切分和初始权重上。
4.5 重跑结果和实验报告对不上:随机种子与切分顺序
现象:在pycharm里重新运行main.py,第二次跑出的最终准确率和第一次不一致,甚至和rar里实验报告记录的数据差好几个百分点。
原因:代码里没有固定随机种子。train_test_split每次切分训练集和测试集会得到不同的数据划分,权重初始化每次也是不同的随机矩阵。模型本身在这两个随机变量的影响下,测试准确率可能在92%到98%之间波动,这不是bug,是随机性。
解决:在数据切分之前和后、权重初始化之前分别固定随机种子,np.random.seed(0),并给train_test_split传入random_state=42。实验报告里也应该记录这两个值,否则任何第三方都无法复现原始结果。
5. 模型验证与进阶:把跑通变成一场可控的对比实验
对动手能力更强的读者,一个值得做的进阶操作是写一个循环,把隐藏层节点数当成变量,自动跑多组配置并输出对比结果。这个操作能让实验报告的“参数讨论”部分变得扎实,因为你给出的每一个结论都有数据支撑。
results = [] for h in [4, 6, 8, 12]: W1 = np.random.randn(input_dim, h) * 0.1 b1 = np.zeros(h) W2 = np.random.randn(h, output_dim) * 0.1 b2 = np.zeros(output_dim) # 重复第3.1节训练循环,固定epochs=2000, lr=0.1 z1 = X_test.dot(W1) + b1 a1 = relu(z1) z2 = a1.dot(W2) + b2 acc = np.mean(np.argmax(z2, axis=1) == y_test) results.append((h, acc)) for h, acc in results: print(f"hidden={h:2d}, acc={acc:.4f}")我一般还会在main.py最后加一个中间结果保存,把每轮loss和最终测试准确率写入CSV文件,这样报告里的图表可以随时回到代码核对。固定随机种子、记录参数、导出运行日志,这三件事配合起来,模型调参才有真正的参照系。从那以后我每次跑这种小数据集实验都会在文件开头固定随机种子,每轮改完参数先跑一遍基线对比,再去动网络结构。希望帮到你。
本文还有配套的精品资源,点击获取