File size: 6,980 Bytes
fc53c27
ee3db37
 
fc53c27
ee3db37
 
 
 
 
 
 
 
 
 
 
 
 
 
fc53c27
ee3db37
 
 
 
 
 
 
 
084dc6e
 
0607622
 
 
 
ee3db37
 
 
 
 
 
084dc6e
 
 
ee3db37
12fa79a
ee3db37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
language:
- ru
license: mit
base_model: DeepPavlov/rubert-base-cased
tags:
- text-classification
- spam-detection
- russian
- telegram-moderation
- bert
pipeline_tag: text-classification
datasets:
- alt-gnome/telegram-spam
metrics:
- f1
- precision
- recall
---

# AIMS-RU-Binary-Spam-Classifier

Бинарный классификатор спама для русскоязычных сообщений, часть проекта **AIMS (AI Moderation System)**.

Модель дообучена поверх [`DeepPavlov/rubert-base-cased`](https://huggingface.co/DeepPavlov/rubert-base-cased) и определяет, является ли сообщение **SPAM** или **SAFE**.
Лёгкая и быстрая модель.

## Обновлено. Фикс некоторых ложных срабатываний

## Обучающие данные

Обучение проводилось на сообщениях, собранных в активных публичных Telegram чатах осенью-весной 2025-2026 года.

## Метрики

Оценка проводилась на внешнем датасете [`alt-gnome/telegram-spam`](https://huggingface.co/datasets/alt-gnome/telegram-spam), не пересекающемся с обучающей выборкой:

| Метрика   | Значение |
|-----------|----------|
| F1        | 0.95     |
| Precision | 0.98     |
| Recall    | 0.92     |

Высокий precision (0.98) означает низкую долю ложных срабатываний — модель редко помечает безопасные сообщения как спам, что важно для автоматической модерации без риска задеть живых пользователей.

## Архитектура

- Энкодер: `DeepPavlov/rubert-base-cased`
- Голова классификации: `Dropout(0.2) → Linear(hidden_size, 1)`
- Пулинг: эмбеддинг токена `[CLS]`
- Функция потерь при обучении: `BCEWithLogitsLoss` с весом положительного класса (`pos_weight`) для компенсации дисбаланса SAFE/SPAM
- Выход: один логит → `sigmoid` → вероятность класса SPAM

## РЕКОМЕНДАЦИЯ Нормализация текста

Перед подачей в модель рекомендуется нормализовать текст, это использовалось при обучении.

- убирает "разрежённое" написание (`п.р.и.в.е.т.``привет`)
- заменяет гомоглифы (похожие символы из других алфавитов — `ᴀ`, `α`, латиница вместо кириллицы и т.д.) на кириллические эквиваленты
- удаляет ссылки и команды ботов (`/cmd@bot`)
- схлопывает повторяющиеся буквы (`приввееееет``привет`)
- приводит текст к нижнему регистру



## Быстрый запуск

```bash
pip install torch transformers huggingface_hub
```

```python
import re
import json
import torch
import torch.nn as nn
from transformers import AutoTokenizer, AutoModel
from huggingface_hub import hf_hub_download

REPO = "SafeTechDev/AIMS-RU-Binary-Spam-Classifier"
device = "cuda" if torch.cuda.is_available() else "cpu"

# ── Архитектура ──────────────────────────────────────────────────────────
class BinaryModel(nn.Module):
    def __init__(self, model_name):
        super().__init__()
        self.bert = AutoModel.from_pretrained(model_name, low_cpu_mem_usage=True)
        hidden = self.bert.config.hidden_size
        self.binary = nn.Sequential(
            nn.Dropout(0.2),
            nn.Linear(hidden, 1)
        )

    def forward(self, input_ids, attention_mask):
        outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
        pooled = outputs.last_hidden_state[:, 0]
        return self.binary(pooled).squeeze(-1)

# ── Загрузка ─────────────────────────────────────────────────────────────
config_path = hf_hub_download(REPO, "config.json")
weights_path = hf_hub_download(REPO, "pytorch_model.bin")

with open(config_path, encoding="utf-8") as f:
    cfg = json.load(f)

MAX_LENGTH = cfg.get("max_length", 40)

tokenizer = AutoTokenizer.from_pretrained(REPO)

model = BinaryModel(cfg["model"]).to(device)
model.load_state_dict(torch.load(weights_path, map_location=device, weights_only=True))
model.eval()

# ── Инференс ─────────────────────────────────────────────────────────────
def classify(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))

    prob_spam = float(torch.sigmoid(logits).squeeze().cpu().item())
    label = "SPAM" if prob_spam >= 0.7 else "SAFE"

    return {"label": label, "prob_spam": prob_spam}


print(classify("Дам денег, работу, пишите в лс"))
# {'label': 'SPAM', 'prob_spam': 0.98...}

print(classify("Привет, как дела?"))
# {'label': 'SAFE', 'prob_spam': 0.02...}
```

### Через pipeline (упрощённо, без кастомной нормализации)

Модель хранит веса в формате `pytorch_model.bin` с кастомной головой, поэтому напрямую через `transformers.pipeline("text-classification", ...)` она не запустится — используйте код выше.


## Ограничения

- Модель обучена и валидирована на **русскоязычном** тексте; на других языках качество не гарантируется.
- Порог 0.7 используется по умолчанию для разделения SAFE/SPAM — в проде рекомендуется подбирать порог под свои данные (например, по максимальному F1), т.к. соотношение precision/recall меняется в зависимости от домена.
- Модель не учитывает контекст переписки — классифицируется только отдельное сообщение.

## Автор

[SafeTechDev](https://huggingface.co/SafeTechDev)