ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

强化学习动态规划:策略迭代与值迭代原理及实现

强化学习动态规划:策略迭代与值迭代原理及实现

1. 从“查表”到“迭代”:动态规划到底在强化学习里扮演什么角色

很多人第一次接触强化学习,脑子里浮现的都是神经网络、梯度下降、各种复杂的策略优化。但如果你把时间倒回这门学科刚起步的年代,会发现最核心的求解工具其实非常朴素——动态规划。它不依赖任何函数逼近,也不需要采样,只要你能把问题写成一个马尔可夫决策过程,并且状态空间和动作空间都足够小,它就能给你算出精确的最优策略。

我刚开始学这块的时候也有个误解,觉得动态规划就是个“老古董”,实际项目里根本用不上。后来做了一些小规模的调度和路径规划问题才发现,动态规划给出的解往往是最好的基准线。你后面用Q-learning也好,用策略梯度也好,最终效果好不好,拿动态规划的结果一对比就清楚了。它就像一把尺子,量的是你的算法到底偏离最优解有多远。

这篇文章我打算把动态规划在强化学习里的两个核心算法——策略迭代和值迭代——从头到尾拆一遍。不是那种教科书式的公式罗列,而是把我自己推导时卡住的地方、写代码时踩过的坑、以及为什么这么设计的原因都讲清楚。适合已经了解MDP基本概念、但还没真正动手实现过动态规划的读者。如果你连状态、动作、回报这些词都还陌生,建议先补一下基础再来看,不然容易卡在中间。

先说结论:动态规划的核心思想就一句话——用贝尔曼方程把一个大问题拆成一层层的小问题,然后反复迭代直到收敛。听起来简单,但魔鬼全在细节里。

2. 贝尔曼方程:动态规划能跑起来的唯一支点

2.1 为什么必须先把贝尔曼方程吃透

动态规划之所以能用在强化学习上,根本原因在于价值函数满足贝尔曼方程。这个方程的本质是一个递归关系:当前状态的价值,等于你采取某个动作后获得的即时回报,加上折扣后下一个状态的价值期望。

用大白话说就是:一个位置好不好,取决于从这儿出发能拿到多少即时好处,以及下一步能走到多好的位置。这个递归结构让“全局最优”这个问题变成了“每一步都做对”的问题。

贝尔曼方程有两种形式,分别对应状态价值和动作价值:

  • 状态价值函数 V(s):从状态s出发,按照策略π行动,能获得的期望回报。
  • 动作价值函数 Q(s,a):从状态s出发,先执行动作a,之后按照策略π行动,能获得的期望回报。

这两个函数之间的关系是理解后面所有算法的关键。V(s)等于在s下对所有动作的Q(s,a)按策略概率加权求和;而Q(s,a)等于即时回报加上折扣后所有可能下一状态的V的期望。

我当初学的时候最大的困惑是:这两个函数到底哪个先算?后来想明白了——它们互相依赖,所以动态规划的做法就是先随便猜一个,然后反复迭代让它们互相逼近,直到不再变化。

2.2 贝尔曼最优方程和普通贝尔曼方程的区别

这里有个容易混淆的点。普通贝尔曼方程描述的是“给定策略π下的价值”,而贝尔曼最优方程描述的是“在所有可能策略中,最优的那个价值是多少”。

区别体现在动作选择上:普通贝尔曼方程对动作做加权平均(按π的概率),而最优贝尔曼方程直接取最大值。这个“取最大”的操作就是贪心策略的来源。

注意:取最大值这个操作看起来简单,但它引入了一个非线性因素。这也是为什么动态规划需要反复迭代——因为max操作让方程不再有闭式解,只能一步步逼近。

2.3 折扣因子γ到底在调节什么

折扣因子γ(0到1之间)控制的是未来回报的权重。γ越接近0,智能体越短视,只看眼前利益;γ越接近1,智能体越有远见,愿意为长期回报牺牲即时收益。

但γ还有一个容易被忽略的作用:它决定了价值函数的数值范围。如果即时回报的最大绝对值是R_max,那么任何状态的价值都不会超过R_max/(1-γ)。这个性质在数值计算时很有用,可以帮你判断迭代是否已经收敛到合理范围。

我在实际调参时发现,γ设成0.9到0.99之间通常比较合理。太低会导致策略过于短视,太高则会让价值函数收敛变慢,而且数值容易变得很大。具体选多少,要看你问题的“视野”有多长——如果完成一个任务需要几十步,γ至少得0.95以上。

3. 策略迭代:先评估再改进,循环往复

3.1 策略评估的完整推导过程

策略迭代的第一步是策略评估:给定一个策略π,算出这个策略下每个状态的价值V_π(s)。

做法很简单,就是把贝尔曼方程当成一个迭代更新公式:

V_{k+1}(s) = Σ_a π(a|s) [ R(s,a) + γ Σ_{s'} P(s'|s,a) V_k(s') ]

这个公式的意思是:新的价值估计等于“按当前策略选动作,每个动作的即时回报加上折扣后下一状态旧价值的期望”的加权和。

实际操作时,我们初始化V(s)=0(或者任意值),然后反复用这个公式更新所有状态的价值,直到V的变化量小于某个阈值(比如1e-6)。

这里有个细节:更新顺序会影响收敛速度。如果按状态编号顺序更新,后面的状态会用上前面刚更新的值,这叫“异步更新”,通常比同步更新(所有状态用旧值算新值)收敛更快。我在代码里一般直接用异步更新,省事又高效。

3.2 策略改进:贪心为什么能保证不退化

策略评估完成后,我们有了每个状态的V值。接下来做策略改进:对每个状态,选择那个能让Q值最大的动作。

新策略π'(s) = argmax_a [ R(s,a) + γ Σ_{s'} P(s'|s,a) V(s') ]

