莫烦Python代码实践(四)——DQN基础算法工程化解析

提示:转载请注明出处,若文本无意侵犯到您的合法权益,请及时与作者联系。


声明

本文是作者学习莫烦Python的代码笔记总结,如想深入可移步莫烦Python的该课程。本文只讨论Agent角度的代码实现。


一、DQN算法是什么?

DQN算法的算法流程如下:

以上就是DQN算法的基本流程,它是在Q-Learning算法的基础上进行了一些升级改造,主要包括:

  • 记忆库(experience replay)
  • 神经网络计算Q值
  • 暂时冻结q_target参数(切断相关性)

二、DQN算法的工程化

1.DQN算法的整体流程

从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值。

2.搭建记忆池存储Agent轨迹

首先我们要明确开发需求,我们想要的是一个可以存储Agent在每个step下的状态、动作、立即回报和后继状态的记忆库,然后在需要的时候可以从里面随机取出一定数量的记录来进行学习

好的,现在来对以上需求进行工程化描述:

  1. 我们需要一个二维数组作为记忆存储库,其每一行都是一个step的观测值动作立即回报后继观测值的元素集合;
  2. 这个二维数组可以动态添加一行数据,并且存在一个最大行数的限制,当超过这个大小添加一行数据时则从头开始覆盖;
  3. 我们要可以从这个二维数组中随机取出若干行数据进行使用。

接下来,我们分别从"搭建记忆池"、"向记忆池中存储数据"和"从记忆池中取出数据"三个角度来叙述。

2.1 搭建记忆池  

一个满足我们需求的二维数组需要通过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)个数据需要存储。

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

2.3 从记忆池中取出数据

假设我们每次需要从二维数组中随机取出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, :]

3.使用神经网络进行Q值的学习

3.1 搭建两个神经网络的网络结构

DQN算法中需要用到两个神经网络,我们这里暂且命名为eval_nettarget_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的参数。

3.2 从采样数据中学习

这一步就是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]

3.3 以固定频率复制evql_net参数

我们前面提到,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')

4.Agent如何选择动作

本部分时Agent以什么样的策略采取策略,输入观测值,输出选择的动作。

首先要对输入的观测值进行一个处理,将其从一维数组转换为二维数组:

# 将一维数组转换为二维数组,虽然只有一行
s = s[np.newaxis,:]

在本文中Agent以epsilon greedy策略选择动作,即又epsilon的概率选择价值最大的动作,有1-epsilon的概率随机选择一个动作:

        if np.random.uniform()<self.epsilon:
            action_values = self.Session.run( fetches=self.q_eval,feed_dict={self.s:s})
            action = np.argmax(action_values)
        else:
            action = np.random.randint(0,self.n_actions)
        return action

以上的难点选择价值最大的动作?使用的是神经网络来正向输入一个观测值,测到其对应的该观测值下的所有的动作价值,然后使用np.argmax()函数来获得动作最大的索引。


猜你喜欢

转载自blog.csdn.net/qq_41959920/article/details/109167264