# Policy Gradient
[TOC]
分類 :
* `Model-Free`
* `Policy Based`
* `on Policy`
* `Discrete Action Space`
* `Discrete/Continuous State Space`
## Introduction
**Policy Gradient** 是一種 **Policy-Based** 的算法,與 **Value-Based** 的 **DQN** 不一樣的地方就是輸出不是期望獎勵,而是輸出機率,用 **Neural Network** 建立一個 **Model**,**Input** 為 **State** $s_t$,最後一層的 **Activation Function** 要改成 **Softmax**,這樣就能輸出機率並且所有機率加總為 1,**Model** 的參數為 $\theta$,用參數 $\theta$ 決定 **Action** 的 **Neural Network** 我們稱為 $\pi$。

以 $s_t$ 為 **Input**,$\pi$ 使用參數 $\theta$ 做出動作 $A_0$ 的機率就是$p_\theta(A_0|s_t)$,因為經過 **Softmax**,所以所有機率加總 $p_\theta(A_0|s_t)+p_\theta(A_1|s_t)=1$。

以 CartPole 來舉例,在$s_t$為 Input 的情況下,**Neural Network** $\pi$ 使用參數 $\theta$ 向左走($A_0$)的機率為 $p_\theta(A_0|s_t)$,ex.向左 **0.7**、向右 **0.3**,$s_t$ 為推車位置、速度和木棍的速度、角速度。
我們可以把根 **Environment** 互動的過程記錄成一段軌跡(Trajectory),可以寫成 :
## Trajectory
Agent 與環境互動的方式如下 :

上面的一個循環就是一次 **Step**,**Agent** 會一直循環值到環境或遊戲結束,一場遊戲稱為 **Episode**,一個 **Episode** 的 **Total Reward** 可以寫成 $R=\sum_{t=1}^Tr_t$,Agent 的目標就是獲取最大的 **Episode Reward**。
將一個 **Episode** 的所有資訊組合起來就是 **Trajectory**(軌跡),可以寫成 :
$\tau^n=\{(s^n_1,a^n_1,r^n_1),...,(s^n_t,a^n_t,r^n_t)\}$
* $n$ : **Episode**
* $t$ : **Step**
與環境互動一個 **Episode** 獲得 **Trajectory**,只要裡面的 **state、action、reward** 任一個不一樣,或是順序不一樣,就算是一個不同的 **Trajectory**
## 期望值 Overview
用機器學習常用的寫法來描述期望值,用骰子舉例 :
$E_{x\sim p(x)}[g(x)]=\int^\infty_{-\infty}p(x)g(x)dx$
* $x$ : 隨機變數,可以當作骰子 1 ~ 6
* $p(x)$ : 隨機變數 $x$ 被 Sample 的機率密度函數,以骰子來說的話 x 不管輸入甚麼,輸出的機率都是 $\frac{1}{6}$(其他範例有可能因為 $x$ 的 Input 而不一樣)。
* $E_{x\sim p(x)}$ : 使用機率密度函數(**Probability Density Function**) $p(x)$ 來 **sample** 出 $x$ 的期望值。
* $g(x)$ : 代表隨機變數實際獲得的價值,但骰子的例子的話 $g(1)=1$,是一樣的。
* $E_{x\sim p(x)}[g(x)]$ : 代表了使用機率密度函數 $p(x)$ **sample** 出 $x$ 預計能夠獲得的期望值。
* $\int^\infty_{-\infty}p(x)g(x)dx$ : 前面算是一種表示式,這邊的就很像是實際的計算方法,積分的意義是面積,在現實面就是連續的加總,好消息是骰子不連續,也就是$x$ 為 1~6 總共 6 種情況,所以其實也可以寫成 $\sum_{x=1}^6 g(x)p(x)$。
$g(x)=x,p(x)=\frac{1}{6}$ ,所以如果要計算骰子的期望值的話 :
$E_{x\sim p(x)}[g(x)]=\sum_{x=1}^6 g(x)p(x)=\sum_{x=1}^6 x\frac{1}{6}$
$=1\frac{1}{6}+2\frac{1}{6}+3\frac{1}{6}+4\frac{1}{6}+5\frac{1}{6}+6\frac{1}{6}=3.5$
如果無法窮舉所有 $x$ ,或是無法獲得機率密度函數$p(x)$怎麼辦?,我們可以寫成取樣並近似的方法 :
$\frac{1}{N}\sum_{n=1}^N V_n$
$V$ 為 Sample 出的骰子數值,因為 **Sample** 出甚麼數值本身就有被機率影響,出現次數多的機率自然高,反之亦然,所以可以把 $p(x)$ 拿掉,並取平均。
---
程式測試
```python!
import numpy as np
sum = 0
for i in range(6):
data = i+1
sum += (1/6)*data
print("Real Expected :",sum)
sum = 0
num = 300000
sample = np.random.choice(6, num)+1 # sample 1~ 6 100 times
for d in sample:
#print(d)
sum += d
sum /= num
print("Sample Expected :",sum)
```

