在状态空间和动作空间离散且有限的情况下,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算法的证明过程已经超出了本书范围,有兴趣的读者可以阅读前文中提到的两篇论文。
我们尝试使用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