提示:转载请注明出处,若文本无意侵犯到您的合法权益,请及时与作者联系。
声明
一、DQN算法是什么?
二、DQN算法的工程化
1.DQN算法的整体流程
2.搭建记忆池存储Agent轨迹
2.1 搭建记忆池
2.2 向记忆池中存储数据
2.3 从记忆池中取出数据
3.使用神经网络进行Q值的学习
3.1 搭建两个神经网络的网络结构
3.2 从采样数据中学习
3.3 以固定频率复制evql_net参数
4.Agent如何选择动作
本文是作者学习莫烦Python的代码笔记总结,如想深入可移步莫烦Python的该课程。本文只讨论Agent角度的代码实现。
DQN算法的算法流程如下:
以上就是DQN算法的基本流程,它是在Q-Learning算法的基础上进行了一些升级改造,主要包括:
从DQN的算法流程中,我们可以看出DQN算法与环境交互的整体流程也是一个双层循环:
def run_maze():
step = 0 # 用来控制什么时候学习
for episode in range(300):
# 初始化环境
observation = env.reset()
while True:
# 刷新环境
env.render()
# DQN 根据观测值选择行为
action = RL.choose_action(observation)
# 环境根据行为给出下一个 state, reward, 是否终止
observation_, reward, done = env.step(action)
# DQN 存储记忆
RL.store_transition(observation, action, reward, observation_)
# 控制学习起始时间和频率 (先累积一些记忆再开始学习)
if (step > 200) and (step % 5 == 0):
RL.learn()
# 将下一个 state_ 变为 下次循环的 state
observation = observation_
# 如果终止, 就跳出循环
if done:
break
step += 1 # 总步数
# end of game
print('game over')
env.destroy()
在上述算法流程中,存在2个难点,首先是如何搭建一个记忆池,其次是如何搭建两个神经网络来交替学习Q值。
首先我们要明确开发需求,我们想要的是一个可以存储Agent在每个step下的状态、动作、立即回报和后继状态的记忆库,然后在需要的时候可以从里面随机取出一定数量的记录来进行学习。
好的,现在来对以上需求进行工程化描述:
- 我们需要一个二维数组作为记忆存储库,其每一行都是一个step的观测值、动作、立即回报和后继观测值的元素集合;
- 这个二维数组可以动态添加一行数据,并且存在一个最大行数的限制,当超过这个大小添加一行数据时则从头开始覆盖;
- 我们要可以从这个二维数组中随机取出若干行数据进行使用。
接下来,我们分别从"搭建记忆池"、"向记忆池中存储数据"和"从记忆池中取出数据"三个角度来叙述。
一个满足我们需求的二维数组需要通过3个参数来协助完成:
n_features = 2 # 观测特征数,影响二维数组列数,初始化后不再改变
memory_size = 2000 # 二维数组行数,初始化后不再改变
memory_counter = 0 # 当前添加行数,每个step都要更新自身
现在我们先来初始化我们的二维数组,我们要创建一个行数为memory_size,列数为n_features*2+2,初始值全为0的二维数组:
memory = np.zeros((self.memory_size, n_features*2+2))
这里我们直接使用np.zeros()函数来创建了一个全零的二维矩阵,和二维数组等价。为什么我们的列数是n_features*2+2呢?
每一行的数据存储的是观测值、动作、状态、后继观测值,其中观测值和后继观测值不是一个数,而是一个元素集合,其集合个数为n_features,所以这行总共会有(n_features*2+2)个数据需要存储。
现在假设我们已经得到了要存储的数据,用s,a,r,s_来分别表示:观测值、动作、状态、后继观测值。现在我们要将这些数据添加为二维数组的一行,首先,我们将s,a,r,s_存储到一个数组中,但是由于s和s_是一个数组,不是一个数,所以我们需要使用如下语句处理下(当然,如果你的二维数组希望每一行存储的数据类型是多样的,可以不进行这样的处理):
s=(1,2)
a=3
r=4
s_=(2,3)
#transition = (s,a,r,s_) # 输出((1, 2), 3, 4, (2, 3))
transition = np.hstack((s,a,r,s_)) # 拼凑数组 [1 2 3 4 2 3],该函数只接受一个参数
如果记忆池满了就从头覆盖添加,这里使用了求余数的一个编程技巧:
# 总 memory 大小是固定的, 如果超出总大小, 旧 memory 就被新 memory 替换
index = memory_counter % memory_size
memory[index, :] = transition # 替换过程
memory_counter += 1
假设我们每次需要从二维数组中随机取出batch_size=32的行数据,我们使用np的一个操作方法np.random.choice()方法从二维数组的行数中取出随机的索引,然后根据索引的数组取出相应的二维数组:
# 从 memory 中随机抽取 batch_size 这么多记忆
if memory_counter > self.memory_size:
sample_index = np.random.choice(memory_size, size=self.batch_size)
else:
sample_index = np.random.choice(memory_counter, size=self.batch_size)
batch_memory = memory[sample_index, :]
DQN算法中需要用到两个神经网络,我们这里暂且命名为eval_net和target_net
。这两个神经网络的网络结构是一样的,可以是CNN、RNN等,具体的网络结构等需要根据处理的问题来设计,不同的是两个神经网络的参数以及参数的更新时机。
eval_net
用于预测q_eval,即Q估计值
, 该神经网络具有最新的神经网络参数,会及时地进行参数更新。target_net
用于预测q_target
,即Q现实值, 该神经网络具有延迟的神经网络参数,不会及时地进行参数更新。
这里的案例我们使用的是都是双层的全连接神经网络(FNN),即包含一个隐藏层和一个输出层。使用TensorFlow搭建FNN的基本流程我们不再叙述(不懂的同学可以参考我的博客:使用TensorFlow搭建FNN(全连接神经网络)的基本步骤)
现在我们给出该案例中的两个神经网络的网络结构的大致描述:
- 输入层(Input):(n_features=2)个神经元,输入值为某个step的观测值observation,这个观测值由n_features个数组成。
- 隐藏层(L1):10个神经元。
- 输出层(L2):(n_action=4)个神经元,输出值为动作空间中每个动作的价值,动作空间是离散的n_action个数。
下面看第一个神经网络的搭建过程:
(1)使用占位符定义网络的输入输出数据形式:
s = tf.placeholder(tf.float32, [None, self.n_features], name='s') # 用来接收 observation
q_target = tf.placeholder(tf.float32, [None, self.n_actions], name='Q_target') # 用来接收 q_target 的值, 这个之后会通过计算得到
(2)设置神经层的配置参数:
# c_names(collections_names) 是在更新 target_net 参数时会用到
c_names, n_l1, w_initializer, b_initializer = \
['eval_net_params', tf.GraphKeys.GLOBAL_VARIABLES], 10, \
tf.random_normal_initializer(0., 0.3), tf.constant_initializer(0.1) # config of layers
上面分别定义了参数所属的集合名称、L1层的神经元个数、权重和阈值的初始化方式:
(3)搭建2层神经层
搭建第一层神经层:
# eval_net 的第一层. collections 是在更新 target_net 参数时会用到
with tf.variable_scope('l1'):
w1 = tf.get_variable('w1', [n_features, n_l1], initializer=w_initializer, collections=c_names)
b1 = tf.get_variable('b1', [1, n_l1], initializer=b_initializer, collections=c_names)
l1 = tf.nn.relu(tf.matmul(self.s, w1) + b1)
搭建第二层神经层:
# eval_net 的第二层. collections 是在更新 target_net 参数时会用到
with tf.variable_scope('l2'):
w2 = tf.get_variable('w2', [n_l1, n_actions], initializer=w_initializer, collections=c_names)
b2 = tf.get_variable('b2', [1, n_actions], initializer=b_initializer, collections=c_names)
q_eval = tf.matmul(l1, w2) + b2
(4)搭建该神经网路的损失函数和优化器:
with tf.variable_scope('loss'): # 求误差
loss = tf.reduce_mean(tf.squared_difference(q_target, q_eval))
with tf.variable_scope('train'):# 梯度下降
train_op = tf.train.RMSPropOptimizer(lr).minimize(loss)
在上述过程中我们搭建了第一个神经网络eval_net,它的输入形式为s,输出形式为q_eval,标签形式为q_target.
同样地,我们可以搭建出第二个神经网络target_net,它的输入形式为s_,输出形式为q_next。
注意:eval_net是我们的真正用来决策的网络,是主要的网络,我们为它定义了损失函数和优化器,将会使用反向传播算法中的梯度下降算法来优化它的参数。
target_net并不会定义它的损失函数和优化器,因为这个网络只是帮助我们作为某个之前时刻的eval_net的替代,相当于作为一个过去的"eval_net"来帮助我们进行计算,它的参数是以固定频率复制eval_net的参数。
这一步就是DQN算法实现的难点,如何从记忆库中取出采样数据?如何从采样数据学习来更新神经网络?
之前我们已经解决了前一个问题,现在我们来解决后一个问题,假设我们已经得到了采样数据:行数为batch_size,列数为n_features*2+2的二维矩阵batch_data
现在我们要学习更新我们的eval_net网络的参数,在之前的网络搭建中我们已经搭建了它的优化器,现在只需要传入参数即可:
# 进行神经网络的参数优化
_,self.cost = self.Session.run(fetches=[self.train_step,self.loss],
feed_dict={
self.s: batch_data[:,:self.n_features],
self.q_target:q_target})
因为我们已经得到了batch_data,所以可以从这个矩阵中取出包含所有观测值的子矩阵作为s的输入,而Q现实值(标签)q_target则需要我们进行计算。
注意,这里的q_target是一个shape为(batch_size,n_actions)的二维矩阵,我们在计算的时候采样了一个编程技巧:
因为损失函数为(q_target-q_evql)的平方和,我们这里直接先使用q_target复制q_evql的值,然后使用q_next、reward和gamma来只更新采取的值,即在q_target我们每一行只会更新一个动作价值。
q_target的更新计算公式如下:
q_target[batch_index,eval_act_index] = reward+self.gamma*np.max(q_next,axis=1)
如此我们只需要求出batch_index,eval_act_index、q_next、q_evql和reward、gamma。
# 像两个神经网络中输入观测值获取对应的动作价值,输出为行数为采样个数,列数为动作数的矩阵
q_eval,q_next = self.Session.run(
fetches=[self.q_eval,self.q_next],
feed_dict = {
self.s:batch_data[:,:self.n_features],
self.s_:batch_data[:,-self.n_features:]
})
# 获取立即回报
q_target = q_eval.copy()
# 获取采样数据的索引,要修改的矩阵的行
batch_index = np.arange(self.batch_size,dtype=np.int32)
# 获取评估的动作的索引,要修改的矩阵的列
eval_act_index = batch_data[:,self.n_features].astype(int)
# 获取要修改Q值的立即回报
reward = batch_data[:, self.n_features + 1]
我们前面提到,target_net是eval_net的"以前的状态'',随着训练进行,这个网络的参数也要进行,如果将eval_net的网络参数直接复制给target_net呢?
# 当前学习更新eval_target次数
self.learn_step_counter=0
# eval_net的网络参数集合
t_params = tf.get_collection('target_net_params')
# target_net的网络参数集合
e_params = tf.get_collection('eval_net_params')
# 遍历替换变量
self.replace_target_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]
接下来只需要在每次学习的时候检查下是否需要更新target_net的网络参数:
self.learn_step_counter += 1
# 检查是否复制参数给target_net
if self.learn_step_counter % self.replace_target_iter == 0:
self.Session.run(self.replace_target_op)
print('\ntarget_net的参数被更新\n')
本部分时Agent以什么样的策略采取策略,输入观测值,输出选择的动作。
首先要对输入的观测值进行一个处理,将其从一维数组转换为二维数组:
# 将一维数组转换为二维数组,虽然只有一行
s = s[np.newaxis,:]
在本文中Agent以epsilon greedy策略选择动作,即又epsilon的概率选择价值最大的动作,有1-epsilon的概率随机选择一个动作:
if np.random.uniform()
以上的难点选择价值最大的动作?使用的是神经网络来正向输入一个观测值,测到其对应的该观测值下的所有的动作价值,然后使用np.argmax()函数来获得动作最大的索引。