## Expected Reward
我們的 **Agent** 是一個 **Neural Network**,會輸出每個動作的機率,也可以想成 **Agent** 覺得哪個動作獲得獎勵的機會,比如往左 0.7,往右 0.3,那就是 **Agent** 覺得往左可以獲得更多 Reward。
一個 **Episode** 的 **Total Reward** 或 **Trajectory** 的總 **Reward** 可以寫成 :
$R(\tau)=\sum_{t=1}^Tr_t$
如果要計算 **Neural Network** 能夠獲得的期望獎勵,一樣參考 **MDP**,但是要帶入轉移機率,所以可以寫成 :
$\hat{R}_\theta=E_{\tau\sim p_\theta(\tau)}[R(\tau)]=\sum_\tau R(\tau)p_\theta(\tau)$
* $\hat{R}_\theta$ : **Neural Network** 在參數 $\theta$ 的情況下所能獲得的期望獎勵
* $\tau$ : **Trajectory** (軌跡),代表一個 **Episode** 與環境互動的資訊,包含 **state、action、reward**,按時間順序組合在一起。
* $p_\theta(\tau)$ : 機率密度函數(**Probability Density Function**),**Neural Network** 使用參數 $\theta$ Sample 出隨機變數 $\tau$ 的機率。
* $E_{\tau\sim p_\theta(\tau)}[R(\tau)]$ : 使用 $p_\theta(\tau)$ 這個機率密度函數 **Sample** 出 $\tau$,以及 $\tau$ 所能獲得的期望值,也就是期望獎勵。
* $\sum_\tau R(\tau)p_\theta(\tau)$ : 窮舉所有可能的 $\tau$ ,並用機率$p_\theta(\tau)$ 乘上$\tau$ 的總獎勵 $R(\tau)$
到這邊因為我們無法窮舉所有 $\tau$,所以要用取樣近似的方法,但是如果機率被拔掉了,**Neural Network** 就沒辦法參與計算了,所以要繼續推導簡化。
## Trajectory Probability
剛剛有說期望獎勵(Expected Reward)如下 :
$\hat{R_\theta}=\sum_\tau R(\tau)p_\theta(\tau)$
展開 :
$p_\theta(\tau)$
$=p(s_1)p_\theta(a_1|s_1)p(s_2|s_1,a_1)p_\theta(a_2|s_2)p(s_3|s_2,a_2)...$
可以看到上面的 $p(\tau)$ 展開式三個部分重複循環的,個別如下 :
* $p(s_1)$ : **Sample** 出環境初始化的第一個 **state** $s_1$ 的機率,可以看到他是沒有 $\theta$ 的,也就是這個機率由環境控制,**Neural Network** 無法參與。
* $p_\theta(a_1|s_1)$ : **Neural Network** 使用參數 $\theta$,將 $s_1$ 作為 **Input** , **Sample** 出動作 $a_1$ 的機率
* $p(s_2|s_1,a_1)$ : 環境根據先前的$s_1$ 和 **Agent** 做出的 $a_1$ **Sample** 出 $s_2$ 的機率
因為他是循環的,所以可以寫成累乘的形式。
$p(\tau)$
$=p(s_1) \prod_{t=1}^Tp_\theta(a_t|s_t)p(s_{t+1}|s_t,a_t)$
## Policy Gradient
我們知道現在 **Deep Learning** 是使用 **Gradient Descent**(梯度下降)來優化參數,但 **Gradient Descent** 的優化是將 **Loss** 降低。我們如果希望 **Reward** 最大化的話就是用 **Gradient Ascent**,更新參數的方法要從$\theta_{t+1}=\theta_t-\nabla f(\theta_t)$ 改成 $\theta_{t+1}=\theta_t+\nabla f(\theta_t)$
如果要更新 **Neural Network** 的參數 $\theta$,則對 **Expected Reward Function** 的 $\theta$ 做偏導 :
$\hat{R_\theta}=\sum_\tau R(\tau)p_\theta(\tau)$
微分 :
$\nabla\hat{R}_\theta=\sum_\tau R(\tau)\nabla p_\theta(\tau)$
因為只有 $p_\theta(\tau)$ 跟 $\theta$ 有關,所以只需要對$p_\theta$ 微分,但我們前面有提到一個問題,就是我們無法窮舉 $\tau$,所以還要繼續推導
分子分母同乘 $p_\theta(\tau)$
$\large \nabla\hat{R}_\theta=\sum_\tau R(\tau)p_\theta(\tau)\frac{\nabla p_\theta(\tau)}{p_\theta(\tau)}$
用公式代換 :
$\nabla f(x)=f(x)\nabla \mathrm{log}f(x)$
$\large \nabla\hat{R}_\theta(\tau)=\sum_\tau R(\tau)p_\theta(\tau)\frac{p_\theta(\tau)\nabla \mathrm{log} p_\theta(\tau)}{p_\theta(\tau)}$
簡化為 :
$\nabla\hat{R}_\theta(\tau)=\sum_\tau R(\tau)p_\theta(\tau))\nabla\mathrm{log}p_\theta(\tau)$
寫回期望值型式 :
$E_{\tau\sim p_\theta(\tau)}[R(\tau)\nabla\mathrm{log}p_\theta(\tau)]=\int_{-\infty}^\infty p_\theta(\tau)R(\tau)\nabla \mathrm{log}p_\theta(\tau)$
上面窮舉的形式我們絕對實現不了,所以寫成取樣近似的形式(可以把機率密度函數 $p_\theta(\tau)$ 拿掉 ) ,用 **Sample** 出的資料來計算平均值近似 :
$E_{\tau\sim p_\theta(\tau)}[R(\tau)p_\theta(\tau)\nabla\mathrm{log}p_\theta(\tau)]\approx\frac{1}{N}\sum_{n=1}^N R(\tau^n)\nabla\mathrm{log}p_\theta(\tau^n)$
$p_\theta(\tau)$ 包含了 $\theta$ 相關的機率和 **Environment** 的機率,我們無法得知 **Enviroment** 相關的機率 $p(s_1)$、$p(s_{t+1}|s_t,a_t)$,且他跟 Agent 或參數 $\theta$ 是沒關係的,所以我們把它拔掉。
$\approx\frac{1}{N}\sum_{n=1}^N R(\tau^n)\nabla\mathrm{log}\prod_{t=1}^Tp_\theta(a^n_t|s^n_t)$
因為 $\mathrm{log}(xy)=log(x)+log(y)$ ,所以 log 裡面的累乘可以寫成加總 :
$\large=\frac{1}{N}\sum_{n=1}^N R(\tau^n)[\sum_{t=1}^{T_n} \nabla\mathrm{log}p_\theta(a^n_t|s^n_t)]$
調換順序,可以把$R(\tau^n)$乘進去 :
$\large=\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n} R(\tau^n)\nabla\mathrm{log}p_\theta(a^n_t|s^n_t)$
* $n$ : **Episode**
* $t$ : **Step**
這個也很直覺,假如 **Reward** = 100 那我們只要提高機率 $p_\theta(a^n_t|s^n_t)$ 就能使 **Expected Reward** 增加。
$log(x)$ :

