时序差分算法 tqmd time.sleep() with ...as reversed 无偏估计

时序差分算法

5.1 简介

动态规划算法要求马尔可夫决策过程是已知的,即要求与智能体交互的环境是完全已知的(例如迷宫或者给定规则的网格世界)。在此条件下,智能体其实并不需要和环境真正交互去采样数据,直接用动态规划算法就可以解出最有价值或策略。这就好比对于有监督学习任务,如果直接显式给出了数据的分布公式,那么也可以通过在期望层面上直接最小化模型的泛化误差来更新模型参数,并不需要采样任何数据点。

但这在大部分场景下并不现实,机器学习的主要方法都是在数据分布未知情况下针对具体的数据点来对模型做出更新的。对于大部分强化学习现实场景(例如电子游戏或者一些复杂物理环境),其马尔可夫决策过程的状态转移概率是无法写出来的,也就无法直接进行动态规划。在这种情况下,智能体只能和环境进行交互,通过采样到的数据来学习,这类学习方法统称为无模型的强化学习(model-free reinforcement learning).

不同于动态规划算法,无模型的强化学习算法不需要事先知道环境的奖励函数和状态转移函数,而是直接使用和环境交互的过程中采样到的数据来学习,这使得它可以被应用到一些简单的实际场景中。本章将要讲解无模型的强化学习中的两大经典算法:Sarsa和Q-learning,他们都是基于时序差分(temporal difference,TD)的强化学习算法。同时,本章还会引入一组概念:在线策略学习和离线策略学习。通常来说,在线策略学习要求使用在当前策略下采样得到的样本进行学习,一旦策略被更新,当前的样本就被放弃了,就好像在水龙头下用自来水洗手。因此,离线策略学习往往能够更好的利用历史数据,并且有更小的样本复杂度(算法达到收敛结果需要在环境中采样的样本数量),这使得其被广泛地应用。

5.2 时序差分算法

时序差分算法是一种用来估计一个策略的价值函数的方法,它结合了蒙特卡洛和动态规划算法的思想。时序差分算法和蒙特卡洛的相似之处在于可以从样本数据中学习,不需要事先知道环境;和动态规划算法的相似之处在于根据贝尔曼方程的思想,利用后续状态的价值估计来更新当前状态的价值估计。回顾一下蒙特卡洛方法对价值函数的增量更新方式:

V ( s t ) ← V ( s t ) + α [ G t − V ( s t ) ] V(s_t)←V(s_t)+α[G_t−V(s_t)] V(st)V(st)+α[GtV(st)]

{ G t − V ( s t ) G_t−V(s_t) GtV(st)}是指该状态下的回报减去当前时刻的回报即t+1时刻及之后时刻的折扣回报的和 乘上一个系数是因为这是在这个环境中所有有这个状态的和 所以一般要除以遇到状态s的次数

这里我们将动态路径规划节的 1 N ( s ) \frac{1}{N(s)} N(s)1替换成了 α \alpha α ,表示对价值估计更新的步长。可以将 α 取为一个常数,此时更新方式不再像蒙特卡洛方法那样严格地取期望。蒙特卡洛方法必须要等整个序列结束之后才能计算得到这一次的回报 G t G_{t} Gt ,而时序差分方法只需要当前步结束即可进行计算。具体来说,时序差分算法用当前获得的奖励加上下一个状态的价值估计来作为在当前状态会获得的回报,即:

V ( s t ) ← V ( s t ) + α [ r t + γ V ( s t + 1 ) − V ( s t ) ] V\left(s_{t}\right) \leftarrow V\left(s_{t}\right)+\alpha\left[r_{t}+\gamma V\left(s_{t+1}\right)-V\left(s_{t}\right)\right] V(st)V(st)+α[rt+γV(st+1)V(st)]

其中 R t + γ V ( s t + 1 ) − V ( s t ) R_{t}+\gamma V\left(s_{t+1}\right)-V\left(s_{t}\right) Rt+γV(st+1)V(st) 通常被称为时序差分 (temporal difference,TD) 误差(error),时序差分算法将其与步长的乘积作为状态价值的更新量。可以用 r t + γ V ( s t + 1 ) r_{t}+\gamma V\left(s_{t+1}\right) rt+γV(st+1) 来代替 G t G_{t} Gt 的原因是:

V π ( s ) = E π [ G t ∣ S t = s ] = E π [ Σ ∞ k = 0 γ k R t + k ∣ S t = s ] = E π [ R t + γ Σ ∞ k = 0 γ k R t + k + 1 ∣ S t = s ] = E π [ R t + γ V π ( S t + 1 ) ∣ S t = s ] V_π(s)=E_π[G_t∣S_t=s]=E_π[\underset{k=0}{\overset{∞}{\varSigma}}γ_k R_{t+k}∣S_t=s]=E_π[R_t+γ\underset{k=0}{\overset{∞}{\varSigma}}γ_k R_{t+k+1}∣S_t=s]=E_π[R_t+γV_π(S_{t+1})∣S_t=s] Vπ(s)=Eπ[GtSt=s]=Eπ[k=0ΣγkRt+kSt=s]=Eπ[Rt+γk=0ΣγkRt+k+1St=s]=Eπ[Rt+γVπ(St+1)St=s]

因此蒙特卡洛方法将上式第一个等式作为更新的目标,而时序差分算法将上式最后一个等式作为更新的目标。于是,在用策略和环境交互时,每采样一步,我们就可以用时序差分算法来更新状态价值估计。时序差分算法用到了 V ( s t + 1 ) V(s_{t+1}) V(st+1)的估计值,可以证明他最终收敛到策略 π \pi π的价值函数。

5.3 S a r s a Sarsa Sarsa算法

( S a r s a Sarsa Sarsa算法中的 α \alpha α相当于蒙特卡洛算法中的求期望的那个分母( N ( s ) N(s) N(s)),后面的差分是时序差分算法的思想)

既然我们可以用时序差分算法来估计价值函数,那一个很自然的问题是,我们能否用类似策略迭代的方法来进行强化学习。策略评估已经可以通过时许差分算法实现,那么在不知道奖励函数和状态转移函数的情况下该怎么进行策略提升呢?答案是可以直接用时序差分算法来估计动作价值函数Q:

Q ( s t , a t ) < − − Q ( s t , a t ) + α [ r t + γ Q ( s t + 1 , a t + 1 ) − Q ( s t , a t ) ] Q(s_t,a_t)<--Q(s_t,a_t)+\alpha[r_t+\gamma Q(s_{t+1},a_{t+1})-Q(s_t,a_t)] Q(st,at)<Q(st,at)+α[rt+γQ(st+1,at+1)Q(st,at)]

然后我们用贪婪算法来选取在某个状态下动作价值最大的那个动作,即 a r g m a x a Q ( s , a ) arg max_{a}Q(s,a) argmaxaQ(s,a)。这样似乎已经形成了一个完整的强化学习算法:用贪婪算法根据动作价值选取动作来和环境交互,再根据得到的数据用时序差分算法更新动作价值估计。

(待解决问题:最一开始迭代时动作价值函数怎么得到)

一开始的动作价值函数都是零,之后会根据每个状态的奖励进行计算和更新,并且Q_table中的Q值一直保存之后是更新 不会清零。

