原文:Overview and benchmark of traditional and deep learning models in text classification

本文是我在试验Twitter数据情感分析时所写的前一篇文章的扩展。 回到过去,我探索了一个简单的模型:一个在keras上训练的双层前馈神经网络。 输入推文被表示为文档向量,这是由组成推文的单词的嵌入的加权平均值产生的。

我使用的嵌入是一个word2vec模型,我使用gensim从头开始训练语料库。 任务是二进制分类,我能够使用此设置达到79%的准确率。

这篇文章的目标是探索在同一数据集上训练的其他NLP模型,然后在给定的测试集上对它们各自的性能进行基准测试。

我们将通过不同的模型:从依赖于词汇表示的简单模型到部署卷积/循环网络的重型机器:我们将看到我们的准确度是否超过79%!

我将从简单的模型开始,逐步增加复杂性。 目标也在于表明简单模型也能很好地工作。

所以我要试试这些:

  • 用词ngram的逻辑回归
  • 具有字符ngram的逻辑回归
  • 用词和字符ngram的逻辑回归
  • 没有预先训练的嵌入的递归神经网络(双向GRU)
  • 具有GloVe预训练嵌入的递归神经网络(双向GRU)
  • 多通道卷积神经网络
  • RNN(双向GRU)+ CNN模型

到本文结束时,您将获得每种NLP技术的样板代码。 它将帮助您启动您的NLP项目并最终获得最先进的结果(其中一些模型非常强大)。

我们还将提供一个全面的基准,我们将从中了解哪种模型最适合预测推文的情感。

在相关的git repo中,我将发布不同的模型,它们的预测以及测试集。 您可以自己尝试并对结果充满信心

Let's get started!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 - Data pre-processing

可以从此链接下载数据集。

我们将加载它并将自己限制在我们需要的变量(Sentiment和SentimentText)。

它包含1578614个分类推文,每行标记为1表示积极情绪,0表示负面情绪。

作者建议使用1/10来测试算法,其余用于训练。

1
2
3
4
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)
1
(1578614, 2)
1
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! 

推文有很多噪声,让我们通过删除网址,主题标签和用户提及来清理它们。

1
2
3
4
5
6
7
8
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

数据清理完毕后,我们将其保存在磁盘上。

1
2
3
4
5
6
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)
1
(1575026, 2)
1
data.head()
  sentiment cleaned_text
0 0 playing with my routers looks like i might hav...
1 1 sleeeep agh im so tired and they wrote gay on ...
2 0 alan ignored me during the concert boo
3 1 really want some mini eggs why are they only a...
4 0 thanks guys sorry i had to miss your show at m...

现在清理了数据集,让我们准备一个训练/测试分割来构建我们的模型。

我们将在整个笔记本中使用这种分割。

1
2
3
4
5
6
7
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)
1
(1417523,) (157503,) (1417523,) (157503,)

我将测试标签保存在磁盘上供以后使用。

1
pd.DataFrame(y_test).to_csv('./predictions/y_true.csv', index=False, encoding='utf-8')

让我们现在开始应用一些机器学习:

1 - Bag of word model based on word ngrams

所以。 什么是n-gram?

 正如我们在此图中看到的那样,n-gram只是源文本中可以找到的长度为n的相邻单词(在本例中)的所有组合。

在我们的模型中,我们将使用unigrams(n = 1)和bigrams(n = 2)作为特征。

 因此,数据集将表示为矩阵,其中每行对应一条推文,每列对应从文本中提取的特征(unigram或bigram)(在标记化和清理之后)。 每个单元格将是tf-idf分数。 (我们也可以使用简单的计数但是tf-idf通常更常用,通常效果更好)。 我们将此矩阵称为文档术语矩阵。

你可以想象,150万个推文语料库中独特的unigrams和bigrams的数量是巨大的。 实际上,出于计算原因,我们将此数字设置为固定值。 您可以使用交叉验证来确定此值。

这是矢量化后语料库应该是什么样子。

I like pizza a lot

假设我们想使用上述特征将此句子提供给预测模型。

鉴于我们正在使用unigrams和bigrams,该模型将提取以下功能:

i, like, pizza, a, lot, i like, like pizza, pizza a, a lot

因此,句子将由包含大量零的大小为N(=令牌总数)的向量和这些ngram的tf-idf得分组成。 所以你可以清楚地看到我们将处理大而稀疏的向量。

在处理大型和稀疏数据时,线性模型通常表现良好。 此外,它们比其他类型的模型(例如基于树的模型)更快地训练。

从过去的经验我可以看出,逻辑回归在稀疏tf idf矩阵之上运行良好。

1
2
3
4
5
6
7
8
9
10
11
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)

在为训练集和测试集生成tfidf矩阵之后,我们可以构建我们的第一个模型进行测试。

tifidf矩阵是逻辑回归的特征。

1
2
lr_word = LogisticRegression(solver='sag', verbose=2)
lr_word.fit(tfidf_matrix_word_train, y_train)

一旦模型被训练,我们将其应用于测试数据以获得预测。 然后我们将这些值以及模型保存在磁盘上。

1
2
3
4
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)

让我们看看我们得到的准确度分数:

1
2
y_pred_word = pd.read_csv('./predictions/lr_word_ngram.csv')
print(accuracy_score(y_test, y_pred_word))
1
0.782042246814

第一个模型的准确度为78.2%! 还不错。 让我们转到下一个模型。

2 - Bag of word model based on character ngrams

我们从未说过ngram只是用于单词。 我们也可以在角色级别应用它们。

你看到它来了,对吗? 我们将把相同的代码应用于字符ngram,而我们将达到4-grams。

这基本上意味着像“我喜欢这部电影”这样的句子将具有以下特征:

I, l, i, k, e, …, I li, lik, like, …, this, … , is m, s mo, movi, …

字符ngram令人惊讶地非常有效。 在建模语言任务时,它们甚至可以胜过单词标记。 例如,垃圾邮件过滤器或本地语言识别严重依赖于字符ngram。

与之前学习单词组合的模型不同,该模型学习字母组合,可以处理单词的形态构成。

基于字符的表示的一个优点是更好地处理拼写错误的单词。

让我们运行相同的管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
1
2
y_pred_char = pd.read_csv('./predictions/lr_char_ngram.csv')
print(accuracy_score(y_test, y_pred_char))
1
0.80420055491

精度80.4%! 字符图表比word-ngrams表现更好。

3 - Bag of word model based on word and character ngrams

字符ngram特征似乎提供比字ngram更好的准确性。 但是两者的结合怎么样:单词+字符ngrams?

让我们连接我们生成的两个tfidf矩阵并构建一个新的混合tfidf矩阵。

这个模型将帮助我们学习一个单词及其可能的邻居的身份以及它的形态结构。

这些属性结合在一起。

1
2
3
4
5
6
7
8
9
10
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)
1
2
y_pred_word_char = pd.read_csv('./predictions/lr_word_char_ngram.csv')
print(accuracy_score(y_test, y_pred_word_char))
1
0.81423845895

太棒了:81.4%的准确率。 我们只增加了一整个单位,并且超过了之前的两个设置。

在我们继续之前,我们可以对词袋模型说些什么呢?

  • 优点:由于它们的简单性,它们可以令人惊讶地强大,它们训练速度快,易于理解。
  • 缺点:尽管ngrams在单词之间带来了一些上下文,但是单词模型包在模拟序列中单词之间的长期依赖性时失败。

现在我们将深入研究深度学习模型。 深度学习优于词袋模型的原因是能够捕捉句子中单词之间的顺序依赖性。 由于发明了称为回归神经网络的特殊神经网络架构,这是可能的。

我不会介绍RNN的理论基础,但这里有一个值得一读的链接。 它来自Cristopher Olah的博客。 它详细介绍了LSTM:长期短期记忆。 一种特殊的RNN。

在开始之前,我们必须设置一个深度学习专用环境,在Tensorflow之上使用Keras。 老实说,我试图在我的个人笔记本电脑上运行所有东西,但考虑到数据集的重要大小和RNN架构的复杂性,这是不切实际的。 完全没有。

一个很好的选择是AWS。 我通常在EC2 p2.xlarge实例上使用这种深度学习AMI。 Amazon AMI是预先配置的VM映像,其中安装了所有软件包(Tensorflow,PyTocrh,Keras等)。 我强烈推荐这个我已经使用了一段时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 - Recurrent Neural Network without pre-trained embedding

RNN可能看起来很吓人。 虽然它们很难理解,但它们非常有趣。 它们封装了一个非常漂亮的设计,克服了传统神经网络在处理序列数据时出现的缺点:文本,时间序列,视频,DNA序列等。

RNN是一系列神经网络块,它们像链一样彼此链接。 每个人都将消息传递给继任者。

再次,如果你想深入了解内部机制,我强烈推荐Colah的博客,其中包含下图。 

我们将处理文本数据,这是一种序列类型。 单词的顺序对表示非常重要。 希望RNN能够处理这个问题并捕获长期依赖关系。

要在文本数据上使用Keras,我们必须对其进行预处理。 为此,我们可以使用Keras的Tokenizer类。 该对象采用num_words参数作为参数,这是基于字频率进行标记化后保留的最大字数。

1
2
3
4
MAX_NB_WORDS = 80000
tokenizer = Tokenizer(num_words=MAX_NB_WORDS)

tokenizer.fit_on_texts(data['cleaned_text'])

一旦将标记化器安装在数据上,我们就可以使用它将文本字符串转换为数字序列。

这些数字代表字典中每个单词的位置(将其视为映射)。

我们来看一个例子:

1
x_train[15]
1
'breakfast time happy time'

这是令牌器将其转换为数字序列的方式。

1
tokenizer.texts_to_sequences([x_train[15]])
1
[[530, 50, 119, 50]]

现在让我们在训练和测试序列上应用这个标记器:

1
2
train_sequences = tokenizer.texts_to_sequences(x_train)
test_sequences = tokenizer.texts_to_sequences(x_test)

现在推文被映射到整数列表。 但是,由于它们具有不同的长度,我们仍然不能将它们堆叠在一起。 希望Keras允许将序列填充为0到最大长度。 我们将此长度设置为35.(这是推文中令牌的最大数量)。

1
2
3
MAX_LENGTH = 35
padded_train_sequences = pad_sequences( train_sequences, maxlen=MAX_LENGTH)
padded_test_sequences = pad_sequences( test_sequences, maxlen=MAX_LENGTH)
1
padded_train_sequences
1
2
3
4
5
6
7
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]])
1
padded_train_sequences.shape
1
(1417523, 35)

现在数据已准备好送到RNN。

以下是我将使用的架构的一些元素:

  • 嵌入维度为300.这意味着我们将使用的80000中的每个单词都被映射到300维密集向量(浮点数)。映射将在整个训练期间进行调整。
  • 在嵌入层上应用空间丢失以减少过度拟合:它基本上查看35x300矩阵的批次并在每个矩阵中随机丢弃(设置为0)字向量(即行)。这有助于不专注于特定的单词以试图概括。
  • 双向门控循环单元(GRU):这是循环网络部分。它是LSTM架构的更快变体。可以把它想象成两个循环网络的组合,它们在两个方向上扫描文本序列:从左到右,从右到左。这允许网络在读取给定单词时通过使用来自过去和未来信息的上下文来理解它。 GRU将多个单元作为参数,该单元是每个网络块的输出h_t的维度。我们将此数字设置为100.由于我们使用的是GRU的双向版本,因此每个RNN块的最终输出将为200。

 双向GRU的输出具有维度(batch_size,timesteps,units)。 这意味着如果我们使用256的典型批量大小,此维度将为(256,35,200)

  • 在每个批次的顶部,我们应用全局平均池化,其中包括平均对应于每个时间步的输出向量(即单词)
  • 我们对最大池化应用相同的操作。
  • 我们连接前两个操作的输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()

让我们看看这个模型的不同层:

1
2
3
4
plot_model(rnn_simple_model, 
to_file='./images/article_5/rnn_simple_model.png',
show_shapes=True,
show_layer_names=True)

在训练期间,使用模型检查点。 它允许在每个epoch结束时自动保存(在磁盘上)最佳模型(w.r.t精度测量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
1
2
y_pred_rnn_simple = pd.read_csv('./predictions/y_pred_rnn_simple.csv')
print(accuracy_score(y_test, y_pred_rnn_simple))
1
0.826219183127

准确率为82.6%! 还不错! 我们现在比以前的词袋模型表现更好,因为我们考虑了文本的顺序性质。

我们可以做得更好吗?

5 - Recurrent Neural Network with GloVe pre-trained embeddings

在最后一个模型中,嵌入矩阵是随机初始化的。 如果我们可以使用预先训练的单词嵌入来初始化它怎么办?

让我们举一个例子:假设你的语料库中有一个单词pizza。 按照以前的架构,您可以将其初始化为随机浮点值的300维向量。 这很好。 你可以做到这一点,这种嵌入将调整整个训练过程中的进化。 但是,你可以做的而不是随机选择pizza的矢量是使用这个词的嵌入,这个词是从一个非常大的语料库中的另一个模型中学到的。 这是一种特殊的转移学习。

使用外部嵌入的知识可以提高RNN的精确度,因为它集成了关于单词的新信息(词汇和语义),这些信息已经在非常大的数据集上进行了训练和提炼。

我们将使用的预训练嵌入是GloVe

官方文档:GloVe是一种无监督学习算法,用于获取单词的向量表示。 对来自语料库的聚合全局词 - 词共现统计进行训练,并且所得到的表示展示词向量空间的有趣线性子结构。

我将使用的GloVe嵌入式的训练是在一个非常大的常见互联网爬虫中进行的,其中包括:

  • 840 Billion tokens,
  • 2.2 million size vocab

压缩文件是2.03 GB下载。 请注意,此文件无法轻松加载到标准笔记本电脑上。

GloVe嵌入的维度是300。

GloVe嵌入有原始文本数据,每行包含一个单词和300个浮点数(相应的嵌入)。 所以要做的第一件事就是将这个结构转换为python字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

 一旦创建了嵌入索引,我们提取所有向量,我们将它们堆叠在一起并计算它们的均值和标准差。

1
2
3
4
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中,我们选择它的嵌入。

否则,我们pass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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)
1
2
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))
1
0.837203100893

