当前位置: 首页 > news >正文

Q-learning强化算法万字详解

前言

Hi,everyone,相信现在看到这篇文章的程序员或者未来的程序员们,或多或少在曾经某个懵懂的青春岁月里都想过“我以后要做一款属于自己的游戏!”这样美好的念头。当然,时过境迁,有的忘记了曾经的誓言,有的当一个笑话,不管怎么说,今天我们来聊聊,但不是说我们要开始制作出LOL那样的MOBA类游戏,也不是CS那样的FPS游戏,甚至不是一款游戏,而是游戏的一部分——游戏AI。

当然,游戏AI也有多种多样的,有与人进行博弈的,有伪装成玩家的解决游戏问题的,甚至,在大语言模型盛行的今天,也有进行文字情感交流的。但归根结底,笔者认为它们都有一些共通点:

  • 能够获取环境输入
  • 能够识别并处理环境
  • 输出一个行为
  • 行为改变环境并获得一些反馈

如三国志(顺便一提,笔者非常喜欢玩三国志13和14),总而言之呢,我们的目标,就是设计一个智能体,它能获得环境(游戏内的场景战况等whatever)的输入,能处理环境,并输出一个行为(上下左右跳跑等whatever),这个行为会导致环境发生变化,环境的变化会给予智能体一个反馈,让它能够计算自己距离目标(广义上的距离,不一定是位置的移动,也可能是击中数击杀数最终胜利等whatever),来判断和抉择下一步行为。

要设计这样一个智能体,我们理所当然地想到强化学习,至此引出本文的第一个重点概念:什么是强化学习

本文近万字,制作不易,请各位读者老爷不吝赏一个赞吧!这对我来说真的很重要,感谢感谢!

强化学习

什么是强化学习

强化学习方法起源于动物心理学的相关原理,模仿人类和动物学习的试错机制,是通过与环境交互,学习状态到行为的映射关系,以获得最大累计期望回报的方法。状态到行为的映射关系也即策略,表示在各个状态下,智能体采取的行为或行为概率。

强化学习就像是人类的学习,本质上是通过与环境交互进行学习,智能体会从与环境交互的过程中获得反馈,可能是正反馈也可能是负反馈,这种反馈经验刺激智能体去拟合出一条奖励更多或者损失更小的路线。从交互中学习就是强化学习的理论基础概念。

强化学习是通过智能体与环境的交互,学习状态到行为之间的映射关系,因此,它包括智能体环境两大对象。同时,还需要注意的是,强化学习是与时间序列有关的。

智能体

智能体,指的是要完成某项任务的对象,强化学习的最终目的,是让智能体通过大量的学习,能够在环境中完成某个目的。在一个离散的时间序列t=0,1,2…中,智能体会在每一个时间tnt_ntn内,从环境中接收一个状态sts_tst和回报rtr_trt,并通过rtr_trt和当前自身所处的状态sts_tst,以及已产生的经验教训判断,产生下一步at+1a_{t+1}at+1,以期望获得最大的回报。

环境

环境,指的是智能体所处的环境环境范围,其包含智能体可变更的状态集合以及每一种状态所获得的回报,例如:环境可以是一个二维的4*4地图,其中包含了智能体可以行进的16个位置以及每个位置对应的智能体到达该位置时可获得的回报。在离散的时间序列t=0,1,2,…中,智能体会在每个时间tnt_ntn内进行相应的行为ata_tat,该行为会使智能体的环境发生改变(也可能不会发生改变),环境会向智能体反馈当前的状态sts_tst和回报rtr_trt

强化学习原理

智能体不会被告知其在当前状态下,下一步行为应当采取何种行为,只能通过不断地尝试每一种动作,并收集环境中给予的反馈,来改善自己的行为,其改善行为的本质,就是追求回报最大化或损失最小化,在经过不断地尝试(多次迭代)后,智能体的行为会收敛成某个路径,即学习到最优的行为方式。

当然,这仅是笼统的强化学习原理,智能体作为一个学习行为的对象,其不一定只能通过试完每一步行为才能收敛,开发者可以通过一些算法,来加速最优行为的收敛。

Q-learning

Q-learning 是一种强化学习算法,其核心是一个叫Q表的表格Q(state, action),这个表格一轴表示state(状态),一轴表示action(行为),Q(i,j) = k就表示智能体在 i 这个状态下,如果采取 j 这个行为会带来奖励 k ,Q表示Q-learning算法的核心,用来记录不同状态下不同行为的收益,随着迭代次数的增加,Q函数会被拟合得越来越好直至收敛或者达到指定的迭代次数。

下面笔者将分别介绍智能体与环境两个对象,并以gym库提供的模拟游戏环境FrozenLake为例,实现Q-learning算法,训练一个智能体。

Q-learning智能体

我们知道,Q-learning算法是一种强化学习算法,强化学习有智能体环境两个对象,本小节我们就来介绍Q-learning智能体。

Q-learning智能体自身主要是有两个主要动作:选择行为更新Q表

  • 选择行为:指的是智能体在接收外部给予的信息后,选择下一步行动的过程,其中,智能体需判断是否进行探索?如果不探索,该以何种方式来选择行为?
  • 更新Q表:在选择行为后,智能体会从外部环境接收信息,然后根据外部环境提供的回报,状态等信息来更新Q表。更新Q表的函数笔者会在后面给出并详细解释。

参数解释

Q-learning智能体自身维护了一个Q表,在初始化Q-learning时会初始化自身的Q表,与此同时,智能体还需输入几个参数:学习率、折扣因子(奖励衰减)、贪婪度。

在解释上面几个参数之前,我们要先给出几个词的定义,以避免在解释参数时产生误解:

  • 回报:回报指的是环境给予智能体的反馈,是外部输入,比如:掉进冰湖为-1,到达终点为1,正常路径为0等,当智能体处于某一状态时,,就算它在原地打转或再次回到此处,它所获得的回报是相同的。
  • 奖励:奖励指的是智能体自身Q表中的记录,是内部变化,记录其在s状态,a行为下的奖励,Q表的值可能会随着智能体不断地运动,时间不断地推移而发生改变,即Q表更新,在下文中我们会给出Q表更新函数。智能体的每一次行动,都会产生一次Q表更新。Q表的值是动态的,会改变的,随着迭代次数增加,慢慢会出现,某些状态下的某些行为会更具奖励,促使智能体去更愿意尝试那样的行为,这就是“收敛”。读者也可以将奖励理解为经验
  • 探索:探索指的是当智能体进行贪婪策略时,会跳出原定的行为策略,尝试随机地探索其它区域,适当的探索有利于使智能体尝试新的未知的状态,而不束缚于“经验主义”。

OK,当你理解这几个词的意思后,我们现在开始解释参数:

  • 学习率:学习率这个参数是各大机器学习算法的常客,在这里表示智能体对所获得的经验的学习程度,一般在属于[0,1],学习率越大,会使得智能体跨的步伐越大,使之收敛速度越快,但注意,步伐越大可能存在的误差越大,导致无法正确收敛(陷入局部最优),损失会震荡甚至变大。反之,学习率越小,步伐越小,收敛的速度越慢。学习率是一个超参数,即由程序员事先拟定,在整个迭代过程中不可更改。
  • 折扣因子:折扣因子也叫奖励衰减,是对未来奖励的衰减值,当衰减值为0时,则代表智能体近视,看不见下一状态的最优行为,即智能体只在乎当前状态下环境给出的回报,而不在乎下一状态的奖励。换而言之,智能体只在乎眼下能获得的回报。当衰减值为1时,代表智能体带了眼镜,能够看得更远,它完完整整地看清楚了下一步自己会获得多大的奖励,而不仅仅只看见眼前的回报。
  • 贪婪度:贪婪度表示智能体尝试其它行为的可能性,如果贪婪度为0,则说明智能体只会考虑已有的奖励来选择行为,而贪婪度越高,则智能体越有可能随机选择一个行为(探索),而不是根据奖励来选择,为什么贪婪度是必要的呢,是为了避免智能体陷入局部最优解而无法收敛,随机的动作可以让智能体有机会跳出局部最优解,但务必把握好平衡,太过贪婪也可能导致智能体的行为混乱而无法收敛。

下面给出智能体的初始化方法:

    # 初始化,参数:状态数,动作数,学习率,折扣因子(奖励衰减),贪婪度def __init__(self, n_states, n_actions, learning_rate=0.1, gamma=0.99, epsilon=0.1):self.q_table = np.zeros((n_states, n_actions))      #初始化Q表,Q表的初始化为0,也可以初始化为随机值self.learning_rate = learning_rate                  #初始化学习率,学习率默认为0.1self.discount_factor = gamma                        #初始化折扣因子,折扣因子默认为0.99self.epsilon = epsilon                              #初始化贪婪度,贪婪度默认为0.1# 打印初始化Q表print("initial q_table:")print(self.q_table)

