
Как бы мы предсказывали настроение новых обзоров?
В предыдущей части моего письма я попытался охватить шаги для анализа текстовых данных. Анализ, проведенный в предыдущей части, носит описательный характер. В этой части мы попытаемся объяснить, как мы будем предсказывать настроения новых обзоров. Для этой части я многое изучаю из этой Статьи на 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']))

Окончательная проверка и заключение
Мы можем сказать, что модель хорошо показала себя на тестовых данных, но мы можем улучшить ее, выполнив настройку гиперпараметров, установив значение отсечки вероятности и т. д. Мы запускаем модель на данных проверки, чтобы еще раз проверить, как она работает с невидимыми данными.

Вы можете видеть, что отчет о классификации ничем не отличается от отчета о классификации для тестовых данных.
Это письмо, хотя и далекое от совершенства, должно было дать грубый пошаговый процесс того, что мы должны делать, когда хотим манипулировать текстовыми данными. Есть много методов манипулирования текстом, которые я сам до конца не понял и не пытаюсь изучить, но я надеялся, что смогу поделиться этими небольшими знаниями с другими новичками в НЛП.
Как обычно, я надеюсь, что вы, ребята, оставите мне какие-либо советы относительно моего подхода в разделе комментариев, аплодируйте, если вы найдете этот пост полезным. :)
Увидимся в следующий раз!