動手學強化學習(四):動態規劃演算法

jasonzhangxianrong發表於2024-03-03

第 4 章 動態規劃演算法

4.1 簡介

動態規劃(dynamic programming)是程式設計演算法中非常重要的內容,能夠高效解決一些經典問題,例如揹包問題和最短路徑規劃。動態規劃的基本思想是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到目標問題的解。動態規劃會儲存已解決的子問題的答案,在求解目標問題的過程中,需要這些子問題答案時就可以直接利用,避免重複計算。本章介紹如何用動態規劃的思想來求解在馬爾可夫決策過程中的最優策略。

基於動態規劃的強化學習演算法主要有兩種:一是策略迭代(policy iteration),二是價值迭代(value iteration)。其中,策略迭代由兩部分組成:策略評估(policy evaluation)和策略提升(policy improvement)。具體來說,策略迭代中的策略評估使用貝爾曼期望方程來得到一個策略的狀態價值函式,這是一個動態規劃的過程;而價值迭代直接使用貝爾曼最優方程來進行動態規劃,得到最終的最優狀態價值。

不同於 3.5 節介紹的蒙特卡洛方法和第 5 章將要介紹的時序差分演算法,基於動態規劃的這兩種強化學習演算法要求事先知道環境的狀態轉移函式和獎勵函式,也就是需要知道整個馬爾可夫決策過程。在這樣一個白盒環境中,不需要透過智慧體和環境的大量互動來學習,可以直接用動態規劃求解狀態價值函式。但是,現實中的白盒環境很少,這也是動態規劃演算法的侷限之處,我們無法將其運用到很多實際場景中。另外,策略迭代和價值迭代通常只適用於有限馬爾可夫決策過程,即狀態空間和動作空間是離散且有限的。

4.2 懸崖漫步環境

本節使用策略迭代和價值迭代來求解懸崖漫步(Cliff Walking)這個環境中的最優策略。接下來先簡單介紹一下該環境。

懸崖漫步是一個非常經典的強化學習環境,它要求一個智慧體從起點出發,避開懸崖行走,最終到達目標位置。如圖 4-1 所示,有一個 4×12 的網格世界,每一個網格表示一個狀態。智慧體的起點是左下角的狀態,目標是右下角的狀態,智慧體在每一個狀態都可以採取 4 種動作:上、下、左、右。如果智慧體採取動作後觸碰到邊界牆壁則狀態不發生改變,否則就會相應到達下一個狀態。環境中有一段懸崖,智慧體掉入懸崖或到達目標狀態都會結束動作並回到起點,也就是說掉入懸崖或者達到目標狀態是終止狀態。智慧體每走一步的獎勵是 −1,掉入懸崖的獎勵是 −100。

img

圖4-1 Cliff Walking環境示意圖

接下來一起來看一看 Cliff Walking 環境的程式碼吧。

import copy


class CliffWalkingEnv:
    """ 懸崖漫步環境"""
    def __init__(self, ncol=12, nrow=4):
        self.ncol = ncol  # 定義網格世界的列
        self.nrow = nrow  # 定義網格世界的行
        # 轉移矩陣P[state][action] = [(p, next_state, reward, done)]包含下一個狀態和獎勵
        self.P = self.createP()

    def createP(self):
        # 初始化
        P = [[[] for j in range(4)] for i in range(self.nrow * self.ncol)]
        # 4種動作, change[0]:上,change[1]:下, change[2]:左, change[3]:右。座標系原點(0,0)
        # 定義在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]
        for i in range(self.nrow):
            for j in range(self.ncol):
                for a in range(4):
                    # 位置在懸崖或者目標狀態,因為無法繼續互動,任何動作獎勵都為0
                    if i == self.nrow - 1 and j > 0:
                        P[i * self.ncol + j][a] = [(1, i * self.ncol + j, 0,
                                                    True)]
                        continue
                    # 其他位置
                    next_x = min(self.ncol - 1, max(0, j + change[a][0]))
                    next_y = min(self.nrow - 1, max(0, i + change[a][1]))
                    next_state = next_y * self.ncol + next_x
                    reward = -1
                    done = False
                    # 下一個位置在懸崖或者終點
                    if next_y == self.nrow - 1 and next_x > 0:
                        done = True
                        if next_x != self.ncol - 1:  # 下一個位置在懸崖
                            reward = -100
                    P[i * self.ncol + j][a] = [(1, next_state, reward, done)]
        return P

