ARTICLE DETAIL

资讯详情

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

基于LSTM与遗传算法的共享单车调度路径优化

基于LSTM与遗传算法的共享单车调度路径优化 简介基于Python与BP神经网络开发的共享单车调度系统完整源码面向计算机科学、数据科学与大数据、人工智能等专业在校学生、教师及行业入门者主要解决共享单车区域分布不均时的最优调度路径规划问题。压缩包共含16个文件包括11个Python脚本、4个NumPy数据文件与1个说明文档总大小约548KB结构精简、便于部署学习。脚本依次覆盖geohash坐标解码、区域划分、POI辅助决策、需求统计、训练与测试数据生成、BP神经网络建模、平均误差计算以及蚁群算法路径寻优等关键环节npy文件用于存储输入输出样本说明书则对运行流程作必要提示能完整体现数据驱动调度的实现思路。项目兼具算法完整性与工程可读性已有160人学习下载适合作为毕业设计、课程设计、学科竞赛或入门进阶的实践素材也支持在此基础上扩展二次开发。1. 共享单车调度系统神经网络为什么能算最优单车调度路径调度车每天在城市里跑上百公里却常有一半车厢是空的早高峰把车从住宅区运到地铁口晚高峰又原路搬回去。共享单车调度系统的核心难点不是车队规模而是怎么算出这条最优单车调度路径——往哪些站点调车、调多少辆、按什么顺序走能让调度车里程和站点空满率同时说得过去。用Python做一套基于神经网络的共享单车调度系统工程上的分工通常很明确神经网络负责预测每个站点未来一两个小时的借还车差额路径优化程序负责把这堆差额转换成一个接近最优的访问顺序。这套方案适合正在做智能调度、想给现有系统加预测和路径能力的开发者也适合想用Python把时序预测和组合优化前后端打通的同学。2. 把调度问题拆成“预测差额 求最优路径”神经网络在这里管预约不直接生成长路线很多人拿到这类源码工程会有一个误区以为神经网络应该直接输出一整条“先到A站再到B站”的路径。真实工程不会这么写原因后面会展开。常规做法是把调度问题拆成两个子问题预测站点需求差值再对调度车访问顺序做组合优化。神经网络负责前者元启发式算法负责后者这样的系统既稳又敢上线。2.1 先理解调度对象站点净差额和车辆容量一个站点在一段时间内的净差额定义为“归还量 - 借出量”也可以是“借出量 - 归还量”关键是一套代码里从头到尾只用一个方向。比如某个站未来2小时预测归还60辆、借出20辆净差额为40说明站上会胀库调度车要优先来把车拉走反过来如果归还是20、借出是60净差额为-40调度车要在这个站卸下车辆补位。调度车本身有容量约束常见调度三轮车一次能装12到18辆这个数字在路径规划里就是背包容量。理解了这个最优单车调度路径的优化目标就清楚了在满足各站调运需求的前提下让调度车的总行驶里程最短。它是一个典型的带容量约束的路径问题站点数量通常几十到几百个但真正需要进调度计划的目标站往往只有十几个。所以路径搜索空间没有想象中大遗传算法完全跑得动。2.2 模型选型为什么从LSTM起步而不是前馈或CNN调度预测对象是时间序列数据天然带早晚高峰的周期性和连续几天的趋势性这就决定了默认候选是循环神经网络里的LSTM。前馈神经网络也能做但你需要手动构造滞后特征、滚动均值、星期类型等几十列输入工程量大且效果不一定更好。卷积神经网络擅长抽取局部空间特征用在站点坐标成图、周边POI热度这类二维输入上有价值但单纯预测时序差额时优势不明显还会让网络结构变重。LSTM的门控结构能自动记住几天前同一时段的高峰形态是最稳的起步选择。如果你的站点的借还车行为受天气和活动影响明显可以把天气温度、节假日特征并进输入向量而不是指望LSTM从订单流水里自己推出来。这一点在后面的特征工程里会具体落到代码。2.3 输入输出定义一个调度窗口的需求向量我给这个系统定义的预测粒度是30分钟一个调度窗口是未来2小时也就是4个时段。模型输入用过去48个时段一天的历史数据输出未来2小时的净差额。这个窗口长度兼顾时效性和稳定性窗口太短比如15分钟调度车还没到站预测就过期了窗口太长比如6小时高峰形态变化大误差累积明显。输入张量的形状是(站点数, 48, 特征数)特征数根据你合并的外部数据来定。下面是一组我在类似项目里常用的字段定义特征类型说明该时段借出量数值按站点和时段聚合该时段归还量数值按站点和时段聚合站内当前车辆数数值借出减归还后的存量估计是否周末0/1周末骑行节奏与工作日明显不同温度数值超过30度或低于0度时骑行量骤降是否为高峰时段0/1早高峰7-9点、晚高峰17-19点输出层只放一个神经元预测未来2小时净差额用MSE作为损失函数。我见过有人尝试一次输出48个未来时段做成多步预测效果在短窗口内并未超过单步模型却把训练时间拉长了一倍。这个取舍不值得。3. 数据准备与特征工程从订单流水到训练样本的完整转换再好的神经网络也怕脏数据。调度系统里最常见的数据源是两张表站点基础表和订单流水表。站点表给出站点ID、经纬度和容量订单流水表给出每一笔借还记录和对应站点。你需要把这两张表加工成模型能吃的三维张量。3.1 两张核心表站点表与订单流水站点表长这样我们只关心站点ID、名称、经纬度和容量上限。经纬度在路径优化里用来算站点间真实距离容量上限在预测之后用来判断某站是否容易被预测的净差额顶到胀库或搬空。订单流水表通常一个城市一个月就是几百万行字段再多也只用得上借车时间、借车站点、还车时间、还车站点四个字段。多余的用户ID字段在聚合阶段反而拖慢速度。先写一段加载和基础校验的代码import pandas as pd stations pd.read_csv(data/stations.csv) trips pd.read_csv(data/trips.csv, parse_dates[borrow_time, return_time]) # 剔除经纬度缺失的站点这类站点在算距离时会直接报错 stations stations.dropna(subset[lng, lat]) # 订单里出现未知站点ID多半是历史关站残留过滤掉 valid_ids set(stations[station_id]) trips trips[trips[borrow_station].isin(valid_ids)] trips trips[trips[return_station].isin(valid_ids)] print(f有效站点数: {len(stations)}, 有效订单数: {len(trips):,})这段代码的逻辑是先把脏数据挡在特征工程之前。站在一线工程视角这类数据清洗不是可选步骤——站点经纬度缺失会让后面欧氏距离计算直接得到NaN未知站点ID会让聚合结果出现幽灵行。用isin过滤而不是merge后删空是因为你需要明确知道丢了多少数据便于给业务方交底。3.2 构造站点时段特征矩阵下一步是把订单流水按站点和30分钟时段聚合生成每个站点的时序矩阵。典型的做法是先用pivot_table把借出量和归还量分别展开成“站点 × 时段”的宽表再叠加上时间特征和站点存量特征。注意时段索引要连续缺失的时段补0不要用dropna把空档删掉否则LSTM看到的序列长度就不再是固定长度。freq 30min # 借出量聚合每个站点在每个30分钟内发生了多少笔借车 borrow (trips.groupby([borrow_station, trips[borrow_time].dt.floor(freq)]) .size().rename(borrow_count).reset_index()) # 归还量聚合结构同上 return_ (trips.groupby([return_station, trips[return_time].dt.floor(freq)]) .size().rename(return_count).reset_index()) # 透视成 站点 × 时段 矩阵缺失时段自动补0 borrow_wide borrow.pivot_table(indexborrow_station, columnsborrow_time, valuesborrow_count).fillna(0) return_wide return_.pivot_table(indexreturn_station, columnsreturn_time, valuesreturn_count).fillna(0)这里的dt.floor(30min)会把 10:17 归到 10:00 到 10:30 这个时段天然屏蔽了秒级噪声。聚合后有两个细节值得注意一是fillna(0)必须显式做pandas 在透视时会把组合中不存在的格子留成 NaN带着 NaN 进LSTM会直接污染梯度二是两个宽表的列索引必须按时间对齐如果订单导入延迟造成某个站点少一段记录后续拼接特征时会出现错位所以建议统一用reindex补全所有时段后再合并。3.3 时间特征和标签未来2小时净差额特征矩阵拼完之后要给每个时段补充时间特征和外部特征。时间特征不是直接把“10:00”丢给神经网络而要做成星期类型、是否高峰等离散列。标签则取未来2小时的净差额也就是以当前时段为基准往后数4个时段用未来4个时段的“归还量合计 - 借出量合计”作为监督值。这里最容易出错的是对齐构建标签时必须从数据末尾切掉最后4个时段否则未来时段不存在标签会整体错位一行。import numpy as np # 取某个站点的完整时序数据 sid stations[station_id].iloc[0] s_borrow borrow_wide.loc[sid].astype(float) s_return return_wide.loc[sid].astype(float) # 滑窗构造特征序列每个样本用前48个时段 seq_len 48 lookahead 4 # 未来2小时 4个30分钟 feature_cols [borrow, return, is_weekend, is_peak] examples, labels [], [] values np.stack([s_borrow, s_return], axis1) for i in range(seq_len, len(values) - lookahead): window values[i - seq_len:i] # 这里可以并上 is_weekend、is_peak 等外部特征列 examples.append(window) future values[i 1:i 1 lookahead] labels.append(future[:, 1].sum() - future[:, 0].sum()) # 净差额 X np.array(examples) y np.array(labels) print(X.shape, y.shape)这段循环把滑动窗口的语义讲清楚了每个样本看到过去24小时的借还曲线预测下一个调度窗口会胀库还是会缺车。future[:, 1]是归还量future[:, 0]是借出量相减就是净差额。实际工程中感受最深的坑是标签方向不稳定有的团队用“借出 - 归还”有的用“归还 - 借出”如果代码里没有统一写清楚遗传算法那边会把正负号理解反调度车往错误的方向跑。4. 用Python搭建神经网络调度系统训练、调参与输出最优路径模型选型和特征准备好之后进入核心搭建环节。这一章给出一个最小可跑的代码链路LSTM定义、训练、预测站点差额字典、再用遗传算法求最优单车调度路径。代码全部基于纯Python和TensorFlow/Keras不依赖额外的商业求解器方便你直接改成自己的工程。4.1 定义LSTM网络序列长度、隐藏层与超参选择LSTM模型的输入形状是(batch, 48, 特征数)输出只有一个数值。我一般用两级LSTM第一层返回完整序列第二层只返回最后一个时间步的输出后面接两个全连接层。第一层设64个单元第二层设32个单元在20到100个站点的数据规模上训练速度和精度都比较均衡。Dropout放在两个LSTM层之间取值0.2对防止过拟合有效果。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense n_features X.shape[2] model Sequential([ LSTM(64, return_sequencesTrue, input_shape(seq_len, n_features)), Dropout(0.2), LSTM(32), Dense(16, activationrelu), Dense(1) # 输出未来2小时净差额 ]) model.compile(optimizeradam, lossmse, metrics[mae]) print(model.summary())return_sequencesTrue这一行的意义是让第一层输出完整的时间步序列第二层才能继续做时序计算如果第一层不保留序列第二层LSTM根本收不到时序行为。Dense(16) 用ReLU激活是为了让网络具备拟合非线性借还行为的能力。这里的n_features由上一章的特征矩阵决定如果后面在特征工程里加了天气列模型代码不需要改动Keras会自动把输入维度适配过来。4.2 训练与预测把模型输出转成站点差额字典训练参数里最值得调的是batch_size和epochs。这个任务的数据量通常是几万到几十万样本batch_size32起步显存不够就降到16epochs30配合validation_split0.15看验证集MAE如果15轮后验证损失不再下降就提前停。不建议一上来就跑50轮LSTM在小数据集上很容易过拟合验证集曲线会先降后升。history model.fit(X, y, batch_size32, epochs30, validation_split0.15, verbose1) # 预测所有站点的净差额生成调度需求字典 def predict_demands(model, feature_map, stations, seq_len): demands {} for sid in stations[station_id]: recent feature_map[sid][-seq_len:] x recent.reshape(1, seq_len, n_features) pred float(model.predict(x, verbose0)[0, 0]) demands[sid] round(pred, 1) return demands # feature_map: dict[站点ID] - (seq_len, n_features) 的矩阵这里feature_map是预测阶段准备的输入直接用每个站点最近48个时段的特征矩阵。训练时的滑窗循环在预测阶段简化为只看最后一段窗口省掉大量重复计算。预测结果放进字典后只保留绝对值大于阈值的站点进入调度计划通常取2辆车以上的差额才值得派调度车跑一趟。阈值设置过小会让路径优化在派车细节上空转设置过大会把本该补的站点漏掉后面避坑章节会具体讲。4.3 遗传算法求最优单车调度路径拿到站点差额字典后路径优化就变成一个带容量约束的站点访问顺序问题。遗传算法在这个规模下表现稳定编码方式选“站点排列”每个个体是一条从车场出发再回车场的访问序列。适应度函数把总行驶里程作为主目标未满足的调度需求量作为惩罚项这样即使某辆车容量不够遗传算法也会优先保证把最重要的站点调了。import random from math import radians, sin, cos, sqrt, asin def haversine(a, b): lng1, lat1 a; lng2, lat2 b p1, p2 radians(lat1), radians(lat2) dp p2 - p1 dl radians(lng2 - lng1) h sin(dp/2)**2 cos(p1)*cos(p2)*sin(dl/2)**2 return 6371.0 * 2 * asin(sqrt(h)) def evaluate(route, demands, dist_matrix, vehicle_cap): total_dist, load, penalty 0.0, 0.0, 0.0 cur 0 # 车场编号 for sid in route: total_dist dist_matrix[cur][sid] cur sid d demands[sid] # 正数表示要拉走负数表示要放下 if d 0: take min(d, vehicle_cap - load) penalty (d - take) * 500 load take else: give min(-d, load) penalty (-d - give) * 500 load - give total_dist dist_matrix[cur][0] return total_dist penalty def ga_dispatch(demands, dist_matrix, vehicle_cap, pop_size80, generations200): nodes list(demands.keys()) pop [random.sample(nodes, len(nodes)) for _ in range(pop_size)] for gen in range(generations): pop.sort(keylambda r: evaluate(r, demands, dist_matrix, vehicle_cap)) new_pop [pop[0]] # 精英保留 while len(new_pop) pop_size: p1, p2 random.sample(pop[:pop_size//3], 2) # 顺序交叉OX保证子代仍是一个合法排列 cut1, cut2 sorted(random.sample(range(len(nodes)), 2)) child p1[cut1:cut2] for g in p2: if g not in child: child.append(g) if random.random() 0.15: # 交换变异 i, j random.sample(range(len(child)), 2) child[i], child[j] child[j], child[i] new_pop.append(child) pop new_pop return pop[0]适应度函数里的vehicle_cap是关键参数调度车一次能装多少辆车直接决定路径形态。容量小车必须频繁回车场卸载路径总里程上升容量大一个调度车就能覆盖更多站点但车辆满载导致灵活性下降。penalty系数500表示一次未满足的调度需求按500单位成本计入适应度这个系数如果设得太低遗传算法会倾向于牺牲偏远站点的调度来省里程设得太高又会为了多调一辆车绕路10公里。系数一般参考单次调度的人力与油耗成本标定。遗传算法的超参数在20到100个目标站点时pop_size80、generations200是一个性价比很高的起点。代码里的精英保留机制保证最优解不丢失OX交叉保证了子代不出现站点重复交换变异提供跳出局部最优的随机性。在调度规模扩大时优先加大迭代代数而不是种群规模收益更稳定。5. 调度系统落地避坑5个最容易翻车的现场这一章写的是把系统从“能跑通”推到“能上线”过程中我真实遇到过的翻车问题。每条都按现象、原因、解决来写你可以直接照着排查自己的项目。5.1 小样本站点预测等于白算现象模型跑完十几个站点的预测差额几乎全部接近同一个数有的站点日均订单只有个位数预测结果几乎完全一样。原因LSTM在极度稀疏的时间序列上很难学到有效模式梯度被高流量站点主导小站点相当于在拟合站点均值。解决按站点日均订单量分组建模。日均订单低于20的小站点不要进LSTM直接使用同类站点的历史同期均值加上周末偏移量做兜底预测模型只负责日均订单较高的活跃站点。这样整体预测精度反而是提升的因为模型不再被迫为噪声数据花钱买参数。5.2 预测差额和调度容量单位不一致现象遗传算法输出的路线里调度车经常出现“装不下还要装”的情况同一个站点同一辆车要跑两趟。原因预测差额是“辆”调度车载重也是“辆”但代码里直接用浮点数相减没有做整数化和容量上限校验。训练时预测值会带小数一个站点的预测差额是1.4辆车遗传算法却把它当成真实数量去累加累积误差让容量约束失效。解决对预测差额做round取整并在遗传算法适应度函数里硬性约束min(d, vehicle_cap - load)。凡是预测值小于1的数直接视为不需要调度。这个改动看似简单实际能把路径里程下降10%以上。5.3 时间边界错位高峰时段整体平移现象训练集损失很低但预测曲线显示早高峰的缺车时段比真实情况提前了1小时。原因订单时间表解析时带了时区偏移或者站点数据的基准时间与服务器默认时区不一致。比如订单存的是UTC时间但周末特征按本地时间计算预测窗口就整体错位了。解决在数据加载阶段统一时间戳时区建议全部按本地固定偏移量处理并在特征工程入口打印一天的时段对齐表做人工核验。时间对齐问题在代码里不报错是最难排查的静默故障。5.4 遗传算法把同一个站点访问两次现象调度路线图里出现一个站点被两辆调度车先后访问但该站点差额只有一辆车。原因初始种群生成用random.sample确保不重复但OX交叉算子实现有缺陷比如把父代1的片段和父代2的剩余站点拼接时没有检查重复导致子代里出现重复站点。这是所有排列编码遗传算法实现里的经典bug。解决在交叉后加一轮去重校验用set长度判断子代是否合法。更稳妥的做法是先用小规模数据跑一个 verify 脚本检查所有子代是否都是原站点序列的合法排列再进入正式迭代。5.5 模型上线两周后预测开始漂移现象第一周预测很准到第二周开始系统性偏差工作日预测偏低周末预测偏高。原因骑行行为受学校开学、天气转凉、新地铁站开通等因素影响训练集分布与当前分布逐渐脱节。神经网络不会自己察觉分布漂移。解决搭建每日增量训练任务用最近30天的数据重新训练模型旧模型只保留作为回测对比基线。增量训练不必从头开始在旧权重基础上继续训练10到15个epoch即可。同时在监控面板里记录每日平均预测误差误差连续3天超过阈值时自动告警触发重训。6. 验证调度效果的可靠方法历史回测、关键指标与参数敏感性分析调度系统上线前我最推荐先做一次历史回测拿上周的真实订单数据把模型当成“预言家”放进历史里跑一遍看它敢不敢在正确的时间把调度车派到正确的站点。回测不需要真车出动成本为零但能暴露八成上线后才看得出来的问题。回测第一步是从历史数据中选一段没有重大事件的时间区间比如一个完整的工作周加一个周末。用前30天训练模型预测这一周的每个调度窗口再把预测的调度路径与真实发生缺车或胀库的站点做交集比对。核心指标我用三个调度缺口满足率表示被正确调度了的站点数占真实需要调度站点数的比例单次调度里程按调度车实际行驶总里程除以调度车次数空满率变化对比引入调度前后各站点存量方差是否下降。三个指标里缺口满足率最重要它代表模型有没有在关键时刻做对关键决策里程是成本指标方差是业务改善指标。对遗传算法本身建议再做一次参数敏感性实验固定模型预测结果不变只把承运量分别设为10、15、20辆看路径总里程的变化。你会发现容量从10提高到15时里程显著下降从15到20则收益递减。这个临界点就是采购调度车辆的决策依据。我习惯在每个迭代版本里保留一次完整的回测结果快照换模型参数后拿新旧快照对比而不是只看训练集上那点损失数字。“预测变好了”和“调度效果变好了”是两回事前者看MAE后者看缺口的满足率。最后说一个我自己栽过的教训最初我迷信更复杂的网络结构觉得换上更大规模的LSTM一定能压过原有时序模型的精度结果验证集MAE只降了不到百分之三训练和部署成本反而涨了四倍。后来规规矩矩做数据清洗、把小站点分流出去、统一预测差额单位效果比换模型明显得多。这个领域的胜负手往往不在模型复杂度而在工程细节是否较真希望帮到你。本文还有配套的精品资源点击获取
返回列表