NLP學習計劃(九):Text-CNN原理及使用Text-CNN文本分類的keras實現

1. 卷積的定義與動機

1.1 卷積運算的定義

一維卷積的數學形式化定義如下:

\small S(t) = \int x(t-a)w(a) da

離散形式如下:

\small s(t) = \sum\limits_ax(t-a)w(a)

矩陣形式如下:

\small s(t)=(X*W)(t)

其中星號表示卷積。

二維卷積的表達式如下:

\small s(i,j)=(X*W)(i,j) = \sum\limits_m \sum\limits_n x(i-m,j-n) w(m,n)

而在深度學習的CNN中,沒有‘翻轉’這一步,因此表達式是如下形式:

\small s(i,j)=(X*W)(i,j) = \sum\limits_m \sum\limits_n x(i+m,j+n) w(m,n)

我們叫W爲我們的卷積核,也可以把w稱爲濾波器。而X則爲我們的輸入。如果X是一個二維輸入的矩陣,而W也是一個二維的矩陣。但是如果X是多維張量,那麼W也是一個多維的張量。

1.2 卷積層的計算原理

在下圖這個例子中,輸入數據是有高長方向的形狀的數據,濾波器也一樣,有高長方向上的維度。假設用(height, width)表示數據和濾波器的形狀,則在本例中,輸入大小是(4, 4),濾波器大小是(3, 3),輸出大小是(2, 2)。

現在用另一張圖解釋以下運算過程:對於輸入數據,卷積運算以一定間隔滑動濾波器的窗口並應用。這裏所說的窗口是指下圖中灰色的3 × 3的部分。如下圖所示,將各個位置上濾波器的元素和輸入的對應元素相乘,然後再求和(有時將這個計算稱爲乘積累加運算)。然後,將這個結果保存到輸出的對應位置。將這個過程在所有位置都進行一遍,就可以得到卷積運算的輸出。


在全連接的神經網絡中,除了權重參數,還存在偏置。 CNN中,濾波器的參數就對應之前的權重。並且, CNN中也存在偏置。包含偏置的卷積運算的處理流如下圖:

在卷積運算過程中需要制定兩個參數:‘“是否填充padding”’和“步幅大小”。

1.3卷積運算的動機

1.3.1 稀疏交互(sparseinteractions)

卷積運算通過三個重要的思想來幫助改進機器學習系統: 稀疏交互(sparseinteractions)、 參數共享(parameter sharing)、 等變表示(equivariant representations)。

傳統的神經網絡使用矩陣乘法來建立輸入與輸出的連接關係。其中,參數矩陣中每一個單獨的參數都描述了一個輸入單元與一個輸出單元間的交互。這意味着每一個輸出單元與每一個輸入單元都產生交互。然而, 卷積網絡具有 稀疏交互(sparse interactions)(也叫做 稀疏連接(sparse connectivity)或者 稀疏權重(sparse weights))的特徵。這是使核的大小遠小於輸入的大小來達到的。舉個例子,當處理一張圖像時,輸入的圖像可能包含成千上萬個像素點,但是我們可以通過只佔用幾十到上百個像素點的核來檢測一些小的有意義的特徵,例如圖像的邊緣。這意味着我們需要存儲的參數更少,不僅減少了模型的存儲需求,而且提高了它的統計效率。這也意味着爲了得到輸出我們只需要更少的計算量。這些效率上的提高往往是很顯著的。如果有 m 個輸入和 n 個輸出,那麼矩陣乘法需要 m × n 個參數並且相應算法的時間複雜度爲 O(m × n)(對於每一個例子)。如果我們限制每一個輸出擁有的連接數爲 k,那麼稀疏的連接方法只需要 k × n 個參數以及 O(k × n) 的運行時間。在很多實際應用中,只需保持 k 比 m 小几個數量級,就能在機器學習的任務中取得好的表現。在深度卷積網絡中,處在網絡深層的單元可能與絕大部分輸入是間接交互的,這允許網絡可以通過只描述稀疏交互的基石來高效地描述多個變量的複雜交互。

稀疏連接,對每幅圖從下往上看。我們強調了一個輸入單元 x3 以及在 s 中受該單元影響的輸出單元。 (上) 當 s 是由核寬度爲 3 的卷積產生時,只有三個輸出受到 x 的影響。 (下) 當 s是由矩陣乘法產生時,連接不再是稀疏的,所以所有的輸出都會受到 x3 的影響。
 

