Как устроен ГИГАХРУЩ: Как мы заставили линейную алгебру и теорию категорий писать процедурную речь для НПС. генерация текста.. генерация текста. гигахрущ.. генерация текста. гигахрущ. дистрибутивная семантика.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра. марковские цепи.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра. марковские цепи. метод главных компонент.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра. марковские цепи. метод главных компонент. процедурная генерация.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра. марковские цепи. метод главных компонент. процедурная генерация. самосбор.. генерация текста. гигахрущ. дистрибутивная семантика. классы эквивалентности. линейная алгебра. марковские цепи. метод главных компонент. процедурная генерация. самосбор. теория категорий.

В прошлых статьях про инженерную архитектуру ГИГАХРУЩА мы разбирали, как в одном браузерном файле на чистом TypeScript держится тороидальный клеточный мир 1024x1024, кастомный WebGL-рэйкастер, симуляция A-Life и объемный генератор MESH PASS.

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

В этой статье мы покажем, как с помощью векторного анализа корпусов, классов эквивалентности и PCA-осей нам удалось заставить Марковское ядро генерировать живую, контекстную речь для тысяч НПС — без сторонних фреймворков ("dependencies": {}), с задержкой 0 миллисекунд и 100% детерминизмом по SeedRng.

Приступим-с!

Приступим-с!

1. Откровение дистрибутивной алгебры: Как математика видит слова в тексте

Когда мы только подступили к задаче озвучки НПС (диалоги, баблы над головами, лог речи, посты в локальную соцсеть «Инфосеть Демос»), перед нами встала дилемма.

  • Жесткий хардкод (If-Else / шаблоны): Написать 5000 заготовок "{npc} пришел в {place}". Скучно, видно швы, игроку надоедает через полчаса.

  • Наивный Марков по сырому тексту: Выдает колоссальное разнообразие, но страдает «синтаксической шизофренией» — НПС придумывает ложные факты, путает термины и уходит в бессвязный бред.

  • LLM в облаке: Недопустимо. Контракт ГИГАХРУЩА — автономный браузерный билд, который должен работать 60 FPS даже на кассовом терминале без доступа к сети.

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

Мы взяли 11 обучающих корпусов (16,900+ предложений из «Пикника на обочине», «Самосбор-тредов», «Метро 2033», «S.T.A.L.K.E.R.», «Града обреченного», «Соляриса», советских инструкций Гражданской Обороны и ГОСТов) и написали офлайн-исследователь scripts/discover_categories.ts.

Факторизация словаря ()

Мы не кормили алгоритму готовые словари. Вместо этого алгоритм скользил окном контекста по тысячам страниц и строил вектор окружения для каждого слова w in W.

Затем мы ввели на словаре отношение эквивалентности sim:

w_1 sim w_2 iff cos(vec{v}_{context}(w_1), vec{v}_{context}(w_2)) > theta

И произошло то, что поразило нас самих: математика без знания русского языка сама сгруппировала слова в идеальные смысловые кластеры (фактор-множество W / sim)!

Вот реальный фрагмент вывода нашего скрипта scripts/discover_categories.ts:

