'What is the problem in this genetic algorithm?
I recently started learning AI and i watched this video of Code Bullet, presenting a very simple genetic algorithm of a simple game where dots need to reach to a goal. I wanted to recreate this game in python using pygame
. Since it didn't work at all, I tried redesigning it a bit.
Here is the code:
import pygame as pg
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, 0)
width = 5
if self.fitness > .005:
color = (0, 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, self.pos.y, GOAL.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, 0), tuple(self.pos), 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()
The important part of this code is the natural_selection()
function in the population class:
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()
What it does (or supposed to do) is to calculate the fitness of the dots, sort the list of dots by fitness from highest to lowest, cuts the list in half and duplicating it, so the first and second halves are the same, and then sets the first dot to be the best and mutates the second half. The problem is that as the print in line 6 shows, it doesn't really mutate the dots, and the result is that in every generation it just takes the same repeating list and sorts it, and the fitnesses look like this:
[0.003441755148372998, 0.0034291486755453414, 0.003070574887525978, 0.0030339596318951327, 0.003030855079397534,...]
[0.00481387362410465, 0.00481387362410465, 0.003468488512721536, 0.003468488512721536, 0.0032419920180191478,...]
[0.004356736047656708, 0.004356736047656708, 0.004356736047656708, 0.004356736047656708, 0.003056862712974015,...]
I checked the brain.mutate
function and it seems to work just fine.
Solution 1:[1]
I've rewritten your natural_selection
method to be a little more structured (to me at least), and it seems to work for my simple test case. Can you try if this works for you? I added some comments to explain my steps.
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)
Full test case:
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()
Returns:
[2100, 5662, 7942, 7572, 7256, 1516, 3089, 1965, 5058, 7233]
[7942, 7572, 7256, 7233, 5662, 7943, 7573, 7257, 7234, 5663]
So the original population fitness is printed on the first line. In the second line, the mutated population fitness is shown. My mutations simply add one to the dots brain value, so the second half is a copy of the first half with 1 added to each value.
Solution 2:[2]
Moved from an edit to the question by the OP to an answer.
There is a problem with copying the list in natural_selection
:
from copy import deepcopy
...
def natural_selection(self):
# 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 = deepcopy(offspring)
for d in mutated_offspring:
d.brain.mutate()
# Join original best half and mutated best half to get new dots
new_dots = best_half + 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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | vinzee |