![](https://habrastorage.org/webt/iq/f_/fg/iqf_fgkfyqb1rbtwvba0p4gbsiq.png)
Это одна из ключевых и классических задач Natural Language Processing (NLP). Она заключается в создании алгоритма, который принимает на вход текст и на выходе выдаёт его сокращённую версию. Причем в ней сохраняется корректная структура (соответствующая нормам языка) и правильно передается основная мысль текста.
Такие алгоритмы широко используются в индустрии. Например, они полезны для поисковых движков: с помощью сокращения текста можно легко понять, коррелирует ли основная мысль сайта или документа с поисковым запросом. Их применяют для поиска релевантной информации в большом потоке медиаданных и для отсеивания информационного мусора. Сокращение текста помогает в финансовых исследованиях, при анализе юридических договоров, аннотировании научных работ и многом другом. Кстати, алгоритм саммаризации сгенерировал и все подзаголовки для этого поста.
К моему удивлению, на Хабре оказалось совсем немного статей о саммаризации, поэтому я решил поделиться своими исследованиями и результатами в этом направлении. В этом году я участвовал в соревновательной дорожке на конференции «Диалог» и ставил эксперименты над генераторами заголовков для новостных заметок и для стихов с помощью нейронных сетей. В этом посте я вначале вкратце пробегусь по теоретической части саммаризации, а затем приведу примеры с генерацией заголовков, расскажу, какие трудности возникают у моделей при сокращении текста и как можно эти модели улучшить, чтобы добиться выдачи более качественных заголовков.
Ниже — пример новости и ее оригинального эталонного заголовка. Модели, о которых я буду рассказывать, будут тренироваться генерировать заголовки на этом примере:
![image](https://habrastorage.org/webt/8n/yy/gc/8nyygcx_ymy38dwrqpbqdiylvsu.png)
Секреты сокращения текста архитектуры seq2seq
Выделяют два типа методов сокращения текста:
- Извлекающий. Он заключается в нахождении наиболее информативных частей текста и конструировании из них корректной для данного языка аннотации. Эта группа методов использует только те слова, которые есть в исходном тексте.
- Абстрактный. Заключается в извлечении смысловых связей из текста, при этом учитываются языковые зависимости. При абстрактной саммаризации слова аннотации выбираются не из сокращаемого текста, а из словаря (списка слов для данного языка) — тем самым происходит перефразирование основной мысли.
Второй подход подразумевает, что алгоритм должен учитывать языковые зависимости, перефразировать и обобщать. Ещё ему желательно иметь какие-то знания о реальном мире, чтобы не допускать фактических ошибок. Долгое время это считалось сложной задачей, и исследователи не могли получить качественного решения — грамматически верного текста с сохранением основной мысли. Именно поэтому в прошлом большинство алгоритмов основывалось на извлекающем подходе, так как выделение цельных кусков текста и перенос их в результат позволяет сохранить тот же уровень грамотности, что и у источника.
Но так было до бума нейронных сетей и его неминуемого проникновения в NLP. В 2014 году была представлена архитектура seq2seq с механизмом внимания, способная читать одни текстовые последовательности и генерировать другие (какие именно – зависит от того, что училась выводить модель) (статья от Sutskever et al.). В 2016 году такую архитектуру применили непосредственно к решению задачи саммаризации, тем самым реализовав абстрактный подход и получив результат, сравнимый с тем, что мог бы написать грамотный человек (статья от Nallapati et al., 2016; статья от Rush et al., 2015;). Как же устроена эта архитектура?
![image](https://habrastorage.org/webt/px/4x/wi/px4xwin577dd65z-lahsx7fvexs.png)
Seq2Seq состоит из двух частей:
- кодировщика (Encoder) – это двухсторонний RNN, который используется для чтения входной последовательности, то есть последовательно обрабатывает элементы входа одновременно слева направо и справа налево для лучшего учёта контекста.
- декодировщика (Decoder) – односторонний RNN, который последовательно и поэлементно выдаёт выходную последовательность.
Сначала входная последовательность переводится в последовательность эмбеддингов (если вкратце, то эмбеддинг — сжатое представление о слове в виде вектора). Затем эмбеддинги проходят через рекуррентную сеть кодировщика. Так мы на каждое слово получаем скрытые состояния кодировщика (обозначены на схеме красными прямоугольниками), а они содержат в себе информацию как о самом токене, так и о его контексте, позволяя учитывать языковые связи между словами.
Обработав вход, кодировщик передаёт своё последнее скрытое состояние (которое содержит сжатую информацию обо всём тексте) декодировщику, который принимает на вход специальный токен
![image](https://habrastorage.org/webt/qn/ud/38/qnud38u15vpvzie4zkne-doze7k.png)
![image](https://habrastorage.org/webt/vw/kv/4g/vwkv4gs-ul7vlvnkjwxag8njicw.png)
Чтобы вывести очередной элемент, декодировщик точно так же, как и кодировщик, преобразует входной токен в эмбеддинг, делает шаг рекуррентной сети и получает очередное скрытое состояние декодировщика (жёлтые прямоугольники на схеме). Затем из него при использовании полносвязного слоя получается распределение вероятности по всем словам из заранее составленного словаря модели. Наиболее вероятные слова и будут выведены моделью.
Добавление механизма внимания помогает декодировщику лучше использовать информацию о входном тексте. Механизм на каждом шаге генерации определяет так называемое распределение внимания (синие прямоугольники на рисунке — множество весов, соответствующих элементам исходной последовательности, сумма весов равна 1, все веса >= 0), а из него получает взвешенную сумму всех скрытых состояний кодировщика, тем самым образуя вектор контекста (на схеме — красный прямоугольник с синей обводкой). Этот вектор конкатенируется с эмбеддингом входного слова декодировщика на этапе вычисления скрытого состояния и с самим скрытым состоянием на этапе определения следующего слова. Так на каждом шаге вывода модель может определить, какие состояния кодировщика ей важнее всего на данный момент. Иначе говоря, решает, контекст каких слов входа нужно учитывать больше всего (например, на картинке, выводя слово «beat», механизм внимания даёт большие веса токенам «victorious» и «win», а остальным – близкие нулю).
Так как генерация заголовков тоже одна из задач саммаризации, только с минимально возможным выводом (1-12 слов), то я решил применить seq2seq с механизмом внимания и для нашего случая. Обучаем такую систему на текстах с заголовками, например, на новостях. Причём желательно на этапе обучения подавать на вход декодировщику не его собственный выход, а слова реального заголовка (приём teacher forcing), облегчая жизнь себе и модели. В качестве функции ошибки используем стандартную кросс-энтропийную функцию потерь, показывающую, насколько близки вероятностные распределения выходного слова и слова из реального заголовка:
![](https://habrastorage.org/webt/3v/co/b9/3vcob9esvdhgsydjcnjfpzv2wbi.png)
При использовании обученной модели применяем лучевой поиск для нахождения более вероятной последовательности слов, чем при использовании жадного алгоритма. Для этого на каждом шаге генерации выводим не самое вероятное слово, а одновременно смотрим на beam_size наиболее вероятных последовательностей слов. Когда они закончатся (каждый из них окончится на
![image](https://habrastorage.org/webt/vw/kv/4g/vwkv4gs-ul7vlvnkjwxag8njicw.png)
![image](https://habrastorage.org/webt/t2/dt/ni/t2dtnicefjxn0hwd0elaeduekbk.png)
![](https://habrastorage.org/webt/yx/ln/q-/yxlnq-7z2-lwuyylmz83pferopm.png)
![](https://habrastorage.org/webt/ey/81/9u/ey819uvi_jhx4wbwchr-wsixvgs.png)
![](https://habrastorage.org/webt/q1/tn/zu/q1tnzuopjtdslrltsmn8euup-kq.png)
Эволюция модели
Одна из проблем модели на seq2seq – это невозможность привести слова, которых нет в словаре. Например, у модели нет никаких шансов вывести «obamacare» из статьи выше. То же самое касается:
- редких фамилий и имён,
- новых терминов,
- слов на других языках,
- различных пар слов, соединённых дефисом (как «сенатор-республиканец»)
- и прочих конструкций.
Конечно, можно расширить словарь, но это сильно увеличивает количество обучаемых параметров. Кроме того, нужно обеспечить большое количество документов, в которых встречаются эти редкие слова, чтобы генератор научился качественно их использовать.
Другое и более элегантное решение данной проблемы было представлено в статье 2017 года – «Get To The Point: Summarization with Pointer-Generator Networks» (Abigail See et al.). Она добавляет новый механизм в нашу модель – механизм указывания (pointer), который умеет выбирать слова из исходного текста и непосредственно вставлять в генерируемую последовательность. Если в тексте встречается OOV (out of vocabulary – слово, отсутствующее в словаре), то модель, если она посчитает это нужным, может вычленить OOV и вставить его на выходе. Такая система называется «указыватель-генератор» (pointer-generator или pg) и представляет собой синтез сразу двух подходов к саммаризации. Она сама может решать, на каком шаге ей быть абстрактной, а на каком – извлекающей. Как она это делает, мы сейчас и разберемся.
![image](https://habrastorage.org/webt/f-/9y/xw/f-9yxwborbgpjpalzwd5e_f74xi.png)
Основное отличие от модели на обычном seq2seq заключается в дополнительном действии, на котором вычисляется pgen – вероятность генерации. Это делается с использованием скрытого состояния декодера и контекстного вектора. Смысл дополнительного действия прост. Чем ближе pgen к 1, тем более вероятно, что модель выдаст слово из своего словаря, используя абстрактную генерацию. Чем ближе pgen к 0, тем более вероятно, что генератор извлечёт слово из текста, руководствуясь полученным ранее распределением внимания. Итоговое распределение вероятности выдачи слов складывается из сгенерированного распределения вероятности по словам (в котором нет OOV), домноженного на pgen, и распределения внимания (в котором OOV есть, например, «2-0» на картинке), домноженного на (1 — pgen).
![image](https://habrastorage.org/webt/iv/8f/8q/iv8f8qvol1j3bbyx79vt5zo7-oq.png)
![](https://habrastorage.org/webt/if/ew/s3/ifews3gqgojf5mclbxd0lpkz2q8.png)
Кроме механизма указывания в статье вводится механизм покрытия (coverage), который помогает избежать повторения слов. Я также проводил эксперименты с ним, но не заметил значительных улучшений в качестве заголовков – он не очень-то и нужен. Скорее всего, это происходит из-за специфики задачи: так как выводить надо небольшое количество слов, генератор просто не успевает повторить себя самого. Зато для других задач саммаризации, например, аннотирования, он может пригодиться. Если интересно, то можете прочесть о нём в оригинальной статье.
![image](https://habrastorage.org/webt/4q/bx/op/4qbxopqt862cphikmbezyaoadlk.png)
![](https://habrastorage.org/webt/--/3m/mz/--3mmzahhdk8kelwoqyh9rdjg8e.png)
Великое многообразие русских слов
Ещё один способ улучшить качество выдаваемых заголовков — это правильный препроцессинг входной последовательности. Кроме очевидного избавления от прописных символов, я также пробовал преобразовывать слова из исходного текста в пары стем и флексий (т.е. основ и окончаний). Для разбиения используем стеммер Портера.
![](https://habrastorage.org/webt/jy/j8/-y/jyj8-yonzpmvpyoye6mbygrlcjg.png)
![](https://habrastorage.org/webt/_k/__/vy/_k__vyhouqm_ctxpsp8ukcselwe.png)
Помечаем все флексии символом «+» в начале, чтобы отличать их от других токенов. Рассматриваем каждую стему и флексию как отдельное слово и обучаемся на них точно так же, как и на словах. То есть получаем из них эмбеддинги и выводим последовательность (так же разбитую на основы и окончания), которую можно легко превратить в слова.
Такое преобразование очень полезно в случае работы с морфологически богатыми языками, как русский. Вместо того чтобы составлять огромные словари с великим многообразием русских словоформ, можно ограничиться большим количеством основ этих слов (их в несколько раз меньше, чем количество словоформ) и совсем маленьким набором окончаний (у меня получилось множество из 450 флексий). Тем самым мы облегчаем модели работу с этим «богатством» и при этом не повышаем сложность архитектуры и количество параметров.
![image](https://habrastorage.org/webt/a4/es/s5/a4ess5qr3vaxprksv7avun3obl8.png)
![](https://habrastorage.org/webt/3d/rk/6h/3drk6hntzcow2i3soi-heul7ckq.png)
Также я пробовал использовать преобразование лемма+граммемы. То есть из каждого слова перед обработкой можно получить его начальную форму и грамматическое значение с помощью пакета pymorphy (например, «была»
![image](https://habrastorage.org/webt/95/wb/-a/95wb-aopfalycfvrshqfynxnwd4.png)
Я экспериментировал с еще одним добавлением в оригинальную архитектуру указывателя-генератора, которое, правда, не относится к препроцессингу. Это увеличение количества слоёв (до 3-х) рекуррентных сетей кодировщика и декодировщика. Увеличение глубины рекуррентной сети может повысить качество выдаваемых результатов, так как скрытое состояние последних слоёв может содержать информацию о гораздо более длинной подпоследовательности входа, чем скрытое состояние однослойного RNN. Это способствует учёту сложных протяжённых смысловых связей между элементами входной последовательности. Правда, это обходится значительным увеличением числа параметров модели и усложняет обучение.
![image](https://habrastorage.org/webt/cf/7g/ej/cf7gejf-hxd5pbscvqgnvuuviqy.png)
![](https://habrastorage.org/webt/4g/ww/zy/4gwwzytsrt506_xllgy5xcis6vk.png)
Эксперименты с генераторами заголовков
Все мои эксперименты над генераторами заголовков можно разделить на два типа: эксперименты с новостными статьями и со стихами. Расскажу о них по порядку.
Эксперименты с новостями
При работе с новостями я применял такие модели, как seq2seq, pg, pg со стемами и флексиями – однослойный и трёхслойный. Я также рассматривал модели, работающие с граммемами, но всё, что я хотел о них поведать, я уже описал выше. Сразу оговорюсь, что все описываемые в этом разделе pg использовали механизм покрытия, хотя его влияние на результат сомнительно (так как без него было не намного хуже).
Тренировался я на датасете РИА «Новости», который предоставило новостное агентство «Россия сегодня» для проведения дорожки по генерации заголовков на конференции «Диалог». Датасет содержит 1 003 869 новостных статей, опубликованных с января 2010 года по декабрь 2014-го.
Все исследуемые модели использовали один и тот же размер эмбеддингов (128), словаря (100k) и скрытого состояния (256) и тренировались в течение одинакового количество эпох. Поэтому повлиять на результат могли лишь качественные изменения в архитектуре либо в препроцессинге.
Модели, заточенные на работу с предобработанным текстом, дают более хорошие результаты, чем модели, работающие со словами. Лучше всего справляется трёхслойный pg, использующий информацию о стемах и флексиях. При использовании любого pg также проявляется ожидаемое улучшение качества заголовков по сравнению с seq2seq, что намекает нам на предпочтительное использование указателя при генерации заголовков. Вот и пример работы всех моделей:
![](https://habrastorage.org/webt/6w/4u/0r/6w4u0ruudngxtt2szek31w0achq.png)
Глядя на генерируемые заголовки, можно выделить у исследуемых моделей такие проблемы:
- Модели часто используют неправильные формы слов. От этого недостатка в большей степени избавлены модели со стемами (как в примере выше);
- Все модели, кроме тех что работают со стемами, могут выдавать заголовки, которые кажутся незаконченными, либо странные конструкции, которых нет в языке (как в примере выше);
- Все исследуемые модели часто путают описываемых персон, подставляют неправильные даты или используют не совсем подходящие слова.
![image](https://habrastorage.org/webt/9c/-b/ot/9c-bote3bwomvfu_3bwaixhqnks.png)
![](https://habrastorage.org/webt/e-/3c/px/e-3cpxuknykthyu-xvf-cp-rdsi.png)
![image](https://habrastorage.org/webt/lf/ry/ep/lfryepksl4kmcqxttb14ikgx4wg.png)
![](https://habrastorage.org/webt/b3/4m/fw/b34mfwpsffud2kaa93tccjvyja0.png)
Эксперименты со стихами
Так как у трехслойного pg со стемами меньше всего неточностей в генерируемых заголовках, именно эту модель я выбрал для экспериментов со стихами. Я обучал её на корпусе, составленном из 6 миллионов русских стихотворений с сайта «stihi.ru». Они включают любовную (этой теме посвящены примерно половина стихов), гражданскую (примерно четверть), городскую и пейзажную поэзию. Период написания: январь 2014 — май 2019. Приведу примеры генерируемых заголовков для стихов:
![](https://habrastorage.org/webt/yv/mo/da/yvmodarkck7cgra-ymxigffwhik.png)
![](https://habrastorage.org/webt/3u/mb/fn/3umbfnpzo1hwhqy0_glxyhxynu4.png)
![](https://habrastorage.org/webt/tp/tk/5e/tptk5eoq9wa1xrnj9w0gewjeayk.png)
Модель получилась в основном извлекающей: почти все заголовки представляют собой одну строку, зачастую извлечённую из первой или последней строфы. В исключительных случаях модель может сгенерировать слова, которых в стихотворении нет. Это объясняется тем, что очень большое количество текстов в корпусе действительно имеют в качестве названия одну из строк.
В качестве заключения скажу, что указыватель-генератор, работающий на стемах и использующий однослойные декодировщик и кодировщик, занял второе место на соревновательной дорожке по генерации заголовков для новостных статей на научной конференции по компьютерной лингвистике «Диалог». Основной организатор этой конференции — ABBYY, компания занимается исследованиями практически по всем современным направлениям Natural Language Processing.
Напоследок, предлагаю вам небольшой интерактив: присылайте в комментарии новости, и посмотрим, какие заголовки для них сгенерирует нейросеть.
Матвей, разработчик в NLP Group в ABBYY