{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению\n", "
Автор материала: Трунов Артем Геннадьевич, @datamove." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#
Pipeline, FeatureUnion – практика применения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Введение
\n", "В этой статье будем разбираться с классами пакета sklearn, которые представляют значительное удобство и экономию времени в работе. Многие, наверное, любят, когда код иллюстрируется диаграммами классов или каким-нибудь метакодом, который позволяет убрать из поля зрения все детали реализации и оставить на виду только самое главное. Pipeline в sklearn - это и есть такой вот метакод, с помошью которого модель видна как на ладони.\n", "\n", "В качестве же примеров будем использовать не измусоленные со всех сторон встроенные в sklearn датасеты, а знакомые читателю по домашним работам и соревнованию 'Alice' данные. Надеюсь, что кого-то эта статья побудит исправить свой код и вдохновит на новые засылки на Kaggle!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Pipeline
\n", "Итак, начнем с перевода и определения. Русские слова \"труба\" и,тем паче, \"трубопровод\", мы, пожалуй, использовать не будем, а вот вариант \"конвейер данных\" кажется мне наиболее подходящим и благозвучным.\n", "\n", "Документация Pipeline определяет этот класс как конвейер преобразования данных с финальным эстиматором (обучающей моделью), применяющийся для того, чтобы можно было легко менять параметры на каждом этапе конвейера и сравнивать результаты. С такой же легкостью можно заменять и сами этапы преобразований данных и финальную модель.\n", "\n", "Давайте сразу окунемся в пример. Рассмотрим датасет Самсунга из домашней работы №7. Мы применяли к данным алгоритм PCA для уменьшения размерности, а что бы он работал как надо, предварительно масштабировали данные. Для классификации использовали метод опорных векторов. Таким образом, наш конвейер будет состоять из двух шагов обработки (StandardScaler, PCA) и финальной модели (LinearSVC)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#изменить соответственно\n", "PATH_TO_DATA=\"../../\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#загрузка данных\n", "#На всякий случай - ссылка https://cloud.mail.ru/public/3EJK/cB2VXsyrP\n", "import numpy as np\n", "\n", "X_train = np.loadtxt(PATH_TO_DATA+\"data/samsung_HAR/samsung_train.txt\")\n", "y_train = np.loadtxt(PATH_TO_DATA+\"data/samsung_HAR/samsung_train_labels.txt\").astype(int)\n", "\n", "X_test = np.loadtxt(PATH_TO_DATA+\"data/samsung_HAR/samsung_test.txt\")\n", "y_test = np.loadtxt(PATH_TO_DATA+\"data/samsung_HAR/samsung_test_labels.txt\").astype(int)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.decomposition import PCA\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.svm import LinearSVC\n", "\n", "pipeline = Pipeline([\n", " ('scaler', StandardScaler()),\n", " ('pca', PCA(n_components=65)),\n", " ('svc', LinearSVC())\n", "])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ну как, красиво? Давайте разберем. Конструктор Pipeline() принимает массив кортежей, в каждом из которых мнемоническое обозначение этапа преобразования и экземпляр класса преобразователя, инстанциированный \"на лету\". Первый этап предразования, 'scaler', принимает исходные данные, и выдает отмасштабированные на выход, который является входом второго этапа - 'pca'. Из 'pca' в 'svc' поступает урезанная матрица главных компонентов числом 65 штук. Обучение проводится именно на ней.\n", "\n", "Давайте запустим обучение и получим результат.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pipeline.fit(X_train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#\"Валидируемся\" на том же тренировочном датасете\n", "pred = pipeline.predict(X_train)\n", "\n", "from sklearn.metrics import accuracy_score, roc_auc_score\n", "\n", "accuracy_score(pred, y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Заметим, что метод fit() вызывается на всех этапах конвейера, а метод predict() - только для финального эстиматора. Тоже самое, разумеется, происходит и с другим набором данных:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Валидируемся на тестовом датасете, для которого у нас есть разметка\n", "test_pred = pipeline.predict(X_test)\n", "accuracy_score(test_pred, y_test)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это значит, что нам не надо тащить за собой хвост из преобразований тестовой выборки, об этом позаботится наш конвейер!\n", "\n", "Так как конвейер обладает интерфейсом модели обучения (fit(), predict() etc), то мы можем использовать его напрямую с полюбившимися методами, такими как кросс-валидация:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import cross_val_score\n", "\n", "cross_val_score(pipeline, X_train, y_train, cv=3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "а также GridSearchCV:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import GridSearchCV\n", "\n", "gcv_params = {'pca__n_components': [20,60,100], \n", " 'svc__C': [0.001, 0.01, 0.1, 1, 10] }\n", "\n", "gcv = GridSearchCV(pipeline, gcv_params, cv=3)\n", "gcv.fit(X_train,y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gcv.best_params_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\"Так,\" - скажет внимательный читатель,- \"а ведь нам надо было выбрать число компонент так, чтобы оставить 90% дисперсии исходных данных\". Как же мы используем это условие в конвейере?\". Вообще-то оно уже реализовано в классе PCA - достаточно передать параметер n_components=0.9 в конструктор класса. Но давайте сделаем сами. Для этого нам придется реализовать собственный класс эстиматора, для многих - первый в их жизни! Сейчас увидим, что на самом деле, это - легко!\n", "\n", "Мы унаследуем класс PCA и перегрузим методы fit(), transform() и fit_tranaform() так, чтобы возвращать матрицу с числом компонентов, объясняющих exp_var% дисперсии.\n", "\n", "Затем построим конвейер с новым классом." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class PCAExplainedVariance(PCA):\n", " #констуктор принимает и сохраняет значение желаемой дисперсии\n", " def __init__(self, exp_var=1.0 ):\n", " super().__init__(copy=True)\n", " self.exp_var = exp_var #желаемая дисперсия исходных данных\n", " self.N_ = 0 #число компонент, тербуемых для достижения заданной дисперсии\n", "\n", " # Находим соответствующее число компонент\n", " def fit(self, X, y=None):\n", " super().fit(X, y)\n", " self.N_ = len(X)\n", " cum_var = 0\n", " for i, component in enumerate(self.components_):\n", " cum_var += self.explained_variance_ratio_[i]\n", " if cum_var>=self.exp_var:\n", " self.N_ = i + 1\n", " break\n", " \n", " # возвращаем усеченный по числу компонент датасет\n", " def transform(self, X, y=None):\n", " U = X[:,:self.N_]\n", " return U\n", " \n", " # fit + transform в одном флаконе\n", " def fit_transform(self, X, y=None):\n", " self.fit(X)\n", " U = X[:, :self.N_]\n", "\n", " return U" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Снова собираем конвейер\n", "pipeline = Pipeline([\n", " ('scaler', StandardScaler()),\n", " ('pca', PCAExplainedVariance(exp_var=0.9)),\n", " ('svc', LinearSVC())\n", "])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#На этот раз запустим с GridSearchCV\n", "gcv_params = {'svc__C': [0.001, 0.01, 0.1, 1, 10] }\n", "gcv = GridSearchCV(pipeline, gcv_params, cv=3)\n", "gcv.fit(X_train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gcv.best_params_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Объект конвейера предоставляет доступ и к экземплярам составляющих его классов. Например, чтобы посмотреть, какое число компонент оставил наш новый PCA-эстиматор:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gcv.best_estimator_.named_steps['pca'].N_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Feature Union
\n", "\n", "Давайте идти дальше и расширять диапазон применяемых средств. Для этого возьмем в качестве примера более сложный случай.\n", "\n", "В соревновании Catch me if you can (aka \"Alice\") на Kaggle, мы отдельно обрабатываем посещаемые пользователями сайты с помощью техники Bag of Words, и отдельно конструируем новые признаки из чего только можно. Затем объединяем частотную матрицу с матрицей признаков и применяем логистическую регрессию.\n", "\n", "Попробуем запрограммировать этот сценарий в конвейер.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Загрузка и предобработка данных - код от @yorko\n", "import pandas as pd\n", "\n", "train_df = pd.read_csv(PATH_TO_DATA+\"../Alice-comp/train_sessions.csv\", index_col=\"session_id\")\n", "#test_df = pd.read_csv(PATH_TO_DATA+\"../Alice-comp/test_sessions.csv\", index_col=\"session_id\")\n", " \n", "# приведем колонки time1, ..., time10 к временному формату\n", "times = ['time%s' % i for i in range(1, 11)]\n", "train_df[times] = train_df[times].apply(pd.to_datetime).fillna(method='ffill', axis=1)\n", "#test_df[times] = test_df[times].apply(pd.to_datetime).fillna(method='ffill', axis=1)\n", "\n", "# отсортируем данные по времени\n", "train_df = train_df.sort_values(by='time1')\n", "\n", "sites = ['site%s' % i for i in range(1,11)]\n", "train_df[sites] = train_df[sites].fillna(0).astype('int')\n", "#test_df[sites] = test_df[sites].fillna(0).astype('int')\n", "\n", "#целевая переменая\n", "y_train = train_df['target']\n", "train_df.drop('target', axis=1, inplace=True)\n", "\n", "train_df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, у нас есть такой вот датафрейм и мы хотим:\n", "\n", " а) составить Bag Of Words из сайтов - код взят из ноутбука @yorko\n", " б) нагенерить признаки, связанные со временем, любезно подсказанные @yorko: year_month, start_hour, morning (последний признак - бинарный)\n", " \n", "Реализуем a), б) по отдельности как классы-трансформеры, а потом объединим результаты.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from scipy.sparse import csr_matrix\n", "# Этот класс-трансформер возвращает разреженную матрицу сайтов\n", "#\n", "from sklearn.base import BaseEstimator, TransformerMixin\n", "\n", "\n", "class ColsToCountMatrix(BaseEstimator, TransformerMixin):\n", " #констуктор принимает и сохраняет название колонок для сливания в текст\n", " def __init__(self, columns=[]):\n", " self.columns=columns\n", " \n", " # fit() ничего не делает\n", " def fit(self, X, y = None):\n", " return self\n", " \n", " #преобразуем посещения сайтов в частотную матрицу\n", " def transform(self, X):\n", " # последовательность с индексами\n", " sites_flatten = X[self.columns].values.flatten()\n", "\n", " # искомая матрица\n", " sites_sparse = csr_matrix(([1] * sites_flatten.shape[0],\n", " sites_flatten,\n", " range(0, sites_flatten.shape[0] + 10, 10)))[:, 1:]\n", " return sites_sparse\n", "#Unit test\n", "sparse_matrix = ColsToCountMatrix(columns=sites).transform(train_df.head(3))\n", "print(sparse_matrix.shape)\n", "print(sparse_matrix)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Этот класс-трансформер возвращает матрицу с новыми признаками\n", "#\n", "from sklearn.base import BaseEstimator, TransformerMixin\n", "\n", "\n", "class TimeToFeatures(BaseEstimator, TransformerMixin):\n", " # берем и сохраняем колонки, которые используем для приготовления новых признаков\n", " def __init__(self, columns=[]):\n", " self.columns = columns\n", " # бездельник опять\n", " def fit(self,X,y=None):\n", " return self\n", " # работяга\n", " def transform(self, X):\n", " # это колонка 'time1' начального датафрейма\n", " time1=self.columns[0] \n", " # создаем пустой датафрейм для новых признаков\n", " new_features = pd.DataFrame(index=X.index)\n", " # делаем новые признаки\n", " new_features['year_month'] = X[time1].apply(lambda ts: ts.year*100 + ts.month)\n", " new_features['start_hour'] = X[time1].apply(lambda ts: ts.hour)\n", " new_features['morning'] = new_features['start_hour'].apply(lambda sh: 1 if 4Заметки
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Что ж, неплохо получилось! Наша модель описывается 16-ю строками, после того как мы реализовали преобразования данных в классах-трансформерах. \n", "\n", "Давайте теперь разберем некоторые вопросы применения конвейеров и объединителей признаков.\n", "\n", "1. Для того, чтобы сделать предсказания обученной модели для тестовой выборки, вызовите метод pipeline.predict_proba(df_proba)\n", "\n", "2. Мы не можем (по крайней мере, с легкостью) в нашем конвейере сделать чаcтотную матрицу на объединенной тренировочной и тестовой выборках, как @yorko делал это на мастер-классе. Автор решил эту проблему таким образом. Вместо класса ColsToCountMatrix, который работает с колонками sites, используем класс ColsToText, определенный ниже. Он собирает сайты из всех колонок в \"текст\", который принимает библиотечный CountVectorizer. В констукторе этого класса читатель найдет не только опцию vocabulary для передачи словаря объединенной тренировочной и тестовой выборки, но и некоторые опции, которые имеет смысл попробовать для улучшения результатов модели.\n", "\n", "3. Если читатель решит применить другую модель обучения, например SGDClassifier, в котором не реализована кросс-валидация, то можно \"обернуть\" его в GridSearchCV:\n", "\n", "

('gcv', GridSearchCV(SGDClassifier(**sgd_params), gcv_sgd_params, **gcv_params))

\n", " \n", "\n", "Надеюсь, что читатель сможет теперь сам улучшать свою модель для соревнования - работать над признаками и подбирать параметры.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
В заключение
\n", "Что можно посоветовать читателю в плане дальнейшего изучения предмета?\n", "\n", "1. Изучить официальную документацию: PipeLine, FeatureUnion, и разобрать статьи-примеры.\n", " \n", "2. Взять на вооружение библиотеку mlextend Себастьяна Рашки. Там можно найти много интересных классов, не реализованных в стандартной библиотеке sklearn.\n", "\n", "3. Посмотреть sklearn-pandas - облегчение работы именно с датафреймами. Например, можно некоторые колонки преобразовать масштабированием, другие - по принципу one-hot-encoding.\n", "\n", "4. Стремиться создавать такие конвейеры, которые позволяют быстро проверять модели и признаки.\n", "\n", "Успехов!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#это подготовительный этап трансформации данных, \n", "#перед тем как применим CountVectorizer\n", "from sklearn.base import BaseEstimator, TransformerMixin\n", "\n", "\n", "class ColsToText(BaseEstimator, TransformerMixin):\n", " #конструктор принимает и сохраняет название колонок для сливания в текст\n", " def __init__(self,columns=[]):\n", " self.columns = columns\n", " # fit() отдыхает - делать нечего\n", " def fit(self, X, y= None):\n", " return self\n", " # сливаем содержимое колонок в одну строку, кроме нулей\n", " def transform(self, X):\n", " return X[self.columns]\\\n", " .apply(lambda x: \" \".join([str(a) for a in x.values if not a==0]), axis=1)\\\n", " .values.reshape(len(X),1)\n", " #заметьте - возвращаем numpy.ndarray\n", "# Unit test\n", "ColsToText(columns=sites).transform(train_df.head())" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.0" } }, "nbformat": 4, "nbformat_minor": 2 }