Files
RL-Study/Notebooks/C2.ipynb
T
2026-02-27 21:17:25 +08:00

7.4 KiB

第 2 章:状态价值与贝尔曼方程

贝尔曼方程描述了所有状态价值之间的关系。 我们将使用矩阵-向量形式 来表示贝尔曼方程: v_{\pi} = r_{\pi} + \gamma P_{\pi}v_{\pi} 以及使用迭代法 来求解它: v_{k+1} = r_{\pi} + \gamma P_{\pi}v_{k}

In [4]:
import numpy as np

# 1. 设定折扣因子
gamma = 0.9

# 2. 定义书本图 2.5 中的期望奖励向量 r_pi (Reward vector)
# s1 的期望奖励是 0.5*0 + 0.5*(-1) = -0.5
r_pi = np.array([-0.5, 1.0, 1.0, 1.0])

# 3. 定义状态转移矩阵 P_pi (Transition matrix)
# 每一行代表 s1, s2, s3, s4; 每一列代表转移到 s1, s2, s3, s4 的概率
P_pi = np.array([
    [0.0, 0.5, 0.5, 0.0],  # s1 有 0.5 概率到 s2, 0.5 概率到 s3
    [0.0, 0.0, 0.0, 1.0],  # s2 有 1.0 概率到 s4
    [0.0, 0.0, 0.0, 1.0],  # s3 有 1.0 概率到 s4
    [0.0, 0.0, 0.0, 1.0]   # s4 有 1.0 概率停留在 s4
])

# 4. 迭代法求解状态价值 (Iterative solution)
v_values = np.zeros(4) # 初始时,假设所有状态价值为 0
iterations = 100

print("--- 贝尔曼方程迭代求解过程 ---")
for k in range(iterations):
    # 核心公式:v_{k+1} = r_pi + gamma * P_pi * v_k
    v_next = r_pi + gamma * P_pi.dot(v_values)
    
    if k < 5 or k == iterations - 1:
        print(f"{k+1} 次迭代: v(s1)={v_next[0]:.4f}, v(s2)={v_next[1]:.4f}, v(s3)={v_next[2]:.4f}, v(s4)={v_next[3]:.4f}")
        
    v_values = v_next

print("\n最终收敛的状态价值:", np.round(v_values, 2))
# 你可以将这个输出结果与书中 2.5 节手算的 8.5, 10, 10, 10 进行对比!
--- 贝尔曼方程迭代求解过程 ---
第 1 次迭代: v(s1)=-0.5000, v(s2)=1.0000, v(s3)=1.0000, v(s4)=1.0000
第 2 次迭代: v(s1)=0.4000, v(s2)=1.9000, v(s3)=1.9000, v(s4)=1.9000
第 3 次迭代: v(s1)=1.2100, v(s2)=2.7100, v(s3)=2.7100, v(s4)=2.7100
第 4 次迭代: v(s1)=1.9390, v(s2)=3.4390, v(s3)=3.4390, v(s4)=3.4390
第 5 次迭代: v(s1)=2.5951, v(s2)=4.0951, v(s3)=4.0951, v(s4)=4.0951
第 100 次迭代: v(s1)=8.4997, v(s2)=9.9997, v(s3)=9.9997, v(s4)=9.9997

最终收敛的状态价值: [ 8.5 10.  10.  10. ]

2.8 从状态价值到动作价值 (Action Value)

动作价值 q_\pi(s,a) 评估的是在特定状态下采取特定动作的好坏。

它由两部分组成:

  1. 即时奖励的期望:采取动作 a 后立刻得到的奖励。
  2. 未来奖励的期望:进入下一个状态 s' 后,未来的状态价值 $\gamma v_\pi(s')$。

数学公式为:

