三、时序差分方法Temporal-Difference

1 简介

不同于动态规划算法,无模型的强化学习算法不需要事先知道环境的奖励函数和状态转移函数,而是直接使用和环境交互的过程中采样到的数据来学习,这使得它可以被应用到一些简单的实际场景中。本章将要讲解无模型的强化学习中的两大经典算法:SarsaQ-learning,它们都是基于时序差分(temporal difference,TD) 的强化学习算法。同时,本章还会引入一组概念:在线策略学习(on-policy)和离线策略学习(off-policy)。

2 时序差分方法

蒙特卡洛方法必须要等整个序列(episode)结束之后才能计算得到这一次的回报,而时序差分方法只需要当前步结束即可进行计算。具体来说,时序差分算法用当前获得的奖励加上下一个状态的 state value 来作为在当前状态会获得的回报。即为一种增量式的 state value 更新方式:
image
其中,\(α\) 为步长/学习率。

3 Sarsa 算法

Sarsa 算法也属于时序差分算法,不过它有特殊的用途。
第2节中的算法只能够用于估计 state value,而这一节中的 Sarsa 算法则可以直接估计 action value,该方法也很重要因为它可以与上一章中策略迭代(policy iteraion)的策略提升(policy improvement)一步相结合,以得到最优策略。
image
然后我们用 greedy 算法来选取在某个状态下 action value 最大的那个动作,即。这样似乎已经形成了一个完整的强化学习算法:用 greedy 算法根据 action value 选取动作来和环境交互,再根据得到的数据用时序差分算法更新 action value 的估计。
但是如果在策略提升中一直根据 greedy 算法得到一个确定性策略,可能会导致某些状态动作对(state-action pair)永远没有在序列中出现,以至于无法对其 action value 进行估计,进而无法保证策略提升后的策略比之前的好。我们在第一章中对此有详细讨论。简单常用的解决方案是不再一味使用 greedy 算法,而是采用一个 \(ϵ\)-greedy 算法:有一定概率采用 action value 最大的那个动作,也有一定概率从动作空间中随机采取一个动作,其公式表示为:

\[π_{t+1}(a|s_t)= \begin{cases} 1-\frac{ϵ}{|\mathcal{A}(s_t)|}(|A(s_t)|-1),a=argmax_aq_{t+1}(s_t,a)\\ \frac{ϵ}{|\mathcal{A}(s_t)|},otherwise\\ \end{cases} \]

3.1 代码

我们仍然在悬崖漫步环境下尝试 Sarsa 算法。首先来看一下悬崖漫步环境的代码,此时环境不需要提供奖励函数和状态转移函数,而需要提供一个和智能体进行交互的函数step(),该函数将智能体的动作作为输入,输出奖励和下一个状态给智能体。

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm  # tqdm是显示循环进度条的库

class CliffWalkingEnv:
    def __init__(self, ncol, nrow):
        self.nrow = nrow
        self.ncol = ncol
        self.x = 0  # 记录当前智能体位置的横坐标
        self.y = self.nrow - 1  # 记录当前智能体位置的纵坐标

    def step(self, action):  # 外部调用这个函数来改变当前位置
        # 4种动作, change[0]:上, change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
        # 定义在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]
        self.x = min(self.ncol - 1, max(0, self.x + change[action][0]))
        self.y = min(self.nrow - 1, max(0, self.y + change[action][1]))
        next_state = self.y * self.ncol + self.x
        reward = -1
        done = False
        if self.y == self.nrow - 1 and self.x > 0:  # 下一个位置在悬崖或者目标
            done = True
            if self.x != self.ncol - 1:
                reward = -100
        return next_state, reward, done

    def reset(self):  # 回归初始状态,坐标轴原点在左上角
        self.x = 0
        self.y = self.nrow - 1
        return self.y * self.ncol + self.x

class Sarsa:
    """ Sarsa算法 """
    def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
        self.Q_table = np.zeros([nrow * ncol, n_action])  # 初始化Q(s,a)表格为全0
        self.n_action = n_action  # 动作个数
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # ϵ-greedy策略中的参数

    def take_action(self, state):  # 选取下一步的操作,具体实现为ϵ-greedy
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.n_action)  # 随机动作
        else:
            action = np.argmax(self.Q_table[state])  # Q值最大的动作
        return action

    def best_action(self, state):  # 用于打印策略
        Q_max = np.max(self.Q_table[state])
        a = [0 for _ in range(self.n_action)]
        for i in range(self.n_action):  # 若两个动作的价值一样,都会记录下来
            if self.Q_table[state, i] == Q_max:
                a[i] = 1
        return a

    def update(self, s0, a0, r, s1, a1):  # 更新Q值,意义是对于当前的state根据Q值决定action
        # 以下参见Sarsa公式
        td_error = self.Q_table[s0, a0] - (r + self.gamma * self.Q_table[s1, a1])
        self.Q_table[s0, a0] -= self.alpha * td_error

ncol = 12
nrow = 4
env = CliffWalkingEnv(ncol, nrow)
np.random.seed(0)
epsilon = 0.1
alpha = 0.1
gamma = 0.9
agent = Sarsa(ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500  # 智能体在环境中运行的序列的数量

return_list = []  # 记录每一条序列的回报
for i in range(10):  # 显示10个进度条
    # tqdm的进度条功能
    with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
        for i_episode in range(int(num_episodes / 10)):  # 每个进度条的序列数为50
            episode_return = 0  # 一次episode得到的return
            state = env.reset()  # 重置agent的状态为初始状态
            action = agent.take_action(state)  # 采用ϵ-greedy策略获取当前state下采取的action
            done = False
            while not done:  # 反复迭代(行走)直至终止状态
                next_state, reward, done = env.step(action)  # 一次迭代(行走)后得到下一state及其reward
                next_action = agent.take_action(next_state)  # 采用ϵ-greedy策略获取action
                episode_return += reward  # 这里回报的计算不进行折扣因子衰减
                agent.update(state, action, reward, next_state, next_action)  # 更新Q值
                state = next_state  # 将下一时刻的state/action更新为当前的state/action
                action = next_action
            return_list.append(episode_return)
            if (i_episode + 1) % 10 == 0:  # 每10条序列打印一下这10条序列的平均回报
                pbar.set_postfix({
                    'episode':
                    '%d' % (num_episodes / 10 * i + i_episode + 1),
                    'return':
                    '%.3f' % np.mean(return_list[-10:])
                })
            pbar.update(1)

episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Sarsa on {}'.format('Cliff Walking'))
plt.show()

def print_agent(agent, env, action_meaning, disaster=[], end=[]):
    for i in range(env.nrow):
        for j in range(env.ncol):
            if (i * env.ncol + j) in disaster:
                print('****', end=' ')
            elif (i * env.ncol + j) in end:
                print('EEEE', end=' ')
            else:
                a = agent.best_action(i * env.ncol + j)
                pi_str = ''
                for k in range(len(action_meaning)):
                    pi_str += action_meaning[k] if a[k] > 0 else 'o'
                print(pi_str, end=' ')
        print()

action_meaning = ['^', 'v', '<', '>']
print('Sarsa算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
Iteration 0: 100%|██████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 1192.34it/s, episode=50, return=-26.500]
Iteration 1: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 8148.39it/s, episode=100, return=-35.200]
Iteration 2: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 2985.31it/s, episode=150, return=-20.100] 
Iteration 3: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4263.46it/s, episode=200, return=-27.200] 
Iteration 4: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 3639.75it/s, episode=250, return=-19.300] 
Iteration 5: 100%|████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 23126.95it/s, episode=300, return=-27.400] 
Iteration 6: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 5021.19it/s, episode=350, return=-28.000] 
Iteration 7: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 3373.96it/s, episode=400, return=-36.500] 
Iteration 8: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 3875.50it/s, episode=450, return=-27.000] 
Iteration 9: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4993.93it/s, episode=500, return=-19.100] 
Sarsa算法最终收敛得到的策略为:
ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ovoo
^ooo ^ooo ^ooo oo<o ^ooo ^ooo ^ooo ^ooo ooo> ooo> ^ooo ovoo
ooo> ^ooo ^ooo ^ooo ^ooo ^ooo ^ooo ooo> ooo> ^ooo ooo> ovoo
^ooo **** **** **** **** **** **** **** **** **** **** EEEE

image
我们发现,随着训练的进行,Sarsa 算法获得的回报越来越高。在进行 500 条序列的学习后,可以获得 −20 左右的回报,此时已经非常接近最优策略了;同时,Sarsa 算法会采取比较远离悬崖的策略来抵达目标。

3 多步 Sarsa 算法

蒙特卡洛方法利用当前状态之后每一步的奖励而不使用任何价值估计,时序差分算法只利用一步奖励和下一个状态的价值估计。那它们之间的区别是什么呢?总的来说,蒙特卡洛方法是无偏(unbiased)的,但是具有比较大的方差;时序差分算法具有非常小的方差,因为只关注了一步状态转移,用到了一步的奖励,但是它是有偏的。将两者的优势结合得到多步时序差分
多步时序差分的意思是使用 \(n\) 步的奖励,然后使用之后状态的价值估计,直接给出公式:
image
可以发现,当 \(n=1\) 时,就是上节介绍的 Sarsa 算法;当 \(n\rightarrow∞\)时,就是蒙特卡洛方法。

3.1 代码

在 Sarsa 代码的基础上进行修改,引入多步时序差分计算。主要就是将 Sarsa 类改为了nstep_Sarsa 类。

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm  # tqdm是显示循环进度条的库

class CliffWalkingEnv:
    def __init__(self, ncol, nrow):
        self.nrow = nrow
        self.ncol = ncol
        self.x = 0  # 记录当前智能体位置的横坐标
        self.y = self.nrow - 1  # 记录当前智能体位置的纵坐标

    def step(self, action):  # 外部调用这个函数来改变当前位置
        # 4种动作, change[0]:上, change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
        # 定义在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]
        self.x = min(self.ncol - 1, max(0, self.x + change[action][0]))
        self.y = min(self.nrow - 1, max(0, self.y + change[action][1]))
        next_state = self.y * self.ncol + self.x
        reward = -1
        done = False
        if self.y == self.nrow - 1 and self.x > 0:  # 下一个位置在悬崖或者目标
            done = True
            if self.x != self.ncol - 1:
                reward = -100
        return next_state, reward, done

    def reset(self):  # 回归初始状态,坐标轴原点在左上角
        self.x = 0
        self.y = self.nrow - 1
        return self.y * self.ncol + self.x

class nstep_Sarsa:
    """ n步Sarsa算法 """
    def __init__(self, n, ncol, nrow, epsilon, alpha, gamma, n_action=4):
        self.Q_table = np.zeros([nrow * ncol, n_action])
        self.n_action = n_action
        self.alpha = alpha
        self.gamma = gamma
        self.epsilon = epsilon
        self.n = n  # 采用n步Sarsa算法
        self.state_list = []  # 保存之前的状态
        self.action_list = []  # 保存之前的动作
        self.reward_list = []  # 保存之前的奖励

    def take_action(self, state):
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.n_action)
        else:
            action = np.argmax(self.Q_table[state])
        return action

    def best_action(self, state):  # 用于打印策略
        Q_max = np.max(self.Q_table[state])
        a = [0 for _ in range(self.n_action)]
        for i in range(self.n_action):
            if self.Q_table[state, i] == Q_max:
                a[i] = 1
        return a

    def update(self, s0, a0, r, s1, a1, done):
        # 保存之前的state、action、reward,用于更新Q值
        self.state_list.append(s0)
        self.action_list.append(a0)
        self.reward_list.append(r)
        if len(self.state_list) == self.n:  # 若保存的数据可以进行n步更新
            G = self.Q_table[s1, a1]  # 得到Q(s_{t+n}, a_{t+n})
            for i in reversed(range(self.n)):  # 从后向前滚动计算
                G = self.reward_list[i] + self.gamma * G  # 不断向前计算每一步的回报
                # 如果到达终止状态,最后几步虽然长度不够n步,也将其进行更新
                if done and i > 0:
                    s = self.state_list[i]
                    a = self.action_list[i]
                    self.Q_table[s, a] -= self.alpha * (self.Q_table[s, a] - G)
            s = self.state_list.pop(0)  # 将需要更新的状态动作从列表中删除,下次不必更新
            a = self.action_list.pop(0)
            self.reward_list.pop(0)
            # n步Sarsa的主要更新步骤
            self.Q_table[s, a] -= self.alpha * (self.Q_table[s, a] - G)
        if done:  # 如果到达终止状态,即将开始下一条序列,则将列表全清空
            self.state_list = []
            self.action_list = []
            self.reward_list = []

