Давно хотел поиграть с OpenAi Gym. Если вы не слышали об этом, это аркадная игровая площадка с играми и средами, которые были аккуратно упакованы в библиотеку Python, чтобы сделать доступными среды обучения с подкреплением для таких людей, как вы и я. Существуют десятки игр, начиная от простых задач по раскачиванию маятника и заканчивая Atari pong и даже симуляторами ходьбы гуманоидов. Установить библиотеку очень просто.

Что не так, так это понимание RL и методов, которые люди используют для решения этих проблем в наши дни. Я потратил много времени на просмотр видео и чтение статей, чтобы на высоком уровне понять различные варианты методов, но, поскольку я не применил ничего из этого в приложении, я чувствую, что мои знания покидают меня. так же быстро, как и входит. Хотел бы я заморозить свои веса ... Я, вероятно, мог бы объяснить это недостаточным усилием, но когда я узнал об эволюционных алгоритмах, я сразу же оживился.

В отличие от сетей политик, Q-сетей, всевозможных сетей эволюционные алгоритмы кажутся интуитивно понятными. Мы все усвоили идею эволюции, основанную на генетической мутации и выживании наиболее приспособленных, и кодирование чего-то для подражания казалось забавным. Хотя цель игры одинакова для RL и ES (эволюционные стратегии, как их называют в большинстве статей), модели разрабатываются совершенно по-разному. Я сделаю все возможное, чтобы дать интуитивный обзор обоих, прежде чем погрузиться в свой код эволюционного алгоритма. Я ни в коем случае не являюсь экспертом ни в одном из них, поэтому, пожалуйста, примите это как введение в идеи, лежащие в основе обоих.

Агенты

В основе обучения с подкреплением лежит идея агентов. Агенты такие же, как ты или я. Они представляют собой единое целое, которое методом проб и ошибок учится использовать свои действия для получения награды. Определяя функцию вознаграждения и приписывая вознаграждение действию или действиям, предпринятым агентом, агент имеет возможность узнать, какие действия приводят к вознаграждению. Одна из моих любимых частей RL - это центральная концепция, называемая «исследование против эксплуатации». А поскольку людей можно рассматривать как сложных агентов в нашей собственной маленькой земной среде, эта концепция не только невероятно проста и красива, но и актуальна как вне RL, так и внутри.

В основе этого лежит вопрос о том, что является более ценным в данный момент. Изучение неизвестного? Или используя известное?

Для людей исследование может быть таким же широким, как выполнение различных работ во время кооператива и уроков в университете, или может быть столь же узким, как использование нового веб-браузера, нового ресторана или даже новой стрижки. Каждый раз, когда мы решаем исследовать, мы открываемся возможности увеличить или уменьшить нашу текущую награду. Может быть, Mozilla действительно лучше Chrome. (Я бы не знал, потому что я эксплуатирую дерьмо из Chrome) Или, может быть, новые Halal Guys на улице не лучше, чем Chicken and Rice Guys. Но если все, что мы будем делать, это исследовать, мы будем сдерживаться от прогресса, известного как банк, который также зарабатывает вознаграждения. Итак, мы определяем наши предпочтения и используем их. Мы всегда ходим домой одним и тем же маршрутом или всегда едем в одно и то же место в отпуск, потому что мы знаем награду и довольны ею. Некоторые эксплуатируют больше, чем другие, и, возможно, эксплуатация некоторых людей заключается в том, чтобы исследовать больше. Кто знает. Это сложный мир. Все это, чтобы сказать, есть компромисс между двумя, и каждый имеет свои собственные допуски для каждого.

Итак, как агент контролирует свою терпимость к исследованию и эксплуатации? Это проистекает из стохастической природы принятия решений агентом, контролируемой переменной во времени, называемой эпсилон. У агента обычно есть функция ожидаемого вознаграждения, которая моделирует действия, которые он должен предпринять в любой момент времени. И хотя он постоянно обновляется, чтобы отразить полученный опыт, если всегда следовать ему, он не допускает случайных актов исследования. Имея большое значение эпсилон, вы в основном говорите, что в 50% (полностью произвольно) случаях, когда агент принимает решение, отменяет «следующее лучшее действие» на основе моей ожидаемой функции вознаграждения и выполняет полностью случайное действие. По мере того, как агент учится и награда становится больше, эпсилон уменьшается, чтобы большую часть времени выполнять «следующее лучшее действие».