q_{\pi}(s,a) = \sum_{r\in\mathcal{R}}p(r|s,a)r + \gamma\sum_{s'\in\mathcal{S}}p(s'|s,a)v_{\pi}(s')

状态价值其实就是所有可能动作价值的加权平均:

v_{\pi}(s) = \sum_{a\in\mathcal{A}}\pi(a|s)q_{\pi}(s,a)
In [5]:
import numpy as np

# 1. 继承之前的已知数据
gamma = 0.9
# 假设这是我们在 Cell 8 算出来的最终状态价值
v_values = {"s1": 8.5, "s2": 10.0, "s3": 10.0, "s4": 10.0}

# 2. 定义动作价值计算函数 q(s, a)
def calculate_q_value(state, action, reward, next_state, gamma, v_values):
    # q(s,a) = 立即奖励 + gamma * 下一个状态的价值
    return reward + gamma * v_values[next_state]

print("--- 计算在状态 s1 下,所有可能动作的价值 (Q-values) ---")

# --- 策略会选择的动作 (向右 a2, 向下 a3) ---
# a2: 向右进入 s2,得奖励 -1
q_s1_a2 = calculate_q_value("s1", "a2", -1, "s2", gamma, v_values)
print(f"策略内的动作: q(s1, a2向右) = -1 + 0.9 * v(s2) = {q_s1_a2}") # 预期为 8.0

# a3: 向下进入 s3,得奖励 0
q_s1_a3 = calculate_q_value("s1", "a3", 0, "s3", gamma, v_values)
print(f"策略内的动作: q(s1, a3向下) = 0 + 0.9 * v(s3) = {q_s1_a3}") # 预期为 9.0


# --- 策略不会选择的动作 (向上 a1, 向左 a4, 原地 a5) ---
# a1: 向上撞墙,弹回 s1,得奖励 -1
q_s1_a1 = calculate_q_value("s1", "a1", -1, "s1", gamma, v_values)
print(f"\n策略外的动作: q(s1, a1向上) = -1 + 0.9 * v(s1) = {q_s1_a1:.2f}") # 预期为 6.65

# a4: 向左撞墙,弹回 s1,得奖励 -1
q_s1_a4 = calculate_q_value("s1", "a4", -1, "s1", gamma, v_values)
print(f"策略外的动作: q(s1, a4向左) = -1 + 0.9 * v(s1) = {q_s1_a4:.2f}") # 预期为 6.65

# a5: 原地不动,停在 s1,得奖励 0
q_s1_a5 = calculate_q_value("s1", "a5", 0, "s1", gamma, v_values)
print(f"策略外的动作: q(s1, a5原地) =  0 + 0.9 * v(s1) = {q_s1_a5:.2f}") # 预期为 7.65


print("\n--- 验证公式:状态价值是动作价值的加权平均 ---")
# 我们的策略是: 0.5概率选a2, 0.5概率选a3
v_s1_calculated = 0.5 * q_s1_a2 + 0.5 * q_s1_a3
print(f"根据 Q 值反推的 v(s1) = 0.5 * {q_s1_a2} + 0.5 * {q_s1_a3} = {v_s1_calculated}")
print(f"这与我们之前迭代出来的 v(s1) = {v_values['s1']} 完全一致!")
--- 计算在状态 s1 下,所有可能动作的价值 (Q-values) ---
策略内的动作: q(s1, a2向右) = -1 + 0.9 * v(s2) = 8.0
策略内的动作: q(s1, a3向下) = 0 + 0.9 * v(s3) = 9.0

策略外的动作: q(s1, a1向上) = -1 + 0.9 * v(s1) = 6.65
策略外的动作: q(s1, a4向左) = -1 + 0.9 * v(s1) = 6.65
策略外的动作: q(s1, a5原地) =  0 + 0.9 * v(s1) = 7.65

--- 验证公式:状态价值是动作价值的加权平均 ---
根据 Q 值反推的 v(s1) = 0.5 * 8.0 + 0.5 * 9.0 = 8.5
这与我们之前迭代出来的 v(s1) = 8.5 完全一致!