NLP實踐-Task7

任務鏈接:https://wx.zsxq.com/dweb/#/index/222248424811

1.卷積

參考鏈接:https://zhuanlan.zhihu.com/p/57575810

1.1卷積

卷積:圖像中不同數據窗口的數據和卷積核(一個濾波矩陣)作內積的操作叫做卷積。其計算過程又稱爲濾波,本質是提取圖像不同頻段的特徵。
卷積核:也稱爲濾波器filter,帶着一組固定權重的神經元,通常是n*m二維的矩陣,n和m也是神經元的感受野。
卷積層:多個濾波器疊加便成了卷積層。
卷積流程示例圖:

1.2轉置卷積

轉置卷積:向普通卷積方向相反的轉換,即執行上採樣。例子:生成高分辨率圖像、將低維特徵圖映射到高維空間。
在卷積中,我們定義C爲卷積核,Large爲輸入圖像,Small爲輸出圖像。經過卷積(矩陣乘法)後,我們將大圖像下采樣爲小圖像。這種矩陣乘法的卷積的實現遵照:C x Large = Small
下面的例子展示了這種運算的工作方式。它將輸入平展爲 16×1 的矩陣,並將卷積核轉換爲一個稀疏矩陣(4×16)。然後,在稀疏矩陣和平展的輸入之間使用矩陣乘法。之後,再將所得到的矩陣(4×1)轉換爲 2×2 的輸出。

現在,如果我們在等式的兩邊都乘上矩陣的轉置 CT,並藉助「一個矩陣與其轉置矩陣的乘法得到一個單位矩陣」這一性質,那麼我們就能得到公式 CT x Small = Large,如下圖所示。

這裏可以看到,我們執行了從小圖像到大圖像的上採樣。這正是我們想要實現的目標。

1.3擴張卷積

擴張卷積就是通過在覈元素之間插入空格來使核「膨脹」。新增的參數 l(擴張率)表示我們希望將核加寬的程度。具體實現可能各不相同,但通常是在覈元素之間插入 l-1 個空格。下面展示了 l = 1, 2, 4 時的核大小。

在這張圖像中,3×3 的紅點表示經過卷積後,輸出圖像是 3×3 像素。儘管所有這三個擴張卷積的輸出都是同一尺寸,但模型觀察到的感受野有很大的不同。l=1 時感受野爲 3×3,l=2 時爲 7×7。l=3 時,感受野的大小就增加到了 15×15。有趣的是,與這些操作相關的參數的數量是相等的。我們「觀察」更大的感受野不會有額外的成本。因此,擴張卷積可用於廉價地增大輸出單元的感受野,而不會增大其核大小,這在多個擴張卷積彼此堆疊時尤其有效。

1.4可分卷積

1.4.1空間可分卷積

空間可分卷積操作的是圖像的 2D 空間維度,即高和寬。從概念上看,空間可分卷積是將一個卷積分解爲兩個單獨的運算。對於下面的示例,3×3 的 Sobel 核被分成了一個 3×1 核和一個 1×3 核。

在卷積中,3×3 核直接與圖像卷積。在空間可分卷積中,3×1 核首先與圖像卷積,然後再應用 1×3 核。這樣,執行同樣的操作時僅需 6 個參數,而不是 9 個。
此外,使用空間可分卷積時所需的矩陣乘法也更少。給一個具體的例子,5×5 圖像與 3×3 核的卷積(步幅=1,填充=0)要求在 3 個位置水平地掃描核(還有 3 個垂直的位置)。總共就是 9 個位置,表示爲下圖中的點。在每個位置,會應用 9 次逐元素乘法。總共就是 9×9=81 次乘法。

另一方面,對於空間可分卷積,我們首先在 5×5 的圖像上應用一個 3×1 的過濾器。我們可以在水平 5 個位置和垂直 3 個位置掃描這樣的核。總共就是 5×3=15 個位置,表示爲下圖中的點。在每個位置,會應用 3 次逐元素乘法。總共就是 15×3=45 次乘法。現在我們得到了一個 3×5 的矩陣。這個矩陣再與一個 1×3 核卷積,即在水平 3 個位置和垂直 3 個位置掃描這個矩陣。對於這 9 個位置中的每一個,應用 3 次逐元素乘法。這一步需要 9×3=27 次乘法。因此,總體而言,空間可分卷積需要 45+27=72 次乘法,少於普通卷積。

儘管空間可分卷積能節省成本,但深度學習卻很少使用它。一大主要原因是並非所有的核都能分成兩個更小的核。如果我們用空間可分卷積替代所有的傳統卷積,那麼我們就限制了自己在訓練過程中搜索所有可能的核。這樣得到的訓練結果可能是次優的。

1.4.2深度可分卷積

在描述這些步驟之前,有必要回顧一下我們之前介紹的 2D 卷積核 1×1 卷積。首先快速回顧標準的 2D 卷積。舉一個具體例子,假設輸入層的大小是 7×7×3(高×寬×通道),而過濾器的大小是 3×3×3。經過與一個過濾器的 2D 卷積之後,輸出層的大小是 5×5×1(僅有一個通道)。

一般來說,兩個神經網絡層之間會應用多個過濾器。假設我們這裏有 128 個過濾器。在應用了這 128 個 2D 卷積之後,我們有 128 個 5×5×1 的輸出映射圖(map)。然後我們將這些映射圖堆疊成大小爲 5×5×128 的單層。通過這種操作,我們可將輸入層(7×7×3)轉換成輸出層(5×5×128)。空間維度(即高度和寬度)會變小,而深度會增大。

 

現在使用深度可分卷積,看看我們如何實現同樣的變換。

首先,我們將深度卷積應用於輸入層。但我們不使用 2D 卷積中大小爲 3×3×3 的單個過濾器,而是分開使用 3 個核。每個過濾器的大小爲 3×3×1。每個核與輸入層的一個通道卷積(僅一個通道,而非所有通道!)。每個這樣的卷積都能提供大小爲 5×5×1 的映射圖。然後我們將這些映射圖堆疊在一起,創建一個 5×5×3 的圖像。經過這個操作之後,我們得到大小爲 5×5×3 的輸出。現在我們可以降低空間維度了,但深度還是和之前一樣。

 

深度可分卷積——第一步:我們不使用 2D 卷積中大小爲 3×3×3 的單個過濾器,而是分開使用 3 個核。每個過濾器的大小爲 3×3×1。每個核與輸入層的一個通道卷積(僅一個通道,而非所有通道!)。每個這樣的卷積都能提供大小爲 5×5×1 的映射圖。然後我們將這些映射圖堆疊在一起,創建一個 5×5×3 的圖像。經過這個操作之後,我們得到大小爲 5×5×3 的輸出。

在深度可分卷積的第二步,爲了擴展深度,我們應用一個核大小爲 1×1×3 的 1×1 卷積。將 5×5×3 的輸入圖像與每個 1×1×3 的核卷積,可得到大小爲 5×5×1 的映射圖。

因此,在應用了 128 個 1×1 卷積之後,我們得到大小爲 5×5×128 的層。

 

通過這兩個步驟,深度可分卷積也會將輸入層(7×7×3)變換到輸出層(5×5×128)。

下圖展示了深度可分卷積的整個過程。
|
所以,深度可分卷積有何優勢呢?效率!相比於 2D 卷積,深度可分卷積所需的操作要少得多。

1.5分組卷積

在分組卷積中,過濾器會被分爲不同的組。每一組都負責特定深度的典型 2D 卷積。下面的例子能讓你更清楚地理解。

