Фаза 05 · урок 06
Распознавание именованных сущностей
Цель урока: «Apple sued Google over its iPhone search deal in the US.» Пять сущностей: Apple (ORG), Google (ORG), iPhone (PRODUCT), search deal (возможно), US (GPE). Хорошая система NER извлекает их все с корректными типами. Плохая пропускает…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Проблема
- Концепция
- Соберите это
- Шаг 1: вспомогательные функции для BIO-разметки
- Шаг 2: созданные вручную признаки
- Шаг 3: простой базовый вариант на правилах и словаре
- Шаг 4: шаг с CRF (набросок, не полная реализация)
- Шаг 5: что добавляет BiLSTM-CRF
- Примените это
- NER на основе LLM (вариант 2026 года)
- Где классический NER всё ещё побеждает
- Где это разваливается
- Подготовьте к поставке
- Упражнения
- Ключевые термины
- Дополнительное чтение
Извлеките имена. Звучит просто — пока не столкнётесь с неоднозначными границами, вложенными сущностями и отраслевым жаргоном.
Тип: Сборка Языки: Python Предварительные требования: Фаза 5 · 02 (BoW + TF-IDF), Фаза 5 · 03 (Векторные представления слов) Время: ~75 минут
Проблема
«Apple sued Google over its iPhone search deal in the US.» Пять сущностей: Apple (ORG), Google (ORG), iPhone (PRODUCT), search deal (возможно), US (GPE). Хорошая система NER извлекает их все с корректными типами. Плохая пропускает iPhone, путает Apple — фрукт — с Apple — компанией — и помечает «US» как PERSON.
NER — рабочая лошадка, лежащая в основе любого конвейера структурированного извлечения. Разбор резюме, сканирование журналов соответствия требованиям, анонимизация медицинских карт, понимание поисковых запросов, привязка ответов чат-бота к фактам, извлечение данных из юридических контрактов. Его почти никогда не видно; от него всегда зависишь.
Этот урок проходит классический путь (основанные на правилах методы, HMM, CRF) к современному (BiLSTM-CRF, затем трансформеры). Каждый шаг решает конкретное ограничение предыдущего. В этом и состоит урок.
Концепция
BIO-разметка (или BILOU) превращает извлечение сущностей в задачу последовательностной разметки. Пометьте каждый токен как B-TYPE (начало сущности), I-TYPE (внутри сущности) или O (вне любой сущности).
Apple B-ORG
sued O
Google B-ORG
over O
its O
iPhone B-PRODUCT
search O
deal O
in O
the O
US B-GPE
. O
Многотокенные сущности образуют цепочку: New B-GPE, York I-GPE, City I-GPE. Модель, понимающая BIO, может извлекать произвольные отрезки.
Развитие архитектур:
- Основанные на правилах методы. Регулярные выражения + поиск по газетирам. Высокая точность для известных сущностей, нулевое покрытие для новых.
- HMM. Скрытая марковская модель (Hidden Markov Model). Вероятность эмиссии токена при данном теге, вероятность перехода от тега к тегу. Декодирование Витерби. Обучается на размеченных данных.
- CRF. Условное случайное поле (Conditional Random Field). Похоже на HMM, но дискриминативно, поэтому можно смешивать произвольные признаки (форма слова, регистр, соседние слова). Даже в 2026 году классическая производственная рабочая лошадка для развёртываний с ограниченными ресурсами.
- BiLSTM-CRF. Нейронные признаки вместо созданных вручную. LSTM читает предложение в обоих направлениях, а слой CRF поверх него обеспечивает согласованные последовательности тегов.
- На основе трансформеров. Дообучите BERT с головой классификации токенов. Наилучшая точность. Больше всего вычислений.
ner-bio-tagging
Соберите это
Шаг 1: вспомогательные функции для BIO-разметки
def spans_to_bio(tokens, spans):
labels = ["O"] * len(tokens)
for start, end, label in spans:
labels[start] = f"B-{label}"
for i in range(start + 1, end):
labels[i] = f"I-{label}"
return labels
def bio_to_spans(tokens, labels):
spans = []
current = None
for i, label in enumerate(labels):
if label.startswith("B-"):
if current:
spans.append(current)
current = (i, i + 1, label[2:])
elif label.startswith("I-") and current and current[2] == label[2:]:
current = (current[0], i + 1, current[2])
else:
if current:
spans.append(current)
current = None
if current:
spans.append(current)
return spans
>>> tokens = ["Apple", "sued", "Google", "over", "iPhone", "sales", "."]
>>> labels = ["B-ORG", "O", "B-ORG", "O", "B-PRODUCT", "O", "O"]
>>> bio_to_spans(tokens, labels)
[(0, 1, 'ORG'), (2, 3, 'ORG'), (4, 5, 'PRODUCT')]
Шаг 2: созданные вручную признаки
Для классического (не нейронного) NER признаки решают всё. Полезны следующие:
def token_features(token, prev_token, next_token):
return {
"lower": token.lower(),
"is_upper": token.isupper(),
"is_title": token.istitle(),
"has_digit": any(c.isdigit() for c in token),
"suffix_3": token[-3:].lower(),
"shape": word_shape(token),
"prev_lower": prev_token.lower() if prev_token else "<BOS>",
"next_lower": next_token.lower() if next_token else "<EOS>",
}
def word_shape(word):
out = []
for c in word:
if c.isupper():
out.append("X")
elif c.islower():
out.append("x")
elif c.isdigit():
out.append("d")
else:
out.append(c)
return "".join(out)
word_shape("iPhone") возвращает xXxxxx. word_shape("USA-2024") возвращает XXX-dddd. Шаблоны капитализации — сильный сигнал для имён собственных.
Шаг 3: простой базовый вариант на правилах и словаре
ORG_GAZETTEER = {"Apple", "Google", "Microsoft", "OpenAI", "Meta", "Amazon", "Netflix"}
GPE_GAZETTEER = {"US", "USA", "UK", "India", "Germany", "France"}
PRODUCT_GAZETTEER = {"iPhone", "Android", "Windows", "ChatGPT", "Claude"}
def rule_based_ner(tokens):
labels = []
for token in tokens:
if token in ORG_GAZETTEER:
labels.append("B-ORG")
elif token in GPE_GAZETTEER:
labels.append("B-GPE")
elif token in PRODUCT_GAZETTEER:
labels.append("B-PRODUCT")
else:
labels.append("O")
return labels
Промышленные газетиры содержат миллионы записей, собранных из Wikipedia и DBpedia. Покрытие хорошее. Дизамбигуация (Apple как компания или фрукт) ужасна. Поэтому статистические модели победили.
Шаг 4: шаг с CRF (набросок, не полная реализация)
Полный CRF с нуля в 50 строках не будет понятен без основ теории вероятностей. Вместо этого используйте sklearn-crfsuite:
import sklearn_crfsuite
def to_features(tokens):
out = []
for i, tok in enumerate(tokens):
prev = tokens[i - 1] if i > 0 else ""
nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
out.append({
"word.lower()": tok.lower(),
"word.isupper()": tok.isupper(),
"word.istitle()": tok.istitle(),
"word.isdigit()": tok.isdigit(),
"word.suffix3": tok[-3:].lower(),
"word.shape": word_shape(tok),
"prev.word.lower()": prev.lower(),
"next.word.lower()": nxt.lower(),
"BOS": i == 0,
"EOS": i == len(tokens) - 1,
})
return out
crf = sklearn_crfsuite.CRF(algorithm="lbfgs", c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True)
X_train = [to_features(s) for s in sentences_tokenized]
crf.fit(X_train, bio_labels_train)
c1 и c2 — L1- и L2-регуляризация. all_possible_transitions=True позволяет модели усвоить, что незаконные последовательности (например, I-ORG после O) маловероятны: так CRF обеспечивает согласованность BIO, и вам не приходится писать ограничение самостоятельно.
Шаг 5: что добавляет BiLSTM-CRF
Признаки становятся обучаемыми. Входы: векторные представления токенов (GloVe или fastText). LSTM читает слева направо и справа налево. Конкатенированные скрытые состояния проходят через выходной слой CRF. CRF по-прежнему обеспечивает согласованность последовательности тегов; LSTM заменяет созданные вручную признаки обученными.
import torch
import torch.nn as nn
class BiLSTM_CRF_Head(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, n_labels):
super().__init__()
self.embed = 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, n_labels)
def forward(self, token_ids):
e = self.embed(token_ids)
h, _ = self.lstm(e)
emissions = self.fc(h)
return emissions
Для слоя CRF используйте torchcrf.CRF (pip install pytorch-crf). Прирост по сравнению с CRF на признаках, созданных вручную, измерим, но меньше, чем можно ожидать, если только у вас нет десятков тысяч размеченных предложений.
Примените это
spaCy поставляет готовый для производства NER прямо из коробки.
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple sued Google over its iPhone search deal in the US.")
for ent in doc.ents:
print(f"{ent.text:20s} {ent.label_}")
Apple ORG
Google ORG
iPhone ORG
US GPE
Обратите внимание: iPhone помечен как ORG, а не как PRODUCT — малая модель spaCy слабо покрывает сущности-продукты. Большая модель (en_core_web_lg) справляется лучше. Модель-трансформер (en_core_web_trf) — ещё лучше.
Hugging Face для NER на основе BERT:
from transformers import pipeline
ner = pipeline("ner", model="dslim/bert-base-NER", aggregation_strategy="simple")
print(ner("Apple sued Google over its iPhone in the US."))
[{'entity_group': 'ORG', 'word': 'Apple', ...},
{'entity_group': 'ORG', 'word': 'Google', ...},
{'entity_group': 'MISC', 'word': 'iPhone', ...},
{'entity_group': 'LOC', 'word': 'US', ...}]
aggregation_strategy="simple" объединяет смежные токены B-X, I-X в один отрезок. Без неё вы получите метки на уровне токенов и будете вынуждены объединять их самостоятельно.
NER на основе LLM (вариант 2026 года)
NER с LLM в zero-shot- и few-shot-режимах теперь конкурентоспособен с дообученными моделями во многих предметных областях и значительно лучше там, где размеченных данных мало.
- Промптинг zero-shot. Дайте LLM список типов сущностей и пример схемы. Попросите вывести JSON. Работает из коробки; точность на новых предметных областях умеренная.
- Промптинг в стиле ZeroTuneBio. Разложите задачу на извлечение кандидатов → объяснение смысла → суждение → повторную проверку. Многоэтапный промпт (не одноразовый) заметно повышает точность биомедицинского NER. Тот же паттерн работает для юридических, финансовых и научных областей.
- Динамический промптинг с RAG. Для каждого инференс-вызова извлекайте наиболее похожие размеченные примеры из небольшого начального аннотированного набора; few-shot-промпт собирайте на лету. В бенчмарках 2026 года это повышает F1 биомедицинского NER GPT-4 на 11–12 % по сравнению со статическим промптингом.
- Декомпозиция по типам сущностей. В длинных документах один вызов, извлекающий все типы сущностей сразу, теряет полноту по мере роста длины. Выполняйте один проход извлечения для каждого типа сущностей. Стоимость инференса выше, точность существенно выше. Это стандартный паттерн для клинических заметок и юридических контрактов.
Рекомендация для производства по состоянию на 2026 год: начните с zero-shot-базового варианта на LLM, прежде чем собирать обучающие данные. Часто F1 достаточно хорош, и дообучение вообще не требуется.
Где классический NER всё ещё побеждает
Даже при доступности LLM классический NER побеждает, когда:
- Бюджет задержки меньше 50 мс.
- У вас тысячи размеченных примеров и требуется F1 98 % или выше.
- В предметной области стабильная онтология, для которой предобученный CRF или BiLSTM хорошо переносится.
- Нормативные ограничения требуют локальной негенеративной модели.
Где это разваливается
- Сдвиг домена. NER, обученный на CoNLL, на юридических контрактах работает хуже газетира. Дообучите на своём домене.
- Вложенные сущности. «Bank of America Tower» одновременно ORG и FACILITY. Стандартный BIO не может представить перекрывающиеся отрезки. Нужен вложенный NER (многопроходные или основанные на отрезках модели).
- Длинные сущности. «United States Federal Deposit Insurance Corporation.» Токен-уровневые модели иногда разбивают это на части. Используйте
aggregation_strategyили постобработку. - Редкие типы. В медицинском NER встречаются метки вроде DRUG_BRAND, ADVERSE_EVENT, DOSE. Модели общего назначения о них ничего не знают. Начинайте со Scispacy и BioBERT.
Подготовьте к поставке
Сохраните как outputs/skill-ner-picker.md:
---
name: ner-picker
description: Pick the right NER approach for a given extraction task.
version: 1.0.0
phase: 5
lesson: 06
tags: [nlp, ner, extraction]
---
Given a task description (domain, label set, language, latency, data volume), output:
1. Approach. Rule-based + gazetteer, CRF, BiLSTM-CRF, or transformer fine-tune.
2. Starting model. Name it (spaCy model ID, Hugging Face checkpoint ID, or "custom, trained from scratch").
3. Labeling strategy. BIO, BILOU, or span-based. Justify in one sentence.
4. Evaluation. Use `seqeval`. Always report entity-level F1 (not token-level).
Refuse to recommend fine-tuning a transformer for under 500 labeled examples unless the user already has a pretrained domain model. Flag nested entities as needing span-based or multi-pass models. Require a gazetteer audit if the user mentions "production scale" and labels are unchanged from CoNLL-2003.
Упражнения
- Легко. Реализуйте
bio_to_spans(обратную кspans_to_bioфункцию) и проверьте согласованность обхода туда-обратно на 10 предложениях. - Средне. Обучите приведённый выше CRF из sklearn-crfsuite на английском наборе данных CoNLL-2003 NER. Сообщите F1 по каждой сущности с помощью
seqeval. Типичный результат: ~84 F1. - Сложно. Дообучите
distilbert-base-casedна предметно-специфичном наборе данных NER (медицинском, юридическом или финансовом). Сравните с малой моделью spaCy. Задокументируйте проверки утечки данных и опишите то, что вас удивило.
Ключевые термины
| Термин | Как обычно говорят | Что это на самом деле означает |
|---|---|---|
| NER | Извлечь имена | Размечать отрезки токенов типами (PERSON, ORG, GPE, DATE, …). |
| BIO | Схема разметки | B-X начинает, I-X продолжает, O находится вне сущности. |
| BILOU | Улучшенный BIO | Добавляет L-X (последний), U-X (единичный) для более чистых границ. |
| CRF | Структурированный классификатор | Моделирует переходы между метками, а не только эмиссии. Обеспечивает допустимые последовательности. |
| Вложенный NER | Перекрывающиеся сущности | Один отрезок — сущность, отличная от его подотрезка. BIO не может это выразить. |
| F1 на уровне сущностей | Правильная метрика NER | Предсказанный отрезок должен точно совпасть с истинным отрезком. F1 на уровне токенов завышает точность. |
Дополнительное чтение
- Lample et al. (2016). Neural Architectures for Named Entity Recognition — статья о BiLSTM-CRF. Каноническая.
- Devlin et al. (2018). BERT: Pre-training of Deep Bidirectional Transformers — вводит паттерн классификации токенов, ставший стандартом.
- Лингвистические возможности spaCy — именованные сущности — практический справочник по каждому атрибуту
Doc.entsиSpan. - seqeval — правильная библиотека метрик. Используйте её всегда.
Источник: Named Entity Recognition 05.05 — Анализ тональности · Фаза 5 — Основы и продвинутые темы NLP · Полный каталог · 05.07 — POS-разметка и синтаксический разбор