--- 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)