使用Q-Learning 和 Sara 解决GridWorld 炸弹环境
一.实验原理
1.1 Q-learning 和 Sara 的异同
1.1.1 相似之处
- 两种算法本质都是通过策略迭代得到最优策略。
- 两种算法都是基于时序差分法进行更新,可以看作蒙特卡洛仿真和动态规划的结合。
- 在选择策略时,都使用 ϵ−greedy\epsilon - greedyϵ−greedy 算法,即以ϵ\epsilonϵ 的概率选择使得动作-值函数最大的动作,以1−ϵ1-\epsilon1−ϵ的概率随机选择。
1.1.2 不同之处
Q-Learning是强化学习算法中value-based的算法,Q即为Q(s,a)就是在某一时刻的 s 状态下(s∈S),采取 动作a (a∈A)动作能够获得收益的期望,环境会根据agent的动作反馈相应的回报reward r,所以算法的主要思想就是将State与Action构建成一张Q-table来存储Q值,然后根据Q值来选取能够获得最大的收益的动作。
Q-learing 算法可用如下伪代码表示:
Sara和Q-Learning基本一致,可用如下伪代码表示:
从两个算法的伪代码可以看出,两者的最大区别在于Q-table的更新方式不同:
Q-Learning更新Q值的公式为:
Q(St,At)←Q(St,At)+α[Rt+1+γmaxaQ(St+1,a)−Q(St,At)]Q(S_t,A_t) \leftarrow Q(S_t, A_t) + \alpha[R_{t+1}+\gamma \underset{a}{max}Q(S_{t+1},a)-Q(S_t,A_t)]Q(St,At)←Q(St,At)+α[Rt+1+γamaxQ(St+1,a)−Q(St,At)]
Sara更新Q值的公式为:
Q(St,At)←Q(St,At)+α[Rt+1+γQ(St+1,At+1)−Q(St,At)]Q(S_t,A_t)\leftarrow Q(S_t,A_t)+\alpha[R_{t+1}+\gamma Q(S_{t+1},A_{t+1})-Q(S_t,A_t)]Q(St,At)←Q(St,At)+α[Rt+1+γQ(St+1,At+1)−Q(St,At)]
- **Q-learning:**在状态StS_tSt下,根据 ϵ−greedy\epsilon-greedyϵ−greedy策略选择动作AtA_{t}At 到达St+1S_{t+1}St+1后,利用状态St+1S_{t+1}St+1下的最佳Q值Q(St+1,a)Q(S_{t+1},a)Q(St+1,a)来更新Q(St,At)Q(S_t,A_{t})Q(St,At),但并不真正采取动作(St+1,a)(S_{t+1},a)(St+1,a) 。更新Q-table用到的值有<St,At,reward,St+1><S_t,A_t,reward,S_{t+1}><St,At,reward,St+1>
- Sara: 在状态StS_tSt下,根据 ϵ−greedy\epsilon-greedyϵ−greedy 策略选择动作AtA_tAt到达St+1S_{t+1}St+1之后,选择最大的(St+1,a)(S_{t+1},a)(St+1,a)并真正采取该动作。更新Q-table用到的值有<St,At,reward,St+1,At+1><S_t,A_t,reward,S_{t+1},A_{t+1}><St,At,reward,St+1,At+1>
- Q−learning选取动作和更新Q表值的方法不同,而Sarsa选取动作和更新Q表值的方法相同。Q-Learning算法,先假设下一步选取最大奖赏的动作,更新值函数。然后再通过ε-greedy策略选择动作。Sarsa算法,先通过ε-greedy策略执行动作,然后根据所执行的动作,更新值函数。
- 可以看出Q-Learning使用的更新方法更激进,即直接选择下一个状态下的最大值进行更新。而Sara算法更保守,基于现有的步骤进行更新,整体上来说Sara更偏向于避免陷阱。
1.2 算法图解
两种算法的基本流程出了训练过程中更新参数的方法不同,其余流程相同。可用下图表示:
二.算法实现
整体分为环境类和代码类。
2.1 环境
定义类FronzenLakeWapper(gym.Wrapper)
,主要实现以下接口:
draw_box
: 绘制一个坐标处的矩形框,并做以下填充:
-
起点:红色
-
出口:黄色
-
炸弹:黑色
-
平地:白色
move_player(self, x, y)
:将智能体移动到对应的坐标
render(self)
:渲染一帧图像
step(self,action)
:根据传入的动作,计算智能体的新坐标,以及对应的返回值。为了训练智能体避免炸弹并且尽量减少路径长度,将奖励值设置如下:
- 起点或空地:
reward = -2
- 炸弹:
reward = -20
- 终点:
reward = 10
代码文件’gridWorld.py’如下:
import gym
import turtle
import time
class FrozenLakeWapper(gym.Wrapper):
def __init__(self, env):
gym.Wrapper.__init__(self, env)
self.max_y = env.desc.shape[0] # 行数
self.max_x = env.desc.shape[1] # 列数
self.t = None
self.unit = 50
def draw_box(self, x, y, fillcolor='', line_color='gray'):
self.t.up()
self.t.goto(x * self.unit, y * self.unit)
self.t.color(line_color)
self.t.fillcolor(fillcolor)
self.t.setheading(90)
self.t.down()
self.t.begin_fill()
for _ in range(4):
self.t.forward(self.unit)
self.t.right(90)
self.t.end_fill()
def move_player(self, x, y):
self.t.up()
self.t.setheading(90)
self.t.fillcolor('blue')
self.t.goto((x + 0.5) * self.unit, (y + 0.5) * self.unit)
def render(self):
if self.t == None:
self.t = turtle.Turtle()
self.wn = turtle.Screen()
self.wn.setup(self.unit * self.max_x + 100,
self.unit * self.max_y + 100)
self.wn.setworldcoordinates(0, 0, self.unit * self.max_x,
self.unit * self.max_y)
self.t.shape('circle')
self.t.width(2)
self.t.speed(0)
self.t.color('gray')
for i in range(self.desc.shape[0]):
for j in range(self.desc.shape[1]):
x = j
y = self.max_y - 1 - i
if self.desc[i][j] == b'S': # 起点
self.draw_box(x, y, 'red')
elif self.desc[i][j] == b'F': # 空地
self.draw_box(x, y, 'white')
elif self.desc[i][j] == b'G': # 终点
self.draw_box(x, y, 'yellow')
elif self.desc[i][j] == b'H': # 炸弹
self.draw_box(x, y, 'black')
else:
self.draw_box(x, y, 'white')
self.t.shape('turtle')
# time.sleep(20)
x_pos = self.s % self.max_x
y_pos = self.max_y - 1 - int(self.s / self.max_x)
self.move_player(x_pos, y_pos)
def step(self, action):
observation, reward, done, info, _ = self.env.step(action)
x_pos = int(observation / self.desc.shape[1])
y_pos = int(observation % self.desc.shape[1])
if self.desc[x_pos][y_pos] == b'F':
reward -= 2
done = False
elif self.desc[x_pos][y_pos] == b'S':
reward -= 2
done = False
elif self.desc[x_pos][y_pos] == b'G':
reward += 10
done = True
info = "Success"
elif self.desc[x_pos][y_pos] == b'H':
reward -= 20
done = True
info = "Faild"
return observation, reward, done, info, _
def GridWorld(gridmap=None, is_slippery=False):
# 环境3:自定义格子世界,可以配置地图, S为出发点Start, F为平地Floor, H为洞Hole, G为出口目标Goal
# 0 left, 1 down, 2 right, 3 up
if gridmap is None:
gridmap = ['SFFF', 'FHFH', 'FFFH', 'HFFG']
env = gym.make("FrozenLake-v1", desc=gridmap, is_slippery=False)
env = FrozenLakeWapper(env)
# print(f"env.max_x{env.max_x}, env.max_y{env.max_y}")
return env
2.2 智能体
根据使用的算法不同,分别创建类QLearningAgent(object)
和 SaraAgent(object)
。
两个类有以下相同接口:
sample(self, obs)
:根据输入的观察值,使用ϵ−greedy\epsilon-greedyϵ−greedy 策略选择动作。
predict(self, obs)
: 根据输入的观察值,预测输出的动作值。
save(self, npy_file)
: 将Q表保存到文件中。
restore(self, npy_file)
: 从文件中读取Q表数据。
根据Q表更新公式的不同,实现不同的学习函数。
QLearningAgent.learn(self,obs,action,next_obs,reward,done)
:根据当前状态和动作以及下个状态更新Q表。
QLearningAgent.learn(self,obs,action,next_obs,next_action,reward,done)
:根据当前状态和动作以及下个状态和下个动作更新Q表。
代码文件agent.py
如下:
# -*- coding: utf-8 -*-
import numpy as np
import os
# Qlearnsing 算法
class QLearningAgent(object):
def __init__(self,
obs_n,
act_n,
learning_rate=0.01,
gamma=0.9,
e_greed=0.1):
self.act_n = act_n # 动作维度,有几个动作可选
self.lr = learning_rate # 学习率
self.gamma = gamma # reward的衰减率
self.epsilon = e_greed # 按一定概率随机选动作
self.Q = np.zeros((obs_n, act_n))
# 根据输入观察值,采样输出的动作值,带探索
def sample(self, obs):
if np.random.uniform(0, 1) < (1.0 - self.epsilon): #根据table的Q值选动作
action = self.predict(obs)
else:
action = np.random.choice(self.act_n) #有一定概率随机探索选取一个动作
return action
# 根据输入观察值,预测输出的动作值
def predict(self, obs):
# print(type(self.Q), self.Q.shape, obs)
Q_list = self.Q[obs, :]
maxQ = np.max(Q_list) # 最大Q值对应的动作即为最优动作
action_list = np.where(Q_list == maxQ)[0] # maxQ可能对应多个action
action = np.random.choice(action_list)
return action
# 学习方法,也就是更新Q-table的方法
def learn(self, obs, action, next_obs, next_action, reward, done):
""" off-policy
obs: 交互前的obs, s_t
action: 本次交互选择的action, a_t
reward: 本次动作获得的奖励r
next_obs: 本次交互后的obs, s_t+1
done: episode是否结束
"""
predict_Q = self.Q[obs, action]
if done:
target_Q = reward # 没有下一个状态了
else:
target_Q = reward + self.gamma * np.max(
self.Q[next_obs, :]) # Q-learning
self.Q[obs, action] += self.lr * (target_Q - predict_Q) # 修正q
# 把 Q表格 的数据保存到文件中
def save(self, npy_file = './q_table.npy'):
np.save(npy_file, self.Q)
print(npy_file + ' saved.')
# 从文件中读取数据到 Q表格
def restore(self, npy_file='./q_table.npy'):
self.Q = np.load(npy_file)
print(npy_file + ' loaded.')
# Sara 算法
class SarsaAgent(object):
def __init__(self,
obs_n,
act_n,
learning_rate=0.01,
gamma=0.9,
e_greed=0.1):
self.act_n = act_n # 动作维度,有几个动作可选
self.lr = learning_rate # 学习率
self.gamma = gamma # reward的衰减率
self.epsilon = e_greed # 按一定概率随机选动作
self.Q = np.zeros((obs_n, act_n))
# 根据输入观察值,采样输出的动作值,带探索
def sample(self, obs):
if np.random.uniform(0, 1) < (1.0 - self.epsilon): #根据table的Q值选动作
action = self.predict(obs)
else:
action = np.random.choice(self.act_n) #有一定概率随机探索选取一个动作
return action
# 根据输入观察值,预测输出的动作值
def predict(self, obs):
# print(type(self.Q), self.Q.shape, obs)
Q_list = self.Q[obs, :]
maxQ = np.max(Q_list) # 最大Q值对应的动作即为最优动作
action_list = np.where(Q_list == maxQ)[0] # maxQ可能对应多个action
action = np.random.choice(action_list)
return action
# 学习方法,也就是更新Q-table的方法
def learn(self, obs, action, next_obs, next_action, reward, done):
""" off-policy
obs: 交互前的obs, s_t
action: 本次交互选择的action, a_t
reward: 本次动作获得的奖励r
next_obs: 本次交互后的obs, s_t+1
done: episode是否结束
"""
predict_Q = self.Q[obs, action]
if done: # 游戏结束
target_Q = reward # 没有下一个状态了
else:
target_Q = reward + self.gamma * self.Q[next_obs, next_action]
# 用 reward 和 交互后状态下,选择的下一个动作对应的 Q 值,综合得到新的 Q 值 \ Sarsa
self.Q[obs, action] += self.lr * (target_Q - predict_Q) # 修正q
# 把 Q表格 的数据保存到文件中
def save(self, npy_file = './qlearning/q_table.npy'):
np.save(npy_file, self.Q)
print(npy_file + ' saved.')
# 从文件中读取数据到 Q表格
def restore(self, npy_file='./sara/q_table.npy'):
self.Q = np.load(npy_file)
print(npy_file + ' loaded.')
主函数如下
import time
import agent
import gridWorld
import os
def train(env, robot, episodeAll=100, storeDir = "./result/qlearning/"):
file = open("output.txt","w") # 用于存储训练结果
preReward = 0 # 记录前一轮的奖励值
k = 0 # 记录奖励值多少局没有变化了
for episode in range(episodeAll):
done = False
steps = 0
rewardSum = 0
obs = env.reset()[0] # 初始状态
action = robot.sample(obs) # 根据初始状态选择一个动作
while done == False: # 直到踩雷或者到达终点
next_obs, reward, done, info, _ = env.step(action) # 跟环境交互,执行动作 (下个状态, 奖励, 是否结束, 信息, 其他)
next_action = robot.sample(next_obs) # 根据新的状态,选择新的动作
# 训练智能体
robot.learn(obs, action, next_obs, next_action, reward, done)
# 更新状态
obs = next_obs
action = next_action
# 计算总的奖励和步数
rewardSum += reward
steps += 1
# 渲染一帧图像
env.render()
# 如果连续5局奖励值没有变化,则认为已经训练完成,提前终止训练并保存Q表
if k == 5:
print("train Finish!")
robot.save(npy_file = f"{storeDir}q_table{episode}.npy")
return
else:
if preReward == rewardSum:
k += 1
else :
k = 0
preReward = rewardSum
# 打印信息并写入文件
print(f"episode : {episode}, steps : {steps}, sum of reward{rewardSum}, info : {info}")
file.write(f"episode : {episode}, steps : {steps}, sum of reward{rewardSum}, info : {info}\n") # 写入文档
# 每500局记录一次Q表
if episode % 500 ==0:
robot.save(npy_file = f"{storeDir}q_table{episode}.npy")
file.close()
return robot # 返回训练好的智能体
def test(env, robot):
done = False
steps = 0
rewardSum = 0
obs = env.reset()[0] # 初始状态
action = robot.sample(obs) # 根据初始状态选择一个动作
while done == False: # 直到踩雷或者到达终点
next_obs, reward, done, info, _ = env.step(action) # 跟环境交互,执行动作 (下个状态, 奖励, 是否结束, 信息, 其他)
next_action = robot.sample(next_obs) # 根据新的状态,选择新的动作
# 更新状态
obs = next_obs
action = next_action
# 计算总的奖励和步数
rewardSum += reward
steps += 1
# 渲染一帧图像
env.render()
time.sleep(3)
print(f"steps : {steps}, sum of reward{rewardSum}, info : {info}")
if info == "Success":
print(f"you take {steps} steps to success")
else :
print(f"Faild")
return rewardSum, steps
if __name__ == '__main__':
## 读取地图
gridmap = []
inputFile = "input.txt" # 输入迷宫
if os.path.exists(inputFile):
with open('input.txt', 'r', encoding='utf-8') as file:
gridmap = [line.strip() for line in file]
else: ## 如果读取失败,使用缺省地图
print("There is no input file named input.txt ")
gridmap = [ # 传入一个环境
'FFFSFFFFFFHFHFFFF',
'FHFFFFFHHHFFFFGFF',
'FHFFFFFHFGFFFFFHF',
'FFFHFFFGFGFFFFHHF',
'FFFGFFFFHGFFHFFFF',
'FFFHFFGFFFHFFHFFF',
'FFFFGFFFFFHFFHFFF' ]
obs_n = len(gridmap) * len(gridmap[0]) # 状态数,有多少个格子
episodeAll = 10001 #训练多少局
act_n = 4 # 动作空间,上下左右
preTrainFile = ""
# 创建环境
env = gridWorld.GridWorld(gridmap)
### Qlearning 算法
QLearningPreTrainDir = "./result/qlearning/"
storeDir = "./result/qlearning/"
# if os.path.exists(QLearningPreTrainDir) & len(os.listdir(QLearningPreTrainDir)):
# print(storeDir)
if os.path.exists(QLearningPreTrainDir) & len(os.listdir(QLearningPreTrainDir)):
preTrainFile = os.path.join(storeDir, os.listdir(QLearningPreTrainDir)[-1])
robot = agent.QLearningAgent(obs_n = obs_n, gamma=0.6, act_n = act_n)
##### Sarsa 算法
# SaraPreTrainDir = "./result/sara/"
# storeDir = "./result/sara/"
# if os.path.exists(SaraPreTrainDir) & len(os.listdir(QLearningPreTrainDir)):
# preTrainFile = os.path.join(SaraPreTrainDir, os.listdir(QLearningPreTrainDir)[-1])
# robot = agent.SarsaAgent(obs_n = obs_n, act_n = act_n)
# 如果有保存的训练结果,加载结果Q表
if os.path.exists(preTrainFile):
print(preTrainFile)
robot.restore(preTrainFile )
## 训练
print("train Begin!")
robot = train(env, robot, episodeAll = episodeAll, storeDir = storeDir)
## 测试
print("test Begin!")
test(env, robot)
三.实验结果及分析
使用文件输入,在input.txt
中输入矩阵,例如下图,输入一个7×177\times 177×17 的矩阵,其中S表示起点,F表示空地,H表示炸弹,G表示出口。
3.3 学习参数对策略收敛的影响
训练过程中,当连续五局游戏都成功且总奖励值不变时,认为模型已经收敛
3.3.1 Q-Learning 算法
模型的收敛速度随着回报衰减系数变化如下图:
从图中可以看出,随着gamma值的增大,模型收敛速度越来越快,从Q-Learning的Q表更新公式可以看出,gamma值越大,更新程度越大,所需的训练次数也越小。
对各个gmma值下的收敛模型进行1000次测试,所得的成功率和平均步数如下:
从图中可以看出,当gamma=0.6时,模型的成功率最高并且平均步数最少。原因可能是:gamma值教小时,无法充分学习每步的未来收益,而gamma值过大时,模型采取的策略过于激进,可能出现过拟合。
3.3.2 Sara 算法
模型的收敛速度随着回报衰减系数变化如下图:
可以看出,整体来说,随着gamma值的变大,模型收敛所需的训练次数逐渐减少。但gamma从0.1变为0.2时,训练次数显著增加,可能是gamma=0.1时模型陷入局部最优。
对各个gmma值下的收敛模型进行1000次测试,所得的成功率和平均步数如下:
从图中可以看出,当 gamma=0.7时,模型的成功率最高并且平均步数最少,原因可能是取一个适中的gamma值更能平衡当前收益和未来收益。同时可以看出,当gamma值由0.1变为0.2时,平均步数显著减少,可能是gamma=0.1时模型陷入局部最优。