强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践

目录

一、算法介绍

二、n步时序差分预测

2.1 算法介绍

2.2 算法应用

三、n步Sarsa

3.1 算法介绍

3.2 算法应用

四、n步离轨策略学习

4.1 算法介绍

4.2 带控制变量的每次决策型方法

4.3 n步树回溯

4.3.1 算法介绍

4.3.2 算法应用

五、总结


一、算法介绍

        上篇文章对(单步)时序差分算法做了介绍:

强化学习丨时序差分算法TD(0)及相关编程仿真icon-default.png?t=LA92https://blog.csdn.net/qq_56937808/article/details/121439265?spm=1001.2014.3001.5501       

        现在我们已经知道,强化学习时序差分算法TD(0)综合了动态规划算法(DP)与蒙特卡洛方法(MC),解决了无模型先验知识条件下的MDP问题。

        现在我们将思路打开,不妨将价值的预测估计进行泛化,即我们无需像MC算法一样只有观测完整个幕序列才对价值进行整体更新,也无需像TD(0)那样仅仅观测一步过程就利用下个时刻的状态价值来估计后续幕回报,而是任意定义观测步n的值,记录这n步的幕过程(S_{t}[,A_{t}],R_{t+1}),再利用n步后的状态价值来估计剩余幕回报,这就是n步时序差分算法(n-step Bootstrapping)

二、n步时序差分预测

2.1 算法介绍

        我们还是先来关注预测算法,即对一定策略下状态价值的估计。如前所述,无论是DP、MC、TD(0)还是n步时序差分算法,其差别就根于我们要用什么来计算回报,从而利用回报来更新价值。DP是利用下一步的收益期望与状态价值期望,MC是观测完整个序列得到真实回报,TD(0)是利用单步收益与后继状态价值来估计,n步时序差分算法则是利用n步收益值与n步后的状态价值来估计,也即有:

G_{t:t+n}=R_{t+1}+\gamma R_{t+2}+\cdots +\gamma^{n-1}R_{t+n}+\gamma^{n}V_{t+n-1}(S_{t+n})

        其中V_{t+n-1}的下标表示该时刻的状态价值,可以看到状态价值随着幕过程的推进而不断更新,得到回报后即可利用下式进行价值更新:

V_{t+n}(S_{t})=V_{t+n-1}(S_{t})+\alpha[G_{t:t+n}-V_{t+n-1}(S_{t})],0\leq t<T

        其中T表示幕终止时刻,\alpha表示增量步长(对该参数不明白的可参文章开头提到的那篇文章)。由于需要存储n步过程值,因此我们不妨利用一个n+1大的数组存储过程状态值S_{t}\cdots S_{t+n}(也可存储状态-动作二元组),利用一个n大的数组存储过程收益值R_{t+1}\cdots R_{t+n},这样一来我们就无需记录整个幕序列,而通过对其数组的大小(n或者n+1)进行取余进行存取操作,从而得到如下算法流程:

n步时序差分预测

Step1:输入一个策略\pi

Step2:输入一个增量步长\alpha \in (0,1],观测步长n

Step3:对任意的s \in S,任意初始化V(S)

Step4:遍历每幕:

                存储一个非终止状态的初始状态S_{0}

                赋予T一个极大值,如+\infty

                循环t=0,1,2,\cdots:

                        如果t<T,那么:

                                依据\pi(\cdot | S_{t})采取策略

                                观察和存储下一时刻的收益R_{t+1}和状态S_{t+1}

                                若S_{t+1}是终止状态,则T = t+1

                        \tau = t-n+1(当前更新时刻)

                        如果\tau \geq 0(即已经度过开始时无法更新的n步):

                                G = \sum_{\tau+1}^{min(\tau+n,T)}\gamma^{i-\tau-1}R_{i}

                                如果\tau+n<T(即采样还没到终止时刻),那么:

                                        G=G+\gamma^{n}V(S_{\tau+n})

                                V(S_{\tau}) = V(S_{\tau}) + \alpha[G-V(S_{\tau})]

                                如果\tau=T-1(即更新时刻达到终止时刻)

                                        结束本幕训练

        通过上述流程可以看到, 状态的价值更新是随着幕序列采样同步进行的,其中\tau表示我们需要状态更新的\tau时刻,t则表示采样时刻,采样与更新的过程可由下图表示:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第1张图片

        上图右下角两个箭头表示更新时刻对应的采样时刻。

        通过上述流程可见,TD(0)与MC算法分别是n步时序差分算法的两种极端,当n为1时即为TD(0),当n值取一个较大值(大于幕长度的最大值)时即为MC。并且已经被证明,n步回报的期望的最坏误差能够保证不大于直接用V_{t+n-1}来估计状态价值的最坏误差的\gamma^{n}倍:

\underset{a}{max}|E_{\pi}[G_{t:t+n}|S_{t}=s]-v_{\pi}(s)| \leq \gamma^{n} \underset{s}{max}|V_{t+n-1}(s)-v_{\pi}(s)|

         这就是n步回报的误差减少性质

2.2 算法应用

        现对上文提到的随机游走问题进行拓展,并对n步TD方法进行编程实践:

随机行走的n步时序差分方法

        在上文《强化学习|时序差分算法TD(0)及相关编程仿真》中提到一个仅有5个中间状态的随机游走问题,保留两侧的终止状态,将中间状态拓展至19个,向左达到终止状态的收益为-1,向右到达终止状态的收益为+1,其余状态转移收益为0。现比较不同n值下n步时序差分的性能。

        在比较不同n值下n步时序差分的性能之前,我们不妨先来看下一定n值下,状态价值随训练幕的变化,首先导入所需要的库:

# Algorithm: n-step Bootstrapping——Policy Evaluation
# Project  :Random Walking
# Author   : XD_MaoHai
# Reference: Jabes
# Date     : 2021/12/11

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from tqdm import tqdm

        然后设置常量参数:

# 状态数
N_STATE = 19
# 折扣因子
GAMMA = 1
# 起始状态为第10个状态
START_STATE = 10
# 左右两侧存在终止状态
END_STATE = [0, N_STATE + 1]
# 各状态真实价值
TRUE_VALUE = np.arange(-20, 22, 2)/20.0
# 左右终止状态均价值均为0
TRUE_VALUE[0] = TRUE_VALUE[-1] = 0

         之后即编写n步时序差分预测算法的代码:

# n步时序差分预测算法
def n_step_prediction(value, n, alpha):
    """
    value : 状态价值向量
    n     : 观测步数
    alpha : 增量步长(遗忘因子)
    """
    # 初始化状态
    state = START_STATE
    # 初始化存储状态轨迹列表
    state_trajectory = np.zeros(n+1)
    # 初始化收益值的轨迹列表
    reward_trajectory = np.zeros(n)
    # 先将初始状态存储进状态轨迹列表中
    state_trajectory[0] = state
    # 初始化采样时间
    t = 0
    T = float('inf')
    while True:
        # 若非幕终止则继续动作
        if t < T:
            # 随机采取动作
            if np.random.binomial(1, 0.5) == 1:
                state = state + 1
            else:
                state = state - 1
            # 存储新状态
            state_trajectory[np.mod(t+1, n+1)] = state
            # 存储收益值
            if state in END_STATE:
                T = t + 1
                if state == 0:
                    reward_trajectory[np.mod(t, n)] = -1
                else:
                    reward_trajectory[np.mod(t, n)] = 1
            else:
                reward_trajectory[np.mod(t, n)] = 0
        # 定义更新时刻
        tau = t - n + 1
        # 最开始的n-1个时刻后开始更新,
        if tau >= 0:
            # 记录n步观测的回报
            returns = 0.0
            for i in range(tau+1, min(tau+n, T)+1):
                returns += np.power(GAMMA, i-tau-1) * reward_trajectory[np.mod(i-1, n)]
            if tau+n < T:
                returns += np.power(GAMMA, n) * value[state]
            # 加上n步时刻的状态价值
            value[int(state_trajectory[np.mod(tau, n+1)])] += alpha * (returns - value[int(state_trajectory[np.mod(tau, n+1)])])
            # 若到达终止状态则结束该幕
            if tau == T - 1:
                break
        t += 1
    return T

        最后在主程序里调用并绘图:

# 主函数
if __name__ == '__main__':
    # 观测步数
    n = 32
    # 增量步长
    alpha = 0.1
    # 初始化状态价值函数向量
    value = np.zeros(N_STATE + 2)
    # 在以下幕次序画图
    episodes = [0, 1, 10, 100]
    # 开始训练
    for run in tqdm(range(0, episodes[-1]+1)):
        if run in episodes:
            plt.plot(value, label=str(run) + ' episodes')
        n_step_prediction(value, n, alpha)
    plt.plot(TRUE_VALUE, label='True values')
    plt.xlabel('State')
    plt.ylabel('Values')
    plt.legend()

         得到状态价值随训练幕的变化图如下:

         可见随着训练幕数的增加,预测结果愈加接近真实值。

        此处我们利用均方根误差(Root Mean Squared Error,RMSE)来反映算法性能,其他代码不变,只需更改主函数即可:

# 主函数
if __name__ == '__main__':
    # 记录一下平均幕步长与最大最短长度
    cnt = 0
    episode_len_av = 0
    episode_len_min = 10000
    episode_len_max = 0
    # 选择2的整数次幂作为观测步长
    n_list = np.power(2, np.arange(0, 10))
    # 增量步长
    alphas = np.arange(0, 1.1, 0.1)
    # 每次实验的训练幕数
    episodes = 10
    # 实验次数
    runs = 100
    # 初始化均方根误差
    errors = np.zeros((len(n_list), len(alphas)))
    # 开始训练
    for n in tqdm(range(0, 10)):
        for alpha_ind, alpha in enumerate(alphas):
            for run in range(0, runs):
                # 每次实验重置状态价值向量
                value = np.zeros(N_STATE+2)
                # value = 0.5 * np.ones(N_STATE+2)
                # value[0] = 0
                # value[-1] = 0
                for ep in range(0, episodes):
                    episode_len = n_step_prediction(value, np.power(2, n), alpha)
                    cnt += 1
                    episode_len_av += 1 / cnt * (episode_len - episode_len_av)
                    episode_len_min = np.min([episode_len_min, episode_len])
                    episode_len_max = np.max([episode_len_max, episode_len])
                # 单次训练均方根误差
                error = np.sqrt(np.sum(np.power(value - TRUE_VALUE, 2)) / N_STATE)
                # 更新训练均方根误差
                errors[n, alpha_ind] += 1/(run+1) * (error - errors[n, alpha_ind])
    # 输出平均幕长度与最大最小长度
    print("The average length of episodes is {}".format(episode_len_av))
    print("The minimum length of episodes is {}".format(episode_len_min))
    print("The maximum length of episodes is {}".format(episode_len_max))
    # 绘图
    for i in range(0, len(n_list)):
        plt.plot(alphas, errors[i, :], label='n = %d' % (n_list[i]))
    plt.xlabel('alpha')
    plt.ylabel('RMSE')
    # plt.ylim([0.25, 0.55])
    plt.legend()

        可以看到,在上述代码中我还添加了记录幕最大、最小以及平均长度的代码,而且利用最开始的10幕,并重复100次求平均(增量式求平均)的方法来计算RMSE。运行程序,得到不同n值下,RMSE随\alpha的变化如下:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第2张图片 

         从图中可以看出,n取中间大小的值时效果最好,这也印证了n步时序差分算法较TD(0)与MC算法的优越性。

三、n步Sarsa

3.1 算法介绍

        在没有环境先验知识的情况下,我们就需要状态-动作二元组来学习到最优策略,相较于上面对状态价值的预测公式,对状态-动作二元组价值的预测的回报估计公式为:

G_{t:t+n}=R_{t+1}+\gamma R_{t+2}+\cdots +\gamma^{n-1}R_{t+n}+\gamma^{n}Q_{t+n-1}(S_{t+n},A_{t+n})

        价值更新公式为:

 Q_{t+n}(S_{t},A_{t})=Q_{t+n-1}(S_{t},A_{t})+\alpha[G_{t:t+n}-Q_{t+n-1}(S_{t},A_{t})],0\leq t< T

         有了价值更新算法后,我们仿照着上篇文章中TD(0)的Sarsa算法不难得到n步时序差分算法的Sarsa算法如下:

n步Sarsa

Step1:对于任意的s\in S,a\in A,任意初始化Q(s,a)

Step2:将\pi初始化为对应Q\epsilon-贪心策略

Step3:设置增量步长\alpha\in(0,1],较小的\epsilon以及观测步长n

Step4:遍历每幕:

                存储一个非终止状态的初始状态S_{0}

                根据\pi选择并存储动作A_{0}

                赋予T一个极大值,如+\infty

                循环t=0,1,2,\cdots:

                        如果t<T,那么:

                                依据\pi采取动作A_{t}

                                观察和存储下一时刻的收益R_{t+1}和状态S_{t+1}

                                若S_{t+1}是终止状态,则T = t+1

                                否则根据\pi选择并存储动作A_{t+1} 

                        \tau = t-n+1

                        如果\tau \geq 0:

                                G = \sum_{\tau+1}^{min(\tau+n,T)}\gamma^{i-\tau-1}R_{i}

                                如果\tau+n<T,那么:

                                        G=G+\gamma^{n}Q(S_{\tau+n},A_{\tau+n})

                                Q(S_{\tau},A_{\tau}) = Q(S_{\tau},A_{\tau}) + \alpha[G-Q(S_{\tau},A_{\tau})]

                                如果\tau=T-1

                                        结束本幕训练

        通过上述算法我们可以看到,n步时序差分算法综合了MC可以预先得知n步实际收益的优势与TD(0)算法符合MDP模型的最大似然参数估计的优点。

        上篇文章我们介绍了Sarsa算法中的期望Sarsa算法,也即令学习的对象为下次时刻动作价值的期望,对于n步Sarsa算法而言,需把回报估计变为:

G_{t:t+n}=R_{t+1}+\gamma R_{t+2}+\cdots +\gamma^{n-1}R_{t+n}+\gamma^{n}\sum_{a}\pi(a|s)Q_{t+n-1}(S_{t+n},A_{t+n})

        也即是对采样的后续状态-动作二元组的价值做以期望,进而利用上面的价值更新公式进行价值更新便得到了n步期望Sarsa。 

3.2 算法应用

        我们依旧利用上篇文章《强化学习丨时序差分算法TD(0)及相关编程仿真》中的悬崖游走问题来对n步Sarsa算法做编程实践。

        首先导入所用到的库:

# Algorithm: n-step Bootstrapping——Sarsa
# Project  :Random Walking
# Author   : XD_MaoHai
# Date     : 2021/12/11

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

        其次编写\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))

         然后编写n步Sarsa算法的代码:

# Sarsa算法
def n_step_sarsa(env, num_episodes, n, alpha, gamma=1.0):
    """
    env: 游戏环境
    num_episodes:总的训练幕数
    alpha: 增量步长
    gamma: 折扣因子
    return:预测的状态价值
    """
    # 初始化存储状态-动作轨迹列表
    state_action_trajectory = np.zeros((n+1, 2), dtype=int)
    # 初始化收益值的轨迹列表
    reward_trajectory = np.zeros(n)
    # 环境总的可选动作数
    nA = env.action_space.n
    # 初始化状态-动作价值表
    Q = defaultdict(lambda: np.zeros(nA))
    # 开始训练迭代
    for each_episode in range(1, num_episodes + 1):
        # 进度显示
        print("Episode {}/{}".format(each_episode, num_episodes), end="\r")
        sys.stdout.flush()
        # 环境重置
        state = env.reset()
        # 设置逐渐递减的epsilon
        epsilon = 1 / each_episode
        # 选择动作
        action = epsilon_greedy(Q, state, nA, epsilon)
        # 记录初始时刻的状态-动作
        state_action_trajectory[0] = [state, action]
        # 初始化采样时间
        t = 0
        T = float('inf')
        while True:
            # 若非幕终止则继续动作
            if t < T:
                # 执行动作
                next_state, reward, done, info = env.step(action)
                reward_trajectory[np.mod(t, n)] = reward
                # 如果下个时刻幕终止则记录终止时刻与状态
                if done:
                    T = t + 1
                    next_action = action
                # 否则记录下状态-动作二元组
                else:
                    next_action = epsilon_greedy(Q, next_state, nA, epsilon)
                    action = next_action
                state_action_trajectory[np.mod(t + 1, n + 1)] = [next_state, next_action]
            # 定义更新时刻
            tau = t - n + 1
            # 最开始的n-1个时刻后开始更新,
            if tau >= 0:
                # 记录n步观测的回报
                returns = 0.0
                for i in range(tau + 1, min(tau + n, T) + 1):
                    returns += np.power(gamma, i - tau - 1) * reward_trajectory[np.mod(i - 1, n)]
                if tau + n < T:
                    # n步后的状态和动作
                    # state_t = state_action_trajectory[np.mod(tau + n, n + 1), 0]
                    # action_t = state_action_trajectory[np.mod(tau + n, n + 1), 1]
                    returns += np.power(gamma, n) * Q[next_state][next_action]
                # 当前的状态和动作
                state_tau = state_action_trajectory[np.mod(tau, n + 1), 0]
                action_tau = state_action_trajectory[np.mod(tau, n + 1), 1]
                # 价值更新
                Q[state_tau][action_tau] += alpha * (returns - Q[state_tau][action_tau])
            if tau == T - 1:
                break
            t += 1
    return Q

        然后编写绘制状态价值图形函数plot_values(V)

