Любите загадки? Событие еще доступно на сайте.

Борьба со спамом в телеграм группе Laravel

Каждый из нас кто, активно общается в публичных группах, сталкивался с раздражающим спамом, который отвлекает от действительно важного контента. По ощущениям один из самых современных месенжеров Telegram с этим ничего не делает 🤨.

Появились сотни ботов, предлагающих различные методы для борьбы с надоевшими рекламными обьявлениями. Некоторые предлагают в качестве решение ввод капчи, нажатие на кнопку, голосование или другие различные механизмы защиты. 🤖

Ну, ребята, давайте без спама.

В какой-то момент времени я задумался о том, как мы можем отсеять спам-сообщения? Ведь это одна из самых старых проблем: сервисы электронной почты столкнулись с этим больше 20 лет назад. Что же мешает нам воспользоваться их опытом?

Ни один почтовый сервис не требует от пользователя пройти капчу перед отправкой письма. А забавно было бы, не правда? 😂 Представьте, каждый раз при отправке письма вам нужно решить головоломку. Вот-вот, скорее всего это вызовет недоумение, но в самом современном мессенджере это норма. Предлагаю попытаться улучшить опыт!

Классификатор

Одним из успешных методов борьбы со спамом стал байесовский классификатор. Он анализируя обучающую выборку и делая предположение на её основе. Используя простую статистику и немного математики для вычисления результата. Например, рассмотрим следующую обучающую выборку, состоящую из 4 строк:

Сообщение Тип
Люблю читать книги о приключениях и путешествиях. HAM
Мы часто проводим выходные на даче с любимой семьей HAM
Купите таблетки для похудения уже сегодня, скидка 50%! SPAM
Увеличьте свой доход, зарегистрируйтесь на нашем сайте бесплатно! SPAM

То есть, если передать классификатору “Получите доход не вставая с дивана уже сегодня!”, он должен вернуть предположение что это скорее всего SPAM.

Реализуем на практике

Прежде чем алгоритм сможет что-либо сделать, ему нужна обучающая выборка с исторической информацией в которой он должен знать две вещи:

  • Как часто каждое слово встречается для каждого Type
  • Сколько документов (или утверждений) приходится на каждый Type

Реализация будет хранить эту информацию в двух массивах:

private array $words = [];
private array $documents = [];

Вся остальная информация может быть агрегирована из этих массивов. Реализация будет выглядеть так:

public function learn(string $statement, string $type): self
{
    $words = $this->getWords($statement);

    foreach ($words as $word) {
        if (!isset($this->words[$type][$word])) {
            $this->words[$type][$word] = 0;
        }

        // увеличиваем счётчик слов для данного типа
        $this->words[$type][$word]++;
    }

    if (!isset($this->documents[$type])) {
        $this->documents[$type] = 0;
    }

    // увеличиваем счётчик документов для данного типа
    $this->documents[$type]++;

    return $this;
}

Алгоритм должен учитывать только слова и игнорировать всё остальное. Простой метод, который даёт список слов из строки, можно реализовать следующим образом:

protected function getWords(string $string): array
{
    $cleaned = preg_replace('/[^A-Za-zА-Яа-я\s]/u', '', strtolower($string));

    return array_filter(preg_split('/\s+/', $cleaned));
}

В реальной жизни, лучше всего убирать нейтральные предлоги, союзы и их эквиваленты. Например: и, в, на, но и т.п. Так как они нейтральные и встречаться вне зависимости от типа. Самый простой способ фильтровать слова по количеству символов.

Итак, теперь мы сохраняем исторические данные и почти готовы к тому, что бы угадывать принадлежность введёной строки к типу.

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

protected function typeProbability(string $type)
{
    return $this->documents[$type] / array_sum($this->documents);
}

Напримере набора из таблицы и для HAM , и для SPAM типов результат будет 0.5, так как есть 2 типа для каждого из 4 документов. 2 / 4= 0.5

Теперь нам нужно определить вероястность того, что слово принадлежит к определённому типу. Для этого посчитаем сколько раз слово встречалось в документах для данного типа, а затем разделим на общее количество слов в документах этого же типа:

protected function conditionalWordProbability($word, $type)
{
    $count = $this->words[$type][$word] ?? 0;

    return ($count + 1) / (array_sum($this->words[$type]) + 1);
}

В текущим наборе вероятность того, что слово "сегодня" относится к спаму, будет около 0.176. Так как оно употребляется дважды: (2 + 1) / (16 + 1).

Всё готово, пора начинать угадывать тип!

Угадываем тип

Чтобы определить, к какому типу относится предложение, мы вначале узнаём вероятность типа. Затем мы строим цепочку к каждому слову в предложении. Для каждого слова мы умножаем вероятность этого слова для каждого типа на текущую вероятность этого типа. Таким образом, мы вычисляем вероятность того, что предложение принадлежит каждому типу. В конце концов, мы выбираем тип с наибольшей вероятностью для данного предложения.

Важно отметить, что это не полная реализация теоремы и в контексте задачи опущены некоторые шаги поскольку нас интересует только наивысшая вероятность, а не фактическое значение.

Посмотрим на реализацию:

public function guess(string $statement)
{
    // разбиваем на токены (В данном случае слова)
    $words = $this->getWords($statement);
    
    $bestLikelihood = 0;
    $bestType = null;

    $types = array_keys($this->documents);

    foreach ($types as $type) {
        // Начальная вероятность для данного типа
        $likelihood = $this->typeProbability($type); 

        foreach ($words as $word) {
            // Умножить на условную вероятность слова для данного типа
            $likelihood *= $this->conditionalWordProbability($word, $type);
        }

        if ($likelihood > $bestLikelihood) {
            $bestLikelihood = $likelihood;
            $bestType = $type;
        }
    }

    return $bestType;
}

Вот и всё, теперь мы может угадывать, к какому типу относится утверждение. Осталось только собрать всё вместе:

$classifier = new Classifier();
$classifier
    ->learn('Люблю читать книги о приключениях и путешествиях.', 'ham')
    ->learn('Мы часто проводим выходные на даче с любимой семьей', 'ham')

    ->learn('Купите таблетки для похудения уже сегодня, скидка 50%!', 'spam')
    ->learn('Увеличьте свой доход за неделю, зарегистрируйтесь на нашем сайте бесплатно!', 'spam');

var_dump($classifier->guess('Игра Монополия" наша любимая, мы часто играем в нее семьей.')); // string(3) "ham"
var_dump($classifier->guess('Получите доход не вставая с дивана уже сегодня!')); // string(4) "spam"

Доработанную реализацию я добавил в репозийторий на GitHub:

Делаем предположения на основе набора данных.

Кроме того, теперь реализация работает на благо нашего сообщества и фильтрует спам сообщения в телеграмм группе https://t.me/laravelrus Встретимся там 😉!

Александр Черняев

Я здесь для тебя

4

Вакансии

Спонсоры

Помощь в разработке вашего проекта на Laravel

Независимо от сложности проекта эти кампании помогают сообществу и всем его участникам воплощать идеи в элегантные приложения.

Присоединиться

Инструменты для управления эмоциями, которые помогают людям контролировать свою жизнь и лучше понимать себя.

Перейти

Подкасты c зажигательными эпизодами, которые заставят задуматься и приведут к новым перспективам.

Перейти