资讯详情 基于Q-Learning的迷宫序贯决策实战:从零实现强化学习入门
📅 2026/10/20 19:13:51
1. 从迷宫到序贯决策为什么用Q-Learning入门强化学习很多人第一次接触强化学习脑子里冒出来的画面是机器人走路、机械臂抓取、自动驾驶变道感觉离自己很远。但如果你动手写过一遍迷宫寻路就会发现强化学习最核心的那套逻辑——智能体在环境里不断试错根据奖励调整策略最终学会一条从起点到终点的最优路径——其实用一个几十行的Python脚本就能跑通。这也是我为什么一直推荐新手从迷宫场景切入状态空间小、奖励结构清晰、可视化直观跑完一遍能立刻看到智能体从“瞎撞”变成“直奔终点”的全过程成就感比调参调半天看不到变化强太多。这篇内容要聊的就是基于Q-Learning算法实现迷宫序贯决策的完整实例。所谓序贯决策说白了就是“每一步的选择会影响后面的局面”你不能只看眼前这一步走得爽不爽还得考虑这一步之后会不会把自己逼进死胡同。迷宫天然就是序贯决策的典型场景在某个格子里往哪个方向走决定了你下一步站在哪里、还有哪些路可选。Q-Learning要做的就是给每一个“状态-动作”组合打一个分这个分叫Q值代表“在这个位置做这个动作长期来看能拿多少总奖励”。智能体反复跑、反复更新这张Q表最后只要在每个位置挑Q值最大的动作就能走出一条最优路径。适合谁来读如果你已经写过Python基础语法懂一点numpy对“智能体”“奖励”“状态”这些词有模糊印象但没真正落地过那这篇就是给你准备的。我会把环境搭建、Q表更新、探索与利用的平衡、参数怎么调、跑不出来怎么排查全部拆开讲清楚。代码可以直接抄但我更希望你看完能明白每一行为什么这么写换一个迷宫地图也能自己改。2. 迷宫环境设计与Q-Learning核心思路拆解2.1 为什么选表格型Q-Learning而不是神经网络强化学习的算法家族很庞大光是一阶的就有Q-Learning、SARSA、DQN、Policy Gradient等等。很多人一上来就想用DQN觉得神经网络高级。但迷宫这种场景状态是离散的格子坐标动作是上下左右四个方向状态-动作对的数量是有限的——一个10x10的迷宫最多也就100个状态乘以4个动作400个Q值用一张表格就能存下。这种时候上神经网络等于用大炮打蚊子不仅训练慢还容易因为过拟合或者超参数没调好而学不出来。表格型Q-Learning的优势在于每一轮更新都能直接看到Q值的变化调试的时候你可以直接把Q表打印出来看哪个状态的Q值不对劲定位问题非常快。而神经网络是个黑盒你只能看loss曲线排查起来费劲得多。所以我的建议是状态离散且数量可控的场景优先用表格型Q-Learning打基础等你能把表格版跑通了再迁移到DQN去处理连续状态理解会深刻得多。2.2 迷宫的状态、动作与奖励怎么定义先明确三个核心概念在迷宫里的对应关系。状态就是智能体当前所在的格子可以用一个二维坐标(row, col)表示也可以拉平成一个整数索引。动作有四个上、下、左、右分别对应行号减一、行号加一、列号减一、列号加一。奖励的设计直接决定智能体学出来的行为这里有几个常见坑后面会细说先给一个基础版本每走一步给一个小的负奖励比如-0.01目的是让智能体尽量走短路径别绕远路。走到终点给一个大的正奖励比如1.0这是智能体的终极目标。撞墙或者走出边界给一个负奖励比如-0.1同时让智能体留在原地相当于“这一步白走了还扣分”。这里有个细节为什么每步要给负奖励而不是0如果每步奖励是0只有终点是1那么智能体在学会走最短路径之前可能会在迷宫里无限绕圈因为绕圈也不扣分它没有动力去缩短路径。给一个小的负奖励相当于给时间加了一个成本智能体就会倾向于用更少的步数到达终点。这个值不能太大否则智能体会变得过于激进甚至宁愿撞墙也不愿意多走一步反而学不出正确路径。2.3 Q表的结构与更新公式的直观理解Q表本质上是一个二维数组行是状态编号列是动作编号Q[state][action]就是“在state这个位置做action这个动作的长期价值”。更新公式是Q-Learning的灵魂Q(s, a) Q(s, a) α * [r γ * max(Q(s, a)) - Q(s, a)]拆开看α是学习率控制新信息覆盖旧信息的比例r是这一步拿到的即时奖励γ是折扣因子控制未来奖励的权重max(Q(s, a))是到了下一个状态s之后所有可能动作里最大的Q值代表“从s出发能拿到的最好结果”。整个中括号里的东西叫时序差分误差意思是“我原本以为Q(s,a)值这么多但实际走了一步发现能拿到r加上未来的最好值两者差了多少就按这个差值的α倍去修正”。用生活化的类比你每天上班走同一条路心里对“这条路要花多久”有个预期。某天你走了一次发现实际用时比预期短你就会把预期往下调一点如果发现堵车比预期久就把预期往上调。α就是你调整预期的幅度γ是你有多看重“这条路后面会不会更堵”。Q-Learning就是让智能体在迷宫里反复走不断修正每个位置每个方向的“预期价值”直到这张表收敛。3. 完整代码实现与逐段拆解3.1 迷宫地图的表示与可视化准备我用一个二维列表表示迷宫0代表可走的空地1代表墙2代表终点。起点固定在左上角(0,0)。这种表示方式直观改地图只需要改列表里的数字。import numpy as np import random # 迷宫地图0可走1墙2终点 maze [ [0, 1, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 1, 0], [1, 1, 0, 0, 0], [0, 0, 0, 1, 2] ] ROWS len(maze) COLS len(maze[0]) START (0, 0) ACTIONS [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上下左右这里ACTIONS用行、列的增量表示上就是行减一下就是行加一左是列减一右是列加一。用增量而不是写死方向是为了后面判断边界和撞墙时逻辑统一不用写四个if分支。提示迷宫地图的尺寸不一定要正方形但建议行列数不要太小5x5适合快速验证10x10以上更能看出算法的收敛过程。改地图时注意起点和终点都要是0或2别被墙围死了。3.2 Q表的初始化与动作选择策略Q表用numpy的二维数组行数是状态总数列数是动作数。状态编号用row * COLS col计算这样每个格子对应唯一一个整数。N_STATES ROWS * COLS N_ACTIONS len(ACTIONS) q_table np.zeros((N_STATES, N_ACTIONS)) def state_to_index(state): return state[0] * COLS state[1] def choose_action(state, epsilon): if random.random() epsilon: return random.randint(0, N_ACTIONS - 1) else: s state_to_index(state) return int(np.argmax(q_table[s]))动作选择用的是ε-贪婪策略。ε是一个0到1之间的数每次选动作时以ε的概率随机选一个动作以1-ε的概率选当前Q值最大的动作。随机选是为了探索避免智能体过早锁定一条看起来不错但实际不是最优的路选最大Q值是为了利用已有经验。这两者的平衡是强化学习里最核心的权衡之一。ε不能一直固定。常见做法是从大到小衰减一开始ε1.0完全随机探索随着训练轮数增加逐渐降到0.1甚至0.01让智能体越来越依赖学到的Q表。我一般用线性衰减或者指数衰减下面训练循环里会具体写。3.3 单步交互与Q值更新的实现这一步是整个算法的核心。智能体在当前状态选一个动作计算新位置判断是否撞墙或到终点拿到奖励然后更新Q表。def step(state, action): dr, dc ACTIONS[action] new_row state[0] dr new_col state[1] dc # 边界判断 if new_row 0 or new_row ROWS or new_col 0 or new_col COLS: return state, -0.1, False # 撞墙判断 if maze[new_row][new_col] 1: return state, -0.1, False # 到达终点 if maze[new_row][new_col] 2: return (new_row, new_col), 1.0, True # 正常走一步 return (new_row, new_col), -0.01, False注意撞墙和出边界时返回的是原状态也就是智能体留在原地。这样做的好处是Q表更新时s和s是同一个状态智能体会学到“在这个位置往这个方向走没有好结果”。奖励给-0.1而不是-1.0是因为迷宫里有大量靠近墙的位置如果撞墙惩罚太重Q值会被这些负值主导反而影响正常路径的学习。Q值更新那行代码def update_q(state, action, reward, next_state, alpha, gamma): s state_to_index(state) s_next state_to_index(next_state) best_next np.max(q_table[s_next]) q_table[s][action] alpha * (reward gamma * best_next - q_table[s][action])这里有个容易忽略的点如果next_state是终点best_next应该按0算还是按终点状态的Q值算严格来说终点是终止状态没有后续动作所以它的未来价值应该是0。但因为我们把终点也编了号它的Q值初始是0而且训练过程中终点状态不会被选为“当前状态”去更新因为到了终点就结束这一轮所以它的Q值始终保持0用np.max取出来也是0不影响结果。但如果你在代码里让终点也能继续走那就要显式处理终止状态把best_next设为0。3.4 训练主循环与ε衰减策略把上面的零件拼起来就是完整的训练循环。每一轮从起点出发最多走MAX_STEPS步要么到终点要么步数用完。alpha 0.1 gamma 0.9 epsilon 1.0 epsilon_min 0.05 epsilon_decay 0.995 EPISODES 2000 MAX_STEPS 200 for episode in range(EPISODES): state START total_reward 0 for step_count in range(MAX_STEPS): action choose_action(state, epsilon) next_state, reward, done step(state, action) update_q(state, action, reward, next_state, alpha, gamma) state next_state total_reward reward if done: break epsilon max(epsilon_min, epsilon * epsilon_decay) if episode % 200 0: print(fEpisode {episode}, Total Reward: {total_reward:.2f}, Epsilon: {epsilon:.3f})ε衰减用的是指数衰减每轮乘以0.9952000轮之后从1.0降到大约0.05左右。这个衰减速度是我试过比较稳的太快降到很低智能体还没探索够就停止探索容易卡在局部最优太慢则训练轮数要拉很长。你可以根据迷宫复杂度调整迷宫越大、岔路越多衰减应该越慢。注意total_reward在训练初期通常是很大的负数因为智能体到处撞墙、绕路。随着训练进行它会逐渐变大最后稳定在一个接近最优路径总奖励的值。如果你看到total_reward一直上不去先别急着改算法往下看排查部分。4. 参数调优与训练过程观察4.1 学习率α和折扣因子γ怎么选α控制每次更新时新信息覆盖旧信息的比例。α0.1是比较通用的起点意思是每次只把Q值往目标方向修正10%。α太大比如0.9Q值会剧烈波动智能体的行为变得不稳定今天学到的路径明天可能就忘了α太小比如0.01学习速度极慢可能要跑上万轮才能收敛。我的经验是迷宫状态少100以内用0.1到0.3状态多1000以上用0.01到0.05因为状态多了之后每个状态被访问的次数相对减少需要更保守的更新来保证稳定性。γ控制未来奖励的权重。γ0.9意味着智能体相当看重未来愿意为了后面的大奖励牺牲眼前的利益。如果γ设成0.5智能体就会变得短视可能宁愿在终点附近绕圈拿每步的小奖励也不愿意走一条稍长但能到终点的路。迷宫场景我一般用0.9到0.99因为路径长度有限不需要太高的折扣。但γ不能等于1否则在无限步数的场景下Q值可能不收敛。4.2 训练轮数和步数上限的设置EPISODES设多少合适这取决于迷宫大小和ε衰减速度。5x5的迷宫2000轮足够收敛10x10的迷宫可能需要5000到10000轮。判断收敛的标准不是看轮数而是看最近若干轮的平均总奖励是否稳定。我通常每100轮记录一次平均奖励如果连续500轮的平均奖励波动小于5%就认为收敛了。MAX_STEPS是每轮的最大步数防止智能体在迷宫里无限绕圈导致训练卡死。这个值应该设为最优路径长度的3到5倍。比如5x5迷宫最优路径大概8到10步MAX_STEPS设50就够了。设太大浪费计算时间设太小可能导致智能体还没走到终点就被强制结束学不到完整的路径。4.3 用可视化观察智能体的学习过程光看数字不够直观我习惯每训练500轮就把当前Q表对应的最优路径画出来看看智能体学到什么程度了。提取最优路径的方法很简单从起点开始每一步选Q值最大的动作走到终点或者走够MAX_STEPS为止。def get_optimal_path(): state START path [state] for _ in range(MAX_STEPS): s state_to_index(state) action int(np.argmax(q_table[s])) next_state, _, done step(state, action) if next_state state: # 撞墙或出界说明策略有问题 break state next_state path.append(state) if done: break return path把path打印出来或者用matplotlib画成图能直接看到路径是不是越来越短、越来越合理。训练初期路径可能乱七八糟中期开始有方向感后期基本就是一条干净的最短路径。这个过程比看奖励曲线更有成就感也更容易发现“智能体卡在某个角落反复横跳”这类问题。5. 常见问题排查与避坑经验5.1 智能体一直在原地打转或撞墙这是新手最常遇到的现象。训练了几百轮智能体还是在一个小区域里来回走或者不停撞同一面墙。原因通常有三个一是ε太大智能体还在大量随机探索没来得及利用学到的Q值二是撞墙惩罚太重导致智能体对墙产生了“恐惧”宁愿在原地不动也不愿意靠近墙三是学习率太高Q值震荡导致策略不稳定。排查顺序先把ε手动设成0看智能体按当前Q表走会走成什么样。如果ε0时路径合理说明只是探索还没结束继续训练等ε降下来就行。如果ε0时还是乱走检查撞墙惩罚是不是设成了-1.0这种大负数改成-0.1试试。再不行就把α降到0.05让Q值更新更平滑。5.2 总奖励不升反降或剧烈波动训练过程中总奖励偶尔下降是正常的因为ε在衰减过程中智能体的行为模式在变化。但如果连续几百轮总奖励都在下降或者波动幅度超过50%那就有问题了。常见原因是γ设得太高加上奖励设计不合理。比如γ0.99每步奖励-0.01终点奖励1智能体可能会发现“在终点附近绕圈”能拿到接近1的折扣未来奖励而走到终点反而结束了一轮于是它选择不走到终点。解决办法是给终点奖励设得足够大或者给每步的负奖励稍微加大一点让绕圈的成本高于收益。另一个原因是Q表初始化值太大。如果Q表初始全是0而奖励有正有负初期Q值会快速变化看起来波动大。等训练久了自然会稳。如果用的是随机初始化建议改成全零初始化减少初期的不确定性。5.3 终点被墙围死或起点在墙里这是地图设计的问题不是算法的问题。改地图之后一定要先检查起点和终点是不是可走的格子以及从起点到终点是否存在至少一条通路。我写过一个快速检查函数用广度优先搜索判断连通性训练前跑一遍避免白跑几千轮才发现地图根本走不通。from collections import deque def is_reachable(): visited set() queue deque([START]) visited.add(START) while queue: r, c queue.popleft() if maze[r][c] 2: return True for dr, dc in ACTIONS: nr, nc r dr, c dc if 0 nr ROWS and 0 nc COLS: if maze[nr][nc] ! 1 and (nr, nc) not in visited: visited.add((nr, nc)) queue.append((nr, nc)) return False这个函数在训练前调用一次返回False就直接报错别浪费时间训练。5.4 常见问题速查表现象可能原因排查方法解决方式智能体原地打转ε太大或撞墙惩罚过重设ε0看路径继续训练或降低撞墙惩罚总奖励持续下降γ太高导致绕圈有利检查终点奖励和每步奖励比例增大终点奖励或加大每步负奖励Q值震荡剧烈学习率α太大打印Q表看变化幅度α降到0.05或更低训练几千轮不收敛地图不连通或状态空间太大跑连通性检查改地图或增加训练轮数最优路径绕远路每步负奖励太小计算绕路和直走的奖励差加大每步负奖励绝对值到达终点后不停止终止状态处理有误检查done标志确保终点返回doneTrue6. 从迷宫扩展到更真实的序贯决策场景迷宫跑通之后这套Q-Learning的骨架可以直接迁移到很多其他序贯决策问题上。比如库存管理状态是当前库存量动作是进货多少奖励是销售收入减去库存成本目标是在不缺货的前提下把库存压到最低。又比如简单游戏AI状态是游戏画面离散化后的格子动作是上下左右加攻击奖励是得分变化。再比如任务调度状态是当前各任务的完成进度动作是选择下一个执行哪个任务奖励是任务完成的收益减去等待成本。迁移的时候核心改动只有三处状态的定义方式、动作空间的设计、奖励函数的形状。Q-Learning的更新公式和训练循环完全不用动。这也是为什么我建议新手一定要亲手写一遍迷宫版本——它把强化学习的核心逻辑剥离得最干净没有神经网络、没有连续状态、没有复杂的环境模拟让你能专注于“状态-动作-奖励-更新”这个循环本身。把这个循环刻在脑子里后面学DQN、PPO这些算法时你看到的只是“怎么用神经网络替代Q表”“怎么处理连续动作”底层逻辑还是同一套。我在实际带新人的过程中发现凡是迷宫版本自己从头写出来、调通、并且能解释清楚每个参数作用的人后面学深度强化学习时上手速度明显快很多。反过来跳过这一步直接看DQN代码的人往往卡在“为什么要有目标网络”“为什么经验回放能起作用”这些问题上因为他们没有经历过Q表更新的直观过程很难理解那些技巧要解决的是什么问题。最后分享一个我常用的练习方法把迷宫地图改成随机生成每次训练前重新生成一张连通的地图然后观察Q-Learning需要多少轮才能适应新地图。这个练习能帮你理解状态空间变化对收敛速度的影响也能让你对ε衰减、学习率这些参数在不同难度下的表现有更具体的感受。等你觉得随机地图也能稳定收敛了就可以尝试把Q表换成一个小型神经网络用同样的训练循环跑一遍对比两者的收敛曲线和最终路径你会对表格型和函数逼近型的区别有非常直观的认识。