零、題記
強化學習的知識很瑣碎,我自己當初學的很吃力,特此整理部落格,以便後人批判性學習………………
這篇部落格一方面為了記錄當前所整理的知識點,另一方面PPO演算法實在是太重要了,不但要從理論上理解它到底是怎樣實現的,還需要從程式碼方面進行學習,這裡我就通俗的將這個知識點進行簡單的記錄,用來日後自己的回顧和大家的交流學習。
下面均是我自己個人見解,如有不對之處,歡迎評論區指出錯誤!
一. 公式推導
這裡簡要交代PPO的演算法原理及思想過程,主要記錄自己的筆記,公式記錄比較詳細,我這裡就不再贅述了,後面程式碼會緊緊貼合前面的內容,並且會再次提到一些細節。
好到這裡就是PPO的基本思想和RL的前期鋪墊工作了,這就是理論,脫離實踐的理論永遠也沒辦法好好理解,那麼下面我們來看看程式碼部分。
二 程式碼公示
程式碼選擇的是 動手學強化學習
下面我將逐一刨析整個程式碼的全部過程,之後大家可以將這些過程遷移到Issac Gym中進行學習和思考。
1 task_PPO.py檔案
點選檢視程式碼
import gym
import torch
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
import rl_utils
class PolicyNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim, action_dim):
super(PolicyNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
return F.softmax(self.fc2(x), dim=1)
class ValueNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim):
super(ValueNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
return self.fc2(x)
class PPO:
''' PPO演算法,採用截斷方式 '''
def __init__(self, state_dim, hidden_dim, action_dim, actor_lr, critic_lr,
lmbda, epochs, eps, gamma, device):
self.actor = PolicyNet(state_dim, hidden_dim, action_dim).to(device)
self.critic = ValueNet(state_dim, hidden_dim).to(device)
self.actor_optimizer = torch.optim.Adam(self.actor.parameters(),
lr=actor_lr)
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(),
lr=critic_lr)
self.gamma = gamma
self.lmbda = lmbda
self.epochs = epochs # 一條序列的資料用來訓練輪數
self.eps = eps # PPO中截斷範圍的引數
self.device = device
def take_action(self, state):
state = torch.tensor([state], dtype=torch.float).to(self.device)
probs = self.actor(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample()
return action.item()
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(-1, 1).to(self.device)
td_target = rewards + self.gamma * self.critic(next_states) * (1 -
dones)
td_delta = td_target - self.critic(states)
advantage = rl_utils.compute_advantage(self.gamma, self.lmbda,
td_delta.cpu()).to(self.device)
old_log_probs = torch.log(self.actor(states).gather(1,
actions)).detach()
for _ in range(self.epochs):
log_probs = torch.log(self.actor(states).gather(1, actions))
ratio = torch.exp(log_probs - old_log_probs)
surr1 = ratio * advantage
surr2 = torch.clamp(ratio, 1 - self.eps,
1 + self.eps) * advantage # 截斷
actor_loss = torch.mean(-torch.min(surr1, surr2)) # PPO損失函式
critic_loss = torch.mean(
F.mse_loss(self.critic(states), td_target.detach()))
self.actor_optimizer.zero_grad()
self.critic_optimizer.zero_grad()
actor_loss.backward()
critic_loss.backward()
self.actor_optimizer.step()
self.critic_optimizer.step()
if __name__ == "__main__":
#初始化引數
actor_lr = 1e-3
critic_lr = 1e-2
num_episodes = 500
hidden_dim = 128
gamma = 0.98
lmbda = 0.95
epochs = 10
eps = 0.2
device = torch.device("cpu")
env_name = "CartPole-v1"
env = gym.make(env_name)
env.reset(seed=0)
torch.manual_seed(0)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
#初始化智慧體
agent = PPO(state_dim, hidden_dim, action_dim, actor_lr, critic_lr, lmbda,
epochs, eps, gamma, device)
#開始訓練
return_list = rl_utils.train_on_policy_agent(env, agent, num_episodes)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('PPO on {}'.format(env_name))
plt.show()
mv_return = rl_utils.moving_average(return_list, 9)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('PPO on {}'.format(env_name))
plt.show()
2 rl_utils.py檔案
點選檢視程式碼
from tqdm import tqdm
import numpy as np
import torch
import collections
import random
class ReplayBuffer:
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity)
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
transitions = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
def size(self):
return len(self.buffer)
def moving_average(a, window_size):
cumulative_sum = np.cumsum(np.insert(a, 0, 0))
middle = (cumulative_sum[window_size:] - cumulative_sum[:-window_size]) / window_size
r = np.arange(1, window_size - 1, 2)
begin = np.cumsum(a[:window_size - 1])[::2] / r
end = (np.cumsum(a[:-window_size:-1])[::2] / r)[::-1]
return np.concatenate((begin, middle, end))
def train_on_policy_agent(env, agent, num_episodes):
return_list = []
for i in range(10):
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
transition_dict = {'states': [], 'actions': [], 'next_states': [], 'rewards': [], 'dones': []}
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
transition_dict['states'].append(state)
transition_dict['actions'].append(action)
transition_dict['next_states'].append(next_state)
transition_dict['rewards'].append(reward)
transition_dict['dones'].append(done)
state = next_state
episode_return += reward
return_list.append(episode_return)
agent.update(transition_dict)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[-10:])})
pbar.update(1)
return return_list
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
return_list = []
for i in range(10):
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
episode_return += reward
if replay_buffer.size() > minimal_size:
b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
transition_dict = {'states': b_s, 'actions': b_a, 'next_states': b_ns, 'rewards': b_r,
'dones': b_d}
agent.update(transition_dict)
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[-10:])})
pbar.update(1)
return return_list
def compute_advantage(gamma, lmbda, td_delta):
td_delta = td_delta.detach().numpy()
advantage_list = []
advantage = 0.0
for delta in td_delta[::-1]:
advantage = gamma * lmbda * advantage + delta
advantage_list.append(advantage)
advantage_list.reverse()
return torch.tensor(advantage_list, dtype=torch.float)
3 開始刨析
(1)先從主函式執行開始,其他的遇到了我們再來分析
首先是對函式的一些變數進行初始化,都包括Adam最佳化器的學習率,episode的長度,隱藏層的維數,獎勵函式的係數\(\gamma\)值,GAE的\(\lambda\)值,epoch的長度(本程式碼表示的是進行梯度反向傳播的輪數),eps表示PPO中截斷範圍clip函式的引數(-eps~+eps),device是什麼裝置(原始碼是cuda,這裡大家可以更改下,因為我是用cpu跑的),然後下面的就是程式碼的一些初始化了,我這裡不再贅述。
這哥倆就表述:前者是觀測值的數量(比如讀取的電機這時的角度、力矩、電機此時速度、機器人此時尤拉角、base線速度等【扯遠了】),後者是動作空間的數量(比如輸出的電機該咋轉的角度,比如一隻A1有12個電機,那麼輸出的action的維數就是12,代表著每一個電機最後應該轉多少角度,往哪裡轉)
(2)初始化智慧體
這時我們呼叫PPO函式,然後我們來看一下PPO函式具體都幹了些什麼。(這裡就是起了這個名字,不要誤會它在這個函式里把所有PPO演算法都實現了)
我們下面一步一步來看。
a.第一個地方
這裡是對Actor-Critic演算法的復現,定義了兩個神經網路,這兩個神經網路具體長什麼樣子呢?我們來看看:
PolicyNet是Actor,相當於前面我們提到的Policy Improvement,簡稱PI,是策略改善的網路,最終我們希望輸入一個狀態,我們就知道該使用什麼樣子的action,黑盒明白吧。
ValueNet是Critic,對Actor的動作做評估,進而更新state value或者action value的值。網路結構我也大致畫一下:
其實也就相當於前面去掉了softmax,至於為什麼呢?
你看哈,actor網路的作用是選擇一個動作進行輸出,所以我們使用softmax進行分類,理論上選擇讓累計獎勵更大的動作作為較大機率的輸出(greedy action),所以這裡用了一個分類器。
b.第二個地方
由於需要對兩個網路進行反向傳播,所以這裡定義了兩個Adam最佳化器進行。
普通咱一般用的是這種:
Adam最佳化器是這樣子:(如果比較感興趣的話,這篇部落格個人感覺講的比較好!)
程式碼裡的lr就是咱前面提到的學習率,一般都是10的負幾次方。
c.第三個地方就沒啥說的了,幾個賦值
記住以後再看torch的程式碼,python的程式碼,一般很喜歡用class的形式,這時你就把self
看成不同函式之間變數的搬運工,只要有定義self.xxxx
你就要知道一般這東西是在別處定義的,或者別處也能用得到的,這一點不是很絕對,但至少對初學者來說很實用。
(3)開始訓練
我們轉到對應的函式檔案去看看(ctrl+滑鼠左鍵)
可以說這個函式是這個程式碼最核心的部分,也是PPO演算法最核心的部分,我們來看看咋實現的。
a.第一話
首先迴圈10次,每一輪咱訓練50個episodes,一個episode就包含著很多個\(<state,action>\)對(這個對長度不一定,可能隨時停止,也可能儘可能探索到最大的episode_max長度),最後10x50等於500,所有episodes都執行完畢,此處對應程式碼:num_episodes = 500
然後是用tqdm定義一個進度條(這個不是重點,知道就行)
初始化變數\(transition_dict\),這個變數包含5個值:現在的狀態states,執行的動作actions,執行完到下一個時刻的狀態next_states,獎勵函式rewards,以及這一時刻執行完的結束標誌done(結束為1,否則為0,記住後面程式碼裡要用到)
b.第二話
接著上面的程式碼再繼續分析
I)take actions
可以看到首先將輸入的state轉換為tensor,然後因為actor網路的最後一層是softmax函式,所以透過actor網路輸出兩個執行兩個動作可能性的大小,然後透過action_dist = torch.distributions.Categorical(probs) action = action_dist.sample()
根據可能性大小進行取樣最後得到這次選擇動作1進行返回。
II)env.step(action)
按照採取的動作,和環境進行互動,返回到達下一時刻狀態的值、獎勵函式以及一個episode是否結束的結束標誌done。
III)transition_dist
然後將這些返回的值裝進這個字典裡面,有點像\(replay buffer\)我個人感覺。
然後更新下一時刻的狀態值,這個episode的獎勵函式也將累加episode_return += reward
然後透過return_list.append(episode_return)
記錄下這一整個episode的總的累計獎勵R,用來plt繪圖吧,這個就類似TensorBoard的觀測輸出了。
IV)agent.update(...)
agent.update(transition_dict)
到了這個函式。
這個函式說明了我們如何利用這些變數進行更新呢?
首先先從transition_dict
取出變數存入tsnsor
中便於計算
然後定義了TD Target:
然後是TD error
最後這倆就一起說了
前一個是計算GAE
advantage
表示的是某狀態下采取某動作的優勢值,也就是咱前面提到的PPO演算法公式中的A:
old_log_probs
則表示的是舊策略下某個狀態下采取某個動作的機率值的對數值。對應下面PPO演算法公式中的clip函式中分母:
然後這個函式下面就老有意思了:
首先第一部分: log_probs = torch.log(self.actor(states).gather(1, actions))
求對數值,然後下面新舊相減:
ratio = torch.exp(log_probs - old_log_probs)
這不就是\(e^{[ln(a)-ln(b)]} = \frac{a}{b}嘛\)
其實它就是在計算這個:
然後下面的surr1和surr2起初我也看不懂,後來看看演算法自然而然就明白是幹嘛的了:
這倆其實就是用前面的ratio
分別相乘來作為下圖的兩個值,結果再求最小值,講約束條件納入公式裡面,這就是PPO的核心!!!
這其實就已經寫完了actor的損失函式了,也就是策略的損失函式了。至於critic的損失函式,是在求td target和critic(states)
的MSE,均方誤差。
然後清零Adam,梯度反向傳播,更新。。迴圈epoch=10
次進行引數的神經網路引數的更新,每一次相當於收集了50個episodes。
c.
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[-10:])})
pbar.update(1)
更新進度條,跟咱要學的PPO沒關係。
d.
return return_list
返回這個獎勵函式表。用來最後的輸出評估:
(4)記錄
大家可能對整個流程比較糊塗,這裡簡單的做一個梳理:
下面引用自其他佬的部落格:
三、Acknowledge
感謝這些大佬的部落格和思路分享,才讓我漸漸理解整個程式碼思想。
鳴謝1
鳴謝2
鳴謝3