ARTICLE DETAIL

资讯详情

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

共享单车需求预测与智能调度:从LSTM到运筹优化实战

共享单车需求预测与智能调度:从LSTM到运筹优化实战 简介这是一套面向毕业设计、期末大作业和课程案例的共享单车预测与调度实战源码包系统展示如何用深度学习处理真实业务问题。项目从数据层出发覆盖Geohash解码、区域划分与POI分析、多表合并、训练测试集生成等完整预处理链路随后构建LSTM/BP神经网络等预测模型配合误差统计与评估脚本实现量化分析最后引入蚁群算法完成动态调度优化形成从数据清洗到决策输出的全过程。资源共15个文件其中11个Python脚本承担数据加工、模型训练、预测评估与智能调度等核心功能4个npy文件保存输入输出数组压缩包仅516KB结构紧凑、便于移植。已有175人学习适合需要快速搭建深度学习项目原型的高校学生与算法入门者。通过研读源码可直接复用其数据管道和模型训练框架并理解预测结果如何落到车辆调度策略中。项目按数据预处理、模型构建、误差评估、调度优化分模块组织每个步骤均有对应脚本便于逐步对照学习。1. 共享单车调度难题本质是「预测」与「运力分配」的耦合周一早高峰的 8 点 15 分地铁站 B 口对面的单车停放区已经空了而在 400 米外的写字楼下车辆从昨晚就一直淤积到无法还车。这类潮汐现象的核心矛盾不是单车总量不够而是「不知道下一小时哪里缺、哪里溢」与「知道了也来不及搬」两个问题的叠加。基于深度学习的共享单车预测与调度解决方案做的就是先用历史订单、天气和站点属性预测未来数小时的借还需求再把预测结果换算成可执行的调度任务交给调度车和运维人员。这套思路适用于正在搭建城市骑行运营系统的数据工程师、算法工程师也适合已经看了很久报表、却始终调不动车的业务团队你会看到深度学习的价值不只在精度数字上更在于它是否能让调度动作提前发生。2. 共享单车预测的数据口径与特征工程2.1 先定预测粒度站点级、小时级、多步滚动要预测的不是全市总骑行量而是落到站点、落到未来 4 到 6 小时的借还数。调度车的搬运动作至少需要提前一小时下达否则车在路上时站点已经空掉而如果只看全天总量你无法判断该从某个站点调出 2 辆车还是 20 辆车。所以行业里常见的做法是以站点为最小单位用 15 分钟或 1 小时作为预测粒度一次性输出未来多步的结果。15 分钟粒度对实时调度更细但很多站点在夜间和凌晨的订单几乎为 0稀疏数据会放大模型的波动1 小时粒度更平滑和调度任务的执行周期也匹配。我一般会同时产出 15 分钟与 1 小时两套预测训练和推理用 15 分钟序列等到生成调度任务时再聚合到 1 小时。这样既保留了站点在半小时内的突发波动又不会让调度人员面对碎到没法执行的任务清单。调度响应窗口则要和运维排班对齐。一线城市要求 4 小时以内的响应二线城市通常可以放到 6 到 8 小时。预测步长跟随这个窗口比如每 15 分钟一个点输出未来 24 个点正好 6 小时。窗口再长误差会迅速变大调度计划反而失去参考意义。2.2 特征工程时间、天气、历史骑行与站点关系共享单车需求的特征可以分成四类时间特征、历史需求特征、天气特征、空间关系特征。不需要一次全部堆进去但下面这些字段在落地项目中几乎都要出现特征分组具体字段说明数据粒度时间特征小时、星期、是否工作日、是否节假日捕捉通勤与休闲的周期差异每个预测时刻历史需求过去 24 小时借还量、过去 7 天同一时刻借还量让模型看到短期趋势与同期水平站点 × 时刻天气特征温度、降水概率、降水量、风力、空气质量雨天和降温对骑行影响最直接小时级全局数据空间特征站点周边 500 米站点密度、临近站点的净需求捕捉潮汐迁移方向站点级静态动态天气数据有一个容易被忽略的细节人们对天气的反应是滞后且非线性的。降水开始后的前 15 到 30 分钟借车量反而会短暂升高因为很多人是看到快下雨才急着骑走随后需求才迅速跌入低谷。因此模型输入不能只给当前时刻的天气最好把「未来 1 小时降水强度」也作为特征让网络提前看到降雨的进入与退出。站点周边信息也值得花时间整理。比如一个站点离地铁口 30 米还是 200 米早高峰的净需求方向很可能相反站点附近有没有常驻的演出场馆、大型超市也会让周末需求形态完全不同。用站点经纬度和 POI 数据做聚类再把聚类编号作为类别特征喂给模型是我常用的低成本做法。2.3 把原始订单整理成监督学习样本原始订单表通常只有几列订单号、车辆号、开始时间、结束时间、开始站点、结束站点。第一步是把每条记录拆成一次借出和一次归还再按站点和整点聚合。import pandas as pd import numpy as np orders pd.read_csv(orders.csv, parse_dates[start_time, end_time]) orders[start_hour] orders[start_time].dt.floor(H) orders[end_hour] orders[end_time].dt.floor(H) # 借出量按「开始站点 开始小时」计数 borrow orders.groupby([start_station, start_hour]).size().rename(borrow) # 归还量按「结束站点 结束小时」计数 return_cnt orders.groupby([end_station, end_hour]).size().rename(return_cnt) series pd.concat([borrow, return_cnt], axis1).fillna(0).sort_index() series[net] series[borrow] - series[return_cnt] weather pd.read_csv(weather.csv, parse_dates[time]).set_index(time) df series.join(weather, onstart_hour).sort_index()floor(H)是 pandas 里把时间截断到整小时的常用写法保证所有记录落在同一个时间桶里。groupby分别聚合借出和归还再 concat 成一张宽表比先合并再分组更清晰也不会把借出与归还记录相互污染。接下来把宽表转成滑窗样本。每一步输入过去history个时间点输出未来horizon个时间点的借车量def to_sequences(df, history72, horizon6, step1): X, y, meta [], [], [] for sid, g in df.groupby(start_station): g g.sort_index() feat_cols [c for c in g.columns if c ! start_station] arr g[feat_cols].values for i in range(history, len(arr) - horizon, step): X.append(arr[i - history:i]) y.append(arr[i:i horizon, feat_cols.index(borrow)]) meta.append((sid, g.index[i horizon - 1])) return np.stack(X), np.stack(y), metahistory72表示用过去 72 小时作为上下文对小时级数据就是 3 天horizon6表示输出未来 6 小时。若用 15 分钟粒度这两个值要对应改成 288 和 24。step1会让相邻样本大量重叠好处是样本量足坏处是训练慢、数据冗余高站点多时可以加大到 4 或 8。注意划分训练集和验证集时只能按时间切不能随机抽样。随机抽样会把未来信息混进训练集验证指标会虚高上线后立刻暴露。3. 共享单车时空预测模型从基线到 LSTM 再到新架构3.1 先跑通基线再谈深度学习任何深度学习网络上场之前我一般先跑一个 LightGBM 或 XGBoost 基线特征就用 2.2 节里准备好的那几类。为什么共享单车订单是强周期、强天气响应、有长期趋势的行为序列深度学习能带来的提升主要在长周期依赖与冷启动站点但训练成本和调参成本也高。如果基线已经做到整体 MAPE 15% 以下深度学习的价值就应该体现在早晚高峰峰值的误差改善上而不是盲目追求整体损失数字变低。踩过几次坑之后我对模型选型的判断是数据量少于 3 个月、站点少于 50 个直接用树模型更省事数据跨了春夏秋冬、站点上千深度学习才真正划算。树模型也能做滑窗特征但它对序列的时间位置不敏感长距离依赖要靠人工滞后特征去补滞后窗口一不小心就把特征维度撑爆。LSTM 这类循环网络把时间结构内置进参数天然更合适。Transformer 这两年也成为选项它的注意力机制能直接建模长距离依赖但对站点的冷启动和缺失数据更敏感训练周期也更长。这一两年出现的状态空间模型 Mamba则试图在长序列场景里替代 Transformer 的注意力结构降低推理开销在共享单车这类多站点长序列预测里已经有从业者在尝试核心思路仍是控制长序列建模的成本。基线没跑通之前这些新架构都可以先不碰。3.2 用 PyTorch 搭一个可落地的 LSTM 预测模型在明确特征工程之后模型结构可以保持简单。下面是一个可训练的单站点 LSTM 示例输入形状是(batch, history, n_features)输出是未来 6 小时的借车量。import torch import torch.nn as nn class BikeDemandLSTM(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, dropout0.2, out_horizon6): super().__init__() self.lstm nn.LSTM(input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.GELU(), nn.Dropout(0.1), nn.Linear(32, out_horizon) ) def forward(self, x): out, _ self.lstm(x) last_hidden out[:, -1, :] return self.regressor(last_hidden)hidden_size64是隐藏状态维度决定网络容量站点特征越复杂可以提到 128但太小欠拟合、太大过拟合。num_layers2是大多数时序任务的合理起点堆到 3 层以上收益有限且收敛变慢。batch_firstTrue让输入形状是(batch, time, feature)与 pandas 的组织方式一致不容易搞混维度。forward里取out[:, -1, :]也就是最后一个时间步的隐藏状态再接 MLP 输出 6 步预测。这种直接多步输出的方式比「先预测一步、再把结果当作输入滚动预测」更稳后者会把误差一步步放大。训练时我常用 HuberLoss 而不是 MSE因为单车数据里偶尔会有大型活动带来的离群需求Huber 对离群的惩罚相对温和模型不会为了压住一两个极端点而扭曲整体拟合from torch.utils.data import TensorDataset, DataLoader train_ds TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) loader DataLoader(train_ds, batch_size256, shuffleTrue) model BikeDemandLSTM(n_featuresX_train.shape[-1]) optimizer torch.optim.AdamW(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) loss_fn nn.HuberLoss(delta1.0) for epoch in range(30): model.train() for xb, yb in loader: optimizer.zero_grad() loss loss_fn(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() # 每个 epoch 结束用按时间切分的验证集计算 MAE做早停AdamW配合CosineAnnealingLR是现在训练这类中型网络的常见组合学习率会从初始值平滑退火减少收敛后期震荡。clip_grad_norm_把梯度范数限制在 5.0 以内防止 LSTM 训练中常见的梯度爆炸。shuffleTrue只用于训练集内部验证集和测试集永远按时间顺序送入。3.3 训练、验证与参数选择的几个关键点多站点联合训练时一个最容易踩的坑是把所有站点拼在一起却不区分站点身份。不同站点的需求量级差异很大地铁站日借还 500 次居民区可能只有 50 次模型会偏向大站。解决方法是给每个站点一个可学习的 embedding拼到每个时间步的特征里让网络学会「同样 30 辆的净需求在不同站点含义不同」。超参数常用区间调整说明hidden_size32 - 128站点多、特征多取上限数据量小取下限num_layers1 - 3超过 3 层收益递减且容易过拟合dropout0.1 - 0.3数据量大可以降到 0.1否则保持 0.3learning_rate1e-4 - 3e-3用余弦退火时从 1e-3 起步比较稳history 窗口24 - 168 小时与天气周期和调度响应窗口匹配验证时不要只看整体 MAE一定要按「站点 × 小时」切片对比。很多模型的整体误差看起来不错实际是夜间大量零值拉低了平均线早高峰的误差却完全不可用。这个问题在后续调度验证中会被放大因为调度任务恰恰集中在高峰前后。4. 从预测到调度任务生成与运力分配4.1 把逐站点净需求转成调度任务模型输出的是每个站点未来若干小时的借车量和归还量。调度关心的是净变化如果预测借出远多于归还站点会空反之会积压。实际做调度任务时不能只看预测净需求还要叠加当前在桩车辆数和桩位容量。一个站点即使预测接下来会被借走 50 辆如果它当前有 80 辆在库也还不需要立刻补给。常用的计算公式是gap_pred borrow_pred - return_pred current_stock - capacity * reserve_ratiocurrent_stock是当前停在桩上的车辆数capacity是站点总桩位数reserve_ratio是预留的安全还车比例一般取 0.2。当gap_pred大于正阈值说明车辆即将溢出需要调出小于负阈值说明即将缺车需要调入。用代码表达merged[net_pred] merged[borrow_pred] - merged[return_pred] merged[gap] (merged[net_pred] merged[stock] - merged[capacity] * 0.2) THRESHOLD 8 pickup merged[merged[gap] THRESHOLD].copy() deliver merged[merged[gap] -THRESHOLD].copy()THRESHOLD是关键参数。阈值太小时任务量大调度车一趟只搬几辆成本极高太大则站点已经空了才触发任务失去调度意义。常见取值范围是 3 到 10 辆具体要看调度车的单趟装载上限与人力成本。业务上还需要过滤掉那些预测值本身就不可信的站点比如历史数据不足 7 天的新站点直接用规则补车而不是硬套模型。4.2 调度运筹化最小化缺车与搬运成本有了待调出点和待调入点之后下一步是把运力分配给具体站点。一个调出点可以补给多个调入点一辆调度车也可以跑多个站点这本质上是一个带容量约束的运输问题。成本包含两部分车辆搬运的里程成本和单次出车的人力成本。用最小成本流求解是这一步最常见的做法。from scipy.optimize import linprog import numpy as np # cost[i][j] 表示从调出点 i 运一辆车到调入点 j 的折算成本 c cost_matrix.reshape(-1) # 约束1每个调出点运出的总车辆数 可调出车辆数 A_ub np.zeros((n_pick n_deliver, n_pick * n_deliver)) for i in range(n_pick): A_ub[i, i * n_deliver:(i 1) * n_deliver] 1 # 约束2每个调入点收到的总车辆数 需求车辆数 for j in range(n_deliver): A_ub[n_pick j, j::n_deliver] 1 b_ub np.concatenate([pickup_amounts, deliver_amounts]) res linprog(c, A_ubA_ub, b_ubb_ub, bounds(0, None), methodhighs) flow res.x.reshape(n_pick, n_deliver)A_ub矩阵前n_pick行约束每个调出点的运出总量后n_deliver行约束每个调入点的接收总量。bounds(0, None)保证每个站点对之间的调运量不为负。methodhighs是 SciPy 1.6 之后默认推荐的线性规划求解器速度和稳定性都比旧方法好。这个模型适合站点对之间直达、单趟只服务一个调入点的场景。如果调度车需要一趟串多个站点就要升级为带时间窗的车辆路径问题这时通常切换到 OR-Tools 的 routing 库。更复杂的情况是把预测不确定性也放进去用随机规划或滚动优化每次只执行未来一小时的调度单到点后重新预测再规划。4.3 离线计划与在线微调的结合方式实际运营中调度任务往往分两层离线层提前 6 小时生成一次粗粒度计划把调度车从早上 6 点开始的人力排出来在线层每 15 到 30 分钟重新预测一次把临时订单和突发天气带来的新需求插入到剩余任务里。两层之间要有一个去重机制记录已经下发的任务避免同一个站点在 30 分钟内被重复下两次单。站点规模上来之后模型推理与调度任务生成会变成典型的批处理任务按站点分片并行计算由调度引擎统一管理执行队列对失败任务进行重试。这一层与集群调度的基础设施逻辑是相通的任务超时、幂等去重、优先级抢占都是同一套思路。如果公司已有海豚调度器这类的任务编排平台把每日预测、预测回放和调度任务生成串成工作流会比在单机脚本里堆积要省心得多。5. 预测与调度上线的验证方法不要只看整体误差5.1 按站点和时段切片看误差整体 MAE 会掩盖高峰误差而调度预算恰恰花在高峰时段。上线后我习惯把预测结果和真实值按站点、按小时做透视找出误差最大的站点组合。valid[error] valid[y_pred] - valid[y_true] summary (valid.groupby([station, valid[hour].dt.hour]) .apply(lambda d: np.sqrt(np.mean(d[error] ** 2)), include_groupsFalse) .rename(rmse) .reset_index()) worst summary.sort_values(rmse, ascendingFalse).head(10)error是有方向的正值表示预测偏多负值表示预测偏少。调度上更怕的是负误差也就是预测说站点够用实际却空了。worst表格里如果看到大量早高峰站点说明模型对峰值估计系统性偏低这时可以检查两点损失函数是否对高峰样本做了加权以及天气特征是否使用了「未来时段降水强度」而不是当前值。另一个常见修正方式是改用分位数损失让模型直接输出 P90 的预测值把安全余量放进调度参数里。5.2 预测回放与极端天气事件告警每天凌晨用当前模型重新预测一遍昨天第二天与真实值对齐形成预测回放报表。回放能持续发现两种问题一是特征漂移比如某站点周边开了新小区需求整体上升模型还停留在旧分布上二是天气极值比如暴雨、台风这类样本在训练集里很少模型会明显失效。回放表一般保留最近 30 天按站点计算连续误差replay pred_table.merge(actual_table, on[station, ts]) replay[error] replay[pred] - replay[actual] # 连续 3 天同方向误差超过 10 辆的站点优先排查业务原因 err_station_day (replay.groupby([station, replay[ts].dt.date]) .apply(lambda d: d[error].mean(), include_groupsFalse) .rename(daily_err) .reset_index()) err_station_day[sign] np.sign(err_station_day[daily_err]) consecutive (err_station_day.groupby([station, sign]) .apply(lambda d: d[ts].nunique(), include_groupsFalse))出现连续同向偏差时我一半以上的排查经验都指向业务变化而不是模型参数出了问题地铁口临时围蔽、站点附近学校放暑假、周边主路施工都会让一个站点的需求结构发生不可逆的改变。这类变化用再复杂的网络也学不出来正确做法是手动把站点标记为「业务变更」调整它的历史数据权重。5.3 从残差归因到阈值微调调度系统的整体效果不只看预测精度还要看调度车执行后的站点服务水平。缺车率、还车失败率、调度车单趟搬运量这三个指标比 MAE 更能反映业务结果。当预测回放显示某类天气下误差偏高时可以针对性地微调调度参数。调整项影响方向什么时候调调度触发阈值阈值调大任务变少、单车趟次效率高调度车运力紧张时预测响应窗口窗口拉长准备时间变多但误差增大夜间、凌晨需求平稳时安全桩位比例比例调高还车成功率升但可调度车辆变少写字楼区域晚高峰前高峰时段误差权重权重调高模型更贴合峰值但整体误差可能上升早高峰缺车投诉集中时最后分享一个调试顺序遇到效果变差先跑 5.2 的预测回放把误差样本按天气、节假日、站点类型分组如果雨天的误差比晴天高一倍就去把天气特征细化比如把中雨和大雨拆成两档或加入连续降雨天数。这一条排查路径通常比反复调学习率更快见效。本文还有配套的精品资源点击获取
返回列表