Tensorflow實現(xiàn)CNN文本分類

?? 摘要:本文主要是用于學(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:步長。
feature map 長度計算公式

?? 在整個計算卷積計算的過程中,主要有兩個重要的特性: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繪制的圖像。

卷積層
loss

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. 第1層表示為輸入層,輸入為一個短句,每個字通過word2vec來表示為一個行向量,表示為一個二維矩陣(實際使用卷積層應(yīng)該為4維矩陣);
  2. 第2層我們可以看到設(shè)置了3種不同尺寸的卷積核,表示為filter_size:[2,3,4],其中表示每個卷積核進行卷積計算獲取特征的詞個數(shù)
    (相鄰2個字,相鄰3個字,相鄰4個字),其中每種尺寸卷積核的個數(shù)為2,所以在第二層中一共包含6個卷積核,3種卷積核,每種卷積核的列數(shù)和輸入數(shù)據(jù)為列數(shù)一致。
  3. 將第2層中的卷積核和第一層中的矩陣輸入進行卷積計算,獲取到的結(jié)果即為第3層,即生成6個feature map
  4. 第3層中每個卷積核生成的feature map進行max pool最大池化,即取得所有輸出的最大值,進行concat聚合操作,這樣即得到CNN抽取出的所有特征。
  5. 4層結(jié)果和最后一層輸出層進行全連接操作,輸出個數(shù)根據(jù)實際應(yīng)用進行設(shè)置。
    這樣,就完成了一個CNN文本分類的操作,具體損失函數(shù),優(yōu)化函數(shù)的計算,都是可以直接通過tensorflow調(diào)用。
cnn文本分類

?? 接下來,我們從編碼實現(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é)果如下:

  1. 添加預(yù)訓(xùn)練word2vec向量,分類結(jié)果96%;
  2. L2正則,分類結(jié)果97.1%;
  3. dropout、learning_rate衰減沒有明顯變化

參考文獻

https://zhuanlan.zhihu.com/p/27685641
https://github.com/Delphine0379/text_classification

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • CNN on TensorFlow 本文大部分內(nèi)容均參考于: An Intuitive Explanation o...
    _Randolph_閱讀 8,009評論 2 31
  • 你配不上自己的野心,也辜負(fù)了所受的苦難.
    松磊607閱讀 204評論 0 0
  • 最近很熱的詞,文盲的我后來才知道什么是開掛的人生,怪不得這樣的詞和我八竿子也打不著。平淡,普通,這樣的詞也許更能貼...
    晨晨獸進化閱讀 317評論 0 1
  • 其實女人也很色,雖然這事大家不承認(rèn)。 一聽到他們說“男人嘛,長相無所謂的”,我就來氣。一想到這句話的潛臺詞是“男人...
    水亦煮粥閱讀 1,670評論 3 0
  • 你是我的四月,風(fēng)輕柔了思念,暖融化心湖。你是我的綠衣,風(fēng)流了繁花,駐留單純。你帶著淡淡的煙火,溫?fù)嵊孤担瑴\淺歲月因...
    弄塵閱讀 709評論 0 0

友情鏈接更多精彩內(nèi)容