ner_rus_bert-secret_detection

Дообученная голова BertForTokenClassification поверх DeepPavlov/rubert-base-cased, которая размечает в русскоязычном тексте учётные данные (логины, пароли, токены, ключи), персональные данные (ФИО, организации, локации) и номера договоров. Основной сценарий, под который она обучалась - маскирование пользовательских сообщений и фрагментов конфигов перед отправкой во внешнюю LLM.

Модель — половина системы. В проде она работает в объединении с детерминированным regex-слоем, и все опубликованные ниже цифры покрытия учётных данных относятся к объединению NER ∪ regex. Одна NER заметно слабее по recall — так и задумано: её задача добирать то, что не ловится регулярками (естественная формулировка, кириллица, контекст), не переусердствуя с ложными срабатываниями. Обе половины лежат в этом репозитории: core/scrubber.py + gitleaks.toml См. Детерминированный слой.

Метки

17 BIO-тегов, 8 типов сущностей:

Группа Метки
Учётные данные LOGIN, PASSWORD, AUTH_TOKEN, SECRET_KEY
ПДн PERSON, LOCATION, ORGANIZATION
Прочее CONTRACT_NUMBER

Схема — B-/I-/O, полный список в config.json (id2label).

Быстрый старт

from transformers import pipeline

ner = pipeline(
    "token-classification",
    model="fef2/ner_rus_bert-secret_detection",
    aggregation_strategy="simple",
)

text = 'user = svc_billing, password = "Xt7#pQm2Zr", api_key: sk-live-4f9a2b7c1e6d8f3a0b5c9d2e'
for e in ner(text):
    print(f"{e['entity_group']:<12} {e['score']:.3f} {text[e['start']:e['end']]!r}")
LOGIN        0.936 'svc_billing'
PASSWORD     0.993 'Xt7#pQm2Zr'
SECRET_KEY   0.996 'sk-live-4f9a2b7c1e6d8f3a0b5c9d2e'

Обратите внимание: режьте исходную строку по start/end, а не берите поле word — pipeline отдаёт его склеенным из wordpiece, с лишними пробелами ('svc _ billing').

У pipeline() три ограничения, из-за которых он годится только для проб. Он не режет длинный вход на окна — текст длиннее 512 wordpiece падает с RuntimeError: The size of tensor a (1218) must match the size of tensor b (512); не нормализует юникод; и это только NER, без детерминированного слоя. Для настоящей работы возьмите predict.py из этого репозитория.

Правильный путь: predict.py

predict.py — перенос инференс-логики развёрнутого сервиса целиком: NFC-нормализация, нарезка на окна по 384 реальных wordpiece по границам предложений, BIO-декод, детерминированный слой, смещения в символах, склейка пересекающихся спанов. Нужны только torch и transformers — regex-половина на чистом stdlib.

python predict.py --text 'password = "Xt7#pQm2Zr"'      # замаскированный текст
python predict.py --file документ.txt --spans           # спаны построчно
cat лог.txt | python predict.py --json                  # спаны в JSON
cat лог.txt | python predict.py --json --no-rules       # только NER, без regex
from predict import Detector

det = Detector("fef2/ner_rus_bert-secret_detection", device="cpu")   # cuda / mps

text = ('Коллеги, доступ к стенду: user = svc_billing, password = "Xt7#pQm2Zr", '
        'api_key: sk-live-4f9a2b7c1e6d8f3a0b5c9d2e. Ответственный — Петров Сергей '
        'Иванович (ПАО «Сбербанк», Санкт-Петербург), договор № 77-АБ/2025-4412.')

for s in det.spans(text):
    print(f"{s.start:>4} {s.end:>4}  {s.label:<16} {s.source:<5} {s.text!r}")

