Борьба со спамом в телеграм группе 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 Встретимся там 😉!