核心概念解析

簡而言之,強化學習是關於智能體(agent)以及它們如何通過試錯來學習的研究。它將這樣一種理念形式化:對智能體的行為進行獎勵或懲罰,會使它在未來更有可能重複或放棄該行為。

強化學習能做什麼?

強化學習方法近年來在多個領域取得了廣泛的成功。例如:

  • 它被用於教計算機在模擬環境中控制機器。
  • 也能在現實世界中控制機器
  • 它還因創造出在複雜策略遊戲中具有突破性的人工智能而聞名,最著名的是圍棋和 Dota
  • 在大模型的 post-training 過程中被用來調整模型輸出"對齊"人類的喜好

核心概念和術語

強化學習的主要角色是智能體和環境。環境是智能體所生活和交互的世界。在每一步交互中,智能體看到世界狀態的(可能是部分的)觀察,然後決定採取的行動。當智能體對環境採取行動時,環境會發生變化,但環境本身也可能自行變化。

智能體還能從環境中感知到獎勵信號,這是一個告訴它當前世界狀態好壞的數字。智能體的目標是最大化其累積獎勵,稱為回報。強化學習方法是智能體學習實現其目標的行為的方式。

狀態與觀察

狀態$s$是對世界狀態的完整描述。關於世界的信息沒有任何是隱藏在狀態之外的。觀察$o$是對狀態的部分描述,可能會遺漏信息。

在深度強化學習中,我們幾乎總是用實值向量、矩陣或更高階的張量來表示狀態和觀察。例如,視覺觀察可以用其像素值的 RGB 矩陣表示;機器人的狀態可以用其關節角度和速度表示。

當智能體能夠觀察到環境的完整狀態時,我們説環境是完全可觀察的。當智能體只能看到部分觀察時,我們説環境是部分可觀察的。

動作空間

不同的環境允許不同種類的動作。在給定環境中所有有效動作的集合通常稱為動作空間。有些環境,如雅達利(Atari)和圍棋,具有離散的動作空間,其中智能體只有有限數量的移動可用。其他環境,如智能體在物理世界中控制機器人的環境,具有連續的動作空間。在連續空間中,動作是實值向量。

這種區別對深度強化學習中的方法有相當深遠的影響。有些算法家族只能直接應用於一種情況,而要用於另一種情況則必須進行大量修改。

策略

策略是智能體用來決定採取什麼動作的規則。它可以是確定性的,在這種情況下通常用$\mu$表示:

$a_t = \mu(s_t)$

或者它可能是隨機的,在這種情況下通常用$\pi$表示:

$a_t \sim \pi(\cdot | s_t)$

因為策略本質上是智能體的 “大腦”,用 “策略” 代替 “智能體” 這個詞並不少見,例如説 “策略正試圖最大化獎勵”。

在深度強化學習中,我們處理參數化的策略:其輸出是可計算函數的策略,這些函數依賴於一組參數(例如神經網絡的權重和偏置),我們可以通過一些優化算法來調整這些參數以改變行為。

我們通常用$\theta$或$\phi$表示這種策略的參數,然後將其作為下標寫在策略符號上以突出這種聯繫:

$a_t = \mu_\theta(s_t)$ ,$a_t \sim \pi_\theta(\cdot | s_t)$

確定性策略

示例:確定性策略

這是一個使用torch.nn包在 PyTorch 中為連續動作空間構建簡單確定性策略的代碼片段:

pi_net = nn.Sequential(
              nn.Linear(obs_dim, 64),
              nn.Tanh(),
              nn.Linear(64, 64),
              nn.Tanh(),
              nn.Linear(64, act_dim)
            )

這構建了一個多層感知器(MLP)網絡,具有兩個大小為 64 的隱藏層和 tanh 激活函數。如果obs是包含一批觀察的 Numpy 數組,則可以使用pi_net獲得一批動作,如下所示:

obs_tensor = torch.as_tensor(obs, dtype=torch.float32)


actions = pi_net(obs_tensor)
隨機策略

深度強化學習中最常見的兩種隨機策略是分類策略和對角高斯策略。

分類策略可用於離散動作空間,而對角高斯策略用於連續動作空間。

使用和訓練隨機策略有兩個關鍵計算至關重要:

  • 從策略中採樣動作,
  • 計算特定動作的對數似然,$\log \pi_\theta(a|s)$。

下面,我們將描述如何對分類策略和對角高斯策略進行這些計算。

分類策略

分類策略就像對離散動作的分類器。你為分類策略構建神經網絡的方式與為分類器構建神經網絡的方式相同:輸入是觀察,然後有一個最終的線性層,給出每個動作的 logits。

採樣:給定每個動作的概率,像 PyTorch 和 Tensorflow 這樣的框架都有內置的採樣工具。例如,請參見 PyTorch 中分類分佈的文檔,torch.multinomialtf.distributions.Categoricaltf.multinomial

對數似然:將最後一層的概率表示為$P_\theta(s)$。它是一個向量,其條目數量與動作數量相同,因此我們可以將動作視為該向量的索引。動作$a$的對數似然可以通過索引到該向量中獲得:

$\log \pi_\theta(a|s) = \log [P_\theta(s)_a]$

對角高斯策略

多元高斯分佈(或者如果你願意,也可以叫多元正態分佈)由均值向量$\mu$和協方差矩陣$\Sigma$描述。對角高斯分佈是協方差矩陣僅在對角線上有條目的特殊情況。因此,我們可以用一個向量來表示它。

對角高斯策略總是有一個神經網絡,將觀察映射到平均動作$\mu_\theta(s)$。協方差矩陣通常有兩種不同的表示方式。

第一種方式:有一個單一的對數標準差向量$\log \sigma$,它不是狀態的函數:$\log \sigma$是獨立的參數。(你應該知道:我們對 VPG、TRPO 和 PPO 的實現就是這樣做的。)

第二種方式:有一個神經網絡將狀態映射到對數標準差$\log \sigma_\theta(s)$。它可以選擇與均值網絡共享一些層。

請注意,在這兩種情況下,我們輸出的是對數標準差,而不是直接輸出標準差。這是因為對數標準差可以取$(-\infty, \infty)$中的任何值,而標準差必須是非負的。如果你不必強制執行這類約束,訓練參數會更容易。通過對對數標準差取指數,可以立即得到標準差,因此我們以這種方式表示它們不會丟失任何信息。

採樣:給定平均動作$\mu_\theta(s)$和標準差$\sigma_\theta(s)$,以及來自球形高斯的噪聲向量$z$($z \sim \mathcal{N}(0, I)$),動作樣本可以通過以下方式計算:

$a = \mu_\theta(s) + \sigma_\theta(s) \odot z$

其中$\odot$表示兩個向量的元素相乘。標準框架有內置的生成噪聲向量的方法,如torch.normaltf.random_normal。或者,你可以構建分佈對象,例如通過torch.distributions.Normaltf.distributions.Normal,並使用它們生成樣本。(後一種方法的優點是這些對象也可以為你計算對數似然。)

對數似然:對於具有均值$\mu = \mu_\theta(s)$和標準差$\sigma = \sigma_\theta(s)$的對角高斯分佈,$k$維動作$a$的對數似然由下式給出:

$\log \pi_\theta(a|s) = -\frac{1}{2} \left( \sum_{i=1}^k \left( \frac{(a_i - \mu_i)2}{\sigma_i2} + 2 \log \sigma_i \right) + k \log 2\pi \right)$

軌跡

軌跡$\tau$是世界中狀態和動作的序列:

$\tau = s_0, a_0, s_1, a_1, \ldots, s_T, a_T$

世界的第一個狀態$s_0$是從起始狀態分佈中隨機採樣的,有時用$\rho_0$表示:

$s_0 \sim \rho_0(\cdot)$

狀態轉換(在時間$t$的狀態$s_t$和時間$t+1$的狀態$s_{t+1}$之間世界發生的情況)由環境的自然規律支配,並且僅取決於最近的動作$a_t$。它們可以是確定性的:

$s_{t+1} = f(s_t, a_t)$

或者是隨機的:

$s_{t+1} \sim P(\cdot | s_t, a_t)$

動作來自智能體,根據其策略。

你應該知道

軌跡也經常被稱為情節(episodes)或展開(rollouts)。

獎勵與回報

獎勵函數$R$在強化學習中至關重要。它取決於世界的當前狀態、剛剛採取的動作和世界的下一個狀態:

