You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

什么是强化学习

强化学习是一类算法,是让计算机实现从一开始完全随机的进行操作,通过不断地尝试,从错误中学习,最后找到规律,学会了达到目的的方法。这就是一个完整的强化学习过程。让计算机在不断的尝试中更新自己的行为,从而一步步学习如何操自己的行为得到高分。

它主要包含四个元素Agent、环境状态、行动、奖励强化学习的目标就是获得最多的累计奖励。

让我们想象一下比赛现场:

计算机有一位虚拟的裁判,这个裁判他不会告诉你如何行动,如何做决定,他为你做的事只有给你的行为打分,最开始,计算机完全不知道该怎么做,行为完全是随机的,那计算机应该以什么形式学习这些现有的资源,或者说怎么样只从分数中学习到我应该怎样做决定呢?很简单,只需要记住那些高分,低分对应的行为,下次用同样的行为拿高分, 并避免低分的行为。

计算机就是 Agent他试图通过采取行动来操纵环境并且从一个状态转变到另一个状态当他完成任务时给高分(奖励),但是当他没完成任务时,给低分(无奖励)。这也是强化学习的核心思想。

在强化学习中有很多算法,如果按类别划分可以划分成 model-based (基于模型)和 model-free (不基于模型)两大类。

如果我们的 Agent 不理解环境,环境给了什么就是什么,我们就把这种方法叫做 model-free这里的 model 就是用模型来表示环境,理解环境就是学会了用一个模型来代表环境,所以这种就是 model-based 方法。

Model-free 的方法有很多, 像 Q learning、Sarsa、Policy Gradients 都是从环境中得到反馈然后从中学习。而 model-based 只是多了一道程序,为真实世界建模,也可以说他们都是 model-free 的强化学习, 只是 Model-based 多出了一个虚拟环境,我们可以先在虚拟环境中尝试,如果没问题,再拿到现实环境中来。

model-free 中, Agent 只能按部就班,一步一步等待真实世界的反馈,再根据反馈采取下一步行动。而 model-based能通过想象来预判断接下来将要发生的所有情况然后选择这些想象情况中最好的那种并依据这种情况来采取下一步的策略这也就是围棋场上 AlphaGo 能够超越人类的原因。

在这里主要介绍一下 model-free 中基于策略的一种算法Policy Gradient。在介绍该算法之前我们先要明确一下这个雅达利乒乓球游戏中的环境状态是游戏画面,Agent是我们操作的挡板,奖励是分数,动作是上或者下。

Policy Gradient

Policy Gradient的核心思想

其实 Policy Gradient 的核心思想非常简单,就是找一个函数\pi$,这个函数\pi能够根据现在环境的状态来产生接下来要采取的行动或者动作。即\pi(状态)\rightarrow动作$。

函数\pi$$其实可以看成是一个模型,那么想在无数次尝试中寻找出能让 Agent 尽量拿高分的模型应该怎样来找呢?我相信您应该猜到了!没错!就是神经网络!

我们可以将游戏画面传给神经网络作为输入,然后神经网络预测一下当前游戏画面下,下一步动作的概率分布。

细心的您可能会发现,如果每次取概率最高的动作作为下一步的动作,那不就成分类了么。其实 Policy Gradient 的并不是每次都选取概率最高的动作,而是根据动作的概率分布进行采样。也就是说就算我预测出来的向上挪的概率为 80% ,也不一定会向上挪。

那么为什么采样而不是直接选取概率最大的呢?因为这样很有灵性。可以想象一下,我们和别人下棋的时候,如果一直按照套路来下,那么对手很可能能够猜到我们下一步棋会怎么走,从而占据主动。如果我们时不时地不按套路出牌,但是这种不按套路的动作不会降低太多对于我们能够赢下这一局棋的几率。那么对手很可能会不知所措,主动权就掌握在我们手里。就像《天龙八部》中虚竹大破珍珑棋局时一样,可能有灵性一点,会有意想不到的效果。

Policy Gradient 的原理

现在已经知道 Policy Gradient 是通过神经网络来训练模型,该模型需要根据环境状态来预测出下一步动作的概率分布,并根据这个概率分布进行采样,将采样到的动作作为下一步的动作。

那么会有一个灵魂拷问,就是怎样来鉴定我的神经网络是好还是坏呢?很显然,当然是赢的越多越好了!所以我们不妨假设,让计算机玩 10 把乒乓球游戏,那么可能会有这样的一个统计结果。

那么怎样评价这 10 把游戏打的好还是不好呢?也很明细,把 10 把游戏的所有反馈全部都加起来就好了。如果把这些反馈的和称为总反馈(总得分),那么就有总反馈(总得分)=第1把反馈1+第1把反馈2+...+第10把反馈m。也就是说总反馈越高越好。

说到这,有一个问题需要弄清楚:假设总共玩了 100 把,每 10 把计算一次总反馈,那么这 10 次的总反馈会不会是一模一样的呢?其实仔细想想会发现不会一摸一样,因为:

  • 游戏的状态实时在变,所以环境状态不可能一直是一样的。
  • 动作是从一个概率分布中采样出来的。

