共享单车需求预测是我这两年在智能交通方向做得最完整的一个项目。起因很简单,某城市的共享单车运营方反馈:早晚高峰期,热门站点门口空荡荡一辆车都找不到,而居民区附近的站点却堆满了车,调度车辆疲于奔命,成本居高不下。这个问题本质上是需求预测没做好。于是我用LSTM神经网络搭建了一套共享单车需求预测系统,把历史骑行数据、天气数据、时间特征揉在一起,对未来时段各站点的租车需求做了滚动预测,效果比我预先设想的基线模型好了不少。这篇文章把我从数据预处理、特征工程、模型调参到系统落地部署的完整过程写出来,既有思路也有可复现的代码细节,适合正在做时间序列项目,或者想进入智能交通方向的同学参考。
1. 方案选型:为什么在众多模型里选LSTM
1.1 共享单车需求预测到底是个什么问题
共享单车需求预测,本质上是一个多变量时间序列回归问题。我们需要用历史若干个时间步的数据,预测未来一个或多个时间步的需求量。但和一般的销量预测、流量预测不同,单车需求有几个非常明显的特征。
第一个是强周期性。城市通勤决定了它的日周期非常稳定:早上7点到9点,住宅区到办公区的方向需求暴涨;晚上18点到20点,方向完全反过来。同时,一周之内工作日和周末的出行模式截然不同,周一早高峰和周六上午根本没有可比性。这种多周期叠加的信号,如果模型没有记忆能力,很难捕捉。
第二个是强非线性。下雨天、大风天、极端高温或者低温,都会让骑行需求骤降,但不同站点的下降幅度又不一样。靠近地铁站的站点,雨天反而可能出现“接驳需求”的上升。单纯从历史均值外推,很难处理这种突变。
第三个是空间异质性。市中心核心站点的需求量峰值可能是郊区站点的几十倍。有的站点是“潮汐型”的,早晚高峰需求方向完全相反;有的站点是“稳定型”的,全天需求变化不大。这些差异让统一建模变得困难。
1.2 传统模型和机器学习模型的局限
我在项目初期先跑了几个基线模型,不是为了应付差事,而是为了搞清楚问题难在哪里、LSTM的优势有没有被高估。
ARIMA类模型对平稳性要求很高。单车需求数据有明显的趋势和周期,必须先做差分。差分会损失掉一部分“量级信息”,比如高峰期是500辆还是550辆,这类绝对量信息在差分后被抹平了。而且ARIMA处理不了外部变量,天气、节假日这些因素根本加不进去,顶多做个外生回归的ARIMAX,效果也比较勉强。
XGBoost、LightGBM这类树模型我实际测下来表现并不差。它们可以吃进大量特征,对非线性拟合得很好,训练速度也快。但它们的问题是:对时间序列的“顺序性”并不敏感。你可以构造滞后特征,比如前3个小时的需求量、昨天同时段的需求量,把顺序信息塞给树模型,但窗口长度固定后,它就很难再捕捉更长周期的模式。而且步长稍长一点的预测,树模型需要为每个预测目标单独建模,维护成本高。
LSTM的优势在于,它内部有一组门控结构——输入门、遗忘门、输出门——可以在训练中自己学会“什么东西该记住、什么东西该忘掉”。你不需要手工构造“过去24小时的平均需求”这种特征,它自己去学。这对捕捉长周期依赖和突变后的恢复过程非常有帮助。而且LSTM天然支持多步预测,训练一个模型就能输出未来多个时段的结果,工程上更干净。
1.3 LSTM的局限和我做的兜底设计
LSTM不是万能的。它训练周期长,对超参数敏感,而且数据量不足时很容易过拟合。我在项目里做了一个关键决定:不追求一个“庞大的深层网络”,而是把重点放在特征构造和数据质量上。最终模型只有两层LSTM,每层128个隐藏单元,效果已经明显超过所有基线。
另外,我对LSTM的“黑盒”属性做了兜底设计。系统里同时跑了一个基于历史均值加规则修正的简单模型作为对照。如果LSTM服务的输出和对照模型偏差过大,会触发告警,提醒人工核查。这样既享受了深度模型的准确率,又避免了完全失控的风险。
2. 数据准备与特征工程的关键细节
2.1 数据来源和预处理
项目用的是某开放平台提供的共享单车骑行记录数据,包括每次骑行的开始时间、结束时间、起点站点、终点站点。我先按站点ID + 小时维度做了聚合,统计出每个站点在每个小时的租车量。
选择小时作为时间粒度是反复权衡过的。按分钟聚合,数据噪声太大,很多时段需求量是0,训练效率低;按天聚合,又会把早晚高峰的波动完全抹平,调度场景根本用不了。小时粒度在业务上刚刚好——调度策略的决策周期通常就是1小时。
数据清洗主要做了三件事:
- 剔除营业时间外的记录。有些骑行记录的时间戳在凌晨3点,量很少但存在,先把异常时间段的数据单独标记,而不是直接删除。
- 处理站点的启停运时段。部分站点因施工或设备维护会临时关闭,这些时段的需求真实值是“无服务”而非“零需求”,直接填0会让模型学到错误的模式。我用一个单独的掩码特征标识站点是否在运营。
- 异常尖峰处理。某个站点单小时需求偶尔窜到日均水平的10倍以上,大概率是数据错误,用前后24小时的中位数替换掉。
2.2 天气数据和节假日特征的影响
天气数据我接的是公开气象站的历史记录,包括温度、体感温度、降水量、风速、相对湿度。这些数据按小时记录,与单车数据做时间对齐。
天气特征对预测精度的影响很容易被低估。我做了个简单的特征重要性分析,在没有天气特征的情况下,测试集RMSE大概在12.5;加入温度、降水、风速后,RMSE降到了10.8,提升非常明显。尤其是降水,对需求的压制效应在雨量达到中雨级别时几乎是线性的。
节假日特征也很有意思。单纯的“是否节假日”太粗糙,我把节假日细分为三类:工作日、普通休息日、法定节假日。原因是这三类日期的出行模式差别很大,法定节假日和普通周末并不能直接等价。另外我还手工标定了“节假日前后一天”的状态——周六虽然属于休息日,但如果周五是法定节假日,周六上午的出行量会和普通周六不一样,因为不少人会拼假延长休息时间。
2.3 滞后特征和窗口构造
LSTM的输入是一个三维张量,形状是[samples, timesteps, features]。这里timesteps是历史窗口长度,features是每个时间步的特征向量。
窗口长度的选择需要业务知识和实验结合。我先按直觉尝试了几个值:6、12、24、48小时。实测下来,24小时窗口效果最好。窗口太短,模型看不到完整的日周期;窗口太长,超过48小时后信息增益很小,训练开销却明显增加。最直观的解释是:共享单车需求的核心周期就是24小时,模型需要至少看到整个前一天的模式,才能对今天做出合理推断。
每一条训练样本的构造方式是这样的:用第[t-window, t-1]时间步的74维特征向量,预测第t到t+23时间步共24小时的租车量。也就是说,模型一次输出24个值,对应未来24小时的逐小时预测。这是“多步直接预测”,比“递归预测”(先预测第t+1步,再用预测值预测第t+2步)稳定得多,不会累积误差。
2.4 特征标准化和归一化的取舍
数值特征归一化是LSTM训练的必要步骤,这一点我踩过坑。最开始我把温度、降水量、需求量原始数值直接喂进去,模型训练loss迟迟不降。原因很简单:需求量可能从0到600变化,而温度是-10到35,降水量是0到20,不同量纲的特征在梯度计算中的影响差异太大,LSTM内部的状态更新会被大尺度特征主导。
我用的是MinMaxScaler,把每个特征压缩到[0,1]区间。没有用StandardScaler的原因是:单小时需求量分布严重右偏,大量时段需求接近0,标准化后仍然不是正态分布,但MinMax至少保留了相对量级关系。对于稀疏数据,把0映射到固定的最小值附近,实值需求映射到0到1区间,比强制去均值更自然。
需要特别注意:拟合scaler时必须只用训练集数据,绝对不能用全量数据拟合,否则会造成信息泄露,让验证指标的参考价值大打折扣。我在代码里专门写了Pipeline,确保测试集只被transform,不被fit。
3. 模型构建与训练参数
3.1 网络结构设计
最终网络结构很简单:
- 输入层:形状为[None, 24, 74],None是batch size,24是时间步,74是特征维度
- 第一层LSTM:128个隐藏单元,返回完整序列
- Dropout层:丢弃率0.2
- 第二层LSTM:128个隐藏单元,只返回最后一个时间步的输出
- Dropout层:丢弃率0.2
- 全连接层:64个神经元,ReLU激活
- 输出层:24个神经元,对应未来24小时的需求量
选两层LSTM的理由:一层LSTM提取局部的短期模式,两层可以叠加提取更高层的抽象模式。实测结果,第二层的收益在RMSE上大约减少了3%,收益不大,但参数量和训练时间增加了近一倍。如果你的数据量不是很大,或者训练资源有限,单层LSTM配大一点隐藏单元数可能是更划算的选择。
隐藏单元数量128这个值,我是从64开始往上试的。64效果不够好,256比128的提升微乎其微,再加上训练时间成倍增加,就停在了128。这个值的选取没有绝对公式,靠的是实验和验证集上的早停提前终止。
3.2 损失函数和优化器
损失函数选的是Huber Loss,这个选择是在看了误差分布之后做出的。大部分时段模型的预测误差不大,但尖峰时段(早晚高峰)误差会明显放大。如果用MSE,会过度惩罚那些极端时段,导致模型为了把高峰尖峰拟合好,牺牲掉大部分时段的精度。Huber Loss相当于在误差较小时用MSE的平方损失,误差超过阈值后用线性损失,结合了两者的优点。
优化器用的Adam,初始学习率0.001。Adam的自适应学习率特性让训练初期收敛很快,省去了手动调整学习率表的麻烦。我加了EarlyStopping,监控验证集loss,patience设为10。训练大约在20个epoch左右就触发了早停,避免了过拟合。
实测中还试过SGD配合学习率衰减,收敛慢但最终验证集loss略低。如果你追求极致的指标,可以尝试后者;如果追求工程效率,Adam是更稳妥的起点。
3.3 训练细节和防过拟合技巧
过拟合在这个项目上很现实。单车需求数据虽然有几十万条样本,但很多相同站点的数据高度相似——比如某个站点工作日上午8点的需求量可能常年都稳定在一个区间。模型很容易“记住”这些重复模式,而不是真正学会泛化规律。
我用了几个策略:
- EarlyStopping是最关键的一道防线。patience设10,如果验证集loss连续10个epoch不降就停止。
- Dropout。在每层LSTM后面加Dropout层,丢弃率0.2,只在训练时生效。注意不要在输出层前加Dropout,不然预测会不稳定。
- 验证集早停的“最佳模型”回滚。Keras的ModelCheckpoint配合save_best_only=True,保存验证集loss最小的模型权重。
- 样本加权。这个比较进阶:对工作日晚高峰(早上7-9点、晚上17-20点)的样本乘以1.5的权重,让模型重点关注业务上最难预测的时段。实测MAPE减少了约4个百分点。
3.4 代码实现核心片段
这里是数据窗口化和模型训练的简化代码,展示关键逻辑,具体路径和参数按你本地数据调整:
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint from sklearn.preprocessing import MinMaxScaler # 假设 data_array 是 [样本数, 特征数] 的原始单变量/多变量数据 # 构造滑动窗口样本 def make_samples(data, window=24, horizon=24): X, y = [], [] for i in range(window, len(data) - horizon + 1): X.append(data[i - window:i, :]) # 历史窗口 y.append(data[i:i + horizon, 0]) # 预测目标列 return np.array(X), np.array(y) # 特征与目标归一化 scaler_x = MinMaxScaler() scaler_y = MinMaxScaler() X_scale = scaler_x.fit_transform(features) y_scale = scaler_y.fit_transform(target.reshape(-1, 1)).ravel() X, y = make_samples(X_scale, window=24, horizon=24) # 模型定义 model = Sequential([ LSTM(128, return_sequences=True, input_shape=(24, X.shape[2])), Dropout(0.2), LSTM(128, return_sequences=False), Dropout(0.2), Dense(64, activation='relu'), Dense(24) ]) model.compile(optimizer='adam', loss='huber_loss', metrics=['mae']) callbacks = [ EarlyStopping(patience=10, restore_best_weights=True), ModelCheckpoint('best_model.h5', save_best_only=True) ] history = model.fit( X, y, validation_split=0.1, batch_size=64, epochs=50, callbacks=callbacks, verbose=1 )注意:实际项目中按站点分组训练会更合理。把所有站点数据混合训练,模型会学到“平均站点”的模式,对于需求特别高或特别低的站点并不友好。我在后文会讨论分组建模。
3.5 数据划分的坑:不能随机切分
时间序列数据划分和普通机器学习完全不同。我见过很多新手直接把数据随机打散再切训练集和测试集,这是致命的错误——相邻时间段的数据高度相关,随机打散会把“未来信息”泄漏给模型,测试指标虚高得离谱。
正确的划分方式是按时间顺序切分。我用前70%的数据做训练集,中间15%做验证集,最后15%做测试集。验证集不参与训练,只用来挑超参数和早停;测试集最后才碰,用来评估模型的真实泛化性能。
更严格的做法是使用时间序列交叉验证,比如滚动前向验证。但项目时间有限,一次切分加上组内随机采样已经能保证公平性。只要记住:任何随机打散都会美化结果。
4. 评估结果与误差分析
4.1 评估指标的选择
需求预测的评估指标我用了四个:RMSE、MAE、MAPE、R²。
RMSE对大规模点位的偏差敏感,适合衡量整体预测稳定性;MAE更直观,含义是“平均每个预测小时偏差多少辆车”;MAPE则反映相对误差。但MAPE有个天然缺陷:当真实需求量接近于0时,MAPE会爆炸。凌晨时段很多站点的真实需求就是0,预测值是0.3,MAPE算出来就是无穷大。所以我的MAPE只算在真实需求量超过5辆的时段上。
R²作为补充指标,反映模型对需求方差的解释程度。我的测试集R²达到0.86,对多维时间序列预测来说已经是比较理想的效果。
4.2 各模型效果对比
| 模型 | RMSE | MAE | MAPE(>=5辆时段) | R² |
|---|---|---|---|---|
| 历史均值法 | 18.3 | 12.6 | 37% | 0.42 |
| ARIMA | 15.1 | 10.8 | 29% | 0.61 |
| XGBoost(滞后特征) | 12.9 | 8.6 | 23% | 0.73 |
| LSTM(本文方案) | 10.2 | 6.3 | 16% | 0.86 |
这个表格是我在相同特征工程条件下跑出来的。XGBoost表现不差,但LSTM在MAE上领先了约27%。有趣的是,LSTM对早高峰尖峰的拟合能力特别明显,XGBoost的预测曲线在尖峰处总是“矮一截”,因为树模型对未见过的组合泛化能力不如LSTM。
4.3 误差分布规律
误差分析让我对业务有了更深理解。模型的主要误差集中在两类情况:
第一类是极端天气突变。比如连续几天晴天后的突然暴雨,LSTM学会了“晴天需求高”的模式,对暴雨的抑制效应响应偏慢,往往要过2-3个小时才能调整过来。这在业务上是可以接受的,毕竟提前告知极端天气之后,运营人员可以人工干预。
第二类是大型活动的叠加效应。某站点附近临时举办活动,需求会在1小时内激增数倍。这不是周期性模式,纯数据驱动模型很难准确预测。系统在设计时保留了一个人工标记特征入口——运营人员在后台手动标记“某站点活动时间”,模型会自动把该时段特征加强。
深夜时段(23点到凌晨5点)的绝对误差很小,但相对误差大。比如真实值2辆、预测值1.5辆,相对误差25%。对业务来说这不重要,因为调度力量本来就集中在高峰时段,深夜的绝对量太小,不值得精确预测。
4.4 从预测到实际业务动作的闭环
预测本身不是终点,重点是预测结果如何指导调度。
我用预测结果生成了一张“供需预测热力图”:以站点为维度,未来24小时逐小时预测减去当前可用车辆数,得到“净缺口”和“净盈余”。净缺口超过阈值且持续超过两个小时的站点,系统自动生成调度工单,推荐从临近盈余站点调车。
这套闭环上线后,运营方的调度效率提升了约20%。核心原因不是预测准确率有多高,而是调度动作从“被动响应”变成了“提前布局”。LSTM的预测序列给到了未来几小时的方向,调度车辆可以提前在缺口出现前就完成补车。
5. 系统架构与工程化落地
5.1 整体架构
整个系统分四层:数据层、特征层、模型服务层、应用层。
数据层用的是MySQL存储业务数据,配合时序数据库存聚合后的窗口数据。特征层的任务是基于原始订单表做窗口聚合,产出模型可直接消费的特征矩阵。模型服务层用TensorFlow Serving部署,暴露gRPC接口。应用层是一个可视化看板,地图上展示各站点实时和预测需求,并支持调度工单管理。
最关键的设计决策是:特征计算和模型推理分开。特征层每天定时预计算好未来24小时的特征,存成文件;模型服务层只负责加载特征文件、跑模型、输出预测。这样调度端不需要关心特征工程细节,只管读取预测结果。
5.2 预测接口设计
接口设计的核心原则是:返回结果要包含可解释性信息,而不仅仅是24个数。
{ "station_id": "S1002", "generated_at": "2025-01-20T18:00:00+08:00", "prediction": { "horizon": 24, "unit": "hour", "values": [12, 15, 18, 8, 3, 1, 2, 5, 28, 45, 52, 40, 33, 30, 25, 35, 42, 50, 60, 45, 30, 22, 18, 12], "confidence": "high", "top_factors": [ {"factor": "晚高峰时段", "contribution": 0.42}, {"factor": "附近地铁站接驳", "contribution": 0.25}, {"factor": "晴朗天气", "contribution": 0.18} ] } }confidence字段由历史误差分布估算:如果当前站点的历史预测误差标准差小,就标记为high,业务上可以放心按预测值排班;反之标记为medium或low,调度时需保留冗余运力。
5.3 实时滚动预测与成本控制
模型预测不需要每次都重算历史窗口。系统设计成一个定时任务,每30分钟滚动预测一次未来24小时的需求,并缓存结果。由于预测输入用的是最近24小时的历史特征,一旦某个站点的最新需求数据更新,缓存会失效,再触发重算。
推理成本控制上,一次批量预测1000个站点大约需要2秒GPU时间。如果只用一个离线GPU机器跑定时任务,完全够用。无需常驻在线服务,因为调度人员看的是每30分钟刷新一次的预测结果,实时性要求没那么高。
5.4 模型分组建模的进阶思考
我在后期做了分组建模实验。把站点按需求规模和潮汐模式聚类成4组,每组训练一个专属模型。效果是整体RMSE下降了约8%,但模型数量变成了4个,运维复杂度上升。
分组的意义在于解决“平均化效应”。全量数据训练的模型天然偏向预测需求中等的站点,对高需求核心站点(单小时几百辆)和低需求长尾站点(单小时个位数)都不友好。分组后,每组模型针对性学习各自量级和波动模式,精度提升明显。
如果你的站点数量不多(比如几十个),可以直接按站点独立建模。但站点过多时,数据稀疏站点会崩掉,这类站点更适合用“相似站点迁移”策略,从同组模型中复用权重。
6. 常见问题与排查经验
6.1 模型收敛慢或loss震荡
如果你发现训练loss下降很慢,或者忽高忽低,我最先建议排查这两个地方:
- 学习率太大或太小。Adam默认的0.001是个好起点,但如果你改了网络结构,可能得调低到0.0005甚至0.0003。
- 数据归一化是否正确。检查是不是有特征直接用的是“原始量级”,把0-600的需求量和0-1的归一化特征混在一起。用训练集执行scaler后,打印几行缩放后的数据看看分布范围。
6.2 预测结果滞后于真实变化
LSTM做时序预测最常见的问题之一是:预测曲线比真实曲线滞后一个步长。如果你看到预测高峰比真实高峰延后1小时左右,说明模型本质上在“复读”上一时刻的值,没有学会真正的动态变化。
解决方案有几个方向:
- 对目标序列做一阶差分预测,预测的是“增量”而不是“绝对量”。训练时把预测目标改为diff(y, 1),推理时用前值加上预测的增量子恢复。
- 增加时间步长,让模型能看到更长历史窗口,从而更好地理解趋势。
- 加入更多外生变量,尤其是天气、节假日这类能够辅助判断突变的信息。
6.3 高需求站点被低估
这是分组建模前最典型的误差模式。大站点的绝对需求很高,但其变化规律和小站点完全不同。如果你发现核心站点的预测总是偏低,优先尝试下面两步:
- 单独为大站点分配独立模型。不需要聚类,直接选出Top 10需求量的站点,每个站点一个模型。
- 在损失函数中引入相对误差项。比如用MAPE作为补充loss,让大站点和小站点的误差权重更均衡。
6.4 预测结果如何赢得业务信任
这个坑属于工程落地层面。光给运营团队看RMSE和R²没有用,他们要的是“我凭什么按这个结果派车”。我总结出来的经验是:必须做可解释性对照。
我在看板上加了一个“相似日追溯”模块。每一条预测结果旁边,附上历史上最相似的日期和那天的真实情况。比如预测明天早高峰某站点需要30辆车,相似日一栏显示“上周二真实需求28辆”,运营人员看到这个就会对预测结果产生信赖。
另一个做法是展示置信区间。LSTM本身不输出不确定性,但可以用历史误差分布近似估计。以预测值为中心,上下加减1个历史标准差,得到80%置信区间。这个区间画出来之后,调度人员会把运力安排放在区间上限位置,既不过度配置又保留了缓冲。
6.5 冷启动与新站点问题
新站点没有历史数据是逃不开的场景。共享单车运营方会定期新增站点。我的兜底策略是:先用同区域现有站点的历史模式初始化预测。具体做法是,找出地理最近的5个同类站点,取它们的平均需求序列作为新站点的初始基线,上线两周后逐步切换成新站点自己的数据训练模型。
这个方法不完美,但至少让新站点在冷启动期不会完全裸奔。
7. 最后的几点心得和后续扩展
按我的实际体会,这类项目的成败关键是特征和数据质量远大于模型结构。LSTM是锦上添花的那部分,真正打基础的是对业务的理解和对数据的敬畏。如果你正准备做类似项目,别急着堆网络层数,先把数据预处理和窗口设计做扎实。
选LSTM还有一个前提:你有足够的历史数据。如果某个站点每天的需求量就只有个位数,那再多层LSTM也学不出模式,这时候老老实实上规则模型或者直接用历史均值反而更靠谱。
这个项目后续可以走的方向还有不少。一个是加入图神经网络,把站点之间的空间关联建模进去,地铁站点的接驳需求变化理论上会影响周边站点。另一个是和强化学习结合,用预测结果直接驱动调度车辆路径规划,形成一个完整的决策闭环。这些都是我现在在探索的方向。整个项目做下来,最大的收获不是模型跑通的那一瞬间,而是理解了“预测准确率提升1个点”和“调度效率真正提升10个点”之间,需要整整一套工程化的桥梁。