
1. 为什么MDP是强化学习的“地基”很多刚接触强化学习的朋友在看完环境搭建和基础概念之后都会卡在同一个地方马尔可夫决策过程和贝尔曼方程。这两个词听起来像是数学系研究生的专属但实际上它们就是强化学习最底层的“游戏规则说明书”。你后面要学的Q-Learning、DQN、策略梯度甚至现在火热的大模型对齐技术全都建立在这套规则之上。我见过不少同学直接跳到代码实现结果遇到状态转移、折扣因子、值函数迭代这些概念时只能靠死记硬背。这样做的后果就是一旦环境稍微复杂一点或者需要自己设计奖励函数就完全不知道从何下手。所以这一篇我打算用最直白的方式把MDP和贝尔曼方程拆开揉碎让你不仅知道公式长什么样更知道它为什么长这样以及在实际写代码时怎么用。这篇文章适合已经了解强化学习基本概念智能体、环境、奖励、策略的读者。如果你还没看过前两篇建议先补一下基础。我会从MDP的五个核心要素讲起然后推导贝尔曼方程最后用一个完整的网格世界例子手把手带你实现值迭代和策略迭代。整个过程我会穿插自己踩过的坑和实操心得保证你看完能自己写出一个可运行的MDP求解器。2. 马尔可夫决策过程到底在描述什么2.1 从“马尔可夫性”说起马尔可夫性用一句话概括就是未来的状态只取决于当前状态与过去无关。这听起来像是一句废话但在强化学习里它是一个极其强力的假设。有了它我们才能把复杂的历史交互压缩成一个当前状态从而用有限的数学工具去描述无限的时间序列。举个生活化的例子。你玩一个迷宫游戏角色现在站在某个格子里。下一步能走到哪里、会得到什么奖励只跟这个格子的位置以及你选择的动作有关跟你之前是怎么绕到这个格子的完全没关系。这就是马尔可夫性。如果游戏设计成“必须按照特定路径才能触发隐藏奖励”那它就不满足马尔可夫性标准的MDP框架就不适用了。在实际建模时我们通常假设环境满足马尔可夫性。如果不满足常见的做法是把历史信息也编码进状态里强行让它满足。比如把最近几帧画面堆叠起来作为状态输入这也是DQN处理Atari游戏时的标准操作。2.2 MDP的五元组拆解一个标准的MDP由五个元素组成我习惯把它们记成“状态、动作、转移、奖励、折扣”状态集合 S环境所有可能状态的集合。在网格世界里就是所有格子的坐标。动作集合 A智能体在每个状态下可以采取的动作。比如上下左右四个方向。转移概率 P(s|s,a)在状态s采取动作a后转移到状态s的概率。确定性环境下这个概率是1或0随机环境下则是分布。奖励函数 R(s,a,s)从状态s经过动作a转移到s时获得的即时奖励。折扣因子 γ取值在0到1之间用来衡量未来奖励的重要性。这五个元素一旦确定整个强化学习问题就形式化了。智能体的目标就是找到一个策略π(a|s)使得从任意状态出发累积折扣奖励的期望最大。注意很多教程会把奖励写成R(s,a)或者R(s)这取决于具体问题的建模方式。只要前后一致不影响最终结果。但在推导贝尔曼方程时用R(s,a,s)最严谨也最不容易出错。2.3 回报、值函数与策略回报是从当前时刻开始未来所有奖励的折扣累加G_t R_{t1} γR_{t2} γ²R_{t3} ...折扣因子γ的作用有两个一是让无穷级数收敛二是调节智能体的“短视”程度。γ接近0时智能体只看眼前利益γ接近1时它会考虑长远收益。实际项目中γ通常取0.9到0.99之间。如果任务步数很长比如上千步γ可以取0.99甚至0.999但要注意数值稳定性。状态值函数 Vπ(s)表示在状态s下按照策略π行动所能获得的期望回报。动作值函数 Qπ(s,a)则表示在状态s下先采取动作a之后再按照策略π行动所能获得的期望回报。这两个函数是强化学习的核心贝尔曼方程就是描述它们之间递归关系的等式。3. 贝尔曼方程把递归思想装进公式里3.1 贝尔曼期望方程是怎么来的贝尔曼方程的本质是递归。当前状态的价值等于即时奖励加上折扣后的下一状态价值。用公式写出来就是Vπ(s) Σ_a π(a|s) Σ_{s} P(s|s,a) [ R(s,a,s) γ Vπ(s) ]这个公式看起来复杂但拆开看非常直观。对于每个可能的动作a我们按照策略π的概率去选择它对于每个动作环境可能转移到不同的s我们按照转移概率加权每次转移获得即时奖励R再加上折扣后的下一状态价值γVπ(s)。全部加起来就是当前状态的价值。同理动作值函数的贝尔曼期望方程是Qπ(s,a) Σ_{s} P(s|s,a) [ R(s,a,s) γ Σ_{a} π(a|s) Qπ(s,a) ]这两个方程是等价的可以互相推导。在实际算法中我们通常根据具体情况选择用V还是Q。比如值迭代用V更方便而Q-Learning直接用Q。3.2 贝尔曼最优方程与最优策略贝尔曼期望方程描述的是“给定策略下的价值”而贝尔曼最优方程描述的是“所有策略中最好的那个价值”V*(s) max_a Σ_{s} P(s|s,a) [ R(s,a,s) γ V*(s) ]注意这里的max操作。它表示在状态s下我们不再按照某个固定策略选择动作而是直接选择那个能让价值最大的动作。对应的最优策略就是π*(s) argmax_a Σ_{s} P(s|s,a) [ R(s,a,s) γ V*(s) ]贝尔曼最优方程是非线性的因为里面有max操作。这导致它不能像期望方程那样直接解线性方程组而需要迭代求解。值迭代和策略迭代就是两种经典的迭代方法。实操心得很多同学在推导时会混淆V和Q的贝尔曼方程。我的建议是先记住V的版本然后利用Q(s,a) R γV(s)的关系去推导Q的版本。这样不容易记错。3.3 折扣因子的实际影响折扣因子γ的选择会直接影响智能体的行为。我做过一个简单的实验在一个需要绕路才能到达终点的网格世界里γ0.5时智能体宁愿原地打转也不愿意绕远路γ0.9时它能找到最优路径γ0.99时路径更倾向于最短但收敛速度明显变慢。从数学上看γ决定了有效视野长度。大约1/(1-γ)步之后的奖励会被折扣到可以忽略不计。γ0.9对应约10步γ0.99对应约100步。所以如果你的任务需要智能体考虑100步以后的奖励γ至少要设到0.99。另外γ1时如果任务没有终止状态回报可能发散导致值函数无穷大。所以实际项目中除非有明确的终止条件否则不要设γ1。4. 手把手实现值迭代与策略迭代4.1 环境设计一个4x4网格世界为了让你能直接复现我用一个经典的4x4网格世界作为例子。规则如下状态16个格子用0到15编号。动作0上1下2左3右。转移确定性转移撞墙则停在原地。奖励到达终点格子15奖励1其他转移奖励-0.01。折扣因子γ0.99。终止状态格子15。这个环境足够简单但包含了MDP的所有核心要素。你可以把它复制到任何Python环境里运行。import numpy as np class GridWorld: def __init__(self, size4): self.size size self.n_states size * size self.n_actions 4 self.goal self.n_states - 1 self.gamma 0.99 self.reward -0.01 self.goal_reward 1.0 def step(self, state, action): row, col divmod(state, self.size) if action 0: # 上 row max(row - 1, 0) elif action 1: # 下 row min(row 1, self.size - 1) elif action 2: # 左 col max(col - 1, 0) elif action 3: # 右 col min(col 1, self.size - 1) next_state row * self.size col if next_state self.goal: return next_state, self.goal_reward, True return next_state, self.reward, False这段代码里step函数返回下一个状态、即时奖励和是否终止。注意撞墙时状态不变但仍然会得到-0.01的惩罚这鼓励智能体尽快到达终点。4.2 值迭代的实现与参数选择值迭代的核心思想是反复应用贝尔曼最优方程直到值函数收敛。伪代码如下初始化V(s)0对所有s。重复直到收敛对每个状态s计算所有动作的Q值。更新V(s) max_a Q(s,a)。输出最优值函数和最优策略。def value_iteration(env, theta1e-6, max_iter10000): V np.zeros(env.n_states) for i in range(max_iter): delta 0 for s in range(env.n_states): if s env.goal: continue q_values [] for a in range(env.n_actions): s_next, r, done env.step(s, a) q_values.append(r env.gamma * V[s_next]) best max(q_values) delta max(delta, abs(best - V[s])) V[s] best if delta theta: print(f值迭代在第{i1}轮收敛) break policy np.zeros(env.n_states, dtypeint) for s in range(env.n_states): if s env.goal: continue q_values [] for a in range(env.n_actions): s_next, r, done env.step(s, a) q_values.append(r env.gamma * V[s_next]) policy[s] np.argmax(q_values) return V, policy这里theta是收敛阈值我设的是1e-6。实际测试中这个4x4网格大约在30到50轮就收敛了。如果你把γ改成0.999收敛轮数会明显增加可能需要几百轮。注意事项值迭代每一轮都要遍历所有状态和动作复杂度是O(|S|·|A|)。状态数很大时比如连续状态空间就需要用函数逼近那就是DQN的范畴了。4.3 策略迭代的实现与对比策略迭代分为两步策略评估和策略改进。策略评估是给定策略π反复应用贝尔曼期望方程直到V收敛策略改进是根据V选择贪心动作得到新策略。重复这两步直到策略不再变化。def policy_evaluation(env, policy, theta1e-6, max_iter1000): V np.zeros(env.n_states) for _ in range(max_iter): delta 0 for s in range(env.n_states): if s env.goal: continue a policy[s] s_next, r, done env.step(s, a) new_v r env.gamma * V[s_next] delta max(delta, abs(new_v - V[s])) V[s] new_v if delta theta: break return V def policy_iteration(env, max_iter100): policy np.zeros(env.n_states, dtypeint) for i in range(max_iter): V policy_evaluation(env, policy) stable True for s in range(env.n_states): if s env.goal: continue old_action policy[s] q_values [] for a in range(env.n_actions): s_next, r, done env.step(s, a) q_values.append(r env.gamma * V[s_next]) policy[s] np.argmax(q_values) if old_action ! policy[s]: stable False if stable: print(f策略迭代在第{i1}轮收敛) break return V, policy策略迭代通常比值迭代收敛更快因为每次策略评估后策略改进的幅度更大。但每次策略评估本身需要迭代很多轮所以单轮计算量更大。在我的测试中4x4网格上策略迭代大约5到8轮就收敛了而值迭代需要30多轮。但总计算时间两者差不多因为策略迭代每轮更耗时。4.4 结果可视化与策略解读运行完两个算法后你可以把最优策略打印出来。用箭头表示动作你会看到智能体从任意格子出发都会沿着最短路径走向右下角的终点。如果某个格子有多个最优动作argmax会返回第一个所以策略可能不是唯一的。我建议你把V值也打印出来观察一下数值分布。靠近终点的格子V值接近1远离终点的格子V值接近-0.1左右。这是因为每走一步都有-0.01的惩罚折扣因子又让未来奖励打折扣。这个分布直观地反映了“距离终点越远价值越低”的规律。如果你想进一步验证可以手动计算某个格子的V值然后跟程序输出对比。比如终点左边那个格子向右走到终点获得1所以V -0.01 0.99*1 0.98。程序输出应该非常接近这个值。5. 常见问题与排查技巧实录5.1 值函数不收敛或发散怎么办这是新手最常见的问题。我总结了几种典型情况和对应的排查思路现象可能原因解决方法V值越来越大最终变成inf折扣因子γ≥1且没有终止状态把γ设为0.99或更小V值震荡不收敛学习率过大或转移概率有误检查转移函数确保概率和为1收敛极慢γ太接近1或状态空间太大适当降低γ或使用函数逼近策略在多个动作间跳变存在多个等价最优动作这是正常现象固定一个即可我踩过的一个坑是在写转移函数时忘记处理撞墙情况导致智能体可以“穿墙”到达终点V值异常高。后来加上边界检查才正常。所以写环境时一定要仔细检查边界条件。5.2 贝尔曼方程推导中的常见误区很多同学在推导时会犯两个错误。第一个是把V和Q的方程搞混第二个是忘记对动作和下一状态求和。我的建议是推导时先把所有可能的动作和下一状态列出来然后逐个加权求和不要跳步。另一个误区是认为贝尔曼方程只能用于确定性环境。实际上它天然支持随机环境只要把转移概率写进去就行。随机环境下的值迭代和策略迭代代码几乎不用改只需要把step函数改成返回概率分布即可。5.3 实际项目中的参数调优经验在实际项目中γ和奖励函数的设计往往比算法本身更重要。我做过一个路径规划项目一开始γ0.9智能体总是选择绕远路去捡小奖励而不愿意走最短路径到达终点。后来把γ提高到0.99同时把每步惩罚从-0.01调到-0.05智能体才学会了直奔终点。奖励函数的设计原则是让智能体的目标与你的目标一致。如果你希望它尽快到达终点就给每步一个小的负奖励如果你希望它探索更多区域就给探索新状态一个正奖励。但要注意奖励太稀疏会导致学习困难太密集又容易让智能体“刷分”。实操心得调试MDP时先用小规模环境比如3x3验证算法正确性再扩展到大规模。小环境下手动计算几个V值跟程序输出对比能快速发现公式实现错误。5.4 从MDP到深度强化学习的过渡当你理解了MDP和贝尔曼方程再去看DQN就会觉得顺理成章。DQN本质上就是用神经网络来近似Q(s,a)然后用贝尔曼最优方程构造损失函数L E[(r γ max_{a} Q(s,a) - Q(s,a))²]这个损失函数就是贝尔曼最优方程的残差。值迭代是直接迭代V而DQN是迭代Q网络的参数。理解了这一点你就知道为什么DQN需要目标网络、经验回放这些技巧了——它们都是为了让这个迭代过程更稳定。如果你已经能熟练实现值迭代和策略迭代我建议下一步尝试用Q表格实现Q-Learning然后再过渡到DQN。这个路径比直接看DQN论文要平滑得多。6. 从公式到代码的完整闭环6.1 一个可复现的完整示例把前面的代码片段拼起来加上可视化和测试就是一个完整的MDP求解器。我建议你按照以下步骤操作创建GridWorld类实现step方法。实现value_iteration函数设置theta1e-6。实现policy_iteration函数设置max_iter100。分别运行两个算法打印收敛轮数和最终策略。用箭头可视化策略检查是否符合预期。如果你运行结果跟我的描述有出入先检查step函数的边界处理再检查γ和奖励值是否一致。这两个地方最容易出错。6.2 扩展思路从确定性到随机性把确定性环境改成随机环境只需要改一处step函数不再返回单个下一状态而是返回一个列表包含所有可能的下一状态及其概率。比如向右走有80%概率到达右边格子20%概率停在原地。然后值迭代和策略迭代的代码只需要把V[s_next]改成加权求和即可。这个扩展能帮你理解为什么随机环境下的值函数更平滑。因为每个动作的结果不确定智能体不能依赖某个特定结果所以值函数会综合考虑所有可能性。6.3 我个人的一些经验体会最后分享几个我在实际项目中总结的小技巧。第一先验证再优化。不要一上来就调参先用小环境确认算法逻辑正确。第二打印中间结果。值迭代每一轮的V值变化、策略迭代每一轮的动作变化都能帮你快速定位问题。第三善用可视化。把V值画成热力图把策略画成箭头图一眼就能看出哪里不对。贝尔曼方程和MDP是强化学习的“内功”练好这部分后面学任何算法都会觉得有迹可循。我见过太多人跳过这部分直接学DQN结果遇到问题只能靠猜。花时间把这一篇的内容吃透绝对值回票价。