200行Keras代码实现DQN玩转FlappyBird

基于Keras的200 行 Python 代码实现 DQN 玩 FlappyBird

运行图

概要

本项目将介绍如何基于Keras使用Deep-Q Learning算法来玩转Flappy Bird
这篇文章主要针对是对强化学习感兴趣的初学者。

安装依赖

  • Python 2.7
  • Keras 1.0
  • pygame
  • scikit-image

如何运行?

仅CPU ( TensorFlow )

1
2
3
git clone https://github.com/yanpanlau/Keras-FlappyBird.git
cd Keras-FlappyBird
python qlearn.py -m "Run"

GPU版本 ( Theano )

1
2
3
git clone https://github.com/yanpanlau/Keras-FlappyBird.git
cd Keras-FlappyBird
THEANO_FLAGS=device=gpu,floatX=float32,lib.cnmem=0.2 python qlearn.py -m "Run"

lib.cnmem=0.2 表示你仅分配20%的显存用来运行程序

PS: 如果你想自己训练神经网络,请删除根目录下”model.h5”后运行

1
qlearn.py -m "Train"

什么是Deep Q-Network?

Deep Q-NetWork 是Google DeepMind 开发用来玩Atari游戏的学习算法。他们论证了一个计算机如何仅仅通过观察屏幕像素和得分的变化,在“玩”2600次Atari游戏中进行学习。研究成果是令人惊叹的,因为它证实了算法足够聪明应对各种各样的游戏。

如果你对深度强化学习感兴趣的话,强烈推荐你阅读下面的文章:

Demystifying Deep Reinforcement Learning

代码详解

现在来逐行分析qlearn.py。如果你对Keras和DQN很熟悉的话,你可以跳过这一部分。

代码要做的事情可以总结如下:

  1. 代码获取游戏屏幕像素数组输入
  2. 代码对图像进行预处理
  3. 处理图像会被喂给卷积神经网络,然后神经网络将决定最佳动作( Flap或者不Flap )
  4. 神经网络使用Q-learning算法训练数百万次

游戏屏幕输入

首先,FlappyBird已经包含在Python开发包pygame中了。

以下是使用FlappyBird API的代码片段:

1
2
import wrapped_flappy_bird as game
x_t1_colored, r_t, terminal = game_state.frame_step(a_t)

FlappyBird的API非常简单,函数frame_step的输入a_t只有0和1:

  • 0 : 表示不flap
  • 1 : 表示flap

然后API会返回输出x_t1_colored,r_tterminal
其中,r_t有三种表示:

  • 0.1 : 表示bird是活的
  • +1 : 表示bird通过了管道
  • -1 : 表示bird死掉了

其中,terminal是一个二值标志表示游戏是否结束。

DeepMind 建议将得分限定在[-1,+1]区间来改善稳定性。然而我还没有机会去测试不同的得分函数对最终性能表现的影响。感兴趣的读者可以尝试自行修改得分函数:game/wrapped_flapy_bird.py下的def frame_step(self, input_actions)函数

图像处理

Flappybird

为了让代码训练速度更快,做一些图像预处理的工作非常重要。以下是关键步骤:

  1. 先将彩色图像转换为灰度图
  2. 调整图像的大小为 80 X 80 像素
  3. 一次性提供4帧图像给神经网络进行训练

为什么要一次性提供4帧图像呢?这是让模型能够理解Bird的速度信息的方法。

1
2
3
4
5
6
x_t1 = skimage.color.rgb2gray(x_t1_colored)
x_t1 = skimage.transform.resize(x_t1,(80,80))
x_t1 = skimage.exposure.rescale_intensity(x_t1, out_range=(0, 255))
x_t1 = x_t1.reshape(1, 1, x_t1.shape[0], x_t1.shape[1])
s_t1 = np.append(x_t1, s_t[:, :3, :, :], axis=1)

x_t1是单独的一帧(1x1x80x80),而s_t1是多帧图像(1x4x80x80)。你可能会问,为什么输入的维度是(1x4x80x80)而不是(4x80x80)?好吧,这个是Kerasde要求,我们遵守它就好了。

PS:一些读者可能会问为什么axis=1?这个的意思是当我在存多帧图像时,我是存储在第二维的,也即(1x4x80x80)中。

卷积神经网络

现在,我们能把处理过的屏幕输入到神经网络中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def buildmodel():
print("Now we build the model")
model = Sequential()
model.add(Convolution2D(32, 8, 8, subsample=(4,4),init=lambda shape, name: normal(shape, scale=0.01, name=name), border_mode='same',input_shape=(img_channels,img_rows,img_cols)))
model.add(Activation('relu'))
model.add(Convolution2D(64, 4, 4, subsample=(2,2),init=lambda shape, name: normal(shape, scale=0.01, name=name), border_mode='same'))
model.add(Activation('relu'))
model.add(Convolution2D(64, 3, 3, subsample=(1,1),init=lambda shape, name: normal(shape, scale=0.01, name=name), border_mode='same'))
model.add(Activation('relu'))
model.add(Flatten())
model.add(Dense(512, init=lambda shape, name: normal(shape, scale=0.01, name=name)))
model.add(Activation('relu'))
model.add(Dense(2,init=lambda shape, name: normal(shape, scale=0.01, name=name)))
adam = Adam(lr=1e-6)
model.compile(loss='mse',optimizer=adam)
print("We finish building the model")
return model