$r_t = R(s_t, a_t, s_{t+1})$

儘管通常這會簡化為僅依賴於當前狀態$r_t = R(s_t)$,或狀態 - 動作對$r_t = R(s_t, a_t)$。

智能體的目標是最大化軌跡上的某種累積獎勵,但這實際上可能有幾種含義。我們將用$R(\tau)$表示所有這些情況,從上下文應該可以清楚地知道指的是哪種情況,或者它可能並不重要(因為相同的方程適用於所有情況)。

一種回報是有限 horizon 無折扣回報,它只是在固定步驟窗口中獲得的獎勵總和:

$R(\tau) = \sum_{t=0}^T r_t$

另一種回報是無限 horizon 折扣回報,它是智能體獲得的所有獎勵的總和,但會按它們在未來出現的距離進行折扣。這種獎勵表述包括一個折扣因子$\gamma \in (0, 1)$:

$R(\tau) = \sum_{t=0}^\infty \gamma^t r_t$

但是,我們為什麼會想要折扣因子呢?我們不就是想要獲得所有獎勵嗎?我們確實是,但折扣因子在直觀上很有吸引力,並且在數學上很方便。從直觀層面來説:現在的現金比以後的現金好。從數學層面來説:無限 horizon 的獎勵總和可能不會收斂到一個有限值,並且在方程中很難處理。但是,在合理的條件下,有了折扣因子,這個無限總和會收斂。

強化學習問題

無論選擇哪種回報度量(無論是無限 horizon 折扣回報,還是有限 horizon 無折扣回報),也無論選擇哪種策略,強化學習的目標都是選擇一個策略,當智能體按照該策略行動時,能最大化期望回報。

要談論期望回報,我們首先必須談論軌跡上的概率分佈。

假設環境轉換和策略都是隨機的。在這種情況下,$T$步軌跡的概率是:

$P(\tau | \pi) = \rho_0(s_0) \prod_{t=0}^T P(s_{t+1} | s_t, a_t) \pi(a_t | s_t)$

(無論哪種度量的)期望回報,記為$J(\pi)$,則是:

$J(\pi) = \int P(\tau | \pi) R(\tau) d\tau = \mathbb{E}_{\tau \sim \pi}[R(\tau)]$

強化學習中的核心優化問題可以表示為:

$\pi^* = \arg \max_\pi J(\pi)$

其中$\pi^*$是最優策略。

價值函數

知道一個狀態或狀態 - 動作對的價值通常是有用的。這裏的價值是指如果你從該狀態或狀態 - 動作對開始,然後永遠按照特定策略行動,所獲得的期望回報。幾乎所有強化學習算法都以這樣或那樣的方式使用價值函數。

這裏有四個主要的值得注意的函數。

在策略價值函數$V^\pi(s)$,它給出如果你從狀態$s$開始並始終按照策略$\pi$行動的期望回報:

$V^\pi(s) = \mathbb{E}_\pi[R(\tau) | s_0 = s]$

在策略動作價值函數$Q^\pi(s, a)$,它給出如果你從狀態$s$開始,採取任意動作$a$(可能不是來自該策略),然後永遠按照策略$\pi$行動的期望回報:

$Q^\pi(s, a) = \mathbb{E}_\pi[R(\tau) | s_0 = s, a_0 = a]$

最優價值函數$V^*(s)$,它給出如果你從狀態$s$開始並始終按照環境中的最優策略行動的期望回報:

$V^*(s) = \max_\pi \mathbb{E}_\pi[R(\tau) | s_0 = s]$

最優動作價值函數$Q^*(s, a)$,它給出如果你從狀態$s$開始,採取任意動作$a$,然後永遠按照環境中的最優策略行動的期望回報:

$Q^*(s, a) = \max_\pi \mathbb{E}_\pi[R(\tau) | s_0 = s, a_0 = a]$

價值函數和動作價值函數之間有兩個關鍵的聯繫經常出現:

$V^\pi(s) = \mathbb{E}_{a \sim \pi}[Q^\pi(s, a)]$

$V^(s) = \max_a Q^(s, a)$

這些關係直接來自剛才給出的定義:你能證明它們嗎?

最優 Q 函數與最優動作

最優動作價值函數$Q^(s, a)$和最優策略選擇的動作之間有一個重要的聯繫。根據定義,$Q^(s, a)$給出從狀態$s$開始,採取(任意)動作$a$,然後永遠按照最優策略行動的期望回報。

狀態$s$下的最優策略將選擇能最大化從狀態$s$開始的期望回報的動作。因此,如果我們有$Q*$,我們可以通過以下方式直接獲得最優動作$a*(s)$:

$a^(s) = \arg \max_a Q^(s, a)$

注意:可能有多個動作能最大化$Q^*(s, a)$,在這種情況下,所有這些動作都是最優的,最優策略可以隨機選擇其中任何一個。但總會有一個確定性地選擇動作的最優策略。

貝爾曼方程

所有四個價值函數都遵循稱為貝爾曼方程的特殊自洽方程。貝爾曼方程背後的基本思想是:

你的起點的價值是你期望從那裏獲得的獎勵,加上你接下來到達的地方的價值。

在策略價值函數的貝爾曼方程是:

$V^\pi(s) = \mathbb{E}_{a \sim \pi, s' \sim P}[r(s, a) + \gamma V^\pi(s')]$

$Q^\pi(s, a) = \mathbb{E}{s' \sim P}[r(s, a) + \gamma \mathbb{E}[Q^\pi(s', a')]]$

其中$s' \sim P$是$s' \sim P(\cdot | s, a)$的簡寫,表示下一個狀態$s'$是從環境的轉換規則中採樣的;$a \sim \pi$是$a \sim \pi(\cdot | s)$的簡寫;$a' \sim \pi$是$a' \sim \pi(\cdot | s')$的簡寫

強化學習算法種類介紹

強化學習算法的分類

需要先聲明的是:為現代強化學習領域的算法繪製一個準確且包羅萬象的分類是相當困難的,因為算法的模塊性很難用樹狀結構來表示。此外,為了使內容能在一頁內呈現且在介紹性文章中易於理解,我們不得不省略相當多更高級的內容(如探索、遷移學習、元學習等)。儘管如此,我們的目標是:

  • 強調深度強化學習算法在學習內容和學習方式上最基本的設計選擇;
  • 揭示這些選擇中的權衡;
  • 將一些著名的現代算法置於這些選擇的背景下進行闡述。

無模型(Model-Free)與有模型(Model-Based)強化學習

強化學習算法中一個最重要的分支點是智能體是否能夠訪問(或學習)環境模型。環境模型指的是一個能夠預測狀態轉換和獎勵的函數。

擁有模型的主要優勢在於,它允許智能體通過前瞻性思考進行規劃,查看一系列可能選擇的結果,並明確地在這些選項中做出決策。然後,智能體可以將前瞻性規劃的結果提煉為一個學習到的策略。一個特別著名的例子是 AlphaZero。當這種方法奏效時,與不使用模型的方法相比,它能顯著提高樣本效率。

主要的缺點是,智能體通常無法獲得環境的真實模型。在這種情況下,如果智能體想要使用模型,就必須純粹從經驗中學習模型,這會帶來幾個挑戰。最大的挑戰是模型中的偏差可能會被智能體利用,導致智能體在學習到的模型上表現良好,但在真實環境中卻表現不佳(或者非常糟糕)。模型學習本質上是困難的,所以即使投入大量的時間和計算資源,也可能無法取得成效。

使用模型的算法稱為有模型方法,不使用模型的算法稱為無模型方法。雖然無模型方法放棄了使用模型可能帶來的樣本效率提升,但它們往往更易於實現和調優。在撰寫本介紹時(2018 年 9 月),無模型方法比有模型方法更受歡迎,並且得到了更廣泛的開發和測試。

學習內容

強化學習算法另一個關鍵的分支點是學習內容的問題。常見的學習內容包括:

  • 策略,無論是隨機的還是確定性的;
  • 動作價值函數(Q 函數);
  • 價值函數;
  • 和 / 或環境模型。
無模型強化學習中的學習內容

在無模型強化學習中,表示和訓練智能體主要有兩種方法:

  1. 策略優化(Policy Optimization):這類方法將策略明確表示為$\pi_\theta(a|s)$。它們通過對性能目標$J(\pi)$進行梯度上升直接優化參數$\theta$,或者通過最大化$J(\pi)$的局部近似來間接優化。這種優化幾乎總是在線策略(on-policy)的,這意味着每次更新只使用在按照最新版本的策略行動時收集的數據。策略優化通常還包括學習在線策略價值函數$V^\pi(s)$的近似器$V_\phi(s)$,它用於確定如何更新策略。

一些策略優化方法的例子有:

  • A2C/A3C,它通過梯度上升直接最大化性能;
  • PPO,其更新通過最大化一個替代目標函數來間接最大化性能,該替代目標函數對$J(\pi)$因更新而產生的變化提供了一個保守估計。
  1. Q 學習(Q-Learning):這類方法學習最優動作價值函數$Q^(s,a)$的近似器$Q_\theta(s,a)$。通常,它們使用基於貝爾曼方程的目標函數。這種優化幾乎總是離線策略(off-policy)的,這意味着每次更新可以使用訓練過程中任何時候收集的數據,無論數據是智能體在探索環境時如何選擇動作收集的。相應的策略通過 Q 和$\pi^$之間的聯繫獲得:Q 學習智能體採取的動作由$a(s)=\arg\max_a Q_\theta(s,a)$給出。

Q 學習方法的例子包括:

  • DQN,一個經典的方法,極大地推動了深度強化學習領域的發展;
  • C51,一種變體,它學習回報的分佈,其期望為 Q。
  1. 策略優化與 Q 學習之間的權衡:策略優化方法的主要優勢在於其原則性,即直接優化我們想要的目標。這往往使它們穩定且可靠。相比之下,Q 學習方法只是通過訓練$Q_\theta$滿足自洽方程來間接優化智能體性能。這種學習方式有很多失敗模式,因此往往不太穩定 [1]。但是,當 Q 學習方法奏效時,它們具有顯著更高的樣本效率,因為與策略優化技術相比,它們能更有效地重用數據。
  2. 策略優化與 Q 學習之間的結合:幸運的是,策略優化和 Q 學習並非互不相容(在某些情況下,事實證明它們是等價的),並且存在一系列介於這兩個極端之間的算法。處於這個範圍內的算法能夠謹慎地權衡雙方的優缺點。例子包括:
  • DDPG,一種同時學習確定性策略和 Q 函數的算法,通過相互利用來改進彼此;
  • SAC,一種變體,它使用隨機策略、熵正則化和其他一些技巧來穩定學習,並在標準基準測試中取得比 DDPG 更高的分數。
有模型強化學習中的學習內容

與無模型強化學習不同,有模型強化學習沒有少量易於定義的方法集羣:使用模型的方式有很多正交的方式。我們將給出一些例子,但這個列表遠非詳盡無遺。在每種情況下,模型既可以是給定的,也可以是學習到的。

MBMF 的研究探索了將學習到的環境模型與 MPC 結合在一些深度強化學習的標準基準任務上的應用。

專家迭代(Expert Iteration):純規劃的一個直接後續方法涉及使用和學習策略$\pi_\theta(a|s)$的明確表示。智能體在模型中使用規劃算法(如蒙特卡洛樹搜索),通過從當前策略中採樣來生成計劃的候選動作。規劃算法產生的動作比單獨的策略產生的動作更好,因此相對於該策略而言,它是一個 “專家”。之後,策略會被更新以產生更接近規劃算法輸出的動作。

ExIt 算法使用這種方法來訓練深度神經網絡玩 Hex 遊戲。AlphaZero 是這種方法的另一個例子。

  • 無模型方法的數據增強(Data Augmentation for Model-Free Methods):使用無模型強化學習算法來訓練策略或 Q 函數,但要麼 1)在更新智能體時用虛構經驗增強真實經驗,要麼 2)僅使用虛構經驗來更新智能體。

MBVE 是用虛構經驗增強真實經驗的一個例子。World Models 是僅使用虛構經驗來訓練智能體的一個例子,他們稱之為 “在夢中訓練”。

  • 將規劃循環嵌入策略(Embedding Planning Loops into Policies):另一種方法將規劃過程直接作為子程序嵌入策略中,以便完整的計劃成為策略的輔助信息,同時使用任何標準的無模型算法訓練策略的輸出。這個框架中的關鍵概念是,策略可以學習選擇如何以及何時使用這些計劃。這減少了模型偏差帶來的問題,因為如果模型在某些狀態下不適合規劃,策略可以簡單地學習忽略它。

策略優化入門:從理論到實現

在強化學習領域,策略優化算法是一類重要的方法,它們直接對策略進行參數化並通過梯度上升來最大化預期回報。本文將深入探討策略優化算法的數學基礎,並結合代碼示例進行講解。我們將涵蓋策略梯度理論中的三個關鍵成果:描述策略性能相對於策略參數梯度的最簡單方程、允許從表達式中刪除無用項的規則,以及允許向表達式中添加有用項的規則。最後,我們將把這些結果結合起來,描述基於優勢的策略梯度表達式 —— 這也是我們在 Vanilla Policy Gradient(香草策略梯度)實現中使用的版本。

最簡單策略梯度的推導

這裏,我們考慮一個隨機的、參數化的策略$\pi_{\theta}$。我們的目標是最大化預期回報$J(\pi_{\theta}) = \mathbb{E}[R(\tau)]$,其中$\tau$是一條軌跡。為了便於推導,我們將$R(\tau)$視為有限 horizon 無折扣回報,但對於無限 horizon 折扣回報的情況,推導過程幾乎相同。

我們希望通過梯度上升來優化策略,即:

$\theta_{k+1} = \theta_k + \alpha \nabla_{\theta} J(\pi_{\theta})|_{\theta_k}$

策略性能的梯度$\nabla_{\theta} J(\pi_{\theta})$被稱為策略梯度,使用這種方式優化策略的算法被稱為策略梯度算法(例如 Vanilla Policy Gradient 和 TRPO。PPO 通常也被稱為策略梯度算法,儘管這並不完全準確)。

要實際使用該算法,我們需要一個可以數值計算的策略梯度表達式。這涉及兩個步驟:1)推導策略性能的解析梯度,結果表明它具有期望值的形式;2)形成該期望值的樣本估計,這可以通過有限數量的智能體 - 環境交互步驟的數據來計算。

在本小節中,我們將找到該表達式的最簡單形式。在後面的小節中,我們將展示如何改進這個最簡單的形式,以得到我們在標準策略梯度實現中實際使用的版本。

我們首先列出一些對推導解析梯度有用的事實:

  1. 軌跡的概率:在動作來自策略$\pi_{\theta}$的情況下,軌跡$\tau = (s_0, a_0, ..., s_{T+1})$的概率為:
    $P(\tau|\theta) = \rho_0(s_0) \prod_{t=0}^{T} P(s_{t+1}|s_t, a_t) \pi_{\theta}(a_t|s_t)$
  2. 對數導數技巧:對數導數技巧基於微積分中的一個簡單規則:$log(v)$對$x$的導數是$\frac{1}{v} \frac{dv}{dx}$。重新排列並結合鏈式法則,我們得到:
    $\nabla_{\theta} P(\tau|\theta) = P(\tau|\theta) \nabla_{\theta} log P(\tau|\theta)$
  3. 軌跡的對數概率:軌跡的對數概率為:
    $log P(\tau|\theta) = log \rho_0(s_0) + \sum_{t=0}^{T} (log P(s_{t+1}|s_t, a_t) + log \pi_{\theta}(a_t|s_t))$
  4. 環境函數的梯度:環境與參數$\theta$無關,因此$\rho_0(s_0)$、$P(s_{t+1}|s_t, a_t)$和$R(\tau)$的梯度都為零。
  5. 軌跡對數概率的梯度:因此,軌跡對數概率的梯度為:
    $\nabla_{\theta} log P(\tau|\theta) = \sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t)$

綜合以上所有內容,我們可以推導出:

基本策略梯度的推導

$\begin{aligned}
\nabla_{\theta} J(\pi_{\theta}) &= \nabla_{\theta} \mathbb{E}{\tau \sim \pi{\theta}}[R(\tau)] \
&= \nabla_{\theta} \int_{\tau} P(\tau|\theta) R(\tau) d\tau \
&= \int_{\tau} \nabla_{\theta} P(\tau|\theta) R(\tau) d\tau \
&= \int_{\tau} P(\tau|\theta) \nabla_{\theta} log P(\tau|\theta) R(\tau) d\tau \
&= \mathbb{E}{\tau \sim \pi{\theta}}[\nabla_{\theta} log P(\tau|\theta) R(\tau)] \
&= \mathbb{E}{\tau \sim \pi{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) R(\tau)]
\end{aligned}$

