Как ускорить обработку естественного языка Python в 100 раз?

Python NLP
Как ускорить обработку естественного языка Python в 100 раз?

Эта статья была впервые опубликована сколонна Джижи


Использованная литература:medium.com

В прошлом году Томас Вольф, эксперт по машинному обучению в технологическом стартапе Hugging Face, возглавил команду для запуска набора инструментов для парсинга кореференций Python.NeuralCoref, используя нейронные сети для анализа общих референтов в предложениях.

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

В конце концов, Томас решил эту проблему с помощью различных усилий, и новая версия NeuralCoref увеличила скорость обработки в 100 раз, обеспечив точность! Кроме того, набор инструментов остается простым в использовании и подходит для экосистемы библиотек Python.

Затем Томас подытожил свой опыт решения этой проблемы и поделился опытом, как увеличить скорость обработки естественного языка Python в 100 раз, в том числе:

  • Как разработать эффективный модуль в Python
  • Как воспользоваться встроенными структурами данных spaCy для разработки сверхэффективных функций обработки естественного языка

В этой статье Томас объяснит, как использовать Cython и spaCy, чтобы сделать Python в сто раз быстрее для задач обработки естественного языка.

Прежде чем мы начнем, я (автор Томас Вольф - примечание переводчика) должен признать, что статья немного похожа на заголовок, потому что, хотя мы будем обсуждать Python, она также будет содержать некоторые хитрости Cython. Однако знаете что? Cython — это надмножество Python, так что не пугайтесь его!

Проект Python, который вы сейчас пишете, уже является проектом Cython.

Вот некоторые ситуации, в которых вам могут понадобиться стратегии ускорения Python, описанные в этой статье:

  • Вы разрабатываете модуль продукта на Python для задач НЛП.
  • Вы вычисляете аналитические данные для большого набора данных НЛП в Python.
  • Вы предварительно обрабатываете большой набор обучающих данных для платформы глубокого обучения, такой как PyTorch/TensorFlow, или пакетный загрузчик вашей модели глубокого обучения использует очень сложную логику обработки, которая значительно замедляет время обучения.

Первый шаг к ускорению в сто раз: проанализировать код

Первое, что вам нужно знать, это то, что большая часть вашего кода будет нормально работать на чистом Python, но некоторые из этих узких мест производительности могут ускорить вашу программу на несколько величин, если вы «заботитесь».

Поэтому вы должны начать анализировать свой код Python, чтобы найти те части, которые работают медленно. Один из способов исправить это — использовать cProfile:

import cProfile
import pstats
import my_slow_module
cProfile.run('my_slow_module.run()', 'restats')
p = pstats.Stats('restats')
p.sort_stats('cumulative').print_stats(30)

Вы можете обнаружить, что медленные части — это в основном какие-то циклы, или у вас слишком много операций с массивами Numpy в нейронной сети (я не буду подробно обсуждать Numpy здесь, потому что по этому поводу есть много материала для анализа).

Так как же нам ускорить эти циклы?

Ускоряем циклы в Python с помощью небольших хитростей Cython

Поясним это на простом примере. Допустим, у нас есть много прямоугольников, сохраните их как список объектов Python, таких как экземпляры класса Rectangle. Основная задача нашего модуля — пройтись по этому списку и подсчитать, сколько прямоугольников имеют площадь, превышающую установленный порог. Наш модуль Python будет таким же простым:

from random import random

class Rectangle:
    def __init__(self, w, h):
        self.w = w
        self.h = h
    def area(self):
        return self.w * self.h

def check_rectangles(rectangles, threshold):
    n_out = 0
    for rectangle in rectangles:
        if rectangle.area() > threshold:
            n_out += 1
    return n_out

def main():
    n_rectangles = 10000000
    rectangles = list(Rectangle(random(), random()) for i in range(n_rectangles))
    n_out = check_rectangles(rectangles, threshold=0.25)
print(n_out)

Функция Check_rectangles здесь является узким местом, которое мы хотим решить! Он зацикливается на большом количестве объектов Python, что может быть очень медленным, поскольку итератор Python выполняет много работы за кулисами (запрос методов области в классах, упаковка и распаковка параметров, вызов API-интерфейсов Python...).

Здесь мы можем использовать Cython, чтобы ускорить цикл.

Язык Cython является расширенным набором Python, который содержит два типа объектов:

  • Объекты Python — это объекты, которыми мы манипулируем в обычном Python, такие как числа, строки, списки, экземпляры классов...
  • Объекты Cython C — это объекты C или C++, такие как двойные числа, целые числа, числа с плавающей запятой, структуры и векторы, которые Cython может компилировать с помощью низкоуровневого кода, который работает очень быстро.

Цикл здесь мы используем цикл Cython для ускорения работы, и нам нужно только получить объект Cython C.

Простой способ спроектировать такой цикл — определить структуру C, которая будет содержать все, что нам нужно для наших вычислений: в нашем случае длину и ширину прямоугольника.

Затем мы сохраняем список прямоугольников в массиве определяемых нами структур C, которые мы передаем в функцию check_rectangle. Теперь функция должна принимать массив C в качестве входных данных, поэтому она будет определена как функция Cython с использованием ключевого слова cdef вместо def (cdef также используется для определения объектов Cython C).

Вот как выглядит высокоскоростная версия Cython нашего модуля Python:

from cymem.cymem cimport Pool
from random import random

cdef struct Rectangle:
    float w
    float h

cdef int check_rectangles(Rectangle* rectangles, int n_rectangles, float threshold):
    cdef int n_out = 0
    # C arrays contain no size information => we need to give it explicitly
    for rectangle in rectangles[:n_rectangles]:
        if rectangle[i].w * rectangle[i].h > threshold:
            n_out += 1
    return n_out

def main():
    cdef:
        int n_rectangles = 10000000
        float threshold = 0.25
        Pool mem = Pool()
        Rectangle* rectangles = <Rectangle*>mem.alloc(n_rectangles, sizeof(Rectangle))
    for i in range(n_rectangles):
        rectangles[i].w = random()
        rectangles[i].h = random()
    n_out = check_rectangles(rectangles, n_rectangles, threshold)
print(n_out)

Здесь мы используем собственный массив указателей C, но вы также можете выбрать другие параметры, особенно структуры C++, такие как векторы, кортежи, очереди и т. д. В этом сценарии я также использую обширный объект управления памятью Pool() cymem, чтобы избежать необходимости вручную освобождать выделенное пространство памяти массива C. Когда Python больше не нуждается в пуле, он автоматически освобождает память, которую мы использовали для его применения.

Давайте попробуем код!У нас есть много способов тестировать, редактировать и распространять код Cython! Cython можно даже использовать непосредственно в Jupyter Notebook, например Python.

Установите Cython с помощью pip install cython.

Первый тест в Jupyter

Загрузите расширения Cython с помощью %load_ext Cython в блокнотах Jupyter.

Теперь мы можем писать код на Cython, как код на Python, с помощью волшебной команды %%cython.

Если вы получаете ошибку компиляции при выполнении блока кода Cython, обязательно проверьте выходные данные терминала Jupyter, чтобы убедиться, что информация полная.

Большую часть времени вы можете компилировать на C++, пропустив тег a-+ после %%cython (например, когда вы используете spaCy Cython API), или если компилятор жалуется на Numpy, вы можете пропустить импорт Numpy.

Пишите, используйте и распространяйте код Cython

Код Cython записывается в виде файлов .pyx. Эти файлы компилируются компилятором Cython в файлы C или C++, которые затем компилируются системным компилятором C в файлы байт-кода. Затем файл байт-кода может использоваться интерпретатором Python.

Вы можете загружать файлы .pyx непосредственно в Python с помощью pyximport:

>>> import pyximport; pyximport.install()
>>> import my_cython_module

Вы также можете создать свой собственный код Cython в виде пакета Python, импортировать или распространять его как обычный пакет Python. Эта часть работает или требует немного времени. Если вам нужен рабочий пример, сценарий установки spaCy является более подробным примером.

Прежде чем мы поговорим о НЛП, давайте быстро поговорим о ключевых словах def, cdef и cpdef, поскольку они являются основными моментами, которые вам необходимо понять, чтобы начать работу с Cython.

