Прогнозирование голосов за комментарии Reddit с помощью машинного обучения

В этой статье мы будем использовать Python и пакет scikit-learn, чтобы предсказать количество голосов за комментарий на Reddit. Мы подбираем различные регрессионные модели и сравниваем их эффективность, используя следующие показатели:

  • R² для измерения степени соответствия
  • средняя абсолютная ошибка (MAE) и среднеквадратичная ошибка (RMSE) на тестовом наборе для измерения точности.

Эта статья основана на работе из этого репозитория Github. Код можно найти в этой записной книжке.

Фон

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

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

Наша цель - предсказать количество голосов, которые получат комментарии.

Данные

Данные, файл pickle, содержащий 1 205 039 строк (комментариев), произошедший в мае 2015 года, размещен на Google Диске и может быть загружен по этой ссылке.

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

Целевая переменная

  • оценка: количество голосов за комментарий.

Функции уровня комментариев

  • позолоченный: количество позолоченных тегов (премиум лайков) на комментарии.
  • отличительный: тип пользователя на странице. Либо "модератор", "администратор", либо "пользователь".
  • противоречивость: логическое значение, указывающее, является ли (1) или нет (0) комментарий спорным (популярные комментарии, количество голосов за которые приближается к тому же количеству голосов против).
  • over_18: была ли тема помечена как NSFW.
  • time_lapse: время в секундах между комментарием и первым комментарием в цепочке.
  • hour_of_comment: комментарий часа дня был опубликован
  • рабочий день: был опубликован комментарий дня недели.
  • is_flair: есть ли текст для комментария (https://www.reddit.com/r/help/comments/3tbuml/whats_a_flair/)
  • is_flair_css: есть ли класс CSS для чутья комментариев.
  • глубина: глубина комментария в цепочке (количество родительских комментариев, которые есть в комментарии).
  • no_of_linked_sr: количество субреддитов, упомянутых в комментарии
  • no_of_linked_urls: количество URL-адресов, связанных в комментарии.
  • субъективность: количество экземпляров «я»
  • is_edited: независимо от того, редактировался ли комментарий.
  • is_quoted: цитирует ли комментарий другой
  • no_quoted: количество цитат в комментарии.
  • senti_neg: оценка негативных настроений
  • senti_neu: оценка нейтрального настроения
  • senti_pos: оценка положительного настроения
  • senti_comp: сложная оценка тональности
  • word_count: количество слов в комментарии.

Особенности родительского уровня

  • time_since_parent: время в секундах между комментарием и родительским комментарием.
  • parent_score: оценка родительского комментария (NaN, если у комментария нет родителя)
  • parent_cos_angle: косинусное сходство между комментарием и вложениями его родительского комментария (https://nlp.stanford.edu/projects/glove/)

Функции корня дерева комментариев

  • is_root: является ли комментарий корневым
  • time_since_comment_tree_root: время в секундах между комментарием и корнем дерева комментариев.
  • comment_tree_root_score: оценка корня дерева комментариев

Функции уровня потока

  • link_score: есть голоса за комментарий обсуждения.
  • upvote_ratio: процент положительных голосов от всех голосов за комментарий цепочки установлен
  • link_ups: количество голосов за тему
  • time_since_link: время в секундах с момента создания цепочки.
  • no_past_comments: количество комментариев к цепочке до публикации комментария
  • score_till_now: оценка ветки на момент публикации этого комментария
  • title_cos_angle: косинусное сходство между комментарием и вложениями заголовка его цепочки.
  • is_selftext: был ли у потока самотекст.

Настраивать

Загрузим все необходимые библиотеки.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelBinarizer
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import LassoCV
from sklearn.linear_model import RidgeCV
from sklearn.linear_model import ElasticNetCV
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
import warnings
warnings.filterwarnings('ignore')

Мы также определяем некоторые функции для взаимодействия с моделями.

def model_diagnostics(model, pr=True):
    """
    Returns and prints the R-squared, RMSE and the MAE for a trained model
    """
    y_predicted = model.predict(X_test)
    r2 = r2_score(y_test, y_predicted)
    mse = mean_squared_error(y_test, y_predicted)
    mae = mean_absolute_error(y_test, y_predicted)
    if pr:
        print(f"R-Sq: {r2:.4}")
        print(f"RMSE: {np.sqrt(mse)}")
        print(f"MAE: {mae}")
    
    return [r2,np.sqrt(mse),mae]
def plot_residuals(y_test, y_predicted):
    """"
    Plots the distribution for actual and predicted values of the target variable. Also plots the distribution for the residuals
    """
    fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharey=True)
    sns.distplot(y_test, ax=ax0, kde = False)
    ax0.set(xlabel='Test scores')
    sns.distplot(y_predicted, ax=ax1, kde = False)
    ax1.set(xlabel="Predicted scores")
    plt.show()
    fig, ax2 = plt.subplots()
    sns.distplot((y_test-y_predicted), ax = ax2,kde = False)
    ax2.set(xlabel="Residuals")
    plt.show()
def y_test_vs_y_predicted(y_test,y_predicted):
    """
    Produces a scatter plot for the actual and predicted values of the target variable
    """
    fig, ax = plt.subplots()
    ax.scatter(y_test, y_predicted)
    ax.set_xlabel("Test Scores")
    ax.set_ylim([-75, 1400])
    ax.set_ylabel("Predicted Scores")
    plt.show()
def get_feature_importance(model):
    """
    For fitted tree based models, get_feature_importance can be used to get the feature importance as a tidy output
    """
    X_non_text = pd.get_dummies(df[cat_cols])
    features = numeric_cols + bool_cols + list(X_non_text.columns)
    feature_importance = dict(zip(features, model.feature_importances_))
    for name, importance in sorted(feature_importance.items(), key=lambda x: x[1], reverse=True):
        print(f"{name:<30}: {importance:>6.2%}")
        print(f"\nTotal importance: {sum(feature_importance.values()):.2%}")
    return feature_importance

Считывание данных

df = pd.read_pickle('reddit_comments.pkl')

Обработка отсутствующих значений

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

  • parent_score: у некоторых комментариев не было родителя (условно)
  • comment_tree_root_score и time_since_comment_tree_root: некоторые комментарии были корнем дерева комментариев (условно)
  • parent_cosine, parent_euc, title_cosine, title_euc: в некоторых комментариях отсутствовали слова, в которые были встроены слова-перчатки (опущены). Кроме того, у некоторых комментариев не было родителя (вмененный parent_cosine, parent_title).
df = df[~df.title_cosine.isna()] # drop where parent/title_cosine is NaN
parent_scrore_impute = df.parent_score.mode()[0] # impute with mode of parent_score column
comment_tree_root_score_impute = df.comment_tree_root_score.mode()[0] # impute with mode of comment_tree_root_score column
time_since_comment_tree_root_impute = df.time_since_comment_tree_root.mode()[0] # impute with mode of time_since_comment_tree_root column
parent_cosine_impute = 0
parent_euc_impute = 0
df.loc[df.parent_score.isna(), 'parent_score'] = parent_scrore_impute
df.loc[df.comment_tree_root_score.isna(), 'comment_tree_root_score'] = comment_tree_root_score_impute
df.loc[df.time_since_comment_tree_root.isna(), 'time_since_comment_tree_root'] = time_since_comment_tree_root_impute
df.loc[df.parent_cosine.isna(), 'parent_cosine'] = parent_cosine_impute
df.loc[df.parent_euc.isna(), 'parent_euc'] = parent_euc_impute

Выберите переменные

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

bool_cols = ['over_18', 'is_edited', 'is_quoted', 'is_selftext']
cat_cols = ['subreddit', 'distinguished', 'is_flair', 'is_flair_css','hour_of_comment', 'weekday']

numeric_cols = ['gilded', 'controversiality', 'upvote_ratio','time_since_link',
                'depth', 'no_of_linked_sr', 'no_of_linked_urls', 'parent_score',
                'comment_tree_root_score', 'time_since_comment_tree_root',
                'subjectivity', 'senti_neg', 'senti_pos', 'senti_neu',
                'senti_comp', 'no_quoted', 'time_since_parent', 'word_counts',
                'no_of_past_comments', 'parent_cosine','parent_euc',
                'title_cosine', 'title_euc', 'no_quoted','link_score']

Используя наш список переменных, мы можем подготовить данные для моделирования. В приведенном ниже шаге используется LabelBinarizer scikit-learn, чтобы создать фиктивные переменные из категориальных столбцов, а затем объединить все переменные.

lb = LabelBinarizer()
cat = [lb.fit_transform(df[col]) for col in cat_cols]
bol = [df[col].astype('int') for col in bool_cols]
t = df.loc[:, numeric_cols].values
final = [t] + bol + cat
y = df.score.values
x = np.column_stack(tuple(final))

Мы разбиваем данные на обучающий и тестовый набор, используя разбиение 80–20.

X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=10)

Моделирование

В этом разделе мы используем scikit-learn, чтобы подогнать модели к данным Reddit. Мы начинаем с базовой модели, а затем пытаемся улучшить результаты с помощью лассо, гребня и эластичной сетевой регрессии. Кроме того, мы пробуем использовать метод K-ближайших соседей, дерево решений, случайный лес и регрессию с градиентным усилением.

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

model_performance_dict = dict()

Модели линейной регрессии

Базовая модель

Мы подбираем простую модель, чтобы установить базовый уровень. Эта модель всегда предсказывает среднее количество голосов за.

baseline = DummyRegressor(strategy='mean')
baseline.fit(X_train,y_train)
model_performance_dict["Baseline"] = model_diagnostics(baseline)

Линейная регрессия

linear = LinearRegression()
linear.fit(X_train,y_train)
model_performance_dict["Linear Regression"] = model_diagnostics(linear)

Регрессия лассо

lasso = LassoCV(cv=30).fit(X_train, y_train)
model_performance_dict["Lasso Regression"] = model_diagnostics(lasso)

Риджевая регрессия

ridge = RidgeCV(cv=10).fit(X_train, y_train)
model_performance_dict["Ridge Regression"] = model_diagnostics(ridge)

Эластичная чистая регрессия

elastic_net = ElasticNetCV(cv = 30).fit(X_train, y_train)
model_performance_dict["Elastic Net Regression"] = model_diagnostics(elastic_net)

Модели нелинейной регрессии

Регрессия K-ближайшего соседа

knr = KNeighborsRegressor()
knr.fit(X_train, y_train)
model_performance_dict["KNN Regression"] = model_diagnostics(knr)

Регрессия дерева решений

dt = DecisionTreeRegressor(min_samples_split=45, min_samples_leaf=45, random_state = 10)
dt.fit(X_train, y_train)
model_performance_dict["Decision Tree"] = model_diagnostics(dt)

Регрессия случайного леса

rf = RandomForestRegressor(n_jobs=-1, n_estimators=70, min_samples_leaf=10, random_state = 10)
rf.fit(X_train, y_train)
model_performance_dict["Random Forest"] = model_diagnostics(rf)

Регрессия с усилением градиента

gbr = GradientBoostingRegressor(n_estimators=70, max_depth=5)
gbr.fit(X_train, y_train)
model_performance_dict["Gradient Boosting Regression"] = model_diagnostics(gbr)

Сравнение моделей

Мы сравниваем модели на основе трех показателей: R², MAE и RMSE. Для этого мы определяем функцию ниже.

def model_comparison(model_performance_dict, sort_by = 'RMSE', metric = 'RMSE'):

    Rsq_list = []
    RMSE_list = []
    MAE_list = []
    for key in model_performance_dict.keys():
        Rsq_list.append(model_performance_dict[key][0])
        RMSE_list.append(model_performance_dict[key][1])
        MAE_list.append(model_performance_dict[key][2])

    props = pd.DataFrame([])

    props["R-squared"] = Rsq_list
    props["RMSE"] = RMSE_list
    props["MAE"] = MAE_list
    props.index = model_performance_dict.keys()
    props = props.sort_values(by = sort_by)

    fig, ax = plt.subplots(figsize = (12,6))

    ax.bar(props.index, props[metric], color="blue")
    plt.title(metric)
    plt.xlabel('Model')
    plt.xticks(rotation = 45)
    plt.ylabel(metric)

Давайте воспользуемся этой функцией для сравнения моделей на основе каждого показателя.

model_comparison(model_performance_dict, sort_by = 'R-squared', metric = 'R-squared')

model_comparison(model_performance_dict, sort_by = 'R-squared', metric = 'MAE')

model_comparison(model_performance_dict, sort_by = 'R-squared', metric = 'RMSE')

Интерпретация результатов

Модель случайного леса - разумный выбор, если учесть производительность и время обучения. Средняя абсолютная ошибка составляет примерно 9,7, что означает, что в среднем оценка модели отклоняется примерно на 9,7 голосов. Давайте посмотрим на несколько графиков для получения дополнительной информации о производительности модели.

y_predicted = rf.predict(X_test)
plot_residuals(y_test,y_predicted)

Сравнивая гистограммы результатов тестов и прогнозируемых результатов, мы замечаем, что модель имеет тенденцию переоценивать целевую переменную, когда она мала. Кроме того, модель никогда не предсказывает, что целевая переменная будет намного больше 2000. Похоже, что результаты искажены из-за нескольких случаев, когда целевая переменная велика. Большинство комментариев имеют лишь небольшое количество голосов, но модель ожидает, что они получат больше, чем они. Однако, когда у комментария слишком много голосов, модель его недооценивает.

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

Древовидные модели также позволяют нам количественно оценить важность используемых ими функций.

rf_importances = get_feature_importance(rf)

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

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

Также важно отметить, что многие из функций, которые имели большое значение, имели пропущенные значения. По этой причине более глубокий анализ того, как обрабатывались пропущенные значения, может привести к повышению производительности модели (например, когда мы отбросили корни дерева комментариев, родительская оценка была, безусловно, самой важной функцией, ~ 25%). Также стоит протестировать интерполяцию с использованием среднего, медианы или прогноза с использованием простой линейной регрессии.

Заключение

В этой статье мы описали рабочий процесс машинного обучения, который использует библиотеку python scikit-learn для прогнозирования голосов за комментарии Reddit. Мы сравнили производительность моделей линейной и нелинейной регрессии и обнаружили, что регрессор случайного леса был оптимальным выбором.

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

  • Подбираем модели с меньшим количеством функций и сравниваем их характеристики с оригиналами
  • Анализ недостающих значений и их влияние на производительность модели
  • Укладка моделей для повышения производительности

Эта статья основана на проекте, который изначально был завершен Адамом Ривзманом, Гокул Кришна Гурусвами, Хай Ле, Максимилианом Альфаро и Пракхаром Агравалом во время курса Введение в машинное обучение в Университете Сан-Франциско. Наука в области науки о данных. Соответствующие работы можно найти в этом репозитории Github, а код из этой статьи - в этом блокноте.

Буду рад получить отзывы по любому из вышеперечисленных. Со мной всегда можно связаться в LinkedIn или по электронной почте [email protected].