在Pytorch中使用RNN查找評論隱藏的情緒

本文的目的是以簡單易懂的方式向您介紹RNN(循環神經網絡)在自然語言處理任務中的作用,以及如何將RNN用於情緒分類。如今,情緒分析已經成為越來越多應用程序的重要組成部分。現在幾乎所有品牌都想通過在線評論瞭解公眾對他們的整體態度,大多數人在購買或預訂任何商品時都會通過在線評論查看一下評論內容。因此,回答這些問題當然可以運用情緒分析。讓我們深入研究這個有趣的關於RNNs的問題。

我們為什麼要使用RNN?

句子中的單詞順序對準確預測句子的情緒有很大幫助。為什麼我們不能只使用簡單的單詞包,並根據單個詞的情緒預測整個段落的全部內容。由於使用單詞的順序,文本得到了它的真正意義。在線的評論有時充滿了諷刺和語言的扭曲使用,這需要複雜的系統來捕捉那些真實的情緒。我們來看看一些酒店預訂的評論:

我們訂的兩個雙人間,但驚喜的是他們給了我們兩個單人間!

這個句子不包含任何負面詞語,所以考慮到個別詞語的內涵來得出最終的情緒是不可能的。如果我只是顛倒幾個字,那麼這篇評論就會變得積極(從獲得更好的房間的角度來看)。

我們訂的兩個單人間,但驚喜的是他們給了我們兩個雙人間!

當然,酒店過於慷慨,額外提供令人驚喜的,或者他們正在嘗試這種驚喜優惠以吸引更多客戶,然後在積極情緒下獲取此評論將有助於他們更快地獲得選擇。這只是一個例子,你可以很容易地想象單詞的順序如何在理解句子的情緒中發揮關鍵作用。因此,如果您想讓您的情緒分析器更加智能,那麼單詞序列需要特別注意。

捕獲序列的方法意味著我們需要有某種記憶來記住前面的單詞,這正是RNN在正常的前饋神經網絡上提供的特徵。RNN可以記住過去的隱藏狀態,這實際上有助於引導最終輸出。

RNNs架構

簡單地說,RNN是具有環路的神經網絡。循環是用於提供先前狀態以使用過去信息的循環。確實在您進行下一次輸入時促進了先前狀態的信息流。輸入將一次提供一個字,因此當Xt-1被饋送到網絡時,時間Xt處的輸入將接收ht-1先前隱藏狀態。從下圖中可以看出,Whh是附加權重矩陣,有助於學習將前一個隱藏狀態連接到下一個隱藏狀態的權重。基本上,現在輸出不僅僅是輸入的函數,而是(當前輸入+先前狀態)的函數。

在Pytorch中使用RNN查找評論隱藏的情緒

帶有周期的RNN展開網絡

在每個步驟中,從先前輸入Xt-1生成的輸出ht-1被饋送到處理中,然後執行這個操作,直到序列中的最後一個元素。在時間t處計算ht的等式如下:

在Pytorch中使用RNN查找評論隱藏的情緒

這裡f通常是tanh或ReLU函數。在時間t處的輸出yt將是可以從softmax函數導出的類的概率分佈。

在Pytorch中使用RNN查找評論隱藏的情緒

序列中最後一個單詞的最終輸出是我們唯一關心情緒分類的輸出,但是在預測下一個單詞的問題時,我們會考慮每個時間的輸出。

在神經網絡訓練期間,RNN使用反向傳播時間(BPTT)。首先,根據ht和實際值計算損失,然後使用BPTT。為了訓練神經網絡,通常採用反向傳播的隨機梯度下降(SGD)法來最小化損失函數。如果您知道反向傳播是如何工作的,隨著越來越多的層的消失,同樣的問題也適用於RNN。神經網絡存在梯度消失問題。本文中非常直觀的解釋了這一點這使得理解這個問題以及訓練過程變得非常容易。

現在,您已經對RNNs的內部機制有一定的瞭解了,讓我們看看在pytorch中如何實現:

在Pytorch中實現

使用RNNs進行情緒分類實驗的數據集是Kaggle的twitter數據。該數據集包含有關美國航空公司的評論內容,並且有關於評論內容的三種情緒,其中包括標籤:

  1. 積極
  2. 中性
  3. 消極

顯示人們對這些航空公司的看法。我們的目標是根據評論上的文字來預測用戶當時的情緒。

1.加載和預處理數據

首先,我們加載必要的庫。我正在使用torch 1.0.0 。在預處理部分,對於每個句子,我們首先需要將它們分成標記。我也用Porter Stemmer來做詞幹。使用nltk包刪除stopwords。在讀取文件時,我將tweet文本添加到tweets變量中,並在tweet_sent_class中收集對應tweet的sentiment類。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
在Pytorch中使用RNN查找評論隱藏的情緒

2.構建特徵向量

在這裡,我們構建詞彙表以將唯一索引映射到單詞以獲得句子的輸入向量。然後將每個單詞通過單詞嵌入層傳遞,以獲得單詞特徵向量,該特徵向量將是我們RNN的實際輸入。在下面的代碼中,該函數map_word_vocab將從vocab中為句子中的每個單詞創建ids的張量。map_class將對向量中每個句子的輸出進行編碼,併為其所屬的情緒設置為1。我們將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):
classes = [0 for i in range(len(sentiment_class))]
classes[class_dict[sentiment]] = 1
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
在Pytorch中使用RNN查找評論隱藏的情緒

3.定義RNN類

現在我們進入了RNN類了。這將有4層:

  1. 輸入層
  2. 嵌入層
  3. RNN層
  4. 輸出層

輸入是單詞的序列。每個單詞都根據唯一單詞進行編碼。對於每個單詞,我們得到維度50的單詞嵌入,然後這些單詞依次傳遞到隱藏層大小為10的RNN網絡。輸出層將預測它最後所屬的類,因此它的大小等於類的數量(= 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)
在Pytorch中使用RNN查找評論隱藏的情緒

從類定義中可以看出,nn.embeddings用於創建單詞嵌入,輸入和隱藏之間以及輸入到輸出之間的層是簡單的線性層。輸出層是softmax層。

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()
在Pytorch中使用RNN查找評論隱藏的情緒

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))
在Pytorch中使用RNN查找評論隱藏的情緒

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))
在Pytorch中使用RNN查找評論隱藏的情緒

獲得的結果:

['negative', 'positive', 'neutral']
[[2980 291 935]
[ 203 255 143]

[ 373 107 353]]
0.6361702127659574

當然,使用不同的隱藏和嵌入大小選擇以及週期數量,可以使用更多文檔來改進此結果。被刪除的stopwords也可以保留,因為它可能表示更好的序列直覺,而且不要忘記他們是評論,它包含特殊字符和標記,因此刪除也可以提高輸入的質量。我們在這個實驗中使用了可變大小的輸入(句子長度),填充輸入向量也可以用來改善訓練。RNN只能存儲很少的上下文,因此使用LSTMs和GRUs來處理這些上下文較長的任務會更好。


分享到:


相關文章: