当前位置:   article > 正文

Python神经网络学习(六)--机器学习--强化学习_强化学习 python

强化学习 python

前言:

属实是失踪人口回归了。继续神经网络系列。

强化学习:

强化学习也是一个很重要的方向了,很多人用强化学习玩游戏,可能有人觉得强化学习很难(包括我),但是我今天用网上流传很广的、很经典的一个例子(悬崖徒步, CliffWalking),去带领大家明白强化学习,大概分为两期(本期和下一期)讲明白这个例子。

今天就从最简单的方式:表格型入手,开始入门强化学习。

什么是强化学习?

强化学习是Reinforcement Learning,我也不知道为什么把Reinforcement翻译成强化,按照我的英语水平,inforce(应该是通enforce)是强迫,re-代表又,就是一再强迫,就是强迫一个东西一遍又一遍的学习。这也突出了强化学习的本质:一遍又一遍。

就好像小时候我们玩红白机,super mario的时候,一遍又一遍的玩,我们玩那个游戏的过程就可以成为强化学习的过程。我们玩的时候,每次死亡都能知道下一次应该怎么做,包括,哪一个管道可以蹲下去,得到很多金币,都会被我们探索出来,这些都在强化学习中会有体现。

和mnist手写数字识别那种学习方式的不同?

最明显的一个区别就是,我给出一张mnist数据集的图片,能清楚的知道他的正确答案是什么。而我给出一个RL的场景,很少有人能直接给正确(或者最优)方案。

比如给出一个mario在水管上(这个水管可以蹲下去吃金币)的场景,问这个场景最有解决方案,应该至少会有下面三个版本:

1. 应该蹲下去,因为金币很多,还能省很大一段路。

2. 应该继续走,前面有个加命的蘑菇。

3. 你们俩都太弱了,慢慢的掐距离,可以做到蘑菇吃完回来蹲进水管。

至于更多的方案,我玩的还是太少了,留给大家探索吧。

而mnist数据集这种有监督学习,给出场景,答案就是确定的,这个确定的答案(groundtruth)就是标签(label),被用来计算损失从而让学习有进程。

强化学习的过程,由于没有正确答案,只能一次又一次(reinforcement)的玩,然后被驱动着通关。

强化学习的驱动是什么?

奖励!

如mario游戏,吃金币+命,吃绿蘑菇也+命,两个都吃加更多的命。但是这些都是次要的,主要是通关。所以一般都会把通关的奖励设置的很高,死亡的奖励设置的很低。

当然,有些人喜欢探索环境,说明他们把其他的奖励设置的很高,比如发现密道(如哪个管道能蹲下去)等。

CliffWalking 悬崖徒步

环境说明

环境设定

这个环境是一个4x12的环境,游戏角色(agent)从坐标(3, 0)出生,目的是达到对面(3, 11),

 如图,cliff是悬崖,掉进去就死亡。

当然,这个任务非常简单(对于人类来讲),一般的人类能一眼看到最优路径,但是电脑不会,电脑只能通过一遍又一遍的(reinforcemental)学习从而知道怎么解决这个任务(甚至不一定是最优方案)。

在这里有一些设定:超过边缘视为无动作。如(0, 0)处向左向上走,视为这一步是停止。如此,这就是今天要涉及到的环境了。

奖励驱动设定

不同的奖励驱动达到的目标效果不一样,如果想让他尽早达到终点,可以让他每走一步给出负奖励,他为了让奖励最大化,就能尽早走到终点。如果想让他多走几个格子,可是让他每走到一个和当前路径不相交的位置时给一个正奖励,他应该(因为我没试过)会走遍格子最后到终点。

到达终点给正奖励,掉下悬崖给负奖励这就不说了。

