{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению. Сессия № 3\n", "\n", "###
Автор материала: Кощегулов Эльдар, allincool@mail.ru" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Индивидуальный проект по анализу данных
\n", "##
Классификация спама в SMS
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Описание набора данных и признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Цель работы. \n", "Задача состоит в том, чтобы построить модель классификации спам сообщений в SMS, на основе имеющихся данных.\n", "#### Входные данные.\n", "Решаться задача будет на датасете взятом тут: https://www.kaggle.com/uciml/sms-spam-collection-dataset\n", "\n", "* v1 метка spam/ham\n", "* v2 текст sms\n", "\n", "Целевой признак является метка spam/ham является ли SMS спамом или нет" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Первичный анализ " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import seaborn as sns\n", "import scipy\n", "import matplotlib.pyplot as plt\n", "from nltk.stem.wordnet import WordNetLemmatizer\n", "from sklearn.feature_extraction.text import TfidfVectorizer\n", "from sklearn.model_selection import train_test_split, validation_curve\n", "from sklearn.metrics import roc_auc_score, precision_score\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.linear_model import LogisticRegression, LogisticRegressionCV\n", "from sklearn.naive_bayes import MultinomialNB\n", "from sklearn.model_selection import StratifiedKFold\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings(module='sklearn*', action='ignore', category=DeprecationWarning)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = pd.read_csv('../../data/spam.csv', encoding='latin-1')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### В заявленных признаках v1 и v2 пропущенных значений нет. Видим что помимо признаков v1 и v2 имеем еще 3 признака. Скорее всего это какой-то мусор" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['Unnamed: 2'].unique()[: 5]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[df['Unnamed: 2'] == ' PO Box 5249']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Добавим данные из трех \"левых\" столбцов к тесту SMS и удалим их. Переименуем признаки. Для удобства переобозначим метки\n", "* spam - 1\n", "* ham - 0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['v2'] = df['v2'] + df['Unnamed: 2'].fillna('') + df['Unnamed: 3'].fillna('') + df['Unnamed: 4'].fillna('')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.drop(columns = ['Unnamed: 2', 'Unnamed: 3', 'Unnamed: 4'], inplace = True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.rename(columns = {'v1' : 'label', 'v2' : 'sms'}, inplace = True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['label'] = df['label'].map({'spam' : 1, 'ham' : 0})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Датасет содержит 5572 объекта. Теперь пропущенных значений в нем нет." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Посмотрим как выглядит обычное SMS и спам SMS" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[df['label'] == 0].sample(3)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[df['label'] == 1].sample(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### В спам-сообщениях часто много заглавных букв, восклицательных знаков, и чисел, типа поздравляем вы выиграли миллион" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Посмотрим на распределение классов." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "_, ax = plt.subplots()\n", "plt.bar(np.arange(2), df['label'].value_counts(), color = ['green', 'red'])\n", "ax.set_xticks(np.arange(2))\n", "ax.set_xticklabels(['ham', 'spam']);" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['label'].value_counts()[1] / df.shape[0], df['label'].value_counts()[0] / df.shape[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Видим что классы несбалансированы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Инсайты" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Здравый смысл подсказывает, что обычно в спам сообщениях вам пишут какие-то левые люди, которые представляются вашими друзьями и зовут куда-то зарегистрироваться или вас поздравляют с выигрышами в лотерею. Значит признаки большого количества заглавных букв, обилия знаков препинания и чисел в текстах сообщений, должны что-то дать" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Генерация признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Пока что будем генерировать признаки для объединенной выборки. Удалим знаки препинания, удалим опечатки, приведем тексты к нижнему регистру, сгенерируем признаки длина текста, число знаков препинания, наличие символа, не являющегося цифрой или буквой алфавита." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Первым признаком который мы создадим будет длина SMS. Обычно SMS имеют ограничения на количество слов, поэтому спамеры чтобы не платить много денежек стараются не превосходить эту длину" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['len'] = df['sms'].apply(lambda x : len(x.strip().split()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Создадим счетчик знаков препинания в тексте SMS, а затем удалим знаки препинания. В идеале нужен счетчик восклицательных знаков, так как в спаме вас обычно поздравляют с выигрышами в лотереях и прочем и используют много восклицаний" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import regex as re " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['punctuation'] = df['sms'].apply(lambda x : len(re.findall(\"[^\\P{P}-]+\", x)))\n", "df['punctuation'] = df['sms'].apply(lambda x : len(re.findall(\"[^\\P{P}-]+\", x)))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['sms'] = df['sms'].apply(lambda x : re.sub(\"[^\\P{P}-]+\", \"\", x))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Создадим счетчик заглавных букв в тексте SMS, а затем приведем тексты к нижнему регистру.. Зачастую в спам сообщениях пишут капсом." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['capital'] = df['sms'].apply(lambda x : sum(1 for c in x if c.isupper()))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['sms'] = df['sms'].apply(lambda x : str.lower(x))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Посмотрим какие символы встречаются в текстах. Видим что помимо букв и цифр еще встречается много мусора. Создадим бинарный признак: содержит ли текст SMS символ кроме буквы и цифры. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "symbols = {}\n", "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n", " if x in symbols :\n", " symbols[x] += 1\n", " else :\n", " symbols[x] = 1\n", "symbols" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "volwes = 'aeiou'\n", "consonant = 'bcdfghjklmnpqrstvwxyz'\n", "digits = '0123456789'\n", "alphabet = set(volwes) | set(consonant) | set(digits)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "len(alphabet)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bad_symbols = [x for x in symbols if x not in alphabet]\n", "bad_symbols = ''.join(set(bad_symbols) - set(' '))\n", "bad_symbols" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['badsymbol'] = df['sms'].apply(lambda x :1 if len([s for s in x if s in bad_symbols]) > 0 else 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Попробуем исправить опечатки" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['sms'] = df['sms'].str.replace('å', 'a').str.replace('ä', 'a').str.replace('â', 'a').str.replace('á', 'a')\n", "df['sms'] = df['sms'].str.replace('õ', 'o').str.replace('ò', 'o').str.replace('ð', 'o').str.replace('ö', '0') \\\n", " .str.replace('ó', 'o').str.replace('ô', 'o')\n", "df['sms'] = df['sms'].str.replace('û', 'u')\n", "df['sms'] = df['sms'].str.replace('è', 'e')\n", "df['sms'] = df['sms'].str.replace('ì', '1').str.replace('ï', 'l')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### В спам сообщениях часто упоминаются крупные денежные выигрыши. Нужно создать признаки : наличие числа в тексте и наличие символа валюты" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Замечаем что среди символов в текстах имеются '$' и '£'. Создадим признак для них." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['moneysign'] = df['sms'].apply(lambda x : 1 if ('$' in list(x)) or ('£' in list(x)) else 0 )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Остальные символы поудаляем. Вообще, возможно что при удалении знаков препинания мы поудаляли смайлы и возможно наличие/отсутствие смайла будет хорошим признаком. Если будет время надо подумать над этим. Признак исправлял ли я или нет" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "symbols = {}\n", "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n", " if x in symbols :\n", " symbols[x] += 1\n", " else :\n", " symbols[x] = 1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bad_symbols = [x for x in symbols if x not in alphabet]\n", "bad_symbols = ''.join(set(bad_symbols) - set(' '))\n", "bad_symbols" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for symb in bad_symbols : \n", " df['sms'] = df['sms'].str.replace(symb, '')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "symbols = {}\n", "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n", " if x in symbols :\n", " symbols[x] += 1\n", " else :\n", " symbols[x] = 1\n", "symbols" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Создадим признак: наличие в тексте SMS числа(возможно надо проверять не просто число, а число с множествой нулей)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['num'] = df['sms'].apply(lambda x : 1 if len([s for s in x if s in digits]) > 0 else 0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.columns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Полезность признаков будем исследовать в дальнейшем с помощью модели" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Разобьем данные на трейн и тест с одинаковым распределением целевой переменной" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "target = df['label'].values" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train, X_test, y_train, y_test = train_test_split(df, target, test_size = 0.2, stratify = target, random_state = 10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y_train.sum() / len(y_train), y_test.sum() / len(y_test)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train.shape, X_test.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### В трейне 4457 объектов, в тесте 1115" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Визуальный анализ" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Построим гистограммы созданных признаков слева и гистограммы созданных признаков в зависимости от целевой переменной справа" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for col in X_train.columns[2 :] :\n", " fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (20, 10))\n", "# ax.set_ylabel('% фрагментов', fontsize=12)\n", "# ax.set_xlabel('Имя автора', fontsize=12) \n", " axes[0].set_title(col)\n", " axes[0].hist(X_train[col], bins = 200);\n", " axes[1].set_title(col)\n", " axes[1].hist(X_train[col][X_train['label'] == 0], bins = 200, label = 'ham')\n", " axes[1].hist(X_train[col][X_train['label'] == 1], bins = 200, label = 'spam')\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Судя по гистограммам признаов, почти все спам сообщения содержат символ валюты. Также половина спам сообщений содержит число в своем тексте и опечатку. При генерации этих признаков подобный эффект и ожидался." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize = (20, 10))\n", "sns.heatmap(X_train[['label', 'len', 'punctuation', 'capital', 'badsymbol',\n", " 'moneysign', 'num']].corr())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### - Во-первых, длина SMS коррелирует с числом гласных/согласных, числом знаков препинания, тут ничего удивительного.\n", "#### - Во-вторых, видим корреляцию между наличием символа, не являющегося цифрой или буквой алфавита, и наличием символов \"$\" и \"£\", так как второе является подмножество первого.\n", "#### - В-третьих, видим корреляцию между целевой переменной и наличием числа в тексте SMS и наличием символа денежки." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Выбор метрики" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Решается задача классификации на два класса. Классы несбалансированы, FP - нормальное SMS помечено как спам, это недопустимо. FN - спам помечен как нормальное SMS, допустимо, но не сильно хочется. Поэтому в качестве метрики будем использовать rocauc." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Выбор модели" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### На заре развития спам-фильтров их строили используя наивный байесовский классификатор, поэтому будем рассматривать эту модель. Также у нас ожидается много признаков после использования преобразования tfidf к тексту SMS, поэтому будем рассматривать логистическую регрессию. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Предобработка данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Будем использовать преобразование tfidf для текста SMS, так же отмасштабируем признаки." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scaler = StandardScaler()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cols = ['len', 'punctuation', 'capital', 'badsymbol', 'moneysign', 'num']\n", "X_train_scaled = pd.DataFrame(scaler.fit_transform(X_train[cols]), columns = cols)\n", "X_test_scaled = pd.DataFrame(scaler.transform(X_test[cols]), columns = cols)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Данных у нас не так много, поэтому выбираем кросс-валидацию на 10 фолдов. Для начала посмотрим на наши модели из коробки, ничего не настраивая." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def valid(model, n, bayes = False) :\n", " skf = StratifiedKFold(n_splits = n, random_state = 17)\n", " auc_scores = []\n", " for train_index, valid_index in skf.split(X_train_scaled, y_train):\n", " X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n", " y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n", " \n", " X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n", " cv = TfidfVectorizer(ngram_range = (1, 3))\n", " X_train_bow = cv.fit_transform(X_train_sms)\n", " X_valid_bow = cv.transform(X_valid_sms) \n", " if bayes :\n", " X_train_new = X_train_bow\n", " X_valid_new = X_valid_bow\n", " else :\n", " X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n", " X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n", " model.fit(X_train_new, y_train_part)\n", " model_pred_for_auc = model.predict_proba(X_valid_new)\n", " auc_scores.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n", " return np.mean(auc_scores)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logit = LogisticRegression(random_state = 17)\n", "bayes = MultinomialNB()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scores_logit = valid(logit, 10)\n", "print('Logistic regreession - rocauc : {}'.format(scores_logit))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scores_bayes = valid(bayes, 10, True)\n", "print('Bayessian classfier - rocauc : {}'.format(scores_bayes))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Видим, что логистическая регрессия справляется получше. Дальше будем работать только с ней." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Настройка гиперпараметров и построение кривых валидации и обучения." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def valid_for_valid_plots(model, n, bayes = False) :\n", " skf = StratifiedKFold(n_splits = n, random_state = 17)\n", " auc_scores_cv = []\n", " auc_scores_valid = []\n", " for train_index, valid_index in skf.split(X_train_scaled, y_train):\n", " X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n", " y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n", " \n", " X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n", " cv = TfidfVectorizer(ngram_range = (1, 3))\n", " X_train_bow = cv.fit_transform(X_train_sms)\n", " X_valid_bow = cv.transform(X_valid_sms) \n", " if bayes :\n", " X_train_new = X_train_bow\n", " X_valid_new = X_valid_bow\n", " else :\n", " X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n", " X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n", " \n", " model.fit(X_train_new, y_train_part)\n", " auc_scores_cv.append(roc_auc_score(y_train_part, model.predict_proba(X_train_new)[:, 1]))\n", " model_pred_for_auc = model.predict_proba(X_valid_new)\n", " auc_scores_valid.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n", " return 1 - np.mean(auc_scores_valid), 1 - np.mean(auc_scores_cv)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Построим кривые валидации" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Cs = [0.1 * i for i in range(1, 21)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scores = []\n", "for c in Cs :\n", " logit = LogisticRegression(C = c, random_state = 17)\n", " scores.append(valid_for_valid_plots(logit, 10))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(nrows = 1, ncols = 1, figsize = (20, 10))\n", "plt.plot(Cs, [i[0] for i in scores], color = 'blue', label='holdout')\n", "plt.plot(Cs, [i[1] for i in scores], color = 'red', label='CV')\n", "plt.ylabel(\"ROCAUC\")\n", "plt.xlabel(\"C\")\n", "plt.title('Validation curve for C in (0.1, 2)');" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "#### Будем перебирать значения C в интервале [0.5, 1.5]. При С < 0.5 происходит недообучение. При С > 1.5 ошибка на трейне упирается в ноль, а на валидации не падает, это переобучение." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Cs = np.linspace(0.5, 1.5, 10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for c in Cs :\n", " logit = LogisticRegression(C = c, random_state = 17)\n", " print(c, valid(logit, 10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### C_opt = 1.5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "C_opt = 1.5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Построим кривые обучения" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def valid_for_train_plots(model, n, alpha, bayes = False) :\n", " skf = StratifiedKFold(n_splits = n, random_state = 17)\n", " auc_scores_cv = []\n", " auc_scores_valid = []\n", " for train_index, valid_index in skf.split(X_train_scaled[: int(X_train_scaled.shape[0] * alpha)], y_train[: int(X_train_scaled.shape[0] * alpha)]):\n", " X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n", " y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n", " \n", " X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n", " cv = TfidfVectorizer(ngram_range = (1, 3))\n", " X_train_bow = cv.fit_transform(X_train_sms)\n", " X_valid_bow = cv.transform(X_valid_sms) \n", " if bayes :\n", " X_train_new = X_train_bow\n", " X_valid_new = X_valid_bow\n", " else :\n", " X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n", " X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n", " \n", " model.fit(X_train_new, y_train_part)\n", " auc_scores_cv.append(roc_auc_score(y_train_part, model.predict_proba(X_train_new)[:, 1]))\n", " model_pred_for_auc = model.predict_proba(X_valid_new)\n", " auc_scores_valid.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n", " return np.mean(auc_scores_valid), np.mean(auc_scores_cv)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alphas = [0.1 * i for i in range(1, 11)]\n", "scores = []\n", "for alpha in alphas :\n", " logit = LogisticRegression(C = C_opt, random_state = 17)\n", " scores.append(valid_for_train_plots(logit, 10, alpha = alpha))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(nrows = 1, ncols = 1, figsize = (20, 10))\n", "plt.plot(alphas, [i[0] for i in scores], color = 'blue', label='holdout')\n", "plt.plot(alphas, [i[1] for i in scores], color = 'red', label='CV')\n", "plt.ylabel(\"ROCAUC\")\n", "plt.xlabel(\"C\")\n", "plt.title('Learnings curve with optimal C');" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Судя по кривым обучения, происходит недообучение и для улучшения результата надо усложнить модель." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Прогноз для тестовой выборки" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cv = TfidfVectorizer(ngram_range = (1, 3))\n", "X_train_sms = cv.fit_transform(X_train['sms'])\n", "X_test_sms = cv.transform(X_test['sms'])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_sms, X_train_scaled]))\n", "test = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_test_sms, X_test_scaled]))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logit = LogisticRegression(C = C_opt, random_state = 17)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logit.fit(train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for x, y in zip(cols, logit.coef_[0][len(cv.get_feature_names()) :]) :\n", " print(x, y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Видим, что для нашей модели признаки наличия числа и наличие символа валюты в тексте SMS являются важными, также число слов в тексте и число заглавных букв, а вот признаки наличия опечаток и знаков препинания не так уж и важны." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logit_pred = logit.predict_proba(test)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "roc_auc_score(y_test, logit_pred[:, 1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Качество на тесте соответствует ожиданиям после кросс-валидации" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Выводы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Предложено решение задачи фильтрации спама на основе модели логистической регрессии. Можно использовать подобные спам-фильтры для SMS, электронной почты.\n", "\n", "#### Дальнейшее развитие модели может быть связано с лемматизацией/стеммингом текстов SMS. Использовать стекинг/блендинг нескольких моделей." ] } ], "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.4" } }, "nbformat": 4, "nbformat_minor": 2 }