强化学习基石:值迭代与策略迭代算法深度解析 很多人刚开始接触强化学习时容易被“智能体跟环境不断试错”这个叙事带偏觉得强化学习就是从零开始瞎探索。但实际上强化学习里有一整套完全不一样的思路当环境模型——也就是状态转移概率和奖励函数——你已经知道得明明白白的时候根本不需要采样试错只需要解方程。而值迭代算法和策略迭代算法就是解决这类已知模型问题的两大经典武器。先把前提说清楚我默认你已经知道MDP马尔可夫决策过程的五元组明白状态、动作、奖励、转移概率和折扣因子gamma分别是什么意思。如果你还不熟悉建议先把贝尔曼方程的基础补上因为今天要讲的这两个算法本质上就是贝尔曼方程在两种不同求解路径上的具体展开。这篇博客写给两类读者一是刚学强化学习、理论知识停留在概念层的初学者希望帮你把公式和代码对应上二是已经有了点基础、正在写Demo验证理解的实践者希望我踩过的坑能帮你省下几个晚上。1. 为什么说动态规划方法才是强化学习的“地基”先泼一盆冷水很多入门的课程和教程为了展示强化学习的酷炫一上来就甩出Q-learning或DQN好像强化学习非得连个神经网络才能跑起来。但有一句在强化学习圈子里流传很广的话——无模型算法的每一个关键设计几乎都能在经典动态规划方法里找到对应的原型。这背后的道理很简单。动态规划类方法处理的核心问题是“规划”不是“学习”。它假设你有一张完整的MDP地图知道从状态 s 执行动作 a 会以多大的概率到 s、拿到多少奖励。在这种假设下寻找最优策略就是一个纯粹的数学迭代问题。我们可以把问题拆成两步看第一步给定一个策略算出这个策略下每个状态的价值是多少这叫“策略评估”。第二步根据评估出来的价值改进策略让每个状态都选择当前看起来最好的动作这叫“策略改进”。策略迭代就是这两个步骤不断交替直到策略不再变化。值迭代则是把这两步揉成一整步直接基于贝尔曼最优方程原地迭代。你可能会疑惑现在谁还有完整的MDP模型啊大多数真实场景确实没有但有完整模型的场景反而更多——比如棋盘游戏、导航地图、下棋引擎以及很多运筹优化问题。就算没有完整模型搞清楚这两个算法也能帮你彻底想明白Q-learning为什么用maxActor-Critic为什么要有两套网络。它们是理解整个强化学习演进的坐标原点没有之一。1.1 一个容易混淆的概念规划与学习很多人把“规划”和“学习”混着用但严格来说这是一条清晰的分界线规划planning给定MDP模型不和环境交互直接通过动态规划、搜索或线性规划求解最优策略。学习learning不知道模型只能通过实际采样获取经验再从中估计价值函数或直接优化策略。值迭代和策略迭代都属于第一类。你不需要派一个智能体去跑地图只需要在纸上、代码里反复迭代价值表最优策略就浮出来了。我当年学到这里非常震撼因为这种“不开一枪就把战争赢了”的感觉和前面学的蒙特卡洛完全是两种画风。你掌握了这类方法再回头去看DQN的target network、策略梯度的baseline都会觉得似曾相识因为源头上全都是这两套迭代机制。1.2 两个算法放在一起讲天生一对这两个算法经常被并列拿出来讲因为它们解决同一个问题求解MDP的最优策略。但求解路径完全不同后面我会详细展开。先记住一句话值迭代解的是贝尔曼最优方程一步步把价值函数逼近到最优。策略迭代解的是贝尔曼期望方程反复评估-改进让策略往回跳跃式前进。理解了这一点你会自然地看到二者的优势和劣势也理解了为什么实际科研中有人偏好值迭代有人偏好策略迭代。2. 收敛性从哪来贝尔曼算子的“压缩”魔术在写代码之前我想先带你花五分钟搞清楚一件极其重要的底层问题为什么这两个算法反反复复迭代就一定会收敛为什么要确保这点因为如果不理解收敛性你写代码时根本不知道怎么判断“到底该停在哪一轮”。这两个算法都依赖于一个关键的工具——贝尔曼算子。你可以把它理解成一个“更新规则发生器”它接收一个价值函数输出一个新的价值函数。如果这个算子是压缩的那么反复应用它价值函数一定会逼近某个不动点这个不动点正是我们要求的最优价值函数。用大白话说压缩映射的性质是这样的经过一次算子操作之后新函数和旧函数之间的距离不会变大反而会缩小到一个固定的比例。这就好比你手里有一把刻度不怎么准的尺子每次量完都更靠近一个常数最终稳定下来。这个比例恰好是折扣因子 gamma。所以为什么 gamma 必须严格小于1因为在收敛性证明里它是至关重要的一环。2.1 贝尔曼最优算子的压缩性我尽量不堆数学用一个简单例子说明。你在4x4网格世界的每个格子都有一个价值估计值值迭代的更新公式长这样V_new(s) max_a [ r(s,a) gamma * V(s) ]这个公式里藏着一个非常强的性质从中取出两个不同的价值函数 V 和 U那么对所有状态 s它们的最大差值的上界会被压缩成一个 gamma 倍的旧差值上界。自后一步的 diff一定比前一步的 diff 小。这就是为什么即使从一个全0的价值表开始迭代下去也一定会逼近最优价值函数而不是原地打转。我第一次看完这个证明的时候有种恍然大悟的感觉。原来强化学习里那么多看起来很“玄”的收敛定理落到这两个经典算法上就是贝尔曼算子的压缩性加上不动点定理。这个洞察后来在我实现各种改进算法时帮了大忙——只要你能证明你的新算子还是压缩的那你就知道引入它不会破坏收敛性。2.2 为什么策略迭代的收敛是“跳跃式”的策略迭代没有像值迭代那样直接用压缩映射一次到底而是分成了两段。它先对当前策略做价值评估这一步其实也是压缩迭代——反复用期望方程更新价值速度慢一些但稳定然后它做一个贪婪改进把所有状态的动作换成当前价值下最有利的那个。这里有个非常关键的结论我必须强调如果新策略里某一个状态的动作已经被换成更优动作那么新策略的每个状态价值都不会比旧策略更差。这就是所谓的“策略改进定理”。它保证每一次策略改进都不会退步要么保持原策略要么想办法变得更好。在一个有限状态、有限动作的MDP里策略总数是有限的所以这个“单调提升”的过程最终会在某个策略上停下来。换句话说策略迭代不是一步步爬向山峰而是一次次做局部最优决策从一片小高地跳到另一片更高的高地最后站上山顶。如果你在代码里观察过策略的变化会看到策略表经常整体变化很快这正是因为它每一轮都在做全局贪心带着强烈的“逼近”意味。3. 值迭代核心实现用4x4网格世界快速跑通理论说了不少现在动手。我选的实验场景是一个非常经典的网格世界4x4每个格子是一个状态坐标编号从0到15右下角格子是终点。所有正常移动的奖励都是-1终点转移的奖励是0。这样智能体为了拿到最大的累积奖励必须用最短的步数走到终点。为什么用这个环境因为它小到你可以在几毫秒内跑完所有状态又大到你肉眼能看出最优策略是什么样方便验证你的代码对不对。为了让事情更直观我给这部分代码用了Go语言因为Go的数组遍历逻辑清晰而且后面如果要接入其他工程场景也更方便。3.1 环境定义与转移逻辑先定义环境。这是一个确定性转移的环境也就是说你朝某个方向走就一定会走到相邻格子除非出界出界时位置不变。package main import ( fmt math ) const ( rows 4 cols 4 gamma 0.9 theta 1e-6 actUp 0 actDown 1 actLeft 2 actRight 3 ) var actions []int{actUp, actDown, actLeft, actRight} func terminal(s int) bool { return s rows*cols-1 } func step(s, a int) (int, float64) { r, c : s/cols, s%cols switch a { case actUp: if r 0 { s - cols } case actDown: if r rows-1 { s cols } case actLeft: if c 0 { s-- } case actRight: if c cols-1 { s } } if terminal(s) { return s, 0 } return s, -1 }这段代码里我做了两个关键决定终点的奖励返回0其他全返回-1。这样设计的好处是最优策略会很自然地趋向“尽快走到终点”不会为了让奖励为正而在原地打转。这点比你随便设计一个正奖励要重要得多。3.2 值迭代主循环值迭代的实现非常短核心就是一个循环里面对所有非终止状态做贝尔曼最优更新func valueIteration() ([]float64, []int) { statesCnt : rows * cols V : make([]float64, statesCnt) policy : make([]int, statesCnt) for { delta : 0.0 for s : 0; s statesCnt; s { if terminal(s) { continue } bestVal : math.Inf(-1) for _, a : range actions { s2, r : step(s, a) val : r gamma*V[s2] if val bestVal { bestVal val } } delta math.Max(delta, math.Abs(bestVal-V[s])) V[s] bestVal } if delta theta { break } } // 从收敛的价值函数提取最优策略 for s : 0; s statesCnt; s { if terminal(s) { policy[s] -1 continue } bestA, bestVal : -1, math.Inf(-1) for _, a : range actions { s2, r : step(s, a) val : r gamma*V[s2] if val bestVal { bestVal val bestA a } } policy[s] bestA } return V, policy }你可能注意到这里有个小细节在同一轮循环里我更新V[s]的时候后面格子可能用到的V[s2]已经是这一轮更新过的值了。这就是所谓的“原地更新”也叫Gauss-Seidel式更新它比“先算完整份新表再替换”的Jacobi式更新通常会更快收敛而且在这个场景下完全正确。不少教材教的是后者但我推荐实战时直接用原地更新速度更快代码还更简洁。跑完这个函数之后打印策略表你会看到一个非常漂亮的结果起点右上方的格子会一直向右走起点左边的格子会一路向右或向下所有格子都会挑离终点最近的方向。这时候你就知道值迭代真的从一团零值里“长”出了最优策略。3.3 值迭代的终止条件怎么选我用了delta theta来判断终止这是最标准的方式取所有状态本轮更新的最大差值。如果你把 theta 设置成1e-6那最终价值函数会非常接近数学上的最优价值。但如果你只关心“策略对不对”其实不需要等到这么精细。我在实际操作中发现值迭代有一件非常有趣的事策略的稳定通常远早于价值的完全收敛。比如在这个4x4世界里迭代到二十多轮时每个格子的最优动作其实已经定下来了但价值数字还在慢慢往下掉因为远处的奖励还在通过 gamma 一层层传过来。所以如果你只是想拿到最优策略可以把 theta 调大一些比如1e-3这会快得多。这个“提前终止”的思路不是偷懒而是工程优化。等你在更大规模的地图上跑实验时会感同身受什么叫“价值细节不重要动作排序才重要”。4. 策略迭代实现评估与改进的两阶段交替策略迭代的代码量会比值迭代多一截因为你要把策略评估和策略改进拆成两个独立的阶段。结构倒是非常对称先给一个乱来的初始策略然后循环——评估它、改进它、再评估、再改进——直到某轮改进后策略完全没变。这种写法的好处是你能清楚观察到策略的演化轨迹。第一次改进往往特别野蛮因为它会把所有状态的动作直接推向当前近似价值下的最优选择但经过一两轮评估修正后策略会稳定在一个更合理的状态。4.1 策略评估解开贝尔曼期望方程策略评估的核心逻辑是假设策略已经固定然后反复套用期望方程更新价值表。这看起来和值迭代的代码很像但有一个本质区别它不用max而是严格按当前策略选动作。func policyEval(policy []int) []float64 { statesCnt : rows * cols V : make([]float64, statesCnt) for { delta : 0.0 for s : 0; s statesCnt; s { if terminal(s) { continue } a : policy[s] s2, r : step(s, a) newV : r gamma*V[s2] delta math.Max(delta, math.Abs(newV-V[s])) V[s] newV } if delta theta { break } } return V }如果你的策略是随机的、包含多种动作这里就要改成对动作概率求期望。我这为了演示简单用了确定性策略所以代码看起来特别短。但逻辑大家要记住评估的目标是算出“这个策略对每个状态给出的长期累积回报预期”它只问当前策略会怎么做绝不问“还有什么更好的做法”。一个经验之谈策略评估内部这个循环收敛速度取决于 gamma 的大小。gamma 越接近1价值传播越慢循环次数就越多。我经常在测试时先从 gamma0.9 起步确认逻辑通畅后再调整到更高值不要一上来就0.99调试时收敛慢很痛苦的。4.2 策略改进贪心不是坏事下一步就是拿价值函数反过来更新策略。每个状态都选能让r gamma * V(s)最大的动作。这个操作有个专有名词——贪婪化因为它在每个状态都只盯着当前看起来最优的动作。func policyImprove(policy []int, V []float64) ([]int, bool) { statesCnt : rows * cols newPolicy : make([]int, statesCnt) changed : false for s : 0; s statesCnt; s { if terminal(s) { newPolicy[s] -1 continue } bestA, bestVal : -1, math.Inf(-1) for _, a : range actions { s2, r : step(s, a) val : r gamma*V[s2] if val bestVal { bestVal val bestA a } } if bestA ! policy[s] { changed true } newPolicy[s] bestA } return newPolicy, changed }看到没这里同样没有重新计算价值只是从现有价值函数里挑动作。如果你评估做得够准贪心出来的新策略就必然不会比旧策略差。这正是我们要的核心性质。4.3 主循环交替走到策略不动把上面两个函数串起来就是完整策略迭代func policyIteration() ([]int, []float64) { statesCnt : rows * cols policy : make([]int, statesCnt) for s : 0; s statesCnt; s { policy[s] actUp } for { V : policyEval(policy) newPolicy, changed : policyImprove(policy, V) policy newPolicy if !changed { return policy, V } } }这里有几个值得指出的工程细节初始策略我让所有状态都选“向上”这其实是个很差的策略——左上角的格子永远在撞墙。但它依然能跑通因为策略评估会如实地把这种低效率体现在惩罚上之后改进阶段就会把它纠正过来。终止条件是策略没变就停。注意我判断的是动作是否变了而不是价值还是不是变了。因为策略迭代的目标就是找策略价值只是辅助工具。我在主循环里每轮都从头做一次完整的策略评估。这个决定效率不高但逻辑极其干净也方便调试。后面我会讲一个实际优化技巧。在这个4x4世界里只要两三轮策略迭代策略表就会稳下来。这和值迭代那种慢慢磨的感觉很不一样策略迭代像带着地图找路三步五步就到目的地了。5. 同一张网格图上两个算法的真实差异我在本地把两个算法都跑了一遍记录了一些直观的数据。这些数字不是算法的理论复杂度但非常能说明问题。指标值迭代策略迭代外层或迭代次数约33轮价值迭代约5轮评估-改进循环每轮成本一次完整状态动作扫描评估阶段多次完整状态扫描收敛标准的实现难度低直接看最大差值需要额外判断策略是否变化中间产物对调试的友好度价值表一直在变看不出策略逻辑每轮都有明确的策略快照在大规模状态下单步便宜轮数多总开销可接受策略评估可能很贵轮数少但不一定快从迭代次数看策略迭代似乎是碾压值迭代的事实也确实如此——前提是状态空间不大。但当我按总计算量去估计时事情就没那么简单了。策略迭代的每一轮评估阶段内部可能要跑十几轮甚至几十轮的价值迭代如果你把评估阶段的内部迭代也累加起来它的总扫描次数可能跟值迭代差不多。所以这里有个很反直觉的结论策略迭代收敛轮数少但不绝对代表总计算量更小。我写过一次状态很多的迷宫测试策略迭代里简单的一次策略评估就要花几百轮网格扫描而值迭代反而因为单步简单、轮数可接受而跑得更快。5.1 从两种视角理解值迭代里的max我特别想再深挖一个点这可能是初学者最容易混淆的地方。看值迭代的更新公式它用的是max也就是对每个状态直接挑最好的动作来更新价值。这看起来好像它也在隐含做一个贪心策略选择但实际上它做得比策略迭代更“激进的激进”策略迭代是先算当前策略的价值再用这个价值改策略改完再重新算价值。值迭代是每次更新价值时都假设下一步一定会用最优动作直接把这个最优动作对应的价值写进当前状态。所以值迭代可以看成是策略评估和策略改进的极端混合体——它只做一次改进选max又只做一次评估更新V然后立刻接着下一个状态。理论上有一种说法是值迭代的本质是策略评估只迭代一次就马上做策略改进最后再取极限。这种视角有助于你理解为什么值迭代在价值上常常“超前”因为它的价值表里每一步都混入了对未来最优行为的预期。这个细微差别在调试时特别明显。你打印值迭代过程中间的价值表会发现数字很快就“像样了”但策略还没完全定型。而策略迭代的价值表则是基于一个明确策略算出来的中间每一轮都有一个自洽的解释——所有状态的价值都是针对当前策略而言的。5.2 值不动策略却在变一个让我困扰很久的现象我需要专门提一个我在调试中遇到的灵异现象值迭代已经跑到了收敛标准价值表几乎不动了但我每次从价值表提取策略时策略却不稳定会在少数几个格子之间来回跳。后来我想明白了这是因为价值表在几个备选动作上的差异非常非常小小到比收敛阈值还小。你把迭代停在这个位置正常来说策略已经是“接近最优”的了但如果你的目标是要一个严格的确定性最优策略就要么把theta调小一点要么在最后一步多跑几个额外的迭代。这个现象在现实场景中其实挺常见的尤其当你把gamma调到接近1时不同路径的长期奖励差距被压得越来越小。解决办法也不难在收敛标准之外加一个“策略在最近几轮是否稳定”的辅助判断。我后面再详细说。6. 调参和Debug我踩过的坑和总结的经验光看代码能跑通是不够的真正让你有收获的是调试过程中发现的问题。我把我踩过的坑按重要性列出来你也可以直接当作一份避坑清单。6.1 终点状态必须特殊处理否则会翻车这是我觉得最容易犯的错。如果你在值迭代或策略评估里不对终止状态做特殊处理那么终止状态也会参与更新会出现两种恶果终止状态的价值会被算成一个负值因为从终点继续走会撞墙、会扣分。策略会倾向于让智能体“不要到终点”因为停在终点前的状态反而能拿到更多负奖励的延迟补偿。正确做法就是我在前面代码里做的遇到终止状态直接跳过更新终点的价值保持为0。这是一个约定俗成的设计但也有人用“自循环终止奖励”方案终止后仍然返回0且转移回自己。两种都可以只是你写代码时要清楚自己在用哪种约定别让终点状态变成“被诅咒的格子”。6.2 gamma的选择直接影响收敛速度和合理性gamma在强化学习里是折扣因子它的意义不仅是数学技巧更是你对“长远”和“眼前”之间的权衡。当gamma0时智能体只看一步奖励永远不会规划未来——这在网格世界里会表现为每个格子都冲向最近的那一步不想多走任何弯路。当gamma0.99时智能体会为了更晚的奖励忍受大量前期惩罚学习也会慢得让人焦虑。我的实用建议先设0.9跑通确认自己的算法有收敛趋势然后再调到0.95或0.99观察策略的变化。如果策略没有明显变得更长远的规划说明你的环境设计或奖励信号可能有问题排查起来也方便。6.3 记录迭代轮数和策略快照比只看价值更直观我把两条调试经验一并分享给你第一不要在调试时只print价值表。价值表是数字数字在缓慢变化时你很难一眼看出来策略的趋势。我建议每轮迭代都print一次策略网格图用符号表示动作方向一眼就能看出哪些格子的策略在抖动。第二记录每一轮的delta值。你看到delta的下降曲线不是单调的不要慌这是正常现象。重要的是它最终是否突破theta。如果delta在大幅波动说明gamma太大或状态间有循环路径需要警惕。6.4 “一次Deep评估”和“截断评估”到底选哪个关于策略迭代有一个经典优化不用等策略评估完全收敛再做改进。你可以只允许评估阶段跑有限几轮比如5轮或10轮就立刻做策略改进。这种做法叫截断策略迭代它的收敛性在理论上也是被保障的而且工程上可以大幅减少计算量。我个人的经验是在状态空间很大时完全收敛的评估非常奢侈像是为了切一棵树把整片森林都锯了。你可以这样设置评估阶段设一个稍微宽松的内部阈值比如1e-2或者直接限制上限迭代次数为10次。这样一轮策略迭代的代价降低了整体策略到达最优的速度反而可能更快。6.5 用“策略稳定性”作为额外终止信号前面提到的价值没完全收敛而策略已经在跳的问题解决起来其实很简单。我后来给自己写了一条经验不管用值迭代还是策略迭代在收敛到价值阈值后额外执行几次迭代来确认策略是否保持稳定。如果连续几轮提取出来的策略完全没有变化那就可以非常有底气地终止计算。在值迭代的代码里这就是多花几轮扫描的代价换来策略的可靠性。我现在写强化学习玩具代码时已经默认这种行为权当保险丝。6.6 从经典DP到现代算法一条清晰的观察路径最后我想说这两个算法的价值不仅在于它们本身能解网格世界而在于你把它们吃透之后再看现代算法确实顺眼很多。Q-learning的更新式子里面那个max其实就是值迭代算子的采样版Actor-Critic结构里的actor做策略改进、critic做策略评估简直就是策略迭代在函数逼近时代的徒子徒孙甚至DDPG、PPO这些看起来时髦的算法剥开壳依然是评估-改进的骨架。所以花费一个晚上把这20年前的经典算法跑明白是真的不亏。如果还想继续深入我的建议是给这个网格世界加入随机转移看两个算法在带噪声时谁更稳健改成更大的迷宫环境测量一下两者的总计算时间亲身体会“迭代轮数少不等于总计算量小”打印出每一步的价值表和贝尔曼方程的数值解核对一遍保证你的理解没有偏差。这些实验做下来你对强化学习的信心会完全不一样。不要小看这几个代码短小的玩具问题它们才是帮你建立直觉的核心材料。