ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于BP神经网络实现鸢尾花分类的完整实践指南

基于BP神经网络实现鸢尾花分类的完整实践指南 简介一套基于 BP 神经网络实现鸢尾花分类的 Python 项目实践包面向机器学习入门者与高校学生可作为期末大作业或课程设计参考。资源由 15 个文件组成核心为 6 个 Python 源码、8 个 CSV 数据集与 1 份 Markdown 文档压缩包仅 24KB体量轻但覆盖完整流程目录结构清晰。源码包含 BP 神经网络 V1/V2 两种实现并附带 KNN、决策树、聚类等对比算法脚本从数据预处理、训练集/测试集划分到模型评估均有代码支撑CSV 文件分别存放原始数据与拆分后的训练测试数据方便对照比较不同算法的分类效果。配套文档说明实现思路与运行方式下载后简单部署即可使用无需复杂环境配置对课程设计与期末大作业有较高参考价值。已有 244 人浏览学习适合想通过经典鸢尾花分类快速上手神经网络项目的读者。1. 用Python基于BP神经网络实现鸢尾花分类是很多人第一个跑通的人工智能项目数据集只有150条样本、4个数值特征、3个类别小到用Excel都能打开却能完整走一遍“数据预处理 → 模型训练 → 结果评估”的全流程。手写BP神经网络做这个分类核心收获不是那90%多的准确率而是把反向传播的每一步都变成看得见、改得动的代码。这篇笔记按“原理 → 源码 → 踩坑 → 验证”展开让新手能跟着跑通让熟手能直接对照调参。准备动手前记住一句话这个项目代码量不大但最容易翻车的不是网络结构而是归一化、标签编码和数据划分这三件小事。2. 先看懂鸢尾花数据和BP神经网络的结构再写代码不迟2.1 鸢尾花数据集150条样本、4个特征、3个类别先看清数据再动手鸢尾花数据集的历史相当悠久它由英国统计学家Ronald Fisher在1936年引入是模式识别文献里被引用最多次的数据集之一。sklearn里内置了它所以做人工智能大作业或课程实验时几乎不需要额外准备数据文件。数据内容很简单三类鸢尾花分别是Setosa、Versicolour和Virginica每类正好50条样本没有类别不均衡的问题。四个特征是花萼长度、花萼宽度、花瓣长度、花瓣宽度单位是厘米。下面是直接从原数据集里摘出来的几条样例可以看到特征值的量级差异花萼长(cm)花萼宽(cm)花瓣长(cm)花瓣宽(cm)类别5.13.51.40.2Setosa4.93.01.40.2Setosa7.03.24.71.4Versicolour6.33.36.02.5Virginica5.02.03.51.0Versicolour注意一个关键事实Setosa的花瓣长度和宽度明显小于另外两类所以它和另外两类是线性可分的但Versicolour和Virginica在特征空间上有重叠区域这正是考验模型泛化能力的地方。如果你看到某个BP实现把训练集准确率做到100%但测试集只有85%多半不是网络结构不好而是数据划分出了问题后面会专门讲。这个数据集还有一个特点四个特征之间不存在严重的相关性冗余所以不需要做特征选择四条全用就行。常见误区是有人一上来就想用PCA降维对只有4个特征的数据做降维属于给自己找麻烦。标签编码方面源码里通常用0、1、2表示三个类别。这里有个很容易被忽略的坑如果你在后续代码里把标签写成1、2、3那只是表示符号变了问题不大但如果你直接用这个整数做回归目标是另一回事第4章会展开说。2.2 BP神经网络结构输入层4个、隐藏层8个、输出层3个隐藏层不是拍脑袋定的讲清楚数据之后看BP神经网络的结构。这是一个经典的三层结构输入层、隐藏层、输出层。输入层神经元数由特征数决定这里就是4个输出层神经元数由类别数决定这里用3个配合one-hot编码每个神经元对应一个类别。真正需要你自己定的是隐藏层的层数和神经元个数。针对鸢尾花这个任务单层隐藏层就够用。隐藏层神经元数没有严格公式常见做法是在4到12之间试我一般先取8。为什么是8几个经验公式交叉验证取输入输出层数的中间值4和3的平均值是3.5偏小用sqrt(n_in * n_out)计算约等于3.5再放大一到两倍后落入6到10区间。隐藏层太少模型表达能力不够训练集和测试集的准确率都会卡在80%左右上不去隐藏层太多比如50个模型会把训练集的噪声也记住测试集准确率反而下降这就是过拟合。激活函数的选择在这个项目里值得多写几笔。隐藏层用sigmoid这是经典BP教科书的标准做法导数是y乘以(1-y)计算简单输出范围在0到1之间适合小网络小数据。很多人一上来就换ReLU但ReLU在输入为负时梯度恒为0如果权重初始化不好训练初期就可能有一批神经元永久死亡体现在loss曲线上就是卡在一个平台再也不动。sigmoid在数据量小、网络浅的场景下反而更稳。输出层的激活函数也是sigmoid。严格来说多分类问题应该用softmax输出层每个神经元的取值代表该类别的概率三个值加起来等于1。但经典BP教材里用sigmoid加MSE损失也能在这个数据集上收敛到90%以上且梯度推导更直观适合把原理吃透。等你跑通之后想追求更高准确率再换softmax和交叉熵那是锦上添花的事。2.3 前向传播与反向传播误差怎么沿着网络往回传权重怎么一步步更新这节内容是整个源码的核心理论。前向传播长这样输入X经过隐藏层权重W1和偏置b1得到z1 X·W1 b1再过sigmoid得到a1 σ(z1)a1再经过输出层权重W2和偏置b2得到z2 a1·W2 b2再过sigmoid得到a2 σ(z2)。a2就是网络的预测输出。用均方误差衡量预测和真实标签的差距E 0.5 * Σ(a2 - y)²其中y是one-hot编码后的真实标签。反向传播做的事情就是把误差E对每一层权重的偏导数算出来。核心是链式法则。拿输出层权重W2举例∂E/∂W2 (a2 - y) · σ(z2) · a1其中σ(z2)是sigmoid的导数a1是隐藏层的输出。求隐藏层权重W1的梯度要把误差继续往前传一层∂E/∂W1 (a2 - y) · σ(z2) · W2 · σ(z1) · X。这个式子看着长但实际写代码时只需要保存每一层的中间输出从后往前逐层算不需要手动展开。权重更新用的就是梯度下降W W - η · ∂E/∂Wη是学习率。学习率太小loss下降像蜗牛爬学习率太大权重跨过最优点来回震荡。在鸢尾花任务上0.1到0.5之间是常见区间。如果只记住一句话那就是反向传播本质上是把“输出误差应该由哪些权重负责”这件事逐层分发回每一层参数上。不理解这句话后面调参会变成玄学。3. 手写BP神经网络源码数据预处理、网络类、训练评估三步走3.1 环境准备与数据预处理用sklearn加载数据手动把归一化做明白先说环境。这个项目只需要Python 3和两个库numpy做矩阵运算scikit-learn用来加载数据集和划分训练测试集。如果你还没装Python环境去官网下载3.8以上的版本装好后用pip install numpy scikit-learn就行。不需要GPU不需要深度学习框架这是这个项目适合入门的重要原因。下面是最小可执行的数据加载和预处理代码import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 加载鸢尾花数据X是150x4的特征矩阵y是0/1/2的标签数组 iris load_iris() X, y iris.data, iris.target # 按7:3比例划分训练集和测试集 # stratifyy 让每个类别的样本在划分后保持同等比例 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 标准化每个特征减去均值、除以标准差消除量纲差异 scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) print(X_train.shape, X_test.shape) # 输出(105, 4) (45, 4)代码里几个参数需要说明。test_size0.3表示测试集占30%45条样本训练集105条。random_state42是随机种子固定它保证每次运行划分结果一致这是实验结果可复现的前提。如果你不设这个参数每次跑代码划分都不同准确率会上下波动后面就没法对照调参了。stratifyy是容易被新手忽略的参数它保证训练集和测试集里三类鸢尾花各占三分之一不会出现某个类别在训练集里只有20条、在测试集里却有30条的情况。为什么要归一化四个特征虽然单位相同但花瓣宽的数据分布范围远小于花萼长。如果不做标准化损失函数在不同维度上的等高线会拉成一个狭长的椭圆梯度下降在其中来回震荡收敛极慢。StandardScaler的做法是让每个特征变成均值0、标准差1的分布。注意fit_transform和transform的区别fit_transform在训练集上计算均值和标准差transform在测试集上套用训练集的同一组参数。这是一个重要的工程习惯——测试集不能参与训练集的统计计算否则会造成信息泄漏。如果你不想依赖sklearn手动归一化也就三行mean X_train.mean(axis0) std X_train.std(axis0) X_train (X_train - mean) / std X_test (X_test - mean) / stdaxis0表示按列计算得到的是每个特征各自的均值和标准差。我建议你在源码里保留这种手动版本它比调用StandardScaler更能说明你理解了这个步骤在做的事。3.2 BP神经网络类初始化、前向、反向、训练四个方法逐个拆下面是一份可直接放进源码文件的手写BP神经网络实现。我把它设计成一个类四个核心方法对应四个阶段。import numpy as np class BPNetwork: 三层BP神经网络输入层 - 隐藏层 - 输出层 def __init__(self, n_input, n_hidden, n_output, learning_rate0.1): # 权重初始化为小随机数这里用randn乘0.5控制量级 # 不能全零初始化否则所有神经元会学到相同的特征 self.W1 np.random.randn(n_input, n_hidden) * 0.5 self.b1 np.zeros((1, n_hidden)) self.W2 np.random.randn(n_hidden, n_output) * 0.5 self.b2 np.zeros((1, n_output)) self.lr learning_rate def _sigmoid(self, x): # 裁剪防止exp溢出导致nan x np.clip(x, -500, 500) return 1.0 / (1.0 np.exp(-x)) def _sigmoid_deriv(self, y): # sigmoid的导数等于 y * (1 - y) return y * (1 - y) def forward(self, X): # 前向传播保存中间变量供反向传播使用 self.z1 np.dot(X, self.W1) self.b1 self.a1 self._sigmoid(self.z1) self.z2 np.dot(self.a1, self.W2) self.b2 self.a2 self._sigmoid(self.z2) return self.a2 def backward(self, X, y_onehot): # 反向传播逐层计算误差并更新权重 m X.shape[0] # 样本数 # 输出层误差 (预测值 - 真实值) * sigmoid导数 delta2 (self.a2 - y_onehot) * self._sigmoid_deriv(self.a2) dW2 np.dot(self.a1.T, delta2) / m db2 np.sum(delta2, axis0, keepdimsTrue) / m # 误差传回隐藏层 delta1 np.dot(delta2, self.W2.T) * self._sigmoid_deriv(self.a1) dW1 np.dot(X.T, delta1) / m db1 np.sum(delta1, axis0, keepdimsTrue) / m # 梯度下降更新所有参数 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 self.W1 - self.lr * dW1 self.b1 - self.lr * db1 def train(self, X, y_onehot, epochs500, verboseTrue): # 全量梯度下降每轮用全部样本计算一次梯度 for epoch in range(epochs): output self.forward(X) self.backward(X, y_onehot) if verbose and epoch % 100 0: # 均方误差0.5系数是为了求导方便 loss 0.5 * np.mean((output - y_onehot) ** 2) print(fepoch {epoch:4d}, loss{loss:.6f}) def predict(self, X): # 取输出层最大值下标作为预测类别 output self.forward(X) return np.argmax(output, axis1)代码的逻辑核心在前向和反向的配合。forward里用self.z1、self.a1这种变量名保存中间结果backward直接使用它们避免了重复计算。这是实现BP神经网络的标准做法前向传播时缓存中间值反向传播才能拿到sigmoid的输入来计算导数。参数方面初始化时n_input4、n_hidden8、n_output3learning_rate0.1。这里权重用np.random.randn乘0.5让初始权重落在[-0.5, 0.5]区间附近。如果想更稳可以用np.random.randn(n_input, n_hidden) * np.sqrt(2 / n_input)这是Xavier初始化的近似能缓解深层网络的梯度消失但在这个三层小网络上差别不大。训练用的是全量梯度下降即每个epoch用105条样本算一个平均梯度更新一次权重。数据集小全量计算代价很低梯度方向也稳定。有一个细节值得注意backward里算梯度时除以了m样本数。这是求平均梯度好处是梯度的量级不会因为batch变大而变化学习率不用随数据量额外调整。如果你看到别人的源码里没除m那他的学习率就需要相应调小两种写法都能收敛但参数含义不同切换时容易踩坑。3.3 主流程与训练评估one-hot编码、训练、预测、算准确率网络类写好之后主流程代码就清爽了。下面是完整的最小可执行脚本# 标签转为one-hot编码0 - [1,0,0]1 - [0,1,0]2 - [0,0,1] y_train_onehot np.eye(3)[y_train] y_test_onehot np.eye(3)[y_test] # 创建网络实例4输入 - 8隐藏 - 3输出 net BPNetwork(4, 8, 3, learning_rate0.1) net.train(X_train, y_train_onehot, epochs1000) # 对训练集和测试集分别做预测 pred_train net.predict(X_train) pred_test net.predict(X_test) # 准确率预测和真实标签相等的比例 train_acc np.mean(pred_train y_train) test_acc np.mean(pred_test y_test) print(f训练集准确率: {train_acc:.4f}) print(f测试集准确率: {test_acc:.4f})np.eye(3)[y_train]是生成one-hot编码最简洁的方式np.eye(3)生成3x3单位矩阵然后用y_train里的整数索引取对应行整个过程没有循环速度也快。预测阶段用np.argmax取输出层最大的下标三个输出值哪个最大就预测为哪一类。正常跑完1000轮训练集准确率通常在97%以上测试集在95%左右。如果结果明显更低不要急着调网络结构先去检查第4章讲的几个坑。如果loss卡在0.1以上不动优先怀疑隐藏层神经元数太少或学习率不合适。调参时建议每次只改一个参数同时固定random_state不然你无法判断准确率的变化是参数引起的还是随机划分引起的。养成记录每次实验参数和结果的习惯后面写文档说明时能省大量时间。4. BP神经网络训练避坑指南鸢尾花分类里最常见的5个翻车现场4.1 特征没归一化loss曲线像过山车现象不归一化直接训练loss在前面几百个epoch反复横跳甚至越训练越大。最终测试集准确率只有60%左右怎么调学习率都不见效。原因四个特征虽然量纲相同但分布范围差异明显。花瓣宽的数据集中在0.2到2.5之间花萼长的数据分布在4.3到7.9之间。损失函数对不同维度的敏感度不同导致梯度下降路径严重歪斜在狭长区域来回震荡。解决讲道理先做标准化再训练这个坑的解法在第3章已经给出来了。标准化的效果是让每个特征都在同一尺度上梯度下降能走直线。这不是可选项是必选项。很多源码包里的文档说明会特意写一句“数据经StandardScaler归一化”这句话值得你自己跑实验验证一遍归一化前跑一次归一化后跑一次对比loss曲线印象会非常深刻。4.2 学习率设太大loss不降反升现象learning_rate直接设成2.0第一个epoch的loss就从0.2飙到几十后面直接变成nan。预测结果全部是同一个类别。原因学习率过大权重更新的步长跨过了最优区域误差在每次迭代中持续累积最终权重数值爆炸。尤其是sigmoid的输入绝对值变得很大之后梯度几乎为0网络彻底瘫痪。解决把学习率从1.0降到0.1问题立刻消失。调学习率有个实操习惯从0.01开始每次乘以3或10去试找到loss能稳定下降的最大值。我的经验是鸢尾花任务0.1到0.3之间是最佳区间。如果你改完学习率还是nan检查权重初始化是不是用了太大的随机数randn乘1.0以上在深层结构里也容易爆。4.3 隐藏层神经元数拍脑袋过拟合还是欠拟合现象隐藏层神经元设为2个训练集和测试集准确率都卡在85%左右loss降到0.1后不再动改成50个训练集准确率到了99%测试集反而跌到90%。原因神经元太少模型表达能力不足学不出Versicolour和Virginica之间那条复杂一点的边界神经元太多模型把训练集里个别异常样本的噪声也记住了换到测试集上泛化变差。150条样本的数据集模型容量需要克制。解决固定其他参数把隐藏层神经元数依次设为4、6、8、12、16跑5次对比测试集准确率。8的效果通常已经很好再往上收益变小且过拟合风险增加。不要追求测试集准确率的微小提升而不断增加神经元数对一个入门项目来说训练集和测试集准确率差距在2到3个百分点以内是可接受的超过5个百分点就是明显的过拟合信号。4.4 标签编码和损失函数没配对loss算出一堆NaN现象没有把标签转成one-hot而是直接用整数标签0、1、2和网络输出做均方误差。loss一开始就很大训练中频繁出现nan。原因直接拿整数标签当回归目标等于告诉模型“类别0和类别1之间的距离是1类别0和类别2之间的距离是2”。鸢尾花的三类之间没有这种有序数值含义模型学到的边界是错的。另外手写sigmoid在输入值很大时numpy的exp会溢出产生inf再参与运算就变成nan。解决先把标签转成one-hot再训练这个坑消除一大半。sigmoid里加np.clip(x, -500, 500)把输入限制在安全范围防止溢出。如果你是在现有代码里排查nan先检查y_train_onehot的形状是不是(105, 3)再检查损失函数用的是不是(output - y_onehot)而不是(output - y)。4.5 训练测试集划分不当准确率虚高或虚低现象跑十次代码准确率从82%到98%之间波动每次都不一样。有时测试集里某个类别只有2条样本全预测错准确率直接跌破80%。有人不划分数据集直接用全量数据训练又用全量数据评估准确率100%“看起来很漂亮”。原因150条样本本身很少随机划分的偶然性很大。不固定随机种子每次划分的训练测试集都不同结果没有可比性。不划分数据集或用全量数据评估模型已经见过训练样本去评估它等于考试时直接给答案得到的准确率没有实际意义。解决固定random_state42同时用stratifyy保持类别比例。更进一步的做法是做5折交叉验证把150条样本分成5份轮流拿4份训练、1份验证5次准确率取平均值。这样做出来的数字才是一个真实可信的模型水平。一个能写进文档说明的习惯是给出最终用的划分方式、随机种子和单次准确率再给出交叉验证的平均准确率两个数字一起呈现。5. 用sklearn验证手写BP的正确性再把文档写成能交付的样子5.1 用MLPClassifier做基准对比准确率对得上才算真的跑通手写BP最尴尬的情况是代码跑出结果但你不确定结果对不对。最直接的验证方法是用sklearn的MLPClassifier用相同的数据、相同的网络结构和训练参数比较准确率。from sklearn.neural_network import MLPClassifier # hidden_layer_sizes(8,) 表示1层隐藏层、8个神经元 # activationlogistic 对应sigmoid激活函数 clf MLPClassifier( hidden_layer_sizes(8,), activationlogistic, learning_rate_init0.1, max_iter1000, random_state42 ) clf.fit(X_train, y_train) print(sklearn测试集准确率:, clf.score(X_test, y_test))注意两点sklearn的MLPClassifier默认用Adam优化器这里设了learning_rate_init为0.1且没换优化器实际用的是它内部的sgd风格更新尽量贴近手写实现。activationlogistic就是sigmoid。如果你的手写BP和sklearn的测试集准确率差距在2个百分点以内说明反向传播实现基本正确。如果差距偏大先检查梯度符号——这个错误最隐蔽表现出来是loss先降后升很多项目翻车都翻在这里。5.2 文档说明怎么写把实验结果整理成可交付的项目报告“源码文档说明”这个标题里文档的分量不低于源码。一份合格的项目文档建议包含以下内容项目背景一句话说明任务环境依赖列清Python版本和第三方库数据集说明写清来源、样本数、特征和类别分布模型结构画出网络层数和各层神经元数训练参数写清学习率、epochs、随机种子实验结果给出训练集和测试集准确率最好附一张loss下降曲线图关键代码放核心类并加注释问题和改进列几条你实际遇到的坑。这些内容不需要多华丽但每一条都要能对应到代码里。5.3 这个项目值不值得做价值不在鸢尾花本身我做了这个项目之后的一个实际体会是单纯跑通代码很快真正花时间的是反复对照实验结果、理解每个参数为什么这样设。所以如果你在做人工智能大作业或者准备面试项目不要只交一个能跑的结果把上文提到的实验记录、调参试错过程、对手写BP和sklearn实现的对比一起写进文档这个项目的价值会翻倍。我自己做这类入门项目时最后悔的一件事就是早期只跑通代码没留下实验记录后来写简历时想描述调参过程手头却拿不出任何数据。你可以从这个项目开始给每个关键参数留一行记录哪怕只是几行文本。等你做完这一步自然会发现这个方向值得继续投入希望帮到你。本文还有配套的精品资源点击获取
返回列表