【深度强化学习】Q-learning 和 贝尔曼方程

前言

重读《Deep Reinforcemnet Learning Hands-on》, 常读常新, 极其深入浅出的一本深度强化学习教程。 本文的唯一贡献是对其进行了翻译和提炼, 加一点自己的理解组织成一篇中文笔记。

原英文书下载地址: 传送门
原代码地址: 传送门

终于安定下来了, 以后周末固定更新, 工作日随缘更新, 暑假结束前争取写完全书。

第五章 Q-learning 和 贝尔曼方程

其实原文的标题是 Tabular Learning, 但是读下来之后, 觉得其实就是 大名鼎鼎的 Q-learning, 因此以此作为本章的题目。 在前面的章节中,我们讨论了交叉熵方法,接下来的这部分, 我们要介绍更灵活且更强大的 Q-learning 方法。

第一章中,我们提到过一个强化学习中非常重要的概念: 状态的价值——the value of the state。 我们将其定义为:

V(s)=E[t=0rtγt]V(s)=\mathbb{E}\left[\sum_{t=0}^{\infty} r_{t} \gamma^{t}\right]

其中 rtr_t是第t步时的reward。 γ\gamma则是折扣系数, 决定是否更在意当下reward还是长远。每个State的价值,应该和 采取的策略 紧密相连, 因为策略决定了后续步骤将到达的状态s(t)s(t)和获得的奖励r(t)r(t)。 举一个最简单的例子:

在这里插入图片描述

  • 我们的初始状态是 1:start。
  • 往右走到达终点状态2,并获得reward 1。
  • 往下走到达终点状态3; 并获得reward 2。

这是一个确定的environment。 现在,我们的问题是, State 1 的 Value是多少?

考虑以下4种策略, 再基于 V(s)=E[t=0rtγt]V(s)=\mathbb{E}\left[\sum_{t=0}^{\infty} r_{t} \gamma^{t}\right],有:

  • Agent无脑往右, 到达状态2。 那么Value 就是 1.
  • Agent无脑往下, 到达状态3。 那么Value 就是 2.
  • Agent 有 50%的概率往右,50%概率往下。 根据Value的计算公式, 此时应该求取期望, 即0.5×1+0.5×2=1.50.5\times 1 + 0.5 \times 2 = 1.5
  • Agent 有 10%的概率往右,90%概率往下。 根据Value的计算公式, 此时应该求取期望, 即1.5×1+0.9×2=1.91.5\times 1 + 0.9 \times 2 = 1.9

因此, 状态State的价值,与采取的策略直接挂钩。

那么,什么是这个环境的最佳策略? 强化学习的目标就是最大化reward, 而本例中, reward等于状态1的Value(因为到达的下两个状态都是终止状态),所以最好的策略就是无脑往下。

显然,实际的问题往往不会这么简单。 不用担心, 我们将学会如果让Agent学习到最优的策略。

贝尔曼方程

理查德贝尔曼 (Richard Bellman), 美国数学家, 提出了Q-learning的基石:贝尔曼方程。

我们首先考虑一个**确定性(deterministic)**环境, 即所有的动作都有确定的收益reward。假设在我们当前的State s0s_0处, 共有NN个动作,依次对应下个状态s1,s2,...s_1, s_2,..., 获得的收益为 r0,r1,...r_0, r_1, ..., 如图所示:

在这里插入图片描述
V1,V2,...V_1, V_2, ...就是上一节讨论的 s1,s2s_1, s_2状态所对应的 Value。
V0V_0代表我们想要探究的当前状态s0s_0的Value:
根据之前的公式: V0(a=ai)=E[t=0rtγt]=E[rt=0]+E[t=1rtγt]=ri+γViV_0(a=a_i)=\mathbb{E}\left[\sum_{t=0}^{\infty} r_{t} \gamma^{t}\right] =\mathbb{E}[ r_{t=0}] + \mathbb{E}\left[\sum_{t=1}^{\infty} r_{t} \gamma^{t}\right]=r_i+\gamma V_i
这个可能反而不太好理解:最简单的就直接直觉上理解:

当前状态下, 使用aia_i动作的价值 = 马上得到的价值(reward) + 下一个状态的价值。 因此, 假设我们的Agent足够优化, 我们肯定会选择最优的一个策略, 也就是说:

