首页 / 资讯中心 / 文章详情

Python从零实现Q-Learning迷宫求解器:强化学习序贯决策入门实战

Python从零实现Q-Learning迷宫求解器:强化学习序贯决策入门实战 ★ FEATURED ARTICLE
1. 从迷宫出发为什么用Q-Learning做序贯决策入门很多人第一次接触强化学习脑子里冒出来的画面就是机器人走迷宫。这个直觉其实非常准——迷宫问题天然具备序贯决策的全部要素状态明确、动作有限、奖励稀疏、需要多步之后才能知道当初的选择对不对。而Q-Learning恰好是理解这一切最合适的切入点它不需要环境模型不需要梯度传播一张表格就能把在什么状态下做什么动作最划算这件事说清楚。这篇内容要聊的就是怎么用Python从零实现一个Q-Learning迷宫求解器。我会把上一篇自主寻优的思路延续下来重点讲清楚从盲目试错到有策略地试错这一步是怎么跨过去的。适合已经写过一些Python、对numpy不陌生、但强化学习还没真正上手的朋友。如果你之前看过一些理论推导但不知道怎么落地这篇就是帮你把公式变成可运行代码的那座桥。核心关键词就几个Q-Learning、序贯决策、迷宫、自主寻优、强化学习实例。整篇内容围绕它们展开不跑偏。先说清楚这个实例最终能做什么给定一个用二维网格表示的迷宫智能体从起点出发通过反复尝试最终学会一条稳定到达终点的路径。整个过程不需要人告诉它怎么走它自己撞墙、自己回头、自己积累经验。代码量不大但麻雀虽小五脏俱全Q表更新、探索与利用平衡、奖励设计、收敛判断这些关键环节一个不少。我见过不少人学强化学习卡在两点上一是数学符号太多看懂了公式但不知道对应代码里哪一行二是跑通了代码但不知道为什么参数要那么设。这篇会重点解决这两个问题每个参数我都会说清楚它的作用以及调整它会带来什么后果。你照着敲一遍再自己改改参数观察变化基本就能把Q-Learning的核心逻辑吃透。2. 迷宫环境设计与Q-Learning核心逻辑拆解2.1 迷宫怎么建网格、状态与动作的映射关系迷宫本质上是一个离散状态空间。我习惯用一个二维数组来表示0代表可走的路1代表墙2代表终点3代表起点。比如一个5x5的迷宫长这样maze [ [3, 0, 1, 0, 0], [0, 0, 1, 0, 1], [0, 0, 0, 0, 1], [0, 1, 1, 0, 0], [0, 0, 0, 0, 2] ]每个格子就是一个状态。状态总数是行数乘以列数这里就是25个。但墙的格子智能体永远走不进去所以实际有效的状态少一些。不过为了编码方便我通常还是按全部格子来建Q表墙对应的那些行永远不会被更新浪费一点内存但逻辑简单很多。动作空间有四个上、下、左、右。可以用0、1、2、3来表示也可以用元组(-1,0)、(1,0)、(0,-1)、(0,1)来表示位移。我倾向于用字典把动作编号和位移对应起来这样后面写代码可读性更好actions { 0: (-1, 0), # 上 1: (1, 0), # 下 2: (0, -1), # 左 3: (0, 1) # 右 }这里有个细节要注意动作编号和位移的对应关系一旦定下来就不要改因为Q表是按动作编号索引的。如果你中途改了映射之前训练的结果就全乱了。我在早期就犯过这个错调了半天以为算法有问题结果是动作映射写反了。状态转移规则也很直接当前位置加上动作位移如果超出边界或者是墙就留在原地不动同时给一个负奖励作为惩罚。如果到达终点给一个大的正奖励并且结束当前回合。其他情况每走一步给一个小的负奖励这个设计是为了让智能体尽快找到终点而不是在原地绕圈。2.2 Q表到底存了什么从状态动作对到价值估计Q表是Q-Learning的核心数据结构。它的维度是[状态数, 动作数]每个元素Q[s][a]表示在状态s下执行动作a所能获得的长期累积奖励的估计值。注意这里说的是长期累积奖励不是即时奖励。这是Q-Learning和贪心策略最本质的区别——它考虑的是长远利益。初始化的时候Q表全部置零。这意味着智能体一开始对所有状态动作对的价值一无所知认为它们都一样好。随着不断尝试Q值会逐渐分化好的动作Q值升高差的动作Q值降低。Q表的更新公式是整个算法的灵魂Q(s,a) Q(s,a) α * [r γ * max(Q(s,a)) - Q(s,a)]这个公式看起来简单但每一项都有明确的含义。α是学习率控制新信息覆盖旧信息的程度。γ是折扣因子衡量未来奖励的重要性。r是即时奖励。max(Q(s,a))是下一个状态中最大的Q值代表智能体对未来的最优估计。我第一次看到这个公式的时候最困惑的是为什么要用max。后来想明白了Q-Learning是异策略off-policy算法它在更新时假设下一步会采取最优动作而不管实际采取的是什么动作。这个特性让它可以大胆探索而不影响学习效果也是它比SARSA更常用的原因之一。2.3 探索与利用的平衡ε-贪心策略的实操细节如果智能体永远选择当前Q值最高的动作它可能会陷入局部最优永远发现不了更好的路径。如果它永远随机选择那学到的Q表就没有意义。所以需要在探索和利用之间找一个平衡。ε-贪心策略是最常用的方法以ε的概率随机选择一个动作以1-ε的概率选择当前Q值最高的动作。ε通常从一个较大的值开始比如1.0然后逐渐衰减到一个较小的值比如0.01。我一般用指数衰减epsilon epsilon * decay_ratedecay_rate通常设在0.99到0.999之间。衰减太快智能体还没来得及探索就停止学习了衰减太慢后期还在大量随机动作Q表难以收敛。这个参数需要根据具体问题调没有万能值。还有一个细节ε不应该衰减到0。保留一点点随机性可以让智能体在环境有微小变化时仍能适应。我通常把下限设在0.01到0.05之间。2.4 奖励函数设计稀疏奖励下的信号引导迷宫问题的奖励非常稀疏——只有到达终点才有正奖励其他时候都是0或者小的负值。这种稀疏奖励会让学习变得很慢因为智能体很难把最终的成功和前面几十步的选择关联起来。我的做法是给每一步一个小的负奖励比如-0.01。这样智能体有动力尽快到达终点而不是无限绕圈。撞墙给一个稍大的负奖励比如-0.1让它学会避开墙。终点给1.0。奖励的绝对值大小会影响学习速度。如果步进惩罚太大智能体可能会变得过于冒险如果太小它可能不在乎绕路。我试过-0.01、-0.05、-0.1几个值-0.01在5x5到10x10的迷宫上表现比较均衡。还有一个技巧是给终点附近的格子一些小的正奖励形成梯度引导。但这会改变问题的最优解需要谨慎使用。在标准迷宫问题里我一般不加这个。3. 完整代码实现与逐行解析3.1 环境类的封装状态重置与步进函数我习惯把迷宫环境封装成一个类这样代码结构清晰也方便后续替换成其他环境。核心方法有两个reset()和step()。reset()把智能体放回起点返回初始状态。step(action)执行动作返回(next_state, reward, done)。done表示是否到达终点或达到最大步数。import numpy as np import random class MazeEnv: def __init__(self, maze): self.maze np.array(maze) self.rows, self.cols self.maze.shape self.start tuple(np.argwhere(self.maze 3)[0]) self.goal tuple(np.argwhere(self.maze 2)[0]) self.state self.start self.max_steps 200 self.steps 0 def reset(self): self.state self.start self.steps 0 return self.state def step(self, action): self.steps 1 dr, dc actions[action] nr, nc self.state[0] dr, self.state[1] dc if nr 0 or nr self.rows or nc 0 or nc self.cols: next_state self.state reward -0.1 elif self.maze[nr][nc] 1: next_state self.state reward -0.1 else: next_state (nr, nc) if next_state self.goal: reward 1.0 else: reward -0.01 self.state next_state done (next_state self.goal) or (self.steps self.max_steps) return next_state, reward, done这里有个容易忽略的点状态用元组表示而不是用单个整数。用元组可以直接作为字典的键也可以直接索引二维数组。如果要用整数表示状态需要做一个线性映射state_id row * cols col。两种方式都可以元组更直观整数更省内存。小迷宫用元组完全没问题。max_steps的设置也很重要。如果没有这个限制智能体可能在两个格子之间无限来回走永远不结束。200步对于5x5迷宫足够了更大的迷宫需要相应增加。3.2 Q-Learning训练主循环从随机试错到策略收敛训练主循环的结构很固定外层循环控制回合数内层循环控制每回合的步数。每走一步更新Q表然后进入下一个状态。def train(env, episodes1000, alpha0.1, gamma0.9, epsilon1.0, epsilon_min0.01, decay0.995): n_states env.rows * env.cols n_actions 4 q_table np.zeros((n_states, n_actions)) for episode in range(episodes): state env.reset() state_id state[0] * env.cols state[1] done False while not done: if random.random() epsilon: action random.randint(0, 3) else: action np.argmax(q_table[state_id]) next_state, reward, done env.step(action) next_state_id next_state[0] * env.cols next_state[1] best_next np.max(q_table[next_state_id]) td_target reward gamma * best_next * (1 - done) td_error td_target - q_table[state_id][action] q_table[state_id][action] alpha * td_error state_id next_state_id epsilon max(epsilon_min, epsilon * decay) return q_table这段代码里有几个关键点值得展开说。第一(1 - done)这个因子。当done为True时说明已经到达终点没有下一个状态了所以未来的价值应该为0。如果不乘这个因子智能体会认为到达终点后还能继续获得价值导致Q值虚高。第二状态ID的转换。我在循环外面把元组转成整数循环内部一直用整数索引避免重复转换。这个细节在状态多的时候能省不少时间。第三epsilon的衰减放在回合结束后而不是每一步之后。按步衰减也可以但按回合衰减更符合直觉——每个回合代表一次完整的尝试。3.3 策略提取与路径可视化验证学习效果训练完成后Q表就是最终的学习成果。提取策略很简单每个状态选择Q值最大的动作。def extract_policy(q_table, env): policy {} for r in range(env.rows): for c in range(env.cols): if env.maze[r][c] 1: continue state_id r * env.cols c best_action np.argmax(q_table[state_id]) policy[(r, c)] best_action return policy def visualize_path(env, policy): state env.start path [state] visited set() visited.add(state) for _ in range(env.max_steps): if state env.goal: break action policy.get(state) if action is None: break dr, dc actions[action] next_state (state[0] dr, state[1] dc) if next_state in visited: print(检测到循环策略未完全收敛) break visited.add(next_state) path.append(next_state) state next_state return path可视化的时候我用简单的字符画把路径上的格子标出来。这样不用装matplotlib也能看到效果。如果要更直观可以用matplotlib画热力图把每个格子的最大Q值画出来颜色越深表示价值越高。我实测下来5x5的迷宫大概200到500个回合就能收敛10x10的需要2000到5000个回合。收敛的判断标准是连续多个回合都能在较少的步数内到达终点。3.4 参数调优实战学习率、折扣因子与探索率的配合参数调优是Q-Learning最需要经验的部分。我把我常用的参数范围和调整逻辑整理成表参数常用范围作用调大后果调小后果α学习率0.01-0.5新信息覆盖旧信息的程度学习快但不稳定学习慢但平滑γ折扣因子0.8-0.99未来奖励的重要性重视长远利益短视ε初始值0.5-1.0初始探索程度探索充分但初期慢初期快但可能错过最优ε衰减率0.99-0.999探索减少的速度探索时间长收敛快但可能不充分步进惩罚-0.01到-0.1鼓励快速到达过于冒险不在乎绕路我一般先用α0.1、γ0.9、ε1.0、decay0.995跑一遍观察收敛曲线。如果收敛太慢把α调到0.2或0.3如果Q值震荡厉害把α降到0.05。γ在迷宫问题里通常设0.9到0.95因为路径长度有限不需要太远的视野。有个经验α和ε要配合调整。如果ε衰减很快α可以适当大一点因为后期主要是利用需要快速稳定。如果ε衰减慢α要小一点避免在探索阶段把Q值搞得太激进。4. 常见问题排查与实战避坑指南4.1 智能体原地打转不前进怎么办这是最常见的问题。智能体在起点附近来回走Q值不分化看起来像卡住了。原因通常有三个一是奖励设计有问题步进惩罚太小智能体不在乎绕路二是ε太大一直在随机探索三是学习率太小Q值更新太慢。排查顺序先看奖励把步进惩罚从-0.01调到-0.05试试再看ε打印每个回合的ε值确认它在正常衰减最后看Q值打印几个状态的Q值如果全是0或者全相等说明学习率太小或者回合数不够。我遇到过一次特殊情况迷宫起点被墙包围只有一个出口但出口方向需要先往上走再绕。智能体在起点附近随机走了很久才找到出口。这种时候可以手动增加起点附近的初始探索或者把ε的初始值设得更高。4.2 Q值不收敛或震荡的排查思路Q值震荡表现为相邻回合的路径长度差异很大或者Q表的值忽大忽小。最可能的原因是学习率太大。α0.5以上在大多数迷宫问题上都会震荡。降到0.1或0.05通常能解决。第二个原因是γ太接近1比如0.99以上。这会让未来奖励的估计方差很大导致Q值不稳定。迷宫问题γ0.9到0.95就够了。第三个原因是环境有随机性。标准迷宫是确定性的同样的状态同样的动作结果一样。如果你加了随机风或者随机初始位置Q值收敛会慢很多需要更多回合。还有一个隐蔽的原因状态表示有歧义。比如用元组表示状态时如果两个不同的格子映射到了同一个状态IDQ表就会混乱。检查状态ID的计算公式确保每个格子对应唯一的ID。4.3 训练速度太慢的优化技巧5x5迷宫跑1000回合大概几秒钟但10x10跑5000回合可能要一两分钟。如果迷宫更大纯Python循环会明显变慢。优化方向有几个。第一用numpy向量化操作替代循环。Q表的更新可以批量进行但迷宫问题里状态转移是串行的向量化空间有限。第二减少不必要的状态ID转换在循环外算好映射表。第三用更小的状态空间墙的格子不建Q表行。我试过用字典代替numpy数组存Q表在状态稀疏的时候字典更快但状态密集时numpy的索引优势明显。迷宫问题状态比较密集numpy更合适。还有一个技巧是提前终止。如果连续N个回合都在最优步数内到达终点就可以停止训练。我一般设N50。这能省不少时间尤其是参数设得比较保守的时候。4.4 迷宫变体从固定地图到随机障碍的扩展标准迷宫是固定地图智能体学到的策略只适用于这一张图。如果想让它适应多张图需要把地图作为输入的一部分或者用函数逼近代替Q表。一个折中方案是随机生成障碍但保持起点和终点固定。这样智能体需要学到更通用的策略。实现上每次reset时重新生成迷宫但Q表保留。这相当于在线学习智能体要同时适应多张地图。我试过这个方案收敛需要的回合数大概是固定地图的3到5倍。但学到的策略更鲁棒换一张新图也能走个大概。如果迷宫尺寸不大这个扩展很有意思能让你更深刻地理解Q-Learning的泛化能力边界。另一个扩展是加入随机风即动作有一定概率偏离预期方向。这会让环境变成非确定性Q-Learning仍然适用但需要更多训练。这个扩展能帮你理解为什么Q-Learning在非确定性环境下仍然有效——因为它学的是期望价值。4.5 常见问题速查表问题现象可能原因排查方法解决方案原地打转步进惩罚太小打印每回合步数增大步进惩罚Q值震荡学习率太大观察Q值变化降低α收敛太慢ε衰减太慢打印ε值加快衰减到达终点后不停止done因子未乘检查td_target计算加(1-done)策略有循环训练不充分可视化路径增加回合数某些状态Q值全为0从未访问过检查访问计数增加探索路径不是最短γ太小检查γ值增大γ这张表是我踩坑踩出来的基本上覆盖了90%的常见问题。遇到问题先查表能省很多时间。5. 从迷宫到真实问题Q-Learning的适用边界与我的实操体会迷宫问题虽然简单但它揭示的序贯决策框架可以迁移到很多场景。只要一个问题满足状态有限、动作有限、奖励可定义、决策有先后这四个条件Q-Learning就能用。比如库存管理、简单游戏AI、资源调度甚至一些推荐系统的冷启动问题。但Q-Learning的局限也很明显。状态一多Q表就爆炸。10x10迷宫100个状态还能接受100x100就是10000个状态1000x1000就是一百万内存和训练时间都受不了。这时候就需要函数逼近用神经网络代替Q表也就是DQN的思路。但那是另一个话题了迷宫问题先把表格方法吃透再往深度强化学习走会顺很多。我在实际项目里用Q-Learning的经验是它最适合状态离散且数量可控、环境确定性较强、奖励可以清晰定义的场景。如果状态是连续的先做离散化但离散化的粒度需要仔细调——太粗会丢失信息太细状态爆炸。我一般先用较粗的离散化跑通流程再逐步细化。还有一个体会是奖励设计比算法选择更重要。同样的Q-Learning奖励设计得好收敛快且策略合理奖励设计得差怎么调参数都不对。迷宫问题里步进惩罚和终点奖励的比例需要反复试我建议从1:100开始调比如-0.01和1.0。最后分享一个小技巧训练过程中定期保存Q表尤其是当回合步数创出新低的时候。这样即使后续训练震荡了你也能回滚到最好的版本。我一般每100回合存一次文件名带上回合数和平均步数方便对比。这个迷宫实例后续还可以这样扩展加入多终点让智能体学习优先级加入动态障碍考察在线适应能力或者把Q表换成神经网络看看深度强化学习的效果差异。每一个扩展都能让你对序贯决策的理解更深一层。
阅读完成 · 觉得有帮助?
咨询建站