这里有个重要的理论保证:新策略一定不比旧策略差。也就是说,对任意状态s,V_{π'}(s) ≥ V_π(s)。这个叫策略改进定理。

为什么?因为如果你在每个状态都选了当前V下最好的动作,那至少不会比原来按π选动作差。如果所有状态都选不出更好的动作了,那说明π已经是最优策略。

我当初对这个定理的理解卡了很久,后来用反证法想通了:假设存在某个状态s,新策略的价值比旧策略低,那说明在s下按π选的动作比贪心动作更好,但这与贪心动作的定义矛盾。所以新策略不可能更差。

3.3 策略迭代的收敛条件和终止判断

策略迭代的流程是:评估→改进→再评估→再改进,直到策略不再变化。

终止条件就是:策略改进后,所有状态的动作选择都和上一轮一样。这时候策略已经稳定,再迭代下去也不会变了。

但实际写代码时,判断“策略不再变化”比判断“价值不再变化”更直接。因为价值可能因为数值精度问题一直在微小波动,但策略早就稳定了。

提示:策略评估不需要做到完全收敛。实践中,评估迭代几次就够了,因为策略改进会纠正评估的误差。这种“不精确评估”的策略迭代通常收敛更快。

3.4 一个手算例子:4x4网格世界

假设有一个4x4的网格,左上角是起点,右下角是终点,每走一步回报-1,走到终点回报0,γ=1。动作有上下左右四个方向,撞墙则留在原地。

用策略迭代求解:初始策略可以随便设,比如所有状态都往右走。第一轮评估后,靠近终点的状态价值会变高(因为离终点近,负回报少)。然后策略改进会让这些状态指向终点方向。几轮之后,所有状态都会指向一条最短路径。

这个例子虽然简单,但能帮你直观感受动态规划的工作方式。我建议你拿张纸手算一遍,比看十遍公式都管用。

4. 值迭代:把评估和改进揉成一步

4.1 值迭代和策略迭代的本质区别

值迭代的思路更直接:我不维护显式的策略,直接迭代最优价值函数。

更新公式是:

V_{k+1}(s) = max_a [ R(s,a) + γ Σ_{s'} P(s'|s,a) V_k(s') ]

对比策略迭代的评估公式,你会发现值迭代把“按π加权求和”换成了“取最大值”。这一步就把策略评估和策略改进合并了——因为取最大值本身就意味着在改进策略。

值迭代每轮只做一次更新,而策略迭代每轮要做多次评估加一次改进。所以值迭代通常更快,尤其是状态空间大的时候。

但值迭代也有代价:它不显式维护策略,所以中间过程你看不到策略长什么样。如果你需要观察策略的演化过程,策略迭代更直观。

4.2 值迭代的收敛性怎么保证

值迭代的收敛性依赖于贝尔曼最优算子是压缩映射这个性质。简单说,每次迭代后,价值函数和最优价值函数的差距会缩小到原来的γ倍。因为γ<1,所以反复迭代一定会收敛。

这个压缩性质也给出了收敛速度的估计:如果初始误差是E,迭代k轮后误差不超过γ^k * E。所以γ越小,收敛越快;γ越接近1,收敛越慢。

我在实际跑值迭代时,通常设最大迭代次数1000,阈值1e-6。大部分小规模问题几百轮就收敛了。如果超过1000轮还没收敛,要么是γ太接近1,要么是问题本身有环(比如某些状态之间可以无限循环),需要检查模型定义。

4.3 同步更新和异步更新的选择

值迭代也有同步和异步两种更新方式。

  • 同步更新:每轮用上一轮的V值计算所有状态的新V值,然后统一替换。实现简单,但需要额外空间存旧值。
  • 异步更新:按顺序遍历状态,每个状态直接用最新的V值更新。收敛更快,空间更省。

我一般用异步更新,因为代码写起来更简洁,而且实测收敛速度确实快不少。但异步更新的收敛性证明比同步复杂,不过实践中基本没问题。

注意:异步更新时,遍历顺序会影响结果。如果按状态编号顺序遍历,可能会导致某些状态被“过度更新”。一个常见的做法是随机打乱顺序,或者按某种优先级排序(比如优先更新价值变化大的状态)。

4.4 值迭代的代码实现框架

下面是一个值迭代的伪代码框架,用Python风格写:

def value_iteration(states, actions, transition, reward, gamma, theta=1e-6): V = {s: 0 for s in states} while True: delta = 0 for s in states: v_old = V[s] v_new = max( sum(transition(s, a, s_next) * (reward(s, a, s_next) + gamma * V[s_next]) for s_next in states) for a in actions ) V[s] = v_new delta = max(delta, abs(v_old - v_new)) if delta < theta: break # 提取最优策略 policy = {} for s in states: policy[s] = max(actions, key=lambda a: sum( transition(s, a, s_next) * (reward(s, a, s_next) + gamma * V[s_next]) for s_next in states )) return V, policy

这段代码的核心就是两层循环:外层判断收敛,内层遍历所有状态做max操作。提取策略时再遍历一次,选Q值最大的动作。

实际写的时候要注意:transition和reward函数的设计直接影响性能。如果状态多,最好用矩阵或张量预存转移概率和回报,避免每次循环都查字典。

5. 动态规划的边界:它能做什么,不能做什么

5.1 动态规划的三个硬性前提

动态规划在强化学习里能用的前提非常苛刻:

  1. 模型完全已知:你必须知道转移概率P(s'|s,a)和回报函数R(s,a,s')。如果不知道,动态规划直接没法用。
  2. 状态空间有限且不大:因为要对所有状态做遍历,状态数太多(比如连续状态)就不可行。
  3. 马尔可夫性质成立:下一状态只依赖当前状态和动作,与历史无关。

这三个条件缺一个,动态规划就用不了。这也是为什么实际项目里更多用采样方法(如Q-learning)——因为采样方法不需要模型,也能处理连续状态。

但动态规划的价值在于:它给出了理论上的最优解。你可以用它来验证采样方法的收敛性,或者在小规模问题上做基准测试。

5.2 维数灾难:状态爆炸怎么破

动态规划最大的敌人是维数灾难。假设你有10个维度,每个维度10个取值,状态空间就是10^10,根本存不下。

解决思路有几条:

  • 状态聚合:把相似状态合并,减少状态数。但会损失精度。
  • 函数逼近:用参数化函数表示价值函数,而不是查表。这就是深度强化学习的思路。
  • 采样:不遍历所有状态,只采样一部分。这就是蒙特卡洛和时序差分方法。

动态规划本身没法解决维数灾难,但它是理解这些高级方法的基石。你只有先搞懂精确解怎么求,才能理解近似解在近似什么。

5.3 动态规划和蒙特卡洛、时序差分的关系

这三者的关系可以用一句话概括:动态规划用模型和自举,蒙特卡洛用采样和完整回报,时序差分用采样和自举。

  • 动态规划:需要模型,用下一状态的估计值更新当前状态(自举)。
  • 蒙特卡洛:不需要模型,用完整回合的实际回报更新。
  • 时序差分:不需要模型,用下一状态的估计值更新(自举)。

时序差分可以看成动态规划和蒙特卡洛的结合体。它既有动态规划的自举特性,又有蒙特卡洛的无模型特性。理解了动态规划,时序差分就很好理解了。

我在学习路径上建议:先手推动态规划,再写代码实现,然后对比蒙特卡洛和时序差分。这样一路下来,整个强化学习的脉络就清晰了。

6. 实操中容易踩的坑和调试技巧

6.1 收敛阈值设多少才合适

阈值设太大,结果不精确;设太小,迭代次数暴增。我的经验是:

  • 如果回报量级在1左右,阈值设1e-6到1e-8比较合适。
  • 如果回报量级很大(比如几千),阈值要按比例放大。
  • 可以先跑一次看delta的变化曲线,找一个明显变平的拐点。

另外,不要只看delta,还要看策略是否稳定。有时候delta已经很小了,但策略还在微调。这时候多跑几轮更保险。

6.2 价值函数初始化的影响

理论上,动态规划对初始值不敏感,因为压缩映射保证收敛到唯一不动点。但实践中,好的初始化能显著减少迭代次数。

一个常用技巧是:用问题的启发式信息初始化V。比如在网格世界里,可以用到终点的曼哈顿距离的负值作为初始V。这样一开始就离最优解不远,收敛更快。

如果没有任何先验信息,全零初始化就行。不要用随机初始化,因为随机值可能让前几轮迭代波动很大。

6.3 策略迭代中评估轮数怎么定

策略迭代的评估步骤不需要做到完全收敛。我通常设一个较小的最大迭代次数(比如10到20轮),或者用较松的阈值(比如1e-3)。

原因很简单:评估不精确没关系,策略改进会纠正。而且下一轮评估会基于改进后的策略重新算,误差不会累积。

但要注意:如果评估轮数太少,策略改进可能选错动作。一个折中方案是:前几轮策略迭代用较少的评估轮数,后面逐渐增加。

6.4 怎么验证动态规划结果的正确性

验证方法有几种:

  • 小规模暴力枚举:如果状态和动作很少,可以枚举所有策略,找最优的。对比动态规划结果。
  • 策略稳定性检查:最优策略下,每个状态的动作是否都是贪心动作。如果不是,说明还没收敛。
  • 贝尔曼误差检查:计算每个状态的贝尔曼误差(左边V(s)减去右边max Q(s,a)),如果都接近零,说明收敛。

我一般用第二种和第三种结合。先看策略是否稳定,再看贝尔曼误差是否够小。两个都满足,基本就没问题了。

7. 从动态规划到深度强化学习的自然延伸

动态规划虽然在实际项目中直接用的场景不多,但它的思想贯穿了整个强化学习。你后面遇到的几乎所有算法,都能在动态规划里找到影子。

比如DQN里的目标网络,本质上是在模仿动态规划的自举更新,只是用神经网络代替了查表。再比如策略梯度里的优势函数,其实就是在估计动态规划里的Q值和V值之差。

我个人的学习体会是:动态规划是强化学习的“内功”。你不需要在实际项目里天天用它,但如果不理解它,后面学什么都是浮在表面。尤其是当你调参调不动、算法不收敛的时候,回到动态规划想一想,往往能找到问题的根源。

如果你现在正在学强化学习,我建议你至少手写一遍策略迭代和值迭代的代码,在一个小规模问题上跑通。这个过程花不了太多时间,但能帮你建立非常扎实的直觉。后面再学Q-learning、SARSA、DQN,你会发现很多东西都是水到渠成的。

最后分享一个我常用的调试技巧:当你的算法不收敛时,先在一个极小的状态空间(比如3个状态、2个动作)上跑动态规划,看看最优解长什么样。然后让你的算法在同样的问题上跑,对比结果。如果动态规划能收敛而你的算法不能,问题一定出在算法实现或超参上,而不是问题本身。这个对比法帮我省了很多排查时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进