Привет, Хабр. Я последние пару лет играюсь с естественной речью на русском языке. Решил поделиться своим опытом по работе с поэзией. Будет две статьи: вот эта и про рифму (когда дойдут руки всё доделать).

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

Немного теории

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

Буря мглою небо кроет

| – | – | – | –

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

Моё море, прошу тебя, не выплюни меня на берег

– | | – – | – | – | – – – | – | –

Считать ли поэзией, например, верлибр — вопрос дискуссионный и выходящий за рамки данного текста. Как писал Томас Элиот: «Автор верлибра свободен во всём, если не считать необходимости создавать хорошие стихи».

Стихотворные размеры

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

Название размера

Схема

Пример

Хорей

| – | – | – | –

Буря мглою небо кроет

Ямб

– | – | – | – |

Товарищ, верь: взойдёт она

Дактиль

| – – | – –

Тучки небесные, вечные странники

Амфибрахий

– | – – | –

Есть женщины в русских селеньях

Анапест

– – | – – |

На заре ты её не буди

Необязательные детали

Если зарываться в стихосложение далее, то в некоторых языках (но не русском) длительность, с которой вы произносите тот или иной гласный звук, является значимой и влияет на восприятие текста. Этот факт использует, например, гекзаметр — размер, которым написаны Илиада и Одиссея. С точки зрения русского языка он неотличим от дактиля, но на древнегреческом есть особенности в чередовании долгих и кратких слогов. Такие размеры в целом называются силлабо-метрическими.

Навскидку задача и правда выглядит простой: мы должны расставить ударения в исходной строке и сравнить её с одной из пяти схем. Где совпало — там и размер.

Ударения

Некоторое время назад я написал библиотеку Nestor на основе найдённого когда-то в сети словаря под авторством Михаила Хагена, который является переработанным словарём словоформ Зализняка. Эта библиотека по смыслу и по устройству похожа на pymorphy2, но написана на C# и содержит в себе ударения, что стало для меня ключевой причиной того, зачем я вообще её делал. В репозитории есть словарь, если вы захотите переписать её на свой любимый язык.

В русском языке есть омонимы с ударениями на разные слоги. Если человек написал с клавиатуры слово «замок», мы не знаем, что именно он имел ввиду: замок — средневековая крепость, или замок — устройство для блокировки двери. Заставлять пользователя размечать ударения вручную это примерно то же самое, что заставлять его самому назвать стихотворный размер — не наш путь. Кстати, при работе с навыками для голосовых ассистентов (Алиса, Салют, Маруся) формально у системы есть данные о том, на какой слог человек сделал ударение, но по факту эти данные не выдаются, и разработчику навыка просто приходит такое же обезличенное слово, как если бы ввод был текстовым.

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

Интересный факт про ударения

Всем известны слова с двумя ударениями, такие, как замок и замок. Я задумался, а бывает ли с тремя? Оказалось да, таких слов около пятнадцати. Вот, например, слово вывозите может иметь ударения на любом из первых трёх слогов:

  1. Вы сейчас вывозите себя в этой грязи, перестаньте!

  2. А по каким дням вы вывозите мусор?

  3. Эй, вы двое, вывозите отсюда все свои вещи!

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

Слов более чем с тремя ударениями в словаре Хагена-Зализняка не нашлось.

В общем, первая сложность:

  • Возможное ударение в слове может приходиться на несколько слогов.

Односложные слова

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

Мороз и солнце; день чудесный!

Ещё ты дремлешь, друг прелестный —

<...>

Скользя по утреннему снегу,

Друг милый, предадимся бегу

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

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

Вот ямбом:

Рубился я в картишки,

Сел на пол сдал по три,

За ними были фишки,

А после — пузыри.

А вот амфибрахием:

Играем в картишки,

Сел на пол, сдал по три,

Потом будут фишки,

А дальше — посмотрим.

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

Таким образом, вторая сложность:

  • Односложные слова могут быть и в ударной и в безударной позиции.

Алгоритм

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

Для начала разметим строку. Для каждого слова вернём массив StressType[], индексы в котором соответствуют номерам слогов в слове (начиная с нуля). А значения могу быть такими: «слог точно ударный», «слог точно безударный» и «слог может быть ударным».

public enum StressType
{
    StrictlyUnstressed,
    CanBeStressed,
    StrictlyStressed,
}

Слоги считаем по числу гласных. Для нуля гласных ответ пустой. Если в слове один слог, то сразу возвращаем для него CanBeStressed.

На вход в метод приходит string word.

