- BrainTools - https://www.braintools.ru -
Дисклеймер.
Все примеры текстов и сущностей в статье являются синтетическими и не содержат реальных персональных данных. Любые совпадения с реальностью случайны.
В последние годы системы детекции и очистки персональных данных стали неотъемлемой частью NLP-пайплайнов, особенно в сценариях, где тексты передаются во внешние LLM-провайдеры и используются в LLM-агентах.
На практике такие системы решают задачу детекции и маскирования персональных данных, среди них можно выделить: Presidio [1], LLM Guard [2], NvidiaNeMo Guardrails [3] и другие.
Хотя на уровне API результат выглядит достаточно простым.
Например, когда вы используете presidio и получаете подобный ответ:
from presidio_analyzer import AnalyzerEngine
text="My phone number is 212-555-5555"
analyzer = AnalyzerEngine()
# Call analyzer to get results
results = analyzer.analyze(text=text,
entities=["PHONE_NUMBER"],
language='en')
print(results)
# type: PHONE_NUMBER, start: 19, end: 31, score: 0.75]
Или обращаетесь в какой-то сервис и получаете аналогичный ответ:
{
"text": "My phone number is 212-555-5555",
"entities": [
{
"type": "PHONE_NUMBER",
"text": "212-555-5555",
"start": 19,
"end": 31,
"score": 0.75
}
]
}
Внутри NER-пайплайна скрывается ряд архитектурных решений.
Одно из ключевых — формат и схема аннотации данных, на которых обучается и валидируется не только одна модель, но и весь пайплайн.
Разметка в NER — это не вспомогательный этап, а основа, которая напрямую влияет на: выбор подхода (regex, ML, эвристики), архитектуру модели, качество, скорость и сложность пост-обработки.
Задача NER заключается в том, чтобы найти некоторые фрагменты (spans) текста, которые считаются именованными сущностями, например:
[PER] Петр Петров [/PER] работает в [LOC] Москве [/LOC]
PER span: Петр Петров
LOC span: Москве
Спаны в NER датасетах как правило представлены следующим образом:
{
"text": "Письмо от Ивана Петрова Сергею Сидорову",
"entities": [
{ "start": 10, "end": 23, "type": "PERSON" },
{ "start": 24, "end": 39, "type": "PERSON" }
]
}
Некоторые датасеты могут содержать в себе вложенные сущности:
{
"text": "Санкт-Петербург, Невский проспект, дом 45",
"entities": [
{ "start": 0, "end": 42, "label": "ADDRESS" }, // весь адрес
{ "start": 0, "end": 15, "label": "CITY" }, // город
{ "start": 17, "end": 33, "label": "STREET" }, // улица
{ "start": 39, "end": 42, "label": "BUILDING" } // номер дома
]
}
Но далеко не все схемы кодирования могут представить такую структуру.
Данные с разметкой на уровне спанов можно найти у Nvidia:
https://huggingface.co/datasets/nvidia/Nemotron-PII [4]
Также есть небольшой набор архитектур моделей, которые работают именно со спанами, например: Span marker [5] и Gliner [6].
Span-level разметка удобна тем, что она универсальна, поскольку любая модель или эвристика в NER-пайплайне после некоторой пост-обработки будет выдавать свой ответ именно в этом формате, что дает возможность держать данные в едином формате для бенчмаркинга пайплайнов.
Однако большинство моделей работают иначе – они классифицируют токены, а спаны восстанавливаются из меток. Что ведет нас к следующей части данной статьи.
В рамках token-level схем токен является минимальной единицей, к которой применяется разметка
Исторически и в большинстве индустриальных NER пайплайнов формулируется как задача классификации токенов, из которых затем восстанавливаются спаны сущностей.
Каждый раз когда вы делаете:
from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
"distilbert/distilbert-base-uncased",
num_labels=13,
id2label=id2label,
label2id=label2id
)
Модель оптимизируется под задачу классификации каждого входного токена в один из классов.
На уровне модели не существует понятия сущности как цельного объекта — она появляется только после декодирования последовательности токенов в спаны.
Аналогично, если у вас есть некоторый набор данных в span-level разметке, вам нужно будет конвертировать их в token-level схему.
Давайте посмотрим какие они бывают.
К распространенным token-level схемам относятся:
IO, IOB, IOE, IOBES, BI, IE и BIES
Рассмотрим их более подробно.
Итак, мы решаем задачу классификации и наша задача классифицировать токены, самое элементарное что нам придет в голову – это сказать, что каждый токен это какой-то конкретный класс, если классов много – значит много или их просто нет (I-CLASS или O).
Формально:
Каждый токен в датасете принимает значение I-CLASS или O
I-inside tag – означает что токен отмечен как именованная сущность
O- outside tag – означает что у токена сущностей нет
Примеры
Вот так может выглядеть IO схема:
["Петр", "Иванов", "живет", "на", "ленина", "13"]
["I-PERSON", "I-PERSON", "O", "O", "I-ADDRESS", "I-ADDRESS"]
А еще вот так:
["Петр Иванов", "живет", "на", "ленина", "13"]
["I-PERSON", "O", "O", "I-ADDRESS", "I-ADDRESS"]
Или вот так:
["Петр Иванов", "живет", "на", "ленина 13"]
["I-PERSON", "O", "O", "I-ADDRESS"]
Все 3 варианта формально допустимы и зависят от контекста постановки задачи.
Просто помните что если модель выучит что любое число это адрес – скорее всего у вас будет высокий FP.
С IO возникает проблема, заключающаяся в том, что мы не совсем понимаем когда закончилась конкретная сущность:
["Письмо", "от", "Ивана", "Петрова", "Сергею", "Сидорову"]
["O", "O", "I-PERSON", "I-PERSON", "I-PERSON", "I-PERSON"]
Как видите [“Ивана”, “Петрова”, “Сергею”, “Сидорову”] – все это 4 I-сущности без начала и конца. Если вам надо понимать кого из них выделять и не терять контекст при этом, у вас возникнут сложности.
Например вы решили замаскировать их:
["PERSON_1", "PERSON_2", "PERSON_3", "PERSON_4"]
Получается так что 2 человека внезапно превратились в 4 разных, что может быть критичным в определенных юзкейсах, где важно не просто замаскировать, а сохранить контекст.
Технически пример выше можно разметить еще и вот так:
["Письмо", "от", "Ивана Петрова Сергею Сидорову"]
["O", "O", "I-PERSON"]
Поздравляю, теперь у вас не 2 а одна PERSON сущность.
С точки зрения [7] семантики это выглядит полным абсурдом, ведь письмо от Ивана Сергею, это два разных человека.
С другой стороны если ваша задача просто маскировать сущности и не важно их явно разделять, проблем такая разметка у вас скорее всего не вызовет, сохраняя при этом адекватный баланс классов.
В качестве решения проблемы этой была представлена BIO схема.
Как мы уже поняли нам нужен некоторый разделитель, который
скажет нам когда сущность начинается (или заканчивается но это уже IOE)
Формально
I- inside
O- outside
B- begin тег – означающий начало сущности
Примеры
Тогда данные можно разметить так:
["Письмо", "от", "Ивана", "Петрова", "Сергею", "Сидорову"]
["O", "O", "B-PERSON", "I-PERSON", "B-PERSON", "I-PERSON"]
Данный вид разметки также известен как CONLL формат.
Тут начинаются потенциальные проблемы с дисбалансом классов, поскольку “O” будет доминирующим тегом, а частота B/I зависит от средней длины сущностей
Также возникает проблема вложенных сушностей, а точнее невозможность их как то разметить:
ADDRESS: "Санкт-Петербург"_CITY, "Невский проспект"_STREET, "дом 45"_HOUSE_NUMBER, "квартира 23"_APARTMENT
Данная проблема характерна для всех видов token-level аннотаций.
Попробуйте адаптировать эту сущность к BIO схеме, а к IO?
Вот примеры для BIO:
["Санкт-Петербург", ",", "Невский", "проспект", ",", "дом", "45", ",", "корпус", "2", ",", "квартира", "23"]
["B-ADDRESS", "O", "B-ADDRESS", "I-ADDRESS", "O",
"B-ADDRESS", "I-ADDRESS", "O", "B-ADDRESS", "I-ADDRESS", "O", "B-ADDRESS", "I-ADDRESS"]
тут мы теряем все вложенные сущности (CITY, STREET, HOUSE_NUMBER, APARTMENT)
или:
["Санкт-Петербург", ",", "Невский", "проспект", ",", "дом", "45", ",", "корпус", "2", ",", "квартира", "23"]
["B-CITY", "O", "B-STREET", "I-STREET","O",
"B-NUM", "I-NUM", "O", "B-NUM", "I-NUM", "O", "B-APART" "I-APART",]
Тут мы теряем сущность которая придает какой-то смысл происходящему здесь – ADDRESS.
Аналогичен IOB, просто вместо Begin-тега – у нас идет End-тег.
Формально
I- inside
O- outside
E- end tag – означающий конец сущности
Пример
["Письмо", "от", "Ивана", "Петрова", "Сергею", "Сидорову"]
["O", "O", "I-PERSON", "E-PERSON", "I-PERSON", "E-PERSON"]
Не судите по названию что тут всего лишь 2 вида тэгов, хотя тэгов действительно 2, но они применяются теперь не только к токенам сущностей (entities) но к “не сущностям” (non-entities), то есть токенам которые не являются сущностями.
Формально
B – begin of entity/non-entity
I – inside entity/non-entity
Пример
["Письмо", "от", "Ивана", "Петрова", "Сергею", "Сидорову"]
["B-O", "I-O", "B-PERSON", "I-PERSON", "B-PERSON", "I-PERSON"]
Как следствие классов у нас уже не 2 как в IO, а 4, что повышает потенциальный дисбаланс классов.
Помните IOB и IOE?
Так вот кейс такой же, просто “переворачиваем” и берем что? Правильно End tag.
Формально
I – inside entity/non-entity
E – end of entity/non-entity
Пример
["Письмо", "от", "Ивана", "Петрова", "Сергею", "Сидорову"]
["I-O", "E-O", "I-PERSON", "E-PERSON", "I-PERSON", "E-PERSON"]
Продолжает идею IOB и IOE, сделав merge и добавив в них S-тег
Ее также называют BILOU и она является достаточно популярной, наравне с BIO.
Формально
I- inside – где-то внутри сущности (между началом и концом)
O- outside
B- begin – начало сущности
E- end – конец сущности
S- single – одиночная сущность
Пример
["Петр", "Сергеевич", "Иванов", "живет", "в", "Москве", "на", "Тверской", "15"]
["B-PERSON", "I-PERSON", "E-PERSON", "O", "O", "S-GPE", "O","B-ADDRESS", "E-ADDRESS"]
BIES берет идею IOBES(BILOU), но еще делаем concat с BI и IE
Звучит как апофеоз всего зоопарка, поскольку проблема вложенных сущностей все еще не решена, а классов становится все больше и больше…
Формально
I- inside entity/non-entity
O- outside of entity/non-entity
B- begin – начало сущности
E- end – конец сущности
S- single – одиночная сущность
Пример
["Петр", "Сергеевич", "Иванов", "живет", "в", "Москве", "на", "Тверской", "15"]
["B-PERSON", "I-PERSON", "E-PERSON", "B-O", "E-O", "S-GPE", "S-O","B-ADDRESS", "E-ADDRESS"]
Каждая схема позволяет решить определенный класс задач с некоторыми трейд-оффами. Тем не менее, если вы не совсем понимаете какая именно разметка вам нужна, просто берите span-level, а из него конвертируйте в BIO, BILOU или что-то менее популярное по необходимости.
В этой статье мы рассмотрели два уровня разметки, которые решают разные задачи.
Span-level разметка является универсальным форматом представления сущностей.
Не все датасеты и бенчмарки используют её напрямую, однако именно к этому формату в итоге приводятся результаты работы NER-пайплайнов. Span-level разметка удобна для сравнения разных подходов, анализа ошибок и оценки качества на уровне целых сущностей. Также она дает возможность решать проблему вложенных сущностей, однако для этого требуется отдельный класс моделей.
Token-level разметка используется в первую очередь для обучения [8] и тюнинга моделей.
Большинство архитектур работают с последовательностями токенов, поэтому при обучении span-level аннотации (если они есть) преобразуются в token-level схемы. Разные схемы (IO, BIO, IOBES и другие) отличаются способом кодирования границ сущностей.
Главное ограничение token-level подхода заключается в том, что он не поддерживает вложенные сущности и не оперирует спанами как цельными объектами.
На практике это приводит к следующему пайплайну: данные хранятся в span-level формате, затем конвертируются в token-level схемы для обучения модели, а предсказания модели декодируются обратно в спаны на этапе инференса.
Автор: Bogdan_m01
Источник [9]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/25017
URLs in this post:
[1] Presidio: https://microsoft.github.io/presidio/
[2] LLM Guard: https://protectai.github.io/llm-guard/
[3] NvidiaNeMo Guardrails: https://github.com/NVIDIA-NeMo/Guardrails/tree/develop
[4] https://huggingface.co/datasets/nvidia/Nemotron-PII: https://huggingface.co/datasets/nvidia/Nemotron-PII
[5] Span marker: https://github.com/tomaarsen/SpanMarkerNER
[6] Gliner: https://github.com/urchade/GLiNER
[7] зрения: http://www.braintools.ru/article/6238
[8] обучения: http://www.braintools.ru/article/5125
[9] Источник: https://habr.com/ru/companies/raft/articles/991404/?utm_campaign=991404&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.