强化学习丨时序差分算法TD(0)及相关编程仿真

目录

一、算法简介

二、时序差分预测

2.1 算法介绍

2.2 算法应用:随机游走问题

2.3 TD(0)与MC的比较以及批量更新

三、Sarsa:同轨策略下的时序差分控制算法

3.1 算法介绍

3.2 算法应用:悬崖行走问题(Cliff Walking)

四、Q学习:离轨策略下的时序差分控制算法

4.1 算法介绍

4.2 算法应用

五、期望Sarsa

5.1 算法介绍

5.2 算法应用

5.3 三种控制算法之间的比较

六、最大化偏差与双学习

6.1 最大化偏差(Maximization Bias)

6.2 双学习(Double Learning)

参考文献与网址


一、算法简介

        在上两篇文章中,笔者分别介绍了利用自举思想并需要完整环境知识的动态规划算法(DP),以及无需先验环境知识而仅根据经验来预测与决策的蒙特卡洛方法(MC),两者各有所长,若将DP自举的思想与MC无需构建环境模型的优势结合在一起,我们就得到了一种新的方法,也即是强化学习所有的思想中最核心、最新颖的思想——时序差分算法(Temporal Difference,TD)

二、时序差分预测

2.1 算法介绍

        所有强化学习的算法都会解决最基本的一个问题,那就是对某一策略\pi下状态价值的预测,在无环境模型的情况下,如上篇文章讲到的MC算法,智能体是通过幕序列的训练来对访问状态的回报平均进行价值估计:

V(S_{t}) = V(S_{t}) + \frac{1}{n}[G_{t}-V(S_{t})]

        这是一种无遗忘因子的估计方式,对于平稳问题较为有效,若要跟踪一个非平稳问题,我们可以将增量步长\frac{1}{n}设置为常数\alpha,估计公式变为:

V(S_{t}) = V(S_{t}) + \alpha[G_{t}-V(S_{t})]

        这就是常量\alphaMC,在笔者的文章强化学习丨多臂老虎机相关算法的总结及其MATLAB仿真中中有提到,这是一种指数近因加权方式,也即引入了遗忘因子\alpha(也叫做增量步长),人为的使智能体对近阶段的学习更深刻些,从而跟踪非平稳问题。然而,对与上式而言,我们为了得到回报值G_{t},往往需要等到一幕结束,而对于一个较长的幕序列,甚至一个持续性任务,这种方式是不必要以及费时的。

        在某一时刻我们可以得到此时刻的收益R_{t+1},为了得到此时刻的回报G_{t},我们不妨用下一状态的估计价值V(S_{t+1})来替代下一时刻的回报值,再结合回报的递推公式可得到新的价值估计公式如下:

V(S_{t}) = V(S_{t}) + \alpha[R_{t+1}+\gamma V(S_{t+1})-V(S_{t})]

        这就是时序差分预测算法的价值更新公式,有了该公式,我们就不难得到其算法流程:

时序差分预测算法TD(0)

Step1: 输入待评估的策略\pi

Step2: 定义增量步长\alpha \in (0,1]

Step3: 初始化状态价值函数V({S})为零向量

Step4: 遍历每幕:

                初始化S

                生成幕的同时进行价值更新:

                        根据策略\pi 选择动作并执行,观测到R,S^{'}

                        V(S)=V(S)+\alpha[R+\gamma V(S^{'}) - V(S)]

                        S = S^{'}

                        若S为终止状态则终止本幕,开始访问下一幕

        通过上述式子可以看出,在预测算法中,MC方法是将回报G_{t}的估计值作为学习目标,而TD算法则是将R+\gamma V(S^{'})的估计值作为学习目标。上面流程中提到的TD(0)的括号里的数值就是该TD算法下,S_{t}的估计值V(S_{t})与更好的估计R+\gamma V(S^{'})之间的误差,表示为:

\delta_{t}=R_{t+1}+\gamma V(S_{t+1}) - V(S_{t})

         如果价值函数数组V在一幕内没有变化(指幕的生成过程中),例如在MC算法中就是如此,则MC误差可写为TD误差之和,由于篇幅有限以及推导过程较易,这里直接给出下式,具体过程见Sutton的《强化学习》的式(6.6):

G_{t}-V({S_{t}}) = \sum_{k=t}^{T-1}\gamma^{t-1}\delta_{k}

        当价值函数数组V在一幕内发生变化时,例如TD(0)的情况下V被不断更新,不妨用V^{*}表示未更新的价值,V表示已经更新的价值,则MC误差依然可表示为TD误差之和,推导如下:

\begin{align*} G_{t}-V(S_{t}) &= R_{t+1}+\gamma G_{t+1}-V(S_{t})+\gamma V^{*}(S_{t+1})-\gamma V^{*}(S_{t+1})\\ &=(R_{t+1}+\gamma V^{*}(S_{t+1})-V^{*}(S_{t}))+\gamma(G_{t+1}-V^{*}(S_{t+1}))-(V(S_{t})-V^{*}(S_{t}))\\ &=\delta_{t}+\gamma[G_{t+1}-V(S_{t+1})+(V(S_{t+1})-V^{*}(S_{t+1}))]-\alpha \delta_{t}\\ &=(1-\alpha)\delta_{t}+\gamma\alpha\delta_{t+1} +\gamma[G_{t+1}-V(S_{t+1})]\\ &=(1-\alpha)\delta_{t}+\gamma\alpha\delta_{t+1} +\gamma[(1-\alpha)\delta_{t+1}+\gamma\alpha\delta_{t+2}+\gamma(G_{t+1}-V(S_{t+1})) ]\\ &=(1-\alpha)\delta_{t}+\gamma\delta_{t+1}+\gamma^{2}\alpha\delta_{t+2}+\gamma^{2}(G_{t+1}-V(S_{t+1}))\\ &=(1-\alpha)\delta_{t}+\gamma\delta_{t+1}+\gamma^{2}\delta_{t+2}+\cdots+\gamma^{T-t-1}\delta_{T-1}+\gamma^{T-t}(G_{T}-V(S_{T}))\\ &=-\alpha\delta_{t}+\sum_{k=t}^{T-1}\gamma^{k-t}\delta_{k} \end{align*}

        可见,当增量步长\alpha较小时,可用前式近似替代上式。

2.2 算法应用:随机游走问题

随机游走问题

        存在一个状态转移路径如下:

        所有幕都是从中心状态C开始,在每个时刻以相同的概率向左或向右移动一个状态,左侧和右侧的两个实心方框为终止状态,当终止于右侧方框时会有+1的收益,除此之外每次动作的收益均为0,假设该任务无折扣,估计该策略下(随机向左向右移动)各状态的价值。

        因为该任务无折扣,所以不难发现每个状态的真实价值是从该状态开始终止与最右侧的概率,由于该问题模型比较简单,环境模型比较容易构建,因此可以很容易的列出该问题的贝尔曼方程并进行求解得到,状态A~E的真实价值分别为:\frac{1}{6},\frac{2}{6},\frac{3}{6},\frac{4}{6},\frac{5}{6}。如果采用TD(0)算法,根据上述算法流程也不难可以编写出该问题的TD(0)算法求解的代码:

# Algorithm: Temporal Difference——Policy Evaluation
# Project  :Random Walking
# Author   : XD_MaoHai
# Reference: Jabes
# Date     : 2021/11/18


# 导入库函数
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
# 设置进度条
from tqdm import tqdm


# 初始化A-E的状态价值为0.5,右侧终点值为1,左侧终点值为0
# VALUES = [左侧终点 A B C D E 右侧终点]
VALUES = np.zeros(7)
VALUES[1:6] = 0.5
VALUES[6] = 1
# 各个状态真实价值
VALUES_TRUE = np.zeros(7)
VALUES_TRUE[1:7] = np.arange(1, 7)/6.0
# 定义向左向右走的动作
ACTION_LEFT = 0
ACTION_RIGHT = 1
# 状态转移收益为0
REWARD = 0


# 时序差分算法
def temporal_difference(values, alpha=0.1, batch_updating=False):
    """
    values: 状态价值矩阵
    alpha : 增量步长
    batch_updating: 是否批量更新
    return: 返回本幕序列
    """
    # 起始状态为C
    state = 3
    # 定义轨迹列表
    trajectory = [state]
    # 定义收益列表
    rewards = [0]
    # 生成幕序列
    while True:
        old_state = state
        # 以0.5的概率随机选择向左向右走
        if np.random.binomial(1, 0.5) == ACTION_LEFT:
            state -= 1
        else:
            state += 1
        # 更新轨迹列表
        trajectory.append(state)
        # 若非批量更新
        if not batch_updating:
            # TD更新
            values[old_state] += alpha * (REWARD + values[state] - values[old_state])
        # 判断该幕是否终止
        if state == 6 or state == 0:
            break
        rewards.append(REWARD)
    return trajectory, rewards



# 主函数
if __name__ == '__main__':
    # 在以下幕次序画图
    episodes = [0, 1, 10, 100]
    # 初始化价值矩阵
    current_values = np.copy(VALUES)
    plt.figure(1)
    # 开始迭代
    for i in range(episodes[-1] + 1):
        if i in episodes:
            plt.plot(current_values, label=str(i) + 'episodes')
        temporal_difference(current_values)
    plt.plot(VALUES_TRUE, label='True values')
    plt.xlabel('State')
    plt.ylabel('Values')
    plt.legend()

        运行程序后可得到各幕数量下状态估计价值函数如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第1张图片

        其中紫线表示真实状态价值,可见随着训练幕数的增多,各状态价值渐渐趋近真实价值。 

2.3 TD(0)与MC的比较以及批量更新

        上面已经提到,TD(0)算法优于MC的一个地方就是其利用了自举的思想,不需要等到幕结束就可以根据下一状态价值进行本时刻状态价值估计,并且该算法已经被理论证明,对于任何固定的策略\pi,如果增量步长\alpha选择一个足够小的常数,那么它的均值就能收敛到真实价值v_{\pi}

        虽然TD(0)与MC都具有收敛性,但遗憾的是,目前两者的收敛速度还没能从数学角度上进行比较证明,但在实践中,对于类似上面随机游走问题的随机任务,TD方法通常比常量\alphaMC方法收敛的快,为比较两者在随机游走问题中的收敛速度,在上述代码段中添加常量\alphaMC方法求解的代码以及计算均方根误差(Root Mean Squared Error,RMSE)的代码:

# 蒙特卡洛算法(每次访问)
def monte_carlo(values, alpha=0.1, batch_updating=False):
    """
    values: 状态价值矩阵
    alpha : 增量步长
    batch_updating: 是否批量更新
    return: 返回本幕序列
    """
    # 起始状态为C
    state = 3
    # 定义轨迹列表
    trajectory = [state]
    # 生成幕序列
    while True:
        old_state = state
        # 以0.5的概率随机选择向左向右走
        if np.random.binomial(1, 0.5) == ACTION_LEFT:
            state -= 1
        else:
            state += 1
        # 更新轨迹列表
        trajectory.append(state)
        # 判断该幕是否终止
        if state == 6:
            reward = 1.0
            break
        elif state == 0:
            reward = 0.0
            break
    # 若非批量更新
    if not batch_updating:
        for state_ in trajectory[:-1]:
            # MC更新
            values[state_] += alpha * (reward - values[state_])
    return trajectory, [reward] * (len(trajectory) - 1)


# 计算平均经验均方根误差
def rms_error():
    # 设置TD与MC的增量步长参数
    td_alphas = [0.15, 0.1, 0.05]
    mc_alphas = [0.01, 0.02, 0.03, 0.04]
    # 设定幕数量
    episodes = 100
    # 设定遍历次数
    runs = 100
    # 计算每个alpha下的均方根误差
    for i, alpha in enumerate(td_alphas + mc_alphas):
        total_errors = np.zeros(episodes + 1)
        if i < len(td_alphas):
            method = 'TD'
            linestyle = 'solid'
        else:
            method = 'MC'
            linestyle = 'dashdot'
        # runs次独立迭代运算
        for r in tqdm(range(runs)):
            errors = []
            current_values = np.copy(VALUES)
            # 基于episodes个幕迭代
            for i in range(0, episodes + 1):
                # 计算A~E几个价值估计的均方根误差
                errors.append(np.sqrt(np.sum(np.power(VALUES_TRUE[1:6] - current_values[1:6], 2)) / 5.0))
                if method == 'TD':
                    temporal_difference(current_values, alpha=alpha)
                else:
                    monte_carlo(current_values, alpha=alpha)
            total_errors += np.asarray(errors)
        total_errors /= runs
        plt.plot(total_errors, linestyle=linestyle, label=method + ', alpha = %.02f' % (alpha))
    plt.xlabel('Episodes')
    plt.ylabel('RMSE')
    plt.legend()

         之后在主函数进行调用即可:

# 计算均方根误差
    plt.figure(2)
    rms_error()

        得到两种算法,不同增量步长\alpha下均方根误差随幕数增长的变化曲线如下: 

强化学习丨时序差分算法TD(0)及相关编程仿真_第2张图片

        可见,在该问题中,TD方法要比MC方法表现的好。

        通过,TD(0)的价值更新公式可以看出,该算法是利用单次时刻的误差\delta_{t}进行价值更新的,如果现在训练幕数或是时间步长有限,为了避免单次误差的随机性,我们不妨将每个时刻该状态价值估计的偏差进行记录求和,在所有的幕训练结束后再利用这些偏差和进行价值更新,这样就相当于对偏差做了一次期望(但并未平均),这样的更新方法就叫做批量更新

        同样,MC算法也可以利用批量更新的方式进行求解,但是,在选择的步长参数\alpha足够小的情况下,TD(0)可以确定的收敛到与\alpha无关的唯一结果,而常数\alphaMC方法在相同条件下也能确定的收敛,但是会收敛到不同的结果。

         为比较批量更新方法下,TD(0)与MC的收敛速度,现在上面随机游走问题的代码段中添加批量更新代码部分:

# 批量更新
def batch_updating(method, episodes, alpha):
    # 设定遍历次数
    runs = 100
    # 初始化均方根误差矩阵
    total_errors = np.zeros(episodes)
    # runs次独立迭代运算
    for r in tqdm(range(0, runs)):
        # 初始化状态价值
        current_values = np.copy(VALUES)
        # 初始化当前幕数量下的均方根误差列表
        errors = []
        # 定义当前记录的所有幕
        trajectories = []
        # 定义当前记录的幕对应的收益序列
        rewards = []
        # 基于episodes个幕迭代
        for ep in range(episodes):
            # 选择当前算法:TD or MC
            if method == 'TD':
                trajectory_, rewards_ = temporal_difference(current_values, alpha=alpha, batch_updating=True)
            else:
                trajectory_, rewards_ = monte_carlo(current_values, alpha=alpha, batch_updating=True)
            # 记录幕序列和收益序列
            trajectories.append(trajectory_)
            rewards.append(rewards_)
            # 批量更新直至状态价值收敛到一定阈值
            while True:
                # 初始化误差列表
                updates = np.zeros(7)
                # 遍历
                for trajectory_, rewards_ in zip(trajectories, rewards):
                    for i in range(0, len(trajectory_) - 1):
                        if method == 'TD':
                            updates[trajectory_[i]] += rewards_[i] + current_values[trajectory_[i+1]] - current_values[trajectory_[i]]
                        else:
                            updates[trajectory_[i]] += rewards_[i] - current_values[trajectory_[i]]
                updates *= alpha
                # 批量更新
                current_values += updates
                # 当满足收敛精度(1e-3)时
                if np.sum(np.abs(updates)) < 1e-3:
                    break
            # 计算均方根误差
            errors.append(np.sqrt(np.sum(np.power(current_values[1:6] - VALUES_TRUE[1:6], 2)) / 5.0))
        total_errors += np.asarray(errors)
    total_errors /= runs
    return total_errors

        之后在主函数中进行调用绘图即可:

# 批量更新下TD与MC比较
    td_rmse = batch_updating('TD', episodes=episodes[-1], alpha=0.001)
    mc_rmse = batch_updating('MC', episodes=episodes[-1], alpha=0.001)
    # 绘图
    plt.figure(3)
    plt.plot(td_rmse, label='TD')
    plt.plot(mc_rmse, label='MC')
    plt.xlabel('Episodes')
    plt.ylabel('RMSE')
    plt.legend()

        运行程序可得到批量更新方法下,TD(0)与MC方法均方根误差随幕数的增长而变化的曲线:

强化学习丨时序差分算法TD(0)及相关编程仿真_第3张图片

         可见,批量TD方法比批量MC方法表现的要好。

        其实,这种TD(0)的最优性也不难理解,通过对批量TD(0)与批量MC两种算法的原理可以得知,由于没有自举特性而全然依赖样本序列,批量MC算法总是在找出最小化训练集上均方误差的估计,相当于仅对回报统计量进行期望估计;而由于偏差求和的操作,批量TD(0)的算法暗含了对MDP环境模型参数的估计,比如下个时刻状态的转移概率以及相应的收益期望,也就是说批量TD(0)总是在找出符合MDP模型的最大似然参数估计,并且等价于假设潜在过程参数的估计是确定性的而非近似的,这种估计就叫做确定性等价估计,这也解释了在与预测回报相关的任务中该方法的优越性。

        虽然非批量TD(0)并不能达到确定性估计,但它大致朝着这些方向在更新,因此它可能比常数\alphaMC方法更快。

三、Sarsa:同轨策略下的时序差分控制算法

        解决完预测问题,接下来就要解决控制问题。在上篇文章中,笔者介绍了MC算法的同轨和离轨两种控制方法,同样TD(0)算法也具有此两种方法,同时仍然遵循广义策略迭代(GPI)的模式,以下先对同轨策略下的TD(0)算法做一介绍。

3.1 算法介绍

        在没有环境模型的情况下,为了找到最优策略,我们需要参照的是动作价值函数,同状态价值函数的更新公式类似,TD(0)算法下动作价值函数更新公式如下:

Q(S_{t},A_{t})=Q(S_{t},A_{t})+\alpha[R_{t+1}+\gamma Q(S_{t+1},A_{t+1})-Q(S_{t},A_{t})]

        由于上式涉及两个时刻状态-动作二元组函数以及一个时刻的收益,因此同轨策略下的时序差分控制算法又叫做Sarsa算法。利用上式可解决GPI中的策略评估问题,而对于策略改进,我们依旧采用选择贪婪动作的改进方法,但是在同轨策略下,为了增加试探概率,我们需要采用\epsilon-软性策略,该策略说明可参笔者上篇文章强化学习丨蒙特卡洛方法及关于“二十一点”游戏的编程仿真的3.2.1部分。

        另外,为了防止策略稳定时智能体还在以\epsilon的概率盲目试探,不妨使得该探测概率随训练幕数的增长而递减,由此得到Sarsa算法流程如下:

Sarsa算法

Step1: 输入增量步长\alpha \in (0,1]

Step2: 初始化状态-动作价值函数Q({S,A})为零向量

Step3: 对每幕循环:

                \epsilon = 1/num_{ep}num_{ep}为当前共训练的幕数

                初始化S

                使用\epsilon-贪心策略在S出选择A

                生成幕的同时进行价值更新:

                        执行A,观测到R,S^{'}

                        使用\epsilon-贪心策略在S^{'}出选择A^{'}

                         Q(S,A)=Q(S,A)+\alpha[R+\gamma Q(S^{*},A)-Q(S,A)]

                        S=S^{'},A=A^{'}

                        若S为终止状态则终止本幕,开始访问下一幕

3.2 算法应用:悬崖行走问题(Cliff Walking)

悬崖行走问题(Cliff Walking)

        存在如下图的一个网格世界,包含起点和目标状态,在每个方格处可执行上下左右这些常见的动作,掉下悬崖会(Cliff)得到-100的收益并把智能体送回起点,其它的转移得到的收益都是-1。

强化学习丨时序差分算法TD(0)及相关编程仿真_第4张图片

         现寻求一条最短路径使得智能体能安全到达终点。

         基于上述Sarsa算法流程,不难对该问题进行编程求解。首先载入需要用到的库:

# Algorithm: Temporal Difference——Sarsa
# Project  : Cliff Walking
# Author   : XD_MaoHai
# Reference: Stan Fu
# Date     : 2021/11/19

import sys
import gym
import numpy as np
import random
from collections import defaultdict, deque
import matplotlib.pyplot as plt

        编写更新动作价值的函数(该函数是为了使程序结构更为清晰有条理,当然也可以直接在Sarsa迭代过程中直接进行价值更新):

# 更新状态-动作价值表
def update_Q(alpha, gamma, Q, state, action, reward, next_state=None, next_action=None):
    """
    alpha: 增量步长
    gamma: 折扣因子
    Q: 状态-动作价值表
    state: 当前状态
    action:当前动作
    reward:回报
    next_state:下个状态
    next_action:下个动作
    return:返回更新的Q
    """
    # 当前状态-动作价值
    current_Q = Q[state][action]
    # 下个时刻状态-动作价值
    next_Q = Q[next_state][next_action] if next_state is not None else 0
    # 学习对象
    target = reward + gamma * next_Q
    # 更新
    new_Q = current_Q + alpha * (target - current_Q)
    return new_Q

         编写 \epsilon-贪心策略函数:

# ε-贪心策略
def epsilon_greedy(Q, state, nA, epsilon):
    """
    Q: 状态-动作价值表
    state: 当前状态
    nA:动作数
    epsilon:探索概率
    return:返回选择的动作
    """
    # 如果随机数大于epsilon则选择贪心动作
    if random.random() > epsilon:
        return np.argmax(Q[state])
    else:
        return random.choice(np.arange(nA))

        编写 Sarsa算法的函数:

# Sarsa算法
def sarsa(env, num_episodes, alpha, gamma=1.0):
    """
    env: 游戏环境
    num_episodes:总的训练幕数
    alpha: 增量步长
    gamma: 折扣因子
    return:预测的状态价值
    """
    # 环境总的可选动作数
    nA = env.action_space.n
    # 初始化状态-动作价值表
    Q = defaultdict(lambda: np.zeros(nA))
    # 建立关于单幕总收益的双向队列
    tmp_scores = deque()
    # 建立关于平均收益的双向队列
    avg_scores = deque(maxlen=num_episodes)
    # 开始训练迭代
    for each_episode in range(1,num_episodes+1):
        # 进度显示
        print("Episode {}/{}".format(each_episode, num_episodes), end="\r")
        sys.stdout.flush()
        # 初始化单幕总收益
        scores = 0
        # 环境重置
        state = env.reset()
        # 设置逐渐递减的epsilon
        epsilon = 1.0 / each_episode
        # 选择动作
        action = epsilon_greedy(Q, state, nA, epsilon)
        # 单幕训练
        while True:
            # 执行动作
            next_state, reward, done, info = env.step(action)
            # 更新单幕收益和
            scores += reward
            # 如果幕结束则更新Q后记录本幕收益和
            if done:
                Q[state][action] = update_Q(alpha, gamma, Q, state, action, reward)
                tmp_scores.append(scores)
                break
            # 更新动作、状态和Q
            next_action = epsilon_greedy(Q, next_state, nA, epsilon)
            Q[state][action] = update_Q(alpha, gamma, Q, state, action, reward, next_state, next_action)
            state = next_state
            action = next_action
        # 更新平均收益和
        avg_scores.append(np.mean(tmp_scores))
    # 绘图
    plt.plot(np.asarray(avg_scores))
    plt.xlabel('Episode')
    plt.ylabel('Average Reward Sum')
    plt.show()
    return Q

        最后在主函数中选择gym库中的CliffWalking-v0环境,并进行Sarsa函数调用以及绘图:

# 主函数
if __name__ == '__main__':
    # 载入环境
    env = gym.make('CliffWalking-v0')
    # 输出动作空间和观测空间
    print("Action space:{}".format(env.action_space.n))
    print("Observation space:{}".format(env.observation_space.n))
    # 训练智能体得到最优动作价值函数
    Q_opt = sarsa(env, 20000, .01)
    print(Q_opt.items())
    # 输出最优策略
    policy_opt = np.array([np.argmax(Q_opt[key]) if key in Q_opt else -1 for key in np.arange(48)]).reshape(4, 12)
    print("\nOptimal Policy (UP = 0, RIGHT = 1, DOWN = 2, LEFT = 3, No Action = -1):")
    print(policy_opt)
    # 绘制估计的最优状态价值函数
    V_opt = ([np.max(Q_opt[key]) if key in Q_opt else 0 for key in np.arange(48)])
    plot_values(V_opt)

        其中绘制状态价值函数图的函数plot_values的代码如下:

# Author   : Stan Fu
# 绘制状态价值图形函数
def plot_values(V):
    # 重装V
    V = np.reshape(V, (4, 12))
    # 绘图
    fig = plt.figure(figsize=(15, 5))
    ax = fig.add_subplot(111)
    im = ax.imshow(V, cmap='cool')
    for (j, i), label in np.ndenumerate(V):
        ax.text(i, j, np.round(label, 3), ha='center', va='center', fontsize=14)
    plt.tick_params(bottom='off', left='off', labelbottom='off', labelleft='off')
    plt.title('Optimal State Value Function')
    plt.show()

         运行程序,可得到训练20000幕后最优状态价值函数如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第5张图片

        其中黑线就是最后学习的最优路径,这与我们的直观认知一致。同时还得到随着幕数的增长,每幕的平均收益和曲线如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第6张图片

         可见,每幕的平均收益和得到了较快的收敛。

四、Q学习:离轨策略下的时序差分控制算法

4.1 算法介绍

        现在考虑另一种算法,仍用\epsilon-贪心策略生成幕序列(行为策略),但利用绝对贪心动作价值含函数进行价值更新,如下:

Q(S_{t},A_{t})=Q(S_{t},A_{t})+\alpha[R_{t+1}+\gamma \underset{a}{max} Q(S_{t+1},a)-Q(S_{t},A_{t})]

        对比上式和Sarsa算法价值更新公式可知,上式没有选择与行为策略,也即\epsilon-贪心策略产生的相同的后继状态-动作价值进行更新,而是直接选择了贪心动作,并将之视为学习目标,而与行动策略无关,因此这种更新方式是一种离轨策略,这种方法又叫做Q学习(Q Learning)

        有了价值更新公式,这里仿照Sarsa算法可以得到Q学习的算法流程图如下:

Q-Learning算法

Step1: 输入增量步长\alpha \in (0,1]

Step2: 初始化状态-动作价值函数Q({S,A})为零向量

Step3: 对每幕循环:

                \epsilon = 1/num_{ep}num_{ep}为当前共训练的幕数

                初始化S

                生成幕的同时进行价值更新:

                        使用\epsilon-贪心策略在S出选择A

                        执行A,观测到R,S^{'}

                         ​​​​​​​Q(S,A)=Q(S,A)+\alpha[R+\gamma \underset{a}{max} Q(S^{*},a)-Q(S,A)]

                        S=S^{'}

                        若S为终止状态则终止本幕,开始访问下一幕

4.2 算法应用

        这里依然用悬崖游走的问题对Q-Learning算法进行实例分析,依据上述算法流程,首先对价值更新函数进行更改如下:

# 下个时刻状态-动作价值
    next_Q = np.max(Q[next_state]) if next_state is not None else 0

        其次将sarsa()函数替换为Q_learning()函数:

# Q_learning算法
def Q_learning(env, num_episodes, alpha, gamma=1.0):
    """
    env: 游戏环境
    num_episodes:总的训练幕数
    alpha: 增量步长
    gamma: 折扣因子
    return:返回最优状态价值函数
    """
    # 环境总的可选动作数
    nA = env.action_space.n
    # 初始化状态-动作价值表
    Q = defaultdict(lambda: np.zeros(nA))
    # 建立关于单幕总收益的双向队列
    tmp_scores = deque()
    # 建立关于平均收益的双向队列
    avg_scores = deque(maxlen=num_episodes)
    # 开始训练迭代
    for each_episode in range(1, num_episodes+1):
        # 进度显示
        print("Episode {}/{}".format(each_episode, num_episodes), end="\r")
        sys.stdout.flush()
        # 初始化单幕总收益
        scores = 0
        # 环境重置
        state = env.reset()
        # 设置逐渐递减的epsilon
        epsilon = 1.0 / each_episode
        # 单幕训练
        while True:
            # 选择动作
            action = epsilon_greedy(Q, state, nA, epsilon)
            # 执行动作
            next_state, reward, done, info = env.step(action)
            # 更新单幕收益和
            scores += reward
            # 如果幕结束则更新Q后记录本幕收益和
            if done:
                Q[state][action] = update_Q(alpha, gamma, Q, state, action, reward)
                tmp_scores.append(scores)
                break
            # 更新状态和Q
            Q[state][action] = update_Q(alpha, gamma, Q, state, action, reward, next_state)
            state = next_state
        # 更新平均收益和
        avg_scores.append(np.mean(tmp_scores))
    # 绘图
    plt.plot(np.asarray(avg_scores))
    plt.xlabel('Episode')
    plt.ylabel('Average Reward Sum')
    plt.show()
    return Q

         最后在主函数中调用该函数并绘图即可,运行程序,可得到训练20000幕后最优状态价值函数如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第7张图片

         同时还得到随着幕数的增长,每幕的平均收益和曲线如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第8张图片

        可见上述结果均与Sarsa算法结果基本一致。 

五、期望Sarsa

5.1 算法介绍

        上文已经介绍,Sarsa算法是利用 \epsilon-贪心策略产生的动作价值进行价值更新,而Q-Learning算法则是利用绝对贪心动作进行价值更新,现在我们将两者融合,即利用 \epsilon-贪心策略下次时刻动作价值的期望做为学习目标进行价值更新,公式如下:

\begin{aligned} Q(S_{t},A_{t})&=Q(S_{t},A_{t})+\alpha[R_{t+1}+\gamma E[Q(S_{t+1},A_{t+1})|S_{t+1}] -Q(S_{t},A_{t})]\\ &=Q(S_{t},A_{t})+\alpha[R_{t+1}+\gamma \sum_{a}\pi(a|S_{t+1})Q(S_{t+1},a) -Q(S_{t},A_{t})] \end{aligned}

        上述公式虽然计算复杂,但是利用求期望的方式消除了因为随机选择A_{t+1} 所带来的方差,因此这种方法又叫做期望Sarsa,可以看出当上式中策略\pi为绝对贪心策略时,期望Sarsa就退化成了Q学习方法。仿照上述算法可列出该算法流程如下:

期望Sarsa算法

Step1: 输入增量步长\alpha \in (0,1]与探索概率\epsilon \in (0,1]

Step2: 初始化状态-动作价值函数Q({S,A})为零向量

Step3: 对每幕循环:

                初始化S

                生成幕的同时进行价值更新:

                        使用\epsilon-贪心策略在S出选择A

                        执行A,观测到R,S^{'}

                     Q(S,A)=Q(S,A)+\alpha[R+\gamma \sum_{a}\pi(a|S^{*})Q(S^{*},a) -Q(S,A)]

                        S=S^{'}

                        若S为终止状态则终止本幕,开始访问下一幕

5.2 算法应用

        依然用悬崖游走问题对期望Sarsa算法做实例分析,首先更改价值更新函数:

# 更新状态-动作价值表
def update_Q(alpha, gamma, Q, state, action, reward, next_state=None):
    """
    alpha: 增量步长
    gamma: 折扣因子
    Q: 状态-动作价值表
    state: 当前状态
    action:当前动作
    reward:回报
    next_state:下个状态
    next_action:下个动作
    return:返回更新的Q
    """
    # 当前状态-动作价值
    current_Q = Q[state][action]
    # 下个时刻状态-动作价值
    next_Q = np.max(Q[next_state]) if next_state is not None else 0
    # 学习对象
    target = reward + gamma * next_Q
    # 更新
    new_Q = current_Q + alpha * (target - current_Q)
    return new_Q

        其次将Q_learning()函数替换为expected_sarsa()函数:

# 期望Sarsa算法
def expected_sarsa(env, num_episodes, alpha, gamma=1.0):
    """
    env: 游戏环境
    num_episodes:总的训练幕数
    alpha: 增量步长
    gamma: 折扣因子
    return:返回最优状态价值函数
    """
    # 环境总的可选动作数
    nA = env.action_space.n
    # 初始化状态-动作价值表
    Q = defaultdict(lambda: np.zeros(nA))
    # 建立关于单幕总收益的双向队列
    tmp_scores = deque()
    # 建立关于平均收益的双向队列
    avg_scores = deque(maxlen=num_episodes)
    # 开始训练迭代
    for each_episode in range(1, num_episodes+1):
        # 进度显示
        print("Episode {}/{}".format(each_episode, num_episodes), end="\r")
        sys.stdout.flush()
        # 初始化单幕总收益
        scores = 0
        # 环境重置
        state = env.reset()
        # 设置epsilon
        epsilon = 0.005
        # 单幕训练
        while True:
            # 选择动作
            action = epsilon_greedy(Q, state, nA, epsilon)
            # 执行动作
            next_state, reward, done, info = env.step(action)
            # 更新单幕收益和
            scores += reward
            # 更新Q
            Q[state][action] = update_Q(alpha, gamma, nA, epsilon, Q, state, action, reward, next_state)
            # 如果幕结束则更新Q后记录本幕收益和
            if done:
                tmp_scores.append(scores)
                break
            # 更新状态
            state = next_state
        # 更新平均收益和
        avg_scores.append(np.mean(tmp_scores))
    # 绘图
    plt.plot(np.asarray(avg_scores))
    plt.xlabel('Episode')
    plt.ylabel('Average Reward Sum')
    plt.show()
    return Q

          最后在主函数中调用该函数并绘图即可,运行程序,可得到训练20000幕后最优状态价值函数如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第9张图片

         同时还得到随着幕数的增长,每幕的平均收益和曲线如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第10张图片

         所得图像也均与上述算法结果一致。

5.3 三种控制算法之间的比较

        上述三种算法最重要的区别就是价值更新公式的不同,为比较三种算法的性能,此处引用Sutton书中的一个比较图如下:

 强化学习丨时序差分算法TD(0)及相关编程仿真_第11张图片

        这是悬崖行走问题三种方法的长期性能和短期性能曲线,上图中横轴为增量步长\alpha,纵轴是每幕的平均收益和。可以看出, 期望Sarsa保持了Sarsa优于Q学习的显著优势,同时在短期和长期表现中期望Sarsa也均显著胜于Sarsa。而对于Sarsa算法而言,其仅在\alpha较小时能够有良好的长期表现。

六、最大化偏差与双学习

6.1 最大化偏差(Maximization Bias

        注意到我们在上面讲述的控制算法中,都包含了最大化的操作,也即是贪心算法,也就是根据所有动作价值的最大值来进行价值更新,然而,当这些动作的回报服从一定的分布或是单纯的噪声时,这种贪心的做法往往会使得价值估计变大,由此产生的一个正偏差叫做最大化偏差。我们用下面一个经典的例子对其进行解释:

最大化偏差的例子

        考虑下面一个MDP过程:

强化学习丨时序差分算法TD(0)及相关编程仿真_第12张图片

         这个MDP有两个非终止节点A和B,每幕从A开始,每步随机选择向左向右的动作,直至到达两侧方框表示的终止状态。由B达到左侧终止状态对应有N个动作,同时每次都会得到一个服从均值为-0.1、方差为1.0的正态分布的收益,其余状态转移收益为0,求状态转移的最优路径。

         这是一个非常简单而直观的问题,由于由B达到左侧终止状态,各动作收益的均值为-0.1且同分布,因此最优路径一定会是由A向右达到右侧终止状态。但是利用Q学习算法得到的智能体选择由A向左移动的概率曲线如下:

强化学习丨时序差分算法TD(0)及相关编程仿真_第13张图片

        上图中黄线表示Q学习算法策略稳定下智能体选择由A向左移动的概率,该值为0.05是使用\epsilon=0.1\epsilon-贪心策略选择动作而引起的最低向左运动的概率(\frac{\epsilon}{|A|}=\frac{\epsilon}{2}=0.05)。

        从上图可以看到,虽然Q学习算法学习曲线最后慢慢趋近最优学习曲线,但是在开始时,也即训练幕数较低时,由于由B达到左侧终止状态的N个动作价值的样本量不够,其价值还得不到收敛,而此时价值更新选择的是N个动作中此刻价值最大的价值函数,因此会使得对A向左移动的价值函数估计偏高,从而选择左移动的概率变大,这就是最大化偏差带来的影响。

        但当样本数量慢慢增多时,由B达到左侧终止状态的N个动作价值得到收敛,从A向左移动的价值函数也会慢慢收敛,选择左移动的概率慢慢变小。

6.2 双学习(Double Learning)

        为了解决开始时样本数量过少且选择贪心算法带来的最大化偏差问题,不妨引入两个动作价值函数表Q_{1}Q_{2},每步价值更新依然选择贪心动作,但是等概率的从其中一个Q表选择贪心动作:

A^{*}=\underset{a}{argmax}\ Q(S_{t+1},a)

        而从另外一个表中选择该动作对应的Q值进行价值更新:

Q(S_{t},A_{t})=Q(S_{t},A_{t})+\alpha[R_{t+1}+\gamma Q_{the\_other}(S_{t+1},A^{*})-Q(S_{t},A_{t})]

        这样一来就消除了变量分布产生的随机性所带来了估计误差,这种方法就叫做双学习,对于Q学习的双学习算法叫做双Q学习算法,依据上式可得其算法流程如下:

双Q学习算法

Step1: 输入增量步长\alpha \in (0,1]与探索概率\epsilon \in (0,1]

Step2: 初始化Q_{1}(S,A)Q_{2}(S,A)为零向量 

Step3: 对每幕循环:

                初始化S

                生成幕的同时进行价值更新:

                        基于Q_{1}+Q_{2}之和,使用 \epsilon-贪心策略在S出选择A

                        执行A,观测到R,S^{'}

                        以0.5的概率执行:

                          Q_{1}(S,A)=Q_{1}(S,A)+\alpha[R+\gamma Q_{2}(S^{'},\underset{a}{argmax}Q_{1}(S^{'},a))-Q_{1}(S,A)]

                        或者执行:

                         Q_{2}(S,A)=Q_{2}(S,A)+\alpha[R+\gamma Q_{1}(S^{'},\underset{a}{argmax}Q_{2}(S^{'},a))-Q_{2}(S,A)]

                           S=S^{'}

                        若S为终止状态则终止本幕,开始访问下一幕

        利用该算法对上面的那个MDP过程进行求解,并与Q学习算法学习曲线对比得:

强化学习丨时序差分算法TD(0)及相关编程仿真_第14张图片

         上述算法的代码可参下面链接,这里由于本文篇幅有限就不再贴了:

阿亮算法 ​:解剖[强化学习]Double Learning本质并对比Q-Learning与期望Sarsaicon-default.png?t=LA92https://zhuanlan.zhihu.com/p/269870048        通过上图可以看出,双Q学习并没有受到最大化偏差的影响,从训练开始就一致趋近并最终收敛于最优学习曲线,可见该算法在此类问题中的优越性。

参考文献与网址

[1] Sutton R , Barto A . Reinforcement Learning:An Introduction Second Edition

[2] 强化学习基础篇(十九)TD与MC在随机游走问题应用

[3] 强化学习之时序差分学习

[4] 强化学习(五) - 时序差分学习(Temporal-Difference Learning)及其实例----Sarsa算法, Q学习, 期望Sarsa算法

[5] 解剖[强化学习]Double Learning本质并对比Q-Learning与期望Sarsa

你可能感兴趣的:(强化学习,算法,python,强化学习,机器学习)