Извлечение фактов с помощью Томита-парсера

В предыдущем посте мы узнали что обычно понимается под термином "Извлечение информации" и заодно рассмотрели формальные грамматики, которые могут быть полезны для этого самого извлечения информации. Теперь пора взять в руки парсер и сделать для него грамматику.

Как вы, наверное, уже догадались, использовать мы будем Томита-парсер – это такая реализация GLR-парсера от Яндекса. Томита-парсер подходит только для русского языка, есть аналоги для английского, например в составе системы GATE (и ещё парочку), но для русского альтернативы вроде как и нет.

вроде бы GATE поддерживает и русский, но подробностей я не знаю

Томита-парсер

Первые шаги

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

С докой на первый взгляд все хорошо – она есть и вроде бы довольно подробная, но с примерами всё очень плохо. Конечно, те два с половиной примера, что лежат на сайте Яндекса, дают какую-то информацию о парсере и о том как им пользоваться, но после них остается разрыв между "поняли принципы работы" и "пишем грамматику для извлечения фактов". И заполнить эту пустоту практически нечем (и это основная причина из-за которой я начал писать этот пост). В процессе изучения мне помогли две ссылки с более-менее реальными примерами, ищите эти ссылки в конце.

Теперь о задаче – в примерах от Яндекса есть вариант извлечения из текстов информации о фильмах, правда там нет никакого примера грамматики, но идея кажется интересной. Возьмем оттуда текст и расширим его текстами из википедии – у меня получилось это. Извлекать будем Название, Жанр, Режиссера и Дату выхода на экран.

Для начала создадим каркас проекта для томиты (или стырим из того же примера). Все файлы проекта в коммите.

Посмотрим в первую очередь на файл film.cxx – это наша основная грамматика, для извлечения фактов.

Для начала, напишем в этом файле S -> Adj Noun, потом запустим парсер – .\tomitaparser.exe config.proto и откроем файл output\output.html, в нем мы увидим как ловко парсер извлек все словосочитания прилагательное-существительное. Важно обратить внимание на то, что парсер разбивает предложения по точке, в результате чего "Звездные войны. Эпизод I" оказались двумя предложениями, а это значит что в одно поле "название" это никак не запихать, придется сделать некоторую предобработку и заменить точку на запятую в этих случаях.

Извлекаем названия

Теперь попробуем вытащить все названия фильмов – в наших примерах фильмы указаны в кавычках. Для того чтобы извлечь слова, стоящие в кавычках, достаточно в film.cxx указать S -> Word<h-reg1, quoted>;. Word значит что достаем просто любое слово, h-reg1 - что слово должно начинаться с большой буквы, а quoted - что оно должно стоять в кавычках. Но не все названия состоят из одного слова, расширим запись:

Title -> Word<h-reg1, quoted>;
Title -> Word<h-reg1, l-quoted> Word<r-quoted>;
Title -> Word<h-reg1, l-quoted> AnyWord+ Word<r-quoted>;
S -> Title<quoted>;

Вот только теперь мы получили проблемы с названиями вроде <"Заложница" и "Заложница 2">, для того, чтобы заставить парсер понимать, что это разные названия, добавим ограничение – внутреннее слово должно быть без кавычек Title -> Word<h-reg1, l-quoted, ~r-quoted> AnyWord<~r-quoted>+ Word<~l-quoted, r-quoted>;

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

Названия в кавычках извлекли, в результатах есть ложные срабатывания – разберемся с ними позже. А пока, можно сохранить названия как факты.

Для этого добавляем в facttypes.proto описание структуры для хранения фактов о фильме. Подключаем файл с фактами к конфигу и словарю mydic.gzt. А в film.cxx меняем последнее выражение на S -> Title interp(Film.Name);. Теперь название у нас извлекается не просто так, а ещё и интерпретируется как поле факта. Все необходимые изменения в коммите

Вроде всё уже не плохо, но многие названия ошибочно нормализованы - вместо "Неприкасаемые" у нас "Неприкасаемый" и другие ошибки. Скажем парсеру, что нормализовывать не надо - S -> Title interp(Film.Name::not_norm);

Убираем "ложные" фильмы

Теперь пора убрать "Оскар" и другие ложные срабатывания из списка названий фильмов. Давайте подумаем как человек мог бы отличить названия фильмов от не фильмов? Правильно, по тому, что у фильмов указано слово "фильм" или жанр. Давайте добавим в грамматику дескрипторы. В простом случае - это просто слово фильм.

FilmDescriptor -> "фильм";
S -> Title interp(Film.Name::not_norm) Hyphen* FilmDescriptor;

Теперь фильмами у нас считаются только те названия, после которых стоит слово "фильм". К сожалению теперь мы потеряли много валидных примеров. Расширим грамматрику до FilmDescriptor -> Adj* Word<wff=/фильм(-.+)?/>;. Теперь дескриптор содержит не только "фильм" но и все возможные сочетания "прилагательное-'фильм'", такие как "комедийный фильм". wff=/фильм(-.+)?/ означает, что дескриптор фильм может быть не строгим, а содержит ещё и все возможные варианты через дефис, например фильм-катастрофа.

Теперь добавим ещё и словарик с жанрами, чтобы уверено понимать что комедия - это тоже фильм. Пропишем словарь c жанрами в mydic.gzt, а в грамматике добавим Genre -> Word<kwtype="жанр">;. Теперь любое слово из словаря "жанр" будет интерпретировать как жанр. Добавим второй вариант дескриптора - FilmDescriptor -> Adj* Genre;. Подробности в коммите

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

Кстати, стоит добавить обработку случаев <фильм "Заложница" и "Заложница 2">, нужно поправить грамматику, чтобы дескриптор указывал не только на первый фильм. Попробуйте сами, если не получится, то можно подсмотреть в подсказку ;)

Извлекаем режиссера

Пора вернуться к Александру, первый фильм в списке не извлекается потому, что между дескриптором и названием стоит имя режиссера, что ж это распространённый случай, надо добавить его в грамматику:

Name -> Word<h-reg1, ~fw, nc-agr[1]> Word<h-reg1, nc-agr[1]>*;
Director -> Name<gram="род"> interp (Film.Director);
...
S -> FilmDescriptor (Director) FilmTitle;

Опишем что имя состоит из одного или большего количества слов с большой буквы, <~fw> указывает на то, что первое слово не должно быть первым словом предложения, иначе будут извлекаться не только имена. <nc-agr[1]> указывает что слова должны быть согласованы между собой по числу и падежу (number case). <gram="род"> означает что имя перед фильмом должно стоять в родительном падеже.

Чтобы извлечь режиссера Титаника, необходимо обработать ещё и дату, которая может стоять после названия. Обратите внимание, что мы описали отдельную грамматику для дат, и подключаем ее, так же как словарь Date -> AnyWord<kwtype="даты">;

И добавим обработку слова-дескриптора, которое указывает на режиссера - 'снятый'. Кроме того, "режиссер" тоже является хорошим дескриптором - изменения. У нас остается пример со звездными войнами, который можно пока решить хаком.

Извлечение дат премьер

В большом количестве случаев дата премьеры описана в отдельном предложении, поэтому нет возможности извлечь ее вместе с названием фильма (факт не делится на несколько предложений). Поэтому придется изменить required string Name = 1; на optional string Name = 1;. Теперь факт о фильме может быть без названия, зато с датой :) Видимо придется потом склеивать разные факты в один.

Для того чтобы извлечь премьеры надо написать простенькую грамматику:

Premiere -> "премьера" | "выход" "в" (Adj) "прокат";
PlacePrep -> "в" | "на";
Place -> PlacePrep (Word) Noun;
S -> Premiere (Word) Verb (Place) (Prep) Date interp (Film.Date);

Заключение

Теперь можно запустить парсер ещё раз и увидеть извлеченные факты о фильмах. Все уже неплохо! Надо только разобраться с третий фильм, чтобы исключить числительные, исправим строчку GenreDescr -> Word<kwtype="жанр"> | Word<wff=/фильм-.+/> | Adj<gram='A'> "фильм";

В итоге у нас получилось следующее:

Name Genre Director Date
Александр Оливер Стоун
Титаник фильм-катастрофа Джеймс Кэмерон 1997 года
Неприкасаемые трагикомедийный фильм 2011 года
Звёздные войны , Эпизод I : Скрытая угроза эпический приключенческий фильм Джордж Лукас
19 мая 1999 года
Интерстеллар научно-фантастический фильм Кристофер Нолан 26 октября 2014 года
Бёрдмэн американская чёрная комедия Алехандро Г. Иньяррит
17 октября 2014 года
Марсианин Ридль Скотт
Заложница 3 боевик Оливье Мегатон
Заложница
Заложница 2
16 декабря 2014 года
2015 году
Пятый элемент фантастический боевик Люк Бессон 7 мая 1997 года
Стражи Галактики американский приключенческий фильм Джеймс Ганн 2014 года
21 июля 2014 года
Стражи Галактики , Часть 3
Железный человек 3 фантастический боевик Шейн Блэк
24 апреля 2013 года
Голодные игры Гэри Росс
12 марта 2012 года

Финальная версия кода

Мы добились достаточно хорошего результата небольшим количеством усилий, конечно нам потребовалась предобработка и потребуется ещё и постобработка полученных результатов, для того, чтобы правильно "соединить" даты и остальную информацию о фильме. Полученная грамматика, естественно, будет давать низкую полноту и ее надо расширять, смотря на большее количество примеров. Однако это хорошая база для старта. Дальше самое время погрузиться в решения реальных задач, заглядывая при этом в документацию - Руководство для разработчика от Яндекса.

При подготовке этого поста использовались следующие замечательные ссылки:


comments powered by HyperComments
Яндекс.Метрика