ARTICLE DETAIL

资讯详情

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

共享单车预测与调度Python源码:LSTM/GRU时间序列模型实战

共享单车预测与调度Python源码:LSTM/GRU时间序列模型实战 简介压缩包内含一套基于深度学习的共享单车预测与调度完整解决方案面向毕业设计、课程设计及共享出行方向初学者。项目以神经网络构建单车需求量与时段、地理画像的关联实现分区域精准预测并引入蚁群算法规划最优调度路径兼顾预测与调度两端。包内共十六个文件以十一个Python脚本为主覆盖从地理编码解码、区域划分、需求统计到BP神经网络训练、误差计算与蚁群算法优化的完整流程另有四个numpy数据文件和一个文本说明文档辅助上手。所有脚本均已测试运行通过可直接用于课程实验或毕业设计二次开发。整体仅五百四十八KB轻量易部署已有三百七十四人下载学习适合具备Python与机器学习基础的计算机相关专业学生参考。1. 共享单车预测与调度为什么值得用深度学习解决有人把调度员从早忙到晚站点还是出现空桩和满桩问题不在调度车跑得慢而在预测错得太远。共享单车预测与调度这个方向就是先拿深度学习把“未来三小时哪个站点缺车、哪个站点淤积”算出来再让调度方案跟着预测结果走。标题里那套 python 源码 zip做的就是这件事数据清洗、序列建模、调度生成一体打包适合做城市交通项目的从业者也适合拿它当毕业设计起点的同学。它把共享单车预测抽象成一个可复现的时间序列问题同时把调度环节做成一个能直接跑出方案结果的模块避免你在业务里从零搭一套。2. 数据先于模型把原始订单变成站点小时级需求矩阵的完整链路2.1 从订单表到站点小时级需求矩阵聚合与补零不管 zip 里给的原始数据是 csv 还是 sqlite核心字段一般是这几类订单编号、单车编号、用户类型、租车时间、租车站点、还车时间、还车站点。要做预测第一件事是把「一次骑行」变成「一个站点在一个小时内的租车量和还车量」。常见做法是直接用 pandas 按站点和小时聚合代码很短但有几个细节值得较真。import pandas as pd df pd.read_csv(orders.csv, parse_dates[start_time, end_time]) # 对齐到小时 df[start_hour] df[start_time].dt.floor(H) df[end_hour] df[end_time].dt.floor(H) hire ( df.groupby([start_station_id, start_hour]) .size() .rename(hire_cnt) .reset_index() ) return_ ( df.groupby([end_station_id, end_hour]) .size() .rename(return_cnt) .reset_index() )parse_dates 参数把两个时间字段转成 datetime 类型dt.floor(H) 会把 08:45 归到 08:0008:12 也归到 08:00这样同一小时的骑行都进同一个桶。groupby 后面的 size() 数的是记录条数也就是这一小时这个站点的订单量。需要注意跨小时订单8 点租车 9 点还车租车量记在 8 点还车量记在 9 点这符合站点存量的物理含义没有问题。聚合出来的表往往不连续某个站点某小时一条订单都没有这个站-小时组合就会缺行。直接把缺行交给深度学习模型模型会把缺值当成 NaN或者因为张量对齐失败而报错。所以下一步要 reindex 补零把缺失的小时填成 0而不是删掉。idx pd.MultiIndex.from_product( [range(n_stations), pd.date_range(train_start, train_end, freqH)], names[station_id, hour], ) hire hire.set_index([station_id, hour]).reindex(idx, fill_value0).reset_index()补零的意义在于让时间轴完整模型才能看到连续的周期模式。如果某个站点连续几天完全无订单那它大概率已经停用这种站点要么在预处理里标记成静态零要么直接过滤掉否则会把 loss 拖向一个很小的均值。我一般会把「全量历史订单数小于某个阈值」的站点列出来单独存一个 low_activity 名单调度环节对它们用简单规则兜底而不是让深度模型去拟合噪声。2.2 天气、节假日和空间特征哪些能进模型哪些会引入时间穿越天气是共享单车需求最敏感的外部变量。容易踩的问题是把天气数据当成「当天标签」直接 merge 进训练样本。比如用当天的实测温度去预测当天的未来三小时离线测试看起来很好上线后调度模块运行在 t 时刻只能拿到 t 之前发布的天气预报根本拿不到未来实测值。正确做法是把天气特征按「预报发布时间」对齐到小时训练和推断都只用 t 时刻之前能拿到的信息。import numpy as np def build_features(df, weather, poi): # 周期特征用 sin/cos避免 23 点和 0 点被模型当成两个极端 df[hour_sin] np.sin(2 * np.pi * df[hour].dt.hour / 24) df[hour_cos] np.cos(2 * np.pi * df[hour].dt.hour / 24) df[week_sin] np.sin(2 * np.pi * df[hour].dt.dayofweek / 7) df[week_cos] np.cos(2 * np.pi * df[hour].dt.dayofweek / 7) # 天气对齐这里 weather 索引是预报小时不能写未来实测值 weather_hour weather.set_index(hour)[[temp, humidity, windspeed, precip]] df df.join(weather_hour, onhour) # 周边兴趣点特征站点 500 米内地铁站、商场、住宅数量 df df.merge(poi, left_onstation_id, right_indexTrue, howleft) return df天气字段里温度、湿度、风速、降水和需求的关系不是线性的。雨天对租车量是抑制作用但还车量会滞后爆发因为用户会把车停在目的地附近然后等雨停。风速过高时共享单车的使用量整体下滑这类非线性关系交给模型去拟合没问题但特征数值要归一化。我习惯在进模型前用 sklearn 的 StandardScaler 对连续特征做标准化注意只拿训练集的均值和方差去 transform 验证集和测试集不能混在一起 fit。时间特征比天气好处理。直接用 hour 这个整数进模型会让 23 和 0 被切到两端模型要额外学一个“跨天很近”的关系。sin/cos 编码可以把小时和星期投到单位圆上23 点和 0 点在余弦值上接近这才符合人的直觉。节假日是另一个常见坑如果只是给一个 is_holiday 布尔值模型很难区分春节和普通周末的差异。更实际的做法是设置几个档位工作日、普通周末、小长假、长假首日、长假最后一日。不同档位对潮汐方向的影响完全不同分开编码比单纯布尔值要好。2.3 滑动窗口切分与数据集划分别让未来信息混进训练预测模型的任务是给一个长度为 L 的历史窗口预测未来 H 小时的需求。滑窗构造样本时需要注意两点一是步长常见做法是每小时滑一步这样样本之间有大量重叠会增加训练量但也能让模型更平滑如果数据量大可以每 15 分钟滑一步来降低训练成本。二是不能打乱样本顺序时间序列一旦 shuffle后面时间段的样本会泄漏给前面验证指标会虚高。SEQ_LEN 24 # 看过去 24 小时 HORIZON 3 # 预测未来 3 小时 def make_sequences(X, y, seq_len, horizon): Xs, ys [], [] for i in range(len(X) - seq_len - horizon 1): Xs.append(X[i : i seq_len]) ys.append(y[i seq_len : i seq_len horizon]) return np.stack(Xs), np.stack(ys)这里 X 的形状是 (总时间步, 特征数)y 的形状是 (总时间步, 2)两列分别代表租车量和还车量。第一个样本用第 0 到第 23 小时预测第 24 到第 26 小时下一个样本用第 1 到第 24 小时预测第 25 到第 27 小时。这样每个样本的标签都不会落在历史窗口内。数据切分按时间顺序走前 70% 训练中间 15% 验证最后 15% 测试。但要注意按日期切分而不是按样本数切分避免把一个完整星期的数据拦腰切开。我一般先按日期排序取日期边界做切分保证验证集和测试集各自覆盖至少一个完整周。模型在共享单车场景里的核心周期就是七天测试集如果只有三天模型吃了几个星期一的样本却拿一个单独的星期一去评估结果会剧烈抖动。3. 模型选型与源码结构LSTM 和 GRU 为什么是这类问题的默认解3.1 需求序列的特性为什么 Transformer 未必更优共享单车需求序列有两个明显特性周期性很强但周期会被天气、节假日和突发活动打乱。一个普通工作日地铁口早高峰的租车量曲线和前一星期很相似可一旦下雨曲线形状会变平甚至反向。所以模型首先要能记忆「上周一是什么样的」再去结合「今天的雨有多大」做修正。LSTM 就是为这种场景设计的。它的门控机制能让模型在长序列里保留星期级别的时间依赖同时又能对新的天气输入快速响应。GRU 是 LSTM 的简化版参数量更少训练更快在站点数量几百个、数据量几十万条量级的场景里效果和 LSTM 差距不大。相比之下Transformer 需要更大的数据量才能发挥注意力机制的优势小数据集上容易过拟合而且训练时间明显更长。如果你的数据规模到了全城上千站点、几年历史、分钟级粒度再考虑上 Transformer 或时空图网络才划算。源码包里的模型部分常见做法是给一个 LSTM 或 GRU 的封装类可以切换。这类项目里模型的输入维度取决于特征数输出维度取决于预测目标。我一般会让模型同时输出租车量和还车量这两个目标而不是分别训练两个模型因为它们共享同一套站点状态和天气特征联合学习反而能互相正则。3.2 拿到 zip 后别急着跑 train先看源码结构一个规范的预测与调度项目源码目录通常分成四块数据预处理、模型定义、训练评估、调度方案生成。zip 包解压后一般会看到类似结构shared_bike_prediction/ ├── data/ │ ├── raw/ │ ├── processed/ │ └── preprocess.py ├── model/ │ ├── lstm.py │ ├── trainer.py │ └── evaluate.py ├── dispatch/ │ ├── optimizer.py │ └── simulate.py ├── train.py ├── predict.py └── requirements.txt拿到压缩包后第一件事不是直接跑而是按顺序检查三处processed 目录里有没有已经切好的训练张量这决定你能不能跳过数据预处理train.py 最底部有没有 ifname main 入口这决定你从哪个文件开始requirements.txt 里有没有版本冲突特别要注意 pytorch 和 numpy 的兼容关系。很多中专包或课程设计包会把「数据预处理」和「模型训练」写在一个文件里跑一次要半天。如果 processed 里已经有 .npy 或 .pkl说明作者已经把特征工程跑完了你可以直接进模型调参阶段。如果只有 raw 数据那就要自己走一遍第 2 章的流程。3.3 训练主循环从 batch 形状到梯度裁剪在 PyTorch 里定义一个共享单车 LSTM 模型核心就是输入张量形状和输出头。输入形状是 (batch, seq_len, features)batch_firstTrue 让时间维放在第二维读代码的人不容易晕。import torch import torch.nn as nn class BikeLSTM(nn.Module): def __init__(self, n_features, hidden_size128, num_layers2, horizon3): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, ) self.head nn.Linear(hidden_size, horizon * 2) def forward(self, x): # x: (batch, seq_len, n_features) out, _ self.lstm(x) last out[:, -1, :] # 只取最后一个时间步的隐状态 return self.head(last).view(-1, 2, horizon)输出维度是 batch × 2 × horizon2 对应租车量和还车量horizon 对应未来 3 小时。这样输出的 6 个数值可以直接 reshape 成 target 的形状计算 loss 时不需要写循环。hidden_size 决定 LSTM 内部记忆容量站点特征越复杂这个值越要大但也不能大到把噪声都背下来。训练循环里除了常规的 loss.backward() 和 optimizer.step()我还会加两件事梯度裁剪和学习率衰减。optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(80): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss_value nn.SmoothL1Loss()(pred, yb) loss_value.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()loss 我不用 MSE 而是 SmoothL1Loss也叫 Huber 损失。共享单车需求里有极端值比如演唱会散场后某站点突然涌入几百次还车MSE 会被这些离群点带着走Huber 损失在误差大的地方从平方退化成线性对极端值宽容很多。梯度裁剪同理LSTM 在长序列上训练容易出现梯度爆炸把梯度范数限制在 1.0 以内训练过程不容易崩。StepLR 每 20 个 epoch 把学习率降一半让模型在后期做精细调整。4. 把源码跑起来训练命令与四个必调参数4.1 python 环境安装与最小跑通命令这类源码一般基于 PyTorch 或 TensorFlowzip 包里会带 requirements.txt。新建一个干净的虚拟环境是第一步避免把系统里的 python 环境搅乱。我通常用 conda 或 venvpython 版本选 3.10 左右比较稳太新的版本偶尔会遇到 PyTorch 轮子没跟上。conda create -n bike python3.10 conda activate bike pip install torch --index-url https://download.pytorch.org/whl/cpu pip install numpy pandas scikit-learn在本地机器上先装 CPU 版 torch 就足够跑通小规模训练和预测。如果之后要上 GPU再按 CUDA 版本重装对应轮子。安装包的时候如果卡在下载阶段多半是网络问题可以换个镜像源但这是环境问题不是项目本身的问题。跑通最小流程只需要两个文件train.py 和 predict.py。假设源码包已经按第 2 章把数据预处理成 processed/bike.pkl训练命令通常是python train.py --data data/processed/bike.pkl --seq-len 24 --horizon 3 --epochs 50训练结束后模型权重会写到 checkpoints 目录再执行预测python predict.py --checkpoint checkpoints/best_model.pth --station 123 --steps 24predict.py 会加载模型取最新 24 小时的历史数据输出站点 123 未来 24 小时的租车量和还车量曲线。调度模块再拿这份曲线生成调车建议。如果包的作者把接口写得不太规范命令行参数可能需要改最少要先确认 train.py 里默认的 data 路径和实际 processed 目录名字一致。4.2 四个必调参数sequence_length、hidden_size、batch_size、learning_rate参数不是玄学每一个都直接对应数据规模。共享单车预测里我先调这四个参数常用区间影响调参建议seq_len24 到 72 小时决定模型看多长的历史序列太短学不到日周期太长引入噪声先用 24验证集欠拟合再往上加到 48、72hidden_size64 到 256隐状态维度决定模型容量站点多或特征多时上调数据量少时保守batch_size32 到 64影响梯度平滑度和训练速度小数据集用 32不要在时序数据上开太大learning_rate5e-4 到 2e-3决定收敛速度和训练稳定性从 1e-3 开始配 StepLR 衰减seq_len 试参时先看验证集 loss 曲线。如果 seq_len 从 24 加到 48 之后验证 loss 明显下降说明 24 小时不够模型看清完整日周期如果加长后验证 loss 反而上升说明模型被太远的过去干扰了。hidden_size 不是越大越好站点数量只有 50、样本量不到 10 万时hidden_size 开 512 会让训练很慢还过拟合。batch_size 在时序数据上有讲究。共享单车历史序列里相邻样本高度重叠batch 太大会让梯度冗余太小则抖动剧烈。我一般从 32 起步显存不够就降到 16128 以上很少用。learning_rate 是最容易出现翻车现场的地方1e-2 会让 loss 在训练初期直接冲上天1e-4 又收敛太慢。用 Adam 优化器时 1e-3 是常见起点后期靠 StepLR 衰减。4.3 调度模块如何接入预测结果训练和预测只是上半场调度模块才是把预测变成业务动作的部分。共享单车调度的本质是根据未来几小时的租还车量计算出每个站点的净流入把淤积站点的车搬到缺车站点。预测模块输出的租车量和还车量是分开的两组数先做差得到净流入。def net_inflow(pred_hire, pred_return): # pred_hire: (stations, hours) # pred_return: (stations, hours) return pred_return.sum(axis1) - pred_hire.sum(axis1)净流入为正表示这一站未来会堆车需要调出为负表示这一站未来会缺车需要调入。调度算法要做的是决定从哪里搬到哪里、搬多少辆。最简单的实现是贪心把淤积量最大的站点和缺车量最大的站点配对。def greedy_dispatch(net_flow, current_stock, capacity): surplus sorted([(s, v) for s, v in enumerate(net_flow) if v 0], keylambda x: -x[1]) deficit sorted([(s, -v) for s, v in enumerate(net_flow) if v 0], keylambda x: -x[1]) moves [] for s, avail in surplus: for d, need in deficit: move min(avail, need, max(0, capacity[d] - current_stock[d])) if move 0: continue moves.append((s, d, move)) avail - move need - move current_stock[s] - move current_stock[d] move if avail 0: break return moves这段代码的关键约束是 capacity[d] - current_stock[d]它保证搬入后的站点不会超过桩位容量。贪心解不是全局最优但胜在快可以作为基线方案先跑通业务流。如果调度车有容量上限、装卸时间、站点间距离等约束就要把问题升级成整数线性规划用 PuLP 或 OR-Tools 求解源码包里如果没实现你可以把它当作进阶改造项。5. 共享单车预测与调度的避坑指南5 个让预测翻车的常见问题5.1 现象小站点预测值被“平均化”调度方案直接忽略它们现象模型训练完成后大站点预测曲线有高有低小站点却几乎是一条水平线数值接近全站平均值。调度模块认为小站点没有缺车风险实际运营里小站反而经常一辆车都没有。原因绝大多数站点的需求稀疏一天只有几次租还记录模型为了让整体 loss 最小学会了输出一个平均期望值。站点之间的样本量差距太大少数大站主导了梯度方向。解决在 loss 里按站点样本量加权小站点的误差给更高权重。station_count y_train.sum(axis(1, 2)) weight torch.clamp(station_count / station_count.mean(), 0.2, 5.0) loss (loss_per_sample * weight).mean()clamp 把权重限制在 0.2 到 5.0 之间避免某个极端小站点权重过大主导整个训练。更彻底的方案是直接把 low_activity 站点从深度模型预测清单里摘出来用“过去 7 天同一小时平均 天气修正”兜底这类站点的调度需求本来就弱不值得为它增加模型复杂度。5.2 现象租还车量被当成连续值RMSE 很低但调度单没法执行现象验证集 RMSE 只有 2.3看起来不错但预测值是 0.7 辆、1.2 辆这样的非整数调度算法不知道是该搬 0 辆还是 1 辆。原因模型输出层是线性层默认假设目标服从高斯分布可租还车量是非负整数而且有大量 0 值。RMSE 对大量 0 的序列不敏感评估好看不代表决策可用。解决对目标做 log1p 变换再让模型去拟合变换后的值预测完用 expm1 还原最后取整。y_log np.log1p(y) # 训练标签 y_pred np.expm1(model(x)) # 预测还原 y_pred np.clip(np.round(y_pred), 0, None)log1p 压缩大数值的尺度0 映射到 010 映射到约 2.4模型不需要再硬学一个右偏分布。评估指标也换一下多用带符号误差或分位数误差看「预测比实际大了多少、小了多少」这比平均绝对误差更接近调度业务的语言。5.3 现象离线测试很好一上线调度就恶化问题出在天气特征现象回测时缺车率下降了 20%切到线上跑了一周调度效果反而不如原来的固定班次。原因特征工程用了“预测时刻的实测天气”。离线训练时这个字段来自历史数据线上推理时还没有未来实测值只能填 0 或填预报值训练和推断的特征分布不一致模型当然翻车。解决训练和推理都只用 t 时刻之前已经发布的气象预报数据。更稳妥的做法是在特征列里明确标注信息时间temperature_now 用上一小时观测值temperature_future 用未来 3 小时的预报值缺失的预报列用 0 或中位数填充。# 错误做法直接取目标时刻的实测天气 # df[temp] weather_at_target_hour[temp] # 正确做法只取当前时刻能拿到的观测值 df[temp_last] weather.shift(1)[temp] df[precip_forecast] forecast.set_index(issue_time)[precip]这个坑在共享单车预测里非常隐蔽因为离线评估时你不会意识到代码里用了未来信息只有上了线才发现预测分布漂移。拿到的源码包如果没有在特征列里区分观测和预报建议先按这个思路改造再上线。5.4 现象调度量巨大且反复倒腾车辆在站点之间来回搬运现象调度方案生成后调度车一上午要在十几个站点之间来回跑调车量比需求量大好几倍运输成本反而更高。原因调度模块直接用点预测结果做决策没有考虑预测的不确定性。某个站点的预测净流入是 -3但置信区间是 -1 到 -8算法直接按 -3 排了一辆车等真实需求出来发现只需要 -1白跑一趟。解决给调度模块加一个空闲阈值只有预测缺车量超过阈值才触发调度。if predicted_deficit dispatch_threshold: schedule_to(station, amountpredicted_deficit - safety_stock)dispatch_threshold 可以通过历史误差分布来确定我一般取预测误差的 80 分位数。缺车量预测误差在正负 2 辆以内就把阈值设为 3让调度只对大缺口响应。这样调度次数会下降缺车率不会明显反弹整体成本反而更优。5.5 现象zip 解压后路径报错代码里引用的文件找不到现象下载的 python 源码 zip 解压后执行 train.py 报 FileNotFoundError找不到 data/processed/bike.pkl。检查目录结构发现文件明明存在。原因zip 解压时外层多套了一层同名目录或者代码里用的是相对路径而你在其他目录下运行。Windows 下还会遇到文件编码问题processed 文件名里带中文或空格pandas 读取时报错。解决先看目录结构再统一用 Path(file) 锚定基准路径。# 解压后先列出第一层目录 tree /Ffrom pathlib import Path BASE_DIR Path(__file__).resolve().parent DATA_PATH BASE_DIR / data / processed / bike.pkl用 Path(file) 之后无论你从哪个目录执行脚本实际寻根都会落在 train.py 所在目录不会受到终端当前目录和 zip 外层嵌套的影响。另外注意不要把 zip 包直接解压到中文路径下某些旧版 pandas 或 torch 在中文路径下会读不到缓存文件这是血泪经验。6. 判断这套方案值不值得投入验证方法与进阶改造6.1 回测框架先于调参用调度收益评估模型共享单车预测是手段调度才是目的。所以验证时不只看 RMSE更要看「用了预测模型之后比固定班次调度多减少了多少缺车小时数」。缺车小时数指的是站点库存为 0 且仍有人想租车的小时数满桩小时数同理。回测时把历史数据切成天站在每天 6 点用前 72 小时数据预测未来 24 小时需求生成调度单再拿真实发生的数据去比对统计缺车小时数、满桩小时数和调度里程。这个框架跑通之后换模型、换参数才有意义否则你只是在优化一个和业务脱钩的指标。6.2 从站点级预测升级到区域级预测的实际改造站点级模型噪声大是结构性问题不是调参能解决的。可以把站点按 500 米网格聚合先预测区域级净流入调度车在区域之间搬运到达区域内再按站点存量做二次分配。区域级数据更密集模型拟合更稳站点级的缺口兜底交给规则。我第一次做这个项目时把大量时间花在换网络结构上调度收益一直上不去后来把目标从点预测改成区间预测、把评估从 RMSE 改成缺车小时数业务指标才真正改善。这个方向的关键不是模型有多深而是预测和调度目标是否对齐希望帮到你。本文还有配套的精品资源点击获取
返回列表