Вы можете использовать 3 типа функций в Cython:

  • Функции Python определяются с помощью общего ключевого слова def. Его ввод и вывод являются объектами Python. Внутри функций можно использовать как объекты Python, так и объекты C/C++, а также вызывать функции Python и Cython.

  • Функции Cython определяются ключевым словом cdef. Вы можете использовать объекты Python и C/C++ в качестве входных и выходных данных и манипулировать ими внутри. Функции Cython недоступны напрямую из среды Python (интерпретатор Python и другие чистые модули Python импортируют ваши модули Cython), но могут быть импортированы другими модулями Cython.

  • Функции Cython, определенные с помощью ключевого слова cpdef, аналогичны функциям, определенным cdef, но они поставляются с оболочкой Python, поэтому к ним можно получить доступ из среды Python (объекты Python для ввода и вывода) и других модулей Cython (C/C++ или Объекты Python для ввода) ) могут вызывать их.

Есть еще одно применение ключевого слова Cdef, которое заключается в вводе Cython C/C++ в код. Если вы не вводите свои объекты с этим ключевым словом, они будут рассматриваться как объекты Python (что замедлит доступ).

Ускорьте решение задач НЛП с помощью Cython и spaCy

Сейчас все идет хорошо и быстро, но... мы еще не касались задач НЛП! Нет никаких манипуляций со строками, никаких кодировок Unicode и никаких трюков, которые мы могли бы использовать при обработке естественного языка.

В общем, если вы точно не знаете, что делаете, используйте не строки C-типа, а строковые объекты Python.

Итак, когда мы работаем со строками, как нам создавать быстрые циклы в Cython?

spaCy — наш «талисман». Способ, которым spaCy решает эту проблему, очень умен.

Преобразование всех строк в 64-битные хэш-коды

В spaCy все строки Unicode (текст токена, его текст в нижнем регистре, теги тегов POS, теги зависимостей дерева синтаксического анализа, теги именованных объектов и т. д.) хранятся в единой структуре данных, называемой StringStore, доступ к которой можно получить с помощью 64-битного хэша. индекс кода, который является типом C unit64_t.

Объекты StringStore реализуют сопоставление поиска между строками Unicode Python и 64-битными хэш-кодами.

Доступ к нему можно получить из любого места в spaCy и из любого объекта (как показано на рисунке ниже), такого как npl.vocab.strings, doc.vocab.strings или span.doc.vocab.string.

Когда модулю требуется более быстрая обработка определенных токенов, он может использовать 64-битные хэш-коды C-типа вместо строк. Вызов таблицы поиска StringStore вернет строку Unicode Python, связанную с этим хэш-кодом.

Но spaCy делает больше, он также позволяет нам получить полностью заполненную структуру C-типа для документов и словарей, которую мы можем использовать в циклах Cython без необходимости создавать наши собственные структуры.

Внутренние структуры данных spaCyОсновной структурой данных, относящейся к spaCy, является объект Doc, который имеет последовательность токенов обрабатываемой строки, а все его комментарии в объекте типа языка C называются doc.c, который представляет собой массив структур TokenC.

Структура TokenC содержит всю необходимую нам информацию о каждом токене. Эта информация хранится в виде 64-битного хэш-кода, который можно повторно связать со строкой Unicode, которую мы только что видели.

Если вы хотите увидеть, что именно находится в этих структурах C-типа, просто посмотрите документ Cython API только что созданного spaCy.

Далее рассмотрим простой пример обработки естественного языка.

Быстрое выполнение задач обработки естественного языка с помощью spaCy и CythonПредположим, у нас есть набор данных текстовых документов для анализа.

Ниже приведен сценарий, который я написал, который создает список из 10 документов, проанализированных spaCy, каждый из которых содержит около 170 000 слов. Мы также могли бы проанализировать 170 000 документов, каждый из которых содержит 10 слов (например, набор данных диалога), но этот метод создания намного медленнее, поэтому мы по-прежнему принимаем форму 10 документов.

import urllib.request
import spacy

with urllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt') as response:
   text = response.read()
nlp = spacy.load('en')
doc_list = list(nlp(text[:800000].decode('utf8')) for i in range(10))

Мы хотим выполнить некоторые задачи обработки естественного языка с этим набором данных. Например, мы хотим подсчитать, сколько раз слово «бег» используется в качестве существительного в наборе данных (например, помечено как тег части речи «NN» от spaCy).

Процесс реализации приведенного выше анализа с использованием цикла Python очень прост и понятен:

def slow_loop(doc_list, word, tag):
    n_out = 0
    for doc in doc_list:
        for tok in doc:
            if tok.lower_ == word and tok.tag_ == tag:
                n_out += 1
    return n_out