print(det.mask(text))
  33   44  LOGIN            ner   'svc_billing'
  58   68  PASSWORD         both  'Xt7#pQm2Zr'
  80  112  SECRET_KEY       both  'sk-live-4f9a2b7c1e6d8f3a0b5c9d2e'
 130  152  PERSON           ner   'Петров Сергей Иванович'
 154  167  ORGANIZATION     ner   'ПАО «Сбербанк'
 170  185  LOCATION         ner   'Санкт-Петербург'
 198  213  CONTRACT_NUMBER  ner   '77-АБ/2025-4412'

source говорит, кто нашёл спан: ner, regex или both (нашли оба — самый спокойный случай). У чисто regex-спанов score равен None: у детерминированного правила нет вероятности, и подставлять туда единицу было бы враньём.

Документ на 6800 символов режется на 4 окна и размечается целиком — граница окна не теряет спаны, потому что нарезка идёт по концам предложений и абзацев.

Четыре контракта, которые легко нарушить

Окно — 384 реальных wordpiece. Модель обучалась на окнах такого размера, и plan_windows закладывает 384 токена без учёта [CLS]/[SEP], то есть max_length=386 при токенизации. Если считать по-хагингфейсовски — 384 вместе со служебными, — в окно попадёт 382 реальных токена, и предсказания на длинных текстах разойдутся: два лишних wordpiece меняют каждый контекстный эмбеддинг последовательности. На эталонном сплите в этот зазор попадают 280 строк из 6312, и 5 из них предсказываются иначе.

Смещения — по NFC-нормализованному тексту. Detector.spans() сам приводит вход к NFC, и start/end — индексы кодовых точек нормализованной строки. Для латиницы и обычной кириллицы это то же самое, но текст с составными символами (диакритика, вставки из PDF) после нормализации меняет длину, и спаны «поедут». Режьте по спанам нормализованную строку:

import unicodedata
normalized = unicodedata.normalize("NFC", text)   # ровно то, что видела модель
fragment = normalized[span.start:span.end]

Батчи, а не по одному. Пропускная способность здесь берётся из батчей; Detector(batch_size=...) задаёт размер микро-батча окон. На CPU замеренная скорость NER-половины — около 9.5k токенов/с; на GPU в FP16 (fp16=True, включается автоматически при device="cuda") она перестаёт быть узким местом задолго до regex-слоя.

Модель не решает, что делать со спаном. mask() подставляет [REDACTED:LABEL] — это пример, а не политика. Обратимое маскирование (плейсхолдеры с обратной подстановкой), пороги по score, отдельная политика на PERSON против SECRET_KEY — всё это ваш слой поверх спанов.

Детерминированный слой

Рядом с моделью по тому же тексту работает второй, полностью детерминированный детектор — core.scrubber.credential_sites. Итоговый набор спанов есть объединение двух источников с последующей склейкой. Он даёт постоянный, не зависящий от чекпоинта пол по типовым форматам, а NER добирает то, что регуляркой не описывается. Внутри — три детектора:

Детектор Что ищет Метки
kv ключ = значение с онтологией ключей (password, пароль, api_key, токен, login…) PASSWORD SECRET_KEY AUTH_TOKEN LOGIN
cli значения у известных флагов известных команд (--password=…, curl -u user:pass) PASSWORD AUTH_TOKEN
opaque непрозрачные высокоэнтропийные значения + правила gitleaks AUTH_TOKEN SECRET_KEY SECRET

Правила gitleaks отбираются по идентификатору (…token…AUTH_TOKEN, …key…/…secret…SECRET_KEY), из 222 правил конфига до рантайма доходят 168 — остальные отсеиваются фильтром по имени либо не компилируются под re без предупреждений. load_gitleaks_rules возвращает вторым значением именно счётчик отброшенных.

