Используя NEAT, могу ли я получить лучший результат для игры змея? - PullRequest
0 голосов
/ 22 марта 2020

Я создал игру змея в 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
...