1. 程式人生 > >文字情感分類---搭建LSTM(深度學習模型)做文字情感分類的程式碼

文字情感分類---搭建LSTM(深度學習模型)做文字情感分類的程式碼

來源:http://mp.weixin.qq.com/s?__biz=MzA3MDg0MjgxNQ==&mid=2652391534&idx=1&sn=901d5e55971349697e023f196037675d&chksm=84da48beb3adc1a886e2a0d9d45ced1e8d89d4add88a9b6595f21784fcc461938b19a7385684&mpshare=1&scene=23&srcid=0904TM0ogmvDF8vKGCMlVb7m#rd

傳統的文字情感分類思路簡單易懂,而且穩定性也比較強,然而存在著兩個難以克服的侷限性:

一、精度問題,傳統思路差強人意,當然一般的應用已經足夠了,但是要進一步提高精度,卻缺乏比較好的方法;

二、背景知識問題,傳統思路需要事先提取好情感詞典,而這一步驟,往往需要人工操作才能保證準確率,換句話說,做這個事情的人,不僅僅要是資料探勘專家,還需要語言學家,這個背景知識依賴性問題會阻礙著自然語言處理的進步。

慶幸的是,深度學習解決了這個問題(至少很大程度上解決了),它允許我們在幾乎“零背景”的前提下,為某個領域的實際問題建立模型。本文延續上一篇文章所談及的文字情感分類為例,簡單講解深度學習模型。其中上一篇文章已經詳細討論過的部分,本文不再詳細展開。

深度學習與自然語言處理

近年來,深度學習演算法被應用到了自然語言處理領域,獲得了比傳統模型更優秀的成果。如Bengio等學者基於深度學習的思想構建了神經概率語言模型,並進一步利用各種深層神經網路在大規模英文語料上進行語言模型的訓練,得到了較好的語義表徵,完成了句法分析和情感分類等常見的自然語言處理任務,為大資料時代的自然語言處理提供了新的思路。

經過筆者的測試,基於深度神經網路的情感分析模型,其準確率往往有95%以上,深度學習演算法的魅力和威力可見一斑!

關於深度學習進一步的資料,請參考以下文獻:

[1] Yoshua Bengio, Réjean Ducharme Pascal Vincent, Christian Jauvin. A Neural Probabilistic Language Model, 2003
[2] 一種新的語言模型:http://blog.sciencenet.cn/blog-795431-647334.html
[3] Deep Learning(深度學習)學習筆記整理:http://blog.csdn.net/zouxy09/article/details/8775360
[4] Deep Learning:http://deeplearning.net
[5] 漫話中文自動分詞和語義識別:http://www.matrix67.com/blog/archives/4212
[6] Deep Learning 在中文分詞和詞性標註任務中的應用:http://blog.csdn.net/itplus/article/details/13616045

語言的表達

建模環節中最重要的一步是特徵提取,在自然語言處理中也不例外。在自然語言處理中,最核心的一個問題是,如何把一個句子用數字的形式有效地表達出來?如果能夠完成這一步,句子的分類就不成問題了。顯然,一個最初等的思路是:給每個詞語賦予唯一的編號1,2,3,4...,然後把句子看成是編號的集合,比如假設1,2,3,4分別代表“我”、“你”、“愛”、“恨”,那麼“我愛你”就是[1, 3, 2],“我恨你”就是[1, 4, 2]。這種思路看起來有效,實際上非常有問題,比如一個穩定的模型會認為3跟4是很接近的,因此[1, 3, 2]和[1, 4, 2]應當給出接近的分類結果,但是按照我們的編號,3跟4所代表的詞語意思完全相反,分類結果不可能相同。因此,這種編碼方式不可能給出好的結果。

讀者也許會想到,我將意思相近的詞語的編號湊在一堆(給予相近的編號)不就行了?嗯,確實如果,如果有辦法把相近的詞語編號放在一起,那麼確實會大大提高模型的準確率。可是問題來了,如果給出每個詞語唯一的編號,並且將相近的詞語編號設為相近,實際上是假設了語義的單一性,也就是說,語義僅僅是一維的。然而事實並非如此,語義應該是多維的。

比如我們談到“家園”,有的人會想到近義詞“家庭”,從“家庭”又會想到“親人”,這些都是有相近意思的詞語;另外,從“家園”,有的人會想到“地球”,從“地球”又會想到“火星”。換句話說,“親人”、“火星”都可以看作是“家園”的二級近似,但是“親人”跟“火星”本身就沒有什麼明顯的聯絡了。此外,從語義上來講,“大學”、“舒適”也可以看做是“家園”的二級近似,顯然,如果僅通過一個唯一的編號,是很難把這些詞語放到適合的位置的。

Word2Vec:高維來了

從上面的討論可以知道,很多詞語的意思是各個方向發散開的,而不是單純的一個方向,因此唯一的編號不是特別理想。那麼,多個編號如何?換句話說,將詞語對應一個多維向量?不錯,這正是非常正確的思路。

