{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению. Сессия № 2\n", "\n", "###
Автор материала: Мороз Денис Анатольевич (@denismoroz)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Индивидуальный проект по анализу данных
\n", "###
Отгадай писателя ужастиков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Данный проект базируется на соревновании Kaggle по угадыванию вероятностей принадлежности фрагмента текста одному из трех писателей рассказов ужасов https://www.kaggle.com/c/spooky-author-identification. \n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Даны размеченные данные с тремя признаками: \n", "id - номер фрагмента текста; \n", "text - фрагмент текста\n", "author - автор фрагмента (целевая переменная)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import re\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.metrics import accuracy_score\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from tqdm import tqdm\n", "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Подтянем базы данных и ознакомимся с их структурой." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_texts = pd.read_csv('../../data/spooky_writer_train.csv')\n", "test = pd.read_csv('../../data/spooky_writer_test.csv')\n", "sample_sub= pd.read_csv('../../data/spooky_writer_sample_submission.csv')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_texts.info(())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test.info()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_texts.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_sub.info()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_sub.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ознакомимся с данными. Построим на графике количество фрагментов текста по каждому автору в тестовой выборке. В целевой переменной у нас всего три автора и фрагменты распределены плюс минус равномерно. Как видим, это задача мультиклассовой классификации с тремя целевыми признаками." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_total = len(train_texts)\n", "num_eap = len(train_texts.loc[train_texts.author == 'EAP'])\n", "num_hpl = len(train_texts.loc[train_texts.author == 'HPL'])\n", "num_mws = len(train_texts.loc[train_texts.author == 'MWS'])\n", "\n", "fig, ax = plt.subplots()\n", "eap, hpl, mws = plt.bar(np.arange(1, 4), [(num_eap/num_total)*100, (num_hpl/num_total)*100, (num_mws/num_total)*100])\n", "ax.set_xticks(np.arange(1, 4))\n", "ax.set_xticklabels(['Edgar Allan Poe (EAP)', 'H.P. Lovecraft (HPL)', 'Mary Shelley (MWS)'])\n", "ax.set_ylim([0, 60])\n", "ax.set_ylabel('% фрагментов', fontsize=12)\n", "ax.set_xlabel('Имя автора', fontsize=12)\n", "ax.set_title('Распределение фрагментов')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вполне очевидно, что каждого автора отличает определенный лексический запас и на этом в первую очередь необходимо базироваться при построении обучающейся модели. Напишем функцию, которая выводит \n", "матрицу общих пар слов больше трех символов (или комбинации из двух слов) для пар авторов. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def common_words_matrix(df): \n", " columns=[\"words\",\"author\"]\n", " words_pool=pd.DataFrame(columns=columns)\n", " for t in tqdm(range(len(df))):\n", " words=re.findall('\\w{3,}', df[\"text\"].iloc[t].lower())\n", " words = [' '.join(ws) for ws in zip(words, words[1:])]\n", " \n", " for word in words:\n", " words_pool.loc[words_pool.shape[0]]=[word,df[\"author\"].iloc[t]]\n", " \n", " cross=pd.crosstab(words_pool.words,words_pool.author)\n", " columns_m=[\"EAP\",\"HPL\",\"MWS\"]\n", " index_m=[\"EAP\",\"HPL\",\"MWS\"]\n", " matrix=pd.DataFrame(columns=columns_m,index=index_m)\n", " matrix.loc[\"EAP\",\"EAP\"]=len(cross.loc[cross.EAP==1])\n", " matrix.loc[\"HPL\",\"HPL\"]=len(cross.loc[cross.HPL==1])\n", " matrix.loc[\"MWS\",\"MWS\"]=len(cross.loc[cross.MWS==1])\n", " matrix.loc[\"EAP\",\"HPL\"]=len(cross.loc[(cross.EAP==1) & (cross.HPL==1)])\n", " matrix.loc[\"HPL\",\"EAP\"]=len(cross.loc[(cross.EAP==1) & (cross.HPL==1)])\n", " matrix.loc[\"EAP\",\"MWS\"]=len(cross.loc[(cross.EAP==1) & (cross.MWS==1)])\n", " matrix.loc[\"MWS\",\"EAP\"]=len(cross.loc[(cross.EAP==1) & (cross.MWS==1)])\n", " matrix.loc[\"HPL\",\"MWS\"]=len(cross.loc[(cross.HPL==1) & (cross.MWS==1)])\n", " matrix.loc[\"MWS\",\"HPL\"]=len(cross.loc[(cross.HPL==1) & (cross.MWS==1)])\n", " print(\"Количество общих слов (комбинаций слов)\")\n", " print(matrix)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запустим функцию common_words_matrix, чтобы увидеть сколько общих пар слов, которые \n", "идут подряд, используют авторы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "common_words_matrix(train_texts)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из полученных матриц мы видим, что количество общих пар слов не так уж велико, это может характеризовать некую уникальность каждого автора и на этом можно базироваться, подбирая признаки для модели. Комбинации из трех и более пар слов считаю использовать нецелесообразно, т.к. модель может переобучиться. Фрагменты текстов относительно коротки и априори маловероятно, что те же тройки слов, которые встречались в тренировочной выборке станут попадаться в тестовых выборках." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для решения данной задачи классификации я решил использовать инструмент Vowpal Wabbit (VW), который имеет следующие преимущества:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- хорошая скорость обучение модели и прогнозирования; \n", "\n", "- возможность использования нелинейных признаков (посредством ngram);\n", "\n", "- удобная настройка параметров;\n", "\n", "- встроенная кросс-валидация." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Самое главное достоинство - это удобное разбиение признакового пространства слов на пары - свойства, которое я собираюсь использовать для обучения модели и предстказания." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Закодируем буквенные символы авторов с помощью цифр 1,2,3, т.к. многоклассовая классификация VW принимает на вход только цифры." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d = {\"EAP\":1,\"MWS\":2,\"HPL\":3}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_texts[\"author_code\"]=train_texts[\"author\"].map(d)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_texts.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Напишем функцию для записи таблиц в формат VWю" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def to_vw_format(out_vw,df,is_train=True):\n", " with open(out_vw,\"w\") as out:\n", " for i in range(df.shape[0]):\n", " \n", " if is_train:\n", " target = df[\"author_code\"].iloc[i]\n", " else:\n", " target = 1 # в тестовой выборке target может быть любым\n", " text = df[\"text\"].iloc[i].replace(\"\\n\",\"\").replace(\"|\",\"\").replace(\":\",\"\").lower() #удалим спецсимволы\n", " text = \" \".join(re.findall(\"\\w{3,}\",text)) #оставим слова более 2 символов\n", " s = \"{} |text {}\\n\".format(target,text)\n", " out.write(s) \n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Разобьем выборку на обучающую и тестовую, % разбиения - по умолчанию." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train, valid = train_test_split(train_texts,random_state=13)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим размеры выборок" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(train.shape[0],test.shape[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запишем преобразованные выборки в файлы. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "to_vw_format(\"train.vw\",train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!head -2 train.vw" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "to_vw_format(\"valid.vw\",valid)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!head -2 valid.vw" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "to_vw_format(\"test.vw\",test,is_train=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!head -2 test.vw" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запустим Vowpal Wabbit на сформированном файле. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!rm train.vw.cache" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!vw --oaa 3 train.vw -f model.vw -b 24 --random_seed 17 --loss_function logistic --ngram 2 --passes 30 \\\n", "--learning_rate 0.5 --power_t 0.5 -k -c -q ff" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим на валидационной выборке." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "!vw -i model.vw -t -d valid.vw -p valid_pred.txt --random_seed 17" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В задании на Kaggle точность оценивается с помощью метрики logloss. В связи с эти в модели VW была настроена логистическая функция потерь. Кросс-валидации на тренировочной выборке дала хороший результат; average loss = 0.0139812, на валидационной выборке - 0.155669. \n", "\n", "Также метрикой для оценки модели на валидационной выборке может служить accuracy_score, которая широко используется на практике для оценки задач классификации. В данном случае, как видно ниже, она показывает неплохой результат для такой простой модели, как наша." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with open('valid_pred.txt') as pred_file:\n", " valid_pred = [float(label) for label in pred_file.readlines()]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "accuracy_score(valid[\"author_code\"], valid_pred)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вручную я проводил изменения параметров VW, таких как количество проходов, количество ngram, регуляризация и пр., однако они дают худший результат." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запустим на тестовой выборке и сформируем посылку." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "!vw -i model.vw -t -d test.vw -p test_pred.csv --random_seed 17 -r test_prob.txt" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test_prob=pd.read_csv(\"test_prob.txt\",header=None,sep=\" |:\",names=[\"x\",\"EAP\",\"x\",\"MWS\",\"x\",\"HPL\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test_prob.head()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Используя формулу 1/(1+exp(-score)) преобразуем полученные значения вероятностей." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for i in [\"EAP\",\"MWS\",\"HPL\"]:\n", " test_prob[i] = 1/(1+np.exp(-test_prob[i])) " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test_prob.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сверим размеры, полученного файла с прогнозами, и формы для посылки, скачанного с Kaggle." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(test_prob.shape)\n", "print(sample_sub.shape)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_sub.EAP=test_prob[\"EAP\"]\n", "sample_sub.HPL=test_prob[\"HPL\"]\n", "sample_sub.MWS=test_prob[\"MWS\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_sub.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_sub.to_csv('benchmark_submission.csv',index=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bench=pd.read_csv(\"benchmark_submission.csv\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bench.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посылку отправили на Kaggle. С первого раза 209 место из 519. Думаю, неплохо!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Вывод: VW c минимальным количеством признаком дал неплохой результат, который может служить базой для дальнейших улучшений. В первую очередь необходимо добавить количество признаков, напр., количество знаков пунктуации, соотношение позитивных/негативных слов и др. Также улучшить результат позволит использование других моделей NLP, таких как word2vec." ] } ], "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.1" } }, "nbformat": 4, "nbformat_minor": 2 }