由于笔者的模拟环境为gym库提供的ForzenLake,这里的n_states是冰湖的地图大小,即4 * 4 = 16个格子,行为数为4,分别是向左,向下,向右,向上移动。

行为选择

现在我们来看一看智能体是如何选择自己的行为的,智能体的行为选择策略原理非常简单,大伙简单看看,理解就行。

当智能体到达某一状态(位置)时,首先会开始选择行为,即接下来我该往哪个方向走。但在动脚起身之前,我们先要抛一个硬币,诶,就是贪婪度,在上面的初始化过程中,我们将贪婪度设置为0.1,即我们获取一个随机数,当数小于0.1时,则采取探索策略,当数大于0.1时,我们按照正常的策略前进,毕竟人不能太贪婪嘛(智能体也是)。

最简单的贪婪策略就是随机,即随机选择一个行为,根本不在乎Q表上记录了哪一行为奖励最高(就是任性!)。当然你也可以选择其它的贪婪策略,根据自己的需要选择,但切记,贪婪的目的是为了让智能体尝试采取其它行为,不束缚于“经验主义”,陷入局部最优解之中。试想以一下,当智能体来到一个岔路口,它曾经走过向左的方向,也确实从左方能够到达终点获得相应的回报,但从上帝视角来看,往右走会有一条更近的路抵达终点,但是由于智能体没去过,且贪婪度为0,那么它将“永远”不会尝试这条路。

正常策略即通过Q表,来选择最优行为了,因为智能体会根据环境的反馈以及Q表更新函数,来记录哪一个行为它曾经试过且“最好”,因此,它会选择那个最优的行为,也就是上面那个例子中的“向左走”,因为它曾经确实走通过这条路,并拿到过不错的回报。

我们在这里并没有说贪婪策略就一定好,或者正常策略就一定好,两种策略是互补的关系,就像现实社会中,总要有人在已探明的道路上巩固地基,也要有人跃跃欲试去探索未知的道路,关键在于开发智能体的你,是否能够平衡两者。

下面给出行为选择的方法:

    # 选择动作,参数:状态def choose_action(self, state):# 判断贪婪度if np.random.uniform(0, 1) < self.epsilon:# 如果进入了贪婪状态,那么agent将会尝试随机选择一个动作,此步称之为【探索】return np.random.choice(self.q_table.shape[1])else:# 选择最优动作,即在Q表中该状态下最大的Q值,此步称之为【利用】  state_all = self.q_table[state, :]# 根据Q值最大原则选取action,如果有多个action的Q相同且最大,则随机选取一个max_indices = np.argwhere(state_all == np.max(state_all)).flatten()return np.random.choice(max_indices)

Q表更新

终于到Q表更新了,这个更新是Q-learning算法的一大难点,也牵扯到上面的两大参数学习率与折扣因子,因此我们必须好好聊聊这个函数:
Q(st,at)←Q(st,at)+α[Rt+1+γmaxa′(st+1,a′)−Q(st,at)]Q(s_t,a_t) \leftarrow Q(s_t,a_t) + \alpha[R_{t+1} + \gamma max_{a'}(s_{t+1},a') - Q(s_t,a_t)] Q(st,at)Q(st,at)+α[Rt+1+γmaxa(st+1,a)Q(st,at)]

  • Q(st,at)Q(s_t,a_t)Q(st,at):指的是Q表中,当前状态sts_tst,行为ata_tat时的奖励
  • α\alphaα:指的是学习率
  • Rt+1R_{t+1}Rt+1:指的是下一个状态St+1S_{t+1}St+1下,环境给予智能体的回报
  • γ\gammaγ:指的是折扣因子,也叫奖励衰减
  • maxa′(st+1,a′)max_{a'}(s_{t+1},a')maxa(st+1,a):指的是奖励, 实际上是一个Q表中的值,该值是下一个状态st+1s_{t+1}st+1的所有行为a0,a1,a2,...a_0,a_1,a_2,...a0,a1,a2,...中,最大的一个。

Q表更新函数与上面的参数结合起来看,有助于你理解什么是步伐,什么是奖励,什么是回报,请允许我不厌其烦再唠叨一遍:

  • Q(st,at)Q(s_t,a_t)Q(st,at):是当前状态,当前行为的奖励
  • maxa′(st+1,a′)max_{a'}(s_{t+1},a')maxa(st+1,a):是下一步状态,所有行为中的最大奖励。
  • Rt+1R_{t+1}Rt+1:下一步状态的回报
  • Rt+1+γmaxa′(st+1,a′)−Q(st,at)R_{t+1} + \gamma max_{a'}(s_{t+1},a') - Q(s_t,a_t)Rt+1+γmaxa(st+1,a)Q(st,at):步伐,即此次Q表需要更新的幅度

