写在前面

前段时间考虑离职的事,由于我的大哥在上海碰壁。我失去了上海的铁靠山,基本是黄了。(;´д`)ゞ
刚好又很倒霉的碰到一连串烦心事,都没怎么碰代码。(;′⌒`)
这两天有所好转,赶紧捧起我热爱的事业。ヾ(o・ω・)ノ

环境

IDE:PyCharm 2018.3.5
python:3.7.2
gym:0.12.1

我们要做什么?

这里借鉴了两篇文章来研究,文章中代码有很多错误的地方。
在下已经全部在本文中做了修改,这里不再赘述。

https://blog.csdn.net/extremebingo/article/details/80867486

https://blog.csdn.net/gg_18826075157/article/details/78163386

大家可以先看看这两篇文章,本文的思路以及代码大量借鉴其中的内容。


有这么一个4x4的静态迷宫

棋盘.png

绿色为机器人,红色为陷阱,蓝色为终点。
每一局迷宫游戏开始机器人会随机出生在随机空白格(蔡健雅:????)上,机器人可以向四个方向进行移动。
走出棋盘掉入陷阱行走步数超过10步判定为失败,仅有走到终点即为成功。

何为强化学习?

强化学习是无监督学习的一种派生。让AI在沙盒游戏中自己玩,不过多干预过程。只在打通关游戏以后,告诉AI你打的是真结局还是假结局。
举个栗子,老师给学生布置了作业(State)。但每次他不会教学生怎么做(Action),只在改作业时给学生打分(Reward)。学生一开始并不知怎么解题,得到的漫天零分。在不断做作业的过程中,偶然有一次做对了,老师给了满分。从此以后每次作业(New State)学生就想起来上次做对的作业解法,用来套公式到每次作业中去。

S->A->R->S'即为强化学习的核心思想。让电脑在无数次的尝试中不断碰壁,直到掌握了正确的解法。

Q_table数组

q_table是强化学习的结果,用以记录强化学习过程生产的经验。不同的环境将有不同维度以及长度的数组结构。
这是本文中用到的q_table数组

[[-1.93748179e+01 -7.99410584e-01 -2.35827399e+01  1.80962424e+01]
 [-2.05447572e+01  2.44316176e+01 -4.14201004e+00 -4.03409513e+00]
 [-2.03147620e+01  4.26440554e+01 -9.63157995e-01 -4.68341076e+00]
 [-1.92971284e+01  2.11102313e+01 -4.38015537e+00 -2.38355875e+01]
 [-7.67945265e-01 -4.23656569e+00 -2.02877598e+01  2.99706996e+01]
 [-7.27730066e-01  2.33176608e+01 -4.39736078e+00 -1.14873394e+00]
 [-4.04511529e+00  3.89279064e+01 -9.09163020e-01 -3.89627379e+00]
 [-9.60090348e-01 -2.01753287e+01  4.32167191e+01 -1.96520882e+01]
 [-4.20625646e+00 -2.04973205e+01 -2.01521669e+01  1.83522858e+01]
 [-7.53492745e-01 -3.45275838e+00 -8.17027268e-01  3.70995093e+01]
 [ 3.61133987e-01  7.31144855e+01 -8.41688830e-01 -9.39701381e-01]
 [-6.97570770e-02 -3.64522724e-01 -4.12423797e-01 -4.98892269e-01]
 [-6.93187560e-01 -9.16230169e-01 -1.57087234e-02 -1.32728935e-01]
 [-5.32453106e-01 -1.95276016e+01  9.46696315e-02  4.12691722e+01]
 [ 4.42798225e-01  2.96291770e-01  3.27970130e-01  1.00600689e+02]
 [ 6.93055206e-01  4.62338894e-01 -9.15100375e-01  3.94975678e-01]]

其结构为16*4的二维数组。16为棋盘的初始状态(state),即每次初始化时机器人可能出现的16个棋盘格。4为每种状态下可能做出的动作:上下左右(action)。而取值为每种状态下,某操作对于当前状态的优劣评定值。每次操作AI将会在4中操作中选择最大值的action来应对当前的state。在每次ation后,根据reward的好坏,对action评判值进行更新。好的reward将会增加刚才的action评判值的大小,反之减小。

构建用户自定环境

打开 \RobotHeart\venv\Lib\site-packages\gym\envs\__init__.py
在末尾加入如下代码,对自定环境进行注册

# user
# ---------
register(
    id='GridWorld-v1',
    entry_point='gym.envs.user:GridEnv1',
    max_episode_steps=200,
    reward_threshold=100.0,
    )

C:\Users\76980\PycharmProjects\RobotHeart\venv\Lib\site-packages\gym\envs下新建user文件夹
在刚才新建的user文件下加入\_\_init\_\_.py代码如下:

from gym.envs.user.grid_mdp_v1 import GridEnv1

再次在user新建文件grid_mdp_v1.py,这个文件就是我们新建的env环境了。

import logging
import random
import gym

logger = logging.getLogger(__name__)

class GridEnv1(gym.Env):
    metadata = {
        'render.modes': ['human', 'rgb_array'],
        'video.frames_per_second': 2
    }

    def __init__(self):

        self.states = list(range(16)) #状态空间

        self.states.remove(11)
        self.states.remove(12)
        self.states.remove(15)

        self.x=[150,250,350,450] * 4
        self.y=[450] * 4 + [350] * 4 + [250] * 4 + [150] * 4

        self.terminate_states = dict()  #终止状态为字典格式
        self.terminate_states[11] = 1
        self.terminate_states[12] = 1
        self.terminate_states[15] = 1

        self.actions = [0, 1, 2, 3]

        self.rewards = dict();        #回报的数据结构为字典
        self.rewards['7_1'] = -100.0
        self.rewards['8_1'] = -100.0
        self.rewards['10_3'] = -100.0
        self.rewards['13_2'] = -100.0
        self.rewards['14_3'] = 100.0

        self.t = dict();             #状态转移的数据格式为字典

        self.size = 4


        for i in range(self.size, self.size * self.size): #上、下、左、右各方向寻路
            self.t[ str(i) + '_0'] = i - 4

        for i in range(self.size * (self.size - 1)):
            self.t[ str(i) + '_1'] = i + 4

        for i in range(1, self.size *self.size):
            if i % self.size == 0:
                continue
            self.t[str(i) + '_2'] = i - 1

        for i in range(self.size *self.size):
            if (i+1) % self.size == 0:
                continue
            self.t[str(i) + '_3'] = i + 1




        self.gamma = 0.8         #折扣因子
        self.viewer = None
        self.state = None

    def _seed(self, seed=None):
        self.np_random, seed = random.seeding.np_random(seed)
        return [seed]

    def getTerminal(self):
        return self.terminate_states

    def getGamma(self):
        return self.gamma

    def getStates(self):
        return self.states

    def getAction(self):
        return self.actions

    def getTerminate_states(self):
        return self.terminate_states

    def setAction(self,s):
        self.state=s

    def step(self, action):
        #系统当前状态
        state = self.state


        # if state in self.terminate_states:
        #     return state, 0, True, {}



        key = "%d_%d"%(state, action)   #将状态和动作组成字典的键值

        #状态转移
        if key in self.t:
            next_state = self.t[key]
        else:
            next_state = state
            r = -100.0
            is_terminal = True
            return next_state, r, is_terminal, {}
  
  
        self.state = next_state

        is_terminal = False

        if next_state in self.terminate_states:
            is_terminal = True

        if key not in self.rewards:
            r = 0.0
        else:
            r = self.rewards[key]

        return next_state, r, is_terminal,{}

    def reset(self):
        self.state = self.states[int(random.random() * len(self.states))]
        return self.state


    def render(self, mode='human'):
        from gym.envs.classic_control import rendering
        screen_width = 600
        screen_height = 600

        if self.viewer is None:

            self.viewer = rendering.Viewer(screen_width, screen_height)

            #创建网格世界
            self.line1 = rendering.Line((100,100),(500,100))
            self.line2 = rendering.Line((100, 200), (500, 200))
            self.line3 = rendering.Line((100, 300), (500, 300))
            self.line4 = rendering.Line((100, 400), (500, 400))
            self.line5 = rendering.Line((100, 500), (500, 500))
            self.line6 = rendering.Line((100, 100), (100, 500))
            self.line7 = rendering.Line((200, 100), (200, 500))
            self.line8 = rendering.Line((300, 100), (300, 500))
            self.line9 = rendering.Line((400, 100), (400, 500))
            self.line10 = rendering.Line((500, 100), (500, 500))

            # #创建石柱
            # self.shizhu = rendering.make_circle(40)
            # self.circletrans = rendering.Transform(translation=(250,350))
            # self.shizhu.add_attr(self.circletrans)
            # self.shizhu.set_color(0.8,0.6,0.4)

            #创建第一个火坑
            self.fire1 = rendering.make_circle(40)
            self.circletrans = rendering.Transform(translation=(450, 250))
            self.fire1.add_attr(self.circletrans)
            self.fire1.set_color(1, 0, 0)

            #创建第二个火坑
            self.fire2 = rendering.make_circle(40)
            self.circletrans = rendering.Transform(translation=(150, 150))
            self.fire2.add_attr(self.circletrans)
            self.fire2.set_color(1, 0, 0)

            #创建宝石
            self.diamond = rendering.make_circle(40)
            self.circletrans = rendering.Transform(translation=(450, 150))
            self.diamond.add_attr(self.circletrans)
            self.diamond.set_color(0, 0, 1)

            #创建机器人
            self.robot= rendering.make_circle(30)
            self.robotrans = rendering.Transform()
            self.robot.add_attr(self.robotrans)
            self.robot.set_color(0, 1, 0)

            self.line1.set_color(0, 0, 0)
            self.line2.set_color(0, 0, 0)
            self.line3.set_color(0, 0, 0)
            self.line4.set_color(0, 0, 0)
            self.line5.set_color(0, 0, 0)
            self.line6.set_color(0, 0, 0)
            self.line7.set_color(0, 0, 0)
            self.line8.set_color(0, 0, 0)
            self.line9.set_color(0, 0, 0)
            self.line10.set_color(0, 0, 0)

            self.viewer.add_geom(self.line1)
            self.viewer.add_geom(self.line2)
            self.viewer.add_geom(self.line3)
            self.viewer.add_geom(self.line4)
            self.viewer.add_geom(self.line5)
            self.viewer.add_geom(self.line6)
            self.viewer.add_geom(self.line7)
            self.viewer.add_geom(self.line8)
            self.viewer.add_geom(self.line9)
            self.viewer.add_geom(self.line10)
            # self.viewer.add_geom(self.shizhu)
            self.viewer.add_geom(self.fire1)
            self.viewer.add_geom(self.fire2)
            self.viewer.add_geom(self.diamond)
            self.viewer.add_geom(self.robot)

        if self.state is None:
            return None

        self.robotrans.set_translation(self.x[self.state], self.y[self.state])

        return self.viewer.render(return_rgb_array=mode == 'rgb_array')

    def close(self):
        if self.viewer:
            self.viewer.close()
            self.viewer = None

正片开始

新建 \RobotHeart\main.py

import gym
import numpy as np
import time
import sys

env = gym.make('GridWorld-v1')

env.reset()   # 初始化本场游戏的环境
env.render()    # 更新并渲染游戏画面
time.sleep(2)
env.close()
sys.exit()

我们已经能看到棋盘渲染出来两秒。接下来我们让机器人出生后,朝随机方向移动。

num_episodes = 5000# 共进行5000场游戏
max_number_of_steps = 10# 每场游戏最大步数


# 以栈的方式记录成绩
goal_average_steps = 100 # 平均分
num_consecutive_iterations = 100 # 栈的容量
last_time_steps = np.zeros(num_consecutive_iterations)  # 只存储最近100场的得分(可以理解为是一个容量为100的栈)

env = gym.make('GridWorld-v1')

for episode in range(num_episodes):
    env.reset()  # 初始化本场游戏的环境
    episode_reward = 0  # 初始化本场游戏的得分

    for t in range(max_number_of_steps):
        # env.state = 10
        env.render()    # 更新并渲染游戏画面
        state = env.state
        action = np.random.choice([0, 1, 2, 3])  # 随机决定小车运动的方向
        observation, reward, done, info = env.step(action)  # 进行活动,并获取本次行动的反馈结果

        if done:
            print('已完成 %d 次训练,本次训练共进行 %d 步数。episode_reward:%d,平均分: %f' % (episode, t + 1, reward, last_time_steps.mean()))
            last_time_steps = np.hstack((last_time_steps[1:], [reward]))    # 更新最近100场游戏的得分stack
            break

env.close()
sys.exit()

我们可以看到机器人已经开始到处蹦跶了。由于完全是随机的,所以平均成绩在-90左右。接下来我们加入q_table。

num_episodes = 5000# 共进行5000场游戏
max_number_of_steps = 10# 每场游戏最大步数


# 以栈的方式记录成绩
goal_average_steps = 100 # 平均分
num_consecutive_iterations = 100 # 栈的容量
last_time_steps = np.zeros(num_consecutive_iterations)  # 只存储最近100场的得分(可以理解为是一个容量为100的栈)

env = gym.make('GridWorld-v1')




