Чтобы построить эту модель, мы будем использовать набор данных, найденный на этой странице (https://www.kaggle.com/datasets/andrewmvd/trip-advisor-hotel-reviews), который содержит около 20491 отзыва плюс соответствующий рейтинг (от 1 –5) оставили на TripAdvisor информацию об опыте проживания разных людей в соответствующих гостиничных номерах.

trip = pd.read_csv("tripadvisor_hotel_reviews.csv")
trip.shape
(20491, 2)
trip.head()

Предварительная обработка и исследовательский анализ

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

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

trip["Complaints"] = np.where(trip["Rating"] < 5, 1, 0)

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

trip["length"] = trip["Review"].apply(lambda x: len(x.split(" ")))

Теперь давайте посмотрим на распределение оценки рецензента (жалобы или нет).

plt.figure(figsize = (12,8))

sns.set_style("whitegrid")
sns.histplot(x = "Complaints", data = trip)
plt.title("Reviewer's grade distribution")
plt.xlabel("Grade")
Text(0.5, 0, 'Grade')

Как мы видим, распределение кажется довольно сбалансированным: около 44% наблюдений имеют те или иные жалобы, а остальные 56% считают, что сервис был идеальным (нет жалоб).

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

plt.figure(figsize = (12,8))
sns.set_style("whitegrid")
sns.histplot(x = "length", data = trip, kde = True, bins = 50)
plt.title("Review's length distribution")
plt.xlabel("Length")
plt.show()

trip.describe()

Мы видим, что абсолютное большинство обзоров, как правило, относительно короткие. В частности, около 75% полученных отзывов содержат 130 или менее слов, что немного.

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

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

print(trip[trip["length"] == 1933][["Review","Complaints"]])
Review  Complaints
7072  honest review visit 5/21-5/28 let begin saying...           1

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

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

trip = trip[trip["length"] < 80]
trip.shape
(10330, 4)

Делая это, мы получаем около 10330 отзывов в наборе данных.

trip.hist(column = "length", by = "Complaints",figsize = (12,8), bins = 20)
plt.show()

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

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

Предварительная обработка для отзывов

В этом разделе нас особенно интересует форматирование обзоров таким образом, чтобы наша модель могла лучше давать результаты. Эта предварительная обработка включает в себя:

  • Удаление знаков препинания.
  • Удаление номеров.
  • Удаление URL.
  • Удаление стоп-слов (очень распространенных слов, которые не добавляют никакой ценности)
  • Лемматизация (преобразование слов в их простейшую форму, например, бег и бег становятся бегом)
import nltk 
from nltk.corpus import stopwords 

import re
import string

from textblob import Word, TextBlob
def preprocess_reviews(review):
    pre_review = review
    
    pre_review = "".join(word for word in pre_review if word not in
                         string.punctuation) #Remove punctuation
    
    pre_review = "".join(word for word in pre_review if not word.isdigit())
    
    pre_review = " ".join(Word(word).lemmatize() for word in pre_review.split()) #Take every word to root 
    
    pre_review = re.sub(r'http\S+', '', pre_review) #Remove URL's
    
    pre_review = " ".join(word for word in pre_review.split() if word.lower() not in
                         [stopwords.words("english"), "hotel","room"])
    
    return pre_review
trip["ReviewPre"] = trip["Review"].apply(preprocess_reviews)

После выполнения упомянутых шагов некоторые обзоры в наборе данных показаны ниже:

trip.head()

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

Создание счетчика

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

from collections import Counter


def word_counter(x):
    count = Counter()
    for text in x.values:
        for word in text.split():
            count[word] += 1
    return count

counter = word_counter(trip["ReviewPre"])

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

counter.most_common(5)
[('great', 8331),
 ('not', 6546),
 ('staff', 6405),
 ('stay', 6141),
 ('location', 5658)]

Самое распространенное слово — «отлично», что является очень хорошим признаком, поскольку это означает, что подавляющее большинство отзывов, как правило, положительно отзываются об отелях на tripadvisor. С другой стороны, также очень ясно, что слово «не» чрезвычайно распространено, а это означает, что в некоторых обзорах, в которых упоминается что-то «хорошее», на самом деле может упоминаться что-то «плохое».

Наконец, одними из наиболее распространенных тем, упоминаемых в отзывах, являются персонал и расположение отеля.

Разделение данных

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

from sklearn.model_selection import train_test_split
X = trip["ReviewPre"]

y = trip["Complaints"]

В частности, мы будем использовать 80% доступных данных для обучающей части и оставшиеся 20% для тестовой части.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 1902)

Токенизация слов

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

Таким образом, каждое конкретное слово будет обозначаться числом, представляющим собой индекс отсортированных слов от наиболее часто встречающихся к наименее часто встречающимся, а это означает, что, как мы видели ранее, слово «великолепный», являющееся наиболее распространенным, будет сокращаться. отождествляется с цифрой 1.

from tensorflow.keras.preprocessing.text import Tokenizer
num_unique_words = len(counter)

tokenizer = Tokenizer(num_words = num_unique_words)
tokenizer.fit_on_texts(X_train)
word_index = tokenizer.word_index

Далее мы видим, например, данный индекс как для слова «отличный», так и для слова «персонал».

print(word_index["great"])
print(word_index["staff"])
1
3

Преобразование текстов в последовательности и использование заполнения

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

X_train_seq = tokenizer.texts_to_sequences(X_train)
X_test_seq = tokenizer.texts_to_sequences(X_test)
print(X_train[20:25])
print(X_train_seq[20:25])
10006    great based tripadvisor review booked threenig...
18058    lovely paper wall stay du petit moulin weekend...
2484     not satisfied price dissapointed stay year ago...
20293    comfortable central stayed night really enjoye...
10963    luxury nt think ok provides amenity good clean...
Name: ReviewPre, dtype: object
[[1, 483, 489, 54, 63, 6775, 4, 3770, 128, 116, 43, 3, 15, 1, 10, 219, 195, 145, 412, 179, 344, 354, 9, 57, 241, 987, 16, 23, 2, 223, 153, 2, 153, 89, 3766, 670, 16, 3, 406, 16, 11, 3771, 1508, 2, 2289, 4, 2478, 569, 11, 63, 99, 167], [52, 1249, 250, 4, 1482, 2479, 5544, 112, 52, 4, 3, 14, 17, 44, 1078, 511, 1, 5, 41, 8, 57, 51, 82, 4175, 1450, 1321, 610, 1249, 250, 30], [2, 922, 34, 1483, 4, 83, 671, 19, 710, 19, 36, 5545, 80, 19, 11, 482, 16, 1981, 65, 2, 298, 14, 24, 22, 363, 79, 519, 189, 2193, 548, 5546, 608, 244, 21, 956, 10, 1849, 163, 748, 2739, 259, 104, 20, 2048, 1117, 174, 19], [26, 102, 7, 10, 31, 95, 26, 102, 6, 58, 4755, 3, 17, 956, 697, 3772, 9459, 374, 757, 9460, 4, 33], [377, 11, 154, 134, 1229, 305, 6, 9, 26, 25, 8, 12, 1000, 89, 34, 2, 158, 184, 222, 385, 377, 377, 563, 2049, 68, 216, 311, 399, 1118, 460, 630, 283, 617, 1030, 117, 923, 1290, 141, 217, 3231, 2194, 2864, 68, 398, 399, 377, 116, 214, 140, 2864, 141, 517, 185, 1290, 134, 2195, 586, 20, 82, 617, 207, 182, 583, 2196, 642, 549]]

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

По этой причине мы будем использовать «заполнение», таким образом, для нашего конкретного случая размер ввода будет равен 70 словам в каждом обзоре. Если в каком-то тексте не так много слов, пустые места будут заменены нулями.

# In order to have the same length for every sequence, we use padding
from tensorflow.keras.preprocessing.sequence import pad_sequences

max_length = 70

train_padded = pad_sequences(X_train_seq, maxlen = max_length, padding = "post", truncating = "post")
test_padded = pad_sequences(X_test_seq, maxlen = max_length, padding = "post", truncating = "post")

train_padded.shape, test_padded.shape
((8264, 70), (2066, 70))

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

train_padded[5]
array([   1, 1320,  188,    7,  170, 1289,  582,  215,   18,  219,   97,
         17,  224,  216,   22,  403, 9430, 9431,  531,    7,   81, 2378,
        154,   56,   32, 1076, 1159,  899,   24,   14,  359,  872,   71,
        127,  647,  596,   11,   75,  361,   72,   85,   10,  592,   15,
         29,  104,  984,  144,  405,  294,  762,  199,  203,  814,  130,
       1225,  187,   15,   84,  376,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0])
X_train.index = range(0, X_train.shape[0])

y_train.index = range(0, X_train.shape[0])

Более конкретно, давайте посмотрим на ту же последовательность через 3 основных шага предварительной обработки, которые мы предприняли. Первый - это фактическое предложение после удаления знаков препинания и стоп-слов; второй - индексированная последовательность с идентификационными номерами; и последний — это последняя последовательность после заполнения, где мы добавляем нули, пока у нас не будет 70 чисел в последовательности.

print(X_train[15])
print(" ")
print(X_train_seq[15])
print(" ")
print(train_padded[15])
small wonderful super location selected based trip advisor review not disappointed elegant staff outstanding alessandro called friend drive town replace lost camerea helping reservation practical suggestion liked canal having drink outside watching passing scene fixture murano glass added elegant touch minute
 
[24, 37, 331, 5, 1449, 483, 43, 562, 54, 2, 241, 862, 3, 351, 6770, 385, 155, 901, 220, 3476, 832, 9454, 1573, 216, 3768, 822, 368, 369, 199, 166, 150, 1022, 3041, 1507, 1793, 5542, 506, 807, 862, 321, 40]
 
[  24   37  331    5 1449  483   43  562   54    2  241  862    3  351
 6770  385  155  901  220 3476  832 9454 1573  216 3768  822  368  369
  199  166  150 1022 3041 1507 1793 5542  506  807  862  321   40    0
    0    0    0    0    0    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0    0    0    0    0]

Подгонка модели классификации

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

Прежде чем погрузиться в построение модели, давайте кратко рассмотрим ее структуру. Первым слоем нашего классификатора будет встраиваемый слой. Встраивание — это более современный способ представления слов с использованием плотного вектора, который представляет собой проекцию слова в непрерывное векторное пространство. Положение слова в векторе узнается из заданных текстов и основано на словах, которые его окружают, когда это слово используется. Эту позицию мы называем «встраиванием».

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

from tensorflow.keras import layers
from tensorflow.keras.callbacks import EarlyStopping
model = keras.models.Sequential()
model.add(layers.Embedding(num_unique_words, 16, input_length = max_length))


model.add(layers.LSTM(32))
model.add(layers.Dropout(0.15))
model.add(layers.Dense(1))
model.add(layers.Activation("sigmoid"))

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 70, 16)            425984    
_________________________________________________________________
lstm (LSTM)                  (None, 32)                6272      
_________________________________________________________________
dropout (Dropout)            (None, 32)                0         
_________________________________________________________________
dense (Dense)                (None, 1)                 33        
_________________________________________________________________
activation (Activation)      (None, 1)                 0         
=================================================================
Total params: 432,289
Trainable params: 432,289
Non-trainable params: 0
_________________________________________________________________

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

early_stop = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=20) 
    

model.compile(loss = "binary_crossentropy",
             optimizer = "adam",
             metrics = ["accuracy"])

Первоначально мы будем использовать 100 эпох, а также размер пакета 100.

history = model.fit(train_padded, y_train, 
          batch_size = 100, 
          epochs = 100, 
          validation_data=(test_padded, y_test),
             callbacks = [early_stop])
Train on 8264 samples, validate on 2066 samples
Epoch 1/100
8264/8264 [==============================] - 2s 269us/sample - loss: 0.0281 - accuracy: 0.9936 - val_loss: 1.3494 - val_accuracy: 0.7338
Epoch 2/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0160 - accuracy: 0.9976 - val_loss: 1.4349 - val_accuracy: 0.7270
Epoch 3/100
8264/8264 [==============================] - 1s 76us/sample - loss: 0.0140 - accuracy: 0.9982 - val_loss: 1.5659 - val_accuracy: 0.7260
Epoch 4/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0135 - accuracy: 0.9982 - val_loss: 1.5703 - val_accuracy: 0.7280
Epoch 5/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0136 - accuracy: 0.9978 - val_loss: 1.6140 - val_accuracy: 0.7246
Epoch 6/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0143 - accuracy: 0.9976 - val_loss: 1.5129 - val_accuracy: 0.7231
Epoch 7/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0228 - accuracy: 0.9959 - val_loss: 1.5931 - val_accuracy: 0.7231
Epoch 8/100
8264/8264 [==============================] - 1s 79us/sample - loss: 0.0472 - accuracy: 0.9877 - val_loss: 1.1907 - val_accuracy: 0.7285
Epoch 9/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0158 - accuracy: 0.9975 - val_loss: 1.5240 - val_accuracy: 0.7241
Epoch 10/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0119 - accuracy: 0.9984 - val_loss: 1.5472 - val_accuracy: 0.7188
Epoch 11/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0108 - accuracy: 0.9987 - val_loss: 1.6002 - val_accuracy: 0.7241
Epoch 12/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0101 - accuracy: 0.9988 - val_loss: 1.6486 - val_accuracy: 0.7188
Epoch 13/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0098 - accuracy: 0.9988 - val_loss: 1.6749 - val_accuracy: 0.7173
Epoch 14/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0095 - accuracy: 0.9988 - val_loss: 1.6863 - val_accuracy: 0.7178
Epoch 15/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0100 - accuracy: 0.9988 - val_loss: 1.6933 - val_accuracy: 0.7178
Epoch 16/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0099 - accuracy: 0.9988 - val_loss: 1.6784 - val_accuracy: 0.7173
Epoch 17/100
8264/8264 [==============================] - 1s 76us/sample - loss: 0.0097 - accuracy: 0.9988 - val_loss: 1.6861 - val_accuracy: 0.7193
Epoch 18/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0102 - accuracy: 0.9988 - val_loss: 1.6802 - val_accuracy: 0.7193
Epoch 19/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0099 - accuracy: 0.9988 - val_loss: 1.7047 - val_accuracy: 0.7197
Epoch 20/100
8264/8264 [==============================] - 1s 74us/sample - loss: 0.0100 - accuracy: 0.9988 - val_loss: 1.6641 - val_accuracy: 0.7212
Epoch 21/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0089 - accuracy: 0.9989 - val_loss: 1.6858 - val_accuracy: 0.7202
Epoch 22/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0090 - accuracy: 0.9989 - val_loss: 1.6959 - val_accuracy: 0.7217
Epoch 23/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0088 - accuracy: 0.9989 - val_loss: 1.6894 - val_accuracy: 0.7217
Epoch 24/100
8264/8264 [==============================] - 1s 76us/sample - loss: 0.0088 - accuracy: 0.9989 - val_loss: 1.6489 - val_accuracy: 0.7227
Epoch 25/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0089 - accuracy: 0.9989 - val_loss: 1.7599 - val_accuracy: 0.7217
Epoch 26/100
8264/8264 [==============================] - 1s 75us/sample - loss: 0.0095 - accuracy: 0.9988 - val_loss: 1.5357 - val_accuracy: 0.7197
Epoch 27/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0079 - accuracy: 0.9990 - val_loss: 1.6903 - val_accuracy: 0.7202
Epoch 28/100
8264/8264 [==============================] - 1s 73us/sample - loss: 0.0073 - accuracy: 0.9992 - val_loss: 1.7159 - val_accuracy: 0.7246
Epoch 00028: early stopping

Используя ранее полученные данные тестирования, мы получаем расчетную точность около 72%. Это означает, что мы можем правильно предсказать, если у клиента есть жалобы на конкретный отель, используя слова, которые он использует для описания этого отеля, примерно в 72% случаев.

def plot_metric(history, metric):
    train_metrics = history.history[metric]
    val_metrics = history.history['val_'+metric]
    epochs = range(1, len(train_metrics) + 1)
    plt.plot(epochs, train_metrics)
    plt.plot(epochs, val_metrics)
    plt.title('Training and validation '+ metric)
    plt.xlabel("Epochs")
    plt.ylabel(metric)
    plt.legend(["train_"+metric, 'val_'+metric])
    plt.show()
plt.figure(figsize = (12,8))
plot_metric(history, "loss")

predictions = model.predict(test_padded)
pred_0_1 = [round(x[0]) for x in predictions]
from sklearn.metrics import classification_report, confusion_matrix
print(confusion_matrix( y_test,pred_0_1))
print(classification_report(y_test,pred_0_1))
[[735 278]
 [302 751]]
              precision    recall  f1-score   support

           0       0.71      0.73      0.72      1013
           1       0.73      0.71      0.72      1053

    accuracy                           0.72      2066
   macro avg       0.72      0.72      0.72      2066
weighted avg       0.72      0.72      0.72      2066

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

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

model2 = keras.models.Sequential()
model2.add(layers.Embedding(num_unique_words, 60, input_length = max_length))


model2.add(layers.LSTM(40))
model2.add(layers.Dropout(0.2))
model2.add(layers.Dense(32))
model2.add(layers.Dropout(0.2))
model2.add(layers.Dense(16))
model2.add(layers.Dense(1))
model2.add(layers.Activation("sigmoid"))

model2.summary()
Model: "sequential_12"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_12 (Embedding)     (None, 70, 60)            1597440   
_________________________________________________________________
lstm_12 (LSTM)               (None, 40)                16160     
_________________________________________________________________
dropout_18 (Dropout)         (None, 40)                0         
_________________________________________________________________
dense_28 (Dense)             (None, 32)                1312      
_________________________________________________________________
dropout_19 (Dropout)         (None, 32)                0         
_________________________________________________________________
dense_29 (Dense)             (None, 16)                528       
_________________________________________________________________
dense_30 (Dense)             (None, 1)                 17        
_________________________________________________________________
activation_12 (Activation)   (None, 1)                 0         
=================================================================
Total params: 1,615,457
Trainable params: 1,615,457
Non-trainable params: 0
_________________________________________________________________
early_stop = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=20) 
    

model2.compile(loss = "binary_crossentropy",
             optimizer = "adam",
             metrics = ["accuracy"])
history2 = model2.fit(train_padded, y_train, 
          batch_size = 30, 
          epochs = 100, 
          validation_data=(test_padded, y_test),
             callbacks = [early_stop])
Train on 8264 samples, validate on 2066 samples
Epoch 1/100
8264/8264 [==============================] - 7s 788us/sample - loss: 0.6901 - accuracy: 0.5120 - val_loss: 0.6919 - val_accuracy: 0.5237
Epoch 2/100
8264/8264 [==============================] - 4s 532us/sample - loss: 0.5841 - accuracy: 0.6971 - val_loss: 0.4801 - val_accuracy: 0.7778
Epoch 3/100
8264/8264 [==============================] - 4s 528us/sample - loss: 0.3681 - accuracy: 0.8473 - val_loss: 0.4948 - val_accuracy: 0.7788
Epoch 4/100
8264/8264 [==============================] - 4s 520us/sample - loss: 0.2301 - accuracy: 0.9155 - val_loss: 0.5594 - val_accuracy: 0.7536
Epoch 5/100
8264/8264 [==============================] - 5s 553us/sample - loss: 0.1493 - accuracy: 0.9471 - val_loss: 0.6004 - val_accuracy: 0.7536
Epoch 6/100
8264/8264 [==============================] - 5s 545us/sample - loss: 0.1074 - accuracy: 0.9660 - val_loss: 0.8424 - val_accuracy: 0.7202
Epoch 7/100
8264/8264 [==============================] - 4s 536us/sample - loss: 0.0702 - accuracy: 0.9774 - val_loss: 1.2820 - val_accuracy: 0.7236
Epoch 8/100
8264/8264 [==============================] - 4s 543us/sample - loss: 0.0641 - accuracy: 0.9820 - val_loss: 0.8639 - val_accuracy: 0.7401
Epoch 9/100
8264/8264 [==============================] - 5s 549us/sample - loss: 0.0472 - accuracy: 0.9867 - val_loss: 1.1572 - val_accuracy: 0.7357
Epoch 10/100
8264/8264 [==============================] - 4s 533us/sample - loss: 0.0343 - accuracy: 0.9910 - val_loss: 1.1266 - val_accuracy: 0.7193
Epoch 11/100
8264/8264 [==============================] - 4s 524us/sample - loss: 0.0438 - accuracy: 0.9877 - val_loss: 1.5101 - val_accuracy: 0.7318
Epoch 12/100
8264/8264 [==============================] - 4s 511us/sample - loss: 0.0287 - accuracy: 0.9925 - val_loss: 1.2244 - val_accuracy: 0.7348
Epoch 13/100
8264/8264 [==============================] - 4s 501us/sample - loss: 0.0234 - accuracy: 0.9950 - val_loss: 1.6815 - val_accuracy: 0.7299
Epoch 14/100
8264/8264 [==============================] - 4s 504us/sample - loss: 0.0177 - accuracy: 0.9965 - val_loss: 1.5120 - val_accuracy: 0.7352
Epoch 15/100
8264/8264 [==============================] - 4s 504us/sample - loss: 0.0237 - accuracy: 0.9946 - val_loss: 1.3144 - val_accuracy: 0.7328
Epoch 16/100
8264/8264 [==============================] - 4s 511us/sample - loss: 0.0351 - accuracy: 0.9924 - val_loss: 1.1855 - val_accuracy: 0.7309
Epoch 17/100
8264/8264 [==============================] - 4s 505us/sample - loss: 0.0332 - accuracy: 0.9912 - val_loss: 1.3751 - val_accuracy: 0.7280
Epoch 18/100
8264/8264 [==============================] - 4s 503us/sample - loss: 0.0269 - accuracy: 0.9930 - val_loss: 1.4329 - val_accuracy: 0.7328
Epoch 19/100
8264/8264 [==============================] - 4s 500us/sample - loss: 0.0207 - accuracy: 0.9959 - val_loss: 1.2505 - val_accuracy: 0.7289
Epoch 20/100
8264/8264 [==============================] - 4s 506us/sample - loss: 0.0088 - accuracy: 0.9983 - val_loss: 1.7137 - val_accuracy: 0.7241
Epoch 21/100
8264/8264 [==============================] - 4s 500us/sample - loss: 0.0172 - accuracy: 0.9967 - val_loss: 1.5518 - val_accuracy: 0.7425
Epoch 22/100
8264/8264 [==============================] - 4s 503us/sample - loss: 0.0195 - accuracy: 0.9946 - val_loss: 1.8774 - val_accuracy: 0.7328
Epoch 00022: early stopping
predictions2 = model2.predict(test_padded)
pred2_0_1 = [round(x[0]) for x in predictions2]
print(confusion_matrix( y_test,pred2_0_1))
print(classification_report(y_test,pred2_0_1))
[[673 340]
 [212 841]]
              precision    recall  f1-score   support

           0       0.76      0.66      0.71      1013
           1       0.71      0.80      0.75      1053

    accuracy                           0.73      2066
   macro avg       0.74      0.73      0.73      2066
weighted avg       0.74      0.73      0.73      2066

С этой новой моделью мы достигли именно того, к чему стремились. В отчете о классификации четко указывается значение чувствительности около 80% без потери какой-либо «глобальной» предсказательной силы (поскольку точность, по сути, такая же, как у предыдущей модели), что в этом контексте означает, что наша модель была в состоянии определить 80% людей, которые были недовольны каким-либо аспектом отеля за счет выявления только 66% людей, которые дали идеальный отзыв.

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

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