Вам надоело читать длинную и показную страницу описания продукта? Вы когда-нибудь видели несоответствие звездных рейтингов продукта с его комментариями? Вот решение с помощью некоторых методов машинного обучения:

Требуемые пакеты Python:

  • Панды
  • Re
  • Склеарн
  • Нампи
  • текстовый объект
  • matplotlib
  • СПАСИ

Извлечение и очистка текста

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

import pandas as pd
df = pd.read_excel('Lamp.xlsm')
df.head(5)
df['text review'][2]

Изучив набор данных, мы добавили столбец с названием rating% на основе звездных рейтингов, чтобы упростить последующие процессы анализа и прогнозирования.

# transfer the rating data from string to a %number 
# no scientific base in this transformation
rating = [(float(x)-1)/4 for x in df['rating'].str[0:3]]
df['rating%'] = rating

На основе созданного столбца rating% мы классифицируем отзывы на положительные, нейтральные или отрицательные. Чтобы справиться с отсутствующими значениями, мы добавили «без комментариев» в столбец текстовый обзор.

# using percentage rating to generalise a sentiment for the reviews
df.loc[(df['rating%'] == 0) | 
\ (df['rating%'] == 0.25), 'sentiment'] = 'negative'
df.loc[df['rating%'] == 0.5, 'sentiment'] = 'neutral'
df.loc[(df['rating%'] == 0.75) | \
(df['rating%'] == 1.0), 'sentiment'] = 'positive'

# fill in any missing reviews with 'no comment'
df.loc[df['text review'].isnull(), 'text review'] = 'no comment'

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

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

Рейтинг

df.rating.value_counts().plot(kind = 'pie')

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

Рейтинги с течением времени

longitudinalRating = df['rating%'].groupby(df['date']).mean()
longitudinalRating.plot(figsize = (12, 3))

Судя по приведенному выше графику, рейтинг, кажется, разделен на 2 общих периода, причем 2021–2008 годы являются водоразделом. Отрицательные рейтинги до 2021–2008 годов были спорадическими и редкими, однако после 2021–2008 годов нейтральные / отрицательные рейтинги стали более распространенными. Поскольку у нас есть только минимальная информация о продукте, возможная причина, объясняющая этот сценарий, заключается в том, что продавец сменил производителя. Продавец должен провести дополнительную проверку, чтобы решить, что произойдет до и после 2021–2008 годов, чтобы улучшить свои услуги.

Сгруппировать слова по тональности

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

# word tokenization example to inspect the grammar structure of the reviews 

sample_comment = textReview[0]

doc = nlp(sample_comment)
for token in doc:
    # print out the token, part of speech, and lemmatized word.
    print(token, ' | ', token.pos_, ' | ', token.lemma_, ' | ', \
    spacy.explain(token.tag_))
 

Это результат:

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

Теперь давайте разделим прилагательные и существительные на 3 класса: положительные, нейтральные и отрицательные.

# grouping the dataset by sentiment, i.e. 'positive', 'neutral', 'negative'
reviewPositive = df.loc[df['sentiment'] == 'positive', 'text review']
reviewNeutral = df.loc[df['sentiment'] == 'neutral', 'text review']
reviewNegative = df.loc[df['sentiment'] == 'negative', 'text review']

Далее мы подсчитываем количество слов — это позволяет нам нормализовать подсчет на основе количества положительных/нейтральных/отрицательных отзывов. Причина нормализации количества слов заключается в том, что мы хотим устранить влияние некоторых общих слов, которые появляются в положительных/нейтральных/отрицательных словах.

lenPositive, lenNeutral, lenNegative = \
len(reviewPositive), len(reviewNeutral), len(reviewNegative)

Теперь разделим отзывы на прилагательные и существительные.

# counting nouns and adjectives in comments (groupby sentiment)

def getWordCount(reviewSet):
    """
    filter out the nouns and adjectives in the review and count each word's
    appearance. Returns two python dictionary with word as the key, count
    as the value. 
    """
    
    nouns, adjectives = {}, {}
    for comment in reviewSet:
        if pd.isnull(comment): 
          continue
        for token in nlp(comment):
            if token.pos_ == 'NOUN': 
              nouns[token.lemma_] = nouns.get(token.lemma_, 0) + 1
            elif token.pos_ == 'ADJ': 
              adjectives[token.lemma_] = adjectives.get(token.lemma_, 0) + 1
    
    return nouns, adjectives


# get noun and adjective word count for positive, neutral, 
# negative reviews respectively
nounsCountPositive, adjectiveCountPositive = getWordCount(reviewPositive)
nounsCountNeutral, adjectiveCountNeutral = getWordCount(reviewNeutral)
nounsCountNegative, adjectiveCountNegative = getWordCount(reviewNegative)

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

# centerize and normalize (account for # of reviews) the word count 
# there are much more positive reviews than neutral and negative ones

for key, value in nounsCountPositive.items():
    nounsCountPositive[key] = value - (nounsCountNeutral.get(key, 0)* \
    1000/lenNeutral + nounsCountNegative.get(key, 0)*1000/lenNegative + \
    value*1000/lenPositive)*lenPositive/3000

for key, value in adjectiveCountPositive.items():
    adjectiveCountPositive[key] = value - \
    (adjectiveCountNeutral.get(key, 0)*1000/lenNeutral + \
    adjectiveCountNegative.get(key, 0)*1000/lenNegative + \
    value*1000/lenPositive)*lenPositive/3000

for key, value in nounsCountNeutral.items():
    nounsCountNeutral[key] = value - (nounsCountPositive.get(key, 0)* \
    1000/lenPositive + nounsCountNegative.get(key, 0)*1000/lenNegative + \
    value*1000/lenNeutral)*lenNeutral/3000

for key, value in adjectiveCountNeutral.items():
    adjectiveCountNeutral[key] = \
    value - (adjectiveCountPositive.get(key, 0)*1000/lenPositive + \
    adjectiveCountNegative.get(key, 0)*1000/lenNegative + \ 
    value*1000/lenNeutral)*lenNeutral/3000

for key, value in nounsCountNegative.items():
    nounsCountNegative[key] = value - (nounsCountNeutral.get(key, 0)* \
    1000/lenNeutral + nounsCountPositive.get(key, 0)*1000/lenPositive + \
    value*1000/lenNegative)*lenNegative/3000

for key, value in adjectiveCountNegative.items():
    adjectiveCountNegative[key] = value - \
    (adjectiveCountNeutral.get(key, 0)*1000/lenNeutral + \
    adjectiveCountPositive.get(key, 0)*1000/lenPositive + \
    value*1000/lenNegative)*lenNegative/3000

Теперь, чтобы визуализировать количество слов:

# convert dict like dataset to pandas dataframe to facilitate plotting

def sortAndDf(data):
    """
    takes in dict-like word count dataset, sort it based on count, 
    convert to pandas dataframe and return
    """
    
    data_sorted = sorted(data.items(), key = lambda x: x[1], reverse = True)
    return pd.DataFrame(data_sorted, columns = ['keyword', 'count'])

nounsCountPositiveDf, adjectiveCountPositiveDf = \
sortAndDf(nounsCountPositive), sortAndDf(adjectiveCountPositive)

nounsCountNeutralDf, adjectiveCountNeutralDf = \
sortAndDf(nounsCountNeutral), sortAndDf(adjectiveCountNeutral)

nounsCountNegativeDf, adjectiveCountNegativeDf = \
sortAndDf(nounsCountNegative), sortAndDf(adjectiveCountNegative)
# start plotting
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows = 3, ncols = 2, figsize = (12, 16))

nounsCountPositiveDf[:20].plot(x = 'keyword', y = 'count', kind = 'barh', 
ax = axes[0,0], title = 'positive review noun count', 
color = (174/255, 255/255, 216/255))

adjectiveCountPositiveDf[:20].plot(x = 'keyword', y = 'count', 
kind = 'barh', ax = axes[0,1], title = 'positive review adjective count',
color = (174/255, 255/255, 216/255))

nounsCountNeutralDf[:20].plot(x = 'keyword', y = 'count', kind = 'barh', 
ax = axes[1,0], title = 'neutral review noun count', 
color = (142/255, 164/255, 210/255))

adjectiveCountNeutralDf[:20].plot(x = 'keyword', y = 'count', 
kind = 'barh', ax = axes[1,1], title = 'neutral review adjective count', 
color = (142/255, 164/255, 210/255))

nounsCountNegativeDf[:20].plot(x = 'keyword', y = 'count', 
kind = 'barh', ax = axes[2,0], title = 'negative review noun count', 
color = (255/255, 105/255, 120/255))

adjectiveCountNegativeDf[:20].plot(x = 'keyword', y = 'count', 
kind = 'barh', ax = axes[2,1], title = 'negative review adjective count', 
color = (255/255, 105/255, 120/255))

Выход:

Интересные выводы:

  • Цвет, температура, контроль и тон — вот некоторые из характеристик продукта, которые нравятся покупателям.
  • Довольные покупатели нашли лампу простой (может быть, в сборке?)
  • Клиенты оставляли нейтральные отзывы, упоминая большую часть мощности и батареи, может быть, их это не устраивает?
  • Покупатели оставляли отрицательные отзывы с упоминанием денег, может быть, они считают, что цена на товар завышена?

Области улучшения: углубленное исследование необычных слов

  • месяц негативных отзывов (что это значит?)
  • почему негативные отзывы имеют подавляющее количество ламповых

Подбор линейной модели, чтобы попытаться предсказать значение рейтинга

*Обратите внимание, что мы использовали 80 % данных в качестве обучающих наборов, а остальные — в качестве тестовых наборов.*

Во-первых, импортируйте все необходимые библиотеки:

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import \
CountVectorizer, TfidfVectorizer
from textblob import TextBlob
from sklearn.linear_model import LinearRegression
from sklearn.naive_bayes import MultinomialNB

Мы реализовали два типа методов лемматизации/стемминга на выбор:

Тип 1: использование библиотечного текстового объекта

def textblob_tokenizer(str_input):
    """
    Input a text in string format, and use the library 
    textblob to tokenise and lemmatise.
    """
    
    blob = TextBlob(str_input.lower())
    tokens = blob.words
    words = [token.stem() for token in tokens]
    return words

Тип 2: использование функции NLP library-spacy

def my_tokenizer(text_input):
    """
    Chop a paragraph into individual, no-tense, words using nlp library 
    spacy's function to tokenise and lemmatise a review
    
    """
    doc = nlp(text_input)
    return [token.lemma_ for token in doc]

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

def fitModel(model, vectorizer, response, tokenizer, manual_inspect = False):
    """
    Allow users to choose the ML model, how to vectorize word tokens, 
    currently just CountVectorizer and TF-IDF. Also allows to choose
    which response to use- continous number, ordinal value as well as the 
    tokenisation method. Returns a new predicted star rating
    """
    
    # split data set to training set and test set
    X_train, X_test, y_train, y_test = train_test_split(textReview, 
    df[response], test_size = 0.2, random_state=1)
    
    # transform comment into bag of words/TF-IDF vectorizer
    vec = vectorizer(tokenizer = tokenizer, stop_words = "english" , 
    lowercase = True)
    
    X_train_vec = vec.fit_transform(X_train)
    X_test_vec = vec.transform(X_test)
    
    # fitting model
    
    model_out = model()
    model_out.fit(X_train_vec, y_train)
    
    # predict values

    y_predict = model_out.predict(X_test_vec)
    
    # manually inspect linear regression

    if manual_inspect:
        for i in range(len(X_test)):
            print(X_test.iloc[i])
            print(y_test.iloc[i])
            print(y_predict[i])
            user_input = input("press enter to continue, or q to quit")
            if user_input == 'q':
                break
                
    return model_out, y_predict, y_test

Некоторые примеры выходных данных:

Пример 1 (с использованием линейной регрессии, CountVectorizer):

model1 = fitModel(LinearRegression, CountVectorizer, 'rating%', my_tokenizer, True)

Выход:

Пример 2 (с использованием линейной регрессии, TfidfVectorizer):

model2 = fitModel(LinearRegression, TfidfVectorizer, 'rating%', my_tokenizer, True)

Выход:

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

Поэтому мы пришли к выводу, что линейная модель ведет себя очень плохо, просто взглянув на несколько прогнозов на тестовом наборе. Большинство прогнозов завышают оценки, вероятно, потому, что в данных преобладают 5-звездочные рейтинги (см. предварительный анализ). Если бы у нас было дополнительное время, мы могли бы поработать над улучшением переменной-предиктора. Также кажется, что TfidfVectorizer работает лучше, чем countVectorizer.

Пример 3 (с использованием полиномиального наивного байесовского классификатора):

model3, predict3, y_test_3 = fitModel(MultinomialNB, CountVectorizer, 'sentiment', my_tokenizer, True)

Выход:

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

Итак, давайте посчитаем некоторые оценочные метрики, чтобы увидеть, может ли это впечатление быть оправданным:

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix

def evaluate(model, predict, y_test):
    """
    prints out some common evaluation metrics to evaluate the model.
    """
    accuracy = accuracy_score(y_test, predict)
    precision = precision_score(y_test, predict, average = 'weighted')
    recall = recall_score(y_test, predict, average = 'weighted')
    f1 = f1_score(y_test, predict, average = 'weighted')
    confusion = confusion_matrix(y_test, predict)
    
    print("accuracy: ", accuracy)
    print('precision: ', precision)
    print('recall: ', recall)
    print('f1:', f1)
    print('confusion matrix: ', '\n', confusion)

Теперь мы оцениваем модель3:

evaluate(model3, predict3, y_test_3)

Выход:

Если мы просто будем следовать максимальной вероятности и пометим все как положительное (см. ниже), у нас будет уровень точности 76,12%, но использование классификатора MNB даст точность 81,09%. Это немалое улучшение, учитывая небольшой размер выборки, которую мы здесь используем. Если бы нам дали больше времени, мы могли бы попробовать отрегулировать параметры, обучить модель на большем наборе данных или попробовать более сложные модели, которые лучше соответствуют свойствам нашего набора данных.

Несколько заключительных замечаний…

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

В противном случае, спасибо, что дочитали до конца!