原文链接补一波之前说好的用DQN自动玩Chrome浏览器的小恐龙游戏呗~mp.weixin.qq.com
效果展示
在cmd窗口运行如下命令即可:
模型训练:
python TRexRush.py --resume
模型测试:
python TRexRush.py --mode test
效果如下:dqn玩t-rex rushhttps://www.zhihu.com/video/1245767210263580672
导语
月初的时候推过一篇文章:
当时说之后会出个强化学习的版本,结果一直没出。原因很简单,模型训好之后我手贱不小心秀了一波rm -rf(被删的模型发挥的比较好的时候都可以跑到几万分了),搞得我心态有点崩,所以就弃坑了一段时间。
言归正传,今天花了点时间又重新训了一个,虽然效果没之前的好(毕竟只训了几个小时),不过作为效果演示用也已经足够了。感兴趣的小伙伴可以在我训的模型的基础上继续多训一会,然后调一调参数啥的。
废话不多说,让我们愉快地开始吧~
相关文件CharlesPikachu/AIGamesgithub.com
开发工具
Python版本:3.6.4
相关模块:
pytorch模块(version>=1.0);
numpy模块;
pillow模块;
selenium模块;
opencv-python模块;
argparse模块;
以及一些python自带的模块。
环境搭建
安装Python并添加到环境变量,pip安装需要的相关模块即可。
原理简介
关于DQN的原理介绍,小伙伴们还是参考公众号之前的文章吧:
本文就不作赘述了。这里只介绍一下如何把DQN用在玩T-Rex Rush这款谷歌浏览器自带的彩蛋游戏里。这个游戏玩法很简单,玩家通过操纵空格键来控制小恐龙跳跃或者不跳跃,从而躲避路上的障碍物。当小恐龙不小心撞到障碍物时,游戏结束。
首先,我们来定义一个控制器类,它的主要作用是提供控制小恐龙的接口。具体而言,代码实现如下:
class GameController():
def __init__(self, cfg, **kwargs):
chrome_options = Options()
chrome_options.add_argument("disable-infobars")
self.driver = webdriver.Chrome(
executable_path=cfg.DRIVER_PATH,
chrome_options=chrome_options
)
self.driver.maximize_window()
self.driver.get(cfg.GAME_URL)
self.driver.execute_script("Runner.config.ACCELERATION=0")
self.driver.execute_script("document.getElementsByClassName('runner-canvas')[0].id = 'runner-canvas'")
self.restart()
self.jump()
'''jump'''
def jump(self):
self.driver.find_element_by_tag_name("body").send_keys(Keys.ARROW_UP)
'''bow the head'''
def bowhead(self):
self.driver.find_element_by_tag_name("body").send_keys(Keys.ARROW_DOWN)
'''pause the game'''
def pause(self):
return self.driver.execute_script("Runner.instance_.stop();")
'''restart the game'''
def restart(self):
self.driver.execute_script("Runner.instance_.restart();")
time.sleep(0.2)
'''resume the game'''
def resume(self):
return self.driver.execute_script("Runner.instance_.play();")
'''stop the game'''
def stop(self):
self.driver.close()
'''get the game state'''
def state(self, type_):
assert type_ in ['crashed', 'playing', 'score']
if type_ == 'crashed':
return self.driver.execute_script("return Runner.instance_.crashed;")
elif type_ == 'playing':
return self.driver.execute_script("return Runner.instance_.playing;")
else:
digits = self.driver.execute_script("return Runner.instance_.distanceMeter.digits;")
score = ''.join(digits)
return int(score)
即我们用chromedriver来控制小恐龙的行动。为了方便起见,我们再定义一个外部统一调用的接口,即只需要传入小恐龙当前需要采取的行动就可以自动实现小恐龙的跳跃/不动/低头,并返回一些必要的关于游戏当前状态的信息:
'''run'''
def run(self, action):
# operate T-Rex according to the action
if action[0] == 1:
pass
elif action[1] == 1:
self.jump()
elif action[2] == 1:
self.bowhead()
# get score
score = self.state('score')
# whether die or not
if self.state('crashed'):
self.restart()
is_dead = True
else:
is_dead = False
# get game image
image = self.screenshot()
# return necessary info
return image, score, is_dead
其中,游戏画面的截屏效果大概是这样子的:
接着,我们来搭建一个简单的神经网络:
'''build network'''
class DeepQNetwork(nn.Module):
def __init__(self, imagesize, num_input_frames, num_actions, **kwargs):
super(DeepQNetwork, self).__init__()
assert imagesize == (84, 84)
self.conv1 = nn.Conv2d(in_channels=num_input_frames, out_channels=32, kernel_size=8, stride=4, padding=0)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=0)
self.conv3 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=0)
self.fc1 = nn.Linear(in_features=3136, out_features=512)
self.fc2 = nn.Linear(in_features=512, out_features=num_actions)
def forward(self, x):
batch_size = x.size(0)
x = self.conv1(x)
x = F.relu(x, inplace=True)
x = self.conv2(x)
x = F.relu(x, inplace=True)
x = self.conv3(x)
x = F.relu(x, inplace=True)
x = x.view(batch_size, -1)
x = F.relu(self.fc1(x), inplace=True)
x = self.fc2(x)
return x
@staticmethod
def initWeights(m):
if type(m) == nn.Conv2d or type(m) == nn.Linear:
nn.init.uniform_(m.weight, -0.01, 0.01)
m.bias.data.fill_(0.01)
网络结构很简单,就是三个卷积层再加两个全连接层。网络最终的输出是一个三维向量[x1, x2, x3]。当x1较大时,小恐龙不动;x2较大时,小恐龙跳跃;x3较大时,小恐龙低头。
对于网络输入,为了让网络可以更好地进行决策,我们保留游戏最近的4帧画面作为网络的输入,来预测小恐龙下一步的行动。另外,考虑到这个游戏的特殊性,除了有障碍物那会需要让小恐龙进行跳跃或者低头外,其他大多数时候小恐龙其实不动一直向前冲就好了。所以我们增加一下限制,网络每秒钟至多做25次决策:
# control the FPS
if last_time:
fps_now = 1 / (time.time() - last_time)
if fps_now > self.fps:
time.sleep(1 / self.fps - 1 / fps_now)
last_time = time.time()
接下来,我们就可以开始训练我们的网络啦。即一开始让小恐龙随机地进行行动,以储备网络的训练数据,当训练数据达到一定的量级时,我们每次从中随机选出一个batch的数据(比如32个),输入到网络进行模型训练:
if self.num_iters > self.num_observes:
self.optimizer.zero_grad()
minibatch = random.sample(self.replay_memory_record, self.batch_size)
states, states1, actions, is_deads, rewards = zip(*minibatch)
states = torch.from_numpy(np.concatenate(states)).type(self.FloatTensor)
states1 = torch.from_numpy(np.concatenate(states1)).type(self.FloatTensor)
actions = torch.from_numpy(np.concatenate(actions)).type(self.FloatTensor).view(self.batch_size, -1)
is_deads = torch.from_numpy(np.concatenate(is_deads)).type(self.FloatTensor)
rewards = torch.from_numpy(np.concatenate(rewards)).type(self.FloatTensor)
with torch.no_grad():
targets = rewards + self.discount_factor * self.dqn_model(states1).max(-1)[0] * (1 - is_deads)
targets = targets.detach()
preds = torch.sum(self.dqn_model(states) * actions, dim=1)
loss = self.loss_func(preds, targets)
loss.backward()
self.optimizer.step()
其中rewards是人为定的,这里我规定:
reward=0.1: 平安无事
reward=-1: 小恐龙死掉了
states则代表之前的游戏画面,states1代表采取了行动后,获得的当前游戏画面。is_deads则代表这局游戏有没有结束。与此同时,我们会不断更新当前的训练数据,即把旧的训练数据剔除:
if len(self.replay_memory_record) > self.replay_memory_size:
self.replay_memory_record.popleft()
然后添加最近获得的新的训练数据(因为网络训练的同时,游戏也在同步地进行,从而产生新的训练数据,此时游戏中的小恐龙不再是完全随机行动了,而是采用ϵ-greedy策略,即以一定的概率随机行动,一定的概率由网络控制行动,且随机行动的概率会逐渐减小,最终到几乎完全由网络来控制小恐龙的行动):
if is_dead or random.random() <= self.pos_save_prob:
self.replay_memory_record.append([input_image_prev, self.input_image, action, np.array([int(is_dead)]), np.array([reward])])
其中pos_save_prob用于控制正样本的数量,因为一般情况下会有很多奖励reward=0.1的训练数据,而只有极少数的reward=-1的训练数据,为了让网络更快地明白自己是怎么犯蠢从而把小恐龙玩死的,我们可以以一定的概率保留reward=0.1的训练数据,而reward=-1的训练数据则全部保留。此外,ϵ-greedy策略的代码实现如下:
# randomly or use dqn_model to decide the action of T-Rex
action = np.array([0] * self.num_actions)
if random.random() <= self.epsilon:
action[random.choice(list(range(self.num_actions)))] = 1
else:
self.dqn_model.eval()
input_image = torch.from_numpy(self.input_image).type(self.FloatTensor)
with torch.no_grad():
preds = self.dqn_model(input_image).cpu().data.numpy()
action[np.argmax(preds)] = 1
self.dqn_model.train()
嗯,大概就是这样,完整源代码详见相关文件呗。填坑成功咯~