Лемматизируй это быстрее (PyMorphy2, PyMystem3 и немного магии)

  • Tutorial
Я работаю программистом, и в том числе занимаюсь машинным обучением применительно к анализу текстов. При обработке естественного языка требуется предварительная подготовка документов, и одним из способов является лемматизация – приведение всех слов текста к их нормальным формам с учетом контекста.

Недавно мы столкнулись с проблемой больших временных затрат на этот процесс. В конкретной задаче было более 100000 документов, средняя длина которых около 1000 символов, и требовалось реализовать обработку на обычном локальном компьютере, а не на нашем сервере для вычислений. Решение на просторах интернета мы найти не смогли, но нашли его сами, и я хотел бы поделиться — продемонстрировать сравнительный анализ двух наиболее популярных библиотек по лемматизации в этой статье.



PyMorphy2


Одной из самых популярных является PyMorphy2 — она встречается почти в каждом решении, которое можно найти в сети. Мы также пользовались этой библиотекой, и она отлично себя показывала, пока не потребовалось сделать лемматизацию для всей базы (как я писал выше, это более 100 тысяч небольших документов). Для того чтобы проанализировать такой объём документов, PyMorphy2 потребовалось бы почти 10 часов, при этом нагрузка на процессор всё это время будет в среднем около 30% (Intel Core i7 7740X).

PyMystem3


В поисках другого решения мы проанализировали библиотеку от Яндекса PyMystem3, но результат оказался чуть ли не вдвое хуже (по времени), чем у PyMorphy2: чтобы обработать 100 тысяч документов, ей бы потребовалось 16ч.

Немного магии


Нам показалось странным, что при этом нагрузка на процессор была почти нулевой. Также странно было то, что для получения результата от одного текста, даже большого (3-4 тысячи символов), PyMystem3 требовалось около 1 секунды. Поэтому мы решили объединить тексты, добавив какой-нибудь разделитель между ними, по которому мы могли бы заново вернуть структуру нашего списка документов, и отдать их на лемматизацию.

Код решения на Python:

def checkExecTimeMystemOneText(texts):
    lol = lambda lst, sz: [lst[i:i+sz] for i in range(0, len(lst), sz)]
    txtpart = lol(texts, 1000)
    res = []
    for txtp in txtpart:
        alltexts = ' '.join([txt + ' br ' for txt in txtp])

        words = mystem.lemmatize(alltexts)
        doc = []
        for txt in words:
            if txt != '\n' and txt.strip() != '':
                if txt == 'br':
                    res.append(doc)
                    doc = []
                else:
                    doc.append(txt)

Мы брали по 1000 документов для объединения, разделитель между ними — «br» (надо отметить, что тексты на русском языке, и латиницу и спецсимволы мы предварительно убирали). Данное решение значительно ускорило лемматизацию: в среднем, получилось около 25 минут на все 100 тысяч документов, нагрузка на процессор 20-40%. Правда это решение больше грузит ОЗУ: в среднем, это около 3-7Гб, но это хороший результат. Если же памяти недостаточно, то можно изменить количество объединяемых документов, это хоть и замедлит обработку, но всё равно будет гораздо быстрее, чем по одному тексту за раз. Также если объём памяти позволяет, то можно увеличить это значение и получить результат ещё быстрее.

В таблице — сравнение библиотек при обработке на локальном компьютере (Intel Core i7 7740X, 32 ГБ ОЗУ):
Метод Время ОЗУ (Мб) Проц
PyMorphy2 ~9,5 час 500 — 600 25 — 30%
PyMystem3 по 1 предложению ~16 час 500 — 700 0 — 1%
PyMystem3 по 1000 предложений 26 минут 2000 — 7000 25 — 40%

Интересно узнать мнение других специалистов. Возможно, кто-то нашёл более эффективный по времени способ лемматизации коротких текстов?

Кабаков Михаил, старший инженер-программист,
Михайлова Анна, начальник отдела интеллектуального анализа данных,
Консорциум «Кодекс»
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    0

    pip install pymorphy2[fast] пробовали?

      0
      эта команда не работает, по идее она должна установить стабильную версию, но сейчас стабильная версия 0.8, мы её и использовали
      +1

      PyMystem3 использует для обработки внешний исполняемый файл mystem.exe. Видимо, такая архитектура на частых маленьких вызовах обходится очень дорого. Соответственно, ваш подход с объединением многих предложений в одно для обработки — это действительно хорошая идея.


      По поводу дальнейшего ускорения. Итерация по массиву — это задача, которая хорошо параллелится. Можно попробовать joblib в качестве старта. Если расширить ваш пример с "упаковкой" текстов, получится что-то такое:


      from pymystem3 import Mystem
      from tqdm import tqdm
      
      from joblib import Parallel, delayed
      
      batch_size = 1000
      texts = ["Мама мыла раму {}".format(i) for i in range(1000000)]
      
      text_batch = [texts[i: i + batch_size] for i in range(0, len(texts), batch_size)]
      
      def lemmatize(text):
          m = Mystem()
          merged_text = "|".join(text)
      
          doc = []
          res = []
      
          for t in m.lemmatize(merged_text):
              if t != '|':
                  doc.append(t)
              else:
                  res.append(doc)
                  doc = []
      
          return res
      
      # Вот здесь тоже немного магии :)
      processed_texts = Parallel(n_jobs=-1)(delayed(lemmatize)(t) for t in tqdm(text_batch))

      У меня на ноуте c Intel Core i5-7300U @2.6Ghz при запуске в 4 процесса, миллион "мама мыла раму" лемматизируется за 16 минут. tqdm показывает ~1.1 it/sec


      Ну и дальше, если корпус еще больше — можно на dask кластер вытягивать.

        0
        инициализацию mystem надо вынести за lemmatize(), она занимает большую часть времени. на собственно лемматизацию приходится совсем мало
          0

          Это понятно. Я бы так и сделал, если бы не параллельные процессы — в них это так не работает.

          0
          Да, смысл именно в том, что PyMystem3 работает быстрее, чем pymorphy2 на одном тексте, но при каждом вызове поднимает mystem (и ещё делает кучу всего), что как раз и замедляет на куче. Предложенное решение вроде простое, но не всегда очевидное. Может, кому-то тоже поможет, как и нам.
          За joblib спасибо, параллельные потоки всегда актуальная тема, хотя не стали сильно в неё углубляться в этом случае: при текущей реализации вышли на приемлемое время подготовки, дальше уже больше внимания на модели
            0
            Видимо, такая архитектура на частых маленьких вызовах обходится очень дорого
            Точноейшим образом совпадает с моими тестами на Windows, по итогу я то ли баг заводил, то ли в существующий отписывался — решения пока нет. На Linux такой проблемы нет, лемматизатор летает.
            0
            У меня была задача морфологического анализа базы opensubtitles.org для английского языка (~1.7 миллиона слов после минимизации). Сделаю оговорку, что анализ нужен был без разбивки по отдельным «документам». С другой стороны, нужно было категоризировать слова по группам наследования, частям речи, и некоторым другим параметрам.

            Короче, покопавшись немного с внешними библиотеками, решил сделать все «в лоб» на голом SQL. Использовались только базы opensubtitles.org и Helsinki Finite-State Technology.

            Полный разбор базы получился где-то в районе 40 секунд — минуты.
              0
              А на php что сейчас наиболее модно на тему лемматизации? Для русского/англ языка.
                0
                Если вам нужно сравнительно быстро и качественно — всё упирается в mystem.
                Группируете тексты, поднимаете процесс mystem-а, суёте, разбираете.

                Насколько я искал некоторое время назад — яндекс нигде не раскрывает исходники мистема, везде приходится использовать как процесс.
                Теоретически можно попробовать расковырять exe-шник, и попытаться сделать из него библиотеку, но, подозреваю, этого нельзя делать с точки зрения лицензии и довольно сложно технически.
                0

                А спросить у kmike по поводу оптимизации pymorphy2 не пробовали?

                  0

                  Скажите, а вы станфордский Stanza не пробовали, если да то как в сравнении? Спасибо.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое