Что же такое, этот ваш NER?
Named Entity Recognition (NER) — это задача в области NLP (Natural Language Processing), направленная на выделение фрагментов в тексте, относящихся к классам, таким как имена людей, названия организаций, даты, местоположения, суммы денег и любые другие классы, на определение которых можно обучить модель
Как работает NER
Теперь рассмотрим шаги, которые выполняет система распознавания именованных объектов (NER):
Токенизация текста: Текст разбивается на отдельные слова или токены.
-
Выделение признаков: Каждому токену назначаются признаки, описывающие его окружение и контекст, такие как:
Предыдущие и следующие слова
Части речи
Другие лингвистические характеристики
Применение модели: Модель анализирует признаки каждого токена и определяет, является ли он именованной сущностью.
-
Объединение результатов: Результаты анализа токенов объединяются для формирования именованных сущностей, которым назначаются соответствующие метки классов, например такие как:
«PER» (персона)
«ORG» (организация)
Постобработка: Происходит дополнительная обработка для уточнения результатов и исправления ошибок.
Применение
NER применяется если нужный элемент модели невозможно определить через синонимы, regex и т.д., а создание правил поиска по сложности превосходит программирование логики.
NER является важным компонентом многих NLP-приложений, таких как извлечение информации, анализ тональности, вопросно-ответные системы и многие другие (голосовые ассистенты, смс, телефонные разговоры).
Python библиотеки для NER
Из всего многообразия существующих библиотек я выделяю эти:
spaCy — В ней есть функции NER, POS-тегирования, разбора зависимостей, векторов слов и многое другое. Примечательно, что в предобученных моделях имеются модели, поддерживающие русский язык
StanfordCoreNLP — Он предоставляет простой API для таких задач обработки текста, как токенизация, тегирование частей речи, реконфигурация именованных сущностей, синтаксический разбор, синтаксический разбор зависимостей и другие.
SpaCy
Об этой библиотеке мы поговорим ввиду банальной простоты ее использования, а также моделях на русском языке со стока
Установка SpaCy
GPU версия (рекомендую для большей производительности)
Здесь представлен пример для карт Nvidia. Перед установкой библиотек вам потребуется установить CUDA, прямая ссылка
PyPi:
pip install 'spacy[cuda12x]'
pip install cupy-cuda12x
Anaconda
conda create -n NER python=3.10.9 spacy spacy-transformers cupy -c conda-forge
CPU версия (только если нет видеокарты)
PyPi:
pip install spacy
Anaconda
conda create -n NER python=3.10.9 spacy -c conda-forge
Пример кода
В примере кода мы подгрузим предобученную русскую модель
Установка модели:
python -m spacy download ru_core_news_sm
Тестовый код:
import spacy
nlp = spacy.load("ru_core_news_sm")
doc = nlp("Apple рассматривает возможность покупки британского стартапа за 1 миллиард долларов")
for token in doc:
print(token.text, token.pos_, token.dep_)
Вывод:
Apple PROPN nsub
рассматривает VERB ROOT
возможность NOUN obj
покупки NOUN nmod
британского ADJ amod
стартапа NOUN nmod
за ADP case
1 NUM nummod
миллиард NOUN nummod:gov
долларов NOUN obl
Подготовка к обучению
С предобученной моделью мы ознакомились, пора бы научиться их обучать.
Понятие обучения
Обучение — это итерационный процесс, при котором прогнозы модели сравниваются с эталонными значениями для оценки лосса. Затем лосс используется для вычисления градиента весов с помощью метода обратного распространения. Градиенты показывают, как необходимо скорректировать веса, чтобы улучшить точность прогнозов модели.
Примечательно, что SpaCy защищает модели от переобучения. Как только начнется переобучение, тренировка модели будет прервана.
Конфиг для обучения
Файл config.cfg
для обучения модели SpaCy содержит различные параметры и настройки, которые позволяют адаптировать модель к конкретной задаче. Давайте рассмотрим подробно, что может включать данный файл:
1. Данные:
Этот раздел содержит информацию о датасетах, которые будут использоваться для обучения, проверки и тестирования модели. Например:
Пути к файлам тренировочного и тестового наборов данных.
Формат данных (JSON, CSV и др.)
2. Компоненты модели:
Этот раздел описывает компоненты NLP, которые используются в модели, такие как:
Токенизатор (Tokenizer)
Лемматизатор (Lemmatizer)
Часть речи (Tagger)
Распознавание именованных сущностей (NER)
Анализ зависимостей (Parser)
3. Оптимизаторы:
Здесь описываются методы оптимизации и параметры обучения, такие как:
Тип оптимизатора (Adam, SGD)
Параметры оптимизатора (коэффициент обучения, коэффициенты регуляризации и др.)
4. Гиперпараметры:
Этот раздел включает параметры, которые управляют процессом обучения:
Размер батча
Количество эпох
Частота сохранения модели на диск
5. Векторные представления:
Этот раздел определяет векторные представления слов (вектора Word2Vec, GloVe и др.), которые будут использоваться для инициализации эмбеддингов.
6. Настройки среды:
Здесь находятся настройки для среды выполнения, такие как:
Использование GPU или CPU
Пути к исходным файлам и директориям
7. Настройки логирования:
Этот раздел может включать параметры для логирования процесса обучения:
Уровень логирования (DEBUG, INFO, WARNING)
Формат логов и пути к ним
8. Метрики:
Здесь описываются метрики, которые будут использоваться для оценки модели:
Точность (Accuracy)
Полнота (Recall)
F1-меры и др.
Пример части файла config.cfg:
[paths]
train = "path/to/train_data"
dev = "path/to/dev_data"
vectors = "path/to/vectors"
[nlp]
lang = "ru"
pipeline = ["tok2vec","ner"]
batch_size = 1000
[components.tok2vec]
factory = "tok2vec"
[training.optimizer]
@optimizers = "Adam.v1"
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
Создание конфигурации обучения (CPU/GPU)
Вообще, в официальной документации SpaCy имеется генератор конфигураций, но если взять его конфиг, то за редким случаем у вас выйдет точность выше 64%
Поэтому я дам вам свои конфиги для CPU и GPU, показавшие среднюю точность 99% обученных на них моделей
GPU
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
[nlp]
lang = "ru"
pipeline = ["tok2vec","ner"]
batch_size = 1000
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
[components]
[components.ner]
factory = "ner"
incorrect_spans_key = null
moves = null
scorer = {"@scorers":"spacy.ner_scorer.v1"}
update_with_oracle_cut_size = 100
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = true
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
width = 256
depth = 8
window_size = 1
maxout_pieces = 3
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = true
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = true
limit = 0
augmenter = null
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
accumulate_gradient = 1
patience = 1600
max_epochs = 0
max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
before_to_disk = null
before_update = null
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
get_length = null
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = true
[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = false
eps = 0.00000001
learn_rate = 0.001
[training.score_weights]
ents_f = 1.0
ents_p = 0.0
ents_r = 0.0
ents_per_type = null
[pretraining]
[initialize]
vectors = ${paths.vectors}
init_tok2vec = ${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
[initialize.tokenizer]
CPU
[paths]
train = null
dev = null
vectors = null
[system]
gpu_allocator = null
[nlp]
lang = "ru"
pipeline = ["tok2vec","ner"]
batch_size = 1000
[components]
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
rows = [5000, 1000, 2500, 2500]
include_static_vectors = true
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
width = 256
depth = 8
window_size = 1
maxout_pieces = 3
[components.ner]
factory = "ner"
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
[corpora]
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
[training.optimizer]
@optimizers = "Adam.v1"
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
[initialize]
vectors = ${paths.vectors}
Отличия от стоковой конфигурации
Отличия имеются только для GPU версии, ввиду того что там включен gold preprocessing
. Если ваши датасеты тщательно аннотированы и вы уверены в их качестве, включение gold preprocessing
рекомендуется для повышения точности и надежности модели.
Создание датасетов
Для обучения модели необходимо разметить данные и создать датасеты, чем мы сейчас и займемся. Для начала, нам следует определиться с классами, которые будет определять наша модель, а также найти данные, которые будут размечены в датасеты. Количество строк в каждом классе должно быть не менее 1000, иначе возможны проблемы с точностью распознавания из-за недостаточного количества данных.
Поиск данных на разметку
Я выделяю три основных места, в которых мы можем найти данные на разметку:
HuggingFace — огромное количество данных на любой случай жизни и пусть датасетов под NER всего ничего, но взяв условный csv можно достать из него данные и разметить
Kaggle — аналогично первому пункту
Google Dorking — поиск информации, проиндексированной поисковой системой. Можно извлечь файлы с нужной информацией, я, к примеру, таким образом набирал названия компаний на разметку
Разметка данных
Теперь приступим, непосредственно, к разметке данных.
Перед разметкой собранные ранее данные следует собрать в txt файл
-
Заходим на сайт
-
Открываем файл и настраиваем классы
Размечаем данные до конца, одна строка может иметь как один, так и несколько классов, все зависит от формата данных и их наполнения
Загружаем размеченные данные через
Annotations -> Export
После выполнения вышеуказанных действий произойдет загрузка файла
annotations.json
Конвертация данных
Как мы видим. загруженный файл имеет формат JSON, который нам не подходит. Необходимо преобразовать его в поддерживаемый формат SpaCy для последующего создания датасетов. В этом нам может помочь следующий Python код:
import json
# Исходный JSON
data = open(input('Введите путь до JSON файла: ')).read()
# Загружаем данные из JSON
json_data = json.loads(data)
# Конвертируем данные в необходимый формат
converted_data = []
for item in json_data["annotations"]:
text, annotation = item
entities = annotation["entities"]
converted_data.append((text, entities))
# Сохраняем результат в training_data.txt
with open("training_data.txt", "w", encoding="utf-8") as f:
for entry in converted_data:
f.write(f"{entry}\n")
Создание датасетов
Вот мы и подобрались к заключительному этапу подготовки данных для обучения модели. Для конвертации training_data.txt
в датасеты, нам потребуется запустить еще один код, вот он:
import spacy
from spacy.tokens import DocBin
from collections import defaultdict
import math
import random
# Загружаем модель SpaCy
nlp = spacy.blank("ru")
# Функция для конвертации данных в формат spacy
def convert_to_spacy_format(data, nlp):
doc_bin = DocBin()
for item in data:
text, annotations = item
doc = nlp.make_doc(text)
ents = []
for start, end, label in annotations:
span = doc.char_span(start, end, label=label)
if span:
ents.append(span)
doc.ents = ents
doc_bin.add(doc)
return doc_bin
# Чтение данных на конвертацию из файла
data = [
eval(line.strip().rstrip(","))
for line in open("training_data.txt", encoding="utf-8")
]
# Группируем данные по классам
class_data = defaultdict(list)
for item in data:
if isinstance(item, tuple) and len(item) == 2:
_, annotations = item
for _, _, label in annotations:
class_data[label].append(item)
break # Предполагаем, что каждый текст относится к одному классу
# Устанавливаем соотношение для train, dev и test наборов
train_ratio = 0.7
dev_ratio = 0.2
test_ratio = 0.1
train_data = []
dev_data = []
test_data = []
# Вычисляем количество элементов для каждого класса
for label, items in class_data.items():
n_items = len(items)
n_train = math.ceil(train_ratio * n_items)
n_dev = math.ceil(dev_ratio * n_items)
n_test = n_items - n_train - n_dev # Оставшееся количество идёт в тестовый набор
# Добавляем данные в наборы
train_data.extend(items[:n_train])
dev_data.extend(items[n_train:n_train + n_dev])
test_data.extend(items[n_train + n_dev:])
# Перемешиваем данные внутри каждого набора для случайного распределения
random.shuffle(train_data)
random.shuffle(dev_data)
random.shuffle(test_data)
# Конвертируем данные в формат spacy и сохраняем
train_doc_bin = convert_to_spacy_format(train_data, nlp)
train_doc_bin.to_disk("train.spacy")
dev_doc_bin = convert_to_spacy_format(dev_data, nlp)
dev_doc_bin.to_disk("dev.spacy")
test_doc_bin = convert_to_spacy_format(test_data, nlp)
test_doc_bin.to_disk("test.spacy")
По окончанию работы у нас появится 3 файла: train.spacy (данные на которых будет обучаться модель, 70% всех данных), dev.spacy (данные, которыми будут проверяться предсказания модели, 20% всех данных), test.spacy (данные для бенчмарка, 10% всех данных)
Обучение модели
Итак, наконец у нас есть три файла с датасетами и конфиг, можно начать обучение
GPU:
python -m spacy train config.cfg --output ./output --paths.train ./train.spacy --paths.dev ./dev.spacy -g 0
CPU:
python -m spacy train config.cfg --output ./output --paths.train ./train.spacy --paths.dev ./dev.spacy
Отлично, обучение запущено. Теперь давайте разберемся со значением каждой колонки:
#
- Это порядковый номер итерации в процессе обучения модели.LOSS TOK2VEC
- Показывает потери (ошибки) во время обучения компонентаtok2vec
. Этот компонент используется для векторизации токенов (слов) в модели.LOSS NER
- Показывает потери (ошибки) во время обучения компонента NER (Named Entity Recognition). Этот компонент отвечает за распознавание именованных сущностей в тексте.ENTS_F
- Это метрика F1-score для распознавания именованных сущностей. Она представляет собой гармоническое среднее между точностью и полнотой.ENTS_P
- Показывает точность (precision) распознавания именованных сущностей. Она отражает долю правильно классифицированных именованных сущностей среди всех предсказанных.ENTS_R
- Показывает полноту (recall) распознавания именованных сущностей. Это отношение правильно классифицированных именованных сущностей к общему количеству настоящих именованных сущностей.SCORE
- Это общая оценка или показатель успеха модели на данной итерации обучения. Этот показатель может учитывать различные метрики и параметры модели для оценки её качества. Умножение на 100 этого значения даст процент успеха модели.
По окончанию процесса лучшая модель сохранится по пути
./output/model-best
Использование готовой модели
Использовать модель достаточно просто. Для этого создайте файл for_model.txt
и построчно внесите в него значения, которые хотите обработать моделью. Далее запустите следующий код:
import spacy
import re
import openpyxl
# Загрузите модель для русского языка, например, 'ru_core_news_sm'
nlp_ru = spacy.load("./output/model-best")
f = open("for_model.txt", encoding="utf-8")
output_xlsx_file = "output_data.xlsx"
# Функция для извлечения сущностей заданного типа из документа
def extract_entities(doc, entity_type):
return [ent.text for ent in doc.ents if ent.label_ == entity_type]
# Получаем список уникальных типов сущностей из модели
entity_types = nlp_ru.pipe_labels["ner"]
# Создаем workbook и worksheet для xlsx файла
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "NER Output"
# Записываем заголовок колонок
ws.append(["Input Text"] + entity_types)
for input in f:
line = input.strip()
doc_ru = nlp_ru(line)
row_data = [line]
# Извлекаем сущности для каждого типа и удаляем недопустимые компании
for entity_type in entity_types:
entities = extract_entities(doc_ru, entity_type)
# Добавляем сущности в соответствующую колонку по их классу
row_data.append(", ".join(list(set(entities))))
# Записываем строку в xlsx файл
ws.append(row_data)
# Закрываем вводной файл
f.close()
# Сохраняем и закрываем рабочую книгу
wb.save(output_xlsx_file)
wb.close()
print(f"Данные успешно сохранены в файл {output_xlsx_file}")
По окончанию работы кода будет создан файл
output_data.xlsx
.
На этом все!
ValeriyPushkarev
Ой как сложно.
Может сделать так:
1) Разобрать грамматически
2) Найти все типы грамматического дерева, соответствующие именованным сущностям (adj+noun, noun, etc)
3) Используя словарь синонимов (Word2Vec, например), проверить по интересующим типам (по близости векторов)
В чем магия - с вероятностью 99% именованные сущности это подлежащее, определение (существительное+прилагательное), и т.д.
Плюсы - машинное обучение нужно только на этапе 2. Сама модель - просто образец Explainable Machine Learning.
И еще небольшой плюс:
Нет, при таком подходе хватит пары абзацев.
whynothacked Автор
Неплохой вариант развития событий, просто я действовал отталкиваясь от того что у нас только NER (в случаях когда не нужно определять что существительное, что прилагательное и тп, а просто определять по парочке классов данные текста, будь тоназвания компаний или что-то еще)
ValeriyPushkarev
Еще у вас появилась необходимость в 1000 примерах (чтобы найти контекстные синонимы Четверг-дата).
А для контекстных синонимов уже есть добрый десяток решений.
Ну и бонус - вы получаете сколько угодно классов именованных сущностей (без необходимости разметки).
Т.е. в общем пайплайн - находим все, что попадает под определение именованных сущностей, определяем класс.