4.3 策略迭代演算法

策略迭代是策略評估和策略提升不斷迴圈交替,直至最後得到最優策略的過程。本節分別對這兩個過程進行詳細介紹。

4.3.1 策略評估

策略評估這一過程用來計算一個策略的狀態價值函式。回顧一下之前學習的貝爾曼期望方程:

\[V^{\pi}(s)=\sum_{a\in A}\pi(a|s)\left(r(s,a)+\gamma\sum_{s'\in S}p(s'|s,a)V^{\pi}(s')\right) \]

其中,\(\pi(a|s)\)是策略\(\pi\)在狀態\(s\)下采取動作\(a\)的機率。可以看到,當知道獎勵函式和狀態轉移函式時,我們可以根據下一個狀態的價值來計算當前狀態的價值。因此,根據動態規劃的思想,可以把計算下一個可能狀態的價值當成一個子問題,把計算當前狀態的價值看作當前問題。在得知子問題的解後,就可以求解當前問題。更一般的,考慮所有的狀態,就變成了用上一輪的狀態價值函式來計算當前這一輪的狀態價值函式,即

\[V^{k+1}(s)=\sum_{a\in A}\pi(a|s)\left(r(s,a)+\gamma\sum_{s'\in S}P(s'|s,a)V^k(s')\right) \]

我們可以選定任意初始值\(V^0\)。根據貝爾曼期望方程,可以得知\(V^k=V^\pi\)是以上更新公式的一個不動點 (fixed point)。事實上,可以證明當\(k\to\infty\)時,序列\(\{V^k\}\)會收斂到\(V^{\pi}\),所以可以據此來計算得到一個策略的狀態價值函式。可以看到,由於需要不斷做貝爾曼期望方程迭代,策略評估其實會耗費很大的計算代價。在實際的實現過程中,如果某一輪

\(\max_{s\in S}|V^{k+1}(s)-V^k(s)|\)的值非常小,可以提前結束策略評估。這樣做可以提升效率,並且得到的價值也非常接近真實的價值。

4.3.2 策略提升

使用策略評估計算得到當前策略的狀態價值函式之後,我們可以據此來改進該策略。假設此時對於策略\(\pi\),我們已經知道其價值\(V^{\pi}\), 也就是知道了在策略\(\pi\)下從每一個狀態\(s\)出發最終得到的期望回報。我們要如何改變策略來獲得在狀態\(s\)下更高的期望回報呢?假設智慧體在狀態\(s\)下采取動作\(a\),之後的動作依舊遵循策略\(\pi\), 此時得到的期望回報其實就是動作價值\(Q^{\pi}(s,a)\)。如果我們有\(Q^{\pi}(s,a)>V^{\pi}(s)\),則說明在狀態\(s\)下采取動作\(a\)會比原來的策略\(\pi(a|s)\)得到更高的期望回報。以上假設只是針對一個狀態,現在假設存在一個確定性策略\(\pi^{\prime}\),在任意一個狀態\(s\)下,都滿足

\[Q^\pi(s,\pi^{\prime}(s))\geq V^\pi(s) \]

於是在任意狀態\(s\)下,我們有

\[V^{\pi^{\prime}}(s)\geq V^\pi(s) \]

這便是策略提升定理 (policy improvement theorem)。於是我們可以直接貪心地在每一個狀態選擇動作價值最大的動作,也就是

\[\pi'(s)=\arg\max_aQ^\pi(s,a)=\arg\max_a\{r(s,a)+\gamma\sum_{s'}P(s'|s,a)V^\pi(s')\} \]

我們發現構造的貪心策略\(\pi^{\prime}\)滿足策略提升定理的條件,所以策略\(\pi^{\prime}\)能夠比策略\(\pi\)更好或者至少與其一樣好。這個根據貪心法選取動作從而得到新的策略的過程稱為策略提升。當策略提升之後得到的策略\(\pi^{\prime}\)和之前的策略\(\pi\)一樣時,說明策略迭代達到了收斂,此時\(\pi\)\(\pi^{\prime}\)​就是最優策略。

策略提升定理的證明透過以下推導過程可以證明,使用上述提升公式得到的新策略\(\pi^{\prime}\)在每個狀態的價值不低於原策略\(\pi\)​在該狀態的價值。

\[\begin{aligned} V^{\pi}(s)& \leq Q^\pi(s,\pi^{\prime}(s)) \\ &=\mathbb{E}_{\pi^{\prime}}[R_t+\gamma V^{\pi}(S_{t+1})|S_t=s] \\ &\leq\mathbb{E}_{\pi^{\prime}}[R_t+\gamma Q^{\pi}(S_{t+1},\pi^{\prime}(S_{t+1}))|S_t=s] \\ &=\mathbb{E}_{\pi^{\prime}}[R_t+\gamma R_{t+1}+\gamma^2V^\pi(S_{t+2})|S_t=s] \\ &\leq\mathbb{E}_{\pi^{\prime}}[R_t+\gamma R_{t+1}+\gamma^2R_{t+2}+\gamma^3V^\pi(S_{t+3})|S_t=s] \\ &\leq\mathbb{E}_{\pi^{\prime}}[R_t+\gamma R_{t+1}+\gamma^2R_{t+2}+\gamma^3R_{t+3}+\cdots|S_t=s] \\ &=V^{\pi^{\prime}}(s) \end{aligned}\]

可以看到,推導過程中的每一個時間步都用到區域性動作價值優勢\(V^{\pi}(S_{t+1})\leq Q^{\pi}(S_{t+1},\pi^{\prime}(S_{t+1}))\),累積到無窮步或者終止狀態時,我們就得到了整個策略價值提升的不等式。

4.3.3 策略迭代演算法

總體來說,策略迭代演算法的過程如下:對當前的策略進行策略評估,得到其狀態價值函式,然後根據該狀態價值函式進行策略提升以得到一個更好的新策略,接著繼續評估新策略、提升策略……直至最後收斂到最優策略(收斂性證明參見 4.7 節):

結合策略評估和策略提升,我們得到以下策略迭代演算法:

\(\text{隨機初始化策略 }\pi(s)\text{ 和價值函式 }V(s)\)

\(\mathrm{while~}\Delta>\theta\text{ do}:\quad(\text{策略評估迴圈})\)

\(\Delta\leftarrow0\)

\(\text{對於每一個狀態 }s\in\mathcal{S}{:}\)

\(v\leftarrow V(S)\)

\(V(s)\leftarrow r(s,\pi(s))+\gamma\sum_{s'}P(s'|s,\pi(s))V(s')\)

\(\Delta\leftarrow\max(\Delta,|v-V(s)|)\)

  • end while
  • $\pi_{\mathrm{old}}\leftarrow\pi $
  • \(\text{對於每一個狀態 }s\in\mathcal{S}{:}\)
  • \(\pi(s)\leftarrow\arg\max_ar(s,a)+\gamma\sum_{s^{\prime}}P(s^{\prime}|s,a)V(s^{\prime})\)
  • \(\pi_{\mathrm{old}}=\pi\),則停止演算法並返回\(V\)\(\pi\);否則轉到策略評估迴圈

我們現在來看一下策略迭代演算法的程式碼實現過程。

class PolicyIteration:
    """ 策略迭代演算法 """
    def __init__(self, env, theta, gamma):
        self.env = env
        self.v = [0] * self.env.ncol * self.env.nrow  # 初始化價值為0
        self.pi = [[0.25, 0.25, 0.25, 0.25]
                   for i in range(self.env.ncol * self.env.nrow)]  # 初始化為均勻隨機策略
        self.theta = theta  # 策略評估收斂閾值
        self.gamma = gamma  # 折扣因子

    def policy_evaluation(self):  # 策略評估
        cnt = 1  # 計數器
        while 1:
            max_diff = 0
            new_v = [0] * self.env.ncol * self.env.nrow
            for s in range(self.env.ncol * self.env.nrow):
                qsa_list = []  # 開始計算狀態s下的所有Q(s,a)價值
                for a in range(4):
                    qsa = 0
                    for res in self.env.P[s][a]:
                        p, next_state, r, done = res
                        qsa += p * (r + self.gamma * self.v[next_state] * (1 - done))
                        # 本章環境比較特殊,獎勵和下一個狀態有關,所以需要和狀態轉移機率相乘
                    qsa_list.append(self.pi[s][a] * qsa)
                new_v[s] = sum(qsa_list)  # 狀態價值函式和動作價值函式之間的關係
                max_diff = max(max_diff, abs(new_v[s] - self.v[s]))
            self.v = new_v
            if max_diff < self.theta: break  # 滿足收斂條件,退出評估迭代
            cnt += 1
        print("策略評估進行%d輪後完成" % cnt)

    def policy_improvement(self):  # 策略提升
        for s in range(self.env.nrow * self.env.ncol):
            qsa_list = []
            for a in range(4):
                qsa = 0
                for res in self.env.P[s][a]:
                    p, next_state, r, done = res
                    qsa += p * (r + self.gamma * self.v[next_state] * (1 - done))
                qsa_list.append(qsa)
            maxq = max(qsa_list)
            cntq = qsa_list.count(maxq)  # 計算有幾個動作得到了最大的Q值
            # 讓這些動作均分機率
            self.pi[s] = [1 / cntq if q == maxq else 0 for q in qsa_list]
        print("策略提升完成")
        return self.pi

    def policy_iteration(self):  # 策略迭代
        while 1:
            self.policy_evaluation()
            old_pi = copy.deepcopy(self.pi)  # 將列表進行深複製,方便接下來進行比較
            new_pi = self.policy_improvement()
            if old_pi == new_pi: break

現在我們已經寫好了環境程式碼和策略迭代程式碼。為了更好地展現最終的策略,接下來增加一個列印策略的函式,用於列印當前策略在每個狀態下的價值以及智慧體會採取的動作。對於列印出來的動作,我們用^o<o表示等機率採取向左和向上兩種動作,ooo>表示在當前狀態只採取向右動作。

def print_agent(agent, action_meaning, disaster=[], end=[]):
    print("狀態價值:")
    for i in range(agent.env.nrow):
        for j in range(agent.env.ncol):
            # 為了輸出美觀,保持輸出6個字元
            print('%6.6s' % ('%.3f' % agent.v[i * agent.env.ncol + j]), end=' ')
        print()

    print("策略:")
    for i in range(agent.env.nrow):
        for j in range(agent.env.ncol):
            # 一些特殊的狀態,例如懸崖漫步中的懸崖
            if (i * agent.env.ncol + j) in disaster:
                print('****', end=' ')
            elif (i * agent.env.ncol + j) in end:  # 目標狀態
                print('EEEE', end=' ')
            else:
                a = agent.pi[i * agent.env.ncol + j]
                pi_str = ''
                for k in range(len(action_meaning)):
                    pi_str += action_meaning[k] if a[k] > 0 else 'o'
                print(pi_str, end=' ')
        print()


env = CliffWalkingEnv()
action_meaning = ['^', 'v', '<', '>']
theta = 0.001
gamma = 0.9
agent = PolicyIteration(env, theta, gamma)
agent.policy_iteration()
print_agent(agent, action_meaning, list(range(37, 47)), [47])
策略評估進行60輪後完成
策略提升完成
策略評估進行72輪後完成
策略提升完成
策略評估進行44輪後完成
策略提升完成
策略評估進行12輪後完成
策略提升完成
策略評估進行1輪後完成
策略提升完成
狀態價值:
-7.712 -7.458 -7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710
-7.458 -7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710 -1.900
-7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710 -1.900 -1.000
-7.458  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000
策略:
ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovoo
ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovoo
ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ovoo
^ooo **** **** **** **** **** **** **** **** **** **** EEEE

經過 5 次策略評估和策略提升的迴圈迭代,策略收斂了,此時將獲得的策略列印出來。用貝爾曼最優方程去檢驗其中每一個狀態的價值,可以發現最終輸出的策略的確是最優策略。

4.4 價值迭代演算法

從上面的程式碼執行結果中我們能發現,策略迭代中的策略評估需要進行很多輪才能收斂得到某一策略的狀態函式,這需要很大的計算量,尤其是在狀態和動作空間比較大的情況下。我們是否必須要完全等到策略評估完成後再進行策略提升呢?試想一下,可能出現這樣的情況:雖然狀態價值函式還沒有收斂,但是不論接下來怎麼更新狀態價值,策略提升得到的都是同一個策略。如果只在策略評估中進行一輪價值更新,然後直接根據更新後的價值進行策略提升,這樣是否可以呢?答案是肯定的,這其實就是本節將要講解的價值迭代演算法,它可以被認為是一種策略評估只進行了一輪更新的策略迭代演算法。需要注意的是,價值迭代中不存在顯式的策略,我們只維護一個狀態價值函式。

確切來說,價值迭代可以看成一種動態規劃過程,它利用的是貝爾曼最優方程:

\[V^*(s)=\max_{a\in\mathcal{A}}\{r(s,a)+\gamma\sum_{s'\in\mathcal{S}}P(s'|s,a)V^*(s')\} \]

將其寫成迭代更新的方式為

\[V^{k+1}(s)=\max_{a\in\mathcal{A}}\{r(s,a)+\gamma\sum_{s'\in\mathcal{S}}P(s'|s,a)V^k(s')\} \]

價值迭代便是按照以上更新方式進行的。等到\(V^{k+1}\)\(V^k\)相同時,它就是貝爾曼最優方程的不動點,此時對應著最優狀態價值函式\(V^*\)。然後我們利用\(\pi(s)=\arg\max_a\{r(s,a)+\gamma\sum_{s^{\prime}}p(s^{\prime}|s,a)V^{k+1}(s^{\prime})\}\)​,從中恢復出最優策略即可。

價值迭代演算法流程如下:

\(\bullet\text{ 隨機初始化}V(s)\)

while \(\Delta>\theta\)​​ do :

\(\bullet\quad\Delta\leftarrow0\)

· 對於每一個狀態 \(s\in\mathcal{S}:\)

​ $ v\leftarrow V(s)$​

\(V(s)\leftarrow\max_ar(s,a)+\gamma\sum_{s^{\prime}}P(s^{\prime}|s,a)V(s^{\prime})\)

\(\Delta\leftarrow\max(\Delta,|v-V(s)|)\)

· end while

·返回一個確定性策略 \(\pi(s)=\arg\max_a\{r(s,a)+\gamma\sum_{s^{\prime}}P(s^{\prime}|s,a)V(s^{\prime})\}\)

我們現在來編寫價值迭代的程式碼。

class ValueIteration:
    """ 價值迭代演算法 """
    def __init__(self, env, theta, gamma):
        self.env = env
        self.v = [0] * self.env.ncol * self.env.nrow  # 初始化價值為0
        self.theta = theta  # 價值收斂閾值
        self.gamma = gamma
        # 價值迭代結束後得到的策略
        self.pi = [None for i in range(self.env.ncol * self.env.nrow)]

    def value_iteration(self):
        cnt = 0
        while 1:
            max_diff = 0
            new_v = [0] * self.env.ncol * self.env.nrow
            for s in range(self.env.ncol * self.env.nrow):
                qsa_list = []  # 開始計算狀態s下的所有Q(s,a)價值
                for a in range(4):
                    qsa = 0
                    for res in self.env.P[s][a]:
                        p, next_state, r, done = res
                        qsa += p * (r + self.gamma * self.v[next_state] * (1 - done))
                    qsa_list.append(qsa)  # 這一行和下一行程式碼是價值迭代和策略迭代的主要區別
                new_v[s] = max(qsa_list)
                max_diff = max(max_diff, abs(new_v[s] - self.v[s]))
            self.v = new_v
            if max_diff < self.theta: break  # 滿足收斂條件,退出評估迭代
            cnt += 1
        print("價值迭代一共進行%d輪" % cnt)
        self.get_policy()

    def get_policy(self):  # 根據價值函式匯出一個貪婪策略
        for s in range(self.env.nrow * self.env.ncol):
            qsa_list = []
            for a in range(4):
                qsa = 0
                for res in self.env.P[s][a]:
                    p, next_state, r, done = res
                    qsa += p * (r + self.gamma * self.v[next_state] * (1 - done))
                qsa_list.append(qsa)
            maxq = max(qsa_list)
            cntq = qsa_list.count(maxq)  # 計算有幾個動作得到了最大的Q值
            # 讓這些動作均分機率
            self.pi[s] = [1 / cntq if q == maxq else 0 for q in qsa_list]


env = CliffWalkingEnv()
action_meaning = ['^', 'v', '<', '>']
theta = 0.001
gamma = 0.9
agent = ValueIteration(env, theta, gamma)
agent.value_iteration()
print_agent(agent, action_meaning, list(range(37, 47)), [47])
價值迭代一共進行14輪
狀態價值:
-7.712 -7.458 -7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710
-7.458 -7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710 -1.900
-7.176 -6.862 -6.513 -6.126 -5.695 -5.217 -4.686 -4.095 -3.439 -2.710 -1.900 -1.000
-7.458  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000
策略:
ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovoo
ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovo> ovoo
ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ovoo
^ooo **** **** **** **** **** **** **** **** **** **** EEEE

可以看到,解決同樣的訓練任務,價值迭代總共進行了數十輪,而策略迭代中的策略評估總共進行了數百輪,價值迭代中的迴圈次數遠少於策略迭代。

4.5 冰湖環境

除了懸崖漫步環境,本章還準備了另一個環境——冰湖(Frozen Lake)。冰湖環境的狀態空間和動作空間是有限的,我們在該環境中也嘗試一下策略迭代演算法和價值迭代演算法,以便更好地理解這兩個演算法。

冰湖是 OpenAI Gym 庫中的一個環境。OpenAI Gym 庫中包含了很多有名的環境,例如 Atari 和 MuJoCo,並且支援我們定製自己的環境。在之後的章節中,我們還會使用到更多來自 OpenAI Gym 庫的環境。如圖 4-2 所示,冰湖環境和懸崖漫步環境相似,也是一個網格世界,大小為\(4\times4\)。每一個方格是一個狀態,智慧體起點狀態\(S\)在左上角,目標狀態\(G\)在右下角,中間還有若干冰洞\(H\)​。在每一個狀態都可以採取上、下、左、右 4 個動作。由於智慧體在冰面行走,因此每次行走都有一定的機率 滑行到附近的其它狀態,並且到達冰洞或目標狀態時行走會提前結束。每一步行走的獎勵是0,到達目標的獎勵是 1。

img

圖4-2 Frozen Lake環境示意圖

我們先建立 OpenAI Gym 中的 FrozenLake-v0 環境,並簡單檢視環境資訊,然後找出冰洞和目標狀態。

import gym
env = gym.make("FrozenLake-v0")  # 建立環境
env = env.unwrapped  # 解封裝才能訪問狀態轉移矩陣P
env.render()  # 環境渲染,通常是彈窗顯示或列印出視覺化的環境

holes = set()
ends = set()
for s in env.P:
    for a in env.P[s]:
        for s_ in env.P[s][a]:
            if s_[2] == 1.0:  # 獲得獎勵為1,代表是目標
                ends.add(s_[1])
            if s_[3] == True:
                holes.add(s_[1])
holes = holes - ends
print("冰洞的索引:", holes)
print("目標的索引:", ends)

for a in env.P[14]:  # 檢視目標左邊一格的狀態轉移資訊
    print(env.P[14][a])
SFFF
FHFH
FFFH
HFFG
冰洞的索引: {11, 12, 5, 7}
目標的索引: {15}
[(0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 13, 0.0, False), (0.3333333333333333, 14, 0.0, False)]
[(0.3333333333333333, 13, 0.0, False), (0.3333333333333333, 14, 0.0, False), (0.3333333333333333, 15, 1.0, True)]
[(0.3333333333333333, 14, 0.0, False), (0.3333333333333333, 15, 1.0, True), (0.3333333333333333, 10, 0.0, False)]
[(0.3333333333333333, 15, 1.0, True), (0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 13, 0.0, False)]

首先,我們發現冰洞的索引是\(\{5,7,11,12\}\)(集合 set 的索引是無序的),起點狀態(索引為 0) 在左上角,和懸崖漫步環境一樣。其次,根據第 15 個狀態 (即目標左邊一格,陣列下標索引為 14) 的資訊,我們可以看到每個動作都會等機率“滑行”到3 種可能的結果,這一點和懸崖漫步環境是不一樣的。我們接下來先在冰湖環境中嘗試一下策略迭代演算法。

# 這個動作意義是Gym庫針對冰湖環境事先規定好的
action_meaning = ['<', 'v', '>', '^']
theta = 1e-5
gamma = 0.9
agent = PolicyIteration(env, theta, gamma)
agent.policy_iteration()
print_agent(agent, action_meaning, [5, 7, 11, 12], [15])
策略評估進行25輪後完成
策略提升完成
策略評估進行58輪後完成
策略提升完成
狀態價值:
 0.069  0.061  0.074  0.056
 0.092  0.000  0.112  0.000
 0.145  0.247  0.300  0.000
 0.000  0.380  0.639  0.000
策略:
<ooo ooo^ <ooo ooo^
<ooo **** <o>o ****
ooo^ ovoo <ooo ****
**** oo>o ovoo EEEE

這個最優策略很看上去比較反直覺,其原因是這是一個智慧體會隨機滑向其他狀態的冰凍湖面。例如,在目標左邊一格的狀態,採取向右的動作時,它有可能會滑到目標左上角的位置,從該位置再次到達目標會更加困難,所以此時採取向下的動作是更為保險的,並且有一定機率能夠滑到目標。我們再來嘗試一下價值迭代演算法。

action_meaning = ['<', 'v', '>', '^']
theta = 1e-5
gamma = 0.9
agent = ValueIteration(env, theta, gamma)
agent.value_iteration()
print_agent(agent, action_meaning, [5, 7, 11, 12], [15])
價值迭代一共進行60輪
狀態價值:
 0.069  0.061  0.074  0.056
 0.092  0.000  0.112  0.000
 0.145  0.247  0.300  0.000
 0.000  0.380  0.639  0.000
策略:
<ooo ooo^ <ooo ooo^
<ooo **** <o>o ****
ooo^ ovoo <ooo ****
**** oo>o ovoo EEEE

可以發現價值迭代演算法的結果和策略迭代演算法的結果完全一致,這也互相驗證了各自的結果。

4.6 小結

本章講解了強化學習中兩個經典的動態規劃演算法:策略迭代演算法和價值迭代演算法,它們都能用於求解最優價值和最優策略。動態規劃的主要思想是利用貝爾曼方程對所有狀態進行更新。需要注意的是,在利用貝爾曼方程進行狀態更新時,我們會用到馬爾可夫決策過程中的獎勵函式和狀態轉移函式。如果智慧體無法事先得知獎勵函式和狀態轉移函式,就只能透過和環境進行互動來取樣(狀態-動作-獎勵-下一狀態)這樣的資料,我們將在之後的章節中講解如何求解這種情況下的最優策略。

4.7 擴充套件閱讀:收斂性證明

4.7.1 策略迭代

策略迭代的過程如下:

\[\pi^0\overset{\text{ 策略評估}}{ \operatorname* { \longrightarrow }}V^{\pi^0}\overset{\text{策略提升}}{ \operatorname* { \longrightarrow }}\pi^1\overset{\text{策略評估}}{ \operatorname* { \longrightarrow }}V^{\pi^1}\overset{\text{策略提升}}{ \operatorname* { \longrightarrow }}\pi^2\overset{\text{策略評估}}{\operatorname*{\longrightarrow}}\ldots\overset{\text{策略提升}}{\operatorname*{\longrightarrow}}\pi^* \]

根據策略提升定理,我們知道更新後的策略的價值函式滿足單調性,即\(V^{\pi^{k+1}}\geq V^{\pi^k}\)。所以只要所有可能的策略個數是有限的,策略迭代就能收斂到最優策略。假設 MDP 的狀態空間大小為\(|S|\),動作空間大小為\(|A|\),此時所有可能的策略個數為
\(|\mathcal{A}|^{|S|}\),是有限個,所以策略迭代能夠在有限步找到其中的最優策略。
還有另一種類似的證明思路。在有限馬爾可夫決策過程中,如果\(\gamma<1\),那麼很顯然存在一個上界\(C=R_{\max}/(1-\gamma)\)(這裡
\(R_\mathrm{max}\)為最大單步獎勵值),使得對於任意策略\(\pi\)和狀態\(s\),其價值\(V^{\pi}(s)<C\)。因此,對於每個狀態\(s\),我們可以將策略迭代得到的價值寫成數列\(\{V^{\pi^k}\}_{k=1\ldots\infty}\)​。根據實數列的單調有界收斂定理,該數列一定收斂,也即是策略迭代演算法一定收斂。

4.7.2 價值迭代

價值迭代的更新公式為:

\[V^{k+1}(s)=\max_{a\in\mathcal{A}}\{r(s,a)+\gamma\sum_{s'\in\mathcal{S}}P(s'|s,a)V^k(s')\} \]

我們將其定義為一個貝爾曼最優運算元T:

\[V^{k+1}(s)=\mathcal{T}V^k(s)=\max_{a\in\mathcal{A}}\{r(s,a)+\gamma\sum_{s'\in\mathcal{S}}P(s'|s,a)V^k(s')\} \]

然後我們引入壓縮運算元 (contraction operator): 若\(O\)是一個運算元,如果滿足\(\|OV-OV^{\prime}\|_q\leq\|V-V^{\prime}\|_q\)條件,則我們稱\(O\)是一個壓縮運算元。其中\(\|x\|_q\)表示\(x\)\(L_q\)範數,包括我們將會用到的無窮範數\(\|x\|_\infty=\max_i|x_i|\)
我們接下來證明當\(\gamma<1\)時,貝爾曼最優運算元\(\mathcal{T}\)是一個\(\gamma\)-壓縮運算元。

\[\begin{aligned} \|\mathcal{T}V-\mathcal{T}V^{\prime}\|_{\infty}& =\max_{s\in\mathcal{S}}|\max_{a\in\mathcal{A}}\{r(s,a)+\gamma\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a)V(s^{\prime})\}-\max_{a^{\prime}\in\mathcal{A}}\{r(s,a^{\prime})+\gamma\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a^{\prime})V^{\prime}(s^{\prime})\} \\ &\leq\max_{s,a}\left|r(s,a)+\gamma\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a)V(s^{\prime})-r(s,a)-\gamma\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a)V^{\prime}(s^{\prime})\right| \\ &=\gamma\max_{s,a}\left|\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a)(V(s^{\prime})-V^{\prime}(s^{\prime}))\right. \\ &\leq\gamma\max_{s,a}\sum_{s^{\prime}\in\mathcal{S}}P(s^{\prime}|s,a)\max_{s^{\prime}}|V(s^{\prime})-V^{\prime}(s^{\prime})| \\ &=\gamma\|V-V^{\prime}\|_{\infty} \end{aligned} \]

\(V^{\prime}\)設為最優價值函式\(V^*\),於是有:

\[\|V^{k+1}-V^*\|_\infty=\|\mathcal{T}V^k-\mathcal{T}V^*\|_\infty\leq\gamma\|V^k-V^*\|_\infty\leq\cdots\leq\gamma^{k+1}\|V^0-V^*\|_\infty \]

這意味著,在\(\gamma\leq1\)的情況下,隨著迭代次數\(k\)越來越大,\(V^k\)會越來越接近\(V^*\),即\(\lim_{k\to\infty}V^k=V^*\)​。至此,價值迭代的收斂性得到證明。

4.8 參考文獻

[1] GERAMIFARD A, WALSN T J, TELLEX S, et al. A tutorial on linear function approximators for dynamic programming and reinforcement learning [J]. Foundations and Trends®in Machine Learning, 2013, 6 (4): 375-451.

[2] SZEPESVÁRI C, LITTMAN M L. Generalized markov decision processes: dynamic-programming and reinforcement-learning algorithms [C]//International Conference of Machine Learning, 1996: 96.

相關文章