稀疏連接,對每幅圖從上往下看。我們強調了一個輸出單元 s3 以及 x 中影響該單元的輸入單元。這些單元被稱爲 s3 的 接受域(receptive field) 。 (上) 當 s 是由核寬度爲 3 的卷積產生時,只有三個輸入影響 s3。 (下) 當 s 是由矩陣乘法產生時,連接不再是稀疏的,所以所有的輸入都會影響 s3

處於卷積網絡更深的層中的單元,它們的接受域要比處在淺層的單元的接受域更大。如果網絡還包含類似步幅卷積或者池化之類的結構特徵,這種效應會加強。這意味着在卷積網絡中儘管直接連接都是很稀疏的,但處在更深的層中的單元可以間接地連接到全部或者大部分輸入圖像。

1.3.2 參數共享(parameter sharing)

參數共享(parameter sharing)是指在一個模型的多個函數中使用相同的參數。在傳統的神經網絡中,當計算一層的輸出時,權重矩陣的每一個元素只使用一次,當它乘以輸入的一個元素後就再也不會用到了。作爲參數共享的同義詞,我們可以說一個網絡含有 綁定的權重(tied weights),因爲用於一個輸入的權重也會被綁定在其他的權重上。在卷積神經網絡中,核的每一個元素都作用在輸入的每一位置上(是否考慮邊界像素取決於對邊界決策的設計)。卷積運算中的參數共享保證了我們只需要學習一個參數集合,而不是對於每一位置都需要學習一個單獨的參數集合。這雖然沒有改變前向傳播的運行時間(仍然是 O(k × n)),但它顯著地把模型的存儲需求降低至 k 個參數,並且 k 通常要比 m 小很多個數量級。因爲 m 和 n 通常有着大致相同的大小, k 在實際中相對於 m × n 是很小的。因此,卷積在存儲需求和統計效率方面極大地優於稠密矩陣的乘法運算。如下圖所示:

1.3.3 等變表示

對於卷積,參數共享的特殊形式使得神經網絡層具有對平移 等變(equivariance)的性質。如果一個函數滿足輸入改變,輸出也以同樣的方式改變這一性質,我們就說它是等變 (equivariant) 的。

2. 反捲積

反捲積可以簡單理解爲卷積層運算的逆過程,當做從一個通過卷積運算後縮小的特徵圖去恢復初始圖。

å·ç§¯ $\ padding=0,stride=1$

這個圖是卷積層求取過程(從藍色到綠色)

åå·ç§¯$\ padding=0,stride=1$

這個圖是反捲積層求取過程(從綠色到藍色)

3. 池化層的定義、種類和動機

3.1 池化運算的定義

池化是縮小高、長方向上的空間的運算。比如,如下圖所示爲Max池化的處理順序,進行將2 × 2的區域集約成1個元素的處理,縮小空間大小。

上圖的例子是按步幅2進行2 × 2的Max池化時的處理順序。“Max池化”是獲取最大值的運算,“2 × 2”表示目標區域的大小。如圖所示,從
2 × 2的區域中取出最大的元素。此外,這個例子中將步幅設爲了2,所以2 × 2的窗口的移動間隔爲2個元素。另外,一般來說,池化的窗口大小會和步幅設定成相同的值。比如, 3 × 3的窗口的步幅會設爲3, 4 × 4的窗口的步幅會設爲4。

池化層的特徵:

1)沒有要學習的參數

池化層和卷積層不同,沒有要學習的參數。池化只是從目標區域中取最大值(或者平均值),所以不存在要學習的參數。

2)通道數不發生變化
經過池化運算,輸入數據和輸出數據的通道數不會發生變化。如下圖所示計算是按通道獨立進行的。

3)對微小的位置變化具有魯棒性(健壯)

輸入數據發生微小偏差時,池化仍會返回相同的結果。因此,池化對輸入數據的微小偏差具有魯棒性。
 

3.2 池化層的種類

除了Max池化之外,還有Average池化等。相對於Max池化是從目標區域中取出最大值,Average池化則是計算目標區域的平均值。在圖像識別領域,主要使用Max池化。

3.3 池化的動機

(1)首要作用,下采樣(downsamping)

(2)降維、去除冗餘信息、對特徵進行壓縮、簡化網絡複雜度、減小計算量、減小內存消耗等等。各種說辭吧,總的理解就是減少參數量。

(3)實現非線性(這個可以想一下,relu函數,是不是有點類似的感覺?)。

(4)可以擴大感知野。

(5)可以實現不變性,其中不變形性包括,平移不變性、旋轉不變性和尺度不變性(暫未理解透徹

4.Text-CNN原理

CNN模型首次使用在文本分類,是Yoon Kim發表的“Convolutional Neural Networks for Sentence Classification”論文中。

NLP的輸入是一個個句子或者文檔。句子或文檔在輸入時經過embedding(word2vec或者Glove)會被表示成向量矩陣,其中每一行表示一個詞語,行的總數是句子的長度,列的總數就是詞表的長度。例如一個包含十個詞語的句子,使用了100維的embedding,最後我們就有一個輸入爲10x100的矩陣。

在CV中,filters是以一個patch(任意長度x任意寬度)的形式滑過遍歷整個圖像,但是在NLP中,對於文本數據,filter不再橫向滑動,僅僅是向下移動,filters會覆蓋到所有的詞表長度,也就是形狀爲 [filter_size, vocabulary_size]。更爲具體地理解可以看下圖,輸入爲一個7x5的矩陣,filters的高度分別爲2,3,4,寬度和輸入矩陣一樣爲5。每個filter對輸入矩陣進行卷積操作得到中間特徵,然後通過pooling提取最大值,在池化層到全連接層之前可以加上dropout防止過擬合。最終得到一個包含6個值的特徵向量。

放上一張TEXT-CNN的圖:
 

TEXT-CNN

  • 這圖中word embedding的維度是5。對於句子 i like this movie very much。可以轉換成如上圖所示的矩陣AϵR7×5AϵR7×5
  • 有6個卷積核,尺寸爲(2×5)(2×5), (3×5)(3×5), 4×54×5,每個尺寸各2個.
  • 樣本分別與以上卷積核進行卷積操作,再用激活函數激活。每個卷積核都得到了特徵向量(feature maps)
  • 使用1-max pooling提取出每個feature map的最大值,然後在級聯得到最終的特徵表達。
  • 將特徵輸入至softmax layer進行分類, 在這層可以進行正則化操作( l2-regulariation)

參數與超參數:

  • sequence_length (Q: 對於CNN, 輸入與輸出都是固定的,可每個句子長短不一, 怎麼處理? A: 需要做定長處理, 比如定爲n, 超過的截斷, 不足的補0. 注意補充的0對後面的結果沒有影響,因爲後面的max-pooling只會輸出最大值,補零的項會被過濾掉)
  • num_classes (多分類, 分爲幾類)
  • vocabulary_size (語料庫的詞典大小, 記爲|D|)
  • embedding_size (將詞向量的維度, 由原始的 |D| 降維到 embedding_size)
  • filter_size_arr (多個不同size的filter)

2015年“A Sensitivity Analysis of (and Practitioners' Guide to) Convolutional Neural Networks for Sentence Classification”論文詳細地闡述了關於TextCNN模型的調參心得
--------------------- 

  • 使用預訓練的word2vec 、 GloVe初始化效果會更好。一般不直接使用One-hot。
  • 卷積核的大小影響較大,一般取1~10,對於句子較長的文本,則應選擇大一些。
  • 卷積核的數量也有較大的影響,一般取100~600 ,同時一般使用Dropout(0~0.5)。
  • 激活函數一般選用ReLU 和 tanh。
  • 池化使用1-max pooling。
  • 隨着feature map數量增加,性能減少時,試着嘗試大於0.5的Dropout。
  • 評估模型性能時,記得使用交叉驗證。
     

5.利用Text-CNN進行文本分類

# coding: utf-8
import pickle
import logging
import tensorflow as tf
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s',level=logging.INFO)

class TextCNN(object):
    """
    A CNN for text classification.
    Uses an embedding layer, followed by a convolution, max-pooling and soft-max layer.
    """
    def __init__(self, config):
        self.lr = config['lr']
        self.batch_size = config['batch_size']
        # 詞典的大小
        self.vocab_size = config['vocab_size']
        self.num_classes = config['num_classes']
        self.keep_prob = config['keep_prob']
        # length of word embedding
        self.embedding_size = config['embedding_size']
        # seting filter sizes, type of list
        self.filter_sizes = config['filter_sizes']
        # max length of sentence
        self.sentence_length = config['sentence_length']
        # number of filters
        self.num_filters = config['num_filters']

    def add_placeholders(self):
        self.X = tf.placeholder('int32', [None, self.sentence_length])
        self.y = tf.placeholder('int32', [None, ])

    def inference(self):
        with tf.variable_scope('embedding_layer'):
            # loading embedding weights
            with open('Text_cnn/embedding_matrix.pkl','rb') as f:
                embedding_weights = pickle.load(f)
            # non-static 
            self.W = tf.Variable(embedding_weights, trainable=True, name='embedding_weights',dtype='float32')
            # shape of embedding chars is (None, sentence_length, embedding_size)
            self.embedding_chars = tf.nn.embedding_lookup(self.W, self.X)
            # shape of embedding char expanded is (None, sentence_length, embedding_size, 1)
            self.embedding_chars_expanded = tf.expand_dims(self.embedding_chars, -1)
        with tf.variable_scope('convolution_pooling_layer'):
            pooled_outputs = []
            for i, filter_size in enumerate(self.filter_sizes):
                filter_shape = [filter_size, self.embedding_size, 1, self.num_filters]
                W = tf.get_variable('W'+str(i), shape=filter_shape,
                                    initializer=tf.truncated_normal_initializer(stddev=0.1))
                b = tf.get_variable('b'+str(i), shape=[self.num_filters],
                                    initializer=tf.zeros_initializer())
                conv = tf.nn.conv2d(self.embedding_chars_expanded, W, strides=[1,1,1,1],
                                    padding='VALID', name='conv'+str(i))
                # apply nonlinearity
                h = tf.nn.relu(tf.add(conv, b))
                # max pooling
                pooled = tf.nn.max_pool(h, ksize=[1, self.sentence_length - filter_size + 1, 1, 1],
                    strides=[1, 1, 1, 1], padding='VALID', name="pool")
                # shape of pooled is (?,1,1,300)
                pooled_outputs.append(pooled)
            # combine all the pooled features
            self.feature_length = self.num_filters * len(self.filter_sizes)
            self.h_pool = tf.concat(pooled_outputs,3)
            # shape of (?, 900)
            self.h_pool_flat = tf.reshape(self.h_pool, [-1, self.feature_length])
        # add dropout before softmax layer
        with tf.variable_scope('dropout_layer'):
            # shape of [None, feature_length]
            self.features = tf.nn.dropout(self.h_pool_flat, self.keep_prob)
        # fully-connection layer
        with tf.variable_scope('fully_connection_layer'):
            W = tf.get_variable('W', shape=[self.feature_length, self.num_classes],
                                initializer=tf.contrib.layers.xavier_initializer())
            b = tf.get_variable('b', shape=[self.num_classes],
                                initializer=tf.constant_initializer(0.1))
            # shape of [None, 2]
            self.y_out = tf.matmul(self.features, W) + b
            self.y_prob = tf.nn.softmax(self.y_out)

    def add_loss(self):
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
        self.loss = tf.reduce_mean(loss)
        tf.summary.scalar('loss',self.loss)

    def add_metric(self):
        self.y_pred = self.y_prob[:,1] > 0.5
        self.precision, self.precision_op = tf.metrics.precision(self.y, self.y_pred)
        self.recall, self.recall_op = tf.metrics.recall(self.y, self.y_pred)
        # add precision and recall to summary
        tf.summary.scalar('precision', self.precision)
        tf.summary.scalar('recall', self.recall)

    def train(self):
        # Applies exponential decay to learning rate
        self.global_step = tf.Variable(0, trainable=False)
        # define optimizer
        optimizer = tf.train.AdamOptimizer(self.lr)
        extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        with tf.control_dependencies(extra_update_ops):
            self.train_op = optimizer.minimize(self.loss, global_step=self.global_step)

    def build_graph(self):
        """build graph for model"""
        self.add_placeholders()
        self.inference()
        self.add_loss()
        self.add_metric()
        self.train()

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章