
來源 | 知乎
作者 | SPiriT
說明
最近強化學習的風又有點吹起來了,作為曾經的RL從業者,開始翻一些以前的經典資料,溫故而知新。這篇複習的是22年的經典博文,細緻研究了ppo的一些實現細節,對我當時實現 ppo 給於很大幫助。
https://iclr-blog-track.github.io/2022/03/25/ppo-implementation-details/
注意
-
• 以前的rl都集中在序列決策問題上,即怎麼最大化累計收益的期望,所以背景大都是遊戲和控制相關,例如Atari game, Mujoco等, 與當前(2025年3月)rl在llm中的熱度方向不一致。 -
• 原博文內容也集中以解決“遊戲決策”為背景,同時以演算法實現細節為基礎。 -
• 本文會根據原博內容進行酌情增減
正文
原文主要目的是:復現官方的PPO實現,在github開原始碼中,最終對比選擇了 openai 的 baselines\ppo2[1] 作為復現參考。原因如下:
-
• ppo2 演算法在 Atari 和 MuJoCo 的任務上都表現不錯 -
• 包含了LSTM和處理多維離線空間(MultiDiscrete)的一些高階處理,能處理 RTS 遊戲(實施策略遊戲,例如星際爭霸、王者榮耀這種)
ppo 原論文提出了2個演算法實現,ppo1 是動態縮放kl,而 ppo2 演算法是直接clip,更簡單好用。所以 ppo2 才是現在大家更熟知的 ppo 演算法實現,以下都預設為 ppo2 演算法實現
13個關鍵實現細節
1. 向量化架構
envs = VecEnv(num_envs=N) agent = Agent() next_obs = envs.reset() next_done = [0, 0, ..., 0] # of length N for update in range(1, total_timesteps // (N*M)): data = [] # ROLLOUT PHASE for step in range(0, M): obs = next_obs done = next_done action, other_stuff = agent.get_action(obs) next_obs, reward, next_done, info = envs.step( action ) # step in N environments data.append([obs, action, reward, done, other_stuff]) # store data # LEARNING PHASE agent.learn(data, next_obs, next_done) # `len(data) = N*M`
使用多程序,序列或並行的初始化一個向量化的環境 envs ,包含了 N 個獨立環境。訓練時同時獲得 N 個 obs ,執行 N 個action,最後獲得 N 個 next_obs。其中 next_done 代表各個環境裡每一步有沒有done。
這裡面有2個迴圈
-
• Rollout phase:每個環境裡的 agent 執行 M 步收集資料。 -
• Learning phase:從上面收集好的資料裡學習更新網路引數,資料規模是 N*M 。
向量化環境的優勢很明顯:
-
• 1、併發收集資料更快,學習更快。 -
• 2、各環境獨立保證資料多樣性,學習更穩。
這種併發是基操,但困難也很明顯:
-
• 1、很吃cpu,尤其是動作空間或狀態空間很大的遊戲,而一些重的env有時候很難併發。 -
• 2、learner的消費可能跟不上生產導致資料浪費,所以要實驗配比。
需要理解 next_obs 和 next_done 的角色:done代表是否結束,比如遊戲結束或者被截斷了。當環境done後,next_obs就是下一個階段的第一個觀測。這樣即使一個episode只執行 M 步,但 ppo 依然能在沒法停止或階段的環境裡學習。
上面這個最開始沒看懂,後來才理解這個 reset 可能是 env 裡面的“原地”reset。這個不太符合一些遊戲設定,因為大部分 reset 是直接回到 env 的“起點”,比如王者榮耀ai,在純粹的遊戲設立裡,ai done後肯定是回到老家水晶,而不是在原地復活繼續 battle。但為了實現類似功能,一般都會讓 ai 自由出生,或者從 env 的某個中間狀態 reset(比如半血在中路位置復活),而不是完整受限於遊戲程序,這樣能減少訓練的探索成本。
一個比較普遍的錯誤實現如下。錯誤有兩處:
-
• 1、單個env效率很低。 -
• 2、對於RTS遊戲(一個簡單epsiode至少1M step)記憶體不夠(相比上面,在取樣相同step的資料而言)。
env = Env()agent = Agent()for episode in range(1, num_episodes): next_obs = env.reset() data = [] for step in range(1, max_episode_horizon): obs = next_obs action, other_stuff = agent.get_action(obs) next_obs, reward, done, info = env.step(action) data.append([obs, action, reward, done, other_stuff]) # store data if done: break agent.learn(data)
關於 N 和 M 的取值經驗:在相同的 N*M 資料需求下,並不是 N 越大越好,相反 M 太小會導致探索太短。因為“shortened experience chunks”和“earlier value bootstrapping.”
關於上面說的錯誤實現,和前面“正確實現”的主要區別在於:done後要不要break。前面的實現是一直走M步,但如果中間某步done=True了,後面全部的done都是True,這樣在計算V值時只會計算單步的reward,序列決策的反饋就沒有,資料學習效率不高。不過反過來想,前面的實現或許默認了“done後的step依然生效”這一邏輯,不用管done的值來中斷epsiode,程式碼結構上更統一。更細節是,平時自己寫是 data.append([obs, action, reward, next_obs]),即儲存一個完整的 state transition。這樣能避免上面的問題,但問題是多存了個next_obs對 mem 不友好。
向量化的環境天然支援 MARL (多智慧體強化學習),相容性更強。比如1個環境下初始化 N 個player,這樣 M 步的資料量就變成了在同一環境下 N 個 player 執行得到的所有資料。
2. 網路的權重正交初始化、偏差常數初始化
原始碼實現如下:



上面CNN、MLP裡的正交初始化和 pytroch 自帶的 api 實現不太一致,但對效能沒什麼影響。有一些證據證明orthogonal 相比 Xavier 要好一些。
3. Adam 最佳化器的epsilon引數值
ppo 實現裡面的值是 1e-5 ,這和 pytorch (1e-8) 和tf裡(1e-7)都不一樣,為了保證可復現性保持一致。
4. Adam 學習率退火
Atari 的 lr 是從 2.5e-4 隨著 step 線性衰減到0,Mujoco 任務裡是從 3e-4 線性衰減到 0
5. GAE
gae 的提出要晚於 ppo 演算法,但它現在是個預設實現,在 gamma 取特殊值時(0 和 1),gae 等效 td 和 mc。而且有證據表現:gae 要比 N-step 的 returns 要好(更好的 trade off 近視和遠視)。
6. Mini-batch 更新
對於上面的N*M 資料,一般會先把資料 shuffle,然後用一個小的 batch_size 分批計算梯度更新網路。
一些錯誤實現:
-
• 1、直接用全部資料計算,而不是分mini batch_size -
• 2、隨機選擇 mini-batchs而不是全部學習
這個 mini batch size 是一個比較重要的引數,在訓練初期需要微調來平衡效果和效能。這個原理其實好理解,首先 N*M 資料總量通常是比較大的,比如 N=20,M=2048 等,不可能全部直接計算梯度去更新網路,所以需要min batch size的概念。其次,ppo 是 on-policy 演算法,學習完的資料只能丟棄重新收集,最好能充分利用資料,所以 mini batchs 能讓 ppo “小步快跑“的充分學習,並透過 clip 的方式保證穩定性。最後,一般資料都是序列收集的,時序關聯太強,shuffle 後再 mini batch 對學習更友好
7. advantages 歸一化
gae 計算完後建議歸一化,就是簡單的減去均值除以方差。注意是在 mini_batch 裡做而不是針對整個資料。
因為是對 mini_batch 計算梯度來更新,所以得在 mini_batch 裡用。不過這一細節影響不大。
8. clipped surrogate objective
ppo 論文核心細節,有些任務裡直接的 cliped 和 trpo 演算法最後效果是一樣的,但優勢在 ppo 實現比 trpo 更簡單
9. value loss clipping
對 v_net 的 loss 也進行 clipping,不過一些研究表明這個作用不大。注意這裡是用 t-1和 t 時刻做對比,下面是不同數值clip 的結果:
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

