问题描述
我最近开始学习人工智能,并观看了 Code Bullet 的 this 视频,该视频展示了一个简单游戏的非常简单的遗传算法,其中点需要达到一个目标。我想使用 pygame 在 python 中重新创建这个游戏。因为它根本不起作用,我尝试重新设计它。 代码如下:
from DotGame.DotGameFunctions import *
import random as r
import time
pg.init()
WIN_WIDTH = 500
WIN_HEIGHT = 500
win = pg.display.set_mode((WIN_WIDTH,WIN_HEIGHT))
pg.display.set_caption('DotGame')
SPAWN = Vector2(WIN_WIDTH / 2,WIN_HEIGHT - 40)
GOAL = Vector2(WIN_WIDTH / 2,10)
class Dot:
def __init__(self,pos: Vector2):
self.pos = pos
self.dead = False
self.reachedGoal = False
self.is_best = False
self.fitness = 0
self.brain = Brain(1000)
self.vel = Vector2()
def move(self):
if self.brain.step < len(self.brain.directions):
self.vel = self.brain.directions[self.brain.step]
self.brain.step += 1
else:
self.dead = True
self.pos = self.pos + self.vel
def update(self):
if not self.dead and not self.reachedGoal:
self.move()
if self.pos.x < 5 or self.pos.y < 5 or self.pos.x > WIN_WIDTH - 5 or self.pos.y > WIN_HEIGHT - 5:
self.dead = True
elif dis(self.pos.x,self.pos.y,GOAL.x,GOAL.y) < 5:
self.reachedGoal = True
if self.is_best:
color = (0,255,0)
width = 7
else:
color = (0,0)
width = 5
if self.fitness > .005:
color = (0,255)
width = 7
pg.draw.circle(win,color,tuple(self.pos),width)
def calculate_fitness(self):
if self.reachedGoal:
self.fitness = 1 / 16 + 10000 / self.brain.step
else:
distance_to_goal = dis(self.pos.x,GOAL.y)
self.fitness = 1 / distance_to_goal
def make_baby(self):
baby = Dot(SPAWN)
baby.brain = self.brain.clone()
return baby
class Goal:
def __init__(self):
self.pos = GOAL
def update(self):
pg.draw.circle(win,(255,0),10)
class Brain:
def __init__(self,size):
self.size = size
self.step = 0
self.directions = []
self.randomize()
self.mutationRate = 1
def randomize(self):
self.directions = []
for i in range(self.size):
self.directions.append(Vector2.from_angle(r.random() * 2 * math.pi) * 4)
def clone(self):
clone = Brain(len(self.directions))
clone.directions = self.directions
return clone
def mutate(self):
for i in range(len(self.directions) - 1):
rand = r.random()
if rand < self.mutationRate:
self.directions[i] = Vector2.from_angle(r.random() * 2 * math.pi) * 4
class Population:
def __init__(self,size):
self.size = size
self.Dots = []
for i in range(size):
self.Dots.append(Dot(SPAWN))
self.minStep = 20
self.gen = 0
self.bestDot = 0
self.fit_sum = 0
def update(self):
for d in self.Dots:
d.update()
def fitness(self):
for d in self.Dots:
d.calculate_fitness()
def end_of_generation(self):
for d in self.Dots:
if not d.dead and not d.reachedGoal:
return False
return True
def natural_selection(self):
self.fitness()
new_dots = sorted(self.Dots,key=lambda x: x.fitness)
new_dots.reverse()
new_dots = new_dots[:len(new_dots)//2] * 2
print([i.fitness for i in new_dots])
self.Dots = [d.make_baby() for d in new_dots]
self.Dots[0].is_best = True
for d in range(len(self.Dots) // 2,len(self.Dots)):
self.Dots[d].brain.mutate()
test = Population(100)
goal = Goal()
run = True
while run:
win.fill('white')
goal.update()
if test.end_of_generation():
test.natural_selection()
time.sleep(1)
else:
test.update()
# for event in pg.event.get():
# if event.type == pg.QUIT:
# run = False
pg.display.update()
time.sleep(0.005)
pg.quit()
这段代码的重要部分是population类中的natural_selection()函数
def natural_selection(self):
self.fitness()
new_dots = sorted(self.Dots,len(self.Dots)):
self.Dots[d].brain.mutate()
它做(或应该做)的是计算点的适应度,按适应度从高到低对点列表进行排序,将列表切成两半并复制它,所以前半部分和后半部分是相同,然后将第一个点设置为最佳点并改变后半部分。 问题在于,正如第 6 行中的打印所示,它并没有真正改变点,结果是在每一代中它只是采用相同的重复列表并对其进行排序,并且适应度如下所示:
[0.003441755148372998,0.0034291486755453414,0.003070574887525978,0.0030339596318951327,0.003030855079397534,...]
[0.00481387362410465,0.00481387362410465,0.003468488512721536,0.0032419920180191478,...]
[0.004356736047656708,0.004356736047656708,0.003056862712974015,...]
我检查了 Brain.mutate 函数,它似乎工作得很好。
我想不出任何可能导致此问题的原因,因为一切似乎都正常。 我希望我足够清楚。
编辑:我导入了一个名为 DotGame.DotGameFunctions 的文件,但忘记显示它:
import math
class Vector2:
def __init__(self,x=0.0,y=0.0):
self.x = x
self.y = y
def __add__(self,other):
return Vector2(self.x + other.x,self.y + other.y)
def __mul__(self,other: int):
return Vector2(self.x * other,self.y * other)
def __iadd__(self,other):
self.x += other.x
self.y += other.y
def __iter__(self):
yield self.x
yield self.y
def copy(self):
return Vector2(self.x,self.y)
@staticmethod
def from_angle(angle):
return Vector2(math.cos(angle),math.sin(angle))
def dis(x1,y1,x2,y2):
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
解决方法
我已经重写了您的 natural_selection
方法,使其更加结构化(至少对我而言),并且它似乎适用于我的简单测试用例。你能试试这对你有用吗?我添加了一些注释来解释我的步骤。
def natural_selection(self):
# Calculate fitness
self.fitness()
# Print initial generation
print(pop.Dots)
# Sort dots by fitness
new_dots = sorted(self.Dots,key=lambda x: x.fitness)
new_dots.reverse()
# Save first half of best individuals
best_half = new_dots[:len(new_dots)//2]
# Get offspring dots from best half
offspring = [d.make_baby() for d in best_half]
# copy best half and mutate all dots
mutated_offspring = [d.make_baby() for d in best_half]
for d in mutated_offspring:
d.brain.mutate()
# Join original best half and mutated best half to get new dots
new_dots = offspring + mutated_offspring
# Set first new dot as best dot
new_dots[0].is_best = True
# Set new_dots as self.Dots
self.Dots = new_dots
# Print new generation
self.fitness()
print(pop.Dots)
完整的测试用例:
import random
# Brains only consist of some random number.
# Mutations just add one to that number.
class Brain:
def __init__(self):
self.num = self.randomize()
def clone(self):
clone = Brain()
clone.num = self.num
return clone
def randomize(self):
return random.randint(1000,9000)
def mutate(self):
self.num += 1
# Dots consist of a brain an a fitness.
# Fitness is simply the brain's number.
class Dot:
def __init__(self):
self.brain = Brain()
self.fitness = None
def __repr__(self):
return str(self.fitness)
def make_baby(self):
baby = Dot()
baby.brain = self.brain.clone()
return baby
class Population:
def __init__(self):
self.Dots = [Dot() for i in range(10)]
def fitness(self):
for d in self.Dots:
d.fitness = d.brain.num
def natural_selection(self):
# Calculate fitness
self.fitness()
# Print initial generation
print(pop.Dots)
# Sort dots by fitness
new_dots = sorted(self.Dots,key=lambda x: x.fitness)
new_dots.reverse()
# Save first half of best individuals
best_half = new_dots[:len(new_dots)//2]
# Get offspring dots from best half
offspring = [d.make_baby() for d in best_half]
# copy best half and mutate all dots
mutated_offspring = [d.make_baby() for d in best_half]
for d in mutated_offspring:
d.brain.mutate()
# Join original best half and mutated best half to get new dots
new_dots = offspring + mutated_offspring
# Set first new dot as best dot
new_dots[0].is_best = True
# Set new_dots as self.Dots
self.Dots = new_dots
# Print new generation
self.fitness()
print(pop.Dots)
random.seed(1)
pop = Population()
pop.natural_selection()
返回:
[2100,5662,7942,7572,7256,1516,3089,1965,5058,7233]
[7942,7233,7943,7573,7257,7234,5663]
所以原始种群适应度打印在第一行。在第二行中,显示了变异的种群适应度。我的突变只是在点脑值上加 1,所以后半部分是前半部分的副本,每个值加 1。