Извлечение фактов из текста - типичная задача при работе с естественным языком. Ее постоянно решает Яндекс, например, когда выделяет время и место из полученного письма и предлагает внести событие в календарь. Подобные интересные задачи возникают сплошь и рядом в системах, которые напрямую связаны с естественным языком. Поэтому Яндекс разработали свой морфологический парсер и назвали его Томита. Он позволяет выделять факты в тексте, благодаря написанию своих грамматик и словарей. В этом посте мы разберемся как это работает на примере задачи выделения результатов матчей из футбольных новостей.
Для знакомства с Томита-парсером я собрал несколько десятков футбольных новостей из яндексовского RSS. Среди них есть как информация об околофутбольных событиях (например, трансферы или интервью со спортсменами), так и новости о результатах матчей. Вот небольшая вырезка из RSS:
Простым xslt я выделил все описания новостей (поле Description) в отдельный текстовый файл. Наша цель: выделить из всего множества статей только факты, содержащие названия двух команд и счет.
Сразу скажу, что на сайте Яндекса есть очень подробная документация, а также короткий видеокурс, благодаря которому меньше чем за час можно разобраться в основных принципах работы парсера.
В парсере имеется три основных понятия:
Эти сущности описываются в разных файлах и мы постепенно будем их создавать. Начнем со словаря.
Для описания словаря нужно создать файл с расширением gzt. Этот файл является обязательным и без него парсер работать не будет (другим таким файлом является конфигурационный, но о нем несколько позже). Во-первых, все файлы, где используется русский текст, необходимо начинать с явного определения кодировки utf8. Далее необходимо импортировать служебные файлы. После того как мы создадим файлы со своими типами и фактами, мы импортируем и их.
Теперь мы создадим несколько так называемых статей, в которых определим наборы глаголов, описывающих результаты матчей. Для этого введем новый тип статей - result_verb. Поле key определяет какие глаголы входят в статью. Поле lemma позволяет заменить найденную цепочку, на ту, которая указана в этом поле.
Для определения новых типов статей нужно создать новый файл. В нем после служебных импортов с помощью ключевого слова message указываются новые типы. При этом они должны наследоваться от базового типа TAuxDicArticle. Код файла kwtypes_football.proto:
После создания файла его нужно импортировать в наш словарь:
Теперь перейдем к созданию грамматики.
Создадим еще один файл - football_result.cxx. В нем мы опишем набор правил, используемых для распознавания цепочек. Правила состоят из левой и правой частей, разделенных символом ->. В левой части всегда стоит один нетерминал. В правой - последовательность терминалов и нетерминалов. В роли терминалов выступают либо конкретные леммы, заключенные в кавычки, либо предопределенные парсером ключевые слова. Например: Noun (существительное), Word (любое слово), Punct (точка) и др.
Для более тонкой настройки терминалов и нетерминалов, такой как, например, определение регистра символов или связей по падежу между двумя словами, используются пометы. Они приписываются после (не)терминалов в треугольных скобках через запятую. Полный список помет есть в документации. Здесь я опишу несколько из них, которые будут использованы в нашей грамматике:
Еще одна ключевая возможность при написании правил заключается в применении операторов к не(терминалам). Оператор | позволяет указывать множественный выбор в правой части правил, * означает, что символ встречается ноль или более раз, а оператор + означает появление символа один или более раз. Теперь нам хватит теории, чтобы описать свою грамматику для распознавания цепочек с результатами матчей.
Первоначально создадим правила для распознавания названий команд:
Первое правило относится к национальным сборным. В новостях их, как правило, записывают в одном из двух форматов, на примере нашей Родины: либо Россия, либо сборная России. Соответственно, мы ищем слова, являющиеся географическими объектами и пары, которые согласуются по падежу. Второе и третье правило относятся к клубам. Названия клубов пишут в кавычках и с большой буквы, при этом они могут состоять из нескольких слов. Нетерминал Team будем использовать для распознавания всех типов команд. Теперь добавим правило для глагола, из нашего словаря:
Кроме того, нам нужен нетерминал для распознавания счета. Для этого будем использовать регулярное выражение:
Нам осталось определить корневой нетерминал и правила, которые соберут вместе те нетерминалы, что мы уже создали. Корневой нетерминал назовем S и выпишем несколько возможных вариантов построения предложения:
После создания грамматики нужно сослаться на нее в словаре:
Теперь мы можем находить цепочки слов в тексте, но мы еще не научились выделять из них важную для нас информацию. Для этого нужно создать еще один файл - facttypes.proto. В нем мы определим новый тип фактов ResultFact и унаследуем его от NFactType.TFact. Среди атрибутов факта будут использоваться названия команд, счет и слово, описывающее результат (для демонстрации работы нормализации обнаруженных лемм). Атрибуты могут быть обязательными и опциональными:
Теперь немного изменим нашу грамматику, добавив в нее интерпретацию фактов. Для этого используется ключевое слово interp. К выводу фактов можно применять граммемы. Воспользуемся этим для вывода результата в именительном падеже, единственном числе:
Без этого файла работа парсера невозможна. Он содержит информацию обо всех необходимых сущностях и передается программе как единственный аргумент. В полях конфига указываются словарь, грамматика и факты. Также можно определить отличные от stdin и stdout вход и выход. С помощью PrettyOutput генерируется html-файл с удобным для чтения форматом вывода совпавших цепочек и обнаруженных фактов.
Теперь все готово для запуска парсера. Вот список строк в файле articles.txt, содержащих какую-либо информацию о результатах матчей:
А вот такие факты были обнаружены и отображены в файле results.html:
ResultFact | |||||||||
FirstTeam | Result | SecondTeam | Score | ||||||
---|---|---|---|---|---|---|---|---|---|
сборная Бразилии | победа | Япония | 3:1 | ||||||
польша | ничья | Уругвай | 0:0 | ||||||
Бельгия | Мексика | 3:3 | |||||||
Марсель | Виктория | 0:1 | |||||||
сборная России | поражение | Аргентина | 0:1 | ||||||
Сокол | поражение | Красногорск | 1:5 | ||||||
сборная России | поражение | Румыния | 1:2 | ||||||
сборная России | Катар | 1:1 |
Как видите, часть записей не была обработана, так как не содержала необходимой информации для парсера, а часть была обработана не совсем так как мы хотели. Однако, в целом результаты получились достаточно пристойными.
Извлечение фактов из естественного языка является совсем нетривиальной задачей в мире IT. Томита-парсер предоставляет возможности для элегантного решения этой непростой проблемы. Тем не менее, все равно крайне сложно написать универсальную грамматику, которая разберется во всевозможных вариантах входных последовательностей. Поэтому работа с естественными языками по-прежнему остается достаточно сложной.
Written on November 11th, 2017 by Alexey Kalina