强化学习之DQN(附莫烦代码)

1.简介

想象用Q-learning 电子游戏的每一帧来学习电子游戏,每个图片就可以是一种状态,游戏中的角色又可以有多种动作(上下左右,下蹲跳跃等等)。如果用Q表来记录每一个动作所对应的状态,那么这张Q表将大到无法想象。DQN不用Q表记录Q值,而是用神经网络来预测Q值,并通过不断更新神经网络从而学习到最优的行动路径。

深度 Q 网络(DQN)是将 Q learning 和卷积神经网络(CNN)结合在一起

Off-policy是Q-Learning的特点,DQN中也延用了这一特点。而不同的是,Q-Learning中用来计算target和预测值的Q是同一个Q,也就是说使用了相同的神经网络。这样带来的一个问题就是,每次更新神经网络的时候,target也都会更新,这样会容易导致参数不收敛。回忆在有监督学习中,标签label都是固定的,不会随着参数的更新而改变。

因此DQN在原来的Q网络的基础上又引入了一个target Q网络,即用来计算target的网络。它和Q网络结构一样,初始的权重也一样,只是Q网络每次迭代都会更新,而target Q网络是每隔一段时间才会更新。DQN的target是 Rt+1+γmaxa′Q(St+1,a′;ω−)。用 ω−表示它比Q网络的权重 ω更新得要慢一些。在训练神经网络参数时用到的损失函数(Loss function),实际上就是q_target 减 q_eval的结果 (loss = q_target- q_eval )

强化学习之DQN(附莫烦代码)_第1张图片

反向传播真正训练的网络是只有一个,就是eval_net。target_net 只做正向传播得到q_target (q_target = r +γ*max Q(s,a)). 其中 Q(s,a)是若干个经过target-net正向传播的结果。

相比于Q-Learning,DQN做的改进:一个是使用了卷积神经网络来逼近行为值函数,一个是使用了target Q network来更新target,还有一个是使用了经验回放Experience replay。由于在强化学习中,我们得到的观测数据是有序的,step by step的,用这样的数据去更新神经网络的参数会有问题。回忆在有监督学习中,数据之间都是独立的。因此DQN中使用经验回放,即用一个Memory来存储经历过的数据,每次更新参数的时候从Memory中抽取一部分的数据来用于更新,以此来打破数据间的关联。

  • 首先初始化Memory D,它的容量为N;
  • 初始化Q网络,随机生成权重ω;
  • 初始化target Q网络,权重为ω−=ω;
  • 循环遍历episode =1, 2, …, M:
  • 初始化initial state S1;
  • 循环遍历step =1,2,…, T:
    • 用ϵ−greedy策略生成action at:以ϵ概率选择一个随机的action,或选择at=maxaQ(St,a;ω);
    • 执行action at,接收reward rt及新的state St+1;
    • 将transition样本 (St,at,rt,St+1)存入D中;
    • 从D中随机抽取一个minibatch的transitions(Sj,aj,rj,Sj+1);
    • 令yj=rj,如果 j+1步是terminal的话,否则,令 yj=rj+γmaxa′Q(St+1,a′;ω−);
    • 对(yj−Q(St,aj;ω))2关于ω使用梯度下降法进行更新;
    • 每隔C steps更新target Q网络,ω−=ω。
  • End For;
  • End For.

2.代码展示

(1)这里利用 Scipy 的 imresize 函数来下采样图像。函数 preprocess 会在将图像输入到 DQN 之前,对图像进行预处理:

def preprocess(img):
    img_temp=img[31:195]   #choose the important area of the image
    img_temp=img_temp.mean(axis=2)   #convert to Grayscale
    #downsample image
    img_temp=imresize(img_temp,size=(IM_SIZE,IM_SIZE),interp='nearest')
    return img_temp

IM_SIZE 是一个全局参数,这里设置为 80。该函数具有描述每个步骤的注释。下面是预处理前后的观测空间:

强化学习之DQN(附莫烦代码)_第2张图片

考虑四个动作和观测序列来确定当前情况并训练智能体。update_state 函数用来将当前观测状态附加到以前的状态,从而产生状态序列:

def update_state(state,obs):
    obs_small=preprocess(obs)
    return np.append(state[1:],np.expand_dims(obs_small,0),axis=0)

