{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "##
[mlcourse.ai](https://mlcourse.ai) – открытый курс OpenDataScience по машинному обучению \n", "\n", " \n", "Автор материала: Ольга Дайховская (@aiho в Slack ODS). Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#
Домашнее задание № 4 (демо).\n", "##
Прогнозирование популярности статей на TechMedia (Хабр) с помощью линейных моделей\n", " \n", "**В задании Вам предлагается разобраться с тем, как работает TfidfVectorizer и DictVectorizer, затем обучить и настроить модель линейной регрессии Ridge на данных о публикациях на Хабрахабре. Пройдя все шаги, вы сможете получить бейзлайн для [соревнования](https://www.kaggle.com/c/howpop-habrahabr-favs-lognorm) (несмотря на old в названии, для этого задания соревнование актуально). \n", "Ответьте на все вопросы в этой тетрадке и заполните ответы в [гугл-форме](https://docs.google.com/forms/d/1gPt401drm84N2kdezwGWtPJN_JpaFqXoh6IwlWOslb4).**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Описание соревнования**\n", "\n", "Предскажите, как много звездочек наберет статья, зная только ее текст и время публикации\n", "\n", "Необходимо предсказать популярность поста на Хабре по содержанию и времени публикации. Как известно, пользователи Хабра могут добавлять статьи к себе в избранное. Общее количество пользователей, которое это сделали отображается у статьи количеством звездочек. Будем считать, что число звездочек, поставленных статье, наиболее хорошо отражает ее популярность.\n", "\n", "Более формально, в качестве метрики популярности статьи будем использовать долю статей за последний месяц, у которых количество звездочек меньше чем у текущей статьи. А точнее, доле числа звездочек можно поставить в соответствие квантили стандартного распределения, таким образом получаем числовую характеристику популярности статьи. Популярность статьи 0 означает, что статья получила ровно столько звездочек, сколько в среднем получают статьи. И соответственно чем больше звездочек получила статья по сравнению со средним, тем выше это число." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Приступим:** импортируем необходимые библиотеки и скачаем данные" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import scipy\n", "from sklearn.feature_extraction import DictVectorizer\n", "from sklearn.feature_extraction.text import TfidfVectorizer\n", "from sklearn.linear_model import Ridge\n", "from sklearn.metrics import mean_squared_error\n", "from sklearn.model_selection import train_test_split\n", "\n", "%matplotlib inline\n", "from matplotlib import pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Скачайте [данные](https://drive.google.com/file/d/1nV2qV9otN3LnVSDqy95hvpJdb6aWtATk/view?usp=sharing) соревнования (данные были удалены с Kaggle ради организации последующего идентичного соревнования, так что тут ссылка на Google Drive)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# при необходимости поменяйте путь к данным \n", "train_df = pd.read_csv('../../data/howpop_train.csv')\n", "test_df = pd.read_csv('../../data/howpop_test.csv')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_df.head(1).T" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_df.shape, test_df.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Убедимся, что данные отсортированы по признаку `published`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_df['published'].apply(lambda ts: pd.to_datetime(ts).value).plot();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Чтобы ответить на вопросы 1 и 2, можно использовать [pandas.DataFrame.corr()](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.corr.html), [pandas.to_datetime()](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.to_datetime.html) и [pandas.Series.value_counts()](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.value_counts.html)**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 1. Есть ли в train_df признаки, корреляция между которыми больше 0.9? Обратите внимание, именно различные признаки - корреляция признака с самим собой естественно больше 0.9 :)\n", "- да\n", "- нет\n", "- не знаю" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ваш код здесь" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 2. В каком году было больше всего публикаций? (Рассматриваем train_df)\n", "- 2014\n", "- 2015\n", "- 2016\n", "- 2017" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ваш код здесь" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Разбиение на train/valid\n", "Используем только признаки 'author', 'flow', 'domain' и 'title'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "features = ['author', 'flow', 'domain','title']\n", "train_size = int(0.7 * train_df.shape[0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "len(train_df), train_size" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X, y = train_df.loc[:, features], train_df['favs_lognorm'] #отделяем признаки от целевой переменной\n", "\n", "X_test = test_df.loc[:, features]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train, X_valid = X.iloc[:train_size, :], X.iloc[train_size:,:]\n", "\n", "y_train, y_valid = y.iloc[:train_size], y.iloc[train_size:]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Применение TfidfVectorizer\n", "\n", "**TF-IDF** (от англ. TF — term frequency, IDF — inverse document frequency) — статистическая мера, используемая для оценки важности слова в контексте документа, являющегося частью коллекции документов или корпуса. Вес некоторого слова пропорционален количеству употребления этого слова в документе, и обратно пропорционален частоте употребления слова в других документах коллекции. [Подробнее в источнике](https://ru.wikipedia.org/wiki/TF-IDF)\n", "\n", "TfidfVectorizer преобразует тексты в матрицу TF-IDF признаков.\n", "\n", "**Основные параметры TfidfVectorizer в sklearn:**\n", "- **min_df** - при построении словаря слова, которые встречаются *реже*, чем указанное значение, игнорируются\n", "- **max_df** - при построении словаря слова, которые встречаются *чаще*, чем указанное значение, игнорируются\n", "- **analyzer** - определяет, строятся ли признаки по словам или по символам (буквам)\n", "- **ngram_range** - определяет, формируются ли признаки только из отдельных слов или из нескольких слов (в случае с analyzer='char' задает количество символов). Например, если указать analyzer='word' и ngram_range=(1,3),то признаки будут формироваться из отдельных слов, из пар слов и из троек слов.\n", "- **stop_words** - слова, которые игнорируются при построении матрицы\n", "\n", "Более подробно с параметрами можно ознакомиться в [документации](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Инициализируйте TfidfVectorizer с параметрами min_df=3, max_df=0.3 и ngram_range=(1, 3).
\n", "Примените метод fit_transform к X_train['title'] и метод transform к X_valid['title'] и X_test['title']**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 3. Какой размер у полученного словаря?\n", "- 43789\n", "- 50624\n", "- 93895\n", "- 74378" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vectorizer_title = # ваш код здесь\n", "\n", "X_train_title = # и здесь\n", "X_valid_title = # и тут тоже\n", "X_test_title = # и тут" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Можно посмотреть словарь в виде {'термин': индекс признака,...}\n", "vectorizer_title.vocabulary_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ваш код здесь" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 4. Какой индекс у слова 'python'?\n", "- 1\n", "- 10\n", "- 9065\n", "- 15679" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ваш код здесь" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Инициализируйте TfidfVectorizer, указав analyzer='char'.
\n", "Примените метод fit_transform к X_train['title'] и метод transform к X_valid['title'] и X_test['title']**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 5. Какой размер у полученного словаря?\n", "- 218\n", "- 510\n", "- 125\n", "- 981" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vectorizer_title_ch = # ваш код здесь\n", "\n", "X_train_title_ch = #...\n", "X_valid_title_ch = #...\n", "X_test_title_ch = #..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Здесь так же можно посмотреть словарь\n", "# Заметьте насколько отличаются словари для TfidfVectorizer с analyzer='word' и analyzer='char'\n", "vectorizer_title_ch.vocabulary_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ваш код здесь" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Работа с категориальными признаками\n", "\n", "Для обработки категориальных признаков 'author', 'flow', 'domain' мы будем использовать DictVectorizer из sklearn." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "feats = ['author', 'flow', 'domain']\n", "X_train[feats][:5]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим как он работает на примере первых пяти строк" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# сначала заполняем пропуски прочерком\n", "X_train[feats][:5].fillna('-')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Преобразуем датафрейм в словарь, где ключами являются индексы объектов (именно для этого мы транспонировали датафрейм),\n", "# а значениями являются словари в виде 'название_колонки':'значение'\n", "X_train[feats][:5].fillna('-').T.to_dict()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# В DictVectorizer нам нужно будет передать список словарей для каждого объекта в виде 'название_колонки':'значение',\n", "# поэтому используем .values()\n", "X_train[feats][:5].fillna('-').T.to_dict().values()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# В итоге получается разреженная матрица\n", "dict_vect = DictVectorizer()\n", "dict_vect_matrix = dict_vect.fit_transform(X_train[feats][:5].fillna('-').T.to_dict().values())\n", "dict_vect_matrix" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Но можно преобразовать ее в numpy array с помощью .toarray()\n", "dict_vect_matrix.toarray()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# В получившейся матрице 5 строк (по числу объектов) и 9 столбцов\n", "# Далее разберемся почему колонок именно 9\n", "dict_vect_matrix.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим сколько уникальных значений в каждой колонке.
\n", "Суммарно их 9 - столько же, сколько и колонок. Это объясняется тем, что для категориальных признаков со строковыми значениями DictVectorizer делает кодирование бинарными признаками - каждому уникальному значению признака соответствует один новый бинарный признак, который равен 1 только в том случае, если в исходной матрице этот признак принимает значение, которому соответствует эта колонка новой матрицы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for col in feats:\n", " print(col,len(X_train[col][:5].fillna('-').unique()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Также можно посмотреть что означает каждая колонка полученной матрицы" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# например, самая первая колонка называется 'author=@DezmASter' - то есть принимает значение 1 только если автор @DezmASter\n", "dict_vect.feature_names_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Инициализируйте DictVectorizer с параметрами по умолчанию.
\n", "Примените метод fit_transform к X_train[feats] и метод transform к X_valid[feats] и X_test[feats]**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vectorizer_feats = #ваш код здесь\n", "\n", "X_train_feats = #...\n", "X_valid_feats = #...\n", "X_test_feats = #..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train_feats.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Соединим все полученные матрицы при помощи scipy.sparse.hstack()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train_new = scipy.sparse.hstack([X_train_title, X_train_feats, X_train_title_ch])\n", "X_valid_new = scipy.sparse.hstack([X_valid_title, X_valid_feats, X_valid_title_ch])\n", "X_test_new = scipy.sparse.hstack([X_test_title, X_test_feats, X_test_title_ch])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Обучение модели\n", "\n", "Далее будем использовать Ridge, линейную модель с l2-регуляризацией.\n", "[Документация](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html)\n", "\n", "Основной параметр Ridge - **alpha, коэффициент регуляризации**. Регуляризация используется для улучшения обобщающей способности модели - прибавляя к функционалу потерь сумму квадратов весов, умноженную на коэффициент регуляризации (та самая alpha), мы штрафуем модель за слишком большие значения весов и не позволяем ей переобучаться. Чем больше этот коээфициент, тем сильнее эффект." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Обучите две модели на X_train_new, y_train, задав в первой alpha=0.1 и random_state = 1, а во второй alpha=1.0 и random_state = 1**\n", "\n", "**Рассчитайте среднеквадратичную ошибку каждой модели (mean_squared_error). Сравните значения ошибки на обучающей и тестовой выборках и ответьте на вопросы.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 6. Выберите верные утверждения:\n", "- обе модели показывают одинаковый результат (среднеквадратичная ошибка отличается не больше чем на тысячные), регуляризация ничего не меняет\n", "- при alpha=0.1 модель переобучается\n", "- среднеквадратичная ошибка первой модели на тесте меньше\n", "- при alpha=1.0 у модели обобщающая способность лучше, чем у при alpha=0.1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "model1 = #ваш код здесь\n", "#здесь тоже ваш код" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_preds1 = model1.predict(X_train_new)\n", "valid_preds1 = model1.predict(X_valid_new)\n", "\n", "print('Ошибка на трейне',mean_squared_error(y_train, train_preds1))\n", "print('Ошибка на тесте',mean_squared_error(y_valid, valid_preds1))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "model2 = #ваш код здесь\n", "#здесь тоже ваш код" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_preds2 = model2.predict(X_train_new)\n", "valid_preds2 = model2.predict(X_valid_new)\n", "\n", "print('Ошибка на трейне',mean_squared_error(y_train, train_preds2))\n", "print('Ошибка на тесте',mean_squared_error(y_valid, valid_preds2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Baseline\n", "\n", "**Теперь попытаемся получить бейзлайн для соревования - используйте Ridge с параметрами по умолчанию и обучите модель на всех данных - соедините X_train_new X_valid_new (используйте scipy.sparse.vstack()), а целевой переменной будет y.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "model = # ваш код здесь\n", "\n", "# обучите модель на всех данных\n", "\n", "test_preds = model.predict(X_test_new)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_submission = pd.read_csv('../../data/habr_sample_submission.csv', \n", " index_col='url')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_submission.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ridge_submission = sample_submission.copy()\n", "ridge_submission['favs_lognorm'] = test_preds\n", "# это будет бейзлайн \"Простое решение\"\n", "ridge_submission.to_csv('ridge_baseline.csv') " ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 1 }