NLP学习笔记(三):模型训练之深度学习方案详解(关键词:深度学习,词向量,RNN,LSTM)

0. 前言

接上一节NLP学习笔记(二):创建特征及训练(关键词:词袋,TFIDF),我们在预处理完成后,使用简单的词袋模型(CountVectorizer, TfidfVectorizer)来创建了特征,并使用常用的机器学习算法随机森林(RandomForest)、逻辑回归(LogisticReggression)、朴素贝叶斯(NaiveBayes)进行训练,同时使用网格搜索(GridSearchCV)进行调参。这就是我们上次创建的一个思路简单的baseline。

这次我们在预处理的基础上使用深度学习的方法进行训练,关于NLP中常见的深度学习方案,在不同类型的问题中我们可能会使用到CNN,RNN等,网上常见的LSTM/GRU等模型,它们都是RNN的变体(本质上还是RNN)。关于RNN、LSTM的详细说明,可看个人总结:从RNN(内含BPTT以及梯度消失/爆炸)到 LSTM(内含GRU) 注意力机制。

我们可能经常在网上看到Word Embedding这个词,字面翻译是词嵌入,但是我们一般理解是词向量。

1. NLP深度学习模型使用与介绍

网上常用的是keras(TensorFlow as backend),它的接口使用比TensorFlow更加方便,想用Keras使用GPU跑代码的话,博主的情况是之前就预装了GPU版的TensorFlow,所以直接装keras后运行就是使用的GPU,关于其他方案没有了解过。没有GPU的同学用CPU也可以跑,只不过要将下面的CuDNNLSTM换为LSTM。其他算法也是同理。

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, Flatten, Dense, Input, Dropout, BatchNormalization
from keras.layers import CuDNNLSTM, Bidirectional, GlobalMaxPool1D
from keras.models import Sequential, Model
from keras.optimizers import Adam

我们首先进行初始化设置:

vocab_size = 5000
max_len = 300

tokenizer = Tokenizer(num_words = vocab_size)

这里vocab_size和我们之前提到的max_features是同一个东西,都是字典大小,其实经过之前的结果,感觉这个值还可以设置的再大一点也没关系,只不过为了和前面进行比较,这里还是设置成5000。

max_len是为后面做铺垫先设置的一个值,接下来会介绍。 

官方介绍:

Tokenizer是一个用于向量化文本,或将文本转换为序列(即单词在字典中的下标构成的列表,从1算起)的类。

有点抽象,我们接着看。

tokenizer.fit_on_texts(clean_train_reviews)
train_sequences = tokenizer.texts_to_sequences(clean_train_reviews)
test_sequences = tokenizer.texts_to_sequences(clean_test_reviews)

这里的又出现了fit,和之前的fit()本质上是相同的,为什么不在测试集上使用可以参考这篇博文关于sklearn里面的fit transform fit_transform的区别(为什么训练集用fit_transform()而测试集用transform()?)

text_to_sequences方法来了,刚刚不是感觉有点抽象么,我们来看看到底是什么情况。

clean_train_reviews
['stuff going moment mj started listening music watching odd documentary watched wiz watched moonwalker maybe want get certain insight guy thought really cool eighties maybe make mind whether guilty innocent moonwalker part biography part feature film remember going see cinema originally released subtle messages mj feeling towards press also obvious message drugs bad kay visually impressive course michael jackson unless remotely like mj anyway going hate find boring may call mj egotist consenting making movie mj fans would say made fans true really nice actual feature film bit finally starts minutes excluding smooth criminal sequence joe pesci convincing psychopathic powerful drug lord wants mj dead bad beyond mj overheard plans nah joe pesci character ranted wanted people know supplying drugs etc dunno maybe hates mj music lots cool things like mj turning car robot whole speed demon sequence also director must patience saint came filming kiddy bad sequence usually directors hate working one kid let alone whole bunch performing complex dance scene bottom line movie people like mj one level another think people stay away try give wholesome message ironically mj bestest buddy movie girl michael jackson truly one talented people ever grace planet guilty well attention gave subject hmmm well know people different behind closed doors know fact either extremely nice stupid guy one sickest liars hope latter',  ...  ]
train_sequences

[[404, 70, 419, 506, 2456, 115, 54, 873, 516, 178, 178, 165, 78, 14, 662, 2457, 117, 92, 10, 499, 4074, 165, 22, 210, 581, 2333, 1194, 71, 4826, 71, 635, 2, 253, 70, 11, 302, 1663, 486, 1144, 3265, 411, 793, 3342, 17, 441, 600, 1500, 15, 4424, 1851, 998, 146, 342, 1442, 743, 2424, 4, 418, 70, 637, 69, 237, 94, 541, 120, 1, 323, 8, 47, 20, 323, 167, 10, 207, 633, 635, 2, 116, 291, 382, 121, 3315, 1501, 574, 734, 923, 822, 1239, 1408, 360, 221, 15, 576, 2274, 734, 27, 340, 16, 41, 1500, 388, 165, 3962, 115, 627, 499, 79, 4, 1430, 380, 2163, 114, 1919, 2503, 574, 17, 60, 100, 4875, 260, 1268, 15, 574, 493, 744, 637, 631, 3, 394, 164, 446, 114, 615, 3266, 1160, 684, 48, 1175, 224, 1, 16, 4, 3, 507, 62, 25, 16, 640, 133, 231, 95, 600, 3439, 1864, 1, 128, 342, 1442, 247, 3, 865, 16, 42, 1487, 997, 2333, 12, 549, 386, 717, 12, 41, 16, 158, 362, 4392, 3388, 41, 87, 225, 438, 207, 254, 117, 3, 316, 1356], ... ]

这样就可以很清楚的看出来,单词都被转化为了字典中对应的下标了,并且返回一个列表。

train_df_features = pad_sequences(train_sequences, maxlen = max_len)
test_df_features = pad_sequences(test_sequences, maxlen = max_len)

这里就用到了刚刚提到的max_len,这是因为我们假设每个评论有300个词左右(实际可以做个平均值测试,大概250),然后对序列化后的数据,如果它们不够300个,就进行补零操作。如果超出的话,就进行截断。

这样我们原始数据的特征就构造完成了,我们来看一下

train_df_features.shape
train_df_features
(25000, 300)

Out[33]:

array([[   0,    0,    0, ...,    3,  316, 1356],
       [   0,    0,    0, ...,  623, 4628, 1251],
       [   0,    0,    0, ...,  535,  700, 1175],
       ..., 
       [   0,    0,    0, ...,   10,   14,  207],
       [   0,    0,    0, ...,  218, 1903,   16],
       [   0,    0,    0, ..., 1108,  109,  350]])

 接下来我们就开始创建模型了,在Keras中,有比较方便的创建模型的API,通常使用的大概有两种:


一、使用keras.models.Model()

from keras.models import Model

能够比较自由的创建需要的模型,书写格式如下:

from keras.layers import Input, Dense

a = Input(shape=(32,))
b = Dense(32)(a)
model = Model(inputs=a, outputs=b)

最后编译一下

model.compile(optimizer, loss=None, metrics=None, loss_weights=None, sample_weight_mode=None, weighted_metrics=None, target_tensors=None)

这里没有设定参数,读者可根据自己需要进行设置。

二、使用keras.models.Sequential()

在NLP中,这也是常用的一种建模方式,从它的名字就可看出适合于建立时序模型。

常用表达:

from keras.models import Sequential
from keras.layers import Dense, Activation

model = Sequential([
    Dense(32, input_shape=(784,)),
    Activation('relu'),
    Dense(10),
    Activation('softmax'),
])

或者使用add():

from keras.models import Sequential
from keras.layers import Dense, Activation

model = Sequential()

model.add(Dense(32, input_dim=784))
model.add(Activation('relu'))

最后同样也是编译

# 针对多分类问题
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# 针对二分类问题
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

# 针对均方差回归问题
model.compile(optimizer='rmsprop',
              loss='mse')

好,题外话不多说了,我们开始创建我们的深度学习模型,我们就使用Model()来创建好了

 

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, Flatten, Dense, Input, Dropout, BatchNormalization
from keras.layers import CuDNNLSTM, Bidirectional, GlobalMaxPool1D
from keras.models import Sequential, Model
from keras.optimizers import Adam

inp = Input(shape = (max_len, ))
x = Embedding(input_dim = vocab_size, output_dim = embed_size, input_length = max_len)(inp)
x = Bidirectional(CuDNNLSTM(32, return_sequences = True))(x)
x = GlobalMaxPool1D()(x)
x = Dense(16, activation = 'relu')(x)
x = Dropout(0.05)(x)
x = Dense(1, activation = "sigmoid")(x)

model = Model(inputs = inp, outputs = x)
model.compile(loss = 'binary_crossentropy', optimizer = 'adam', metrics = ['accuracy'] )

一句一句来分析,首先是Input(),里面的shape指的是每次输入的序列长度,我们之前将每个评论进行规整成了max_len这么长,而我们每次输入一个评论进入模型,所以shape = (max_len, )

第二句Embedding()层,这一层就是词向量化层,其中input_dim必须是我们设置的字典的大小(也可以说是特征的数量),output_dim是我们向量化后的词向量维度。关于词向量,网上有很多资料但是很乱,国内比较好的资料推荐来斯惟的博士论文基于神经网络的词和文档语义向量表示方法研究,国外的资料推荐XinRongword2vec Parameter Learning Explainedinput_length依然是我们输入的序列长度,所以还是刚刚的max_len。

第三句Bidirectional(CuDNNLSTM())首先CuDNNLSTM简单说来就是LSTM的GPU版本,如果没有GPU版本的话将其换为LSTM就可以了(记住import也要改)。加上Bidirectional就是双向LSTM,想详细了解可以查找论文,这里就不多解释了。32这个参数是units,指的是输出的output维度。return_sequences是否将上一个输出返回,一般我们是选择True,毕竟RNN的特性需要体现嘛,信息“循环”利用。

第四句GlobalMaxPool1D()顾名思义就是最大池化层。

第五句Dense()就是全连接层。

第六句Dropout()就是在输入任意drop掉那么多比例的数据,作用是预防过拟合。

肯定会有读者在想,为啥是这个顺序的建模,为啥用那些activation呢?在这里博主能力不足,只能这么解释,前人尝试过很多方案,这种方案应该算是效果比较好的。

最后记住编译一下 model.compile()。

好了,我们打印下看看参数的维度

print(model.summary())
Layer (type)                 Output Shape              Param #   
=================================================================
input_4 (InputLayer)         (None, 300)               0         
_________________________________________________________________
embedding_3 (Embedding)      (None, 300, 200)          1000000   
_________________________________________________________________
bidirectional_3 (Bidirection (None, 300, 64)           59904     
_________________________________________________________________
global_max_pooling1d_2 (Glob (None, 64)                0         
_________________________________________________________________
dense_3 (Dense)              (None, 16)                1040      
_________________________________________________________________
dropout_2 (Dropout)          (None, 16)                0         
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 17        
=================================================================
Total params: 1,060,961
Trainable params: 1,060,961
Non-trainable params: 0

上面的None就是batch_size啦,是我们每次输入的训练数据的数量。

embedding那里的1000000是我们需要训练的词向量矩阵包含的参数个数,其维度为5000x200。别忘了5000是我们设置的字典的大小。

bidirectional那里的64,是因为我们设置的LSTM的输出维度为32,但是双向之后就需要乘以2了。

这里双向LSTM的参数个数怎么计算的呢,LSTM的参数个数由4 * [(x_dim + y_dim) * y_dim + y_dim]计算

NLP学习笔记(三):模型训练之深度学习方案详解(关键词:深度学习,词向量,RNN,LSTM)_第1张图片

可以上图中间那个celll里有四个小黄框,里面写的是激活函数。其实每个都相当于一个MLP,我们可以拆解开看。 

 

NLP学习笔记(三):模型训练之深度学习方案详解(关键词:深度学习,词向量,RNN,LSTM)_第2张图片

进入第一个sigmoid前的h(t - 1)与Xt,这两个向量进行concatenate,就是直接相连(比如一个维度为(10, )的向量连上一个维度(20, )的向量,就得到一个维度(30, )的向量),Xt对应的维度为x_dim,而h(t - 1)对应的维度为y_dim,我们希望这个“MLP”输出的ft维度为y_dim,所以中间的矩阵Wf维度即为(x_dim + y_dim) *y_dim,再算上bias后,即为(x_dim + y_dim) *y_dim + y_dim了,而我们一共有四个小黄框,所以将这个值乘以4就是参数的数量了。

这里x_dim为输入的向量维度,这里是200,y_dim为输出的向量维度,为32,最后双向再上面公式乘以2就得到了59904。但是一般我们不用算这么精确,只需要知道一个大概数量级就可以了,比如可以简化成4*x_dim*y_dim(未使用双向时)。

全连接层的参数个数,也就是相当于要知道矩阵维度,我们输入64,输出16,那肯定这个矩阵就是(64,16)嘛,这样就1024个参数了,最后还有16个bios,加起来一共1040个。

可以看出,我们整个模型的主体是训练我们的Embedding层,也就是词向量层,它的参数数量占了我们模型总体参数量的绝大多数。

现在我们开始训练模型

model.fit(train_df_features, train_df['sentiment'], batch_size = 32, epochs = 2, validation_split = 0.1)

模型训练完毕后,我们对测试集进行预测

pred_y_prob = model.predict(test_df_features)

pred_y_prob
array([[ 0.99660575],
       [ 0.02011632],
       [ 0.93720984],
       ..., 
       [ 0.05806568],
       [ 0.99249506],
       [ 0.89172608]], dtype=float32)

得到的是一组概率,我们需要找到一个最佳阈值来划分在哪个范围为正项,哪个范围为负项,现在就需要使用到验证集来帮助我们找到最佳阈值了。

from sklearn import metrics
from sklearn.model_selection import train_test_split


train_X, val_X, train_y, val_y = train_test_split(train_df_features, train_df['sentiment'], test_size = 0.2, random_state = 666)

pred_val_y_prob = model.predict([val_X], batch_size = 64)
for thresh in np.arange(0.3, 0.701, 0.01):
    thresh = np.round(thresh, 2)
    print("F1 score at threshold {0} is {1}".format(thresh, metrics.f1_score(val_y, (pred_val_y_prob > thresh).astype(int).reshape(pred_val_y.shape[0]))))

 找到最佳阈值后,我们就可以整合和提交啦

pred_y = (pred_y_prob > your_best_threshold).astype(int).reshape(25000, )

output = pd.DataFrame( data={"id":test_df["id"], "sentiment":pred_y} )

# Use pandas to write the comma-separated output file
output.to_csv( "/Bags_of_Words_Meets_Bags_of_Popcorn/Bag_of_Words_model_lstm.csv", index=False, quoting=3 )

稍显遗憾,只有0.87左右,不过也比我们之前0.84好了一些,不过这次我们没有用到GridSearchCv来调参,也许好好调调参能上去不少呢,我是这么感觉的,毕竟咱们的字典也才用了5000,还有深度学习的那些参数,我们都可以调整,感觉上升空间还挺大的~

最后提一句,我们经常可以看见类似这种预训练的word embedding

这都是别人通过特定的语料训练好的词向量,在针对相似环境的问题也许能比我们自己训练词向量发挥更好的作用。

关于如何使用的话可以参考一下以下代码:

EMBEDDING_FILE = '../input/embeddings/glove.840B.300d/glove.840B.300d.txt'

def get_coefs(word,*arr): 
    return word, np.asarray(arr, dtype='float32')

embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(EMBEDDING_FILE))

all_embs = np.stack(embeddings_index.values())
emb_mean,emb_std = all_embs.mean(), all_embs.std()
embed_size = all_embs.shape[1]

word_index = tokenizer.word_index
nb_words = min(max_features, len(word_index))
embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size))

for word, i in word_index.items():
    if i >= max_features: continue
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None: embedding_matrix[i] = embedding_vector

其中embedding_matrix就是别人预训练好的词向量矩阵,因为本身语料比较大,而我们需要把这些全部放入内存,所以建议运行的同学内存至少8G以上。

我们将词向量矩阵直接放入模型就可以开始训练我们的数据了

inp = Input(shape=(maxlen,))
x = Embedding(max_features, embed_size, weights=[embedding_matrix])(inp)
x = Bidirectional(CuDNNLSTM(64, return_sequences=True))(x)
x = GlobalMaxPool1D()(x)
x = Dense(16, activation="relu")(x)
x = Dropout(0.1)(x)
x = Dense(1, activation="sigmoid")(x)

model = Model(inputs=inp, outputs=x)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
model.fit(train_df_features, train_df['sentiment'], test_size = 0.2, random_state = 666)

后面的操作和我们前面是一致的。

你可能感兴趣的:(自然语言处理,深度学习)