Генетический алгоритм для Snake, не сходящийся

#python #machine-learning #genetic-algorithm

#python #машинное обучение #genetic-алгоритм

Вопрос:

Я пытаюсь обучить ИИ играть в snake с помощью генетического алгоритма. Я использую библиотеку Python NEAT для обучения. Проблема в том, что обучение не сходится, и ИИ не обучается. Вот обучающий код:

 class SnakeEnv():

def __init__(self, screen):
    self.action_space = np.array([0, 1, 2, 3])
    self.state = None
    pygame.init()
    self.screen = screen
    self.snakes = [] 
    self.total_reward = 0

def reset(self):
    self.__init__()

    
def get_state(self):
    return np.reshape(self.snake.board, (400, 1)).T / 5

def render(self, snake):
    self.screen.fill((0, 0, 0))
    snake.food.render()
    snake.render()
    pygame.display.flip()

def step(self, snake, action):
    snake.move(action)
    self.render(snake)

def close(self):
    pygame.quit()


def eval_genomes(self, genomes, config):
    global nets_g
    nets_g = []
    nets = []
    snakes = []
    global ge_g
    ge_g = []
    ge = []
    for genome_id, genome in genomes:
        genome.fitness = 0
        net = neat.nn.FeedForwardNetwork.create(genome, config)
        nets.append(net)
        snakes.append(Snake(self.screen))
        ge.append(genome)
    
    ge_g = ge.copy()
    nets_g = nets.copy()
    run = True
    #Main loop
    while run:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                pygame.quit()
                quit()
                break

        for x, snake in enumerate(snakes):
            if(snake.done):
                continue
            ge[x].fitness  = 0.1

            """
            Inputs to the neural net:
            Vertical distance from food to head
            Horizontal distance from food to head
            Vertical distance to nearest wall from head
            Horizontal distance to nearest wall from head
            Distance from head to body segment (default -1)
            """

            snake_x = snake.head.x
            snake_y = snake.head.y
            food_x = snake.food.x 
            food_y = snake.food.y 

            food_vert = snake_y - food_y
            food_horz = snake_x - food_x
            wall_vert = min(snake_y, 600 - snake_y)
            wall_horz = min(snake_x, 600 - snake_x)
            body_front = snake.body_front()
            output = np.argmax(nets[snakes.index(snake)].activate((food_vert, food_horz, wall_vert, wall_horz, body_front)))
            state = snake.move(output)
            if state["Food"] == True:
                ge[snakes.index(snake)].fitness  = 1

            if state["Died"] == True:
                ge[snakes.index(snake)].fitness -= 1
                #nets.pop(snakes.index(snake))
                #ge.pop(snakes.index(snake))
                #snakes.pop(snakes.index(snake))
            all_done = [snake.done for snake in snakes]
            if(False not in all_done):
                run = False


def run(self, config_file):
    config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, config_file)
    population = neat.Population(config)
    population.add_reporter(neat.StdOutReporter(True))
    stats = neat.StatisticsReporter()
    population.add_reporter(stats)
    best = population.run(self.eval_genomes, 200)
    print('nBest genome:n{!s}'.format(best))
    best_net = nets_g[ge_g.index(best)]
    pickle.dump(best_net, open('best.pkl', 'wb'))
  

(Представьте, что в моем коде есть отступ, редактор по какой-то причине не работает)
Вот conf.txt файл:

 [NEAT]
fitness_criterion     = max
fitness_threshold     = 20
pop_size              = 50
reset_on_extinction   = False

[DefaultGenome]
# node activation options
activation_default      = relu
activation_mutate_rate  = 0.0
activation_options      = relu

# node aggregation options
aggregation_default     = sum
aggregation_mutate_rate = 0.0
aggregation_options     = sum

# node bias options
bias_init_mean          = 0.0
bias_init_stdev         = 1.0
bias_max_value          = 10.0
bias_min_value          = -10.0
bias_mutate_power       = 0.5
bias_mutate_rate        = 0.9
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.7
conn_delete_prob        = 0.7

# connection enable options
enabled_default         = True
enabled_mutate_rate     = 0.01

feed_forward            = True
initial_connection      = full

# node add/remove rates
node_add_prob           = 0.7
node_delete_prob        = 0.7

