habrahabr

Генерация текста на русском по шаблонам

  • вторник, 15 октября 2019 г. в 00:32:24
https://habr.com/ru/post/471278/
  • Разработка веб-сайтов
  • Open source
  • Python
  • Разработка игр
  • Natural Language Processing


Когда я только начинал работать над своей текстовой игрой, решил, что одной из её главных фич должны стать красивые художественные описания действий героев. Отчасти хотел «сэкономить», поскольку в графику не умел. Экономии не получилось, зато получилась Python библиотека (github, pypi) для генерации текстов с учётом зависимости слов и их грамматических особенностей.

Например, из шаблона:
[Hero] [проходил|hero] мимо неприметного двора и вдруг [заметил|hero] играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил [игрушечный|hero.weapon|вн] [hero.weapon|вн], выкрикнул: «[Я|hero] [великий|hero] [Hero]! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. [Хмыкнул|hero] и [сам|hero] [Hero], но не [стал|hero] выходить к малышне.
Мы можем получить такой текст (жирным выделены изменяющиеся слова):
Халлр проходил мимо неприметного двора и вдруг заметил играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил игрушечную золочёную шпагу, выкрикнул: «Я великий Халлр! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. Хмыкнул и сам Халлр, но не стал выходить к малышне.
Или такой:
Фиевара проходила мимо неприметного двора и вдруг заметила играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил игрушечный катар, выкрикнул: «Я великая Фиевара! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. Хмыкнула и сама Фиевара, но не стала выходить к малышне.

Пара оговорок
Оговорка 1. Я не лингвист и библиотека писалась «чтобы работала», а не «чтобы точно соответствовала всем правилам языка». Поэтому заранее извиняюсь за неточности в терминологии или неполную трактовку правил русского языка.

Оговорка 2. Библиотека разрабатывалась около 5 лет назад, сейчас могли появиться (или дорасти до нормального состояния) альтернативные средства генерации текста. Например, что-нибудь интересное может быть в софте для локализации.

О сложности генерации текстов


Русский язык сложен во многих своих аспектах. В частности, слова имеют большое количество морфологических форм. Например, прилагательные могут иметь полную и краткую формы, изменяются по роду, числу, падежу, одушевлённости и степени сравнения. Выбор же конкретной формы зависит от других слов в предложении. Мы говорим «красивая женщина», но «красивый мужчина». Слово «красивый» в данном случае зависит от слов «мужчина» / «женщина» — его форма определяется родом главного слова.

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

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

Возможности библиотеки


UTG (universal text generator — не очень скромное название) позволяет создавать шаблоны для генерации текста с указанием:

  • переменных (например, имени персонажа);
  • зависимостей слов от переменных (например, прилагательного от существительного);
  • Зависимостей одних переменных от других;
  • Явных свойств слов и переменных (например, можно указать, что имя персонажа вставляется в родительском падеже);

При формировании текста по шаблону:

  • На зависимые слова переносятся необходимые свойства главного слова. Например, на прилагательное переносится род существительного.
  • Согласуется форма зависимых слов с числительными (с учётом формы зависимых слов).
  • Модифицируются предлоги если необходимо (например, обо мне / о тебе), предлог для этого должен быть размечен.

Дополнительно реализованы:

  • Словарь для хранения необходимых слов.
  • Хранилище шаблонов для их хранения по типу и выбора случайного.

Библиотека «знает» о существовании существительных, прилагательных, местоимений, глаголов, причастий, чисел, предлогов и «цитат» (неизменяемого текста).

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

Формат шаблонов и пример использования


Давайте разберём простой шаблон:
Вчера [mob] [укусил|mob] [hero|вн].
В зависимости от значений переменных, шаблон может отобразиться как такой фразой:
Вчера гиена укусила Халлра.
так и такой:
Вчера светлячки укусили привидение.
Рассмотрим шаблон подробнее:

  • Вчера — обычный текст.
  • [mob] — переменная, вместо которой подставится название монстра.
  • [укусил|mob] — слово, зависимое от переменной, часть его свойств будет изменяться в зависимости от свойств названия монстра (например, число). Генератор текста автоматически распознаёт свойства формы слова и пытается их сохранить (например, будет распознано и сохранено прошедшее время, поэтому указывать его не надо).
  • [hero|вн] — переменная, вместо которой подставится имя героя. Дополнительно указано, что имя должно быть в винительном падеже.

Больше примеров шаблонов
Некоторые технические примеры можно найти в тестах.

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

И переменные и зависимые слова в шаблоне выделяются одинаково и имеют следующий формат:

  • [ — открывающая квадратная скобка.
  • слово — зависимое слово или идентификатор переменной. Генератор сначала проверяет наличие переменной с таким именем, если такой переменной нет, то слово ищется в словаре.
  • | — вертикальная черта — разделитель, нужен если указываем дополнительные свойства.
  • имя переменной — переменная, от которой зависит форма слова, может отсутствовать.
  • | — вертикальная черта — разделитель, нужен если указываем дополнительные свойства.
  • свойства слова через запятую — описание требуемой формы слова (падеж, род и так далее). Их список можно найти на страницах проекта в github и pypi.
  • ] — закрывающая квадратная скобка.

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

[переменная 1|переменная 2|вн,мр|переменная 3|прш,ед,од]

