基于LSTM的情绪分析
1. 摘要
自然語言處理是當代機器學習一塊很重要的分支,而情緒分析也是NLP中的重要研究部分。本文為基于簡單的“情緒數據集”,通過詞向量模型,LSTM等方法訓練神經網絡模型,對句子進行情緒上的分類與識別。最終識別準確率可達到90.95%。
關鍵詞:NLP, 文本情感分析,情緒分析,詞向量模型,LSTM,神經網絡,深度學習。
2. 引言
自然語言處理(NLP)是一種專業分析人類語言的人工智能。它接收自然語言,
轉譯自然語言,最后分析自然語言并輸出結果。文本情感分析(Sentiment Analysis)是指利用自然語言處理和文本挖掘技術,對帶有情感色彩的主觀性文本進行分析、處理和抽取的過程。
深度學習技術發展到今天,在自然語言處理領域有很多的應用。而由于人類語言的多樣性、多意性,使得NLP的難度成倍增加。例如由相同的三個字形成的組合“不怕辣”、“辣不怕”、“怕不辣”、“怕辣不”表達了不同的含義。因此,對于自然語言的處理仍具有挑戰性與發展空間。本文旨在分析自然語言語句(英語為例),來對其表達的情緒進行歸類于分析。
3. 主要研究技術與方法
3.1 詞向量模型
人類想要讀懂一句話往往非常容易,然而對于機器來講卻是一個需要不斷學習的漫長而復雜的過程。想要讓機器讀懂一句話,我們必須將自然語言轉化成計算機可識別的數字。文本向量化表示就是用數值向量來表示文本的語義。我們人類在讀一段文本后立刻就能明白它要表達的內容,如何讓機器也能擁有這樣的能力呢?
文本分類領域使用了信息檢索領域的詞袋模型,詞袋模型在部分保留文本語義的前提下對文本進行向量化表示。通過詞袋模型(bag of words)對句子進行分詞并向量化,形成D維向量,再輸入網絡中計算:
- One-Hot:
One-Hot表示法的數值計算規則為:詞語序列中出現的詞語其數值為1,詞語序列中未出現的詞語其數值為0。對應關系如下圖所示:
- Word2Vec:
這個模型根據上下文的語境來推斷出每個詞的詞向量。如果兩個詞在上下文的語境中,可以被互相替換,那么這兩個詞的距離就非常近。在自然語言中,上下文的語境對分析詞語的意義是非常重要的。所以,這個模型的作用就是從一大堆句子(以Wikipedia為例)中為每個獨一無二的單詞進行建模,并且輸出一個唯一的向量。Word2Vec模型的輸出被稱為一個嵌入矩陣。
- Tokenizer:
Tokenizer 是keras中一個用于向量化文本,或將文本轉換為序列的類。計算機在處理語言文字時,是無法理解文字的含義,通常會把一個詞(中文單個字或者詞組認為是一個詞)轉化為一個正整數,于是一個文本就變成了一個序列。Tokenizer 的核心任務就是做這個事情。本文將單詞向量化用的方法就是此方法。
3.2 循環神經網絡RNN(Recurrent Neural Networks)
在得到了神經網絡的輸入數據——詞向量后,我們需要確定將要構建的神經網絡。NLP數據的一個獨特之處是它是時間序列數據。每個單詞的出現都依賴于它的前一個單詞和后一個單詞。由于這種依賴的存在,我們使用循環神經網絡來處理這種時間序列數據。
循環神經網絡的結構和你之前看到的那些前饋神經網絡的結構可能有一些不一樣。前饋神經網絡由三部分組成,輸入層、隱藏層和輸出層。
前饋神經網絡和RNN之前的主要區別就是RNN考慮了時間的信息。在RNN中,句子中的每個單詞都被考慮上了時間步驟。實際上,時間步長的數量將等于最大序列長度。
與每個時間步驟相關聯的中間狀態也被作為一個新的組件,稱為隱藏狀態向量h(t)。從抽象的角度來看,這個向量是用來封裝和匯總前面時間步驟中所看到的所有信息。就像x(t)表示一個向量,它封裝了一個特定單詞的所有信息。
隱藏狀態是當前單詞向量和前一步的隱藏狀態向量的函數。并且這兩項之和需要通過激活函數來進行激活。
然而,RNN存在一個致命的缺點——即梯度消失的問題,很難處理長序列的數據。為了解決此問題,我們又引入了本文所用到的方法:LSTM。
3.3 長時間的短期記憶模型LSTM(Long Short-Term Memory)
為了解決RNN存在問題,后續人們對RNN做了改進,得到了RNN的特例LSTM,它可以避免常規RNN的梯度消失,因此在工業界得到了廣泛的應用。LSTM模型是RNN的變體,它能夠學習長期依賴,允許信息長期存在。
舉個例子來講:比如人們讀文章的時候,人們會根據已經閱讀過的內容來對后面的內容進行理解,不會把之前的東西都丟掉從頭進行思考,對內容的理解是貫穿的。
傳統的神經網絡即RNN做不到這一點,LSTM是具有循環的網絡,解決了信息無法長期存在的問題,在工業界普遍使用有良好的效果。
-
RNN與LSTM之間的聯系:
RNN具有如下的結構,每個序列索引位置t都有一個隱藏狀態h(t):
如果略去每層都有的o(t),L(t),y(t),則RNN的模型可以簡化成如下圖的形式:
可以看出h(t)由x(t)和h(t?1)得到。得到h(t)后一方面用于當前層的模型損失計算,另一方面用于計算下一層的h(t+1)。
為了避免RNN的梯度消失,LSTM將tanh激活函數轉為更為復雜的結構。LSTM的結構如下圖:
粉紅色圓圈表示點向運算,如向量加法、點乘,而黃色框是學習神經網絡層。 線的合并表示連接,而線的交叉表示其內容正在復制,副本將轉到不同的位置。
-
LSTM的工作原理:
對于一個典型的RNN網絡,隱藏狀態向量對于第二句的存儲信息量可能比第一句的信息量會大很多。但是LSTM,基本上就會判斷哪些信息是有用的,哪些是沒用的,并且把有用的信息在LSTM中進行保存。LSTM的單元根據輸入數據x(t),隱藏層輸出h(t)。在這些單元中,h(t)的表達形式比經典的RNN網絡會復雜很多。這些復雜組件分為四個部分:輸入門、輸出門、遺忘門和一個記憶控制器。
每個門都將x(t)和h(t-1)作為輸入,并且利用這些輸入來計算一些中間狀態。每個中間狀態都會被送入不同的管道,并且這些信息最終會匯集到h(t)。這些門可以被認為是不同的模塊,各有不同的功能。Ct是控制參數,控制什么樣的值保留,什么樣的值舍棄,輸入門決定在每個輸入上施加多少強調,遺忘門決定我們將丟棄什么信息,輸出門根據中間狀態來決定最終的h(t)。
簡要來說,LSTM 單元能夠學習到識別重要輸入(輸入門作用),存儲進長時狀態,并保存必要的時間(遺忘門功能),并學會提取當前輸出所需要的記憶。這也解釋了 LSTM 單元能夠在提取長時序列,長文本,錄音等數據中的長期模式的驚人成功的原因。
4. 神經網絡的搭建與分析
4.1數據集描述及導入
本次的數據集來自Kaggel上專門為情感分析提供的數據集。地址 https://www.kaggle.com/praveengovi/emotions-dataset-for-nlp
該數據集包含了三個文檔
- 16000行的訓練數據集:train.txt
- 2000行的測試數據集:test.txt
- 2000行的驗證數據集:val.txt
通過這三個文檔我們將建立機器學習模型。以下是train.txt的部分數據:
Eg: i didnt fell humiliated;sadness,這條數據中包含了:1. 句子的具體內容: i didnt fell humiliated; 2. 事先人為分類號的情緒標簽: sadness。
導入并展示:
# 導入數據集 train = pd.read_csv('E:/大三下/機器學習/NLP情感分析/train.txt', sep=';', header=None) # 重命名Dataframe列名 train.columns=['Sentence', 'Sentiment'] # 統計數據集中情感種類以及出現次數 train['Sentiment'].value_counts() joy 5362 sadness 4666 anger 2159 fear 1937 love 1304 surprise 572 Name: Sentiment, dtype: int64 train| i didnt feel humiliated | sadness |
| i can go from feeling so hopeless to so damned... | sadness |
| im grabbing a minute to post i feel greedy wrong | anger |
| i am ever feeling nostalgic about the fireplac... | love |
| i am feeling grouchy | anger |
| ... | ... |
| i just had a very brief time in the beanbag an... | sadness |
| i am now turning and i feel pathetic that i am... | sadness |
| i feel strong and good overall | joy |
| i feel like this was such a rude comment and i... | anger |
| i know a lot but i feel so stupid because i ca... | sadness |
16000 rows × 2 columns
4.2 數據預處理:數據清洗及向量化
在拿到一份數據集時,首先要做的事就是對數據集進行預處理,以便得到可用于模型訓練的數據集。
在清洗完成過后,我們進行詞的向量化:
作為該領域的一個最大玩家,Google已經幫我們在大規模數據集上訓練出來了Word2Vec模型,包括1000億個不同的詞。在這個模型中,谷歌能創建300萬個詞向量,每個向量維度為300。在理想情況下,我們將使用這些向量來構建模型,但是因為這個單詞向量矩陣想當大(3.6G),跑起來十分緩慢,同時自己訓練Word2Vec的效果也不是最佳,因此本文采用更為輕量化的keras提供的Tokenizer分詞器。我定義最大特征的數量為2000,并使用Tokenizer向量化和將文本轉換為序列,以便網絡可以處理它作為輸入。
max_fatures = 2000 tokenizer = Tokenizer(num_words = max_fatures, split=' ') tokenizer.fit_on_texts(train['Sentence'].values) X = tokenizer.texts_to_sequences(train['Sentence'].values) X = pad_sequences(X)4.3 LSTM神經網絡的搭建
超參數調優: 選擇合適的超參數來訓練你的神經網絡是至關重要的。訓練損失值與你選擇的優化器、學習率和網絡架構都有很大的關系。特別是在RNN和LSTM中,單元數量和詞向量的大小都是重要因素。embed_dim、lstm_out、batch_size、droupout_x變量都是超參數,它們的值在某種程度上是直觀的,正確地調整使用它們才能獲得良好的結果。文中沒有設置的參數按照默認參數配置。
- max_fatures: 詞匯表大小。這里設置為2000;
- embed_dim: 詞向量的維度。這里設置為128;
- input_length: 輸入序列的長度,當它是固定的時。如果你需要連接Flatten和Dense層,則這個參數是必須的。這里設置為61;
- SpatialDropout1D: 一種dropout方法。隨機地將部分區域置零。這里設置為0.4;
- lstm_out: LSTM的輸出維度。這里設置為196;
- dropout: 使多少比重的神經元輸出(unit的輸出)激活失效,默認為0。這里設置為0.2;
- recurrent_dropout: recurrent_dropout是給遞歸狀態 C 設置的Dropout參數。這里設置為0.2;
- Dense: 全連接層,最后輸出為6。并且最后一層的激活函數選用 softmax;
- loss: 損失函數。這里選用交叉熵損失函數。交叉熵損失函數經常用于分類問題中,它能衡量同一個隨機變量中的兩個不同概率分布的差異程度,在機器學習中就表示為真實概率分布與預測概率分布之間的差異。交叉熵的值越小,模型預測效果就越好;
- optimizer: 優化器。這里選用Adam。它結合AdaGrad和RMSProp兩種優化算法的優點。對梯度的一階矩估計和二階矩估計進行綜合考慮,計算出更新步長;
- metrics: 定義評價函數。這里選用accuracy;
查看神經網絡情況:
print(model.summary()) _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 61, 128) 256000 _________________________________________________________________ spatial_dropout1d_1 (Spatial (None, 61, 128) 0 _________________________________________________________________ lstm_1 (LSTM) (None, 196) 254800 _________________________________________________________________ dense_1 (Dense) (None, 6) 1182 ================================================================= Total params: 511,982 Trainable params: 511,982 Non-trainable params: 0 _______________________________________________________________ None分離訓練集,測試集:
Y = pd.get_dummies(train['SentiID']).values X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.33, random_state = 42) print(X_train.shape,Y_train.shape) print(X_test.shape,Y_test.shape) (10720, 61) (10720, 6) (5280, 61) (5280, 6)訓練網絡:
batch_size = 32 history = model.fit(X_train, Y_train, epochs = 20, batch_size = batch_size, verbose = 1, validation_split = 0.2) print(model.evaluate(X_test, Y_test)) Instructions for updating: Use tf.cast instead. Train on 8576 samples, validate on 2144 samples Epoch 1/20 8576/8576 [==============================] - 24s 3ms/step - loss: 1.5044 - acc: 0.3981 - val_loss: 1.1840 - val_acc: 0.5648 Epoch 2/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.7453 - acc: 0.7556 - val_loss: 0.5421 - val_acc: 0.8321 Epoch 3/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.3271 - acc: 0.8935 - val_loss: 0.3255 - val_acc: 0.8839 Epoch 4/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.2076 - acc: 0.9254 - val_loss: 0.2561 - val_acc: 0.9072 Epoch 5/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.1603 - acc: 0.9405 - val_loss: 0.2710 - val_acc: 0.8969 Epoch 6/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.1316 - acc: 0.9492 - val_loss: 0.2504 - val_acc: 0.9128 Epoch 7/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.1128 - acc: 0.9574 - val_loss: 0.2466 - val_acc: 0.9104 Epoch 8/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.1048 - acc: 0.9595 - val_loss: 0.2413 - val_acc: 0.9165 Epoch 9/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0837 - acc: 0.9668 - val_loss: 0.2586 - val_acc: 0.9109 Epoch 10/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0819 - acc: 0.9684 - val_loss: 0.2777 - val_acc: 0.9095 Epoch 11/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0749 - acc: 0.9722 - val_loss: 0.2732 - val_acc: 0.9067 Epoch 12/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0700 - acc: 0.9734 - val_loss: 0.2782 - val_acc: 0.9137 Epoch 13/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0567 - acc: 0.9788 - val_loss: 0.2935 - val_acc: 0.9062 Epoch 14/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.0596 - acc: 0.9782 - val_loss: 0.2943 - val_acc: 0.9090 Epoch 15/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0548 - acc: 0.9788 - val_loss: 0.3360 - val_acc: 0.9086 Epoch 16/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0454 - acc: 0.9836 - val_loss: 0.3751 - val_acc: 0.9016 Epoch 17/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.0519 - acc: 0.9823 - val_loss: 0.3499 - val_acc: 0.9072 Epoch 18/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0381 - acc: 0.9869 - val_loss: 0.3477 - val_acc: 0.9114 Epoch 19/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0391 - acc: 0.9865 - val_loss: 0.3552 - val_acc: 0.9062 Epoch 20/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0454 - acc: 0.9848 - val_loss: 0.3569 - val_acc: 0.9058 5280/5280 [==============================] - 4s 695us/step [0.3139003471513702, 0.9094696969696969]由結果可看出,最終模型準確率達到90.95%。
最終畫出模型準確率以及損失隨迭代次數的變化圖像:
# 準確率的變化 plt.plot(history.history['acc'], color = 'orange') plt.title("Train_history") plt.ylabel('accuracy') plt.xlabel('Epoch') plt.show() # 訓練中的損失圖像 plt.plot(history.history['loss']) plt.title("Train_history") plt.ylabel('loss') plt.xlabel('Epoch') plt.show()5.模型測試
# "joy": 0, "sadness": 1, "anger": 2, "fear": 3, "love": 4, "surprise": 5 txt = ['The sentence you are going to predict'] txt = tokenizer.texts_to_sequences(txt) txt = pad_sequences(txt, maxlen=61, dtype='int32', value=0) sentiment = model.predict(txt, batch_size=1, verbose = 1)[0] if(np.argmax(sentiment) == 0):print("情緒為:joy") elif (np.argmax(sentiment) == 1):print("情緒為:sadness") elif (np.argmax(sentiment) == 2):print("情緒為:anger") elif (np.argmax(sentiment) == 3):print("情緒為:fear") elif (np.argmax(sentiment) == 4):print("情緒為:love") elif (np.argmax(sentiment) == 5):print("情緒為:surprise")i had been talking to coach claudia barcomb and coach ali boe for a long time and they both made me feel very welcomed at union,預分類為joy:
txt = ['i had been talking to coach claudia barcomb and coach ali boe for a long time and they both made me feel very welcomed at union'] 1/1 [==============================] - 0s 7ms/step情緒為:joy預測成功;
im feeling rather rotten so im not very ambitious right now,預分類為sadness:
txt = ['im feeling rather rotten so im not very ambitious right now'] 1/1 [==============================] - 0s 8ms/step情緒為:sadness預測成功;
i jest i feel grumpy tired and pre menstrual which i probably am but then again its only been a week and im about as fit as a walrus on vacation for the summer,預分類為anger:
txt = ['i jest i feel grumpy tired and pre menstrual which i probably am but then again its only been a week and im about as fit as a walrus on vacation for the summer'] 1/1 [==============================] - 0s 7ms/step情緒為:anger預測成功;
i cant walk into a shop anywhere where i do not feel uncomfortable,預分類為fear:
txt = ['i cant walk into a shop anywhere where i do not feel uncomfortable'] 1/1 [==============================] - 0s 8ms/step情緒為:fear預測成功;
i were to go overseas or cross the border then i become a foreigner and will feel that way but never in my beloved land,預分類為love:
txt = ['i were to go overseas or cross the border then i become a foreigner and will feel that way but never in my beloved land'] 1/1 [==============================] - 0s 7ms/step情緒為:love預測成功;
i am right handed however i play billiards left handed naturally so me trying to play right handed feels weird,預分類為surprise:
txt = ['i am right handed however i play billiards left handed naturally so me trying to play right handed feels weird'] 1/1 [==============================] - 0s 8ms/step情緒為:surprise預測成功。
總結
以上是生活随笔為你收集整理的基于LSTM的情绪分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Premiere Pro入门
- 下一篇: 阿里云SLB实现负载均衡