# 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$