Как и в жизни, это последовательный процесс. Агент должен «пережить» опыт, чтобы обновить свою модель ожидаемого вознаграждения с помощью обратного распространения. Поэтому исследователи разработали ряд уловок, чтобы ускорить этот процесс. Один из способов - сохранить переживания и воспроизводить воспоминания снова и снова. Другой ... не знаю! Как правило, это сети глубокого обучения, для которых требуются мощные графические процессоры и часы вычислений.

Поколения

Поскольку это соответствует теме людей как агентов, я буду называть группы индивидуумов в рамках эволюционных алгоритмов поколениями. Вместо проверки на микроуровне решений и действий отдельной сущности в RL, ES имитирует макроуровень жизни, создавая поколения людей. Он создает единое поколение, скажем, из 10 человек со случайно сгенерированными весами, оценивает их всех, а затем воспроизводит лучших исполнителей и их мутировавшие версии для создания следующего поколения. За счет многократного прохождения этого процесса отдельные элементы алгоритма эволюционируют.

ES уравновешивает разведку и эксплуатацию совершенно иначе, чем RL. Хотя вы можете рассматривать мутировавших лучших исполнителей как постоянный исследовательский метод, каждое последующее поколение сохраняет предыдущих лучших исполнителей, эффективно используя прогресс последнего поколения. Если все мутировавшие модели окажутся вонючими, прежние лучшие исполнители, которые сейчас являются лучшими исполнителями, перейдут к следующему поколению, используя все накопленные знания.

Мутации являются стохастическими как по объему, так и по величине. Объем мутаций может быть некоторым числом от 0 до длины ДНК человека, тогда как величина может быть случайным числом от -1 до 1. Каждый раз, когда выполняется мутация, случайный индекс выбирается из веса человека и величины мутация добавляется к его стоимости. Вероятно, существует математически правильный способ их оптимизации, но, судя по моему ограниченному опыту, имеет смысл контролировать величину во временной зависимости. Там, где величина должна уменьшаться, алгоритм становится лучше, тем самым настраивая лучших исполнителей с помощью более тонкой ручки мутации. Конечно, эта ручка - метафора, и все это происходит случайно. Жаль, что не было ручки.

Это означает, что вам не нужно передавать нейронные сети в обратном направлении. Фактически, если бы обратное распространение было синонимом градиента, то вы бы назвали эту оптимизацию без градиента, что и есть. Это было для меня огромным сюрпризом, так как мои ограниченные знания заставили меня поверить в то, что для использования нейронных сетей всегда нужен обратный сигнал. Конечно, это важно при исправлении ошибки модели путем обратного распространения «правильного» ответа через веса и соответствующей их корректировки. Но в ES мы не обновляем никаких моделей. Мы просто оцениваем их, а затем копируем лучшие. Это означает, что их можно распараллеливать, и время вычислений может быть значительно сокращено.

Эта недавняя статья формулирует это преимущество и сравнивает различия в современных алгоритмах RL и ES. Вывод состоит в том, что ES превосходит RL ~ 1/2 по времени и RL ES ~ 1/2 по времени. Интересно, мог бы ES стать более целенаправленным в своих мутациях, если бы он каким-то образом ограничивался только весами, модифицированными в раунде обратного распространения по той же модели ... В любом случае, это захватывающий и супер-крутой материал.

Тележка

Https://gym.openai.com/v2018-02-21/videos/CartPole-v1-fb83b2dc-e624-485a-ad24-273c2746d3be/original.mp4

Итак, как я уже сказал, я какое-то время хотел сделать алгоритм, который побьет телегу. И я наконец дошел до этого в виде эволюционного алгоритма. Пока я кодировал все это сам, я был вдохновлен и украл эволюционные идеи из этого гитхаба.