既然总反馈一直会变,那么我们可以尝试换一种思路,即计算总反馈的期望,即总反馈的期望越高越好。那这个期望怎么算呢?

首先我们可以将每一把游戏看成一个游戏序列(状态1->动作1->反馈1->状态2->动作2->反馈2 ... 状态N->动作N->反馈N)。那么每一个游戏序列(即每一把游戏)的反馈=反馈1+反馈2+...+反馈N。因此,若假设$R(\tau)表示游戏序列\tau的反馈,则有:R(\tau)=\sum_{n=1}^N\tau_n$。

如果我们把整个乒乓球游戏所有可能出现的状态,动作,反馈组合起来看成是玩了 N(N很大很大) 把游戏,就会有 N 个游戏序列(游戏序列1游戏序列2游戏序列3, ... , 游戏序列N)。那么我们在玩游戏时所得到的游戏序列实际上就是从这 N 个游戏序列中采样得到的。

所以我们游戏的总的反馈期望\overline{R_\theta}可表示为:\overline{R_\theta}=\sum_\tau R(\tau)P(\tau|\theta)。这个公式看起来复杂,其实不难理解。

假设我们玩了 10 把游戏,就相当于得到了 10 个游戏序列[\tau_1, \tau_2, ..., \tau_{10}]。这 10 个游戏序列就相当于从 P 中采样了 10 次\tau$。所以总反馈期望\overline{R_\theta}$又可以近似的表示为:


\overline{R_\theta} \approx \frac{1}{N}\sum_{n=1}^NR(\tau^n)

由于\overline{R_\theta}的值越大越好,所以我们可以使用梯度上升的方式来更新\theta$$。所以就有如下数学推导:

又由于:


\overline{R_\theta} = \sum_\tau R(\tau)P(\tau|\theta) \approx \frac{1}{N}\sum_{n=1}^NR(\tau^n)

所以就有:


\nabla \overline{R_\theta} \approx \frac{1}{N}\sum_{n=1}^NR(\tau^n) \nabla logP(\tau^n|\theta)

您会发现\sum_{n=1}^NR(\tau^n)很好算,只要把反馈全部加起来就完事了,难算的是\nabla logP(\tau^n|\theta)。所以我们来看一下\nabla logP(\tau^n|\theta)应该怎么算。

由于一个游戏序列\tau$$是由多个状态,动作,反馈构成的,即:


\tau=\{s_1, a_1, r_1, s_2, a_2, r_2, ..., s_T, a_T, r_T\}

所以:


P(\tau|\theta)=P(s_1)P(a_1|s_1,\theta)P(r_1,s_2|s_1,a_1)P(a_2|s_2,\theta)P(r_2,s_3|s_2,a_2)...
$$。

稍微整理一下可知:

P(\tau|\theta)=P(s_1)\prod_{t=1}^TP(a_t|s_t,\theta)P(\tau_t,s_{t+1}|s_t,a_t)



然后两边取$$log$$会得到:

logP(\tau|\theta)=\sum_{t=1}^T\nabla logP(a_t|s_t,\theta)

。

$$P(a_t|s_t,\theta)$$其实就是我们神经网络根据环境状态预测出来的下一步的动作概率分布。

![](./img/9.jpg)

OK到这里Policy Gradient的数学推导全部推导完毕了。我们不妨用一张图来总结一下 Policy Gradient 的算法流程。流程如下:

![](./img/10.jpg)



# 使用Policy Gradient玩乒乓球游戏

## 安装 gym 

想要玩乒乓球游戏首先得有乒乓球游戏。OpenAI 的 gym 为我们提供了模拟游戏的环境。使得我们能够很方便地得到游戏的环境状态,并作出动作。想要安装 gym 非常简单,只要在命令行中输入`pip install gym`即可。


## 安装 atari_py

由于乒乓球游戏是雅达利游戏机上的游戏,所以需要安装 atari_py 来实现雅达利环境的模拟。安装 atari_py 也很方便,只需在命令行中输入`pip install --no-index -f https://github.com/Kojoley/atari-py/releases atari_py` 即可。


## 开启游戏

当安装好所需要的库之后,我们可以使用如下代码开始游戏:

```python
# 开启乒乓球游戏环境
import gym

env = gym.make('Pong-v0')

# 一直渲染游戏画面
while True:
    env.render()
    # 随机做动作,并得到做完动作之后的环境(observation),反馈(reward),是否结束(done)
    observation, reward, done, _ = env.step(env.action_space.sample())

```

## 游戏画面预处理

由于`env.step`返回出来的 observation 是一张RGB的三通道图而且我们的挡板怎么移动只跟挡板和球有关系所以我们可以尝试将三通道图转换成一张二值化的图其中挡板和球是 1 ,背景是 0 。

