---
# System prepended metadata

title: Twin Delayed Deep Deterministic Policy Gradient (TD3)
tags: [Reinforcement Learning, Deep Learning]

---

# 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** 要計算