然而这个简单的算法存在两个需要进一步考虑的问题。第一,如果用时序差分算法来准确的估计策略的状态价值函数,我们需要用极大量的样本来进行更新。但实际上我们可以忽略这一点,直接用一些样本来评估策略,然后就可以更新策略了。我们可以这么做的原因是策略提升可以在策略评估未完全进行的情况下进行,回顾一下,价值迭代就是这样,这其实是广义策略迭代的思想。第二,如果在策略提升中一直根据贪婪算法得到一个确定性策略,可能会导致某些状态动作对(s,a)永远没有在序列中出现,以至于无法对其动作价值进行估计,进而无法保证策略提升后的策略比之前的好。我们在第二章中对此有详细讨论。简单常用的解决方案是不再一味使用贪婪算法,而是采用一个 ϵ \epsilon ϵ-贪婪策略:有1- ϵ \epsilon ϵ的概率采用动作价值最大的那个动作,另外有 ϵ \epsilon ϵ的概率从动作空间中随机从动作空间中随机采取一个动作,其公式表示为:
π ( a ∣ s ) = { ϵ / ∣ A ∣ + 1 − ϵ   如果 a = a r g   max ⁡ Q ( s , a ′ ) a ′ ϵ / ∣ A ∣            其他动作   \pi \left( a|s \right) =\left\{ \begin{array}{l} \epsilon /|A|+1-\epsilon \ \ \text{如果}a=arg\ \underset{a'}{\max Q\left( s,a' \right)}\\ \epsilon /|A|\ \ \ \ \ \ \ \ \ \ \ \text{其他动作}\\ \end{array} \right. \ π(as)={ϵ/∣A+1ϵ  如果a=arg amaxQ(s,a)ϵ/∣A           其他动作 
现在我们就可以得到一个实际的基于时序差分方法的强化学习算法。这个算法被称为Sarsa,因为它的动作价值更新用到了当前状态s、当前动作a、获得的奖励r、下一个状态s’和下一个动作a‘,将这些符号拼接后就得到了算法名称。Sarsa的具体算法如下:

  • 初始化 Q ( s , a ) Q(s,a) Q(s,a)
  • for序列 e = 1 − − > E e=1-->E e=1>Edo
  • ​ 得到初始状态 s s s
  • ​ 用 ϵ − g r e e d y \epsilon-greedy ϵgreedy策略根据Q选择当前s下的动作a
  • for时间步 t − − > T t-->T t>Tdo:
  • ​ 得到环境反馈的r,s’
  • ​ 用 ϵ − g r e e d y \epsilon-greedy ϵgreedy策略根据 Q Q Q选择当前状态s‘下的动作a’
  • Q ( s , a ) < − − Q ( s , a ) + α [ r + γ Q ( s ′ , a ′ ) − Q ( s , a ) ] Q(s,a)<--Q(s,a)+\alpha[r+\gamma Q(s',a')-Q(s,a)] Q(s,a)<Q(s,a)+α[r+γQ(s,a)Q(s,a)]
  • s < − − s ′ , a < − − a ′ s<--s',a<--a' s<s,a<a
  • end for
  • end for

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

import matplotlib.pyplot as plt
import numpy as np
from 代码测试 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]))  # action为0  1 2 3 放在change里面的二维数组选择  为上下左右
		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  # 返回的是序号

然后我们来实现 S a r s a Sarsa Sarsa算法,主要维护一个表格Q_table(),用来储存当前策略下所有状态动作对的价值,在用 S a r s a Sarsa Sarsa算法和环境交互时,用 ϵ − \epsilon- ϵ贪婪策略进行采样,在更新 S a r s a Sarsa Sarsa算法时,使用时序差分公式。我们默认终止状态时所有动作的价值都是0,这些价值在初始化为0后就不会进行更新。

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  # epsilon-贪婪策略中的参数

    def take_action(self, state):  # 选取下一步的操作,具体实现为epsilon-贪婪
        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)]  # 四个动作的值都为0 是一个矩阵
        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):
        td_error = r + self.gamma * self.Q_table[s1, a1] - self.Q_table[s0, a0]
        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 = []  # 记录每一条序列的回报
# print(return_list)

for i in range(10):  # 显示10个进度条
    # tqdm的进度条功能
    with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:   # 每个进度条的长度为total 500/10=50
        for i_episode in range(int(num_episodes / 10)):  # 每个进度条的序列数
            episode_return = 0
            state = env.reset()  # 回到原位置
            action = agent.take_action(state)  # 根据epsilon贪心策略进行下一动作的选取
            done = False
            while not done:  # 进行循环直到下一个位置在悬崖或者目标 即done=true
                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)  # 更新Q_table表格
                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)  # 1s更新一个序列

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, 725.04it/s, episode=50, return=-119.400]
Iteration 1: 100%|██████████| 50/50 [00:00<00:00, 1563.04it/s, episode=100, return=-63.000]
Iteration 2: 100%|██████████| 50/50 [00:00<00:00, 3341.49it/s, episode=150, return=-51.200]
Iteration 3: 100%|██████████| 50/50 [00:00<00:00, 4178.68it/s, episode=200, return=-48.100]
Iteration 4: 100%|██████████| 50/50 [00:00<00:00, 4176.68it/s, episode=250, return=-35.700]
Iteration 5: 100%|██████████| 50/50 [00:00<00:00, 6266.70it/s, episode=300, return=-29.900]
Iteration 6: 100%|██████████| 50/50 [00:00<00:00, 6265.20it/s, episode=350, return=-28.300]
Iteration 7: 100%|██████████| 50/50 [00:00<00:00, 7168.77it/s, episode=400, return=-27.700]
Iteration 8: 100%|██████████| 50/50 [00:00<00:00, 6266.89it/s, episode=450, return=-28.500]
Iteration 9: 100%|██████████| 50/50 [00:00<00:00, 6128.08it/s, episode=500, return=-18.900]

Sarsa算法最终收敛得到的策略为:
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 ooo> ^ooo ooo> ooo> ooo> ooo> ^ooo ^ooo ooo> ooo> ovoo 
^ooo **** **** **** **** **** **** **** **** **** **** EEEE 

5.4 多步 S a r s a Sarsa Sarsa 算法

(采用多步 S a r s a Sarsa Sarsa算法主要是因为蒙特卡洛算法需要很多的数据才能做到无偏估计 所有结合两种方法的优势就是多次采用时序差分算法,这样可以弥补时序差分是有偏的缺陷)

蒙特卡洛方法利用当前状态之后每一步的奖励而不使用任何价值估计。那它们之间的区别是什么呢?总的来说,蒙特卡洛方法是无偏的,但是具有比较大的方差,因为每一步的状态转移都有不确定性,而每一步状态采取的动作所得到的不一样的奖励最终都会加起来,这会极大影响最终的价值估计;时序差分算法具有非常小的方差,因为只关注了一步状态转移,用到了一步的奖励,但是他是有偏的,因为用到了下一个状态的价值估计而不是其真实的价值。那么有没有什么方法可以结合二者的优势呢?答案是多步时序差分!多步时序差分的意思是使用n步的奖励,然后使用之后状态的价值估计。用公式表示,将

G t = r t + γ Q ( s t + 1 , a t + 1 ) G_t=r_t+\gamma Q(s_{t+1},a_{t+1}) Gt=rt+γQ(st+1,at+1)

替换成:

G t = r t + γ r t + 1 + ⋯ + γ n Q ( s t + n , a t + n ) G_t=r_t+\gamma r_{t+1}+\cdots +\gamma ^nQ\left( s_{t+n},a_{t+n} \right) Gt=rt+γrt+1++γnQ(st+n,at+n)

于是相应存在一种多步 S a r s a Sarsa Sarsa算法,它把 S a r s a Sarsa Sarsa算法中的动作价值函数的更新公式(参见3.5节)

Q ( s t , a t ) < − − Q ( s t , a t ) + α [ r t + γ Q ( s t + 1 , a t + 1 ) − Q ( s t , a t ) ] Q(s_t,a_t)<--Q(s_t,a_t)+\alpha[r_t+\gamma Q(s_{t+1},a_{t+1})-Q(s_t,a_t)] Q(st,at)<Q(st,at)+α[rt+γQ(st+1,at+1)Q(st,at)]

替换成:

Q ( s t , a t ) < − − Q ( s t , a t ) + α [ r t + γ r t + 1 + ⋯ + γ n Q ( s t + n , a t + n ) − Q ( s t , a t ) ] Q(s_t,a_t)<--Q(s_t,a_t)+\alpha[r_t+\gamma r_{t+1}+\cdots +\gamma ^nQ\left( s_{t+n},a_{t+n} \right)-Q(s_t,a_t)] Q(st,at)<Q(st,at)+α[rt+γrt+1++γnQ(st+n,at+n)Q(st,at)]

我们接下来用代码实现多步(n步) S a r s a Sarsa Sarsa 算法。在 S a r s a Sarsa Sarsa 代码的基础上进行修改,引入多步时序差分计算。

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])  # 将Q_table进行初始化操作
        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):  # 多步体现在更新Q值上
        self.state_list.append(s0)
        self.action_list.append(a0)
        self.reward_list.append(r)
        if len(self.state_list) == self.n:  # 若保存的数据可以进行n步更新  即 保存的状态值有n个了
            G = self.Q_table[s1, a1]  # 得到Q(s_{t+n}, a_{t+n})
            for i in reversed(range(self.n)):  # 将n进行倒序然后取出来
                G = self.gamma * G + self.reward_list[i]  # 不断向前计算每一步的回报
                # 如果到达终止状态,最后几步虽然长度不够n步,也将其进行更新
                if done and i > 0:
                    s = self.state_list[i]
                    a = self.action_list[i]
                    self.Q_table[s, a] += self.alpha * (G - self.Q_table[s, a])
            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 * (G - self.Q_table[s, a])
        if done:  # 如果到达终止状态,即将开始下一条序列,则将列表全清空
            self.state_list = []
            self.action_list = []
            self.reward_list = []

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()
print(agent.Q_table)

该程序中Q值的计算是倒叙 是为了叠加相乘 γ \gamma γ

**注意: S a r s a Sarsa Sarsa中步数的选择非常非常重要,不能让 S a r s a Sarsa Sarsa的数值大于每一次循环的序列数 因为不然可能一致让第一个状态当作需要更新的Q值 **

5.5 Q-learning算法

除了 S a r s a Sarsa Sarsa,还有一种非常著名的基于时序差分算法的强化学习算法——Q-learning。Q-learning 和 S a r s a Sarsa Sarsa 的最大区别在于 Q-learning 的时序差分更新方式为

Q ( s t , a t ) < − − Q ( s t , a t ) + α [ R t + γ m a a x Q ( s t + 1 , a ) − Q ( s t , a t ) ] Q(s_t,a_t)<--Q(s_t,a_t)+\alpha[R_t+\gamma \underset{a} max Q(s_{t+1},a)-Q(s_t,a_t)] Q(st,at)<Q(st,at)+α[Rt+γamaxQ(st+1,a)Q(st,at)]

Q-learning 算法的具体流程如下:

  • 初始化 Q ( s , a ) Q(s,a) Q(s,a)
  • for 序列 e = 1 − − > E e = 1-->E e=1>E do
  • ​ 得到初始状态s
  • for时间步 t = 1 − − > E t=1-->E t=1>E do
  • ​ 用 ϵ − g r e e d y \epsilon-greedy ϵgreedy策略根据 Q Q Q选择当前状态s下的动作a
  • ​ 得到环境反馈的 r , s ′ r,s' r,s
  • Q ( s , a ) < − − Q ( s , a ) + α [ r + γ m a ′ a x Q ( s ′ , a ′ ) − Q ( s , a ) ] Q(s,a)<--Q(s,a)+\alpha[r+\gamma \underset{a'} max Q(s',a')-Q(s,a)] Q(s,a)<Q(s,a)+α[r+γamaxQ(s,a)Q(s,a)]
  • s < − − s ′ s<--s' s<s
  • end for
  • end for

我们可以用价值迭代的思想来理解Q-learning,即Q-learning是直接在估计 Q ∗ Q^* Q,因为动作价值函数的贝尔曼最优方程是

Q ∗ ( s , a ) = r ( s , a ) + γ Σ s ′ ∈ S P ( s ′ ∣ s , a ) max ⁡ a ′ Q ∗ ( s ′ , a ′ ) Q^*\left( s,a \right) =r\left( s,a \right) +\gamma \underset{s'\in S}{\varSigma}P\left( s'|s,a \right) \underset{a'}{\max}Q^*\left( s',a' \right) Q(s,a)=r(s,a)+γsSΣP(ss,a)amaxQ(s,a)

S a r s a Sarsa Sarsa估计当前 ϵ − \epsilon- ϵ贪婪策略的动作价值函数。需要强调的是,Q-learning的更新并非必须使用当前贪心策略 a r g m a ′ a x arg \underset{a'}max argamax采样得到的数据,因为给定任意任意 ( s , a , r , s ′ ) (s,a,r,s') (s,a,r,s)都可以直接根据公式来更新Q,为了探索,我们通常使用一个 ϵ − \epsilon- ϵ贪婪策略来与环境交互。 S a r s a Sarsa Sarsa必须使用当前 ϵ − \epsilon- ϵ贪婪策略采样的得到的数据,因为它的更新中用到的 Q ( s ′ , a ′ ) Q(s',a') Q(s,a) a ′ a' a是当前策略在 s ′ s' s下的动作。我们称 S a r s a Sarsa Sarsa是在线策略(on-policy)算法,称Q-learning是离线策略(off-policy),这两个概念在强化学习中也非常重要。

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

我们称采样数据的策略为行为策略(behavior policy),称用这些数据来更新的策略为目标策略(traget policy)。在线策略(on-policy)算法表示行为策略和目标策略是同一个策略;而离线策略(off-policy)算法表示行为策略和目标策略不是同一个策略。 S a r s a Sarsa Sarsa是典型的在线策略算法,而Q-learning是典型的离线策略算法。判断二者类别的一个重要手段是看计算时序差分的价值目标的数据是否来自当前的策略,如图5-1所示。具体而言:

  • 对于 S a r s a Sarsa Sarsa,它的更新公式必须使用来自当前策略采样得到的五元组 ( s , a , r , s ′ , a ′ ) (s,a,r,s',a') (s,a,r,s,a),因此他是在线策略学习;
  • 对于Q-learning,它的更新公式使用的是四元组 ( s , a , r , s ′ ) (s,a,r,s') (s,a,r,s)来更新当前状态动作对的价值 Q ( s , a ) Q(s,a) Q(s,a),数据中的s和a是给定的条件, r r r s ′ s' s皆由环境采样得到,该四元组并不需要一定是当前策略采样得到的数据,也可以来自行为策略,因此它是离线策略算法。

在这里插入图片描述

离线策略算法能够重复使用过往训练样本,往往具有更小的样本复杂度。

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]))  # action为0  1 2 3 放在change里面的二维数组选择  为上下左右
        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   # 返回的是序号


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()


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):
        td_error = r + self.gamma * self.Q_table[s1].max() - self.Q_table[s0, a0]
        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
            """和sarsa最大的区别在这里,即动作的选择,并且下一个状态价值是根据最大的动作价值选择的,并不是根据策略得到的动作选择的  所以不是实时的"""
            while not done:
                action = agent.take_action(state)
                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()

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, 698.35it/s, episode=50, return=-105.700]
Iteration 1: 100%|██████████| 50/50 [00:00<00:00, 1726.73it/s, episode=100, return=-70.900]
Iteration 2: 100%|██████████| 50/50 [00:00<00:00, 2506.97it/s, episode=150, return=-56.500]
Iteration 3: 100%|██████████| 50/50 [00:00<00:00, 3342.39it/s, episode=200, return=-46.500]
Iteration 4: 100%|██████████| 50/50 [00:00<00:00, 3856.26it/s, episode=250, return=-40.800]
Iteration 5: 100%|██████████| 50/50 [00:00<00:00, 4557.84it/s, episode=300, return=-20.400]
Iteration 6: 100%|██████████| 50/50 [00:00<00:00, 5031.43it/s, episode=350, return=-45.700]
Iteration 7: 100%|██████████| 50/50 [00:00<00:00, 5541.72it/s, episode=400, return=-32.800]
Iteration 8: 100%|██████████| 50/50 [00:00<00:00, 7121.30it/s, episode=450, return=-22.700]
Iteration 9: 100%|██████████| 50/50 [00:00<00:00, 6268.58it/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 

Process finished with exit code 0

在这里插入图片描述

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

5.6 小结

本章介绍了无模型的强化学习中的一种非常重要的算法——时序差分算法。时序差分算法的核心思想是用对未来动作选择的价值估计来更新对当前动作选择的价值估计,这是强化学习中的核心思想之一。本章重点讨论了 Sarsa 和 Q-learning 这两个最具有代表性的时序差分算法。当环境是有限状态集合和有限动作集合时,这两个算法非常好用,可以根据任务是否允许在线策略学习来决定使用哪一个算法。 值得注意的是,尽管离线策略学习可以让智能体基于经验回放池中的样本来学习,但需要保证智能体在学习的过程中可以不断和环境进行交互,将采样得到的最新的经验样本加入经验回放池中,从而使经验回放池中有一定数量的样本和当前智能体策略对应的数据分布保持很近的距离。如果不允许智能体在学习过程中和环境进行持续交互,而是完全基于一个给定的样本集来直接训练一个策略,这样的学习范式被称为离线强化学习(offline reinforcement learning),第 18 章将会介绍离线强化学习的相关知识。

t q d m tqdm tqdm模块

Tqdm 是一个快速,可扩展的Python进度条,可以在 Python 长循环中添加一个进度提示信息,用户只需要封装任意的迭代器 tqdm(iterator)。

总之,它是用来显示进度条的,很漂亮,使用很直观(在循环体里边加个tqdm),而且基本不影响原程序效率。名副其实的“太强太美”了!这样在写运行时间很长的程序时,是该多么舒服啊!

作用:显示程序的运行进度

1.传入可迭代对象

用法:

from tqdm import tqdm
import time


print("Start : %s" % time.ctime())
for i in tqdm(range(1000)):
    time.sleep(0.01)
print("End : %s" % time.ctime())

运行结果如下:

Start : Tue Nov 14 16:13:17 2023
100%|██████████| 1000/1000 [00:15<00:00, 64.13it/s]
End : Tue Nov 14 16:13:32 2023

进度条每0.01秒前进一次,运行完此次程序大概用时1000*0.01=10s 实际用时32-17=15s 因为还有for i in tqdm(range(1000)):代码的用时

使用下列程序是一样的

from tqdm import trange
for i in trange(1000):
    time.sleep(.01)

结果是:

100%|██████████| 1000/1000 [00:15<00:00, 64.20it/s]

2.为进度条设置描述

for循环外部初始化tqdm,可以打印其他信息:

pbar = tqdm(["a", "b", "c", "d"])

for char in pbar:
    pbar.set_description("Processing %s" % char)  # 设置描述
    time.sleep(10)  # 每个任务分配10s

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

输出是按照tqdm中字符串的数据进行字符串进度条加载的

且输出进度条的后面是说明每10s进行一次进度条的更新 (这四个进度条其实是一个进行更新的)

3.手动控制进度

with tqdm(total=200) as pbar:
    for i in range(20):
        pbar.update(10)
        time.sleep(1)

第一行代码可以认为是

pbar = tqdm(rang(200))

然后将这200个数字分为20组进行进度条的更新,一组10个 且每组更新延迟时间为一秒

# 结果如下,一共更新了20次
0%|          | 0/200 [00:00<?, ?it/s]

 10%|| 20/200 [00:00<00:00, 199.48it/s]

 15%|█▌        | 30/200 [00:00<00:01, 150.95it/s]

 20%|██        | 40/200 [00:00<00:01, 128.76it/s]

 25%|██▌       | 50/200 [00:00<00:01, 115.72it/s]

 30%|███       | 60/200 [00:00<00:01, 108.84it/s]

 35%|███▌      | 70/200 [00:00<00:01, 104.22it/s]

 40%|████      | 80/200 [00:00<00:01, 101.42it/s]

 45%|████▌     | 90/200 [00:00<00:01, 98.83it/s] 

 50%|█████     | 100/200 [00:00<00:01, 97.75it/s]

 55%|█████▌    | 110/200 [00:01<00:00, 97.00it/s]

 60%|██████    | 120/200 [00:01<00:00, 96.48it/s]

 65%|██████▌   | 130/200 [00:01<00:00, 96.05it/s]

 70%|███████   | 140/200 [00:01<00:00, 95.25it/s]

 75%|███████▌  | 150/200 [00:01<00:00, 94.94it/s]

 80%|████████  | 160/200 [00:01<00:00, 95.08it/s]

 85%|████████▌ | 170/200 [00:01<00:00, 93.52it/s]

 90%|█████████ | 180/200 [00:01<00:00, 94.28it/s]

 95%|█████████▌| 190/200 [00:01<00:00, 94.43it/s]

100%|██████████| 200/200 [00:02<00:00, 94.75it/s]

将代码更改为:

with tqdm(total=200) as pbar:
    for i in range(20):
        pbar.update(5)
        time.sleep(1)

进度条的总长度是200 循环进行20次 每次更新5个进度条 循环结束一共是100个进度条 进度为50%

运行中的一个结果:

在这里插入图片描述

运行结束:

在这里插入图片描述

4.tqdm的write方法

bar = trange(10)
for i in bar:
    time.sleep(0.1)
    if not (i % 3):
        tqdm.write("Done task %i" % i)

输出结果是 每次进度条的后面都写上 Done task %i这个内容


 10%|| 1/10 [00:00<00:00,  9.98it/s]Done task 0
 40%|████      | 4/10 [00:00<00:00,  9.17it/s]Done task 3
 70%|███████   | 7/10 [00:00<00:00,  9.21it/s]Done task 6
100%|██████████| 10/10 [00:01<00:00,  9.22it/s]
Done task 9

5.自定义进度条显示信息

from random import random, randint

with trange(10) as t:
    for i in t:
        # 设置进度条左边显示的信息
        t.set_description("GEN %i" % i)
        # 设置进度条右边显示的信息
        t.set_postfix(loss=random(), gen=randint(1, 999), str="h", lst=[1, 2])
        time.sleep(0.1)

time.sleep()

time.sleep()函数推迟调用线程的运行,可通过参数sesc(秒数),表示进程挂起的时间

语法:

time.sleep(t)

t:推迟执行的秒数

该函数没有返回值

# -*- coding:utf-8 -*-
from 代码测试 import tqdm
import time

for i in tqdm(range(100)):
	# print(i)

	print("Start : %s" % time.ctime())
	time.sleep(10)

	print("End : %s" % time.ctime())

在这里插入图片描述

可以看到输出的结果每次都相隔10秒

with as语句

基本格式是:

with 表达式 [as target]:
    代码块

此格式中,用 [] 括起来的部分可以使用,也可以省略。其中,target 参数用于指定一个变量,该语句会将 expression 指定的结果保存到该变量中。with as 语句中的代码块如果不想执行任何语句,可以直接使用 pass 语句代替。
with as 即 target = 表达式

with as 语句的作用主要如下:
1、解决异常退出时资源释放的问题;
2、解决用户忘记调用close方法而产生的资源泄漏问题;

with open(args.labels_file, 'w') as f:
    for dirname in dirnames:
        f.write(dirname + '\n')

reversed

reversed()是python内置函数之一,其功能是对于给定的序列(包括列表、元组、字符串以及range(n)区间),该函数可以返回一个逆序列的迭代器(用于遍历该逆序序列)

#将列表进行逆序
print([x for x in reversed([1,2,3,4,5])])
#将元组进行逆序
print([x for x in reversed((1,2,3,4,5))])
#将字符串进行逆序
print([x for x in reversed("abcdefg")])
#将 range() 生成的区间列表进行逆序
print([x for x in reversed(range(10))])

输出:

[5, 4, 3, 2, 1]
[5, 4, 3, 2, 1]
['g', 'f', 'e', 'd', 'c', 'b', 'a']
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

扩展

无偏估计

估计是用样本统计量(可以理解为随机抽样)来估计总体参数时的一种无偏推断。

无偏估计的要求就是:估计出来的参数的数学期望等于被估计参数的真实值。(如: X ˉ \bar X Xˉ是总体参数 μ \mu μ的估计量,而 μ \mu μ是被估计参数)(无偏性是一种评价估计量优良性的准则)

无偏估计的意义:在多次重复下,估计量的平均值 ≈ \approx 被估计参数真值

所以可以看出:估计量也是一个变量

方差的无偏估计是:

σ 2 = 1 n − 1 Σ ( x i − X ˉ ) \sigma ^2=\frac{1}{n-1}\varSigma \left( x_i-\bar{X} \right) σ2=n11Σ(xiXˉ)

无偏估计的直观理解

在这里插入图片描述

判断估计量好坏的标准

  • 无偏
  • 有效
  • 一致

有效性

有效性越高就越说明,估计量的方差小,估计量更靠近目标值

一致性

所以蒙特卡洛算法就是用状态价值的期望即平均值来近似状态价值的真实值的

r x in reversed([1,2,3,4,5])])
#将元组进行逆序
print([x for x in reversed((1,2,3,4,5))])
#将字符串进行逆序
print([x for x in reversed(“abcdefg”)])
#将 range() 生成的区间列表进行逆序
print([x for x in reversed(range(10))])


