【强化学习】06周博磊强化学习纲要学习笔记——第三课下

今日课程提纲

接下来将介绍model-free control。就是当没法得到马尔科夫决策过程里面模型的情况下,如何去优化它的价值函数,如何去得到一个最佳的策略。这里我们将把之前我们介绍的policy iteration进行一个广义的推广,使它能够兼容MC和TD。

目录

  • 二、Model-free
    • [2.2 Model-free control](#2.2 Model-free control)
      • [2.2.1 policy iteration回顾](#2.2.1 policy iteration回顾)
      • [2.2.2 generalize policy iteration](#2.2.2 generalize policy iteration)
        • [2.2.2.1 MC方法求q table](#2.2.2.1 MC方法求q table)
        • [2.2.2.2 TD方法求q table](#2.2.2.2 TD方法求q table)
  • 三、总结
  • 四、代码

二、Model-free

2.2 Model-free control

2.2.1 policy iteration回顾

首先是一个policy iteration的复习,policy iteration由两部分组成。第一部分是通过一个迭代的过程去估计它的价值函数,就给定一个当前的policy π,然后估计它的价值函数。第二部分是我们得到了估计出来的价值函数过后,我们通过一个Greedy的办法去改进它的一个算法。所以这两个步骤是一个互相迭代的过程,我们逐渐就从初始化得到了一个最佳的v和跟π,就通过这个evaluation and improvement的互相迭代就逐渐改进。

但是这里面临一个问题,得到了一个价值函数过后,并不知道它的奖励函数以及状态转移,所以就没法估计它的Q函数。所以一个问题是,当不知道奖励函数以及状态转移矩阵的时候,如何进行策略的一个优化。所以这里就有一个广义的generalize的policy的一个方法。

2.2.2 generalize policy iteration

2.2.2.1 MC方法求q table

根据这两个步骤,policy evaluation以及policy improvement,这里我们在第一部分里面可以直接把用MC的一个方法去替代之前的DP的方法去估计这个Q函数。当我们得到Q函数过后,就可以通过Greedy的一个办法去改进它。所以这里是用MC方法去估计Q函数的一个算法。

这里假设是我们的每一个episode都有一个exploring starts。因为这个exploring starts就类似于我们每一个步骤,每一个状态都希望能采样能采到。所以就需要这个exploring starts作为一个保证。就我们所有的action,所有的状态都可以在无限步的执行过后采到,这样我们才能很好的去估计。

这里是具体的一个算法,通过蒙特卡罗采样的方法产生很多轨迹,然后每一个轨迹都可以算得它的价值。这样得到过后,去通过这个average的一个方法去估计它的这个Q函数。因为这个Q函数可以把它看成一个table,就这个表格它的横轴是状态,纵轴是action。这样就通过采样的方法,把这个表格上面每一个单元都的值都填上。就通过这个mean average的一个办法然后填上。当得到这个表格过后,就可以通过第二步的这个policy improvement,然后去选取它的更好的一个策略。所以这里的核心就是如何利用MC方法去填这个q table这个表格。


这里就牵涉到一个怎么能确保MC算法能有足够的探索。所以这里面临一个trade off,就面临一个exploration跟exploitation的一个trade off。在第一节课我也给大家介绍了exploration and exploitation的trade off,是在强化学习里面非常核心的一个问题。因为强化学习的核心思想就是让他在这个环境里面探索,这样可以获得最佳的策略。所以面临一个问题是怎么能确保这个agent在环境里面有更多、更好的探索。一个比较简单的方法,使得它能确保足够的探索,算法叫做ε-Greedy Exploration。所以这个算法大致意思说在每一步选取策略的时候有ε概率。这个ε在开始的时候是比较大的,比如说80%逐渐,然后它可以减小,然后到20%或者10%。

所以每次它概率有ε,比如说20%的概率,它随机选取一个行为,另外有1-ε,另外有80%的概率,他会采取这个Greedy的策略。因为Greedy策略可以确保你获取足够的奖励。这个ε的概率可以确保你对这个不同的行为足够的探索,有更高的概率获取可以获得更高奖励的行为。

可以把这个ε-Greedy写成一个概率的表达形式。这个等式是确保我们加和它还是一个概率。进一步推导,当follow这个ε-Greedy policy的时候,整个它的Q函数以及它的价值函数是单调递增的。

这里我给了一个简单的一个提示,感兴趣的同学可以进一步推导。就是当用蒙特卡罗以及这个ε-Greedy探索这个形式的时候,我们可以确保它的这个价值函数是monotonic单调改进。

所以这里是一个ε-Greedy的一个简单的算法表示。

刚开始这个q table是随机初始化的。所以我们MC的这个核心就是利用当前的这个策略,然后对这个环境进行探索,然后得到了一个一些轨迹。得到这些轨迹过后,可以开始更新。通过他得到了的return,然后通过这个incremental mean的方法去更新它的这个q table。q table里面有两个量,就是它的状态以及它的这个action,这样就可以估计出这个q table。当得到这个q table过后,进一步更新他的这个策略,就这个policy improvement,这样就可以得到下一阶段的这个策略。得到下一阶段的策略过后,又用更好的策略进行数据的采集。这样通过一个迭代的过程,就得到一个广义的一个policy iteration。

2.2.2.2 TD方法求q table

也可以把TD替代进去,因为MC是一种估计q table的方法,那么自然而然,也可以把TD这个方法估计进去。这里也是TD方法跟MC方法的一个对比,TD相对于MC来说的话,它的variance是比较低的。而且对于这个没有结束的游戏,已经可以开始更新它的这个q table,而且它可以处理不完整的序列。所以这里可以把TD也放到这个control loop里面去估计q table。然后再采取这个ε-Greedy的policy improvement。这样就可以在同一个episode没有结束的时候,就可以开始更新它已经采集到的状态。



接下来回忆一下TD prediction这个步骤。TD prediction给定了一个策略,然后去估计它的价值函数。所以这里采取的办法是TD(0) 的办法是根据当前策略,采取了一个行为,执行了这个行为,会观测到他的奖励以及进入到下一个状态。然后就可以构造出它的这个TD target,就是有它获取的奖励以及bootstrapping它下一步这个状态的值,然后作为它的TD target,然后更新它当前状态这个V(St)它的值。所以现在我们面临的问题是怎么把这个TD prediction的框架来估计它的这个action value function,就是它的这个q function。

1、on-policy TD control
Sarsa算法

所以我们这里有了这个on-policy TD control的一个算法,叫Sarsa。这个算法就是基于这个on-policy TD control。on-policy的意思是现在只有同一个policy,既利用这个policy来采集数据,这个policy也是我们优化的policy,所以这里只存在一个policy。为什么他名字叫Sarsa?

SARSA就是它这个过程,它需要采集到这种turbo,需要采集到有两个state,就是从你当前这个S开始。然后执行了一个action,就是第一个A然后会得到一个reward,然后会进入下一个状态。然后现在进一步执行一个action,这样就得到了第二个A所以他就缩写过来,所以就变成Sarsa这个词了。所以这个Sarsa算法也是跟TD prediction类似的,它是直接去估计这个q table,估计这个q table的话也是我们构造出这个TD target的,就是由它已经得到了这个reward以及bootstrapping他下一步要更新的这个Q,然后来更新它的当前这个q table的这个值。当我们得到这个q table过后,然后就可以进行采取它这个greedy的这个策略,更新它的这个策略。

这也是Sarsa具体的算法:刚开始初始化这个q table,现在开始基于当前这个策略,然后执行命令。先采样,通过当前的这个q table采样一个A,就得到了Sarsa里面的第一个A。我们采取这个action a然后会得到一个奖励以及这个S'。S'就是它的进入到下一个状态。现在我们有个A',就是第二个A的出现。A'就是通过再一次我们通过这个策略,这个q table,然后来采样得到一个A'的行为。现在收集到所有的data过后,就既有了有了两个A以及两个S以及一个reward。然后我们就有了所有信息去更新这个q table的所有信息。更新过后,我们会往前走一步,所以当前这个状态S就会变成S',然后当前这个action也会变成这个A'。这样就通过一步一步就可以进行迭代更新。

n step Sarsa算法

之前我们说了,可以把TD算法扩展它的步数,所以可以得到多个步的Sarsa,n step Sarsa算法,就通过调整它往前走的步数。比如说我们一步Sarsa,就是说我们得到往前走一步过后,然后就开始更新它的这个TD target。然后也可以有多步,比如说走两步,然后two step Sarsa,然后就是得到两个实际得到的奖励,然后再booststrapping它这个Q的价值,Q值,然后更新它的这个TD target。所以这里也可以进一步推广到N。到整个结束过后,那么它这个Sarsa算法就变成MC的这种更新的一个方法。当我们多步n step Sarsa update得到过后,就可以进行这个q table的更新。


2、off-policy TD control

另外off-policy learning的概念就是说在策略的学习过程中,可以保留两种不同的策略。第一个策略是进行优化的这个策略,希望学到一个最佳的策略。另外一个策略是拿来探索的这个策略。所以第二个策略因为它属于探索策略,就可以让它更激进的去对这个环境探索。所以这个off-policy learning的概念是说需要去学习这个策略,policy π。但是我们利用的数据采集到的trajectories是用第二个策略μ产生的。所以这里有两个策略,一个是这个policy π,就target policy是我们需要去学习的policy,policy μ是我们的行为policy。我们利用策略μ然后去采集轨迹,采集数据。利用采集到的这个数据,然后再喂给我们target policy进行学习。

一个容易理解的例子,你可以把它看成是这个学习的过程,就是这个环境是一个在海上波涛汹涌的环境。但是我们这里学习的这个learning policy,可能他自己太胆小了,他没法直接去跟这个环境进行学习。所以我们这里有了第二次behavior policy,它是一个可能无畏这个风浪的海盗,它非常激进。然后可以在这个环境里面实际去探索,然后产生了很多经验。他就会把他实际产生的这个经验写下来,写成这个稿子,然后喂给这个learning policy。所以就可以让这个胆小的这个policy通过这个behavior policy得到经验教训,然后来进行学习。这样他就不用直接跟这个环境进行交互。

所以在这个off-policy learning的过程中,这些观测轨迹都是通过policy μ跟环境进行交互产生的。当我们得到这些轨迹过后,让我们去update这个policy π,需要去学习的这个target policy。这样的off-policy操作有很多好处

第一是可以学到最佳的策略,利用一个更激进的这exploratory policy,这样就会使得我们的学习效率也非常高。

第二则是这个框架是可以让我们学习其他意见的行为。就比如说我们采集到这些轨迹可能是人产生的。就通过模仿学习人的轨迹,这样我们可以学习,也可以是其他Agent产生的。

第三个好处是可以重用一些之前老的策略产生的轨迹。因为这一点是非常重要,因为在这个探索过程是需要消耗非常多的计算资源。需要很多计算机计算资源来做rollout(轨迹采样),产生这个轨迹。如果对于我们当前优化的,我们之前产生的轨迹不能利用的话,这样就浪费了很多资源。所以通过这个off-policy learning的方法,我们就可以使得之前比较老的这些轨迹产生的这些观测,产生的这些trajectory也可以继续存下来,然后继续用。这个思想也是我们下一次课或者下次课会介绍的这个deep Q-learning采取的思想。他就用一个replay buffer来包含之前比较老的轨迹产生的这些经验。然后我们带着这些老的经验进行采样,然后构建了新的training batch来更新我们的这个target policy。


Q-learning算法

所以这就是我们off policy Q-learning的算法。这里有两种policy,一种是behavior policy,另外一个是target policy。我们这里target policy π,就是说他直接利用他这个greedy,就直接让他在这个q table上面取他greedy是他的policy。所以对于某一个状态,那么它下一步的最佳策略就应该是这个arg max这个操作,就取它下一步可能得到所有状态。另外我们的这个behavior policy μ可以是一个随机的policy。

但是我们这里采取的是follow一个ε-Greedy,就让它这个behavior policy不至于它是完全随机的,它还是有些随机性。但是它也是基于我们这个q table组件在进行改进的,所以我们这里用ε-Greedy policy。所以我们这里看到有两种policy。一种是greedy policy以及另外一种是ε-Greedy policy。这两种policy在这个策略优化刚刚开始的时候是非常不同的。因为我们之前说过ε-Greedy可能是在刚刚开始的时候,这个ε值是非常大。它可能是百分之百或者90%的随机扰动,然后产生数据,然后再来学习。在训练,逐渐更接近收敛的时候,这个ε的值也会逐渐变小,变成10%。

所以这两个策略会在后期的时候是越来越像。当我们采取这个Q-learning的时候,就可以构造出它的这个Q-learning target。Q-learning target就会使得现在每一步,它后面采取的这个策略都应该是这个arg max这个操作。所以我们直接把这个arg max代入进来,然后进行一个变化。然后就会把这个arg max这个值放到外面,然后就是直接是取的max这个值。所以就构建出了它当前的这个TD target,Q-learning的这个TD target的要优化的值。所以我们应该把Q-learning这个update写成这个incremental learning的形式。这个TD target就变成这个max这个值

所以这个是我们Q-learning的算法。你可以发现当我们采取当前的这个行为过后,choose a from s using policy derived from q这里我们就是follow这个ε-Greedy的这个policy,得到了当前的这个action。然后我们采取当前的action,然后观测到了reward S'。

这里跟Sarsa算法很重要的不同是,我们并没有采样第二个action。因为第二个action是需要我们构造这个TD target的,所以在Sarsa里面我们是需要去遵从我们的这个target policy去采样第二个A。但是在这个 Q-learning 里面我们并没有直接去采样。我们这里采取的操作是直接去看那个 Q-table,然后取它的这个 max 的值,这样就构造出了它的这个 TD target,然后就可以对它的这个 Q 值进行一个优化。然后优化过后,我们现在把进入下一步的这个 S 的状态,继续作为新的当前状态,重复同样的更新过程。


Q-learning跟Sarsa的对比

这里可以把Q-learning跟Sarsa进行一个对比。这里Sarsa算法是on-policy TD control,Q-learning是off-policy TD control。这两者虽然只有一些很非常细小的一个差别,但是会决定这个两种算法他的行为是完全不同的。

就对于Sarsa来说,你可以发现这里他有两个action,就At跟At+1。这两个action都是通过他的一个同一个policy,然后采样出来,采样出来过后,Q才能进行更新。但是对于Q-learning其实只执行了第一个action。比如说当前这个action At。然后是从他的behavior policy里面采样出来的。然后他当前的这个At+1其实是它并没有采取这个行为,是它imagine出来的。使得这个值org max,即这个值达到极大化的那个action,应该是他下一步的这个action。所以就构造出了它的这个TD target。

有这个max的Operator在这个TD target里面,然后进行这个incremental learning,然后去nudge这个Q所以这是Sarsa跟q turning两者非常不同的一个地方。

把这个backup diagram进行一个对比。Sarsa只有一条路,它通过构造出当前这个S然后采样出来这个A然后得到这个奖励,然后到达这个S',然后再采样它的target policy,然后得到这个A',就这样就可以构造出它的这个更新了。但是对于这个q-learning,它有了这个SA采样过后,然后产生这个reward,它的S'。然后它这里有个Operator,就是max Operator,就是采样他当前要去的这个max Operator,然后作为他的下一步最可能的这个action。所以它在Sarsa里面,A跟A'都是从同一个policy里面产生的,所以它是on-policy。但是在Q-learning里面,它的A跟A'它是从不同policy里面产生。所以A会更exploration,但A'是直接从这个max Operator里面执行产生。

这里我提供了一个简单的一个Cliff work一个环境,可以对比出Sarsa跟Q-learning的算法得出的不同。这个环境是在这个great world里面,这个agent需要从这个S的这个格子开始,然后到达这个G这个格子。然后这里它在这个格子里面可以上下左右移动,然后它得避免这个Cliff这几个格子。如果它进入这个Cliff的这个格子过后,它就会得到-100的奖励。然后他每走一步会有一个-1的奖励。所以这里Sarsa出来的结果,他得到的最佳轨迹,结果会跟Q-learning非常不同。

因为Sarsa是on-policy learning,所以他会采取一个非常保守的一个策略。因为他如果掉到这个悬崖下去过后,然后他就会得到很负的奖励。所以他整个策略的学习,他会倾向于一个非常保守的一个策略。所以你看他最后收敛过后,他得到的这个reward,你就左下角这个值,R就是它决定这个轨迹。

你可以发现这个Sarsa选出来的这个policy,它就是会逐渐往上走,就走到非常上面,然后再到下面,然后再走下来。这样使这个agent尽量远离这个Cliff的位置。但是这个Q-learning它学出来的会非常激进,然后去学出一个沿着这个悬崖边上走,最后到达这个G的位置。但是在这个环境里面,它最佳策略就是尽量靠近这个Cliff。所以这个Q-learning它会采取一个非常激进的学习策略,这样就会他学到最佳的策略。

右下角的learning curve里面,在学习的过程中,这个Q-learning它其实一直他的这个learning curve是相对于Sarsa是要更低的。因为他采取的这个策略是非常激进,他有一个behavior policy是非常随机去探索这个环境,所以他有更大的概率掉到这个悬崖下面去,所以他这个learning reward是相对比较低的。但是Sarsa会保证一个比较保守的一个策略,所以它的moving average会比这个Q-learning更高。但是当整个策略整个学习过程完成过后,我们得到这个最佳策略会发现Q-learning得到这个最佳策略会更接近于实际的最佳策略。然后我在这个code base里面也提供了一个code,大家可以实际去是运行这个代码。

三、总结

我们对于之前的这个policy iteration,如果是用dynamic programming,用动态规划的方法来执行的话,那么就直接policy evaluation也是算他这个期望。对这个Q-learning,q police iteration也是把这个期望带进去。然后value iteration也是这个过程,就算它的expectation。如果我们这里用TD的方法,就用sample base的方法,就会产生新的这个TD target。就会这个target就会有它实际得到的奖励,以及这个bootstripping产生的这个value价值进行一个更新。然后基于他的这个更新策略的不同,这个Sarsa是on-policy learning,所以他会直接去执行,到下一步来进行这个bootstripping的更新。但是Q-learning采取更激进的更新,所以它会把这个max Operator放到它的这个TD target里面去。然后我们就我看出了基于这个算法,就你是用DP或者TD然后会得到不同的target。

这就是我们第三次课的内容,做一个简单的总结。我们分析了model-free prediction,就如何在一个马尔科夫决策过程里面,我们并没有模型的时候怎么去估计它的价值函数。然后我们进一步把这个model-free prediction扩展到这个model-free control。就给定一个未知的一个马尔科夫决策过程,我们并没有他的知道它的奖励函数以及它的转移矩阵。怎么通过Sarsa算法以及Q-learning算法对他进行控制,获取它的最佳策略。

四、代码

这里我提供首先是提供了这个Cliff work的这个例子。我们首先来看一下这个play work的代码,这里我们这个代码它实现了Sarsa、Q-learning两种方法。对于Sarsa算法,这里只有一种ε-Greedy policy.

你发现我们这里先采取第一个action,然后第二个action是直接从这个采样采出来的,然后得到了这两个action过后,我们就可以开始构造这个TD target。这里就构造出了它的TD他给然后我们算它的这个TD error,然后再对它的这个Q值进行更新。所以这个Sarsa是比较容易理解的,就直接利用这样采样两次action,然后得到了它的reward,往前走一步过后然后进行更新。

我们再来看一下这个Q-learning,他这里第一个action是ε-Greedy产生的,然后他往前走了一步。这个跟Sarsa很不同的是走一步过后,他就可以通过bootstripping去看这个q table上面谁是max的这个值,然后就构造出他当前的这个TD target。有了当前的TD target过后,然后他就可以立刻去更新它的这个q value的值了。它并不需要去执行第二个action,所以这样然后它再更新它的这个state,进入到下一个state,让我们实际来运行一下这个带这他会可视化出他最后学习的这个pass。然后第一个轨迹它出现的是这个Q-learning出来的,你看它沿着这个悬崖走。然后第二个是他的Sarsa出来的轨迹,它就会远离他的这个悬崖,这样就会得到一个更保守这个sharp optimal的一个策略。

cliffwalk.py

python 复制代码
# Example 6.6 Cliff Walking in Chapter 6: Temporal Difference Learning in Sutton and Barto Textbook
# 这是Sutton和Barto强化学习教材第6章的悬崖行走问题示例

import matplotlib.pyplot as plt  # 导入matplotlib用于可视化
import numpy as np  # 导入numpy用于数值计算
from matplotlib.colors import hsv_to_rgb  # 导入HSV到RGB的颜色转换函数


def change_range(values, vmin=0, vmax=1):
    """
    将数值范围归一化到指定的最小值和最大值之间
    用于将Q值映射到颜色强度范围
    """
    start_zero = values - np.min(values)  # 将最小值平移到0
    # 归一化到[0,1],然后映射到[vmin, vmax]
    return (start_zero / (np.max(start_zero) + 1e-7)) * (vmax - vmin) + vmin


class GridWorld:
    """
    网格世界环境类
    实现了一个4x12的网格世界,其中包含正常区域、悬崖和目标
    """
    # 定义不同地形的颜色(HSV格式)
    terrain_color = dict(normal=[127 / 360, 0, 96 / 100],  # 正常区域:灰色
                         objective=[26 / 360, 100 / 100, 100 / 100],  # 目标:黄色
                         cliff=[247 / 360, 92 / 100, 70 / 100],  # 悬崖:蓝色
                         player=[344 / 360, 93 / 100, 100 / 100])  # 玩家:粉红色

    def __init__(self):
        """初始化网格世界"""
        self.player = None  # 玩家位置初始化为None
        self._create_grid()  # 创建网格
        self._draw_grid()  # 绘制网格
        self.num_steps = 0  # 步数计数器

    def _create_grid(self, initial_grid=None):
        """创建4x12的网格,初始化所有格子为正常地形"""
        self.grid = self.terrain_color['normal'] * np.ones((4, 12, 3))
        self._add_objectives(self.grid)  # 添加悬崖和目标

    def _add_objectives(self, grid):
        """
        添加特殊地形:
        - 最后一行的第2到第11列设置为悬崖
        - 最后一行最后一列设置为目标
        """
        grid[-1, 1:11] = self.terrain_color['cliff']  # 悬崖区域
        grid[-1, -1] = self.terrain_color['objective']  # 目标位置

    def _draw_grid(self):
        """初始化matplotlib图形用于可视化"""
        self.fig, self.ax = plt.subplots(figsize=(12, 4))  # 创建图形
        self.ax.grid(which='minor')  # 显示次要网格线
        
        # 为每个格子创建文本对象,用于显示Q值
        self.q_texts = [self.ax.text(*self._id_to_position(i)[::-1], '0',
                                     fontsize=11, verticalalignment='center',
                                     horizontalalignment='center') for i in range(12 * 4)]

        # 显示网格图像
        self.im = self.ax.imshow(hsv_to_rgb(self.grid), cmap='terrain',
                                 interpolation='nearest', vmin=0, vmax=1)
        # 设置主刻度和次刻度
        self.ax.set_xticks(np.arange(12))
        self.ax.set_xticks(np.arange(12) - 0.5, minor=True)
        self.ax.set_yticks(np.arange(4))
        self.ax.set_yticks(np.arange(4) - 0.5, minor=True)

    def reset(self):
        """
        重置环境到初始状态
        返回:初始状态的ID
        """
        self.player = (3, 0)  # 玩家从左下角开始
        self.num_steps = 0  # 重置步数
        return self._position_to_id(self.player)  # 返回状态ID

    def step(self, action):
        """
        执行一个动作,返回新状态、奖励和是否结束
        
        参数:
            action: 0=上, 1=下, 2=右, 3=左
        
        返回:
            next_state: 新状态的ID
            reward: 获得的奖励
            done: 是否到达终止状态
        """
        # 根据动作更新玩家位置(带边界检查)
        if action == 0 and self.player[0] > 0:  # 向上移动
            self.player = (self.player[0] - 1, self.player[1])
        if action == 1 and self.player[0] < 3:  # 向下移动
            self.player = (self.player[0] + 1, self.player[1])
        if action == 2 and self.player[1] < 11:  # 向右移动
            self.player = (self.player[0], self.player[1] + 1)
        if action == 3 and self.player[1] > 0:  # 向左移动
            self.player = (self.player[0], self.player[1] - 1)

        self.num_steps = self.num_steps + 1  # 增加步数计数
        
        # 根据新位置确定奖励和是否结束
        if all(self.grid[self.player] == self.terrain_color['cliff']):
            # 掉入悬崖:大负奖励,回合结束
            reward = -100
            done = True
        elif all(self.grid[self.player] == self.terrain_color['objective']):
            # 到达目标:0奖励,回合结束
            reward = 0
            done = True
        else:
            # 正常移动:小负奖励(鼓励快速到达目标),继续
            reward = -1
            done = False

        return self._position_to_id(self.player), reward, done

    def _position_to_id(self, pos):
        """将二维坐标(行,列)映射到唯一的状态ID"""
        return pos[0] * 12 + pos[1]

    def _id_to_position(self, idx):
        """将状态ID映射回二维坐标(行,列)"""
        return (idx // 12), (idx % 12)

    def render(self, q_values=None, action=None, max_q=False, colorize_q=False):
        """
        渲染当前环境状态
        
        参数:
            q_values: Q值表,用于显示
            action: 当前执行的动作
            max_q: 是否只显示最大Q值
            colorize_q: 是否用颜色编码Q值
        """
        assert self.player is not None, 'You first need to call .reset()'

        if colorize_q:
            # 使用Q值的颜色编码来显示网格
            assert q_values is not None, 'q_values must not be None for using colorize_q'
            grid = self.terrain_color['normal'] * np.ones((4, 12, 3))
            # 将每个状态的最大Q值映射到颜色饱和度
            values = change_range(np.max(q_values, -1)).reshape(4, 12)
            grid[:, :, 1] = values  # 修改饱和度通道
            self._add_objectives(grid)  # 重新添加悬崖和目标
        else:
            grid = self.grid.copy()

        # 在网格上标记玩家位置
        grid[self.player] = self.terrain_color['player']
        self.im.set_data(hsv_to_rgb(grid))

        if q_values is not None:
            xs = np.repeat(np.arange(12), 4)  # x坐标
            ys = np.tile(np.arange(4), 12)  # y坐标

            # 更新每个格子的Q值文本
            for i, text in enumerate(self.q_texts):
                if max_q:
                    # 只显示最大Q值
                    q = max(q_values[i])
                    txt = '{:.2f}'.format(q)
                    text.set_text(txt)
                else:
                    # 显示所有动作的Q值
                    actions = ['U', 'D', 'R', 'L']
                    txt = '\n'.join(['{}: {:.2f}'.format(k, q) for k, q in zip(actions, q_values[i])])
                    text.set_text(txt)

        if action is not None:
            # 在标题中显示当前动作
            self.ax.set_title(action, color='r', weight='bold', fontsize=32)

        plt.pause(0.01)  # 短暂暂停以更新显示


def egreedy_policy(q_values, state, epsilon=0.1):
    """
    ε-贪婪策略:用于平衡探索和利用
    
    参数:
        q_values: Q值表
        state: 当前状态
        epsilon: 探索概率
    
    返回:
        选择的动作
    
    工作原理:
        - 以ε概率随机选择动作(探索)
        - 以1-ε概率选择Q值最大的动作(利用)
    """
    if np.random.random() < epsilon:
        return np.random.choice(4)  # 随机探索
    else:
        return np.argmax(q_values[state])  # 贪婪利用


def q_learning(env, num_episodes=500, render=True, exploration_rate=0.1,
               learning_rate=0.5, gamma=0.9):
    """
    Q-Learning算法:Off-policy TD控制算法
    
    核心思想:使用目标策略(贪婪)来更新Q值,但使用行为策略(ε-贪婪)来选择动作
    
    参数:
        env: 环境对象
        num_episodes: 训练回合数
        render: 是否可视化
        exploration_rate: 探索率ε
        learning_rate: 学习率α
        gamma: 折扣因子γ
    
    返回:
        ep_rewards: 每个回合的总奖励
        q_values: 学习到的Q值表
    """
    # 初始化Q值表为全0
    q_values = np.zeros((num_states, num_actions))
    ep_rewards = []  # 记录每个回合的奖励

    for _ in range(num_episodes):
        state = env.reset()  # 重置环境,获取初始状态
        done = False
        reward_sum = 0  # 累计奖励

        while not done:
            # 【步骤1】使用ε-贪婪策略选择动作(行为策略)
            action = egreedy_policy(q_values, state, exploration_rate)
            
            # 【步骤2】执行动作,观察奖励和新状态
            next_state, reward, done = env.step(action)
            reward_sum += reward
            
            # 【步骤3】Q-Learning更新公式(核心)
            # TD目标 = r + γ * max_a Q(s', a)  <- 使用贪婪策略(目标策略)
            td_target = reward + 0.9 * np.max(q_values[next_state])
            # TD误差 = TD目标 - 当前Q值
            td_error = td_target - q_values[state][action]
            # 更新Q值:Q(s,a) <- Q(s,a) + α * TD误差
            q_values[state][action] += learning_rate * td_error
            
            # 【步骤4】转移到下一个状态
            state = next_state

            if render:
                env.render(q_values, action=actions[action], colorize_q=True)

        ep_rewards.append(reward_sum)  # 记录本回合总奖励

    return ep_rewards, q_values


def sarsa(env, num_episodes=500, render=True, exploration_rate=0.1,
          learning_rate=0.5, gamma=0.9):
    """
    SARSA算法:On-policy TD控制算法
    
    核心思想:使用实际执行的动作来更新Q值(行为策略和目标策略相同)
    
    参数:
        env: 环境对象
        num_episodes: 训练回合数
        render: 是否可视化
        exploration_rate: 探索率ε
        learning_rate: 学习率α
        gamma: 折扣因子γ
    
    返回:
        ep_rewards: 每个回合的总奖励
        q_values_sarsa: 学习到的Q值表
    
    与Q-Learning的区别:
        - Q-Learning: TD目标使用max Q(s',a') (贪婪)
        - SARSA: TD目标使用实际选择的Q(s',a') (ε-贪婪)
    """
    # 初始化Q值表为全0
    q_values_sarsa = np.zeros((num_states, num_actions))
    ep_rewards = []  # 记录每个回合的奖励

    for _ in range(num_episodes):
        state = env.reset()  # 重置环境
        done = False
        reward_sum = 0
        
        # 【步骤1】使用ε-贪婪策略选择初始动作
        action = egreedy_policy(q_values_sarsa, state, exploration_rate)

        while not done:
            # 【步骤2】执行动作,观察奖励和新状态
            next_state, reward, done = env.step(action)
            reward_sum += reward

            # 【步骤3】为下一个状态选择动作(使用相同的ε-贪婪策略)
            next_action = egreedy_policy(q_values_sarsa, next_state, exploration_rate)
            
            # 【步骤4】SARSA更新公式(核心)
            # TD目标 = r + γ * Q(s', a')  <- 使用实际要执行的动作a'
            td_target = reward + gamma * q_values_sarsa[next_state][next_action]
            # TD误差 = TD目标 - 当前Q值
            td_error = td_target - q_values_sarsa[state][action]
            # 更新Q值:Q(s,a) <- Q(s,a) + α * TD误差
            q_values_sarsa[state][action] += learning_rate * td_error

            # 【步骤5】更新状态和动作(S, A, R, S', A' 五元组)
            state = next_state
            action = next_action  # SARSA的关键:使用已选择的下一个动作

            if render:
                env.render(q_values, action=actions[action], colorize_q=True)

        ep_rewards.append(reward_sum)  # 记录本回合总奖励

    return ep_rewards, q_values_sarsa


def play(q_values):
    """
    使用学习到的Q值表来演示一个完整回合
    采用完全贪婪策略(ε=0)
    """
    # 创建新环境实例
    env = GridWorld()
    state = env.reset()  # 重置到初始状态
    done = False

    while not done:
        # 使用贪婪策略选择动作(不再探索)
        action = egreedy_policy(q_values, state, 0.0)
        # 执行动作
        next_state, reward, done = env.step(action)

        # 更新状态
        state = next_state

        # 可视化
        env.render(q_values=q_values, action=actions[action], colorize_q=True)


# ==================== 主程序 ====================

# 定义动作常量
UP = 0
DOWN = 1
RIGHT = 2
LEFT = 3
actions = ['UP', 'DOWN', 'RIGHT', 'LEFT']

### 创建环境
env = GridWorld()
num_states = 4 * 12  # 状态空间大小:4行 × 12列 = 48个状态
num_actions = 4  # 动作空间大小:上、下、右、左

### 使用Q-Learning训练
print("=== 训练Q-Learning ===")
# 训练单次,gamma=0.9, learning_rate=1
q_learning_rewards, q_values = q_learning(env, gamma=0.9, learning_rate=1, render=False)
env.render(q_values, colorize_q=True)  # 可视化最终的Q值

# 运行10次实验取平均,评估性能稳定性
q_learning_rewards, _ = zip(*[q_learning(env, render=False, exploration_rate=0.1,
                                         learning_rate=1) for _ in range(10)])
avg_rewards = np.mean(q_learning_rewards, axis=0)  # 计算平均奖励
mean_reward = [np.mean(avg_rewards)] * len(avg_rewards)  # 总平均奖励

# 绘制Q-Learning的学习曲线
fig, ax = plt.subplots()
ax.set_xlabel('Episodes using Q-learning')
ax.set_ylabel('Rewards')
ax.plot(avg_rewards)  # 每个回合的平均奖励
ax.plot(mean_reward, 'g--')  # 总体平均奖励(绿色虚线)

print('Mean Reward using Q-Learning: {}'.format(mean_reward[0]))

### 使用SARSA训练
print("\n=== 训练SARSA ===")
# 训练单次,learning_rate=0.5, gamma=0.99
sarsa_rewards, q_values_sarsa = sarsa(env, render=False, learning_rate=0.5, gamma=0.99)

# 运行10次实验取平均
sarsa_rewards, _ = zip(*[sarsa(env, render=False, exploration_rate=0.2) for _ in range(10)])
avg_rewards = np.mean(sarsa_rewards, axis=0)
mean_reward = [np.mean(avg_rewards)] * len(avg_rewards)

# 绘制SARSA的学习曲线
fig, ax = plt.subplots()
ax.set_xlabel('Episodes using Sarsa')
ax.set_ylabel('Rewards')
ax.plot(avg_rewards)
ax.plot(mean_reward, 'g--')

print('Mean Reward using Sarsa: {}'.format(mean_reward[0]))

# 可视化推理阶段的表现
print("\n=== Q-Learning策略演示 ===")
play(q_values)  # 使用Q-Learning学到的策略
print("\n=== SARSA策略演示 ===")
play(q_values_sarsa)  # 使用SARSA学到的策略
相关推荐
白杨SEO营销2 小时前
白杨SEO:看“20步:从0-1做项目的笨办法”来学习如何选一个项目做及经验分享
前端·学习
无所事事的程序员2 小时前
Claude指令学习
学习
学习路上_write2 小时前
AD5293驱动学习
c语言·单片机·嵌入式硬件·学习
遇到困难睡大觉哈哈2 小时前
HarmonyOS —— Remote Communication Kit 定制处理行为(ProcessingConfiguration)速记笔记
笔记·华为·harmonyos
菥菥爱嘻嘻3 小时前
组件测试--React Testing Library的学习
前端·学习·react.js
白帽子黑客罗哥3 小时前
零基础转行渗透测试 系统的学习流程(非常详细)
学习·网络安全·渗透测试·漏洞挖掘·护网行动
李洛克073 小时前
RDMA 编程完整学习路线图
学习·rdma·路线
暴风游侠3 小时前
linux知识点-服务相关
linux·服务器·笔记
你想知道什么?4 小时前
JNI简单学习(java调用C/C++)
java·c语言·学习