4. 转移概率 (Transition Probability) —— 数学定义
好,我们接着往下聊。
前面我们把 MDP 的「状态」和「动作」拆开了。但真正让 MDP 动起来的,是这两个东西之间的桥梁——转移概率。说白了,它就是回答一个问题:你在状态 s 下,做了动作 a,下一时刻到底会跳到哪个状态?
有人可能会说,“那还用问,如果环境是确定的,那不就只有一个结果吗?” 嗯,现实中很多问题恰恰不是确定的。我调过一个机器人路径规划的项目,同样的指令丢出去,轮子打滑、传感器噪声、地面摩擦系数变化……每次落点都不一样。这时候,转移概率就是描述这种不确定性的数学模型。
4.1 从直观到数学
我们先不看公式,先想一个简单例子。
假设你站在一个 2×2 的网格里,当前位置是 (0,0)。你选择「向右走」这个动作。理想情况下,你会到达 (0,1)。但在真实环境中,有 0.1 的概率你撞墙留在原地,有 0.1 的概率你滑到 (1,0)。
这就是一个典型的概率转移场景。MDP 把这个关系规整成一个函数:
转移概率的核心理念
P(s' | s, a) = 从状态 s 执行动作 a 后,转移到状态 s' 的概率
这个概率必须满足:
- 对于给定的 (s, a),所有可能的 s' 的概率之和为 1
- 每个概率值都在 [0, 1] 之间
你看,它本质上是一个 条件概率分布。数学上它是 MDP 四元组 (S, A, P, R) 里的那个关键 P(注意,有时候大家用 T 来表示,别被符号搞混了)。
4.2 完整数学定义
我们把话说得更标准一点。
给定一个 MDP,记状态空间为 S,动作空间为 A。转移概率函数定义为:
P : S × A × S → [0, 1]
并且满足:
对于所有 s ∈ S, a ∈ A:
Σ_{s' ∈ S} P(s' | s, a) = 1
这个定义看着简单,但我在实践中发现,很多初学者会忽略一个细节:这个函数对 (s, a) 的所有可能 s' 求和才是 1,而不是对某个具体的 s' 取值为 1。
举个例子,我刚才说的网格世界,如果 P(s'=(0,1) | s=(0,0), a=向右) = 0.8, P(s'=(0,0) | s=(0,0), a=向右) = 0.1, P(s'=(1,0) | s=(0,0), a=向右) = 0.1,那么这三个加起来是 1.0,符合定义。但如果我只定义了一个 s' 的概率是 0.8,其他概率没补全,那这个模型就是不合法的。
避坑指南
我曾经在给别人review RL环境的代码时,发现一个 Bug:设计者定义了某个动作有 0.6 的概率转移到目标状态,0.2 概率留在原地——加起来只有 0.8。然后剩下的 0.2 概率去哪了?环境在运行时直接报错了。所以,定义转移概率时,一定要覆盖所有可能的后续状态分支。如果某个状态理论上不可能达到,那它的概率应该是 0,不是你就不写它。
4.3 两种表示形式
在实际代码里,我们一般用两种方式存这玩意儿。
方式一:稠密矩阵
如果状态和动作数量不大(比如 100 以下),直接用三维张量。形状是 [|S|, |A|, |S|]。
import numpy as np
# 假设 3 个状态,2 个动作
num_states = 3
num_actions = 2
# 初始化转移概率矩阵
P = np.zeros((num_states, num_actions, num_states))
# 设置:在状态 0,执行动作 0,有 0.9 概率到状态 0,0.1 概率到状态 1
P[0, 0, :] = [0.9, 0.1, 0.0]
# 检查合法性:每个 [s, a] 的行求和必须等于 1
for s in range(num_states):
for a in range(num_actions):
total = np.sum(P[s, a, :])
print(f"State {s}, Action {a}: sum = {total:.2f}")
方式二:稀疏字典
当状态数量大到十万、百万级别时,稠密矩阵存不下。我会用字典或专门的稀疏格式。
# 用字典表示:key 是 (s, a),value 是 {s': prob}
transition = {
(0, 'right'): {1: 0.8, 0: 0.1, 2: 0.1},
(0, 'left'): {0: 0.9, 1: 0.1},
(1, 'right'): {2: 1.0}, # 确定性转移
(1, 'left'): {0: 1.0},
(2, 'right'): {1: 1.0},
(2, 'left'): {1: 1.0}
}
# 验证有效性
for key, probs in transition.items():
total = sum(probs.values())
assert abs(total - 1.0) < 1e-9, f"Invalid transition probabilities for {key}"
我个人习惯是:小空间用稠密矩阵(方便直接做矩阵运算),大空间用稀疏字典(节省内存)。代码写完后,加个断言检查合法性——这是一条好习惯。
4.4 转移概率的几种特殊形式
有些时候,问题本身会约束转移概率的结构。我挑几个典型的说说。
| 类型 | 特征 | 数学表示 | 实际例子 |
|---|---|---|---|
| 确定性转移 | 执行动作后,下一状态唯一确定 | P(s'|s,a) = 1 对于某个 s',其他为 0 | 棋盘游戏:落子位置精确 |
| 马尔可夫链 | 转移概率与动作无关(无动作控制) | P(s'|s,a) = P(s'|s) 对所有 a | 简单的天气模型 |
| 随机性转移 | 执行相同动作,结果有多种可能 | P(s'|s,a) 在多个 s' 上非均匀分布 | 机器人控制、复杂物理仿真 |
| 吸收态转移 | 进入某个状态后,永远停在那里 | P(s'=s_abs|s_abs,a) = 1 对所有 a | 游戏结束状态、死锁状态 |
我的经验是:在搭建仿真环境时,永远先用确定性转移做测试。确定性能帮你快速验证奖励函数和策略逻辑是否正确。等基础通了,再引入随机性。否则你分不清问题是出在概率噪声上,还是代码逻辑上。这个「先确定、后随机」的调式原则,我在至少三个项目里都用上了,每次都省了不少时间。
4.5 转移概率在 MDP 中的角色
为了让你更直观地理解转移概率在整个 MDP 流程里的位置,我画了张图。
你看这张图就清楚了:状态 s → 动作 a → 转移概率决定下一状态 s',同时奖励也在这个转移过程中产生。而吸收态(比如游戏里的 Game Over)是一种特殊 s',进去就出不来了。那在代码里怎么处理呢?
# 吸收态处理示例
absorbing_state = 999
def is_absorbing(state):
# 检查状态是否是吸收态
return state == absorbing_state
# 当智能体进入吸收态后,所有动作都停留在该状态
for a in range(num_actions):
P[absorbing_state, a, :] = 0 # 先清零
P[absorbing_state, a, absorbing_state] = 1.0 # 自循环概率为1
4.6 为什么需要转移概率?
你想想看,如果不要这个概率,只用确定性映射会怎样?
- 强化学习算法(比如 Q-learning、Policy Gradient)会丧失收敛保证。很多理论推导都依赖概率转移函数的存在。
- 环境的不确定性建模不了。真实世界永远有噪声,没有概率你就是在玩一个假的模型。
- 策略评估会出错。贝尔曼期望方程里的期望值 E 就是基于转移概率求的。
我记得有一次做无人车仿真,有人提议“反正都是仿真,直接把转移设成确定的多简单”。确实简单。但后来把训练好的模型放到带随机噪声的物理引擎上时,直接崩了。原因就是策略学到的全是“确定性最佳路径”,根本没预留容错性。这种事我经历一次就记住了。
核心小结
转移概率是 MDP 的「动态规则说明书」。它告诉我们:
- 在给定状态下做某个动作,每个可能后续状态的概率是多少
- 所有概率加起来必须严格等于 1
- 它可以是确定的,也可以是随机的,取决于你建模的问题
- 实现时务必加合法性检查(sum 校验和范围校验)
下一段我们来谈奖励函数 R —— 但这里先不展开。记住,没有转移概率,MDP 就是死的。有了它,你才能开始计算期望回报、做动态规划、跑强化学习。它是整个 MDP 计算链条的「发动机」。