為什麼多維向量可行?首先,多維向量解決了詞語的多方向發散問題,僅僅是二維向量就可以360度全方位旋轉了,何況是更高維呢(實際應用中一般是幾百維)。其次,還有一個比較實際的問題,就是多維向量允許我們用變化較小的數字來表徵詞語。怎麼說?我們知道,就中文而言,詞語的數量就多達數十萬,如果給每個詞語唯一的編號,那麼編號就是從1到幾十萬變化,變化幅度如此之大,模型的穩定性是很難保證的。如果是高維向量,比如說20維,那麼僅需要0和1就可以表達(100萬)個詞語了。變化較小則能夠保證模型的穩定性。

扯了這麼多,還沒有真正談到點子上。現在思路是有了,問題是,如何把這些詞語放到正確的高維向量中?而且重點是,要在沒有語言背景的情況下做到這件事情?(換句話說,如果我想處理英語語言任務,並不需要先學好英語,而是隻需要大量收集英語文章,這該多麼方便呀!)在這裡我們不可能也不必要進行更多的原理上的展開,而是要介紹:而基於這個思路,有一個Google開源的著名的工具——Word2Vec。

簡單來說,Word2Vec就是完成了上面所說的我們想要做的事情——用高維向量(詞向量,Word Embedding)表示詞語,並把相近意思的詞語放在相近的位置,而且用的是實數向量(不侷限於整數)。我們只需要有大量的某語言的語料,就可以用它來訓練模型,獲得詞向量。詞向量好處前面已經提到過一些,或者說,它就是問了解決前面所提到的問題而產生的。另外的一些好處是:詞向量可以方便做聚類,用歐氏距離或餘弦相似度都可以找出兩個具有相近意思的詞語。這就相當於解決了“一義多詞”的問題(遺憾的是,似乎沒什麼好思路可以解決一詞多義的問題。)

關於Word2Vec的數學原理,讀者可以參考這系列文章。

http://blog.csdn.net/itplus/article/details/37969519

而Word2Vec的實現,Google官方提供了C語言的原始碼,讀者可以自行編譯。而Python的Gensim庫中也提供現成的Word2Vec作為子庫(事實上,這個版本貌似比官方的版本更加強大)。

表達句子:句向量

接下來要解決的問題是:我們已經分好詞,並且已經將詞語轉換為高維向量,那麼句子就對應著詞向量的集合,也就是矩陣,類似於影象處理,影象數字化後也對應一個畫素矩陣;可是模型的輸入一般只接受一維的特徵,那怎麼辦呢?一個比較簡單的想法是將矩陣展平,也就是將詞向量一個接一個,組成一個更長的向量。這個思路是可以,但是這樣就會使得我們的輸入維度高達幾千維甚至幾萬維,事實上是難以實現的。(如果說幾萬維對於今天的計算機來說不是問題的話,那麼對於1000x1000的影象,就是高達100萬維了!)

事實上,對於影象處理來說,已經有一套成熟的方法了,叫做卷積神經網路(CNNs),它是神經網路的一種,專門用來處理矩陣輸入的任務,能夠將矩陣形式的輸入編碼為較低維度的一維向量,而保留大多數有用資訊。卷積神經網路那一套也可以直接搬到自然語言處理中,尤其是文字情感分類中,效果也不錯,相關的文章有《Deep Convolutional Neural Networks for Sentiment Analysis of Short Texts》。但是句子的原理不同於影象,直接將影象那一套用於語言,雖然略有小成,但總讓人感覺不倫不類。因此,這並非自然語言處理中的主流方法。

在自然語言處理中,通常用到的方法是遞迴神經網路迴圈神經網路(都叫RNNs)。它們的作用跟卷積神經網路是一樣的,將矩陣形式的輸入編碼為較低維度的一維向量,而保留大多數有用資訊。跟卷積神經網路的區別在於,卷積神經網路更注重全域性的模糊感知(好比我們看一幅照片,事實上並沒有看清楚某個畫素,而只是整體地把握圖片內容),而RNNs則是注重鄰近位置的重構,由此可見,對於語言任務,RNNs更具有說服力(語言總是由相鄰的字構成詞,相鄰的詞構成短語,相鄰的短語構成句子,等等,因此,需要有效地把鄰近位置的資訊進行有效的整合,或者叫重構)。

說到模型的分類,可真謂無窮無盡。在RNNs這個子集之下,又有很多個變種,如普通的RNNs,以及GRU、LSTM等,讀者可以參考Keras的官方文件:http://keras.io/models/,它是Python是一個深度學習庫,提供了大量的深度學習模型,它的官方文件既是一個幫助教程,也是一個模型的列表——它基本實現了目前流行的深度學習模型。

搭建LSTM模型

吹了那麼久水,是該乾點實事了。現在我們基於LSTM(Long-Short Term Memory,長短期記憶人工神經網路)搭建一個文字情感分類的深度學習模型,其結構圖如下:



模型結構很簡單,沒什麼複雜的,實現也很容易,用的就是Keras,它都為我們實現好了現成的演算法了。

現在我們來談談有意思的兩步。

第一步是標註語料的收集。要注意我們的模型是監督訓練的(至少也是半監督),所以需要收集一些已經分好類的句子,數量嘛,當然越多越好。而對於中文文字情感分類來說,這一步著實不容易,中文的資料往往是相當匱乏的。筆者在做模型的時候,東拼西湊,通過各種渠道(有在網上搜索下載的、有在資料堂花錢購買的)收集了兩萬多條中文標註語料(涉及六個領域)用來訓練模型。(文末有共享)

第二步是模型閾值選取問題。事實上,訓練的預測結果是一個[0, 1]區間的連續的實數,而程式預設情況下會將0.5設為閾值,也就是將大於0.5的結果判斷為正,將小於0.5的結果判斷為負。這樣的預設值在很多情況下並不是最好的。如下圖所示,我們在研究不同的閾值對真正率和真負率的影響之時,發現在(0.391, 0.394)區間內曲線曲線了陡變。

雖然從絕對值看,只是從0.99下降到了0.97,變化不大,但是其變化率是非常大的。正常來說都是平穩變化的,陡變意味著肯定出現了什麼異常情況,而顯然這個異常的原因我們很難發現。換句話說,這裡存在一個不穩定的區域,這個區域內的預測結果事實上是不可信的,因此,保險起見,我們扔掉這個區間。只有結果大於0.394的,我們才認為是正,小於0.391的,我們才認為是負,是0.391到0.394之間的,我們待定。實驗表明這個做法有助於提高模型的應用準確率。

說點總結

文章很長,粗略地介紹了深度學習在文字情感分類中的思路和實際應用,很多東西都是泛泛而談。筆者並非要寫關於深度學習的教程,而是隻想把關鍵的地方指出來,至少是那些我認為是比較關鍵的地方。關於深度學習,有很多不錯的教程,最好還是閱讀英文的論文,中文的比較好的就是部落格http://blog.csdn.net/itplus了,筆者就不在這方面獻醜了。

下面是我的語料和程式碼。讀者可能會好奇我為什麼會把這些“私人珍藏”共享呢?其實很簡單,因為我不是幹這行的哈,資料探勘對我來說只是一個愛好,一個數學與Python結合的愛好,因此在這方面,我不用擔心別人比我領先哈。

語料下載:sentiment.zip

http://kexue.fm/usr/uploads/2015/08/646864264.zip

採集到的評論資料:sum.zip

http://kexue.fm/usr/uploads/2015/09/829078856.zip

搭建LSTM做文字情感分類的程式碼:

import pandas as pd #匯入Pandas
import numpy as np #匯入Numpy
import jieba #匯入結巴分詞
from keras.preprocessing import sequence
from keras.optimizers import SGD, RMSprop, Adagrad
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import LSTM, GRU

from __future__ import absolute_import #匯入3.x的特徵函式
from __future__ import print_function

neg=pd.read_excel('neg.xls',header=None,index=None)
pos=pd.read_excel('pos.xls',header=None,index=None) #讀取訓練語料完畢
pos['mark']=1
neg['mark']=0 #給訓練語料貼上標籤
pn=pd.concat([pos,neg],ignore_index=True) #合併語料
neglen=len(neg)
poslen=len(pos) #計算語料數目
cw = lambda x: list(jieba.cut(x)) #定義分詞函式
pn['words'] = pn[0].apply(cw)

comment = pd.read_excel('sum.xls') #讀入評論內容
#comment = pd.read_csv('a.csv', encoding='utf-8')
comment = comment[comment['rateContent'].notnull()] #僅讀取非空評論
comment['words'] = comment['rateContent'].apply(cw) #評論分詞
d2v_train = pd.concat([pn['words'], comment['words']], ignore_index = True)

w = [] #將所有詞語整合在一起
for i in d2v_train:
w.extend(i)

dict = pd.DataFrame(pd.Series(w).value_counts()) #統計詞的出現次數
del w,d2v_train
dict['id']=list(range(1,len(dict)+1))

get_sent = lambda x: list(dict['id'][x])
pn['sent'] = pn['words'].apply(get_sent) #速度太慢
maxlen = 50
print("Pad sequences (samples x time)")
pn['sent'] = list(sequence.pad_sequences(pn['sent'], maxlen=maxlen))

x = np.array(list(pn['sent']))[::2] #訓練集
y = np.array(list(pn['mark']))[::2]
xt = np.array(list(pn['sent']))[1::2] #測試集
yt = np.array(list(pn['mark']))[1::2]
xa = np.array(list(pn['sent'])) #全集
ya = np.array(list(pn['mark']))

print('Build model...')
model = Sequential()
model.add(Embedding(len(dict)+1, 256))
model.add(LSTM(256, 128)) # try using a GRU instead, for fun
model.add(Dropout(0.5))
model.add(Dense(128, 1))
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy', optimizer='adam', class_mode="binary")

model.fit(x, y, batch_size=16, nb_epoch=10) #訓練時間為若干個小時
classes = model.predict_classes(xt)
acc = np_utils.accuracy(classes, yt)
print('Test accuracy:', acc)