Categorical DQN-一种建模价值分布的深度强化学习方
之前介绍的DQN及其各种变体,网络输出的都是状态-动作价值Q的期望预估值。而本文将介绍的Categorical DQN,它建模的是状态-动作价值Q的分布。这样的估计方法使得估计结果更加细致可信。
本文的论文名称为《A Distributional Perspective on Reinforcement Learning》,地址为:https://arxiv.org/abs/1707.06887。
不过论文里数学公式非常多,如果想要快速了解这种方法的原理,建议大家阅读《强化学习精要:核心算法与Tensorflow实现》一书。
1、Categorical DQN
1.1 为什么要输出价值分布?
之前介绍的DQN及其各种变体,网络输出的都是状态-动作价值Q的期望预估值。这个期望值其实忽略很多信息。比如同一状态下的两个动作,能够获得的价值期望是相同的,比如都是20,第一个动作在90%的情况下价值是10,在10%的情况下是110,另一个动作在50%的情况下是15,在50%的情况下是25。那么虽然期望一样,但如果我们想要减小风险,我们应该选择后一种动作。而只有期望值的话,我们是无法看到动作背后所蕴含的风险的。
所以从理论上来说,从分布视角(distributional perspective)来建模我们的深度强化学习模型,可以获得更多有用的信息,从而得到更好、更稳定的结果。
1.2 Categorical DQN原理
我们首先需要考虑的一个问题是,选择什么样的分布呢?一种很自然的想法是一个正态分布,我们需要估计的是动作状态价值的期望和方差,但是使用正态分布有很多限制,这就将状态价值限制为了中间概率大,两头概率小的一种分布形式,如果是两头概率大,中间概率小呢?同时,在训练时,我们计算两个分布的差距,选择正态分布从计算的层面也是非常困难的。因此,我们选择的分布至少需要满足两个条件:
- 可以表示各种各样的分布形式,不受太多的限制;
- 便于损失函数的计算和模型参数的更新。
基于以上两点,我们选择用直方图来表示一个分布。同时我们假设价值的最终值落在[Vmin,Vmax]之间。我们要在这段中均匀找N个价值采样点。要找N个价值采样点,两个点之间的间距计算为△z = (Vmax - Vmin)/(N-1),从而采样点的集合为{zi = Vmin + i △z,i=0,1,...,N-1}。
所以,我们的模型要输出一个N个值的向量,每一个值代表一个价值采样点出现的概率。而输入是当前的状态以及选择的动作。
接下来的关键是,如何进行更新?既然是分布,我们自然地想到使用交叉熵损失函数来刻画两个分布的差距。而根据强化学习的思想,我么会有一个价值的估计分布,以及一个价值的实际分布。估计分布的价值采样点是z,这没问题,而实际分布的价值采样点呢?z' = r + gamma * z。举个简单的例子:
可以看到,预估的价值分布和实际的价值分布,由于它们的采样点变的不一样了,我们不能直接比较两个分布的差距,因此我们需要把实际的价值分布的采样点,变换成跟预估的价值分布的采样点一样,即将[0.8,1.7,2,6,3.5,4.4,5.3] 投影为[0,1,2,3,4,5],当然,相应的概率也会发生变化。为了更方便的解释,我们称原有的价值采样点为z,而经过r+gamma*z得到的价值采样点为z'。
为了进行投影,我们首先要对z'的两头进行裁剪,也就是把小于0的变为0,大于5的变为5,此时概率不变,所以经过第一步,价值采样点变为z'=[0.8,1.7,2,6,3.5,4.4,5]。
接下来,我们就要进行采样点的投影了。N个价值采样点共有N-1个间隔,我们首先需要判断z'中每个采样点属于z中的第几个间隔,然后把概率按照距离分配给该间隔两头的价值采样点上。举例来说,z'中第一个价值采样点0.8在z的第一个间隔,其两头的价值采样点分别是0和1。根据距离,其对应概率的20%(0.2 *0.2 = 0.04)应该分配到0这个采样点上,80% (0.2 * 0.8 = 0.16)应该分配到1这个采样点上。这里你可能没有绕过弯来,0.8距离1较近,分配的概率应该越多。对z'所有采样点进行相同的操作,就可以把对应的概率投影到原有采样点z上。过程的示意图如下:
其中,1这个价值采样点的概率计算如下:
z'中有两个采样点的概率要分配到1这个采样点上来,分别是0.8和1.7。原有的价值采样点的间隔是1,所以0.8距离1的上一个价值采样点0的是0.8个间隔,距离1是0.2个间隔,所以0.8对应概率的80%应该分配到1上面,同理,1.7对应概率的30% 要分配到1上,所以投影后1对应的概率是0.2 * 0.8 + 0.3 * 0.3 = 0.25。
在进行裁剪和投影之后,实际的价值分布和预估的价值分布的价值采样点都统一了,我们就可以计算交叉熵损失,并更新模型的参数了。
2、Categorical DQN的Tensorflow实现
本文代码的实现地址为:https://github.com/princewen/tensorflow_practice/tree/master/RL/Basic-DisRL-Demo
这里我们玩的还是atrai游戏,只介绍一下模型实现的最关键的地方。
首先看模型的输入,我们这里不用batch的形式了,一次只输入一个状态动作进行更新,m_input是经过投影后实际的价值分布:
target_state_shape = [1]
target_state_shape.extend(self.state_shape)
self.state_input = tf.placeholder(tf.float32,target_state_shape)
self.action_input = tf.placeholder(tf.int32,[1,1])
self.m_input = tf.placeholder(tf.float32,[self.atoms])
随后是我们的价值采样点:
self.delta_z = (self.v_max - self.v_min) / (self.atoms - 1)
self.z = [self.v_min + i * self.delta_z for i in range(self.atoms)]
接下来是构建我们的dqn网络结构,一个是eval-net,一个是target-net,网络的输入是当前的state以及采取的动作action,只不过action是在中间过程中拼接上去的,而不是最开始就输入进去的:
def build_layers(self, state, action, c_names, units_1, units_2, w_i, b_i, reg=None):
with tf.variable_scope('conv1'):
conv1 = conv(state, [5, 5, 3, 6], [6], [1, 2, 2, 1], w_i, b_i)
with tf.variable_scope('conv2'):
conv2 = conv(conv1, [3, 3, 6, 12], [12], [1, 2, 2, 1], w_i, b_i)
with tf.variable_scope('flatten'):
flatten = tf.contrib.layers.flatten(conv2)
with tf.variable_scope('dense1'):
dense1 = dense(flatten, units_1, [units_1], w_i, b_i)
with tf.variable_scope('dense2'):
dense2 = dense(dense1, units_2, [units_2], w_i, b_i)
with tf.variable_scope('concat'):
concatenated = tf.concat([dense2, tf.cast(action, tf.float32)], 1)
with tf.variable_scope('dense3'):
dense3 = dense(concatenated, self.atoms, [self.atoms], w_i, b_i) # 返回
return tf.nn.softmax(dense3)
def build_cate_dqn_net(self):
with tf.variable_scope('target_net'):
c_names = ['target_net_arams',tf.GraphKeys.GLOBAL_VARIABLES]
w_i = tf.random_uniform_initializer(-0.1,0.1)
b_i = tf.constant_initializer(0.1)
self.z_target = self.build_layers(self.state_input,self.action_input,c_names,24,24,w_i,b_i)
with tf.variable_scope('eval_net'):
c_names = ['eval_net_params',tf.GraphKeys.GLOBAL_VARIABLES]
w_i = tf.random_uniform_initializer(-0.1,0.1)
b_i = tf.constant_initializer(0.1)
self.z_eval = self.build_layers(self.state_input,self.action_input,c_names,24,24,w_i,b_i)
可以看到,我们这里使用的是两层卷积和三层全连接操作,动作只在最后一层全连接时拼接上去。最后的输出经过softmax变为每个价值采样点的概率。
因此我们可以根据分布求出q值:
self.q_eval = tf.reduce_sum(self.z_eval * self.z)
self.q_target = tf.reduce_sum(self.z_target * self.z)
构建好了两个网络,我们怎么进行训练呢?我们的经验池中还是存放了(state,action,reward,next_state),我们首先根据把next_state放入到target-net中,遍历每个可行的动作,找到q值最大的动作,作为next_action:
list_q_ = [self.sess.run(self.q_target,feed_dict={self.state_input:[s_],self.action_input:[[a]]}) for a in range(self.action_dim)]
a_ = tf.argmax(list_q_).eval()
接下来,我们使用target-net计算(next_state,next_action)在原始价值采样点下的概率分布:
p = self.sess.run(self.z_target,feed_dict = {self.state_input:[s_],self.action_input:[[a_]]})[0]
随后就是进行投影操作了,过程我们刚才已经介绍过了,这里不在赘述:
m = np.zeros(self.atoms)
for j in range(self.atoms):
Tz = min(self.v_max,max(self.v_min,r+gamma * self.z[j]))
bj = (Tz - self.v_min) / self.delta_z # 分在第几个块里
l,u = math.floor(bj),math.ceil(bj) # 上下界
pj = p[j]
m[int(l)] += pj * (u - bj)
m[int(u)] += pj * (bj - l)
这样,我们就得到了当前状态动作的实际价值分布。然后我们可以将当前的state和action输入到eval-net中,并通过交叉熵损失来对模型参数进行更新:
self.sess.run(self.optimizer,feed_dict={self.state_input:[s] , self.action_input:[action], self.m_input: m })
其中,优化器的定义如下:
self.cross_entropy_loss = -tf.reduce_sum(self.m_input * tf.log(self.z_eval))
self.optimizer = tf.train.AdamOptimizer(self.config.LEARNING_RATE).minimize(self.cross_entropy_loss)
好了,代码部分就介绍到这里,关于Categorical DQN的更多的知识,大家可以结合论文和代码进行更深入的理解!
参考文献
https://baijiahao.baidu.com/s?id=1573880107529940&wfr=spider&for=pc