---
# System prepended metadata

title: Soft Actor-Critic
tags: [Deep Reinforcement Learning, Reinforcement Learning, Deep Learning]

---

# Soft Actor-Critic


[TOC]


## Related Work

可先參考 : 
* [**TD3**](https://hackmd.io/@bGCXESmGSgeAArScMaBxLA/rkdfHyzqex)
* [**DDPG**](https://hackmd.io/@bGCXESmGSgeAArScMaBxLA/SyVyCVxos)


### Entropy

**Entropy** 是拿來衡量一個 **Distribution** 不確定性， **Entropy** 越大代表 **Policy** 越隨機，計算方式為 : 
$$
\mathcal{H}(P)=-\sum_{x\sim P}P(x)\log(P(x))
$$


如果我們有兩個機率(有限)，所有可能的組合可以畫出 **Entropy** 分布圖 : 
![image](https://hackmd.io/_uploads/rk1FcwNiel.png =70%x)

前面的算法是在 **Probability Distribution** 離散且有限的情況下，我們可以窮舉所有可能的機率，但是如果 $x\sim P$ 有無限種可能，但我們又知道 **Distribution Sample** 出當前 **x** 的機率，那我們可以寫成取樣平均來近似 : 

$$
\mathcal{H}(P)=-\sum_{x\sim P}P(x)\log(P(x))=\mathbb{E}_{x\sim P}[-\log P(x)]
$$

我們可以用 **Normal(Gaussian) Distribution** 來舉例，給定 $\sigma$ 和固定的 $\mu=0$ 來取樣近似 : 

![image](https://hackmd.io/_uploads/S1OWuAg3xg.png)





## Maximum Entropy Reinforcement Learning



傳統的強化學習算法都會希望最大化 **Expected Reward** : 
$$
\large\sum_{t=0}^\infty\mathbb{E}_{(s_t,a_t)\sim \rho_\pi}[r(s_t,a_t)]
$$

**SAC** 的最大目標就是在追求最大 **Expected Reward** 之外，他也要最大化 **Policy** 的 **Entropy**，**Entropy** 是用來描述一個機率分布的不確定性，也就是希望 **Policy** 能盡可能嘗試多一些不同的可能，主要是想彌補像 **DDPG** 這種 **Deterministic Policy**，對於探索來說能力較差的缺點。



所以 **SAC** 定義了要最大化的 **Objective Function** : 

$$
\large \pi^*=\arg\max_\pi\sum_{t=0}^\infty\mathbb{E}_{(s_t,a_t)\sim\rho_\pi}[r(s_t,a_t)+\alpha\mathcal{H}(\pi(\cdot|s_t))]
\tag1
$$

跟傳統強化學習不一樣的地方在於多加了一個 **Entropy** 項，這個 **Objective Function** 希望能調整 **Policy** $\pi$，去最大化 **Reward** 和 **Entropy**，當然還有一個 **Tempture** $\alpha$ 來調節 **Entropy** 的比例，因為我們的最終目標還是要最大化 **Reward** ，如果一直去探索不管最大化 **Reward**，那訓練就會不穩定


## Q-Function

在 **Actor-Critic** 架構中的 **Critic** 會用來近似 **Expected Reward**，並且帶有 **Discount Factor**，在應用於連續環境的 **DDPG** 中，**Q-Function** 的 **Input** 包含 **State** 和 **Action** ，用來預測、近似 **Expected Reward** : 

$$
\large Q_\theta(s_t,a_t)\approx \mathbb{E}_{(s_t,a_t)\sim\pi_\phi}[\sum_{i=t}^\infty \gamma^{i-t}r(s_i,a_i)]
$$

我們知道 $t$ 以後的 **Reward** 可以用 $Q_\theta(s_{t+1},a_{t+1})$ 代替，也就是 **TD-Error** : 

$$
\large Q_\theta(s_t,a_t)\approx \mathbb{E}_{(s_t,a_t)\sim\pi_\phi}[r(s_t,a_t)+\gamma Q_\theta(s_{t+1},a_{t+1})]
$$

如果是 **Maximum Entropy** : 

$$
\large Q_\theta(s_t,a_t)\approx \mathbb{E}_{(s_t,a_t)\sim\pi_\phi}[\sum_{i=t}^\infty \gamma^{t-i}\Big(r(s_i,a_i)+\alpha\mathcal{H}(\pi(\cdot|s_i)) \Big)]
$$

將 $r(s_i,a_i)+\alpha\mathcal{H}(\pi(\cdot|s_i))$ 代換成 $G_i$ : 

$$
\large Q_\theta(s_t,a_t)\approx\mathbb{E}_{(s_t,a_t)\sim\pi_\phi}\left[\sum_{i=t}^\infty\gamma^{i-t}G_i\right]
$$


把在 $s_t,a_t$ 下的 **Q-Function** 展開 : 
$$
Q_\theta(s_t,a_t)\approx G_t + \gamma G_{t+1} + \gamma^2G_{t+2}+...
$$

$$
Q_\theta(s_{t+1},a_{t+1})\approx G_{t+1} + \gamma G_{t+2}+...
$$

所以可以近似代換成 : 

$$
Q_\theta(s_t,a_t)\approx G_t + \gamma Q_\theta(s_{t+1},a_{t+1})
$$

還原 $G_t$

$$
Q_\theta(s_t,a_t)\approx r(s_t,a_t)+\alpha\mathcal{H}\Big(\pi(\cdot|s_t)\Big) + \gamma Q_\theta(s_{t+1},a_{t+1})
$$


**Entropy** 的定義 : 
$$
\mathcal{H}(\pi(\cdot|s_t))=\mathbb{E}_{(s_t,a_t)\sim\pi_\phi}[-\log\pi_\phi(a_t|s_t)]
$$

最後寫成 **Q-Function** 要近似的 **Objective Function** : 

$$
\large Q_\theta(s_t,a_t)\approx r(s_t,a_t)-\alpha\log\pi_\phi(a_t|s_t)+\gamma Q_\theta(s_{t+1},a_{t+1})
$$

如果不包含當前的 Entropy : 

$$
\large Q_\theta(s_t,a_t)\approx r(s_t,a_t)-\alpha\log\pi_\phi(a_t|s_t)+\gamma Q_\theta(s_{t+1},a_{t+1})
$$


## Soft Actor-Critic


在 **DDPG** 和 **TD3** 的 **Actor** 輸出的是 **Deterministic** ，為了讓他變成 **Stochastic** ，可以使用 **Gaussian Distribution** 來 **Sample Action**，舉例來說就是讓 **Actor** 的 **Neural Network** 輸出 $\mu$ 和 $\sigma$，然後帶入並 **Sample** 出 **action** $a\sim\mathcal{N}(\mu,\sigma)$ 

如果按照 **TD3** 的方法讓 **Policy** 最大化 **Q-Function** : 
$$
\max_\phi \frac{1}{N}\sum Q_\theta(\tilde{a}_t,s_t)
$$

這樣的寫法會在實作時，$\tilde{a}_t$ 是 **Policy** $\pi_\phi$ **Sample** 出來的，這邊的 **Q-Function** 是近似當前的 **Expected Reward** 和 **Entropy** 的，但我們可以發現這邊不包含任何關於 **Policy** 的 **Entropy** 項，導致不穩定。


為了讓 **Policy** 穩定訓練，要改成 : 
$$
\max_\phi \frac{1}{N}\sum Q_\theta(\tilde{a}_t,s_t)-\alpha\log(\tilde{a}_t,s_t)
$$

---

**Policy** 改了之後 **Q-Function** 就要跟著改，也就是 **Q-Function** 要近似的目標，不能包含當前的 **Entropy** $\mathcal{H}(\pi(\cdot|s_t))$




**Maximum Entropy** : 

$$
\large Q_\theta(s_t,a_t)\approx \mathbb{E}_{(s_t,a_t)\sim\pi_\phi}[\sum_{i=t}^\infty \gamma^{i-t}\Big(r(s_i,a_i)+\alpha\mathcal{H}(\pi(\cdot|s_i)) \Big)]
$$



當 $s_t,a_t$ 作為 **Input** 時，**Q-Function** 不能包含當前 **Timestep** 的 **Entropy** $\mathcal{H}(\pi(\cdot|s_t))$，然後我們展開 : 

$$
Q_\theta(s_t,a_t)\approx r(s_t,a_t) + \gamma[\alpha\mathcal{H}(\pi(\cdot|s_{t+1}))]+\gamma r(s_{t+1},a_{t+1})
$$

$$+ \gamma^2\Big[r(s_{t+2},a_{t+2})+\alpha\mathcal{H}(\pi(\cdot|s_{t+2}))\Big]
$$

$$+ \gamma^3\Big[r(s_{t+3},a_{t+3})+\alpha\mathcal{H}(\pi(\cdot|s_{t+3}))\Big]+...
$$


並且

$$
Q_\theta(s_{t+1},a_{t+1})\approx r(s_{t+1},a_{t+1}) + \gamma \Big[r(s_{t+2},a_{t+2})+\alpha\mathcal{H}(\pi(\cdot|s_{t+2}))\Big] 
$$

$$+ \gamma^2\Big[r(s_{t+3},a_{t+3})+\alpha\mathcal{H}(\pi(\cdot|s_{t+3}))\Big]+...
$$

我們發現 $Q_\theta(s_t,a_t)$ 的後半段可以替換成 $Q_\theta(s_{t+1},a_{t+1})$ : 

$$
Q_\theta(s_t,a_t)\approx r(s_t,a_t) + \gamma[\alpha\mathcal{H}(\pi(\cdot|s_{t+1}))] + \gamma Q_\theta(s_{t+1},a_{t+1})
$$

整理 : 

$$
Q_\theta(s_t,a_t)\approx r(s_t,a_t) + \gamma\Big[Q_\theta(s_{t+1},a_{t+1})-\alpha\log\pi_\phi(a_t|s_t)\Big]
$$


## Detail

### Double Q Learning

在比較新的 **SAC** 方法中，參考了 **TD3** 更新 **Q-Function** 的概念，會有兩個 **Critic**，分別為 $Q_{\theta_1}$ 和 $Q_{\theta_2}$，並且有對應的 **Target Network** $Q'_{\theta_1}$ 和 $Q'_{\theta_2}$，使用較緩慢的 **Target Network** 來更新 **Critic**，能讓 **Critic** 預測的數值波動降低，並且會在兩個 **Target Network** 選較小的作為 **Target Value**，能有項避免過度估計(**Over Estimate**)，並且也可以帶入 **Delay**，每隔固定次數更新一次 **Target Network**


### Update Alpha

在一些關於 **SAC** 的論文中，會使用拉格朗日函數(**Lagrangian**) 來自動優化 **Alpha**，但後來的方法有提出另一種方法讓 **Alpha** 收斂到我們指定的 **Entropy**，我們會設定一個固定的 **Target Entropy**，並且透過優化 **Alpha** 讓 **Policy** 的 **Entropy** 接近 **Target Entropy**

$$
\arg\min_\alpha\frac{1}{N}\sum-\alpha\log\pi_\phi(a_t|s_t)-\alpha\mathcal{H}_0
$$

其中 $\mathcal{H}_0$ 就是我們要設定的 **Target Entropy**，其中只有 $\alpha$ 是可微的，這個 **Target Entropy** 通常會設為負的 **Action Space Dimension**，像 `HalfCheetah-v5` 這個環境的 **Dimension** 就是 **6**，那我們的 **Target Entropy** 就會設為 **-6**

## Algorithm


**Policy Sample Action** : 
* $\tilde{a}_t\sim\pi_\phi(\cdot|s_t)$

**Sample Action by Squashed Gaussian Distribution** : 
* $\tilde{a}_t=\tanh(\xi) , \space \space \xi\sim\mathcal{N}(\mu_t,\sigma_t) ,\space(\mu_t,\sigma_t)\sim\pi_\phi(\cdot|s_t)$


---
**Initialize** : 
* **Initial Actor Network** : $\pi_{\phi}$
* **Initial Critic Network** : $Q_{\theta_1},Q_{\theta_2}$
* **Initial Target Network** : $\theta_1'=\theta_1,\theta_2'=\theta_2$

**Rollout** : 
* **Select Action** : $a_t\sim\pi(\cdot|s_t)$
* **Storage to Replaybuffer** : $\mathcal{D}\cup\{(s_t,a_t,r_t,s_{t+1})\}$

**Update** : 
* **Sample Batch From ReplayBuffer** $\mathcal{D}$
* **Sample next action from policy** : $\tilde{a}_{t+1}\sim\pi_\phi(\cdot|s_{t+1})$
* **Target Value** : 
$$
\large y=r+\gamma\min_{i=1,2}\left(Q_{\theta_i'}(s_{t+1},\tilde{a}_{t+1})-\alpha\log(\pi_\phi(\tilde{a}_{t+1}|s_{t+1}))\right)
$$
* **Update Critic $Q_{\theta_1},Q_{\theta_2}$** : $Loss_i=\frac{1}{N}\sum (y-Q_{\theta_i}(s_t,a_t))^2$
* **Sample action from policy** : $\tilde{a}_{t}\sim\pi_\phi(\cdot|s_t)$
* **Update Actor** : 
$$
Loss=-\frac{1}{N}\sum\min_{i=1,2}Q_{\theta_i}(s_t,\tilde{a}_t)-\alpha\log\pi_\phi(\tilde{a}_t|s_t)
$$
* **Update** $\alpha$ : 
$$
Loss = \frac{1}{N}\sum-\alpha\log\pi_\phi(a_t|s_t)-\alpha\mathcal{H}_0
$$

## Evaluation

在訓練時通常會給定一個與訓練步數相當的 **Replay Buffer**

## Result


**Pendulum-v1**
```
d = 1
lr = 0.0003
tau = 0.005
gamma = 0.99
init_alpha = 0.2
mem_min = 1000
mini_batch_size = 512
buffer_size = 10000
max_train_steps = 60000
evaluate_freq_steps = 2000.0
num_actions = 1
num_states = 3
action_max = 2.0
---------------
[4, 256, 256, 1]
[4, 256, 256, 1]
```

![image](https://hackmd.io/_uploads/ByZPk_Jpxe.png)
![image](https://hackmd.io/_uploads/ByhD1uy6el.png)
![image](https://hackmd.io/_uploads/ry7ukukalx.png)
![1759637381254](https://hackmd.io/_uploads/BJ6qyOyall.gif)


---

**HalfCheetah-v5**

```
d = 1
lr = 0.0003
tau = 0.005
gamma = 0.99
init_alpha = 0.2
mem_min = 10000
mini_batch_size = 256
buffer_size = 1000000
max_train_steps = 1000000
evaluate_freq_steps = 5000
num_actions = 6
num_states = 17
action_max = 1.0
---------------
[23, 256, 256, 1]
[23, 256, 256, 1]
```

![image](https://hackmd.io/_uploads/B1H9kvJaex.png)

![image](https://hackmd.io/_uploads/ry9iJPk6gg.png)

![image](https://hackmd.io/_uploads/ryd2kDkpel.png)

![1759633430404](https://hackmd.io/_uploads/BkzNlvk6xl.gif)


## Code

**Github Code** : https://github.com/jason19990305/SAC.git

以下列出與 **DDPG** 和 **TD3** 差異較大的部分

**Actor** : 

```python=
def forward(self, x):
    # Pass input through all layers except the last, applying ReLU activation
    for i in range(len(self.layers)):
        x = self.relu(self.layers[i](x))
    mean = self.mean_linear(x)
    log_std = self.std_linear(x)
    log_std = torch.clamp(log_std, min=-20, max=2)
    return mean , log_std
```

**Actor** 為了方便取得 $\mu,\sigma$ ， **Forward** 有額外兩層 **Linear** 是用來算 $\mu,\sigma$，並且沒有 **Activation Function**，因為是 **Regression** ，數值範圍也沒有限制，且 **Actor** 是 **Predict** $\log(\sigma)$，在使用時要取 **Exponential** ，這樣就能保證 $\sigma$ 永遠為正，`log_std` 有 **Clip** 限制最大最小值。

```python=
def sample(self,state):
    mean , log_std = self.forward(state)
    std = torch.exp(log_std)
    try:
        dist = Normal(mean,std)
    except Exception as e:
        print("mean:", mean)
        print("std:", std)
        for param in self.parameters():
            print("Actor output out of range, check the input state or model parameters.")
            print("actor parameter:", param.data)
    x_t = dist.rsample()  # for reparameterization trick (mean + std * N(0,1))
    y_t = torch.tanh(x_t)
    action = y_t * self.action_max
    log_prob = dist.log_prob(x_t)
    # Enforcing Action Bound
    log_prob -= torch.log(self.action_max * (1 - y_t.pow(2)) + epsilon)
    log_prob = log_prob.sum(1, keepdim=True)
    mean = torch.tanh(mean) * self.action_max
    return action, log_prob, mean
```

`sample()` 是用來 **Select Action** ，用於與環境互動和計算 **Loss Function**，裡面有使用 **Pytorch** 的 **Normal Distribution**，為了可以傳遞 **Gradient** 到 **Actor** 的參數，所以要使用 `rsample()` 而不是 `sample()`。

然後在取得 **Action** 的方式使用了 **Squashed Gaussian Distribution**，所以在計算 **log_prob** 的方式也不太一樣，`self.action_max` 是 **Environment** 的 **Action** 最大值，我這邊偷懶沒有用 **Bias**，假設都是以 **0** 為中心。


---

**Alpha Initial** : 

```python=
# Alpha optimizer
self.log_alpha = torch.tensor(np.log(self.init_alpha))
self.log_alpha.requires_grad = True
self.target_entropy = - torch.tensor(self.num_actions, dtype=torch.float)
self.optimizer_alpha = torch.optim.Adam([self.log_alpha] , lr=self.lr, eps=1e-5)
```

```python=
@property
def alpha(self):
    return self.log_alpha.exp()
```

因為 **Tempture** $\alpha$ 應永遠為正，所以這邊也一樣是用 `log_alpha` 來當作實體變數，要使用時寫 `self.alpha` 就能自動取 **Exponential**，這邊寫的版本是能自動調整 $\alpha$ 的，這邊讓他設為可優化，初始值用 `self.init_alpha` 來設定。

---

**Update Critic** : 
```python=
# Get target value (Maximum Entropy)
with torch.no_grad():
    next_action , next_log_prob , _ = self.actor.sample(minibatch_s_)
    next_value1 = self.critic1_target(minibatch_s_,next_action)
    next_value2 = self.critic2_target(minibatch_s_,next_action)
    next_min_value = torch.min(next_value1 , next_value2)
    target_value = minibatch_r + self.gamma * (next_min_value * (1 - minibatch_done) - self.alpha * next_log_prob)
```

首先要計算出 **Q-Function** 要近似的 **Objective**，會重新從 **Actor Sample** 出 **Next Action** 和 **Log Probability**

```python=
# 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()
```

這邊就是將當前 **Q-Function** 與目標 **Value** 計算 **MSE Loss**，然後優化參數

---

**Update Actor** : 

```python=
 # Update Actor
action , log_prob , _ = self.actor.sample(minibatch_s)
value1 = self.critic1(minibatch_s , action)
value2 = self.critic2(minibatch_s , action)
min_value = torch.min(value1,value2)
actor_loss =  (self.alpha.detach() * log_prob - min_value).mean()
self.optimizer_actor.zero_grad()
actor_loss.backward()
self.optimizer_actor.step()
```
* 從 $Q_{\theta_1},Q_{\theta_2}$ 之間取一個最小的
* 重新 **Sample** 當前 **Action** 和 **Log Probability**


---

**Update Alpha** : 
```python=
alpha_loss = (self.alpha * (-log_prob - self.target_entropy).detach()).mean()        
self.optimizer_alpha.zero_grad()    
alpha_loss.backward()
self.optimizer_alpha.step()
```

按照演算法寫的更新 $\alpha$


