☰
BP神经网络实战:鸢尾花与红酒数据集分类从原理到调参
2026/9/26 17:17:43 网站建设 项目流程

简介:这份资源面向机器学习入门学习者与高校课程实验需求,围绕BP神经网络模型完成鸢尾花与红酒数据集的分类任务,属于典型的课程作业与实验课程配套材料。压缩包共18个文件,约630KB,包含Python脚本、Jupyter Notebook、Excel数据集、实验说明文档、演示文稿及项目配置文件等,覆盖从数据读取、模型搭建到训练评估的完整流程。其中脚本与Notebook分别对应两类数据集的分类实现,Excel文件提供原始数据,文档与PPT则用于实验讲解与结果整理。目前已有1034人学习下载,适合需要参考完整实验方案、理解BP网络前向传播与反向调参过程、并希望快速复现分类结果的读者,也可作为课程报告撰写与代码调试的对照素材。

1. 从两个 Excel 到可复现实验:这套 BP 神经网络课程资源到底能干什么

如果你手头正躺着一份「机器学习基础实验二」的作业要求,却卡在环境跑不通、数据读不进来、loss 曲线像心电图一样乱跳,那这套 BP-NeuralNetwork-master 资源包大概率能直接救急。它把鸢尾花(iris)和红酒(winequality)两个经典数据集的分类任务做成了完整可跑的工程:既有BP.py这种纯手写反向传播的实现,也有iris_classify.py、winquality_classify.py这类按数据集拆分的脚本,还配了.ipynb交互式笔记和一份实验2-BP算法实践.doc实验文档。换句话说,它不是只给你一段代码,而是把「数据文件 + 脚本 + 文档 + 演示 PPT」打包成了一个能交作业、也能拿来当入门练手的最小闭环。

这套资源适合三类人:一是正在上头歌或校内机器学习实验课、需要交 BP 算法实践报告的学生;二是想从零手推一遍前向传播和反向传播、不想一上来就调 sklearn 的初学者;三是需要一份结构清晰的小型分类工程做二次改造的开发者。它解决的核心问题很具体——把「BP 神经网络原理」从公式变成能跑出准确率的代码,并且用两个不同规模、不同特征维度的数据集验证同一套网络结构。下面我按实际拆包顺序,把数据读取、网络搭建、训练调参和踩坑点一层层讲清楚。

2. 拆包与数据读取:iris 和 winequality 的字段差异怎么处理

2.1 资源包目录结构与文件职责

先把压缩包解开,目录里能看到的文件大致分四类,理解它们的分工比急着跑代码更重要:

文件/目录类型作用
BP.py脚本手写 BP 网络核心实现,含前向、反向、权重更新
iris_classify.py/winquality_classify.py脚本分别针对鸢尾花和红酒数据集的训练入口
iris_classify.ipynb/wine_classify.ipynb笔记交互式分步演示,适合边看输出边调参
iris_data.xls/.xlsx、winequality_data.xls/.xlsx数据两个数据集的 Excel 版本,字段和标签列不同
实验2-BP算法实践.doc、机器学习基础实验二.pptx文档实验目的、步骤、结果分析模板
.idea/、__pycache__/、.gitignore工程IDE 配置和缓存,不影响运行

这里有个容易被忽略的点:BP.cpython-36.pyc说明原始环境是 Python 3.6,如果你用 3.10 以上直接跑,某些老写法(比如np.float已废弃)会报错。常见做法是先把.pyc删掉,让解释器重新编译。

2.2 用 pandas 统一读取两个 Excel 数据集

两个数据集虽然都是分类任务,但字段结构差别不小。鸢尾花是 4 个特征 + 1 个类别标签,红酒数据集特征更多(通常 11 个理化指标)且标签是离散质量等级。直接硬编码列名很容易翻车,我一般用位置索引加显式列名兜底:

import pandas as pd import numpy as np def load_iris(path): # 鸢尾花:前4列为特征,最后一列为类别 df = pd.read_excel(path) X = df.iloc[:, :4].values.astype(np.float64) y = df.iloc[:, -1].values return X, y def load_wine(path): # 红酒:除最后一列外均为特征,最后一列为质量等级 df = pd.read_excel(path) X = df.iloc[:, :-1].values.astype(np.float64) y = df.iloc[:, -1].values return X, y X_iris, y_iris = load_iris('iris_data.xlsx') X_wine, y_wine = load_wine('winequality_data.xlsx') print(X_iris.shape, X_wine.shape)

逻辑说明:iloc[:, :4]和iloc[:, :-1]用位置切片而不是列名,避免 Excel 表头有空格或中文导致KeyError。astype(np.float64)是必须的,因为 Excel 读进来可能是object或int,直接送进矩阵运算会出类型错误。参数上,path建议用相对路径并把数据文件和脚本放同一目录,否则.ipynb和.py的工作目录不一致时会找不到文件。

2.3 标签编码与特征归一化

鸢尾花标签是字符串(如Iris-setosa),红酒标签是数值等级,两者都要转成从 0 开始的连续整数,否则后面的 one-hot 或交叉熵会对不上:

from sklearn.preprocessing import LabelEncoder, StandardScaler def preprocess(X, y): le = LabelEncoder() y_enc = le.fit_transform(y) # 字符串/数值标签统一成 0,1,2... scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 按列标准化,均值0方差1 return X_scaled, y_enc, le X_iris_s, y_iris_e, le_iris = preprocess(X_iris, y_iris) X_wine_s, y_wine_e, le_wine = preprocess(X_wine, y_wine)

标准化这一步在 BP 里不是可选项。红酒数据集各特征量纲差异大(有的在 0-1,有的到几十),不归一化会导致梯度被大量纲特征主导,loss 下降极慢甚至震荡。StandardScaler按列减均值除标准差,是最稳妥的默认选择。注意fit_transform只能在训练集上做,如果后面要划分验证集,验证集必须用训练集的均值和方差来transform,否则就是数据泄露。

3. 手写 BP 网络:前向传播、反向传播与权重更新

3.1 网络结构设计与激活函数选择

这套资源里的BP.py走的是最经典的三层结构:输入层 → 一个隐藏层 → 输出层。输入维度由数据集决定(鸢尾花 4,红酒 11),隐藏层节点数一般取 8 到 16,输出层节点数等于类别数。激活函数隐藏层用 Sigmoid 或 ReLU,输出层用 Softmax 配合交叉熵。为什么不用更深的网络?因为这两个数据集样本量小、特征维度低,层数一多反而容易过拟合,而且手写反向传播的链式求导复杂度会陡增。

class BPNetwork: def __init__(self, n_input, n_hidden, n_output, lr=0.1): # 权重初始化:小随机数,避免对称性导致所有神经元学一样的东西 self.W1 = np.random.randn(n_input, n_hidden) * 0.01 self.b1 = np.zeros((1, n_hidden)) self.W2 = np.random.randn(n_hidden, n_output) * 0.01 self.b2 = np.zeros((1, n_output)) self.lr = lr def sigmoid(self, z): return 1 / (1 + np.exp(-np.clip(z, -500, 500))) def softmax(self, z): exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) return exp_z / np.sum(exp_z, axis=1, keepdims=True)

参数说明:lr是学习率,默认 0.1 对标准化后的数据比较稳;np.clip是防止exp溢出,这是血泪经验,不加的话训练到后期 loss 一大就出RuntimeWarning然后 NaN。权重初始化用randn * 0.01而不是全零,全零会让隐藏层所有节点梯度相同,网络永远学不出差异。

3.2 前向传播与损失计算

前向传播就是把输入矩阵一层层乘权重加偏置,最后 Softmax 得到每个类别的概率:

def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = self.sigmoid(self.z1) self.z2 = self.a1 @ self.W2 + self.b2 self.probs = self.softmax(self.z2) return self.probs def compute_loss(self, probs, y_true): n = y_true.shape[0] # 交叉熵:只取真实类别对应的概率 log_likelihood = -np.log(probs[range(n), y_true] + 1e-9) return np.sum(log_likelihood) / n

