Как ускорить в python преобразование огромного списка/кортежа русских слов в нормальную форму?

Есть потребность преобразовывать на регулярной основе большие списки русских слов в нормальную форму. Популярная библиотека Pymorphy2 работает очень качественно, но очень медленно на больших количествах слов в циклах. Например, 880 тыс. слов на моём компьютере (AMD Phenom II X6 1090T, 3617 MHz, 20Гб ОЗУ) обрабатывает 4 минуты вот в таком коде:

import pymorphy2
morph = pymorphy2.MorphAnalyzer()
<...>
lst_newlist = [morph.parse(str_word)[0].normal_form for str_word in tuple_somewords]

Экспериментировал с массивами numpy - ускорения не получил. Всё равно упирался в цикл for. Пытался распараллелить c помощью библиотеки joblib - тоже не получил ускорения.

В самой библиотеке Pymorphy2, насколько я понял, проблема в том, что она не заточена на обработку массивов/списков слов - при каждом вызове очередного слова снова загружает в память свои словари и выполняет одну и ту же рутину, которую можно было бы по-хорошему сделать один раз перед обработкой списка, а не одного слова.

Копаться и переделывать код Pymorphy2 не готов.

Pymystem3 работает значительно быстрее (раза в четыре), но по идеалогическим причинам не хочу загружать скомпилированные exe файлы на компьютер. И, похоже, что при каждом обращении к Pymystem3 требуется доступ к интернет, иначе вываливаются ошибки исполнения.

Стемминг категорически не пригоден.

Есть ли возможность ускорить Pymorphy2? Или, может быть, есть другие качественные и быстрые библиотеки для преобразования русских слов в нормальную форму?


Ответы (1 шт):

Автор решения: MaxU

Попробуйте закешировать результат (если память позволяет):

import pymorphy2

morph = pymorphy2.MorphAnalyzer()
norm = {}

def norm_form(word):
    res = norm.get(word)
    if res:
        return res
    res = morph.parse(word)[0].normal_form
    norm[word] = res
    return res

lst_newlist = [norm_form(str_word) for str_word in tuple_somewords]

Можно также воспользоваться встроенным Python cache декоратором, как советует @insolor:

from functools import lru_cache

# начиная с Python 3.9 можно использовать `@cache` вместо `@lru_cache(maxsize=None)`
@lru_cache(maxsize=None) 
def norm_form_cached(word):
    return morph.parse(word)[0].normal_form

Сравнение скорости обработки для ~202.000 слов (роман "Идиот" Достоевского):

In [61]: with open("idiot.txt") as f:
    ...:     data = f.read()
    ...:

In [62]: %time norm_words = [morph.parse(word)[0].normal_form for word in word_tokenize(data) if word.isalpha()]
CPU times: user 35.9 s, sys: 42.8 ms, total: 36 s
Wall time: 36.1 s

In [63]: norm = {}

In [64]: %time norm_words = [norm_form(word) for word in word_tokenize(data) if word.isalpha()]
CPU times: user 4.25 s, sys: 7.62 ms, total: 4.26 s
Wall time: 4.27 s

In [65]: len(norm_words)
Out[65]: 202336

In [66]: 36.1 / 4.27
Out[66]: 8.45433255269321

In [83]: %time norm_words = [norm_form_cached(word) for word in word_tokenize(data) if word.isalpha()]
CPU times: user 4.18 s, sys: 8.45 ms, total: 4.19 s
Wall time: 4.2 s

Выигрыш в ~8.5 раз. Статистика использования кеша:

In [84]: norm_form_cached.cache_info()
Out[84]: CacheInfo(hits=174582, misses=27754, maxsize=None, currsize=27754)

Дальше можно попробовать обрабатывать текст параллельно, разбив текст на куски.

→ Ссылка