ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LSTM交通客流预测实战:数据预处理与PyTorch实现要点

LSTM交通客流预测实战:数据预处理与PyTorch实现要点 简介这是一份基于LSTM的交通客流预测项目资源面向数据科学学习者、交通行业数据分析师及竞赛参与者以某地铁站2019年日常客流量数据为基础补充每日天气因素剔除节假日影响后完成数据处理并按8:2比例划分训练集与测试集开展神经网络建模与客流预测可视化。资源共17个文件压缩包约3.95MB包含csv格式的原始客流与天气数据、h5格式的LSTM模型权重文件、py格式的训练预测脚本以及doc、ppt、md、xlsx等说明文档便于系统理解项目数据、模型与答辩展示流程。目前已有1292人学习浏览。通过本资源可完整掌握numpy、pandas进行数据清洗与特征构造使用sklearn完成数据划分与评估借助matplotlib、seaborn绘制预测对比图并深入理解LSTM在时序客流预测中的建模与调参思路无论用于课程设计、毕业设计或实际业务分析都具有较强的参考价值。1. 交通客流预测的开箱体验这份LSTM方案到底解决什么问题基于LSTM交通客流预测看起来是个标准的深度学习demo但真正动手做过的人都知道坑全在数据准备上。地铁断面客流、公交站点上下车人数、高速收费站流量这类数据天然带着早晚高峰、节假日扰动和长周期趋势用传统ARIMA需要人工判断阶数用LSTM则可以直接从序列里学出模式。这份zip里通常装着历史客流数据、LSTM网络定义、训练脚本和预测代码是一个完整的可运行方案。适合正在做客流时序建模、想把预测结果接入运营系统的工程师也适合作为LSTM时间序列预测Python项目的入门模板。它解决的核心问题是给定过去一段时间窗口的客流预测未来一个或多个时段的流量而不是简单拟合历史曲线。2. 把客流矩阵切成LSTM能吃的序列滑窗、归一化与时间序划分2.1 为什么必须先切成“窗口-标签”的形式普通回归模型的输入是一行特征输出是一个目标值。LSTM网络不一样它的输入是三维张量形状为(batch, seq_len, input_size)。 seq_len代表网络一次能看到的连续时间步数量input_size代表每个时间步上有几个特征。客流数据本身是二维的时间戳、客流量只有先按滑动窗口切成一个个样本才能送进LSTM训练。我一般会把“窗口长度”和“预测步长”分开设两个参数。窗口长度seq_len代表回看多久比如地铁客流按15分钟一条记录seq_len12就是看过去3小时预测步长horizon代表要预测多远horizon1表示预测下一个15分钟的客流。这两个参数是本类项目里最先要确认的业务口径直接决定后面样本怎么切。2.2 原始数据清理时间戳重采样与缺失值填充打开zip里的数据文件常见格式是两列timestamp和flow。第一件事不是建模而是把时间轴理顺。有些数据源会在高峰时段多记、半夜漏记时间戳不是等间距的直接切窗口会切出大量脏样本。import pandas as pd # 读取原始客流数据 df pd.read_csv(traffic_flow.csv, parse_dates[timestamp]) df df.set_index(timestamp).sort_index() # 统一重采样到15分钟粒度取均值 df df.resample(15min).mean() # 短期缺口用前向填充最多连续补6个时段 df[flow] df[flow].ffill(limit6)这段代码做三件事解析时间戳、按固定间隔重采样、有限填充缺失值。重采样这一步很关键它把不同来源的时间粒度统一到同一个节奏上ffill只能用于短时间缺口如果某个站点连续几小时没有数据硬填会把“设备故障期”当成“零客流期”模型会学到错误模式。遇到这种长缺口我的习惯是直接删除该时段样本或者补一个缺失标记特征而不是填进去。2.3 归一化只在训练段上做scaler.fit交通客流的数值范围可能从0到几万不归一化直接进LSTM梯度会在反向传播里剧烈震荡。常见的做法是用MinMaxScaler缩放到[0,1]之间。但这里埋着一个很容易翻车的细节scaler只能拿训练段的数据来fit不能拿全部数据fit。import numpy as np from sklearn.preprocessing import MinMaxScaler data df[[flow]].values.astype(float) n_train int(len(data) * 0.7) n_val int(len(data) * 0.9) scaler MinMaxScaler() scaler.fit(data[:n_train]) # 只用训练段确定最小值和最大值 train_part scaler.transform(data[:n_train]) val_part scaler.transform(data[n_train:n_val]) test_part scaler.transform(data[n_val:])如果在整条序列上做fit训练阶段就已经看到了未来数据的数值范围这属于一种隐蔽的数据泄漏。它不会让loss变得异常只会让模型在真实场景里失效。另一个容易踩的坑是时序数据划分不能用随机洗牌必须严格按时间顺序切成训练段、验证段、测试段因为客流数据前后相关打散之后模型会“看到未来”。2.4 create_sequences把数组切成LSTM样本归一化之后需要把一长条序列切成若干个“窗口-标签”对。这个函数是整个LSTM时序预测Python方案里最核心的预处理逻辑几乎所有开源实现都会包含一个类似版本。def create_sequences(data, seq_len12, horizon1): X, y [], [] # 最后一个样本必须留出horizon长度的真实值作为标签 for i in range(len(data) - seq_len - horizon 1): X.append(data[i:i seq_len]) # 输入窗口 y.append(data[i seq_len:i seq_len horizon]) # 未来horizon个时段的真值 return np.array(X), np.array(y) X_train, y_train create_sequences(train_part, seq_len12, horizon1) X_val, y_val create_sequences(val_part, seq_len12, horizon1) X_test, y_test create_sequences(test_part, seq_len12, horizon1)这段代码生成的X形状是(样本数, seq_len, 特征数)y形状是(样本数, horizon)。注意样本之间是重叠的第一个窗口覆盖0到11第二个窗口覆盖1到12这完全正常。LSTM本来就是滚动学习的不需要刻意去掉重叠。如果数据里有多个站点或线路要按站点分组后再分别调这个函数不要把不同站的客流混在一条序列里切否则模型会把站间差异也当成时序规律一起学。3. 用PyTorch搭建LSTM客流预测网络模型定义、训练循环与关键超参3.1 nn.LSTM的参数与输入输出形状数据准备好了接下来是模型部分。PyTorch的nn.LSTM是封装好的标准层不需要自己写门控逻辑但它的输入输出形状必须搞清楚否则后面维度对不上。import torch.nn as nn lstm nn.LSTM( input_size1, # 每个时间步的特征数只有客流就是1 hidden_size64, # 隐层神经元数量决定记忆容量 num_layers2, # LSTM层数2层在客流场景通常是够用的起点 batch_firstTrue, # 输入和输出的第一维是batch dropout0.2 # 层间dropout防止过拟合 ) # 假设输入 X: (batch32, seq_len12, input_size1) out, (h_n, c_n) lstm(X)batch_firstTrue会让输入形状保持(batch, seq_len, input_size)更符合直觉。out是每个时间步的隐状态输出形状(batch, seq_len, hidden_size)h_n是最后一层的最终隐状态形状(num_layers, batch, hidden_size)c_n是细胞状态形状相同。LSTM网络最早由Hochreiter和Schmidhuber提出它的价值在于用门控机制保留长期信息这对早晚高峰这种“几小时前甚至一周前的模式影响今天”的场景很合适。3.2 模型封装预测头怎么接工程上不会裸用nn.LSTM而是把它包进一个nn.Module里再接一个全连接输出层。关键问题是LSTM输出了一整条时间步的隐状态取哪个去接回归头。class TrafficLSTMModel(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout ) # 预测未来horizon个时段时把输出维度改成horizon即可 self.head nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last out[:, -1, :] # 取最后一个时间步的隐状态 return self.head(last) # (batch, 1)取最后一个时间步的原因是对于短时预测最后一个隐状态已经汇总了窗口内全部历史信息。不要对整条out做全局平均那样会削弱近期数据的影响。如果以后做多步预测只需把self.head的输出维度改成horizon标签也换成未来多个时段的真实值。3.3 训练循环与参数起点训练部分最需要注意的是DataLoader的shuffle参数。时序预测里shuffleFalse更合理因为样本本身就是按时间先后生成的打乱顺序不会带来优化优势反而破坏了验证集的时间一致性。import torch from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train) ) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) model TrafficLSTMModel(input_sizeX_train.shape[-1]) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() total_loss 0 for X_b, y_b in train_loader: optimizer.zero_grad() pred model(X_b) loss criterion(pred, y_b) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch:02d}, loss {total_loss / len(train_loader):.4f})这里有个参数起点表适合从零开始跑的时候参考参数起步值调整方向seq_len12序列越长越能看到周期但样本数会减少hidden_size6432到128之间试探过大容易过拟合num_layers2数据量少时回到1层更稳lr1e-3如果loss震荡就降到1e-4batch_size64小数据集用32即可epochs30以验证集为准不看训练集收敛就停如果训练loss卡住不动先看是不是学习率太小如果loss上下跳先降学习率。调参时每次只动一个变量记录结果不要同时改三个参数否则出了问题不知道是谁引起的。3.4 评估指标不要只看MSE模型训练完不能只看MSE。MSE对异常大的误差敏感但对业务来说客流预测的衡量标准是“差多少人”。我一般会同时算MAE和RMSE如果给运营部门看还会加一个MAPE。def mae(y_true, y_pred): return np.mean(np.abs(y_true - y_pred)) def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) def mape(y_true, y_pred): mask y_true ! 0 return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100MAPE在客流场景有个明显的毛病夜间低峰期客流量接近0分母接近0误差百分比会被瞬间放大到离谱。所以我在评估时会单独分早高峰、平峰、晚高峰三段来计算不只算全天平均值。高峰期误差才是运营最关心的指标。4. 避坑客流预测项目的5条踩坑记录与排查方法4.1 验证集很完美发布后却像“看着未来做预测”现象训练和验证的loss都正常但模型上线后预测结果明显偏移尤其在节假日前后完全跟不上真实客流变化。原因最常见的是两处。第一scaler在全量数据上fit训练阶段已经看了未来的数据范围第二用train_test_split默认参数划分时序数据它默认随机打乱模型在训练时见过未来样本。这两种都属于数据泄漏表现就是离线指标好看在线效果翻车。解决严格按照时间顺序切分并且只在训练段上做scaler.fit。有一个快速排查泄漏的方法把训练段fit出的scaler去transform测试段如果测试段的归一化数值大量超出[0,1]范围说明数据分布本身在漂移这时不要硬建模先把特征工程做扎实比如加入星期、小时和节假日属性。4.2 预测曲线总是比真实值滞后模型学会了“复制粘贴”现象把预测值和真实值画在一起两条曲线形状高度相似但预测曲线整体向右平移了一个时段早高峰总是慢半拍。原因交通客流自相关性极强下一个15分钟的客流大概率接近当前时段。模型发现“直接用最后一步输入当预测值”能让MSE很低于是走了一条捷径没有真正学到周期模式。这个问题在单步预测中非常常见属于LSTM时间序列预测项目里的经典玄学翻车点。解决第一步把seq_len从12加大到48让网络多看几个小时的上下文第二步加入星期、小时这类时间特征让模型有依据判断“现在处于什么周期”如果滞后依旧直接改成多步预测train的时候输出未来2到3个时段让模型不能只靠复制上一个值。验证时单独看早高峰7点到9点的MAE滞后问题在高频时段最容易暴露出真实差距。4.3 夜间客流全是0模型训到最后输出也全是0现象训练loss下降很慢验证集预测值集中在0附近。检查数据发现夜间低峰时段大量为0归一化后这些样本全部贴在下界。原因0值样本占比过高模型用“全预测成0”就能得到一个较低的MSE梯度也难以把输出推离下界。尤其在地铁停运时段输入窗口和后段标签全是0模型学到的是“这段时间就只能输出0”。解决如果业务只关心运营时段直接过滤夜间数据只保留5点到23点再训练效果立竿见影。不能过滤时可以对归一化结果做一个保护下界比如np.clip(scaled, 1e-4, 1.0)避免所有输入都压死在0。更讲究一点的做法是在损失函数上屏蔽停运时段但这种实现成本高优先级低于直接裁剪数据范围。4.4 结构一改就NaN或乱跳状态量取错了维度现象把网络从1层改成2层或者输出维度改成预测未来多个时段训练开始后loss变成NaN或者预测结果忽高忽低。原因LSTM返回的h_n形状是(num_layers, batch, hidden_size)c_n也一样。有人直接取h_n去接全连接没意识到多层时h_n的维度第一维是层数还有人把out、h_n、c_n混在一起做拼接维度对不上梯度一传播就炸。解决统一用out[:, -1, :]去衔接回归层别直接操作h_n。调试阶段在forward里打印shape比猜维度快得多def forward(self, x): out, (h_n, c_n) self.lstm(x) print(out:, out.shape, h_n:, h_n.shape) # 调试用稳定后删掉 last out[:, -1, :] return self.head(last)另外给优化器加上梯度裁剪防止梯度爆炸把loss冲成NaNtorch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)4.5 同一份代码跑三遍三个结果没固定随机种子现象调参时发现某个改动效果提升明显但多跑两遍效果又不一致最后说不清是改对了还是随机波动。原因PyTorch默认随机初始化numpy和python的random也参与样本扰动。不固定种子两次训练的网络初始权重就不同结果自然有差异。这是新手最容易忽略的一步也是最便宜的一个“后悔药”。解决在训练脚本入口统一固定随机种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True固定种子不能消除所有不确定性但能把“模型结构调整”和“随机性波动”分开。以后每次实验都记录命令行参数和seed复盘的时候才知道结果差异来自哪里。5. 从.ipynb到工程化把LSTM客流预测代码改成可维护脚本5.1 拿到zip先做数据探查很多打包成zip的LSTM项目是从notebook改过来的文件组织混乱、路径写死、参数埋在代码中间。拿到之后不要急着跑先做两件事看文件结构、看数据长什么样。unzip -l traffic_lstm.zip head -20 data/traffic_flow.csv这样能快速确认里面到底有几份数据、代码是不是单文件、有没有现成的模型权重。常见的项目解压后会包含下面这些部分我通常会按这个思路重新组织路径内容data/raw/原始客流时间序列通常是csv格式data/processed/重采样、归一化后的序列数据src/preprocess.py数据清洗、滑窗实现src/model.pyLSTM网络结构定义src/train.py训练入口包含参数解析src/predict.py加载权重、预测、输出结果outputs/训练好的模型与预测结果文件如果zip里是几份散落的notebook我建议直接按这个结构搬一遍花的时间远少于后期反复改路径的时间。5.2 train.py需要一套命令行参数notebook调参要用鼠标一个个改工程化的第一步就是让超参数变成命令行选项。这样每次实验跑一条命令就够记录也方便。import argparse from src.model import TrafficLSTMModel from src.preprocess import load_sequence_data def parse_args(): parser argparse.ArgumentParser() parser.add_argument(--data, defaultdata/traffic_flow.csv) parser.add_argument(--seq_len, typeint, default12) parser.add_argument(--horizon, typeint, default1) parser.add_argument(--hidden_size, typeint, default64) parser.add_argument(--num_layers, typeint, default2) parser.add_argument(--lr, typefloat, default1e-3) parser.add_argument(--epochs, typeint, default50) parser.add_argument(--batch_size, typeint, default64) parser.add_argument(--seed, typeint, default42) return parser.parse_args() def main(): args parse_args() set_seed(args.seed) X_train, y_train, X_val, y_val, scaler load_sequence_data( args.data, seq_lenargs.seq_len, horizonargs.horizon ) model TrafficLSTMModel( input_sizeX_train.shape[-1], hidden_sizeargs.hidden_size, num_layersargs.num_layers ) # 训练循环省略和3.3节一致 torch.save({model_state: model.state_dict()}, outputs/model.pth) if __name__ __main__: main()对应的运行命令python train.py --data data/traffic_flow.csv --seq_len 12 --horizon 1 --hidden_size 64 --epochs 50注意把preprocess、model、train拆成三个模块导入的时候不会出现循环依赖。调用load_sequence_data时要在内部把训练段、验证段、测试段分别执行create_sequences不要对全量数据切完再划分那会引入边界泄漏。5.3 加载权重预测次日客流训练完保存的是state_dict而不是整个model对象。state_dict体积小、跨版本兼容好这是工程上的常规做法。预测时重新实例化模型结构再加载权重。ckpt torch.load(outputs/model.pth, map_locationcpu) model TrafficLSTMModel(input_sizeX_test.shape[-1]) model.load_state_dict(ckpt[model_state]) model.eval() with torch.no_grad(): pred_scaled model(torch.FloatTensor(X_test[-1:])) pred scaler.inverse_transform(pred_scaled.numpy()) pd.DataFrame({pred_flow: pred.flatten()}).to_csv( outputs/predictions.csv, indexFalse )预测完必须做inverse_transform把数值从归一化空间还原回真实的客流量单位。很多人在这一步漏了输出的预测值全在0到1之间业务方拿到也没法用。model.eval()和torch.no_grad()要成对出现前者关掉dropout后者关掉梯度计算缺一个都会让预测结果带噪音。5.4 上线前先跑一个基线LSTM不是所有场景的银弹。交通客流单步预测尤其要跟一个最简单的基线比用上一时段的真实客流直接当作下一时段的预测值。这个基线叫naive forecast虽然简单但非常能打。# 把真实测试序列整体平移一位用t时刻的真实值预测t1时刻 baseline_mae np.mean(np.abs(y_test[1:] - y_test[:-1])) lstm_mae mae(y_test.flatten(), pred.flatten()) print(naive MAE:, baseline_mae) print(lstm MAE :, lstm_mae)如果单步预测的LSTM只比naive好一点点说明模型学到的东西有限真正的价值应该体现在多步预测上预测未来1小时、3小时那是naive根本做不到的。不要因为用了神经网络就觉得一定碾压传统方法先把收益点找准模型上线才有说服力。6. 进阶多步预测、外部特征与注意力让客流结果更贴近业务6.1 多步预测先选直接多输出业务方真正关心的是未来1小时甚至更长的客流不是下一个15分钟。多步预测有三种常见做法递归预测是训练一个单步模型拿预测结果继续当输入往里推实现简单但误差会逐级累积直接多输出是修改回归头让输出维度等于horizon一次性给出未来几个时段的预测稳定性最好seq2seq结构适合更长时程但需要更多训练数据。我的做法是数据量不大时先上直接多输出代码改动量最小。6.2 外部特征比调大模型更划算很多LSTM客流预测项目卡在纯客流序列上hidden_size怎么调都不涨点。此时加一列“星期几”往往立竿见影。把星期做one-hot编码拼在每一个时间步的特征末尾模型就能区分工作日和周末的差异。节假日标记、小时的正弦编码、降雨量也可以照同样的方式拼进特征维度。客流不是纯粹的随机序列它是日历、天气和出行习惯共同作用的结果这些外部特征的价值经常被低估。6.3 在LSTM输出后加一个注意力层当序列比较长的时候最后一个时间步的隐状态携带的信息会被早期的关键模式稀释。一个低成本改进是在LSTM输出后加一层注意力让每个时间步学习一个权重加权求和后再过回归头。class AttnLSTM(nn.Module): def __init__(self, input_size, hidden_size, horizon): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.attn nn.Linear(hidden_size, 1) self.head nn.Linear(hidden_size, horizon) def forward(self, x): out, _ self.lstm(x) # (batch, seq_len, hidden) w torch.softmax(self.attn(out), dim1) # 每个时间步一个权重 ctx (out * w).sum(dim1) # 加权聚合 return self.head(ctx) # (batch, horizon)注意力会把早高峰、节假日前夜这类关键时间步的权重放大比单纯加LSTM层数更有效果。我自己跑这类项目最后总会做一件事把最近一周预测误差按早高峰、平峰、晚高峰拆开画成三张图。如果早高峰系统性偏低我不会急着调hidden_size而是先去补节假日和天气特征。多步预测也一样先确认要预测的时间尺度再决定网络结构。希望帮到你。本文还有配套的精品资源点击获取
返回列表