上圖展示了具有兩個過濾器分組的分組卷積。在每個過濾器分組中,每個過濾器的深度僅有名義上的 2D 卷積的一半。它們的深度是 Din/2。每個過濾器分組包含 Dout/2 個過濾器。第一個過濾器分組(紅色)與輸入層的前一半([:, :, 0:Din/2])卷積,而第二個過濾器分組(橙色)與輸入層的後一半([:, :, Din/2:Din])卷積。因此,每個過濾器分組都會創建 Dout/2 個通道。整體而言,兩個分組會創建 2×Dout/2 = Dout 個通道。然後我們將這些通道堆疊在一起,得到有 Dout 個通道的輸出層。

小卷積核VS大卷積核

在AlexNet中有有11x11的卷積核與5x5的卷積核,但是在VGG網絡中因爲層數增加,卷積核都變成3x3與1x1的大小啦,這樣的好處是可以減少訓練時候的計算量,有利於降低總的參數數目。關於如何把大卷積核替換爲小卷積核,本質上有兩種方法。

1.將二維卷積差分爲兩個連續一維卷積

二維卷積都可以拆分爲兩個一維的卷積,這個是有數學依據的,所以11x11的卷積可以轉換爲1x11與11x1兩個連續的卷積覈計算,總的運算次數:

  • 11x11 = 121次
  • 1x11+ 11x1 = 22次

2.將大二維卷積用多個連續小二維卷積替代

可見把大的二維卷積核在計算環節改成兩個連續的小卷積核可以極大降低計算次數、減少計算複雜度。同樣大的二維卷積核還可以通過幾個小的二維卷積核替代得到。比如:5x5的卷積,我們可以通過兩個連續的3x3的卷積替代,比較計算次數

  • 5x5= 25次
  • 3x3+ 3x3=18次

2.池化層

參考鏈接:https://www.baidu.com/link?url=TtZEGYQuVFHT8GNFPTq6L2_TjT8Os_JmhEdXGCmUPwL16bD6KtyFK-WXO1EOVjyzR6MyDThS_N45MaQsMdCaSa&wd=&eqid=c0fbdf0c0011df28000000035c8a4bd3

池化層的具體操作與卷基層的操作基本相同,只不過池化層的卷積核爲只取對應位置的最大值、平均值等(最大池化、平均池化),並且不經過反向傳播的修改。池化的時候同樣需要提供filter的大小、步長、下面就是3x3步長爲1的filter在5x5的輸入圖像上均值池化計算過程與輸出結果

改用最大值做池化的過程與結果如下:

3.TextCNN的原理

參考鏈接:https://www.cnblogs.com/bymo/p/9675654.html

 TextCNN的詳細過程原理圖如下:

TextCNN詳細過程:

  • Embedding:第一層是圖中最左邊的7乘5的句子矩陣,每行是詞向量,維度=5,這個可以類比爲圖像中的原始像素點。
  • Convolution:然後經過 kernel_sizes=(2,3,4) 的一維卷積層,每個kernel_size 有兩個輸出 channel。
  • MaxPolling:第三層是一個1-max pooling層,這樣不同長度句子經過pooling層之後都能變成定長的表示。
  • FullConnection and Softmax:最後接一層全連接的softmax 層,輸出每個類別的概率。

通道(Channels):

  • 圖像中可以利用 (R, G, B) 作爲不同channel;
  • 文本的輸入的channel通常是不同方式的embedding方式(比如 word2vec或Glove),實踐中也有利用靜態詞向量和fine-tunning詞向量作爲不同channel的做法。

一維卷積(conv-1d):

  • 圖像是二維數據;
  • 文本是一維數據,因此在TextCNN卷積用的是一維卷積(在word-level上是一維卷積;雖然文本經過詞向量表達後是二維數據,但是在embedding-level上的二維卷積沒有意義)。一維卷積帶來的問題是需要通過設計不同 kernel_size 的 filter 獲取不同寬度的視野

Pooling層:

利用CNN解決文本分類問題的文章還是很多的,比如這篇 A Convolutional Neural Network for Modelling Sentences 最有意思的輸入是在 pooling 改成 (dynamic) k-max pooling ,pooling階段保留 k 個最大的信息,保留了全局的序列信息。

4.TextCNN代碼

import os
import numpy as np
import tensorflow as tf
from collections import Counter
import tensorflow.contrib.keras as kr


class Text(object):
    # 打開文件
    def open_file(self, filename, mode='r'):
        return open(filename, mode, encoding='utf-8', errors='ignore')

    # 讀取文件
    def read_file(self, filename):
        contents, labels = [], []
        with self.open_file(filename) as f:
            for line in f:
                try:
                    label, content = line.strip().split('\t')
                    if content:
                        contents.append(list(content))
                        labels.append(label)
                except:
                    pass
        return contents, labels

    # 讀取詞彙表,一個詞對應一個id
    def read_vocab(self, vocab_dir):
        with self.open_file(vocab_dir) as fp:
            words = [_.strip() for _ in fp.readlines()]
        word_to_id = dict(zip(words, range(len(words))))
        return words, word_to_id

    # 讀取分類目錄,一個類別對應一個id
    def read_category(self):
        categories = ['體育', '財經', '房產', '家居', '教育', '科技', '時尚', '時政', '遊戲', '娛樂']
        cat_to_id = dict(zip(categories, range(len(categories))))
        return categories, cat_to_id

    # 根據訓練集構建詞彙表,存儲
    def build_vocab(self, train_dir, vocab_dir, vocab_size=5000):
        data_train, _ = self.read_file(train_dir)

        all_data = []
        for content in data_train:
            all_data.extend(content)

        counter = Counter(all_data)
        count_pairs = counter.most_common(vocab_size - 1)
        words, _ = list(zip(*count_pairs))
        # 添加一個 <PAD> 來將所有文本pad爲同一長度
        words = ['<PAD>'] + list(words)
        self.open_file(vocab_dir, mode='w').write('\n'.join(words) + '\n')

    # 將文件轉換爲id表示
    def process_file(self, filename, word_to_id, cat_to_id, max_length=600):
        contents, labels = self.read_file(filename)

        data_id, label_id = [], []
        for i in range(len(contents)):
            data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])
            label_id.append(cat_to_id[labels[i]])

        # 使用keras提供的pad_sequences來將文本轉爲固定長度,不足的補0
        x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
        y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id))  # 將標籤轉換爲one-hot表示

        return x_pad, y_pad

    # 獲取數據
    def get_data(self, filenname, text_length):
        vocab_dir = './data/cnews/cnews.vocab.txt'
        categories, cat_to_id = text.read_category()
        words, word_to_id = text.read_vocab(vocab_dir)
        x, y = text.process_file(filenname, word_to_id, cat_to_id, text_length)
        return x, y


class TextCNN(object):
    def __init__(self):
        self.text_length = 600  # 文本長度
        self.num_classer = 10  # 類別數

        self.vocab_size = 5000  # 詞彙表達小
        self. word_vec_dim = 64  # 詞向量維度

        self.filter_width = 5  # 卷積核尺寸
        self.filter_width_list = [2, 3, 4, 5]  # 卷積核尺寸列表
        self.num_filters = 10  # 卷積核數目

        self.dropout_prob = 0.5  # dropout概率
        self.learning_rate = 0.005  # 學習率
        self.iter_num = 10  # 迭代次數
        self.batch_size = 64  # 每輪迭代訓練多少數據
        self.model_save_path = './model/'  # 模型保存路徑
        self.model_name = 'mnist_model'  # 模型的命名
        self.embedding = tf.get_variable('embedding', [self.vocab_size, self.word_vec_dim])

        self.fc1_size = 32  # 第一層全連接的神經元個數
        self.fc2_size = 64  # 第二層全連接的神經元個數
        self.fc3_size = 10  # 第三層全連接的神經元個數

    # 模型1,使用多種卷積核
    def model_1(self, x, is_train):
        # embedding層
        embedding_res = tf.nn.embedding_lookup(self.embedding, x)

        pool_list = []
        for filter_width in self.filter_width_list:
            # 卷積層
            conv_w = self.get_weight([filter_width, self.word_vec_dim, self.num_filters], 0.01)
            conv_b = self.get_bias([self.num_filters])
            conv = tf.nn.conv1d(embedding_res, conv_w, stride=1, padding='VALID')
            conv_res = tf.nn.relu(tf.nn.bias_add(conv, conv_b))

            # 最大池化層
            pool_list.append(tf.reduce_max(conv_res, reduction_indices=[1]))
        pool_res = tf.concat(pool_list, 1)

        # 第一個全連接層
        fc1_w = self.get_weight([self.num_filters * len(self.filter_width_list), self.fc1_size], 0.01)
        fc1_b = self.get_bias([self.fc1_size])
        fc1_res = tf.nn.relu(tf.matmul(pool_res, fc1_w) + fc1_b)
        if is_train:
            fc1_res = tf.nn.dropout(fc1_res, 0.5)

        # 第二個全連接層
        fc2_w = self.get_weight([self.fc1_size, self.fc2_size], 0.01)
        fc2_b = self.get_bias([self.fc2_size])
        fc2_res = tf.nn.relu(tf.matmul(fc1_res, fc2_w) + fc2_b)
        if is_train:
            fc2_res = tf.nn.dropout(fc2_res, 0.5)

        # 第三個全連接層
        fc3_w = self.get_weight([self.fc2_size, self.fc3_size], 0.01)
        fc3_b = self.get_bias([self.fc3_size])
        fc3_res = tf.matmul(fc2_res, fc3_w) + fc3_b

        return fc3_res

    # 模型2,使用一個卷積核
    def model_2(self, x, is_train):
        # embedding層
        embedding_res = tf.nn.embedding_lookup(self.embedding, x)

        # 卷積層
        conv_w = self.get_weight([self.filter_width, self.word_vec_dim, self.num_filters], 0.01)
        conv_b = self.get_bias([self.num_filters])
        conv = tf.nn.conv1d(embedding_res, conv_w, stride=1, padding='VALID')
        conv_res = tf.nn.relu(tf.nn.bias_add(conv, conv_b))

        # 最大池化層
        pool_res = tf.reduce_max(conv_res, reduction_indices=[1])

        # 第一個全連接層
        fc1_w = self.get_weight([self.num_filters, self.fc1_size], 0.01)
        fc1_b = self.get_bias([self.fc1_size])
        fc1_res = tf.nn.relu(tf.matmul(pool_res, fc1_w) + fc1_b)
        if is_train:
            fc1_res = tf.nn.dropout(fc1_res, 0.5)

        # 第二個全連接層
        fc2_w = self.get_weight([self.fc1_size, self.fc2_size], 0.01)
        fc2_b = self.get_bias([self.fc2_size])
        fc2_res = tf.nn.relu(tf.matmul(fc1_res, fc2_w) + fc2_b)
        if is_train:
            fc2_res = tf.nn.dropout(fc2_res, 0.5)

        # 第三個全連接層
        fc3_w = self.get_weight([self.fc2_size, self.fc3_size], 0.01)
        fc3_b = self.get_bias([self.fc3_size])
        fc3_res = tf.matmul(fc2_res, fc3_w) + fc3_b

        return fc3_res

    # 定義初始化網絡權重函數
    def get_weight(self, shape, regularizer):
        w = tf.Variable(tf.truncated_normal(shape, stddev=0.1))
        tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(regularizer)(w))  # 爲權重加入L2正則化
        return w

    # 定義初始化偏置項函數
    def get_bias(self, shape):
        b = tf.Variable(tf.ones(shape))
        return b

    # 生成批次數據
    def batch_iter(self, x, y):
        data_len = len(x)
        num_batch = int((data_len - 1) / self.batch_size) + 1
        indices = np.random.permutation(np.arange(data_len))  # 隨機打亂一個數組
        x_shuffle = x[indices]  # 隨機打亂數據
        y_shuffle = y[indices]  # 隨機打亂數據
        for i in range(num_batch):
            start = i * self.batch_size
            end = min((i + 1) * self.batch_size, data_len)
            yield x_shuffle[start:end], y_shuffle[start:end]


# 訓練
def train(cnn, X_train, y_train):
    x = tf.placeholder(tf.int32, [None, cnn.text_length])
    y = tf.placeholder(tf.float32, [None, cnn.num_classer])
    y_pred = cnn.model_1(x, True)

    # 聲明一個全局計數器,並輸出化爲0,存放到目前爲止模型優化迭代的次數
    global_step = tf.Variable(0, trainable=False)

    # 損失函數,交叉熵
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y)
    loss = tf.reduce_mean(cross_entropy)

    # 優化器
    train_step = tf.train.AdamOptimizer(learning_rate=cnn.learning_rate).minimize(loss, global_step=global_step)

    saver = tf.train.Saver()  # 實例化一個保存和恢復變量的saver

    # 創建一個會話,並通過python中的上下文管理器來管理這個會話
    with tf.Session() as sess:
        # 初始化計算圖中的變量
        init_op = tf.global_variables_initializer()
        sess.run(init_op)

        # 通過checkpoint文件定位到最新保存的模型
        ckpt = tf.train.get_checkpoint_state(cnn.model_save_path)
        if ckpt and ckpt.model_checkpoint_path:
            # 加載最新的模型
            saver.restore(sess, ckpt.model_checkpoint_path)

        # 循環迭代,每次迭代讀取一個batch_size大小的數據
        for i in range(cnn.iter_num):
            batch_train = cnn.batch_iter(X_train, y_train)
            for x_batch, y_batch in batch_train:
                loss_value, step = sess.run([loss, train_step], feed_dict={x: x_batch, y: y_batch})
                print('After %d training step(s), loss on training batch is %g.' % (i, loss_value))
                saver.save(sess, os.path.join(cnn.model_save_path, cnn.model_name), global_step=global_step)


# 預測
def predict(cnn, X_test, y_test):
    # 創建一個默認圖,在該圖中執行以下操作
    x = tf.placeholder(tf.int32, [None, cnn.text_length])
    y = tf.placeholder(tf.float32, [None, cnn.num_classer])
    y_pred = cnn.model_1(x, False)

    saver = tf.train.Saver()  # 實例化一個保存和恢復變量的saver

    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_pred, 1))  # 判斷預測值和實際值是否相同
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))  # 求平均得到準確率

    with tf.Session() as sess:
        ckpt = tf.train.get_checkpoint_state(cnn.model_save_path)
        if ckpt and ckpt.model_checkpoint_path:
            saver.restore(sess, ckpt.model_checkpoint_path)

            # 根據讀入的模型名字切分出該模型是屬於迭代了多少次保存的
            global_step = ckpt.model_checkpoint_path.split('/')[-1].split(' ')[-1]

            # 計算出測試集上準確
            accuracy_score = sess.run(accuracy, feed_dict={x: X_test, y: y_test})
            print('After %s training step(s), test accuracy = %g' % (global_step, accuracy_score))
        else:
            print('No checkpoint file found')
            return


if __name__ == '__main__':
    text_length = 600  # 文本長度
    text = Text()
    X_train, y_train = text.get_data('./data/cnews/cnews.train.txt', text_length)  # X_train shape (50000, 300)
    X_test, y_test = text.get_data('./data/cnews/cnews.test.txt', text_length)  # X_test shape (10000, 300)
    X_val, y_val = text.get_data('./data/cnews/cnews.val.txt', text_length)  # X_val shape (5000, 300)

    is_train = True
    cnn = TextCNN()
    if is_train:
        train(cnn, X_train, y_train)
    else:
        predict(cnn, X_val, y_val)

 

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