## Update model
前面有提到要做 Gradient Ascent :
* $\theta_{t+1} = \theta + \eta \nabla \hat{R}_\theta$
* $\nabla\hat{R}_\theta=\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n} R(\tau^n)\nabla\mathrm{log}p_\theta(a^n_t|s^n_t)$
我們後面會使用 **Pytorch** 來實作,只要給 **Loss Function** 就能夠自動微分,再搭配優化器(ex.**Adam**)來訓練 **Neural Network**,最後的 **Loss Function** 就再加個負號就可以 **Gradient Ascent** 了 :
$Loss=-\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n} R(\tau^n)\mathrm{log}p_\theta(a^n_t|s^n_t)$
### Advantage
我們前面講的 $R(\tau)$ 可以換成其他 **Function**,在 **Policy Gradient** 系列的延伸會叫他 **Advantage Function**,在下圖寫成 $\Psi$,並可以替換成 1~6 的寫法 :

第 1 個是我們一開始推導的,也就是 **Episode total reward**,第 2 個是從 **step** $t$ 開始累加到結束的 **reward**,很像 **DQN** 的 **TD-error**,只是少了折扣因子(Discount factor) $\gamma$
第 3 則是第 2 個方法的 **baseline** 版本,其實就是讓 **Agent** 有比較的效果,使得計算結果有正有負,最簡單的方法就是取 **Episode** 的平均值,讓每個 **step** 得到的 **reward** 去減平均值
4~6 都是 **Actor-Critic** 使用的方法
## CartPole Example