# q_table是一个256*2的二维数组
# 离散化后的状态共有4^4=256中可能的取值,每种状态会对应一个行动
# q_table[s][a]就是当状态为s时作出行动a的有利程度评价值
# 我们的AI模型要训练学习的就是这个映射关系表
# 这里的4*4=16是棋盘上棋子的位置数量,第二个参数的4为每个位置对应的4个方向的可能操作。
# q_table的纵坐标是state可能出现的情况之和,很坐标为对应每种state可以做出的action
# 而取值是每种action对于每种state有利程度的评价值
# q_table = np.loadtxt("q_table.txt", delimiter=",")

q_table = np.random.uniform(low=-1, high=1, size=(4 * 4, 4))


# 根据本次的行动及其反馈(下一个时间步的状态),返回下一次的最佳行动
# epsilon_coefficient为贪心策略中的ε,取值范围[0,1],取值越大,行为越随机
# 当epsilon_coefficient取值为0时,将完全按照q_table行动。故可作为训练模型与运用模型的开关值。
def get_action(state, action, observation, reward, episode, epsilon_coefficient=0.0):
    # print(observation)
    next_state = observation
    epsilon = epsilon_coefficient * (0.99 ** episode)  # ε-贪心策略中的ε
    if epsilon <= np.random.uniform(0, 1):
        next_action = np.argmax(q_table[next_state])
    else:
        next_action = np.random.choice([0, 1, 2, 3])
    # -------------------------------------训练学习,更新q_table----------------------------------
    alpha = 0.2  # 学习系数α
    gamma = 0.99  # 报酬衰减系数γ
    q_table[state, action] = (1 - alpha) * q_table[state, action] + alpha * (
            reward + gamma * q_table[next_state, next_action])
    # -------------------------------------------------------------------------------------------
    return next_action, next_state


timer = time.time()
for episode in range(num_episodes):
    env.reset()  # 初始化本场游戏的环境
    episode_reward = 0  # 初始化本场游戏的得分
    q_table_cache = q_table # 创建q_table还原点,如若训练次数超次,则不作本次训练记录。

    for t in range(max_number_of_steps):
        # env.state = 10
        env.render()    # 更新并渲染游戏画面
        state = env.state
        action = np.argmax(q_table[state])
        # action = np.random.choice([0, 1, 2, 3])  # 随机决定小车运动的方向
        observation, reward, done, info = env.step(action)  # 进行活动,并获取本次行动的反馈结果
        action, state = get_action(state, action, observation, reward, episode, 0.5)  # 作出下一次行动的决策
        episode_reward += reward
        if done:
            np.savetxt("q_table.txt", q_table, delimiter=",")
            print('已完成 %d 次训练,本次训练共进行 %d 步数。episode_reward:%d,平均分: %f' % (episode, t + 1, reward, last_time_steps.mean()))
            last_time_steps = np.hstack((last_time_steps[1:], [reward]))    # 更新最近100场游戏的得分stack
            break
    q_table = q_table_cache # 超次还原q_table

    episode_reward = -100
    print('已完成 %d 次训练,本次训练共进行 %d 步数。episode_reward:%d,平均分: %f' % (episode, t + 1, reward, last_time_steps.mean()))
    last_time_steps = np.hstack((last_time_steps[1:], [reward]))  # 更新最近100场游戏的得分stack

    if (last_time_steps.mean() >= goal_average_steps):
        np.savetxt("q_table.txt", q_table, delimiter=",")
        print('用时 %d s,训练 %d 次后,模型到达测试标准!' % (time.time() - timer, episode))

        env.close()

        sys.exit()


env.close()
sys.exit()

可以看到,在开始的10次训练后,ai就迅速吸取了经验,大幅度提高了平均分。由于平均分取样在100场的原因,导致进行了120+场次才完成训练。而实际观察可发现ai在50场次时就已经初窥门径,几乎不会犯错了。看来16*4的环境对于电脑来说还是太简单啦。
aaa.gif

写在最后

欢迎大家在评论区留言交流,项目源码我将会上传到附件和gitee,有需要的童鞋可以自取。|´・ω・)ノ

https://gitee.com/Shxuai/RobotHeart.git

RobotHeart.rar

下一篇我将会对电脑提高难度,将环境改成动迷宫,来增加训练难度。

如有不足还请批评指正,欢迎转载。٩(ˊᗜˋ*)و

最后修改:2021 年 11 月 29 日
如果觉得我的文章对你有用,请随意赞赏