這是一個期望值,這意味着我們可以用樣本均值來估計它。如果我們收集一組軌跡$\mathcal{D} = {\tau_i}{i=1..N}$,其中每條軌跡都是通過讓智能體使用策略$\pi$在環境中行動而獲得的,那麼策略梯度可以估計為:

$\hat{g} = \frac{1}{N} \sum_{i=1}^{N} \sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_{i,t}|s_{i,t}) R(\tau_i)$

這個最後的表達式是我們所期望的可計算表達式的最簡單版本。假設我們已經以一種允許我們計算$\nabla_{\theta} log \pi_{\theta}(a|s)$的方式表示了我們的策略,並且如果我們能夠運行該策略在環境中收集軌跡數據集,我們就可以計算策略梯度並進行更新步驟。

實現最簡單的策略梯度

預期梯度 - 對數概率引理(Expected Grad-Log-Prob Lemma)

在本小節中,我們將推導一個在策略梯度理論中廣泛使用的中間結果。我們將其稱為預期梯度 - 對數概率(EGLP)

EGLP 引理:假設$P_{\theta}$是一個關於隨機變量$x$的參數化概率分佈。那麼:

$\mathbb{E}{x \sim P{\theta}}[\nabla_{\theta} log P_{\theta}(x)] = 0$

證明

回想所有概率分佈都是歸一化的:

$\int P_{\theta}(x) dx = 1$

對兩邊取梯度:

$\nabla_{\theta} \int P_{\theta}(x) dx = \nabla_{\theta} 1 = 0$

使用對數導數技巧:

$0 = \int \nabla_{\theta} P_{\theta}(x) dx = \int P_{\theta}(x) \nabla_{\theta} log P_{\theta}(x) dx = \mathbb{E}{x \sim P{\theta}}[\nabla_{\theta} log P_{\theta}(x)]$

不要讓過去幹擾你

檢查我們最近得到的策略梯度表達式:

$\nabla_{\theta} J(\pi_{\theta}) = \mathbb{E}{\tau \sim \pi{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) R(\tau)]$

用這個梯度進行更新會按比例增加每個動作的對數概率,比例為$R(\tau)$,即所有獲得的獎勵之和。但這並不太合理。

智能體實際上應該只根據動作的後果來強化動作。在採取動作之前獲得的獎勵與該動作的好壞無關:只有之後的獎勵才有關係。

事實證明,這種直覺在數學中也有所體現,我們可以證明策略梯度也可以表示為:

$\nabla_{\theta} J(\pi_{\theta}) = \mathbb{E}{\tau \sim \pi{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})]$

在這種形式中,動作只根據其採取後獲得的獎勵進行強化。

我們將這種形式稱為 “reward-to-go 策略梯度”,因為軌跡中某一點之後的獎勵之和,即

$R_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})$

被稱為從該點的 reward-to-go,並且這個策略梯度表達式依賴於狀態 - 動作對的回報reward-to-go。

策略梯度中的基線(Baselines in Policy Gradients)

預期梯度對數概率引理(EGLP 引理)有一個直接的推論:對於任何僅依賴於狀態的函數$b$,都有:

$
\mathbb{E}{\pi{\theta}} \left[ \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) b(s_t) \right] = 0
$

這意味着我們可以在策略梯度的表達式中隨意添加或減去這類項,而不會改變其期望值。基於此,策略梯度可以表示為:

$
\nabla_{\theta} J(\pi_{\theta}) = \underset{\tau \sim \pi_{\theta}}{\mathbb{E}} \left[ \sum_{t=0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \left( \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t) \right) \right]
$

在這裏,任何以這種方式使用的函數$b$都被稱為基線(baseline)。

最常用的基線是在策略價值函數$V^{\pi}(s_t)$。它表示智能體從狀態$s_t$開始,之後按照策略$\pi$行動所獲得的平均回報。

從經驗來看,選擇$b(s_t) = V^{\pi}(s_t)$能夠有效降低策略梯度樣本估計的方差,從而使策略學習更快、更穩定。從概念上講,這也很合理:如果智能體獲得了預期的回報,它應該對此保持 “中立” 態度。

需要注意的是,在實際應用中,$V^{\pi}(s_t)$無法精確計算,因此必須進行近似。通常會使用神經網絡$V_{\phi}(s_t)$來近似,並且該神經網絡會與策略同時更新(以確保價值網絡始終近似最新策略的價值函數)。

大多數策略優化算法(包括 VPG、TRPO、PPO 和 A2C)中,學習$V_{\phi}$的最簡單方法是最小化均方誤差目標:

$
\phi_k = \arg \min_{\phi} \mathbb{E}{s_t, R \sim \pi_k} \left( V(s_t) - R \right)^2
$

其中$\pi_k$是第$k$個週期的策略。這通常通過從之前的價值參數$\phi_{k-1}$開始,執行一步或多步梯度下降來實現。

策略梯度的其他形式

到目前為止,我們已經瞭解到策略梯度的一般形式為:

$
\nabla_{\theta} J(\pi_{\theta}) = \underset{\tau \sim \pi_{\theta}}{\mathbb{E}} \left[ \sum_{t=0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \Phi_t \right]
$

其中$\Phi_t$可以是以下幾種形式之一:

  • $\Phi_t = R(\tau)$(整個軌跡的回報)
  • $\Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})$(從時刻$t$開始的回報總和,即回報到 - go)
  • $\Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t)$(減去基線後的回報到 - go)

這些選擇雖然會導致策略梯度的樣本估計具有不同的方差,但它們的期望值是相同的。此外,還有另外兩種重要的$\Phi_t$選擇值得了解:

  1. 在策略動作價值函數(On-Policy Action-Value Function):選擇$\Phi_t = Q^{\pi}(s_t, a_t)$也是有效的。有關這一結論的證明(可選)可以參考相關頁面。
  2. 優勢函數(Advantage Function):動作的優勢定義為$A^{\pi}(s_t, a_t) = Q^{\pi}(s_t, a_t) - V^{\pi}(s_t)$,它描述了相對於當前策略下的平均水平,該動作是更好還是更差。選擇$\Phi_t = A^{\pi}(s_t, a_t)$同樣有效。這是因為它等價於使用$\Phi_t = Q^{\pi}(s_t, a_t)$然後再減去一個價值函數基線,而我們知道添加或減去基線是被允許的。

基於優勢函數的策略梯度公式應用極為廣泛,不同的算法會採用多種不同的方法來估計優勢函數。

Vanilla Policy Gradient:策略梯度方法的基礎標杆

在強化學習的策略梯度(Policy Gradients)家族中,Vanilla Policy Gradient(VPG) 扮演着至關重要的基礎角色。它是理解更復雜策略梯度算法(如 PPO、A2C 等)的基石,其核心思想簡潔而深刻:通過提升帶來高回報的動作的概率,降低帶來低迴報的動作的概率,逐步優化策略直至達到最優。

作為最經典的策略梯度方法之一,VPG 為後續更先進的算法提供了關鍵的理論和實踐基礎。許多高級策略梯度算法本質上都是在 VPG 的框架上進行改進,例如通過引入剪輯機制(如 PPO)來穩定訓練,或通過多線程並行化(如 A2C)來提高數據效率。因此,深入理解 VPG 的原理、特性和實現細節,是掌握強化學習中策略梯度方法的必要前提。

VPG 的核心特性

1. 在線策略(On-policy)特性

VPG 是一種在線策略算法,這意味着它必須基於當前正在訓練的策略所收集的數據來更新策略參數。每次策略更新後,之前收集的數據就會被丟棄,需要用新的策略重新與環境交互以獲取新數據。這種特性雖然在數據利用效率上不如離線策略(Off-policy)算法,但保證了訓練過程的穩定性,因為更新始終基於最新的策略行為。

2. 適用的動作空間

VPG 具有良好的通用性,既可以應用於離散動作空間,也可以應用於連續動作空間。在離散動作空間中,策略可以直接輸出每個動作的概率分佈;而在連續動作空間中,策略通常輸出高斯分佈等連續概率分佈的參數(如均值和標準差),通過採樣來選擇動作。這種靈活性使得 VPG 能夠應對多種不同類型的強化學習任務,從簡單的遊戲(如 Atari 系列)到複雜的機器人控制問題。