V0=maxa1N(ra+γVa)V_{0}=\max _{a \in 1 \ldots N}\left(r_{a}+\gamma V_{a}\right)

这个很好理解: 就像我高考考了700分这个节点作为我当前的状态, 那我可以考虑采取的动作是 报考清华和复读一年。 前者的收益相当高——当时就得到了录取通知书(rar_a), 且到达一个极佳的状态(进入清华攻读), 而后者的收益极低。 那么如何评估 高考考了700分 这个节点的价值呢? 显然应该用优秀的动作 (报考清华) 的收益来衡量其价值。 这就是贝尔曼方程, 给出了我们对 状态价值 (the value of the state)的准确定义。

很容易地推广到随机性环境——同一个动作可能有概率得到不同的rar_aVaV_a, 那就应该通过期望来判定:

SS代表aa动作可能得到的状态集合。 pa,0sp_{a, 0\rightarrow s}代表转移概率。

这个方程看上去像作弊了:我们定义了一个东西, 却在定义中先用到了这个定义的东西 —— 我们在对State 0 V0V_0 的价值的定义中,用到了对State s 的价值VsV_s 的定义。 然而这其实是一种广泛且通用的方法,如 动态规划, 数学归纳法中。 贝尔曼方程 不仅给出了我们所能得到的最大Reward, 也能给出最佳的策略(动作的选择)。

这本书这一部分讲的有点绕, 我总结一下,只需要记住这个方程就够了, 衡量State的价值:

V0=maxaAEsS[rs,a+γVs]=maxaAsSpa,0s(rs,a+γVs)V_{0}=\max _{a \in A} \mathbb{E}_{s \sim S}\left[r_{s, a}+\gamma V_{s}\right]=\max _{a \in A} \sum_{s \in S} p_{a, 0 \rightarrow s}\left(r_{s, a}+\gamma V_{s}\right)

Value of Action 动作的价值

为了让问题更简单, 我们可以定义除VsV_s外, 其他的度量:Qs,aQ_{s,a}, 作为对在ss状态下选取aa动作时的策略评估。 简单而言, Qs,aQ_{s,a}应当定于为在state s时执行动作aa时能获得的total reward。基于动作价值的方法, 就是大名鼎鼎的 Q-learning。

Qs,aQ_{s,a}的定义基于我们刚刚对State Value的定义:
Qs,a=EsS[rs,a+γVs]=sSpa,ss(rs,a+γVs)Q_{s, a}=\mathbb{E}_{s^{\prime} \sim S}\left[r_{s, a}+\gamma V_{s^{\prime}}\right]=\sum_{s^{\prime} \in S} p_{a, s \rightarrow s^{\prime}}\left(r_{s, a}+\gamma V_{s^{\prime}}\right)

对比上一节中让大家记住的方程,显然有:

Vs=maxaAQs,aV_{s}=\max _{a \in A} Q_{s, a}

这就是上一节中说的——高考考700分这一状态的价值, 是取决于你最优的决策报考清华。

同样,我们也可以基于上面这个方程, 把QQ用自己来表示:

Q(s,a)=rs,a+γmaxaAQ(s,a)Q(s, a)=r_{s, a}+\gamma \max _{a^{\prime} \in A} Q\left(s^{\prime}, a^{\prime}\right)

一个简单的例子

在这里插入图片描述
考虑上图这个环境——由于s1, s2, s3, s4都是终止状态,因此他们的价值等于0. 箭头线条旁标的是转移的概率。 如, 当选择 "up"动作时, 各有三分之一的概率到s1, s2, 和 s4状态。 由上一节定义的公式:
Qs,a=EsS[rs,a+γVs]=sSpa,ss(rs,a+γVs)Q_{s, a}=\mathbb{E}_{s^{\prime} \sim S}\left[r_{s, a}+\gamma V_{s^{\prime}}\right]=\sum_{s^{\prime} \in S} p_{a, s \rightarrow s^{\prime}}\left(r_{s, a}+\gamma V_{s^{\prime}}\right), 我们有:

Qs0,up=0.33(1+0)+0.33(2+0)+0.33(4+0)=2.31Q_{s_0, up} =0.33 * (1+0) + 0.33 * (2 + 0) + 0.33 * (4 + 0) = 2.31

同理可以计算其他3个动作left, right, down的Q值。