Первый шаг - создать простую сетевую сеть и оценить ее в среде OpenAI. Поскольку я изучал идею эволюционных алгоритмов (как то, что я там делал?), Я упростил замену архитектур простых моделей и оценку их производительности.

def create_model(layers=[4,4,2,1]):
    model = Sequential()
    model.add(Dense(layers[1],input_shape=(layers[0],),activation='relu'))
    for neurons_in_layer in layers[2:-1]:
        model.add(Dense(neurons_in_layer,activation='relu'))
    model.add(Dense(layers[-1],activation='sigmoid'))
    return model
def s(obs,num=4):
    return np.reshape(obs, [1, num])
def experience_env(env,individual):
    obs = env.reset()
    award = 0
    done = False
    while done == False:
        action = individual.predict_classes(s(obs))
        obs, reward, done, info = env.step(action[0][0])
        award += reward
    return award
env = gym.make('CartPole-v0')
experience_env(env,create_model())

Но для эволюции нам, очевидно, нужно создать поколение людей, поэтому я создал это, а также способ для всего поколения испытать окружение. Вот где я обнаружил, что количество переживаний имеет значение. Вначале я использовал только 5, но когда я хотел оценить последовательность индивидуума, было полезно увеличить его до 10 или 20. Таким образом, я мог быть уверен, что у меня достаточно данных, чтобы сделать разумный выбор, когда дело доходит до выживания в лучшие.

gen = create_first_gen(10)
gen,gen_reward = generation_experience_env(env,gen,10)
gen,gen_reward
([<keras.models.Sequential at 0x10b4f59e8>,
  <keras.models.Sequential at 0x10bdacb70>,
  <keras.models.Sequential at 0x10be97400>,
  <keras.models.Sequential at 0x10bf91fd0>,
  <keras.models.Sequential at 0x10bf98be0>,
  <keras.models.Sequential at 0x10ace6668>,
  <keras.models.Sequential at 0x10abd20b8>,
  <keras.models.Sequential at 0x10c106be0>,
  <keras.models.Sequential at 0x10c2a8048>,
  <keras.models.Sequential at 0x10c40c358>],
 array([[ 31.,  17.,  33.,  22.,  50.,  23.,  27.,  37.,  67.,  87.],
        [ 39.,  30.,  99.,  21., 170.,  24.,  28.,  22., 123.,  19.],
        [ 11.,  10.,   9.,  10.,   9.,   9.,   8.,   9.,  10.,   9.],
        [ 10.,   9.,   9.,  10.,  10.,   9.,  10.,  10.,   9.,  10.],
        [ 18.,   9.,  10.,  10.,  10.,  10.,   8.,   9.,  10.,   9.],
        [ 10.,  10.,  29.,  10.,  27.,  18.,   9.,  15.,  20.,  18.],
        [  9.,   8.,  10.,  10.,   9.,  10.,   9.,  10.,   9.,  10.],
        [ 81., 119., 111., 197., 112.,  81.,  47., 125., 100.,  60.],
        [  8.,   9.,  10.,  10.,  10.,  10.,   9.,   9.,   9.,   9.],
        [  9.,  11.,   8.,   8.,  10.,   9.,   9.,   8.,   8.,  10.]]))

Оказавшись там, следующий шаг - решить, какие два исполнителя являются лучшими. В приведенном выше примере ... ну. Но не всегда это так очевидно. Фактически, здесь я какое-то время использовал среднее значение общих баллов, но обнаружил, что некоторые модели ведут себя как все или ничего, получая 4 раза по 100+ точек, а затем 6 раз по 25 точек. Поскольку среднее значение сильно искажено большими числами, я выбрал медиану. Это потребовало от меня дополнительно добавить второй уровень сортировки, как только модели улучшатся достаточно, чтобы ограничить медианное значение 200, поэтому я сортирую как по медиане, так и по максимальной сумме вознаграждения, чтобы выбрать окончательных победителей.

def best(current_reward,num=2):
    # rank based on median then sum
    top = ((rankdata(np.median(1000/current_reward,axis=1))+rankdata(np.sum(1000/current_reward,axis=1))).max()/(rankdata(np.median(1000/current_reward,axis=1))+rankdata(np.sum(1000/current_reward,axis=1)))).argsort()[-num::][::-1]
    print('best models:',top)
    print('median:\t',np.median(current_reward,axis=1))
    print('sum:\t',np.sum(current_reward,axis=1))
    return top
def survival_of_the_fittest(current_generation,current_reward,fittest=2):
    fittest_idx = best(current_reward)
    fittest_individuals = [current_generation[idx] for idx in fittest_idx]
    fittest_weights = [[layer.get_weights()[0] for layer in individual.layers] for individual in fittest_individuals]
    return fittest_weights,fittest_individuals

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

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

def mutate(dna,chance=1,min_mutations=10,max_mutations=0,alter=True,max_change=.5):
    if max_mutations == 0:
        max_mutations = dna.shape[0]
    div = max_change*1000
    severity = np.random.randint(min_mutations,max_mutations)
    if np.random.rand() <= chance:
        for each_mutation in range(severity):
            base = np.random.randint(0,dna.shape[0])
            if alter:
                change = np.random.randint(0,div)/1000
                if np.random.choice(['add','sub']) == 'add':
                    dna[base] = dna[base] + change
                else:
                    dna[base] = dna[base] - change
            else:
                dna[base] = 1-(np.random.rand()*2)
    return dna

Последняя функция, которую я сделал, заключалась в том, чтобы передать распутывание, мутацию и перераспределение весов в функцию. Этот не требует пояснений и, по сути, выводит на тестирование модели следующего поколения.

def create_next_generation(fittest_dna_and_shapes,num_individuals=10,layers=[4,6,5,2,1],chance=1,max_mutations=20,min_mutations=10,alter=True,max_change=.5):
    next_generation_dna_and_shapes = list(fittest_dna_and_shapes)
    
    # mutate
    for individual in range(num_individuals):
        random.shuffle(fittest_dna_and_shapes)
        dna,weight_shapes = np.copy(fittest_dna_and_shapes[0][0]),np.copy(fittest_dna_and_shapes[0][1])
        mutated_dna_and_shapes = (mutate(dna,chance=chance,min_mutations=min_mutations,max_mutations=max_mutations,alter=alter,max_change=max_change),weight_shapes)
        next_generation_dna_and_shapes.append(mutated_dna_and_shapes)
    
    
    # dna -> weights
    next_generation_weights = []
    for (dna,weight_shapes) in next_generation_dna_and_shapes:
        #print('next gen pairs:',(dna,weight_shapes))
        next_generation_weights.append(reshape_dna(dna,weight_shapes))
        
    # weights -> models
    next_generation = []
    for weights in next_generation_weights:
        model = create_model(layers)
        model.set_weights(weights)
        next_generation.append(model)
        
    return next_generation

Наконец, последний бит - это полный код для запуска.

num_individuals = 10
num_best = 2
num_new_individuals = num_individuals - num_best
num_generations = 10
num_experiences = 10
max_change = .5
layers=[4,4,2,1]
print('----- first gen ------')
first_generation = create_first_gen(num_individuals=num_individuals,layers=layers)
current_generation,current_reward = generation_experience_env(env,first_generation)
fittest_weights,fittest_individuals = survival_of_the_fittest(current_generation,current_reward)
nb_watch_model(fittest_individuals[0],layers)
fittest_dna_and_shapes = weights_to_dna(fittest_weights)
try:
    for i in range(num_generations):
        print('----- next gen ------')
        # min and max mutations
        dna_length = fittest_dna_and_shapes[0][0].shape[0]
        min_mutations = 10
        max_mutations = dna_length 
        
        next_generation = create_next_generation(fittest_dna_and_shapes,num_individuals=num_new_individuals,chance=1,alter=True,min_mutations=min_mutations,max_mutations=max_mutations,max_change=max_change,layers=layers)
        #next_generation = fittest_individuals+next_generation
        current_generation,current_reward = generation_experience_env(env,next_generation,trys=num_experiences)
# scale learning rate
        total_reward = current_reward.shape[0]*current_reward.shape[1]*300
        total_current_reward = current_reward.sum()
        max_change = ((total_reward - current_reward.sum()) / total_reward)/2
        print('max change:',max_change)
fittest_weights,fittest_individuals = survival_of_the_fittest(current_generation,current_reward)
        nb_watch_model(fittest_individuals[0],layers)
        fittest_dna_and_shapes = weights_to_dna(fittest_weights)
    
except ValueError:
    print('----- the end ------')
    print('perfect score!!!')
    print('it took {} generations of {} individuals. there were {} cartpole games played!!'.format(i,num_individuals,(i*num_individuals*num_experiences)))

Чрезвычайно удачный пример вывода: обычно вы не можете найти модель лучше, чем медиана 100 в первом поколении.

----- first gen ------
best models: [4 8]
median:	 [ 10.  10.  10.  10. 200.   9.   8.  10.  14.  10.]
sum:	 [ 49.  49.  47.  62. 906.  47.  41.  76. 117.  47.]
----- next gen ------
max change: 0.20291666666666666
best models: [2 0]
median:	 [185.   18.  200.    9.5  32.    9.  194.  145.    9.5  13.5]
sum:	 [1851.  182. 1876.   94.  345.   90. 1736. 1390.   96.  165.]
----- next gen ------
max change: 0.04041666666666666
best models: [6 4]
median:	 [200.  200.  200.  147.  200.  120.  200.  195.5 200.  188. ]
sum:	 [1774. 1888. 1754. 1450. 1969. 1419. 2000. 1655. 1884. 1782.]
----- next gen ------
max change: 0.010333333333333333
best models: [7 5]
median:	 [200.  196.5 200.  200.  199.  200.  182.  200.  200.  200. ]
sum:	 [2000. 1888. 2000. 2000. 1844. 2000. 1759. 2000. 1987. 1902.]
----- next gen ------
max change: 0.0
best models: [9 8]
median:	 [200. 200. 200. 200. 200. 200. 200. 200. 200. 200.]
sum:	 [2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000. 2000.]
----- next gen ------
----- the end ------
perfect score!!!
it took 4 generations of 10 individuals. there were 400 cartpole games played!!

Мне нужно было проделать свой путь к отображению рендеринга Cartpole по мере того, как происходила эволюция, поскольку я не мог заставить его работать в самом Jupyter. Итак, эта спорная функция позволила мне узнать, как работают мои модели.

def nb_watch_model(model,layers):
    model.save_weights('model.cartpole')
    str_layers = ','.join([str(i) for i in layers])
    os.system('/Users/xbno/anaconda3/bin/python single_model.py -m model.cartpole -l {}'.format(str_layers))

Заворачивать

Я должен сказать, что был очень взволнован, увидев, что мой алгоритм превзошел Cartpole. У меня нет ни детей, ни домашних животных. Все, что я могу сказать, это то, что теперь я сочувствую всем этим побочным родителям. А поскольку в OpenAI Gym есть масса других сред, с которыми можно поиграть, я займусь этим некоторое время.

Я скоро выложу свой блокнот jupyter на свой гитхаб! Всем спасибо за чтение!

Танцы

У меня возникла эта глупая идея после того, как я смог заставить свою установку производить способные к игре модели на тележке, чтобы заставить модели танцевать, когда они балансируют на шесте. Изменив функцию вознаграждения, включив в нее позиционирование тележки, я подумал, что смогу заставить ее покачиваться взад и вперед. Хотя это сработало в краткосрочной перспективе, было трудно равномерно сбалансировать долгосрочное игровое вознаграждение с вознаграждением за позицию. слишком много одного, и тележка будет постоянно сосредоточиваться на одной метрике. Кто-нибудь знает, как оценивать модели по множеству вознаграждений или как можно объединить эти два значения в хорошо сбалансированную функцию вознаграждения? Большое спасибо!