這個 value clipp 以前沒用過,對於為什麼取 max也沒很理解。
從上面表格能看出,只有當 相比 超過 eps 更靠近 時,這個 clip 才會實際生效。這個 max操作變相的增加了原來的 loss。
10.entropy loss
原文裡為了增加策略隨機性,把 policy 的 entropy 也加入 loss 計算,也是常規細節之一。
loss = policy_loss - entropy * entropy_coefficient + value_loss * value_coefficient
11. global gradient clipping
把全域性引數的 l2_norm 截斷到 0.5 內
12. debug變數
ppo 實現裡有很多用於 debug 的變數,除了上面 3 個 loss,還有以下幾個:
-
• clipfrac: 訓練資料裡觸發 clipped 的比例 -
• approxkl: 新舊策略的近似 kl 散度,計算上直接用 -logratio.mean() 。關於 kl 散度近似計算可以看這個博文[2]
這兩個引數更多是初步調參時需要注意的,尤其用於程式碼 debug,如果 clipfrac 太多說明策略更新太快,通常是有 bug
13. policy_net 和 value_net 網路是否共用 backbone
-
• ppo 預設實現是共享網路結構,然後分別出 policy_head 和 value_head. -
• 原博實現了網路分開實現的程式碼,最後實測效果明顯比 shared 的更好
是否共用 backbone 是 RL 一個常見問題,共用的好處是,節省算力減少開銷、共享特徵資訊。劣勢也明顯,兩個loss 的梯度目標可能並不一致。不過在業務上好像都習慣採用 shared 方式,網路結構影響比想象要小。
9個Atari實現細節
atari遊戲實現相關的一些 trick細節,摘取了一些有參考意義的。
-
• NoopResetEnv:在reset的時候隨機進行一定數量的noop操作【0-30】,目的是使遊戲開始時的初始狀態具有一定的變動。 -
• MaxAndSkipEnv:預設跳 4 幀執行,4 幀內執行重複動作,收集 4 幀累計的 reward 作為實際 reward,能加速模型收斂,畢竟有時候 env.step 要比 policy_net(state) 計算更快。並且狀態選擇最後 2 幀的最大值pixel,應對 atari 畫素遊戲裡的怪物特徵閃爍(遊戲裡的閃爍動效)。 -
• EpisodicLifeEnv:有些遊戲裡角色有多條命,只有命數到 0 時遊戲才結束,這裡包裝成只要命數-1就算 done。不過其他論文說這個對最終表現有害 。 -
• FireResetEnv:有些遊戲可能需要固定按鍵作為開始,這裡就修改成 reset 後執行action-1 和action-2再繼續遊戲。不過這個細節好像並沒啥作用而且沒法追溯設計動機? -
• WarpFrame:把 rgb 影像灰度化,然後 resize(210×160 -> 84×84) -
• ClipRewardEnv:根據 reward 的正負全部對映到 {+1, 0, -1},這樣就能一起學不同的遊戲,但這樣也會影響模型在具體遊戲裡一些細緻化操作。 -
• FrameStack:疊幀,預設疊 4 幀,這樣能感知移動物體的速度和朝向。 -
• policy_net 和 vlaue_net 共用網路的 backbone -
• pixel 值歸一化,除以 255。
9個連續動作空間實現細節 [Mujoco]
1. 基於正態分佈的連續動作
pg 類演算法從正態分佈中取樣來得到連續性動作,所以網路任務就是預測分佈的均值和標準差,通常使用“高斯分佈“。
2. 與狀態無關的對數標準差
網路輸出均值的logits,而對於標準差輸出 log std,初始化為0且和狀態無關。
3. 獨立的動作組合
在很多機器人任務中有不同維度的動作輸出,比如向左移動 3.5m和向上運動 4.2m,輸出是 ,為了可微,ppo把它們視為機率獨立的分佈,所以計算動作的機率就是多個動作機率相乘: .
這種設定假設各個維度的動作是完全獨立,滿足全協方差的高斯分佈,但有些任務裡面動作之間是耦合,所以也有文章研究非獨立的動作選擇問題(例如用自迴歸的策略),這是個開放性問題。
4. 分開實現的 policy_net 和 value_net
對於連續控制問題,ppo 通常的網路設計是 64 hidden MLP,中間用 tanh 作為啟用函式,後面再接兩個 head.
和前面討論的細節一樣,實驗證明把網路 backbone 分離設計最後效果要更好
5. 處理動作越界問題
既然動作是從分佈取樣得到,必然會遇到越界問題,比如動作範圍是 [-10,10],結果採用得到 +12。在實際執行時需要把動作截斷在合法範圍內,但儲存原始動作來計算梯度更新網路。
有些研究會在分佈取樣後面加個可逆壓縮函式(如tanh),這樣就能相容動作合法問題。一些研究證明這種方式更好。
6. 觀測歸一化
資料在餵給網路前會對觀測值 obs 進行動態歸一化,減去 running_mean 再除以 running_std .
注意這裡是 moving average normalization
7. 觀測截斷
對 obs歸一化後再截斷在一個固定區間內,比如程式碼裡是 [-10, 10] 之間。對於obs區間很大的環境會有幫助。
8. 獎勵縮放
就是計算折扣後再動態歸一化
def step_wait(self): obs, rews, news, infos = self.venv.step_wait() self.ret = self.ret * self.gamma + rews obs = self._obfilt(obs) if self.ret_rms: self.ret_rms.update(self.ret) rews = np.clip(rews / np.sqrt(self.ret_rms.var + self.epsilon), -self.cliprew, self.cliprew) self.ret[news] = 0. return obs, rews, news, infos
9. 獎勵截斷
同前。但是目前對 reward 的 scaling 和 clipping 是否有效沒有確鑿證據。
5 個 LSTM實現細節
-
• 權重和偏置初始化為 std=1 和 0。 -
• hidden & cell 都初始化為 0 -
• 在 epsiode結尾 reset LSTM的 state -
• 使用時序連續的訓練資料餵給網路,不能和之前一樣 shuffle -
• Reconstruct LSTM states during training
MultiDiscrete動作空間細節
基於前面的多維動作空間獨立的設定,alphastar和 openai five 都使用 MultiDiscrete 來建模動作空間,例如 openai five 就用了 MultiDiscrete([ 30, 4, 189, 81 ] 。所以最後的動作空間維度是:30 × 4 × 189 × 81 = 1, 837, 080 dimensions
4個輔助實現系列
官方 ppo 沒有實現,但是在某些場景下可能有用的 4 個 trick
clip range 退火
比如從 0.2 逐漸退火到 0
並行梯度更新
多程序平行計算梯度,這樣能利用所有可用程序來縮短訓練時間。
策略最佳化提前終止
在 actor的 mini batch 中,spinning up 裡會在 kl 散度大於閾值後提前結束,而不是跑完固定的 epoch 數。程式碼裡面是 target_kl 預設是 0.01,閾值是 1.5 * 0.01。
非法動作 mask
openai five 和 alphastar都用了大量的非法動作 mask,具體實現上就是在 softmax 之前,用規則把invalid_action的 logits 修改成 -inf。 這個論文[3] 證明這種mask 實際是把 invalid action 的梯度置零
logits = torch.where(self.masks, logits, torch.tensor(-1e8).to(device))
invalid_action_mask 是非常常見且有效的 trick,但是mask一般是人為規則寫的,會損失策略的一部分探索性
結論
建議
一些有用的 debug 方法:
-
• seed anything:在復現時候可以觀察 obs 和 act,對齊各類數值,確保實現沒有問題 -
• 檢查 ratio =1:在第一個 epoch 和第一個 mini-batch 更新期間,檢查 ratio是否為 1,此時新舊策略相同所以 ratio應該一直是1 -
• 檢查 KL散度:approx_kl 通常應該保持在 0.02 以下,如果過高說明策略更新太快,通常是有 bug -
• 檢查其他指標:policy loss \ value loss 這些指標,如果是復現程式碼,檢查和官方的是否相近 -
• 經驗法則:檢查 ppo演算法在 breakout 遊戲裡能否拿到 400 分,作者發現大部分 ppo實現倉庫都沒法得到,說明有細節沒有對齊。
對於 ppo 研究者的一些建議
-
• 列舉使用的實現細節。考慮用本文這種方式來列舉他們 -
• 釋出原始碼。開原始碼並確保能執行,建議用 poetry 和 pipenv 來管理鎖定依賴。很多pip install -e .的程式碼倉庫都沒法直接跑。用 docker 管理裝好依賴的映象也是個好辦法 -
• 管理實驗。跟蹤管理指標、超引數、程式碼和其他東西,會節省大量時間。商業軟體如:Weights and Biases[4]、Neptune[5], 開源專案:Aim[6],ClearML[7],Polyaxon[8]. -
• 單檔案實現。如果研究需要很多調整,建議有單個檔案實現他們,這麼做代價是程式碼重複和難重構。但好處是: -
• 更容易總攬全域性,直接看到所有演算法細節,再去看結構化的 rl倉庫就更輕鬆; -
• 開發更快; -
• 更方便效能歸因,直接檢視檔案 diff就行。
非同步ppo更好嗎?
Asynchronous PPO (APPO)能夠增加吞吐量,消除ppo裡面等待環境互動取樣的空閒時間,提高吞吐量和 gpu、cpu利用率。但 這篇論文[9] 論證了對效能有損害,不過最大的問題是:沒有穩定可靠的 appo benchmark 實現。
相反,使用更快的向量化環境來加速 ppo 更靠譜點,用 envpool 做了對比,發現能比之前快 3 倍。在 pong 上的表現甚至能和 impala 對比。
所以引出一個現實考慮:使非同步的 rl 演算法例如 impala,還不如想辦法讓並行環境執行更快。
一些其他研究
-
• 其他選擇:對連續動作使用其他分佈,lstm 不同初始化等等 -
• 值函式最佳化:ppg 探索了 pi_net 和 v_net更新頻率,能否在 ppo裡用prioritized experience replay?
引用連結
[1]
baselines\ppo2:https://github.com/openai/baselines[2]
關於 kl 散度近似計算可以看這個博文:http://joschu.net/blog/kl-approx.html[3]
這個論文:https://arxiv.org/pdf/2006.14171[4]
Weights and Biases:https://wandb.ai/[5]
Neptune:https://neptune.ai/[6]
Aim:https://github.com/aimhubio/aim[7]
ClearML:https://github.com/clearml/clearml[8]
Polyaxon:https://github.com/polyaxon/polyaxon[9]
這篇論文:https://arxiv.org/pdf/1802.01561
技術交流群邀請函
△長按新增小助手
掃描二維碼新增小助手微信
請備註:姓名-學校/公司-研究方向
(如:小張-哈工大-對話系統)
即可申請加入自然語言處理/Pytorch等技術交流群
關於我們
MLNLP 社群是由國內外機器學習與自然語言處理學者聯合構建的民間學術社群,目前已經發展為國內外知名的機器學習與自然語言處理社群,旨在促進機器學習,自然語言處理學術界、產業界和廣大愛好者之間的進步。
社群可以為相關從業者的深造、就業及研究等方面提供開放交流平臺。歡迎大家關注和加入我們。

掃描二維碼新增小助手微信
關於我們
