За последние 24 часа нас посетил 20121 программист и 1726 роботов. Сейчас ищут 1572 программиста ...

Еще один морфологический анализатор на чистом php

Тема в разделе "PHP для профи", создана пользователем johovich, 30 июл 2018.

  1. johovich

    johovich Активный пользователь

    С нами с:
    24 авг 2016
    Сообщения:
    146
    Симпатии:
    17
    Вчера ночью закончил свою библиотеку для морфологического анализа.

    Вот что пока получилось:
    Требования: >= php 7
    Зависимости: нет

    Возможности:
    1. Привести слово к нормальной форме.
    2. Показать грамматическую информацию о слове и всех его формах

    Устройство:
    Библиотека построена на базе другой моей библиотеки Yatrie, реализующей префиксное дерево trie. Словарь русского языка в библиотеке использован из проекта OpenCorpora.org
    Концепция работы словаря почерпнута из статьи И. Сегаловича "A fast morphological algorithm with unknown word guessing induced by a dictionary for a web search engine" (http://download.yandex.ru/company/iseg-las-vegas.pdf). По мотивам этой статьи (или наоборот) И.Сегалович и В.Титов написали программу mystem. Проект по прежнему развивается в Яндексе (https://tech.yandex.ru/mystem/) Насколько я понимаю данная технология по-прежнему используется в Яндексе. Это я к тому, что концепция очень крутая.
    Я не очень хорошо знаю английский и еще совсем слабо программирую, поэтому мне пришлось незначительно изменить принцип поиска изложенный в вышеуказанной статье И.Сегаловича.
    Вот что у меня получилось:
    Всего в действующей редакции словаря openCorpora (http://opencorpora.org/dict.php) 114 граммем, 389360 лемм, 5100150 форм. Граммема - это род, число, падеж, часть речи и т.д. Лемма как пишут в интернетах нормализованная, основная форма слова, вместе с информацией о построении других форм. Но почему-то у слова любить и полюбить 2 разные леммы в словаре OpenCorpora. Насколько я понял формы одной леммы это всегда одно понятие, но 1 понятие может быть представлено сразу 2 леммами. Например, любить и полюбить это 2 разные леммы.
    Экскурс в историю:
    В 1965 году некто А.А.Зализяк в будущем академик и светило отечественной науки, тогда еще соискатель звания кандидат наук защитил кандидатскую диссертацию по теме «Классификация и синтез именных парадигм современного русского языка». Именно эта идея и легла в основу mystem. Суть ее в следующем:
    Практически все словоформы одной леммы (в последней редакции словаря было всего 12 исключений) имеют некоторую общую часть, И.Сегалович называет эту общую часть "стемма", от слова stemmata, что в переводе с латыни означает венок из ветвей. Я называю эту общую часть просто корень root.
    Пример:
    Код (Text):
    1.  
    2. ┌------------┬---------------------------------------┐
    3. │    form    │                 props                 │
    4. │------------┼---------------------------------------│
    5. │скот        │имя существительное, одушевлённое, мужс│
    6. │            │кой род                                │
    7. │------------┼---------------------------------------│
    8. │скот        │единственное число, именительный падеж │
    9. │------------┼---------------------------------------│
    10. │скота       │единственное число, родительный падеж  │
    11. │------------┼---------------------------------------│
    12. │скота       │единственное число, винительный падеж  │
    13. │------------┼---------------------------------------│
    14. │скоту       │единственное число, дательный падеж    │
    15. │------------┼---------------------------------------│
    16. │скотом      │единственное число, творительный падеж │
    17. │------------┼---------------------------------------│
    18. │скоте       │единственное число, предложный падеж   │
    19. │------------┼---------------------------------------│
    20. │скотам      │множественное число, дательный падеж   │
    21. │------------┼---------------------------------------│
    22. │скотами     │множественное число, творительный падеж│
    23. │------------┼---------------------------------------│
    24. │скотах      │множественное число, предложный падеж  │
    25. │------------┼---------------------------------------│
    26. │скотов      │множественное число, родительный падеж │
    27. │------------┼---------------------------------------│
    28. │скотов      │множественное число, винительный падеж │
    29. │------------┼---------------------------------------│
    30. │скоты       │множественное число, именительный падеж│
    31. └------------┴---------------------------------------┘
     
    Poznakomlus и igordata нравится это.
  2. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    а можно мне пример
    как сделать на запрос "укладка камня на даче" сделать ответ "уложим камень на твоей даче"?
     
  3. johovich

    johovich Активный пользователь

    С нами с:
    24 авг 2016
    Сообщения:
    146
    Симпатии:
    17
    Таким образом часть слова до корня - префикс, а после корня - суффикс. Получается, что для получения любой словоформы нужно иметь. префикс, корень, окончание и грамматическую информацию. Например, словоформа "скоты"
    PHP:
    1. $prefix = '';
    2. $root = 'скот';
    3. $suffix = 'ы';
    4. $props = 'множественное число, именительный падеж';
    Совокупность всех суффиксов, префиксов и грамматической информации одной леммы образуют парадигму. Т.е. шаблон того, как из корня получить любую словоформу. При том, что в языке > 5 млн. слов, уникальных корней ~284 тыс., а парадигм всего 3.5 тыс..
    --- Добавлено ---
    Краткое описание того, как из словаря openCorpora в формате xml получается словарь для моей библиотеки.
    Образец одной леммы из словаря openCorpora
    Код (Text):
    1. <lemma id="323408" rev="323408"><l t="скот"><g v="NOUN"/><g v="anim"/><g v="masc"/></l><f t="скот"><g v="sing"/><g v="nomn"/></f><f t="скота"><g v="sing"/><g v="gent"/></f><f t="скоту"><g v="sing"/><g v="datv"/></f><f t="скота"><g v="sing"/><g v="accs"/></f><f t="скотом"><g v="sing"/><g v="ablt"/></f><f t="скоте"><g v="sing"/><g v="loct"/></f><f t="скоты"><g v="plur"/><g v="nomn"/></f><f t="скотов"><g v="plur"/><g v="gent"/></f><f t="скотам"><g v="plur"/><g v="datv"/></f><f t="скотов"><g v="plur"/><g v="accs"/></f><f t="скотами"><g v="plur"/><g v="ablt"/></f><f t="скотах"><g v="plur"/><g v="loct"/></f></lemma>
     
    #3 johovich, 30 июл 2018
    Последнее редактирование: 30 июл 2018
  4. johovich

    johovich Активный пользователь

    С нами с:
    24 авг 2016
    Сообщения:
    146
    Симпатии:
    17
    1. Сначала весь словарь практически в неизменном виде переносится из xml в mysql.
    Вот какие таблицы:
    forms - lemma_id, word_id, props_id
    words - id, name
    lemmata id, word_id, props_id
    grammemes id, name, parent, alias, desc
    props id, name (в поле name в виде строки через разделитель записаны id граммем)

    2. Обрабатываем все словоформы для каждой леммы и получаем prefix, root, suffix Записываем все в отдельные таблицы. В таблицу forms и lemmata записываем соответствующие prefix_id, root_id, suffix_id. В таблице lemmata содержится нормализованная форма слова и у некоторых лемм, например, наречий, есть только нормализованная форма. Т.е. записи в таблицы forms вообще нет, поэтому prefix_id, root_id, suffix_id есть и в этой таблице.

    3. Берем все словоформы 1 слова, сортируем их в алфавитном порядке по полям: suffix_id, prefix_id, props_id по порядку все суффиксы, префиксы и свойства через |. Например, 1|2|3. Все полученные комбинации записываем через разделитель -. Например, 1|2|3-4|5|6-3|23|5
    Это и есть парадигма леммы. Все парадигмы записываем в отдельную таблицу paradigms, а в таблицу lemmata добавляем соответствующие paradigm_id.

    4. Берем все суффиксы из таблицы суффиксов и переворачиваем их. Например словоформа человеком имеет префикс "че" корень "л" и суффикс "овеком". Перевернутый суффикс будет "мокево". Записываем все эти перевернутые суффиксы в дерево trie.

    5. Переворачиваем все корни и записываем их, добавляя id парадигмы этого корня через специальный разделитель $. Например, корень слова человек «л» записан в дереве вот так:
    Код (Text):
    1. Array
    2. (
    3.     [0] => л$1852
    4.     [1] => л$1984
    5.     [2] => л$1986
    6.     [3] => л$1999
    7.     [4] => л$2001
    8.     [5] => л$2002
    9.     [6] => л$2022
    10.     [7] => л$2023
    11.     [8] => л$2833
    12.     [9] => л$2834
    13.     [10] => л$2835
    14.     [11] => л$2874
    15.     [12] => л$3107
    16.     [13] => л$3212
    17.     [14] => л$3222
    18.     [15] => л$3223
    19.     [16] => л$3254
    20.     [17] => л$411
    21.     [18] => л$532
    22.     [19] => л$534
    23.     [20] => л$968
    24. )
    6. Аналогичным образом записываем еще одно дерево trie со всеми id суффиксов и соответствующие этим суффиксам парадигмы. Например, 12$32
    На этом структуры данных закончены.
    Логика работы поиска:
    1. Ищем в дереве суффиксов, получаем все возможные суффиксы для нашего слова.
    2. Теперь начиная с самого «глубокого» (длинного) суффикса ищем «хвост» в дереве корней. Получаем все возможные корни и их парадигмы, которые записаны на концах ветвей нашего дерева.
    3. Преобразуем каждый найденный на шаге 1 суффикс в id и ищем его в дереве суффиксов и парадигм. Накладываем найденный массив парадигм из этого дерева с массивом парадигм найденном на 2 шаге. Если есть хотя бы 1 совпадение, значит мы нашли наше слово. Иногда совпадений бывает 2. Т.е. есть 2 разных слова с одинаковым написанием. В слове человек пересечение будет только по 1 парадигме 3107. А вот в слове "скот" ,будет пересечение по 2 парадигмам 54, 75. У неодушевленного слова "скот" есть одушевленный омоним "скот".
    4. Берем полученный на этапе 2 корень и номер парадигмы слова и генерируем лемму слова по массивам «ключ-значение» prefix, suffix, root, props.
     
  5. johovich

    johovich Активный пользователь

    С нами с:
    24 авг 2016
    Сообщения:
    146
    Симпатии:
    17
    Сейчас функционал, который бы позволил получить нужную форму еще не готов. Но будет примерно вот так:
    PHP:
    1. $y = new Yamorphy();
    2. $str = "укладка камня на даче";
    3. $a = explode(' ', $str);
    4.  
    5. //тут мы должны получить что-то вроде "укладка камень на дача"
    6. foreach($a as &$w){
    7. $w = $y->morph($w, ['n']); //n это нормальная форма
    8. }
    9. //Чтобы первое слово сделать из существительного "укладка" глаголом "уложим"
    10. $a[0] = $y->morph($w, ['verb', '1per', 'futr', 'indc', 'plur']);
    11.  
    12. //получаем предложный падеж
    13. $last = array_key_last($a);
    14. $a[$last] = $y->morph($a[$last], ['loct']);
    15. $str2 = implode(' ' , $a);
    16. echo $str2; //получаем "уложим камень на даче"
    --- Добавлено ---
    Сейчас это все как-то упакую поприличнее, чтобы на гит можно было выложить и выложу тогда ссылку.
     
    #5 johovich, 30 июл 2018
    Последнее редактирование: 30 июл 2018