ARTICLE DETAIL

资讯详情

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

BP神经网络实现大学生消费预测:从特征工程到SHAP解释

BP神经网络实现大学生消费预测:从特征工程到SHAP解释 简介这份资源面向本科及本科以上、具备一定MATLAB基础的学生与研究人员提供一套基于BP神经网络的大学生消费预测完整实现方案可用于课程设计、数学建模或相关课题的扩展研究。压缩包共4个文件包含2个m脚本、1个xls数据表和1份doc实验报告整体约323KB脚本负责网络构建与训练数据表提供城乡大学生月平均消费样本文档则记录实验过程与结果分析。资源代码完整、注释清晰便于读者理解BP网络的输入输出设计、训练参数设置与预测流程也能在此基础上替换数据或调整网络结构以适配新的研究场景。目前已有65人学习下载适合希望快速上手神经网络预测实践、对照实验报告复现结果并做二次开发的读者参考使用。1. 大学生消费预测这件事为什么用 BP 神经网络而不是线性回归月初生活费到账前两周奶茶外卖不断后两周开始精打细算——这个场景几乎每个在校生都经历过。把这种行为量化成可预测的模型就是「大学生消费预测」要解决的问题给定一个学生的年级、月生活费、性别、是否兼职、月均网购次数等特征预测他下个月的消费总额落在哪个区间。听起来像回归任务很多人第一反应是拿线性回归或决策树跑一遍但真实数据里特征和消费额之间往往不是直线关系——生活费 1500 和 2000 的学生消费差距可能远小于 2000 和 2500 之间的差距而且「是否兼职」和「生活费」之间存在交互效应。BP 神经网络的价值就在于它能自动拟合这种非线性映射不需要你手动构造交叉特征。这篇笔记面向的是想拿一份完整代码和数据、在本地跑通一个可解释的消费预测模型的学生或初级工程师我会把网络结构怎么定、数据怎么洗、参数怎么调、哪里容易翻车讲清楚代码可以直接抄。2. 把消费预测拆成 BP 能吃的输入输出特征工程与网络结构设计2.1 原始问卷数据长什么样哪些字段能进模型假设你手上是一份校园问卷导出的 CSV常见字段包括学号无意义删、性别、年级、专业类别、月生活费、是否兼职、兼职月收入、月均网购次数、月均聚餐次数、是否有恋爱对象、月消费总额。这里有几个坑要先处理学号是唯一标识进模型只会让网络记住样本必须删专业类别是文本需要做独热编码「是否有恋爱对象」这种二值字段直接用 0/1 即可。目标列是「月消费总额」这是一个连续值所以输出层用线性激活损失函数用均方误差 MSE。如果你想把消费分成「低/中/高」三档做分类那输出层换成 softmax损失换成交叉熵——但分类会丢失金额信息预测意义不大我一般直接做回归。特征数量决定了输入层神经元个数。假设清洗后剩下 12 个特征输入层就是 12 个节点。这里有个血泪经验不要一上来就把所有字段塞进去先用相关性分析筛一遍。月生活费和月消费总额的皮尔逊系数通常能到 0.7 以上而「性别」可能只有 0.05这种弱相关特征留着只会增加噪声。2.2 隐藏层到底设几层、每层几个节点BP 神经网络结构图里最常见的是「输入层-隐藏层-输出层」三层结构。对于这种几百到几千条样本的表格数据一层隐藏层就够了两层以上容易过拟合。隐藏层节点数的经验公式是sqrt(输入层节点数 输出层节点数) a其中 a 取 1 到 10 之间的常数。12 个输入、1 个输出算下来大概 5 到 14 个节点。我一般先试 8 和 12 两个值看验证集损失哪个更低。激活函数方面隐藏层用 ReLU 比 Sigmoid 收敛快而且能缓解梯度消失。但 ReLU 有个问题学习率设大了会出现「神经元死亡」输出恒为 0。如果你发现训练几轮后损失不降了先检查是不是这个问题把学习率降到 0.001 试试。下面是用 PyTorch 定义网络结构的代码import torch import torch.nn as nn class ConsumptionNet(nn.Module): def __init__(self, input_dim): super(ConsumptionNet, self).__init__() # 输入层 - 隐藏层112维输入10个神经元 self.fc1 nn.Linear(input_dim, 10) self.relu nn.ReLU() # 隐藏层1 - 输出层输出1个连续值 self.fc2 nn.Linear(10, 1) def forward(self, x): x self.fc1(x) x self.relu(x) x self.fc2(x) return x # 假设输入特征为12维 model ConsumptionNet(input_dim12) print(model)这段代码定义了一个 12-10-1 的三层 BP 网络。nn.Linear(input_dim, 10)是全连接层权重矩阵形状是(10, 12)偏置是 10 维向量。forward里先线性变换再 ReLU 激活最后输出层不加激活函数因为回归任务需要输出原始数值。参数说明input_dim必须和你的特征列数一致多了少了都会报维度错误隐藏层节点数 10 是我在几百条样本上的常用起点你可以改成 8 或 12 对比。2.3 数据标准化不做这一步MSE 会大到没法看月生活费是 1000 到 3000 的量级而「是否兼职」是 0 或 1如果不做标准化网络会认为生活费的重要性远大于兼职梯度更新会被大数值特征主导。常见做法是对连续特征做 Z-score 标准化对二值特征保持不变。注意标准化参数均值和标准差必须用训练集计算然后应用到验证集和测试集否则会造成数据泄露。from sklearn.preprocessing import StandardScaler import pandas as pd # 读取数据 df pd.read_csv(student_consumption.csv) # 连续特征列 cont_cols [月生活费, 兼职月收入, 月均网购次数, 月均聚餐次数] # 二值特征列 bin_cols [性别, 是否兼职, 是否有恋爱对象] # 目标列 target_col 月消费总额 # 划分训练集和测试集8:2 from sklearn.model_selection import train_test_split train_df, test_df train_test_split(df, test_size0.2, random_state42) # 只在训练集上拟合标准化器 scaler StandardScaler() train_df[cont_cols] scaler.fit_transform(train_df[cont_cols]) test_df[cont_cols] scaler.transform(test_df[cont_cols]) # 组装特征矩阵和标签 X_train train_df[cont_cols bin_cols].values y_train train_df[target_col].values.reshape(-1, 1) X_test test_df[cont_cols bin_cols].values y_test test_df[target_col].values.reshape(-1, 1)这段代码的关键点是fit_transform只用在训练集测试集用transform。如果你偷懒对整个数据集做fit_transform测试集的均值和方差信息就泄露到了训练过程验证损失会虚低上线后翻车。random_state42保证每次划分结果一致方便复现。reshape(-1, 1)是把标签从一维数组变成列向量因为 PyTorch 的 MSE 损失要求预测值和真实值形状一致。3. 训练循环与参数调优学习率、批次大小、早停怎么设3.1 用 DataLoader 喂数据批次大小从 32 开始试PyTorch 训练的标准流程是把 numpy 数组转成 Tensor再包成 Dataset 和 DataLoader。批次大小batch_size影响梯度估计的噪声太小如 8会导致损失震荡太大如 256会陷入局部极小。对于几百条样本的数据集32 或 64 是比较稳的选择。from torch.utils.data import TensorDataset, DataLoader # 转为 Tensor X_train_t torch.FloatTensor(X_train) y_train_t torch.FloatTensor(y_train) X_test_t torch.FloatTensor(X_test) y_test_t torch.FloatTensor(y_test) # 构建 DataLoader train_dataset TensorDataset(X_train_t, y_train_t) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) # 定义损失函数和优化器 criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001)shuffleTrue在每个 epoch 打乱样本顺序防止网络学到样本排列的虚假规律。优化器用 Adam 而不是 SGD因为 Adam 对学习率不敏感初始 0.001 通常能直接工作。如果你用 SGD学习率要设到 0.01 左右而且需要手动加动量。3.2 训练循环里必须记录验证损失否则你不知道什么时候该停很多人只盯着训练损失下降就以为模型在变好实际上训练损失一直降、验证损失先降后升就是过拟合的典型信号。正确做法是每个 epoch 结束后在验证集上算一次损失当验证损失连续 10 个 epoch 不下降就早停。import numpy as np epochs 500 patience 10 # 连续10轮验证损失不降就停 best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() train_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() optimizer.step() train_loss loss.item() * batch_x.size(0) train_loss / len(train_dataset) # 验证 model.eval() with torch.no_grad(): val_pred model(X_test_t) val_loss criterion(val_pred, y_test_t).item() if val_loss best_val_loss: best_val_loss val_loss wait 0 torch.save(model.state_dict(), best_model.pth) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break if epoch % 50 0: print(fEpoch {epoch}, Train Loss: {train_loss:.2f}, Val Loss: {val_loss:.2f})optimizer.zero_grad()必须在每个批次开头调用否则梯度会累加。loss.item()取出标量值batch_x.size(0)是当前批次的样本数用来加权平均。torch.save保存验证损失最低的模型参数而不是最后一个 epoch 的——这是后悔药防止训练后期过拟合导致最终模型变差。早停的patience10是我在几百条样本上的常用值样本多时可以设到 20。3.3 学习率调优从 0.01 到 0.0001 扫一遍学习率是 BP 神经网络里最玄学的参数。设大了损失震荡甚至发散设小了收敛慢到怀疑人生。我的做法是先跑一个学习率扫描用 0.01、0.005、0.001、0.0005、0.0001 各训 100 个 epoch画验证损失曲线选收敛最快且最终损失最低的那个。对于这个消费预测任务0.001 通常是最优区间。如果你发现损失在某个值附近来回跳可以加一个学习率衰减策略比如每 100 个 epoch 乘以 0.5。PyTorch 里用torch.optim.lr_scheduler.StepLR就能实现。4. 避坑与排查消费预测模型训练中最容易翻车的 5 个地方4.1 现象损失直接变成 NaN训练几轮就崩原因学习率太大或者输入特征没有标准化导致梯度爆炸。消费金额是几百到几千的量级如果不做标准化直接进网络第一层线性变换的输出会非常大ReLU 之后梯度直接溢出。解决先确认所有连续特征都做了 Z-score 标准化然后把学习率降到 0.0001 再试。如果还是 NaN检查标签里有没有异常值比如月消费 99999 这种录入错误把它删掉或截断到合理范围。4.2 现象训练损失降得很低但测试集预测值全部偏向均值原因模型欠拟合隐藏层节点太少或训练轮数不够。BP 网络在特征与目标关系复杂时需要足够的容量来拟合。如果隐藏层只有 2 个节点网络只能学到一个近似线性的映射预测值自然趋近于均值。解决把隐藏层节点数增加到 10 到 15训练轮数加到 500 以上观察验证损失是否还在下降。如果验证损失和训练损失都高就是欠拟合如果训练损失低但验证损失高才是过拟合。4.3 现象每次重新训练结果差异很大原因PyTorch 默认的权重初始化是随机的不同的初始值会收敛到不同的局部极小。对于小数据集这种方差尤其明显。解决固定随机种子。在代码开头加上torch.manual_seed(42)和np.random.seed(42)这样每次运行结果可复现。另外可以多跑几次取平均但固定种子是最省事的做法。4.4 现象验证损失比训练损失还低原因通常是因为验证集太小或者验证集和训练集分布不一致。如果测试集只有 20 条样本损失波动会很大偶尔低于训练损失是正常的。但如果持续如此要检查数据划分是不是有问题比如测试集里恰好都是消费额接近均值的学生。解决用 K 折交叉验证代替单次划分。把数据分成 5 份每次用 4 份训练、1 份验证最后取平均损失。这样评估结果更稳定也能更充分地利用小数据集。4.5 现象预测值出现负数原因输出层没有加约束线性激活可以输出任意实数。消费金额不可能为负但网络不知道这个常识。解决在输出层后面加一个 ReLU 或者 Softplus 激活把输出限制在正数范围。或者对标签做对数变换预测后再取指数还原。我一般直接用nn.ReLU()包在输出层外面简单有效。5. 用 SHAP 值解释预测结果以及一个提升精度的特征构造技巧模型跑通之后你可能会被问到「为什么这个学生预测消费是 2200 而不是 1800」。BP 神经网络是个黑匣子但 SHAP 值可以告诉你每个特征对单条预测的贡献。安装shap库后用shap.DeepExplainer对训练好的模型做解释import shap # 用训练集的一部分作为背景数据 background X_train_t[:100] explainer shap.DeepExplainer(model, background) shap_values explainer.shap_values(X_test_t[:50]) # 输出每个特征的平均绝对贡献 shap.summary_plot(shap_values, X_test_t[:50].numpy(), feature_namescont_cols bin_cols)background是用于估计基线期望的样本一般取 100 条就够。shap_values的形状和输入一致正值表示该特征推高了预测值负值表示拉低。比如「月生活费」的 SHAP 值通常最大说明它是最重要的特征「月均聚餐次数」的贡献可能排第二。这个结果可以用来跟业务方解释也可以指导特征筛选——如果某个特征的 SHAP 值始终接近 0就可以删掉。另一个提升精度的技巧是构造交互特征。比如「月生活费 / 月均网购次数」可以反映单次网购的平均花费这个比值可能比原始特征更有预测力。构造完新特征后记得重新做标准化然后重新训练。我试过在原始 12 个特征基础上加 3 个比值特征验证集 MSE 能降 8% 左右。最后说一个我自己的习惯每次训练完模型不要只看 MSE把预测值和真实值画成散点图。如果点均匀分布在 yx 对角线附近说明模型没有系统性偏差如果低消费段预测偏高、高消费段预测偏低说明模型被均值拽住了需要增加隐藏层容量或加更多特征。这个图比任何数字都直观。希望帮到你。本文还有配套的精品资源点击获取
返回列表