资讯动态

Q-learning入门:4x4 FrozenLake实战详解与Python代码

发布时间:2026/10/5 1:30:11 来源:尧图企业网站定制
我带过不少人入门强化学习几乎每个人第一次看到Q-learning公式时的表情都一样——这个像乱码一样的式子到底在干嘛我后来总结出一个结论不要在公式里学Q-learning要在例子里学Q-learning。这篇就是用最小的4x4网格世界把Q-learning每一步都跑给你看同时给出一份可以直接复制运行的Python代码。这个例子叫FrozenLake冰冻湖你站在一块4x4的冰面上要从左上角走到右下角中间有几个冰窟窿掉进去就结束。难点在于冰面很滑你按下“往下”不见得真的往下走可能滑到左边或右边。这就是一个带随机性的决策问题。Q-learning要干的事就是在这种不确定环境下通过反复试错学会一条尽可能安全的路线。适合读这篇文章的人是那种刚听说强化学习、知道有个东西叫Q表、但还没真正跑通过一个例子的人。如果你已经看过不少理论却总觉得隔着一层这篇文章能把那层窗户纸捅破。1. 用一个玩明白的例子搞清楚Q-learning到底在学什么1.1 强化学习里的“五个词”到底是什么意思先别急着看公式。Q-learning再花哨本质上是解决一件事让一个智能体agent在环境environment里通过试错学会“在什么状态下做什么动作”。这句话里有五个关键概念我用冰冻湖的例子一一对应状态state你在哪个格子上。4x4网格所以状态就是0到15这16个数字。动作action你决定往哪走。只有四种左、下、右、上。奖励reward你走一步之后环境给你的反馈。走到终点给1掉进冰窟窿给0普通冰面也是0。环境environment整个4x4冰面包括它的打滑规则。智能体agent就是那个每步都在做决策的“你”。很多人看到“掉洞给0”会疑惑掉洞难道不是应该给个负分吗这里就是强化学习和直觉不一样的地方。掉洞的真正惩罚不是那个0分而是“本局游戏直接结束你失去了继续走的机会”。这和真实世界很像——掉进坑里的代价不是被扣了一点分而是这条路彻底断了只能重来。1.2 为什么选4x4冰冻湖当第一个例子我在给新手推荐入门例子时FrozenLake几乎是固定首选。原因很简单状态少只有16个Q表一眼能看完动作少只有4个不用处理复杂动作空间有随机性打滑不是单纯的最短路径问题能体现强化学习“与不确定环境交互”的核心奖励稀疏大部分步骤都是0能逼你理解“长期收益”这个概念。相比之下一上来就上CartPole或者更复杂的连续控制新手很容易被环境本身搞晕根本看不出算法在干什么。FrozenLake的状态和动作都少到可以手算但麻雀虽小五脏俱全Q-learning该有的东西一个不少。你可以把这个例子理解成一个“被冰冻住的迷宫”地面滑不溜秋你没法精确控制自己的移动方向只能在摸索中学会哪条路更不容易掉坑。Q-learning就是在这个过程中逐渐产生记忆的过程——它的记忆就是我们下一节要说的Q表。2. Q表更新公式真正需要啃的只有这一条2.1 Q(s,a)到底是什么当成你的“路况记忆本”Q-learning的核心是一个表格叫Q表。表格的行是状态列是动作每个格子里的值叫Q值。Q值的意思是如果现在处于状态s执行动作a之后每一步都选择最优策略那么能获得的累计折扣奖励期望值。这句话有点绕我拆开说。假设你在状态2第一行第三个格子准备往下走。这个动作放在“现在看”最多也就是个方向选择但从长远看它决定了你接下来会不会掉坑、能不能到终点。Q值就是把“这一刻的动作”和“未来的所有结果”挂钩的一个数字。可以把它想象成一本路况记忆本。每走一步你就在本子上记一笔这个路口往这个方向走后面大概能拿到多少收益。走得多了本子上的记录越来越准确最后你只需要查表哪个动作的Q值大就走哪条路。所以你不需要再问“到底哪条路正确”表格会告诉你。2.2 更新公式逐项拆解Q-learning只有一个核心公式Q(s,a) ← Q(s,a) α × [r γ × max_{a} Q(s,a) − Q(s,a)]看着吓人实际上就是个“用新信息修正旧判断”的过程。我逐个符号解释Q(s,a)旧记忆也就是你在状态s做动作a之前认为能拿到的收益αalpha学习率决定了新信息对旧记忆的修正幅度0到1之间r走这一步立刻拿到的奖励γgamma折扣因子0到1之间表示未来的收益在“今天”值多少钱max_{a} Q(s,a)到了新状态s之后所有可选动作里那个最大的Q值表示“假设到未来每一步都按最好的走”r γ × max_{a} Q(s,a)这一步之后对未来的新估计叫TD目标时序差分目标方括号里整个差值新估计和旧记忆之间的差距叫TD误差。可以把它类比成记账本上的修正你原本记着这条路的收益大概值5块走了一趟发现新估计是8块那你就往5块的方向调一点。α就是调整幅度设0.1就只往新估计方向挪10%设0.5就挪50%。γ的作用更微妙。它让算法不会只看眼前那一两步而是会把未来收益算进来但又不会把遥远的收益当成和眼前的收益等值。比如从起点走到终点需要走7步如果每走一步打一次95折那么终点那1分的奖励折算到现在只有0.95的7次方大约是0.7。如果走6步就到折算过来大约是0.74。走得更短的路折算后的收益就更高算法自然就会偏好短路径。2.3 举一个具体的更新例子假设现在状态是2你选择了“下”这个动作Q表中的旧值是0。走完这一步你到了状态6因为还没有到终点r0。状态6往下、往左、往右、往上四个动作对应的Q值里最大的那个是0.5。那么TD目标 0 0.95 × 0.5 0.475 TD误差 0.475 − 0 0.475如果α0.1那么新的Q(2,下) 0 0.1 × 0.475 0.0475。看到了吗虽然这一步一分钱奖励都没拿到但Q值还是涨了一点因为它把“未来可能拿到的收益”回传到了当前这一步。强化学习里最核心的一个思想就是这个奖励不只是给最后一步它会沿着走过的路径往回“传播”。2.4 探索与利用为什么不能一直选当下最好的动作如果每次都贪心选Q值最大的动作会出现一个问题可能永远发现不了更好的路。假设一开始所有Q值都是0第一次随机走出了某条路勉强到终点那这条路上Q值就变正了。如果你每次都按Q值选就会一直走这条路但这条路未必是最优的。解决办法是epsilon-greedy策略以epsilon的概率随机选动作其余概率选Q值最大的动作。epsilon就像一个“好奇程度”的开关训练开始时设大一点比如1.0让智能体多探索随着训练推进慢慢降下来比如降到0.05这时候大部分动作都按照学到的经验来但偶尔还能尝试新路线。你可以想象成在一家陌生的食堂吃饭如果天天点最熟悉那道菜就永远不会发现隔壁窗口其实更好吃。epsilon就是在“重复已知的好菜”和“试试没吃过的菜”之间做平衡。训练早期好奇心强多试训练后期有经验了就少试。3. 完整Python实现不用装强化学习库也能跑3.1 为什么我不直接用gym/gymnasium很多入门教程会让你先装gym环境库然后调用现成的FrozenLake环境。这本身没问题但实际带人的过程中我踩了不少坑新版gym接口改了reward范围变了render模式变了一报错新手就懵了。所以我在这篇里选择自己写一个最简FrozenLake环境。一来不依赖第三方库装好numpy和matplotlib就能跑二来环境代码只有几十行逻辑透明你随时能改参数、加打印看清楚每一步发生了什么。这对理解算法本身更有利。3.2 完整代码下面这段代码可以直接复制到一个.py文件里运行。我特意把环境、训练、评估、可视化分开写每个部分你都能独立修改。import numpy as np import matplotlib.pyplot as plt class FrozenLake4x4: 4x4 冰冻湖环境。 状态编号: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 动作: 0左, 1下, 2右, 3上 def __init__(self, slipperyTrue): self.n_states 16 self.n_actions 4 self.start 0 self.goal 15 self.holes [5, 7, 11, 12] self.slippery slippery self.reset() def reset(self): self.state self.start return self.state def _take(self, action): # 模拟冰面打滑: 80%按原方向, 10%向右偏, 10%向左偏 if not self.slippery: return action p np.random.rand() if p 0.8: return action elif p 0.9: return (action 1) % 4 else: return (action 3) % 4 def step(self, action): action self._take(action) row, col divmod(self.state, 4) if action 0: # 左 col max(col - 1, 0) elif action 1: # 下 row min(row 1, 3) elif action 2: # 右 col min(col 1, 3) else: # 上 row max(row - 1, 0) next_state row * 4 col reward 0.0 done False if next_state self.goal: reward 1.0 done True elif next_state in self.holes: done True self.state next_state return next_state, reward, done def q_learning(env, episodes3000, alpha0.1, gamma0.95, epsilon_start1.0, epsilon_end0.05, epsilon_decay0.999): q_table np.zeros((env.n_states, env.n_actions)) epsilon epsilon_start rewards [] successes [] for ep in range(episodes): state env.reset() done False total_reward 0.0 while not done: if np.random.rand() epsilon: action np.random.randint(env.n_actions) else: action int(np.argmax(q_table[state])) next_state, reward, done env.step(action) td_error reward gamma * np.max(q_table[next_state]) - q_table[state, action] q_table[state, action] alpha * td_error state next_state total_reward reward epsilon max(epsilon_end, epsilon * epsilon_decay) rewards.append(total_reward) successes.append(total_reward 0) return q_table, rewards, successes def evaluate(q_table, episodes200): env FrozenLake4x4() win 0 for _ in range(episodes): state env.reset() done False while not done: action int(np.argmax(q_table[state])) state, reward, done env.step(action) if reward 0: win 1 return win / episodes def show_policy(q_table): arrows [←, ↓, →, ↑] for i in range(4): line for j in range(4): s i * 4 j if s 0: line S elif s 15: line G elif s in [5, 7, 11, 12]: line X else: line arrows[int(np.argmax(q_table[s]))] print(line) def smooth(values, window100): return [np.mean(values[max(0, i - window 1): i 1]) for i in range(len(values))] # 训练并输出结果 np.random.seed(42) env FrozenLake4x4() q_table, rewards, successes q_learning(env) print(测试成功率:, evaluate(q_table)) print(学习到的策略:) show_policy(q_table) plt.plot(smooth(successes)) plt.xlabel(episode) plt.ylabel(100局内的平均成功率) plt.title(Q-learning on FrozenLake4x4) plt.show()3.3 代码里的几个关键点环境部分最值得看的是step函数里的打滑逻辑。每次你决定要往某个方向走环境会先以80%概率按你的方向走再以10%概率往右偏、10%概率往左偏。这就是为什么FrozenLake不是简单的最短路问题。网格边缘的越界移动会自动留在原地这也是经典FrozenLake的行为。训练主循环里只有三行是最核心的td_error reward gamma * np.max(q_table[next_state]) - q_table[state, action] q_table[state, action] alpha * td_error state next_state整个Q-learning的智慧就浓缩在这三行里。先算TD误差再用它修正Q表然后把状态往前推进。没有batch没有梯度下降就是一行一行地查表、改表。epsilon的衰减方式用的是指数衰减epsilon max(epsilon_end, epsilon * epsilon_decay)每训练一局epsilon就乘一次0.999。3000局下来epsilon大概是最后约1.0 × 0.999^3000 ≈ 0.05。正好落在我们设的下界。这个衰减节奏对入门例子够用但你也可以自己改。为什么seed固定为42因为这个环境里每一步都有随机性不固定随机种子的话你每次跑出来的Q表和成功率都不一样新手很容易误以为代码有问题。固定seed后结果可以稳定复现方便对照排查。4. 输出怎么解读策略图、Q值表、学习曲线4.1 打印出来的策略图怎么读我拿固定seed跑了一次训练3000局打印出来的策略大概是这个形态S → → ↓ ↓ X ↓ X → → ↓ X X → → GS是起点G是终点X是冰窟窿箭头表示“在这个状态下Q值最大的动作”。沿着这个箭头走你会得到一条路径0 → 1 → 2 → 6 → 10 → 14 → 15。在完全不打滑的理想情况下7步到达终点。这条路径绕开了所有洞是比较合理的路线。但注意因为冰面打滑这个策略并不是100%能成功。比如在状态6往下走有10%概率会滑到左边那个洞状态5另外10%概率会滑到右边那个洞状态7。所以实际执行时这条路线偶尔会掉坑里。这就是这个例子的真实感所在——学习出策略不代表每次都能赢但它已经是“平均意义下”最好的选择了。4.2 训练后的Q表局部长什么样某次训练后我把Q表几个关键状态打印出来大概是这样的不同随机种子会有差异只看相对大小状态左下右上决策0起点0.010.020.350.01右10.020.100.420.02右20.010.480.050.04下60.020.350.030.09下100.010.320.020.07下140.010.030.480.02右状态0的“右”明显高于其他动作所以起点应该先往右走。状态14的“右”是0.48因为右边就是终点这是离成功最近的一步Q值自然高。这里面有个细节值得品味离终点越近Q值的绝对数值整体会更高因为奖励在传播过程中离源头更近折损更少。4.3 学习曲线为什么是毛毛糙糙的代码里画的是“每100局的滑动平均成功率”。你会看到曲线不是平滑上升的而是带一堆毛刺这个完全正常。原因是单次训练前几百局epsilon特别大基本在乱走成功率接近0中期开始Q表慢慢学到一点东西成功率逐步抬升但由于打滑的随机性哪怕策略已经稳定100局仍然有20%到30%的概率掉洞所以曲线会持续抖动后期epsilon降到0.05附近策略趋于稳定曲线整体维持在较高平台。我跑出来的最终测试成功率不添加随机探索直接按Q表选动作一般在70%到80%之间。如果你跑出来60%多或80%多都不用惊讶这就是这个问题的正常波动范围。4.4 调参对比影响到底有多大为了让你对参数有个直观感知我把常见参数组合的现象整理成一张表。以下是我实际跑出来的“体感结论”不是精确实验数据但方向不会错参数设定直观现象原因alpha0.05其余不变3000局后成功率偏低还在慢慢涨每次修正幅度太小稀疏奖励下信息传播太慢alpha0.5其余不变前期Q值震荡很大曲线忽上忽下新样本权重过高容易覆盖旧经验gamma0.7其余不变成功率明显下降折扣因子太小算法变得短视只看眼前几步epsilon固定0.1不衰减前期几乎不学习后期也难发现新路径探索太少稀有的成功路径很难被发现没有固定随机种子每次结果差异很大环境本身高随机不固定seed就无法对照调参这件事我觉得只有亲手跑过一组对比才能真正理解每个参数的意义。纸上谈兵看十遍不如实际改一个数字跑三分钟。5. 新手最容易炸的四个坑以及排查思路5.1 失败率居高不下先查环境定义别急着怀疑算法这是我见过最多的情况。代码跑起来不报错但成功率一直在0附近晃学习者第一反应是“Q-learning是不是没用”。绝大多数时候问题出在环境定义而不是算法。我在自己写这个环境时最容易搞错的就是方向映射。比如动作1到底是“下”还是“上”取决于你的坐标系怎么定义。如果你把方向搞反了算法再怎么学都是在学一个错误的地图。排查方法特别简单写一个for循环打印每个状态下执行每个动作后的next_state肉眼检查一遍转移是否符合预期。拿状态0左上角来说动作2右应该到状态1动作1下应该到状态4动作3上应该由于撞墙留在状态0。如果输出不是这样说明方向定义有误。5.2 稀疏奖励下收敛慢是正常的但得区分“慢”和“错”FrozenLake的奖励特别稀疏——大部分episode的所有reward都是0只有偶然摸到终点才有1分。这意味着Q-learning只能靠极少数“走通”的经验来学习早期可能几百局都学不到任何东西。这不是bug是这类问题本身的特性。我建议把episodes至少开到3000如果3000局后成功率还在50%以下再考虑调参数。也可以用一个小技巧每隔100局打印一次当前的平均奖励观察它有没有缓慢上升的趋势。有上升趋势说明在学只是慢一点波动都没有才说明哪里坏了。5.3 别看到Q值变小就以为代码写错了我在初学的时候有过一次“误判”训练过程中打印Q表发现某个状态的Q值不升反降我一度怀疑代码写错了。后来才发现这是算法在正确工作。比如在状态2往下走下一步到了状态6。如果状态6附近有洞那么状态6的后续收益预期就不高这个信息会通过TD误差回传给状态2的Q值导致它下降。Q值下降本质上是在告诉我们“这条路后面有风险”不是什么异常现象。想要验证这一点你可以故意把洞的奖励改成-1跑完再看Q表靠近洞的状态Q值会明显更低。这就是奖励信号在“传导”。5.4 学习曲线后期还在大起大落检查epsilon和评估方式如果你发现后期成功率还在剧烈波动先检查epsilon有没有真正降到低值。有些人把epsilon_end设成0.5那等于一直有一半的探索曲线自然稳不下来。正常做法是训练结束后单独用不探索的贪婪策略去评估几十上百局看真实水平。另外一个小建议评估时不要复用训练环境对象最好新建一个环境。因为训练过程可能已经把环境状态留在某个中间位置复用容易污染测试结果。我的代码里evaluate函数就是内部new了一个新环境这是从项目经验里养成的习惯。6. 玩明白这个例子之后下一步往哪走6.1 Q-learning和Sarsa的差异一个乐观一个保守能把这个例子吃透之后可以对比一下Q-learning和Sarsa两者只有一个公式的差别Q-learning更新时用的是max_{a} Q(s,a)也就是新状态下所有动作里最好的那个Sarsa更新时用的是Q(s,a)其中a是按当前策略实际会选的那个动作。这个差别导致了两种算法性格不同Q-learning比较乐观它假设未来每一步都会选最优动作哪怕当前策略还做不到Sarsa比较保守它按当前策略的真实行为来估算未来收益。在FrozenLake这种有随机性的环境里Sarsa往往成功率略高但路径更绕Q-learning前期更激进。你可以在同一个环境里分别跑这两个算法对比它们的策略图这种感觉就出来了。我个人的建议是不用急着背两者的理论区别各写一遍、各跑一遍、各打印一次Q表差异自然就明白了。6.2 状态一大Q表就扛不住这时候轮到DQNQ表方法有个硬伤状态空间一大表格就爆炸。FrozenLake只有16个状态可以一行一行查表但如果状态是连续的比如机器人关节角度、车辆速度位置你根本没法枚举所有状态。DQNDeep Q-Network的思路就是用神经网络代替Q表。输入是状态输出是每个动作的Q值预测。Q-learning的更新思想保留下来但更新对象从“表格里的一个格子”变成了“网络里的参数”。这也是为什么我始终建议先把Q-learning玩明白——一旦理解了这个更新逻辑DQN里的很多设计比如经验回放、目标网络都是在处理“神经网络版Q表”带来的新问题而不是另起炉灶。如果你已经把FrozenLake这个例子自己从头到尾跑通了下一步可以去看CartPole那是一个状态更丰富、更适合感受DQN优势的环境。但前提是先把这篇里的Q表打印出来、改参数跑十遍把那个更新公式刻进脑子里再上路。我自己带人入门时几乎一定会让他在FrozenLake上多做一件事把Q表打印出来盯着看两分钟观察离终点越近的格子Q值整体越高、靠近洞的格子哪些动作被压制。这个观察过程比看十篇论文都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