Поиск скрытых чувств с помощью Deep Learning в Pytorch

Цель этого блога — простым и понятным способом представить вам, какова роль RNN (рекуррентных нейронных сетей) в задачах обработки естественного языка и как RNN можно использовать для классификации настроений с ее реализацией для данные Twitter. ». В наши дни анализ настроений стал неотъемлемой частью многих будущих приложений. Почти все бренды в настоящее время хотят знать общее отношение публики к ним из онлайн-комментариев, большинство людей просматривают отзывы, прежде чем покупать или бронировать что-либо в Интернете, финансовые отрасли следят за развитием тенденций, будь то политика, где обсуждаются горячие темы. или общественное мнение анализируется. Следовательно, ответы на эти вопросы — это то, где, безусловно, можно использовать анализ настроений. Итак, давайте погрузимся в эту интересную проблему с RNN!

Почему мы должны использовать RNN для этого?

Порядок слов в предложении очень помогает точно предсказать настроение предложения. Ну, почему мы не можем просто использовать простой набор слов и, основываясь на тональности отдельных слов, предсказать, о чем весь абзац. Хм… это не ежу понятно, текст получает свое реальное значение из-за последовательности, в которой используются слова. Не забывайте, что твиты и разговоры в Интернете полны сарказма и искаженного использования слов, что требует сложной системы для улавливания реальных настроений этих людей. Давайте посмотрим на обзор, сделанный для бронирования отелей:

Мы просили два двухместных номера, но неожиданно нам дали два одноместных номера!

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

Мы просили два одноместных номера, но неожиданно нам дали два двухместных номера!

Кому не понравятся двухместные номера, верно!? Конечно, отель слишком щедр на дополнительные предложения, что удивительно, или они пытались использовать эти неожиданные предложения, чтобы привлечь больше клиентов, тогда захват этого отзыва в положительном настроении помог бы им быстрее получить обзор. Таким образом, если мы не можем зафиксировать последовательность, если "одноместный номер" стоит перед словом "но" или "двухместный номер" перед "но" будет сложно уловить настоящие чувства. Это всего лишь один пример, вы можете легко себе представить, как порядок слов играет ключевую роль в понимании настроения предложения. Поэтому, если вы хотите сделать свой анализатор тональности умнее, последовательность слов требует особого внимания.

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

Архитектура RNN

Проще говоря, RNN — это нейронные сети с петлями в них. Как вы могли догадаться, циклы — это те, которые используются для подачи информации из прошлого в предыдущее состояние. Действительно облегчая поток информации из предыдущего состояния, пока вы проходите через следующий вход. Ввод будет предоставляться по одному слову за раз, поэтому ввод в момент времени x_t получит h_t-1 предыдущего скрытого состояния, когда x_t-1 был передан в сеть. Как видно из рисунка ниже, W_hh — это дополнительная матрица весов, которая поможет узнать веса для соединения предыдущего скрытого состояния со следующим. По сути, теперь выход — это не просто функция ввода, а функция (текущий ввод + предыдущее состояние). Вход используется только для изменения состояния хранимой памяти, в отличие от обычной сети, где он напрямую влияет на вывод.

На каждом шаге выход h_t-1, сгенерированный из предыдущего входа x_t-1, подается на обработку, и это продолжается до последнего элемента в последовательности. Уравнение для расчета h_t на временном шаге t выглядит следующим образом:

h_t = f( (W_hh)(h_t-1) + (W_hx)(x_t))

Здесь f обычно представляет собой функцию tanh или ReLU. Выход y_t на временном шаге t будет распределением вероятностей по классам, которое может быть получено из функции softmax.

y_t = softmax((W_yh)(h_t))

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

Во время обучения нейронной сети RNN используют обратное распространение во времени (BPTT). Сначала вы рассчитываете убыток на основе h_t и фактического значения, а затем используете BPTT. Для обучения нейронной сети часто используется стохастический градиентный спуск (SGD) через обратное распространение, чтобы минимизировать функцию потерь. Если вы знаете, как работает обратное распространение, та же проблема с исчезновением градации с увеличением количества слоев применима и к RNN. Здесь вы можете представить каждый временной шаг как слой (только в нашем случае у нас будут одинаковые веса), тогда самый первый временной шаг получит наименьшие корректировки своих весов в виде градиентов, и, таким образом, это все равно, что не помнить первые несколько слова в предложении. RNN страдают от проблемы исчезающего градиента. Это довольно хорошо наглядно объяснено в этой статье, что позволяет очень легко понять эту проблему, а также процесс обучения.

