В этой статье описывается, как можно интерпретировать отзывы Amazon и классифицировать их обратно на низкий или высокий рейтинг, используя подход векторизации набора слов для создания настроения на основе комментария или обзора, оставленного покупателем.

Мы проводим анализ комментариев, оставленных покупателями определенного продукта на Amazon.com, и применяем методы НЛП к отзывам в сочетании с общей оценкой (от 1 до 5), предоставленной покупателем.

Для целей этого анализа был использован один продукт, указанный ASIN (стандартный идентификационный номер Amazon), аналогичный SKU в розничной торговой точке. Название этого продукта, продаваемого на Amazon, — «Цифровой гигрометр и комнатный термометр AcuRite 00613, предварительно откалиброванный датчик влажности».

На момент проведения этого анализа он продавался менее чем за 15 долларов в категории продуктов «Промышленные и научные». И на amazon.com можно было просмотреть 21 тысячу оценок. Набор данных, который используется из архива, полученного для этого проекта, содержит 1229 обзоров конкретно для этого продукта, а также подробный текстовый обзор, а также рейтинг продукта.

Цель этого анализа состоит в том, чтобы предсказать, какой рейтинг может дать пользователь на основе настроений, выраженных в комментариях. Поскольку рейтинги Amazon оцениваются по шкале от 1 до 5, для простоты эта задача с 5 классами была создана как задача с 2 классами путем объединения оценок {1,2,3} в рейтинг «Низкий», а оценок {4 ,5} в рейтинг «Высокий».

Как в этом анализе применяется обработка естественного языка:

В этом анализе есть три основных шага.

(1) Сбор данных и предварительная обработка текстовых данных.

(2) Векторизация данных с использованием набора слов.

(3) Применение наивного байесовского классификатора к этим векторизованным текстовым данным для создания прогностической модели, которая принимает комментарий и предсказывает настроение пользователя как низкое или высокое.

Предварительный просмотр результатов модели

Классификатор очень точен в прогнозировании как «высоких», так и «низких» рейтингов с оценкой F1 0,96 и 0,97 для каждого класса соответственно. Изучив фактические комментарии, мы видим, что предиктор хорошо справился с определением таких настроений, как «действительно хорошо работает», «работает так, как рекламируется», «отличное дополнение» к высокому рейтингу и приписывая такие настроения, как "зачем писать отзывы", "далеко", "работает нормально в течение месяца, но" к Низкому рейтингу. Хотя вы увидите, как на самом деле работает модель в этом анализе, он не выводит настроение на основе последовательности слов, как описано выше в кавычках, а скорее на основе количества слов. М

Мы завершаем этот анализ с некоторыми ограничениями и улучшениями (и если вы угадали Трансформеры как варианты — вы правы!). Итак, давайте рассмотрим:

Шаг 1. Сбор и подготовка данных

Данные, используемые для этого анализа, отличаются от стандартного «корпоративного» набора данных тем, что исходные данные, принадлежащие и отслеживаемые Amazon.com, хранятся в виде табличных данных с 12 атрибутами. Интерес для этого анализа представляют лишь немногие атрибуты, которые были сохранены. Он включает в себя следующее:

· reviewText — фактические текстовые комментарии, введенные покупателем.

· summaryStr — Сводка комментариев, предоставленных покупателем.

· в целом — исходная оценка по шкале от 1 до 5.

· ASIN — это уникальный идентификатор продукта, как отмечалось ранее.

Данные, используемые для этого анализа, были получены из архива на домене ucsd.edu. Исходный файл находится в формате JSON и имеет 77 071 отзыв.

