[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
}