cli бьёт по таблице «команда → флаг» и отдаёт только само значение пароля или токена; логин из -u user:pass в объединение не попадает, а голая строка подключения (postgres://user:pass@host) не покрывается вовсе — это осознанная граница релиза, см. Ограничения.

Слой умеет отличать живое значение от инертного: ${DB_PASSWORD}, os.environ["TOKEN"], <your-key-here>, changeme и уже проставленные маркеры [REDACTED:…] он не трогает (core._shim.is_inert_value). Спан с меткой SECRET — это опознанное непрозрачное значение, тип которого установить не удалось; в модельный набор из 17 меток он не входит и приходит только отсюда.

Замеренная цена этой половины на тестовом сплите: 2891 срабатывание, из них 452 не задевают ни одного gold-спана; вычёркивается 3.03 % всех символов, из которых 0.37 % корпуса лежит вне gold-спанов. Слой чисто питоновский и держит GIL — около 0.7 мс на текст (~460k токенов/с на ядро), так что в проде именно он, а не GPU, упирается в потолок пропускной способности.

Отключается через Detector(rules=None) или --no-rules — но тогда цифры ниже к вам не относятся.

Обучение

Базовая модель DeepPavlov/rubert-base-cased (vocab 119547, 12 слоёв, 768)
Шаги / batch / lr 8000 · 32 · 3e-5, warmup 800, weight decay 0.1, grad clip 1.0, AMP
Окно 384 wordpiece
Выбор чекпоинта по credential_any_f1 на валидации (максимизируется вклад NER в детекцию учётных данных: пол от регулярок постоянен)
Данные синтетические семейства учётных данных в русскоязычном контексте (kv-pair в сообщениях, конфигах и коде) + replay-доля 0.35 на общий русский NER, чтобы не потерять ПДн

Ограничения

  • CLI и DSN — вне зоны ответственности. Строки подключения (postgres://user:pass@host) и пароли в аргументах команд сознательно не входят в целевую поверхность релиза и на ней не измерялись. Модель на них ведёт себя неровно: psql -U reporter -W 'Qw!7823ml' -h db.corp.ru → спан PASSWORD захватывает открывающую кавычку, а хвост имени хоста уезжает в LOGIN. Детерминированный слой подстраховывает только известные флаги известных команд. Целевая поверхность — kv-pair: ключ = значение в сообщениях, конфигах и коде.
  • Границы спанов не всегда аккуратны. Модель может отрезать закрывающую кавычку ('ПАО «Сбербанк') или разделить фразу на два спана. Для маскирования это безопасно (спан всё равно вычёркивается), для извлечения сущностей — учитывайте.
  • Не детектор «всех секретов». Формат, не похожий ни на одно из обученных семейств и не покрытый регуляркой, будет пропущен. Recall 0.967 — это про объединение на распределении сплита, а не гарантия на вашем корпусе. Измерьте на своих данных.
  • Один seed. Веса выпущены под пилот, из одного прогона; трёхсидовая сертификация релиза не проводилась.
  • Происхождение данных. Общая NER-часть (PERSON/ORGANIZATION/LOCATION) опирается на публичные русские NER-корпуса; учётные данные — синтетика. Настоящих секретов в обучающем корпусе нет.

Файлы

config.json · model.safetensors     веса (BertForTokenClassification, fp32, 709 МБ)
tokenizer.json · vocab.txt · …      токенизатор (fast, BertTokenizer, cased)
predict.py                          инференс: окна · NFC · BIO-декод · объединение
gitleaks.toml                       правила детерминированного слоя
core/scrubber.py                    детерминированный слой: kv · cli · opaque
core/_shim.py                       инертные значения, NFC, вспомогательное
core/slot_ontology.yaml             онтология ключей для kv-детектора

core/ скопирован без изменений из masking-service/core бандла r3-2026-07-22 — того же кода, что крутится в проде; predict.py вызывает load_gitleaks_rules и credential_sites из него, а не переписывает их. У scrubber.py есть ещё и CLI датасетного пайплайна (python -m core.scrubber) - к инференсу он отношения не имеет, его можно игнорировать.

Downloads last month
39
Safetensors
Model size
0.2B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for fef2/ner_rus_bert-secret_detection

Finetuned
(72)
this model