时序差分更新与回合更新的区别在于,时需差分更新汲取了动态规划方法中“自益”的思想,用现有的价值估计值来更新价值估计,不需要等到回合结束也可以更新价值估计。时序差分更新也包括同策和异策,同时还能分为单步更新和多步更新。
从给定策略 的情况下动作价值的定义出发,则有下式:
单步时序差分更新就是依据式 来更新的,因此只需采样一步,就可以使用 来估计回报样本的值。为了与由奖励直接计算得到的无偏回报样本 进行区别,使用字母 表示使用自益得到的有偏回报样本。
根据以上分析,可以定义时序差分目标:
其中 的下标 表示用 的估计值来估计 。再推广到多步时序差分目标:
在回合更新算法中是使用 增量更新来学习动作价值函数,而该式子在时序差分中,回报 由 替代;更新系数 被称为学习率,用 替代。考虑到时序差分算法执行的过程中价值函数会越来越准确,进而基于价值函数估计得到的价值函数也会越来越准确,因此估计值的权重可以越来越大,所以算法中可以采用一个固定的学习率 ,在有些问题中,让学习率巧妙地变化能得到更好的效果。
那么可以得到时序差分更新价值的表达式:
根据表达式 可以得到单步时序差分更新评估策略的动作(状态)价值算法:
在无模型的情况下,用回合更新和时序差分更新来评估策略都能渐近得到真实的价值函数,它们各有优劣,目前并没有证明某种方法就比另外一种方法更好。根据经验,学习率为常数的时序差分更新常常比学习率为常数的回合更新更快收敛,但时序差分更新对环境的 Markov 性要求更高。由于时序差分更新的 使用了自益(下一个状态的样本 ),如果环境真的是 Markov 决策过程,那么自益能够更加有效的使用轨迹样本;若环境不是 Markov 决策过程,那么自益反而会导致错误的估计,而回合更新只使用了当前状态的样本,并不会产生错误的估计。
将单步时序差分算法推广到多步,即可得到算法 5-2 :
在评估策略的价值后进行策略改进,可以得到使用同策时序差分更新求解最优策略的“状态 / 动作 / 奖励 / 状态 / 动作”(State-Action-Reward-State-Action,SARSA)算法;其中策略的提升方法可以采用 贪心算法,使得 总是柔性策略,且中间过程的策略既可以显式更新和存储,也可以不显式存储:
在 SARSA 算法中采用多步时序差分目标,即可得到多步 SARSA 算法,类似于从算法 5-1 到 5-2 ,该算法可由算法 5-3 修改得到,此处略。
SARSA 算法有一种变换——期望 SARSA 算法(Expected SARSA),它的不同之处在于,在估计 时,不使用基于动作价值的时序差分目标,而使用基于状态价值的时序差分目标,再利用 Bellman 方程有:
该式需要计算 ,虽然计算量增大,但这样的期望运算减小了 SARSA 算法中出现的个别不恰当决策,因此期望 SARSA 常常有比 SARSA 更大的学习率,在很多情况下,期望 SARSA 的效果会比 SARSA 稍微好一些。算法 5-4 给出了期望 SARSA 算法:
期望 SARSA 算法同样也有多步版本,与前面的多步算法类似,可由单步版本修改得到,此处略。
前面已经介绍过异策算法是基于重要性采样的,将该方法整合到时序差分策略评估动作价值算法或 SARSA 算法中,即可得到它们的重要性采样版本,算法 5-5 给出了多步时序差分的版本:
单步版本也可以由前面的算法修改得到,另外还可以类似的方法将重要性采样运用到时序差分状态价值估计和期望 SARSA 算法中,此处略。
异策时序差分更新是比同策时序差分更新更加流行的算法,特别是 Q 学习算法,已经成为最重要的基础算法之一。该算法是从改进后策略的行为出发,将时序差分目标改为 ,即直接使用根据 改进后的策略来更新,使得可以更接近最优价值。因此 Q 学习的更新式不是基于当前的策略,而是基于另外一个不一定要使用的确定性策略来更新动作价值,从这个意义上来看,Q 学习是一个异策算法:
Q 学习也有多步版本,其目标为 ,此处略。
由于 Q 学习使用 来更新动作价值,当奖励存在偏差时,该方法会导致“最大化偏差”(maximization bias),使得估计的动作价值偏差较大。为解决这一问题,双重 Q 学习(Double Q Learning) 算法使用两个独立的动作价值估计值 和 ,用 或 来代替 Q 学习中的 。由于 和 是相互独立的估计,所以 ,其中 ,这样就消除了偏差。 和 都需要逐渐更新,一种方法是在每步的学习中以等概论选择两个更新中的任意一个:
该算法最终输出的是 和 的平均值,但在中间步骤是直接使用 的,该简化计算并不影响结果。
资格迹是一种让时序差分学习更加有效的机制,它能够在回合更新和单步时序差分更新之间折中,并且实现简单,运行有效。在介绍资格迹之前,先介绍以下内容:
给定 , 回报( return)是时序差分目标 按 加权平均的结果。对于连续性任务有:
对于回合制任务,由于当 时, ,故有:
上式可以看做是回合更新目标 和单步时序差分目标 的推广:当 时, 就是回合更新的回报;当 时, 就是单步时序差分目标。
离线 回报算法(offline -return algorithm)是以 作为目标,试图减小 或 的。对于回合制任务,该算法在回合结束后为每一步 计算 ,并统一更新价值,因此这样的算法称为离线算法(offline algorithm);由于使用的目标在 和 之间做了折中,所以其效果可能比单独使用这两个目标都要好。
但是离线 算法也有明显的缺点:
采用资格迹可以弥补这两个缺点, 是历史上具有重要影响力的强化学习算法,在离线 回报算法的基础上改进而来。事实上,在离线 回报算法中,知道 后就能计算 并部分更新 , ,那么问题就转化为如何求得更新权重了。根据该思想引入资格迹 来表示第 步的单步自益结果 对每个状态动作对 , 需要更新的权重,资格迹(eligibility)定义如下:
其中 是事先给定的参数,表示对当前最新出现的状态动作对 ,它的更新权重则要进行某种强化,其取值常有以下几种:
对于资格迹的表达式可以这么理解:从 回报的各项 中可以看到, 最大权重 是当前步的单步时序差分,因此对于当前的动作状态对,可以进行某种强化;而对于历史上的某个状态动作对 , 在 的权重为 ,因为 ,所以单步时序差分 是以 的比率折算到 中的,即间隔步数每增加一步,原先的资格迹大致需要衰减 倍。(总的来说,对于历史记录越久的状态动作对,资格迹越小,更新力度越小;历史记录越新的状态动作对,资格迹越大,更新力度越大。参考资料:莫烦python:什么是 Sarsa(lambda))
资格迹也可用于状态价值,将动作价值资格迹内容中的动作状态对改为状态即可,此处略。利用资格迹,可以得到 策略评估算法,它是在单步时序差分的基础上,加入资格迹来实现的;资格迹也可以和最优策略求解算法结合,例如和 SARSA 算法结合得到 SARSA() 算法。算法 5-8 给出了 TD() 的动作价值评估或 SARSA() 学习算法,TD() 的状态价值评估可结合算法 5-1 修改得到,此处略。
(资格迹更新与离线 回报更新比较:假设 全部初始化为 0 ,使用替换迹, ,考虑两步,则有
从两者的展开计算结果可以看到,所有的加权项是相同的,只是加权权重不同。)
本节使用 gym 库里的出租车调度问题(Taxi-v3),该问题在一个 5x5 方格表示的地图上,有 4 个出租车停靠点,函数 env.render()
会显示如下所示的地图:
其中,有一个乘客会随机出现在 4 个出租车停靠点中的一个(将显示为蓝色),并想在任意一个停靠点下车(将显示为洋红色);出租车会随机出现在 25 个位置的任意一个(黄色高亮显示,当乘客在出租车上时,会变成绿色高亮显示),出租车只能在地图范围内移动一格,且有竖线阻拦的地方不能横向移动;任务为出租车移动到乘客位置,邀请上车,然后移动到目的地,再让乘客下车。
该环境的观测值是一个范围为 的 int 型数值,该数值唯一的表示了整个环境的状态,可使用 env.decode()
函数将该数值转化为长度为 4 的元组(taxirow, taxical, passloc, destidx
),其各元素也都是 int 型变量,取值和含义如下:
taxirow
和 taxicol
取值为 {0, 1, 2, 3, 4} ,表示当前出租车的位置;passloc
取值为 {0, 1, 2, 3, 4} ,表示乘客的位置,0~3 表示在出租车停靠点,4 表示在车上;destidx
取值为 {0, 1, 2, 3} ,表示目的地位置。全部状态的总数为(5 x 5)x 5 x 4 = 500 。元素取值与地图的对应如下表所示:
passloc 或 destidx | 0 | 1 | 2 | 3 |
---|---|---|---|---|
地图中对应的字母 | R | G | Y | B |
地图上的坐标 | (0, 0) | (0, 4) | (4, 0) | (4, 3) |
这个问题中的动作取值为 {0, 1, 2, 3, 4, 5} ,其含义与对应的奖励值如下表所示:
动作数值 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
含义 | 试图往下 | 试图往上 | 试图往右 | 试图往左 | 请乘客上车 | 请乘客下车 |
执行后的奖励 | -1 | -1 | 1 | -1 | 正确 -1 或错误 -10 | 正确 +20 或错误 -10 |
本节代码将把各个算法实现为一个类,将该类实例化为一个智能体,使用该智能体与环境进行交互。考虑到以上的算在确定动作与环境交互的顺序上略有不同,这里在不改变逻辑的情况下,在代码中进行统一,函数 run_episode
实现了该统一:
xxxxxxxxxx
231def run_episode(env, agent=None, train=False, render=False):
2 episode_reward = 0
3 state = env.reset()
4 if agent is None:
5 action = env.action_space.sample()
6 else:
7 action = agent.choose_action(state)
8 while True:
9 if render:
10 env.render()
11 next_state, reward, done, _ = env.step(action)
12 if agent is None:
13 next_action = env.action_space.sample()
14 else:
15 next_action = agent.choose_action(next_state)
16 if train:
17 agent.learn(state, action, reward, next_state, done, next_action)
18
19 episode_reward += reward
20 if done:
21 break
22 state, action = next_state, next_action
23 return episode_reward
由于本节的算法在很多参数上都是相同的,在选择动作时都能以相同的方法确定策略(如 贪心策略),因此这里将这些总结为一个 Agent
基类,并使用了 贪心策略:
xxxxxxxxxx
141class Agent():
2 def __init__(self, env, gamma=0.9, learning_rate=0.1, epsilon=0.01):
3 self.gamma = gamma
4 self.learning_rate = learning_rate
5 self.epsilon = epsilon
6 self.action_n = env.action_space.n
7 self.q = np.zeros((env.observation_space.n, env.action_space.n))
8
9 def choose_action(self, state):
10 if np.random.uniform() > self.epsilon:
11 action = self.q[state].argmax()
12 else:
13 action = np.random.randint(self.action_n)
14 return action
那么根据各个算法的价值更新步骤,很容易得到对应的智能体类,例如 SARSA 和 QLearning,其中 QLearing 的方法 learn
的参数 *args
是为了兼容 run_episode
下 agent.learn(...)
的传参调用:
xxxxxxxxxx
91class SARSA(Agent):
2 def learn(self, state, action, reward, next_state, done, next_action):
3 u = reward + self.gamma * self.q[next_state][next_action] * (1 - done)
4 self.q[state][action] += self.learning_rate * (u - self.q[state][action])
5
6class QLearning(Agent):
7 def learn(self, state, action, reward, next_state, done, *args):
8 u = reward + self.gamma * self.q[next_state].max() * (1 - done)
9 self.q[state][action] += self.learning_rate * (u - self.q[state][action])
有了以上这些,就能够实例化智能体算法,并将其放入环境进行训练与测试了:
xxxxxxxxxx
121#agent = SARSA(env)
2agent = QLearning(env)
3# 智能体的训练
4for i in range(episodes):
5 episode_rewards.append(run_episode(env, agent, train=True))
6plt.plot(episode_rewards)
7# 智能体的测试
8agent.epsilon = 0
9episode_rewards = [run_episode(env, agent) for _ in range(100)]
10print("平均回合奖励 = {} / {} = {}".format(sum(episode_rewards), \
11 len(episode_rewards), np.mean(episode_rewards)))
12plt.show()
对于 DoubleQLearning 和 SARSA() 这类有部分不同参数的算法,可以继承并改写基类的方法:
xxxxxxxxxx
341class DoubleQLearning(Agent):
2 def __init__(self, env, gamma=0.9, learning_rate=0.1, epsilon=0.01):
3 super().__init__(env, gamma=gamma, learning_rate=learning_rate, epsilon=epsilon)
4 self.q1 = np.zeros((env.observation_space.n, env.action_space.n))
5
6 def choose_action(self, state):
7 if np.random.uniform() > self.epsilon:
8 action = (self.q[state] + self.q1[state]).argmax()
9 else:
10 action = np.random.randint(self.action_n)
11 return action
12
13 def learn(self, state, action, reward, next_state, done, *args):
14 if np.random.randint(2):
15 self.q, self.q1 = self.q1, self.q
16 a = self.q[next_state].argmax()
17 u = reward + self.gamma * self.q1[next_state][a] * (1 - done)
18 self.q[state][action] += self.learning_rate * (u - self.q[state][action])
19
20class SARSALambda(Agent):
21 def __init__(self, env, lamb=0.5, beta=1, gamma=0.9, learning_rate=0.1, epsilon=0.01):
22 super().__init__(env, gamma=gamma, learning_rate=learning_rate, epsilon=epsilon)
23 self.lamb = lamb
24 self.beta = beta
25 self.e = np.zeros((env.observation_space.n, env.action_space.n))
26
27 def learn(self, state, action, reward, next_state, done, next_action):
28 self.e *= (self.lamb * self.gamma)
29 self.e[state][action] = 1 + self.beta * self.e[state][action]
30 u = reward + self.gamma * self.q[next_state][next_action] * (1 - done)
31 self.q += self.learning_rate * self.e * (u - self.q[state][action])
32 # self.q += self.learning_rate * self.e * (u - self.q)
33 if done: # 为下一回合初始化资格迹
34 self.e *= 0
在该问题中,最大化偏差不明显,所以双重 Q 学习往往不能得到好处。本章的算法在该问题上性能各不相同,其中的原因比较复杂,可能是算法本身的问题,也可能是参数选择的问题,但没有一个算法是对所有的任务都有效的。
另外值得注意的是,书上勘误后的 SARSA() 算法是使用 来更新的,对应代码 self.q += self.learning_rate * self.e * (u - self.q[state][action])
,该更新式能够收敛;而在勘误前是使用 来更新的,对应代码 self.q += self.learning_rate * self.e * (u - self.q)
,该更新式也能够收敛。根据资格迹的定义,并从增量更新式的角度来看,被减去的值应该是被更新的 ,而不是固定的 ;但从另一个角度来看,由当前步的好坏 来决定历史动作价值 增减,也是合理的,此时被减去的值应该是固定的 。