环境代码

  1. # -*- coding: utf-8 -*-
  2. import random
  3. import numpy as np
  4. import gym
  5. from gym import spaces
  6. """
  7. nrows
  8. 0 1 2 3 4 5 6 7 8 9 10 11 ncols
  9. ---------------------------------------
  10. 0 | | | | | | | | | | | | |
  11. ---------------------------------------
  12. 1 | | | | | | | | | | | | |
  13. ---------------------------------------
  14. 2 | | | | | | | | | | | | |
  15. ---------------------------------------
  16. 3 * | cliff | ^ |
  17. *: start point
  18. cliff: cliff
  19. ^: goal
  20. """
  21. class CustomCliffWalking(object):
  22. def __init__(self, stepReward: int=-1, cliffReward: int=-10, goalReward: int=10) -> None:
  23. self.sr = stepReward
  24. self.cr = cliffReward
  25. self.gr = goalReward
  26. self.action_space = spaces.Discrete(4) # 上下左右
  27. self.pos = np.array([3, 0], dtype=np.int8) # agent 在3,0处出生,掉到悬崖内就会死亡,触发done和cliffReward
  28. def reset(self, random_reset=False):
  29. """
  30. 初始化agent的位置
  31. random: 是否随机出生, 如果设置random为True, 则出生点会随机产生
  32. """
  33. x, y = 3, 0
  34. if random_reset:
  35. y = random.randint(0, 11)
  36. if y == 0:
  37. x = random.randint(0, 3)
  38. else: # 除了正常坐标之外,还有一个不正常坐标:(3, 0)
  39. x = random.randint(0, 2)
  40. # 严格来讲,cliff和goal不算在坐标体系内
  41. # agent 在3,0处出生,掉到悬崖内就会死亡,触发done和cliffReward
  42. self.pos = np.array([x, y], dtype=np.int8)
  43. def step(self, action: int) -> list[list, int, bool, bool, dict]:
  44. """
  45. 执行一个动作
  46. action:
  47. 0: 上
  48. 1: 下
  49. 2: 左
  50. 3: 右
  51. """
  52. move = [
  53. np.array([-1, 0], dtype=np.int8), # 向上,就是x-1, y不动,
  54. np.array([ 1, 0], dtype=np.int8), # 向下,就是x+1, y不动,
  55. np.array([0, -1], dtype=np.int8), # 向左,就是y-1, x不动,
  56. np.array([0, 1], dtype=np.int8), # 向右,就是y+1, x不动,
  57. ]
  58. new_pos = self.pos + move[action]
  59. # 上左不能小于0
  60. new_pos[new_pos < 0] = 0 # 超界的处理,比如0, 0 处向上或者向右走,处理完还是0,0
  61. # 上右不能超界
  62. if new_pos[0] > 3:
  63. new_pos[0] = 3 # 超界处理
  64. if new_pos[1] > 11:
  65. new_pos[1] = 11
  66. reward = -1
  67. die = False
  68. win = False
  69. info = {
  70. "reachGoal": False,
  71. "fallCliff": False,
  72. }
  73. die = self.__is_pos_die(new_pos.tolist())
  74. if die:
  75. info["fallCliff"] = True
  76. reward = self.cr
  77. win = self.__is_pos_win(new_pos.tolist())
  78. if win:
  79. info["reachGoal"] = True
  80. reward = self.gr
  81. self.pos = new_pos # 更新坐标
  82. return new_pos, reward, die, win, info
  83. def __is_pos_die(self, pos: list[int, int]) -> bool:
  84. """判断自己的这个状态是不是已经结束了"""
  85. return pos in [
  86. [3, 1],
  87. [3, 2],
  88. [3, 3],
  89. [3, 4],
  90. [3, 5],
  91. [3, 6],
  92. [3, 7],
  93. [3, 8],
  94. [3, 9],
  95. [3, 10],
  96. [3, 11],
  97. ]
  98. def __is_pos_win(self, pos: list[int, int]) -> bool:
  99. """判断自己的这个状态是不是已经结束了"""
  100. return pos in [
  101. [3, 11],
  102. ]

学习方式

现在有了环境,有了驱动,怎么学习呢?

时序差分算法

时序差分算法其实很简单:

比如还是mario站在管道上(这个场景记为S_now),区分两种情况(两种动作,a):1)蹲下去;2)向前走。

1) :mario这时候想,蹲下去有很多金币(假设每个奖励是1,下面差不多有不到30个,按照50个算吧),回报是50,然后还不容易死(99%通关吧,1%死),通关回报又是50,死亡回报-100。

即:当前状态+蹲下去 -> 吃很多金币得到50 -> 可能通关可能死的回报 0.99*50 + 0.01* (-100)=48.5.

但是由于通关太远了,所以现在应该打折扣,比如有一个折扣因子(discount factor)γ=0.9,所以当前状态下蹲下去的期望总回报是:50 + γ * 48.5 = 93.65的回报。

2) mario这时候想,我向前走可以多条命,虽然容易死,但是只要不死就是多条命,何乐而不为呢?一条命是100金币,回报就是100,但是通关的概率是50%,通关是50回报。死亡是 -100 回报(因为少了条命)。

即:当前状态+继续走 -> +1条命是100回报 -> 可能通关可能死的回报 0.5*50+0.5*(-100)=-25。

所以当前向前走的期望总回报是:100 + γ * (-25) = 77.5。