# 绘制状态价值图形函数
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()

        最后在主程序中进行调用算法函数并绘图即可:

# 主函数
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 = n_step_sarsa(env, 20000, 4, .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)

         我们选择增量步长\alpha为0.01,将探索因子\epsilon设置为随幕数呈反函数递减的变量,幕训练数为20000次,得到不同观测步长n下最优状态价值函数为: 

        n=1(TD(0)):

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第3张图片

        n=2:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第4张图片

        n=4:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第5张图片

        n=8:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第6张图片

        可见,无论n取值如何,在足够的训练幕数下我们总能通过得到的最优状态价值函数图找到最优路径,也即是沿着悬崖边走,但是通过上图我们也不难发现,达到稳定的最优状态价值好像与实际最优状态价值不一致,拿起始状态来讲,其最优价值应该为13,但是随着n值的增大该值也会不断增大, 并且,如果用状态-价值二元组价值Q{(s,a)}来确定最优路径,那么最优路径总是沿着最优状态价值为整数的方向移动,如-17.0 -> -16.0 -> -15.0->…->-1.0->0.0。

        造成这种现象的原因,笔者猜想,这可能是更新与采样的时刻相差较大所致。对于单步时序差分也即TD(0)而言,在幕序列中进行一步采样后即对现时刻的状态-动作价值进行更新,这有利于下一采样时刻基于Q进行贪心动作的选择。而当n值较大时,尽管除每幕前n时刻采样时无更新操作而其余时刻均同步更新时,不难想到,对于某一状态而言,我们选择最优动作最关心的是其临近的状态,当n值较大,对t时刻进行贪心采样时所参考的还是n时刻以前的Q值,不由自主的想到这种依据可能会有些滞后或者失真,这也许也是n步时序差分的缺点所在。

        又由于贪心策略只会通过Q来选择贪心动作,而不是通过采取动作后状态的价值与收益来选择动作,因为我们假设这里是没有先验环境知识的,就算该问题下动态函数一目了然,因此,在上述的这种采样滞后于更新较大的情况下,并且随着探索因子\epsilon逐渐变小,最优路径不断固化,我们得到最优路径的步长是15或是17甚至更大,但是对于一些环境知识未知的模型,我们相信n步时序差分算法的结果还是很好的,也可以看作在这个问题下,单步时序差分算法(n=1)结果最好。

        另外,对该问题有研究或是见地的朋友欢迎相互交流!

四、n步离轨策略学习

4.1 算法介绍

        上述n步Sarsa可看作是同轨策略,因为我们采样的行为策略目标策略均为\epsilon-贪心策略。为了继续增大试探力度,我们不妨将行为策略与目标策略独立开来,利用行为策略生成的样本对目标策略进行训练更新,这也得到了n步离轨策略学习方法

        离轨策略需要解决的一个关键问题就是利用重要度采样比\rho_{t:h}来对行为策略得到的收益值进行修正,关于该参数的解释笔者已经在下面链接的文章中详细的讲过了,不懂的朋友可自行查阅:

强化学习丨蒙特卡洛方法及关于“二十一点”游戏的编程仿真icon-default.png?t=LA92https://blog.csdn.net/qq_56937808/article/details/121136066?spm=1001.2014.3001.5501        如果了解了重要度采样比的概念,不难得出n步观测的收益值对应的重要度采样比为:

\rho_{t:t+n-1}=\prod_{k=t}^{min(t+n-1,T-1)}\frac{\pi(A_{k}|S_{k})}{b(A_{k}|S_{k})}

        利用该参数实现对状态价值更新的加权:

