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)