AIMS-RU-Toxic-Spam-Classifier
Многоклассовая модель для модерации русскоязычных текстовых сообщений. Модель относит сообщение к одному из трёх классов:
| Класс | Описание |
|---|---|
SAFE |
Безопасное, нейтральное сообщение |
OSK |
Токсичность, оскорбления, унижение, агрессия |
SPAM |
Спам, реклама, предложения "подработки", финансовые схемы и т.д. |
Модель является основной частью проекта AIMS (AI Moderation System) — системы модерации сообщений в онлайн-сообществах.
Архитектура
Модель дообучена на основе DeepPavlov/rubert-base-cased
Данные
Модель обучалась на автоматически собранных сообщениях из реальных Telegram-чатов (сбор: осень 2025 — весна 2026), размеченных вручную/полуавтоматически на классы SAFE, OSK, SPAM. Для оценки качества по отдельным категориям использовались открытые датасеты:
- Токсичность (
OSK) —molyalya/russian-toxicity-dataset, сплитtrain - Спам (
SPAM) —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. При необходимости более агрессивной модерации пороги можно снижать.
Быстрый старт
pip install torch transformers huggingface_hub
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"→SAFElabel == "OSK"иconfidence < 0.70→ трактовать какSAFE(модель недостаточно уверена, чтобы применять модерацию)label == "OSK"иconfidence >= 0.70→ применять модерационное действие для токсичностиlabel == "SPAM"иconfidence < 0.90→ трактовать какSAFElabel == "SPAM"иconfidence >= 0.90→ применять модерационное действие для спама
Пороги 0.70 (OSK) и 0.90 (SPAM) — стартовые рекомендации. Для чатов с более строгой модерацией пороги можно снижать (увеличивая Recall и число ложных срабатываний), для более мягкой — повышать. SPAM держится на более высоком пороге, поскольку ложное срабатывание здесь обычно ощущается пользователем болезненнее, чем на OSK.
Ограничения
- Модель рассчитана на короткие/средние сообщения; на длинных текстах (статьи, посты) качество не гарантируется.
- Обучающая выборка собрана из русскоязычных чатов.
- Модель не различает подкатегории токсичности (угрозы, харассмент, оскорбления по признаку и т.д.) — только бинарно относит к
OSK. Для этого в будущем будут сделаны более точечные модели.
Лицензия
MIT
Автор
- Downloads last month
- 222