int vCount = word.Count(NestorMorph.IsVowel); // количество гласных

if (vCount == 0)
{
	return Array.Empty<StressType>();
}

if (vCount == 1)
{
	return new[] { StressType.CanBeStressed };
}

Далее логика для многосложных слов. Если слово есть в нашем словаре, получаем все его ударения и складываем в HashSet номера ударных слогов (тут они начинаются с единицы).

var knownStressedVowelNumbers = new HashSet<int>();
            
Word[] wordInfos = _nestor.WordInfo(word); // все найденные в словаре лексемы
foreach (Word wordInfo in wordInfos)
{
	WordForm[] exactForms = wordInfo.ExactForms(word); // все омонимичные формы
	foreach (WordForm form in exactForms)
	{
		if (form.Stress > 0)
		{
			knownStressedVowelNumbers.Add(form.Stress);
		}
	}
}

Если известных ударений нет, то любой слог CanBeStressed.

if (knownStressedVowelNumbers.Count == 0)
{
    return Enumerable.Repeat(StressType.CanBeStressed, vCount).ToArray();
}

Если ударение одно, то оно StrictlyStressed, а все остальные слоги StrictlyUnstressed.

StressType[] finalStresses = Enumerable.Repeat(StressType.StrictlyUnstressed, vCount).ToArray();
if (knownStressedVowelNumbers.Count == 1)
{
	finalStresses[knownStressedVowelNumbers.First() - 1] = StressType.StrictlyStressed;
}

Если же ударений несколько, то все они CanBeStressed, а остальные слоги, соотвественно, StrictlyUnstressed.

else
{
	foreach (int knownStressNumber in knownStressedVowelNumbers)
	{
		finalStresses[knownStressNumber - 1] = StressType.CanBeStressed;
	}
}

return finalStresses;

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

Оценка близости

Для каждого размера заранее запишем маску StressType[], которая будет содержать только значения CanBeStressed и StrictlyUnstressed. Почему так? Потому что под ударной долей стихотворного размера может легко находиться безударный слог, если слово длинное, мы такой пример уже видели выше.

Скользя по утреннему снегу,

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

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

new []{ 
  StressType.StrictlyUnstressed, 
  StressType.CanBeStressed, 
  StressType.StrictlyUnstressed, 
  StressType.CanBeStressed, 
  StressType.StrictlyUnstressed, 
  StressType.CanBeStressed, 
  StressType.StrictlyUnstressed, 
  StressType.CanBeStressed, 
  StressType.StrictlyUnstressed
},

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

В маске слог CanBeStressed

В маске слог StrictlyUnstressed

В строке слог StrictlyStressed

Ударный слог в ударной доле, полное совпадение.

Не штрафуем.

Ударный слог не может попадать под безударную долю в размере.

+5 к штрафу

В строке слог CanBeStressed

Слог в строке может быть любым независимо от маски.

Не штрафуем.

Слог в строке может быть любым независимо от маски.

Не штрафуем.

В строке слог StrictlyUnstressed

В ударной доле размера находится безударный слог строки. Возможно, но не слишком хорошо.

+2 к штрафу

Безударный слог в безударной доле, полное совпадение.

Не штрафуем.

private const int WordToMaskMismatchPenalty = 5;
private const int MaskToWordMismatchPenalty = 2;

public int DistanceToFoot(Foot foot, IList<StressType> lineStresses)
{
	StressType[] mask = foot.GetMaskOfLength(lineStresses.Count);
	var dist = 0;
	for (var i = 0; i < mask.Length; i++)
	{
		StressType lineStress = lineStresses[i];
		StressType maskStress = mask[i];

		if (lineStress != StressType.CanBeStressed)
		{
			switch (lineStress)
			{
				case StressType.StrictlyStressed when maskStress == StressType.StrictlyUnstressed:
					dist += WordToMaskMismatchPenalty;
					break;
				
				case StressType.StrictlyUnstressed when maskStress == StressType.CanBeStressed:
					dist += MaskToWordMismatchPenalty;
					break;
			}
    }
	}

	return dist;
}

Финализация

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

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

Краевые случаи

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

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

Реализация

Библиотека на C# доступна с исходным кодом, как часть Nestor.

Практическую реализацию я сделал в виде смартапа в системе Сбер Салют.

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