3. 並行化支持

OpenAI Spinning Up 提供的 VPG 實現支持基於 MPI 的並行化。通過多進程並行與環境交互,可以在相同時間內收集更多的軌跡數據,從而加速訓練過程並提高策略的穩定性。這種並行化能力對於需要大量交互數據的複雜環境尤為重要。

核心公式解析

VPG 的數學基礎圍繞策略性能的梯度展開,核心是如何計算策略目標函數的梯度並進行優化。

策略目標函數的梯度

設$\pi_\theta$為參數化的策略($\theta$為參數),$J(\pi_\theta)$為該策略的期望有限 horizon 無折扣回報。則$J(\pi_\theta)$的梯度可表示為:

$\nabla_\theta J(\pi_\theta) = \underset{\tau \sim \pi_\theta}{\mathbb{E}} \left[ \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \hat{A}_t \right]$

其中:

  • $\tau$表示一條軌跡(trajectory),即狀態、動作、獎勵的序列;
  • $\hat{A}_t$為當前策略的優勢函數估計,用於衡量在狀態$s_t$下執行動作$a_t$相對於平均水平的優勢;
  • 期望$\underset{\tau \sim \pi_\theta}{\mathbb{E}}$表示對所有由策略$\pi_\theta$生成的軌跡取平均。

策略更新規則

VPG 採用隨機梯度上升來更新策略參數,以最大化目標函數$J(\pi_\theta)$。更新公式為:

$\theta_{k+1} = \theta_k + \alpha \nabla_\theta J(\pi_{\theta_k})$

其中$\alpha$為學習率,控制每次參數更新的步長。

需要注意的是,儘管上述公式基於有限 horizon 無折扣回報,但在實際實現中,策略梯度方法通常使用無限 horizon 折扣回報來計算優勢函數估計,以更好地平衡短期和長期回報。

探索與利用的平衡

在強化學習中,智能體需要在探索(Exploration)和利用(Exploitation)之間取得平衡:探索是指嘗試新的動作以發現潛在的高回報;利用是指選擇已知能帶來高回報的動作。

VPG 通過訓練隨機策略來實現探索與利用的平衡。在訓練初期,策略的隨機性較強,智能體會通過採樣不同的動作進行充分探索,以瞭解環境的獎勵機制。隨着訓練的進行,策略會逐漸調整,增加高回報動作的概率,減少低迴報動作的概率,從而更多地利用已發現的高回報策略。

然而,這種機制也存在一定的風險:如果策略過早收斂到局部最優解,可能會陷入局部最優,無法發現更優的全局策略。因此,在實際應用中,通常需要通過調整策略的初始隨機性、學習率等超參數來緩解這一問題。

算法流程(偽代碼)

VPG 的算法流程可以概括為以下步驟:

  1. 輸入初始策略參數$\theta_0$和初始價值函數參數$\phi_0$。
  2. 對於每個迭代$k=0,1,2,...$:
  • 步驟 3:通過當前策略$\pi_k = \pi(\theta_k)$與環境交互,收集軌跡集合$D_k = {\tau_i}$。
  • 步驟 4-5:計算回報到 go(Rewards-to-go)$R_t$,並基於當前價值函數$V_{\phi_k}$計算優勢估計$\hat{A}_t$。
  • 步驟 6:估計策略梯度$\hat{g}k = \frac{1}{|D_k|} \sum \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \big|_{\theta_k} \hat{A}_t$。
  • 步驟 7:通過梯度上升更新策略參數,如$\theta_{k+1} = \theta_k + \alpha_k \hat{g}_k$(或使用 Adam 等優化器)。
  • 步驟 8:通過最小化均方誤差來擬合價值函數:$\phi_{k+1} = \arg \min_\phi \frac{1}{|D_k| T} \sum_{\tau \in D_k} \sum_{t=0}^T (V_\phi(s_t) - \hat{R}_t)^2$,通常使用梯度下降算法。

實現

import numpy as np
import torch
from torch.optim import Adam
import gym
import time
import spinup.algos.pytorch.vpg.core as core
from spinup.utils.logx import EpochLogger
from spinup.utils.mpi_pytorch import setup_pytorch_for_mpi, sync_params, mpi_avg_grads
from spinup.utils.mpi_tools import (
    mpi_fork,
    mpi_avg,
    proc_id,
    mpi_statistics_scalar,
    num_procs,
)


class VPGBuffer:

    def __init__(self, obs_dim, act_dim, size, gamma=0.99, lam=0.95):
        self.obs_buf = np.zeros(core.combined_shape(size, obs_dim), dtype=np.float32)
        self.act_buf = np.zeros(core.combined_shape(size, act_dim), dtype=np.float32)
        self.adv_buf = np.zeros(size, dtype=np.float32)
        self.rew_buf = np.zeros(size, dtype=np.float32)
        self.ret_buf = np.zeros(size, dtype=np.float32)
        self.val_buf = np.zeros(size, dtype=np.float32)
        self.logp_buf = np.zeros(size, dtype=np.float32)
        self.gamma, self.lam = gamma, lam
        self.ptr, self.path_start_idx, self.max_size = 0, 0, size

    def store(self, obs, act, rew, val, logp):
       
        assert self.ptr < self.max_size  # buffer has to have room so you can store
        self.obs_buf[self.ptr] = obs
        self.act_buf[self.ptr] = act
        self.rew_buf[self.ptr] = rew
        self.val_buf[self.ptr] = val
        self.logp_buf[self.ptr] = logp
        self.ptr += 1

    def finish_path(self, last_val=0):

        path_slice = slice(self.path_start_idx, self.ptr)
        rews = np.append(self.rew_buf[path_slice], last_val)
        vals = np.append(self.val_buf[path_slice], last_val)

        # the next two lines implement GAE-Lambda advantage calculation
        deltas = rews[:-1] + self.gamma * vals[1:] - vals[:-1]
        self.adv_buf[path_slice] = core.discount_cumsum(deltas, self.gamma * self.lam)

        # the next line computes rewards-to-go, to be targets for the value function
        self.ret_buf[path_slice] = core.discount_cumsum(rews, self.gamma)[:-1]

        self.path_start_idx = self.ptr

    def get(self):
        
        assert self.ptr == self.max_size  # buffer has to be full before you can get
        self.ptr, self.path_start_idx = 0, 0
        # the next two lines implement the advantage normalization trick
        adv_mean, adv_std = mpi_statistics_scalar(self.adv_buf)
        self.adv_buf = (self.adv_buf - adv_mean) / adv_std
        data = dict(
            obs=self.obs_buf,
            act=self.act_buf,
            ret=self.ret_buf,
            adv=self.adv_buf,
            logp=self.logp_buf,
        )
        return {k: torch.as_tensor(v, dtype=torch.float32) for k, v in data.items()}


