AIMS-RU-Toxic-Spam-Classifier

Многоклассовая модель для модерации русскоязычных текстовых сообщений. Модель относит сообщение к одному из трёх классов:

Класс Описание
SAFE Безопасное, нейтральное сообщение
OSK Токсичность, оскорбления, унижение, агрессия
SPAM Спам, реклама, предложения "подработки", финансовые схемы и т.д.

Модель является основной частью проекта AIMS (AI Moderation System) — системы модерации сообщений в онлайн-сообществах.

Архитектура

Модель дообучена на основе DeepPavlov/rubert-base-cased

Данные

Модель обучалась на автоматически собранных сообщениях из реальных Telegram-чатов (сбор: осень 2025 — весна 2026), размеченных вручную/полуавтоматически на классы SAFE, OSK, SPAM. Для оценки качества по отдельным категориям использовались открытые датасеты:

Метрики

Оценка проводилась в бинарной постановке "целевая категория 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"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

Downloads last month
222
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support