ncol = 12
nrow = 4
env = CliffWalkingEnv(ncol, nrow)
np.random.seed(0)
n_step = 5  # 5步Sarsa算法
alpha = 0.1
epsilon = 0.1
gamma = 0.9
agent = nstep_Sarsa(n_step, ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500  # 智能体在环境中运行的序列的数量

return_list = []  # 记录每一条序列的回报
for i in range(10):  # 显示10个进度条
    #tqdm的进度条功能
    with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
        for i_episode in range(int(num_episodes / 10)):  # 每个进度条的序列数
            episode_return = 0
            state = env.reset()
            action = agent.take_action(state)
            done = False
            while not done:
                next_state, reward, done = env.step(action)
                next_action = agent.take_action(next_state)
                episode_return += reward  # 这里回报的计算不进行折扣因子衰减
                agent.update(state, action, reward, next_state, next_action,
                             done)
                state = next_state
                action = next_action
            return_list.append(episode_return)
            if (i_episode + 1) % 10 == 0:  # 每10条序列打印一下这10条序列的平均回报
                pbar.set_postfix({
                    'episode':
                    '%d' % (num_episodes / 10 * i + i_episode + 1),
                    'return':
                    '%.3f' % np.mean(return_list[-10:])
                })
            pbar.update(1)

episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('5-step Sarsa on {}'.format('Cliff Walking'))
plt.show()

def print_agent(agent, env, action_meaning, disaster=[], end=[]):
    for i in range(env.nrow):
        for j in range(env.ncol):
            if (i * env.ncol + j) in disaster:
                print('****', end=' ')
            elif (i * env.ncol + j) in end:
                print('EEEE', end=' ')
            else:
                a = agent.best_action(i * env.ncol + j)
                pi_str = ''
                for k in range(len(action_meaning)):
                    pi_str += action_meaning[k] if a[k] > 0 else 'o'
                print(pi_str, end=' ')
        print()

action_meaning = ['^', 'v', '<', '>']
print('Sarsa算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
Iteration 0: 100%|██████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 1683.72it/s, episode=50, return=-26.500]
Iteration 1: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4067.95it/s, episode=100, return=-35.200] 
Iteration 2: 100%|████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 11553.28it/s, episode=150, return=-20.100] 
Iteration 3: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 2343.92it/s, episode=200, return=-27.200] 
Iteration 4: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4079.59it/s, episode=250, return=-19.300] 
Iteration 5: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4934.36it/s, episode=300, return=-27.400] 
Iteration 6: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 7454.69it/s, episode=350, return=-28.000]
Iteration 7: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 2874.70it/s, episode=400, return=-36.500] 
Iteration 8: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 3928.43it/s, episode=450, return=-27.000] 
Iteration 9: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4664.80it/s, episode=500, return=-19.100] 

image
通过实验结果可以发现,5 步 Sarsa 算法的收敛速度比单步 Sarsa 算法更快;此时多步 Sarsa 算法得到的策略会在最远离悬崖的一边行走,以保证最大的安全性。

4 Q-learning 算法

