本文记录学习DDPG算法细节中遇到的若干问题。
DDPG的主要特征
DDPG的优点以及特点, 在若干blog, 如Patric Emami以及原始论文中已经详述, 在此不再赘述细节。其主要的tricks在于:
- Memory replay, 与 DQN中想法完全一致;
- Actor-critic 框架, 其中critic负责value iteration, 而actor负责policy iteration;
- Soft update, agent同时维持四个networks, 其中actor与critic各两个, 分别有一个为target network, 其更新方式为soft update, 即每一步仅采用相对小的权重采用相应训练中的network更新;如此的目的在于尽可能保障训练能够收敛;
- Exploration via random process, typically OU process, 为actor采取的action基础上增加一定的随机扰动, 以保障一定的探索完整动作空间的几率。一般的, 相应随机扰动的幅度随着训练的深入而逐步递减(方法5中有实现该特性);
- Batch normalization, 为每层神经网络之前加入batch normalization层, 可以降低不对状态量取值范围差异对模型稳定性的影响程度。
我的困惑
此前套用Ben Lau博客中的代码, 实现了基于DDPG的FL training market中动态博弈问题求解的程序, 但是结果非常不理想。粗略来看, 各个player的决策结果完全由OU过程决定(后来发现, 应该是OU过程中没有对噪声项乘以$\Delta_t$的原因)。
随后打算用Matlab重写一遍代码, 以便调试。前期工作进展比较顺利, 以Matlab的Deep learning toolbox为基础完成了基本环境的搭建, 其中由于Deep learning toolbox目前还处于完善阶段(R2019a), 有若干类型的Layer仍然没有被官方收录, 例如支持多输入流的输入层, sigmoid activation等。为此, 基于Matlab文档提供的思路实现了相应的需求。
但天不遂人愿阿, 当需要实现DDPG中的核心步骤, 即network的更新时, 发现需要使用对神经网络求梯度(autograd)的步骤, 而截至目前, 该功能还未由Deep learning toolbox提供。论坛查询发现, 开发者正在开发中, 计划于下一个版本中引入。但是下一个版本的推出时间将是今年的九月份, 等不起阿。
目前MATLAB 2019b已发布, 其中Deep learning toolbox已更新有关Automatic Differentiation相关的功能。
那么我有两种思路, 第一: 自己尝试实现对深度神经网络求梯度;第二: 放弃Matlab方案。简单查询了一些资料发现基于Matlab有若干已开发的autograd的程序, 但是年代均有些久远, 不确定能否拿来直接用(持怀疑态度), 在扎进去研究之前, 我试着先明确一下DDPG中究竟是如何使用autograd?无论如何, 理解这一细节对于掌握DDPG或者自己用Matlab实现DDPG均是绕不开的一环了。综合来看, 目前放弃Matlab的实现方案转而回头继续写Python看来是唯一的途径了。等将来Matlab完善了Deep learning toolbox后再考虑拾起遗留的进度。
那么, 接下来, 首要的任务就是彻底搞清楚DDPG中actor与critic更新网络的环节。
DDPG网络更新关键
其中critic网络作用在于估计值函数(Value function, 即Q函数), 其输入、输出分别为: states与action、Q值。而actor网络的作用在于根据states决定action, 其输入、输出分别为states、action。
符号说明
| 符号 | 含义 |
|---|---|
| $\theta^Q$ | critic network 参数 |
| $\theta^{Q’}$ | target critic network 参数 |
| $\theta^{\mu}$ | actor network 参数 |
| $\theta^{\mu’}$ | target actor network 参数 |
| $(s_i, a_i, r_i, s_{i+1})$ | Memory pool 中的sample, 四个维度依次表示, 当前时刻的状态, 当前时刻采取的动作, 相应获得的即时reward以及采取动作后的状态 |
Critic network更新
其目的在于获得尽可能准确的Q函数估计, 因此critic network的loss定义如下:
\[L(\theta^Q) = \frac{1}{N} \sum_{i} (y_i - Q(s_i, a_i|\theta^Q))^2,\]其中$y_i$表示实际Q值(由target network给出), 表达式如下:
\[y_i = r_i + \gamma \underbrace{Q'(s_{i+1}, \underbrace{\mu'(s_{i+1}|\theta^{\mu'})} _ {\text{action by target actor}}|\theta^{Q'}} _ {\text{Next state value by target network}}),\]而$Q(s_i, a_i|\theta^Q)$则是当前critic network给出的估计值。因此, critic network的loss函数就定义为估计值与实际值之间的MSE, critic network目的在于最小化该loss。
Actor network更新
另一方面, actor network目的在于选择出最佳的action, 因此该网络的更新方向是最大化Q值的方向。其梯度表示如下:
\[\nabla_{\theta^{\mu}} J \approx \frac{1}{N} \sum_{i} \underbrace{\nabla_a Q(s, a|\theta^Q)|_ {s=s_i, a=\mu(s_i)}}_ {\partial Q / \partial \mu} \underbrace{\nabla_{\theta^{\mu}} \mu(s|\theta^{\mu})|_ {s_i}}_ {\partial \mu / \partial \theta^{\mu}},\]其依据是求导的链式法则, Q值对actor network的梯度表示为$\frac{\partial Q}{\partial \theta^{\mu}}$, 通过链式法则表示如上。
具体如何用程序语言表示actor network的更新方式正是我的疑惑之所在。1 下一节将罗列目前已经查询到的四种DDPG实现方式中更新actor以及critic网络的步骤, 从而对比理解DDPG中的关键点。
DDPG实现方式对比
目前已经查阅的DDPG实现文章/代码有如下四种:
- Blogpost by Chris Yoon, Deep Deterministic Policy Gradients Explained
- Blogpost by Patric Emami, Deep deterministic policy gradients in tensorflow
- Blogpost by Ben Lau, Using Keras and Deep Deterministic Policy Gradient to play TORCS
- rl-keras, Deep Reinforcement Learning for Keras
其中1采用 PyTorch, 2采用 Tensorflow 编写, 3,4采用 Keras(Tensorflow backend) 编写。
Deep deterministic policy gradients in tensorflow
在这篇博客中,通过朝 $Q$ 的梯度方向更新最大化 $Q$ 值, 该梯度表示为 $-\nabla_{\theta^{\mu}} Q$, 根据链式法则等效为 $-\nabla_a Q \cdot \nabla_{\theta_{\mu}}\mu(s|\theta_{\mu})$, 该梯度是$Q$值相对于actor network参数的梯度, 而actor network的目的在于最大化该值, 因此在网络更新时, 其loss函数等效为$-Q$。具体而言, 代码中更新体现如下:
# This gradient will be provided by the critic network
self.action_gradient = tf.placeholder(tf.float32, [None, self.a_dim])
# Combine the gradients, dividing by the batch size to
# account for the fact that the gradients are summed over the
# batch by tf.gradients
self.unnormalized_actor_gradients = tf.gradients(
self.scaled_out, self.network_params, -self.action_gradient)
self.actor_gradients = list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients))
# Optimization Op
self.optimize = tf.train.AdamOptimizer(self.learning_rate).\
apply_gradients(zip(self.actor_gradients, self.network_params))
值得注意的是其中对tf.gradients函数的使用, 是actor network更新操作的精髓所在。tf.gradients接收参数的前三个位置分别是ys, xs和ys_grad, 其含义分别是$\frac{\partial y}{\partial x}$中的分子和分母以及对y的前序求导, 也可以理解为相应的权重;其中第三个参数, 是实现以上链式法则的关键。具体而言, 第三个参数填入了-self.action_gradient, 而self.action_gradient是一个placeholder, 是为$\nabla_a Q$的预留位,因此该行代码整体实现了$-\nabla_a Q \cdot \nabla_{\theta_{\mu}}\mu(s|\theta_{\mu})$。
此外,该步仅实现了梯度的符号化计算,并未实际应用梯度更新,梯度的更新操作是通过接下来的代码实现,即apply_gradients。
Using Keras and Deep Deterministic Policy Gradient to play TORCS
与Deep deterministic policy gradients in tensorflow中actor network的更新方式完全一致。
Deep Deterministic Policy Gradients Explained
此博客采用PyTorch实现, 其中actor network的更新部分代码如下:
# Actor loss
policy_loss = -self.critic.forward(states, self.actor.forward(states)).mean()
# update networks
self.actor_optimizer.zero_grad()
policy_loss.backward()
self.actor_optimizer.step()
其中的关键在于actor network的loss函数定义, 即policy_loss, 其等效于$-Q(s, \mu(s|\theta^{\mu}))$, 而policy_loss.backward()等效于Tensorflow中的tf.gradients操作, 执行符号化的梯度运算, 紧接着的step()执行该步梯度运算, 更新网络参数$\theta^{\mu}$。值得注意的是,此处仅对actor network网络参数的更新是如何体现的?在于actor_optimizer.step()该步, 其中actor_optimizer是如下给出的:
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=actor_learning_rate)
即更新的对象为actor network的参数actor.parameters(), 因此以上的梯度更新只发生于actor network。在这一点上, PyTorch相比于Tensorflow更为直观。
Deep Reinforcement Learning for Keras
该repo中, 是通过Keras实现, 由于其目的是通用的DDPG实现, 因此鲁棒性方面的考虑比较全面, 代码整体稍显“臃肿”, 不过其设计流程有一定的参考性。其中actor network的更新部分的核心代码如下:
combined_output = self.critic(combined_inputs) # 其中combined_inputs与上述`critic.foward`中的输入参数类似
updates = actor_optimizer.get_updates(
params=self.actor.trainable_weights, loss=-K.mean(combined_output))
self.actor_train_fn = K.function(state_inputs + [K.learning_phase()],
[self.actor(state_inputs)], updates=updates)
action_values = self.actor_train_fn(inputs)[0]
以上代码相对分散的放置以及层层封装导致可读性较差, 但是整体思路与PyTorch中并无二致。同样的updates实现了$-Q(s, \mu(s|\theta^{\mu}))$对actor network参数的梯度符号化运算, 通过K.function执行相应的梯度运算。
值得注意的是该repo中对OU过程的实现中$\sigma$逐步递减的设计可以参考,其目的在于逐步降低Exploration的概率。
小结
以上的实现方法中, 涉及Tensorflow, Keras, PyTorch三种主流的机器学习框架, 对actor network更新部分的核心思路均一致: actor network的loss函数为$-Q$, 通过自动梯度运算给出loss函数对actor network的参数$\theta^{\mu}$的梯度, 并通过update/step/function执行相应的梯度运算, 实现网络参数的更新。相比较而言, PyTorch的代码最为简洁直观, 对Tensorflow有了初步了解后也能直观地理解其操作的逻辑, 相比之下封装最为彻底的Keras理解起来就有些费劲了。
其他可能影响DDPG效果的因素2, 3
Noise 添加方式
在Better Exploration with Parameter Noise中提出了一种新的noise添加方式, 有待进一步研究。
Nomalization
DDPG原始论文中提到了需要为网络结构中增加normalization layer, 其原因是消除不同参数范围对结果的影响。而normalization layer的添加方式(放在哪?)存在经验模式。