(2)导入必要的模块。使用 sys 模块的 stdout.flush() 来刷新标准输出(此例中是计算机屏幕)中的数据。random 模块用于从经验回放缓存(存储过去经验的缓存)中获得随机样本。datatime 模块用于记录训练花费的时间:

强化学习之DQN(附莫烦代码)_第3张图片

定义训练的超参数,可以尝试改变它们,定义了经验回放缓存的最小和最大尺寸,以及目标网络更新的次数:

定义 DQN 类,构造器使用 tf.contrib.layers.conv2d 函数构建 CNN 网络,定义损失和训练操作:

 强化学习之DQN(附莫烦代码)_第4张图片

类中用 set_session() 函数建立会话,用 predict() 预测动作值函数,用 update() 更新网络,在 sample_action() 函数中用 Epsilon 贪婪算法选择动作:

强化学习之DQN(附莫烦代码)_第5张图片

另外还定义了加载和保存网络的方法,因为训练需要消耗大量时间:

强化学习之DQN(附莫烦代码)_第6张图片

定义将主 DQN 网络的参数复制到目标网络的方法如下:

强化学习之DQN(附莫烦代码)_第7张图片

定义函数 learn(),预测价值函数并更新原始的 DQN 网络:

 

强化学习之DQN(附莫烦代码)_第8张图片

现在已经在主代码中定义了所有要素,下面构建和训练一个 DQN 网络来玩 Atari 的游戏。代码中有详细的注释,这主要是之前 Q learning 代码的一个扩展,增加了经验回放缓存:

强化学习之DQN(附莫烦代码)_第9张图片

下图是每 100 次运行的平均奖励,更清晰地展示了奖励的提高:

强化学习之DQN(附莫烦代码)_第10张图片

这只是在前 500 次运行后的训练结果。要想获得更好的结果,需要训练更多次,大约 1 万次。训练智能体需要运行很多次游戏,消耗大量的时间和内存。OpenAI Gym 提供了一个封装,将游戏保存为一个视频,因此,无须 render 函数,你可以使用这个封装来保存视频并在以后查看智能体是如何学习的。AI 工程师和爱好者可以上传这些视频来展示他们的结果。

 

莫烦代码

神经网络的搭建
为了使用 Tensorflow 来实现 DQN, 比较推荐的方式是搭建两个神经网络, target_net 用于预测 q_target 值, 他不会及时更新参数,eval_net 用于预测 q_eval, 这个神经网络拥有最新的神经网络参数. 不过这两个神经网络结构是完全一样的, 只是里面的参数不一样。两个神经网络是为了固定住一个神经网络 (target_net) 的参数, target_net 是 eval_net 的一个历史版本, 拥有 eval_net 很久之前的一组参数, 而且这组参数被固定一段时间, 然后再被 eval_net 的新参数所替换. 而 eval_net 是不断在被提升的, 所以是一个可以被训练的网络 trainable=True. 而 target_net 的 trainable=False。