# network parameters
num_hidden              = 0
num_inputs              = 5
num_outputs             = 4

# node response options
response_init_mean      = 1.0
response_init_stdev     = 0.0
response_max_value      = 30.0
response_min_value      = -30.0
response_mutate_power   = 0.0
response_mutate_rate    = 0.0
response_replace_rate   = 0.0

# connection weight options
weight_init_mean        = 0.0
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 = 3.0

[DefaultStagnation]
species_fitness_func = max
max_stagnation       = 20
species_elitism      = 2

[DefaultReproduction]
elitism            = 2
survival_threshold = 0.2
  

Как вы можете видеть, я тренируюсь в течение 200 поколений. Результаты довольно странные. Змея последовательно получает один кусочек пищи, но затем сразу же натыкается на стену. Это своего рода обучение, но не полностью. Я пытался позволить ему обучаться для большего количества поколений, но нет никакой разницы. Я думаю, что проблема может быть в моих входных данных для нейронных сетей, но я не уверен.

РЕДАКТИРОВАТЬ: я изменил сетевую архитектуру так, что теперь она имеет 4 выходных узла с relu активацией. Проблема теперь в том, что код зависает на шаге, на котором вычисляется вывод ( output = np.argmax(nets[snakes.index(snake)].activate((food_vert, food_horz, wall_vert, wall_horz, body_front))) )

Ответ №1:

Просматривая ваш код, у вас, похоже, есть некоторые ошибки:

 for x, snake in enumerate(snakes):
    ge[x].fitness  = 0.1
  

В for цикле вы pop() вводите элементы как из snakes , так и из ge списков. В Python вы никогда не должны изменять список во время итерации по нему. Позже в цикле вы используете snakes.index(snake) insted of x для индексации того же списка. Из-за этого награда за то, что вы остались в живых, вероятно, достанется не тем змеям.

Вы могли бы скопировать список перед итерацией, но повторение snakes.index(snake) везде также является антишаблоном. Вам нужно найти другое решение. Например, вы могли бы использовать snake.dead флаг.

Форма вывода

Похоже, вы масштабируете выходные данные одного нейрона в целочисленный диапазон. Это делает задачу немного сложной (но не невозможной) для решения для NN, потому что близко расположенные числа фактически не сопоставляются с аналогичными действиями.

Более распространенным подходом было бы использовать отдельный нейрон для каждого вывода и выбирать действие с наибольшей активацией. (Или использовать softmax для выбора действия со случайными вероятностями. Это добавляет шума, но делает фитнес-ландшафт намного более плавным, потому что тогда даже небольшие изменения в весах будут оказывать некоторое влияние на фитнес.)

Общие рекомендации

Вы не можете ожидать, что напишете код без ошибок. Когда ваш код является частью цикла оптимизации, отладка становится особенно сложной, потому что оптимизация изменяет эффект ошибок.

Сначала запустите свой код в более простых настройках. Например, вы могли бы игнорировать выходные данные нейронной сети и всегда выполнять одно и то же действие (или случайные действия) вместо этого. Подумайте о том, что должно произойти. Возможно, отследите несколько змей и их вознаграждение вручную, шаг за шагом, например, с помощью инструкций печати.

Суть в том, чтобы уменьшить количество вещей, которые вы отлаживаете одновременно.

Комментарии:

1. Спасибо за информацию. Я изменил его так, что нейронная сеть теперь имеет 4 выходных узла с relu активацией. Однако обучение теперь зависает через несколько поколений. Он почти всегда зависает на строке, где нейронная сеть вычисляет выходные данные.

2. Кроме того, я должен удалить snakes из списка — не работает NEAT, пока размер популяции не равен 0?

3. NEAT имеет постоянную популяцию для каждого поколения (вы настроили ее на 50). Список, из которого вы удаляете, является локальной переменной в eval_genomes() , поэтому он используется только во время оценки пригодности и после этого к нему нельзя получить доступ.

4. Ну, некоторые из ваших списков есть global , но все же они не принадлежат NEAT-python и не передаются обратно, они предназначены только global для печати конечного значения.

5. Просто изменил код, так что теперь змеи не удаляются из списка, а просто пропускаются, когда они закончены, что я отслеживаю с помощью snake.done флага. Однако код по-прежнему зависает, обычно на этапе, когда сеть вычисляет выходные данные.