Вы находитесь на странице: 1из 161

«Аллен Дауни продолжает применять свою превосходную философию использования

программирования, чтобы внедрять, иллюстрировать и улучшать изучение комплексных


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

«Благодаря доступному материалу и конкретным примерам эта книга


стала неотъемлемой частью моего учебного курса».
– Эрик Ма, исследователь, Институт биомедицинских исследований Novartis

Наука о поведении сложных систем использует различные методы в своей


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

С этой книгой вы:


• научитесь работать с массивами NumPy и методами SciPy, включая базо-
вую обработку сигналов и быстрое преобразование Фурье;
• изучите абстрактные модели сложных физических систем, в том числе
степенные законы, фракталы и розовый шум;

Изучение
• получите тетради Jupyter, в которых содержится начальный код и реше-
ния, которые помогут вам повторно реализовать и расширить исходные
эксперименты по теории сложности, а также познакомитесь с такими
моделями вычислений, как тьюрмиты, машины Тьюринга и клеточные

сложных систем
автоматы;
• исследуете философию науки, включая природу научных законов,
теорию выбора, реализм и инструментализм.

с помощью Python
Аллен Б. Дауни (Allen B. Downey) — профессор информатики в колледже имени Франклина В. Олина
и автор серии бесплатных учебников с открытым исходным кодом, относящихся к программному
обеспечению и науке о данных, включая Think Python, Think Bayes и Think Complexity, изданных
O’Reilly Media. Его блог «Probably Overthinking It» содержит статьи о байесовской вероятности и
статистике. Он имеет докторскую степень по информатике и степени магистра и бакалавра наук.

ISBN 978-5-97060-712-1
Аллен Б. Дауни
Интернет-магазин:
www.dmkpress.com
Оптовая продажа:
КТК «Галактика»
books@alians-kniga.ru 9 785970 607121
www.дмк.рф
Аллен Б. Дауни

Изучение сложных систем


с помощью Python
Think Complexity
Complexity Science
and Computational Modeling

Second Edition

Allen B. Downey

 BEIJING • BOSTON • FARNHAM • SEBASTOPOL • TOKYO


Изучение сложных
систем с помощью Python
Аллен Б. Дауни

Москва, 2019
УДК 004.94
ББК 32.972
Д12

Аллен Б. Дауни
Д12 Изучение сложных систем с помощью Python / пер. с анг. Д. А. Беликова. – М.: ДМК
Пресс, 2019. – 160 с.: ил. 

ISBN 978-5-97060-712-1

Наука о сложных системах – это междисциплинарная область на стыке математики, ин-


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

УДК  004.94
ББК  32.972

Original English language edition published by O’Reilly Media, Inc. Copyright © 2018 Allen B. Downey.
All rights reserved. Russian-language edition copyright © 2019 by DMK Press. All rights reserved.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было
форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
Материал, изложенный в данной книге, многократно проверен. Но, поскольку вероятность технических
ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность
приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки,
связанные с использованием книги.

ISBN 978-1-492-04020-0 (анг.) Copyright © 2018 Allen B. Downey. All rights reserved.
ISBN 978-5-97060-712-1 (рус.) ©  Оформление, издание, перевод, ДМК Пресс, 2019
Содержание

Предисловие..................................................................................................................................................................................................... 8
Для кого эта книга?................................................................................................................................................................................ 9
Изменения после выхода первого издания................................................................................................................................ 9
Использование кода............................................................................................................................................................................10
Обозначения, используемые в этой книге..................................................................................................................................11
O'Reilly Safari...........................................................................................................................................................................................11
Как с нами связаться...........................................................................................................................................................................12
Список лиц, принимавших участие в работе над книгой.....................................................................................................12
Об авторе..................................................................................................................................................................................................13
Колофон....................................................................................................................................................................................................13

Глава 1. Наука о сложных системах......................................................................................................................................................14


Меняющиеся критерии науки..........................................................................................................................................................15
Оси научных моделей.........................................................................................................................................................................16
Разные модели для разных целей.................................................................................................................................................17
Инженерия сложных систем.............................................................................................................................................................18
Доктрина сложных систем.................................................................................................................................................................19

Глава 2. Графы.................................................................................................................................................................................................21
Что такое графы?...................................................................................................................................................................................21
NetworkX...................................................................................................................................................................................................23
Случайные графы..................................................................................................................................................................................24
Генерация графов.................................................................................................................................................................................25
Связные графы.......................................................................................................................................................................................26
Генерация графов Эрдёша–Реньи.................................................................................................................................................27
Вероятность связности........................................................................................................................................................................29
Анализ алгоритмов графов...............................................................................................................................................................30
Упражнения.............................................................................................................................................................................................31

Глава 3. Графы «Мир тесен»......................................................................................................................................................................33


Стэнли Милгрэм.....................................................................................................................................................................................33
Ваттс и Строгац......................................................................................................................................................................................34
Кольцевая решетка...............................................................................................................................................................................34
Графы Ваттса–Строгаца......................................................................................................................................................................36
Кластеризация........................................................................................................................................................................................37
Длина кратчайшего пути....................................................................................................................................................................38
Эксперимент Ваттса–Строгаца........................................................................................................................................................39
Что это за объяснение?.......................................................................................................................................................................40
Поиск в ширину.....................................................................................................................................................................................41
Алгоритм Дейкстры..............................................................................................................................................................................43
Упражнения.............................................................................................................................................................................................44

Глава 4. Безмасштабные сети...................................................................................................................................................................46


Данные социальных сетей................................................................................................................................................................46
Модель Ваттса–Строгаца...................................................................................................................................................................48

Содержание  5
Степень......................................................................................................................................................................................................48
Распределения с тяжелыми хвостами..........................................................................................................................................50
Модель Барабаши–Альберта............................................................................................................................................................51
Генерация графов Барабаши–Альберта......................................................................................................................................53
Интегральные распределения.........................................................................................................................................................54
Объяснительные модели....................................................................................................................................................................56
Упражнения.............................................................................................................................................................................................57

Глава 5. Клеточные автоматы..................................................................................................................................................................59


Простой клеточный автомат.............................................................................................................................................................59
Эксперимент Вольфрама...................................................................................................................................................................59
Классификация клеточных автоматов..........................................................................................................................................60
Хаотичность.............................................................................................................................................................................................61
Детерминизм...........................................................................................................................................................................................62
Космические корабли..........................................................................................................................................................................64
Универсальность....................................................................................................................................................................................65
Фальсифицируемость..........................................................................................................................................................................66
Что это за модель?................................................................................................................................................................................67
Реализация клеточных автоматов..................................................................................................................................................68
Взаимная корреляция.........................................................................................................................................................................69
Таблицы клеточных автоматов........................................................................................................................................................71
Упражнения.............................................................................................................................................................................................71

Глава 6. Игра «Жизнь».................................................................................................................................................................................73


Игра «Жизнь» Конвея..........................................................................................................................................................................73
Конструкции игры «Жизнь»..............................................................................................................................................................74
Гипотеза Конвея.....................................................................................................................................................................................75
Реализм.....................................................................................................................................................................................................76
Инструментализм..................................................................................................................................................................................77
Реализация игры «Жизнь».................................................................................................................................................................78
Упражнения.............................................................................................................................................................................................80

Глава 7. Физическое моделирование....................................................................................................................................................82


Диффузия.................................................................................................................................................................................................82
Реакция диффузии................................................................................................................................................................................83
Перколяция..............................................................................................................................................................................................86
Фазовый переход..................................................................................................................................................................................88
Фракталы..................................................................................................................................................................................................89
Фракталы и перколяционные модели..........................................................................................................................................91
Упражнения.............................................................................................................................................................................................92

Глава 8. Самоорганизованная критичность.......................................................................................................................................94


Критические системы..........................................................................................................................................................................94
Песчаные кучи........................................................................................................................................................................................95
Реализация песчаной кучи................................................................................................................................................................95
Распределения с тяжелыми хвостами..........................................................................................................................................98
Фракталы............................................................................................................................................................................................... 100
Розовый шум........................................................................................................................................................................................ 103
Звук песка............................................................................................................................................................................................. 104
Редукционизм и холизм.................................................................................................................................................................. 105

6  Изучение сложных систем с помощью Python


Самоорганизованная критичность, причинность и прогнозирование......................................................................... 107
Упражнения.......................................................................................................................................................................................... 108

Глава 9. Агент-ориентированные модели........................................................................................................................................110


Модель Шеллинга............................................................................................................................................................................... 110
Реализация модели Шеллинга...................................................................................................................................................... 111
Сегрегация............................................................................................................................................................................................ 113
Sugarscape............................................................................................................................................................................................. 114
Имущественное неравенство........................................................................................................................................................ 116
Реализация Sugarscape.................................................................................................................................................................... 117
Миграция и волновое поведение................................................................................................................................................ 119
Эмерджентность................................................................................................................................................................................. 119
Упражнения.......................................................................................................................................................................................... 121

Глава 10. Стаи, стада и пробки..............................................................................................................................................................122


Пробки.................................................................................................................................................................................................... 122
Случайное возмущение................................................................................................................................................................... 124
Boid.......................................................................................................................................................................................................... 125
Алгоритм Boids.................................................................................................................................................................................... 126
Разрешение конфликтов................................................................................................................................................................. 128
Эмерджентность и свобода воли................................................................................................................................................. 129
Упражнения.......................................................................................................................................................................................... 130

Глава 11. Эволюция...................................................................................................................................................................................131


Моделирование эволюции............................................................................................................................................................. 131
Адаптивный ландшафт..................................................................................................................................................................... 132
Агенты..................................................................................................................................................................................................... 133
Моделирование.................................................................................................................................................................................. 133
Нет дифференциации....................................................................................................................................................................... 134
Свидетельство эволюции................................................................................................................................................................ 135
Дифференциальное выживание.................................................................................................................................................. 137
Мутация.................................................................................................................................................................................................. 138
Видообразование.............................................................................................................................................................................. 140
Резюме.................................................................................................................................................................................................... 142
Упражнения.......................................................................................................................................................................................... 143

Глава 12. Эволюция кооперации.........................................................................................................................................................144


Дилемма заключенного.................................................................................................................................................................. 144
Проблема альтруизма...................................................................................................................................................................... 145
Чемпионаты по дилемме заключенного.................................................................................................................................. 146
Моделирование эволюции кооперации................................................................................................................................... 147
Класс Tournament............................................................................................................................................................................... 148
Класс Simulation................................................................................................................................................................................. 150
Результаты............................................................................................................................................................................................. 150
Выводы................................................................................................................................................................................................... 153
Упражнения.......................................................................................................................................................................................... 154

Приложение A. Список литературы....................................................................................................................................................156

Указатель....................................................................................................................................................................................................... 157

Содержание  7
Предисловие
Наука о  сложных системах – ​это междисциплинарная область на  стыке математики, информати-
ки и естествознания, которая фокусируется на сложных системах, представляющих собой системы
со множеством взаимодействующих компонентов.
Одним из основных инструментов науки о сложных системах являются дискретные модели, вклю-
чая сети и графы, клеточные автоматы и агентное моделирование. Эти инструменты используются
в естественных и общественных науках, а иногда в искусстве и гуманитарных науках.
Для получения обзора посетите страницу https://thinkcomplex.com/complex.
Почему вам следует знать о науке о сложных системах? Вот несколько причин:
• наука о сложных системах полезна, особенно если необходимо объяснить поведение природ-
ных и  социальных систем. Начиная с  Ньютона, математическая физика была сосредоточена
на системах с небольшим количеством компонентов и простыми взаимодействиями. Эти моде-
ли эффективны для некоторых приложений, таких как небесная механика, и менее полезны для
других, таких как экономика. Наука о сложных системах обеспечивает разнообразный и адапти-
руемый инструментарий моделирования;
• множество основных результатов науки о  сложных системах удивительно; тема, которая по-
вторяется на протяжении данной книги, состоит в том, что поведение простых моделей может
быть сложным, а следствием этого является то, что иногда можно объяснить сложное поведение
в реальном мире, используя простые модели;
• как я  объясняю в  главе 1, наука о  сложных системах находится в  центре медленного сдвига
в научной деятельности и изменения того, что мы считаем наукой;
• изучение науки о  сложных системах дает возможность узнать о  разнообразных физических
и социальных системах, развить и применить навыки программирования и поразмыслить над
фундаментальными вопросами философии науки.
Прочитав эту книгу и поработав над упражнениями, у вас будет шанс изучить темы и идеи, с которы-
ми вы вряд ли бы столкнулись, попрактиковаться в программировании на Python и узнать больше
о структурах данных и алгоритмах.
Данная книга включает в себя слудующие разделы.

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

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

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

8  Изучение сложных систем с помощью Python


Упражнения и решения
В конце каждой главы я предлагаю упражнения, над которыми вы, возможно, захотите поработать,
и решения.
Для большинства ссылок в этой книге я использую перенаправление URL. Этот механизм имеет не-
достаток, заключающийся в том, что он скрывает назначение ссылки, но делает URL-адреса короче
и менее навязчивыми. Кроме того, и что более важно, это позволяет мне обновлять ссылки, не об-
новляя содержимого книги. Если вы найдете неработающую ссылку, пожалуйста, дайте мне знать,
и я изменю перенаправление.

Для кого эта книга?


Примеры и вспомогательный код, приведенные в этой книге, написаны на Python. Вы должны знать
ядро Python и быть знакомы с его объектно-ориентированными функциями, в частности с использо-
ванием и определением классов.
Если вы еще незнакомы с Python, то можете начать с книги Think Python, которая подходит для лю-
дей, никогда прежде не занимавшихся программированием. Если у вас есть опыт программирова-
ния на другом языке, есть много хороших книг по Python, а также онлайн-ресурсы.
Я использую NumPy, SciPy и NetworkX на протяжении всей книги. Если вы уже знакомы с этими биб­
лиотеками, это здорово, но я также буду рассказывать о них по мере их появления.
Я предполагаю, что читатель имеет определенные познания в математике: в нескольких местах я ис-
пользую логарифмы, а в одном примере – ​векторы. На этом все.

Изменения после выхода первого издания


Во втором издании я добавил две главы, одну об эволюции, а другую об эволюции кооперации.
В первом издании каждая глава давала справочную информацию по теме и предлагала экспери-
менты, которые может выполнить читатель. Во втором издании я провел эти эксперименты. Каждая
глава представляет реализацию и результаты в качестве практического примера, а затем предлагает
дополнительные эксперименты для читателя.
Во втором издании я заменил часть своего кода стандартными библиотеками, такими как NumPy
и NetworkX. Результат получился более кратким и эффективным, что дает читателям возможность
изучить эти библиотеки.
Кроме того, блокноты Jupyter являются новыми. Для каждой главы есть два блокнота: один со-
держит код, приведенный в главе, пояснительный текст и упражнения; другой содержит решения
упражнений.
Наконец, все поддерживающее программное обеспечение было обновлено до Python 3 (но боль-
шая часть его работает без изменений в Python 2).

Предисловие  9
Использование кода
Весь код, использованный в этой книге, доступен в Git-репозитории на GitHub: https://thinkcomplex.
com/repo. Если вы незнакомы с Git, то это система контроля версий, которая позволяет отслеживать
файлы, составляющие проект. Коллекция файлов, находящихся под контролем Git, называется «ре-
позиторий». GitHub – ​это хостинг, который предоставляет хранилище для Git-репозиториев и удоб-
ный веб-интерфейс.
Домашняя страница GitHub моего репозитория предоставляет несколько способов работы с кодом:
• вы можете создать копию моего репозитория, нажав кнопку Fork в правом верхнем углу. Если
у вас еще нет учетной записи GitHub, вам нужно ее создать. После этого у вас будет собственный
репозиторий на GitHub, который вы сможете использовать для отслеживания кода, написанного
вами во время работы над этой книгой. Затем вы можете клонировать репозиторий, что означа-
ет, что вы копируете файлы на свой компьютер;
• вы можете клонировать мой репозиторий, не создавая ответвления; то есть можно сделать ко-
пию моего репозитория на вашем компьютере. Для этого вам не нужна учетная запись GitHub,
однако вы не сможете записать свои изменения обратно в GitHub;
• если вы вообще не хотите использовать Git, то можете загрузить файлы в ZIP-файл, воспользо-
вавшись зеленой кнопкой с надписью Clone or download (Клонировать или скачать).
Работая над этой книгой, я использовал Anaconda из Continuum Analytics, являющийся бесплатным
дистрибутивом Python и включающий в себя все пакеты, которые понадобятся вам для запуска кода
(и многого другого). Я нашел, что Anaconda прост в установке. По умолчанию он выполняет установку
на уровне пользователя, а не на уровне системы, поэтому вам не нужны права администратора. И он
поддерживает как Python 2, так и Python 3. Вы можете скачать Anaconda с https://continentuum.io/
downloads.
Репозиторий включает в себя как сценарии Python, так и блокноты Jupyter. Если вы ранее не поль-
зовались Jupyter, то можете прочитать о нем на странице https://jupyter.org.
Существует три способа работы с блокнотами Jupyter.

Запуск Jupyter на своем компьютере


Если вы установили Anaconda, то можете установить Jupyter, выполнив приведенную ниже ко-
манду в терминале или окне команд:
$ conda install jupyter
Перед тем как запустить Jupyter, вы должны перейти в каталог, содержащий код:
$ cd ThinkComplexity2/code
А затем запустить сервер Jupyter:
$ jupyter notebook
Когда вы запускаете сервер, он должен запустить веб-браузер по умолчанию или создать новую
вкладку в открытом окне браузера. Затем вы можете открыть и запустить блокноты.

Запуск Jupyter на Binder


Binder  – ​это сервис, который запускает Jupyter на  виртуальной машине. Если вы перейде-
те по этой ссылке, https://thinkcomplex.com/binder, вы должны попасть на домашнюю страницу
Jupyter с блокнотами данной книги, вспомогательными данными и сценариями.

10  Изучение сложных систем с помощью Python


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

Просмотр блокнотов на GitHub


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

Удачи и приятного времяпровождения!


Аллен Б. Дауни (Allen B. Downey)
профессор информатики
Инженерный колледж им. Франка В. Олина
Нидхэм, Массачусетс

Обозначения, используемые в этой книге


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

O'Reilly Safari
Safari (ранее Safari Books Online) – ​это основанная на членстве учебная и справочная платформа
для предприятий, правительства, работников образования и частных лиц.
Участники имеют доступ к  тысячам книг, обучающих видео, учебных программ, интерактивных
учебных пособий и  специально отобранных списков воспроизведения от  более чем 250 изда-
тельств, включая O'Reilly Media, Harvard Business Review, Prentice Hall Professional, Addison-Wesley
Professional, Microsoft Press, Sams, Que, Peachpit Press, Adobe, Focal Press, Cisco Press, John Wiley &
Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New
Riders, McGraw-Hill, Jones & Bartlett и Course Technology. Для получения дополнительной информа-
ции, пожалуйста, посетите http://oreilly.com/safari.

Предисловие  11
Как с нами связаться
Пожалуйста, направляйте комментарии и вопросы относительно данной книги издателю:
O'Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
800‑998‑9938 (в США или Канаде)
707‑829‑0515 (международный или местный)
707‑829‑0104 (факс)
Чтобы оставить комментарий или задать технические вопросы по этой книге, отправьте электронное
письмо по адресу bookquestions@oreilly.com.
Для получения дополнительной информации о наших книгах, курсах, конференциях и новостях по-
сетите наш сайт по адресу http://www.oreilly.com.
Ищите нас на Facebook: http://facebook.com/oreilly.
Подпишитесь на нас в Твиттере: http://twitter.com/oreillymedia.
Смотрите нас на канале YouTube: http://www.youtube.com/oreillymedia.

Список лиц, принимавших участие в работе над книгой


Если у  вас есть предложение или исправление, отправьте электронное письмо на  downey@
allendowney.com. Если я  внесу изменения на  основе ваших отзывов, я  добавлю вас в  список лиц,
принимавших участие в работе над книгой (если вы попросите об этом).
Дайте мне знать, с какой версией книги вы работаете и в каком формате. Если вы приведете хотя бы
часть предложения, в котором появляется ошибка, это облегчит поиск. Номера страниц и разделов
тоже подойдут, но с ними не так просто работать.
Спасибо!
• Джон Харли, Джефф Стэнтон, Колден Руло и  Кертик Оманакуттан (John Harley, Jeff Stanton,
Colden Rouleau и Keerthik Omanakuttan) – ​студенты, изучающие вычислительное моделирова-
ние, указали на опечатки;
• Хосе Оскар Мур-Миранда (Jose Oscar Mur-Miranda) обнаружил несколько опечаток;
• Филипп Ло, Кори Долфин, Ноам Рубин и Джулиан Чейпек (Phillip Loh, Corey Dolphin, Noam Rubin
и Julian Ceipek) нашли опечатки и сделали полезные предложения;
• Себастьян Шёнер (Sebastian Schöner) прислал две страницы с исправлениями!
• Филипп Марек (Philipp Marek) прислал ряд исправлений;
• Джейсон Вудар (Jason Woodard) вместе со мной преподавал поведение сложных систем в Ин-
женерном колледже им.  Франка В. Олина, познакомил меня с  моделями NK и  сделал много
полезных предложений и исправлений;
• Дави Пост (Davi Post) прислал ряд исправлений и предложений;
• Грэм Тейлор (Graham Taylor) отправил запрос на принятие изменений на GitHub, что позволило
исправить множество опечаток.

12  Изучение сложных систем с помощью Python


Я особенно хотел  бы поблагодарить технических рецензентов этой книги, которые внесли много
полезных предложений: Винсента Найта (Vincent Knight) и Эрика Ма (Eric Ma).
В числе других лиц, сообщивших об ошибках: Ричард Холландс, Мухаммед Наджми бин Ахмад Заби-
ди, Алекс Хантман и Джонатан Харфорд (Richard Hollands, Muhammad Najmi bin Ahmad Zabidi, Alex
Hantman и Jonathan Harford).

Об авторе
Аллен Б. Дауни – ​профессор информатики в  колледже имени Франклина В. Олина и  автор серии
бесплатных учебников с  открытым исходным кодом, относящихся к  программному обеспечению
и науке о данных, включая Think Python, Think Bayes и Think Complexity, изданных O'Reilly Media. Его
блог «Probably Overthinking It» содержит статьи о байесовской вероятности и статистике. Он имеет
докторскую степень по информатике (Калифорнийский университет в Беркли) и степени магистра
и  бакалавра наук (Массачусетский технологический университет). Он живет недалеко от  Бостона,
штат Массачусетс, со своей женой и двумя дочерьми.

Колофон
Птица, изображенная на обложке Think Complexity, – ​это орел-яйцеед (Ictinaetus malayensis), един-
ственный вид в  своем роде. Они встречаются в  тропической Азии, а  именно в  некоторых частях
Бирмы, Индии, южного Китая, Тайваня и Малайского полуострова. Эти орлы предпочитают лесистую
гористую местность. Они обитают высоко на деревьях и там строят большие гнезда (шириной от 3
до 4 футов).
У этих птиц черное оперение (хотя молодые орлы темно-коричневые), желтые ноги и короткий изо-
гнутый клюв. Орлы-яйцееды – ​крупные птицы, в среднем от 2 до 3 футов в длину, с массивным раз-
махом крыльев в 5 футов. В полете орлы выделяются не только своим цветом и размером, но и мед-
ленной скоростью скольжения, с которой они движутся над пологом леса.
Период размножения происходит где-то между ноябрем и маем (в зависимости от широты). Орлы
совершают крутое пикирование в  воздухе, а  потом поднимаются, и  все это на  высокой скорости.
Брачные пары также будут преследовать друг друга среди деревьев. Обычно они откладывают толь-
ко одно или два яйца одновременно. Диета орла-яйцееда состоит из мелких млекопитающих (кото-
рых они будут добывать с земли), а также из мелких птиц и яиц.
На самом деле орлы-яйцееды являются ненасытными гнездовыми хищниками и  имеют уникаль-
ную охотничью привычку – ​забирать целое гнездо и уносить яйца или птенцов к себе, чтобы потом
съесть. Поскольку когти орла-яйцееда не так резко изогнуты, как у других хищных птиц, им легче это
сделать.
Многие животные, изображенные на обложках изданий O'Reilly, находятся под угрозой исчезно-
вения; все они важны для мира. Чтобы узнать больше о том, как вы можете помочь, посетите сайт
https://www.oreilly.com/animals.csp.
Изображение на  обложке взято из  Meyers Klein Lexicon. Шрифты для обложки – ​URW Typewriter
и  Guardian Sans. Шрифт текста  – ​Adobe Minion Pro; шрифт заголовка Adobe Myriad Condensed,
а шрифт листингов кода – ​Ubuntu Mono от Dalton Maag.

Предисловие  13
Глава 1.
Наука о сложных системах
Наука о сложных системах относительно нова; она стала узнаваемой как область, и ей было дано
имя в 80-х годах. Но ее новизна состоит не в том, что она применяет инструменты науки к новому
предмету, а в том, что использует разные инструменты, допускает различные виды работы и в конеч-
ном итоге меняет то, что мы подразумеваем под словом «наука».
Чтобы продемонстрировать разницу, я начну с примера классической науки: предположим, кто-то
спрашивает вас, почему планетарные орбиты эллиптические. Вы можете вызвать закон всемирного
тяготения Ньютона и использовать его для написания дифференциального уравнения, которое опи-
сывает движение планет. Затем вы можете решить дифференциальное уравнение и показать, что
решение является эллипсом. Что и требовалось доказать!
Большинство людей находит такое объяснение удовлетворительным. Оно включает в себя матема-
тический вывод – ​так что оно имеет некоторую строгость доказательства – ​и объясняет конкретное
наблюдение, эллиптические орбиты, ссылаясь на общий принцип, гравитацию.
Позвольте мне сопоставить это с другим типом объяснения. Предположим, вы переезжаете в такой
город, как Детройт, в котором существует расовая сегрегация, и хотите знать, почему это так. Если вы
проведете исследование, то можете найти статью Томаса Шеллинга «Динамические модели сегрега-
ции», в которой предлагается простая модель расовой сегрегации.
Вот мое описание этой модели, которое приводится в главе 9:

Модель города Шеллинга представляет собой массив ячеек, где каждая ячейка представляет
собой дом.
Дома заняты двумя типами «агентов», обозначенных красным и  синим, в  примерно равных
количествах. Около 10 % домов пусты. В  любой момент времени агент может быть счастлив
или несчастен, в зависимости от других агентов по соседству. В одной версии модели агенты
счастливы, если у них есть как минимум два соседа, таких как они, и недовольны, если у них
есть один сосед или ноль соседей. Симуляция продолжается, выбирая агента наугад и проверяя,
счастлив ли он. Если это так, ничего не происходит; если нет, агент случайным образом выбирает
одну из незанятых ячеек и переезжает.

Если вы начнете со смоделированного города, который полностью несегрегирован, и запустите мо-


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

14  Изучение сложных систем с помощью Python


Очевидно, что модель Шеллинга очень абстрактна, то  есть не  реалистична. Таким образом, у  вас
может возникнуть соблазн сказать, что люди сложнее, чем планеты. Но это не может быть правдой.
В конце концов, на некоторых планетах есть люди, поэтому они должны быть сложнее, чем люди.
Обе системы сложны, и обе модели основаны на упрощениях. Например, в модели движения планет
мы включаем силы между планетой и ее солнцем и игнорируем взаимодействия между планетами.
В модель Шеллинга мы включаем индивидуальные решения, основанные на местной информации,
и игнорируем все остальные аспекты человеческого поведения.
Но есть различия в степени. Что касается движения планет, мы можем защитить модель, показав, что
игнорируемые нами силы меньше, чем те, которые мы включаем. И мы можем расширить модель,
чтобы включить другие взаимодействия и показать, что эффект невелик.
Что касается модели Шеллинга, то здесь сложнее обосновать упрощения.
Еще одно отличие состоит в том, что модель Шеллинга не  обращается к  каким-либо физическим
законам и использует только простые вычисления, а не математический вывод. Такие модели, как
модель Шеллинга, не похожи на классическую науку, и многие люди находят их менее убедитель-
ными, по крайней мере вначале. Но я попытаюсь продемонстрировать, что эти модели выполняют
полезную работу, в  том числе предсказание, объяснение и  проектирование. Одна из  целей этой
книги – ​объяснить, как они это делают.

Меняющиеся критерии науки


Наука о сложных системах – ​это не просто другой набор моделей; это также постепенное изменение
критериев, по которым оцениваются модели, и типов моделей, которые считаются приемлемыми.
Например, классические модели, как правило, основаны на  законах, выражаются в  форме урав-
нений и  решаются с  помощью математического вывода. Модели, попадающие в  зону поведения
сложных систем, часто основаны на  правилах, выражаются в  виде вычислений и  моделируются,
а не анализируются.
Не все считают эти модели удовлетворительными. Например, в своей книге «Sync» Стивен Строгац
пишет о модели спонтанной синхронизации у некоторых видов светлячков.
Он представляет моделирование, которое демонстрирует данный феномен, но затем пишет:

Я повторял моделирование десятки раз с другими случайными начальными условиями и дру-


гим числом генераторов. Каждый раз синхронизация. […] Задача теперь состояла в том, чтобы
доказать это.
Только железное доказательство продемонстрировало  бы, чего никогда не  смог  бы сделать
ни один компьютер, что синхронизация неизбежна; и лучший вид доказательства прояснит, по-
чему она неизбежна.

Строгац – ​математик, поэтому его энтузиазм в  отношении доказательств понятен, но  его доказа-
тельство не касается того, что, на мой взгляд, является наиболее интересной частью данного явле-
ния. Чтобы доказать, что «синхронизация была неизбежна», Строгац делает несколько упрощающих
предположений, в частности что каждый светлячок может видеть всех остальных.
На мой взгляд, интереснее объяснить, как может синхронизироваться целая долина светлячков, не-
смотря на то что они не могут видеть друг друга. Как этот вид глобального поведения возникает
из локальных взаимодействий, является предметом главы 9. При объяснении этих феноменов часто
используют агентные модели, которые исследуют (способами, которые были бы затруднительны или
невозможны при математическом анализе) условия, разрешающие или предотвращающие синхро-
низацию.

Глава 1. Наука о сложных системах   15


Я ученый, поэтому мой энтузиазм относительно вычислительных моделей, вероятно, не удивителен.
Я  не  хочу сказать, что Строгац не  прав, скорее, что люди имеют разные мнения по  поводу, какие
вопросы задавать и какие инструменты использовать, чтобы ответить на них. Эти мнения основаны
на оценочных суждениях, поэтому нет оснований ожидать согласия.
Тем не менее среди ученых существует грубое единодушие в отношении того, какие модели счи-
таются хорошей наукой, а какие – ​пограничной наукой, лженаукой или не являются наукой вовсе.
Основной тезис этой книги состоит в том, что критерии, на которых основан этот консенсус, со вре-
менем меняются и  что появление науки о  сложных системах отражает постепенный сдвиг в  этих
критериях.

Оси научных моделей


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

на основе уравнений → на основе моделирования;


анализ → расчет.

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


знали, что дальше, но некоторые из них могут не иметь смысла, пока вы не увидите примеры, при-
веденные в этой книге.

Непрерывный → дискретный
Классические модели, как правило, основаны на непрерывной математике, такой как матема-
тический анализ. Модели сложных систем часто основаны на дискретной математике, включая
графы и клеточные автоматы.

Линейный → нелинейный
Классические модели часто бывают линейными или используют линейные приближения к нели-
нейным системам. Наука о сложных системах более дружелюбна к нелинейным моделям.

Детерминистический → стохастический
Классические модели обычно детерминированы, что может отражать основной философский
детерминизм, обсуждаемый в главе 5; сложные модели часто включают в себя случайность.

Абстрактный → подробный
В классических моделях планеты представляют собой точечные массы, плоскости не имеют тре-
ния, а коровы имеют сферическую форму (см. https://thinkcomplex.com/cow). Подобные упроще-
ния часто необходимы для анализа, но вычислительные модели могут быть более реалистич-
ными.

16  Изучение сложных систем с помощью Python


Один, два → много
Классические модели часто ограничены небольшим количеством компонентов. Например, в не-
бесной механике задачу двух тел можно решить аналитически; задачу трех тел – ​нет. Наука
о сложных системах часто работает с большим количеством компонентов и большим количест­
вом взаимодействий.

Гомогенный → гетерогенный
В классических моделях компоненты и взаимодействия имеют тенденцию быть идентичными;
сложные модели чаще включают в себя неоднородность.
Это обобщения, поэтому мы не должны относиться к ним слишком серьезно. И я не хочу осуждать
классическую науку. Более сложная модель не обязательно лучше; на самом деле она обычно хуже.
И я не хочу сказать, что эти изменения являются внезапными или полными. Скорее, происходит по-
степенная миграция на границе того, что считается приемлемой, респектабельной работой.
Некоторые инструменты, к которым раньше относились с подозрением, теперь широко распростра-
нены, а некоторые модели, которые получили широкое признание, теперь рассматриваются скрупу-
лезно.
Например, когда Аппель (Appel) и Хакен (Haken) доказали теорему о четырех цветах в 1976 году,
они использовали компьютер для перечисления 1936 особых случаев, которые в некотором смысле
были леммами их доказательства. В то время многие математики не считали теорему истинно дока-
занной. Теперь компьютерные доказательства распространены и общеприняты (но не повсеместно).
И наоборот, существенная часть экономического анализа основана на модели человеческого пове-
дения, называемой «Экономический человек», или, говоря иронически, Homo economicus.
Исследования, основанные на  этой модели, высоко ценились в  течение нескольких десятилетий,
особенно если они подразумевали математическую виртуозность. Совсем недавно к  этой модели
относились со  скептицизмом, а  модели, которые включают в  себя несовершенную информацию
и ограниченную рациональность, являются злободневными темами.

Разные модели для разных целей


Сложные модели часто подходят для разных целей и интерпретаций.

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

Реализм → инструментализм
Классические модели поддаются реалистической интерпретации. Например, большинство лю-
дей признает, что электроны – ​это реальные вещи, которые существуют. Инструментализм – ​это
мнение, что модели могут быть полезны, даже если сущности, которые они постулируют, не су-
ществуют. Джордж Бокс (George Box) написал фразу, которая может быть девизом инструмента-
лизма: «Все модели ошибочны, но некоторые полезны».

Редукционизм → холизм
Редукционизм – ​это принцип, согласно которому поведение системы можно объяснить, понимая
ее компоненты. Например, периодическая таблица элементов является триумфом редукциониз-
ма, поскольку она объясняет химическое поведение элементов с помощью модели электронов
в атомах. Холизм – ​это мнение, что некоторые явления, которые возникают на уровне системы,

Глава 1. Наука о сложных системах   17


не существуют на уровне компонентов и не могут быть объяснены терминами уровня компо-
нентов.
Мы вернемся к объяснительным моделям в главе 4, инструментализму в главе 6 и холизму в главе 8.

Инженерия сложных систем


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

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

Один ко многим → многие ко многим


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

Сверху вниз → снизу вверх


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

Анализ → расчет
В классической инженерии пространство возможных проектов ограничено нашими возмож-
ностями анализа. Например, проектирование Эйфелевой башни стало возможным, потому что
Гюстав Эйфель разработал новые аналитические методы, в  частности для борьбы с  ветровой
нагрузкой. Теперь инструменты автоматизированного проектирования и анализа позволяют по-
строить практически все, что только можно вообразить. Музей Гуггенхейма в  Бильбао – ​мой
любимый пример.

Изоляция → взаимодействие
В классической инженерии сложность больших систем управляется путем изоляции компонен-
тов и  минимизации взаимодействия. Это все еще важный инженерный принцип; тем не  ме-
нее доступность вычислений делает все более возможным проектирование систем со сложным
взаи­модействием между компонентами.

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

18  Изучение сложных систем с помощью Python


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

Аристотелевская логика → многозначная логика


В традиционной логике любое утверждение является либо истинным, либо ложным. Эта си-
стема поддается математическим доказательствам, но не работает (драматически) для многих
реальных приложений. Альтернативы включают в себя многозначную логику, нечеткую логику
и другие системы, предназначенные для обработки неоднозначности и неопределенности. Барт
Коско (Bart Kosko) обсуждает некоторые из этих систем в своей книге «Нечеткое мышление».

Частотная вероятность → Байесианизм


Байесовская вероятность существовала веками, но до недавнего времени широко не исполь-
зовалась, чему способствовали доступность дешевых вычислений и неохотное принятие субъ-
ективности в вероятностных утверждениях. Шарон Бертш МакГрэйн (Sharon Bertsch McGrayne)
представляет эту историю в книге «Теория, которая не умрет».

Объективно → субъективно
Просвещение и философский модернизм основаны на вере в объективную истину, то есть ис-
тины, которые не зависят от людей, которые их держат. События XX века, в том числе квантовая
механика, теорема Гёделя о  неполноте и  изучение истории науки Куна, привлекли внимание
к  казалось  бы неизбежной субъективности даже в  «твердых науках» и  математике. Ребекка
Голдштейн (Rebecca Goldstein) представляет исторический контекст доказательства Гёделя
в книге «Неполнота».

Физический закон → теория → модель


Некоторые люди различают законы, теории и  модели. Называя что-то «законом», подразуме-
вается, что это объективно верно и неизменно, «теория» предполагает, что она подлежит пере-
смотру, а «модель» допускает, что это субъективный выбор, основанный на упрощениях и при-
ближениях.
Я думаю, что все это одно и то же. Некоторые понятия, которые называются законами, на самом
деле являются определениями; другие, по сути, утверждают, что определенная модель особенно
хорошо предсказывает или объясняет поведение системы. Мы вернемся к природе физических
законов в разделах «Объяснительные модели» на стр. 56, «Что это за модель?» на стр. 67 и «Ре-
дукционизм и холизм» на стр. 105.

Детерминизм → Индетерминизм
Детерминизм – ​это точка зрения, согласно которой все события неизбежно связаны с предшест­
вующими событиями. Формы индетерминизма включают в  себя случайность, вероятностную
причинность и фундаментальную неопределенность. Мы вернемся к этой теме в разделе «Де-
терминизм» на стр. 62 и «Эмерджентность и свобода воли» на стр. 129.
Эти тенденции не являются универсальными или полными, но центр мнений смещается вдоль этих
осей. В качестве доказательства рассмотрим реакцию на книгу Томаса Куна (Thomas Kuhn) «Струк-
тура научных революций», которую поносили, когда она была опубликована, а в настоящее время
книга считается практически бесспорной.
Эти тенденции являются как причиной, так и следствием науки о сложных системах. Например, вы-
соко абстрагированные модели сейчас более приемлемы из-за уменьшенного ожидания того, что

Глава 1. Наука о сложных системах   19


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

20  Изучение сложных систем с помощью Python


Глава 2.
Графы
Следующие три главы посвящены системам, состоящим из компонентов и связей между компонен-
тами. Например, в социальной сети компоненты – ​это люди, а связи – ​это дружба, деловые отноше-
ния и т. д. В экологической пищевой сети компоненты являются видами, а связи – ​это отношения
хищник–жертва.
В этой главе я познакомлю вас с NetworkX, пакетом Python для построения моделей этих систем.
Начнем с  модели Эрдёша–Реньи, которая обладает интересными математическими свойствами.
В  следующей главе мы перейдем к  моделям, которые более полезны для объяснения реальных
систем.

Что такое графы?


Для большинства людей «граф» – ​это визуальное представление данных, например гистограмма
или график цен на акции. Эта глава не об этом.
В этой главе граф представляет собой представление системы, которая содержит дискретные, взаи­
мосвязанные элементы. Элементы обозначаются узлами, также называемыми вершинами, а взаи-
мосвязи обозначаются ребрами.
Например, вы можете представить дорожную карту, где узел обозначает город, а  ребро – ​дорогу
между городами. Или вы можете представить социальную сеть, используя узел для обозначения
человека, и ребро, соединяющее двух людей, если они друзья.
В некоторых графах ребра имеют такие атрибуты, как длина, стоимость или вес. Например, в дорож-
ной карте длина ребра может обозначать расстояние между городами или время в пути. В социаль-
ной сети могут быть разные виды ребер, обозначающих разные виды отношений: друзья, деловые
партнеры и т. д.
Ребра могут быть ориентированными или неориентированными, в зависимости от того, являются ли
отношения, которые они представляют, асимметричными или симметричными. На дорожной карте
вы можете представить улицу с  односторонним движением с  ориентированным ребром и  улицу
с  двусторонним движением с  неориентированным ребром. В  некоторых социальных сетях, таких
как Facebook, дружба симметрична: если A дружит с B, тогда B дружит с A. Но в Twitter, например,
отношение «следует» не является симметричным; если A следует за B, это не означает, что B следует
за A. Таким образом, вы можете использовать неориентированные ребра для представления сети
Facebook и ориентированные ребра для Twitter.
Графы обладают интересными математическими свойствами, и существует раздел математики, на-
зываемый теорией графов, который изучает их.
Графы также полезны, потому что существует много реальных проблем, которые могут быть решены
с помощью алгоритмов графов. Например, алгоритм кратчайшего пути Дейкстры – ​это эффективный
способ найти кратчайший путь от одного узла ко всем остальным узлам графа. Путь – ​это последова-
тельность узлов с ребром между каждой последовательной парой.

Глава 2. Графы   21
Графы обычно изображаются с помощью квадратов или кружков для обозначения узлов и линий
для обозначения ребер. Например, ориентированный граф на рис. 2-1 может представлять трех че-
ловек, которые следуют друг за другом в Twitter. Стрелка указывает направление отношений. В этом
примере Алиса и Боб следуют друг за другом, оба следуют за Чаком, а Чак ни за кем не следует.

Рисунок 2-1. Ориентированный граф,


представляющий социальную сеть

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

Рисунок 2-2. Неориентированный граф,


представляющий время в пути между
городами

22  Изучение сложных систем с помощью Python


NetworkX
Для работы с  графами мы будем использовать пакет под названием NetworkX, который является
наиболее часто используемой сетевой библиотекой в Python. Вы можете подробнее прочитать о ней
на странице https://thinkcomplex.com/netx, но я буду объяснять ее по мере продвижения.
Мы можем создать ориентированный граф, импортировав NetworkX (обычно импортируемый как
nx) и создав экземпляр nx.DiGraph:
import networkx as nx
G = nx.DiGraph()
В этот момент G является объектом DiGraph, который не содержит узлов и ребер. Мы можем добавить
узлы, используя метод add_node:
G.add_node('Alice')
G.add_node('Bob')
G.add_node('Chuck')
Теперь мы можем использовать метод nodes, чтобы получить список узлов:
>>> list(G.nodes())
NodeView(('Alice', 'Bob', 'Chuck'))
Метод nodes возвращает NodeView, который можно использовать в цикле for или, как в этом при-
мере, для создания списка.
Добавление ребер работает примерно так же:
G.add_edge('Alice', 'Bob')
G.add_edge('Alice', 'Chuck')
G.add_edge('Bob', 'Alice')
G.add_edge('Bob', 'Chuck')
И мы можем использовать edges, чтобы получить список ребер:
>>> list(G.edges())
[('Alice', 'Bob'), ('Alice', 'Chuck'),
('Bob', 'Alice'), ('Bob', 'Chuck')]
NetworkX предоставляет несколько функций для рисования графов; draw_circular упорядочивает
узлы по кругу и соединяет их с ребрами:
nx.draw_circular(G,
node_color=COLORS[0],
node_size=2000,
with_labels=True)
Это код, который я использую для генерации рис. 2‑1. Опция with_labels помечает узлы; в следую-
щем примере мы увидим, как пометить ребра.
Чтобы сгенерировать рис.  2‑2, я  начну со  словаря, который отображает название каждого города
в его приблизительную долготу и широту:
positions = dict(Albany=(–74, 43),
Boston=(–71, 42),
NYC=(–74, 41),
Philly=(–75, 40))

Глава 2. Графы   23
Поскольку это неориентированный граф, я создаю экземпляр nx.Graph:
G = nx.Graph()
Затем я  могу воспользоваться add_nodes_from, чтобы перебрать ключи positions и  добавить их
в виде узлов:
G.add_nodes_from(positions)
Далее я  создам словарь, который отображает каждое ребро в  соответствующее время поездки
на автомобиле:
drive_times = {('Albany', 'Boston'): 3,
('Albany', 'NYC'): 4,
('Boston', 'NYC'): 4,
('NYC', 'Philly'): 2}
Теперь я могу использовать add_edges_from, который перебирает ключи drive_times и добавляет
их как ребра:
G.add_edges_from(drive_times)
Вместо draw_circular, который размещает узлы в круге, я буду использовать draw, которая прини-
мает словарь позиций в качестве второго параметра:
nx.draw(G, positions,
node_color=COLORS[1],
node_shape='s',
node_size=2500,
with_labels=True)
draw использует positions для определения местоположения узлов.
Чтобы добавить метки ребер, мы применяем draw_networkx_edge_labels:
nx.draw_networkx_edge_labels(G, positions,
edge_labels=drive_times)
Параметр edge_labels ожидает словарь, который отображается из  каждой пары узлов в  метку;
в этом случае метки являются временем поездки на автомобиле между городами. И вот как я сге-
нерировал рис. 2‑2.
В обоих этих примерах узлы являются строками, но в общем они могут иметь любой тип хеширования.

Случайные графы
Случайный граф – ​это граф с узлами и ребрами, сгенерированный случайным образом. Конечно, су-
ществует много случайных процессов, которые могут генерировать графы, поэтому есть много видов
случайных графов.
Одним из наиболее интересных видов является модель Эрдёша–Реньи, изученная Полом Эрдёшем
(Paul Erdős) и Альфредом Реньи (Alfréd Rényi) в 60-х гг.
Граф Эрдёша–Реньи характеризуется двумя параметрами: n – ​число узлов, а p – ​вероятность того,
что между любыми двумя узлами есть ребро. См. https://thinkcomplex.com/er.
Эрдёш и Реньи изучали свойства этих случайных графов. Одним из их удивительных результатов
является наличие резких изменений свойств случайных графов при добавлении случайных ребер.
Одним из свойств, отображающих этот вид перехода, является возможность связности. Неориенти-
рованный граф является связным, если существует путь от каждого узла до каждого другого узла.

24  Изучение сложных систем с помощью Python


В графе Эрдёша–Реньи вероятность того, что граф является связным, очень мала, когда p мала,
и почти равна 1, когда p велика. Между этими двумя режимами происходит быстрый переход при
конкретном значении p, обозначаемом p*.
Эрдёш и Реньи показали, что это критическое значение равно p* = (ln n) / n, где n – ​количество узлов.
Случайный граф G(n, p) вряд ли будет связным, если p < p*, и очень вероятно обратное, если p > p*.
Чтобы проверить данное утверждение, мы разработаем алгоритмы для генерации случайных гра-
фов и проверим, являются ли они связными.

Генерация графов
Я начну с генерации полного графа, который представляет собой граф, в котором каждый узел свя-
зан с любым другим.
Вот функция генератора, которая принимает список узлов и перечисляет все отдельные пары. Если
вы не знакомы с функциями генератора, то можете прочитать о них на странице https://thinkcomplex.
com/gen.
def all_pairs(nodes):
for i, u in enumerate(nodes):
for j, v in enumerate(nodes):
if i>j:
yield u, v
Для построения полного графа мы можем использовать all_pairs:
def make_complete_graph(n):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(all_pairs(nodes))
return G
make_complete_graph принимает число узлов n и возвращает новый Graph с n узлами и ребрами
между всеми парами узлов.
Приведенный ниже код создает полный граф с 10 узлами и рисует его:
complete = make_complete_graph(10)
nx.draw_circular(complete,
node_color=COLORS[2],
node_size=1000,
with_labels=True)
На рис. 2‑3 показан результат. Вскоре мы изменим этот код для генерации графов Эрдёша–Реньи,
но сначала разработаем функции, чтобы проверить, является ли граф связным.

Глава 2. Графы   25
Рисунок 2-3.  Полный граф с десятью узлами

Связные графы
Граф является связным, если существует путь от каждого узла к каждому другому узлу (см. https://
thinkcomplex.com/conn).
Для многих приложений, использующих графы, полезно проверять, является ли граф связным.
К счастью, для этого есть простой алгоритм.
Вы можете начать с любого узла и проверить, можете ли вы добраться до всех других узлов. Если вы
можете добраться до узла v, то можете связаться с любым из соседей v, которые являются узлами,
соединенными с v ребром.
Класс Graph предоставляет метод под названием neighbors, который возвращает список соседей
для данного узла. Например, в полном графе мы сгенерировали в предыдущем разделе:
>>> complete.neighbors(0)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Предположим, мы начинаем с узла s. Мы можем пометить s как «видимый» и пометить его соседей.
Затем мы отмечаем соседей соседей, их соседей и т. д. до тех пор, пока не сможем достичь больше
узлов. Если все узлы видны, граф является связным.
Вот как это выглядит в Python:
def reachable_nodes(G, start):
seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen

26  Изучение сложных систем с помощью Python


reachable_nodes берет Graph и начальный узел, start и возвращает набор узлов, до которых можно
добраться из start.
Изначально набор seen пуст, и мы создаем список с именем stack, отслеживающий узлы, которые
мы обнаружили, но еще не обработали. Первоначально стек содержит один узел start.
Теперь каждый раз через цикл мы:
1) удаляем один узел из стека;
2) если узел уже есть в seen, мы возвращаемся к шагу 1;
3) в противном случае мы добавляем узел в seen и добавляем его соседей в стек.
Когда стек пуст, мы больше не можем добраться до узлов, поэтому мы прерываем выполнение цикла
и возвращаем seen.
В качестве примера можно найти все узлы в полном графе, которые доступны из узла 0:
>>> reachable_nodes(complete, 0)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Первоначально стек содержит узел 0, а seen пуст. В первый раз в цикле узел 0 добавляется в seen,
а все остальные узлы добавляются в стек (поскольку все они являются соседями узла 0).
В следующий раз в цикле pop возвращает последний элемент в стеке, который является узлом 9.
Таким образом, узел 9 добавляется в seen, а его соседи добавляются в стек.
Обратите внимание, что один и тот же узел может появляться в стеке более одного раза; фактически
узел с k соседями будет добавлен в стек k раз. Позже мы будем искать способы сделать этот алгоритм
более эффективным.
Мы можем использовать reachable_nodes, чтобы написать is_connected:
def is_connected(G):
start = next(iter(G))
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)
is_connected выбирает начальный узел, создавая итератор узла и выбирая первый элемент. Затем
он использует reachable, чтобы получить набор узлов, до которых можно добраться из start. Если
размер этого набора совпадает с размером графа, это означает, что мы можем добраться до всех
узлов, что означает, что граф является связным.
Полный граф, что неудивительно, связный:
>>> is_connected(complete)
True
В следующем разделе мы будем генерировать графы Эрдёша–Реньи и проверим, являются ли они
связными.

Генерация графов Эрдёша–Реньи


Граф Эрдёша–Реньи G(n, p) содержит n узлов, а каждая пара узлов соединена ребром с вероятно-
стью p. Генерация графа Эрдёша–Реньи аналогична генерации полного графа.
Приведенная ниже функция генератора перечисляет все возможные ребра и выбирает, какие из них
должны быть добавлены в граф:
def random_pairs(nodes, p):
for edge in all_pairs(nodes):

Глава 2. Графы   27
if flip(p):
yield edge
random_pairs использует flip:
def flip(p):
return np.random.random() < p
Это первый пример, который мы видели, где используется NumPy. Следуя соглашению, я импортирую
numpy как np. NumPy предоставляет модуль random, который предоставляет метод с именем random,
возвращающий число от 0 до 1, равномерно распределенное.
Таким образом, flip возвращает значение True с заданной вероятностью p и значение False с до-
полнительной вероятностью 1-p.
Наконец, make_random_graph генерирует и возвращает граф Эрдёша–Реньи G(n, p):
def make_random_graph(n, p):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(random_pairs(nodes, p))
return G
make_random_graph почти идентичен make_complete_graph; единственное отличие состоит в том, что
он использует random_pairs вместо all_pairs.
Вот пример, где p = 0.3:
random_graph = make_random_graph(10, 0.3)
На рис.  2‑4 показан результат. Этот граф оказывается связным; фактически многие графы Эрдё-
ша–Реньи, где n = 10 и p = 0.3, связные. В следующем разделе мы увидим, сколько их.

Рисунок 2-4.  Граф Эрдёша–Реньи, где n = 10, а p = 0.3

28  Изучение сложных систем с помощью Python


Вероятность связности
Для заданных значений n и p нам хотелось бы знать вероятность того, что G(n, p) связный. Мы можем
оценить это, генерируя большое количество случайных графов и подсчитывая, сколько из них явля-
ются связными. Вот как:
def prob_connected(n, p, iters=100):
tf = [is_connected(make_random_graph(n, p))
for i in range(iters)]
return np.mean(bool)
Параметры n и p передаются в make_random_graph; iters – ​количество генерируемых нами случай-
ных графов.
Эта функция использует понимание списка; если вы незнакомы с данной функцией, вы можете про-
читать о ней на странице https://thinkcomplex.com/comp.
Результат, tf, является списком логических значений: True для каждого связного графа и False для
каждого графа, который таковым не является.
np.mean – ​это функция NumPy, которая вычисляет среднее значение этого списка, рассматривая True
как 1 и False как 0. Результатом является доля случайных связных графов.
>>> prob_connected(10, 0.23, iters=10000)
0.33
Я выбрал 0,23, потому что он близок к критическому значению, при котором вероятность связности
возрастает от 0 до почти 1. Согласно Эрдёшу и Реньи, p* = ln n/n = 0,23.
Мы можем получить более четкое представление о переходе, оценивая вероятность связности для
диапазона значений p:
n = 10
ps = np.logspace(–2.5, 0, 11)
ys = [prob_connected(n, p) for p in ps]
Функция NumPy logspace возвращает массив из 11 значений от 10–25 до 100 = 1, равномерно рас-
пределенных на логарифмической шкале.
Для каждого значения p в массиве мы вычисляем вероятность того, что граф с параметром p являет-
ся связным, и сохраняем результаты в ys.
На рис. 2‑5 показаны результаты с вертикальной линией при вычисленном критическом значении,
p* = 0,23. Как и ожидалось, переход от 0 к 1 происходит вблизи критического значения.
На рис. 2‑6 показаны аналогичные результаты для больших значений n. При увеличении n критиче-
ское значение уменьшается, а переход становится более резким.
Эти экспериментальные результаты согласуются с аналитическими результатами Эрдёша и Реньи,
представленными в их работах.

Глава 2. Графы   29
Рисунок 2-5. Вероятность связности с n = 10
и диапазоном p. Вертикальная
линия показывает прогнозируемое
критическое значение

Рисунок 2-6. Вероятность связности для


нескольких значений n и диапазона p

Анализ алгоритмов графов


Ранее в этой главе я представил алгоритм проверки графа на предмет связности; в следующих гла-
вах мы увидим другие алгоритмы графов. Попутно мы проанализируем производительность этих
алгоритмов, выяснив, как их время выполнения увеличивается по мере увеличения размера графов.
Если вы еще незнакомы с  анализом алгоритмов, можете прочитать приложение B к  книге «Think
Python», 2-е изд., на странице https://thinkcomplex.com/tp2.

30  Изучение сложных систем с помощью Python


Порядок роста для алгоритмов графа обычно выражается в виде функции n, числа вершин (узлов),
и m, числа ребер.
В качестве примера давайте проанализируем reachable_nodes из  раздела «Связные графы»
на стр. 26:
def reachable_nodes(G, start):
seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen
Каждый раз в цикле мы выталкиваем узел из стека; по умолчанию pop удаляет и возвращает послед-
ний элемент списка, который является операцией постоянного времени.
Далее мы проверяем, находится ли узел в seen, который является набором, поэтому проверка член-
ства выполняется с постоянным временем.
Если узла еще нет в seen, мы добавляем его, который является постоянным временем, а затем добав-
ляем соседей в стек, который является линейным по количеству соседей.
Чтобы выразить время выполнения в терминах n и  m, мы можем сложить общее количество раз,
когда каждый узел добавляется в seen и stack.
Каждый узел добавляется в seen только один раз, поэтому общее количество добавлений равно n.
Но узлы могут быть добавлены в stack много раз, в зависимости от того, сколько у них соседей. Если
узел имеет k соседей, он добавляется в stack k раз. Конечно, если у него k соседей, это означает, что
он связан с k ребрами.
Таким образом, общее количество добавлений в stack равно общему количеству ребер, m, удвоен-
ному, потому что мы рассматриваем каждое ребро дважды.
Следовательно, порядок роста этой функции составляет O(n + m), что является удобным способом
сказать, что время выполнения увеличивается пропорционально либо n, либо m, в  зависимости
от того, что больше.
Если мы знаем отношения между n и m, то можем упростить это выражение. Например, в полном
графе число ребер равно n(n – ​1)/2, что в O(n2). Таким образом, для полного графа reachable_nodes
является квадратичным по n.

Упражнения
Код для этой главы находится в chap02.ipynb. Это блокнот Jupyter в репозитории данной книги. Для
получения дополнительной информации о том, как работать с этим кодом, см. «Использование кода»
на странице 10.

Упражнение 2‑1
Запустите chap02.ipynb и выполните код. В блокноте есть несколько коротких упражнений, которые
вы, возможно, захотите попробовать.

Глава 2. Графы   31
Упражнение 2‑2
В разделе «Анализ алгоритмов графов» на  стр. 30 мы проанализировали производительность
reachable_nodes и классифицировали его по O(n + m), где n – ​количество узлов, а m – ​количество
ребер. Продолжая анализ, каков порядок роста is_connected?
def is_connected(G):
start = list(G)[0]
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)

Упражнение 2‑3
В моей реализации reachable_nodes вы можете быть обеспокоены очевидной неэффективностью
добавления всех соседей в стек, не проверяя, находятся ли они уже в seen. Напишите версию этой
функции, которая проверяет соседей, прежде чем добавить их в стек. Меняет ли такая «оптимиза-
ция» порядок роста? Это делает функцию быстрее?

Упражнение 2‑4
На самом деле существует два вида графов Эрдёша–Реньи. Тот, который мы сгенерировали в этой
главе, G(n, p), характеризуется двумя параметрами: количеством узлов и вероятностью ребра между
узлами.
Альтернативное определение, обозначаемое G(n, m), также характеризуется двумя параметрами: ко-
личеством узлов, n, и количеством ребер, m. Согласно этому определению, количество ребер являет-
ся фиксированным, но их расположение случайно.
Повторите эксперименты, которые мы сделали в этой главе, используя вышеуказанное альтернатив-
ное определение. Вот несколько советов касательно того, как действовать.
1. Н
 апишите функцию с именем m_pairs, которая принимает список узлов и количество ребер m
и возвращает случайный выбор m ребер. Простой способ сделать это – ​создать список всех
возможных ребер и использовать random.sample.
2. Напишите функцию с именем make_m_graph, которая принимает n и m и возвращает случай-
ный граф с n узлами и m ребрами.
3. С
 оздайте версию prob_connected, которая использует make_m_graph вместо make_random_
graph.
4. Рассчитайте вероятность связности для диапазона значений m.
Как результаты этого эксперимента сравнимы с результатами, полученными на графе Эрдёша–Ре-
ньи первого типа?

32  Изучение сложных систем с помощью Python


Глава 3.
Графы «Мир тесен»
Многие сети в  реальном мире, в  том числе социальные сети, обладают «свойством “мир тесен”»,
которое заключается в том, что среднее расстояние между узлами, измеренное по количеству ребер
на кратчайшем пути, намного меньше, чем ожидалось.
В этой главе я рассказываю о знаменитом эксперименте Малого мира Стенли Милграма, который
был первой демонстрацией свойства маленького мира в реальной социальной сети. Затем мы рас-
смотрим графы Ваттса–Строгаца, используемые в качестве модели графов «Мир тесен». Я повторю
эксперимент, проведенный Ваттсом и Строгацем, и объясню, что он намеревается показать.
Попутно мы увидим два новых алгоритма графов: поиск в ширину и алгоритм Дейкстры для вычис-
ления кратчайшего пути между узлами в графе.

Стэнли Милгрэм
Стэнли Милгрэм (Stanley Milgram) был американским социальным психологом, который провел два
самых известных эксперимента в области социальных наук: эксперимент Милгрэма, в ходе которого
изучалось подчинение людей власти (https://thinkcomplex.com/milgram), и эксперимент «Мир тесен»,
в котором изучалась структура социальных сетей (https://thinkcomplex.com/small).
В эксперименте «Мир тесен» Милгрэм отправил посылку нескольким случайно выбранным людям
в Вичите, штат Канзас, с инструкциями, в которых их просили направить прилагаемое письмо целе-
вому лицу, идентифицированному по имени и профессии, в Шэрон, штат Массачусетс (оказывается,
это город под Бостоном, где я вырос). Испытуемым сказали, что они могут отправить письмо целево-
му лицу напрямую, только если знают его лично; в противном случае им было поручено отправить
его и те же инструкции родственнику или другу, который, по их мнению, с большей вероятностью
знаком с целевым лицом.
Многие письма так и не были доставлены, но тех, которые имели среднюю длину пути, – ​количество
раз, когда письма были отправлены, – ​было около шести. Этот результат использовался, чтобы под-
твердить предыдущие наблюдения (и предположения) относительно того, что типичное расстояние
между любыми двумя людьми в социальной сети составляет примерно «шесть степеней разделе-
ния».
Этот вывод удивителен, так как большинство людей ожидает, что социальные сети будут локализо-
ваны, – ​люди, как правило, живут рядом со своими друзьями – ​и на графе с локальными связями
длина пути, как правило, увеличивается пропорционально географическому расстоянию. Например,
большинство моих друзей живет поблизости, поэтому я предполагаю, что среднее расстояние меж-
ду узлами в социальной сети составляет около 50 миль. Вичита находится примерно в 1600 милях
от  Бостона, поэтому если письма Милгрэма пересекали типичные ссылки в  социальной сети, они
должны были сделать это 32 раза, а не шесть.

Глава 3. Графы «Мир тесен»   33


Ваттс и Строгац
В 1998 году Дункан Ваттс (Duncan Watts) и Стивен Строгац опубликовали в журнале Nature статью
«Коллективная динамика сетей “Мир тесен”», в которой предложено объяснение феномена малень-
кого мира. Вы можете скачать ее на странице https://thinkcomplex.com/watts.
Ваттс и Строгац начинают с двух типов хорошо понятых графов: случайных и регулярных. В случай-
ном графе узлы связаны случайным образом. В регулярном графе каждый узел имеет одинаковое
количество соседей. Они рассматривают два свойства этих графов, кластеризацию и длину пути:
• кластеризация – ​это степень «кликальности» графа. В графе клика – это подмножество узлов,
которые все связаны друг с другом; в социальной сети клика – ​это группа людей, которые все
дружат друг с другом. Ваттс и Строгац определили коэффициент кластеризации, который коли-
чественно определяет вероятность того, что два узла, которые связаны с одним и тем же узлом,
также связаны друг с другом;
• длина пути – ​это степень среднего расстояния между двумя узлами, которое соответствует сте-
пеням разделения в социальной сети.
Ваттс и Строгац показывают, что регулярные графы имеют высокую кластеризацию и большую длину
пути, тогда как случайные графы одинакового размера обычно имеют низкую кластеризацию и ма-
лую длину пути. Так что ни один из них не является хорошей моделью социальных сетей, которые
сочетают высокую кластеризацию с короткими длинами пути.
Их целью было создать генеративную модель социальной сети. Генеративная модель пытается объ-
яснить явление, моделируя процесс, который создает или ведет к явлению. Ваттс и Строгац предло-
жили данный процесс для создания графов «Мир тесен»:
1) начните с регулярного графа с n узлами и каждым узлом, связанным с k соседями;
2) выберите подмножество ребер и «переплетите» их, заменив их случайными ребрами.
Вероятность того, что ребро переплетено, является параметром p, который определяет, насколько
случайным является граф. При p = 0 граф регулярный; при p = 1 он совершенно случайный.
Ваттс и Строгац обнаружили, что небольшие значения p дают графы с высокой кластеризацией, та-
кие как регулярный граф, и с малой длиной пути, как случайный граф.
В этой главе я повторю эксперимент Ваттса и Строгаца, выполнив следующие шаги.
1. Начнем с построения кольцевой решетки, которая является своего рода регулярным графом.
2. Затем мы перепишем ее так, как это сделали Ваттс и Строгац.
3. М
 ы напишем функцию для измерения степени кластеризации и будем использовать функцию
NetworkX для вычисления длины пути.
4. Затем мы вычислим степень кластеризации и длину пути для диапазона значений p.
5. Н
 аконец, я  расскажу об  алгоритме Дейкстры, который эффективно вычисляет кратчайшие
пути.

Кольцевая решетка
Регулярный граф – ​это граф, в котором каждый узел имеет одинаковое количество соседей; количест­
во соседей также называется степенью узла.
Кольцевая решетка является своего рода регулярным графом, который Ваттс и Строгац используют
в качестве основы своей модели. В кольцевой решетке с n узлами узлы могут быть расположены
по кругу, причем каждый узел соединен с k ближайшими соседями. Например, кольцевая решетка,

34  Изучение сложных систем с помощью Python


где n = 3, а k = 2, будет содержать следующие ребра: (0, 1), (1, 2) и (2, 0). Обратите внимание, что ребра
«сворачиваются» от узла с наибольшим номером обратно до 0.
В целом можно перечислить ребра следующим образом:
def adjacent_edges(nodes, halfk):
n = len(nodes)
for i, u in enumerate(nodes):
for j in range(i+1, i+halfk+1):
v = nodes[j% n]
yield u, v
adjacent_edges принимает список узлов и параметр halfk, который равен половине k. Это функ-
ция генератора, которая дает одно ребро за раз. Она использует оператор модуля % для перехода
от узла с наибольшим номером к самому низкому. Можно проверить это так:
>>> nodes = range(3)
>>> for edge in adjacent_edges(nodes, 1):
… print(edge)
(0, 1)
(1, 2)
(2, 0)
Теперь мы можем использовать adjacent_edges для создания кольцевой решетки:
def make_ring_lattice(n, k):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(adjacent_edges(nodes, k//2))
return G
Обратите внимание, что make_ring_lattice использует деление с остатком для вычисления halfk,
поэтому оно верно, только если k является четным. Если k нечетно, деление с остатком округляется
в меньшую сторону, поэтому получается кольцевая решетка со степенью k – ​1. В качестве одного
из упражнений в конце главы вы будете генерировать регулярные графы с нечетными значениями k.
Мы можем протестировать make_ring_lattice следующим образом:
lattice = make_ring_lattice(10, 4)
На рис. 3‑1 показан результат.

Глава 3. Графы «Мир тесен»   35


Рисунок 3-1.  Кольцевая решетка, где n = 10, а k = 4

Графы Ваттса–Строгаца
Чтобы построить граф Ваттса–Строгаца, мы начнем с кольцевой решетки и «переплетем» некоторые
ребра. В своей работе Ваттс и Строгац рассматривают ребра в определенном порядке и перепле-
тают каждое с вероятностью p. Если ребро переплетено, они оставляют первый узел без измене-
ний и выбирают второй узел случайным образом. Они не допускают петель или нескольких ребер;
то есть у вас не может быть ребра от узла к самому себе, и нельзя иметь более одного ребра между
двумя одинаковыми узлами.
Вот моя реализация этого процесса:
def rewire(G, p):
nodes = set(G)
for u, v in G.edges():
if flip(p):
choices = nodes – ​{u} – ​set(G[u])
new_v = np.random.choice(list(choices))
G.remove_edge(u, v)
G.add_edge(u, new_v)
Параметр p – ​это вероятность переплетения ребра. Цикл for перечисляет ребра и использует flip
(см. «Генерация графов Эрдёша–Реньи» на стр. 27), чтобы выбрать, какие из них будут переплетены.
Если мы переплетаем ребро от узла u к узлу v, мы должны выбрать замену для v, которая называется
new_v.
1. Ч
 тобы вычислить возможные варианты выбора, мы начинаем с nodes, который является набо-
ром, и вычитаем u и его соседей, что позволяет избежать петель и нескольких ребер.
2. Чтобы выбрать new_v, мы используем функцию NumPy choice, которая находится в модуле
random.
3. Затем удаляем существующее ребро от u к v.

36  Изучение сложных систем с помощью Python


4. Добавляем новое ребро от u к new_v.
Кроме того, выражение G[u] возвращает словарь, который содержит соседей u в качестве ключей.
Обычно это быстрее, чем использовать G.neighbors (см. https://thinkcomplex.com/neigh).
Эта функция не учитывает ребра в порядке, указанном Ваттсом и Строгацем, но, похоже, это не влия­
ет на результаты.
На рис. 3‑2 показаны графы Ваттса–Строгаца, где n = 20, k = 4, с диапазоном значений p. Когда p = 0,
граф является кольцевой решеткой. Когда р = 1, он совершенно случайный. Далее мы увидим, что
в промежутке происходят интересные вещи.

Рисунок 3-2. Графы Ваттса–Строгаца, где n = 20,


k = 4, а p = 0 (слева), p = 0.2 (в центре)
и p = 1 (справа)

Кластеризация
Следующим шагом является вычисление коэффициента кластеризации, который количественно
определяет тенденцию узлов формировать клики. Клика – э​ то подмножество узлов, которые полно-
стью связаны; то есть существуют ребра между всеми парами узлов в подмножестве.
Предположим, у определенного узла u есть k соседей. Если все соседи связаны друг с другом, между
ними будет k(k – ​1)/2 ребер. Доля ребер, которые действительно существуют, является локальным
коэффициентом кластеризации u, обозначаемым Cu.
Если мы вычислим среднее значение Cu по всем узлам, то получим «средний коэффициент класте-
ризации в сети», обозначенный как C.
Вот функция, которая вычисляет его:
def node_clustering(G, u):
neighbors = G[u]
k = len(neighbors)
if k < 2:
return np.nan
possible = k * (k‑1) / 2
exist = 0
for v, w in all_pairs(neighbors):
if G.has_edge(v, w):

Глава 3. Графы «Мир тесен»   37


exist +=1
return exist / possible
Я снова использую выражение G[u], которое возвращает словарь с соседями u в качестве ключей.
Если у узла меньше 2 соседей, коэффициент кластеризации не определен, поэтому мы возвращаем
np.nan, которое является специальным значением, указывающим «не число».
В противном случае мы вычисляем количество возможных ребер среди соседей, подсчитываем ко-
личество реально существующих ребер и возвращаем существующую долю.
Можно проверить функцию следующим образом:
>>> lattice = make_ring_lattice(10, 4)
>>> node_clustering(lattice, 1)
0.5
В кольцевой решетке, где k = 4, коэффициент кластеризации для каждого узла равен 0,5 (если вы
не уверены, взгляните еще раз на рис. 3‑1).
Теперь мы можем вычислить средний коэффициент кластеризации в сети следующим образом:
def clustering_coefficient(G):
cu = [node_clustering(G, node) for node in G]
return np.nanmean(cu)
Функция NumPy nanmean вычисляет среднее значение локальных коэффициентов кластеризации,
игнорируя любые значения, которые являются NaN.
Мы можем протестировать clustering_coefficient следующим образом:
>>> clustering_coefficient(lattice)
0.5
В этом графе коэффициент локальной кластеризации для всех узлов равен 0,5, поэтому среднее
значение по  узлам равно 0,5. Конечно, мы ожидаем, что это значение будет разным для графов
Ваттса–Строгаца.

Длина кратчайшего пути


Следующим шагом является вычисление длины пути L, которая является средней длиной кратчай-
шего пути между каждой парой узлов. Чтобы вычислить ее, я начну с функции NetworkX, shorttest_
path_length. Я буду использовать ее для воспроизведения эксперимента Ваттса и Строгаца, а затем
объясню, как она работает.
Вот функция, которая берет граф и возвращает список длин кратчайших путей, по одной для каждой
пары узлов:
def path_lengths(G):
length_map = nx.shortest_path_length(G)
lengths = [length_map[u][v] for u, v in all_pairs(G)]
return lengths
Возвращаемое значение из  nx.shortest_path_length – ​это словарь словарей. Внешний словарь
отображается из каждого узла u в словарь, который отображается из каждого узла v в длину крат-
чайшего пути от u до v.
Используя список длин из path_lengths, мы можем вычислить L следующим образом:

38  Изучение сложных систем с помощью Python


def characteristic_path_length(G):
return np.mean(path_lengths(G))
И можем проверить ее с помощью небольшой кольцевой решетки:
>>> lattice = make_ring_lattice(3, 2)
>>> characteristic_path_length(lattice)
1.0
В этом примере все три узла связаны друг с другом, поэтому средняя длина пути равна 1.

Эксперимент Ваттса–Строгаца
Теперь мы готовы повторить эксперимент Ваттса–Строгаца, который показывает, что для диапазона
значений p граф Ваттса–Строгаца имеет высокую кластеризацию, как у регулярного графа, и корот-
кую длину пути, как у случайного графа.
Я начну с run_one_graph, который принимает n, k и p; он генерирует граф Ваттса–Строгаца с задан-
ными параметрами и вычисляет среднюю длину пути, mpl, и коэффициент кластеризации, cc:
def run_one_graph(n, k, p):
ws = make_ws_graph(n, k, p)
mpl = characteristic_path_length(ws)
cc = clustering_coefficient(ws)
print(mpl, cc)
return mpl, cc
Ваттс и Строгац проводили свой эксперимент, когда n = 1000 и k = 10. С этими параметрами run_
one_graph занимает несколько секунд на моем компьютере; большая часть этого времени тратится
на вычисление средней длины пути.
Теперь нам нужно вычислить эти значения для диапазона р. Я  снова буду использовать функцию
NumPy logspace для вычисления ps:
ps = np.logspace(–4, 0, 9)
Вот функция, которая запускает эксперимент:
def run_experiment(ps, n=1000, k=10, iters=20):
res = []
for p in ps:
t = [run_one_graph(n, k, p) for _ in range(iters)]
means = np.array(t).mean(axis=0)
res.append(means)
return np.array(res)
Для каждого значения р мы генерируем 20 случайных графов и усредняем результаты. Поскольку
возвращаемое значение из run_one_graph представляет собой пару, t – ​это список пар. Когда мы
конвертируем его в массив, то получаем одну строку для каждой итерации и столбцы для L и C.
Вызов mean с  параметром axis = 0 вычисляет среднее значение каждого столбца; в  результате
получается массив с одной строкой и двумя столбцами.
Когда цикл завершается, means – ​это список пар, который мы конвертируем в массив NumPy с одной
строкой для каждого значения р и столбцами для L и C.
Можно извлечь столбцы следующим образом:

Глава 3. Графы «Мир тесен»   39


L, C = np.transpose(res)
Чтобы нанести L и C на одни и те же оси, мы стандартизируем их путем деления по первому эле-
менту:
L /= L[0]
C /= C[0]
На рис. 3‑3 показаны результаты. При увеличении p средняя длина пути быстро уменьшается, по-
тому что даже небольшое количество случайно переплетенных ребер обеспечивает ярлыки между
областями графа, которые находятся далеко друг от друга в решетке. С другой стороны, удаление
локальных ссылок уменьшает коэффициент кластеризации гораздо медленнее.

Рисунок 3-3. Коэффициент кластеризации (C)


и характерная длина пути (L) для
графов Ваттса–Строгаца при
n = 1000, k = 10 с диапазоном p

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

Что это за объяснение?


Если вы спросите меня, почему планетарные орбиты являются эллиптическими, я  мог  бы начать
с моделирования планеты и звезды в виде точечных масс; я бы посмотрел закон всемирного тяго-
тения на странице https://thinkcomplex.com/grav и использовал его, чтобы написать дифференциаль-
ное уравнение для движения планеты. Потом я бы либо вывел уравнение орбиты, либо, что более

40  Изучение сложных систем с помощью Python


вероятно, посмотрел бы его на странице https://thinkcomplex.com/orbit. Используя немного алгебры,
я мог бы вывести условия, которые дают эллиптическую орбиту. Тогда я бы сказал, что объекты, кото-
рые мы считаем планетами, удовлетворяют этим условиям.
Люди или, по крайней мере, ученые, как правило, удовлетворены таким объяснением. Одна из при-
чин его привлекательности состоит в том, что предположения и аппроксимации в модели кажутся
разумными. Планеты и звезды на самом деле не являются точечными массами, но расстояния между
ними настолько велики, что их фактические размеры незначительны. Планеты в одной и той же сол-
нечной системе могут влиять на орбиты друг друга, но эффект обычно невелик. И мы игнорируем
релятивистские эффекты, опять же при условии, что они незначительны.
Это объяснение также привлекательно, потому что оно основано на уравнении. Мы можем выразить
уравнение орбиты в замкнутой форме, что означает, что мы можем эффективно вычислять орбиты.
Это также означает, что мы можем вывести общие выражения для орбитальной скорости, орбиталь-
ного периода и других величин.
Наконец, я думаю, что подобное объяснение привлекательно, потому что оно имеет форму матема-
тического доказательства. Важно помнить, что доказательство относится к модели, а не к реальному
миру. То есть мы можем доказать, что идеализированная модель дает эллиптические орбиты, но мы
не можем доказать, что реальные орбиты являются эллипсами (на самом деле это не так). Тем не ме-
нее сходство с доказательством является привлекательным.
Для сравнения, объяснение Ваттса и  Строгаца феномена «маленького мира» может показаться
менее удовлетворительным. Во-первых, эта модель более абстрактна, то  есть менее реалистична.
Во-вторых, результаты генерируются с помощью моделирования, а не математического анализа. На-
конец, результаты кажутся не столько доказательством, сколько примером.
Многие из  моделей в  этой книге похожи на  модель Ваттса–Строгаца: абстрактные, основанные
на моделировании и (по крайней мере, внешне) менее формальные, чем обычные математические
модели. Одна из целей данной книги – ​рассмотреть вопросы, которые ставят эти модели:
• какую работу могут выполнять эти модели: они прогнозирующие, объяснительные или и  то,
и другое?
• являются ли объяснения этих моделей менее удовлетворительными, чем объяснения, основан-
ные на более традиционных моделях? Почему?
• как мы должны охарактеризовать различия между этими и более традиционными моделями?
Они отличаются по виду или только по степени?
На протяжении всей книги я буду предлагать свои ответы на эти вопросы, но они носят предвари-
тельный, а иногда и умозрительный характер. Я призываю вас скептически относиться к ним и де-
лать собственные выводы.

Поиск в ширину
Когда мы вычисляли кратчайшие пути, то  использовали функцию, предоставляемую NetworkX,
но я не объяснил, как она работает. Для этого я начну с поиска в ширину, который является основой
алгоритма Дейкстры для вычисления кратчайших путей.
В разделе «Связные графы» на стр. 26 я познакомил вас с reachable_nodes, который находит все
узлы, до которых можно добраться из данного начального узла:
def reachable_nodes(G, start):
seen = set()
stack = [start]
while stack:

Глава 3. Графы «Мир тесен»   41


node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen
В то время я не упомянул об этом, но reachable_nodes выполняет поиск в глубину. Теперь мы изме-
ним его для выполнения поиска в ширину.
Чтобы понять разницу, представьте, что вы исследуете замок. Вы начинаете с комнаты с тремя две-
рями, отмеченными как A, B и C. Вы открываете дверь C и обнаруживаете другую комнату с дверями,
отмеченными как D, E и F.
Какую дверь вы откроете дальше? Если вы считаете себя авантюристом, возможно, вы захотите от-
правиться вглубь замка и выберете D, E или F. Это будет поиск в глубину.
Но если вы хотите быть более систематичным, вы можете вернуться и исследовать A и B, перед тем
как приступить к D, E и F. Это будет поиск в ширину.
В reachable_nodes мы используем метод списка pop, чтобы выбрать следующий узел для «исследо-
вания».
По умолчанию pop возвращает последний элемент списка, который является последним, который
мы добавили. В нашем примере это будет дверь F.
Если мы хотим вместо этого выполнить поиск в ширину, самое простое решение – ​это извлечь пер-
вый элемент из списка:
node = stack.pop(0)
Это работает, но медленно. В Python извлечение последнего элемента из списка занимает постоян-
ное время, но извлечение первого элемента является линейным по длине списка. В худшем случае
длина стека составляет O(n), что делает эту реализацию поиска в ширину O(nm), что намного хуже,
чем должно быть, O(n + m).
Мы можем решить эту проблему с помощью двусвязной очереди, также известной как дек. Важной
особенностью дека является то, что вы можете добавлять и удалять элементы с начала или с конца
в постоянное время. Чтобы увидеть, как это реализовано, см. https://thinkcomplex.com/deque.
Python предоставляет deque в модуле collections, поэтому мы можем импортировать его следую-
щим образом:
from collections import deque
И мы можем использовать это, чтобы написать эффективный поиск в ширину:
def reachable_nodes_bfs(G, start):
seen = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in seen:
seen.add(node)
queue.extend(G.neighbors(node))
return seen
Различия состоят в следующем:
• я заменил список с именем stack на дек с именем queue;
• я заменил pop на popleft, который удаляет и возвращает самый левый элемент очереди.

42  Изучение сложных систем с помощью Python


Эта версия вернулась к тому, чтобы стать O(n + m). Теперь мы готовы перейти к поиску кратчайших
путей.

Алгоритм Дейкстры
Эдсгер В. Дейкстра  – ​голландский ученый, который изобрел эффективный алгоритм кратчайше-
го пути (см. https://thinkcomplex.com/dijk). Он также изобрел семафор, который представляет собой
структуру данных, используемую для координации программ, взаимодействующих друг с  другом
(см. https://thinkcomplex.com/sem и «Маленькая книга cемафоров»).
Дейкстра известен (и знаменит) как автор серии очерков по информатике. Некоторые из них, такие
как «Дело в  отношении оператора GO TO», оказали глубокое влияние на  практику программиро-
вания. Другие, такие как «О жестокости реального преподавания компьютерных наук», интересны
своей вздорностью, но менее эффективны.
Алгоритм Дейкстры решает «проблему кратчайшего пути с  одним источником», что означает, что
он находит минимальное расстояние от данного «исходного» узла до каждого другого узла в графе
(или, по крайней мере, каждого связного узла).
Я приведу упрощенную версию алгоритма, которая рассматривает все ребра одинаковой длины.
Более общая версия работает с любыми неотрицательными длинами ребер.
Упрощенная версия аналогична поиску в ширину, о котором шла речь в предыдущем разделе, за ис-
ключением того, что мы заменяем набор с именем seen на словарь с именем dist, который отобра-
жается из каждого узла в его расстояние от источника:
def shortest_path_dijkstra(G, source):
dist = {source: 0}
queue = deque([source])
while queue:
node = queue.popleft()
new_dist = dist[node] + 1
neighbors = set(G[node]).difference(dist)
for n in neighbors:
dist[n] = new_dist
queue.extend(neighbors)
return dist
Вот как это работает:
• первоначально queue содержит один элемент, source, и dist отображается из source в расстоя­
ние 0 (которое является расстоянием от source до самого себя);
• каждый раз в цикле мы используем popleft, чтобы выбрать следующий узел в очереди;
• затем мы находим всех соседей node, которых еще нет в dist;
• поскольку расстояние от source до source равно dist [node], расстояние до любого из необна-
руженных соседей равно dist [node] +1;
• для каждого соседа мы добавляем запись в dist, затем добавляем соседей в очередь.
Этот алгоритм работает, только если мы используем поиск в ширину, а не в глубину. Чтобы понять,
почему, рассмотрим следующее.
1. Первый раз в  цикле node – ​это source, а  new_dist равен 1. Таким образом, соседи source
получают расстояние 1 и идут в очередь.

Глава 3. Графы «Мир тесен»   43


2. К
 огда мы обрабатываем соседей source, все их соседи получают расстояние 2. Мы знаем, что
ни один из них не может иметь расстояние 1, потому что если бы это было так, мы бы обнаружили
их во время первой итерации.
3. Точно так же, когда мы обрабатываем узлы с расстоянием 2, мы даем их соседям расстояние 3. Мы
знаем, что ни один из них не может иметь расстояние 1 или 2, потому что если бы это было так,
мы бы обнаружили их во время предыдущей итерации.
И так далее. Если вы знакомы с доказательством по индукции, вы можете увидеть, куда это ведет.
Но этот аргумент работает только в том случае, если мы обрабатываем все узлы с расстоянием 1,
прежде чем начнем обрабатывать узлы с расстоянием 2, и т. д. И это именно то, что делает поиск
в ширину.
В упражнениях в  конце этой главы вы напишете версию алгоритма Дейкстры с  использованием
поиска в глубину, чтобы у вас была возможность увидеть, что идет не так.

Упражнения
Код, приведенный в этой главе, находится в chap03.ipynb в репозитории для этой книги. Более по­
дробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 3‑1
В кольцевой решетке каждый узел имеет одинаковое количество соседей. Число соседей называет-
ся степенью узла, а граф, в котором все узлы имеют одинаковую степень, называется регулярным.
Все кольцевые решетки регулярны, но не все регулярные графы являются кольцевыми решетками.
В  частности, если k нечетно, мы не  можем построить кольцевую решетку, но  могли  бы построить
регулярный граф.
Напишите функцию с именем make_regular_graph, которая принимает n и k и возвращает регуляр-
ный граф, содержащий n узлов, где каждый узел имеет k соседей. Если невозможно создать регуляр-
ный граф с заданными значениями n и k, функция должна вызвать ValueError.

Упражнение 3‑2
Моя реализация reachable_nodes_bfs эффективна в  том смысле, что она находится в  O(n  + m),
но  требует много затрат, добавляя узлы в  очередь и  удаляя их. NetworkX обеспечивает простую
и быструю реализацию поиска в ширину, доступную в репозитории NetworkX на GitHub по адресу
https://thinkcomplex.com/connx.
Вот версия, которую я изменил, чтобы вернуть набор узлов:
def plain_bfs(G, start):
seen = set()
nextlevel = {start}
while nextlevel:
thislevel = nextlevel
nextlevel = set()
for v in thislevel:
if v not in seen:
seen.add(v)
nextlevel.update(G[v])
return seen

44  Изучение сложных систем с помощью Python


Сравните эту функцию с reachable_nodes_bfs и посмотрите, что быстрее. Затем посмотрите, може-
те ли вы изменить эту функцию для реализации более быстрой версии: shorttest_path_dijkstra.

Упражнение 3‑2
Приведенная ниже реализация поиска в ширину содержит две ошибки производительности. Что это
за ошибки? Каков фактический порядок роста для этого алгоритма?
def bfs(G, start):
visited = set()
queue = [start]
while len(queue):
curr_node = queue.pop(0) #Убираем из очереди;
visited.add(curr_node)
#Ставим в очередь непосещенные и не поставленные в очередь дочерние объекты;
queue.extend(c for c in G[curr_node]
if c not in visited and c not in queue)
return visited

Упражнение 3‑4
В разделе «Алгоритм Дейкстры» на стр. 43 я утверждал, что алгоритм Дейкстры не работает, если
он не использует поиск в ширину. Напишите версию shortest_path_dijkstra, которая использует
поиск в глубину, и протестируйте ее на нескольких примерах, чтобы увидеть, что идет не так.

Упражнение 3‑5
Естественный вопрос касательно статьи Ваттса и Строгаца заключается в том, является ли феномен
«маленького мира» специфическим для их порождающей модели или другие аналогичные модели
дают такой же качественный результат (высокая кластеризация и низкая длина пути).
Чтобы ответить на этот вопрос, выберите вариант модели Ваттса–Строгаца и повторите эксперимент.
Вы можете рассмотреть два варианта:
• вместо того чтобы начинать с регулярного графа, начните с другого графа с высокой кластери-
зацией. Например, вы можете разместить узлы в случайных местах в двухмерном пространстве
и подключить каждый узел к его ближайшим k соседям;
• экспериментируйте с различными видами переплетения.
Если ряд схожих моделей приводит к схожему поведению, мы говорим, что результаты статьи явля-
ются надежными.

Упражнение 3‑6
Алгоритм Дейкстры решает проблему «кратчайшего пути из одного источника», но для вычисления
характеристической длины пути графа мы на  самом деле хотим решить проблему «кратчайшего
пути из всех пар».
Конечно, одним из вариантов является запуск алгоритма Дейкстры n раз, по одному разу для каждо-
го начального узла. И для некоторых приложений это, вероятно, достаточно хорошо. Но есть и более
эффективные альтернативы.
Найдите алгоритм для задачи кратчайшего пути всех пар и реализуйте его. См. https://thinkcomplex.
com/short.
Сравните время выполнения вашей реализации с запуском алгоритма Дейкстры n раз. Какой алго-
ритм лучше в теории? Что лучше на практике? Какой из них использует NetworkX?

Глава 3. Графы «Мир тесен»   45


Глава 4.
Безмасштабные сети
В этой главе мы будем работать с данными из онлайновой социальной сети и будем использовать
граф Ваттса–Строгаца для их моделирования. Модель Ваттса–Строгаца обладает характеристиками
графа «Мир тесен», такими как данные, но, в отличие от данных, она имеет низкую изменчивость
в количестве соседей от узла к узлу.
Это несоответствие является мотивацией для сетевой модели, разработанной Барабаши и Альбер-
том. Модель БА отражает наблюдаемую изменчивость числа соседей и обладает одним из свойств
маленького мира – ​короткой длиной пути, но  не  обладает высокой кластеризацией графов «Мир
тесен».
Глава заканчивается обсуждением графов Ваттса–Строгаца и БA в качестве объяснительных моде-
лей для графов «Мир тесен».

Данные социальных сетей


Графы Ваттса–Строгаца предназначены для моделирования сетей в  естественных и  социальных
нау­ках.
В своей оригинальной статье Ваттс и Строгац изучили сеть актеров кино (связанных, если они появи-
лись в фильме вместе), электросеть на западе США и сеть нейронов в мозгу нематоды C. elegans. Они
обнаружили, что все эти сети обладали высокой связностью и малой длиной пути, характерными для
графов «Мир тесен».
В этом разделе мы проведем тот  же анализ с  другим набором данных, набором пользователей
Facebook и их друзей. Если вы незнакомы с Facebook, пользователей, которые связаны друг с другом,
называют «друзьями», независимо от характера их отношений в реальном мире.
Я буду использовать данные из проекта сетевого анализа университета Стэнфорда (SNAP), который
совместно использует большие наборы данных из социальных сетей и других источников. В част-
ности, я  буду использовать их данные Facebook1, которые включают в  себя 4039 пользователей
и 88 234 дружеских отношения между ними. Этот набор данных находится в репозитории для дан-
ной книги, но также доступен на сайте SNAP (https://thinkcomplex.com/snap).
Файл данных содержит по одной строке на ребро, причем пользователи идентифицируются с по­
мощью целых чисел от 0 до 4038. Вот код, который читает файл:
def read_graph(filename):
G = nx.Graph()
array = np.loadtxt(filename, dtype=int)
G.add_edges_from(array)
return G

1
J. McAuley and J. Leskovec. Learning to Discover Social Circles in Ego Networks. NIPS, 2012

46  Изучение сложных систем с помощью Python


NumPy предоставляет функцию loadtext, которая читает данный файл и возвращает содержимое
в виде массива NumPy. Параметр dtype указывает на то, что «тип данных» массива – ​int.
Затем мы используем add_edges_from для итерации строк массива и создания ребер. Вот результаты:
>>> fb = read_graph('facebook_combined.txt.gz')
>>> n = len(fb)
>>> m = len(fb.edges())
>>> n, m
(4039, 88234)
Количество узлов и ребер соответствует документации набора данных.
Теперь мы можем проверить, обладает ли этот набор данных характеристиками графа «Мир тесен»:
высокой кластеризацией и низкой длиной пути.
В разделе «Кластеризация» на стр. 37 мы написали функцию для вычисления среднего коэффици-
ента кластеризации в сети. NetworkX предоставляет функцию average_clustering, которая делает
то же самое немного быстрее.
Но для больших графов они оба слишком медленные, занимая время, пропорциональное nk2, где
n – к​ оличество узлов, а k – ​количество соседей, к которым подключен каждый узел.
К счастью, NetworkX предоставляет функцию, которая оценивает коэффициент кластеризации
по случайной выборке. Вот как можно ее вызвать:
from networkx.algorithms.approximation import average_clustering
average_clustering(G, trials=1000)
Следующая функция делает нечто подобное для длины пути:
def sample_path_lengths(G, nodes=None, trials=1000):
if nodes is None:
nodes = list(G)
else:
nodes = list(nodes)
pairs = np.random.choice(nodes, (trials, 2))
lengths = [nx.shortest_path_length(G, *pair)
for pair in pairs]
return lengths
G – ​это граф, nodes – ​это список узлов для выборки, а trials – ​это число случайных путей для выбор-
ки. Если nodes равен None, мы проводим выборку из всего графа.
pairs – ​это массив NumPy случайно выбранных узлов с одной строкой для каждого случайного пути
и двумя столбцами.
Понимание списка перечисляет строки в массиве и вычисляет кратчайшее расстояние между каж­
дой парой узлов. Результатом является список длин пути.
estimate_path_length генерирует список произвольных длин пути и возвращает их среднее зна-
чение:
def estimate_path_length(G, nodes=None, trials=1000):
return np.mean(sample_path_lengths(G, nodes, trials))
Я буду использовать average_clustering для вычисления C:
C = average_clustering(fb)
И estimate_path_length для вычисления L:

Глава 4. Безмасштабные сети   47


L = estimate_path_lengths(fb)
Коэффициент кластеризации составляет около 0,61, что является высоким, как мы ожидаем, если эта
сеть обладает свойством «Мир тесен».
А средний путь составляет 3,7, что довольно мало для сети, насчитывающей более 4000 пользовате-
лей. В конце концов, мир тесен.
Теперь давайте посмотрим, сможем  ли мы построить граф Ваттса–Строгаца, который обладает
теми же характеристиками, что и эта сеть.

Модель Ваттса–Строгаца
В наборе данных Facebook среднее число ребер на  узел составляет около 22. Поскольку каждое
ребро связано с двумя узлами, средняя степень в два раза больше числа ребер на узел:
>>> k = int(round(2*m/n))
>>> k
44
Мы можем построить граф Ваттса–Строгаца, где n = 4039 и k = 44. Когда p = 0, мы получаем кольце-
вую решетку:
lattice = nx.watts_strogatz_graph(n, k, 0)
В этом графе высокая кластеризация: C составляет 0,73 по  сравнению с  0,61 в  наборе данных.
Но L 46, намного выше, чем в наборе данных!
При p = 1 мы получаем случайный граф:
random_graph = nx.watts_strogatz_graph(n, k, 1)
В случайном графе L равно 2,6, даже короче, чем в наборе данных (3,7), но С всего лишь 0,011, что
не есть хорошо.
Методом проб и ошибок мы находим, что когда p = 0,05, то получаем граф Ваттса–Строгаца с высо-
кой кластеризацией и малой длиной пути:
ws = nx.watts_strogatz_graph(n, k, 0.05, seed=15)
В этом графе С равно 0,63, что немного выше, чем в наборе данных, а L – ​3,2, немного ниже, чем
в наборе данных. Таким образом, этот граф хорошо моделирует характеристики маленького мира
набора данных.
Пока все идет нормально.

Степень
Если граф Ваттса–Строгаца является хорошей моделью для сети Facebook, он должен иметь одина-
ковую среднюю степень по узлам и в идеале одинаковую дисперсию в степени.
Данная функция возвращает список степеней в графе, по одному для каждого узла:
def degrees(G):
return [G.degree(u) for u in G]
Средняя степень в модели составляет 44, что близко к средней степени в наборе данных, 43,7.
Однако стандартное отклонение степени в  модели составляет 1,5, что не  близко к  стандартному
отклонению в наборе данных, 52,4. Ой!

48  Изучение сложных систем с помощью Python


В чем проблема? Чтобы получить лучшее представление, мы должны посмотреть на распределение
степеней, а не только на среднее значение и стандартное отклонение.
Я покажу распределение степеней с помощью объекта Pmf, который определен в модуле thinkstats2.
Pmf означает «функция вероятности» (probability mass function-PMF). Если вы незнакомы с  этой
концепцией, то можете прочитать главу 3 книги Think Stats, 2-е изд., по адресу https://thinkcomplex.
com/ts2.
Говоря кратко, Pmf отображается из  значений в  их вероятности. Pmf степеней – ​это отображение
из каждой возможной степени d в долю узлов со степенью d.
В качестве примера я построю граф с узлами 1, 2 и 3, соединенными с центральным узлом, 0:
G = nx.Graph()
G.add_edge(1, 0)
G.add_edge(2, 0)
G.add_edge(3, 0)
nx.draw(G)
Вот список степеней в этом графе:
>>> degrees(G)
[3, 1, 1, 1]
Узел 0 имеет степень 3, остальные имеют степень 1. Теперь я  могу сделать функцию вероятности,
которая представляет это распределение степеней:
>>> from thinkstats2 import Pmf
>>> Pmf(degrees(G))
Pmf({1: 0.75, 3: 0.25})
Результатом является объект Pmf, который отображается из каждой степени в долю или вероятность.
В этом примере 75 % узлов имеют степень 1, а 25 % имеют степень 3.
Теперь мы можем сделать функцию вероятности, которая содержит градусы узлов из набора данных,
и вычислить среднее значение и стандартное отклонение:
>>> from thinkstats2 import Pmf
>>> pmf_fb = Pmf(degrees(fb))
>>> pmf_fb.Mean(), pmf_fb.Std()
(43.691, 52.414)
И то же самое для модели Ваттса–Строгаца:
>>> pmf_ws = Pmf(degrees(ws))
>>> pmf_ws.mean(), pmf_ws.std()
(44.000, 1.465)
Можно использовать модуль thinkplot для вывода результатов:
thinkplot.Pdf(pmf_fb, label='Facebook')
thinkplot.Pdf(pmf_ws, label='WS graph')
На рис. 4‑1 показаны два распределения. Они очень разные.
В модели Ваттса–Строгаца большинство пользователей имеет около 44 друзей; минимум – ​38, мак-
симум – ​50. Это не большая разница. В наборе данных есть много пользователей только с одним или
двумя друзьями, но у одного их более 1000!
Распределения, подобные этому, со множеством небольших значений и несколькими очень больши-
ми значениями, называются «тяжелыми хвостами».

Глава 4. Безмасштабные сети   49


Рисунок 4-1. Функция вероятности степени в наборе
данных Facebook и модели Ваттса–Строгаца

Распределения с тяжелыми хвостами


Распределения с тяжелыми хвостами – ​общая черта многих областей науки о  сложных системах,
и они будут постоянной темой этой книги.
Мы можем получить более четкое представление о распределении с тяжелыми хвостами, нанеся его
на ось в двойной логарифмической шкале, как показано на рис. 4‑2. Это преобразование подчерки-
вает хвост распределения, то есть вероятность больших значений.

Рисунок 4-2. Функция вероятности степени в наборе


данных Facebook и модели Ваттса–Строгаца
на двойной логарифмической шкале

50  Изучение сложных систем с помощью Python


При этом преобразовании данные располагаются приблизительно на  прямой линии, что говорит
о наличии степенного закона между наибольшими значениями в распределении и их вероятностя-
ми. Математически распределение подчиняется степенному закону, если
PMF(k) ∼ k–a,
где PMF(k) – ​это доля узлов со степенью k, α – ​параметр, а символ ∼ указывает, что функция вероят-
ности асимптотична k–a при увеличении k.
Если мы берем логарифм обеих сторон, то получаем
log PMF k ∼ – ​α log k.
Поэтому если распределение следует степенному закону и мы строим PMF(k) против k на двойной
логарифмической шкале, то ожидаем прямую линию с наклоном –α, по крайней мере для больших
значений k.
Все распределения по степенному закону с тяжелым хвостом, но есть и другие распределения с тя-
желым хвостом, которые не подчиняются степенному закону. Мы увидим больше примеров в бли-
жайшее время.
Но сначала у нас есть проблема: модель Ваттса–Строгаца имеет высокую кластеризацию и небольшую
длину пути, которые мы видим в данных, но распределение степеней совсем не похоже на данные.
Это несоответствие является мотивацией для нашей следующей темы, модели Барабаши–Альберта.

Модель Барабаши–Альберта
В 1999 году Барабаши и Альберт опубликовали статью «Возникновение масштабирования в случай-
ных сетях», которая характеризует структуру нескольких реальных сетей, включая графы, представ-
ляющие взаимосвязь актеров кино, веб-страниц и элементов в электрической сети на западе США.
Вы можете скачать эту статью на странице https://thinkcomplex.com/barabasi.
Они измеряют степень каждого узла и вычисляют PMF(k), вероятность того, что вершина имеет сте-
пень k. Затем они строят график PMF(k) против k на двойной логарифмической шкале. Графики со-
ответствуют прямой линии, по крайней мере для больших значений k, поэтому Барабаши и Альберт
пришли к выводу, что эти распределения имеют «тяжелый хвост».
Они также предлагают модель, которая генерирует графы с тем же свойством. Существенными осо-
бенностями модели, которые отличают ее от модели Ваттса–Строгаца, являются:

Рост
Вместо того чтобы начинать с фиксированного количества вершин, модель БA начинается с не-
большого графа и добавляет вершины по одной за раз.

Предпочтительное присоединение
Когда создается новое ребро, оно с большей вероятностью соединится с вершиной, которая уже
имеет большое количество ребер. Этот эффект – ​«богатые богатеют» – ​характерен для моделей
роста некоторых реальных сетей.
Наконец, они показывают, что графы, генерируемые моделью Барабаши–Альберта (БA), имеют рас-
пределение степеней, подчиняющееся степенному закону.
Графы с этим свойством иногда называют безмасштабными сетями, по причинам, которые я не буду
объяснять; если вам интересно, вы можете прочитать подробности на странице https://thinkcomplex.
com/scale.
NetworkX предоставляет функцию, которая генерирует графы БA. Сначала мы применим ее, а потом
я покажу вам, как она работает.

Глава 4. Безмасштабные сети   51


ba = nx.barabasi_albert_graph(n=4039, k=22)
Параметрами являются n, количество генерируемых узлов, и k, количество ребер, с которых начина-
ется каждый узел, когда он добавляется в граф. Я выбрал k = 22, потому что это среднее число ребер
на узел в наборе данных.
Результирующий граф имеет 4039 узлов и  21,9 ребра на  узел. Поскольку каждое ребро связано
с двумя узлами, средняя степень составляет 43,8, что очень близко к средней степени в наборе дан-
ных, 43,7.
И стандартное отклонение степени составляет 40,9, что немного меньше, чем в наборе данных, 52,4,
но это намного лучше, чем то, что мы получили из графа Ваттса–Строгаца, 1,5.
На рис. 4‑3 показаны распределения степеней для набора данных Facebook и модели БA на двой-
ной логарифмической шкале. Модель не идеальна; в частности, она отклоняется от данных, когда
k меньше 10. Но хвост выглядит как прямая линия, что предполагает, что этот процесс генерирует
распределения степеней, которые следуют степенному закону.

Рисунок 4-3. Функция вероятности степени в наборе


данных Facebook и модели Барабаши–
Альберта на двойной логарифмической
шкале

Таким образом, модель БA лучше, чем модель Ваттса–Строгаца, при воспроизведении распределе-
ния степеней. Но обладает ли она свойством «Мир тесен»?
В этом примере средняя длина пути, L, составляет 2,5, что является еще более «маленьким миром»,
чем фактическая сеть, у которой L = 3,69. Так что это хорошо, хотя, может быть, слишком хорошо.
С другой стороны, коэффициент кластеризации C равен 0,037, что далеко от значения в наборе дан-
ных, 0,61. Так что это проблема.
Таблица 4‑1 суммирует эти результаты. Модель Ваттса–Строгаца отражает характеристики малень-
кого мира, но не распределение степеней. Модель БA отражает распределение степеней, по крайней
мере приблизительно, и среднюю длину пути, но не коэффициент кластеризации.
В упражнениях в конце этой главы вы можете изучить другие модели, предназначенные для охвата
всех этих характеристик.

52  Изучение сложных систем с помощью Python


Таблица 4-1. Сравнение характеристик набора данных Facebook с двумя моделями
Facebook Модель Ваттса–Строгаца Модель Барабаши–Альберта
C 0.61 0.63 0.037
L 3.69 3.23 2.51
Степень среднего значения 43.7 44 43.7
Степень стандартного 52.4 1.5 40.1
отклонения
Степенной закон? Возможно Нет Да

Генерация графов Барабаши–Альберта


В предыдущих разделах мы использовали функцию NetworkX для генерации графов БА. Теперь по-
смотрим, как она работает. Вот версия barabasi_albert_graph, с некоторыми изменениями, которые
я сделал, чтобы было проще читать:
def barabasi_albert_graph(n, k):
G = nx.empty_graph(k)
targets = list(range(k))
repeated_nodes = []
for source in range(k, n):
G.add_edges_from(zip([source]*k, targets))
repeated_nodes.extend(targets)
repeated_nodes.extend([source] * k)
targets = _random_subset(repeated_nodes, k)
return G
Параметры – ​это n, количество узлов, которое нам нужно, и k, количество ребер, которые получает
каждый новый узел (что будет средним числом ребер на узел).
Мы начнем с графа, который имеет k узлов и не имеет ребер. Затем инициализируем две перемен-
ные:
targets
Список k узлов, которые будут подключены к следующему узлу. Первоначально targets содер-
жит исходные k узлов; позже он будет содержать случайное подмножество узлов.
repeated_nodes
Список существующих узлов, где каждый узел появляется один раз для каждого ребра, c ко-
торым он соединен. Когда мы выбираем из repeated_nodes, вероятность выбора любого узла
пропорциональна количеству ребер, которые он имеет.
Каждый раз в цикле мы добавляем ребра из источника к каждому узлу в targets.
Затем мы обновляем repeated_nodes, добавляя в каждую список узлов (target) один раз и новый
узел k раз.
Наконец, мы выбираем подмножество узлов, которые будут списками узлов для следующей итера-
ции. Вот определение _random_subset:
def _random_subset(repeated_nodes, k):
targets = set()

Глава 4. Безмасштабные сети   53


while len(targets) < k:
x = random.choice(repeated_nodes)
targets.add(x)
return targets
Каждый раз в цикле _random_subset выбирает из repeat_nodes и добавляет выбранный узел к це-
лям. Поскольку targets – ​это набор, он автоматически удаляет дубликаты, поэтому цикл завершается
только тогда, когда мы выбрали k разных узлов.

Интегральные распределения
На рис. 4‑3 показывает распределение степеней путем построения функции вероятности на двой-
ной логарифмической шкале. Таким образом Барабаши и Альберт представляют свои результаты,
и именно это представление используется чаще всего в статьях о распределениях по степенному
закону. Но это не лучший способ просмотра данных.
Лучшей альтернативой является интегральная функция распределения, которая отображается
из значения x в долю значений, меньшую или равную x.
Учитывая Pmf, самый простой способ вычислить интегральную вероятность состоит в том, чтобы сло-
жить вероятности значений вплоть до x:
def cumulative_prob(pmf, x):
ps = [pmf[value] for value in pmf if value<=x]
return np.sum(ps)
Например, учитывая распределение степеней в наборе данных pmf_fb, мы можем вычислить долю
пользователей с 25 или менее друзьями:
>>> cumulative_prob(pmf_fb, 25)
0.506
Результат близок к 0,5, что означает, что среднее число друзей составляет около 25.
Интегральные функции распределения лучше подходят для визуализации, потому что они менее
шумные, по  сравнению с  функциями вероятности. Как только вы привыкнете интерпретировать
функции распределения, они обеспечат более четкое представление о форме распределения, чем
функции вероятности.
В модуле thinkstats есть класс с именем Cdf, который представляет интегральную функцию рас-
пределения. Мы можем использовать его для вычисления функции распределения степени в наборе
данных.
from thinkstats2 import Cdf
cdf_fb = Cdf(degrees(fb), label='Facebook')
А thinkplot предоставляет функцию под названием Cdf, которая строит графики интегральных
функций распределения.
thinkplot.Cdf(cdf_fb)
На рис.  4‑4 показана интегральная функция степени для набора данных Facebook наряду с  мо­
делью Ваттса–Строгаца (слева) и моделью БA (справа). Ось X находится на двойной логарифмиче-
ской шкале.
Очевидно, что интегральная функция распределения для модели Ваттса–Строгаца сильно отлича-
ется от функции распределения из данных. Модель БA лучше, но все же она не совсем подходит,
особенно для небольших значений.

54  Изучение сложных систем с помощью Python


Рисунок 4-4. Функция распределения степени
в наборе данных Facebook с моделью
Ваттса–Строгаца (слева) и моделью
Барабаши–Альберта (справа)
на логарифмической шкале по оси x

В хвосте распределения (значения больше 100) похоже, что модель БA достаточно хорошо соот-
ветствует набору данных, но это трудно увидеть. Мы можем получить более четкое представление
с  другим представлением данных: построив график дополнительной функции интегрального рас-
пределения на двойной логарифмической шкале.
Дополнительная функция распределения определяется как
CCDF x ≡ 1 – C
​ DF x.
Это определение полезно, потому что если функция вероятности следует степенному закону, допол-
нительная функция распределения также следует этому закону:
−α
⎛ x ⎞
CCDF(x) ∼ ⎜ ,
⎝ x m ⎟⎠

где xm – м
​ инимально возможное значение, а α – ​параметр, определяющий форму распределения.
Взяв логарифм обеих сторон, мы получим:
log CCDF x ∼ – ​α (log x – ​log xm).
Таким образом, если распределение подчиняется степенному закону, мы ожидаем, что дополни-
тельная функция интегрального распределения на двойной логарифмической шкале будет прямой
линией с наклоном –α.
На рис. 4‑5 показана дополнительная функция распределения степени для данных Facebook наряду
с моделью Ваттса–Строгаца (слева) и моделью БA (справа), на двойной логарифмической шкале.
При таком взгляде на  данные мы видим, что модель БА достаточно хорошо совпадает с  хвостом
распределения (значения выше 20). Модель Ваттса–Строгаца – ​нет.

Глава 4. Безмасштабные сети   55


Рисунок 4-5. Дополнительная функция
распределения степени в наборе
данных Facebook с моделью
Ваттса–Строгаца (слева) и моделью
Барабаши–Альберта (справа)
на двойной логарифмической шкале

Объяснительные модели
Мы начали обсуждение сетей с эксперимента Милгрэма «Мир тесен», который показывает, что дли-
на путей в социальных сетях на удивление мала; следовательно, «шесть степеней разделения».
Когда мы видим что-то удивительное, естественно спросить «почему?», но  иногда не  ясно, какой
ответ мы ищем. Один из вариантов ответа – ​это объяснительная модель (см. рис. 4‑6).

Рисунок 4-6. Логическая структура


объяснительной модели

Логическая структура объяснительной модели такова.


1. В системе S мы видим нечто наблюдаемое, O, которое требует объяснения.

56  Изучение сложных систем с помощью Python


2. М
 ы строим модель M, аналогичную системе; то есть существует соответствие между элемента-
ми модели и элементами системы.
3. П
 утем моделирования или математического вывода мы показываем, что модель демонстри-
рует поведение B, аналогичное O.
4. Мы заключаем, что S показывает O, потому что S похож на M, M показывает B, а B похож на O.
По сути, это аргумент по аналогии, который говорит, что если две вещи похожи в некоторых отноше-
ниях, они, вероятно, будут похожи в других отношениях.
Аргументация по аналогии может быть полезной, а объяснительные модели могут быть удовлетвори-
тельными, но они не являются доказательством в математическом смысле слова.
Помните, что все модели не  учитывают или «игнорируют» детали, которые, по  нашему мнению,
не важны. Для любой системы существует множество возможных моделей, которые включают или
игнорируют различные функции. И  могут быть модели, демонстрирующие различные поведения,
которые по-разному похожи на О. В таком случае какая модель объясняет O?
В качестве примера можно привести феномен маленького мира: модель Ваттса–Строгаца и модель
Барабаши–Альберта демонстрируют элементы поведения маленького мира, но они предлагают раз-
ные объяснения:
• модель Ваттса–Строгаца предполагает, что социальные сети являются «небольшими», поскольку
они включают в себя как сильно связанные кластеры, так и «слабые связи», которые связывают
кластеры (см. https://thinkcomplex.com/weak);
• модель Барабаши–Альберта предполагает, что социальные сети являются небольшими, потому
что они включают в  себя узлы с  высокой степенью, которые действуют как хабы, и  что хабы
со временем растут из-за предпочтительного присоединения.
Как это часто бывает в молодых областях науки, проблема не в том, что у нас нет объяснений, а в том,
что их слишком много.

Упражнения
Код, приведенный в этой главе, находится в chap04.ipynb в репозитории для этой книги. Более по­
дробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 4‑1
В разделе «Объяснительные модели» на стр. 56 мы обсудили два объяснения феномена маленького
мира: «слабые связи» и «хабы». Совместимы ли эти объяснения; то есть могут ли они оба быть пра-
вы? Какое объяснение вы находите более удовлетворительным и почему?
Существуют ли данные, которые вы могли бы собрать, или эксперименты, которые вы могли бы про-
вести, которые предоставили бы доказательства в пользу одной модели по сравнению с другой?
Выбор среди конкурирующих моделей – ​тема эссе Томаса Куна (Thomas Kuhn) «Объективность, цен-
ностная оценка и выбор теории», которое вы можете прочитать на странице https://thinkcomplex.com/
kuhn.
Какие критерии предлагает Кун для выбора среди конкурирующих моделей? Эти критерии влияют
на ваше мнение о моделях Ваттса–Строгаца и БA? Существуют ли другие критерии, которые, по ва-
шему мнению, следует учитывать?

Упражнение 4‑2
NetworkX предоставляет функцию powerlaw_cluster_graph, которая реализует «алгоритм Холма
и Кима для растущих графов с распределением степеней по степенному закону и приблизительной
средней кластеризацией». Прочитайте документацию к  этой функции (https://thinkcomplex.com/hk)

Глава 4. Безмасштабные сети   57


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

Упражнение 4‑3
Файлы данных из статьи Барабаши и Альберта доступны по адресу https://thinkcomplex.com/netdata.
Их данные о сотрудничестве актеров включены в репозиторий этой книги в файле с именем actor.
dat.gz. Приведенная ниже функция читает файл и создает граф:
import gzip
def read_actor_network(filename, n=None):
G = nx.Graph()
with gzip.open(filename) as f:
for i, line in enumerate(f):
nodes = [int(x) for x in line.split()]
G.add_edges_from(thinkcomplexity.all_pairs(nodes))
if n and i >= n:
break
return G
Вычислите количество актеров в графе и среднюю степень. Нарисуйте график функции вероятно-
сти степени на двойной логарифмической шкале. Также нарисуйте график функции интегрального
распределения степени на шкале x, чтобы увидеть общую форму распределения, и на двойной лога-
рифмической шкале, чтобы увидеть, следует ли хвост степенному закону.
Примечание: сеть актеров не  является связной, поэтому вы можете использовать nx.connected_
component_subgraphs для поиска связных подмножеств узлов.

58  Изучение сложных систем с помощью Python


Глава 5.
Клеточные автоматы
Клеточный автомат (КА) – ​это модель мира с очень простой физикой. «Клеточный» означает, что мир
разделен на отдельные куски, называемые ячейками. «Автомат» – ​это машина, которая выполняет
вычисления, – ​это может быть реальная машина, но чаще всего «машина» – ​это математическая аб-
стракция или компьютерное моделирование.
В этой главе представлены эксперименты, выполненные Стивеном Вольфрамом (Stephen Wolfram)
в 80-х гг., демонстрирующие, что некоторые клеточные автоматы демонстрируют удивительно слож-
ное поведение, в том числе способность выполнять произвольные вычисления.
Я обсуждаю последствия этих результатов и в конце главы предлагаю методы эффективной реали-
зации клеточных автоматов в Python.

Простой клеточный автомат


Клеточные автоматы регулируются правилами, которые определяют, как состояние ячеек изменяет-
ся со временем. В качестве тривиального примера рассмотрим клеточный автомат с одной ячейкой.
Состояние ячейки во  время временного шага i  является целым числом, xi. В  качестве начального
условия предположим, что x0 = 0.
Теперь все, что нам нужно, – ​это правило. Произвольно я выберу xi + 1 = xi + 1, что говорит о том, что
на каждом временном шаге состояние клеточного автомата увеличивается на 1. Так что этот автомат
выполняет простое вычисление: он считает.
Но данный автомат нетипичен; обычно число возможных состояний ограничено. Например, пред-
положим, что ячейка может иметь только одно из двух состояний: 0 или 1. Для клеточного автомата
с двумя состояниями мы можем написать правило, например, xi + 1 = (xi + 1) % 2, где % – ​оператор
деления по модулю.
Поведение этого клеточного автомата простое: он мигает. То есть состояние ячейки переключается
между 0 и 1 в течение каждого временного шага.
Многие клеточные автоматы являются детерминированными, это означает, что в правилах нет слу-
чайных элементов; при одинаковом начальном состоянии они всегда дают одинаковый результат.
Но некоторые автоматы являются недетерминированными; мы увидим примеры далее.
У клеточного автомата в этом разделе есть только одна ячейка, поэтому мы можем рассматривать
его как нульмерный. В оставшейся части этой главы мы исследуем одномерные клеточные автоматы
(1D), а в следующей главе рассмотрим двумерные автоматы.

Эксперимент Вольфрама
В начале 80-х гг. Стивен Вольфрам опубликовал серию работ, в которых систематически изучались
одномерные клеточные автоматы. Он выделил четыре категории поведения, каждая из которых ин-

Глава 5. Клеточные автоматы   59


тереснее предыдущей. Вы можете прочитать одну из этих статей, «Статистическая механика клеточ-
ных автоматов», на странице https://thinkcomplex.com/ca.
В экспериментах Вольфрама ячейки расположены в  решетке (которую вы, возможно, помните
из раздела «Ваттс и Строгац» на стр. 34), где каждая ячейка связана с двумя соседями. Решетка мо-
жет быть ограниченной, бесконечной или располагаться в кольце.
Правила, которые определяют, как система развивается во времени, основаны на понятии «окрест-
ности», которое представляет собой набор ячеек, определяющих последующее состояние данной
ячейки.
В экспериментах Вольфрама используется окрестность из трех ячеек: сама ячейка и два ее соседа.
В этих экспериментах ячейки имеют два состояния, обозначенных как 0 и 1, поэтому правило можно
суммировать с помощью таблицы, которая отображает состояние окрестности (кортеж из трех состоя­
ний) в последующее состояние центральной ячейки. В приведенной ниже таблице дается пример:
prev 111 110 101 100 011 010 001 000
next 0 0 1 1 0 0 1 0
Первая строка показывает восемь состояний, в которых может находиться окрестность. Вторая стро-
ка показывает состояние центральной ячейки во время следующего временного шага. В качестве
краткой кодировки этой таблицы Вольфрам предложил читать нижнюю строку как двоичное число;
поскольку 00110010 в двоичном коде – ​это 50 в десятичном виде, Вольфрам назвал этот клеточный
автомат «Правило 50».
На рис. 5‑1 показано влияние Правила 50 на 10 временных шагов. Первая строка показывает состоя­
ние системы в течение первого временного шага; он начинается с одной ячейки «вкл», а остальные
«выкл». Во второй строке показано состояние системы во время следующего временного шага и т. д.

Рисунок 5-1.  Правило 50 после 10 временных шагов

Треугольная форма, изображенная на рисунке, типична для этих клеточных автоматов; это следствие
формы окрестности. За один временной шаг каждая ячейка влияет на состояние одного соседа в лю-
бом направлении. В  течение следующего временного шага это влияние может распространяться
на еще одну ячейку в каждом направлении. Таким образом, каждая ячейка в прошлом имеет «тре­
угольник влияния», который включает в себя все ячейки, на которые она может воздействовать.

Классификация клеточных автоматов


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

60  Изучение сложных систем с помощью Python


лицах правил есть 8 записей. А так как каждая запись содержит один бит, мы можем определить
таблицу с использованием 8 бит. С помощью 8 бит мы можем установить 256 различных правил.
Одним из первых экспериментов Вольфрама с клеточными автоматами было тестирование всех 256
возможностей и их классификация.
Изучив результаты визуально, он предположил, что поведение клеточных автоматов можно сгруп-
пировать в  четыре класса. Класс 1 содержит самые простые (и  наименее интересные) автоматы,
которые развиваются из  почти любого начального состояния в  один и тот  же шаблон. В  качестве
тривиального примера: правило 0 всегда генерирует пустой шаблон после одного временного шага.
Правило 50 является примером класса 2. Оно генерирует простой шаблон с вложенной структурой,
то есть шаблон, который содержит множество меньших версий самого себя. Правило 18 делает вло-
женную структуру еще более понятной. На рис. 5‑2 показано, как это выглядит после 64 шагов.

Рисунок 5-2.  Правило 18 после 64 временных шагов

Этот шаблон напоминает треугольник Серпинского, о  котором вы можете прочитать на  странице
https://thinkcomplex.com/sier.
Некоторые клеточные автоматы класса 2 генерируют сложные и красивые шаблоны, но по сравне-
нию с классами 3 и 4 они относительно просты.

Хаотичность
Класс 3 содержит клеточные автоматы, которые генерируют хаотичность. Примером является прави-
ло 30. На рис. 5‑3 показано, как это выглядит после 100 временных шагов.
Вдоль левой стороны виден узор, а справа – ​треугольники разных размеров, но центр кажется до-
вольно случайным. На  самом деле, если вы берете центральный столбец и  будете рассматривать
его как последовательность битов, трудно отличить его от действительно случайной последователь-
ности. Он проходит многие статистические тесты, которые используются для проверки случайности
последовательности битов.

Глава 5. Клеточные автоматы   61


Рисунок 5-3. Правило 30 после 100 временных
шагов

Программы, которые производят случайные числа, называются генераторами псевдослучайных чи-


сел. Они не считаются действительно случайными, потому что:
• многие из  них производят последовательности генератора псевдослучайных чисел с  законо-
мерностями, которые можно обнаружить статистически. Например, оригинальная реализация
rand в библиотеке C использовала линейный конгруэнтный генератор, который давал последо-
вательности с легко обнаруживаемыми последовательными корреляциями;
• любой генератор псевдослучайных чисел, который использует конечное количество состояния
(то есть хранилище), в конечном итоге будет повторяться. Одной из характеристик генератора
является период этого повторения;
• основной процесс является в основном детерминированным, в отличие от некоторых физиче-
ских процессов, таких как радиоактивный распад и тепловой шум, которые, по сути, считаются
случайными.
Современные генераторы псевдослучайных чисел производят последовательности, которые ста-
тистически неотличимы от  случайных, и  они могут быть реализованы с  периодами так долго, что
вселенная разрушится, прежде чем они повторятся. Существование этих генераторов ставит вопрос
о том, существует ли реальное отличие между псевдослучайной последовательностью хорошего ка-
чества и  последовательностью, сгенерированной «истинно» случайным процессом. В  своей книге
«Наука нового типа» Вольфрам утверждает, что нет (стр. 315–326).

Детерминизм
Существование клеточного автомата класса 3 удивительно. Чтобы объяснить, насколько удивитель-
но, позвольте мне начать с философского детерминизма (см. https://thinkcomplex.com/deter). Многие
философские позиции трудно определить именно потому, что они бывают разных вкусов.
Я часто нахожу полезным определить их с помощью списка утверждений, упорядоченных от слабых
к сильным.

D1
Детерминированные модели могут делать точные прогнозы для некоторых физических систем.

62  Изучение сложных систем с помощью Python


D2
Многие физические системы могут моделироваться детерминированными процессами, но не-
которые по своей природе случайны.

D3
Все события вызваны предшествующими событиями, но многие физические системы тем не ме-
нее, по сути, непредсказуемы.

D4
Все события вызваны предыдущими событиями и  могут (по  крайней мере, в  принципе) быть
предсказаны.
Моя цель в построении этого диапазона состоит в том, чтобы сделать D1 настолько слабым, что фак-
тически все его примут, а D4 – ​настолько сильным, что почти никто не примет его, с промежуточными
утверждениями, которые некоторые люди принимают.
Центр масс мирового мнения колеблется вдоль этого диапазона в ответ на исторические события
и научные открытия. До научной революции многие люди считали работу вселенной в принципе не-
предсказуемой или контролируемой сверхъестественными силами. После триумфов механики Нью-
тона некоторые оптимисты поверили в нечто вроде D4. Например, в 1814 году Пьер-Симон Лаплас
(Pierre-Simon Laplace) писал:

Мы можем рассматривать нынешнее состояние вселенной как следствие ее прошлого и причи-


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

Этот «интеллект» стали называть «Демон Лапласа». См.  https://thinkcomplex.com/demon. Слово «де-
мон» в данном контексте имеет значение «дух», без каких-либо злых последствий.
Открытия XIX и  XX  веков постепенно разрушили надежду Лапласа. Термодинамика, радиоактив-
ность и квантовая механика создавали последовательные вызовы сильным формам детерминизма.
В 60-х годах теория хаоса показала, что в  некоторых детерминированных системах прогнозиро-
вание возможно только на  коротких временных масштабах, ограниченных точностью измерения
начальных условий.
Большинство этих систем непрерывно в пространстве (если не во времени) и нелинейно, поэтому
сложность их поведения не совсем удивительна. Демонстрация Вольфрамом сложного поведения
в простых клеточных автоматах более удивительна и мешает, по крайней мере, детерминистическо-
му взгляду на мир.
До сих пор я фокусировался на научных вызовах детерминизму, но самым громким возражением
является очевидный конфликт между детерминизмом и свободной волей человека.
Наука о сложных системах обеспечивает возможное разрешение этого конфликта. Я вернусь к этой
теме в разделе «Эмерджентность и свобода воли» на стр. 129.

Глава 5. Клеточные автоматы   63


Космические корабли
Поведение клеточных автоматов класса 4 еще более удивительно. Несколько клеточных автоматов,
прежде всего Правило 110, являются полными по Тьюрингу, что означает, что они могут вычислять
любую вычислимую функцию. Это свойство, также называемое универсальностью, было доказано
Мэттью Куком (Matthew Cook) в 1998 году. См. https://thinkcomplex.com/r110.
На рис. 5‑4 показано, как выглядит Правило 110 с начальным состоянием одной ячейки и 100 вре-
менными шагами. В этом временном масштабе не очевидно, что происходит что-то особенное. Есть
некоторые закономерности, но также есть некоторые особенности, которые трудно охарактеризовать.

Рисунок 5-4. Правило 110 после 100 временных


шагов

На рис. 5‑5 показано изображение побольше, начиная со случайного начального условия и 600 вре-
менных шагов.

Рисунок 5-5. Правило 110 со случайными


начальными условиями и 600
временными шагами

64  Изучение сложных систем с помощью Python


Примерно через 100 шагов фон превращается в простой повторяющийся узор, но есть ряд посто-
янных структур, которые появляются как фоновые помехи. Некоторые из этих структур стабильны,
поэтому они выглядят как вертикальные линии. Другие переводятся в  пространство, выглядя как
диагонали с различными уклонами, в зависимости от того, сколько временных шагов они делают,
чтобы сместиться на один столбец. Эти структуры называются космическими кораблями.
Столкновения между космическими кораблями дают различные результаты в  зависимости от  ти-
пов космических кораблей и фазы, в которой они находятся, когда сталкиваются. В результате од-
них столкновений уничтожаются оба корабля; в ходе других остается один корабль без изменений;
а пос­ле третьих образуется один или несколько кораблей разных типов.
Эти коллизии являются основой вычислений в Правиле 110. Если вы рассматриваете космические
корабли как сигналы, которые распространяются в пространстве, а столкновения как шлюзы, кото-
рые вычисляют логические операции, такие как AND и OR, вы можете увидеть, что значит для кле-
точного автомата выполнение вычислений.

Универсальность
Чтобы понять универсальность, мы должны понять теорию вычислимости, которая касается моделей
вычислений и того, что они вычисляют.
Одной из наиболее общих моделей вычислений является машина Тьюринга, которая представляет
собой абстрактный компьютер, предложенный Аланом Тьюрингом (Alan Turing) в 1936 году. Машина
Тьюринга – ​это одномерный клеточный автомат, бесконечный в обоих направлениях, дополненный
головкой чтения-записи. В любое время головка расположена над одной ячейкой. Она может прочи-
тать состояние ячейки (обычно это только два состояния) и записать новое значение в ячейку.
Кроме того, машина имеет регистр, в котором записывается состояние машины (одно из конечного
числа состояний), и таблицу правил. Для каждого состояния машины и состояния ячейки в таблице
указывается действие. Действия включают в себя изменение ячейки, когда головка останавливается,
и перемещение одной ячейки влево или вправо.
Машина Тьюринга не  является практичным проектом компьютера, но  она моделирует обычные
компью­терные архитектуры. Для данной программы, работающей на  реальном компьютере, воз-
можно (по крайней мере, в принципе) построить машину Тьюринга, которая выполняет эквивалент-
ное вычисление.
Машина Тьюринга полезна, потому что она дает возможность охарактеризовать набор функций, ко-
торые могут быть вычислены машиной Тьюринга, что и  сделал Тьюринг. Функции в  этом наборе
называются «вычислимыми по Тьюрингу».
Сказать, что машина Тьюринга может вычислить любую вычислимую по Тьюрингу функцию, – ​тавто-
логия: это верно по определению. Но вычислимость по Тьюрингу более интересна.
Оказывается, что любая разумная модель вычислений, которую кто-либо придумал, является «пол-
ной по Тьюрингу»; то есть она может вычислять точно такой же набор функций, что и машина Тью-
ринга. Некоторые из  этих моделей, такие как лямбда-исчисление, сильно отличаются от  машины
Тьюринга, поэтому их эквивалентность удивительна.
Это наблюдение привело к тезису Черча–Тьюринга, согласно которому эти определения вычислимо-
сти охватывают нечто существенное, независимое от какой-либо конкретной модели вычислений.
Правило 110 – ​это еще одна модель вычислений, отличающаяся своей простотой.
То, что оно тоже оказывается полным по Тьюрингу, подтверждает тезис Черча–Тьюринга.
В своей книге «Наука нового типа» Вольфрам приводит вариант этого тезиса, который он называет
«принципом вычислительной эквивалентности» (см. https://thinkcomplex.com/equiv):

Глава 5. Клеточные автоматы   65


Почти все процессы, которые не являются явно простыми, могут рассматриваться как вычисле-
ния эквивалентной сложности.
Говоря более конкретно, принцип вычислительной эквивалентности гласит, что системы, встре-
чающиеся в естественном мире, могут выполнять вычисления вплоть до максимального («уни-
версального») уровня вычислительной мощности и что большинство систем действительно до-
стигает этого максимального уровня вычислительной мощности. Следовательно, большинство
систем вычислительно эквивалентно.

Применяя эти определения к клеточным автоматам, классы 1 и 2 «очевидно просты». Может быть
менее очевидно, что класс 3 прост, но в некотором смысле идеальная случайность так же проста, как
и  идеальный порядок; сложность происходит между ними. Таким образом, заявление Вольфрама
состоит в том, что поведение класса 4 распространено в мире природы и что почти все системы,
которые его проявляют, эквивалентны в вычислительном отношении.

Фальсифицируемость
Вольфрам считает, что его принцип – ​более сильное утверждение, чем тезис Черча–Тьюринга, пото-
му что речь идет о мире природы, а не об абстрактных моделях вычислений. Но высказывание о том,
что естественные процессы «можно рассматривать как вычисления», кажется мне скорее утвержде-
нием о выборе теории, чем гипотезой о мире природы.
Кроме того, с  такими оценками, как «почти», и  такими неопределенными терминами, как «оче-
видно просто», его гипотеза может быть нефальсифицируема. Фальсифицируемость – ​это идея
философии науки, предложенная Карлом Поппером (Karl Popper) как разграничение между на-
учными гипотезами и  лженаукой. Гипотеза является фальсифицируемой, если есть эксперимент,
по  крайней мере в  отношении практичности, которая противоречила  бы гипотезе, если  бы она
была ложной.
Например, утверждение о том, что вся жизнь на Земле происходит от общего предка, является фаль-
сифицируемым, поскольку оно делает конкретные прогнозы относительно сходства в генетике со-
временных видов (среди прочего). Если бы мы обнаружили новый вид, чья ДНК почти полностью
отличалась от нашей, это противоречило бы (или, по крайней мере, ставило под сомнение) теории
всеобщего происхождения.
С другой стороны, «особое творение», утверждение о том, что все виды были созданы в их нынеш-
нем виде сверхъестественным агентом, является нефальсифицируемым, потому что нет ничего, что
мы могли бы наблюдать в мире природы, что противоречило бы ему. Любой результат любого экспе-
римента можно отнести к воле создателя.
Нефальсифицируемые гипотезы могут быть привлекательными, потому что их невозможно опро-
вергнуть. Если вы хотите, чтобы ваши утверждения нельзя было опровергнуть, вы должны выбирать
гипотезы, которые являются как можно более нефальсифицируемыми.
Но если ваша цель состоит в том, чтобы делать надежные прогнозы о мире – ​и это, по крайней мере,
одна из целей науки, – ​нефальсифицируемые гипотезы бесполезны. Проблема состоит в том, что они
не имеют последствий (если бы они имели последствия, они были бы фальсифицируемыми).
Например, если бы теория особого творения была правдой, какая мне от этого польза? Это ничего
не скажет мне о создателе, за исключением того, что он обладает «чрезмерной любовью к жукам»
(приписывается Дж. Б. С. Холдейну). И в отличие от теории общего происхождения, которая инфор-
мирует многие области науки и биоинженерии, она была бы бесполезна для понимания мира или
действия в нем.

66  Изучение сложных систем с помощью Python


Что это за модель?
Некоторые клеточные автоматы – ​это прежде всего математические артефакты. Они интересны, по-
тому что удивительны, или полезны, или симпатичны, или потому что предоставляют инструменты
для создания новой математики (например, тезис Черча–Тьюринга).
Но не очевидно, что они являются моделями физических систем. И если это так, они очень абстракт-
ны, то есть не очень подробны или реалистичны.
Например, некоторые виды конусных улиток производят рисунок на своих раковинах, который на-
поминает шаблоны, генерируемые клеточными автоматами (см. https://thinkcomplex.com/cone). По-
этому естественно предположить, что клеточный автомат является моделью механизма, который
производит узоры на раковинах, по мере того как они растут. Но, по крайней мере, на начальном
этапе не ясно, как элементы модели (так называемые ячейки, общение между соседями, правила)
соответствуют элементам растущей улитки (реальные ячейки, химические сигналы, сети взаимодей-
ствия белков).
Для обычных физических моделей реалистичность – ​это добродетель. Если элементы модели соот-
ветствуют элементам физической системы, существует очевидная аналогия между моделью и систе-
мой. В целом мы ожидаем, что модель будет более реалистичной, чтобы делать лучшие прогнозы
и давать более правдоподобные объяснения.
Конечно, это верно только до определенного момента. С более подробными моделями сложнее ра-
ботать, и, как правило, они труднее поддаются анализу. В какой-то момент модель становится на-
столько сложной, что с системой легче экспериментировать.
С другой стороны, простые модели могут быть привлекательными именно потому, что они простые.
Простые модели предлагают объяснения иного рода, нежели подробные модели. В  случае с  по­
дробной моделью аргумент выглядит примерно так: «Мы заинтересованы в физической системе S,
поэтому мы строим подробную модель M и  показываем путем анализа и  моделирования, что M
демонстрирует поведение B, которое аналогично (качественно или количественно) наблюдению
реальной системы О. Так почему же происходит O? Потому что S похож на M, а B похож на O и мы
можем доказать, что M ведет к B».
С простыми моделями мы не можем утверждать, что S похож на M, потому что это не так. Вместо
этого аргумент выглядит так: «Существует набор моделей, которые обладают общим набором при-
знаков. Любая модель, имеющая данные признаки, демонстрирует поведение B. Если мы выполним
наблюдение O, которое напоминает B, один из  способов объяснить это – ​показать, что система S
обладает набором признаков, достаточных для получения B».
Для такого рода аргументов добавление дополнительных признаков не помогает. Создание более
реалистичной модели не делает модель более надежной; это только затемняет разницу между су-
щественными признаками, которые вызывают B, и случайными признаками, которые характерны
для S.
На рис.  5‑6 показана логическая структура модели такого типа. Признаков x и  y достаточно для
создания поведения. Добавление большего количества деталей, таких как признаки w и  z, может
сделать модель более реалистичной, но этот реализм не добавляет объяснительной силы.

Глава 5. Клеточные автоматы   67


Рисунок 5-6. Логическая структура простой
физической модели

Реализация клеточных автоматов


Чтобы сгенерировать рисунки для этой главы, я написал класс Cell1D на Python, представляющий
одномерный клеточный автомат, и класс Cell1DViewer для построения графиков результатов. Оба
определены в Cell1D.py в репозитории для этой книги.
Чтобы сохранить состояние клеточного автомата, я использую массив NumPy с одним столбцом для
каждой ячейки и одной строкой для каждого временного шага.
Чтобы объяснить, как работает моя реализация, я начну с клеточного автомата, который вычисляет
четность ячеек в каждой окрестности. «Четность» числа равна 0, если число четное, и 1, если оно
нечетное.
Я использую функцию NumPy zeros для создания массива нулей, а затем помещаю 1 в середине
первой строки.
rows = 5
cols = 11
array = np.zeros((rows, cols), dtype=np.uint8)
array[0, 5] = 1
print(array)
[[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
Тип данных uint8 указывает, что элементы массива являются 8-разрядными целыми числами без
знака. plot_ca отображает элементы массива графически:
import matplotlib.pyplot as plt
def plot_ca(array, rows, cols):
cmap = plt.get_cmap('Blues')
plt.imshow(array, cmap=cmap, interpolation='none')

68  Изучение сложных систем с помощью Python


Я импортирую pyplot с сокращенным именем plt, что обычно. Функция get_cmap возвращает карту
цветов, которая отображается из  значений в  массиве в  цвета. Карта цветов 'Blues' отображает
включенные ячейки темно-синим цветом, а выключенные – ​светло-синим.
imshow отображает массив в виде «изображения»; то есть он рисует цветной квадрат для каждого
элемента массива. Установка interpolation в none означает, что imshow не должен интерполиро-
вать между включенными и выключенными ячейками.
Чтобы вычислить состояние клеточного автомата в течение временного шага i, мы должны сложить
последовательные элементы массива и вычислить четность суммы. Мы можем сделать это, используя
оператор среза для выбора элементов и оператор модуля для вычисления четности:
def step(array, i):
rows, cols = array.shape
row = array[i‑1]
for j in range(1, cols):
elts = row[j‑1: j+2]
array[i, j] = sum(elts)% 2
rows и cols – э​ то размеры массива. row – ​это предыдущая строка массива.
Каждый раз в  цикле мы выбираем три элемента из  строки, складываем их, вычисляем четность
и сохраняем результат в строке i.
В этом примере решетка конечна, поэтому первая и последняя ячейки имеют только одного соседа.
Чтобы обработать этот особый случай, я не обновляю первый и последний столбцы; они всегда 0.

Взаимная корреляция
Операция, описанная в  предыдущем разделе, – ​выбор элементов из  массива и  добавление их –​
является примером операции, которая настолько полезна во многих доменах, что у нее есть имя:
взаимная корреляция. А NumPy предоставляет функцию с именем correlate, которая ее вычисляет.
В этом разделе я покажу, как мы можем использовать NumPy для написания более простой и быст­
рой версии шага.
Функция correlate принимает массив a и «окно» w с длиной N и вычисляет новый массив c, где
элемент k представляет собой следующую сумму:
N−1
ck = ∑ an+k ⋅w n .
n=0

Мы можем написать эту операцию на языке Python таким образом:


def c_k(a, w, k):
N = len(w)
return sum(a[k: k+N] * w)
Эта функция вычисляет элемент k корреляции между a и w. Чтобы показать, как это работает, я соз-
дам массив целых чисел:
N = 10
row = np.arange(N, dtype=np.uint8)
print(row)
[0 1 2 3 4 5 6 7 8 9]
И окно:

Глава 5. Клеточные автоматы   69


window = [1, 1, 1]
print(window)
В этом окне каждый элемент, c_k, является суммой последовательных элементов из a:
c_k(row, window, 0)
3
c_k(row, window, 1)
6
Мы можем использовать c_k для написания correlate, которая вычисляет элементы c для всех зна-
чений k, где окно и массив перекрываются:
def correlate(row, window):
cols = len(row)
N = len(window)
c = [c_k(row, window, k) for k in range(cols-N+1)]
return np.array(c)
Вот результат:
c = correlate(row, window)
print(c)
[3 6 9 12 15 18 21 24]
Функция correlate делает то же самое:
c = np.correlate(row, window, mode='valid')
print(c)
[3 6 9 12 15 18 21 24]
Аргумент mode = 'valid' означает, что результат содержит только элементы, в которых перекрыва-
ются окно и массив, которые считаются действительными.
Недостатком этого режима является то, что результат не совпадает с размером массива. Мы можем
исправить это с помощью mode = 'same', который добавляет нули в начало и конец массива:
c = np.correlate(row, window, mode='same')
print(c)
[1 3 6 9 12 15 18 21 24 17]
Теперь результат равен размеру массива. В  качестве упражнения в  конце этой главы у  вас будет
возможность написать версию функции correlate, которая делает то же самое.
Мы можем использовать реализацию correlate в NumPy для написания простой и более быстрой
версии шага:
def step2(array, i, window=[1,1,1]):
row = array[i‑1]
c = np.correlate(row, window, mode='same')
array[i] = c% 2
В блокноте для этой главы вы увидите, что step2 дает те же результаты, что и step.

70  Изучение сложных систем с помощью Python


Таблицы клеточных автоматов
Функция, которая у нас есть, работает, если клеточный автомат является «тоталитическим», что озна­
чает, что правила зависят только от суммы соседей. Но большинство правил также зависит от того,
какие соседи включены и выключены. Например, 100 и 001 имеют одинаковую сумму, но для многих
клеточных автоматов они дают разные результаты.
step2 можно сделать более общим, используя окно с элементами [4, 2, 1], которое интерпретирует
окрестность как двоичное число. Например, окрестность 100 дает 4, 010 дает 2, а 001–1. Затем мы
можем взять эти результаты и посмотреть их в таблице правил.
Вот более общая версия step2:
def step3(array, i, window=[4,2,1]):
row = array[i‑1]
c = np.correlate(row, window, mode='same')
array[i] = table[c]
Первые две строки одинаковы. Затем последняя строка ищет каждый элемент из c в таблице и при-
сваивает результат массиву [i].
Вот функция, которая вычисляет таблицу:
def make_table(rule):
rule = np.array([rule], dtype=np.uint8)
table = np.unpackbits(rule)[::-1]
return table
Параметр, rule, является целым числом от 0 до 255. Первая строка помещает rule в массив с одним
элементом, поэтому мы можем использовать unpackbits, который преобразует номер правила в его
двоичное представление. Например, вот таблица для правила 150:
>>> table = make_table(150)
>>> print(table)
[0 1 1 0 1 0 0 1]
Код в этом разделе инкапсулирован в классе Cell1D, определенной в Cell1D.py в репозитории для
этой книги.

Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap05.ipynb в репозитории для этой
книги. Откройте этот блокнот, прочитайте код и запустите ячейки. Вы можете использовать этот блок-
нот для работы над упражнениями в этой главе. Мои решения находятся в chap05soln.ipynb. Более
подробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 5‑1
Напишите версию функции correlate, которая возвращает тот  же результат, что и  np.correlate
с mode = 'same'. Подсказка: используйте панель функций NumPy.

Упражнение 5‑2
В этом упражнении вас попросят поэкспериментировать с Правилом 110 и его космическими ко-
раблями.

Глава 5. Клеточные автоматы   71


1. П
 рочтите страницу на Википедии, посвященную Правилу 110, в которой описан его фоновый
шаблон и космические корабли: https://thinkcomplex.com/r110.
2. С
 оздайте Правило 110 с  начальным условием, которое приводит к  стабильному фоновому
шаблону.
Обратите внимание, что класс Cell1D предоставляет start_string, которая позволяет инициа­
лизировать состояние массива, используя строку из единиц и нулей.
3. И
 змените начальное условие, добавив различные шаблоны в  центре строки, и  посмотрите,
какие из  них дают космические корабли. Вы могли  бы хотеть перечислить все возможные
образцы n бит для некоторого разумного значения n. Можете ли вы найти период и скорость
трансляции для каждого космического корабля? Какой самый большой космический корабль
вы можете найти?
4. Что происходит при столкновении космических кораблей?

Упражнение 5‑3
Цель данного упражнения – ​реализация машины Тьюринга.
1. Прочтите о машинах Тьюринга на странице https://thinkcomplex.com/tm.
2. Н
 апишите класс с  именем Turing, который реализует машину Тьюринга. Для таблицы дей-
ствий используйте правила для усердного бобра с тремя состояниями.
3. Напишите класс с именем TuringViewer, который генерирует изображение, представляющее
состояние ленты, а также положение и состояние головки. Пример того, как это может выгля-
деть, см. на странице https://thinkcomplex.com/turing.

Упражнение 5‑4
В этом упражнении вас попросят внедрить и протестировать несколько генераторов псевдослучай-
ных чисел. Для тестирования вам потребуется установить DieHarder, который вы можете загрузить
по адресу https://thinkcomplex.com/dh, или он может быть доступен в виде пакета для вашей опера-
ционной системы.
1. Напишите программу, которая реализует один из линейных конгруэнтных генераторов, опи-
санных на странице https://thinkcomplex.com/lcg. Протестируйте ее с помощью DieHarder.
2. Прочитайте документацию к модулю Python random. Какой из генераторов псевдослучайных
чисел он использует?
Протестируйте его.
3. Реализуйте Правило 30 с несколькими сотнями ячеек, запустите его на стольких временных
шагах, сколько возможно за разумное время, и выведите центральный столбец в виде после-
довательности битов. Протестируйте его.

Упражнение 5‑5
Фальсифицируемость является привлекательной и полезной идеей, но среди философов науки она,
как утверждает Поппер, не является общепринятой в качестве решения проблемы демаркации.
Прочитайте https://thinkcomplex.com/false и ответьте на следующие вопросы.
1. В чем состоит проблема демаркации?
2. Как, по словам Поппера, фальсифицируемость решает проблему демаркации?
3. Приведите пример двух теорий, одна из  которых считается научной, а  другая – ​ненаучной,
которые успешно различаются по критерию фальсифицируемости.
4. Можете ли вы обобщить одно или несколько возражений, выдвинутых философами и истори-
ками науки в ответ на утверждение Поппера?
5. Есть ли у вас ощущение, что практикующие философы высоко ценят работу Поппера?

72  Изучение сложных систем с помощью Python


Глава 6.
Игра «Жизнь»
В этой главе мы рассмотрим двумерные клеточные автоматы, особенно «Игру жизни» Джона Конвея.
Как и некоторые одномерные клеточные автоматы, о которых шла речь в предыдущей главе, игра
«Жизнь» следует простым правилам и производит удивительно сложное поведение. И подобно Пра-
вилу 110 Вольфрама, игра «Жизнь» оказывается универсальной, то есть она может вычислять любую
вычислимую функцию, по крайней мере в теории.
Сложное поведение в игре поднимает вопросы в философии науки, особенно связанные с научным
реализмом и  инструментализмом. Я  обсуждаю эти вопросы и  предлагаю список дополнительной
литературы.
В конце главы я продемонстрирую способы эффективной реализации игры «Жизнь» на языке Python.

Игра «Жизнь» Конвея


Одним из первых клеточных автоматов, который будет изучен, и, вероятно, самым популярным за все
время является двумерный клеточный автомат «Игра “Жизнь”». Он был разработан Джоном Х. Кон-
веем (John H. Conway) и популяризирован в 1970 году в колонке Мартина Гарднера (Martin Gardner)
в журнале Scientific American. См. https://thinkcomplex.com/gol.
Клетки в игре «Жизнь» расположены в двумерной сетке, то есть массиве строк и столбцов.
Обычно сетка считается бесконечной, но  на  практике она часто «сворачивается»; то  есть правый
край соединен с левым, а верхний край с нижним.
Каждая клетка в сетке имеет два состояния – ​живое и мертвое – ​и 8 соседей – ​север, юг, восток, за-
пад и четыре диагонали. Этот набор соседей иногда называют «окрестностью Мура».
Как и одномерные клеточные автоматы, о которых шла речь в предыдущих главах, игра «Жизнь»
развивается со временем в соответствии с правилами, которые похожи на простые законы физики.
В игре «Жизнь» последующее состояние каждой клетки зависит от ее текущего состояния и количест­
ва живых соседей. Если клетка жива, она остается в живых, если у нее два или три соседа, и умирает
в противном случае.
Если клетка мертва, она остается мертвой, если у нее нет ровно трёх соседей.
Это поведение практически аналогично реальному росту клеток: изолированные или переполнен-
ные клетки погибают; при умеренной плотности они процветают.
Игра «Жизнь» популярна, потому что:
• существуют простые начальные условия, которые дают удивительно сложное поведение;
• есть много интересных стабильных конструкций: некоторые колеблются (с разными периодами),
а другие движутся как космические корабли в Правиле 110 Вольфрама;
• и, подобно Правилу 110, игра «Жизнь» является полной по Тьюрингу;
• другим фактором, вызвавшим интерес, была гипотеза Конвея о том, что не  существует началь-
ных условий, обеспечивающих неограниченный рост числа живых клеток, – ​и о вознаграждении
в размере 50 долларов, которое он предложил любому, кто сможет доказать или опровергнуть это;

Глава 6. Игра «Жизнь»   73


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

Конструкции игры «Жизнь»


Если вы запускаете игру «Жизнь» из случайного начального состояния, вероятно, появится ряд ста-
бильных конструкций. Со временем люди определили эти конструкции и дали им имена.
Например, на  рис.  6‑1 показана стабильная конструкция под названием «улей». Каждая клетка
в улье имеет двух или трёх соседей, поэтому все они выживают, и ни одна из мертвых клеток, приле-
гающих к улью, не имеет трёх соседей, поэтому новые клетки не рождаются.

Рисунок 6-1. Стабильная конструкция под


названием «улей»

Другие конструкции «колеблются»; то есть они изменяются со временем, но в конечном итоге воз-
вращаются к своей начальной конфигурации (при условии что они не сталкиваются с другой кон-
струкцией). Например, на  рис.  6‑2 показана конструкция под названием «жаба». Это осциллятор,
который чередуется между двумя состояниями. «Период» этого осциллятора равен 2.

Рисунок 6-2.  Осциллятор под названием «жаба»

74  Изучение сложных систем с помощью Python


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

Рисунок 6-3. Космический корабль под названием


«планер»

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


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

Гипотеза Конвея
Из большинства начальных условий игра «Жизнь» быстро достигает стабильного состояния, в кото-
ром количество живых клеток почти постоянно (возможно, с некоторыми колебаниями).
Но есть несколько простых начальных условий, которые дают удивительное количество живых кле-
ток и занимают много времени, чтобы успокоиться. Поскольку эти конструкции так долго живут, их
называют «Мафусаилами».
Один из самых простых Мафусаилов – ​это r-пентамино, в котором всего пять ячеек, примерно в фор-
ме буквы «r». На рис. 6‑4 показаны начальная конфигурация r-пентамино и окончательная конфигу-
рация после 1103 шагов.

Рисунок 6-4. Начальная и конечная конфигурации


r-пентамино

Глава 6. Игра «Жизнь»   75


Эта конфигурация является «окончательной» в том смысле, что все остальные конструкции являются
либо стабильными, либо осцилляторами, либо планерами, которые никогда не столкнутся с другой
конструкцией. В общей сложности r-пентамино дает 6 планеров, 8 блоков, 4 мигалки, 4 улья, 1 лодку,
1 корабль и 1 каравай.
Существование долгоживущих конструкций заставило Конвея задуматься, существуют ли начальные
конструкции, которые никогда не стабилизируются. Он предположил, что их не было, но он описал
два типа конструкций, которые могут доказать его неправоту: «ружье» и «паровоз». Ружье – ​это ста-
бильная конструкция, которая периодически производит космический корабль, – ​по мере того как
поток космических кораблей выходит из  источника, количество живых клеток растет бесконечно.
Паровоз – ​это транслирующая конструкция, которая оставляет живые клетки на своем пути.
Оказывается, что обе эти конструкции существуют. Команда, возглавляемая Биллом Госпером (Bill
Gosper), обнаружила первое планерное ружье, которое теперь называется ружьем Госпера. Оно по-
казано на рис. 6‑5. Госпер также обнаружил первый паровоз.

Рисунок 6-5. Планерная пушка Госпера, которая


производит поток планеров

Существует множество конструкций обоих типов, но их нелегко спроектировать или обнаружить. Это
не совпадение. Конвей выбрал правила игры «Жизнь», чтобы его предположение не было очевидно
верным или ложным. Из всех возможных правил для двумерного клеточного автомата большинство
приводит к простому поведению: большинство начальных условий быстро стабилизируется или не-
ограниченно растёт. Избегая неинтересных клеточных автоматов, Конвей также избегал поведения
классов 1 и 2 и, возможно, класса 3.
Если мы верим принципу вычислительной эквивалентности Вольфрама, то  ожидаем, что игра
«Жизнь» будет в классе 4, и это так. В 1982 году было доказано, что игра «Жизнь» полная по Тюрин-
гу (и это было сделано снова, уже независимо, в 1983 году). С тех пор несколько человек создало
конструкции игры «Жизнь», которые реализуют машину Тьюринга или другую машину, о  которой
известно, что она полная по Тюрингу.

Реализм
Стабильные конструкции в игре «Жизнь» трудно не заметить, особенно те, что движутся. Естественно
рассматривать их как постоянные объекты, но помните, что клеточный автомат состоит из ячеек. Нет
такой вещи, как жаба или каравай. Планеры и  другие космические корабли еще менее реальны,
потому что со временем они даже не состоят из одних и тех же клеток. Таким образом, эти конструк-
ции похожи на созвездия звезд. Мы воспринимаем их, потому что хорошо видим конструкции или
потому что у нас есть активное воображение, но они не реальны.

76  Изучение сложных систем с помощью Python


Правильно?
Ну, не так быстро. Многие сущности, которые мы считаем «реальными», также являются постоянными
моделями сущностей в меньшем масштабе. Ураганы – ​это просто модели воздушного потока, но мы
даем им личные имена. И люди, как и планеры, со временем перестают состоять из одних и тех же
клеток.
Это наблюдение не ново – ​около 2500 лет назад Гераклит указал, что вы не можете войти в одну
и ту же реку дважды, – ​но сущности, которые появляются в игре «Жизнь», являются полезным преце-
дентом для размышлений о научном реализме.
Научный реализм относится к научным теориям и сущностям, которые они постулируют. Теория по-
стулирует сущность, если она выражается в  терминах свойств и  поведения сущности. Например,
теории об электромагнетизме выражаются в терминах электромагнитных полей. Некоторые теории
об экономике выражаются в терминах спроса, предложения и рыночных сил. А теории о биологии
выражаются в терминах генов.
Но реальны ли эти сущности? То есть существуют ли они в мире, независимом от нас и наших теорий?
Опять же, я считаю полезным изложить философские позиции, используя ряд сильных сторон. Вот
четыре утверждения научного реализма по возрастающей.

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

НР2
По мере развития науки наши теории становятся более приближенными к реальности. Известно,
что, по крайней мере, некоторые постулируемые сущности являются реальными.

НР3
Некоторые теории точно верны; другие примерно верны. Сущности, постулируемые истинными
теориями, и некоторые сущности в приближенных теориях реальны.

НР4
Теория верна, если она правильно описывает реальность, и ложна в противном случае. Сущно-
сти, постулируемые истинными теориями, реальны; другие нет.
НР4 настолько силен, что, вероятно, является несостоятельным; по такому строгому критерию, как
известно, почти все современные теории являются ложными. Большинство реалистов приняло бы
что-то в диапазоне между НР1 и НР3.

Инструментализм
Но НР1 настолько слаб, что граничит с инструментализмом, который заключается в том, что теории –​
это инструменты, которые мы используем для наших целей: теория полезна или нет в той степени,
в которой она подходит для своих целей, но мы не можем сказать, правда это или нет.
Чтобы проверить, насколько вам удобен инструментализм, я составил следующий тест. Прочитайте
приведенные ниже утверждения и поставьте себе балл за каждое утверждение, с которым вы со-
гласны. Если вы набрали 4 или более баллов, возможно, вы инструменталист!

Глава 6. Игра «Жизнь»   77


«Сущности в игре “Жизнь” не реальны; это просто образцы клеток, которым люди дали милые
имена».
«Ураган – ​это просто модель воздушного потока, но это полезное описание, потому что оно по-
зволяет нам делать прогнозы и сообщать о погоде».
«Фрейдовские сущности, такие как Ид и  Супер-Эго, не  реальны, но  они являются полезными
инструментами для размышлений и общения о психологии (или, по крайней мере, некоторые
люди так думают)».
«Электромагнитные поля являются постулируемыми сущностями в нашей лучшей теории элект­
ромагнетизма, но они не реальны. Мы могли бы построить другие теории без постулирования
полей, которые были бы столь же полезными».
«Многие вещи в мире, которые мы идентифицируем как объекты, являются произвольными кол-
лекциями, такими как созвездия. Например, гриб – ​это просто плодовое тело, большая часть
которого растет под землей в  виде едва смежной сети клеток. Мы ориентируемся на  грибы
по практическим соображениям, таким как видимость и пригодность в пищу».
«Некоторые объекты имеют четкие границы, но  многие размыты. Например, какие молекулы
являются частью вашего тела: воздух в легких? Еда в желудке? Питательные вещества в вашей
крови? Питательные вещества в  клетке? Вода в  клетке? Структурные части клетки? Волосы?
Мертвая кожа? Грязь? Бактерии на вашей коже? Бактерии в вашем кишечнике? Митохондрии?
Сколько из этих молекул входит в ваш состав, когда вы взвешиваетесь? Представление о мире
в терминах отдельных объектов полезно, но сущности, которые мы идентифицируем, не явля-
ются реальными».

Если некоторые из данных утверждений вас устраивают больше, чем других, спросите себя, почему.
Какие различия в этих сценариях влияют на вашу реакцию? Можете ли вы сделать принципиальное
различие между ними?
Для получения дополнительной информации об инструментализме см. https://thinkcomplex.com/instr.

Реализация игры «Жизнь»


В ходе упражнений, приведенных в конце этой главы, вам будет предложено поэкспериментировать,
изменить игру «Жизнь» и реализовать другие двумерные клеточные автоматы. В данном разделе
объясняется моя реализация игры «Жизнь», которую вы можете использовать в качестве отправной
точки для своих экспериментов.
Чтобы представить состояние ячеек, я использую массив NumPy из 8-разрядных целых чисел без
знака. Например, следующая строка создает массив 10×10, инициализированный случайными зна-
чениями 0 и 1:
a = np.random.randint(2, size=(10, 10), dtype=np.uint8)
Существует несколько способов вычислить правила игры «Жизнь». Самое простое – ​использовать
циклы for, чтобы перебирать строки и столбцы массива:
b = np.zeros_like(a)
rows, cols = a.shape
for i in range(1, rows‑1):
for j in range(1, cols‑1):
state = a[i, j]
neighbors = a[i‑1: i+2, j‑1: j+2]

78  Изучение сложных систем с помощью Python


k = np.sum(neighbors) – ​state
if state:
if k==2 or k==3:
b[i, j] = 1
else:
if k == 3:
b[i, j] = 1
Первоначально b – ​это массив нулей того  же размера, что и  a. Каждый раз в  цикле state – ​это
состояние центральной ячейки, а neighbors – ​это окрестность 3×3. k – ​количество живых соседей
(не  включая центральную ячейку). Вложенные операторы if оценивают правила игры «Жизнь»
и включают ячейки в b соответственно.
Эта реализация является прямой трансляцией правил, но она многословная и медленная. Мы можем
добиться большего успеха, используя взаимную корреляцию, как в разделе «Взаимная корреляция»
на  стр. 69. Там мы использовали np.correlate для вычисления одномерной корреляции. Теперь,
чтобы выполнить двумерную корреляцию, мы будем использовать correlate2d из  scipy.signal,
модуля SciPy, который предоставляет функции, связанные с обработкой сигналов:
from scipy.signal import correlate2d
kernel = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]])
c = correlate2d (a, kernel, mode='same')
То, что мы называли «окном» в контексте одномерной корреляции, называется «ядром» в контексте
двумерной корреляции, но  идея та  же: correlate2d умножает ядро и  массив для выбора окрест-
ности, затем складывает результат. Это ядро выбирает 8 соседей, которые окружают центральную
ячейку.
correlate2d применяет ядро к  каждому месту в  массиве. При mode = 'same' размер результата
будет таким же, как у a.
Теперь мы можем использовать логические операторы для вычисления правил:
b = (c==3) | (c==2) & a
b = b.astype(np.uint8)
Первая строка вычисляет логический массив с True, где должна быть живая ячейка, и False в другом
месте. Затем astype преобразует логический массив в массив целых чисел.
Эта версия быстрее и, вероятно, достаточно хороша, но мы можем несколько упростить ее, изменив
ядро:
kernel = np.array([[1, 1, 1],
[1,10, 1],
[1, 1, 1]])
c = correlate2d (a, kernel, mode='same')
b = (c==3) | (c==12) | (c==13)
b = b.astype(np.uint8)
Эта версия ядра включает в себя центральную ячейку и дает ей вес 10. Если центральная ячейка
равна 0, результат находится между 0 и 8; если центральная ячейка равна 1, результат находится
между 10 и  18. Используя это ядро, мы можем упростить логические операции, выбирая только
ячейки со значениями 3, 12 и 13.

Глава 6. Игра «Жизнь»   79


Это может показаться не большим улучшением, но допускает еще одно упрощение: с этим ядром
мы можем использовать таблицу для поиска значений ячеек, как мы это делали в разделе «Таблицы
клеточных автоматов» на стр. 71.
table = np.zeros(20, dtype=np.uint8)
table[[3, 12, 13]] = 1
c = correlate2d (a, kernel, mode='same')
b = table[c]
table имеет нули везде, кроме местоположений 3, 12 и 13. Когда мы используем c в качестве индек-
са в table, NumPy выполняет поэлементный поиск; то есть он берет каждое значение из c, ищет его
в таблице и помещает результат в b.
Эта версия быстрее и более краткая, чем другие. Единственным недостатком является то, что она
требует больше объяснений.
Life.py, который включен в репозиторий для этой книги, предоставляет класс Life, инкапсулирую-
щий данную реализацию правил. При запуске Life.py вы должны увидеть анимацию «паровоза»,
который представляет собой космический корабль, оставляющий за собой осколки.

Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap06.ipynb в репозитории для этой
книги. Откройте этот блокнот, прочитайте код и запустите ячейки. Вы можете использовать блокнот
для работы над следующими упражнениями. Мои решения находятся в chap06soln.ipynb.
Более подробная информация о  работе с  кодом приведена в  разделе «Использование кода»
на стр. 10.

Упражнение 6‑1
Запустите игру в случайном состоянии и выполняйте ее, пока она не стабилизируется. Какие ста-
бильные конструкции вы можете определить?

Упражнение 6‑2
Многие именованные конструкции доступны в  переносимых форматах файлов. Измените Life.py,
чтобы проанализировать один из этих форматов и инициализировать сетку.

Упражнение 6‑3
Одна из самых долгоживущих маленьких конструкций – ​«кролики», которая начинается с 9 живых
клеток и  требует 17 313 шагов для стабилизации. Вы можете получить начальную конфигурацию
в  различных форматах на  странице https://thinkcomplex.com/rabbits. Загрузите эту конфигурацию
и запустите ее.

Упражнение 6‑4
В моей реализации класс Life основан на родительском классе Cell2D, а класс LifeViewer – ​на клас-
се Cell2DViewer. Вы можете использовать эти базовые классы для реализации других двумерных
клеточных автоматов.
Например, разновидность игры «Жизнь», называемая «Highlife», имеет те  же правила, что и  игра
«Жизнь», плюс одно дополнительное правило: оживает мертвая ячейка с 6 соседями.
Напишите класс с именем Highlife, который наследуется от Cell2D и реализует эту версию правил.
Также напишите класс с именем HighlifeViewer, который наследуется от Cell2DViewer, и попробуй-

80  Изучение сложных систем с помощью Python


те различные способы визуализации результатов. В качестве простого примера используйте другую
карту цветов.
Одним из наиболее интересных шаблонов в Highlife является репликатор (см. https://thinkcomplex.
com/repl). Используйте add_cells, чтобы инициализировать Highlife с репликатором и посмотреть,
что он делает.

Упражнение 6‑5
Если вы обобщаете машину Тьюринга на  два измерения или добавляете головку чтения-записи
к  двумерному клеточному автомату, в  результате получается клеточный автомат под названием
Turmite (тьюрмит). Он назван в честь термитов из-за того, как движется головка чтения-записи, одна-
ко название написано неправильно как дань уважения Алану Тьюрингу.
Самым известным тьюрмитом является муравей Лэнгтона, обнаруженный Крисом Лэнгтоном (Chris
Langton) в 1986 году. См. https://thinkcomplex.com/langton.
Муравей – ​это головка для чтения и записи с четырьмя состояниями, которые можно рассматривать
как обращенные на север, юг, восток или запад. Клетки имеют два состояния, черное и белое.
Правила просты. В течение каждого временного шага муравей проверяет цвет ячейки, в которой он
находится. Если она черная, муравей поворачивается вправо, меняет цвет ячейки на белый и пере-
мещается на одну позицию вперед. Если ячейка белая, муравей поворачивает налево, меняет цвет
ячейки на черный и движется вперед.
Учитывая простой мир, простой набор правил и только одну движущуюся часть, вы, возможно, ожи-
даете увидеть простое поведение – ​но  вы должны знать лучше к  настоящему времени. Начиная
со  всех белых клеток, муравей Лэнгтона движется, по-видимому, случайным образом более чем
на 10 000 шагов, прежде чем входит в цикл с периодом в 104 шага. После каждого цикла муравей
переводится по диагонали, поэтому он оставляет след, называемый «шоссе».
Напишите реализацию муравья Лэнгтона.

Глава 6. Игра «Жизнь»   81


Глава 7.
Физическое моделирование
Клеточные автоматы, которые мы видели до сих пор, не являются физическими моделями; то есть
они не предназначены для описания систем в реальном мире. Но некоторые автоматы задуманы
как физические модели.
В этой главе мы рассмотрим клеточный автомат, который моделирует химические вещества, диф-
фундирующие (распространяющиеся) и реагирующие друг с другом. Это процесс, который Алан Тью-
ринг предложил для объяснения того, как развиваются некоторые конструкции животных.
И мы будем экспериментировать с клеточным автоматом, который моделирует перколяцию жидко-
сти через пористый материал, например воды через кофейную гущу. Эта модель является первой
из нескольких моделей, которые демонстрируют поведение фазового перехода и геометрию фрак-
талов, и я объясню, в чем их значение.

Диффузия
В 1952 году Алан Тьюринг опубликовал статью под названием «Химическая основа морфогенеза»,
в которой описывается поведение систем с участием двух химических веществ, которые диффунди-
руют в пространстве и реагируют друг с другом. Он показал, что эти системы производят широкий
спектр конструкций в зависимости от скорости диффузии и реакции, и предположил, что подобные
системы могут быть важным механизмом в процессах биологического роста, особенно в развитии
паттернов окраски животных.
Модель Тьюринга основана на  дифференциальных уравнениях, но  может быть реализована с  ис-
пользованием клеточного автомата.
Прежде чем мы перейдем к модели Тьюринга, мы начнем с чего-то более простого: диффузионной
системы с одним лишь химическим веществом. Мы будем использовать двумерный клеточный авто-
мат, где состояние каждой ячейки – ​непрерывная величина (обычно от 0 до 1), которая представляет
концентрацию химического вещества.
Мы смоделируем процесс диффузии, сравнивая каждую ячейку со средним числом ее соседей. Если
концентрация центральной ячейки превышает среднюю величину по соседству, химическое веще-
ство течет от центра к соседям. Если концентрация центральной ячейки ниже, химическое вещество
течет в другую сторону.
Приведенное ниже ядро вычисляет разницу между каждой ячейкой и средним значением ее сосе-
дей:
kernel = np.array([[0, 1, 0],
[1, –4, 1],
[0, 1, 0]])
Используя np.correlate2d, мы можем применить это ядро к каждой ячейке в массиве:
c = correlate2d (array, kernel, mode='same')

82  Изучение сложных систем с помощью Python


Мы будем использовать диффузионную константу r, которая связывает разницу в  концентрации
со скоростью потока:
array += r * c
На рис. 7‑1 показаны результаты для клеточного автомата с размером n = 9, константой диффузии
r = 0,1 и начальной концентрацией 0 везде, кроме «островка» посередине. На рисунке показана на-
чальная конфигурация и состояние клеточного автомата после 5 и 10 шагов. Химическое вещество
распространяется из центра наружу до тех пор, пока концентрация не станет одинаковой везде.

Рисунок 7-1. Простая диффузная модель после 0, 5


и 10 шагов

Реакция диффузии
Теперь давайте добавим второй реактив. Я  определю новый объект, ReactionDiffusion, который
содержит два массива, по одному для каждого химического вещества:
class ReactionDiffusion(Cell2D):
def __init__(self, n, m, params, noise=0.1):
self.params = params
self.array = np.ones((n, m), dtype=float)
self.array2 = noise * np.random.random((n, m))
add_island(self.array2)
n и m – ​количество строк и столбцов в массиве. params – ​это набор параметров, которые я объясню
далее.
array обозначает концентрацию первого химического вещества, А; функция NumPy ones инициа­
лизирует его везде в  1. Тип данных float указывает на  то, что элементы A являются значениями
с плавающей точкой.
array2 обозначает концентрацию B, которая инициализируется случайными значениями между 0
и  noise, который по  умолчанию равен 0,1. Затем add_island добавляет остров с  более высокой
концентрацией посередине:
def add_island(a, height=0.1):
n, m = a.shape
radius = min(n, m) // 20
i = n//2

Глава 7. Физическое моделирование   83


j = m//2
a[i-radius: i+radius, j-radius: j+radius] += height
Радиус острова составляет одну двадцатую от n или m, в зависимости от того, что меньше. Высота
острова – э​ то height со значением по умолчанию 0,1.
Вот функция step, которая обновляет массивы:
def step(self):
A = self.array
B = self.array2
ra, rb, f, k = self.params
cA = correlate2d (A, self.kernel, **self.options)
cB = correlate2d (B, self.kernel, **self.options)
reaction = A * B**2
self.array += ra * cA – ​reaction + f * (1-A)
self.array2 += rb * cB + reaction – ​(f+k) * B
Параметры:
ra
Скорость диффузии A (аналогично r в предыдущем разделе).
rb
Скорость диффузии B. В большинстве версий этой модели rb составляет около половины от ra.
f
Скорость «подачи», которая контролирует, как быстро A добавляется в систему.
k
Частота «убийств», которая контролирует, как быстро B удаляется из системы.
Теперь давайте более подробно рассмотрим операторы обновления:
reaction = A * B**2
self.array += ra * cA – ​reaction + f * (1-A)
self.array2 += rb * cB + reaction – ​(f+k) * B
Массивы cA и cB являются результатом применения диффузионного ядра к A и B. Умножение на ra
и rb дает скорость диффузии в или из каждой ячейки.
Термин A*B**2 обозначает скорость, с которой A и B реагируют друг с другом. Предполагая, что ре-
акция потребляет A и производит B, мы вычитаем этот член в первом уравнении и добавляем его
во втором.
Термин f*(1–A) определяет скорость, с которой A добавляется в систему. Когда A около 0, макси-
мальная скорость подачи – ​f. Когда A приближается к 1, скорость подачи падает до нуля.
Наконец, член (f+k)*B определяет скорость удаления B из системы.
Когда B приближается к 0, эта скорость стремится к нулю.
Пока параметры скорости не слишком высоки, значения A и B обычно остаются между 0 и 1.
С различными параметрами эта модель может производить конструкции, похожие на полосы и пят-
на у различных животных. В некоторых случаях сходство поразительно, особенно когда параметры
подачи и уничтожения изменяются в пространстве.
Для всех симуляций, приведенных в этом разделе, ra = 0,5 и rb = 0,25.

84  Изучение сложных систем с помощью Python


На рис. 7‑2 показаны результаты, когда f = 0,035 и k = 0,057, причем концентрация B показана в бо-
лее темных цветах. С этими параметрами система развивается в направлении стабильной конфигу-
рации со светлыми пятнами A на темном фоне B.

Рисунок 7-2. Реакционно-диффузная модель


с параметрами f = 0,035 и k = 0,057
после 1000, 2000 и 4000 шагов

На рис. 7‑3 показаны результаты, когда f = 0,055 и k = 0,062, что дает коралловидную конструкцию
B на фоне A.

Рисунок 7-3. Реакционно-диффузная модель


с параметрами f = 0,055 и k = 0,062
после 1000, 2000 и 4000 шагов

На рис. 7‑4 показаны результаты, когда f = 0,039 и k = 0,065. Эти параметры производят пятна B,
которые растут и делятся в процессе, напоминающем митоз, завершаясь стабильной конструкцией
из равномерно расположенных пятен.
С 1952  года наблюдения и  эксперименты оказали определенную поддержку гипотезе Тьюринга.
На  данный момент кажется вероятным, но  еще не  доказанным, что многие паттерны животных
на самом деле формируются в результате реакций диффузии какого-либо рода.

Глава 7. Физическое моделирование   85


Рисунок 7-4. Реакционно-диффузная модель
с параметрами f = 0,0039 и k = 0,065
после 1000, 2000 и 4000 шагов

Перколяция
Перколяция – ​это процесс, при котором жидкость просачивается через полупористый материал.
Примеры включают в себя нефть в горных породах, воду в бумаге и водород в микропорах.
Перколяционные модели также используются для изучения систем, которые не являются букваль-
но перколяционными, включая эпидемии и сети электрических резисторов. См. https://thinkcomplex.
com/perc.
Перколяционные модели часто представлены с использованием случайных графов, подобных тем,
которые мы видели в главе 2, однако они также могут быть представлены с использованием клеточ-
ных автоматов. В следующих нескольких разделах мы рассмотрим двумерный клеточный автомат,
имитирующий перколяцию.
В этой модели:
• первоначально каждая ячейка является «пористой» с вероятностью q или «непористой» с ве-
роятностью 1 – ​q;
• когда начинается симуляция, все ячейки считаются «сухими», кроме верхнего ряда, который
является «мокрым»;
• во время каждого временного шага, если у пористой ячейки есть хотя бы один мокрый сосед,
она становится мокрой. Непористые ячейки остаются сухими;
• симуляция продолжается до тех пор, пока не достигнет «фиксированной точки», когда ячейки
больше не меняют состояние.
Если есть путь мокрых ячеек от верхнего до нижнего ряда, мы говорим, что у клеточного автомата
есть «перколяционный кластер».
Два вопроса, представляющих интерес относительно перколяции: (1) вероятность того, что случай-
ный массив содержит перколяционный кластер, и (2) как эта вероятность зависит от q. Эти вопросы
могут напомнить вам раздел «Случайные графы» на стр. 24, где мы рассматривали вероятность связ-
ности случайного графа Эрдёша–Реньи. Мы увидим ряд связей между той моделью и этой.
Я определяю новый класс для представления модели перколяции:
class Percolation(Cell2D):
def __init__(self, n, q):
self.q = q

86  Изучение сложных систем с помощью Python


self.array = np.random.choice([1, 0], (n, n), p=[q, 1-q])
self.array[0] = 5
n и m – ​количество строк и столбцов в клеточном автомате.
Состояние клеточного автомата хранится в  массиве, который инициализируется с  помощью
np.random.choice, чтобы выбрать 1 (пористый) с вероятностью q и 0 (непористый) с вероятностью
1 – ​q.
Состояние верхнего ряда установлено на 5, который представляет влажную ячейку. Применение 5,
а не более очевидного 2, позволяет использовать correlate2d, чтобы проверить, есть ли у любой
пористой ячейки мокрый сосед. Вот ядро:
kernel = np.array([[0, 1, 0],
[1, 0, 1],
[0, 1, 0]])
Это ядро ​​определяет окрестность «фон Неймана» из 4 ячеек; в отличие от окрестности Мура, кото-
рую мы видели в разделе «Игра “Жизнь” Конвея» на стр. 73, она не включает диагонали.
Это ядро ​​складывает состояния соседей. Если какой-либо из них будет мокрым, результат превы-
сит 5. В противном случае максимальный результат равен 4 (если все соседи оказались пористыми).
Мы можем использовать эту логику, чтобы написать простую, быструю функцию step:
def step(self):
a = self.array
c = correlate2d (a, self.kernel, mode='same')
self.array[(a==1) & (c>=5)] = 5
Эта функция идентифицирует пористые ячейки, где a == 1, которые имеют, по крайней мере, одного
мокрого соседа, где c ≥ 5, и устанавливает их состояние равным 5, что указывает на то, что они мок­
рые.
На рис. 7‑5 показаны первые несколько шагов перколяционной модели, где n = 10, а p = 0,7.

Рисунок 7-5. Первые три шага перколяционной


модели, где n = 10 и p = 0,7

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

Глава 7. Физическое моделирование   87


Фазовый переход
Теперь давайте проверим, содержит ли случайный массив перколяционный кластер:
def test_perc(perc):
num_wet = perc.num_wet()
while True:
perc.step()
if perc.bottom_row_wet():
return True
new_num_wet = perc.num_wet()
if new_num_wet == num_wet:
return False
num_wet = new_num_wet
test_perc принимает объект Percolation в качестве параметра. Каждый раз в цикле он продвигает
клеточный автомат на один временной шаг. Он проверяет нижний ряд на наличие мокрых ячеек;
если это так, он возвращает значение True, чтобы указать, что существует перколяционный кластер.
В течение каждого временного шага он также вычисляет количество мокрых ячеек и проверяет, уве-
личилось ли число с момента последнего шага. Если нет, мы достигли фиксированной точки, не най-
дя перколяционного кластера, поэтому test_perc возвращает значение False.
Чтобы оценить вероятность перколяционного кластера, мы генерируем много случайных массивов
и тестируем их:
def estimate_prob_percolating(n=100, q=0.5, iters=100):
t = [test_perc(Percolation(n, q)) for i in range(iters)]
return np.mean(t)
measure_prob_percolating создает 100 объектов Percolation с заданными значениями n и q и вы-
зывает test_perc, чтобы увидеть, сколько из них имеет перколяционный кластер.
Возвращаемое значение – ​это доля, у которой он есть.
Когда р = 0,55, вероятность перколяционного кластера близка к 0. При р = 0,60 она составляет около
70 %, а при р = 0,65 она близка к 1. Этот быстрый переход предполагает, что существует критическое
значение р около 0,6.
Мы можем оценить критическое значение более точно, используя случайное блуждание. Начиная
с начального значения q, мы создаем объект Percolation и проверяем, есть ли у него перколяцион-
ный кластер. Если он есть, то q, вероятно, слишком велико, поэтому мы уменьшаем его. Если нет, то,
вероятно, q слишком мало, поэтому мы увеличиваем его.
Вот код:
def find_critical(n=100, q=0.6, iters=100):
qs = [q]
for i in range(iters):
perc = Percolation(n, q)
if test_perc(perc):
q -= 0.005
else:
q += 0.005

88  Изучение сложных систем с помощью Python


qs.append(q)
return qs
Результатом является список значений для q. Мы можем оценить критическое значение, q_crit, вы-
числяя среднее значение этого списка. При n = 100 среднее значение qs составляет около 0,59; это
значение, похоже, не зависит от n.
Быстрое изменение поведения вблизи критического значения называется фазовым переходом
по  аналогии с  фазовыми переходами в  физических системах, подобно тому, как вода переходит
из жидкого состояния в твердое в момент замерзания.
Большое разнообразие систем отображает общий набор поведений и характеристик, когда они на-
ходятся в критической точке или около нее. Такое поведение известно под общим названием крити-
ческие явления. В следующем разделе мы рассмотрим одно из них: геометрию фракталов.

Фракталы
Чтобы понять, что такое фракталы, мы должны начать с измерений.
Для простых геометрических объектов размерность определяется с точки зрения поведения мас-
штабирования. Например, если сторона квадрата имеет длину l, его площадь равна l2. Показатель
степени 2 указывает на то, что квадрат является двумерным. Аналогично, если сторона куба имеет
длину l, его объем равен l3, что указывает на то, что куб является трехмерным.
В целом мы можем оценить размер объекта, измерив некоторый размер (например, площадь или
объем) как функцию от некоторой линейной меры (например, длину стороны).
В качестве примера я буду оценивать размерность одномерного клеточного автомата, измеряя его
площадь (общее количество «включенных» ячеек) как функцию количества строк.
На рис. 7‑6 показаны три одномерных клеточных автомата, подобных тем, которые мы видели в раз-
деле «Эксперимент Вольфрама» на  стр. 59. Правило 20 (слева) генерирует набор ячеек, который
выглядит как линия, поэтому мы ожидаем, что он будет одномерным. Правило 50 (в центре) создает
нечто вроде треугольника, поэтому мы ожидаем, что оно будет двумерным. Правило 18 (справа)
также производит что-то вроде треугольника, но  плотность не  является равномерной, поэтому ее
поведение масштабирования не очевидно.

Рисунок 7-6. Одномерные клеточные автоматы


с правилами 20, 50 и 18 после 32
временных шагов

Я буду оценивать размерность этих клеточных автоматов с помощью приведенной ниже функции,
которая подсчитывает количество включенных ячеек после каждого временного шага. Она возвра-
щает список кортежей, где каждый кортеж содержит i, i2 и общее количество ячеек.
def count_cells(rule, n=500):
ca = Cell1D (rule, n)
ca.start_single()

Глава 7. Физическое моделирование   89


res = []
for i in range(1, n):
cells = np.sum(ca.array)
res.append((i, i**2, cells))
ca.step()
return res
На рис. 7‑7 показан график результатов на двойной логарифмической шкале.

Рисунок 7-7. Количество «включенных» ячеек


в сравнении с количеством временных
шагов для правил 20, 50 и 18

На каждом рисунке верхняя пунктирная линия показывает y = i2. Взяв логарифм обеих сторон, мы
получаем log y = 2 log i. Поскольку этот рисунок на двойной логарифмической шкале, наклон этой
линии равен 2.
Точно так же пунктирная линия внизу показывает y = i. На двойной логарифмической шкале наклон
этой линии равен 1.
Правило 20 (слева) производит три ячейки каждые два временных шага, поэтому общее количество
ячеек после i шагов равно y = 1.5i. Взяв логарифм обеих сторон, получаем log y = log 1,5 + log i,
поэтому на двойной логарифмической шкале мы ожидаем линию с наклоном 1. Фактически пред-
полагаемый наклон линии составляет 1,01.
Правило 50 (в центре) создает i + 1 новых ячеек в течение i-го временного шага, поэтому общее
число ячеек после i шагов равно y = i2 + i. Если мы проигнорируем второе слагаемое и возьмем лога-
рифм с обеих сторон, то получим log y ∼ log 2 log i, поэтому когда i становится большим, мы ожидаем
увидеть линию с наклоном 2. Фактически предполагаемый наклон составляет 1,97.
Наконец, для Правила 18 (справа) предполагаемый наклон составляет около 1,57, что явно не равно
1, 2 или любому другому целому числу. Это говорит о том, что паттерн, сгенерированный Прави-
лом 18, имеет «дробное измерение», то есть является фракталом.
Этот способ оценки фрактальной размерности называется box-counting. Для получения дополни-
тельной информации см. https://thinkcomplex.com/box.

90  Изучение сложных систем с помощью Python


Фракталы и перколяционные модели
Теперь вернемся к перколяционным моделям. На рис. 7‑8 показаны кластеры влажных клеток при
моделировании протекания с p = 0,6 и n = 100, 200 и 300. Неформально они напоминают фракталь-
ные конструкции, наблюдаемые в природе и в математических моделях.

Рисунок 7-8. Перколяционные модели, где q = 0,6


и n = 100, 200 и 300

Чтобы оценить их фрактальную размерность, мы можем запустить клеточный автомат с  диапазо-


ном размеров, подсчитать количество мокрых ячеек в каждом перколяционном кластере, а затем
посмот­реть, как вычисляется число ячеек по мере увеличения размера массива.
Следующий цикл запускает симуляции:
res = []
for size in sizes:
perc = Percolation(size, q)
if test_perc(perc):
num_filled = perc.num_wet() – ​size
res.append((size, size**2, num_filled))
Результатом является список кортежей, где каждый кортеж содержит size, size**2 и  количество
ячеек в перколяционном кластере (не включая начальные мокрые ячейки в верхнем ряду).
На рис.  7‑9 показаны результаты для диапазона размеров от  10 до  100. Точками показано коли-
чество клеток в  каждом перколяционном кластере. Наклон линии, соответствующей этим точкам,
часто составляет около 1,85, что говорит о том, что перколяционный кластер фактически является
фрактальным, когда q приближается к критическому значению.

Глава 7. Физическое моделирование   91


Рисунок 7-9. Количество ячеек в перколяционном
кластере в сравнении с размером
клеточного автомата

Когда q больше критического значения, почти каждая пористая ячейка заполняется, поэтому число
мокрых ячеек близко к q*size^2, которое имеет размерность 2.
Когда q существенно меньше критического значения, количество мокрых ячеек пропорционально
линейному размеру массива, поэтому оно имеет размерность 1.

Упражнения
Код, приведенный в этой главе, находится в chap07.ipynb в репозитории для данной книги. Более
подробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 7‑1
В разделе «Фракталы и перколяционные модели» на стр. 91 мы продемонстрировали, что Прави-
ло 18 производит фрактал. Можете ли вы найти другие одномерные клеточные автоматы, которые
производят фракталы?
Примечание: объект Cell1D не  сворачивается от  левого края к  правому, что создает артефакты
на границах для некоторых правил. Возможно, вы захотите использовать Wrap1D. Это дочерний класс
Cell1D, который оборачивается вокруг. Он определен в Cell1D.py в репозитории для этой книги.

Упражнение 7‑2
В 1990  году Бак, Чень и Танг предложили клеточный автомат, который является абстрактной мо­
делью лесного пожара. Каждая ячейка находится в одном из трех состояний: пустая, занята деревом
или в огне.
Правила этого клеточного автомата следующие.
1. Пустая ячейка становится занятой с вероятностью p.
2. Ячейка с деревом горит, если горит кто-то из ее соседей.

92  Изучение сложных систем с помощью Python


3. Ячейка с деревом самопроизвольно горит с вероятностью f, даже если ни один из ее соседей
не горит.
4. В последующем временном шаге ячейка с горящим деревом становится пустой.
Напишите программу, которая реализует данную модель. Возможно, вы захотите наследовать
от класса Cell2D.
Типичными значениями параметров являются p = 0,01 и f = 0,001, но вы, возможно, захотите поэкс-
периментировать с другими значениями.
Начиная со случайного начального условия, запустите модель, пока она не достигнет устойчивого
состояния, при котором количество деревьев больше не увеличивается или не уменьшается после-
довательно.
В устойчивом состоянии является ли геометрия леса фрактальной? Каково ее фрактальное измере-
ние?

Глава 7. Физическое моделирование   93


Глава 8.
Самоорганизованная критичность
В предыдущей главе мы рассмотрели пример системы с критической точкой и изучили одно из об-
щих свойств критических систем – ​фрактальную геометрию.
В этой главе мы рассмотрим два других свойства критических систем: распределения с тяжелыми
хвостами, с которыми мы встречались в разделе «Распределения с тяжелыми хвостами» на стр. 50,
и розовый шум, о котором я расскажу в этой главе.
Эти свойства интересны отчасти потому, что они нередко появляются в природе; то есть многие при-
родные системы создают фрактальную геометрию, распределения с тяжелыми хвостами и розовый
шум.
Это наблюдение поднимает естественный вопрос: почему так много природных систем обладают
свойствами критических систем? Возможный ответ  – ​самоорганизованная критичность, которая
является тенденцией некоторых систем эволюционировать в  критическое состояние и  оставаться
в нем.
В этой главе я познакомлю вас с моделью песчаной кучи, которая была первой системой, продемон-
стрировавшей самоорганизованную критичность.

Критические системы
Многие критические системы демонстрируют общее поведение:
• фрактальная геометрия: например, замерзающая вода имеет тенденцию формировать фрак-
тальные конструкции, включая снежинки и  другие кристаллические структуры. Фракталы ха-
рактеризуются самоподобием; то есть части конструкции похожи на масштабированные копии
целого;
• распределение с тяжелыми хвостами ряда физических величин: например, в замерзающей воде
распределение размеров кристаллов характеризуется степенным законом;
• изменения во  времени, которые демонстрируют розовый шум: сложные сигналы могут быть
разложены на их частотные компоненты. В розовом шуме низкочастотные компоненты имеют
большую мощность, по сравнению с высокочастотными. В частности, мощность на частоте f про-
порциональна 1/f.
Критические системы обычно нестабильны. Например, для поддержания воды в частично заморо-
женном состоянии необходим активный контроль температуры. Если система близка к критической
температуре, небольшое отклонение приводит к смещению системы в ту или другую фазу.
Многие природные системы демонстрируют характерное поведение критичности, но  если крити-
ческие точки нестабильны, они не должны быть общими по своей природе. Это загадка Бака, Танга
и  Визенфельда. Их решение называется самоорганизованной критичностью, где слово «самоор-
ганизованный» означает, что из  любого начального состояния система движется к  критическому
состоя­нию и остается там без внешнего контроля.

94  Изучение сложных систем с помощью Python


Песчаные кучи
Модель песчаной кучи была предложена Баком, Тангом и  Визенфельдом в  1987  году. Она пред-
назначена не для того, чтобы быть реалистичной моделью песчаной кучи. Скорее, это абстракция,
моделирующая физические системы с большим количеством элементов, которые взаимодействуют
со своими соседями.
Модель песчаной кучи – ​это двумерный клеточный автомат, в  котором состояние каждой ячейки
представляет склон части кучи песка. В течение каждого временного шага проверяется, не превы-
шает ли каждая ячейка критическое значение K, которое обычно равно 3. Если это так, она «опро-
кидывается» и переносит песок в четыре соседние ячейки; то есть наклон ячейки уменьшается на 4,
а  каждый из  соседей увеличивается на  1. По  периметру сетки все ячейки находятся на  склоне 0,
поэтому избыток перетекает через край.
Бак, Танг и Визенфельд инициализируют все ячейки на уровне выше K и запускают модель до тех
пор, пока она не стабилизируется. Затем они наблюдают эффект малых возмущений: они выбира-
ют ячейку случайным образом, увеличивают ее значение на 1 и снова запускают модель, пока она
не стабилизируется.
Для каждого возмущения они измеряют T, количество временных шагов, которые делает куча для
стабилизации, и S, общее количество ячеек, которые опрокидываются1.
В большинстве случаев падение одной песчинки не приводит к опрокидыванию ячеек, поэтому T = 1
и S = ​​0.
Но иногда одна песчинка может вызвать лавину, которая затрагивает значительную долю сетки. Рас-
пределения T и S оказываются с тяжелыми хвостами, а это подтверждает утверждение, что система
находится в критическом состоянии.
Они приходят к  выводу, что модель песчаной кучи демонстрирует «самоорганизованную критич-
ность». Это означает, что она развивается до критического состояния без необходимости контроли-
роваться извне или того, что они называют «точной настройкой» каких-либо параметров. И модель
остается в критическом состоянии по мере добавления песчинок.
В следующих разделах я копирую их эксперименты и интерпретирую результаты.

Реализация песчаной кучи


Чтобы реализовать модель песчаной кучи, я  определяю класс SandPile, который наследуется
от Cell2D, определенного в Cell2D.py в репозитории для этой книги.
class SandPile(Cell2D):
def __init__(self, n, m, level=9):
self.array = np.ones((n, m)) * level
Все значения в массиве инициализируются до level, который, как правило, превышает порог опро-
кидывания, K.
Вот метод step, который находит все ячейки выше K и опрокидывает их:
kernel = np.array([[0, 1, 0],
[1, –4, 1],

1
В оригинальной статье используется другое определение S, но в более поздней работе используется это
определение.

Глава 8. Самоорганизованная критичность   95


[0, 1, 0]])
def step(self, K=3):
toppling = self.array > K
num_toppled = np.sum(toppling)
c = correlate2d (toppling, self.kernel, mode='same')
self.array += c
return num_toppled
Чтобы показать, как работает step, я начну с небольшой кучи, в которой есть две ячейки, готовые
опрокинуться:
pile = SandPile(n=3, m=5, level=0)
pile.array[1, 1] = 4
pile.array[1, 3] = 4
Изначально pile.array выглядит следующим образом:
[[0 0 0 0 0]
[0 4 0 4 0]
[0 0 0 0 0]]
Теперь мы можем выбрать ячейки, которые находятся выше порога опрокидывания:
toppling = pile.array > K
В результате получается логический массив, но мы можем использовать его как массив целых чисел,
например так:
[[0 0 0 0 0]
[0 1 0 1 0]
[0 0 0 0 0]]
Если мы сопоставим этот массив с ядром, он создаст копии ядра в каждом месте, где toppling равно 1.
c = correlate2d (toppling, kernel, mode='same')
А вот результат:
[[0 1 0 1 0]
[1–4 2–4 1]
[0 1 0 1 0]]
Обратите внимание, что там, где копии ядра перекрываются, они складываются.
Этот массив содержит изменения для каждой ячейки, которые мы используем для обновления ис-
ходного массива:
pile.array += c
И вот результат:
[[0 1 0 1 0]
[1 0 2 0 1]
[0 1 0 1 0]]
Вот как работает метод step.
При mode = 'same' correlate2d считает, что граница массива зафиксирована на нуле, поэтому лю-
бые песчинки, которые проходят через край, исчезают.
SandPile также предоставляет метод run, который вызывает step до тех пор, пока не будет опроки-
нуто больше ячеек:

96  Изучение сложных систем с помощью Python


def run(self):
total = 0
for i in itertools.count(1):
num_toppled = self.step()
total += num_toppled
if num_toppled == 0:
return i, total
Возвращаемое значение представляет собой кортеж, который содержит количество временных ша-
гов и общее количество ячеек, которые опрокинулись.
Если вы незнакомы с itertools.count, это бесконечный генератор, который отсчитывает от задан-
ного начального значения, поэтому цикл for работает до тех пор, пока step не вернет 0. Вы можете
прочитать о модуле itertools на странице https://thinkcomplex.com/iter.
Наконец, метод drop выбирает случайную ячейку и добавляет песчинку:
def drop(self):
a = self.array
n, m = a.shape
index = np.random.randint(n), np.random.randint(m)
a[index] += 1
Давайте рассмотрим более крупный пример, где n = 20:
pile = SandPile(n=20, level=10)
pile.run()
При начальном уровне 10 эта куча песка делает 332 временных шага, чтобы достичь равновесия.
Количество опрокидываний в общей сложности равно 53,336. На рис. 8‑1 (слева) показана конфигу-
рация после этого начального запуска. Обратите внимание, что она имеет повторяющиеся элементы,
характерные для фракталов. Мы скоро вернемся к ней.
На рис. 8‑1 (в центре) показана конфигурация кучи песка после падения 200 песчинок на случайные
ячейки, каждый раз, пока куча не достигнет равновесия. Симметрия начальной конфигурации была
нарушена; конфигурация выглядит случайной.

Рисунок 8-1. Начальное состояние модели


песчаной кучи (слева), после 200 шагов
(в центре) и 400 шагов (справа)

Глава 8. Самоорганизованная критичность   97


Наконец, на  этом  же рисунке (справа) показана конфигурация после падения 400 песчинок. Она
напоминает предыдущую конфигурацию, когда упали 200 песчинок. Фактически куча сейчас на-
ходится в устойчивом состоянии, при котором ее статистические свойства не меняются с течением
времени. Я объясню некоторые из этих свойств в следующем разделе.

Распределения с тяжелыми хвостами


Если модель песчаной кучи находится в критическом состоянии, мы ожидаем найти распределения
с тяжелыми хвостами для таких величин, как продолжительность и размер лавин. Итак, давайте по-
смотрим.
Я сделаю кучу песка побольше, где n = 50 и начальный уровень 30, и буду запускать ее до достиже-
ния равновесия:
pile2 = SandPile(n=50, level=30)
pile2.run()
Далее я выполню 100 000 случайных падений:
iters = 100000
res = [pile2.drop_and_run() for _ in range(iters)]
Как следует из  названия, drop_and_run вызывает drop и  run и  возвращает длительность лавины
и общее количество опрокинутых ячеек.
Таким образом, res – ​это список (T, S) кортежей, где T – ​это длительность во временных шагах, а S –​
опрокинутые ячейки.
Мы можем использовать np.transpose для распаковки res в два массива NumPy:
T, S = np.transpose(res)
Подавляющее большинство падений обладает длительностью 1 и не имеет опрокинутых ячеек; если
мы отфильтруем их перед построением графика, то получим более четкое представление об осталь-
ной части распределения.
T = T[T>1]
S = S[S>0]
Распределения T и S имеют много небольших значений и несколько очень больших. Я буду исполь-
зовать класс Pmf из thinkstats2, чтобы составить функцию вероятности из значений; то есть отобра-
жение из каждого значения в его вероятность возникновения (см. «Степень» на стр. 48).
pmfT = Pmf(T)
pmfS = Pmf(S)
На рис. 8‑2 показаны результаты для значений менее 50.

98  Изучение сложных систем с помощью Python


Рисунок 8-2. Распределение длительности лавины
(слева) и размера (справа), линейная
шкала

Как мы видели в разделе «Распределения с тяжелыми хвостами» на стр. 50, можно получить более
четкое представление об этих распределениях, построив их график на двойной логарифмической
шкале, как показано на рис. 8‑3.

Рисунок 8-3. Распределение длительности


лавины (слева) и размера (справа),
логарифмическая шкала

Для значений от 1 до 100 распределения являются почти прямыми на шкале, что характерно для
тяжелого хвоста. Серые линии на рисунке имеют наклоны около –1, что говорит о том, что эти рас-
пределения следуют степенному закону с параметрами около α = 1.

Глава 8. Самоорганизованная критичность   99


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

Фракталы
Еще одним свойством критических систем является фрактальная геометрия. Начальная конфигура-
ция на рис. 8‑1 (слева) напоминает фрактал, но не всегда можно определить это по внешнему виду.
Более надежный способ определить фрактал – ​это оценить его фрактальную размерность, как мы
видели в разделах «Фракталы» на стр. 89 и «Фракталы и перколяционные модели» на стр. 91.
Я начну с того, что сделаю кучу песка побольше, где n = 131 и начальный уровень 22.
pile3 = SandPile(n=131, level=22)
pile3.run()
Требуется 28 379 шагов, для того чтобы эта куча достигла равновесия, с более чем 200 млн опроки-
нутых ячеек.
Чтобы более четко увидеть полученную конструкцию, я  выбираю ячейки с  уровнями 0, 1, 2 и  3
и строю их отдельно:
def draw_four(viewer, levels=range(4)):
thinkplot.preplot(rows=2, cols=2)
a = viewer.viewee.array
for i, level in enumerate(levels):
thinkplot.subplot(i+1)
viewer.draw_array(a==level, vmax=1)
draw_four принимает объект SandPileViewer, который определен в Sand.py в репозитории для этой
книги. Параметр level – ​это список уровней, которые мы хотим построить; по умолчанию это диа-
пазон от 0 до 3. Если куча песка дошла до равновесия, это единственные уровни, которые должны
существовать.
Внутри цикла он использует a == level для создания логического массива, который имеет значение
True, где массив является уровнем, или значение False в противном случае. draw_array обрабаты-
вает эти логические типы данных как единицы и нули. На рис. 8‑4 показаны результаты для pile3.
Визуально эти конструкции напоминают фракталы, но внешность может быть обманчива. Чтобы быть
более уверенными, мы можем оценить размерность фрактала для каждой конструкции, используя
метод box-counting, о котором шла речь в разделе «Фракталы» на стр. 89.
Мы посчитаем количество клеток в маленькой ячейке в центре кучи, а затем посмотрим, как увели-
чивается количество клеток по мере увеличения размера ячейки. Вот моя реализация:
def count_cells(a):
n, m = a.shape
end = min(n, m)
res = []

100  Изучение сложных систем с помощью Python


for i in range(1, end, 2):
top = (n-i) // 2
left = (m-i) // 2
box = a[top: top+i, left: left+i]
total = np.sum(box)
res.append((i, i**2, total))
return np.transpose(res)
Параметр a – ​это логический массив. Размер ячейки изначально равен 1. Каждый раз в цикле он
увеличивается на 2, пока не достигнет end, что является меньшим из n и m.
Каждый раз в цикле box представляет собой набор клеток с шириной и высотой i, центрированных
в массиве. total – ​количество «включенных» ячеек в квадрате.

Рисунок 8-4. Модель песчаной кучи в равновесии,


выбирая ячейки с уровнями 0, 1, 2 и 3,
слева направо, сверху вниз

Глава 8. Самоорганизованная критичность   101


Результатом является список кортежей, где каждый кортеж содержит i, i**2 и  количество клеток
в квадрате. Когда мы передаем этот результат в transpose, NumPy преобразует его в массив с тремя
столбцами, а  затем транспонирует его; то  есть превращает столбцы в  строки, а  строки в  столбцы.
В результате получается массив из 3 строк: i, i**2 и total.
Вот как мы используем count_cells:
res = count_cells(pile.array==level)
steps, steps2, cells = res
Первая строка создает логический массив, который содержит значение True, где массив равен level,
вызывает count_cells и получает массив с тремя строками.
Вторая строка распаковывает строки и назначает их steps, steps2 и cells, которые мы можем по-
строить следующим образом:
thinkplot.plot(steps, steps2, linestyle='dashed')
thinkplot.plot(steps, cells)
thinkplot.plot(steps, steps, linestyle='dashed')
На рис. 8‑5 показаны результаты. На двойной логарифмической шкале количество ячеек образует
почти прямые линии, что указывает на то, что мы измеряем фрактальную размерность в допустимом
диапазоне размеров квадратов.
Чтобы расcчитать наклоны этих линий, мы можем использовать функцию SciPy linregress, которая
подгоняет линию к данным с помощью линейной регрессии (см. https://thinkcomplex.com/regress).
from scipy.stats import linregress
params = linregress(np.log(steps), np.log(cells))
slope = params[0]
Расчетные фрактальные размерности:
0 1,871
1 3.502
2 1.781
3 2.084
Фрактальная размерность уровней 0, 1 и 2 кажется явно нецелой. Это указывает на то, что изобра-
жение является фрактальным.
Расчет уровня 3 неотличим от 2, но, учитывая результаты для других значений, кажущуюся кривизну
линии и появление рисунка, представляется вероятным, что он также является фрактальным.
В одном из упражнений, приведенном в блокноте для этой главы, вам будет предложено снова вы-
полнить этот анализ с другими значениями n и начальным уровнем, чтобы проверить, согласуются ли
расчетные размерности.

102  Изучение сложных систем с помощью Python


Рисунок 8-5. Количество квадратов для ячеек
с уровнями 0, 1, 2 и 3 в сравнении
с пунктирными линиями с наклоном
1 и 2

Розовый шум
Название оригинальной статьи, в которой представлена модель песчаной кучи, называется «Само-
организованная критичность: объяснение 1/f-шума». Вы можете прочитать ее на странице https://
thinkcomplex.com/bak.
Как следует из подзаголовка, Бак, Танг и Визенфельд пытались объяснить, почему многие природ-
ные и  инженерные системы демонстрируют 1/f-шум, который также известен как «фликкер-шум»
и «розовый шум».
Чтобы понять, что такое розовый шум, мы должны пойти в обход, чтобы понять, что такое сигналы,
спектры мощности и шум.

Глава 8. Самоорганизованная критичность   103


Сигнал
Сигнал – ​это любая величина, которая изменяется во  времени. Один из  примеров – ​это звук,
который является изменением плотности воздуха. В модели песчаной кучи сигналы, которые мы
будем рассматривать, являются длительностями лавин и  размерами, поскольку они меняются
во времени.

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

Шум
При обычном использовании шум чаще является нежелательным звуком, но в контексте обра-
ботки сигнала это сигнал, который содержит много частотных компонентов.
Есть много видов шума. Например, «белый шум» – ​это сигнал, который имеет компоненты с равной
мощностью в широком диапазоне частот.
Другие виды шума имеют разные отношения между частотой и мощностью. В «красном шуме» мощ-
ность на частоте f равна 1/f 2, которую можно записать так:
P(f)= 1/f 2.
Мы можем обобщить это уравнение, заменив показатель степени 2 параметром β:
P(f) = 1/f β.
Когда β = 0, это уравнение описывает белый шум; когда β = 2, оно описывает красный шум. Когда
параметр около 1, результат называется 1/f-шумом. В принципе, шум с любым значением от 0 до 2
называется «розовым», потому что он находится между белым и красным.
Мы можем использовать данное соотношение, чтобы получить тест на розовый шум. Принимая ло-
гарифм обеих сторон, получаем:
log P(f) = – ​β log f.
Поэтому если мы строим график P(f) против f на  двойной логарифмической шкале, мы ожидаем
прямую линию с наклоном –β.
Какое это имеет отношение к модели песочной кучи? Предположим, что каждый раз, когда ячейка
опрокидывается, она издает звук. Если мы запишем модель песочной кучи во время ее работы, как
она будет звучать? В следующем разделе мы смоделируем звук модели песочной кучи и посмотрим,
будет ли это розовый шум.

Звук песка
Когда выполняется моя реализация класса SandPile, она записывает количество ячеек, которые
опрокидываются в течение каждого временного шага, накапливая результаты в  списке с  именем
toppled_seq. Запустив модель из  раздела «Распределения с  тяжелыми хвостами» на  стр. 98, мы
можем извлечь полученный сигнал:
signal = pile2.toppled_seq
Для вычисления спектра мощности этого сигнала мы можем использовать функцию SciPy welch:
from scipy.signal import welch
nperseg = 2048
freqs, spectrum = welch(signal, nperseg=nperseg, fs=nperseg)

104  Изучение сложных систем с помощью Python


Эта функция использует метод Уэлча, который разбивает сигнал на  сегменты и  вычисляет спектр
мощности каждого сегмента. Результат обычно шумный, поэтому метод Уэлча усредняет по сегмен-
там, чтобы рассчитать среднюю мощность на каждой частоте. Для получения дополнительной ин-
формации о методе Уэлча см. https://thinkcomplex.com/welch.
Параметр nperseg указывает количество временных шагов на сегмент. С более длинными сегмента-
ми мы можем рассчитать мощность для большего количества частот. С более короткими сегментами
мы получаем лучшие расчеты для каждой частоты. Значение, которое я выбрал, 2048, уравновеши-
вает эти компромиссы.
Параметр fs – ​это «частота дискретизации», которая представляет собой количество точек дан-
ных в  сигнале за  единицу времени. Установив fs = nperseg, мы получаем диапазон частот от  0
до nperseg/2. Этот диапазон удобен, но так как единицы времени в модели произвольны, это мало
что значит.
Возвращаемые значения, freqs и powers, – ​это массивы NumPy, содержащие частоты компонентов
и их соответствующие мощности, которые мы можем построить. На рис. 8‑6 показан результат.

Рисунок 8-6. Спектр мощности числа опрокинутых


ячеек с течением времени, двойная
логарифмическая шкала

Для частот между 10 и 1000 (в произвольных единицах) спектр падает на прямую линию, чего мы
и ожидаем для розового или красного шума.
Серая линия на рисунке имеет наклон –1,58, это указывает на то, что
log P(f) ∼ –β log f
с параметром β = 1,58, который является тем же параметром, о котором сообщают Бак, Тан и Визен-
фельд. Этот результат подтверждает, что модель песчаной кучи генерирует розовый шум.

Редукционизм и холизм
Оригинальная статья Бака, Танга и  Визенфельда является одной из  наиболее часто цитируемых
за последние несколько десятилетий. В ряде последующих статей сообщалось о других системах,
которые, по-видимому, являются самоорганизующимися критическими. Другие изучали модель пес-
чаной кучи более подробно.

Глава 8. Самоорганизованная критичность   105


Как оказалось, модель песчаной кучи – ​плохая. Песок плотный и не очень липкий, поэтому импульс
оказывает немалое влияние на  поведение лавин. В  результате количество лавин, очень больших
и очень малых, меньше, чем прогнозирует модель, и распределение может не иметь тяжелых хвос­
тов.
Бак предположил, что это наблюдение упускает из виду. Модель песчаной кучи не должна быть реа-
листичной; это должен быть простой пример широкой категории моделей.
Чтобы понять это, полезно поразмыслить над двумя видами моделей: редукционистской и холисти-
ческой. Редукционистская модель описывает систему, описывая ее части и их взаимодействия. Когда
в качестве объяснения используется редукционистская модель, она зависит от аналогии между ком-
понентами модели и компонентами системы.
Например, чтобы объяснить, почему действует закон идеального газа, мы можем моделировать мо-
лекулы, составляющие газ с точечными массами, и  моделировать их взаимодействия как упругие
удары. Если вы смоделируете или проанализируете эту модель, то обнаружите, что она подчиняется
закону идеального газа. Эта модель является удовлетворительной в той степени, в которой молекулы
в газе ведут себя как молекулы в модели. Аналогия находится между частями системы и частями
модели.
Холистические модели больше ориентированы на сходство между системами и меньше интересу-
ются аналогичными частями. Целостный подход к моделированию состоит из следующих этапов:
• наблюдать за поведением, которое проявляется в различных системах;
• найти простую модель, которая демонстрирует это поведение;
• определить элементы модели, которые необходимы и достаточны для формирования поведения.
Например, в  своей книге «Эгоистичный ген» Ричард Докинз (Richard Dawkins) предполагает, что
генетическая эволюция является лишь одним из  примеров эволюционной системы. Он определя-
ет основные элементы категории – ​дискретные репликаторы, изменчивость и дифференциальное
воспроизведение – ​и предполагает, что любая система с этими элементами будет свидетельствовать
об эволюции.
В качестве еще одного примера эволюционной системы он предлагает «мемы», являющиеся мыс-
лями или поведением, которые копируются при передаче от человека к человеку. Поскольку мемы
конкурируют за ресурс человеческого внимания, они эволюционируют способами, напоминающими
генетическую эволюцию.
Критики модели мемов указывали на то, что мемы – ​плохая аналогия для генов; они отличаются
от генов во многих очевидных отношениях. Докинз утверждал, что эти различия не имеют значения,
потому что мемы не должны быть аналогами генов. Скорее, мемы и генетика являются примерами
из одной и той же категории: эволюционные системы.
Различия между ними подчеркивают реальную точку зрения, которая заключается в том, что эволю-
ция является общей моделью, которая применяется ко многим, казалось бы, разнородным системам.
Логическая структура этого аргумента показана на рис. 8‑7.

Рисунок 8-7. Логическая структура холистической


модели

106  Изучение сложных систем с помощью Python


Бак привел аналогичный аргумент, что самоорганизованная критичность является общей моделью
для широкой категории систем:
Поскольку эти явления проявляются повсюду, они не могут зависеть от какой-либо конкретной
детали… Если физика большого класса задач одинакова, это дает [теоретику] возможность вы-
бора простейшей возможной [модели], принадлежащей этому классу для детального изучения1.
Многие природные системы демонстрируют поведение, характерное для критических систем. Бак
объясняет эту распространенность тем, что данные системы являются примерами широкой катего-
рии самоорганизованной критичности. Есть два способа поддержать этот аргумент. Одним из них
является создание реалистичной модели конкретной системы и  задача показать, что модель де-
монстрирует самоорганизованную критичность. Второй – ​показать, что самоорганизованная кри-
тичность является признаком многих разнообразных моделей, и выявить основные характеристики,
общие для этих моделей.
Первый подход, который я характеризую как редукционистский, может объяснить поведение кон-
кретной системы. Второй подход, который я называю холистическим, может объяснить распростра-
ненность критичности в природных системах. Это разные модели с разными целями.
Для редукционистских моделей реализм является главной добродетелью, а простота вторична. Для
холистических моделей все наоборот.

Самоорганизованная критичность, причинность


и прогнозирование
Если индекс фондового рынка падает на долю процента в день, не нужно ничего объяснять. Но если
он падает на 10 %, люди хотят знать, почему. Ученые мужи по телевидению готовы дать объяснения,
но реальный ответ может состоять в том, что никаких объяснений нет.
Повседневная изменчивость на  фондовом рынке свидетельствует о  критичности: распределение
изменений стоимости имеет тяжелый хвост, а временные ряды демонстрируют розовый шум. Если
фондовый рынок является критической системой, мы должны ожидать случайных больших измене-
ний в рамках обычного поведения рынка.
Распределение размеров землетрясений также имеет тяжелый хвост, и существуют простые модели
динамики геологических разломов, которые могут объяснить данное поведение. Если эти модели
верны, они подразумевают, что сильные землетрясения не являются исключительными; то есть они
не требуют объяснения больше, чем небольшие землетрясения.
Точно так же Чарльз Перроу (Charles Perrow) предположил, что отказы в крупных инженерных си-
стемах, таких как атомные электростанции, напоминают лавины в модели песочной кучи. Большин-
ство сбоев являются небольшими, изолированными и безвредными, но иногда совпадение неудач
приводит к  катастрофе. Когда происходит крупная авария, следователи ищут ее причину, но  если
«нормальная теория» Перроу верна, то особых причин для крупных сбоев может не быть.
Эти выводы неутешительны. Среди прочего они подразумевают, что сильные землетрясения и не-
которые виды несчастных случаев принципиально непредсказуемы. Невозможно посмотреть на со-
стояние критической системы и сказать, является ли большая лавина «надлежащей». Если система
находится в критическом состоянии, то возможна большая лавина. Это зависит только от следующей
песчинки.

1
Bak. How Nature Works. Springer-Verlag, 1996. С. 43.

Глава 8. Самоорганизованная критичность   107


В модели песочной кучи что является причиной большой лавины? Философы иногда различают не-
посредственную причину, которая наиболее непосредственно ответственна, от конечной причины,
которая рассматривается как более глубокое объяснение (см. https://thinkcomplex.com/cause).
В модели песочной кучи непосредственной причиной лавины является песчинка, но песчинка, ко-
торая вызывает большую лавину, идентична любой другой песчинке, поэтому она не дает никаких
особых объяснений. Конечная причина большой лавины – ​структура и  динамика систем в  целом:
большие лавины происходят потому, что являются собственностью системы.
Многие социальные явления, включая войны, революции, эпидемии, изобретения и террористиче-
ские акты, характеризуются распределениями с тяжелыми хвостами. Если эти распределения рас-
пространены, потому что социальные системы являются самоорганизованной критичностью, основ-
ные исторические события могут быть в корне непредсказуемыми и необъяснимыми.

Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap08.ipynb в репозитории для этой
книги. Откройте этот блокнот, прочитайте код и запустите ячейки. Вы можете использовать этот блок-
нот для работы над следующими упражнениями. Мои решения находятся в chap08soln.ipynb. Более
подробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 8‑1
Чтобы проверить, имеют ли распределения T и S тяжелые хвосты, мы построили графики их функций
вероятности на двойной логарифмической шкале, как показано в статье Бака, Танга и Визенфельда.
Но,  как мы видели в  разделе «Интегральные распределения» на  стр. 54, эта визуализация может
затенить форму распределения. Используя те же данные, составьте график, который показывает ин-
тегральные распределения S и  T. Что вы можете сказать об  их форме? Они следуют степенному
закону? Они с тяжелым хвостом?
Возможно, вам будет полезно построить график интегральных распределений на шкале x и на двой-
ной логарифмической шкале.

Упражнение 8‑2
В разделе «Фракталы» на стр. 100 мы продемонстрировали, что первоначальная конфигурация мо-
дели песочной кучи создает фрактальные конструкции. Но после того, как мы бросим большое коли-
чество случайных песчинок, конструкции выглядят более случайными.
Начиная с примера в разделе «Фракталы» на стр. 100, на некоторое время запустите модель песоч-
ной кучи, а затем вычислите фрактальные размерности для каждого из четырёх уровней. Являет-
ся ли модель песочной кучи устойчивой?

Упражнение 8‑3
Еще одна версия модели песочной кучи под названием модель «одного источника» начинается
с другого начального условия: вместо всех ячеек на одном уровне все ячейки установлены в 0, кро-
ме центральной ячейки, которая имеет большое значение. Напишите функцию, которая создает объ-
ект SandPile, устанавливает начальное условие для одного источника и работает до тех пор, пока
куча не достигнет равновесия. Результат кажется фрактальным?
Вы можете прочитать больше об этой версии модели на странице https://thinkcomplex.com/sand.

Упражнение 8‑4
В своей статье 1989 года (см. https://thinkcomplex.com/bak89) Бак, Чень и Кройц предполагают, что
игра «Жизнь» является самоорганизующейся критической системой.

108  Изучение сложных систем с помощью Python


Чтобы воспроизвести их тесты, начните со  случайной конфигурации и  запускайте игру «Жизнь»
до тех пор, пока она не стабилизируется. Затем выберите случайную ячейку и переверните ее. Запус­
кайте клеточный автомат до тех пор, пока он снова не стабилизируется, отслеживая T, количество
временных шагов, которые он делает, и  S, количество затронутых ячеек. Повторите эти действия
для большого количества испытаний и составьте график распределений T и S. Кроме того, оцените
спектры мощности T и S как сигналы во времени и посмотрите, соответствуют ли они розовому шуму.

Упражнение 8‑5
В своей книге «Фрактальная геометрия природы» Бенуа Мандельброт (Benoit Mandelbrot) предлага-
ет то, что он называет «еретическим» объяснением распространенности распределений с тяжелыми
хвостами в природных системах.
Как предполагает Бак, многие системы могут и не генерировать это поведение изолированно. Вмес­
то этого только несколько взаимодействий между системами может привести к распространению
поведения.
Чтобы поддержать этот аргумент, Мандельброт указывает:
• распределение наблюдаемых данных часто является «совместным эффектом фиксированного
базового истинного распределения и сильно изменяющегося фильтра»;
• распределения с тяжелыми хвостами устойчивы к фильтрации; то есть «большое разнообразие
фильтров оставляет их асимптотическое поведение неизменным».
Что вы думаете об этом аргументе? Вы бы охарактеризовали его как редукционистский или холис­
тический?

Упражнение 8‑6
Прочитайте о теории великих людей на странице https://thinkcomplex.com/great. Какое значение име-
ет самоорганизованная критичность для этой теории?

Глава 8. Самоорганизованная критичность   109


Глава 9.
Агент-ориентированные модели
Модели, которые мы видели до сих пор, можно охарактеризовать как «основанные на правилах»
в том смысле, что они включают системы, управляемые простыми правилами. В этой и последующих
главах мы рассмотрим агент-ориентированные модели.
Агент-ориентированные модели включают в  себя агентов, предназначенных для моделирования
людей и других объектов, которые собирают информацию о мире, принимают решения и предпри-
нимают действия.
Агенты обычно располагаются в пространстве или в сети и взаимодействуют друг с другом локально.
У них обычно есть несовершенная или неполная информация о мире.
Часто существуют различия между агентами, в отличие от предыдущих моделей, где все компоненты
идентичны. И агент-ориентированные модели часто включают в себя случайность как среди агентов,
так и в мире.
С 70-х годов агентное моделирование стало важным инструментом в экономике, других социальных
и некоторых естественных науках.
Агент-ориентированные модели полезны для моделирования динамики систем, которые не пребы-
вают в равновесии (хотя они также используются для изучения равновесия). И они особенно полез-
ны для понимания отношений между отдельными решениями и поведением системы.

Модель Шеллинга
В 1969 году Томас Шеллинг опубликовал статью «Модели сегрегации», в которой предложена прос­
тая модель расовой сегрегации. Вы можете прочитать ее на странице https://thinkcomplex.com/schell.
Модель мира Шеллинга – ​это сетка, в которой каждая ячейка обозначает дом. Дома заняты двумя
видами агентов, помеченных красным и синим, в примерно равных количествах. Около 10 % домов
пусты.
В любой момент времени агент может быть счастлив или несчастен, в зависимости от других агентов
по соседству, где «окрестность» каждого дома – ​это набор из восьми соседних ячеек. В одной версии
модели агенты счастливы, если у них есть как минимум два соседа, таких же как они, и несчастны,
если у них есть один сосед или ноль соседей.
Моделирование продолжается, выбирая агента наугад и проверяя, счастлив ли он. Если да, ничего
не происходит; если нет, агент выбирает одну из незанятых ячеек случайным образом и перемеща-
ется.
Вы не будете удивлены, узнав, что эта модель приводит к некоторой сегрегации, но вы можете быть
удивлены степенью. Из  случайной начальной точки кластеры похожих агентов образуются почти
сразу. Кластеры растут и сливаются со временем, пока не появится небольшое количество крупных
кластеров, а большинство агентов не будет жить в однородных окрестностях.
Если вы не знали процесс и видели только результат, вы могли бы предположить, что агенты – ​расис­
ты, но на самом деле все они были бы совершенно счастливы в смешанной окрестности.

110  Изучение сложных систем с помощью Python


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

Реализация модели Шеллинга


Чтобы реализовать модель Шеллинга, я написал еще один класс, который наследует от Cell2D:
class Schelling(Cell2D):
def __init__(self, n, p):
self.p = p
choices = [0, 1, 2]
probs = [0.1, 0.45, 0.45]
self.array = np.random.choice(choices, (n, n), p=probs)
n – ​размер сетки, а p – ​порог доли похожих соседей. Например, если р = 0,3, агент будет несчастен,
если менее 30 % его соседей одного цвета.
array – ​это массив NumPy, где каждая ячейка равна 0, если она пуста, 1, если занята красным аген-
том, и 2, если занята синим агентом. Первоначально 10 % ячеек пустые, 45 % красные и 45 % синие.
Функция step для модели Шеллинга существенно сложнее, чем в предыдущих примерах. Если вас
не интересуют подробности, вы можете перейти к следующему разделу. Но если вы останетесь, мо-
жете взять несколько советов NumPy.
Сначала я создаю логические массивы, которые указывают, какие ячейки являются красными, сини-
ми и пустыми:
a = self.array
red = a==1
blue = a==2
empty = a==0
Затем я  использую функцию correlate2d для подсчета для каждого местоположения количества
соседних ячеек, которые являются красными, синими и непустыми. Мы встречались с correlate2d
в разделе «Реализация игры “Жизнь”» на стр. 78.
options = dict(mode='same', boundary='wrap')
kernel = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]], dtype=np.int8)
num_red = correlate2d (red, kernel, **options)
num_blue = correlate2d (blue, kernel, **options)
num_neighbors = num_red + num_blue

Глава 9. Агент-ориентированные модели   111


options – ​это словарь, содержащий параметры, которые мы передаем correlate2d. Когда mode =
'same', результат будет того же размера, что и входные данные. При использовании border = 'wrap'
верхний край сворачивается так, чтобы соответствовать нижнему, а левый край – ​правому.
Ядро указывает на то, что мы хотим изучить восемь соседей, которые окружают каждую ячейку.
После вычисления num_red и num_blue мы можем вычислить долю соседей для каждого местополо-
жения, которые являются красными и синими.
frac_red = num_red / num_neighbors
frac_blue = num_blue / num_neighbors
Затем мы можем вычислить долю соседей для каждого агента того же цвета, что и агент. Я использую
np.where, который похож на поэлементное выражение if. Первый параметр – ​это условие, которое
выбирает элементы из второго или третьего параметра.
frac_same = np.where(red, frac_red, frac_blue)
frac_same[empty] = np.nan
В этом случае везде, где red имеет значение True, frac_same получает соответствующий элемент
frac_red. Там, где red имеет значение False, frac_same получает соответствующий элемент frac_
blue. Наконец, там, где empty означает, что ячейка пуста, frac_same получает значение np.nan. Это
специальное значение, которое указывает «не число».
Теперь мы можем определить местонахождение несчастных агентов:
unhappy = frac_same < self.p
unhappy_locs = locs_where(unhappy)
locs_where – ​это функция-обертка для np.nonzero:
def locs_where(condition):
return list(zip(*np.nonzero(condition)))
np.nonzero принимает массив и возвращает координаты всех ненулевых ячеек; результатом явля-
ется кортеж массивов, по одному на каждую размерность. Затем locs_where использует list и zip
для преобразования этого результата в список координатных пар.
Аналогично, empty_locs – ​это массив, который содержит координаты пустых ячеек:
empty_locs = locs_where(empty)
Теперь мы добрались до сути моделирования. Мы перебираем несчастных агентов и перемещаем
их:
num_empty = np.sum(empty)
for source in unhappy_locs:
i = np.random.randint(num_empty)
dest = empty_locs[i]
a[dest] = a[source]
a[source] = 0
empty_locs[i] = source
i – ​индекс случайной пустой ячейки; dest – ​это кортеж, содержащий координаты пустой ячейки.
Чтобы переместить агента, мы копируем его значение (1 или 2) из source в dest, а затем устанавли-
ваем значение source в 0 (поскольку оно теперь пустое).
Наконец, мы заменяем запись в  empty_locs на  source, поэтому ячейка, которая только что стала
пустой, может быть выбрана следующим агентом.

112  Изучение сложных систем с помощью Python


Сегрегация
Теперь давайте посмотрим, что происходит, когда мы запускаем модель. Я начну с n = 100 и p = 0.3
и запущу для 10 шагов.
grid = Schelling(n=100, p=0.3)
for i in range(10):
grid.step()
На рис. 9‑1 показана исходная конфигурация (слева), состояние моделирования после двух шагов
(в середине) и состояние после 10 шагов (справа).

Рисунок 9-1. Модель сегрегации Шеллинга с n = 100,


начальным условием (слева), после
2 шагов (в центре) и после 10 шагов
(справа)

Кластеры образуются почти сразу и быстро растут, пока большинство агентов не живёт в высокосег­
регированных районах.
Во время модуляции мы можем вычислить степень сегрегации, которая является средней по агентам
от долей соседей того же цвета, что и агент:
np.nanmean(frac_same)
На рис. 9‑1 средняя доля похожих соседей составляет 50 % в исходной конфигурации, 65 % после
двух шагов и 76 % после 10 шагов!
Помните, что когда р = 0,3, агенты были бы счастливы, если бы три из восьми соседей были их же
цвета, но в конечном итоге они жили в районах, где 6 или 7 их соседей, как правило, их цвета.
На рис. 9‑2 показано, как увеличивается степень сегрегации и где она выравнивается для несколь-
ких значений p. Когда р = 0,4, степень сегрегации в устойчивом состоянии составляет около 82 %,
и у большинства агентов нет соседей с другим цветом.
Эти результаты удивляют многих и служат ярким примером непредсказуемой взаимосвязи между
индивидуальными решениями и поведением системы.

Глава 9. Агент-ориентированные модели   113


Рисунок 9-2. Степень сегрегации в модели
Шеллинга с течением времени для
диапазона p

Sugarscape
В 1996  году Джошуа Эпштейн (Joshua Epstein) и  Роберт Акстелл (Robert Axtell) предложили
Sugarscape, агент-ориентированную модель «искусственного общества», предназначенную для под-
держки экспериментов, связанных с экономикой и другими социальными науками.
Sugarscape – ​это универсальная модель, адаптированная для самых разных тем. В качестве приме-
ров я приведу несколько первых экспериментов из книги Эпштейна и Акстелла «Растущие искус-
ственные общества».
В своей простейшей форме Sugarscape представляет собой модель простой экономики, в которой
агенты перемещаются по двумерной сетке, собирая и накапливая «сахар», который представляет со-
бой экономическое благосостояние. Некоторые части сетки производят больше сахара, чем другие,
и некоторые агенты находят его лучше, чем другие.
Эта версия Sugarscape часто используется для изучения и  объяснения распределения богатства,
в частности тенденции к неравенству.
В сетке Sugarscape каждая ячейка имеет емкость, которая является максимальным количеством са-
хара, которое она может содержать. В исходной конфигурации есть две области с высоким содержа-
нием сахара с емкостью 4, окруженные концентрическими кольцами с емкостями 3, 2 и 1.
На рис. 9‑3 (слева) показана начальная конфигурация с более темными областями, обозначающими
ячейки с большей емкостью, и маленькими точками, обозначающими агентов.

114  Изучение сложных систем с помощью Python


Рисунок 9-3. Репликация оригинальной модели
Sugarscape: начальная конфигурация
(слева), после 2 шагов (в центре)
и после 100 шагов (справа)

Изначально 400 агентов размещены в случайных местах. Каждый агент имеет три случайно выбран-
ных атрибута:

Сахар
Каждый агент начинается с запаса сахара, выбранного из равномерного распределения между
5 и 25 единицами.

Метаболизм
У каждого агента есть некоторое количество сахара, которое он должен потреблять за времен-
ной шаг, выбираемый равномерно между 1 и 4.

Зрение
Каждый агент может «видеть» количество сахара в соседних ячейках и перемещаться в ячейку
с наибольшим количеством, но некоторые агенты могут видеть и двигаться дальше, чем другие.
Расстояние, которое видят агенты, выбирается равномерно между 1 и 6.
В течение каждого временного шага агенты перемещаются по одному в случайном порядке. Каждый
агент следует этим правилам:
• агент просматривает k ячеек в каждом из четырёх направлений компаса, где k – ​диапазон зре-
ния агента;
• выбирает незанятую ячейку с наибольшим количеством сахара. В случае связи он выбирает бо-
лее близкую ячейку; среди ячеек на одинаковом расстоянии он выбирает случайным образом;
• агент перемещается в выбранную ячейку и собирает сахар, добавляя урожай к накопленному
богатству и оставляя ячейку пустой;
• агент потребляет некоторую часть своего богатства в зависимости от своего метаболизма. Если
итоговая сумма отрицательна, агент «голодает» и перемещается.
После того как все агенты выполнили эти шаги, ячейки снова выращивают немного сахара, обычно
это 1 единица, но общее количество сахара в каждой ячейке ограничено ее емкостью.
На рис. 9‑3 (в центре) показано состояние модели после двух шагов. Большинство агентов движется
к районам с наибольшим количеством сахара. Агенты с хорошим зрением движутся быстрее всего;
агенты со слабым зрением, как правило, застревают на плато, беспорядочно блуждая, пока не по-
дойдут достаточно близко, чтобы увидеть следующий уровень.

Глава 9. Агент-ориентированные модели   115


Агенты, родившиеся в районах с наименьшим количеством сахара, могут голодать, если они не об-
ладают высоким начальным запасом и хорошим зрением.
В районах с высоким содержанием сахара агенты конкурируют друг с другом, чтобы найти и собрать
сахар по мере его роста. Агенты с высоким метаболизмом или слабым зрением наиболее подвер-
жены голоданию.
Когда сахар вырастает на  1 единицу за  временной шаг, сахара недостаточно для поддержания
400 агентов, с которых мы начали. Сначала популяция быстро падает, затем медленнее и составляет
около 250 агентов.
На рис. 9‑3 (справа) показано состояние модели после 100 временных шагов с примерно 250 аген-
тами. Агенты, которые выживают, как правило, являются счастливчиками, рожденными с  хорошим
зрением и/или низким метаболизмом. Дожив до  этого момента, они, вероятно, выживут навсегда,
накапливая неограниченные запасы сахара.

Имущественное неравенство
В своей нынешней форме Sugarscape моделирует простую экологию и может использоваться для
изучения взаимосвязи между такими параметрами модели, как скорость роста и атрибуты агентов,
и пропускной способностью системы (количество агентов, которые выживают в устойчивом состоя-
нии). И она моделирует форму естественного отбора, при которой у агентов с более высокой «при-
способленностью» больше шансов выжить.
Данная модель также демонстрирует своеобразное имущественное неравенство, когда некоторые
агенты накапливают сахар быстрее, чем другие. Но было бы трудно сказать что-то конкретное о рас-
пределении богатства, потому что оно не «стационарно»; то есть распределение изменяется со вре-
менем и не достигает устойчивого состояния.
Однако если мы дадим агентам конечную продолжительность жизни, модель произведет стационар-
ное распределение богатства. Затем мы можем провести эксперименты, чтобы увидеть, как парамет­
ры и правила влияют на это распределение.
В этой версии модели возраст агентов увеличивается с каждым временным шагом, а случайная про-
должительность жизни выбирается из равномерного распределения от 60 до 100. Если возраст аген-
та превышает его продолжительность, он умирает.
Когда агент умирает от голода или старости, его заменяет новый агент со случайными атрибутами,
поэтому число агентов остается постоянным.
Начиная с 250 агентов (что близко к пропускной способности), я запускаю модель для 500 шагов.
Пос­ле каждых 100 шагов я строю график интегральной функции распределения сахара, накоплен-
ного агентами. Мы встречались с интегральными функциями распределения в разделе «Интеграль-
ные распределения» на стр. 54.
На рис. 9‑4 показаны результаты на линейной шкале (слева) и шкале x (справа).
Приблизительно после 200 шагов (что в два раза превышает продолжительность жизни) распреде-
ление несильно меняется. И это перекос вправо.
У большинства агентов мало накопленного богатства: 25-й процентиль составляет около 10, а медиа­
на – ​около 20. Но немногие агенты накопили гораздо больше: 75-й процентиль – ​около 40, а наи-
большее значение – ​более 150.
На логарифмической шкале форма распределения напоминает гауссово или нормальное распре-
деление, хотя хвост справа усечен. Если бы оно было действительно нормальным на логарифмиче-
ской шкале, распределение было бы логнормальным, то есть это было бы распределение с тяжелым
хвостом. И на самом деле, распределение богатства практически в каждой стране и в мире – ​это
распределением с тяжелым хвостом.

116  Изучение сложных систем с помощью Python


Рисунок 9-4. Распределение сахара
(благосостояния) после 100, 200, 300
и 400 шагов (серые линии) и 500 шагов
(темная линия). Линейная шкала
(слева) и логарифмическая шкала
по оси х (справа)

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

Реализация Sugarscape
Модель Sugarscape более сложная по сравнению с предыдущими моделями, поэтому я не буду здесь
приводить полную реализацию. Я  обрисую структуру кода, и  вы можете посмотреть подробности
в блокноте Jupyter для этой главы, chap09.ipynb, который находится в репозитории для этой книги.
Если вас не интересуют подробности, вы можете пропустить данный раздел.
На каждом этапе агент перемещается, собирает сахар и стареет. Вот класс Agent и его метод step:
class Agent:
def step(self, env):
self.loc = env.look_and_move(self.loc, self.vision)
self.sugar += env.harvest(self.loc) – ​self.metabolism
self.age += 1
Параметр env – ​это ссылка на среду, которая является объектом Sugarscape. Он предоставляет ме-
тоды look_and_move и harvest:

Глава 9. Агент-ориентированные модели   117


• look_and_move принимает местоположение агента, которое является кортежем координат,
и диа­пазон зрения агента, которое является целым числом. Он возвращает новое местоположе-
ние агента, которое является видимой ячейкой с наибольшим количеством сахара;
• harvest занимает (новое) местоположение агента, перемещается и  возвращает сахар в  этом
месте.
Sugarscape наследуется от Cell2D, поэтому он похож на другие модели на основе сетки, с которыми
мы встречались.
Атрибуты включают в себя agents, представляющий собой список объектов Agent, и occupied, пред-
ставляющий собой набор кортежей, где каждый кортеж содержит координаты ячейки, занятой аген-
том.
Вот класс Sugarscape и его метод step:
class Sugarscape (Cell2D):
def step(self):
#прогоняем через агентов в случайном порядке;
random_order = np.random.permutation(self.agents)
for agent in random_order:
#помечаем, что текущая ячейка не занята;
self.occupied.remove(agent.loc)
#выполняем один шаг;
agent.step(self)
#если агент мертв, удаляем его из списка;
if agent.is_starving():
self.agents.remove(agent)
else:
#в противном случае помечаем, что его ячейка занята;
self.occupied.add(agent.loc)
#выращиваем немного сахара;
self.grow()
return len(self.agents)
В течение каждого шага Sugarscape использует функцию NumPy permutation, поэтому он перебира-
ет агентов в случайном порядке. Он вызывает step для каждого агента, а затем проверяет, мертв ли
он. После того как все агенты переехали, часть сахара вырастает снова. Возвращаемое значение –​
число агентов, которые еще живы.
Я не буду приводить здесь подробности; вы можете увидеть их в блокноте для этой главы. Если вы
хотите узнать больше о NumPy, можете рассмотреть следующие функции, в частности:
• make_visible_locs – ​создает массив местоположений, которые может видеть агент, в зависи-
мости от своего зрения. Местоположения сортируются по расстоянию, при этом местоположе-
ния на одном и том же расстоянии появляются в случайном порядке. Эта функция использует
np.random.shuffle и np.vstack;
• make_capacity  – ​инициализирует емкость ячеек, используя функции NumPy indices, hypot,
minimum и digitize;
• look_and_move – и
​ спользует argmax.

118  Изучение сложных систем с помощью Python


Миграция и волновое поведение
Хотя цель Sugarscape состоит не  в  том, чтобы в  первую очередь исследовать движение агентов
в пространстве, Эпштейн и Акстелл наблюдали некоторые интересные закономерности, когда аген-
ты мигрируют.
Если мы начнем со всех агентов в левом нижнем углу, они быстро сместятся к ближайшему «пику»
ячеек с высокой пропускной способностью. Но если агентов больше, чем может выдержать один пик,
они быстро опустошают запасы сахара и  вынуждены двигаться в  районы с  меньшей пропускной
способностью.
Те, у кого самое длинное зрение, пересекают долину между вершинами и распространяются на се-
веро-восток по  схеме, напоминающей фронт волны. Поскольку они оставляют за  собой полоску
из пустых клеток, другие агенты не следуют за ними, пока сахар не вырастет снова.
Результатом является серия дискретных волн миграции, где каждая волна напоминает целостный
объект, подобно космическим кораблям, которые мы видели в  Правиле 110 и  игре «Жизнь» (см.
«Космические корабли» на стр. 64 и «Конструкции игры “Жизнь”» на стр. 74).
На рис. 9‑5 показано исходное состояние (слева) и состояние модели после 6 шагов (в середине)
и 12 шагов (справа). Можно увидеть первые две волны, достигающие и движущиеся через второй
пик, оставляя позади полосу из пустых ячеек. В блокноте для этой главы можно посмотреть аними-
рованную версию этой модели, где волновые конструкции видны более четко.

Рисунок 9-5. Волновое поведение в Sugarscape:


начальная конфигурация (слева), после
6 шагов (в центре) и после 12 шагов
(справа)

Эти волны движутся по диагонали, что удивительно, потому что сами агенты движутся только на се-
вер или восток, а не на северо-восток. Подобные результаты – ​группы или «совокупности» со свой-
ствами и поведением, которых нет у агентов, – ​распространены в агент-ориентированных моделях.
Мы увидим больше примеров в следующей главе.

Эмерджентность
Примеры, приведенные в этой главе, демонстрируют одну из самых важных идей в науке о сложных
системах: эмерджентность. Эмерджентное свойство – ​это характеристика системы, которая возни-
кает в результате взаимодействия ее компонентов, а не их свойств.

Глава 9. Агент-ориентированные модели   119


Чтобы уточнить, что такое эмерджентность, полезно рассмотреть, чем она не  является. Например,
кирпичная стена является твердой, потому что кирпичи и строительный раствор твердые, поэтому
это не эмерджентное свойство.
В качестве другого примера: некоторые жесткие структуры построены из гибких компонентов, так
что это выглядит как своего рода эмерджентость. Но это в лучшем случае слабый тип, потому что
структурные свойства следуют из хорошо понятых законов механики.
Напротив, сегрегация, которую мы видим в модели Шеллинга, является эмерджентным свойством,
потому что она не вызвана расистскими агентами. Даже когда агенты – ​всего лишь умеренные ксе-
нофобы, результат системы существенно отличается от намерения решений агента.
Распределение богатства в Sugarscape может быть эмерджентным свойством, но это слабый при-
мер, потому что мы можем разумно предсказать его, основываясь на распределении зрения, обмена
веществ и продолжительности жизни. Волновое поведение, которое мы видели в последнем при-
мере, может быть более сильным примером, поскольку волна отображает возможность – ​движение
по диа­гонали, – к​ оторой у агентов нет.
Эмерджентные свойства удивительны: трудно предсказать поведение системы, даже если мы знаем
все правила. Эта трудность не случайна; на самом деле она может быть определяющей характерис­
тикой эмерджентности.
Как говорит Вольфрам в своей книге «Наука нового типа», традиционная наука основана на аксио-
ме: если вы знаете правила, управляющие системой, вы можете предсказать ее поведение.
То, что мы называем «законами», часто представляет собой вычислительные сокращения, которые
позволяют нам предсказать исход системы, не создавая и не наблюдая ее.
Но многие клеточные автоматы являются вычислительно неприводимыми, а это означает, что корот-
ких путей нет. Единственный способ получить результат – ​внедрить систему.
То  же самое можно сказать и  о  сложных системах в  целом. Для физических систем с  более чем
несколькими компонентами обычно не  существует модели, которая дает аналитическое решение.
Численные методы обеспечивают своего рода сокращенный способ вычислений, но  все  же есть
качественное различие.
Аналитические решения часто предоставляют алгоритм прогнозирования с постоянным временем;
то есть время выполнения вычисления не зависит от t, временной шкалы прогнозирования. Но чис-
ленные методы, моделирование, аналоговые вычисления и  подобные методы требуют времени,
пропорционального t. А  для многих систем существует предел, за  которым мы вообще не  можем
вычислить надежные прогнозы.
Эти наблюдения предполагают, что эмерджентные свойства являются в  основном непредсказуе-
мыми и что в случае со сложными системами не следует ожидать, что естественные законы будут
найдены в форме сокращенных способов вычислений.
Для некоторых «эмерджентность» – ​это еще одно название невежества. Исходя из этого, свойство
эмерджентно, если у нас нет редукционистского объяснения для него, но если мы поймем его лучше
в будущем, оно больше не будет эмерджентным.
Статус эмерджентных свойств является предметом дискуссий, поэтому уместно быть скептиком. Ког-
да мы видим явно эмерджентное свойство, мы не должны предполагать, что никогда не может быть
редукционистского объяснения. Но мы также не должны предполагать, что оно должно быть.
Примеры, приведенные в этой книге, и принцип вычислительной эквивалентности дают веские ос-
нования полагать, что по крайней мере некоторые эмерджентные свойства никогда не смогут быть
«объяснены» классической редукционистской моделью.
Подробнее об эмерджентности можно прочитать на странице https://thinkcomplex.com/emerge.

120  Изучение сложных систем с помощью Python


Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap09.ipynb в репозитории для этой
книги. Откройте данный блокнот, прочитайте код и  запустите ячейки. Вы можете использовать
этот блокнот для работы над следующими упражнениями. Мои решения находятся в  chap09soln.
ipynb. Более подробная информация о работе с кодом приведена в разделе «Использование кода»
на стр. 10.

Упражнение 9‑1
Билл Бишоп (Bill Bishop), автор книги «Большая сортировка», утверждает, что американское общест­
во все в  большей степени разделяется политическими взглядами, поскольку люди предпочитают
жить среди соседей-единомышленников.
Механизм, который предлагает Бишоп, заключается не в том, что люди, подобно агентам в модели
Шеллинга, более склонны двигаться, если они изолированы, а в том, что когда они двигаются по ка-
кой-либо причине, они могут выбрать соседство с такими же людьми, как они.
Измените свою реализацию модели Шеллинга, чтобы смоделировать данный тип поведения и по-
смотреть, дает ли он аналогичные степени сегрегации.
Есть несколько способов смоделировать гипотезу Бишопа. В  моей реализации случайный выбор
агентов перемещается на каждом этапе. Каждый агент рассматривает k случайно выбранных пустых
местоположений и выбирает место с наибольшей долей похожих соседей. Как степень сегрегации
зависит от k?

Упражнение 9‑2
В первой версии Sugarscape мы так и не добавили агентов, поэтому когда население уменьшается,
оно никогда не восстанавливается. Во втором варианте мы заменяем агентов, только когда они уми-
рают, поэтому численность населения постоянна. Теперь давайте посмотрим, что произойдет, если
мы добавим некоторое «демографическое давление».
Напишите версию Sugarscape, которая добавляет нового агента в конце каждого шага. Добавьте код,
чтобы вычислить среднее зрение и средний метаболизм агентов в конце каждого шага. Запустите
модель на несколько сотен шагов и постройте график зависимости населения от времени, а также
от среднего зрения и среднего метаболизма.
Вы должны быть в  состоянии реализовать эту модель, наследуя от  Sugarscape и  переопределяя
__init__ и step.

Упражнение 9‑3
Люди, которые изучают философию разума, знакомы с «Сильным ИИ». Это теория, согласно которой
у правильно запрограммированного компьютера может быть разум в том же смысле, что и у людей.
Джон Сёрл (John Searle) представил мысленный эксперимент под названием «Китайская комната»,
призванный показать, что Сильный ИИ является ложным. Вы можете прочитать об этом на странице
https://thinkcomplex.com/searle.
Каков системный ответ на аргумент китайской комнаты? Как то, что вы узнали об эмерджентности,
влияет на вашу реакцию на ответ системы?

Глава 9. Агент-ориентированные модели   121


Глава 10.
Стаи, стада и пробки
Агент-ориентированные модели в  предыдущей главе основаны на  сетках: агенты занимают дис-
кретные местоположения в двумерном пространстве. В этой главе мы рассмотрим агентов, которые
движутся в непрерывном пространстве, включая моделируемые автомобили на одномерной трассе
и моделируемых птиц в трехмерном пространстве.

Пробки
Что вызывает пробки? Иногда существует очевидная причина, например авария, превышение ско-
рости или что-то еще, что мешает движению транспорта. Но в других случаях пробки появляются без
видимой причины.
Агент-ориентированные модели могут помочь объяснить спонтанные пробки. В  качестве приме-
ра я реализую симуляцию автомагистрали на основе модели из книги Митчелла Резника (Mitchell
Resnick) «Черепахи, термиты и пробки».
Вот класс, который представляет «шоссе»:
class Highway:
def __init__(self, n=10, length=1000, eps=0):
self.length = length
self.eps = eps
#создаем водителей;
locs = np.linspace(0, length, n, endpoint=False)
self.drivers = [Driver(loc) for loc in locs]
#и соединяем их;
for i in range(n):
j = (i+1)% n
self.drivers[i].next = self.drivers[j]
n – ​количество автомобилей, length – ​д лина шоссе, а eps – ​количество случайного шума, который
мы добавим в систему.
locs содержит местоположение драйверов. Функция NinsPy linspace создает массив из n местопо-
ложений, равномерно распределенных между 0 и length.
Атрибут drivers представляет собой список объектов Driver. Цикл for связывает их так, что каждый
объект содержит ссылку на следующий. Шоссе является круговым, поэтому последний объект Driver
содержит ссылку на первый.
В течение каждого временного шага Highway перемещает каждого из водителей:
# Highway
def step(self):

122  Изучение сложных систем с помощью Python


for driver in self.drivers:
self.move(driver)
Метод move позволяет объекту Driver выбрать ускорение. Затем move вычисляет обновленную ско-
рость и позицию. Ниже приводится реализация:
# Highway
def move(self, driver):
dist = self.distance(driver)
#позволяем водителю выбрать разгон;
acc = driver.choose_acceleration(dist)
acc = min(acc, self.max_acc)
acc = max(acc, self.min_acc)
speed = driver.speed + acc
#добавляем случайный шум к скорости;
speed *= np.random.uniform(1-self.eps, 1+self.eps)
#оставляем ее неотрицательной с соблюдением скоростного режима;
speed = max(speed, 0)
speed = min(speed, self.speed_limit)
#если при текущей скорости происходит столкновение, останавливаемся;
if speed > dist:
speed = 0
#обновляем скорость и позицию;
driver.speed = speed
driver.loc += speed
dist  – ​расстояние между driver и  следующим водителем впереди. Это расстояние передается
в choose_acceleration, которое определяет поведение водителя. Это единственное решение, кото-
рое может принять водитель; все остальное определяется «физикой» моделирования.
• acc – ​ускорение, которое ограничено min_acc и max_acc. В моей реализации автомобили могут
разгоняться при max_acc=1 и тормозить при min_acc=–10;
• speed – ​это старая скорость плюс запрошенное ускорение, но затем мы вносим некоторые кор-
рективы. Во-первых, мы добавляем случайный шум к  speed, потому что мир не  идеален. eps
определяет величину относительной погрешности; например, если eps равен 0,02, speed умно-
жается на случайный фактор от 0,98 до 1,02;
• скорость ограничена между 0 и speed_limit, что в моей реализации равно 40, поэтому маши-
нам не разрешается двигаться назад или превышать скорость;
• если запрошенная скорость вызовет столкновение со следующей машиной, скорость устанав-
ливается на 0;
• наконец, мы обновляем атрибуты speed и loc водителя.
Вот определение класса Driver:
class Driver:
def __init__(self, loc, speed=0):
self.loc = loc
self.speed = speed
def choose_acceleration(self, dist):

Глава 10. Стаи, стада и пробки   123


return 1
Атрибутами loc и speed являются местоположение и скорость водителя.
Эта реализация choose_acceleration проста: она всегда ускоряется с максимальной скоростью.
Поскольку машины стартуют с  равным интервалом, мы ожидаем, что все они будут разгоняться
до тех пор, пока не достигнут ограничения по скорости или пока их скорость не превысит расстоя­
ние между ними. В этот момент произойдет, по крайней мере, одно «столкновение», в результате
чего какие-то машины остановятся.
На рис. 10–1 показано несколько шагов в этом процессе, начиная с 30 автомобилей и eps = 0,02.
Слева – ​конфигурация после 16 временных шагов с шоссе, отображенным как круг. Из-за случайного
шума некоторые машины едут быстрее, чем другие, и расстояние между ними стало неравномерным.

Рисунок 10-1. Моделирование водителей


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

На следующем временном шаге (в середине) происходят два столкновения, обозначенные треуголь-


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

Случайное возмущение
По мере увеличения количества автомобилей пробки становятся все более серьезными. На рис. 10–2
показана средняя скорость, которую могут достичь автомобили, в зависимости от их количества.
Верхняя строка показывает результаты с eps = 0; то есть без случайного изменения скорости.

124  Изучение сложных систем с помощью Python


При 25 или менее автомобилях расстояние между автомобилями превышает 40, что позволяет авто-
мобилям достигать и поддерживать максимальную скорость, равную 40. При наличии более 25 ав-
томобилей образуются пробки, и средняя скорость быстро падает.
Этот эффект является прямым результатом физики моделирования, поэтому он не должен вас удив-
лять.
Если длина дороги равна 1000, расстояние между n автомобилями составляет 1000/n. А поскольку
автомобили не могут двигаться быстрее, чем пространство перед ними, мы ожидаем, что максималь-
ная средняя скорость будет равна 1000/n или 40, в зависимости от того, что меньше.
Но это лучший вариант развития событий. С небольшой случайностью все становится намного хуже.
На рис. 10–2 также показаны результаты с eps = 0.001 и eps = 0.01, которые соответствуют ошибкам
в скорости 0.1 % и 1 %.

Рисунок 10-2. Средняя скорость как функция


количества автомобилей для трех
величин добавленного случайного
шума

С ошибками 0,1 % пропускная способность шоссе падает с  25 до  20 (под «пропускной способно-
стью» я подразумеваю максимальное количество автомобилей, которые могут достичь и выдержать
ограничение скорости). И с ошибками 1 % пропускная способность падает до 10. Тьфу.
В качестве одного из упражнений, приведенных в конце этой главы, у вас будет возможность раз-
работать лучшего водителя; то  есть вы будете экспериментировать с  различными стратегиями
в choose_acceleration и увидите, сможете ли вы найти поведение водителя, которое улучшит сред-
нюю скорость.

Boid
В 1987 году Крейг Рейнолдс (Craig Reynolds) опубликовал статью «Стаи, стада и косяки: распреде-
ленная модель поведения», в которой описывается агент-ориентированная модель поведения стада.
Вы можете скачать его статью на странице https://thinkcomplex.com/boid.

Глава 10. Стаи, стада и пробки   125


Агенты в этой модели называются «Boid». Это слово образовано в результате сочетания «bird-oid»
(птичий жир) и акцентированного произношения слова bird (птица) (хотя Boid'ы также используют-
ся для моделирования рыб и стадных животных).
Каждый агент имитирует три поведения.

Центрирование стаи
Двигаться к центру стаи.

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

Соответствие скорости
Совмещать скорость (скорость и направление) с соседними агентами.
Boid'ы принимают решения, основываясь только на  локальной информации; каждый Boid видит
(или обращает внимание на) других агентов в поле своего зрения.
В репозитории для этой книги вы найдете Boids7.py, который содержит мою реализацию Boid'ов,
частично основанную на описании, приведенном в книге Гари Уильяма Флейка (Gary William Flake)
«Вычислительная красота природы».
Моя реализация использует VPython, библиотеку с трехмерной графикой.
VPython предоставляет объект Vector, который я использую для представления положения и ско-
рости Boid'ов в трех измерениях. Вы можете прочитать о них на странице https://thinkcomplex.com/
vector.

Алгоритм Boids
Boids7.py определяет два класса: Boid, который реализует поведение Boid'ов, и World, который со-
держит список Boid'ов и «морковку» (приманку), которая их притягивает.
Класс Boid определяет следующие методы.
center
Находит других Boid'ов в  пределах досягаемости и  вычисляет вектор по  направлению к  их
центроиду.
avoid
Находит объекты, в  том числе других Boid'ов, в  пределах заданного диапазона и  вычисляет
вектор, который указывает в сторону от их центроида.
align
Находит других Boid'ов в пределах досягаемости и вычисляет среднее значение их заголовков.
love
Вычисляет вектор, который указывает на приманку.
Ниже приводится реализация метода center:
def center(self, boids, radius=1, angle=1):
neighbors = self.get_neighbors(boids, radius, angle)
vecs = [boid.pos for boid in neighbors]
return self.vector_toward_center(vecs)
Параметры radius и angle – ​это радиус и угол поля зрения, который определяет, какие еще Boid'ы
принимаются во внимание. radius выражается в произвольных единицах длины; angle в радианах.

126  Изучение сложных систем с помощью Python


Center использует get_neighbors, чтобы получить список объектов Boid, которые находятся в поле
зрения.
vecs – ​это список объектов Vector, который обозначает их позиции.
Наконец, vector_toward_center вычисляет вектор, который указывает от  self на  центроид
neighbors.
Вот как работает get_neighbors:
def get_neighbors(self, boids, radius, angle):
neighbors = []
for boid in boids:
if boid is self:
continue
#при отсутствии в пределах досягаемости, пропускаем;
offset = boid.pos – ​self.pos
if offset.mag > radius:
continue
# при отсутствии в пределах угла обзора, пропускаем;
if self.vel.diff_angle(offset) > angle:
continue
#в противном случае добавляем в список;
neighbors.append(boid)
return neighbors
Для каждого другого Boid'а get_neighbors использует векторное вычитание для вычисления векто-
ра от self до boid. Величина этого вектора – ​расстояние между ними; если эта величина превышает
radius, мы игнорируем boid.
diff_angle вычисляет угол между скоростью self, которая указывает направление, в котором дви-
жется Boid, и положением boid. Если этот угол превышает angle, мы игнорируем boid.
В противном случае boid на виду, поэтому мы добавляем его к neighbors.
Теперь я приведу реализацию vector_toward_center, которая вычисляет вектор от self до центрои­
да его соседей:
def vector_toward_center(self, vecs):
if vecs:
center = np.mean(vecs)
toward = vector(center – ​self.pos)
return limit_vector(toward)
else:
return null_vector
Векторы VPython работают с  NumPy, поэтому np.mean вычисляет среднее значение vecs, которое
представляет собой последовательность векторов. limit_vector ограничивает величину результата
до 1; null_vector имеет величину 0.
Для реализации avoid мы используем те же вспомогательные методы:
def avoid(self, boids, carrot, radius=0.3, angle=np.pi):
objects = boids + [carrot]

Глава 10. Стаи, стада и пробки   127


neighbors = self.get_neighbors(objects, radius, angle)
vecs = [boid.pos for boid in neighbors]
return -self.vector_toward_center(vecs)
avoid напоминает метод center, но учитывает как приманку, так и другие Boid'ы. Кроме того, па-
раметры отличаются: radius меньше, поэтому Boid'ы избегают только слишком близких объектов,
а angle шире, поэтому Boid'ы избегают объектов со всех сторон.
Наконец, результат от vector_toward_center отрицается, поэтому он указывает в сторону от центрои­
да любых объектов, которые находятся слишком близко.
Ниже приводится реализация align:
def align(self, boids, radius=0.5, angle=1):
neighbors = self.get_neighbors(boids, radius, angle)
vecs = [boid.vel for boid in neighbors]
return self.vector_toward_center(vecs)
align также напоминает метод center; большая разница состоит в том, что он вычисляет среднее
значение скоростей соседей, а не их положения. Если соседи указывают в определенном направле-
нии, Boid имеет тенденцию двигаться в этом направлении.
Наконец, love вычисляет вектор, который указывает в направлении приманки:
def love(self, carrot):
toward = carrot.pos – ​self.pos
return limit_vector(toward)
Результаты от center, avoid, align и love – ​это то, что Рейнолдс называет «запросами на ускорение»,
где каждый запрос предназначен для достижения разной цели.

Разрешение конфликтов
Чтобы рассудить эти, возможно, противоречивые цели, мы вычисляем взвешенную сумму четырех
запросов:
def set_goal(self, boids, carrot):
w_avoid = 10
w_center = 3
w_align = 1
w_love = 10
self.goal = (w_center * self.center(boids) +
w_avoid * self.avoid(boids, carrot) +
w_align * self.align(boids) +
w_love * self.love(carrot))
self.goal.mag = 1
w_center, w_avoid и другие весовые коэффициенты определяют важность запросов на ускорение.
Обычно w_avoid является относительно высоким, а w_align – ​относительно низким.
После вычисления цели для каждого Boid'а мы обновляем их скорость и положение:
def move(self, mu=0.1, dt=0.1):
self.vel = (1-mu) * self.vel + mu * self.goal

128  Изучение сложных систем с помощью Python


self.vel.mag = 1
self.pos += dt * self.vel
self.axis = self.length * self.vel
Новая скорость – ​это взвешенная сумма старой скорости и цели. Параметр mu определяет, насколь-
ко быстро птицы могут изменять скорость и направление. Затем мы нормализуем скорость, чтобы ее
величина равнялась 1, и ориентируем ось Boid'а в направлении его движения.
Чтобы обновить позицию, мы умножаем скорость на временной шаг, dt, чтобы получить изменение
в позиции. Наконец, мы обновляем axis таким образом, чтобы ориентация Boid'а при его рисова-
нии соответствовала его скорости.
Многие параметры влияют на поведение стаи, в том числе радиус, угол и вес для каждого поведения,
а также маневренность, mu. Эти параметры определяют способность Boid'ов формировать и  под-
держивать стаю, а также модели движения и организации внутри стаи. По некоторым параметрам
Boid'ы напоминают стаю птиц, по другим – ​косяк рыб или тучу летящих насекомых.
В качестве одного из упражнений в конце этой главы вы можете изменить эти параметры и посмот­
реть, как они влияют на поведение Boid'ов.

Эмерджентность и свобода воли


Многие сложные системы в целом обладают свойствами, которых нет у их компонентов:
• клеточный автомат «Правило 30» является детерминированным, и правила, управляющие его
эволюцией, полностью известны. Тем не менее он генерирует последовательность, которая ста-
тистически неотличима от случайной;
• агенты в  модели Шеллинга не  расисты, но  результатом их взаимодействия является высокая
степень сегрегации;
• агенты в модели Sugarscape образуют волны, которые движутся по диагонали, притом что аген-
ты так двигаться не могут;
• пробки движутся в обратном направлении, даже если автомобили в них движутся вперед.
• стаи и стада ведут себя так, словно они организованы централизованно, хотя животные в них
принимают индивидуальные решения на основе локальной информации.
Эти примеры предлагают подход к ряду старых и сложных вопросов, включая проблемы сознания
и свободы воли.
Свобода воли – ​это способность делать выбор, но если наши тела и мозг управляются детерминиро-
ванными физическими законами, наш выбор полностью определен.
Философы и ученые предлагали множество возможных решений этого очевидного конфликта. На-
пример:
• Уильям Джеймс (William James) предложил двухэтапную модель, в  которой возможные дей-
ствия генерируются случайным процессом, а  затем выбираются детерминированным процес-
сом. В этом случае наши действия в принципе непредсказуемы, потому что процесс, который их
генерирует, включает в себя случайный элемент;
• Дэвид Юм (David Hume) предположил, что наше восприятие выбора – ​иллюзия; в этом случае
наши действия являются детерминированными, потому что система, которая их производит, яв-
ляется детерминированной.
Эти аргументы примиряют конфликт в противоположных направлениях, но они согласны с тем, что
конфликт существует: система не может иметь свободы воли, если ее части детерминированы.

Глава 10. Стаи, стада и пробки   129


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

Упражнения
Код для моделирования пробок находится в  блокноте Jupyter chap10.ipynb в  репозитории для
данной книги. Откройте этот блокнот, прочитайте код и запустите ячейки. Вы можете использовать
этот блокнот для работы над следующими упражнениями. Мои решения находятся в  chap10soln.
ipynb. Более подробная информация о работе с кодом приведена в разделе «Использование кода»
на стр. 10.

Упражнение 10–1
В моделировании пробок определите класс BetterDriver, который наследуется от Driver и пере­
определяет choose_acceleration. Посмотрите, можете ли вы определить правила вождения, кото-
рые лучше базовой реализации в Driver. Вы можете попытаться достичь более высокой средней
скорости или меньшего числа столкновений.

Упражнение 10–2
Код моей реализации Boid'а находится в Boids7.py в репозитории для этой книги. Для его запус­
ка вам понадобится VPython, библиотека трехмерной графики и  анимации. Если вы используете
Anaconda (как я  рекомендую в  разделе «Использование кода» на  стр. 10), то  можете выполнить
следующий код в терминале или окне командной строки:
conda install -c vpython vpython
Затем запустите Boids7.py. Он должен либо запустить браузер, либо создать окно в  работающем
браузере и создать анимированный экран, на котором Boid'ы в виде белых конусов обведены крас-
ной сферой. Это приманка. Если щелкнуть и переместить мышь, вы можете переместить приманку
и посмотреть, как реагируют Boid'ы.
Прочитайте код, чтобы увидеть, как параметры управляют поведениями Boid'ов. Поэксперименти-
руйте с разными параметрами. Что произойдет, если вы «отключите» одно из поведений, установив
его вес на 0?
Чтобы создать поведение, более присущее птицам, Флейк предлагает добавить поведение, чтобы
поддерживать четкую линию обзора; иными словами, если впереди другая птица, Boid должен отой-
ти в сторону. Как вы думаете, какое влияние это правило окажет на поведение стаи? Реализуйте его
и посмотрите.

Упражнение 10–3
Узнайте больше о свободе воли на странице https://thinkcomplex.com/will. Представление о том, что
свобода воли совместима с детерминизмом, называется компатибилизм. Одной из самых серьезных
проблем компатибилизма является «аргумент последствий». Что такое аргумент последствий? Какой
ответ вы можете дать на аргумент последствий, основываясь на том, что вы прочитали в этой книге?

130  Изучение сложных систем с помощью Python


Глава 11.
Эволюция
Наиболее важной идеей в биологии и, возможно, во всей науке является теория эволюции путем
естественного отбора, которая утверждает, что новые виды создаются, а существующие изменяются
в результате естественного отбора. Естественный отбор – ​это процесс, в котором унаследованные
различия между индивидуумами вызывают различия в выживании и размножении.
Среди тех, кто что-то понимает в  биологии, теория эволюции широко рассматривается как факт,
который означает, что она согласуется со всеми современными наблюдениями; это вряд ли будет
противоречить будущим наблюдениям; и если он будет пересмотрен в будущем, перемены почти
наверняка оставят основные идеи практически нетронутыми.
Тем не менее многие не верят в эволюцию. В опросе, проведенном исследовательским центром Пью,
респондентов спросили, какое из следующих утверждений ближе к их точке зрения:
1. Люди и другие живые существа развивались с течением времени.
2. Люди и другие живые существа существовали в их нынешнем виде с незапамятных времен.
Около 34 % американцев выбрали второе (см. https://thinkcomplex.com/arda).
Даже среди тех, кто считает, что живые существа эволюционировали, едва ли более половины счита-
ют, что причиной эволюции является естественный отбор. Другими словами, только треть американ-
цев считают, что теория эволюции верна.
Как такое возможно? На мой взгляд, способствующие факторы включают в себя следующее:
• некоторые люди думают, что существует конфликт между эволюцией и их религиозными убеж­
дениями. Чувствуя, что им нужно отвергнуть что-либо одно, они отвергают эволюцию;
• другие были активно дезинформированы, часто членами первой группы, так что многое из того,
что они знают об эволюции, вводит их в заблуждение или является ложным. Например, многие
думают, что эволюция означает, что люди произошли от обезьян. Это не так, и мы не произошли
от обезьян;
• и многие люди просто ничего не знают об эволюции.
Наверное, я мало что могу сделать с первой группой, но думаю, что могу помочь другим. Эмпириче-
ски теория эволюции трудна для понимания людьми. В то же время она очень проста: для многих
людей, когда они ее понимают, она кажется очевидной и неопровержимой.
Чтобы помочь совершить этот переход от путаницы к ясности, самый мощный инструмент, который
я нашел, – ​это вычисления. Идеи, которые трудно понять в теории, можно легко понять, когда мы
видим, как они происходят при моделировании. Такова цель данной главы.

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

Глава 11. Эволюция   131


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

Варьирование
Нам нужна изменчивость в популяции, то есть различия между индивидуумами.

Дифференциальное выживание или воспроизведение


Различия между индивидуумами должны влиять на их способность выживать или размножаться.
Чтобы смоделировать эти признаки, мы определим популяцию агентов, которые представляют от-
дельные организмы. Каждый агент обладает генетической информацией, своим генотипом, который
копируется при репликации агента. В  нашей модели1 генотип представлен последовательностью
из N двоичных цифр (нулей и единиц), где N – ​параметр, который мы выбираем.
Чтобы сгенерировать вариации, мы создаем популяцию с различными генотипами; позже мы рас-
смотрим механизмы, которые создают или увеличивают вариации.
Наконец, для генерации дифференциального выживания и воспроизведения мы определяем функ-
цию, которая отображается из каждого генотипа в приспособленность, где приспособленность – ​это
величина, связанная со способностью агента выживать или размножаться.

Адаптивный ландшафт
Функция, которая отображается из  генотипа в  приспособленность, называется адаптивным ланд-
шафтом. В метафоре ландшафта каждый генотип соответствует местоположению в N-мерном про-
странстве, а приспособленность соответствует «высоте» ландшафта в этом местоположении. Визуа-
лизации, которые могут прояснить эту метафору, приведены на странице https://thinkcomplex.com/fit.
С биологической точки зрения адаптивный ландшафт представляет информацию о том, как генотип
организма связан с его физической формой и возможностями, называемыми его фенотипом, и как
фенотип взаимодействует с окружающей средой.
В реальном мире адаптивные ландшафты сложны, но нам не нужно создавать реалистичные модели.
Чтобы вызвать эволюцию, нам нужны некоторые отношения между генотипом и приспособленно-
стью, но оказывается, что это могут быть любые отношения. Чтобы продемонстрировать это, мы бу-
дем использовать совершенно случайный адаптивный ландшафт.
Вот определение класса, который представляет адаптивный ландшафт:
class FitnessLandscape:
def __init__(self, N):
self.N = N
self.one_values = np.random.random(N)
self.zero_values = np.random.random(N)
def fitness(self, loc):
fs = np.where(loc, self.one_values,

1
Эта модель является вариантом NK-модели, разработанной преимущественно Стюартом Кауфманом
(см. https://thinkcomplex.com/nk).

132  Изучение сложных систем с помощью Python


self.zero_values)
return fs.mean()
Генотип агента, который соответствует его местоположению в адаптивном ландшафте, представлен
массивом NumPy из нулей и единиц, с именем loc. Приспособленность данного генотипа является
средним значением N адаптивных вкладов, по одному на каждый элемент loc.
Чтобы вычислить приспособленность генотипа, FitnessLandscape использует два массива: one_
values, который содержит адаптивные вклады при наличии 1 в каждом элементе loc, и zero_values,
который содержит адаптивные вклады при наличии 0.
Метод fitness использует np.where для выбора значения из one_values, где у loc – ​1, и значения
из zero_values, где у loc – ​0.
В качестве примера предположим, что N = 3 и
one_values = [0.1, 0.2, 0.3]
zero_values = [0.4, 0.7, 0.9]
В таком случае пригодность loc = [0, 1, 0] будет средним значением [0,4, 0,2, 0,9], что составляет 0,5.

Агенты
Далее нам нужны агенты. Ниже приводится определение класса:
class Agent:
def __init__(self, loc, fit_land):
self.loc = loc
self.fit_land = fit_land
self.fitness = fit_land.fitness(self.loc)
def copy(self):
return Agent(self.loc, self.fit_land)
Атрибутами Agent являются:
loc
Местоположение Agent в адаптивном ландшафте.
fit_land
Ссылка на объект FitnessLandscape.
fitness
Приспособленность этого агента в FitnessLandscape, обозначенная числом от 0 до 1.
Агент предоставляет копию, которая точно копирует генотип. Позже мы увидим версию, которая
выполняет копирование с помощью мутации, но мутация не является необходимой для эволюции.

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

Глава 11. Эволюция   133


class Simulation:
def __init__(self, fit_land, agents):
self.fit_land = fit_land
self.agents = agents
Атрибуты Simulation:
• fit_land: ссылка на объект FitnessLandscape;
• agents: массив объектов Agent.
Наиболее важной функцией в Simulation является step, которая имитирует один временной шаг:
class Simulation:
def step(self):
n = len(self.agents)
fits = self.get_fitnesses()
#смотрим, кто умирает;
index_dead = self.choose_dead(fits)
num_dead = len(index_dead)
#заменяем умерших копиями живых;
replacements = self.choose_replacements(num_dead, fits)
self.agents[index_dead] = replacements
step использует три других метода:
• get_fitness возвращает массив, содержащий приспособленность каждого агента;
• choose_dead определяет, какие агенты умирают в течение этого временного шага, и возвращает
массив, который содержит индексы мертвых агентов;
• choose_replacements решает, какие агенты будут воспроизводиться в течение этого временно-
го шага, вызывает copy для каждого из них и возвращает массив новых объектов Agent.
В этой версии моделирования число новых агентов на каждом временном шаге равно количеству
мертвых агентов, поэтому число живых агентов постоянно.

Нет дифференциации
Перед тем как выполнить моделирование, мы должны указать поведение choose_dead и  choose_
replacements. Начнем с простых версий этих функций, которые не зависят от приспособленности:
class Simulation
def choose_dead(self, fits):
n = len(self.agents)
is_dead = np.random.random(n) < 0.1
index_dead = np.nonzero(is_dead)[0]
return index_dead
В choose_dead n – ​это число агентов, а is_dead – ​логический массив, содержащий значение True для
агентов, которые умирают в течение этого временного шага. В данной версии каждый агент имеет
одинаковую вероятность смерти: 0,1. choose_dead использует np.nonzero, чтобы найти индексы не-
нулевых элементов is_dead (True считается ненулевым).

134  Изучение сложных систем с помощью Python


class Simulation
def choose_replacements(self, n, fits):
agents = np.random.choice(self.agents, size=n, replace=True)
replacements = [agent.copy() for agent in agents]
return replacements
В choose_replacements n – ​количество агентов, которые воспроизводятся за  этот временной шаг.
Она использует np.random.choice, чтобы выбрать n агентов с помощью замены. Тогда она вызывает
copy для каждого из них и возвращает список новых объектов Agent.
Эти методы не зависят от приспособленности, поэтому данное моделирование не имеет дифферен-
циального выживания или размножения. В результате мы не должны ожидать эволюции. Но откуда
нам знать?

Свидетельство эволюции
Наиболее всеобъемлющее определение эволюции – ​это изменение в  распределении генотипов
в популяции. Эволюция – ​это совокупный эффект: другими словами, развиваются не индивидуумы.
Это делают популяции.
В этой симуляции генотипы – ​это местоположения в  многомерном пространстве, поэтому сложно
визуализировать изменения в их распределении. Однако если генотипы меняются, мы ожидаем, что
их приспособленность также изменится. Поэтому мы будем использовать изменения в распределении
приспособленности как свидетельство эволюции. В частности, мы посмотрим на среднее значение
и стандартное отклонение приспособленности с течением времени.
Прежде чем выполнить моделирование, мы должны добавить Instrument. Это объект, который об-
новляется после каждого временного шага, вычисляет интересующую статистику или «метрику»
и сохраняет результат в последовательности, график которой мы позже можем построить.
Ниже приведен родительский класс для всех инструментов:
class Instrument:
def __init__(self):
self.metrics = []
А вот определение MeanFitness, инструмента, который вычисляет среднюю приспособленность по-
пуляции на каждом временном шаге:
class MeanFitness(Instrument):
def update(self, sim):
mean = np.nanmean(sim.get_fitnesses())
self.metrics.append(mean)
Теперь мы готовы выполнить моделирование. Чтобы избежать эффекта случайных изменений в на-
чальной популяции, мы начинаем каждое моделирование с одним и тем же набором агентов. И что-
бы убедиться, что мы изучаем весь адаптивный ландшафт, мы начинаем с одного агента в каждом
местоположении. Это код, который создает Simulation:
N = 8
fit_land = FitnessLandscape(N)
agents = make_all_agents(fit_land, Agent)
sim = Simulation(fit_land, agents)

Глава 11. Эволюция   135


make_all_agents создает одного агента для каждого местоположения; реализацию можно посмот­
реть в блокноте для этой главы.
Теперь мы можем создать и добавить инструмент MeanFitness, запустить моделирование и постро-
ить график результатов:
instrument = MeanFitness()
sim.add_instrument(instrument)
sim.run()
Simulation ведет список объектов Instrument. После каждого временного шага он вызывает update
каждого инструмента в списке.
На рис. 11–1 показан результат выполнения этого моделирования в течение 10 раз. Средняя при-
способленность популяции перемещается вверх или вниз произвольно. Поскольку распределение
приспособленности меняется со временем, мы делаем вывод, что распределение фенотипов также
меняется. По наиболее инклюзивному определению, это случайное блуждание является своего рода
эволюцией. Но она не особенно интересна.

Рисунок 11-1. Средняя приспособленность для


10 моделирований при отсутствии
дифференциального выживания или
воспроизведения

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

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

Растущее разнообразие
Со временем количество видов на земле в целом увеличилось (несмотря на несколько перио-
дов массового вымирания).

136  Изучение сложных систем с помощью Python


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

Дифференциальное выживание
Давайте добавим еще один ингредиент, дифференциальное выживание. Ниже приведен класс, кото-
рый расширяет Simulation и переопределяет choose_dead:
class SimWithDiffSurvival(Simulation):
def choose_dead(self, fits):
n = len(self.agents)
is_dead = np.random.random(n) > fits
index_dead = np.nonzero(is_dead)[0]
return index_dead
Теперь вероятность выживания зависит от физической формы; на самом деле в этой версии вероят-
ность того, что агент выживет при каждом временном шаге, является его приспособленностью.
Поскольку агенты с низкой приспособленностью с большей вероятностью умирают, агенты с высо-
кой приспособленностью с большей вероятностью выживают достаточно долго, чтобы размножать-
ся. Со  временем мы ожидаем, что количество агентов с  низкой приспособленностью уменьшится,
а количество агентов с высокой приспособленностью увеличится.
На рис. 11–2 показана средняя приспособленность по времени для 10 симуляций с дифференци-
альным выживанием.

Рисунок 11-2. Средняя приспособленность


с течением времени для
10 моделирований при наличии
дифференциального выживания

Средняя приспособленность сначала быстро возрастает, но затем выравнивается.

Глава 11. Эволюция   137


Вероятно, можно выяснить, почему он выравнивается: если в определенном месте находится только
один агент, и он умирает, он оставляет это место незанятым. Без мутации его нельзя занять снова.
Если N = 8, это моделирование начинается с 256 агентов, занимающих все возможные местоположе-
ния. Со временем количество занятых мест уменьшается; если моделирование выполняется доста-
точно долго, в конечном итоге все агенты будут занимать одно и то же место.
Таким образом, эта модуляция начинает объяснять адаптацию: растущая приспособленность озна-
чает, что виду лучше удается выживать в  своей окружающей среде. Но  количество занятых мест
уменьшается с течением времени, поэтому эта модель совсем не объясняет растущее разнообразие.
В блокноте этой главы вы увидите эффект дифференциального воспроизведения.
Как и следовало ожидать, дифференциальное воспроизведение также увеличивает среднюю при-
способленность. Но без мутаций мы все еще не видим растущего разнообразия.

Мутация
До сих пор при моделировании мы начинали с максимально возможного разнобразия – ​по одному
агенту в каждом местоположении ландшафта – ​и заканчивали минимально возможным разнообра-
зием – ​все агенты в одном месте.
Это почти противоположно тому, что произошло в мире природы, который, очевидно, начался с од-
ного вида, который со временем развился до миллионов или, возможно, миллиардов видов на Зем-
ле (см. https://thinkcomplex.com/bio).
При идеальном копировании в нашей модели мы так и не видим растущего разнообразия. Но если
мы добавим мутацию наряду с дифференциальным выживанием и воспроизведением, мы прибли-
зимся на шаг ближе к пониманию эволюции в природе.
Ниже приводится определение класса, которое расширяет Agent и переопределяет copy:
class Mutant(Agent):
def copy(self, prob_mutate=0.05)::
if np.random.random() > prob_mutate:
loc = self.loc.copy()
else:
direction = np.random.randint(self.fit_land.N)
loc = self.mutate(direction)
return Mutant(loc, self.fit_land)
В этой модели мутации каждый раз, когда мы называем copy, существует 5 %-ный шанс мутации.
В случае мутации мы выбираем случайное направление из текущего местоположения, то есть слу-
чайный бит в генотипе, и отражаем его. Вот mutate:
def mutate(self, direction):
new_loc = self.loc.copy()
new_loc[direction] ^= 1
return new_loc
Оператор ^= вычисляет «исключающее ИЛИ»; с операндом 1 он имеет эффект легкого отражения
(см. https://thinkcomplex.com/xor).
Теперь, когда у нас есть мутация, нам не нужно начинать с агента в каждом месте. Вместо этого мы
можем начать с минимальной изменчивости: все агенты находятся в одном месте.

138  Изучение сложных систем с помощью Python


На рис.  11–3 показаны результаты 10 симуляций с  мутацией и  дифференциальным выживанием
и  воспроизведением. В  каждом случае популяция развивается в  направлении местоположения
с максимальной приспособленностью.

Рисунок 11-3. Средняя приспособленность с течением


времени для 10 моделирований при
наличии мутации и дифференциального
выживания и воспроизведения

Чтобы измерить разнообразие популяции, мы можем построить график количества занятых мест
после каждого временного шага. На рис. 11–4 показаны результаты. Мы начинаем со 100 агентов
в одном месте. По мере возникновения мутаций количество занятых мест быстро увеличивается.

Рисунок 11-4. Количество занятых


местоположений с течением времени
для 10 моделирований при наличии
мутации и дифференциального
выживания и воспроизведения

Глава 11. Эволюция   139


Когда агент обнаруживает местоположение с повышенной приспособленностью, он с большей веро-
ятностью выживает и размножается. Агенты в местах с низким уровнем приспособленности в конеч-
ном итоге вымирают. Со временем население мигрирует по ландшафту, пока большинство агентов
не окажется вместе с самой высокой приспособленностью.
В этот момент система достигает равновесия, когда мутация занимает новые местоположения
с той же скоростью, с какой дифференциальное выживание приводит к тому, что местоположения
с более низкой приспособленностью остаются пустыми.
Количество занятых мест в равновесии зависит от частоты мутаций и степени дифференциально-
го выживания. В этих симуляциях количество уникальных занятых местоположений в любой точке
обычно составляет 5–15.
Важно помнить, что агенты в этой модели не двигаются, так же, как не изменяется генотип организ-
ма. Когда агент умирает, он может оставить место незанятым. И когда происходит мутация, она может
занимать новое место. Когда агенты исчезают из некоторых мест и появляются в других, население
мигрирует через ландшафт, как планер в игре «Жизнь». Но организмы не развиваются; это делают
популяции.

Видообразование
Теория эволюции гласит, что естественный отбор изменяет существующие виды и  создает новые.
В нашей модели мы видели изменения, но мы не видели новый вид. Исходя из модели, даже не ясно,
как он будет выглядеть.
Среди видов, которые размножаются половым путем, два организма считаются одним и тем же ви-
дом, если они могут размножаться и производить плодовитое потомство. Но агенты в модели не вос-
производятся половым путем, поэтому это определение не применимо.
Среди организмов, которые размножаются бесполым путем, таких как бактерии, определение видов
не так однозначно. Как правило, популяция считается видом, если их генотипы образуют кластер,
то есть если генетические различия внутри популяции незначительны по сравнению с различиями
между популяциями.
Прежде чем мы сможем моделировать новые виды, нам нужна способность идентифицировать груп-
пы агентов в ландшафте. Это означает, что нам нужно определение расстояния между местоположе-
ниями. Поскольку местоположения представлены массивами битов, мы определим расстояние как
количество битов, которые различаются в разных местоположениях. FitnessLandscape предостав-
ляет метод distance:
class FitnessLandscape
def distance(self, loc1, loc2):
return np.sum(np.logical_xor(loc1, loc2))
Функция logic_xor вычисляет «исключающее ИЛИ», которое истинно для разных битов и ложно для
битов, которые одинаковы.
Чтобы количественно оценить дисперсию популяции, мы можем вычислить среднее расстояние
между парами агентов. В блокноте этой главы вы увидите инструмент MeanDistance, который вычис-
ляет этот показатель после каждого временного шага.
На рис.  11–5 показано среднее расстояние между агентами с течением времени. Поскольку мы
начинаем с идентичных мутантов, начальные расстояния равны 0. По мере возникновения мутаций
среднее расстояние увеличивается, достигая максимума, пока популяция мигрирует через ланд-
шафт.

140  Изучение сложных систем с помощью Python


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

Рисунок 11-5. Среднее расстояние между агентами


с течением времени

Теперь мы готовы приступить к поиску новых видов. Чтобы смоделировать простой тип видообра-
зования, предположим, что популяция эволюционирует в неизменной среде до тех пор, пока не до-
стигнет устойчивого состояния (подобно некоторым видам, которых мы встречаем в природе, кото-
рые, кажется, менялись очень мало в течение длительных периодов времени).
Теперь предположим, что мы либо меняем окружающую среду, либо переносим популяцию в новую
среду. Некоторые признаки, которые повышают приспособленность в старой среде, могут снизить ее
в новой среде, и наоборот.
Мы можем смоделировать эти сценарии, запустив моделирование, пока популяция не  достигнет
устойчивого состояния, затем изменив адаптивный ландшафт и  возобновив моделирование, пока
популяция снова не достигнет устойчивого состояния.
На рис. 11–6 показаны результаты подобного моделирования. Мы начинаем со 100 одинаковых
мутантов в случайном месте и запускаем моделирование для 500 временных шагов. В этот момент
многие агенты находятся в оптимальном месте, которое в данном примере имеет приспособлен-
ность около 0,65. Генотипы агентов образуют кластер со  средним расстоянием между агентами
около 1.

Глава 11. Эволюция   141


Рисунок 11-5. Средняя приспособленность
с течением времени. После
500 временных шагов мы меняем
адаптивный ландшафт

После 500 шагов мы запускаем FitnessLandscape.set_values, который меняет адаптивный ланд-


шафт; затем возобновляем моделирование. Средняя приспособленность ниже, чем мы ожидаем,
потому что оптимальное местоположение в старом ландшафте не лучше, чем случайное местополо-
жение в новом.
После изменения средняя приспособленность снова увеличивается, поскольку популяция мигрирует
через новый ландшафт, в конечном счете находя новый оптимум, приспособленность которого со-
ставляет около 0,75 (что в данном примере выше, но не обязательно).
Как только популяция достигает устойчивого состояния, она снова формирует новый кластер
со средним расстоянием между агентами около 1.
Теперь, если мы вычислим расстояние между местоположениями агентов до  и  после изменения,
они в  среднем различаются более чем на  6. Расстояния между кластерами намного больше, чем
расстояния между агентами в каждом кластере, поэтому мы можем интерпретировать эти кластеры
как отдельные виды.

Резюме
Мы видели, что мутации наряду с дифференциальным выживанием и воспроизведением достаточ-
но вызвать рост приспособленности, разнообразия и простую форму видообразования. Эта модель
не должна быть реалистичной; эволюция в природных системах гораздо более сложнее. Скорее, это
«теорема достаточности», то есть демонстрация того, что возможностей модели достаточно, чтобы
создать поведение, которое мы пытаемся объяснить (см. https://thinkcomplex.com/suff).
Логично, что эта «теорема» не доказывает, что эволюция в природе обусловлена ​​только этими ме-
ханизмами. Но  поскольку эти механизмы все  же появляются во  многих формах в  биологических
системах, разумно полагать, что они, по крайней мере, способствуют естественной эволюции.
Кроме того, эта модель не доказывает, что эти механизмы всегда вызывают эволюцию. Но резуль-
таты, которые мы здесь видим, оказываются надежными: почти в любой модели, которая включает
в себя эти признаки – ​несовершенные репликаторы, изменчивость и дифференциальное воспроиз-
ведение, – п
​ роисходит эволюция.

142  Изучение сложных систем с помощью Python


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

Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap11.ipynb в репозитории для этой
книги. Откройте этот блокнот, прочитайте код и запустите ячейки. Вы можете использовать этот блок-
нот для работы над следующими упражнениями. Мои решения находятся в chap11soln.ipynb. Более
подробная информация о работе с кодом приведена в разделе «Использование кода» на стр. 10.

Упражнение 11–1
В блокноте показаны эффекты дифференциального воспроизведения и  выживания по  отдель-
ности. Что делать, если у  вас есть и  то, и  другое? Напишите класс с  именем SimWithBoth, кото-
рый использует версию choose_dead из  SimWithDiffSurvival и  версию choose_replacements
из SimWithDiffReproduction. Значит ли это, что приспособленность растет быстрее?
Рассматривая это как задачу по Python, можете ли вы написать этот класс, не прибегая к копирова-
нию кода?

Упражнение 11–2
Когда мы меняем ландшафт, как в разделе «Видообразование» на стр. 140, количество занятых мест
и  среднее расстояние обычно увеличиваются, но  эффект не  всегда достаточно велик, чтобы быть
очевидным. Попробуйте несколько случайных семян, чтобы увидеть, каков охват данного эффекта.

Глава 11. Эволюция   143


Глава 12.
Эволюция кооперации
В этой последней главе я беру два вопроса: один из биологии, а другой из философии:
• в биологии «проблема альтруизма» – ​это очевидный конфликт между естественным отбором,
который предполагает, что животные живут в состоянии постоянной конкуренции, и альтруиз-
мом, который состоит в особенности многих животных помогать другим животным даже в ущерб
себе. См. https://thinkcomplex.com/altruism;
• в моральной философии вопрос о человеческой природе спрашивает, являются ли люди в ос-
нове своей добром, или злом, или пустыми состояниями, сформированными их окружающей
средой. См. https://thinkcomplex.com/nature.
Инструменты, которые я использую для решения этих вопросов, – ​это агент-ориентированное моде-
лирование (снова) и теория игр, представляющая собой набор абстрактных моделей, предназначен-
ных для описания способов взаимодействия агентов.
В частности, игра, которую мы рассмотрим, – ​это дилемма заключенного.

Дилемма заключенного
Дилемма заключенного – ​проблема в теории игр, а не забавная игра. Напротив, она проливает свет
на человеческую мотивацию и поведение. Вот статья, посвященная дилемме на Википедии (https://
thinkcomplex.com/pd):
Два члена преступной группировки арестованы и заключены в тюрьму. Каждый заключенный
находится в  одиночном заключении, не  имея возможности общаться с  другим. У  прокуроров
нет достаточных доказательств, чтобы осудить пару по  основному обвинению, но  у  них есть
достаточно улик, чтобы осудить обоих по легкой статье. Одновременно прокуроры предлагают
каждому из заключенных сделку. Им предоставляется возможность: (1) предать другого, свиде-
тельствуя против него, или (2) сотрудничать с другим, храня молчание. Варианты:
• если А и В предают друг друга, каждый из них отбывает по 2 года тюрьмы;
• если A выдает B, но B хранит молчание, A будет освобожден, а B будет отбывать 3 года тюрь-
мы (и наоборот);
• если A и  B оба хранят молчание, оба они будут отбывать только 1  год лишения свободы
(по меньшей мере).
Очевидно, что этот сценарий вымышленный, но он предназначен для представления разнообраз-
ных взаимодействий, когда агентам приходится выбирать, «сотрудничать» ли им друг с другом или
«переметнуться», и  где вознаграждение (или наказание) для каждого агента зависит от  того, что
выбирает другой.
При таком наборе наказаний очень хотелось бы сказать, что игроки должны сотрудничать, то есть
оба должны молчать. Но ни один агент не знает, что будет делать другой, поэтому каждый должен
рассмотреть два возможных результата. Сначала посмотрим на это с точки зрения А:
• если B хранит молчание, A лучше будет переметнуться; он лучше выйдет на свободу, чем отсидит
год;

144  Изучение сложных систем с помощью Python


• если B предаст, для A еще лучше будет сделать то же самое; он будет сидеть только 2 года, а не 3.
Не важно, что делает B, для А лучше будет совершить предательство. И поскольку игра симметрична,
этот анализ такой же и с точки зрения B: независимо от того, что делает A, B лучше будет перемет-
нуться.
В простейшей версии этой игры мы предполагаем, что у A и  B нет других соображений, которые
следует принимать во внимание. Они не могут общаться друг с другом, поэтому не могут вести пере-
говоры, давать обещания или угрожать друг другу. И они рассматривают только непосредственную
цель минимизации своих предложений, не учитывая других факторов.
Согласно этим предположениям, рациональный выбор для обоих агентов заключается в предатель-
стве. Это может быть хорошо, по крайней мере для правосудия. Однако заключенных это удручает,
потому что, по-видимому, они ничего не могут сделать для достижения результата, который им обо-
им нужен. И эта модель применима к другим сценариям в реальной жизни, где сотрудничество будет
лучше как для всеобщего блага, так и для игроков.
Изучение этих сценариев и  способов выхода из  данной дилеммы является предметом внимания
людей, изучающих теорию игр, но не является предметом рассмотрения данной главы. Мы движемся
в другом направлении.

Проблема альтруизма
С тех пор, как дилемма заключенного впервые обсуждалась в 50-х годах, она стала популярной те-
мой изучения в социальной психологии. Основываясь на анализе, приведенном в предыдущем раз-
деле, мы можем сказать, что должен делать совершенно рациональный агент. Труднее предсказать,
что на самом деле делают настоящие люди. К счастью, такой эксперимент был проведен1.
Если мы предположим, что люди достаточно умны, чтобы провести анализ (или понять его, когда
объяснят), и что они, как правило, действуют в своих собственных интересах, мы ожидаем, что они
будут практически постоянно предавать. Но  они этого не  делают. В  большинстве экспериментов
субъекты взаимодействуют гораздо чаще, чем прогнозирует модель рационального агента2.
Наиболее очевидное объяснение этого результата состоит в том, что люди не являются рациональ-
ными агентами, и  это никого не  должно удивлять. Но  почему нет? Потому, что они недостаточно
умны, чтобы понять сценарий, или потому, что они сознательно действуют вопреки своим интересам?
Основываясь на результатах эксперимента, создается впечатление, что, по крайней мере, отчасти это
можно объяснить простым альтруизмом: многие готовы действовать во вред себе, чтобы принести
пользу другому. Теперь, прежде чем выдвигать этот вывод для публикации в Журнале очевидных ре-
зультатов, давайте продолжим спрашивать, почему:
• почему люди помогают другим людям, даже во вред себе? По крайней мере, одна из причин
в том, что они хотят так поступать; это приносит им удовлетворение от самих себя и от окружа-
ющих;
• и почему хорошее чувство заставляет людей чувствовать себя хорошо? Очень хотелось бы ска-
зать, что они были правильно воспитаны, или общество в целом научило их хотеть поступать

1
Вот недавний отчет со ссылками на предыдущие эксперименты: Barreda-Tarrazona, Jaramillo-Gutiérrez, Pavan
and Sabater-Grande. Individual Characteristics vs. Experience: An Experimental Study on Cooperation in Prisoner's
Dilemma. Frontiers in Psychology, 2017; 8: 596. https://thinkcomplex.com/pdexp.
2
Увидеть прекрасное видео, подводящее итог тому, что мы обсуждали до сих пор, можно на странице https://
thinkcomplex.com/pdvid1.

Глава 12. Эволюция кооперации   145


правильно. Но есть небольшое сомнение относительно того, что какая-то часть этого альтруизма
является врожденной; склонность к альтруизму – ​результат нормального развития мозга;
• ну а почему? Врожденные части развития мозга и последующие личные качества являются ре-
зультатом генетической информации. Конечно, отношения между генами и альтруизмом слож-
ны; вероятно, существует множество генов, которые взаимодействуют друг с другом и с факто-
рами окружающей среды, заставляя людей быть более или менее альтруистичными в разных
обстоятельствах. Тем не менее почти наверняка есть гены, которые делают людей альтруистами;
• и наконец, почему это так? Если при естественном отборе животные находятся в постоянной
конкуренции друг с  другом за  выживание и  размножение, то  очевидно, что альтруизм будет
контрпродуктивным. В популяции, где одни люди помогают другим даже в ущерб себе, а другие
являются чистыми эгоистами, кажется, что эгоисты выигрывают, а альтруисты страдают, и гены
альтруизма будут доведены до вымирания.
Это кажущееся противоречие является «проблемой альтруизма»: почему гены альтруизма не  ис-
чезли? Среди биологов есть много возможных объяснений, включая взаимный альтруизм, половой
отбор, родственный отбор и групповой отбор. Среди людей, не являющихся учеными, объяснений
еще больше. Я оставляю это вам, чтобы изучить альтернативы; сейчас я хочу сосредоточиться только
на одном объяснении, пожалуй, самом простом: возможно, альтруизм адаптивен. Другими словами,
возможно, гены альтруизма повышают вероятность для выживания и размножения людей. Оказы-
вается, что дилемма заключенного, которая поднимает проблему альтруизма, также может помочь
решить ее.

Чемпионаты по дилемме заключенного


В конце 70-х гг. Роберт Аксельрод (Robert Axelrod), политолог из Университета Мичигана, организо-
вал чемпионат для сравнения стратегий игры в «Дилемму заключенного».
Он пригласил участников представить стратегии в виде компьютерных программ, затем сыграл про-
граммы друг против друга и вел счет. В частности, они сыграли итеративную версию ДЗ, в которой
агенты играют несколько раундов против одного и того же противника, поэтому их решения могут
основываться на истории.
В чемпионатах Аксельрода простая стратегия, которая на удивление хорошо работала, называлась
«Око за око» (Tit for Tat-TFT). TFT всегда сотрудничает во время первого раунда повторного матча;
после этого он копирует все действия оппонента в предыдущем раунде. Если противник продол-
жает сотрудничать, TFT продолжает сотрудничать. Если оппонент совершает предательство в  лю-
бой точке, TFT совершает предательство в  следующем раунде. Но  если противник возвращается
к сотрудничест­ву, TFT делает то же самое.
Для получения дополнительной информации об этих чемпионатах и объяснения причин успеха TFT
см. это видео на сранице: https://thinkcomplex.com/pdvid2.
Изучив стратегии, которые хорошо показали себя в ходе этих чемпионатов, Аксельрод определил
характеристики, которыми они обычно обладали.

Любезные
Стратегии, которые хорошо взаимодействуют в течение первого раунда и, как правило, взаимо-
действуют так же часто, как и предают в последующих раундах.

Мстящие
Стратегии, которые сотрудничают все время, не  сработали как стратегии, которые мстят, если
оппонент совершает предательство.

146  Изучение сложных систем с помощью Python


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

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

Моделирование эволюции кооперации


Эволюция кооперации – ​так называется первая книга, в которой Аксельрод представил результаты
чемпионатов по дилемме заключенного и обсудил последствия проблемы альтруизма. С тех пор он
и другие исследователи изучали эволюционную динамику чемпионатов ДЗ, а именно как распреде-
ление стратегий со временем изменяется в популяции участников ДЗ. В оставшейся части данной
главы я запускаю версию этих экспериментов и показываю результаты.
Во-первых, нам нужен способ кодирования стратегии ДЗ как генотипа. В этом эксперименте я рас-
сматриваю стратегии, в которых выбор агента в каждом раунде зависит только от выбора оппонента
в  предыдущих двух раундах. Я  представляю стратегию, используя словарь, который сопоставляет
предыдущие два варианта выбора оппонента со следующим выбором агента.
Ниже приводится определение класса для этих агентов:
class Agent:
keys = [(None, None),
(None, 'C'),
(None, 'D'),
('C', 'C'),
('C', 'D'),
('D', 'C'),
('D', 'D')]
def __init__(self, values, fitness=np.nan):
self.values = values
self.responses = dict(zip(self.keys, values))
self.fitness = fitness
keys – ​это последовательность ключей в словаре каждого агента, где кортеж ('C', 'C') означает, что
оппонент сотрудничал в предыдущих двух раундах. (None, 'C') означает, что был сыгран только один
раунд и оппонент сотрудничал, а (None, None) означает, что раунды не были сыграны.

Глава 12. Эволюция кооперации   147


В методе __init__ values – ​это последовательность вариантов, 'C' или 'D', которые соответствуют
ключам. Таким образом, если первый элемент значений – ​'C', это означает, что этот агент будет со-
трудничать в первом раунде. Если последним элементом values является 'D', этот агент совершит
предательство, если противник сделал то же в предыдущих двух раундах.
В данной реализации генотипом агента, который всегда предает, является 'DDDDDDD'; генотип агента,
который всегда сотрудничает, – 'CCCCCCC', а генотип TFT – ​'CCDCDCD'.
Класс Agent предоставляет копию, которая создает другого агента с таким же генотипом, но с неко-
торой вероятностью мутации:
def copy(self, prob_mutate=0.05):
if np.random.random() > prob_mutate:
values = self.values
else:
values = self.mutate()
return Agent(values, self.fitness)
Мутация работает путем выбора случайного значения в генотипе и переключения с 'C' на 'D', или
наоборот:
def mutate(self):
values = list(self.values)
index = np.random.choice(len(values))
values[index] = 'C' if values[index] == 'D' else 'D'
return values
Теперь, когда у нас есть агенты, нам нужен чемпионат.

Класс Tournament
Класс Tournament включает в себя детали соревнования по ДЗ:
payoffs = {('C', 'C'): (3, 3),
('C', 'D'): (0, 5),
('D', 'C'): (5, 0),
('D', 'D'): (1, 1)}
num_rounds = 6
def play(self, agent1, agent2):
agent1.reset()
agent2.reset()
for i in range(self.num_rounds):
resp1 = agent1.respond(agent2)
resp2 = agent2.respond(agent1)
pay1, pay2 = self.payoffs[resp1, resp2]
agent1.append(resp1, pay1)
agent2.append(resp2, pay2)
return agent1.score, agent2.score

148  Изучение сложных систем с помощью Python


payoffs – ​это словарь, который отображается из выбора агентов в их вознаграждения. Например,
если оба агента сотрудничают, они получают по 3 очка. Если один предает, а другой сотрудничает,
предатель получает 5, а  кооператор получает 0. Если они оба совершают предательство, каждый
получает 1. Эти вознаграждения (payoffs) Аксельрод использовал в своих чемпионатах.
Метод play запускает несколько раундов игры ДЗ. Он использует следующие методы из  класса
Agent.
reset
Инициализирует агентов перед первым туром, сбрасывает их оценки и историю их ответов.
respond
Запрашивает у каждого агента свой ответ, учитывая предыдущие ответы оппонента.
append
Обновляет каждого агента, сохраняя выбор и суммируя оценки последовательных раундов.
После заданного количества раундов игра возвращает общее количество очков для каждого агента.
Я выбрал num_rounds = 6, чтобы к каждому элементу генотипа обращались примерно с одинако-
вой частотой. Первый элемент доступен только в первом раунде, или одну шестую часть времени.
Следующие два элемента доступны только во втором раунде, или одну двенадцатую времени. К по-
следним четырем элементам обращаются четыре из шести раз, или одну шестую времени в среднем.
Класс Tournament предоставляет второй метод, melee, который определяет, какие агенты конкури-
руют друг с другом:
def melee(self, agents, randomize=True):
if randomize:
agents = np.random.permutation(agents)
n = len(agents)
i_row = np.arange(n)
j_row = (i_row + 1)% n
totals = np.zeros(n)
for i, j in zip(i_row, j_row):
agent1, agent2 = agents[i], agents[j]
score1, score2 = self.play(agent1, agent2)
totals[i] += score1
totals[j] += score2
for i in i_row:
agents[i].fitness = totals[i] / self.num_rounds / 2
melee принимает список агентов и  логическое значение randomize, которое определяет, будет  ли
каждый агент каждый раз сражаться с одними и теми же соседями, или же пары будут рандомизи-
рованы.
i_row и j_row содержат индексы спариваний. totals содержит итоговую оценку каждого агента.
Внутри цикла мы выбираем двух агентов, запускаем play и обновляем totals. В конце мы вычис-
ляем среднее количество очков, которое получил каждый агент, за раунд и на каждого оппонента
и сохраняем результаты в атрибуте fitness каждого агента.

Глава 12. Эволюция кооперации   149


Класс Simulation
Класс Simulation в этой главе основан на классе, описанном в разделе «Моделирование» на стр. 133;
единственные различия заключаются в __init__ и step.
Ниже приводится метод __init__:
class PDSimulation(Simulation):
def __init__(self, tournament, agents):
self.tournament = tournament
self.agents = np.asarray(agents)
self.instruments = []
Объект Simulation содержит объект Tournament, последовательность агентов и последовательность
объектов Instrument (как описано в разделе «Свидетельство эволюции» на стр. 135).
Ниже приводится метод step:
def step(self):
self.tournament.melee(self.agents)
Simulation.step(self)
Эта версия step использует Tournament.melee, который устанавливает атрибут fitness для каждого
агента; затем он вызывает метод step из класса Simulation, воспроизведенный здесь:
class Simulation
def step(self):
n = len(self.agents)
fits = self.get_fitnesses()
#смотрим, кто умирает;
index_dead = self.choose_dead(fits)
num_dead = len(index_dead)
#заменяем умерших копиями живых;
replacements = self.choose_replacements(num_dead, fits)
self.agents[index_dead] = replacements
#обновляем инструменты;
self.update_instruments()
Simulation.step собирает пригодности агентов в массиве; затем он вызывает choose_dead, чтобы
решить, какие агенты умирают, и choose_replacements, чтобы решить, какие агенты воспроизводятся.
Мое моделирование включает в себя дифференциальное выживание, как описано в разделе «Диф-
ференциальное выживание» на стр. 137, но не дифференциальное воспроизведение. Детали можно
посмотреть в тетради для этой главы. В качестве одного из упражнений у вас будет возможность
исследовать эффект дифференциального воспроизведения.

Результаты
Предположим, что мы начинаем с популяции, состоящей из трех агентов: один всегда сотрудничает,
один всегда предает, а другой играет в стратегию TFT. Если мы запустим Tournament.melee с этой
популяцией, сотрудничающий получает 1,5 очка за раунд, агент TFT – ​1,9 очка, а предатель – ​3,33.

150  Изучение сложных систем с помощью Python


Данный результат предполагает, что «всегда предавать» должна быстро стать доминирующей стра-
тегией.
Но стратегия «всегда предавать» содержит в  себе семена самоуничтожения. Если более хорошие
стратегии доведены до исчезновения, предателями некому будет воспользоваться. Их приспособ­
ленность падает, и они становятся уязвимыми для вторжения со стороны сотрудничающих.
Основываясь на  этом анализе, нелегко предсказать, как будет вести себя система: найдет  ли она
устойчивое равновесие или будет колебаться между различными точками в ландшафте генотипа?
Давайте запустим симуляцию и узнаем!
Я начинаю со 100 одинаковых агентов, которые всегда предают, и запускаю симуляцию для 5000
шагов:
tour = Tournament()
agents = make_identical_agents(100, list('DDDDDDD'))
sim = PDSimulation(tour, agents)
На рис. 12–1 показана средняя приспособленность с течением времени (с помощью инструмента
MeanFitness из  раздела «Свидетельство эволюции» на  стр. 135). Первоначально средняя приспо-
собленность равна 1, потому что когда предатели сталкиваются друг с другом, они получают только
1 очко за раунд.

Рисунок 12-1. Средняя приспособленность (очки,


набранные за раунд дилеммы
заключенного)

Приблизительно после 500 временных шагов средняя приспособленность увеличивается почти


до 3, что и получают сотрудничающие, когда они сталкиваются друг с другом. Однако, как мы и по-
дозревали, эта ситуация нестабильна. В течение следующих 500 шагов средняя приспособленность
падает ниже 2, поднимается обратно к 3 и продолжает колебаться.
Остальная часть моделирования сильно варьируется, но, за исключением одного большого падения,
средняя приспособленность обычно составляет от 2 до 3, а долгосрочная средняя близка к 2,5.
И это не плохо! Это не совсем утопия кооперации, которая в среднем составляет 3 очка за раунд,
но она далека от дистопии вечного предательства. И это намного лучше, чем мы могли бы ожидать
от естественного отбора эгоистичных агентов.

Глава 12. Эволюция кооперации   151


Чтобы получить представление об этом уровне приспособленности, рассмотрим еще несколько ин-
струментов. Niceness измеряет долю кооперации в генотипах агентов после каждого временного
шага:
class Niceness(Instrument):
def update(self, sim):
responses = np.array([agent.values
for agent in sim.agents])
metric = np.mean(responses == 'C')
self.metrics.append(metric)
responses – ​это массив с одной строкой для каждого агента и одним столбцом для каждого элемен-
та генома. metric – э​ то доля элементов, которые являются 'C', в среднем по агентам.
На рис. 12–2 (слева) показаны результаты: начиная с 0, средняя любезность (niceness) быстро уве-
личивается до 0,75, затем колеблется между 0,4 и 0,85, с долгосрочным средним значением около
0,65. Опять же, это очень любезно!

Рисунок 12-2. Средняя любезность по всем геномам


в популяции (слева) и доля популяции,
которая сотрудничает в первом
раунде (справа)

Рассматривая конкретно первый шаг, мы можем отслеживать долю агентов, которые сотрудничают
в первом раунде. Вот инструмент:
class Opening(Instrument):
def update(self, sim):
responses = np.array([agent.values[0]
for agent in sim.agents])
metric = np.mean(responses == 'C')
self.metrics.append(metric)

152  Изучение сложных систем с помощью Python


На рис.  12–2 (справа) показаны результаты, которые сильно варьируются. Доля агентов, которые
сотрудничают в первом раунде, часто составляет около 1, а иногда и около 0. Долгосрочное среднее
значение близко к 0,65, что аналогично общей любезности. Эти результаты соответствуют чемпио-
натам Аксельрода; в общем, любезные стратегии – ​молодцы.
Другими характеристиками, которые Аксельрод определяет в успешных стратегиях, являются воз-
мездие и прощение. Чтобы измерить возмездие, я определяю этот инструмент:
class Retaliating(Instrument):
def update(self, sim):
after_d = np.array([agent.values[2::2]
for agent in sim.agents])
after_c = np.array([agent.values[1::2]
for agent in sim.agents])
metric = np.mean(after_d=='D') – ​np.mean(after_c=='D')
self.metrics.append(metric)
Retaliating сравнивает количество элементов во всех геномах, где агент совершает предательство
после предательства оппонента (элементы 2, 4 и 6), с количеством мест, где агент совершает преда-
тельство, после того как оппонент сотрудничает. Как и следовало ожидать, результаты сильно раз-
личаются (график можно увидеть в блокноте). В среднем разница между этими долями составляет
менее 0,1, поэтому если агенты совершают предательство в 30 % случаев, после того как оппонент
сотрудничает, они могут совершать предательства в 40 % случаев после предательства.
Этот результат обеспечивает слабую поддержку утверждению относительно того, что успешные
стратегии дают ответный удар. Но, может быть, не обязательно, чтобы все агенты или даже многие
из них мстили; если есть хоть какая-то тенденция к мести среди популяции в целом, этого может быть
достаточно, чтобы не дать стратегии с высоким уровнем предательства закрепиться.
Чтобы измерить прощение, я определил еще один инструмент, дабы увидеть, могут ли агенты с боль-
шей вероятностью сотрудничать после D-C в предыдущих двух раундах по сравнению с C-D. В моих
симуляциях нет никаких доказательств этого вида прощения. С  другой стороны, стратегии в  этих
симуляциях обязательно прощают, потому что они рассматривают только два предыдущих раунда
истории. В данном контексте забывание является своего рода прощением.

Выводы
Чемпионаты Аксельрода предлагают возможное решение проблемы альтруизма: возможно, быть
любезным, но не слишком любезным – ​это адаптивно. Но стратегии в оригинальных чемпионатах
были разработаны людьми, а не эволюцией, и распределение стратегий не менялось на протяжении
чемпионатов.
Таким образом, возникает вопрос: такие стратегии, как TFT, могли бы преуспеть в фиксированной
популяции разработанных человеком стратегий, но  могут  ли они развиваться? Другими словами,
могут ли они появиться в популяции через мутации, успешно конкурировать со своими предками
и противостоять вторжению своих потомков?
Моделирование в этой главе предполагает:
• популяции предателей уязвимы для вторжения благодаря более удачным стратегиям;
• слишком приятное население уязвимо для вторжения предателей;

Глава 12. Эволюция кооперации   153


• в результате средний уровень любезности колеблется, но средний уровень любезности, как пра-
вило, высок, а средний уровень приспособленности, как правило, ближе к утопии кооперации,
чем к дистопии предательства;
• TFT, которая была успешной стратегией в чемпионатах Аксельрода, не представляется особенно
оптимальной для развивающейся популяции. На самом деле, вероятно, стабильной оптималь-
ной стратегии не существует;
• некоторая степень ответного удара может быть адаптивной, но,  может, и  нет необходимости
в том, чтобы все агенты принимали ответные меры. Если в популяции в целом достаточно мести,
этого может быть достаточно для предотвращения вторжения предателей1.
Очевидно, что агенты в  этих симуляциях просты, и  дилемма заключенного является крайне аб-
страктной моделью ограниченного круга социальных взаимодействий. Тем не  менее результаты,
приведенные в  этой главе, дают некоторое представление о  человеческой природе. Может быть,
наши склонности к кооперации, возмездию и прощению являются врожденными, по крайней мере
частично. Эти характеристики являются результатом того, как устроен наш мозг, что контролируется
нашими генами, по крайней мере частично. И возможно, наши гены строят наш мозг таким образом,
потому что за всю историю человеческой эволюции гены для менее альтруистического мозга были
менее подвержены распространению.
Может быть, поэтому эгоистичные гены создают альтруистический мозг.

Упражнения
Код, приведенный в этой главе, находится в блокноте Jupyter chap12.ipynb в репозитории для этой
книги. Откройте блокнот, прочитайте код и запустите ячейки. Вы можете использовать этот блокнот
для работы над следующими упражнениями. Мои решения находятся в chap12soln.ipynb.
Для получения дополнительной информации о  работе с  этим кодом см.  раздел «Использование
кода» на стр. 10.

Упражнение 12–1
Моделирование в этой главе зависит от условий и параметров, которые я выбрал произвольно. В ка-
честве упражнения я призываю вас изучить другие условия, чтобы увидеть, как они влияют на ре-
зультаты. Вот некоторые предложения.
1. И
 змените начальные условия: вместо того чтобы начинать со всех перебежчиков, посмотрите,
что произойдет, если вы начнете со всех кооператоров, всех TFT или случайных агентов.
2. В
   Tournament.melee я  перетасовываю агентов в  начале каждого временного шага, поэто-
му каждый агент играет против двух случайно выбранных агентов. Что произойдет, если вы
не будете тасовать? В этом случае каждый агент неоднократно играет против одних и тех же
соседей. Это может помочь стратегии меньшинства вторгнуться в  большинство, используя
преимущества населенного пункта.
3. П
 оскольку каждый агент играет только против двух других агентов, результат каждого раун-
да сильно варьируется: агент, который преуспеет против большинства других агентов, может
потерпеть неудачу в любом данном раунде, или наоборот. Что произойдет, если вы увеличите
количество противников, с которыми каждый агент играет в каждом раунде? Или что, если со-
стояние агента в конце каждого шага является средним его текущего счета и его приспособ­
ленности в конце предыдущего раунда?

1
А это вводит совершенно новую проблему в теории игр, проблему фрирайдера (см. https://thinkcomplex.com/rider).

154  Изучение сложных систем с помощью Python


4. Ф
 ункция, которую я выбрал для prob_survival, варьируется от 0,7 до 0,9, поэтому наименее
подходящий агент с p = 0,7 живет в среднем 3,33 временных шага, а наиболее подходящий
агент – ​10 временных шагов. Что произойдет, если вы сделаете степень дифференциального
выживания более или менее «агрессивной»?
5. Я
  выбрал num_rounds = 6, чтобы каждый элемент генома оказывал примерно одинаковое
влияние на  результат матча. Но  это существенно короче того, что Аксельрод использовал
в своих чемпионатах. Что произойдет, если вы увеличите num_rounds?
Примечание: если вы исследуете влияние этого параметра, вы можете изменить Niceness,
чтобы измерить любезность последних четырёх элементов генома, которые будут подвергать-
ся более избирательному давлению при увеличении num_rounds.
6. В моей реализации есть дифференциальное выживание, но нет дифференциального воспро-
изведения. Что произойдет, если вы добавите дифференциальное воспроизведение?

Упражнение 12–2
В моем моделировании популяция никогда не сходится к состоянию, в котором большинство име-
ет один и тот же, предположительно оптимальный, генотип. Есть два возможных объяснения этого
результата: во‑первых, нет оптимальной стратегии, потому что всякий раз, когда в популяции преоб-
ладает генотип большинства, это условие создает возможность для вторжения меньшинства; другая
возможность заключается в том, что частота мутаций достаточно высока, чтобы поддерживать раз-
нообразие генотипов.
Чтобы различать эти объяснения, попробуйте снизить частоту мутаций, чтобы увидеть, что проис-
ходит. Также начните со случайной популяции и выполните запуск без мутации, пока не выживет
только один генотип. Или выполните запуск с мутацией, пока система не достигнет чего-то вроде
устойчивого состояния; затем выключите мутацию и запускайте, пока не останется только один вы-
живший генотип. Каковы особенности генотипов, которые преобладают в этих условиях?

Упражнение 12–3
Агенты в  моем эксперименте «реактивны» в том смысле, что их выбор в  каждом раунде зависит
только от того, что делал оппонент в предыдущих раундах. Изучите стратегии, которые также учи-
тывают прошлый выбор агента. Эти стратегии могут отличать оппонента, который наносит ответный
удар, от оппонента, который совершает предательство без провокации.

Глава 12. Эволюция кооперации   155


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

• Axelrod Robert. Complexity of Cooperation. Princeton University Press, 1997.


• Axelrod Robert. The Evolution of Cooperation. Basic Books, 2006.
• Bak Per. How Nature Works. Copernicus (Springer), 1996.
• Barabasi Albert-Laszlo. Linked. Perseus Books Group, 2002.
• Buchanan Mark. Nexus. W. W. Norton & Company, 2002.
• Dawkins Richard. The Selfish Gene. Oxford University Press, 2016.
• Epstein Joshua and Axtell Robert. Growing Artificial Societies. Brookings Institution Press & MIT
Press, 1996.
• Fisher Len. The Perfect Swarm. Basic Books, 2009.
• Flake Gary William. The Computational Beauty of Nature. MIT Press, 2000.
• Goldstein Rebecca. Incompleteness. W. W. Norton & Company, 2005.
• Goodwin Brian. How the Leopard Changed Its Spots. Princeton University Press, 2001.
• Holland John. Hidden Order. Basic Books, 1995.
• Johnson Steven. Emergence. Scribner, 2001.
• Kelly Kevin. Out of Control. Basic Books, 2002.
• Kosko Bart. Fuzzy Thinking. Hyperion, 1993.
• Levy Steven. Artificial Life. Pantheon, 1992.
• Mandelbrot Benoit. Fractal Geometry of Nature. Times Books, 1982.
• McGrayne Sharon Bertsch. The Theory That Would Not Die. Yale University Press, 2011.
• Mitchell Melanie. Complexity: A Guided Tour. Oxford University Press, 2009.
• Waldrop M. Mitchell. Complexity: The Emerging Science at the Edge of Order and Chaos. Simon &
Schuster, 1992.
• Resnick Mitchell. Turtles, Termites and Traffic Jams. Bradford, 1997.
• Rucker Rudy. The Lifebox, the Seashell, and the Soul. Thunder's Mouth Press, 2005.
• Sawyer R. Keith. Social Emergence: Societies as Complex Systems. Cambridge University Press, 2005.
• Schelling Thomas. Micromotives and Macrobehaviors. W. W. Norton & Company, 2006.
• Strogatz Steven. Sync. Hachette Books, 2003.
• Watts Duncan. Six Degrees. W. W. Norton & Company, 2003.
• Wolfram Stephen. A New Kind Of Science. Wolfram Media, 2002.

156  Изучение сложных систем с помощью Python


Указатель

A S
Anaconda 10, 130 Sugarscape 114, 116, 117, 118, 119, 120, 121, 129

B T
Boid 126, 127, 128, 129, 130 thinkstats2 98
Turmite 81
C
Cell1D 68, 71, 72, 92 V
Cell1DViewer 68 VPython 126, 127, 130
Cell2D 80, 93, 95, 111, 118
correlate2d 79, 87, 96, 111, 112 A
Агент 110, 115, 122, 133
D Агент-ориентированные модели 110
dtype 47 Адаптивный ландшафт 132
Аксельрод 146, 147, 149, 153, 155
G Акстелл 114, 119
Алгоритм Дейкстры 43, 45
Git 10
Анализ 18
GitHub 10, 11, 12, 44
Анализ алгоритмов 32
Аналогия 106
J Аппель 17
Jupyter 8, 9, 10, 31, 71, 80, 108, 117, 121, 130,
 143, 154 Б
Байесовская вероятность 19
L Бак 92, 95, 103, 105, 106, 107, 108, 109
LifeViewer 80 Бишоп 121

N В
NetworkX 9, 21, 23, 34, 38, 41, 44, 45, 47, 51, Ваттс 34, 36, 39, 40, 46, 60
 53, 57 Вичита 33
np.correlate2d 82 Вольфрам 59, 60, 62, 65, 66, 120
NumPy 9, 28, 29, 36, 38, 39, 47, 68, 69, 70, 71, 78, Временной шаг 60, 88, 115, 116, 134
 80, 83, 98, 102, 105, 111, 118, 127, 133 Вычисления 15, 33, 34, 35, 39, 41, 45, 47, 54, 59,
 66, 69, 74, 79, 104, 112, 120, 128, 131
P Вычислительная красота природы 126
Вычислительные модели 16
Python 8, 9, 10, 13, 21, 23, 30, 42, 59, 68, 69, 72, 73,
143
Г
R Генератор псевдослучайных чисел 62
Генотип 133
r-пентамино 75, 76 Голдштейн 19

Указатель  157
Госпер 76 Модель Барабаши–Альберта 51, 57
Граф Ваттса–Строгаца 36 Модель Шеллинга 110
Графы 21, 22, 46, 51 Музей Гуггенхейма 18
Муравей Лэнгтона 81
Д Мутация 138, 148
Движение планет 14
Двусвязная очередь 42 О
Дейкстра 43 Око за око 146
Дек 42
Детерминизм 16 П
Детройт 14
Паровоз 76
Джеймс 129
Поведение 59, 64
Дилемма заключенного 144, 145, 146, 154
Поиск в ширину 41
Дифференциальное выживание 137, 150
Полная по Тюрингу 76
Докинз 106
Поппер 72
Порядок роста 31, 32, 45
З Правило 18 92
Закон идеального газа 106 Правило 30 61, 72, 129
Землетрясения 107 Правило 110 72
Приспособленность 133
И Проблема демаркации 72
Игра «Жизнь» 73
Изоляция 18 Р
Инженерия 18 Радиоактивный распад 62
Инструментализм 17, 77 Расизм 111
Исследовательский центр Пью 131 Распределение 49, 51, 52, 54, 55, 58, 94, 106,
 107, 116, 117, 136, 147, 153
К Распределение степеней 49, 51, 52, 54, 58
Распределения
Класс 3 66
с тяжелыми хвостами 50, 94, 99, 104, 109
Клеточный автомат 59, 129
Расстояние 21, 33, 43, 44, 47, 123, 124, 125,
Кольцевая решетка 34, 35
 127, 140, 141, 142, 143
Конвей 76
Регулярный граф 44
Коско 19
Редукционизм 17, 19, 106
Космический корабль 72, 75, 76, 80
Резник 122
Критическое значение 25, 29, 88, 89, 95
Рейнолдс 125, 128
Кун 57
Реньи 21, 24, 25, 27, 28, 29, 32, 36, 86
Розовый шум 94, 103, 104, 105, 107
Л
Лавина 107 С
Свобода воли 129
М Случайное блуждание 88, 136
МакГрэйн 19 Случайный граф 24, 25
Мандельброт 109 Степень 14, 48, 98
Машина Тьюринга 65 Строгац 15, 16, 34, 36, 39, 40, 46, 60
Метод Уэлча 105
Милгрэм 33 Т
Моделирование 110, 150, 153, 154
Тезис Черча–Тьюринга 65, 66, 67
Модель 14, 17, 46, 52, 54, 55, 57, 82, 95, 106, 117
Теорема Гёделя о неполноте 19

158  Изучение сложных систем с помощью Python


Теория эволюции 131 Холистические модели 106
Тьюринг 65, 82
Ч
У Частота 84
Усердный бобёр 72 Шеллинг 15, 110

Ф Э
Фальсифицируемость 66, 72 Эволюция 131, 135, 147
Флейк 130 Эволюция кооперации 147
Фракталы 89 Эгоистичный ген 106
Фрактальная геометрия 94, 109 Эйфель 18
Фрактальная геометрия природы 109 Экономика 8
Функция вероятности 49, 51, 55 Эмерджентность 120
Функция генератора 25, 27, 35 Эпштейн 114, 119
Функция распределения 54, 55 Эрдёш 24, 25

Х Я
Хакен 17 Ядро 112
Холизм 17 Ячейка 92, 93

Указатель  159
Книги издательства «ДМК Пресс» можно заказать
в торгово-издательском холдинге «Планета Альянс» наложенным платежом,
выслав открытку или письмо по почтовому адресу:
115487, г. Москва, 2-й Нагатинский пр-д, д. 6А.
При оформлении заказа следует указать адрес (полностью),
по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине: www.a-planeta.ru.
Оптовые закупки: тел. (499) 782-38-89.
Электронный адрес: books@alians-kniga.ru.

Аллен Б. Дауни

Изучение сложных систем с помощью Python


Главный редактор Мовчан Д. А.
dmkpress@gmail.com
Перевод Беликов Д. А.
Корректор Синяева Г. И.
Верстка Арифулин Г. Р.
Дизайн обложки Мовчан А. Г.

Формат 70×100 1/16.


Гарнитура «PT Sans». Печать цифровая.
Усл. печ. л. 13,16. Тираж 200 экз.

Веб-сайт издательства: www.dmkpress.com