强化学习DQN玩转FlappyBird|前景提要

2020-11-03 16:19:36 浏览数 (1)

前景提要

强化学习是机器学习大家族中的一大类, 使用强化学习能够让机器学着如何在环境中拿到高分, 表现出优秀的成绩. 而这些成绩背后却是他所付出的辛苦劳动, 不断的试错, 不断地尝试, 累积经验, 学习经验。

而DQN更是强化学习家族中最会玩游戏的一位,甚至可以在一些游戏中超越人类。

经典游戏--FlappyBird

介绍了玩游戏的DQN,我们再来看一看我们要玩的游戏,FlappyBird。

FlappyBird,这是一款简单又困难的手机游戏,游戏中玩家控制着一只胖乎乎的小鸟,跨越由各种不同长度水管所组成的障碍。小鸟会在引力的作用下向下坠落,玩家点击一次屏幕,小鸟就会向上弹跳一次。每通过一个障碍,便可以得到1分,如果小鸟掉下来或者撞到水管,便游戏结束。规则非常简单,但是想拿到高分可不简单,因为小鸟飞行的轨迹是一个个的抛物线,稍有不慎便会触碰到水管,那就要从头再来啦(像小编这样的手残玩家,玩了一下午最高分只能30多分,而训练了六个小时的DQN稳稳当当的可以得到100多分)。

然而DQN说我是大佬,我来玩!

下面就来介绍我们如何使用DQN玩转FlappyBird!

在代码中我们采用了CV2库去读取80*80的图像来获取FlappyBird的状态,不了解CV2的小伙伴不用着急,我们在推文最后有对CV2库的简单介绍,大家可以了解一下。

而神经网络的训练的优化器我们选择了Adam方法,以加快训练速度。

那什么是Adam优化器,为什么它可以加快训练速度:

如图,传统的参数W的更新是把原始的W累加上一个负的学习率(learning rate) 乘以校正值 (dx)。这种方法可能会让学习过程曲折无比, 看起来像喝醉的人回家时, 摇摇晃晃走了很多弯路。

而如果这个人从平地上放到了一个斜坡上, 只要他往下坡的方向走一点点, 由于向下的惯性, 他不自觉地就一直往下走, 走的弯路也变少了。这就是Momentum参数更新方法。如图是其表达式。

如果我们不是给喝醉酒的人安排另一个下坡, 而是给他一双不好走路的鞋子, 使得他一摇晃着走路就脚疼, 鞋子成为了走弯路的阻力, 逼着他往前直着走。这就是AdaGrad 参数更新方法。如图是其表达式。

说到这,肯定有小伙伴已经猜到了!如果我们同时给他一个下坡, 一双破鞋子,他会走得更好,更新的速度将会更快,这就是Adam更新方法。计算m 时有 momentum 下坡的属性, 计算 v 时有 adagrad 阻力的属性,又快又好的达到目标, 迅速收敛。

铺垫完了,大家是不是等着急了

下面我们就来看一看DQN是如何玩转FlappyBird的!!

介绍代码之前先上我们的训练结果。

我们将训练的每回合得分制成柱状图,可以看出,在前一千回合内,DQN几乎玩FlappyBird得分为零,而在训练了1200多回合后,开始渐渐得分了,但不高在几十左右;而随着训练的继续,在2700多回合后,DQN得分基本稳定在一百多,而继续再训练几百回合,就能够达到四五百分,可谓进步神速。

附上训练视频:

最开始没有经过任何学习,FlappyBird总是会撞到水管上而失败。

当训练到240000多步时,FlappyBird已经可以开始飞起来了。

网络构造

首先我们来看一看DQN强大的大脑构造吧

哦不,是神经网络的构造!

注:小编代码中tensorflow的用法是1点几的版本,如果大家安装的是最新的tensorflow,运行源码需要加上下面的代码来降版本!

代码语言:javascript复制
import tensorflow.compat.v1 as tf
代码语言:javascript复制
tf.disable_v2_behavior()

首先是对各个参数以及神经网络的初始化:

代码语言:javascript复制
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
import numpy as np 
import random
from collections import deque 
FRAME_PER_ACTION = 1 # 每一个动作的帧数
GAMMA = 0.99 # 对观测值的衰减程度
OBSERVE = 100. # 训练前观测的步数
EXPLORE = 200000. # 探索阶段的步数
FINAL_EPSILON = 0 # epsilon的最终值,刚开始训练设置为0.001
INITIAL_EPSILON = 0 # epsilon的初始值,刚开始训练设置为0.01
REPLAY_MEMORY = 50000 # 经验池回顾的步数
BATCH_SIZE = 32 # minibatch的大小
UPDATE_TIME = 100 # 每过UPDATE_TIME,更新Target Q Network
try:
    tf.mul
except:
    # 如果是新版本,用tf.multiply替换replace tf.mul
    tf.mul = tf.multiply
class BrainDQN:
   def __init__(self,actions):
      # 初始化经验池
      self.replayMemory = deque()
      # 初始化参数
      self.timeStep = 0
      self.epsilon = INITIAL_EPSILON
      self.actions = actions
      # 初始化Q network   self.stateInput,self.QValue,self.W_conv1,self.b_conv1,self.W_conv2,self.b_conv2,self.W_conv3,self.b_conv3,self.W_fc1,self.b_fc1,self.W_fc2,self.b_fc2 = self.createQNetwork()
      # 初始化Target Q Network    self.stateInputT,self.QValueT,self.W_conv1T,self.b_conv1T,self.W_conv2T,self.b_conv2T,self.W_conv3T,self.b_conv3T,self.W_fc1T,self.b_fc1T,self.W_fc2T,self.b_fc2T = self.createQNetwork()

复制Q(估计)网络参数给Q(现实)网络:

代码语言:javascript复制
# 复制Q network,更新Target Q Network
self.copy = [self.W_conv1T.assign(self.W_conv1),self.b_conv1T.assign(self.b_conv1),self.W_conv2T.assign(self.W_conv2),self.b_conv2T.assign(self.b_conv2),self.W_conv3T.assign(self.W_conv3),self.b_conv3T.assign(self.b_conv3),self.W_fc1T.assign(self.W_fc1),self.b_fc1T.assign(self.b_fc1),self.W_fc2T.assign(self.W_fc2),self.b_fc2T.assign(self.b_fc2)]

使用Adam优化器来最小化cost,达到训练目的,并对神经网络进行存取。

代码语言:javascript复制
# 创建训练方法
self.actionInput = tf.placeholder("float",[None,self.actions])
self.yInput = tf.placeholder("float", [None]) 
Q_Action = tf.reduce_sum(tf.mul(self.QValue, self.actionInput), reduction_indices = 1)
self.cost = tf.reduce_mean(tf.square(self.yInput - Q_Action))
# 这里使用了一个叫亚当的自适应优化算法,学习率为1e-6
self.trainStep = tf.train.AdamOptimizer(1e-6).minimize(self.cost)
# 存取神经网络
self.saver = tf.train.Saver()
self.session = tf.InteractiveSession()
self.session.run(tf.initialize_all_variables())
checkpoint = tf.train.get_checkpoint_state("networks")
if checkpoint and checkpoint.model_checkpoint_path:
      self.saver.restore(self.session, checkpoint.model_checkpoint_path)
      print ("Successfully loaded:", checkpoint.model_checkpoint_path)
else:
print ("Could not find old network weights")

神经网络的架构,我们设定了三个卷积层,两个全连接层来输入的图像进行处理,并最终输出Q值列表。

代码语言:javascript复制
# 四通道输入图像
def setInitState(self,observation):
self.currentState = np.stack((observation, observation, observation, observation), axis = 2)  
# 定义权重、偏置、卷积和池化函数
def weight_variable(self,shape):
   initial = tf.truncated_normal(shape, stddev = 0.01)
   return tf.Variable(initial)
def bias_variable(self,shape):
   initial = tf.constant(0.01, shape = shape)
   return tf.Variable(initial)
def conv2d(self,x, W, stride):
   return tf.nn.conv2d(x, W, strides = [1, stride, stride, 1], padding = "SAME")
def max_pool_2x2(self,x):
   return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = "SAME")
   
def createQNetwork(self):
   # 第一层卷积层
   W_conv1 = self.weight_variable([8,8,4,32])
   b_conv1 = self.bias_variable([32])
   # 第二层卷积层
   W_conv2 = self.weight_variable([4,4,32,64])
   b_conv2 = self.bias_variable([64])
   # 第三层卷积层
   W_conv3 = self.weight_variable([3,3,64,64])
   b_conv3 = self.bias_variable([64])
   # 第一层全连接层
   W_fc1 = self.weight_variable([1600,512])
   b_fc1 = self.bias_variable([512])
   # 第二层全连接层
   W_fc2 = self.weight_variable([512,self.actions])
   b_fc2 = self.bias_variable([self.actions])
   #第一层卷积层,有32个卷积核(过滤器),每个卷积核的尺寸是8x8,x轴和y轴的步幅都是4,补零,并使用了一个relu激活函数。
   #第二层卷积层,有64个卷积核(过滤器),每个卷积核的尺寸是4x4,x轴和y轴的步幅都是2,补零,并使用了一个relu激活函数。
   #第三层卷积层,有64个卷积核(过滤器),每个卷积核的尺寸是3x3,x轴和y轴的步幅都是1,补零,并使用了一个relu激活函数。

   # 输入层
   stateInput = tf.placeholder("float",[None,80,80,4])
   # 隐藏层
   # 第一层隐藏层(用了一个池化层)
   h_conv1 = tf.nn.relu(self.conv2d(stateInput,W_conv1,4)   b_conv1)
   h_pool1 = self.max_pool_2x2(h_conv1)
   # 第二层隐藏层
   h_conv2 = tf.nn.relu(self.conv2d(h_pool1,W_conv2,2)   b_conv2)
   # 第三层隐藏层
   h_conv3 = tf.nn.relu(self.conv2d(h_conv2,W_conv3,1)   b_conv3)
   # 展开
   h_flat = tf.reshape(h_conv3,[-1,1600])
   # 全连接层
   h_fc1 = tf.nn.relu(tf.matmul(h_flat,W_fc1)   b_fc1)#全连接层
   # 输出层,输出动作对应的Q值列表
   QValue = tf.matmul(h_fc1,W_fc2)   b_fc2
   return stateInput,QValue,W_conv1,b_conv1,W_conv2,b_conv2,W_conv3,b_conv3,W_fc1,b_fc1,W_fc2,b_fc2

这里我们构造了个经验池,用来保存历史数据,并可从经验池中抽取数据来进行对神经网络的训练,这也是DQN为什么玩游戏玩得好的重要原因。经验池保存的是一个马尔科夫序列,功能主要是解决相关性及非静态分布问题。具体做法是把每个时间步agent与环境交互得到的转移样本储存到回放记忆单元,要训练时就随机拿出一些(minibatch)来训练其实就是将游戏的过程打成碎片存储,训练时随机抽取就避免了相关性问题。

代码语言:javascript复制
# 更新状态、经验池
def setPerception(self,nextObservation,action,reward,terminal):
   newState = np.append(self.currentState[:,:,1:],nextObservation,axis = 2)
   self.replayMemory.append((self.currentState,action,reward,newState,terminal))
   if len(self.replayMemory) > REPLAY_MEMORY:
      self.replayMemory.popleft()
   if self.timeStep > OBSERVE:
      self.trainQNetwork()
   state = ""
   if self.timeStep <= OBSERVE:
      state = "observe"
   elif self.timeStep > OBSERVE and self.timeStep <= OBSERVE   EXPLORE:
      state = "explore"
   else:
      state = "train"

   print ("TIMESTEP = ", self.timeStep, "  STATE = ", state, 
           "  EPSILON = ", self.epsilon)
   self.currentState = newState
   self.timeStep  = 1  
def trainQNetwork(self):
   # 从经验池中抽取小批量样本
   # 下面分别对应currentState,action,reward,newState,terminal
   minibatch = random.sample(self.replayMemory,BATCH_SIZE)
   state_batch = [data[0] for data in minibatch] # 当前状态
   action_batch = [data[1] for data in minibatch] # 输入动作
   reward_batch = [data[2] for data in minibatch] # 返回奖励
   nextState_batch = [data[3] for data in minibatch] # 返回的下一状态
   # 得到预测的以输入动作为索引的Q值
   y_batch = []
   # 获得下一状态的Q值
   QValue_batch = self.QValueT.eval(feed_dict={self.stateInputT:nextState_batch})
   for i in range(0,BATCH_SIZE):
      terminal = minibatch[i][4]
      #布尔值terminal表示游戏是否结束
      if terminal:
         y_batch.append(reward_batch[i])
      else:
         y_batch.append(reward_batch[i]   GAMMA * np.max(QValue_batch[i]))
   # 进行训练
   self.trainStep.run(feed_dict={
      self.yInput : y_batch,
      self.actionInput : action_batch,
      self.stateInput : state_batch
      })
   # 每10000步保存一次
   if self.timeStep % 10000 == 0:
      self.saver.save(self.session, 'networks/'   'network'   '-dqn', global_step = self.timeStep)
   if self.timeStep % UPDATE_TIME == 0:
      self.session.run(self.copy)

最后是动作Action的获取。

代码语言:javascript复制
def getAction(self):
 # 这里的训练数据为之前提到的四通道图像的模型输出
   QValue = self.QValue.eval(feed_dict= {self.stateInput:[self.currentState]})[0]
   action = np.zeros(self.actions)
   action_index = 0
   # 根据ε概率选择一个Action
   if self.timeStep % FRAME_PER_ACTION == 0:
      if random.random() <= self.epsilon:
         action_index = random.randrange(self.actions)
         action[action_index] = 1
      else:
         action_index = np.argmax(QValue)
         action[action_index] = 1
   else:
      action[0] = 1 # 不做动作
   # episilon随着步数而减小
   if self.epsilon > FINAL_EPSILON and self.timeStep > OBSERVE:
      self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON)/EXPLORE
 return action

讲完了神经网络的构造,我们再来看看他是如何通过神经网络来学着玩FlappyBird的吧。

这里我们定义了Bird的初始环境与动作,并输入神经网络开始学习,大概学了三个多小时DQN终于成为了玩FlappyBird的高手。

代码语言:javascript复制
import cv2
import sys
sys.path.append("game/")
import wrapped_flappy_bird as game
from BrainDQN_Nature import BrainDQN
import numpy as np
# 把图像处理成80*80,进行灰度化和二值化
def preprocess(observation):
   observation = cv2.cvtColor(cv2.resize(observation, (80, 80)), cv2.COLOR_BGR2GRAY)
   ret, observation = cv2.threshold(observation,1,255,cv2.THRESH_BINARY)
   return np.reshape(observation,(80,80,1))
def playFlappyBird():
   actions = 2
   brain = BrainDQN(actions)
   flappyBird = game.GameState()
   # 初始化选择的动作、返回的状态、得分
   action0 = np.array([1,0]) # [1,0]表示不做动作,[0,1]表示跳
   observation0, reward0, terminal = flappyBird.frame_step(action0)
   observation0 = cv2.cvtColor(cv2.resize(observation0, (80, 80)), cv2.COLOR_BGR2GRAY)
   ret, observation0 = cv2.threshold(observation0,1,255,cv2.THRESH_BINARY)
   brain.setInitState(observation0)
   while True:
      action = brain.getAction()
      # 从游戏中返回状态、得分、游戏是否结束
      nextObservation,reward,terminal = flappyBird.frame_step(action)
      nextObservation = preprocess(nextObservation)
      brain.setPerception(nextObservation,action,reward,terminal)
def main():
   playFlappyBird()
if __name__ == '__main__':
   main()

CV2的简单介绍

对于HappyBird的状态的设定,我们是通过python的CV2库来读入80*80的图片来作为HappyBird的状态。

那么CV2库怎么用呢?

下面来简单介绍一下:

Cv2提供了简单的图片加载,显示保存方法:

加载图片:

cv2.imread(path, flags)函数

窗体建立:(用来显示图片)

cv2.namedWindow()

显示图像:

cv2.imshow(wname,img),第一个参数是显示图像的窗口的名字,第二个参数是要显示的图像

图片存储:

cv2.imwrite()

另外还提供了对于图片的一些操作:

颜色空间转换:

彩色图像转为灰度图像

img2 = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY)

灰度图像转为彩色图像

img3 = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)

基本图像处理:

cv2.resize()实现缩放:

裁剪则是利用array自身的下标截取实现

Threshold():固定阈值二值化

ret, dst = cv2.threshold(src, thresh, maxval, type)

Src: 输入图,只能输入单通道图像,通常来说为灰度图

Dst: 输出图

Thresh: 阈值

Maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值

Type:二值化操作的类型,包含以下5种类型: cv2.THRESH_BINARY;cv2.THRESH_BINARY_INV;cv2.THRESH_TRUNC;cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV

参考资料:

莫烦强化学习教学视频https://mofanpy.com/tutorials/machine-learning/reinforcement-learning/

莫烦tensorflow教学视频:

https://mofanpy.com/tutorials/machine-learning/tensorflow/

Csdn博客:

https://blog.csdn.net/qq_29462849/article/details/80904469?utm_medium=distribute.wap_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.wap_blog_relevant_pic&depth_1-utm_source=distribute.wap_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.wap_blog_relevant_pic

- END -

0 人点赞