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

В этой статье вы узнаете, как использовать логистическую регрессию для анализа настроений для нескольких классов. Вы также узнаете, как уменьшить дисбаланс данных с помощью увеличения текста и пониженной выборки. Ссылку на набор данных можно найти здесь. Набор данных содержит отзывы от Amazon о косметическом продукте класса люкс с рейтингом от 1 до 5. Мы будем прогнозировать рейтинги, используя текстовые отзывы клиентов.

Давайте углубимся в это

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

import json #the file is json
import pandas as pd #to help with data transformations
import numpy as np 
import matplotlib.pyplot as plt #plotting

Теперь, когда мы импортировали эти библиотеки, давайте прочитаем набор данных.

#read json into a list
f = open('Luxury_Beauty.json', 'r')
dataset = [] #create an empty list
for line in f:
    dataset.append(json.loads(line)) #append to list
#convert list to pandas dataframe
dataset = pd.DataFrame(dataset)
dataset.head(3)

Для этого анализа нам нужны только общие столбцы (рейтинг), reviewText и сводка. Мы объединим столбцы ReviewText и Summary в один столбец с именем final_review.

dataset['final_review'] = dataset['reviewText'] + ' ' + dataset['summary']
#final_dataset 
dataset = dataset[['final_review', 'overall']]
dataset.head(2)

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

#to get count of empty rows
dataset[dataset['final_review'].isna()].count()
#to get % of empty rows
dataset[dataset['final_review'].isna()].count()/len(dataset)

Ух ты! 575 строк пусты. Нам нужно удалить их, так как это всего 0,1% нашего набора данных, а пустые отзывы нам не нужны.

dataset = dataset.dropna()
#let's reset the index
dataset.reset_index(inplace = True)
del dataset['index']

Теперь, когда мы удалили пустые строки, давайте реклассифицируем набор данных всего на 3 класса: положительные (4 и 5), отрицательные (1 и 2) и нейтральные (3). Положительный будет представлен как 1, нейтральный как 0 и отрицательный как-1. После переклассификации мы проверим распределение наших оценок, потому что сильно несбалансированный набор данных может привести к систематической ошибке выборки.

#reclassify
dataset['overall'] = dataset['overall'].apply(lambda x : 1 if x in (4,5) else -1 if x in (1,2) else 0)
#look at the destribution of data on our target variable
dataset['overall'].describe()
dataset['overall'].value_counts()/len(dataset)

Набор данных явно несбалансирован и приведет к систематической ошибке выборки, если ее не исправить. Мы можем уменьшить дисбаланс, либо передискретизируя классы меньшинства с помощью увеличения текста, либо занижая выборку классов большинства. Увеличение текста используется для увеличения набора данных путем создания синтетических данных с существующим набором данных. Существуют различные методы дополнения, такие как обратный перевод, замена синонимов, перетасовка, случайная вставка, случайное удаление и т. д. Для этого доступны библиотеки, такие как NLPAUG и textaugment. Подробнее об увеличении текста

Прежде чем мы расширим наши текстовые данные, нам нужно разделить их на тестовые и обучающие. Аугментация выполняется только для данных поезда, чтобы избежать утечки данных. Из-за явного дисбаланса в нашем наборе данных мы разделим его с помощью стратифицированного случайного разделения sklearn. Стратифицированное случайное разбиение разбивает набор данных таким образом, что и обучающий, и тестовый наборы имеют близкое к тому же распределение классов, что и полный набор данных. См. код ниже, используя размер теста 0,2 или 20%

from sklearn.model_selection import StratifiedShuffleSplit
split= StratifiedShuffleSplit(n_splits = 1, test_size = 0.2, random_state = 42) 
for train_index, test_index in split.split(dataset, dataset['overall']):
    strat_train_set = dataset.loc[train_index]
    strat_test_set = dataset.loc[test_index]

давайте проверим распределение классов тестового набора

strat_test_set['overall'].value_counts()/len(strat_test_set)

Большой!!! Он близок к оригиналу.

Теперь время для аугментации в поезде

Увеличение текста

Мы будем использовать WordNet в библиотеке textaugment.

#import necessary nltk libraries
import nltk
nltk.download('punkt')
nltk.download('wordnet')
from textaugment import Wordnet, Translate

Давайте проведем тест

t=Wordnet()

#test
sentence ='Hello, I have been so exhausted lately. I need to rest'
t.augment(sentence)

Из приведенного выше примера ясно, что WordNet использует технику замены синонимов для расширения.

Мы продолжим добавлять отзывы с оценками, не равными 1 (неположительные оценки).

#create an empty dataframe for augmented text
augmented = pd.DataFrame()
#append augmented text to the empty dataframe
for i in strat_train_set[strat_train_set['overall'] != 1].index:
    text_aug = t.augment(str(strat_train_set['final_review'][i]))
    augmented = augmented.append({'final_review':text_aug ,'overall': dataset['overall'][i]} , ignore_index=True)
#append augmented to strat_train_set
strat_train_set = strat_train_set.append(augmented)

Мы уменьшим выборку нашего набора поездов, отбросив 60% большинства классов.

#drop 60% of records with scores equal to 1 and add augmented to train set
part_na = round(strat_train_set[strat_train_set['overall'] == 5].shape[0]*0.6) #count of 60%
strat_train_set.head()
#get indices where overall is 1
five_indices = strat_train_set[strat_train_set.overall == 1].index
#select 60% at random
random_indices = np.random.choice(five_indices, part_na, replace= False)
#drop random indices from train set
strat_train_set.drop(random_indices, inplace = True)
#new class distribution
strat_train_set['overall'].value_counts()/len(strat_train_set)

Теперь, когда наш набор поездов готов, пришло время для извлечения признаков.

Извлечение признаков с использованием TF-IDF

TF-IDF — это сокращение от «Частота термина — обратная частота документа». Он используется для количественной оценки того, насколько важны слова в корпусе. Он учитывает частоту слов в документе после исключения стоп-слов (естественно встречающихся слов в английском языке, таких как the, is и т. д.). Библиотека TF-IDF извлекает слова из корпуса и превращает их во взвешенные векторы.

#import TF-IDF module
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer() #we can use to limit the number of features selected using max_features 
#fit test data to the vectorizer to get the total number of features in test
X_test =vectorizer.fit_transform(strat_test_set['final_review'])
#transform train data to the shape of test
X_train = vectorizer.transform(strat_train_set['final_review'])
y_train = strat_train_set['overall']
y_test = strat_test_set['overall']

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

Импорт библиотек, связанных с машинным обучением

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score,precision_score,recall_score
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)  
print('Accuracy', model.score(X_test, y_test))
print('F1', f1_score(y_test,model.predict(X_test), average="macro"))
print('Precision', precision_score(y_test, model.predict(X_test), average="macro"))
print('Recall', recall_score(y_test, model.predict(X_test), average="macro"))

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

from sklearn.metrics import confusion_matrix, multilabel_confusion_matrix
from sklearn.metrics import ConfusionMatrixDisplay
y_pred = model.predict(X_test)
cm = multilabel_confusion_matrix(y_test, y_pred)
f, axes = plt.subplots(2, 3, figsize=(20, 10))
plt.subplots_adjust(wspace=0.40, hspace=0.1)
axes = axes.ravel()
for ind in range(5):
    cm_display = ConfusionMatrixDisplay(cm[ind], display_labels=[0, model.classes_[ind]]).plot()
    cm_display.plot(ax=axes[ind], xticks_rotation=45)
    cm_display.ax_.set_title(name)
    cm_display.im_.colorbar.remove()
    cm_display.ax_.set_xlabel('')
    if i!=0:
        cm_display.ax_.set_ylabel('')
    
f.text(0.4, 0.1, 'Predicted label', ha='left')
f.colorbar(cm_display.im_, ax=axes)

Заключение

Точность высока и составляет 90%, но, судя по отзыву, модель правильно предсказывает только 78% своего класса. Точность, скорее всего, высока, потому что в тестовых данных больше положительных настроений, и из матрицы путаницы мы можем сказать, что модель лучше предсказывает положительные и отрицательные настроения, чем нейтральные. Возможно, это связано с тем, что в наборе данных все еще существует некоторый дисбаланс, а нейтральные настроения наименее представлены среди всех классов. Предложения по повышению точности модели могут включать использование других методов увеличения для увеличения набора нейтральных поездов, чтобы он был правильно представлен.

Ссылка на Jupyter Notebook

Ссылки

  1. Обоснование рекомендаций с использованием удаленных отзывов и детальных аспектов
    Цзяньмо Ни, Цзячэн Ли, Джулиан Маколи
    Эмпирические методы обработки естественного языка (EMNLP), 2019 г.
  2. Увеличение данных в NLP: лучшие практики от мастера Kaggle
    Shahul ES
    https://neptune.ai/blog/data-augmentation-nlp