這個 **Environment** 是一個不固定的木棍接在推車上,推車沿著無摩擦的軌道左右移動,目標是通過推車左右移動來保持平衡。
### Action Space
這個 **Environment** 是離散動作空間(**Discrete Action Space**),與環境互動需要的動作數量只要 1
* 0 : **Move Left**
* 1 : **Move Right**
### Observation Space
**State** 會回傳四個 **float**,分別為推車位置(**Cart Position**)、推車速度(**Cart Velocity**)、木棍角度(**Pole Angle**)、木棍角速度(**Pole Angular Velocity**)

---
### Rewards
這個環境的目標是盡可能長時間保持木棍直立,每一個 **Step** 都會給予 **Reward** 1,包括木棍失去平衡倒下而中止。
如果 `sutton_barto_reward=True` ,則每一個 **Step** 都會獲得 **Reward** 0,而中止時會獲得 -1
---
### Starting State
起始的 **State** 會隨機將數值分配在 (-0.05,0.05) 的範圍。
---
### Episode End
Termination :
* 木棍角度大於 +-$12$
* 推車超過邊緣
**Truncation** (有使用 `TimeLimit` 時)
* **Episode** 的 Step 長度大於 500 (v0 為200)
## CartPole-v1 Policy Gradient
**Github Code** : https://github.com/jason19990305/PolicyGradient.git
### Network
```python=
import torch.nn as nn
class Network(nn.Module):
def __init__(self, args, hidden_layers=[64, 64]):
super(Network, self).__init__()
self.num_states = args.num_states
self.num_actions = args.num_actions
# Insert input and output sizes into hidden_layers
hidden_layers.insert(0, self.num_states)
hidden_layers.append(self.num_actions)
# Create fully connected layers
fc_list = []
for i in range(len(hidden_layers) - 1):
num_input = hidden_layers[i]
num_output = hidden_layers[i + 1]
layer = nn.Linear(num_input, num_output)
fc_list.append(layer)
# Convert list to ModuleList for proper registration
self.layers = nn.ModuleList(fc_list)
self.relu = nn.ReLU()
self.softmax = nn.Softmax(dim=1) # Softmax for action probabilities
def forward(self, x):
# Pass input through all layers except the last, applying ReLU activation
for i in range(len(self.layers) - 1):
x = self.relu(self.layers[i](x))
# The final layer outputs the Q-value directly (no activation)
action_probability = self.softmax(self.layers[-1](x))
return action_probability
```
* 建立一個 **Neural Network** 的工具很多,這邊選擇用 **Pytorch**,`class Network(nn.Module)` 是繼承 `torch.nn.Module` 類別,建立起一個基本的 **Neural Network**
* `args` 是從主程式的 **Hyperparameter** 傳進來的, `num_states` 和 `num_actions` 為 **Environment** 的 **state** 、 **action** 大小,`num_actions` 為動作的種類,實際輸出的動作只有一個純量,有些環境要求輸出多個 action,如六軸機械手臂,要多注意。
* `hidden_layers` 為 **List**,主要用來定義 **Hidden layer** 的維度,也可以理解為 **Output shape**,定義這一層要輸出多大的 **Vector**,**Input/Output layer** 則由 `num_states` 和 `num_actions` 決定。
```python=
# Create fully connected layers
fc_list = []
for i in range(len(hidden_layers) - 1):
num_input = hidden_layers[i]
num_output = hidden_layers[i + 1]
layer = nn.Linear(num_input, num_output)
fc_list.append(layer)
# Convert list to ModuleList for proper registration
self.layers = nn.ModuleList(fc_list)
self.relu = nn.ReLU()
```
* 上面是建立 **Linear Layer** 的實體(**DNN**,全連接),並轉為 `ModuleList` 儲存。
* **Activation Function** 選擇用常見的 **ReLU**
```python=
def forward(self, x):
# Pass input through all layers except the last, applying ReLU activation
for i in range(len(self.layers) - 1):
x = self.relu(self.layers[i](x))
# The final layer outputs the Q-value directly (no activation)
action_probability = self.softmax(self.layers[-1](x))
return action_probability
```
* **Overwrite** `forward()` 這個 **Function**,定義 **Policy Gradient** 的計算方式,`i` 代表的是當前層數,最後一層要的輸出要計算 **Softmax**,這樣才會輸出機率,因為 **Policy Gradient** 是 **Policy-base** 的算法,一定要使用機率計算 **loss**,並且所有動作的機率加總為 1。
* `self.relu(self.layers[i](x))` 為經過全連接層後再經過 **ReLU Function**
---
### Replay Buffer
```python=
import numpy as np
import torch
class ReplayBuffer:
def __init__(self, args):
self.clear_batch()
self.episode_count = 0
self.episode_batch = []
def clear_batch(self):
self.s = []
self.a = []
self.r = []
self.s_ = []
self.done = []
self.count = 0
def store(self, s, a , r, s_, done):
self.s.append(s)
self.a.append(a)
self.r.append(r)
self.s_.append(s_)
self.done.append(done)
def to_episode_batch(self):
s = torch.tensor(np.array(self.s), dtype=torch.float)
a = torch.tensor(np.array(self.a), dtype=torch.int64)
r = torch.tensor(np.array(self.r), dtype=torch.float)
s_ = torch.tensor(np.array(self.s_), dtype=torch.float)
done = torch.tensor(np.array(self.done), dtype=torch.float)
self.episode_batch.append((s, a, r, s_, done))
self.episode_count += 1
self.clear_batch()
def clear_episode_batch(self):
self.episode_batch = []
self.episode_count = 0
```
* `__init__` : 初始化 **Batch** `(s,a,r,s_,done)` 和 **Episode** (**Trajectory** $\tau$)
* `clear_batch` : 清除 **Batch** 裡的資料,會在一個 **Episode** 結束後 **call** 這個 **Function**
* `store` : 每一個 `step` 之後要儲存,**State、Action、Reward、Next State** 和 **Done** ,儲存到 Batch
* `to_episode_batch` : 將當前 **Episode** 的 **Batch** 儲存到大的 **Buffer**,裡面以 **Episode** 為單位儲存 **Trajectory** $\tau$
* `clear_episode_batch` : 清除所有 **Trajectory** $\tau$,通常會在 **Neural Network Update** 結束後清除,因為機率改變了,所以 **Trajectory** 應該會完全不一樣,原本的資料已經不能評估現在的 **Neural Network** 了
### Initialize
```python
def __init__(self , args , env , hidden_layer_list=[64,64]):
# Hyperparameter
self.training_episodes = args.training_episodes
self.advantage = args.advantage
self.num_states = args.num_states
self.num_actions = args.num_actions
self.epochs = args.epochs
self.lr = args.lr
# Variable
self.episode_count = 0
# other
self.env = env
self.replay_buffer = ReplayBuffer(args)
# The model interacts with the environment and gets updated
self.actor = Network(args , hidden_layer_list.copy())
print(self.actor)
self.optimizer = torch.optim.Adam(self.actor.parameters() , lr = self.lr , eps=1e-5)
```
面的是 Class `Agent` 的初始化,各變數說明如下 :
* `training_episodes` : 要 **Sample** 的 **Trajectory** 的數量,有這個資料量後就進行 **Training**
* `advantage` : 用於切換 **Advantage function**
* `0` : **Total Reward**
* `1` : **Reward Following**
* `2` : **Baseline**
* `num_states` : **State** 個數
* `num_actions` : 可輸出的 **Action** 種類,但要注意
* `epochs` : **Agent Rollout** 次數,也就是要跟環境互動多少次 **Episode**
* `lr` : **Learning Rate**
---
* `actor` : **Neural Network**
* `optimizer` : **Adam** 優化器,並指定去優化 `actor` 這個 **Neural Network** 的參數
---
### Choose action
```python=
def choose_action(self, state):
with torch.no_grad():
state = torch.unsqueeze(torch.tensor(state), dim=0)
action_probability = self.actor(state).numpy().flatten()
action = np.random.choice(self.num_actions, p=action_probability)
return action
```
這個 Function 用於 Training 階段用來 Sample Action,`with torch.no_grad()` 讓下面的計算不會加入 Backward,因為現在是與環境互動還沒有要計算 loss。
* `unsqueeze` : 可以把 Tensor 降低一個維度,ex. [1,2] -> [2]
* `action_probability` : 給予 **Agent Input State**,輸出每個 Action 的機率 $p_\theta(a_t|s_t)$
* `np.random.choice` : 這個 **Function** 可以給予機率分布,然後根據機率輸出 0 ~ `num_actions` - 1 的數值,ex.prob=[0.9,0.1] , 輸出 0 的機率就為 90%
---
### Evaluate action
```python=
def evaluate_action(self, state):
with torch.no_grad():
# choose the action that have max q value by current state
state = torch.unsqueeze(torch.tensor(state), dim=0)
action_probability = self.actor(state)
action = torch.argmax(action_probability).item()
return action
```
跟 `choose_action` 相似,但不一樣的地方是要無條件選擇機率最大的 **Action**,`torch.argmax` 可以回傳數值最大的 **Index**
### Train
```python=
def train(self):
episode_reward_list = []
episode_count_list = []
episode_count = 0
# Training loop
for epoch in range(self.epochs):
# reset environment
state, info = self.env.reset()
done = False
while not done:
action = self.choose_action(state)
# interact with environment
next_state , reward , terminated, truncated, _ = self.env.step(action)
done = terminated or truncated
self.replay_buffer.store(state, action, [reward], next_state, [done])
state = next_state
self.replay_buffer.to_episode_batch() # Convert to episode batch
if (epoch + 1)% self.training_episodes == 0 and epoch != 0:
# Update the model
self.update()
self.replay_buffer.clear_episode_batch() # Clear the episode batch after updating
if epoch % 10 == 0:
evaluate_reward = self.evaluate(self.env)
print("Epoch : %d / %d\t Reward : %0.2f"%(epoch,self.epochs , evaluate_reward))
episode_reward_list.append(evaluate_reward)
episode_count_list.append(episode_count)
episode_count += 1
# Plot the training curve
plt.plot(episode_count_list, episode_reward_list)
plt.xlabel("Episode")
plt.ylabel("Reward")
plt.title("Training Curve")
plt.show()
```
* `replay_buffer.store` : 把這次 Step 的 $(s_t,a_t,r_t,s_{t+1})$ 還有結束狀態 done 加入到 Buffer 中
* `replay_buffer.to_episode_batch()` : 將這次的 Trajectory 資料加到 Episode Buffer
* `update` : 每當互動次數到達 `training_episodes` 指定的次數後就進行 Neural Network 進行參數更新
* `clear_episode_batch` : 每 Buffer 中的所有資料清空
### Update
```python=
def update(self):
loss = 0
base_line = 0
for batch in self.replay_buffer.episode_batch:
s, a, r, s_, done = batch
base_line += self.TotalReward(r)
base_line /= self.replay_buffer.episode_count # Normalize baseline by number of episodes
for batch in self.replay_buffer.episode_batch:
s, a, r, s_, done = batch
a = a.view(-1, 1) # Reshape action from (N) -> (N, 1) for gathering
if self.advantage == 0:
adv = self.TotalReward(r)
elif self.advantage == 1:
adv = self.RewardFollowing(r)
elif self.advantage == 2:
adv = self.RewardFollowing(r)
adv = adv - base_line
prob = self.actor(s).gather(dim=1, index=a) # Get action probability from the model
log_prob = torch.log(prob + 1e-10) # Add small value to avoid log(0)
loss += (adv * log_prob).sum()
loss = - loss / self.replay_buffer.episode_count # Normalize loss by number of episodes
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
```
---
第一個 for loop 用來計算 baseline 會計算以 Episode total reward 為單位的平均
第二個 for loop 一開始會先調整 `a` 的維度,從 [N] -> [N,1],然後計算 advantage,這邊有三種
1. **Total Reward** : $\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n}r_t^n$
2. **Reward Following** : $\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n}\sum_{t'=t}^{T_n}r_{t'}$
3. **Baseline** : $\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n}\sum_{t'=t}^{T_n}r_{t'}-b(s_t)$
算完 **Advantage** 後要計算 **log Probability**,然後計算 **loss**,再來取平均,就會是我們前面說的公式
$Loss=-\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n} \Psi_t\mathrm{log}p_\theta(a^n_t|s^n_t)$
$\Psi$ 就是 **Advantage Function**,加上負號是為了 **Gradient Ascent**,然後就可以 `zero_grad()`、`backward()`、`step()` 根據 Loss 優化參數
## Result
**Hyperparameter** :
```
training_episodes = 3
advantage = 0
epochs = 5000
lr = 0.0001
num_states = 4
num_actions = 2
---------------
Network(
(layers): ModuleList(
(0): Linear(in_features=4, out_features=128, bias=True)
(1): Linear(in_features=128, out_features=128, bias=True)
(2): Linear(in_features=128, out_features=2, bias=True)
)
(relu): ReLU()
(softmax): Softmax(dim=1)
)
```
結果 :


