写在前面
前段时间考虑离职的事,由于我的大哥在上海碰壁。我失去了上海的铁靠山,基本是黄了。(;´д`)ゞ
刚好又很倒霉的碰到一连串烦心事,都没怎么碰代码。(;′⌒`)
这两天有所好转,赶紧捧起我热爱的事业。ヾ(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的静态迷宫
绿色为机器人,红色为陷阱,蓝色为终点。
每一局迷宫游戏开始机器人会随机出生在随机空白格(蔡健雅:????)上,机器人可以向四个方向进行移动。
走出棋盘或掉入陷阱或行走步数超过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的环境对于电脑来说还是太简单啦。
写在最后
欢迎大家在评论区留言交流,项目源码我将会上传到附件和gitee,有需要的童鞋可以自取。|´・ω・)ノ
https://gitee.com/Shxuai/RobotHeart.git
下一篇我将会对电脑提高难度,将环境改成动迷宫,来增加训练难度。
如有不足还请批评指正,欢迎转载。٩(ˊᗜˋ*)و
1 条评论
老哥有下一篇吗,想看动迷宫!