Author: Beyond
Time: 2019.04.26
情感分析是上手NLP的最簡單的任務之一,它就是一個簡單的文本分類問題,判斷一段文本的情感極性。最簡單的就是二分類,判斷是積極的還是消極的;更難一點的就是三分類,除了積極消極還有無情感傾向的;更加複雜的就比如情感打分,例如電影打1~5分,這就是五分類。但本質上都一樣,無非類別太多更難以學習罷了。 IMDB是一個專業的電影評論網站,類似國內的豆瓣,IMDB的電影評論數據是大家經常使用來練手的情感分析數據集,也是各種比賽,如Kaggle,和各種學者做研究常用的數據集。
情感分析是上手NLP的最簡單的任務之一,它就是一個簡單的文本分類問題,判斷一段文本的情感極性。最簡單的就是二分類,判斷是積極的還是消極的;更難一點的就是三分類,除了積極消極還有無情感傾向的;更加複雜的就比如情感打分,例如電影打1~5分,這就是五分類。但本質上都一樣,無非類別太多更難以學習罷了。
數據集地址:http://ai.stanford.edu/~amaas/data/sentiment/
本文採用Keras作為框架在進行模型搭建。
本文目錄:
其實,keras自帶了IMDB的已經進行很好的預處理的數據集,可以一行代碼下載,不需要進行任何的處理就可以訓練,而且效果比較好。但是,這樣就太沒意思了。在真實場景中,我們拿到的都是臟髒的數據,我們必須自己學會讀取、清洗、篩選、分成訓練集測試集。而且,從我自己的實踐經驗來看,數據預處理的本事才是真本事,模型都好搭,現在的各種框架已經讓搭建模型越來越容易,但是數據預處理只能自己動手。所有往往實際任務中,數據預處理花費的時間、精力是最多的,而且直接影響後面的效果。
另外,我們要知道,對文本進行分析,首先要將文本數值化。因為計算機不認字的,只認數字。所以最後處理好的文本應該是數值化的形式。而keras自帶的數據集全都數值化了,而它並不提供對應的查詢字典讓我們知道每個數字對應什麼文字,這讓我們只能訓練模型,看效果,無法拓展到其他語料上,也無法深入分析。綜上,我上面推薦的數據集,是原始數據集,都是真實文本,當然,為了方便處理,也已經被斯坦福的大佬分好類了。但是怎麼數值化,需要我們自己動手。
下載後解壓,會看到有兩個文件夾,test和train:
test
train
我們點進train中,會發現正樣本和負樣本已經分好類了:
neg和pos分別是負樣本和正樣本,unsup是未標註的樣本,可用後續需要採用。其他的都自己去看看吧。
neg
pos
unsup
打開pos文件,看看裡面啥樣:
都是一個個文本。
注意到,這些文本一般都不短...
數據集中,共有5w條文本,test集和train集各半,每個集合中,pos和neg也是各半。
當然,他們劃分的train和test,你不一定真的要這樣用。例如本文中,我為了方便,就吧train集合當做我所有的數據,在這2.5w條數據中再按照7:3劃分train set和test set.
import os datapath = rdatasetsaclImdb_v1 rain pos_files = os.listdir(datapath+/pos) neg_files = os.listdir(datapath+/neg) print(len(pos_files)) print(len(neg_files))
輸出:
12500 12500
所以我們總共有12500個正樣本和12500個負樣本。
import numpy as np pos_all = [] neg_all = [] for pf,nf in zip(pos_files,neg_files): with open(datapath+/pos+/+pf,encoding=utf-8) as f: s = f.read() pos_all.append(s) with open(datapath+/neg+/+nf,encoding=utf-8) as f: s = f.read() neg_all.append(s) print(len(pos_all)) print(len(neg_all)) X_orig = np.array(pos_all+neg_all) Y_orig = np.array([1 for _ in range(12500)] + [0 for _ in range(12500)]) print("X_orig:",X_orig.shape) print("Y_orig:",Y_orig.shape)
上面代碼的主要作用是把一個個樣本放進正負樣本對應的列表中,同時配上對應的label。代碼很好理解。
12500 12500 X_orig: (25000,) Y_orig: (25000,)
前面提到過,NLP問題比CV問題更難的一部分原因,就是文本都是離散化的數據,不像圖像數據都是連續的數值數據,所以我們要想辦法把一系列文本轉化成一系列數字。
這裡的方法很多,我們這裡採用的方法是,給辭彙表中每一個詞一個index,用index代替那個詞。如一個語料庫共有1w個詞,那麼就設置1w個index,每個詞直接替換程index就行。
但是,很多問題中,辭彙量巨大,但是可能大部分詞都是低頻詞,對訓練模型的貢獻很小,反而會嚴重拖累模型的訓練。所以,一般我們可以分析一下文本辭彙的詞頻分布特徵,選取詞頻佔大頭的一批詞就行了。
例如,在本文的任務中,數據集共涉及到的辭彙量有8~9w,這樣訓練起來會很慢。經過分析,發現大概2w個詞就已經覆蓋了絕大部分篇幅,所以我就選取詞典大小為2w。然後,對文本數值化的時候,那些低頻詞就直接過濾掉了,只留下高頻詞。這樣,模型訓練起來效率就會大大提高。
詞向量
如果你接觸過詞向量,那麼一定會想到可以使用詞向量吧文本轉化成數值類型。不錯,我們在本文中也會這麼做。但是,如果直接吧文本轉化成詞向量,輸入進模型的話,我們可能無法繼續調優(fine-tune),詞向量相當於是對文本的特徵的一種表示,本身性質已經很好了。但是對於特定任務場景,我們一般都希望可以在訓練好的詞向量的基礎上,繼續用對應領域的數據對詞向量進一步進行優化。所以,今天我們會探索,如果在加入詞向量後,可以接著fine-tune。
keras自帶的文本預處理的工具十分好用,具體可參加我單獨寫的一個短文:https://beyondguo.github.io/2019-03-18-Keras-Text-Preprocessing/
我們設置詞典大小為20000,文本序列最大長度為200.
from keras.preprocessing.text import text_to_word_sequence,one_hot,Tokenizer from keras.preprocessing.sequence import pad_sequences import time vocab_size = 20000 maxlen = 200 print("Start fitting the corpus......") t = Tokenizer(vocab_size) # 要使得文本向量化時省略掉低頻詞,就要設置這個參數 tik = time.time() t.fit_on_texts(X_orig) # 在所有的評論數據集上訓練,得到統計信息 tok = time.time() word_index = t.word_index # 不受vocab_size的影響 print(all_vocab_size,len(word_index)) print("Fitting time: ",(tok-tik),s) print("Start vectorizing the sentences.......") v_X = t.texts_to_sequences(X_orig) # 受vocab_size的影響 print("Start padding......") pad_X = pad_sequences(v_X,maxlen=maxlen,padding=post) print("Finished!")
上面的代碼可以第一次讀會比較難理解,這裡稍微解釋一下:
Tokenizer是一個類,可以接收一個vocab_size的參數,也就是詞典大小。設置了詞典大小後,在後面生成文本的向量的時候,會把那些低頻詞(詞頻在20000開外的)都篩掉。
Tokenizer
定義了Tokenizer的一個實例t,然後調用方法t.fit_on_texts(X_orig)的作用,就是把我們所有的預料丟進去,讓t去統計,它會幫你統計詞頻,給每個詞分配index,形成字典等等。
t
t.fit_on_texts(X_orig)
想獲取index和詞的對照字典的話,就使用t.word_index方法。注意,獲取字典的時候,不會篩掉那些低頻詞,是所有詞的一個字典。
t.word_index
然後,想把一個句子、段落,轉化成對應的index表示的向量怎麼辦呢?Tokenizer也提供了便捷的方法,不用你自己去慢慢查表,直接使用t.texts_to_sequences(X_orig)方法,就可以獲取每句話的index組成的向量表示。注意,這裡,就已經吧低頻詞給過濾掉了,比如一句話有100個詞,其中有30個低頻詞,那麼經過這個函數,得到的就是長度為70的一個向量。
t.texts_to_sequences(X_orig)
得到每個句子的向量後,會發現大家長度各有不同,長的長短的短,這樣在後面的RNNs訓練時,就不方便批處理。所以,我們還需要對句子進行一個padding(填白,補全),把所有句子弄程統一長度,短的補上0,長的切掉。用的方法就是pad_sequences。
pad_sequences
上面代碼的輸出是:
Start fitting the corpus...... all_vocab_size 88582 Fitting time: 9.10555362701416 s Start vectorizing the sentences....... Start padding...... Finished!
可以看到,我們2.5w個文本,幾百萬詞,丟進去統計,效率還是挺高的,不到10秒就統計好了。
剛剛說了,獲取字典的時候,不會篩掉那些低頻詞,是所有詞的一個字典。但後面我們需要只保留那些高頻詞的一個字典,所以需要進行這樣一個操作,形成一個高頻詞字典:
import copy x = list(t.word_counts.items()) s = sorted(x,key=lambda p:p[1],reverse=True) small_word_index = copy.deepcopy(word_index) # 防止原來的字典也被改變了 print("Removing less freq words from word-index dict...") for item in s[20000:]: small_word_index.pop(item[0]) print("Finished!") print(len(small_word_index)) print(len(word_index))
Removing less freq words from word-index dict... Finished! 20000 88582
詞向量的導入:
import gensim model_file = ../big_things/w2v/GoogleNews-vectors-negative300.bin print("Loading word2vec model......") wv_model = gensim.models.KeyedVectors.load_word2vec_format(model_file,binary=True)
這裡採用Google發布的使用GoogleNews進行訓練的一個300維word2vec詞向量。這個讀者可以自行去網上下載。如果無法下載,可以到公眾號留言申請。
現在,我們需要把這個詞向量,跟我們本任務中的辭彙的index對應起來,也就是構建一個embedding matrix這樣就可以通過index找到對應的詞向量了。方法也很簡單:
embedding matrix
先隨機初始化一個embedding matrix,這裡需要注意的是,我們的辭彙量vocab_size雖然是20000,但是訓練的時候還是會碰到不少詞不在辭彙表裡,也在詞向量也查不到,那這些詞怎麼處理呢?我們就需要單獨給這些未知詞(UNK)一個index,在keras的文本預處理中,會默認保留index=0給這些未知詞。
embedding_matrix = np.random.uniform(size=(vocab_size+1,300)) # +1是要留一個給index=0 print("Transfering to the embedding matrix......") # sorted_small_index = sorted(list(small_word_index.items()),key=lambda x:x[1]) for word,index in small_word_index.items(): try: word_vector = wv_model[word] embedding_matrix[index] = word_vector except: print("Word: [",word,"] not in wvmodel! Use random embedding instead.") print("Finished!") print("Embedding matrix shape: ",embedding_matrix.shape)
通過上面的操作,所有的index都對應上了詞向量,那些不在word2vec中的詞和index=0的詞,詞向量就是隨機初始化的值。
劃分訓練集和測試集,當然使用經典的sklearn的train_test_split了。
train_test_split
廢話少說,直接上代碼:
from sklearn.model_selection import train_test_split np.random.seed = 1 random_indexs = np.random.permutation(len(pad_X)) X = pad_X[random_indexs] Y = Y_orig[random_indexs] print(Y[:50]) X_train, X_test, y_train, y_test = train_test_split(X,Y,test_size=0.2) print("X_train:",X_train.shape) print("y_train:",y_train.shape) print("X_test:",X_test.shape) print("y_test:",y_test.shape) print(list(y_train).count(1)) print(list(y_train).count(0))
[0 0 1 0 0 1 0 0 0 1 0 1 0 0 0 1 1 1 0 1 1 0 1 0 1 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 0 1 1 0 1 0 0 1 1 0] X_train: (20000, 200) y_train: (20000,) X_test: (5000, 200) y_test: (5000,) 9982 10018
訓練樣本2w,測試樣本5k.
唯一值得注意的一點就是,由於前面我們載入數據集的時候,正樣本和負樣本都聚在一塊,所以我們在這裡要把他們隨機打亂一下,用的就是numpy的random.permutation方法。這些都是慣用伎倆了。
random.permutation
恭喜!您已閱讀本文80%的內容!
做完了數據的預處理,後面的東西,就都是小菜一碟了。那麼多框架是幹嘛的?就是為了讓你用儘可能少的代碼把那些無聊的事情給做了!Keras尤其如此。
處理NLP問題,最常用的模型的就是RNN系列,LSTM和GRU隨便用。然後,一般還會在前面加一個embedding層。
之前我一直以為embedding層就是把預訓練好的詞向量加進去,實際上不是。即使沒有訓練好的詞向量,我們也可以使用embedding層。因為我們可以用我們的訓練數據,來訓練出詞的embedding,只不過這個embedding不同於word2vec的那種表達詞的含義的embedding,更多的是針對特定場景下的一個embedding。(不知道這樣說有沒有說清楚...)
所以,我們直接配置一個embedding層,不提供詞向量都可以訓練。如果提供了詞向量,這樣可以加速我們的訓練,相當於我們已經有一個訓練好的參數,提供給了模型,模型無非就需要接著改一改即可,而不是從一個隨機的狀態來慢慢訓練。
Talk is cheap, the code below is also cheap:
import keras from keras.models import Sequential,Model from keras.layers import Input,Dense,GRU,LSTM,Activation,Dropout,Embedding from keras.layers import Multiply,Concatenate,Dot
inputs = Input(shape=(maxlen,)) use_pretrained_wv = True if use_pretrained_wv: wv = Embedding(VOCAB_SIZE+1,wv_dim,input_length=MAXLEN,weights=[embedding_matrix]) (inputs) else: wv = Embedding(VOCAB_SIZE+1,wv_dim,input_length=MAXLEN)(inputs)
h = LSTM(128)(wv) y = Dense(1,activation=sigmoid)(h) m = Model(input=inputs,output=y) m.summary()
m.compile(optimizer=adam,loss=binary_crossentropy,metrics=[accuracy]) m.fit(X_train,y_train,batch_size=32,epochs=3,validation_split=0.15)
從上面的代碼可以知道,想要把預訓練的word2vec詞向量加入到模型中,就是把詞向量作為embedding層的參數(weights),具體我們需要先構建一個embedding matrix,這個我們在前面已經構建好了,然後傳進embedding層即可。
運行!輸出:
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_7 (InputLayer) (None, 200) 0 _________________________________________________________________ embedding_7 (Embedding) (None, 200, 128) 2560128 _________________________________________________________________ lstm_7 (LSTM) (None, 128) 131584 _________________________________________________________________ dense_7 (Dense) (None, 1) 129 ================================================================= Total params: 2,691,841 Trainable params: 2,691,841 Non-trainable params: 0 _________________________________________________________________ Train on 17000 samples, validate on 3000 samples Epoch 1/3 17000/17000 [==============================] - 178s 10ms/step - loss: 0.6711 - acc: 0.5692 - val_loss: 0.6701 - val_acc: 0.5697 Epoch 2/3 17000/17000 [==============================] - 168s 10ms/step - loss: 0.5964 - acc: 0.6479 - val_loss: 0.5072 - val_acc: 0.7940 Epoch 3/3 17000/17000 [==============================] - 169s 10ms/step - loss: 0.5104 - acc: 0.7171 - val_loss: 0.4976 - val_acc: 0.7943
可以發現,參數的大部分,都是embedding層的參數。所以,讀者可以嘗試一下將詞向量參數固定,可以發現訓練速度會快得多。但是效果可能會略差一些。
建議讀者對比一下:
①不使用word2vec作為embedding的參數
②使用word2vec作為embedding的參數並固定參數
③使用word2vec作為embedding的參數並繼續fine-tune
相信會有一些有意思的發現。
但是你可能沒時間(~~多半是懶!~~),所以這裡我也告訴大家我的實驗結果:
①效果最差,時間最長
②效果最好,時間較長
③效果中等,時間最快
本文帶著讀者詳細的了解了使用keras進行文本預處理,如何將詞向量加入到訓練模型中提升性能,動手的讀者更可以體會到不同詞向量使用方法的差別。 這裡,我們差不多直觀上感受到了NLP是啥感覺,後面的文章,會主要探討一下Attention機制在這個基礎上的應用,然後我們還會嘗試使用CNN來做一下同樣的任務,看看效果如何。相信我們會有新的發現!
本文帶著讀者詳細的了解了使用keras進行文本預處理,如何將詞向量加入到訓練模型中提升性能,動手的讀者更可以體會到不同詞向量使用方法的差別。
如果喜歡我的教程,歡迎關注我的專欄:
【 DeepLearning.ai學習筆記】