LSTM sentiment analysis(情感分析)

LSTM情感分析

分析

詞向量模型

計算機可只認識數字!
在這裏插入圖片描述
我們可以將一句話中的每一個詞都轉換成一個向量
在這裏插入圖片描述
你可以將輸入數據看成是一個 16D 的一個矩陣。

詞向量是具有空間意義的並不是簡單的映射!例如,我們希望單詞 “love” 和 “adore” 這兩個詞在向量空間中是有一定的相關性的,因爲他們有類似的定義,他們都在類似的上下文中使用。單詞的向量表示也被稱之爲詞嵌入。
在這裏插入圖片描述

Word2Vec

爲了去得到這些詞嵌入,我們使用一個非常厲害的模型 “Word2Vec”。簡單的說,這個模型根據上下文的語境來推斷出每個詞的詞向量。如果兩個個詞在上下文的語境中,可以被互相替換,那麼這兩個詞的距離就非常近。在自然語言中,上下文的語境對分析詞語的意義是非常重要的。比如,之前我們提到的 “adore” 和 “love” 這兩個詞,我們觀察如下上下文的語境。

在這裏插入圖片描述

從句子中我們可以看到,這兩個詞通常在句子中是表現積極的,而且一般比名詞或者名詞組合要好。這也說明了,這兩個詞可以被互相替換,他們的意思是非常相近的。對於句子的語法結構分析,上下文語境也是非常重要的。所有,這個模型的作用就是從一大堆句子(以 Wikipedia 爲例)中爲每個獨一無二的單詞進行建模,並且輸出一個唯一的向量。Word2Vec 模型的輸出被稱爲一個嵌入矩陣。
在這裏插入圖片描述
這個嵌入矩陣包含訓練集中每個詞的一個向量。傳統來講,這個嵌入矩陣中的詞向量數據會很大。

Word2Vec 模型根據數據集中的每個句子進行訓練,並且以一個固定窗口在句子上進行滑動,根據句子的上下文來預測固定窗口中間那個詞的向量。然後根據一個損失函數和優化方法,來對這個模型進行訓練。

RNN

現在,我們已經得到了神經網絡的輸入數據 —— 詞向量,接下來讓我們看看需要構建的神經網絡。NLP 數據的一個獨特之處是它是時間序列數據。每個單詞的出現都依賴於它的前一個單詞和後一個單詞。由於這種依賴的存在,我們使用循環神經網絡來處理這種時間序列數據。

循環神經網絡的結構和你之前看到的那些前饋神經網絡的結構可能有一些不一樣。前饋神經網絡由三部分組成,輸入層,隱藏層和輸出層。
在這裏插入圖片描述
前饋神經網絡和 RNN 之前的主要區別就是 RNN 考慮了時間的信息。在 RNN 中,句子中的每個單詞都被考慮上了時間步驟。實際上,時間步長的數量將等於最大序列長度。

在這裏插入圖片描述

與每個時間步驟相關聯的中間狀態也被作爲一個新的組件,稱爲隱藏狀態向量 h(t) 。從抽象的角度來看,這個向量是用來封裝和彙總前面時間步驟中所看到的所有信息。就像 x(t) 表示一個向量,它封裝了一個特定單詞的所有信息。

隱藏狀態是當前單詞向量和前一步的隱藏狀態向量的函數。並且這兩項之和需要通過激活函數來進行激活。
在這裏插入圖片描述
在這裏插入圖片描述

LSTM

長短期記憶網絡單元,是另一個 RNN 中的模塊。從抽象的角度看,LSTM 保存了文本中長期的依賴信息。正如我們前面所看到的,H 在傳統的RNN網絡中是非常簡單的,這種簡單結構不能有效的將歷史信息鏈接在一起。舉個例子,在問答領域中,假設我們得到如下一段文本,那麼 LSTM 就可以很好的將歷史信息進行記錄學習。

在這裏插入圖片描述
在這裏,我們看到中間的句子對被問的問題沒有影響。然而,第一句和第三句之間有很強的聯繫。對於一個典型的RNN網絡,隱藏狀態向量對於第二句的存儲信息量可能比第一句的信息量會大很多。但是LSTM,基本上就會判斷哪些信息是有用的,哪些是沒用的,並且把有用的信息在 LSTM 中進行保存。

我們從更加技術的角度來談談 LSTM 單元,該單元根據輸入數據 x(t) ,隱藏層輸出 h(t) 。在這些單元中,h(t) 的表達形式比經典的 RNN 網絡會複雜很多。這些複雜組件分爲四個部分:輸入門,輸出門,遺忘門和一個記憶控制器。

在這裏插入圖片描述
每個門都將 x(t) 和 h(t-1) 作爲輸入(沒有在圖中顯示出來),並且利用這些輸入來計算一些中間狀態。每個中間狀態都會被送入不同的管道,並且這些信息最終會彙集到 h(t) 。爲簡單起見,我們不會去關心每一個門的具體推導。這些門可以被認爲是不同的模塊,各有不同的功能。輸入門決定在每個輸入上施加多少強調,遺忘門決定我們將丟棄什麼信息,輸出門根據中間狀態來決定最終的 h(t) 。

案例流程

  1. 製作詞向量,可以使用gensim這個庫,也可以直接用現成的
  2. 詞和ID的映射
  3. 構建RNN網絡架構
  4. 訓練我們的模型
  5. 測試

代碼演示

導入數據

首先,我們需要去創建詞向量。

# write in jupyter notebook

import numpy as np
wordsList = np.load('./training_data/wordsList.npy')
print('Loaded the word list!')
wordsList = wordsList.tolist() #Originally loaded as numpy array
wordsList = [word.decode('UTF-8') for word in wordsList] #Encode words as UTF-8
wordVectors = np.load('./training_data/wordVectors.npy')
print ('Loaded the word vectors!')

詞向量創建完成之後,我們的第一步就是輸入一個句子,然後構造它的向量表示。假設我們現在的輸入句子是 “I thought the movie was incredible and inspiring”。爲了得到詞向量,我們可以使用 TensorFlow 的嵌入函數。這個函數有兩個參數,一個是嵌入矩陣(在我們的情況下是詞向量矩陣),另一個是每個詞對應的索引。

import tensorflow as tf
maxSeqLength = 10 #Maximum length of sentence
numDimensions = 300 #Dimensions for each word vector
firstSentence = np.zeros((maxSeqLength), dtype='int32')
firstSentence[0] = wordsList.index("i")
firstSentence[1] = wordsList.index("thought")
firstSentence[2] = wordsList.index("the")
firstSentence[3] = wordsList.index("movie")
firstSentence[4] = wordsList.index("was")
firstSentence[5] = wordsList.index("incredible")
firstSentence[6] = wordsList.index("and")
firstSentence[7] = wordsList.index("inspiring")
#firstSentence[8] and firstSentence[9] are going to be 0
print(firstSentence.shape)
print(firstSentence) #Shows the row index for each word

在整個訓練集上面構造索引之前,我們先花一些時間來可視化我們所擁有的數據類型。這將幫助我們去決定如何設置最大序列長度的最佳值。在前面的例子中,我們設置了最大長度爲 10,但這個值在很大程度上取決於你輸入的數據。

訓練集我們使用的是 IMDB 數據集。這個數據集包含 25000 條電影數據,其中 12500 條正向數據,12500 條負向數據。這些數據都是存儲在一個文本文件中,首先我們需要做的就是去解析這個文件。正向數據包含在一個文件中,負向數據包含在另一個文件中。

from os import listdir
from os.path import isfile, join
import matplotlib.pyplot as plt
%matplotlib inline

positiveFiles = ['./training_data/positiveReviews/' + f for f in listdir('./training_data/positiveReviews/') if isfile(join('./training_data/positiveReviews/', f))]
negativeFiles = ['./training_data/negativeReviews/' + f for f in listdir('./training_data/negativeReviews/') if isfile(join('./training_data/negativeReviews/', f))]
numWords = []
for pf in positiveFiles:
    with open(pf, "r", encoding='utf-8') as f:
        line=f.readline()
        counter = len(line.split())
        numWords.append(counter)       
print('Positive files finished')

for nf in negativeFiles:
    with open(nf, "r", encoding='utf-8') as f:
        line=f.readline()
        counter = len(line.split())
        numWords.append(counter)  
print('Negative files finished')

numFiles = len(numWords)
print('The total number of files is', numFiles)
print('The total number of words in the files is', sum(numWords))
print('The average number of words in the files is', sum(numWords)/len(numWords))

plt.hist(numWords, 50)
plt.xlabel('Sequence Length')
plt.ylabel('Frequency')
plt.axis([0, 1200, 0, 8000])
plt.show()

在這裏插入圖片描述
從直方圖和句子的平均單詞數,我們認爲將句子最大長度設置爲 250 是可行的。

maxSeqLength = 250

接下來,我們將它轉換成一個索引矩陣。

# 刪除標點符號、括號、問號等,只留下字母數字字符
import re
strip_special_chars = re.compile("[^A-Za-z0-9 ]+")

def cleanSentences(string):
    string = string.lower().replace("<br />", " ")
    return re.sub(strip_special_chars, "", string.lower())
firstFile = np.zeros((maxSeqLength), dtype='int32')
with open(fname) as f:
    indexCounter = 0
    line=f.readline()
    cleanedLine = cleanSentences(line)
    split = cleanedLine.split()
    for word in split:
        try:
            firstFile[indexCounter] = wordsList.index(word)
        except ValueError:
            firstFile[indexCounter] = 399999 #Vector for unknown words
        indexCounter = indexCounter + 1

現在,我們用相同的方法來處理全部的 25000 條評論。我們將導入電影訓練集,並且得到一個 25000 * 250 的矩陣。這是一個計算成本非常高的過程,可以直接使用理好的索引矩陣文件。

ids = np.load('./training_data/idsMatrix.npy')

輔助函數

from random import randint

def getTrainBatch():
    labels = []
    arr = np.zeros([batchSize, maxSeqLength])
    for i in range(batchSize):
        if (i % 2 == 0): 
            num = randint(1,11499)
            labels.append([1,0])
        else:
            num = randint(13499,24999)
            labels.append([0,1])
        arr[i] = ids[num-1:num]
    return arr, labels

def getTestBatch():
    labels = []
    arr = np.zeros([batchSize, maxSeqLength])
    for i in range(batchSize):
        num = randint(11499,13499)
        if (num <= 12499):
            labels.append([1,0])
        else:
            labels.append([0,1])
        arr[i] = ids[num-1:num]
    return arr, labels

RNN Model

首先,我們需要去定義一些超參數,比如批處理大小,LSTM的單元個數,分類類別和訓練次數。

batchSize = 24
lstmUnits = 64
numClasses = 2
iterations = 50000

現在我們需要指定兩個佔位符,一個用於數據輸入,另一個用於標籤數據。對於佔位符,最重要的一點就是確定好維度。

標籤佔位符代表一組值,每一個值都爲 [1,0] 或者 [0,1],這個取決於數據是正向的還是負向的。輸入佔位符,是一個整數化的索引數組。

import tensorflow as tf
tf.reset_default_graph()

labels = tf.placeholder(tf.float32, [batchSize, numClasses])
input_data = tf.placeholder(tf.int32, [batchSize, maxSeqLength])

一旦,我們設置了我們的輸入數據佔位符,我們可以調用 tf.nn.embedding_lookup() 函數來得到我們的詞向量。該函數最後將返回一個三維向量,第一個維度是批處理大小,第二個維度是句子長度,第三個維度是詞向量長度。

data = tf.Variable(tf.zeros([batchSize, maxSeqLength, numDimensions]),dtype=tf.float32)
data = tf.nn.embedding_lookup(wordVectors,input_data)

現在我們已經得到了我們想要的數據形式,那麼揭曉了我們看看如何才能將這種數據形式輸入到我們的 LSTM 網絡中。首先,我們使用 tf.nn.rnn_cell.BasicLSTMCell 函數,這個函數輸入的參數是一個整數,表示需要幾個 LSTM 單元。這是我們設置的一個超參數,我們需要對這個數值進行調試從而來找到最優的解。然後,我們會設置一個 dropout 參數,以此來避免一些過擬合。

最後,我們將 LSTM cell 和三維的數據輸入到 tf.nn.dynamic_rnn ,這個函數的功能是展開整個網絡,並且構建一整個 RNN 模型。

lstmCell = tf.contrib.rnn.BasicLSTMCell(lstmUnits)
lstmCell = tf.contrib.rnn.DropoutWrapper(cell=lstmCell, output_keep_prob=0.75)
value, _ = tf.nn.dynamic_rnn(lstmCell, data, dtype=tf.float32)

堆棧 LSTM 網絡是一個比較好的網絡架構。也就是前一個LSTM 隱藏層的輸出是下一個LSTM的輸入。堆棧LSTM可以幫助模型記住更多的上下文信息,但是帶來的弊端是訓練參數會增加很多,模型的訓練時間會很長,過擬合的機率也會增加。

dynamic RNN 函數的第一個輸出可以被認爲是最後的隱藏狀態向量。這個向量將被重新確定維度,然後乘以最後的權重矩陣和一個偏置項來獲得最終的輸出值。

weight = tf.Variable(tf.truncated_normal([lstmUnits, numClasses]))
bias = tf.Variable(tf.constant(0.1, shape=[numClasses]))
value = tf.transpose(value, [1, 0, 2])
#取最終的結果值
last = tf.gather(value, int(value.get_shape()[0]) - 1)
prediction = (tf.matmul(last, weight) + bias)

接下來,我們需要定義正確的預測函數和正確率評估參數。正確的預測形式是查看最後輸出的0-1向量是否和標記的0-1向量相同。使用一個標準的交叉熵損失函數來作爲損失值。對於優化器,我們選擇 Adam,並且採用默認的學習率。

