Машинное обучение (ML) - это область искусственного интеллекта, в которой алгоритмы, управляемые данными, изучают закономерности, подвергаясь воздействию соответствующих данных. ML приобрел огромное значение в области обработки естественного языка (NLP), то есть интерпретации человеческого языка. В этой статье мы сосредоточимся на использовании машинного обучения для прогнозирования оценок отзывов пользователей. Данные, использованные в этой статье, были взяты из Kaggle (Ссылка), где было собрано около 20000 отзывов от Trip Advisor.

Весь код в этой статье был написан на Python 3 с использованием Jupyter Notebook.

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

import numpy as np 
import pandas as pd
import re
import spacy
from nltk.corpus import stopwords
from wordcloud import WordCloud,STOPWORDS
from tensorflow.keras.utils import to_categorical
import matplotlib.pyplot as plt
import string

Давайте посмотрим на некоторые строки данных.

data = pd.read_csv('input/trip-advisor-hotel-reviews/tripadvisor_hotel_reviews.csv')
data.head()

Давайте посмотрим небольшое описание данных.

data.describe()

Как видим, в данных 20491 наблюдение. Средняя оценка близка к 4. Посмотрим, нет ли недостающих данных.

data.isnull().mean()

Результат выглядит следующим образом:

Review    0.0
Rating    0.0
dtype: float64

Отлично! Данные отсутствуют. Посмотрим, сколько уникальных оценок в данных.

data['Rating'].unique()

Результат выглядит следующим образом:

array([4, 2, 3, 5, 1])

У нас всего 5 уникальных значений переменной Рейтинг. Поэтому мы будем рассматривать эту проблему как задачу классификации softmax. Формально нашей задачей было бы обучить модель классифицировать отзыв об отеле как 1,2,3 ,, или 5 звезд.

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

data['Rating'].value_counts(normalize=True)

Результат, который мы получаем:

5    0.441853
4    0.294715
3    0.106583
2    0.087502
1    0.069348
Name: Rating, dtype: float64

Мы видим, что большинство оценок - это 5-звездочные рейтинги, а наименьшие - 1-звездочные. Таким образом, мы должны позаботиться о том, чтобы обучить нашу модель достаточному количеству оценок в одну звезду. Таким образом, при обучении данных мы должны стратифицировать наш набор данных и наши данные. Это обеспечит равномерное распределение всех классов между поездом, проверкой и тестовым набором. Это поможет предотвратить появление неизвестных классов в проверочном или тестовом наборе.

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

def wordCloud_generator(data, title=None):
    wordcloud = WordCloud(width = 800, height = 800,
                          background_color ='black',
                          min_font_size = 10
                         ).generate(" ".join(data.values))                      
    plt.figure(figsize = (8, 8), facecolor = None) 
    plt.imshow(wordcloud, interpolation='bilinear') 
    plt.axis("off") 
    plt.tight_layout(pad = 0) 
    plt.title(title,fontsize=30)
    plt.show()
wordCloud_generator(data['Review'], title="Top words in reviews")

Мы создадим копии наших данных, чтобы сохранить оригинал.

X = data['Review'].copy()
y = data['Rating'].copy()

Текстовые данные содержат много шума, такого как стоп-слова (is, an, the, because и т. Д.), Пунктуация, различные формы одного и того же слова (например, play, plays, play, play и т. Д., Где корневым словом является play. Этот процесс называется лемматизацией) и отрицанием слов (вроде «не сделал», а не «не сделал»). Все это мешает процессу обучения нашей модели и увеличивает словарный запас нашей модели. Так что мы должны с ними разобраться.

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

При более внимательном рассмотрении данных мы можем заметить, что в них есть слова с ошибками (как показано ниже). Например, «not» в некоторых случаях пишется как «n’t», а некоторые отрицания, такие как «did not», записываются как «did n’t». Мы также исправим эти несоответствия в нашем процессе очистки.

