hands-on-modern-rl:动手学强化学习 贝尔曼方程

文章目录

贝尔曼期望方程

给定一个固定策略 π,贝尔曼期望方程为

V π ( s ) = Σ a π ( a ∣ s ) ∗ R ( s , a ) + γ ∗ Σ s ′ P ( s ′ ∣ s , a ) ∗ V π ( s ′ ) V^π(s) = Σ_a π(a|s) * R(s,a) + γ \* Σ_{s'} P(s'\|s,a) \* V\^π(s') Vπ(s)=Σaπ(a∣s)∗R(s,a)+γ∗Σs′P(s′∣s,a)∗Vπ(s′)

  • 贝尔曼最优方程
    V ∗ ( s ) = max ⁡ a Σ s ′ P ( s ′ ∣ s , a ) R ( s , a ) + γ V ∗ ( s ′ ) V^*(s) = \max_a Σ_{s'} P(s'|s,a) R(s,a) + γ V\^\*(s') V∗(s)=amaxΣs′P(s′∣s,a)R(s,a)+γV∗(s′)
python 复制代码
"""
第3章:贝尔曼方程的数值验证
用代码验证贝尔曼期望方程和贝尔曼最优方程

本实验展示:
1. 手工计算贝尔曼期望方程 V^π(s)
2. 用价值迭代(Value Iteration)数值求解 V^π(s) 和 V*(s)
3. 对比两种方法的结果,验证一致性
4. 展示价值迭代逐步收敛的过程

运行方式:
    python bellman_equation_verify.py
"""

import os
import numpy as np
import matplotlib.pyplot as plt

# 创建输出目录
os.makedirs("output", exist_ok=True)


# ==========================================
# 第一部分:定义一个简单的 3 状态 MDP
# ==========================================
# 我们手工构造一个微型 MDP,方便手工计算和代码验证。
#
# 状态集合:S = {s0, s1, s2}
# 动作集合:A = {a0, a1}(每个状态可选两个动作)
#
# 转移概率 P(s'|s,a) 和奖励 R(s,a) 定义如下:
#
# 从 s0 出发:
#   执行 a0 → 转移到 s1(概率 1.0),奖励 = 1
#   执行 a1 → 转移到 s2(概率 1.0),奖励 = 2
#
# 从 s1 出发:
#   执行 a0 → 转移到 s0(概率 0.5),奖励 = -1
#                 转移到 s2(概率 0.5),奖励 = -1
#   执行 a1 → 转移到 s1(概率 0.8),奖励 = 0
#                 转移到 s2(概率 0.2),奖励 = 0
#
# 从 s2 出发:
#   执行 a0 → 转移到 s1(概率 1.0),奖励 = 3
#   执行 a1 → 转移到 s2(概率 1.0),奖励 = 1(自循环)

# 状态和动作数量
N_STATES = 3
N_ACTIONS = 2

# 转移概率:P[s][a] = {下一状态: 概率}
P = {
    0: {  # s0
        0: {1: 1.0},    # a0 → s1 概率 1.0
        1: {2: 1.0},    # a1 → s2 概率 1.0
    },
    1: {  # s1
        0: {0: 0.5, 2: 0.5},  # a0 → s0 概率 0.5, s2 概率 0.5
        1: {1: 0.8, 2: 0.2},  # a1 → s1 概率 0.8, s2 概率 0.2
    },
    2: {  # s2
        0: {1: 1.0},    # a0 → s1 概率 1.0
        1: {2: 1.0},    # a1 → s2 概率 1.0(自循环)
    },
}

# 奖励函数:R[s][a] = 立即奖励
R = {
    0: {0: 1, 1: 2},     # s0: a0 奖励 1, a1 奖励 2
    1: {0: -1, 1: 0},    # s1: a0 奖励 -1, a1 奖励 0
    2: {0: 3, 1: 1},     # s2: a0 奖励 3, a1 奖励 1
}

GAMMA = 0.9  # 折扣因子


# ==========================================
# 第二部分:贝尔曼期望方程 ------ 手工计算
# ==========================================
def manual_bellman_expectation():
    """
    手工计算贝尔曼期望方程 V^π(s)

    给定一个固定策略 π,贝尔曼期望方程为:
        V^π(s) = Σ_a π(a|s) * [R(s,a) + γ * Σ_{s'} P(s'|s,a) * V^π(s')]

    我们使用一个简单的均匀随机策略:
        π(a|s) = 0.5(每个动作等概率选择)

    手工推导(令 V^π(s0) = v0, V^π(s1) = v1, V^π(s2) = v2):

    V^π(s0) = 0.5 * [R(s0,a0) + γ * V^π(s1)] + 0.5 * [R(s0,a1) + γ * V^π(s2)]
            = 0.5 * [1 + 0.9 * v1] + 0.5 * [2 + 0.9 * v2]
            = 0.5 + 0.45*v1 + 1 + 0.45*v2
            = 1.5 + 0.45*v1 + 0.45*v2  ........................ (方程1)

    V^π(s1) = 0.5 * [R(s1,a0) + γ * (0.5*V^π(s0) + 0.5*V^π(s2))]
            + 0.5 * [R(s1,a1) + γ * (0.8*V^π(s1) + 0.2*V^π(s2))]
            = 0.5 * [-1 + 0.9*(0.5*v0 + 0.5*v2)]
            + 0.5 * [0 + 0.9*(0.8*v1 + 0.2*v2)]
            = -0.5 + 0.225*v0 + 0.225*v2 + 0.36*v1 + 0.09*v2
            = -0.5 + 0.225*v0 + 0.36*v1 + 0.315*v2  ............ (方程2)

    V^π(s2) = 0.5 * [R(s2,a0) + γ * V^π(s1)] + 0.5 * [R(s2,a1) + γ * V^π(s2)]
            = 0.5 * [3 + 0.9 * v1] + 0.5 * [1 + 0.9 * v2]
            = 1.5 + 0.45*v1 + 0.5 + 0.45*v2
            = 2.0 + 0.45*v1 + 0.45*v2  ........................ (方程3)
    """
    print("=" * 60)
    print("  贝尔曼期望方程 ------ 手工推导")
    print("=" * 60)
    print()
    print("给定策略:均匀随机 π(a|s) = 0.5")
    print(f"折扣因子:γ = {GAMMA}")
    print()
    print("列方程组(v0 = V^π(s0), v1 = V^π(s1), v2 = V^π(s2)):")
    print("  v0 = 1.5   + 0.45*v1 + 0.45*v2  ...... (方程1)")
    print("  v1 = -0.5  + 0.225*v0 + 0.36*v1 + 0.315*v2  (方程2)")
    print("  v2 = 2.0   + 0.45*v1 + 0.45*v2  ...... (方程3)")
    print()

    # 求解线性方程组 A * v = b
    # 方程1: v0 - 0.45*v1 - 0.45*v2 = 1.5
    # 方程2: -0.225*v0 + (1-0.36)*v1 - 0.315*v2 = -0.5
    # 方程3: -0.45*v1 + (1-0.45)*v2 = 2.0

    A = np.array([
        [1.0,   -0.45,   -0.45],
        [-0.225, 0.64,   -0.315],
        [0.0,   -0.45,    0.55],
    ])
    b = np.array([1.5, -0.5, 2.0])

    manual_V = np.linalg.solve(A, b)

    print("手工求解线性方程组得到:")
    for i in range(N_STATES):
        print(f"  V^π(s{i}) = {manual_V[i]:.6f}")
    print()
    return manual_V


# ==========================================
# 第三部分:策略评估 ------ 迭代求解贝尔曼期望方程
# ==========================================
def policy_evaluation(policy, max_iter=1000, tol=1e-8):
    """
    策略评估:通过迭代求解贝尔曼期望方程

    贝尔曼期望方程(迭代形式):
        V(s) ← Σ_a π(a|s) * [R(s,a) + γ * Σ_{s'} P(s'|s,a) * V(s')]

    反复迭代直到 V(s) 收敛,收敛后的 V(s) 就是 V^π(s)。

    参数:
        policy: 策略 π(a|s),形状 (N_STATES, N_ACTIONS)
        max_iter: 最大迭代次数
        tol: 收敛阈值
    返回:
        V: 状态价值函数
        history: 每次迭代的 V 值记录(用于可视化收敛过程)
    """
    V = np.zeros(N_STATES)
    history = [V.copy()]

    for iteration in range(max_iter):
        V_new = np.zeros(N_STATES)

        for s in range(N_STATES):
            # 贝尔曼期望方程:对所有动作求加权和
            for a in range(N_ACTIONS):
                # π(a|s) * [R(s,a) + γ * Σ P(s'|s,a) * V(s')]
                action_value = R[s][a]
                for next_s, prob in P[s][a].items():
                    action_value += GAMMA * prob * V[next_s]
                V_new[s] += policy[s][a] * action_value

        # 检查是否收敛
        delta = np.max(np.abs(V_new - V))
        history.append(V_new.copy())
        V = V_new

        if delta < tol:
            break

    return V, history


# ==========================================
# 第四部分:价值迭代 ------ 求解贝尔曼最优方程
# ==========================================
def value_iteration(max_iter=1000, tol=1e-8):
    """
    价值迭代:求解贝尔曼最优方程,找到 V*(s)

    贝尔曼最优方程(迭代形式):
        V(s) ← max_a [R(s,a) + γ * Σ_{s'} P(s'|s,a) * V(s')]

    与贝尔曼期望方程的区别:
    - 期望方程:给定策略 π,求 V^π(s)
    - 最优方程:对所有策略取最优,求 V*(s)

    V*(s) 满足:
        V*(s) = max_a Σ_{s'} P(s'|s,a) [R(s,a) + γ * V*(s')]

    参数:
        max_iter: 最大迭代次数
        tol: 收敛阈值
    返回:
        V_star: 最优状态价值函数
        optimal_policy: 最优策略
        history: 收敛过程
    """
    V = np.zeros(N_STATES)
    history = [V.copy()]

    for iteration in range(max_iter):
        V_new = np.zeros(N_STATES)

        for s in range(N_STATES):
            # 对每个动作计算 Q(s, a)
            q_values = []
            for a in range(N_ACTIONS):
                q = R[s][a]
                for next_s, prob in P[s][a].items():
                    q += GAMMA * prob * V[next_s]
                q_values.append(q)

            # 贝尔曼最优方程:取最大值而不是期望
            V_new[s] = max(q_values)

        delta = np.max(np.abs(V_new - V))
        history.append(V_new.copy())
        V = V_new

        if delta < tol:
            break

    # 从 V* 提取最优策略
    optimal_policy = extract_optimal_policy(V)

    return V, optimal_policy, history


def extract_optimal_policy(V):
    """
    从最优价值函数 V* 提取最优策略 π*

    π*(s) = argmax_a [R(s,a) + γ * Σ_{s'} P(s'|s,a) * V*(s')]
    """
    policy = np.zeros((N_STATES, N_ACTIONS))

    for s in range(N_STATES):
        q_values = []
        for a in range(N_ACTIONS):
            q = R[s][a]
            for next_s, prob in P[s][a].items():
                q += GAMMA * prob * V[next_s]
            q_values.append(q)

        best_action = np.argmax(q_values)
        policy[s][best_action] = 1.0  # 确定性策略

    return policy


# ==========================================
# 第五部分:结果对比与可视化
# ==========================================
def verify_results():
    """验证手工计算与迭代计算的一致性"""

    # 均匀随机策略
    uniform_policy = np.ones((N_STATES, N_ACTIONS)) / N_ACTIONS

    print("=" * 60)
    print("  贝尔曼方程数值验证")
    print("=" * 60)
    print()

    # ------------------------------------------
    # 对比1:手工计算 vs 迭代求解(贝尔曼期望方程)
    # ------------------------------------------
    print("-" * 60)
    print("  对比1:贝尔曼期望方程 V^π(s) 的两种求解方式")
    print("-" * 60)

    # 手工计算
    manual_V = manual_bellman_expectation()

    # 迭代计算
    iter_V, iter_history = policy_evaluation(uniform_policy)
    print("策略评估(迭代求解)结果:")
    for i in range(N_STATES):
        print(f"  V^π(s{i}) = {iter_V[i]:.6f}")
    print()

    # 对比
    print(">>> 对比结果:")
    print(f"  {'状态':<8s} {'手工计算':<15s} {'迭代求解':<15s} {'误差':<15s}")
    for i in range(N_STATES):
        error = abs(manual_V[i] - iter_V[i])
        print(f"  s{i:<6d} {manual_V[i]:<15.8f} {iter_V[i]:<15.8f} {error:<15.2e}")

    all_match = np.allclose(manual_V, iter_V, atol=1e-6)
    print(f"\n  结论:{'完全一致 ✓' if all_match else '存在差异 ✗'}")
    print(f"  (手工求解线性方程组 = 迭代法逐步收敛,殊途同归!)")
    print()

    # ------------------------------------------
    # 对比2:贝尔曼期望方程 vs 贝尔曼最优方程
    # ------------------------------------------
    print("-" * 60)
    print("  对比2:V^π(s) vs V*(s)")
    print("-" * 60)
    print()

    V_star, optimal_policy, vi_history = value_iteration()

    print("贝尔曼期望方程 V^π(s)(均匀随机策略下):")
    for i in range(N_STATES):
        print(f"  V^π(s{i}) = {iter_V[i]:.6f}")

    print()
    print("贝尔曼最优方程 V*(s)(最优策略下):")
    for i in range(N_STATES):
        print(f"  V*(s{i}) = {V_star[i]:.6f}")

    print()
    print("最优策略 π*:")
    action_names = ['a0', 'a1']
    for s in range(N_STATES):
        best = np.argmax(optimal_policy[s])
        print(f"  π*(s{s}) = {action_names[best]}")

    print()
    print(f"  {'状态':<8s} {'V^π(s) 随机策略':<20s} {'V*(s) 最优策略':<20s} {'提升':<10s}")
    for i in range(N_STATES):
        improvement = V_star[i] - iter_V[i]
        print(f"  s{i:<6d} {iter_V[i]:<20.6f} {V_star[i]:<20.6f} {improvement:>+10.6f}")

    print()
    print("  分析:V*(s) ≥ V^π(s) 对所有状态成立(最优策略不会更差)")

    # ------------------------------------------
    # 可视化:价值迭代收敛过程
    # ------------------------------------------
    visualize_convergence(iter_history, vi_history)


def visualize_convergence(expectation_history, optimal_history):
    """
    可视化价值迭代的收敛过程

    左图:策略评估(贝尔曼期望方程)的收敛过程
    右图:价值迭代(贝尔曼最优方程)的收敛过程
    """
    plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei']
    plt.rcParams['axes.unicode_minus'] = False

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # ------------------------------------------
    # 左图:贝尔曼期望方程收敛过程
    # ------------------------------------------
    ax1 = axes[0]
    history_arr = np.array(expectation_history)
    colors = ['#e74c3c', '#2ecc71', '#3498db']
    state_labels = ['V^π(s0)', 'V^π(s1)', 'V^π(s2)']

    for s in range(N_STATES):
        ax1.plot(history_arr[:, s], color=colors[s], label=state_labels[s], linewidth=2)

    ax1.set_xlabel('迭代次数', fontsize=11)
    ax1.set_ylabel('状态价值 V(s)', fontsize=11)
    ax1.set_title('策略评估收敛过程\n(贝尔曼期望方程)', fontsize=12)
    ax1.legend(fontsize=10)
    ax1.grid(True, alpha=0.3)

    # 只显示前 30 次迭代(后面的已经收敛)
    n_show = min(30, len(expectation_history))
    ax1.set_xlim(0, n_show)

    # ------------------------------------------
    # 右图:贝尔曼最优方程收敛过程
    # ------------------------------------------
    ax2 = axes[1]
    history_arr2 = np.array(optimal_history)

    state_labels_star = ['V*(s0)', 'V*(s1)', 'V*(s2)']
    for s in range(N_STATES):
        ax2.plot(history_arr2[:, s], color=colors[s], label=state_labels_star[s], linewidth=2)

    ax2.set_xlabel('迭代次数', fontsize=11)
    ax2.set_ylabel('状态价值 V(s)', fontsize=11)
    ax2.set_title('价值迭代收敛过程\n(贝尔曼最优方程)', fontsize=12)
    ax2.legend(fontsize=10)
    ax2.grid(True, alpha=0.3)

    n_show2 = min(30, len(optimal_history))
    ax2.set_xlim(0, n_show2)

    plt.tight_layout()
    plt.savefig('output/bellman_equation_verify_results.png', dpi=150, bbox_inches='tight')
    print("\n图表已保存至 output/bellman_equation_verify_results.png")
    plt.show()


# ==========================================
# 第六部分:逐步展示价值迭代过程
# ==========================================
def print_value_iteration_steps(n_steps=10):
    """
    打印价值迭代的前 n_steps 步,展示逐步收敛的过程

    这让读者能直观看到:
    - 初始时 V(s) = 0(什么都不知道)
    - 每一步,V(s) 通过贝尔曼方程更新,向真实值逼近
    - 经过足够多步后,V(s) 收敛到最优值
    """
    print()
    print("=" * 60)
    print("  价值迭代逐步收敛过程")
    print("=" * 60)
    print()
    print("  迭代  |  V*(s0)    V*(s1)    V*(s2)   |  最大变化")
    print("  " + "-" * 55)

    V = np.zeros(N_STATES)

    for iteration in range(n_steps):
        V_new = np.zeros(N_STATES)

        for s in range(N_STATES):
            q_values = []
            for a in range(N_ACTIONS):
                q = R[s][a]
                for next_s, prob in P[s][a].items():
                    q += GAMMA * prob * V[next_s]
                q_values.append(q)
            V_new[s] = max(q_values)

        delta = np.max(np.abs(V_new - V))

        print(f"  {iteration + 1:4d}  |"
              f"  {V_new[0]:>8.4f}  {V_new[1]:>8.4f}  {V_new[2]:>8.4f} |"
              f"  {delta:>8.6f}")

        V = V_new

        if delta < 1e-8:
            print(f"\n  在第 {iteration + 1} 步收敛!")
            break

    print("  " + "-" * 55)
    print(f"\n  最终最优状态价值函数:")
    for i in range(N_STATES):
        print(f"    V*(s{i}) = {V[i]:.6f}")

    # 打印最优策略
    print(f"\n  最优策略:")
    action_names = ['a0', 'a1']
    for s in range(N_STATES):
        q_values = []
        for a in range(N_ACTIONS):
            q = R[s][a]
            for next_s, prob in P[s][a].items():
                q += GAMMA * prob * V[next_s]
            q_values.append(q)
        best = np.argmax(q_values)
        print(f"    π*(s{s}) = {action_names[best]}  "
              f"(Q(s{s},a0)={q_values[0]:.4f}, Q(s{s},a1)={q_values[1]:.4f})")


# ==========================================
# 主程序
# ==========================================
def main():
    """主函数:运行所有验证实验"""

    # 1. 验证手工计算与迭代计算的一致性
    verify_results()

    # 2. 展示价值迭代逐步收敛
    print_value_iteration_steps(n_steps=20)


if __name__ == "__main__":
    main()
python 复制代码
============================================================
  贝尔曼方程数值验证
============================================================

------------------------------------------------------------
  对比1:贝尔曼期望方程 V^π(s) 的两种求解方式
------------------------------------------------------------
============================================================
  贝尔曼期望方程 ------ 手工推导
============================================================

给定策略:均匀随机 π(a|s) = 0.5
折扣因子:γ = 0.9

列方程组(v0 = V^π(s0), v1 = V^π(s1), v2 = V^π(s2)):
  v0 = 1.5   + 0.45*v1 + 0.45*v2  ...... (方程1)
  v1 = -0.5  + 0.225*v0 + 0.36*v1 + 0.315*v2  (方程2)
  v2 = 2.0   + 0.45*v1 + 0.45*v2  ...... (方程3)

手工求解线性方程组得到:
  V^π(s0) = 8.714450
  V^π(s1) = 6.817661
  V^π(s2) = 9.214450

策略评估(迭代求解)结果:
  V^π(s0) = 8.714449
  V^π(s1) = 6.817660
  V^π(s2) = 9.214449

>>> 对比结果:
  状态       手工计算            迭代求解            误差             
  s0      8.71444954      8.71444945      8.81e-08       
  s1      6.81766055      6.81766046      8.81e-08       
  s2      9.21444954      9.21444945      8.81e-08       

  结论:完全一致 ✓
  (手工求解线性方程组 = 迭代法逐步收敛,殊途同归!)

------------------------------------------------------------
  对比2:V^π(s) vs V*(s)
------------------------------------------------------------

贝尔曼期望方程 V^π(s)(均匀随机策略下):
  V^π(s0) = 8.714449
  V^π(s1) = 6.817660
  V^π(s2) = 9.214449

贝尔曼最优方程 V*(s)(最优策略下):
  V*(s0) = 13.362256
  V*(s1) = 10.694143
  V*(s2) = 12.624729

最优策略 π*:
  π*(s0) = a1
  π*(s1) = a0
  π*(s2) = a0

  状态       V^π(s) 随机策略          V*(s) 最优策略           提升        
  s0      8.714449             13.362256             +4.647806
  s1      6.817660             10.694143             +3.876483
  s2      9.214449             12.624729             +3.410279

  分析:V*(s) ≥ V^π(s) 对所有状态成立(最优策略不会更差)

图表已保存至 output/bellman_equation_verify_results.png

============================================================
  价值迭代逐步收敛过程
============================================================

  迭代  |  V*(s0)    V*(s1)    V*(s2)   |  最大变化
  -------------------------------------------------------
     1  |    2.0000    0.0000    3.0000 |  3.000000
     2  |    4.7000    1.2500    3.7000 |  2.700000
     3  |    5.3300    2.7800    4.3300 |  1.530000
     4  |    5.8970    3.3470    5.5020 |  1.172000
     5  |    6.9518    4.1296    6.0123 |  1.054800
     6  |    7.4111    4.8338    6.7166 |  0.704295
     7  |    8.0449    5.3574    7.3505 |  0.633866
     8  |    8.6154    5.9279    7.8217 |  0.570479
     9  |    9.0395    6.3967    8.3351 |  0.513431
    10  |    9.5016    6.8186    8.7570 |  0.462088
    11  |    9.8813    7.2164    9.1367 |  0.397794
    12  |   10.2231    7.5581    9.4948 |  0.358014
    13  |   10.5453    7.8730    9.8023 |  0.322213
    14  |   10.8221    8.1564   10.0857 |  0.283399
    15  |   11.0771    8.4085   10.3408 |  0.255059
    16  |   11.3067    8.6381   10.5677 |  0.229553
    17  |   11.5109    8.8435   10.7743 |  0.206598
    18  |   11.6968    9.0283   10.9591 |  0.185938
    19  |   11.8632    9.1952   11.1255 |  0.166858
    20  |   12.0129    9.3449   11.2757 |  0.150172
  -------------------------------------------------------

  最终最优状态价值函数:
    V*(s0) = 12.012939
    V*(s1) = 9.344911
    V*(s2) = 11.275659

  最优策略:
    π*(s0) = a1  (Q(s0,a0)=9.4104, Q(s0,a1)=12.1481)
    π*(s1) = a0  (Q(s1,a0)=9.4799, Q(s1,a1)=8.7580)
    π*(s2) = a0  (Q(s2,a0)=11.4104, Q(s2,a1)=11.1481)

Q-learning

Q-Learning 的核心更新公式(贝尔曼最优方程的迭代形式):

Q ( s , a ) ← Q ( s , a ) + α ∗ r + γ ∗ max ⁡ a ′ Q ( s ′ , a ′ ) − Q ( s , a ) Q(s, a) ← Q(s, a) + α * r + γ \*\\max_{a'} Q(s', a') - Q(s, a) Q(s,a)←Q(s,a)+α∗r+γ∗a′maxQ(s′,a′)−Q(s,a)

复制代码
其中:
    - s: 当前状态
    - a: 当前动作
    - r: 获得的奖励
    - s': 下一个状态
    - α: 学习率(控制更新步长)
    - γ: 折扣因子(未来奖励的重要程度)
    - max_a' Q(s', a'): 下一个状态的最大 Q 值
python 复制代码
"""
第3章:4×4 GridWorld Q-Learning 实验
在网格世界中学习最优路径,直观理解 Q 值和贝尔曼方程

运行方式:
    python gridworld_q_learning.py
"""

import os
import numpy as np
import matplotlib.pyplot as plt

# 创建输出目录
os.makedirs("output", exist_ok=True)


# ==========================================
# 第一部分:GridWorld 环境
# ==========================================
class GridWorld:
    """
    4×4 网格世界环境

    网格布局(4行4列):
        0,0  0,1  0,2  0,3
        1,0  1,1  1,2  1,3
        2,0  2,1  2,2  2,3
        3,0  3,1  3,2  3,3

    - 起点:(0, 0)
    - 终点:(3, 3),到达获得 +10 奖励
    - 障碍物:(1, 1) 和 (2, 2),撞到获得 -5 惩罚
    - 每走一步:-1 奖励(鼓励尽快到达终点)
    - 撞墙:-5 奖励(位置不变)

    动作空间:
        0 = 上 (↑), 1 = 下 (↓), 2 = 左 (←), 3 = 右 (→)
    """

    def __init__(self):
        self.rows = 4
        self.cols = 4
        self.start = (0, 0)
        self.goal = (3, 3)
        self.obstacles = [(1, 1), (2, 2)]
        self.n_actions = 4  # 上、下、左、右
        self.action_names = ['上(↑)', '下(↓)', '左(←)', '右(→)']
        self.reset()

    def reset(self):
        """重置环境到起点,返回初始状态"""
        self.agent_pos = self.start
        return self.agent_pos

    def step(self, action):
        """
        执行动作,返回 (下一状态, 奖励, 是否结束)

        动作映射:
            0 = 上 → 行 -1
            1 = 下 → 行 +1
            2 = 左 → 列 -1
            3 = 右 → 列 +1
        """
        row, col = self.agent_pos

        # 根据动作计算新位置
        if action == 0:    # 上
            new_pos = (row - 1, col)
        elif action == 1:  # 下
            new_pos = (row + 1, col)
        elif action == 2:  # 左
            new_pos = (row, col - 1)
        elif action == 3:  # 右
            new_pos = (row, col + 1)
        else:
            raise ValueError(f"无效动作: {action}")

        # 检查是否撞墙(出界)
        new_row, new_col = new_pos
        if new_row < 0 or new_row >= self.rows or new_col < 0 or new_col >= self.cols:
            # 撞墙:位置不变,给予惩罚
            return self.agent_pos, -5, False

        # 检查是否撞到障碍物
        if new_pos in self.obstacles:
            # 撞障碍物:位置不变,给予惩罚
            return self.agent_pos, -5, False

        # 合法移动:更新位置
        self.agent_pos = new_pos

        # 检查是否到达终点
        if self.agent_pos == self.goal:
            return self.agent_pos, 10, True  # 到达终点,+10 奖励

        # 普通移动:-1 奖励(鼓励尽快到达)
        return self.agent_pos, -1, False


# ==========================================
# 第二部分:Q-Learning 算法
# ==========================================
def epsilon_greedy(Q, state, epsilon, n_actions):
    """
    ε-贪心动作选择策略

    以 ε 的概率随机探索,以 1-ε 的概率选择当前 Q 值最大的动作。
    这是 Q-Learning 中平衡"探索"与"利用"的标准方法。
    """
    if np.random.random() < epsilon:
        return np.random.randint(n_actions)  # 探索:随机选动作
    else:
        return np.argmax(Q[state])  # 利用:选 Q 值最大的动作


def train_q_learning(env, n_episodes=500, alpha=0.1, gamma=0.95,
                     epsilon_start=1.0, epsilon_end=0.01, epsilon_decay=0.995):
    """
    Q-Learning 训练

    Q-Learning 的核心更新公式(贝尔曼最优方程的迭代形式):
        Q(s, a) ← Q(s, a) + α * [r + γ * max_a' Q(s', a') - Q(s, a)]

    其中:
        - s: 当前状态
        - a: 当前动作
        - r: 获得的奖励
        - s': 下一个状态
        - α: 学习率(控制更新步长)
        - γ: 折扣因子(未来奖励的重要程度)
        - max_a' Q(s', a'): 下一个状态的最大 Q 值

    参数:
        n_episodes: 训练回合数
        alpha: 学习率
        gamma: 折扣因子
        epsilon_start: 初始探索率
        epsilon_end: 最低探索率
        epsilon_decay: 探索率衰减因子
    """
    # 初始化 Q 表:所有 Q 值设为 0
    # Q[state][action] = 估计的最优动作价值
    Q = np.zeros((env.rows, env.cols, env.n_actions))

    # 记录训练过程中的数据
    episode_rewards = []  # 每回合的累计奖励
    episode_steps = []    # 每回合的步数
    epsilon = epsilon_start

    print("=" * 60)
    print("  Q-Learning 训练")
    print("=" * 60)
    print(f"  学习率 α = {alpha}")
    print(f"  折扣因子 γ = {gamma}")
    print(f"  初始探索率 ε = {epsilon_start}")
    print(f"  训练回合数 = {n_episodes}")
    print("-" * 60)

    for episode in range(n_episodes):
        state = env.reset()
        total_reward = 0
        steps = 0
        done = False

        while not done:
            # 1. 用 ε-贪心策略选择动作
            action = epsilon_greedy(Q, state, epsilon, env.n_actions)

            # 2. 执行动作,观察奖励和下一状态
            next_state, reward, done = env.step(action)

            # 3. Q-Learning 更新(核心公式)
            #    注意:这里用的是 max_a' Q(s', a'),不关心实际采取了什么策略
            #    这就是 Q-Learning "off-policy" 的特点
            best_next_q = np.max(Q[next_state])
            td_target = reward + gamma * best_next_q
            td_error = td_target - Q[state][action]
            Q[state][action] += alpha * td_error

            # 4. 转移到下一状态
            state = next_state
            total_reward += reward
            steps += 1

            # 安全阀:防止无限循环
            if steps > 200:
                break

        # 衰减探索率
        epsilon = max(epsilon_end, epsilon * epsilon_decay)

        episode_rewards.append(total_reward)
        episode_steps.append(steps)

        # 每 100 回合打印一次进度
        if (episode + 1) % 100 == 0:
            avg_reward = np.mean(episode_rewards[-100:])
            avg_steps = np.mean(episode_steps[-100:])
            print(f"  回合 {episode + 1:4d} | "
                  f"近100回合平均奖励: {avg_reward:7.2f} | "
                  f"平均步数: {avg_steps:5.1f} | "
                  f"ε: {epsilon:.4f}")

    print("-" * 60)
    return Q, episode_rewards, episode_steps


# ==========================================
# 第三部分:结果可视化
# ==========================================
def print_q_table(Q, env):
    """
    打印格式化的 Q 表

    Q 表展示了每个状态下每个动作的 Q 值(估计的最优动作价值)。
    Q 值越高,说明在该状态下执行该动作的预期累计奖励越大。
    """
    print("\n" + "=" * 60)
    print("  最终 Q 表")
    print("=" * 60)
    print(f"{'状态':<10s}", end="")
    for name in env.action_names:
        print(f"{name:<12s}", end="")
    print(f"{'最优动作':<12s}")
    print("-" * 60)

    for r in range(env.rows):
        for c in range(env.cols):
            state = (r, c)
            if state in env.obstacles:
                print(f"({r},{c}) 障碍  ", end="")
                print("    ---      ---      ---      ---     障碍物")
                continue
            if state == env.goal:
                print(f"({r},{c}) 终点  ", end="")
                print("    ---      ---      ---      ---     终点")
                continue

            print(f"({r},{c})       ", end="")
            for a in range(env.n_actions):
                print(f"{Q[r][c][a]:>8.2f}   ", end="")
            best_action = np.argmax(Q[r][c])
            print(f"  {env.action_names[best_action]}")

    print("-" * 60)


def extract_optimal_path(Q, env):
    """
    从 Q 表中提取最优路径

    在每个状态选择 Q 值最大的动作,根据网格规则计算下一状态。
    不依赖环境的 step() 函数,避免环境状态被意外修改。
    """
    # 动作对应的位移:0=上, 1=下, 2=左, 3=右
    deltas = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}

    state = env.start
    path = [state]
    visited = set()

    while state != env.goal:
        if state in visited:
            break  # 防止死循环
        visited.add(state)
        action = np.argmax(Q[state])
        dr, dc = deltas[action]
        new_state = (state[0] + dr, state[1] + dc)

        # 检查新位置是否合法(不越界、不是障碍物)
        if (0 <= new_state[0] < env.rows and 0 <= new_state[1] < env.cols
                and new_state not in env.obstacles):
            state = new_state
        # 如果越界或撞障碍物,状态不变(可能导致死循环,由 visited 保护)
        path.append(state)
        if state == env.goal:
            break

    return path


def visualize_results(Q, episode_rewards, env):
    """
    可视化 Q-Learning 的学习结果
    - 图1:每个动作的 Q 值热力图
    - 图2:最优路径在网格上的展示
    - 图3:每回合累计奖励的变化曲线
    """
    plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei']
    plt.rcParams['axes.unicode_minus'] = False

    fig = plt.figure(figsize=(16, 12))

    # ------------------------------------------
    # 图1:四个动作的 Q 值热力图
    # ------------------------------------------
    action_names_short = ['上(↑)', '下(↓)', '左(←)', '右(→)']

    for i in range(4):
        ax = fig.add_subplot(2, 3, i + 1)
        q_values = Q[:, :, i]  # 取出某个动作在所有状态的 Q 值

        im = ax.imshow(q_values, cmap='RdYlGn', aspect='equal')
        # 在每个格子上标注 Q 值
        for r in range(env.rows):
            for c in range(env.cols):
                if (r, c) in env.obstacles:
                    ax.text(c, r, 'X', ha='center', va='center',
                            fontsize=14, fontweight='bold', color='black')
                elif (r, c) == env.goal:
                    ax.text(c, r, 'G', ha='center', va='center',
                            fontsize=14, fontweight='bold', color='blue')
                else:
                    ax.text(c, r, f'{q_values[r, c]:.1f}', ha='center', va='center',
                            fontsize=9)
        ax.set_title(f'Q(s, {action_names_short[i]})', fontsize=12)
        ax.set_xticks(range(env.cols))
        ax.set_yticks(range(env.rows))
        ax.set_xticklabels(range(env.cols))
        ax.set_yticklabels(range(env.rows))
        plt.colorbar(im, ax=ax, shrink=0.8)

    # ------------------------------------------
    # 图2:最优路径可视化
    # ------------------------------------------
    ax_path = fig.add_subplot(2, 3, 5)
    # 绘制网格底色
    grid = np.zeros((env.rows, env.cols))
    for obs in env.obstacles:
        grid[obs] = -1
    grid[env.goal] = 2

    ax_path.imshow(grid, cmap='Set3', aspect='equal', vmin=-2, vmax=3)

    # 提取并绘制最优路径
    path = extract_optimal_path(Q, env)
    path_rows = [p[0] for p in path]
    path_cols = [p[1] for p in path]
    ax_path.plot(path_cols, path_rows, 'b-o', linewidth=2.5, markersize=10)

    # 标注起点、终点、障碍物
    ax_path.text(0, 0, 'S', ha='center', va='center', fontsize=16,
                 fontweight='bold', color='green')
    ax_path.text(3, 3, 'G', ha='center', va='center', fontsize=16,
                 fontweight='bold', color='red')
    for obs in env.obstacles:
        ax_path.text(obs[1], obs[0], 'X', ha='center', va='center',
                     fontsize=16, fontweight='bold', color='black')

    # 在路径上标注步数
    for idx, (r, c) in enumerate(path):
        ax_path.text(c, r, str(idx), ha='center', va='center',
                     fontsize=8, color='white',
                     bbox=dict(boxstyle='round,pad=0.2', fc='blue', alpha=0.5))

    ax_path.set_title('最优路径', fontsize=12)
    ax_path.set_xticks(range(env.cols))
    ax_path.set_yticks(range(env.rows))
    ax_path.set_xticklabels(range(env.cols))
    ax_path.set_yticklabels(range(env.rows))
    ax_path.grid(True, alpha=0.3)

    # ------------------------------------------
    # 图3:训练奖励曲线
    # ------------------------------------------
    ax_reward = fig.add_subplot(2, 3, 6)
    ax_reward.plot(episode_rewards, alpha=0.3, color='lightblue', label='单回合奖励')
    # 计算滑动平均
    window = 20
    if len(episode_rewards) >= window:
        moving_avg = np.convolve(episode_rewards,
                                 np.ones(window) / window, mode='valid')
        ax_reward.plot(range(window - 1, len(episode_rewards)),
                       moving_avg, color='blue', linewidth=2,
                       label=f'{window}回合滑动平均')
    ax_reward.set_xlabel('回合', fontsize=11)
    ax_reward.set_ylabel('累计奖励', fontsize=11)
    ax_reward.set_title('训练奖励曲线', fontsize=12)
    ax_reward.legend(fontsize=9)
    ax_reward.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('output/gridworld_q_learning_results.png', dpi=150, bbox_inches='tight')
    print("\n图表已保存至 output/gridworld_q_learning_results.png")
    plt.show()


# ==========================================
# 第四部分:主程序
# ==========================================
def main():
    """主函数:创建环境 → 训练 → 打印 Q 表 → 可视化"""

    # 创建 GridWorld 环境
    env = GridWorld()
    print("GridWorld 环境创建完成")
    print(f"  起点: {env.start}")
    print(f"  终点: {env.goal}")
    print(f"  障碍物: {env.obstacles}")
    print(f"  动作空间: {env.action_names}")

    # 训练 Q-Learning
    Q, episode_rewards, episode_steps = train_q_learning(env, n_episodes=500)

    # 打印最终 Q 表
    print_q_table(Q, env)

    # 提取并打印最优路径
    path = extract_optimal_path(Q, env)
    print(f"\n最优路径: {' → '.join([str(p) for p in path])}")
    print(f"路径长度: {len(path) - 1} 步")

    # 计算最优路径的总奖励
    total_r = 0
    for i in range(len(path) - 1):
        if i < len(path) - 2:
            total_r += -1  # 普通步骤:-1
        else:
            total_r += 10  # 到达终点的最后一步:+10
    print(f"最优路径总奖励: {total_r}")

    # 可视化结果
    visualize_results(Q, episode_rewards, env)


if __name__ == "__main__":
    main()
python 复制代码
GridWorld 环境创建完成
  起点: (0, 0)
  终点: (3, 3)
  障碍物: [(1, 1), (2, 2)]
  动作空间: ['上(↑)', '下(↓)', '左(←)', '右(→)']
============================================================
  Q-Learning 训练
============================================================
  学习率 α = 0.1
  折扣因子 γ = 0.95
  初始探索率 ε = 1.0
  训练回合数 = 500
------------------------------------------------------------
  回合  100 | 近100回合平均奖励:  -53.89 | 平均步数:  26.4 | ε: 0.6058
  回合  200 | 近100回合平均奖励:   -9.74 | 平均步数:  11.1 | ε: 0.3670
  回合  300 | 近100回合平均奖励:   -0.70 | 平均步数:   7.9 | ε: 0.2223
  回合  400 | 近100回合平均奖励:    0.67 | 平均步数:   7.6 | ε: 0.1347
  回合  500 | 近100回合平均奖励:    3.44 | 平均步数:   6.6 | ε: 0.0816
------------------------------------------------------------

============================================================
  最终 Q 表
============================================================
状态        上(↑)        下(↓)        左(←)        右(→)        最优动作        
------------------------------------------------------------
(0,0)          -2.01       1.09      -1.99       3.21     右(→)
(0,1)          -0.86      -0.84       1.97       4.44     右(→)
(0,2)           0.35       5.66       3.14       5.72     右(→)
(0,3)           1.68       7.07       4.37       1.70     下(↓)
(1,0)          -0.59       2.98      -5.10      -5.86     下(↓)
(1,1) 障碍      ---      ---      ---      ---     障碍物
(1,2)           3.32       0.64       0.35       7.07     右(→)
(1,3)           5.70       8.50       5.64       3.05     下(↓)
(2,0)          -1.31       5.02      -3.85       1.42     下(↓)
(2,1)          -3.76       5.40      -1.29      -3.26     下(↓)
(2,2) 障碍      ---      ---      ---      ---     障碍物
(2,3)           6.98      10.00       4.49       4.47     下(↓)
(3,0)          -0.32      -2.33      -2.27       6.82     右(→)
(3,1)           1.22       1.35       2.55       8.43     右(→)
(3,2)           3.13       2.69       4.10       9.99     右(→)
(3,3) 终点      ---      ---      ---      ---     终点
------------------------------------------------------------

最优路径: (0, 0) → (0, 1) → (0, 2) → (0, 3) → (1, 3) → (2, 3) → (3, 3)
路径长度: 6 步
最优路径总奖励: 5

图表已保存至 output/gridworld_q_learning_results.png
相关推荐
颜酱15 小时前
12 | 组装 SQL 生成上下文
人工智能·python
上海云盾-小余16 小时前
CC 高频请求 + DDoS 流量攻击实战防御:全链路 WAF 流量清洗部署完整流程
网络·爬虫·安全·小程序·ddos
The_Ticker16 小时前
大宗商品行情API接入教程
开发语言·python·websocket·程序人生·区块链
用户83562907805116 小时前
如何使用 Python 合并多个 Excel 文件
后端·python
颜酱16 小时前
11 | 合并过滤召回结果 - 确定需要的表、字段和指标
人工智能·python
Infedium16 小时前
Qt嵌入式开发核心亮点:线程安全硬件交互技术
开发语言·qt
阿里嘎多学长16 小时前
2026-07-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
circuitsosk16 小时前
AI系统的效果评测体系:从离线指标到线上业务反馈闭环
人工智能·python
gugucoding16 小时前
24. 【Java】枚举:更安全的常量
java·开发语言
一只旭宝17 小时前
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析
开发语言·c++·面试