Bootstrap

【ML】Q-Learning应用于具有连续状态的问题(Q-Learning 学习滑冰)

 🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

在本课中,我们将把 Q-Learning 的相同原理应用于具有连续状态的问题,即由一个或多个实数给出的状态。我们将处理以下问题:

问题:如果彼得想要逃离狼,他需要能够更快地移动。我们将看到 Peter 如何使用 Q-Learning 学习滑冰,尤其是保持平衡。

彼得和他的朋友们创造性地逃离了狼!图片由Jen Looper提供

我们将使用称为CartPole问题的简化版平衡。在cartpole 世界中,我们有一个可以左右移动的水平滑块,目标是平衡滑块顶部的垂直杆。

先决条件

在本课中,我们将使用一个名为OpenAI Gym的库来模拟不同的环境。您可以在本地运行本课的代码(例如,从 Visual Studio Code),在这种情况下,模拟将在新窗口中打开。在线运行代码时,您可能需要对代码进行一些调整,如此处所述

OpenAI 健身房

在上一课中,游戏规则和状态由Board我们自己定义的类给出。在这里,我们将使用一个特殊的模拟环境,模拟平衡杆后面的物理。训练强化学习算法的最流行的模拟环境之一称为Gym,由OpenAI维护。通过使用这个健身房,我们可以创建从购物车模拟到 Atari 游戏的不同环境。

注意:您可以在此处查看 OpenAI Gym 提供的其他环境。

首先,让我们安装gym并导入所需的库(代码块1):

import sys
!{sys.executable} -m pip install gym 

import gym
import matplotlib.pyplot as plt
import numpy as np
import random

练习 - 初始化一个 cartpole 环境

要处理cartpole 平衡问题,我们需要初始化相应的环境。每个环境都与一个相关联:

  • 定义我们从环境中接收到的信息结构的观察空间。对于cartpole 问题,我们接收到杆的位置、速度和其他一些值。

  • 定义可能动作的动作空间。在我们的例子中,动作空间是离散的,由两个动作组成——leftright。(代码块 2)

  1. 要初始化,请键入以下代码:

    env = gym.make("CartPole-v1")
    print(env.action_space)
    print(env.observation_space)
    print(env.action_space.sample())

