动手学强化学习——动态规划算法 强化学习公式 空矩阵的构建 deepcopy gym中env的unwrapped

第四章动态规划算法

4.1 简介

动态规划是程序设计算法中非常重要的内容,能够高效解决一些经典问题。动态规划的基本思想是将待求解的问题分解成若干个子问题,然后从这些子问题的解得到目标问题的解。动态规划会保存已解决的子问题的答案,在求解目标问题的过程中,需要这些子问题答案时就可以直接利用,避免重复计算。本章介绍如何用动态规划思想来求解在马尔可夫决策过程中的最优策略。

基于动态规划的强化学习算法主要是有两种:一种是策略迭代,二是价值迭代。其中,策略迭代由两部分组成:策略评估和策略提升。具体来说,策略迭代中的策略评估使用贝尔曼期望方程来得到一个策略的状态价值函数,这是一个动态规划的过程;而价值迭代直接使用贝尔曼最优方程来进行动态规划,得到最终的最优动态价值。

基于动态规划的这两种强化学习算法要求事先知道环境的状态转移函数和奖励函数,也就是需要知道整个马尔可夫决策过程。在这样一个白盒环境中,不需要通过智能体和环境的大量交互来学习,可以直接用动态规划求解状态价值函数。但是,现实中的白盒环境很少,这也是动态规划算法的局限之处,我们无法将其运用到很多实际场景中。另外,策略迭代和价值迭代通常只适用于有限马尔可夫决策过程,即状态空间和动作空间是离散且有限的。

4.2 悬崖漫步环境

本节使用策略迭代和价值迭代来求解悬崖漫步(Cliff Walking)这个环境中的最优策略。接下来先简单
介绍一下该环境。

悬崖漫步是一个非常经典的强化学习环境,它要求一个智能体从起点出发,避开悬崖行走,最终到达目标位置。如图 4-1 所示,有一个 4×12 的网格世界,每一个网格表示一个状态。智能体的起点是左下角的状态,目标是右下角的状态,智能体在每一个状态都可以采取 4 种动作:上、下、左、右。如果智能体采取动作后触碰到边界墙壁则状态不发生改变,否则就会相应到达下一个状态。环境中有一段悬崖,智能体掉入悬崖或到达目标状态都会结束动作并回到起点,也就是说掉入悬崖或者达到目标状态是终止状态。智能体每走一步的奖励是 −1,掉入悬崖的奖励是 −100。

在这里插入图片描述

构造悬崖漫步的环境代码:(即转移矩阵P[state][action] = [(p, next_state, reward, done)]包含下一个状态和奖励)

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)]
         # print(P)
        # 4种动作, change[0]:上,change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
        # 定义在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]  # change也是一个矩阵 第一个位置为动作上  第二个是动作下  第三个是动作左   第四个是右
        # [[0, -1], [0, 1], [-1, 0], [1, 0]]
        # print(change)
        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]))
                    # print("a=", a)
                    # print([change[a][0]])
                    next_y = min(self.nrow - 1, max(0, i + change[a][1]))
                    # change[a][0]和change[a][1]一起构成动作方向
                    next_state = next_y * self.ncol + next_x  # 给地图中的4*12个位置进行标序号
                    # print("next_state=", next_state)
                    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
i j01234567891011
001234567891011
1121314151617181920212223
2242526272829303132333435
3363738394041424344454647

(0,0)为起始状态 (3,11)即序号47为终点状态,序号37-46为悬崖边上

本段代码主要将环境分为两大部分

  • 第一部分是普通状态
    • 普通状态的下一状态还是普通状态 reward=-1,done=false
    • 普通状态的下一状态为悬崖 reward=-100,done=true
  • 第二部分为悬崖边上 reward=0,done=true

位置在36时且动作为下及之前的所有可能动作