下面来解析一下上述代码:神经网络的输入包含 4x80x80 的图片。

  • 第一层: 8x8的32个卷积核,采样步长是4,采用Relu激活函数
  • 第二层: 4x4的64个卷积核,采样步长是2,采用Relu激活函数
  • 第三层: 3x3的64个卷积核,采样步长是1,采用Relu激活函数

最后隐藏层是全连接的,包含512个节点。输出是一个单节点全连接线性层。

等一下!什么是卷积?理解卷积最好的方式就是想象有一个滑动窗口函数作用在矩阵上。下面的GIF清晰地展现了卷积的过程。

卷积示意图

你可能再会问,进行卷积操作的目的是什么?实际上,卷积作用是帮助计算机理解图像的更高层的特性,比如边缘和轮廓。下面的例子展现了图像在卷积操作后清晰展现出了边缘。

卷积后展现边缘

关于更多有关于神经网络的卷积运算的知识,可以参阅Understanding Convolution Neural Networks for NLP

需要注意的

Keras让构建卷积神经网络变得十分简单。然而,这里有一些我想要强调的要点:

  1. 选取正确的初始化函数非常重要。我选取的正态分布参数σ=0.01

    1
    init=lambda shape, name: normal(shape, scale=0.01, name=name)
  2. 维度的顺序是非常重要的。默认的设置是 4x80x80 (Theano方式),但如果你的输入是 80x80x4 (Tensorflow方式) ,你就会遇到麻烦,这个时候你需要做的是设置dim_ordering = tf (tf表示tensorflow,th表示theano)

  3. 在Keras中,subsample=(2,2)表示把(80x80)大小的图像降采样为(40x40)
  4. 我们采用了自适应学习算法ADAM在进行优化。学习速度为1-e6

对更多其他学习算法感兴趣的朋友可以查阅文章:An overview of gradient descent optimization algorithms

DQN

终于,我们能使用Q-learing算法来训练神经网络了。

那到底什么是Q-learning呢?要搞清楚什么是Q-learning,首先要弄明白Q-function,也即Q(s,a)。Q(s,a)表示当我们出于states时选择actiona的最大未来得分预估。Q(s,a)会给你一个在states时选择actiona的量化预计。

你可能会问:

  1. 为什么Q-function有用?
  2. 怎么样才能得到Q-function?

让我来对Q-function作一个简单的比喻:假设你在玩一个非常难的RPG游戏,而且你又不知道怎么玩。如果说你带了一本攻略,里面包含了所有特定场合下的暗示和指导,那么这样玩游戏是不是很容易呢?你只需要照着攻略做就好。在这里,Q-function就是这样一本攻略书。假设你在states下,然后你需要决定是选actiona还是b。如果你有这个神奇的Q-function,选择就非常简单–选择带有最高Q-value的action即可!

$$
\pi (s)={ argmax }_{ a }Q(s,a)
$$

在这里,π表示policy,注意这个表示会在ML的论文中经常出现。
随着时间流动,定义动作的总未来得分为

$$
{ R_{ t }=r_{ t }+r_{ t+1 }+r_{ t+2 }…+r_{ n } }
​​$$

但是因为我们的环境是随机的无法确定,越远时间我们越难估计。因此,定义一个未来得分折扣(discount future reward)是比较通用的做法:

$$
R_{ t }=r_{ t }+\gamma r_{ t+1 }+\gamma ^{ 2 }r_{ t+2 }…+\gamma ^{ n-t }r_{ n }
$$

也就是说,未来的得分都会加上一个和折扣(discount)有关的系数$\gamma$。
回顾一下Q-function的定义(在states下选择actiona的max future reward):

$$
Q(s_{ t },a_{ t })=maxR_{ t+1 }
$$

因此,我们能重写Q-function:

$$
Q(s,a)=r+\gamma \ast max_{ a’ }Q(s’,a’)
$$

用通俗的话讲,当前state和action下的(s,a)max future reward等于当前rewardr加上下一个state和action的(${s​​’​,a​​’}$​​​​)的max future reward

我们现在能通过迭代的方法来求解Q-function。给定$(s,a,r,s’)$,我们将这个episode转换为神经网络的训练集,也即,我们试图让$r+γmax​a​​Q(s,a)$等于$Q(s,a)$。你现在能把求解Q-value作为一个回归分析任务。我有一个预估函数$r+γmax​a​​Q(s,a)$和预测函数$Q(s,a)$,然后定义均方差(MSE),也即损失函数:

$$
L=[r+\gamma max_{ a’ }Q(s’,a’)-Q(s,a)]^{ 2 }
$$

给定$(s,a,r,s’)$,怎样最优化我的Q-function使得它返回最小的均方差损失?方法是,随着L的变小,我们的Q-function就会不断的接近最优值。

现在,你可能会问,神经网络的作用是什么?现在开始讲一下Deep Q-learning怎么来的。如果采用数组表的方式表示的话,$Q(s,a)$会包含上百万条states和actions。DQN的思想是压缩Q-table($Q(s,a)$数组表)后用参数$θ$(我们成为神经网络权重)表示。因此,不需要考虑处理巨大的数据表,我们只需要考虑神经网络的权重参数:
$$
Q(s,a)=f_{ \theta }(s)
$$

其中$f$是我们神经网络,输入为$s$,权重参数为$θ$

下面是对以上解释的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if t > OBSERVE:
#sample a minibatch to train on
minibatch = random.sample(D, BATCH)
inputs = np.zeros((BATCH, s_t.shape[1], s_t.shape[2], s_t.shape[3])) #32, 80, 80, 4
targets = np.zeros((inputs.shape[0], ACTIONS)) #32, 2
#Now we do the experience replay
for i in range(0, len(minibatch)):
state_t = minibatch[i][0]
action_t = minibatch[i][1] #This is action index
reward_t = minibatch[i][2]
state_t1 = minibatch[i][3]
terminal = minibatch[i][4]
# if terminated, only equals reward
inputs[i:i + 1] = state_t #I saved down s_t
targets[i] = model.predict(state_t) # Hitting each buttom probability
Q_sa = model.predict(state_t1)
if terminal:
targets[i, action_t] = reward_t
else:
targets[i, action_t] = reward_t + GAMMA * np.max(Q_sa)
loss += model.train_on_batch(inputs, targets)
s_t = s_t1
t = t + 1

重放(Experience Replay)

如果你详细查看以上的代码,你会看到一条“重放(Experience Replay)”的注释。让我来解释一下这个是做什么的:使用非线性函数如神经网络来近似Q-value已经被发现是不稳定的。因此,我的处理方法是,在游行进行中,所有的episode$(s,a,r,s’)$都会存储在重放内存D中(我使用Python的deque来存储)。在训练神经网络时,随机的重放内存Dmini-batches会被使用,而不是最近邻的episode,这能大大提高神经网络的稳定性。

广度还是深度?

这是强化学习算法中的一个经典问题,广度优先还是深度优先?或者说,强化学习需要花多少时间去进行广度搜索,多少时间去进行深度搜索?我们其实在现实生活中也经常遇到类似的情况。比方说,在星期六的晚上,选择去吃哪一家餐厅。我们总是有一个可以选择的餐厅列表,就像有一本Q(s,a)的攻略书一样。如果我们单凭我们平时的一贯口味来看,很大可能我们会从我们知道的餐厅里面选择一家最好的。然而,有时候,我们也会愿意去尝试一些新餐厅,也许会有更好的不是嘛?强化学习也是类似的。为了最大化future reward,强化学习需要平衡现有的policy搜索(也可以称作“贪心”)和新policy搜索的时间开销。一个流行的方式叫做${\partial}$贪心策略。${\partial}$的取值范围是[0,1],决定了强化学习会花多长比例的时间随机地开始广度搜索。这个方法帮助算法不时的尝试一些新的路径以验证是否还有更好的策略。

1
2
3
4
5
6
7
8
9
if random.random() <= epsilon:
print("----------Random Action----------")
action_index = random.randrange(ACTIONS)
a_t[action_index] = 1
else:
q = model.predict(s_t) #input a stack of 4 images, get the prediction
max_Q = np.argmax(q)
action_index = max_Q
a_t[max_Q] = 1

希望本篇文章能帮助你理解DQN是如何工作的。

FAQ

我的训练速度非常慢

可能你需要一个GPU来加速计算。我使用的是TITAN X,训练了至少1百万帧才得到一个差不多的结果。

后续的工作和思考

  1. 现在的DQN依赖于大量的重放(Experience Replay)。是否有可能替换或者去掉这个步骤?
  2. 如果决定最优化卷积神经网络的方法?
  3. 训练速度很慢,有没有加速的方法?
  4. 神经网络究竟学了什么?这个模型是否可以迁移?

我相信这些问题还没有得到很好的解决。这也是Matching Learning正在活跃研究的部分。

参考

[1] Mnih Volodymyr, Koray Kavukcuoglu, David Silver, Andrei A. Rusu, Joel Veness, Marc G. Bellemare, Alex Graves, Martin Riedmiller, Andreas K. Fidjeland, Georg Ostrovski, Stig Petersen, Charles Beattie, Amir Sadik, Ioannis Antonoglou, Helen King, Dharshan Kumaran, Daan Wierstra, Shane Legg, and Demis Hassabis. Human-level Control through Deep Reinforcement Learning. Nature, 529-33, 2015.

声明

本文章的工作高度依赖于下面的repo:

  1. https://github.com/yenchenlin/DeepLearningFlappyBird
  2. http://edersantana.github.io/articles/keras_rl/

译者声明

本文章来自于Using Keras and Deep Q-Network to Play FlappyBird

修改了原文中一些明显错误。
向原作者表示致敬。