def vpg(
    env_fn,
    actor_critic=core.MLPActorCritic,
    ac_kwargs=dict(),
    seed=0,
    steps_per_epoch=4000,
    epochs=50,
    gamma=0.99,
    pi_lr=1e-3,
    vf_lr=1e-3,
    train_v_iters=80,
    lam=0.97,
    logger_kwargs=dict(),
    save_freq=10,
):
    # Special function to avoid certain slowdowns from PyTorch + MPI combo.
    setup_pytorch_for_mpi()

    # Set up logger and save configuration
    logger = EpochLogger(**logger_kwargs)
    logger.save_config(locals())

    # Random seed
    seed += 10000 * proc_id()
    torch.manual_seed(seed)
    np.random.seed(seed)

    # Instantiate environment
    env = env_fn()
    obs_dim = env.observation_space.shape
    act_dim = env.action_space.shape

    # Create actor-critic module
    ac = actor_critic(env.observation_space, env.action_space, **ac_kwargs)

    # Sync params across processes
    sync_params(ac)

    # Count variables
    var_counts = tuple(core.count_vars(module) for module in [ac.pi, ac.v])
    logger.log("\nNumber of parameters: \t pi: %d, \t v: %d\n" % var_counts)

    # Set up experience buffer
    local_steps_per_epoch = int(steps_per_epoch / num_procs())
    buf = VPGBuffer(obs_dim, act_dim, local_steps_per_epoch, gamma, lam)

    # Set up function for computing VPG policy loss
    def compute_loss_pi(data):
        obs, act, adv, logp_old = data["obs"], data["act"], data["adv"], data["logp"]

        # Policy loss
        pi, logp = ac.pi(obs, act)
        loss_pi = -(logp * adv).mean()

        # Useful extra info
        approx_kl = (logp_old - logp).mean().item()
        ent = pi.entropy().mean().item()
        pi_info = dict(kl=approx_kl, ent=ent)

        return loss_pi, pi_info

    # Set up function for computing value loss
    def compute_loss_v(data):
        obs, ret = data["obs"], data["ret"]
        return ((ac.v(obs) - ret) ** 2).mean()

    # Set up optimizers for policy and value function
    pi_optimizer = Adam(ac.pi.parameters(), lr=pi_lr)
    vf_optimizer = Adam(ac.v.parameters(), lr=vf_lr)

    # Set up model saving
    logger.setup_pytorch_saver(ac)

    def update():
        data = buf.get()

        # Get loss and info values before update
        pi_l_old, pi_info_old = compute_loss_pi(data)
        pi_l_old = pi_l_old.item()
        v_l_old = compute_loss_v(data).item()

        # Train policy with a single step of gradient descent
        pi_optimizer.zero_grad()
        loss_pi, pi_info = compute_loss_pi(data)
        loss_pi.backward()
        mpi_avg_grads(ac.pi)  # average grads across MPI processes
        pi_optimizer.step()

        # Value function learning
        for i in range(train_v_iters):
            vf_optimizer.zero_grad()
            loss_v = compute_loss_v(data)
            loss_v.backward()
            mpi_avg_grads(ac.v)  # average grads across MPI processes
            vf_optimizer.step()

        # Log changes from update
        kl, ent = pi_info["kl"], pi_info_old["ent"]
        logger.store(
            LossPi=pi_l_old,
            LossV=v_l_old,
            KL=kl,
            Entropy=ent,
            DeltaLossPi=(loss_pi.item() - pi_l_old),
            DeltaLossV=(loss_v.item() - v_l_old),
        )

    # Prepare for interaction with environment
    start_time = time.time()
    o, _ = env.reset()
    ep_ret, ep_len = 0, 0

    # Main loop: collect experience in env and update/log each epoch
    for epoch in range(epochs):
        for t in range(local_steps_per_epoch):
            a, v, logp = ac.step(torch.as_tensor(o, dtype=torch.float32))

            next_o, r, d, truncated, _ = env.step(a)
            ep_ret += r
            ep_len += 1

            # save and log
            buf.store(o, a, r, v, logp)
            logger.store(VVals=v)

            # Update obs (critical!)
            o = next_o

            timeout = truncated
            terminal = d or timeout
            epoch_ended = t == local_steps_per_epoch - 1

            if terminal or epoch_ended:
                if epoch_ended and not (terminal):
                    print(
                        "Warning: trajectory cut off by epoch at %d steps." % ep_len,
                        flush=True,
                    )
                # if trajectory didn't reach terminal state, bootstrap value target
                if timeout or epoch_ended:
                    _, v, _ = ac.step(torch.as_tensor(o, dtype=torch.float32))
                else:
                    v = 0
                buf.finish_path(v)
                if terminal:
                    # only save EpRet / EpLen if trajectory finished
                    logger.store(EpRet=ep_ret, EpLen=ep_len)
                o, _ = env.reset()
                ep_ret, ep_len = 0, 0

        # Save model
        if (epoch % save_freq == 0) or (epoch == epochs - 1):
            logger.save_state({"env": env}, None)

        # Perform VPG update!
        update()

        # Log info about epoch
        logger.log_tabular("Epoch", epoch)
        logger.log_tabular("EpRet", with_min_and_max=True)
        logger.log_tabular("EpLen", average_only=True)
        logger.log_tabular("VVals", with_min_and_max=True)
        logger.log_tabular("TotalEnvInteracts", (epoch + 1) * steps_per_epoch)
        logger.log_tabular("LossPi", average_only=True)
        logger.log_tabular("LossV", average_only=True)
        logger.log_tabular("DeltaLossPi", average_only=True)
        logger.log_tabular("DeltaLossV", average_only=True)
        logger.log_tabular("Entropy", average_only=True)
        logger.log_tabular("KL", average_only=True)
        logger.log_tabular("Time", time.time() - start_time)
        logger.dump_tabular()


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--env", type=str, default="HalfCheetah-v2")
    parser.add_argument("--hid", type=int, default=64)
    parser.add_argument("--l", type=int, default=2)
    parser.add_argument("--gamma", type=float, default=0.99)
    parser.add_argument("--seed", "-s", type=int, default=0)
    parser.add_argument("--cpu", type=int, default=4)
    parser.add_argument("--steps", type=int, default=4000)
    parser.add_argument("--epochs", type=int, default=200)
    parser.add_argument("--exp_name", type=str, default="vpg")
    args = parser.parse_args()

    mpi_fork(args.cpu)  # run parallel code with mpi

    from spinup.utils.run_utils import setup_logger_kwargs

    logger_kwargs = setup_logger_kwargs(args.exp_name, args.seed)

    vpg(
        lambda: gym.make(args.env),
        actor_critic=core.MLPActorCritic,
        ac_kwargs=dict(hidden_sizes=[args.hid] * args.l),
        gamma=args.gamma,
        seed=args.seed,
        steps_per_epoch=args.steps,
        epochs=args.epochs,
        logger_kwargs=logger_kwargs,
    )

Trust Region Policy Optimization(TRPO):強化學習中的信賴域策略優化算法

信賴域策略優化(TRPO)算法通過在滿足新舊策略相似度約束的前提下,儘可能採取最大步長來提升策略性能。這種約束通過 KL 散度(Kullback-Leibler Divergence)來表達,KL 散度是一種衡量概率分佈之間 “距離”(類似但不完全等同於傳統距離)的指標。

這與普通的策略梯度方法有顯著區別:傳統策略梯度方法通過限制參數空間中新舊策略的距離來保持相似性,但即使參數空間中看似微小的差異,也可能導致策略性能的巨大波動 —— 一步錯誤的更新就可能使策略性能急劇下降。這使得在使用 vanilla 策略梯度時,採用大步長存在風險,從而降低了樣本效率。而 TRPO 巧妙地避免了這種性能崩潰問題,並且能夠快速、單調地提升策略性能。

核心特點速覽

  • 策略類型:TRPO 是一種 on-policy(同策略)算法。
  • 適用環境:可用於具有離散或連續動作空間的環境。
  • 並行支持:OpenAI Spinning Up 的 TRPO 實現支持通過 MPI 進行並行計算。

關鍵公式解析

令$\pi_\theta$表示帶有參數$\theta$的策略。TRPO 的理論更新公式為:

$\theta_{k+1} = \arg\max_\theta L(\theta_k, \theta)$

$s.t. D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \leq \delta$

其中,$L(\theta_k, \theta)$是替代優勢函數(surrogate advantage),用於衡量新策略$\pi_\theta$相對於舊策略$\pi_{\theta_k}$的性能,其計算基於舊策略收集的數據:

$L(\theta_k, \theta) = \mathbb{E}{s,a \sim \pi{\theta_k}} \left[ \frac{\pi_\theta(a|s)}{\pi_{\theta_k}(a|s)} A_{\theta_k}(s,a) \right]$

而$D_{KL}(\pi_\theta \parallel \pi_{\theta_k})$是在舊策略訪問過的狀態上,新舊策略之間的平均 KL 散度:

$D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) = \mathbb{E}{s \sim \pi{\theta_k}} \left[ D_{KL}(\pi_\theta(\cdot|s) \parallel \pi_{\theta_k}(\cdot|s)) \right]$

由於理論上的 TRPO 更新難以直接計算,TRPO 通過一些近似來快速求解。我們將目標函數和約束條件在$\theta_k$處進行泰勒展開至一階項:

$L(\theta_k, \theta) \approx g^T (\theta - \theta_k)$

$D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \approx \frac{1}{2} (\theta - \theta_k)^T H (\theta - \theta_k)$

由此得到近似的優化問題:

$\theta_{k+1} = \arg\max_\theta g^T (\theta - \theta_k)$

$s.t. \frac{1}{2} (\theta - \theta_k)^T H (\theta - \theta_k) \leq \delta$

