共享单车调度难,本质上是"需求预测"没有解决。站点潮汐效应明显,早高峰写字楼区域爆满,晚高峰又空到一车难求,调度车跑来跑去全靠经验判断。我们当时接了这个需求,目标很简单:预测未来一段时间内每个热门站点的借还车需求,让运调部门提前安排车辆。一开始想得很乐观,用历史均值加个时间窗口估计就行,结果被现实狠狠教育——天气突变、节假日、演唱会散场这类突发因素,均值模型完全反应不过来。后来切入LSTM神经网络方案,才把预测准确率拉到可用的水平。
这篇内容我会从头到尾拆解整个项目的实现过程:数据怎么处理、为什么选LSTM而不是其他模型、网络结构怎么定、训练中掉过哪些坑、模型上线后怎么和实际运调结合。适合正在做时间序列预测或者接手共享单车、出租车、网约车、充电桩这类短时需求预测项目的朋友参考,不是纯理论,每一步都是跑过之后沉淀下来的实操经验。
1. 需求预测的前置工作:先搞清调度方真正要什么
1.1 预测目标不是"总骑行量",而是站点级借还差额
项目启动会上,运营方上来就说要"预测明天全城的骑行需求"。这个目标看着明确,实际无法直接指导调度——调度车是分片区、分站点跑的,全城总量再准,到了具体站点还是不知道该怎么调。我们和运营一起把需求拆成了可执行的口径:
- 时间粒度:按小时预测,覆盖未来24小时。
- 空间粒度:按站点聚合,但只覆盖借还量排名前30%的热门站点,这些站点的订单量占了全局的约75%。
- 核心指标:每个站点每个小时的借出量、还入量,以及二者的差值(净流量)。净流量为正说明该站车辆在净流出,需要补充;为负说明车辆在堆积,得提前腾出位置。
这里有个容易被忽略的点:调度方最关心的不是绝对用车量,而是"差额"。某站点早上8点到9点借出80辆、还入20辆,意味着净流出60辆,如果不提前补车,9点半之后站点就空了。预测"借出"和"还入"两个序列,再取差值,比直接预测一个"净流量"更灵活,因为还入受周边站点联动影响,单独建模后也可以结合其他站点的数据交叉验证。
1.2 数据源盘点与基础统计特征
数据来自运营方数据库里的历史订单流水,每条记录包含订单编号、车辆编号、租借站点、租借时间、还车站点、还车时间。另外还接入了气象接口,包括温度、湿度、风速、降雨量,以及日历信息(是否为工作日、是否节假日、当天星期几)。
拿到原始数据后第一件事不是建模,而是做基础统计,把数据分布摸清楚:
- 订单量的日周期性非常明显,工作日有两个高峰(7-9点、17-20点),周末则是午后到傍晚一个较宽的高峰。
- 降雨对订单量的影响是断崖式的,小雨天降幅约20%,中到大雨降幅可以超过50%,而且这种影响有延迟——雨停后2小时内需求仍被压制。
- 站点之间的相关性极强,中心区写字楼站点的还入高峰,恰好是周边居住区站点的借出高峰,两者相差1-2小时。
这些统计结论直接决定了后续特征怎么构造。比如降雨量不能只当作一个静态特征丢进去,它的影响有持续性,需要我们把它按小时处理成"过去3小时是否有降雨""当前降雨强度"这样的滑窗特征。
1.3 基线模型先跑,别急着上神经网络
很多人一上来就调LSTM的框架,我的建议是先花两天时间把两种简单模型跑通:一是历史同期均值(比如预测某站点周一九点,就取过去四周周一九点的平均借出量),二是带星期和小时哑变量的线性回归。
基线模型的意义有三个:一是衡量复杂模型提升幅度的参照物;二是帮助检查特征和数据的正确性,比如线性回归跑出来的系数是否合理,季节项是否捕捉到了;三是给团队一个预期管理——如果LSTM比最简单的均值模型提升还不到10%,说明问题可能不在模型复杂度上,而在数据或者特征上。
我们当时的基线结果:历史均值模型的站点级小时借出量MAE在4.2左右,线性回归在3.8左右。这个数字成了后面所有模型对比的起跑线。
2. 数据清洗与特征构造:决定预测上限的关键环节
2.1 时间特征、天气特征、空间联动特征怎么切
特征工程这部分直接决定模型的上限,网络结构再花哨也补不回来。我们最终使用的特征分为三组:
时间组:
- 小时(0-23),映射到正余弦编码,避免23点和0点的数值跳跃。
- 星期几(0-6),同样做了正余弦编码。
- 是否为工作日(0/1)。
- 是否为节假日(0/1),这里要注意把调休的工作日也算进去,否则节假日预测必偏。
天气组:
- 温度(连续值)。
- 相对湿度。
- 风速。
- 降雨强度等级(0-4),比直接塞降水量数值效果更稳定,因为降水量本身就是偏态分布的。
- 过去3小时累计降雨量(滑窗特征)。
空间联动特征:某一站点的借出和还入,和周边3公里范围内站点过去2小时的平均借出量有很强的相关性。这个特征在建模时帮了大忙,相当于让单个站点"感受"到了周边区域的热度变化。
特征构造有一个原则:所有特征必须使用预测时刻之前已发生或已确定的数据,不能把未来信息带进去。比如不能用当天全天的天气均值来预测上午9点的需求,因为9点时你并不知道当天下午的天气状况。
2.2 天气与订单的异常值处理
订单数据里的异常值主要集中在两类:
- 极端天气导致的单站零订单(比如台风天部分站点全天没有一笔订单)。对这类数据,不能直接当作缺失值删掉,因为"零订单"本身就是一种有效模式。我们的处理是单独加一个"极端天气"布尔特征,让模型自己去学这个特征与订单量的关系。
- 运维操作导致的异常。比如调度车批量从某站点拉走50辆车,表现在订单数据上就是该站点的还入数量突然归零,车辆数也骤降。这类是"操作噪声",不是真实的用户需求,如果不处理,模型会把这种异常学到"还入需求突然为0"的模式里。处理方式是对还入序列做中位数滤波,把单点突变拉平。
2.3 时间序列的数据泄漏:切分训练集最容易踩的坑
代码实现上最需要注意的是数据划分。LSTM用的是滑窗样本,如果按传统机器学习的方式随机shuffle再切train/test,就会把未来时间的信息泄漏进训练集——模型已经"见过"测试时段的数据了,验证效果虚高,上线直接崩。
正确做法是按时间顺序切分:假设有90天数据,前70天做训练集,中间10天做验证集,最后10天做测试集。当构造滑窗样本时,窗口内的序列必须严格在当前预测时间点之前。
有个细节值得单独强调:滑窗步长是1小时,窗口长度是168小时(7天)还是24小时?我们对比过,168小时窗口带来的提升很小,但训练时间多了近3倍。最终选了72小时窗口,因为需求的强周期性基本在周内,7天前的特征对当前时刻的影响已经很弱了,序列太长反而让模型更关注到噪声。
3. LSTM网络结构设计:从序列建模原理到具体参数
3.1 为什么LSTM适合这个场景,它到底解决了什么问题
共享单车需求是一个典型的多变量时间序列问题:当前时刻的借出量不仅取决于当前的特征(天气、时间),还和过去几个小时的状态相关。比如早高峰的借出高峰,其实是昨晚上一个时段还入量积累的结果,车辆的可供性是有"惯性"的。
传统方法的问题在于,无法自动学习这种时序依赖,需要人工构造大量滞后特征。LSTM的核心价值是门控机制——输入门决定记住什么,遗忘门决定丢掉什么,输出门决定输出什么,这让它在训练中自动学到"什么时候该参考过去的信息,什么时候该忘记"。
我经常用的一个类比:LSTM像一个有选择的记忆体,它知道看天气突变时要多参考最近2小时的数据,而在平稳时期可以更多依赖周期规律。这不是人为设定的规则,是训练中自己学出来的参数。
3.2 网络结构的具体配置与选型原因
最终网络结构如下(基于开源框架实现):
Layer (type) Output Shape Param # lstm_1 (LSTM) (None, 72, 128) 115200 dropout_1 (Dropout) (None, 72, 128) 0 lstm_2 (LSTM) (None, 128) 131584 dropout_2 (Dropout) (None, 128) 0 dense_1 (Dense) (None, 64) 8256 dense_2 (Dense) (None, 1) 65两个关键选择:
- 两层LSTM优于单层。单层LSTM在表达能力上只够捕捉"当前输入到输出的直接映射",而两层能叠加出更高阶的时间特征——第一层捕捉基础模式(每日周期、早晚高峰),第二层组合成更复杂的依赖(比如"工作日+降雨+前一日还入情况"三者共同作用的效果)。
- 后接两个全连接层是因为多变量特征(天气、时间、周边站点)与LSTM输出的时序特征需要融合,全连接层可以学到一个低维组合映射,把高维时序特征压缩成单点预测值。
LSTM单元数从64试到256,128是个性价比最好的点。64稍微欠拟合,验证MAE大约3.3;128的验证MAE降到了2.9;256提升到2.8,但训练时间直接翻倍,还更容易过拟合。在项目周期性迭代的节奏下,128是最实用的选择。
3.3 多步预测的处理策略:直接多输出还是递归预测
项目要求预测未来24小时,有两条路可以走:
- 递归预测:每次预测下一个时间点,把预测值当作输入再预测下下个点,循环24次。
- 直接多输出:把输出层改为24个节点,一次predict出24小时的序列。
递归预测的缺点非常明显:误差会累积,第24小时的预测误差远高于第1小时。直接多输出虽然在理论上没有误差累积,但我们的实测中,24小时输出的尾部预测精度反而不如递归,因为一层的线性输出很难表达"未来24小时"这么长跨度内的时间依赖关系,而且每个预测节点共享同一个LSTM特征,相互独立性不足。
最终采用的方案是一个妥协:分三段预测,每段8小时。模型输出节点数为8,预测完第一段后,把输出结果拼回输入序列,再预测下一段。这样每段的误差累积范围被限制在8小时以内,同时避免了24小时一次性输出的表达瓶颈。
4. 训练细节与超参数调试:那些让模型收敛变慢的隐形因素
4.1 输入特征的归一化必须"按时间窗口"做,不能全局统一
这是一个看起来不起眼、但影响很大的细节。如果对全部历史数据的每个特征做全局Min-Max归一化,然后用这个尺度去归一化训练集和测试集,假设测试期的温度是35度,而历史最高温只有33度,那么35度的值会被映射到1以上的区间,模型从未见过这个范围,预测就会出现漂移。
我们采用的方案是"按训练集拟合归一化参数,用该参数处理验证集和测试集",即只统计训练集的均值、方差或最大最小值。如果测试期的确有超出训练集范围的新值,宁可让其落在0到1.1之间,也不要让归一化参数随新数据持续变化——否则模型上线后每次归一化参数都在变,预测结果永远不稳定。
对于序列数据,每个样本的归一化还有一个更细的选择:按特征维度归一化(所有时间步共享同一个缩放系数)还是按时间步归一化(每个时间步单独缩放)。我们实验中,按特征维度归一化的效果更好,因为不同天气变量和时间变量所处的数值范围不同,但同一特征跨时间的分布相对稳定。按时间步归一化会破坏序列内部的相对大小关系,模型很容易学成输出一个"均值附近"的平庸预测。
4.2 损失函数选择与训练策略的调整
损失函数默认用MSE(均方误差),在调试到后期时切换成了Huber损失。原因是MSE对异常值过于敏感——运维操作导致的异常还入量,会让模型为了拟合这个离群点而扭曲整体的预测趋势。Huber损失在误差较小时表现如MSE,在误差大时表现为线性损失,对异常值更稳健。
训练参数:
- batch_size=256,因为样本量较大(约70天乘24小时乘热门站点数,总计约25万样本),batch太小导致收敛震荡,batch太大则每个epoch太慢,256是在GPU利用率与收敛速度之间实测出来的。
- 优化器用Adam,初始学习率0.001,每5个epoch若验证损失不下降则乘以0.5衰减。
- Early stopping的patience设为8,即8个epoch内验证集损失无改善就停止训练并恢复最优权重。
- Dropout率0.2,只加在LSTM层之间和LSTM到全连接之间,不对输入层加。
有一个很多人会忽略的坑:LSTM层的dropout和recurrent dropout是两个不同的东西。常规dropout作用于输入和输出,recurrent dropout作用于时间步之间的循环连接。我们只用了dropout(输入输出路径),如果时间步之间的关系本身很脆弱,可以尝试recurrent_dropout,但要注意它会显著增加训练时间。
4.3 训练过程中的监控指标:不能只盯训练集损失
我在训练过程中同时监控三个指标:
- 训练集MAE,判断是否欠拟合。
- 验证集MAE,判断是否过拟合以及泛化能力。
- 早高峰时段(7-11点)的MAE,因为这是调度最关心的时段,如果整体MAE很低但早高峰MAE很高,说明模型倾向于低估高峰需求,业务上是不可接受的。
实际训练中发生过这样的事:全局验证MAE稳定在2.9,但细看早高峰时段的MAE高达5.6,明显是模型对高峰时段变化幅度的捕捉不够。后来在损失函数里对早高峰时段的样本加了1.5倍的权重,全局MAE升到3.1,但早高峰MAE掉到了3.8,整体调度效果反而更好。这提醒我:模型做出来是给调度用的,指标要跟着业务的关键时段走,不是所有样本都同等重要。
5. 模型评估与误差分析:理解误差来源才能有针对性地优化
5.1 按时段、按天气拆解的误差复盘
测试集上的整体MAE分别为:借出量2.9,还入量3.1,换算成单车调度场景,意味着单站点每小时平均有约3辆车的误差。听起来不大,但热门站点高峰小时借出量动辄超过100,3辆车的误差占比只有3%,而在夜间低峰时段站点可能只有2-3辆车的借还,此时3辆车的误差占比就超过100%。
拆解后的主要误差集中在三类时段:
- 早高峰首小时(7-8点):模型预测的借出量比实际少了约8%,原因是这一时段的需求有一部分是前一日晚高峰未还入的"滞留需求",比如前一天晚上用户骑行后把车停在了非热门站点,早上这批车的可用性低于模型预期。
- 降雨刚开始的1-2小时:模型已经降了预测值,但实际订单下降更剧烈。我们分析下来,是因为降雨特征在数据里是小时级的,而实际人群的反应有10-20分钟的滞后且更剧烈。
- 大型活动散场后的2-3小时(如演唱会、体育赛事):这种事件的时空特征是突发的,训练集中类似样本极少,模型几乎不可能预测到。我们的处理是单独做事件日历,把已知的大型活动时间表提前打标进特征,让模型至少知道"明天有活动"这个信息。
5.2 从RMSE到业务指标:预测误差如何转化为调度策略
技术指标之外,一定要解决一个问题:误差可接受,但调度怎么用?我们做了一个换算——把MAE转换成"补车失误率":
假设某站点预测未来1小时净流出30辆,实际净流出35辆,调度车到了现场发现还是缺5辆,这5辆就是预测误差带来的缺口。定义"缺口率"为(实际净流量-预测净流量)的绝对值除以实际净流量的绝对值,当缺口率超过30%时,视为一次调度失误。
在测试集上,模型的调度失误率约为21%,比基线历史均值模型的34%提升了近40%。这个数字不完美,但对实际运营已经有明显的指导价值——调度车至少知道哪个站点"大概率缺车",效率比盲目巡查提高了不止一个档次。
还要强调一点:预测输出不能只给一个点估计,要给置信区间。我们在部署时同时计算预测值和预测误差的标准差,标准差大的时候调度系统会给人工复核提醒。代入一个讨巧的做法:用同一模型训练多个随机种子,对预测结果做集成,把多个模型的预测方差当作不确定性估计。效果还不错,计算成本多3倍,但确实避免了不少"预测值看着正常、实际偏差巨大"的情况。
6. 从模型到系统:部署上线与持续迭代的几个实操要点
6.1 预测服务的接口设计与刷新策略
模型训练的产物不大,整体模型文件约35MB,单次预测耗时在CPU上约40ms,GPU上约8ms。部署上我们采用了简单的方案:训练好的模型文件打包成预测服务,对外暴露一个HTTP接口,入参为站点ID、预测起始时间、特征数据(天气、日历),出参为24小时的借出量、还入量预测序列。
关键决策是刷新频率。共享单车需求受天气影响剧烈,如果一天只预测一次,上午用早上8点的预测结果,下午突然降温降雨,预测就完全失效了。我们的做法是每小时重新预测一次未来24小时,相当于每次只"滑"进了一个小时的新数据。考虑LSTM有72小时的输入窗口,每次预测需要最近72小时的特征,这个特征数据需要每小时从数据库和天气接口拉取,再拼装成序列输入。
这里必须注意一个工程细节:特征拼装的时间对齐。数据库里的订单是按小时聚合的,但数据写入有延迟,比如14:00的订单可能15:20才落库完成。如果整点触发预测,拿到的14:00订单数据其实不全。解决方式是预测服务延迟30分钟触发,确保上一小时的数据已经完整落库。这是建模时完全不会想到的坑,上线第一天就踩爆了——早高峰预测用上了不完整的7-8点数据,整条预测曲线都偏低。
6.2 模型漂移与定期重训练的节奏
共享单车的使用模式会随着季节、城市基础设施变化而漂移。比如夏天共享单车的午后骑行量明显增加,冬天则早晚高峰更集中;某个地铁站开通后,周边三个站点的需求模式全部改变。
处理方式分两层:
- 短期:每天晚上用当天新增的完整数据做一次增量训练,保持模型对近段时间变化的敏感度。增量训练不是全量重训,而是加载已有权重,用当天数据继续反向传播几个epoch,学习率调低一个数量级,避免把已有的稳定参数冲坏。
- 长期:每两周做一次全量重训练,用过去90天数据从头训练,并根据验证集效果决定是否替换线上模型。
有个止损经验:增量训练次数不能贪多,每天增量训练的epoch数设定为1-2个就足够。我们曾经设了5个epoch,结果新数据被反复过拟合,模型对前一天的偶发事件(比如临时封路)过度敏感,第二天预测反而更差。
6.3 冷启动和弱数据站点的兜底方案
不是所有站点都有足够的历史数据。新建站点可能只有两周数据,偏远站点的每日订单量只有个位数。LSTM对这些站点的预测毫无意义,模型训练出来的权重完全被热门站点主导。
我们的做法是对站点做了分层:
- 第一层(有充足历史数据的热门站点):走完整LSTM预测。
- 第二层(数据量中等但模式稳定的站点):直接用LSTM模型在少量数据上微调,冻结前两层LSTM参数,只训练全连接层。
- 第三层(新站或超低量站点):不进入模型预测,直接回退到历史均值加上邻近站点联动修正——用周边3公里内所有站点的净流量均值乘一个面积系数作为估算值。
这个兜底策略的触发条件也是自动化的:站点历史数据天数小于14天,或近30天日均订单量小于10,自动归入第三层策略。上线稳定期后,约78%的站点走完整LSTM预测,14%走微调,8%走兜底方案,整体覆盖率已经是一个可接受的运营配置。
6.4 预测系统上线前后的效果验证清单
最后给一套上线前后必做验证清单,是我在这个项目里反复沉淀下来的:
- 特征分布对比:上线前对比训练期特征和线上实时特征的分布,一旦差异超过2倍标准差,立刻告警,多半是数据源出问题了。
- 预测值合理性校验:给预测接口加一个兜底规则,预测值必须是正数且不能超过该站历史最大值的1.5倍,否则截断。看似粗暴,但能挡住不少数据意外。
- 调度动作模拟回放:用过去一周的真实数据跑一遍预测,把预测结果和历史调度记录对齐,看"如果按照预测来调度,缺口能减少多少"。这比看MAE更直观。
- 灰度发布:新模型先在两三个片区试运行,和旧模型的预测结果同时推送给调度系统,但只把新模型的结果作为考核指标,不直接影响调度指令,运行三天后再切换。
个人体会是,这个项目的难点不在于LSTM本身,而在于把业务问题翻译成数据问题,再数据问题翻译成模型问题。每一步翻译不到位,模型效果就大打折扣。现在回头看最值得花时间的,反而是最开始数据探索和特征构造的那两周,它们决定了整个项目的天花板。