强化学习极简实战:从手写环境到 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',还用 matplotlib 和 IPython.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 次通关全过程!