☰
numpy手写BP神经网络回归:Excel数据预测与可视化实战
2026/10/2 2:37:07 网站建设 项目流程

简介:这份资源面向希望入门神经网络回归预测的Python学习者与数据分析人员,提供一套可直接运行的BP神经网络数据回归预测方案,用于解决房价等连续值预测问题。压缩包共6个文件,约208KB,包含1个Python主脚本、2个xlsx格式的训练与测试数据集、2张结果图及1份说明文档,脚本基于numpy实现网络结构,无需复杂框架即可完成数据加载、训练与预测全流程。运行后可通过matplotlib绘制散点图与对比折线图,直观查看预测值与真实值的拟合效果,便于分析模型表现。数据集采用Excel形式存储,读者可方便地替换为自己的数据,快速迁移到其他回归场景。目前已有2255人学习下载,适合作为课程作业、入门实验或算法验证的参考案例,帮助理解BP网络的前向传播、误差反传与参数更新等核心环节。

1. 从一份 Excel 到一条预测曲线:这套 BP 回归代码到底能干什么

手里有一批 Excel 数据,想预测一个连续值——房价、销量、温度、良品率——但不想一上来就装 PyTorch、配 GPU、调 DataLoader,只想用 numpy 把 BP 神经网络的每一步算清楚。这份dp_neural_network资源就是为这个场景准备的:dp_nn.py单文件实现,boston_housing_train_data.xlsx和boston_housing_test_data.xlsx两个 Excel 直接喂进去,训练完用 matplotlib 画出预测值和真实值的散点图与对比折线图。它适合两类人:一类是刚学完反向传播公式、想找一份能跑通的最小实现对照代码的新手;另一类是手头只有几百到几千条小样本仿真数据、需要快速验证回归可行性的工程师。核心逻辑不依赖深度学习框架,全部用 numpy 矩阵运算手写,改结构、换激活函数、替换数据集都只需要动几个参数。

2. 拆开dp_nn.py:numpy 手写 BP 的四个关键模块

2.1 数据加载与 Excel 读取路径

这份代码用 Excel 而不是 CSV 存数据,好处是列名、表头、多 sheet 都能保留,坏处是读取多一层依赖。常见做法是用 pandas 的read_excel,底层走 openpyxl。先确认环境里有没有这两个包:

pip install numpy pandas openpyxl matplotlib

如果安装 numpy 时卡在installing backend dependencies,多半是网络源的问题,换国内镜像即可:

pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple

读取逻辑一般长这样,注意最后一列通常是标签列,前面是特征列:

import pandas as pd import numpy as np def load_data(train_path, test_path): # 读取 Excel,header=0 表示第一行是列名 train_df = pd.read_excel(train_path, header=0) test_df = pd.read_excel(test_path, header=0) # 最后一列为预测目标,其余为特征 x_train = train_df.iloc[:, :-1].values.astype(np.float64) y_train = train_df.iloc[:, -1].values.astype(np.float64).reshape(-1, 1) x_test = test_df.iloc[:, :-1].values.astype(np.float64) y_test = test_df.iloc[:, -1].values.astype(np.float64).reshape(-1, 1) return x_train, y_train, x_test, y_test

iloc[:, :-1]取所有行、除最后一列外的所有列,reshape(-1, 1)把一维标签变成列向量,方便后面做矩阵减法。astype(np.float64)是血泪经验:Excel 里只要有一格是文本或空值,整列会变成 object 类型,矩阵乘法直接报错。替换成自己的数据集时,只要保证最后一列是连续数值标签、前面列全是数值特征,这个函数不用改。

2.2 网络结构定义与前向传播

BP 回归的网络结构不复杂:输入层节点数等于特征数,一到两个隐藏层,输出层一个节点。dp_nn.py里通常把权重和偏置初始化成小随机数,避免全部置零导致对称失效。

class BPNetwork: def __init__(self, n_input, n_hidden, n_output=1, lr=0.01): # 权重用小随机数初始化,打破对称性 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 forward(self, x): # 隐藏层用 tanh,输出层线性,适合回归 self.z1 = x @ self.w1 + self.b1 self.a1 = np.tanh(self.z1) self.z2 = self.a1 @ self.w2 + self.b2 return self.z2

n_input由数据特征数决定,n_hidden是唯一需要调的容量参数,小样本回归一般 8 到 32 就够。隐藏层激活用tanh而不是sigmoid,是因为tanh输出以零为中心,梯度更稳;输出层必须线性,回归预测的是任意实数,套 sigmoid 会把结果压到 0 到 1 之间,这是新手最常见的翻车点。lr学习率默认 0.01,后面会讲怎么判断它合不合适。

2.3 损失函数与反向传播的矩阵写法

回归任务用均方误差 MSE,反向传播就是对它求导链式展开。numpy 手写的价值在这一段体现得最清楚——每一步梯度的形状都能打印出来核对。