精度为83.7%! 从外部词嵌入转移学习有效! 对于本教程的其余部分,我将在嵌入矩阵中使用GloVe嵌入。

6 - Multi-channel Convolutional Neural Network

在本节中,我正在尝试我在这里阅读的卷积神经网络架构。 CNN通常用于计算机视觉。 但是,我们最近开始将它们应用于NLP任务,结果很有希望。

让我们简要地看一下当我们在文本数据上使用卷积时会发生什么。 为了解释这一点,我从wildm.com(一个非常好的博客)借用这个着名的图表(下面)!

让我们考虑它使用的例子:我非常喜欢这部电影! (7个代币)

  • 每个单词的嵌入维度为5.因此,该句子由维度矩阵(7,5)表示。 您可以将其视为“图像”(〜数字/浮点矩阵)。
  • 6个过滤器,2个尺寸(2,5)(3,5)和(4,5)应用于该矩阵。 这些滤波器的特殊性在于它们不是方形矩阵,其宽度等于嵌入矩阵的宽度。 因此每个卷积的结果将是列向量。
  • 使用最大池化操作对从卷积得到的每个列向量进行二次采样。
  • 最大池化操作的结果在最终向量中连接,该向量被传递给softmax函数以进行分类。

背后的直觉是什么?

当检测到特殊模式时,每个卷积的结果将触发。 通过改变内核的大小并连接它们的输出,您可以自己检测多个大小的模式(2,3或5个相邻的单词)。

模式可以是表达式(单词ngrams?),如“我讨厌”,“非常好”,因此CNN可以在句子中识别它们而不管它们的位置如何。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
1
2
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))
1
0.826409655689

82.6%的准确度,我们不如RNN精确,但仍然优于BOW模型。 也许对超参数(过滤器的数量和大小)的调查给出了优势?

7 - Recurrent + Convolutional neural network

RNN功能强大。 但是,有些人发现通过在回流层顶部添加卷积层可以使它们更加稳健。

理性的背后是RNN允许您嵌入有关序列和先前单词的信息,CNN采用此嵌入并从中提取局部特征。 将这两个层一起工作是一个成功的组合。

更多关于这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
1
2
y_pred_rnn_cnn = pd.read_csv('./predictions/y_pred_rnn_cnn.csv')
print(accuracy_score(y_test, y_pred_rnn_cnn))
1
0.837882453033

准确率为83.8%。 迄今为止最好的模特。

8 - Conclusion

我们运行了七种不同的模型。 让我们看看他们如何比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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)

让我们快速检查模型预测之间的相关性。

1
2
fig = plt.figure(figsize=(10, 5))
sns.heatmap(predictions.drop('y_true', axis=1).corr(method='kendall'), cmap="Blues", annot=True);

Conclusion

以下是我认为值得分享的快速发现:

  • 使用字符ngram的词袋模型可以非常有效。 不要低估他们! 它们的计算成本相对较低,而且易于理解。
  • RNN功能强大。 但是,您有时可以使用GloVe等外部预先训练的嵌入物来泵送它们。 您还可以使用其他流行的嵌入,如word2vec和FastText。
  • CNN可以应用于文本。 他们的主要优势是训练速度非常快。 此外,它们从文本中提取局部特征的能力对于nlp任务特别有意义。
  • RNN和CNN可以堆叠在一起,以利用两种架构的优势。

这篇文章很长,我希望你喜欢它。 如果您有任何问题或建议,请随时发表评论。

Some helpful links to explore

这是我在撰写这篇文章时使用的很好的资源: