Урок 49. Извлечение сущностей (NER)
Зачем тебе этот урок
Уроки 43–48 учили представлять текст числами и строить модели поверх этих чисел. Заказчику нужно другое: в архиве сорок тысяч договоров, и надо знать — кто с кем, на какую сумму и до какого числа. Не «вектор документа», а таблица с колонками.
Задача называется NER — Named Entity Recognition. Аналогия у тебя уже есть: Урок 41 про детекцию. Там модель отвечала «что и где» на картинке — рамка плюс класс. Здесь она отвечает «что и где» в тексте — границы фрагмента плюс тип. Метрики почти те же, и болит то же место — границы.
1. Что за задача
На вход обычный текст, на выход список фрагментов, у каждого начало, конец и тип:
«5 марта ООО «Северный ветер» перечислило Ивану Петрову 120 000 рублей»
DATE [0:7] 5 марта
ORG [8:28] ООО «Северный ветер»
PER [41:54] Ивану Петрову
MONEY [55:69] 120 000 рублей
Классический набор типов — люди (PER), организации (ORG), места (LOC), даты и суммы. Но деньги проекту приносят обычно свои типы: номер договора, артикул, диагноз, марка стали. Универсальная модель их не знает — это твоя предметная область.
| Задача | Что на выходе | Аналог в зрении |
|---|---|---|
| Классификация текста | Один ярлык на весь документ | Классификация картинки (37) |
| NER | Список фрагментов: границы + тип | Детекция рамками (41) |
| Разметка всех токенов | Тег на каждое слово, включая служебные | Сегментация по пикселям (42) |
2. Разметка BIO
Первая мысль — помечать каждое слово классом. Ломается на самом частом случае, двух сущностях подряд:
Работали с ООО Вектор ООО Гамма
ORG ORG ORG ORG ← одна организация или две?
Поэтому в тег добавляют признак начала. Схема BIO: B-ТИП — первый токен сущности, I-ТИП — продолжение, O — не сущность. Новая B- значит «предыдущая закончилась, началась следующая». Предложение потокенно:
| Токен | Тег | Токен | Тег |
|---|---|---|---|
| 5 | B-DATE | перечислило | O |
| марта | I-DATE | Ивану | B-PER |
| ООО | B-ORG | Петрову | I-PER |
| « | I-ORG | 120 | B-MONEY |
| Северный | I-ORG | 000 | I-MONEY |
| ветер | I-ORG | рублей | I-MONEY |
| » | I-ORG | . | O |
Обрати внимание на кавычки и на «ООО»: включать их в границы или нет — соглашение, а не истина. Решаешь ты, записываешь в инструкцию разметчику и держишься везде.
Есть расширения BIOES и BILOU, где отдельными тегами помечают последний токен и односложную сущность. Смысл тот же, тегов больше, выигрыш небольшой. Начинай с BIO.
3. Три поколения подходов
Правила и словари. Регулярки на всё с жёстким форматом: телефон, ИНН, номер договора, артикул, дата. Плюс газеттиры — готовые списки городов, стран, юрлиц. Подход не устарел: на форматных сущностях регулярка до сих пор лучше любой сети — точна на сто процентов, объяснима, чинится за минуту и не требует ни одного размеченного примера.
Классический ML по признакам. Для токена собирают признаки (заглавная ли буква, суффикс, часть речи, есть ли слово в газеттире, какие соседи) и учат модель предсказывать тег. Чаще всего CRF (conditional random field, условное случайное поле): он выбирает не тег каждого токена по отдельности, а всю последовательность целиком, поэтому не выдаёт бессмыслицу вроде I-ORG сразу после O. Разметки требует немного.
Нейросетевой. Эмбеддинги (44) → LSTM (46) или трансформер (48) → тег на каждый токен, признаки модель придумывает сама. Сверху часто всё равно вешают CRF-слой — по той же причине, ради связности тегов.
| Когда | Чем брать |
|---|---|
| Формат жёсткий: номер, дата, ИНН, артикул | Регулярка. Всегда. Не учи этому сеть |
| Закрытый список: города, юрлица из справочника | Газеттир плюс нечёткое сопоставление |
| Смысл зависит от контекста: люди, организации, роли | Нейросеть |
В живом проекте это не «или-или», а слоёный пирог: сеть на смысловые типы, регулярки на форматные, словарь на справочные. Так почти всегда точнее чистой сети.
4. Классификация токенов на трансформере
Устройство простое до неприличия. Энкодер (48) выдаёт вектор на каждый токен, сверху вешается один линейный слой, переводящий вектор в вероятности по всем тегам: для пяти типов это 2 × 5 + 1 = 11 классов. Задача так и называется — token classification.
Почему encoder-only, а не декодер: энкодер видит контекст с обеих сторон, и для NER это принципиально.
«Вашингтон подписал указ» → PER (подсказка справа)
«Вашингтон принял делегацию» → LOC или ORG (подсказка справа)
«Джордж Вашингтон родился...» → PER (подсказка слева)
Слева от слова информации часто не хватает. Декодер, который по устройству видит только левый контекст, работает вслепую на половине случаев. Отсюда правило: генерация — декодер, извлечение и классификация — энкодер.
5. Особенности русского текста
Падежи. «Петров», «Петрова», «Петрову», «Петровым» — одна сущность в четырёх формах. Модель это осилит, она видит достаточно словоформ. Проблема вылезает дальше, на нормализации: в базу надо класть одну запись, а не четыре — значит нужна лемматизация.
Подсловная токенизация. Трансформер режет редкие слова на куски (48): «Череповце» становится чем-то вроде Че + репов + це. Тег предсказывается на каждый кусок, и сущность надо собирать обратно. Стандартный приём — брать тег первого подслова, склеивать соседние куски, а границы возвращать не в номерах токенов, а в смещениях по символам исходной строки. Вывод: работай со start и end в символах, иначе однажды отрежешь половину фамилии.
Заглавные помогают меньше, чем кажется. В английском они почти маркер сущности, в русских реальных текстах — нет: в деловой переписке пишут «ооо ромашка», в чатах поддержки всё строчными, а заголовок бывает НАБРАН КАПСОМ ЦЕЛИКОМ. Модель, выучившая «с заглавной = сущность», развалится на первых настоящих данных. Проверяй качество отдельно на строчном тексте.
6. Метрики: считаем сущности, а не токены
Самая частая методическая ошибка — мерить долю верно размеченных токенов. В обычном тексте около 90 % токенов имеют тег O, и модель, не нашедшая вообще ничего, покажет 90 % «точности». На границах та же беда: в «Открытое акционерное общество Вектор» модель угадала три токена из четырёх — по токенам 75 %, а по сути сущность извлечена неверно, в базу уйдёт мусор.
Единица счёта — сущность целиком:
TP — предсказанная сущность совпала с размеченной
FP — нашли то, чего нет, или ошиблись в границах либо типе
FN — размеченную сущность пропустили
Precision = TP / (TP + FP) какая доля находок настоящая
Recall = TP / (TP + FN) какую долю настоящих мы нашли
F1 = 2PR / (P + R) сводная
Режима сравнения два. Строгое совпадение: границы символ в символ и тип верный, иначе не засчитано. Мягкое: тип верный и отрезки пересекаются — прямой аналог порога IoU из Урока 41, та же идея «насколько близко попали», только в одном измерении вместо двух.
По умолчанию берут строгое, мягкое полезно для диагностики: большой разрыв между ними означает, что модель находит сущности правильно, но промахивается по границам — а это чинится инструкцией и разметкой, а не сменой архитектуры. И смотри F1 по каждому типу отдельно: общее среднее прячет тип, который не работает совсем.
7. Разметка данных
Сколько нужно. Универсального числа нет, считай не документы, а вхождения сущностей каждого типа. Ориентир для дообучения готовой модели — от нескольких сотен вхождений на тип. Разнообразие важнее объёма: сто договоров от разных контрагентов полезнее тысячи копий одного шаблона.
Инструменты: Label Studio, doccano, brat, Prodigy. Все умеют выделять фрагмент мышкой и экспортировать спаны со смещениями.
Согласованность разметчиков. Дай двум людям одну сотню документов и сравни результат. Согласны между собой на 70 % — никакая модель выше не прыгнет: потолок ставит разметка, а не архитектура. Споры, которые всплывут в первый же день:
- «Москва подписала соглашение» — LOC или ORG? Формально город, по смыслу орган власти.
- Входят ли «ООО» и кавычки в границы организации?
- «в прошлый вторник» — это DATE или размечаем только явные даты?
- Должность вместе с именем или отдельно: «директор Иванов»?
- Вложенность: в «Университет города Череповец» внутри ORG сидит LOC.
Правило простое: каждый спор превращается в строку инструкции с примером. Инструкция пишется до старта разметки и растёт по ходу.
8. Готовые решения и дообучение
Для базовых типов на русском изобретать ничего не надо: есть spaCy с русскими пайплайнами, есть Natasha — библиотека, заточенная под русский язык и его морфологию, есть предобученные энкодеры с NER-головой в открытых каталогах моделей. Для «найди людей, организации и места» их обычно хватает без единого размеченного примера.
Свои типы — дообучение: берёшь предобученный энкодер, ставишь новую голову token classification под свой набор тегов и учишь на своей разметке. Логика ровно как в Уроке 38 про transfer learning: язык модель знает, ей остаётся выучить твою специфику.
9. LLM для извлечения — честное сравнение
LLM извлекает сущности по описанию, без обучения: перечисляешь типы словами, просишь вернуть структуру, получаешь результат за час вместо недель разметки. Новый тип — строка в промпте. На старте преимущество огромное.
Дальше расплата. На потоке в миллион документов LLM дороже специализированной модели на порядки и в разы медленнее. Ответ недетерминированный: тот же текст завтра разметится иначе. И главное — LLM генерирует текст, а не размечает существующий: она спокойно вернёт «Иванов» там, где в документе «Иванову», точных смещений от неё не добьёшься, а изредка она выдумает сущность, которой в тексте не было.
| Правила и словари | Своя модель | LLM | |
|---|---|---|---|
| Нужна разметка | Нет | Сотни примеров на тип | Нет |
| Время до результата | Часы | Недели | Часы |
| Цена на большом потоке | Копейки | Низкая | Высокая |
| Скорость | Очень высокая | Высокая | Низкая |
| Точность границ | Абсолютная, где формат жёсткий | Высокая | Плавает |
| Новый тип | Новая регулярка | Разметка плюс дообучение | Строка в промпте |
| Предсказуемость | Полная | Высокая | Средняя |
Как выбирать. Формат жёсткий — регулярка, без вариантов. Поток большой, типы устоялись — своя модель. Типов много, меняются каждую неделю, объём небольшой, а извлекать надо смысл («причина отказа», «озвученное возражение») — LLM.
Рабочий гибрид: LLM размечает первые пару тысяч документов, человек правит её разметку (править быстрее, чем размечать с нуля — та же логика, что с предразметкой кадров в Уроке 41), на ней учится своя модель, и в проде стоит уже она.
10. Что делать с результатом
Извлечение — половина работы, сырые спаны в базу класть нельзя.
Нормализация. Даты в единый формат, суммы в число плюс валюту, имена в начальную форму, номера очистить от пробелов. Иначе «5 марта», «05.03» и «пятого марта» останутся тремя разными значениями.
Связывание (entity linking). «Северный ветер», «ООО «Северный ветер»» и «СВ» — одна компания. Надёжно связывают по идентификатору: извлечён рядом ИНН — сопоставляй по нему, а не по названию. Без идентификатора остаётся нечёткое сопоставление строк с порогом плюс ручная проверка спорных. Соседняя задача — кореференция: «компания», «он», «указанный контрагент» тоже указывают на сущность, но имени не содержат.
Куда складывать. Структура плоская — обычная таблица в базе. Важны связи между сущностями (кто кому поставщик, кто чей учредитель) — граф знаний из Урока 32. NER как раз и есть механизм, который наполняет граф автоматически: раньше тройки «субъект — отношение — объект» вбивали руками, теперь их добывают из текста.
11. Код: извлечение и метрики
PER, где-то PERSON, место бывает LOC или GPE. Это разные словари, приводи к одному до сравнения с разметкой.
from transformers import pipeline
import re
MODEL = "russian-ner-model" # подставь актуальное имя из каталога моделей
ner = pipeline("token-classification", model=MODEL,
aggregation_strategy="simple") # склеивает подслова обратно
TEXT = ("5 марта ООО «Северный ветер» перечислило Ивану Петрову "
"120 000 рублей по договору №14/2025, подписанному в Череповце.")
for e in ner(TEXT):
frag = TEXT[e["start"]:e["end"]] # режем ТОЛЬКО по смещениям в символах
print(f"{e['entity_group']:6s} {e['score']:.2f} [{e['start']}:{e['end']}] {frag}")
# Номер договора сети не отдаём — формат жёсткий, регулярка точнее и дешевле
DOC = re.compile(r"№\s?\d+/\d{4}")
for m in DOC.finditer(TEXT):
print(f"DOC 1.00 [{m.start()}:{m.end()}] {m.group()}")
# ---- метрики по сущностям, а не по токенам ----
GOLD = [("5 марта", "DATE"), ("ООО «Северный ветер»", "ORG"),
("Ивану Петрову", "PER"), ("120 000 рублей", "MONEY"),
("Череповце", "LOC")]
def spans(text, pairs):
"""Эталон задаём подстроками — смещения считаем, а не вбиваем руками."""
out = set()
for frag, tag in pairs:
i = text.find(frag)
assert i != -1, f"не найдено в тексте: {frag}"
out.add((i, i + len(frag), tag))
return out
def prf(pred, gold):
tp = len(pred & gold)
p = tp / len(pred) if pred else 0.0
r = tp / len(gold) if gold else 0.0
f = 2 * p * r / (p + r) if p + r else 0.0
return p, r, f
gold = spans(TEXT, GOLD)
pred = {(e["start"], e["end"], e["entity_group"]) for e in ner(TEXT)}
p, r, f = prf(pred, gold)
print(f"строго: P={p:.2f} R={r:.2f} F1={f:.2f}")
print("пропустили:", gold - pred)
print("лишнее: ", pred - gold)
# Мягкое совпадение: тип верный и отрезки пересекаются хотя бы одним символом
soft = sum(1 for ps, pe, pt in pred
if any(pt == gt and ps < ge and gs < pe for gs, ge, gt in gold))
print(f"мягких попаданий: {soft} из {len(gold)}")
# Большой разрыв между строгим и мягким = проблема в границах, а не в модели
12. Тонкости и подводные камни
- Границы важнее, чем кажется. «120 000 рублей» и «120 000» — на глаз мелочь, в базе разные вещи: во втором валюта потеряна. Половина ошибок NER в проде — ошибки границ, а не типов.
- Невидимые символы. Неразрывный пробел в сумме, мягкий перенос, «ё» вместо «е», двойные пробелы из PDF — смещения поедут, регулярки промахнутся. Нормализуй текст до извлечения и храни ровно тот же нормализованный текст, иначе спаны перестанут совпадать.
- Вложенные сущности. Плоская схема BIO не выражает сущность внутри сущности. Либо берёшь самый длинный спан, либо делаешь отдельный проход.
- Текст после распознавания. Сканы дают опечатки и склеенные слова. Меряй качество на реальном выходе OCR, а не на чистом наборе — иначе отчёт и прод разойдутся вдвое.
- Утечка при делении данных. Документы одного клиента идут по одному шаблону: разложишь их случайно между обучением и тестом — метрика взлетит на пустом месте, как соседние кадры видео в Уроке 41. Дели по клиентам, источникам, периодам.
- Персональные данные. NER вытаскивает ФИО, телефоны и адреса — он же и обезличивает. Помни: неполное обезличивание опаснее никакого, потому что создаёт ложное чувство безопасности.
13. Глоссарий
NER
Named Entity Recognition — поиск в тексте упоминаний сущностей и определение их типа.
Спан
Фрагмент текста, заданный началом и концом в символах, плюс тип. Единица результата NER.
BIO-разметка
Схема тегов: B — начало сущности, I — продолжение, O — не сущность. Позволяет различить две сущности подряд.
Token classification
Постановка задачи: модель предсказывает тег для каждого токена. Так NER решают на энкодере.
Газеттир
Словарь-список известных сущностей: города, страны, юрлица. Простейший и очень надёжный источник разметки.
Entity linking
Связывание найденного упоминания с конкретной записью справочника. Три написания компании — одна строка в базе.
14. Практика (40 минут)
- Возьми свой реальный русский текст на пару абзацев — договор, письмо, новость, переписку с поддержкой. Прогони готовой моделью из раздела 11 и распечатай спаны со смещениями и уверенностью.
- Размети те же сущности руками в формате
GOLDиз кода и посчитай строгий и мягкий F1. Большая разница между ними — проблема в границах. - Разбери каждую ошибку и подпиши причину: пропуск, лишнее, не тот тип, съехала граница. Четыре причины лечатся по-разному.
- Добавь регуляркой свой тип, которого в модели нет — номер договора, артикул, телефон. Засеки, сколько это заняло и сколько ошибок дало.
- Прогони тот же текст в нижнем регистре и сравни F1 с исходным: увидишь, насколько модель опирается на заглавные буквы.
- Попроси LLM извлечь те же типы по текстовому описанию и посчитай F1 её ответа тем же кодом. Отдельно проверь, все ли выданные строки реально встречаются в тексте.
- Запиши в
progress.md: строгий и мягкий F1 у модели и у LLM, какая из четырёх причин ошибок оказалась главной и что дал прогон на нижнем регистре.
15. Проверь себя
1. Зачем в разметке нужен префикс B-, разве не хватило бы типа?
Без него две сущности одного типа подряд сливаются в одну: непонятно, где кончается первая организация и начинается вторая.
2. Почему для NER берут encoder-only, а не декодер?
Тип упоминания часто определяется словами справа: «Вашингтон подписал» и «Вашингтон принял делегацию» — разные типы. Энкодер видит контекст с обеих сторон, декодер только левый.
3. Модель размечает 96 % токенов верно. Хорошо ли это?
Ничего не говорит: около 90 % токенов имеют тег O, их легко угадать. Считать надо precision, recall и F1 по сущностям целиком и по каждому типу отдельно.
4. Нужно извлекать номера договоров из потока писем. Что взять?
Регулярное выражение. Формат жёсткий, значит правило точнее сети, дешевле, быстрее и не требует ни одного размеченного примера.
5. LLM извлекла «Иванов», хотя в тексте написано «Иванову». Почему это проблема?
Она не разметила текст, а сгенерировала ответ. Точных границ нет, сверить спан с исходником нельзя, а рядом с переписанной формой легко появится и выдуманная сущность.
16. Что должно остаться в голове
- NER — «что и где» в тексте: границы фрагмента плюс тип. Прямой аналог детекции рамками из Урока 41.
- Ценность проекта обычно не в людях и городах, а в своих типах: номер договора, артикул, диагноз.
- Схема BIO нужна, чтобы различать две соседние сущности одного типа. Границы — предмет соглашения, оно пишется в инструкцию.
- Три поколения подходов живут одновременно: в проде работает слоёный пирог — сеть на смысл, регулярка на формат, словарь на справочник.
- На трансформере это token classification поверх энкодера: двусторонний контекст здесь не роскошь, а необходимость.
- Русский добавляет падежи, подсловное дробление и ненадёжные заглавные. Границы всегда веди в символах, а не в токенах.
- Метрики считаются по сущностям, а не по токенам. Разрыв между строгим и мягким F1 показывает, что болят именно границы.
- Потолок качества ставит согласованность разметчиков, а не архитектура модели.
- LLM хороша на старте и дорога на потоке, границы у неё плавают — валидируй схемой и проверяй вхождение строки в текст.
- Спаны — не конечный результат: дальше нормализация, связывание со справочником и укладка в базу или граф знаний (32).