简介:本周的强化学习我们来到实践部分。我以我在 GitHub 上开源的项目
PiperLiu / Amazing-Brick-DFS-and-DRL
为对象,从零开始与各位朋友分享:如何用 python 写一个小游戏 、 如何匹配传统的深度优先搜索算法来控制 、 如何匹配传统的广度优先搜索算法来控制 、 如何匹配深度强化学习算法来控制 、 强化学习的优势在哪里 。无论你是零基础还是有项目经验,我都希望能给你带来收获。
去年在 B 站看到大佬 UP The CW[1] 的视频:用AI在手游中作弊!内藏干货:神经网络、深度/强化学习讲解[2],当时觉得很有趣;但代码部分没有开源,于是我便想着复现一下这位 UP 的作品,仅作为学习之用。
我的复现
深度学习先驱 Yann LeCun 将强化学习比作“蛋糕上的樱桃”,因为强化学习能获得的数据实在是太少了。 一般的强化学习,必须不断与环境交互,获得数据。 (好比网游中,只能通过刷野怪来获得经验值升级)
。
因此,遇到一个新问题时: 监督学习 会思考如何 给大量数据打标签 ,而 强化学习 会开始着手 写仿真/写游戏 。
我们今天来自动控制一个“躲避障碍”的小游戏,全部由 python 实现,我的项目地址为:
•https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL
我希望本文带给你的收获有二: ① 如何写一个简单的小游戏,并且提供控制程序的接口; ② 强化学习控制与深度优先搜索有什么区别,优劣的对比。 本文结构为:
•游戏实现思路•什么是深度优先搜索DFS?用其控制小游戏•什么是广度优先搜索BFS?用其控制小游戏•深度强化学习控制小游戏•强化学习与传统控制的优劣比较
我用手玩这个小游戏,重力很大,按键后左右加速度也很大,很不好控制(我刻意将参数设置得如此反人类,为了增加游戏难度)
这个游戏共涉及三个 .py 脚本:
amazing_brick / amazing_brick_utils.py / wrapped_amazing_brick.pykeyboard_play.py
从玩家角度看,该游戏是动态的;但实际上,由于我没有使用已有物理引擎/游戏引擎,我是 基于每一帧对游戏进行设计、并迭代画面的。
keyboard_play.py
在操作时,游戏类实体: game_state.frame_step(action)
处于一个无限循环中:
•每执行一次 game_state.frame_step(action)
, game_state
会判断位移、是否碰撞、是否得分,并绘制这一帧,并显示;•默认收到的动作 action=1
,即什么也不干;•玩家按下按钮,将改变下一帧 action
的赋值。
如图,在游戏中需要绘制在屏幕上的,一共有三种实体:
•玩家(黑色方块);•方块障碍物;•中间留有空隙的长条障碍物。
基于这三个实体,我们主要需要考虑以下五个事件:
•简易的物理引擎,考虑重力、阻力与加速度;•当玩家上升时,屏幕要随之上升;•检测得分,当玩家穿过间隙时,得分加一;•检测碰撞,当玩家碰到障碍物或撞墙时,游戏结束;•新建随机障碍物。
下面我将展开分别讲解上述事件的实现。
简易物理引擎是最简单的部分,我们为玩家(黑色方块)声明几个变量,作为定位的依据,我这里选择的是左上点 (x, y)
。
此外,玩家还应该具有速度变量。在 2D 空间里,速度是一个矢量(有大小,有方向),为了方便计算,我用横轴坐标方向的速度值表示 (velX, velY)
,即:单位时间内的 X 、 Y 轴位移量来表示速度。
此外,还有加速度系统。为玩家声明四个变量,分布表示重力加速度、横向空气阻力带来的加速度、按下按钮后带来的横向加速度、按下按钮后带来的纵向加速度: gravity, dragForce, AccX, AccY
。
因此,我们就能很轻松地实现符合物理公式的运动系统:
•首先根据加速度计算速度;•接下来根据速度计算玩家应该处于什么位置。
game/amazing_brick_utils.py[3] :
class Play: def __init__(self): self.x = ... self.y = ... self.x_= ... self.y_= ... # 如果你觉得游戏太难的话,可以改变这些物理参数 self.gravity = 0.35 self.dragForce = 0.01 self.velX = 0 self.velY = 0 self.AccX = 4.5 self.AccY = 2.5 def lFlap(self): # 按下左边按钮时,玩家获得一个向左上的力 # 因此速度发生改变 self.velX -= self.AccX self.velY -= (self.AccY - self.gravity) def rFlap(self): # 按下右边按钮时,玩家获得一个向右上的力 # 因此速度发生改变 self.velX += self.AccX self.velY -= (self.AccY - self.gravity) def noneDo(self): # 没有按按钮 # 玩家因为横向空气阻力而减缓横向速度 # 此外,还因为重力向下加速 if self.velX > 0: self.velX -= self.dragForce elif self.velX < 0: self.velX += self.dragForce self.velY += self.gravity
在 game/wrapped_amazing_brick.py[4] 中,我在每帧的迭代代码中,添加了下述代码,用来根据当前速度,确定玩家的新位置:
class GameState: def __init__(self, ifRender=True, fps=30): ... def frame_step(self, action): ... if action == 0: self.player.noneDo() elif action == 1: self.player.lFlap() elif action == 2: self.player.rFlap() ... # player's movement self.player.x += self.player.velX self.player.x_ += self.player.velX self.player.y += self.player.velY self.player.y_ += self.player.velY
有两个思路:
•第一个是,让所有障碍物在每帧下移固定距离,从而造成“玩家在上升”的假象;•另一个是,建立一个“摄像头”,摄像头本身有一个坐标,摄像头随着玩家的上升而上升。无论是障碍物还是玩家,都有两套坐标,一套是真实的、绝对的坐标,另一套是相对于“摄像头”的坐标。我们计算碰撞时,基于前者即真实的坐标;绘图时,基于后者即相对于“摄像头”的坐标。
我采用了第二个思路。这样做的好处是,无需每时每刻对所有障碍物的坐标进行更新,且让镜头的移动更加灵活。
我在 game/wrapped_amazing_brick.py[5] 中将这个“摄像头”实现了:
class ScreenCamera: def __init__(self): self.x = 0 self.y = 0 self.width = CONST['SCREEN_WIDTH'] self.height = CONST['SCREEN_HEIGHT'] self.x_ = self.x + self.width self.y_ = self.y + self.height def __call__(self, obj: Box): # output the obj's (x, y) on screen x_c = obj.x - self.x y_c = obj.y - self.y # 每个实体:玩家、障碍物都有一套相对坐标,即 x_c, y_c # obj.set_camera(x_c, y_c) 将其在屏幕上的新位置告诉它 # 绘图时,就根据其 x_c, y_c 来将其绘制在屏幕上 obj.set_camera(x_c, y_c) return obj def move(self, obj: Player): # 如果玩家此时在屏幕上的坐标将高于屏幕的 1/2 # 镜头上移 # 即不允许玩家跑到屏幕上半部分去 self(obj) if obj.y_c < self.height / 2: self.y -= (self.height / 2 - obj.y_c) else: pass
值得注意的是,pygame中的坐标系是右下为正方向的。
如图,因为相机的移动,我们的玩家一直处于屏幕中央。
在 game/wrapped_amazing_brick.py[6] 中,我在每帧的迭代代码中,添加了下述代码,用来检测得分:
class GameState: def __init__(self, ifRender=True, fps=30): ... def frame_step(self, action): ... # check for score playerMidPos = self.s_c(self.player).y_c + self.player.height / 2 for ind, pipe in enumerate(self.pipes): if ind % 2 == 1: continue self.s_c(pipe) # 判断 Y 轴是否处于间隙中央 if pipe.y_c <= playerMidPos <= pipe.y_c + pipe.height: if not pipe.scored: self.score += 1 # 不能在一个间隙中得两次分 pipe.scored = True # reward 用于强化学习 reward = 1
只要在Y轴方向经过了间隙中央,则得分。
以下情况视为碰撞发生,游戏结束:
•碰到障碍物;•碰到边缘镜头。
其中,“碰到障碍物”用实际坐标计算:
•对于两个物体,取其中心点;•当满足如下图片两个条件时,视为碰撞。
碰到边缘镜头则用相对坐标判断。
因为每次碰撞都要遍历所有障碍物,因此当障碍物淡出屏幕后,就要将障碍物从内存中删除,以确保程序不会越来越卡顿。
我使用两个列表保存所有已有障碍物:
class GameState: def __init__(self, ifRender=True, fps=30): ... self.pipes = [] self.blocks = [] def frame_step(self, action): ... # 判断是否新增障碍物 low_pipe = self.pipes[0] if self.s_c(low_pipe).y_c >= self.s_c.height - low_pipe.width \ and len(self.pipes) < 6: # 满足条件,新增障碍物 self._getRandomPipe() # 如果条形障碍物超出屏幕,则删除 if self.s_c(low_pipe).y_c >= self.s_c.height \ and len(self.pipes) > 4: self.pipes.pop(0) self.pipes.pop(0) # 如果块状障碍物超出屏幕,则删除 for block in self.blocks: self.s_c(block) x_flag = - CONST['BLOCK_WIDTH'] <= block.x_c <= self.s_c.width y_flag = block.y_c >= self.s_c.height
此外,还需新增障碍物。这里我使用随机数生成。
class GameState: ... def _getRandomPipe(self, init=False): if self.score % 5 == 4: self.color_ind = (self.color_ind + 1) % 5 gap_left_topXs = list(range(100, 190, 20)) if init: index = random.randint(0, len(gap_left_topXs)-1) x = gap_left_topXs[index] y = CONST['SCREEN_HEIGHT'] / 2 - CONST['PIPE_WIDTH'] / 2 first_pipes = pipes(x, y, self.color_ind) self.pipes.append(first_pipes[0]) self.pipes.append(first_pipes[1]) self._addBlocks() index = random.randint(0, len(gap_left_topXs)-1) x = self.s_c.x + gap_left_topXs[index] y = self.pipes[-1].y - CONST['SCREEN_HEIGHT'] / 2 pipe = pipes(x, y, self.color_ind) self.pipes.append(pipe[0]) self.pipes.append(pipe[1]) self._addBlocks() def _addBlocks(self): x = (self.pipes[-2].x_ + self.pipes[-1].x) / 2 y = (self.pipes[-2].y + self.pipes[-2].y_) / 2 for i in range(2, 0, -1): y_block = y + i * CONST['BLOCK_SPACE'] x_block = x + np.random.normal() * CONST['PIPE_GAPSIZE'] / 2.5 block = Block(x_block, y_block, self.color_ind) self.blocks.append(block)
如上图,我们将使用“深度优先搜索”的方法,来控制黑色方块自动闯关。
所谓“深度优先搜索”,即:
•搜索: 精准预测下一步操作后 ,黑色方块将到达什么位置;并再次精准预测在这个位置进行操作后,黑色方块将到达什么位置... 直到触发终止条件,即找到最终得分的路径 ;•深度优先:假设黑色方块有两个动作可以选择:A与B,那么 黑色方块做出“选择A后应该到达的位置”的预测后,继续接着这条路径预测 ,而非去预测在初始状态下“选择B后应该到达的位置”。具体原理如下图。
图片生成自:https://visualgo.net/zh/dfsbfs
在我写的小游戏中,我们的小方块时刻面临 三个选项 :
•给自己一个左上的力;•给自己一个右上的力;•什么也不做,这一时刻任由自己受重力牵制而掉落。
因此,我们每层也就有三个结点,如下图:
但是因为 算法本身的时间复杂度过大 ,我们可以不考虑“什么也不做”这一动作。否则,将如下图,需要搜索的结点过多,导致程序运行过慢或内存溢出。
如果不考虑“什么也不做”这一动作,每层的父结点就只有两个子结点, 大大减少需要遍历的空间。
我使用递归来实现 DFS 算法,我大概描述一下这个过程。数据结构不够硬的同学,应该静下心来读读我的源码、或者其他经典的 DFS 教程、或者刷刷 LeetCode 。
我的源码见:https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/blob/master/dfs_play.py
final_s_a_list = []def dfs_forward(root_state, show=False): # 最后需要返回的就是这个(状态、动作)列表 global final_s_a_list final_s_a_list = [] # 在内部定义 dfs ,用于递归 # 在递归过程中,修改 final_s_a_list 的值 # 总是保留目前最优解 def dfs(state, s_a_list): global final_s_a_list # a trick # 每次结点的排列都不一样 # 这样搜索速度更快 # 能更快地找到可行解 if len(s_a_list) % 2 == 1: ACTIONS_tmp = (2, 1) else: ACTIONS_tmp = (1, 2) for action in ACTIONS_tmp: if len(final_s_a_list) > 0: break new_state = move_forward(state, action) new_s_a_list = s_a_list.copy() new_s_a_list.append((new_state, action)) if check_crash(new_state): if show: # 绘图部分 pygame.draw.rect(SCREEN, (255, 0, 0), \ (new_state['x'] - game_state.s_c.x, new_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() del new_state del new_s_a_list else: if show: # 绘图部分 pygame.draw.rect(SCREEN, (100, 100, 100), \ (new_state['x'] - game_state.s_c.x, new_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() if check_for_score(new_state): if show: # 绘图部分 pygame.draw.rect(SCREEN, (0, 0, 255), \ (new_state['x'] - game_state.s_c.x, new_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() final_s_a_list = new_s_a_list break dfs(new_state, new_s_a_list) # 开始递归 dfs(root_state, []) return final_s_a_list
我这里 DFS 算法效果较好:
python dfs_play.py
输入参数 --display
可以查看寻路过程:
python dfs_play.py --display
如上图,我们将使用“广度优先搜索”的方法,来控制黑色方块自动闯关。
所谓“广度优先搜索”,即:
•搜索:精准预测一步操作后,黑色方块将到达什么位置;并再次精准预测在这个位置进行操作后,黑色方块将到达什么位置...直到触发终止条件,即找到最终得分的路径;•广度优先:假设黑色方块有两个动作可以选择:A与B,那么黑色方块做出“选择A后应该到达的位置”的预测后,不继续接着这条路径预测;而是去预测在初始状态下“选择B后应该到达的位置”。具体原理如下图。
图片生成自:https://visualgo.net/zh/dfsbfs
为了更好地了解 BFS 的特性,你可以用 DFS(深度优先搜索) 进行对比:
同样,在小游戏中,我们的小方块时刻面临 三个选项 :
•给自己一个左上的力;•给自己一个右上的力;•什么也不做,这一时刻任由自己受重力牵制而掉落。
因此,我们每层也就有三个结点,如下图:
但是同样,因为算法本身的时间复杂度过大, 我们可以不考虑“什么也不做”这一动作。这样,每层的父结点就只有两个子结点,大大减少需要遍历的空间。 否则,需要搜索的结点过多,导致程序运行过慢或内存溢出。
我使用队列来实现 BFS 算法,我大概描述一下这个过程。数据结构不够硬的同学,应该静下心来读读我的源码、或者其他经典的 BFS 教程、或者刷刷 LeetCode 。
我的源码见:https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/bfs_play.py
Node = namedtuple("Node", ['sta' , 'act', 'father'])game_state = GameState(True)# 为了避免搜索空间过大# 这里调高了游戏的力学参数game_state.player.velMaxY = 20game_state.player.AccY = 5ACTIONS = (0, 1, 2)def bfs_forward(root_state, show=False): # 保存结点的队列 q = Queue() for action in ACTIONS: node = Node(root_state.copy(), action, None) q.put(node) while True: # 如果队列为空 # 则说明所有可行结点已经遍历 if q.empty(): break father_node = q.get() father_state = father_node.sta if check_for_score(father_state): # 如果得分 # 说明可行路径已经找到 # 跳出循环 if show: pygame.draw.rect(SCREEN, (0, 0, 255), \ (father_state['x'] - game_state.s_c.x, father_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() break # 只考虑动作 1 和 2 for action in ACTIONS[1:]: # father_state = move_forward(father_state, ACTIONS[0]) new_state = move_forward(father_state, action) if check_crash(new_state): if show: pygame.draw.rect(SCREEN, (255, 0, 0), \ (new_state['x'] - game_state.s_c.x, new_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() else: if show: pygame.draw.rect(SCREEN, (100, 100, 100), \ (new_state['x'] - game_state.s_c.x, new_state['y'] - game_state.s_c.y, game_state.player.width, game_state.player.height)) pygame.display.update() node = Node(new_state, action, father_node) q.put(node) return father_node
我这里 BFS 算法效果不好:
python bfs_play.py
输入参数 --display
可以查看寻路过程:
python bfs_play.py --display
强化学习是一种“学习”,这意味着可以 从两个角度 理解, 学习 和 应用 :
•在“学习”时,我们需要将数据“喂”给神经网络(或者其他映射结构),其本身根据新老数据,基于一个较为复杂的数学关系,进行参数迭代。 你可以将其理解为,输入数据,参数改变,导致映射结构更加强大准确。•而“应用”则非常好理解,对于神经网络(或者其他映射结构),我们输入某个/些值,则获得其输出的值。 比如强化学习中,我们输入环境信息,得到当前做什么动作最优。
对于我们的小游戏,则是根据当前环境信息(小黑点位置,障碍物位置等),实时输出此使做什么动作:向左发力
、向右发力
、不发力
三选一。
在项目中,涉及的 .py
文件有:
DQN_train/gym_warpper.pyDQN_train/dqn_train3.pyDQN_train/dqn_render3.py
tianshoupytorch > 1.40gym
在本项目地址中,你可以使用如下文件对我训练的模型进行测试,或者继续训练。
继续训练该模型:
python DQN_train/dqn_train3.py
我已经训练了 40 次(每次5个epoch),输入上述命令,你将开始第 41 次训练,如果不使用任务管理器强制停止,计算机将一直训练下去,并自动保存每一代的权重。
查看效果:
python DQN_train/dqn_render3.py 3
注意参数 3 ,输入 3 代表使用训练 3 次后的权重。
效果如图:
训练不够充分,强化学习智能体没有找到最优解(避开障碍,过关得分;只找到了次优解,在下面苟着命,不得分,但也不会因为碰到障碍物受伤)
我保留了该模型的所有历史权重。你还可以输入参数:1-40,查看历代神经网络的表现。如果你继续训练了模型,你可以输入更大的参数,如 41 。
输入 10 则代表使用训练 10 次后的权重:
python DQN_train/dqn_render3.py 25
效果如图:
输入 30 则代表使用训练 30 次后的权重:
python DQN_train/dqn_render3.py 30
效果如图:
事件 | 奖励 |
---|---|
动作后碰撞障碍物、墙壁 | -1 |
动作后无事发生 | 0.0001 |
动作后得分 | 1 |
在第一层滞留过久(超过500步) | -10 |
可以看出,我将动作后无事发生
的奖励从 0.1 降低到了 -1 ,是为了:
•突出动作后得分
这项的奖励;•如此,智能体第一次得分后,会很“欣喜地发现”上升一层的快乐远远大于在第一层苟命的快乐。
此外,如果智能体在第一层滞留过久
,也是会受到 -10 的惩罚的:
•这是为了告诉智能体,在第一层过久是不被鼓励的;•因为状态是链式的,因此最后的惩罚会回溯分配到之前的“苟命”策略上。
封装代码在 gym_wrapper.py[7] 中,使用类 AmazingBrickEnv3
。
我之前常识过将 2 帧的数据输入到线性层中,效果并不理想。我进一步帮助机器提取了信息
,并且预处理了数据
:
•不再将巨大的 2 帧数据输入到网络中;•取而代之的是,当前状态的速度向量(velx, vely)
(共2个数);•再加上玩家xy坐标
、左障碍物右上顶点xy坐标
、右障碍物左上顶点xy坐标
、4个障碍方块的左上顶点的xy坐标
(共14个数);•如此,输入层只有 16 个神经元即可,且每 1 帧做一次决策。
我还放慢了 epsilon (探索概率)的收敛速度,让智能体更多地去探索动作,不局限在局部最优解中。
此外,我对输入数据进行了归一化处理比如,玩家的坐标 x, y 分别除以了屏幕的 宽、高。从结果和训练所需的代数更少来看,我认为这对于机器学习有极大的帮助。
class Net(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(16, 128) self.fc2 = nn.Linear(128, 256) self.fc3 = nn.Linear(256, 128) self.fc4 = nn.Linear(128, 3) def forward(self, obs, state=None, info={}): if not isinstance(obs, torch.Tensor): obs = torch.tensor(obs, dtype=torch.float) x = F.relu(self.fc1(obs)) x = F.relu(self.fc2(x)) x = F.relu(self.fc3(x)) x = self.fc4(x) return x, state
如上,共四层线性网络。
为了保存训练好的权重,且在需要时可以暂停并继续训练,我新建了一个.json
文件用于保存训练数据。
dqn2_path = osp.join(path, 'DQN_train/dqn_weights/')if __name__ == '__main__': try: with open(dqn3_path + 'dqn3_log.json', 'r') as f: jlist = json.load(f) log_dict = jlist[-1] round = log_dict['round'] policy.load_state_dict(torch.load(dqn3_path + 'dqn3round_' + str(int(round)) + '.pth')) del jlist except FileNotFoundError as identifier: print('\n\nWe shall train a bright new net.\n') # 第一次训练时,新建一个 .json 文件 # 声明一个列表 # 以后每次写入 json 文件,向列表新增一个字典对象 with open(dqn3_path + 'dqn3_log.json', 'a+') as f: f.write('[]') round = 0 while True: round += 1 print('\n\nround:{}\n\n'.format(round)) result = ts.trainer.offpolicy_trainer( policy, train_collector, test_collector, max_epoch=max_epoch, step_per_epoch=step_per_epoch, collect_per_step=collect_per_step, episode_per_test=30, batch_size=64, # 如下,每新一轮训练才更新 epsilon train_fn=lambda e: policy.set_eps(0.1 / round), test_fn=lambda e: policy.set_eps(0.05 / round), writer=None) print(f'Finished training! Use {result["duration"]}') torch.save(policy.state_dict(), dqn3_path + 'dqn3round_' + str(int(round)) + '.pth') policy.load_state_dict(torch.load(dqn3_path + 'dqn3round_' + str(int(round)) + '.pth')) log_dict = {} log_dict['round'] = round log_dict['last_train_time'] = datetime.datetime.now().strftime('%y-%m-%d %I:%M:%S %p %a') log_dict['best_reward'] = result['best_reward'] with open(dqn3_path + 'dqn3_log.json', 'r') as f: """dqn3_log.json should be inited as []""" jlist = json.load(f) jlist.append(log_dict) with open(dqn3_path + 'dqn3_log.json', 'w') as f: json.dump(jlist, f) del jlist
import os.path as ospimport sysdirname = osp.dirname(__file__)path = osp.join(dirname, '..')sys.path.append(path)from amazing_brick.game.wrapped_amazing_brick import GameStatefrom amazing_brick.game.amazing_brick_utils import CONSTfrom DQN_train.gym_wrapper import AmazingBrickEnv3import tianshou as tsimport torch, numpy as npfrom torch import nnimport torch.nn.functional as Fimport jsonimport datetimetrain_env = AmazingBrickEnv3()test_env = AmazingBrickEnv3()state_shape = 16action_shape = 1net = Net()optim = torch.optim.Adam(net.parameters(), lr=1e-3)'''args for rl'''estimation_step = 3max_epoch = 5step_per_epoch = 300collect_per_step = 50policy = ts.policy.DQNPolicy(net, optim, discount_factor=0.9, estimation_step=estimation_step, use_target_network=True, target_update_freq=320)train_collector = ts.data.Collector(policy, train_env, ts.data.ReplayBuffer(size=2000))test_collector = ts.data.Collector(policy, test_env)
采用这种方式获得了不错的效果,在第 40 代训练后(共 40 * 5 * 300 = 6000 个 step),智能体已经能走 10 层左右。
相信继续的迭代会获得更好的成绩。
首先明确一个概念,在这个案例中, 深度优先搜索 DFS 与广度优先搜索 BFS 作弊了。
开始的时候,时间被暂停了?
以如上是 DFS 的一个实例,你有没有发现, DFS 只有 把接下来的所有步骤都算好了 ,才能 开始行动 。这就有两点要求:
•DFS / BFS 这种搜索算法 所处的环境必须是稳定的 ,即其计算出的下一步、下下一步到哪里等,都必须是准确的,如果环境中有风吹草动这种不可预测因素,则无法进行“搜索”•DFS / BFS 这种搜索算法 必须暂停时间 ,如上图中,其不可以一遍掉落一遍计算路径(因为掉落对其来说是环境的改变,环境改变了,就要重新搜索、计算)
但也正是因为这种“局限性”,一个问题一旦能用 DFS / BFS 求解,求出来的解一定是最优解。比如这里,尽管 DFS 空间、时间消耗很大,但是其可以 “一丁点失误都没有” 。
强化学习则不需要 “作弊” ,在这里,我们完全可以把强化学习理解为一个人:
•人类无法精准地计算出接下来小黑点会到达什么位置,强化学习也是•但是人类有“感觉”这种东西,练习多了,人类就能大概感觉出来: 这种情况下,按左键;在那种情况下,按右键。 强化学习也是如此,只根据当前的情况,做出当前的决策, 但是其考虑到了长远的总收益
训练10个epoch,强化学习稍微会玩了一点
我在前文提到过“传统控制算法”,实际上,DFS、BFS只是一种数据结构搜索算法,并算不上 “传统控制算法” 。
“传统控制算法” 当然也不需要 “作弊” ,其贯穿的数学原理 + 算法控制 + 硬件实现可谓 巧妙惊人 。父母都是理工名校工科专业出身,我本人从小也向往“制造机器人”,虽然没有学习自动控制相关专业(而是学了管理),但是会将其作为兴趣钻研下去,尽管目前尚无精力。
目前看来,传统控制算法依然在大部分场景下优于强化学习。未来有机会,我会与各位分享经典的 PID 控制
与其他学习心得。欢迎关注我,此外,请点击“在看”支持我。
[1]
The CW: https://space.bilibili.com/13081489
[2]
用AI在手游中作弊!内藏干货:神经网络、深度/强化学习讲解: https://www.bilibili.com/video/BV1Ft411E79Y
[3]
game/amazing_brick_utils.py: https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/amazing_brick/game/amazing_brick_utils.py
[4]
game/wrapped_amazing_brick.py: https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/amazing_brick/game/wrapped_amazing_brick.py
[5]
game/wrapped_amazing_brick.py: https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/amazing_brick/game/wrapped_amazing_brick.py
[6]
game/wrapped_amazing_brick.py: https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/amazing_brick/game/wrapped_amazing_brick.py
[7]
gym_wrapper.py: https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL/blob/master/DQN_train/gym_wrapper.py