
简介一份面向强化学习入门者的Python源码包基于Deep Q-learning实现最短路径规划适合学习DQN算法、路径搜索或完成课程设计的开发者。包内共有8个文件包括6个Python脚本、1个Markdown说明文档和1个requirements依赖列表压缩包仅7KB轻量且易于运行。代码同时提供Q-learning与Deep Q-learning两种解法内置工具函数、可视化模块与统一运行入口可自由调整网格规模、起点终点及奖励参数直观对比经典表格方法与深度网络的收敛表现。目前已有383人学习下载适合作为从理论到实战的过渡参考。通过阅读与运行源码可深入理解状态编码、奖励塑造、ε-greedy探索、经验回放和目标网络等强化学习关键环节并结合可视化结果观察智能体逐步找到最短路径的过程。README文档和依赖清单可帮助快速搭建运行环境节省配置时间。1. 最短路径问题为什么值得用强化学习再做一遍如果你写过一次 Dijkstra大概会觉得最短路径已经被前人解完了没必要再用神经网络折腾一遍。但这个项目偏偏反着来它把“找最短路径”重新定义成一个强化学习任务——智能体不知道地图长什么样靠一次次试错、撞墙、绕路最终学会从起点走到终点的最短路线。Deep Q-learning 在其中替代了传统的 Q 表用神经网络去逼近状态-动作的价值这正好是强化学习里最经典的实战入门场景。项目代码量不大主文件三个外加一个可视化模块对刚接触强化学习、想做点能跑能看的实验的人来说非常合适。你既能从 QLearning 版本理解表格型强化学习的完整流程也能在 DeepQlearning 版本里看到神经网络如何接管决策。整份源码就是一份最小可复现的“深度强化学习最短路径”研究型代码适合用来做算法对比、调参练习或者作为把强化学习落地到路径规划问题的起点。2. 项目骨架梳理从目录结构认识每个模块的边界拿到压缩包之后建议先别急着跑Run.py先把文件之间的调用关系确认一遍。这个项目结构不复杂但每个模块的分工是清楚的入口、算法、工具函数、可视化各自独立改造时不用牵一发动全身。2.1 目录结构与职责分配解开压缩包后看到的核心文件是这样的RL-ShortestPath-master/ ├── README.md ├── requirements.txt ├── Run.py ├── ShortestPathQLearning.py ├── ShortestPathDeepQlearning.py ├── Visualizations.py └── utils/ ├── __init__.py └── Utilities.py从文件命名就能看出模块边界Run.py是程序入口负责编排整个训练流程ShortestPathQLearning.py实现经典的表格型 Q-learningShortestPathDeepQlearning.py是深度版本用神经网络近似 Q 值utils/Utilities.py负责构造网格环境、定义动作、计算奖励这类公共逻辑Visualizations.py把训练过程和路径画出来。README 和 requirements.txt 分别承担说明和依赖管理。这种拆分方式很轻量比把几百行代码全塞进一个文件里好维护得多。如果要做二次开发核心工作基本集中在Utilities.py和两个算法文件里入口和可视化基本不用动。2.2 Run.py全局训练流程的编排入口Run.py是整个项目最先被执行的脚本。打开之后可以看到典型的“参数定义 → 环境初始化 → 训练 → 可视化”流程。粗略读下来的逻辑是这样from utils.Utilities import GridWorld from ShortestPathDeepQlearning import DeepQAgent from Visualizations import plot_training_rewards, plot_path def main(): # 地图与起点终点参数 grid GridWorld(width8, height8, obstacles[...]) agent DeepQAgent( state_size2, action_size4, gamma0.95, epsilon1.0, epsilon_decay0.995, learning_rate0.001 ) rewards_history [] for episode in range(500): state grid.reset() done False total_reward 0 while not done: action agent.choose_action(state) next_state, reward, done grid.step(action) agent.remember(state, action, reward, next_state, done) agent.replay(batch_size32) state next_state total_reward reward rewards_history.append(total_reward) plot_training_rewards(rewards_history) if __name__ __main__: main()这个入口层面对用户只暴露两个关键对象GridWorld负责环境DeepQAgent负责决策。训练主循环只做三件事环境推进、经验收集、经验回放。参数集中在 main 开头想换地图尺寸、调整邻居数量、改障碍位置都只需要在这里动几行。这里有个细节值得注意state_size2说明状态不是网格坐标的一维展开而是直接传入了(x, y)二维坐标。对小型网格这种表达够用也符合直觉。后面如果要扩展到更大的地图可以考虑加入目标点相对坐标、周围障碍信息等特征。2.3 utils/Utilities.py网格环境如何被抽象成强化学习接口Utilities.py承担的是环境角色。强化学习环境的标准动作是reset和step这个文件围绕这两个方法设计了网格世界的交互规则。class GridWorld: def __init__(self, width8, height8, start(0,0), goal(7,7), obstaclesNone): self.width width self.height height self.start start self.goal goal self.obstacles obstacles or [] self.agent_pos start def reset(self): self.agent_pos list(self.start) return self.agent_pos def step(self, action): # 0: up, 1: down, 2: left, 3: right moves [(-1,0), (1,0), (0,-1), (0,1)] dx, dy moves[action] new_x self.agent_pos[0] dx new_y self.agent_pos[1] dy # 边界检测 if new_x 0 or new_x self.width or new_y 0 or new_y self.height: return self.agent_pos, -1, False # 障碍检测 if (new_x, new_y) in self.obstacles: return self.agent_pos, -1, False self.agent_pos [new_x, new_y] # 到达终点 if tuple(self.agent_pos) self.goal: return self.agent_pos, 10, True # 普通移动 return self.agent_pos, 0, Falsestep的返回值是一个四元组新状态、即时奖励、是否结束。动作空间是离散的四方向移动撞墙和撞障碍都返回 -1 作为惩罚到达终点返回 10 并终止回合普通移动返回 0。这个奖励设计非常直觉也让智能体必须学会“绕开障碍、直奔终点”。需要留意的设计点是碰撞时self.agent_pos没有更新但返回值里带的是当前原地位置也就是说智能体会明确收到“这个方向走不出去”的负反馈。这个设计比“原地不动但给正惩罚”更清晰对于 Q 值学习也更友好。3. 核心算法拆解从 Q 表到深度网络的每一段关键逻辑这一章是全文的核心。只有理解了 Q-learning 到 Deep Q-learning 的演进逻辑才能知道改哪里、怎么调。项目把两个算法放在独立文件里天然形成了一组对照实验。3.1 QLearning 基线表格型强化学习如何更新价值ShortestPathQLearning.py里维护着一张 Q 表表的每一行是状态即网格坐标每一列是四个动作。每次转移后用贝尔曼方程更新 Q 值def update_q_table(self, state, action, reward, next_state, done): best_next_q np.max(self.q_table[next_state[0], next_state[1], :]) td_target reward self.gamma * best_next_q * (1 - done) td_error td_target - self.q_table[state[0], state[1], action] self.q_table[state[0], state[1], action] self.learning_rate * td_error代码逻辑是典型的时间差分TD更新先用下一状态的最大 Q 值计算目标值再用目标值减去当前 Q 值得到 TD 误差最后乘学习率做增量更新。done时best_next_q被置零避免把终止状态的价值继续往后传播。表格型 Q-learning 在这个小网格里跑起来效果很好收敛也快。但它的问题直接暴露了两个一是状态一旦变多比如网格 100×100Q 表规模就爆炸二是表格无法泛化没走过的状态永远没有值。这就是为什么需要深度网络版本。3.2 Deep Q-learning 的模型结构神经网络如何替代 Q 表ShortestPathDeepQlearning.py的核心区别在于不再存表格而是训练一个神经网络输入状态、输出每个动作的 Q 值估计。模型结构并不复杂def build_model(self): model Sequential() model.add(Dense(24, input_dim2, activationrelu)) model.add(Dense(24, activationrelu)) model.add(Dense(4, activationlinear)) model.compile(optimizerAdam(lrself.learning_rate), lossmse) return model状态是两个浮点数x 坐标和 y 坐标输入层维度 2中间两层各 24 个神经元输出层维度 4对应四个动作的 Q 值。损失函数选均方误差优化器用 Adam这是深度强化学习里最常见的一套配置。为什么中间层用 24 而不是更大的数字对小网格环境24 个神经元的表达能力已经足够拟合 Q 函数的分布调大后反而容易过拟合到训练轨迹上。如果地图改到 20×20 以上我会先把神经元加到 64 再观察收敛速度。需要强调的是这里用linear激活而非relu因为 Q 值需要输出负数和正数最后一层做线性映射是标准做法。3.3 经验回放让训练数据不再是马尔可夫链深度 Q-learning 里有个经典问题直接从环境中逐条拿数据训练样本之间高度相关网络容易在局部震荡。项目里的解决办法是维护一个经验池随机采样打破相关性def remember(self, state, action, reward, next_state, done): self.memory.append((state, action, reward, next_state, done)) if len(self.memory) self.memory_size: self.memory.pop(0) def replay(self, batch_size32): if len(self.memory) batch_size: return batch random.sample(self.memory, batch_size) for state, action, reward, next_state, done in batch: target reward if not done: target self.gamma * np.max(self.model.predict(next_state.reshape(1,2))[0]) target_f self.model.predict(state.reshape(1,2)) target_f[0][action] target self.model.fit(state.reshape(1,2), target_f, epochs1, verbose0)这里执行了predict和fit交替的操作先预测当前状态下的四动作 Q 值只把被选动作的 Q 值替换成 TD 目标其余保持不变然后做一次梯度下降。replay方法里的逻辑看起来是在循环里逐个样本训练实际操作时也可以改成批量计算项目里为了简单直观采用了逐样本更新。经验池上限和批次大小直接决定训练效果。我一般会把memory_size设为 2000、batch_size设为 32如果地图复杂化了批次可以提到 64。pop(0)清掉最老经验的方式是环形队列的朴素实现数据量大之后效率稍差但小项目里完全够用。这里有个陷阱要提醒如果replay里不判断len(self.memory) batch_size前几十回合直接崩掉。这也是我最初看到代码时要确认的第一个点。项目里做了这个保护但自己复写时很容易漏掉。3.4 探索策略epsilon 贪婪如何平衡“探路”和“走熟路”强化学习必须解决探索与利用的矛盾。项目采用 epsilon-greedy 策略大部分时候选择当前 Q 值最大的动作但有 epsilon 概率随机走一步。epsilon 随训练过程逐步衰减def choose_action(self, state): if np.random.rand() self.epsilon: return np.random.randint(self.action_size) state state.reshape(1, 2) q_values self.model.predict(state)[0] return np.argmax(q_values)epsilon_decay0.995的含义是每个回合结束后乘以 0.995500 回合后 epsilon 大约是1.0 * 0.995^500 ≈ 0.081已经很低说明智能体主要靠学到的策略走路径。衰减速率如果设置太慢比如 0.999后期仍然在随机探路路径会不停抖动太快比如 0.9则训练不够网络学不到完整地图。跑这个项目时如果想加速看结果可以把epsilon_decay调成 0.995 左右训练 300 回合就能看到智能体稳定走通。如果调成 0.99300 回合后 epsilon 只有 0.049明显更“专一”。3.5 Visualizations.py训练曲线与路径复现Visualizations.py负责把训练效果转成直观图像。项目里我拆出两个主要函数一个画每回合累计奖励的曲线另一个把最终学习到的路径标注在网格上。def plot_path(grid, path, titleLearned Path): fig, ax plt.subplots() grid_matrix np.zeros((grid.height, grid.width)) for (x, y) in grid.obstacles: grid_matrix[x, y] -1 ax.imshow(grid_matrix, cmapgray, originupper) path_x [p[0] for p in path] path_y [p[1] for p in path] ax.plot(path_y, path_x, markero, colorred, linewidth2) ax.scatter([grid.goal[1]], [grid.goal[0]], cgreen, s200) ax.set_title(title) plt.show()坐标轴的处理稍微有点绕imshow默认把数组第一维当 y 轴所以画路径时要用path_y作为 x、path_x作为 y。如果直接按直觉传坐标画出来的路径会是镜像的。这个坐标换位问题也在我的踩坑记录里占了一条。4. 环境准备与运行从 requirements 到第一张训练曲线想把项目跑起来并不难真正容易翻车的是依赖版本和 Python 环境。这个项目没有特别新的依赖但有个别版本组合会踩坑。4.1 环境安装与依赖检查先看requirements.txt里面列的通常是这几项numpy、matplotlib、tensorflow 或 keras、scikit-learn用于数据预处理。我的建议是新建一个干净的 virtualenv 再装python -m venv rl_env source rl_env/bin/activate pip install -r requirements.txt如果之前机器上装了最新版 TensorFlow2.10跑往期代码时偶尔会遇到 Keras API 兼容警告但这个小项目用到的Sequential、Dense、Adam都是稳定 API基本不会出问题。真正要留意的是 Python 版本。TensorFlow 对 Python 3.11 的支持本身就有限制建议直接用 3.9 或 3.10。4.2 标准运行流程环境配好后直接执行python Run.py正常情况下脚本会边训练边输出每个回合的奖励值训练结束后弹出两张图一张是奖励随回合变化的曲线另一张是智能体最终走出的路径。第一张图值得仔细看如果曲线前期在负分附近徘徊、后期跳到正分说明智能体在学习如果全程基本没有波动那大概率是探索率或者奖励设计的问题。启动后可以盯一下前 50 回合的输出。初期智能体基本是乱撞的累计奖励会持续为负到了某个回合数累计奖励突然变正并稳定说明它找到了从起点到终点的可行路径。这个“顿悟”时刻的早晚直接反映参数设置是否合理。4.3 参数调整的优先级如果发现训练效果不理想我按影响程度排序会先调这几个参数参数当前值调整方向影响learning_rate0.0010.0001~0.01过大不收敛过小收敛慢gamma0.950.9~0.99越小越只顾眼前越大越看全局epsilon_decay0.9950.99~0.999决定探索和利用的切换速度batch_size3216~64影响梯度估计的稳定性调参原则是每次只动一个变量。如果你同时调了 gamma 和 epsilon_decay事后很难定位是哪个参数导致的失败。我会把每次实验的参数记录在文件名里比如run_gamma099_decay0995.png后面回溯时省力很多。5. 避坑与排查训练不收敛、路径异常时的小白自救指南这个项目我自己跑第一遍时就出了一堆问题下面这几条是最常遇到的。每条都是“现象、原因、解决”三段式照着排查能少走不少弯路。5.1 训练进度条跑完但智能体永远到不了终点现象是最直接的训练了 500 回合可视化路径还是一团乱麻奖励曲线一直在负分区域徘徊终点从未被激活。原因是奖励稀疏加上惩罚过重。撞墙 -1、普通移动 0、到达终点 10这种设置下智能体随机走了几百步累计惩罚就非常低。更关键的是到达终点的概率极低在探索率较高时这个 10 的奖励根本无法被捕获。解决方法是引入“靠近目标的正向塑形奖励”把上一步距离终点的曼哈顿距离和当前步的距离做差距离缩短就给一个小正奖励拉远就给负奖励distance abs(x - goal_x) abs(y - goal_y) reward 0.1 * (prev_distance - distance) if reward 0: reward -0.1这样智能体能逐步建立“越走越近”的梯度信号训练效率提升一个量级。5.2 障碍物周围路径出现抖动现象智能体虽然能走到终点但会贴着障碍物绕圈或者明明有一条直路不走偏要绕远。原因是障碍物边界处 Q 值不平稳。撞到障碍返回 -1 的惩罚和正常路径的 0 奖励之间缺少过渡网络在障碍边界附近会出现价值震荡。加上坐标型状态没有“前方是否有障碍”的信息智能体只能靠试错记住边界。我尝试过在状态里增加“前方三格是否有障碍”的标志位状态从二维变成五维抖动明显缓解。这个改动在Utilities.py里加一个返回障碍邻接信息的函数即可。5.3 路径可视化出来是镜像的现象明明智能体从左上角走到了右下角画出来却是反向的或者路径里的坐标和网格对不上。原因是imshow的坐标轴方向和网格数组的行列方向不一致。网格第 0 行在图像顶部但很多人在画图时把 x 坐标直接透传给plot导致上下翻转。解决方法是统一坐标约定。我当时最简单的修法是在plot_path里做一次坐标转置或者直接在GridWorld内部把坐标和矩阵索引的映射关系注释清楚状态图上始终用plt.gca().invert_yaxis()保证数据显示和视觉一致。5.4 epsilon 衰减到 0 后仍然随机乱走现象训练到后期epsilon 已经降到 0.01但每回合路径仍然不同不收敛到一个稳定解。原因是 epsilon 只控制随机动作概率但choose_action里用的是单次最大 Q 值。如果两个动作的 Q 值非常接近即使没有随机探索argmax也可能在不同回合做出不同选择。说白了是 Q 函数在两条路径上的价值差异太小网络没有完全分辨出“这条路更短”。解决方法是拉大奖励差异把最短路径奖励设为 10非最短路径设为 5或者提高 gamma让折扣累积奖励更能反映路径长短差异。5.5predict和fit的维度不匹配现象训练跑到几十个回合后报维度错误ValueError提示输入形状不对。原因是state.reshape(1, 2)传入的是一维坐标但如果某处state被当成四维状态传入input_dim就匹配不上。这类问题多发生在改了状态表示但忘了同步修改build_model的input_dim的时候。解决方法是给状态表示定义一个统一函数比如state_to_feature(x, y)整个项目的所有模块只调这个函数。这样改状态设计时只需要动这一个函数。6. 二次开发扩展把玩具网格改造成实用路径规划器跑通只是第一步。这个项目的价值在于环境和算法分离二次开发的空间很大改起来也不算难。6.1 替换环境从网格到图结构网格世界适合教学但实际路径规划场景更多是图结构。你可以把GridWorld换成图搜索环境节点是坐标点边是连通路径class GraphWorld: def __init__(self, nodes, edges, start, goal): self.graph {node: [] for node in nodes} for u, v in edges: self.graph[u].append(v) self.graph[v].append(u) def step(self, action): next_nodes self.graph[tuple(self.agent_pos)] next_state next_nodes[action] ...状态从二维坐标变成节点编号动作从四方向变成邻居选择。DeepQAgent里的state_size也要相应调整其他部分基本复用。这就是模块化设计带来的红利。6.2 自定义奖励函数让智能体学会“最短”项目中到达终点给固定 10 的奖励设计不够精细因为无论走 15 步还是 25 步拿到的奖励一样。想让它学出最短路径可以把奖励改为与步数相关if reached_goal: reward 20 - step_count * 0.1 elif hit_obstacle: reward -1 else: reward -0.02每一步扣 0.02到终点时步数越少奖励越高。这样即使两条路径都能到达终点Q 值也会偏向更短那条。注意每步的小惩罚要设置得足够小否则智能体为了少走步数会想办法反复尝试高风险动作。6.3 训练过程监控保存模型与回放轨迹训练过程的监控能显著提升调参效率。我通常会在训练循环中加一个模型保存点每 50 回合记录一次权重文件方便对比不同阶段的决策变化if episode % 50 0: agent.model.save(fmodel_ep{episode}.h5)再配合Visualizations.py可以额外绘制一张“每 50 回合的路径对比图”直接观察探索导致的路径变化率。从实践角度看这个项目值得注意的一点是如果你的地图比 8×8 更大上面提到的 24 神经元隐藏层可能不够用建议先扩到 64 再训练并在训练初期观察前 100 回合的奖励曲线是否出现上升趋势。最近我再跑这个项目时习惯在choose_action里把 Q 值最大的动作和随机动作各自标记不同颜色画在同一张图上。这样能直观看到探索和利用的切换时机比只看奖励曲线有用得多。如果你打算在这个项目上做实验强烈建议把这个“探索轨迹 vs 利用轨迹”的对比可视化加进去那会让你对强化学习收敛过程的理解深一个台阶。希望这些拆解和改造思路能帮到你。本文还有配套的精品资源点击获取