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