這個近似問題可以通過拉格朗日對偶方法解析求解,得到:

$\theta_{k+1} = \theta_k + \alpha \frac{H^{-1} g}{\sqrt{\frac{g^T H^{-1} g}{2\delta}}}$

如果僅停留於此,該算法就等價於自然策略梯度(Natural Policy Gradient)。但由於泰勒展開引入的近似誤差,這個解可能不滿足 KL 約束,或者無法提升替代優勢函數。因此 TRPO 增加了回溯線搜索(backtracking line search)機制:

$\theta_{k+1} = \theta_k + \alpha^j \frac{H^{-1} g}{\sqrt{\frac{g^T H^{-1} g}{2\delta}}}$

其中$\alpha \in (0,1)$是回溯係數,$j$是滿足 KL 約束且能產生正替代優勢的最小非負整數。

最後需要注意的是:當處理具有數千或數百萬參數的神經網絡策略時,計算和存儲矩陣$H^{-1}$的成本極高。TRPO 通過共軛梯度算法(conjugate gradient algorithm)來求解$Hx = g$,得到$x = H^{-1}g$,這僅需要計算矩陣 - 向量乘積$Hx$,而無需直接計算和存儲整個矩陣$H$。具體而言,通過符號運算計算:

$Hx = \nabla_\theta \left( \nabla_\theta D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \cdot x \right)$

即可在不計算完整矩陣的情況下得到正確結果。

探索與利用的平衡

TRPO 以 on-policy 的方式訓練隨機策略。這意味着它通過根據最新的隨機策略採樣動作來進行探索。動作選擇的隨機性大小取決於初始條件和訓練過程。在訓練過程中,策略的隨機性通常會逐漸降低,因為更新規則會鼓勵策略利用已發現的獎勵。但這也可能導致策略陷入局部最優。

算法偽代碼

算法 1:Trust Region Policy Optimization

  1. 輸入:初始策略參數$\theta_0$,初始價值函數參數$\phi_0$
  2. 超參數:KL 散度限制$\delta$,回溯係數$\alpha$,最大回溯步數$K$
  3. 對於$k=0,1,2,...$循環:
  • 通過運行策略$\pi_k = \pi(\theta_k)$在環境中收集軌跡集$D_k = {\tau_i}$
  • 計算回報 - to-go(rewards-to-go)$R_t$
  • 基於當前價值函數$V_{\phi_k}$計算優勢估計$A_t$(可使用任何優勢估計方法)
  • 估計策略梯度:$\hat{g}k = \frac{1}{|D_k|} \sum \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t|s_t) \big|_{\theta_k} \hat{A}_t$
  • 使用共軛梯度算法計算$\hat{x}_k \approx \hat{H}_k^{-1} \hat{g}_k$,其中$H_k$是樣本平均 KL 散度的 Hessian 矩陣
  • 通過回溯線搜索更新策略:$\theta_{k+1} = \theta_k + \alpha^j \sqrt{\frac{2\delta}{\hat{x}_k^T \hat{H}_k \hat{x}_k}} \hat{x}_k$,其中$j \in {0,1,2,...,K}$是滿足樣本 KL 約束且能提升樣本損失的最小值
  • 通過均方誤差迴歸擬合價值函數:$\phi_{k+1} = \arg\min_\phi \frac{1}{|D_k| T} \sum_{\tau \in D_k} \sum_{t=0}^T (V_\phi(s_t) - \hat{R}_t)^2$,通常使用梯度下降算法
  • 結束循環

Proximal Policy Optimization(PPO):簡單高效的強化學習算法

PPO(Proximal Policy Optimization)的提出源於與 TRPO 相同的問題:如何利用當前擁有的數據,在策略上邁出儘可能大的改進步驟,同時又不會因為步子過大而意外導致性能崩潰。

與 TRPO 採用複雜的二階方法解決該問題不同,PPO 是一系列一階方法,它通過一些其他技巧來保持新策略與舊策略的接近性。PPO 方法實現起來明顯更簡單,並且從經驗上看,其性能至少與 TRPO 相當。

PPO 主要有兩種變體:PPO - Penalty 和 PPO - Clip。

PPO - Penalty 近似求解類似 TRPO 的 KL 約束更新,但它在目標函數中對 KL 散度進行懲罰,而不是將其作為硬約束,並且在訓練過程中自動調整懲罰係數,以使其得到適當的縮放。

PPO - Clip 的目標函數中沒有 KL 散度項,也根本沒有約束。它依靠目標函數中的專門剪輯來消除新策略遠離舊策略的動機。本文將重點介紹 PPO - Clip(這是 OpenAI 主要使用的變體)。

二、快速 facts

  • PPO 是一種 on - policy 算法。
  • PPO 可用於具有離散或連續動作空間的環境。
  • Spinning Up 實現的 PPO 支持通過 MPI 進行並行化。

三、關鍵方程

PPO - Clip 通過以下方式更新策略:

$\theta_{k + 1}=\arg\max_{\theta}E\left[L(s,a,\theta_{k},\theta)\right]$,其中$s,a\sim\pi_{\theta_{k}}$

通常會採取多個步驟(通常是小批量)的 SGD 來最大化目標。這裏的$L$由下式給出:

$L(s,a,\theta_{k},\theta)=\min\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}A{\pi_{\theta_{k}}}(s,a),g(\epsilon,A{\pi_{\theta_{k}}}(s,a))\right)$

其中,$g(\epsilon,A)=\begin{cases}(1 + \epsilon)A& A\geq0\(1-\epsilon)A& A<0\end{cases}$

這個表達式相當複雜,乍一看很難理解它在做什麼,以及它如何幫助保持新策略與舊策略的接近。事實證明,這個目標有一個相當簡化的版本,更容易理解(這也是我們在代碼中實現的版本)。

為了理解其背後的直覺,讓我們看看單個狀態 - 動作對$s,a$,並考慮不同情況。

當優勢為正時:假設該狀態 - 動作對的優勢為正,在這種情況下,它對目標的貢獻簡化為$L(s,a,\theta_{k},\theta)=\min\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)},(1 + \epsilon)\right)A^{\pi_{\theta_{k}}}(s,a)$。

由於優勢為正,如果動作變得更有可能(即$\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}$增大),目標將會增加。但該項中的最小值限制了目標的增加幅度。一旦$\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}>(1 + \epsilon)$,最小值開始起作用,該項達到$(1 + \epsilon)A^{\pi_{\theta_{k}}}(s,a)$的上限。因此,新策略不會從遠離舊策略中獲益。

當優勢為負時:假設該狀態 - 動作對的優勢為負,此時它對目標的貢獻簡化為$L(s,a,\theta_{k},\theta)=\max\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)},(1-\epsilon)\right)A^{\pi_{\theta_{k}}}(s,a)$。

由於優勢為負,如果動作變得更不可能(即$\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}$減小),目標將會增加。但該項中的最大值限制了目標的增加幅度。一旦$\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}<(1-\epsilon)$,最大值開始起作用,該項達到$(1-\epsilon)A^{\pi_{\theta_{k}}}(s,a)$的上限。因此,同樣,新策略不會從遠離舊策略中獲益。

到目前為止,我們已經瞭解到剪輯通過消除策略發生巨大變化的動機而起到正則化的作用,超參數$\epsilon$對應於新策略在仍然能從目標中獲利的情況下可以偏離舊策略的程度。

需要知道的是,雖然這種剪輯在很大程度上有助於確保合理的策略更新,但仍然有可能得到一個與舊策略相差太遠的新策略,不同的 PPO 實現使用了許多技巧來避免這種情況。在我們的實現中,我們使用一種特別簡單的方法:早停。如果新策略與舊策略的平均 KL 散度超過一個閾值,我們就停止採取梯度步驟。

當你對基本的數學和實現細節感到滿意時,值得查看其他實現,看看它們是如何處理這個問題的!

四、探索與利用

PPO 以 on - policy 的方式訓練隨機策略。這意味着它通過根據最新版本的隨機策略採樣動作來進行探索。動作選擇中的隨機量取決於初始條件和訓練過程。在訓練過程中,策略通常會逐漸減少隨機性,因為更新規則鼓勵它利用已經發現的獎勵。這可能會導致策略陷入局部最優。

五、偽代碼

Algorithm 1 PPO - Clip

1: 輸入:初始策略參數$\theta_{0}$,初始價值函數參數$\phi_{0}$

