使用Q-Learning 和 Sara 解决GridWorld 炸弹环境

6 篇文章 0 订阅
2 篇文章 0 订阅

使用Q-Learning 和 Sara 解决GridWorld 炸弹环境

一.实验原理
1.1 Q-learning 和 Sara 的异同
1.1.1 相似之处
  1. 两种算法本质都是通过策略迭代得到最优策略。
  2. 两种算法都是基于时序差分法进行更新,可以看作蒙特卡洛仿真和动态规划的结合。
  3. 在选择策略时,都使用 ϵ − g r e e d y \epsilon - greedy ϵgreedy 算法,即以 ϵ \epsilon ϵ 的概率选择使得动作-值函数最大的动作,以 1 − ϵ 1-\epsilon 1ϵ的概率随机选择。
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 ( S t , A t ) ← Q ( S t , A t ) + α [ R t + 1 + γ m a x a Q ( S t + 1 , a ) − Q ( S t , A t ) ] 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 ( S t , A t ) ← Q ( S t , A t ) + α [ R t + 1 + γ Q ( S t + 1 , A t + 1 ) − Q ( S t , A t ) ] 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)]

  1. **Q-learning:**在状态 S t S_t St下,根据 ϵ − g r e e d y \epsilon-greedy ϵgreedy策略选择动作 A t A_{t} At 到达 S t + 1 S_{t+1} St+1后,利用状态 S t + 1 S_{t+1} St+1下的最佳Q值 Q ( S t + 1 , a ) Q(S_{t+1},a) Q(St+1,a)来更新 Q ( S t , A t ) Q(S_t,A_{t}) Q(St,At),但并不真正采取动作 ( S t + 1 , a ) (S_{t+1},a) (St+1,a) 。更新Q-table用到的值有 < S t , A t , r e w a r d , S t + 1 > <S_t,A_t,reward,S_{t+1}> <St,At,reward,St+1>
  2. Sara: 在状态 S t S_t St下,根据 ϵ − g r e e d y \epsilon-greedy ϵgreedy 策略选择动作 A t A_t At到达 S t + 1 S_{t+1} St+1之后,选择最大的 ( S t + 1 , a ) (S_{t+1},a) (St+1,a)并真正采取该动作。更新Q-table用到的值有 < S t , A t , r e w a r d , S t + 1 , A t + 1 > <S_t,A_t,reward,S_{t+1},A_{t+1}> <St,At,reward,St+1,At+1>
  3. Q−learning选取动作和更新Q表值的方法不同,而Sarsa选取动作和更新Q表值的方法相同。Q-Learning算法,先假设下一步选取最大奖赏的动作,更新值函数。然后再通过ε-greedy策略选择动作。Sarsa算法,先通过ε-greedy策略执行动作,然后根据所执行的动作,更新值函数。
  4. 可以看出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):根据输入的观察值,使用 ϵ − g r e e d y \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 × 17 7\times 17 7×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时模型陷入局部最优。

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值