def main_nlp_slow(doc_list):
    n_out = slow_loop(doc_list, 'run', 'NN')
print(n_out)

Но работает очень медленно! На моем ноутбуке этот фрагмент кода занял 1,4 секунды, чтобы получить результат. Если бы у нас были миллионы документов, на получение ответа ушло бы больше суток.

Мы могли бы использовать многопоточность, но обычно это не очень хорошее решение в Python, потому что вам приходится иметь дело с GIL (GIL — это глобальная блокировка интерпретатора). Кроме того, Cython может использовать многопоточность! На самом деле, это, вероятно, лучшая часть Cython, поскольку Cython в основном вызывает OpenMP непосредственно за кулисами. Подробно вопрос параллелизма здесь обсуждаться не будет, можно нажатьздесьДополнительная информация.

Затем мы используем spaCy и Cython для ускорения нашего кода Python.

Во-первых, мы должны подумать о структуре данных. Нам нужно получить массив типа C для набора данных и иметь указатели на массив TokenC для каждого документа. Нам также необходимо преобразовать используемые тестовые строки («run» и «NN») в 64-битные хеш-коды.

Если все данные, которые нам нужны в нашей обработке, являются объектами типа C, тогда мы можем перебирать весь набор данных с чистой скоростью C.

Вот пример, который можно реализовать с помощью Cython и spaCy:

%%cython -+
import numpy # Sometime we have a fail to import numpy compilation error if we don't import numpy
from cymem.cymem cimport Pool
from spacy.tokens.doc cimport Doc
from spacy.typedefs cimport hash_t
from spacy.structs cimport TokenC

cdef struct DocElement:
    TokenC* c
    int length

cdef int fast_loop(DocElement* docs, int n_docs, hash_t word, hash_t tag):
    cdef int n_out = 0
    for doc in docs[:n_docs]:
        for c in doc.c[:doc.length]:
            if c.lex.lower == word and c.tag == tag:
                n_out += 1
    return n_out

def main_nlp_fast(doc_list):
    cdef int i, n_out, n_docs = len(doc_list)
    cdef Pool mem = Pool()
    cdef DocElement* docs = <DocElement*>mem.alloc(n_docs, sizeof(DocElement))
    cdef Doc doc
    for i, doc in enumerate(doc_list): # Populate our database structure
        docs[i].c = doc.c
        docs[i].length = (<Doc>doc).length
    word_hash = doc.vocab.strings.add('run')
    tag_hash = doc.vocab.strings.add('NN')
    n_out = fast_loop(docs, n_docs, word_hash, tag_hash)
print(n_out)

Код немного длинный, потому что мы должны объявить и заполнить структуру C в main_nlp_fast перед вызовом функции Cython[*].

Но код работает намного быстрее! В моем блокноте Jupyter этот код Cython выполняется примерно за 20 микросекунд, что в 80 раз быстрее, чем наш предыдущий цикл, полностью написанный на Python.

Скорость написания модулей с помощью Jupyter Notebook также впечатляет, и его можно естественным образом связать с другими модулями и функциями Python: до 1,7 миллиона слов можно обработать за 20 микросекунд, а это значит, что мы можем обрабатывать до 80 миллионов слов в секунду!

Это краткое введение в то, как наша команда использует Cython для задач НЛП, надеюсь, вам понравится.

Эпилог

О Cython еще многое предстоит узнать, вы можете проверитьОфициальный учебник CythonПолучите обзор и на spaCyКонтент Cython для обработки задач НЛП.

Если вы используете низкоуровневые структуры несколько раз в своем коде, вместо того, чтобы каждый раз заполнять структуру C-типа, лучший вариант — спроектировать наш код Python вокруг низкоуровневой структуры, обернув структуру C-типа Cython. тип расширения. Именно так построена большая часть spaCy, которая не только работает быстро и потребляет мало памяти, но также позволяет нам легко взаимодействовать с внешними библиотеками и функциями Python.

Посмотреть весь код в этой статьеКолонка Jizhi — Как увеличить скорость обработки естественного языка Python в 100 раз?


0806 «Искусственный интеллект — от нуля до мастера» со скидкой, ограниченной по времени!

Нажмите здесь, чтобы узнать подробности

Болтать и смеяться Онлайн-программирование Узнать об этом?

(Первые 25 студентов также могут получить купон на 200 иен)