Я создал игру змея в python и использую NEAT, чтобы создать нейронную сеть для игры. Я потратил много времени на то, чтобы поиграться с файлом конфигурации и функциями фитнеса, но средняя пригодность не увеличивается. Я был бы очень рад, если бы кто-нибудь мог дать какой-нибудь совет.
Я приложил файл python для игры, а также файл конфигурации NEAT, который я использовал, и прокомментировал код для удобства чтения.
import os
import random
import pygame
import neat
from scipy.spatial import distance
gen = 0
snakes = []
snacks = []
rows = 20
def draw_grid(w, surface):
size_btwn = w // rows
x, y = 0, 0
for l in range(rows):
x = x + size_btwn
y = y + size_btwn
pygame.draw.line(surface, (255, 255, 255), (x, 0), (x, w))
pygame.draw.line(surface, (255, 255, 255), (0, y), (w, y))
def redraw_window(surface):
global rows, snakes, snacks
surface.fill((0, 0, 0))
for i, s1 in enumerate(snakes):
s1.draw(surface)
s1.snack.draw(surface)
draw_grid(width, surface)
pygame.display.update()
class Cube:
rows = 20
w = 500
def __init__(self, position, color=(255, 0, 0)):
self.pos = position
self.color = color
def draw(self, surface):
dis = self.w // self.rows
i = self.pos[0]
j = self.pos[1]
pygame.draw.rect(surface, self.color, (i * dis + 1, j * dis + 1, dis - 2, dis - 2))
class Snake:
def __init__(self, pos):
self.head = Cube(pos)
self.body = []
self.body.append(self.head)
self.dirnx = 0
self.dirny = 1
self.added_cube = False
self.snack = Cube(randomSnack(rows), color=(0, 255, 0))
self.time = 50
def change_dir(self, direction_x, direction_y):
self.dirnx = direction_x
self.dirny = direction_y
def move(self):
self.head = self.body[-1]
# new_x, new_y = (self.head.pos[0] + self.dirnx) % rows, (self.head.pos[1] + self.dirny) % rows # add this to stop death from wall hit
new_x, new_y = (self.head.pos[0] + self.dirnx), (self.head.pos[1] + self.dirny)
c1 = Cube([new_x, new_y])
self.body.append(c1)
if not self.added_cube:
del self.body[0]
self.added_cube = False
def add_cube(self):
self.added_cube = True
def draw(self, surface):
for i, cube in enumerate(self.body):
cube.draw(surface)
def randomSnack(rows):
x = random.randrange(rows)
y = random.randrange(rows)
return [x, y]
def check_dir_changed():
for event in pygame.event.get():
keys = pygame.key.get_pressed()
for snake in snakes:
for key in keys:
if keys[pygame.K_LEFT]:
snake.change_dir(-1, 0)
elif keys[pygame.K_RIGHT]:
snake.change_dir(1, 0)
elif keys[pygame.K_UP]:
snake.change_dir(0, -1)
elif keys[pygame.K_DOWN]:
snake.change_dir(0, 1)
def eval_genomes(genomes, config):
global width, rows, snakes, snacks, gen
gen += 1
width = 500
rows = 20
win = pygame.display.set_mode((width, width))
nets = []
snakes = []
snacks = []
ge = []
for genome_id, genome in genomes:
genome.fitness = 1 # start with fitness level of 1
net = neat.nn.FeedForwardNetwork.create(genome, config)
nets.append(net)
start_position_x, start_position_y = random.randrange(0, 20), random.randrange(0, 20)
snakes.append(Snake([start_position_x, start_position_y]))
ge.append(genome)
clock = pygame.time.Clock()
while True and len(snakes) > 0:
pygame.time.delay(50)
clock.tick(10)
check_dir_changed()
for i, snake in enumerate(snakes):
# send info and determine from network what direction to go
output = nets[snakes.index(snake)].activate(
(snake.dirnx, snake.dirny, snake.snack.pos[0], snake.snack.pos[1],
snake.head.pos[0], snake.head.pos[1], distance.euclidean(snake.snack.pos, snake.head.pos),
distance.euclidean(snake.snack.pos[0], snake.head.pos[0]),
distance.euclidean(snake.snack.pos[1], snake.head.pos[1])))
# get the right move to make
max_output = -2
best_output = 0
for j, out in enumerate(output):
if out > max_output:
max_output = out
best_output = j
# 0 is right, 1 is left, 2 is down, 3 is up
if best_output == 0:
snake.change_dir(1, 0)
elif best_output == 1:
snake.change_dir(-1, 0)
elif best_output == 2:
snake.change_dir(0, 1)
elif best_output == 3:
snake.change_dir(0, -1)
snake.move()
# take 1 from the current snake's time, this stops snakes running around forever
snake.time -= 1
# add fitness depending how close snake is to the snack
ge[snakes.index(snake)].fitness += 20 - distance.euclidean(snake.snack.pos, snake.head.pos)
# if snake head eats snack
if snake.body[-1].pos == snake.snack.pos:
ge[snakes.index(snake)].fitness += 1000
snake.time += 40 # give snake more time since they got a snack
snake.add_cube()
snake.snack = Cube(randomSnack(rows), color=(0, 255, 0))
# if snake ran out of time without getting snack
if snake.time < 1:
ge[snakes.index(snake)].fitness -= 5
nets.pop(snakes.index(snake))
ge.pop(snakes.index(snake))
snakes.pop(snakes.index(snake))
break
# if snake hits a wall
if snake.body[-1].pos[0] > 20 or snake.body[-1].pos[0] < 0 or snake.body[-1].pos[1] > 20 or snake.body[-1].pos[1] < 0:
ge[snakes.index(snake)].fitness -= 1000
nets.pop(snakes.index(snake))
ge.pop(snakes.index(snake))
snakes.pop(snakes.index(snake))
break
# if snake eats itself
for x in range(len(snake.body) - 1):
if snake.body[x].pos == snake.body[-1].pos:
ge[snakes.index(snake)].fitness -= 10
nets.pop(snakes.index(snake))
ge.pop(snakes.index(snake))
snakes.pop(snakes.index(snake))
print('Score: ', len(snake.body))
break
redraw_window(win)
pass
def run(config_file):
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_file)
# Create the population, which is the top-level object for a NEAT run.
p = neat.Population(config)
# Add a stdout reporter to show progress in the terminal.
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
# p.add_reporter(neat.Checkpointer(5))
# Run for up to 50 generations.
winner = p.run(eval_genomes, 100)
# show final stats
print('\nBest genome:\n{!s}'.format(winner))
if __name__ == '__main__':
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, 'config-feedforward.txt')
run(config_path)
[NEAT]
fitness_criterion = max
fitness_threshold = 10000000
pop_size = 20
reset_on_extinction = True
[DefaultGenome]
# node activation options
activation_default = sigmoid
activation_mutate_rate = 0.05
activation_options = sigmoid gauss
#abs clamped cube exp gauss hat identity inv log relu sigmoid sin softplus square tanh
# node aggregation options
aggregation_default = random
aggregation_mutate_rate = 0.05
aggregation_options = sum product min max mean median maxabs
# node bias options
bias_init_mean = 0.05
bias_init_stdev = 1.0
bias_max_value = 30.0
bias_min_value = -30.0
bias_mutate_power = 0.5
bias_mutate_rate = 0.7
bias_replace_rate = 0.1
# genome compatibility options
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.5
# connection add/remove rates
conn_add_prob = 0.5
conn_delete_prob = 0.5
# connection enable options
enabled_default = True
enabled_mutate_rate = 0.5
feed_forward = False
#initial_connection = unconnected
initial_connection = partial_nodirect 0.5
# node add/remove rates
node_add_prob = 0.5
node_delete_prob = 0.2
# network parameters
num_hidden = 0
num_inputs = 9
num_outputs = 4
# node response options
response_init_mean = 1.0
response_init_stdev = 0.05
response_max_value = 30.0
response_min_value = -30.0
response_mutate_power = 0.1
response_mutate_rate = 0.75
response_replace_rate = 0.1
# connection weight options
weight_init_mean = 0.1
weight_init_stdev = 1.0
weight_max_value = 30
weight_min_value = -30
weight_mutate_power = 0.5
weight_mutate_rate = 0.8
weight_replace_rate = 0.1
[DefaultSpeciesSet]
compatibility_threshold = 2.5
[DefaultStagnation]
species_fitness_func = max
max_stagnation = 50
species_elitism = 0
[DefaultReproduction]
elitism = 3
survival_threshold = 0.3