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