资讯详情

资讯详情

BP神经网络手写实现iris分类:从原理到PyCharm跑通完整指南

简介面向机器学习初学者的BP神经网络Iris分类完整实验包基于Python与PyCharm环境实现解决经典鸢尾花三分类问题。压缩包共4个文件含两个Python脚本网络模型与用户调用、一份CSV数据集和一份Word实验报告合计342KB结构精简便于直接运行与对照学习。已有668人学习下载适合正在学习神经网络反向传播原理、想通过完整代码掌握数据预处理、前向传播、误差计算与权重更新流程的读者。代码注释清晰主函数与网络结构分离便于修改和复用实验报告记录了网络结构、参数设置及优化分析还包含数据划分、训练过程与结果分析。其中网络模型脚本实现前向传播与反向传播算法用户脚本负责数据加载、训练循环与结果可视化可辅助完成课程作业或入门实战。1. 一份 rar 里的 BP 神经网络先把 iris 跑通再谈其他如果你搜到“BP神经网络模型求解iris分类-pycharm.rar”说明你手里大概率拿到一份压缩包里面是一段用 PyCharm 写的 BP 神经网络代码目标是解决 iris鸢尾花三分类问题。这个标题里的组合其实点破了一个很现实的场景你不需要 TensorFlow也不需要 PyTorch用纯 Python 手工实现一个 BP 网络就能在 iris 上拿到 95% 以上的准确率。iris 分类是神经网络入门里最经典的“Hello World”数据只有 150 条、4 个特征、3 个类别恰好够把前向传播、反向传播、梯度下降这三件事讲透又不至于让训练等太久。适合的人群很明确刚学完 Python 语法、想搞清楚神经网络内部机制、又不想被深度学习框架的黑匣子劝退的人。这篇文章把这份代码从解压到跑通、从调参到避坑的完整路径拆给你照着做就行。2. iris 分类用什么网络结构先看懂 4-5-3 是怎么来的2.1 输入层 4 个节点不是随便定的iris 数据集里每行样本有 4 个数值特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。BP 网络输入层的节点数就是特征维度所以输入层定成 4 个节点。这个没什么好争论的特征有多少个输入就有多少个。但有个容易忽略的点特征数值范围不一样花萼长度在 4.3 到 7.9 之间花瓣宽度在 0.1 到 2.5 之间量纲差了好几倍。如果直接喂给网络梯度更新会被大数值的特征主导小数值特征对权重的贡献就被淹没了。所以代码里第一步通常做标准化把每个特征缩放到均值为 0、方差为 1 的分布上。这一点在你自己写网络时特别容易漏掉漏了之后准确率会掉到 80% 上下不是网络不行是数据预处理没做干净。输出层为什么是 3 个节点iris 有三个类别setosa、versicolor、virginica。一种做法是输出层用 1 个节点编码成 0、1、2但这会引入类别之间的顺序关系模型会误以为 setosa 和 versicolor 的距离比 setosa 和 virginica 更近这是错误的先验。常见做法是用 one-hot 编码setosa 对应 [1,0,0]versicolor 对应 [0,1,0]virginica 对应 [0,0,1]。输出层每个节点代表一个类别的概率谁的值大就判给谁。这个设计决策直接影响损失函数怎么选后面会细说。2.2 隐藏层节点数为什么先试 5 个隐藏层节点数没有理论上的最优解属于典型的“靠经验 少量实验”来定的参数。iris 场景下输入 4 维、输出 3 类数据量只有 150 条隐藏层 5 个节点起步是合理的。原因是分类边界本质上是在 4 维特征空间里切几刀5 个神经元提供的非线性拟合能力对这个小数据集已经够用。节点太少比如 2 个网络表达力不足分类边界太粗糙节点太多比如 50 个150 条样本根本喂不饱参数量网络会开始死记硬背训练集验证集准确率反而往下掉。我一般会给一个经验区间隐藏层节点数取输入层和输出层的几何平均值附近也就是 sqrt(4*3) ≈ 3.5向上取整 4 到 5 个。你可以在代码里把隐藏层节点数设成一个变量跑完 5 个节点后再改成 8 个、12 个对比一下。你会发现 5 个和 8 个的最终准确率差距很小但训练收敛速度有明显差异。这里的重点是理解“隐藏层宽度是容量控制阀”而不是执着于某个具体数字。2.3 激活函数组合隐藏层用 sigmoid输出层用 softmaxBP 网络里激活函数的选择决定了梯度能不能顺利传回去。隐藏层用 sigmoid 是传统 BP 网络的标配写法公式是 f(x) 1 / (1 exp(-x))输出范围在 0 到 1 之间有平滑的梯度。但 sigmoid 有两个明显毛病一是输出不以 0 为中心会让下一层的输入全部为正导致权重更新呈锯齿状二是两端饱和输入绝对值大了之后梯度趋近于 0这就是所谓的“梯度消失”。在 iris 这种 3 层浅网络里梯度消失影响不大所以 sigmoid 完全可用。如果换用 tanh输出范围 -1 到 1收敛通常会更快一点因为它以 0 为中心这也是很多实现偏爱 tanh 的原因。输出层不能用 sigmoid因为三个输出节点各自独立地做 sigmoid得到的值加起来不等于 1不能解释成概率分布。正确做法是 softmax对每个输出取指数再除以所有输出指数之和保证三个值非负且和为 1。softmax 和交叉熵损失是一对黄金搭档交叉熵的梯度形式是 (预测值 - 真实值)计算简单且梯度不会因为概率接近 0 或 1 而饱和。这一个组合直接决定训练能不能稳定收敛。3. 从零手写 BP 网络跑通 iris完整代码与参数说明3.1 数据准备加载、标准化、切分训练集下面这份代码不依赖任何深度学习框架只用 numpy 和 scikit-learn 的数据加载工具。把它完整放进 PyCharm 的一个 .py 文件里就能跑。python import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler加载数据data load_iris() X data.data # shape (150, 4)四列分别是花萼长宽、花瓣长宽 y data.target # 0, 1, 2 三个类别标签数据标准化均值为0方差为1避免量纲差异影响梯度scaler StandardScaler() X_scaled scaler.fit_transform(X)one-hot 编码标签0-[1,0,0], 1-[0,1,0], 2-[0,0,1]def one_hot(labels, num_classes3): return np.eye(num_classes)[labels]Y one_hot(y)切分训练集 120 条测试集 30 条固定随机种子保证可复现X_train, X_test, Y_train, Y_test train_test_split( X_scaled, Y, test_size0.2, random_state42, stratifyy )print(f训练集大小: {X_train.shape[0]}, 测试集大小: {X_test.shape[0]})这里有两个参数值得留意。第一train_test_split 里的 stratifyy 表示按类别比例分层抽样。iris 数据如果不分层切分运气差时可能出现测试集里完全没有某个类别的情况准确率指标就失真了。第二random_state42 固定了随机种子保证每次跑出来的切分结果一致。这个细节在做实验对比时特别重要没有固定种子的话你会分不清准确率的波动是模型引起的还是数据切分引起的。3.2 网络结构与前向传播python class BPNetwork: definit(self, input_size, hidden_size, output_size, learning_rate0.1): # 输入层到隐藏层的权重和偏置 self.W1 np.random.randn(input_size, hidden_size) * 0.1 self.b1 np.zeros((1, hidden_size)) # 隐藏层到输出层的权重和偏置 self.W2 np.random.randn(hidden_size, output_size) * 0.1 self.b2 np.zeros((1, output_size)) # 学习率控制每次参数更新的步长 self.lr learning_ratedef sigmoid(self, x): # 数值稳定写法x很大时 exp(-x) 趋近0不会溢出 return 1 / (1 np.exp(-x)) def softmax(self, x): # 减去最大值防止 exp 上溢不影响概率结果 exp_x np.exp(x - np.max(x, axis1, keepdimsTrue)) return exp_x / np.sum(exp_x, axis1, keepdimsTrue) def forward(self, X): # 隐藏层输出先线性变换再过 sigmoid self.z1 np.dot(X, self.W1) self.b1 self.a1 self.sigmoid(self.z1) # 输出层输出先线性变换再过 softmax得到各类别概率 self.z2 np.dot(self.a1, self.W2) self.b2 self.a2 self.softmax(self.z2) return self.a2, self.z1, self.a1, self.z2权重用 np.random.randn 初始化并乘以 0.1这是刻意的。randn 生成的是标准正态分布随机数乘以 0.1 后数值范围被压缩到 -0.1 到 0.1 之间。如果权重初始值太大经过 sigmoid 后输出会饱和在 0 或 1 附近梯度几乎为 0网络根本学不动。而偏置初始化为 0 没问题因为偏置本身不承担打破对称性的职责。learning_rate0.1 是 iris 场景下比较稳的取值后面会单独讲怎么调它。3.3 反向传播梯度推导与矩阵实现python def backward(self, X, Y, output, cache): z1, a1, z2 cache m X.shape[0] # 样本数量用于归一化梯度# 输出层误差softmax 交叉熵的梯度恰好是 (预测值 - 真实值) dz2 output - Y # 隐藏层到输出层的梯度 dW2 np.dot(a1.T, dz2) / m db2 np.sum(dz2, axis0, keepdimsTrue) / m # 隐藏层误差输出层误差通过 W2 反传再乘 sigmoid 的导数 a1*(1-a1) da1 np.dot(dz2, self.W2.T) dz1 da1 * a1 * (1 - a1) # 输入层到隐藏层的梯度 dW1 np.dot(X.T, dz1) / m db1 np.sum(dz1, axis0, keepdimsTrue) / m return dW1, db1, dW2, db2关键点都在注释里。softmax 和交叉熵配合后误差项 dz2 就是 output 减 Y这是白嫖了数学推导的结果。如果你输出层改用 sigmoid 而损失用均方误差那误差项就要乘以 sigmoid 的导数 output * (1 - output)训练速度和稳定性都会差一截。sigmoid 导数是 a1 * (1 - a1)这是 BP 公式推导里最经典的中间结果建议能自己推一遍不然以后换网络结构很容易栽在梯度形状对不上这个坑里。3.4 训练循环梯度下降更新权重python def update(self, dW1, db1, dW2, db2): # 梯度下降更新新权重 旧权重 - 学习率 * 梯度 self.W1 - self.lr * dW1 self.b1 - self.lr * db1 self.W2 - self.lr * dW2 self.b2 - self.lr * db2def train(self, X, Y, epochs500): losses [] for epoch in range(epochs): # 前向传播 output, z1, a1, z2 self.forward(X) # 计算交叉熵损失 loss -np.mean(np.sum(Y * np.log(output 1e-8), axis1)) # 反向传播得到梯度 dW1, db1, dW2, db2 self.backward(X, Y, output, (z1, a1, z2)) # 更新参数 self.update(dW1, db1, dW2, db2) losses.append(loss) if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.6f}) return losses训练循环里有个容易被忽略的细节loss 计算中使用了 output 1e-8。这是因为 log(0) 是负无穷如果某个样本的预测概率恰好好为 0损失就直接崩掉了。加上一个极小正数只是兜底不改变梯度的方向。epochs500 对 iris 这个规模足够一般跑 200 轮左右损失就降到 0.1 以下了。3.5 预测与准确率评估python def predict(self, X): output, _, _, _ self.forward(X) return np.argmax(output, axis1)完整训练流程model BPNetwork(input_size4, hidden_size5, output_size3, learning_rate0.1) losses model.train(X_train, Y_train, epochs500)train_pred model.predict(X_train) test_pred model.predict(X_test)train_acc np.mean(train_pred np.argmax(Y_train, axis1)) test_acc np.mean(test_pred np.argmax(Y_test, axis1))print(f训练集准确率: {train_acc:.4f}) print(f测试集准确率: {test_acc:.4f})预测阶段只需要前向传播取 softmax 输出中概率最大的那个节点索引作为类别。argmax 返回的是 0、1、2 的索引正好对应原始数据的类别编号。这里有个经验值可以参考iris 上用这份代码训练集准确率和测试集准确率都应该在 95% 以上。如果测试集准确率明显低于训练集比如训练集 100%、测试集 80%说明过拟合了常见对策是把 hidden_size 调小、增大训练数据但 iris 就 150 条或者加正则化。4. 在 PyCharm 里把代码跑起来环境配置与运行细节4.1 用社区版 PyCharm 就够不用折腾激活PyCharm 社区版是免费且开源的对跑 BP 网络这种纯 Python 项目来说一点问题都没有。专业版的优势在于 Django、数据库、远程开发这些功能跟 numpy 和 scikit-learn 完全无关。如果你手上只有社区版直接用它建项目就行。新建项目时关键一步是解释器选择PyCharm 会默认用 New environment using Virtualenv这就是虚拟环境每个项目一套独立的 Python 和第三方包避免多个项目依赖互相打架。创建好项目后在底部 Terminal 面板里安装依赖。注意一定要先确认 Terminal 里显示的路径是当前项目目录前缀会出现项目名。如果发现 pip 装到了全局环境大概率是解释器没选对去 File - Settings - Project - Python Interpreter 里检查。python pip install numpy scikit-learn matplotlib这三个包是这份 BP 网络代码的全部依赖。numpy 负责矩阵运算scikit-learn 只用来加载数据和切分数据集matplotlib 用来画损失曲线和分类结果图。安装过程中如果遇到网络慢或者超时常见做法是换镜像源例如清华源或者阿里源这里就不展开说了。4.2 新建 Python 文件并运行解释三种运行方式把上一章的三段代码合并到一个文件里命名为 bp_iris.py放在项目根目录。然后右键点击文件选择 Run bp_iris。PyCharm 会弹出运行窗口底部显示输出结果。如果你之前没配过解释器PyCharm 会提示 No Python interpreter configured这时点击提示里的 Configure选一个已经安装的 Python 3.8 以上版本即可。运行后看到输出结果大致是这样的text 训练集大小: 120, 测试集大小: 30 Epoch 0, Loss: 1.086312 Epoch 100, Loss: 0.128563 Epoch 200, Loss: 0.073214 Epoch 300, Loss: 0.052431 Epoch 400, Loss: 0.041927 训练集准确率: 0.9833 测试集准确率: 0.9667这就是一次成功的跑通。第一种运行方式是右键直接运行适合首次验证代码能跑。第二种是在 Terminal 里执行python bp_iris.py适合要传命令行参数时用。第三种是 PyCharm 的 Run Configuration 配置可以设置工作目录和环境变量一般到项目复杂了才需要。对这份 BP 网络代码来说第一种方式就够关键是先看到上面这样的输出。4.3 解决 PyCharm 里最常见的三个环境报错第一个报错是 ModuleNotFoundError: No module named numpy。现象是运行代码时红字提示找不到 numpy。原因是当前解释器里没装这个包或者 PyCharm 用的解释器跟你 pip install 的是同一个吗排查步骤打开 File - Settings - Project - Python Interpreter看右边列表里有没有 numpy。没有就点 号搜索安装。装完后如果还报错把解释器路径跟 Terminal 里which python的结果对照一下确认是同一个。第二个报错是 SyntaxError经常发生在 Python 2 环境里。iris BP 代码用了 print 函数带括号的写法这只有 Python 3 才合法。PyCharm 新建项目默认用 Python 3但如果你的解释器指向了系统自带的 Python 2.7老机器常见代码第一行就跑不过去。解决方法是在解释器设置里换成 Python 3 的路径。第三个问题是运行没报错但准确率只有 30% 左右看起来像“能用”但结果是随机的。这个问题的根源几乎都是数据没做标准化或者权重初始化用了不带缩放的大随机数。检查一下 X_scaled 是不是喂进了网络以及 W1 初始化是否乘以了 0.1 这个缩放因子。iris 分类准确率低于 85% 都是不正常的优先检查预处理不要先怀疑网络结构。5. 跑通之后必读BP 网络 iris 的五个高频踩坑记录5.1 损失不降反升输出全是 NaN现象训练到某个 epoch 后 loss 变成 nan或者从第 1 轮开始就是 nan后续输出全是 nan。原因只有一个方向数值溢出。最常见的是 exp 函数溢出softmax 里如果没有先减最大值当某个输出层的线性计算结果 z2 超过 88 时np.exp(z2) 直接变成无穷大后续所有计算全部崩掉。另一个常见原因是学习率过大导致权重更新一步跨得太远下一轮前向传播的加权和就飞了。解决softmax 里先做 x - np.max(x, axis1, keepdimsTrue)代码里已经写好。学习率从 0.1 调到 0.01 再看。如果原来学习率是 0.5改成 0.1 或 0.05nan 通常就消失了。权重初始化乘 0.1 这个细节也能兜底权重初始值小z 的数值就不会一开始就很大。5.2 测试集准确率一直停在 66.7%看起来像“学到了但没学全”现象损失在下降训练集准确率到 90% 以上但测试集准确率卡在 66.7%也就是大约三分之二。原因很可能是数据切分出了问题。iris 数据集有一个天然顺序陷阱前 50 条全是 setosa中间 50 条全是 versicolor最后 50 条全是 virginica。如果你忘了设 stratifyytrain_test_split 默认的随机切分也有一定概率让测试集里几乎全是两类样本。更极端的情况是某类样本一条都没分到测试集里这时候准确率会虚高或者恰好某一类完全没学到。解决切分代码里保持 stratifyy。如果代码里没有就加上。这个参数按原始类别比例分层保证训练集和测试集里三个类别的占比都接近 1:1:1。补上之后再跑测试集准确率通常回到 95% 以上。5.3 每次运行结果都不一样无法判断是模型好还是运气好现象同一份代码连续跑三次准确率分别是 96.7%、90.0%、93.3%。原因来自两个随机源一是权重初始化的随机数二是 train_test_split 的随机切分。这两个环节都没有固定随机种子结果就会波动。做实验时这种波动会严重干扰判断你没法区分准确率提升是因为模型改好了还是这次运气好。解决在代码最前面加两行np.random.seed(42)和固定 train_test_split 里的 random_state42。这样每次运行的数据切分和权重初始化都完全一致结果可复现。你的第一优先目标是可复现其次才追求更高的准确率。没有可复现性的调参完全是玄学。5.4 输出层加了 sigmoid准确率上不去现象网络结构看起来没毛病但训练 500 轮后准确率只有 85% 上下而且损失很难降到 0.1 以下。原因输出层用了 sigmoid 替换 softmax三个输出节点各自独立映射到 0 到 1互相之间没有竞争关系。这样的输出不能解释为概率分布交叉熵损失算出来也不是标准的类别交叉熵梯度的性质就变了。解决输出层改回 softmax。你要记住一条规律多分类任务的输出层几乎总是 softmax隐藏层用 sigmoid 或 tanh这个组合是 BP 网络在分类任务上的标准配置。不要因为 sigmoid 在隐藏层好用就顺手在输出层也用了。5.5 PyCharm 里运行正常但打包给别人跑不起来现象代码在自己的 PyCharm 里跑得很好压缩成 rar 发给你对方电脑上一运行就报 ModuleNotFoundError。原因对方的电脑没有配置 Python 环境或者没有安装 numpy 和 scikit-learn。即使对方装了 Python也可能用了不同的虚拟环境。跟代码本身没关系。解决拿到这种包时先看有没有 requirements.txt 文件有的话在 PyCharm Terminal 里执行pip install -r requirements.txt。没有的话手动装三个依赖。新建项目时解释器选 Virtualenv别选全局的。养成习惯把一份代码发给别人时带上 requirements.txt哪怕里面只有三行依赖也能省掉对方大量时间。6. 把准确率再往前的验证技巧学习率衰减与决策边界可视化对 iris 这种小规模数据集模型跑通只是第一步真正让你对 BP 网络建立直觉的是两个验证动作一是观察学习率对收敛路径的影响二是把分类边界画出来看网络到底学到了什么。可视化决策边界是检验网络学习质量最直观的手段。iris 四个特征无法一次画全常见做法是固定花萼长度和花萼宽度两个特征在二维平面上生成网格点用训练好的网络预测每个点的类别再用 plt.contourf 填色。下面的代码改一下 forward 的输入就能复用注意网格点也要做标准化而且要用之前 fit 好的 scaler 做 transform。python import matplotlib.pyplot as pltdef plot_decision_boundary(model, scaler, X_data, y_data): # 取前两个特征画图后面两个特征固定为均值 x_min, x_max X_data[:, 0].min() - 0.5, X_data[:, 0].max() 0.5 y_min, y_max X_data[:, 1].min() - 0.5, X_data[:, 1].max() 0.5 xx, yy np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200)) # 网格点补上另外两个特征的均值标准化后的0 grid np.c_[xx.ravel(), yy.ravel(), np.zeros_like(xx.ravel()), np.zeros_like(xx.ravel())] grid_scaled scaler.transform(grid) pred model.predict(grid_scaled) pred pred.reshape(xx.shape) plt.contourf(xx, yy, pred, alpha0.6, cmapRdYlBu) plt.scatter(X_data[:, 0], X_data[:, 1], cy_data, edgecolorsk, cmapRdYlBu) plt.xlabel(Sepal Length (standardized)) plt.ylabel(Sepal Width (standardized)) plt.show()plot_decision_boundary(model, scaler, X_test, np.argmax(Y_test, axis1))如果画出来的三个区域之间边界平滑、训练数据点基本落在对应颜色区域内说明网络学到了合理的非线性边界。如果边界剧烈扭曲且出现大量孤岛状小区域说明网络过拟合了隐藏层节点数减到 3 或 4 重试。我每次调完网络都会画这张图它比准确率数字更能说明问题——准确率只能告诉你“对不对”边界图能告诉你“为什么对”。至于学习率衰减这个小技巧你可以先跑一次 0.1 的学习率看损失曲线再把学习率改成 0.5 跑一次对比损失曲线的震荡幅度。你会发现 0.5 时损失下降更快但后期震荡明显准确率反而可能更低。动手改改 learning_rate 这个参数观察三种结果曲线欠拟合损失下降太慢、正常快速下降后平稳、发散损失震荡上升。这个实验做完你对 BP 网络调参的理解超过看十篇原理文章。我自己的习惯是跑完 iris 后把隐藏层节点数改成 8 和 3 各跑一遍把三份损失曲线叠在一张图里对比用半小时理解“容量”这个词的直观含义。这份 rar 里的代码对你来说不应该只是跑通交差值得花一个下午把每个参数都改一遍看效果。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →