加载中...

「Gym 课程笔记 03」经典控制 - Cart Pole 倒立摆


0x00 问题描述

Cart Pole 是一个倒立摆问题:一根杆子通过非驱动接头直立放置在小车上,小车沿着无摩擦的轨道移动。

目标是通过在小车上向左和向右施加力来平衡杆,坚持得越久越好。

0x10 问题解读

Cart Pole 页面的描述中,我们可以得到不少关键信息:

0x11 环境说明

首先看到这个这个表,它的含义是:

  1. 在 python 通过以下语句可以创建 CartPole(版本 v1)的预设环境:
    • import gymnasium
    • env = gymnasium.make("CartPole-v1")
  2. 而在这个预设环境中:
    • 执行 env.action_space 可以得到动作空间(Action Space)为 Discrete(2)
    • 执行 env.observation_space 可以得到观测空间(Observation Space)为 Box([-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38], [4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38], (4,), float32)

对于 “环境”、“动作空间”、“观测空间” 等术语我们已经在第一节里面解释过,这里就不再赘述其含义了。

0x12 动作空间

Cart Pole 文档中给出的动作空间为:

  • 动作 0: Push cart to the left (推动小车向左)
  • 动作 1: Push cart to the right (推动小车向右)

env.action_space 得到值范围是一致的,这是两个离散动作(离散空间)。

0x13 观测空间

观测空间 observation_space 初始状态为: Box([-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38], [4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38], (4,), float32)

因为使用了 Box 类型,显然这是一个连续空间,其含义为:

  • [-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38] 是观察空间中每个维度的最小值
  • [4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38] 是观察空间中每个维度的最大值
  • (4,) 表示观察空间是一个四维的空间。这是一个元组,其中只有一个元素,即 4,表明有 4 个独立的观察值。
  • float32 表示这些值是 32 位浮点数

结合 Cart Pole 文档给出的表格:

从表格可知,observation_space 的 4 个独立的观察值分别代表:

  • 0: Cart Position: 小车位置,即 x 坐标轴的范围
  • 1: Cart Velocity: 小车速度
  • 2: Pole Angle: 极角,即杆子和垂直线之间的夹角
  • 3: Pole Angular Velocity: 杆子绕其轴的角速度,即其倾斜速率的快慢

0x14 奖励

Cart Pole 的目标是尽可能长时间地保持杆子直立,因此在结束回合前,它的每一步都可以获得 +1 的奖励值。

在 v1 版本中,奖励的上限值为 500; v0 版本奖励的上限值为 200。

0x15 初始状态

初始状态中的每个参数均在 (-0.05, 0.05) 之间统一初始化。

0x16 回合终止

如果发生以下情况之一,则回合终止:

  • 终止: 极角大于 ±12°
  • 终止: 小车位置大于 ±2.4 (小车中心到达显示屏边缘)
  • 中止: 回合步数大于 500(v0 版本 为 200)

可以视 中止 条件就是成功完成挑战的条件,因为只有达到步数上限才能得到最大奖励

0x20 解题过程

0x21 初步分析

乍一看 Cart Pole 与上一节的 Acrobot 问题很相似:两者都属于连续状态空间、离散动作空间问题。

唯一区别是完成挑战的目标:

  • Acrobot 成功挑战的目标是尽快完成特定任务(使杆子摆动到一定高度)
  • Cart Pole 成功挑战的目标是尽可能长时间地保持杆子直立,直到达到步数上限(v1 版本为 500 步)

刚开始的时候,我并没有在意这个区别,毕竟 Cart Pole 累积越多步数、总奖励就会越高。于是我尝试直接使用 Acrobot 的代码 去求解 Cart Pole 。

但是在训练的过程中,我就发现了一个问题: 不论训练多久,智能体始终倾向于在 100 - 200 步左右提前终止挑战,而没有考虑最大步数(500)的情况。

观察 tensorboard 的曲线,反映出的也是一样的情况:

0x22 尝试增加一次性奖励

其实这是和 DQN 算法特性有关,它通过学习一个价值函数来估计每个动作的期望回报,并利用这些信息来选择下一步动作。但是 DQN 本质是贪婪的(短视的)算法,它只注重短期的收益回报,当继续坚持的风险高于奖励的价值时,智能体就会倾向提前中止挑战。

简而言之,每一步 +1 的奖励,在 DQN 策略下不足以激励智能体完成更多的回合数

于是我在 Acrobot 的代码 的基础上,进行 reward shaping(奖励重塑):

def exec_next_action(targs: TrainArgs, action, epoch=-1, step_counter=-1) :
    next_raw_obs, reward, terminated, truncated, info  = targs.env.step(action)
    next_obs = to_tensor(next_raw_obs, targs)
    done = terminated or truncated

    # ---------- 修改的代码 ----------
    # 当智能体以最大步数完成挑战时,获得一次性的大额额外奖励
    if truncated :
        reward += MAX_STEP
    # -------------------------------
    return (next_obs, reward, done)

如代码所示,当智能体以最大步数完成挑战时,获得一次性的大额额外奖励 +500。

但是重新训练发现,情况依旧。

我观察了一下训练过程,智能体完成 500 步的回合数寥寥可数,也就是说这个大额奖励太遥远了,可能智能体压根就不知道有这个大额奖励的存在

0x23 尝试增加持续激励

我突然联想到了在《猫和老鼠》中,Tom 诱捕 Jerry 的一幕:

太遥远的奖励智能体很难触达到,但是我可以每隔一定的步数,给智能体一个额外的 “糖果” 作为里程碑目标!

这样就就可起到一个持续激励的作用,智能体会更愿意获得 “糖果” 而坚持更多的步数。

于是我就设置了一个阶梯式的奖励机制:

# 每坚持 10 步,可以在原奖励基础上得到额外的 +10 奖励(额外小糖果,持续激励)
if step_counter % 10 == 0 :
    reward += 10

# 每坚持 100 步,可以在原奖励基础上得到额外的 +100 奖励(额外大糖果,持续激励)
if step_counter % 100 == 0 :
    reward += 100

同时为了进一步鼓励智能体努力延长整个回合的持续步数,我修改了每一步固定的 +1 奖励,这个奖励会乘以一个递减因子,使得智能体在回合的后期、难以依赖步数增长获得更多奖励,只能通过 “糖果” 增加收益:

# 智能体每一步得到的奖励逐步递减,目的是使得智能体不要专注短期收益
increment_factor = 1 - step_counter / MAX_STEP     # 递减因子 = 1 - 当前步数 / 步数上限
reward = reward * increment_factor

完整的修改代码可以参考 02_Cart_Pole/train_DQN.py

通过平衡短期和长远激励策略,智能体在训练到 7500 回合之后,可以 100% 以最大的步数完成挑战:


文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
 上一篇
「RO 笔记」攻击伤害字型修改 「RO 笔记」攻击伤害字型修改
当你看到别人在游戏里输出的时候、跳出来伤害数字字型非常特别,你是否很好奇是怎么做到的 ?本文将教会你如何创造自己喜欢的攻击伤害字型。
2023-12-26
下一篇 
「RO 笔记」SDE 基础使用指引 「RO 笔记」SDE 基础使用指引
SDE 最初主要是为了作为 RO 客户端的数据库(物品、任务、成就)编辑器而开发的,随着不断优化,它得以兼容服务端的数据库格式(txt、yml、sql),并将两端的资源关联起来,使得用户可以轻松管理两端的多种数据库。
2023-12-23
  目录