# Twin Delayed Deep Deterministic Policy Gradient (TD3) Paper : https://arxiv.org/abs/1802.09477 [TOC] ## Introduction 在 [DQN](https://hackmd.io/@bGCXESmGSgeAArScMaBxLA/rkWNLbwMxg) 中有提到過度估計(**Overestimate**)的問題,**DDPG** 也有類似問題,**TD3** 就是針對 [DDPG](https://hackmd.io/@bGCXESmGSgeAArScMaBxLA/SyVyCVxos) 中 **Q Value** 估計不準確或過度估計而做的改良,如下圖 : ![image](https://hackmd.io/_uploads/HyZ9FYVqge.png) 上面是 **Paper** 中展示的圖,展示的就是實際值與估計值的差異,我自己也有嘗試畫,使用的是 `Pendulum` 這個 **Environment** : ![image](https://hackmd.io/_uploads/Skw29KE9el.png) **MC-Error** : $\large Q^{eval}(s_t,a_t)=r+\gamma Q^{target}(s_{t+1},\mu^{target}(s_{t+1}))$ 在 **DQN** 中會過度估計是因為他永遠只選擇 $Q^{target}(s_{t+1},a)$ 最大的 **Value**,並用來計算 **TD-Error** 更新 $Q^{eval}(s_t,a_t)$,這樣就會導致常常在選擇 **Value** 時,意外選擇到因為誤差而過大 **Value**,並往下傳遞,即便經過長時間的更新都難以估計正確。 那 **DDPG** 為什麼也有過度估計的問題呢? **DDPG** 的 **Actor** 會決定一個確定的 **Action**,目標是選一個好的 **Action** 使 **Critic** 越大越好,那在幾次更新之後就會像 **DQN** 整體會去選擇 **Q Value** 較大的 **Action**,而這個 **Value** 可能存在誤差,並逐漸往下傳遞、累積誤差。 ## Clipped Double Q-Learning for Actor-Critic 在 **Double DQN** 中計算 **TD-Error** 的方式 : $\large Q^{eval}(s_t,a_t)=r_{t+1}+\gamma Q^{target}(s_{t+1}, arg\max_{a}Q^{eval}(s_{t+1},a))$ 在 DDPG 這種 Actor-Critic 的架構也可以實現 : $\large Q^{eval}(s_t,a_t)=r+\gamma Q^{target}(s_{t+1},\mu^{eval}(s_{t+1}))$ 只要把 **Actor** 從 **Target Network** 改成 **Evaluate Network** 就可以了 >上面的寫法代表的是希望左式要盡量接近右式,不代表實際是這樣更新,實際更新參數是使用 **Adam** --- **DDPG** 實際上 **Evaluate** 和 **Target Network** 差異不大,所以帶來的改善是有限的,論文中說可以採用原始 **Double Q-Learning** 建議的方式,建立一組 Evaluate Actor($\pi_{\phi_1}$ , $\pi_{\phi_2}$) 和一組 Evaluate Critic($Q_{\theta_1}$ , $Q_{\theta_2}$) Update Critic : * $Q^{eval}_{\theta_1}(s_t,a_t)=r+\gamma Q^{target}_{\theta_2}(s_{t+1},\pi^{eval}_{\phi_1}(s_{t+1}))$ * $Q^{eval}_{\theta_2}(s_t,a_t)=r+\gamma Q^{target}_{\theta_1}(s_{t+1},\pi^{eval}_{\phi_2}(s_{t+1}))$ **Update Actor** 方法與 **DDPG** 一樣,只是使用的 **Critic** 不一樣 : * $\pi_{\phi_1}$ 使用 $Q_{\theta_1}$ 計算 **Loss** * $\pi_{\phi_2}$ 使用 $Q_{\theta_2}$ 計算 **Loss** --- 以上的方法可以減少過度估計,但無法完全消除,為了進一步減少誤差,論文中提出可以在兩個 **Critic** 中選擇一個較小的來當作 **Target Value**,也就是這個章節的標題 **Clipped Double Q-Learning** 的計算方式 : $y_1=r+\gamma\min_{i=1.2}Q_{\theta_i'}(s_{t+1},\pi_{\phi_1}(s_{t+1}))$ 上面是以更新 $Q^{eval}_{\theta1}$ 來舉例的,為了降低運算成本,可以只使用一個 **Actor**,產生的 **Target Value** 用來優化 $Q_{\theta_1}$ 和 $Q_{\theta_2}$。 ## Target Network and Delayed Policy Update 這邊更新 **Target Network** 的方式跟 **DDPG** 差不多 : $\theta^{Q'} =\tau\theta^Q+(1-\tau)\theta^{Q'}$ 論文中有根據不同的 $\tau$ 畫出比較圖 : ![image](https://hackmd.io/_uploads/H1RCPnLcxl.png) 論文中提出在 **Fixed Policy** 時,**Critic** 需要一定的時間才能到達 **True Value**,所以認為 **Actor** 不應該過度劇烈的變動,**Actor** 的更新頻率應該應該比 **Critic** 還低,具體的方法是在 **Critic** 更新固定次數 $d$ 之後才會更新 **Actor**。 ## Target Policy Smoothing Regularization **DDPG** 這種確定性的 **Policy**,可能會 **Overfitting** 變異較大的數值(偏差值),在更新 **Critic** 時,使用確定性 **Policy** 來 **Update Critic** 則容易讓 **Critic** 學到不準確的影響,增加 **Variance**,論文提出可以透過 **Regularization** 來減少,叫做 **Target Policy Smoothing**,靈感來自 **SARSA**,作者認為相似的 **Action** 應該具有相似的 **Value**,實作是在計算 **Target Value** 時使用的 **Target Actor** 計算出的 **Next Action** 加上 **Noise** $y=r+\gamma Q_{\theta'}(s_{t+1},\pi{\phi'}(s_{t+1})+\epsilon \space\space\space\space,\space\space\space\space \epsilon\sim\mathrm{clip}(\mathcal{N}(0,\sigma),-c,c)$ $\epsilon$ 代表被加入的 **Noise**,這個 **Noise** 會被 **Clip**,以確保 **Target Action** 仍接近原本的 **Action**,$\sigma$ 則是定義 **Normal Distribution** 的變異數,代表 **Noise** 的範圍 ## Algorithm **Rollout** : * **Initialize Critic Network** : $\large Q_{\theta_1},Q_{\theta_2}$ * **Initialize Actor Network** : $\large \pi_\phi$ * **Initialize target network** : $\large\theta'_1=\theta_1,\theta'_2=\theta_2,\phi'=\phi$ * **Select action** : $\large a\sim\pi_\phi(s_t)+\epsilon, \epsilon\sim \mathcal{N}(0,\sigma)$ --- **Update** : * **Next action** : $\large \hat{a}=\pi_{\phi'}(s_{t+1})+\epsilon, \epsilon\sim\mathrm{clip}(\mathcal{N}(0,\hat{\sigma},-c,c))$ * **Target Value** : $\large y=r+\gamma\mathrm{min}(Q_{\theta'_1}(s_{t+1},\hat{a}),Q_{\theta'_2}(s_{t+1},\hat{a}))$ * **Update Critic** : $Loss_{j=1,2}=\frac{1}{N}\sum(y-Q_{\theta_{j}}(s_t,a_t))^2$ * $t$ : **Soft Update** 次數 * 當 $mod(t,d)==0$ 時 **Update** $\phi$ * **Update Actor** : $Loss=-\frac{1}{N}\sum Q_{\theta_1}(s_t,\pi_\phi(s_t))$ * **Update Target Network By** $\tau$ ## Evaluation * **Actor Critic** 的 **Neural Network** 都是 `[400,300]`,隱藏層使用 **ReLU**, * **Actor** 的 **Output Layer** 用 **Tanh** 作為 **Activation Function** * **Critic** 將 **State** 和 **Action** 做為 **Input**, * **Optimizer** 使用 **Adam**,**Learning rate** $10^-3$ * 每一個 **Step** 結束後會抽取 **100** 筆 **mini-batch** 進行訓練(從 **Replay Buffer**) * **Target Policy Smoothing** 的實作是在 **Target Actor** 選出的 **Action** 加入 **Noise** $\epsilon\sim\mathcal{N}(0,0.2)$,並 **Clip** 在 $(-0.5,0.5)$ 的範圍內 * **Delayed Policy Updates** 做法是每隔 $d=2$ 次才更新 **Actor** 與 **Target Critic** **Network** * 兩個 **Target Network** 皆使用 $\tau=0.005$ 進行更新 * **Explore** 策略是前 **1000** **Time Step** 在 **Choose Action** 加上 $\mathcal{N}(0,0.1)$ 的 **Gaussion Noise** ## Result **HalfCheetah** : ![image](https://hackmd.io/_uploads/B1ZlEK99eg.png) ![1757231926018](https://hackmd.io/_uploads/SJuX3h9qxl.gif) **Walk2d** : ![image](https://hackmd.io/_uploads/S10gFtFcex.png) ![1757232143754](https://hackmd.io/_uploads/Hkdm33q5le.gif) ## Code **Github Code** : https://github.com/jason19990305/TD3.git 主要跟 **DDPG** 不一樣的地方,更新參數的 **Code** : ```python! def update(self): minibatch_s, minibatch_a, minibatch_r, minibatch_s_, minibatch_done = self.replay_buffer.sample_minibatch() # Clipped Double Q-Learning for Actor-Critic with torch.no_grad(): # Target Policy Smoothing Regularization noise = ( torch.randn_like(minibatch_a) * self.sigma ).clamp(-self.c, self.c) #noise = torch.normal(mean=0, std=self.sigma, size = minibatch_a.shape).to(self.device) next_action = self.actor_target(minibatch_s_) + noise#torch.clamp( noise , -self.c , self.c) next_action = torch.clamp(next_action , -self.action_max , self.action_max) next_value1 = self.critic1_target(minibatch_s_,next_action) next_value2 = self.critic2_target(minibatch_s_,next_action) target_value = minibatch_r + self.gamma * torch.min(next_value1 , next_value2) * (1 - minibatch_done) # Update Critic 1 value1 = self.critic1(minibatch_s,minibatch_a) critic1_loss = F.mse_loss(value1,target_value) self.optimizer_critic1.zero_grad() critic1_loss.backward() self.optimizer_critic1.step() # Update Critic 2 value2 = self.critic2(minibatch_s,minibatch_a) critic2_loss = F.mse_loss(value2,target_value) self.optimizer_critic2.zero_grad() critic2_loss.backward() self.optimizer_critic2.step() # Target Network and Delayed Policy Update if self.total_steps % self.d == 0 : # Update Actor action = self.actor(minibatch_s) actor_loss = - self.critic1(minibatch_s,action).mean() self.optimizer_actor.zero_grad() actor_loss.backward() self.optimizer_actor.step() self.soft_update(self.critic1_target,self.critic1, self.tau) self.soft_update(self.critic2_target,self.critic2, self.tau) self.soft_update(self.actor_target, self.actor, self.tau) ``` 用前面描述的演算法依序處理,更新 **Actor** 和 **softupdate** 每 $d$ 個 **Timestep** 會執行一次 ## Conclusion 雖然從耗費的 **Step** 角度來看效果不錯,但是實際訓練的時間是真的比 **PPO** 長,畢竟多一組 **Critic** 要計算