image
可以看到,Q-learning 与 Sarsa 的最大不同在于 Q-learning 是直接估计 q(s,a),而 Sarsa 估计 \(ϵ\)-greedy 策略下的 q(s,a),换句话说,Q-learning 采取的 action value 是由 \(s_{t+1}\) 下所有 action value 中的最大值直接决定的,而 Sarsa 采取的 action value 是由 \(s_{t+1}\)下的 \(ϵ\)-greedy 策略间接决定的。

  • Sarsa 更新公式必须使用来自当前策略采样得到的五元组 \((s,a,r,s',a')\),为在线策略(on-policy)算法;
  • Q-learning 更新公式使用四元组 \((s,a,r,s')\),r 和 s' 为采样得到,该四元组并不一定需要是当前策略采样得到的数据,为离线策略(off-policy)算法。

需要注意的是,on-policy 和 off-policy 这两个概念在强化学习中非常重要,具体请看4.2节。
Q_learning 算法的优势在于它是离线策略算法,能够使用经验回放池(我们在后续的 DQN 算法中会介绍)重复使用过往训练样本,往往具有更小的样本复杂度(算法达到收敛结果需要在环境中采样的样本数量),也因此更受欢迎。

4.1 代码

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm  # tqdm是显示循环进度条的库

class CliffWalkingEnv:
    def __init__(self, ncol, nrow):
        self.nrow = nrow
        self.ncol = ncol
        self.x = 0  # 记录当前智能体位置的横坐标
        self.y = self.nrow - 1  # 记录当前智能体位置的纵坐标

    def step(self, action):  # 外部调用这个函数来改变当前位置
        # 4种动作, change[0]:上, change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
        # 定义在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]
        self.x = min(self.ncol - 1, max(0, self.x + change[action][0]))
        self.y = min(self.nrow - 1, max(0, self.y + change[action][1]))
        next_state = self.y * self.ncol + self.x
        reward = -1
        done = False
        if self.y == self.nrow - 1 and self.x > 0:  # 下一个位置在悬崖或者目标
            done = True
            if self.x != self.ncol - 1:
                reward = -100
        return next_state, reward, done

    def reset(self):  # 回归初始状态,坐标轴原点在左上角
        self.x = 0
        self.y = self.nrow - 1
        return self.y * self.ncol + self.x

class QLearning:
    """ Q-learning算法 """
    def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
        self.Q_table = np.zeros([nrow * ncol, n_action])  # 初始化Q(s,a)表格
        self.n_action = n_action  # 动作个数
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # epsilon-贪婪策略中的参数

    def take_action(self, state):  #选取下一步的操作
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.n_action)
        else:
            action = np.argmax(self.Q_table[state])
        return action

    def best_action(self, state):  # 用于打印策略
        Q_max = np.max(self.Q_table[state])
        a = [0 for _ in range(self.n_action)]
        for i in range(self.n_action):
            if self.Q_table[state, i] == Q_max:
                a[i] = 1
        return a

    def update(self, s0, a0, r, s1):
        ## 与 Sarsa 算法的主要区别,更新方式(Q值)的不同
        td_error = self.Q_table[s0, a0] - (r + self.gamma * self.Q_table[s1].max())
        self.Q_table[s0, a0] -= self.alpha * td_error

ncol = 12
nrow = 4
env = CliffWalkingEnv(ncol, nrow)
np.random.seed(0)
epsilon = 0.1
alpha = 0.1
gamma = 0.9
agent = QLearning(ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500  # 智能体在环境中运行的序列的数量

return_list = []  # 记录每一条序列的回报
for i in range(10):  # 显示10个进度条
    # tqdm的进度条功能
    with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
        for i_episode in range(int(num_episodes / 10)):  # 每个进度条的序列数
            episode_return = 0
            state = env.reset()
            done = False
            while not done:
                action = agent.take_action(state) # 由于更新不需要next_action,所以在这里take_action()
                next_state, reward, done = env.step(action)
                episode_return += reward  # 这里回报的计算不进行折扣因子衰减
                agent.update(state, action, reward, next_state)
                state = next_state
            return_list.append(episode_return)
            if (i_episode + 1) % 10 == 0:  # 每10条序列打印一下这10条序列的平均回报
                pbar.set_postfix({
                    'episode':
                    '%d' % (num_episodes / 10 * i + i_episode + 1),
                    'return':
                    '%.3f' % np.mean(return_list[-10:])
                })
            pbar.update(1)

episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Q-learning on {}'.format('Cliff Walking'))
plt.show()

def print_agent(agent, env, action_meaning, disaster=[], end=[]):
    for i in range(env.nrow):
        for j in range(env.ncol):
            if (i * env.ncol + j) in disaster:
                print('****', end=' ')
            elif (i * env.ncol + j) in end:
                print('EEEE', end=' ')
            else:
                a = agent.best_action(i * env.ncol + j)
                pi_str = ''
                for k in range(len(action_meaning)):
                    pi_str += action_meaning[k] if a[k] > 0 else 'o'
                print(pi_str, end=' ')
        print()

action_meaning = ['^', 'v', '<', '>']
print('Q-learning算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
Iteration 0: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 1504.42it/s, episode=50, return=-105.700]
Iteration 1: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 1631.21it/s, episode=100, return=-70.900] 
Iteration 2: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 2566.89it/s, episode=150, return=-56.500] 
Iteration 3: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 2673.17it/s, episode=200, return=-46.500] 
Iteration 4: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 3955.02it/s, episode=250, return=-40.800] 
Iteration 5: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4360.07it/s, episode=300, return=-20.400] 
Iteration 6: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 4366.34it/s, episode=350, return=-45.700] 
Iteration 7: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 5693.21it/s, episode=400, return=-32.800] 
Iteration 8: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 5227.72it/s, episode=450, return=-22.700] 
Iteration 9: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 5478.60it/s, episode=500, return=-61.700] 
Q-learning算法最终收敛得到的策略为:
^ooo ovoo ovoo ^ooo ^ooo ovoo ooo> ^ooo ^ooo ooo> ooo> ovoo
ooo> ooo> ooo> ooo> ooo> ooo> ^ooo ooo> ooo> ooo> ooo> ovoo
ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ooo> ovoo
^ooo **** **** **** **** **** **** **** **** **** **** EEEE

