资讯动态

CliffWalking-v0:用48个格子跑通Q-Learning强化学习入门

发布时间:2026/10/21 18:19:50 来源:尧图企业网站定制
第一次接触强化学习最头疼的往往不是公式本身而是公式和代码对不上。Q值到底存在哪ε-greedy怎么落地环境返回的 observation 和 reward 又该怎么理解如果你也有这些困惑找一个足够小、又五脏俱全的环境踏踏实实跑一遍远比继续啃理论有效。CliffWalking-v0正是这样一个入口级环境一个4×12的小网格智能体要从左下角走到右下角中间横着一排“悬崖”。把这个案例跑通、读透表格型强化学习里最核心的奖励设计、Q表、探索与利用、时序差分更新你都会有非常直观的感受。1. 场景拆解先搞懂CliffWalking-V0在讲什么故事1.1 一个极小但五脏俱全的强化学习沙盒CliffWalking 在强化学习入门环境里和 FrozenLake 齐名。FrozenLake 考验的是“不确定性下做决策”冰面会打滑你选了方向也不一定按那个方向走CliffWalking 则把“每一步都要付出代价”这件事摆到了台面上——规规矩矩走一步扣1分掉下悬崖直接扣100分。这个设计特别适合用来理解“奖励函数如何塑造行为”。环境本身是一个4行12列的网格共48个小格子。起点在左下角终点在右下角最下面一行从第1列到第10列是悬崖区域。用坐标来说x表示列从0到11y表示行从0到3。起点是(x0, y3)终点是(x11, y3)悬崖是(x1~10, y3)。智能体每一步可以执行上、右、下、左四个动作中的一个。这里有一个关键细节环境返回的状态不是(x, y)坐标而是一个0到47的整数。老版本gym和新版gymnasium都采用公式s y * 12 x做编码。也就是说状态0代表左上角状态36代表左下角起点状态47代表右下角终点状态37到46就是那一排悬崖。动手写代码之前先把这条编码规则记在心里后面很多bug都出在这个换算上。1.2 状态、动作、奖励先建立坐标系在CliffWalking-v0里observation_space是Discrete(48)action_space是Discrete(4)。四个动作分别对应0上、1右、2下、3左。环境每次step会返回四个关键信息下一状态、即时奖励、是否终止、附加信息。奖励规则是整个环境设计最值得品的地方情况奖励是否终止正常走一步-1否踩进悬崖-100是回到起点到达终点0是每走一步扣1分意味着浪费步数就是浪费长期收益。如果没有任何步数惩罚智能体完全可以在安全区闲逛直到天荒地老反正最后总分也不会更差。加上步数惩罚之后“尽快到达终点”就变成了一个可优化的目标。这个“用奖励函数表达目标”的思路在真实项目里太常用了做推荐、做调度、做机器人控制本质上都是同一件事告诉智能体什么行为有价值什么行为有成本。为什么要扣100分而不是直接判负因为如果一个行为带来的惩罚不够显著智能体可能认为“掉下悬崖也没多大事”。-100和-1的差距足够大让智能体在更新Q值时能明显感知到危险的边界这也是强化学习奖励塑形中很常见的技巧让风险行为的代价远大于正常行为的代价。1.3 为什么它适合当第一个练手项目我推荐新手先跑CliffWalking而不是直接上CartPole或Atari有几个很现实的原因。第一状态空间小。表格型Q表只有48×4192个值一个二维数组就能装下训练几秒钟就能收敛。你可以随时把Q表打出来逐个格子观察智能体的判断。第二可解释性强。每个状态都有明确的物理意义策略好不好一眼就能看出来。相比之下深度强化学习动辄几十万参数模型学得好不好你只能看回报曲线猜。第三调参反馈快。学习率、折扣因子、探索率改一改重新训练也就几秒钟非常适合做对比实验。第四经典结论明确。CliffWalking最经典的现象是Q-Learning和Sarsa学出两种风格完全不同的路径一个贴崖走最短路一个绕路求稳。这种“同环境不同算法产生不同行为”的对比是理解on-policy和off-policy区别最好的入口。2. 环境搭建与基础交互先把“游戏”跑起来2.1 安装环境并确认版本环境安装本身很简单直接装gymnasium就行pip install gymnasium需要注意一个历史问题老版本的gym是很多旧教程里的主角但这个包已经不再积极维护社区现在统一使用gymnasium作为继承者。接口上它们大部分兼容但在reset和step的返回值上有明显差异。装好之后先写一段验证代码import gymnasium as gym env gym.make(CliffWalking-v0, render_modehuman) print(状态空间:, env.observation_space.n) print(动作空间:, env.action_space.n) state, info env.reset() print(初始状态:, state) env.close()如果看到状态空间输出48、动作空间输出4说明环境正常。注意新版gymnasium的reset返回两个值state和info老版gym的reset只返回一个int。这个差异我在后面专门列一个对照表。2.2 新旧接口的坑reset、step返回值差异很多新手拿着网上两三年前的代码直接跑报错或者行为诡异基本都是接口不匹配导致的。这里列一张对照表接口老版gym新版gymnasium安装包gymgymnasiumreset返回值state(state, info)step返回值(obs, reward, done, info)(obs, reward, terminated, truncated, info)终止标志doneTrue表示episode结束terminated表示环境自然结束truncated表示超时等截断渲染env.render()make时传render_modehuman听上去只是多拆一个返回值但我见过太多人在这上面卡半小时。老教程写的是obs env.reset() obs, reward, done, info env.step(action)新版本必须改成obs, info env.reset() obs, reward, terminated, truncated, info env.step(action) done terminated or truncated我的建议是新项目一律用gymnasium代码里统一处理terminated or truncated。这样以后从这个小环境迁移到Atari、MuJoCo这些复杂环境代码框架都不用换。2.3 跑一个随机策略感受环境动态正式写Q-Learning之前建议先跑一个完全随机的策略看看环境长什么样。这段代码什么都不学只是随机按方向键感受一下“掉悬崖”是什么体验import gymnasium as gym env gym.make(CliffWalking-v0, render_modehuman) state, info env.reset() done False step_count 0 while not done: action env.action_space.sample() next_state, reward, terminated, truncated, info env.step(action) done terminated or truncated print(fstep{step_count}, state{state}, action{action}, reward{reward}, next_state{next_state}) state next_state step_count 1 if done: print(fEpisode结束共走了{step_count}步) break env.close()跑几次你就能直观看到随机策略大概率很快踩进悬崖拿到-100的惨痛回报。这个“痛感”很重要——它帮你建立一种直觉强化学习的目标本质上就是让策略从“随机乱撞”变成“稳定拿到更多负分”最终逼近理论上的最优-13。3. Q-Learning算法拆解与代码实现3.1 Q值是什么一张记录状态-动作好坏的表格Q-Learning的核心是一个叫Q表的二维数组。行是状态列是动作值代表“在这个状态下先执行这个动作之后继续按最优策略走能拿到的期望总回报”。用CliffWalking来说Q[s][a]就是“当我在格子s朝某个方向走一步之后每一步都尽量聪明地走最后累计能拿多少分”。分数总体是负的因为每步都要扣分所以数值越大越好越接近0越好。初始时所有Q值都是0意味着智能体对任何格子、任何动作都没有偏好所有尝试全靠随机探索。Q表是这个环境最“透明”的产物。训练结束后你完全可以把它打印出来看看哪个格子下哪个动作的Q值最高。这样做的好处是学习过程不再是黑盒出了任何问题都能直接看到模型内部出了什么偏差。3.2 更新公式逐项拆开TD error到底是什么Q-Learning的更新公式长这样Q(s, a) ← Q(s, a) α [ r γ max_a Q(s, a) - Q(s, a) ]这里面的每个符号都值得掰开揉碎讲。r是执行动作a后拿到的即时奖励。γ是折扣因子代表未来的反馈相对于当前的折扣程度越接近1说明越看重长期收益。max_a Q(s, a) 是在新状态s下所有动作里Q值最大的那个表示“如果到了新位置后采取最优行动能期望到什么程度”。方括号里的整体叫TD error字面意思是“此时预测和事后看来的差距”。可以类比你在导航App里估计到车站要10分钟结果走到一半发现前方拥堵系统重新估算为15分钟——新增信息量与原估计的差值就是类似TD error的东西。Q-Learning每次只把这个差距的一部分由α控制合并进原来的估计所以叫增量学习而不是一步到位。特别强调一点当episode结束时更新公式里的未来项应该去掉。也就是说如果执行动作后环境返回terminatedTruetd_target就直接等于r不再加γ·max Q(s, a)。原因很简单都结束了后面没有状态了哪来的未来回报3.3 epsilon-greedy如何平衡探索与利用Q-Learning要选择动作又不能每次都选Q值最大的。如果一开始就在Q表全零的基础上贪心智能体会一直选动作0上永远走不出起点那一亩三分地。所以工程上最常用的方法是epsilon-greedy策略以ε的概率随机选一个动作探索未知信息以1-ε的概率选Q值最大的动作利用已有经验。def choose_action(state, epsilon): if np.random.random() epsilon: return env.action_space.sample() return int(np.argmax(Q[state]))ε设成0.1意味着平均每10步会有1步是随机的。探索太多学出来的策略不够稳探索太少可能学不到完整地图。对于CliffWalking固定0.1就能跑通如果想要更快的收敛可以让ε随着训练进程逐渐衰减从0.5掉到0.01。3.4 完整训练代码关键行逐条解读下面是完整的Q-Learning训练代码。我把终止条件的处理单独拎了出来避免很多教程里那个常见的bug。import numpy as np import gymnasium as gym # 1. 创建环境 env gym.make(CliffWalking-v0) # 2. 初始化Q表48个状态4个动作 Q np.zeros((env.observation_space.n, env.action_space.n)) # 3. 超参数 alpha 0.1 # 学习率新信息覆盖旧估计的强度 gamma 0.9 # 折扣因子未来回报的折现 epsilon 0.1 # 探索率随机动作概率 num_episodes 1000 def choose_action(state, epsilon): if np.random.random() epsilon: return env.action_space.sample() return int(np.argmax(Q[state])) rewards_per_episode [] # 4. 训练循环 for episode in range(num_episodes): state, _ env.reset() done False total_reward 0 while not done: action choose_action(state, epsilon) next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated # 5. 计算TD目标 if done: td_target reward else: td_target reward gamma * np.max(Q[next_state]) # 6. 更新Q值 Q[state, action] alpha * (td_target - Q[state, action]) state next_state total_reward reward rewards_per_episode.append(total_reward) print(训练完成)这段代码里最值得注意的就是第5步。走到悬崖时环境返回的next_state其实是重置后的起点reward是-100done为True。如果还机械地加上gamma * np.max(Q[next_state])就会把“从起点出发的最大价值”混入悬崖状态的更新里导致悬崖位置的Q值被莫名抬高。很多教程代码为了简洁不区分这个小环境里问题不明显但一旦换到复杂环境这种含糊写法会让算法行为变得难以捉摸。alpha取0.1是表格型方法比较稳妥的起点。太大会导致Q值来回震荡太小会拖慢收敛速度。gamma取0.9已经够用因为CliffWalking的episode很短未来回报不会因为折扣过大而丢失。如果想看更“目光长远”的智能体可以把gamma改到0.95或0.99对比一下路径变化。4. 训练过程与细节优化从震荡到收敛4.1 观察学习曲线怎么判断智能体学会了训练过程中我把每个episode的总回报存在了rewards_per_episode里。plot出来大概长这样前面几十个episode惨不忍睹经常是-50、-80甚至更差因为智能体在疯狂踩悬崖慢慢曲线开始向上爬最终稳定在一个区间震荡。import matplotlib.pyplot as plt plt.plot(rewards_per_episode) plt.xlabel(Episode) plt.ylabel(Total Reward) plt.title(Q-Learning on CliffWalking-v0) plt.show()单轮回报看起来很抖因为即使策略已经不错ε探索还是会让智能体偶尔踩崖。这时候用一个滑动平均看趋势更直观def moving_average(data, window50): return np.convolve(data, np.ones(window) / window, modevalid) smoothed moving_average(rewards_per_episode, 50) plt.plot(smoothed) plt.show()CliffWalking的理论最优总回报是-13对应贴崖最短路径起点向上1步向右11步再向下1步。如果你的滑动平均曲线最后能稳定在-15或更好的水平说明策略已经相当不错了。注意这里不用纠结必须精确到-13因为固定ε0.1的训练过程里始终有探索噪声。训练结束后把ε设为0再跑100轮记录平均回报这才是“无噪声”的测试表现。通常测出来会比训练曲线更好因为每一步都选Q值最大的动作不会再乱踩悬崖。4.2 Sarsa对比为什么Q-Learning路径更激进CliffWalking最有名的实验就是拿Q-Learning和Sarsa对比。两种算法在更新公式上只差一个字Q-Learning用max_a Q(s, a)作为未来回报估计Sarsa用的是Q(s, a)其中a是在s下实际按当前ε策略选出的动作。# Sarsa更新 Q[state, action] alpha * (reward gamma * Q[next_state, next_action] - Q[state, action])看起来只是“选最大值”和“按策略选一个”的区别但行为差异非常大对比维度Q-LearningSarsa属于哪类方法off-policy评估的是最优策略on-policy评估的是当前含探索的策略学到的训练路径贴悬崖边沿追求最短路绕到上方安全区求稳测试时平均回报接近-13大约-17或更差一些训练过程中踩崖频率较高因为贴着危险区较低路径天生保守原因用一句话概括Q-Learning的最大化操作“无视”了未来探索的风险它假设后面每一步都会完美执行于是敢贴崖走Sarsa把当前策略里“还会有ε概率随机乱走”这个事实算了进去因此评估出的价值里已经包含了“不小心踩崖”的代价路径自然远离悬崖。如果把两种算法训练完后的Q表打印出来你会发现Sarsa在最上面一行格子的Q值更高而Q-Learning在悬崖上方那一行格子的Q值更高。这种从数值到策略层面的差异是理解off-policy与on-policy最直观的一课。4.3 画一张策略表看清每个格子的决策数值总归抽象我更推荐把最终策略画成一张“箭头地图”。每个非起点、非终点、非悬崖的格子打印出Q值最高的动作arrows [↑, →, ↓, ←] for y in range(4): line for x in range(12): s y * 12 x if (x, y) (0, 3): line S elif (x, y) (11, 3): line G elif y 3 and 1 x 10: line X else: line arrows[np.argmax(Q[s])] print(line)训练完成后这段代码输出的策略图会非常直观地展示Q-Learning学到的那条贴崖路径上方两行基本都是“→”到了最右侧往下拐起点附近会先“↑”再“→”。看到这张图的时候你对“Q表表达策略”这件事就有了肌肉记忆。以后无论换什么环境只要状态空间还不算大我都会先画策略表看看这一步比看十行回报曲线的信息量都大。5. 常见问题与排查技巧实录5.1 问题速查表这段时间帮不少同学看过CliffWalking相关代码遇到的坑高度集中在下面几个问题现象常见原因解决方法运行报错“unexpected return from reset”新版gymnasium返回两个值按老版解包改成state, info env.reset()训练一直不收敛总在悬崖附近徘徊epsilon太大或未衰减尝试epsilon0.1固定值或从0.5衰减到0.01学出的路径绕远路gamma太小忽视长期收益调到0.9以上再试悬崖状态Q值莫名变高更新时done分支没有单独处理误加了未来项确认done时td_target reward多次运行结果完全不同没有设置随机种子np.random.seed(0)并在make环境前固定seed渲染窗口打开但不更新渲染模式或版本问题新版用render_modehuman结束后env.close()5.2 两个最容易忽视的隐性bug第一个是np.max和Python内置max混用。Q表是二维数组np.max(Q[next_state])是求一行里所有动作的Q值最大值这是我们要的如果随手写成max(Q[next_state])在某些版本下会返回整行的最大元素序列行为完全走样。写代码时宁可多打两个字符也不要在这上面省事。第二个是update时用错状态。我这里再强调一次TD target里用的是next_state的Q值不是当前state的Q值。很多初学代码写着写着就顺手抄成Q[state, action] alpha * (reward gamma * np.max(Q[state]) - Q[state, action])等于用自己更新自己训练曲线会非常离谱。出现这种情况直接在纸上把一次转移的三元组(s, a, r, s)写出来对着更新公式逐项标一遍一眼就能看出哪里写串了。5.3 我常用的一套排查流程遇到学不好的情况我不建议瞎调参而是按顺序排查。先看环境本身。随机策略跑几十轮确认step返回、reward、终止逻辑都符合预期。接着看Q表是否在变化。训练50轮后把Q[0]那个状态的一行打出来如果四个动作的Q值完全没变说明更新根本没走到如果变了说明主循环没问题。然后看单episode回报。挑几个episode手动打印状态轨迹看看智能体在哪个位置反复踩崖。最后才动超参数。epsilon、alpha、gamma逐个验证一次只改一个不然出了问题你根本不知道是谁引起的。这套流程在CliffWalking上跑通后迁移到其他表格型环境一样适用。调强化学习模型和做传统程序调试最大的区别是信号是带噪声的。曲线抖不代表代码错完全平滑反而要怀疑是不是策略已经僵化。你要学会区分“正常噪声”和“系统性偏差”前者多看几条曲线就能判断后者通常具有反复出现的规律比如每到某一个特定状态附近就莫名变差。结尾一点个人体会我至今觉得CliffWalking-v0是被低估的入门环境。它的奖励设定足够简单但又足以支撑你认真思考“为什么这里的步数惩罚会让智能体选择绕路”、“为什么离悬崖越近反而Q值越高”、“为什么不同算法会走出风格迥异的路线”。我在第一次画出那张贴崖路径策略表的时候突然明白了之前看论文时不太理解的“探索-利用困境”一个在悬崖边来回试探的智能体本质上就是在一个高不确定性区域不断做抽样代价大但收益也大。这个环境像一座小的训练场练完之后你再看DQN、PPO那套复杂框架至少理解它们想解决的原始问题是什么。如果你也在学强化学习的路上卡壳我真心建议你先把这48个格子玩透再往深了走。

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

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

免费获取报价 →
↑