手写BP神经网络实现鸢尾花和红酒分类:从原理到避坑
简介这是一份面向高校机器学习课程的BP神经网络实验资源以鸢尾花与红酒数据集为对象完成二分类/多分类建模练习适合正在学习前馈神经网络、反向传播算法或需要快速搭建课程实验的学生参考。压缩包共收录18个文件包括Python脚本、Jupyter Notebook、Excel格式的数据集、算法实践文档与实验讲义PPT其中脚本和Notebook可直接运行Excel保存原始数据文档与PPT则梳理实验流程与要点整体约630KB目录结构直观便于对照数据和代码进行调试。目前已有1034人学习下载具备较高的参考价值。资源内提供可直接运行的BP实现涵盖数据读取、网络训练、分类评估等完整流程同时附带实验说明文档和演示文稿可帮助理解网络层数、学习率、迭代次数等关键参数的调节思路也能为同类课程作业提供可复用的代码模板对实验中的损失变化与结果分析也有简要说明能减少初学者的踩坑时间。1. BP神经网络课程作业手写实现到底比调库难在哪做机器学习课程实验时最容易被“调库五分钟出结果”带偏等真要交一份BP神经网络课程作业要求手写反向传播、用BP网络完成鸢尾花和红酒数据集分类时很多人第一反应是去抄一个TensorFlow版本。实际翻开这份实验资源会发现它没有依赖深度学习框架而是用原生Python手写BP把网络结构定义、前向传播、反向传播、训练循环和两个数据集的分类脚本全部打包在同一个工程里。解压之后直接跑脚本就能拿到三分类结果和训练过程对写实验报告、理解梯度怎么一层层传回去帮助比调库大得多。2. 资源包拆解与手写BP的原理先看懂代码骨架再动手2.1 文件清单哪些是核心哪些只是附加材料拿到压缩包后先别急着跑把文件按用途分一下类。资源里看起来文件不少但真正决定“能不能跑通”的只有三个BP.py、iris_classify.py、winquality_classify.py。其余是数据、说明文档和课堂PPT。文件作用使用方式BP.py手写BP网络核心模块定义前向传播、反向传播和训练函数被分类脚本 import是整份资源的心脏iris_classify.py鸢尾花三分类脚本读取 iris_data.xls直接运行或作为实验报告代码附录winquality_classify.py红酒数据集分类脚本读取 winequality_data.xls直接运行注意数据预处理部分iris_classify.ipynb鸢尾花分类的 Notebook 版本适合逐步查看中间结果课堂演示用wine_classify.ipynb红酒分类的 Notebook 版本同上以单元格为单位执行iris_data.xls / xlsx鸢尾花实验数据150 条样本4 个特征 3 个类别pandas 读取winequality_data.xls / xlsx红酒质量数据约 1600 条样本11 个化学特征加 1 个评分列pandas 读取实验2-BP算法实践.doc实验指导文档包含实验目的、步骤和要填的结果表交作业前对照检查机器学习基础实验二.pptx课程讲义讲BP原理和实验要求写实验原理部分参考资源里还有个细节值得注意__pycache__目录下有BP.cpython-36.pyc这说明原始运行环境是 Python 3.6。新版 Python 3.8 / 3.10 / 3.11 跑也没问题只是会重新生成 pyc 缓存但要注意新版 Python 里pandas读取.xls的方式有变化这一点在后面的避坑章节单独说。2.2 手写BP的三段式骨架前向、反向、权重更新实验要求的核心是“自己实现BP”不是调用现成的神经网络库。所以 BP.py 里最关键的是网络结构定义和训练循环。课程作业里最常见的实现方式是把网络写成一个类这里按实验常见的写法给出一段骨架# BP.py 核心骨架与实验源码结构保持一致 import numpy as np def sigmoid(x): return 1.0 / (1.0 np.exp(-x)) def sigmoid_derivative(a): # a 是 sigmoid 激活后的输出直接用 a*(1-a) 计算导数 return a * (1.0 - a) class BP: def __init__(self, input_size, hidden_size, output_size, lr0.1): self.lr lr # 权重初始化乘 0.1避免 sigmoid 一开始就进入饱和区 self.w1 np.random.randn(input_size, hidden_size) * 0.1 self.b1 np.zeros(hidden_size) self.w2 np.random.randn(hidden_size, output_size) * 0.1 self.b2 np.zeros(output_size) def forward(self, x): self.z1 x.dot(self.w1) self.b1 self.a1 sigmoid(self.z1) self.z2 self.a1.dot(self.w2) self.b2 self.a2 sigmoid(self.z2) return self.a2 def backward(self, x, y, output): # 输出层误差(output - y) 来自 MSE 的导数 delta2 (output - y) * sigmoid_derivative(output) # 隐层误差按权重回传 delta1 delta2.dot(self.w2.T) * sigmoid_derivative(self.a1) # 梯度下降更新权重和偏置 self.w2 - self.lr * self.a1.T.dot(delta2) self.b2 - self.lr * delta2.sum(axis0) self.w1 - self.lr * x.T.dot(delta1) self.b1 - self.lr * delta1.sum(axis0)这段代码的三个关键点第一forward里每一层先做线性变换z x.dot(w) b再过sigmoid激活。第二backward里delta2用的是 MSE 损失对输出层加权输入的导数课程实验最常用的组合就是sigmoid MSE所以输出层误差直接写成了(output - y) * a2*(1-a2)。第三参数更新的方向是梯度下降学习率lr统一乘在梯度前面。从参数角度看input_size由数据集特征数决定鸢尾花是 4红酒是 11hidden_size是超参数课程实验里一般取 6 到 12太少拟合不动太多在小数据集上容易过拟合output_size是类别数鸢尾花 3红酒如果做三档分类也是 3。lr在 0.1 到 0.3 之间最稳太小收敛慢太大 loss 会震荡。2.3 输出层激活函数与损失函数的组合选择实验指导文档里对“输出层该用什么激活”通常不会写死这就导致很多人照抄其他代码时把组合混了。手写BP课程作业常见有三种组合第一种是sigmoid MSE这是资源里的默认组合也是最好实现的。输出层每个节点输出 0 到 1 之间的值配合 One-Hot 标签哪个节点值最大就预测为哪一类。第二种是softmax 交叉熵分类问题上理论上更合理但手写求导麻烦交叉熵对 softmax 输出的梯度恰好是output - y写起来比 MSE 还短。不过课程作业按sigmoid MSE交作业完全够用分数不会低。第三种是输出层不激活直接用线性输出配 MSE。这种写法在回归问题里常见但用在分类上输出层梯度不回传容易让训练卡在局部最优。所以我的建议是资源里的 BP.py 怎么写的就怎么用不要自己顺手把输出层改成softmax除非你有把握把backward一起改掉。混搭才是翻车重灾区。3. 鸢尾花分类实战从 xls 读取到三分类结果3.1 读取 xls 数据并做 One-Hot 标签编码实验数据是 xls 格式第一行通常是特征名后面每行是一条样本。鸢尾花数据前四列是花萼长宽、花瓣长宽第五列是品种名。这里有个容易踩的细节xls 和 xlsx 的读取引擎不一样直接用pandas.read_excel读 .xls 时需要确认本机装了 xlrd且版本要匹配。import numpy as np import pandas as pd # 实验文件没有严格表头时用 headerNone如果第一行是列名则改成 header0 df pd.read_excel(iris_data.xls, headerNone) X df.iloc[:, :4].values.astype(float) y_raw df.iloc[:, 4].values # 鸢尾花三个品种映射为 One-Hot 向量 species {Iris-setosa: 0, Iris-versicolor: 1, Iris-virginica: 2} y np.zeros((len(y_raw), 3)) for i, label in enumerate(y_raw): # strip 去掉单元格里可能存在的空格 y[i, species[str(label).strip()]] 1这段代码的逻辑是把字符串标签转成 3 维 One-Hot 向量比如Iris-setosa变成[1, 0, 0]Iris-versicolor变成[0, 1, 0]。为什么必须 One-Hot因为输出层节点数是 3每个节点对应一个类别训练时如果直接用整数 0、1、2 当标签网络会误以为类别之间有大小顺序训练出来的边界会很怪。注意headerNone和header0的区别实验文件如果第一行是“花萼长度”这类列名就改成header0否则会把列名当成一条样本数据。我一般先df.head()看一眼再决定这是最省事的习惯。3.2 网络参数怎么设隐层节点、学习率、迭代次数鸢尾花数据集只有 150 条样本30 条做测试的话训练集就 120 条。网络参数在这种小数据量场景非常敏感参考实验文档和资源脚本的运行效果推荐从这组参数起步参数推荐值说明输入节点4花萼长、花萼宽、花瓣长、花瓣宽隐层节点6 到 10节点太少欠拟合太多过拟合输出节点3三个品种配 One-Hot 标签学习率0.1 到 0.30.5 以上loss震荡0.01 以下收敛太慢迭代次数500 到 2000鸢尾花 2000 次以内足够批次方式全量梯度下降150 条样本不需要 mini-batch训练循环直接调用 BP 类bp BP(input_size4, hidden_size8, output_size3, lr0.1) for epoch in range(1500): output bp.forward(X_train) bp.backward(X_train, y_train, output) if (epoch 1) % 300 0: loss np.mean((output - y_train) ** 2) print(fepoch {epoch 1}, loss {loss:.6f})这里用的是整批训练也就是每轮把 120 条样本全部算一遍前向和反向再统一更新一次权重。150 条数据量下这样做没问题也不需要写 mini-batch 循环。loss打印出来是单调下降的到后面几个 epoch 基本不动说明训练已经收敛。参数调整思路如果 loss 在 0.5 以上下不去先降学习率如果 loss 一路降到小数点后四位但仍然分类错增大隐层节点或加迭代次数。这个顺序比乱调参数靠谱得多。3.3 切分训练集测试集shuffle 和 stratify 一个都不能省课程作业里最常见的问题是“不洗牌直接切分”。iris_data.xls 的文件顺序是 setosa 前 50 条、versicolor 中间 50 条、virginica 后 50 条如果直接取前 100 条训练、后 50 条测试测试集里就只有 virginica准确率根本没意义。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, shuffleTrue, stratifyy, random_state42 )shuffleTrue保证切分前打乱顺序stratifyy保证切分后训练集和测试集里三个类别各占三分之一这个参数在类别不平衡时尤其重要。红酒数据里 5 分 6 分样本多、3 分 4 分样本少没有stratify很容易某一档在测试集里数量接近 0。预测时取输出层三个节点里的最大值下标pred bp.forward(X_test).argmax(axis1) true y_test.argmax(axis1) acc (pred true).mean() print(ftest acc: {acc:.4f})这组参数下鸢尾花测试准确率通常在 95% 以上如果低于 90%优先检查标签编码和切分方式而不是急着调网络结构。手写 BP 在这样的小数据集上准确率上不去八成是数据预处理的问题。4. 红酒数据集分类从 11 维特征到质量分档4.1 红酒数据与鸢尾花的三个关键差异红酒质量数据集和鸢尾花虽然都是表格数据但处理方式差别很大。第一个差异是特征维度红酒有 11 个化学特征包括固定酸度、挥发性酸度、柠檬酸、残糖、氯化物、游离二氧化硫、总二氧化硫、密度、pH、硫酸盐、酒精。第二个差异是量纲固定酸度在 0.2 到 1.5 之间总二氧化硫可以到几十甚至上百酒精含量在 8 到 15 之间特征之间相差两个数量级。第三个差异是标签形式红酒数据集的标签是 0 到 10 的质量评分不是离散类别名所以要自己做分档。实验文档里的标准做法是把评分映射成三档常见映射是评分大于等于 7 为高质量大于等于 5 且小于 7 为中质量小于 5 为低质量。也有版本按大于等于 6 和小于 6 做二分类。资源里的winquality_classify.py用的是三档分类输出层节点数取 3。4.2 归一化是必须做的第一步不做必翻车这是红酒数据分类里最关键的预处理没有之一。BP 网络使用 sigmoid 激活输入过大时加权和会进饱和区梯度趋近于 0weight 几乎不更新。红酒数据里总二氧化硫上百、酒精十几直接送进网络训练时 loss 常见的情况是剧烈震荡或者一开始就卡住。from sklearn.preprocessing import StandardScaler df_wine pd.read_excel(winequality_data.xls, header0) X_wine df_wine.iloc[:, :11].values.astype(float) y_score df_wine.iloc[:, 11].values # 标准化的核心每个特征减均值除标准差变成均值0方差1 scaler StandardScaler() X_wine scaler.fit_transform(X_wine) # 质量评分映射为三档 def score_to_label(s): if s 7: return 0 # 高质量 elif s 5: return 1 # 中质量 else: return 2 # 低质量 y_wine np.zeros((len(y_score), 3)) for i, s in enumerate(y_score): y_wine[i, score_to_label(s)] 1StandardScaler的fit_transform会先计算每个特征的均值和标准差再用同样的均值和标准差去处理数据这是 z-score 标准化。另一种方案是 min-max 归一化把数值压到 0 到 1 之间公式是(x - min) / (max - min)。两个方案都能用但注意测试集要用训练集拟合出来的 scaler 去转换不能重新 fit否则测试集的数据分布被提前看过了。为什么是归一化而不是“把学习率调小”学习率调小只能缓解梯度震荡但特征之间量纲差距仍然存在高量纲特征直接把梯度方向带偏。归一化是解决量纲问题的根源学习率只是辅助顺序不能反。鸢尾花四个特征都在 0.1 到 7.9 厘米之间量纲接近所以不归一化也能跑红酒这组数据不做这一步后面所有调参都是白费。4.3 训练红酒网络隐层加大、迭代加长、看类别准确率红酒样本量约 1600 条比鸢尾花大一个量级网络容量也要相应调整。输入节点从 4 变成 11隐层节点建议从 8 加到 12 到 15输出节点保持 3。学习率建议从 0.1 往下试因为特征经过标准化后数值范围变小梯度幅度和鸢尾花不一样。# 红酒数据11个特征 - 12个隐层节点 - 3个输出节点 bp_wine BP(input_size11, hidden_size12, output_size3, lr0.1) Xw_train, Xw_test, yw_train, yw_test train_test_split( X_wine, y_wine, test_size0.25, shuffleTrue, stratifyy_wine, random_state42 ) for epoch in range(2000): out bp_wine.forward(Xw_train) bp_wine.backward(Xw_train, yw_train, out) if (epoch 1) % 500 0: loss np.mean((out - yw_train) ** 2) print(fepoch {epoch 1}, loss {loss:.6f}) pred_wine bp_wine.forward(Xw_test).argmax(axis1) true_wine yw_test.argmax(axis1) acc_wine (pred_wine true_wine).mean()注意这里有个坑红酒质量评分分布极不均匀评分 5 和 6 占了大多数3 分 4 分的比例很低。总体准确率会被多数类拉高比如全预测成中质量档也能刷出七八成的总体准确率。所以要看每类的召回率至少打印出混淆矩阵from sklearn.metrics import confusion_matrix print(confusion_matrix(true_wine, pred_wine))如果某一行几乎全是 0说明那个类别根本没被学出来多数原因是样本太少少数原因是网络容量不够或学习率太小。常见处理是增加隐层节点到 15或者把迭代次数提到 3000但别指望低分档的召回率能到 90%因为它的原始样本量就决定了上限。5. 避坑与常见问题五个让 loss 变成玄学的细节5.1 忘了归一化红酒准确率卡在六成现象红酒数据集训练 2000 次loss 在 0.2 附近震荡下不去测试准确率徘徊在 58% 到 65%怎么调学习率都没用。原因红酒 11 个特征量纲差异太大总二氧化硫和固定酸度相差两个数量级sigmoid 的输入饱和梯度更新被大数值特征主导。解决对特征做StandardScaler标准化处理把每个特征缩放到均值 0 方差 1然后重新训练。归一化之后 loss 能稳定降到 0.1 以下准确率通常能提升到 85% 以上。从那以后我拿到带“含量”“浓度”这类特征的数据集第一件事就是检查特征数值范围。5.2 xlrd 版本不兼容read_excel 报错现象代码里pd.read_excel(iris_data.xls)报错提示Excel xlsx file; not supported或者xlrd.biffh.XLRDError。原因xlrd 库从 2.0 版本开始只支持.xls不支持.xlsx而部分实验文件是 xlsx 格式或者你的 pandas 版本默认走 xlrd 引擎。解决如果是.xlsx文件指定引擎pd.read_excel(xxx.xlsx, engineopenpyxl)如果是.xls文件确认 xlrd 版本装成 1.2.0 而不是 2.x。最保险的做法是统一读取自动判断扩展名。资源里两个格式都给了优先用.xlsx加openpyxl引擎。5.3 学习率设成 0.5loss 一条锯齿线现象loss 打印出来不是下降曲线而是在某个值上下反复横跳比如 0.3、0.6、0.2、0.7。原因学习率过大每一步梯度下降跨过了最优点权重在极小值附近来回震荡始终无法收敛。手写 BP 没有梯度裁剪这个问题表现得更明显。解决把lr改成 0.1 再试loss 曲线会平滑很多。如果 0.1 还是震荡就 0.05。判断标准很简单loss 连续 100 次迭代没有上升趋势就说明学习率合适。课程作业阶段不用上动态学习率衰减固定 0.1 足够。5.4 不洗牌切分测试集里只有一个类别现象鸢尾花分类测试准确率极高接近 100%但换一个 random_state 之后骤降或者训练集准确率 99% 而测试集只有 40%。原因iris_data 文件按类别顺序排列前 50 条 setosa、中间 50 条 versicolor、后 50 条 virginica。不洗牌直接切分训练集和测试集类别分布完全失衡。解决train_test_split里加shuffleTrue和stratifyy。shuffle打乱样本顺序stratify保证切分前后的类别比例一致。这个习惯对红酒数据同样重要因为评分分布不均匀不 stratify 会让低分档从测试集里消失。5.5 用整数标签配 sigmoid 输出loss 曲线下不去现象标签没做 One-Hot 编码直接用一个整数 0、1、2 当 y训练过程中 loss 始终在 0.3 以上准确率时好时坏。原因输出层有 3 个节点每个节点输出 0 到 1 之间的小数而整数标签是单个数值和 3 维输出对不上。有的代码会强行把整数转成和输出一样的形状但网络把 0、1、2 当回归目标学分类边界完全错乱。解决把标签编码成 One-Hot 向量再训练预测时用argmax还原类别。这是第 3 章已经演示的species映射方式红酒数据三档分类同样处理。跳过这一步的代价是莫名其妙地多调两小时参数回头一看问题根本不在网络结构上。6. 进阶验证用五次重复实验判断代码到底行不行课程作业写完最怕被问一句“你这份结果稳不稳定”。单次运行准确率 96% 说明不了问题换个random_state可能就掉到 90%。等到交实验报告时我是用重复实验把训练结果变成一组统计数字而不是单个孤立的准确率。具体做法是把训练和评估包进循环跑五次完整流程记录每次的测试准确率from sklearn.model_selection import train_test_split accs [] for seed in range(5): Xtr, Xte, ytr, yte train_test_split( X, y, test_size0.25, shuffleTrue, stratifyy, random_stateseed ) bp BP(input_size4, hidden_size8, output_size3, lr0.1) for epoch in range(1500): out bp.forward(Xtr) bp.backward(Xtr, ytr, out) pred bp.forward(Xte).argmax(axis1) accs.append((pred yte.argmax(axis1)).mean()) print(accs:, accs) print(mean: %.4f, std: %.4f % (np.mean(accs), np.std(accs)))这段代码用不同随机种子做五次训练测试切分网络本身也会因为随机初始化而不同。输出的均值和标准差能直观反映稳定性均值高说明模型有效标准差小说明结果稳定。如果五次结果里有一次特别低先看那次切分里测试集类别分布是否正常通常就能找到问题。这个验证技巧同样适用于红酒数据集只需要把输入节点改成 11、隐层节点改成 12其他逻辑完全复用。课程实验报告里把均值加标准差写进结果表比单次准确率可信得多导师看了也知道你是真的跑透了代码而不是碰运气出的数字。从最开始拿到这份资源时直接运行、看输出、改参数到后来每次拿到新数据集都强制走一遍“归一化检查、切分检查、loss 曲线检查”这套流程帮我在重复性的实验里少踩了太多坑。尤其是红酒数据那次归一化前后的结果差距大到让我一度以为是代码写错了。希望这份拆解能帮你绕开同样的坑把时间用在真正该调的参数上。本文还有配套的精品资源点击获取