---
```
training_episodes = 3
advantage = 1
epochs = 5000
lr = 0.0001
num_states = 4
num_actions = 2
---------------
Network(
(layers): ModuleList(
(0): Linear(in_features=4, out_features=128, bias=True)
(1): Linear(in_features=128, out_features=128, bias=True)
(2): Linear(in_features=128, out_features=2, bias=True)
)
(relu): ReLU()
(softmax): Softmax(dim=1)
)
```


---
## Conclusion
這次有實作三種 **Advantage Function**,我寫的 **Baseline** 是比較簡單的,還是要搭配 **Actor-Critic** 效果才會比較好,**Policy Gradient** 缺點還是很多,只是在 **CartPole** 這個環境比較簡單,而且這種 **Policy Gradient** 只能用在離散動作空間(**Discrete Action Space**),如果想要用在連續動作空間,**on-policy** 的強化學習算法通常會搭配 **Normal Distribution** 來生成連續的動作,並且依然可以求得 Action 的機率,可以參考 **Vanila Policy Gradient**,在 **PPO** 也有類似的方法可以達成。
還有一個需要注意的是這邊實作的方法只能用於輸出單一動作,如果這個環境要求輸出多個動作,如機械手臂需要同時給出多個 **Joint** 的角度的話就需要 **Agent** 產生多個動作,多個動作的機率必須相乘才會得到做出這組動作的機率,這樣才能後續計算。
如果想要讓 **Policy Gradient** 效果更好,可以加入 **policy entropy**,就是 Deep Learning 分類問題常用的方法,單我們是計算 **Agent Action** 機率 的 **Entropy**,只要機率越平均,**Entropy** 就越大,我們一樣在 **Loss - Entropy**,這樣他優化時就會希望每種動作越平均越好,以此防止進入局部最優。