Спасибо, что дочитали. В следующей статье мы поговорим про рифму, там всё ещё хитрее.

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


  1. sidristij
    14.02.2022 13:43
    +5

    Это очень круто! :)


    1. vectorplus
      14.02.2022 20:14

      Я лингвистам показал, они говорят - велосипед.

      Давно уже есть, довольно надежный для силлабо-тоники.


      1. Enfriz Автор
        14.02.2022 20:44
        +1

        С открытым исходным кодом? А дайте ссылку, хоть посмотрю.


      1. sidristij
        15.02.2022 10:34
        +2

        Ну велосипед - не велосипед, а всё, что связано с лингвистикой у меня трепет вызывает ))


        1. spacediver
          15.02.2022 11:46

          Напомнило: у нас на факультете висело объявление о спецкурсе:

          «Компьютерная лингвистка»


        1. vectorplus
          15.02.2022 17:42

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

          Я потому у лингвистов в чате и сижу, хотя сам отношения не имею ????


  1. emaxx
    14.02.2022 13:55

    А у слова зеленый есть два варианта: собственно, зелёный цвет, и слово из поговорки молодо-зелено.

    Но ведь слова "зЕленый" же нет?


    1. Enfriz Автор
      14.02.2022 14:04

      И правда. Был уверен, что это ошибка словаря, а это ошибка моя. Сейчас исправлю и обновлю статью, спасибо )


  1. emaxx
    14.02.2022 14:17

    А как подбирались коэффициенты "+2 к штрафу" и "+5 к штрафу"? Есть ли контрпримеры? (Длинные слова, в которых алгоритм ошибочно переставляет ударение?)


    1. Enfriz Автор
      14.02.2022 14:24

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


  1. M_AJ
    14.02.2022 14:30
    +2

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

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


    1. Enfriz Автор
      14.02.2022 14:39
      +3

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


  1. XaBoK
    14.02.2022 15:22
    +2

    А почему задача ставится как: "определение размера по одной строке"? Разве не лучше будет анализировать, допустим, четверостишье? Так можно выбрать мин. дистанцию из 4х вариантов и определить размер более точно. Можно даже обратный тест провести - зная размер проанализировать строки на противоречие выбранной парадигме.


    1. Enfriz Автор
      14.02.2022 15:50

      Такой метод в библиотеке тоже есть, и я о нём скользь упомянул. Сейчас я просто считаю наибольшее число совпадений по строкам. Но с близостью тоже можно.

      Я взял задачу по строке потому что использовал голосовой ввод. А в нём непонятно где заканчивается одна строка и начинается другая, если читать четверостишье. Хотя можно оценить примерно по числу слогов. Но четверостишье уже длинновато для одной голосовой команды.


  1. M_AJ
    14.02.2022 15:33

    Ну тут вопрос в том, считать ли размером отсутствие размера.

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


  1. vya
    14.02.2022 15:45
    +10

    Скажите, будет ли нейронка?

    А то лениво день за днём

    В стихах выравнивать колонки,

    И наблюдать, как пальцы гнём.


    1. Enfriz Автор
      14.02.2022 15:52
      +7

      Персонально я не фанат ML и стараюсь решать как можно больше задач прямым алгоритмическим подходом.

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

      А четверостишье прикольное :)


  1. inkoziev
    14.02.2022 18:18
    +2

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

    два лингвиста споря тво́рог иль творо́г
    мордами друг друга били об порог


  1. perlestius
    14.02.2022 18:53
    +3

    138 5 15
    12 8 45
    17 19 20
    4 225


    1. vectorplus
      14.02.2022 20:17
      +2

      Самый обычный четырехстопный ямб


      1. Browning
        15.02.2022 01:05

        Пиррихированный.


  1. Aleshonne
    14.02.2022 23:26
    +2

    Интересно, что на этот стих алгоритм выдаст.

    Георгий Шенгели — Барханы

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

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

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


    1. Enfriz Автор
      15.02.2022 00:21
      +1

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


  1. sophist
    15.02.2022 03:31
    +2

    Продолжаем стишки:

    Сел на пол сдал по три.

    Фишек нет, есть мешки,

    А в мешках – словари!


  1. Yuriy_75
    15.02.2022 10:12

    >>В общем случае, если мы представим себе стихотворение только из односложных слов, то можем читать его любым размером!

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


  1. win32asm
    16.02.2022 06:39
    +2

    А есть смысл уменьшить штраф для безударной в ударном положении, если в этом же слове есть ударная в другом ударном положении?

    В примерах '... предадимся бегу...' это норм, а '... доел сорок яблок' на том же месте - очевидно нет.


    1. Enfriz Автор
      16.02.2022 12:15

      Логично, да. Как возьмусь за правки наверное учту это. Наверное нужно новый StressType вводить под такое.