Таким образом, самые достойные существа продолжают свое дело и дают потомков, а самые слабые — отсеиваются в процессе отбора. Несколько экспериментов и их результаты — под катом.
Первые пробы. Прохождение по лабиринту
В качестве пробы пера я решил научить существ (будем называть их так, поскольку термин «программа» слишком громкий для этого случая) искать путь в лабиринте, как на картинке сверху. Сразу оговорюсь, что в данном случае целью было не вывести с помощью генной инженерии существо, которое будет находить путь в произвольном лабиринте, а найти путь в одном конкретном лабиринте, причем чтобы у существ не было органов чувств. После недолгих размышлений было решено использовать простой набор команд, который может легко мутировать и исполняться существом. Итак, наша «программа», или «геном» — это просто набор команд, в какую сторону следует продвинуться существу. В дополнение, был добавлен цикл, который сможет повторять участок программы несколько раз. Геном может быть, например таким:
Здесь команда FOR (IxT) означает, что следующие I инструкций будут выполнены T раз. Существо с таким геномом пройдет на 2 клетки вправо, затем на 2 клетки вниз и еще раз на 2 клетки вправо.
Теперь займемся нашим существом, по сути являющимся интерпретатором генома. У каждого нашего существа будет «имя», или идентификатор, по которому мы потом будем его узнавать, «геном», по которому наше существо будет действовать, метод Step, по которому существо будет выполнять следующий ген (при этом будем проверять, не ходит ли существо сквозь стены), функция Fitness, которая будет определять, насколько близко существо подобралось к финишной клетке, и метод Mutate, который будет менять часть генома. О мутациях чуть позже, а пока посмотрим, как наш милый квадратик выполняет нашу тестовую программу:
Fitness, или соответствие существа нашим требованиям будем считать как сумму разностей координат финиша и существа. Так, существо с наименьшим расстоянием до цели будет наиболее подходящим.
Эволюция
Существа у нас будут эволюционировать поколение за поколением, по 100 существ в каждом. В рамках каждого поколения мы будем повторять одни и те же шаги:
- Проверка существ на соответствие (вычисление Fitness);
- Сортировка существ по возрастанию расстояния до цели;
- В зависимости от положения в списке:
— либо не будем менять существо, с предположением что оно и так достаточно хорошо, и незачем его портить;
— либо изменяем геном: удаляем, добавляем, изменяем часть генов;
— либо заменяем новым случайным существом, старое при этом «вымирает» как недостойное к продолжению рода.
Методом тыка выберем такое отношение: 10 лучших не меняем, следующие 60 мутируют, остальные 30 вымирают. Пробуем, на 292 поколении получаем результат:
Геном нашего победителя такой:
Y_INC; X_INC; Y_INC; FOR_2x3; X_INC; Y_INC; FOR_1x4; X_DEC; FOR_2x3; Y_INC; X_INC; Y_DEC; Y_INC; FOR_2x4; Y_INC; X_INC; Y_INC; FOR_2x4; X_DEC; X_INC; X_INC;
Как можно увидеть, существо часто топчется на одном месте, что, в общем-то, не совсем хорошо. Попробуем взять в расчет еще и длину генома. Но поставим ее на второе место по важности после расстояния до финиша, чтобы не оказалось так, что короткие программы из 2-3 генов, которые стоят на старте, оказались лучше в сравнении с теми, которые доходят ближе до цели, но при этом имеют длинный геном. Методом, опять же, тыка, пробуем формулу:
Теперь поиск решения идет несколько дольше. На 335 поколении мы получаем точно такой же результат, но уже со следующим геномом:
FOR_2x4; X_INC; Y_INC; FOR_2x3; Y_INC; X_INC; Y_DEC; Y_INC; FOR_2x4; Y_INC; X_INC;
Собственно, здесь еще многое можно доработать, но задача решена, с приемлемым качеством и с приемлемыми трудозатратами. Свой код я намеренно не привожу в связи с его недоработанностью. Поэтому с чистой совестью можно перейти к теме, вынесенной в заголовок.
ELTRUT-проблема
В 2012 году выпускник Университета штата Калифорния Бенджамин Буш занимался генетическими алгоритмами и, в частности, так называемой ELTRUT-проблемой. Суть задачи такова: существует растровое изображение, найти последовательность команд для черепашки Лого, которая нарисует подобное изображение. Вот видео, где все довольно доступно объяснено:
Если коротко, Бенджамин предлагает такое решение: откажемся от простого линейного генома в пользу двумерного. Почему? Потому что двумерный геном обладает избыточностью, так что далеко не каждая мутация спровоцирует кардинальное изменение генома. Также Бенджамин предлагает интересную форму представления генома: каждому пикселу изображения сопоставляется ген, который указывает в какую сторону и на какое расстояние отправиться черепашке, если она попадет в эту точку, и нужно ли ей при этом рисовать линию. Визуально геном можно отобразить так:
Здесь коричневым выделен избыточный геном. Видно, что почти в 80% мутаций изменений в поведении черепашки не произойдет. Вдобавок, при решении этой задачи предлагается использовать «половое размножение», или кроссинговер: два существа предыдущего поколения обмениваются генами, чтобы получить потомков. Далее идет речь о трехмерном геноме и об оптимизации алгоритма, но оптимизацией я заниматься не хотел, да и три измерения это несколько чересчур для ознакомления с генетическими алгоритмами, поэтому я решил остановиться на воплощении упрощенного алгоритма.
Итак, аналогично предыдущей задаче, в каждом поколении у нас будет по 100 существ. В каждом поколении мы будем:
- Скрещивать 50 лучших существ и заменять 50 худших их потомками;
- Изменять по одному гену каждому существа, кроме 10 лучших;
- Если существо уже выполнило свою задачу, то оно мутировать не будет.
Как мы будем определять степень соответствия существ? За каждый правильно нарисованный пиксел начислим существу некоторое количество очков, за каждый неверно нарисованный будем штрафовать. Если там, где пиксел не закрашен, и он не должен быть закрашен, не меняем счет. Итого, примерный алгоритм подсчета очков:
Сразу решим еще одну проблему (как раз она и должна решаться трехмерным геномом): если черепашка встанет на пиксел, где она уже была, то выполнение программы зациклится. Поэтому сделаем так, чтобы наступив на посещенный пиксел, черепашка останавливалась. Заодно мы не будем до посинения гонять бедную рептилию.
Итак, пробуем. Изображение 5х5, не очень сложное, сможем ли мы получить результат?
Rotate 270
Forward 1
PenUp
Rotate -146.3
Forward 3.6
PenDown
Rotate -123.7
Forward 4
Rotate 270
Forward 4
Rotate 270
Forward 4
Rotate 270
Forward 4
Неплохо, но как программа справится с несколько большим изображением, например, 8х8?
Видео сильно ускорено (предыдущее тоже ускорено, но слегка). 2000 поколений так и не дали правильный результат. Можно было бы попробовать продолжить, но как видно по графику, около 1000 поколений никаких изменений не происходило, так что вряд ли это решило проблему. Ну и нелюбовь к оптимизации сделали свое дело: на изображение 8х8 потребовалось около 2 часов на моем компьютере. Так что, эксперимент признается частично проваленным.
Спасибо за уделенное время, надеюсь, эта статья покажется кому-то интересной.
Комментарии (20)
gurux13
07.04.2016 13:48+4Насколько я понимаю определение генетического алгоритма, первый алгоритм в Вашей статье — не генетический (нет кроссинговера). Это больше похоже на случайные блуждания в пространстве решений, с отсевом по fitness. Если бы Ваши «существа» умели скрещиваться (например, обмениваться кусками маршрута на совпадающих точках лабиринта), сойтись могло сильно быстрее. Кроме того, рандомизацию часто имеет смысл добавлять не только в мутации, но и в отбор — например, убивать любое существо с вероятностью, антимонотонно зависящей от функции качества.
Yuuri
07.04.2016 14:58То есть генетические алгоритмы обязательно должны основываться именно на «половом» размножении?
gurux13
07.04.2016 15:16+7В целом, да («Отличительной особенностью генетического алгоритма является акцент на использование оператора скрещивания»). Если особи не взаимодействуют друг с другом никак, это просто случайный поиск. Храним топ решений, как-то случайно их перефигачиваем, обновляем топ. Это сработает для монотонно улучшаемых задач, а вот если нужно слегка ухудшить решение, чтобы от него можно было дойти до более оптимального, будет хуже. В случае кроссинговера особи могут обмениваться удачными «частями» решения целиком, плюс хорошо, если любая особь может выжить с некоторой вероятностью.
JGDger
07.04.2016 17:49Да, вы правы, кроссинговер по-хорошему является неотъемлемой частью генетический алгоритмов. На самом деле я не смог придумать как сделать вменяемый обмен генов, чтобы в нем был смысл, а работает, в общем-то, и без кроссинговера
fareloz
07.04.2016 14:38+1Чем обусловлено отсутствие мутации у десяти лучших особей?
ComradeAndrew
07.04.2016 15:51Полагаю нужно для того, чтобы не было деградации. Если всегда 10 лучших особей не будут изменятся, а появятся на некотором поколении особи лучше них, то уже новые 10 не будут мутировать. Если бы мутировали все особи, то лучше могли бы деградировать и алгоритм в этом случае явно справлялся бы с задачей хуже на бОльших объемах входных данных.
Хотя если вопрос именно в числе 10 особей, то вероятно просто выбор автора.gurux13
07.04.2016 17:03Как правило, мутировать лучших — это хорошее дело. Только надо и самих лучших в популяции оставлять тоже, тогда не будет деградации.
fareloz
07.04.2016 17:21+1нет, вопрос не в количестве. Я, как и комментатор выше, полагаю лучше их также мутировать методом клонирование+мутирование.
ComradeAndrew
07.04.2016 17:30Я об этом же подумал. Но автор не использует скрещивание, а оно тут как раз давало бы наиболее эффективные результаты с выборкой скрещивания полезных признаков. Ну или как-то так. Хотя, честно признаться, я в таком типе моделей не сильно разбираюсь. Пока только логически предполагаю на основе некоторых знаний в области.
JGDger
07.04.2016 17:55Лучшие особи я решил оставлять без изменений как раз для того, чтобы они не деградировали, иначе любая мутация (а в большинстве своём они негативные) резко бы ухудшила их способности. Ну и в итоге это дало неплохой прирост в скорости нахождения решения (кстати, не совсем ясно, почему). Ещё раз оговорюсь, что это был эксперимент, и разработка велась по принципу «а что, если — о, работает».
datacompboy
07.04.2016 14:48+3Простое расстояние для первого лабиринта сломается, как только лабиринт примет вид «прямая ветка почти до выхода, а ответвления были в начале в сторону и назад»
daiver19
07.04.2016 18:42+1В чем смысл решения задач, оптимально решаемых другими методами, с помощью ГА? Кстати, по-моему скромному опыту, даже задачи оптимизации, где актуальны ГА, лучше решаются более простыми методами а ля иммитация отжига или даже простой градиентный спуск.
fareloz
08.04.2016 11:09+3Во-первых это весело. Во-вторых концепция ГА достаточно интересна с точки зрения моделирования.
Автор вроде не претендовал на какую-то научность, а проводил исключительно эксперимент.
Holms
07.04.2016 20:39Посмотрите на этот сайт http://www.critticall.com/, этот чувак только этим и занимается.
mickvav
08.04.2016 19:06Интересно бы увидеть как влияют пороги и число существ в поколении на скорость решения.
IvaYan
А код примеров (с лабиринтом) доступен?
digger3d
Хорошо бы еще и с реализацией на OpenCL
digger3d
А недоработанность кода, думаю, хабражители простят, тема ведь не о коде, а о генах =)
JGDger
Вообще, код писался для себя,
так что его прочтение может вызвать тошноту, головокружение, рвоту и приступы эпилепсии, но вот: https://github.com/JGDger/ELTRUT