输出:

```python
[5, 4, 3, 2, 1]
[5, 4, 3, 2, 1]
['g', 'f', 'e', 'd', 'c', 'b', 'a']
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

扩展

无偏估计

估计是用样本统计量(可以理解为随机抽样)来估计总体参数时的一种无偏推断。

无偏估计的要求就是:估计出来的参数的数学期望等于被估计参数的真实值。(如: X ˉ \bar X Xˉ是总体参数 μ \mu μ的估计量,而 μ \mu μ是被估计参数)(无偏性是一种评价估计量优良性的准则)

无偏估计的意义:在多次重复下,估计量的平均值 ≈ \approx 被估计参数真值

所以可以看出:估计量也是一个变量

方差的无偏估计是:

σ 2 = 1 n − 1 Σ ( x i − X ˉ ) \sigma ^2=\frac{1}{n-1}\varSigma \left( x_i-\bar{X} \right) σ2=n11Σ(xiXˉ)

无偏估计的直观理解

[外链图片转存中…(img-5IuhnosJ-1700043753771)]

判断估计量好坏的标准

  • 无偏
  • 有效
  • 一致

有效性

有效性越高就越说明,估计量的方差小,估计量更靠近目标值

一致性

所以蒙特卡洛算法就是用状态价值的期望即平均值来近似状态价值的真实值的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值