{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "##
[mlcourse.ai](https://mlcourse.ai) – открытый курс OpenDataScience по машинному обучению \n", " \n", "Автор материала: Юрий Кашницкий (@yorko в Slack ODS). Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#
Домашнее задание № 8 (демо)\n", "##
Реализация алгоритмов онлайн-обучения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вам предлагается реализовать два алгоритма – регрессор и классификатор, обучаемые стохастическим градиентным спуском (Stochastic Gradient Descent, SGD). [Веб-форма](https://docs.google.com/forms/d/1xlbc0CaUaNpVs-fhxkHUe61AtsvS_aDqQ0CLFxs6UDA) для ответов." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## План домашнего задания\n", " 1. Линейная регрессия и SGD\n", " 2. Логистическая регрессия и SGD\n", " 3. Логистическая регрессия и SGDClassifier в задаче классификации отзывов к фильмам" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В [статье](https://habrahabr.ru/company/ods/blog/326418/) было описано, как таким образом обучать регрессор, т.е. минимизировать квадратичную функцию потерь. Реализуем этот алгоритм." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Линейная регрессия и SGD" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from sklearn.base import BaseEstimator\n", "from sklearn.metrics import log_loss, mean_squared_error, roc_auc_score\n", "from sklearn.model_selection import train_test_split\n", "from tqdm import tqdm\n", "\n", "%matplotlib inline\n", "import seaborn as sns\n", "from matplotlib import pyplot as plt\n", "from sklearn.preprocessing import StandardScaler" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Реализуйте класс `SGDRegressor`. Спецификация:\n", "- класс наследуется от `sklearn.base.BaseEstimator`\n", "- конструктор принимает параметры `eta` – шаг градиентного спуска (по умолчанию $10^{-3}$) и `n_iter` – число проходов по выборке (по умолчанию 10)\n", "- также в конструкторе должны создаваться списки `mse_` и `weights_` для отслеживания значений среднеквадратичной ошибки и вектора весов по итерациям градиентного спуска\n", "- Класс имеет методы `fit` и `predict`\n", "- Метод `fit` принимает матрицу `X` и вектор `y` (объекты `numpy.array`), добавляет к матрице `X` слева столбец из единиц, инициализирует вектор весов `w` **нулями** и в цикле с числом итераций `n_iter` обновляет веса (см. [статью](https://habrahabr.ru/company/ods/blog/326418/)), а также записывает получившиеся на данной итерации значения среднеквадратичной ошибки (именно MSE, SE слишком большими будут) и вектор весов `w` в предназначенные для этого списки. \n", "- В конце метод `fit` создает переменную `w_`, в которой хранится тот вектор весов, при котором ошибка минимальна\n", "- Метод `fit` должен возвращать текущий экземпляр класса `SGDRegressor`, т.е. `self`\n", "- Метод `predict` принимает матрицу `X`, добавляет к ней слева столбец из единиц и возвращает вектор прогнозов модели, используя созданный методом `fit` вектор весов `w_`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class SGDRegressor(BaseEstimator):\n", " \"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим работу алгоритма на данных по росту и весу. Будем прогнозировать рост (в дюймах) по весу (в фунтах)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data_demo = pd.read_csv(\"../../data/weights_heights.csv\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.scatter(data_demo[\"Weight\"], data_demo[\"Height\"])\n", "plt.xlabel(\"Вес (фунты)\")\n", "plt.ylabel(\"Рост (дюймы)\");" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X, y = data_demo[\"Weight\"].values, data_demo[\"Height\"].values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выделим 70% под обучение, 30% – под проверку и масштабируем выборку." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train, X_valid, y_train, y_valid = train_test_split(\n", " X, y, test_size=0.3, random_state=17\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scaler = StandardScaler()\n", "X_train_scaled = scaler.fit_transform(X_train.reshape([X_train.shape[0], 1]))\n", "X_valid_scaled = scaler.transform(X_valid.reshape([X_valid.shape[0], 1]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обучите созданный вами `SGDRegressor` на выборке `(X_train_scaled, y_train)`. Параметры оставьте по умолчанию." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Изобразите на графике процесс обучения – как среднеквадратичная ошибка зависит от номера итерации стохастического градиентного спуска." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выведите наименьшее значение среднеквадратичной ошибки и лучший вектор весов модели." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Постройте график того, как менялись значения весов модели ($w_0$ и $w_1$) по мере обучения." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сделайте прогноз для отложенной выборки `(X_valid_scaled, y_valid)` и посмотрите на MSE." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь следайте то же самое, но с `LinearRegression` из `sklearn.linear_model`. Посчитайте MSE для отложенной выборки." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Вопрос 1. В каком знаке после разделителя отличаются MSE линейной регрессии и `SGDRegressor` для отложенной выборки?\n", " - 2\n", " - 3\n", " - 4\n", " - 5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Логистическая регрессия и SGD\n", "Теперь давайте разберемся, как при таком же стохастическом подходе обучать логистическую регрессию." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Задача классификации, $X$ – обучающая выборка размеров $\\ell \\times (d+1)$ (первый столбец – вектор из единиц), $y$ – вектор ответов, $y_i \\in \\{-1, 1\\}$.\n", "В [4 статье](https://habrahabr.ru/company/ods/blog/323890/) серии мы подробно разбирали, как логистическая регрессия с $L_2$-регуляризацией сводится к задаче минимизации:\n", "$$ C\\sum_{i=1}^\\ell \\log{(1 + e^{-y_iw^Tx_i})} + \\frac{1}{2}\\sum_{j=1}^d w_j^2 \\rightarrow min_w$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 2. По какой формуле будут пересчитываться веса логистической регрессии при обучении стохастическим градиентным спуском?\n", " - $w_j^{(t+1)} = w_j^{(t)} + \\eta (Cy_i x_{ij} \\sigma(y_iw^Tx_i) + \\delta_{j\\neq0} w_j)$\n", " - $w_j^{(t+1)} = w_j^{(t)} - \\eta (Cy_i x_{ij} \\sigma(-y_iw^Tx_i) + \\delta_{j\\neq0}w_j)$\n", " - $w_j^{(t+1)} = w_j^{(t)} - \\eta (Cy_i x_{ij} \\sigma(-y_iw^Tx_i) - \\delta_{j\\neq0}w_j )$\n", " - $w_j^{(t+1)} = w_j^{(t)} + \\eta (Cy_i x_{ij} \\sigma(-y_iw^Tx_i) - \\delta_{j\\neq0}w_j)$\n", " \n", "Здесь \n", "- $i \\in {0,\\ldots, \\ell-1}, j \\in {0,\\ldots, d}$\n", "- C – коэффициент регуляризации\n", "- $x_{ij} $ – элемент матрицы X в строке $i$ и столбце $j$ (нумерация с 0), \n", "- $x_i$ – $i$-ая строка матрицы $X$ (нумерация с 0), \n", "- $w_j^{(t)}$ – значение $j$-ого элемента вектора весов $w$ на шаге $t$ стохастического градиентного спуска\n", "- $\\eta$ – небольшая константа, шаг градиентного спуска\n", "- $\\delta_{j\\neq0}$ – символ Кронекера, то есть 1, когда $j\\neq0$ и $0$ – в противном случае" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Реализуйте класс `SGDClassifier`. Спецификация:\n", "- класс наследуется от `sklearn.base.BaseEstimator`\n", "- конструктор принимает параметры `eta` – шаг градиентного спуска (по умолчанию $10^{-3}$), `n_iter` – число проходов по выборке (по умолчанию 10) и C – коэффициент регуляризации\n", "- также в конструкторе должны создаваться списки `loss_` и `weights_` для отслеживания значений логистических потерь и вектора весов по итерациям градиентного спуска\n", "- Класс имеет методы `fit`, `predict` и `predict_proba`\n", "- Метод `fit` принимает матрицу `X` и вектор `y` (объекты `numpy.array`, рассматриваем только случай бинарной классификации, и значения в векторе `y` могут быть -1 и 1), добавляет к матрице `X` слева столбец из единиц, инициализирует вектор весов `w` **нулями** и в цикле с числом итераций `n_iter` обновляет веса по выведенной вами формуле, а также записывает получившиеся на данной итерации значения log_loss и вектор весов `w` в предназначенные для этого списки. \n", "- В конце метод `fit` создает переменную `w_`, в которой хранится тот вектор весов, при котором ошибка минимальна\n", "- Метод `fit` должен возвращать текущий экземпляр класса `SGDClassifier`, т.е. `self`\n", "- Метод `predict_proba` принимает матрицу `X`, добавляет к ней слева столбец из единиц и возвращает матрицу прогнозов модели (такую же, какую возвращают методы `predict_proba` моделей `sklearn`), используя созданный методом `fit` вектор весов `w_`\n", "- Метод `predict` вызывает метод `predict_proba` и возвращает вектор ответов: -1, если предсказанная вероятность класса 1 меньше 0.5 и 1 – в противном случае\n", "- И еще **важный момент**: во избежание вычислительных проблем из-за слишком больших или малых значений под экспонентной (overflow & underflow) используйте написанную функцию `sigma`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def sigma(z):\n", " z = z.flatten()\n", " z[z > 100] = 100\n", " z[z < -100] = -100\n", " return 1.0 / (1 + np.exp(-z))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class SGDClassifier(BaseEstimator):\n", " \"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим `SGDClassifier` на данных UCI по раку молочной железы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.datasets import load_breast_cancer" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cancer = load_breast_cancer()\n", "# поменяем метки в y с 0 на -1\n", "X, y = cancer.data, [-1 if i == 0 else 1 for i in cancer.target]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выделим 70% под обучение, 30% – под проверку и масштабируем выборку." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train, X_valid, y_train, y_valid = train_test_split(\n", " X, y, test_size=0.3, random_state=17\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scaler = StandardScaler()\n", "X_train_scaled = scaler.fit_transform(X_train)\n", "X_valid_scaled = scaler.transform(X_valid)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обучите на масштибированной выборке `SGDClassifier` с параметрами `C`=1, `eta`=$10^{-3}$ и `n_iter`=3." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Постройте график изменения log_loss." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь обучите `SGDClassifier` с параметром `C`=1000, число проходов по выборке увеличьте до 10." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрите на веса модели, при которых ошибка на обучении была минимальна." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 3. Какой признак сильнее остальных влияет на вероятность того, что опухоль доброкачественна, согласно обученной модели `SGDClassifier`? (будьте внимательны – проверьте длину вектора весов, полученного после обучения, сравните с числом признаков в исходной задаче)\n", " - worst compactness\n", " - worst smoothness\n", " - worst concavity\n", " - concave points error\n", " - concavity error\n", " - compactness error\n", " - worst fractal dimension\n", " - radius error" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посчитайте log_loss и ROC AUC на отложенной выборке, проделайте все то же с `sklearn.linear_model.LogisticRegression` (параметры по умолчанию, только random_state=17) и сравните результаты." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Логистическая регрессия и SGDClassifier в задаче классификации отзывов к фильмам" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь посмотрим на логистическую регрессию и ее же версию, но обучаемую стохастическим градиентным спуском, в задаче классификации отзывов IMDB. С этой задачей мы знакомы по 4 и 8 темам курса. Данные можно скачать [отсюда](https://drive.google.com/open?id=1xq4l5c0JrcxJdyBwJWvy0u9Ad_pvkJ1l).\n", "\n", "Импортируем файлы, и обучим на имеющихся данных `CountVectorizer`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.datasets import load_files\n", "from sklearn.feature_extraction.text import CountVectorizer\n", "from sklearn.linear_model import SGDClassifier" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# поменяйте путь к файлу\n", "reviews_train = load_files(\n", " \"/Users/y.kashnitsky/Documents/Machine_learning/datasets/imdb_reviews/train\"\n", ")\n", "text_train, y_train = reviews_train.data, reviews_train.target" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "reviews_test = load_files(\n", " \"/Users/y.kashnitsky/Documents/Machine_learning/datasets/imdb_reviews/test\"\n", ")\n", "text_test, y_test = reviews_test.data, reviews_test.target" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обучим на имеющихся данных `CountVectorizer`, считая биграммы, то есть перейдем к разреженному представлению данных, где каждому уникальному слову и паре подряд идущих слов в обучающей выборке соответсвует признак. Всего таких признаков получается более 1.5 млн." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "cv = CountVectorizer(ngram_range=(1, 2))\n", "X_train = cv.fit_transform(text_train)\n", "X_test = cv.transform(text_test)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train.shape, X_test.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обучите на выборке `(X_train, y_train)` логистическую регрессию с параметрами по умолчанию (только укажите `random_state`=17) и посчитайте ROC AUC на тестовой выборке. Замерьте время обучения модели. Данные можно не масштабировать, так как признаки – по сути, счетчики, и они уже все измеряются примерно в одном диапазоне." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь перейдем к онлайн-алгоритму. Мы написали свой `SGDClassifier` и принцип его работы поняли, надо еще немного постараться, чтобы сделать его эффективным, например, сделать поддержку разреженных данных. Но мы теперь перейдем к `sklearn`-реализации SGD-алгоритма. Прочитайте документацию [SGDClassifier](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html), сделайте выводы, чем `SGDClassifier` из `Sklearn` более продвинут, чем наша реализация SGD-классификатора. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 4. Чем `sklearn`-реализация стохастического классификатора более продвинута, чем `SGDClassifier`, который мы реализовали? Отметьте все подходящие варианты.\n", " - Изменяемый шаг градиентного спуска\n", " - Реализован линейный SVM\n", " - Реализована ранняя остановка во избежание переобучения\n", " - Есть распараллеливание по процессорам\n", " - Можно обучать LASSO\n", " - Поддерживается онлайн-обучение деревьев решений\n", " - Поддерживается mini-batch подход (обновление весов по нескольким объектом сразу, а не по одному)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проведите 100 итераций SGD-логрегрессии (опять `random_state`=17) на той же выборке. Опять замерьте время обучения модели и обратите внимание, насколько оно меньше, чем время обучения логистической регрессии." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" ВАШ КОД ЗДЕСЬ \"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вопрос 5. В каком знаке после разделителя отличаются ROC AUC на тестовой выборке логистической регрессии и SGD-классификатора `Sklearn` с логистической функцией потерь?\n", " - 2\n", " - 3\n", " - 4\n", " - 5" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" } }, "nbformat": 4, "nbformat_minor": 2 }