將進行以下嘗試:
-
用詞級的 ngram 做 logistic 回歸
-
用字符級的 ngram 做 logistic 回歸
-
用詞級的 ngram 和字符級的 ngram 做 Logistic 回歸
-
在沒有對詞嵌入進行預訓練的情況下訓練循環神經網絡(雙向 GRU)
-
用 GloVe 對詞嵌入進行預訓練,然后訓練循環神經網絡
-
多通道卷積神經網絡
-
RNN(雙向 GRU)+ CNN 模型
數據集下載地址:http://thinknook.com/twitter-sentiment-analysis-training-corpus-dataset-2012-09-22
該數據集包含 1,578,614 個分好類的推文,每一行都用 1(積極情緒)和 0(消極情緒)進行了標記。
作者建議用 1/10 的數據進行測試,其余數據用於訓練。
0. 數據預處理
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
from tqdm import tqdm tqdm.pandas(desc="progress-bar")
train_path="C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\Sentiment Analysis Dataset.csv" data = pd.read_csv(train_path, encoding='latin1', usecols=['Sentiment', 'SentimentText']) data.columns = ['sentiment', 'text']
#隨機抽取若干行,frac為抽取的比例 data = data.sample(frac=1, random_state=42) print(data.shape) for row in data.head(10).iterrows(): print(row[1]['sentiment'], row[1]['text'])

推文數據中存在很多噪聲,我們刪除了推文中的網址、主題標簽和用戶提及來清理數據。
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
將清理好的數據保存在硬盤上:
import nltk
nltk.download('punkt')
data['tokens'] = data.text.progress_map(tokenize) data['cleaned_text'] = data['tokens'].map(lambda tokens: ' '.join(tokens)) data[['sentiment', 'cleaned_text']].to_csv('C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\cleaned_text.csv')
data = pd.read_csv('C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\cleaned_text.csv')
本文數據都是用這種方式分割的。
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'])
將測試集標簽存儲在硬盤上以便后續使用。
pd.DataFrame(y_test).to_csv('C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\y_true.csv', index=False, encoding='utf-8')
接下來就可以應用機器學習方法了。
1. 基於詞級 ngram 的詞袋模型
從過去的經驗可知,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.values.astype('U'))
tfidf_matrix_word_train = vectorizer_word.transform(x_train.values.astype('U'))
tfidf_matrix_word_test = vectorizer_word.transform(x_test.values.astype('U'))
在為訓練集和測試集生成了 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, 'C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\lr_word_ngram.pkl') y_pred_word = lr_word.predict(tfidf_matrix_word_test) pd.DataFrame(y_pred_word, columns=['y_pred']).to_csv('C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\lr_word_ngram.csv', index=False)
得到准確率:
y_pred_word = pd.read_csv('C:\\Users\\Administrator\\Desktop\\nlpdata\\sentiment\\lr_word_ngram.csv') print(accuracy_score(y_test, y_pred_word))
輸出結果:0.7822401844649124
2. 基於字符級 ngram 的詞袋模型
vectorizer_char = TfidfVectorizer(max_features=40000, min_df=5, max_df=0.5, analyzer='char', ngram_range=(1, 4)) vectorizer_char.fit(x_train.values.astype('U')) tfidf_matrix_char_train = vectorizer_char.transform(x_train.values.astype('U')) tfidf_matrix_char_test = vectorizer_char.transform(x_test.values.astype('U')) 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, '/content/drive/My Drive/nlpdata/sentiment/lr_char_ngram.pkl') pd.DataFrame(y_pred_char, columns=['y_pred']).to_csv('/content/drive/My Drive/nlpdata/sentiment/lr_char_ngram.csv', index=False) y_pred_char = pd.read_csv('/content/drive/My Drive/nlpdata/sentiment/lr_char_ngram.csv') print(accuracy_score(y_test, y_pred_char))
輸出結果:0.8045064676742978
3. 基於詞級 ngram 和字符級 ngram 的詞袋模型
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.values.astype('U')) tfidf_matrix_word_train = vectorizer_word.transform(x_train.values.astype('U')) tfidf_matrix_word_test = vectorizer_word.transform(x_test.values.astype('U')) vectorizer_char = TfidfVectorizer(max_features=40000, min_df=5, max_df=0.5, analyzer='char', ngram_range=(1, 4)) vectorizer_char.fit(x_train.values.astype('U')) tfidf_matrix_char_train = vectorizer_char.transform(x_train.values.astype('U')) tfidf_matrix_char_test = vectorizer_char.transform(x_test.values.astype('U')) 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, n_jobs=4) 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, '/content/drive/My Drive/nlpdata/sentiment/lr_word_char_ngram.pkl') pd.DataFrame(y_pred_word_char, columns=['y_pred']).to_csv('/content/drive/My Drive/nlpdata/sentiment/lr_word_char_ngram.csv', index=False) y_pred_word_char = pd.read_csv('/content/drive/My Drive/nlpdata/sentiment/lr_word_char_ngram.csv') print(accuracy_score(y_test, y_pred_word_char))
跑不動。。。。,不過效果會比單獨比char和word的效果要好
關於詞袋模型
-
優點:考慮到其簡單的特性,詞袋模型已經很強大了,它們訓練速度快,且易於理解。
-
缺點:即使 ngram 帶有一些單詞間的語境,但詞袋模型無法建模序列中單詞間的長期依賴關系。
有一個很好的選擇是 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
NN 可能看起來很可怕。盡管它們因為復雜而難以理解,但非常有趣。RNN 模型封裝了一個非常漂亮的設計,以克服傳統神經網絡在處理序列數據(文本、時間序列、視頻、DNA 序列等)時的短板。
RNN 是一系列神經網絡的模塊,它們彼此連接像鎖鏈一樣。每一個都將消息向后傳遞。強烈推薦大家從 Colah 的博客中深入了解它的內部機制,下面的圖就來源於此。

我們要處理的序列類型是文本數據。對意義而言,單詞順序很重要。RNN 考慮到了這一點,它可以捕捉長期依賴關系。
為了在文本數據上使用 Keras,我們首先要對數據進行預處理。可以用 Keras 的 Tokenizer 類。該對象用 num_words 作為參數,num_words 是根據詞頻進行分詞后保留下來的最大詞數。
MAX_NB_WORDS = 80000 tokenizer = Tokenizer(num_words=MAX_NB_WORDS) data['cleaned_text']=data['clean_text'].astype(str) tokenizer.fit_on_texts(data['cleaned_text'])
當分詞器適用於數據時,我們就可以用分詞器將文本字符級 ngram 轉換為數字序列。
這些數字表示每個單詞在字典中的位置(將其視為映射)。如下例所示:
from sklearn.model_selection import train_test_split 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'])
x_train[15]
found closed road w did some fiesta 060 runs 11 seconds need to dyno look power curve move
這里說明了分詞器是如何將其轉換為數字序列的。
tokenizer.texts_to_sequences([x_train[15]])
[[313, 1092, 911, 406, 122, 65, 6623, 41805, 2229, 1011, 2141, 89, 2, 51298, 209, 903, 5443, 595]]
接下來在訓練序列和測試序列中應用該分詞器:
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, ..., 9, 1348, 5], [ 0, 0, 0, ..., 23, 4, 50], [ 0, 0, 0, ..., 6753, 4188, 654], ..., [ 0, 0, 0, ..., 17, 54, 1545], [ 0, 0, 0, ..., 67, 118, 6051], [ 0, 0, 0, ..., 38, 1689, 2774]], dtype=int32)
現在就可以將數據傳入 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='/content/drive/My Drive/nlpdata/sentiment/rnn_simple_model.png', show_shapes=True, show_layer_names=True)

在訓練期間使用了模型檢查點。這樣可以在每個 epoch 的最后將最佳模型(可以用准確率度量)自動存儲(在硬盤上)。
filepath="/content/drive/My Drive/nlpdata/sentiment/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('/content/drive/My Drive/nlpdata/sentiment/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('/content/drive/My Drive/nlpdata/sentiment/y_pred_rnn_simple.csv', index=False) y_pred_rnn_simple = pd.read_csv('/content/drive/My Drive/nlpdata/sentiment/y_pred_rnn_simple.csv') print(accuracy_score(y_test, y_pred_rnn_simple))
輸出結果:0.826219183127
這真是很不錯的結果了!現在的模型表現已經比之前的詞袋模型更好了,因為我們將文本的序列性質考慮在內了。
還能做得更好嗎?
下面的這些就沒有自己去跑了,就直接摘在這了。
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))
准確率達到了 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)
0.837203100893

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)

我們可以很快地看出在這些模型的預測值之間的關聯。

結 論
以下是幾條我認為值得與大家分享的發現:
-
使用字符級 ngram 的詞袋模型很有效。不要低估詞袋模型,它計算成本低且易於解釋。
-
RNN 很強大。但你也可以用 GloVe 這樣的外部預訓練嵌入套在 RNN 模型上。當然也可以用 word2vec 和 FastText 等其他常見嵌入。
-
CNN 也可以應用於文本。CNN 的主要優勢在於訓練速度很快。此外,對 NLP 任務而言,CNN 從文本中提取局部特征的能力也很有趣。
-
RNN 和 CNN 可以堆疊在一起,可以同時利用這兩種結構。
這篇文章很長。希望本文能對大家有所幫助。
原文鏈接:https://ahmedbesbes.com/overview-and-benchmark-of-traditional-and-deep-learning-models-in-text-classification.html
中文:https://mp.weixin.qq.com/s/efcW-Koszh8rtXuVsWtTAg