def train_step(self, x, y): # 前向 pred = self.forward(x) # MSE 损失 loss = np.mean((pred - y) ** 2) # 反向:输出层梯度 dz2 = 2 * (pred - y) / y.shape[0] dw2 = self.a1.T @ dz2 db2 = np.sum(dz2, axis=0, keepdims=True) # 隐藏层梯度,tanh 导数 = 1 - a1^2 da1 = dz2 @ self.w2.T dz1 = da1 * (1 - self.a1 ** 2) 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 return loss

dz2里的2 / y.shape[0]是 MSE 求导后对 batch 取平均的系数,漏掉它梯度会大一个数量级,训练直接发散。dw2 = self.a1.T @ dz2是矩阵形式的链式法则,a1形状是(batch, hidden),dz2是(batch, 1),转置相乘得到(hidden, 1),正好和w2形状对齐。1 - self.a1 ** 2是 tanh 的导数,如果换成 ReLU,这里要改成(self.z1 > 0).astype(float)。每一步更新都用-=,别写成=,否则梯度下降变成梯度覆盖。

2.4 训练循环与预测输出

把前向、反向、更新串起来,跑固定轮数,每轮打印损失观察收敛。

def fit(self, x, y, epochs=2000, print_every=200): for i in range(epochs): loss = self.train_step(x, y) if i % print_every == 0: print(f"epoch {i}, loss {loss:.6f}") def predict(self, x): return self.forward(x)

epochs设 2000 是这类小样本的常见量级,print_every用来观察损失是否单调下降。如果损失前几百轮就卡住不动,多半是学习率太小或数据没归一化;如果损失变成 nan,多半是学习率太大。预测阶段直接调forward,不做任何额外处理,因为输出层已经是线性的。

2.5 结果可视化:散点图与对比折线图

资源里Figure_1.png和Figure_2.png就是这两张图。散点图看整体拟合趋势,折线图看逐样本误差。

import matplotlib.pyplot as plt def plot_result(y_true, y_pred): # 散点图:横轴真实值,纵轴预测值,越贴近对角线越准 plt.figure() plt.scatter(y_true, y_pred, s=10) lim = [min(y_true.min(), y_pred.min()), max(y_true.max(), y_pred.max())] plt.plot(lim, lim, 'r--') # 理想对角线 plt.xlabel('True') plt.ylabel('Pred') plt.savefig('Figure_1.png') # 折线图:逐样本对比 plt.figure() plt.plot(y_true, label='True') plt.plot(y_pred, label='Pred') plt.legend() plt.savefig('Figure_2.png')

散点图里点越靠近红色虚线说明预测越准,如果点分布成一条水平带,说明网络没学到东西,输出接近常数。折线图适合看测试集上哪些样本偏差大,通常极端值附近误差最大,这是回归任务的通病,不是代码 bug。

3. 从零跑通:环境配置、训练与替换自己的 Excel

3.1 环境准备与 numpy 版本匹配

这份代码只依赖 numpy、pandas、openpyxl、matplotlib 四个包。Python 3.8 到 3.11 都能跑,numpy 建议 1.21 以上。如果遇到ModuleNotFoundError: No module named 'numpy',说明当前解释器没装,先确认用的是哪个 Python:

python -c "import sys; print(sys.executable)"

输出的路径要和 pip 安装的目标一致。用 VS Code 的话,右下角选解释器,别让编辑器用一个环境、终端用另一个,这是新手最常踩的环境错位坑。numpy 版本不匹配的典型报错是A module that was compiled using NumPy 1.x cannot be run in NumPy 2.x,解决办法是锁版本:

pip install "numpy<2.0"

3.2 运行dp_nn.py的完整流程

把资源解压后,目录结构是dp_nn.py加两个 xlsx 加 README。在目录下执行:

python dp_nn.py

脚本内部流程是:加载两个 Excel → 初始化网络 → 训练 2000 轮 → 在测试集上预测 → 画两张图。终端会打印每 200 轮的损失,正常情况损失从大逐渐变小并趋于平稳。跑完当前目录会生成Figure_1.png和Figure_2.png,打开看拟合效果。如果脚本里写死了文件名,换数据集时要么改代码里的路径,要么把自己的文件改成同名。

3.3 替换成自己的数据集要改哪几处

替换数据集是这份资源最实用的地方,但有几个硬约束。第一,Excel 最后一列必须是数值标签,前面列全是数值特征,中间不能有文本列。第二,训练集和测试集列数必须一致,列顺序也要一致。第三,特征量纲差异大时要做归一化,否则梯度会被大量纲特征主导。

# 在 load_data 之后加一段归一化 def normalize(x_train, x_test): mu = x_train.mean(axis=0) sigma = x_train.std(axis=0) sigma[sigma == 0] = 1 # 防止除零 return (x_train - mu) / sigma, (x_test - mu) / sigma

注意归一化参数只能用训练集的均值和标准差,再套用到测试集,不能各算各的,否则测试集信息泄漏,评估结果虚高。标签列如果数值范围很大(比如房价几十万),也建议归一化,预测完再反归一化回来。

3.4 关键参数对照表

参数位置典型值作用与调整方向
n_hidden网络初始化8~32隐藏层节点数,太小欠拟合,太大过拟合
lr网络初始化0.001~0.05学习率,损失震荡就调小,收敛慢就调大
epochsfit 调用1000~5000训练轮数,看损失曲线决定
print_everyfit 调用100~500打印间隔,只影响观察不影响训练

这张表里的范围是小样本回归的经验值,不是定律。特征多、样本多可以适当加大n_hidden,但几百条数据配几百个隐藏节点基本必过拟合。

4. 避坑与排查:五个真实翻车现场

4.1 损失变成 nan 或 inf

现象:训练几轮后终端打印loss nan,后续全是 nan。原因:学习率过大,梯度更新一步跨太远,权重爆炸;或者数据没归一化,大量纲特征让梯度数量级失控。解决:先把lr降到 0.001 试,再检查数据是否归一化,必要时对梯度做裁剪,把dw限制在[-1, 1]区间。

4.2 损失不下降,输出接近常数

现象:损失从第一轮到最后几乎不变,散点图是一条水平带。原因:权重初始化太小加上学习率太小,或者隐藏层激活全落在饱和区。解决:把权重初始化从* 0.01改成* 0.1或使用 Xavier 初始化,学习率调到 0.01 以上,确认隐藏层用的是 tanh 而不是已经饱和的 sigmoid。

4.3 Excel 读取报类型错误

现象:could not convert string to float或矩阵乘法报object类型。原因:Excel 某列混入了文本、空格或空单元格,pandas 推断成 object。解决:打开 Excel 检查有没有空行、合并单元格、单位后缀(如“100万”),用pd.to_numeric(df[col], errors='coerce')强制转数值并检查 NaN 数量。

4.4 训练集准、测试集差

现象:训练集散点贴着对角线,测试集散点散开。原因:过拟合,隐藏层节点太多或训练轮数太多;也可能是归一化用了全量数据导致信息泄漏。解决:减少n_hidden,减少epochs,归一化严格只用训练集统计量,样本太少时可以加 L2 正则,在损失里加0.001 * (w1**2).sum()。

4.5 图形不显示或中文乱码

现象:跑完没有图弹出,或标题中文变方块。原因:matplotlib 后端在无显示环境默认不弹窗,中文字体没配。解决:确认脚本里有plt.savefig而不是只plt.show,无显示环境直接看保存的 png;中文乱码加plt.rcParams['font.sans-serif'] = ['SimHei'],或者干脆用英文标签。

5. 进阶:用损失曲线和残差图判断模型到底学没学会

跑通只是第一步,判断这份 BP 实现有没有真正学到规律,靠两张图不够,我一般会补两条曲线。第一条是损失随轮数的变化曲线,把每轮 loss 存进列表再画出来:

losses = [] for i in range(epochs): loss = net.train_step(x_train, y_train) losses.append(loss) plt.figure() plt.plot(losses) plt.xlabel('epoch') plt.ylabel('loss') plt.yscale('log') # 对数轴更容易看出后期收敛 plt.savefig('loss_curve.png')

健康的损失曲线是前期快速下降、后期缓慢趋平。如果曲线一直抖,学习率偏大;如果几千轮还在缓慢下降,轮数不够或学习率偏小;如果训练损失降但测试损失升,过拟合。对数纵轴能把后期的小变化放大,比线性轴好用。

第二条是残差图,残差等于真实值减预测值:

residual = y_test.flatten() - y_pred.flatten() plt.figure() plt.scatter(y_pred.flatten(), residual, s=10) plt.axhline(0, color='r', linestyle='--') plt.xlabel('Pred') plt.ylabel('Residual') plt.savefig('residual.png')

理想残差应该随机分布在零线两侧,没有明显形状。如果残差呈喇叭形,说明大值预测误差更大,标签可能需要做对数变换;如果残差呈弯曲趋势,说明线性输出层加单隐藏层的容量不够,得加隐藏层或换更复杂的结构。这两张图配合原来的散点图和折线图,四个视角基本能定位大部分问题。

还有一个容易被忽略的点:随机种子。np.random.randn每次运行结果不同,同一份数据两次跑出来的损失曲线可能差不少。做对比实验前先固定种子:

np.random.seed(42)

固定之后,改一个参数看一次结果,才知道到底是哪个改动起了作用。不固定种子就调参,等于在噪声里找规律,纯玄学。

从那以后我每次拿到这种手写 BP 的小项目,都强制先固定种子、先画损失曲线、先看残差分布,再谈调参。希望帮到你。

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

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

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

立即咨询