为了了解环境的工作原理,让我们运行一个 100 步的简短模拟。在每一步,我们都提供了一个要采取的动作——在这个模拟中,我们只是从 中随机选择一个动作action_space

  1. 运行下面的代码,看看它会导致什么。

    ✅请记住,最好在本地 Python 安装上运行此代码!(代码块 3)

    env.reset()
    
    for i in range(100):
       env.render()
       env.step(env.action_space.sample())
    env.close()

    您应该会看到类似于此图像的内容:

  2. 在模拟过程中,我们需要获得观察结果才能决定如何行动。事实上,step 函数会返回当前观察值、奖励函数和指示继续模拟是否有意义的 done 标志:(代码块 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()

    您最终会在笔记本输出中看到类似这样的内容:

    [ 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
    

    在模拟的每一步返回的观察向量包含以下值:

    • 推车位置
    • 推车速度
    • 极角
    • 磁极转速
  3. 获取这些数字的最小值和最大值:(代码块 5)

    print(env.observation_space.low)
    print(env.observation_space.high)

    您可能还注意到,每个模拟步骤的奖励值始终为 1。这是因为我们的目标是尽可能长时间地生存,即在最长的时间内将杆保持在合理的垂直位置。

    ✅事实上,如果我们设法在 100 次连续试验中获得 195 次的平均奖励,则认为 CartPole 模拟已解决。

状态离散化

在 Q-Learning 中,我们需要构建 Q-Table 来定义在每个状态下要做什么。为了能够做到这一点,我们需要谨慎的状态,更准确地说,它应该包含有限数量的离散值。因此,我们需要以某种方式离散化我们的观察,将它们映射到一组有限的状态。

我们有几种方法可以做到这一点:

  • 分成垃圾箱。如果我们知道某个值的区间,我们可以把这个区间分成若干个bin,然后用它所属的 bin 号替换这个值。这可以使用 numpydigitize方法来完成。在这种情况下,我们将准确地知道状态大小,因为它取决于我们为数字化选择的 bin 数量。

✅我们可以使用线性插值将值带到某个有限区间(例如,从 -20 到 20),然后通过四舍五入将数字转换为整数。这使我们对状态大小的控制较少,尤其是在我们不知道输入值的确切范围的情况下。例如,在我们的案例中,4 个值中有 2 个没有其值的上限/下限,这可能会导致无限数量的状态。

在我们的示例中,我们将采用第二种方法。正如您稍后可能会注意到的那样,尽管未定义上限/下限,但这些值很少会在某些有限区间之外取值,因此具有极值的状态将非常罕见。

  1. 这是将从我们的模型中获取观察结果并生成 4 个整数值的元组的函数:(代码块 6)

    def discretize(x):
        return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))

  2. 让我们也探索另一种使用 bin 的离散化方法:(代码块 7)

    def create_bins(i,num):
        return np.arange(num+1)*(i[1]-i[0])/num+i[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))

  3. 现在让我们运行一个简短的模拟并观察那些离散的环境值。随意尝试两者discretizediscretize_bins看看是否有区别。

    ✅discretize_bins 返回 bin 编号,从 0 开始。因此,对于 0 附近的输入变量的值,它返回区间中间的数字 (10)。在离散化中,我们不关心输出值的范围,允许它们为负值,因此状态值不会移位,0 对应于 0。(代码块 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 过程中使用这种“隐形”执行。

Q表结构

在我们上一课中,状态是一对从 0 到 8 的简单数字,因此可以方便地用 8x8x2 形状的 numpy 张量来表示 Q-Table。如果我们使用 bins 离散化,我们的状态向量的大小也是已知的,所以我们可以使用相同的方法并用一个形状为 20x20x10x10x2 的数组表示状态(这里 2 是动作空间的维度,第一维度对应于我们为观察空间中的每个参数选择使用的箱)。

然而,有时观察空间的精确尺寸是未知的。在discretize函数的情况下,我们可能永远无法确定我们的状态保持在一定的范围内,因为一些原始值是不受约束的。因此,我们将使用稍微不同的方法并用字典来表示 Q-Table。

  1. 使用对(state,action)作为字典键,该值将对应于 Q-Table 条目值。(代码块 9)

    Q = {}
    actions = (0,1)
    
    def qvalues(state):
        return [Q.get((state,a),0) for a in actions]

    在这里,我们还定义了一个函数qvalues(),它返回一个给定状态的 Q-Table 值列表,该状态对应于所有可能的动作。如果 Q-Table 中不存在该条目,我们将返回 0 作为默认值。

让我们开始 Q-Learning

现在我们准备教彼得平衡!

  1. 首先,让我们设置一些超参数:(代码块 10)

    # hyperparameters
    alpha = 0.3
    gamma = 0.9
    epsilon = 0.90

    这里alpha学习率,它定义了我们应该在多大程度上调整每一步的 Q-Table 的当前值。在上一课中,我们从 1 开始,然后alpha在训练期间降低到较低的值。在这个例子中,为了简单起见,我们将保持不变,alpha稍后您可以尝试调整值。

    gamma折扣因子,它显示了我们应该在多大程度上优先考虑未来的奖励而不是当前的奖励。

    epsilon探索/开发因素,它决定了我们是否应该更喜欢探索而不是开发,反之亦然。在我们的算法中,我们将在epsilon百分比的情况下根据 Q-Table 值选择下一个动作,而在剩余数量的情况下,我们将执行随机动作。这将使我们能够探索我们以前从未见过的搜索空间区域。

    ✅在平衡方面 - 选择随机行动(探索)将充当错误方向的随机打击,杆必须学习如何从这些“错误”中恢复平衡

改进算法

我们还可以对上一课的算法进行两项改进:

  • 计算多次模拟的平均累积奖励。我们将每 5000 次迭代打印一次进度,并将在这段时间内平均我们的累积奖励。这意味着如果我们得到超过 195 分 - 我们可以认为问题已解决,质量甚至高于要求。

  • 计算最大平均累积结果Qmax,我们将存储该结果对应的 Q-Table。当您运行训练时,您会注意到有时平均累积结果开始下降,我们希望保持与训练期间观察到的最佳模型相对应的 Q-Table 值。

  1. 在向量处收集每个模拟的所有累积奖励,以rewards供进一步绘制。(代码块 11)

    def probs(v,eps=1e-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_reward=0
        # == 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,weights=v)[0]
            else:
                # exploration - randomly chose the action
                a = np.random.randint(env.action_space.n)
    
            obs, rew, done, info = env.step(a)
            cum_reward+=rew
            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%5000==0:
            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=[]

您可能会从这些结果中注意到:

  • 接近我们的目标。我们已经非常接近实现在 100+ 次连续模拟运行中获得 195 个累积奖励的目标,或者我们可能已经实现了!即使我们得到较小的数字,我们仍然不知道,因为我们平均超过 5000 次运行,而正式标准只需要 100 次运行。

  • 奖励开始下降。有时奖励开始下降,这意味着我们可以用那些使情况变得更糟的值来“破坏” Q-Table 中已经学习的值。

如果我们绘制训练进度,这个观察会更清晰可见。

绘制训练进度

在训练期间,我们将每次迭代的累积奖励值收集到rewards向量中。这是我们根据迭代次数绘制它时的样子:

plt.plot(rewards)

从这张图中,无法说明任何事情,因为由于随机培训过程的性质,培训课程的长度变化很大。为了更好地理解这个图表,我们可以计算一系列实验的运行平均值np.convolve,比如 100。这可以很方便地使用:(代码块 12)

def running_average(x,window):
    return np.convolve(x,np.ones(window)/window,mode='valid')

plt.plot(running_average(rewards,100))

 

变化的超参数

为了使学习更加稳定,在训练期间调整我们的一些超参数是有意义的。尤其是:

  • 对于学习率alpha我们可以从接近 1 的值开始,然后不断减小参数。随着时间的推移,我们将在 Q-Table 中获得良好的概率值,因此我们应该稍微调整它们,而不是用新值完全覆盖。

  • 增加 epsilon。我们可能想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,weights=v)[0]
   obs,_,done,_ = env.step(a)
env.close()

您应该看到如下内容:

;