CNN也能用於NLP任務,一文簡述文本分類任務的7個模型

選自Ahmed BESBES,做者:Ahmed Besbes,機器之心編譯。html

本文介紹了用於文本分類任務的 7 個模型,包括傳統的詞袋模型、循環神經網絡,也有經常使用於計算機視覺任務的卷積神經網絡,以及 RNN + CNN。

本文是我以前寫過的一篇基於推特數據進行情感分析的文章(ahmedbesbes.com/sentiment-a…)的延伸內容。那時我創建了一個簡單的模型:基於 keras 訓練的兩層前饋神經網絡。用組成推文的詞嵌入的加權平均值做爲文檔向量來表示輸入推文。git

我用的嵌入是用 gensim 基於語料庫從頭訓練出來的 word2vec 模型。該是一個二分類任務,準確率能達到 79%。github

本文目標在於探索其餘在相同數據集上訓練出來的 NLP 模型,而後在給定的測試集上對這些模型的性能進行評估。web

咱們將經過不一樣的模型(從依賴於詞袋錶徵的簡單模型到部署了卷積/循環網絡的複雜模型)瞭解可否獲得高於 79% 的準確率!算法

首先,將從簡單的模型開始,逐步增長模型的複雜度。這項工做是爲了說明簡單的模型也能頗有效。bash

我會進行這些嘗試:網絡

  • 用詞級的 ngram 作 logistic 迴歸
  • 用字符級的 ngram 作 logistic 迴歸
  • 用詞級的 ngram 和字符級的 ngram 作 Logistic 迴歸
  • 在沒有對詞嵌入進行預訓練的狀況下訓練循環神經網絡(雙向 GRU)
  • 用 GloVe 對詞嵌入進行預訓練,而後訓練循環神經網絡
  • 多通道卷積神經網絡
  • RNN(雙向 GRU)+ CNN 模型

文末附有這些 NLP 技術的樣板代碼。這些代碼能夠幫助你開啓本身的 NLP 項目並得到最優結果(這些模型中有一些很是強大)。架構

咱們還能夠提供一個綜合基準,咱們能夠利用該基準分辨哪一個模型最適合預測推文中的情緒。app

在相關的 GitHub 庫中還有不一樣的模型、這些模型的預測結果以及測試集。你能夠本身嘗試並獲得可信的結果。dom

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. 數據預處理

你能夠從該連接(thinknook.com/twitter-sen…)下載數據集。

加載數據並提取所需變量(情感及情感文本)。

該數據集包含 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'http\S+', '', tweet)
    tweet = re.sub(r"#(\w+)", '', tweet)
    tweet = re.sub(r"@(\w+)", '', tweet)
    tweet = re.sub(r'[^\w\s]', '', 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 的理論基礎,但該連接(colah.github.io/posts/2015-…)中的內容值得一讀。這篇文章來源於 Cristopher Olah 的博客,詳細敘述了一種特殊的 RNN 模型:長短時間記憶網絡(LSTM)。

在開始以前,要先設置一個深度學習專用的環境,以便在 TensorFlow 上使用 Keras。誠實地講,我試着在我的筆記本上運行這些代碼,但考慮到數據集的大小和 RNN 架構的複雜程度,這是很不實際的。還有一個很好的選擇是 AWS。我通常在 EC2 p2.xlarge 實例上用深度學習 AMI(aws.amazon.com/marketplace…)。亞馬遜 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. 多通道卷積神經網絡

這一部分實驗了我曾瞭解過的卷積神經網絡結構(www.wildml.com/2015/11/und…)。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 可使用這些嵌入並從中提取局部特徵。這兩個層一塊兒工做能夠稱得上是強強聯合。

更多相關信息請參閱:konukoii.com/blog/2018/0…

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 能夠堆疊在一塊兒,能夠同時利用這兩種結構。

這篇文章很長。但願本文能對你們有所幫助。

原文連接:ahmedbesbes.com/overview-an…

相關文章
相關標籤/搜索