5.6 Deep Q Network算法

在状态空间和动作空间离散且有限的情况下,Sarsa和Q Learning算法可以非常有效地解决强化学习的问题,但是当状态空间和动作空间是连续空间时,Sarsa和Q Learning算法难以胜任,基于二维数组表示Q函数难以实现,这个时候就需要其他算法来解决了。比如在CartPole问题中,黑色的小车上面支撑的一个连杆,连杆会自由摆动,我们需要控制黑色的小车,通过左右移动小车,保持连杆的平衡(见图5-4)。该问题的动作空间是离散且有限的,只有两种状态,但是该问题的状态空间是一个连续空间,且每个状态是个四维向量。

图5-4 CartPole问题

Google在2013年和2015年分别发表了两篇堪称经典的关于强化学习的论文,分别为:

·Mnih,Volodymyr,et al."Playing atari with deep reinforcement learning."arXiv preprint arXiv:1312.5602(2013)

·Mnih,Volodymyr,et al."Human-level control through deep reinforcement learning."Nature 518.7540(2015):529-533

如图5-5所示,Google在论文中提出了使用深度学习网络结合Q Learning算法解决强化学习问题的思路。其中论文中涉及的问题,状态空间通常是高维的数据,比如空间位置甚至是图像;动作空间通常是低维的离散动作,比如左右移动两个动作。

图5-5 通过DQN解决强化学习问题的例子

DQN(Deep Q Network)算法的核心思路有以下几点:

·使用深度学习网络表示Q函数,训练的数据是状态s,训练的标签是状态s对应的每个动作的Q值,即标签是由Q值组成的向量,向量的长度与动作空间的长度相同。

·动作选择的算法使用∈-贪婪算法,其中可以是静态的也可以是随着时间动态变化的。

·Q值的更新与Q Leaning算法相同。

·定义一段所谓的记忆体,在记忆体中保存具体某一时刻的当前状态、奖励、动作、迁移到的下一个状态、状态是否结束等信息,定期从记忆体中随机选择固定大小的一段记忆,用于训练深度神经网络。

DQN算法的证明过程已经超出了本书范围,有兴趣的读者可以阅读前文中提到的两篇论文。

案例5-3:使用DQN算法处理CartPole问题

我们尝试使用DQN算法来处理这个问题,本例代码在本书对应的GitHub的code/dqn-cartpole-v0.py文件,整个代码的实现基于Keras。

1.定义DQNAgent类

我们把DQN的算法封装成一个类DQNAgent,DQNAgent的定义如图5-6所示,其中比较重要的属性的含义为:

·state_size,定义状态的维度大小,本例中状态是个四维向量,所以大小为4。

·action_size,定义动作空间的大小,本例动作空间只包含两个动作,所以大小为2。

·memory,使用一个队列使用的记忆体,用于保存所谓的记忆。

·gamma,衰减因子。

·epsilon,∈-贪婪算法中的∈。

·model,保存深度学习网络。

其中比较重要的方法的功能为:

·build_model,创建深度学习网络。

·remember,保存记忆,即保存具体某一时刻的当前状态、奖励、动作、迁移到的下一个状态、状态是否结束等信息。

·act,基于∈-贪婪算法选择动作。

·replay,重放记忆,即定期从记忆体中随机选择固定大小的一段记忆,用于训练深度神经网络。

图5-6 DQNAgent类图

2.创建深度学习网络。

常见的深度学习网络包括多层感知机(MLP),卷积神经网络(CNN)以及循环神经网络(RNN),本例中使用最基本的MLP,其结构如图5-7所示。

图5-7 MLP结构图

在Keras中构造MLP非常容易,首先定义输入层,输入层的节点数与状态的维度大小相同都为4,然后接着是两个节点数均为24的隐藏层,输入层、隐藏层和输入层都是用全连接,其中输入层和隐藏层,隐藏层之间的激活函数都用relu,隐藏层和输入层之间的激活函数用linear函数,代码如下:


def _build_model(self):
    model = Sequential()
    model.add(Dense(24, input_dim=self.state_size, activation='relu'))
    model.add(Dense(24, activation='relu'))
    model.add(Dense(self.action_size, activation='linear'))
    model.compile(loss='mse',
                  optimizer=Adam(lr=self.learning_rate))
    return model

Keras自身也支持将MLP结构可视化,需要引用如下函数:


from keras.utils.vis_utils import plot_model

同时为了支持可视化,还要安装软件graphviz,对应地址为:


http://www.graphviz.org/

然后安装两个辅助的库:


pip install graphviz
pip install pydot==1.1.0

至此,就可以在创建MLP后调用函数将MLP可视化了,如图5-8所示。


plot_model(self.model, to_file='dqn-cartpole-v0-mlp.png', show_shapes=True)

图5-8 Keras下可视化的MLP结构图(展示细节)

如果不想展示太多细节,仅查看基本的MLP结构,可以将参数show_shapes设置为False,结果如图5-9所示。


