十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

限速坡道地铁节能策略:强化学习Q-learning与DQN代码包解析

限速坡道地铁节能策略:强化学习Q-learning与DQN代码包解析 简介这是一份面向地铁列车运行控制与能耗管理场景的强化学习项目核心是基于Q-learning算法优化列车在限速坡道条件下的牵引与制动策略以实现能耗最小化。代码工程围绕环境建模、控制模型与训练评估展开适合轨道交通自动化、计算机智能决策方向的研究者、工程师及高年级学生用作课程设计、科研预研或工程参考。压缩包共54个文件总大小约789KB以Python源码、CSV数据、XML配置为主要内容12个py脚本覆盖Q-learning及其DQN变体的实现10个CSV文件提供线路限速、坡度、时间误差及成本等实验数据另含说明文档与README便于快速上手并对照复现。当前已有78人学习读者可从中学到状态空间与动作空间的抽象方式、奖励函数设计、Q值更新与探索策略以及将强化学习嵌入到列车运动仿真中的完整思路。1. 限速坡道上的地铁节能策略这份强化学习代码包里到底有什么地铁列车的节能运行本质上是一个顺序决策问题每一秒要选牵引、惰行还是制动让全程能耗最小、准点且不碰限速。过去这类问题多用动态规划或解析最优控制可一旦线路叠加限速坡道状态转移矩阵迅速膨胀手工建模查表都不现实。这份代码用 Q-learning 封装了一个 gym 风格的地铁运行控制环境状态取位置和速度动作分牵引、惰行、制动几挡奖励函数把省电、准点、不超速折成一个标量训练后输出可解读的运行策略曲线。包里还附了 DQN 和 Dueling DQN 两种深度强化学习实现以及磁渠、宋家庄两段真实线路的限速数据和最小运行时间曲线。适合做地铁节能研究复现也适合拿来当强化学习的工程练习场景。2. Q-learning 在限速坡道场景的建模状态、动作、奖励三要素怎么取舍2.1 状态空间设计为什么是位置-速度二维离散粒度怎么定先说结论这份代码里状态取的是「位置 速度」二维组合不把坡度、限速单独编码成状态维度而是通过环境动力学和奖励函数隐式承载。这个选择背后的依据是马尔可夫性。列车的运动方程里下一步状态只依赖当前状态和当前动作。位置定了坡度、限速、曲线半径这些线路属性就是定值速度定了当前克服基本阻力所需的牵引力或制动力也基本定了。所以 (位置, 速度) 对下一时刻的状态有完全解释力不需要额外塞进历史信息也不需要给车重、站间客流单独开一个状态通道。表格型 Q-learning 的前提是状态离散。常见做法是沿线位置按 50100 米一段划分速度从 0 到线路最高限速按 25 km/h 一挡划分。以 2 公里区间、最高限速 80 km/h 为例位置取 30 段、速度取 20 挡就有 600 个状态再乘 5 个动作Q 表只有 3000 个格子训练起来非常轻松。可一旦把区间拉长到 10 公里以上的市域线位置段数破百状态数涨到两三万表格更新的代价和探索所需的数据量都会指数级上升这时就要考虑换 DQN也就是第 5 章要展开的内容。离散粒度对策略质量的影响非常直接。位置段分得太粗列车在 100 米范围内只能选同一个动作制动点对不齐坡道底部或限速变化点多余的速度要拿更多牵引去补能耗自然会高。速度挡分得太细相邻两挡之间的 Q 值差异小探索阶段容易反复横跳学习曲线发散。我一般建议先按上述量级粗调一轮等策略能跑通以后再加密一倍对比能耗曲线的边际收益。如果加密后能耗只降了不到 2%说明当前粒度已经够用不要再往上堆格子数省下来的训练时间可以拿来做奖励函数的权重调参。这套代码在 subway_env.py 里把上述逻辑封装成了环境类。线路数据、限速数据由 TrainAndRoadCharacter.py 读取并保存环境中查询当前位置的速度限制时走的是工具函数里线性插值出来的结果。需要注意一个细节限速并不是作为硬性状态过滤条件出现的而是通过「超速即给大负奖励」的方式实现的。这个设计的好处是让 Q-learning 自己学会躲开超速状态而不是在采样阶段就把动作空间截断坏处是奖励权重没调好时列车可能为了省电故意长时间贴着限速跑这一点在第 4 章的避坑里还会具体展开。2.2 动作空间与奖励函数牵引、惰行、制动与能耗惩罚的权衡动作空间必须贴合真实列车的控制接口。地铁牵引系统通常有几个牵引位、惰行位和分级制动位不可能像赛车游戏那样输出连续百分比油门。简化到强化学习环境里常见动作集是五挡最大牵引、中等牵引、惰行、小制动、大制动。每个动作经过 controlModel.py 转成目标加速度再交给 trainRunningModel.py 里的单质点动力学模型算出实际速度变化。单质点模型对地铁够用牵引力减去基本阻力、坡道附加阻力再除以整备质量与载荷修正系数就得到加速度。奖励函数是这份代码的灵魂。跑过 CartPole 的人都清楚CartPole 的奖励是一个固定 1倒杆就结束列车节能场景明显复杂得多必须在每一仿真步给出一个能同时反映能耗、准时和安全的标量。常见的分解方式是四部分安全项超限速给一个大的负奖励到达项准点到达终点给正奖励准时项运行时间与最小运行时间曲线的偏差越大惩罚越大能耗项牵引功率在时间上的积分作为功耗累加惩罚。写成代码大致是这样的结构def compute_reward(self, obs, done, info): r 0.0 if info[overspeed]: r - 10.0 # 安全约束超速一次重罚 if done and info[arrived]: r 50.0 # 到达奖励给足完成信号 r - abs(info[run_time] - self.min_time_curve) * 0.1 else: r - info[traction_energy] * 0.05 # 每步能耗累积惩罚 return r逻辑说明第一行是超速惩罚一次给一个大的负奖励相当于在状态空间里竖起一道围墙。第二行是到达奖励只有列车准点到达终点才触发用来抵消全程累计的能耗惩罚。第三行是运行时间与最小运行时间曲线的偏差码里 min_time_curve 对应的就是 minTimeCurve_ciqu.csv 和 minTimeCurve_songjiazhuang.csv。第四行是牵引能耗累积每一个仿真步把牵引功率按步长积分的结果累加惩罚这一步直接驱动算法减少无谓牵引。参数说明10.0、50.0、0.1、0.05 这四个数字决定了策略的倾向而不是随便填的。超速惩罚必须显著大于正常行驶单步奖励的绝对值否则算法会为了省电去冒超速的险。到达奖励定在 50 这个量级是为了盖过全程累计的能耗惩罚。你可以自己估算假设一个区间全程能耗折算 200 个单位每步惩罚 0.05累计约为 10和到达奖励同量级训练才不会只追求「到达」而忽略能耗。如果手一抖把到达奖励调到 500算法必然全力牵引跑完全程能耗报表直接崩掉。2.3 动态规划与 Q-learning 的分工minTimeCurve 不是用来算 Q 值的很多人在标题里看到「强化学习动态规」会问既然都已经在用动态规划求最小运行时间了为什么还要训练 Q-learning实际上 minTimeCurve 是用动态规划或解析方法算出来的「最短时间」基准它的定位是给奖励函数里的时间项提供一个标尺而不是直接参与 Q 值更新。换句话说动态规划负责把「准点」这个约束量化成数值Q-learning 负责在此约束下探索节电策略两者是分工关系。从理论上看在确定性环境里 Q-learning 会收敛到与值迭代等价的最优 Q 表也就是说 Q-learning 的无模型特性在这里没有明显优势。但列车运行不是纯确定系统载客量波动、轮轨黏着变化、列车质量误差都会让转移概率偏离设计值。动态规划一旦测得不准就要重新算全表Q-learning 只需要在真实环境里继续采样就能自适应修正。这也是工程上把两套东西放在一起的原因动态规划给出理论上限作为校验依据强化学习产出实际可由控制系统执行的策略。这里顺带提一句状态价值函数。很多入门者会把状态价值和状态-动作价值混在一起在 Q-learning 里更新的是 Q(s,a)也就是状态-动作价值。minTimeCurve 本质上是动态规划在这个确定性短时间问题里解出来的状态价值序列它反映的是「从某个位置出发最快多久能到终点」Q-learning 要学的则是在省电约束下每个状态该选哪个动作。两者不是同一个量不能直接拿 minTimeCurve 当 Q 表的初始值但可以用它来计算「时间余量」作为奖励函数里的动态惩罚项这是把先验信息注入强化学习的一种有效方式。3. 跑通 run_this.py从 CSV 数据到节能策略输出的完整链路3.1 代码结构subway_env 与 RL_brain 之间的调用关系第一次打开这个压缩包文件数量确实有点唬人但核心链路其实只有三个文件subway_env.py、RL_brain.py、run_this.py外加一个 trainRunningModel.py 做动力学推进。环境类负责回答四个问题当前状态是什么、执行某动作后状态怎么变、奖励是多少、这一轮是否结束。RL_brain.py 里的 QLearningTable 类负责回答一个核心问题在当前状态下五个动作的 Q 值各是多少应该选哪一个训练后如何更新。run_this.py 就是那个把两边反复对接的主循环。主循环的骨架和 OpenAI gym 的经典写法完全一致如果你跑过 cartpole 入门代码上手会非常快import numpy as np from subway_env import SubwayEnv from RL_brain import QLearningTable env SubwayEnv(ciqu) agent QLearningTable(list(range(env.n_actions)), learning_rate0.1, reward_decay0.9, e_greedy1.0) for episode in range(300): obs env.reset() total_reward 0 while True: action agent.choose_action(str(obs)) obs_, reward, done, info env.step(action) agent.learn(str(obs), action, reward, str(obs_)) obs obs_ total_reward reward if done: break if episode % 20 0: print(episode, total_reward, info)逻辑说明choose_action 内部按 ε-greedy 策略决定输出随机动作还是当前 Q 值最大的动作learn 更新 Q(s,a) 的核心公式是 Q(s,a) ← Q(s,a) α[r γ·max Q(s,a) − Q(s,a)]state 在传入学习器时转成字符串形式这是表格型 Q-learning 管理稀疏状态的一种常见做法直接以元组做字典键也行字符串在可视化调试时更直观。参数说明learning_rate0.1 表示每次更新只吸收 10% 的新信息防止单步噪声把 Q 表带偏reward_decay0.9 是折扣因子代表未来奖励折算到当前的价值0.9 左右在列车这种慢系统中较为稳妥接近 1.0 会让算法过分关注远期到达奖励训练前期学习变慢e_greedy1.0 表示初始化时全随机探索随训练轮次逐步衰减。3.2 数据准备限速 CSV 和最小运行时间曲线的格式检查跑通代码之前最容易被忽视的是数据文件本身的格式。speedLimit_ciqu.csv 和 speedLimit_songjiazhuang.csv 是两段线路上采集的限速数据通常每一行包含位置和限速值。minTimeCurve_ciqu.csv 对应的是同一段线路在纯最短时间目标下的运行曲线。这份代码里 tool.py 的作用就是把这些 CSV 读入并统一做插值处理。自己替换线路数据时建议先做一次简单的读入检查import pandas as pd speed pd.read_csv(speedLimit_ciqu.csv) min_time pd.read_csv(minTimeCurve_ciqu.csv) print(speed.head()) print(speed.describe()) print(min_time.head())逻辑说明head() 看列名和单位describe() 看位置列是否有覆盖不到的地方min_time 里的时间序列要和限速文件的位置范围对齐。很多翻车现场都是在这里埋下的比如限速文件的位置列是从大到小排列的插值函数如果用位置索引去查结果会完全错位。参数说明如果你自己的线路数据是 Excel 导出的先另存为纯 CSV注意编码统一 UTF-8列名不要带空格。位置列推荐用米作单位限速推荐用 km/h。代码内部如果不做单位换算高速区段会把 Q 表状态空间撑得非常大训练耗时直线上升。这也是为什么我在第 4 章会把单位换算单独列成一个坑。3.3 训练启动以后看什么reward 曲线、能耗曲线和时间误差训练不是启动完就坐着等至少要看三张曲线。reward.csv 里记录的是每一轮的回合总奖励理想情况下应呈现上升后进入平台期的形态costData.csv 里记录每个决策步的能耗累加重点看平台期的均值是否明显下降timeError.csv 记录到达时间与最小运行时间的偏差正值代表比最短时间慢负值代表比最短时间快。这三张图在 run_this.py 跑完后如果没直接输出可以自己用 pandas 读出来画一遍import pandas as pd import matplotlib.pyplot as plt cost pd.read_csv(costData.csv) time_err pd.read_csv(timeError.csv) reward pd.read_csv(reward.csv) fig, axes plt.subplots(1, 3, figsize(14, 4)) axes[0].plot(reward, labelreward) axes[1].plot(cost, labelcost) axes[2].plot(time_err, labeltimeError) plt.show()逻辑说明三张图并排看才能判断策略有没有在正道上前进。reward 上升说明算法的整体价值在提升但它可能只是因为准时性变好cost 下降才是节能的直接证据timeError 保持在一定容差内通常 ±30 秒才算没为了省电牺牲运行效率。参数说明画图时注意 costData 和 timeError 可能是按决策步长记录的不是按整轮记录需要先对每一轮求均值再画整条训练趋势。如果想看平滑曲线可以加一个滑动平均窗口窗口长度一般取训练总轮数的 5% 左右。如果发现 reward 在涨但 cost 也在涨九成是奖励权重失衡回头调第 2.2 节里那四个系数。4. 避坑指南磁渠宋家庄限速坡道场景上的五个翻车现场与修复4.1 训练曲线漂亮但列车提前趴窝现象reward 曲线一路向上看着像收敛了实际把策略拿出来跑一遍列车在半路一个位置连续减速到 0再也起不来训练日志里没有到达终点的记录。原因位置网格分得太粗制动动作一旦触发就跨越了一个坡道区段下一位置格还没到终点速度先归零。环境里把速度为零且不在终点的情况当成正常终止导致算法学会了一种「提前停车」的偷懒策略用负奖励换终止逃避后续的能耗惩罚。解决把位置网格加密到 50 米或更小并在状态转移里增加判断速度为零但未到终点的状态直接标记为无效末端奖励设为比超速惩罚更大的负值。改完以后你会发现策略自然会把制动点往后推迟因为提前停车的代价变大了。4.2 能耗不降反升准时性完美但成本爆炸现象timeError.csv 越来越接近零列车几乎贴着最小运行时间曲线在跑但 costData.csv 里的能耗同步上涨训练完的能耗比不优化还高。原因奖励函数里时间惩罚权重压过了能耗项。算法发现只要准时单步能耗惩罚微不足道于是在所有爬坡区段全力牵引完全放弃惰行和巡航。解决把时间惩罚系数降一个量级或者改成区间判断运行时间在最小时间基准的 30 秒容差内时时间惩罚为零超出容差才开始惩罚。这样算法不会为了把 3 分钟的误差压到 1 分钟内去无限加速。这个 30 秒的容差不是拍脑袋城市地铁运营图通常就按秒级的冗余在排跑表误差在这个范围内系务调度完全可接受。4.3 探索率衰减太快Q 表大片格子是空的现象前 50 轮性能快速提升之后完全停滞Q 表在终态附近的格子大多没有被访问过手动查看状态覆盖率为零的状态数量占比很大。原因ε-greedy 的衰减率设得过高前期随机探索还没覆盖完整状态空间就已经进入高比例贪婪模式。对限速坡道这种状态转移有向性的环境前半段一旦错过某些低速状态后半段永远不会再有机会访问。解决把 ε 的衰减率从每轮 0.99 改到 0.995并设置下限 0.05到达下限后不再衰减保持最低探索率持续采集数据。简单算一下300 轮训练在 0.99 衰减率下第 200 轮 ε 已经小于 0.15换成 0.995第 200 轮还有 0.37后半段仍在继续探索。代价是收敛稍慢但 Q 表覆盖面明显更完整。4.4 换线路数据后限速完全不生效现象把 speedLimit_songjiazhuang.csv 替换进去后训练出的策略在部分区间明显超速但超速惩罚从未触发日志里 overspeed 始终为 False。原因替换进来的 CSV 文件位置列不是从小到大排列或者限速单位混用了 km/h 和 m/s。限速查询函数做线性插值时如果传入的位置参数落到了排序混乱的区间查出来的限速值就会跳到另一个站段的数值。解决读入数据后强制做一次 sort_values(position) 排序并把限速统一成 km/h 再进环境。再手动验证限速文件的首末位置是否覆盖整条线路如果只覆盖了部分区段超速罚在未覆盖区间自然失效。这个检查放在 tool.py 的读取函数里做成一个数据校验模块以后换新数据就不再踩第二次。4.5 minTimeCurve 与限速文件位置错位时间误差指标失真现象timeError.csv 始终保持一个常数无论训练怎么进行都不变。原因最小运行时间曲线和限速数据可能来自两套采集系统一个按里程桩号记录一个按设计里程记录起点基准不一致导致整条曲线整体平移了一段距离。时间误差算出来变成了固定偏差无法反映策略的真实表现。解决在 trainRunningModel 或 tool 层把两条曲线插值到统一的里程网格上再做差。代码里专门放一个 tool.py 而不是让各模型文件直接 read_csv就是为了把这类对齐逻辑收口到一个函数里。从那以后我每次换线路数据都会先画一张叠了限速、坡度和 minTimeCurve 的图肉眼看一遍三个数据源的位置基准是否一致再启动训练。5. 从 Q-learning 到 DQN表格法走到尽头时的 Dueling 结构怎么改5.1 什么时候该从表格法换到深度 Q-learning第 2 章算过2 公里区间、五挡动作时 Q 表只有几千个格子没问题。但当你把场景换成市域快线或者多区间连续驾驶位置段数上百、速度挡数三四十状态数会冲到几十万甚至上百万。表格法的问题不只是内存更重要的是稀疏性——训练数据根本覆盖不了这么多格子Q 表里大部分格子永远是初始值。这时就该换 DQN。代码包里的 RL_brain_Dueling.py 和 dqn_env.py 就是干这个的。DQN 用神经网络作为 Q 值的函数逼近器输入是连续的观测值输出是每个动作的 Q 值天然解决了离散化粒度带来的组合爆炸问题。原来的位置-速度离散网格变成神经网络的输入特征状态空间从「查表」变成「插值」邻近状态之间还能共享学习到的特征收敛所需的数据量反而小于表格法在高维稀疏场景下的需求。5.2 DQN 最小改动经验回放与目标网络和 Q-learning 相比DQN 至少要补两个机制。经验回放是把每一步的 (state, action, reward, next_state) 存进缓冲区训练时随机抽一批样本更新网络打破相邻采样之间的相关性。目标网络是给 Q 值的更新提供一个延迟更新的对照否则网络参数一边做预测一边被更新回归目标不停移动训练很容易震荡。如果从 RL_brain.py 改造训练循环大概会长成这样from dqn_env import SubwayEnvDQN from RL_brain_Dueling import DuelingDQN import numpy as np env SubwayEnvDQN(ciqu) agent DuelingDQN(n_actionsenv.n_actions, n_features2, learning_rate0.001, memory_size10000, batch_size32, replace_target_iter100) for episode in range(500): obs env.reset() total_reward 0 while True: action agent.choose_action(obs) obs_, reward, done, info env.step(action) agent.store_transition(obs, action, reward, obs_) if agent.memory_counter agent.batch_size: agent.learn() obs obs_ total_reward reward if done: break逻辑说明store_transition 写入经验池learn 内部每隔 replace_target_iter 步把评估网络参数同步给目标网络。注意神经网络的输入是连续的位置和速度特征不再需要字符串化的离散网格状态。参数说明learning_rate0.001 是神经网络训练的常见起点比表格法的 0.1 低两个数量级memory_size 设 10000太少会导致经验池样本多样性不足太多会拖慢随机采样速度replace_target_iter 设为 100 表示每 100 步把评估网络复制成目标网络。如果你发现训练曲线剧烈震荡优先检查目标网络更新间隔是否太短。5.3 Dueling 结构在列车控制上的实际收益Dueling DQN 和普通 DQN 的差别在输出层。普通 DQN 直接输出每个动作的 Q 值Dueling 结构则把输出拆成状态价值 V(s) 和优势函数 A(s,a)最后组合成 Q(s,a)。代码包里对应 RL_brain_Dueling.py。这个拆分在列车节能场景里有明确含义状态价值 V(s) 代表「列车在这个位置以这个速度行驶未来能为全局省多少电」优势函数 A(s,a) 代表「在这个状态下选牵引、惰行还是制动比平均动作好多少」。这样做的好处是当多个动作的 Q 值差异很小时网络不需要精确拟合每一个 Q 值只要把状态价值学准就能做出正确决策。列车运行大多数时候处于惰行或巡航的平稳状态动作之间差异并不大Dueling 结构正好擅长在这种场景下降低方差、加速收敛。5.4 用自带的 CartPole 和 MountainCar 脚本验证代码正确性包里的 run_CartPole.py 和 run_MountainCar.py 不是拿来充数的它们是很好的自检工具。当你改了 DQN 网络结构或调了超参数先在 CartPole 上跑一遍。CartPole 是强化学习里验证算法正确性的标准试验台如果连 CartPole 都不能稳定收敛问题大概率不在列车环境而在 DQN 本身。反过来如果 CartPole 正常但列车环境不收敛就把注意力放到奖励函数和环境状态转移上而不是继续调网络结构。这个排查逻辑能省下大量毫无头绪的调参时间。我之前遇到过一个人Dueling 网络在 CartPole 上收敛正常但列车环境怎么也跑不通后来发现是 dqn_env.py 里把终止条件写反了列车提前停车也被算成成功到达奖励直接混淆。这种层面的 bug靠看训练曲线是看不出来的必须跳到环境代码里做单步调试。6. 把新线路数据喂进去数据格式对齐与复现验证流程拿到这套代码后如果想迁移到自己的线路按下面四步走就行。第一步准备新线路的限速 CSV位置列按米从小到大排列限速统一选 km/h 或 m/s两者不能混用。第二步修改 TrainAndRoadCharacter.py 里的列车整备质量、持续速度和线路长度这些参数直接影响训练出来的加速度如果沿用原代码的车辆参数跑出来的策略在实际列车上根本没参考价值。第三步把 minTimeCurve 更新为基线时间曲线不要拿限速曲线硬凑否则准时性惩罚完全失真。第四步跑一个短回合数的训练做冒烟验证比如 30 轮确认 reward 曲线至少出现上升趋势、timeError 不是固定常数再放长训练。验证完成的标志只有一个训练后的策略能耗相对惰行基准有明显的下降同时运行时间落在运营图的允许误差范围内。如果发现能耗下降但到达时间差异过大回头调整奖励权重而不是急着加网络层数。代码包里自带的 costData.csv、timeError.csv、reward.csv 三件套就是为这个复现验证流程准备的。我从这个包里踩过最多的坑就是数据对齐。从那以后我每次换线路数据都强制先跑一遍插值对齐脚本也就是第 4 章提到的那个做法把限速、坡道和最小时间曲线画在同一张图上确认位置基准然后才允许自己动 run_this.py。这套流程看起来多花十分钟实际省掉的却是后面一整天的无效训练。希望这份笔记能让你少走同样的弯路。本文还有配套的精品资源点击获取
返回列表