[36, 3]
[[[(1, 0, -1, False)], [(1, 12, -1, False)], [(1, 0, -1, False)], [(1, 1, -1, False)]], [[(1, 1, -1, False)], [(1, 13, -1, False)], [(1, 0, -1, False)], [(1, 2, -1, False)]], [[(1, 2, -1, False)], [(1, 14, -1, False)], [(1, 1, -1, False)], [(1, 3, -1, False)]], [[(1, 3, -1, False)], [(1, 15, -1, False)], [(1, 2, -1, False)], [(1, 4, -1, False)]], [[(1, 4, -1, False)], [(1, 16, -1, False)], [(1, 3, -1, False)], [(1, 5, -1, False)]], [[(1, 5, -1, False)], [(1, 17, -1, False)], [(1, 4, -1, False)], [(1, 6, -1, False)]], [[(1, 6, -1, False)], [(1, 18, -1, False)], [(1, 5, -1, False)], [(1, 7, -1, False)]], [[(1, 7, -1, False)], [(1, 19, -1, False)], [(1, 6, -1, False)], [(1, 8, -1, False)]], [[(1, 8, -1, False)], [(1, 20, -1, False)], [(1, 7, -1, False)], [(1, 9, -1, False)]], [[(1, 9, -1, False)], [(1, 21, -1, False)], [(1, 8, -1, False)], [(1, 10, -1, False)]], [[(1, 10, -1, False)], [(1, 22, -1, False)], [(1, 9, -1, False)], [(1, 11, -1, False)]], [[(1, 11, -1, False)], [(1, 23, -1, False)], [(1, 10, -1, False)], [(1, 11, -1, False)]], [[(1, 0, -1, False)], [(1, 24, -1, False)], [(1, 12, -1, False)], [(1, 13, -1, False)]], [[(1, 1, -1, False)], [(1, 25, -1, False)], [(1, 12, -1, False)], [(1, 14, -1, False)]], [[(1, 2, -1, False)], [(1, 26, -1, False)], [(1, 13, -1, False)], [(1, 15, -1, False)]], [[(1, 3, -1, False)], [(1, 27, -1, False)], [(1, 14, -1, False)], [(1, 16, -1, False)]], [[(1, 4, -1, False)], [(1, 28, -1, False)], [(1, 15, -1, False)], [(1, 17, -1, False)]], [[(1, 5, -1, False)], [(1, 29, -1, False)], [(1, 16, -1, False)], [(1, 18, -1, False)]], [[(1, 6, -1, False)], [(1, 30, -1, False)], [(1, 17, -1, False)], [(1, 19, -1, False)]], [[(1, 7, -1, False)], [(1, 31, -1, False)], [(1, 18, -1, False)], [(1, 20, -1, False)]], [[(1, 8, -1, False)], [(1, 32, -1, False)], [(1, 19, -1, False)], [(1, 21, -1, False)]], [[(1, 9, -1, False)], [(1, 33, -1, False)], [(1, 20, -1, False)], [(1, 22, -1, False)]], [[(1, 10, -1, False)], [(1, 34, -1, False)], [(1, 21, -1, False)], [(1, 23, -1, False)]], [[(1, 11, -1, False)], [(1, 35, -1, False)], [(1, 22, -1, False)], [(1, 23, -1, False)]], [[(1, 12, -1, False)], [(1, 36, -1, False)], [(1, 24, -1, False)], [(1, 25, -1, False)]], [[(1, 13, -1, False)], [(1, 37, -100, True)], [(1, 24, -1, False)], [(1, 26, -1, False)]], [[(1, 14, -1, False)], [(1, 38, -100, True)], [(1, 25, -1, False)], [(1, 27, -1, False)]], [[(1, 15, -1, False)], [(1, 39, -100, True)], [(1, 26, -1, False)], [(1, 28, -1, False)]], [[(1, 16, -1, False)], [(1, 40, -100, True)], [(1, 27, -1, False)], [(1, 29, -1, False)]], [[(1, 17, -1, False)], [(1, 41, -100, True)], [(1, 28, -1, False)], [(1, 30, -1, False)]], [[(1, 18, -1, False)], [(1, 42, -100, True)], [(1, 29, -1, False)], [(1, 31, -1, False)]], [[(1, 19, -1, False)], [(1, 43, -100, True)], [(1, 30, -1, False)], [(1, 32, -1, False)]], [[(1, 20, -1, False)], [(1, 44, -100, True)], [(1, 31, -1, False)], [(1, 33, -1, False)]], [[(1, 21, -1, False)], [(1, 45, -100, True)], [(1, 32, -1, False)], [(1, 34, -1, False)]], [[(1, 22, -1, False)], [(1, 46, -100, True)], [(1, 33, -1, False)], [(1, 35, -1, False)]], [[(1, 23, -1, False)], [(1, 47, -1, True)], [(1, 34, -1, False)], [(1, 35, -1, False)]], [[(1, 24, -1, False)], [(1, 36, -1, False)], [(1, 36, -1, False)], [(1, 37, -100, True)]], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []]]