"nice rooms not 4* experience hotel monaco seattle good hotel n't 4* level.positives large bathroom mediterranean suite comfortable bed pillowsattentive housekeeping staffnegatives ac unit malfunctioned stay desk disorganized, missed 3 separate wakeup calls, concierge busy hard touch, did n't provide guidance special requests.tv hard use ipod sound dock suite non functioning. decided book mediterranean suite 3 night weekend stay 1st choice rest party filled, comparison w spent 45 night larger square footage room great soaking tub whirlpool jets nice shower.before stay hotel arrange car service price 53 tip reasonable driver waiting arrival.checkin easy downside room picked 2 person jacuzi tub no bath accessories salts bubble bath did n't stay, night got 12/1a checked voucher bottle champagne nice gesture fish waiting room, impression room huge open space felt room big, tv far away bed chore change channel, ipod dock broken disappointing.in morning way asked desk check thermostat said 65f 74 2 degrees warm try cover face night bright blue light kept, got room night no, 1st drop desk, called maintainence came look thermostat told play settings happy digital box wo n't work, asked wakeup 10am morning did n't happen, called later 6pm nap wakeup forgot, 10am wakeup morning yep forgotten.the bathroom facilities great room surprised room sold whirlpool bath tub n't bath amenities, great relax water jets going,  "
#sample review from the dataset

Таким образом, словарь апострофов, который мы получили ранее, будет изменен с учетом этого следующим образом:

apposV2 = {
"are not" : "are not",
"ca" : "can",
"could n't" : "could not",
"did n't" : "did not",
"does n't" : "does not",
"do n't" : "do not",
"had n't" : "had not",
"has n't" : "has not",
"have n't" : "have not",
"he'd" : "he would",
"he'll" : "he will",
"he's" : "he is",
"i'd" : "I would",
"i'd" : "I had",
"i'll" : "I will",
"i'm" : "I am",
"is n't" : "is not",
"it's" : "it is",
"it'll":"it will",
"i've" : "I have",
"let's" : "let us",
"might n't" : "might not",
"must n't" : "must not",
"sha" : "shall",
"she'd" : "she would",
"she'll" : "she will",
"she's" : "she is",
"should n't" : "should not",
"that's" : "that is",
"there's" : "there is",
"they'd" : "they would",
"they'll" : "they will",
"they're" : "they are",
"they've" : "they have",
"we'd" : "we would",
"we're" : "we are",
"were n't" : "were not",
"we've" : "we have",
"what'll" : "what will",
"what're" : "what are",
"what's" : "what is",
"what've" : "what have",
"where's" : "where is",
"who'd" : "who would",
"who'll" : "who will",
"who're" : "who are",
"who's" : "who is",
"who've" : "who have",
"wo" : "will",
"would n't" : "would not",
"you'd" : "you would",
"you'll" : "you will",
"you're" : "you are",
"you've" : "you have",
"'re": " are",
"was n't": "was not",
"we'll":"we will",
"did n't": "did not"
}
appos = {
"aren't" : "are not",
"can't" : "cannot",
"couldn't" : "could not",
"didn't" : "did not",
"doesn't" : "does not",
"don't" : "do not",
"hadn't" : "had not",
"hasn't" : "has not",
"haven't" : "have not",
"he'd" : "he would",
"he'll" : "he will",
"he's" : "he is",
"i'd" : "I would",
"i'd" : "I had",
"i'll" : "I will",
"i'm" : "I am",
"isn't" : "is not",
"it's" : "it is",
"it'll":"it will",
"i've" : "I have",
"let's" : "let us",
"mightn't" : "might not",
"mustn't" : "must not",
"shan't" : "shall not",
"she'd" : "she would",
"she'll" : "she will",
"she's" : "she is",
"shouldn't" : "should not",
"that's" : "that is",
"there's" : "there is",
"they'd" : "they would",
"they'll" : "they will",
"they're" : "they are",
"they've" : "they have",
"we'd" : "we would",
"we're" : "we are",
"weren't" : "were not",
"we've" : "we have",
"what'll" : "what will",
"what're" : "what are",
"what's" : "what is",
"what've" : "what have",
"where's" : "where is",
"who'd" : "who would",
"who'll" : "who will",
"who're" : "who are",
"who's" : "who is",
"who've" : "who have",
"won't" : "will not",
"wouldn't" : "would not",
"you'd" : "you would",
"you'll" : "you will",
"you're" : "you are",
"you've" : "you have",
"'re": " are",
"wasn't": "was not",
"we'll":" will",
"didn't": "did not"
}

Теперь мы настроим нашу функцию очистки.

nlp = spacy.load('en',disable=['parser','ner'])
stop = stopwords.words('english')
def cleanData(reviews):
    all_=[]
    for review in reviews:
        lower_case = review.lower() #lower case the text
        lower_case = lower_case.replace(" n't"," not") #correct n't as not
        lower_case = lower_case.replace("."," . ")
        lower_case = ' '.join(word.strip(string.punctuation) for word in lower_case.split()) #remove punctuation
        words = lower_case.split() #split into words
        words = [word for word in words if word.isalpha()] #remove numbers
        split = [apposV2[word] if word in apposV2 else word for word in words] #correct using apposV2 as mentioned above
        split = [appos[word] if word in appos else word for word in split] #correct using appos as mentioned above
        split = [word for word in split if word not in stop] #remove stop words
        reformed = " ".join(split) #join words back to the text
        doc = nlp(reformed)
        reformed = " ".join([token.lemma_ for token in doc]) #lemmatiztion
        all_.append(reformed)
    df_cleaned = pd.DataFrame()
    df_cleaned['clean_reviews'] = all_
    return df_cleaned['clean_reviews']
X_cleaned = cleanData(X)
X_cleaned.head()

Получаем такой вывод:

0    nice hotel expensive parking get good deal sta...
1    ok nothing special charge diamond member hilto...
2    nice room experience hotel monaco seattle good...
3    unique great stay wonderful time hotel monaco ...
4    great stay great stay go seahawk game awesome ...
Name: clean_reviews, dtype: object

Мы также закодируем нашу целевую переменную Rating в один горячий вектор.

encoding = {1: 0,
            2: 1,
            3: 2,
            4: 3,
            5: 4
           }
labels = ['1', '2', '3', '4', '5']
           
y = data['Rating'].copy()
y.replace(encoding, inplace=True)
y = to_categorical(y,5)

Пришло время разделить наши данные на обучающие и тестовые наборы. Мы будем использовать 80% данных для обучения, 10% для проверки и 10% для тестирования.

X_train, X_test, y_train, y_test = train_test_split(X_cleaned, y, stratify=y, random_state=42,test_size=0.1)
#validation split will done when fitting the model

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

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
max_length = max([len(x) for x in X_train])
vocab_size = len(tokenizer.word_index)+1 #add 1 to account for unknown word
print("Vocabulary size: {}".format(vocab_size))
print("Max length of sentence: {}".format(max_length))
X_train = pad_sequences(X_train, max_length ,padding='post')

Получаем такой вывод:

Vocabulary size: 41115
Max length of sentence: 1800

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