假设mario走第一条路,所以当前状态下他下蹲的期望回报Q(S_now, 下蹲)(Q就是当前状态下他下蹲的期望回报)就是93.65。

然后他下蹲,拿到了管道下的金币奖励50,出了管道,然后他面临两个蘑菇(这个时候记为S_next)。这时候他面临:

当前状态+撞上去(假设他之前算的不对,发现避不开了,自己死亡的概率是100%) -> 立死 -100

当前状态+跳过去 (假设他之前算的不对,现在发现存活概率是0%)-> 肯定能通关了+50

这俩的期望回报(也就是V(S_next))是:-100(虽然有些极端,但是能理解就行)。

到这里,有两个数据:

1. 走了一步之后的立即回报50(记为R) + gamma * 下一个状态下的期望回报(V(S_next))-100共-40.

2. (贪心的mario)走之前算的那时候的期望回报(V(S_now))是93.65

V(S_now):可以看作是,我认为我可以得到这么多奖励。

Q(S_now, a):我认为我执行a动作可以得到这么多奖励

R +V(S_next):可以看作是:我实际上可以得到这么多奖励。

这时就可以求出误差:error = R + V(S_next)   -   V(S_now) = -40 - 93.65 = -133.65,就知道自己计算的差别在哪了。

然后可以设一个学习率因子 lr = 0.5,更新 Q(S_now, a)。

Q(S_now, a) = Q(S_now, a) + lr * error = 93.65 - 0.5 * (-133.65)

这个error,就是时序误差。按照这样的方式,agent就能一遍又一遍地(reinforcementally)纠正自己的估计错误。直到自己估计正确。

SARSA

现在就要引入强化学习很经典的一个算法了:SARSA,是一个on-policy(这个国内翻译版本不是唯一的,所以我就不翻译了)的TD(时序差分,time difference)算法。

SARSA和上面差不多,只不过在S_next处会走一步,计算Q(S_next, a)进行计算误差更新。这也就是为啥他叫SARSA(S_now, action_now, reward, S_next, action_next)方式。

大概是:

Q(S_now, action_now)  = Q(S_now, action_now) +

        lr * (Reward_now + gamma * Q(S_next, action_next)  -  Q(S_now, action_now) )

今天就会使用SARSA算法进行这个cliffwalking的更新。

强化学习代码(表格型)

由于这个Cliff Walking任务很简单,可以用一个表格来模拟,这样的话,更直观,容易理解。

这个任务是4*12的表格,每个位置有四个动作,所以形状是4x12x4

Q = np.zeros((4, 12, 4), dtype=np.float32)  # 价值表格,

然后实例化环境:

cw = CustomCliffWalking(stepReward=sr, cliffReward=cr, goalReward=gr)

然后根据上面的更新公式实现代码,完整代码如下:

  1. # -*- coding: utf-8 -*-
  2. import random
  3. import numpy as np
  4. from env.cliffwalking import CustomCliffWalking
  5. import matplotlib.pyplot as plt
  6. nepisodes = 100000 # total 10w episodes
  7. epsilon = 0.05 # epsilon greedy policy
  8. gamma = 0.9 # discount factor
  9. lr = 0.1
  10. random_reset = False
  11. seed = 42
  12. sr = -1
  13. cr = -10
  14. gr = 10
  15. def select_action(Q: np.ndarray, pos: np.ndarray, nact: int, epsilon=0) -> int:
  16. """选择动作,默认是贪心,"""
  17. # epsilon贪心算法选择动作,也可以把epsilon设置为0,就是完全贪心选择动作
  18. if random.random() < epsilon:
  19. action = random.randint(0, nact-1)
  20. else: # 按照表格选取动作,如果多个动作价值一样,则取下标靠前的
  21. action = np.argmax(Q[pos[0], pos[1], :])
  22. return action
  23. def main():
  24. """实现悬崖徒步,表格形式的"""
  25. np.random.seed(seed=seed)
  26. random.seed(seed)
  27. Q = np.zeros((4, 12, 4), dtype=np.float32) # 价值表格,
  28. cw = CustomCliffWalking(stepReward=sr, cliffReward=cr, goalReward=gr) # 实例化环境
  29. nact = cw.action_space.n
  30. for i in range(1, nepisodes + 1):
  31. if i % 1000 == 0:
  32. print("{}/{}".format(i, nepisodes))
  33. cw.reset(random_reset=random_reset) # 不随机产生位置,随机应该更好一点,这里不随机产生了
  34. steps = 0
  35. while True:
  36. steps += 1
  37. old_pos = cw.pos # 保留旧的位置,也就是 S_now
  38. action = select_action(Q=Q, pos=old_pos, nact=nact, epsilon=epsilon) # 也就是 action_now
  39. # print(new_pos, reward, die, win, info)
  40. new_pos, reward, die, win, info = cw.step(action=action)
  41. # 这里得到了 S_next 和 Reward_now
  42. action_next = select_action(Q=Q, pos=new_pos, nact=nact, epsilon=epsilon)
  43. # 这里是 action_next
  44. # 如果死了或者过关了,那么就没有后续了,就不需要后面的了
  45. actual_reward = reward + (1-(die or win)) * gamma * Q[new_pos[0], new_pos[1], action_next]
  46. # 计算走一步的instant + gamma * Q(S_next, a_next)
  47. target_reward = Q[old_pos[0], old_pos[1], action] # Q(S_now, a)
  48. # print("target_reward:", target_reward)
  49. bellman_error = actual_reward - target_reward # 计算估计的误差
  50. Q[old_pos[0], old_pos[1], action] = Q[old_pos[0], old_pos[1], action] + lr * bellman_error
  51. # Q(S_now, action_now) = Q(S_now, action) + lr * 误差
  52. if die or win:
  53. break # 胜利或失败
  54. # 训练完了,具象化显示学习到的价值
  55. for i in range(nact):
  56. plt.subplot(nact, 1, i+1)
  57. plt.imshow(Q[:, :, i])
  58. plt.axis('off')
  59. plt.colorbar()
  60. if i == 0:
  61. plt.title("up")
  62. elif i == 1:
  63. plt.title("down")
  64. elif i == 2:
  65. plt.title("left")
  66. elif i == 3:
  67. plt.title("right")
  68. plt.savefig("./out/table/Q_sarsa_"+str(sr)+"_"+str(gr)+"_"+str(cr)+".png")
  69. plt.clf()
  70. plt.close()
  71. path = np.zeros((4, 12), dtype=np.float64)
  72. cw.reset()
  73. x = cw.pos[0]
  74. y = cw.pos[1]
  75. while True: # 走
  76. # 贪心算法选择动作
  77. action= np.argmax(Q[x, y, :])
  78. print(x, y, action)
  79. new_pos, reward, die, win, info = cw.step(action=action)
  80. x, y = new_pos[0], new_pos[1]
  81. if win:
  82. print("[+] you win!")
  83. break
  84. if die:
  85. print("[+] you lose!")
  86. break
  87. x = new_pos[0]
  88. y = new_pos[1]
  89. if x >= 0 and x <= 3 and y >= 0 and y <= 11:
  90. path[x, y] = 1.0
  91. plt.imshow(path)
  92. plt.colorbar()
  93. plt.savefig("./out/table/path_sarsa_"+str(sr)+"_"+str(gr)+"_"+str(cr)+".png")
  94. # 保存学习到的价值
  95. np.savetxt("out/table/cliff_walking_table_{}_{}_上.csv".format(gr, cr), Q[:,:,0],
  96. delimiter="\t", fmt="%.2f")
  97. np.savetxt("out/table/cliff_walking_table_{}_{}_下.csv".format(gr, cr), Q[:,:,1],
  98. delimiter="\t", fmt="%.2f")
  99. np.savetxt("out/table/cliff_walking_table_{}_{}_左.csv".format(gr, cr), Q[:,:,2],
  100. delimiter="\t", fmt="%.2f")
  101. np.savetxt("out/table/cliff_walking_table_{}_{}_右.csv".format(gr, cr), Q[:,:,3],
  102. delimiter="\t", fmt="%.2f")
  103. if __name__ == "__main__":
  104. main()

结果

我的CPU还是很快就运行完了,,因该也不会太慢。。如果你的太慢,我试了试,一万个回合的结果也收敛了。

(注意:运行时确保环境内无其他程序使用matplotlib,否则会出现闪退情况)

价值计算图

行走路径图

计算的价值结果

这个是每个位置向上的价值。

 这个是每个位置向下的价值,可以看到,目标点上面向下都是10,悬崖上面向下都是-10,和我们的预期一样。

 这个是每个位置向左的价值。可见,每一行越往左,这个价值越低,和我们预期也一样,因为越向左越远,按理来讲折扣价值就是更低。

 这个是每个位置向右的价值,可见越向右价值越高。(除了地图边缘处向右是为了给自己多-1的惩罚)

 结束语

本来想着稍微写一下,写完之后发现竟然达到了八千多字,应该分开写的,,下次我会加入神经网络的元素,希望大家看完能有所收获!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/926148
推荐阅读
相关标签
  

闽ICP备14008679号