Привет, Хабр. Я последние пару лет играюсь с естественной речью на русском языке. Решил поделиться своим опытом по работе с поэзией. Будет две статьи: вот эта и про рифму (когда дойдут руки всё доделать).
Половина программистов, прочитав заголовок, скорее всего подумала, что задача очень простая: сравнить две маски. Но есть нюансы, которые сильно влияют на результат, и о них то я и расскажу.
Немного теории
Речь пойдёт о так называемом силлабо-тоническом стихосложении — то есть буквально таком способе написания стихотворений, в котором ударные и безударные слоги чередуются с заданной ритмичностью. Большинство из нас именно такой стих представляет себе в первую очередь, когда слышит о поэзии. Этот стиль является одним из самых распространённых — если вообще не самым — особенно в произведениях на русском языке. И он достаточно математичен, чтобы поддаваться алгоритмизации.
Буря мглою небо кроет
| – | – | – | –
Следует обозначить отличия от обычного тонического стихосложения, которое тоже используется очень часто. В современном языке мы можем услышать такие тексты в рэпе — между ударными слогами в общем случае произвольное число безударных, которые все вместе проговариваются на одну или более безударные музыкальные доли.
Моё море, прошу тебя, не выплюни меня на берег
– | | – – | – | – | – – – | – | –
Считать ли поэзией, например, верлибр — вопрос дискуссионный и выходящий за рамки данного текста. Как писал Томас Элиот: «Автор верлибра свободен во всём, если не считать необходимости создавать хорошие стихи».
Стихотворные размеры
Мы возьмём классические пять силлабо-тонических размеров, которые многие наверняка проходили в школе по литературе (а ведь учитель вам говорил, что это пригодится в жизни!). Два двухсложных и три трёхсложных.
Название размера |
Схема |
Пример |
Хорей |
| – | – | – | – |
Буря мглою небо кроет |
Ямб |
– | – | – | – | |
Товарищ, верь: взойдёт она |
Дактиль |
| – – | – – |
Тучки небесные, вечные странники |
Амфибрахий |
– | – – | – |
Есть женщины в русских селеньях |
Анапест |
– – | – – | |
На заре ты её не буди |
Необязательные детали
Если зарываться в стихосложение далее, то в некоторых языках (но не русском) длительность, с которой вы произносите тот или иной гласный звук, является значимой и влияет на восприятие текста. Этот факт использует, например, гекзаметр — размер, которым написаны Илиада и Одиссея. С точки зрения русского языка он неотличим от дактиля, но на древнегреческом есть особенности в чередовании долгих и кратких слогов. Такие размеры в целом называются силлабо-метрическими.
Навскидку задача и правда выглядит простой: мы должны расставить ударения в исходной строке и сравнить её с одной из пяти схем. Где совпало — там и размер.
Ударения
Некоторое время назад я написал библиотеку Nestor на основе найдённого когда-то в сети словаря под авторством Михаила Хагена, который является переработанным словарём словоформ Зализняка. Эта библиотека по смыслу и по устройству похожа на pymorphy2, но написана на C# и содержит в себе ударения, что стало для меня ключевой причиной того, зачем я вообще её делал. В репозитории есть словарь, если вы захотите переписать её на свой любимый язык.
В русском языке есть омонимы с ударениями на разные слоги. Если человек написал с клавиатуры слово «замок», мы не знаем, что именно он имел ввиду: замок — средневековая крепость, или замок — устройство для блокировки двери. Заставлять пользователя размечать ударения вручную это примерно то же самое, что заставлять его самому назвать стихотворный размер — не наш путь. Кстати, при работе с навыками для голосовых ассистентов (Алиса, Салют, Маруся) формально у системы есть данные о том, на какой слог человек сделал ударение, но по факту эти данные не выдаются, и разработчику навыка просто приходит такое же обезличенное слово, как если бы ввод был текстовым.
Итак, у любого слова может быть некоторое число возможных ударений. Минимально 1 — когда ударение совершенно точно известно, и нет никаких других омонимов (частным случаем этого пункта являются слова с буквой ё). Максимально — по числу слогов в слове, когда ударение, например, вообще неизвестно, слово отсутствует в словаре, тогда стоит предположить, что ударным может быть любой слог.
Интересный факт про ударения
Всем известны слова с двумя ударениями, такие, как замок и замок. Я задумался, а бывает ли с тремя? Оказалось да, таких слов около пятнадцати. Вот, например, слово вывозите может иметь ударения на любом из первых трёх слогов:
Вы сейчас вывозите себя в этой грязи, перестаньте!
А по каким дням вы вывозите мусор?
Эй, вы двое, вывозите отсюда все свои вещи!
Круто, да? Ещё пример такого слова: округа, может иметь ударение на любом из своих слогов. Можете сами поприкидывать варианты в качестве упражнения.
Слов более чем с тремя ударениями в словаре Хагена-Зализняка не нашлось.
В общем, первая сложность:
Возможное ударение в слове может приходиться на несколько слогов.
Односложные слова
Другой интересной особенностью поэзии является наше свободное обращение с ударениями односложных слов. Формально, если в слове один слог, то он и есть ударный. Фактически же такие слова спокойно могут находиться как в ударном, так и в безударном положении. Люди сами модифицируют произношение фразы так, чтобы либо делать акцент на слове, либо проглатывать его. Причём, я сначала подумал, что это относится только к вспомогательным частям речи: предлогам, союзам и частицам. Но быстро нашёл примеры обратного.
Мороз и солнце; день чудесный!
Ещё ты дремлешь, друг прелестный —
<...>
Скользя по утреннему снегу,
Друг милый, предадимся бегу
Это классический очень распространённый в русской поэзии пятистопный ямб с сокращённой последней стопой. Слова день и друг под ударной позицией в первых двух строках, но в последней слово друг уже безударное. Конкретно Пушкин почти всегда старался ставить вне ударения как раз предлоги и частицы, но даже у него есть исключения.
В общем случае, если мы представим себе стихотворение только из односложных слов, то можем читать его любым размером! Давайте за основу возьмём строчку Сел на пол сдал по три (я её выдумал, попытавшись составить что-то осмысленное из слов в один слог).
Вот ямбом:
Рубился я в картишки,
Сел на пол сдал по три,
За ними были фишки,
А после — пузыри.
А вот амфибрахием:
Играем в картишки,
Сел на пол, сдал по три,
Потом будут фишки,
А дальше — посмотрим.
Прочитайте вслух, так будет понятнее. Да простят меня настоящие поэты.
Таким образом, вторая сложность:
Односложные слова могут быть и в ударной и в безударной позиции.
Алгоритм
Мы принимаем на вход текстовую строку, полагая, что это один стих — то есть строка стихотворения. Затем нам нужно оценить её близость к каждому из пяти рассматриваемых размеров.
Для начала разметим строку. Для каждого слова вернём массив 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
тем ближе). Изначально равно нулю. Далее при сопоставлении слогов возможны такие варианты:
В маске слог |
В маске слог |
|
В строке слог |
Ударный слог в ударной доле, полное совпадение. Не штрафуем. |
Ударный слог не может попадать под безударную долю в размере. +5 к штрафу |
В строке слог |
Слог в строке может быть любым независимо от маски. Не штрафуем. |
Слог в строке может быть любым независимо от маски. Не штрафуем. |
В строке слог |
В ударной доле размера находится безударный слог строки. Возможно, но не слишком хорошо. +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)
emaxx
14.02.2022 14:17А как подбирались коэффициенты "+2 к штрафу" и "+5 к штрафу"? Есть ли контрпримеры? (Длинные слова, в которых алгоритм ошибочно переставляет ударение?)
Enfriz Автор
14.02.2022 14:24Ну, подобрал на ручных тестах. Наверное по хорошему нужно туда загрузить какой-то корпус стихотворений, но пока до этого не дошли руки.
M_AJ
14.02.2022 14:30+2Считать ли поэзией, например, верлибр — вопрос дискуссионный и выходящий за рамки данного текста
Мне кажется странным, что вокруг этого вопроса до сих пор существуют какие-то дискуссии. Если Блок, Маяковский и Бродский поэты, то почему вдруг размеры сложнее классических, и эксперименты с размерами перестают быть поэзией?
Enfriz Автор
14.02.2022 14:39+3Ну тут вопрос в том, считать ли размером отсутствие размера. К сожалению эта линия рассуждений может быстро привести к тому, чтобы считать абсолютно всё искусством.
XaBoK
14.02.2022 15:22+2А почему задача ставится как: "определение размера по одной строке"? Разве не лучше будет анализировать, допустим, четверостишье? Так можно выбрать мин. дистанцию из 4х вариантов и определить размер более точно. Можно даже обратный тест провести - зная размер проанализировать строки на противоречие выбранной парадигме.
Enfriz Автор
14.02.2022 15:50Такой метод в библиотеке тоже есть, и я о нём скользь упомянул. Сейчас я просто считаю наибольшее число совпадений по строкам. Но с близостью тоже можно.
Я взял задачу по строке потому что использовал голосовой ввод. А в нём непонятно где заканчивается одна строка и начинается другая, если читать четверостишье. Хотя можно оценить примерно по числу слогов. Но четверостишье уже длинновато для одной голосовой команды.
M_AJ
14.02.2022 15:33Ну тут вопрос в том, считать ли размером отсутствие размера.
Его ведь не обязательно должно не быть вовсе. Он может просто быть сложным и периодически меняться. В музыке это уже довольно давно норма жизни, размеры там уже не ограничиваюся двумя, тремя, и четремя четвертями. Авторы используют, если нужно, гораздо более экзотические метры, меняя их по ходу произведения, если этого требует художественная задача. Почему у литературоведов подобное обращение с метром вдруг стало вызывать какие-то вопросы мне решительно непонятно.
vya
14.02.2022 15:45+10Скажите, будет ли нейронка?
А то лениво день за днём
В стихах выравнивать колонки,
И наблюдать, как пальцы гнём.
Enfriz Автор
14.02.2022 15:52+7Персонально я не фанат ML и стараюсь решать как можно больше задач прямым алгоритмическим подходом.
Что касается разбиения фразы на строки, если я правильно понял ваш вопрос — это тоже можно алгоритмически решить с высокой точностью. Но я пока таким не занимался.
А четверостишье прикольное :)
inkoziev
14.02.2022 18:18+2Отлично, верно отработала первая строчка из моего любимого тестового двустрочника (хотя пришлось челюсти размять, чтобы asr не менял иль на или):
два лингвиста споря тво́рог иль творо́г мордами друг друга били об порог
perlestius
14.02.2022 18:53+3138 5 15
12 8 45
17 19 20
4 225
Aleshonne
14.02.2022 23:26+2Интересно, что на этот стих алгоритм выдаст.
Георгий Шенгели — Барханы
Безводные золотистые пересыпчатые барханы
Стремятся в полусожженную неизведанную страну,
Где правят в уединении златолицые богдыханы,
Вдыхая тяжелодымную златоопийную волну.Где в набережных фарфоровых императорские каналы
Поблескивают, переплескивают коричневой чешуей,
Где в белых обсерваториях и библиотеках опахалы
Над рукописями ветхими — точно ветер береговой.Но медленные и смутные не колышатся караваны,
В томительную полуденную не продвинуться глубину.
Лишь яркие золотистые пересыпчатые барханы
Стремятся в полусожженную неизведанную страну.Enfriz Автор
15.02.2022 00:21+1Спасибо, узнал новый размер для себя! Теоретически можно очень быстро расширить алгоритм до способности определить этот размер, достаточно правильно маски посоставлять.
sophist
15.02.2022 03:31+2Продолжаем стишки:
Сел на пол сдал по три.
Фишек нет, есть мешки,
А в мешках – словари!
Yuriy_75
15.02.2022 10:12>>В общем случае, если мы представим себе стихотворение только из односложных слов, то можем читать его любым размером!
Это объясняет, почему очень легко добиться нужного размера на английском. Много односложных слов, они и выручают.
win32asm
16.02.2022 06:39+2А есть смысл уменьшить штраф для безударной в ударном положении, если в этом же слове есть ударная в другом ударном положении?
В примерах '... предадимся бегу...' это норм, а '... доел сорок яблок' на том же месте - очевидно нет.
Enfriz Автор
16.02.2022 12:15Логично, да. Как возьмусь за правки наверное учту это. Наверное нужно новый StressType вводить под такое.
sidristij
Это очень круто! :)
vectorplus
Я лингвистам показал, они говорят - велосипед.
Enfriz Автор
С открытым исходным кодом? А дайте ссылку, хоть посмотрю.
sidristij
Ну велосипед - не велосипед, а всё, что связано с лингвистикой у меня трепет вызывает ))
spacediver
Напомнило: у нас на факультете висело объявление о спецкурсе:
«Компьютерная лингвистка»
vectorplus
Тут я полностью разделяю чувства! Сам всегда хотел заниматься NLP, даже читал какие-то туториалы по сентимент анализу, пытался скрейпить Фейсбук, но до дела так руки и не дошли, к сожалению.
Я потому у лингвистов в чате и сижу, хотя сам отношения не имею ????