ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

多站点LSTM时序预测的PyTorch实现:共享单车停放数量联合建模实战

多站点LSTM时序预测的PyTorch实现:共享单车停放数量联合建模实战 简介一套基于PyTorch与LSTM的城市共享单车多站点停放数量时序预测系统项目资源。面向深度学习入门者、交通数据分析人员及共享单车运营方解决多站点历史用车序列建模与未来时段预测问题。资源共13个文件约92KB含5个Python脚本覆盖数据解析、LSTM模型定义、训练与评估全流程另有CSV历史数据、已训练模型权重、Markdown说明、TXT注释及附赠Word文档可直接运行或二次开发。目前已有30人学习下载。模型利用LSTM门控机制缓解梯度消失适合非平稳多变量序列配套文档说明数据集字段、参数设置与训练注意事项。通过研读代码可完整复现数据预处理、模型构建、参数调优到预测输出链路掌握PyTorch动态计算图开发也可基于现有权重验证多站点预测或替换数据迁移实验为共享单车智能调度与城市规划提供量化参考。1. 多站点时序预测的本质单车停放数量不是单条曲线问题共享单车停放数量预测这个需求落到工程上就是经典的时序回归问题用 PyTorch 搭 LSTM 建模也是网上最常见的套路。但真正让这类项目翻车的不是网络结构而是“多站点”三个字。一个城市的单车停放数据往往来自几十到上百个站点每个站点都有自己独立的潮汐规律写字楼站点工作日早晚高峰两个波峰居民区站点晚上和早晨反向波动学校站点节假日直接归零。如果把所有站点当成一条序列训练模型做出来的预测会趋向于“平均站点”每个站都预测得不准。这里的关键不是把 LSTM 调得多深而是先想清楚数据如何组织、按什么粒度建模、单站点与全局模型各自适合什么场景。这篇笔记会从建模思路、数据处理、PyTorch 实现、参数取舍和踩坑记录一路讲完适合做共享单车调度预测、短时交通流预测以及类似的金融时序预测迁移场景的从业者。2. LSTM 在共享单车多站点预测里的建模思路为什么用一个网络训练所有站点2.1 为什么不用 ARIMA 或给每个站点各训一个模型共享单车停放数量有一个非常明显的特征强周期 强扰动。日周期、周周期都很稳定但天气突变、市政活动、节假日会让序列在某个时间点突然偏离常态。ARIMA 类统计模型能抓住周期但对这类外部扰动几乎没有建模能力因为它本质上是把过去值的线性组合外推到未来突发变化会被当成噪声抹平。另一个常见做法是给每个站点单独训一个 LSTM。站点少的时候没问题但站点一旦到几十个就会暴露两个现实问题一是大部分站点日租还量只有几十辆单站点样本量撑不起一个有泛化能力的循环网络训练集 loss 很低验证集一塌糊涂二是维护成本失控新增一个站点就要重新训练一套模型线上部署时还要管理几十个权重文件。所以工业场景里的共享单车停车数量预测主流做法是用一个模型吃下所有站点的历史数据输入和输出都带站点维度。网络同时看到所有站点的演变轨迹就能学到“一个典型的市中心站点在雨天会怎样变化”这类跨站点共性的规律而不是死记某一条曲线的形状。2.2 多站点联合建模的取舍共享稀疏性、平滑噪声、可扩展多站点联合建模的第一好处是共享稀疏性。冷启动站点只有几个月数据单独训练必然过拟合但放进全局模型后它可以借助其他站点学到的周期特征快速收敛相当于迁移学习的雏形。第二个好处是噪声平滑把几十条序列放在一起算梯度单站点的异常波动对整体参数更新的影响被稀释模型更稳。代价是模型必须有能力区分站点差异否则会退化成“平均预测”。解决办法是在数据层面按站点分别归一化而不是全局做一次缩放在模型层面则要让每个时间步的输入是一个包含全部站点数值的向量。这样 LSTM 在每个时间步看到的是全站点的截面状态输出头再一次性预测所有站点的未来数值。这种设计的另一个隐性好处是扩展性。新站点加入时只需要把矩阵新增一列用已有权重继续 fine-tune 几步就能上线不需要重写数据处理逻辑也不需要重训整个模型体系。2.3 样本量怎么算日粒度还是小时粒度数据粒度决定了这个项目能不能做成而不是 LSTM 层数。以天为粒度一年只有 365 个时间点滑窗 28 天能切出 300 多个训练样本对多站点联合模型来说偏少但勉强可用以小时为粒度一年有 8760 个点样本量充足可以抓到早晚高峰的精细变化但噪声也大模型要额外学习一天内不同时段的波动形态。我一般按预测需求反推粒度。如果是给调度团队做次日车辆调运日粒度加 7 天预测窗口就够用如果是做站点潮汐预警、需要知道某个站点晚上 8 点会不会淤积那就必须用小时粒度。小时粒度模型的输入特征里最好显式加入“当天是星期几”和“当前是一天中的第几个小时”因为 LSTM 虽然能自己学周期但加上时间索引可以让它在数据量不足时收敛更快更稳。样本量的经验判断是小时粒度下单站点至少要有 3 个月连续数据联合模型的总样本数 滑动窗口切出的窗口数量这个数量级在几千到几万都属正常。低于这个量级时把预测粒度调粗比加复杂模型更实际。3. 用 PyTorch 搭建 LSTM 多站点预测数据管道与模型定义3.1 把 CSV 变成站点×时间矩阵pivot 与缺失值处理原始数据通常是长表格式每一行是一条记录包含站点编号、时间戳和停放数量。第一步是把长表转成宽表行为时间、列为站点这样后续滑窗和模型输入都清晰。缺失值处理上连续缺失不超过 2 个时间点可以用线性插值超过 2 个的时间段直接置空并在构造样本时跳过千万不要用整列均值填充那会把站点个性抹掉。import pandas as pd import numpy as np df pd.read_csv(bike_usage.csv, parse_dates[timestamp]) # 长表转宽表行是时间列是站点 matrix df.pivot_table( indextimestamp, columnsstation_id, valuescount, aggfuncsum ).sort_index() # 阈值内的缺失线性插值超过阈值的置 NaN 后整段丢弃 matrix matrix.interpolate(limit2, limit_areainside) matrix matrix.dropna(axis0, howany) # 保留完整站点缺失率超过 30% 的站点直接剔除 valid_stations matrix.columns[matrix.isna().mean() 0.3].tolist() matrix matrix[valid_stations] print(matrix.shape)逻辑说明pivot_table 里 aggfunc 用 sum 是因为同一站点在同一时间戳可能出现多条记录需要聚合interpolate 只处理内部缺失序列首尾的缺口不补dropna(axis0, howany) 保证每条样本的所有站点数值完备避免训练时出现 NaN 传播。valid_stations 的过滤是防止一个几乎没数据的站点拖累全局归一化。3.2 按站点归一化 滑窗采样Dataset 怎么写归一化是数据管道里最重要的一步。停车数量分布极度不均匀热门站点日均 300 辆冷门站点可能只有 3 辆。全局 MinMax 会把冷门站点压缩到接近 0LSTM 几乎学不到它们的信号。正确做法是每个站点单独做 MinMaxScaler把每个站点的数值映射到 [0,1]预测完再反归一化回原始数量。import torch from torch.utils.data import Dataset from sklearn.preprocessing import MinMaxScaler def fit_scalers(matrix): scalers {} scaled np.zeros_like(matrix, dtypenp.float32) for i, col in enumerate(matrix.columns): s MinMaxScaler(feature_range(0, 1)) scaled[:, i] s.fit_transform(matrix[col].values.reshape(-1, 1)).ravel() scalers[col] s return scaled, scalers class MultiSiteDataset(Dataset): def __init__(self, scaled, window28, horizon7): self.window window self.horizon horizon # 输入特征和预测目标都来自同一矩阵但时间段错开 self.X, self.y [], [] for i in range(len(scaled) - window - horizon 1): self.X.append(scaled[i : i window]) self.y.append(scaled[i window : i window horizon]) self.X torch.tensor(np.array(self.X), dtypetorch.float32) self.y torch.tensor(np.array(self.y), dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx]逻辑说明fit_scalers 按列拟合每列的缩放参数只由该站点自己的历史数据决定这样模型输入里每个维度都处于可比尺度又保留了站点间相对差异。MultiSiteDataset 每次返回一个形状为 (window, n_station) 的输入和一个形状为 (horizon, n_station) 的目标网络要学的是从过去 28 天所有站点的状态映射到未来 7 天所有站点的状态。注意点时序数据的 train/test 切分必须按时间顺序不能用随机划分。通常按时间取前 80% 做训练、后 20% 做验证并且验证集的归一化参数必须来自训练集的 scaler不能重新 fit。这个坑在第五章还会展开。3.3 LSTM 模型定义input_size 为什么等于站点数模型结构是这套方案的核心。既然前面把输入做成了多站点向量LSTM 的 input_size 就应该是站点数而不是 1。每个时间步 LSTM 接收一个向量向量的每个维度对应一个站点的当日数值hidden state 在两个时间步之间传递的是“整个城市停放状态的演变”信息。这样设计后站点之间的相关性是模型天然学习的对象而不是人工强加的特征。import torch.nn as nn class MultiSiteLSTM(nn.Module): def __init__(self, n_station, hidden_size64, num_layers2, horizon7, dropout0.2): super().__init__() self.n_station n_station self.horizon horizon self.lstm nn.LSTM( input_sizen_station, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.head nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Linear(hidden_size // 2, horizon * n_station) ) def forward(self, x): # x: (batch, window, n_station) lstm_out, _ self.lstm(x) # 取最后一个时间步的 hidden state last_hidden lstm_out[:, -1, :] # (batch, hidden_size) out self.head(last_hidden) # (batch, horizon * n_station) return out.view(-1, self.horizon, self.n_station)逻辑说明nn.LSTM 的 batch_firstTrue 表示输入形状是 (batch, seq_len, input_size)这里 seq_len 对应滑窗长度input_size 对应站点数。取 lstm_out[:, -1, :] 是拿最后一个时间步的输出作为整段序列的编码再接全连接头一次性输出未来 horizon 步、每步 n_station 个数值。参数说明hidden_size 控制网络的记忆容量64 对几十个站点通常够用num_layers2 能捕捉更抽象的时序模式但层数超过 2 在数据量不大时收益有限还容易过拟合dropout 只加在多层 LSTM 的层间单层 LSTM 加 dropout 会破坏记忆传递。预测头用了一个带 ReLU 的两层全连接给非线性映射留空间但如果你的数据噪声很大也可以直接用一个 Linear 层更不容易过拟合。3.4 训练主循环里不能省的五件事训练循环是新手最容易忽略细节的地方。很多人直接 for epoch 套 batch 就算完结果 loss 曲线反复震荡或者训到一半 loss 变成 NaN。下面这个训练函数收敛了常见的最佳实践。import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau def train_model(model, train_loader, val_loader, epochs60, lr1e-3): torch.manual_seed(42) np.random.seed(42) optimizer optim.AdamW(model.parameters(), lrlr, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience8) criterion nn.HuberLoss(delta1.0) best_loss float(inf) best_state None wait 0 for epoch in range(epochs): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() # 梯度裁剪防止循环网络梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * x_batch.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred model(x_batch) val_loss criterion(pred, y_batch).item() * x_batch.size(0) val_loss / len(val_loader.dataset) scheduler.step(val_loss) # 早停连续 12 轮验证 loss 不降就停 if val_loss best_loss: best_loss val_loss best_state {k: v.clone() for k, v in model.state_dict().items()} wait 0 else: wait 1 if wait 12: break model.load_state_dict(best_state) return model逻辑说明clip_grad_norm 是 LSTM 训练的必备项循环网络在长序列上反向传播时梯度范数很容易超过 1不裁剪会出现 loss 突然跳到 NaN。ReduceLROnPlateau 在验证 loss 不降时把学习率减半比固定学习率训到底更稳。早停保存的是验证集上最优的权重不是最后一轮的权重因为最后一轮很可能已经过拟合。参数说明HuberLoss 比 MSELoss 对极端值更鲁棒。停放数量序列里偶尔会有异常峰值暴雨前的集中还车MSE 会把 loss 抬高几个数量级导致训练震荡Huber 在误差大时退化为线性损失梯度更温和。weight_decay 给 LSTM 加上 L2 正则建议从 1e-4 开始调。4. 训练配置与多步预测参数取舍窗口、批次、学习率与未来步长4.1 滑窗、horizon、batch 与站点数之间的张力这几个参数不是独立调的它们互相挤占。滑窗长度决定了模型能看到多长的历史horizon 决定了要预测多远batch 大小影响梯度稳定性和显存占用站点数则固定了输入向量的维度。下面是我常用的经验参数表按日粒度数据整理。参数推荐范围说明window_size7 ~ 28日粒度数据用 14 或 28 天小时粒度数据可以到 48 ~ 168horizon1 ~ 7预测超过 7 天时中期预测会明显退化hidden_size32 ~ 128站点数在 50 以内用 64超过 100 可考虑 128batch_size32 ~ 256样本数少时 batch 小一点避免梯度过于平滑num_layers1 ~ 2数据量低于 1 万样本用单层更稳learning_rate1e-4 ~ 3e-3多站点联合模型建议从 1e-3 起调batch 与站点数的关系是输入每个样本的形状是 (window, n_station)如果站点数上百每步输入的向量就是上百维LSTM 的参数量也随之增大此时 hidden_size 和 batch 都要往低调否则显存和过拟合风险同时上升。多站点场景下 batch 还有一个选择每次迭代只采样部分站点而不是全部站点。这在站点数特别多时能显著降低显存压力但代价是模型在一个 batch 内看不到完整的城市空间截面。我的经验是 100 个站点以内直接用全部站点超过 200 个再考虑按站点采样。4.2 优化器、学习率调度与损失函数的选择逻辑AdamW 和 Adam 的差别在于 weight_decay 的实现方式AdamW 把权重衰减和梯度更新解耦正则效果更干净是当前训练循环网络的主流选择。学习率调度上ReduceLROnPlateau 比 CosineAnnealingLR 更适合时序预测因为时序任务的验证 loss 曲线通常不是平滑下降的Cosine 的周期性容易在 loss 上升阶段继续大步长更新。损失函数的选择要看数据分布。停车数量序列是长尾的大多数站点数值很小少数热门站点数值很大。MSELoss 对热门站点的误差惩罚呈平方放大训练过程会过度关注那两三个大站。HuberLoss 的 delta 参数控制从平方损失过渡到线性损失的阈值delta1.0 时误差小于 1 的样本用平方惩罚大于 1 的样本用线性惩罚既保留了梯度平滑性又不会让极端站点主导训练。如果调度需求是“不要预测得太离谱”也可以考虑 MAE如果要做不确定性估计就得分位数损失这个在第六章展开。4.3 反归一化与无偏评估指标必须在原始尺度算训练 loss 是在归一化后的空间计算的数值很小不代表真实预测精度好。评估时要把预测结果反归一化回原始数量再计算 MAE、RMSE 这类指标否则冷门站点和热门站点的误差权重是失真的。def inverse_transform(pred_norm, scalers, stations): # pred_norm: (batch, horizon, n_station) pred_raw np.zeros_like(pred_norm) for i, col in enumerate(stations): s scalers[col] pred_raw[:, :, i] s.inverse_transform( pred_norm[:, :, i].reshape(-1, 1) ).reshape(-1, pred_norm.shape[1]) return pred_raw逻辑说明反归一化是按站点逐个做的每个站点用自己的 scaler 还原。聚合总误差时也要先还原到原始尺度再算而不是把归一化空间里的误差直接平均。评估指标还有一个常用细节MAPE 在低流量站点上会爆炸。某个站点真实值只有 2预测值 4MAPE 就是 100%而同一个误差发生在真实值 200 的站点上只有 2%。所以多站点评估建议分组看按站点日均流量把站点分成高、中、低三档分别统计 MAE这样哪个档位预测差一目了然也方便定向优化。5. 多站点 LSTM 预测避坑指南五个让模型翻车的细节5.1 现象随机打乱数据后训练 loss 下降正常但实测一塌糊涂原因时间序列的样本之间不是独立的。如果把滑窗后的样本随机 shuffle 进训练集和验证集测试集里会出现“未来数据训练过”的情况这属于典型的标签泄露。时序预测的评估目标是预测未来一旦未来混进训练集loss 就会失真。解决切分必须按时间顺序。常见做法是按时间取前 80% 做训练、后 20% 做验证并且确保验证集的最小时间戳大于训练集的最大时间戳。shuffle 只允许发生在训练集内部而且实际收益有限我的习惯是不 shuffle 时序数据让模型严格按时间顺序消化样本。5.2 现象冷门站点的预测值几乎恒定为该站点的历史均值原因全局 MinMax 归一化。所有站点共用一个缩放器时热门站点的数值范围可能是 0 到 300冷门站点是 0 到 5缩放后热门站点的差异占据了绝大部分数值空间模型完全感知不到冷门站点的波动。解决按站点独立归一化这正是前面代码里 fit_scalers 的做法。另一个辅助手段是 loss 按站点加权给低流量站点更高的权重但这一步要在按站点归一化之后才有意义。优先做归一化站点加权只在你确认某个站点群是业务重点时再用。5.3 现象无论输入什么模型输出都趋向于近期均值原因这是多个因素叠加的结果。最常见的是数据不平稳比如站点因为周边施工出现整体数量漂移LSTM 对这种趋势性变化反应滞后其次是 loss 函数和正则太强把模型压得过于保守预测值向均值收缩还有可能是滑窗太长模型在大量历史信息面前找不到真正关键的近期信号。解决先检查数据平稳性如果序列有明显趋势对序列做一阶差分后再训练预测完再加回差分其次调低 weight_decay观察预测方差是否变大最后缩短滑窗到 14 天或 7 天让近期信号在输入中占比更高。不要一上来就加双向 LSTM 或 Attention这些在单序列预测里很少是根因。5.4 现象平时预测还行一到节假日误差突然翻倍原因纯数值序列的 LSTM 只能从历史数值里推断规律节假日这种“时间索引特殊 行为模式突变”的情况它只能靠相邻年份同一天的记忆数据量不够时就只能猜。解决把日期特征作为额外输入拼进每个时间步。具体做法是在 (window, n_station) 的基础上在每个时间步后面拼接“是否周末”“是否节假日”“星期几”三个特征输入维度变成 n_station 3。这个改动一般能让节假日预测误差下降 20% 以上是这类系统中性价比最高的特征工程。5.5 现象预测长度越长后期的预测越像一条平线原因多步预测有两条路线。直接预测是模型一次输出未来 7 天训练时它已经看到中间时刻的误差反馈递归预测是用 t1 的预测值作为 t2 的输入误差会随步长累积预测越长越失真。如果你用的显式递归推理平线几乎必然出现。解决默认用直接多步输出也就是前文模型里定义的 (batch, horizon, n_station) 结构。如果业务确实需要超长预测把 horizon 拆成几段分段训练三个模型分别预测 1-3 天、4-7 天、8-14 天比一个模型硬撑 14 天可靠得多。6. 预测结果的检验与进阶损失值之外你要看的东西单一 MAE 值无法证明这个系统能上线我一般做三件事。第一按站点日均流量分桶看误差如果高流量站点 MAE 占比异常高说明模型在“和稀泥”优先检查归一化和损失函数如果低流量站点误差大优先加日期特征。第二抽样画真实值 vs 预测值的曲线重点看波峰和波谷是否对齐错位一个时间步比数值偏差更容易被曲线图发现。第三看预测是否具备业务可用性——对调度来说点预测只告诉你会淤积多少辆车但“50 到 80 辆”这个区间比“60 辆”这个单点更有决策价值。进阶方向是把 LSTM 的输出头改成三个分位数头分别预测 10%、50%、90% 分位数损失函数用分位数损失这样模型直接输出预测区间。改动量不大只涉及模型输出维度和损失计算但调度团队可以据此判断“是否需要提前调车”实用性上了一个台阶。如果以后数据量大了可以对比一下把 LSTM 编码器替换成 Transformer 编码器的效果。Transformer 在长序列上能捕捉更远距离的依赖但它需要的数据量也更大样本量低于一万时LSTM 往往表现得比 Transformer 更稳。我做这类预测项目的习惯是第一版永远只用最朴素的结构把数据管道的坑全部踩平再谈加复杂模块。多站点 LSTM 的价值不在模型花哨而在于数据组织和评估方式是否对得起业务诉求。希望帮到你。本文还有配套的精品资源点击获取
返回列表