Span vs BIO разметка для NER в Presidio и LLM Guard | Гайд 2026 | AiManual
AiManual Logo Ai / Manual.
01 Фев 2026 Гайд

Span vs BIO: Почему вы неправильно размечаете данные для NER в системах защиты персональных данных

Полный гайд по правильной разметке данных для NER в системах защиты PII. Span vs BIO, Presidio analyzer, LLM Guard, практические примеры на Python.

Ломаем систему: почему ваша разметка 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% команд.

💡
Правило №1: Выбирайте формат разметки ДО начала проекта. Не исходя из того, что "проще", а исходя из того, какие инструменты будете использовать в финальной системе.

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% данных и переобучить модели. Цена ошибки на этапе проектирования всегда выше, чем кажется.