这是训练机器人走迷宫系列文章的第二篇也是最后一篇,在这篇文章中我们将讲解gym库的使用,并讲解我们实现的使用Q Learning成功学会100%走出8X8迷宫的机器人代码。
如果读者有需要了解项目的基本信息和Q Learning的概念和算法,可以从下边的链接进入系列文章的第一篇:
训练机器人走迷宫1 – 强化学习之Q Learning
1.Gym库和迷宫环境
在开始之前,请确保安装了gym, numpy, tqdm和matplotlib库,如果没有的话需要pip一下。安装成功以后,导入必要的库,这次我们将用到下边这些库。
1
2
3
4
5
6
7
|
from os import system
import time
import gym
import numpy as np
from tqdm import tqdm
import pickle
import matplotlib.pyplot as plt
|
简单说明一下重点的部分:
- gym - 为我们提供了迷宫环境,是我们项目的主要环境
- tqdm - 一个可以生成命令行进度条的库,可以方便我们直观的知道训练进度
- pickle - python自带的数据序列号库,用于将python数据结构存储到文件中,我们利用它来将训练好的Q Table保存下来
- matplotlib - 用来绘制一个机器人性能随着训练次数的变化曲线
我们先来研究一下Gym给我们提供的这个迷宫环境,使用下边代码来打印一些关键数据结果出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
env = gym.make('FrozenLake-v0', map_name='8x8', is_slippery=False)
print()
print("Observation Space")
print(env.observation_space)
print("Action Space")
print(env.action_space)
observation = env.reset()
print()
print("Maze Reset")
env.render()
observation, reward, done, info = env.step(1)
print("Robot moved by one step(down)")
env.render()
print()
print(f"observation={observation}")
print(f"reward={reward}")
print(f"done={done}")
env.close()
|
第1行代码,我们使用gym库的make方法将迷宫环境创建出来,这个方法接收一个字符串参数,用来生成对应的环境,因为我们用到的是FrozenLake-v0这个环境,因此将其作为字符串传入,我们的机器人走出一个8X8大小的迷宫,同时is_slippery设置为False,is_slippery参数控制机器人会不会在行走中打滑,如果为True的话,gym迷宫环境会让机器人随机打滑,假设机器人想要像右走一步,那么有可能会因为打滑,它又向左划了一步,体现到结果上就是它在原地没有动,因为打滑的方向是随机的,这会给机器人学习路径带来困难,对于本次Q Learning我们将禁止这个特性,以便达到更好的效果。
第5行和第7行代码分别打印出了observation空间和action空间的状态,observation空间表示机器人和环境的状态(一共有多少种状态),我们这个环境有64种状态,对应于机器人出现在8X8迷宫中的任何一个位置,action空间表示机器人每次可以从几种行为中选择一种来行动,我们这个环境提供了4中行为,即向上,向下,向左,向右。
第9行代码,我们调用env的reset方法,对环境进行重置,重置环境意味着机器人要从头开始走迷宫了,每一次Episode训练之前都需要重置环境。reset方法会返回重置以后的环境状态(state),这个状态永远是机器人处在S位置。第13行的render方法会将reset之后的状态打印在终端中。
第15行,我们使用env的step方法,命令机器人执行编号为1的动作,即让机器人向下走,这个方法会返回当前的状态(observation)观察者给这一步动作的打分(reward),游戏有没有结束(done,即机器人有没有走到H位置或者到达G位置),最后还有一些调试信息(info),对于Q Learning的学习,我们会用到observation(用于从Q Table查找下个action),reward(用于更新Q Table),done(用于判断当前Episode是否结束)。随后的render方法将执行完这个动作以后的状态打印在终端。
2.Q Learning实现
当我们掌握了gym的环境以后,就可以套用在第一篇文章中介绍的算法,将其转换为python代码来进行机器人训练。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
LEARNING_RATE = 0.1
DISCOUNT = 0.95
EPISODES = 20000
STATS_EVERY = 100
SHOW_EVERY = 1000
epsilon = 1
START_EPSILON_DECAYING = 1
END_EPSILON_DECAYING = EPISODES*7//10
epsilon_decay_value = epsilon/(END_EPSILON_DECAYING - START_EPSILON_DECAYING)
env = gym.make('FrozenLake-v0',map_name="8x8",is_slippery=False)
def render_env(env):
system('clear')
env.render(mode='human')
ep_rewards = []
aggr_ep_rewards = {'ep': [], 'avg': [], 'max': [], 'min': []}
q_table = np.random.uniform(low=-1, high=1, size=([env.observation_space.n, env.action_space.n]))
for episode in tqdm(range(EPISODES)):
episode_reward = 0
done = False
observation = env.reset()
while not done:
# lookup q_table to get next action
if np.random.random() > epsilon:
# Get action from Q table
action = np.argmax(q_table[observation])
else:
# Get random action
action = np.random.randint(0, env.action_space.n)
new_observation, reward, done, info = env.step(action)
if episode % SHOW_EVERY == 0:
render_env(env)
time.sleep(0.1)
episode_reward += reward
if not done:
max_future_q = np.max(q_table[new_observation])
current_q = q_table[observation][action]
new_q = (1 - LEARNING_RATE) * current_q + LEARNING_RATE * (reward + DISCOUNT * max_future_q)
q_table[observation][action] = new_q
else:
q_table[observation][action] = reward
observation = new_observation
if END_EPSILON_DECAYING >= episode >= START_EPSILON_DECAYING:
epsilon -= epsilon_decay_value
ep_rewards.append(episode_reward)
if not episode % STATS_EVERY:
average_reward = sum(ep_rewards)/len(ep_rewards)
aggr_ep_rewards['ep'].append(episode)
aggr_ep_rewards['avg'].append(average_reward)
aggr_ep_rewards['max'].append(max(ep_rewards))
aggr_ep_rewards['min'].append(min(ep_rewards))
ep_rewards = []
env.close()
with open('q_table_saved.pkl', 'wb') as f:
pickle.dump(q_table, f)
plt.plot(aggr_ep_rewards['ep'], aggr_ep_rewards['avg'], label="average rewards")
plt.plot(aggr_ep_rewards['ep'], aggr_ep_rewards['max'], label="max rewards")
plt.plot(aggr_ep_rewards['ep'], aggr_ep_rewards['min'], label="min rewards")
plt.legend()
plt.show()
|
首先我们使用np.random随机的初始化了q_table,而q_table的大小为64X4的二维表,第一维是环境状态,第二维是行为,表中的每一个元素代表该状态下做某一个行为的打分(Q Value),后续的代码都是通过Q Learning算法来更新这些打分
1
|
q_table = np.random.uniform(low=-1, high=1, size=([env.observation_space.n, env.action_space.n]))
|
接着我们使用for循环来创建训练迭代,我们将尝试EPISODES=20000个Episode,在每次一尝试前,将env重置,当本次尝试中机器人没有掉坑或者成功走出去则不停的使用env.step方法让机器人移动,移动的方向使用探索和利用规则(第一篇文章中有详细介绍)来选择,在学习前期,让机器人尽可能多的使用随机的方式移动来探索环境,在学习后期,让机器人尽可能多的利用已经学会的知识(Q Table中的值)来移动,即根据当前环境状态,选取状态下对应的分数最高的action来移动,我们使用了递减的epsilon来控制什么时候为学习前期,什么时候为学习后期。
epsilon从1开始不停的递减,每次机器人移动以前会生成一个在0到1之间的随机数,当这个随机数大于epsilon的时候,从Q Table中选择行为,当这个随机数小于epsilon的时候则随机移动。因此,最初,epsilon比较大,机器人倾向于随机移动来探索环境,随着epsilon的减小,机器人慢慢倾向于使用Q Table来移动,进入利用阶段。
每次移动之后,我们使用观察者给我们的打分来更新Q Table,如果本次Episode没有达到结束条件(掉坑或者走出去)则使用Q Value公式来更新,如果本次Episode达到结束条件,因为已经没有现在状态的max_futur_q了,则直接使用观察者给定的分数作为我们的Q Value。公式中包含有LEARNING_RATE和DISCOUNT的超参数,我们可以调整超参数的值,让机器人最终达到我们的预设目标。
1
2
3
4
5
6
7
|
if not done:
max_future_q = np.max(q_table[new_observation])
current_q = q_table[observation][action]
new_q = (1 - LEARNING_RATE) * current_q + LEARNING_RATE * (reward + DISCOUNT * max_future_q)
q_table[observation][action] = new_q
else:
q_table[observation][action] = reward
|
我们通过一个episode_reward来统计一个 Episode的总分,在每次移动,我们都会将观察者针对移动的打分加到本次Episode的总分上,因为这个迷宫环境只有在机器人到达G位置的时候打分为1,其它状态的打分均为0,因此,每个Episode的总分不是0就是1。
然后我们没100个Episode会计算一下平均打分,打分最大值和最小值,最后,当20000训练结束以后,使用matplotlib绘制机器人学习质量曲线,我们期望从某个Episode开始,连续100个Episode的平均打分,打分最大值和最小值均为1,即达到了我们预设的目标。
最后,我们使用pickle库将训练好的Q Table保存到文件中,以后,不用训练直接加载这个Q Table就可以直接让机器人走出迷宫。
3.训练结果
使用我们源代码中的超参数,经过训练以后,在14000个Episode之后,能够实现连续100个Episode都全部成功走出迷宫。本项目的代码可以从GitHub仓库:https://github.com/pythonlibrary/ai-plays-maze-q_learning 获得
下边这个视频展示了,模型从最初训练,到最终达到目标以后的走迷宫效果。