强化学习研讨会(三)

原文:annas-archive.org/md5/e0caa69bfbd246ee6119f0157bdca923

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:5. 动态规划

概述

在本章中,您将了解动态规划的驱动原理。您将了解经典的零钱兑换问题,并将其作为动态规划的应用。此外,您还将学习如何实现策略评估、策略迭代和价值迭代,并了解它们之间的差异。到本章结束时,您将能够使用强化学习RL)中的动态规划来解决问题。

引言

在上一章中,我们介绍了 OpenAI Gym 环境,并学习了如何根据应用需求实现自定义环境。您还了解了 TensorFlow 2 的基础知识,如何使用 TensorFlow 2 框架实现策略,以及如何使用 TensorBoard 可视化学习成果。在本章中,我们将从计算机科学的角度,了解动态规划DP)的一般工作原理。接着,我们将讨论它在强化学习中的使用方式及其原因。然后,我们将深入探讨经典的动态规划算法,如策略评估、策略迭代和价值迭代,并进行比较。最后,我们将实现经典零钱兑换问题中的算法。

动态规划是计算机科学中最基本和最基础的主题之一。此外,强化学习算法,如价值迭代策略迭代等,正如我们将看到的,使用相同的基本原理:避免重复计算以节省时间,这正是动态规划的核心。动态规划的哲学并不新鲜;一旦学会了解决方法,它是显而易见且普遍的。真正困难的部分是识别一个问题是否可以用动态规划来解决。

这个基本原理也可以用简单的方式向孩子解释。想象一下在一个盒子里数糖果的数量。如果你知道盒子里有 100 颗糖果,而店主又给了你 5 颗额外的糖果,你就不会重新开始数糖果。你会利用已有的信息,将 5 颗糖果加到原来的数量上,并说:“我有 105 颗糖果。”这就是动态规划的核心:保存中间信息并在需要时重新利用,以避免重复计算。虽然听起来简单,但如前所述,真正困难的部分是确定一个问题是否可以用动态规划来解决。正如我们稍后在识别动态规划问题一节中所看到的,问题必须满足特定的前提条件,如最优子结构和重叠子问题,才能用动态规划解决,我们将在识别动态规划问题一节中详细研究。一旦一个问题符合要求,就有一些著名的技术,比如自顶向下的备忘录法,即以无序的方式保存中间状态,以及自底向上的表格法,即将状态保存在有序的数组或矩阵中。

结合这些技巧可以显著提升性能,相比使用暴力算法进行求解。另外,随着操作次数的增加,时间差异也会变得更加明显。从数学角度来说,使用动态规划求解的方案通常在 O(n²)时间内运行,而暴力算法则需要 O(2ⁿ)时间,其中"O"(大 O 符号)可以粗略理解为执行的操作次数。所以,举个例子,如果 N=500,这是一个相对较小的数字,动态规划算法大约需要执行 500²次操作,而暴力算法则需要执行 2500 次操作。作为参考,太阳中有 280 个氢原子,这个数字无疑要比 2500 小得多。

以下图展示了两种算法执行操作次数的差异:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_01.jpg

图 5.1:可视化大 O 值

现在我们开始研究求解动态规划问题的方法。

求解动态规划问题

解决动态规划问题的两种常见方法是:表格法和备忘录法。在表格法中,我们构建一个矩阵,在查找表中逐一存储中间值。另一方面,在备忘录法中,我们以非结构化的方式存储相同的值。这里所说的非结构化方式是指查找表可能一次性填满所有内容。

想象你是一个面包师,正在向商店出售蛋糕。你的工作是出售蛋糕并获得最大利润。为了简化问题,我们假设所有其他成本都是固定的,而你产品的最高价格就是利润的唯一指标,这在大多数商业案例中是合理的假设。所以,自然地,你会希望把所有蛋糕卖给提供最高价格的商店,但你需要做出决定,因为有多个商店提供不同价格和不同大小的蛋糕。因此,你有两个选择:卖多少蛋糕,和选择哪家商店进行交易。为了这个例子,我们将忽略其他变量,假设没有额外的隐藏成本。我们将使用表格法和备忘录法来解决这个问题。

正式描述问题时,你有一个重量为 W 的蛋糕,以及一个各个商店愿意提供的价格数组,你需要找出能够获得最高价格(根据之前的假设,也就是最高利润)的最优配置。

注意

在接下来本节列出的代码示例中,我们将利润和价格互换使用。所以,例如,如果你遇到一个变量,如best_profit,它也可以表示最佳价格,反之亦然。

比如说,假设 W = 5,意味着我们有一个重 5 千克的蛋糕,以下表格中列出的价格是餐馆所提供的价格:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_02.jpg

图 5.2:不同重量蛋糕的不同价格

现在考虑餐厅 A 支付 10 美元购买 1 千克蛋糕,但支付 40 美元购买 2 千克蛋糕。那么问题是:我应该将 5 千克的蛋糕分割成 5 个 1 千克的切片出售,总价为 45 美元,还是应该将整个 5 千克的蛋糕作为一个整体卖给餐厅 B,后者提供 80 美元?在这种情况下,最优的配置是将蛋糕分割成 3 千克的部分,售价 50 美元,和 2 千克的部分,售价 40 美元,总计 90 美元。以下表格显示了各种分割方式及其对应的价格:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_03.jpg

图 5.3:蛋糕分割的不同组合

从前面的表格来看,显然最佳的价格由 2 千克+3 千克的组合提供。但为了真正理解暴力破解法的局限性,我们假设我们不知道哪个组合能够获得最大价格。我们将尝试用代码实现暴力破解法。实际上,对于一个实际的商业问题,观察的数据量可能过大,以至于你无法像在这里一样快速得到答案。前面的表格只是一个例子,帮助你理解暴力破解法的局限性。

那么,让我们尝试使用暴力破解法解决这个问题。我们可以稍微重新表述这个问题:在每个决策点,我们有一个选择——分割或不分割。如果我们首先选择将蛋糕分割成两部分不等的部分,左侧部分可以视为蛋糕的一部分,右侧部分则视为独立分割。在下一次迭代中,我们只集中于右侧部分/其他部分。然后,我们再次可以对右侧部分进行分割,右侧部分成为进一步分割的蛋糕部分。这种模式也称为递归

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_04.jpg

图 5.4:蛋糕分割成几块

在前面的图中,我们可以看到蛋糕被分割成多个部分。对于一块 5 千克重的蛋糕(假设你可以按照每个分割部分至少 1 千克的重量进行分割,因此每个分割部分的重量只能是 1 的整数倍),我们会看到"分割或不分割"一共出现了 32 次;如下所示:

2 x 2 x 2 x 2 x 2 = 25= 32

所以,首先让我们这样做:对于每一种 32 种可能的组合,计算总价,最后报告价格最高的组合。我们已经定义了价格列表,其中索引表示切片的重量:

PRICES = ["NA", 9, 40, 50, 70, 80]

例如,出售一整个 1 千克的蛋糕,售价为 9 美元;而出售一个 2 千克的蛋糕/切片,售价为 40 美元。零索引处的价格为 NA,因为我们不可能有 0 千克重的蛋糕。以下是实现上述情境的伪代码:

def partition(cake_size):
    """
    Partitions a cake into different sizes, and calculates the
    most profitable cut configuration
    Args:
        cake_size: size of the cake
    Returns:
        the best profit possible
    """
    if cake_size == 0:
        return 0
    best_profit = -1
    for i in range(1, cake_size + 1):
        best_profit = max(best_profit, PRICES[i] \
                         + partition(cake_size - i))
    return best_profit

上面的partition函数,cake_size将接受一个整数输入:蛋糕的大小。然后,在for循环中,我们会以每种可能的方式切割蛋糕并计算最佳利润。由于我们对每个位置都做出分割/不分割的决策,代码运行的时间复杂度是 O(2n)。现在让我们使用以下代码来调用该函数。if __name__块将确保代码仅在运行脚本时执行(而不是在导入时):

if __name__ == '__main__':
    size = 5
    best_profit_result = partition(size)
    print(f"Best profit: {best_profit_result}")

运行后,我们可以看到大小为5的蛋糕的最佳利润:

Best profit: 90

上述方法解决了计算最大利润的问题,但它有一个巨大的缺陷:非常慢。我们正在进行不必要的计算,并且要遍历整个搜索树(所有可能的组合)。为什么这是个坏主意?想象一下你从 A 点旅行到 C 点,费用是$10。你会考虑从 A 到 B,再到 D,再到 F,最后到 C,可能需要花费$150 吗?当然不会,对吧?这个思路是类似的:如果我知道当前的路径不是最优路径,为什么还要去探索那条路?

为了更高效地解决这个问题,我们将研究两种优秀的技术:表格法和备忘录法。它们的原理相同:避免无效的探索。但它们使用略有不同的方式来解决问题,正如你将看到的那样。

接下来我们将深入研究备忘录法。

备忘录法

备忘录法是指一种方法,在这种方法中,我们将中间输出的结果保存在一个字典中,供以后使用,也就是所谓的备忘录。因此得名“备忘录法”。

回到我们的蛋糕分割示例,如果我们修改partition函数,并打印cake_size的值以及该大小的最佳解决方案,就会发现一个新的模式。使用之前暴力方法中相同的代码,我们添加一个print语句来显示蛋糕大小及对应的利润:

def partition(cake_size):
    """
    Partitions a cake into different sizes, and calculates the
    most profitable cut configuration
    Args:
        cake_size: size of the cake
    Returns:
        the best profit possible
    """
    if cake_size == 0:
        return 0
    best_profit = -1
    for i in range(1, cake_size + 1):
        best_profit = max(best_profit, PRICES[i] \
                      + partition(cake_size - i))
    print(f"Best profit for size {cake_size} is {best_profit}")
    return best_profit

使用main块调用函数:

if __name__ == '__main__':
    size = 5
    best_profit_result = partition(size)
    print(f"Best profit: {best_profit_result}")

然后我们会看到如下输出:

Best profit for size 1 is 9
Best profit for size 2 is 40
Best profit for size 1 is 9
Best profit for size 3 is 50
Best profit for size 1 is 9
Best profit for size 2 is 40
Best profit for size 1 is 9
Best profit for size 4 is 80
Best profit for size 1 is 9
Best profit for size 2 is 40
Best profit for size 1 is 9
Best profit for size 3 is 50
Best profit for size 1 is 9
Best profit for size 2 is 40
Best profit for size 1 is 9
Best profit for size 5 is 90
Best profit: 90

正如前面的输出所示,这里有一个模式——对于给定大小的最佳利润保持不变,但我们计算了多次。特别需要注意的是计算的大小和顺序。它会先计算大小为 1 的利润,然后是 2,当它要计算大小为 3 时,它会从头开始计算,首先是 1,然后是 2,最后是 3。这种情况会不断重复,因为它不存储任何中间结果。一个显而易见的改进是将利润存储在一个备忘录中,然后稍后使用它。

我们在这里做了一个小修改:如果给定cake_sizebest_profit已经计算过,我们就直接使用它,而不再重新计算,代码如下所示:

    if cake_size == 0:
        return 0
    if cake_size in memo:
        return memo[cake_size]

现在让我们看一下完整的代码片段:

def memoized_partition(cake_size, memo):
    """
        Partitions a cake into different sizes, and calculates the
        most profitable cut configuration using memoization.
        Args:
            cake_size: size of the cake
            memo: a dictionary of 'best_profit' values indexed
                by 'cake_size'
        Returns:
            the best profit possible
        """
    if cake_size == 0:
        return 0
    if cake_size in memo:
        return memo[cake_size]
    else:
        best_profit = -1
        for i in range(1, cake_size + 1):
            best_profit = max(best_profit, \
                              PRICES[i] + memoized_partition\
                                          (cake_size - i, memo))
        print(f"Best profit for size {cake_size} is {best_profit}")
        memo[cake_size] = best_profit
        return best_profit

现在如果我们运行这个程序,我们将得到以下输出:

Best profit for size 1 is 9
Best profit for size 2 is 40
Best profit for size 3 is 50
Best profit for size 4 is 80
Best profit for size 5 is 90
Best profit: 90

在这里,我们不是运行计算 2n 次,而是只运行n次。这是一个巨大的改进。我们所需要做的只是将输出结果保存在字典或备忘录中,因此这种方法叫做备忘录化。在这种方法中,我们本质上将中间解保存到字典中,以避免重新计算。这个方法也被称为自顶向下方法,因为我们遵循自然顺序,类似于在二叉树中查找,例如。

接下来,我们将探讨表格方法。

表格方法

使用备忘录化方法,我们随意地存储中间计算结果。表格方法几乎做了相同的事情,只是方式稍有不同:它按预定顺序进行,这几乎总是固定的——从小到大。这意味着,为了获得最有利的切割,我们将首先获得 1 公斤蛋糕的最有利切割,然后是 2 公斤蛋糕,接着是 3 公斤蛋糕,依此类推。通常使用矩阵完成此操作,这被称为自底向上方法,因为我们先解决较小的问题。

考虑以下代码片段:

def tabular_partition(cake_size):
    """
    Partitions a cake into different sizes, and calculates the
    most profitable cut configuration using tabular method.
    Args:
        cake_size: size of the cake
    Returns:
        the best profit possible
    """
    profits = [0] * (cake_size + 1)
    for i in range(1, cake_size + 1):
        best_profit = -1
        for current_size in range(1, i + 1):
            best_profit = max(best_profit,\
                          PRICES[current_size] \
                          + profits[i - current_size])
        profits[i] = best_profit
    return profits[cake_size]

输出结果如下:

Best profit: 90

在前面的代码中,我们首先遍历尺寸,然后是切割。一个不错的练习是使用 IDE 和调试器运行代码,查看 profits 数组是如何更新的。首先,它会找到大小为 1 的蛋糕的最大利润,然后找到大小为 2 的蛋糕的最大利润。但是在这里,第二个 for 循环会尝试两种配置:一种是切割(两块大小为 1 的蛋糕),另一种是不切割(一个大小为 2 的蛋糕),由 profits[i – current_size] 指定。现在,对于每个尺寸,它都会尝试在所有可能的配置中切割蛋糕,而不会重新计算较小部分的利润。例如,profits[i – current_size] 会返回最佳配置,而无需重新计算。

练习 5.01:实践中的备忘录化

在这个练习中,我们将尝试使用备忘录化方法解决一个动态规划问题。问题如下:

给定一个数字 n,打印第 n 个三斐波那契数。三斐波那契数列类似于斐波那契数列,但使用三个数字而不是两个。这意味着,第 n 个三斐波那契数是前面三个数字的和。以下是一个示例:

斐波那契数列 0, 1, 2, 3, 5, 8……定义如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_05.jpg

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_05.jpg)

图 5.5:斐波那契数列

三斐波那契数列 0, 0, 1, 1, 2, 4, 7……定义如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_06.jpg

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_06.jpg)

图 5.6:三斐波那契数列

三斐波那契数列的广义公式如下:

Fibonacci(n) = Fibonacci(n – 1) + Fibonacci(n – 2)
Tribonacci(n) = Tribonacci(n – 1) \
                + Tribonacci(n – 2) + Tribonacci(n – 3)

以下步骤将帮助你完成练习:

  1. 现在我们知道了公式,第一步是用 Python 创建一个简单的递归实现。使用描述中的公式并将其转换为 Python 函数。你可以选择在 Jupyter notebook 中做,或者直接用一个简单的 .py Python 文件:

    def tribonacci_recursive(n):
        """
        Uses recursion to calculate the nth tribonacci number
        Args:
            n: the number
        Returns:
            nth tribonacci number
        """
        if n <= 1:
            return 0
        elif n == 2:
            return 1
        else:
            return tribonacci_recursive(n - 1) \
                   + tribonacci_recursive(n - 2) \
                   + tribonacci_recursive(n - 3)
    

    在前面的代码中,我们递归地计算 Tibonacci 数的值。此外,如果数字小于或等于 1,我们知道答案将是 0,2 的答案将是 1,因此我们添加了if-else条件来处理边缘情况。要测试前面的代码,只需在main块中调用它,并检查输出是否符合预期:

    if __name__ == '__main__':
        print(tribonacci_recursive(6))
    
  2. 正如我们所学到的,这个实现非常慢,并且随着n的增加,增长速度呈指数级。现在,使用备忘录法,存储中间结果,以便它们不被重新计算。创建一个字典来检查该第n个 Tibonacci 数的答案是否已经添加到字典中。如果是,则直接返回;否则,尝试计算:

    def tribonacci_memo(n, memo):
        """
        Uses memoization to calculate the nth tribonacci number
        Args:
            n: the number
            memo: the dictionary that stores intermediate results
        Returns:
            nth tribonacci number
        """
        if n in memo:
            return memo[n]
        else:
            ans1 = tribonacci_memo(n - 1, memo)
            ans2 = tribonacci_memo(n - 2, memo)
            ans3 = tribonacci_memo(n - 3, memo)
            res = ans1 + ans2 + ans3
            memo[n] = res
            return res
    
  3. 现在,使用前面的代码片段,不使用递归来计算第n个 Tibonacci 数。运行代码并确保输出与预期相符,通过在main块中运行它:

    if __name__ == '__main__':
        memo = {0: 0, 1: 0, 2: 1}
        print(tribonacci_memo(6, memo))
    

    输出结果如下:

    7
    

如您在输出中看到的,和是7。我们已经学会了如何将一个简单的递归函数转换为记忆化的动态规划代码。

注意

要访问此特定部分的源代码,请参考packt.live/3dghMJ1

您还可以在线运行此示例,网址为packt.live/3fFE7RK

接下来,我们将尝试使用表格方法做同样的事情。

练习 5.02:表格法在实践中的应用

在这个练习中,我们将使用表格方法解决一个动态规划问题。练习的目标是识别两个字符串之间的最长公共子串的长度。例如,如果两个字符串分别是BBBABDABAAAAAABDABBAABB,那么最长的公共子串是ABDAB。其他公共子串有AABBBA,以及BAA,但它们不是最长的:

  1. 导入numpy库:

    import numpy as np
    
  2. 实现暴力法,首先计算两个字符串的最长公共子串。假设我们有两个变量ij,它们表示子串的开始和结束位置。使用这些指针来指示两个字符串中子串的开始和结束位置。您可以使用 Python 中的==运算符来查看字符串是否匹配:

    def lcs_brute_force(first, second):
        """
        Use brute force to calculate the longest common 
        substring of two strings
        Args:
            first: first string
            second: second string
        Returns:
            the length of the longest common substring
        """
        len_first = len(first)
        len_second = len(second)
        max_lcs = -1
        lcs_start, lcs_end = -1, -1
        # for every possible start in the first string
        for i1 in range(len_first):
            # for every possible end in the first string
            for j1 in range(i1, len_first):
                # for every possible start in the second string
                for i2 in range(len_second):
                    # for every possible end in the second string
                    for j2 in range(i2, len_second):
                        """
                        start and end position of the current
                        candidates
                        """
                        slice_first = slice(i1, j1)
                        slice_second = slice(i2, j2)
                        """
                        if the strings match and the length is the
                        highest so far
                        """
                        if first[slice_first] == second[slice_second] \
                           and j1 - i1 > max_lcs:
                            # save the lengths
                            max_lcs = j1 - i1
                            lcs_start = i1
                            lcs_end = j1
        print("LCS: ", first[lcs_start: lcs_end])
        return max_lcs
    
  3. 使用main块调用函数:

    if __name__ == '__main__':
        a = "BBBABDABAA"
        b = "AAAABDABBAABB"
        lcs_brute_force(a, b)
    

    我们可以验证输出是否正确:

     LCS:  ABDAB
    
  4. 让我们实现表格方法。现在我们有了一个简单的解决方案,我们可以继续优化它。看看主循环,它嵌套了四次。这意味着该解决方案的运行时间是O(N⁴)。无论我们是否有最长公共子串,解决方案都会执行相同的计算。使用表格方法得出更多的解决方案:

    def lcs_tabular(first, second):
        """
        Calculates the longest common substring using memoization.
        Args:
            first: the first string
            second: the second string
        Returns:
            the length of the longest common substring.
        """
        # initialize the table using numpy
        table = np.zeros((len(first), len(second)), dtype=int)
        for i in range(len(first)):
            for j in range(len(second)):
                if first[i] == second[j]:
                    table[i][j] += 1 + table[i - 1][j - 1]
        print(table)
        return np.max(table)
    

    这个问题具有天然的矩阵结构。将其中一个字符串的长度视为矩阵的行,另一个字符串的长度视为矩阵的列。将该矩阵初始化为0。矩阵中位置i, j的值将表示第一个字符串的第i个字符是否与第二个字符串的第j个字符相同。

    现在,最长公共子串将具有在对角线上最多的 1 个数字。利用这个事实,如果当前位子匹配且i-1j-1位置上有1,则将最大子串的长度增加 1。这将表明有两个连续的匹配。使用np.max(table)返回矩阵中的max元素。我们也可以查看对角线递增的序列,直到该值达到5

  5. 使用main模块调用该函数:

    if __name__ == '__main__':
        a = "BBBABDABAA"
        b = "AAAABDABBAABB"
        lcs_tabular(a, b)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_07.jpg

图 5.7:LCS 输出结果

如你所见,行(第一列)和列(第二列)之间存在直接的映射关系,因此 LCS(最长公共子序列)字符串只会是从 LCS 长度开始倒数的对角线元素。在前面的输出中,你可以看到最高的元素是 5,因此你知道长度是 5。LCS 字符串将是从元素5开始的对角线元素。字符串的方向总是向上对角线,因为列总是从左到右排列。请注意,解决方案仅仅是计算 LCS 的长度,而不是找到实际的 LCS。

注意

要访问这一特定部分的源代码,请参考 packt.live/3fD79BC

你也可以在网上运行这个示例,网址是 packt.live/2UYVIfK

现在我们已经学会了如何解决动态规划问题,接下来我们应该学习如何识别这些问题。

识别动态规划问题

虽然一旦识别出问题如何递归,解决动态规划问题就变得很容易,但确定一个问题是否可以通过动态规划来解决却是很困难的。例如,旅行商问题,你被给定一个图,并希望在最短的时间内覆盖所有的顶点,这是一个无法用动态规划解决的问题。每个动态规划问题必须满足两个先决条件:它应该具有最优子结构,并且应该有重叠子问题。我们将在接下来的部分中详细了解这些条件的含义以及如何解决它们。

最优子结构

回想一下我们之前讨论的最佳路径示例。如果你想从 A 点通过 B 点到达 C 点,并且你知道这是最佳路径,那么就没有必要探索其他路径。换句话说:如果我想从 A 点到达 D 点,而我知道从 A 点到 C 点的最佳路径,那么从 A 点到 D 点的最佳路径一定会包含从 A 点到 C 点的路径。这就是所谓的最优子结构。本质上,它意味着问题的最优解包含了子问题的最优解。记得我们在知道一个大小为n的蛋糕的最佳利润后,就不再重新计算它吗?因为我们知道,大小为n+1的蛋糕的最佳利润会包括n,这是在考虑切分蛋糕为大小n1时得到的。再重复一遍,如果我们要使用动态规划(DP)解决问题,最优子结构的属性是一个必要条件。

重叠子问题

记得我们最初在设计蛋糕分配问题的暴力解法时,后来又采用了备忘录法。最初,暴力解法需要 32 步才能得到解,而备忘录法只需要 5 步。这是因为暴力解法重复执行相同的计算:对于大小为 3 的问题,它需要先解决大小为 2 和 1 的问题。然后,对于大小为 4 的问题,它又需要解决大小为 3、2 和 1 的问题。这个递归重计算是由于问题的性质:重叠子问题。这也是我们能够将答案保存在备忘录中,之后使用相同的解而不再重新计算的原因。重叠子问题是使用动态规划(DP)来解决问题的另一个必要条件。

