这就就不可避免的降低了估值函数的准确度,由于估值方法的计算依据贝尔曼方程,即使用后续状态对估值进行更新,这种性质又加剧了精确度的下降。在每一次更新策略时,使用一个不准确的估计值将会导致错误被累加。这些被累加的错误会导致某一个不好的状态被高估,最终导致策略无法被优化到最优,并使算法无法收敛。
在DQN算法中针对Q值过估计的问题采用的是利用双网络分别实现动作的选择和评估,也就是DDQN算法。在TD3算法中,我们也使用两个Critic网络来评估Q值,然后选取较小的那个网络的Q值来更新,这样就可以缓解Q值高估现象。这样或许会导致些许低估,低估会导致训练缓慢,但是总比高估要好得多。
注意:这里我们使用了两个Critic网络,每个Critic网络都有相应的Target网络,可以理解为这是两套独立的Critic网络,都在对输入的动作进行评估,然后通过min()函数求出较小值作为更新目标。所以TD3算法一共用到6个网络。
代码实现:
self.q_net1=QNetwork(state_dim,action_dim,hidden_dim)self.q_net2=QNetwork(state_dim,action_dim,hidden_dim)self.target_q_net1=QNetwork(state_dim,action_dim,hidden_dim)self.target_q_net2=QNetwork(state_dim,action_dim,hidden_dim)self.policy_net=PolicyNetwork(state_dim,action_dim,hidden_dim,action_range)self.target_policy_net=PolicyNetwork(state_dim,action_dim,hidden_dim,action_range)如上所示,包含两套Q网络,用来估计Q值,一套策略网络。具体的网络更新部分和DDPG是流程是一样的,唯一不同的是两个Critic网络算出Q值后,选取最小值去计算目标值:
target_q_min=tf.minimum(self.target_q_net1(target_q_input), self.target_q_net2(target_q_input))target_q_value=reward+(1-done)*gamma*target_q_min然后就是分别对Critic网络和policy网络进行更新。
TD3中使用的第二个技巧就是对Policy进行延时更新。在双网络中,我们让target网络与当前网络更新不同步,当前网络更新d次之后在对target网络进行更新(复制参数)。这样就可以减少积累误差,从而降低方差。同样的我们也可以policy网络进行延时更新,因为actor-critic方法中参数更新缓慢,进行延时更新一方面可以减少不必要的重复更新,另一方面也可以减少在多次更新中累积的误差。在降低更新频率的同时,还应使用软更新:
关于policy网络延时更新的实现也很简单,只需要一个if语句就可以实现
ifself.update_cnt%self.policy_target_update_interval==0其中update_cnt是更新的次数,policy_target_update_interval是policy网络更新的周期,每当critic更新了一定次数后,再更新policy网络。
误差的根源是值函数估计产生的偏差。知道了原因我们就可以去解决它,在机器学习中消除估计的偏差的常用方法就是对参数更新进行正则化,同样的,我们也可以将这种方法引入强化学习中来:
在强化学习中一个很自然的想法就是:对于相似的action,他们应该有着相似的value。
这里的噪声可以看作是一种正则化方式,这使得值函数更新更加平滑。
defevaluate(self,state,eval_noise_scale):state=state.astype(np.float32)action=self.forward(state)action=self.action_range*action#addnoisenormal=Normal(0,1)noise=normal.sample(action.shape)*eval_noise_scaleeval_noise_clip=2*eval_noise_scalenoise=tf.clip_by_value(noise,-eval_noise_clip,eval_noise_clip)action=action+noisereturnaction如代码所示,给动作加上噪音这部分在策略策略网络评估部分实现,evaluate()函数有两个参数,state是输入的状态,参数eval_noise_scale用于调节噪声的大小。可以看到,首先经过前向计算得到输出的动作action。下面详细说下如何给动作加上噪音:首先我们构造一个正太分布,然后根据动作的形状进行取样normal.sample(action.shape),然后乘以参数eval_noise_scale实现对噪音进行缩放,为了防止抽出的噪音很大或者很小的情况,我们对噪音进行剪切,范围相当于两倍的eval_noise_scale。最后把噪音加到action上并输出。