如果我们拥有了准确的Q值, 策略就自然而生了——选择执行每个状态时对应Q值最大的动作即可。 然而,在实际中, 我们很难直接得到如此准确的概率及对应的reward, 以及状态转换的情况。 我们往往需要通过训练。从经验中学习。 下一节中, 我们会介绍一种Q value的迭代方法来解决之前提到的FrozenLake问题。

值迭代算法

所谓值迭代算法, 就是指,通过训练积累经验,再根据经验更新值(如Q值), Q值随着训练愈发精确, 也就代表策略的愈发优秀。

V值迭代算法

  • 初始化所有State的VV值为0
  • 进行训练(随机和环境交互), 根据环境返回的信息——S’, r, 使用贝尔曼方程更新VV
    Vsmaxaspa,ss(rs,a+γVs)V_{s} \leftarrow \max _{a} \sum_{s^{\prime}} p_{a, s \rightarrow s^{\prime}}\left(r_{s, a}+\gamma V_{s^{\prime}}\right)
  • 重复步骤2, 直到VV值的改变很小。

Q值迭代算法

  • 初始化所有的动作Q值 Qs,aQ_{s,a}
  • 利用训练时的信息, 根据贝尔曼方程更新Q值
    Qs,aspa,ss(rs,a+γmaxaQs,a)Q_{s, a} \leftarrow \sum_{s^{\prime}} p_{a, s \rightarrow s^{\prime}}\left(r_{s, a}+\gamma \max _{a^{\prime}} Q_{s^{\prime}, a^{\prime}}\right)
  • 重复步骤2, 直到收敛

两种方法中, V值迭代的话需要维护一张保存了所有S个状态value的表, 即一个S×1S\times1的张量。 Q值迭代中则需要维护一张保存了所有S个状态下N个动作的Q值表, 即一个S×QS\times Q的张量(每个状态下有N个动作可选择)。实际践行策略时, 在s0状态时, V值迭代 会 遍历N个可选动作, 并计算各自的 r+Vsr + V_srr是执行这个动作的当即收益, VsV_s是执行后到达的状态,其Value可以直接读取(保存在维护的Value表格中)。然后,Agent选择值最大的一个动作执行。 Q值迭代就更简单了, 读取维护的Q值表格中, 代表了状态s0时所有动作Q值的那一行 ,选取Q值最大的一个动作执行, 即可。 两种方法本质上基本是完全一样的。

实例: FrozenLake中的值迭代算法

FrozenLake环境,在前一章中已介绍过, 不再赘述。 这里介绍使用值迭代法来解决。
书中选用的是V值迭代方法——后台维护一张表, 记录了所有状态的V值, 然后在训练中不断更新这张表, 最后用这张表来决定策略。

不多说了, 直接介绍代码。

import gym
import collections
from tensorboardX import SummaryWriter

ENV_NAME = "FrozenLake-v0"
GAMMA = 0.9
TEST_EPISODES = 20

一开始是简单的全局变量。TEST_EPISODES是用于评估Agent性能,所要进行的测试的游戏次数。

class Agent:
    def __init__(self):
        self.env = gym.make(ENV_NAME)
        self.state = self.env.reset()
        self.rewards = collections.defaultdict(float)
        self.transits = collections.defaultdict(collections.Counter)
        self.values = collections.defaultdict(float)

创建一个Agent类, 首先注意我们代码的核心思想,就在这个__init__初始化中。 **正如上面所说, 我们需要维护一张V值表。 但要想得到这个V值表,根据贝尔曼公式, 我们还需要知道每个动作对应的转换概率(用于计算期望值)。同时,由于我们最后根据V值表选取动作策略时, 还要考虑动作本身的当即reward,因此,也要维护一张reward表。 这个数据结构是理解算法的重点,这里重点介绍一下。

collection.defaultdict

作者使用了collection库的defaultdict类来建立数据结构。 这个类很容易理解,只有两点:

  • 其他所有使用方法和普通的字典dict完全一样,也是key值和value值配对。
  • 顾名思义, 可以指定字典的默认值。 如:
    在这里插入图片描述
    如果你访问普通字典(上例中的a)中的未定义的键值时, 会直接报KeyError错误。 但如果是defaultdict,会根据你一开始的参数(float), 返回一个默认值。 如,我们在[27]中,把float作为输入的参数建立了一个defaultdict b, 那么当我们访问没有定义的b[0]时,会自动返回默认初始化值,float对应的是0.

