image

Чтобы написать настоящую жизнь не нужно ничего, кроме того, чтобы победить самого себя полностью и целиком. Я продолжаю ее создавать и в последствии напишу большущую статью на эту тему, но, ё-мое, какая же это вещь в себе и как сложно 7 раз отмерить, один отрезать, чтобы потом отмерять еще 77 раз и в итоге не отрезать. Мерять, еще раз мерять! — вот девиз того, кто хочет создать что-то по-настоящему необычное. Кодить куда легче, чем думать. А думать, — это вообще больно и непривычно.

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

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

Вспомним, что скорее всего т.н. «жизнь» на Земле зародилась в результате хаотического взаимодействия химических элементов. Такой жизни никогда бы не получилось бы, не умей одна молекула тянуть электроны от другой или наоборот, расставаться с собственными. Сама эта ВОЗМОЖНОСТЬ только и обусловила то, что из элементов появились соединения, а из соединений первые репликанты ну и организмы. Следовательно, если мы хотим создать цифровую жизнь, мы должны ДАТЬ ВОЗМОЖНОСТЬ взаимодействия.

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

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

Когда пишешь хаотический код, то неизбежно наталкиваешься на то, что нужна развилка. Рандом-опыт. Т.е. либо дальше двигаться хаотично, либо дать возможность образования опыта. Т.е. дать возможность не просто частицам соударяться, но и взаимодействовать. Мы ищем аналог именно биологической жизни, которая немыслима без передачи и регенерации опыта. РНК/ДНК — это записанный опыт, который передается.

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

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

Предположим, мы рандомно сгенерировали исполняемую строчку кода в ответ на рандомное входящее численное множество. Можно предположить, что ответная генерация, или реакция — это и есть опыт. Если что-то реагирует стабильно одинаково на одинаковые входящие сигналы, можно сказать, что это что-то использует опыт. Следовательно, сгенерированную строку хотелось бы запомнить. Но этого нельзя делать напрямую! Нельзя просто взять эту строку и в обязательном порядке запихнуть куда-нибудь в переменную массива экземпляра класса и начать формировать таким образом некую библиотеку реакций, выписывая нечто вроде IF [вход.множество in массив_памяти] THEN [массив.памяти->реакция]. Нельзя! Потому что не факт, что именно так работают «биологические системы памяти». И не факт, что их построение изначально было таким же, каким мы видим его сейчас.

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

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

Чтобы было понятна проблема, представим, что у нас есть 2 возможных действия в коде. Нельзя прямо указать, какой из них выбрать, ибо это будет детерминацией. А если всё хаотизировать, то надо использовать вероятности. Только какие? 50/50? 30/70? 80/20? Предположим, что при прочих равных 50/50 является действительно самым неопределенным отношением. Тогда код будет выглядеть как-то так:

if random.random() >= 0.5:
      действие1
else:
     действие2


Что не так с этим кодом? А то, что при каждой итерации цикла, этот код будет ВСЕГДА хаотично выбирать либо первое действие, либо второе. Другими словами, такой хаос никогда не структурируется, ибо у него даже нет такой теоретической возможности. По большому счету здесь мы имеем дело с той же самой детерминацией. Выход из этого положения есть. Он состоит в том, что сами действие1 и действие2 должны изменять условия при которых они применяются. Здесь есть, я думаю, разные варианты, но я хотел бы предложить следующий.

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

В нашем случае принцип приоритетов позволит нам менять не потоки на исполнение на ядре, а части кода, куда будет передано управление. И если мы сделаем одну часть кода, как «рандомную», а вторую, как «опытную», то их поочередная смена и будет тем самым решением, которое мы ищем.

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

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

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

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

import random

deter_numb = 69
p_deter = 0.01
p_random = 0.99
iteration = 1

while True:
        print('iter = ',iteration)
        print('p_rand =  ', p_random)         
        print('p_deter = ', p_deter)
        input()
        if p_random > random.random():
            p_random-=0.01                     # изменение приоритета
            p_deter+=0.01
            print(random.random())    # действия рандома
        if p_deter > random.random():       
            p_deter-=0.01                      # изменение приоритета
            p_random+=0.01
            print(deter_numb)            # действия опыта
        iteration+=1


Я призываю попробовать запустить этот скрипт у себя и посмотреть на то, как он работает. Это самобалансирующийся код. В качестве действия1 выступает
print(random.random())
— т.е. просто вывод случайного числа, а действия 2 —
print(deter_numb)
— вывод детерминированного числа. В коде, на их месте будут действия, принты тут я сделал просто для того, чтобы наглядно показать принцип.

При переходе управления в первое условие(рандомная часть кода), код сам себе снижает вероятность повторения (p_random-=0.01 — декремент самого себя) и одновременно повышает вероятность исполнения части кода, где применяется опыт (p_deter+=0.01 — инкремент чужой вероятности). Тоже самое происходит при передаче управление во второе условие. Таким образом оба эти куска кода, выполняясь, сами себя «тормозят» и одновременно «разгоняют» другую «конкурирующую» часть кода.

Обратите внимание, что изначальная вероятность того, что управление будет передано в рандомную часть кода, равна 99%, а в опытную — 1%. Это чтобы показать, что система балансируется даже при таких кардинально разных вероятностях. При исполнении кода будет видно, как это соотношение уже через сотню итераций придет к 50/50, образовав нормальное распределение с явной вершиной в 0.5 и рабочей окрестностью примерно в 6% (на следующих 3-5 сотнях итерациях).

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

Здесь же видна еще особенность — на экран выводится то случайное число, то детерминированное (69), то они оба сразу, что означает, что система может либо применить опыт, а потом действовать рандомно, либо наоборот, либо только применить опыт, либо только поступить рандомно. Иными словами, само исполнение кода остается рандомным, но мы дали возможность системе применять опыт, т.е. мы избавились от детерминации при принятии решения о действии, что собственно нам и нужно было.

В следующей статье мы рассмотрим само образование рандомных действий и опыта. Ну или чего-то в отдельности, посмотрим, как пойдет.