Пора приступить к моделированию. Создадим нашу модель. Создадим последовательную модель.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM,Dense,Dropout
from tensorflow.keras.layers import Bidirectional,Embedding,Flatten
from tensorflow.keras.callbacks import EarlyStopping,ModelCheckpoint
embedding_vector_length=32
num_classes = 5
model = Sequential()
model.add(Embedding(vocab_size,embedding_vector_length,input_length=X_train.shape[1]))
model.add(Bidirectional(LSTM(250,return_sequences=True)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128,activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(64,activation='relu'))
model.add(Dense(32,activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(16,activation='relu'))
model.add(Dense(num_classes,activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
callbacks = [EarlyStopping(monitor='val_loss', patience=5),
             ModelCheckpoint('../model/model.h5', save_best_only=True, 
                             save_weights_only=False)]
model.summary()

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

Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 1800, 32)          1315680   
_________________________________________________________________
bidirectional_1 (Bidirection (None, 1800, 500)         566000    
_________________________________________________________________
dropout_3 (Dropout)          (None, 1800, 500)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 900000)            0         
_________________________________________________________________
dense_5 (Dense)              (None, 128)               115200128 
_________________________________________________________________
dropout_4 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_6 (Dense)              (None, 64)                8256      
_________________________________________________________________
dense_7 (Dense)              (None, 32)                2080      
_________________________________________________________________
dropout_5 (Dropout)          (None, 32)                0         
_________________________________________________________________
dense_8 (Dense)              (None, 16)                528       
_________________________________________________________________
dense_9 (Dense)              (None, 5)                 85        
=================================================================
Total params: 117,092,757
Trainable params: 117,092,757
Non-trainable params: 0
_________________________________________________________________

Подбираем нашу модель и начинаем тренировочный процесс.

history = model.fit(X_train, y_train, validation_split=0.11, 
                    epochs=15, batch_size=32, verbose=1,
                    callbacks=callbacks)

Наше обучение дает следующие результаты.

Epoch 1/15
513/513 [==============================] - 195s 368ms/step - loss: 1.6616 - accuracy: 0.4265 - val_loss: 0.9994 - val_accuracy: 0.5002
Epoch 2/15
513/513 [==============================] - 189s 369ms/step - loss: 0.9146 - accuracy: 0.5659 - val_loss: 0.9282 - val_accuracy: 0.5619
Epoch 3/15
513/513 [==============================] - 188s 367ms/step - loss: 0.7914 - accuracy: 0.6282 - val_loss: 0.9804 - val_accuracy: 0.5722
Epoch 4/15
513/513 [==============================] - 189s 368ms/step - loss: 0.6786 - accuracy: 0.7044 - val_loss: 0.9794 - val_accuracy: 0.6077
Epoch 5/15
513/513 [==============================] - 189s 368ms/step - loss: 0.5620 - accuracy: 0.7673 - val_loss: 1.0368 - val_accuracy: 0.5973
Epoch 6/15
513/513 [==============================] - 188s 367ms/step - loss: 0.4566 - accuracy: 0.8180 - val_loss: 1.2449 - val_accuracy: 0.5875
Epoch 7/15
513/513 [==============================] - 189s 369ms/step - loss: 0.3666 - accuracy: 0.8607 - val_loss: 1.3929 - val_accuracy: 0.5954

Мы можем заметить, что модель остановилась на 7 эпохах, хотя она была установлена ​​на 15 эпох. Это из-за нашего обратного вызова. Он остановил процесс обучения, как только обнаружил, что не было улучшения точности проверки после 5 эпох. Кроме того, он также сохранил веса модели, что лучше всего предотвратит переоснащение.

Мы достигли точности 60% на наборе для проверки и 70% на нашем наборе поездов (4-я эпоха). Мы можем еще больше уменьшить переоснащение, увеличив слои Dropout. Построим график наших результатов.

import matplotlib.pyplot as plt

plt.plot(history.history['loss'], label='Training')
plt.plot(history.history['val_loss'], label='Validation')
plt.legend()
plt.title('Training and Validation Loss')
plt.figure()

plt.plot(history.history['accuracy'],label='Training')
plt.plot(history.history['val_accuracy'],label='Validation')
plt.legend()
plt.title('Training and Validation accuracy')

Давайте сделаем некоторые прогнозы на нашем тестовом наборе.

X_test_token = tokenizer.texts_to_sequences(X_test)
X_test_token = pad_sequences(X_test_token, max_length ,padding='post')
pred = model.predict(X_test_token)
pred = to_categorical(pred,5)

Мы можем получить оценку точности и отчет о классификации следующим образом:

from sklearn.metrics import classification_report,accuracy_score
print('Test Accuracy: {}'.format(accuracy_score(pred, y_test)))
print(classification_report(y_test, pred, target_names=labels))

Результат следующий:

Test Accuracy: 0.5936585365853658
            precision    recall  f1-score   support

           1       0.61      0.64      0.63       142
           2       0.40      0.31      0.35       179
           3       0.45      0.32      0.38       219
           4       0.47      0.57      0.52       604
           5       0.75      0.72      0.73       906

   micro avg       0.59      0.59      0.59      2050
   macro avg       0.54      0.51      0.52      2050
weighted avg       0.59      0.59      0.59      2050
 samples avg       0.59      0.59      0.59      2050

Из нашего отчета о классификации мы видим, что наша модель достаточно хорошо работает с рейтингами 1,4 и 5 звезд и относительно плохо - с рейтингами 2 и 3 звезды. Чтобы повысить производительность, мы можем более внимательно изучить отзывы, отмеченные 2 и 3 звездами. Мы также можем реализовать другие методы предварительной обработки, такие как Stemming вместо лемматизации, и посмотреть, как это влияет на производительность.

Это все для этой статьи. До скорого!