项目介绍
使用Python实现《Flappy Bird》类,主要包括物理引擎和死亡机制以及像素精度碰撞检测
利用N.E.A.T实现神经网络,通过鸟类的每代繁殖,获得一定阈值的适应度,通过神经网络能计算出模拟场景的解决方案。
使用了以下的技术和工具来完成这个项目:
- MacOS上的Python 3.8.5
- Vim和jupyter notebook
- Python的库
- 其他常见库,如pygame、time、random、os
前期准备
Python的基础知识:包括类、函数和对象等
游戏“Flappy Bird”是如何运作的(理解在这个项目中执行的物理引擎和死亡机制)。
什么是N.E.A.T,它如何工作?
NEAT(NeuroEvolution of Augmenting Topologies.)使用增强拓扑的神经进化。从根本上说,它本质上是一种复制自然界进化的尝试。在这个项目中我们所做的是生成一群随机玩家并对他们进行测试。表现最好的玩家会被送去繁殖,而其他的则会变异。因此,下一代玩家都是最优秀的玩家,这个过程不断重复,直到我们拥有一个能够做我们想做的事情的网络。
在解释NEAT在我们项目中的实现之前,我想先解释一下神经网络是如何工作的。
神经网络是如何工作的?
本质上,神经网络是分层的。第一层是输入层。输入层是传递到我们的网络的信息,也就是网络实际知道和看到的东西。对于我们的项目,我们可以告诉鸟,鸟和顶部/底部管道之间的距离作为输入层。最后一层是输出层,它将告诉网络能它应该做什么。在我们的例子中,它将告诉鸟是否应该跳跃。
数学原理
在处理神经网络时,我们首先将一些值传递给输入层神经元,然后通过连接将它们传递给输出神经元。所有的连接都有所谓的权重,它是一个表示连接强弱的数字。我们会调整这些数字,让我们的人工智能表现得更好。在这之后,我们将计算加权和,也就是说,我们将取每个权重并将其乘以其相应的输入值,然后将它们相加。
然后在加权和的基础上,我们加上两项:
(i)偏差:这个数字可以让我们调整我们的神经网络,让我们上下移动我们的网络,并把它移到可能的最佳位置。
(ii)激活函数:这个函数将帮助我们得到神经元在两个数字范围内的值。这是非常有用的-我们可以告诉A.I跳跃或不跳跃取决于激活函数返回的值(鸟跳跃如果输出> 0在我们的例子中)。对于这个项目,我们将使用TanH作为我们的激活函数。
所以,你可能想知道我们如何为我们的鸟得到正确的权重和偏差?
其实我们不需要。我们只需要告诉计算机为我们做这件事。这就是NEAT发挥作用的地方!(端到端学习)
这里,我们从创造一个完全随机的鸟类种群开始。每只鸟都有不同的神经网络来控制它。这些神经网络都将以随机权值和偏差开始。然后,我们把他们放在同一水平上,看看他们表现如何,并相对调整他们的健康状况来鼓励他们。
适应性本质上是针对所考虑问题的解决方案的“适合”程度或“良好”程度,并且因任务而异。在我们的案例中,我们将增加成功通过管道的鸟的适应度,并降低失败的鸟的适应度。为了鼓励鸟类存活和移动,如果它在不死的情况下向前移动,我们还将提高其适应性。
最后,在模拟结束时,表现最好的鸟类将被繁殖,形成一个新的种群世代+=1。这一代的鸟会表现得更好,这样的循环会持续下去,直到获得所需的适合度。在输入层和输出层之间还有n个隐藏层。隐藏层通过发现输入特性之间的关系来捕获越来越多的复杂性。
总而言之,我们使用的是“前馈”神经网络,其中输入乘以权重,求和,然后通过非线性选择方程,即激活函数Sigmoid,ReLU,TanH。
(隐藏层将非线性应用于神经网络的输入,并且堆叠在一起的隐藏层越多,就可以建模的功能越复杂。)
实现Flappy Bird并添加NEAT
首先,导入必要的库。由于在线上找到的所有图像都很小,因此我们将使用pygame.transform.scale2x()将它们的大小相乘。这是代码:
import os
import neat
import time
import random
import pygame
pygame.font.init()
pygame.display.set_caption('Flappy Bird AI')
window_font = pygame.font.SysFont("arial", 18)
flappy_font = pygame.font.Font("assets/flappy-font.ttf", 50)
window_width = 500
window_height = 800
generations = 0
all_bird_imgs = [pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird1.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join(
"assets", "bird2.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird3.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird4.png")))]
img_pipe = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "pipe.png")))
img_base = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "base.png")))
img_background = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "background.png")))
现在,我们所有的资产都已加载,我们将开始使用Bird()类:在此类中,我们将定义以下方法:
rip_animation(self):如果鸟撞到管道,将调用此方法。鸟会垂直向下,直到撞到地面。
move(self):为了使事情变得清楚,鸟实际上不必向前移动,而只是上下移动,因为管道和地面都在移动。在这里,我们将创建一种物理机制,根据经过的时间调整鸟类从y轴的位移。
jump(self):我们将在此处为鸟添加-纵向速度,因为pygame窗口的左上角具有坐标(0,0),而右下角具有坐标(500,800)。因此,要使鸟向上运动,我们将不得不减小其y坐标。
draw(self,win):在这种方法中,我们将根据鸟的前进方向倾斜它。我们需要做的另一件事是告诉小鸟如果它掉下来了,不要播放拍打的图像,因为那看起来很愚蠢。
get_mask(self):这里我们将使用内置的pygame函数来屏蔽鸟类的每个像素,因为我们希望获得像素完美的碰撞。
您也可能对如何实现鸟类的速度感到困惑。好吧,为此,解决方案是将速度设置为某个值,并使用它来实现公式以计算位移d。然后以60fps的速度运行游戏,我们每秒调用main()60次,这还将调用包含位移的方法,因此,每帧(1/60秒)将鸟移动一定距离d。这将使其以一定速度运动时看起来非常平滑。这是代码:
"""
Code for the Bird Class
"""
class Bird():
animation_imgs = all_bird_imgs
# while the bird moves up and down
maximum_rotation = 24
# velocity with which we rotate the bird
rotation_velocity = 18
# flap animation duration
flap_animation = 8
def __init__(self, x, y, rip):
# x, y are the starting coordinates, rip (rest in peace) is the boolean value that checks if the bird is alive or not
self.x = x
self.y = y
self.rip = rip
self.tilt = 0
self.ticks = 0
self.vel = 0
self.height = self.y
self.img_count = 0
self.img = self.animation_imgs[0]
def rip_animation(self):
# positive velocity will make it go in down as the y coordinate of the pygame window increases as we go down
self.vel = 10
self.ticks = 0
# if bird is rip (by hitting a pipe) then it will turn the bird red and move it to the ground where we will remove it from the list of birds
self.height = window_height
def move(self):
self.ticks = self.ticks + 1
# d stands for displacement
d = self.vel * (self.ticks) + 1.5 * self.ticks**2
if d >= 14:
d = 14
if d < 0:
d -= 2
self.y = self.y + d
if d < 0 or self.y < self.height + 50:
if self.tilt < self.maximum_rotation:
self.tilt = self.maximum_rotation
else:
if self.tilt > -90:
self.tilt -= self.rotation_velocity
def jump(self):
# since top left corner of pygame window has coordinates (0,0), so to go upwards we need negative velocity
self.vel = -10
self.ticks = 0
self.height = self.y
def draw(self, win):
# img_count will represent how many times have we already shown image
self.img_count = self.img_count + 1
# condition to check if the bird is alive
if self.rip == False:
# checking what image of the bird we should show based on the current image count
if self.img_count <= self.flap_animation:
self.img = self.animation_imgs[0]
elif self.img_count <= self.flap_animation * 2:
self.img = self.animation_imgs[1]
elif self.img_count <= self.flap_animation * 3:
self.img = self.animation_imgs[2]
elif self.img_count <= self.flap_animation * 4:
self.img = self.animation_imgs[1]
elif self.img_count == self.flap_animation * 4 + 1:
self.img = self.animation_imgs[0]
self.img_count = 0
# this will prevent flapping of the birds wings while going down
if self.tilt <= -80:
self.img = self.animation_imgs[1]
self.img_count = self.flap_animation * 2
# condition if the bird is rip
elif self.rip == True:
self.tilt = -90
self.img = self.animation_imgs[3]
self.rip_animation()
# to rotate image in pygame
rotated_image = pygame.transform.rotate(self.img, self.tilt)
new_rect = rotated_image.get_rect(
center=self.img.get_rect(topleft=(self.x, self.y)).center)
# blit means draw, here we will draw the bird depending upon its tilt
win.blit(rotated_image, new_rect.topleft)
# since we want pixel perfect collision, and not just have a border around the bird, we mask the bird
def get_mask(self):
return pygame.mask.from_surface(self.img)
下一个类是Pipe():在该类中,我们将定义以下方法:
"""
Code for the Pipe Class
"""
class Pipe:
gap = 220
# velocity of pipes, since the pipes move and the bird does not
vel = 4
def __init__(self, x):
# x because the pipes are going to be random
self.x = x
self.height = 0
# creating varibles to keep track of where the top and bottom of the pipe are going to be drawn
self.top = 0
self.bottom = 0
self.top_pipe = pygame.transform.flip(img_pipe, False, True)
self.bottom_pipe = img_pipe
# if the bird has passed the pipe
self.passed = False
# this method will show where our pipes are and what the gap between them is
self.set_height()
def set_height(self):
# randomizes the placement of pipes
self.height = random.randrange(40, 450)
self.top = self.height - self.top_pipe.get_height()
self.bottom = self.height + self.gap
def move(self):
self.x = self.x - self.vel
def draw(self, win):
win.blit(self.top_pipe, (self.x, self.top))
win.blit(self.bottom_pipe, (self.x, self.bottom))
def collide(self, bird, win):
bird_mask = bird.get_mask()
top_mask = pygame.mask.from_surface(self.top_pipe)
bottom_mask = pygame.mask.from_surface(self.bottom_pipe)
top_offset = (self.x - bird.x, self.top - round(bird.y))
bottom_offset = (self.x - bird.x, self.bottom - round(bird.y))
# finding the point of collision
bottom_point = bird_mask.overlap(bottom_mask, bottom_offset)
top_point = bird_mask.overlap(top_mask, top_offset)
if top_point or bottom_point:
return True
return False
下一个类是Base()。我们将使基底看起来是无限移动的。但在理论上,我们实际上是在循环两张照片(一张在另一张后面)。查看下面解释这一点的图表。
我们将在这个类中定义的方法是:
move(self):这个方法将每帧图像向左移动一段距离。然后,当第一张图像完全离开屏幕时,它很快就会转到第二张图像后面,这样循环直到终止。
draw(self, win):在窗口上绘制两个基础图像。
"""
Code for the Base Class
"""
class Base():
vel = 5
width = img_base.get_width()
img = img_base
def __init__(self, y):
self.y = y
self.x1 = 0
self.x2 = self.width
def move(self):
self.x1 -= self.vel
self.x2 -= self.vel
if self.x1 + self.width < 0:
self.x1 = self.x2 + self.width
if self.x2 + self.width < 0:
self.x2 = self.x1 + self.width
def draw(self, win):
win.blit(self.img, (self.x1, self.y))
win.blit(self.img, (self.x2, self.y))
下一步是在窗口上绘制所有内容,如所有管道、分数、其他数据等。这里要做的重要的事情是确保你是在类之外写的,也就是说,从零缩进开始。
"""
Code to Render Pipes, Birds, Stats, etc. on the Window
"""
def draw_window(win, birds, pipes, base, score, gen, pipe_number, fitness):
if gen == 0:
gen = 1
win.blit(img_background, (0, 0))
for pipe in pipes:
pipe.draw(win)
# showing all the text labels
score_text = flappy_font.render(str(score), 0, (255, 255, 255))
win.blit(score_text, (window_width - score_text.get_width() - 230, 100))
# showing the generation number
gen_text = window_font.render(
"Species Generation Num: " + str(gen), 1, (0, 0, 0))
win.blit(gen_text, (10, 5))
# showing the number of birds that are alive in the provided frame
alive_text = window_font.render("Alive: " + str(len(birds)), 1, (0, 0, 0))
win.blit(alive_text, (10, 25))
# showing the total number of birds that have been mutated in the current frame
mutated_text = window_font.render(
"Mutated: " + str(15 - len(birds)), 1, (231, 84, 128))
win.blit(mutated_text, (10, 45))
# showing the fitness value of the birds
fitness_text = window_font.render(
"Fitness: " + str(fitness), 1, (0, 255, 0))
win.blit(fitness_text, (10, 65))
# showing the fitness threshold that should be reached before automatically terminating the program
fitness_t_text = window_font.render(
"Fitness Threshold: 1000", 1, (0, 0, 0))
win.blit(fitness_t_text, (window_width - fitness_t_text.get_width() - 10, 5))
# showing the population of the birds that will be bred every generation
population_text = window_font.render("Population: 15", 1, (0, 0, 0))
win.blit(population_text, (window_width -
population_text.get_width() - 10, 25))
base.draw(win)
for bird in birds:
bird.draw(win)
pygame.display.update()
现在是时候编写main()了。它也将充当我们项目的适应度函数。在此,我们将执行以下操作:
使用配置文件(在main之后导入)为基因组设置FeedForward神经网络。
然后,我们将放置初始管道和基座,并将时钟设置为每秒60次重复运行。
我们的下一个目标是确保鸟儿看着它前面的管道,而不是已经通过的管道。
然后,如果神经网络返回的输出> 0,我们将指示飞鸟跳跃。如果飞鸟进入管道之间或在给定帧中还活着,我们将提高飞鸟的适应度(这将鼓励它保持生命并振翅高飞)
如果一只鸟确实撞到了管道,我们将降低其适应性(这样它就不会繁殖形成下一代)并将该鸟的“ rip”属性设置为True,这会将鸟变成红色并触发rip_animation使其摔倒在地上。
每当鸟撞到地面或试图通过在屏幕上方越过管道来欺骗系统时,我们都会使用pop()函数将其从鸟列表中删除(还要确保弹出与之相关的网络和基因组在弹出实际的鸟之前那只鸟)。
"""
Code for main()
This will also behave as a fitness function for the program
"""
# the main() will also act as a fitness function for the program
def main(genomes, config):
global generations
generations += 1
nets = []
ge = []
birds = []
# setting neural network for genome, using _, g as 'genomes' is a tuple that has the genome id as well as the genome object
for _, g in genomes:
net = neat.nn.FeedForwardNetwork.create(g, config)
nets.append(net)
birds.append(Bird(210, 320, rip=False))
g.fitness = 0
ge.append(g)
base = Base(730)
# first pipe will be a little away that other pipes so the birds knows that they can gain fitness by staying alive
pipes = [Pipe(600)]
win = pygame.display.set_mode((window_width, window_height))
clock = pygame.time.Clock()
score = 0
run = True
while run and len(birds) > 0:
clock.tick(60)
# keeps track if something happens like whenever user clicks keys etc.
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# quits the program when user hits the cross
pygame.quit()
quit()
# making sure the bird looks only at first pipe and not the second pipe, since there can be multiple pipes generated
pipe_number = 0
if len(birds) > 0:
# if we pass the first pipe, then look at the next pipe
if len(pipes) > 1 and birds[0].x > pipes[0].x + pipes[0].top_pipe.get_width():
pipe_number = 1
for bird in birds:
bird.move()
# adding fitness since it has come to this level, also giving such little fitness value because the for loop will run 60 times a second so every second our bird stays alive, it will give it some fitness point, so this encourages the bird to stay alive
ge[birds.index(bird)].fitness += 0.2
output = nets[birds.index(bird)].activate((bird.y, abs(
bird.y - pipes[pipe_number].height), abs(bird.y - pipes[pipe_number].bottom)))
if output[0] > 0:
bird.jump()
base.move()
# list to remove pipes
add_pipe = False
rem = []
for pipe in pipes:
pipe.move()
# checking for collision
for bird in birds:
if pipe.collide(bird, win):
# every time a bird hits a pipe, we will reduce its score, so we are encouraging the bird to go between the pipes
ge[birds.index(bird)].fitness -= 1
birds[birds.index(bird)].rip = True
# no need to add the below lines to delete the bird as, on hitting a pipe, we play the rip animation which turns the bird red and moves it to the ground and we already will pop the bird if it hits the ground so we dont need this.
# nets.pop(birds.index(bird))
# ge.pop((birds.index(bird)))
# birds.pop(birds.index(bird))
# this is checking if our pipe is off the screen so we can generate another pipe
if pipe.x + pipe.top_pipe.get_width() < 0:
rem.append(pipe)
if not pipe.passed and pipe.x < bird.x:
pipe.passed = True
add_pipe = True
if add_pipe:
score += 1
for g in ge:
g.fitness += 4
# distance between pipes
pipes.append(Pipe(550))
for r in rem:
pipes.remove(r)
# checks if the bird hits the ground or goes above the screen and avoids the pipe
for bird in birds:
if bird.y + bird.img.get_height() - 10 >= 730 or bird.y < -50:
# mutate the bird if it hits the ground
nets.pop(birds.index(bird))
ge.pop((birds.index(bird)))
birds.pop(birds.index(bird))
draw_window(win, birds, pipes, base, score,
generations, pipe_number, g.fitness)
最后,我们将导入位于'assets / flappy-config.txt'的NEAT配置文件。该文件包含A.I.的调整和值。 配置文件采用Python ConfigParser文档中描述的格式。当前,必须在配置文件中明确枚举所有值。这使得代码更改不太可能导致使用不同的NEAT设置而导致您的项目无法运行。所以一定要注意
设置“neat.StdOutReporter(True)”将为我们提供终端的详细统计信息。例如以下显示:
"""
Code for importing and running the config file located in the assets directory
"""
def run(config_path):
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation, config_path)
p = neat.Population(config)
# now we will add stats reporters which will give us some outputs in the console where we will see the detailed statistics of each generation and the fitness etc.
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
winner = p.run(main, 100)
if __name__ == "__main__":
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, "assets/flappy-config.txt")
run(config_path)
现在我们可以运行我们的项目了
感谢您的阅读,完整代码在这里:http://github.com/yaashwardhan
作者:Yashwardhan Deshmukh
deephub翻译组