python

Сравнение и создание морфологических анализаторов в NLTK

  • четверг, 19 октября 2017 г. в 03:12:54
https://habrahabr.ru/post/340404/
  • Python


Здравствуйте. Это статья о сравнении существующих и создании своих морфологических анализаторов в библиотеке NLTK.

Введение


NLTK — пакет библиотек и программ для символьной и статистической обработки естественного языка, написанных на языке программирования Python. Отлично подходит для людей, изучающих компьютерную лингвистику, машинное обучение, информационный поиск [1].
В данной статье я буду сопровождать примеры кодом на языке Python (версии 2.7).

Приступим


Перед тем, как начать процесс, надо установить и настроить сам пакет NLTK.

Это можно сделать через pip:

pip install nltk

Теперь настроим пакет. Для этого в Python GUI нужно ввести:

>>> import nltk
>>> nltk.download()

Откроется окно, в котором можно установить пакеты к NLTK, в том числе необходимый нам корпус Brown. Отмечаем нужный пакет и нажимаем «Download». Всё, настройка закончена. Теперь можно приступать к работе.

Выборка и обучение


Как будет проходить тестирование? Перед тем, как тестировать сам анализатор, нам нужно его обучить. А обучение производится с помощью уже готовых теггированных слов. Использовать будем корпус Brown, точнее его часть под названием «news» – это достаточно большая категория материала в корпусе, в основном состоящая из текстов новостей, как ни странно.

Для обучения будет использоваться 90% всей выборки, а для тестирования – всего оставшиеся 10%. Проверять результат будем с помощью метода

tagger.evaluate(test_sents)

В результате получим значение от 0 до 1. Его можно умножить на 100, чтобы получилось процентное соотношение.

Сначала определим обучающие и тестируемые предложения. Узнаем количество предложений из 90% корпуса Brown.

>>> training_count = int(len(nltk.corpus.brown.tagged_sents(categories='news')) * 0.9)
>>> training_count
4160

4160 – это количество обучающих предложений для каждого анализатора. Остальные же, как было сказано, будут использоваться для тестирования.

Определим уже выборки, с ними мы будем и работать. Добавим еще сами предложения, чтобы показывать работу:

>>> training_sents = nltk.corpus.brown.tagged_sents(categories='news')[:training_count]
>>> testing_sents = nltk.corpus.brown.tagged_sents(categories='news')[training_count+1:]
>>> test_sents_notags = nltk.corpus.brown.sents(categories='news')[training_count+1:]

Существующие анализаторы


Существует несколько морфологических анализаторов в пакете NLTK. Но самые популярные из них это: анализатор по умолчанию, Unigram анализатор, N-gram анализатор, анализатор на основе регулярных выражений. Также можно создавать свои на их основе(но об этом позже) Давайте остановимся на каждом из них подробнее:

  1. Анализатор по умолчанию.

    Пожалуй, самый простой из всех существующих в NLTK. Автоматически обозначает тот же тег каждому слову. Этот анализатор можно использовать, если нужно присвоить самый используемый тег. Найдем его:

    >>> tags = [tag for (word, tag) in nltk.corpus.brown.tagged_words(categories='news')]
    >>> nltk.FreqDist(tags).max()
    'NN'

    В результате получаем NN(noun, имя существительное). В приведенном листинге создадим анализатор по умолчанию. Также сразу проверим его работу:

    >>> default_tagger = nltk.DefaultTagger('NN')
    >>> default_tagger.tag(testing_sents_notags[10])
    [('The', 'NN'), ('evidence', 'NN'), ('in', 'NN'), ('court', 'NN'), ('was', 'NN'), ('testimony', 'NN'), ('about', 'NN'), ('the', 'NN'), ('interview', 'NN'), (',', 'NN'), ('which', 'NN'), ('for', 'NN'), ('Holmes', 'NN'), ('lasted', 'NN'), ('an', 'NN'), ('hour', 'NN'), (',', 'NN'), ('although', 'NN'), ('at', 'NN'), ('least', 'NN'), ('one', 'NN'), ('white', 'NN'), ('student', 'NN'), ('at', 'NN'), ('Georgia', 'NN'), ('got', 'NN'), ('through', 'NN'), ('this', 'NN'), ('ritual', 'NN'), ('by', 'NN'), ('a', 'NN'), ('simple', 'NN'), ('phone', 'NN'), ('conversation', 'NN'), ('.', 'NN')]
    

    Как и было сказано, все слова (и даже не слова) помечены одним тегом. Этот анализатор редко где используется в одиночку, ибо является грубым.

    Теперь узнаем точность:

    >>> default_tagger.evaluate(testing_sents)
    0.1262832652247583

    Точность всего ~13% – это очень небольшой показатель.
    Перейдем к более сложным анализаторам.
  2. Анализатор на основе регулярных выражений.

    Это очень интересный анализатор, на мой взгляд. Он устанавливает тег на основе некоторого шаблона. Допустим, можно предположить, что каждое слово, заканчивающееся на -ed – это past participle в глаголах, если на -ing, то это герундий.

    Давайте создадим анализатор и сразу же проверим его:

    >>> patterns = [
         (r'.*ing$', 'VBG'),               # gerunds
         (r'.*ed$', 'VBD'),                # simple past
         (r'.*es$', 'VBZ'),                # 3rd singular present
         (r'.*ould$', 'MD'),               # modals
         (r'.*\'s$', 'NN$'),               # possessive nouns
         (r'.*s$', 'NNS'),                 # plural nouns
         (r'^-?[0-9]+(.[0-9]+)?$', 'CD'),  # cardinal numbers
         (r'.*', 'NN')                     # nouns (default)
    ]
    >>> regexp_tagger = nltk.RegexpTagger(patterns)
    >>> regexp_tagger.tag(testing_sents_notags[10])
    [('The', 'NN'), ('evidence', 'NN'), ('in', 'NN'), ('court', 'NN'), ('was', 'NNS'), ('testimony', 'NN'), ('about', 'NN'), ('the', 'NN'), ('interview', 'NN'), (',', 'NN'), ('which', 'NN'), ('for', 'NN'), ('Holmes', 'VBZ'), ('lasted', 'VBD'), ('an', 'NN'), ('hour', 'NN'), (',', 'NN'), ('although', 'NN'), ('at', 'NN'), ('least', 'NN'), ('one', 'NN'), ('white', 'NN'), ('student', 'NN'), ('at', 'NN'), ('Georgia', 'NN'), ('got', 'NN'), ('through', 'NN'), ('this', 'NNS'), ('ritual', 'NN'), ('by', 'NN'), ('a', 'NN'), ('simple', 'NN'), ('phone', 'NN'), ('conversation', 'NN'), ('.', 'NN')]
    

    Как видно, большинство слов всё же обозначены «default» тегом NN. Но некоторые отмеченные другими благодаря шаблону.

    Проверим точность:

    >>> regexp_tagger.evaluate(testing_sents)
    0.2047244094488189

    20% – уже этот анализатор справляется неплохо, по сравнению с анализатором по умолчанию
  3. Unigram анализатор.

    Использует простой статистический алгоритм маркирования слов. Каждому слову (токену) ставится тег, наиболее вероятный для этого слова.

    Сначала создадим и обучим анализатор, также покажем его в работе:

    
    >>> unigram_tagger = nltk.UnigramTagger(training_sents)
    >>> unigram_tagger.tag(testing_sents_notags[10])
    [('The', 'AT'), ('evidence', 'NN'), ('in', 'IN'), ('court', 'NN'), ('was', 'BEDZ'), ('testimony', 'NN'), ('about', 'IN'), ('the', 'AT'), ('interview', 'NN'), (',', ','), ('which', 'WDT'), ('for', 'IN'), ('Holmes', None), ('lasted', None), ('an', 'AT'), ('hour', 'NN'), (',', ','), ('although', 'CS'), ('at', 'IN'), ('least', 'AP'), ('one', 'CD'), ('white', 'JJ'), ('student', 'NN'), ('at', 'IN'), ('Georgia', 'NP-TL'), ('got', 'VBD'), ('through', 'IN'), ('this', 'DT'), ('ritual', None), ('by', 'IN'), ('a', 'AT'), ('simple', 'JJ'), ('phone', 'NN'), ('conversation', 'NN'), ('.', '.')]

    Уже результат намного лучше, чем у анализатора по умолчанию. Но можно заметить, что в результате есть слова, которые не отмечены тегом (стоит None). Это означает, что эти слова не появлялись при тренировке. Проверим точность анализатора:

    >>> unigram_tagger.evaluate(testing_sents)
    0.8110236220472441

    ~81% — это очень хороший показатель. Всего 19% слов отмечены или неправильно, или эти же слова вообще не появлялись при тренировке.
  4. N-грамы.

    Если в предыдущем анализаторе тег ставился на основе слова, которое встречалось в обучении, при этом не учитывался его контекст. Например, слово wind будет промаркировано одинаковым тегов, вне зависимости, что до него стоит: to или the. Анализатор на основе N-грамов позволяет решить эту проблему. Это общий случай Unigram анализатора, когда для установки тега для текущего слова используется тег n-1 предыдущих слов.

    Сейчас проверим работу BigramTagger – анализатора для n и n-1 слова.

    >>> bigram_tagger = nltk.BigramTagger(training_sents)
    >>> bigram_tagger.tag(testing_sents_notags[10])
    [('The', 'AT'), ('evidence', 'NN'), ('in', 'IN'), ('court', 'NN'), ('was', 'BEDZ'), ('testimony', None), ('about', None), ('the', None), ('interview', None), (',', None), ('which', None), ('for', None), ('Holmes', None), ('lasted', None), ('an', None), ('hour', None), (',', None), ('although', None), ('at', None), ('least', None), ('one', None), ('white', None), ('student', None), ('at', None), ('Georgia', None), ('got', None), ('through', None), ('this', None), ('ritual', None), ('by', None), ('a', None), ('simple', None), ('phone', None), ('conversation', None), ('.', None)]
    

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

    Из-за этой проблемы у этого анализатора точность будет небольшой:

    >>> bigram_tagger.evaluate(testing_sents)
    0.10216286255357321

    Всего 10% — очень маленький показатель. Такой способ маркирования слов не используется в одиночку из-за малой точности. Но это весьма мощное средство при использовании комбинации анализаторов.

    Есть еще TrigramTagger, который действует по такому же принципу, что и Bigram анализатор, только анализируются не один, а два предыдущих тега. Но его точность, конечно же, будет ещё ниже.

Комбинации из разных анализаторов


Наконец мы дошли до самого интересного – создание комбинаций из анализаторов. Например, можно комбинировать результаты работы Bigram анализатора, Unigram анализатора и анализатора по умолчанию. Это делается с помощью параметра backoff при создании анализатора. Каждый анализатор (кроме анализатора по умолчанию) может иметь указатель на использование другого анализатора для постройки многопроходного анализатора.

Давайте создадим его:

>>> default_tagger = nltk.DefaultTagger('NN')
>>> unigram_tagger = nltk.UnigramTagger(training_sents, backoff=default_tagger)
>>> bigram_tagger = nltk.BigramTagger(training_sents, backoff=unigram_tagger)

Давайте проверим анализатор:

>>> bigram_tagger.tag(test_sents_notags[10])
[('The', 'AT'), ('evidence', 'NN'), ('in', 'IN'), ('court', 'NN'), ('was', 'BEDZ'), ('testimony', 'NN'), ('about', 'IN'), ('the', 'AT'), ('interview', 'NN'), (',', ','), ('which', 'WDT'), ('for', 'IN'), ('Holmes', 'NN'), ('lasted', 'NN'), ('an', 'AT'), ('hour', 'NN'), (',', ','), ('although', 'CS'), ('at', 'IN'), ('least', 'AP'), ('one', 'CD'), ('white', 'JJ'), ('student', 'NN'), ('at', 'IN'), ('Georgia', 'NP'), ('got', 'VBD'), ('through', 'IN'), ('this', 'DT'), ('ritual', 'NN'), ('by', 'IN'), ('a', 'AT'), ('simple', 'JJ'), ('phone', 'NN'), ('conversation', 'NN'), ('.', '.')]

Как видно, все слова отмечены. Теперь проверим точность данного анализатора:

>>> bigram_tagger.evaluate(testing_sents)
0.8447124489185687

В результате получаем ~84%. Это очень хороший показатель. Можно комбинировать разные анализаторы, брать больше обучающую выборку для достижения результата получше.

Вывод


Какой можно сделать вывод? Лучше всего, конечно, использовать комбинации из анализаторов. Но Unigram анализатор справился не хуже и на него было потрачено меньше времени на обучение.

Надеюсь, данная статья поможет в выборе анализатора. Спасибо за внимание.

Ссылки


  1. Пакет NLTK. Официальный сайт
  2. NLTK. Документация