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

Я читал об эволюции Дарвина в школе и тогда особо об этом не задумывался. Типичный пример того, как жирафы получили длинную шею, был интересен, но почему-то казалось, что процесс надуманный. Случайные мутации время от времени приводят к появлению жирафов с более длинной шеей. Жирафы с более длинной шеей имеют больше шансов на выживание, потому что они могут доставать более высокие ветки деревьев. Этим жирафам благоволит Природа — естественный отбор гарантирует, что они выживут, а их мутации перейдут в последующие поколения. И весь этот повторяющийся много раз процесс создает то, что мы сейчас имеем у жирафов с длинной шеей. Хотя все это звучит хорошо в теории, когда я впервые прочитал об этом, все же это звучало как небольшая выдумка. На самом деле для описания странностей эволюции придумана особая фраза — эволюция подобна тысяче обезьян, стучащих по клавиатуре и производящих произведения Шекспира!

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

В книге «Слепой часовщик» Докинз представляет две компьютерные программы, демонстрирующие процесс эволюции в упрощенной симуляции. Мы возьмем первую программу и воссоздадим ее в Clojure. Основная предпосылка этой программы проста — может ли случайность порождать порядок, если мы моделируем процессы эволюции? Могут ли тысячи обезьян, печатающих на клавиатуре в случайном порядке, создать что-то осмысленное, например, фразу из Шекспира? В книге Докинз использует фразу «Я ДУМАЮ, ЧТО ЭТО ПОХОЖЕ НА ЛАСКУ» — отсюда и название программы «Ласка». Фундаментальные процессы эволюции — это огромное количество воспроизведений и естественный отбор наиболее приспособленных из этой популяции потомства. Размножение в целом — это самовоспроизведение — создание копий организма. Но если каждая копия идентична своему источнику, то не будет вариаций и эволюции. Таким образом, пока организм копирует себя во время самовоспроизведения, он вносит в копию случайные небольшие мутации. Некоторые из этих мутаций могут в конечном итоге уменьшить шансы организма на выживание, в то время как небольшое их количество может помочь ему выжить и размножаться.

В нашей программе

  1. Мы начинаем со случайной строки букв и пробелов.
  2. В каждой итерации мы реплицируем эту строку и создаем большое количество дочерних популяций.
  3. В то время как строка реплицируется на основе случайного случая, мы вносим мутации в копию.
  4. Каждая строка в совокупности имеет оценку — насколько она далека от желаемой строки «Я ДУМАЮ, ЧТО ЭТО ПОХОЖЕ НА ЛАСКУ». Это просто разница между символами строки и целевой строки, суммированная. В нашем случае nature выберет вариант с минимальным баллом, поскольку он ближе всего к целевой строке.

Итак, давайте напишем код, чтобы посмотреть, все ли работает!

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

(def codons (conj (map char (range (int \A) (inc (int \Z)))) \space))

(defn rand-char []
  (rand-nth codons))

(defn rand-str [len]
  (apply str (map (fn [_] (rand-char))
            (range len))))

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

(defn in-range? [n r]
  (and (>= n
           (apply min r))
       (<= n
           (apply max r))))

(defn copy [s]
  (apply str (map (fn [c] 
                    (if (in-range? (rand-nth (range 100)) (range 40 46)) 
                        (rand-char) 
                        c)) 
                  s)))

Идея состоит в том, что мы выбираем случайное число от 0 до 99, и если это число находится между 40 и 46, мы вводим мутацию. В противном случае функция просто копирует тот же символ из исходной строки.

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

(defn clones [orig]
  (cons (copy orig)
        (lazy-seq (clones orig))))

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

(defn score [s t]
  (reduce + (map (fn [x y] (Math/abs (- (int x) (int y)))) s t)))

(defn survivor [pop t]
  (first (sort-by (fn [x] (score x t)) pop)))

Обе функции вычисляют свои результаты на основе цели t.

Все, что нам нужно сейчас, это собрать эти функции вместе.

(defn generations [orig t n]
  (loop [g 1
         s orig]
    (let [pop (take 1000 (clones s))]
      (if (or (> g n)
              (= s t))
        [g s]
        (do (println (str g " " s " " (score s t)))
            (recur (inc g)
                   (survivor pop t)))))))

Хорошо, теперь посмотрим, как обстоят дела с Шекспиром!

(def target "ME THINKS IT IS LIKE A WEASEL")

(println
 (generations (rand-str (count target))
              target
              400))

Начать со случайной строки той же длины, что и целевая строка, и ограничить наш эксперимент 400 поколениями.

Один запуск генерирует это:

1 PKGJCO HFFPUDWENZUPWFSUNZDPSV 505
2 PKGJCSJHFFPUDOENZUPWHSANZDPSV 441
3 PKGJCSJHFFPUDOE ZUPWHSANZDPSV 395
4 PGGJCSJHF PUDOE ZUPIHSANZDPSV 339
5 PGGJOSJHF PU IE ZUPIHSANYDPSV 298
6 PGGJOSJHF PU IE ZKPIHCANYDPFV 259
7 PGGJOSJHF PU IE ZKPIHC NBDPFV 209
8 PGGJOSJHF PU IT ZKPI C NBDPFV 156
9 PG JOSJHF PU IT ZKPI C NBDPFV 117
10 PG JOSJHT PU IT UKPI C SBDPFK 86
11 PG JOLJHS HU IT UKPI C SBDPFK 72
12 PG TOLJHS HU IT UKOI C SBDPFK 61
13 LG TOLJHS HU IT LKOI C SBDPFK 50
14 LG TOLNHS HU IT LKOD C SBDPFK 43
15 LG TGLNHS HU IT LKOD C SBDPFK 37
16 LG TGINHS HU IT LKOD C SBDPFK 34
17 LG TGINHS HU IT LKOD C SBDSFK 31
18 LG TGINHS HU IT LKOD C WBDSFK 27
19 LG TGINHS HU IT LKKD C WBDSFK 23
20 LG TGINHS HU IT LKKD C WBASFK 20
21 LG TGINKS HU IT LKKD C WBASFK 17
22 LG TGINKS HU IT LIKD C WBASFK 15
23 LE TGINKS HU IT LIKD C WBASFK 13
24 LE TGINKS HU IT LIKD C WEASFK 10
25 LE TGINKS HU IT LIKD A WEASFK 8
26 LE THINKS HT IT LIKD A WEASFK 6
27 LE THINKS HT IT LIKD A WEASEK 5
28 LE THINKS HT IS LIKD A WEASEK 4
29 LE THINKS HT IS LIKD A WEASEL 3
30 ME THINKS HT IS LIKD A WEASEL 2
31 ME THINKS IT IS LIKD A WEASEL 1
32 ME THINKS IT IS LIKD A WEASEL 1
33 ME THINKS IT IS LIKD A WEASEL 1
34 ME THINKS IT IS LIKD A WEASEL 1
35 ME THINKS IT IS LIKD A WEASEL 1
[36 ME THINKS IT IS LIKE A WEASEL]

В поколении 1 мы начинаем со строки PKGJCO HFFPUDWENZUPWFSUNZDPSV, которая имеет действительно большой балл 505 — слишком далеко от целевой строки. Но в поколении 2 мы видим видоизмененную строку PKGJCSJHFFPUDOENZUPWSANZDPSV с уменьшенным значением 441 — наиболее подходящим для поколения 2. В поколении 36 наша строка имеет значение 0!

Другой запуск выглядит так:

1 CIAA OMHFPNDHXLHREVNDOUXYAIWP 486
2 CIAAOOMHFPNDHXR RVVNDOUXYAIWP 416
3 CIAABOMHFPNDAXR RVVNDO XYAIWP 355
4 CIBABOMHF NDAXR RVVNDO XYAIWP 308
5 CIBABJMHF ND XR RUVNDO XYAPWP 262
6 CI ABJMHF ND XR RFVNDO XYAPWP 219
7 CI PBJMHF ND XR RFVN O XYAPWP 168
8 LI PBJMHF ND XR RFVN O XDAPWP 140
9 LI PHJMHN ND HR RFVN O XDAPWP 112
10 LI PHJMHN ND HR RFVN H XDAPFP 88
11 LI PHJMHN QS HR RFON H XDAPFP 69
12 LI PHJMHV QS HR RFOD H VDAPFP 59
13 LF PHJMHV IS HR RFOD H VFAPFP 48
14 LF WHJMHV IS HR RFOD B VFAPFP 41
15 LF WHJMHV IS HR LFOD B VFAPFP 35
16 LF WHJMHV IS HR LFOD B VFAPFL 31
17 LF WHJMHV IS HR LIOD B VFAPFL 28
18 LF WHJMHV IS HR LIKD B VFAPFL 24
19 LF WHJMHV IS HR LIKD B VFATFL 22
20 LF WHJMHS IS HR LIKD B VFATFL 19
21 LE WHJMIS IS HR LIKD B VFATFL 17
22 LE THJMIS IS HR LIKD B VFATFL 14
23 LE THJMKS IS HR LIKD B VFATFL 12
24 LE THJMKS IS IR LIKD B VFATFL 11
25 LE THJMKS IS IR LIKD B VEATFL 10
26 ME THJMKS IS IR LIKD B VEATFL 9
27 ME THJMKS IS IR LIKD A VEATFL 8
28 ME THIMKS IS IR LIKD A VEATFL 7
29 ME THIMKS IS IR LIKD A VEATEL 6
30 ME THIMKS IS IR LIKD A WEATEL 5
31 ME THIMKS IS IR LIKD A WEASEL 4
32 ME THIMKS IT IR LIKD A WEASEL 3
33 ME THIMKS IT IR LIKD A WEASEL 3
34 ME THIMKS IT IS LIKD A WEASEL 2
35 ME THIMKS IT IS LIKD A WEASEL 2
36 ME THIMKS IT IS LIKD A WEASEL 2
37 ME THIMKS IT IS LIKD A WEASEL 2
38 ME THIMKS IT IS LIKD A WEASEL 2
39 ME THINKS IT IS LIKD A WEASEL 1
40 ME THINKS IT IS LIKD A WEASEL 1
[41 ME THINKS IT IS LIKE A WEASEL]

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

Когда я впервые прочитал книгу и воссоздал эту программу на Turbo Pascal (да, именно с этого я начал свое путешествие по структурированному программированию), я продолжал запускать программу, чтобы увидеть, как случайный набор символов медленно, через множество поколений воспроизведений и выборок, стать целевой строкой. Существует целая область алгоритмов, основанных на этой самой базовой предпосылке — генетическом программировании и вычислениях, вдохновленных природой.

Однако следует отметить несколько вещей — эволюция на самом деле не работает таким образом; программа Weasel представляет собой очень общую и простую версию процесса. В природе мутации гораздо сложнее, и их выражение на фенотипе иногда очень тонкое. Выживание наиболее приспособленных всегда зависит не от эффекта мутации, а также от окружающей среды. Что еще более важно, естественный отбор — слепой процесс (отсюда и название Слепой часовщик). В приведенной выше программе мы хотели, чтобы она превратилась в целевую строку, и поэтому на каждом шаге мы соответственно вычисляли оценку. Но в естественной эволюции нет цели для достижения, выжившие зависят исключительно от того, что заставляет их приспосабливаться к текущей среде.

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

Если это не изгиб разума, то что?