ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

手写BP神经网络实现鸢尾花分类:完整工程与实验报告

手写BP神经网络实现鸢尾花分类:完整工程与实验报告

简介:这份资源面向机器学习入门者与需要完成课程实验的学生,围绕经典Iris鸢尾花数据集,用Python实现BP神经网络分类任务,帮助读者理解多层前馈网络从数据预处理、前向传播到反向传播的完整流程。压缩包共4个文件,包含2个py脚本、1份docx实验报告和1份csv数据文件,整体约342KB,脚本对应网络构建与训练逻辑,文档记录实验过程与参数设置,数据文件即Iris样本集。目前已有669人学习下载。资源价值在于将网络结构设计、权重初始化、激活函数选择、损失计算与模型评估等环节落到可运行代码中,并配有实验报告说明网络结构与超参数设置,便于读者对照复现、分析不同学习率与迭代次数对分类性能的影响,适合作为BP神经网络原理学习与实验报告的参考范例。

1. 从一份能跑通的 BP 神经网络作业说起

第一次拿到这个压缩包,我以为是那种只有一份 Word 实验报告的“交作业专用”资源,解压之后发现里面确实有可运行的 Python 工程,PyCharm 直接打开就能跑,iris 数据集也一并带上了。它做的事情很具体:用纯手写的 BP 神经网络完成鸢尾花三分类,前向传播、反向传播、权重更新全部自己实现,没有调用 sklearn 的 MLPClassifier,也没有用 PyTorch。对正在做课程实验、需要交一份带实验报告的 BP 神经网络作业的人来说,这东西的价值不在于算法多新,而在于链路完整——数据加载、网络初始化、训练循环、损失曲线、混淆矩阵、报告文档,一个不缺。下面我按“它是什么、怎么跑、代码怎么写的、哪里容易翻车、怎么改成自己的”这个顺序拆一遍。

2. 工程结构与运行链路:从解压到第一张损失曲线

2.1 目录里都有什么

解压后的结构大致是这样,不同人打包习惯略有差异,但核心文件跑不掉:

BP-iris/ ├── data/ │ └── iris.csv # 150 条样本,4 特征 + 1 标签 ├── src/ │ ├── bp_network.py # 网络核心:初始化、前向、反向、更新 │ ├── train.py # 训练入口,读数据、跑 epoch、存模型 │ ├── evaluate.py # 加载权重,算准确率、混淆矩阵 │ └── utils.py # 归一化、one-hot、划分训练测试集 ├── outputs/ │ ├── loss_curve.png # 训练过程损失曲线 │ ├── confusion_matrix.png # 测试集混淆矩阵 │ └── weights.npz # 训练好的权重 ├── report/ │ └── 实验报告.docx # 实验目的、原理、结果分析 ├── requirements.txt └── README.md

data/iris.csv是标准 UCI 鸢尾花数据,150 行,三类各 50 条,特征为花萼长宽、花瓣长宽四个连续值。src/下四个文件职责分得很干净:bp_network.py只管网络数学,train.py管流程编排,evaluate.py管指标,utils.py管数据预处理。这种拆法对二次开发很友好,想换数据集只动utils.py,想换网络结构只动bp_network.py。

2.2 环境准备与依赖

requirements.txt里通常只有三样东西:numpy、matplotlib、pandas。没有深度学习框架,这是这个项目的定位决定的——它要展示 BP 的每一个矩阵运算,而不是调包。安装命令:

# 建议在虚拟环境里装,避免和本机其他项目的 numpy 版本打架 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt

Python 版本 3.8 以上都能跑,numpy 建议 1.21 以上。这里有个血泪经验:如果本机装过 TensorFlow 或 PyTorch,numpy 版本可能被锁在某个旧版本,np.float这类老写法在新 numpy 里已经移除,跑起来会报AttributeError。遇到这种情况,别急着改代码,先pip show numpy看一眼版本,必要时在虚拟环境里单独装一个干净的。

2.3 跑通训练与评估

训练入口是train.py,直接运行:

python src/train.py

跑完会在outputs/下生成loss_curve.png和weights.npz。再跑评估:

python src/evaluate.py

终端会打印测试集准确率和每个类别的精确率、召回率,同时生成混淆矩阵图。正常配置下,测试集准确率在 95% 以上,150 条样本里错 2 到 3 条属于正常波动。如果准确率只有 33% 左右,基本等于随机猜,说明训练没收敛或者标签编码对不上,后面避坑章节会细说。

提示:PyCharm 里直接右键train.py→ Run 也可以,但工作目录默认是项目根目录,代码里读data/iris.csv用的是相对路径,换到终端跑时要在项目根目录下执行,否则会报FileNotFoundError。

3. 核心代码拆解:一个 batch 在 BP 网络里经历了什么

3.1 网络初始化与参数设计

bp_network.py里网络结构是 4-8-3:输入层 4 个节点对应四个特征,隐藏层 8 个节点,输出层 3 个节点对应三类。权重用 Xavier 方式初始化,偏置置零:

import numpy as np class BPNetwork: def __init__(self, input_size=4, hidden_size=8, output_size=3, lr=0.1): # Xavier 初始化:方差与前后层节点数相关,避免信号在深层衰减 self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1.0 / input_size) self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1.0 / hidden_size) self.b2 = np.zeros((1, output_size)) self.lr = lr def forward(self, X): # 隐藏层用 sigmoid,输出层用 softmax self.z1 = X @ self.W1 + self.b1 self.a1 = 1.0 / (1.0 + np.exp(-self.z1)) self.z2 = self.a1 @ self.W2 + self.b2 exp_z = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True)) # 防溢出 self.a2 = exp_z / np.sum(exp_z, axis=1, keepdims=True) return self.a2

W1形状是(4, 8),W2是(8, 3),b1、b2用行向量是为了配合批量输入做广播。lr默认 0.1,iris 这种小数据集上 0.1 到 0.5 都能收敛,太大容易震荡,太小收敛慢。softmax 里减np.max是数值稳定技巧,不做的话当z2数值偏大时np.exp会溢出成inf,损失直接变nan,这是新手最常踩的坑之一。

3.2 反向传播与权重更新

反向传播的核心是链式法则,输出层用 softmax + 交叉熵,梯度形式很简洁:a2 - y_onehot。隐藏层再乘上W2转置和 sigmoid 导数:

def backward(self, X, y_onehot): m = X.shape[0] # 输出层梯度:softmax + 交叉熵的导数就是预测减真实 dz2 = (self.a2 - y_onehot) / m dW2 = self.a1.T @ dz2 db2 = np.sum(dz2, axis=0, keepdims=True) # 隐藏层梯度:链式回传,sigmoid 导数 = a1 * (1 - a1) da1 = dz2 @ self.W2.T dz1 = da1 * self.a1 * (1 - self.a1) dW1 = X.T @ dz1 db1 = np.sum(dz1, axis=0, keepdims=True) # 梯度下降更新 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1 self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2

m是当前 batch 的样本数,除以m是对 batch 内梯度取平均,保证学习率不随 batch 大小漂移。dz2的形状是(m, 3),dW2是(8, 3),和W2对得上。这里最容易写错的是dz1那行:da1是(m, 8),self.a1 * (1 - self.a1)也是(m, 8),逐元素相乘得到dz1。如果误写成矩阵乘,形状对不上会直接报错;如果形状碰巧对上了但语义错了,训练会不收敛,这种错误最难查。

3.3 训练循环与损失记录

train.py把上面两块串起来,每个 epoch 做一次全量前向、算损失、反向更新,并把损失存下来画曲线:

from bp_network import BPNetwork from utils import load_iris_data, one_hot, normalize X_train, X_test, y_train, y_test = load_iris_data("data/iris.csv") X_train, X_test = normalize(X_train, X_test) y_train_oh = one_hot(y_train, num_classes=3) net = BPNetwork(input_size=4, hidden_size=8, output_size=3, lr=0.1) losses = [] for epoch in range(1000): y_pred = net.forward(X_train) # 交叉熵损失,加 1e-8 防止 log(0) loss = -np.mean(np.sum(y_train_oh * np.log(y_pred + 1e-8), axis=1)) net.backward(X_train, y_train_oh) losses.append(loss) if epoch % 100 == 0: print(f"epoch {epoch}, loss {loss:.4f}")

normalize对训练集和测试集用同一组均值和标准差,这点很关键,分开归一化会导致训练和推理分布不一致。one_hot把标签 0/1/2 转成三列独热向量。1000 个 epoch 对 iris 来说足够,损失通常从 1.1 左右降到 0.1 以下。打印间隔设 100 是为了看趋势,调试时可以改成 10。

3.4 评估与混淆矩阵

evaluate.py加载weights.npz,对测试集前向一次,取 argmax 得到预测类别,再算指标:

import numpy as np from bp_network import BPNetwork from utils import load_iris_data, normalize net = BPNetwork() weights = np.load("outputs/weights.npz") net.W1, net.b1 = weights["W1"], weights["b1"] net.W2, net.b2 = weights["W2"], weights["b2"] X_train, X_test, y_train, y_test = load_iris_data("data/iris.csv") _, X_test = normalize(X_train, X_test) y_pred = np.argmax(net.forward(X_test), axis=1) acc = np.mean(y_pred == y_test) print(f"test accuracy: {acc:.4f}")