correctPred = tf.equal(tf.argmax(prediction,1), tf.argmax(labels,1))
accuracy = tf.reduce_mean(tf.cast(correctPred, tf.float32))
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=labels))
optimizer = tf.train.AdamOptimizer().minimize(loss)

訓練

訓練過程的基本思路是,我們首先先定義一個 TensorFlow 會話。然後,我們加載一批評論和對應的標籤。接下來,我們調用會話的 run 函數。這個函數有兩個參數,第一個參數被稱爲 fetches 參數,這個參數定義了我們感興趣的值。我們希望通過我們的優化器來最小化損失函數。第二個參數被稱爲 feed_dict 參數。這個數據結構就是我們提供給我們的佔位符。我們需要將一個批處理的評論和標籤輸入模型,然後不斷對這一組訓練數據進行循環訓練。

sess = tf.InteractiveSession()
saver = tf.train.Saver()
sess.run(tf.global_variables_initializer())

for i in range(iterations):
    #Next Batch of reviews
    nextBatch, nextBatchLabels = getTrainBatch();
    sess.run(optimizer, {input_data: nextBatch, labels: nextBatchLabels}) 
    
    if (i % 1000 == 0 and i != 0):
        loss_ = sess.run(loss, {input_data: nextBatch, labels: nextBatchLabels})
        accuracy_ = sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})
        
        print("iteration {}/{}...".format(i+1, iterations),
              "loss {}...".format(loss_),
              "accuracy {}...".format(accuracy_))    
    #Save the network every 10,000 training iterations
    if (i % 10000 == 0 and i != 0):
        save_path = saver.save(sess, "models/pretrained_lstm.ckpt", global_step=i)
        print("saved to %s" % save_path)

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
查看上面的訓練曲線,我們發現這個模型的訓練結果還是不錯的。損失值在穩定的下降,正確率也不斷的在接近 100% 。然而,當分析訓練曲線的時候,我們應該注意到我們的模型可能在訓練集上面已經過擬合了。過擬合是機器學習中一個非常常見的問題,表示模型在訓練集上面擬合的太好了,但是在測試集上面的泛化能力就會差很多。也就是說,如果你在訓練集上面取得了損失值是 0 的模型,但是這個結果也不一定是最好的結果。當我們訓練 LSTM 的時候,提前終止是一種常見的防止過擬合的方法。基本思路是,我們在訓練集上面進行模型訓練,同事不斷的在測試集上面測量它的性能。一旦測試誤差停止下降了,或者誤差開始增大了,那麼我們就需要停止訓練了。因爲這個跡象表明,我們網絡的性能開始退化了。

導入一個預訓練的模型需要使用 TensorFlow 的另一個會話函數,稱爲 Server ,然後利用這個會話函數來調用 restore 函數。這個函數包括兩個參數,一個表示當前的會話,另一個表示保存的模型。

sess = tf.InteractiveSession()
saver = tf.train.Saver()
saver.restore(sess, tf.train.latest_checkpoint('models'))

測試

從我們的測試集中導入一些電影評論。請注意,這些評論是模型從來沒有看見過的。

iterations = 10
for i in range(iterations):
    nextBatch, nextBatchLabels = getTestBatch();
    print("Accuracy for this batch:", (sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})) * 100)

在這裏插入圖片描述

注意:!!!

超參數調整

選擇合適的超參數來訓練你的神經網絡是至關重要的。你會發現你的訓練損失值與你選擇的優化器(Adam,Adadelta,SGD,等等),學習率和網絡架構都有很大的關係。特別是在RNN和LSTM中,單元數量和詞向量的大小都是重要因素。

  • 學習率:RNN最難的一點就是它的訓練非常困難,因爲時間步驟很長。那麼,學習率就變得非常重要了。如果我們將學習率設置的很大,那麼學習曲線就會波動性很大,如果我們將學習率設置的很小,那麼訓練過程就會非常緩慢。根據經驗,將學習率默認設置爲0.001 是一個比較好的開始。如果訓練的非常緩慢,那麼你可以適當的增大這個值,如果訓練過程非常的不穩定,那麼你可以適當的減小這個值。
  • 優化器:這個在研究中沒有一個一致的選擇,但是 Adam 優化器被廣泛的使用。
  • LSTM單元的數量:這個值很大程度上取決於輸入文本的平均長度。而更多的單元數量可以幫助模型存儲更多的文本信息,當然模型的訓練時間就會增加很多,並且計算成本會非常昂貴。
  • 詞向量維度:詞向量的維度一般我們設置爲50到300。維度越多意味着可以存儲更多的單詞信息,但是你需要付出的是更昂貴的計算成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章