Bootstrap

10天速通强化学习-004

思考

动态规划算法:

有前提条件,需要知道状态转移函数和奖励函数,这就相当于知道了MRP,智能体不需要和外界环境互动就可以接出最优价值或策略

但是:

实际场景中数据分布未知,只能靠与环境交互才行---这种类型:无模型的强化学习--时序差分算法

时序差分算法(TD算法)

原理:蒙特卡洛和动态规划

公式:

V\left(s_{t}\right) \leftarrow V\left(s_{t}\right)+\alpha\left[G_{t}-V\left(s_{t}\right)\right]

与蒙特卡洛相比,不再依赖状态被访问的次数,而是依赖学习率\alpha,这里的\alpha是取值在(0,1)之间(这里为什么不适用次数而是用学习率,是因为变成学习率之后,\alpha只代表一个固定的补偿,不考虑样本出现的频次差异,也就是说不用严格取期望)

更进一步的:

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]

这里可以这样变的原因:

\begin{aligned} V_\pi(s) & =\mathbb{E}_\pi\left[G_t \mid S_t=s\right] \\ & =\mathbb{E}_\pi\left[\sum_{k=0}^{\infty} \gamma^k R_{t+k} \mid S_t=s\right] \\ & =\mathbb{E}_\pi\left[R_t+\gamma \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} \mid S_t=s\right] \\ & =\mathbb{E}_\pi\left[R_t+\gamma V_\pi\left(S_{t+1}\right) \mid S_t=s\right] \end{aligned}

这个公式什么意思呢:第一行相当于求的是状态s时刻的所有回报的期望--蒙特卡洛方法

最后一行相当于求的是:在t时刻是状态s的情况下,t+1时刻的所有状态的状态价值函数衰减和 + t时刻到达t+1时刻的即使奖励  的期望---时序差分

对比一下可以看到:蒙特卡洛计算某一个状态价值函数时,需要将从这个状态出发的这条序列走完,遍历完序列中所有的状态,得到所有状态的状态价值函数才能得到这条序列的期望回报。(针对某一个序列而言,并非是所有的从这个状态出发的序列,意思是序列总数不变,变得是遍历序列中状态的个数)但是时序差分不用,时序差分用的是固定的步长也就是学习率和下一个状态的状态价值函数就可以了,就可以计算出当前状态的状态价值函数,也就是即采即用,可以证明最终收敛到策略\pi的状态价值函数

最大的区别,计算某条序列的价值时,不需要采集完所有的状态了

这两个是一样的,一个算的是从根节点出发的所有回报期望,一个算的是所有分支的期望回报的期望

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]

在这个公式中,r_{t}+\gamma V\left(s_{t+1}\right)-V\left(s_{t}\right)ji就是时序差分误差,时序差分算法:时序差分误差和步长的乘积===状态价值的更新量(物理意义也很好理解)

时序差分算法有两种;Sarsa算法和Q-learning 算法

Sarsa算法

回想一下,动态规划算法,不管是价值迭代(只有一次策略评估)和策略迭代,其实都是两个部分:策略评估和策略提升两个部分,经过上面的讨论,策略评估已经有了,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]

现在就只需要进行策略提升了,自然的我们能想到的是取最大的状态动作对

$Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha[r_t+\gamma Q(s_{t+1},a_{t+1})-Q(s_t,a_t)]$

这是时序差分的动作价值对的更新公式,推理不在解释

但是我们要明白:时序差分算法估算出来的状态价值函数并不准确(相比与蒙特卡罗算法,TD算法连单条序列都没有走完就开始估计,虽然调整学习率可以更快的收敛,但是估计值还是相对来说不准确的),所以如果一直选择最大的动作价值函数的动作,就会可能遗漏掉最佳策略,所以采用不完全贪婪策略

 这个公式表示,有\epsilon的概率采用动作价值最大的那个动作,另外有1-\epsilon的概率从动作空间中随机采取一个动作

伪代码:

  • 初始化Q(s,a)--------------在从这里初始化就可以避免初始化策略和初始化状态价值函数
  •   for 序列 e = 1 \rightarrow E do: ---序列执行E次初始化次数e=1,每次循环的都是一个序列-回合
    • 得到初始状态s
    • 用不完全贪婪根据Q选择当前状态s下的a ----第一个时刻也来不完全贪婪策略
      • for 时间步 t=1 \rightarrow T  do   --- 控制每个序列的时间步:
        • 得到环境反馈的r,s'
        • 用不完全贪婪根据Q,选择当前状态s'下的动作a'
        • $Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha[r_t+\gamma Q(s_{t+1},a_{t+1})-Q(s_t,a_t)]$
        • s\leftarrow s',a\leftarrow a'
      • end for (相当于隐式更新了策略,既可以初始化策略,又可以更新策略)
  • end for (相当于策略提升阶段,每次大循环都是一个新序列)

代码

环境设置

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

实现Sarsa算法,从伪代码中我们可以看出主要就是状态动作对的更新,所以我们需要维护一个表格来存储这些东西。智能体和环境交互时使用不完全贪婪策略,更新时使用时序差分的公式

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)表格
        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)]
        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

运行

这里在计算单个回合的回报的时候并没有进行衰减求和,这样的话 可以反应出在这个序列中从开始到结束的总奖励之和,就是每一个动作中的独立表现。但是在计算长期价值的时候需要加权,比如更新Q值

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)):  # 每个进度条的序列数
            #初始化原点状态
            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)
                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('Sarsa on {}'.format('Cliff Walking'))
plt.show()

在进行500条序列的学习后,已经非常接近最优策略了,打印策略

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

多步Sarsa算法

思考:

蒙特卡洛:  --无偏,

$V^\pi(s)=\mathbb{E}_\pi[G_t|S_t=s]\approx\frac{1}{N}\sum_{i=1}^NG_t^{(i)}$

使用的是每一步的奖励,但是不使用任何价值估计(这也是为什么Sarsa编程时单个序列中不衰减求和的原因)

时序差分:--有偏,

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]

只利用一步奖励和下一个状态的价值估计 

结合:多步时序差分--使用n步奖励,然后使用后续状态的价值估计

Sarsa:

$G_t=r_t+\gamma Q(s_{t+1},a_{t+1})$

替换:

$G_t=r_t+\gamma r_{t+1}+\cdots+\gamma^nQ(s_{t+n},a_{t+n})$

这就是多步Sarsa算法;

$Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha[r_t+\gamma r_{t+1}+\cdots+\gamma^nQ(s_{t+n},a_{t+n})-Q(s_t,a_t)]$ 

修改代码:

与之前的代码相比,就是更新的时候进行了变化,需要维护的列表变多了

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):
        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 = rG + R
            G = self.Q_table[s1, a1]  # 得到Q(s_{t+n}, a_{t+n})
            for i in reversed(range(self.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的主要更新步骤
            #公式 Q(s,a) = Q(s,a) + a(G - Q(s,a))
            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()

 5步更快收敛

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

 注意:Sarsa算法的策略会在远离悬崖的一边行走,保证最大的安全性

 Q-learning 算法

思考

Sarsa算法通过智能体与外界交互,得到数据之后更新本策略,一直到得到最优的策略,经过乙烯类复杂的操作求得是\pi ^{*}V^{*},Q*

我们可不可以直接在智能体与外界交互时,就直接求解Q*--Q-learning

由动作价值函数得贝尔曼最优公式:

Q^*(s, a)=r(s, a)+\gamma \sum_{s^{\prime} \in \mathcal{S}} P\left(s^{\prime} \mid s, a\right) \max _{a^{\prime}} Q^*\left(s^{\prime}, a^{\prime}\right)

可以推导出

Q\left(s_t, a_t\right) \leftarrow Q\left(s_t, a_t\right)+\alpha\left[R_t+\gamma \max _a Q\left(s_{t+1}, a\right)-Q\left(s_t, a_t\right)\right]

这就是Q-learning算法的时序差分的更新方式,需要指出的是,Q*的更新并非必须使用当前贪心策略采样得到的数据。它可以从历史经验池中寻找。

举个例子:迷宫

Q-learning : 智能体要找到起点到终点的最优路径,,该算法通过不断估计每个状态下采取不同动作的价值,来趋近于最优策略下的动作价值函数

Sarsa:该算法在每一步都是基于当前不完全贪婪的策略去评估动作价值,以改进这个策略

伪代码:

  • 初始化Q(s,a)
  • for 序列 e = 1 \leftarrow E do:
    • 得到初始状态s
    • for 时间步 t = 1 \rightarrow T do:
      • 用不完全贪婪策略根据Q选择当前状态s下的动作a 
      • 得到环境的反馈r ,s'
      • Q\left(s, a\right) \leftarrow Q\left(s, a\right)+\alpha\left[R_t+\gamma \max _a Q\left(s', a'\right)-Q\left(s, a\right)\right] (这就是最大的不同,该算法可以采取不同的策略,例如,在一个机器人导航的场景中,智能体从历史经验池中随机抽取数据(这些都是某一个策略下的最大值)来更新Q值,这些数据可能是之前使用不同探索程度的策略收集的,并不一定是当前贪心策略收集的)
      • s\leftarrows'
    • end for 
  • end for

所以我们思考,Q-learning 算法和Sarsa算法的编程区别在哪,其实就在更新Q_table[] 列表上,Sarsa算法使用的是当前策略下采样得到的数据来更新Q(s,a)才更新,但是Q-learning是不管策略了(其实就是其他序列下的下一个状态的最大值)来更新了

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

变化就在这里了,其他的差不多就一样了

代码

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
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_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])

 Q-learning 贴着悬崖走,为什么?理解一下;贴着悬崖走的策略最优,因为Sarsa算法始终是根据不完全贪婪策略来平衡和利用,不太能接受掉入悬崖这个策略,因为掉下去之后maxQ(s,a)会很小,但是Q-learning算法直接计算Q-learning的期望值,会有一定概率掉入悬崖。(有一句比较绕口的话:最大的不一定是最优的,但是最优的一定是最大的,前者针对某条序列,后者针对全部序列)