最后p的所有可能

[[[(1, 0, -1, False)], [(1, 12, -1, False)], [(1, 0, -1, False)], [(1, 1, -1, False)]], [[(1, 1, -1, False)], [(1, 13, -1, False)], [(1, 0, -1, False)], [(1, 2, -1, False)]], [[(1, 2, -1, False)], [(1, 14, -1, False)], [(1, 1, -1, False)], [(1, 3, -1, False)]], [[(1, 3, -1, False)], [(1, 15, -1, False)], [(1, 2, -1, False)], [(1, 4, -1, False)]], [[(1, 4, -1, False)], [(1, 16, -1, False)], [(1, 3, -1, False)], [(1, 5, -1, False)]], [[(1, 5, -1, False)], [(1, 17, -1, False)], [(1, 4, -1, False)], [(1, 6, -1, False)]], [[(1, 6, -1, False)], [(1, 18, -1, False)], [(1, 5, -1, False)], [(1, 7, -1, False)]], [[(1, 7, -1, False)], [(1, 19, -1, False)], [(1, 6, -1, False)], [(1, 8, -1, False)]], [[(1, 8, -1, False)], [(1, 20, -1, False)], [(1, 7, -1, False)], [(1, 9, -1, False)]], [[(1, 9, -1, False)], [(1, 21, -1, False)], [(1, 8, -1, False)], [(1, 10, -1, False)]], [[(1, 10, -1, False)], [(1, 22, -1, False)], [(1, 9, -1, False)], [(1, 11, -1, False)]], [[(1, 11, -1, False)], [(1, 23, -1, False)], [(1, 10, -1, False)], [(1, 11, -1, False)]], [[(1, 0, -1, False)], [(1, 24, -1, False)], [(1, 12, -1, False)], [(1, 13, -1, False)]], [[(1, 1, -1, False)], [(1, 25, -1, False)], [(1, 12, -1, False)], [(1, 14, -1, False)]], [[(1, 2, -1, False)], [(1, 26, -1, False)], [(1, 13, -1, False)], [(1, 15, -1, False)]], [[(1, 3, -1, False)], [(1, 27, -1, False)], [(1, 14, -1, False)], [(1, 16, -1, False)]], [[(1, 4, -1, False)], [(1, 28, -1, False)], [(1, 15, -1, False)], [(1, 17, -1, False)]], [[(1, 5, -1, False)], [(1, 29, -1, False)], [(1, 16, -1, False)], [(1, 18, -1, False)]], [[(1, 6, -1, False)], [(1, 30, -1, False)], [(1, 17, -1, False)], [(1, 19, -1, False)]], [[(1, 7, -1, False)], [(1, 31, -1, False)], [(1, 18, -1, False)], [(1, 20, -1, False)]], [[(1, 8, -1, False)], [(1, 32, -1, False)], [(1, 19, -1, False)], [(1, 21, -1, False)]], [[(1, 9, -1, False)], [(1, 33, -1, False)], [(1, 20, -1, False)], [(1, 22, -1, False)]], [[(1, 10, -1, False)], [(1, 34, -1, False)], [(1, 21, -1, False)], [(1, 23, -1, False)]], [[(1, 11, -1, False)], [(1, 35, -1, False)], [(1, 22, -1, False)], [(1, 23, -1, False)]], [[(1, 12, -1, False)], [(1, 36, -1, False)], [(1, 24, -1, False)], [(1, 25, -1, False)]], [[(1, 13, -1, False)], [(1, 37, -100, True)], [(1, 24, -1, False)], [(1, 26, -1, False)]], [[(1, 14, -1, False)], [(1, 38, -100, True)], [(1, 25, -1, False)], [(1, 27, -1, False)]], [[(1, 15, -1, False)], [(1, 39, -100, True)], [(1, 26, -1, False)], [(1, 28, -1, False)]], [[(1, 16, -1, False)], [(1, 40, -100, True)], [(1, 27, -1, False)], [(1, 29, -1, False)]], [[(1, 17, -1, False)], [(1, 41, -100, True)], [(1, 28, -1, False)], [(1, 30, -1, False)]], [[(1, 18, -1, False)], [(1, 42, -100, True)], [(1, 29, -1, False)], [(1, 31, -1, False)]], [[(1, 19, -1, False)], [(1, 43, -100, True)], [(1, 30, -1, False)], [(1, 32, -1, False)]], [[(1, 20, -1, False)], [(1, 44, -100, True)], [(1, 31, -1, False)], [(1, 33, -1, False)]], [[(1, 21, -1, False)], [(1, 45, -100, True)], [(1, 32, -1, False)], [(1, 34, -1, False)]], [[(1, 22, -1, False)], [(1, 46, -100, True)], [(1, 33, -1, False)], [(1, 35, -1, False)]], [[(1, 23, -1, False)], [(1, 47, -1, True)], [(1, 34, -1, False)], [(1, 35, -1, False)]], [[(1, 24, -1, False)], [(1, 36, -1, False)], [(1, 36, -1, False)], [(1, 37, -100, True)]], [[(1, 37, 0, True)], [(1, 37, 0, True)], [(1, 37, 0, True)], [(1, 37, 0, True)]], [[(1, 38, 0, True)], [(1, 38, 0, True)], [(1, 38, 0, True)], [(1, 38, 0, True)]], [[(1, 39, 0, True)], [(1, 39, 0, True)], [(1, 39, 0, True)], [(1, 39, 0, True)]], [[(1, 40, 0, True)], [(1, 40, 0, True)], [(1, 40, 0, True)], [(1, 40, 0, True)]], [[(1, 41, 0, True)], [(1, 41, 0, True)], [(1, 41, 0, True)], [(1, 41, 0, True)]], [[(1, 42, 0, True)], [(1, 42, 0, True)], [(1, 42, 0, True)], [(1, 42, 0, True)]], [[(1, 43, 0, True)], [(1, 43, 0, True)], [(1, 43, 0, True)], [(1, 43, 0, True)]], [[(1, 44, 0, True)], [(1, 44, 0, True)], [(1, 44, 0, True)], [(1, 44, 0, True)]], [[(1, 45, 0, True)], [(1, 45, 0, True)], [(1, 45, 0, True)], [(1, 45, 0, True)]], [[(1, 46, 0, True)], [(1, 46, 0, True)], [(1, 46, 0, True)], [(1, 46, 0, True)]], [[(1, 47, 0, True)], [(1, 47, 0, True)], [(1, 47, 0, True)], [(1, 47, 0, True)]]]

在[36,3]p没有全部展现出来是因为只进行到 continue位置 就结束了本次循环 代码没有执行到print(P)

4.3策略迭代算法

策略迭代是策略评估和策略提升不断循环交替,直至最后得到最优策略的过程。本节分别对这两个过程进行详细介绍。

4.3.1策略评估

策略评估这一过程用来计算一个策略的状态价值函数。回顾一下之前学习的贝尔曼期望方程:

V π ( s ) = Σ a ∈ A π ( a ∣ s ) ( r ( s , a ) + γ Σ s ′ ∈ S P ( s ′ ∣ s , a ) V π ( s ′ ) ) V^{\pi}\left( s \right) =\underset{a\in A}{\varSigma}\pi \left( a|s \right) (r\left( s,a \right) +\gamma \underset{s'\in S}{\varSigma}P\left( s'|s,a \right) V^{\pi}\left( s' \right) ) Vπ(s)=aAΣπ(as)(r(s,a)+γsSΣP(ss,a)Vπ(s))

其中, π ( a ∣ s ) \pi \left( a|s \right) π(as)是策略 π \pi π在状态 s s s下采取动作 a a a的概率。可以看到,当知道奖励函数和状态转移函数时,我们可以根据下一个状态的价值来计算当前状态的价值。因此,根据动态规划的思想,可以把计算下一个可能状态的价值当成一个子问题,把计算当前状态的价值看作当前问题。在得知子问题的解后,就可以求解当前问题。更一般的,考虑所有的状态,就变成了用上一轮的状态价值函数来计算当前这一轮的状态价值函数,即

V k + 1 ( s ) = Σ a ∈ A π ( a ∣ s ) ( r ( s , a ) + γ Σ s ′ ∈ S P ( s ′ ∣ s , a ) V k ( s ′ ) ) V^{k+1}\left( s \right) =\underset{a\in A}{\varSigma}\pi \left( a|s \right) (r\left( s,a \right) +\gamma \underset{s'\in S}{\varSigma}P\left( s'|s,a \right) V^{k}\left( s' \right) ) Vk+1(s)=aAΣπ(as)(r(s,a)+γsSΣP(ss,a)Vk(s))

我们可以选定任意初始值 V 0 V^0 V0。根据贝尔曼期望方程,可以得知 V k = V π V^k=V^{\pi} Vk=Vπ是以上更新公式的一个不动点(fixed point)。事实上,可以证明当时,序列 V k {V^k} Vk会收敛到 V π V^{\pi} Vπ,所以可以据此来计算得到一个策略的状态价值函数。可以看到,由于需要不断做贝尔曼期望方程迭代,策略评估其实会耗费很大的计算代价。在实际的实现过程中,如果某一轮 m a x s ′ ∈ S ∣ V k + 1 ( s ) − V k ( s ) ∣ max_{s'\in S}|V^{k+1}(s)-V^k(s)| maxsSVk+1(s)Vk(s)的值非常小,可以提前结束策略评估。这样做可以提升效率,并且得到的价值也非常接近真实的价值。

4.3.2策略提升

在这里插入图片描述

4.3.3策略迭代算法

总体来说,策略迭代算法的过程如下:对当前的策略进行策略评估,得到其状态价值函数,然后根据该状态价值函数进行策略提升以得到一个更好的新策略,接着继续评估新策略、提升策略…直至最后收敛到最优策略(收敛性证明参见4.7节)
π 0 ⟶ 策略评估 V π 0 ⟶ 策略提升 π 1 ⟶ 策略评估 V π 1 ⟶ 策略提升 π 2 ⟶ 策略评估 . . . . . . ⟶ 策略提升 π ∗ \pi ^0\overset{\text{策略评估}}{\longrightarrow}V^{\pi ^0}\overset{\text{策略提升}}{\longrightarrow}\pi ^1\overset{\text{策略评估}}{\longrightarrow}V^{\pi ^1}\overset{\text{策略提升}}{\longrightarrow}\pi ^2\overset{\text{策略评估}}{\longrightarrow}......\overset{\text{策略提升}}{\longrightarrow}\pi ^* π0策略评估Vπ0策略提升π1策略评估Vπ1策略提升π2策略评估......策略提升π
结合策略评估和策略提升,我们得到以下策略迭代算法:

  • 随机初始化策略 π ( s ) \pi(s) π(s)和价值函数 V ( s ) V(s) V(s)
  • while Δ > θ \varDelta > \theta Δ>θ do:(策略评估循环)
  • Δ < − − 0 \Delta<--0 Δ<0
  • ​ 对于每一个状态 s ∈ S s\in S sS:
  • v < − − V ( s ) v<--V(s) v<V(s)
  • V ( s ) < − − r ( s , π ( s ) ) + γ Σ s ′ P ( s ′ ∣ s , π ( s ) ) V ( s ′ ) V(s)<--r(s,\pi(s))+\gamma{\varSigma}_{s'}P( s'|s,\pi(s)) V( s') V(s)<r(s,π(s))+γΣsP(ss,π(s))V(s)
  • Δ < − − m a x ( Δ , ∣ v − V ( s ) ∣ ) \Delta<--max(\Delta,|v-V(s)|) Δ<max(Δ,vV(s))
  • end while
  • π o l d < − − π \pi_{old}<--\pi πold<π
  • 对于每一个状态 s ∈ S : s\in S: sS:
  • π ( s ) < − − a r g m a x a r ( s , a ) + γ Σ s ′ P ( s ′ ∣ s , π ( s ) ) V ( s ′ ) \pi(s)<--arg max_a r(s,a)+\gamma{\varSigma}_{s'}P( s'|s,\pi(s)) V( s') π(s)<argmaxar(s,a)+γΣsP(ss,π(s))V(s)
  • π o l d = π \pi_{old}=\pi πold=π,则停止算法并返回V和 π \pi π;否则转到策略评估循环
class PolicyIteration:
    """ 策略迭代算法 """
    def __init__(self, env, theta, gamma):
        self.env = env
        self.v = [0] * self.env.ncol * self.env.nrow  # 初始化状态价值为0
        # print("v=",self.v) # 输出是:v= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        self.pi = [[0.25, 0.25, 0.25, 0.25]
                   for i in range(self.env.ncol * self.env.nrow)]  # 初始化为均匀随机策略
        # print(("pi= ",self.pi)) 输出是12*4个[0.25, 0.25, 0.25, 0.25]
        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)价值  存储的内容是在状态s下  选择每个动作的价值 共四个动作价值
                for a in range(4):
                    qsa = 0
                    for res in self.env.P[s][a]:   # s为悬崖漫步环境中的状态序号  a为向上下左右的动作   也即为环境最后的输出 有(p, next_state, reward, done)
                        p, next_state, r, done = res
                        qsa += p * (r + self.gamma * self.v[next_state] * (1 - done))
                        # print(self.v[next_state])
                        # print("qsa=", qsa)
                        # 本章环境比较特殊,奖励和下一个状态有关,所以需要和状态转移概率相乘
                    qsa_list.append(self.pi[s][a] * qsa)
                # print(qsa_list)
                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)  # 贪心策略  选出在这个状态下动作价值最高的动作
            # print(qsa_list)
            cntq = qsa_list.count(maxq)  # 计算有几个动作得到了最大的Q值
            # 让这些动作均分概率
            self.pi[s] = [1 / cntq if q == maxq else 0 for q in qsa_list]  # 将得到的概率分别放入对应的动作位置
            # print(self.pi[s])
        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

# p = PolicyIteration(CliffWalkingEnv(), 0.01, 0.5)
# p.policy_evaluation()
# p.policy_improvement()


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])

代码思路是:先进行策略评估,策略评估的思想是将策略评估进行多次循环,使用本次策略评估的结果当作下一次策略评估的下一状态价值(本次策略评估得到所有状态的价值下一次使用时用的是当前当前时刻但是是下一状态的状态价值,相当于在一下一时刻已知了每个状态的状态价值,可能不是很准确,所以需要迭代多次使状态价值收敛),使其接近 V π V^\pi Vπ状态价值,直到两次策略评估的结果差比较小,或者说两次策略评估的结果无限接近时,评估停止,进行策略提升,策略提升的思路是:将当前状态的四个价值分别进行计算,选择动作价值最高的动作,当两次策略提升的结果相同时,结束循环。

现在我们已经写好了环境代码和策略迭代代码。为了更好地展现最终的策略,接下来增加一个打印策略的函数,用于打印当前策略在每个状态下的价值以及智能体会采取的动作。对于打印出来的动作,我们用^o<o表示等概率采取向左和向上两种动作,ooo>表示在当前状态只采取向右动作。

输出结果:

策略评估进行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

4.4 价值迭代算法

从上面的代码运行结果中我们能发现,策略迭代中的策略评估需要进行很多轮才能收敛得到某一策略的状态函数,这需要很大的计算量,尤其是在状态和动作空间比较大的情况下。我们是否必须要完全等到策略评估完成后再进行策略提升呢?试想一下,可能出现这样的情况:虽然状态价值函数还没有收敛,但是不论接下来怎么更新状态价值,策略提升得到的都是同一个策略。如果只在策略评估中进行一轮价值更新,然后直接根据更新后的价值进行策略提升,这样是否可以呢?答案是肯定的,这其实就是本节将要讲解的价值迭代算法,它可以被认为是一种策略评估只进行了一轮更新的策略迭代算法。需要注意的是,价值迭代中不存在显式的策略,我们只维护一个状态价值函数。

确切来说,价值迭代可以看成一种动态规划过程,它利用的是贝尔曼最优方程:

V ∗ ( s ) = m a x { a ∈ A r ( s , a ) + γ Σ s ′ ∈ S P ( s ′ ∣ s , a ) V ∗ ( s ′ ) } V^*\left( s \right) =\underset{a\in A}{max\left\{ \right.}r\left( s,a \right) +\gamma \underset{s'\in S}{\varSigma}P\left( s'|s,a \right) V^*\left( s' \right) \left. \right\} V(s)=aAmax{r(s,a)+γsSΣP(ss,a)V(s)}

将其写成迭代更新的方式为

V k + 1 ( s ) = m a x { a ∈ A r ( s , a ) + γ Σ s ′ ∈ S P ( s ′ ∣ s , a ) V k ( s ′ ) } V^{k+1}\left( s \right) =\underset{a\in A}{max\left\{ \right.}r\left( s,a \right) +\gamma \underset{s'\in S}{\varSigma}P\left( s'|s,a \right) V^k\left( s' \right) \left. \right\} Vk+1(s)=aAmax{r(s,a)+γsSΣP(ss,a)Vk(s)}

价值迭代便是按照以上更新方式进行的。等到 V k + 1 V^{k+1} Vk+1 V k V^k Vk相同时,它就是贝尔曼最优方程的不动点,此时对应着最优状态价值函数 V ∗ V^* V。然后我们利用 π ( s ) = a r g m a x a r ( s , a ) + γ Σ s ′ P ( s ′ ∣ s , a ) V k + 1 ( s ′ ) \pi(s)=arg max_a r(s,a)+\gamma{\varSigma}_{s'}P( s'|s,a) V^{k+1}( s') π(s)=argmaxar(s,a)+γΣsP(ss,a)Vk+1(s),从中恢复出最优策略即可。

价值迭代算法流程如下:

  • 随机初始化价值函数 V ( s ) V(s) V(s)
  • while Δ > θ \varDelta > \theta Δ>θ do:
  • Δ < − − 0 \Delta<--0 Δ<0
  • ​ 对于每一个状态 s ∈ S s\in S sS:
  • v < − − V ( s ) v<--V(s) v<V(s)
  • V ( s ) < − − m a x r ( s , a ) + γ Σ s ′ P ( s ′ ∣ s , a ) V ( s ′ ) V(s)<--max r(s,a)+\gamma{\varSigma}_{s'}P( s'|s,a) V( s') V(s)<maxr(s,a)+γΣsP(ss,a)V(s)
  • Δ < − − m a x ( Δ , ∣ v − V ( s ) ∣ ) \Delta<--max(\Delta,|v-V(s)|) Δ<max(Δ,vV(s))
  • end while
  • 返回一个确定性策略 π ( s ) = a r g m a x a r ( s , a ) + γ Σ s ′ P ( s ′ ∣ s , π ( s ) ) V ( s ′ ) \pi(s)=arg max_a r(s,a)+\gamma{\varSigma}_{s'}P( s'|s,\pi(s)) V( s') π(s)=argmaxar(s,a)+γΣsP(ss,π(s))V(s)
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)]
        # print(self.pi)
        #  [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]

    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)
        # print(self.v)
        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.pi)
print_agent(agent, action_meaning, list(range(37, 47)), [47])

策略迭代和价值迭代的区别:

策略迭代进行策略评估的时候使用的是在当前状态下各个动作的加权和,价值迭代在进行评估时使用的是在当前状态下动作价值最大的动作价值为状态价值。

策略迭代是进行策略评估,策略提升,再使用提升的策略进行策略评估,以此类推

价值迭代是没有策略评估和策略提升的迭代,通过价值迭代直接得到最优价值,再使用最优价值得到一个策略函数

4.5 冰湖环境

除了悬崖漫步环境,本章还准备了另一个环境——冰湖(Frozen Lake)。冰湖环境的状态空间和动作空间是有限的,我们在该环境中也尝试一下策略迭代算法和价值迭代算法,以便更好地理解这两个算法。

冰湖是 OpenAI Gym 库中的一个环境。OpenAI Gym 库中包含了很多有名的环境,例如 Atari 和 MuJoCo,并且支持我们定制自己的环境。在之后的章节中,我们还会使用到更多来自 OpenAI Gym 库的环境。如图 4-2 所示,冰湖环境和悬崖漫步环境相似,也是一个网格世界,大小为4×4。每一个方格是一个状态,智能体起点状态s在左上角,目标状态G在右下角,中间还有若干冰洞H。在每一个状态都可以采取上、下、左、右 4 个动作。由于智能体在冰面行走,因此每次行走都有一定的概率滑行到附近的其它状态,并且到达冰洞或目标状态时行走会提前结束。每一步行走的奖励是 0,到达目标的奖励是 1。

策略迭代:

import gym

env = gym.make("FrozenLake-v1")  # 创建环境  新的环境  即冰湖环境  也就不再需要建模的悬崖环境了
env = env.unwrapped  # 解封装才能访问状态转移矩阵P
env.render()  # 环境渲染,通常是弹窗显示或打印出可视化的环境
# print(env.P)
holes = set()  # 集合
ends = set()

# 以下循环是三层的   第一层是所有的状态   第二层是每个状态的四个动作  第三层是每个动作带来的三个可能滑倒的动作
for s in env.P:  # 该环境的P包含状态和动作,同时P中的p是指到达这个位置之后确定在这个位置的概率  该环境还有可能滑倒到其他三个相邻的位置中  所以也包括在内
    print("s:", s)
    for a in env.P[s]:
        print("a:", a)
        for s_ in env.P[s][a]:
            if s_[2] == 1.0:  # 获得奖励为1,代表是目标
                ends.add(s_[1])
                # print("s_", s_)
            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])

# 这个动作意义是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 小结

本章讲解了强化学习中两个经典的动态规划算法:策略迭代算法和价值迭代算法,它们都能用于求解最优价值和最优策略。动态规划的主要思想是利用贝尔曼方程对所有状态进行更新。需要注意的是,在利用贝尔曼方程进行状态更新时,我们会用到马尔可夫决策过程中的奖励函数和状态转移函数。如果智能体无法事先得知奖励函数和状态转移函数,就只能通过和环境进行交互来采样(状态-动作-奖励-下一状态)这样的数据,我们将在之后的章节中讲解如何求解这种情况下的最优策略。

python代码

空矩阵的构建

[[]for i in range(4*12)]

输出是:

[[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]

输出了4*12个[]

当变成

[[[]for j in range(4)] for i in range(48)]

就会生成四个一组的[] 生成48组

deepcopy()

copy.copy()是浅拷贝,只拷贝父对象,不会拷贝对象的内部的子对象。copy.deepcopy()是深拷贝,会拷贝对象及其子对象,哪怕以后对其有改动,也不会影响其第一次的拷贝。

代码:

import copy

List1 = ['1', '2', 3, 'a', ['b', 'c']]
List2 = List1        # 将List1赋给List2,相当于list2与list1指向同一块地址空间,list1里面内容发生,list2内容也会跟着发生改变
List3 = copy.copy(List1)       # 浅拷贝,只拷贝深拷贝的第一层(父对象)
List4 = copy.deepcopy(List1)   # 深拷贝,从上到底全部对象内容拷贝

List1.append('test')     # 在List1末尾添加'test'
List1[4].append('d')     # 在List1中['b','c']的末尾添加'd'

print('List1:%s' % List1)
print('List2:%s' % List2)
print('List3:%s' % List3)
print('List4:%s' % List4)


输出结果:

List1:['1', '2', 3, 'a', ['b', 'c', 'd'], 'test']
List2:['1', '2', 3, 'a', ['b', 'c', 'd'], 'test']
List3:['1', '2', 3, 'a', ['b', 'c', 'd']]
List4:['1', '2', 3, 'a', ['b', 'c']]

gym中env的unwrapped

一般从gym中引用环境只需要用gym.make就可以了,比如

env = gym.make("FrozenLake-v1")

但在很多程序中,还有这样一句

env = env.unwrapped

关于这个unwrapped的含义,文章gym中env的unwrapped中是这么解释的:

Open AI gym提供了许多不同的环境。每一个环境都有一套自己的参数和方法。然而,他们通常由一个类Env包装(就像这是面向对象编程语言(OOPLs)的一个接口)。这个类暴露了任一环境的最常用的,最本质的方法,比如step,reset,seed。拥有这个“接口”类非常好,因为它允许您的代码不受环境限制。如果您希望在不同的环境中测试单个代理,那么它还使事情变得更简单。
然而,如果你想访问一个特定环境的场景动态后面的东西,需要使用unwrapped属性。

#还原env的原始设置,env外包了一层防作弊层

据说gym的多数环境都用TimeLimit(源码)包装了,以限制Epoch,就是step的次数限制,比如限定为200次。所以小车保持平衡200步后,就会失败。

= gym.make(“FrozenLake-v1”)


但在很多程序中,还有这样一句

```python
env = env.unwrapped

关于这个unwrapped的含义,文章gym中env的unwrapped中是这么解释的:

Open AI gym提供了许多不同的环境。每一个环境都有一套自己的参数和方法。然而,他们通常由一个类Env包装(就像这是面向对象编程语言(OOPLs)的一个接口)。这个类暴露了任一环境的最常用的,最本质的方法,比如step,reset,seed。拥有这个“接口”类非常好,因为它允许您的代码不受环境限制。如果您希望在不同的环境中测试单个代理,那么它还使事情变得更简单。
然而,如果你想访问一个特定环境的场景动态后面的东西,需要使用unwrapped属性。

#还原env的原始设置,env外包了一层防作弊层

据说gym的多数环境都用TimeLimit(源码)包装了,以限制Epoch,就是step的次数限制,比如限定为200次。所以小车保持平衡200步后,就会失败。

用env.unwrapped可以得到原始的类,原始类想step多久就多久,不会200步后失败

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值