2: for $k = 0,1,2,...$ do

3: 通過運行策略$\pi_{k}=\pi(\theta_{k})$在環境中收集軌跡集合$\mathcal{D}{k}={\tau{i}}$

4: 計算未來獎勵$R_{t}$

5: 基於當前價值函數$V_{\phi_{k}}$計算優勢估計$A_{t}$(使用任何優勢估計方法)

6: 通過最大化 PPO - Clip 目標來更新策略:$\theta_{k + 1}=\arg\max_{\theta}\frac{1}{|\mathcal{D}{k}|T}\sum{k}}\sum{T}min\left(\frac{\pi_{\theta}(a_{t}|s_{t})}{\pi_{\theta_{k}}(a_{t}|s_{t})}A{\pi_{\theta_{k}}}(s_{t},a_{t}),g(\epsilon,A^{\pi_{\theta_{k}}}(s_{t},a_{t}))\right)$,通常通過隨機梯度上升實現

7: 通過均方誤差迴歸擬合價值函數:$\phi_{k + 1}=\arg\min_{\phi}\frac{1}{|\mathcal{D}{k}|T}\sum{k}}\sum{T}(V_{\phi}(s_{t})-\hat{R}_{t})$,通常通過某種梯度下降算法實現

8: end for

Deep Deterministic Policy Gradient(DDPG):連續動作空間的強化學習算法

深度確定策略梯度(DDPG)算法旨在解決具有連續動作空間環境中的強化學習問題。在傳統的策略梯度方法中,策略通常是隨機的,即給定狀態下,策略輸出的是動作的概率分佈,然後根據概率採樣選擇動作。然而,在許多實際應用場景中,如機器人控制、自動駕駛等,動作空間是連續的,從概率分佈中採樣動作效率較低,並且可能導致策略的不穩定。

DDPG 算法基於確定性策略,直接輸出具體的動作值,避免了隨機策略中的採樣過程,提高了決策效率。此外,DDPG 還結合了深度神經網絡強大的表達能力,能夠處理複雜的高維狀態空間,在連續動作空間的強化學習任務中表現出色。

DDPG 算法借鑑了多個經典算法的思想,它結合了深度 Q 網絡(DQN)的經驗回放(Experience Replay)和目標網絡(Target Network)技術,以及確定性策略梯度(Deterministic Policy Gradient,DPG)算法的理論基礎,通過不斷學習和優化,使智能體能夠在連續動作空間的環境中找到最優策略。

二、快速 facts

  • 算法類型:DDPG 是一種 off - policy 算法,這意味着用於學習策略的數據可以來自於與當前策略不同的策略所產生的軌跡。
  • 適用場景:特別適用於動作空間為連續的強化學習環境,如機器人的關節控制、飛行器的姿態調整等。
  • 網絡結構:包含四個深度神經網絡,分別為 Actor 網絡、Actor 目標網絡、Critic 網絡、Critic 目標網絡。其中 Actor 網絡用於輸出確定性動作,Critic 網絡用於評估動作的價值。
  • 超參數特性:對超參數較為敏感,超參數的選擇會顯著影響算法的性能和收斂速度 。

三、關鍵方程

DDPG 算法的核心在於對 Actor 網絡和 Critic 網絡的更新。

Actor 網絡更新

Actor 網絡的目標是最大化 Q 值,其更新的梯度為:

$\nabla_{\theta^{\mu}}\bar{J} \approx \frac{1}{N}\sum_{i}\nabla_{a}Q(s,a;\theta^{Q})\big|{s = s^{i},a = \mu(s{i};\theta)}\nabla{\theta{\mu}}\mu(s;\theta)\big|_{s^{i}}$

其中,$\theta^{\mu}$是 Actor 網絡的參數,$\bar{J}$是目標函數,$N$是樣本數量,$Q(s,a;\theta^{Q})$是 Critic 網絡輸出的 Q 值,$\mu(s;\theta^{\mu})$是 Actor 網絡根據狀態$s$輸出的動作。該公式的含義是通過計算 Q 值對動作的梯度與 Actor 網絡輸出動作對其參數的梯度的乘積,來更新 Actor 網絡的參數,從而使得 Actor 網絡輸出的動作能夠最大化 Q 值。

Critic 網絡更新

Critic 網絡的目標是最小化 TD 誤差( temporal - difference error),其損失函數為:

$L = \frac{1}{N}\sum_{i}(y^{i} - Q(s{i},a;\theta{Q}))$

其中,$y^{i}$是目標 Q 值,計算方式為:

$y^{i} = r^{i} + \gamma Q'(s'{i},\mu'(s';\theta^{\mu'}) ;\theta^{Q'})$

這裏,$r{i}$是在狀態$s$執行動作$a^{i}$後獲得的獎勵,$\gamma$是折扣因子,$Q'$和$\mu'$分別是 Critic 目標網絡和 Actor 目標網絡,$\theta{Q'}$和$\theta$是它們對應的參數。Critic 網絡通過最小化損失函數$L$,來不斷調整自身參數,以更準確地評估動作的價值。

此外,為了使算法更加穩定,DDPG 採用了目標網絡緩慢更新的策略,即:

$\theta{Q'}\leftarrow\tau\theta+(1 - \tau)\theta^{Q'}$

$\theta{\mu'}\leftarrow\tau\theta+(1 - \tau)\theta^{\mu'}$

其中,$\tau$是一個較小的更新系數(通常遠小於 1),通過這種軟更新方式,使得目標網絡的參數變化較為緩慢,從而提高算法的穩定性和收斂性。

四、探索與利用

作為一種 off - policy 算法,DDPG 通過在確定性動作上添加噪聲來實現探索與利用的平衡。常見的做法是添加高斯噪聲(Gaussian Noise),即在 Actor 網絡輸出的確定性動作上疊加一個符合高斯分佈的隨機噪聲,使智能體能夠在一定程度上探索新的動作。隨着訓練的進行,噪聲的強度可以逐漸衰減,使得智能體更多地利用已經學習到的較好策略。

例如,在訓練初期,較大的噪聲可以幫助智能體廣泛地探索環境,發現潛在的高回報動作;而在訓練後期,較小的噪聲可以使智能體專注於已經發現的較優策略,進一步優化動作選擇,以獲得更高的累計獎勵。

五、偽代碼

Algorithm 1 DDPG

1: 初始化 Actor 網絡$\mu(s;\theta^{\mu})$、Critic 網絡$Q(s,a;\theta{Q})$,以及對應的目標網絡$\mu'(s;\theta)$、$Q'(s,a;\theta{Q'})$,設置參數$\theta=\theta{\mu}$,$\theta=\theta^{Q}$

2: 初始化經驗回放緩衝區$D$

3: for episode = 1, M do

4: 初始化環境狀態$s$

5: for t = 1, T do

6: 根據當前策略$\mu(s;\theta^{\mu})$並添加噪聲生成動作$a$

7: 在環境中執行動作$a$,獲取下一個狀態$s'$、獎勵$r$和是否結束的標誌$done$

8: 將$(s,a,r,s',done)$存儲到經驗回放緩衝區$D$中

9: 從$D$中隨機採樣一批樣本$(s_{i},a_{i},r_{i},s_{i}',done_{i})$

10: 計算目標 Q 值$y_{i}=r_{i}+\gamma Q'(s_{i}',\mu'(s_{i}';\theta^{\mu'}) ;\theta^{Q'})$,若$done_{i}$為 True,則$y_{i}=r_{i}$

11: 更新 Critic 網絡:通過最小化$(y_{i}-Q(s_{i},a_{i};\theta{Q}))$來更新$\theta^{Q}$

12: 更新 Actor 網絡:通過計算$\nabla_{\theta^{\mu}}\bar{J} \approx \frac{1}{N}\sum_{i}\nabla_{a}Q(s,a;\theta^{Q})\big|{s = s,a = \mu(s_{i};\theta{\mu})}\nabla_{\theta{\mu}}\mu(s;\theta{\mu})\big|_{s_{i}}$來更新$\theta$

13: 軟更新目標網絡:$\theta{Q'}\leftarrow\tau\theta+(1 - \tau)\theta{Q'}$,$\theta\leftarrow\tau\theta^{\mu}+(1 - \tau)\theta^{\mu'}$

14: 如果$done$為 True,跳出循環

15: end for

16: end for

參考

OpenAI Spinning UP