简介:《强化学习的数学原理》英文专著PDF,由西湖大学赵世钰教授撰写,面向希望从数学层面系统理解强化学习的学习者与研究者,也适合具备一定数学基础、需要夯实理论框架的读者。全书从基础概念与网格世界示例切入,依次讲解状态值与贝尔曼方程、最优状态值、值迭代与策略迭代、蒙特卡洛方法、随机近似、时间差分、值函数近似、策略梯度及Actor-Critic方法,附录还讨论序列收敛性。压缩包仅含1个PDF文件,大小约18.9MB,便于集中阅读与检索。已有848人学习,内容以大量例子串联公式推导,强调从零开始揭示本质原理,可帮助读者掌握核心思想、理解算法背后的数学工具,并用于分析强化学习方法的收敛性与设计思路。
1. 强化学习不是调参玄学:十万行训练代码背后其实只在解一个方程
多数人第一次接触强化学习,是从一条起伏不定的 reward 曲线开始的:跑起来、调学习率、看它涨不涨,涨了就说 work 了,崩了就开始怀疑环境、怀疑随机种子。这种体验很容易让人把强化学习归到玄学那一栏。但只要把视角往底层推一层,任何一次q(s,a) ← q(s,a) + α[...]的更新,本质上都是在逼近某个方程的解——要么是贝尔曼方程,要么是贝尔曼最优方程。收敛与否,在写下那个方程的时候就已经基本决定了,和你堆多深的网络、采多少样本关系没那么大。
《强化学习的数学原理》这份书稿的价值就在这里:它没有从"智能体怎么和环境交互"这种工程视角铺开,而是先把网格世界拆成状态、动作、转移、奖励、策略、回报,紧接着把问题写成矩阵方程,再一路推到不动点。搞懂这条线索,那些看起来靠运气调出来的曲线,就变成了有据可查的数值过程。
2.1 网格世界:把环境压缩成四件套
整本书只用了一个环境——网格世界。这个选择乍看有点"偷懒",实则是最省认知成本的做法:状态是格子的坐标,动作是上下左右,撞墙就停在原地,到达目标格给正奖励,踩到陷阱格给负奖励。一旦环境小到可以用纸笔算出每个状态的值,你写的代码算错了就无处可藏。很多人在 CartPole 上跑了几百次,最后也说不清问题出在算法还是实现;而在一个 5×5 的格子上,答案通常是唯一的。
形式化之后,一个马尔可夫决策过程由五样东西定义:状态集合 S、动作集合 A、转移概率 p(s'|s,a)、奖励 r(s,a)、折扣因子 γ。转移概率是环境的"物理规律",奖励是环境给的即时反馈,γ 是一个介于 0 和 1 之间的数。策略 π(a|s) 是智能体自己的规则,它和环境的区别必须分清楚——策略是你训练的、可以被改的,转移概率是你改不了的。这条边界在后面讲无模型方法时会变得极其重要。
马尔可夫性要单独说一句。它要求下一状态只依赖当前状态和动作,与更早的历史无关。现实任务里这个假设经常不成立(比如只看一帧画面判断不了球的运动方向),工程上的补救办法是把最近几帧拼成一个状态,让拼出来的状态重新满足马尔可夫性。这不是数学上的妥协,而是把"记忆"塞进了状态的表示里。
2.2 状态值:为什么不能盯着即时奖励看
回报 G_t 的定义是把未来所有奖励按 γ 折现加起来:
G_t = R_{t+1} + γR_{t+2} + γ²R_{t+3} + ...折扣因子 γ 的存在有两个理由,一个是数学上的,一个是经济上的。数学上的理由是:如果 γ = 1 且任务没有终止,这个级数可能不收敛,值函数直接发散;γ < 1 时,因为每个奖励都有界,级数被一个几何级数压住,必然收敛。经济上的理由是:同样的奖励,早拿到比晚拿到更有价值,γ 就是这个"时间偏好"的量化。
状态值 v_π(s) 定义为从状态 s 出发、始终遵循策略 π 时回报的期望:
v_π(s) = E[G_t | S_t = s]这里必须取期望,因为转移是随机的、策略也可能是随机的,同一条轨迹再走一遍回报就不一样。状态值刻画的是"这个位置有多好",注意它绑定在某个策略上——换一个策略,同一个格子的值就变了。很多人第一次看到 v_π(s) 会下意识以为它描述环境,其实它描述的是"环境 + 策略"的组合。
2.3 贝尔曼方程:一次递归就把无穷项折没了
直接在定义里展开 G_t,会得到一个无穷长的和,没法算。贝尔曼方程做的唯一一件事,就是把第一项拆出来,剩下的部分重新识别成"下一状态的状态值":
v_π(s) = Σ_a π(a|s) Σ_{s'} p(s'|s,a) [ r(s,a) + γ v_π(s') ]这一步之所以成立,靠的是马尔可夫性和期望的线性性。拆完之后,无穷项消失了,取而代之的是一个关于 v_π 的线性方程组——等号两边都出现了 v_π。这就是它被称为"方程"而不是"公式"的原因:它不是在算一个值,而是在描述一组值之间必须满足的约束关系。
把状态维度拉出来写成向量,方程变成:
v_π = r_π + γ P_π v_π其中 P_π 是 |S|×|S| 的矩阵,第 s 行第 s' 列的元素是"在状态 s 下遵循 π 后转移到 s' 的总概率":
[P_π]_{s,s'} = Σ_a π(a|s) p(s'|s,a)这个矩阵每行加起来等于 1,本质上是一个马尔可夫链的转移矩阵。r_π 是把奖励对动作求期望后得到的向量。这一步转换的意义在于:原来那个带 Σ 的递归式,被压成了线性代数里最标准的形式,接下来怎么解,全是现成的工具。
2.4 闭式解与迭代解:有解析解为什么还要迭代
既然 v_π = r_π + γP_π v_π 是线性方程组,直接移项就能得到闭式解:
v_π = (I - γP_π)^{-1} r_π这个式子成立的前提是 I - γP_π 可逆。因为 P_π 的谱半径不超过 1,γ < 1 时 γP_π 的谱半径严格小于 1,所以 I - γP_π 一定可逆,解唯一。数学上很漂亮,但工程上有代价:矩阵求逆是 O(|S|³),状态数上万时基本没法用。
另一条路是把它当成不动点来迭代:
v_{k+1} = r_π + γP_π v_k每次迭代只做一次矩阵-向量乘法,复杂度 O(|S|²)。更重要的是,这个迭代的误差按 γ^k 的速度几何衰减,γ 越接近 1 收敛越慢——这解释了为什么折扣因子设成 0.99 时,训练前期总是磨磨蹭蹭。
下面这段代码把两种解法放在一起,可以直接跑:
import numpy as np def policy_eval_closed(P_pi, r_pi, gamma): """闭式解:v = (I - gamma * P_pi)^(-1) r_pi P_pi: (S, S),策略诱导的转移矩阵 r_pi: (S,),策略下的期望即时奖励 状态数上千时慎用,O(S^3) """ S = P_pi.shape[0] return np.linalg.solve(np.eye(S) - gamma * P_pi, r_pi) def policy_eval_iter(P_pi, r_pi, gamma, tol=1e-10, max_iter=100000): """迭代解:反复套 v <- r + gamma * P v 每轮 O(S^2),误差每轮至少缩 gamma 倍 """ v = np.zeros(P_pi.shape[0]) for k in range(max_iter): v_next = r_pi + gamma * P_pi @ v if np.max(np.abs(v_next - v)) < tol: return v_next, k + 1 v = v_next return v, max_iter两个函数算出来的是同一个向量,差别只在代价结构。闭式解一步到位但内存和算力开销大,迭代解慢但可以随时中断、随时看当前误差。这个"可中断"的性质在后面会变得关键:当模型未知、只能靠采样估计的时候,闭式解彻底失效,剩下的只有迭代这一条路,而迭代的形式恰好允许你把某一步的精确计算换成采样估计。
顺带说一句动作值。状态值回答"这个位置有多好",动作值 q_π(s,a) 回答"在这个位置做这个动作有多好"。两者的关系是 q_π(s,a) = Σ_{s'} p(s'|s,a)[r(s,a) + γv_π(s')],也就是把贝尔曼方程里对动作的那层求和拆掉。为什么需要它?因为选动作的时候要比较的是"做 a 和做 b 哪个好",而 v_π(s) 已经把动作平均掉了,没法用来做决策。这个区分在后面讲无模型方法时会直接决定算法能不能落地。
3. 贝尔曼最优方程:max 算子凭什么保证迭代一定收敛
3.1 从"评估一个策略"到"找一个最好的策略"
上一章的贝尔曼方程解决的是评估问题:给定 π,算出 v_π。但强化学习真正想要的是一个好策略。定义最优状态值 v*(s) = max_π v_π(s),即在所有可能的策略里挑出使该状态值最大的那个。这个定义有个隐患:max 是对无穷多个策略取的,怎么保证最大值真的取得到?
答案分两步。第一步,在所有策略里,一定存在一个确定性的、只依赖当前状态的策略不劣于任何其他策略,所以搜索空间可以从"所有策略"缩到"确定性策略",而确定性策略的数量是有限的 |A|^|S|,最大值必然存在。第二步,这个最优策略可以用贪心方式从 v* 直接读出来:
π*(a|s) = 1 当 a = argmax_a q*(s,a)这条性质是整个领域的基石。它的意思是:你不需要在策略空间里搜索,只要拿到最优值函数,策略是免费的。
3.2 贝尔曼最优方程:max 让线性变成了非线性
把贝尔曼方程里的"对动作求期望"换成"对动作取最大",就得到贝尔曼最优方程(BOE):
v(s) = max_a Σ_{s'} p(s'|s,a) [ r(s,a) + γ v(s') ]写成矩阵-向量形式是 v = max_a (r_a + γP_a v),这里的 max 是逐元素取的——对每个状态分别选出让它最大的那个动作。这个细节很容易被写错:有人会试图把它写成 v = max_π (r_π + γP_π v),形式上像,但那个 max 是对策略取的,而策略是动作上的分布,两个 max 的含义完全不同。
差别在哪?前一个 max 作用在向量上,得到的是逐状态的凸函数;后一个 max 作用在策略集合上,本质是线性规划。前者可以直接迭代,后者不行。书里强调这一点的用意是:BOE 之所以需要压缩映射定理,正是因为右端那个逐元素 max 破坏了线性性,闭式解那一套用不了了。
还有一个重要的观察:如果已知 v*,那么 q*(s,a) 可以直接算出来,最优动作就是让 q* 最大的那个。而当多个动作并列最优时,任何在这些动作上的分布都是最优策略——这解释了为什么实践中不同的随机种子会收敛到行为不同、但值相同的策略上。
3.3 压缩映射定理:γ 到底压住了什么
定义一个算子 f,它的输入是一个值向量,输出是 BOE 的右端:
f(v)(s) = max_a Σ_{s'} p(s'|s,a)[r(s,a) + γv(s')]如果能证明 f 是一个压缩映射,那么它就只有一个不动点,且从任意初值出发反复迭代 f 都会收敛到这个不动点。压缩性用无穷范数写出来是:
||f(v1) - f(v2)||∞ ≤ γ ||v1 - v2||∞推导只用两步。第一步,max 算子本身是 1-Lipschitz 的:两个向量逐元素取 max,差的最大值不会超过原向量差的最大值。第二步,转移概率对 s' 求和等于 1,所以 Σ_{s'} p(s'|s,a) 乘上任何不超过 δ 的量,结果也不超过 δ。两步合起来,γ 原封不动地保留下来,压缩系数就是 γ。
γ 在这里的角色变得很清晰:它不只是"折扣"这样一个含糊的调节旋钮,它直接就是收敛速率。γ = 0.9 意味着每迭代一次误差至少缩到 0.9 倍,γ = 0.999 则是每轮只缩千分之一。这就解释了那个常见的困惑:为什么把 γ 调大之后训练变得又慢又抖?因为你不只是改变了任务的目标(更看重远期),还改变了求解算法的收敛速度。
由压缩映射可以直接得到两个结论:不动点唯一(v*)、迭代误差满足 ||v_k - v*||∞ ≤ γ^k ||v_0 - v*||∞。后一个式子给了你一个可以写进代码的停止准则:当相邻两次迭代的差小于 (1-γ)/γ 乘以容差时,就能保证当前结果与真解的误差在容差之内。
3.4 值迭代与策略迭代:解同一个方程的两条路径
值迭代就是直接把 f 反复作用:
v_{k+1}(s) = max_a Σ_{s'} p(s'|s,a)[r(s,a) + γv_k(s')]它的本质是"一边评估一边改进"糅在一起做。每次迭代都同时完成了两步:用当前的 v 算出每个动作的 q,然后贪心地选最大值。因为 v_k 本身还不是真值,这个贪心是"短视"的,但压缩映射保证了短视的累积仍然指向正确方向。
策略迭代走的是另一条路,把评估和改进显式拆开:先完整地算出当前策略的 v_π(内层循环,可能需要迭代很多轮),再根据 v_π 贪心得到新策略(外层循环),然后重复。它的收敛性来自策略改进定理——贪心地基于 v_π 更新策略,得到的新策略一定不劣于旧策略,且如果不等,至少有一个状态的值严格提升。因为确定性策略的数量有限,这个过程必然在有限步内停在一个不再变化的位置,那个位置就是最优。
两条路的代价结构不一样:值迭代每轮很便宜但轮数多,策略迭代每轮很贵(内层要收敛)但外层轮数通常很少。书里给出的第三条路——截断策略迭代——把它们统一了:内层不跑到底,只跑固定 k 轮。k = 1 时退化成值迭代,k = ∞ 时就是策略迭代。这个统一视角在工程上很有用,它告诉你中间那些"评估跑几轮就换"的变体不是随意发明出来的,而是同一个谱系上的不同位置。
下面这段代码用同一个网格世界把三种算法并排实现,方便对照:
import numpy as np def value_iteration(P, r, gamma, tol=1e-8): """P: (S, A, S) 转移概率;r: (S, A) 即时奖励""" S, A, _ = P.shape v = np.zeros(S) while True: q = r + gamma * np.einsum('sas,s->sa', P, v) # 每个动作的 q v_next = q.max(axis=1) # 逐状态取 max if np.max(np.abs(v_next - v)) < tol: return v_next, q.argmax(axis=1) v = v_next def policy_evaluation(P_pi, r_pi, gamma, tol=1e-8): v = np.zeros(P_pi.shape[0]) while True: v_next = r_pi + gamma * P_pi @ v if np.max(np.abs(v_next - v)) < tol: return v_next v = v_next def policy_iteration(P, r, gamma, eval_iters=None): """eval_iters=None 表示内层跑到收敛(标准策略迭代) eval_iters=k 表示内层只跑 k 轮(截断策略迭代,k=1 即值迭代) """ S, A, _ = P.shape pi = np.zeros(S, dtype=int) while True: # 用当前策略展开成 P_pi 和 r_pi P_pi = P[np.arange(S), pi, :] r_pi = r[np.arange(S), pi] if eval_iters is None: v = policy_evaluation(P_pi, r_pi, gamma) else: v = np.zeros(S) for _ in range(eval_iters): v = r_pi + gamma * P_pi @ v # 策略改进:对每个状态选 q 最大的动作 q = r + gamma * np.einsum('sas,s->sa', P, v) new_pi = q.argmax(axis=1) if np.array_equal(new_pi, pi): return v, pi pi = new_pi第三行那个np.einsum是把"对 s' 求和"写成张量缩并,等价于对每个 (s,a) 做一次内积。如果你把它换成显式循环,会发现值迭代的每一轮和 BOE 的右端是一一对应的——代码结构就是方程结构,这不是巧合。eval_iters参数则把值迭代、策略迭代、截断策略迭代收进了同一个函数,改一个数字就能在三种算法之间切换。
4. 从有模型到无模型:蒙特卡洛和随机近似怎么用采样替掉期望
4.1 MC Basic:把期望换成算术平均
前面所有算法都默认你能拿到 p(s'|s,a)。现实里这个概率几乎永远拿不到——你没法知道机器人在某个位置执行某个动作后,精确地转移到哪个位姿的概率是多少。无模型方法要解决的就是这件事:把对模型的依赖全部换掉。
策略迭代里有两处用到模型。第一处是策略评估,算 v_π 时要用 P_π;第二处是策略改进,算 q_π 时要用 p(s'|s,a)。蒙特卡洛的做法很直接:既然 v 和 q 都是期望,那就采样,用样本均值去估计期望。从状态 s 出发按策略 π 跑完整条轨迹,把沿途的回报记下来,重复 N 次取平均,就得到 v_π(s) 的估计。动作值同理,从 (s,a) 出发跑 N 条轨迹取平均。
这一步替换带来的连锁反应值得细看。因为不再有模型,你没法从 v 推出 q(那个式子需要 p),所以 MC 方法直接估计 q 而不是 v。这不是设计上的偏好,是被逼出来的:策略改进必须比较动作,而比较动作只能用 q。
MC Basic 是最朴素的版本,它的缺点也很明显:每条轨迹只用来更新一个 (s,a),样本利用率极低;而且它要求每个 (s,a) 都能被访问到足够多次,否则那些没被采到的组合估计值一直是零。这两个问题分别催生了后面的两个补丁。
4.2 Exploring Starts 与 ε-greedy:探索的两个补丁
第一个补丁叫探索性起点。既然问题是"有些 (s,a) 采不到",那就强制每条轨迹都从不同的 (s,a) 出发,保证覆盖。这个假设在数学上很干净,在工程上基本不可行——你没法让真实机器人从任意位姿开始,也没法让游戏从任意中间帧启动。所以它更多是一个理论上的过渡形态。
第二个补丁是 ε-greedy 策略,它把"强制探索"从环境侧挪到了策略侧。策略不再总是选当前最优动作,而是以 1-ε 的概率选贪心动作,以 ε 的概率在全部动作上均匀随机:
π(a|s) = 1 - ε + ε/|A| 如果 a 是贪心动作 π(a|s) = ε/|A| 其他动作这个式子的关键性质是:每个动作的概率都严格大于零。只要 ε > 0,任意 (s,a) 在无限长的轨迹里都会被访问到无穷多次,覆盖性就有了保证,不再需要探索性起点。同时,ε-greedy 关于 q 是单调的——如果某个动作的 q 变大了,它在 ε-greedy 下的概率也随之变大,这个性质保证了策略改进的正确性不会因为引入随机而丢失。
ε 怎么选是个老问题。ε 太大,策略长期在随机游走,学到的 q 一直带着探索噪声;ε 太小,某些动作几乎采不到,估计偏差会固化下来。常见的做法是前期大后期小,但衰减节奏要和任务长度匹配——如果衰减得比学习速度快太多,等于没探索过就锁死了策略。
4.3 Robbins-Monro:步长为什么必须满足两个条件
从"平均 N 条完整轨迹"过渡到"每来一个样本就更新一次",中间需要一套理论支撑,这就是随机近似。Robbins-Monro 算法处理的是这样一类问题:你要求方程 g(w) = 0 的根,但只能拿到 g 的带噪观测。它的迭代形式是:
w_{k+1} = w_k - α_k * ĝ(w_k)收敛性依赖步长序列的两个条件:
Σ α_k = ∞ 且 Σ α_k² < ∞第一个条件保证步长总和足够大,不管起点离根多远都能走到;第二个条件保证步长平方和有限,后期的扰动不会累积成持续的震荡。1/k 是满足这两个条件的经典选择,常数步长则不满足第二条——用常数步长的算法不会精确收敛到不动点,而是在附近一个有界的邻域内来回跳。
这个结论解释了很多实践中的现象。为什么学习率衰减能提升最终精度?因为常数步长下误差有一个由 α 决定的下界,衰减才能把这个下界压下去。为什么有些实现用 1/k 反而学得很慢?因为早期步长掉得太快,还没走到大致正确的区域就已经不怎么动了。折中方案是分段常数或者 1/k^0.6 这类介于两者之间的形式,理论保证弱一点,实践中更稳。
把随机近似用在均值估计上,就得到"新估计 ← 旧估计 + α(新样本 - 旧估计)"这个形式。这个式子看起来平平无奇,但它是后面所有 TD 方法的骨架:把"新样本"换成"回报",就是蒙特卡洛;把"新样本"换成"一步奖励加下一状态估计",就是时序差分。
4.4 从 SGD 到 TD:bootstrap 换来了什么,又付出了什么
随机梯度下降是随机近似在优化上的应用。给定目标函数 J(w) = E[(v_π(S) - v̂(S,w))²],它的梯度需要知道 v_π 这个真值,而你恰恰不知道。用采样替代之后,就得到标准的 SGD 更新。
TD 学习把这个思路往前推了一步。它不去估计"从 s 出发的完整回报",而是用一步奖励加上下一状态的当前估计来构造目标:
v(s_t) ← v(s_t) + α [ r_{t+1} + γ v(s_{t+1}) - v(s_t) ]方括号里那项叫 TD error,是"实际看到的"减去"原本以为的"。这个更新的精妙之处在于:目标 r + γv(s_{t+1}) 里的 v 是你自己的估计,用它来更新它自己,这个操作叫 bootstrap。
bootstrap 带来的好处是方差大幅下降。蒙特卡洛的目标是一整条轨迹的回报,轨迹越长随机性累积越多,方差越大;TD 的目标只有一步随机奖励,方差小得多。代价是偏差:因为 v(s_{t+1}) 本身还不准,目标就是有偏的。所以 MC 是无偏但高方差,TD 是有偏但低方差,这个取舍贯穿了整个领域。
书里用一张统一视角把 MC 和 TD 串起来:如果把更新目标写成"若干步真实奖励加若干步后的估计值",那么步数等于轨迹长度时是 MC,等于 1 时是 TD(0),中间就是 n-step TD。它们不是互不相干的算法,而是同一个谱系上取不同截断点。
把 TD 从状态值推广到动作值,有两个分支,区别只在更新目标里的"下一状态值"怎么取:
def sarsa_update(q, s, a, r, s_next, a_next, alpha, gamma, done): # on-policy:用实际执行的下一个动作 a_next 来构造目标 target = r if done else r + gamma * q[s_next, a_next] q[s, a] += alpha * (target - q[s, a]) def qlearning_update(q, s, a, r, s_next, alpha, gamma, done): # off-policy:不管实际执行什么,都假设下一步走最优动作 target = r if done else r + gamma * q[s_next].max() q[s, a] += alpha * (target - q[s, a])两者的差别只有一行,但性质完全不同。Sarsa 用实际执行的动作 a_next,所以它在评估的就是当前行为策略,属于 on-policy;Q-learning 用 max 替换 a_next,它在评估的是最优策略,与行为策略无关,属于 off-policy。off-policy 的好处是你可以用一个探索性的策略采样、同时学一个贪心策略,样本利用率更高;坏处是行为策略和目标任务之间的分布差异会带来偏差,这个偏差在表格情形下不影响收敛,一旦加上函数逼近就可能炸。
5. 排查手册:五个让"看起来在学"其实没在学的坑
强化学习的调试难在它不会报错。代码跑得通、损失在下降、曲线也在动,但学到的策略就是不对。下面这五条是我在实际项目里反复撞到的,按"现象 → 原因 → 解决"整理。
5.1 回报不折扣,值函数直接发散
现象:训练一开始值函数数值就飞快增长,几轮之后出现 inf 或 nan,或者曲线在某次更新后突然跳到离谱的量级。
原因:γ 被设成了 1,或者任务本身没有终止状态。书里的贝尔曼方程收敛性依赖 γ < 1 这个前提,一旦取 1,级数可能不收敛,闭式解里的 (I - γP)^{-1} 也失去意义。更隐蔽的情况是代码里写了 γ = 0.99,但终止判断有 bug,轨迹永远不结束,回报累加到后来数值精度扛不住。
解决:先确认环境真的有吸收态,并且终止信号传到了更新公式里——终止那一步的目标必须是 r 本身,不能是 r + γv(s')。如果任务确实没有自然终止(比如持续运行的调度任务),就定义平均奖励形式,或者人为加一个上限并显式截断。我一般会在训练前先打印一条完整轨迹的回报和它的长度,看一眼量级是否合理。
5.2 终止状态的值没钉成 0,整张表整体漂移
现象:策略看起来是合理的,但所有状态的值都比预期高出一个固定的偏移量,而且这个偏移随训练轮数缓慢增长。
原因:终止状态被当成普通状态参与了自举更新。终止状态之后没有奖励,它的值必须永远是 0;如果代码里把它也塞进了状态表并且允许被更新,那么 v(terminal) 会被 TD error 一点点推高,再通过自举把这个偏移传播回所有能到达终止的状态。
解决:在更新前显式判断 done,done 为真时目标只取 r。状态表初始化时给终止状态一个固定值,并且跳过对它的写操作。这一条在表格情形下表现为整体偏移,在函数逼近情形下表现为收敛到一个偏高的平台,都不是很好定位,所以最好在写第一个版本时就把它处理干净。
5.3 ε 衰减和探索需求完全脱节
现象:训练前期学得很快,中途突然卡住,最终策略总是绕开某片区域,或者在某几个状态下反复横跳。
原因:ε 衰减得太快,或者衰减的步数和训练总步数不匹配。策略过早地变成了近似贪心,那些还没被充分采样的 (s,a) 就一直停留在早期的错误估计上。ε-greedy 的收敛性依赖"每个动作都被无穷多次采到",这条前提被破坏,估计偏差就固化成了策略错误。
解决:把 ε 的衰减挂在实际交互步数上而不是 episode 数上,并确保每个 (s,a) 的访问次数没有严重的长尾。排查时可以统计一下访问频次分布,如果存在大量访问次数为个位数的状态动作对,说明探索还不够。另外要注意评估时的 ε 应该设为 0,用纯贪心策略去测,否则测出来的性能被随机动作拉低,容易被误判成学得不好。
5.4 离策略更新漏了重要性采样比
现象:用 Q-learning 或者带经验回放的实现时,值函数在训练一段时间后开始缓慢发散,或者收敛到一个明显次优的固定点。
原因:off-policy 的更新假设了目标策略和行为策略之间的分布关系,在表格情形下它恰好能容忍这个差异,但一旦进入函数逼近或者使用了重要性采样却没做截断,方差会急剧放大。反过来,如果算法本身要求重要性采样(比如某些 off-policy 策略梯度方法),而代码里直接省掉了那个比值,算出来的梯度方向就是错的。
解决:先明确你的算法是 on-policy 还是 off-policy,再决定要不要重要性采样。需要的话,比值必须用截断(clipping)控制方差,否则单条长轨迹的连乘会让比值指数爆炸。经验回放缓冲区的数据分布也要检查,如果早期用随机策略采的数据在缓冲区里占了大部分,实际训练用的分布和目标策略差得很远,学习会变得很不稳定。
5.5 函数逼近下把 TD 当成真梯度用
现象:表格实现完全正常,换成线性函数或者神经网络之后,权重越来越大,最后发散。
原因:函数逼近下,TD 更新用的目标是 r + γv̂(s',w),这个目标本身也依赖 w。真正的梯度要把它一起求导,但半梯度 TD 直接把它当常数,所以更新的方向不是目标函数的真梯度。当状态被反复访问、特征之间高度相关、又叠加 off-policy 的分布偏移时,这个近似误差会累积成发散——这就是所谓的"死亡三角":函数逼近 + 自举 + 离策略,三个凑齐就有风险。
解决:优先从特征设计入手,让状态表示之间的相关性降下来;其次降低学习率、缩小网络;再不行就引入目标网络把自举的目标固定几个步长,或者用经验回放打散样本的时间相关性。诊断的时候可以同时监控权重范数和 TD error 的绝对值,如果权重范数单调增长而 TD error 不降,基本就是这个问题。
6. 设计取舍:为什么这本书把十章画成了一张二维地图
书稿开篇有一张图,把十章摊在两个维度上:横轴是"有模型 → 无模型",纵轴是"表格表示 → 函数表示"。第 2 到 4 章在有模型、表格的象限里,讲贝尔曼方程、贝尔曼最优方程、值迭代和策略迭代;第 5 到 7 章进入无模型、表格,讲蒙特卡洛、随机近似、时序差分;第 8 章跨到函数逼近,第 9、10 章在函数表示上做策略梯度和演员-评论家。
这个排布顺序不是随手排的,它背后有一个很务实的设计取向:先让你在能算对的地方算对,再去处理算不对的地方。如果一上来就讲无模型的 Q-learning,你只知道更新公式长什么样,但不知道它在逼近什么;而先在有模型的情形下把贝尔曼方程解出来,无模型方法的每一次采样估计都能和一个已知的真值对照,偏差出现在哪一步一目了然。这就是为什么书里所有算法都在同一个网格世界上跑——它给了你一个可以手算的参考答案。
另一个取舍是数学深度。书稿把概率论和线性代数的基础放进附录,正文里那些偏测度论的部分用灰底框标出,可以跳过。这个做法承认了一件事:读这本书的人目标不一样,有人想要完整的收敛性证明,有人只想知道这个算法在解哪个方程。把两部分显式分开,比模糊地折中要好。
如果你要验证自己是不是真读懂了,我建议的检验方式是:合上书,自己在网格世界上把值迭代、MC Basic、TD(0)、Q-learning 各实现一遍,然后逐轮打印 v 或 q,和手算结果对照。四个都对齐了,说明你理解的是方程而不是公式。我自己的习惯是每换一个新环境,先跑一遍值迭代拿到"标准答案",再拿它去校准后面所有采样算法的结果,这样一旦曲线不对,能立刻分清是算法理解错了还是实现写错了。
从那以后我每次上手一个新的强化学习算法,都会强制先在小规模、可穷举的环境上把它跑通,确认它收敛到的手算解一致,再迁移到大环境上。这个习惯救过我很多次——那些后来在大任务上炸掉的 bug,基本都能在小格子上提前暴露。希望帮到你。
本文还有配套的精品资源,点击获取