尧图精选

ML-For-Beginners 强化学习实战:用 OpenAI Gym CartPole 环境与 Q-Learning 学会平衡

🕒 发布时间:2026/9/10 0:23:32 📁 来源:尧图网络
ML-For-Beginners 强化学习实战用 OpenAI Gym CartPole 环境与 Q-Learning 学会平衡【免费下载链接】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”一节讲解如何将 Q-Learning 从离散状态棋盘问题迁移到 OpenAI Gym 的 CartPole 连续状态环境包括环境初始化与观测/动作空间、连续观测的两种状态离散化方法、以字典实现的 Q 表结构、epsilon 探索与 Q 值更新的完整训练循环以及训练进度的滑动平均分析与超参数调节策略。读完后你可以独立完成从随机乱撞、到训练出能长时间保持平衡的策略、再到正式验证“195 分标准”的全流程并知道如何把同一套算法以极小改动迁移到 MountainCar 环境。一、课程定位与前置条件本节是 8-Reinforcement 模块的第二课。第一课用自研的Board类实现了离散状态棋盘格点下的 Q-Learning本课把同一套 Q-Learning 原理应用到一个连续状态问题上——状态由一个或多个实数给出。课程用“Peter 要滑板溜走躲避狼”的故事线引入 CartPole小车悬杆平衡问题在 CartPole 世界里有一条可以左右移动的水平滑块目标是在滑块上保持一根竖直杆的平衡。运行本课程的代码有两种方式本地运行例如在 Visual Studio Code 中直接执行env.render()会在新窗口中打开动画窗口这是课程推荐的运行方式在线运行Binder/Colab 一类的环境渲染需要额外调整可能需要按在线环境的特殊方式处理render调用。安装 Gym 并导入所需库代码块 1import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import random注意上一课游戏规则与状态由我们自己定义的Board类给出本课改用专门的仿真环境由 Gym 来模拟平衡杆背后的物理。Gym 是训练强化学习算法最流行的仿真环境库之一由 OpenAI 维护覆盖从 CartPole 到 Atari 游戏的大量环境。二、初始化 CartPole 环境观测空间与动作空间要处理 CartPole 平衡问题需要初始化对应的环境。Gym 中的每个环境都关联两类“空间”观测空间observation space定义从环境收到的信息的结构。CartPole 中我们获得杆的位置、速度等值动作空间action space定义可能的动作。本课的动作空间是离散的只包含两个动作——左和右。初始化并查看两个空间代码块 2env gym.make(CartPole-v1) print(env.action_space) print(env.observation_space) print(env.action_space.sample())env.action_space.sample()会从动作空间中随机采样一个动作方便我们后续做随机策略实验从源码结构看所有 Gym 环境都暴露统一的 API——reset、step、render以及action space与observation space抽象。这也是为什么本课程 作业 能要求你“几乎不改算法、只换环境”地把 Q-Learning 迁移到 MountainCar。2.1 跑 100 步的随机模拟为观察环境如何工作先运行一段 100 步的短模拟。每一步从action_space中随机选取一个动作代码块 3env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close()由于动作完全是随机的小车基本无法平衡杆很快倒下你会看到类似下面的动画2.2 理解step的返回值模拟过程中我们需要拿到观测来决定下一步动作。step函数会返回当前观测、奖励值以及一个done标志表示是否还有必要继续模拟代码块 4env.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每步返回的观测向量包含 4 个分量小车的位置position of cart小车的速度velocity of cart杆的角度angle of pole杆的角速度rotation rate of pole用low/high可以查看这些数值的边界代码块 5print(env.observation_space.low) print(env.observation_space.high)从输出可以看出位置与杆角度有上下界而两个速度分量是无界的——这一点在下一节的离散化方案选择中非常关键。另外注意每个模拟步的奖励恒为1.0。这是因为我们的目标就是活尽可能久——让杆尽可能长时间保持在大致竖直的位置。课程给出 CartPole 的正式判定标准✅ CartPole 仿真被认定为“已解决solved”当我们能在100 次连续试验中取得平均奖励 195。三、状态离散化把连续观测映射到有限状态Q-Learning 需要一张 Q 表来定义“每个状态下该做什么”因此状态必须是离散的——即包含有限个离散值。连续观测就必须被离散化映射到一个有限状态集合上。课程给出两种方案分箱bins若知道某个值的取值区间可把区间划分成若干个bin然后用该值所属的箱号替换原值可用 numpy 的digitize实现。此法能精确控制状态空间大小——它取决于你为每个参数选了多少个箱归一化取整用线性方式把值压缩到某个有限区间例如 -20 到 20再取整。对状态大小的控制稍弱尤其是当输入值的确切范围未知时——本课 4 个观测值中有 2 个没有上下界理论上会导致无穷多个状态。本课采用第二种方案。原因尽管上/下界未定义这些值实际很少落到某些有限区间之外极端值对应的状态会非常稀少可以接受。方案一归一化取整代码块 6。该函数把观测向量除以一组比例因子后取整生成一个 4 个整数组成的元组def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))4 个比例因子分别对应“小车位置、小车速度、杆角度、杆角速度”位置与速度每 0.25 一格角度每 0.01 弧度一格角速度每 0.1 一格。由于取整前不控制输出范围离散状态可以为负数且 0 恰好对应 0。方案二分箱代码块 7。先按区间与箱数生成箱边界再用np.digitize把每个分量映射到箱号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))这里为 4 个参数分别指定了取值区间ints与箱数nbins20、20、10、10。两种方案的行为差异值得注意discretize_bins返回的是从 0 开始的箱号所以输入值在 0 附近时返回的是区间中间的箱号例如 10discretize不关心输出值域状态值可以是负数0 对应 0。跑一段短模拟来观察离散后的状态值代码块 8。可以把discretize和discretize_bins都试一遍比较输出差异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 训练全程都使用这种无渲染的“隐形”执行方式。四、Q 表结构张量还是字典上一课状态是 0~8 的简单数字对用形状8x8x2的 numpy 张量表示 Q 表很自然。若采用分箱离散化状态向量大小是已知的同样可以用张量形状为20x20x10x10x2最后的 2 是动作空间维度前面的维度依次对应观测空间每个参数选取的箱数。但用discretize函数时我们无法保证状态永远落在某个界限内部分原始值无界因此改用字典表示 Q 表代码块 9以(state, action)二元组为键Q 值作为字典值Q {} actions (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions]qvalues()返回给定状态在全部可能动作下的 Q 值列表若条目尚不存在Q.get((state,a),0)默认返回 0。这个“缺失即 0”的约定很关键它意味着任何从未访问过的状态-动作对都从 0 起步训练过程中逐步被 Q 更新公式填充。五、开始 Q-Learning超参数与训练循环现在可以教 Peter 保持平衡了。5.1 三个超参数# hyperparameters alpha 0.3 gamma 0.9 epsilon 0.90alpha是学习率决定每步对 Q 表当前值的调整幅度。上一课我们从 1 开始并逐步调低本例为简洁起见保持恒定你之后可以自行实验调节gamma是折扣因子表示未来奖励相对当前奖励应被优先重视的程度epsilon是探索/利用因子算法在epsilon比例的步里按 Q 表值选择下一个动作其余步执行随机动作从而有机会探索从未见过的搜索空间区域。课程打了个形象的比方对平衡杆而言选择随机动作探索就像在错误的方向上随机“捣一棍”杆必须学会从这些“失误”中重新找回平衡。5.2 对上一课算法的两处改进计算平均累计奖励在一段时间5000 次模拟上求平均并打印进度。若平均超过 195可以认为问题已解决而且质量比正式标准要求的更高记录最佳平均累计结果Qmax并保存对应的 Q 表训练中平均累计奖励有时会开始回落——我们可能正在用更差的值覆盖已学到的好值。为了保住训练中观测到的最好的那张 Q 表每当刷新Qmax时就顺手把当前 Q 表存档到Qbest。5.3 完整训练循环代码块 11每轮模拟的累计奖励收集进rewards向量供后续绘图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[]逐段解析关键逻辑动作选择random.random() epsilon成立时走“利用”分支——probs把当前状态的两个 Q 值做最小值平移加eps1e-4防止除零后归一化成概率分布再random.choices按概率采样一个动作否则走“探索”分支np.random.randint(env.action_space.n)随机取 0/1。按文档定义epsilon0.90表示 90% 的步按 Q 表选动作、10% 的步随机探索Q 更新Q[(s,a)] (1 - alpha) * Q.get((s,a),0) alpha * (rew gamma * max(qvalues(ns)))这正是 Q-Learning 的时序差分更新新 Q 值是旧值与目标值rew gamma * max_a Q(s, a)之间按学习率alpha的加权平均。由于每步奖励恒为 1rew始终等于 1进度跟踪每 5000 轮打印区间平均奖励若刷新Qmax则把 Q 表引用存入Qbest并清空cum_rewards窗口。从训练结果中通常能观察到两点接近目标我们已经非常接近甚至可能已达到“100 连续模拟平均 195 累计奖励”的目标。即使数字略小也未必是坏事——因为我们是在 5000 轮上求平均而正式标准只要求 100 轮奖励会开始回落有时奖励开始下降意味着我们正在用让情况变差的值**“毁掉”** Q 表里已经学到的好值。这一现象在训练进度图上更直观。六、可视化训练进度从“看不出信息”到滑动平均把每轮累计奖励按迭代号直接画出来plt.plot(rewards)这张图几乎读不出任何结论强化学习训练过程是随机的不同训练回合的长度存活步数差异很大原始曲线因此杂乱无章。让曲线有意义的方法是计算滑动平均——例如取 100 个实验的窗口用np.convolve非常方便地实现代码块 12def running_average(x,window): return np.convolve(x,np.ones(window)/window,modevalid) plt.plot(running_average(rewards,100))平滑后的曲线显示出清晰的上升趋势说明 Q 表在持续变好。从配套 notebook.ipynb 的结构看课程还在最后补了一个“把结果存成 GIF”的单元逐帧调用env.render(modergb_array)收集图像再用 PIL 保存为images/cartpole-balance.gif并把动作策略切换为按Qbest的概率分布采样——即“用最佳 Q 表出片”。七、调节超参数让学习更稳定为了让学习更稳定可以在训练过程中动态调整超参数学习率alpha可以从接近 1 的值开始然后逐步降低。随着 Q 表中的值越来越好就应当只做小幅调整而不是被新值完全覆盖epsilon可以缓慢提高epsilon从而少探索、多利用——从较低的epsilon起步逐步升到接近 1。课程给出的两个练习任务 1调节超参数看能否取得更高的累计奖励。能否超过 195任务 2要“正式”解决问题需要在100 次连续运行中取得 195 的平均奖励。请在训练中实测这一标准确认你真的正式解决了问题八、观察训练结果按 Q 表概率分布采样出招训练结束后直接看模型的实际表现。运行模拟并沿用与训练时相同的动作选择策略——按 Q 表中的概率分布采样代码块 13obs 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()此时杆能稳定保持平衡类似课程展示的动画效果见文首的 cartpole-balance.gif。挑战任务任务 3上面用的是训练结束时的最终 Q 表副本它未必是最好的一张。还记得我们把表现最好的 Q 表存进了Qbest变量吗把Qbest拷贝到QQ Qbest或直接引用重跑同一示例看看有无差别。任务 4这里每步并不是选“最优动作”而是按概率分布采样。是否应该永远选择 Q 表值最高的动作可以用np.argmax找到最高 Q 值对应的动作号来实现该策略。实现它并检查是否改善了平衡效果。从源码结构看Qbest Q保存的是引用而非深拷贝因此“用Qbest替换Q”几乎零成本——这也解释了为什么任务 3 只要求“把Qbest拷贝到Q”。而任务 4 的argmax策略与训练期的概率采样形成对照训练期保留随机性有利于探索评估期是否该完全贪婪正是值得动手验证的开放问题。九、课程小结与延伸同一套算法迁移到 MountainCar我们学会了如何仅凭一个奖励函数定义游戏期望状态加上对搜索空间的智能探索就能训练出表现良好的智能体Q-Learning 已被成功应用于离散与连续观测空间动作仍为离散的场景当动作空间也是连续的、观测空间更复杂例如 Atari 游戏屏幕图像时往往需要神经网络等更强的机器学习技术——课程将其留作后续进阶 AI 课程的主题。本节的学习成果可以直接迁移Gym 的所有环境提供同一套 APIreset/step/render与同样的 action/observation space 抽象因此把现有 Q-Learning 算法适配到其他环境只需极少的代码改动。课程 作业 正是这一思路的练习——训练一辆 Mountain Car环境是掉进山谷里的小车目标是冲出山谷并到达旗子position 0.5。由于发动机不足以一次冲上山唯一成功路径是来回行驶积攒动量动作空间有 3 个值0 向左加速1 不加速2 向右加速观测空间只有 2 个值小车位置-1.2 ~ 0.6与小车速度-0.07 ~ 0.07奖励机制与 CartPole 相反到达旗子得 0 分未达到position 0.5每步扣 1 分回合在 position 0.5 或超过 200 步时结束。作业要求你从现有 notebook.ipynb 出发替换为新环境、改写状态离散化函数尽量用最小改动让算法跑起来并通过调节超参数使其收敛评分标准的核心是算法成功移植、且能在 200 步内到达旗子。完整的参考实现可对照 solution/notebook.ipynb德语课程材料含本 README 与 assignment位于 translations/de/8-Reinforcement/2-Gym。十、环境适配与注意事项本课程的代码按课程编写时的gym版本旧式 API设计step返回 4 元组(obs, rew, done, info)env.render()打开新窗口。若在更新版本的 Gym/Gymnasium 中运行API 可能已有变化例如step返回带truncated字段的 5 元组需要自行适配文档中的discretize使用了np.int在新版 NumPy 中该别名已被弃用可替换为int或np.int64在线环境Binder/Colab无法弹开渲染窗口训练阶段建议保持env.render()注释掉的“隐形”执行方式仅在最终演示时本地渲染训练 100000 轮在纯 Python 环境下需要一定时间属正常现象滑动平均曲线出现稳定上升趋势即可认为算法工作正常。【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →