Задание выполнил(а): Подчезерцев Алексей
Дата выдачи: 28.04.2019
Дедлайн: 23:59 12.05.2019
За сдачу задания позже срока на итоговую оценку за задание накладывается штраф в размере 1 балл в день, но получить отрицательную оценку нельзя.
Внимание! Домашнее задание выполняется самостоятельно. «Похожие» решения считаются плагиатом и все задействованные студенты (в том числе те, у кого списали) не могут получить за него больше 0 баллов.
Стирать условия нельзя!
Загрузка файлов с решениями происходит в системе Anytask.
Формат названия файла: homework_03_Фамилия_Имя.ipynb
import numpy as np
import pandas as pd
В задании вам предоставлены реальные текстовые данные.
Необходимо построить алгоритм, который будет по тексту документа определять тип источника:
[2 балла]
Скачаем данные отсюда: https://yadi.sk/d/o3cPgFAq5gALiw
D = pd.read_csv('texts_dataset.csv', sep=';', index_col=0)
D.head()
Далее будем использовать лишь поля "Текст", "Тип источника"
D = D[[ "Текст", "Тип источника"]]
D.head()
for i in D['Тип источника'].unique():
print (i)
D.info()
Удалим объекты с пропусками
D.dropna(axis = 0, inplace=True)
for i in D['Тип источника'].unique():
print(i, '\t\t', D[D['Тип источника'] == i]['Текст'].str.len().mean())
Наименьшая длина текстов в категории Микроблогов $\approx 170$, далее идут форумы и отзывы $\approx 500$, после Видео и Мессенджеры $ \approx 930$.
Наибольшее количество символов категории Новости $-$ 2611 и Блоги $-$ 3500.
Результаты метрик не противоречат смыслу и жизненому опыту.
используйте word_tokenize из nltk.tokenize
from nltk.tokenize import word_tokenize
%%time
D['Текст'] = D['Текст'].str.lower()
D['tokens'] = D.apply(lambda x: word_tokenize(x['Текст']), axis=1)
D.head()
%%time
D['tokens'] = D.apply(lambda x : [w for w in x['tokens'] if w.isalpha()], axis=1)
D.head()
import collections
words = collections.Counter()
for line in D["tokens"]:
for word in set(line):
words[word] += 1
for i in words.most_common(20):
print(*i)
большая часть слов является предлогами и союзами и не несут смысловой нагрузки. Из 20 популярных слов смысл есть только у 3, причем 2 из них отличаются только на окончание
for i in words.most_common()[len(words)-20:]:
print(*i)
[3 балла]
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
D['Text'] = D.apply(lambda x: ' '.join(x['tokens']), axis=1)
D_train, D_test = train_test_split(D, test_size=0.3, random_state=42)
%%time
cnt_vec = CountVectorizer()
D_train_bow = cnt_vec.fit_transform(D_train["Text"])
D_test_bow = cnt_vec.transform(D_test["Text"])
D_train_bow, D_test_bow
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vec = TfidfVectorizer()
%%time
X_TFIDF = tfidf_vec.fit_transform(D_train["Text"])
X_test_TFIDF = tfidf_vec.transform(D_test["Text"])
X_TFIDF
X_test_TFIDF
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('russian')
%%time
D_train['stemmed'] = D_train.apply(lambda x: ' '.join([stemmer.stem(w) for w in x['tokens']]), axis=1)
D_train.head()
%%time
D_test['stemmed'] = D_test.apply(lambda x: ' '.join([stemmer.stem(w) for w in x['tokens']]), axis=1)
D_test.head()
tfidf_vec_stem = TfidfVectorizer()
%%time
X_stem_TFIDF = tfidf_vec_stem.fit_transform(D_train["stemmed"])
X_test_stem_TFIDF = tfidf_vec_stem.transform(D_test["stemmed"])
D_train_bow.shape, D_test_bow.shape
X_TFIDF.shape, X_test_TFIDF.shape
X_stem_TFIDF.shape, X_test_stem_TFIDF.shape
После стемминга количество уникальных слов уменьшилось примерно в 2 раза.
[2 балла]
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import accuracy_score
lr1 = SGDClassifier()
lr1.fit(D_train_bow, D_train["Тип источника"])
y_predict = lr1.predict(D_test_bow)
print("BOW algo accuracy:", accuracy_score(D_test["Тип источника"], y_predict))
lr2 = SGDClassifier()
lr2.fit(X_TFIDF, D_train["Тип источника"])
y_predict = lr2.predict(X_test_TFIDF)
print("TFIDF algo accuracy:", accuracy_score(D_test["Тип источника"], y_predict))
lr3 = SGDClassifier()
lr3.fit(X_stem_TFIDF, D_train["Тип источника"])
y_predict = lr3.predict(X_test_stem_TFIDF)
print("TFIDF with stem algo accuracy:", accuracy_score(D_test["Тип источника"], y_predict))
Результат работы всех алгоритмов примерно одинаковый, результат отличается лишь на сотые доли.
Лучший результат показал BOW алгоритм.
BOW отличается от TFIDF на 0.007 по качеству. Возможно, тексты подобраны на одну и ту же тематику, поэтому разделение по частоте встречамости в тексте не дает значительного изменения.
После стэминга качество упало на 0.012 по сравнению с обычным TFIDF. Возможная причина - потеря смысла некоторых слов при откидывании окончаний и суффиксов (смысл слова в работе не восстанавливался).
_D_test = D_test.reindex()
_D_test.index=np.arange(_D_test.shape[0])
for i,r in _D_test[_D_test["Тип источника"] != y_predict][:10].iterrows():
print("Text:")
print(r["Текст"])
print("Stammed text:")
print(r["stemmed"])
print("Predicted: ", r["Тип источника"])
print("Actual: ", y_predict[i])
print("="*60)
По некоторым текстам и человеку трудно сказать, к какой именно категории относится текст из-за неоднозначности информации и пересечений категорий.
Кроме того, часть информации была потеряна при обработке данных, например в последней записи (теги форумов).
Возможно, стоило добавить анализ тегов и разметки, но это бы было в какой-то мере читерством)
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from sklearn.utils.multiclass import unique_labels
%matplotlib inline
Всмомогательная статистика по текстам:
_D_test[_D_test["Тип источника"] ==y_predict].groupby("Тип источника")["Text"].describe()
_D_test[_D_test["Тип источника"] !=y_predict].groupby("Тип источника")["Text"].describe()
cm = confusion_matrix(D_test["Тип источника"], y_predict)
classes = unique_labels(D_test["Тип источника"], y_predict)
fig, ax = plt.subplots(figsize=(10,8))
im = ax.imshow(cm, interpolation='nearest', cmap=plt.cm.Oranges)
ax.figure.colorbar(im, ax=ax)
ax.set(xticks=np.arange(cm.shape[1]),yticks=np.arange(cm.shape[0]), xticklabels=classes, yticklabels=classes,
title='Матрица ошибок',
ylabel='Истинное значение',
xlabel='Предсказанное значение')
fmt = 'd'
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
ax.text(j, i, format(cm[i, j], fmt),
ha="center", va="center",
color="white" if cm[i, j] > thresh else "black")
fig.tight_layout()
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
classes = unique_labels(D_test["Тип источника"], y_predict)
fig, ax = plt.subplots(figsize=(10,8))
im = ax.imshow(cm, interpolation='nearest', cmap=plt.cm.Oranges)
ax.figure.colorbar(im, ax=ax)
ax.set(xticks=np.arange(cm.shape[1]), yticks=np.arange(cm.shape[0]), xticklabels=classes, yticklabels=classes,
title='Матрица ошибок',
ylabel='Истинное значение',
xlabel='Предсказанное значение')
fmt = '.3f'
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
ax.text(j, i, format(cm[i, j], fmt),
ha="center", va="center",
color="white" if cm[i, j] > thresh else "black")
fig.tight_layout()
Из 2 диаграммы видно, что модель хорошо предсказывает категории микроблоги, новости, отзывы и форумы. Хорошим остается предсказание для видео.
Модель совершенно не угадывает блоги и мессенджеры и относит их чаще к новостям.
Кроме того, модель часто неверно определяет другие категории не только как новости, но и форумы.
[3 балла]
Для наших экспериентов возьмём обучающую выборку отсюда.
train = pd.read_csv('train.csv')
Решается задача многоклассовой классификации — определение ценовой категории телефона. Для простоты перейдём к задаче бинарной классификации — пусть исходные классы 0 и 1 соответствуют классу 0 новой целевой переменной, а остальные классу 1. Замените целевую переменную, отделите её в отдельную переменную и удалите из исходной выборки.
y = train["price_range"].apply(lambda x: 0 if x == 0 or x == 1 else 1)
X = train.drop("price_range", axis=1)
X.head()
Разделите выборку на обучающую и тестовую части в соотношении 7 к 3. Для этого можно использовать train_test_split из scikit-learn. Не забудьте зафиксировать сид для разбиения.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
В этой части вы будете обучать самый простой бинарный классификатор — логистическую регрессию. Будем использовать готовую реализацию LogisticRegression из scikit-learn.
Логистическая регрессия — линейный метод, то есть в нём предсказание алгоритма вычислаяется как скалярное произведение признаков и весов алгоритма:
$$ b(x) = w_0 + \langle w, x \rangle = w_0 + \sum_{i=1}^{d} w_i x_i $$Для вычисления вероятности положительного класса применяется сигмода. В результате предсказание вероятности принадлежности объекта к положительному классу можно записать как:
$$ P(y = +1 | x) = \frac{1}{1 + \exp(- w_0 - \langle w, x \rangle )} $$Не забывайте, что для линейных методов матрицу объекты-признаки необходимо предварительно нормировать (то есть привести каждый признак к одному и тому же масштабу одним из способов). Для этого можно воспользоваться StandardScaler или сделать это вручную.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
Обучите логистическую регрессию. Сделайте предсказания для тестовой части, посчитайте по ним ROC-AUC и Accuracy (порог 0.5). Хорошо ли удаётся предсказывать целевую переменную? Не забывайте, что метод predict_proba вычисляет вероятности обоих классов выборки, а в бинарной классификации нас интересует в первую очередь вероятность принадлежности к положительному классу.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
lr = LogisticRegression()
lr.fit(X_train, y_train)
y_predict = lr.predict_proba(X_test)[:,1]
print("ROC-AUC: ", roc_auc_score(y_test, y_predict))
print("Accuracy: ", accuracy_score(y_test, np.round(y_predict)))
У обученной логистической регрессии есть два аттрибута: coef_ и intercept_, которые соответствуют весам $w$ и $w_0$. Это и есть результат обучения логистической регрессии. Попробуйте с помощью них (с помощью всё той же обученной ранее логистической регрессии) посчитать "сырое" предсказание алгоритма $b(x)$.
Постройте гистограмму полученных значений и ответьте на вопросы:
b_x = lr.intercept_[0] + lr.coef_[0].dot(X_test.T)
plt.figure(figsize=(20,10))
plt.grid(True)
plt.title('"сырое" предсказание алгоритма b(x)')
plt.xlabel('Предсказание')
plt.ylabel('Количество')
plt.hist(b_x, bins=np.int(np.sqrt(b_x.shape[0])))
plt.show()
Данные распределены примерно одинаково относительно 0 (303 и 297 записей).
Заметны некоторые отколонения колчества записей в некоторых диапазонах (например -11..-10), но в целом количество записей у краев распределения уменьшается.
Значения похожи на вероятность принадлежности к классу. Диапазон значений больше единицы на количество признаков - 20.
Реализуйте сигмоиду и постройте её график. Что вы можете сказать об этой функции?
def sigma(x):
return 1/(1 + np.exp(-x))
plt.figure(figsize=(20,10))
_x = np.linspace(-6, 6, 1000)
_y = sigma(_x)
plt.grid(True)
plt.title('Сигмоида')
plt.xlabel('X')
plt.ylabel('Сигмоида')
plt.plot(_x, _y)
plt.show()
Примените реализованную сигмоиду к $b(x)$. Вы должны получить вероятности принадлежности к положительному классу. Проверьте, что ваши значения совпали с теми, которые получены с помощью predict_proba.
b_xx = sigma(b_x)
sum(b_xx != y_predict)
Значения совпали
Таким образом, обучение логистической регрессии — настройка параметров $w$ и $w_0$, а применение — подсчёт вероятностей принадлежности положительному классу как применение сигмоды к скалярному произведению признаков и параметров.
Постройте для обученной логистической регрессии ROC-кривую roc_curve и PR-кривую precision_recall_curve.
from sklearn.metrics import precision_recall_curve, roc_curve
f, (ax1, ax2) = plt.subplots(1, 2, sharey=True, figsize=(14,7))
ax1.grid(True)
ax1.set(title='ROC-кривая', xlabel ='False positive rate',ylabel ='True positive rate')
fpr, tpr, thresholds = roc_curve(y_test, y_predict)
ax1.plot(fpr, tpr)
fpr, tpr, thresholds = precision_recall_curve(y_test, y_predict)
ax2.grid(True)
ax2.set(title='Precision-recall-кривая', xlabel ='Recall',ylabel ='Precision')
ax2.plot(fpr, tpr)
[2 бонусных балла]
В этой части вы будете обучать самый простой бинарный классификатор — логистическую регрессию. Будем использовать готовую реализацию LogisticRegression из scikit-learn.
Логистическая регрессия — линейный метод, то есть в нём предсказание алгоритма вычислаяется как скалярное произведение признаков и весов алгоритма:
$$ b(x) = w_0 + \langle w, x \rangle = w_0 + \sum_{i=1}^{d} w_i x_i $$Для вычисления вероятности положительного класса применяется сигмода. В результате предсказание вероятности принадлежности объекта к положительному классу можно записать как:
$$ P(y = +1 | x) = \frac{1}{1 + \exp(- w_0 - \langle w, x \rangle )} $$Если выше вручную мы только применяли логистическую регрессию, то здесь предлагается реализовать обучение с помощью полного градиентного спуска. Если кратко, то обучение логистической регрессии с $L_2$-регуляризацией можно записать следующим образом:
$$ Q(w, X) = \frac{1}{l} \sum_{i=1}^{l} \log (1 + \exp(- y_i \langle w, x_i \rangle )) + \frac{\lambda_2}{2} \lVert w \rVert _2^2 \to \min_w $$Считаем, что $y_i \in \{-1, +1\}$, а нулевым признаком сделан единичный (то есть $w_0$ соответствует свободному члену). Искать $w$ будем с помощью градиентного спуска:
$$ w^{(k+1)} = w^{(k)} - \alpha \nabla_w Q(w, X) $$В случае полного градиентного спуска $\nabla_w Q(w, X)$ считается напрямую (как есть, то есть, используя все объекты выборки). Длину шага $\alpha > 0$ в рамках данного задания предлагается брать равной некоторой малой константе. Градиент по объекту $x_i$ считается по следующей формуле:
$$ \nabla_w Q(w, x_i) = - \frac{y_i x_i}{1 + \exp(y_i \langle w, x_i \rangle)} + \lambda_2 w $$На самом деле неправильно регуляризировать свободный член $w_0$ (то есть при добавлении градиента для $w_0$ не надо учитывать слагаемое с $\lambda_2$). Но в рамках этого задания мы не обращаем на это внимания и работаем со всеми вектором весов одинаково.
В качестве критерия останова необходимо использовать (одновременно):
Инициализировать веса можно случайным образом или нулевым вектором.
Реализуйте обучение логистической регрессии. Для удобства ниже предоставлен прототип с необходимыми методами. В loss_history необходимо сохранять вычисленное на каждой итерации значение функции потерь.
from sklearn.base import BaseEstimator
class LogReg(BaseEstimator):
def __init__(self, lambda_2=1.0, tolerance=1e-4, max_iter=100, alpha=0.005):
"""
lambda_2: L2 regularization param
tolerance: for stopping gradient descent
max_iter: maximum number of steps in gradient descent
alpha: learning rate
"""
self.lambda_2 = lambda_2
self.tolerance = tolerance
self.max_iter = max_iter
self.alpha = alpha
self.w = None
self.loss_history = None
def fit(self, X, y):
"""
X: np.array of shape (l, d)
y: np.array of shape (l)
---
output: self
"""
if type(X) is pd.core.series.Series:
X = X.values
if type(y) is pd.core.series.Series:
y = y.values
self.loss_history = []
shape = X.shape
self.w = np.zeros(shape[1])
for step in range(self.max_iter):
self.w_curr = self.w - self.alpha * self.calc_gradient(X, y)
self.loss_history.append(self.calc_loss(X,y))
if np.linalg.norm(self.w_curr - self.w) < self.tolerance:
break
self.w = self.w_curr
return self
def predict_proba(self, X):
"""
X: np.array of shape (l, d)
---
output: np.array of shape (l, 2) where
first column has probabilities of -1
second column has probabilities of +1
"""
if self.w is None:
raise Exception('Not trained yet')
proba = sigma(np.dot(X_test, self.w))
return np.array([proba, 1 - proba]).T
def calc_gradient(self, X, y):
"""
X: np.array of shape (l, d) (l can be equal to 1 if stochastic)
y: np.array of shape (l)
---
output: np.array of shape (d)
"""
g = 0
for i in range(X.shape[0]):
g += y[i] * X[i] * sigma(y[i] * self.w.dot(X[i]))
g /= X.shape[0]
g += self.lambda_2 * self.w
return g
def calc_loss(self, X, y):
"""
X: np.array of shape (l, d)
y: np.array of shape (l)
---
output: float
"""
return sum([sigma(y[i] * self.w.dot(X[i])) for i in range(X.shape[0]) ]) / X.shape[0] + self.lambda_2 / 2 * (np.linalg.norm(self.w) ** 2)
lr = LogReg()
lr.fit(X_train, y_train)
y_predict = lr.predict_proba(X_test)[:,1]
print("ROC-AUC: ", roc_auc_score(y_test, y_predict))
print("Accuracy: ", accuracy_score(y_test, np.round(y_predict)))
plt.figure(figsize=(20,10))
plt.grid(True)
plt.title('LogReg')
plt.xlabel('Итерация')
plt.ylabel('Loss')
plt.plot(range(len(lr.loss_history)), lr.loss_history)
plt.show()