简介:这份资源面向本科及以上阶段、需要完成神经网络课程设计或数据建模练习的学生与研究人员,围绕城乡大学生月平均消费数据,提供一套可直接运行的MATLAB BP神经网络预测方案,用于解决消费趋势拟合与预测问题。压缩包共4个文件,约323KB,包含2个m脚本文件、1个xls数据表和1个doc实验报告,脚本分别对应城乡两类样本的建模流程,数据表提供原始月均消费记录,报告则梳理实验背景与结果分析,便于对照理解。目前已有65人学习下载。代码注释较为完整,读者可据此掌握BP网络的参数设置、训练流程与预测输出,并在此基础上替换数据、调整隐含层结构或扩展为其他回归预测任务,适合作为课程作业、入门实践与二次开发的参考模板。
1. 从一份校园消费流水说起:BP神经网络做大学生消费预测到底靠不靠谱
每年开学季,辅导员的桌上都会多出几张表:谁的生活费又超支了,谁在食堂的月均消费突然掉了一半,谁在双十一前后刷卡频率翻了三倍。这些数据散落在校园卡系统、食堂刷卡机、宿舍水电缴费记录里,单看每一笔都平平无奇,合在一起却能勾勒出一个学生的消费画像。问题在于,画像容易画,预测难做——下个月这个学生会花多少钱?他会不会在某类消费上突然失控?这才是学工和后勤真正关心的。
BP神经网络做大学生消费预测,本质上是把「历史消费行为」映射到「未来消费金额」的一个回归问题。输入层放的是可量化的特征:月均食堂消费、超市消费频次、水电充值间隔、是否申请过助学金、年级、专业类别等;输出层通常就是一个连续值——下月总消费或某类消费的预测值。它适合谁?适合手里有几千到几万条校园卡流水、想用Python快速搭一个能跑通、能解释、能迭代的预测原型的同学。代码完整、数据齐全的版本,意味着你不需要从零爬数据,拿到就能跑,跑完就能看结果。但「能跑」和「有用」之间隔着特征工程、网络结构、过拟合这三道坎,下面一层层拆。
2. 数据准备与特征工程:把校园卡流水变成BP神经网络能吃的矩阵
2.1 原始消费数据的清洗与聚合口径
拿到的大学生消费数据,常见格式是每笔交易一条记录:学号、交易时间、交易金额、交易地点(食堂/超市/水电/其他)、交易类型(消费/充值)。直接喂给BP神经网络是行不通的,因为网络需要的是「样本×特征」的二维矩阵,而不是流水日志。第一步必须做聚合,把「人×月」作为基本分析单元。
清洗时重点处理三类脏数据:金额为负的退款记录、交易地点为空的记录、同一秒内重复出现的记录。退款记录如果混在消费里,会把月消费总额拉低,导致预测偏保守;地点为空则无法做分类特征。我一般用pandas做三步走:先按学号和时间排序,再标记异常记录,最后按月分组聚合。
import pandas as pd import numpy as np # 读取原始流水,假设字段为:stu_id, trans_time, amount, location, trans_type raw = pd.read_csv('campus_consumption.csv', parse_dates=['trans_time']) # 只保留消费类型,剔除退款和充值 raw = raw[raw['trans_type'] == '消费'].copy() raw = raw[raw['amount'] > 0] # 剔除地点缺失的记录 raw = raw.dropna(subset=['location']) # 生成月份列,用于按月聚合 raw['month'] = raw['trans_time'].dt.to_period('M') # 按学号+月份聚合:总消费、食堂消费、超市消费、交易笔数 agg = raw.groupby(['stu_id', 'month']).agg( total_spend=('amount', 'sum'), canteen_spend=('amount', lambda x: x[raw.loc[x.index, 'location'] == '食堂'].sum()), market_spend=('amount', lambda x: x[raw.loc[x.index, 'location'] == '超市'].sum()), trans_count=('amount', 'count') ).reset_index()这段代码的逻辑是:先过滤掉非消费记录,再按「学号+月份」做分组聚合。total_spend是当月总消费,canteen_spend和market_spend是分场景消费,trans_count是交易笔数。参数上,to_period('M')把时间戳转成月份周期,方便后续做时间序列的滑窗。注意lambda里用raw.loc[x.index, 'location']回查原始地点,这是为了在聚合时保留分类维度,代价是速度稍慢,数据量超过十万行时建议先做透视表再合并。
2.2 构造滞后特征与滑窗统计量
BP神经网络本身没有记忆能力,它不会自动知道「上个月花了多少」。所以必须手动构造滞后特征:把前1个月、前2个月、前3个月的总消费作为当前月的输入特征。同时,滑窗统计量也很关键——近3个月消费的均值、标准差、最大值,能反映消费的稳定性和波动幅度。
# 按学号分组后按月份排序,构造滞后特征 agg = agg.sort_values(['stu_id', 'month']) agg['lag1'] = agg.groupby('stu_id')['total_spend'].shift(1) agg['lag2'] = agg.groupby('stu_id')['total_spend'].shift(2) agg['lag3'] = agg.groupby('stu_id')['total_spend'].shift(3) # 近3个月滑动均值和标准差 agg['roll_mean_3'] = agg.groupby('stu_id')['total_spend'].transform( lambda x: x.rolling(3, min_periods=1).mean() ) agg['roll_std_3'] = agg.groupby('stu_id')['total_spend'].transform( lambda x: x.rolling(3, min_periods=1).std() ) # 剔除因滞后产生的空值行 agg = agg.dropna(subset=['lag1', 'lag2', 'lag3'])这里shift(1)表示取上个月的值,rolling(3)表示近3个月窗口。min_periods=1允许窗口内至少有一个值就计算,避免月初数据被大量丢弃。做完这一步,每个样本就有了「历史消费+当前特征」的完整输入向量。注意:滞后特征必须按学号分组后做,否则会把不同学生的消费混在一起,这是新手最容易翻车的地方。
2.3 类别特征编码与归一化
年级、专业类别、是否申请助学金这些类别特征,不能直接塞进网络。常见做法是独热编码(One-Hot),但类别数超过10个时会导致维度爆炸。我一般用嵌入(Embedding)的思路先做目标编码,或者直接限制类别数,把冷门专业归为「其他」。
归一化方面,BP神经网络对输入尺度敏感。总消费金额可能在500到5000之间,而交易笔数只有10到100,如果不做归一化,金额特征会主导梯度更新。用MinMaxScaler把每个特征缩放到[0,1]区间是最稳妥的。
from sklearn.preprocessing import MinMaxScaler # 数值特征列 num_cols = ['total_spend', 'canteen_spend', 'market_spend', 'trans_count', 'lag1', 'lag2', 'lag3', 'roll_mean_3', 'roll_std_3'] scaler = MinMaxScaler() agg[num_cols] = scaler.fit_transform(agg[num_cols]) # 类别特征做独热编码,限制类别数 agg = pd.get_dummies(agg, columns=['grade', 'major_type'], drop_first=True)fit_transform只在训练集上做,验证集和测试集要用同样的scaler做transform,否则会引入未来信息。drop_first=True是为了避免独热编码的共线性问题。做完这些,数据就变成了一个干净的二维矩阵,可以直接送进BP网络。
3. 用PyTorch搭一个能跑通的BP神经网络:结构、损失与训练循环
3.1 网络结构设计:几层、几个神经元、什么激活函数
大学生消费预测是一个中小规模的回归任务,输入特征通常在20到50维之间,样本量几千到几万。这种场景下,不需要深层网络。我一般用「输入层 → 隐藏层1(64神经元,ReLU)→ 隐藏层2(32神经元,ReLU)→ 输出层(1神经元,线性)」的结构。隐藏层太多容易过拟合,太少又学不到非线性关系。
激活函数选ReLU是因为它计算简单、梯度不容易消失。输出层必须用线性激活,因为预测的是连续金额,不能用Sigmoid把输出压到[0,1]——除非你对目标做了归一化,那输出层也要对应反归一化。
import torch import torch.nn as nn class ConsumptionBPNet(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) # 输出层线性,预测连续值 ) def forward(self, x): return self.net(x) # 假设输入特征维度为30 model = ConsumptionBPNet(input_dim=30)nn.Linear(input_dim, 64)是第一层全连接,把30维输入映射到64维隐藏表示。nn.ReLU()引入非线性。最后一层nn.Linear(32, 1)输出一个标量。参数上,隐藏层神经元数量可以按「输入维度×2」起步,再根据验证集损失调整。如果验证损失比训练损失高很多,说明过拟合,要减少神经元或加Dropout。
3.2 损失函数与优化器的选择
回归任务默认用均方误差(MSELoss),它对大误差惩罚重,适合消费预测这种「大额偏差比小额偏差更不可接受」的场景。但如果数据里有很多极端值(比如某个月突然买了台电脑),MSE会被拉偏,这时可以换HuberLoss,它在误差较小时等价于MSE,误差大时退化为线性,更鲁棒。
优化器选Adam,学习率设0.001。Adam对初始学习率不敏感,适合新手。如果训练损失震荡,把学习率降到0.0005;如果收敛太慢,升到0.002试试。
criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 如果数据有极端值,改用HuberLoss # criterion = nn.HuberLoss(delta=1.0)delta=1.0是HuberLoss的阈值参数,误差小于1时用平方,大于1时用线性。这个值要根据目标变量的尺度来定,如果消费金额已经归一化到[0,1],delta设0.1更合适。
3.3 训练循环与早停策略
训练循环的核心是:前向传播算预测值,计算损失,反向传播算梯度,更新参数。每轮记录训练损失和验证损失,当验证损失连续多轮不下降时,触发早停,防止过拟合。
def train_model(model, train_loader, val_loader, epochs=200, patience=20): best_val_loss = float('inf') wait = 0 train_losses, val_losses = [], [] for epoch in range(epochs): model.train() epoch_train_loss = 0 for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() epoch_train_loss += loss.item() model.eval() epoch_val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: pred = model(xb) epoch_val_loss += criterion(pred, yb).item() train_losses.append(epoch_train_loss / len(train_loader)) val_losses.append(epoch_val_loss / len(val_loader)) # 早停判断 if val_losses[-1] < best_val_loss: best_val_loss = val_losses[-1] wait = 0 torch.save(model.state_dict(), 'best_model.pth') else: wait += 1 if wait >= patience: print(f'Early stop at epoch {epoch}') break return train_losses, val_lossesmodel.train()和model.eval()切换训练/评估模式,影响Dropout和BatchNorm的行为。torch.no_grad()在验证时关闭梯度计算,节省显存。patience=20表示验证损失20轮不降就停。保存best_model.pth是为了保留验证集上最好的模型,而不是最后一轮的模型。这个训练循环是BP神经网络最标准的写法,改任务时只需要换数据加载器和损失函数。
4. 避坑与排查:大学生消费预测里最容易翻车的5个地方
4.1 现象:模型在训练集上损失很低,验证集损失居高不下
原因:过拟合。大学生消费数据里,同一个学生不同月份的消费模式可能高度相似,如果训练集和验证集按随机划分,同一个学生的数据会同时出现在两边,模型实际上在「背答案」。解决:按学号划分训练集和验证集,确保同一个学生的数据只出现在一边。另外,加Dropout层或L2正则化也能缓解。
4.2 现象:预测值全部集中在均值附近,没有波动
原因:归一化时用了全局数据,或者损失函数被极端值主导。如果目标变量没有做归一化,MSE会被大额消费拉偏,模型倾向于预测一个「安全」的中间值。解决:对目标变量做标准化(减均值除标准差),训练完再反变换回来。同时检查是否有异常月份(如开学季集中采购)需要单独处理。
4.3 现象:滞后特征构造后,样本量骤减
原因:shift(3)导致每个学生的前3个月数据被丢弃,如果学生只有4个月记录,就只剩1个样本。解决:对记录月份少的学生,用min_periods=1的滑动统计量替代固定滞后,或者把滞后阶数降到1。也可以在聚合时只保留至少有6个月记录的学生,牺牲样本量换特征质量。
4.4 现象:类别特征独热编码后维度爆炸
原因:专业类别可能有几十个,年级有4个,独热后特征维度从30涨到100多,网络参数暴增。解决:把低频类别合并为「其他」,或者用目标编码(Target Encoding)把类别映射为该类别的平均消费。目标编码要注意在训练集上计算,验证集用训练集的映射表。
4.5 现象:训练损失震荡不收敛
原因:学习率太大,或者输入特征没有归一化。检查每个特征的取值范围,如果某个特征在[0,10000]而其他在[0,1],梯度更新会被大特征主导。解决:统一做MinMax归一化,学习率从0.001开始试,如果震荡就减半。另外,Batch Size太小也会导致震荡,可以调到64或128。
5. 进阶技巧:用交叉验证和特征重要性把预测模型从「能跑」推到「能用」
5.1 按学号做分组交叉验证
随机划分训练集和验证集在消费预测里是自欺欺人。同一个学生的消费习惯高度稳定,随机划分会让模型「见过」验证集里的学生,导致验证损失虚低。正确做法是GroupKFold,按学号分组,确保每个学生的数据只出现在一个折里。
from sklearn.model_selection import GroupKFold gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(X, y, groups=stu_ids): X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx] # 重新初始化模型和优化器,训练并记录验证损失groups=stu_ids是关键,它告诉交叉验证器哪些样本属于同一个学生。5折意味着每个学生有80%的概率出现在训练集,20%出现在验证集。这样得到的验证损失才是对「新学生」的预测能力估计。
5.2 用Permutation Importance看哪些特征真正在起作用
BP神经网络是黑匣子,但你可以用排列重要性(Permutation Importance)来探测。思路很简单:把某一列特征随机打乱,看验证集损失涨多少。涨得越多,说明这个特征越重要。
from sklearn.inspection import permutation_importance import numpy as np def score_model(model, X_val, y_val): model.eval() with torch.no_grad(): pred = model(torch.tensor(X_val, dtype=torch.float32)) return -nn.MSELoss()(pred, torch.tensor(y_val, dtype=torch.float32)).item() result = permutation_importance( model, X_val, y_val, scoring=score_model, n_repeats=10, random_state=42 ) # 输出特征重要性排序 for i in np.argsort(result.importances_mean)[::-1][:10]: print(f'Feature {i}: {result.importances_mean[i]:.4f}')n_repeats=10表示每个特征打乱10次取平均,减少随机性。scoring函数返回负MSE,因为sklearn的permutation_importance默认越大越好。跑完这个,你通常会发现「近3个月滑动均值」和「上个月总消费」是最重要的两个特征,而「年级」和「专业类别」的重要性可能很低——这意味着你可以砍掉这些特征,简化模型。
5.3 一个我常用的验证习惯:留出最近一个月做时间外推
分组交叉验证解决的是「新学生」问题,但实际部署时还要面对「新月份」问题。我一般会额外留出最近一个月的数据作为时间外推测试集,不参与任何训练和调参。如果模型在这个月上的表现和交叉验证结果差距很大,说明消费模式随时间漂移了,需要重新训练或加入时间衰减权重。
这个习惯来自一次血泪经验:某次模型在交叉验证上MSE只有0.02,但上线后第一个月预测误差翻了三倍,原因是开学季消费模式突变,而训练数据里没有类似月份。后来我固定留出最近一个月做「后悔药」,每次训练完先看这个月的误差,再决定要不要部署。
希望帮到你。
本文还有配套的精品资源,点击获取