Алгоритм коллапса волновой функции (Wavefunction Collapse Algorithm) учит компьютер импровизировать. На входе он получает архетипичные данные и создаёт процедурно генерируемые данные, похожие на исходные.


(Источник)

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


(Источник)

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

Большинство реализаций и объяснений коллапса волновой функции — это полная, оптимизированная по скорости версия алгоритма. Разумеется, все они важны и необходимы, но в них сложно разобраться с нуля. В этом посте я буду объяснять всё понятным я простым языком, сосредоточившись на версии Wavefunction с ограничениями, которую я назвал Even Simpler Tiled Model. Кроме того, я выложил пример реализации ESTM на Github. Код в нём неэффективный и медленный, но очень хорошо читаемый и подробно прокомментирован. Как только вы разберётесь в технологии, лежащей в основе ESTM, то станете ближе к пониманию более сложных версий алгоритма. Если хотите понять алгоритм коллапса волновой функции, то эта статья будет хорошим началом.

Давайте начнём с истории.

Свадьба


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

Вы начинаете с длинного списка правил и пустого плана рассаживания гостей.


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


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

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


Этот выбор имеет последствия, распространяющиеся на волновые функции остальных стульев. Если дядя Рой будет сидеть за столом 2, то кузен Фрэнк и Мишель Обама (друг семьи вашего партнёра) точно не будут рядом с ним. А если Мишель не сядет за стол 2, то Барака за ним тоже не будет. Мы обновляем волновую функцию плана расположения, вычёркивая людей из списков возможных кандидатов.


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

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

Если вы достигли противоречия, то проще всего будет начать сначала. Отбросить всю предыдущую работу, найти новый пустой план и запустить алгоритм заново, выполнив коллапс волновой функции для другого случайного стула. Можно также реализовать систему возврата назад, позволяющую отменять отдельный выбор, а не отказываться сразу от всего («что если пересадить Шейлу на стул 54?»).

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

От свадьбы к битовым картам


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

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

Давайте начнём исследование реального коллапса волновой функции с рассмотрения простого особого случая, который ExUtumno (создатель алгоритма) называет простой тайловой моделью (Simple Tiled Model).

Simple Tiled Model


В модели Simple Tiled Model входящие и выходящие изображения строятся из небольшого количества заранее определённых тайлов, и каждый квадрат в выходящем изображении ограничивается только его четырьмя ближайшими соседями. Например, предположим, что мы генерируем случайные миры для двухмерной игры с видом сверху. У нас могут быть тайлы для суши, побережья и моря, а также набор правил вида «побережье может находиться рядом с морем», «суша может быть рядом с побережьем» и «море может быть рядом с другим морем».


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


Эта обработка симметрии обеспечивает более качественные выходные изображения, но усложняет код. Чтобы не усложнять, давайте рассмотрим ещё более простой вид коллапса волновой функции, который я назвал Even Simpler Tiled Model.

Even Simpler Tiled Model


Even Simpler Tiled Model («ещё более простая тайловая модель») похожа на Simple Tiled Model, но её тайлы не имеют свойств симметрии. Каждый тайл — это один пиксель одного цвета, то есть мы никак не сможем перепутать их края.


Правила Even Simpler Tiled Model определяют, какие тайлы можно размещать рядом друг с другом и в какой ориентации. Каждое правило представляет собой кортеж из трёх элементов (3-tuple): двух тайлов и направления. Например, (SEA, COAST, LEFT) означает, что тайл SEA (море) может размещаться СЛЕВА от тайла COAST (побережье). Это правило должно сопровождаться другим правилом, описывающим ситуацию с точки зрения COAST(COAST, SEA, RIGHT).


Если вы хотите, чтобы тайлы SEA могли располагаться не только СЛЕВА, но и СПРАВА от тайлов COAST. то им нужны дополнительные правила: (SEA, COAST, RIGHT) и (COAST, SEA, LEFT).

Как я сказал выше, нам не нужно создавать список всех этих правил самостоятельно. Коллапс волновой функции может создать набор правил для Even Simpler Tile Model парсингом изображения-примера и собиранием списка всех 3-tuple, которые в нём содержатся.


Исследовав показанный выше пример изображения, Even Simpler Tiled Model замечает, что тайлы моря могут быть только под или сбоку от тайлов побережья, или в любом месте рядом с другими тайлами моря. Также она замечает, что тайлы побережья могут располагаться рядом с сушей, морем или другими тайлами побережья, но только над тайлами моря и под тайлами суши. Она не пытается вывести никакие более сложные правила, например «тайлы моря должны быть рядом по крайней мере с одним тайлом моря» или «каждый остров должен содержать как минимум один тайл суши». Ни один из тайлов не может влиять на то, что какие-то типы тайлов могут или не могут располагаться в двух или более квадратах от них. Это похоже на модель плана свадьбы, в которой единственное правило: «X может сидеть рядом с Y».

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

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

Коллапс


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


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

Например, в модели Even Simpler Tile Model квадрат без информации об окружающих его квадратах ничем не ограничен и может стать любым тайлом. Следовательно, он имеет очень высокую энтропию. Но квадрат, вокруг которого уже коллапсировало несколько квадратов, может иметь на выбор всего 2 тайла.


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

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

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

# Sums are over the weights of each remaining
# allowed tile type for the square whose
# entropy we are calculating.
shannon_entropy_for_square =
  log(sum(weight)) -
  (sum(weight * log(weight)) / sum(weight))

Вычислив квадрат волновой функции с наименьшей энтропией, мы коллапсируем её волновую функцию. Мы делаем это, случайным образом выбирая один из тайлов, пока ещё доступных для квадрата, взвешенный на веса тайлов, которые мы спарсили из входящего изображения. Веса используются потому, что это обеспечивает более реалистичное изображение на выходе. Допустим, волновая функция квадрата сообщает, что он может быть сушей или побережьем. Мы не всегда должны выбирать один из вариантов с вероятностью 50%. Если во входящем изображении больше тайлов суши, чем побережья, то нам стоит отразить этот перевес и в выходном изображении. Реализуется это при помощи простых глобальных весов. Если в примере изображения есть 20 тайлов суши и 10 тайлов побережья, то квадрат коллапсирует в сушу с вероятностью 2/3, а в побережье — с оставшейся вероятностью 1/3.

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

В итоге мы создали мир (или ошибку).

Куда двигаться дальше


Разобравшись с моделью Even Simpler Tiled Model, вы готовы подниматься выше по лестнице мощности и сложности алгоритма. Начните с Simple Tiled Model, которую мы упоминали в начале этого поста, затем перейдите к полной Overlapping Model. В Overlapping Model тайлы или пиксели влияют друг на друга издалека. Если вы понимаете в таких вещах, то ExUtumno замечает, что Simple Tiled Model схожа с цепью Маркова порядка-1, а более сложные модели напоминают цепи большего порядка.

Wavefunction Collapse даже может учитывать дополнительные ограничения, например «этот тайл должен быть морем» или «этот пиксель должен быть красным» или «в выходных данных может быть только один монстр». Обо всё этом рассказывается README основного проекта. Также можно изучить оптимизации скорости, внесённые в полную реализацию. Необязательно повторно вычислять энторпию каждого квадрата в каждой итерации, а распространение информации по волновой функции можно сделать значительно быстрее. Эти аспекты становятся важнее при увеличении размеров выходящих изображений.

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

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


  1. GCU
    26.07.2019 10:02
    +1

    Чем-то напоминает плитки (домино) Вана или решение/построение Судоку :)


    1. deitry
      26.07.2019 17:23

      Последняя картинка невозбранно отдаёт Сапёром


  1. cadovvl
    26.07.2019 10:59

    В подобных алгоритмах у меня всегда возникает вопрос: каким требованиям должны соответствовать исходные правила, чтобы алгоритм
    1) Гарантированно сошелся?
    2) Гарантированно сошелся не более чем за M шагов?


  1. Vindicar
    26.07.2019 11:34

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


    1. wladyspb
      26.07.2019 12:10

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


    1. neurocore
      27.07.2019 22:28

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


  1. user_man
    26.07.2019 13:27
    +1

    Какой-то ёперный театр…

    Зачем менять одно всем понятное слово «перебор» на кучу никому непонятных фраз про функцию, волну, коллапс, суперпозицию?

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

    Хотя да, принцип работы квантового компьютера теперь знаком каждому малышу из нашего детского сада. Ура, товарищи малыши!


    1. ipswitch
      26.07.2019 13:46
      +1

      Если кто-то играл в TIS-100, ближе к последним уровням названия становятся всё более и более харакетрными — SPATIAL PATH FINDER, и конечно же, WAVE FUNCTION COLLAPSE SUPERVISOR.
      steamcommunity.com/sharedfiles/filedetails/?id=1312967423

      Уже тут многие подумали, что всё не так просто :)
      А описание бесспорно хорошее. Как и подход. Порассуждать о высоком на кубиках зачастую делает это высокое не заумной хренью, а чем-то, к чему можно подступиться.
      Так что и физику полезны игрушки и кубики:)


    1. GCU
      26.07.2019 15:08

      принцип работы квантового компьютера теперь знаком каждому малышу из нашего детского сада

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


      1. user_man
        27.07.2019 13:51

        >> А что плохого в знании редукции фон Неймана с детского сада?

        Было бы ничего, если бы детям более совсем нечем было бы заняться.

        Или вы за отказ от всего ради величия физики?

        >> это несколько другая задача

        Не понял ваших намёков.


        1. GCU
          27.07.2019 14:54

          вы за отказ от всего ради величия физики?

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

          Не понял ваших намёков.

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


          1. user_man
            28.07.2019 13:50

            >> Основное применение алгоритма для процедурной генерации карт и текстур

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

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


  1. DimPal
    26.07.2019 14:22

    Статью нужно назвать «Максимально запутанное объяснение алгоритма ...». В статье показано как объяснить простые вещи максимально недоступно.


    1. usdglander
      26.07.2019 14:51

      Вы и это объяснение не поняли? =)


      1. perfect_genius
        28.07.2019 12:20

        Простое объяснение через квантовую физику — что может быть проще?


  1. CaptainFlint
    26.07.2019 14:51

    По-моему, волновые функции с коллапсами тут вообще ни при чём. Коллапс он на то и коллапс, что всё состояние схлопывается сразу и целиком. А механизм последовательной фильтрации допустимых вариантов мне всегда казался совершенно очевидным; я его ещё 15 лет назад реализовывал для своей задачки, не выдумывая никаких волновых функций.


  1. DatUser
    26.07.2019 16:35

    Люди, пишущие на императивных языках, столкнулись с задачами декларативных языков, взяли из них кусочек алгоритма решения и сразу «Вааааау!». Ждём проникновения декларативных возможностей во все популярные ЯП, как это сейчас было (и всё ещё продолжается) с проникновением функционального программирования.


  1. rmrfchik
    26.07.2019 17:18

    Всё это дико напоминает алгоритм поиска решения в Прологе с бэктрекингом. Единственное добавление это выбор начального узла, что делается в Прологе вручную несложно.


  1. Fen1kz
    27.07.2019 02:34

    Советую добавить в статью ссылку сюда: https://github.com/mxgmn/WaveFunctionCollapse — куча крышесносных примеров. Там же есть веб-версия: http://www.kchapelier.com/wfc-example/simple-tiled-model-animated.html