2016-01-29

Python NLTK + Stanford NLP

Как известно, в Python стандартом работы с натуральным языком де-факто является NLTK. Несмотря на это, я довольно долго использовал Pattern от CLiPS из-за его простоты и скорости (многие отмечают тормознутость NLTK).

Но наступил момент, когда почти вся кодовая база была успешно портирована на Python 3.5, а разработчики Pattern так и не сделали версию с поддержкой третьей версии. И, судя по всему, не собираются.

Что-ж, будем использовать NLTK. От него мне нужны: токенизация, выделение POS (part-of-speech), получение N-grams и классификация твитов на группы с использованием Naive Bayes. Все это дело на Python 3.5.


pip3 install nltk

Модули для NLTK устанавливаются так:


import nltk
nltk.download()

... или так:


python3 -m nltk.downloader punkt averaged_perceptron_tagger
# python3 -m nltk.downloader all

Теперь немного примеров.


tweet = '''
    Kinto by Mozilla - An open source Parse alternative >>
    https://github.com/Kinto/kinto/ #python #parse
'''

# Получаем токены. Стандартный универсальный метод:
from nltk import word_tokenize
print(word_tokenize(tweet))
'''
[
    'Kinto', 'by', 'Mozilla', '-', 'An', 'open', 'source',
    'Parse', 'alternative','>', '>', 'https', ':',
    '//github.com/Kinto/kinto/', '#', 'python', '#', 'parse'
]
'''

# С использованием специализированного класса
from nltk.tokenize import TweetTokenizer
# Убрать имена пользователей и многократно повторяемые символы
tt = TweetTokenizer(preserve_case=True, reduce_len=False, strip_handles=False)
print(tt.tokenize(tweet))
'''
[
    'Kinto', 'by', 'Mozilla', '-', 'An', 'open',
    'source', 'Parse', 'alternative', '>', '>',
    'https://github.com/Kinto/kinto/', '#python', '#parse'
]
'''

# Возьмем немного почищеные токены. Воспользуемся обычным токенизатором
tokens = [
    t for t in word_tokenize(tweet)
        if len(t) > 1 and not t.startswith('http')
        and not t.startswith('/')
]
'''
[
    'Kinto', 'by', 'Mozilla', 'An', 'open', 'source',
    'Parse', 'alternative', 'python', 'parse'
]
'''

Токены получили, теперь будем узнавать части речи. Можно использовать встроенный метод, а можно пойти интересным путем, и привлечь Stanford NLP библиотеки. Для этого скачаем и распакуем каталог с ними в нашу рабочую директорию, где пишем код. В Python укажем место расположения JAR и модулей через переменные окружения.


import os
from nltk.tag import StanfordPOSTagger
os.environ['CLASSPATH'] = os.path.join(
    os.path.curdir, 'stanford-postagger-2015-04-20'
)
os.environ['STANFORD_MODELS'] = os.path.join(
    os.path.curdir, 'stanford-postagger-2015-04-20', 'models'
)
stanford_tagger = StanfordPOSTagger('english-bidirectional-distsim.tagger')

Теперь можно сравнить результаты методов:


from nltk import pos_tag
print(pos_tag(tokens))
print(stanford_tagger.tag(tokens))

Результаты почти идентичны, за исключением "An". StanfordPOSTagger считает его существительным. Допустим, мы выберем последний вариант. Найдем все имена существительные (NN, NNS, NNP, NNP-PERS, NNP-ORG):


tagger = stanford_tagger.tag
nouns = [word.lower() for word, pos in tagger(tokens) if pos.startswith('NN')]
print(nouns)  # ['kinto', 'mozilla', 'an', 'source', 'parse', 'python', 'parse']

Теперь N-grams. Не проблема сделать самостоятельно, но в NLTK уже есть пара готовых функций.


from nltk.util import everygrams, ngrams
print(list(ngrams(nouns, 2)))
# [('kinto', 'mozilla'), ('mozilla', 'an'), ...  ('python', 'parse')]
print(list(everygrams(nouns, min_len=2, max_len=3)))
'''
[
    ('kinto', 'mozilla'), ('mozilla', 'an'), ... ('python', 'parse'),
    ('kinto', 'mozilla', 'an'), ('mozilla', 'an', 'source'),
    ... ('parse', 'python', 'parse')
]
'''

Как я уже говорил, NLTK порой очень медленный. С использованием Stanford библиотек он медленнее в разы. Существенно улучшает ситуацию обработка больших объемов твитов разом, а не один за другим.


from nltk import pos_tag, pos_tag_sents
tokens_group = [tokens for i in range(100)]
print(pos_tag_sents(tokens_group))  # 0.3 sec
print(stanford_tagger.tag_sents(tokens_group))  # 6.4 sec !!

На сём откланиваюсь.

2016-01-19

Postgres: generate the most large followers intersections

Задача: получить список пользователей твиттера, с которыми у одного из них есть общие фоловеры, и отсортировать по их количеству. Лучше всего продемонстрировать на примере. Создадим таблицу с идентификаторами пользователей и фоловеров:


CREATE TEMP TABLE f(uid INT, fid INT);
INSERT INTO f(uid, fid) VALUES
 (1, 10), (1, 11), (1, 12), (1, 13), (1, 14),
 (2, 10), (2, 11), (2, 12),
 (3, 13), (3, 14),
 (4, 13), (4, 14), (4, 10), (4, 11)
;

А вот собственно и запрос, интересуют пересечения с пользователем 2, самые большие сверху.


SELECT uid, array_agg(fid) followers_intersect
FROM f GROUP BY uid
ORDER BY cardinality(
 (
  SELECT array_agg(a1) FROM unnest(array_agg(fid)) a1
  INNER JOIN unnest(
   (SELECT array_agg(fid) FROM f WHERE uid=2)::INT[]
  ) a2
  ON a1=a2
 )::INT[]
) DESC NULLS LAST;

Результат, как и ожидалось:

  • 1,"{10,11,12,13,14}"
  • 2,"{10,11,12}"
  • 4,"{13,14,10,11}"
  • 3,"{13,14}"