ML-For-Beginners 强化学习实战用 OpenAI Gym 与 Q-Learning 让智能体学会 CartPole 平衡【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners本篇指南取自 ML-For-Beginners 课程 8-Reinforcement/2-Gym本仓库对应译文见 translations/cs/8-Reinforcement/2-Gym/README.md的核心内容。在上一个课时中课程通过自建Board棋盘解决了Petr 与狼这一离散状态的 Q-Learning 问题本课则把同样的算法思想迁移到连续状态环境——使用 OpenAI Gym 的 CartPole-v1 模拟器让智能体学会在滑动小车上保持竖直杆平衡。读完本文你将掌握 Gym 环境的基本 APIreset/step/render与观测/动作空间、两种状态离散化方法、基于字典的 Q-Table 结构以及从零训练并可视化一个 Q-Learning 平衡智能体的完整代码路径。图Peter 和他的朋友为了逃出灰狼的追击而学会了滑冰出自本课 README。一、从离散棋盘到连续状态为什么要引入 Gym前一课解决的找苹果躲灰狼问题看似玩具但棋盘类游戏国际象棋、围棋本质与其相同都存在一块棋盘、明确的规则以及离散的状态。然而现实世界中的大量问题并不满足这种条件。本课要处理的是连续状态问题——状态由一个或多个实数描述例如杆的位置、速度、角度等。课程以Petr 学习滑冰保持平衡作为叙事场景如果 Petr 想逃过灰狼他必须学会移动得更快而保持平衡正是滑冰的第一步。为此我们使用其简化物理模型CartPole水平方向有一个可左右移动的滑块cart目标是在滑块顶部保持一根竖直杆pole的平衡时间越长越好。对 CartPole 的问题设定课程的 2-Gym README 给出了明确说明在 CartPole 世界中我们有一个可以向左或向右移动的水平滑块目标是在滑块顶部平衡一根垂直杆。在上一课里游戏规则和状态由我们自己定义的Board类承载见 8-Reinforcement/1-QLearning/rlboard.py 及 1-QLearning README。本课不再自行编写环境而是采用OpenAI Gym提供的专用模拟环境由它负责模拟平衡杆背后的物理规律。Gym 是训练强化学习算法最流行的模拟环境库之一既能模拟 CartPole也能模拟 Atari 游戏等多种环境。运行前提本课代码建议在本地 Python 环境如 Visual Studio Code中运行此时render()会弹出新的窗口显示模拟画面若在在线环境运行通常需要针对渲染方式做一些调整课程的 README 中对此有说明。本仓库中的可运行载体为 notebook.ipynb并提供了已填充代码的 solution/notebook.ipynb 供对照。二、环境初始化与空间认知2.1 安装与导入首先安装 Gym 并导入所需库对应 notebook 中的第 1 个代码块import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import random代码用!{sys.executable} -m pip install gym把包安装到当前正在运行 notebook 的 Python 解释器避免装到了错误的 Python这一经典问题。2.2 初始化 CartPole-v1 环境每个 Gym 环境都关联两类核心对象观测空间observation space定义了从环境获取信息的结构。CartPole 问题中我们获得杆的位置、速度等数值动作空间action space定义了可执行的动作集合。CartPole 的动作空间是离散的只有两个动作——向左与向右。初始化并查看空间对应第 2 个代码块与 notebook 中第 3 个单元格一致env gym.make(CartPole-v1) print(env.action_space) print(env.observation_space) print(env.action_space.sample())第 3 行env.action_space.sample()随机抽取一个动作用于验证动作空间的定义。2.3 随机策略下的 100 步仿真为了直观理解环境如何运转先运行一段 100 步的短仿真。每一步我们给定一个待执行的动作这里是从action_space随机选取第 3 个代码块env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close()推荐在本地 Python 上运行此代码以看到渲染窗口屏幕上会观察到杆很快倾倒类似下面这张动图所示的效果2.4 step 的返回值观测、奖励与 done 标志仿真过程中我们必须拿到观测才能决定如何行动。实际上step函数会返回四项内容当前观测obs、奖励函数值rew、done标志表示继续仿真是否还有意义以及附加信息info第 4 个代码块env.reset() done False while not done: env.render() obs, rew, done, info env.step(env.action_space.sample()) print(f{obs} - {rew}) env.close()在 notebook 输出中会看到类似这样的序列[ 0.03403272 -0.24301182 0.02669811 0.2895829 ] - 1.0 [ 0.02917248 -0.04828055 0.03248977 0.00543839] - 1.0 [ 0.02820687 0.14636075 0.03259854 -0.27681916] - 1.0 [ 0.03113408 0.34100283 0.02706215 -0.55904489] - 1.0 [ 0.03795414 0.53573468 0.01588125 -0.84308041] - 1.0 ... [ 0.17299878 0.15868546 -0.20754175 -0.55975453] - 1.0 [ 0.17617249 0.35602306 -0.21873684 -0.90998894] - 1.0每个 step 返回的观测向量长度为 4 的实数数组依次包含小车的位置cart position小车的速度cart velocity杆的角度pole angle杆的旋转速率pole rotation rate关于奖励可以发现每一步的奖励恒为 1。原因很简单——我们的目标就是存活得尽可能久即让杆在尽量长的时间内保持合理竖直。若查看这些数值的范围第 5 个代码块print(env.observation_space.low) print(env.observation_space.high)判定标准CartPole 仿真被认为解决当且仅当能在连续 100 次试验中获得平均奖励 195课程的 2-Gym README 给出了这条形式化标准。三、状态离散化把连续观测变成有限状态Q-Learning 需要一张 Q-Table用以定义在每个状态该做什么。这要求状态必须是离散的——即由有限个离散值构成。因此必须对连续观测做离散化把它映射到有限状态集合。3.1 两种离散化思路课程给出了两种做法见 2-Gym README 的 State discretization 小节划分区间bins如果我们知道某个值的取值范围就把该区间切成若干个bins再用该值所属 bin 的编号替换原值。可借助 numpy 的digitize实现。这种做法的好处是状态规模精确已知——它完全取决于我们为每个维度选取的 bin 数量。线性缩放后取整先把数值通过线性插值映射到某个有限区间例如 -2020再四舍五入转为整数。这种做法的缺点是对状态规模的控制较弱尤其当我们并不确知输入值的精确范围时。例如在本问题中4 个观测值里有 2 个本身没有明确的上下界若不加约束可能导致无限多状态。本课最终采用第二种思路。其依据是虽然这些值理论上无界但实际中它们极少超出某些有限区间因此极端状态出现的概率非常低可以放心离散化。3.2 量化式离散化函数 discretize下面这个函数接收环境的观测x产出由 4 个整数构成的元组第 6 个代码块def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))其思想是用一组合适的尺度因子0.25、0.25、0.01、0.1对四个观测分量分别做粗量化角度、速度等物理量的微小波动被折叠进同一个整数桶从而压缩状态空间。分母越小对该维度的分辨越细。3.3 基于 bins 的离散化函数 discretize_bins同时课程也演示了基于区间划分的另一种方案第 7 个代码块def create_bins(i,num): return np.arange(num1)*(i[1]-i[0])/numi[0] print(Sample bins for interval (-5,5) with 10 bins\n,create_bins((-5,5),10)) ints [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # intervals of values for each parameter nbins [20,20,10,10] # number of bins for each parameter bins [create_bins(ints[i],nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i],bins[i]) for i in range(4))这里create_bins在指定区间内生成等宽网格ints依次给出四个观测分量各自的经验区间小车位置与速度、杆角度与角速度nbins给出每个分量划分的份数20、20、10、10。discretize_bins则调用np.digitize把每个分量化归到对应 bin。两种方法的输出语义有区别discretize_bins返回的是0-based 的 bin 编号因此当某个输入分量取值在 0 附近时它返回的是区间中部的编号如 10而discretize没有刻意平移输出范围允许出现负整数0 依然对应 0状态值不被偏移。3.4 在短仿真中观察离散状态运行一小段随机仿真来打印离散化后的环境状态第 8 个代码块env.reset() done False while not done: #env.render() obs, rew, done, info env.step(env.action_space.sample()) #print(discretize_bins(obs)) print(discretize(obs)) env.close()如果希望看到环境实时画面取消注释以env.render()开头的那一行否则程序可在后台隐形执行速度更快。随后的 Q-Learning 训练将统一采用这种隐形执行模式对应源码实现可在 solution/notebook.ipynb 中逐格对照。四、Q-Table 的数据结构选择在上一课中状态只是 08 的一对整数因此 Q-Table 用形状为8x8x2的 numpy 张量表示非常方便。若采用 bins 离散化状态向量规模同样已知可以沿用张量方案把状态表示为形状20x20x10x10x2的数组——其中最后的 2 对应动作空间维度其余四个维度分别对应我们为观测空间每个分量选定的 bin 数量。但有些时候观测空间的精确维度并不确定例如discretize函数产生的状态因为原始值无界我们永远无法保证其落在特定范围内。因此课程改用更灵活的字典结构第 9 个代码块Q {} actions (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions]核心设计以(state, action)二元组作为字典Q的键键对应的值即该 (状态, 动作) 组合的 Q-Table 条目配套的qvalues(state)返回给定状态下、遍历所有可能动作actions (0, 1)得到的 Q 值列表若条目尚不存在dict.get的第二个参数0保证返回默认值 0——即未知状态下默认每个动作价值相同。字典方案天然适配未知边界的状态空间只有被真实访问过的 (状态, 动作) 对才会占据内存。这一取舍正是从上一课自建棋盘、张量 Q-Table到本课真实连续环境、字典 Q-Table的递进体现。五、Q-Learning 训练循环让 Petr 学会平衡5.1 超参数设定在开始学习前先设定超参数第 10 个代码块# hyperparameters alpha 0.3 gamma 0.9 epsilon 0.90三个超参数的含义课程 README 中有逐条解释alpha为学习率决定每一步应以多大程度修正 Q-Table 的当前值。上一课从 1 起步、训练中逐渐调低本课为简化起见保持恒定读者之后可以自行实验调整。gamma为折扣因子反映未来奖励相对当前奖励的优先程度。取值接近 1 意味着智能体更看重长期收益。epsilon为探索/利用因子决定算法在探索与利用之间的倾向。在本算法的实现中epsilon比例的情况下按 Q-Table 值选动作其余情况执行随机动作从而有机会触及此前从未见过的搜索空间区域。就平衡问题而言随机动作探索相当于朝错误方向胡乱打了一拳杆必须学会从这些失误中恢复平衡——这正是探索对学习有价值的直观原因。5.2 两个算法改进相比上一课的算法课程引入两个增强见 2-Gym README 的 Improve the algorithm 小节统计平均累积奖励每 5000 次迭代打印一次进度并把该窗口内的累积奖励做平均。若平均分超过 195就可以认为问题已解决且质量高于形式化要求。追踪最优快照Qmax与Qbest记录训练中出现的最大平均累积奖励并保存与之对应的那版 Q-Table。训练中偶尔会发现平均累积结果下滑此时我们希望保留的是历史上表现最好的模型参数而不是最新的可能已被破坏的参数。5.3 完整训练循环把每一步的累积奖励收集进rewards向量供后续绘图第 11 个代码块def probs(v,eps1e-4): v v-v.min()eps v v/v.sum() return v Qmax 0 cum_rewards [] rewards [] for epoch in range(100000): obs env.reset() done False cum_reward0 # do the simulation while not done: s discretize(obs) if random.random()epsilon: # exploitation - chose the action according to Q-Table probabilities v probs(np.array(qvalues(s))) a random.choices(actions,weightsv)[0] else: # exploration - randomly chose the action a np.random.randint(env.action_space.n) obs, rew, done, info env.step(a) cum_rewardrew ns discretize(obs) Q[(s,a)] (1 - alpha) * Q.get((s,a),0) alpha * (rew gamma * max(qvalues(ns))) cum_rewards.append(cum_reward) rewards.append(cum_reward) # Periodically print results and calculate average reward if epoch%50000: print(f{epoch}: {np.average(cum_rewards)}, alpha{alpha}, epsilon{epsilon}) if np.average(cum_rewards) Qmax: Qmax np.average(cum_rewards) Qbest Q cum_rewards[]逐段拆解训练循环中的关键逻辑probs(v)把 Q 值向量转成合法的概率分布先减去最小值并加上一个极小量eps1e-4保证数值稳定、避免除零再做归一化。这样Q 值更高的动作被选中的概率更大但并非每次必选最优——保留了随机性。主循环设定epoch上限为 100000 次完整仿真episode。每个 episode 内当done为真杆已倒下即结束。动作选择random.random() epsilon时走利用路线——用probs(qvalues(s))构造概率random.choices依权重采样动作否则走探索路线——np.random.randint(env.action_space.n)均匀随机选一个动作。Q 值更新执行动作后拿到新观测ns按 Q-Learning 更新公式Q[(s,a)] (1 - alpha) * Q.get((s,a),0) alpha * (rew gamma * max(qvalues(ns)))即新值 旧值 学习率 × (即时奖励 折扣 × 后继状态最优值 − 旧值)的展开写法。进度报告每 5000 个 epoch 打印当前窗口平均累积奖励并在超过历史最优Qmax时用Qbest Q保存最优 Q-Table随后清空cum_rewards开始新一轮统计。从运行结果能观察到的两个典型现象课程的 README 中有原述接近目标我们很可能已经达到或逼近连续 100 次运行累积奖励 195的目标即便平均数值较小也不能断言失败因为这里对 5000 次运行取平均而形式化标准只要求连续 100 次。奖励回退有时平均奖励开始下降说明新写入 Q-Table 的某些条目反而污染了已学好的值使局面变差——这正是需要Qbest快照机制的原因。六、训练进度可视化训练过程中每个迭代的累积奖励都被写入rewards向量。直接把它按迭代序号绘图plt.plot(rewards)由于随机训练过程的固有特性各轮训练session的长度差异极大原始曲线几乎无法读出任何趋势。为了提取有效信息可以计算运行平均值running average——例如以 100 为窗口对实验序列做滑动平均。借助np.convolve可非常方便地实现第 12 个代码块def running_average(x,window): return np.convolve(x,np.ones(window)/window,modevalid) plt.plot(running_average(rewards,100))np.convolve用长度为window、取值全为1/window的核与信号做卷积等价于逐点滑动求平均modevalid让输出只在核完全覆盖信号的位置产生长度相应缩短。滑动平均后的曲线能清晰显示出整体上升与局部的回退波动。七、训练中动态调节超参数为了让学习更稳定一个自然改进是在训练过程中动态调整超参数课程 README 的原述建议学习率alpha可从接近 1 的初值开始随后逐步调低。随着时间推移Q-Table 中会积累出较好的概率值此时应做微调而非用新值整体覆盖旧值。探索率epsilon倾向于随时间缓慢增大从而少探索、多利用。合理的做法是从较低的epsilon起步逐步提升到接近 1。任务 1实验不同的超参数取值看看能否拿到更高的累积奖励——是否突破了 195任务 2要形式化地解决该问题必须在连续 100 次运行中平均奖励达到 195。请在训练中实时统计这一指标确认已经正式解决问题。八、把训练好的模型跑起来训练完毕后最直观的验证是让学到的模型真实运行。以下代码与训练时采用相同的动作选择策略——按 Q-Table 中的概率分布采样第 13 个代码块obs env.reset() done False while not done: s discretize(obs) env.render() v probs(np.array(qvalues(s))) a random.choices(actions,weightsv)[0] obs,_,done,_ env.step(a) env.close()打开渲染窗口后会看到杆被持续扶正、较长时间保持平衡的画面效果类似这张动图在仓库的 solution/notebook.ipynb 末尾还给出了一个进阶技巧把渲染帧用 PIL 逐帧抓取、合成动画 GIF 保存到images/cartpole-balance.gif。其关键代码片段如下注意它使用的是最优快照Qbest而非最终的Qfrom PIL import Image obs env.reset() done False i0 ims [] while not done: s discretize(obs) imgenv.render(modergb_array) ims.append(Image.fromarray(img)) v probs(np.array([Qbest.get((s,a),0) for a in actions])) a random.choices(actions,weightsv)[0] obs,_,done,_ env.step(a) i1 env.close() ims[0].save(images/cartpole-balance.gif,save_allTrue,append_imagesims[1::2],loop0,duration5) print(i)要点env.render(modergb_array)返回图像帧而非弹窗Image.fromarray把帧转为 PIL 图像append_imagesims[1::2]隔帧采样以压缩体积loop0表示无限循环duration5为每帧毫秒数。九、挑战与延伸思考课程为读者预留了两组开放性挑战见 2-Gym README 的 Challenge任务 3上述演示使用的是训练结束时的最终 Q-Table它未必是最优的——还记得训练时我们把表现最好的 Q-Table 存进了Qbest吗请把Qbest复制回Q再跑一遍观察平衡表现是否有差异。任务 4上文每一步并非选取最优动作而是按概率分布采样。始终选择 Q 值最高的动作用np.argmax找到最大 Q 值对应的动作编号是否更合理请实现这一贪心策略并观察它是否改善了平衡。这两项任务恰好构成一对张力任务 4 的贪心策略对应纯利用能充分榨取已学知识任务 3 则检验训练中最优快照的价值。两者结合可以更深入理解探索与利用的权衡。9.1 配套作业把同一套算法迁移到 Mountain Car环境 API 的统一设计使得迁移算法成本极低Gym 中所有环境都提供相同的reset、step、render方法以及相同的动作空间/观测空间抽象。因此可以把 CartPole 的 Q-Learning 几乎原样搬到另一个环境。课程的 assignment.md 要求训练Mountain Car山地小车一辆陷入山谷的小车需要通过反复来回行驶积蓄动量最终冲出山谷抓住旗子。其动作空间是 3 个离散值值含义0向左加速1不加速2向右加速观测空间只有两个分量序号观测最小值最大值0小车位置-1.20.61小车速度-0.070.07奖励系统也很有特色到达山顶旗子处位置 0.5得 0 分否则每一步得 -1 分当小车位置超过 0.5 或 episode 超过 200 步时终止。关键难点在于小车引擎动力不足无法一次冲上陡坡唯一的成功路径是前后往返积累动量。作业要求读者从 notebook.ipynb 的现有代码出发替换新环境、改写状态离散化函数以尽可能小的改动让算法在新问题上收敛并通过调参优化结果。Mountain Car 环境的示意图见仓库图片 mountaincar.png。十、小结与下一步本课的核心收获可以概括为一条从玩具问题通往真实问题的路径学会使用OpenAI Gym抽象env.make建环境reset重开一局step返回 (观测, 奖励, done, info)render可视化理解连续状态必须离散化既可用np.digitize分 bin 精确控制状态规模也可用带尺度因子的量化方法discretize配合字典结构处理无界观测掌握字典版 Q-Table与qvalues()辅助函数的写法适配未知边界的连续环境跑通含探索/利用策略、Q 值迭代更新、最优快照保存Qmax/Qbest的完整训练循环并用running_average平滑曲线评估收敛借助统一环境 API把算法低成本迁移到 Mountain Car 等新任务assignment.md。课程在 2-Gym README 的结论 中同时指出了边界与方向本课成功把 Q-Learning 应用于离散与连续两类环境但动作仍然是离散的。当动作空间也连续、或观测空间复杂到如图像级别例如 Atari 游戏画面时往往需要更强大的机器学习工具——例如神经网络——来逼近值函数或策略。这是下一阶段更高级的 AI 课程将要探讨的主题。想继续深入学习本系列可回到上一课 8-Reinforcement/1-QLearning 复习 Q-Learning 与 Bellman 方程的原理或进入仓库中 8-Reinforcement 的目录结构浏览完整课程组织。参考实现完整的 13 个代码块均已按序嵌入 notebook.ipynb学习用骨架代码块以注释占位带全部代码的成稿位于 solution/notebook.ipynb本文对应的译文原文在 translations/cs/8-Reinforcement/2-Gym/README.md。【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考