1. 程式人生 > >深度學習----基於keras的LSTM三分類的文字情感分析原理及程式碼

深度學習----基於keras的LSTM三分類的文字情感分析原理及程式碼

文章目錄


背景介紹

文字情感分析作為NLP的常見任務,具有很高的實際應用價值。本文將採用LSTM模型,訓練一個能夠識別文字postive, neutral, negative三種情感的分類器。

本文的目的是快速熟悉LSTM做情感分析任務,所以本文提到的只是一個baseline,並在最後分析了其優劣。對於真正的文字情感分析,在本文提到的模型之上,還可以做很多工作,以後有空的話,筆者可以再做優化。


理論介紹

RNN應用場景

         ~~~~~~~~ RNN相對於傳統的神經網路,它允許我們對向量序列進行操作:輸入序列、輸出序列、或大部分的輸入輸出序列。如下圖所示,每一個矩形是一個向量,箭頭則表示函式(比如矩陣相乘)。輸入向量用紅色標出,輸出向量用藍色標出,綠色的矩形是RNN的狀態(下面會詳細介紹)。從做到右:
        

~~~~~~~~ (1)沒有使用RNN的Vanilla模型,從固定大小的輸入得到固定大小輸出(比如影象分類)。
         ~~~~~~~~
(2)序列輸出(比如圖片字幕,輸入一張圖片輸出一段文字序列)。
         ~~~~~~~~ (3)序列輸入(比如情感分析,輸入一段文字然後將它分類成積極或者消極情感)。(4)序列輸入和序列輸出(比如機器翻譯:一個RNN讀取一條英文語句然後將它以法語形式輸出)。
         ~~~~~~~~ (5)同步序列輸入輸出(比如視訊分類,對視訊中每一幀打標籤)。我們注意到在每一個案例中,都沒有對序列長度進行預先特定約束,因為遞迴變換(綠色部分)是固定的,而且我們可以多次使用。


word2vec 演算法

         ~~~~~~~~ 建模環節中最重要的一步是特徵提取,在自然語言處理中也不例外。在自然語言處理中,最核心的一個問題是,如何把一個句子用數字的形式有效地表達出來?如果能夠完成這一步,句子的分類就不成問題了。顯然,一個最初等的思路是:給每個詞語賦予唯一的編號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就可以表達2^20=1048576220=1048576(100萬)個詞語了。變化較小則能夠保證模型的穩定性。
         ~~~~~~~~ >簡單來說,Word2Vec就是完成了上面所說的我們想要做的事情——用高維向量(詞向量,Word Embedding)表示詞語,並把相近意思的詞語放在相近的位置,而且用的是實數向量(不侷限於整數)。我們只需要有大量的某語言的語料,就可以用它來訓練模型,獲得詞向量。詞向量好處前面已經提到過一些,或者說,它就是問了解決前面所提到的問題而產生的。另外的一些好處是:詞向量可以方便做聚類,用歐氏距離或餘弦相似度都可以找出兩個具有相近意思的詞語。這就相當於解決了“一義多詞”的問題(遺憾的是,似乎沒什麼好思路可以解決一詞多義的問題。)

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

句向量

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

在自然語言處理中,通常用到的方法是遞迴神經網路或迴圈神經網路(都叫RNNs)。它們的作用跟卷積神經網路是一樣的,將矩陣形式的輸入編碼為較低維度的一維向量,而保留大多數有用資訊

資料預處理與詞向量模型訓練

參考資料二中有很翔實的處理過程,包括:

  1. 不同類別資料整理成輸入矩陣
  2. jieba分詞
  3. Word2Vec詞向量模型訓練

本文中就不做重複介紹了,想要了解的,可以去參考資料二的博文中查詢。

三分類除了涉及到positive和negative兩種情感外,還有一種neural情感,從原始資料集中可以提取到有語義轉折的句子,“然而”,“但”都是關鍵詞。從而可以得到3份不同語義的資料集.

LSTM三分類模型程式碼

def get_data(index_dict,word_vectors,combined,y):
    n_symbols = len(index_dict) + 1  # 所有單詞的索引數,頻數小於10的詞語索引為0,所以加1
    embedding_weights = np.zeros((n_symbols, vocab_dim)) # 初始化 索引為0的詞語,詞向量全為0
    for word, index in index_dict.items(): # 從索引為1的詞語開始,對每個詞語對應其詞向量
        embedding_weights[index, :] = word_vectors[word]
    x_train, x_test, y_train, y_test = train_test_split(combined, y, test_size=0.2)
    y_train = keras.utils.to_categorical(y_train,num_classes=3) 
    y_test = keras.utils.to_categorical(y_test,num_classes=3)
    # print x_train.shape,y_train.shape
    return n_symbols,embedding_weights,x_train,y_train,x_test,y_test


##定義網路結構
def train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,y_test):
    print 'Defining a Simple Keras Model...'
    model = Sequential()  # or Graph or whatever
    model.add(Embedding(output_dim=vocab_dim,
                        input_dim=n_symbols,
                        mask_zero=True,
                        weights=[embedding_weights],
                        input_length=input_length))  # Adding Input Length
    model.add(LSTM(output_dim=50, activation='tanh'))
    model.add(Dropout(0.5))
    model.add(Dense(3, activation='softmax')) # Dense=>全連線層,輸出維度=3
    model.add(Activation('softmax'))

    print('Compiling the Model...')
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',metrics=['accuracy'])

    print "Train..." # batch_size=32
    model.fit(x_train, y_train, batch_size=batch_size, epochs=n_epoch,verbose=1)

    print("Evaluate...")
    score = model.evaluate(x_test, y_test,
                                batch_size=batch_size)

    yaml_string = model.to_yaml()
    with open('../model/lstm.yml', 'w') as outfile:
        outfile.write( yaml.dump(yaml_string, default_flow_style=True) )
    model.save_weights('../model/lstm.h5')
    print('Test score:', score)

測試程式碼如下:

def lstm_predict(string):
    print 'loading model......'
    with open('../model/lstm.yml', 'r') as f:
        yaml_string = yaml.load(f)
    model = model_from_yaml(yaml_string)

    print 'loading weights......'
    model.load_weights('../model/lstm.h5')
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',metrics=['accuracy'])
    data=input_transform(string)
    data.reshape(1,-1)
    #print data
    result=model.predict_classes(data)
    # print result # [[1]]
    if result[0]==1:
        print string,' positive'
    elif result[0]==0:
        print string,' neutral'
    else:
        print string,' negative'

參考:https://www.ctolib.com/Edward1Chou-SentimentAnalysis.html