Теперь, когда у вас есть некоторое представление о внутренних механизмах RNN, давайте посмотрим, как это реализовать в pytorch :)

Реализация в Pytorch

Набор данных, используемый для экспериментов с классификацией настроений с RNN, — это данные Twitter от Kaggle. Набор данных содержит твиты об авиакомпаниях США и имеет три мнения о твитах, включая теги:

  1. Положительный
  2. Нейтральный
  3. Отрицательный

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

1. Загрузка и предварительная обработка данных

Сначала загружаем необходимые библиотеки. Я использую torch 1.0.0 . В части предварительной обработки для каждого предложения нам нужно сначала разбить их на токены. Я также использовал Портера Стеммера, чтобы расшифровать слова. Удаление стоп-слов выполняется с помощью пакета nltk. При чтении файла я добавляю текст твита в переменную tweets и собираю класс тональности соответствующего твита в tweet_sent_class. sentiment_class используется для отслеживания уникальных классов. (Здесь мы знаем, что это три класса, но для вашей будущей программы это можно использовать для автоматического определения классов)

#Importing the necessary libraries
import csv
import torch
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
# Variable for location of the tweets.csv file
INPUTFILE_PATH = "data/twitter-airline-sentiment/Tweets.csv"
tweets = []
train_tweets =[]
test_tweets = []
sentiment_class = set()
tweet_sent_class= []
porter = PorterStemmer()
stop_words = set(stopwords.words('english'))
def tokenizer(sentence):
    tokens = sentence.split(" ")
    tokens = [porter.stem(token.lower()) for token in tokens if not token.lower() in stop_words]
    return tokens
i = 0
with open(INPUTFILE_PATH, 'r') as csvfile:
    tweetreader = csv.reader(csvfile, delimiter=',', quotechar='"')
    for row in tweetreader:
        # For skipping the headerline
        if i == 0:
            i += 1
            continue
        # tweets will contain the tweet text 
        tweets.append(tokenizer(row[10]))
        tweet_sent_class.append(row[1])
        sentiment_class.add(row[1])
        i += 1

2. Построение вектора признаков

Здесь мы создаем словарь для сопоставления уникального индекса со словами для получения входного вектора для предложения. Затем каждое слово будет проходить через слой встраивания слов, чтобы получить вектор признаков слова, который будет фактическим входом в нашу RNN. В следующем коде функция map_word_vocab создаст тензор идентификаторов из словаря для каждого слова в предложении. map_class будет кодировать вывод для каждого предложения в векторе с индексом класса тональности. Мы устанавливаем EMBEDDING_DIM на 50 и HIDDEN_DIM на 10, что будет размером скрытого слоя в нашей сети RNN. EMBEDDING_DIM — это размер встраивания слов, который нам нужен. Разделение обучения и тестирования выполняется с первыми 9000 документами для обучения и 5640 для тестирования.

class_dict = {}
for index, class_name in enumerate(sentiment_class):
    class_dict[class_name] = index
vocab = {}
vocab_index = 0
for tokens in tweets:
    for key, token in enumerate(tokens):
        #all_tokens.add(token)
        if token not in vocab:
            vocab[token] = vocab_index
            vocab_index += 1
#train test split
train_tweets = tweets[:9000]
test_tweets = tweets[9000:]
def map_word_vocab(sentence):
    idxs = [vocab[w] for w in sentence]
    return torch.tensor(idxs, dtype=torch.long)
def map_class(sentiment):
    return torch.tensor([class_dict[sentiment]], dtype=torch.long)
def prepare_sequence(sentence):
    # create the input feature vector
    input = map_word_vocab(sentence)
    return input
EMBEDDING_DIM = 50
HIDDEN_DIM = 10

3. Определение класса RNN

Теперь мы, наконец, переходим к нашему классу RNN. Здесь будет 4 слоя:

  1. Входной слой
  2. Встраиваемый слой
  3. Слой RNN
  4. Выходной слой

Вход - это последовательность слов. Каждое слово кодируется уникальным словом. Для каждого слова мы получаем вложение слова размерности 50, а затем каждое из этих слов последовательно передается в нашу сеть RNN со скрытым размером слоя 10. Выходной слой будет предсказывать класс, к которому он принадлежит в конце, поэтому он имеет размер равно количеству классов (=3). Мы инициализируем класс RNN в конце вместе с функцией потерь negative log likelihood и оптимизаторомSGD.

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, vocab_size, output_size):
        super(RNN, self).__init__()
        self.hidden_size = hidden_size
        self.word_embeddings = nn.Embedding(vocab_size, input_size)
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)
def forward(self, word, hidden):
        embeds = self.word_embeddings(word)
        combined = torch.cat((embeds.view(1, -1), hidden), 1)
        hidden = self.i2h(combined)
        output = self.i2o(combined)
        output = self.softmax(output)
        return output, hidden
def init_hidden(self):
        return torch.zeros(1, self.hidden_size)
# creating an instance of RNN
rnn = RNN(EMBEDDING_DIM, HIDDEN_DIM, len(vocab), len(sentiment_class))
# Setting the loss function and optimizer
loss_function = nn.NLLLoss()
optimizer = optim.SGD(rnn.parameters(), lr=0.001)

Как видно из определения класса, nn.embeddings используется для создания вложений слов, слои между входным и скрытым, а также между входным и выходным являются простыми линейными слоями. Выходной слой - это слой softmax. Вы можете быстро взглянуть на архитектуру этого из учебника pytorch по классификации уровней символов с использованием RNN (сетевая диаграмма), на которую я ссылался.

4. Обучение сети

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

for epoch in range(30):  
    if epoch % 5 == 0:
        print("Finnished epoch " + str(epoch / 30 * 100)  + "%")
    for i in range(len(train_tweets)):
        sentence = train_tweets[i]
        sent_class = tweet_sent_class[i]
# Step 1. Remember that Pytorch accumulates gradients.
        # We need to clear them out before each instance
# Also, we need to clear out the hidden state of the LSTM,
        # detaching it from its history on the last instance.
        hidden = rnn.init_hidden()
        rnn.zero_grad()
# Step 2. Get our inputs ready for the network, that is, turn them into
        # Tensors of word indices.
        sentence_in = prepare_sequence(sentence)
        target_class = map_class(sent_class)

        # Step 3. Run our forward pass.
        for i in range(len(sentence_in)):
            class_scores, hidden = rnn(sentence_in[i], hidden)

        # Step 4. Compute the loss, gradients, and update the parameters by
        #  calling optimizer.step()
        loss = loss_function(class_scores, target_class)
        loss.backward()
        optimizer.step()

5. Прогноз для тестовых данных

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

# Convert the sentiment_class from set to list
sentiment_class = list(sentiment_class)

y_pred = []
y_actual = []
with torch.no_grad():
    for i in range(len(test_tweets)):
        sentence = test_tweets[i]
        sent_class = tweet_sent_class[9000+i]
        inputs = prepare_sequence(sentence)
        hidden = rnn.init_hidden()
        for i in range(len(inputs)):
            class_scores, hidden = rnn(inputs[i], hidden)
        # for word i. The predicted tag is the maximum scoring tag.
        y_pred.append(sentiment_class[((class_scores.max(dim=1)[1].numpy()))[0]])
        y_actual.append(str(sent_class))

6. Результаты

Используя эту простую модель с 30 эпохами и всего 9000 обучающими документами, полученная точность составила ~ 63,62%, а матрица путаницы показана ниже.

print(sentiment_class)
print(confusion_matrix(y_actual, y_pred, labels=sentiment_class))
print(accuracy_score(y_actual, y_pred))

Полученные результаты:

['negative', 'positive', 'neutral']
[[2980  291  935]
 [ 203  255  143]
 [ 373  107  353]]
0.6361702127659574

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

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

Надеюсь, вы найдете это полезным в своем путешествии по изучению и исследованию !! :)

Ссылки:

  1. Глубокое обучение для анализа настроений: https://arxiv.org/pdf/1801.07883.pdf

2. LSTM: http://colah.github.io/posts/2015-08-Understanding-LSTMs/

3. Обратное распространение: http://colah.github.io/posts/2015-08-Backprop/

4. Классификация уровня персонажа Pytorch с RNN: https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html

5. РНС с нуля на питоне (интуитивная визуализация): https://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/

Автор: Дипика Баад