前言:
深度學習模型在計算機視覺與語音識別方面取得了卓越的成就,在 NLP 領域也是可以的。將卷積神經網絡CNN應用到文本分類任務,利用多個不同size的kernel來提取句子中的關鍵信息(類似 n-gram 的關鍵信息),從而能夠更好地捕捉局部相關性。
文本分類是自然語言處理領域最活躍的研究方向之一,目前文本分類在工業界的應用場景非常普遍,從新聞的分類、商品評論信息的情感分類到微博信息打標籤輔助推薦系統,瞭解文本分類技術是NLP初學者比較好的切入點,較簡單且應用場景高頻。
一、論文筆記
1、Yoon Kim在2014年 “Convolutional Neural Networks for Sentence Classification” 論文中提出TextCNN(利用卷積神經網絡對文本進行分類的算法)(該論文翻譯)。
上圖很好地詮釋了模型的框架。假設我們有一些句子需要對其進行分類。句子中每個詞是由n維詞向量組成的,也就是說輸入矩陣大小爲m*n,其中m爲句子長度。CNN需要對輸入樣本進行卷積操作,對於文本數據,filter不再橫向滑動,僅僅是向下移動,有點類似於N-gram在提取詞與詞間的局部相關性。圖中共有三種步長策略,分別是2,3,4,每個步長都有兩個filter(實際訓練時filter數量會很多)。在不同詞窗上應用不同filter,最終得到6個卷積後的向量。然後對每一個向量進行最大化池化操作並拼接各個池化值,最終得到這個句子的特徵表示,將這個句子向量丟給分類器進行分類,至此完成整個流程。
(1)嵌入層(Embedding Layer)
通過一個隱藏層, 將 one-hot 編碼的詞投影到一個低維空間中,本質上是特徵提取器,在指定維度中編碼語義特徵。 這樣, 語義相近的詞, 它們的歐氏距離或餘弦距離也比較近。(作者使用的單詞向量是預訓練的,方法爲fasttext得到的單詞向量,當然也可以使用word2vec和GloVe方法訓練得到的單詞向量)。
(2)卷積層(Convolution Laye)
在處理圖像數據時,CNN使用的卷積核的寬度和高度的一樣的,但是在text-CNN中,卷積核的寬度是與詞向量的維度一致!這是因爲我們輸入的每一行向量代表一個詞,在抽取特徵的過程中,詞做爲文本的最小粒度。而高度和CNN一樣,可以自行設置(通常取值2,3,4,5),高度就類似於n-gram了。由於我們的輸入是一個句子,句子中相鄰的詞之間關聯性很高,因此,當我們用卷積核進行卷積時,不僅考慮了詞義而且考慮了詞序及其上下文(類似於skip-gram和CBOW模型的思想)。
(3)池化層(Pooling Layer)
因爲在卷積層過程中我們使用了不同高度的卷積核,使得我們通過卷積層後得到的向量維度會不一致,所以在池化層中,我們使用1-Max-pooling對每個特徵向量池化成一個值,即抽取每個特徵向量的最大值表示該特徵,而且認爲這個最大值表示的是最重要的特徵。當我們對所有特徵向量進行1-Max-Pooling之後,還需要將每個值給拼接起來。得到池化層最終的特徵向量。在池化層到全連接層之前可以加上dropout防止過擬合。
(4)全連接層(Fully connected layer)
全連接層跟其他模型一樣,假設有兩層全連接層,第一層可以加上’relu’作爲激活函數,第二層則使用softmax激活函數得到屬於每個類的概率。
(5)TextCNN的小變種
在詞向量構造方面可以有以下不同的方式: CNN-rand: 隨機初始化每個單詞的詞向量通過後續的訓練去調整。 CNN-static: 使用預先訓練好的詞向量,如word2vec訓練出來的詞向量,在訓練過程中不再調整該詞向量。 CNN-non-static: 使用預先訓練好的詞向量,並在訓練過程進一步進行調整。 CNN-multichannel: 將static與non-static作爲兩通道的詞向量。
(6)參數與超參數
sequence_length (Q: 對於CNN, 輸入與輸出都是固定的,可每個句子長短不一, 怎麼處理? A: 需要做定長處理, 比如定爲n, 超過的截斷, 不足的補0. 注意補充的0對後面的結果沒有影響,因爲後面的max-pooling只會輸出最大值,補零的項會被過濾掉)
num_classes (多分類, 分爲幾類)
vocabulary_size (語料庫的詞典大小, 記爲|D|)
embedding_size (將詞向量的維度, 由原始的 |D| 降維到 embedding_size)
filter_size_arr (多個不同size的filter)
2、2015年“A Sensitivity Analysis of (and Practitioners' Guide to) Convolutional Neural Networks for Sentence Classification”論文詳細地闡述了關於TextCNN模型的調參心得。
(1)TextCNN詳細過程:
- Embedding:第一層是圖中最左邊的7乘5的句子矩陣,每行是詞向量,維度=5,這個可以類比爲圖像中的原始像素點。
- Convolution:然後經過 kernel_sizes=(2,3,4) 的一維卷積層,每個kernel_size 有兩個輸出 channel。
- MaxPolling:第三層是一個1-max pooling層,這樣不同長度句子經過pooling層之後都能變成定長的表示。
- FullConnection and Softmax:最後接一層全連接的 softmax 層,輸出每個類別的概率。
(2)論文調參結論:
- 使用預訓練的word2vec 、 GloVe初始化效果會更好。一般不直接使用One-hot。
- 卷積核的大小影響較大,一般取1~10,對於句子較長的文本,則應選擇大一些。
- 卷積核的數量也有較大的影響,一般取100~600 ,同時一般使用Dropout(0~0.5)。
- 激活函數一般選用ReLU 和 tanh。
- 池化使用1-max pooling。
- 隨着feature map數量增加,性能減少時,試着嘗試大於0.5的Dropout。
- 評估模型性能時,記得使用交叉驗證。
二、Keras文本預處理
1、讀取數據集
2、使用Tokenizer將文字轉換成數字特徵
使用Keras的Tokenizer模塊實現轉換。當我們創建了一個Tokenizer對象後,使用該對象的fit_on_texts()函數,可以將輸入的文本中的每個詞編號,編號是根據詞頻的,詞頻越大,編號越小。使用word_index屬性可以看到每次詞對應的編碼。
3、將數據集中的每條文本轉換爲數字列表,使用每個詞的編號進行編號
使用該對象的texts_to_sequences()函數,將每條文本轉變成一個向量。
4、使用pad_sequences()讓每句數字影評長度相同
由於每句話的長度不唯一,需要將每句話的長度設置一個固定值。將超過固定值的部分截掉,不足的在最前面用0填充。
5、使用Embedding層將每個詞編碼轉換爲詞向量
Embedding層基於上文所得的詞編碼,對每個詞進行one-hot編碼,每個詞都會是一個vocabulary_size維的向量;然後通過神經網絡的訓練迭代更新得到一個合適的權重矩陣(具體實現過程可以參考skip-gram模型),行大小爲vocabulary_size,列大小爲詞向量的維度,將本來以one-hot編碼的詞向量映射到低維空間,得到低維詞向量。需要聲明一點的是Embedding層是作爲模型的第一層,在訓練模型的同時,得到該語料庫的詞向量。當然,也可以使用已經預訓練好的詞向量表示現有語料庫中的詞。
文本預處理目的:將每個樣本轉換爲一個數字矩陣,矩陣的每一行表示一個詞向量。
Keras文本預處理代碼實現:
from sklearn.model_selection import train_test_split
import pandas as pd
import jieba
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
if __name__=='__main__':
dataset = pd.read_csv('sentiment_analysis/data_train.csv', sep='\t',names=['ID', 'type', 'review', 'label']).astype(str)
cw = lambda x: list(jieba.cut(x))
dataset['words'] = dataset['review'].apply(cw)
tokenizer=Tokenizer() #創建一個Tokenizer對象
#fit_on_texts函數可以將輸入的文本中的每個詞編號,編號是根據詞頻的,詞頻越大,編號越小
tokenizer.fit_on_texts(dataset['words'])
vocab=tokenizer.word_index #得到每個詞的編號
x_train, x_test, y_train, y_test = train_test_split(dataset['words'], dataset['label'], test_size=0.1)
# 將每個樣本中的每個詞轉換爲數字列表,使用每個詞的編號進行編號
x_train_word_ids=tokenizer.texts_to_sequences(x_train)
x_test_word_ids = tokenizer.texts_to_sequences(x_test)
#序列模式
# 每條樣本長度不唯一,將每條樣本的長度設置一個固定值
x_train_padded_seqs=pad_sequences(x_train_word_ids,maxlen=50) #將超過固定值的部分截掉,不足的在最前面用0填充
x_test_padded_seqs=pad_sequences(x_test_word_ids, maxlen=50)
三、基於keras的TextCNN模型的構建、訓練與測試
1、基礎版CNN(模仿LeNet-5)
LeNet-5是卷積神經網絡的作者Yann LeCun用於MNIST識別任務提出的模型。模型很簡單,就是卷積池化層的堆疊,最後加上幾層全連接層。將其運用在文本分類任務中。
#構建CNN分類模型(LeNet-5)
#模型結構:嵌入-卷積池化*2-dropout-BN-全連接-dropout-全連接
def CNN_model(x_train_padded_seqs, y_train, x_test_padded_seqs, y_test):
model = Sequential()
model.add(Embedding(len(vocab) + 1, 300, input_length=50)) #使用Embeeding層將每個詞編碼轉換爲詞向量
model.add(Conv1D(256, 5, padding='same'))
model.add(MaxPooling1D(3, 3, padding='same'))
model.add(Conv1D(128, 5, padding='same'))
model.add(MaxPooling1D(3, 3, padding='same'))
model.add(Conv1D(64, 3, padding='same'))
model.add(Flatten())
model.add(Dropout(0.1))
model.add(BatchNormalization()) # (批)規範化層
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.1))
model.add(Dense(3, activation='softmax'))
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
one_hot_labels = keras.utils.to_categorical(y_train, num_classes=3) # 將標籤轉換爲one-hot編碼
model.fit(x_train_padded_seqs, one_hot_labels,epochs=5, batch_size=800)
y_predict = model.predict_classes(x_test_padded_seqs) # 預測的是類別,結果就是類別號
y_predict = list(map(str, y_predict))
print('準確率', metrics.accuracy_score(y_test, y_predict))
print('平均f1-score:', metrics.f1_score(y_test, y_predict, average='weighted'))
2、簡單版TextCNN
#構建TextCNN模型
#模型結構:詞嵌入-卷積池化*3-拼接-全連接-dropout-全連接
def TextCNN_model_1(x_train_padded_seqs,y_train,x_test_padded_seqs,y_test):
main_input = Input(shape=(50,), dtype='float64')
# 詞嵌入(使用預訓練的詞向量)
embedder = Embedding(len(vocab) + 1, 300, input_length=50, trainable=False)
embed = embedder(main_input)
# 詞窗大小分別爲3,4,5
cnn1 = Conv1D(256, 3, padding='same', strides=1, activation='relu')(embed)
cnn1 = MaxPooling1D(pool_size=48)(cnn1)
cnn2 = Conv1D(256, 4, padding='same', strides=1, activation='relu')(embed)
cnn2 = MaxPooling1D(pool_size=47)(cnn2)
cnn3 = Conv1D(256, 5, padding='same', strides=1, activation='relu')(embed)
cnn3 = MaxPooling1D(pool_size=46)(cnn3)
# 合併三個模型的輸出向量
cnn = concatenate([cnn1, cnn2, cnn3], axis=-1)
flat = Flatten()(cnn)
drop = Dropout(0.2)(flat)
main_output = Dense(3, activation='softmax')(drop)
model = Model(inputs=main_input, outputs=main_output)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
one_hot_labels = keras.utils.to_categorical(y_train, num_classes=3) # 將標籤轉換爲one-hot編碼
model.fit(x_train_padded_seqs, one_hot_labels, batch_size=800, epochs=10)
#y_test_onehot = keras.utils.to_categorical(y_test, num_classes=3) # 將標籤轉換爲one-hot編碼
result = model.predict(x_test_padded_seqs) # 預測樣本屬於每個類別的概率
result_labels = np.argmax(result, axis=1) # 獲得最大概率對應的標籤
y_predict = list(map(str, result_labels))
print('準確率', metrics.accuracy_score(y_test, y_predict))
print('平均f1-score:', metrics.f1_score(y_test, y_predict, average='weighted'))
3、使用Word2Vec詞向量的TextCNN
w2v_model=Word2Vec.load('sentiment_analysis/w2v_model.pkl')
# 預訓練的詞向量中沒有出現的詞用0向量表示
embedding_matrix = np.zeros((len(vocab) + 1, 300))
for word, i in vocab.items():
try:
embedding_vector = w2v_model[str(word)]
embedding_matrix[i] = embedding_vector
except KeyError:
continue
#構建TextCNN模型
def TextCNN_model_2(x_train_padded_seqs,y_train,x_test_padded_seqs,y_test,embedding_matrix):
# 模型結構:詞嵌入-卷積池化*3-拼接-全連接-dropout-全連接
main_input = Input(shape=(50,), dtype='float64')
# 詞嵌入(使用預訓練的詞向量)
embedder = Embedding(len(vocab) + 1, 300, input_length=50, weights=[embedding_matrix], trainable=False)
#embedder = Embedding(len(vocab) + 1, 300, input_length=50, trainable=False)
embed = embedder(main_input)
# 詞窗大小分別爲3,4,5
cnn1 = Conv1D(256, 3, padding='same', strides=1, activation='relu')(embed)
cnn1 = MaxPooling1D(pool_size=38)(cnn1)
cnn2 = Conv1D(256, 4, padding='same', strides=1, activation='relu')(embed)
cnn2 = MaxPooling1D(pool_size=37)(cnn2)
cnn3 = Conv1D(256, 5, padding='same', strides=1, activation='relu')(embed)
cnn3 = MaxPooling1D(pool_size=36)(cnn3)
# 合併三個模型的輸出向量
cnn = concatenate([cnn1, cnn2, cnn3], axis=-1)
flat = Flatten()(cnn)
drop = Dropout(0.2)(flat)
main_output = Dense(3, activation='softmax')(drop)
model = Model(inputs=main_input, outputs=main_output)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
one_hot_labels = keras.utils.to_categorical(y_train, num_classes=3) # 將標籤轉換爲one-hot編碼
model.fit(x_train_padded_seqs, one_hot_labels, batch_size=800, epochs=20)
#y_test_onehot = keras.utils.to_categorical(y_test, num_classes=3) # 將標籤轉換爲one-hot編碼
result = model.predict(x_test_padded_seqs) # 預測樣本屬於每個類別的概率
result_labels = np.argmax(result, axis=1) # 獲得最大概率對應的標籤
y_predict = list(map(str, result_labels))
print('準確率', metrics.accuracy_score(y_test, y_predict))
print('平均f1-score:', metrics.f1_score(y_test, y_predict, average='weighted'))
四、使用keras的plot_model()畫出的TextCNN模型結構圖
1、環境配置
(1)安裝graphviz模塊
首先,命令行pip install graphviz;其次,安裝graphviz軟件,官網下載:graphviz-2.38.msi ;最後,將安裝目錄中的graphviz-2.38\release\bin添加進Path環境變量。
(2)安裝pydot模塊
命令行pip install pydot
(3)在運行程序中加入下面兩行代碼
import os
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
2、使用plot_model()畫出模型圖
from keras.utils import plot_model
#生成一個模型圖,第一個參數爲模型,第二個參數爲要生成圖片的路徑及文件名,還可以指定兩個參數:
#show_shapes:指定是否顯示輸出數據的形狀,默認爲False
#show_layer_names:指定是否顯示層名稱,默認爲True
plot_model(model,to_file='sentiment_analysis/model.png',show_shapes=True,show_layer_names=False)
模型圖如下:
五、keras模型的保存與加載
from keras.models import load_model
#模型的保存
model.save('model.h5')
#模型的加載
model=load_model('model.h5')
參考學習資料:
(1)Keras之文本分類實現
(3)NLP論文
(5)用深度學習(CNN RNN Attention)解決大規模文本分類問題 - 綜述和實踐