Q表更新的难理解处:

笔者一开始没有搞清楚Q表的更新逻辑,这条Q表更新函数中的maxa′(st+1,a′)max_{a'}(s_{t+1},{a'})maxa(st+1,a)究竟怎么计算的,其实当我们知道了什么时候开始更新Q表时,这个Q表更新函数就可以看得懂了:

  • 在经过 t 时间后,智能体来到了位置(状态)sts_tst处,此时,智能体会先选择下一个行为,但请注意,此时智能体还未动身前往下一个位置
  • 通过环境反馈,智能体获取当采取某一行为后,判断出下一位置的状态st+1s_{t+1}st+1,以及可获得的回报Rt+1R_{t+1}Rt+1,但请注意,此时智能体还停留在sts_tst处,也就是智能体先不更新自己的位置,而是先从st+1s_{t+1}st+1处获取相应的信息更新Q表。
  • 更新Q表
  • 智能体动身前往下一位置(状态)

因此,我们可知maxa′(st+1,a′)max_{a'}(s_{t+1},a')maxa(st+1,a)表示的是,智能体在选择下一步行为后得到的下一步状态,那个状态下的最优行为。总而言之,智能体的奖励逻辑是,根据下一状态中4个动作中的最大奖励值,来计算出当前的这个状态所执行的这个动作的奖励值,这样的计算方式避免智能体只看当前状态的回报,也要根据“历史经验”适当考虑未来的收益。如果当前状态的回报丰厚,但是根据Q表的记录,未来下一步的奖励并不理想,也会拉低智能体对当前这一状态的“印象”。使之不会在Q表中给予很高的分数。

这里可能有些同学觉得过于啰嗦,但这是笔者踩过坑且困惑的地方,为了避免其它小伙伴也对此函数产生困惑,笔者还是要尽量解释一番。

我们来看下面这个例子,先看Frozen Lake的4*4地图:

FrozenLake

在这个地图中,智能体出发点为左上角的格子,即第1行第1列处,此处对应Q表的第1行。终点在右下角的格子,即第4行第4列处,此处对应Q表的第16行。Q表将4*4的地图划为一个16个数据的一维数组。每个格子表示一个状态,所以我们可以说:状态即智能体所在的位置。在每个状态中,智能体可以执行4个操作,即:向左,向下,向右,向上移动,移动后,智能体会获得相应的奖励,如下所示:

Q_table

在上述Q表中可以看到,第16行,即最下方的[0,0,0,0],是终点,到了此处,显然就不需要再移动了,因此此处的向左,向下,向右,向上移动的奖励值都为0。

我们以第15行的数据来举例说明,第15行的数据为[0, -1, 0.8, 0],此处是第4行,第3列的位置,即Forzen Lake地图中终点左边的格子。因此可以看到,在这个格子中,如果智能体向右走一步,即可达到终点,因此当智能体在这个状态下尝试向右走,那么根据Q表的更新运算规则:
Q(st,at)←Q(st,at)+α[Rt+1+γmaxa′(st+1,a′)−Q(st,at)]Q(s_t,a_t) \leftarrow Q(s_t,a_t) + \alpha[R_{t+1} + \gamma max_{a'}(s_{t+1},a') - Q(s_t,a_t)] Q(st,at)Q(st,at)+α[Rt+1+γmaxa(st+1,a)Q(st,at)]
显然,从初始化开始时,智能体是没有在这个位置(状态)下向右(行为)行走过的,在茫茫多次选择后,某一次,智能体恰好来到这个位置(状态),它打算向右(行为)行走试试。确定要向右走后,它开始计算自己想向右走的这一个行为会获得多少奖励。

我们开始算一下:

在此时,Q(st,at)Q(s_t,a_t)Q(st,at)为0,因为自初始化伊始,智能体还没有达到过终点,而我们定下的规则,只有达到终点,才有一个奖励“1”,其它的状态都没有奖励“0”。因此还没有尝试过在此处向右走的智能体原本的该状态该动作奖励值为0

学习率α\alphaα我们定为0.8

智能体很惊喜地发现,下一步就是终点,因此下一步可获得的奖励Rt+1R_{t+1}Rt+11

奖励衰减因子γ\gammaγ我们定为0.99

