飞道的博客

Python_强化学习_Q-Learning算法_二维迷宫游戏

802人阅读  评论(0)

1.问题描述

在该项目中,你将使用强化学习算法(本文使用的Q-Learning),实现一个自动走迷宫的机器人。

用AI玩二维迷宫

如B站以上视频所示,机器人初始位置在地图左上角。在我们的迷宫中,有墙壁(黑色方块)、元宝(黄色圆块)及终点(绿色方块)。机器人要尽可能避开陷阱,并且拿到元宝后,以最少的步子到达终点。
机器人可执行的动作包括:向左走 L 、向右走 R 、向上走 U 、向下走 D 。

2.强化学习:算法理解

强化学习作为机器学习算法的一种,其模式也是让智能体在“训练”中学到“经验”,以实现给定的任务。但不同于监督学习与非监督学习,在强化学习的框架中,我们更侧重通过智能体与环境的交互来学习。通常在监督学习和非监督学习任务中,智能体往往需要通过给定的训练集,辅之以既定的训练目标(如最小化损失函数),通过给定的学习算法来实现这一目标。然而在强化学习中,智能体则是通过其与环境交互得到的奖励进行学习。这个环境可以是虚拟的(如虚拟的迷宫),也可以是真实的(自动驾驶汽车在真实道路上收集数据)。

在强化学习中有五个核心组成部分,它们分别是:环境(Environment)、智能体(Agent)、状态(State)、动作(Action)和奖励(Reward)。

3 定义动作

接下来,定义机器人是如何选择行动的。这里需要引入增强学习中epsilon greedy的概念。因为在初始阶段, 随机的探索环境, 往往比固定的行为模式要好, 所以这也是累积经验的阶段, 我们希望探索者不会那么贪婪(greedy)。说说我的理解,上图迷宫中,当机器人第一次找到黄金后,如果不控制他的贪婪程度,那么很可能他每次都会直奔去,加入地图中还有第二个黄金,则很有可能被忽略(即缺少对地图的完全搜索)。
  所以epsilon就是用来控制贪婪程度的值。epsilon可以随着探索时间不断提升(越来越贪婪), 不过在这个例子中, 我们就固定成 epsilon = 0.7, 70% 的时间是选择最优策略, 30% 的时间来探索。

完整Python代码如下:

import random
import time
import tkinter as tk
import pandas as pd

'''
此文件实现Q_learning 强化学习算法解决二维迷宫问题
一个小球在一个迷宫中,四周都是陷阱,设置一个入口,设置一个出口。
小球怎么走到出口,迷宫中间会陷阱。打印出路线。

以下提供了二维迷宫问题的一个比较通用的模板,拿到后需要修改的地方非常少。
对于任意的二维迷宫的 class Agent,
只需修改三个地方:MAZE_R, MAZE_R, rewards,其他的不要动!如下所示:

 6*6 的迷宫:
-------------------------------------------
| 入口 | 陷阱 |      |      |      |      |
-------------------------------------------
|      | 陷阱 |      |      | 陷阱 |      |
-------------------------------------------
|      | 陷阱 |      | 陷阱 |      |      |
-------------------------------------------
|      | 陷阱 |      | 陷阱 |      |      |
-------------------------------------------
|      | 陷阱 |      | 陷阱 | 元宝  |      |
-------------------------------------------
|      |      |     | 陷阱 |      | 出口 |
-------------------------------------------
'''


class Maze(tk.Tk):
    '''环境类(GUI),主要用于画迷宫和小球'''
    UNIT = 40  # 像素
    MAZE_R = 6  # grid row
    MAZE_C = 6  # grid column

    def __init__(self):
        super().__init__()
        self.title('迷宫')
        h = self.MAZE_R * self.UNIT
        w = self.MAZE_C * self.UNIT
        self.geometry('{0}x{1}'.format(h, w))  # 窗口大小
        self.canvas = tk.Canvas(self, bg='white', height=h, width=w)
        # 画网格
        for c in range(1, self.MAZE_C):
            self.canvas.create_line(c * self.UNIT, 0, c * self.UNIT, h)
        for r in range(1, self.MAZE_R):
            self.canvas.create_line(0, r * self.UNIT, w, r * self.UNIT)
        # 画入口
        self._draw_rect(0, 0, 'blue')
        # 画陷阱
        self._draw_rect(1, 0, 'black')  # 在1列、0行处,下同
        self._draw_rect(1, 1, 'black')
        self._draw_rect(1, 2, 'black')
        self._draw_rect(1, 3, 'black')
        self._draw_rect(1, 4, 'black')
        self._draw_rect(3, 2, 'black')
        self._draw_rect(3, 3, 'black')
        self._draw_rect(3, 4, 'black')
        self._draw_rect(3, 5, 'black')
        self._draw_rect(4, 1, 'black')
        # 画奖励
        self._draw_rect(4, 4, 'yellow')
        # 画出口
        self._draw_rect(5, 5, 'green')
        # 画玩家(保存!!)
        self.rect = self._draw_oval(0, 0, 'red')
        self.canvas.pack()  # 显示画作!

    def _draw_rect(self, x, y, color):
        '''画矩形,  x,y表示横,竖第几个格子'''
        padding = 5  # 内边距5px,参见CSS
        coor = [self.UNIT * x + padding, self.UNIT * y + padding, self.UNIT * (x + 1) - padding,
                self.UNIT * (y + 1) - padding]
        return self.canvas.create_rectangle(*coor, fill=color)

    def _draw_oval(self, x, y, color):
        '''画矩形,  x,y表示横,竖第几个格子'''
        padding = 6  # 内边距5px,参见CSS
        coor = [self.UNIT * x + padding, self.UNIT * y + padding, self.UNIT * (x + 1) - padding,
                self.UNIT * (y + 1) - padding]
        return self.canvas.create_oval(*coor, fill=color)

    def move_agent_to(self, state):
        '''移动玩家到新位置,根据传入的状态'''
        coor_old = self.canvas.coords(self.rect)  # 形如[5.0, 5.0, 35.0, 35.0](第一个格子左上、右下坐标)
        x, y = state % 6, state // 6  # 横竖第几个格子
        padding = 5  # 内边距5px,参见CSS
        coor_new = [self.UNIT * x + padding, self.UNIT * y + padding, self.UNIT * (x + 1) - padding,
                    self.UNIT * (y + 1) - padding]
        dx_pixels, dy_pixels = coor_new[0] - coor_old[0], coor_new[1] - coor_old[1]  # 左上角顶点坐标之差
        self.canvas.move(self.rect, dx_pixels, dy_pixels)
        self.update()  # tkinter内置的update!


class Agent(object):
    '''个体类'''
    MAZE_R = 6  # 迷宫行数
    MAZE_C = 6  # 迷宫列数

    def __init__(self, alpha=0.1, gamma=0.9):
        '''初始化'''
        self.states = range(self.MAZE_R * self.MAZE_C)  # 状态集。0~35 共36个状态
        self.actions = list('udlr')  # 动作集。上下左右  4个动作 ↑↓←→
        self.rewards = [0, -10, 0, 0, 0, 0,
                        0, -10, 0, 0, -10, 0,
                        0, -10, 0, -10, 0, 0,
                        0, -10, 0, -10, 0, 0,
                        0, -10, 0, -10, 2, 0,
                        0, 0, 0, -10, 0, 10]  # 奖励集。出口奖励10,陷阱奖励-10,元宝奖励2
        # 贝尔曼方程的两个参数,alpha = 0.1     # 学习率
        # gamma = 0.9     # 奖励递减值
        self.alpha = alpha
        self.gamma = gamma
        # Q表格环境
        self.q_table = pd.DataFrame(data=[[0 for _ in self.actions] for _ in self.states],
                                    index=self.states,
                                    columns=self.actions)

    def choose_action(self, state, epsilon=0.8):
        '''选择相应的动作。根据当前状态,随机或贪婪,按照参数epsilon'''
        # if (random.uniform(0,1) > epsilon) or ((self.q_table.ix[state] == 0).all()):  # 探索
        if random.uniform(0, 1) > epsilon:  # 探索,这是非常有必要的,不探索会困在元宝里
            action = random.choice(self.get_valid_actions(state))
        else:
            # action = self.q_table.loc[state].idxmax() # 利用 当有多个最大值时,会锁死第一个!
            # 重大改进!然鹅与上面一样
            # action = self.q_table.loc[state].filter(items=self.get_valid_actions(state)).idxmax()
            # 从q表格中找到相应方向的反馈值
            s = self.q_table.loc[state].filter(items=self.get_valid_actions(state))
            # 最大的反馈值可能都是1,从里面随机选择一个!
            action = random.choice(s[s == s.max()].index)
        return action

    def get_q_values(self, state):
        '''取给定状态state的所有Q value'''
        q_values = self.q_table.loc[state, self.get_valid_actions(state)]
        return q_values

    def update_q_value(self, state, action, next_state_reward, next_state_q_values):
        '''更新Q value,根据贝尔曼方程'''
        self.q_table.loc[state, action] += self.alpha * (
                next_state_reward + self.gamma * next_state_q_values.max() - self.q_table.loc[state, action])

    def get_valid_actions(self, state):
        '''取当前状态下所有的合法动作'''
        valid_actions = set(self.actions)
        if state // self.MAZE_C == 0:  # 首行,则 不能向上
            valid_actions -= {
   'u'}
        elif state // self.MAZE_C == self.MAZE_R - 1:  # 末行,则 不能向下
            valid_actions -= {
   'd'}

        if state % self.MAZE_C == 0:  # 首列,则 不能向左
            valid_actions -= {
   'l'}
        elif state % self.MAZE_C == self.MAZE_C - 1:  # 末列,则 不能向右
            valid_actions -= {
   'r'}

        return list(valid_actions)

    def get_next_state(self, state, action):
        '''对状态执行动作后,得到下一状态'''
        # u,d,l,r,n = -6,+6,-1,+1,0
        if action == 'u' and state // self.MAZE_C != 0:  # 除首行外,向上-MAZE_C
            next_state = state - self.MAZE_C
        elif action == 'd' and state // self.MAZE_C != self.MAZE_R - 1:  # 除末行外,向下+MAZE_C
            next_state = state + self.MAZE_C
        elif action == 'l' and state % self.MAZE_C != 0:  # 除首列外,向左-1
            next_state = state - 1
        elif action == 'r' and state % self.MAZE_C != self.MAZE_C - 1:  # 除末列外,向右+1
            next_state = state + 1
        else:
            next_state = state
        return next_state

    def learn(self, env=None, episode=222, epsilon=0.8):
        '''q-learning算法'''
        print('Agent is learning...')
        for i in range(episode):
            # 将起始位置设为入口位置
            current_state = self.states[0]

            env.move_agent_to(current_state)
            # 如果没有走到出口就一直执行
            while current_state != self.states[-1]:
                current_action = self.choose_action(current_state, epsilon)  # 按一定概率,随机或贪婪地选择
                next_state = self.get_next_state(current_state, current_action)
                next_state_reward = self.rewards[next_state]
                next_state_q_values = self.get_q_values(next_state)
                self.update_q_value(current_state, current_action, next_state_reward, next_state_q_values)
                current_state = next_state
                env.move_agent_to(current_state)
            print(i)
        print('\n學習完畢!')

    def test_agent(self):
        '''测试agent是否能在36步之内走出迷宫'''
        count = 0
        current_state = self.states[0]
        while current_state != self.states[-1]:
            current_action = self.choose_action(current_state, 1.)  # 1., 100%贪婪
            next_state = self.get_next_state(current_state, current_action)
            current_state = next_state
            count += 1

            if count > self.MAZE_R * self.MAZE_C:  # 没有在36步之内走出迷宫,则
                print('无智能')
                return False  # 无智能
        print('有智能')
        return True  # 有智能

    def play(self, env=None):
        '''玩游戏,使用策略'''
        print('测试agent是否能在36步之内走出迷宫')
        if not self.test_agent():  # 若尚无智能,则
            print("I need to learn before playing this game.")
            self.learn(env, episode=222, epsilon=0.7)
        print('Agent is playing...')
        current_state = self.states[0]
        env.move_agent_to(current_state)
        while current_state != self.states[-1]:
            current_action = self.choose_action(current_state, 1)
            next_state = self.get_next_state(current_state, current_action)
            current_state = next_state
            env.move_agent_to(current_state)
            time.sleep(0.4)
        print('\nCongratulations, Agent got it!')


if __name__ == '__main__':
    env = Maze()  # 环境
    agent = Agent()  # 个体(智能体)
    '''
    epsilon = 0.9   # 贪婪度 greedy,0-1,越接近1随机性越小,会导致智能体死在元寶裏面
    Q_learning比较“大胆”,此处设置为1,Sarsa就显得比较“谨慎”,设置0.5-0.9都可以
    Q_learning采取最大化的策略,即我不管后面的动作怎样,我只选取下一个可能性最大的动作来更新表格。这种训练方法训练智能体,在CliffWalking环境里会比较明显,
    它会紧贴着元宝走到终点,因为这种寻路的方法是路径最短的。但是智能体在训练初期相比于Sarsa很容易掉入元宝。
    对于Sarsa,需要考虑到下一个动作具体是什么,也即考虑到“长期”的动作的影响。在CliffWalking环境中,智能体会远离元宝动到终点。
    因为掉入元宝会得到很坏的回报,由于算法考虑“长期”的回报,所以智能体会尽可能的远离元宝。
    设置episode参数会影响算法训练的时间,越大越慢,学得越好
    '''
    agent.learn(env, episode=333, epsilon=0.7)  # 先学习
    agent.play(env)  # 再玩耍


转载:https://blog.csdn.net/qq_30803353/article/details/114783723
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场