class DeepQNetwork:
    def _build_net(self):
        # ------------------ build evaluate_net ------------------
        self.s = tf.placeholder(tf.float32, [None, self.n_features], name='s')  # input
        self.q_target = tf.placeholder(tf.float32, [None, self.n_actions], name='Q_target')  # for calculating loss
        with tf.variable_scope('eval_net'):
            # c_names(collections_names) are the collections to store variables
            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

            # first layer. collections is used later when assign to target net
            with tf.variable_scope('l1'):
                w1 = tf.get_variable('w1', [self.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)

            # second layer. collections is used later when assign to target net
            with tf.variable_scope('l2'):
                w2 = tf.get_variable('w2', [n_l1, self.n_actions], initializer=w_initializer, collections=c_names)
                b2 = tf.get_variable('b2', [1, self.n_actions], initializer=b_initializer, collections=c_names)
                self.q_eval = tf.matmul(l1, w2) + b2

        with tf.variable_scope('loss'):
            self.loss = tf.reduce_mean(tf.squared_difference(self.q_target, self.q_eval))
        with tf.variable_scope('train'):
            self._train_op = tf.train.RMSPropOptimizer(self.lr).minimize(self.loss)

        # ------------------ build target_net ------------------
        self.s_ = tf.placeholder(tf.float32, [None, self.n_features], name='s_')    # input
        with tf.variable_scope('target_net'):
            # c_names(collections_names) are the collections to store variables
            c_names = ['target_net_params', tf.GraphKeys.GLOBAL_VARIABLES]

            # first layer. collections is used later when assign to target net
            with tf.variable_scope('l1'):
                w1 = tf.get_variable('w1', [self.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)

            # second layer. collections is used later when assign to target net
            with tf.variable_scope('l2'):
                w2 = tf.get_variable('w2', [n_l1, self.n_actions], initializer=w_initializer, collections=c_names)
                b2 = tf.get_variable('b2', [1, self.n_actions], initializer=b_initializer, collections=c_names)
                self.q_next = tf.matmul(l1, w2) + b2

2.思维决策的过程
定义完上次的神经网络部分以后, 这次来定义其他部分,首先是函数值的初始化。

class DeepQNetwork:
    def __init__(
            self,
            n_actions,
            n_features,
            learning_rate=0.01,
            reward_decay=0.9,
            e_greedy=0.9,
            replace_target_iter=300,
            memory_size=500,
            batch_size=32,
            e_greedy_increment=None,
            output_graph=False,
    ):
        self.n_actions = n_actions
        self.n_features = n_features
        self.lr = learning_rate
        self.gamma = reward_decay
        self.epsilon_max = e_greedy  # epsilon 的最大值
        self.replace_target_iter = replace_target_iter    # 更换 target_net 的步数
        self.memory_size = memory_size    # 记忆上限
        self.batch_size = batch_size       # 每次更新时从 memory 里面取多少记忆出来
        self.epsilon_increment = e_greedy_increment   # epsilon 的增量
        self.epsilon = 0 if e_greedy_increment is not None else self.epsilon_max     # 是否开启探索模式, 并逐步减少探索次数

        # total learning step
        self.learn_step_counter = 0

        # initialize zero memory [s, a, r, s_]
        self.memory = np.zeros((self.memory_size, n_features * 2 + 2))

        # consist of [target_net, evaluate_net]
        self._build_net()
        t_params = tf.get_collection('target_net_params')  # 提取 target_net 的参数
        e_params = tf.get_collection('eval_net_params')    # 提取  eval_net 的参数
        self.replace_target_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]

        self.sess = tf.Session()

        if output_graph:
            # $ tensorboard --logdir=logs
            # tf.train.SummaryWriter soon be deprecated, use following
            tf.summary.FileWriter("logs/", self.sess.graph)

        self.sess.run(tf.global_variables_initializer())
        self.cost_his = []   # 记录所有 cost 变化, 用于最后 plot 出来观看

记忆存储,DQN 的精髓部分之一: 记录下所有经历过的步, 这些步可以进行反复的学习, 所以是一种 off-policy 方法

class DeepQNetwork:
    def store_transition(self, s, a, r, s_):
        if not hasattr(self, 'memory_counter'):
            self.memory_counter = 0

        transition = np.hstack((s, [a, r], s_))

        # replace the old memory with new memory
        index = self.memory_counter % self.memory_size
        self.memory[index, :] = transition

        self.memory_counter += 1

行为选择,让 eval_net 神经网络生成所有 action 的值, 并选择值最大的 action;学习过程就是在 DeepQNetwork 中, 是如何学习, 更新参数的. 这里涉及了 target_net 和 eval_net 的交互使用,这是非常重要的一步。

class DeepQNetwork:
    def choose_action(self, observation):
        # to have batch dimension when feed into tf placeholder
        observation = observation[np.newaxis, :]

        if np.random.uniform() < self.epsilon:
            # forward feed the observation and get q value for every actions
            actions_value = self.sess.run(self.q_eval, feed_dict={self.s: observation})
            action = np.argmax(actions_value)
        else:
            action = np.random.randint(0, self.n_actions)
        return action

    def learn(self):
        # check to replace target parameters
        if self.learn_step_counter % self.replace_target_iter == 0:
            self.sess.run(self.replace_target_op)
            print('\ntarget_params_replaced\n')

        # sample batch memory from all memory
        if self.memory_counter > self.memory_size:
            sample_index = np.random.choice(self.memory_size, size=self.batch_size)
        else:
            sample_index = np.random.choice(self.memory_counter, size=self.batch_size)
        batch_memory = self.memory[sample_index, :]

         # 获取 q_next (target_net 产生了 q) 和 q_eval(eval_net 产生的 q)
        q_next, q_eval = self.sess.run(
            [self.q_next, self.q_eval],
            feed_dict={
                self.s_: batch_memory[:, -self.n_features:],  # fixed params
                self.s: batch_memory[:, :self.n_features],  # newest params
            })

        # 下面这几步十分重要. q_next, q_eval 包含所有 action 的值,
        # 而我们需要的只是已经选择好的 action 的值, 其他的并不需要.
        # 所以我们将其他的 action 值全变成 0, 将用到的 action 误差值 反向传递回去, 作为更新凭据.
        # 这是我们最终要达到的样子, 比如 q_target - q_eval = [1, 0, 0] - [-1, 0, 0] = [2, 0, 0]
        # q_eval = [-1, 0, 0] 表示这一个记忆中有我选用过 action 0, 而 action 0 带来的 Q(s, a0) = -1, 所以其他的 Q(s, a1) = Q(s, a2) = 0.
        # q_target = [1, 0, 0] 表示这个记忆中的 r+gamma*maxQ(s_) = 1, 而且不管在 s_ 上我们取了哪个 action,
        # 我们都需要对应上 q_eval 中的 action 位置, 所以就将 1 放在了 action 0 的位置.

        # 下面是为了达到上面说的目的, 不过为了更方面让程序运算, 达到目的的过程有点不同.
        # 是将 q_eval 全部赋值给 q_target, 这时 q_target-q_eval 全为 0,
        # 不过 我们再根据 batch_memory 当中的 action 这个 column 来给 q_target 中的对应的 memory-action 位置来修改赋值.
        # 使新的赋值为 reward + gamma * maxQ(s_), 这样 q_target-q_eval 就可以变成我们所需的样子.
        # change q_target w.r.t q_eval's action
        q_target = q_eval.copy()

        batch_index = np.arange(self.batch_size, dtype=np.int32)
        eval_act_index = batch_memory[:, self.n_features].astype(int)
        reward = batch_memory[:, self.n_features + 1]

        q_target[batch_index, eval_act_index] = reward + self.gamma * np.max(q_next, axis=1)

        # train eval network
        _, self.cost = self.sess.run([self._train_op, self.loss],
                                     feed_dict={self.s: batch_memory[:, :self.n_features],
                                                self.q_target: q_target})
        self.cost_his.append(self.cost)

        # increasing epsilon
        self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
        self.learn_step_counter += 1

3.交互过程
DQN 与环境交互的过程总体与Q-Learning一致,仅仅增加了记忆存储的过程,这与前边提到的 “Q-Leaning 方法基于当前策略进行交互和改进,每一次模型利用交互生成的数据进行学习,学习后的样本被直接丢弃” 是一致的。

from maze_env import Maze
from RL_brain import DeepQNetwork

def run_maze():
    step = 0
    for episode in range(1000):
        # initial observation
        observation = env.reset()

        while True:
            # fresh env
            env.render()

            # RL choose action based on observation
            action = RL.choose_action(observation)

            # RL take action and get next observation and reward
            observation_, reward, done = env.step(action)

            RL.store_transition(observation, action, reward, observation_)

            if (step > 200) and (step % 5 == 0):
                RL.learn()

            # swap observation
            observation = observation_

            # break while loop when end of this episode
            if done:
                break
            step += 1

    # end of game
    print('game over')
    env.destroy()


if __name__ == "__main__":
    # maze game
    env = Maze()
    RL = DeepQNetwork(env.n_actions, env.n_features,
                      learning_rate=0.01,
                      reward_decay=0.9,
                      e_greedy=0.9,
                      replace_target_iter=200,
                      memory_size=20000,
                      output_graph=True
                      )
    env.after(100, run_maze)
    env.mainloop()
    RL.plot_cost()

最后表示对莫烦的感谢!!

 

 

 

 

你可能感兴趣的:((1)强化学习)