强化学习极简实战:从手写环境到 Gymnasium 策略迭代 (Policy Iteration)
导读:强化学习初学者往往容易被两件事卡住:一是面对 Gym 框架时不知道它底层是怎么和数学公式关联的;二是不知道策略评估和策略提升这两套循环到底在算什么。
本教程以经典的冰湖环境 (FrozenLake-4x4)为例,带你彻底穿透 Gymnasium 封装的本质,手把手掌握基于模型的动态规划核心算法——策略迭代(Policy Iteration)。
🎬 最终实机运行效果
经过策略迭代求解后,智能体大脑中形成最优策略矩阵,在冰湖地图上自动避开所有陷阱并以最短路径(6步)直达终点:
目录
- 一、环境建模的两种范式:手写 move() vs Gymnasium 的 env.P
- 二、策略迭代核心算法(数学推导与代码实现)
- 1. 广义策略迭代 (GPI) 双循环闭环
- 2. 策略评估 (Policy Evaluation) 与贝尔曼期望方程
- 3. 策略提升 (Policy Improvement) 与均分概率
- 三、从“离线算脑”到“实机表演”
- 1. 训练结果收敛可视化
- 2. 深度理解加权随机抽样 np.random.choice
- 四、框架进阶:从 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):
defmove(row,col,action):# 如果已经掉入陷阱或到达终点,无法再行动ifget_state(row,col)in['trap','terminal']:returnrow,col,0# 根据动作改变坐标ifaction==0:row-=1# 上ifaction==1:row+=1# 下ifaction==2:col-=1# 左ifaction==3:col+=1# 右# 不允许走出地图外面 (边界撞墙截断)row=max(0,min(3,row))col=max(0,min(3,col))# 奖励机制:每走一步扣1分,掉入陷阱扣100分reward=-1ifget_state(row,col)=='trap':reward=-100returnrow,col,reward2. Gymnasium 的封装思想:全局离线转移查找表 (env.P)
Gymnasium 通过env.unwrapped.P直接向算法暴露了环境的动力学模型(Transition Dynamics)。
它不是在运行时一步一步去move,而是在初始化时,就已经把 16 个格子 × 4 种动作可能产生的全部情况都预先计算好,保存为一个嵌套字典:
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.01.0;若打滑,则目标方向及垂直两侧各为1 3 \frac{1}{3}31。next_state:执行动作后的下一个状态编号 (0 ∼ 15 0 \sim 150∼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.01.0分,第 2 步才到就变成1.0 × 0.9 = 0.9 1.0 \times 0.9 = 0.91.0×0.9=0.9分,第 3 步就是0.81 0.810.81分……折扣机制天然惩罚了多余步数,驱动算法自动寻找最短路径。
二、策略迭代核心算法(数学推导与代码实现)
1. 广义策略迭代 (GPI) 双循环闭环
策略迭代由策略评估与策略提升交替执行,直至收敛:
2. 策略评估 (Policy Evaluation) 与贝尔曼期望方程
数学原理:在当前策略π \piπ下,计算每个状态s ss的长期折现价值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):
# 计算在状态 state 执行 action 时的动作价值 Q(s, a)defget_qsa(env,values,state,action):value=0.0# 遍历该动作可能产生的所有结果forprop,next_state,reward,overinenv.P[state][action]:# 贝尔曼折现项:gamma = 0.9next_value=values[next_state]*0.9# 如果掉进陷阱或到达终点,终止状态未来没有后续收益ifover:next_value=0.0# Q(s, a) = 即时奖励 + 衰减的未来价值next_value+=reward next_value*=prop value+=next_valuereturnvalue# 全图状态价值更新defget_values(env,values,pi,algorithm):new_values=np.zeros([16])forstateinrange(16):action_value=np.zeros(4)foractioninrange(4):action_value[action]=get_qsa(env,values,state,action)ifalgorithm=='策略迭代':# 按策略概率加权求期望:V(s) = sum(pi(a|s) * Q(s, a))new_values[state]=(action_value*pi[state]).sum()elifalgorithm=='价值迭代':# 直接贪婪取最大值:V(s) = max_a Q(s, a)new_values[state]=action_value.max()returnnew_values3. 策略提升 (Policy Improvement) 与均分概率
数学原理:基于评估出来的价值函数,贪婪更新策略:
π ′ ( s ) = arg max a Q ( s , a ) \pi'(s) = \arg\max_{a} Q(s, a)π′(s)=argamaxQ(s,a)
如果存在多个动作并列第一,则让这些动作均分概率:
defget_pi(env,values):new_pi=np.zeros([16,4])forstateinrange(16):action_value=np.zeros(4)foractioninrange(4):action_value[action]=get_qsa(env,values,state,action)# 统计达到最大分数的动作数量count=(action_value==action_value.max()).sum()# 让并列第一的最优动作均分概率foractioninrange(4):ifaction_value[action]==action_value.max():new_pi[state,action]=1/countelse:new_pi[state,action]=0.0returnnew_pi三、从“离线算脑”到“实机表演”
1. 训练结果收敛可视化
训练代码只需一个清晰的交替循环:
for_inrange(10):for_inrange(100):values=get_values(env,values,pi,algorithm)pi=get_pi(env,values)训练后得到的最优状态价值V ( s ) V(s)V(s):
[[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.90.9逐步衰减)。
最优动作策略地图:
↓ → ↓ ← ↓ ○ ↓ ○ → ↓ ↓ ○ ○ → → ❤️2. 深度理解加权随机抽样np.random.choice
在实机表演中,执行动作的代码为:
action=np.random.choice(np.arange(4),size=1,p=pi[index])[0]- 当只有一个第一名时:该动作概率为1.0 1.01.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. 文件结构
强化学习/ ├── common.py # Qsa计算、策略评估、策略提升公共算法库 ├── main.py # 主入口:环境创建、模型训练、原生GUI实机表演 ├── frozenlake.gif # 实机运行录屏动画 └── README.md # 完整技术教程文档2. 快速运行
# 激活虚拟环境.\.venv\Scripts\activate.bat# 启动训练与 100 遍动画表演python main.py终端将输出最优价值矩阵与箭头策略地图,随后桌面将弹出 Pygame 窗口,丝滑自动表演智能体 100 次通关全过程!