因为下一个状态就是终点,此前智能体从未到达,即使到达,它也无需在此处做多余的动作,也无需再走下一状态,因此下一个状态的所有动作的奖励值都为0,因此maxa′(st+1,a′)max_{a'}(s_{t+1},a')maxa(st+1,a)0

我们可以得到算式:Q(st,at)=0+0.8×(1+0.99×0−0)=0.8Q(s_t,a_t) = 0 + 0.8\times(1 + 0.99\times0 - 0)=0.8Q(st,at)=0+0.8×(1+0.99×00)=0.8

回头再看看Q表,是不是发现,在第15行的第3个元素的数据为0.8。没错,这个Q表就是在智能体第一次访问终点时得到的Q表。

你可能会感到奇怪,为什么明明智能体已经到达了终点,为什么这一趟旅程仅仅只是让Q表更新了一个有用的元素。

对,第一次到达终点,仅仅会让智能体的上一个状态的动作获得奖励,而非整条路线都获得奖励。但我们从宏观上来看,原本仅仅在终点才有1的奖励值,现在在终点旁的一个位置St−1S_{t-1}St1,也有0.8的奖励值了,这多出来的具有正向奖励值的点会有什么作用呢?

如果我们把整个地图看作是一个海洋,终点是密度最大的地方,在一开始,除终点外,每个地方的密度都是一样的,智能体不知道该往哪个地方移动,只能随机尝试,只有当智能体非常靠近密度最大的终点时,智能体才会意识到,旁边有个疑似终点的地方。而当智能体到达终点StS_{t}St一次后,它会尝试记录下之前的动作,但肯定不是整条路线都记下来(指不定它绕了几次弯,走了几次胡同),因此它只记录最后一个步骤。怎么记录呢,就改变最后一个步骤的地区St−1S_{t-1}St1的密度,当然不能改成和终点一样,那么下次就无法分辨哪个是终点了。因此,现在又多了一个地方的密度变大了,智能体下一次从起始点触发,就有更大的概率碰到密度不一样的地方,当重新来到St−1S_{t-1}St1时那智能体就知道了,这个地方它留下过痕迹,那它就更愿意往这个地方走,同时嘞,它会重复上面的操作,基于这个地方,改变上一个地区St−2S_{t-2}St2的密度,然后再检查这个地区St−1S_{t-1}St1,当它开始检查这个地方St−1S_{t-1}St1,嘿,您猜怎么着,它又发现了一个密度更大的地方(终点StS_{t}St)。这样,基于终点,慢慢地往外其它地方的密度都发生了变化,发生变化是基于曾经智能体来过这个区域,且它曾成功地通过该区域进入过终点的。

慢慢地,智能体进入终点StS_{t}St的次数越来越多,修改St−1S_{t-1}St1的次数也越来越多,St−1S_{t-1}St1的密度慢慢变得越来越大,也越来越接近终点StS_{t}St的密度了,同时呢,可能St−2S_{t-2}St2St−3S_{t-3}St3都可以进入St−1S_{t-1}St1,但如果St−2S_{t-2}St2St−3S_{t-3}St3更容易进入St−1S_{t-1}St1:(P(St−2∣St−1)>P(St−3∣St−1)P(S_{t-2}|S_{t-1})>P(S_{t-3}|S_{t-1})P(St2St1)>P(St3St1)),那么St−2S_{t-2}St2密度上升的速度就会比St−3S_{t-3}St3快,对于智能体来说,这个St−2S_{t-2}St2St−3S_{t-3}St3更像终点(或者更接近终点),因此形成一个正反馈,越来越多地走St−2S_{t-2}St2St−1S_{t-1}St1StS_{t}St这条路线。

一个离终点两格远,但距离St−1S_{t-1}St1仅一格远的位置St−2S_{t-2}St2的密度,也慢慢接近st−1s_{t-1}st1且比st−3s_{t-3}st3的密度更大。慢慢地,随着迭代次数的增加,在海洋中,就出现了一条密度明显高于其它地方的路径,就算智能体重新回到起点,它也能根据这一条路径快速走到终点,而不必像一开始时乱走一通。这样我们就称之为收敛了,也就是训练完成。

小结,Q表就是智能体的小本本,它在不断地试错中,渐渐摸索出了自己的行为方式,在s0s_0s0时采取ai0a_{i0}ai0的方式最好,在s1s_1s1时采取ai1a_{i1}ai1的行为最好等等…它小本本中的奖励,是从终点(有回报点)不断向外延伸的,而非到达终点后,此次到达终点所经历的所有点,采取的所有行为都会获得奖励。其次,大家要理解Q表的更新函数,这是Q-learning算法在重要的环节之一,更新函数所考虑的不仅仅是当前状态下的回报,也不好高骛远,只看未来不看当下,如何平衡奖励衰减与学习率,就要靠大家慢慢摸索了。