В исходном файле, полученном для анализа, имелись рейтинги и комментарии для ряда продуктов категории «Промышленные и научные». Мы выбрали один конкретный ASIN (продукт), для которого можно выполнить этот анализ. Группировка по операциям выполнялась с использованием функции valuecounts() для получения количества записей на основе ASIN, и для этого анализа использовалась та, у которой больше всего записей. Причина выбора ASIN с наибольшим количеством записей заключалась в том, чтобы мы могли работать с достаточно большим количеством записей для обучения и тестирования классификатора. Окончательно обработанный набор данных имел форму 1229 x 4. 4 члена в DataFrame имеют тип «объект», и поэтому их необходимо преобразовать в форматы string (для просмотра) и int (для оценки).

from urllib.request import urlopen
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.metrics import confusion_matrix
import nltk
import os
import json
import gzip
import pandas as pd
import wget
import matplotlib.pyplot as plt
import seaborn as sns
import string
url="http://deepyeti.ucsd.edu/jianmo/amazon/categoryFilesSmall/Industrial_and_Scientific_5.json.gz"
textfile=wget.download(url)

data = []
with gzip.open(textfile) as f:
    for l in f:
        data.append(json.loads(l.strip()))
    
# total length of list, this number equals total number of products
print(len(data))

# first row of the list
print(data[0])


#quick review of data structures - list, df
df = pd.DataFrame.from_dict(data) #convert dictionary  to dataframe

#check length of dataframe
print(len(df))

#check content of first record
df.iloc[0]
      
#check comments of first record
df.reviewText.iloc[0]

#check summary stats of the dataframe
df.asin.describe()

#alternative way to find the most frequently occuring ASIN to identify candidate product for analysis
df['asin'].value_counts() #shows B0013BKDO8  has the most ratings

asindf=df[df['asin']=='B0013BKDO8'] # keep product of interest

#check shape of new df
asindf.shape #reduced to a single ASIN as above.

asindf=asindf[['asin','overall','reviewText','summary']] # keep four cols

Базовый исследовательский анализ данных

Вот краткий обзор того, как выглядит этот набор данных. Последний столбец, названный newrating, был создан путем дополнительной обработки данных, описанной ниже.

Изначально сохранены только четыре переменные, поэтому EDA выполняется довольно быстро. Во-первых, давайте посмотрим textLength.

На приведенной выше диаграмме показано распределение длины текстовых комментариев, которые покупатели предоставляют в дополнение к своим рейтингам. А вот так выглядит раздача. Средняя длина строки составляет 153 символа со стандартным отклонением 239. Это сильно асимметричное распределение.

import klib as kl
kl.dist_plot(finaldf.textLength)

Интересно, что более длинный текст связан с более высокими оценками, как видно из этой диаграммы.

plt.figure(figsize=(15, 5))
sns.boxplot(data=finaldf, x="rating", y="textLength")

Далее мы увидим рейтинг.

Это интересное наблюдение, рейтинги сильно смещены в сторону 5, а затем 4. Это должен быть один из тех продуктов, которые действительно хорошо работают для большинства покупателей. Я, конечно, могу поручиться за это, потому что у меня есть один из них, и он всегда стоит у моей кровати. Вот изображение устройства, которое я купил много лет назад (до сих пор хорошо работает!)

Корреляция длины текста с рейтингом очень низкая (0,18). Как мы также можем видеть ниже, почти ничего не заметно о влиянии длины комментария на 5 отдельных рейтингов.

Мы бинаризируем рейтинги с помощью нового атрибута newrating, который имеет два значения {Низкий, Высокий}. Рейтинги 1,2,3 помещаются в корзину под Низким. Рейтинги 4,5 попадают в категорию High.

# derive new rating
def set_new_rating (row):
   if row['rating'] == 1 :
      return 'Low'
   if row['rating'] == 2 :
      return 'Low'
   if row['rating'] ==3 :
      return 'Low'
   if row['rating'] ==4 :
      return 'High'
   return 'High'


finaldf['newrating'] = finaldf.apply(set_new_rating, axis=1)

Шаг 2. Предварительная обработка текста для обработки естественного языка

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

а. удаление знаков препинания при чтении комментариев.

б. удаление английских стоп-слов.

