Привет! Я Андрей Иванов, NLP-исследователь в R&D-лаборатории red_mad_robot.
Персональные данные попадают в языковые модели постоянно. Отдельные пользователи и компании отправляют модели рабочие тексты, и вместе с ними уходят имена, телефоны, номера документов и другие чувствительные данные.
Чтобы избежать рисков утечек, компании разрабатывают целые системы маскировки персональных данных, которые передают модели уже обезличенные тексты. Но у такого процесса тоже есть свои минусы: защита данных ломает рабочие сценарии, ведь модель возвращает точно такие же обезличенные тексты.
Поэтому в последнее время запрос к системам маскировки изменился. Теперь они должны уметь не только скрывать данные, но и подставлять их обратно в ответы модели, которая работала с обезличенным текстом. В этой статье я расскажу, как мы строили такую систему и какими путями шли к ней — результаты работы лежат в репозитории. В конце покажу метрики на датасетах и сравню с другими открытыми решениями.
Полный цикл обработки запроса: замена ПД плейсхолдерами перед отправкой в модель и их обратное восстановление в финальном ответе.
Правила и модель разбирают текст независимо друг от друга, их находки сходятся на слиянии, а спорные места разбирает арбитр.
Первый поток — правила. Текст нормализуется без сдвига границ, дальше ищутся кандидаты: сначала паттерны с буквами в серии, вроде свидетельства о рождении и военного билета, потом общие числовые последовательности. Для каждого кандидата готовится узкий и широкий контекст, классификаторы разбирают его по приоритету и применяют контрольные суммы и ключевые слова. В конце отбрасываются дубликаты с одинаковыми цифрами.
Второй поток — модель. Она читает тот же текст целиком и отдаёт свои находки: имена, адреса, телефоны, а заодно документы, не разобранные правилами.
Потоки друг о друге не знают и встречаются в модуле разрешения конфликтов. Здесь три варианта развития событий:
Одна фраза в трёх состояниях. Звёздочки прячут данные вместе со смыслом, и модели работать не с чем. Метка сохраняет структуру фразы, поэтому модель отвечает как на обычный текст, а настоящие значения потом возвращаются на место.
Поэтому мы делаем псевдоанонимизацию. Настоящие значения уходят из текста, а на их место встают псевдонимы, с которыми модель работает как с данными. Фраза «У Татьяны Владимировны Козловой нет времени» превращается в «У
Hivetrace, в отличие от alexen2, alrosait и нашего датасета, собран двумя способами сразу и дальше идёт двумя сплитами — entity и domain. Entity проверяет покрытие типов, domain — поведение на бытовых текстах, где данных может не быть вовсе. Считаем мы их порознь: усреднять нечего, в entity типы весят поровну по построению, в domain — как выпало по сценариям.
Спан считается найденным по одному из двух правил:
micro-F1, %. Точность и полнота по строгому правилу
Типы данных, которых мы не поддерживаем, из подсчётов убраны: на hivetrace это ОГРН, КПП, CVC и токены, вместе они дают треть эталона. Оставь их — и метрика покажет 69,7 вместо 93,3, но измерит она список функций, а не качество детекции. Ровно поэтому дальше, где систем становится четыре, область считается по пересечению.
Строка alexen2 отдельно. Строгая метрика там низкая у всех участников, потому что разметка захватывает точку в конце предложения, а подравнивать границы мы себе запретили. Настоящий результат на этом наборе — мягкие 98,1.
Область считаем по пересечению: в зачёт идут только те типы, которые размечены в датасете и которые умеют находить все сравниваемые системы. Всё остальное убирается со всех сторон сразу — и из эталона, и из предсказаний. Иначе метрика измеряет наличие функции, а не качество детекции.
Пересечение четырёх систем — шесть типов: имя, адрес, почта, телефон, паспорт, банковская карта.
Строгое совпадение, micro-F1, %.
Совпадение по типу, micro-F1, %.
Мы впереди везде. Разрыв тем шире, чем дальше набор от простых имён с телефонами: на alexen2 он почти исчезает, на нашем бенчмарке доходит до шести раз.
Отдельная оговорка про GLiNER: мы измеряем голую модель, тогда как в их продуктовом контуре рядом стоят такие же регулярки с валидаторами, что и у нас. Этот контур не опубликован, поэтому в прогон он войти не может.
Шесть типов — это цена того, что в таблице четыре системы: чем больше участников, тем у́же пересечение. С cloud.ru вдвоём область шире на три типа — ИНН, СНИЛС и IP-адрес, — и на них картина видна полнее.
В двух таблицах ниже - те же пять наборов и те же две метрики, но посчитанные на девяти типах вместо шести.
На трёх добавленных типах отрыв только растёт.
Откуда берётся такой отрыв, видно напрямую. Cloud.ru Guardrails filter построен на регулярках, и если отключить у нас модель, оставив голую ветку правил, на нашем бенчмарке две системы сходятся почти в одну точку: 49,0 против 48,9. То есть правила против правил идут вровень, а весь остальной отрыв приносит модель.
Мягкое правило щедро ко всем одинаково: пересечение в один символ засчитывается как попадание. Довести эту логику до предела — и система, которая замажет текст целиком, получит по ней сто процентов. Поэтому читать её как абсолютную оценку защиты нельзя.
Заодно чем правило грубее, тем ближе друг к другу выглядят системы с очень разным качеством границ. На нашем бенчмарке против GLiNER uni строгое даёт 87,1 против 50,6, а совсем грубое правило, засчитывающее любое перекрытие без разбора границ, — 93,5 против 65,2. Разрыв сжимается вдвое на тех же самых предсказаниях.
Само правило сопоставления мы тоже проверили: посчитали все системы тремя разными способами, от строгого до самого снисходительного. Порядок систем во всех трёх один и тот же — меняется только видимый разрыв между ними.
Наборы тоже спорят между собой о границах. Паспорт в hivetrace размечен одним спаном вместе со словами — «серия 7518, номер 492137», — а в нашем бенчмарке двумя, только данные, потому что слово «серия» персональными данными не является. Совпасть с обоими соглашениями нельзя, и на hivetrace строгая метрика прижимает всех, кто отдаёт узкие спаны: на паспортах там 77,5 у cloud.ru, 75,7 у нас, 62,9 у GLiNER. На сравнение это не влияет, а вот читать 90,9 как абсолютную оценку не стоит.
Отдельно стоит сравнить два типа, где решает именно модель. Остальные одиннадцать типов у всех закрыты правилами, и среднее по ним говорит о качестве регулярок, а не систем.
Строгая метрика, hivetrace, режим со склейкой спанов. Цифры GLiNER Guard — опубликованные, наши и cloud.ru — наш прогон.
Про склейку надо сказать прямо. Мы размечаем имя и фамилию отдельными спанами, а эталон даёт один спан на человека. С адресом та же история: у нас город, улица и дом порознь, у эталона всё одним куском. Без склейки строгая метрика на этих двух типах равна нулю. Ни одна наша находка не совпадёт с эталоном, хотя вместе они покрывают его полностью. Это документированный режим самого датасета, и здесь важно показать обе цифры: 0,0 без склейки против 96,6 и 86,5 с ней.

Начали с регулярок, но быстро утонули
Первая версия детектора состояла из регулярных выражений — это естественный выбор. Большая часть персональных данных имеет формат: у телефона, ИНН или номера карты своя длина и своя структура. Набор шаблонов собирается быстро, и найти заметную часть данных можно сразу. Сложности начались на настоящих текстах. Номера пишут с пробелами, дефисами и скобками, с ошибками и лишними символами. Шаблоны приходилось расширять, их число росло, как и сложность. Они начали конфликтовать друг с другом, и качество пошло вниз. Но главной проблемой оказалось другое: формат шаблона не определяет тип данных. Строка из 16 цифр — это номер карты или id интернет-заказа. Десять цифр подходят и под ИНН, и под номер паспорта. Регулярка находит шаблон, но не может сказать, что именно нашла. Ошибка стоит дорого в обе стороны: пропустили карту — отдали её наружу, замаскировали безопасную последовательность цифр — испортили UX. Одного шаблона мало, нужен способ подтвердить, что нашли именно документ. И такой способ нашёлся.Смотрим на Луну и другие контрольные суммы
Номер банковской карты умеет проверять сам себя. Последняя цифра в нём считается из всех предыдущих по алгоритму Луна, и стоит одной цифре измениться, как сумма перестаёт сходиться. Для нас это простой фильтр: случайная последовательность из 16 цифр проходит проверку примерно в одном случае из десяти, остальные отсеиваются сразу. Такое контрольное число есть не только у карт. У ИНН и СНИЛС свои формулы взвешенной суммы, у полиса ОМС — снова Луна. Там, где контрольного числа нет, проверяем структуру: у БИК это первые цифры и диапазон номера банка, у почтового индекса — существующие серии префиксов. Ложных срабатываний на числах стало заметно меньше. И у каждой находки появилось объяснение, какую именно проверку она прошла. Но алгоритмы и формулы не знают, о чём текст. Результат вычисления уравнения случайно сходится по контрольной сумме, а десять цифр из накладной формально проходят проверку ИНН. Чтобы отличить документ от совпадения, приходится смотреть на контекст.Смотрим по сторонам
Логика тут простая. Рядом с документом почти всегда стоит слово, которое его называет: «ИНН», «паспорт», «полис», «счёт». Значит, достаточно вырезать окно вокруг кандидата и поискать в нём такие слова. На практике этот подход быстро начинает ошибаться. Возьмём фразу «Паспорт мы выслали ещё вчера, трек-номер 1234567890». В окне есть слово «паспорт», десять цифр попадут в паспортные данные, хотя это номер отправления. Само наличие слова в окне только добавляет ложных срабатываний, и чем шире окно, тем больше в нём случайных подсказок. Поэтому мы смотрим на близость и направление. Ключевое слово должно стоять рядом с кандидатом и не дальше своего предела, у каждого типа он свой — у БИК это всего 40 символов. Побеждает ближайшее слово, и эта конкуренция между типами разбирает большую часть спорных случаев сама. Запрещающие слова живут по тому же правилу и соревнуются с подсказками за близость. Отменить находку независимо от расстояния может лишь пара слов. Слово «биржевой» отменяет почтовый индекс всегда, потому что определяет собой сам «индекс» и потому стоит дальше от цифр, чем он. Общим механизмом запрещающий список сделать не вышло. Пришлось бы взвешивать слова руками, а от этого пайплайн становится ненадёжным. Каждая проверка возвращает балл уверенности и разбирает типы документов от самых узнаваемых форматов к самым спорным. Свидетельство о рождении и военный билет с их буквенными сериями идут первыми, а паспорт, индекс и водительское удостоверение — последними, потому что это простые шесть или десять цифр. Если претендентов всё равно несколько, побеждает больший балл.Шаблоны сдались, вышла модель
Контекст сильно помог, но вопрос не закрыл. Подсказки в тексте есть далеко не всегда: в строке «отправил 4111 1111 1111 1111» слова «карта» нет вовсе, и правилу не на что опереться. Запрещающий список работает так же грубо в другую сторону, далёкое слово «паспорт» снимает верный индекс. А каждый новый тип — это новый набор слов и новые пороги, которые кто-то подбирает руками. И остаётся главное. Имён, адресов и телефонов мы до этого момента не объявляли вовсе: правила их просто не находили. Формата, который описывается шаблоном, у них нет. «Александр Иванович Лужин» и «улица Мира, 12» состоят из обычных слов, а заглавная буква ничего не гарантирует: с неё начинается и название компании, и первое слово предложения. Правила живут в синтаксисе: длина, разделители, контрольные цифры, соседнее слово-подсказка. Человек опознаёт в «Лужине» фамилию по смыслу фразы, и форма записи ему для этого не нужна. Значит, нужен семантический подход, понимание написанного вместо сверки с шаблоном. Эту часть задачи правилами не закрыть в принципе, сколько бы их ни было. Дальше всё просто. Мы разметили свои сущности — тестовый и тренировочный датасеты можно посмотреть по ссылкам — и дообучили на них NER-модель на основе ruBert-base. Работы у неё при этом две. В именах и адресах модель опирается на семантику, в номерах документов только на контекст: заучивать сами цифры ей незачем.Две ветки и один арбитр
По отдельности оба подхода упираются в свой предел. Правила хорошо отвечают на вопрос о валидности: контрольное число сошлось, длина совпала, формат подходит. Смысла фразы они при этом не видят вовсе. Модель видит смысл и окружение, зато арифметику ей поручать незачем: там, где хватает точного вычисления, она будет угадывать. Самое логичное решение — поставить их рядом и дать закрывать слабости друг друга. Правила отвечают за арифметику и формат, модель — за смысл и контекст. Итоговая схема простая: два независимых потока сходятся в конце.
- Находки пересеклись, но метки разные. В этом случае место забирают правила. Например, контрольная сумма сошлась на СНИЛС, а модель разметила те же цифры как ИНН. Детерминированная арифметика надёжнее вероятностной модели, поэтому спор решается в её пользу.
- Правила промолчали, модель нашла тип. Берём находку модели. Так закрываются грязные данные вроде «Паспрт 12 34 567890», где опечатка и лишние пробелы ломают шаблон, а по контексту всё очевидно.
- Спаны не пересеклись. В результат идут находки обоих потоков: правила вытащили номер карты, модель в соседнем предложении нашла ФИО и неформатированный паспорт.
Данные уходят, смысл остаётся
Найти и закрыть персональные данные мало. Всем хочется получить ответ модели осмысленным и связным — таким же, как если бы она работала с настоящими данными. Маска этого не даёт. По строке «**** позвонил по ****» модель не поймёт, сколько в тексте людей, кто кому звонил и что отвечать. Смысл держится на связях между сущностями, а звёздочки эти связи стирают.
<PII type="PERSON" id="1" /> нет времени». Модель видит структуру: здесь один человек, вот его роль в предложении. Настоящее значение сохраняется отдельно, в таблице соответствий.
Таблица соответствий возвращается вызывающему вместе с обезличенным текстом. Сервис при этом ничего не хранит.
Метке тоже нужны документы
Формат взяли XML-подобный: в нём удобно держать атрибуты. С такой разметкой модели работают охотно, и по нашим наблюдениям такой тег переносится в ответ без переписывания. Сам вид плейсхолдера тоже приходится выбирать аккуратно. Он не должен случайно встретиться в обычном тексте или в коде, совпасть с разметкой соседних систем и агентов, и его не должен ненароком повторить пользователь. Иначе чужая строка будет принята за нашу метку, и на её месте появятся настоящие данные. Тип объясняет модели, с чем она имеет дело, потому что телефон и адрес требуют разного обращения. Номер различает сущности одного типа, два разных человека получат id 1 и 2. Для людей понадобился третий атрибут. Русский язык требует согласования по роду, а из метки род не виден, поэтому «Козлова сказала» в ответе легко превращается в «сказал». Пол определяет по имени библиотека русской морфологии, и результат кладётся прямо в тег — <PII type="PERSON" gender="female" id="1" />.Падежи размножают людей
Самое трудное здесь — удержать идентичность сущности. В русском тексте один человек встречается в разных формах: «Михаил», «Михаилу», «Михаила». Метка у него должна быть одна, иначе таблица распухает, а модель решит, что в тексте три разных человека. Решаем в два шага. Сначала соседние токены одного человека склеиваются в одну сущность, имя с отчеством и фамилией становятся единым спаном. Потом значение приводится к именительному падежу, и уже эта форма нечётко сравнивается с уже собранными из таблицы — совпадение не обязательно должно быть полным. Похожее делаем и с локациями, только сравниваем строже. Тип входит в ключ, поэтому фамилия Иванов и город Иванов лежат в разных корзинах и не сольются при любой похожести. Ошибиться этот механизм всё же может, и в обе стороны. Фраза «Моё имя Петр, фамилия Козлов» даёт две независимые метки, потому что имя и фамилия разнесены по разным частям предложения. Формально разметка верна, а на деле это один человек, разложенный на две записи. Обратная ошибка случается на голых фамилиях: «Иванов» и «Иванова» похожи достаточно, чтобы отец с дочерью получили одну метку на двоих. Стоит рядом появиться имени, и такая пара уже расходится.Возврат с уроком грамматики
Модель работает с тегами и в ответе их сохраняет. Настоящие значения подставляем мы в её готовый текст. Тут-то и вылезает грамматика. Род модель согласует сама, потому что видит его прямо в теге. А падеж на неё не повесить. В ответе тег стоит как есть, «Свяжитесь с<PII type="PERSON" id="1" />», и прямая подстановка даст «Свяжитесь с Иван Петров».
Падеж определяется местом, куда попала метка, поэтому мы делаем синтаксический разбор: берём окно вокруг тега и смотрим, от какого слова метка зависит и какую роль играет в предложении. Падеж подсказывает предлог перед меткой, а если предлога нет, роль в предложении. Само склонение делает морфологический анализатор.
Остальные типы возвращаются прямой подстановкой. Номер телефона, ИНН и адрес электронной почты падежей не имеют, и трогать их незачем.
Проверяем себя
Систему тестируем на четырёх открытых датасетах, один из которых наш собственный. Рядом измеряем другие открытые решения.
- Строгое совпадение, strict: начало, конец и тип предсказания совпадают с эталоном точно, без нормализации границ.
- Совпадение по типу, type: тип данных совпадает, спаны пересекаются хотя бы на один символ.