总结

个人理解:

Sarsa算法和多步Sarsa算法:使用越多的即时奖励来求解,,得到的值就越接近当前序列的真值(当前序列的真值就是蒙特卡洛算法中遍历所有状态得到的真值),也就说这两个算法,始终都是在求解某一个序列的真值。求完该序列之后来更新Q(s,a)--其实就是策略,才以此进行更新下一个序列(根据上一个策略的Q(s,a)来选取下一个策略的动作)。最后更新到新序列的真值最大。就完成了目标

而Q-learning算法不同,在某条序列中更新Q值得时候,不依赖于本次策略,可以基于经验回放池中的样本来学习下一个状态的最大值。以期望的得到最优的动作价值函数

在线策略和离线策略

Sarsa算法和多步Sarsa算法就是在线策略:它的行为策略(采样,用于与环境交互产生数据的策略)和目标策略(用于更新的策略)是同一个策略,即当前的不完全贪婪策略

Q-learning 算法就是离线策略,因为它的目标策略和行为策略是不一样的,简单来说就是更新时可以用其他序列中的当前状态的最大值。

判断

重大区别;计算时序差分的价值目标的数据是否来自当前的策略

 这个图已经很明白了,

  • 对于Sarsa,更新公式必须使用来自当前策略下采样得到的五元组(s,a,r,s',a')--在线(相当于一个序列单独作用)
  • 对于Q-learning,更新公式使用四元组(s,a,s',r)来更新当前状态动作对的价值Q(s,a),其中s和a是给定的条件,r和s'都是由环境采样得到(在本节中当前策略是不完全贪婪策略,而在一个序列中也使用的是不完全策略),该四元组不一定需要是当前策略的采样的数据,也可以是来自行为策略(行为策略;采样策略的数据,目标策略:利用这些数据来更新的策略)--离线(相当于多个序列一起使用)

注意

当环境是有限状态和有限动作的时候,非常好用(毕竟从动态规划演化过来的)。

离线策略--可以基于经验池学习,但是要保证智能体不断的与环境交互,讲采样得到的最新经验样本加入经验池中,从而使经验池中有一定数量的样本和当前智能体策略对应的数据分布保持很近的距离。

如果不允许与环境交互,就是一个给定样本的来训练策略,这就是离线强化学习。

 选做

我们可以打印出每一步的Q-table表格来看一下具体变化,迭代10轮

修改方式

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

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
        #打印更新后的状态动作函数
        print(self.Q_table)
        print("----------------------")

当前格数太大了,需要变换一下--->3*3

示例:可以自己写一下加深印象

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

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)表格
        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)]
        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

def visualize_Q_table(Q_table, ncol, nrow, episode):
    fig, axes = plt.subplots(nrow,  ncol, figsize=(ncol, nrow))
    for i in range(nrow):
        for j in range(ncol):
            state = i * ncol + j
            q_values = Q_table[state]
            ax = axes[i, j]
            ax.set_xticks([]) 
            ax.set_yticks([]) 
            # 定义颜色映射
            norm = mcolors.Normalize(vmin=-100, vmax=0)
            colors = [plt.cm.Reds(norm(q)) for q in q_values]
            # 绘制箭头
            if q_values[0] == max(q_values):
                ax.arrow(0.5,  0.5, 0, -0.4, head_width=0.1, head_length=0.1, fc=colors[0], ec=colors[0])
            if q_values[1] == max(q_values):
                ax.arrow(0.5,  0.5, 0, 0.4, head_width=0.1, head_length=0.1, fc=colors[1], ec=colors[1])
            if q_values[2] == max(q_values):
                ax.arrow(0.5,  0.5, -0.4, 0, head_width=0.1, head_length=0.1, fc=colors[2], ec=colors[2])
            if q_values[3] == max(q_values):
                ax.arrow(0.5,  0.5, 0.4, 0, head_width=0.1, head_length=0.1, fc=colors[3], ec=colors[3])
            # 显示 Q 值
            ax.text(0.5,  0.5, f'{np.max(q_values):.2f}',  ha='center', va='center', fontsize=8)
    plt.suptitle(f'Q-table  at Episode {episode}')
    plt.show() 

ncol = 3
nrow = 3
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 = []  # 记录每一条序列的回报
episode_count = 0
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)
                episode_count += 1
                # 可视化 Q_table 每一次更新
                visualize_Q_table(agent.Q_table, ncol, nrow, episode_count)
                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('Sarsa  on Cliff Walking')
plt.show() 

;