?? 摘要:本文主要是用于學(xué)習(xí)。從實踐中出發(fā),利用TensorFlow解決NLP中的分類問題,主要包括多分類、多標(biāo)簽分類問題。我打算學(xué)習(xí)深度學(xué)習(xí)中的不同算法進行探討研究,主要包括CNN、LSTM、Fasttext、seq2seq等一系列算法,在實際應(yīng)用中的一些問題及track。這是本系列的第一篇文章,主要介紹了CNN卷積神經(jīng)網(wǎng)絡(luò)的原理,以及使用Tensorflow實現(xiàn)CNN文本分類的編碼實現(xiàn)。
CNN卷積神經(jīng)網(wǎng)絡(luò)簡介
?? 神經(jīng)網(wǎng)絡(luò)Neural Networks方面的研究在國外是從很早很早就已經(jīng)開始發(fā)展了,從最開始的淺層神經(jīng)網(wǎng)絡(luò)ANN算法研究,到BP反饋神經(jīng)網(wǎng)絡(luò)的發(fā)展,以及現(xiàn)在非?;鸬纳疃葘W(xué)習(xí)神經(jīng)網(wǎng)絡(luò)。都是想要對單個神經(jīng)元進行建模,模擬人腦神經(jīng)網(wǎng)絡(luò)系統(tǒng)的功能,從而構(gòu)建一個網(wǎng)絡(luò),具有學(xué)習(xí)的能力。BP神經(jīng)網(wǎng)絡(luò),深度學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)都是遵循一個最基本的特點:前向傳播數(shù)據(jù)信號,反向傳播誤差值。從而讓神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)擬合到一個較好的狀態(tài)。
- 卷積層
?? 對于卷積層,其中核心即是通過卷積核(filter)計算提取特征feature map。對于卷積操作而言,通過每一個filter和input卷積計算,則會得到一個新的二維數(shù)組,可以理解為對原始輸入進行特征提取,一般表示為feature map,filter的個數(shù)影響feature map的數(shù)量,卷積層的輸出維度則和卷積核以及輸入相關(guān),一般計算卷積輸出有公式如下,其中W:輸入;F:卷積核;P:0填充;S:步長。

?? 在整個計算卷積計算的過程中,主要有兩個重要的特性:1.局部連通 2.參數(shù)共享
?? 1. 局部連通:CNN和全連接神經(jīng)網(wǎng)絡(luò)不一樣的地方在于,CNN的神經(jīng)元和下一層的神經(jīng)元并不是全連接的,而是通過不同的卷積核(filter),針對樣本某一局部的數(shù)據(jù)窗口進行卷積計算,卷積核就好比學(xué)習(xí)對象,不同的卷積核對同樣的輸入樣本進行學(xué)習(xí)、特征提取,最好的即是每一個卷積核都提取到樣本不同的特征,比如對于圖片而言,不同的filter可能提取圖片的顏色深淺,輪廓等特征,每個filter都是對輸入數(shù)據(jù)的局部進行計算處理,這就是CNN的局部連通特點。
?? 2. 參數(shù)共享:對于同一個filter而言,可以當(dāng)成是提取特征的容器,用相同的filter提取特征,而與樣本的輸入位置無關(guān),表示在輸入樣本的所有區(qū)域,都能夠使用相同的學(xué)習(xí)特征,雖然樣本局部的數(shù)據(jù)在變化,可是每一個filter的權(quán)重是固定不變的,這個即是CNN的參數(shù)共享特點,它降低了整個網(wǎng)絡(luò)的復(fù)雜性。
- 池化層
?? 在CNN網(wǎng)絡(luò)中另外一個非常重要的操作則是池化操作,池化可以將一幅大的圖像縮小,同時又保留其中的重要信息。 它就是將輸入圖像進行縮小,減少像素信息,只保留重要信息。通常情況下,池化都是22大小,比如對于max-pooling來說,就是取輸入圖像中22大小的塊中最大值,作為結(jié)果的像素值,相當(dāng)于將原始圖像縮小了4倍。(注:同理,對于average-pooling來說,就是取2*2大小塊的平均值作為結(jié)果的像素值。)因為最大池化(max-pooling)保留了每一個小塊內(nèi)的最大值,所以它相當(dāng)于保留了這一塊最佳的匹配結(jié)果(因為值越接近1表示匹配越好)。這也就意味著它不會具體關(guān)注窗口內(nèi)到底是哪一個地方匹配了,
Tensorflow簡介
?? 對于Tensorflow,我想現(xiàn)在大部分的研究人員都不陌生,可是為什么Tensorflow在整個深度學(xué)習(xí)領(lǐng)域會變得如此的流行?毫無疑問的是,因為Tensorflow的流行讓深度學(xué)習(xí)的門檻變得越來越低了,只要有python和機器學(xué)習(xí)基礎(chǔ),使得實現(xiàn)以及應(yīng)用神經(jīng)網(wǎng)絡(luò)模型變得非常簡單易上手。以前要編碼實現(xiàn)一個神經(jīng)網(wǎng)絡(luò),需要對前饋傳播,反向傳播有很深刻的理解,并需要花費一定的時間進行編碼開發(fā),所以Tensorflow的出現(xiàn),大大降低了深度神經(jīng)網(wǎng)絡(luò)的開發(fā)成本和開發(fā)難度,使得算法研究人員能夠快速驗證算法的適用性,能夠?qū)⒏嗟木Ψ诺骄W(wǎng)絡(luò)的設(shè)計以及性能的調(diào)優(yōu)。Tensorflow是很強大的,它支持Python和C++,也可以使用CPU和GPU的分布式計算,除了Tensorflow,現(xiàn)在也有很多封裝更高層的庫支持進行深度神經(jīng)網(wǎng)絡(luò)計算,如Theano,Keras,Torch,MXnet等,其中Keras使用是非常簡單的,他支持Theano,Tensorflow,只需要幾行代碼就可以構(gòu)建一個神經(jīng)網(wǎng)絡(luò)。大家感興趣的話,可以了解并學(xué)習(xí)比較一下Keras和Tensorflow實現(xiàn)相同的功能時需要的代碼。
?? 關(guān)于Tensorflow,還有一個比較有意思的是可視化功能,一方面是我們可以直接看到自己設(shè)計的網(wǎng)絡(luò)結(jié)構(gòu);另一方面,我們可以將我們想要關(guān)注的參數(shù)通過圖表的形式記錄下來,通過關(guān)注圖表變化趨勢,可以更方便后期我們對模型的調(diào)優(yōu)與測試。如下所示為使用tensorboard繪制的圖像。


Word2Vec簡介
?? 因為在自然語言處理(NLP)任務(wù)中,使用神經(jīng)網(wǎng)絡(luò)進行計算的時候往往都需要將文本信息用矩陣的形式表達,所以作為NLP中一個非常重要的工具Word2Vec,通過word2vec對數(shù)據(jù)進行訓(xùn)練得到的結(jié)果——詞向量(word wmbedding)即可很好的完成這樣一個工作。其實word2vec只是一個工具,在其背后其實主要是指Cbow模型和Skip-gram模型的一個淺層神經(jīng)網(wǎng)絡(luò),通過該模型可以在大量的數(shù)據(jù)集上進行訓(xùn)練,最終得到詞向量。通過python使用gensim庫可以很方便的進行word2vec的訓(xùn)練。
?? 詞向量有什么用?通過向量表示詞語,這使得在處理很多NLP任務(wù)的時候,變得更為方便,如計算詞語的相似性,可以直接對詞向量進行tf-idf進行計算處理,通過詞向量來度量詞與詞之間的相似性;比如在訓(xùn)練神經(jīng)網(wǎng)絡(luò)的時候,可以將詞向量作為網(wǎng)絡(luò)輸入。
Tensorflow實現(xiàn)CNN文本分類
?? 本文主要是想要在實踐中學(xué)習(xí)Tensorflow及一些基本的文本分類算法,CNN在計算機視覺領(lǐng)域取得了很好的結(jié)果,其實在NLP分類任務(wù)中,CNN也是具有很好的效果。因為CNN是有監(jiān)督學(xué)習(xí)算法,所以想要基于CNN進行文本分類,主要包含數(shù)據(jù)預(yù)處理、網(wǎng)絡(luò)模型構(gòu)建。
- 進行數(shù)據(jù)預(yù)處理
?? 如果想要一個神經(jīng)網(wǎng)絡(luò)有比較好的效果,數(shù)據(jù)占據(jù)著非常重要的地位,訓(xùn)練集的數(shù)量以及訓(xùn)練集的質(zhì)量都是非常重要的因素。為了能夠在訓(xùn)練過程中直觀的看到訓(xùn)練效果,我們可以將數(shù)據(jù)集拆分為訓(xùn)練集以及驗證集,用于在訓(xùn)練過程中對數(shù)據(jù)進行校驗。 - 網(wǎng)絡(luò)模型構(gòu)建
?? 前面在描述了CNN的基本原理外,我們也清楚NLP中往往是將文本矩陣化表示,所以我們需要清楚,在使用一個矩陣表示文本的時候,矩陣中的每一行都對應(yīng)于一個元素,一般是一個詞語,即表明矩陣中的每一行都是一個元素的向量化表示。想要構(gòu)建一個有效的CNN網(wǎng)絡(luò),其中主要需要控制詞向量大小,以及進行卷積計算的卷積核大小及卷積核個數(shù)。
?? 我覺得下面這個圖很形象的描述了CNN網(wǎng)絡(luò)在NLP中的應(yīng)用,下面可以可以從左往右分析一下整個算法的應(yīng)用過程:
- 第1層表示為輸入層,輸入為一個短句,每個字通過word2vec來表示為一個行向量,表示為一個二維矩陣(實際使用卷積層應(yīng)該為4維矩陣);
- 第2層我們可以看到設(shè)置了3種不同尺寸的卷積核,表示為filter_size:[2,3,4],其中表示每個卷積核進行卷積計算獲取特征的詞個數(shù)
(相鄰2個字,相鄰3個字,相鄰4個字),其中每種尺寸卷積核的個數(shù)為2,所以在第二層中一共包含6個卷積核,3種卷積核,每種卷積核的列數(shù)和輸入數(shù)據(jù)為列數(shù)一致。 - 將第2層中的卷積核和第一層中的矩陣輸入進行卷積計算,獲取到的結(jié)果即為第3層,即生成6個feature map
- 第3層中每個卷積核生成的feature map進行max pool最大池化,即取得所有輸出的最大值,進行concat聚合操作,這樣即得到CNN抽取出的所有特征。
- 4層結(jié)果和最后一層輸出層進行全連接操作,輸出個數(shù)根據(jù)實際應(yīng)用進行設(shè)置。
這樣,就完成了一個CNN文本分類的操作,具體損失函數(shù),優(yōu)化函數(shù)的計算,都是可以直接通過tensorflow調(diào)用。

?? 接下來,我們從編碼實現(xiàn)的角度談一下應(yīng)該怎么從0開始構(gòu)建一個cnn神經(jīng)網(wǎng)絡(luò)用語文本分類。了解Tensorflow的小伙伴應(yīng)該清楚,使用Tensorflow的時候,是需要我們先定義把整個網(wǎng)絡(luò)結(jié)構(gòu),其中包括通過占位符placeholder為待訓(xùn)練數(shù)據(jù)占坑,使用Variable或get_variable設(shè)置訓(xùn)練過程中所需要的一些變量,如下表示:
?? 定義輸入變量、網(wǎng)絡(luò)訓(xùn)練過程中的一些參數(shù):
print('定義占位符,輸入輸出變量.....')
self.input_x = tf.placeholder(tf.int32, [None, self.sequence_length], name="input_x")
self.input_y = tf.placeholder(tf.int32, [None,], name="input_y")
self.global_step = tf.Variable(0, trainable=False, name="global_step")
self.epoch_step = tf.Variable(0, trainable=False, name="epoch_step")
self.initializer = tf.random_normal_initializer(stddev=0.1)
?? 定義Embedding,全連接層的權(quán)重W以及偏置b
def instantiate_weights(self):
'''
初始化網(wǎng)絡(luò)參數(shù)
Args:
Embedding:[self.vocab_size, self.embed_size]
W_projection:[self.num_filter_total, self.num_classes]
b_projection:[self.num_classes]
'''
with tf.name_scope("Variables"):
with tf.name_scope("Embedding"):
self.Embedding = tf.get_variable("Embedding",
shape=[self.vocab_size, self.embed_size], initializer=self.initializer)
with tf.name_scope("W_projection"): #計算輸出層的參數(shù)
self.W_projection = tf.get_variable("W_projection",
shape=[self.num_filters_total, self.num_classes], initializer=self.initializer)
variable_summaries(self.W_projection)
with tf.name_scope("b_projection"):
self.b_projection = tf.get_variable("b_projection",
shape=[self.num_classes])
variable_summaries(self.b_projection)
?? 接下來當(dāng)然到了最核心的部分,應(yīng)該怎么設(shè)計網(wǎng)絡(luò)結(jié)構(gòu),使得我們的模型能夠發(fā)揮它最大的作用呢?其實這個問題我也不太清楚,不過基本的都是Conv2d->activation(可以省略)->Pool,其實主要還是根據(jù)效果,慢慢的對網(wǎng)絡(luò)進行調(diào)整。
def inference(self):
'''
構(gòu)建網(wǎng)絡(luò)結(jié)構(gòu)
Args:
Conv.Input:[filter_height, filter_width, in_channels, out_channels]
Conv.Returns:[batch_size,sequence_length-filter_size+1,1,num_filters]
input_data:NHWC:[batch, height, width, channels]
pool.Input:[batch, height, width, channels]
Returns:
網(wǎng)絡(luò)結(jié)構(gòu)每次訓(xùn)練返回的結(jié)果:[batch_size, self.num_classes]
'''
with tf.name_scope("Layer_Embedding"):
#[None, sentence_length, embed_size]
self.embedded_words = tf.nn.embedding_lookup(self.Embedding, self.input_x)
self.sentence_embeddings_expanded = tf.expand_dims(self.embedded_words, -1,
name="embedding_word") #[None, sentence_length, embed_size, 1]
pooled_outputs = []
with tf.name_scope("Conv2d"):
for i, filter_size in enumerate(self.filter_sizes):
with tf.name_scope("convolution-%s" %filter_size):
filter = tf.get_variable("filter-%s"%filter_size,
[filter_size,self.embed_size,1,self.num_filters], initializer=self.initializer)
#[batch_size, self.sequence_size-filter_size, 1, 1]
conv = tf.nn.conv2d(self.sentence_embeddings_expanded, filter,
strides=[1,1,1,1], padding="VALID", name="conv")
with tf.name_scope("relu-%s"%filter_size):
b = tf.get_variable("b-%s"%filter_size, [self.num_filters])
h = tf.nn.relu(tf.nn.bias_add(conv, b), "relu")
with tf.name_scope("pool-%s"%filter_size):
pooled = tf.nn.max_pool(h, ksize=[1,self.sequence_length-filter_size+1,1,1],
strides=[1,1,1,1], padding="VALID", name="pool")
pooled_outputs.append(pooled)
with tf.name_scope("Pool_Flat"):
self.h_pool = tf.concat(pooled_outputs,3) #[batch_size, 1, 1, num_filters_total]
self.h_pool_flat = tf.reshape(self.h_pool, [-1,self.num_filters_total])
if self.is_dropout:
print('需要dropout操作')
with tf.name_scope("DropOut"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, keep_prob=self.dropout_keep_prob)
with tf.name_scope("Output"):
#tf.matmul([None,self.embed_size],[self.embed_size,self.num_classes])
logits = tf.matmul(self.h_pool_flat, self.W_projection) + self.b_projection #[None, self.num_classes]
return logits
?? 熟悉神經(jīng)網(wǎng)絡(luò)的小伙伴應(yīng)該清楚,在訓(xùn)練網(wǎng)絡(luò)的時候,其實只有神經(jīng)網(wǎng)絡(luò)的輸出是不行的,最重要的是應(yīng)該根據(jù)每一次的輸出結(jié)果和標(biāo)準(zhǔn)輸出
進行損失值計算,再根據(jù)梯度下降算法逐步的調(diào)整網(wǎng)絡(luò)參數(shù),讓整個網(wǎng)絡(luò)更好的擬合數(shù)據(jù)。
?? 如下為計算loss的過程:
def loss(self, l2_lambda=0.0001):
'''
根據(jù)每次訓(xùn)練的預(yù)測結(jié)果和標(biāo)準(zhǔn)結(jié)果比較,計算誤差
loss = loss + l2_lambda*1/2*||variables||2
Args:
l2_lambda:超參數(shù),l2正則,保證l2_loss和train_loss在同一量級
Returns:
每次訓(xùn)練的損失值loss
'''
with tf.name_scope("Loss"):
losses = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=self.input_y, logits=self.logits)
loss = tf.reduce_mean(losses)
if self.is_l2:
print("需要對loss進行l(wèi)2正則化")
l2_losses = tf.add_n([tf.nn.l2_loss(v) for v in tf.trainable_variables() if 'bias' not in v.name]) * l2_lambda
loss = loss + l2_losses
variable_summaries(loss)
return loss
如下為梯度下降:
def train(self):
'''
通過梯度下降最小化損失loss的操作
Args:
Returns:
返回包含了訓(xùn)練操作(train_op)輸出結(jié)果的tensor
'''
if self.is_decay:
print("需要對學(xué)習(xí)率進行指數(shù)衰減")
with tf.name_scope("LearningRate"):
#學(xué)習(xí)率指數(shù)衰減 learning_rate=learning_rate*decay_rate^(global_step/decay_steps)
learning_rate = tf.train.exponential_decay(self.learning_rate, self.global_step,
self.decay_steps, self.decay_rate, staircase=True)
with tf.name_scope("Train"):
optimizer = tf.train.GradientDescentOptimizer(self.learning_rate)
train_op = optimizer.minimize(self.loss_val, global_step=self.global_step)
#train_op = tf.contrib.layers.optimize_loss()
return train_op
模型優(yōu)化改進
?? 至此,我就完成了一個基本的CNN神經(jīng)網(wǎng)絡(luò)可以用于文本分類,訓(xùn)練數(shù)據(jù)大概100W,一共4個類別。實踐證明,CNN的效果還是挺好的,在對batch_size,learning_rate,filter_size,num_filters,進行調(diào)整以后,整個網(wǎng)絡(luò)模型的效果在驗證集上即有97%的準(zhǔn)確率。
?? 在這個基礎(chǔ)上,針對Embedding層,如果直接加載預(yù)訓(xùn)練的word2vec詞向量,對分類效果會不會有幫助呢?為了防止Overfitting,在輸出層添加dropout對模型會有幫助嗎?在計算loss的時候,通過L2正則化以及訓(xùn)練過程中l(wèi)earning_rate動態(tài)更新,對模型的影響是什么?所以我進行了驗證,結(jié)果如下:
- 添加預(yù)訓(xùn)練word2vec向量,分類結(jié)果96%;
- L2正則,分類結(jié)果97.1%;
- dropout、learning_rate衰減沒有明顯變化
參考文獻
https://zhuanlan.zhihu.com/p/27685641
https://github.com/Delphine0379/text_classification