Как бы мы предсказывали настроение новых обзоров?
В предыдущей части моего письма я попытался охватить шаги для анализа текстовых данных. Анализ, проведенный в предыдущей части, носит описательный характер. В этой части мы попытаемся объяснить, как мы будем предсказывать настроения новых обзоров. Для этой части я многое изучаю из этой Статьи на Medium.
Ссылка на предыдущую часть написания:
https://sea-remus.medium.com/text-processing-and-classification-intro-part-1-sentiment-analysis-7e22a83e1c4
Краткий обзор части 1
Данные, которые мы хотели бы использовать, имеют следующие столбцы:
Мы понимаем, что настроения в обзорах на самом деле классифицируются по столбцу «Оценка» (что делает моделирование фактически излишним), но в этом тексте предполагается, что у нас нет столбца «Оценка».
Анализируя наши текстовые данные, мы знаем, что на рынке присутствуют 2 настроения. Мы классифицируем эти настроения как положительные и отрицательные. Мы будем использовать стоп-слова для удаления лишних слов, в число которых также войдут «br», «href», «amazon», «продукт», «один», «найти», «вкус», «вкус», «хороший», « купить», «сделать» и «кофе», так как в результате анализа мы обнаружили, что эти слова также являются излишними.
Из предыдущего анализа и EDA мы также окончательно пришли к выводу, что не будем использовать столбец «Сводка», поскольку некоторые пользователи не предоставили сводку своих отзывов.
Предварительная обработка данных
Сначала мы импортируем необходимые пакеты и получаем данные, используя следующий код:
#essentials import pandas as pd import numpy as np import sqlite3 import matplotlib.pyplot as plt import seaborn as sns #obtaining the data con = sqlite3.connect('database.sqlite') raw = pd.read_sql_query("select ProductId,ProfileName, HelpfulnessNumerator, HelpfulnessDenominator, Time, Text, case when Score >= 4 then 1 else 0 end Sentiment from Reviews", con) con.close()
Прежде чем мы начнем обрабатывать данные, мы разделим данные на 3 части, которые будут данными обучения, тестовыми данными и данными проверки. На самом деле цель состоит в том, чтобы различать тестовые данные, используемые для настройки гиперпараметров (если они есть), и тестовые данные, используемые для имитации новых данных реального мира.
from sklearn.model_selection import train_test_split xtrain, xsplit, ytrain, ysplit = train_test_split(raw.drop('Sentiment',axis = 1), raw.Sentiment, test_size = 0.3, random_state = 42) xtest, xval, ytest, yval = train_test_split(xsplit,ysplit, test_size = 0.5, random_state = 42)
Мы также определили бы функцию токенизации. Мы бы токенизировали текст после удаления избыточных слов (стоп-слов) и лематизировали текст после токенизации.
Итак, для тех, кто не имеет никакого представления об обработке текста при чтении этого письма:
Токенизация в обработке естественного языка в основном означает разделение предложений (последовательности слов) на слова (или токены). На самом деле мы можем сделать это либо с помощью встроенного строкового метода Python .split(), либо с помощью word_tokenize() из пакета NLTK.
Лемматизация — это процесс получения корня слова. Альтернативой лемматизации является стемминг, который заключается в простом выделении слов. Идея обоих состоит в том, чтобы сокращать слова так, чтобы, если бы мы нашли два слова или более, которые имеют схожие значения, мы считали бы их одним и тем же. Например, у нас есть список таких слов, как [‘играть’, ‘играть’, ‘играть’]. При вычленении или лематизации мы получили бы [‘играть’, ‘играть’, ‘играть’]. В некоторых случаях стемминг лучше, чем лемматизация, а в других случаях все наоборот. Думайте об этом как о нормализации и стандартизации числовых данных.
def tokenize(txt): import re from wordcloud import STOPWORDS import nltk from nltk.tokenize import word_tokenize from nltk.stem.wordnet import WordNetLemmatizer stpwrd = set(STOPWORDS) #we then add frequent irrelevant words discovered before stpwrd.update(['br','href','amazon','product','one', 'find','taste','flavor','good','buy', 'make','coffee']) removeapos = txt.lower().replace("'",'') #remove any apostrophe text = re.sub(r"[^a-zA-Z0-9\s]"," ", removeapos) #sub weird char to space words = word_tokenize(text) lemma = [WordNetLemmatizer().lemmatize(w) for w in words if w not in stpwrd] return lemma
После определения функции токенизации мы определяем функцию для обработки отсутствующих значений и создания новых переменных.
def preprocess_engineer(data,train = True): #this is to handle missing values before using the data data['ProfileName'] = data['ProfileName'].apply(lambda x: 'Anonymous' if (x == 'nan')|(x == 'NaN')| (x == 'N/A')|(x == '0')| (x == '')|(x == '-1')| (x == 'null')|(x == 'Null')| (x == 'NA')|(x == 'na')| (x == 'none')|(x == 'unknown') else x) #this is to simulate that after data train we would #face a number of new reviews if train == True: countprof = data.ProfileName.value_counts().reset_index() countprof.columns = ['ProfileName','ProfReviewCount'] countprod = data.ProductId.value_counts().reset_index() countprod.columns = ['ProductId','ProdReviewCount'] countspam = data.groupby(['ProductId', 'ProfileName']).size().reset_index() countspam.columns = ['ProductId','ProfileName','SpamReviewCount'] engineered = pd.merge( pd.merge( pd.merge(data,countprof,how='left',on='ProfileName'), countprod,how = 'left', on = 'ProductId'), countspam, how = 'left',on =['ProductId', 'ProfileName']) return engineered if train == False: #this is from train countproftrain = xtrain.ProfileName.value_counts().reset_index() countproftrain.columns = ['ProfileName','ProfReviewCounttrain'] countprodtrain = xtrain.ProductId.value_counts().reset_index() countprodtrain.columns = ['ProductId','ProdReviewCounttrain'] countspamtrain = xtrain.groupby(['ProductId', 'ProfileName']).size().reset_index() countspamtrain.columns = ['ProductId', 'ProfileName', 'SpamReviewCounttrain'] #this is from the newly introduced data countprof = data.ProfileName.value_counts().reset_index() countprof.columns = ['ProfileName','ProfReviewCountadd'] countprod = data.ProductId.value_counts().reset_index() countprod.columns = ['ProductId','ProdReviewCountadd'] countspam = data.groupby(['ProductId', 'ProfileName']).size().reset_index() countspam.columns = ['ProductId','ProfileName','SpamReviewCountadd'] #this is to add newly introduced data with train #ProfileName countproffinal = countprof.merge(countproftrain, how = 'left', on ='ProfileName') countproffinal.fillna(0,inplace = True) countproffinal['ProfReviewCount'] = countproffinal.ProfReviewCounttrain + countproffinal.ProfReviewCountadd countproffinal.drop(['ProfReviewCounttrain', 'ProfReviewCountadd'], axis = 1,inplace = True) #ProductId countprodfinal = countprod.merge(countprodtrain, how = 'left', on = 'ProductId') countprodfinal.fillna(0,inplace = True) countprodfinal['ProdReviewCount'] = countprodfinal.ProdReviewCounttrain + countprodfinal.ProdReviewCountadd countprodfinal.drop(['ProdReviewCounttrain', 'ProdReviewCountadd'], axis = 1,inplace = True) #SpamReviewCount countspamfinal = countspam.merge(countspamtrain,how = 'left',on = ['ProductId','ProfileName']) countspamfinal.fillna(0,inplace = True) countspamfinal['SpamReviewCount'] = countspamfinal.SpamReviewCounttrain + countspamfinal.SpamReviewCountadd countspamfinal.drop(['SpamReviewCounttrain', 'SpamReviewCountadd'], axis = 1,inplace = True) engineered = pd.merge( pd.merge( pd.merge(data,countproffinal, how='left',on='ProfileName'), countprodfinal, how = 'left', on = 'ProductId'), countspamfinal, how = 'left', on = ['ProductId','ProfileName']) return engineered
Подводя итог приведенной выше функции, в основном идея состоит в том, чтобы заполнить недостающие значения в столбце ProfileName, а также создать новые переменные, которые говорят о количестве отзывов, сделанных определенным клиентом, количестве отзывов, полученных определенным продуктом, и количестве нескольких отзывов, сделанных одним и тем же покупателем на определенный продукт.
Завершите создание функций, теперь мы применим функцию.
xtrainpreprocessed = preprocess_engineer(xtrain,train = True) xtestpreprocessed = preprocess_engineer(xtest,train = False) xvalpreprocessed = preprocess_engineer(xval,train = False)
Обратите внимание, что мы не использовали токенизацию. Я займусь этим чуть позже.
Текстовые данные в качестве входных данных модели
Прежде чем мы приступим к моделированию, мы должны преобразовать текстовые данные в числовые данные. На данный момент я изучил 2 основных способа сделать это: первый — Bag of Words (BOW), а второй — встраивание Word (Word2Vec, W2V). сильный>. Я не собираюсь глубоко погружаться в теории и то, как это работает, вместо этого я просто расскажу вам, что это такое, поскольку это письмо является лишь введением.
- По сути, BOW пытается преобразовать слова в ваших текстовых данных в числа, которые показывают, как часто слово появляется в ваших данных. Частота появления слова может быть представлена количеством слов (CountVectorizer) или весом слова (TF-IDF). Вы можете попытаться найти их в Google, чтобы лучше понять их.
- Word2Vec является альтернативой BOW. Word2Vec, как и его название, пытается преобразовать слово в ваших данных в векторы. Word2Vec использует нейронную сеть для создания векторов, которые представляют наши исходные слова. Есть два способа сделать это, первый — CBOW (Continuous Bag of Words) и Skip-gram.
Скажем, у нас есть это предложение:
я пойду по магазинам
Чтобы объяснить это кратко,
CBOW пытается предсказать слово, глядя на другие слова вокруг (пример: output = «собираюсь», input = [«я», «иду», «шопинг»]).< br /> Skip-gram пытается предсказать другие слова вокруг определенного слова (пример: ввод = «собираюсь», вывод = [«я», «иду», «за покупками»])
Мы будем использовать Word2Vec для преобразования текстовых данных в числовые, потому что одним из недостатков метода Bag of Words является то, что он часто сталкивается с проблемами размерности.
Представьте, что у нас есть кадр данных, как указано выше. Количество слов, которые существуют в ваших данных, становится количеством столбцов в вашем фрейме данных. Фрейм данных выше предполагает, что вы используете CountVectorizer в качестве подхода Bag of Words, поэтому вы можете видеть, что, скажем, для первой строки (первых текстовых данных) ваших данных есть 3 «получить» слова, 1 «это» слово, 4 «out». ” слова и 3 слова “can”. Теперь вы можете себе представить, насколько большими будут входные данные, если мы воспользуемся подходом Bag of Words.
С Word2Vec мы можем справиться с этим, вычислив среднее (среднее) векторов слов в текстовых данных.
Воображаемый кадр данных выше показывает вывод Word2Vec. Вектор нулей означает, что слово не появляется в текстовых данных (например, в 3-х текстовых данных нет «это» и «можно»). Обычно векторы каждой точки данных усредняются. После усреднения количество столбцов уже не равно количеству слов. Вместо этого количество столбцов будет равно размеру вектора.
Преобразование текста в числа
Разобравшись с краткими понятиями, мы теперь будем кодировать сразу несколько вещей.
#function to average def AverageVectors(wordvectmod,oritokenizedtxt): todict = dict(zip(wordvectmod.wv.index_to_key, wordvectmod.wv.vectors)) #if a text is empty we should return a vector of zeros #with the same dimensionality as all the other vectors dim = len(next(iter(todict.values()))) return np.array([np.mean([todict[w] for w in words if w in todict] or [np.zeros(dim)],axis = 0) for words in oritokenizedtxt ]) w2v = Word2Vec(xtrainpreprocessed.Text.apply(tokenize), sg = 0, hs = 1, seed = 42, vector_size = 128) #convert to vect average xtrainvect = AverageVectors(w2v,xtrainpreprocessed.Text.apply(tokenize)) xtestvect = AverageVectors(w2v,xtestpreprocessed.Text.apply(tokenize)) xvalvect = AverageVectors(w2v,xvalpreprocessed.Text.apply(tokenize))
Что я сделал в приведенном выше коде, так это создал функцию, которая примет созданную нами модель Word2Vec, а затем усреднит векторы слов для каждой точки данных. После этого мы применяем функцию к имеющимся у нас текстовым данным.
После того, как векторы из Word2Vec усреднены, мы конвертируем их в фрейм данных, а затем добавляем другие функции, которые могут быть полезны для определения настроений клиентов.
#add the remaining features def vecttodata(aftervec,beforevec): df = pd.DataFrame(aftervec) df['HelpfulnessNumerator'] = beforevec.reset_index(drop=True)['HelpfulnessNumerator'] df['HelpfulnessDenominator'] = beforevec.reset_index(drop=True)['HelpfulnessNumerator'] df['Time'] = beforevec.reset_index(drop=True)['Time'] df['ProfReviewCount'] = beforevec.reset_index(drop=True)['ProfReviewCount'] df['ProdReviewCount'] = beforevec.reset_index(drop=True)['ProdReviewCount'] df['SpamReviewCount'] = beforevec.reset_index(drop=True)['SpamReviewCount'] return df xtraindf = vecttodata(xtrainvect,xtrainpreprocessed) xtestdf = vecttodata(xtestvect,xtestpreprocessed) xvaldf = vecttodata(xvalvect,xvalpreprocessed)
Модель классификации обучения
При этой возможности я хотел бы использовать классификатор XGBoost для построения модели классификации. Время обучения относительно велико, и на завершение тренировочного процесса ушло около часа. Существуют однородные (каждый атрибут/функция имеет схожую природу с одним и другим, например текст, изображение, видео и т. д.) и гетерогенные (каждый атрибут/функция имеет различную природу для одного и другого, например, возраст с доходом, смешанный набор данных и т. д.) данные. Я пытался использовать XGBoost, потому что он хорош для разнородных данных, но если читатели захотят попробовать построить модель классификации, используя в качестве входных данных только текстовые данные, я рекомендую использовать нейронные сети, потому что это, вероятно, намного быстрее и предпочтительнее для однородных наборов данных.
В предыдущей части вы также можете видеть, что количество положительных и отрицательных настроений несбалансировано, поэтому для устранения дисбаланса мы можем либо выполнить повышающую дискретизацию данных, либо вычислить выборочные веса для обучающих данных. Я выбираю последнее. Ниже приведен код, который я написал для построения и обучения модели классификации настроений.
rom xgboost import XGBClassifier from sklearn.utils.class_weight import compute_sample_weight sample_weights = compute_sample_weight( class_weight='balanced', y=ytrain ) xgb = XGBClassifier(booster = 'gbtree', objective = 'binary:logistic', eval_metric='auc', seed = 42, use_label_encoder = False, num_parallel_tree = 10, n_estimators = 50, verbosity = 2) xgb.fit(xtraindf,ytrain,sample_weight = sample_weights) pred = xgb.predict(xtestdf) from sklearn.metrics import confusion_matrix,classification_report print(classification_report(pred,ytest, target_names = ['Negative','Positive']))
Окончательная проверка и заключение
Мы можем сказать, что модель хорошо показала себя на тестовых данных, но мы можем улучшить ее, выполнив настройку гиперпараметров, установив значение отсечки вероятности и т. д. Мы запускаем модель на данных проверки, чтобы еще раз проверить, как она работает с невидимыми данными.
Вы можете видеть, что отчет о классификации ничем не отличается от отчета о классификации для тестовых данных.
Это письмо, хотя и далекое от совершенства, должно было дать грубый пошаговый процесс того, что мы должны делать, когда хотим манипулировать текстовыми данными. Есть много методов манипулирования текстом, которые я сам до конца не понял и не пытаюсь изучить, но я надеялся, что смогу поделиться этими небольшими знаниями с другими новичками в НЛП.
Как обычно, я надеюсь, что вы, ребята, оставите мне какие-либо советы относительно моего подхода в разделе комментариев, аплодируйте, если вы найдете этот пост полезным. :)
Увидимся в следующий раз!