Этот блог станет продолжением моего первого, в котором я использовал НЛП для выявления спам-ответов в Твиттере. Вместо использования текстовых данных и НЛП сегодня мы собираемся использовать классические модели машинного обучения и табличные данные в нашей попытке найти спам в Твиттере. В конце я также приведу пример создания нейронной сети с использованием табличных данных.

Сбор данных:

Для начала нам нужно будет собрать наши данные. Как упоминалось ранее, мы хотим, чтобы эти данные были табличными — то есть они будут выглядеть как электронная таблица, как в Excel. Код для сбора данных будет частично похож на код нашего первого блога, но на этот раз нам нужно будет проработать некоторые функции. Разработка функций — это извлечение и преобразование данных в переменные, которые могут помочь нам определить, является ли ответ спамом или нет. Например, при получении ответов мы захотим обратить внимание на некоторые функции или независимые переменные, которые, по нашему мнению, могут быть полезны при обнаружении спама. Поскольку ответы всегда ботируются, хорошим примером может служить вычисление того, насколько быстро был сделан каждый ответ на исходный твит. Боты обычно не отвечают через несколько минут после публикации твита, поскольку их ответ настроен сразу после того, как определенная учетная запись отправит новый твит. Будет еще пара примеров созданных мною функций, которые, по моему мнению, будут полезны. А пока давайте перейдем к коду для сбора данных:

Примечание: к большому сожалению, Илон внес изменения в API Twitter. Чтобы собирать подобные данные сейчас, вам необходимо подписаться на базовый план API Twitter, который обойдется вам в 100 долларов США в месяц.

#Imports 
import os
import requests
import json 
from dotenv import load_dotenv
import time
load_dotenv()
import pandas as pd 
import csv
import tweepy
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import re
import numpy as np

#setup api keys
twitter_api_key = os.getenv("TWITTER_API_KEY")
twitter_secret_key = os.getenv("TWITTER_SECRET_KEY")
twitter_access_token = os.getenv("TWITTER_ACCESS_TOKEN")
twitter_secret_token = os.getenv("TWITTER_SECRET_TOKEN")

#authorize api and create api object
def oAuth():
    try:
        auth = tweepy.OAuthHandler(twitter_api_key,twitter_secret_key)
        auth.set_access_token(twitter_access_token,twitter_secret_token)
        return auth
    except Exception as e:
        return None

oauth= oAuth()

tweepy_api = tweepy.API(oauth)

#Collect tweets from account
def get_new_tweets(names):
    print("Retrieving tweets")
    corpus = []                                                                                        
    for name in names:
        tweets = tweepy_api.user_timeline(screen_name = name, include_rts=False, count=5, tweet_mode="extended", exclude_replies = True)          
        time.sleep(4)
        corpus.extend(tweets)                                                                          
    data = [[tweet.id_str, tweet.user.screen_name, tweet.full_text, tweet.created_at] for tweet in corpus]
    tweets = pd.DataFrame(data, columns=['tweet_id', 'screen_name', 'text', 'timestamp'])                

    return tweets

# Change this to whoever you want
screen_names = ['twitter user of your choice']

#Get tweets from twitter user of choice
user_tweets = get_new_tweets(screen_names)
user_tweets.head()

#Twitter user + tweet ID you want replies to:
name = 'twitter user of your choice'
tweet_id = 'ID of tweet you selected from their tweets'

#Gather replies
replies=[]
for tweet in tweepy.Cursor(tweepy_api.search_tweets,q='to:'+name, result_type = 'recent', tweet_mode = 'extended').items(100):
    if hasattr(tweet, 'in_reply_to_status_id_str'):
        if (tweet.in_reply_to_status_id_str==tweet_id):
            replies.append(tweet)

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

#Empty lists to append our data to that we want
text = []
screen_name = []
followers = []
following= [] #friends-count
account_age = []
verified = []
tweet_count = [] #statuses count
default_profile_image = []
user_mentions = []
linked_urls = []
reply_time = []
has_hashtag =[]

for i in range(len(replies)):
    text.append(replies[i].full_text)
    screen_name.append(replies[i].user.screen_name)
    followers.append(replies[i].user.followers_count)
    following.append(replies[i].user.friends_count)
    account_age.append(replies[i].user.created_at)
    verified.append(replies[i].user.verified)
    tweet_count.append(replies[i].user.statuses_count)
    default_profile_image.append(replies[i].user.default_profile_image)
    user_mentions.append(len(replies[i].entities['user_mentions'])) # Determines # of user mentions
    linked_urls.append(len(replies[i].entities['urls'])) # Determines if they linked a URL
    reply_time.append(replies[i].created_at)

Все вышеперечисленное можно собрать непосредственно из объекта ответов, который у нас был. После изучения многочисленных ответов я обнаружил, что часто твиты ботов, как правило, включают в текст #, поэтому я создал следующий цикл, чтобы определить, использовал ли каждый человек # в своем письменном тексте:

for i in range(len(replies)):
    if '#' in replies[i].full_text:
        has_hashtag.append('yes')
    else:
        has_hashtag.append('no') 

Затем мы можем использовать списки данных для создания фрейма данных pandas:

df = pd.DataFrame(
    {'text': text,
     'screen_name': screen_name,
     'followers':followers,
     'following':following,
     'account_age': account_age,
     'verified': verified,
     'tweet_count':tweet_count,
     'default_prof_img':default_profile_image,
     'user_mentions': user_mentions,
     'linked_urls' : linked_urls,
     'reply_time': reply_time,
     'has_hashtag' : has_hashtag
    })

В результате у вас должен получиться фрейм данных, который выглядит примерно так:

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

По моему мнению, каждая из вышеперечисленных функций, которая может ловить ботов, такова (слева направо):

Аккаунты ботов, вероятно, довольно часто блокируются после того, как Твиттер в конце концов находит их, или о них достаточно сообщают. Таким образом, большинство учетных записей ботов, скорее всего, будут иметь меньшее количество подписчиков, возможно, будут подписываться только на определенные учетные записи, на которые они хотят ответить, иметь более молодой возраст учетной записи, не проверены, иметь большее количество твитов, поскольку они ботируют свои ответы, иметь изображение профиля по умолчанию (добавление изображения профиля не заняло времени), частое упоминание других пользователей (в блоге NLP мы видели, что многие ответы хотели, чтобы вы перешли на другие учетные записи), часто ссылаются на URL-адреса (нажимайте на вредоносные ссылки), быстро отвечайте, и склонны использовать хэштеги.

Мы все еще можем добавить больше столбцов с помощью разработки функций, и нам также нужно немного очистить эти данные, потому что, например, признак «возраст учетной записи» хранится как точная дата, но модели машинного обучения будут гораздо полезнее, если мы сможем просто рассчитайте такую ​​функцию, как возраст аккаунта в днях. Давайте перейдем к этому.

Очистка данных и разработка функций:

Первая функция, которую мы собирались добавить: как быстро этот человек ответил на исходный твит?

# Set time variable for when the original tweet was tweeted
tweet_time = user_tweets['timestamp'][0]

# Refers to original tweet creation time from earlier variable
df['tweet_time'] = tweet_time

# Calculates how long after the tweet was sent, the user replied to the tweet
df['time_to_respond_minutes'] = (user_tweets['timestamp'][0] - df['reply_time']).astype('timedelta64[m]')*-1

df = df.drop(['tweet_time','reply_time'],axis= 1)

Первая строка кода просто собирает временную метку исходного твита. Оттуда мы добавляем его во фрейм данных. Затем мы создаем еще одну переменную «time_to_respond_mines» и добавляем ее в фрейм данных. Чтобы рассчитать это, мы берем отметку времени исходного твита и вычитаем ее из отметки времени ответа, которую мы собрали ранее в столбце «reply_time». Теперь мы знаем, сколько минут потребовалось каждому ответившему, чтобы ответить на исходный твит после его написания. Теперь, когда это у нас есть, мы можем удалить столбцыtwitter_time и report_time.

Теперь мы собираемся написать код, который изменит столбец возраста учетной записи на возраст учетной записи в днях:

# Create a variable for todays date to calculate how old the account is
today = pd.Timestamp.now()
today = today.date()

#calculates the accounts age in days
df['account_age_days'] = today - df['account_age'][i].date()
for i in range(len(replies)):    
    df['account_age_days'][i] = today - df['account_age'][i].date()
    df['account_age_days'][i] = df['account_age_days'][i].days

# cast as float so python knows its a number
df['account_age_days'] = df['account_age_days'].astype('float64')

#drop account age column
df = df.drop(['account_age'],axis= 1)

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

Еще два столбца, которые я хотел добавить, — это соотношение подписчиков к числу подписчиков и среднее количество твитов, которые они пишут в день. Моя интуиция подсказала, что у ботов, вероятно, будет мало подписчиков, и они могут подписываться на многие аккаунты, на которые они отвечают, поэтому соотношение будет иметь тенденцию быть низким для учетных записей ботов и выше для реальных учетных записей. Я полагал, что среднее количество твитов в день также будет выше обычного, поскольку они постоянно отвечают на твиты других, а большинство людей не пишут в Твиттере ежедневно. Оба значения можно просто вычислить, используя данные из уже имеющихся столбцов:

# follower to following ratio
df['follwers_to_following_ratio'] = (df['followers']/df['following']).round(2)

#avg tweets/day
df['avg_tweets_per_day'] = (df['tweet_count']/df['account_age_days']).round(2)

Затем мы можем записать этот фрейм данных pandas в файл Excel и начать сбор данных:

import pandas as pd

with pd.ExcelWriter('Twitter_data.xlsx', mode= 'a', engine='openpyxl', if_sheet_exists='new') as writer:
    df.to_excel(writer)

Тогда для непрограммистов остался гораздо более простой для чтения фрейм данных, который вы, вероятно, привыкли видеть в Excel:

Наш окончательный результат имеет 12 столбцов/независимых переменных: подписчики, подписчики, проверенные (да/нет), количество твитов, изображение профиля по умолчанию (да/нет), количество упомянутых пользователей, количество связанных URL-адресов, если их ответ содержал # ( да/нет), время ответа на исходный твит, возраст аккаунта в днях, соотношение подписчиков к числу подписчиков и среднее количество твитов в день.

Наша зависимая переменная — бот это или нет, которую я вручную пометил для каждого образца как 0 — нет и 1 — да. Мы собираемся попытаться передать все наши независимые переменные в модель машинного обучения и посмотреть, сможет ли она использовать эти данные для точного прогнозирования нашей зависимой переменной, бот/не бот.

На что следует обратить внимание:

  • Колонки, которые у нас есть, и разработанные мной функции основаны на моем интуитивном понимании того, что, по моему мнению, приведет нас к поиску ботов. То, что обнаруживают модели машинного обучения, может сильно отличаться от того, что мы изначально думали.
  • Если пользователь упоминает › 1, это означает, что в своем ответе он упомянул кого-то другого, кроме оригинального автора. (По умолчанию все ответы включают @originalpostername, а затем ответ, поэтому все ответы начинаются с упоминания пользователя = 1).
  • Если URL-адрес ссылки › 0, они разместили внешнюю ссылку в своем ответе.

Модели зданий:

Прежде всего, давайте импортируем наши библиотеки, загрузим наши данные и получим представление о том, как выглядят наши данные:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder, StandardScaler
import seaborn as sns
from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
import tensorflow as tf
from tensorflow.keras.layers import TextVectorization
import random
from tensorflow.keras import layers
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
import tensorflow_hub as hub
import xgboost as xgb
from fastai.tabular.all import *

#read in df
df = pd.read_csv('twitter_replies_data.csv')
df.head()

#drop unwanted columns
df = df.drop(columns=["Unnamed: 0",'screen_name','clean_text'])

#check our columns
df.columns

#see if there is any missing data
df.isna().sum()

#check count of bot vs non bot 
df['is_bot?'].value_counts()

Приведенный выше код считывает фрейм данных, удаляет ненужные столбцы, а затем показывает нам, сколько у нас столбцов (12), проверяет, есть ли у нас какие-либо пропущенные значения в наших данных (необходимо заполнить или удалить, если есть) в нашем случае. пропущенных значений нет, а затем проверяет, сколько имеющихся у нас образцов являются ответами ботов и реальными ответами. Всего у нас 257 образцов, из них 210 реальных и 47 ответов ботов.

Затем мы можем быстро просмотреть наши данные. У нас есть 9 числовых столбцов, один столбец объекта (has_#) и два логических столбца (проверено, default_profile_image). Логическое значение просто означает «истина/ложь».

df.describe(include=(np.number))
df.describe(include=('object'))
df.describe(include=('bool'))

Вот как выглядит описание наших числовых значений:

Это показывает нам среднее значение, стандартное отклонение, минимум, максимум и процентили всех наших числовых переменных. Давайте посмотрим на некоторые диаграммы, основанные на некоторых функциях, которые, по нашему мнению, важны для прогнозирования ботов:

fig,axs = plt.subplots(1,2, figsize=(11,5))
sns.barplot(data=df, y=dep, x="user_mentions", ax=axs[0]).set(title="User Mentions")
sns.countplot(data=df, x="user_mentions", ax=axs[1]).set(title="Histogram")

Слева вы можете видеть, что чем больше пользователей упоминается в ответах, тем выше вероятность того, что ответ является ответом бота. Справа вы можете увидеть, сколько ответов содержало 1, 2 или 3 упоминания пользователя. Давайте сделаем то же самое для используемых хэштегов:

fig,axs = plt.subplots(1,2, figsize=(11,5))
sns.barplot(data=df, y=dep, x="has_#", ax=axs[0]).set(title="Hashtages")
sns.countplot(data=df, x="has_#", ax=axs[1]).set(title="Histogram")

Ответы с хэштегами с гораздо большей вероятностью были ответами ботов, и справа вы можете видеть, что очень немногие ответы действительно содержали хэштеги.

Давайте построим нашу первую модель, поскольку построение модели может дать нам основу для начала и улучшения, а также поможет нам лучше понять наши данные. Во-первых, нам нужно разделить наши данные на обучающий набор и набор проверки. Обучающий набор будет представлять собой часть наших данных (обычно 80%), на которых мы хотим, чтобы наша модель обучалась. Затем мы используем модели, которые мы обучили на обучающем наборе, чтобы предсказать результаты остальных 20% наших данных или проверочного набора. Причина, по которой мы это делаем, заключается в том, что мы хотим убедиться, что мы не переоснащаемся. Не вдаваясь в подробности, переобучение происходит, когда модель хорошо прогнозирует данные обучения, но когда вы показываете ей данные, которых она раньше не видела (набор проверки), модель прогнозирует плохо. По сути, модель начинает «запоминать» данные, которые она видит в обучающем наборе, вместо того, чтобы фактически учиться на них. Чтобы этого не произошло, мы выделили набор проверочных данных, которые он не видел раньше, и в зависимости от того, как он работает по сравнению с обучающими данными, вы можете получить представление о том, является ли он переоснащенным или нет. Короче говоря, если модель хорошо работает на обучающем наборе, но плохо на проверочном наборе, вы переобучаетесь.

Это похоже на то, как если бы перед вами был тест с несколькими вариантами ответов, и перед тестом у вас был ключ ответа (a, b, c, d) для каждого вопроса, и вы их запомнили. На самом деле вы не знаете, почему это ответ, вы просто запомнили его, что дало вам хороший результат. Но на практике вы на самом деле не будете знать, что делаете.

Итак, мы собираемся взять наш фрейм данных и отделить наши независимые переменные (x), которые представляют собой наши 12 столбцов, от зависимой переменной (y), которая определяет, бот это или нет. Затем мы собираемся разделить наши данные обучения и проверки.

X = df.drop(columns=['is_bot?'])

y = df['is_bot?']

X_train,X_val,y_train,y_val = train_test_split(X,y,test_size=.2, random_state=42,shuffle=True)

С этого момента данные будут разделены на «X_train», «y_train» → которые являются независимыми переменными обучающих данных вместе с их метками, и «X_val», «y_val» → которые являются независимыми переменными данных проверки. и соответствующие им метки.

Деревья решений:

Первая модель, которую мы собирались построить, — это дерево решений. Я думаю, что это хорошее место для начала, потому что это модель, с которой каждый знаком и, вероятно, использовал ее в какой-то момент своей жизни. Кроме того, они отлично подходят для визуализации и требуют минимальной предварительной обработки данных, что необходимо будет сделать для более поздних моделей. Ниже мы создадим нашу модель, подгоним ее к нашим обучающим данным, а затем создадим визуальное дерево решений, которое мы сможем изучить:

tree_model = DecisionTreeClassifier(max_leaf_nodes=10)
tree_model.fit(X_train,y_train)

import graphviz

def draw_tree(t, df, size=10, ratio=0.6, precision=2, **kwargs):
    s=export_graphviz(t, out_file=None, feature_names=df.columns, filled=True, rounded=True,
                      special_characters=True, rotate=False, precision=precision, **kwargs)
    return graphviz.Source(re.sub('Tree {', f'Tree {{ size={size}; ratio={ratio}', s))

draw_tree(model,X_train,size=10)

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

# Function to evaluate: accuracy, precision, recall, f1-score
from sklearn.metrics import accuracy_score, precision_recall_fscore_support

def calculate_results(y_true, y_pred):
  # Calculate model accuracy
  model_accuracy = accuracy_score(y_true, y_pred) * 100
  # Calculate model precision, recall and f1 score using "weighted" average
  model_precision, model_recall, model_f1, _ = precision_recall_fscore_support(y_true, y_pred, average="weighted")
  model_results = {"accuracy": model_accuracy,
                  "precision": model_precision,
                  "recall": model_recall,
                  "f1": model_f1}
  return model_results

# Train results
y_pred = tree_model.predict(X_train)
tree_clf_score = calculate_results(y_train,y_pred)
tree_clf_score

>{'accuracy': 98.53658536585365,
 'precision': 0.9856225930680359,
 'recall': 0.9853658536585366,
 'f1': 0.985121434412649}

# Validation results
y_pred = tree_model.predict(X_val)
tree_clf_score = calculate_results(y_val,y_pred)
tree_clf_score

> {'accuracy': 96.15384615384616,
 'precision': 0.967948717948718,
 'recall': 0.9615384615384616,
 'f1': 0.9628176701347433}

По сути, все, что мы делаем, — это создаем прогнозы (боты или нет) в нашем наборе проверки, передавая независимые переменные нашего набора проверки в нашу подобранную/обученную древовидную модель. Модель возьмет эти независимые переменные и передаст их в дерево решений выше. Отсюда мы можем рассчитать результаты, сравнивая наши прогнозы (y_pred) с фактическим результатом (y_val). Как видите, точность данных обучения составляет 98,53%, а точность данных проверки — 96,15%, что указывает на то, что мы, возможно, немного переоснащаемся.

Случайные леса:

Случайные леса строятся на основе деревьев решений. Они используют ансамблевый метод, называемый baging. Ансамбль — это просто красивое слово, обозначающее объединение прогнозов нескольких моделей. Бэггинг — это форма ансамбля, которая случайным образом выбирает подмножество строк ваших данных для создания дерева решений. Затем вы обучаете модель на этом подмножестве, повторяете это несколько раз, а затем делаете прогнозы, усредняя каждый из прогнозов модели.

Хотя подмножество данных будет содержать больше ошибок (меньше данных для обучения), ошибки не будут коррелировать друг с другом, и разные модели будут допускать разные ошибки. Таким образом, среднее значение ошибок всех моделей равно 0. Итак, если мы возьмем среднее значение всех прогнозов моделей, идея состоит в том, что мы будем приближаться все ближе и ближе к ответу. Другое дополнение, которое делают случайные леса, заключается в том, что они также используют случайное подмножество столбцов при создании деревьев решений. Таким образом, случайный лес будет представлять собой набор деревьев решений, каждое дерево решений будет соответствовать случайному подмножеству строк из данных, а также случайному подмножеству столбцов из данных. Затем прогноз каждого дерева будет усреднен для создания единого прогноза.

rf_model = RandomForestClassifier(100, min_samples_leaf=5)
rf_model.fit(X_train,y_train)

y_pred = rf_model.predict(X_train)
rf_clf_score = calculate_results(y_train,y_pred)
rf_clf_score

#Train score
>{'accuracy': 95.1219512195122,
 'precision': 0.9506769038769363,
 'recall': 0.9512195121951219,
 'f1': 0.9494953308666214}

y_pred = rf_model.predict(X_val)
rf_clf_score = calculate_results(y_val,y_pred)
rf_clf_score

#Val Score
>{'accuracy': 94.23076923076923,
 'precision': 0.9410653945537667,
 'recall': 0.9423076923076923,
 'f1': 0.9411288402000475}

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

pd.DataFrame(dict(cols=X_train.columns, imp=rf_model.feature_importances_)).plot('cols', 'imp', 'barh')

Как видите, количество пользователей, упомянутых в ответе, является, безусловно, наиболее важным решающим фактором. Это складывается, поскольку именно здесь наше предыдущее дерево решений решило сделать свое первое разделение. Я попробовал удалить некоторые менее используемые столбцы, чтобы посмотреть, будет ли это иметь значение, но похоже, что это только дает нам худшие результаты, поэтому я не буду включать это сюда. У меня есть тестовый набор, с которым мы сравним все модели в конце блога, а затем посмотрим, как все эти модели на самом деле работают. Тестовый набор — это еще один набор данных, который вы храните как бы «спрятано» и никогда не должны трогать. Это последний набор данных, на котором вы тестируете свои модели, поскольку модели никогда не видели данных и дадут вам лучшее представление о том, как работают ваши модели.

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

XGBoost:

Бустинг — это еще один ансамблевый метод, похожий на объединение, но вместо усреднения моделей мы их добавляем. Для этого вы обучаете небольшую модель, которая не соответствует вашему набору данных, рассчитываете ее прогнозы и вычитаете прогнозы из целевых значений, в результате чего у вас остаются остатки. Затем вы возвращаетесь к первому шагу, но вместо используя исходные цели (0,1), вы используете остатки в качестве целей для обучения. Каждая новая модель пытается учесть ошибку всех предыдущих моделей вместе взятых. Недостаток использования повышения заключается в том, что вы определенно можете переопределить свои данные. XGBoost на самом деле представляет собой просто случайный лес, в котором для ансамблирования используется техника повышения вместо техники объединения.

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

clf = xgb.XGBClassifier(objective='binary:logistic',nthread=4)

parameters = {
    'max_depth': range (2, 15, 1),
    'n_estimators': range(60, 220, 10),
    'learning_rate': [0.1, 0.01, 0.05]
}

grid_search = GridSearchCV(
    estimator=clf,
    param_grid=parameters,
    scoring = 'roc_auc',
    cv = 5,
    verbose=True
)
grid_search.fit(X_train,y_train)

grid_search.best_params_

> {'learning_rate': 0.05, 'max_depth': 2, 'n_estimators': 140}

xgb_model = xgb.XGBClassifier(learning_rate=.05, max_depth=2, n_estimators=140)

xgb_model.fit(X_train,y_train)

# Training Score
y_pred = xgb_model.predict(X_train)
xgb_clf_score = calculate_results(y_train,y_pred)
gbc_clf_score

>{'accuracy': 96.15384615384616,
 'precision': 0.9615384615384616,
 'recall': 0.9615384615384616,
 'f1': 0.9615384615384616}

#Validation Score
y_pred = xgb_model.predict(X_val)
xgb_clf_score = calculate_results(y_val,y_pred)
xgb_clf_score

>{'accuracy': 94.23076923076923,
 'precision': 0.9410653945537667,
 'recall': 0.9423076923076923,
 'f1': 0.9411288402000475}


Похоже, что все модели машинного обучения приходят к одному и тому же выводу: точность наших моделей составляет 94–96%. Теперь давайте попробуем использовать нейронную сеть.

Нейронная сеть:

Давайте получим общее представление о том, как можно использовать нейронную сеть с табличными данными. Если вы вспомните мою статью о том, как работают нейронные сети, вы начинаете со случайного присвоения коэффициента/веса каждой переменной.

Например, давайте посмотрим на первую строку выше. Здесь я включил только 9 признаков фрейма данных, поэтому мы инициализируем первую строку случайным весом, связанным с каждым из 9 признаков. Отсюда мы можем умножить эти веса на число в каждом признаке (случайный вес * 462 для подписчика, случайный вес * 73 для подписчиков и т. д.), а затем суммировать эти значения для строки, чтобы получить наш прогноз. Если сумма >0,5, то мы бы предсказали, что это бот, а если 0,5, то это не бот. Поначалу веса будут иметь ужасную предсказуемость, потому что они случайны. Используя функцию потерь, такую ​​как средняя абсолютная ошибка (MAE), мы можем рассчитать и суммировать потери для каждой строки, а затем использовать градиентный спуск для корректировки весов для каждой строки, пока не получим все лучшие и лучшие результаты.

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

Столбцы «подписчики», «tweet_count» и «account_age_days» имеют большие числа, особенно по сравнению со столбцами, обозначенными в двоичном формате, например «verified» или «default_prof_image». Когда мы умножаем наши случайные коэффициенты/веса на эти числа, гораздо большие числа будут иметь гораздо больший вклад в потери и, следовательно, иметь гораздо большие градиенты. Итак, когда мы корректируем веса по градиенту * скорости обучения, мы будем массово корректировать столбцы с большими числами и практически не вносить корректировки в столбцы с меньшими числами. Это приведет к тому, что нейронная сеть будет делать прогнозы только на основе столбцов с большими непрерывными числами. По этой причине нейронные сети или любая линейная модель должны нормализовать свои числовые данные, если это непрерывная переменная, такая как деньги, возраст и т. д. По этой причине нейронные сети работают значительно лучше, когда числа нормализованы между 0 и 1.

Мы начнем с нашего исходного фрейма данных, чтобы показать, насколько легко очищать и обрабатывать табличные данные с помощью Fastai:

Обратите внимание, что проверенное изображение профиля и изображение профиля по умолчанию по-прежнему имеют в качестве вывода true и false, а хэштег имеет нет/да. Помните, что вам нужно случайным образом инициализировать веса/коэффициенты для каждого столбца, и вы не можете умножить число на ложь/истину или да/нет. Итак, у нас есть несколько категориальных столбцов, которые необходимо обработать, а числовые столбцы необходимо нормализовать.

df = pd.read_csv('/Users/Justin/Desktop/twitter_replies_data.csv')
df = df.drop(columns=["Unnamed: 0",'screen_name','clean_text'])
df = df.rename(columns={"has_#":'hashtag'})
df.head()

splits = (list(train_idx), list(valid_idx))

dls = TabularPandas(
    df, splits=splits,
    procs = [Categorify, FillMissing, Normalize],
    cat_names = ['verified','default_prof_img','hashtag'],
    cont_names = ['followers','following','tweet_count','user_mentions','linked_urls','time_to_respond_minutes','account_age_days','follwers_to_following_ratio','avg_tweets_per_day'],
    y_names = 'is_bot?', y_block = CategoryBlock()
).dataloaders()

learn = tabular_learner(dls, metrics=accuracy, layers = [200,100])
learn.lr_find(suggest_funcs=(slide,valley))

Выше мы просто сообщаем Fastai, какие столбцы являются категориальными, какие непрерывными, а затем рассказываем, как обращаться с ними с помощью категоризации и нормализации.

У Fastai также есть удобная функция, предлагающая вам хорошую скорость обучения, которую я использовал выше. Результат был такой:

Это также может помочь визуально понять скорость обучения. Когда скорость обучения слишком низкая, функция остается плоской, и по мере достижения хорошей скорости обучения она начинает резко снижаться, пока скорость не станет слишком высокой, и функция резко пойдет вверх, поскольку теперь вы настраиваете параметры. слишком сильно.

# Train for 10 epochs
learn.fit_one_cycle(10, .0015)

epoch train_loss valid_loss  accuracy time
0   0.403281 0.515440        0.921569 00:00
1   0.404533 0.474083        0.921569 00:00
2   0.394372 0.437192        0.960784 00:00
3   0.376412 0.396539        0.960784 00:00
4   0.363176 0.370860        0.960784 00:00

Через 10 эпох мы получили нейронную сеть с точностью около 96%. Мы также можем использовать эти нейронные сети для создания ансамбля нейронных сетей. Ниже мы создадим ансамбль из 10 различных нейронных сетей:

# Set splitter
splits = (list(train_idx), list(valid_idx))

# Create dataloader
dls = TabularPandas(
    df, splits=splits,
    procs = [Categorify, FillMissing, Normalize],
    cat_names = ['verified','default_prof_img','hashtag'],
    cont_names = ['followers','following','tweet_count','user_mentions','linked_urls','time_to_respond_minutes','account_age_days','follwers_to_following_ratio','avg_tweets_per_day'],
    y_names = 'is_bot?', y_block = CategoryBlock()
).dataloaders()

# Train ten models
models = []
for i in range(10):
    learn = tabular_learner(dls, layers=[200,120], metrics=accuracy)
    lr =learn.lr_find(suggest_funcs=(slide,valley))
    lr = lr[0]
    learn.fit_one_cycle(10,lr)  
    models.append(learn)

# Get predictions on validation set
def ensemble_predictions(models, dl):
    preds = []
    for model in models:
        pred, _ = model.get_preds(dl=dl)
        preds.append(pred)
    
    # Average the predictions
    ensemble_pred = torch.stack(preds).mean(dim=0)
    
    return ensemble_pred

test_dl = dls.test_dl(df)

preds = ensemble_predictions(models, test_dl)
preds = np.argmax(preds.numpy(), axis=1)

# Calculate the accuracy of the ensemble
ensemble_accuracy = accuracy_score(df['is_bot?'], preds)
print(f'Ensemble accuracy: {ensemble_accuracy}')

> Ensemble accuracy: 0.9494163424124513

Причина, по которой это работает, заключается в том, что каждая нейронная сеть начинается с совершенно разных рандомизированных весов. Поскольку веса умножаются на каждый столбец, градиенты для каждой нейронной сети будут разными, поэтому, поскольку они корректируются в зависимости от скорости обучения, каждая нейронная сеть будет иметь разные результаты. Затем мы можем взять среднее значение прогнозов по всем десяти нейронным сетям и использовать его в качестве наших прогнозов. Здесь мы используем весь набор данных обучения/проверки, чтобы увидеть нашу точность, и получаем 95%.

Оценка наших моделей на нашем тестовом наборе:

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

Для этого мы собираемся взять наш тестовый набор, представляющий собой набор собранных мной данных, которые вообще не видела ни одна из моделей. Мы можем взять наши обученные/подогнанные модели и передать этот тестовый набор данных, чтобы получить представление о том, как они будут работать на самом деле.

Машинное обучение:

# Read in test df
df = test_df = pd.read_csv('./Desktop/test_data.csv')
df = df.drop(columns=["Unnamed: 0",'screen_name','clean_text'])

# adjust categorical data
df['verified'] = pd.Categorical(df.verified)
df['default_prof_img'] = pd.Categorical(df.default_prof_img)

df[cats] = df[cats].apply(lambda X: X.cat.codes)

df = df.rename(columns={"has_hashtag":'hashtag'})
df = pd.get_dummies(df, columns=['hashtag'])

# Create X and y
df_x = df.drop(columns=['is_bot?'])
df_y = df['is_bot?']

# Decision Tree Preds
y_pred_tree = tree_model.predict(df_x)
tree_clf_score = calculate_results(df_y,y_pred_tree)
tree_clf_score

>{'accuracy': 80.35714285714286,
 'precision': 0.8446573751451799,
 'recall': 0.8035714285714286,
 'f1': 0.8175697865353039}

# Random Forest Preds
y_pred_rf = rf_model.predict(df_x)
rf_clf_score = calculate_results(df_y,y_pred_rf)
rf_clf_score

>{'accuracy': 94.64285714285714,
 'precision': 0.9492784992784992,
 'recall': 0.9464285714285714,
 'f1': 0.9474097331240188}

# XGBoost Preds
y_pred_xgb = xgb_model.predict(df_x)
xgb_clf_score = calculate_results(df_y,y_pred_xgb)
xgb_clf_score

>{'accuracy': 89.28571428571429,
 'precision': 0.8928571428571429,
 'recall': 0.8928571428571429,
 'f1': 0.8928571428571429}

Из трех моделей машинного обучения, которые мы использовали, классификатор случайного леса дал наилучшие результаты. Вы можете видеть, что дерево решений и модель XGBoost показали худшие результаты по сравнению с тем, как они работали с наборами данных для обучения и проверки. Это говорит нам о том, что эти две модели были переоснащены, особенно модель дерева решений. Модель случайного леса действительно демонстрирует свою мощь: она невероятно проста в реализации, требует очень незначительной очистки/нормализации данных, не подгоняется под наши данные и обеспечивает наилучшие результаты.

Нейронные сети:

# Load test df
test_df = pd.read_csv('./Desktop/test_data.csv')
test_df = test_df.drop(columns=['Unnamed: 0','screen_name','clean_text'])
test_df = test_df.rename(columns={'has_hashtag':'hashtag'})
test_df = test_df.drop(columns=['is_bot?'])
test_df.head()

y = pd.read_csv('./Desktop/test_data.csv')
y = y['is_bot?']

# apply same changes to test dl
test_dl = dls.test_dl(test_df)

# neural net preds
nn_preds, _ = learn.get_preds(dl=test_dl)

nn_score = calculate_results(y,nn_preds.argmax(1))
nn_score

>{'accuracy': 96.42857142857143,
 'precision': 0.9642857142857143,
 'recall': 0.9642857142857143,
 'f1': 0.9642857142857143}

# test dl for ensemble 
test_dl = dls2.test_dl(test_df)

# ensemble preds
preds = ensemble_predictions(models, test_dl)
preds = preds.argmax(dim=1)

nn_ens_score = calculate_results(y,preds)
nn_ens_score

>{'accuracy': 98.21428571428571,
 'precision': 0.9825227963525835,
 'recall': 0.9821428571428571,
 'f1': 0.9817689384752203}

Обе наши модели нейронных сетей немного превзошли модель случайного леса, при этом ансамбль сетей показал лучшие результаты из всех наших моделей. Здесь мы можем визуально увидеть, как работает каждая модель:

Последние мысли:

В целом, учитывая небольшой объем использованных данных, я доволен результатами. С помощью этого проекта вы сможете получить представление о том, насколько мощными могут быть некоторые из этих моделей. Нейронные сети показали себя лучше всего на тестовых данных, но они лишь немного превзошли случайный лес, который гораздо проще реализовать и требует меньше очистки данных и размышлений. Это подчеркивает, почему случайные леса являются одной из наиболее часто используемых моделей в машинном обучении. Когда дело доходит до обнаружения спама в Твиттере, лучшей моделью, вероятно, будет смесь табличной модели, как здесь, и модели НЛП, которую я создал в своем первом сообщении в блоге. Использование прогнозов сильной табличной модели и их усреднение с прогнозами модели классификации текста, вероятно, даст лучшие и наиболее последовательные результаты.