3 приема SpaCy для эффективной обработки текста и распознавания сущностей
В этой статье мы рассмотрим три важных приема spaCy, которые должны быть в арсенале каждого разработчика для максимальной скорости обработки и настройки распознавания сущностей.

# Введение
Благодаря современным большим языковым моделям, обработка естественного языка (NLP) является фундаментальной опорой современных систем искусственного интеллекта и программного обеспечения. Методы и технологии NLP используются во всем: от поисковых систем и чат-ботов до автоматизированной маршрутизации в службах поддержки клиентов и конвейеров извлечения сущностей. Когда речь идет о NLP производственного уровня на Python, spaCy является бесспорным отраслевым стандартом. spaCy разработан специально для использования в производственных условиях, предлагая высокую скорость, предварительно обученные статистические и трансформерные модели, а также интуитивно понятный API.
К сожалению, многие разработчики рассматривают spaCy как простой монолитный «черный ящик». Они загружают модель, запускают ее на тексте и принимают значения скорости обработки и ограничений извлечения по умолчанию. При масштабировании от локального прототипа до обработки миллионов документов эти настройки по умолчанию могут стать узкими местами в вычислительной системе, что приводит к задержкам, чрезмерному использованию памяти и упущению специфических для предметной области сущностей. Для создания высокопроизводительных конвейеров обработки текста необходимо понимать, как оптимизировать внутренний поток выполнения spaCy.
В этой статье мы рассмотрим три важных приема spaCy, которые должны быть в арсенале каждого разработчика для максимальной скорости обработки и настройки распознавания сущностей: выборочная загрузка конвейера, параллельная пакетная обработка и гибридное статистическое распознавание сущностей на основе правил.
Прежде чем начать, убедитесь, что у вас установлен spaCy, а также его облегченная универсальная англоязычная версия:
pip install spacy python -m spacy download en_core_web_sm
# 1. Выборочная загрузка конвейера и отключение компонентов
По умолчанию при загрузке предварительно обученной модели spaCy (например, en_core_web_sm) spaCy инициализирует полный конвейер обработки естественного языка. Этот конвейер обычно включает в себя:
- токенизатор
- теггер частей речи (теггер)
- парсер зависимостей (парсер)
- лемматизатор (lemmatizer)
- линейка атрибутов (attribute_ruler)
- распознаватель именованных сущностей (NER)
Хотя этот полный набор функций по умолчанию превосходен, он сопряжен со значительными вычислительными затратами. Если вашему приложению нужно только распознавать именованные сущности (NER), запуск парсера зависимостей и лемматизатора будет пустой тратой ресурсов процессора и памяти. И наоборот, если вы только очищаете текст и извлекаете леммы, запуск глубокой статистической модели NER будет крайне неэффективным. Вы можете оптимизировать это, выборочно исключая компоненты во время загрузки или временно отключая их во время выполнения с помощью менеджера контекста.
Этот наивный подход загружает и запускает каждый компонент по умолчанию в тексте, независимо от того, используются ли фактически выходные данные этих компонентов:
import spacy import time # Загрузка небольшой англоязычной модели nlp = spacy.load(«en_core_web_sm») texts = [«Apple рассматривает возможность покупки британского стартапа за 1 миллиард долларов»] * 1000 # Наивное выполнение: запускает теггер, парсер, лемматизатор и NER для каждого документа # Предположим, нас здесь интересуют только именованные сущности start_time = time.time() for text in texts: doc = nlp(text) entities = [(ent.text, ent.label_) for ent in doc.ents] duration_full = time.time() — start_time print(f»Полный конвейер обработал 1000 документов за: {duration_full:.4f} секунд»)
Выход:
Обработка 1000 документов в полном объеме заняла 2,8540 секунды.
Теперь давайте оптимизируем выполнение двумя конкретными способами. Во-первых, мы будем исключать ресурсоемкие, неиспользуемые компоненты, такие как парсер зависимостей, во время загрузки. Во-вторых, мы будем использовать nlp.select_pipes() для временного отключения компонентов при обработке определенных рабочих нагрузок.
import spacy import time # Оптимизация времени загрузки: Исключаем ресурсоемкий парсер и теггер с самого начала # Это сокращает время инициализации и потребление памяти nlp_optimized = spacy.load(«en_core_web_sm», exclude=[«parser», «tagger»]) texts = [«Apple рассматривает возможность покупки британского стартапа за 1 миллиард долларов»] * 1000 # Оптимизация контекстного менеджера, временно отключаем компоненты # Мы полностью исключили парсер и теггер, отключаем здесь линейку атрибутов и лемматизатор start_time = time.time() with nlp_optimized.select_pipes(disable=[«attribute_ruler», «lemmatizer»]): for text in texts: doc = nlp_optimized(text) entities = [(ent.text, ent.label_) for ent in doc.ents] duration_opt = time.time() — start_time print(f»Оптимизированный конвейер обработал 1000 документов в: «Ускорение: в раз быстрее!»)
Давайте сравним время выполнения:
Обработка 1000 документов в полном объеме заняла 2,8739 секунды. Обработка 1000 документов в оптимизированном режиме заняла 1,7859 секунды. Ускорение: в 1,61 раза быстрее!
В оптимизированном примере передача параметра exclude=[«parser», «tagger»] в spacy.load() полностью предотвращает загрузку этих компонентов в память. В альтернативном методе достижения практически того же результата мы передали параметр disable=[«attribute_ruler», «lemmatizer»], чтобы временно отключить их обработку. В результате, при обработке текста spaCy пропускает анализ зависимостей токенов и разметку частей речи, которые являются математически затратными, и сразу переходит к распознаванию сущностей. Это приводит к заметному ускорению без влияния на точность распознавания именованных сущностей, с еще более заметными преимуществами при больших масштабах.
# 2. Высокопроизводительная пакетная обработка с использованием nlp.pipe и распространения метаданных
Если вы обрабатываете большой корпус данных (например, DataFrames pandas, строки базы данных или необработанные текстовые файлы), вызов объекта nlp для отдельных строк в цикле (например, [nlp(text) for text in texts]) является антипаттерном.
Последовательная обработка не позволяет spaCy оптимизировать буферы памяти, выполнять операции группировки и использовать многоядерную параллелизацию. Кроме того, при обработке текста для хранения в базах данных или конвейеров ETL часто необходимо передавать метаданные (например, идентификатор записи, метку времени или категорию) через процесс обработки естественного языка, чтобы сопоставить полученные сущности с соответствующими строками базы данных.
Решение заключается в использовании nlp.pipe(). Этот метод обрабатывает документы как поток, буферизует их внутри и поддерживает многопроцессорную обработку. Установив as_tuples=True, вы можете передавать кортежи (текст, контекст) в spaCy. Он вернет пары (документ, контекст), позволяя передавать метаданные напрямую через конвейер.
Этот наивный подход выполняет обработку последовательно и использует ручное отслеживание индексов для выравнивания полученных документов с их идентификаторами в базе данных, что является ненадежным и медленным:
import spacy import time nlp = spacy.load(«en_core_web_sm», exclude=[«parser», «tagger»]) # Необработанные записи базы данных с уникальными идентификаторами records = [ {«id»: f»DB-REC-{i}», «text»: «Google был основан в сентябре 1998 года Ларри Пейджем и Сергеем Брином.»} for i in range(1000) ] # Последовательный цикл: медленные и управляемые вручную метаданные start_time = time.time() extracted_data = [] for i, record in enumerate(records): doc = nlp(record[«text»]) entities = [(ent.text, ent.label_) for ent in doc.ents] extracted_data.append({ «id»: record[«id»], «entities»: entities }) duration_seq = time.time() — start_time print(f»Последовательный цикл обработал 1000 документов за: {duration_seq:.4f} секунд»)
Выход:
Последовательный цикл обработал 1000 документов за 2,7375 секунды.
Здесь мы передаем данные потоком с помощью nlp.pipe, используя пакетную обработку и многоядерную параллелизацию (n_process), при этом идентификатор базы данных используется в качестве контекстной переменной:
import spacy import time # Сохраняйте импорты и определения глобальными, чтобы дочерние процессы могли их видеть nlp = spacy.load(«en_core_web_sm», exclude=[«parser», «tagger»]) # Оберните фактический код выполнения в основной блок if __name__ == '__main__': records = [ {«id»: f»DB-REC-{i}», «text»: «Google был основан в сентябре 1998 года Ларри Пейджем и Сергеем Брином.»} for i in range(1000) ] start_time = time.time() # Форматируйте входные данные как список кортежей (текст, контекст) stream_input = [(rec[«text»], rec[«id»]) for rec in records] # Потоковая обработка пакетов и использование всех доступных ядер ЦП с n_process=-1 extracted_data_pipe = [] docs_stream = nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1) for doc, rec_id in docs_stream: entities = [(ent.text, ent.label_) for ent in doc.ents] extracted_data_pipe.append({ «id»: rec_id, «entities»: entities }) duration_pipe = time.time() — start_time print(f»nlp.pipe обработал 1000 документов за: {duration_pipe:.4f} секунд») print(f»Ускорение: {duration_seq / duration_pipe:.2f}x быстрее!»)
Выход:
nlp.pipe обработал 1000 документов за 7,1310 секунд.
В оптимизированном фрагменте кода мы преобразуем входной набор данных в последовательность кортежей: (text_string, metadata_context). При вызове nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1):
- Параметр batch_size=256 указывает spaCy буферизовать и обрабатывать тексты группами по 256 фрагментов, минимизируя накладные расходы на внутренний цикл Python.
- Параметр n_process=-1 указывает spaCy автоматически определять количество ядер процессора в вашей системе и распараллеливать токенизацию и извлечение компонентов на всех доступных ядрах.
- Параметр as_tuples=True указывает spaCy выдавать пары (документ, контекст), гарантируя, что метаданные (идентификатор записи) будут идеально соответствовать обрабатываемому документу без необходимости использования массивов индексов или кода выравнивания списков вручную.
Внимательный читатель заметит, что время обработки параллельного пакетного кода фактически увеличилось по сравнению с предыдущим. Однако это связано с накладными расходами на настройку параллельного задания, и экономия станет очевидной по мере роста количества обрабатываемых документов.
Повторный запуск тех же фрагментов кода, что и выше, но с 10 000 записями вместо 1000, даст следующие результаты:
Последовательный цикл обработал 1000 документов за 27,6733 секунды. nlp.pipe обработал 1000 документов за 11,5444 секунды.
Вы можете увидеть, как сбережения будут продолжать расти за счет сложных процентов.
# 3. Гибридное распознавание именованных сущностей с помощью EntityRuler
Предварительно обученные статистические и трансформерные модели распознавания именованных сущностей невероятно эффективны для распознавания общих типов сущностей, таких как ORG, PERSON или DATE, в зависимости от контекста. Однако модели часто не могут распознать специфические для предметной области термины (например, артикулы товаров, устаревшие идентификаторы кодов или узкоспециализированные медицинские термины), поскольку они не были знакомы с ними во время обучения.
Тонкая настройка статистической модели глубокого обучения на основе пользовательских сущностей — одно из решений, но оно требует разметки тысяч предложений и сопряжено с риском «катастрофического забывания», при котором модель забывает, как распознавать стандартные сущности.
Более чистым и высокоэффективным решением является гибридный подход к распознаванию именованных сущностей (NER) с использованием EntityRuler из spaCy. EntityRuler позволяет определять шаблоны (используя регулярные выражения или словари на основе токенов) и внедрять их непосредственно в ваш конвейер обработки. Вы можете добавить его перед статистическим NER — для предварительной разметки детерминированных сущностей и помощи модели в принятии контекстных решений — или после него — в качестве резервного варианта или переопределения.
Разработчики часто пытаются устранить пробелы в статистическом распознавании именованных сущностей, применяя регулярные выражения к тексту после запуска конвейера spaCy, что приводит к необходимости ручного вычисления смещения координат и разрозненным структурам данных:
import spacy import re nlp = spacy.load(«en_core_web_sm») text = «Пожалуйста, проверьте системный идентификатор заявки: TKT-98421 на нашем корпоративном портале.» doc = nlp(text) # Стандартный статистический NER пропускает пользовательские идентификаторы заявок entities = [(ent.text, ent.label_) for ent in doc.ents] print(«До постобработки:», entities) # Патч регулярного выражения после обработки ticket_pattern = r»TKT-d+» matches = re.finditer(ticket_pattern, text) custom_ents = [] for match in matches: # Требуется сложное преобразование смещения символа в токен для построения диапазонов custom_ents.append((match.group(), «TICKET_ID»)) # Теперь у нас есть два несвязанных списка сущностей, которые необходимо объединить вручную print(«Сущности, выраженные регулярным выражением:», custom_ents)
Выход:
До постобработки: [] Сущности регулярного выражения: [('TKT-98421', 'TICKET_ID')]
Добавив компонент EntityRuler непосредственно в конвейер обработки данных, мы объединяем основанные на правилах регулярные выражения и статистический анализ в единый, унифицированный выходной файл doc.ents:
import spacy nlp = spacy.load(«en_core_web_sm») # Добавляем компонент entity_ruler в конвейер перед ner, чтобы он предварительно помечал сущности, но работает и после ruler = nlp.add_pipe(«entity_ruler», before=»ner») # Определяем шаблоны на уровне токенов, включая регулярные выражения patterns = [ # Сопоставление строк, начинающихся с «TKT-«, за которыми следуют цифры {«label»: «TICKET_ID», «pattern»: [{«TEXT»: {«REGEX»: «^TKT-d+$»}}]}, # Точное сопоставление определенных доменных фраз {«label»: «ORG», «pattern»: «corporate portal»} ] ruler.add_patterns(patterns) text = «Пожалуйста, ознакомьтесь с системным тикетом ID: TKT-98421 на нашем корпоративном портале.» doc = nlp(text) # Статистические и основанные на правилах сущности объединяются в doc.ents for ent in doc.ents: print(f»Entity: {ent.text:
Источник: www.kdnuggets.com

Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.