简介:这是一份基于LSTM的轨道交通客流预测完整项目包,面向数据科学、交通数据分析初学者及课程设计人群,解决地铁客流时间序列建模与预测问题。压缩包共17个文件,包含5份客流与天气的表格数据、1份预测脚本、2份LSTM模型权重文件,另附原始Excel数据、答辩演示文稿与使用说明文档等,整体大小仅3.95MB,轻量但模块完整;目前已有1292人学习下载,适合快速复现实验或借鉴项目流程。项目以2019年某地铁站平常日客流量及每日天气因素为基础,完成数据清洗与特征补充后按8:2划分训练集和测试集,利用LSTM神经网络搭建预测模型并输出可视化结果;同时提供天气与客流数据及训练好的模型文件,便于学习者对照理解数据预处理、模型调参与评估。所有文件目录清晰,可直接运行脚本开展客流预测实验。 前些天有人问我有没有一份“基于LSTM交通客流预测”的完整方案,要求是能直接跑起来的代码。这几年我帮人做过不少类似的预测项目,从地铁刷卡数据到公交线路客流再到景区节假日人流,换汤不换药。这篇把我实际落地这套方案时的思路、代码和踩坑记录一起整理出来,如果你是准备做毕设、参加数模,或者公司里刚接手智慧交通客流预测的任务,都可以直接拿这份流程去套。
先说清楚这类项目到底能做什么:输入历史客流量序列,输出未来几个时间段的客流量。它适合站在运营视角解决排班、调度的预判问题,比如提前知道下一小时某站会进站多少人,从而决定是否加开列车或安排引导员。对新手来说,最大的门槛往往不是LSTM本身,而是不熟悉“时间序列建模”的完整链条:数据清洗、滑窗构造、训练集切分、评价指标选择。这篇文章会按我实操的顺序把这些全走一遍。
1. 先把问题想清楚:交通客流预测到底在预测什么
1.1 这不是简单的“猜明天有多少人”
交通客流预测最常见的形式是短时客流预测,粒度可以细到15分钟、30分钟,也可以粗到一天。以地铁为例,一个站点一天内的客流通常会有明显的双高峰:早高峰集中在7点到9点,晚高峰集中在17点到19点,周末和节假日又会切换成另一种模式。你直接用均值去猜,很容易把最高峰低估一大截;而用LSTM去学,至少能把这些周期性特征记下来。
我们在做项目时要先明确三件事:预测目标是谁(站点、线路还是全网)、预测粒度是什么(下一个15分钟还是下一天)、可用特征有哪些(只有历史客流,还是还有天气、节假日、周边活动)。这三句话确定下来,后面所有代码结构都会跟着变。我见过不少人拿到数据就写model.fit,结果连训练数据长什么样都没弄明白,这种项目做到最后基本都会推翻重来。
1.2 为什么选 LSTM 而不是 ARIMA、XGBoost
很多同学在选模型时会纠结。这里我不绕圈子,直接说结论。ARIMA是经典的时间序列模型,但它是线性模型,对强非线性、多变量关系不太友好,一次只能处理单变量,而且参数选择依赖人工看ACF/PACF,很费劲。XGBoost这类树模型能做回归,也能吃很多特征,但需要把滞后项手工构造出来,比如“前1小时客流”“昨天同一时刻客流”,特征工程工作量不小,而且时间依赖越长,越容易堆出一堆冗余特征。
LSTM的门控机制解决了传统RNN的长依赖问题,可以自己学习“早高峰之后会是平峰”“节假日前一天晚上客流会提前释放”这类规律。所以我通常把LSTM作为时序预测的首选基线,不是因为它在所有场景都最准,而是因为它能端到端地处理多输入、多步输出,给后续扩展留了很大空间。
这里有必要说明一点:LSTM不是万能的。如果你的样本量很小(比如只有几十天的日粒度数据),或者数据本身几乎无规律,那它的优势发挥不出来,甚至可能不如一个带星期几的历史均值模型。但在中大规模的短时客流场景下,它确实是见效最快、调参成本适中的方案。
2. 数据准备比模型更关键
2.1 数据来源与字段设计
模型能学到什么上限,由数据决定。我给这个项目设计的最小字段集如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| datetime | 时间戳 | 数据粒度,建议统一为15分钟或1小时 |
| station_id | 分类 | 站点/线路标识,做多序列时可分组 |
| flow | 数值 | 该时间段内的客流量,目标值 |
| weekday | 数值 | 星期几,0-6或1-7 |
| is_holiday | 0/1 | 是否节假日 |
| temp | 数值 | 温度,极端天气会明显影响客流 |
| rain | 0/1或数值 | 降雨量,恶劣天气会让地铁客流升高 |
真实数据很少是干净的。地铁闸机偶尔故障会导致单条记录缺失,网络传输也可能产生重复行。我的处理顺序是:先按时间排序,再检查是否连续,最后用前向填充或线性插值补缺失。注意不要用全局均值去补,因为客流有强烈的时间趋势,用前一天同一时刻的值补会更合理。
另外,如果原始数据是15分钟粒度,你也可以通过resample聚合成小时粒度,这在建模初期能显著减少噪声。聚合时用sum就行,不要mean,因为客流总量是累加语义。
2.2 时间窗口选取与样本构造
LSTM的输入是一个二维截面:某时刻之前的一条序列片段。所以要先把一维客流序列切成长度为lookback的样本,再配上长度为horizon的预测目标。比如用前24小时的逐小时客流预测未来1小时,那lookback=24、horizon=1;如果想预测未来4小时,另一个设计是让模型一次输出4个值,即horizon=4。
窗口长度怎么定?我一般先画自相关图,观察客流序列有没有明显的24小时周期或168小时(7天)周期。如果24小时周期很强,窗口至少要有24个点;如果数据是从两周里切出来的,窗口不宜设成168,否则样本量会骤减。实际操作中,我会对比lookback在12、24、48、72时的验证集误差,选误差相对稳定的那个。对小时级地铁客流,24到48通常是甜点区。
样本构造还有一个容易踩的雷:不能随机打乱。时间序列样本必须按时间顺序切分,把前80%作为训练集、后20%作为测试集。如果用train_test_split(shuffle=True),相当于让模型偷看了未来的信息,测试误差会虚低,真上了线立刻现原形。这一点我会在后面专门展开。
另外,归一化也必须在训练集上fit,再transform全量数据。这个顺序很多人写反,导致测试集信息提前进入训练过程。
3. 模型搭建与训练实录
3.1 核心代码结构
这部分我给出一个能跑通的最小实现,用的是TensorFlow/Keras。假设你已经把原始数据处理成了df,包含datetime、flow两列,并按时间排好了序。
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau # 假设 df 已按时间排序,这里先只取“客流量”一个特征做演示 scaler = MinMaxScaler() df['flow_scaled'] = scaler.fit_transform(df[['flow']]) def make_dataset(data, lookback=24, horizon=4): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) y.append(data[i + lookback:i + lookback + horizon]) return np.array(X), np.array(y) X, y = make_dataset(df['flow_scaled'].values.reshape(-1, 1), lookback=24, horizon=4) # 按时间顺序切分,80%训练,20%测试 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] model = Sequential([ LSTM(64, return_sequences=True, input_shape=(X.shape[1], X.shape[2])), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(horizon) ]) model.compile(optimizer='adam', loss='mse') model.summary()这个代码里有几个关键点。第一,make_dataset返回的X形状是(samples, lookback, features),这里由于只用了单变量,features=1。第二,第一层LSTM必须设置input_shape,且因为后面还要接第二层LSTM,所以return_sequences=True,把完整序列传给下一层。第三,最后一层Dense的输出维度等于horizon,也就是一次预测未来4个点。
如果你的输入带有星期、天气等多个特征,只需要把归一化后的特征矩阵拼在一起传给make_dataset,X.shape[2]自然会变大,input_shape不用手写死。反之,如果你想减少计算量,可以只用一列客流量,效果会打折,但跑通流程没问题。
3.2 训练参数与调参思路
我习惯把这个模型的训练参数固定成一套默认值,再根据验证误差微调:
| 参数 | 默认值 | 说明 |
|---|---|---|
| optimizer | Adam | 自适应学习率,适合LSTM |
| learning_rate | 0.001 | 如果loss震荡,降到0.0005 |
| batch_size | 32 | 数据集大可以提到64或128 |
| epochs | 100 | 配合EarlyStopping使用 |
| dropout | 0.2 | 过拟合时提高到0.4-0.5 |
| hidden_units | 64/32 | 先小后大,不要一上来就128 |
再加两个回调函数,会省很多心。EarlyStopping在验证loss连续N轮不下降时自动停掉,避免白跑。ReduceLROnPlateau在loss进入平台期时自动降低学习率,帮助模型再往下走一小截。代码很简单:
callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5) ] model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=callbacks, verbose=1)调参顺序我一般是这样:先固定batch_size,调节学习率;然后看训练集和测试集loss的差距判断是否过拟合,如果差距大就加Dropout;最后再调hidden_units。不要一开始就上网格搜索,LSTM训练一次要几分钟到几十分钟,盲目搜索很浪费时间。
4. 评估与效果观察
4.1 别只看loss,要看真实误差
训练过程中的loss是归一化空间里的均方误差,数字再小也没有直观含义。真正要给人汇报的一定是反归一化之后、在原始客流单位下的误差。常用指标有三个:
| 指标 | 公式含义 | 使用场景 |
|---|---|---|
| RMSE | 均方根误差 | 对大误差更敏感,适合高峰客流预测 |
| MAE | 平均绝对误差 | 最直观,单位就是人 |
| MAPE | 平均绝对百分比误差 | 适合评估不同量级站点,但客流接近0时不稳定 |
计算方式也不复杂。先把预测值和真实值从归一化空间还原,再套公式。需要注意,如果你同时对多个特征做了归一化,inverse_transform的时候需要取出flow列对应的位置,别直接对整个预测数组反变换。
客流预测的合理误差水平要看业务场景。比如站点日均客流1800人,小时级预测RMSE在120人左右,MAPE大约6%,已经能辅助排班。如果RMSE高到300人以上,那说明预测结果在高峰时段基本不可信,需要优先优化数据或特征,而不是继续堆模型层数。
4.2 预测滞后的现象与原因
这是LSTM客流预测里最常见的现象之一:画出来的预测曲线比真实曲线晚了一个时间步,峰值位置偏移,整体像是被“拖”了一段。很多人一看到这个就疯狂调模型,但其实本质上是因为你训练的是一个“拟合均值”的回归模型,它在面对强波动时会偏向输出一个平滑的折中值。
要缓解这个问题,我有几个实际经验。第一个是增加预测步长,不要用horizon=1做单步预测,而是让模型一次预测未来4到8个时间点,这样强制模型去学习趋势,而不是偷懒复制上一步。第二个是加入外部特征,把星期几、节假日、天气作为输入,模型会更容易分辨“今天是周末,客流形态不同”。第三个是尝试预测客流增量而不是绝对值,即目标从“未来客流”变成“未来客流减当前客流”,这可以减轻序列平稳性不足的问题。
如果你的项目允许,还可以在模型后接一个简单的后处理,比如对预测结果做指数平滑,让曲线更贴合实际。但要记住,这只能改善视觉观感,真正提升精度还是要靠特征和结构。
5. 实践中的坑和排查方法
5.1 loss变成NaN
我说句实话,我见过的新手报错里,有一半以上是第一轮训练loss直接变成NaN。最先检查的应该是数据本身:打印df.isnull().sum(),看看有没有NaN或无穷大值。如果数据没问题,再看归一化范围,我强烈建议用MinMaxScaler把输入压到0-1之间,这样LSTM的梯度不会因为数值范围过大而爆炸。
还有一个容易被忽略的点:学习率太大。Adam默认的0.001在我的数据集上没问题,但如果你自己构造的特征数值差了好几个数量级,或者样本量很小,0.001也可能让loss跑飞。这时可以把learning_rate降到0.0005,甚至在编译时加clipnorm=1.0,给梯度设一个上限。这一招在深层LSTM里很实用。
5.2 输入维度对不上
Keras的LSTM层要求输入是三维张量(samples, timesteps, features),但很多人构造完样本后打印X.shape,发现是二维,甚至是一维,于是在model.fit时报错:
“Input 0 of layer sequential is incompatible: expected ndim=3, found ndim=2”。
问题几乎都出在make_dataset里。请记住:make_dataset返回的X要用np.array括起来,并且如果你传入data时用了reshape(-1, 1),最终X.shape一定会带有features维度。如果你用的是二维特征矩阵,那X.shape就是(samples, lookback, feature_count)。
预测单条样本时也容易踩同样的坑。模型训练好后,要预测最新一条数据,你需要把这条数据构造成(1, lookback, features)的形状,可以用np.expand_dims(sample, axis=0)。我见过有人在预测这步折腾了半个多小时,就是因为忘了加样本维度。
5.3 数据泄露导致成绩虚高
数据泄露在时间序列项目里太隐蔽了。最常见的有两种:一是归一化时直接对全量数据fit_transform,把测试集的均值/最大值也算进了训练过程;二是用train_test_split默认的随机切分,导致训练集和测试集在时间上交错,模型提前“见过”未来的分布。
这样做的后果是验证集指标非常漂亮,但一上真实场景,预测能力直接崩掉。正确做法就一句话:先按时间排序,再训练集上fit归一化器,transform所有数据;样本切分严格按时间顺序,前80%训练、后20%测试。如果你要做一个可靠的实验,还应该在测试集和训练集之间留一段gap,比如把最后5天的数据从训练集里拿掉,防止滑窗样本在边界处造成重叠。
做这种LSTM时间序列项目,模型结构反而只占很小一部分时间,真正花时间的都在数据清洗、窗口构造和实验设计上。我几次跑下来最深的感受是:如果预测总出现滞后,别急着加注意力机制或堆两层LSTM,先把训练数据的时间边界检查一遍,再把外部特征加上,效果往往比换模型更明显。
最后分享一个小技巧:跑实验时固定随机种子,把每次的数据切分和初始权重保持一致,这样你改一个参数时,才能确定变化来自你的改动,而不是随机性。别小看这一步,很多人在调参时被随机噪声折腾得怀疑人生。如果你打算在现有代码上继续扩展,建议优先尝试多变量输入和预测增量两个方向,它们在交通客流预测里被验证最有效。希望这份实操记录能帮你少走点弯路,直接跑通自己的客流预测项目。
本文还有配套的精品资源,点击获取