硬币换零钱问题

硬币换零钱问题是软件工程面试中最常被问到的题目之一。题目很简单:给定一个硬币面额的列表,以及一个总和 N,找出到达该总和的不同方式的数量。例如,如果 N = 3 且硬币面额 D = {1, 2},那么答案是 2。也就是说,有两种方式可以得到 3:{1, 1, 1} 和 {2, 1}:

  1. 为了解决这个问题,你需要准备一个递归公式,计算得到一个总和的不同方式数。为此,你可以从一个简单的版本开始,先解决一个数字的情况,再尝试将其转换为更一般的解法。

  2. 最终输出可能是如下图所示的表格,可用于总结结果。在下表中,第一行表示面额,第一列表示总和。更具体地说,第一行的 0、1、2、3、4、5 表示总和,第一列表示可用的面额。我们将基础情况初始化为 1 而非 0,因为如果面额小于总和,则我们只是将之前的组合复制过来。

    下表表示如何使用硬币 [1, 2] 来计算得到 5 的方法数:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_08.jpg

    图 5.8:计算使用面额为 1 和 2 的硬币得到总和 5 的方法数

  3. 所以,我们可以看到使用面额为 1 和 2 的硬币得到总和 5 的方法数是 3,具体来说就是 1+1+1+1+1、2+1+1+1 和 2+2+1。记住,我们只考虑独特的方式,也就是说,2+2+1 和 1+2+2 是相同的。

让我们通过一个练习来解决硬币换零钱问题。

练习 5.03:解决硬币换零钱问题

在这个练习中,我们将解决经典且非常流行的硬币换零钱问题。我们的目标是找到用面额为 1、2 和 3 的硬币组合得到总和 5 的不同排列数。以下步骤将帮助你完成这个练习:

  1. 导入 numpypandas 库:

    import numpy as np
    import pandas as pd
    
  2. 现在,让我们尝试识别重叠子问题。如之前所述,有一个共同点:我们必须搜索所有可能的面额,并检查它们是否能加起来得到某个数。此外,这比蛋糕示例稍微复杂一些,因为我们有两个变量需要迭代:首先是面额,其次是总和(在蛋糕示例中,只有一个变量,即蛋糕大小)。因此,我们需要一个二维数组或矩阵。

    在列上,我们将展示我们试图达到的和,而在行上,我们将考虑可用的各种面额。当我们遍历面额(列)时,我们将通过首先计算不考虑当前面额时达到某个和的方式数量,然后再加上考虑当前面额的方式数量,来计算合计数。这类似于蛋糕示例,其中我们首先进行切割,计算利润,然后不切割并计算利润。然而,区别在于这次我们会从上方的行中获取之前的最佳配置,并且我们会将这两个数相加,而不是选择其中的最大值,因为我们关心的是到达和的所有可能方式的总数。例如,使用 {1, 2} 求和为 4 的方式是首先使用 {2},然后加上求和为 4 - 2 = 2 的方式数量。我们可以从同一行获取这个值,索引为 2。我们还会将第一行初始化为 1,因为它们要么是无效的(使用 1 到达零的方式数量),要么是有效的,并且只有一个解决方案:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_09.jpg

    def count_changes(N, denominations):
        """
        Counts the number of ways to add the coin denominations
        to N.
        Args:
            N: number to sum up to
            denominations: list of coins
        Returns:
        """
        print(f"Counting number of ways to get to {N} using coins:\
    {denominations}")
    
  3. 接下来,我们将初始化一个尺寸为 len(denomination) x (N + 1) 的表格。列数是 N + 1,因为索引包括零:

        table = np.ones((len(denominations), N + 1)).astype(int)
        # run the loop from 1 since the first row will always 1s
        for i in range(1, len(denominations)):
            for j in range(N + 1):
                if j < denominations[i]:
                    """
                    If the index is less than the denomination
                    then just copy the previous best
                    """
                    table[i, j] = table[i - 1, j]
                else:
                    """
                    If not, the add two things:
                    1\. The number of ways to sum up to 
                       N *without* considering
                       the existing denomination.
                    2\. And, the number of ways to sum up to N minus 
                       the value of the current denomination 
                       (by considering the current and the 
                       previous denominations)
                    """
                    table[i, j] = table[i - 1, j] \
                                  + table[i, j - denominations[i]]
    
  4. 现在,最后我们将打印出这个表格:

        # print the table
        print_table(table, denominations)
    
  5. 创建一个带有以下实用功能的 Python 脚本,它可以漂亮地打印表格。这对于调试非常有用。漂亮打印本质上是用来以更易读和更全面的方式呈现数据。通过将面额作为索引,我们可以更清晰地查看输出:

    def print_table(table, denominations):
        """
        Pretty print a numpy table
        Args:
            table: table to print
            denominations: list of coins
        Returns:
        """
        df = pd.DataFrame(table)
        df = df.set_index(np.array(denominations))
        print(df)
    

    注意

    欲了解更多关于漂亮打印的细节,您可以参考以下链接的官方文档:docs.python.org/3/library/pprint.html

  6. 使用以下配置初始化脚本:

    if __name__ == '__main__':
        N = 5
        denominations = [1, 2]
        count_changes(N, denominations)
    

    输出将如下所示:

    Counting number of ways to get to 5 using coins: [1, 2]
       0  1  2  3  4  5
    1  1  1  1  1  1  1
    2  1  1  2  2  3  3
    

如我们在最后一行和列的条目中看到的,使用 [1, 2] 获得 5 的方式有 3 种。我们现在已经详细了解了动态规划(DP)的概念。

注意

要访问此特定部分的源代码,请参阅 packt.live/2NeU4lT

您也可以在网上运行这个示例,访问 packt.live/2YUd6DD

接下来,让我们看看它是如何用于解决强化学习中的问题的。

强化学习中的动态规划

DP 在 RL 中扮演着重要角色,因为在给定的时刻你所面临的选择太多。例如,机器人在当前环境状态下应该向左转还是向右转。为了求解此类问题,通过蛮力计算每个状态的结果是不可行的。然而,通过 DP,我们可以使用前一节中学到的方法来解决这一问题。

我们在前面的章节中已经看过贝尔曼方程。让我们重述一下基本内容,看看贝尔曼方程如何具备 DP 所需的两个属性。

假设环境是一个有限的马尔可夫决策过程MDP),我们用一个有限的状态集 S 来定义环境的状态。这表示状态配置,例如机器人的当前位置。有限的动作集 A 给出了动作空间,有限的奖励集 R。我们用 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_09a.png 来表示折扣率,这个值介于 0 和 1 之间。

给定一个状态 S,该算法使用一个确定性策略从 A 中选择一个动作,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_02_31b.png。该策略仅仅是状态 S 和动作 A 之间的映射,例如,机器人可能做出的选择,如向左或向右。确定性策略允许我们以非随机的方式选择动作(与随机策略相对,后者包含显著的随机成分)。

为了具体化我们的理解,假设一个简单的自动驾驶汽车。为了简化起见,我们将在这里做一些合理的假设。动作空间可以定义为 {左转,右转,直行,倒退}。一个确定性策略是:如果地面上有个坑,向左或右转以避免它。然而,一个随机策略会说:如果地面上有个坑,以 80% 的概率向左转,这意味着汽车有小概率故意进入坑中。虽然这个动作目前看起来可能没有意义,但我们稍后会看到,在第七章,时间差学习中,这实际上是一个非常重要的举措,并且解决了 RL 中的一个关键概念:探索与利用的困境。

回到使用 DP 在 RL 中的原始点,下面是简化版的贝尔曼方程:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_10.jpg

图 5.10:简化贝尔曼方程

完整方程与简化方程的唯一区别在于我们没有对 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_10a.png 进行求和,这在非确定性环境下是有效的。以下是完整的贝尔曼方程:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_11.jpg

图 5.11:完整的贝尔曼方程

在前面的方程中,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_11a.png是值函数,表示处于特定状态时的奖励。我们稍后会更深入地探讨它。https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_11b.png是采取动作a的奖励,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_11c.png是下一个状态的奖励。你可以观察到以下两点:

如我们稍后将看到的,值函数的结构与我们在硬币面额问题中看到的类似。不同之处在于,我们不再保存到达和为的方式数量,而是保存最佳https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_11h.png,即能够带来最高回报的值函数的最佳值。接下来,我们将探讨策略迭代和价值迭代,它们是帮助我们解决 RL 问题的基本算法。

策略和价值迭代

解决强化学习(RL)问题的主要思路是利用值函数寻找最佳策略(决策方式)。这种方法对于简单的 RL 问题效果很好,因为我们需要了解整个环境的信息:状态的数量和动作空间。我们甚至可以在连续空间中使用此方法,但并不是在所有情况下都能得到精确的解。在更新过程中,我们必须遍历所有可能的场景,这也是当状态和动作空间过大时,使用该方法变得不可行的原因:

  1. 策略迭代:从一个随机策略开始,逐步收敛到最佳策略。

  2. 价值迭代:使用随机值初始化状态,并逐步更新它们直到收敛。

状态值函数

状态值函数是一个数组,表示处于该状态时的奖励。假设在一个特定的游戏中有四个可能的状态:S1S2S3S4,其中S4是终止状态(结束状态)。状态值表可以通过一个数组表示,如下表所示。请注意,值只是示例。每个状态都有一个“值”,因此称为状态值函数。此表格可以用于游戏中稍后的决策:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_12.jpg

图 5.12:状态值函数的示例表格

例如,如果你处于状态S3,你有两个可能的选择,S4S2;你会选择S4,因为在那个状态中的值比S2更高。

动作值函数

动作-值函数是一个矩阵,表示每个状态-动作对的奖励。这同样可以用来选择在特定状态下应该采取的最佳动作。与之前的状态-动作表不同,这次我们为每个动作也关联了奖励,具体如下表所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_13.jpg

图 5.13:动作-值函数的示例表格

请注意,这些只是示例值,实际计算时会使用特定的更新策略。我们将在策略改进部分查看更新策略的更具体示例。这个表格将稍后用于值迭代算法,因此我们可以迭代地更新表格,而不是等到最后一步。更多内容请参考值迭代部分。

OpenAI Gym:Taxi-v3 环境

在前面的章节中,我们已经了解了什么是 OpenAI Gym 环境,但这次我们将玩一个不同的游戏:Taxi-v3。在这个游戏中,我们将教导我们的代理司机接送乘客。黄色方块代表出租车。环境中有四个可能的地点,分别用不同的字符标记:R、G、B 和 Y,分别代表红色、绿色、蓝色和黄色,具体如下图所示。代理需要在某个地点接乘客并将其送到另一个地点。此外,环境中有用 | 表示的墙壁。每当有墙壁时,可能的动作数量就会受到限制,因为出租车不能穿越墙壁。这使得问题变得有趣,因为代理必须巧妙地在网格中导航,同时避开墙壁,找到最佳的(最短的)解决方案:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_14.jpg

图 5.14:Taxi-v3 环境

以下是每个动作对应的奖励列表:

  • +20:成功接送时的奖励。

  • -1:每一步都会发生。这一点很重要,因为我们关注的是找到最短的路径。

  • -10:非法的接送操作。

策略

环境中的每个状态都由一个数字表示。例如,前一张照片中的状态可以用54来表示。在这个游戏中有 500 个这样的独特状态。对于每一个状态,我们都有相应的策略(即,应该执行的动作)。

现在,让我们自己尝试一下这个游戏。

初始化环境并打印可能的状态数和动作空间,当前分别为 500 和 6。在现实问题中,这个数字会非常庞大(可能达到数十亿),我们无法使用离散的代理。但为了简化问题,我们假设这些并进行求解:

def initialize_environment():
    """initialize the OpenAI Gym environment"""
    env = gym.make("Taxi-v3")
    print("Initializing environment")
    # reset the current environment
    env.reset()
    # show the size of the action space
    action_size = env.action_space.n
    print(f"Action space: {action_size}")
    # Number of possible states
    state_size = env.observation_space.n
    print(f"State space: {state_size}")
    return env

上述代码将输出以下内容:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_15.jpg

图 5.15:启动 Taxi-v3 环境

如你所见,网格表示当前(初始)状态的环境。黄色框代表出租车。六个可能的选择是:左、右、上、下、接客和放客。现在,让我们看看如何控制出租车。

使用以下代码,我们将随机地在环境中执行步骤并查看输出。env.step函数用于从一个状态转移到另一个状态。它接受的参数是其动作空间中的有效动作之一。在执行一步后,它会返回几个值,如下所示:

  • new_state:新的状态(一个表示下一个状态的整数)

  • reward:从转移到下一个状态中获得的奖励

  • done:如果环境需要重置(意味着你已经到达了终止状态)

  • info:表示转移概率的调试信息

由于我们使用的是确定性环境,因此转移概率始终为1.0。还有其他环境具有非 1 的转移概率,表示如果你做出某个决策;例如,如果你右转,环境将以相应的概率右转,这意味着即使做出特定的行动后,你也有可能停留在原地。代理在与环境互动时不能学习这些信息,否则如果代理知道环境信息,将会是不公平的:

def random_step(n_steps=5):
    """
    Steps through the taxi v3 environment randomly
    Args:
        n_steps: Number of steps to step through
    """
    # reset the environment
    env = initialize_environment()
    state = env.reset()
    for i in range(n_steps):
        # choose an action at random
        action = env.action_space.sample()
        env.render()
        new_state, reward, done, info = env.step(action)
        print(f"New State: {new_state}\n"\
              f"reward: {reward}\n"\
              f"done: {done}\n"\
              f"info: {info}\n")\
        print("*" * 20)

使用这段代码,我们将在环境中随机(但有效)地执行步骤,并在到达终止状态时停止。如果执行代码,我们将看到以下输出:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_16.jpg

图 5.16:随机遍历环境

通过查看输出,我们可以看到在执行某个动作后,所经历的新状态以及执行该动作所获得的奖励;done会指示我们已经到达了终止阶段;还有一些环境信息,例如转移概率。接下来,我们将查看我们的第一个强化学习算法:策略迭代。

策略迭代

正如其名所示,在策略迭代中,我们会遍历多个策略,然后进行优化。策略迭代算法分为两步:

  1. 策略评估

  2. 策略改进

策略评估计算当前策略的值函数,初始时是随机的。然后,我们使用贝尔曼最优性方程更新每个状态的值。接着,一旦我们得到了新的值函数,就更新策略以最大化奖励并进行策略改进。现在,如果策略发生了更新(即使策略中的一个决策发生了变化),这个更新后的策略保证比旧的策略更好。如果策略没有更新,则意味着当前的策略已经是最优的(否则它会被更新并找到更好的策略)。

以下是策略迭代算法的工作步骤:

  1. 从一个随机策略开始。

  2. 计算所有状态的值函数。

  3. 更新策略,选择能够最大化奖励的行动(策略改进)。

  4. 当策略不再变化时停止。这表明已经获得了最优策略。

让我们手动通过算法进行一次干运行,看看它是如何更新的,使用一个简单的例子:

  1. 从一个随机策略开始。下表列出了代理在 Taxi-v3 环境中给定位置可以采取的可能行动:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_17.jpg

    图 5.17:代理的可能行动

    在前面的图中,表格表示环境,框表示选择。箭头表示如果代理处于该位置,应采取的行动。

  2. 计算所有唯一状态的值函数。下表列出了每个状态的样本状态值。值初始化为零(某些算法的变体也使用接近零的小随机值):https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_18.jpg

    图 5.18:每个状态的奖励值

    为了直观地理解更新规则,我们使用一个极其简单的例子:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_19.jpg

    图 5.19:理解更新规则的示例策略

    从蓝色位置开始,经过第一步policy_evaluation后,策略将到达绿色(终止)位置。值将按照以下方式更新(每次迭代都有一个图示):

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_20.jpg

    图 5.20:每步奖励的乘法

    每一步,奖励都会乘以 gamma(在此示例中为0.9)。此外,在这个例子中,我们已经从最优策略开始,因此更新后的策略将与当前策略完全相同。

  3. 更新策略。让我们通过一个小例子来看看更新规则。假设以下是当前的值函数及其对应的策略:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_21.jpg

    图 5.21:样本值函数及其对应的策略。

    如前图所示,左侧的表格表示值,右侧的表格表示策略(决策)。

    一旦我们执行更新,假设值函数变为如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_22.jpg

    图 5.22:样本值函数的更新值

    现在,策略将在每个单元格中更新,使得行动会带领代理到达能够提供最高奖励的状态,因此对应的策略将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_23.jpg

    图 5.23:更新值函数对应的策略

  4. 重复步骤 1-3,直到策略不再变化。

    我们将训练算法,通过回合迭代地逼近真实的价值函数,并且每个回合都给我们提供最优策略。一个回合是智能体执行一系列动作直到达到终止状态。这可以是目标状态(例如,在 Taxi-v3 环境中的乘客下车状态),也可以是定义智能体可以采取的最大步数的数字,以避免无限循环。

    我们将使用以下代码初始化环境和价值函数表。我们将把价值函数保存在变量V中。此外,根据算法的第一步,我们将使用env.action_space.sample()方法从一个随机策略开始,这个方法每次调用时都会返回一个随机动作:

    def policy_iteration(env):
        """
        Find the most optimal policy for the Taxi-v3 environment 
        using Policy Iteration
        Args:
            env: Taxi=v3 environment
        Returns:
            policy: the most optimal policy
        """
        V = dict()
    
  5. 现在,在下一节中,我们将定义并初始化变量:

    """
    initially the value function for all states
    will be random values close to zero
    """
    state_size = env.observation_space.n
    for i in range(state_size):
        V[i] = np.random.random()
    # when the change is smaller than this, stop
    small_change = 1e-20
    # future reward coefficient
    gamma = 0.9
    episodes = 0
    # train for this many episodes
    max_episodes = 50000
    # initially we will start with a random policy
    current_policy = dict()
    for s in range(state_size):
        current_policy[s] = env.action_space.sample()
    
  6. 现在进入主循环,它将执行迭代:

    while episodes < max_episodes:
        episodes += 1
        # policy evaluation
        V = policy_evaluation(V, current_policy, \
                              env, gamma, small_change)
        # policy improvement
        current_policy, policy_changed = policy_improvement\
                                         (V, current_policy, \
                                          env, gamma)
        # if the policy didn't change, it means we have converged
        if not policy_changed:
            break
    print(f"Number of episodes trained: {episodes}")
    return current_policy
    
  7. 现在我们已经准备好了基本设置,我们将首先使用以下代码进行策略评估步骤:

    def policy_evaluation(V, current_policy, env, gamma, \
                          small_change):
        """
        Perform policy evaluation iterations until the smallest 
        change is less than
        'smallest_change'
        Args:
            V: the value function table
            current_policy: current policy
            env: the OpenAI Tax-v3 environment
            gamma: future reward coefficient
            small_change: how small should the change be for the 
              iterations to stop
        Returns:
            V: the value function after convergence of the evaluation
        """
        state_size = env.observation_space.n
    
  8. 在以下代码中,我们将循环遍历状态并更新https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_23a.png

        while True:
            biggest_change = 0
            # loop through every state present
            for state in range(state_size):
                old_V = V[state]
                # take the action according to the current policy
                action = current_policy[state]
                prob, new_state, reward, done = env.env.P[state]\
                                                [action][0]
    
  9. 接下来,我们将使用贝尔曼最优方程更新https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_23b.png

                V[state] = reward + gamma * V[new_state]
                """
                if the biggest change is small enough then it means
                the policy has converged, so stop.
                """
                biggest_change = max(biggest_change, \
                                     abs(V[state] – old_V))
            if biggest_change < small_change:
                break
        return V
    
  10. 一旦完成策略评估步骤,我们将使用以下代码进行策略改进:

    def policy_improvement(V, current_policy, env, gamma):
        """
        Perform policy improvement using the 
        Bellman Optimality Equation.
        Args:
            V: the value function table
            current_policy: current policy
            env: the OpenAI Tax-v3 environment
            gamma: future reward coefficient
        Returns:
            current_policy: the updated policy
            policy_changed: True, if the policy was changed, 
            else, False
        """
    
  11. 我们首先定义所有必需的变量:

        state_size = env.observation_space.n
        action_size = env.action_space.n
        policy_changed = False
        for state in range(state_size):
            best_val = -np.inf
            best_action = -1
            # loop over all actions and select the best one
            for action in range(action_size):
                prob, new_state, reward, done = env.env.P[state]\
                                                [action][0]
    
  12. 现在,在这里,我们将通过采取这个动作来计算未来的奖励。请注意,我们使用的是简化的方程,因为我们没有非一的转移概率:

                future_reward = reward + gamma * V[new_state]
                if future_reward > best_val:
                    best_val = future_reward
                    best_action = action
            """
            using assert statements we can avoid getting 
            into unwanted situations
            """
            assert best_action != -1
            if current_policy[state] != best_action:
                policy_changed = True
            # update the best action for this current state
            current_policy[state] = best_action
        # if the policy didn't change, it means we have converged
        return current_policy, policy_changed
    
  13. 一旦最优策略被学习,我们将在新的环境中对其进行测试。现在,两个部分都已准备好。让我们通过main代码块调用它们:

    if __name__ == '__main__':
        env = initialize_environment()
        policy = value_iteration(env)
        play(policy, render=True)
    
  14. 接下来,我们将添加一个play函数,用于在新的环境中测试策略:

    def play(policy, render=False):
        """
        Perform a test pass on the Taxi-v3 environment
        Args:
            policy: the policy to use
            render: if the result should be rendered at every step. 
                    False by default
        """
        env = initialize_environment()
        rewards = []
    
  15. 接下来,让我们定义max_steps。这基本上是智能体允许采取的最大步数。如果在此时间内没有找到解决方案,我们将其称为一个回合并继续:

        max_steps = 25
        test_episodes = 2
        for episode in range(test_episodes):
            # reset the environment every new episode
            state = env.reset()
            total_rewards = 0
            print("*" * 100)
            print("Episode {}".format(episode))
            for step in range(max_steps):
    

    在这里,我们将采取之前保存在策略中的动作:

                action = policy[state]
                new_state, reward, done, info = env.step(action)
                if render:
                    env.render()
                total_rewards += reward
                if done:
                    rewards.append(total_rewards)
                    print("Score", total_rewards)
                    break
                state = new_state
        env.close()
        print("Average Score", sum(rewards) / test_episodes)
    

    运行主代码块后,我们看到如下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_24.jpg

图 5.24:智能体将乘客送到正确的位置

如你所见,智能体将乘客送到正确的位置。请注意,输出已被截断以便于展示。

价值迭代

如你在前一节看到的,我们在几次迭代后得到了最优解,但策略迭代有一个缺点:我们只能在多次评估迭代后改进一次策略。

简化的贝尔曼方程可以通过以下方式更新。请注意,这与策略评估步骤相似,唯一的不同是采取所有可能动作的价值函数的最大值:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_25.jpg

图 5.25:更新的贝尔曼方程

该方程可以理解为如下:

对于给定的状态,采取所有可能的动作,然后存储具有最高 V[s] 值的那个。”

就这么简单。使用这种技术,我们可以将评估和改进结合在一个步骤中,正如你现在将看到的那样。

我们将像往常一样,先定义一些重要的变量,比如 gammastate_sizepolicy,以及值函数字典:

def value_iteration(env):
    """
    Performs Value Iteration to find the most optimal policy for the
    Tax-v3 environment
    Args:
        env: Taxiv3 Gym environment
    Returns:
        policy: the most optimum policy
    """
    V = dict()
    gamma = 0.9
    state_size = env.observation_space.n
    action_size = env.action_space.n
    policy = dict()
    # initialize the value table randomly
    # initialize the policy randomly
    for x in range(state_size):
        V[x] = 0
        policy[x] = env.action_space.sample()

使用之前定义的公式,我们将采用相同的循环,并在 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_25a.png 计算部分做出更改。现在,我们使用的是之前定义的更新后的 Bellman 方程:

"""
this loop repeats until the change in value function
is less than delta
"""
while True:
    delta = 0
    for state in reversed(range(state_size)):
        old_v_s = V[state]
        best_rewards = -np.inf
        best_action = None
        # for all the actions in current state
        for action in range(action_size):
            # check the reward obtained if we were to perform
            # this action
            prob, new_state, reward, done = 
              env.env.P[state][action][0]
            potential_reward = reward + gamma * V[new_state]
            # select the one that has the best reward
            # and also save the action to the policy
            if potential_reward > best_rewards:
                best_rewards = potential_reward
                best_action = action
        policy[state] = best_action
        V[state] = best_rewards
        # terminate if the change is not high
        delta = max(delta, abs(V[state] - old_v_s))
    if delta < 1e-30:
        break
if __name__ == '__main__':
    env = initialize_environment()
    # policy = policy_iteration(env)
    policy = value_iteration(env)
    play(policy, render=True)

因此,我们已经成功实现了 Taxi-v3 环境中的策略迭代和值迭代。

在下一个活动中,我们将使用非常流行的 FrozenLake-v0 环境来进行策略迭代和值迭代。在我们开始之前,让我们快速了解一下该环境的基本情况。

FrozenLake-v0 环境

该环境基于一个场景,场景中有一个冰冻湖,除了部分地方冰面已经融化。假设一群朋友在湖边玩飞盘,其中一个人投了一个远离的飞盘,飞盘正好落在湖中央。目标是穿越湖面并取回飞盘。现在,必须考虑的事实是,冰面非常滑,你不能总是按照预期的方向移动。这个表面用以下网格描述:

SFFF       (S: starting point, safe)
FHFH       (F: frozen surface, safe)
FFFH       (H: hole, fall to your doom)
HFFG       (G: goal, where the frisbee is located)

请注意,当其中一名玩家到达目标或掉进洞里时,回合结束。玩家分别会获得 1 或 0 的奖励。

现在,在 Gym 环境中,代理应该相应地控制玩家的移动。正如你所知,网格中的某些方格可以踩上去,而有些方格可能会把你直接带到冰面融化的洞里。因此,玩家的移动非常不可预测,部分取决于代理选择的方向。

注意

更多关于 FrozenLake-v0 环境的信息,请参见以下链接:gym.openai.com/envs/FrozenLake-v0/

现在,让我们实现策略迭代和值迭代技术来解决问题并取回飞盘。

活动 5.01:在 FrozenLake-v0 环境中实现策略迭代和值迭代

在本活动中,我们将通过策略迭代和值迭代来解决 FrozenLake-v0。该活动的目标是定义穿越冰冻湖的安全路径并取回飞盘。当目标达成或代理掉进洞里时,回合结束。以下步骤将帮助你完成此活动:

  1. 导入所需的库:numpygym

  2. 初始化环境并重置当前环境。在初始化器中设置 is_slippery=False。显示动作空间的大小和可能的状态数量。

  3. 执行策略评估迭代,直到最小的变化小于 smallest_change

  4. 使用贝尔曼最优性方程进行策略改进。

  5. 使用策略迭代找到 FrozenLake-v0 环境的最优策略。

  6. 在 FrozenLake-v0 环境上执行测试。

  7. 随机通过 FrozenLake-v0 环境进行步进。

  8. 执行值迭代,以找到 FrozenLake-v0 环境的最优策略。请注意,这里目标是确保每个行动的奖励值为 1(或接近 1),以确保最大奖励。

输出应类似于以下内容:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_05_26.jpg

图 5.26:期望输出平均分(1.0)

注意

本活动的解决方案可以在第 711 页找到。

因此,通过这项活动,我们成功地在 FrozenLake-v0 环境中实现了策略迭代和值迭代方法。

到此为止,我们已经完成了本章内容,你现在可以自信地将本章所学的技术应用于各种环境和场景中。

总结

在本章中,我们探讨了解决动态规划(DP)问题的两种最常用技术。第一种方法是备忘录法,也叫做自顶向下法,它使用字典(或类似 HashMap 的结构)以自然(无序)的方式存储中间结果。第二种方法是表格法,也叫自底向上的方法,它按顺序从小到大解决问题,并通常将结果保存在类似矩阵的结构中。

接下来,我们还探讨了如何使用动态规划(DP)通过策略和值迭代解决强化学习(RL)问题,以及如何通过使用修改后的贝尔曼方程克服策略迭代的缺点。我们在两个非常流行的环境中实现了策略迭代和值迭代:Taxi-v3 和 FrozenLake-v0。

在下一章,我们将学习蒙特卡罗方法,它用于模拟现实世界的场景,并且是金融、机械学和交易等领域中最广泛使用的工具之一。

第六章:6. 蒙特卡洛方法

概述

在这一章中,你将学习各种类型的蒙特卡洛方法,包括首次访问和每次访问技术。如果环境的模型未知,你可以通过生成经验样本或通过仿真来使用蒙特卡洛方法学习环境。本章将教授你重要性采样,并教你如何应用蒙特卡洛方法解决冰湖问题。到本章结束时,你将能够识别可以应用蒙特卡洛方法的强化学习问题。你将能够使用蒙特卡洛强化学习解决预测、估计和控制问题。

介绍

在上一章中,我们学习了动态规划。动态规划是一种在已知环境模型的情况下进行强化学习的方法。强化学习中的智能体可以学习策略、价值函数和/或模型。动态规划帮助解决已知的马尔可夫决策过程MDP)。在 MDP 中,所有可能转换的概率分布都是已知的,并且这是动态规划所必需的。

那么,当环境模型未知时会发生什么呢?在许多现实生活中的情况下,环境模型是事先未知的。那么算法是否能够学习到环境的模型呢?强化学习中的智能体是否仍然能够学会做出正确的决策呢?

蒙特卡洛方法是在环境模型未知时的一种学习方式,因此它们被称为无模型学习。我们可以进行无模型预测,估计未知 MDP 的价值函数。我们还可以使用无模型控制,优化未知 MDP 的价值函数。蒙特卡洛方法也能够处理非马尔可夫领域。

在许多情况下,状态之间的转换概率是未知的。你需要先进行试探,熟悉环境,然后才能学会如何玩好这个游戏。蒙特卡洛方法可以通过经历环境来学习环境的模型。蒙特卡洛方法通过实际或随机仿真场景来获得样本回报的平均值。通过使用来自与环境实际或模拟交互的状态、动作和回报的样本序列,蒙特卡洛方法可以通过经验学习。当蒙特卡洛方法工作时,需要一个明确的回报集合。这个标准仅在情节任务中满足,其中经验被划分为明确定义的情节,并且无论选择的动作如何,情节最终都会终止。一个应用示例是 AlphaGo,它是最复杂的游戏之一;任何状态下可能的动作数量超过 200。用来解决它的关键算法之一是基于蒙特卡洛的树搜索。

在本章中,我们将首先了解蒙特卡洛强化学习方法。我们将把它们应用到 OpenAI 的二十一点环境中。我们将学习各种方法,如首次访问法和每次访问法。我们还将学习重要性采样,并在本章后面重新审视冻结湖问题。在接下来的部分中,我们将介绍蒙特卡洛方法的基本原理。

蒙特卡洛方法的原理

蒙特卡洛方法通过对每个状态-动作对的样本回报进行平均,来解决强化学习问题。蒙特卡洛方法仅适用于情节任务。这意味着经验被分成多个情节,所有情节最终都会结束。只有在情节结束后,价值函数才会被重新计算。蒙特卡洛方法可以逐集优化,但不能逐步优化。

让我们以围棋为例。围棋有数百万种状态;在事先学习所有这些状态及其转移概率将会很困难。另一种方法是反复进行围棋游戏,并为胜利分配正奖励,为失败分配负奖励。

由于我们不了解模型的策略,需要使用经验样本来学习。这种技术也是一种基于样本的模型。我们称之为蒙特卡洛中的情节直接采样。

蒙特卡洛是无模型的。由于不需要了解 MDP(马尔科夫决策过程),模型是从样本中推断出来的。你可以执行无模型的预测或无模型的估计。我们可以对一个策略进行评估,也称为预测。我们还可以评估并改进一个策略,这通常被称为控制或优化。蒙特卡洛强化学习只能从终止的情节中学习。

例如,如果你玩的是一盘棋,按照一套规则或策略进行游戏,那么你就是根据这些规则或策略进行多个情节,并评估策略的成功率。如果我们根据某个策略进行游戏,并根据游戏的结果调整该策略,那就属于策略改进、优化或控制。

通过二十一点理解蒙特卡洛方法

二十一点是一种简单的卡牌游戏,在赌场中非常流行。这是一款非常棒的游戏,因为它简单易模拟并且容易进行采样,适合蒙特卡洛方法。二十一点也可以作为 OpenAI 框架的一部分。玩家和庄家各发两张牌。庄家亮出一张牌,另一张牌面朝下。玩家和庄家可以选择是否要继续发牌:

  • 游戏的目标:获得一副卡牌,其点数之和接近或等于 21,但不超过 21。

  • 玩家:有两个玩家,分别称为玩家和庄家。

  • 游戏开始:玩家被发两张牌,庄家也被发两张牌,剩余的牌堆放在一边。庄家的其中一张牌展示给玩家。

  • 可能的行动停牌或要牌:"停牌"是指停止要求更多的牌。“要牌"是指要求更多的牌。如果玩家手牌的总和小于 17,玩家将选择"要牌”。如果手牌总和大于或等于 17,玩家将选择停牌。是否要牌或停牌的阈值为 17,可以根据需要在不同版本的二十一点中进行调整。在本章中,我们将始终保持这个 17 的阈值,决定是否要牌或停牌。

  • 奖励:赢得一局为 +1,输掉一局为 -1,平局为 0。

  • 策略:玩家需要根据庄家的手牌决定是否停牌或要牌。根据其他牌的点数,王牌可以被视为 1 或 11。

我们将在下表中解释二十一点游戏。该表包含以下列:

  • 游戏:游戏编号和游戏的子状态:i、ii 或 iii

  • 玩家手牌:玩家拥有的牌;例如,K♣, 8♦ 表示玩家有一张梅花国王和一张方块八。

  • 庄家手牌:庄家获得的牌。例如,8♠, Xx 表示庄家有一张黑桃八和一张隐藏牌。

  • 行动:这是玩家决定选择的行动。

  • 结果:根据玩家的行动和庄家手牌的情况,游戏的结果。

  • 玩家手牌总和:玩家两张牌的总和。请注意,国王(K)、皇后(Q)和杰克(J)面牌的点数为 10。

  • 评论:解释为什么采取了某个特定行动或宣布了某个结果。

在游戏 1 中,玩家选择了停牌,因为手牌总和为 18。 "停牌"意味着玩家将不再接收牌。现在庄家展示了隐藏牌。由于庄家和玩家的手牌总和都是 18,结果为平局。在游戏 2 中,玩家的手牌总和为 15,小于 17。玩家要牌并获得另一张牌,总和变为 17。然后玩家停牌,不再接收牌。庄家展示了手牌,由于手牌总和小于 17,庄家要牌。庄家得到新的一张牌,总和为 25,超过了 21。游戏的目标是尽量接近或等于 21,而不超过 21。庄家失败,玩家赢得了第二局。以下图展示了此游戏的总结:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_01.jpg

图 6.1:二十一点游戏的解释

接下来,我们将使用 OpenAI 框架实现一款二十一点游戏。这将作为蒙特卡洛方法的模拟和应用的基础。

练习 6.01:在二十一点中实现蒙特卡洛方法

我们将学习如何使用 OpenAI 框架来玩二十一点,并了解观察空间、动作空间和生成回合。此练习的目标是在二十一点游戏中实现蒙特卡罗技术。

执行以下步骤完成练习:

  1. 导入必要的库:

    import gym
    import numpy as np
    from collections import defaultdict
    from functools import partial
    

    gym是 OpenAI 框架,numpy是数据处理框架,defaultdict用于字典支持。

  2. 我们使用gym.make()启动Blackjack环境,并将其分配给env

    #set the environment as blackjack
    env = gym.make('Blackjack-v0')
    

    找出观察空间和动作空间的数量:

    #number of observation space value
    print(env.observation_space)
    #number of action space value
    print(env.action_space)    
    

    你将得到以下输出:

    Tuple(Discrete(32), Discrete(11), Discrete(2))
    Discrete(2)
    

    观察空间的数量是状态的数量。动作空间的数量是每个状态下可能的动作数。输出结果显示为离散型,因为二十一点游戏中的观察和动作空间不是连续的。例如,OpenAI 中还有其他游戏,如平衡杆和摆钟,这些游戏的观察和动作空间是连续的。

  3. 编写一个函数来玩游戏。如果玩家的卡片总和大于或等于 17,则停牌(不再抽卡);否则,抽牌(选择更多卡片),如以下代码所示:

    def play_game(state):
        player_score, dealer_score, usable_ace = state 
        #if player_score is greater than 17, stick
        if (player_score >= 17):
            return 0 # don't take any cards, stick
        else:
            return 1 # take additional cards, hit
    

    在这里,我们初始化回合,选择初始状态,并将其分配给player_scoredealer_scoreusable_ace

  4. 添加一个字典action_text,它将两个动作整数映射到相应的动作文本。以下是将动作的整数值转换为文本格式的代码:

    for game_num in range(100):
        print('***Start of Game:', game_num)
        state = env.reset()
        action_text = {1:'Hit, Take more cards!!', \
                       0:'Stick, Dont take any cards' }
        player_score, dealer_score, usable_ace = state
        print('Player Score=', player_score,', \
              Dealer Score=', dealer_score, ', \
              Usable Ace=', usable_ace)
    
  5. 以每 100 个回合的批次玩游戏,并计算staterewardaction

        for i in range(100):
            action = play_game(state)
            state, reward, done, info = env.step(action)
            player_score, dealer_score, usable_ace = state
            print('Action is', action_text[action])
            print('Player Score=', player_score,', \
                  Dealer Score=', dealer_score, ', \
                  Usable Ace=', usable_ace, ', Reward=', reward)
            if done:
                if (reward == 1):
                    print('***End of Game:', game_num, \
                          ' You have won Black Jack!\n')
                elif (reward == -1):
                    print('***End of Game:', game_num, \
                          ' You have lost Black Jack!\n')
                elif (reward ==0):
                    print('***End of Game:', game_num, \
                          ' The game is a Draw!\n') 
                break
    

    你将得到以下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_02.jpg

图 6.2:输出的是正在进行的二十一点游戏的回合

注意

蒙特卡罗技术基于生成随机样本。因此,同一段代码的两次执行结果值可能不同。所以,你可能会得到类似的输出,但并不完全相同,适用于所有练习和活动。

在代码中,done的值为TrueFalse。如果doneTrue,游戏结束,我们记录奖励值并打印游戏结果。在输出中,我们使用蒙特卡罗方法模拟了二十一点游戏,并记录了不同的动作、状态和游戏完成情况。我们还模拟了游戏结束时的奖励。

注意

要访问此特定章节的源代码,请参考packt.live/2XZssYh

你也可以在packt.live/2Ys0cMJ在线运行这个示例。

接下来,我们将描述两种不同的蒙特卡罗方法,即首次访问法和每次访问法,这些方法将用于估计值函数。

蒙特卡罗方法的类型

我们使用蒙特卡洛实现了黑杰克游戏。通常,蒙特卡洛轨迹是一个状态、动作和奖励的序列。在多个回合中,可能会出现状态重复。例如,轨迹可能是 S0,S1,S2,S0,S3。我们如何在状态多次访问时处理奖励函数的计算呢?

从广义上讲,这突出了两种蒙特卡洛方法——首次访问和每次访问。我们将理解这两种方法的含义。

如前所述,在蒙特卡洛方法中,我们通过平均奖励来逼近值函数。在首次访问蒙特卡洛方法中,只有在一个回合中首次访问某个状态时才会被用来计算平均奖励。例如,在某个迷宫游戏中,你可能会多次访问同一个地方。使用首次访问蒙特卡洛方法时,只有首次访问时的奖励才会被用于计算奖励。当智能体在回合中重新访问相同的状态时,奖励不会被纳入计算平均奖励中。

在每次访问蒙特卡洛中,每次智能体访问相同的状态时,奖励都会被纳入计算平均回报。例如,使用相同的迷宫游戏。每次智能体到达迷宫中的相同位置时,我们都会将该状态下获得的奖励纳入奖励函数的计算。

首次访问和每次访问都会收敛到相同的值函数。对于较少的回合,首次访问和每次访问之间的选择取决于具体的游戏和游戏规则。

让我们通过理解首次访问蒙特卡洛预测的伪代码来深入了解。

首次访问蒙特卡洛预测用于估算值函数

在用于估算值函数的首次访问蒙特卡洛预测的伪代码中,关键是计算值函数V(s)。Gamma 是折扣因子。折扣因子用于将未来的奖励减少到低于即时奖励:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_03.jpg

图 6.3:首次访问蒙特卡洛预测的伪代码

在首次访问中,我们所做的就是生成一个回合,计算结果值,并将结果附加到奖励中。然后我们计算平均回报。在接下来的练习中,我们将通过遵循伪代码中的步骤应用首次访问蒙特卡洛预测来估算值函数。首次访问算法的关键代码块是仅通过首次访问来遍历状态:

if current_state not in states[:i]:

考虑那些尚未访问过的states。我们通过1增加states的数量,使用增量方法计算值函数,并返回值函数。实现方式如下:

"""
only include the rewards of the states that have not been visited before
"""
            if current_state not in states[:i]:
                #increasing the count of states by 1
                num_states[current_state] += 1

                #finding the value_function by incremental method
                value_function[current_state] \
                += (total_rewards - value_function[current_state]) \
                / (num_states[current_state])
      return value_function

让我们通过下一个练习更好地理解这一点。

练习 6.02:使用首次访问蒙特卡洛预测估算黑杰克中的值函数

本次练习旨在理解如何应用首次访问蒙特卡洛预测来估计黑杰克游戏中的价值函数。我们将按照伪代码中概述的步骤一步步进行。

执行以下步骤以完成练习:

  1. 导入必要的库:

    import gym
    import numpy as np
    from collections import defaultdict
    from functools import partial
    

    gym是 OpenAI 的框架,numpy是数据处理框架,defaultdict用于字典支持。

  2. 在 OpenAI 中选择环境为Blackjack

    env = gym.make('Blackjack-v0')
    
  3. 编写policy_blackjack_game函数,该函数接受状态作为输入,并根据player_score返回01的动作:

    def policy_blackjack_game(state):
        player_score, dealer_score, usable_ace = state
        if (player_score >= 17):
            return 0 # don't take any cards, stick
        else:
            return 1 # take additional cards, hit
    

    在该函数中,如果玩家分数大于或等于17,则不再抽取更多牌。但如果player_score小于 17,则抽取更多牌。

  4. 编写一个生成黑杰克回合的函数。初始化episodestatesactionsrewards

    def generate_blackjack_episode():
        #initializing the value of episode, states, actions, rewards
        episode = []
        states = []
        actions = []
        rewards = []
    
  5. 重置环境,并将state的值设置为player_scoredealer_scoreusable_ace

       #starting the environment
        state = env.reset()
    
        """
        setting the state value to player_score, 
        dealer_score and usable_ace
        """
        player_score, dealer_score, usable_ace = state
    
  6. 编写一个函数从状态中生成动作。然后我们执行该动作,找到next_statereward

        while (True):
            #finding the action by passing on the state
            action = policy_blackjack_game(state)
            next_state, reward, done, info = env.step(action)
    
  7. 创建一个episodestateactionreward的列表,将它们附加到现有列表中:

            #creating a list of episodes, states, actions, rewards
            episode.append((state, action, reward))
            states.append(state)
            actions.append(action)
            rewards.append(reward)
    

    如果这一集已完成(done 为 true),我们就break跳出循环。如果没有,我们更新statenext_state并重复循环:

            if done:
                break
            state = next_state
    
  8. 我们从函数中返回episodesstatesactionsrewards

        return episode, states, actions, rewards
    
  9. 编写一个计算黑杰克价值函数的函数。第一步是初始化total_rewardsnum_statesvalue_function的值:

    def black_jack_first_visit_prediction(policy, env, num_episodes):
        """
        initializing the value of total_rewards, 
        number of states, and value_function
        """
        total_rewards = 0
        num_states = defaultdict(float)
        value_function = defaultdict(float)
    
  10. 生成一个episode,对于每个episode,我们按逆序查找所有states的总rewards

        for k in range (0, num_episodes):
            episode, states, actions, rewards = \
            generate_blackjack_episode()
            total_rewards = 0
            for i in range(len(states)-1, -1,-1):
                current_state = states[i]
                #finding the sum of rewards
                total_rewards += rewards[i]
    
  11. 考虑未访问过的states。我们将states的计数增加1,并使用增量方法计算价值函数,然后返回价值函数:

                """
                only include the rewards of the states that 
                have not been visited before
                """
                if current_state not in states[:i]:
                    #increasing the count of states by 1
                    num_states[current_state] += 1
    
                    #finding the value_function by incremental method
                    value_function[current_state] \
                    += (total_rewards \
                    - value_function[current_state]) \
                    / (num_states[current_state])
        return value_function
    
  12. 现在,执行首次访问预测 10,000 次:

    black_jack_first_visit_prediction(policy_blackjack_game, env, 10000)
    

    你将获得以下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_04.jpg

图 6.4:首次访问价值函数

首次访问的价值函数被打印出来。对于所有的状态,player_scoredealer_scoreusable_space的组合都有一个来自首次访问评估的价值函数值。以(16, 3, False): -0.625为例。这意味着玩家分数为16、庄家分数为3、可用的 A 牌为False的状态的价值函数为-0.625。集数和批次数是可配置的。

注意

要访问此特定部分的源代码,请参考packt.live/37zbza1

你也可以在在线运行这个例子:packt.live/2AYnhyH

本节我们已经覆盖了首次访问蒙特卡洛方法。下一节我们将理解每次访问蒙特卡洛预测以估计价值函数。

每次访问蒙特卡洛预测用于估计价值函数

在每次访问蒙特卡洛预测中,每次访问状态都用于奖励计算。我们有一个 gamma 因子作为折扣因子,用于相对于近期奖励对未来奖励进行折扣:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_05.jpg

图 6.5:每次访问蒙特卡洛预测的伪代码

主要的区别在于每次访问每一步,而不仅仅是第一次,来计算奖励。代码与第一次访问的练习类似,唯一不同的是在 Blackjack 预测函数中计算奖励。

以下这一行在第一次访问实现中检查当前状态是否之前未被遍历。这个检查在每次访问算法中不再需要:

if current_state not in states[:i]:

计算值函数的代码如下:

            #all the state values of every visit are considered
            #increasing the count of states by 1
            num_states[current_state] += 1

            #finding the value_function by incremental method
            value_function[current_state] \
            += (total_rewards - value_function[current_state]) \
            / (num_states[current_state])
    return value_function

在本练习中,我们将使用每次访问蒙特卡洛方法来估计值函数。

练习 6.03:用于估计值函数的每次访问蒙特卡洛预测

本练习旨在帮助理解如何应用每次访问蒙特卡洛预测来估计值函数。我们将一步一步地应用伪代码中概述的步骤。执行以下步骤以完成练习:

  1. 导入必要的库:

    import gym
    import numpy as np
    from collections import defaultdict 
    from functools import partial
    
  2. 在 OpenAI 中选择环境为 Blackjack

    env = gym.make('Blackjack-v0')
    
  3. 编写 policy_blackjack_game 函数,接受状态作为输入,并根据 player_score 返回 action01

    def policy_blackjack_game(state):
        player_score, dealer_score, usable_ace = state 
        if (player_score >= 17):
            return 0 # don't take any cards, stick
        else:
            return 1 # take additional cards, hit
    

    在该函数中,如果玩家的分数大于或等于17,则不再抽取牌。但如果player_score小于17,则会继续抽取牌。

  4. 编写一个生成 Blackjack 回合的函数。初始化 episodestatesactionsrewards

    def generate_blackjack_episode():
        #initializing the value of episode, states, actions, rewards
        episode = []
        states = []
        actions = []
        rewards = []
    
  5. 我们重置环境,并将 state 的值设置为 player_scoredealer_scoreusable_ace,如以下代码所示:

        #starting the environment
        state = env.reset()
        """
        setting the state value to player_score, dealer_score and 
        usable_ace
        """
        player_score, dealer_score, usable_ace = state
    
  6. 编写一个函数,通过 state 生成 action,然后通过 action 步骤找到 next_statereward

        while (True):
            #finding the action by passing on the state
            action = policy_blackjack_game(state)       
            next_state, reward, done, info = env.step(action)
    
  7. 通过将 episodestateactionreward 添加到现有列表中,创建一个列表:

            #creating a list of episodes, states, actions, rewards
            episode.append((state, action, reward))
            states.append(state)
            actions.append(action)
            rewards.append(reward)
    
  8. 如果回合完成(done 为真),我们就 break 循环。如果没有完成,我们更新 statenext_state 并重复循环:

            if done:
                break
            state = next_state
    
  9. 从函数中返回 episodesstatesactionsrewards

        return episode, states, actions, rewards
    
  10. 编写用于计算 Blackjack 值函数的函数。第一步是初始化 total_rewardsnum_statesvalue_function 的值:

    def black_jack_every_visit_prediction\
    (policy, env, num_episodes):
        """
        initializing the value of total_rewards, number of states, 
        and value_function
        """
        total_rewards = 0
        num_states = defaultdict(float)
        value_function = defaultdict(float)
    
  11. 生成一个 episode,对于该 episode,我们在 episode 中逆序找到所有 states 的总 rewards

        for k in range (0, num_episodes):
            episode, states, actions, rewards = \
            generate_blackjack_episode() 
            total_rewards = 0
            for i in range(len(states)-1, -1,-1):
                current_state = states[i]
                #finding the sum of rewards
                total_rewards += rewards[i]
    
  12. 考虑每个访问的 state。我们将 states 的计数增加 1,并通过增量方法计算值函数,然后返回该值函数:

                #all the state values of every visit are considered
                #increasing the count of states by 1
                num_states[current_state] += 1
                #finding the value_function by incremental method
                value_function[current_state] \
                += (total_rewards - value_function[current_state]) \
                / (num_states[current_state])
        return value_function
    
  13. 现在,执行每次访问预测 10,000 次:

    black_jack_every_visit_prediction(policy_blackjack_game, \
                                      env, 10000)
    

    你将得到以下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_06.jpg

图 6.6:每次访问值函数

每次访问的价值函数都会被打印出来。对于所有状态,player_scoredealer_scoreusable_space的组合都有来自每次访问评估的价值函数值。我们还可以增加训练回合数,并再次运行此操作。随着回合数的增大,首次访问和每次访问的函数将逐渐收敛。

注意

要访问此特定部分的源代码,请参考packt.live/2C0wAP4

您还可以在packt.live/2zqXsH3在线运行此示例。

在下一节中,我们将讨论蒙特卡洛强化学习的一个关键概念,即探索与利用的平衡需求。这也是蒙特卡洛方法的贪婪ε策略的基础。平衡探索和利用有助于我们改进策略函数。

探索与利用的权衡

学习是通过探索新事物以及利用或应用之前学到的知识来进行的。这两者的正确结合是任何学习的核心。同样,在强化学习的背景下,我们也有探索和利用。探索是尝试不同的动作,而利用则是采取已知能带来良好奖励的动作。

强化学习必须在探索和利用之间取得平衡。每个智能体只能通过尝试某个动作的经验来学习。探索有助于尝试新的动作,这可能使智能体在未来做出更好的决策。利用是基于经验选择那些能带来良好奖励的动作。智能体需要在通过探索实验来获取奖励和通过利用已知路径来获得奖励之间做出权衡。如果智能体更多地进行利用,可能会错过学习其他更有回报的策略的机会。如果智能体更多地进行探索,可能会错失利用已知路径并失去奖励的机会。

例如,想象一个学生正在努力在大学中最大化自己的成绩。这个学生可以通过选修新学科的课程来“探索”,或者通过选修自己喜欢的课程来“利用”。如果学生倾向于“利用”,他可能会错过在新学科课程中获得好成绩和整体学习的机会。如果学生通过选修太多不同的学科课程来进行探索,这可能会影响他的成绩,并且可能让学习变得过于宽泛。

类似地,如果你选择阅读书籍,你可以通过阅读同一类型或同一作者的书籍来进行“开发”或通过跨越不同类型和作者的书籍来进行“探索”。类似地,当你从一个地方开车到另一个地方时,你可以通过基于过去经验沿用相同的已知路线来进行“开发”或通过选择不同的路线来进行“探索”。在下一部分中,我们将了解“在政策学习”和“脱政策学习”的技术。然后,我们将了解一个名为重要性采样的关键因素,它对于脱政策学习非常重要。

探索与开发是强化学习中常用的技术。在脱政策学习中,你可以将开发技术作为目标策略,而将探索技术作为行为策略。我们可以把贪婪策略作为开发技术,把随机策略作为探索技术。

重要性采样

蒙特卡洛方法可以是“在政策”或“脱政策”的。在在政策学习中,我们从代理遵循的策略经验中进行学习。在脱政策学习中,我们学习如何从遵循不同行为策略的经验中估计目标策略。重要性采样是脱政策学习的关键技术。下图展示了在政策与脱政策学习的对比:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_07.jpg

图 6.7:在政策与脱政策的比较

你可能会认为,在政策学习是在玩耍时学习,而脱政策学习是在观看别人玩耍时学习。你可以通过自己玩板球来提高你的板球水平。这有助于你从自己的错误和最佳行动中学习。这就是在政策学习。你也可以通过观察别人玩板球来学习,并从他们的错误和最佳行动中学习。这就是脱政策学习。

人类通常会同时进行在政策和脱政策学习。例如,骑自行车主要是属于在政策学习。我们通过学习在骑车时保持平衡来学习骑车。跳舞则是一种脱政策学习;你通过观察别人跳舞来学习舞步。

与脱政策方法相比,在政策方法较为简单。脱政策方法更强大,因为它具有“迁移学习”的效果。在脱政策方法中,你是从不同的策略中学习,收敛速度较慢,方差较大。

脱政策学习的优点在于,行为策略可以非常具有探索性,而目标策略可以是确定性的,并贪婪地优化奖励。

脱政策强化方法基于一个名为重要性采样的概念。该方法帮助在一个政策概率分布下估计值,前提是你拥有来自另一个政策概率分布的样本。让我们通过详细的伪代码理解蒙特卡洛脱政策评估。接着我们将在 OpenAI 框架中将其应用到 21 点游戏。

蒙特卡洛脱策略评估的伪代码

我们在下图中看到的是,我们正在通过从行为策略b中学习来估计Q(s,a)

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_08.jpg

图 6.8:蒙特卡洛脱策略评估的伪代码

目标策略是贪婪策略;因此,我们通过使用argmax Q(s,a)选择具有最大回报的动作。Gamma 是折扣因子,它使我们能够将远期奖励与未来即时奖励进行折扣。累积价值函数C(s,a)通过加权W来计算。Gamma 用于折扣奖励。

脱策略蒙特卡洛的核心是遍历每个回合:

for step in range(len(episode))[::-1]:
            state, action, reward = episode[step]
            #G <- gamma * G + Rt+1
            G = discount_factor * G + reward    
            # C(St, At) = C(St, At) + W
            C[state][action] += W
            #Q (St, At) <- Q (St, At) + W / C (St, At)
            Q[state][action] += (W / C[state][action]) \
            * (G - Q[state][action])
            """
            If action not equal to argmax of target policy 
            proceed to next episode
            """
            if action != np.argmax(target_policy(state)):
                break
            # W <- W * Pi(At/St) / b(At/St)
            W = W * 1./behaviour_policy(state)[action]

让我们通过使用重要性采样来理解蒙特卡洛脱策略方法的实现。这个练习将帮助我们学习如何设置目标策略和行为策略,并从行为策略中学习目标策略。

练习 6.04:使用蒙特卡洛进行重要性采样

这个练习的目标是通过使用蒙特卡洛方法进行脱策略学习。我们选择了一个贪婪的目标策略。我们也有一个行为策略,即任何软性、非贪婪策略。通过从行为策略中学习,我们将估计目标策略的价值函数。我们将把这种重要性采样技术应用到 Blackjack 游戏环境中。我们将按步骤执行伪代码中概述的步骤。

执行以下步骤以完成该练习:

  1. 导入必要的库:

    import gym
    import numpy as np
    from collections import defaultdict
    from functools import partial
    
  2. 使用gym.make选择 OpenAI 中的Blackjack环境:

    env = gym.make('Blackjack-v0')
    
  3. 创建两个策略函数。一个是随机策略。随机策略选择一个随机动作,它是一个大小为 n 的列表,每个动作有 1/n 的概率,其中 n 是动作的数量:

    """
    creates a random policy which is a linear probability distribution
    num_Action is the number of Actions supported by the environment
    """
    def create_random_policy(num_Actions): 
    #Creates a list of size num_Actions, with a fraction 1/num_Actions.
    #If 2 is numActions, the array value would [1/2, 1/2]
        Action = np.ones(num_Actions, dtype=float)/num_Actions
        def policy_function(observation):
            return Action
        return policy_function
    
  4. 编写一个函数来创建贪婪策略:

    #creates a greedy policy,
    """
    sets the value of the Action at the best_possible_action, 
    that maximizes the Q, value to be 1, rest to be 0
    """
    def create_greedy_policy(Q):
        def policy_function(state):
            #Initializing with zero the Q
            Action = np.zeros_like(Q[state], dtype = float)
            #find the index of the max Q value 
            best_possible_action = np.argmax(Q[state])
            #Assigning 1 to the best possible action
            Action[best_possible_action] = 1.0
            return Action
        return policy_function
    

    贪婪策略选择一个最大化奖励的动作。我们首先识别best_possible_action,即Q在所有状态中的最大值。然后,我们将值分配给对应于best_possible_actionAction

  5. 定义一个用于 Blackjack 重要性采样的函数,该函数以envnum_episodesbehaviour_policydiscount_factor作为参数:

    def black_jack_importance_sampling\
    (env, num_episodes, behaviour_policy, discount_factor=1.0):
            #Initialize the value of Q
            Q = defaultdict(lambda: np.zeros(env.action_space.n))
            #Initialize the value of C
            C = defaultdict(lambda: np.zeros(env.action_space.n))
            #target policy is the greedy policy
            target_policy = create_greedy_policy(Q)
    

    我们初始化QC的值,并将目标策略设为贪婪策略。

  6. 我们按回合数循环,初始化 episode 列表,并通过env.reset()声明初始状态集:

            for i_episode in range(1, num_episodes + 1):
                episode = []
                state = env.reset()
    
  7. 对于 100 个批次,在某个状态下应用行为策略来计算概率:

                for i in range(100):
                    probability = behaviour_policy(state)
                    action = np.random.choice\
                             (np.arange(len(probability)), p=probability)
                    next_state, reward, done, info = env.step(action)
                    episode.append((state, action, reward))
    

    我们从列表中随机选择一个动作。用随机动作执行一步,返回next_statereward。将stateactionreward附加到 episode 列表中。

  8. 如果episode完成,我们跳出循环并将next_state赋值给state

                    if done:
                        break
                    state = next_state 
    
  9. 初始化G,结果为0,并将W和权重设为1

                   # G <- 0
                         G = 0.0
                         # W <- 0
                         W = 1.0  
    
  10. 使用for循环执行伪代码中详细描述的步骤,如下代码所示:

                """
                Loop for each step of episode t=T-1, T-2,...,0 
                while W != 0
                """
                for step in range(len(episode))[::-1]:
                    state, action, reward = episode[step]
                    #G <- gamma * G + Rt+1
                    G = discount_factor * G + reward
                    # C(St, At) = C(St, At) + W
                    C[state][action] += W
                    #Q (St, At) <- Q (St, At) + W / C (St, At)
                    Q[state][action] += (W / C[state][action]) \
                    * (G - Q[state][action])
                    """
                    If action not equal to argmax of target policy 
                    proceed to next episode
                    """
                    if action != np.argmax(target_policy(state)):
                        break
                    # W <- W * Pi(At/St) / b(At/St)
                    W = W * 1./behaviour_policy(state)[action]
    
  11. 返回 Qtarget_policy

            return Q, target_policy 
    
  12. 创建一个随机策略:

    #create random policy
    random_policy = create_random_policy(env.action_space.n)
    """
    using importance sampling evaluates the target policy 
    by learning from the behaviour policy
    """
    Q, policy = black_jack_importance_sampling\
                (env, 50000, random_policy)
    

    随机策略作为行为策略使用。我们传入行为策略,并使用重要性采样方法,获得 Q 值函数或目标策略。

  13. 遍历 Q 中的项,然后找到具有最大值的动作。然后将其作为相应状态的值函数存储:

    valuefunction = defaultdict(float)
    for state, action_values in Q.items():
        action_value = np.max(action_values)
        valuefunction[state] = action_value
        print("state is", state, "value is", valuefunction[state])
    

    你将得到如下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_09.jpg

图 6.9:离策略蒙特卡罗评估输出

离策略评估已经计算并返回了每个状态-动作对的值函数。在这个练习中,我们使用行为策略应用了重要性采样的概念,并将学习应用于目标策略。输出为每个状态-动作对的组合提供了结果。这帮助我们理解了离策略学习。我们有两个策略——行为策略和目标策略。我们通过遵循行为策略学习目标策略。

注意

要访问此部分的源代码,请参考 packt.live/3hpOOKa

你也可以在线运行此示例:packt.live/2B1GQGa

在接下来的章节中,我们将学习如何使用蒙特卡罗技术解决 OpenAI 框架中的冰冻湖问题。

使用蒙特卡罗解决冰冻湖问题

冰冻湖是 OpenAI 框架中另一个简单的游戏。这是一个经典游戏,你可以用蒙特卡罗强化学习进行采样和模拟。我们已经在 第五章动态规划 中描述并使用了冰冻湖环境。在这里,我们将快速复习游戏的基础知识,以便在接下来的活动中使用蒙特卡罗方法解决它。

我们有一个 4x4 的网格,这就是整个冰冻湖。它包含 16 个格子(一个 4x4 的网格)。这些格子标记为 S – 起始点,F – 冰冻区域,H – 坑洞,G – 目标。玩家需要从起始格子 S 移动到目标格子 G,并且穿过冰冻区域(F 格子),避免掉进坑洞(H 格子)。下图直观地展示了上述信息:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_10.jpg

图 6.10:冰冻湖游戏

下面是游戏的一些基本信息:

  • S)到达目标(G 格子)。

  • 状态 = 16

  • 动作 = 4

  • 总状态-动作对 = 64

  • F)避免掉进湖中的坑洞(H 格子)。到达目标(G 格子)或掉进任何坑洞(H 格子)都会结束游戏。

  • 动作:在任意一个格子中可以执行的动作有:左、下、右、上。

  • 玩家:这是一个单人游戏。

  • F),到达目标(G 格子)得 +1,掉进坑洞(H 格子)得 0。

  • 配置:你可以配置冰湖是滑的还是不滑的。如果冰湖是滑的,那么预期的动作和实际动作可能会有所不同,因此如果有人想向左移动,他们可能最终会向右、向下或向上移动。如果冰湖是非滑的,预期的动作和实际动作始终对齐。该网格有 16 个可能的单元,代理可以在任何时刻处于其中一个单元。代理可以在每个单元中执行 4 种可能的动作。因此,游戏中有 64 种可能性,这些可能性会根据学习过程不断更新。在下一次活动中,我们将深入了解 Frozen Lake 游戏,并了解其中的各种步骤和动作。

活动 6.01:探索 Frozen Lake 问题 – 奖励函数

Frozen Lake 是 OpenAI Gym 中的一款游戏,有助于应用学习和强化学习技术。在本次活动中,我们将解决 Frozen Lake 问题,并通过蒙特卡罗方法确定各种状态和动作。我们将通过一批批的回合来跟踪成功率。

执行以下步骤以完成活动:

  1. 我们导入必要的库:gym 用于 OpenAI Gym 框架,numpy,以及处理字典所需的 defaultdict

  2. 下一步是选择环境为 FrozenLake,并将 is_slippery 设置为 False。通过 env.reset() 重置环境,并通过 env.render() 渲染环境。

  3. 观察空间中可能的值的数量通过 print(env.observation_space) 打印输出。同样,动作空间中的值的数量通过 print(env.action_space) 命令打印输出。

  4. 下一步是定义一个函数来生成一个冰湖 episode。我们初始化回合和环境。

  5. 我们通过使用蒙特卡罗方法模拟不同的回合。然后我们逐步导航,存储 episode 并返回 reward。通过 env.action_space.sample() 获取动作。next_stateactionreward 通过调用 env_step(action) 函数获得。然后将它们附加到回合中。现在,回合变成了一个包含状态、动作和奖励的列表。

  6. 关键是计算成功率,即一批回合的成功概率。我们的方法是计算一批回合中的总尝试次数。我们计算其中有多少次成功到达目标。代理成功到达目标的次数与代理尝试次数的比率即为成功率。首先,我们初始化总奖励。

  7. 我们为每次迭代生成 episodereward,并计算总 reward

  8. 成功率是通过将 total_reward 除以 100 来计算的,并打印输出。

  9. 冰湖预测通过 frozen_lake_prediction 函数计算得出。最终输出将展示游戏的默认成功率,即在没有任何强化学习的情况下,游戏随机进行时的成功率。

    你将获得以下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_11.jpg

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_11.jpg)

图 6.11:没有学习的冰湖输出

该活动的解决方案可以在第 719 页找到。

在下一节中,我们将详细介绍如何通过平衡探索和利用,使用 epsilon 软策略和贪婪策略来实现改进。这可以确保我们平衡探索和利用。

每次访问蒙特卡洛控制伪代码(用于 epsilon 软)

我们之前已经实现了每次访问蒙特卡洛算法来估算价值函数。在本节中,我们将简要描述用于 epsilon 软的每次访问蒙特卡洛控制,以便我们可以在本章的最终活动中使用它。下图展示了通过平衡探索和利用,针对 epsilon 软的每次访问伪代码:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_12.jpg

](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_12.jpg)

图 6.12:蒙特卡洛每次访问的伪代码(用于 epsilon 软)

以下代码以 epsilon 概率选择一个随机动作,并以 1-epsilon 概率选择一个具有最大 Q(s,a) 的动作。因此,我们可以在以 epsilon 概率进行探索和以 1-epsilon 概率进行利用之间做出选择:

while not done:

        #random action less than epsilon
        if np.random.rand() < epsilon:
            #we go with the random action
            action = env.action_space.sample()
        else:
            """
            1 - epsilon probability, we go with the greedy algorithm
            """
            action = np.argmax(Q[state, :])

在下一项活动中,我们将通过实现蒙特卡洛控制每次访问的 epsilon 软方法来评估和改进冰湖问题的策略。

活动 6.02 使用蒙特卡洛控制每次访问解决冰湖问题(epsilon 软)

本活动的目标是通过使用每次访问 epsilon 软方法来评估和改进冰湖问题的策略。

您可以通过导入 gym 并执行 gym.make() 来启动冰湖游戏:

import gym
env = gym.make("FrozenLake-v0", is_slippery=False)

执行以下步骤以完成活动:

  1. 导入必要的库。

  2. 选择环境为 FrozenLakeis_slippery 设置为 False

  3. Q 值和 num_state_action 初始化为零。

  4. num_episodes 的值设置为 100000,并创建 rewardsList。将 epsilon 设置为 0.30

  5. 循环运行直到 num_episodes。初始化环境、results_Listresult_sum 为零。同时重置环境。

  6. 现在我们需要同时进行探索和利用。探索将是一个具有 epsilon 概率的随机策略,利用将是一个具有 1-epsilon 概率的贪婪策略。我们开始一个while循环,并检查是否需要以 epsilon 的概率选择一个随机值,或者以 1-epsilon 的概率选择一个贪婪策略。

  7. 逐步执行 action 并获得 new_statereward

  8. 结果列表将被附加,包括状态和动作对。result_sum 会根据结果的值递增。

  9. new_state 赋值给 state,并将 result_sum 添加到 rewardsList 中。

  10. 使用增量方法计算 Q[s,a],公式为 Q[s,a] + (result_sum – Q[s,a]) / N(s,a)

  11. 打印每 1000 次的成功率值。

  12. 打印最终的成功率。

    您将最初获得以下输出:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_13.jpg

图 6.13:Frozen Lake 成功率的初始输出

最终你将得到以下输出:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_06_14.jpg

图 6.14:Frozen Lake 成功率的最终输出

注意

该活动的解决方案可以在第 722 页找到。

总结

蒙特卡洛方法通过样本情节的形式从经验中学习。在没有环境模型的情况下,智能体通过与环境互动,可以学习到一个策略。在多次仿真或抽样的情况下,情节是可行的。我们了解了首次访问和每次访问评估的方法。同时,我们也学习了探索与利用之间的平衡。这是通过采用一个ε软策略来实现的。接着,我们了解了基于策略学习和非基于策略学习,并且学习了重要性抽样在非基于策略方法中的关键作用。我们通过将蒙特卡洛方法应用于《黑杰克》游戏和 OpenAI 框架中的 Frozen Lake 环境来学习这些方法。

在下一章中,我们将学习时间学习及其应用。时间学习结合了动态规划和蒙特卡洛方法的优点。它可以在模型未知的情况下工作,像蒙特卡洛方法一样,但可以提供增量学习,而不是等待情节结束。

第七章:7. 时序差分学习

概览

在本章中,我们将介绍时序差分TD)学习,并重点讨论它如何发展蒙特卡罗方法和动态规划的思想。时序差分学习是该领域的关键主题之一,研究它使我们能够深入理解强化学习及其在最基本层面上的工作原理。新的视角将使我们看到蒙特卡罗方法是时序差分方法的一个特例,从而统一了这种方法,并将其适用性扩展到非情节性问题。在本章结束时,你将能够实现TD(0)SARSAQ-learning和**TD(λ)**算法,并用它们来解决具有随机和确定性转移动态的环境。

时序差分学习简介

在前几章学习了动态规划和蒙特卡罗方法之后,本章我们将重点讨论时序差分学习,这是强化学习的主要基石之一。我们将从它们最简单的形式——单步方法开始,然后在此基础上构建出它们最先进的形式,基于资格迹(eligibility traces)概念。我们将看到这种新方法如何使我们能够将时序差分和蒙特卡罗方法框架在相同的推导思想下,从而能够对比这两者。在本章中,我们将实现多种不同的时序差分方法,并将它们应用于 FrozenLake-v0 环境,涵盖确定性和随机环境动态。最后,我们将通过一种名为 Q-learning 的离策略时序差分方法解决 FrozenLake-v0 的随机版本。

时序差分学习,其名称来源于它通过在后续时间步之间比较状态(或状态-动作对)值的差异来进行学习,可以被视为强化学习算法领域的一个核心思想。它与我们在前几章中学习的方法有一些重要相似之处——事实上,就像那些方法一样,它通过经验进行学习,无需模型(像蒙特卡罗方法那样),并且它是“自举”的,意味着它能够在达到情节结束之前,利用已经获得的信息进行学习(就像动态规划方法那样)。

这些差异与时序差分方法相对于蒙特卡洛(MC)和动态规划(DP)方法的优势紧密相关:它不需要环境模型,并且相对于 DP 方法,它可以更广泛地应用。另一方面,它的引导能力使得时序差分方法更适合处理非常长的任务回合,并且是非回合任务的唯一解决方案——蒙特卡洛方法无法应用于这种任务。以长期或非回合任务为例,想象一个算法,它用于授予用户访问服务器的权限,每次将排队中的第一个用户分配到资源时会获得奖励,如果没有授予用户访问权限,则没有奖励。这个队列通常永远不会结束,因此这是一个没有回合的持续任务。

正如前几章所见,探索与利用的权衡是一个非常重要的话题,在时序差分算法中同样如此。它们分为两大类:在政策方法和脱离政策方法。正如我们在前面章节中所看到的,在在政策方法中,所学习的政策用于探索环境,而在脱离政策方法中,二者可以不同:一个用于探索,另一个是目标政策,旨在学习。在接下来的部分中,我们将讨论为给定政策估计状态价值函数的通用问题。然后,我们将看到如何基于它构建一个完整的强化学习算法,训练在政策和脱离政策方法,以找到给定问题的最优策略。

让我们从时序差分方法的世界开始第一步。

TD(0) – SARSA 和 Q-Learning

时序差分方法是无模型的,这意味着它们不需要环境模型来学习状态值表示。对于给定的策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_00a.png,它们累积与之相关的经验,并更新在相应经验中遇到的每个状态的值函数估计。在这个过程中,时序差分方法使用在接下来的时间步骤中遇到的状态(或状态)来更新给定状态值,状态是在时间t访问的,因此是t+1t+2、…、t+n。一个抽象的例子如下:一个智能体在环境中初始化并开始通过遵循给定的策略与环境互动,而没有任何关于哪个动作会生成哪些结果的知识。经过一定数量的步骤,智能体最终会到达一个与奖励相关的状态。该奖励信号用于通过时序差分学习规则增加先前访问的状态(或动作-状态对)的值。实际上,这些状态帮助智能体达到了目标,因此应该与较高的值相关联。重复这个过程将使得智能体构建一个完整且有意义的所有状态(或状态-动作对)的价值图,以便它利用所获得的知识选择最佳动作,从而达到与奖励相关的状态。

这意味着,TD 方法不需要等到本回合结束才改进策略;相反,它们可以基于遇到的状态的值进行构建,学习过程可以在初始化之后立即开始。

在本节中,我们将重点讨论所谓的一步法,也称为 TD(0)。在这种方法中,唯一被考虑用于构建给定状态价值函数更新的数值是下一个时间步的数值,别无其他。因此,举例来说,时间t时刻状态的价值函数更新如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01.jpg

图 7.1:时间’t’时刻状态的价值函数更新

这里,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01a.png是环境转移后的下一个状态,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01b.png是转移过程中获得的奖励,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01c.png是学习率,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01d.png是折扣因子。很明显,TD 方法是如何“自举”的:为了更新状态(t)的价值函数,它们使用下一个状态(t+1)的当前价值函数,而无需等待直到本回合结束。值得注意的是,前面方程中方括号内的量可以被解释为误差项。这个误差项衡量了状态 St 的估计值与新的、更好的估计值之间的差异,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_01e.png。这个量被称为 TD 误差,我们将在强化学习理论中多次遇到它:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_02.jpg

图 7.2:时间’t’时刻的 TD 误差

该误差是针对计算时所使用的特定时间而言的,它依赖于下一个时间步的数值(即,时间 t 的误差依赖于时间t+1的数值)。

TD 方法的一个重要理论结果是它们的收敛性证明:事实上,已经证明,对于任何固定的策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_02a.png,前面方程中描述的算法 TD(0)会收敛到状态(或状态-动作对)价值函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_02b.png。当步长参数足够小且为常数时,收敛是可以达到的,并且如果步长参数根据一些特定的(但容易遵循的)随机逼近条件减小,则以概率1收敛。这些证明主要适用于算法的表格版本,表格版本是用于强化学习理论介绍和理解的版本。这些版本处理的问题是状态和动作空间维度有限,因此可以通过有限变量组合进行穷举表示。

然而,当状态和动作空间如此庞大以至于不能通过有限变量的有限组合来表示时(例如,当状态空间是 RGB 图像空间时),这些证明的大多数可以轻松地推广到依赖于近似的算法版本时,这些近似版本被用于算法版本。

到目前为止,我们一直在处理状态值函数。为了解决时序差异控制的问题,我们需要学习一个状态-动作值函数,而不是一个状态值函数。事实上,通过这种方式,我们将能够为状态-动作对关联一个值,从而构建一个值映射,然后可以用来定义我们的策略。我们如何具体实现这一点取决于方法类别。首先,让我们看看所谓的在线策略方法,由所谓的 SARSA 算法实现,然后看看所谓的 Q 学习算法,它实现了离线策略方法。

SARSA – 在线策略控制

对于一个在线策略方法,目标是估计https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_02c.png,即当前行为策略下的状态-动作值函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_02d.png,适用于所有状态和所有动作。为此,我们只需将我们在状态值函数中看到的方程应用于状态-动作函数。由于这两种情况是相同的(都是马尔可夫链和奖励过程),关于状态值函数收敛到与最优策略相对应的值函数的定理(因此解决了找到最优策略的问题)在这种新设置中也是有效的,其中值函数涉及状态-动作对。更新方程如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03.jpg

图 7.3:时间‘t’时的状态-动作值函数

这种更新应该在每次从非终止状态https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03a.png转移到另一个状态后执行。如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03b.png是一个终止状态,则https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03c.png的值设为0。正如我们所见,更新规则使用了五元组https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03d.png的每个元素,这解释了与转换相关联的状态-动作对之间的转换,以及与转换相关联的奖励。正是因为这种形式的五元组,这个算法被称为SARSA

使用这些元素,可以很容易地基于它们设计一个基于在线策略的控制算法。正如我们之前提到的,所有在线策略方法都估计行为策略https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03e.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03e.png,同时基于https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03f.png更新https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03g.png。SARSA 控制算法的方案可以描述如下:

  1. 选择算法参数;即步长,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03i.png,该值必须在区间(0, 1]内,并且ε-贪心策略的ε参数必须小且大于 0,因为它表示选择非最优动作的概率,以便进行探索。这可以通过以下代码实现:

    alpha = 0.02
    epsilon = 0.05
    
  2. 初始化https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03j.png,对于所有的https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03k.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03l.png,随意设置,唯一例外是 Q(terminal, ·) = 0,如以下代码片段所示,在一个有16个状态和4个动作的环境中:

    q = np.ones((16,4))
    
  3. 为每个回合创建一个循环。初始化https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03m.png,并使用从 Q 导出的策略(例如,ε-贪心)从https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03o.png中选择https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03n.png。这可以通过以下代码片段实现,其中初始状态由环境的reset函数提供,动作通过专门的ε-贪心函数选择:

    for i in range(nb_episodes):
            s = env.reset()
            a = action_epsilon_greedy(q, s, epsilon=epsilon)
    
  4. 为每个步骤创建一个循环。执行动作https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03p.png并观察https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03q.png。从https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03s.png中选择https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03r.png,使用从 Q 导出的策略(例如,ε-贪心)。使用 SARSA 规则更新选定状态-动作对的状态-动作值函数,该规则将新值定义为当前值与 TD 误差乘以步长的和,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_03t.png,如以下表达式所示:![图 7.4:使用 SARSA 规则更新状态-动作值函数]

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_04.jpg)

图 7.4:使用 SARSA 规则更新状态-动作值函数

然后,使用https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_04a.png将新状态-动作对更新至旧状态-动作对,直到https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_04b.png是一个终止状态。所有这些都通过以下代码实现:

while not done:
            new_s, reward, done, info = env.step(a)
            new_a = action_epsilon_greedy(q, new_s, epsilon=epsilon)
            q[s, a] = q[s, a] + alpha * (reward + gamma \
                      * q[new_s, new_a] - q[s, a])
            s = new_s
            a = new_a

注意

该算法的步骤和代码最初由Sutton, Richard S. 《强化学习导论》。剑桥,马萨诸塞州:麻省理工学院出版社,2015 年开发并概述。

在以下条件下,SARSA 算法可以以概率1收敛到最优策略和最优动作值函数:

  • 所有的状态-动作对需要被访问无限多次。

  • 在极限情况下,该策略会收敛为贪心策略,这可以通过ε-贪心策略实现,其中ε随时间消失(这可以通过设置ε = 1/t来完成)。

本算法使用了 ε-greedy 算法。我们将在下一章详细解释这一点,因此这里只做简要回顾。当通过状态-动作值函数学习策略时,状态-动作对的值被用来决定采取哪个最佳动作。在收敛时,给定状态下会从可用的动作中选择最佳的那个,并选择具有最高值的动作:这就是贪婪策略。这意味着对于每个给定的状态,始终会选择相同的动作(如果没有动作具有相同的值)。这种策略对于探索来说并不是一个好选择,尤其是在训练的初期。因此,在这个阶段,优先采用 ε-greedy 策略:最佳动作的选择概率为 1-ε,而其他情况下则选择一个随机动作。随着 ε 渐变为 0,ε-greedy 策略最终会变成贪婪策略,且当步数趋近于无穷大时,ε-greedy 策略会趋近于贪婪策略。

为了巩固这些概念,让我们立即应用 SARSA 控制算法。以下练习将展示如何实现 TD(0) SARSA 来解决 FrozenLake-v0 环境,首先使用其确定性版本。

这里的目标是观察 SARSA 算法如何恢复最优策略,而我们人类可以提前估算出这一策略,针对给定的问题配置。在深入之前,我们先快速回顾一下冰湖问题是什么,以及我们希望代理人找到的最优策略。代理人看到的是一个 4 x 4 的网格世界。

该网格包含一个起始位置 S(左上角),冰冻的方块 F,洞 H,以及一个目标 G(右下角)。当代理人到达终极目标状态时,它会获得 +1 的奖励,而如果它到达由洞构成的终极状态,则该回合结束且没有奖励。下表表示了环境:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_05.jpg

图 7.5:FrozenLake-v0 环境

如前图所示,S是起始位置,F表示冰冻的方块,H表示空洞,G是目标。在确定性环境中,最优策略是能够让智能体在最短时间内到达目标的策略。严格来说,在这个特定环境中,由于没有对中间步骤的惩罚,因此最优路径不一定是最短的。每一条最终能够到达目标的路径在累计期望奖励方面都是同样最优的。然而,我们将看到,通过适当使用折扣因子,我们将能够恢复最优策略,而该策略也考虑了最短路径。在这种情况下,最优策略在下图中有所表示,其中每个四个动作(向下、向右、向左、向上)都由其首字母表示。有两个方块,对于它们来说,两个动作将导致相同的最优路径:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_06.jpg

图 7.6: 最优策略

在上面的图示中,D表示向下,R表示向右,U表示向上,L表示向左。!代表目标,而表示环境中的空洞。

我们将使用一个递减的ε值来逐步减少探索的范围,从而使其在极限时变为贪婪的。

这种类型的练习在学习经典强化学习算法时非常有用。由于是表格化的(这是一个网格世界示例,意味着它可以用一个 4x4 的网格表示),我们可以跟踪领域中发生的所有事情,轻松跟随在算法迭代过程中状态-动作对的值的更新,查看根据选定策略的动作选择,并收敛到最优策略。在本章中,你将学习如何在强化学习的背景下编写一个参考算法,并深入实践所有这些基本方面。

现在让我们继续进行实现。

练习 7.01: 使用 TD(0) SARSA 解决 FrozenLake-v0 确定性过渡

在这个练习中,我们将实现 SARSA 算法,并用它来解决 FrozenLake-v0 环境,在该环境中仅允许确定性过渡。这意味着我们将寻找(并实际找到)一个最优策略,以便在这个环境中取回飞盘。

以下步骤将帮助你完成这个练习:

  1. 导入所需的模块:

    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline
    import gym
    
  2. 实例化一个名为FrozenLake-v0gym环境。将is_slippery标志设置为False以禁用其随机性:

    env = gym.make('FrozenLake-v0', is_slippery=False)
    
  3. 看一下动作空间和观察空间:

    print("Action space = ", env.action_space)
    print("Observation space = ", env.observation_space)
    

    这将打印出以下内容:

    Action space =  Discrete(4)
    Observation space =  Discrete(16)
    
  4. 创建两个字典,以便轻松将动作编号转换为动作:

    actionsDict = {}
    actionsDict[0] = " L "
    actionsDict[1] = " D "
    actionsDict[2] = " R "
    actionsDict[3] = " U "
    actionsDictInv = {}
    actionsDictInv["L"] = 0
    actionsDictInv["D"] = 1
    actionsDictInv["R"] = 2
    actionsDictInv["U"] = 3
    
  5. 重置环境并渲染它,以便能够查看网格问题:

    env.reset()
    env.render()
    

    输出将如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_07.jpg

    图 7.7: 环境的初始状态

  6. 可视化该环境的最优策略:

    optimalPolicy = ["R/D"," R "," D "," L ", \
                     " D "," - "," D "," - ", \
                     " R ","R/D"," D "," - ", \
                     " - "," R "," R "," ! ",]
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出将如下所示:

    Optimal policy:  
    R/D  R   D  L
     D   -   D  -
     R  R/D  D  -
     -   R   R  !
    

    这表示该环境的最优策略,显示在 4x4 网格中表示的每个环境状态下,在四个可用动作中选择的最优动作:向上移动向下移动向右移动,和向左移动。除了两个状态外,所有其他状态都有唯一的最优动作。实际上,如前所述,最优动作是那些通过最短路径将智能体带到目标的动作。两个不同的可能性为两个状态产生相同的路径长度,因此它们是同样的最优解。

  7. 定义函数来执行ε-贪心动作。第一个函数实现了一个具有1 - ε概率的ε-贪心策略。选择的动作是与状态-动作对关联的最大值所对应的动作;否则,返回一个随机动作。第二个函数仅通过lambda函数在传递时调用第一个函数:

    def action_epsilon_greedy(q, s, epsilon=0.05):
        if np.random.rand() > epsilon:
            return np.argmax(q[s])
        return np.random.randint(4)
    def get_action_epsilon_greedy(epsilon):
        return lambda q,s: action_epsilon_greedy\
                           (q, s, epsilon=epsilon)
    
  8. 定义一个函数来执行贪婪动作:

    def greedy_policy(q, s):
        return np.argmax(q[s])
    
  9. 现在,定义一个函数来计算智能体表现的平均值。首先,我们将定义用于计算平均表现的集数(在此例中为500),然后在循环中执行所有这些集数。我们将重置环境并开始该集中的循环以进行此操作。接着,我们根据要衡量表现的策略选择一个动作,使用所选动作推进环境,最后将奖励添加到累积回报中。我们重复这些环境步骤,直到集数完成:

    def average_performance(policy_fct, q):
        acc_returns = 0.
        n = 500
        for i in range(n):
            done = False
            s = env.reset()
            while not done:
                a = policy_fct(q, s)
                s, reward, done, info = env.step(a)
                acc_returns += reward
        return acc_returns/n
    
  10. 设置总集数和步骤数,指定估算智能体的平均表现的频率,并设置ε参数,该参数决定其衰减方式。使用初始值、最小值和衰减范围(以集数为单位):

    nb_episodes = 80000
    STEPS = 2000
    epsilon_param = [[0.2, 0.001, int(nb_episodes/2)]]
    
  11. 将 SARSA 训练算法定义为一个函数。在此步骤中,Q 表被初始化。所有的值都等于1,但终止状态的值被设置为0

    def sarsa(alpha = 0.02, \
              gamma = 1., \
              epsilon_start = 0.1, \
              epsilon_end = 0.001, \
              epsilon_annealing_stop = int(nb_episodes/2), \
              q = None, \
              progress = None, \
              env=env):
        if q is None:
            q = np.ones((16,4))
            # Set q(terminal,*) equal to 0
            q[5,:] = 0.0
            q[7,:] = 0.0
            q[11,:] = 0.0
            q[12,:] = 0.0
            q[15,:] = 0.0
    
  12. 在所有集数中开始一个for循环:

        for i in range(nb_episodes):
    
  13. 在循环内,首先根据当前集数定义 epsilon 值:

            inew = min(i,epsilon_annealing_stop)
            epsilon = (epsilon_start \
                       *(epsilon_annealing_stop - inew)\
                       +epsilon_end * inew) / epsilon_annealing_stop
    
  14. 接下来,重置环境,并使用ε-贪心策略选择第一个动作:

            done = False
            s = env.reset()
            a = action_epsilon_greedy(q, s, epsilon=epsilon)
    
  15. 然后,我们开始一个集内循环:

            while not done:
    
  16. 在循环内,环境通过所选动作和新状态以及奖励进行推进,并获取 done 条件:

                new_s, reward, done, info = env.step(a)
    
  17. 选择一个新的动作,使用ε-贪心策略,通过 SARSA TD(0)规则更新 Q 表,并更新状态和动作的值:

                new_a = action_epsilon_greedy\
                        (q, new_s, epsilon=epsilon)
                q[s, a] = q[s, a] + alpha * (reward + gamma \
                          * q[new_s, new_a] - q[s, a])
                s = new_s
                a = new_a
    
  18. 最后,估算智能体的平均表现:

            if progress is not None and i%STEPS == 0:
                progress[i//STEPS] = average_performance\
                                     (get_action_epsilon_greedy\
                                     (epsilon), q=q)
        return q, progress
    

    提供ε参数减少的简要描述可能会有所帮助。这由三个参数决定:起始值、最小值和减少范围(称为epsilon_annealing_stop)。它们的使用方式如下:ε从起始值开始,然后在由参数“范围”定义的回合数中线性递减,直到达到最小值,之后保持不变。

  19. 定义一个数组,用于在训练过程中收集所有智能体的性能评估,以及 SARSA TD(0)训练的执行过程:

    sarsa_performance = np.ndarray(nb_episodes//STEPS)
    q, sarsa_performance = sarsa(alpha = 0.02, gamma = 0.9, \
                                 progress=sarsa_performance, \
                                 epsilon_start=epsilon_param[0][0],\
                                 epsilon_end=epsilon_param[0][1], \
                                 epsilon_annealing_stop = \
                                 epsilon_param[0][2])
    
  20. 绘制 SARSA 智能体在训练过程中平均奖励的历史记录:

    plt.plot(STEPS*np.arange(nb_episodes//STEPS), sarsa_performance)
    plt.xlabel("Epochs")
    plt.title("Learning progress for SARSA")
    plt.ylabel("Average reward of an epoch")
    

    这会生成以下输出:

    Text(0, 0.5, 'Average reward of an epoch')
    

    这可以通过以下方式进行可视化。它展示了 SARSA 算法的学习进展:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_08.jpg

    图 7.8:训练过程中每个周期的平均奖励趋势

    如我们所见,随着ε参数的退火,SARSA 的表现随着时间的推移不断提升,从而在极限时达到了0的值,从而获得了贪心策略。这也证明了该算法在学习后能够达到 100%的成功率。

  21. 评估经过训练的智能体(Q 表)在贪心策略下的表现:

    greedyPolicyAvgPerf = average_performance(greedy_policy, q=q)
    print("Greedy policy SARSA performance =", greedyPolicyAvgPerf)
    

    输出如下所示:

    Greedy policy SARSA performance = 1.0
    
  22. 显示 Q 表的值:

    q = np.round(q,3)
    print("(A,S) Value function =", q.shape)
    print("First row")
    print(q[0:4,:])
    print("Second row")
    print(q[4:8,:])
    print("Third row")
    print(q[8:12,:])
    print("Fourth row")
    print(q[12:16,:])
    

    输出如下所示:

    (A,S) Value function = (16, 4)
    First row 
    [[0.505 0.59  0.54  0.506]
     [0.447 0.002 0.619 0.494]
     [0.49  0.706 0.487 0.562]
     [0.57  0.379 0.53  0.532]]
    Second row
    [[0.564 0.656 0\.    0.503]
     [0\.    0\.    0\.    0\.   ]
     [0.003 0.803 0.002 0.567]
     [0\.    0\.    0\.    0\.   ]]
    Third row
    [[0.62  0\.    0.728 0.555]
     [0.63  0.809 0.787 0\.   ]
     [0.707 0.899 0\.    0.699]
     [0\.    0\.    0\.    0\.   ]]
    Fourth row
    [[0\.    0\.    0\.    0\.   ]
     [0\.    0.791 0.9   0.696]
     [0.797 0.895 1\.    0.782]
     [0\.    0\.    0\.    0\.   ]]
    

    该输出展示了我们问题的完整状态-动作值函数的值。这些值随后用于通过贪心选择规则生成最优策略。

  23. 打印出找到的贪心策略并与最优策略进行比较。在计算出状态-动作值函数后,我们可以从中提取出贪心策略。事实上,正如前面所解释的,贪心策略选择的是对于给定状态,Q 表中与之关联的最大值所对应的动作。为此,我们使用了argmax函数。将其应用于 16 个状态(从 0 到 15)中的每一个时,它返回与该状态相关的四个动作(从 0 到 3)中具有最大值的动作索引。在这里,我们还直接使用预先构建的字典输出与动作索引相关的标签:

    policyFound = [actionsDict[np.argmax(q[0,:])],\
                   actionsDict[np.argmax(q[1,:])], \
                   actionsDict[np.argmax(q[2,:])], \
                   actionsDict[np.argmax(q[3,:])], \
                   actionsDict[np.argmax(q[4,:])], \
                   " - ",\
                   actionsDict[np.argmax(q[6,:])], \
                   " - ",\
                   actionsDict[np.argmax(q[8,:])], \
                   actionsDict[np.argmax(q[9,:])], \
                   actionsDict[np.argmax(q[10,:])], \
                   " - ",\
                   " - ",\
                   actionsDict[np.argmax(q[13,:])], \
                   actionsDict[np.argmax(q[14,:])], \
                   " ! "]
    print("Greedy policy found:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(policyFound[idx+0], policyFound[idx+1], \
              policyFound[idx+2], policyFound[idx+3])
    print(" ")
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出如下所示:

    Greedy policy found: 
     D   R   D  L
     D   -   D  -
     R   D   D  -
     -   R   R  !  
    
    Optimal policy:  
    R/D  R   D  L
     D   -   D  -
     R  R/D  D  -
     -   R   R  !
    

正如前面的输出所示,我们实现的 TD(0) SARSA 算法仅通过与环境交互并通过回合收集经验,然后采用在SARSA – On-Policy Control部分中定义的 SARSA 状态-动作对值函数更新规则,成功地学到了该任务的最优策略。实际上,正如我们所看到的,对于环境中的每个状态,我们算法计算的 Q 表获得的贪心策略所指定的动作与为分析环境问题而定义的最优策略一致。如我们之前所见,在两个状态中有两个同样最优的动作,智能体能够正确地执行其中之一。

注意

要访问此特定部分的源代码,请参考packt.live/3fJBLBh

你还可以在packt.live/30XeOXj在线运行这个示例。

随机性测试

现在,让我们来看一下如果在 FrozenLake-v0 环境中启用随机性会发生什么。为这个任务启用随机性意味着每个选定动作的转移不再是确定性的。具体来说,对于一个给定的动作,有三分之一的概率该动作会按预期执行,而两个相邻动作的概率各占三分之一和三分之一。反方向的动作则没有任何概率。因此,例如,如果设置了下(Down)动作,智能体会有三分之一的时间向下移动,三分之一的时间向右移动,剩下的三分之一时间向左移动,而绝不会向上移动,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_09.jpg

图 7.9:如果从中心瓦片执行下(Down)动作时,各个结果状态的百分比

环境设置与我们之前看到的 FrozenLake-v0 确定性案例完全相同。同样,我们希望 SARSA 算法恢复最优策略。在这种情况下,这也可以事先进行估算。为了使推理更容易,这里有一个表示该环境的表格:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_10.jpg

图 7.10:问题设置

在上面的图示中,S是起始位置,F表示冰冻瓦片,H表示坑洞,G是目标。对于随机环境,最优策略与对应的确定性情况有很大不同,甚至可能显得违背直觉。关键点是,为了保持获得奖励的可能性,我们唯一的机会就是避免掉入坑洞。由于中间步骤没有惩罚,我们可以继续绕行,只要我们需要。唯一确定的做法如下:

  1. 移动到我们下一个发现的坑洞的反方向,即使这意味着远离目标。

  2. 以各种可能的方式避免掉入那些有可能大于 0 的坑洞的瓦片:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_11.jpg

图 7.11:环境设置(A),智能体执行的动作(B),以及在每个位置结束时接近起始状态的概率(C)

例如,考虑我们问题设置中左侧第二行的第一个瓦片,如前图表B所示。在确定性情况下,最优行动是向下移动,因为这样能使我们更接近目标。而在这个案例中,最佳选择是向左移动,即使向左意味着会碰到墙壁。这是因为向左是唯一不会让我们掉进坑里的行动。此外,有 33%的概率我们会最终到达第三行的瓦片,从而更接近目标。

注释

上述行为遵循了标准的边界实现。在该瓦片中,你执行“向左移动”这个完全合法的动作,环境会理解为“碰壁”。算法只会向环境发送一个“向左移动”的指令,环境会根据这个指令采取相应的行动。

类似的推理可以应用到其他所有瓦片,同时要记住前面提到的关键点。然而,值得讨论一个非常特殊的情况——这是我们无法实现 100%成功的唯一原因,即使使用最优策略:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_12.jpg

图 7.12:环境设置(A)、代理执行“向左移动”动作(B)、以及每个位置结束时接近起始状态的概率(C)

现在,让我们来看一下我们问题设置中左侧第二行的第三个瓦片,如前图表 B 所示。这个瓦片位于两个坑之间,因此没有任何行动是 100%安全的。在这里,最佳行动实际上是向左或向右的坑移动!这是因为,向左或向右移动,我们有 66%的机会向上或向下移动,只有 33%的机会掉进坑里。向上或向下移动意味着我们有 66%的机会向右或向左移动,掉进坑里,只有 33%的机会真正向上或向下移动。由于这个瓦片是我们无法在 100%情况下实现最佳表现的原因,最好的办法是避免到达这个瓦片。为了做到这一点,除了起始瓦片外,最优策略的第一行所有的行动都指向上方,以避免落到这个有问题的瓦片上。

除了目标左侧的瓦片,所有其他值都受到坑的邻近影响:对于这个瓦片,最优行动选择是向下移动,因为这保持了到达目标的机会,同时避免落到上面的瓦片,在那里,代理会被迫向左移动以避免掉进坑里,从而冒着掉到两个坑之间的瓦片的风险。最优策略总结如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_13.jpg

图 7.13:最优策略

前面的图表显示了先前解释的环境的最优策略,其中 D 表示向下移动,R 表示向右移动,U 表示向上移动,L 表示向左移动。

在下面的例子中,我们将使用 SARSA 算法来解决这个新版本的 FrozenLake-v0 环境。为了获得我们刚才描述的最优策略,我们需要调整我们的超参数 —— 特别是折扣因子,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_13a.png。事实上,我们希望给予代理人足够多的步骤自由度。为了做到这一点,我们必须向目标传播价值,以便所有目标中的轨迹都能从中受益,即使这些轨迹不是最短的。因此,我们将使用一个接近1的折扣因子。在代码中,这意味着我们将使用gamma = 1,而不是gamma = 0.9

现在,让我们看看我们的 SARSA 算法在这个随机环境中的工作。

练习 7.02:使用 TD(0) SARSA 解决 FrozenLake-v0 随机转移问题

在本练习中,我们将使用 TD(0) SARSA 算法来解决 FrozenLake-v0 环境,并启用随机转移。正如我们刚才看到的,由于需要考虑随机性因素,最优策略看起来与之前的练习完全不同。这给 SARSA 算法带来了新的挑战,我们将看到它如何仍然能够解决这个任务。这个练习将展示给我们这些健壮的 TD 方法如何处理不同的挑战,展示出了显著的鲁棒性。

按照以下步骤完成此练习:

  1. 导入所需的模块:

    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline
    import gym
    
  2. 实例化 gym 环境,称为 FrozenLake-v0,使用 is_slippery 标志设置为 True 以启用随机性:

    env = gym.make('FrozenLake-v0', is_slippery=True)
    
  3. 查看动作和观察空间:

    print("Action space = ", env.action_space)
    print("Observation space = ", env.observation_space)
    

    输出如下:

    Action space =  Discrete(4)
    Observation space =  Discrete(16)
    
  4. 创建两个字典,以便轻松地将 actions 索引(从 03)映射到标签(左、下、右和上):

    actionsDict = {}
    actionsDict[0] = "  L  "
    actionsDict[1] = "  D  "
    actionsDict[2] = "  R  "
    actionsDict[3] = "  U  "
    actionsDictInv = {}
    actionsDictInv["L"] = 0
    actionsDictInv["D"] = 1
    actionsDictInv["R"] = 2
    actionsDictInv["U"] = 3
    
  5. 重置环境并渲染以查看网格问题:

    env.reset()
    env.render()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_14.jpg

    图 7.14:环境的初始状态

  6. 可视化此环境的最优策略:

    optimalPolicy = ["L/R/D","  U  ","  U  ","  U  ",\
                     "  L  ","  -  "," L/R ","  -  ",\
                     "  U  ","  D  ","  L  ","  -  ",\
                     "  -  ","  R  ","  D  ","  !  ",]
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出如下:

    Optimal policy:  
      L/R/D  U    U    U
        L    -   L/R   -
        U    D    L    -
        -    R    D    !
    

    这代表了此环境的最优策略。除了两个状态外,所有其他状态都有一个与之关联的单一最优动作。事实上,正如先前描述的,这里的最优动作是将代理人远离洞穴或具有导致代理人移动到靠近洞穴的几率大于零的瓦片的动作。两个状态有多个与之关联的同等最优动作,这正是此任务的意图。

  7. 定义函数来执行 ε-greedy 动作:

    def action_epsilon_greedy(q, s, epsilon=0.05):
        if np.random.rand() > epsilon:
            return np.argmax(q[s])
        return np.random.randint(4)
    def get_action_epsilon_greedy(epsilon):
        return lambda q,s: action_epsilon_greedy\
                           (q, s, epsilon=epsilon)
    

    第一个函数实现了ε-贪婪策略:以1 - ε的概率,选择与状态-动作对关联的最高值的动作;否则,返回一个随机动作。第二个函数通过lambda函数传递时,简单地调用第一个函数。

  8. 定义一个函数,用于执行贪婪策略:

    def greedy_policy(q, s):
        return np.argmax(q[s])
    
  9. 定义一个函数,用于计算代理的平均性能:

    def average_performance(policy_fct, q):
        acc_returns = 0.
        n = 100
        for i in range(n):
            done = False
            s = env.reset()
            while not done:
                a = policy_fct(q, s)
                s, reward, done, info = env.step(a)
                acc_returns += reward
        return acc_returns/n
    
  10. 设置总回合数,表示评估代理平均性能的间隔步数的步数,以及控制ε参数减少的参数,即起始值、最小值和范围(以回合数表示):

    nb_episodes = 80000
    STEPS = 2000
    epsilon_param = [[0.2, 0.001, int(nb_episodes/2)]]
    
  11. 将 SARSA 训练算法定义为一个函数。初始化 Q 表时,所有值设置为1,但终止状态的值设置为0

    def sarsa(alpha = 0.02, \
              gamma = 1., \
              epsilon_start = 0.1,\
              epsilon_end = 0.001,\
              epsilon_annealing_stop = int(nb_episodes/2),\
              q = None, \
              progress = None, \
              env=env):
        if q is None:
            q = np.ones((16,4))
            # Set q(terminal,*) equal to 0
            q[5,:] = 0.0
            q[7,:] = 0.0
            q[11,:] = 0.0
            q[12,:] = 0.0
            q[15,:] = 0.0
    
  12. 在所有回合之间开始一个循环:

        for i in range(nb_episodes):
    
  13. 在循环内,首先根据当前的回合数定义 epsilon 值。重置环境,并确保第一个动作是通过ε-贪婪策略选择的:

            inew = min(i,epsilon_annealing_stop)
            epsilon = (epsilon_start \
                       * (epsilon_annealing_stop - inew)\
                       + epsilon_end * inew) \
                       / epsilon_annealing_stop
            done = False
            s = env.reset()
            a = action_epsilon_greedy(q, s, epsilon=epsilon)
    
  14. 然后,开始一个回合内的循环:

            while not done:
    
  15. 在循环内,使用选定的动作在环境中执行步骤,并确保获取到新状态、奖励和完成条件:

                new_s, reward, done, info = env.step(a)
    
  16. 使用ε-贪婪策略选择一个新的动作,使用 SARSA TD(0)规则更新 Q 表,并确保状态和动作更新为其新值:

                new_a = action_epsilon_greedy\
                        (q, new_s, epsilon=epsilon)
                q[s, a] = q[s, a] + alpha \
                          * (reward + gamma \
                             * q[new_s, new_a] - q[s, a])
                s = new_s
                a = new_a
    
  17. 最后,估算代理的平均性能:

            if progress is not None and i%STEPS == 0:
                progress[i//STEPS] = average_performance\
                                     (get_action_epsilon_greedy\
                                     (epsilon), q=q)
        return q, progress
    

    提供关于ε参数减小的简要描述可能会很有用。它受三个参数的控制:起始值、最小值和减小范围。它们的使用方式如下:ε从起始值开始,然后在由参数“范围”定义的集数内线性减小,直到达到最小值,并保持该值不变。

  18. 定义一个数组,用来在训练和执行 SARSA TD(0)训练期间收集所有代理的性能评估:

    sarsa_performance = np.ndarray(nb_episodes//STEPS)
    q, sarsa_performance = sarsa(alpha = 0.02, gamma = 1,\
                                 progress=sarsa_performance, \
                                 epsilon_start=epsilon_param[0][0],\
                                 epsilon_end=epsilon_param[0][1], \
                                 epsilon_annealing_stop = \
                                 epsilon_param[0][2])
    
  19. 绘制 SARSA 代理在训练期间的平均奖励历史:

    plt.plot(STEPS*np.arange(nb_episodes//STEPS), sarsa_performance)
    plt.xlabel("Epochs")
    plt.title("Learning progress for SARSA")
    plt.ylabel("Average reward of an epoch")
    

    这将生成以下输出,展示了 SARSA 算法的学习进度:

    Text(0, 0.5, 'Average reward of an epoch')
    

    图将如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_15.jpg

    图 7.15:训练回合期间一个周期的平均奖励趋势

    这个图清楚地展示了即使在考虑随机动态的情况下,SARSA 算法的性能是如何随回合数提升的。在约 60k 回合时性能的突然下降是完全正常的,尤其是在随机探索起主要作用,且随机过渡动态是环境的一部分时,正如在这个案例中所展示的那样。

  20. 评估贪婪策略在训练代理(Q 表)下的表现:

    greedyPolicyAvgPerf = average_performance(greedy_policy, q=q)
    print("Greedy policy SARSA performance =", greedyPolicyAvgPerf)
    

    输出将如下所示:

    Greedy policy SARSA performance = 0.75
    
  21. 显示 Q 表的值:

    q = np.round(q,3)
    print("(A,S) Value function =", q.shape)
    print("First row")
    print(q[0:4,:])
    print("Second row")
    print(q[4:8,:])
    print("Third row")
    print(q[8:12,:])
    print("Fourth row")
    print(q[12:16,:])
    

    将生成以下输出:

    (A,S) Value function = (16, 4)
    First row
    [[0.829 0.781 0.785 0.785]
     [0.416 0.394 0.347 0.816]
     [0.522 0.521 0.511 0.813]
     [0.376 0.327 0.378 0.811]]
    Second row
    [[0.83  0.552 0.568 0.549]
     [0\.    0\.    0\.    0\.   ]
     [0.32  0.195 0.535 0.142]
     [0\.    0\.    0\.    0\.   ]]
    Third row
    [[0.55  0.59  0.546 0.831]
     [0.557 0.83  0.441 0.506]
     [0.776 0.56  0.397 0.342]
     [0\.    0\.    0\.    0\.   ]]
    Fourth row
    [[0\.    0\.    0\.    0\.   ]
     [0.528 0.619 0.886 0.506]
     [0.814 0.943 0.877 0.844]
     [0\.    0\.    0\.    0\.   ]]
    

    该输出显示了我们问题的完整状态-动作值函数的值。这些值随后通过贪婪选择规则来生成最优策略。

  22. 打印出找到的贪婪策略,并与最优策略进行比较。计算出状态-动作值函数后,我们能够从中提取出贪婪策略。事实上,正如之前所解释的,贪婪策略会选择在给定状态下与 Q 表中最大值关联的动作。为此,我们使用了 argmax 函数。当该函数应用于 16 个状态(从 0 到 15)时,它返回的是在四个可用动作(从 0 到 3)中,哪个动作与该状态的最大值关联的索引。在这里,我们还通过预先构建的字典直接输出与动作索引关联的标签:

    policyFound = [actionsDict[np.argmax(q[0,:])],\
                   actionsDict[np.argmax(q[1,:])],\
                   actionsDict[np.argmax(q[2,:])],\
                   actionsDict[np.argmax(q[3,:])],\
                   actionsDict[np.argmax(q[4,:])],\
                   "  -  ",\
                   actionsDict[np.argmax(q[6,:])],\
                   "  -  ",\
                   actionsDict[np.argmax(q[8,:])],\
                   actionsDict[np.argmax(q[9,:])],\
                   actionsDict[np.argmax(q[10,:])],\
                   "  -  ",\
                   "  -  ",\
                   actionsDict[np.argmax(q[13,:])],\
                   actionsDict[np.argmax(q[14,:])],\
                   "  !  "]
    print("Greedy policy found:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(policyFound[idx+0], policyFound[idx+1], \
              policyFound[idx+2], policyFound[idx+3])
    print(" ")
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出将如下所示:

    Greedy policy found:
        L    U    U    U
        L    -    R    -
        U    D    L    -
        -    R    D    !
    Optimal policy:  
      L/R/D  U    U    U
        L    -   L/R   -
        U    D    L    -
        -    R    D    !
    

如你所见,和之前的练习一样,我们的算法通过简单地探索环境,成功找到了最优策略,即使在环境转移是随机的情况下。正如预期的那样,在这种设置下,不可能 100% 的时间都达到最大奖励。事实上,正如我们所看到的,对于环境中的每个状态,通过我们的算法计算出的 Q 表所获得的贪婪策略都建议一个与通过分析环境问题定义的最优策略一致的动作。如我们之前所见,有两个状态中有许多不同的动作是同样最优的,代理正确地执行了其中之一。

注意

要访问该特定部分的源代码,请参考 packt.live/3eicsGr

你也可以在 packt.live/2Z4L1JV 在线运行此示例。

现在我们已经熟悉了在策略控制,是时候转向离策略控制了,这是强化学习中的一次早期突破,追溯到 1989 年的 Q-learning。

注意

Q-learning 算法最早由 Watkins 在 Mach Learn 8, 279–292 (1992) 提出。在这里,我们仅提供一个直观理解,以及简要的数学描述。有关更详细的数学讨论,请参阅原始论文 link.springer.com/article/10.1007/BF00992698

Q-learning – 离策略控制

Q-learning 是识别一类离策略控制时序差分算法的名称。从数学/实现的角度来看,与在策略算法相比,唯一的区别在于用于更新 Q 表(或近似方法的函数)的规则,具体定义如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16.jpg

图 7.16:近似方法的函数

关键点在于如何为下一个状态选择动作,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16a.png。实际上,选择具有最大状态-动作值的动作直接近似了找到最优 Q 值并遵循最优策略时发生的情况。此外,它与用于收集经验的策略无关,而是在与环境互动时得到的。探索策略可以与最优策略完全不同;例如,它可以是ε-greedy 策略以鼓励探索,并且在一些容易满足的假设下,已经证明 Q 会收敛到最优值。

在*第九章,什么是深度 Q 学习?*中,你将研究这种方法在非表格化方法中的扩展,我们使用深度神经网络作为函数近似器。这种方法叫做深度 Q 学习。Q-learning 控制算法的方案可以如下所示:

  1. 选择算法参数:步长,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16b.png,它必须位于区间(0, 1]内,以及ε-greedy 策略的ε参数,它必须较小且大于0,因为它表示选择非最优动作的概率,以促进探索:

    alpha = 0.02
    epsilon_expl = 0.2
    
  2. 对所有https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16d.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16e.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16c.png进行初始化,任意设置,除了 Q(terminal, *) = 0:

    q = np.ones((16, 4))
    # Set q(terminal,*) equal to 0
    q[5,:] = 0.0
    q[7,:] = 0.0
    q[11,:] = 0.0
    q[12,:] = 0.0
    q[15,:] = 0.0
    
  3. 在所有回合中创建一个循环。在该循环中,初始化s

    for i in range(nb_episodes):
        done = False
        s = env.reset()
    
  4. 为每个回合创建一个循环。在该循环中,使用从 Q 派生的策略(例如,ε-greedy)从https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16g.png中选择https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16f.png

        while not done:
            # behavior policy
            a = action_epsilon_greedy(q, s, epsilon=epsilon_expl)
    
  5. 执行动作,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16h.png,观察https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16i.png。使用 Q-learning 规则更新所选状态-动作对的状态-动作值函数,该规则将新值定义为当前值加上与步长https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_16j.png相乘的与离策略相关的 TD 误差。可以表示如下:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_17.jpg

图 7.17:更新的状态-动作值函数的表达式

前面的解释可以通过代码实现如下:

        new_s, reward, done, info = env.step(a)
        a_max = np.argmax(q[new_s]) # estimation policy 
        q[s, a] = q[s, a] + alpha \
                  * (reward + gamma \
                     * q[new_s, a_max] -q[s, a])
        s = new_s

如我们所见,我们只是将新状态下采取动作的随机选择替换为与最大 q 值相关的动作。这种(看似)微小的变化,可以通过适配 SARSA 算法轻松实现,但对方法的性质有着重要影响。我们将在接下来的练习中看到它的效果。

练习 7.03:使用 TD(0) Q-Learning 解决 FrozenLake-v0 的确定性转移问题

在本练习中,我们将实现 TD(0) Q 学习算法来解决 FrozenLake-v0 环境,其中只允许确定性转移。在本练习中,我们将考虑与练习 7.01,使用 TD(0) SARSA 解决 FrozenLake-v0 确定性转移中相同的任务,即取回飞盘的最优策略,但这次我们不使用 SARSA 算法(基于策略),而是实现 Q 学习(非基于策略)。我们将观察该算法的行为,并训练自己通过恢复智能体的最优策略来实现一种新的估算 q 值表的方法。

按照以下步骤完成此练习:

  1. 导入所需的模块,如下所示:

    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline
    import gym
    
  2. 实例化名为FrozenLake-v0gym环境,设置is_slippery标志为False,以禁用随机性:

    env = gym.make('FrozenLake-v0', is_slippery=False)
    
  3. 查看动作空间和观察空间:

    print("Action space = ", env.action_space)
    print("Observation space = ", env.observation_space)
    

    输出结果如下:

    Action space =  Discrete(4)
    Observation space =  Discrete(16)
    
  4. 创建两个字典,方便将actions数字转换为动作:

    actionsDict = {}
    actionsDict[0] = " L "
    actionsDict[1] = " D "
    actionsDict[2] = " R "
    actionsDict[3] = " U "
    actionsDictInv = {}
    actionsDictInv["L"] = 0
    actionsDictInv["D"] = 1
    actionsDictInv["R"] = 2
    actionsDictInv["U"] = 3
    
  5. 重置环境并渲染以查看网格问题:

    env.reset()
    env.render()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_18.jpg

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_18.jpg)

    图 7.18:环境的初始状态

  6. 可视化该环境的最优策略:

    optimalPolicy = ["R/D"," R "," D "," L ",\
                     " D "," - "," D "," - ",\
                     " R ","R/D"," D "," - ",\
                     " - "," R "," R "," ! ",]
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出结果如下:

    Optimal policy:  
    R/D  R   D  L
     D   -   D  -
     R  R/D  D  -
     -   R   R  !
    

    这表示该环境的最优策略,并显示在 4x4 网格中表示的每个环境状态,对于四个可用动作中最优的动作:上移、下移、右移和左移。除了两个状态外,所有其他状态都有与之关联的唯一最优动作。实际上,正如前面所述,最优动作是那些将智能体带到目标的最短路径。两个不同的可能性导致两个状态具有相同的路径长度,因此它们都同样最优。

  7. 接下来,定义将执行ε-贪婪动作的函数:

    def action_epsilon_greedy(q, s, epsilon=0.05):
        if np.random.rand() > epsilon:
            return np.argmax(q[s])
        return np.random.randint(4)
    
  8. 定义一个函数来执行贪婪动作:

    def greedy_policy(q, s):
        return np.argmax(q[s])
    
  9. 定义一个函数来计算智能体表现的平均值:

    def average_performance(policy_fct, q):
        acc_returns = 0.
        n = 500
        for i in range(n):
            done = False
            s = env.reset()
            while not done:
                a = policy_fct(q, s)
                s, reward, done, info = env.step(a)
                acc_returns += reward
        return acc_returns/n
    
  10. 初始化 Q 表,使得所有值都等于1,除了终止状态的值:

    q = np.ones((16, 4))
    # Set q(terminal,*) equal to 0
    q[5,:] = 0.0
    q[7,:] = 0.0
    q[11,:] = 0.0
    q[12,:] = 0.0
    q[15,:] = 0.0
    
  11. 设置总集数、表示我们评估智能体平均表现的间隔步数、学习率、折扣因子、ε值(用于探索策略),并定义一个数组以收集训练过程中所有智能体的表现评估:

    nb_episodes = 40000
    STEPS = 2000
    alpha = 0.02
    gamma = 0.9
    epsilon_expl = 0.2
    q_performance = np.ndarray(nb_episodes//STEPS)
    
  12. 使用 Q-learning 算法训练代理:外部循环负责生成所需的回合数。然后,回合内循环完成以下步骤:首先,使用 ε-贪心策略选择一个探索动作,然后环境通过选择的探索动作进行一步,获取 new_srewarddone 条件。为新状态选择新动作,使用贪心策略更新 Q-table,使用 Q-learning TD(0) 规则更新状态的新值。每隔预定步骤数,就会评估代理的平均表现:

    for i in range(nb_episodes):
        done = False
        s = env.reset()
        while not done:
            # behavior policy
            a = action_epsilon_greedy(q, s, epsilon=epsilon_expl)
            new_s, reward, done, info = env.step(a)
            a_max = np.argmax(q[new_s]) # estimation policy 
            q[s, a] = q[s, a] + alpha \
                      * (reward + gamma \
                         * q[new_s, a_max] - q[s, a])
            s = new_s
        # for plotting the performance
        if i%STEPS == 0:
            q_performance[i//STEPS] = average_performance\
                                      (greedy_policy, q)
    
  13. 绘制 Q-learning 代理在训练过程中的平均奖励历史:

    plt.plot(STEPS * np.arange(nb_episodes//STEPS), q_performance)
    plt.xlabel("Epochs")
    plt.ylabel("Average reward of an epoch")
    plt.title("Learning progress for Q-Learning")
    

    这将生成以下输出,展示 Q-learning 算法的学习进度:

    Text(0.5, 1.0, 'Learning progress for Q-Learning')
    

    输出将如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_19.jpg

    图 7.19:训练轮次中每个时期的平均奖励趋势

    正如我们所看到的,图表展示了随着代理收集越来越多的经验,Q-learning 性能如何在多个周期中快速增长。它还表明,算法在学习后能够达到 100% 的成功率。还可以明显看出,与 SARSA 方法相比,在这种情况下,测量的算法性能稳步提高,且提高速度要快得多。

  14. 评估训练好的代理(Q-table)的贪心策略表现:

    greedyPolicyAvgPerf = average_performance(greedy_policy, q=q)
    print("Greedy policy Q-learning performance =", \
          greedyPolicyAvgPerf)
    

    输出将如下所示:

    Greedy policy Q-learning performance = 1.0
    
  15. 显示 Q-table 的值:

    q = np.round(q,3)
    print("(A,S) Value function =", q.shape)
    print("First row")
    print(q[0:4,:])
    print("Second row")
    print(q[4:8,:])
    print("Third row")
    print(q[8:12,:])
    print("Fourth row")
    print(q[12:16,:])
    

    以下输出将被生成:

    (A,S) Value function = (16, 4)
    First row
    [[0.531 0.59  0.59  0.531]
     [0.617 0.372 0.656 0.628]
     [0.672 0.729 0.694 0.697]
     [0.703 0.695 0.703 0.703]]
    Second row
    [[0.59  0.656 0\.    0.531]
     [0\.    0\.    0\.    0\.   ]
     [0.455 0.81  0.474 0.754]
     [0\.    0\.    0\.    0\.   ]]
    Third row
    [[0.656 0\.    0.729 0.59 ]
     [0.656 0.81  0.81  0\.   ]
     [0.778 0.9   0.286 0.777]
     [0\.    0\.    0\.    0\.   ]]
    Fourth row
    [[0\.    0\.    0\.    0\.   ]
     [0\.    0.81  0.9   0.729]
     [0.81  0.9   1\.    0.81 ]
     [0\.    0\.    0\.    0\.   ]]
    

    此输出显示了我们问题的完整状态-动作价值函数的值。这些值随后用于通过贪心选择规则生成最优策略。

  16. 打印出找到的贪心策略,并与最优策略进行比较:

    policyFound = [actionsDict[np.argmax(q[0,:])],\
                   actionsDict[np.argmax(q[1,:])],\
                   actionsDict[np.argmax(q[2,:])],\
                   actionsDict[np.argmax(q[3,:])],\
                   actionsDict[np.argmax(q[4,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[6,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[8,:])],\
                   actionsDict[np.argmax(q[9,:])],\
                   actionsDict[np.argmax(q[10,:])],\
                   " - ",\
                   " - ",\
                   actionsDict[np.argmax(q[13,:])],\
                   actionsDict[np.argmax(q[14,:])],\
                   " ! "]
    print("Greedy policy found:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(policyFound[idx+0], policyFound[idx+1], \
              policyFound[idx+2], policyFound[idx+3])
    print(" ")
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出将如下所示:

    Greedy policy found: 
     D   R   D  L
     D   -   D  -
     R   D   D  -
     -   R   R  !  
    Optimal policy:  
    R/D  R   D  L
     D   -   D  -
     R  R/D  D  -
     -   R   R  !
    

正如这些输出所示,Q-learning 算法能够像 SARSA 一样,通过经验和与环境的互动,成功地提取最优策略,正如在 练习 07.01,使用 TD(0) SARSA 解决 FrozenLake-v0 确定性转移 中所做的那样。

正如我们所看到的,对于网格世界中的每个状态,使用我们算法计算的 Q 表得到的贪心策略都能推荐一个与通过分析环境问题定义的最优策略一致的动作。正如我们已经看到的,有两个状态在其中存在多个不同的动作,它们同样是最优的,且代理正确地实现了其中的一个。

注意

要访问此特定部分的源代码,请参考 packt.live/2AUlzym

您也可以在线运行此示例,网址为 packt.live/3fJCnH5

对于 SARSA 来说,如果我们启用随机转移,看看 Q-learning 的表现将会是很有趣的。这将是本章末尾活动的目标。两个算法遵循的程序与我们在 SARSA 中采用的完全相同:用于确定性转移情况的 Q-learning 算法被应用,您需要调整超参数(特别是折扣因子和训练轮次),直到在随机转移动态下获得对最优策略的收敛。

为了完善 TD(0) 算法的全貌,我们将引入另一种特定的方法,该方法是通过对前面的方法进行非常简单的修改得到的:期望 SARSA。

期望 SARSA

现在,让我们考虑一个与 Q-learning 非常相似的学习算法,唯一的区别是将下一个状态-动作对的最大值替换为期望值。这是通过考虑当前策略下每个动作的概率来计算的。这个修改后的算法可以通过以下更新规则来表示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20.jpg

图 7.20:状态-动作值函数更新规则

与 SARSA 相比,额外的计算复杂性提供了一个优势,即消除了由于随机选择 At+1 所带来的方差,这对于显著提高学习效果和鲁棒性是一个非常强大的技巧。它可以同时用于在策略和非策略的方式,因此成为了 SARSA 和 Q-learning 的一种抽象,通常其性能优于两者。以下片段提供了该更新规则的实现示例:

q[s, a] = q[s, a] + alpha * (reward + gamma * 
    (np.dot(pi[new_s, :],q[new_s, :]) - q[s, a])

在前面的代码中,pi 变量包含了每个状态下每个动作的所有概率。涉及 piq 的点积是计算新状态期望值所需的操作,考虑到该状态下所有动作及其各自的概率。

现在我们已经学习了 TD(0) 方法,让我们开始学习 N 步 TD 和 TD(λ) 算法。

N 步 TD 和 TD(λ) 算法

在上一章中,我们研究了蒙特卡罗方法,而在本章前面的部分,我们学习了 TD(0) 方法,正如我们很快会发现的,它们也被称为一步时序差分方法。在本节中,我们将它们统一起来:事实上,它们处在一系列算法的极端(TD(0) 在一端,MC 方法在另一端),而通常,性能最优的方法是处于这个范围的中间。

N 步时序差分算法是对一阶时序差分方法的扩展。更具体地说,它们将蒙特卡罗和时序差分方法进行了概括,使得两者之间的平滑过渡成为可能。正如我们已经看到的,蒙特卡罗方法必须等到整个回合结束后,才能将奖励反向传播到之前的状态。而一阶时序差分方法则直接利用第一个可用的未来步骤进行自举,并开始更新状态或状态-动作对的价值函数。这两种极端情况很少是最佳选择。最佳选择通常位于这一广泛范围的中间。使用 N 步方法可以让我们调整在更新价值函数时考虑的步数,从而将自举方法分散到多个步骤上。

在资格迹的背景下也可以回忆起类似的概念,但它们更为一般,允许我们在多个时间间隔内同时分配和扩展自举。这两个话题将单独处理,以便清晰起见,并且为了帮助你逐步建立知识,我们将首先从 N 步方法开始,然后再讲解资格迹。

N 步时序差分

正如我们已经看到的一阶时序差分方法一样,接近 N 步方法的第一步是专注于使用策略生成的样本回合来估计状态值函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20a.png。我们已经提到,蒙特卡罗算法必须等到回合结束后,才能通过使用给定状态的整个奖励序列来进行更新。而一阶方法只需要下一个奖励。N 步方法采用了一种中间规则:它们不仅依赖于下一个奖励,或者依赖于回合结束前的所有未来奖励,而是采用这两者之间的一个值。例如,三步更新将使用前三个奖励和三步后达到的估计状态值。这可以对任意步数进行形式化。

这种方法催生了一系列方法,它们仍然是时序差分方法,因为它们使用目标状态之后遇到的 N 步来更新其值。显然,我们在本章开始时遇到的方法是 N 步方法的特例。因此,它们被称为“一阶时序差分方法”。

为了更正式地定义它们,我们可以考虑状态的估计值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20b.png,作为状态-奖励序列的结果,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20c.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20d.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20e.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20f.png,…,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20g.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_20h.png(不包括动作)。在 MC 方法中,这个估计值只有在一集结束时才会更新,而在一步法中,它会在下一步之后立即更新。另一方面,在 N 步法中,状态值估计是在 N 步之后更新的,使用一种折扣n未来奖励以及未来 N 步后遇到的状态值的量。这个量被称为 N 步回报,可以通过以下表达式定义:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_21.jpg

图 7.21:N 步回报方程(带状态值函数)

这里需要注意的一个关键点是,为了计算这个 N 步回报,我们必须等到达到时间t+1,以便方程中的所有项都可以使用。通过使用 N 步回报,可以直接将状态值函数更新规则形式化,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_22.jpg

图 7.22:使用 N 步回报的自然状态值学习算法的表达式

请注意,所有其他状态的值保持不变,如以下表达式所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_23.jpg

图 7.23:指定所有其他值保持恒定的表达式

这是将 N 步 TD 算法形式化的方程。值得再次注意的是,在我们可以估计 N 步回报之前,在前n-1步期间不会进行任何更改。需要在一集结束时进行补偿,当剩余的n-1更新在到达终止状态后一次性执行。

与我们之前看到的 TD(0)方法类似,并且不深入讨论数据,N 步 TD 方法的状态值函数估计在适当的技术条件下会收敛到最优值。

N 步 SARSA

扩展我们在介绍一步法时看到的 SARSA 算法到其 N 步版本是非常简单的。就像我们之前做的那样,唯一需要做的就是将值函数的 N 步回报中的状态-动作对替换为状态,并在刚才看到的更新公式中结合ε-贪婪策略。N 步回报(更新目标)的定义可以通过以下方程描述:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_24.jpg

图 7.24:N 步回报方程(带状态-动作值函数)

这里,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_24a.png,如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_24b.png。状态-动作值函数的更新规则表达如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25.jpg

图 7.25:状态-动作值函数的更新规则

请注意,其他所有状态-动作对的值保持不变:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25a.png,对所有s的值都适用,因此https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25b.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25c.png。N 步 SARSA 控制算法的方案可以如下表示:

  1. 选择算法的参数:步长https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25d.png,它必须位于区间(0, 1]内,和ε-贪心策略的ε参数,它必须小且大于0,因为它表示选择非最优动作以偏向探索的概率。必须选择步数n的值。例如,可以使用以下代码来完成此选择:

    alpha = 0.02
    n = 4
    epsilon = 0.05
    
  2. 初始化https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25e.png,对于所有https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25f.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25g.png,任意选择,除了 Q(终止, ·) = 0:

    q = np.ones((16,4))
    
  3. 为每个回合创建一个循环。初始化并存储 S0 ≠ 终止状态。使用ε-贪心策略选择并存储动作,并将时间T初始化为一个非常大的值:

    for i in range(nb_episodes):
            s = env.reset()
            a = action_epsilon_greedy(q, s, epsilon=epsilon)
            T = 1e6
    
  4. 为 t = 0, 1, 2,… 创建一个循环。如果 t < T,则执行动作https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25h.png。观察并存储下一奖励为https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25i.png,下一状态为https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25j.png。如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25k.png是终止状态,则将T设置为t+1

    while True:
                new_s, reward, done, info = env.step(a)
                if done:
                    T = t+1
    
  5. 如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25l.png不是终止状态,则选择并存储新状态下的动作:

            new_a = action_epsilon_greedy(q, new_s, epsilon=epsilon)
    
  6. 定义用于更新估计的时间tau,等于t-n+1

    tau = t-n+1
    
  7. 如果tau大于 0,则通过对前 n 步的折扣回报求和,并加上下一步-下一动作对的折扣值来计算 N 步回报,并更新状态-动作值函数:

    G = sum_n(q, tau, T, t, gamma, R, new_s, new_a)
    q[s, a] = q[s, a] + alpha * (G- q[s, a]) 
    

通过一些小的改动,这个规则可以轻松扩展以适应预期的 SARSA。正如本章之前所见,它只需要我们用目标策略下第 N 步最后一个时间点的估计动作值来替代状态的预期近似值。当相关状态是终止状态时,它的预期近似值定义为 0。

N 步非策略学习

为了定义 N 步方法的离策略学习,我们将采取与一阶方法相似的步骤。关键点在于,像所有离策略方法一样,我们是在为策略 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25m.png 学习价值函数,同时遵循一个不同的探索策略,假设为 b。通常,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25n.png 是当前状态-动作值函数估计的贪婪策略,而 b 具有更多的随机性,以便有效探索环境;例如,ε-贪婪策略。与我们之前看到的一阶离策略方法的主要区别在于,现在我们需要考虑到,我们正在使用不同于我们想要学习的策略来选择动作,并且我们是进行多步选择。因此,我们需要通过测量在两种策略下选择这些动作的相对概率来适当加权所选动作。

通过这个修正,我们可以定义一个简单的离策略 N 步 TD 版本的规则:在时间 t (实际上是在时间 t + n)进行的更新可以通过 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_25o.png 进行加权:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_26.jpg

图 7.26:时间 ‘t’ 时刻 N 步 TD 离策略更新规则

这里,V 是价值函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_26a.png 是步长,G 是 N 步回报,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_26b.png 称为重要性采样比率。重要性采样比率是指在两种策略下,从 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_26c.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_26d.png 执行 n 个动作的相对概率,其表达式如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27.jpg

图 7.27:采样比率方程

这里,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27a.png 是智能体策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27b.png 是探索策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27c.png 是动作,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27d.png 是状态。

根据这个定义,很明显,在我们想要学习的策略下永远不会选择的动作(即它们的概率为 0)会被忽略(权重为 0)。另一方面,如果我们正在学习的策略下某个动作相对于探索策略具有更高的概率,那么分配给它的权重应高于 1,因为它会更频繁地被遇到。显然,对于在策略情况下,采样比率始终等于 1,因为 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27e.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_27f.png 是相同的策略。因此,N 步 SARSA 在策略更新可以视为离策略更新的一个特例。该更新的通用形式,可以从中推导出在策略和离策略方法,表达式如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28.jpg

图 7.28:离策略 N 步 TD 算法的状态-动作值函数

如你所见,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28a.png是状态-动作值函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28b.png是步长,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28c.png是 N 步回报,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28d.png是重要性抽样比率。完整算法的方案如下:

  1. 选择一个任意行为策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28e.png,使得每个状态的每个动作的概率对于所有状态和动作都大于 0。选择算法参数:步长,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28f.png,其必须在区间(0, 1]内,并为步数选择一个值n。这可以通过以下代码实现:

    alpha = 0.02
    n = 4
    
  2. 初始化https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28g.png,对于所有https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28h.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28i.png可以任意选择,除非 Q(终止,·) = 0:

    q = np.ones((16,4))
    
  3. 初始化策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28j.png,使其对 Q 采取贪婪策略,或设定为一个固定的给定策略。为每个回合创建一个循环。初始化并存储 S0 ≠终止状态。使用 b 策略选择并存储一个动作,并将时间T初始化为一个非常大的值:

    for i in range(nb_episodes):
            s = env.reset()
            a = action_b_policy(q, s)
            T = 1e6
    
  4. 为 t = 0, 1, 2, … 创建一个循环。如果 t < T,则执行动作https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28k.png。观察并存储下一次奖励为https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28l.png,并将下一个状态存储为https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28m.png。如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28n.png是终止状态,则将T设置为t+1

    while True:
                new_s, reward, done, info = env.step(a)
                if done:
                    T = t+1
    
  5. 如果https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28o.png不是终止状态,则选择并存储新状态的动作:

            new_a = action_b_policy(q, new_s)
    
  6. 定义估计更新的时间tau,使其等于t-n+1

    tau = t-n+1
    
  7. 如果tau大于或等于0,则计算抽样比率。通过对前 n 步的折扣回报求和,并加上下一步-下一动作对的折扣值来计算 N 步回报,并更新状态-动作值函数:

    rho = product_n(q, tau, T, t, R, new_s, new_a)
    G = sum_n(q, tau, T, t, gamma, R, new_s, new_a)
    q[s, a] = q[s, a] + alpha * rho * (G- q[s, a]) 
    

现在我们已经研究了 N 步方法,是时候继续学习时序差分方法的最一般且最高效的变体——TD(λ)了。

TD(λ)

流行的 TD(λ)算法是一种时序差分算法,利用了资格迹概念。正如我们很快将看到的,这是一种通过任意步数适当加权状态(或状态-动作对)的价值函数贡献的过程。名称中引入的lambda项是一个参数,用于定义和参数化这一系列算法。正如我们很快会看到的,它是一个加权因子,可以让我们适当地加权涉及算法回报估计的不同贡献项。

任何时间差方法,例如我们已经看到的(Q-learning 和 SARSA),都可以与资格迹概念结合,我们将在接下来实现。这使我们能够获得一种更通用的方法,同时也更高效。正如我们之前预期的,这种方法实现了 TD 和蒙特卡罗方法的最终统一与推广。同样,关于我们在 N 步 TD 方法中看到的内容,在这里我们也有一个极端(λ = 0)的单步 TD 方法和另一个极端(λ = 1)的蒙特卡罗方法。这两个边界之间的空间包含了中间方法(就像 N 步方法中有限的 n > 1 一样)。此外,资格迹还允许我们使用扩展的蒙特卡罗方法进行所谓的在线实现,这意味着它们可以应用于非回合性问题。

相对于我们之前看到的 N 步 TD 方法,资格迹具有额外的优势,使我们能够显著提升这些方法的计算效率。如我们之前所提到的,选择 N 步方法中 n 的正确值往往不是一项简单的任务。而资格迹则允许我们将不同时间步对应的更新“融合”在一起。

为了实现这一目标,我们需要定义一种方法来加权 N 步返回值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28p.png,使用一个随着时间呈指数衰减的权重。通过引入一个因子 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28q.png,并用 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28r.png 对第 n 次返回进行加权。

目标是定义一个加权平均值,使得所有这些权重的总和为 1。标准化常数是收敛几何级数的极限值:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28s.png。有了这个,我们可以定义所谓的 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_28t.png-返回,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_29.jpg

图 7.29:lambda 返回的表达式

该方程定义了我们选择的 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_29a.png 如何影响给定返回随着步数的增加呈指数下降的速度。

我们现在可以使用这个新的返回值作为状态(或状态-动作对)值函数的目标,从而创建一个新的值函数更新规则。此时看起来,为了考虑所有的贡献,我们应该等到本回合结束,收集所有未来的返回值。这个问题通过资格迹的第二个基本新颖性得到了解决:与其向前看,我们反转了视角,智能体根据资格迹规则,使用当前返回值和价值信息来更新过去访问过的所有状态(状态-动作对)。

资格迹初始化为每个状态(或状态-动作对)都为 0,在每一步时间更新时,访问的状态(或状态-动作对)的值加 1,从而使其在更新值函数时权重最大,并通过https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_29b.png因子逐渐衰退。这个因子是资格迹随着时间衰退的组合,如之前所解释的 (https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_29c.png),以及我们在本章中多次遇到的熟悉的奖励折扣因子https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_29d.png。有了这个新概念,我们现在可以构建新的值函数更新规则。首先,我们有一个方程式来调节资格迹的演化:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_30.jpg

图 7.30:时间‘t’时状态的资格迹初始化和更新规则

然后,我们有了新的 TD 误差(或δ)的定义。状态值函数的更新规则如下:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_31.jpg

图 7.31:使用资格迹的状态值函数更新规则

现在,让我们看看如何在 SARSA 算法中实现这个思想,以获得一个具有资格迹的策略控制算法。

SARSA(λ)

直接将状态值更新转换为状态-动作值更新,允许我们将资格迹特性添加到我们之前看到的 SARSA 算法中。资格迹方程可以如下修改:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_32.jpg

图 7.32:时间‘t’时状态-动作对的资格迹初始化和更新规则

TD 误差和状态-动作值函数更新规则如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33.jpg

图 7.33:使用资格迹的状态-动作对值函数更新规则

一个完美总结所有这些步骤并展示完整算法的示意图如下:

  1. 选择算法的参数:步长https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33a.png,该值必须位于区间(0, 1]内,和ε-贪心策略的ε参数,该参数必须小且大于 0,因为它代表选择非最优动作的概率,旨在促进探索。必须选择一个lambda参数的值。例如,可以通过以下代码来实现:

    alpha = 0.02
    lambda = 0.3
    epsilon = 0.05
    
  2. 初始化https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33b.png,对于所有https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33c.pnghttps://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33d.png,任选初始化,除了 Q(terminal, ·) = 0:

    q = np.ones((16,4))
    
  3. 为每个回合创建一个循环。将资格迹表初始化为0

    E = np.zeros((16, 4))
    
  4. 初始化状态为非终止状态,并使用ε-贪心策略选择一个动作。然后,开始回合内循环:

        state = env.reset()
        action = action_epsilon_greedy(q, state, epsilon)
        while True:
    
  5. 为每个回合的每一步创建一个循环,更新 eligibility traces,并将值为 1 分配给最后访问的状态:

            E = eligibility_decay * gamma * E 
            E[state, action] += 1
    
  6. 通过环境并使用 ε-贪婪策略选择下一个动作:

            new_state, reward, done, info = env.step(action)
            new_action = action_epsilon_greedy\
                         (q, new_state, epsilon)
    
  7. 计算https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33e.png 更新,并使用 SARSA TD(https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33f.png) 规则更新 Q 表:

            delta = reward + gamma \
                    * q[new_state, new_action] - q[state, action]
            q = q + alpha * delta * E 
    
  8. 使用新状态和新动作值更新状态和动作:

            state, action = new_state, new_action
            if done:
                break
    

我们现在准备好在已经通过单步 SARSA 和 Q-learning 解决的环境中测试这个新算法。

练习 7.04:使用 TD(λ) SARSA 解决 FrozenLake-v0 确定性转移问题

在这个练习中,我们将实现 SARSA(λ) 算法来解决 FrozenLake-v0 环境下的确定性环境动态。在这个练习中,我们将考虑与 练习 7.01,使用 TD(0) SARSA 解决 FrozenLake-v0 确定性转移问题练习 7.03,使用 TD(0) Q-learning 解决 FrozenLake-v0 确定性转移问题 中相同的任务,但这一次,我们将不再使用像 SARSA(基于策略)和 Q-learning(非基于策略)这样的单步 TD 方法,而是实现 TD(λ),这是一种与 eligibility traces 功能结合的时序差分方法。我们将观察这个算法的行为,并训练自己实现一种新的方法,通过它来估算 Q 值表,从而恢复智能体的最优策略。

按照这些步骤完成此练习:

  1. 导入所需模块:

    import numpy as np
    from numpy.random import random, choice
    import matplotlib.pyplot as plt
    %matplotlib inline
    import gym
    
  2. 使用 is_slippery 标志设置为 False 来实例化名为 FrozenLake-v0gym 环境,以禁用随机性:

    env = gym.make('FrozenLake-v0', is_slippery=False)
    
  3. 看一下动作和观察空间:

    print("Action space = ", env.action_space)
    print("Observation space = ", env.observation_space)
    

    输出将如下所示:

    Action space =  Discrete(4)
    Observation space =  Discrete(16)
    
  4. 创建两个字典,以便轻松将 actions 数字转换为动作:

    actionsDict = {}
    actionsDict[0] = " L "
    actionsDict[1] = " D "
    actionsDict[2] = " R "
    actionsDict[3] = " U "
    actionsDictInv = {}
    actionsDictInv["L"] = 0
    actionsDictInv["D"] = 1
    actionsDictInv["R"] = 2
    actionsDictInv["U"] = 3
    
  5. 重置环境并渲染它,查看网格:

    env.reset()
    env.render()
    

    输出将如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_34.jpg

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_34.jpg)

    图 7.34:环境的初始状态

  6. 可视化该环境的最优策略:

    optimalPolicy = ["R/D"," R "," D "," L ",\
                     " D "," - "," D "," - ",\
                     " R ","R/D"," D "," - ",\
                     " - "," R "," R "," ! ",]
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    输出将如下所示:

    Optimal policy:  
    R/D  R   D  L
     D   -   D  -
     R  R/D  D  -
     -   R   R  !
    

    这是我们在处理单步 TD 方法时已经遇到的确定性情况的最优策略。它展示了我们希望智能体在这个环境中学习的最优动作。

  7. 定义将采取 ε-贪婪动作的函数:

    def action_epsilon_greedy(q, s, epsilon=0.05):
        if np.random.rand() > epsilon:
            return np.argmax(q[s])
        return np.random.randint(4)
    def get_action_epsilon_greedy(epsilon):
        return lambda q,s: action_epsilon_greedy\
                           (q, s, epsilon=epsilon)
    
  8. 定义一个将采取贪婪动作的函数:

    def greedy_policy(q, s):
        return np.argmax(q[s])
    
  9. 定义一个将计算智能体平均表现的函数:

    def average_performance(policy_fct, q):
        acc_returns = 0.
        n = 500
        for i in range(n):
            done = False
            s = env.reset()
            while not done:
                a = policy_fct(q, s)
                s, reward, done, info = env.step(a)
                acc_returns += reward
        return acc_returns/n
    
  10. 设置总回合数、表示我们评估智能体平均表现的间隔步数、折扣因子、学习率,以及控制其下降的 ε 参数——起始值、最小值和范围(以回合数为单位)——以及适用的 eligibility trace 衰减参数:

    # parameters for sarsa(lambda)
    episodes = 30000
    STEPS = 500
    gamma = 0.9
    alpha = 0.05
    epsilon_start = 0.2
    epsilon_end = 0.001
    epsilon_annealing_stop = int(episodes/2)
    eligibility_decay = 0.3
    
  11. 初始化 Q 表,除了终止状态外,所有值都设置为 1,并设置一个数组来收集训练过程中智能体的所有表现评估:

    q = np.zeros((16, 4))
    # Set q(terminal,*) equal to 0
    q[5,:] = 0.0
    q[7,:] = 0.0
    q[11,:] = 0.0
    q[12,:] = 0.0
    q[15,:] = 0.0
    performance = np.ndarray(episodes//STEPS)
    
  12. 通过在所有回合中循环来启动 SARSA 训练循环:

    for episode in range(episodes):
    
  13. 根据当前回合的运行定义一个 epsilon 值:

        inew = min(episode,epsilon_annealing_stop)
        epsilon = (epsilon_start * (epsilon_annealing_stop - inew) \
                   + epsilon_end * inew) / epsilon_annealing_stop
    
  14. 将资格迹表初始化为0

        E = np.zeros((16, 4))
    
  15. 重置环境,使用ε-贪婪策略选择第一个动作,并开始回合内循环:

        state = env.reset()
        action = action_epsilon_greedy(q, state, epsilon)
        while True:
    
  16. 更新资格迹并为最后访问的状态分配一个1的权重:

            E = eligibility_decay * gamma * E
            E[state, action] += 1
    
  17. 使用选定的动作在环境中执行一步,获取新的状态、奖励和结束条件:

            new_state, reward, done, info = env.step(action)
    
  18. 使用ε-贪婪策略选择新的动作:

            new_action = action_epsilon_greedy\
                         (q, new_state, epsilon)
    
  19. 计算https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33e.png更新,并使用 SARSA TD(https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_33h.png)规则更新 Q 表:

            delta = reward + gamma \
                    * q[new_state, new_action] - q[state, action]
            q = q + alpha * delta * E 
    
  20. 使用新的状态和动作值更新状态和动作:

            state, action = new_state, new_action
            if done:
                break
    
  21. 评估智能体的平均表现:

        if episode%STEPS == 0:
            performance[episode//STEPS] = average_performance\
                                          (get_action_epsilon_greedy\
                                          (epsilon), q=q)
    
  22. 绘制 SARSA 智能体在训练过程中的平均奖励历史:

    plt.plot(STEPS*np.arange(episodes//STEPS), performance)
    plt.xlabel("Epochs")
    plt.title("Learning progress for SARSA")
    plt.ylabel("Average reward of an epoch")
    

    这将生成以下输出:

    Text(0, 0.5, 'Average reward of an epoch')
    

    这个过程的图表可以如下可视化:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_35.jpg

    图 7.35:训练回合中一个 epoch 的平均奖励趋势

    如我们所见,SARSA 的 TD(https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_35a.png)表现随着ε参数的退火而逐步提高,从而在极限情况下达到 0,并最终获得贪婪策略。这还表明该算法能够在学习后达到 100%的成功率。与单步 SARSA 模型相比,正如图 7.8所示,我们可以看到它更快地达到了最大性能,表现出显著的改进。

  23. 评估训练后的智能体(Q 表)在贪婪策略下的表现:

    greedyPolicyAvgPerf = average_performance(greedy_policy, q=q)
    print("Greedy policy SARSA performance =", greedyPolicyAvgPerf)
    

    输出将如下所示:

    Greedy policy SARSA performance = 1.0
    
  24. 显示 Q 表的值:

    q = np.round(q,3)
    print("(A,S) Value function =", q.shape)
    print("First row")
    print(q[0:4,:])
    print("Second row")
    print(q[4:8,:])
    print("Third row")
    print(q[8:12,:])
    print("Fourth row")
    print(q[12:16,:])
    

    这将生成以下输出:

    (A,S) Value function = (16, 4)
    First row
    [[0.499 0.59  0.519 0.501]
     [0.474 0\.    0.615 0.518]
     [0.529 0.699 0.528 0.589]
     [0.608 0.397 0.519 0.517]]
    Second row
    [[0.553 0.656 0\.    0.489]
     [0\.    0\.    0\.    0\.   ]
     [0\.    0.806 0\.    0.593]
     [0\.    0\.    0\.    0\.   ]]
    Third row
    [[0.619 0\.    0.729 0.563]
     [0.613 0.77  0.81  0\.   ]
     [0.712 0.9   0\.    0.678]
     [0\.    0\.    0\.    0\.   ]]
    Fourth row
    [[0\.    0\.    0\.    0\.   ]
     [0.003 0.8   0.9   0.683]
     [0.76  0.892 1\.    0.787]
     [0\.    0\.    0\.    0\.   ]]
    

    该输出显示了我们问题的完整状态-动作价值函数的值。这些值随后用于通过贪婪选择规则生成最优策略。

  25. 打印出找到的贪婪策略,并与最优策略进行比较:

    policyFound = [actionsDict[np.argmax(q[0,:])],\
                   actionsDict[np.argmax(q[1,:])],\
                   actionsDict[np.argmax(q[2,:])],\
                   actionsDict[np.argmax(q[3,:])],\
                   actionsDict[np.argmax(q[4,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[6,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[8,:])],\
                   actionsDict[np.argmax(q[9,:])],\
                   actionsDict[np.argmax(q[10,:])],\
                   " - ",\
                   " - ",\
                   actionsDict[np.argmax(q[13,:])],\
                   actionsDict[np.argmax(q[14,:])],\
                   " ! "]
    print("Greedy policy found:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(policyFound[idx+0], policyFound[idx+1], \
              policyFound[idx+2], policyFound[idx+3])
    print(" ")
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    这将产生以下输出:

    Greedy policy found:
     R   R   D   L 
     D   -   D   - 
     R   D   D   - 
     -   R   R   ! 
    Optimal policy:
    R/D  R   D   L 
     D   -   D   - 
     R  R/D  D   - 
     -   R   R   !
    

如您所见,我们的 SARSA 算法已经能够通过在确定性转移动态下学习最优策略来正确解决 FrozenLake-v0 环境。实际上,正如我们所见,对于网格世界中的每个状态,通过我们的算法计算出的 Q 表获得的贪婪策略都会给出与通过分析环境问题定义的最优策略一致的动作。正如我们之前看到的,有两个状态具有两个同等最优的动作,智能体正确地执行了其中之一。

注意

要访问该特定部分的源代码,请参阅packt.live/2YdePoa

您也可以在packt.live/3ek4ZXa上在线运行此示例。

我们现在可以继续,测试它在暴露于随机动态下时的表现。我们将在下一个练习中进行测试。就像使用一步 SARSA 时一样,在这种情况下,我们希望给予智能体自由,以便利用中间步骤的零惩罚,减少掉入陷阱的风险,因此,在这种情况下,我们必须将折扣因子 gamma 设置为 1。 这意味着我们将使用 gamma = 1.0,而不是使用 gamma = 0.9

练习 7.05:使用 TD(λ) SARSA 解决 FrozenLake-v0 随机过渡

在本练习中,我们将实现我们的 SARSA(λ) 算法来解决在确定性环境动态下的 FrozenLake-v0 环境。正如我们在本章前面看到的,当谈论一步 TD 方法时,最优策略与前一个练习完全不同,因为它需要考虑随机性因素。这对 SARSA(λ) 算法提出了新的挑战。我们将看到它如何仍然能够在本练习中解决这个任务。

按照以下步骤完成本练习:

  1. 导入所需的模块:

    import numpy as np
    from numpy.random import random, choice
    import matplotlib.pyplot as plt
    %matplotlib inline
    import gym
    
  2. 使用设置了 is_slippery=True 标志的 gym 环境实例化 FrozenLake-v0,以启用随机性:

    env = gym.make('FrozenLake-v0', is_slippery=True)
    
  3. 看一下动作和观察空间:

    print("Action space = ", env.action_space)
    print("Observation space = ", env.observation_space)
    

    这将打印出以下内容:

    Action space =  Discrete(4)
    Observation space =  Discrete(16)
    
  4. 创建两个字典,以便轻松地将actions数字转换为移动:

    actionsDict = {}
    actionsDict[0] = "  L  "
    actionsDict[1] = "  D  "
    actionsDict[2] = "  R  "
    actionsDict[3] = "  U  "
    actionsDictInv = {}
    actionsDictInv["L"] = 0
    actionsDictInv["D"] = 1
    actionsDictInv["R"] = 2
    actionsDictInv["U"] = 3
    
  5. 重置环境并渲染它,以查看网格问题:

    env.reset()
    env.render()
    

    输出将如下所示:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_36.jpg

    ](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_36.jpg)

    图 7.36:环境的初始状态

  6. 可视化该环境的最优策略:

    optimalPolicy = ["L/R/D","  U  ","  U  ","  U  ",\
                     "  L  ","  -  "," L/R ","  -  ",\
                     "  U  ","  D  ","  L  ","  -  ",\
                     "  -  ","  R  ","  D  ","  !  ",]
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    这将打印出以下输出:

    Optimal policy:  
      L/R/D  U    U    U
        L    -   L/R   -
        U    D    L    -
        -    R    D    !
    

    这代表了该环境的最优策略。除了两个状态,其他所有状态都与单一的最优行为相关联。事实上,正如本章前面描述的,最优行为是那些将智能体远离陷阱的行为,或者远离可能导致智能体掉入陷阱的格子。两个状态有多个等效的最优行为,这正是本任务的要求。

  7. 定义将采取 ε-贪心行为的函数:

    def action_epsilon_greedy(q, s, epsilon=0.05):
        if np.random.rand() > epsilon:
            return np.argmax(q[s])
        return np.random.randint(4)
    def get_action_epsilon_greedy(epsilon):
        return lambda q,s: action_epsilon_greedy\
                           (q, s, epsilon=epsilon)
    
  8. 定义一个将采取贪心行为的函数:

    def greedy_policy(q, s):
        return np.argmax(q[s])
    
  9. 定义一个函数,计算智能体的平均表现:

    def average_performance(policy_fct, q):
        acc_returns = 0.
        n = 500
        for i in range(n):
            done = False
            s = env.reset()
            while not done:
                a = policy_fct(q, s)
                s, reward, done, info = env.step(a)
                acc_returns += reward
        return acc_returns/n
    
  10. 设置总回合数、表示我们将评估智能体平均表现的步数间隔、折扣因子、学习率以及控制ε衰减的参数——起始值、最小值以及在一定回合数内衰减的范围,以及资格迹衰减参数:

    # parameters for sarsa(lambda)
    episodes = 80000
    STEPS = 2000
    gamma = 1
    alpha = 0.02
    epsilon_start = 0.2
    epsilon_end = 0.001
    epsilon_annealing_stop = int(episodes/2)
    eligibility_decay = 0.3
    
  11. 初始化 Q 表,将所有值设置为 1,除终止状态外,并设置一个数组来收集在训练过程中所有智能体的表现评估:

    q = np.zeros((16, 4))
    # Set q(terminal,*) equal to 0
    q[5,:] = 0.0
    q[7,:] = 0.0
    q[11,:] = 0.0
    q[12,:] = 0.0
    q[15,:] = 0.0
    performance = np.ndarray(episodes//STEPS)
    
  12. 通过在所有回合中循环,开始 SARSA 训练循环:

    for episode in range(episodes):
    
  13. 根据当前剧集运行定义ε值:

        inew = min(episode,epsilon_annealing_stop)
        epsilon = (epsilon_start * (epsilon_annealing_stop - inew) \
                   + epsilon_end * inew) / epsilon_annealing_stop
    
  14. 初始化资格迹表为 0:

        E = np.zeros((16, 4))
    
  15. 重置环境并根据ε-贪婪策略设置初始动作选择。然后,开始剧集内部循环:

        state = env.reset()
        action = action_epsilon_greedy(q, state, epsilon)
        while True:
    
  16. 通过应用衰减并使最后一个状态-动作对最重要来更新资格迹:

            E = eligibility_decay * gamma * E
            E[state, action] += 1
    
  17. 定义环境步骤,选择动作并获取新的状态、奖励和完成条件:

            new_state, reward, done, info = env.step(action)
    
  18. 使用ε-贪婪策略选择新动作:

            new_action = action_epsilon_greedy(q, new_state, epsilon)
    
  19. 计算https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_36a.png更新并使用 SARSA TD(https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_36b.png)规则更新 Q 表:

            delta = reward + gamma \
                    * q[new_state, new_action] - q[state, action]
            q = q + alpha * delta * E 
    
  20. 使用新值更新状态和动作:

            state, action = new_state, new_action
            if done:
                break
    
  21. 评估代理的平均表现:

        if episode%STEPS == 0:
            performance[episode//STEPS] = average_performance\
                                          (get_action_epsilon_greedy\
                                          (epsilon), q=q)
    
  22. 绘制 SARSA 代理在训练期间的平均奖励历史:

    plt.plot(STEPS*np.arange(episodes//STEPS), performance)
    plt.xlabel("Epochs")
    plt.title("Learning progress for SARSA")
    plt.ylabel("Average reward of an epoch")
    

    这会生成以下输出:

    Text(0, 0.5, 'Average reward of an epoch')
    

    可以通过以下方式可视化该图:

    https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_37.jpg

    图 7.37:训练过程中每个时期的平均奖励趋势

    再次与图 7.15中看到的先前 TD(0) SARSA 情况进行比较,图形清楚地展示了即使在考虑随机动态时,算法的性能如何随着训练轮次的增加而改善。行为非常相似,也表明在随机动态的情况下,无法获得完美的表现,换句话说,无法 100%达到目标。

  23. 评估训练代理(Q 表)的贪婪策略表现:

    greedyPolicyAvgPerf = average_performance(greedy_policy, q=q)
    print("Greedy policy SARSA performance =", greedyPolicyAvgPerf)
    

    这会打印出以下输出:

    Greedy policy SARSA performance = 0.734
    
  24. 显示 Q 表的值:

    q = np.round(q,3)
    print("(A,S) Value function =", q.shape)
    print("First row")
    print(q[0:4,:])
    print("Second row")
    print(q[4:8,:])
    print("Third row")
    print(q[8:12,:])
    print("Fourth row")
    print(q[12:16,:])
    

    这会生成以下输出:

    (A,S) Value function = (16, 4)
    First row
    [[0.795 0.781 0.79  0.786]
     [0.426 0.386 0.319 0.793]
     [0.511 0.535 0.541 0.795]
     [0.341 0.416 0.393 0.796]]
    Second row
    [[0.794 0.515 0.541 0.519]
     [0\.    0\.    0\.    0\.   ]
     [0.321 0.211 0.469 0.125]
     [0\.    0\.    0\.    0\.   ]]
    Third row
    [[0.5   0.514 0.595 0.788]
     [0.584 0.778 0.525 0.46 ]
     [0.703 0.54  0.462 0.365]
     [0\.    0\.    0\.    0\.   ]]
    Fourth row
    [[0\.    0\.    0\.    0\.   ]
     [0.563 0.557 0.862 0.508]
     [0.823 0.94  0.878 0.863]
     [0\.    0\.    0\.    0\.   ]]
    

    此输出显示了我们问题的完整状态-动作值函数的值。然后,通过贪婪选择规则使用这些值来生成最优策略。

  25. 打印出找到的贪婪策略,并与最优策略进行比较:

    policyFound = [actionsDict[np.argmax(q[0,:])],\
                   actionsDict[np.argmax(q[1,:])],\
                   actionsDict[np.argmax(q[2,:])],\
                   actionsDict[np.argmax(q[3,:])],\
                   actionsDict[np.argmax(q[4,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[6,:])],\
                   " - ",\
                   actionsDict[np.argmax(q[8,:])],\
                   actionsDict[np.argmax(q[9,:])],\
                   actionsDict[np.argmax(q[10,:])],\
                   " - ",\
                   " - ",\
                   actionsDict[np.argmax(q[13,:])],\
                   actionsDict[np.argmax(q[14,:])],\
                   " ! "]
    print("Greedy policy found:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(policyFound[idx+0], policyFound[idx+1], \
              policyFound[idx+2], policyFound[idx+3])
    print(" ")
    print("Optimal policy:")
    idxs = [0,4,8,12]
    for idx in idxs:
        print(optimalPolicy[idx+0], optimalPolicy[idx+1], \
              optimalPolicy[idx+2], optimalPolicy[idx+3])
    

    这会生成以下输出:

    Greedy policy found: 
        L    U    U    U
        L    -    R    -
        U    D    L    -
        -    R    D    !
    Optimal policy:  
      L/R/D  U    U    U
        L    -   L/R   -
        U    D    L    -
        -    R    D    !
    

同样,在随机环境动态的情况下,带资格迹的 SARSA 算法也能够正确学习到最优策略。

注意

要访问此特定部分的源代码,请参考packt.live/2CiyZVf

您还可以在packt.live/2Np7zQ9上在线运行此示例。

通过本练习,我们完成了对时间差分方法的学习,涵盖了从最简单的一步式公式到最先进的方法。现在,我们能够在不受剧集结束后更新状态值(或状态-动作对)函数限制的情况下,结合多步方法。为了完成我们的学习旅程,我们将快速比较本章解释的方法与第五章 动态规划第六章 蒙特卡洛方法中解释的方法。

动态规划(DP)、蒙特卡洛(Monte-Carlo)和时间差分(TD)学习之间的关系

从我们在本章中学到的内容来看,正如我们多次提到的,时间差分学习(TD)显然具有与蒙特卡罗方法和动态规划方法相似的特点。像前者一样,TD 直接从经验中学习,而不依赖于表示过渡动态的环境模型或任务中涉及的奖励函数的知识。像后者一样,TD 通过自举(bootstrapping)更新,即部分基于其他估计更新值函数,这样就避免了必须等待直到一轮结束的需求。这个特点尤为重要,因为在实际中,我们可能遇到非常长的回合(甚至是无限回合),使得蒙特卡罗方法变得不切实际且过于缓慢。这种严格的关系在强化学习理论中扮演着核心角色。

我们还学到了 N 步方法和资格迹,这两个不同但相关的主题让我们能够将 TD 方法的理论框架呈现为一个通用的图景,能够将蒙特卡罗和 TD 方法融合在一起。特别是资格迹的概念让我们能够正式地表示它们,具有额外的优势,即将视角从前向视角转变为更高效的增量反向视角,从而使我们能够将蒙特卡罗方法扩展到非周期性问题。

当把 TD 和蒙特卡罗方法纳入同一个理论框架时,资格迹(eligibility traces)展示了它们在使 TD 方法更稳健地应对非马尔科夫任务方面的价值,这是蒙特卡罗算法表现更好的典型问题。因此,资格迹即使通常伴随着计算开销的增加,通常也能提供更好的学习能力,因为它们既更快速又更稳健。

现在是时候处理本章的最后一个活动了,我们将把在理论和已覆盖的 TD 方法练习中学到的知识付诸实践。

活动 7.01:使用 TD(0) Q-Learning 解决 FrozenLake-v0 随机过渡问题

本活动的目标是让你将 TD(0) Q-learning 算法应用于解决 FrozenLake-v0 环境中的随机过渡动态。我们已经看到,最优策略如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_07_38.jpg

图 7.38: 最优策略 – D = 向下移动,R = 向右移动,U = 向上移动,L = 向左移动

让 Q-learning 在这个环境中收敛并非易事,但这是可能的。为了让这个过程稍微简单一些,我们可以使用一个折扣因子 γ 值,设为 0.99。以下步骤将帮助你完成此练习:

  1. 导入所有必要的模块。

  2. 实例化健身环境并打印出观察空间和动作空间。

  3. 重置环境并呈现初始状态。

  4. 定义并打印出最优策略以供参考。

  5. 定义实现贪婪和 ε-贪婪策略的函数。

  6. 定义一个函数来评估智能体的平均表现,并初始化 Q 表。

  7. 定义学习方法的超参数(ε、折扣因子、总集数等)。

  8. 实现 Q 学习算法。

  9. 训练智能体并绘制平均性能随训练轮次变化的图表。

  10. 显示找到的 Q 值,并在将其与最优策略进行比较时,打印出贪婪策略。

本活动的最终输出与本章所有练习中遇到的非常相似。我们希望将使用指定方法训练的智能体找到的策略与最优策略进行比较,以确保我们成功地让其正确地学习到最优策略。

最优策略应如下所示:

Greedy policy found:
    L    U    U    U
    L    -    R    -
    U    D    L    -
    -    R    D    !
Optimal policy:  
  L/R/D  U    U    U
    L    -   L/R   -
    U    D    L    -
    -    R    D    !

注意

本活动的解决方案可以在第 726 页找到。

完成此活动后,我们学会了如何通过适当调整超参数,正确地实现和设置单步 Q 学习算法,以解决具有随机过渡动态的环境问题。我们在训练过程中监控了智能体的表现,并面对了奖励折扣因子的作用。我们为其选择了一个值,使得我们的智能体能够学习到针对这一特定任务的最优策略,即便该环境的最大奖励是有限的,并且无法保证 100%完成任务。

总结

本章讨论了时序差分学习。我们首先研究了单步方法,包括其在策略内部和策略外部的实现,进而学习了 SARSA 和 Q 学习算法。我们在 FrozenLake-v0 问题上测试了这些算法,涉及了确定性和随机过渡动态。接着,我们进入了 N 步时序差分方法,这是 TD 和 MC 方法统一的第一步。我们看到,策略内和策略外方法在这种情况下是如何扩展的。最后,我们研究了带有资格迹的时序差分方法,它们构成了描述 TD 和 MC 算法的统一理论的最重要步骤。我们还将 SARSA 扩展到资格迹,并通过实现两个练习在 FrozenLake-v0 环境中应用这一方法,涵盖了确定性和随机过渡动态。通过这些,我们能够在所有情况下成功地学习到最优策略,从而证明这些方法是可靠且健壮的。

现在,是时候进入下一章了,在这一章中,我们将讨论多臂老虎机问题,这是一个经典的设置,在研究强化学习理论及其算法应用时常常会遇到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值