Token Classification
Transformers
Safetensors
Russian
English
bert
ner
pii
secret-detection
credentials
masking
russian
Instructions to use fef2/ner_rus_bert-secret_detection with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use fef2/ner_rus_bert-secret_detection with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="fef2/ner_rus_bert-secret_detection")# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("fef2/ner_rus_bert-secret_detection") model = AutoModelForTokenClassification.from_pretrained("fef2/ner_rus_bert-secret_detection", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| language: | |
| - ru | |
| - en | |
| license: apache-2.0 | |
| base_model: DeepPavlov/rubert-base-cased | |
| library_name: transformers | |
| pipeline_tag: token-classification | |
| tags: | |
| - ner | |
| - token-classification | |
| - pii | |
| - secret-detection | |
| - credentials | |
| - masking | |
| - russian | |
| - bert | |
| # 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`). | |
| ## Быстрый старт | |
| ```python | |
| 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. | |
| ```bash | |
| 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 | |
| ``` | |
| ```python | |
| 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) после нормализации меняет длину, и спаны «поедут». | |
| Режьте по спанам нормализованную строку: | |
| ```python | |
| 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`) - к инференсу он отношения не имеет, его можно игнорировать. |