Files
python-course-ipynb/ml.ipynb

2758 lines
98 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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",
"![](fig-ml/lin-class.png)<!-- 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
}