神經網絡基礎-卷積神經網絡

在深度學習的路上,從頭開始瞭解一下各項技術。本人是DL小白,連續記錄我自己看的一些東西,大家可以互相交流。

本文參考:本文參考吳恩達老師的Coursera深度學習課程,很棒的課,推薦 

本文默認你已經大致瞭解深度學習的簡單概念,如果需要更簡單的例子,可以參考吳恩達老師的入門課程:

http://study.163.com/courses-search?keyword=%E5%90%B4%E6%81%A9%E8%BE%BE#/?ot=5

轉載請註明出處,其他的隨你便咯

一、前言

卷積神經網絡(Convolutional Neural Network, CNN)是一種深度前饋人工神經網絡,已成功地應用於圖像識別領域,同時在聲紋識別領域,也取得了不小的成果。

二、卷積運算

卷積運算是卷積神經網絡的基本組成部分,我們用邊緣檢測的例子來說明一下卷積運算的過程。

在下圖中,我們通過垂直邊緣檢測和水平邊緣檢測,可以獲得右邊的圖像:

è¿å¨è·¯ä¸ï¼ç¨ç­...

即,僅保留圖片中的垂直邊緣或水平邊緣。

垂直邊緣檢測:

假設一張圖片的大小爲 6×6(數字表示圖片大小,具體爲像素值),和一個3×3的filter(卷積核)進行卷積運算,用“*”符號表示。圖片和垂直邊緣檢測器分別如圖中左1矩陣和左2矩陣:

è¿å¨è·¯ä¸ï¼ç¨ç­...

卷積運算的過程,實際上是讓filter從圖片左上角開始不斷移動,不斷地和其大小相同的部分做對應元素的乘法運算並求和,最終得到的數字相當於新圖片的一個像素值,如右矩陣所示,最終得到一個 4×4 大小的圖片。這個過程即爲卷積運算。

PS.給出卷積緯度計算公式:

圖片爲 n×n 大小,filter爲 f×f 大小,結果爲(n - f + 1)×(n - f + 1)

三、填充(Padding)

在卷積運算的過程中,我們發現瞭如下的問題:

  • 每次卷積操作,會讓圖片縮小;
    • 如例子中,6×6的圖片,經過 3×3 filter的卷積後,只剩下4×4 大小
  • 角落和邊緣位置的像素在卷積運算的過程中,參與的次數很少,最終結果中,可能會損失數據。

爲了解決如上的兩個問題,我們在進行卷積運算前,爲圖片加Padding,包圍角落和邊緣的像素,使得通過filter的卷積運算後,圖片大小不變,也不會丟失角落和邊緣的信息。Padding的過程如圖所示:

è¿å¨è·¯ä¸ï¼ç¨ç­...

用p來表示Padding的值,當輸入爲n×n大小的圖片,最終得到的圖片大小爲(n + 2p − f + 1)×(n + 2p − f + 1),爲了讓圖片大小保持不變,需根據filter的大小f來調整p的值。根據是否使用Padding我們可以將卷積分爲兩種。

Valid / Same 卷積:

  • Valid卷積:沒有padding,輸入n×n的圖像,輸出(n - f + 1)×(n - f + 1)的圖像;
  • Same卷積:有padding,輸出與輸入圖片大小相同,p = (f - 1) / 2。

四、卷積步長(stride)

卷積的步長是構建CNN的一個基本的屬性。在上述的例子裏面,我們使用的步長 stride = 1,即每次filter移動一個像素。當stride = 2時,卷積運算如下:

è¿å¨è·¯ä¸ï¼ç¨ç­...

即每次filter在計算完一個數值時,會移動兩個像素的位移,繼續計算下一個數值。

我們用s表示stride步長的大小,那麼在進行卷積運算後,圖片從n×n大小,變爲如下大小:

五、立體卷積

在上面的例子和介紹中,我們的圖像都是二維的。在應用於彩色圖像時,我們用RGB三通道來表示一張圖片。其中R紅色,G綠色和B藍色,該表示法是用三個三原色的數值,來組合成一個圖片。當我們對彩色圖片進行卷積的時候,此時的卷積核應爲三維卷積核。

è¿å¨è·¯ä¸ï¼ç¨ç­...

其實與二維的卷積類似,只是我們現在的filter與圖片的維度保持一致,卷積核的第三個維度需要與圖片保持一致,在計算之後求和的過程,是f × f × f個數字之和。

多卷積核

單個卷積核應用於圖片時,可以提取特定的特徵。我們也可以同時用多個卷積核來同時提取不同的特徵:

è¿å¨è·¯ä¸ï¼ç¨ç­...

在上圖中,我們用了兩個3×3×3的卷積核分別提取了圖片的垂直邊緣和水平邊緣,最終得到了2通道的4×4的特徵圖片。

總結而言:

輸入的圖片表示爲:n×n×nc(通道數)

卷積核爲:f×f×nc(與圖標通道數一致)

運算結果爲:(n − f + 1)×(n − f + 1)×nc

六、簡單卷積網絡

卷積神經網絡和普通的神經網絡前向傳播的過程類似。如下圖所示:

è¿å¨è·¯ä¸ï¼ç¨ç­...

CNN也是先由輸入和權重做線性運算,然後將結果送進一個激活函數,得到最終的輸出,不同點在於卷積神經網絡中,權重是filter,輸入是多維度的矩陣,二者進行卷積運算。

單層卷積的參數個數

在一個卷積層中,假設我們有10個3×3×3的卷積核,每個卷積核有1個偏置,那麼對於一個卷積層的參數個數爲:

(3×3×3+1) ×10 = 280

那麼,無論圖片的有多大,上例中的卷積層的參數是固定的280個,相對與普通的深度神經網絡而言,卷積神經網絡的參數少了很多。

標記總結:

如果 l 表示一個卷積層:

卷積網絡示例:

一個卷積網絡,通常由多個卷積層組合而成,下面我們給出一個例子,方便大家理解:

è¿å¨è·¯ä¸ï¼ç¨ç­...

在上述例子中,除了卷積層,我們還加入了池化層(POOL)和全連接層(FC)。

七、池化層(POOL)

池化層是CNN中的一個重要概念,一般是在卷積層之後。池化層用來對輸入進行降採樣,降低維度並保留顯著的參數。其特點是輸出一個固定大小的矩陣,並且降低輸出結果的維度。

最大池化(Max Pooling)

最大池化層是對前一層得到的特徵圖進行池化減小,僅由當前小區域內的最大值來代表赤化後的值。

è¿å¨è·¯ä¸ï¼ç¨ç­...

如上圖所示,我們將原有的矩陣劃分爲4個區域,每個區域僅取其最大值做代表。

在最大池化中,有一組超參數需要進行調整。其中,f表示池化的大小,s表示步長。

池化前:n×n ;

池化後:

平均池化(Average Pooling)

平均池化與最大池化的唯一不同是其選取的是小區域內的均值來代表該區域內的值。

è¿å¨è·¯ä¸ï¼ç¨ç­...

 

總結而言

池化層的超參數:

PS.池化層中一般不用Padding,並且池化層沒有需要學習的參數。

八、基於Tensorflow的CNN實現

接下來,我們基於TensorFlow模型,來實現一個簡單CNN。在這個例子中,我們將用一個CNN來識別0-5的圖像:

上圖爲我們實現的數據集。

加載包

import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import scipy
from PIL import Image
from scipy import ndimage
import tensorflow as tf
from tensorflow.python.framework import ops
from cnn_utils import *

%matplotlib inline
np.random.seed(1)

# 下載數據集(signs)
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()

數據集

接下來,我們可以看一下該數據集的形狀:

X_train = X_train_orig/255.
X_test = X_test_orig/255.
Y_train = convert_to_one_hot(Y_train_orig, 6).T
Y_test = convert_to_one_hot(Y_test_orig, 6).T
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))
conv_layers = {}

輸出值爲:

number of training examples = 1080
number of test examples = 120
X_train shape: (1080, 64, 64, 3)
Y_train shape: (1080, 6)
X_test shape: (120, 64, 64, 3)
Y_test shape: (120, 6)

可以看出X爲(1080,64,64,3),對應1080個樣本,每個樣本形狀爲64×64×3。

1、創建佔位符placeholder

在TensorFlow模型中,我們需要爲輸入數據創建佔位符,並在Session中傳入模型中。

因爲我們在訓練、測試中,使用的數據量不同,所以我們可以將X,Y的維度設定爲[None, n_h0, n_w0, n_c0]和[None, n_y]。

def create_placeholders(n_H0, n_W0, n_C0, n_y):
     """
    Creates the placeholders for the tensorflow session.

    Arguments:
    n_H0 -- scalar, height of an input image
    n_W0 -- scalar, width of an input image
    n_C0 -- scalar, number of channels of the input
    n_y -- scalar, number of classes

    Returns:
    X -- placeholder for the data input, of shape [None, n_H0, n_W0, n_C0] and dtype "float"
    Y -- placeholder for the input labels, of shape [None, n_y] and dtype "float"
    """
    
    X = tf.placeholder(tf.float32, shape = [None, n_H0, n_W0, n_C0])
    Y = tf.placeholder(tf.float32, shape = [None, n_y])

    return X, Y

2、初始化參數

我們需要初始化filter(卷積核),在TensorFlow模型中,我們可以用

tf.contrib.layers.xavier_initializer(seed = 0)

對於b(偏置),TensorFlow模型會自動處理。

def initialize_parameters():
    """
    Initializes weight parameters to build a neural network with tensorflow. The shapes are:
                        W1 : [4, 4, 3, 8]
                        W2 : [2, 2, 8, 16]
    Returns:
    parameters -- a dictionary of tensors containing W1, W2
    """
    
    tf.set_random_seed() #爲了讓最終結果一致,自己實現時可以隨即設置

    W1 = tf.get_variable("W1", [4, 4, 3, 8], initlializer = tf.contrib.layers.xavier_initializer(seed = 0))
    W2 = tf.get_variable("W2", [2, 2, 8, 16], initlializer = tf.contrib.layers.xavier_initializer(seed = 0))

    parameters = {"W1": W1,
                  "W2": W2}

    return parameters

3、前向傳播

在TensorFlow模型中,最好的一點是我們可以使用內置函數來執行卷積步驟。

tf.nn.conv2d(X, W1, strides = [1, s, s, 1], padding = 'SAME'):其中X爲輸入值,W1爲filter,步長爲s,padding的類型是SAME卷積。

tf.nn.max_pool(A, ksize = [1, f, f, 1], strides = [1, s, s, 1], padding = 'SAME'):其中A爲輸入值,用窗口大小爲(f, f)和步長(s, s),在每個窗口上進行最大池化。

tf.nn.relu(Z1):計算Z1的ReLU,激活函數。

tf.contrib.layers.flatten(P):其中輸入爲P,將輸入展開爲一個一維向量,形狀爲[batch_size, k]。

tf.contrib.layers.fully_connected(F, num_outputs):給定一個一維向量F,返回使用全連接的層計算的輸出值。(模型會自定義初始值)

我們將構建如下模型:

CONV2D(卷積層) -> ReLU(激活函數) -> MAXPOOL(最大赤化層) -> CONV2D -> ReLU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

def forward_propagation(X, parameters):
     """
    Implements the forward propagation for the model:
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

    Arguments:
    X -- input dataset placeholder, of shape (input size, number of examples)
    parameters -- python dictionary containing your parameters "W1", "W2"
                  the shapes are given in initialize_parameters

    Returns:
    Z3 -- the output of the last LINEAR unit
    """

    W1 = parameters['W1']
    W2 = parameters['W2']

    #CONV2D: strides = 1, padding = 'SAME'
    Z1 = tf.nn.conv2d(X, W1, strides = [1, 1, 1, 1], padding = 'SAME')
    #ReLU
    A1 = tf.nn.relu(Z1)
    #MAXPOOL: windows = (8, 8),strides = 8, padding = 'SAME'
    P1 = tf.nn.max_pool(A1, ksize = [1, 8, 8, 1], strides = [1, 8, 8, 1], padding = 'SAME')
    # CONV2D: stride = 1, padding = 'SAME'
    Z2 = tf.nn.conv2d(P1, W2, strides=[1,1,1,1], padding='SAME')
    # ReLU
    A2 = tf.nn.relu(Z2)
    # MAXPOOL: window = (4, 4), stride = 4, padding = 'SAME'
    P2 = tf.nn.max_pool(A2, ksize=[1,4,4,1], strides=[1,4,4,1], padding='SAME')
    # FLATTEN
    P2 = tf.contrib.layers.flatten(P2)
    # FULLY-CONNECTED 6個結點,沒有激活函數
    Z3 = tf.contrib.layers.fully_connected(P2, 6, activation_fn=None)

    return Z3

4、計算成本

在CNN中,我們也使用代價函數,來優化模型。可以使用到如下的內置函數:

tf.nn.softmax_entropy_with_logits(logits = Z3, labels = Y):計算softmax的熵損失。即能計算softmax激活函數,也能計算損失函數,logits爲輸入值,labels爲真實樣本標籤。

tf.reduce_mean:計算元素均值,可以計算所有損失函數,可以得到代價函數。

def compute_cost(Z3, Y):
     """
    Computes the cost

    Arguments:
    Z3 -- output of forward propagation (output of the last LINEAR unit), of shape (6, number of examples)
    Y -- "true" labels vector placeholder, same shape as Z3

    Returns:
    cost - Tensor of the cost function
    """

    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = Z3, labels = Y))

    return cost

5、構建模型

模型應該實現如下功能:

  • 創建佔位符
  • 初始化參數
  • 向前傳播
  • 計算成本
  • 創建一個優化器

其中,在該模型中用到了random_mini_batches(),這個是小批量梯度下降法,思想是在每一次只用一個批量的數據來進行前向和反向的調整,這樣可以提高訓練速度。

def model(X_train, Y_train, X_test, Y_test, learning_rate = 0.009,
          num_epochs = 100, minibatch_size = 64, print_cost = True):
    """
    Implements a three-layer ConvNet in Tensorflow:
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

    Arguments:
    X_train -- training set, of shape (None, 64, 64, 3)
    Y_train -- test set, of shape (None, n_y = 6)
    X_test -- training set, of shape (None, 64, 64, 3)
    Y_test -- test set, of shape (None, n_y = 6)
    learning_rate -- learning rate of the optimization
    num_epochs -- number of epochs of the optimization loop
    minibatch_size -- size of a minibatch
    print_cost -- True to print the cost every 100 epochs

    Returns:
    train_accuracy -- real number, accuracy on the train set (X_train)
    test_accuracy -- real number, testing accuracy on the test set (X_test)
    parameters -- parameters learnt by the model. They can then be used to predict.
    """

    ops.reset_default_graph()                        # 可以讓我們在每次調用模型時,初始化參數
    tf.set_random_seed(1)                            # 確保輸出值,與我們提供的樣例一致
    seed = 3                                         
    (m, n_H0, n_W0, n_C0) = X_train.shape             
    n_y = Y_train.shape[1]                            
    costs = []                                       # 保存cost的緩存

    #創建placeholder
    X, Y = create_placeholder(n_H0, n_W0, n_C0, n_y)

    #初始化參數
    parameters = initialize_parameters()

    #前向傳播
    Z3 = forward_propagation(X, parameters)

    #計算損失函數
    cost = compute_cost(Z3)

    #BP算法
    optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)

    #初始化所有張量,運行計算圖
    init = tf.global_variables_initializer()
    with tf.Session() as sess:
        sess.run(init)
        #循環訓練CNN
        for epoch in range(num_epochs):
            
            minibatch_cost = 0.
            num_minibatches = int(m / minibatch_size)
            seed = seed + 1 #每次minibatch都有不同的seed,保證每次訓練樣本順序都不一致
            minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)
            
            for minibatch in minibatches:
                (minibatch_X, minibatch_Y) = minibatch
                
                _, temp_cost = sess.run([optimizer, cost], feed_dict = {X: minibatch_X, minibatch_Y})

                minibatch_cost += temp_cost / num_minibatches
 
            # 每5次打印出損失函數
            if print_cost == True and epoch % 5 == 0:
                print ("Cost after epoch %i: %f" % (epoch, minibatch_cost))
            if print_cost == True and epoch % 1 == 0:
                costs.append(minibatch_cost)

        # 畫出損失函數的圖像
        plt.plot(np.squeeze(costs))
        plt.ylabel('cost')
        plt.xlabel('iterations (per tens)')
        plt.title("Learning rate =" + str(learning_rate))
        plt.show()

        # 預測函數
        predict_op = tf.argmax(Z3, 1)
        correct_prediction = tf.equal(predict_op, tf.argmax(Y, 1))

        # 測試集的效果計算
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        print(accuracy)
        train_accuracy = accuracy.eval({X: X_train, Y: Y_train})
        test_accuracy = accuracy.eval({X: X_test, Y: Y_test})
        print("Train Accuracy:", train_accuracy)
        print("Test Accuracy:", test_accuracy)

        return train_accuracy, test_accuracy, parameters

 接下來,如果一切都工作正常,我們會得到如下結果:

我們可以看到損失一直在下降,損失圖像如下:

總結而言:

卷積神經網絡(CNN)實際上,是在輸入後,先對輸入值進行提取特徵,並對特徵進行處理(池化)。這個過程不僅提取了過程,也讓特徵值更顯著,更容易被我們學習,之後再進行全連接,得到我們的輸出值。CNN對比普通NN而言,減少了參數個數,加快了訓練速度,與此同時也提高了識別效果。

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