Впервые про моделирование эволюции я прочитал в 13 лет в статье «Жить и умереть в компьютере» (Техника — Молодежи, №5 1993 год). Она произвела на меня столь неизгладимое впечатление, что я тут же загорелся идеей создать что-то подобное.

Однако никак не удавалось проработать законы мира. Как организмы будут «смотреть» на окружающий мир? Как общаться? Как атаковать? Как кушать друг друга? Наконец, как будет устроен их «мозг»? Реализовать виртуальную машину, как в статье из журнала, или использовать что-нибудь проще, типа конечного автомата или схемы из блоков И-НЕ?

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

Бесплатный базовый курс по JS: «Основы JavaScript: от переменных до функций». Внутри рассказываем, как работать с переменными, типами данных, функциями и многом другом!

Начать изучение →

Присказка

Моделей эволюции уже существует огромное множество, зачем еще одна? Я хочу предложить вам максимально простую модель, которую можно создать за 20-30 минут, а интересных результатов добиться практически сразу (а не за 4 миллиарда лет).

Однажды я натолкнулся на формулу «вечного календаря» — она вычисляла день недели по дате, учитывая все нюансы определения високосных годов. Так родилась мысль написать кратчайшую формулу, вычисляющую количество дней в месяце (без учета високосных годов).

То есть с одной стороны, задача простая — написать формулу, которая по номеру месяца (нумерация с нуля, январь — нулевой месяц) возвращает количество дней в нем. Однако есть нюанс — формула должна быть максимально короткой. Прекрасная возможность опробовать генетические алгоритмы в деле!

Чтобы было интереснее, подумайте, как бы вы это реализовали? Самый очевидный вариант — просто лукап по таблице. Однако здесь явно прослеживается закономерность: дни с 30 и 31 днями чередуются. Правда, в августе эта закономерность слегка икает. Да еще и этот февраль…

Можно реализовать с помощью условий, особенно в языках, где поддерживается тернарный оператор или логические операторы с short-circuit evaluation. Однако будет ли это кратчайшим решением?

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

Проект мира

Итак, нам нужно создать модель эволюции (или реализовать генетический алгоритм, если вам так больше нравится). Организмами в нашем мире будут формулы. Их генетический код — строка с арифметическим выражением. Например, «2+2» — рядовой житель этого мира. Не самый успешный, поскольку для выживания нужно уметь отвечать на главный вопрос жизни, Вселенной и всего такого.

Хорошим жителем считается формула, выдающая более-менее близкие ответы, отличным — выдающая верные ответы для каждого месяца, а идеалом — кратчайшая из них.

Первоначально строительными блоками я выбрал цифры, арифметические операции и круглые скобки. Разумеется, формулы, возвращающие константное значение, вряд ли способны решить нашу задачу. Необходим параметр — номер месяца. Он будет называться «x». Например, еще один житель нашего мира: «25+x». И он реально хорош — выдает правильные ответы для июня и июля! Итого, изначально строительными блоками генетического кода были символы:

0123456789+-*/()x

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

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

В общем, все очень постепенно и обдуманно. Здесь же изменение всего лишь одного символа может привести к кардинальным и даже катастрофическим последствиям. Например, жила-была формула 500+x, у нее родился ребенок с мутаций — знак деления вместо первого нуля: 5/0+x — бах, деление на ноль. И это еще повезло, что знак деления не появился на месте пятерки. /00+x — это вообще что за монстр? Однако скоро мы увидим, что это как раз-таки не является особой проблемой.

Итак, жителями этого мира являются формулы, а создать мир хотелось очень быстро — за 20-30 минут. Следовательно, выбор языка программирования пал на скриптовые языки, в которых есть крайне непопулярная функция eval(). Зато не нужно заморачиваться с поиском подходящего интерпретатора формул, он уже идет из коробки. 

Сперва попробовал Python, но столкнулся с одной крайне неприятной проблемой с производительностью. Не потому, что Python сам по себе не особо быстр, а потому что он прозрачно поддерживает длинную арифметику, а в мире случайных формул вполне может появится, например, такая: x**967**9813. Python не моргнув глазом пытается ее вычислить, и его нисколько не смущает, что это займет миллиард-другой лет. В итоге пришлось переметнуться на старый добрый JavaScript, в котором есть JIT-компилятор и арифметическое переполнение.

Первичный бульон

Идеал должен родиться из полного хаоса, поэтому изначально мир должен быть заселен абсолютно случайными обитателями. Я просто сгенерировал 100 случайных строк длиной от 5 до 20 символов из набора «0123456789+-*/()x». 

Подавляющее большинство, разумеется, получились безобразными монстрами. Например, такими:

87365*)0-2(622

2)3-(510(

)6(7)2+/442/70x+x3

(*x/*5*87

9+8-(/4+84(9

Однако попадались и приличные:

98+651-x

623+15

9984*6

+65*7

Разумеется, если скормить монстра функции eval(), получим исключение, поэтому нужно обернуть вызов в блок try…catch. Для тех формул, которые исполняемы, нужно сравнить ответы с правильными и дать оценку, насколько она далека от идеала. Изначально я просто использовал сумму квадратов разниц, то есть классический LSE. Чем больше — тем хуже, ноль — идеал. Неисполняемым формулам назначил оценку плюс бесконечность.

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

У eval() есть «приятное» свойство: она исполняет код в текущем контексте, поэтому тот имеет доступ в том числе и к локальным переменным. Обычно это угроза безопасности, но чего нам здесь бояться? Что рандомные формулы похитят наши куки? Таким образом, если в цикле менять переменную x от 0 до 11, то это значение и будет использоваться в качестве икса в формуле. 

Оценка формулы первоначально выглядела примерно так:

function evaluate(formula) {
    try {
        for (let x=0; x<12; x++) {
            const answer = eval(formula);
            // сравнить с правильными ответами и оценить качество
            // ...
        }
    } catch {
        return +Infinity;  // строго наказать
    }
}

Посчитав оценку для каждой из ста формул, можно просто отсортировать их по оценке. 

Однако не зря ругают eval(). Сначала я столкнулся с формулами-мошенниками и формулами-злодеями. Если формула содержала ++x или x++, она изменяла переменную, используемую в цикле, и пропускала некоторые значения. Соответственно, штрафы она получала не за 12 месяцев, как нормальные люди, а всего за 6 или даже меньше. Но это всего лишь мошенники, они не так страшны. Куда опаснее оказались злодеи, рожденные, чтобы разрушить этот мир. В них присутствовал фрагмент --x или x--, а это приводило к бесконесному циклу.

Проблему исправил колхозным сохранением переменной перед eval() и восстановлением значения после. После этого все заработало как надо, хотя и не совсем так, как хотелось, но об этом позже.

Так как хороших формул на порядок меньше, чем монстров, я решил, что выживать будут только топ-10, остальных — на помойку. Это освобождает 90 мест под Солнцем для новых жителей. Соответственно, девяносто жителей получают шанс создавать потомство. Таким образом, десять лучших выживают сами и еще дают потомство, десять худших просто получают премию Дарвина, а средние восемьдесят хоть сами и не сохраняются, но дают потомство.

Что такое потомство в мире формул? Разумеется, создавать точную копию существующей формулы не имеет смысла, ничего нового она нам не даст. Поэтому каждый потомок всегда имеет мутации. Я выбрал три возможных мутации:

  • случайно выбранный символ меняется на другой,

  • случайно выбранный символ удаляется,

  • в случайное место вставляется случайный символ.

Таким образом, в процессе эволюции формулы могут как расти в размере, так и уменьшаться.

При рождении потомок получал одну из мутаций, выбранную случайным образом. Изменение символа имело шанс 90%, появление нового символа — 5,5%, удаление — 4,5%. Таким образом, имелась тенденция к неспешному росту. Кажется, что это противоречит нашим целям — ведь нужно получить самую короткую формулу, но именно поэтому и нужен такой перекос. В результате естественного отбора длинные формулы гибнут чаще, а рост через мутации компенсирует это.

Итак, я создал мир, в котором население всегда составляло 100 жителей. Продолжительность существования одного мира установил в 10 000 поколений (которые по привычке назвал эпохами). А так как одна полная симуляция мира занимала всего около 30 секунд, можно было перезапускать многократно, получая параллельные миры (или просто другие планеты).

Дальше вас ждет самая интересная глава — а что же из этого вышло?

Приспособляемость

Примерно так выглядела история мира на экране. Здесь показано не все население, а только повелители (топ-1), их год восхождения на престол, оценка и те ответы, которые они выдают для каждого месяца. Разумеется, когда появляется новая, более продвинутая формула, она занимает престол до тех пор, пока не появится формула еще лучше.

Несмотря на то, что первичный бульон состоял из абсолютно рандомных и по большей части некорректных формул, уже в течение первых 100 поколений (доли секунды в реальном мире) появлялись интересные решения, порой даже с частично правильными ответами. 

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

При перезапусках, безусловно, история шла альтернативными путями — другие лидеры, другие года. Однако некоторые формулы оказывались настолько живучими, что переживали даже полное уничтожение всего живого! То есть, даже после перезапуска симуляции с нуля, опять с рандомного первичного бульона, в результате могла получиться почти такая же формула, как и при предыдущих запусках.

Один из этих фениксов выглядел так:

365/12

Внезапно! Да, она коротная, но это совершенно точно не то, что я искал. Она вообще не зависит от икса. Хотя логика в этом есть — в месяце в среднем действительно 365/12 дней, и с точки зрения наименьших квадратов это и правда довольно близкий ответ на главный вопрос жизни, Вселенной и всего такого.

Иногда эта формула рождалось с маскировкой, так, что сразу и не узнать:

01332/24

Что-то здесь не так… Знаменатель удвоился, а вот числитель вовсе не похож на 730. Оказывается, это действительно 730. Дело в том, что JS трактует числа, начинающиеся с нуля, как записанные в восьмеричной системе счисления. 01332₈ = 730₁₀.

Были и другие дроби с близкими результатами:

213/7

517/17

973/32

Это меня категорически не устраивало. В январе все-таки 31 день, а не 30,42! Значит, нужно менять законы этого мира. Дробные ответы как финальное решение мне не нужны, но и полностью от них отказываться не хотелось, поскольку они могли послужить промежуточным звеном, поэтому я решил просто за них слегка наказывать.

if (answer%1 != 0) {
    loss += 1;
}

Даже такая мягкая кара сразу кардинально изменила идеалы красоты в этом мире.

Отныне королем стала формула:

30

Именно то, что заказывали — целое число! И даже четыре раза в год выдает точный ответ!

Важно понимать: это был не просто победитель одной симуляции. Это победитель каждой симуляции. Формула «в каждом месяце 30 дней» стала единственной силой во Вселенной — при каждом перезапуске в конечном итоге она становилась венцом творения. По идее, именно так должна вести себя идеальная формула, но с точки зрения Главного Вопроса эта формула далека от абсолютной истины.

Тем не менее, пользу она принесла, напомнив, что точный ответ — это тоже важный критерий. LSE хорошо справляется, когда ответы далеки от ожидаемых, однако к близким (но неверным) ответам он слишком лоялен. Нужно насыпать немного перфекционизма.

if (answer != expected) {
    loss += 1;
}

И повелитель Вселенной тут же меняется! Теперь это… ТА-ДАМ!

31

Короткая? Короткая. Целые числа? Целые. Ответы правильные? В 7 случаях из 12! Гораздо лучше, чем предыдущий лидер! 

С этим нужно было кардинально что-то делать. Нужно было так изменить законы физики, чтобы бред был принципиально невозможен.

Общая проблема всех вышеперечисленных формул-лидеров — они на самом деле константы и никак не зависят от икса. Посему ввел новую заповедь: формула должна содержать «x». Формулы без «x» строго карать.

if (formula.indexOf('x') == -1) {
    loss += 10000;
}

Ну теперь-то я точно получу что-то стоящее! Получил…

Суровая кара за отсутствие икса не осталась незамеченной. Эволюция тут же ответила идеальным решением:

31//x

Если ваш родной язык Python, то вы наверняка недоумеваете, зачем здесь нужен оператор целочисленного деления. Но это JavaScript, детка. Здесь // — это начало комментария.

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

Причем, как видно по истории, «идеал» родился уже в 37-м поколении, а дальше просто вытряхивал мусор из карманов. Что с этим делать? Продолжаем метод кнута — запрещаем комментарии.

if (formula.indexOf('//') != -1) {
    loss += 10000;
}

Ну теперь-то лазеек точно не осталось, казалось мне. Остались…

31+x-x

31+x*0

31+0x0

Последний особенно впечатляет. Да, действительно, eval() имеет доступ к локальным переменным и автоматически их подставляет… Кроме тех случаев, когда «x» — это не переменная, а часть числа в шестнадцатеричной системе.

А иногда, очень-очень редко, получалось такое:

31/*x*/

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

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

Пришлось запоминать все ответы (раньше они просто обрабатывались по одному и выбрасывались), а затем проверять на уникальность.

if ((new Set(answers)).size == 1) {
    loss += 100000;
}

Теперь у констант больше не было ни единого шанса удержаться в лидерах.

Я надеялся, что это наконец-то даст толчок бурному развитию формул, и начнут появляться те, которые выдают правильный ответ в 12 из 12 случаев, пусть даже они будут невероятно длинными, громоздкими и запутанными. Ожидания опять не оправдались. Новый постоянный лидер:

30+x/11

Это просто линейная регрессия от 30 (в январе) до 31 (в декабре). В плане качества стало даже хуже: всего два целых ответа, всего один — правильный. Я увеличил количество эпох до 100 000, сделал несколько перезапусков — все без толку, эта формула (и различные ее вариации) доминировала.

Я уже почувствовал разочарование во всей этой затее и хотел бросить, как тут хлопнул себя по лбу! Ну конечно же! Линейная регрессия! А чего я хотел? У меня же только арифметические операции.  Конечно, с их помощью можно задать и нелинейные функции: полиномиальные и экспоненциальные, можно даже возводить (-1) в степень икс, чтобы получить так необходимый «заборчик». Но все равно арифметических операций, скорее всего, просто недостаточно, чтобы решить задачу. И я даровал жителям новые инструменты.

Оптимизация законов Вселенной

Даже добавление всего одной операции — деление по модулю — уже сдвинуло мир с мертвой точки. Появилась крутая формула:

31-x%7%2

Деление по модулю 2, или проверка четности, — простой способ задать чередование месяцев с 30 и 31 днями, а деление по модулю 7 сбрасывает чередование между июлем и августом. В итоге — 11 правильных ответов из 12! Один лишь февраль не укладывается в схему.

Тогда я щедро насыпал остальных операций: битовые операции, операции сравнения, тернарный оператор и т. д. Теперь алфавит стал выглядеть так:

0123456789+-*/()x%?:|&^<>=!~

Как ни странно, результат стал только хуже. Внезапно галактику стала захватывать такая формула:

5%x|30

Знаете, как в фантастических сериалах — куда бы герои ни улетели, даже на самых отдаленных планетах все равно живут гуманоиды. По официальной версии — потому что при похожих условиях эволюция развивается похожим образом, и в итоге все равно получится кто-то человекообразный. Хотя мы понимаем, что все потому, что арахниды и силикоиды не очень охотно подписывают контракты с киностудиями, а актерам-людям проще нанести грим, чем отыграть гигантскую амебу.

Тем не менее, версия про схожесть эволюции на разных планетах получила подтверждение: примерно в 80% перезапусков эволюция приходила именно к 5%x|30, даже несмотря на то, что она уступает 31-x%7%2.

Одна из причин этого — у некоторых формул шанс появиться и занять место под Солнцем выше, чем у других, поэтому по статистике они появляются раньше. Например, так как 31 в двоичной системе записывается как 11111, а 30 — как 11110, то операция побитового OR, призванная выставлять биты в единицу, очень благоприятна для этой задачи. Как только в формуле появляется OR, ее шансы пробиться в лидеры резко возрастают. А дальше уже дело техники — подобрать коэффициенты.

Но что мешает более крутым формулам появиться чуть позже и перетянуть одеяло на себя? Причина оказалась в кумовстве. Я добавил код, который после завершения симуляции выводит, кто в итоге остался жить в этом мире. Не все население в 100 жителей — это уже многовато, а просто топ-10.

Несмотря на то, что мир существовал на протяжении 10 000 эпох, после 376-й никаких потрясений не было — лидер занял трон и оставался на нем до конца.

В нижней части скриншота виден лог, показывающий претендентов. Видно, что все они — ближайшая родня. Отличия незначительные: где-то добавлен плюс, где-то ноль, где-то еще что-то, не влияющее на конечный результат, только на длину.

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

Чтобы этого избежать, я добавил защиту от клонов: в мире не должно быть двух формул, дающих одинаковый результат. Если вдруг такое произошло, останется та, у которой меньше длина.

Увы, резких перемен это не дало, просто родня стала более дальней. 5%x|30 продолжала появляться почти в каждом перезапуске, хотя иногда все же сдавалась и уступала своему четвероюродному брату, у которого теперь шансы появится на свет стали выше:

3/x^31

Здесь, кстати, достаточно интересная конструкция, которая абьюзит арифметику JS. Сразу бросается в глаза деление на икс, но при x = 0 получается деление на ноль! JS, в отличие от многих других языков, не считает деление на ноль причиной для паники, просто получается +Infinity. По идее, бесконечность бесконечно далека от правильного ответа, и эта формула должна была последовать за монстрами из первичного бульона, если бы не идущий следом побитовый XOR. В JS битовые операции, прежде чем выполниться, преобразуют операнды в int32 (хотя обычные числа в JS, даже целые — это всегда float64). Бесконечность от такого преобразования становится нулем! 0 xor 31 = 31, и именно столько дней в нулевом месяце январе.

Замечательно в этой конструкции и то, что при x=1 получаем 3 xor 31 = 28. Таким образом, это та редкая формула, которая правильно считает дни в феврале! А при x > 3 получается число меньше единицы, которое благодаря битовой операции тоже становится нулем.

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

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

На этот раз импакт оказался получше. 5%x|30 почти перестала появляться. Разнообразие результатов увеличилось. Например, родились такие формулы:

x%7+1|30

x%7^9|22

x%7^29|6

x^7>x|30

Они тоже правильно считают для 11 месяцев — всех, кроме февраля. Обратите внимание на последний вариант — мы его еще увидим. Или вот такой злой гений, который пытается уничтожить мир (теперь безуспешно), но зато правильно обрабатывает февраль:

28|7+!--x

В целом, вариативность выросла, получалось много интересных хороших формул, но вот отличных — которые выдают правильный ответ для всех 12 месяцев — как не было, так и нет. 

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

Я увеличил шансы выпадения этих символов в надежде, что что-нибудь хорошее с ними все же соберется, но тщетно. Такие сложные конструкции оказались эволюции не по плечу. Тогда я сделал наоборот — вообще удалил их из алфавита. Раз они все равно не используются, то пусть тогда и не мешают, ведь именно из-за скобок многие формулы в первичном бульоне были невалидными, а так бульон стал чуть более жизнеспособным. Как ни странно, это стало шагом назад: галактику поделили между собой кузены 5%x|30 и 3/x^31.

Увеличение размера формул в первичном бульоне тоже не дало особого эффекта. И тогда я решился на отчаянный шаг — половое скрещивание.

На пути к идеалу

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

Первый подход был осторожным. Во-первых, скрещивать не всех, а только две лучшие формулы в текущем населении. И смешивать через арифметическое среднее. Есть <формула1> и <формула2>, совмещаем их таким образом:

((формула1)+(формула2))/2

Скобки нужны, чтобы новая формула гарантированно была валидной вне зависимости от приоритета операторов. Результат получился хорошим. Даже слишком хорошим. Новая формула практически гарантированно оказывалась лучше, чем родитель №2, а порой и лучше, чем родитель №1. Соответственно, в следующем поколении уже она становилась родителем, и так далее. Это привело к взрывному росту длины генома.

Тогда я изменил способ скрещения:

формула1+формула2

Без скобок и без деления на два. Казалось бы — результат получится явно хуже. Во-первых, если в формулах используются битовые операции, их приоритет ниже, чем у плюса. Во-вторых, даже если с приоритетами повезет, у нас получится формула, где одно слагаемое примерно 30, другое примерно 30, в сумме 60 — явно хуже.

Но это и прекрасно! Это значит, что она не займет тут же место в топе, не создаст взрывообразного роста, а останется где-то в серединке, где будет постепенно мутировать из поколения в поколение, пока из нее не получится что-то интересное. И это сработало!

Впервые удалось получить формулу, которая верно решает для каждого из 12 месяцев. Выглядит она достаточно просто:

36-5^7<9^7+3<x+9*2%4^35<-6^7<x&5^7+2<x+3*2%9^3<3<62>x%5^2/x

Для сравнения длины, вот рукописная формула, которая делает лукап по таблице:

[31,28,31,30,31,30,31,31,30,31,30,31][x]

У эволюции получилось длиннее, чем при разумном творении, но все равно это уже невероятный успех! Наверняка формулу можно как-то сократить, но для этого сперва нужно разобраться, как она работает. Мне почему-то не хотелось. Зато хотелось проверить: был ли это счастливый случай, или модель наконец-то стала способна создавать формулы, решающие задачу.

Оказалось — да! Правильные формулы появлялись далеко не при каждом рестарте, но все же стали появляться. После нескольких рестартов родился вариант покороче:

x+8%8^71772%x%9^x<8|6+x*2|24+4%7^8&7

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

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

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

И так постепенно шаг за шагом был получен ИДЕАЛ.

2/x^30|x^7>x

Одна часть похожа на старого знакомого, который абьюзил деление на ноль, чтобы решить проблему с февралем. Другая — на старого знакомого, который умеет решать все, кроме февраля. Скрещивание прошло успешно!

Если вы пробовали составить формулу самостоятельно, сравните то, что получилось у вас, с тем, что появилось само.

Заключение

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

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

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

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

Наконец, нас окружают вещи, которые с точки зрения эволюции не особо-то и нужны. Та же философия особо не помогает в выживании и размножении — даже, скорее, наоборот. Тем не менее, она откуда-то возникла, не благодаря эволюции, а вопреки. Возможно, не все в этом мире подчиняется исключительно эволюционным процессам.

Комментарии (7)


  1. Fragster
    08.07.2025 09:45

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

    Помню, залипал в boxcar2d. Жаль, что оригинал на флеше. Беглый гуглинг дал что-то похожее https://rednuht.org/genetic_cars_2/

    Особенно впечатляло - плато на длительный период, а потом резкий скачок и новый фаворит.


  1. Emelian
    08.07.2025 09:45

    минималистичная модель эволюции

    Интересная идея, вполне заслуживает своего развития.

    С одной стороны, мы показываем, что мутации и естественный отбор могут из полного хаоса создать что-то упорядоченное. С другой стороны — мы при этом выступаем в роли Демиурга, которому приходится контролировать процесс и настраивать параметры.

    Из этого можно извлечь философский подтекст. Во-первых, естественно, наличие «Высшего Заказчика» («Демиурга», по-вашему). Кто он или что из себя представляет – неважно. Главное, что есть Интерес, Желание, Цель и Возможности реализации некой Идеи.

    Таковой Идеей может выступить концепция Развития, Разнообразия, Гармонии и Красоты. Или, что вы там еще подразумевали?

    В качестве инструментов вы выбрали «мутации» и «естественный отбор», но при этом оставили право за «Исполнителем Заказа» «Высшего Заказчика» (которые могут быть в одном Лице) «контролировать процесс и настраивать параметры».

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

    Отсюда следует что? То, что первопричина нашего Мира – субъективна. Иначе, надо придумывать идеи «Большого Взрыва», «возникновения Всего из Ничего», вечные циклы, вроде, «Хаос – Самоорганизация – Саморазрушение», или, там, всё «Само – Само – Само» и т.п. Убедительно? Как, по мне, то так себе… :)


  1. qqqgod
    08.07.2025 09:45

    Очень интересная статья, прям как будто прочитал фантастику))


  1. ncix
    08.07.2025 09:45

    Прочитал с огромным интересом!
    Много лет назад в детстве я тоже делал модели эволюции, на QuickBasic 4.5. Организмы имели несколько параметров - размер, скорость, что-то еще. Мир представлял собой зацикленное по двум краям поле, по которому снова и снова медленно проходила от края до края широкая полоса - типа световой день. На свету организмы заряжались "энергией", в темноте тратили. Двигаясь тоже тратили. Для рождения потомства нужна была энергия, мутации случайно немного меняли параметры потомства. В итоге популяция через много циклов почти всегда делилась на "растения", которые теряли способность двигаться и легко переживали темноту, и "животных" - быстрых и легких которые успешно бегали за солнцем. Растения создавали колонии заселяя потомками место вокруг себя. Животные - не очень устойчивые популяции, которые могли в процессе миграций вымереть полностью.
    Правда до взаимодействия организмов (поедания, скрещивания) я не добрался - начался институт а потом взрослая жизнь ))


  1. axion-1
    08.07.2025 09:45

    Почему бы в качестве loss функции не взять просто количество неправильных ответов, от 0 до 12?

    По поводу Python:

    Не потому, что Python сам по себе не особо быстр, а потому что он прозрачно поддерживает длинную арифметику, а в мире случайных формул вполне может появится, например, такая: x**967**9813.

    import numpy as np
    a = np.float32(10)
    a**967
    
    "<stdin>:1: RuntimeWarning: overflow encountered in scalar power
    np.float32(inf)"

    Правда пришлось бы предварительно парсить формулу чтобы обернуть числовые константы во float32, но это не так уж сложно.


  1. riv2
    08.07.2025 09:45

    Спасибо, было интересно почитать (͡°͜ʖ͡°)


  1. DmitryOlkhovoi
    08.07.2025 09:45

    Что-то про эволюцию помню писал)
    https://habr.com/ru/articles/498914/