#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
флага. Однако код по-прежнему зависает, обычно на этапе, когда сеть вычисляет выходные данные.