File size: 12,394 Bytes
a8599cf 3413c54 a8599cf 3413c54 a8599cf 3413c54 15f2131 3413c54 1a42b76 3413c54 1a42b76 3413c54 | 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 | ---
language:
- ru
license: mit
tags:
- text-classification
- toxicity
- spam
- spam-detection
- toxic-detection
- russian
pipeline_tag: text-classification
widget:
- text: "Привет, как дела?"
- text: "ты полное ничтожество, заткнись"
- text: "Ищем людей на удалённую подработку, доход от 5000₽/день, пиши в лс"
---
# AIMS-RU-Toxic-Spam-Classifier
Многоклассовая модель для модерации русскоязычных текстовых сообщений. Модель относит сообщение к одному из трёх классов:
| Класс | Описание |
|--------|------------------------------------------------------------------|
| `SAFE` | Безопасное, нейтральное сообщение |
| `OSK` | Токсичность, оскорбления, унижение, агрессия |
| `SPAM` | Спам, реклама, предложения "подработки", финансовые схемы и т.д. |
Модель является основной частью проекта **AIMS (AI Moderation System)** — системы модерации сообщений в онлайн-сообществах.
## Архитектура
Модель дообучена на основе [`DeepPavlov/rubert-base-cased`](https://huggingface.co/DeepPavlov/rubert-base-cased)
## Данные
Модель обучалась на автоматически собранных сообщениях из реальных Telegram-чатов (сбор: осень 2025 — весна 2026), размеченных вручную/полуавтоматически на классы `SAFE`, `OSK`, `SPAM`. Для оценки качества по отдельным категориям использовались открытые датасеты:
- **Токсичность (`OSK`)** — [`molyalya/russian-toxicity-dataset`](https://huggingface.co/datasets/molyalya/russian-toxicity-dataset), сплит `train`
- **Спам (`SPAM`)** — [`alt-gnome/telegram-spam`](https://huggingface.co/datasets/alt-gnome/telegram-spam), сплит `train`
## Метрики
Оценка проводилась в бинарной постановке "целевая категория vs всё остальное". Порог решения подобран отдельно для каждой категории:
### OSK (токсичность) — `molyalya/russian-toxicity-dataset`
Порог: **0.70**
| Метрика | Значение |
|-----------|----------|
| Accuracy | 0.9302 |
| Precision | 0.9813 |
| Recall | 0.8770 |
| F1-score | 0.9262 |
### SPAM — `alt-gnome/telegram-spam`
Порог: **0.90**
| Метрика | Значение |
|-----------|----------|
| Accuracy | 0.9832 |
| Precision | 0.9841 |
| Recall | 0.9643 |
| F1-score | 0.9741 |
> Метрики приведены для бинарной классификации целевого класса от всех остальных категорий (например, для OSK — "OSK против SAFE+SPAM"). Пороги подобраны как компромисс между Precision и Recall для каждой категории отдельно; для SPAM используется более высокий порог (0.90), так как ложные срабатывания на этой категории (блокировка легитимного сообщения как спама) обычно более чувствительны для пользователей, чем на OSK. При необходимости более агрессивной модерации пороги можно снижать.
## Быстрый старт
```bash
pip install torch transformers huggingface_hub
```
```python
import json
import pickle
import torch
import torch.nn as nn
from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer, AutoModel
REPO_ID = "SafeTechDev/AIMS-RU-Toxic-Spam-Classifier"
# Пороги окончательного решения — свои для каждой категории
THRESHOLDS = {
"OSK": 0.70,
"SPAM": 0.90,
}
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ---- загрузка конфига и label encoder ----
config_path = hf_hub_download(REPO_ID, "config.json")
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
le_path = hf_hub_download(REPO_ID, "label_encoder.pkl")
with open(le_path, "rb") as f:
label_encoder = pickle.load(f)
ALL_CATEGORIES = config["all_classes"] # ["SAFE", "OSK", "SPAM"]
NUM_CLASSES = config["num_classes"]
MAX_LENGTH = config["max_length"]
SAFE_INDEX = config["safe_index"]
# ---- архитектура модели ----
class MulticlassModel(nn.Module):
def __init__(self, model_name, num_classes):
super().__init__()
self.encoder = AutoModel.from_pretrained(model_name, low_cpu_mem_usage=True)
hidden = self.encoder.config.hidden_size
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(hidden * 3, 512),
nn.GELU(),
nn.Dropout(0.1),
nn.Linear(512, num_classes),
)
def forward(self, input_ids, attention_mask):
hs = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
cls_p = hs[:, 0]
mask = attention_mask.unsqueeze(-1).float()
mean_p = (hs * mask).sum(1) / mask.sum(1).clamp(min=1e-9)
max_p = hs.masked_fill(attention_mask.unsqueeze(-1) == 0, float("-inf")).max(dim=1).values
return self.classifier(torch.cat([cls_p, mean_p, max_p], dim=-1))
# ---- загрузка весов ----
model = MulticlassModel(config["model"], NUM_CLASSES).to(device)
weights_path = hf_hub_download(REPO_ID, "pytorch_model.bin")
model.load_state_dict(torch.load(weights_path, map_location=device, weights_only=True))
model.eval()
tokenizer = AutoTokenizer.from_pretrained(REPO_ID)
# ---- инференс ----
def predict(text: str) -> dict:
enc = tokenizer(
text, truncation=True, padding="max_length",
max_length=MAX_LENGTH, return_tensors="pt",
)
with torch.no_grad():
logits = model(enc["input_ids"].to(device), enc["attention_mask"].to(device))
probs = torch.softmax(logits, dim=-1).squeeze().cpu().numpy()
pred_idx = int(probs.argmax())
label = label_encoder.classes_[pred_idx]
confidence = float(probs[pred_idx])
threshold = THRESHOLDS.get(label, 1.0) # для SAFE порог не применяется
is_final = label == "SAFE" or confidence >= threshold
return {
"label": label if is_final else "SAFE",
"raw_label": label,
"confidence": confidence,
"probs": {cls: float(p) for cls, p in zip(label_encoder.classes_, probs)},
"is_safe": is_final and label == "SAFE" or not is_final,
"is_final": is_final,
}
if __name__ == "__main__":
examples = [
"Привет! Как прошёл день?",
"ты конченый придурок, никто тебя не любит",
"Набираем людей на удалённую работу, доход от 5000 руб/день, пиши в лс",
]
for text in examples:
result = predict(text)
print(f"{text!r} -> {result['label']} ({result['confidence']*100:.1f}%)")
```
### Рекомендации по предобработке текста
Приведённый выше quickstart подаёт текст в модель как есть, без предварительной нормализации. В боевых условиях (модерация реальных Telegram-чатов) пользователи часто пытаются обойти фильтр за счёт обфускации текста, поэтому перед инференсом рекомендуется:
- **Приводить текст к нижнему регистру** — модель чувствительна к регистру, а обфускация часто использует смешанный регистр.
- **Схлопывать буквы, разделённые пробелами/точками/дефисами** (например, `п.р.и.в.е.т` → `привет`) — частый приём обхода фильтров.
- **Заменять визуально похожие символы на кириллические аналоги** — латинские буквы, цифры и символы, похожие на кириллицу (например, `a`→`а`, `e`→`е`, `0`→`о`), а также диакритические варианты латиницы и греческие буквы, используемые как омоглифы.
- **Удалять ссылки** — URL обычно не несут полезного сигнала для классификации и могут шуметь в токенизации.
- **Убирать посторонние символы**, оставляя только буквы, цифры и базовую пунктуацию.
- **Схлопывать повторяющиеся символы** (например, `круууууто` → `круто`) — распространённый способ обойти точное совпадение по словарю и "размыть" токенизацию.
Такая нормализация не входит в веса модели и должна выполняться на стороне вызывающего кода перед токенизацией. Без неё качество распознавания на намеренно обфусцированных сообщениях (сленг с заменой букв, "растянутые" слова) может заметно снижаться — сообщение просто не будет похоже на то, что модель видела при обучении.
## Использование в проде
Рекомендуемая логика принятия решения:
- `label == "SAFE"` → `SAFE`
- `label == "OSK"` и `confidence < 0.70` → трактовать как `SAFE` (модель недостаточно уверена, чтобы применять модерацию)
- `label == "OSK"` и `confidence >= 0.70` → применять модерационное действие для токсичности
- `label == "SPAM"` и `confidence < 0.90` → трактовать как `SAFE`
- `label == "SPAM"` и `confidence >= 0.90` → применять модерационное действие для спама
Пороги `0.70` (OSK) и `0.90` (SPAM) — стартовые рекомендации. Для чатов с более строгой модерацией пороги можно снижать (увеличивая Recall и число ложных срабатываний), для более мягкой — повышать. SPAM держится на более высоком пороге, поскольку ложное срабатывание здесь обычно ощущается пользователем болезненнее, чем на OSK.
## Ограничения
- Модель рассчитана на короткие/средние сообщения; на длинных текстах (статьи, посты) качество не гарантируется.
- Обучающая выборка собрана из русскоязычных чатов.
- Модель не различает подкатегории токсичности (угрозы, харассмент, оскорбления по признаку и т.д.) — только бинарно относит к `OSK`. Для этого в будущем будут сделаны более точечные модели.
## Лицензия
MIT
## Автор
[SafeTechDev](https://huggingface.co/SafeTechDev) |