NER: Извлечение именованных сущностей с LLM
Введение
NER (Named Entity Recognition) — это задача извлечения именованных сущностей из текста: имён людей, организаций, мест, дат, количеств и других. С LLM NER стал значительно точнее и проще в настройке.
Что такое NER?
Текст:
"Илон Маск основал Tesla в 2003 году в Калифорнии."
Сущности:
Илон Маск → PERSON
Tesla → ORGANIZATION
2003 → DATE
Калифорнии → LOCATION
Типичные типы сущностей:
- PER — Person (имя человека)
- ORG — Organization (организация)
- LOC — Location (место)
- DATE — Date (дата)
- MONEY — Деньги
- QUANTITY — Количество
- PRODUCT — Продукт
- GPE — Geo-Political Entity (страна, город)
Классические подходы
CRF (Conditional Random Fields)
from sklearn_crfsuite import CRF
from sklearn_crfsuite import metrics
# Извлечение признаков
def word2features(sent, i):
word = sent[i]
return {'word': word, 'is_upper': word.isupper()}
# Обучение
crf = CRF(algorithm='lbfgs', c1=0.1, c2=0.1)
crf.fit(X_train, y_train)
# Предсказание
predictions = crf.predict(X_test)
Плюсы: ✅ Быстрое обучение ✅ Мало данных нужно ✅ Интерпретируемо
Минусы: ❌ Ручной feature engineering ❌ Плохо обобщается ❌ Не понимает контекст
BiLSTM-CRF
import torch
import torch.nn as nn
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_set, embed_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim,
bidirectional=True, batch_first=True)
self.fc = nn.Linear(hidden_dim * 2, tag_set)
self.transitions = nn.Parameter(
torch.randn(tag_set, tag_set)
)
def forward(self, x, tags=None):
embeds = self.embedding(x)
lstm_out, _ = self.lstm(embeds)
emissions = self.fc(lstm_out)
# CRF layer для последовательности
return self.crf_forward(emissions, tags)
Плюсы: ✅ Ловит контекст ✅ Учитывает последовательность тегов ✅ Лучше CRF
Минусы: ❌ Нужно много данных ❌ Долгое обучение ❌ Трудно адаптировать
NER с LLM
Zero-shot NER
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """Извлеки именованные сущности из текста.
Ответь в формате JSON:
{
"entities": [
{"text": "...", "type": "PERSON|ORG|LOC|DATE|..."}
]
}"""
},
{"role": "user", "content": "Apple Inc. купила стартап в Берлине в 2024 году."}
],
response_format={"type": "json_object"}
)
# Результат:
# {"entities": [
# {"text": "Apple Inc.", "type": "ORG"},
# {"text": "Берлине", "type": "LOC"},
# {"text": "2024", "type": "DATE"}
# ]}
Плюсы: ✅ Не нужно обучать модель ✅ Работает сразу ✅ Поддержка многих языков
Минусы: ❌ Дорого ❌ Медленно ❌ Может галлюцинировать
Few-shot NER
messages = [
{
"role": "system",
"content": "Извлекай сущности в формате JSON."
},
{"role": "user", "content": "Москва — столица России."},
{"role": "assistant", "content": '{"entities": [{"text": "Москва", "type": "LOC"}, {"text": "России", "type": "GPE"}]}'},
{"role": "user", "content": "Париж — столица Франции."},
{"role": "assistant", "content": '{"entities": [{"text": "Париж", "type": "LOC"}, {"text": "Франции", "type": "GPE"}]}'},
{"role": "user", "content": "Google основал Ларри Пейдж."}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
In-context learning с локальной моделью
# Используем llama.cpp для локального NER
import requests
prompt = """Извлеки именованные сущности из текста.
Пример 1:
Текст: "Обаму посетил Макрон в Париже."
Результат: {"entities": [{"text": "Обаму", "type": "PERSON"}, {"text": "Макрон", "type": "PERSON"}, {"text": "Париже", "type": "LOC"}]}
Текст: "Microsoft купила GitHub за 7.5 миллиардов долларов."
Результат: """
response = requests.post(
"http://localhost:8080/completion",
json={"prompt": prompt, "max_tokens": 200}
)
Fine-tuning для NER
Подготовка данных
{"text": "Илон Маск родился в Претории.", "entities": [{"text": "Илон Маск", "type": "PERSON", "start": 0, "end": 10}, {"text": "Претории", "type": "LOC", "start": 22, "end": 30}]}
{"text": "OpenAI находится в Сан-Франциско.", "entities": [{"text": "OpenAI", "type": "ORG", "start": 0, "end": 6}, {"text": "Сан-Франциско", "type": "LOC", "start": 19, "end": 32}]}
Fine-tuning через OpenAI
# Загрузка файла с примерами
file = client.files.create(
file=open("ner_training.jsonl", "rb"),
purpose="fine-tune"
)
# Создание fine-tune job
job = client.fine_tuning.jobs.create(
model="gpt-4o-mini",
training_file=file.id,
hyperparameters={
"n_epochs": 3,
"learning_rate_multiplier": 0.1
}
)
# Результат: ft:gpt-4o-mini:custom:ner-model-abc123
Fine-tuning open-source моделей
from transformers import AutoModelForTokenClassification, AutoTokenizer, TrainingArguments, Trainer
model = AutoModelForTokenClassification.from_pretrained(
"microsoft/deberta-v3-base",
num_labels=9, # BIO tagging: B-PER, I-PER, B-ORG, ...
id2label={
0: "O", 1: "B-PER", 2: "I-PER",
3: "B-ORG", 4: "I-ORG",
5: "B-LOC", 6: "I-LOC",
7: "B-DATE", 8: "I-DATE"
}
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/deberta-v3-base")
training_args = TrainingArguments(
output_dir="./ner-model",
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5,
evaluation_strategy="epoch"
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
eval_dataset=eval_dataset
)
trainer.train()
BIO тегирование
B-XXX — Begin of entity type XXX
I-XXX — Inside of entity type XXX
O — Outside any entity
Пример:
"Apple Inc. купила Google"
Apple → B-ORG
Inc. → I-ORG
купил → O
Google → B-ORG
Почему BIO?
Без BIO:
Apple → ORG, Inc. → ORG
Модель не знает, что это одна сущность
С BIO:
Apple → B-ORG, Inc. → I-ORG
Модель понимает последовательность
NER с spaCy + LLM
import spacy
from spacy.tokens import Doc
nlp = spacy.load("ru_core_news_md")
# Нейроспаcy для базового NER
text = "Вчерa Путин встретился с Макроном в Москве."
doc = nlp(text)
for ent in doc.ents:
print(ent.text, ent.label_)
# вчерa → DATE
# Путин → PERSON
# Макроном → PERSON
# Москве → LOC
# LLM для уточнения
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"Уточни сущности: {[(ent.text, ent.label_) for ent in doc.ents]}"
}]
)
NER с LlamaIndex
from llama_index.core import Document
from llama_index.core.extractors import (
EntityExtractor,
MetadataExtractor
)
documents = [
Document(text="OpenAI выпустила GPT-4 в марте 2023.")
]
extractors = [
EntityExtractor(
model="gpt-4o",
description="извлеки организации, людей, даты, места"
)
]
metadata = MetadataExtractor(
extractors=extractors
).extract_from_documents(documents)
# Результат:
# entities: [OpenAI, GPT-4, март 2023]
Оценка NER
Метрики
Precision: из всех предсказанных сущностей, сколько правильных
Recall: из всех правильных сущностей, сколько нашли
F1: гармоническое среднее
Пример:
True: [Москва, Россия]
Predicted: [Москва, Берлин]
Precision: 1/2 = 50%
Recall: 1/2 = 50%
F1: 50%
Код оценки
from sklearn.metrics import classification_report
# y_true — реальные метки
# y_pred — предсказанные
print(classification_report(y_true, y_pred, target_names=[
"O", "B-PER", "I-PER", "B-ORG", "I-ORG",
"B-LOC", "I-LOC", "B-DATE", "I-DATE"
]))
# precision recall f1-score
# O 0.98 0.99 0.98
# B-PER 0.92 0.90 0.91
# I-PER 0.90 0.88 0.89
# B-ORG 0.94 0.93 0.93
Практические применения
1. Анализ новостей
# Извлечение сущностей из новостных статей
articles = load_news_articles()
for article in articles:
entities = ner_extract(article["text"])
print(f"Организации: {[e for e in entities if e['type'] == 'ORG']}")
print(f"Люди: {[e for e in entities if e['type'] == 'PERSON']}")
2. Автоматическая категоризация
# Категоризация документов по сущностям
doc = "Соглашение между Россией и Китаем о торговле"
ents = ner_extract(doc)
if any(e['type'] == 'GPE' for e in ents):
category = "geopolitics"
if any(e['type'] == 'MONEY' for e in ents):
category = "finance"
3. Построение knowledge graph
# Извлечение сущностей и связей
text = "Маск основал Tesla в Калифорнии."
ents = ner_extract(text)
# [Musk→PERSON, Tesla→ORG, California→LOC]
# Затем relation extraction для связей:
# (Musk, founded, Tesla)
# (Tesla, located_in, California)
4. Автоматическое аннотирование
# Аннотирование медицинских записей
medical_text = "Пациенту назначен Амоксициллин 500мг."
ents = ner_extract(medical_text, types=["DRUG", "DOSAGE", "DISEASE"])
# [Амоксициллин → DRUG, 500мг → DOSAGE]
Open Source модели для NER
Модели:
✅ spaCy (en/ru/de/fr)
✅ Stanza (Stanford NLP)
✅ DeBERTa-v3 (HuggingFace)
✅ mBERT (multilingual BERT)
✅ XLM-RoBERTa
✅ LLM-based (GPT, Llama)
API:
✅ Google NLP API
✅ AWS Comprehend
✅ Azure AI Language
✅ GCP Cloud NLP
Итоги
NER с LLM стал значительно доступнее. Zero-shot подход работает для большинства задач, а fine-tuning даёт дополнительную точность.
Ключевые выводы:
- Zero-shot NER с GPT-4o работает отлично
- Fine-tuning DeBERTa даёт хороший баланс цена/качество
- BIO тегирование — стандарт для sequence labeling
- LLM упрощают кастомные сущности