Определение архитектуры модели LSTM и ее реализация для анализа настроений

Что такое анализ настроений?

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

Построение классификатора с более высокой точностью для определения тональности предложения было одной из наиболее изученных областей обработки естественного языка (NLP). В этой статье мы создадим классификатор тональности для английского языка, используя особый вид рекуррентной нейронной сети (RNN): сеть с долгой краткосрочной памятью (LSTM).

Что такое LSTM?

LSTM был представлен Hochreiter & Schmidhuber в 1997 году как решение проблемы исчезающего градиента, с которой сталкивается большинство RNN. Проще говоря, читая книгу, вы вспоминаете, что произошло в предыдущей главе. RNN может запоминать предыдущую информацию и использовать ее в качестве входных данных для обработки. Но недостатком RNN является то, что они не могут запоминать долгосрочные состояния из-за проблемы исчезающего градиента.

Поэтому LSTM специально разработаны, чтобы избежать этой проблемы. LSTM имеет соединение обратной связи, которое может обрабатывать последовательности данных. Наличие обратной связи помогает обрабатывать более одной точки данных. Базовая модель LSTM состоит из ячейки памяти, входного элемента, выходного элемента и элемента забывания. Ячейка памяти помогает сохранять информацию в памяти в течение более длительного периода. Таким образом, эта сеть хорошо подходит для классификации, прогнозирования на основе временных данных и т. Д.

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

Обязательные зависимости

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

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import nltk

from nltk.tokenize import word_tokenize
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Embedding, LSTM, Dense, Dropout, Bidirectional
from keras.initializers import Constant
from sklearn.preprocessing import LabelEncoder

import warnings
warnings.filterwarnings('ignore')
sns.set()

Выше представлены библиотеки, состоящие из основных инструментов анализа и интеллектуального анализа данных, таких как NumPy, Pandas, Seaborn и matplotlib, набор инструментов естественного языка (NLTK) для обработки естественного языка и Keras для создания нашей искусственной нейронной сети.

Подпишитесь на еженедельник Deep Learning Weekly и присоединяйтесь к более чем 15 000 ваших коллег. Еженедельный доступ к последним новостям индустрии глубокого обучения, исследованиям, библиотекам кода, руководствам и многому другому.

Чтение набора данных

dataset = pd.read_csv("IMDB Dataset.csv")
dataset.head()
--Output--
                                             review  | sentiment
0 |One of the other reviewers has mentioned that ... | positive
1 |A wonderful little production. <br /><br />The... | positive
2 |I thought this was a wonderful way to spend ti... | positive
3 |Basically there's a family where a little boy ... | negative
4 |Petter Mattei's "Love in the Time of Money" is... | positive

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

dataset.sentiment.value_counts()
--Output--
positive    25000
negative    25000
Name: sentiment, dtype: int64

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

Разложение предложений на слова

Нам нужно разделить предложения на слова для токенизации и встраивания слов (подробнее об этих процессах см. Ниже).

word_corpus = []
for text in dataset['review']:
    words = [word.lower() for word in word_tokenize(text)] 
    word_corpus.append(words)
numberof_words = len(word_corpus)
print(numberof_words)
--Output--
50000

Разделение набора данных

Набор данных необходимо разделить на обучение и тестирование (80% и 20%).

train_size = int(dataset.shape[0] * 0.8)
X_train = dataset.review[:train_size]
y_train = dataset.sentiment[:train_size]
X_test = dataset.review[train_size:]
y_test = dataset.sentiment[train_size:]

Токенизация

Токенизация - это процесс разбиения фразы, предложения, абзаца или полного текста документа на более мелкие компоненты, такие как слова или термины.

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

tokenizer = Tokenizer(numberof_words)
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_train = pad_sequences(X_train, maxlen=128, truncating='post', padding='post')

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

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

X_train[0], len(X_train[0])
--Output--
(array([ 27,    4,   1,   80, 2102,  45, 1073,   12, 100,
        147,   39, 316, 2968,  409, 459,   26, 3173,  33,
         23,  200,  14,   11,    6, 614,   48,  606,  16,
         68,    7,   7,    1,   87, 148,   12, 3256,  68,
         41, 2968,  13,   92, 5626,   2,16202,  134,   4,
        569,   60, 271,    8,  200,  36,    1,  673, 139,
       1712,   68,  11,    6,   21,   3,  118,   15,   1,
       7870, 2257,  38,11540,   11, 118, 2495,   54,5662,
         16, 5182,   5, 1438,  377,  38,  569,   92,   6,
       3730,    8,   1,  360,  353,   4,    1,  673,   7,
          7,    9,   6,  431, 2968,  14,   12,    6,   1,
      11736,  356,   5,    1,14689,6526, 2594, 1087,   9,
       2661, 1432,  20,22583,  534,  32, 4795, 2451,   4,
          1, 1193, 117,   29,    1,6893,   25, 2874,12191,
          2,  392], dtype=int32), 128)

Как видно из вышеприведенного вывода, все слова представлены числами. Следовательно, слова, доступные в X_train, были фактически токенизированы.

Проделаем то же самое с набором X_test.

X_test = tokenizer.texts_to_sequences(X_test)
X_test = pad_sequences(X_test, maxlen=128, truncating='post', padding='post')

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

index = tokenizer.word_index
print("Count of unique words: {}".format(len(index)))
--Output--
Count of unique words: 112173

Вложение слов

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

Во время этой реализации мы будем использовать вложения GloVe. Вы можете попробовать различные инструменты для встраивания слов, такие как Word2Vec, FastText и т. Д.

Во-первых, нам нужно составить словарь всех слов в корпусе, используя предварительно обученные вложения GloVe. (Щелкните встроенную ссылку, чтобы загрузить предварительно обученные векторы слов)

embeddings = {}
with open("glove.twitter.27B.100d.txt") as file:
    for line in file:
        val = line.split()
        words = val[0]
        vectors = np.asarray(val[1:], 'float32')
        embeddings[words] = vectors
file.close()

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

emb_matrix = np.zeros((numberof_words, 100))
for i, word in tokenizer.index_word.items():
    if i < (numberof_words+1):
        vector = embeddings.get(word)
        if vector is not None:
            emb_matrix[i] = vector

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

Создание модели

Перед созданием модели нам нужно закодировать метки в наборе данных с помощью LabelEncoder().

labels = LabelEncoder()
y_train = labels.fit_transform(y_train)
y_test = labels.transform(y_test)

Есть два типа способов создать модель с помощью Keras.

  • Последовательный API
  • Функциональный API

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

model = Sequential()
model.add(Embedding(input_dim=num_words, output_dim=100, 
                  embeddings_initializer=Constant(embedding_matrix), 
                    input_length=128, trainable=False))
model.add(LSTM(100, dropout=0.1))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

Я применил следующие слои:

  • 1-й уровень - слой встраивания: преобразует токенизированные слова во встраивание определенного размера.
  • 2-й слой - слой LSTM: содержит затемнения скрытого состояния и дополнительные слои
  • 3-й слой - плотный слой: соединяет все выходы из предыдущих слоев со всеми его нейронами.
  • Функция активации - сигмовидная функция активации: преобразует все выходы в значения от 0 до 1.
  • Функция потерь - двоичная кросс-энтропия: сравнивает выходные данные и прогнозирует фактический выход класса между 0 и 1
model.summary()
--Output--
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 128, 100)          5000000   
_________________________________________________________________
lstm (LSTM)                  (None, 100)               80400     
_________________________________________________________________
dense (Dense)                (None, 1)                 101       
=================================================================
Total params: 5,080,501
Trainable params: 80,501
Non-trainable params: 5,000,000
_________________________________________________________________

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

history = model.fit(X_train, y_train, epochs=5, batch_size=2048, validation_data=(X_test, y_test))
--Output--
Epoch 1/5
20/20 [==============================] - 55s 3s/step - loss: 0.6879 - accuracy: 0.5345 - val_loss: 0.6384 - val_accuracy: 0.6338
Epoch 2/5
20/20 [==============================] - 45s 2s/step - loss: 0.6074 - accuracy: 0.6816 - val_loss: 0.5314 - val_accuracy: 0.7451
Epoch 3/5
20/20 [==============================] - 45s 2s/step - loss: 0.5362 - accuracy: 0.7417 - val_loss: 0.5010 - val_accuracy: 0.7680
Epoch 4/5
20/20 [==============================] - 45s 2s/step - loss: 0.5080 - accuracy: 0.7597 - val_loss: 0.4718 - val_accuracy: 0.7783
Epoch 5/5
20/20 [==============================] - 45s 2s/step - loss: 0.4871 - accuracy: 0.7646 - val_loss: 0.4719 - val_accuracy: 0.7745

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

Построение убытков

plt.figure(figsize=(16,5))
epochs = range(1, len(history.history['accuracy'])+1)
plt.plot(epochs, history.history['loss'], 'b', label='Training Loss', color='red')
plt.plot(epochs, history.history['val_loss'], 'b', label='Validation Loss')
plt.legend()
plt.show()

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

Построение графиков точности

plt.figure(figsize=(16,5))
epochs = range(1, len(history.history['accuracy'])+1)
plt.plot(epochs, history.history['accuracy'], 'b', label='Training Accuracy', color='red')
plt.plot(epochs, history.history['val_accuracy'], 'b', label='Validation Accuracy')
plt.legend()
plt.show()

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

Проверка

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

sentence = ['This movie was the worst. Acting was not good.']
sentence_tokened = tokenizer.texts_to_sequences(sentence)
sentence_padded = pad_sequences(sentence_tokened,maxlen=128,truncating='post',                                     padding='post')
print(sentence[0])
print("Positivity of the sentence:{}".format(model.predict(sentence_padded)[0]))
--Output--
This movie was the worst. Acting was not good.
Positivity of the sentence: [0.14108086]

Как видно из вышеприведенных результатов, наша модель смогла идентифицировать предложение как предложение с низким рейтингом положительности, даже со словом «хорошо», включенным в него. Вы можете включить матрицу по вашему выбору, чтобы пометить вывод как положительный или отрицательный. Например, 0–40 - отрицательное, 40–60 - нейтральное и 60–100 - положительное.

Исходный код:

Заключение

Вы можете использовать разные модели с указанным выше руководством и проверить их точность. Так вы поймете, чем LSTM выделяется по сравнению с другими RNN. Но есть и более новые модели, такие как двунаправленный LSTM, BERT, EMO и т. Д., Которые могут обеспечить еще более высокие показатели точности.

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

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

Являясь независимой редакцией, Heartbeat спонсируется и публикуется Comet, платформой MLOps, которая позволяет специалистам по данным и группам машинного обучения отслеживать, сравнивать, объяснять и оптимизировать свои эксперименты. Мы платим участникам и не продаем рекламу.

Если вы хотите внести свой вклад, отправляйтесь на наш призыв к участникам. Вы также можете подписаться на наши еженедельные информационные бюллетени (Deep Learning Weekly и Comet Newsletter), присоединиться к нам в » «Slack и подписаться на Comet в Twitter и LinkedIn для получения ресурсов, событий и гораздо больше, что поможет вам быстрее создавать лучшие модели машинного обучения.