强化学习—DQN训练计算机玩Flappy Bird游戏
1 Flappy Bird游戏简述
Flappy Bird是2013年开发的一款休闲游戏,曾经风靡一时,游戏中玩家操纵一只小鸟越过管道障碍物,玩家只可以进行“跳跃”或者“不操作”两种操作。游戏截图如下:

我们的目标是教会计算机自己玩Flappy Bird游戏,注意,在训练计算机玩游戏时,我们告诉计算机的信息,仅仅包括游戏的每一帧的图像、小鸟的动作列表(跳跃、无操作)、计算机做出动作之后的reward、游戏是否结束的标志。也就是说,计算机获得的信息其实和人 类 玩家是一样的,也就是说计算机并没有“作弊”。训练计算机玩游戏,想想就觉得非常有趣,Let’s do it。
2 Q-Learning简述
关于Q-Learning,觉得这篇博客写得比较通俗易懂:A Painless Q-learning Tutorial (一个 Q-learning 算法的简明教程)
PaddlePaddle版Flappy-Bird—使用DQN算法实现游戏智能
Q-Learning的核心是两张表,R表与Q表,R表与Q表的维度是相同的,行表示状态state,列表示动作action。 经过反复不断的学习后,使得Q表收敛,我们就可以根据Q表,在当前状态下执行回报值最大的动作
在博客A Painless Q-learning Tutorial (一个 Q-learning 算法的简明教程)中根据值迭代计算出目标Q值,然后直接将这个Q值(是估计值)直接赋予新的Q,转移规则如下:

但实际上,不直接使用估计Q值来更新Q值,而是采用渐进的方式,类似梯度下降,朝目标迈近一小步,迈的步子的大小取决于α,这就能够减少估计误差造成的影响,最后可以收敛到最优的Q值,转移规则如下:

下面详细说明一下上述Q值的更新方法:
3 Deep Q Network(DQN)
DQN论文:Playing Atari with Deep Reinforcement Learning
3.1 为什么要用DQN
在Q-learning中,每一行代表一个状态,每一列代表一个动作,对于Flappy Bird游戏来说,假设我们设定输入一帧图片的 像素 矩阵为80x80,每个像素有256种可能,那么一帧图像就有 256^(80x80) 种状态,再加上游戏中的小鸟有两种动作(跳跃、无操作)。若使用Q-learning,那么程序就需要维护一个大小为2x256^(80x80)的Q表和R表,无论是维护表格,还是查表,代价都是比较大的。
我们知道,神经网络天生比较适合用来表示十分复杂的形式,所以用神经网络来维护Q表是比较合适的方式。DQN就是Q-learning+神经网络的结果,在Q-learning中,Q值需要使用一张表来进行维护,而在DQN中,输入一个状态(4帧图片),神经网络直接生成Q值。 由此可见,DQN更加适合用来处理状态复杂的问题
3.2 DQN中的几个巧妙的地方
3.2.1 experience replay(经验池、记忆库)几个变量:
s t s_{t} st:t时刻的游戏状态 a t a_{t} at:小鸟做出的动作 r t r_{t} rt:小鸟做出该动作at之后获得的直接reward s t + 1 s_{t+1} st+1:小鸟做出动作at之后的游戏状态在t时刻,小鸟做出动作at之后,产生的样本记做( s t s_{t} st, a t a_{t} at, r t r_{t} rt, s t + 1 s_{t+1} st+1) ,我们将( s t s_{t} st, a t a_{t} at, r t r_{t} rt, s t + 1 s_{t+1} st+1) 保存在experience replay中。也就是说,记忆库中保存着小鸟曾经的经历。当我们需要对计算机进行训练时,我们便从记忆库中随机抽样,取出 batch 个样本,然后进行训练。先抛出一个结论:由于样本之间的强相关性,直接从连续样本中学习是低效的,随机化样本会破坏这些相关性,从而减少更新的方差。因此这里采取记忆库+随机抽样,破坏样本的连续性,使得训练更加有效。这也是experience replay的巧妙之处
3.2.2 使用Q-target网络来更新Q网络我们使用神经网络来预测动作,这就意味着我们需要训练神经网络的权重,那么问题来了,如何训练该网络?该网络的损失函数是什么? 这也是我一开始最困惑的地方,在分类问题中,每个训练数据都有便签,输出层的每个 神经元 对应一个类别,因此可以使用交叉熵函数作为损失函数。但在DQN中,输出层有两个神经元,分别对应两个动作,而且对于每个输入状态没有标签,到底如何训练该网络?
作者使用了两个结构一模一样的DQN网络,Q_net与Q_netT,两个网络唯一的区别在于参数不同。Q_net是我们要训练的网络,Q_netT中的参数是旧的Q_net的参数。其实就是说每隔若干个timestep训练之后,将Q_net的参数赋予Q_netT。Q_net的损失函数如下,其中 y i y_{i} yi表示的是训练网络Q_net计算出来的Q值,而Q(s,a; θ i theta_{i} θi)表示的是旧的网络Q_netT预测的Q值。使用参数化逼近,参数化逼近是指值函数可以由一组参数 θ 来近似,如 Q-learning 中的 Q(s,a) 可以写成 Q(s,a|θ) 的形式。在文章PaddlePaddle版Flappy-Bird—使用DQN算法实现游戏智能中的参数化逼近部分说得比较清楚

让我们再回顾一下在Bellman方程中我们是如何更新Q值的,其更新公式如下:

许多强化学习算法背后的基本思想是通过使用上面的Bellman方程来迭代更新Q值,但实际上,这种方法是不可行的,因为动作值函数是针对每个序列单独估计的,没有任何泛化能力。相反,通常使用上述所说的函数逼近的方法来训练网络。
3.3 DQN实现细节
3.3.1 神经网络结构神经网络结构:
输入层:从记忆库里随机选取4帧图像,因为颜色对于该游戏没有意义,先将4帧图像进行预处理,处理成二值的黑白图像,看做4通道的输入卷积层:输入的数据经过三个卷积层全连接层:有两个全连接层输出层:因为该游戏只有两个动作,所有输出为2
3.3.2 代码实现的细节 代码的实现的一些细节(用言语表达起来感觉还是比较吃力,结合代码看会更清楚):
记忆库replayMemory使用一个大小确定的队列来进行维护,其中存放的是游戏过程中的数据( s t s_{t} st, a t a_{t} at, r t r_{t} rt, s t + 1 s_{t+1} st+1,terminal)。 s t s_{t} st表示游戏在t时刻的状态,包含4帧连续的图,也就是一个4通道的输入 a t a_{t} at表示游戏执行的动作 r t r_{t} rt是游戏自行返回的,表示执行动作 a t a_{t} at之后的reward s t + 1 s_{t+1} st+1表示在状态 s t s_{t} st下,执行动作 a t a_{t} at,得到的新状态 s t + 1 s_{t+1} st+1。terminal也是是游戏自行返回的,作为游戏是否结束的标志 游戏开始时,获取游戏的第一帧图像,将该图像重复四次,便得到 s 1 s_{1} s1,就是这么简单粗暴每执行一次动作,游戏会返回执行该动作之后的一帧图像。假设 s t s_{t} st是连续的四帧图像[1,2,3,4],执行 a t a_{t} at之后,游戏会返回一帧图像5,则 s t + 1 s_{t+1} st+1=[2,3,4,5],然后会将( s t s_{t} st, a t a_{t} at, r t r_{t} rt, s t + 1 s_{t+1} st+1,terminal)存入记忆库中,若记忆库已满,则将最早存入的数据替换出去。每个timestep,从记忆库中随机选择4个样本,将输入变成0-255的灰色图像,然后二值化处理,变成黑白图像前OBSERVE轮次不会对网络进行训练,让小鸟采取随机动作,主要是为了收集state,并将这些state存到记忆库中,供后续训练使用经过OBSERVE轮次的采集数据之后,开始对网络进行训练。从记忆库中随机取出batch个数据,上面我们说过,DQN中有两个神经网络,分别计算出两个网络预测Q值,使用误差平方和作为损失函数,来优化网络神经网络的Q值是什么?这个问题纠结了很久,神经网络输出层是两个神经元,连个神经元的输出值可以看做两个动作的reward值,将输出值最大的作为网络输出的Q值即可小鸟采取的动作并不都是根据神经网络计算出来的,有epsilonde概率是随机选择动作,1-epsilon的概率执行神经网络预测的动作每隔UPDATE_TIME轮次,用训练的网络的参数来更新旧的网络的参数算法流程如下:

4 问题
随着训练次数增大,貌似训练效果有时会更差。训练70万次时,可以越过15次左右障碍物,但训练到90万次时,效果反而更差了,有时只能越过两三次障碍物,可能此时训练次数还不够吧。
5 代码注释
代码详见:Playing-Flappy-Bird-by-DQN-on-PyTorch
我对 main 文件下的代码进行了比较详细的注释,另外两个代码文件是用pygame写的游戏代码,大可不必深究细节
import pdb import cv2 import sys import os sys.path.append("/") import wrapped_flappy_bird as game import random import numpy as np from collections import deque import torch from torch.autograd import Variable import torch.nn as nn GAME = 'bird' # the name of the being played for log files ACTIONS = 2 # number of valid actions GAMMA = 0.99 # decay rate of past observations OBSERVE = 1000. # 前OBSERVE轮次,不对网络进行训练,只是收集数据,存到记忆库中 # 第OBSERVE到OBSERVE+EXPLORE轮次中,对网络进行训练,且对epsilon进行退火,逐渐减小epsilon至FINAL_EPSILON # 当到达EXPLORE轮次时,epsilon达到最终值FINAL_EPSILON,不再对其进行更新 EXPLORE = 2000000. FINAL_EPSILON = 0.0001 # epsilon的最终值 INITIAL_EPSILON = 0.0001 # epsilon的初始值 REPLAY_MEMORY = 50000 # 记忆库 BATCH_SIZE = 32 # 训练批次 FRAME_PER_ACTION = 1 # 每隔FRAME_PER_ACTION轮次,就会有epsilon的概率进行探索 UPDATE_TIME = 100 # 每隔UPDATE_TIME轮次,对target网络的参数进行更新 width = 80 height = 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, (1, 80, 80)) # 神经网络结构,结构较为容易理解 class DeepNetWork(nn.Module): def __init__(self, ): super(DeepNetWork, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(in_channels=4, out_channels=32, kernel_size=8, stride=4, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2) ) self.conv2 = nn.Sequential( nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=1), nn.ReLU(inplace=True) ) self.conv3 = nn.Sequential( nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True) ) self.fc1 = nn.Sequential( nn.Linear(1600, 256), nn.ReLU() ) self.out = nn.Linear(256, 2) def forward(self, x): x = self.conv1(x); x = self.conv2(x); x = self.conv3(x); x = x.view(x.size(0), -1) x = self.fc1(x); return self.out(x) class BrainDQNMain(object): def save(self): print("save model param") torch.save(self.Q_net.state_dict(), 'params3.pth') def load(self): if os.path.exists("params3.pth"): print("load model param") self.Q_net.load_state_dict(torch.load('params3.pth')) self.Q_netT.load_state_dict(torch.load('params3.pth')) def __init__(self, actions): # 在每个timestep下agent与环境交互得到的转移样本 (st,at,rt,st+1) 储存到回放记忆库, # 要训练时就随机拿出一些(minibatch)数据来训练,打乱其中的相关性 self.replayMemory = deque() # init some parameters self.timeStep = 0 # 有epsilon的概率,随机选择一个动作,1-epsilon的概率通过网络输出的Q(max)值选择动作 self.epsilon = INITIAL_EPSILON # 初始化动作 self.actions = actions # 当前值网络 self.Q_net = DeepNetWork() # 目标值网络 self.Q_netT = DeepNetWork(); # 加载训练好的模型,在训练的模型基础上继续训练 self.load() # 使用均方误差作为损失函数 self.loss_func = nn.MSELoss() LR = 1e-6 self.optimizer = torch.optim.Adam(self.Q_net.parameters(), lr=LR) # 使用minibatch训练网络 def train(self): # Step 1: obtain random minibatch from replay memory # 从记忆库中随机获得BATCH_SIZE个数据进行训练 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] # Step 2: calculate y # y_batch用来存储reward y_batch = np.zeros([BATCH_SIZE, 1]) nextState_batch = np.array(nextState_batch) # print("train next state shape") # print(nextState_batch.shape) nextState_batch = torch.Tensor(nextState_batch) action_batch = np.array(action_batch) # 每个action包含两个元素的数组,数组必定是一个1,一个0,最大值的下标也就是该action的下标 index = action_batch.argmax(axis=1) print("action " + str(index)) index = np.reshape(index, [BATCH_SIZE, 1]) # 预测的动作的下标 action_batch_tensor = torch.LongTensor(index) # 使用target网络,预测nextState_batch的动作 QValue_batch = self.Q_netT(nextState_batch) QValue_batch = QValue_batch.detach().numpy() # 计算每个state的reward for i in range(0, BATCH_SIZE): # terminal是结束标志 terminal = minibatch[i][4] if terminal: y_batch[i][0] = reward_batch[i] else: # 这里的QValue_batch[i]为数组,大小为所有动作集合大小,QValue_batch[i],代表 # 做所有动作的Q值数组,y计算为如果游戏停止,y=rewaerd[i],如果没停止,则y=reward[i]+gamma*np.max(Qvalue[i]) # 代表当前y值为当前reward+未来预期最大值*gamma(gamma:经验系数) #网络的输出层的维度为2,将输出值中的最大值作为Q值 y_batch[i][0] = reward_batch[i] + GAMMA * np.max(QValue_batch[i]) y_batch = np.array(y_batch) y_batch = np.reshape(y_batch, [BATCH_SIZE, 1]) state_batch_tensor = Variable(torch.Tensor(state_batch)) y_batch_tensor = Variable(torch.Tensor(y_batch)) y_predict = self.Q_net(state_batch_tensor).gather(1, action_batch_tensor) loss = self.loss_func(y_predict, y_batch_tensor) print("loss is " + str(loss)) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # 每隔UPDATE_TIME轮次,用训练的网络的参数来更新target网络的参数 if self.timeStep % UPDATE_TIME == 0: self.Q_netT.load_state_dict(self.Q_net.state_dict()) self.save() # 更新记忆库,若轮次达到一定要求则对网络进行训练 def setPerception(self, nextObservation, action, reward, terminal): # print(nextObservation.shape) # 每个state由4帧图像组成 # nextObservation是新的一帧图像,记做5。currentState包含4帧图像[1,2,3,4],则newState将变成[2,3,4,5] newState = np.append(self.currentState[1:, :, :], nextObservation, axis=0) # newState = np.append(nextObservation,self.currentState[:,:,1:],axis = 2) # 将当前状态存入记忆库 self.replayMemory.append((self.currentState, action, reward, newState, terminal)) # 若记忆库已满,替换出最早进入记忆库的数据 if len(self.replayMemory) > REPLAY_MEMORY: self.replayMemory.popleft() # 在训练之前,需要先观察OBSERVE轮次的数据,经过收集OBSERVE轮次的数据之后,开始训练网络 if self.timeStep > OBSERVE: # Train the network self.train() # print info state = "" # 在前OBSERVE轮中,不对网络进行训练,相当于对记忆库replayMemory进行填充数据 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 getAction(self): currentState = torch.Tensor([self.currentState]) # QValue为网络预测的动作 QValue = self.Q_net(currentState)[0] action = np.zeros(self.actions) # FRAME_PER_ACTION=1表示每一步都有可能进行探索 if self.timeStep % FRAME_PER_ACTION == 0: if random.random() <= self.epsilon: # 有epsilon得概率随机选取一个动作 action_index = random.randrange(self.actions) print("choose random action " + str(action_index)) action[action_index] = 1 else: # 1-epsilon的概率通过神经网络选取下一个动作 action_index = np.argmax(QValue.detach().numpy()) print("choose qnet value action " + str(action_index)) action[action_index] = 1 else: # 程序貌似不会走到这里 action[0] = 1 # do nothing # 随着迭代次数增加,逐渐减小episilon if self.epsilon > FINAL_EPSILON and self.timeStep > OBSERVE: self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / EXPLORE return action # 初始化状态 def setInitState(self, observation): # 增加一个维度,observation的维度是80x80,讲过stack()操作之后,变成4x80x80 self.currentState = np.stack((observation, observation, observation, observation), axis=0) print(self.currentState.shape) if __name__ == '__main__': actions = 2 # 动作个数 brain = BrainDQNMain(actions) flappyBird = game.GameState() action0 = np.array([1, 0]) # 一个随机动作 # 执行一个动作,获得执行动作后的下一帧图像、reward、游戏是否终止的标志 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) # 将一帧图片重复4次,每一张图片为一个通道,变成通道为4的输入,即初始输入是4帧相同的图片 brain.setInitState(observation0) print(brain.currentState.shape) while 1 != 0: # 获取下一个动作 action = brain.getAction() # 执行动作,获得执行动作后的下一帧图像、reward、游戏是否终止的标志 nextObservation, reward, terminal = flappyBird.frame_step(action) # 将一帧彩色图像处理成黑白的二值图像 nextObservation = preprocess(nextObservation) # print(nextObservation.shape) brain.setPerception(nextObservation, action, reward, terminal)
py
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236相关知识
强化学习—DQN训练计算机玩Flappy Bird游戏
AI玩Flappy Bird│基于DQN的机器学习实例【完结】
使用神经网络+遗传算法玩转Flappy Bird
《Flappy Bird》升级版即将在亚马逊Fire TV上架
一种很小的游戏,就触摸屏幕控制小鸟的高度通过障碍~
天下苦深度强化学习久矣,这有一份训练与调参技巧手册
强化学习:通过奖励与惩罚驱动智能体学习的方法
机械迷城花园第五个灯怎么过(机械迷城点亮绿灯攻略)
强化学习
bird alone游戏下载
网址: 强化学习—DQN训练计算机玩Flappy Bird游戏 https://www.mcbbbk.com/newsview1354410.html
| 上一篇: 百灵鸟怎么养 百灵鸟喜欢吃稻子和 |
下一篇: 中央大街4200㎡室内动物主题乐 |
推荐分享
- 1养玉米蛇的危害 28694
- 2狗交配为什么会锁住?从狗狗生 7180
- 3我的狗老公李淑敏33——如何 6236
- 4豆柴犬为什么不建议养?可爱的 4637
- 5南京宠物粮食薄荷饼宠物食品包 4563
- 6中国境内禁养的十大鸟种,你知 4429
- 7湖南隆飞尔动物药业有限公司宠 4259
- 8自制狗狗辅食:棉花面纱犬的美 4257
- 9家养水獭多少钱一只正常 4212
- 10广州哪里卖宠物猫狗的选择性多 4122
