Pull to refresh

Анализатор морфологии на автоматах

Lumber room
Периодически на хабре проскакивают статьи о том, как написать программу для анализа морфологии. В основном авторы пользуются базами данных, либо стандартными структурами, такими как словари. Но это не всегда удобно. Во-первых, страдает скорость. Во-вторых, некоторые алгоритмы, такие как предсказание морфологии незнакомых слов, реализуются нетривиально.

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

Итак, чего мы хотим от морфологии. Обычно, от модуля анализа морфологии требуют:
  • получение леммы по заданному слову
  • получение морфологической информации по заданному слову
  • предсказание морфологии

Готовить все это будем из двух ингредиентов: морфологического словаря и библиотеки автоматов. Первый возьмем (как уже принято) из проекта aot.ru, спасибо им за LGPL. А библиотеку автоматов — отсюда: openfst.org. Кода будет по минимуму: все нужные операции уже реализованы в библиотеке.

Шаг первый. Парсим морфологический словарь.


В морфологическом словаре находятся строки вида:
МГЛ А га Ы гб Е гв У гг ОЙ гд ОЮ гд Е ге Ы гж АМ ги Ы гй АМИ гк АХ гл
Здесь приведен пример для слова 'мгла'. Буквы МГЛ — это корень слова. Остальное — окончания (флексии). Лемму, или нормализованное слово мы можем получить, сложив корень и первую флексию, в данном случае — МГЛА. Малые двухбуквенные сочетания — это анкод. Т.е. это код, однозначно определяющий морфологию словоформы. К примеру для 'га' это — 'существительное, женский род, единственное число, именительный падеж'.

Перейдем к действиям. Для каждой строки словаря строим вот такую структуру.
image

Это конечный преобразователь. Работает следующим образом. Каждый переход маркируется двумя буквами, где первая — входной символ, вторая — символ на выходе. Если мы «соберем» входящие и исходящие символы вдоль любого из путей из начального состояния в конечное, то получим два слова. Магия в том, что здесь входным словом будет одна из морфологических форм слова, а выходом — лемма + анкод. К примеру, «скормив» в этот автомат слово «МГЛОЮ», мы получим навыходе «МГЛАмг». Стоит отметить, что "-" обозначает пустой символ, и при проходе по графу мы можем его игнорировать.

Вообще, получить выходное слово по входному можно применив стандартную операцию композиции:
если D — словарь, как на картинке выше,
W — слово, где W = <(М, М)(Г, Г)(Л, Л)(О, О)(Ю, Ю),(И, И)>
То M — результат композиции — будет равен (за вычетом пустых символов)
M = W o D = <(М, М)(Г, Г)(Л, Л)(А, А)(а, а)(м, м)

Шаг второй. Оптимизация.


У структуры на рисунке выше есть проблема. Она не оптимальна. Первое — много лишних пустых символов, которые бесполезно «раздувают» преобразователь. Второе — этот преобразователь сам по себе не оптимален. Если построить такую конструкцию для полного словаря, последний будет очень большим.

И так, сначала сдвигаем исходящие символы в сторону начального состояния. Этим мы уменьшим количество пустых символов. В библиотеке openfst реализована такая операция. Зовется fstpush.
image
Как видно на рисунке появились дуги с метками "-:-". Они удалятся в процессе упаковки.
Далее, упаковываем преобразователь. Здесь есть тонкость. В общем случае конечный преобразователь не может быть упакован как обычный конечный автомат. Дело в том, что для минимизации преобразователя, как и автомата, он должен быть детерминирован (каждому входному слову соответствует единственное выходное). Но данный преобразователь таковым не является.
Чтобы решить эту проблему, можно
  • привести преобразователь к конечному автомату
  • упаковать автомат (по стандартному алгоритму)
  • привести получившийся автомат к преобразователю

Все эти операции стандартны, код писать нет нужды. Результат для слова «мгла» на картинке ниже.
image

Как видно из рисунка, количество состояний и переходов заметно сократилось. А это и есть наша цель.

Шаг третий. Предсказание морфологии неизвестного слова.


Здесь все просто. Все что нам нужно — для входящего слова W построить композицию с двумя преобразователями и найти кратчайший путь до конечного состояния:
P = shortest(W o T o D)
где, D — словарь
T — специальный преобразователь с весами, который поглощает префикс слова, при этом штрафуя за каждый «съеденный» символ.
image

Предсказание я пока не реализовал в примере. Возможно, позже.

Выводы


На мой взгляд это довольно интересный способ работать с морфологией. Я могу выделить два существенных плюса:
  • скорость и ресурсы
  • простота разработки
  • универсальность алгоритмов

Относительно скорости в данном примере мне не удалось с наскока добиться хороших результатов. Получилось что-то вроде 2000 слов/сек. Но я использовал стандартную библиотеку и не очень заботился об оптимизации. Возможно, если использовать другие виды трансдьюсеров скорость может возрасти. Словарь весил около 35М (однако, если использовать компактное представление, он сжимается в 3 раза).

На счет простоты: здесь ненужно много кода. Достаточно пользоваться стандартными операциями: композиция, конкатенация, объединение и поиск кратчайшего пути. Конструкция словаря заняло что-то около 50 строк кода. Тарбол примера с исходниками и питоновскими байндингами можно скачать здесь.

Кроме того, у данного подхода есть очень интересная особенность. Он позволяет выстраивать сложные преобразования за счет объединения трансдьюсеров в более сложные схемы. К примеру, добавив в предиктор автомат из моей прошлой статьи о спелчекере мы получим логический дивайс, который ответит на вопрос: «а является ли это слово действительно неизвестным, или оно содержит ошибку».
Пожалуй, пока все. Надеюсь, было интересно.
Tags:
Hubs:
Total votes 25: ↑25 and ↓0 +25
Views 3.2K
Comments Leave a comment