```python

# 游戏画面预处理
def prepro(I):
    I = I[35:195]  #不要上面的记分牌
    I = I[::2, ::2, 0]  #scale 0.5所以I是高为80宽为80的单通道图
    I[I == 144] = 0  # 背景赋值为0
    I[I == 109] = 0  # 背景赋值为0
    I[I != 0] = 1  # 目标为1
    return I.astype(np.float).ravel() #将二维图压成一维的数组

# cur_x为预处理后的游戏画面
cur_x = prepro(observation)
```

游戏的画面是逐帧组成的,如果我们将当前帧和上一帧的图像相减就能得到能够表示两帧之间的变化的帧差图,将这样的帧差图作为神经网络的输入的话会是个不错的选择。

```python
# x为帧差图
x = cur_x - prev_x
# 将当前帧更新为上一帧
prev_x = cur_x
```

## 搭建神经网络

神经网络可以根据自己的喜好来搭建,在这里我使用最简单的只有两层全连接层的网络模型来进行预测,由于我们挡板的动作只有上和下,所以最后的激活函数为 sigmoid 函数。

```python
# 神经网络中神经元的参数
model = {}
# 随机初始化第一层的神经元参数总共200个神经元
model['W1'] = np.random.randn(H, D) / np.sqrt(D)
# 随机初始化第二层的神经元参数总共200个神经元
model['W2'] = np.random.randn(H) / np.sqrt(H)

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

# 神经网络的前向传播x为输入的帧差图
def policy_forward(x):
    h = np.dot(model['W1'], x)
    # relu
    h[h < 0] = 0
    logp = np.dot(model['W2'], h)
    # sigmoid激活
    p = sigmoid(logp)
    # p为下一步要往下挪的概率h为隐藏层中神经元的参数
    return p, h


# 算每层的参数偏导eph为一个游戏序列的隐藏层中神经元的参数epdlogp为一个游戏序列中反馈期望的偏导。
def policy_backward(eph, epdlogp):
    dW2 = np.dot(eph.T, epdlogp).ravel()
    dh = np.outer(epdlogp, model['W2'])
    dh[eph <= 0] = 0
    dW1 = np.dot(dh.T, epx)
    return {'W1': dW1, 'W2': dW2}

```

## 训练神经网络

```python
while True:
    env.render()

    # 游戏画面预处理
    cur_x = prepro(observation)
    # 得到帧差图
    x = cur_x - prev_x if prev_x is not None else np.zeros(D)
    # 将上一帧更新为当前帧
    prev_x = cur_x

    #前向传播
    aprob, h = policy_forward(x)
    #从动作概率分布中采样action=2表示往上挪action=3表示往下挪
    action = 2 if np.random.uniform() < aprob else 3

    # 环境
    xs.append(x)  
    # 隐藏层状态
    hs.append(h) 
    # 将2和3改成1和0因为sigmoid函数的导数为f(x)*(1-f(x))
    y = 1 if action == 2 else 0
    dlogps.append(y - aprob)

    # 把采样到的动作传回环境
    observation, reward, done, info = env.step(action)
    # 如果得一分则reward为1丢一份则reward为-1
    reward_sum += reward

    # 记录反馈
    drs.append(reward)

    # 当有一方得到21分后游戏结束
    if done:
        episode_number += 1

        epx = np.vstack(xs)
        eph = np.vstack(hs)
        epdlogp = np.vstack(dlogps)
        epr = np.vstack(drs)
        discounted_epr = discount_rewards(epr)
        # 将反馈进行zscore归一化有利于训练
        discounted_epr -= np.mean(discounted_epr)
        discounted_epr /= np.std(discounted_epr)

        #算期望
        epdlogp *= discounted_epr
        #算梯度
        grad = policy_backward(eph, epdlogp)
        for k in model:
            grad_buffer[k] += grad[k]

        # 每batch_size次游戏更新一次参数
        if episode_number % batch_size == 0:
            #rmsprop梯度上升
            for k, v in model.items():
                g = grad_buffer[k]
                rmsprop_cache[k] = decay_rate * rmsprop_cache[k] + (1 - decay_rate) * g ** 2
                model[k] += learning_rate * g / (np.sqrt(rmsprop_cache[k]) + 1e-5)
                grad_buffer[k] = np.zeros_like(v)

        # 每100把之后保存模型
        if episode_number % 100 == 0:
            pickle.dump(model, open('save.p', 'wb'))
        reward_sum = 0
        # 重置游戏
        observation = env.reset()
        prev_x = None
```

## 加载模型玩游戏

经过漫长的训练过程后,我们可以将训练好的模型加载进来开始玩游戏了。

```python
import numpy as np
import pickle
import gym

model = pickle.load(open('save.p', 'rb'))

env = gym.make("Pong-v0")
observation = env.reset()

while True:
    env.render()
    cur_x = prepro(observation)
    x = cur_x - prev_x if prev_x is not None else np.zeros(80*80)
    prev_x = cur_x
    aprob, h = policy_forward(x)
    #从动作概率分布中采样
    action = 2 if np.random.uniform() < aprob else 3
    observation, reward, done, info = env.step(action)

    if done:
        observation = env.reset()
        prev_x = None

```