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
File size: 16,919 Bytes
4a83520 0936c1f 4a83520 0936c1f 8bdd8da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | ---
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`) - к инференсу он отношения не имеет, его можно игнорировать. |