下面给出更新Q表的方法:

    # 更新Q表,参数:状态,动作,奖励,下一个状态,是否结束def update(self, state, action, reward, next_state, done):#下面4行代码就是Q表的更新函数best_next_action = np.argmax(self.q_table[next_state])td_target = reward + self.discount_factor * self.q_table[next_state][best_next_action]td_error = td_target - self.q_table[state][action]self.q_table[state][action] += self.learning_rate * td_error
#####################  手动修改Q值,加快收敛速度,这里看似“作弊”,但是不影响Q-learning的原理 ############# if state == next_state:#     self.q_table[state][action] = -1# if done:#     if reward == 0:#         self.q_table[state][action] = -1
######################  当开启手动修改Q值时,会加速迭代的速度,但会影响学习曲线 #########################

ForzenLake模拟环境

gym库是由OpenAI推出的一个强化学习实验环境库,FrozenLake就是其库下的一个游戏模拟环境,简单介绍一下游戏规则:

环境设定为游戏角色在结冰的湖面中前进,寻找宝箱,但在冰湖中存在着一些洞,一旦掉入冰洞则失败,游戏结束,找到宝箱则成功,游戏结束。

以一个4*4的小冰湖地图为例:

FrozenLake

显然,在这个冰湖中,角色初始位置在左上角的位置,从二维数组上的角度来看就是[0,0],从一维数组的角度来看就是[0],宝藏在地图右下角,从二维数组的角度上来看就是[3,3],从一位数组的角度上来看就是[15],注意:上面的地图仅仅是我们人类观察者所见到的地图,而智能体看到的并不是这样的地图,它仅能够知道自己走到了第几个格子或者第几行第几列的格子,并从中获得什么回报,因此笔者才强调从数组的角度出发,因为这是智能体的角度。

其次,作为上帝视角的我们,可以再冰湖上增加一些特殊的机制,比如湖面可能打滑,有概率往随机的方向上行动而不遵循智能体的意志,但仅仅是示例,咱们就不做那些复杂的操作了。

现在我们给出了环境的地图,接下来给出环境的回报规则:

只有在角色找到宝藏时,才会获得回报1,掉进冰洞与冰面行走回报都为0,掉进冰洞直接结束游戏,开启下一轮迭代。

by the way,因为掉进冰洞和在冰面行走的回报都为0,因此智能体是分不清掉进冰洞与冰面行走的区别,因此如果你想加速迭代,让智能体明白掉进冰洞是危险的,可以将掉进冰洞的回报设置为-1,让智能体意识到这里有惩罚,下一次迭代的策略就会避开这个位置。(这也是上面更新Q表代码中我们做的一点小小的“作弊”)。

接下来我们给出环境的代码:

def run_episode(env, agent):state, info = env.reset()	#重置环境,即更新角色状态(位置)#print(state)total_reward = 0	done = Falsewhile not done:											 #判断游戏是否结束action = agent.choose_action(state)					   #智能体选择接下来的行为#print(env.step(action))next_state, reward, done, _, _ = env.step(action)		#将行为输入给环境,环境会根据行为反馈下一步状态,回报,是否结束游戏等信息agent.update(state, action, reward, next_state, done)    #根据环境反馈的信息以及自身的位置,智能体更新Q表  state = next_state									  #更新智能体的状态total_reward += reward								  #将当前回报加总,在整个一轮游戏结束后,看看此轮游戏获得的总回报,判断此轮游戏的收敛效果。return total_reward										# 游戏结束后,返回本轮游戏的总回报值,可用于后续的曲线绘制和分析研究# 使用示例
env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False, render_mode = "human")	#创建冰湖环境,呈现画面
# env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False, render_mode = "ansi")
# env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False)
agent = QLearningAgent(env.observation_space.n, env.action_space.n)		#初始化智能体
n_episodes = 10000	#迭代次数
rewards = []	#用来记录回报曲线,展示收敛效果for episode in range(n_episodes):reward = run_episode(env, agent)	#每一次run_episode,都是一次游戏进程,只有当游戏结束(掉进冰湖或拿到宝箱),才会返回rewards.append(reward)			   #将本轮游戏的总体回报记录下来,以便后续绘制曲线,展示收敛效果

在上面的代码中,我们完成了整个模拟环境的搭建以及迭代过程,基本上在每一行代码中都添加了注释,下面就不过多赘述了。reward列表和total_reward是非必要的选择,但会给我们后续研究智能体的迭代速度提供直观的数据支持,如果读者们要调参,修改策略等进阶玩法,则这一步是很重要的,当然读者们不一定只看回报这一个指标,你也可以将其它指标也收集起来,比如每一轮游戏的Q表等等,通过以下代码可以将每轮游戏的最终Q表打印出来:

# 打印最终的Q表
print("final q_table:")
print(agent.q_table)

最后,我们来解释一下gym.make方法的各种参数:

  • FrozenLakeLake:说明我们需要创建的模拟环境为冰湖
  • desc:作用是自定义网格地图布局,我们设置为None,即使用默认的预定义地图,如果需要自定义地图,可以传入一个字符串列表,如:[“SFFF”, “FHFH”, “FFFH”, “HFFG”],其中:S表示起点,F表示安全冰面,H表示冰洞,G表示目标
  • map_name:选择预定的网格地图,当desc为None时生效,表明采用预定的4*4大小的地图
  • is_slippery:控制移动的随机性,True(默认),智能体执行动作时有1/3概率打滑,False则执行动作是确定的(智能体严格按照指定方向移动)
  • render_mode:设置环境渲染模式,human在屏幕上实时显示图形化界面,ansi在终端输出文本网格,None不渲染(默认)

总结

上面,咱们完整地讲解了Q-learning算法的基本原理,智能体与环境如何实现,有什么难点和需要掌握的地方,总的来说,Q-learning的本质核心其实也就是强化学习的本质核心——试错!只有在不断地跌倒和捶打中,才能诞生强大的智能体!抓住这一核心,其余的部分都可以做优化和修改,包括最关键的Q表更新函数,只要能够让智能体进化以达到自身目的,将其改的面目全非甚至不是Q-learning了又何尝不可呢,笔者只是通过Q-learning来初步学习强化学习,而强化学习并非只有Q-learning。笔者也把当初自己在学习过程中遇到的难点给详细阐述了一遍,当然笔者的语言功底有限,或许有部分还未讲清楚,也欢迎各位大佬留言。当笔者进一步学习其它强化学习知识点和算法的时候,再出文章,那么,下篇文章再见啦,祝各位变得更强!

完整代码

这代码笔者参考了下面参考文章的代码,浅浅修改了一些地方,并添加了一些注释,仅供参考:

import gym
import numpy as np
import matplotlib.pyplot as pltclass QLearningAgent:# 初始化,参数:状态数,动作数,学习率,折扣因子(奖励衰减),贪婪度def __init__(self, n_states, n_actions, learning_rate=0.1, gamma=0.99, epsilon=0.1):self.q_table = np.zeros((n_states, n_actions))      #初始化Q表,Q表的初始化为0,也可以初始化为随机值self.learning_rate = learning_rate                  #初始化学习率,学习率默认为0.1self.discount_factor = gamma                        #初始化折扣因子,折扣因子默认为0.99self.epsilon = epsilon                              #初始化贪婪度,贪婪度默认为0.1# 打印初始化Q表print("initial q_table:")print(self.q_table)# 选择动作,参数:状态def choose_action(self, state):# 判断贪婪度if np.random.uniform(0, 1) < self.epsilon:# 如果进入了贪婪状态,那么agent将会尝试随机选择一个动作,此步称之为【探索】return np.random.choice(self.q_table.shape[1])else:# 选择最优动作,即在Q表中该状态下最大的Q值,此步称之为【利用】# return np.argmax(self.q_table[state])# 根据Q值最大原则选取action,如果有多个action的Q相同且最大,则随机选取一个state_all = self.q_table[state, :]max_indices = np.argwhere(state_all == np.max(state_all)).flatten()return np.random.choice(max_indices)# 更新Q表,参数:状态,动作,奖励,下一个状态,是否结束def update(self, state, action, reward, next_state, done):best_next_action = np.argmax(self.q_table[next_state])td_target = reward + self.discount_factor * self.q_table[next_state][best_next_action]td_error = td_target - self.q_table[state][action]self.q_table[state][action] += self.learning_rate * td_error
#####################  手动修改Q值,加快收敛速度,这里看似“作弊”,但是不影响Q-learning的原理 ############# if state == next_state:#     self.q_table[state][action] = -1# if done:#     if reward == 0:#         self.q_table[state][action] = -1
######################  当开启手动修改Q值时,会加速迭代的速度,但会影响学习曲线 #########################def run_episode(env, agent):state, info = env.reset()                   #重置环境,即更新角色状态(位置)#print(state)total_reward = 0done = Falsewhile not done:                                             #判断游戏是否结束action = agent.choose_action(state)                     #智能体选择接下来的行为#print(env.step(action))next_state, reward, done, _, _ = env.step(action)       #将行为输入给环境,环境会根据行为反馈下一步状态,回报,是否结束游戏等信息agent.update(state, action, reward, next_state, done)   #根据环境反馈的信息以及自身的位置,智能体更新Q表  state = next_state                                      #更新智能体的状态total_reward += reward                                  #将当前回报加总,在整个一轮游戏结束后,看看此轮游戏获得的总回报,判断此轮游戏的收敛效果。return total_reward                                         # 游戏结束后,返回本轮游戏的总回报值,可用于后续的曲线绘制和分析研究# 使用示例
env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False, render_mode = "human")    #创建冰湖环境,呈现画面
# env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False, render_mode = "ansi")
# env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False)
agent = QLearningAgent(env.observation_space.n, env.action_space.n)     #初始化智能体
n_episodes = 10000           #迭代次数
rewards = []                 #用来记录回报曲线,展示收敛效果for episode in range(n_episodes):       #每一次run_episode,都是一次游戏进程,只有当游戏结束(掉进冰湖或拿到宝箱),才会返回reward = run_episode(env, agent)    #将本轮游戏的总体回报记录下来,以便后续绘制曲线,展示收敛效果rewards.append(reward)# 打印最终的Q表
print("final q_table:")
print(agent.q_table)# 绘制学习曲线
plt.plot(np.cumsum(rewards) / (np.arange(n_episodes) + 1))
plt.xlabel('Episode')
plt.ylabel('Average Reward')
plt.title('Q-Learning on FrozenLake')
plt.show()# 测试学习到的策略
n_test_episodes = 100
test_rewards = []for _ in range(n_test_episodes):state, info = env.reset()total_reward = 0done = Falsewhile not done:action = np.argmax(agent.q_table[state])state, reward, done, _, _ = env.step(action)total_reward += rewardtest_rewards.append(total_reward)print(f"Average test reward: {np.mean(test_rewards)}")

参考文章

智能体入门——遗传算法与Qlearning_genetic algorithm和qlearning的区别和关系-CSDN博客

【强化学习】Q-Learning算法详解-CSDN博客

【机器学习】Q-Learning详细介绍-CSDN博客

强化学习之迷宫Q-Learning实践笔记——入门篇 - 知乎

http://www.lryc.cn/news/615100.html

相关文章:

  • 关于C语言本质的一些思考
  • Python(6) -- 数据容器
  • Python映射合并技术:多源数据集成的高级策略与工程实践
  • 3D感知多模态(图像、雷达感知)
  • 容器技术基础与实践:从镜像管理到自动运行配置全攻略
  • 大数据与财务管理:未来就业的黄金赛道
  • 深入理解C++构造函数与初始化列表
  • centos 怎么将一些命令设置为快捷命令
  • 当配置项只支持传入数字,即无法指定单位为rem,需要rem转px
  • 第六章:【springboot】框架springboot原理、springboot父子工程与Swagger
  • visual studio 字体设置
  • 动态路由菜单:根据用户角色动态生成菜单栏的实践(包含子菜单)
  • 【Python 语法糖小火锅 · 第 5 涮 · 完结】
  • java练习题:数字位数
  • 【Java基础】字符串不可变性、string的intern原理
  • C++11 ---- 线程库
  • 3.2Vue Router路由导航
  • B.10.01.3-性能优化实战:从JVM到数据库的全链路优化
  • 区块链密码学简介
  • (LeetCode 每日一题) 231. 2 的幂 (位运算)
  • 基于clodop和Chrome原生打印的标签实现方法与性能对比
  • 通过 SCP 和 LXD 配置迁移 CUDA 环境至共享(笔记)
  • 数据标准化与归一化的区别与应用场景
  • FAN5622SX 四通道六通道电流吸收线性LED驱动器,单线数字接口 数字式调光, 2.7 → 5.5 V 直流直流输入, 30mA输出FAN5622S
  • C++ unordered_map 和 unordered_set 的使用
  • 新手向:Python开发简易待办事项应用
  • 【JS-8-Json】深入理解JSON语法及Java中的JSON操作
  • Visual Studio Code (v1.103) 中 GitHub Copilot 最新更新!
  • [TryHackMe]Challenges---Game Zone游戏区
  • 避不开的数据拷贝(2)