简介这份压缩包面向地铁运行控制研究者与强化学习算法工程师针对列车在限速坡道场景下的节能优化问题提供基于Q-learning的完整实现方案。内容涵盖状态空间与动作空间定义、Q值表更新机制并附带DQN等扩展思路可直接用于牵引制动策略的智能调节与能耗最小化实验验证。资源共54个文件以Python脚本、CSV仿真数据、XML工程配置及说明文档为主另含模型checkpoint与结果图片压缩包整体约789KB结构便于按模块查阅。目前已有80人下载学习。通过源码、运行脚本和附赠文档使用者可快速了解列车动力学建模、限速曲线拟合与奖励函数设计等关键环节也可基于现有框架进行二次开发对比不同场景下的节能效果适合作为课程设计或科研入门参考。1. 地铁列车节能优化不是黑匣子一份能直接跑的 Q-learning 列车运行控制工程包拿到这份资源的第一件事我是去找 run_this.py。强化学习做列车节能优化论文里的 Q-learning 方案很多但能把环境模型、线路数据、算法代码和主入口一起打包的工程不多这份算一个。核心场景很明确地铁列车运行控制与能耗管理列车在限速坡道上按位置、速度、坡度选择牵引、惰行、制动目标是能耗最小且准点到达。和传统动态规划求最优解不同Q-learning 不依赖模型先验靠的就是环境交互试错。拆完目录有几个意外收获subway_env.py 是 Gym 风格环境trainRunningModel.py 带戴维斯阻力和坡道附加阻力RL_brain.py 管 Q 表dqn_env.py 和 dqn_brain.py 是深度 Q 网络版本RL_brain_Dueling.py 是 Dueling DQN 变体。两条线路限速数据 speedLimit_ciqu.csv、speedLimit_songjiazhuang.csv 和训练记录 reward.csv、costData.csv 都在包里连带 tensorflow checkpoint 都给你留好了。适合谁做列车运行控制课题的学生、想把 Q-learning 迁到深度强化学习的工程师、手里有线路数据想做能耗预演的地铁从业者。解压后照 README.md 和说明文件.txt 的顺序走十分钟内能跑起来。2. 先把环境立起来subway_env 与列车运动学建模的两个关键选择强化学习项目里有个铁律环境错了策略学得再好也是白搭。这套工程把环境和算法拆得比较干净算法部分后面再讲先从环境说起。2.1 列车运动学模型trainRunningModel.py 里的阻力和加速度计算trainRunningModel.py 承担的是单步物理仿真核心是带阻力的运动学更新工程里的逻辑和下面这段等价class TrainRunningModel: def __init__(self, mass287000, rotary_mass0.06, resistance(2.0, 0.003, 0.0004)): self.mass mass # 列车总质量 kgA 型车满载约 287t self.rotary_mass rotary_mass # 回转质量系数取值大约 0.05~0.10 self.a, self.b, self.c resistance # 戴维斯阻力系数 self.v 0.0 # 当前速度 m/s self.pos 0.0 # 当前位置 m self.time 0.0 self.energy 0.0 # 累计牵引能耗 kWh def step(self, action, grade, dt0.1): # action: 0惰行, 1牵引, 2制动 if action 1: force self._max_traction(self.v) # 牵引力随速度下降 elif action 2: force -self._max_brake(self.v) # 制动力受黏着限制 else: force 0.0 # 惰行只受阻力 basic_resistance self.a self.b * self.v self.c * self.v ** 2 grade_resistance grade * 9.8 * self.mass / 1000 # grade 为千分数 acc (force - basic_resistance - grade_resistance) / (self.mass * (1 self.rotary_mass)) self.v acc * dt self.v max(0.0, self.v) self.pos self.v * dt self.energy self._traction_power(force, self.v) * dt / 3600000.0 return self.pos, self.v, self.acc三个容易出错的地方。第一个是单位坡度 grade 用千分数除以 1000 再乘 mg得到的是牛顿量级的坡道附加阻力必须和戴维斯阻力算出来的单位一致否则坡道效应会差出数量级。第二个是回转质量系数轮对、电机这些转动部件的等效质量不能忽略漏掉这一项加速度会整体偏大 6%~8%放在节能优化里会直接影响牵引和惰行的切换点。第三个是 dt 的选择0.1 秒对策略学习够了但要跑几十个 episode 的话建议把仿真步长放到 0.5 秒并相应调整能耗换算公式训练速度能快好几倍。注意力的符号一定要统一牵引为正、制动为负、惰行为零。动作号到力值的映射最好在 controlModel.py 里收敛成一张表别在环境里到处散落魔法数字。2.2 限速 CSV 怎么读speedLimit_ciqu 与 speedLimit_songjiazhuang 的解析方式包里的 speedLimit_ciqu.csv 和 speedLimit_songjiazhuang.csv 是两条线路的限速数据字段结构通常是一列里程或位置点加一列限速值。读取时有个实际问题线路数据经常是 GBK 编码pandas 默认 utf-8 会直接解码报错第一行就要注意。import pandas as pd import numpy as np def load_speed_limit(csv_path): df pd.read_csv(csv_path, encodinggbk) df.columns [pos, limit] # 统一列名 df df.sort_values(pos).drop_duplicates(pos) return df def limit_at(df, pos): return float(np.interp(pos, df[pos], df[limit]))这里用 np.interp 做线性插值限速台阶比如 80 直接掉到 60会被平滑成一段斜坡。这对 Q-learning 反而有利状态空间不会在限速突变点出现硬边界策略更容易收敛。但注意插值只解决限速连续化不解决超速判罚每一步结束都要拿当前速度和新位置的限速比一次超了就按超速量给惩罚这个判断必须放在环境层而不是运动学模型里。2.3 observation 怎么组成位置、速度、坡度的离散编码subway_env.py 的 reset 和 step 是 Gym 风格接口返回的 observation 就是算法看到的世界。工程里用的是三元组位置、速度、坡度。位置用来查限速和坡度表速度是控制的核心量坡度决定了惰行能不能溜得动。class SubwayEnv: def __init__(self, speed_limit_csv, min_time_csv, dt0.1): self.dt dt self.speed_limit load_speed_limit(speed_limit_csv) self.min_time pd.read_csv(min_time_csv) # 最短运行时间曲线 self.model TrainRunningModel() self.total_distance self.speed_limit[pos].max() self.scheduled_time self.min_time[time].max() def reset(self): self.model TrainRunningModel() return self._encode_state() def _encode_state(self): pos int(self.model.pos // 25) # 25m 一个位置桶 v int(self.model.v // 1.0) # 1 m/s 一个速度桶 grade int(self._grade_at(self.model.pos) // 1.0) return fp{pos}_v{v}_g{grade}分桶粒度是第一个要调的参数。位置 25 米一档、速度 1 m/s 一档是市区线路的常见折中。分桶越小状态越多Q 表越大、训练越慢分桶太大则限速完全不同的两个位置会被编进同一个状态策略无所适从。拿到新线路先扫一眼限速 CSV 的突变点密度再定 pos 分桶这是我每次必做的第一步。2.4 controlModel.py 与 tool.py 在链路里的角色controlModel.py 封装控制层逻辑负责把算法的动作解释成牵引力或制动力输出给 trainRunningModel动作号到级位的映射、牵引力随速度的限幅曲线都在这一层。tool.py 是工具集限速插值、能耗累计、绘图辅助都归它管。跑 run_this.py 时建议只盯三样东西环境返回的 observation、每一步的 reward、每轮的能耗和到站时间。这三样直接决定训练结果能不能用。3. Q-learning 核心实现状态离散化、动作空间与奖励函数的三个设计点表格型 Q-learning 的全部秘密在三个设计点状态怎么切、动作选几个、奖励怎么算。这三个点直接决定算法能不能收敛、收敛出来的策略是不是真省电。3.1 状态离散化分桶粒度是 Q 表大小的唯一决定因素前面讲的分桶已经给出状态编码这里补一个量化视角。假设线路 3000 米位置 25m 一档是 120 档速度按地铁最高 80 km/h约 22 m/s切 22 档坡度按常见的 -30‰~30‰ 切 60 档状态数约 120 × 22 × 60 ≈ 158400。每个状态配 3 个动作Q 表接近 50 万格。pandas DataFrame 能扛住但收敛速度会明显变慢。工程实践里经常做状态降维坡度不单独进状态而是算进阻力后在运动学里体现状态只保留位置和速度两维或者把位置桶放大到 50 米。def encode_state(pos, v, pos_gap50, v_gap1.0): # 坡度不编码进状态作为环境属性在每一步查询后参与计算 return fp{int(pos // pos_gap)}_v{int(v // v_gap)}代价是策略对坡度的感知变弱上坡段可能牵引不足、速度掉太多。折中办法是把坡度按正负和大小分 3~5 个粗档放进状态而不是细分 60 档。这个取舍要和线路的实际坡道特征对齐坡道平缓的线路完全可以省掉坡度维度。3.2 动作空间牵引、惰行、制动的粒度决定控制上限工程里用的是离散动作集。最简版本是 3 个动作牵引最大牵引力、惰行、制动最大制动力。进阶版本是 5 个动作加入 50% 牵引和 50% 制动两个中间级位。动作集内容节能上限训练难度适用场景3 动作牵引 / 惰行 / 制动中低快速验证链路5 动作全牵引 / 半牵引 / 惰行 / 半制动 / 全制动高中追求精细节能我的建议是先用 3 动作跑通整个链路确认奖励函数和限速逻辑没问题再升级到 5 动作对比能耗。直接上 5 动作最容易出的问题是训练初期奖励噪声变大到时候很难分清是算法问题还是环境问题。3.3 奖励函数能耗、准点、超速三个目标如何加权奖励设计是这条链路里最像玄学的部分。纯省电会让列车一路惰行到站严重晚点纯准点会变成全力跑。常见做法是把能耗、时间、超速三个惩罚项线性加权def calc_reward(self, action, dt, limit): energy_pen self.w_energy * self.model.energy_step # 本步耗能 time_pen self.w_time * dt # 每步时间成本 over max(0.0, self.model.v - limit) over_pen self.w_over * over * dt # 超速惩罚 r -(energy_pen time_pen) - over_pen if self.reach_end: # 到站结算 r self.arrive_bonus r - self.w_late * abs(self.model.time - self.scheduled_time) return r参数含义w_energy 是能耗权重w_time 是时间权重w_over 是超速权重arrive_bonus 是到站奖励w_late 是晚点惩罚。这三个权重的比例决定了策略性格——w_time 偏大列车倾向高速巡航w_energy 偏大列车倾向早惰行、晚牵引。我会先用包里的 minTimeCurve 跑一个最短时间曲线那个时长就是 w_time 的参照系代表「全力跑」的下界再在这个时长上加 10%~15% 的余量作为计划运行时间准点惩罚往这个值上对齐比拍脑袋定权重要可靠得多。3.4 RL_brain.py 的 Q 表更新与探索策略RL_brain.py 是标准 Q-learning 的表格实现核心是 epsilon-greedy 探索和 Q 值迭代class QLearningTable: def __init__(self, actions, learning_rate0.05, reward_decay0.95, e_greedy0.9): self.actions actions self.lr learning_rate # 学习率 self.gamma reward_decay # 折扣因子 self.epsilon e_greedy # 探索率 self.q_table pd.DataFrame(columnsactions, dtypenp.float64) def choose_action(self, obs): self.check_state_exist(obs) if np.random.uniform() self.epsilon: row self.q_table.loc[obs, :] return np.random.choice(row[row row.max()].index) return np.random.choice(self.actions) def learn(self, obs, action, reward, obs_next, done): self.check_state_exist(obs_next) q_predict self.q_table.loc[obs, action] if done: q_target reward # 终止状态直接取真实奖励 else: q_target reward self.gamma * self.q_table.loc[obs_next, :].max() self.q_table.loc[obs, action] self.lr * (q_target - q_predict) def check_state_exist(self, obs): if obs not in self.q_table.index: self.q_table self.q_table.append( pd.Series(0.0, indexself.actions, nameobs))这段有个容易看漏的细节choose_action 里取最大 Q 值动作时用 np.random.choice 而不是直接 idxmax是为了打破平局——早期 Q 表全是 0固定取第一个动作的话很多状态永远探索不到。learn 里 done 时分两种情况处理终止状态不回传下一状态的估值这是 Q-learning 的标准收尾逻辑。learning_rate 建议 0.01~0.1gamma 取 0.9~0.99取值越大越看重远期节能收益。4. 从 Q-learning 到 DQN 与 Dueling DQN三套代码的取舍与运行参数包里带的不止 Q-learningdqn_env.py、dqn_brain.py 和 RL_brain_Dueling.py 都在还有 tensorflow checkpoint 和 run_MountainCar.py、run_CartPole.py 两个经典验证脚本。这一章把什么时候必须换深度强化学习、三套代码怎么配合讲清楚。4.1 为什么状态一多表格就撑不住前面算过考虑坡度时状态数能到十几万Q 表几十万格训练慢还不一定收敛。更麻烦的是泛化表格方法对没见过的状态只能靠随机探索硬学而连续速度下几乎每个 episode 都会踩进新格子。深度 Q 网络DQN解决的就是这个问题输入连续原始量输出每个动作的 Q 值网络天然做泛化。dqn_env.py 的作用和 subway_env.py 一样只是把状态从离散编码改成浮点数组def _get_state(self): return np.array([self.model.pos / self.total_distance, # 归一化位置 self.model.v / self.v_max, # 归一化速度 self._grade_at(self.model.pos)], # 坡度原值 dtypenp.float32)注意前两维做了归一化这是深度网络能不能训出来的关键。位置 0~3000 米和速度 0~22 m/s 不在一个量纲直接喂网络会让梯度更新被大数值维度带偏收敛速度肉眼可见地慢。4.2 DQN 的网络结构和经验回放dqn_brain.py 是 TensorFlow 1.x 风格的网络核心是 eval 网络和 target 网络双结构加上经验回放缓冲class DeepQNetwork: def __init__(self, n_actions, n_features, learning_rate0.01, reward_decay0.9, e_greedy0.9, replace_target_iter300, memory_size5000, batch_size32): self.n_actions n_actions self.n_features n_features self.lr learning_rate self.gamma reward_decay self.epsilon e_greedy self.replace_target_iter replace_target_iter # 每 N 步同步一次 target self.memory_size memory_size self.batch_size batch_size self.memory np.zeros((memory_size, n_features * 2 2)) self.memory_counter 0 self._build_net() def _build_net(self): self.s tf.placeholder(tf.float32, [None, self.n_features], names) self.s_ tf.placeholder(tf.float32, [None, self.n_features], names_) with tf.variable_scope(eval_net): l1 tf.layers.dense(self.s, 32, tf.nn.relu) self.q_eval tf.layers.dense(l1, self.n_actions) with tf.variable_scope(target_net): l1_t tf.layers.dense(self.s_, 32, tf.nn.relu) self.q_next tf.layers.dense(l1_t, self.n_actions)记忆库每一行存 s、a、r、s_、done 五元组训练时随机抽 batch_size 条打破样本间的时间相关性。replace_target_iter 控制 target 网络多久同步一次 eval 网络的参数300 步是常见值。注意这段代码是 TF1 API如果机器上是 TF2import 就会报 placeholder 不存在需要改写成 tf.keras.Input 或开 compat.v1 模式。注意checkpoint 目录下是 file.meta、file.index、file.data-00000-of-00001 这种 TF1 格式加载权重时务必确认训练用的 network 结构定义和 checkpoint 里的图结构一致否则变量取名对不上就直接加载失败。4.3 Dueling DQN 改在哪状态价值和优势函数拆开RL_brain_Dueling.py 是 Dueling DQN 变体改动只在网络尾部把 Q 值拆成状态价值 V(s) 和动作优势 A(s,a)。with tf.variable_scope(dueling_net): l1 tf.layers.dense(self.s, 32, tf.nn.relu) v tf.layers.dense(l1, 1) # 状态价值 a tf.layers.dense(l1, self.n_actions) # 动作优势 self.q_eval v (a - tf.reduce_mean(a, axis1, keep_dimsTrue))这样改的道理在列车场景里特别直观限速坡道上的大部分状态动作优劣并不明显比如匀速巡航段牵引和惰行差别不大Q 值主要靠状态价值撑起来只有到限速突变点和坡道起终点动作优势才开始起作用。Dueling 结构让网络不用花容量去记那些「所有动作差不多」的状态收敛速度比普通 DQN 快尤其是训练后期奖励接近饱和时差距更明显。4.4 用 CartPole 和 MountainCar 先验证算法骨架run_CartPole.py 和 run_MountainCar.py 是包里的两个实验脚本算法骨架和地铁场景完全一致。我拿到新环境的第一天一定先跑这两个CartPole 验证 DQN 的记忆库和 target 网络逻辑MountainCar 验证稀疏奖励下 Q-learning 的收敛能力。两个脚本能跑出稳定曲线再去动 subway_env否则环境问题混进算法问题debug 起来非常痛苦。这两个经典环境跑一轮只要几十秒是性价比最高的冒烟测试。5. 数据文件与常见问题排查从 costData 到奖励震荡的五条踩坑记录这部分是血泪经验。包里留了一大堆 CSVcostData.csv、reward.csv、timeError.csv、EData.csv、rd.csv、sl.csv。这些文件不只是训练产物更是排查问题的线索。5.1 先看懂每个 CSV 是什么文件内容在调试里的用途speedLimit_ciqu.csv / speedLimit_songjiazhuang.csv线路限速表位置 限速值环境约束检查数据质量minTimeCurve_ciqu.csv / minTimeCurve_songjiazhuang.csv最短运行时间曲线准点参照与奖励标定costData.csv每轮能耗成本看节能趋势是否下降reward.csv每步 / 每轮奖励判断收敛和震荡timeError.csv到站时间与计划时间误差判断准点性EData.csv / rd.csv / sl.csv能耗明细 / 线路数据逐段分析速度曲线画图时我会把 reward.csv 平滑后叠加 costData.csv 一起看costData 单调下降但 reward 还在震荡说明奖励里噪声项太多两个都在抖说明探索率太高或学习率太大。5.2 环境版本不匹配pyc 后缀和 checkpoint 格式直接暴露问题现象run_this.py 运行到 import tensorflow 就报错AttributeError 或 ImportError换 Python 3.8 后问题依旧。 原因包里的 pyc 文件后缀是 cpython-36 和 cpython-37checkpoint 目录下是 file.meta、file.index、file.data-00000-of-00001 这种 TF1 专用格式。这套工程写在 TensorFlow 1.x 加 Python 3.6/3.7 时代TF2 默认不兼容 tf.placeholder 和 tf.layers。 解决用 conda 单独建 Python 3.6 或 3.7 环境安装 TensorFlow 1.x 系列版本注意不是 2.x。别硬在 TF2 上迁除非你想顺便把网络定义也重写一遍。5.3 奖励曲线一直震荡几百轮不收敛现象reward.csv 画出来像噪声带costData 也上下乱跳训练完导出的策略明显不合理——限速前还在猛加速。 原因epsilon 固定在 0.9 不变训练后期还在大量随机探索刚学到的策略下一轮就被随机动作覆盖。 解决让探索率随 episode 衰减。我一般从 0.9 线性降到 0.05衰减窗口放在总 episode 数的前 70%if episode 700: agent.epsilon max(0.05, 0.9 - 0.85 * episode / 700)改完再看 reward.csv基线会明显收窄。如果还在震荡下一步查 learning_rate 是不是超过了 0.1。5.4 超速惩罚集中在限速骤降段速度图像是锯齿现象策略在其他路段正常一到限速从 80 掉到 60 的突变位置就反复牵引、制动速度曲线锯齿状能耗反而更高。 原因位置分桶太粗限速突变点两侧 25 米内的位置被编进同一个状态算法在这个状态里无法区分突变前后只能反复试探。 解决把限速突变点附近的位置桶加密突变点前后各 50 米用 5 米一档其他区域保持 25 米或者干脆换 DQN 输入连续位置让网络自己学限速边界。5.5 timeError 越跑越大列车越来越「懒」现象timeError.csv 的绝对值随 episode 递增某轮开始到站时间比计划晚 10 秒以上能耗确实降了但结果没法用。 原因奖励函数里 w_time 权重太小算法发现惰行省电的收益大于准点惩罚策略退化成一路溜车。 解决把准点约束从每步时间惩罚里拆出来到站时按 timeError 施加惩罚并把 w_late 调到 w_energy 的 3 倍以上同时用 minTimeCurve 给全程时间一个硬下界低于最短时间曲线的跑法直接判无效。5.6 换线路直接翻车宋家庄训练结果套不到 ciqu现象speedLimit_songjiazhuang 上训练好的 Q 表直接填到 ciqu 线路上跑要么全程超速要么在坡道段掉速到停。 原因Q 表状态里包含绝对位置分桶两条线路的里程长度、限速分布、坡度完全不同状态编码对不上。 解决两条路——要么对新线路重新训练要么把状态改成归一化相对里程当前里程除以总里程让策略学到的是比例位置而不是绝对位置。相对里程方案对同类型线路迁移有效但坡度差异大时还是要重训。6. 把训练好的策略搬到真实线路Q 表导出、离线验证与超参调试习惯训练完成不是终点能导出策略并在新数据上复现节能效果这份资源才算真正吃透。6.1 Q 表导出一行 argmax 拿到完整控制策略表格 Q-learning 的产物是 Q 表策略就是每个状态下 Q 值最大的动作。导出时注意先处理空行和重复索引import pandas as pd def export_policy(q_table_csv, policy_csv): q pd.read_csv(q_table_csv, index_col0) q q.dropna(howall) # 去掉从未访问的状态 policy q.idxmax(axis1) policy policy.rename(action) policy.to_csv(policy_csv) return policy导出的策略表每一行是「状态 → 动作」几百到几千行不等。拿到策略后先别急着信要做离线重放。6.2 离线重放验证策略的三步流程def replay(policy, env, max_steps10000): obs env.reset() done False step 0 while not done and step max_steps: action policy.get(obs, 1) # 未见过的状态默认牵引 obs, reward, done env.step(action) step 1 return env.model.time, env.model.energy, step结果怎么判能耗对比 minTimeCurve 的能耗能贴近最短时间曲线能耗的 1.1~1.3 倍说明节能空间已经压得很小timeError 落在 ±5 秒内说明准点约束生效全程无超速告警说明限速逻辑没漏。三个条件都满足策略才能拿去报数据。6.3 我现在的固定调试顺序第一跑 run_CartPole.py 确认 DQN 骨架可用第二用 3 动作加简化状态位置 速度跑通 subway_env看 costData 是否下降第三加回坡度和准点惩罚观察 timeError 不劣化第四升级 5 动作或 Dueling DQN 压能耗第五导出策略做离线重放。从那以后我每次拿到新线路 CSV都强制走一遍这个顺序先扫限速突变点再跑最短时间曲线打底确认 epsilon 衰减和 reward 收敛后才敢把能耗数字报出去。这套流程看着笨但确实帮我少翻了好几次车。希望帮到你。本文还有配套的精品资源点击获取