2758 lines
98 KiB
Plaintext
2758 lines
98 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"<!-- dom:TITLE: Машинное обучиение с использованием библиотек Python -->\n",
|
||
"# Машинное обучиение с использованием библиотек Python\n",
|
||
"<!-- dom:AUTHOR: С.В. Лемешевский Email:sergey.lemeshevsky@gmail.com at Институт математики НАН Беларуси -->\n",
|
||
"<!-- Author: --> \n",
|
||
"**С.В. Лемешевский** (email: `sergey.lemeshevsky@gmail.com`), Институт математики НАН Беларуси\n",
|
||
"\n",
|
||
"Date: **May 4, 2020**\n",
|
||
"\n",
|
||
"<!-- Common Mako variable and functions -->\n",
|
||
"<!-- -*- coding: utf-8 -*- -->\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"# Основные определения и постановки задач машинного обучения\n",
|
||
"<div id=\"ml:intro\"></div>\n",
|
||
"\n",
|
||
"*Машинное обучение* — это раздел математики, изучающий способы\n",
|
||
"извлечения закономерностей из ограниченного числа примеров. \n",
|
||
"\n",
|
||
"\n",
|
||
"## Примеры задач машинного обучения\n",
|
||
"<div id=\"ml:intro:examples\"></div>\n",
|
||
"\n",
|
||
"Рассмотрим несколько примеров задач, которые решаются с помощью\n",
|
||
"машинного обучения.\n",
|
||
"\n",
|
||
"** Кредитный скоринг.**\n",
|
||
"\n",
|
||
" *Задача*: выяснить, какие заявки на кредит можно одобрить.\n",
|
||
"\n",
|
||
"**Лента Facebook/Дзен по интересности (вместо сортировки по времени).**\n",
|
||
"\n",
|
||
" *Задача*: показать посты, наиболее интересные для конкретного человека.\n",
|
||
"\n",
|
||
"**Детектирование некорректной работы.**\n",
|
||
"\n",
|
||
" Предположим, что у нас есть завод, на котором происходят некоторые\n",
|
||
" процессы (стоят какие-то котлы, станки, работают сотрудники). На\n",
|
||
" предприятии может произойти поломка, например, сломается датчик\n",
|
||
" уровня жидкости в баке, из-за чего насос не остановится при\n",
|
||
" достижении нужного уровня и нефть начнёт разливаться по полу, что\n",
|
||
" может привести к неизвестным последствиям. Или же сотрудники объявят\n",
|
||
" забастовку и вся работа остановится. Мы хотим, чтобы завод работал\n",
|
||
" исправно, а обо всех проблемах узнавать как можно раньше. \n",
|
||
"\n",
|
||
" *Задача*: предсказать поломки/нештатные ситуации на заводе.\n",
|
||
"\n",
|
||
"**Вопросно-ответная система (как Siri).**\n",
|
||
"\n",
|
||
" *Задача*: ответить голосом на вопрос, заданный голосом.\n",
|
||
"\n",
|
||
"**Self-driving cars.**\n",
|
||
"\n",
|
||
" *Задача*: доехать из точки $А$ в точку $В$.\n",
|
||
"\n",
|
||
"**Перенос стиля изображения.**\n",
|
||
"\n",
|
||
" *Задача*: перенести стиль одного изображения на другое (смешать\n",
|
||
" стиль одного с контекстом другого). \n",
|
||
"\n",
|
||
"\n",
|
||
"Как видим, задачи очень разнообразны. Мы начнем наш путь со следующей\n",
|
||
"классической постановки (к которой, кстати, сводятся многие\n",
|
||
"вышеперечисленные задачи): по имеющемуся признаковому описанию объекта\n",
|
||
"$x \\in \\mathbb{R}^m$ предсказать значение целевой переменной $y \\in\n",
|
||
"\\mathbb{R}^k$ для данного объекта. Обычно $k=1$. \n",
|
||
"\n",
|
||
"Например, в случае кредитного скоринга $x$-ом являются все известные о\n",
|
||
"клиенте данные (доход, пол, возраст, кредитная история и т.д.), а\n",
|
||
"$y$-ом одобрение или неодобрение заявки на кредит.\n",
|
||
"\n",
|
||
"Библиотеки с алгоритмами машинного обучения, которые будем изучать:\n",
|
||
"* [scikit-learn](http://scikit-learn.github.io/stable),\n",
|
||
"\n",
|
||
"* [XGBoost](https://xgboost.readthedocs.io/en/latest/) и\n",
|
||
"\n",
|
||
"* [pytorch](https://pytorch.org). \n",
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- doconce-section-nickname: \"intro\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"\n",
|
||
"## Линейная регрессия\n",
|
||
"<div id=\"ml:linear-regression\"></div>\n",
|
||
"\n",
|
||
"Начнем с подключения необходимых библиотек"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"%matplotlib inline\n",
|
||
"\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"import seaborn as sns\n",
|
||
"import pandas as pd\n",
|
||
"import numpy as np"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"*Линейная регрессия* — это модель следующего вида:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"<!-- Equation labels as ordinary links -->\n",
|
||
"<div id=\"ml:linear-regression:eq:lin-reg\"></div>\n",
|
||
"\n",
|
||
"$$\n",
|
||
"\\begin{equation}\n",
|
||
"\\label{ml:linear-regression:eq:lin-reg} \\tag{1}\n",
|
||
"a(x) = \\langle a, w \\rangle + w_0 = \\sum_{i = 1}^{d} w_i x_i + w_0,\n",
|
||
"\\end{equation}\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"где $w \\in \\mathbb{R}^d$, $w_0 \\in \\mathbb{R}$. Параметрами модели\n",
|
||
"являются *веса* или *коэффициенты* $w_i$. Вес $w_0$ также называется \n",
|
||
"*свободным коэффициентом* или *сдвигом* (bias). Обучить линейную\n",
|
||
"регрессию — значит найти $w$ и $w_0$.\n",
|
||
"\n",
|
||
"В машинном обучении часто говорят об *обобщающей способности модели*,\n",
|
||
"то есть о способности модели работать на новых, тестовых данных\n",
|
||
"хорошо. Если модель будет идеально предсказывать выборку, на которой\n",
|
||
"она обучалась, но при этом просто ее запомнит, не «вытащив» из данных\n",
|
||
"никакой закономерности, от нее будет мало толку. Такую модель\n",
|
||
"называют *переобученной*: она слишком подстроилась под обучающие\n",
|
||
"примеры, не выявив никакой полезной закономерности, которая позволила\n",
|
||
"бы ей совершать хорошие предсказания на данных, которые она ранее не\n",
|
||
"видела. \n",
|
||
"\n",
|
||
"Рассмотрим следующий пример, на котором будет хорошо видно, что значит\n",
|
||
"переобучение модели. Для этого нам понадобится сгенерировать\n",
|
||
"синтетические данные. Рассмотрим зависимость $y(x) = \\cos(1.5\\pi x)$,\n",
|
||
"$y$ — целевая переменная (таргет), а $x$ — объект (просто число от $0$ до\n",
|
||
"$1$). В жизни мы наблюдаем какое-то конечное количество пар\n",
|
||
"объект-таргет, поэтому смоделируем это, взяв $30$ случайных точек $x_i$\n",
|
||
"в отрезке $[0;1]$. Более того, в реальной жизни целевая переменная\n",
|
||
"может быть зашумленной (измерения в жизни не всегда точны),\n",
|
||
"смоделируем это, зашумив значение функции нормальным шумом:\n",
|
||
"$\\tilde{y}_i = y(x_i) + \\mathcal{N}(0, 0.01)$:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"np.random.seed(36)\n",
|
||
"x = np.linspace(0, 1, 100)\n",
|
||
"y = np.cos(1.5 * np.pi * x)\n",
|
||
"\n",
|
||
"x_objects = np.random.uniform(0, 1, size=30)\n",
|
||
"y_objects = np.cos(1.5 * np.pi * x_objects) + np.random.normal(scale=0.1, size=x_objects.shape)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Попытаемся обучить три разных линейных модели: признаки для первой\n",
|
||
"--- $\\{x\\}$, для второй --- $\\{x, x^2, x^3, x^4\\}$, для\n",
|
||
"третьей --- $\\{x, \\dots, x^{20}\\}$:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import LinearRegression\n",
|
||
"from sklearn.preprocessing import PolynomialFeatures\n",
|
||
"\n",
|
||
"\n",
|
||
"fig, axs = plt.subplots(figsize=(16, 4), ncols=3)\n",
|
||
"for i, degree in enumerate([1, 4, 20]):\n",
|
||
" X_objects = PolynomialFeatures(degree).fit_transform(x_objects[:, None])\n",
|
||
" X = PolynomialFeatures(degree).fit_transform(x[:, None])\n",
|
||
" regr = LinearRegression().fit(X_objects, y_objects)\n",
|
||
" y_pred = regr.predict(X)\n",
|
||
" axs[i].plot(x, y, label=\"Real function\")\n",
|
||
" axs[i].scatter(x_objects, y_objects, label=\"Data\")\n",
|
||
" axs[i].plot(x, y_pred, label=\"Prediction\")\n",
|
||
" if i == 0:\n",
|
||
" axs[i].legend()\n",
|
||
" axs[i].set_title(\"Degree = %d\" % degree)\n",
|
||
" axs[i].set_xlabel(\"$x$\")\n",
|
||
" axs[i].set_ylabel(\"$f(x)$\")\n",
|
||
" axs[i].set_ylim(-2, 2)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Чтобы избежать переобучения, модель регуляризуют. Обычно переобучения\n",
|
||
"в линейных моделях связаны с большими весами, а поэтому модель часто\n",
|
||
"штрафуют за большие значения весов, добавляя к функционалу качества,\n",
|
||
"например, квадрат $\\ell^2$-нормы вектора $w$:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"<!-- Equation labels as ordinary links -->\n",
|
||
"<div id=\"ml:linear-regression:eq:2\"></div>\n",
|
||
"\n",
|
||
"$$\n",
|
||
"\\label{ml:linear-regression:eq:2} \\tag{2}\n",
|
||
"Q_{reg}(X, y, a) = Q(X, y, a) + \\lambda \\|w\\|_2^2\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Это слагаемое называют $\\ell_2$-регуляризатором, а коэффициент\n",
|
||
"$\\lambda$ --- коэффициентом регуляризации.\n",
|
||
"\n",
|
||
"## Загрузка данных\n",
|
||
"<div id=\"ml:linear-regression:load\"></div>\n",
|
||
"\n",
|
||
"Мы будем работать с данными из соревнования\n",
|
||
"[House Prices: Advanced Regression Techniques](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview),\n",
|
||
"в котором требовалось предсказать стоимость жилья. Давайте сначала\n",
|
||
"загрузим и немного изучим данные (`train.csv` со страницы соревнования)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data = pd.read_csv(\"train.csv\")\n",
|
||
"data.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Первое, что стоит заметить — у нас в данных есть уникальное для\n",
|
||
"каждого объекта поле `id`. Обычно такие поля только мешают и\n",
|
||
"способствуют переобучению. Удалим это поле из данных:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data = data.drop(columns=[\"Id\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Разделим данные на обучающую и тестовую выборки. Для простоты не будем\n",
|
||
"выделять дополнительно валидационную выборку (хотя это обычно стоит\n",
|
||
"делать, она нужна для подбора гиперпараметров модели, то есть\n",
|
||
"параметров, которые нельзя подбирать по обучающей\n",
|
||
"выборке). Дополнительно нам придется отделить значения целевой\n",
|
||
"переменной от данных."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import train_test_split\n",
|
||
"\n",
|
||
"y = data[\"SalePrice\"]\n",
|
||
"X = data.drop(columns=[\"SalePrice\"])\n",
|
||
"\n",
|
||
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Посмотрим сначала на значения целевой переменной:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"sns.distplot(y_train)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Судя по гистограмме, у нас есть примеры с нетипично большой\n",
|
||
"стоимостью, что может помешать нам, если наша функция потерь слишком\n",
|
||
"чувствительна к выбросам. В дальнейшем мы рассмотрим способы, как\n",
|
||
"минимизировать ущерб от этого.\n",
|
||
"\n",
|
||
"Так как для решения нашей задачи мы бы хотели обучить линейную\n",
|
||
"регрессию, было бы хорошо найти признаки, «наиболее линейно» связанные\n",
|
||
"с целевой переменной, иначе говоря, посмотреть на коэффициент\n",
|
||
"корреляции Пирсона между признаками и целевой переменной. Заметим, что\n",
|
||
"не все признаки являются числовыми, пока что мы не будем рассматривать\n",
|
||
"такие признаки.\n",
|
||
"\n",
|
||
"> **Коэффициент корреляции Пирсона.**\n",
|
||
">\n",
|
||
"> Коэффициент корреляции Пирсона характеризует существование линейной\n",
|
||
"> зависимости между двумя величинами.\n",
|
||
"> \n",
|
||
"> Пусть даны две выборки $x = (x_1, x_2, \\ldots, x_m)$ и $y = (y_1, y_2,\n",
|
||
"> \\ldots, y_m$; коэффициент корреляции Пирсона по формуле:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"r_{xy} = \\frac{\\sum_{i=1}^{m}(x_i-\\bar{x})(y_i -\n",
|
||
"\\bar{y})}{\\sqrt{\\sum_{i=1}^{m}(x_i - \\bar{x})^2 \\sum_{i=1}^{m}(y_i -\n",
|
||
"\\bar{y})^2}} = \\frac{cov(x, y)}{\\sqrt{s_x^2s_y^2}},\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> где $\\bar{x}$, $\\bar{y}$ — выборочные средние, $s_x^2$, $s_y^2$ —\n",
|
||
"> выборочные дисперсии, $r_{xy} \\in [-1, 1]$.\n",
|
||
"> * $|r_{xy}| = 1 \\Rightarrow$ $x$, $y$ — линейно зависимы,\n",
|
||
"> \n",
|
||
"> * $|r_{xy}| = 0 \\Rightarrow$ $x$, $y$ — линейно не зависимы."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"numeric_data = X_train.select_dtypes([np.number])\n",
|
||
"numeric_data_mean = numeric_data.mean()\n",
|
||
"numeric_features = numeric_data.columns\n",
|
||
"\n",
|
||
"X_train = X_train.fillna(numeric_data_mean)\n",
|
||
"X_test = X_test.fillna(numeric_data_mean)\n",
|
||
"\n",
|
||
"correlations = {\n",
|
||
" feature: np.corrcoef(X_train[feature], y_train)[0][1]\n",
|
||
" for feature in numeric_features\n",
|
||
"}\n",
|
||
"sorted_correlations = sorted(correlations.items(), key=lambda x: x[1], reverse=True)\n",
|
||
"features_order = [x[0] for x in sorted_correlations]\n",
|
||
"correlations = [x[1] for x in sorted_correlations]\n",
|
||
"\n",
|
||
"plot = sns.barplot(y=features_order, x=correlations)\n",
|
||
"plot.figure.set_size_inches(15, 10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Посмотрим на признаки из начала списка. Для этого нарисуем график\n",
|
||
"зависимости целевой переменной от каждого из признаков. На этом\n",
|
||
"графике каждая точка соответствует паре признак-таргет (такие графики\n",
|
||
"называются `scatter-plot`)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"fig, axs = plt.subplots(figsize=(16, 5), ncols=3)\n",
|
||
"for i, feature in enumerate([\"GrLivArea\", \"GarageArea\", \"TotalBsmtSF\"]):\n",
|
||
" axs[i].scatter(X_train[feature], y_train, alpha=0.2)\n",
|
||
" axs[i].set_xlabel(feature)\n",
|
||
" axs[i].set_ylabel(\"SalePrice\")\n",
|
||
"plt.tight_layout()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Видим, что между этими признаками и целевой переменной действительно\n",
|
||
"наблюдается линейная зависимость. \n",
|
||
"\n",
|
||
"## Первая модель\n",
|
||
"<div id=\"ml:linear-regression:1st\"></div>\n",
|
||
"\n",
|
||
"В арсенале дата-саентиста кроме `pandas` и `matplotlib` должны быть\n",
|
||
"библиотеки, позволяющие обучать модели. Для простых моделей (линейные\n",
|
||
"модели, решающее дерево, ...) отлично подходит `sklearn`: в нем очень\n",
|
||
"понятный и простой интерфейс. Несмотря на то, что в `sklearn` есть\n",
|
||
"реализация бустинга и простых нейронных сетей, ими все же не\n",
|
||
"пользуются и предпочитают специализированные библиотеки: `XGBoost`,\n",
|
||
"`LightGBM` и пр. для градиентного бустинга над деревьями, `PyTorch`,\n",
|
||
"`Tensorflow` и пр. для нейронных сетей. Так как мы будем обучать\n",
|
||
"линейную регрессию, нам подойдет реализация из `sklearn`. \n",
|
||
"\n",
|
||
"\n",
|
||
"Попробуем обучить линейную регрессию на числовых признаках из нашего\n",
|
||
"датасета. В `sklearn` есть несколько классов, реализующих линейную\n",
|
||
"регрессию: \n",
|
||
"\n",
|
||
"* [`LinearRegression`](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html) — «классическая» линейная регрессия с оптимизацией MSE. Веса находятся как точное решение: $w^* = (X^TX)^{-1}X^Ty$\n",
|
||
"\n",
|
||
"* [`Ridge`](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html) — линейная регрессия с оптимизацией MSE и $\\ell_2$-регуляризацией\n",
|
||
"\n",
|
||
"* [`Lasso`](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Lasso.html) — линейная регрессия с оптимизацией MSE и $\\ell_1$-регуляризацией\n",
|
||
"\n",
|
||
"У моделей из `sklearn` есть методы `fit` и `predict`. Первый принимает\n",
|
||
"на вход обучающую выборку и вектор целевых переменных и обучает\n",
|
||
"модель, второй, будучи вызванным после обучения модели, возвращает\n",
|
||
"предсказание на выборке. Попробуем обучить нашу первую модель на\n",
|
||
"числовых признаках, которые у нас сейчас есть:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import Ridge\n",
|
||
"from sklearn.metrics import mean_squared_error\n",
|
||
"\n",
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train[numeric_features], y_train)\n",
|
||
"y_pred = model.predict(X_test[numeric_features])\n",
|
||
"y_train_pred = model.predict(X_train[numeric_features])\n",
|
||
"\n",
|
||
"print(\"Test MSE = %.4f\" % mean_squared_error(y_test, y_pred))\n",
|
||
"print(\"Train MSE = %.4f\" % mean_squared_error(y_train, y_train_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Мы обучили первую модель и даже посчитали ее качество на отложенной\n",
|
||
"выборке! Давайте теперь посмотрим на то, как можно оценить качество\n",
|
||
"модели с помощью кросс-валидации. Принцип кросс-валидации изображен на\n",
|
||
"рисунке\n",
|
||
"\n",
|
||
"<img src=\"https://docs.splunk.com/images/thumb/e/ee/Kfold_cv_diagram.png/1200px-Kfold_cv_diagram.png\" width=50%>\n",
|
||
"\n",
|
||
"При кросс-валидации мы делим обучающую выборку на $n$ частей\n",
|
||
"(fold). Затем мы обучаем $n$ моделей: каждая модель обучается при\n",
|
||
"отсутствии соответствующего фолда, то есть $i$-ая модель обучается на\n",
|
||
"всей обучающей выборке, кроме объектов, которые попали в $i$-ый фолд\n",
|
||
"(out-of-fold). Затем мы измеряем качество $i$-ой модели на $i$-ом\n",
|
||
"фолде. Так как он не участвовал в обучении этой модели, мы получим\n",
|
||
"«честный результат». После этого, для получения финального значения\n",
|
||
"метрики качества, мы можем усреднить полученные нами $n$ значений."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import cross_val_score\n",
|
||
"\n",
|
||
"cv_scores = cross_val_score(model, X_train[numeric_features], y_train, cv=10, scoring=\"neg_mean_squared_error\")\n",
|
||
"print(\"Cross validation scores:\\n\\t\", \"\\n\\t\".join(\"%.4f\" % x for x in cv_scores))\n",
|
||
"print(\"Mean CV MSE = %.4f\" % np.mean(-cv_scores))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Обратите внимание на то, что результаты `cv_scores` получились\n",
|
||
"отрицательными. Это соглашение в `sklearn` (скоринговую функцию нужно\n",
|
||
"максимизировать). Поэтому все стандартные скореры называются `neg_*`,\n",
|
||
"например, `neg_mean_squared_error`.\n",
|
||
"\n",
|
||
"В качестве метрики качества в соревновании использовалось RMSE (\n",
|
||
"Root Mean Squared Error), а не MSE, которое мы считали выше (и по\n",
|
||
"отложенной выборке и при кросс-валидации):"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\text{RMSE}(X, y, a) = \\sqrt{\\frac{1}{\\ell}\\sum_{i=1}^{\\ell} (y_i -\n",
|
||
"a(x_i))^2}\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"RMSE в чистом виде не входит в стандартные метрики `sklearn`, но мы\n",
|
||
"всегда можем определить свою метрику и использовать ее в некоторых\n",
|
||
"функциях `sklearn`, например, `cross_val_score`. Для этого нужно\n",
|
||
"воспользоваться `sklearn.metrics.make_scorer`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.metrics import make_scorer\n",
|
||
"\n",
|
||
"def rmse(y_true, y_pred):\n",
|
||
" error = (y_true - y_pred) ** 2\n",
|
||
" return np.sqrt(np.mean(error))\n",
|
||
"\n",
|
||
"rmse_scorer = make_scorer(\n",
|
||
" rmse,\n",
|
||
" greater_is_better=False\n",
|
||
")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import Ridge\n",
|
||
"\n",
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train[numeric_features], y_train)\n",
|
||
"y_pred = model.predict(X_test[numeric_features])\n",
|
||
"y_train_pred = model.predict(X_train[numeric_features])\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))\n",
|
||
"print(\"Train RMSE = %.4f\" % rmse(y_train, y_train_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import cross_val_score\n",
|
||
"\n",
|
||
"cv_scores = cross_val_score(model, X_train[numeric_features], y_train, cv=10, scoring=rmse_scorer)\n",
|
||
"print(\"Cross validation scores:\\n\\t\", \"\\n\\t\".join(\"%.4f\" % x for x in cv_scores))\n",
|
||
"print(\"Mean CV RMSE = %.4f\" % np.mean(-cv_scores))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Для того, чтобы иметь некоторую точку отсчета, удобно посчитать\n",
|
||
"оптимальное значение функции потерь при константном предсказании."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"best_constant = y_train.mean()\n",
|
||
"print(\"Test RMSE with best constant = %.4f\" % rmse(y_test, best_constant))\n",
|
||
"print(\"Train RMSE with best constant = %.4f\" % rmse(y_train, best_constant))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Давайте посмотрим на то, какие же признаки оказались самыми\n",
|
||
"«сильными». Для этого визуализируем веса, соответствующие\n",
|
||
"признакам. Чем больше вес — тем более сильным является признак."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 16,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"def show_weights(features, weights, scales):\n",
|
||
" fig, axs = plt.subplots(figsize=(14, 10), ncols=2)\n",
|
||
" sorted_weights = sorted(zip(weights, features, scales), reverse=True)\n",
|
||
" weights = [x[0] for x in sorted_weights]\n",
|
||
" features = [x[1] for x in sorted_weights]\n",
|
||
" scales = [x[2] for x in sorted_weights]\n",
|
||
" sns.barplot(y=features, x=weights, ax=axs[0])\n",
|
||
" axs[0].set_xlabel(\"Weight\")\n",
|
||
" sns.barplot(y=features, x=scales, ax=axs[1])\n",
|
||
" axs[1].set_xlabel(\"Scale\")\n",
|
||
" plt.tight_layout()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"show_weights(numeric_features, model.coef_, X_train[numeric_features].std())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Будем масштабировать наши признаки перед обучением модели. Это, среди,\n",
|
||
"прочего, сделает нашу регуляризацию более честной: теперь все признаки\n",
|
||
"будут регуляризоваться в равной степени.\n",
|
||
"\n",
|
||
"Для этого воспользуемся трансформером\n",
|
||
"[`StandardScaler`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html).\n",
|
||
"Трансформеры в `sklearn` имеют методы `fit` и `transform` (а еще\n",
|
||
"`fit_transform`). Метод `fit` принимает на вход обучающую выборку и\n",
|
||
"считает по ней необходимые значения (например статистики, как\n",
|
||
"`StandardScaler`: среднее и стандартное отклонение каждого из\n",
|
||
"признаков); `transform` применяет преобразование к переданной\n",
|
||
"выборке."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 18,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.preprocessing import StandardScaler\n",
|
||
"\n",
|
||
"scaler = StandardScaler()\n",
|
||
"X_train_scaled = scaler.fit_transform(X_train[numeric_features])\n",
|
||
"X_test_scaled = scaler.transform(X_test[numeric_features])\n",
|
||
"\n",
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train_scaled, y_train)\n",
|
||
"y_pred = model.predict(X_test_scaled)\n",
|
||
"y_train_pred = model.predict(X_train_scaled)\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))\n",
|
||
"print(\"Train RMSE = %.4f\" % rmse(y_train, y_train_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 19,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"scales = pd.Series(data=X_train_scaled.std(axis=0), index=numeric_features)\n",
|
||
"show_weights(numeric_features, model.coef_, scales)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Наряду с параметрами (веса $w$, $w_0$), которые модель оптимизирует на\n",
|
||
"этапе обучения, у модели есть и гиперпараметры. У нашей модели это\n",
|
||
"`alpha` — коэффициент регуляризации. Подбирают его обычно по\n",
|
||
"сетке, измеряя качество на валидационной (не тестовой) выборке или с\n",
|
||
"помощью кросс-валидации. Посмотрим, как это можно сделать (заметьте,\n",
|
||
"что мы перебираем `alpha` по логарифмической сетке, чтобы узнать\n",
|
||
"оптимальный порядок величины)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 20,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import GridSearchCV\n",
|
||
"\n",
|
||
"alphas = np.logspace(-2, 3, 20)\n",
|
||
"searcher = GridSearchCV(Ridge(), [{\"alpha\": alphas}], scoring=rmse_scorer, cv=10)\n",
|
||
"searcher.fit(X_train_scaled, y_train)\n",
|
||
"\n",
|
||
"best_alpha = searcher.best_params_[\"alpha\"]\n",
|
||
"print(\"Best alpha = %.4f\" % best_alpha)\n",
|
||
"\n",
|
||
"plt.plot(alphas, -searcher.cv_results_[\"mean_test_score\"])\n",
|
||
"plt.xscale(\"log\")\n",
|
||
"plt.xlabel(\"alpha\")\n",
|
||
"plt.ylabel(\"CV score\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Попробуем обучить модель с подобранным коэффициентом\n",
|
||
"регуляризации. Заодно воспользуемся очень удобным классом\n",
|
||
"[`Pipeline`](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html):\n",
|
||
"обучение модели часто представляется как последовательность некоторых\n",
|
||
"действий с обучающей и тестовой выборками (например, сначала нужно\n",
|
||
"отмасштабировать выборку (причем для обучающей выборки нужно применить\n",
|
||
"метод `fit`, а для тестовой --- `transform`), а затем\n",
|
||
"обучить/применить модель (для обучающей `fit`, а для тестовой ---\n",
|
||
"`predict`). `Pipeline` позволяет хранить эту последовательность шагов\n",
|
||
"и корректно обрабатывает разные типы выборок: и обучающую, и\n",
|
||
"тестовую."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 21,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.pipeline import Pipeline\n",
|
||
"\n",
|
||
"simple_pipeline = Pipeline([\n",
|
||
" ('scaling', StandardScaler()),\n",
|
||
" ('regression', Ridge(best_alpha))\n",
|
||
"])\n",
|
||
"\n",
|
||
"model = simple_pipeline.fit(X_train[numeric_features], y_train)\n",
|
||
"y_pred = model.predict(X_test[numeric_features])\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Работа с категориальными признаками\n",
|
||
"<div id=\"ml:linear-regression:categ\"></div>\n",
|
||
"\n",
|
||
"Сейчас мы явно вытягиваем из данных не всю информацию, что у нас есть,\n",
|
||
"просто потому, что мы не используем часть признаков. Эти признаки в\n",
|
||
"датасете закодированы строками, каждый из них обозначает некоторую\n",
|
||
"категорию. Такие признаки называются категориальными. Давайте выделим\n",
|
||
"такие признаки и сразу заполним пропуски в них специальным значением\n",
|
||
"(то, что у признака пропущено значение, само по себе может быть\n",
|
||
"хорошим признаком)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 22,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"categorical = list(X_train.dtypes[X_train.dtypes == \"object\"].index)\n",
|
||
"X_train[categorical] = X_train[categorical].fillna(\"NotGiven\")\n",
|
||
"X_test[categorical] = X_test[categorical].fillna(\"NotGiven\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 23,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_train[categorical].sample(5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Сейчас нам нужно как-то закодировать эти категориальные признаки\n",
|
||
"числами, ведь линейная модель не может работать с такими\n",
|
||
"абстракциями. Два стандартных трансформера из `sklearn` для работы с\n",
|
||
"категориальными признаками\n",
|
||
"* [`LabelEncoder`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html) просто перенумеровывает значения признака натуральными числами\n",
|
||
"\n",
|
||
"* [`OneHotEncoder`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html) ставит в соответствие каждому признаку целый вектор, состоящий из нулей и одной единицы (которая стоит на месте, соответствующем принимаемому значению, таким образом кодируя его)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 24,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.preprocessing import OneHotEncoder\n",
|
||
"from sklearn.compose import ColumnTransformer\n",
|
||
"\n",
|
||
"column_transformer = ColumnTransformer([\n",
|
||
" ('ohe', OneHotEncoder(handle_unknown=\"ignore\"), categorical),\n",
|
||
" ('scaling', StandardScaler(), numeric_features)\n",
|
||
"])\n",
|
||
"\n",
|
||
"pipeline = Pipeline(steps=[\n",
|
||
" ('ohe_and_scaling', column_transformer),\n",
|
||
" ('regression', Ridge())\n",
|
||
"])\n",
|
||
"\n",
|
||
"model = pipeline.fit(X_train, y_train)\n",
|
||
"y_pred = model.predict(X_test)\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Посмотрим на размеры матрицы после OneHot-кодирования:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 25,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(\"Size before OneHot:\", X_train.shape)\n",
|
||
"print(\"Size after OneHot:\", column_transformer.transform(X_train).shape)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Как видим, количество признаков увеличилось более, чем в 3 раза. Это\n",
|
||
"может повысить риски переобучиться: соотношение количества объектов к\n",
|
||
"количеству признаков сильно сократилось. \n",
|
||
"\n",
|
||
"Попытаемся обучить линейную регрессию с $\\ell_1$-регуляризатором. На\n",
|
||
"лекциях вы узнаете, что $\\ell_1$-регуляризатор разреживает признаковое\n",
|
||
"пространство, иными словами, такая модель зануляет часть весов."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 26,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import Lasso\n",
|
||
"\n",
|
||
"column_transformer = ColumnTransformer([\n",
|
||
" ('ohe', OneHotEncoder(handle_unknown=\"ignore\"), categorical),\n",
|
||
" ('scaling', StandardScaler(), numeric_features)\n",
|
||
"])\n",
|
||
"\n",
|
||
"lasso_pipeline = Pipeline(steps=[\n",
|
||
" ('ohe_and_scaling', column_transformer),\n",
|
||
" ('regression', Lasso())\n",
|
||
"])\n",
|
||
"\n",
|
||
"model = lasso_pipeline.fit(X_train, y_train)\n",
|
||
"y_pred = model.predict(X_test)\n",
|
||
"print(\"RMSE = %.4f\" % rmse(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 27,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"ridge_zeros = np.sum(pipeline.steps[-1][-1].coef_ == 0)\n",
|
||
"lasso_zeros = np.sum(lasso_pipeline.steps[-1][-1].coef_ == 0)\n",
|
||
"print(\"Zero weights in Ridge:\", ridge_zeros)\n",
|
||
"print(\"Zero weights in Lasso:\", lasso_zeros)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Подберем для нашей модели оптимальный коэффициент\n",
|
||
"регуляризации. Обратите внимание, как перебираются параметры у\n",
|
||
"`Pipeline`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 28,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"alphas = np.logspace(-2, 4, 20)\n",
|
||
"searcher = GridSearchCV(lasso_pipeline, [{\"regression__alpha\": alphas}], scoring=rmse_scorer, cv=10)\n",
|
||
"searcher.fit(X_train, y_train)\n",
|
||
"\n",
|
||
"best_alpha = searcher.best_params_[\"regression__alpha\"]\n",
|
||
"print(\"Best alpha = %.4f\" % best_alpha)\n",
|
||
"\n",
|
||
"plt.plot(alphas, -searcher.cv_results_[\"mean_test_score\"])\n",
|
||
"plt.xscale(\"log\")\n",
|
||
"plt.xlabel(\"alpha\")\n",
|
||
"plt.ylabel(\"CV score\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 29,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"column_transformer = ColumnTransformer([\n",
|
||
" ('ohe', OneHotEncoder(handle_unknown=\"ignore\"), categorical),\n",
|
||
" ('scaling', StandardScaler(), numeric_features)\n",
|
||
"])\n",
|
||
"\n",
|
||
"pipeline = Pipeline(steps=[\n",
|
||
" ('ohe_and_scaling', column_transformer),\n",
|
||
" ('regression', Lasso(best_alpha))\n",
|
||
"])\n",
|
||
"\n",
|
||
"model = pipeline.fit(X_train, y_train)\n",
|
||
"y_pred = model.predict(X_test)\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 30,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"lasso_zeros = np.sum(pipeline.steps[-1][-1].coef_ == 0)\n",
|
||
"print(\"Zero weights in Lasso:\", lasso_zeros)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Иногда очень полезно посмотреть на распределение остатков. Нарисуем\n",
|
||
"гистограмму распределения квадратичной ошибки на обучающих объектах:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 31,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"error = (y_train - model.predict(X_train)) ** 2\n",
|
||
"sns.distplot(error)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Как видно из гистограммы, есть примеры с очень большими\n",
|
||
"остатками. Попробуем их выбросить из обучающей выборки. Например,\n",
|
||
"выбросим примеры, остаток у которых больше 0.95-квантили."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 32,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"mask = (error < np.quantile(error, 0.95))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 33,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"column_transformer = ColumnTransformer([\n",
|
||
" ('ohe', OneHotEncoder(handle_unknown=\"ignore\"), categorical),\n",
|
||
" ('scaling', StandardScaler(), numeric_features)\n",
|
||
"])\n",
|
||
"\n",
|
||
"pipeline = Pipeline(steps=[\n",
|
||
" ('ohe_and_scaling', column_transformer),\n",
|
||
" ('regression', Lasso(best_alpha))\n",
|
||
"])\n",
|
||
"\n",
|
||
"model = pipeline.fit(X_train[mask], y_train[mask])\n",
|
||
"y_pred = model.predict(X_test)\n",
|
||
"print(\"Test RMSE = %.4f\" % rmse(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 34,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_train = X_train[mask]\n",
|
||
"y_train = y_train[mask]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 35,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"error = (y_train - model.predict(X_train)) ** 2\n",
|
||
"sns.distplot(error)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Видим, что качество модели заметно улучшилось! Также бывает очень\n",
|
||
"полезно посмотреть на примеры с большими остатками и попытаться\n",
|
||
"понять, почему же модель на них так сильно ошибается: это может дать\n",
|
||
"понимание, как модель можно улучшить. \n",
|
||
"\n",
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- doconce-section-nickname: \"linear-regression\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"\n",
|
||
"# Предобработка данных\n",
|
||
"<div id=\"ml:features\"></div>\n",
|
||
"\n",
|
||
"\n",
|
||
"Начнем с подключения необходимых библиотек и модулей:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 36,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"import pandas as pd\n",
|
||
"import seaborn as sns\n",
|
||
"from tqdm import tqdm\n",
|
||
"from sklearn.datasets import fetch_20newsgroups\n",
|
||
"\n",
|
||
"from sklearn.model_selection import train_test_split\n",
|
||
"from sklearn.linear_model import Ridge\n",
|
||
"from sklearn.metrics import mean_squared_error"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Работа с текстовыми данными\n",
|
||
"<div id=\"ml:features:text-work\"></div>\n",
|
||
"\n",
|
||
"\n",
|
||
"Как правило, модели машинного обучения действуют в предположении, что\n",
|
||
"матрица «объект-признак» является вещественнозначной, поэтому при\n",
|
||
"работе с текстами сперва для каждого из них необходимо составить его\n",
|
||
"признаковое описание. Для этого широко используются техники\n",
|
||
"векторизации, tf-idf и пр.\n",
|
||
"\n",
|
||
"<!-- Рассмотрим их на примере -->\n",
|
||
"<!-- \"датасета\": \"src-features/banki_responses.json.bz2\" -->\n",
|
||
"<!-- отзывов о банках. -->\n",
|
||
"\n",
|
||
"Сперва загрузим данные:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 37,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data = fetch_20newsgroups(subset='all', categories=['comp.graphics', 'sci.med'])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Данные содержат тексты новостей, которые надо классифицировать на разделы."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 38,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data['target_names']"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 39,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"texts = data['data']\n",
|
||
"target = data['target']"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Например:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 40,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"texts[0]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 41,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data['target_names'][target[0]]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Bag-of-words\n",
|
||
"\n",
|
||
"<div id=\"ml:features:bag-of-words\"></div>\n",
|
||
"\n",
|
||
"Самый очевидный способ формирования признакового описания текстов —\n",
|
||
"векторизация. Простой способ заключается в подсчёте, сколько раз встретилось каждое слово\n",
|
||
"в тексте. Получаем вектор длиной в количество уникальных слов, встречающихся во\n",
|
||
"всех объектах выборки. В таком векторе много нулей, поэтому его удобнее хранить\n",
|
||
"в разреженном виде. \n",
|
||
"\n",
|
||
"Пусть у нас имеется коллекция текстов $D = \\{d_i\\}_{i=1}^l$\n",
|
||
"и словарь всех слов, встречающихся в выборке $V = \\{v_j\\}_{j=1}^d.$ В\n",
|
||
"этом случае некоторый текст $d_i$ описывается вектором\n",
|
||
"$(x_{ij})_{j=1}^d,$ где"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"x_{ij} = \\sum_{v \\in d_i} [v = v_j].\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Таким образом, текст $d_i$ описывается вектором количества вхождений\n",
|
||
"каждого слова из словаря в данный текст."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 42,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.feature_extraction.text import CountVectorizer\n",
|
||
"\n",
|
||
"vectorizer = CountVectorizer(encoding='utf8', min_df=1)\n",
|
||
"_ = vectorizer.fit(texts)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Результатом является разреженная матрица."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 43,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"vectorizer.transform(texts[:1])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 44,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(vectorizer.transform(texts[:1]).indptr)\n",
|
||
"print(vectorizer.transform(texts[:1]).indices)\n",
|
||
"print(vectorizer.transform(texts[:1]).data)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Такой способ представления текстов называют *мешком слов* (bag-of-words).\n",
|
||
"\n",
|
||
"### TF-IDF\n",
|
||
"\n",
|
||
"<div id=\"ml:features:tf-idf\"></div>\n",
|
||
"\n",
|
||
"Очевидно, что не все слова полезны в задаче прогнозирования. Например, мало\n",
|
||
"информации несут слова, встречающиеся во всех текстах. Это могут быть\n",
|
||
"как стоп-слова, так и слова, свойственные всем текстам выборки (в\n",
|
||
"текстах про автомобили употребляется слово «автомобиль»). Эту проблему\n",
|
||
"решает TF-IDF (*T*erm *F*requency–*I*nverse *D*ocument *F*requency)\n",
|
||
"преобразование текста.\n",
|
||
"\n",
|
||
"Рассмотрим коллекцию текстов $D$. Для каждого уникального слова $t$\n",
|
||
"из документа $d \\in D$ вычислим следующие величины: \n",
|
||
"\n",
|
||
"* TD (Term Frequency) – количество вхождений слова в отношении к общему числу слов в тексте:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\textrm{tf}(t, d) = \\frac{n_{td}}{\\sum_{t \\in d} n_{td}},\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"где $n_{td}$ — количество вхождений слова $t$ в текст $d$.\n",
|
||
"\n",
|
||
"* IDF (Inverse Document Frequency):"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\textrm{idf}(t, D) = \\log \\frac{\\left| D \\right|}{\\left| \\{d\\in D: t \\in d\\} \\right|},\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"где $\\left| \\{d\\in D: t \\in d\\} \\right|$ – количество текстов в коллекции, содержащих слово $t$.\n",
|
||
"\n",
|
||
"Тогда для каждой пары (слово, текст) $(t, d)$ вычислим величину:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\textrm{tf-idf}(t,d, D) = \\text{tf}(t, d)\\cdot \\text{idf}(t, D).\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Отметим, что значение $\\text{tf}(t, d)$ корректируется для часто\n",
|
||
"встречающихся общеупотребимых слов при помощи значения\n",
|
||
"$\\textrm{idf}(t, D)$."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 45,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
|
||
"\n",
|
||
"vectorizer = TfidfVectorizer(encoding='utf8', min_df=1)\n",
|
||
"_ = vectorizer.fit(texts)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"На выходе получаем разреженную матрицу."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 46,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"vectorizer.transform(texts[:1])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 47,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(vectorizer.transform(texts[:1]).indptr)\n",
|
||
"print(vectorizer.transform(texts[:1]).indices)\n",
|
||
"print(vectorizer.transform(texts[:1]).data)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Заметим, что оба метода возвращают вектор длины 32548 (размер нашего словаря).\n",
|
||
"\n",
|
||
"Заметим, что одно и то же слово может встречаться в различных формах\n",
|
||
"(например, «сотрудник» и «сотрудника»), но описанные выше методы\n",
|
||
"интерпретируют их как различные слова, что делает признаковое описание\n",
|
||
"избыточным. Устранить эту проблему можно при помощи **лемматизации** и\n",
|
||
"**стемминга**. \n",
|
||
"\n",
|
||
"\n",
|
||
"### Стемминг\n",
|
||
"\n",
|
||
"<div id=\"ml:features:stemming\"></div>\n",
|
||
"\n",
|
||
"*Стемминг* — это процесс нахождения основы слова. В результате применения\n",
|
||
"данной процедуры однокоренные слова, как правило, преобразуются к одинаковому\n",
|
||
"виду. \n",
|
||
"\n",
|
||
"\n",
|
||
"## Таблица 1 : Примеры стемминга\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"<table border=\"1\">\n",
|
||
"<thead>\n",
|
||
"<tr><th align=\"left\"> Слово </th> <th align=\"left\"> Основа</th> </tr>\n",
|
||
"</thead>\n",
|
||
"<tbody>\n",
|
||
"<tr><td align=\"left\"> вагон </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> вагона </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> вагоне </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> вагонов </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> вагоном </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> вагоны </td> <td align=\"left\"> вагон </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важная </td> <td align=\"left\"> важн </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важнее </td> <td align=\"left\"> важн </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важнейшие </td> <td align=\"left\"> важн </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важнейшими </td> <td align=\"left\"> важн </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важничал </td> <td align=\"left\"> важнича </td> </tr>\n",
|
||
"<tr><td align=\"left\"> важно </td> <td align=\"left\"> важн </td> </tr>\n",
|
||
"</tbody>\n",
|
||
"</table>\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"[Snowball](http://snowball.tartarus.org/) — фрэймворк для написания\n",
|
||
"алгоритмов стемминга (библиотека `nltk`). Алгоритмы стемминга отличаются для разных языков\n",
|
||
"и используют знания о конкретном языке — списки окончаний для разных\n",
|
||
"чистей речи, разных склонений и т.д. Пример алгоритма для русского\n",
|
||
"языка – [Russian stemming](http://snowballstem.org/algorithms/russian/stemmer.html)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 48,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"import nltk\n",
|
||
"stemmer = nltk.stem.snowball.RussianStemmer()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 49,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(stemmer.stem(u'машинное'), stemmer.stem(u'обучение'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 50,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"stemmer = nltk.stem.snowball.EnglishStemmer()\n",
|
||
"\n",
|
||
"def stem_text(text, stemmer):\n",
|
||
" tokens = text.split()\n",
|
||
" return ' '.join(map(lambda w: stemmer.stem(w), tokens))\n",
|
||
"\n",
|
||
"stemmed_texts = []\n",
|
||
"for t in tqdm(texts[:1000]):\n",
|
||
" stemmed_texts.append(stem_text(t, stemmer))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 51,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(texts[0])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 52,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(stemmed_texts[0])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Как видим, стеммер работает не очень быстро и запускать его для всей\n",
|
||
"выборки достаточно накладно. \n",
|
||
"\n",
|
||
"\n",
|
||
"### Лематизация\n",
|
||
"\n",
|
||
"<div id=\"ml:features:lemma\"></div>\n",
|
||
"\n",
|
||
"*Лемматизация* — процесс приведения слова к его нормальной форме (лемме):\n",
|
||
"* для существительных — именительный падеж, единственное число;\n",
|
||
"\n",
|
||
"* для прилагательных — именительный падеж, единственное число, мужской род;\n",
|
||
"\n",
|
||
"* для глаголов, причастий, деепричастий — глагол в инфинитиве.\n",
|
||
"\n",
|
||
"Лемматизация — процесс более сложный по сравнению со стеммингом. Стеммер\n",
|
||
"просто «режет» слово до основы.\n",
|
||
"\n",
|
||
"Например, для русского языка есть библиотека `pymorphy2`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 53,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"import pymorphy2\n",
|
||
"morph = pymorphy2.MorphAnalyzer()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 54,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"morph.parse('играющих')[0]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Сравним работу стеммера и лемматизатора на примере:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 55,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"stemmer = nltk.stem.snowball.RussianStemmer()\n",
|
||
"print(stemmer.stem('играющих'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 56,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(morph.parse('играющих')[0].normal_form)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Трансформация признаков и целевой переменной\n",
|
||
"<div id=\"ml:features:trans\"></div>\n",
|
||
"\n",
|
||
"Разберёмся, как может влиять трансформация признаков или целевой\n",
|
||
"переменной на качество модели.\n",
|
||
"\n",
|
||
"### Логарифмирование\n",
|
||
"\n",
|
||
"<div id=\"ml:features:log\"></div>\n",
|
||
"\n",
|
||
"Воспользуется датасетом с ценами на дома, с которым мы уже\n",
|
||
"сталкивались ранее\n",
|
||
"([House Prices: Advanced Regression Techniques](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview))."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 57,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"!wget https://slemeshevsky.github.io/python-course/ml/html/src-ml/train.csv"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 58,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data = pd.read_csv('train.csv')\n",
|
||
"\n",
|
||
"data = data.drop(columns=[\"Id\"])\n",
|
||
"y = data[\"SalePrice\"]\n",
|
||
"X = data.drop(columns=[\"SalePrice\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Посмотрим на распределение целевой переменной"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 59,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"plt.figure(figsize=(12, 5))\n",
|
||
"\n",
|
||
"plt.subplot(1, 2, 1)\n",
|
||
"sns.distplot(y, label='target')\n",
|
||
"plt.title('target')\n",
|
||
"\n",
|
||
"plt.subplot(1, 2, 2)\n",
|
||
"sns.distplot(data.GrLivArea, label='area')\n",
|
||
"plt.title('area')\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Видим, что распределения несимметричные с тяжёлыми правыми хвостами.\n",
|
||
"\n",
|
||
"Оставим только числовые признаки, пропуски заменим средним значением."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 60,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_train, X_test, y_train, y_test = train_test_split(\n",
|
||
" X, y, test_size=0.3, random_state=10)\n",
|
||
"\n",
|
||
"numeric_data = X_train.select_dtypes([np.number])\n",
|
||
"numeric_data_mean = numeric_data.mean()\n",
|
||
"numeric_features = numeric_data.columns\n",
|
||
"\n",
|
||
"X_train = X_train.fillna(numeric_data_mean)[numeric_features]\n",
|
||
"X_test = X_test.fillna(numeric_data_mean)[numeric_features]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Если разбирать линейную регрессия с\n",
|
||
"вероятностной точки зрения, то можно получить, что шум должен быть\n",
|
||
"распределён нормально. Поэтому лучше, когда целевая переменная\n",
|
||
"распределена также нормально.\n",
|
||
"\n",
|
||
"Если прологарифмировать целевую переменную, то её распределение станет\n",
|
||
"больше похоже на нормальное:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 61,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"sns.distplot(np.log(y+1), label='target')\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Сравним качество линейной регрессии в двух случаях:\n",
|
||
"* Целевая переменная без изменений.\n",
|
||
"\n",
|
||
"* Целевая переменная прологарифмирована.\n",
|
||
"\n",
|
||
"> **Предупреждение.**\n",
|
||
">\n",
|
||
"> Не забудем во втором случае взять экспоненту от предсказаний!"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 62,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train, y_train)\n",
|
||
"y_pred = model.predict(X_test)\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % mean_squared_error(y_test, y_pred) ** 0.5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 63,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train, np.log(y_train+1))\n",
|
||
"y_pred = np.exp(model.predict(X_test))-1\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % mean_squared_error(y_test, y_pred) ** 0.5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Попробуем аналогично логарифмировать один из признаков, имеющих также\n",
|
||
"смещённое распределение (этот признак был вторым по важности!)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 64,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_train.GrLivArea = np.log(X_train.GrLivArea + 1)\n",
|
||
"X_test.GrLivArea = np.log(X_test.GrLivArea + 1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 65,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train[numeric_features], y_train)\n",
|
||
"y_pred = model.predict(X_test[numeric_features])\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % mean_squared_error(y_test, y_pred) ** 0.5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 66,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"model = Ridge()\n",
|
||
"model.fit(X_train[numeric_features], np.log(y_train+1))\n",
|
||
"y_pred = np.exp(model.predict(X_test[numeric_features]))-1\n",
|
||
"\n",
|
||
"print(\"Test RMSE = %.4f\" % mean_squared_error(y_test, y_pred) ** 0.5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Как видим, преобразование признаков влияет слабее. Признаков много, а\n",
|
||
"вклад размывается по всем. К тому же, проверять распределение\n",
|
||
"множества признаков технически сложнее, чем одной целевой переменной. \n",
|
||
"\n",
|
||
"## Бинаризация\n",
|
||
"<div id=\"ml:features:binarize\"></div>\n",
|
||
"\n",
|
||
"Мы уже смотрели, как полиномиальные признаки могут помочь при\n",
|
||
"восстановлении нелинейной зависимости линейной моделью. Альтернативный\n",
|
||
"подход заключается в бинаризации признаков. Мы разбиваем ось значений\n",
|
||
"одного из признаков на куски (бины) и добавляем для каждого куска-бина\n",
|
||
"новый признак-индикатор попадения в этот бин."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 67,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import LinearRegression\n",
|
||
"\n",
|
||
"np.random.seed(36)\n",
|
||
"X = np.random.uniform(0, 1, size=100)\n",
|
||
"y = np.cos(1.5 * np.pi * X) + np.random.normal(scale=0.1, size=X.shape)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 68,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"plt.scatter(X, y)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 69,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X = X.reshape((-1, 1))\n",
|
||
"thresholds = np.arange(0.2, 1.1, 0.2).reshape((1, -1))\n",
|
||
"\n",
|
||
"X_expand = np.hstack((\n",
|
||
" X,\n",
|
||
" ((X > thresholds[:, :-1]) & (X <= thresholds[:, 1:])).astype(int)))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 70,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import KFold\n",
|
||
"from sklearn.model_selection import cross_val_score"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 71,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"-np.mean(cross_val_score(\n",
|
||
" LinearRegression(), X, y, cv=KFold(n_splits=3, random_state=123),\n",
|
||
" scoring='neg_mean_squared_error'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 72,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"-np.mean(cross_val_score(\n",
|
||
" LinearRegression(), X_expand, y, cv=KFold(n_splits=3, random_state=123),\n",
|
||
" scoring='neg_mean_squared_error'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Так линейная модель может лучше восстанавливать нелинейные зависимости.\n",
|
||
"\n",
|
||
"## Транзакционные данные\n",
|
||
"<div id=\"ml:features:transactions\"></div>\n",
|
||
"\n",
|
||
"Напоследок посмотрим, как можно извлекать признаки из транзакционных данных.\n",
|
||
"\n",
|
||
"Транзакционные данные характеризуются тем, что есть много строк,\n",
|
||
"характеризующихся моментов времени и некоторым числом (суммой денег,\n",
|
||
"например). При этом если это банк, то каждому человеку принадлежит не\n",
|
||
"одна транзакция, а чаще всего надо предсказывать некоторые сущности\n",
|
||
"для клиентов. Таким образом, надо получить признаки для пользователей\n",
|
||
"из множества их транзакций. Этим мы и займёмся. \n",
|
||
"\n",
|
||
"Для примера возьмём данные [отсюда](https://www.kaggle.com/regivm/retailtransactiondata/). Задача\n",
|
||
"детектирования фродовых клиентов."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 73,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"!wget https://slemeshevsky.github.io/python-course/ml/html/src-ml/Retail_Data_Response.csv\n",
|
||
"!wget https://slemeshevsky.github.io/python-course/ml/html/src-ml/Retail_Data_Transactions.csv"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 74,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"customers = pd.read_csv('Retail_Data_Response.csv')\n",
|
||
"transactions = pd.read_csv('Retail_Data_Transactions.csv')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 75,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"customers.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 76,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"transactions.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 77,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"transactions.trans_date = transactions.trans_date.apply(\n",
|
||
" lambda x: datetime.datetime.strptime(x, '%d-%b-%y'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Посмотрим на распределение целевой переменной:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 78,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"customers.response.mean()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Получаем примерно 1 к 9 положительных примеров. Если такие данные\n",
|
||
"разбивать на части для кросс валидации, то может получиться так, что в\n",
|
||
"одну из частей попадёт слишком мало положительных примеров, а в другую\n",
|
||
"— наоборот. На случай такого неравномерного баланса классов есть\n",
|
||
"`StratifiedKFold`, который бьёт данные так, чтобы баланс классов во всех\n",
|
||
"частях был одинаковым."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 79,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import StratifiedKFold"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Когда строк на каждый объект много, можно считать различные\n",
|
||
"статистики. Например, средние, минимальные и максимальные суммы,\n",
|
||
"потраченные клиентом, количество транзакий, ..."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 80,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"agg_transactions = transactions.groupby('customer_id').tran_amount.agg(\n",
|
||
" ['mean', 'std', 'count', 'min', 'max']).reset_index()\n",
|
||
"\n",
|
||
"data = pd.merge(customers, agg_transactions, how='left', on='customer_id')\n",
|
||
"\n",
|
||
"data.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 81,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import LogisticRegression\n",
|
||
"\n",
|
||
"np.mean(cross_val_score(\n",
|
||
" LogisticRegression(),\n",
|
||
" X=data.drop(['customer_id', 'response'], axis=1),\n",
|
||
" y=data.response,\n",
|
||
" cv=StratifiedKFold(n_splits=3, random_state=123),\n",
|
||
" scoring='roc_auc'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Но каждая транзакция снабжена датой! Можно посчитать статистики только\n",
|
||
"по свежим транзакциям. Добавим их."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 82,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"transactions.trans_date.min(), transactions.trans_date.max()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 83,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"agg_transactions = transactions.loc[transactions.trans_date.apply(\n",
|
||
" lambda x: x.year == 2014)].groupby('customer_id').tran_amount.agg(\n",
|
||
" ['mean', 'std', 'count', 'min', 'max']).reset_index()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 84,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"data = pd.merge(data, agg_transactions, how='left', on='customer_id', suffixes=('', '_2014'))\n",
|
||
"data = data.fillna(0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 85,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"np.mean(cross_val_score(\n",
|
||
" LogisticRegression(),\n",
|
||
" X=data.drop(['customer_id', 'response'], axis=1),\n",
|
||
" y=data.response,\n",
|
||
" cv=StratifiedKFold(n_splits=3, random_state=123),\n",
|
||
" scoring='roc_auc'))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Можно также считать дату первой и последней транзакциями\n",
|
||
"пользователей, среднее время между транзакциями и прочее. \n",
|
||
"\n",
|
||
"\n",
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- doconce-section-nickname: \"features\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"\n",
|
||
"# Простые модели классификации\n",
|
||
"<div id=\"ml:class\"></div>\n",
|
||
"\n",
|
||
"*Классификация* — отнесение объекта к одной из категорий на основании его признаков.\n",
|
||
"\n",
|
||
"Рассмотрим задачу бинарной классификации. Пусть $X = \\mathbb{R}^d$ —\n",
|
||
"пространство объектов, $Y = {−1, +1}$ — множество допустимых ответов,\n",
|
||
"$X = {(x_i , y_i )}_{i=1}^{\\ell}$ — обучающая выборка. Иногда мы будем \n",
|
||
"класс «+1» называть положительным, а класс «−1» — отрицательным.\n",
|
||
"\n",
|
||
"Будем считать, что классификатор имеет вид"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"a(x) = \\mathrm{sign}(b(x)−t) = 2[b(x) > t] − 1.\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"В такого рода задачах возникает необходимость в изучении различных\n",
|
||
"аспектов качества уже обученного классификатора. Сначала обсудим один\n",
|
||
"из подходов к измерению качества таких моделей.\n",
|
||
"\n",
|
||
"## Матрица ошибок\n",
|
||
"<div id=\"ml:class:conf-matr\"></div>\n",
|
||
"\n",
|
||
"*Матрица ошибок* — это способ разбить объекты на четыре категории в\n",
|
||
"зависимости от комбинации истинного ответа и ответа алгоритма\n",
|
||
"(см. таблицу [ml:class:tbl:2](#ml:class:tbl:2)). Через элементы этой матрицы можно,\n",
|
||
"например, выразить долю правильных ответов:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\text{accuracy} = \\frac{\\mathrm{TP} + \\mathrm{TN}}{\\mathrm{TP} +\n",
|
||
"\\mathrm{FP} +\\mathrm{FN} + \\mathrm{TN}}.\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Таблица 2 : Матрица ошибок <div id=\"ml:class:tbl:2\"></div>\n",
|
||
"\n",
|
||
"\n",
|
||
"<table border=\"1\">\n",
|
||
"<thead>\n",
|
||
"<tr><th align=\"left\"> $y=1$ </th> <th align=\"left\"> $y = -1$ </th> </tr>\n",
|
||
"</thead>\n",
|
||
"<tbody>\n",
|
||
"<tr><td align=\"left\"> TP (True Positive) </td> <td align=\"left\"> FP (False Positive) </td> </tr>\n",
|
||
"<tr><td align=\"left\"> FN (False Negative) </td> <td align=\"left\"> TN (True Negatiive) </td> </tr>\n",
|
||
"</tbody>\n",
|
||
"</table>\n",
|
||
"\n",
|
||
"\n",
|
||
"Данная матрика имеет существенный недостаток — её значение необходимо\n",
|
||
"оценивать в контексте баланса классов. Eсли в выборке $950$\n",
|
||
"отрицательных и $50$ положительных объектов, то при абсолютно случайной\n",
|
||
"классификации мы получим долю правильных ответов $0.95$. Это означает,\n",
|
||
"что доля положительных ответов сама по себе не несет никакой\n",
|
||
"информации о качестве работы алгоритма $a(x)$, и вместе с ней следует \n",
|
||
"-анализировать соотношение классов в выборке. \n",
|
||
"\n",
|
||
"Гораздо более информативными критериями являются *точность* (precision)\n",
|
||
"и *полнота* (recall).\n",
|
||
"\n",
|
||
"Точность показывает, какая доля объектов, выделенных классификатором\n",
|
||
"как положительные, действительно является положительными:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\text{precision} = \\frac{\\mathrm{TP}}{\\mathrm{TP} + \\mathrm{FP}}\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Полнота показывает, какая часть положительных объектов была выделена\n",
|
||
"классификатором:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"\\text{precision} = \\frac{\\mathrm{TP}}{\\mathrm{TP} + \\mathrm{FN}}\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Существует несколько способов получить один критерий качества на основе\n",
|
||
"точности и полноты. Один из них — $F$-мера, гармоническое среднее\n",
|
||
"точности и полноты:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"$$\n",
|
||
"F_\\beta = (1+\\beta^2) \\frac{\\text{precision}\\cdot\n",
|
||
"\\text{recall}}{\\beta^2 \\cdot \\text{precision} + \n",
|
||
"\\text{recall}}.\n",
|
||
"$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Среднее гармоническое обладает важным свойством — оно близко к нулю,\n",
|
||
"если хотя бы один из аргументов близок к нулю. Именно поэтому оно\n",
|
||
"является более предпочтительным, чем среднее арифметическое (если\n",
|
||
"алгоритм будет относить все объекты к положительному классу, то он\n",
|
||
"будет иметь $\\text{recall} = 1$ и $\\text{precision} > 0$, а их среднее\n",
|
||
"арифметическое будет больше $1/2$, что недопустимо).\n",
|
||
"\n",
|
||
"Чаще всего берут $\\beta = 1$ хотя иногда встречаются и другие\n",
|
||
"модификации. $F_2$ острее реагирует на recall (т. е. на долю\n",
|
||
"ложноположительных ответов), а $F_{0.5}$ чувствительнее к точности\n",
|
||
"(ослабляет влияние ложноположительных ответов).\n",
|
||
"\n",
|
||
"В `sklearn` есть удобная функция\n",
|
||
"`sklearn.metrics.classification_report`, которая возвращает recall,\n",
|
||
"precision и $F$-меру для каждого из классов, а также количество\n",
|
||
"экземпляров каждого класса."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 86,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.metrics import classification_report\n",
|
||
"y_true = [0, 1, 2, 2, 2]\n",
|
||
"y_pred = [0, 0, 2, 2, 1]\n",
|
||
"target_names = ['class 0', 'class 1', 'class 2']\n",
|
||
"print(classification_report(y_true, y_pred, target_names=target_names))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Линейная классификация\n",
|
||
"<div id=\"ml:class:linear-class\"></div>\n",
|
||
"\n",
|
||
"Основная идея линейного классификатора заключается в том, что\n",
|
||
"признаковое пространство может быть разделено гиперплоскостью на две\n",
|
||
"полуплоскости, в каждой из которых прогнозируется одно из двух\n",
|
||
"значений целевого класса. Если это можно сделать без ошибок, то\n",
|
||
"обучающая выборка называется *линейно разделимой*.\n",
|
||
"\n",
|
||
"<!-- dom:FIGURE: [fig-ml/lin-class.png, width=800 frac=1.0] -->\n",
|
||
"<!-- begin figure -->\n",
|
||
"<!-- end figure -->\n",
|
||
"\n",
|
||
"\n",
|
||
"Указанная разделяющая плоскость называется *линейным дискриминантом*.\n",
|
||
"\n",
|
||
"\n",
|
||
"### Логистическая регрессия\n",
|
||
"\n",
|
||
"<div id=\"ml:class:linear-class:logistic\"></div>\n",
|
||
"\n",
|
||
"*Логистическая регрессия* является частным случаем линейного\n",
|
||
"классификатора, но она обладает хорошим «умением» – прогнозировать\n",
|
||
"вероятность отнесения наблюдения к классу. Таким образом, результат\n",
|
||
"логистической регрессии всегда находится на отрезке $[0, 1]$. Возьмем\n",
|
||
"данные по [ирисам](https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 87,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"!wget https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 88,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"iris = pd.read_csv(\"https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 89,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"iris.describe()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 90,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"sns.pairplot(iris, hue=\"species\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 91,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"sns.lmplot(x=\"petal_length\", y=\"petal_width\", data=iris)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 92,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X = iris.iloc[:, 2:4].values\n",
|
||
"y = iris['species'].values"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 93,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"y[:5]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 94,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.preprocessing import LabelEncoder\n",
|
||
"\n",
|
||
"le = LabelEncoder()\n",
|
||
"le.fit(y)\n",
|
||
"y = le.transform(y)\n",
|
||
"y[:5]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 95,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"iris_pred_names = le.classes_\n",
|
||
"iris_pred_names"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 96,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import train_test_split\n",
|
||
"\n",
|
||
"X_train, X_test, y_train, y_test = train_test_split(\n",
|
||
" X, y, test_size=0.3, random_state=0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 97,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.preprocessing import StandardScaler\n",
|
||
"\n",
|
||
"sc = StandardScaler()\n",
|
||
"sc.fit(X_train)\n",
|
||
"X_train_std = sc.transform(X_train)\n",
|
||
"X_test_std = sc.transform(X_test)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 98,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_train[:5], X_train_std[:5]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 99,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import LogisticRegression\n",
|
||
"\n",
|
||
"lr = LogisticRegression(C=100.0, random_state=1)\n",
|
||
"lr.fit(X_train_std, y_train)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 100,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"lr.predict_proba(X_test_std[:3, :])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 101,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"lr.predict_proba(X_test_std[:3, :]).sum(axis=1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 102,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"y_test[:3]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 103,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"lr.predict_proba(X_test_std[:3, :]).argmax(axis=1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Предсказываем класс первого наблюдения"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 104,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"lr.predict(X_test_std[0, :].reshape(1, -1))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"На основе его коэффициентов:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 105,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_test_std[0, :]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 106,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"X_test_std[0, :].reshape(1, -1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 107,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"y_pred = lr.predict(X_test_std)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 108,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"len(iris_pred_names)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 109,
|
||
"metadata": {
|
||
"collapsed": false
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(classification_report(y_test, y_pred, target_names=iris_pred_names))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- doconce-section-nickname: \"class\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"\n",
|
||
"# Задание\n",
|
||
"<div id=\"ml:task\"></div>\n",
|
||
"\n",
|
||
"Задание состоит из двух основных частей. В первой части необходимо сделать\n",
|
||
"простой препроцессинг и произвести разведывательный анализ данных. \n",
|
||
"\n",
|
||
"Во второй части у Вас будет выбор между двумя вариантами: Вы можете\n",
|
||
"провести регрессионный анализ данных или заняться обработкой\n",
|
||
"естественного языка и построением классификатора текстов.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"<!-- --- begin exercise --- -->\n",
|
||
"\n",
|
||
"## Задание по базе wine\n",
|
||
"<div id=\"ml:task:1\"></div>\n",
|
||
"\n",
|
||
"\n",
|
||
"**a)**\n",
|
||
"**Загрузка и разведывательный анализ.**\n",
|
||
"* Загрузите данные ([скачать](src-ml/wine_reviews.csv.zip)).\n",
|
||
"\n",
|
||
"* Посчитайте размерность данных.\n",
|
||
"\n",
|
||
"* Посчитайте количество пропущенных значений в каждой переменной.\n",
|
||
"\n",
|
||
"* Выведите тип данных каждой переменной. Переконвертируйте при необходимости.\n",
|
||
"\n",
|
||
"* Вина какой области (province) получают наилучшие рейтинги?\n",
|
||
"\n",
|
||
"* На основе словаря color оздайте переменную, в которой закодирован цвет вина.\n",
|
||
"\n",
|
||
"* Удалите наблюдения для которых цвет (color) не указан.\n",
|
||
"\n",
|
||
"* Визуализируйте распределения числовых переменных.\n",
|
||
"\n",
|
||
"* Для каждой страны рассчитайте долю каждого вида вина. В какой стране доля белого вина наибольшая, а в какой красного? (Нужен ответ вида: в стране А наибольшая доля белого вина, а в стране B — красного.\n",
|
||
"\n",
|
||
"* Разделите выборку на обучающую и тестовую\n",
|
||
"\n",
|
||
"**b)**\n",
|
||
"**Регрессионная модель.**\n",
|
||
"* На обучающей выборке постройте регрессионную модель, показывающую зависимость между баллом (зависимая переменная) и ценой. Визуализируйте эту зависимость. На сколько изменяется оценка при изменении цены на одну условную единицу?\n",
|
||
"\n",
|
||
"* Оцените качество модели на основе предсказаний по тестовой выборке по помощи стандартных метрик качества для регрессионных моделей.\n",
|
||
"\n",
|
||
"* Добавьте в модель переменную, в которой закодирован цвет вина. Как изменилось качество?\n",
|
||
"\n",
|
||
"ИЛИ\n",
|
||
"\n",
|
||
"**c)**\n",
|
||
"**Классификация текстов.**\n",
|
||
"* Сделайте препроцессинг текстов в поле description.\n",
|
||
"\n",
|
||
"* На обучающей выборке постройте модель классификации текста, которая бы классифицировала вина по цвету на основе текстов из описания.\n",
|
||
"\n",
|
||
"* Оцените качество работы модели по помощи стандартных метрик качества для алгоритмов классификации. Использование автоматических методов подбора параметров (Grid Search) не обязательно, но в случае наличия — зачтётся.\n",
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- doconce-section-nickname: \"task\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"<!-- Local Variables: -->\n",
|
||
"<!-- doconce-chapter-nickname: \"ml\" -->\n",
|
||
"<!-- End: -->\n",
|
||
"\n",
|
||
"Имя файла: `task_surname.ipynb`.\n",
|
||
"\n",
|
||
"<!-- --- end exercise --- -->"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
}
|