入門|CNN也能用於NLP任務,一文簡述文本分類任務的7個模型
選自Ahmed BESBES
作者:Ahmed Besbes
機器之心編譯
參與:李詩萌、路
本文介紹了用於文本分類任務的 7 個模型,包括傳統的詞袋模型、循環神經網路,也有常用於計算機視覺任務的卷積神經網路,以及 RNN + CNN。
本文是我之前寫過的一篇基於推特數據進行情感分析的文章(https://ahmedbesbes.com/sentiment-analysis-on-twitter-using-word2vec-and-keras.html)的延伸內容。那時我建立了一個簡單的模型:基於 keras 訓練的兩層前饋神經網路。用組成推文的詞嵌入的加權平均值作為文檔向量來表示輸入推文。
我用的嵌入是用 gensim 基於語料庫從頭訓練出來的 word2vec 模型。該是一個二分類任務,準確率能達到 79%。
本文目標在於探索其他在相同數據集上訓練出來的 NLP 模型,然後在給定的測試集上對這些模型的性能進行評估。
我們將通過不同的模型(從依賴於詞袋錶征的簡單模型到部署了卷積/循環網路的複雜模型)了解能否得到高於 79% 的準確率!
首先,將從簡單的模型開始,逐步增加模型的複雜度。這項工作是為了說明簡單的模型也能很有效。
我會進行這些嘗試:
- 用詞級的 ngram 做 logistic 回歸
- 用字元級的 ngram 做 logistic 回歸
- 用詞級的 ngram 和字元級的 ngram 做 Logistic 回歸
- 在沒有對詞嵌入進行預訓練的情況下訓練循環神經網路(雙向 GRU)
- 用 GloVe 對詞嵌入進行預訓練,然後訓練循環神經網路
- 多通道卷積神經網路
- RNN(雙向 GRU)+ CNN 模型
文末附有這些 NLP 技術的樣板代碼。這些代碼可以幫助你開啟自己的 NLP 項目並獲得最優結果(這些模型中有一些非常強大)。
我們還可以提供一個綜合基準,我們可以利用該基準分辨哪個模型最適合預測推文中的情緒。
在相關的 GitHub 庫中還有不同的模型、這些模型的預測結果以及測試集。你可以自己嘗試並得到可信的結果。
import os
import re
import warnings
warnings.simplefilter("ignore", UserWarning)
from matplotlib import pyplot as plt
%matplotlib inline
import pandas as pd
pd.options.mode.chained_assignment = None
import numpy as np
from string import punctuation
from nltk.tokenize import word_tokenize
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, auc, roc_auc_score
from sklearn.externals import joblib
import scipy
from scipy.sparse import hstack
0. 數據預處理
你可以從該鏈接(http://thinknook.com/twitter-sentiment-analysis-training-corpus-dataset-2012-09-22/)下載數據集。
載入數據並提取所需變數(情感及情感文本)。
該數據集包含 1,578,614 個分好類的推文,每一行都用 1(積極情緒)和 0(消極情緒)進行了標記。
作者建議用 1/10 的數據進行測試,其餘數據用於訓練。
data = pd.read_csv("./data/tweets.csv", encoding="latin1", usecols=["Sentiment", "SentimentText"])
data.columns = ["sentiment", "text"]
data = data.sample(frac=1, random_state=42)
print(data.shape)
(1578614, 2)
for row in data.head(10).iterrows():
print(row[1]["sentiment"], row[1]["text"])
1 http://www.popsugar.com/2999655 keep voting for robert pattinson in the popsugar100 as well!!
1 @GamrothTaylor I am starting to worry about you, only I have Navy Seal type sleep hours.
0 sunburned...no sunbaked! ow. it hurts to sit.
1 Celebrating my 50th birthday by doing exactly the same as I do every other day - working on our websites. It"s just another day.
1 Leah and Aiden Gosselin are the cutest kids on the face of the Earth
1 @MissHell23 Oh. I didn"t even notice.
0 WTF is wrong with me?!!! I"m completely miserable. I need to snap out of this
0 Was having the best time in the gym until I got to the car and had messages waiting for me... back to the down stage!
1 @JENTSYY oh what happened??
0 @catawu Ghod forbid he should feel responsible for anything!
推文數據中存在很多雜訊,我們刪除了推文中的網址、主題標籤和用戶提及來清理數據。
def tokenize(tweet):
tweet = re.sub(r"httpS+", "", tweet)
tweet = re.sub(r"#(w+)", "", tweet)
tweet = re.sub(r"@(w+)", "", tweet)
tweet = re.sub(r"[^ws]", "", tweet)
tweet = tweet.strip().lower()
tokens = word_tokenize(tweet)
return tokens
將清理好的數據保存在硬碟上。
data["tokens"] = data.text.progress_map(tokenize)
data["cleaned_text"] = data["tokens"].map(lambda tokens: " ".join(tokens))
data[["sentiment", "cleaned_text"]].to_csv("./data/cleaned_text.csv")
data = pd.read_csv("./data/cleaned_text.csv")
print(data.shape)
(1575026, 2)
data.head()
既然數據集已經清理乾淨了,就可以準備分割訓練集和測試集來建立模型了。
本文數據都是用這種方式分割的。
x_train, x_test, y_train, y_test = train_test_split(data["cleaned_text"],
data["sentiment"],
test_size=0.1,
random_state=42,
stratify=data["sentiment"])
print(x_train.shape, x_test.shape, y_train.shape, y_test.shape)
(1417523,) (157503,) (1417523,) (157503,)
將測試集標籤存儲在硬碟上以便後續使用。
pd.DataFrame(y_test).to_csv("./predictions/y_true.csv", index=False, encoding="utf-8")
接下來就可以應用機器學習方法了。
1. 基於詞級 ngram 的詞袋模型
那麼,什麼是 n-gram 呢?
如圖所示,ngram 是將可在源文本中找到的長度為 n 的相鄰詞的所有組合。
我們的模型將以 unigrams(n=1)和 bigrams(n=2)為特徵。
用矩陣表示數據集,矩陣的每一行表示一條推文,每一列表示從推文(已經經過分詞和清理)中提取的特徵(一元模型或二元模型)。每個單元格是 tf-idf 分數(也可以用更簡單的值,但 tf-idf 比較通用且效果較好)。我們將該矩陣稱為文檔-詞項矩陣。
略經思考可知,擁有 150 萬推文的語料庫的一元模型和二元模型去重後的數量還是很大的。事實上,出於計算力的考慮,我們可將這個數設置為固定值。你可以通過交叉驗證來確定這個值。
在向量化之後,語料庫如下圖所示:
I like pizza a lot
假設使用上述特徵讓模型對這句話進行預測。
由於我們使用的是一元模型和二元模型後,因此模型提取出了下列特徵:
i, like, pizza, a, lot, i like, like pizza, pizza a, a lot
因此,句子變成了大小為 N(分詞總數)的向量,這個向量中包含 0 和這些 ngram 的 tf-idf 分數。所以接下來其實是要處理這個大而稀疏的向量。
一般而言,線性模型可以很好地處理大而稀疏的數據。此外,與其他模型相比,線性模型的訓練速度也更快。
從過去的經驗可知,logistic 回歸可以在稀疏的 tf-idf 矩陣上良好地運作。
vectorizer_word = TfidfVectorizer(max_features=40000,
min_df=5,
max_df=0.5,
analyzer="word",
stop_words="english",
ngram_range=(1, 2))
vectorizer_word.fit(x_train, leave=False)
tfidf_matrix_word_train = vectorizer_word.transform(x_train)
tfidf_matrix_word_test = vectorizer_word.transform(x_test)
在為訓練集和測試集生成了 tf-idf 矩陣後,就可以建立第一個模型並對其進行測試。
tf-idf 矩陣是 logistic 回歸的特徵。
lr_word = LogisticRegression(solver="sag", verbose=2)
lr_word.fit(tfidf_matrix_word_train, y_train)
一旦訓練好模型後,就可以將其應用於測試數據以獲得預測值。然後將這些值和模型一併存儲在硬碟上。
joblib.dump(lr_word, "./models/lr_word_ngram.pkl")
y_pred_word = lr_word.predict(tfidf_matrix_word_test)
pd.DataFrame(y_pred_word, columns=["y_pred"]).to_csv("./predictions/lr_word_ngram.csv", index=False)
得到準確率:
y_pred_word = pd.read_csv("./predictions/lr_word_ngram.csv")
print(accuracy_score(y_test, y_pred_word))
0.782042246814
第一個模型得到了 78.2% 的準確率!真不賴。接下來了解一下第二個模型。
2. 基於字元級 ngram 的詞袋模型
我們從未說過 ngram 僅為詞服務,也可將其應用於字元上。
如你所見,我們將對字元級 ngram 使用與圖中一樣的代碼,現在直接來看 4-grams 建模。
基本上這意味著,像「I like this movie」這樣的句子會有下列特徵:
I, l, i, k, e, ..., I li, lik, like, ..., this, ... , is m, s mo, movi, ...
字元級 ngram 很有效,在語言建模任務中,甚至可以比分詞表現得更好。像垃圾郵件過濾或自然語言識別這樣的任務就高度依賴字元級 ngram。
與之前學習單片語合的模型不同,該模型學習的是字母組合,這樣就可以處理單詞的形態構成。
基於字元的表徵的一個優勢是可以更好地解決單詞拼寫錯誤的問題。
我們來運行同樣的流程:
vectorizer_char = TfidfVectorizer(max_features=40000,
min_df=5,
max_df=0.5,
analyzer="char",
ngram_range=(1, 4))
vectorizer_char.fit(tqdm_notebook(x_train, leave=False));
tfidf_matrix_char_train = vectorizer_char.transform(x_train)
tfidf_matrix_char_test = vectorizer_char.transform(x_test)
lr_char = LogisticRegression(solver="sag", verbose=2)
lr_char.fit(tfidf_matrix_char_train, y_train)
y_pred_char = lr_char.predict(tfidf_matrix_char_test)
joblib.dump(lr_char, "./models/lr_char_ngram.pkl")
pd.DataFrame(y_pred_char, columns=["y_pred"]).to_csv("./predictions/lr_char_ngram.csv", index=False)
y_pred_char = pd.read_csv("./predictions/lr_char_ngram.csv")
print(accuracy_score(y_test, y_pred_char))
0.80420055491
80.4% 的準確率!字元級 ngram 模型的性能要比詞級的 ngram 更好。
3. 基於詞級 ngram 和字元級 ngram 的詞袋模型
與詞級 ngram 的特徵相比,字元級 ngram 特徵似乎提供了更好的準確率。那麼將字元級 ngram 和詞級 ngram 結合效果又怎麼樣呢?
我們將兩個 tf-idf 矩陣連接在一起,建立一個新的、混合 tf-idf 矩陣。該模型有助於學習單詞形態結構以及與這個單詞大概率相鄰單詞的形態結構。
將這些屬性結合在一起。
tfidf_matrix_word_char_train = hstack((tfidf_matrix_word_train, tfidf_matrix_char_train))
tfidf_matrix_word_char_test = hstack((tfidf_matrix_word_test, tfidf_matrix_char_test))
lr_word_char = LogisticRegression(solver="sag", verbose=2)
lr_word_char.fit(tfidf_matrix_word_char_train, y_train)
y_pred_word_char = lr_word_char.predict(tfidf_matrix_word_char_test)
joblib.dump(lr_word_char, "./models/lr_word_char_ngram.pkl")
pd.DataFrame(y_pred_word_char, columns=["y_pred"]).to_csv("./predictions/lr_word_char_ngram.csv", index=False)
y_pred_word_char = pd.read_csv("./predictions/lr_word_char_ngram.csv")
print(accuracy_score(y_test, y_pred_word_char))
0.81423845895
得到了 81.4% 的準確率。該模型只加了一個整體單元,但結果比之前的兩個都要好。
關於詞袋模型
- 優點:考慮到其簡單的特性,詞袋模型已經很強大了,它們訓練速度快,且易於理解。
- 缺點:即使 ngram 帶有一些單詞間的語境,但詞袋模型無法建模序列中單詞間的長期依賴關係。
現在要用到深度學習模型了。深度學習模型的表現優於詞袋模型是因為深度學習模型能夠捕捉到句子中單詞間的順序依賴關係。這可能要歸功於循環神經網路這一特殊神經網路結構的出現了。
本文並未涵蓋 RNN 的理論基礎,但該鏈接(http://colah.github.io/posts/2015-08-Understanding-LSTMs/)中的內容值得一讀。這篇文章來源於 Cristopher Olah 的博客,詳細敘述了一種特殊的 RNN 模型:長短期記憶網路(LSTM)。
在開始之前,要先設置一個深度學習專用的環境,以便在 TensorFlow 上使用 Keras。誠實地講,我試著在個人筆記本上運行這些代碼,但考慮到數據集的大小和 RNN 架構的複雜程度,這是很不實際的。還有一個很好的選擇是 AWS。我一般在 EC2 p2.xlarge 實例上用深度學習 AMI(https://aws.amazon.com/marketplace/pp/B077GCH38C?qid=1527197041958&sr=0-1&ref_=srh_res_product_title)。亞馬遜 AMI 是安裝了所有包(TensorFlow、PyTorch 和 Keras 等)的預先配置過的 VM 圖。強烈推薦大家使用!
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.text import text_to_word_sequence
from keras.preprocessing.sequence import pad_sequences
from keras.models import Model
from keras.models import Sequential
from keras.layers import Input, Dense, Embedding, Conv1D, Conv2D, MaxPooling1D, MaxPool2D
from keras.layers import Reshape, Flatten, Dropout, Concatenate
from keras.layers import SpatialDropout1D, concatenate
from keras.layers import GRU, Bidirectional, GlobalAveragePooling1D, GlobalMaxPooling1D
from keras.callbacks import Callback
from keras.optimizers import Adam
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras.models import load_model
from keras.utils.vis_utils import plot_model
4. 沒有預訓練詞嵌入的循環神經網路
RNN 可能看起來很可怕。儘管它們因為複雜而難以理解,但非常有趣。RNN 模型封裝了一個非常漂亮的設計,以克服傳統神經網路在處理序列數據(文本、時間序列、視頻、DNA 序列等)時的短板。
RNN 是一系列神經網路的模塊,它們彼此連接像鎖鏈一樣。每一個都將消息向後傳遞。強烈推薦大家從 Colah 的博客中深入了解它的內部機制,下面的圖就來源於此。
我們要處理的序列類型是文本數據。對意義而言,單詞順序很重要。RNN 考慮到了這一點,它可以捕捉長期依賴關係。
為了在文本數據上使用 Keras,我們首先要對數據進行預處理。可以用 Keras 的 Tokenizer 類。該對象用 num_words 作為參數,num_words 是根據詞頻進行分詞後保留下來的最大詞數。
MAX_NB_WORDS = 80000
tokenizer = Tokenizer(num_words=MAX_NB_WORDS)
tokenizer.fit_on_texts(data["cleaned_text"])
當分詞器適用於數據時,我們就可以用分詞器將文本字元級 ngram 轉換為數字序列。
這些數字表示每個單詞在字典中的位置(將其視為映射)。
如下例所示:
x_train[15]
"breakfast time happy time"
這裡說明了分詞器是如何將其轉換為數字序列的。
tokenizer.texts_to_sequences([x_train[15]])
[[530, 50, 119, 50]]
接下來在訓練序列和測試序列中應用該分詞器:
train_sequences = tokenizer.texts_to_sequences(x_train)
test_sequences = tokenizer.texts_to_sequences(x_test)
將推文映射到整數列表中。但是由於長度不同,還是沒法將它們在矩陣中堆疊在一起。還好 Keras 允許用 0 將序列填充至最大長度。我們將這個長度設置為 35(這是推文中的最大分詞數)。
MAX_LENGTH = 35
padded_train_sequences = pad_sequences(train_sequences, maxlen=MAX_LENGTH)
padded_test_sequences = pad_sequences(test_sequences, maxlen=MAX_LENGTH)
padded_train_sequences
array([[ 0, 0, 0, ..., 2383, 284, 9],
[ 0, 0, 0, ..., 13, 30, 76],
[ 0, 0, 0, ..., 19, 37, 45231],
...,
[ 0, 0, 0, ..., 43, 502, 1653],
[ 0, 0, 0, ..., 5, 1045, 890],
[ 0, 0, 0, ..., 13748, 38750, 154]])
padded_train_sequences.shape
(1417523, 35)
現在就可以將數據傳入 RNN 了。
以下是我將使用的架構的一些元素:
- 嵌入維度為 300。這意味著我們使用的 8 萬個單詞中的每一個都被映射至 300 維的密集(浮點數)向量。該映射將在訓練過程中進行調整。
- 在嵌入層上應用 spatial dropout 層以減少過擬合:按批次查看 35*300 的矩陣,隨機刪除每個矩陣中(設置為 0)的詞向量(行)。這有助於將注意力不集中在特定的詞語上,有利於模型的泛化。
- 雙向門控循環單元(GRU):這是循環網路部分。這是 LSTM 架構更快的變體。將其視為兩個循環網路的組合,這樣就可以從兩個方向同時掃描文本序列:從左到右和從右到左。這使得網路在閱讀給定單詞時,可以結合之前和之後的內容理解文本。GRU 中每個網路塊的輸出 h_t 的維度即單元數,將這個值設置為 100。由於用了雙向 GRU,因此每個 RNN 塊的最終輸出都是 200 維的。
雙向 GRU 的輸出是有維度的(批尺寸、時間步和單元)。這意味著如果用的是經典的 256 的批尺寸,維度將會是 (256, 35, 200)。
- 在每個批次上應用的是全局平均池化,其中包含了每個時間步(即單詞)對應的輸出向量的平均值。
- 我們應用了相同的操作,只是用最大池化替代了平均池化。
- 將前兩個操作的輸出連接在了一起。
def get_simple_rnn_model():
embedding_dim = 300
embedding_matrix = np.random.random((MAX_NB_WORDS, embedding_dim))
inp = Input(shape=(MAX_LENGTH, ))
x = Embedding(input_dim=MAX_NB_WORDS, output_dim=embedding_dim, input_length=MAX_LENGTH,
weights=[embedding_matrix], trainable=True)(inp)
x = SpatialDropout1D(0.3)(x)
x = Bidirectional(GRU(100, return_sequences=True))(x)
avg_pool = GlobalAveragePooling1D()(x)
max_pool = GlobalMaxPooling1D()(x)
conc = concatenate([avg_pool, max_pool])
outp = Dense(1, activation="sigmoid")(conc)
model = Model(inputs=inp, outputs=outp)
model.compile(loss="binary_crossentropy",
optimizer="adam",
metrics=["accuracy"])
return model
rnn_simple_model = get_simple_rnn_model()
該模型的不同層如下所示:
plot_model(rnn_simple_model,
to_file="./images/article_5/rnn_simple_model.png",
show_shapes=True,
show_layer_names=True)
在訓練期間使用了模型檢查點。這樣可以在每個 epoch 的最後將最佳模型(可以用準確率度量)自動存儲(在硬碟上)。
filepath="./models/rnn_no_embeddings/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor="val_acc", verbose=1, save_best_only=True, mode="max")
batch_size = 256
epochs = 2
history = rnn_simple_model.fit(x=padded_train_sequences,
y=y_train,
validation_data=(padded_test_sequences, y_test),
batch_size=batch_size,
callbacks=[checkpoint],
epochs=epochs,
verbose=1)
best_rnn_simple_model = load_model("./models/rnn_no_embeddings/weights-improvement-01-0.8262.hdf5")
y_pred_rnn_simple = best_rnn_simple_model.predict(padded_test_sequences, verbose=1, batch_size=2048)
y_pred_rnn_simple = pd.DataFrame(y_pred_rnn_simple, columns=["prediction"])
y_pred_rnn_simple["prediction"] = y_pred_rnn_simple["prediction"].map(lambda p: 1 if p >= 0.5 else 0)
y_pred_rnn_simple.to_csv("./predictions/y_pred_rnn_simple.csv", index=False)
y_pred_rnn_simple = pd.read_csv("./predictions/y_pred_rnn_simple.csv")
print(accuracy_score(y_test, y_pred_rnn_simple))
0.826219183127
準確率達到了 82.6%!這真是很不錯的結果了!現在的模型表現已經比之前的詞袋模型更好了,因為我們將文本的序列性質考慮在內了。
還能做得更好嗎?
5. 用 GloVe 預訓練詞嵌入的循環神經網路
在最後一個模型中,嵌入矩陣被隨機初始化了。那麼如果用預訓練過的詞嵌入對其進行初始化又當如何呢?舉個例子:假設在語料庫中有「pizza」這個詞。遵循之前的架構對其進行初始化後,可以得到一個 300 維的隨機浮點值向量。這當然是很好的。這很好實現,而且這個嵌入可以在訓練過程中進行調整。但你還可以使用在很大的語料庫上訓練出來的另一個模型,為「pizza」生成詞嵌入來代替隨機選擇的向量。這是一種特殊的遷移學習。
使用來自外部嵌入的知識可以提高 RNN 的精度,因為它整合了這個單詞的相關新信息(辭彙和語義),而這些信息是基於大規模數據語料庫訓練和提煉出來的。
我們使用的預訓練嵌入是 GloVe。
官方描述是這樣的:GloVe 是一種獲取單詞向量表徵的無監督學習演算法。該演算法的訓練基於語料庫全局詞-詞共現數據,得到的表徵展示出詞向量空間有趣的線性子結構。
本文使用的 GloVe 嵌入的訓練數據是數據量很大的網路抓取,包括:
- 8400 億個分詞;
- 220 萬詞。
下載壓縮文件要 2.03GB。請注意,該文件無法輕鬆地載入在標準筆記本電腦上。
GloVe 嵌入有 300 維。
GloVe 嵌入來自原始文本數據,在該數據中每一行都包含一個單詞和 300 個浮點數(對應嵌入)。所以首先要將這種結構轉換為 Python 字典。
def get_coefs(word, *arr):
try:
return word, np.asarray(arr, dtype="float32")
except:
return None, None
embeddings_index = dict(get_coefs(*o.strip().split()) for o in tqdm_notebook(open("./embeddings/glove.840B.300d.txt")))
embed_size=300
for k in tqdm_notebook(list(embeddings_index.keys())):
v = embeddings_index[k]
try:
if v.shape != (embed_size, ):
embeddings_index.pop(k)
except:
pass
embeddings_index.pop(None)
一旦創建了嵌入索引,我們就可以提取所有的向量,將其堆疊在一起並計算它們的平均值和標準差。
values = list(embeddings_index.values())
all_embs = np.stack(values)
emb_mean, emb_std = all_embs.mean(), all_embs.std()
現在生成了嵌入矩陣。按照 mean=emb_mean 和 std=emb_std 的正態分布對矩陣進行初始化。遍歷語料庫中的 80000 個單詞。對每一個單詞而言,如果這個單詞存在於 GloVe 中,我們就可以得到這個單詞的嵌入,如果不存在那就略過。
word_index = tokenizer.word_index
nb_words = MAX_NB_WORDS
embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size))
oov = 0
for word, i in tqdm_notebook(word_index.items()):
if i >= MAX_NB_WORDS: continue
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
else:
oov += 1
print(oov)
def get_rnn_model_with_glove_embeddings():
embedding_dim = 300
inp = Input(shape=(MAX_LENGTH, ))
x = Embedding(MAX_NB_WORDS, embedding_dim, weights=[embedding_matrix], input_length=MAX_LENGTH, trainable=True)(inp)
x = SpatialDropout1D(0.3)(x)
x = Bidirectional(GRU(100, return_sequences=True))(x)
avg_pool = GlobalAveragePooling1D()(x)
max_pool = GlobalMaxPooling1D()(x)
conc = concatenate([avg_pool, max_pool])
outp = Dense(1, activation="sigmoid")(conc)
model = Model(inputs=inp, outputs=outp)
model.compile(loss="binary_crossentropy",
optimizer="adam",
metrics=["accuracy"])
return model
rnn_model_with_embeddings = get_rnn_model_with_glove_embeddings()
filepath="./models/rnn_with_embeddings/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor="val_acc", verbose=1, save_best_only=True, mode="max")
batch_size = 256
epochs = 4
history = rnn_model_with_embeddings.fit(x=padded_train_sequences,
y=y_train,
validation_data=(padded_test_sequences, y_test),
batch_size=batch_size,
callbacks=[checkpoint],
epochs=epochs,
verbose=1)
best_rnn_model_with_glove_embeddings = load_model("./models/rnn_with_embeddings/weights-improvement-03-0.8372.hdf5")
y_pred_rnn_with_glove_embeddings = best_rnn_model_with_glove_embeddings.predict(
padded_test_sequences, verbose=1, batch_size=2048)
y_pred_rnn_with_glove_embeddings = pd.DataFrame(y_pred_rnn_with_glove_embeddings, columns=["prediction"])
y_pred_rnn_with_glove_embeddings["prediction"] = y_pred_rnn_with_glove_embeddings["prediction"].map(lambda p:
1 if p >= 0.5 else 0)
y_pred_rnn_with_glove_embeddings.to_csv("./predictions/y_pred_rnn_with_glove_embeddings.csv", index=False)
y_pred_rnn_with_glove_embeddings = pd.read_csv("./predictions/y_pred_rnn_with_glove_embeddings.csv")
print(accuracy_score(y_test, y_pred_rnn_with_glove_embeddings))
0.837203100893
準確率達到了 83.7%!來自外部詞嵌入的遷移學習起了作用!本教程剩餘部分都會在嵌入矩陣中使用 GloVe 嵌入。
6. 多通道卷積神經網路
這一部分實驗了我曾了解過的卷積神經網路結構(http://www.wildml.com/2015/11/understanding-convolutional-neural-networks-for-nlp/)。CNN 常用於計算機視覺任務。但最近我試著將其應用於 NLP 任務,而結果也希望滿滿。
簡要了解一下當在文本數據上使用卷積網路時會發生什麼。為了解釋這一點,我從 wildm.com(一個很好的博客)中找到了這張非常有名的圖(如下所示)。
了解一下使用的例子:I like this movie very much!(7 個分詞)
- 每個單詞的嵌入維度是 5。因此,可以用一個維度為 (7,5 的矩陣表示這句話。你可以將其視為一張「圖」(數字或浮點數的矩陣)。
- 6 個濾波器,大小為 (2, 5) (3, 5) 和 (4, 5) 的濾波器各兩個。這些濾波器應用於該矩陣上,它們的特殊之處在於都不是方矩陣,但它們的寬度和嵌入矩陣的寬度相等。所以每個卷積的結果將是一個列向量。
- 卷積產生的每一列向量都使用了最大池化操作進行下採樣。
- 將最大池化操作的結果連接至將要傳遞給 softmax 函數進行分類的最終向量。
背後的原理是什麼?
檢測到特殊模式會激活每一次卷積的結果。通過改變卷積核的大小和連接它們的輸出,你可以檢測多個尺寸(2 個、3 個或 5 個相鄰單詞)的模式。
模式可以是像是「我討厭」、「非常好」這樣的表達式(詞級的 ngram?),因此 CNN 可以在不考慮其位置的情況下從句子中分辨它們。
def get_cnn_model():
embedding_dim = 300
filter_sizes = [2, 3, 5]
num_filters = 256
drop = 0.3
inputs = Input(shape=(MAX_LENGTH,), dtype="int32")
embedding = Embedding(input_dim=MAX_NB_WORDS,
output_dim=embedding_dim,
weights=[embedding_matrix],
input_length=MAX_LENGTH,
trainable=True)(inputs)
reshape = Reshape((MAX_LENGTH, embedding_dim, 1))(embedding)
conv_0 = Conv2D(num_filters,
kernel_size=(filter_sizes[0], embedding_dim),
padding="valid", kernel_initializer="normal",
activation="relu")(reshape)
conv_1 = Conv2D(num_filters,
kernel_size=(filter_sizes[1], embedding_dim),
padding="valid", kernel_initializer="normal",
activation="relu")(reshape)
conv_2 = Conv2D(num_filters,
kernel_size=(filter_sizes[2], embedding_dim),
padding="valid", kernel_initializer="normal",
activation="relu")(reshape)
maxpool_0 = MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[0] + 1, 1),
strides=(1,1), padding="valid")(conv_0)
maxpool_1 = MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[1] + 1, 1),
strides=(1,1), padding="valid")(conv_1)
maxpool_2 = MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[2] + 1, 1),
strides=(1,1), padding="valid")(conv_2)
concatenated_tensor = Concatenate(axis=1)(
[maxpool_0, maxpool_1, maxpool_2])
flatten = Flatten()(concatenated_tensor)
dropout = Dropout(drop)(flatten)
output = Dense(units=1, activation="sigmoid")(dropout)
model = Model(inputs=inputs, outputs=output)
adam = Adam(lr=1e-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
model.compile(optimizer=adam, loss="binary_crossentropy", metrics=["accuracy"])
return model
cnn_model_multi_channel = get_cnn_model()
plot_model(cnn_model_multi_channel,
to_file="./images/article_5/cnn_model_multi_channel.png",
show_shapes=True,
show_layer_names=True)
filepath="./models/cnn_multi_channel/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor="val_acc", verbose=1, save_best_only=True, mode="max")
batch_size = 256
epochs = 4
history = cnn_model_multi_channel.fit(x=padded_train_sequences,
y=y_train,
validation_data=(padded_test_sequences, y_test),
batch_size=batch_size,
callbacks=[checkpoint],
epochs=epochs,
verbose=1)
best_cnn_model = load_model("./models/cnn_multi_channel/weights-improvement-04-0.8264.hdf5")
y_pred_cnn_multi_channel = best_cnn_model.predict(padded_test_sequences, verbose=1, batch_size=2048)
y_pred_cnn_multi_channel = pd.DataFrame(y_pred_cnn_multi_channel, columns=["prediction"])
y_pred_cnn_multi_channel["prediction"] = y_pred_cnn_multi_channel["prediction"].map(lambda p: 1 if p >= 0.5 else 0)
y_pred_cnn_multi_channel.to_csv("./predictions/y_pred_cnn_multi_channel.csv", index=False)
y_pred_cnn_multi_channel = pd.read_csv("./predictions/y_pred_cnn_multi_channel.csv")
print(accuracy_score(y_test, y_pred_cnn_multi_channel))
0.826409655689
準確率為 82.6%,沒有 RNN 那麼高,但是還是比 BOW 模型要好。也許調整超參數(濾波器的數量和大小)會帶來一些提升?
7. RNN + CNN
RNN 很強大。但有人發現可以通過在循環層上疊加卷積層使網路變得更強大。
這背後的原理在於 RNN 允許嵌入序列和之前單詞的相關信息,CNN 可以使用這些嵌入並從中提取局部特徵。這兩個層一起工作可以稱得上是強強聯合。
更多相關信息請參閱:http://konukoii.com/blog/2018/02/19/twitter-sentiment-analysis-using-combined-lstm-cnn-models/
def get_rnn_cnn_model():
embedding_dim = 300
inp = Input(shape=(MAX_LENGTH, ))
x = Embedding(MAX_NB_WORDS, embedding_dim, weights=[embedding_matrix], input_length=MAX_LENGTH, trainable=True)(inp)
x = SpatialDropout1D(0.3)(x)
x = Bidirectional(GRU(100, return_sequences=True))(x)
x = Conv1D(64, kernel_size = 2, padding = "valid", kernel_initializer = "he_uniform")(x)
avg_pool = GlobalAveragePooling1D()(x)
max_pool = GlobalMaxPooling1D()(x)
conc = concatenate([avg_pool, max_pool])
outp = Dense(1, activation="sigmoid")(conc)
model = Model(inputs=inp, outputs=outp)
model.compile(loss="binary_crossentropy",
optimizer="adam",
metrics=["accuracy"])
return model
rnn_cnn_model = get_rnn_cnn_model()
plot_model(rnn_cnn_model, to_file="./images/article_5/rnn_cnn_model.png", show_shapes=True, show_layer_names=True)
filepath="./models/rnn_cnn/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor="val_acc", verbose=1, save_best_only=True, mode="max")
batch_size = 256
epochs = 4
history = rnn_cnn_model.fit(x=padded_train_sequences,
y=y_train,
validation_data=(padded_test_sequences, y_test),
batch_size=batch_size,
callbacks=[checkpoint],
epochs=epochs,
verbose=1)
best_rnn_cnn_model = load_model("./models/rnn_cnn/weights-improvement-03-0.8379.hdf5")
y_pred_rnn_cnn = best_rnn_cnn_model.predict(padded_test_sequences, verbose=1, batch_size=2048)
y_pred_rnn_cnn = pd.DataFrame(y_pred_rnn_cnn, columns=["prediction"])
y_pred_rnn_cnn["prediction"] = y_pred_rnn_cnn["prediction"].map(lambda p: 1 if p >= 0.5 else 0)
y_pred_rnn_cnn.to_csv("./predictions/y_pred_rnn_cnn.csv", index=False)
y_pred_rnn_cnn = pd.read_csv("./predictions/y_pred_rnn_cnn.csv")
print(accuracy_score(y_test, y_pred_rnn_cnn))
0.837882453033
這樣可得到 83.8% 的準確率,這也是到現在為止最好的結果。
8. 總結
在運行了 7 個不同的模型後,我們對比了一下:
import seaborn as sns
from sklearn.metrics import roc_auc_score
sns.set_style("whitegrid")
sns.set_palette("pastel")
predictions_files = os.listdir("./predictions/")
predictions_dfs = []
for f in predictions_files:
aux = pd.read_csv("./predictions/{0}".format(f))
aux.columns = [f.strip(".csv")]
predictions_dfs.append(aux)
predictions = pd.concat(predictions_dfs, axis=1)
scores = {}
for column in tqdm_notebook(predictions.columns, leave=False):
if column != "y_true":
s = accuracy_score(predictions["y_true"].values, predictions[column].values)
scores[column] = s
scores = pd.DataFrame([scores], index=["accuracy"])
mapping_name = dict(zip(list(scores.columns),
["Char ngram + LR", "(Word + Char ngram) + LR",
"Word ngram + LR", "CNN (multi channel)",
"RNN + CNN", "RNN no embd.", "RNN + GloVe embds."]))
scores = scores.rename(columns=mapping_name)
scores = scores[["Word ngram + LR", "Char ngram + LR", "(Word + Char ngram) + LR",
"RNN no embd.", "RNN + GloVe embds.", "CNN (multi channel)",
"RNN + CNN"]]
scores = scores.T
ax = scores["accuracy"].plot(kind="bar",
figsize=(16, 5),
ylim=(scores.accuracy.min()*0.97, scores.accuracy.max() * 1.01),
color="red",
alpha=0.75,
rot=45,
fontsize=13)
ax.set_title("Comparative accuracy of the different models")
for i in ax.patches:
ax.annotate(str(round(i.get_height(), 3)),
(i.get_x() + 0.1, i.get_height() * 1.002), color="dimgrey", fontsize=14)
我們可以很快地看出在這些模型的預測值之間的關聯。
fig = plt.figure(figsize=(10, 5))
sns.heatmap(predictions.drop("y_true", axis=1).corr(method="kendall"), cmap="Blues", annot=True);
結論
以下是幾條我認為值得與大家分享的發現:
- 使用字元級 ngram 的詞袋模型很有效。不要低估詞袋模型,它計算成本低且易於解釋。
- RNN 很強大。但你也可以用 GloVe 這樣的外部預訓練嵌入套在 RNN 模型上。當然也可以用 word2vec 和 FastText 等其他常見嵌入。
- CNN 也可以應用於文本。CNN 的主要優勢在於訓練速度很快。此外,對 NLP 任務而言,CNN 從文本中提取局部特徵的能力也很有趣。
- RNN 和 CNN 可以堆疊在一起,可以同時利用這兩種結構。
這篇文章很長。希望本文能對大家有所幫助。
※專欄|有趣!用計算機視覺技術與PaddlePaddle打造AI控煙項目
TAG:機器之心 |