Ломаем систему: почему ваша разметка NER работает хуже, чем должна
Вы скачали Presidio, настроили analyzer, обучили модель на своих данных. Точность 85%. Неплохо, думаете вы. Пока не видите, как модель пропускает "Иванов Иван Иванович", но находит "Иванов". Пока не сталкиваетесь с тем, что LLM Guard в 30% случаев не маскирует номера телефонов в сложных формулировках. Проблема не в моделях. Проблема в том, как вы размечаете данные.
В 2026 году инструменты для защиты персональных данных стали сложнее, но фундаментальная ошибка осталась прежней: разработчики не понимают разницу между Span и BIO аннотацией. И платят за это ложными срабатываниями, пропущенными сущностями и недовольством клиентов.
Критическая ошибка: 80% команд используют BIO там, где нужен Span, и наоборот. Результат — системы защиты, которые либо слишком агрессивны (маскируют всё подряд), либо слишком пассивны (пропускают реальные угрозы).
Что такое Span и BIO, если забыть про академические определения
Давайте без воды. Span аннотация — это когда вы отмечаете целый кусок текста. "[PER: Иван Иванов] работает в компании". BIO (Begin-Inside-Outside) — это когда вы размечаете каждое слово. "Иван B-PER, Иванов I-PER, работает O, в O, компании O".
Кажется просто? Подождите. Presidio по умолчанию работает со Span. LLM Guard в некоторых реализациях требует BIO. Большинство моделей NER, обученных на публичных датасетах, используют BIO. И вот здесь начинается ад.
Почему выбор формата — это не технический, а архитектурный вопрос
Выбор между Span и BIO определяет:
- Как вы будете собирать данные (разметка вручную в 3 раза быстрее для Span)
- Как будете обучать модели (BIO требует больше вычислительных ресурсов)
- Как будете интегрировать с другими системами (Presidio ожидает Span, но многие ML-фреймворки выдают BIO)
- Как будете оценивать качество (метрики для Span и BIO рассчитываются по-разному)
Представьте, что вы построили пайплайн на BIO, а потом узнали, что SentinLLM, который вы хотите подключить для дополнительной проверки, работает только со Span. Переделывать всё с нуля? Именно так и поступают 60% команд.
Presidio analyzer: почему он ненавидит BIO
Presidio 2.0 (актуальная версия на февраль 2026) использует Span аннотацию на уровне ядра. Когда вы создаете custom recognizer или дообучаете модель, вы должны предоставить данные в формате (start, end, entity_type). Вот как выглядит правильная разметка для обучения:
# ПРАВИЛЬНО: Span аннотация для Presidio
annotations = [
{
"text": "Клиент Петров Сергей по телефону +7 (999) 123-45-67",
"entities": [
{"start": 7, "end": 21, "entity_type": "PERSON"},
{"start": 36, "end": 53, "entity_type": "PHONE_NUMBER"}
]
}
]
# НЕПРАВИЛЬНО: Попытка использовать BIO
# Presidio просто проигнорирует эту разметку
annotations_wrong = [
{
"text": "Клиент Петров Сергей по телефону +7 (999) 123-45-67",
"tokens": ["Клиент", "Петров", "Сергей", "по", "телефону", "+7", "(999)", "123-45-67"],
"labels": ["O", "B-PER", "I-PER", "O", "O", "B-PHONE", "I-PHONE", "I-PHONE"]
}
]
Почему Presidio так устроен? Потому что он изначально создавался для работы с неструктурированным текстом, где токенизация (разбиение на слова) не всегда однозначна. Разные токенизаторы могут по-разному разбить "Иван-Иванович". Span аннотация избегает этой проблемы.
LLM Guard: хитрый гибридный подход
LLM Guard 1.8 (последняя версия на 2026 год) использует более гибкую систему. Внутри он может работать и с Span, и с BIO, но для кастомных моделей рекомендует конкретный формат в зависимости от типа сущности:
| Тип сущности | Рекомендуемый формат | Причина |
|---|---|---|
| Имена, организации | BIO | Лучше справляется с составными сущностями |
| Номера (телефон, кредитка) | Span | Формат обычно фиксированный, нет неоднозначности |
| Адреса | BIO со специальными тегами | Части адреса имеют разный уровень чувствительности |
Это создает дополнительную сложность: теперь вам нужно поддерживать два формата разметки в одном проекте. Но именно такой подход дает LLM Guard преимущество в точности детекции сложных сущностей.
1 Практика: строим пайплайн разметки для гибридной системы
Допустим, вы создаете систему защиты персональных данных, которая использует и Presidio для базовой детекции, и LLM Guard для проверки сложных случаев (как в LLM-Shield). Вот как должен выглядеть пайплайн подготовки данных:
import json
from typing import List, Dict
class HybridAnnotationPipeline:
def __init__(self):
self.span_annotations = [] # Для Presidio
self.bio_annotations = [] # Для LLM Guard
def annotate_text(self, text: str, entities: List[Dict]) -> None:
"""Основной метод разметки, который создает оба формата"""
# 1. Span аннотация (для Presidio)
span_data = {
"text": text,
"entities": entities # Уже в формате [{start, end, type}]
}
self.span_annotations.append(span_data)
# 2. Конвертация в BIO (для LLM Guard)
tokens = self._tokenize_with_spans(text, entities)
bio_labels = self._spans_to_bio(tokens, entities)
bio_data = {
"text": text,
"tokens": tokens,
"labels": bio_labels
}
self.bio_annotations.append(bio_data)
def _tokenize_with_spans(self, text: str, entities: List[Dict]) -> List[str]:
"""Токенизация с учетом границ сущностей"""
# Используем whitespace tokenizer, но можно заменить на любой
# Важно: токенизатор должен быть одинаковым при обучении и инференсе
tokens = text.split()
return tokens
def _spans_to_bio(self, tokens: List[str], entities: List[Dict]) -> List[str]:
"""Конвертация Span аннотации в BIO"""
# Создаем массив меток O
labels = ["O"] * len(tokens)
# Для каждой сущности находим токены и размечаем
for entity in entities:
entity_start = entity["start"]
entity_end = entity["end"]
entity_type = entity["entity_type"]
# Находим индексы токенов, которые попадают в сущность
token_indices = []
current_pos = 0
for i, token in enumerate(tokens):
token_start = text.find(token, current_pos)
token_end = token_start + len(token)
# Проверяем пересечение токена с сущностью
if token_start < entity_end and token_end > entity_start:
token_indices.append(i)
current_pos = token_end
# Размечаем BIO
if token_indices:
labels[token_indices[0]] = f"B-{entity_type}"
for idx in token_indices[1:]:
labels[idx] = f"I-{entity_type}"
return labels
def save_annotations(self, output_dir: str):
"""Сохранение в разных форматах для разных систем"""
# Для Presidio
with open(f"{output_dir}/presidio_annotations.json", "w", encoding="utf-8") as f:
json.dump(self.span_annotations, f, ensure_ascii=False, indent=2)
# Для LLM Guard
with open(f"{output_dir}/llm_guard_annotations.json", "w", encoding="utf-8") as f:
json.dump(self.bio_annotations, f, ensure_ascii=False, indent=2)
Этот пайплайн решает главную проблему: вы размечаете данные один раз, а получаете два формата. Но есть нюанс — качество конвертации зависит от токенизатора. Если в Presidio и LLM Guard используются разные токенизаторы, возникнут расхождения.
Предупреждение: Не пытайтесь конвертировать BIO в Span автоматически для production систем. Разные токенизаторы создадут "сдвиг" в границах сущностей, что приведет к ошибкам детекции.
2 Типичные ошибки и как их избежать
За 5 лет работы с системами защиты данных я видел одни и те же ошибки снова и снова:
Ошибка 1: Смешивание форматов в одном датасете
Что делают: берут публичные датасеты в формате BIO (например, CoNLL-2003), добавляют свои данные в формате Span, смешивают и удивляются, почему модель работает плохо.
Как исправить: выберите один основной формат и конвертируйте ВСЕ данные в него. Если используете Presidio — конвертируйте BIO в Span. Если LLM Guard — наоборот. Но лучше собирать данные изначально в нужном формате.
Ошибка 2: Игнорирование вложенных сущностей
Что делают: "Москва, ул. Ленина, д. 15" размечают как LOCATION. Пропускают, что "ул. Ленина" — это тоже чувствительная информация.
Как исправить: используйте иерархическую разметку. Для адресов: COUNTRY, REGION, CITY, STREET, BUILDING. BIO формат лучше подходит для таких случаев, потому что позволяет размечать части сущности разными тегами.
Ошибка 3: Несоответствие токенизаторов
Что делают: размечают данные с помощью одного токенизатора (например, spaCy), а в production используют другой (например, BERT tokenizer из Hugging Face).
Как исправить: зафиксируйте токенизатор на этапе разметки и используйте тот же в production. Сохраняйте mapping между токенами и исходным текстом.
3 Специфика защиты персональных данных: что меняется в 2026
Современные системы защиты данных сталкиваются с проблемами, которых не было 2-3 года назад:
- Мультиязычные данные: Имена на разных языках требуют разных подходов к токенизации. "Mohammed Al-Masri" на арабском разбивается иначе, чем на английском.
- Контекстуальные сущности: Одно и то же слово может быть PII или нет в зависимости от контекста. "Apple" — это компания или фрукт? BIO хуже справляется с такими случаями.
- Частичное маскирование: Иногда нужно замаскировать только часть сущности (последние 4 цифры карты). Span аннотация позволяет это делать точнее.
- Реальные данные против синтетических: Синтетические данные, которые многие используют для обучения (особенно в курсах по защите персональных данных), часто имеют идеальную разметку. Реальные данные — грязные, неоднозначные, с ошибками.
Мой совет: если вы работаете с реальными данными клиентов, используйте гибридный подход. Span — для быстрой первоначальной разметки, BIO — для дообучения на сложных случаях. Как в системе StruQ и SecAlign, где комбинирование методов дало лучший результат.
FAQ: ответы на вопросы, которые вы постеснялись задать
Вопрос: Можно ли полностью автоматизировать разметку?
Ответ: Нет. Даже лучшие модели (включая GPT-4 Turbo 2026 года) делают ошибки в 15-20% случаев для PII. Нужна human-in-the-loop проверка хотя бы для 10% данных.
Вопрос: Какой формат лучше для имён на русском?
Ответ: BIO. Русские имена часто состоят из нескольких слов (отчество), и BIO лучше捕捉ляет границы. Но если имена идут слитно (без пробелов), Span может быть точнее.
Вопрос: Что делать, если данные приходят в разных форматах (JSON, PDF, сканы)?
Ответ: Преобразуйте всё в plain text ДО разметки. PDF-to-text конвертеры часто ломают форматирование, что влияет на позиции символов в Span аннотации.
Вопрос: Сколько данных нужно для обучения?
Ответ: Для базовых сущностей (имена, телефоны) — 1000 размеченных примеров. Для сложных (адреса, медицинские данные) — 5000+. Помните про сублиминальное обучение LLM — даже небольшой bias в данных проявится позже.
Последний совет: тестируйте на реальных edge cases
Соберите "адский датасет" из самых сложных случаев:
- Имена с дефисами (Анна-Мария)
- Номера телефонов в разных форматах (+7 999 123 45 67, 89991234567, 999-123-45-67)
- Адреса с опечатками (ул. Ленена вместо Ленина)
- Контекстуальные PII ("я живу в Париже" vs "Париж Хилтон")
- Смешанные языки ("Mr. Иванов работает в Google")
Протестируйте на этом датасете оба формата разметки. Посмотрите, где Span даёт false positive, а BIO — false negative. Только так вы поймёте, какой формат действительно лучше для вашего случая.
И помните: идеальной разметки не существует. Есть компромисс между скоростью, точностью и совместимостью с вашим стеком технологий. Presidio или LLM Guard, Span или BIO — выбор за вами. Главное — сделать его осознанно, а не потому что "так в документации написано".
P.S. Если вы дочитали до этого места и думаете "ой, а у нас в production уже запущено с неправильной разметкой" — не паникуйте. Миграция возможна. Но готовьтесь к тому, что придётся переразметить 20-30% данных и переобучить модели. Цена ошибки на этапе проектирования всегда выше, чем кажется.