简介:这份资源面向本科及以上阶段、需要完成课程设计或数据建模实践的学习者,围绕城乡大学生月平均消费数据,用MATLAB实现BP神经网络预测模型,帮助理解神经网络从数据加载、训练到结果输出的完整流程。压缩包共4个文件,约323KB,包含2个m脚本文件、1个xls数据表和1个doc实验报告,脚本可直接运行并附有注释,数据表提供建模所需的消费样本,报告文档则梳理了实验背景与结果分析,便于对照代码理解建模思路。目前已有65人学习下载。读者可据此掌握BP网络的参数设置、训练过程与预测评估方法,并在此基础上替换数据或调整网络结构,扩展至其他回归预测场景,适合作为入门神经网络与MATLAB编程的实践参考。
1. 大学生消费预测这件事,为什么用 BP 神经网络能跑通
月初生活费到账,一周内花掉一半,月底靠泡面续命——这个场景几乎每个大学生都经历过。把这件事做成一个可预测的模型,就是「大学生消费预测」要解决的问题:给定一个学生的性别、年级、月生活费、是否兼职、消费习惯等特征,预测他下个月的消费金额或消费等级。它适合两类人:一类是刚学完 BP 神经网络、想找一个完整数据集练手的学生;另一类是想把预测结果用于校园贷风控、助学金精准发放、校园商户备货的从业者。
为什么选 BP 神经网络而不是线性回归或 XGBoost?线性回归只能拟合线性关系,而消费行为里「生活费越高、消费增速反而放缓」这种非线性拐点很常见;XGBoost 精度往往更高,但需要调参经验,对新手不友好。BP 神经网络结构简单、可解释性够用、代码量小,是入门预测任务里性价比最高的选择。这一章先把「这是什么、能解决什么、适合谁」讲清楚,后面几章带你从数据到模型完整跑一遍。
2. 数据从哪来、怎么洗:大学生消费数据集的字段与预处理
2.1 一份能用的消费数据集该有哪些字段
网上流传的大学生消费数据集质量参差不齐,很多只有几百条、字段还残缺。一份能支撑 BP 神经网络训练的消费数据,至少要包含三类字段:人口属性(性别、年级、专业类别)、经济属性(月生活费、是否兼职、兼职收入、家庭月收入档位)、行为属性(月消费总额、餐饮占比、购物占比、娱乐占比、是否使用分期)。目标字段通常是「下月消费总额」或「消费等级(低/中/高)」。
字段类型决定了预处理方式。性别、是否兼职这类二值字段做 0/1 编码;年级、专业类别这类无序多分类字段做独热编码;月生活费、消费总额这类连续字段做归一化。这里有个血泪经验:很多人直接把原始金额丢进网络,结果训练 loss 一直震荡不收敛,就是因为量纲差异太大——生活费可能是 2000,餐饮占比是 0.3,网络会把大数值字段当成主导特征。
2.2 用 pandas 做缺失值处理和归一化
import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取数据,假设文件为 student_consumption.csv df = pd.read_csv('student_consumption.csv') # 1. 缺失值处理:数值字段用中位数填充,类别字段用众数填充 num_cols = ['monthly_allowance', 'part_time_income', 'total_consumption'] cat_cols = ['gender', 'grade', 'major_type', 'use_installment'] for col in num_cols: df[col] = df[col].fillna(df[col].median()) for col in cat_cols: df[col] = df[col].fillna(df[col].mode()[0]) # 2. 独热编码:把无序类别字段展开 df = pd.get_dummies(df, columns=['grade', 'major_type'], drop_first=True) # 3. 归一化:只对连续字段做,0/1 字段保持原样 scaler = MinMaxScaler() df[num_cols] = scaler.fit_transform(df[num_cols]) # 4. 划分特征和标签 X = df.drop(columns=['total_consumption']).values y = df['total_consumption'].values.reshape(-1, 1) print('特征维度:', X.shape, '标签维度:', y.shape)这段代码的逻辑是「先补缺、再编码、后归一」。fillna用中位数而不是均值,是因为消费数据里高消费的极端值会拉高均值,中位数更稳。get_dummies的drop_first=True是为了避免独热编码后的多重共线性,虽然神经网络对共线性不敏感,但少一列能减少参数量。MinMaxScaler把连续字段压到 [0,1],注意 scaler 要保存下来,预测新样本时得用同一个 scaler 变换,否则结果全错。
参数上,num_cols和cat_cols必须按你实际数据集的列名改,别照抄。如果目标字段是消费等级而不是金额,y要做 LabelEncoder 转成 0/1/2,输出层神经元数也要从 1 改成 3。
3. 用 PyTorch 搭一个能收敛的 BP 神经网络
3.1 网络结构怎么定:层数、神经元数、激活函数
BP 神经网络结构图里最常见的是「输入层-隐藏层-输出层」三层结构。对于大学生消费预测这种特征维度在 10~30 之间的任务,隐藏层设 1~2 层就够了,每层神经元数取输入维度的 1~2 倍。层数堆太多,几百条数据根本喂不饱,直接过拟合。
激活函数隐藏层用 ReLU,输出层看任务:预测金额用恒等函数(不加激活),预测等级用 Softmax。损失函数对应选 MSE 或 CrossEntropyLoss。优化器用 Adam,学习率 0.001 起步,这是最不容易翻车的组合。
3.2 完整训练代码与关键参数说明
import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader from sklearn.model_selection import train_test_split # 转成 tensor X_tensor = torch.FloatTensor(X) y_tensor = torch.FloatTensor(y) # 划分训练集和测试集,8:2 X_train, X_test, y_train, y_test = train_test_split( X_tensor, y_tensor, test_size=0.2, random_state=42) train_ds = TensorDataset(X_train, y_train) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) # 定义 BP 网络 class BPNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) # 回归任务输出 1 维 ) def forward(self, x): return self.net(x) model = BPNet(X_train.shape[1]) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 训练循环 for epoch in range(200): model.train() epoch_loss = 0 for xb, yb in train_loader: pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss += loss.item() if (epoch + 1) % 20 == 0: print(f'Epoch {epoch+1}, Loss: {epoch_loss/len(train_loader):.6f}')逻辑说明:nn.Sequential把层按顺序串起来,前向传播自动完成。batch_size=32是经验值,数据量小可以调到 16,数据量大调到 64。shuffle=True必须开,否则每轮喂入顺序一样,梯度更新会走偏。训练 200 轮是保守估计,实际看 loss 曲线,连续 20 轮不降就可以停。
参数上,lr=0.001是 Adam 的甜点值,loss 震荡就降到 0.0005,收敛太慢就升到 0.002。隐藏层 64 和 32 这两个数可以改,但别设成 512 这种大数,几百条数据撑不住。random_state=42固定划分,保证每次跑结果可复现。
4. 模型评估与调参:别只看 loss 下降就以为成了
4.1 回归任务该看哪些指标
训练 loss 下降不代表模型能用。消费预测是回归任务,要看 MAE(平均绝对误差)、RMSE(均方根误差)和 R²。MAE 告诉你平均预测偏了多少块钱,最直观;RMSE 对大误差惩罚更重,能暴露极端预测;R² 看模型解释了多大比例的方差,低于 0.5 基本说明特征没选对。
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model.eval() with torch.no_grad(): pred_test = model(X_test).numpy() # 注意:预测值和真实值都是归一化后的,要反归一化才能看真实金额 pred_real = scaler.inverse_transform(pred_test) y_real = scaler.inverse_transform(y_test.numpy()) mae = mean_absolute_error(y_real, pred_real) rmse = np.sqrt(mean_squared_error(y_real, pred_real)) r2 = r2_score(y_real, pred_real) print(f'MAE: {mae:.2f} 元, RMSE: {rmse:.2f} 元, R2: {r2:.4f}')这里有个容易翻车的点:scaler.inverse_transform只能对当初归一化的那些列做逆变换,而total_consumption在预处理时被归一化了,所以能直接逆变换。如果你把标签单独归一化,就得单独存一个标签的 scaler,别混用。
4.2 调参的三个方向与边界
调参优先动这三个:隐藏层神经元数、学习率、batch_size。神经元数从 32 试到 128,看测试集 MAE 什么时候最低;学习率在 0.0005~0.005 之间扫;batch_size 在 16~64 之间试。别一次改多个参数,否则不知道是哪个起了作用。
边界在哪?如果测试集 MAE 已经接近月消费的标准差,说明模型没学到东西,回去检查特征工程;如果训练集 MAE 远低于测试集,就是过拟合,加 Dropout 层或减神经元。数据量低于 300 条时,任何调参都是玄学,先把数据凑够再说。
5. 避坑与排查:消费预测模型最常见的 5 个翻车现场
现象一:loss 变成 NaN。原因通常是学习率太大或输入里有异常值(比如生活费填了 999999)。解决:先把学习率降到 0.0001,再用df.describe()检查每个字段的最大最小值,把超过 3 倍标准差的离群点剔除或截断。
现象二:训练集准确率 99%,测试集一塌糊涂。典型过拟合。原因可能是隐藏层神经元太多、训练轮数太多、数据量太小。解决:加nn.Dropout(0.3),把训练轮数砍一半,或者用早停(验证集 loss 连续 10 轮不降就停)。
现象三:预测值全部集中在均值附近。模型没学到特征,输出了一条水平线。原因多半是特征和标签相关性太弱,或者归一化时把标签也压得太狠。解决:先算一下特征和目标字段的相关系数,低于 0.1 的特征直接删掉;检查标签归一化后的分布,如果全挤在 0.5 附近,说明原始数据本身方差就小。
现象四:换一批数据预测结果全错。忘了保存训练时的 scaler 和编码器。新数据的字段顺序、编码方式必须和训练时完全一致。解决:用joblib.dump把 scaler 和get_dummies后的列名存下来,预测前先对齐列。
现象五:MAE 看着不错,但业务上没法用。比如 MAE 是 200 元,但学生月消费才 1500 元,误差率 13%,对风控来说太高。解决:把回归改成分类,预测「低/中/高」三档,用准确率和召回率评估,业务上更好落地。
6. 让预测结果真正能用:从模型输出到业务决策的最后一公里
模型跑出 MAE 之后,很多人就停了。但一个消费预测模型要真正产生价值,得解决「预测出来给谁用、怎么用」的问题。我一般会做两件事:一是把预测结果分档,二是做特征重要性分析。
分档很简单,把预测金额按分位数切成三档,对应「低消费、中消费、高消费」,输出一个类别标签。这样校园商户备货时看档位就行,不用理解具体金额。特征重要性分析用置换重要性(permutation importance):逐个打乱某个特征的值,看 MAE 涨多少,涨得越多说明该特征越重要。我实测下来,「月生活费」和「餐饮占比」通常排前两位,而「性别」几乎没影响——这意味着做风控时不用在性别上做区分,避免合规风险。
from sklearn.inspection import permutation_importance def score_model(model, X, y): model.eval() with torch.no_grad(): return mean_absolute_error(y, model(X).numpy()) result = permutation_importance( estimator=model, X=X_test, y=y_test.numpy(), scoring=score_model, n_repeats=10, random_state=42) for i in result.importances_mean.argsort()[::-1]: print(f'特征 {i}: 重要性 {result.importances_mean[i]:.4f}')这段代码的scoring传的是自定义函数,因为 sklearn 默认的评分器不认 PyTorch 模型。n_repeats=10表示每个特征打乱 10 次取平均,次数越多越稳但越慢,数据量小的时候 5 次也够。
最后一个习惯:每次训练完,把「数据版本、特征列表、超参数、测试集 MAE」记到一个表格里。我吃过亏,调了三天参发现还不如第一版,结果第一版的配置没存,只能重跑。这个习惯比任何调参技巧都值钱。希望帮到你。
本文还有配套的精品资源,点击获取