[Категория #20] (Векторный узел: ОБОРУДОВАНИЕ / ПАТРОНЫ)
  патронов/патроны (+0.43), оборудования/оборудование (-0.20), 
  огонь (-0.11), заблокировать (-0.21), сальников (-0.25)

[Категория #28] (Векторный узел: ГЕРМЕТИЗАЦИЯ / ПРОХОД)
  герметизация/герметизации (+1.00), кабели/кабелей (+0.57), 
  проход/прохода (+0.50), автомат (+0.10), расход (-0.22)

[Категория #29] (Векторный узел: ЖИЛОЙ БЛОК)
  жилом/жилой (+0.54), свой (+0.56), жертв/жертву (+0.66), ванны/ванной (-0.33)

Алгоритм сам понял, что [чай, кофе, тушенка, концентрат] выполняют одинаковую ролевую функцию в предложении, а [гермозатвор, шлюз, вентиляция, трансформатор] являются объектами пространства!

Живой пример: От Сидоровича и Артема к Ольге Дмитриевне и Майору Громову

Давайте наглядно разберем, как чужие литературные персонажи превращаются в канонических жителей ГИГАХРУЩА.

Представьте исходную строку из обучающего корпуса S.T.A.L.K.E.R. или «Метро 2033»:

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

Алгоритм не хардкодит имена. При факторизации текста по W / sim предложение превращается в абстрактную матричную цепочку:

begin{aligned}text{«Сидорович»} &to langle text{СУБЪЕКТ_АДМИНИСТРАТОР} rangle \text{«Меченого»} &to langle text{СУБЪЕКТ_ИСПОЛНИТЕЛЬ} rangle \text{«бункера»} &to langle text{МЕСТО_УКРЫТИЕ} rangle \text{«вентиляционной шахты»} &to langle text{МЕСТО_ОПАСНОСТЬ} rangle \text{«артефакт»} &to langle text{ЦЕННЫЙ_ПРЕДМЕТ} rangleend{aligned}

Формируется чистый грамматический скелет:

mathcal{S}=langle text{СУБЪЕКТ_АДМИНИСТРАТОР} to text{МЕСТО_УКРЫТИЕ} to text{СУБЪЕКТ_ИСПОЛНИТЕЛЬ} to text{ЦЕННЫЙ_ПРЕДМЕТ} to text{МЕСТО_ОПАСНОСТЬ} rangle

Когда движок ГИГАХРУЩА выполняет Категориальный Изоморфизм mathcal{A} в рантайме, он подставляет персонажей и предметы нашей мегаструктуры:

  • langle text{СУБЪЕКТ_АДМИНИСТРАТОР} rangle to Ольга Дмитриевна (врач санпропускника) или Сержант Баринов (начальник оружейки)

  • langle text{СУБЪЕКТ_ИСПОЛНИТЕЛЬ} rangle to проходчик Яков или ликвидатор

  • langle text{ЦЕННЫЙ_ПРЕДМЕТ} rangle to дозиметр ДП-5В, чистый фильтр АФУ-300, малахитовый артефакт

  • langle text{МЕСТО_УКРЫТИЕ} rangle to защитный шлюз, приёмный покой

И на выходе речевой роутер выдает аутентичную речь ГИГАХРУЩА:

“Ольга Дмитриевна сидит у защитного шлюза санпропускника и просит ликвидатора принести чистый фильтр АФУ-300 из вентиляционной шахты.”

Никакого regex-хардкода: мы взяли живую драму и синтаксис человеческой литературы, но в слоты встали персонажи нашей игры — Ольга Дмитриевна, Сержант Баринов, Яков и Майор Громов!

Ульянка тоже не любит математику!

Ульянка тоже не любит математику!

2. Извлечение синтаксических скелетов (Patterns Discovery)

Чтобы Марковская цепь движка не перескакивала хаотично с существительного на глагол, второй скрипт (scripts/discover_patterns.ts) повторно прошел по корпусам и извлек устойчивые синтаксические скелеты предложений:

mathcal{S}=langle K_1 to K_2 to dots to K_n rangle

Вот реальный вывод из discover_patterns.ts:

Freq: 33 | Pattern: <МЕСТО> -> <ЦЕННЫЙ_ПРЕДМЕТ>
Freq: 29 | Pattern: <СУБЪЕКТ> -> <УГРОЗА>
Freq: 22 | Pattern: <МЕСТО> -> <УГРОЗА>
Freq: 18 | Pattern: <УГРОЗА> -> <МЕСТО>

Марковское ядро больше не блуждает «вслепую». Очерченный скелет указывает графу точный порядок категорий: например, сначала озвучиваем МЕСТО, затем ЦЕННЫЙ_ПРЕДМЕТ, а затем реакцию СУБЪЕКТА.

Если бы мы сразу знали куда нас это заведёт...

Если бы мы сразу знали куда нас это заведёт…

3. Векторизация и PCA-оси: Дорого vs Дешево, Опасно vs Безопасно

Но как НПС понимает, какой именно предмет или угрозу подставить в слот? Ведь и «гаусс-пушка», и «грязный бинт» принадлежат одной категории langle text{ЦЕННЫЙ_ПРЕДМЕТ} rangle, но применить их в одинаковых ситуациях нельзя!

Здесь вступает в силу Метод Главных Компонент (PCA). Внутри каждого класса эквивалентности математика вычисляет оси дисперсии от -1.0 до +1.0:

Категория: <ЦЕННЫЙ_ПРЕДМЕТ>
  -1.0 (Дешевый мусор) <------------------------------------> +1.0 (Редчайший артефакт)
  [грязный бинт: -0.9, талон на воду: -0.7]  ...  [дозиметр: +0.4, гаусс-пушка: +1.0]

Категория: <УГРОЗА>
  -1.0 (Бытовой риск) <-------------------------------------> +1.0 (Смертельная катастрофа)
  [истощение: +0.2, черная слизь: +0.75]   ...   [бетонник: +0.85, Самосбор: +1.0]

В момент генерации движок собирает текущий игровой контекст НПС MarkovTextContext:

vec{v}_{text{context}}=begin{pmatrix} v_{text{danger}} \ v_{text{wealth}} \ v_{text{need}} end{pmatrix} in [-1.0, 1.0]^3

И скатывает марковский выбор по косинусному сходству векторов:

Score(A, mathcal{C})=log P_{text{interp}}(A mid h) + alpha cdot text{bitCount}(text{Tags}_A cap text{Tags}_{mathcal{C}}) + beta cdot cos(vec{v}_{text{atom}}, vec{v}_{text{context}})

Если НПС нашел предмет стоимостью 5000 кредитов (v_{text{wealth}}=+1.0), вероятностная матрица моментально гасит вес грязного бинта и выводит на вершину гаусс-пушку!

Хватит уже математики!

Хватит уже математики!

4. Разбор 10 Примеров Сгенерированной Речи НПС в ГИГАХРУЩЕ

Ниже приведены реальные результаты прогона нашего прототипа scripts/markov_core_prototype.ts.

Какие нехорошие культисты!

Какие нехорошие культисты!

Пример 1: Бытовой диалог обывателя у гермодвери

  • Игровой контекст mathcal{C}: floorZ: 12, roomType: жилая_ячейка, occupation: обыватель, dangerLevel: 5, foundItemValue: 15 (талон на воду).

  • Вектор контекста: vec{v}_{text{context}}=(-0.9, -0.7, -0.5)^T.

  • Синтаксический скелет: langle text{МЕСТО} to text{ЦЕННЫЙ_ПРЕДМЕТ} rangle

  • Результат генерации:

    “Если паразитический мицелий вырвется из талон на воду, бункер Гражданской Обороны придется опечатать на месяц.”

  • Разбор ядра: Мирный бытовой контекст направил Марковский граф по слотам инструкции ГО, вставив в слот предмета бытовой артефакт талон на воду, а в слот места — бункер Гражданской Обороны.


Пример 2: Тревога ликвидатора у вентиляционной шахты

  • Игровой контекст mathcal{C}: floorZ: -4, roomType: вентиляционная_шахта, occupation: ликвидатор, dangerLevel: 85, roomMemory: ['черная_слизь'].

  • Вектор контекста: vec{v}_{text{context}}=(+0.85, +0.1, 0.0)^T.

  • Синтаксический скелет: langle text{УГРОЗА} to text{СУБЪЕКТ} rangle

  • Результат генерации:

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

  • Разбор ядра: Высокий v_{text{danger}}=0.85 поднял веса категории langle text{УГРОЗА} rangle. Движок выбрал бетоноеда и задействовал рефлекс ухода за укрытие.


Пример 3: Сирена САМОСБОРА (Паника и задраивание шлюзов)

  • Игровой контекст mathcal{C}: floorZ: 42, roomType: коридор_сектора, occupation: дежурный_по_герме, dangerLevel: 100, isSamosborActive: true.

  • Вектор контекста: vec{v}_{text{context}}=(+1.0, -1.0, +0.8)^T.

  • Синтаксический скелет: langle text{УГРОЗА} to text{СУБЪЕКТ} rangle

  • Результат генерации:

    “Появилась точная цель, но не сам Самосбор, а его обитатели. Мы ковыряемся в кровавом тупике жизнеобеспечения, выносите мясника из центра!”

  • Разбор ядра: При активной фазе Самосбора v_{text{danger}}=1.0 граф скатился в корпус канонических Самосбор-тредов, активировав атмосферу паники и зачистки тупиков.


Пример 4: Находка ценного хабара (Гаусс-пушка)

  • Игровой контекст mathcal{C}: floorZ: -12, roomType: кровавый_тупик, occupation: проходчик, dangerLevel: 60, foundItemValue: 5000.

  • Вектор контекста: vec{v}_{text{context}}=(+0.6, +1.0, 0.0)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{ЦЕННЫЙ_ПРЕДМЕТ} rangle

  • Результат генерации:

    “Проходчик долго осматривал гаусс-пушку. За такой хабар бригадир смены выставит три нормы пайкового концентрата у шлюза.”

  • Разбор ядра: Выходной параметр ценности предметов foundItemValue: 5000 дал зашкаливающий вектор по оси v_{text{wealth}}, моментально выбравший топовое вооружение из слота ценностей.


Пример 5: Слесарь-параноик у мокрого шва гермодвери

  • Игровой контекст mathcal{C}: floorZ: 88, roomType: трансформаторная, powerStatus: 'off', occupation: слесарь-параноик, dangerLevel: 40.

  • Вектор контекста: vec{v}_{text{context}}=(+0.4, -0.2, +0.4)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{МЕСТО} rangle

  • Результат генерации:

    “Слесарь-параноик не верил тишине: у гермы шов мокрый. Сначала уплотнитель, потом разговор.”

  • Разбор ядра: Профессиональные теги гильдии слесарей repair вытащили из корпуса специфические бытовые действия ремонта и проверки герметичности.


Пример 6: Лаборант НИИ в продувочной прачечной

  • Игровой контекст mathcal{C}: floorZ: 104, roomType: продувочная_прачечная, occupation: лаборант НИИ, dangerLevel: 15.

  • Вектор контекста: vec{v}_{text{context}}=(-0.7, +0.2, -0.8)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{МЕСТО} to text{ЦЕННЫЙ_ПРЕДМЕТ} rangle

  • Результат генерации:

    “Лаборант НИИ установил дозиметр ДП-5В у продувочной прачечной. Реагенты в норме, инспекционный шлюз чист.”

  • Разбор ядра: Категория science подтянула строго научные приборы (дозиметр ДП-5В) и канцелярит НИИ “Щит”.


Пример 7: Раненый домком просит помощи (Критический голод и ранение)

  • Игровой контекст mathcal{C}: floorZ: 3, roomType: жилая_ячейка, hp: 15, thirst: 95, recentTrauma: true, occupation: домком.

  • Вектор контекста: vec{v}_{text{context}}=(+0.3, -0.9, +0.95)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{ЦЕННЫЙ_ПРЕДМЕТ} rangle

  • Результат генерации:

    “Домком едва дышал на полу: грязный бинт и талон на воду решают быстрее любого разговора.”

  • Разбор ядра: Критический уровень потребностей v_{text{need}}=0.95 сфокусировал ядро на предметах первой помощи (грязный бинт, талон на воду), проигнорировав оружие и хабар.


Пример 8: Мясник у Кровавого Алтаря (Пси-излучение)

  • Игровой контекст mathcal{C}: floorZ: -69, roomType: алтарь_чернобожия, karma: -100, occupation: мясник, dangerLevel: 95.

  • Вектор контекста: vec{v}_{text{context}}=(+0.95, +0.5, +0.7)^T.

  • Синтаксический скелет: langle text{УГРОЗА} to text{СУБЪЕКТ} rangle

  • Результат генерации:

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

  • Разбор ядра: Отрицательная карма (karma: -100) и специфическая глубинная комната запустили цепочку атомов с тегами mystic и blood.


Пример 9: Инструктаж бригадира смены у бункера ГО

  • Игровой контекст mathcal{C}: floorZ: 0, roomType: бункер_ГО, karma: 80, occupation: бригадир смены, dangerLevel: 10.

  • Вектор контекста: vec{v}_{text{context}}=(-0.8, +0.4, -0.9)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{МЕСТО} rangle

  • Результат генерации:

    “Бригадир смены проверил защитный шлюз: сначала снимите фильтр АФУ-300, затем доложите дежурному по блоку.”

  • Разбор ядра: Административный статус НПС и высокий показатель кармы задали уставной порядок команд Гражданской Обороны.


Пример 10: Дежурный электрик в сборочном цеху

  • Контекст мира mathcal{C}: floorZ: 16, roomType: сборочный_цех, occupation: дежурный электрик, dangerLevel: 25.

  • Вектор контекста: vec{v}_{text{context}}=(-0.5, 0.0, -0.3)^T.

  • Синтаксический скелет: langle text{СУБЪЕКТ} to text{МЕСТО} to text{ЦЕННЫЙ_ПРЕДМЕТ} rangle

  • Результат генерации:

    “Дежурный электрик зашел в сборочный цех и достал герметизирующую мастику. Если щиток гудит — не трогай тумблер.”

  • Разбор ядра: Производственная зона направила граф по техническому словарю ремонта и работы со станками.

Эмиль в шоке от нашего марковского ядра!

Эмиль в шоке от нашего марковского ядра!

5. Ограничения и главное откровение

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

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

Но в мире с сотнями тысяч процедурных НПС этот метод, как никакой иной, создаёт осязаемый, живой, независимый от игрока, а главное — контекстно осмысленный слой текстов. Теперь у жителей мегаструктуры есть рот, и они наконец могут кричать то, как из их собственной точки отсчёта выглядит бесконечный ГИГАХРУЩ.

Ульяна хочет пить!

Ульяна хочет пить!

Полезные ссылки проекта:

Автор: TENEVIK

Источник