注意normalize这里传的是训练集算均值方差、测试集做变换,函数内部要保证只用训练集统计量。如果评估时重新对测试集单独归一化,准确率会虚高或虚低,这是实验报告里经常被忽略的细节。

4. 避坑与排查:训练不收敛时先看这五处

4.1 损失一直是 1.0986 不动

现象:训练几百个 epoch,loss 稳定在 1.0986 附近,准确率 33%。原因:1.0986 正好是ln(3),等于三分类均匀分布下的交叉熵,说明网络输出完全没学到东西。最常见的原因是标签 one-hot 编码和输出层顺序对不上,或者backward里梯度符号写反了。解决:先打印y_pred[0]和y_train_oh[0],确认预测分布不是均匀的;再检查dz2 = (self.a2 - y_onehot) / m是不是写成了y_onehot - self.a2,符号反了梯度会往反方向走。

4.2 损失变成 nan

现象:跑几十个 epoch 后 loss 突然变nan。原因:softmax 里没做减最大值处理,np.exp溢出;或者学习率设得太大,权重爆炸。解决:确认forward里有np.exp(self.z2 - np.max(...))这一行;把lr从 0.5 降到 0.1 甚至 0.05 再试。如果还不行,在backward后打印np.max(np.abs(self.W1)),正常应该在个位数,到几百就是发散了。

4.3 准确率很高但混淆矩阵某一类全错

现象:整体准确率 66%,混淆矩阵显示其中一类全部预测成另一类。原因:数据划分时没打乱,iris.csv按类别排序,如果直接取前 120 条训练、后 30 条测试,测试集可能只含一两个类别。解决:在load_iris_data里先np.random.shuffle再切分,或者用train_test_split的思路手动打乱索引。这个坑在实验报告里很致命,因为结论会完全失真。

4.4 PyCharm 里跑报路径错误

现象:终端能跑,PyCharm 里 Run 报FileNotFoundError: data/iris.csv。原因:PyCharm 的运行配置工作目录可能被设成了src/而不是项目根目录。解决:Run → Edit Configurations → Working directory 改成项目根目录;或者代码里用os.path.dirname(__file__)拼绝对路径,一劳永逸。

4.5 换了数据集后形状对不上

现象:换成自己 CSV 后报matmul维度不匹配。原因:BPNetwork的input_size和output_size写死成 4 和 3,新数据特征数或类别数不同。解决:在train.py里根据数据动态算input_size = X_train.shape[1]、output_size = len(np.unique(y_train)),再传给构造函数。这是二次开发时第一个要改的地方。

5. 二次开发:把这份作业改成你自己的实验

5.1 换激活函数与网络深度

最直接的改造是把隐藏层 sigmoid 换成 ReLU,观察收敛速度变化。改forward里self.a1 = np.maximum(0, self.z1),同时backward里dz1 = da1 * (self.z1 > 0)。ReLU 在 iris 上收敛更快,但要注意学习率别太大,否则容易出现“死亡神经元”。想加深网络,就把W1/b1那套复制成W1/W2/W3,前向反向各多一层,代码结构不变,只是链式法则多乘一次。我一般会先保持 4-8-3 跑通,再逐层加,每加一层看一次损失曲线,避免一次性改太多不知道哪层出的问题。

5.2 加正则化与早停

过拟合在 iris 上不明显,但换成更大的数据集就会暴露。在损失里加 L2 正则:

# 训练循环里,loss 加上正则项 reg_lambda = 0.01 loss = -np.mean(np.sum(y_train_oh * np.log(y_pred + 1e-8), axis=1)) loss += 0.5 * reg_lambda * (np.sum(net.W1 ** 2) + np.sum(net.W2 ** 2)) # backward 里对应梯度也要加 reg_lambda * W

早停更简单:每个 epoch 后在验证集上算一次准确率,连续 20 个 epoch 不提升就 break,同时保存验证集上最好的那组权重,而不是最后一个 epoch 的权重。这个习惯能省掉很多“训练过头”的后悔药。

5.3 实验报告怎么和代码对齐

报告里的损失曲线、混淆矩阵、准确率数字必须和outputs/下的图、终端打印一致。常见做法是在train.py末尾把最终超参(隐藏层节点数、学习率、epoch 数、正则系数)写进一个outputs/config.json,报告里直接引用。这样老师或同事拿到压缩包,跑一遍能复现出同样的数字,可信度完全不一样。我吃过亏:报告里写隐藏层 10 个节点,代码里是 8 个,被追问时只能现场改,很被动。从那以后我每次交实验材料前,都强制把config.json和报告里的参数表对一遍,跑一次干净环境复现。希望这份拆解帮到你,拿到包之后先跑通train.py,再动bp_network.py,顺序别反。

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

返回列表