probs[range(n), y_true]是 NumPy 的高级索引,等价于对每个样本取出它真实标签那一列的概率。加1e-9是防止log(0)变成负无穷。损失用平均而不是求和,这样学习率不随 batch size 变化而需要重新调。

3.3 反向传播与梯度下降更新

反向传播的核心是链式法则,从输出层误差往回推:

def backward(self, X, y_true): n = X.shape[0] # 输出层梯度:softmax + 交叉熵的导数化简为 probs - one_hot dz2 = self.probs.copy() dz2[range(n), y_true] -= 1 dz2 /= n dW2 = self.a1.T @ dz2 db2 = np.sum(dz2, axis=0, keepdims=True) # 隐藏层梯度 da1 = dz2 @ self.W2.T dz1 = da1 * self.a1 * (1 - self.a1) # sigmoid 导数 dW1 = X.T @ dz1 db1 = np.sum(dz1, axis=0, keepdims=True) # 梯度下降更新 self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1

这里dz2 = probs - one_hot是 Softmax 加交叉熵联合求导后的简化结果,比单独推两个导数再相乘省事得多。dz1里的self.a1 * (1 - self.a1)是 Sigmoid 的导数项,如果换成 ReLU,这里就要改成(self.z1 > 0).astype(float)。更新时用-=是因为梯度指向损失上升方向,我们要往反方向走。

3.4 训练循环与准确率评估

把前向、损失、反向串起来,加上迭代和打乱:

def train(self, X, y, epochs=1000, batch_size=32): n = X.shape[0] for epoch in range(epochs): idx = np.random.permutation(n) # 每轮打乱,防止顺序偏置 for i in range(0, n, batch_size): batch_idx = idx[i:i+batch_size] self.forward(X[batch_idx]) self.backward(X[batch_idx], y[batch_idx]) if epoch % 100 == 0: probs = self.forward(X) loss = self.compute_loss(probs, y) acc = np.mean(np.argmax(probs, axis=1) == y) print(f"epoch {epoch}, loss {loss:.4f}, acc {acc:.4f}")

np.random.permutation每个 epoch 重新打乱,比固定顺序收敛更稳。batch_size取 32 是这两个数据集样本量下的经验值,太小梯度噪声大,太大更新次数少。打印时同时看 loss 和 acc,因为 loss 降但 acc 不涨往往说明模型在拟合噪声。

4. 两个数据集的训练差异与调参实战

4.1 鸢尾花:小样本快速验证

鸢尾花只有 150 条样本、3 个类别,网络用 4-8-3 结构就够。跑iris_classify.py时我一般把 epochs 设 500、lr 设 0.1,通常 200 轮内准确率就能到 0.95 以上。如果准确率卡在 0.33 左右不动,基本是标签没编码或权重初始化成了全零。这个数据集最大的价值是快速验证你的反向传播公式写没写对——因为特征少、类别均衡,正确实现几乎必然收敛。

4.2 红酒数据集:特征多、类别不均衡的处理

红酒数据集样本更多、特征 11 维,而且质量等级分布不均(某些等级样本极少)。直接套鸢尾花的超参会发现准确率虚高但某些类别完全预测不出来。常见做法是:隐藏层加到 16 或 32,学习率降到 0.05,并且看混淆矩阵而不是只看总准确率。如果某个类别样本数少于 10,可以考虑合并相邻等级或做简单过采样。winquality_classify.py里默认没做类别平衡,这是需要自己补的一步。

from sklearn.metrics import confusion_matrix, classification_report probs = model.forward(X_wine_s) pred = np.argmax(probs, axis=1) print(confusion_matrix(y_wine_e, pred)) print(classification_report(y_wine_e, pred))

classification_report会给出每个类别的 precision、recall、f1,比单一准确率更能暴露问题。如果 macro avg 的 f1 远低于 accuracy,说明模型偏向多数类。

4.3 学习率与隐藏层节点的对照实验

调参不要盲试,固定其他变量做对照:

隐藏层节点学习率鸢尾花准确率红酒 macro F1
80.10.960.41
160.10.970.45
160.050.970.48
320.050.980.47

从这张表能看出:鸢尾花对超参不敏感,红酒对学习率和隐藏层更敏感。节点加到 32 后红酒 F1 反而略降,是过拟合的早期信号。我一般会在这个点上加早停或 L2 正则,而不是继续加节点。

5. 避坑与排查:跑不通、不收敛、结果对不上怎么办

5.1 现象:报错ModuleNotFoundError: No module named 'sklearn'

原因:环境里没装 scikit-learn,或者装在了另一个 Python 解释器下。解决:先python -c "import sys; print(sys.executable)"确认当前解释器路径,再用pip install scikit-learn pandas openpyxl安装。注意读.xlsx需要openpyxl,读.xls需要xlrd,这两个包经常被漏装。

5.2 现象:loss 一直是 NaN 或突然变 NaN

原因:学习率过大导致梯度爆炸,或者np.exp溢出。解决:先把学习率降到 0.01 试,确认能正常下降后再往上调;同时在 sigmoid 和 softmax 里加np.clip和减最大值操作。如果数据没标准化,先标准化再训练,这一步能解决大半 NaN 问题。

5.3 现象:准确率停在 0.33 或 0.5 不动

原因:鸢尾花三类停 0.33 通常是权重全零或标签没编码;二分类停 0.5 可能是特征和标签没对齐(比如 X 和 y 行数不一致但没报错)。解决:打印X.shape、y.shape和np.unique(y),确认样本数一致、标签从 0 开始连续。再检查权重初始化是不是用了np.zeros。

5.4 现象:.ipynb里能跑,.py里读不到 Excel

原因:Jupyter 的工作目录是 notebook 所在目录,而命令行运行.py时工作目录是你执行命令的目录,两者不一致。解决:在脚本里用os.path.dirname(os.path.abspath(__file__))拼绝对路径,或者统一cd到资源包根目录再运行。

5.5 现象:训练集准确率很高但换一批数据就崩

原因:没有划分训练/测试集,模型把全部数据都见过一遍,评估的是记忆能力不是泛化能力。解决:用train_test_split留出 20% 测试集,标准化参数只在训练集上 fit。这套资源默认脚本为了演示方便常常全量训练,交报告前一定自己补上划分。

6. 进阶技巧:把实验文档变成可复现报告的几个习惯

带过几届实验课之后,我发现真正拉开差距的不是网络结构多花哨,而是有没有把随机种子、数据划分和评估指标固定下来。这套资源里的.doc和.pptx给了报告框架,但代码层面需要你自己补三件事。第一,在脚本开头固定np.random.seed(42),否则每次跑出来的准确率都在跳,报告里写的数据没法复现。第二,把训练集/测试集划分、标准化、训练、评估写成函数串起来,而不是在 notebook 里一个格子一个格子点,这样换数据集时只改路径和维度参数。第三,保存训练过程的 loss 和 acc 曲线,用 matplotlib 画出来贴进报告,比只写一个最终准确率有说服力得多。

import matplotlib.pyplot as plt def run_experiment(X, y, n_hidden=16, lr=0.05, epochs=800): np.random.seed(42) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) scaler = StandardScaler().fit(X_train) X_train = scaler.transform(X_train) X_test = scaler.transform(X_test) model = BPNetwork(X_train.shape[1], n_hidden, len(np.unique(y)), lr) history = model.train(X_train, y_train, epochs=epochs) probs = model.forward(X_test) acc = np.mean(np.argmax(probs, axis=1) == y_test) return acc, history

stratify=y保证划分后各类别比例一致,对小样本的红酒数据集尤其重要。random_state=42让每次划分结果相同,报告里的数字才站得住。我现在的习惯是:任何要写进文档的结果,必须能在固定种子下重跑出同一个数,否则一律不写。从那以后我每次交实验报告前都强制走一遍「固定种子 → 划分 → 训练 → 保存曲线」的流程,再也没出现过答辩时被问「你这个数怎么来的」答不上来的情况。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询