莫烦强化学习笔记整理(五) DQN-part2

1、OpenAI gym

openAI gym代码地址link.

以下为一个通过晃动学习立杆子的小游戏,这篇文章中延伸DQN算法和基本DQN算法的比较就是通过这个立杆子游戏来实现。

""
Deep Q network,
Using:
Tensorflow: 1.0
gym: 0.7.3
""


import gym
from RL_brain import DeepQNetwork

env = gym.make('CartPole-v0') # 定义使用 gym 库中的那一个环境
env = env.unwrapped #不做这个会有很多限制

print(env.action_space)  # 查看这个环境中可用的 action 有多少个
print(env.observation_space)  # 查看这个环境中可用的 state 的 observation 有多少个
print(env.observation_space.high)  # 查看 observation 最高取值
print(env.observation_space.low)  # 查看 observation 最低取值

# 定义使用 DQN 的算法
RL = DeepQNetwork(n_actions=env.action_space.n,
                  n_features=env.observation_space.shape[0],
                  learning_rate=0.01, e_greedy=0.9,
                  replace_target_iter=100, memory_size=2000,
                  e_greedy_increment=0.001,)

total_steps = 0    # 记录步数


for i_episode in range(100):

    observation = env.reset()    # 获取回合 i_episode 第一个 observation
    ep_r = 0
    while True:
        env.render()   # 刷新环境

        action = RL.choose_action(observation)   # 选行为

        observation_, reward, done, info = env.step(action)

        # the smaller theta and closer to center the better
        x, x_dot, theta, theta_dot = observation_
        r1 = (env.x_threshold - abs(x))/env.x_threshold - 0.8
        r2 = (env.theta_threshold_radians - abs(theta))/env.theta_threshold_radians - 0.5
        reward = r1 + r2

 # x 是车的水平位移
 # theta 是棒子离垂直的角度, 角度越大, 越不垂直.
 # r1 是车越偏离中心, 分越少
 # r2 是棒越垂直, 分越高
 # 总 reward 是 r1 和 r2 的结合既考虑位置也考虑角度, 这样 DQN 学习更有效率
        RL.store_transition(observation, action, reward, observation_)   # 保存这一组记忆

        ep_r += reward
        if total_steps > 1000:
            RL.learn()

        if done:
            print('episode: ', i_episode,
                  'ep_r: ', round(ep_r, 2),
                  ' epsilon: ', round(RL.epsilon, 2))
            break

        observation = observation_
        total_steps += 1

RL.plot_cost()     # 最后输出 cost 曲线

2、Double DQN

Double_DQN代码地址link.

DQN 基于 Q-learning, Q-Learning 中的Qmax会导致 Q现实 当中的过估计 (overestimate), Double DQN 就是用来解决过估计问题的。

在实际问题中, 如果你输出你的 DQN 的 Q 值, 可能就会发现, Q 值都超级大. 这就是出现了过估计。

(1)算法对比

有两个神经网络: Q_eval (Q估计), Q_next (Q现实).用 Q估计 的神经网络估计 Q现实 中 Qmax(s’, a’) 的最大动作值. 然后用这个被 Q估计 估计出来的动作来选择 Q现实 中的 Q(s’).

在DQN中 Q_next = max(Q_next(s’, a_all)).
在这里插入图片描述

在Double DQN 中 Q_next = Q_next(s’, argmax(Q_eval(s’, a_all))).
在这里插入图片描述
Double DQN 和 DQN 在 tensorboard 中的结构相同,但是计算 q_target 的方法是不同的.
在这里插入图片描述
对比DDQ和DQN实现方法的不同
1、将 class 的名字改成 DoubleDQN

class DoubleDQN:   

2、初始化时加一个 double_q 参数来表示使用的是 Natural DQN 还是 Double DQN.

def __init__(..., double_q=True, sess=None):
        ...
        self.double_q = double_q    
        if sess is None:
            self.sess = tf.Session()
            self.sess.run(tf.global_variables_initializer())
        else:
            self.sess = sess

        ...

3、核心的learn算法不同,主要是计算q_target 的方法不同。

def learn(self):
        # 这一段和 DQN 一样:
        if self.learn_step_counter % self.replace_target_iter == 0:
            self.sess.run(self.replace_target_op)
            print('\ntarget_params_replaced\n')

        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, :]

        # 这一段和 DQN 不一样
        q_next, q_eval4next = self.sess.run(
            [self.q_next, self.q_eval],
            feed_dict={self.s_: batch_memory[:, -self.n_features:],    # next observation
                       self.s: batch_memory[:, -self.n_features:]})    # next observation
        q_eval = self.sess.run(self.q_eval, {self.s: batch_memory[:, :self.n_features]})
        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]

        if self.double_q:   # 如果是 Double DQN
            max_act4next = np.argmax(q_eval4next, axis=1)        # q_eval 得出的最高奖励动作
            selected_q_next = q_next[batch_index, max_act4next]  # Double DQN 选择 q_next 依据 q_eval 选出的动作
        else:       # 如果是 Natural DQN
            selected_q_next = np.max(q_next, axis=1)    # natural DQN

        q_target[batch_index, eval_act_index] = reward + self.gamma * selected_q_next


        # 这下面和 DQN 一样:
        _, 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)
        self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
        self.learn_step_counter += 1

(2)DDQN和DQN的对比结果

import gym
from RL_brain import DoubleDQN
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf


env = gym.make('Pendulum-v0')
env.seed(1) # 可重复实验
MEMORY_SIZE = 3000
ACTION_SPACE = 11    # 将原本的连续动作分离成 11 个动作

sess = tf.Session()
with tf.variable_scope('Natural_DQN'):
    natural_DQN = DoubleDQN(
        n_actions=ACTION_SPACE, n_features=3, memory_size=MEMORY_SIZE,
        e_greedy_increment=0.001, double_q=False, sess=sess
    )

with tf.variable_scope('Double_DQN'):
    double_DQN = DoubleDQN(
        n_actions=ACTION_SPACE, n_features=3, memory_size=MEMORY_SIZE,
        e_greedy_increment=0.001, double_q=True, sess=sess, output_graph=True)

sess.run(tf.global_variables_initializer())


def train(RL):
    total_steps = 0
    observation = env.reset()
    while True:
        # if total_steps - MEMORY_SIZE > 8000: env.render()

        action = RL.choose_action(observation)

        f_action = (action-(ACTION_SPACE-1)/2)/((ACTION_SPACE-1)/4)   # 在 [-2 ~ 2] 内离散化动作

        observation_, reward, done, info = env.step(np.array([f_action]))

        reward /= 10     # normalize 到这个区间 (-1, 0). 立起来的时候 reward = 0.
        # 立起来以后的 Q target 会变成 0, 因为 Q_target = r + gamma * Qmax(s', a') = 0 + gamma * 0
        # 所以这个状态时的 Q 值大于 0, 就出现了 overestimate.

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

        if total_steps > MEMORY_SIZE:   # learning
            RL.learn()

        if total_steps - MEMORY_SIZE > 20000:   # stop game
            break

        observation = observation_
        total_steps += 1
    return RL.q # 返回所有动作 Q 值

# train 两个不同的 DQN
q_natural = train(natural_DQN)
q_double = train(double_DQN)

# 出对比图
plt.plot(np.array(q_natural), c='r', label='natural')
plt.plot(np.array(q_double), c='b', label='double')
plt.legend(loc='best')
plt.ylabel('Q eval')
plt.xlabel('training steps')
plt.grid()
plt.show()

Q值结果比较如下,在训练达到一定次数之后,DQN 学得差不 大部分时间的Q估计值大于0,即overestimate, 而 Double DQN 的 Q值 就消除了一些 overestimate, 将估计值保持在 0 左右。
在这里插入图片描述

3、DQN with Prioritized Replay

Prioritized_Replay_DQN代码地址link.

要点:重视少量但值得学习的样本,在batch 抽样时不是随机抽样, 而是按照 Memory 中的样本优先级来抽. 能更有效地找到我们需要学习的样本.

(1)SumTree 有效抽样

SumTree-Prioritized Experience Replay
论文地址: link.

SumTree 是一种树形结构, 每片树叶存储每个样本的优先级 p, 每个树枝节点只有两个分叉, 节点的值是两个分叉的合, 所以 SumTree 的顶端就是所有 p 的合. 最下面一层树叶存储样本的 p。

例如:现在在13-25区间抽取了24,比29小,走左路到29,比13大,走右路,24-13=11,比12小,走左路到12,即选择12对应的数据。
在这里插入图片描述
算法概述

class SumTree(object):
    # 建立 tree 和 data,
    # 因为 SumTree 有特殊的数据结构,
    # 所以两者都能用一个一维 np.array 来存储
    def __init__(self, capacity):

    # 当有新 sample 时, 添加进 tree 和 data
    def add(self, p, data):

    # 当 sample 被 train, 有了新的 TD-error, 就在 tree 中更新
    def update(self, tree_idx, p):

    # 根据选取的 v 点抽取样本
    def get_leaf(self, v):

    # 获取 sum(priorities)
    @property
    def totoal_p(self):

(2)memory方法

class Memory(object):
    # 建立 SumTree 和各种参数
    def __init__(self, capacity):

    # 存储数据, 更新 SumTree
    def store(self, transition):

    # 抽取 sample
    def sample(self, n):

    # train 完被抽取的 samples 后更新在 tree 中的 sample 的 priority
    def batch_update(self, tree_idx, abs_errors):

(3)算法与DQN的对比

class 的名字改成 DQNPrioritiedReplay
在初始化中加一个 prioritized 参数来表示 DQN 是否具备 prioritized 能力

class DQNPrioritiedReplay:
    def __init__(..., prioritized=True, sess=None)
        self.prioritized = prioritized
        ...
        if self.prioritized:
            self.memory = Memory(capacity=memory_size)
        else:
            self.memory = np.zeros((self.memory_size, n_features*2+2))

        if sess is None:
            self.sess = tf.Session()
            self.sess.run(tf.global_variables_initializer())
        else:
            self.sess = sess

算法结构中多了 ISWeights即 Importance-Sampling Weights, 用来恢复被 Prioritized replay 打乱的抽样概率分布.
在这里插入图片描述

class DQNPrioritizedReplay:
    def _build_net(self)
        ...
        # self.prioritized 时 eval net 的 input 多加了一个 ISWeights
        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
        if self.prioritized:
            self.ISWeights = tf.placeholder(tf.float32, [None, 1], name='IS_weights')

        ...
        # 为了得到 abs 的 TD error 并用于修改这些 sample 的 priority, 我们修改如下
        with tf.variable_scope('loss'):
            if self.prioritized:
                self.abs_errors = tf.reduce_sum(tf.abs(self.q_target - self.q_eval), axis=1)    # for updating Sumtree
                self.loss = tf.reduce_mean(self.ISWeights * tf.squared_difference(self.q_target, self.q_eval))
            else:
                self.loss = tf.reduce_mean(tf.squared_difference(self.q_target, self.q_eval))

learn部分的改变

/def learn(self):
        ...
        # 相对于 DQN 代码, 改变的部分
        if self.prioritized:
            tree_idx, batch_memory, ISWeights = self.memory.sample(self.batch_size)
        else:
            sample_index = np.random.choice(self.memory_size, size=self.batch_size)
            batch_memory = self.memory[sample_index, :]

        ...

        if self.prioritized:
            _, abs_errors, self.cost = self.sess.run([self._train_op, self.abs_errors, self.loss],
                                         feed_dict={self.s: batch_memory[:, :self.n_features],
                                                    self.q_target: q_target,
                                                    self.ISWeights: ISWeights})
            self.memory.batch_update(tree_idx, abs_errors)   # update priority
        else:
            _, self.cost = self.sess.run([self._train_op, self.loss],
                                         feed_dict={self.s: batch_memory[:, :self.n_features],
                                                    self.q_target: q_target})

        ...

4、Dueling DQN

Dueling_DQN代码地址link.

(1)要点概括

Dueling DQN 就是将每个动作的 Q 拆分成了 state 的 Value 加上 每个动作的 Advantage.
在这里插入图片描述

(2)和DQN的对比

第一层把Q值拆分成Value和Advantage
第二层仍然使用原来的DQN
在这里插入图片描述

class DuelingDQN:
    def __init__(..., dueling=True, sess=None)
        ...
        self.dueling = dueling  # 会建立两个 DQN, 其中一个是 Dueling DQN
        ...
        if sess is None:    # 针对建立两个 DQN 的模式修改了 tf.Session() 的建立方式
            self.sess = tf.Session()
            self.sess.run(tf.global_variables_initializer())
        else:
            self.sess = sess
        ...

    def _build_net(self):
        def build_layers(s, c_names, n_l1, w_initializer, b_initializer):
            with tf.variable_scope('l1'):   # 第一层, 两种 DQN 都一样
                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(s, w1) + b1)

            if self.dueling:
                # Dueling DQN
                with tf.variable_scope('Value'):    # 专门分析 state 的 Value
                    w2 = tf.get_variable('w2', [n_l1, 1], initializer=w_initializer, collections=c_names)
                    b2 = tf.get_variable('b2', [1, 1], initializer=b_initializer, collections=c_names)
                    self.V = tf.matmul(l1, w2) + b2

                with tf.variable_scope('Advantage'):    # 专门分析每种动作的 Advantage
                    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.A = tf.matmul(l1, w2) + b2

                with tf.variable_scope('Q'):    # 合并 VA, 为了不让 A 直接学成了 Q, 我们减掉了 A 的均值
                    out = self.V + (self.A - tf.reduce_mean(self.A, axis=1, keep_dims=True))     # Q = V(s) + A(s,a)
            else:
                with tf.variable_scope('Q'):    # 普通的 DQN 第二层
                    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)
                    out = tf.matmul(l1, w2) + b2

            return out
        ...

蓝色线为Dueling DQN,红色线为DQN,actions为可选动作数,设置了5,15,25三种难度,动作越多难度越大,学习训练数越多。纵坐标为累计奖励,横坐标为训练次数,杆子立起来的时候奖励 = 0, 其他时候都是负值, 所以当累积奖励没有在降低时, 说明杆子已经被成功立了很久了。

Dueling DQN比DQN学习更快,拐点出现更早,训练曲线收敛更迅速。
在这里插入图片描述

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值