简介:这是一份面向高校机器学习课程的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, lr=0.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(axis=0) self.w1 -= self.lr * x.T.dot(delta1) self.b1 -= self.lr * delta1.sum(axis=0)这段代码的三个关键点:
第一,forward里每一层先做线性变换z = x.dot(w) + b,再过sigmoid激活。第二,backward里delta2用的是 MSE 损失对输出层加权输入的导数,课程实验最常用的组合就是sigmoid + MSE,所以输出层误差直接写成了(output - y) * a2*(1-a2)。第三,参数更新的方向是梯度下降,学习率lr统一乘在梯度前面。
从参数角度看,input_size由数据集特征数决定:鸢尾花是 4,红酒是 11;hidden_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 # 实验文件没有严格表头时用 header=None,如果第一行是列名则改成 header=0 df = pd.read_excel('iris_data.xls', header=None) 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 当标签,网络会误以为类别之间有大小顺序,训练出来的边界会很怪。
注意header=None和header=0的区别:实验文件如果第一行是“花萼长度”这类列名,就改成header=0,否则会把列名当成一条样本数据。我一般先df.head()看一眼再决定,这是最省事的习惯。
3.2 网络参数怎么设:隐层节点、学习率、迭代次数
鸢尾花数据集只有 150 条样本,30 条做测试的话训练集就 120 条。网络参数在这种小数据量场景非常敏感,参考实验文档和资源脚本的运行效果,推荐从这组参数起步:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入节点 | 4 | 花萼长、花萼宽、花瓣长、花瓣宽 |
| 隐层节点 | 6 到 10 | 节点太少欠拟合,太多过拟合 |
| 输出节点 | 3 | 三个品种,配 One-Hot 标签 |
| 学习率 | 0.1 到 0.3 | 0.5 以上loss震荡,0.01 以下收敛太慢 |
| 迭代次数 | 500 到 2000 | 鸢尾花 2000 次以内足够 |
| 批次方式 | 全量梯度下降 | 150 条样本,不需要 mini-batch |
训练循环直接调用 BP 类:
bp = BP(input_size=4, hidden_size=8, output_size=3, lr=0.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(f"epoch {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_size=0.25, shuffle=True, stratify=y, random_state=42 )shuffle=True保证切分前打乱顺序,stratify=y保证切分后训练集和测试集里三个类别各占三分之一,这个参数在类别不平衡时尤其重要。红酒数据里 5 分 6 分样本多、3 分 4 分样本少,没有stratify很容易某一档在测试集里数量接近 0。
预测时取输出层三个节点里的最大值下标:
pred = bp.forward(X_test).argmax(axis=1) true = y_test.argmax(axis=1) acc = (pred == true).mean() print(f"test 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 激活,输入过大时加权和会进饱和区,梯度趋近于 0,weight 几乎不更新。红酒数据里总二氧化硫上百、酒精十几,直接送进网络,训练时 loss 常见的情况是剧烈震荡或者一开始就卡住。
from sklearn.preprocessing import StandardScaler df_wine = pd.read_excel('winequality_data.xls', header=0) 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_size=11, hidden_size=12, output_size=3, lr=0.1) Xw_train, Xw_test, yw_train, yw_test = train_test_split( X_wine, y_wine, test_size=0.25, shuffle=True, stratify=y_wine, random_state=42 ) 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(f"epoch {epoch + 1}, loss {loss:.6f}") pred_wine = bp_wine.forward(Xw_test).argmax(axis=1) true_wine = yw_test.argmax(axis=1) 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', engine='openpyxl');如果是.xls文件,确认 xlrd 版本装成 1.2.0 而不是 2.x。最保险的做法是统一读取,自动判断扩展名。资源里两个格式都给了,优先用.xlsx加openpyxl引擎。
5.3 学习率设成 0.5,loss 一条锯齿线
现象: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里加shuffle=True和stratify=y。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_size=0.25, shuffle=True, stratify=y, random_state=seed ) bp = BP(input_size=4, hidden_size=8, output_size=3, lr=0.1) for epoch in range(1500): out = bp.forward(Xtr) bp.backward(Xtr, ytr, out) pred = bp.forward(Xte).argmax(axis=1) accs.append((pred == yte.argmax(axis=1)).mean()) print("accs:", accs) print("mean: %.4f, std: %.4f" % (np.mean(accs), np.std(accs)))这段代码用不同随机种子做五次训练测试切分,网络本身也会因为随机初始化而不同。输出的均值和标准差能直观反映稳定性:均值高说明模型有效,标准差小说明结果稳定。如果五次结果里有一次特别低,先看那次切分里测试集类别分布是否正常,通常就能找到问题。
这个验证技巧同样适用于红酒数据集,只需要把输入节点改成 11、隐层节点改成 12,其他逻辑完全复用。课程实验报告里把均值加标准差写进结果表,比单次准确率可信得多,导师看了也知道你是真的跑透了代码而不是碰运气出的数字。
从最开始拿到这份资源时直接运行、看输出、改参数,到后来每次拿到新数据集都强制走一遍“归一化检查、切分检查、loss 曲线检查”,这套流程帮我在重复性的实验里少踩了太多坑。尤其是红酒数据那次,归一化前后的结果差距大到让我一度以为是代码写错了。希望这份拆解能帮你绕开同样的坑,把时间用在真正该调的参数上。
本文还有配套的精品资源,点击获取