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 流程里的位置,我画了张图。

转移概率在MDP中的核心作用 当前状态 s 选择动作 a 转移概率 P(s' | s, a) 概率分布 + 求和为1 下一状态 s' 吸收态 / 结束 奖励 R(s,a) 状态更新,进入下一轮决策 ⚠ 关键约束 每个 (s,a) 的转移概率和 = 1

你看这张图就清楚了:状态 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. 在给定状态下做某个动作,每个可能后续状态的概率是多少
  2. 所有概率加起来必须严格等于 1
  3. 它可以是确定的,也可以是随机的,取决于你建模的问题
  4. 实现时务必加合法性检查(sum 校验和范围校验)

下一段我们来谈奖励函数 R —— 但这里先不展开。记住,没有转移概率,MDP 就是死的。有了它,你才能开始计算期望回报、做动态规划、跑强化学习。它是整个 MDP 计算链条的「发动机」。