詳解近端策略優化(ppo,干貨滿滿)


本文首發於行者AI

引言

上一篇文章我們詳細介紹了策略梯度算法(PG),ppo其實就是策略梯度的一種變形。首先介紹一下同策略(on-policy)與異策略(off-policy)的區別。

在強化學習里面,我們需要學習的其實就是一個智能體。如果要學習的智能體跟和環境互動的智能體是同一個的話,稱之為同策略。如果要學習的智能體跟和環境互動的智能體不是同一個的話,稱之為異策略。那么先給童鞋們提出一個問題,ppo算法是同策略還是異策略?

1. 同策略的不足之處

首先我們回顧一下PG的期望獎勵值,公式如下。

\[\begin{aligned} \nabla \bar{R}_{\theta} &=E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] \end{aligned} \]

上面更新的公式中的\(E_{\tau \sim p_{\theta}(\tau)}\)是在策略\(\pi_{\theta}\)的情況下, 所采樣出來的軌跡\(\tau\)做期望。但是如果更新了參數,從\(\theta\)變成\(\theta^{\prime}\),概率\(p_{\theta}(\tau)\)就不對了,之前采樣出來的數據就不能用了。所以PG會花很多時間去采樣數據,可以說大多數時間都在采樣數據,智能體去跟環境做互動以后,接下來就要更新參數,只能用這些數據更新參數一次。接下來就要重新再去收集數據,才能再次更新參數。

2. 改進同策略的思路

策略梯度是同策略的算法,所以非常耗費時間,那么一個可能的改進思路是將同策略變成異策略。簡單的思路就是用另外一個策略\(\pi_{\theta^{\prime}}\), 另外一個演員\(\theta^{\prime}\)去跟環境做互動。用\(\theta^{\prime}\)收集到的數據去訓練\(\theta\)。假設我們可以用\(\theta^{\prime}\)收集到的數據去訓練\(\theta\),意味着說我們可以把\(\theta^{\prime}\)收集到的數據用很多次,也就是可以執行梯度上升好幾次,更新參數好幾次,這都只要用同一筆數據就可以實現。因為假設\(\theta\)有能力學習另外一 個演員\(\theta^{\prime}\)所采樣出來的數據的話,那\(\theta^{\prime}\)就只要采樣一次,也許采樣多一點的數據,讓\(\theta\)去更新很多次, 這樣就會比較有效率。

3. 同策略到異策略的具體實現

那么問題來了, 我們怎么找到這樣的一個演員\(\theta^{\prime}\),使其收集到的數據可以用於訓練\(\theta\),且他們之間的差異可以被忽略不計呢?

首先我們先介紹一個名詞,重要性采樣(importance sampling)。 假設有一個函數\(f(x)\)\(x\)需要從分布\(p\)中采樣。我們應該如何怎么計算\(f(x)\)的期望值呢?假設分布\(p\)不能做積分,那么我們可以從分布\(p\)盡可能多采樣更多的\(x^{i}\)。這樣就會得到更多的\(f(x)\),取它的平均值就可以近似\(f(x)\)的期望值。

現在另外一個問題也來了,假設我們不能在分布\(p\)中采樣數據,只能從另外一個分布\(q\)中去采樣數據,\(q\)可以是任何分布。我們從\(q\)中采樣\(x^{i}\)的話就不能直接套下面的式子。

\[E_{x \sim p}[f(x)] \approx \frac{1}{N} \sum_{i=1}^{N} f\left(x^{i}\right) \]

因為上式是假設\(x\)都是從\(p\)采樣出來的。如果我們想要在\(q\)中采樣的情況下帶入上式,就需要做些變換。期望值\(E_{x \sim p}[f(x)]\)的另一種寫法是\(\int f(x) p(x) d x\),不知道的童鞋可以補習一下萬能的學科--數學,對其進行變換,如下式所示,

\[\int f(x) p(x) d x=\int f(x) \frac{p(x)}{q(x)} q(x) d x=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right] \]

整理得下式,

\[E_{x \sim p}[f(x)]=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right] \]

這樣就可以對分布\(q\)中采樣的\(x\)取期望值。具體來說,我們從\(q\)中采樣\(x\),再去計算\(f(x) \frac{p(x)}{q(x)}\),最后取期望值。所以就算我們不能從\(p\)里面去采樣數據,只要能夠從\(q\)里面去采樣數據,代入上式,就可以計算從分布\(p\)采樣\(x\)代入\(f(x)\)以后所算出來的期望值。

這邊是從\(q\)做采樣,所以我們從\(q\)里采樣出來的每一筆數據,需要乘上一個重要性權重(importance weight)\(\frac{p(x)}{q(x)}\)來修正這兩個分布的差異。\(q(x)\)可以是任何分布。重要性采樣有一些問題。雖然我們可以把\(p\)換成任何的\(q\)。但是在實現上,\(p\)和不\(q\)能差太多。差太多的話,會有一些問題。兩個隨機變量的平均值一樣,並不代表它的方差一樣,這里不展開解釋,感興趣的童鞋可以帶入方差公式\(\operatorname{Var}[X]=E\left[X^{2}\right]-(E[X])^{2}\)推導一下。

現在要做的事情就是把重要性采樣用在異策略的情況,把同策略訓練的算法改成異策略訓練的算法。 怎么改呢,如下式所示,我們用另外一個策略\(\pi_{\theta^{\prime}}\),它就是另外一個演員,與環境做互動,采樣出軌跡\(\theta^{\prime}\),計算\(R(\tau) \nabla \log p_{\theta}(\tau)\)

\[\nabla \bar{R}_{\theta}=E_{\tau \sim p_{\theta^{\prime}(\tau)}}\left[\frac{p_{\theta}(\tau)}{p_{\theta^{\prime}}(\tau)} R(\tau) \nabla \log p_{\theta}(\tau)\right] \]

\(\theta^{\prime}\)的職責是要去示范給\(\theta\)看。它去跟環境做互動,采樣數據來訓練\(\theta\)。這兩個分布雖然不一樣,但其實沒有關系。假設本來是從\(p\)做采樣,但發現不能從\(p\)做采樣,可以把\(p\)\(q\),在 后面補上一個重要性權重。同理,我們把\(\theta\)換成\(\theta^{\prime}\)后,要補上一個重要性權重 \(\frac{p_{\theta}(\tau)}{p_{\theta^{\prime}}(\tau)}\)。這個重要性權重就是某一個軌跡\(\theta^{\prime}\)\(\theta\)算出來的概率除以這個軌跡\(\tau\)\(\theta^{\prime}\)算出來的概率。

實際在做策略梯度的時候,並不是給整個軌跡\(\theta^{\prime}\)都一樣的分數,而是每一個狀態-動作的對會分開來計算。具體可參考上一篇PG的文章。實際上更新梯度的時候,如下式所示。

\[E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta}}\left[A^{\theta}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right)\right] \]

我們用演員\(\theta\)去采樣出\(s_{t}\)\(a_{t}\) ,采樣出狀態跟動作的對,並計算這個狀態跟動作對的優勢\(A^{\theta}\left(s_{t}, a_{t}\right)\)\(A^{\theta}\left(s_{t}, a_{t}\right)\)就是累積獎勵減掉偏置項,這一項是估測出來的。它要估測的是在狀態\(s_{t}\)采取動作\(a_{t}\) 是好的還是不好的。也就是說如果\(A^{\theta}\left(s_{t}, a_{t}\right)\)是正的,就要增加概率,如果是負的,就要減少概率。 所以現在\(s_{t}\)\(a_{t}\)\(\theta^{\prime}\)跟環境互動以后所采樣到的數據。但是拿來訓練,要調整參數的模型是\(\theta\)。因為\(\theta^{\prime}\)\(\theta\)是不同的模型,所以需要用重要性采樣技術去做修正。即把\(s_{t}\)\(a_{t}\)\(\theta\)采樣出來的概率除掉\(s_{t}\)\(a_{t}\)\(\theta^{\prime}\)采樣出來的概率。公式如下。

\[E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(s_{t}, a_{t}\right)}{p_{\theta^{\prime}}\left(s_{t}, a_{t}\right)} A^{\theta}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right)\right] \]

上式中的\(A^{\theta}\left(s_{t}, a_{t}\right)\)有一個上標\(\theta\),代表說是演員\(\theta\)跟環境互動的時候所計算出來的結果。但實際上從\(\theta\)換到\(\theta^{\prime}\)的時候,\(A^{\theta}\left(s_{t}, a_{t}\right)\)應該改成\(A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\),為什么呢?A這一項是想要估測說在某一個狀態采取某一個動作,接下來會得到累積獎勵的值減掉基線。之前是\(\theta\)在跟環境做互動,所以我們可以觀察到的是\(\theta\)可以得到的獎勵。但是現在是\(\theta^{\prime}\)在跟環境做互動,所以我們得到的這個優勢是根據\(\theta^{\prime}\)所估計出來的優勢。但我們現在先不要管那么多,我們就假設\(A^{\theta}\left(s_{t}, a_{t}\right)\)\(A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\)可能是差不多的。

接下來,我們可以拆解\(p_{\theta}\left(s_{t}, a_{t}\right)\)\(p_{\theta^{\prime}}\left(s_{t}, a_{t}\right)\),即

\[\begin{aligned} p_{\theta}\left(s_{t}, a_{t}\right) &=p_{\theta}\left(a_{t} \mid s_{t}\right) p_{\theta}\left(s_{t}\right) \\ p_{\theta^{\prime}}\left(s_{t}, a_{t}\right) &=p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right) p_{\theta^{\prime}}\left(s_{t}\right) \end{aligned} \]

於是可得公式

\[E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} \frac{p_{\theta}\left(s_{t}\right)}{p_{\theta^{\prime}}\left(s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right)\right] \]

這里需要做一件事情,假設模型是\(\theta\)的時候,我們看到\(s_{t}\)的概率,跟模型是\(\theta^{\prime}\)的時候,看到\(s_{t}\)的概率是差不多的,即\(p_{\theta}\left(s_{t}\right)=p_{\theta^{\prime}}\left(s_{t}\right)\)

為什么可以這樣假設呢?一種直觀的解釋就是\(p_{\theta}\left(s_{t}\right)\)很難算,這一項有一個參數\(\theta\),需要拿\(\theta\)去跟環境做互動,算\(s_{t}\)出現的概率。 尤其是如果輸入是圖片的話,同樣的\(s_{t}\)根本就不會出現第二次。我們根本沒有辦法估這一項,所以就直接無視這個問題。但是\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)很好算,我們有\(\theta\)這個參數,它就是個網絡。我們就把\(s_{t}\)帶進去,\(s_{t}\)就是游戲畫面。 我們有個策略的網絡,輸入狀態\(s_{t}\),它會輸出每一個\(a_{t}\)的概率。所以\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)\)這兩項,我們只要知道\(\theta\)\(\theta^{\prime}\)的參數就可以算。實際上在更新參數 的時候,我們就是按照下式來更新參數。公式如下。

\[E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right)\right] \]

所以實際上,我們可以從梯度去反推原來的目標函數,可以用\(\nabla f(x)=f(x) \nabla \log f(x)\)來反推目標函數。當使用重要性采樣的時候,要去優化的目標函數如下式所示,我們把它記\(J^{\theta^{\prime}}(\theta)\)。括號里面的\(\theta\)代表我們需要去優化的參數。用\(\theta^{\prime}\)去做示范采樣數據,采樣出\(s_{t}\)\(a_{t}\)以后,要去計算\(s_{t}\)\(a_{t}\)的優勢,再乘上 \(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)}\)

\[J^{\theta^{\prime}}(\theta)=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] \]

4. PPO

注意,由於在 PPO 中\(\theta^{\prime}\)\(\theta_{\text {old }}\),即行為策略也是\(\pi_{\theta}\),所以 PPO 是同策略的算法。

上面我們通過重要性采樣把同策略換成異策略,但重要性采樣有一個問題:如果\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)\)差太多的話,即這兩個分布差太多的話,重要性采樣的結果就會不好。那么怎么避免差太多呢?這就是 PPO 在做的事情。

PPO在訓練的時候,多加一個約束項。 這個約束是\(\theta\)\(\theta^{\prime}\)輸出的動作的KL散度,簡單來說,這一項的意思就是要衡量說\(\theta\)\(\theta^{\prime}\)有多像。我們希望在訓練的過程中,學習出來的\(\theta\)\(\theta^{\prime}\)越像越好。因為如果\(\theta\)\(\theta^{\prime}\)不像的話,最 后的結果就會不好。所以在 PPO 里面有兩項:一項是優化本來要優化的東西,另一項是一個約束。這個約束就好像正則化的項一樣,作用是希望最后學習出來的\(\theta\)\(\theta^{\prime}\)盡量不用差太多。PPO算法公式如下。

\[\begin{aligned} J_{\mathrm{PPO}}^{\theta^{\prime}}(\theta) &=J^{\theta^{\prime}}(\theta)-\beta \mathrm{KL}\left(\theta, \theta^{\prime}\right) \\ J^{\theta^{\prime}}(\theta) &=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] \end{aligned} \]

4.1 TRPO

PPO 有一個前身:信任區域策略優化(trust region policy optimization,TRPO),TRPO 的式子如下式所示。

\[\begin{array}{r} J_{\mathrm{TRPO}}^{\theta^{\prime}}(\theta)=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] \\ \mathrm{KL}\left(\theta, \theta^{\prime}\right)<\delta \end{array} \]

TRPO 與 PPO 不一樣的地方是約束項擺的位置不一樣,PPO 是直接把約束放到要優化的式子里,可以直接用梯度上升的方法最大化這個式子。但TRPO是把 KL 散度當作約束,它希望\(\theta\)\(\theta^{\prime}\)的 KL 散度小於一個\(\delta\)。如果我們使用的是基於梯度的優化時,有約束是很難處理的,因為它把 KL 散度約束當做一個額外的約束,沒有放目標里面。PPO 跟 TRPO 的性能差不多,但 PPO 在實現上比 TRPO 容易的多,所以我們一般就用 PPO,而不用TRPO。

4.2 PPO算法的兩個主要變種

(1)近端策略優化懲罰(PPO-penalty)

首先初始化一個策略的參數\(\theta^{0}\)。在每一個迭代 里面,我們要用前一個訓練的迭代得到的演員的參數\(\theta^{k}\)去跟環境做互動,采樣到一大堆狀態-動作的對。 根據\(\theta^{k}\)互動的結果,估測\(A^{\theta^{k}}\left(s_{t}, a_{t}\right)\)。如下式所示。

\[J_{\mathrm{PPO}}^{\theta^{k}}(\theta)=J^{\theta^{k}}(\theta)-\beta \mathrm{KL}\left(\theta, \theta^{k}\right) \]

上述KL散度前需要乘一個權重\(β\),需要一個方法來動態調整\(β\)。 這個方法就是自適應KL懲罰:如果 KL(\(\theta\), \(\theta^{k}\) ) > KLmax,增加\(β\);如果 KL(\(\theta\), \(\theta^{k}\) ) < KLmin,減少 \(β\)。簡單來說就是KL散度的項大於自己設置的KL散度最大值,說明后面這個懲罰的項沒有發揮作用,就把\(β\)調大。同理,如果KL 散度比最小值還要小,這代表后面這一項的效果太強了,所以要減少\(β\)。近端策略優化懲罰公式如下。

\[\begin{aligned} J_{P P O}^{\theta^{k}}(\theta)=J^{\theta^{k}}(\theta)-\beta K L\left(\theta, \theta^{k}\right) & \\ J^{\theta^{k}}(\theta) & \approx \sum_{\left(s_{t}, a_{t}\right)} \frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{k}}\left(s_{t}, a_{t}\right) \end{aligned} \]

(2)近端策略優化裁剪(PPO-clip)

如果你覺得算KL散度很復雜,另外一種PPO變種即近端策略優化裁剪。近端策略優化裁剪要去最大化的目標函數如下式所示,式子里面就沒有 KL 散度。

\[\begin{aligned} J_{\mathrm{PPO} 2}^{\theta^{k}}(\theta) \approx \sum_{\left(s_{t}, a_{t}\right)} \min &\left(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{k}}\left(s_{t}, a_{t}\right)\right.\\ &\left.\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}, 1-\varepsilon, 1+\varepsilon\right) A^{\theta^{k}}\left(s_{t}, a_{t}\right)\right) \end{aligned} \]

上式看起來很復雜,其實很簡單,它想做的事情就是希望\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\),也就是做示范的模型跟實際上學習的模型,在優化以后不要差距太大。

  • 操作符min作用是在第一項和第二項中選擇最小的。

  • 第二項前面有個裁剪(clip)函數,裁剪函數是指:在括號里有三項,如果第一項小於第二項,則輸出1 − ε;如果第一項大於第三項的話,則輸出1 + ε。

  • ε 是一個超參數,要需要我們調整的,一般設置為0.1或0.2 。

舉個栗子,假設設ε=0.2,如下式所示。

\[\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}, 0.8,1.2\right) \]

在上式中,如果\(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\)計算結果小於0.8,則clip函數值就是0.8;如果結果大於1.2,則取1.2。當然,如果介於0.8~1.2之間,則輸入等輸出。

我們詳細看看clip函數到底算的是什么。

圖1. clip函數

橫軸是\(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\),縱軸是裁剪函數的輸出。

圖2. clip函數詳細圖

如圖 2-a 所示, \(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\)是綠色的線;\(\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}, 1-\varepsilon, 1+\varepsilon\right)\)是藍色的線;在綠色的線跟藍色的線中間,我們要取最小值。假設前面乘上的這個項 A,它是大於 0 的話,取最小的結果,就是紅色的這一條線。如圖 2-b 所示,如果 A 小於 0 的話,取最小的以后,就得到紅色的這一條線。

這其實就是控制\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\)在優化以后不要差距太大。具體來說:

如果 A > 0,也就是某一個狀態-動作的對是好的,我們希望增加這個狀態-動作對的概率。也就是想要讓\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)越大越好,但它跟\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\)的比值不可以超過1+ε。如果超過 1 +ε 的話,就沒有好處了。紅色的線就是目標函數,我們希望目標越大越好,也就是希望\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)越大越好。但是\(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\)只要大過 1+ε,就沒有好處了。所以在訓練的時候,當 pθ(at |st) 被 訓練到\(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\)> 1 +ε 時,它就會停止。

假設\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\)還要小,並且這個優勢是正的。因為這個動作是好的,我們希望這個動作被采取的概率越大越好,希望\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)越大越好,那就盡量把它變大,但只要大到 1 + ε 就好。

如果 A < 0,也就是某一個狀態-動作對是不好的,我們希望把\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)減小。如果\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\)還大,那我們就盡量把它壓小,壓到\(\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)}\)是 1 − ε 的時候就停了,就不要再壓得更小。這樣的好處就是不會讓\(p_{\theta}\left(a_{t} \mid s_{t}\right)\)\(p_{\theta^{k}}\left(a_{t} \mid s_{t}\right)\)差距太大,並且實現這個方法也比較簡單。

5. 代碼實現

案例:倒立擺問題。鍾擺以隨機位置開始,目標是將其向上擺動,使其保持直立。 測試環境:Pendulum-v1

動作:往左轉還是往右轉,用力矩來衡量,即力乘以力臂。范圍[-2,2]:(連續空間)

狀態:cos(theta), sin(theta) , thetadot。

獎勵:越直立拿到的獎勵越高,越偏離,獎勵越低。獎勵的最大值為0。

定義網絡結構:

class FeedForwardNN(nn.Module):

	def __init__(self, in_dim, out_dim):
		
		super(FeedForwardNN, self).__init__()

		self.layer1 = nn.Linear(in_dim, 64)
		self.layer2 = nn.Linear(64, 64)
		self.layer3 = nn.Linear(64, out_dim)

	def forward(self, obs):
		
		if isinstance(obs, np.ndarray):
			obs = torch.tensor(obs, dtype=torch.float)

		activation1 = F.relu(self.layer1(obs))
		activation2 = F.relu(self.layer2(activation1))
		output = self.layer3(activation2)

		return output

定義PPO類:

