强化学习极简实战:从手写环境到 Gymnasium 策略迭代 (Policy Iteration)

强化学习极简实战:从手写环境到 Gymnasium 策略迭代 (Policy Iteration)

导读 :强化学习初学者往往容易被两件事卡住:一是面对 Gym 框架时不知道它底层是怎么和数学公式关联的;二是不知道策略评估和策略提升这两套循环到底在算什么。

本教程以经典的 冰湖环境 (FrozenLake-4x4) 为例,带你彻底穿透 Gymnasium 封装的本质,手把手掌握基于模型的动态规划核心算法------策略迭代(Policy Iteration)


🎬 最终实机运行效果

经过策略迭代求解后,智能体大脑中形成最优策略矩阵,在冰湖地图上自动避开所有陷阱并以最短路径(6步)直达终点:


目录

  • [一、环境建模的两种范式:手写 move() vs Gymnasium 的 env.P](#一、环境建模的两种范式:手写 move() vs Gymnasium 的 env.P)
  • 二、策略迭代核心算法(数学推导与代码实现)
    • [1. 广义策略迭代 (GPI) 双循环闭环](#1. 广义策略迭代 (GPI) 双循环闭环)
    • [2. 策略评估 (Policy Evaluation) 与贝尔曼期望方程](#2. 策略评估 (Policy Evaluation) 与贝尔曼期望方程)
    • [3. 策略提升 (Policy Improvement) 与均分概率](#3. 策略提升 (Policy Improvement) 与均分概率)
  • 三、从"离线算脑"到"实机表演"
    • [1. 训练结果收敛可视化](#1. 训练结果收敛可视化)
    • [2. 深度理解加权随机抽样 np.random.choice](#2. 深度理解加权随机抽样 np.random.choice)
  • [四、框架进阶:从 Jupyter 历史包袱到 Gymnasium 原生极简渲染](#四、框架进阶:从 Jupyter 历史包袱到 Gymnasium 原生极简渲染)
  • 五、完整工程代码结构与运行指南

一、环境建模的两种范式:手写 move() vs Gymnasium 的 env.P

强化学习解决的是马尔可夫决策过程 (MDP) ,其数学五元组为 ( S , A , P , R , γ ) (S, A, P, R, \gamma) (S,A,P,R,γ)。

对于 4×4 的格子世界,状态 S ∈ { 0 , ... , 15 } S \in \{0, \dots, 15\} S∈{0,...,15},动作 A ∈ { 0 : 左 , 1 : 下 , 2 : 右 , 3 : 上 } A \in \{0:\text{左}, 1:\text{下}, 2:\text{右}, 3:\text{上}\} A∈{0:左,1:下,2:右,3:上}。

1. 传统手写环境的思路:单步动态推算

如果我们不借助任何框架,纯手写一个物理规则函数,通常会写一个 move(row, col, action)

python 复制代码
def move(row, col, action):
    # 如果已经掉入陷阱或到达终点,无法再行动
    if get_state(row, col) in ['trap', 'terminal']:
        return row, col, 0

    # 根据动作改变坐标
    if action == 0: row -= 1  # 上
    if action == 1: row += 1  # 下
    if action == 2: col -= 1  # 左
    if action == 3: col += 1  # 右

    # 不允许走出地图外面 (边界撞墙截断)
    row = max(0, min(3, row))
    col = max(0, min(3, col))

    # 奖励机制:每走一步扣1分,掉入陷阱扣100分
    reward = -1
    if get_state(row, col) == 'trap':
        reward = -100

    return row, col, reward

2. Gymnasium 的封装思想:全局离线转移查找表 (env.P)

Gymnasium 通过 env.unwrapped.P 直接向算法暴露了环境的动力学模型(Transition Dynamics)

它不是在运行时一步一步去 move,而是在初始化时,就已经把 16 个格子 × 4 种动作可能产生的全部情况都预先计算好,保存为一个嵌套字典:

python 复制代码
env.P[state][action] -> [(prop, next_state, reward, over), ...]

每个元组包含 4 个元素:

  • prop (Probability) :状态转移概率 P ( s ′ ∣ s , a ) P(s' \mid s, a) P(s′∣s,a)。当设置 is_slippery=False 时,不打滑,概率为 1.0 1.0 1.0;若打滑,则目标方向及垂直两侧各为 1 3 \frac{1}{3} 31。
  • next_state :执行动作后的下一个状态编号 ( 0 ∼ 15 0 \sim 15 0∼15)。
  • reward:到达下一个状态拿到的即时奖励。
  • over (terminated) :是否进入终止状态(掉进冰洞 H 或到达终点 G)。

💡 核心认知差异:为什么 Gymnasium 不需要每步扣分?

  • 手写 move 的逻辑 :每走一步 reward = -1,通过负反馈逼迫算法快点到达终点;
  • Gymnasium 的逻辑 :普通格子和陷阱 reward = 0.0,终点 reward = 1.0
    • 为什么智能体不会在原地兜圈? 关键在于折扣因子 γ = 0.9 \gamma = 0.9 γ=0.9 !第 1 步到终点拿 1.0 1.0 1.0 分,第 2 步才到就变成 1.0 × 0.9 = 0.9 1.0 \times 0.9 = 0.9 1.0×0.9=0.9 分,第 3 步就是 0.81 0.81 0.81 分......折扣机制天然惩罚了多余步数,驱动算法自动寻找最短路径。

二、策略迭代核心算法(数学推导与代码实现)

1. 广义策略迭代 (GPI) 双循环闭环

策略迭代由策略评估策略提升交替执行,直至收敛:
#mermaid-svg-09kuxSB6yBxbjGIe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-09kuxSB6yBxbjGIe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-09kuxSB6yBxbjGIe .error-icon{fill:#552222;}#mermaid-svg-09kuxSB6yBxbjGIe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-09kuxSB6yBxbjGIe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-09kuxSB6yBxbjGIe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-09kuxSB6yBxbjGIe .marker.cross{stroke:#333333;}#mermaid-svg-09kuxSB6yBxbjGIe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-09kuxSB6yBxbjGIe p{margin:0;}#mermaid-svg-09kuxSB6yBxbjGIe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-09kuxSB6yBxbjGIe .cluster-label text{fill:#333;}#mermaid-svg-09kuxSB6yBxbjGIe .cluster-label span{color:#333;}#mermaid-svg-09kuxSB6yBxbjGIe .cluster-label span p{background-color:transparent;}#mermaid-svg-09kuxSB6yBxbjGIe .label text,#mermaid-svg-09kuxSB6yBxbjGIe span{fill:#333;color:#333;}#mermaid-svg-09kuxSB6yBxbjGIe .node rect,#mermaid-svg-09kuxSB6yBxbjGIe .node circle,#mermaid-svg-09kuxSB6yBxbjGIe .node ellipse,#mermaid-svg-09kuxSB6yBxbjGIe .node polygon,#mermaid-svg-09kuxSB6yBxbjGIe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-09kuxSB6yBxbjGIe .rough-node .label text,#mermaid-svg-09kuxSB6yBxbjGIe .node .label text,#mermaid-svg-09kuxSB6yBxbjGIe .image-shape .label,#mermaid-svg-09kuxSB6yBxbjGIe .icon-shape .label{text-anchor:middle;}#mermaid-svg-09kuxSB6yBxbjGIe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-09kuxSB6yBxbjGIe .rough-node .label,#mermaid-svg-09kuxSB6yBxbjGIe .node .label,#mermaid-svg-09kuxSB6yBxbjGIe .image-shape .label,#mermaid-svg-09kuxSB6yBxbjGIe .icon-shape .label{text-align:center;}#mermaid-svg-09kuxSB6yBxbjGIe .node.clickable{cursor:pointer;}#mermaid-svg-09kuxSB6yBxbjGIe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-09kuxSB6yBxbjGIe .arrowheadPath{fill:#333333;}#mermaid-svg-09kuxSB6yBxbjGIe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-09kuxSB6yBxbjGIe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-09kuxSB6yBxbjGIe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-09kuxSB6yBxbjGIe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-09kuxSB6yBxbjGIe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-09kuxSB6yBxbjGIe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-09kuxSB6yBxbjGIe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-09kuxSB6yBxbjGIe .cluster text{fill:#333;}#mermaid-svg-09kuxSB6yBxbjGIe .cluster span{color:#333;}#mermaid-svg-09kuxSB6yBxbjGIe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-09kuxSB6yBxbjGIe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-09kuxSB6yBxbjGIe rect.text{fill:none;stroke-width:0;}#mermaid-svg-09kuxSB6yBxbjGIe .icon-shape,#mermaid-svg-09kuxSB6yBxbjGIe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-09kuxSB6yBxbjGIe .icon-shape p,#mermaid-svg-09kuxSB6yBxbjGIe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-09kuxSB6yBxbjGIe .icon-shape .label rect,#mermaid-svg-09kuxSB6yBxbjGIe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-09kuxSB6yBxbjGIe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-09kuxSB6yBxbjGIe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-09kuxSB6yBxbjGIe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 计算得到当前真实价值 V(s)
贪婪更新策略 π


初始策略 π₀ (各动作概率0.25)
策略评估 Policy Evaluation
策略提升 Policy Improvement
策略是否稳定?
最优策略 π* 与最优价值 V*


2. 策略评估 (Policy Evaluation) 与贝尔曼期望方程

数学原理 :在当前策略 π \pi π 下,计算每个状态 s s s 的长期折现价值 V ( s ) V(s) V(s):

V k + 1 ( s ) = ∑ a ∈ A π ( a ∣ s ) ∑ s ′ P ( s ′ ∣ s , a ) R ( s , a , s ′ ) + γ V k ( s ′ ) V_{k+1}(s) = \sum_{a \in A} \pi(a \mid s) \sum_{s'} P(s' \mid s, a) \left R(s, a, s') + \\gamma V_k(s') \\right Vk+1(s)=a∈A∑π(a∣s)s′∑P(s′∣s,a)R(s,a,s′)+γVk(s′)

其中动作价值函数(Q函数)为:

Q ( s , a ) = ∑ s ′ P ( s ′ ∣ s , a ) R ( s , a , s ′ ) + γ V ( s ′ ) Q(s, a) = \sum_{s'} P(s' \mid s, a) \left R(s, a, s') + \\gamma V(s') \\right Q(s,a)=s′∑P(s′∣s,a)R(s,a,s′)+γV(s′)

代码实现 (common.py):
python 复制代码
# 计算在状态 state 执行 action 时的动作价值 Q(s, a)
def get_qsa(env, values, state, action):
    value = 0.0
    # 遍历该动作可能产生的所有结果
    for prop, next_state, reward, over in env.P[state][action]:
        # 贝尔曼折现项:gamma = 0.9
        next_value = values[next_state] * 0.9

        # 如果掉进陷阱或到达终点,终止状态未来没有后续收益
        if over:
            next_value = 0.0

        # Q(s, a) = 即时奖励 + 衰减的未来价值
        next_value += reward
        next_value *= prop
        value += next_value

    return value

# 全图状态价值更新
def get_values(env, values, pi, algorithm):
    new_values = np.zeros([16])
    for state in range(16):
        action_value = np.zeros(4)
        for action in range(4):
            action_value[action] = get_qsa(env, values, state, action)

        if algorithm == '策略迭代':
            # 按策略概率加权求期望:V(s) = sum(pi(a|s) * Q(s, a))
            new_values[state] = (action_value * pi[state]).sum()

        elif algorithm == '价值迭代':
            # 直接贪婪取最大值:V(s) = max_a Q(s, a)
            new_values[state] = action_value.max()

    return new_values

3. 策略提升 (Policy Improvement) 与均分概率

数学原理:基于评估出来的价值函数,贪婪更新策略:

π ′ ( s ) = arg ⁡ max ⁡ a Q ( s , a ) \pi'(s) = \arg\max_{a} Q(s, a) π′(s)=argamaxQ(s,a)

如果存在多个动作并列第一,则让这些动作均分概率

python 复制代码
def get_pi(env, values):
    new_pi = np.zeros([16, 4])
    for state in range(16):
        action_value = np.zeros(4)
        for action in range(4):
            action_value[action] = get_qsa(env, values, state, action)

        # 统计达到最大分数的动作数量
        count = (action_value == action_value.max()).sum()

        # 让并列第一的最优动作均分概率
        for action in range(4):
            if action_value[action] == action_value.max():
                new_pi[state, action] = 1 / count
            else:
                new_pi[state, action] = 0.0

    return new_pi

三、从"离线算脑"到"实机表演"

1. 训练结果收敛可视化

训练代码只需一个清晰的交替循环:

python 复制代码
for _ in range(10):
    for _ in range(100):
        values = get_values(env, values, pi, algorithm)
    pi = get_pi(env, values)

训练后得到的最优状态价值 V ( s ) V(s) V(s):

text 复制代码
[[0.5905  0.6561  0.729   0.6561]
 [0.6561  0.0000  0.8100  0.0000]
 [0.7290  0.8100  0.9000  0.0000]
 [0.0000  0.9000  1.0000  0.0000]]
  • 陷阱 H 全为 0.0
  • 终点 G(右下角)为 1.0
  • 距离终点越近,数值越高(以 0.9 0.9 0.9 逐步衰减)。

最优动作策略地图:

text 复制代码
↓ → ↓ ←
↓ ○ ↓ ○
→ ↓ ↓ ○
○ → → ❤️

2. 深度理解加权随机抽样 np.random.choice

在实机表演中,执行动作的代码为:

python 复制代码
action = np.random.choice(np.arange(4), size=1, p=pi[index])[0]
  • 当只有一个第一名时 :该动作概率为 1.0 1.0 1.0,加权抽样等同于 100 % 100\% 100% 必然触发,决定性执行;
  • 当存在多个并列第一时 (如起点 [0.0, 0.5, 0.5, 0.0],向下和向右同样优秀):
    • 若使用 np.argmax(),会死板地永远只选排在前面的"下";
    • 使用 np.random.choice,智能体会各以 50 % 50\% 50% 的概率灵活选择两条最优路径!

四、框架进阶:从 Jupyter 历史包袱到 Gymnasium 原生极简渲染

很多学习者容易困惑:为什么视频里用了 SDL_VIDEODRIVER='dummy',还用 matplotlibIPython.display 搞得极为繁琐?

  • 原因:Jupyter Notebook 在浏览器中运行,无法弹出操作系统的原生 GUI 窗口,所以被迫把画面转成图片数组并不断清屏重绘;
  • 现代化本地写法 :Gymnasium 内置 Pygame 桌面渲染,在本地开发只需设置 render_mode='human' ,在循环中显式调用 env.render(),即可实现极高帧率、零闪烁的原生游戏窗口渲染!

五、完整工程代码结构与运行指南

1. 文件结构

text 复制代码
强化学习/
├── common.py        # Qsa计算、策略评估、策略提升公共算法库
├── main.py          # 主入口:环境创建、模型训练、原生GUI实机表演
├── frozenlake.gif   # 实机运行录屏动画
└── README.md        # 完整技术教程文档

2. 快速运行

bash 复制代码
# 激活虚拟环境
.\.venv\Scripts\activate.bat

# 启动训练与 100 遍动画表演
python main.py

终端将输出最优价值矩阵与箭头策略地图,随后桌面将弹出 Pygame 窗口,丝滑自动表演智能体 100 次通关全过程!

相关推荐
nju_spy8 个月前
强化学习 -- 无导数随机优化算法玩俄罗斯方块Tetris(交叉熵方法CE + ADP近似动态规划CBMPI)
人工智能·强化学习·策略迭代·近似动态规划·交叉熵方法·价值函数近似·无导数优化
2401_8414956410 个月前
【强化学习】动态规划算法
人工智能·python·算法·动态规划·强化学习·策略迭代·价值迭代