BP神经网络回归预测:纯numpy实现与代码拆解
简介这份资源是一套基于BP神经网络的数据回归预测Python代码面向希望掌握回归预测实现方法的Python学习者与数据分析人员。代码使用NumPy自行搭建BP网络以波士顿房价数据集为示例完整覆盖数据读取、模型训练、结果对比与可视化环节。数据文件为Excel格式可直接替换成自己的数据集运行dp_nn.py脚本即可完成预测并通过matplotlib输出真实值与预测值的散点图及折线图。资源压缩包共6个文件包括1个Python脚本、2个Excel数据文件、2张结果图片和1份说明文档整体大小约208KB结构清晰便于直接使用。已有2255人学习下载适合希望通过简洁工程案例理解BP网络原理并快速迁移到自定义回归场景的读者。获取后可获得可直接运行的代码、图表化评价方式与数据集组织范例有效降低入门门槛。1. 基于BP神经网络的数据回归预测一份能跑通的numpy实现值得你花半小时拆一遍把一份Excel表格丢给BP神经网络做数据回归预测很多人第一反应是上TensorFlow或PyTorch但这套基于BP神经网络的数据回归预测Python代码用纯numpy就做到了——它只有一个核心脚本dp_nn.py加载Boston房价的训练和测试Excel数据训练一个隐藏层BP网络迭代完成后直接画出预测值与真实值的散点对比图和折线对比图。代码没套框架前向传播、反向传播、梯度更新全部裸写适合不想被框架黑匣子困住的人。新手可以用它建立回归预测的完整直觉熟手则能快速替换成自己的Excel数据集做基线验证。整个包从数据到图一步到位拿到就能跑下面我把每个环节拆开讲。2. BP网络的前向与反向传播13个特征如何被压缩成一个房价预测值2.1 三层网络结构输入层、隐藏层、输出层各自承担什么Boston房价数据集的标准形态是前13列特征最后一列是目标值MEDV。13个特征包括犯罪率、房间数、房龄、到高速公路距离、税收等它们单位不同、量级差异大所以进网络前必须先做归一化。这份代码的实现里输入层节点数就是13输出层节点数是1隐藏层节点数通常取10左右。三层结构是BP网络最经典的形态隐藏层负责把线性不可分的特征空间做非线性映射。隐藏层的激活函数一般用sigmoid或tanh。sigmoid输出范围是(0,1)和归一化后的目标值在同一量级tanh输出范围是(-1,1)收敛通常更快。这份资源没有在说明里指名激活函数我按最常规的写法推断用的是sigmoid你替换数据时如果发现收敛慢可以改成tanh对比一下。输出层在回归任务里通常不加激活函数或直接用线性输出这样预测值才能突破sigmoid的(0,1)区间限制。2.2 反向传播的四个核心公式权重更新到底在更新什么很多入门者卡在反向传播其实核心就四个公式。设输入层到隐藏层的权重矩阵为W1隐藏层到输出层的权重矩阵为W2激活函数为f。第一计算输出层误差delta_out (y_pred - y_true) 乘以激活函数在输出层的导数。第二计算隐藏层误差delta_hidden W2转置乘以delta_out再逐元素乘以隐藏层激活函数的导数。第三计算梯度W2的梯度是delta_out乘以隐藏层输出W1的梯度是delta_hidden乘以输入。第四更新权重W W - learning_rate乘以梯度。# 核心反向传播逻辑示意 # 这段是numpy实现的BP网络最关键的权重更新部分 delta_out (y_pred - y_batch) * sigmoid_derivative(z_out) # 输出层误差 delta_hidden np.dot(delta_out, W2.T) * sigmoid_derivative(z_hidden) # 隐藏层误差 W2 - learning_rate * np.dot(z_hidden.T, delta_out) # 更新隐藏层到输出层权重 W1 - learning_rate * np.dot(X_batch.T, delta_hidden) # 更新输入层到隐藏层权重这里有个容易被忽略的细节sigmoid的导数是sigmoid(x)乘以(1 - sigmoid(x))当网络输出接近0或1时导数趋近于0梯度近乎消失权重更新速度骤降。这就是为什么数据归一化在BP网络里不是可选项而是必选项——特征不归一化sigmoid的输入很容易落在饱和区Loss卡住不动是常有的事。2.3 训练循环里的关键参数学习率、迭代次数、随机种子这份代码的训练过程是把训练集按批次喂给网络或者全量训练两种方式在numpy实现里都很常见。学习率一般设在0.01到0.1之间太大Loss震荡甚至爆炸太小收敛慢到让人怀疑代码写错了。迭代次数通常设几千次因为BP网络每轮迭代只做一次完整的前后向传播不像现代框架里有batch和epoch的区分。随机种子也是容易被忽略的参数。权重初始化用np.random.randn或np.random.uniform如果不固定种子每次运行时初始权重不同最终Loss曲线和训练效果会略有差异。我建议你第一次跑通后记住当前seed后面对比参数效果时固定它否则不同次运行之间的噪声会让你误判调参方向。这个项目里如果没显式设置seed你可以在初始化权重前加一行np.random.seed(42)让结果可复现。3. 环境搭建与Excel数据准备numpy、openpyxl与文件列序对齐3.1 环境配置与numpy版本核对这份资源的依赖非常轻核心就是numpy和matplotlib外加pandas用于读取Excel。安装命令如下你最好在运行前先核对一下python和numpy的版本网上常见的“numpy版本不匹配”报错多半是多个python环境串了。# 在命令行执行环境安装 pip install numpy pandas matplotlib openpyxl # 核对版本确认当前解释器用的是哪个numpy python -c import numpy; print(numpy.__version__) # 如果环境中已经有旧版numpy需要先升级 pip install --upgrade numpy安装完成后建议跑第一行命令的验证逻辑确认不是pip装到了一个解释器、运行时用的是另一个解释器。这种环境串台的坑在Windows机器上特别多我在第5章会专门讲。openpyxl是pandas读取xlsx文件的后端引擎如果只装了pandas没装openpyxlpd.read_excel会直接抛ImportError。3.2 dp_nn.py的启动方式与迭代轮数调整运行方式很简单在项目根目录下执行python dp_nn.py脚本会按顺序完成数据加载、归一化、网络初始化、迭代训练、绘图和保存。第一次运行时建议打开dp_nn.py找到迭代次数的变量比如epochs 5000先改成500跑一遍确认数据读入和网络形状都对再改回完整轮数。这样做的好处是如果训练代码里有维度错误500轮内就会暴露不用等完整训练跑完才发现。# 在项目根目录执行观察是否成功打印数据形状和初始Loss python dp_nn.py正常情况控制台会输出类似“训练集形状: (404, 13)测试集形状: (102, 13)”的信息注意Boston房价标准数据集是404个训练样本和102个测试样本如果数字对不上检查Excel文件是否被改动过。3.3 替换成自己的Excel数据集列顺序与最后一列的坑替换数据集是这个项目最有价值的地方。Excel文件必须是数值型矩阵最后一列是目标值前面所有列是特征。如果你自己的数据有字符串列、缺失值或单位符号得先在Excel里清洗干净。常见做法是用pd.read_excel读取后在numpy转换前用df.select_dtypes(include[np.number])丢掉非数值列。import pandas as pd import numpy as np # 读取自己的Excel文件 df pd.read_excel(my_data.xlsx, engineopenpyxl) # 只保留数值列避免字符串列破坏矩阵转换 df df.select_dtypes(include[np.number]) # 最后一列是目标值前面所有列是特征 X df.iloc[:, :-1].values y df.iloc[:, -1].values.reshape(-1, 1) print(特征形状:, X.shape, 目标值形状:, y.shape)这段代码里的iloc[:, :-1]表示取所有行、去掉最后一列的所有列iloc[:, -1]单独取出最后一列。目标值reshape成(-1, 1)是因为numpy的矩阵运算里一维数组和二维列向量行为不同BP网络里隐藏层输出是二维矩阵目标值如果不是二维列向量y_pred - y_true会触发广播机制算出一个形状错误的结果这是新手最容易踩的维度坑之一。4. dp_nn.py核心代码逐段拆解从数据加载到Loss曲线落地4.1 数据加载与归一化训练集和测试集共用同一组min和max数据加载之后的第一件事是归一化。这里有一个非常关键的工程习惯归一化的min和max必须从训练集上计算然后拿这组参数去变换测试集。很多人图省事把训练集和测试集拼在一起求min和max这会让测试数据的信息提前泄漏进训练过程导致测试结果虚高。# 归一化函数将数据缩放到[0, 1]区间 def normalize(X, min_valsNone, max_valsNone): if min_vals is None or max_vals is None: min_vals X.min(axis0) # 每个特征列的最小值 max_vals X.max(axis0) # 每个特征列的最大值 X_norm (X - min_vals) / (max_vals - min_vals 1e-8) # 加极小值防除零 return X_norm, min_vals, max_vals # 训练集上计算min和max测试集沿用同一组 X_train_norm, min_vals, max_vals normalize(X_train) X_test_norm, _, _ normalize(X_test, min_vals, max_vals)axis0的意思是沿着行的方向求每一列的极值返回形状是(13,)的数组和特征列数对齐。分母加上1e-8是为了防止某个特征在数据里恒定不变时出现除零。预测完成后如果想还原成真实房价数值需要把预测结果乘以(max_vals[-1] - min_vals[-1])再加上min_vals[-1]这个还原逻辑在第6章展开。4.2 网络初始化与前向传播用均匀分布控制权重尺度权重初始化的尺度直接影响收敛速度。常见做法是用np.random.uniform在(-0.5, 0.5)之间取随机数或者用标准正态分布乘以一个较小的系数。大权重的后果是神经元输出容易落入sigmoid饱和区梯度消失从小权重起步是一种性价比最高的初始化策略。# 网络参数初始化 input_size X_train_norm.shape[1] # 特征数量 hidden_size 10 # 隐藏层神经元数量 output_size 1 # 输出层神经元数量 np.random.seed(42) # 固定随机种子保证结果可复现 W1 np.random.uniform(-0.5, 0.5, (input_size, hidden_size)) b1 np.zeros((1, hidden_size)) W2 np.random.uniform(-0.5, 0.5, (hidden_size, output_size)) b2 np.zeros((1, output_size))偏置b1和b2初始化为零是安全做法权重用均匀分布是为了打破对称性。如果所有权重初始化为同一个数值同一层的神经元会学到完全相同的特征网络退化成只有一个神经元在干活。前向传播就是把输入逐层计算到输出隐藏层经过sigmoid激活输出层直接线性输出这样预测值就不受sigmoid的值域限制。4.3 损失计算、反向传播与参数更新MSE的意义与梯度裁剪回归任务的损失函数用均方误差MSE含义是预测值与真实值差值的平方的平均。为什么用平方而不是绝对值因为平方损失对大误差更敏感梯度方向能更快地把大偏差拉回来。MSE的导数刚好是(y_pred - y_true)乘以2除以样本数这个形式和反向传播的误差项天然衔接。# 训练循环内的核心逻辑 for epoch in range(epochs): # 前向传播 z1 np.dot(X_train_norm, W1) b1 # 隐藏层线性组合 a1 sigmoid(z1) # 隐藏层激活 z2 np.dot(a1, W2) b2 # 输出层线性组合 y_pred z2 # 回归任务输出层不加激活 # 计算MSE损失 loss np.mean((y_pred - y_train) ** 2) # 反向传播推导过程对应第2章的四个公式 delta_out (y_pred - y_train) * 1 # 线性输出的导数为1 delta_hidden np.dot(delta_out, W2.T) * sigmoid_derivative(a1) # 梯度计算与权重更新 grad_W2 np.dot(a1.T, delta_out) / n_samples grad_W1 np.dot(X_train_norm.T, delta_hidden) / n_samples W2 - learning_rate * grad_W2 W1 - learning_rate * grad_W1 b2 - learning_rate * np.mean(delta_out, axis0, keepdimsTrue) b1 - learning_rate * np.mean(delta_hidden, axis0, keepdimsTrue)注意delta_out里乘的“1”就是线性输出的导数很多教程写回归任务时输出层没有激活函数这个导数为1的细节容易被忽略。keepdimsTrue是为了保持偏置的维度是(1, hidden_size)而不是被压缩成一维数组。梯度更新除以n_samples是为了让损失和梯度的尺度不随样本量变化这样当你从404个样本的数据换成4000个样本的数据时学习率不用重新调。4.4 matplotlib绘图散点图看趋势折线图看细节代码里的两张图Figure_2.png和Figure_1.png分别对应真实值与预测值的散点对比和按样本序号排列的折线对比。散点图用来判断整体预测质量理想情况是散点沿对角线分布折线图用来观察局部波动看预测值是否紧跟真实值的起伏。import matplotlib.pyplot as plt # 散点图观察预测值与真实值是否沿对角线分布 plt.figure(figsize(8, 6)) plt.scatter(y_test, y_pred, alpha0.7, edgecolorsk, linewidths0.5) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) plt.xlabel(真实值) plt.ylabel(预测值) plt.title(测试集真实值 vs 预测值) # 折线图按样本序号对比预测与真实的走势 plt.figure(figsize(12, 5)) plt.plot(y_test, label真实值, markero, markersize3) plt.plot(y_pred, label预测值, markers, markersize3) plt.xlabel(样本序号) plt.ylabel(房价) plt.legend()alpha0.7控制散点的透明度样本重叠多时能看出密度差异。红色虚线是理想的完全预测线散点离它越近说明模型越准。折线图的markersize设小一些是经验之谈测试集100多个样本时大标记会让线条变成黑疙瘩反而看不清走势。如果样本上千折线图建议只画前50个样本否则横坐标标签全部挤成一团这也是热词里“python画图横坐标太密集”的高频来源。5. 五个高频问题排查预测值趋平、Loss不降、图表异常的翻车记录5.1 现象训练完预测值全都趋近于同一个常数现象测试集预测结果画出来是接近水平的一条线无论输入特征怎么变化输出都在某个值附近小幅波动。原因最常见的是学习率过大导致权重在最优解附近来回震荡最终收敛到一个糟糕的局部点其次是特征没有归一化或归一化参数用错某个特征数值过大它的梯度过大把其他特征的权重压没了。解决先把学习率调低一个数量级比如从0.1改成0.01再检查训练集和测试集是否用了同一组min和max归一化参数代码里如果测试集单独调用了normalize函数重新计算min和max赶紧改成沿用训练集参数。5.2 现象Loss一直不下降甚至变大现象训练前几轮Loss从某个值开始迭代几百轮后还在原地踏步偶尔还会突然跳到更大值。原因权重初始值过大所有sigmoid神经元都落在饱和区梯度消失。另一种可能是数据里有缺失值被pandas读成了NaNNaN参与矩阵运算后Loss直接变成nan。解决初始化权重前打印一下X_train_norm是否包含np.isnan和np.isinf有就回到Excel清洗。权重初始化改成np.random.randn(input_size, hidden_size) * 0.1把标准差从0.5压到0.1sigmoid的输入范围收窄梯度消失问题能明显缓解。5.3 现象替换数据集后报错“shape mismatch”现象把Boston数据换成自己的Excel后运行报错ValueError: shapes (100,20) and (13,10) not aligned。原因这是维度不匹配dp_nn.py里写死的input_size 13和hidden_size 10你的数据特征数是20和W1的第一维对不上。解决找到input_size X_train_norm.shape[1]这行确认代码是用数据的实际特征数动态取值的而不是写死13。如果是写死的改成动态获取网络初始化部分的代码顺序一定要在数据加载之后。5.4 现象matplotlib画出来的折线图横坐标标签全部糊在一起现象测试集有1000个样本时X轴刻度从0到999每个刻度标签都叠在一起看起来就像一条黑线。原因matplotlib默认把所有刻度标签都画出来样本多时不自适应稀疏化。这个在热词里是高频问题很多人在自己的数据上遇到过不算代码bug。解决画图前用plt.xticks(np.arange(0, len(y_test), step50))强制每50个样本显示一个刻度或者更简单只画前100个样本plt.plot(y_test[:100])。对于回归预测的结果观察前100个样本的走势已经完全够用。5.5 现象pip安装numpy后运行仍然提示ModuleNotFoundError现象明明执行了pip install numpy运行dp_nn.py还是报ModuleNotFoundError: No module named numpy。原因本机装了多个Python环境pip安装到了A环境运行脚本用的是B环境。Windows上更常见的是命令行里输入python打开的是Microsoft Store的Python和刚才pip对应的解释器不是同一个。解决在项目根目录执行python -m pip install numpy pandas matplotlib openpyxl用python -m pip能保证安装到当前python解释器对应的环境里。装完再用python -c import numpy; print(numpy.__file__)确认模块路径看到路径和你运行脚本的解释器一致才算彻底解决。6. 把这份代码用到自己的数据集上隐藏层神经元、归一化边界与早停判断6.1 隐藏层神经元数量从特征数的一半开始试隐藏层神经元数量是BP网络里最需要调的参数它控制模型的拟合能力。神经元太少网络对非线性关系的表达能力不足预测值会偏“钝”太多则容易记住训练集的噪声测试集效果反而变差这就是过拟合。我一般会从特征数的一半开始试比如20个特征就先用10个隐藏神经元跑一遍记录测试集MSE再分别试8、12、16个对比哪个MSE最低。这份代码里如果写死了hidden_size 10你把它改成变量就能批量对比了。注意神经元数量变化时耗时和训练轮数也要相应调整。6.2 归一化边界与预测值还原归一化到[0,1]是最稳妥的选择因为它和目标值的sigmoid输出范围一致。但还原预测值这步很多人会漏掉模型输出的0.6和0.8之间的差距对应的真实房价差距是多少必须用训练集目标列的最大值和最小值换回去。# 还原预测值为真实量纲 y_pred_real y_pred * (max_vals[-1] - min_vals[-1]) min_vals[-1] y_test_real y_test * (max_vals[-1] - min_vals[-1]) min_vals[-1]这两行放在绘图之前执行画出来的散点图坐标轴就是真实房价而不是归一化后的抽象数字。max_vals[-1]和min_vals[-1]是第4章归一化时保存的训练集目标列的极值如果你在替换数据集时没有保存返回值推荐回到4.1节的代码实现返回值里的min_vals和max_vals一定要接住这是还原阶段唯一的钥匙。6.3 从训练曲线判断早停时机没有验证集时很多人傻傻跑满迭代次数在等Loss降到最低其实在训练脚本里加一个每100轮打印Loss的语句就够了if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.6f})观察Loss下降曲线前500轮如果已经从初始值降了80%后面继续跑收益往往很小甚至可能出现Loss先降后升的反常情况那是学习率过大导致权重在最优解附近震荡此时与其增加迭代次数不如调低学习率重新跑。从那以后我每次替换数据集都强制走一遍固定seed、检查归一化参数、打印前500轮Loss、预测值还原对比这条流程这套流程帮我挡下了无数个“看起来训得很好其实是数据泄漏”的假象。希望帮到你。本文还有配套的精品资源点击获取