☰
从CliffWalking到Q-Learning:强化学习入门的第一张价值地图
2026/10/21 8:30:25 网站建设 项目流程

如果你刚接触强化学习,可能看到的第一条入门建议都是“先跑通倒立摆”。但我个人更建议你先玩一遍gym里的CliffWalking-v0。这个4×12的小网格世界看起来简单得像一张跳棋棋盘,却把强化学习最核心的东西一次讲完:状态、动作、奖励、回报、探索与利用。我第一次跑通这个案例时,被一个反直觉的结果惊到了——最优策略不是离悬崖越远越好,而是贴着悬崖边缘走那一条13步的安全通道;保守地绕最远的路,反而因为多走的每一步都在扣分,累计回报更低。

这篇文章把我从环境认识、Q-Learning实现、可视化复盘到调参踩坑的完整过程记录下来,适合刚入门强化学习、想动手完成第一个小项目的人参考。你只需要会一点Python基础,以及“Q表就是一张大号查找表”的直觉就够了。

1. 从零认识CliffWalking-v0:一张地图背后的完整强化学习要素

1.1 为什么拿它当入门案例,而不是倒立摆

倒立摆这类经典环境把状态定义为连续的位置和速度,策略是“别让它倒”,对初学者一点也不直观。CliffWalking-v0则不同:状态就是格子坐标,动作就是上下左右,连策略都能直接画成箭头图。强化学习的术语在这里一个个都有了实物对应——“状态”就是智能体站着的格子,“动作”就是它下一步往哪走,“奖励”就是一路上收到的加减分。搞懂这个环境,等于把打地基的名词都过了一遍。

另一个好处是状态空间足够小。总共48个格子、4个动作,Q表可以完整打印、画图、检查,这对第一次写训练代码的人特别友好。我见过太多人第一次跑强化学习就在CartPole上碰到“明明代码没错但就是学不会”的玄学问题,而在CliffWalking上,你的每一步决策都能看得见、能复盘,排查起来要踏实得多。小环境还有一个隐藏价值:跑一轮训练只需要几秒钟,你可以放心大胆地反复改参数做实验,成本极低。

1.2 地图、坐标与奖励规则

标准地图长这样:

o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o o S H H H H H H H H H H G

第一到第三行是安全通道,第四行从左到右依次是起点S、10个悬崖格H、终点G。用坐标说,S在(3,0),G在(3,11),悬崖是(3,1)到(3,10)。gym里的observation是0到47之间的整数,常见编码是 row*12+col,所以观察值0代表左上角,47代表右下角,起点对应37号,终点对应47号。

奖励规则只有三条,非常干净:每走一步扣1分;走进悬崖扣100分,本回合直接结束;走到终点加1分,本回合结束。需要注意一点:掉进悬崖后环境会把智能体重置回起点,但回合是终止的,不会再让你从起点继续磨。

提示:掉入悬崖时 reward 是 -100,并且 terminated 会变成 True。先记住这条,后面调Q表时你会反复用到。

由此可以算出最优路径的回报:从起点往上一格到第二行,右移11格到终点正上方,再往下一格进终点,一共13步,前12步各-1,最后一步+1,总回报-11。如果绕到最顶上一行走,要17步,回报-15。所以最优策略恰恰是贴着悬崖所在的第二行走,而不是远远绕开——这是环境设计最精髓的地方。它用一组简单的奖励数值,让“风险与收益的权衡”变成了能亲手摸到的格子。

1.3 认识reset、step和返回值

不管算法多复杂,和gym交互的口子就两个方法:reset()开新回合,step(action)走一步。新版gymnasium的写法是:

import gymnasium as gym env = gym.make("CliffWalking-v0") state, info = env.reset() print(state, info) action = env.action_space.sample() # 随机抽一个动作 next_state, reward, terminated, truncated, _ = env.step(action) print(next_state, reward, terminated, truncated)

reset()返回两个值:observation和info;step()返回五个值:新状态、奖励、terminated、truncated、附加信息。terminated表示回合自然结束,对应“到达终点”或“掉进悬崖”;truncated表示被步数上限或外部条件截断;info在CliffWalking里基本是空字典。动作编号0、1、2、3分别对应上、右、下、左,不同版本可能略有差异,稳妥起见可以自己打印环境源码里的动作表确认。

第一次接触这套API时,你只需要建立两个直觉:reset是“开局撒点”,step是“走一步看一步”。后面所有的训练循环,本质都是这两个方法加上一张Q表的无限循环。

2. 理解Q-Learning为什么能学会贴着悬崖走

2.1 MDP四要素在网格世界里的投影

先把术语落地。强化学习建模的马尔可夫决策过程有四个要素:状态集合S、动作集合A、转移概率P、奖励函数R。在CliffWalking里,S就是48个格子,A是4个方向,R就是刚才那三条奖励规则。转移概率P在这个环境里是确定性的——你选择“上”,就一定从当前格子走到上面一格;只有超出边界时会被留在原地并照常扣一分。

确定性是这个入门案例的馈赠。随机环境,比如后续要提到的FrozenLake,同一个动作可能走到不同格子,处理的是“期望”问题,第一次接触时容易分心。先把确定性的Q-Learning彻底搞懂,再去看随机转移,逻辑就顺了。这也是为什么我一直推荐用CliffWalking而不是FrozenLake作为第一个实战案例——先用确定性问题建立直觉,再用随机性问题打破直觉。

2.2 奖励设计是如何塑造“胆量”的

有些读者会疑惑:既然最优路径贴着悬崖,为什么不多绕几行更安全?答案全在奖励里。每走一步-1,意味着路径越长代价越大;悬崖-100,意味着碰到悬崖的瞬间付出巨大代价。这两个信号组合起来,最优路径就是“尽量短且不完全避开悬崖附近”的那条,正好是第二行通道。

这个设计在教学上非常高明:它逼着智能体同时学会两件事——既要追求目标(终点+1),又要控制风险(-100)。如果只有终点奖励没有步罚,智能体可能在起点附近乱逛很久;如果悬崖惩罚不够重,它又会不管死活直穿悬崖。奖励的比值关系,直接决定了策略的“性格”。你甚至可以做个思想实验:把悬崖惩罚改成-10,最优策略很可能就变成“直接横向穿过悬崖行”,因为那只有11步,总回报-9,比绕第二行还高。你想让算法学到什么行为,就得先算清楚奖励数字背后的账。

2.3 折扣因子γ:看得远还是看得近

Q-Learning更新时会出现一个gamma,叫折扣因子,控制未来奖励在智能体眼里的权重。回报的定义是累计折扣奖励:

G_t = R_(t+1) + γR_(t+2) + γ²R_(t+3) + ...

γ=0时,智能体只看眼前一步,Q值只会吸收当前奖励。你可以做个实验:此时除了终点正上方的格子能学会“往下走有+1”,其他格子的Q值几乎一样,策略退化成一通乱走。γ=0.9时,远处终点的+1经过折扣还有不小的分量,智能体才愿意为远目标忍耐一路的-1。

所以CliffWalking里γ建议从0.9起步。这个“看得远”的参数,是强化学习区别于普通贪心算法的灵魂。调小γ,你会看到智能体变得短视;调大γ到0.99,它会对长远收益更敏感,在这个短任务里也能正常收敛。理解γ的最好方式,就是把它当成一个“耐心旋钮”。

3. Q-Learning实战:环境搭建与训练循环

3.1 安装环境与版本选择

这里有一个很多教程没点透的坑:老版gym和新版gymnasium的API不兼容。如果直接搜到一段旧代码贴进新环境,多半会崩在reset或step返回值的数量上。现在推荐安装gymnasium,它由社区持续维护,API也更规范:

pip install gymnasium

创建环境只需要一行:

import gymnasium as gym env = gym.make("CliffWalking-v0") print(env.observation_space) # Discrete(48) print(env.action_space) # Discrete(4)

这个案例跑通之后,同样的API可以无缝切换环境名去跑FrozenLake-v1、Taxi-v3,这是gym生态最值钱的地方。你学会的不是某一个环境的代码,而是一套可以迁移到很多环境上的交互方式。

3.2 Q表与TD更新公式

Q-Learning的核心是维护一张表,行是状态,列是动作,值是“从这格子执行这个动作,之后还能拿多少回报”的估计。CliffWalking的Q表就是一个(48,4)的数组。初始化选全零就好,不用随机初始化,因为这里的值和神经网络里的初始化完全不同,Q表更新会主动探索覆盖所有格子。

更新公式长这样:

Q(s,a) ← Q(s,a) + α * [ R + γ * max_a' Q(s',a') - Q(s,a) ]

方括号里前半部分 R + γ * max_a' Q(s',a') 叫TD目标,意思是“实际走了一步看到的短期奖励 + 下一步最优价值的估计”;当前Q(s,a)是旧预测;两者之差叫TD误差。α是学习率,决定这次用误差修正多少。这个公式的本质是:每走一步,就拿新信息修正旧估计。你可以把它类比成记账:每次行动后对比“原来的预期”和“实际发生的收支”,然后往预期里补一点差值。

特别提醒一点:如果s'是终止状态,一定不要加γ*max项,因为终止之后没有后续了,TD目标直接等于R。不少初学者在这里漏写判断,导致Q值被污染,后面会详细讲。

3.3 完整训练代码:ε-greedy与探索衰减

完整可运行的代码在这里,我加了比较多的注释:

import gymnasium as gym import numpy as np import random env = gym.make("CliffWalking-v0") n_states = env.observation_space.n # 48 n_actions = env.action_space.n # 4 Q = np.zeros((n_states, n_actions)) alpha = 0.5 # 学习率 gamma = 0.9 # 折扣因子 epsilon = 1.0 # 初始探索率 epsilon_min = 0.1 # 最小探索率 epsilon_decay = 0.995 episodes = 500 record = [] best_total_reward = -float("inf") best_Q = Q.copy() for episode in range(episodes): state, _ = env.reset() total_reward = 0 done = False steps = 0 while not done: # ε-greedy:按概率随机探索,否则选当前Q最大的动作 if random.random() < epsilon: action = env.action_space.sample() else: action = int(np.argmax(Q[state])) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated # 计算TD目标,终止状态不再往后看 if done: td_target = reward else: td_target = reward + gamma * np.max(Q[next_state]) Q[state, action] += alpha * (td_target - Q[state, action]) state = next_state total_reward += reward steps += 1 if steps > 200: break epsilon = max(epsilon_min, epsilon * epsilon_decay) record.append(total_reward) # 保存历史最优Q表,用于后期回放 if total_reward > best_total_reward: best_total_reward = total_reward best_Q = Q.copy() np.save("cliff_Q.npy", best_Q)

几个设计理由说一下。ε从1.0开始,前期几乎纯随机,让Q表有机会覆盖所有状态动作对;随着训练推进,ε按0.995指数衰减到0.1,后期大部分动作走贪心,只在少数时候探索。保存best_Q而不是直接用最终Q,是因为ε还在的时候Q表可能被探索动作扰动,用历史最佳快照做回放更稳。训练时不用开渲染,否则500回合的绘制开销会把训练拖慢几十倍。

4. 可视化复盘:看着智能体一点点学乖

4.1 用单回合回报曲线判断训练状态

训练过程中record记录的是每一回合的总回报。直接把500个点画出来会很抖,因为探索阶段经常掉悬崖,单回合可能只有-100多。通常用移动平均看一眼趋势:

import matplotlib.pyplot as plt def moving_average(data, window=20): return np.convolve(data, np.ones(window) / window, mode="valid") plt.plot(moving_average(record, 20)) plt.xlabel("episode") plt.ylabel("average total reward") plt.show()

你会看到曲线刚开始在负值很深的地方徘徊,几十回合后逐渐爬升,最后稳定在-20到-11之间。如果中途偶尔出现-100的尖峰,多半是ε还在触发探索动作、偶尔踏进悬崖,这属于正常现象。如果500回合后移动平均还稳定在-50以下,那就要考虑是不是参数或代码出问题了,后面章节会展开排查。

4.2 用状态价值热力图检查Q表质量

Q表训练完,可以计算每个状态的价值V(s) = max_a Q(s,a),再reshape成4×12画热力图:

import matplotlib.pyplot as plt V = best_Q.max(axis=1).reshape(4, 12) plt.imshow(V, cmap="coolwarm") for i in range(4): for j in range(12): plt.text(j, i, f"{V[i, j]:.1f}", ha="center", va="center", fontsize=8) plt.colorbar() plt.show()

训练完成后,热力图上第二行的价值应该明显高于第一行和第三行,终点上方的格子尤其亮,悬崖那一行则非常暗。这张图最直观的地方在于:它会告诉你智能体脑子里装的不是一条路径,而是一张完整的“价值地图”。如果你看到悬崖行的某些值居然发亮,那多半是更新Q时没正确处理终止状态,把掉崖后续的起点价值也算进去了。

4.3 让训练好的策略跑一遍给你看

可视化回放是整个过程中最有成就感的一步。把保存的Q表加载回来,全程用贪心动作走:

import time import numpy as np import gymnasium as gym Q = np.load("cliff_Q.npy") env = gym.make("CliffWalking-v0", render_mode="human") state, _ = env.reset() done = False while not done: action = int(np.argmax(Q[state])) state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated time.sleep(0.3)

你会看到智能体几乎笔直地从起点绕到第二行,然后一路向右,最后一步下到终点。如果它偶尔在某个位置原地犹豫或绕路,说明那个区域的Q值还不够清晰,可以针对性增加该区域的探索次数。回放不仅好玩,更是调试Q表的好手段——比冷冰冰的数字直观太多了。

5. 调参实验与踩坑记录:让训练又快又稳

5.1 学习率、折扣因子、探索率的实际手感

光看理论很难找到感觉,我强烈推荐动手做三组小实验,每次固定其他参数,只改其中一个:

参数试偏大的值试偏小的值我的基线建议
学习率α0.90.10.5
折扣因子γ0.990.20.9
初始探索率ε从1.0但几乎不衰减从0.2开始1.0,按0.995衰减

α偏大时,Q值每一步被改得很猛,后期会来回跳动,训练曲线像锯齿;α偏小时学得很慢,500回合可能还没摸到终点。γ偏大(0.99)对长远收益更敏感,这个短任务里还算稳定;γ偏小到0.2,智能体基本变成“只顾眼前”,很容易卡在起点附近,甚至在一段负奖励里不敢动弹。ε衰减太快会让它过早锁定一条次优路径,衰减太慢则浪费大量后期回合乱走。

我把这些手感写下来,是想强调一个经验:调参并不是玄学,而是“理解每个旋钮改变的是什么机制”。α决定修正幅度,γ决定规划视野,ε决定探索预算。你在CliffWalking上花半小时试几组参数,比背十遍公式都管用。

5.2 gym与gymnasium的API坑,照着这张表避雷

我见到过的初学者报错,一大半是API版本问题。两个生态的差异,总结成一张表:

差异点旧版 gym新版 gymnasium
安装包pip install gympip install gymnasium
reset返回值observationobservation, info
step返回值obs, reward, done, infoobs, reward, terminated, truncated, info
渲染入口env.render()gym.make(..., render_mode="human")

如果你看到“too many values to unpack”这类报错,通常就是step返回值数量对不上。新版不在make时传render_mode,直接调用env.render()会报错;训练阶段也千万别开human渲染,否则每个回合都要绘制图像,速度会慢到让人怀疑人生。老规矩:训练用默认无渲染,回放时才开human。

5.3 掉崖后状态重置:一个容易污染Q表的细节

CliffWalking有个隐藏行为:智能体一旦掉入悬崖,回合结束,同时环境会把位置重置回起点。这带来一个编程陷阱:如果在done的情况下还按公式加γ*max_a Q(next_state),而next_state已经是起点,就会把起点Q值往“掉崖也没什么大不了,反正回到原点”的方向污染。

正确做法是前面代码里的写法——判定done之后,TD目标直接取reward。这个细节在CliffWalking上特别明显:不做这个判断,训练曲线就会带着莫名的波动,热力图上起点周围也会出现不该有的高亮。类似的陷阱在很多环境里都存在,养成“终止状态不往后看”的习惯,能帮你省下大量排查时间。

5.4 学完这个案例,下一步去看什么

同一个Q-Learning框架,改一行环境名就可以去跑FrozenLake-v1。那里转移概率是随机的,你会发现原代码还能跑,但收敛变慢、偶有震荡——这正好帮你理解“确定性环境与随机环境”的区别,以及为什么Q-Learning里的max要理解为“最优情况估计”而不是“单次采样结果”。

再往后可以看Taxi-v3,状态空间变成500个,Q表有3000个值,仍然跑得动,但你会开始意识到“大状态空间下查表开始吃力”,这为后面引入函数逼近埋下伏笔。等你想处理CartPole这种连续状态时,就该探索深度Q网络了。我个人这段时间的心得是,CliffWalking不只是一个“第一个demo”,它是一张值得反复利用的实验床。想验证任何新想法——改探索策略、换更新规则、测试参数影响——都可以先在这个环境上跑出可解释的结果,再往更复杂的环境迁移。

跑完这个案例后,我最大的收获不是背下了Q-Learning公式,而是建立起“看曲线、看热力图、看回放”这一整套调试直觉。最后再分享一个实际工作中的小习惯:训练完把Q表存成npy文件,之后每次想验证新想法,都从同一份Q表出发对比,而不是每次重新跑一遍全量训练。这个习惯看着简单,却能让实验可复现性提高一大截。如果这篇文章能帮你少踩几个版本坑,那它就没白写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询