Q-learning:那个改变一切的算法
1989 年,Chris Watkins 的博士论文里有一行更新式。它不需要知道游戏规则,不需要知道转移概率,甚至不需要按最优策略去走——却能收敛到最优。这一章我们让它从一张全零的表开始,然后和第 7 章那个动态规划的答案对一对。
把 max 塞进去
上一章的 TD 更新式是在评估一个给定的策略:
V(s) ← V(s) + α [ r + γV(s') - V(s) ]
它只回答「按当前这套习惯走,这儿值多少」。要改进策略,得再走一步。
Q-learning 做了两个改动,就完成了这一步:
改动一:学 Q 不学 V。第 7 章说过原因——有了 V 你还得知道 P 才能选动作,有了 Q 直接挑最大的就行。而 P 正是我们没有的。
改动二:目标里用 max。
Q(s,a) ← Q(s,a) + α [ r + γ max Q(s',a') - Q(s,a) ]
a'
└──┬──┘
「下一步我会挑最好的那个」
完了。这一行就是 Q-learning 的全部。
把第 8 章那张表翻出来对一下:
Q*(s,a) = Σ P(s'|s,a) [ r + γ max Q*(s',a') ] ← 贝尔曼最优方程
s' a'
Q-learning 就是这个方程,把 Σ 换成采样、把等号换成「慢慢挪过去」。
- Σ P(s'|s,a) 换成「实际走到的那一个 s'」 —— 用采样代替期望(第 10 章那一招)
- 等号 换成
+= α(目标 − 当前)—— 一点点挪过去,噪声自然被平均掉
剩下的 max 原封不动地保留了。所以它逼近的是 Q*,也就是最优的那个。
那个 max 带来的怪事:离策略
停下来看一眼这一行里的一个不对称。
agent 实际执行的动作是 ε-greedy 选的——大部分时候贪心,10% 的时候瞎选。但更新目标里用的是 max Q(s',a'),也就是「假设下一步我会走最好的那个」。
这两个不是同一个策略。
走路的策略,和学习的策略,可以不是同一个。
- 行为策略(behavior policy):实际用来采样的,这里是 ε-greedy。它得爱探索。
- 目标策略(target policy):你真正想学会的,这里是纯贪心的最优策略。
这个分离带来三个很实在的好处:
- 可以放心探索。那 10% 的瞎走不会污染「最优策略长什么样」的估计——因为估计里用的是 max,不是实际走的那个。
- 可以从别人的经验里学。人类玩的录像、一个老版本 agent 的日志、甚至完全随机的乱走,都能拿来训练。这直接催生了经验回放(第 15 章)和整个离线强化学习领域。
- 数据可以重复使用。存下来的经验永远不过期——因为你学的是最优策略,和当初是谁走的无关。
但这个便利有代价。第 16 章那个致命三角里,「离策略」正是三根柱子之一。而下一章你会看到它在悬崖边上的另一个后果。
看它从零学起来
每格里的四个数字是四个动作各自的 Q 值。看它们从哪里开始变得非零,往哪个方向长。
盯住上面那个「策略和最优解一致」的计数——看它什么时候变成满分。
三个阶段
第 1 局:一张空表
几乎所有格子还是 0。箭头是乱指的——因为四个动作全是 0,argmax 挑的是第一个。
这个状态值得看一眼,因为它解释了第 21 章那个稀疏奖励的惨案:当所有 Q 都相等的时候,「贪心」和「随机」是同一件事,学习没有任何抓手。
第 20 局:靠近终点的先亮
紧挨着 +1 的那几格开始有值了。和第 7 章那个水波一模一样——只不过这次,波每传一格都需要 agent 真的走过那一格。
这就是无模型比动态规划慢的地方:动态规划一轮扫描就能让所有格子都更新一次,Q-learning 得靠腿走过去。
第 5000 局:和最优解完全一致
策略和第 7 章那个动态规划算出来的每一格都一样,包括左下角那几个「往左绕远路」的反直觉箭头。
而这一路,它从来没有看过转移概率表。
Watkins 证明了:只要每个「状态-动作」对被访问无穷多次、学习率按一定条件衰减,Q-learning 一定收敛到 Q*——不管你用什么策略去走。
这个结论在当时是相当反直觉的。它说的是:
「不知道规则」和「学不到最优」是两回事。
你可以对这个世界一无所知,靠瞎撞收集经验,然后收敛到那个需要完整规则才能算出来的最优解。
当然那两个条件很苛刻——「每个状态-动作对访问无穷多次」在 Atari 里是彻底做不到的(第 14 章会给你那个数字)。但这个定理立了一根桩:这条路走得通。后面二十年的工作,都是在想办法在有限的样本、有限的内存下逼近它。
二十行写完
这个算法短到值得整个抄一遍:
import numpy as np
def q_learning(env, episodes=5000, alpha=0.2, gamma=0.99, eps=0.2):
Q = np.zeros((env.n_states, env.n_actions))
for ep in range(episodes):
s, _ = env.reset()
while True:
# ── 行为策略:ε-greedy ──
if np.random.rand() < eps:
a = np.random.randint(env.n_actions)
else:
a = np.argmax(Q[s])
s2, r, terminated, truncated, _ = env.step(a)
# ── 这一行就是 Q-learning ──
target = r + gamma * np.max(Q[s2]) * (not terminated)
Q[s][a] += alpha * (target - Q[s][a])
s = s2
if terminated or truncated:
break
return Q, np.argmax(Q, axis=1) # Q 表 + 从中读出的策略
去掉注释和样板,核心就是那两行。
1989 年到 2013 年 DQN 出现之间的二十四年,这两行几乎没有变过。变的只是「Q 存在哪里」——从一张表,变成一个神经网络。而那一个改动,就是卷 IV 的全部内容。
这是 Q-learning 一个真实存在、而且很难察觉的毛病,叫最大化偏差。
想想为什么:Q 值是带噪声的估计。取四个带噪声的数的最大值,你倾向于选中那个噪声恰好偏高的。于是 max Q 系统性地大于「真实的最大值」。
更糟的是这个高估会顺着自举往回传:高估的 max Q(s') 抬高了 Q(s,a),后者又抬高了它前面那一格。整张表一起往上飘。
标准解法叫 Double Q-learning:用两张 Q 表,一张负责选动作,另一张负责给那个动作打分。噪声是独立的,于是「选中偏高的」和「打分偏高」不再同时发生。
# 普通 Q-learning:同一张表既选又评 —— 高估 target = r + gamma * np.max(Q[s2]) # Double Q-learning:一张选,一张评 a_star = np.argmax(Q_a[s2]) # A 表选 target = r + gamma * Q_b[s2][a_star] # B 表评
2015 年这个想法被搬进 DQN,叫 Double DQN,是那批改进里效果最扎实、代价最小的一个——改两行代码,Atari 上多个游戏的分数明显上涨。
Watkins 自己说过,Q 就是 quality 的首字母——「这个状态下这个动作的质量」。
没有什么深意,但记住这个对读公式有帮助:V 是「这个位置多好」,Q 是「这个位置做这件事多好」。多一个自变量,多一份信息,而多出来的那份信息恰好是「选动作」需要的那份。
把上面那二十行接到 Gymnasium 上,是这本书里最值得你亲手做一次的练习:
pip install gymnasium
import gymnasium as gym
env = gym.make("FrozenLake-v1", is_slippery=True)
# env.observation_space.n = 16, env.action_space.n = 4
三个检查点,一个个过:
- 先用
is_slippery=False(确定性)跑。几百局就该稳定走到终点。跑不通说明代码有 bug,不是超参问题。 - 再开
is_slippery=True。会难很多,成功率大概只有 70%——因为这个环境是三向均分的滑,比本书那个 80/10/10 狠得多。 - 最后用第 7 章那个
value_iteration()算出精确答案,和你的 Q 表比一比。这一步是整个练习的意义所在——它是你唯一一次能确切知道「学对了没有」的机会。
之后你会跳到 SB3 的 DQN,从此再也不会自己写这两行。但写过一次和没写过,读别人代码的时候完全是两种感觉。
Q-learning 能直接训那个学走路的小人吗?
不能,而且卡在一个很具体的地方:那个 max。
小人的动作是四个关节的连续力矩。max 要求你把所有动作枚举一遍——而连续空间里有无穷多个动作。
两条出路,正好是后面两章:
- 把动作离散化:每个关节只允许 −1 / 0 / +1 三档。四个关节就是 81 种组合,可以枚举了。粗糙,但真的能用。
- 换一条路:不学 Q,直接学策略(第 17 章)。策略网络直接输出四个实数,压根不需要枚举。
这就是为什么连续控制(机器人、走路、开车)几乎全在用 PPO、SAC 这一系,而不是 DQN 这一系。分野点就是这一个 max。
这一章的一句话
把贝尔曼最优方程的 Σ 换成采样、等号换成慢慢挪,就得到了 Q-learning。它证明了「不知道规则」和「学不到最优」是两回事。
下一章:把那个 max 换成「我实际会走的那个动作」,得到一个叫 SARSA 的算法。一个词的差别,两个算法会在悬崖边上走上完全不同的两条路——而且都有道理。