plot_model(self.model, to_file='dqn-cartpole-v0-mlp.png', show_shapes=False)

图5-9 Keras下可视化的MLP结构图(不展示细节)

3.实现记忆以及回放功能。

在本例中,记忆体是用Python的队列实现,队列大小为2000,队列满后自动按照插入队列的时间先后顺序丢弃:


self.memory = deque(maxlen=2000)

我们定义的记忆内容为某一时刻的当前状态、动作、奖励、迁移到的下一个状态、状态是否结束等信息,具体的定义为:


(state, action, reward, next_state, done)

那么记忆功能的实现函数为remember,记忆的过程就是插入队列的过程:


def remember(self, state, action, reward, next_state, done):
    self.memory.append((state, action, reward, next_state, done))

回放的功能相对复杂。回放的核心功能是从记忆体中随机选择固定大小的一段记忆,用于训练深度神经网络,之所以需要随机选择时因为连续的几个记忆存在较大的关联性,会响应Q值的计算效果,也就是影响深度神经网络的训练过程。我们设置每次随机选择的大小为batch_size,随机选择的功能是用Python的random.sample函数实现的,代码如下:


def replay(self, batch_size):
    minibatch = random.sample(self.memory, batch_size)
    for state, action, reward, next_state, done in minibatch:
参数done保存的是该记忆对应的时刻游戏是否结束了,只有当游戏尚未结束时我们才需要使用Q Learning的算法更新Q值。在Q Learning的算法中,需要从下一个状态next_state对应的Q值列表中选择Q值最大的action,正好深度神经网络的标签内容为该状态下各个动作对应的Q值组成的多维数组,使用Python的np.amax选择其中Q值最大对应的索引即可。
target = reward
        if not done:
            target = (reward + self.gamma *
                      np.amax(self.model.predict(next_state)[0]))

计算完Q值以后就需要使用更新了动作action对应的Q值的标签,然后重新去训练深度神经网络。具体方法是使用state从深度神经网络中获取对应的标签target_f,然后更新action对应的Q值,再使用更新后标签target_f和state重新训练网络,代码如下:


target_f = self.model.predict(state)
target_f[0][action] = target
self.model.fit(state, target_f, epochs=1, verbose=0)

4.实现∈-贪婪算法。

在DQN中实现∈-贪婪算法略有不同,因为Q函数是以深度神经网络的形式存在。使用np.random.rand()产生0~1之间随机数,当小于等于self.epsilon时,就在全部action中随机选择一个,概率分布满足平均分布。当大于self.epsilon时,使用贪婪算法从state对应的Q值列表中选择Q值最大的一个的索引,该索引对应的就是满足条件的最佳action,Python的np.argmax可以完成这个功能,代码如下:


def act(self, state):
    if np.random.rand() <= self.epsilon:
        return random.randrange(self.action_size)
    act_values = self.model.predict(state)
    return np.argmax(act_values[0])

5.使用DQN进行强化学习。

我们实现了完整的DQNAgent以后,就可以使用它开始强化学习。

首先我们初始化CartPole-v0环境,获取对应的状态的维度大小以及动作空间的大小,设置批处理的大小为32:


env = gym.make('CartPole-v0')
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
batch_size = 32

然后我们为state_size和action_size创建一个DQNAgent对象:


agent = DQNAgent(state_size, action_size)

设置学习次数EPISODES为1000,即循环1000次进行学习,每次学习的时候都需要初始化环境。设置每次学习的步长为500,我们也可以根据游戏持续的步长来衡量算法的好坏,步长越大说明坚持的时间越长,因此我们定义分数score就等于坚持的步长。如果希望整个过程可视化的话,需要调用env.render()进行绘图,反之就注释掉,代码如下:


EPISODES = 1000
for e in range(EPISODES):
    state = env.reset()
    state = np.reshape(state, [1, state_size])
    for time in range(500):
        #env.render()

使用∈-贪婪算法,根据当前状态state选择最佳的动作action,然后执行动作action,使状态产生迁移,获得对应的下一个状态next_state,奖励reward以及标记是否游戏结束的标志位done。如果游戏结束,需要把奖励reward设置为-10。将当前的状态state,动作action,奖励reward保存到记忆体中,代码如下:


action = agent.act(state)
next_state, reward, done, _ = env.step(action)
reward = reward if not done else -10
next_state = np.reshape(next_state, [1, state_size])
agent.remember(state, action, reward, next_state, done)
state = next_state

定期检测记忆的大小,如果大于批处理大小,就进行记忆回放:


if len(agent.memory) > batch_size:
    agent.replay(batch_size)

完整地执行整个程序,学习1000次,score从最开始的个位数到最后199左右,平均分数也达到了110分,比较不错了:


episode: 996/1000, score: 199, e: 0.4
episode: 997/1000, score: 199, e: 0.4
episode: 998/1000, score: 199, e: 0.4
episode: 999/1000, score: 199, e: 0.4
Avg score:110