image
需要注意的是,打印出来的回报是行为策略在环境中交互得到的,而不是 Q-learning 算法在学习的目标策略的真实回报。我们把目标策略的行为打印出来后,发现其更偏向于走在悬崖边上,这与 Sarsa 算法得到的比较保守的策略相比是更优的。 但是仔细观察 Sarsa 和 Q-learning 在训练过程中的回报曲线图,我们可以发现,在一个序列中 Sarsa 获得的期望回报是高于 Q-learning 的。这是因为在训练过程中智能体采取基于当前 q(s,a) 函数的 \(ϵ\)-greedy 策略来平衡探索与利用,Q-learning 算法由于沿着悬崖边走,会以一定概率探索“掉入悬崖”这一动作,而 Sarsa 相对保守的路线使智能体几乎不可能掉入悬崖。

4.2 在线策略算法与离线策略算法

我们称采样数据的策略为行为策略(behavior policy),称用这些数据来更新的策略为目标策略(target policy)。在线策略(on-policy)算法表示行为策略和目标策略是同一个策略;而离线策略(off-policy)算法表示行为策略和目标策略不是同一个策略。Sarsa 是典型的在线策略算法,而 Q-learning 是典型的离线策略算法。判断二者类别的一个重要手段是看计算时序差分的价值目标的数据是否来自当前的策略。离线策略算法能够重复使用过往训练样本,往往具有更小的样本复杂度,也因此更受欢迎。

参考资料

https://hrl.boyuai.com/chapter/1/时序差分算法
https://www.bilibili.com/video/BV1sd4y167NS?p=31&vd_source=f7563459deb4ecb3add61713c7d5d111

posted @ 2024-04-24 14:01  Hell0er  阅读(38)  评论(0编辑  收藏  举报