В большинстве случаев хватает следующих форматов:

  • [переменная] — вставить переменную в нормальной форме (например, существительное в именительном падеже единственного числа).
  • [переменная|свойства] — вставить переменную с указанными свойствами.
  • [слово|переменная] — вставить слово, согласовав его с переменной (например, прилагательное «красивый» с существительным по роду и падежу).
  • [слово|переменная|свойства] — вставить слово, согласовав его с переменной и указав дополнительные свойства.

Обратите внимание:

  • Указание свойств для слов и переменных действует только в месте вставки, поэтому, чтобы получить словосочетание «красивого героя» мы должны указать винительный падеж явно для двух слов: [красивый|hero|вн] [hero|вн].
  • Генератор текста умеет «угадывать» свойства слова по его форме, например, во фразе [hero] [побежал|hero] можно не указывать время глагола.
  • Свойства, указанные позже, затирают свойства, указанные ранее. Например, во фразе [красивого|hero] [hero|вн] не будет установлен винительный падеж прилагательного, так как он заменится именительным падежом переменной hero.
  • Перечень свойств слов можно найти на страницах библиотеки в github и pypi.

Пример с кодом
Требуется Python 3

Установка

pip install utg

python -m unittest discover utg

Код.

from utg import relations as r
from utg import dictionary
from utg import words
from utg import templates
from utg import constructors

#######################################
# описываем существительное для словаря
#######################################

coins_forms = [# единственнео число
               'монета', 'монеты', 'монете', 'монету', 'монетой', 'монете',
               # множественное число
               'монеты', 'монет', 'монетам', 'монеты', 'монетами', 'монетах',
               # счётное число (заполнено для пример,
               # может быть заполнено методом autofill_missed_forms)
               'монеты', 'монет', 'монетам', 'монеты', 'монетами', 'монетах']

# свойства: неодушевлённое, женский род
coins_properties = words.Properties(r.ANIMALITY.INANIMATE, r.GENDER.FEMININE)

# Для создания слова указывается его тип, формы и свойства
coins_word = words.Word(type=r.WORD_TYPE.NOUN,
                        forms=coins_forms,
                        properties=coins_properties)

# Формы слова должны быть указаны в фиксированном порядке.
# Если вы хотите автоматизировать создание слов,
# найти порядок форм слова и их свойства можно в переменных:
# - utg.data.WORDS_CACHES
# - utg.data.INVERTED_WORDS_CACHES

##############################
# описываем глагол для словаря
##############################

# описываем только нужны нам формы слова
# (порядок важен и определён в utg.data.WORDS_CACHES[r.WORD_TYPE.VERB])
action_forms = (['подарить', 'подарил', 'подарило', 'подарила', 'подарили'] +
                [''] * 15)

# свойства: совершенный, прямой залог
action_properties = words.Properties(r.ASPECT.PERFECTIVE, r.VOICE.DIRECT)

action_word = words.Word(type=r.WORD_TYPE.VERB,
                         forms=action_forms,
                         properties=action_properties)

# заполняем пропущенные формы на основе введённых (выбираются наиболее близкие)
action_word.autofill_missed_forms()

##############################################
# создаём словарь для использования в шаблонах
##############################################

test_dictionary = dictionary.Dictionary(words=[coins_word, action_word])

################
# создаём шаблон
################
template = templates.Template()

# externals — внешние переменные, не обязаны быть в словаре
template.parse('[Npc] [подарил|npc] [hero|дт] [coins] [монета|coins|вн].',
               externals=('hero', 'npc', 'coins'))

##############################
# описываем внешние переменные
##############################

hero_forms = ['герой', 'героя', 'герою', 'героя', 'героем', 'герое',
              'герои', 'героев', 'героям', 'героев', 'героями', 'героях',
              'герои', 'героев', 'героям', 'героев', 'героями', 'героях']

# свойства: одушевлённый, мужской род
hero_properties = words.Properties(r.ANIMALITY.ANIMATE, r.GENDER.MASCULINE)

hero = words.WordForm(words.Word(type=r.WORD_TYPE.NOUN,
                                 forms=hero_forms,
                                 properties=hero_properties))

npc_forms = ['русалка', 'русалки', 'русалке', 'русалку', 'русалкой', 'русалке',
             'русалки', 'русалок', 'русалкам', 'русалок', 'русалками', 'русалках',
             'русалки', 'русалок', 'русалкам', 'русалок', 'русалками', 'русалках']

# свойства: одушевлённое, женский род
npc_properties = words.Properties(r.ANIMALITY.ANIMATE, r.GENDER.FEMININE)

npc = words.WordForm(words.Word(type=r.WORD_TYPE.NOUN,
                                forms=npc_forms,
                                properties=npc_properties))

##########################
# осуществляем подстановку
##########################

result = template.substitute(externals={'hero': hero,
                                        'npc': npc,
                                        'coins': constructors.construct_integer(125)},
                             dictionary=test_dictionary)

##########################
# Проверяем
##########################

result == 'Русалка подарила герою 125 монет.'

О словарях


Как вы могли заметить, UTG требует формирование словаря. Делается это «руками» так как в момент разработки:

  • Общедоступных качественных морфологических словарей я не нашёл.
  • Библиотека pymorphy была ещё первой версии и довольно часто косячила (особенно с винительным падежом), из-за чего пришлось от неё отказаться.

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

Итого


Надеюсь, библиотека окажется полезной.

Если у вас есть идеи по её развитию (а ещё лучше, желание в нём участвовать) — пишите в личку, делайте pull requests, постите баги на гитхаб.