V_{t+n}(S_{t})=V_{t+n-1}(S_{t})+\alpha\rho_{t:t+n-1}[G_{t:t+n}-V_{t+n-1}(S_{t})],0\leq t< T 

        对于状态-动作二元组价值函数而言,其重要度采样比为\rho_{t+1:t+n},相较于状态价值函数的重要度采样比\rho_{t:t+n-1},其修正起始时刻为t+1而非t是因为目标是状态-动作,其初始时刻的动作选择可以看作是已经选定了,其修正终止时刻为t+n而非t+n-1是因为回报值的最后一项是利用状态-动作二元组的价值来代替了后续回报,相当于多进行了一步采样。

        由此得到状态-动作二元组价值函数更新公式如下:

Q_{t+n}(S_{t},A_{t})=Q_{t+n-1}(S_{t},A_{t})+\alpha\rho_{t+1:t+n}[G_{t:t+n}-Q_{t+n-1}(S_{t},A_{t})]

        价值更新公式得到后,不难得到n步离轨策略学习算法如下:

 n步离轨策略学习算法

Step1:对于任意的s\in S,a\in A,任意初始化Q(s,a)

Step2:将\pi初始化为对应Q\epsilon-贪心策略,将b设置为具有一定试探力度的策略

Step3:设置增量步长\alpha\in(0,1],较小的\epsilon以及观测步长n

Step4:遍历每幕:

                存储一个非终止状态的初始状态S_{0}

                根据b选择并存储动作A_{0}

                赋予T一个极大值,如+\infty

                循环t=0,1,2,\cdots:

                        如果t<T,那么:

                                依据b采取动作A_{t}

                                观察和存储下一时刻的收益R_{t+1}和状态S_{t+1}

                                若S_{t+1}是终止状态,则T = t+1

                                否则根据\pi选择并存储动作A_{t+1} 

                        \tau = t-n+1

                        如果\tau \geq 0:

                                \rho=\prod_{i=\tau+1}^{min(\tau+n-1,T-1)}\frac{\pi(A_{k}|S_{k})}{b(A_{k}|S_{k})}

                                G = \sum_{\tau+1}^{min(\tau+n,T)}\gamma^{i-\tau-1}R_{i}

                                如果\tau+n<T,那么:

                                        G=G+\gamma^{n}Q(S_{\tau+n},A_{\tau+n})

                                Q(S_{\tau},A_{\tau}) = Q(S_{\tau},A_{\tau}) + \alpha\rho[G-Q(S_{\tau},A_{\tau})]

                                如果\tau=T-1

                                        结束本幕训练

        n步离轨策略学习算法也具有相应的期望Sarsa算法,此时由于最后n步观测后是通过求期望的方式来进行后续回报的估计,因此只需将重要度采样比改为为\rho_{t+1:t+n-1}即可。

4.2 带控制变量的每次决策型方法

        在上面的离轨策略中,注意到我们利用重要度采样比修正的是更新值G-Q,而不是回报值G,这是因为当重要度采样比因子为0时,也即行为策略采样的样本没有参考价值时,我们希望原价值保持不变,而不是收到值为0的收益。由于重要度采样比同时修饰了Q,因此可能会带来方差增大的负面效果。

        其实为了防止无参考价值的序列对目标策略进行训练,我们可以用下式来代替真实回报值:

G_{t:h}=\rho_{t}(R_{t+1}+\gamma G_{t+1:h})+(1-\rho_{t})V_{h-1}(S_{t})

        其中\rho_{t}=\frac{\pi(A_{k},S_{k})}{b(A_{k},S_{k})} 表示单时刻的重要度采样比,等号右端第一项是每次决策型的回报公式,该方法一改利用\rho_{t:t+n-1}来整体粗糙修正回报值G_{t:t+n}的确定,而利用单步重要度采样比来精细修饰每步收益值,可以带来减小估计方差的好处。等号右端第二项为控制变量,该项存在的意义就在于当重要度采样比为0时保持原来价值不变。上式也可以看作是对原价值与回报值的加权求和。

        对于状态-动作二元组的回报估计公式为:

\begin{aligned} G_{t:h}&=R_{t+1}+\gamma(\rho_{t+1}G_{t+1:h}+\bar{V}_{h-1}(S_{t+1})-\rho_{t+1}Q_{h-1}(S_{t+1},A_{t+1})) \\ &=R_{t+1}+\gamma\rho_{t+1}(G_{t+1:h}-Q_{h-1}(S_{t+1},A_{t+1}))+\gamma\bar{V}_{h-1}(S_{t+1}) \end{aligned}

        其中\bar{V}_{h-1}(S_{t+1})表示状态的价值期望:

\bar{V}(S_{t}) = \sum_{a} \pi (a|S_{t})Q(a,S_{t})

         将上述n步时序差分控制算法中的回报值改为上式即可得到带控制变量的每次决策型算法。

4.3 n步树回溯

4.3.1 算法介绍

        上面我们有介绍n步时序差分的Sarsa与离轨策略算法的期望Sarsa算法,不难发现取期望的操作都只在采样后第n个时刻,现在我们对其进行扩展,也即是对每步更新都采用求期望的形式,这也就是我们要介绍的n步树回溯方法

        我们不妨将该算法用以下回溯图的形式表示:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第7张图片

        从根节点二元组出发进行随机采样,对每个状态或是状态-动作二元组价值的估计都是对叶子结点的加权,而权值就是目标策略下该叶子结点出现的概率,如果t+m时刻的一个叶子结点是采样得到的结果(也即是上图中连贯的序列),那么加权的对象就是此后的收益值G_{t+m:t+n},若不是则加权的对象就是现估计下的价值V或者Q

        此外,由于每次更新都是求期望的操作,我们就无需用重要度采样比来修饰回报值了。由此不难得到该方法下回报估计公式如下:

G_{t:t+n}=R_{t+1}+\gamma\sum_{a\neq A_{t+1}}\pi(a|S_{t+1})Q_{t+n-1}(S_{t+1},a)+\gamma\pi(A_{t+1}|S_{t+1})G_{t+1:t+n}

        则相应价值更新公式为:

Q_{t+n}(S_{t},A_{t})=Q_{t+n-1}(S_{t},A_{t})+\alpha[G_{t:t+n}-Q_{t+n-1}(S_{t},A_{t})]

        由此不难得到算法流程如下:

  n步树回溯

Step1:对于任意的s\in S,a\in A,任意初始化Q(s,a)

Step2:将\pi初始化为对应Q\epsilon-贪心策略

Step3:设置增量步长\alpha\in(0,1],较小的\epsilon以及观测步长n

Step4:遍历每幕:

                存储一个非终止状态的初始状态S_{0}

                随机选择并存储动作A_{0}

                赋予T一个极大值,如+\infty

                循环t=0,1,2,\cdots:

                        如果t<T,那么:

                                随机采取动作A_{t}

                                观察和存储下一时刻的收益R_{t+1}和状态S_{t+1}

                                若S_{t+1}是终止状态,则T = t+1

                                否则随机选择并存储动作A_{t+1} 

                        \tau = t-n+1

                        如果\tau \geq 0:

                                如果t+1\geq T:

                                        G=R_{T}

                                否则:

                                        G=R_{t+1}+\gamma \sum_{a}\pi(a|S_{t+1})Q(S_{t+1},a)

                                循环k=min(t,T-1)递减到\tau+1

                                        G=R_{k}+\gamma\sum_{a\neq A_{k}}\pi(a|S_{k})Q(S_{k},a)+\gamma\pi(A_{k}|S_{k})G

                                Q(S_{\tau},A_{\tau}) = Q(S_{\tau},A_{\tau}) + \alpha\rho[G-Q(S_{\tau},A_{\tau})]

                                如果\tau=T-1

                                        结束本幕训练

4.3.2 算法应用

        我们依旧用悬崖游走的问题对n步树回溯进行编程实践。在上面n步Sarsa代码的基础上对将n_step_sarsa()函数换为以下代码即可:

# n步树回溯算法
def tree_backup(env, num_episodes, n, alpha, gamma=1.0):
    """
    env: 游戏环境
    num_episodes:总的训练幕数
    alpha: 增量步长
    gamma: 折扣因子
    return:预测的状态价值
    """
    # 初始化存储状态-动作轨迹列表
    state_action_trajectory = np.zeros((n+1, 2), dtype=int)
    # 初始化收益值的轨迹列表
    reward_trajectory = np.zeros(n)
    # 环境总的可选动作数
    nA = env.action_space.n
    # 初始化状态-动作价值表
    Q = defaultdict(lambda: np.zeros(nA))
    # 开始训练迭代
    for each_episode in range(1, num_episodes + 1):
        # 进度显示
        print("Episode {}/{}".format(each_episode, num_episodes), end="\r")
        sys.stdout.flush()
        # 环境重置
        state = env.reset()
        # 设置逐渐递减的epsilon
        epsilon = 1 / each_episode
        # 随机选择动作
        action = np.random.randint(4)
        # 记录初始时刻的状态-动作
        state_action_trajectory[0] = [state, action]
        # 初始化采样时间
        t = 0
        T = float('inf')
        while True:
            # 若非幕终止则继续动作
            if t < T:
                # 执行动作
                next_state, reward, done, info = env.step(action)
                reward_trajectory[np.mod(t, n)] = reward
                # 如果下个时刻幕终止则记录终止时刻与状态
                if done:
                    T = t + 1
                    next_action = action
                # 否则记录下状态-动作二元组
                else:
                    next_action = np.random.randint(4)
                    action = next_action
                state_action_trajectory[np.mod(t + 1, n + 1)] = [next_state, next_action]
            # 定义更新时刻
            tau = t - n + 1
            # 最开始的n-1个时刻后开始更新,
            if tau >= 0:
                # 记录下一采样时刻的汇报估计值
                if t+1 >= T:
                    returns = reward_trajectory[np.mod(T-1, n)]
                else:
                    next_state_value = sum(greedy_pro(Q, next_state, act, nA, epsilon) * Q[next_state][act] for act in range(4))
                    returns = reward_trajectory[np.mod(t, n)] + gamma * next_state_value
                for k in range(min(t, T-1), tau+1, -1):
                    state_k = state_action_trajectory[np.mod(k, n + 1), 0]
                    action_k = state_action_trajectory[np.mod(k, n + 1), 1]
                    next_state_value_part = sum(greedy_pro(Q, state_k, act, nA, epsilon) * Q[state_k][act] if act!=action_k else 0 for act in range(4))
                    returns = reward_trajectory[np.mod(k-1, n)] + gamma * next_state_value_part + gamma * greedy_pro(Q, state_k, action_k, nA, epsilon) * returns
                state_tau = state_action_trajectory[np.mod(tau, n + 1), 0]
                action_tau = state_action_trajectory[np.mod(tau, n + 1), 1]
                Q[state_tau][action_tau] += alpha * (returns - Q[state_tau][action_tau])
            if tau == T - 1:
                break
            t += 1
    return Q

        其中计算概率的函数greedy_pro()的代码段如下:

# 计算动作在贪心策略下的概率
def greedy_pro(Q, state, action, nA, epsilon):
    if action == np.argmax(Q[state]):
        return 1 - epsilon + epsilon / nA
    else:
        return epsilon / nA

        运行代码得到增量步长\alpha为0.01,观测步长n为2,训练10000幕后最优状态价值函数图如下:

五、总结-n步Q(\sigma)

         为了综合前面几种算法,我们不妨将上述方法的4步时序差分回溯图表示如下:

强化学习丨n步时序差分算法(n-step Bootstrapping)及编程实践_第8张图片

         可以看到,Sarsa(或是离轨策略)每步都依据一定策略方法来选取动作,并利用回报值来更新价值,而树回溯方法则是随机采取动作,每步都利用期望的方式来更新价值,期望Sarsa则是仅在观测结束时进行期望操作。现在我们不妨将求期望的操作进行泛化,即引入一个参数\sigma来表示此步我们学习方式是不是求期望,当\sigma为1时表示求期望,\sigma为0时则否然,这就是n步Q(\sigma)算法

        现在给出该算法下的回报估计公式如下:

G_{t:h}=R_{t+1}+\gamma(\sigma_{t+1}\rho_{t+1}+(1-\sigma_{t+1})\pi(A_{t+1}|S_{t+1}))(G_{t+1:h}-Q_{h-1}(S_{t+1},A_{t+1}))+\gamma \bar{V}_{h-1}(S_{t+1})

        可以看出上式是n步离轨策略与n步树回溯的回报公式的加权,加权因子即为\sigma,这也很好的表示了n步Q(\sigma)算法是前述算法的融合。

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