这里先理解的话, 把defaultdict看做普通的dict看即可。

然后注意到,self.transits 的defaultdict的默认参数是 collections.Counter, 这个类可以这样理解: 就是一个字典, 键任意,值是一个整数。 这个类经常用于统计词频——键是词语本身, 值是该词出现的次数。 在本例中, 键是状态, 值是出现的次数。 通过训练中,各个状态之间转换的次数统计, 我们可以算出频率, 也就对应于我们想要获得的转换概率,用以计算V值。

我们再结合下一段代码,来理解Agent中维护的这三个表的意义:

 def play_n_random_steps(self, count):
        for _ in range(count):
            action = self.env.action_space.sample()
            new_state, reward, is_done, _ = self.env.step(action)
            self.rewards[(self.state, action, new_state)] = reward
            self.transits[(self.state, action)][new_state] += 1
            self.state = self.env.reset() if is_done else new_state            

这一段,就是我们之前提到的进行训练(本质上是随机玩游戏),再通过积累的经验更新表格。
self.rewards这个字典(其实是defaultdict),键值是(当前状态, 动作, 新状态), 值是这一转换——通过执行该动作从当前状态转换到新状态的reward。
在这里插入图片描述
同理, self.transits里储存的格式为:
在这里插入图片描述
可以理解为字典的字典。 其中,键值是当前的状态和执行的动作, 值是一个字典, 该字典的键是每个可能达到的下一状态, 值是 出现的次数。 比如上图中,第一项代表的是 —— 在0状态时,执行动作2后的 情况—— 达到1状态11次, 达到4状态6次, 保持在0状态5次。 通过这个数据,我们就可以粗略地计算一个转移概率了。

在这里插入图片描述
上图是一个简单的释义——如何通过维护的transit表格, 计算Q值和V值。

接下来, 我们再给出一个计算动作Q值的代码——这不仅在执行策略时需要用到, 也会用于维护V值表(V值相当于当前状态所有动作Q值的最大值)

    def calc_action_value(self, state, action):
        target_counts = self.transits[(state, action)]
        total = sum(target_counts.values())
        action_value = 0.0
        for tgt_state, count in target_counts.items():
            reward = self.rewards[(state, action, tgt_state)]
            action_value += (count / total) * (reward + GAMMA * self.values[tgt_state])
        return action_value

因为我们有一张维护的V值表, 所以Q值的计算只需要由我们维护的reward表中的reward 加上到达状态的V值就行了。 由于到达的状态也有随机性, 因此, 我们需要计算期望。

然后, 我们就拥有了根据训练经验来更新表的能力,由以下代码实现:

    def value_iteration(self):
        for state in range(self.env.observation_space.n):
            state_values = [self.calc_action_value(state, action)
                            for action in range(self.env.action_space.n)]
            self.values[state] = max(state_values)

遍历所有可能的状态, 每个状态的值,用公式 Vs=maxaAQs,aV_{s}=\max _{a \in A} Q_{s, a} 来计算V值进行更新即可,Q值则用刚刚的计算函数计算即可。

最后,写一个测试函数,用于评估策略性能:

   def select_action(self, state):
        best_action, best_value = None, None
        for action in range(self.env.action_space.n):
            action_value = self.calc_action_value(state, action)
            if best_value is None or best_value < action_value:
                best_value = action_value
                best_action = action
        return best_action

  def play_episode(self, env):
        total_reward = 0.0
        state = env.reset()
        while True:
            action = self.select_action(state)
            new_state, reward, is_done, _ = env.step(action)
            self.rewards[(state, action, new_state)] = reward
            self.transits[(state, action)][new_state] += 1
            total_reward += reward
            if is_done:
                break
            state = new_state
        return total_reward

我们的策略就是, 计算每个动作的Q值, 然后选择其中最大的一个执行。 最后的total reward用于评估当前Agent的性能。

最后,运行程序。 每次由于随机性, 结果会有一定的偏差。
在这里插入图片描述

总结

方法的核心就是用贝尔曼公式, 利用训练时的积累经验,不断更新V值和Q值, 最后Agent采取的策略就是利用维护的V值或Q值表, 选择Q值最大的动作执行。 其中巧妙的就是,每个V值定义在下一状态的V值之后, 类似于数学归纳法和动态规划的思想,可以有效解决问题。这也是DQN的核心 这样的递归定义可以有效的解决这种问题。

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值