в. возвращает список чистых токенизированных слов.

Текст обзора до и после этой обработки выглядит так, как показано ниже:

Следующий блок кода показывает предварительную обработку текста:

from nltk.tokenize import word_tokenize


def text_processing(varText):
    review = [char for char in varText if char not in string.punctuation]
    review= ''.join(review)
    return [word for word in review.split() if word.lower() not in stopwords.words('english')]

Для удаления пунктуации каждый символ сверяется с String.punctuation — постоянной строкой, которая является свойством строкового объекта со значением ''!#$%&\'()*+,-./:;‹ =›?@[\\]^_`{|}~'
Если совпадение не найдено, этот символ сохраняется и снова объединяется в строку. Поэтому эта строка была снова разделена для создания слов, чтобы затем сравнить их со стоп-словами английского языка, импортированными из NLTK.corpus. Примеры стоп-слов включают часто используемые в английском языке слова, которые обычно не имеют веса при анализе большинства текстов, такие как «я», «мне», «мне», «мы» и т. д. Каждый текст обзора был обработан для создания токенов. таким образом

Мы могли бы также применить выделение корней и лемматизацию (обозначение похожих слов до одного слова, например, сделал, делал, сделал, рассматривается как одно и то же слово). Это позволяет помечать похожие слова как одно и то же слово, что позволяет более точно рассчитать весовой коэффициент присутствия таких слов. Однако в этом упражнении он был исключен.

Векторизация

Каждый отзыв преобразуется в виде списка токенов. Каждый обзор теперь преобразуется в вектор для обучения модели классификатора. Применим к этому тексту модель «мешок слов (бантик)». Мы используем CountVectorizer Scikit Learn для преобразования слова в каждом из комментариев в матрицу количества токенов (а не сам токен, поскольку мы уже достигли этого на предыдущем шаге). Это позволяет создать словарь из 2326 слов в наборе данных и подсчитывает появление каждого слова. Этот же шаг применяется к отдельным комментариям для создания матрицы 1229 записей X 2326 слов. Получаем разреженную матрицу такой размерности с индексом разреженности 0,57%.

Индивидуальная запись представлена ​​здесь для иллюстрации. Комментарий 8-го рецензента в наборе данных выглядит следующим образом.

import textwrap
reviewer8 = finaldf['reviewTextStr'][7]
print('\n'.join(textwrap.TextWrapper(50).wrap(reviewer8)))
I love this little unit. It helps me when I am
feeling cold to re-assure me that I indeed don't
need to cut on the heater, and that I just need to
wait for my body heat to normalize to the
environment. Also, it has helped when I
accidentally leave the heater on during a nap. I
can say that, thanks to this unit's Low Temp /
High Temp recording feature, I was able to find
out that I had left the heater on by mistake and
achieved a whopping 95 degrees (F) out of the
space heater.

Код векторизации подсчета следует ниже вместе с векторным представлением (усеченным для краткости) Reviewer8.

bow_transformer = CountVectorizer(analyzer=text_processing).fit(finaldf['reviewTextStr'])
# Print total number of vocabulary words
print (len(bow_transformer.vocabulary_))
bow_transformer.vocabulary_ 

Словарь — это весь список уникальных слов, найденных в предварительно обработанном тексте по количеству вхождений. Легко увидеть (но не всегда верно), какие слова могут сопровождать более высокий рейтинг по сравнению с низким рейтингом. Некоторые примеры выделены красным. Кроме того, это подсчет одного токена (одного слова). Мы не смотрим на n-грамму (последовательность n слов) для оценки тональности. Например, мы замечаем, что слово ниже «счастливый» в настоящее время взято изолированно. Настроение может легко указать в отрицательном направлении и понизить рейтинг, который мы пытаемся получить, если «счастливому» предшествует слово «не». Так что это определенно улучшение, которое можно сделать с помощью токенизации bi-gram или n-gram.

bow8 = bow_transformer.transform([reviewer8])
print(bow8)

Это создает сорок токенов из исходного комментария, найденного в записи reviewer8. Некоторые примеры показаны ниже.

#check  the English word for the feature in below vectors

print (bow_transformer.get_feature_names_out()[386]) #Temp
print (bow_transformer.get_feature_names_out()[1153]) #heater

Теперь, когда мы сгенерировали словарь, нам нужно установить весь корпус предварительно обработанных отзывов в виде матрицы токенов. Естественно, мы ожидаем, что количество строк в матрице будет таким же, как количество отзывов, а количество столбцов в матрице будет таким же, как длина словарного запаса. Это приводит нас к форме 1229 записей X 2326 слов, которую мы уже установили выше.

Это код для генерации матрицы счетных векторов.

comments_bow = bow_transformer.transform(finaldf['reviewTextStr'])
print('Shape of Sparse Matrix: ', comments_bow.shape)
print('Amount of Non-Zero occurences: ', comments_bow.nnz)
print('sparsity: %.2f%%' % (100.0 * comments_bow.nnz / (comments_bow.shape[0] * comments_bow.shape[1])))

Теперь, когда мы заложили числовую основу, столь необходимую для вычислений, нам все еще нужно прийти к метрике, которая говорит нам о важности слова по отношению к другому. Это именно то, что делает TFIDF. TFIDF расшифровывается как частота термина, обратная частоте документа. Это произведение двух терминов — частоты термина и обратной частоты документа. Частота термина — это отношение количества раз, которое слово встречается в документе (в нашем случае, весь набор рецензий, использованных для этого анализа) к общему количеству слов. Таким образом, чем выше соотношение, тем важнее термин. Принимая во внимание, что обратная частота документов — это логарифм отношения общего количества документов к количеству документов, в которых встречается термин. Откуда взялось обратное слово в IDF? Это исходит из идеи, что этот термин также может быть выражен как логарифм, обратный соотношению документов, в которых встречается термин. Семантически оно имеет то же значение, что и наше первое формальное определение IDF. По сути, это все равно, что сказать, что х тоже является обратным 1/х.

Мы проверяем значения TFIDF некоторых слов, которые мы считаем важными.

tfidf_transformer = TfidfTransformer().fit(comments_bow)

print(tfidf_transformer.idf_[bow_transformer.vocabulary_['recommend']])
print(tfidf_transformer.idf_[bow_transformer.vocabulary_['great']])
print(tfidf_transformer.idf_[bow_transformer.vocabulary_['fantastic']])
print(tfidf_transformer.idf_[bow_transformer.vocabulary_['amazing']])
print(tfidf_transformer.idf_[bow_transformer.vocabulary_['love']])
print(tfidf_transformer.idf_[bow_transformer.vocabulary_['bad']])
comments_tfidf = tfidf_transformer.transform(comments_bow)

Напомним, наша цель состояла в том, чтобы взять все текстовые рейтинги и посмотреть, сможем ли мы классифицировать их по высокому или низкому рейтингу. Итак, для этого мы проверяем распределение нашего полученного рейтинга из поля newrating.

newaxisvals = finaldf['newrating'].value_counts()
plt.figure(figsize=(30, 10))
plt.subplot(131)
plt.bar(newaxisvals.index,newaxisvals.values)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show()

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

distratio = finaldf['newrating'].value_counts()[0] /  finaldf['newrating'].value_counts()[1] 
print(distratio) # gives a value of 6.68125

Количество записей с высоким рейтингом в 6 раз больше, чем с низким рейтингом. Другими словами, отношение класса большинства к классу меньшинства составляет 6x. Итак, прежде чем мы обучим классификатор, нам нужно сбалансировать эти данные. В противном случае классификатор будет иметь склонность неправильно прогнозировать рейтинги с высоким рейтингом в большей степени, чем с низким рейтингом.

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

from imblearn.over_sampling import RandomOverSampler
oversampler = RandomOverSampler(sampling_strategy='minority', random_state=42)
newx, newy = oversampler.fit_resample(comments_tfidf,finaldf['newrating'])
#when you print newx you will see a sparse matrix of float64 dtype with 2138
#obsevations (upsampled from previously 1229 observations)

И быстрая проверка показывает, что данные теперь сбалансированы.

newy.value_counts()
newaxisvals = newy.value_counts()
plt.figure(figsize=(30, 10))
plt.subplot(141)
plt.bar(newaxisvals.index,newaxisvals.values)
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.show() 

Теперь, когда у нас есть данные, готовые для обучения модели, мы разделяем их на тесты и обучаем, а затем обучаем модель, а затем проверяем, как мы продвигаемся в прогнозировании, сравнивая прогнозируемый рейтинг с фактическим рейтингом.

MultinomialNaiveBayesClassifier — хорошая модель для этой работы, поскольку она принимает разреженную матрицу в качестве аргумента в качестве X, что является именно тем, что мы настроили, и принимает массив для Y. В нашем случае это настроение (ранжирование).

comment_train, comment_test, rating_train, rating_test = train_test_split(newx, newy, test_size=0.3)
rating_detection = MultinomialNB().fit(comment_train, rating_train)
rating_pred = rating_detection.predict(comment_test)
print(classification_report(rating_test, rating_pred))

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

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

Возможно, стоит повторить важность балансировки данных. Если мы не сбалансируем классы, пострадает оценка F1 для низкого рейтинга, что даст нам значение 0,5. Если вам интересно узнать, так ли это, вы можете провести этот эксперимент без передискретизации. Без уравновешивания (соотношение высокого и низкого рейтинга 6:1 в данном случае) это смещение искажает общую точность до 0,91, показывая, что модель очень точна, хотя на самом деле это не так.

#print confusion matrix
cmat = confusion_matrix(rating_test, rating_pred)
plt.figure(figsize=(10,10))
sns.set(font_scale=1.5)
sns.heatmap(cmat.T, square=True, annot=True, fmt='d', cbar=False,
            xticklabels=['High','Low'], yticklabels=['High','Low'])

plt.xlabel('True Rating')
plt.ylabel('Predicted Rating')

В приведенной ниже матрице путаницы показаны истинные положительные, истинные отрицательные, ложноположительные и ложноотрицательные результаты.

Ограничения и улучшения

Есть много улучшений, которые можно сделать с помощью этого эксперимента. Мы использовали количество слов в качестве вектора для текстового представления. Этот эксперимент был проведен для определенного типа нишевого продукта среди миллионов товаров, продаваемых на Amazon. Этот особый метод векторизации с использованием метода мешка слов (BOW) вводит разреженность экспоненциально по мере того, как вводится больше словарного запаса. Это означает, что если бы обзоров было намного больше или мы открыли бы этот анализ для более широкого набора продуктов, мы бы столкнулись с вычислительными проблемами из-за разреженности. BOW также не принимает во внимание порядок слов и значение слова. Хотя это более простой метод, он широко использовался в прошлом для классификации документов и текстов.

Есть лучшие способы обработки классификации текста. Вложения слов предлагают более плотное представление, чем то, что мы видели. Поэтому он вычислительно эффективен. Комбинируя встраивания слов с нейронными сетями, можно классифицировать различные части текста, такие как документы (или, в нашем случае, обзоры).

Наконец, было бы упущением сказать, что Transformers — это еще один и более новый способ обработки классификации текста. Для этого мы можем использовать предварительно обученную модель, такую ​​как BERT, GPT и т. д. Я расскажу об этом в следующих статьях.

Хотите продолжить разговор? Если вы еще не подписаны на мои электронные письма, вот пять причин, по которым вы можете это сделать: www.vijayreddiar.com/email

Вы также можете подписаться на меня в Твиттере [@ReddiarVijay]

До скорого!

Использованная литература: