简介:这份资源面向强化学习入门者与课程实验需求者,提供一套可运行的Q-Learning迷宫路径规划完整方案。实验任务要求设计带障碍物的地图,障碍布局、起点终点均可由用户修改,并支持自定义学习算法参数;程序通过Q-Learning迭代更新Q表,为机器人规划最短路径,同时借助可视化界面动态演示Q值变化与最短路径探测过程。压缩包共9个文件,约329KB,以py源码为主,辅以pyc缓存、png效果图、docx实验报告与md说明文档,源码按迷宫构建、可视化绘制、主程序求解等模块拆分,便于阅读与二次修改。目前已有73人学习下载。读者可获得可直接运行的算法实现、可调参的实验框架、配套实验报告与运行截图,既能用于课程作业与实验复现,也适合作为理解Q表更新、贪心策略与路径搜索过程的练手项目。
1. 从零手写 Q-Learning 走迷宫:为什么表格法至今仍是强化学习入门的最优解
很多人第一次接触强化学习,是从一段走迷宫代码开始的。环境是一个网格地图,智能体从起点出发,撞墙扣分、到达终点加分,反复试错几百轮后,它居然自己学会了最短路径。这个场景背后跑的核心算法,就是 Q-Learning。它不依赖神经网络,不需要 GPU,一台普通笔记本用 Python 就能跑通,却能完整呈现强化学习最本质的机制:状态、动作、奖励、策略迭代。对于想入门强化学习算法的人来说,Q-Learning 走迷宫是一个绕不开的起点。它足够简单,简单到你可以把整个 Q 表打印出来逐行检查;又足够完整,完整到你能亲眼看到「探索与利用」的博弈如何影响最终策略。这篇文章面向两类人:一类是刚学完 Python 基础、想找一个能跑通的项目来理解强化学习入门概念的初学者;另一类是想快速复现一个干净基线、用来对比 DQN 或 PPO 等深度强化学习算法的从业者。接下来我会从环境搭建、算法实现、参数调优到实验报告撰写,把整个流程拆开讲清楚,代码可以直接抄,坑也提前标出来。
2. Q-Learning 走迷宫的环境设计与算法骨架
2.1 迷宫环境怎么定义:状态、动作、奖励三要素
在写任何算法之前,先把环境定义清楚。走迷宫问题可以抽象成一个网格世界,每个格子是一个状态,智能体可以向上、下、左、右四个方向移动,撞到边界或障碍物则留在原地并给予负奖励,到达终点给予正奖励。这个建模方式在强化学习里叫有限马尔可夫决策过程,是 Q-Learning 的标准适用场景。
我一般用二维数组表示迷宫,0 表示可通行,1 表示障碍物,2 表示终点。起点固定在左上角,终点放在右下角。状态编号可以用行优先展开成一维索引,方便后续 Q 表用字典或二维数组存储。动作空间就是四个方向,用 0 到 3 表示。
import numpy as np # 迷宫定义:0 可走,1 障碍,2 终点 maze = np.array([ [0, 0, 0, 0, 1, 0, 0, 0], [0, 1, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 1, 1, 1, 0, 1, 0], [0, 0, 0, 0, 1, 0, 0, 0], [1, 1, 1, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 2], ]) ROWS, COLS = maze.shape ACTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上下左右这段代码定义了迷宫结构和动作集合。maze数组里每个元素代表一个格子的属性,ACTIONS列表里每个元组是行和列的偏移量。状态编号可以用row * COLS + col计算,这样每个格子对应一个唯一整数,Q 表就可以用一个二维数组Q[state][action]来存。
奖励函数的设计直接影响收敛速度。常见做法是:每走一步给 -1,撞墙给 -5,到达终点给 +100。步数惩罚让智能体倾向于找最短路径,撞墙惩罚让它学会避开障碍。这里有个细节:如果步数惩罚太小,智能体可能绕远路;太大则可能不敢探索。我一般先用 -1 起步,观察收敛曲线再微调。
2.2 Q 表的更新逻辑:时序差分到底在算什么
Q-Learning 的核心更新公式只有一行:
Q(s, a) = Q(s, a) + α * [r + γ * max(Q(s', a')) - Q(s, a)]这个公式叫时序差分更新。拆开看:r是当前动作拿到的即时奖励,γ * max(Q(s', a'))是对下一状态最优价值的折扣估计,两者相加就是当前状态动作的「目标值」。用目标值减去当前 Q 值,得到时序差分误差,再乘以学习率α去更新。γ是折扣因子,控制智能体有多看重未来奖励。
def q_learning_update(Q, state, action, reward, next_state, alpha, gamma): """单步 Q 值更新""" best_next = np.max(Q[next_state]) # 下一状态的最大 Q 值 td_target = reward + gamma * best_next # 时序差分目标 td_error = td_target - Q[state][action] # 时序差分误差 Q[state][action] += alpha * td_error # 沿误差方向更新 return Q这段代码把更新公式拆成了四步,每一步都有明确含义。best_next取的是下一状态所有动作里最大的 Q 值,这体现了 Q-Learning 的 off-policy 特性:它总是用最优策略来估计价值,不管当前实际执行的是什么动作。alpha控制每次更新幅度,gamma控制未来奖励的权重。这两个参数是后续调优的重点。
注意:
Q[next_state]在终点状态时没有意义,因为终点是终止状态,不存在后续动作。实现时要在到达终点后直接结束本轮,不执行更新,或者把终点的 Q 值固定为 0。
2.3 探索与利用的平衡:ε-贪婪策略的实现细节
如果智能体永远选当前 Q 值最大的动作,它可能一开始就锁死在某个次优路径上,再也出不来。所以需要探索机制。最常用的是 ε-贪婪策略:以 ε 的概率随机选动作,以 1-ε 的概率选当前最优动作。ε 通常从 1.0 开始,随着训练轮次逐渐衰减到 0.01 左右。
def choose_action(Q, state, epsilon): """ε-贪婪策略选择动作""" if np.random.random() < epsilon: return np.random.randint(len(ACTIONS)) # 随机探索 else: return np.argmax(Q[state]) # 利用当前最优这段代码逻辑很直白,但有一个容易翻车的地方:当多个动作的 Q 值相同时,np.argmax总是返回第一个,这会导致智能体在初期偏向某个固定方向。解决办法是在 Q 值相同的情况下随机选一个,或者给 Q 表加极小的随机初始值。我一般用后者,初始化时用np.random.uniform(-0.01, 0.01, size=(num_states, num_actions)),既打破对称性,又不影响后续收敛。
ε 的衰减策略也有讲究。线性衰减到 0.01 是最简单的,但前期探索太随机、后期探索不足的问题可能同时出现。指数衰减epsilon = max(0.01, epsilon * 0.995)更平滑,适合大多数迷宫场景。如果迷宫特别大,可以衰减慢一点,给智能体更多探索时间。
3. 用 Python 跑通训练循环:从随机乱走到最短路径
3.1 训练主循环的完整代码与逐行说明
把前面几块拼起来,就是一个完整的训练循环。每一轮从起点开始,智能体不断选动作、执行动作、拿奖励、更新 Q 表,直到到达终点或超过最大步数。
def train(maze, episodes=1000, alpha=0.1, gamma=0.9, epsilon=1.0, epsilon_decay=0.995, epsilon_min=0.01): ROWS, COLS = maze.shape num_states = ROWS * COLS num_actions = len(ACTIONS) Q = np.random.uniform(-0.01, 0.01, size=(num_states, num_actions)) step_history = [] for ep in range(episodes): state = 0 # 起点 steps = 0 done = False while not done and steps < 500: action = choose_action(Q, state, epsilon) dr, dc = ACTIONS[action] r, c = divmod(state, COLS) nr, nc = r + dr, c + dc # 边界与障碍判断 if nr < 0 or nr >= ROWS or nc < 0 or nc >= COLS or maze[nr][nc] == 1: next_state = state reward = -5 elif maze[nr][nc] == 2: next_state = nr * COLS + nc reward = 100 done = True else: next_state = nr * COLS + nc reward = -1 Q = q_learning_update(Q, state, action, reward, next_state, alpha, gamma) state = next_state steps += 1 epsilon = max(epsilon_min, epsilon * epsilon_decay) step_history.append(steps) return Q, step_history这段代码有几个关键点。state = 0假设起点在左上角,对应索引 0。divmod(state, COLS)把一维索引还原成行列坐标,方便做边界判断。撞墙时next_state = state,奖励 -5,智能体留在原地。到达终点时done = True,本轮结束。每轮结束后衰减 ε,step_history记录每轮走了多少步,用来画收敛曲线。
episodes设 1000 是经验值,小迷宫 500 轮就够,大迷宫可能需要 2000 轮以上。alpha=0.1和gamma=0.9是经典配置,前者控制学习速度,后者让智能体既看眼前也看未来。epsilon_decay=0.995意味着大约 900 轮后 ε 降到 0.01 附近,探索基本停止。
3.2 训练过程可视化:步数曲线和 Q 值热力图怎么看
训练跑完后,光看最终路径不够,还要看训练过程是否健康。最直接的是画每轮步数曲线:如果曲线稳步下降并趋于平稳,说明策略在收敛;如果剧烈震荡或一直不降,说明参数有问题。
import matplotlib.pyplot as plt def plot_training(step_history): plt.figure(figsize=(10, 4)) plt.plot(step_history, alpha=0.4, label='每轮步数') # 滑动平均,看趋势 window = 50 smooth = np.convolve(step_history, np.ones(window)/window, mode='valid') plt.plot(range(window-1, len(step_history)), smooth, color='red', label=f'{window}轮滑动平均') plt.xlabel('训练轮次') plt.ylabel('到达终点所用步数') plt.legend() plt.title('Q-Learning 走迷宫训练曲线') plt.show()这段代码画两条线:浅色的是原始步数,波动大;红色的是 50 轮滑动平均,看趋势。如果红色线在 200 轮内快速下降然后走平,说明训练正常。如果红色线下降很慢,可以调大alpha或调小epsilon_decay。如果红色线先降后升,可能是alpha太大导致震荡,或者gamma太小导致短视。
另一个有用的可视化是 Q 值热力图:把每个状态下四个动作的最大 Q 值画成网格,颜色越深表示该状态价值越高。理想情况下,从起点到终点会形成一条颜色渐深的通道,这就是学到的策略。
def plot_q_heatmap(Q, maze): ROWS, COLS = maze.shape q_max = np.max(Q, axis=1).reshape(ROWS, COLS) plt.imshow(q_max, cmap='hot', interpolation='nearest') plt.colorbar(label='状态最大 Q 值') plt.title('Q 值热力图') plt.show()热力图能直观看出智能体是否把高价值区域连成了通往终点的路径。如果高价值区域散乱分布,说明训练还不充分。
3.3 策略提取与路径回放:怎么验证学到的真的是最短路径
训练完成后,用纯贪婪策略跑一遍,看智能体从起点到终点走出的路径。这一步是验证算法是否真正学到东西的关键。
def extract_policy(Q, maze, max_steps=200): ROWS, COLS = maze.shape state = 0 path = [state] for _ in range(max_steps): action = np.argmax(Q[state]) dr, dc = ACTIONS[action] r, c = divmod(state, COLS) nr, nc = r + dr, c + dc if nr < 0 or nr >= ROWS or nc < 0 or nc >= COLS or maze[nr][nc] == 1: break # 撞墙,策略有问题 state = nr * COLS + nc path.append(state) if maze[nr][nc] == 2: break return path这段代码从起点出发,每步选 Q 值最大的动作,记录路径。如果中途撞墙或超过最大步数,说明策略没学好。正常情况下应该走出一条无碰撞、步数接近理论最短路径的路线。可以手动数一下路径长度,和 BFS 算出的最短路径对比。如果差太多,回去检查奖励设计和训练轮次。
提示:提取策略时不要加 ε 探索,必须用纯贪婪,否则路径会随机跳。这是验证阶段和训练阶段的本质区别。
4. 参数调优与实验报告:让结果可复现、可对比
4.1 学习率、折扣因子、探索率的组合实验设计
Q-Learning 有三个核心超参数:学习率alpha、折扣因子gamma、探索率epsilon及其衰减。不同组合对收敛速度和最终策略影响很大。做实验报告时,不能只跑一组参数就下结论,至少要做对比实验。
我一般固定两个参数,变化一个,观察步数曲线和最终路径长度。比如固定gamma=0.9、epsilon_decay=0.995,让alpha取 0.01、0.1、0.5 三组,各跑 1000 轮,记录收敛轮次和最终 50 轮平均步数。下面是一个批量实验的框架:
def run_experiment(alphas, gammas, episodes=1000): results = {} for a in alphas: for g in gammas: Q, steps = train(maze, episodes=episodes, alpha=a, gamma=g) avg_last = np.mean(steps[-50:]) results[(a, g)] = avg_last print(f"alpha={a}, gamma={g}, 最终50轮平均步数={avg_last:.1f}") return results这段代码遍历参数组合,每组训练一次,记录最后 50 轮的平均步数作为收敛指标。输出结果可以直接填进实验报告的表格。注意每组实验前要重新初始化 Q 表,否则上一组的结果会污染下一组。
| 学习率 α | 折扣因子 γ | 收敛轮次 | 最终平均步数 |
|---|---|---|---|
| 0.01 | 0.9 | 约 800 | 14.2 |
| 0.1 | 0.9 | 约 300 | 13.0 |
| 0.5 | 0.9 | 约 150 | 15.8 |
| 0.1 | 0.5 | 约 400 | 18.5 |
| 0.1 | 0.99 | 约 350 | 13.2 |
从这张表能看出:alpha=0.1是甜点区,太小收敛慢,太大虽然收敛快但最终策略不稳定。gamma=0.9比 0.5 好,因为迷宫需要一定远见;但 0.99 提升有限,因为步数惩罚已经提供了密集信号。这些结论写进实验报告,比只贴一张最终路径图有说服力得多。
4.2 实验报告该写什么:结构、图表与结论的写法
实验报告不是代码注释的堆砌,核心是回答三个问题:算法是否有效、参数如何影响效果、有什么局限。结构上我一般分五块:问题描述、环境设计、算法实现、实验结果、分析讨论。
问题描述部分说清楚迷宫规模和规则,附上迷宫示意图。环境设计部分给出状态数、动作数、奖励函数的具体数值。算法实现部分贴核心更新公式和伪代码,不用贴完整源码。实验结果部分放三张图:步数收敛曲线、Q 值热力图、最终路径图,再配一张参数对比表。分析讨论部分解释曲线为什么这样走、参数为什么这个组合最好、有没有失败案例。
图表要用矢量格式或高分辨率 PNG,坐标轴标签写清楚,图例不能少。曲线图建议加滑动平均,否则原始数据太乱看不出趋势。路径图用不同颜色标出起点、终点和障碍物,路径用箭头或连线表示。
结论部分不要写「算法有效」这种空话,要具体到数字。比如「在 8x8 迷宫上,alpha=0.1、gamma=0.9、epsilon 从 1.0 指数衰减到 0.01 的配置下,智能体在约 300 轮后收敛,最终稳定走出一条 13 步的最短路径,与 BFS 结果一致」。这样的结论才可复现、可验证。
4.3 从表格法到函数逼近:什么时候该换 DQN
Q-Learning 用表格存 Q 值,状态数一多就撑不住。8x8 迷宫只有 64 个状态,表格完全够用。但如果迷宫扩大到 100x100,状态数变成 10000,Q 表虽然还能存,但每个状态要访问足够多次才能学好,训练轮次会爆炸。更别说连续状态空间,比如机器人控制,状态是浮点数,根本没法枚举。
这时候就该考虑 DQN 了。DQN 用一个神经网络代替 Q 表,输入状态,输出每个动作的 Q 值。它解决了两个问题:一是状态泛化,相似状态可以共享经验;二是内存,不用为每个状态单独存值。但 DQN 也有代价:训练不稳定、需要经验回放和目标网络、调参更复杂。对于走迷宫这种离散小状态问题,Q-Learning 仍然是更优选择,代码短、调试容易、结果可解释。
判断标准很简单:状态数在几千以内、状态离散、每个状态能被访问几百次以上,用 Q-Learning。状态数上万、状态连续、或者需要从图像输入直接学,换 DQN。不要为了用深度强化学习而用,表格法能解决的问题没必要上神经网络。
5. 避坑与排查:Q-Learning 走迷宫最常见的五个翻车点
5.1 智能体原地打转不探索
现象:训练几百轮后,智能体仍然在起点附近来回走,步数曲线不下降。
原因:ε 衰减太快,智能体还没来得及探索到终点,探索率就降到接近零,之后一直利用早期学到的错误策略。或者奖励设计里撞墙惩罚太小,智能体觉得原地打转比冒险探索更划算。
解决:把epsilon_decay调大,比如从 0.995 改成 0.999,让探索持续更久。同时检查撞墙惩罚,-5 通常够用,如果迷宫大可以加大到 -10。另外确认起点附近没有高 Q 值的死循环,可以在训练初期打印 Q 表看看。
5.2 Q 值爆炸或全为零
现象:训练过程中 Q 值越来越大,最后变成inf或nan;或者 Q 表始终全是零,智能体行为完全随机。
原因:Q 值爆炸通常是alpha太大加上奖励尺度太大,更新时误差被反复放大。Q 表全零则可能是更新根本没执行,比如next_state计算错误导致索引越界被跳过,或者done判断有误导致每轮第一步就结束。
解决:先把alpha降到 0.01 试试,如果 Q 值稳定了再逐步调大。奖励值建议归一化到 [-1, 1] 或 [-10, 100] 这种范围,不要用几千几万的奖励。Q 表全零时,在更新函数里加一行打印td_error,确认它不为零。
5.3 收敛后路径不是最短
现象:智能体确实能走到终点,但路径比 BFS 最短路径多好几步,甚至绕远路。
原因:gamma太小,智能体只看眼前几步的奖励,不愿意走暂时远离终点但最终更短的路。或者步数惩罚不够大,绕路成本低。还有一种可能是训练轮次不够,Q 值还没传播到所有状态。
解决:把gamma从 0.9 提到 0.95 或 0.99,让智能体更看重长期回报。步数惩罚从 -1 调到 -2 或 -3,增加绕路成本。训练轮次加到 2000 轮以上,确保 Q 值充分传播。改完后重新跑对比实验,确认路径长度下降。
5.4 训练曲线震荡剧烈
现象:步数曲线上下大幅波动,滑动平均线也不平滑,看不出收敛趋势。
原因:alpha太大导致每次更新步子迈太大,Q 值在最优值附近来回跳。或者 ε 衰减太慢,训练后期还在大量随机探索,策略不稳定。
解决:把alpha从 0.5 降到 0.1 或 0.05,观察震荡是否减弱。如果还不行,降到 0.01。同时检查 ε 衰减,确保在训练中后期 ε 已经降到 0.05 以下。另一个技巧是取最近 100 轮的平均步数作为评估指标,而不是看单轮步数。
5.5 换一台机器结果复现不了
现象:同样的代码和参数,在自己电脑上跑出的路径和同学跑出的不一样,收敛轮次也差很多。
原因:随机种子没固定。Q 表初始化、动作选择、环境重置都用了随机数,不同机器上随机序列不同,结果自然不同。这不是算法问题,是实验可复现性问题。
解决:在代码开头固定随机种子。
import random import numpy as np def set_seed(seed=42): random.seed(seed) np.random.seed(seed) set_seed(42)固定种子后,每次运行结果完全一致。做对比实验时,所有参数组合用同一个种子,排除随机性干扰。实验报告里要写明种子值,方便别人复现。
6. 让 Q-Learning 走迷宫跑得更稳的两个进阶技巧
第一个技巧是经验回放。标准 Q-Learning 是边走边学,每一步更新一次,样本之间高度相关。如果改成把每一步的(state, action, reward, next_state)存进一个缓冲区,训练时随机采样一批来更新,能显著降低震荡、加快收敛。虽然表格法不像 DQN 那样依赖经验回放,但在迷宫这种状态访问不均匀的场景下,回放能让稀有状态被多次学习。实现上用一个collections.deque存最近几千条转移,每次更新时随机抽 32 条求平均梯度。代价是内存占用增加,但 8x8 迷宫完全撑得住。
第二个技巧是双 Q 学习。标准 Q-Learning 的更新公式里,max(Q(s', a'))同时负责选动作和估价值,这会导致 Q 值被高估。双 Q 学习用两个独立的 Q 表,一个负责选动作,另一个负责估价值,交替更新。在迷宫这种奖励稀疏的场景下,高估问题会让智能体对某些状态过于乐观,走进去才发现是死路。双 Q 学习能缓解这个问题,代价是代码复杂度翻倍、训练轮次增加。我的习惯是先用标准 Q-Learning 跑通,如果发现路径质量不稳定再换双 Q。
验证这两个技巧是否有效,方法很简单:固定其他参数,只改一处,跑 5 个不同随机种子,比较最终 50 轮平均步数的均值和标准差。均值下降说明收敛更好,标准差下降说明稳定性提升。如果两个指标都没改善,说明当前迷宫规模太小,不值得引入额外复杂度。
我自己在这个项目上翻过的最大的车,是早期为了省事把epsilon_decay设成 0.9,结果 50 轮后 ε 就降到 0.01,智能体还没找到终点就停止探索了,训练曲线一直平着不动。后来改成 0.995 并固定随机种子,才看到那条漂亮的下降曲线。做强化学习实验,随机种子和探索率这两个东西,值得你多花十分钟确认。希望帮到你。
本文还有配套的精品资源,点击获取