class PPO:

	def __init__(self, policy_class, env, **hyperparameters):

		# PPO 初始化用於訓練的超參數
		self._init_hyperparameters(hyperparameters)

		# 提取環境信息
		self.env = env
		self.obs_dim = env.observation_space.shape[0]
		self.act_dim = env.action_space.shape[0]
        
		# 初始化演員和評論家網絡
		self.actor = policy_class(self.obs_dim, self.act_dim)                                                
		self.critic = policy_class(self.obs_dim, 1)

		# 為演員和評論家初始化優化器
		self.actor_optim = Adam(self.actor.parameters(), lr=self.lr)
		self.critic_optim = Adam(self.critic.parameters(), lr=self.lr)

		# 初始化協方差矩陣,用於查詢actor網絡的action
		self.cov_var = torch.full(size=(self.act_dim,), fill_value=0.5)
		self.cov_mat = torch.diag(self.cov_var)

		# 這個記錄器將幫助我們打印出每個迭代的摘要
		self.logger = {
			'delta_t': time.time_ns(),
			't_so_far': 0,          # 到目前為止的時間步數
			'i_so_far': 0,          # 到目前為止的迭代次數
			'batch_lens': [],       # 批次中的episodic長度
			'batch_rews': [],       # 批次中的rews回報
			'actor_losses': [],     # 當前迭代中演員網絡的損失
		}

	def learn(self, total_timesteps):

		print(f"Learning... Running {self.max_timesteps_per_episode} timesteps per episode, ", end='')
		print(f"{self.timesteps_per_batch} timesteps per batch for a total of {total_timesteps} timesteps")
		t_so_far = 0 # 到目前為止仿真的時間步數
		i_so_far = 0 # 到目前為止,已運行的迭代次數
		while t_so_far < total_timesteps:                                                                  
	
			# 收集批量實驗數據
			batch_obs, batch_acts, batch_log_probs, batch_rtgs, batch_lens = self.rollout()                    

			# 計算收集這一批數據的時間步數
			t_so_far += np.sum(batch_lens)

			# 增加迭代次數
			i_so_far += 1

			# 記錄到目前為止的時間步數和到目前為止的迭代次數
			self.logger['t_so_far'] = t_so_far
			self.logger['i_so_far'] = i_so_far

			# 計算第k次迭代的advantage
			V, _ = self.evaluate(batch_obs, batch_acts)
			A_k = batch_rtgs - V.detach()                                                                 

			# 將優勢歸一化 在理論上不是必須的,但在實踐中,它減少了我們優勢的方差,使收斂更加穩定和快速。
			# 添加這個是因為在沒有這個的情況下,解決一些環境的問題太不穩定了。
			A_k = (A_k - A_k.mean()) / (A_k.std() + 1e-10)
            
			# 在其中更新我們的網絡。
			for _ in range(self.n_updates_per_iteration):  
  
				V, curr_log_probs = self.evaluate(batch_obs, batch_acts)

				# 重要性采樣的權重
				ratios = torch.exp(curr_log_probs - batch_log_probs)

				surr1 = ratios * A_k
				surr2 = torch.clamp(ratios, 1 - self.clip, 1 + self.clip) * A_k

				# 計算兩個網絡的損失。
				actor_loss = (-torch.min(surr1, surr2)).mean()
				critic_loss = nn.MSELoss()(V, batch_rtgs)

				# 計算梯度並對actor網絡進行反向傳播
				# 梯度清零
				self.actor_optim.zero_grad()
				# 反向傳播,產生梯度
				actor_loss.backward(retain_graph=True)
				# 通過梯度下降進行優化
				self.actor_optim.step()

				# 計算梯度並對critic網絡進行反向傳播
				self.critic_optim.zero_grad()
				critic_loss.backward()
				self.critic_optim.step()

				self.logger['actor_losses'].append(actor_loss.detach())
                
			self._log_summary()

			if i_so_far % self.save_freq == 0:
				torch.save(self.actor.state_dict(), './ppo_actor.pth')
				torch.save(self.critic.state_dict(), './ppo_critic.pth')

	def rollout(self):
		"""
			這就是我們從實驗中收集一批數據的地方。由於這是一個on-policy的算法,我們需要在每次迭代行為者/批評者網絡時收集一批新的數據。
		"""
		batch_obs = []
		batch_acts = []
		batch_log_probs = []
		batch_rews = []
		batch_rtgs = []
		batch_lens = []

		# 一回合的數據。追蹤每一回合的獎勵,在回合結束的時候會被清空,開始新的回合。
		ep_rews = []

		# 追蹤到目前為止這批程序我們已經運行了多少個時間段
		t = 0 

		# 繼續實驗,直到我們每批運行超過或等於指定的時間步數
		while t < self.timesteps_per_batch:
			ep_rews = []  每回合收集的獎勵

			# 重置環境
			obs = self.env.reset()
			done = False
            
			# 運行一個回合的最大時間為max_timesteps_per_episode的時間步數
			for ep_t in range(self.max_timesteps_per_episode):
			
				if self.render and (self.logger['i_so_far'] % self.render_every_i == 0) and len(batch_lens) == 0:
					self.env.render()

				# 遞增時間步數,到目前為止已經運行了這批程序
				t += 1

				#  追蹤本批中的觀察結果
				batch_obs.append(obs)

				# 計算action,並在env中執行一次step。
				# 注意,rew是獎勵的簡稱。
				action, log_prob = self.get_action(obs)
				obs, rew, done, _ = self.env.step(action)

				# 追蹤最近的獎勵、action和action的對數概率
				ep_rews.append(rew)
				batch_acts.append(action)
				batch_log_probs.append(log_prob)

				if done:
					break
                    
			# 追蹤本回合的長度和獎勵
			batch_lens.append(ep_t + 1)
			batch_rews.append(ep_rews)

		# 將數據重塑為函數描述中指定形狀的張量,然后返回
		batch_obs = torch.tensor(batch_obs, dtype=torch.float)
		batch_acts = torch.tensor(batch_acts, dtype=torch.float)
		batch_log_probs = torch.tensor(batch_log_probs, dtype=torch.float)
		batch_rtgs = self.compute_rtgs(batch_rews)                                                              

		# 在這批中記錄回合的回報和回合的長度。
		self.logger['batch_rews'] = batch_rews
		self.logger['batch_lens'] = batch_lens

		return batch_obs, batch_acts, batch_log_probs, batch_rtgs, batch_lens

	def compute_rtgs(self, batch_rews):

		batch_rtgs = []
        
		# 遍歷每一回合,一個回合有一批獎勵
		for ep_rews in reversed(batch_rews):
			# 到目前為止的折扣獎勵
			discounted_reward = 0

			# 遍歷這一回合的所有獎勵。我們向后退,以便更順利地計算每一個折現的回報
			for rew in reversed(ep_rews):
                
				discounted_reward = rew + discounted_reward * self.gamma
				batch_rtgs.insert(0, discounted_reward)

		# 將每個回合的折扣獎勵的數據轉換成張量
		batch_rtgs = torch.tensor(batch_rtgs, dtype=torch.float)

		return batch_rtgs

	def get_action(self, obs):
	
		mean = self.actor(obs)

		# 用上述協方差矩陣中的平均行動和標准差創建一個分布。
		dist = MultivariateNormal(mean, self.cov_mat)
		action = dist.sample()
		log_prob = dist.log_prob(action)

		return action.detach().numpy(), log_prob.detach()

	def evaluate(self, batch_obs, batch_acts):
		"""
			估算每個觀察值,以及最近一批actor網絡迭代中的每個action的對數prob。
		"""
        
		# 為每個batch_obs查詢critic網絡的V值。V的形狀應與batch_rtgs相同。
		V = self.critic(batch_obs).squeeze()

		# 使用最近的actor網絡計算批量action的對數概率。
		mean = self.actor(batch_obs)
		dist = MultivariateNormal(mean, self.cov_mat)
		log_probs = dist.log_prob(batch_acts)

		# 返回批次中每個觀察值的值向量V和批次中每個動作的對數概率log_probs
		return V, log_probs

最終的動畫效果如下圖:

訓練結果如下所示:

Average Episodic Length:200

Average Episodic Return:-76.99

Average actor_loss:0.0017

Average value_loss:0.49982

Iteration:10000

6. 總結

PPO其實就是避免在使用重要性采樣時由於在\(\theta\)下的 \(p_{\theta}\left(a_{t} \mid s_{t}\right)\)與在\(\theta^{\prime}\) 下的\(p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)\)差太多,導致重要性采樣結果偏差較大而采取的算法。具體來說就是在訓練的過程中增加一個限制,這個限制對應着\(\theta\)\(\theta^{\prime}\)輸出的動作的 KL 散度,來衡量\(\theta\)\(\theta^{\prime}\)的相似程度。

7. 參考文獻

[1]《Reinforcement+Learning: An+Introduction》

[2] https://medium.com/analytics-vidhya/coding-ppo-from-scratch-with-pytorch-part-1-4-613dfc1b14c8

我們是行者AI,我們在“AI+游戲”中不斷前行。

前往公眾號 【行者AI】,和我們一起探討技術問題吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM