Открытый курс по машинному обучению

Автор материала: Ольга Дайховская (@aiho в Slack ODS)

Материал распространяется на условиях лицензии Creative Commons CC BY-NC-SA 4.0. Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала

Домашнее задание № 4 (демо).

Прогнозирование популярности статей на TechMedia (Хабр) с помощью линейных моделей

В задании Вам предлагается разобраться с тем, как работает TfidfVectorizer и DictVectorizer, затем обучить и настроить модель линейной регрессии Ridge на данных о публикациях на Хабрахабре. Пройдя все шаги, вы сможете получить бейзлайн для соревнования (несмотря на old в названии, для этого задания соревнование актуально). Ответьте на все вопросы в этой тетрадке и заполните ответы в гугл-форме.

Описание соревнования

Предскажите, как много звездочек наберет статья, зная только ее текст и время публикации

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

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

Приступим: импортируем необходимые библиотеки и скачаем данные

In [12]:
import numpy as np
import pandas as pd

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error

import scipy

%matplotlib inline
from matplotlib import pyplot as plt

Скачайте данные соревнования.

In [13]:
train_df = pd.read_csv('../../data/howpop_train.csv')
test_df  = pd.read_csv('../../data/howpop_test.csv')
In [14]:
train_df.head(1).T
Out[14]:
0
url https://habrahabr.ru/post/18284/
domain habrahabr.ru
post_id 18284
published 2008-01-01 18:19:00
author @Tapac
flow develop
polling False
content_len 4305
title Новогодний подарок блоггерам — WordPress 2.3.2
comments 0
favs 0
views 236
votes_plus 0
votes_minus 0
views_lognorm -0.792687
favs_lognorm -1.34407
comments_lognorm -2.43687
In [15]:
train_df.shape, test_df.shape
Out[15]:
((134137, 17), (3990, 9))

Убедимся, что данные отсортированы по признаку published

In [16]:
train_df['published'].apply(lambda ts: pd.to_datetime(ts).value).plot();

Чтобы ответить на вопросы 1 и 2, можно использовать pandas.DataFrame.corr(), pandas.to_datetime() и pandas.Series.value_counts()

Вопрос 1. Есть ли в train_df признаки, корреляция между которыми больше 0.9? Обратите внимание, именно различные признаки - корреляция признака с самим собой естественно больше 0.9 :)

  • да
  • нет
  • не знаю
In [17]:
# ваш код здесь

Вопрос 2. В каком году было больше всего публикаций? (Рассматриваем train_df)

  • 2014
  • 2015
  • 2016
  • 2017
In [18]:
# ваш код здесь

Разбиение на train/valid

Используем только признаки 'author', 'flow', 'domain' и 'title'

In [19]:
features = ['author', 'flow', 'domain','title']
train_size = int(0.7 * train_df.shape[0])
In [20]:
len(train_df), train_size
Out[20]:
(134137, 93895)
In [21]:
X, y = train_df.loc[:, features],  train_df['favs_lognorm'] #отделяем признаки от целевой переменной

X_test = test_df.loc[:, features]
In [22]:
X_train, X_valid = X.iloc[:train_size, :], X.iloc[train_size:,:]

y_train, y_valid = y.iloc[:train_size], y.iloc[train_size:]

Применение TfidfVectorizer

TF-IDF (от англ. TF — term frequency, IDF — inverse document frequency) — статистическая мера, используемая для оценки важности слова в контексте документа, являющегося частью коллекции документов или корпуса. Вес некоторого слова пропорционален количеству употребления этого слова в документе, и обратно пропорционален частоте употребления слова в других документах коллекции. Подробнее в источнике

TfidfVectorizer преобразует тексты в матрицу TF-IDF признаков.

Основные параметры TfidfVectorizer в sklearn:

  • min_df - при построении словаря слова, которые встречаются реже, чем указанное значение, игнорируются
  • max_df - при построении словаря слова, которые встречаются чаще, чем указанное значение, игнорируются
  • analyzer - определяет, строятся ли признаки по словам или по символам (буквам)
  • ngram_range - определяет, формируются ли признаки только из отдельных слов или из нескольких слов (в случае с analyzer='char' задает количество символов). Например, если указать analyzer='word' и ngram_range=(1,3),то признаки будут формироваться из отдельных слов, из пар слов и из троек слов.
  • stop_words - слова, которые игнорируются при построении матрицы

Более подробно с параметрами можно ознакомиться в документации

Инициализируйте TfidfVectorizer с параметрами min_df=3, max_df=0.3 и ngram_range=(1, 3).
Примените метод fit_transform к X_train['title'] и метод transform к X_valid['title'] и X_test['title']

Вопрос 3. Какой размер у полученного словаря?

  • 43789
  • 50624
  • 93895
  • 74378
In [ ]:
vectorizer_title = # ваш код здесь

X_train_title = # и здесь
X_valid_title = # и тут тоже
X_test_title = # и тут
In [ ]:
# Можно посмотреть словарь в виде {'термин': индекс признака,...}
vectorizer_title.vocabulary_
In [ ]:
# ваш код здесь

Вопрос 4. Какой индекс у слова 'python'?

  • 1
  • 10
  • 9065
  • 15679
In [ ]:
# ваш код здесь

Инициализируйте TfidfVectorizer, указав analyzer='char'.
Примените метод fit_transform к X_train['title'] и метод transform к X_valid['title'] и X_test['title']

Вопрос 5. Какой размер у полученного словаря?

  • 218
  • 510
  • 125
  • 981
In [ ]:
vectorizer_title_ch = # ваш код здесь

X_train_title_ch = #...
X_valid_title_ch = #...
X_test_title_ch = #...
In [ ]:
# Здесь так же можно посмотреть словарь
# Заметьте насколько отличаются словари для TfidfVectorizer с analyzer='word' и analyzer='char'
vectorizer_title_ch.vocabulary_
In [ ]:
# ваш код здесь

Работа с категориальными признаками

Для обработки категориальных признаков 'author', 'flow', 'domain' мы будем использовать DictVectorizer из sklearn.

In [ ]:
feats = ['author', 'flow', 'domain']
X_train[feats][:5]

Рассмотрим как он работает на примере первых пяти строк

In [ ]:
# сначала заполняем пропуски прочерком
X_train[feats][:5].fillna('-')
In [ ]:
# Преобразуем датафрейм в словарь, где ключами являются индексы объектов (именно для этого мы транспонировали датафрейм),
# а значениями являются словари в виде 'название_колонки':'значение'
X_train[feats][:5].fillna('-').T.to_dict()
In [ ]:
# В DictVectorizer нам нужно будет передать список словарей для каждого объекта в виде 'название_колонки':'значение',
# поэтому используем .values()
X_train[feats][:5].fillna('-').T.to_dict().values()
In [ ]:
# В итоге получается разреженная матрица
dict_vect = DictVectorizer()
dict_vect_matrix = dict_vect.fit_transform(X_train[feats][:5].fillna('-').T.to_dict().values())
dict_vect_matrix
In [ ]:
# Но можно преобразовать ее в numpy array с помощью .toarray()
dict_vect_matrix.toarray()
In [ ]:
# В получившейся матрице 5 строк (по числу объектов) и 9 столбцов
# Далее разберемся почему колонок именно 9
dict_vect_matrix.shape

Посмотрим сколько уникальных значений в каждой колонке.
Суммарно их 9 - столько же, сколько и колонок. Это объясняется тем, что для категориальных признаков со строковыми значениями DictVectorizer делает кодирование бинарными признаками - каждому уникальному значению признака соответствует один новый бинарный признак, который равен 1 только в том случае, если в исходной матрице этот признак принимает значение, которому соответствует эта колонка новой матрицы.

In [ ]:
for col in feats:
    print(col,len(X_train[col][:5].fillna('-').unique()))

Также можно посмотреть что означает каждая колонка полученной матрицы

In [ ]:
# например, самая первая колонка называется '[email protected]' - то есть принимает значение 1 только если автор @DezmASter
dict_vect.feature_names_

Инициализируйте DictVectorizer с параметрами по умолчанию.
Примените метод fit_transform к X_train[feats] и метод transform к X_valid[feats] и X_test[feats]

In [ ]:
vectorizer_feats = #ваш код здесь

X_train_feats = #...
X_valid_feats = #...
X_test_feats = #...
In [ ]:
X_train_feats.shape

Соединим все полученные матрицы при помощи scipy.sparse.hstack()

In [ ]:
X_train_new = scipy.sparse.hstack([X_train_title, X_train_feats, X_train_title_ch])
X_valid_new = scipy.sparse.hstack([X_valid_title, X_valid_feats, X_valid_title_ch])
X_test_new =  scipy.sparse.hstack([X_test_title, X_test_feats, X_test_title_ch])

Обучение модели

Далее будем использовать Ridge, линейную модель с l2-регуляризацией. Документация

Основной параметр Ridge - alpha, коэффициент регуляризации. Регуляризация используется для улучшения обобщающей способности модели - прибавляя к функционалу потерь сумму квадратов весов, умноженную на коэффициент регуляризации (та самая alpha), мы штрафуем модель за слишком большие значения весов и не позволяем ей переобучаться. Чем больше этот коээфициент, тем сильнее эффект.

Обучите две модели на X_train_new, y_train, задав в первой alpha=0.1 и random_state = 1, а во второй alpha=1.0 и random_state = 1

Рассчитайте среднеквадратичную ошибку каждой модели (mean_squared_error). Сравните значения ошибки на обучающей и тестовой выборках и ответьте на вопросы.

Вопрос 6. Выберите верные утверждения:

  • обе модели показывают одинаковый результат (среднеквадратичная ошибка отличается не больше чем на тысячные), регуляризация ничего не меняет
  • при alpha=0.1 модель переобучается
  • среднеквадратичная ошибка первой модели на тесте меньше
  • при alpha=1.0 у модели обощающая способность лучше, чем у при alpha=0.1
In [ ]:
%%time
model1 = #ваш код здесь
#здесь тоже ваш код
In [ ]:
train_preds1 = model1.predict(X_train_new)
valid_preds1 = model1.predict(X_valid_new)

print('Ошибка на трейне',mean_squared_error(y_train, train_preds1))
print('Ошибка на тесте',mean_squared_error(y_valid, valid_preds1))
In [ ]:
%%time
model2 = #ваш код здесь
#здесь тоже ваш код
In [ ]:
train_preds2 = model2.predict(X_train_new)
valid_preds2 = model2.predict(X_valid_new)

print('Ошибка на трейне',mean_squared_error(y_train, train_preds2))
print('Ошибка на тесте',mean_squared_error(y_valid, valid_preds2))

Baseline

Теперь попытаемся получить бейзлайн для соревования - используйте Ridge с параметрами по умолчанию и обучите модель на всех данных - соедините X_train_new X_valid_new (используйте scipy.sparse.vstack()), а целевой переменной будет y.

In [ ]:
%%time
model = # ваш код здесь

# обучите модель на всех данных

test_preds = model.predict(X_test_new)
In [ ]:
sample_submission = pd.read_csv('../../data/habr_sample_submission.csv', 
                                index_col='url')
In [ ]:
sample_submission.head()
In [ ]:
ridge_submission = sample_submission.copy()
ridge_submission['favs_lognorm'] = test_preds
# это будет бейзлайн "Простое решение"
ridge_submission.to_csv('ridge_baseline.csv')