将进行以下尝试:
- 用词级的 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 可以堆叠在一起,可以同时利用这两种结构。
这篇文章很长。希望本文能对大家有所帮助。