刚刚举行的 WAVE SUMMIT 2019 深度学习开发者峰会上,PaddlePaddle 发布了 PARL 1.1 版本,这一版新增了 IMPALA、A3C、A2C 等一系列并行算法。作者重新测试了一遍内置 example,发现卷积速度也明显加快,从 1.0 版本的训练一帧需大约 1 秒优化到了 0.15 秒(配置:win8,i5-6200U,GeForce-940M,batch-size=32)。
嘿嘿,超级本实现游戏智能的时代终于来临!废话不多说,我们赶紧试试 PARL 的官方 DQN 算法,玩一玩 Flappy-Bird。
关于作者:曹天明(kosora),2011 年毕业于天津科技大学,7 年的 PHP+Java 经验。于2018年9月报名加入光环国际人工智能周末转型班进行学习提升。个人研究方向——融合 CLRS 与 DRL 两大技术体系,并行刷题和模型训练。专注于游戏智能、少儿趣味编程两大领域。
模拟环境
相信大家对于这个游戏并不陌生,我们需要控制一只小鸟向前飞行,只有飞翔、下落两种操作,小鸟每穿过一根柱子,总分就会增加。由于柱子是高低不平的,所以需要想尽办法躲避它们。一旦碰到了柱子,或者碰到了上、下边缘,都会导致 game-over。下图展示了未经训练的小笨鸟,可以看到,他处于人工智障的状态,经常撞柱子或者撞草地:
▲ 未经训练的小笨鸟
先简要分析一下环境 Environment 的主要代码。
BirdEnv.py 继承自 gym.Env,实现了 init、reset、reward、render 等标准接口。init 函数,用于加载图片、声音等外部文件,并初始化得分、小鸟位置、上下边缘、水管位置等环境信息:
def __init__(self):
if not hasattr(self,'IMAGES'):
print('InitGame!')
self.beforeInit()
self.score = self.playerIndex = self.loopIter = 0
self.playerx = int(SCREENWIDTH * 0.3)
self.playery = int((SCREENHEIGHT - self.PLAYER_HEIGHT) / 2.25)
self.baseShift = self.IMAGES['base'].get_width() - self.BACKGROUND_WIDTH
newPipe1 = getRandomPipe(self.PIPE_HEIGHT)
newPipe2 = getRandomPipe(self.PIPE_HEIGHT)
#...other code
step 函数,执行两个动作,0 表示不采取行动(小鸟会自动下落),1 表示飞翔;step 函数有四个返回值,image_data 表示当前状态,也就是游戏画面,reward 表示本次 step 的即时奖励,terminal 表示是否是吸收状态,{} 表示其他信息:
def step(self, input_action=0):
pygame.event.pump()
reward = 0.1
terminal = False
if input_action == 1:
if self.playery > -2 * self.PLAYER_HEIGHT:
self.playerVelY = self.playerFlapAcc
self.playerFlapped = True
#...other code
image_data=self.render()
return image_data, reward, terminal,{}
奖励 reward;初始奖励是 +0.1,表示小鸟向前飞行一小段距离;穿过柱子,奖励 +1;撞到柱子,奖励为 -1,并且到达 terminal 状态:
#飞行一段距离,奖励+0.1
reward = 0.1
#...other code
playerMidPos = self.playerx + self.PLAYER_WIDTH / 2
for pipe in self.upperPipes:
pipeMidPos = pipe['x'] + self.PIPE_WIDTH / 2
#穿过一个柱子奖励加1
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
self.score += 1
reward = self.reward(1)
#...other code
if isCrash:
#撞到边缘或者撞到柱子,结束,并且奖励为-1
terminal = True
reward = self.reward(-1)
reward 函数,返回即时奖励 r:
def reward(self,r):
return r
reset 函数,调用 init,并执行一次飞翔操作,返回 observation,reward,isOver:
def reset(self,mode='train'):
self.__init__()
self.mode=mode
action0 = 1
observation, reward, isOver,_ = self.step(action0)
return observation,reward,isOver
render 函数,渲染游戏界面,并返回当前画面:
def render(self):
image_data = pygame.surfarray.array3d(pygame.display.get_surface())
pygame.display.update()
self.FPSCLOCK.tick(FPS)
return image_data
至此,强化学习所需的状态、动作、奖励等功能均定义完毕。接下来简单推导一下 DQN (Deep-Q-Network) 算法的原理。
DQN的发展过程
DQN 的进化历史可谓源远流长,从最开始 Bellman 在 1956 年提出的动态规划,到后来 Watkins 在 1989 年提出的的 Q-learning,再到 DeepMind 的 Nature-2015 稳定版,最后到 Dueling DQN、Priority Replay Memory、Parameter Noise 等优化算法,横跨整整一个甲子,凝聚了无数专家、教授们的心血。如今的我们站在先贤们的肩膀上,从以下角度逐步分析:
- 贝尔曼(最优)方程与 VQ 树
- Q-learning
- 参数化逼近
- DQN 算法框架
贝尔曼 (最优) 方程与VQ树
我们从经典的表格型强化学习(Tabular Reinforcement Learning)开始,回忆一下马尔可夫决策(MDP)过程,MDP 可由五元组 (S,A,P,R,γ) 表示,其中:
- S 状态集合,维度为 1×|S|
- A 动作集合,维度为 1×|A|
- P 状态转移概率矩阵,经常写成
,其维度为 |S|×|A|×|S|
- R 回报函数,如果依赖于状态值函数 V,维度为 1×|S|,如果依赖于状态-动作值函数 Q,则维度为 |S|×|A|
- γ 折扣因子,用来计算带折扣的累计回报 G(t),维度为 1
S、A、R、γ 均不难理解,可能部分同学对
有疑问——既然 S 和 A 确定了,下一个状态 S' 不是也确定了吗?为什么会有概率转移矩阵呢?
其实我初学的时候也曾经被这个问题困扰过,不妨通过如下两个例子以示区别:
1.
恒等于 1.0 的情况。如图 1 所示,也就是上一次我们在策略梯度算法中所使用的迷宫,假设机器人处于左上角,这时候你命令机器人向右走,那么他转移到红框所示位置的概率就是 1.0,不会有任何异议:
▲ 图1. 迷宫寻宝
2.
不等于 1.0 的情况。假设现在我们下一个飞行棋,如图 2 所示。有两种骰子,第一种是普通的正方体骰子,可以投出 1~6,第二种是正四面体的骰子,可以投出 1~4。现在飞机处于红框所示的位置,现在我们选择投掷第二种骰子这个动作,由于骰子本身具有均匀随机性,所以飞机转移到终点的概率仅仅是 0.25。这就说明,在某些环境中,给定 S、A 的情况下,转移到具体哪一个 S' 其实是不确定的:
▲ 图2. 飞行棋
除了经典的五元组外,为了研究长期回报,还经常加入三个重要的元素,分别是:
- 策略 π(a∣s),维度为 |S|×|A|
- 状态值函数
,维度为 1×|S|,表示当智能体采用策略 π 时,累积回报在状态 s 处的期望值:
▲ 图3. 状态值函数