1. 程式人生 > >第五章(1.5)深度學習——卷積神經網路簡介

第五章(1.5)深度學習——卷積神經網路簡介

卷積神經網路(Convolutional Neural Network, CNN)是一種前饋神經網路, 在計算機視覺等領域被廣泛應用. 本文將簡單介紹其原理並分析Tensorflow官方提供的示例.

一、工作原理

卷積是影象處理中一種基本方法. 卷積核是一個nxn的矩陣通常n取奇數, 這樣矩陣就有了中心點和半徑的概念.

對影象中每個點取以其為中心的n階方陣, 將該方陣與卷積核中對應位置的值相乘, 並用它們的和作為結果矩陣中對應點的值.

下面的動圖展示了卷積的計算過程:
這裡寫圖片描述

上述操作處理影象得到新影象的操作稱為卷積, 卷積得到的結果矩陣被稱為特徵圖(Feature Map). 灰度圖使用一個矩陣便能表示, RGB

影象則需要3個矩陣. 也就是說, 1個RGB影象使用一個卷積核卷積會得到3個Feature Map.

若卷積核中各元素和為1則影象亮度不變, 若小於1則變暗, 大於1則會變亮.

卷積核的中心無法對準原影象中邊緣的畫素點(與邊緣距離小於卷積核半徑), 若要對邊緣的點進行計算必須填充(padding)外部缺少的點使卷積核的中心可以對準它們. 常用的填充策略有:

  • 使用中心點的值代替缺失的點
  • 使用中心點鄰域的均值代替缺失的點
  • 填充為0
  • 特殊的卷積核可以實現特殊的效果:
  • 銳化
    這裡寫圖片描述
  • 提取邊緣
    這裡寫圖片描述
  • 浮雕
    這裡寫圖片描述

下面四張圖片分別為:
- A: 原圖
- B: 銳化
- C: 邊緣檢測
- D: 浮雕

這裡寫圖片描述

二、區域性感知
一般認為人的視覺認知是從區域性到全域性的,而影象的空間聯絡也是區域性的畫素聯絡較為緊密,而距離較遠的畫素相關性則較弱.

同理, 每個神經元其實沒有必要對全域性影象進行感知, 只需要與區域性影象建立連線. 在網路的更深層將神經元的區域性感知進一步綜合就可以瞭解到全域性資訊.

採用區域性感知的方法減少了需要訓練的權值數. 在實際應用中影象的解析度和訓練迭代次數都是有限的, 更少的權值數通常會帶來更高精度.

這裡寫圖片描述

三、權值共享

在卷積神經網路中對於同一個卷積核, 所有卷積層神經元和影象輸入層的連線使用同一個權值矩陣.

權值共享進一步減少了所需訓練的權值數, 一個卷積層的權值數變為了卷積核中元素個數.

權值共享隱含的原理是: 影象的一部分的統計特性與其他部分是一樣的, 在影象某一部分學習到的特徵也能應用到其它部分上.

從上文關於特殊卷積核的描述中可以得知, 一種卷積核通常只能提取影象中的一種特徵. 且權值共享使得連線可以訓練的權值數大為減少. 為了充分提取特徵通常採用使用多個卷積核的方法.

四、池化

通過卷積學習到的影象特徵仍然數量巨大, 不便直接進行分類. 池化層便用於減少特徵數量.

池化操作非常簡單, 比如我們使用一個卷積核對一張圖片進行過濾得到一個8x8的方陣, 我們可以將方陣劃分為16個2x2方陣, 每個小方陣稱為鄰域.

用16個小方陣的均值組成一個4x4方陣便是均值池化, 類似地還有最大值池化等操作. 均值池化對保留背景等特徵較好, 最大值池化對紋理提取更好.

隨機池化則是根據畫素點數值大小賦予概率(權值), 然後按其加權求和.

這裡寫圖片描述

五、TensorFlow實現

TensorFlow的文件Deep MNIST for Experts介紹了使用CNNMNIST資料集上識別手寫數字的方法.

完整程式碼可以在GitHub上找到, 本文將對其進行簡單分析. 原始碼來自tensorflow-1.3.0版本示例.

主要有3條引入:

import tempfilefrom tensorflow.examples.tutorials.mnist import input_dataimport tensorflow as tf
main(_)函式負責網路的構建:

def main(_):
  # 匯入MNIST資料集 
  # FLAGS.data_dir是本地資料的路徑, 可以用空字串代替以自動下載資料集
  mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)
  # x是輸入層, 每個28x28的影象被展開為784階向量
  x = tf.placeholder(tf.float32, [None, 784])
  # y_是訓練集預標註好的結果, 採用one-hot的方法表示10種分類
  y_ = tf.placeholder(tf.float32, [None, 10])
  # deepnn方法構建了一個cnn, y_conv是cnn的預測輸出
  # keep_prob是dropout層的引數, 下文再講
  y_conv, keep_prob = deepnn(x)
  # 計算預測y_conv和標籤y_的交叉熵作為損失函式
  with tf.name_scope('loss'):
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_,
                                                            logits=y_conv)
  cross_entropy = tf.reduce_mean(cross_entropy)
  # 使用Adam優化演算法, 以最小化損失函式為目標
  with tf.name_scope('adam_optimizer'):
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)  # 計算精確度(正確分類的樣本數佔測試樣本數的比例), 用於評估模型效果
  with tf.name_scope('accuracy'):
    correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
    correct_prediction = tf.cast(correct_prediction, tf.float32)
  accuracy = tf.reduce_mean(correct_prediction)
main函式與其它tensorflow神經網路並無二致, 關鍵分析deepnn方法如何構建cnn:

def deepnn(x):
  # x的結構為[n, 784], 將其展開成[n, 28, 28]
  # 第四維表示影象的特徵, 當前為灰度圖故為1. 也就是說每個畫素點需要一個值來描述
  # 類似地, RGB影象為3, RDBA影象為4
  with tf.name_scope('reshape'):
    x_image = tf.reshape(x, [-1, 28, 28, 1])
  # 第一個卷積層將28x28灰度圖使用32個卷積核進行卷積
  with tf.name_scope('conv1'):
    # 初始化連線權值, 為了避免梯度消失權值使用正則分佈進行初始化
    # 使用5x5大小的卷積核, 使用32個卷積核, 從原圖中提取出32個特徵(產生32個Feature-Map)
    W_conv1 = weight_variable([5, 5, 1, 32])    
    # 初始化偏置值, 這裡使用的是0.1
    b_conv1 = bias_variable([32])    
    # conv2d實現: tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
    # strides是卷積核移動的步幅
    # padding有兩個取值: SAME:表示卷積之後Feature-Map的長寬與x_image相同; VALID則表示忽略邊緣畫素, Feature-Map比x_image小
    # h_conv1的結構為[n, 28, 28, 32]
    h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
  # 第一個池化層, 將2x2方陣最大值池化為一個特徵, 池化為14x14矩陣
  with tf.name_scope('pool1'):
    h_pool1 = max_pool_2x2(h_conv1)
  # 第二個卷積層, 將第一個卷積層提取32個特徵使用64個卷積核提取64個特徵
  with tf.name_scope('conv2'):
    # 這裡的卷積核是3維的, 有32個5*5的二維卷積核, 每個二維卷積核與一個14x14Feature-Map進行卷積
    # 將這32個14x14結果矩陣累加起來便得到一個新的Feature-Map
    # 64個三維卷積核得到64個新Feature-Map
    W_conv2 = weight_variable([5, 5, 32, 64])
    b_conv2 = bias_variable([64])
    # h_conv2的結構為[n, 14, 14, 64]
    h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
  # 第二個池化層, 將2x2方陣最大值池化為一個特徵, 池化為7x7矩陣
  with tf.name_scope('pool2'):
    # h_pool2的結構為[n, 7, 7, 64]
    h_pool2 = max_pool_2x2(h_conv2)
  # 第一個全連線層, 將[7, 7, 64]特徵矩陣用全連線層對映到1024各特徵
  with tf.name_scope('fc1'):
    W_fc1 = weight_variable([7 * 7 * 64, 1024])
    b_fc1 = bias_variable([1024])
    h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
  # 使用dropout層避免過擬合
  # 即在訓練過程中的一次迭代中, 隨機選擇一定比例的神經元不參與此次迭代
  # 參與迭代的概率值由keep_prob指定, keep_prob=1.0為使用整個網路
  with tf.name_scope('dropout'):
    keep_prob = tf.placeholder(tf.float32)
    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
  # 第二個全連線層, 將1024個特徵對映到10個特徵, 即10個分類的one-hot編碼
  # one-hot編碼是指用 `100`代替1, `010`代替2, `001`代替3... 的編碼方式
  with tf.name_scope('fc2'):
    W_fc2 = weight_variable([1024, 10])
    b_fc2 = bias_variable([10])
    y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2    
  return y_conv, keep_prob
請重點關注第二個卷積層的實現

整個網路暴露的介面有3個:

輸入層x[n, 784]
輸出層y_conv[n, 10]
dropout保留比例keep_prob[1]
現在可以繼續關注main方法了, 完成網路構建之後main先將網路結構快取到硬碟:

graph_location = tempfile.mkdtemp()print('Saving graph to: %s' % graph_location)train_writer = tf.summary.FileWriter(graph_location)train_writer.add_graph(tf.get_default_graph())
接下來初始化tf.Session()進行訓練:

with tf.Session() as sess:    
     # 初始化全域性變數
    sess.run(tf.global_variables_initializer())    for i in range(10000):        
        # 每次取訓練資料集中50個樣本, 分10000次取出
        # batch[0]為特徵集, 結構為[50, 784]即50組784階向量
        # batch[1]為標籤集, 結構為[50, 10]即50個採用one-hot編碼的標籤
        batch = mnist.train.next_batch(50)
        # 每進行100次迭代評估一次精度
        if i % 100 == 0:
            train_accuracy = accuracy.eval(feed_dict={
                x: batch[0], y_: batch[1], keep_prob: 1.0})            print('step %d, training accuracy %g' % (i, train_accuracy))        # 進行訓練, dropout keep prob設為0.5
        train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
            # 評估最終精度, dropout keep prob設為1.0即使用全部網路
    print('test accuracy %g' % accuracy.eval(feed_dict={
        x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
啟動程式碼會處理命令列引數和選項:

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--data_dir', type=str,
                        default='/tmp/tensorflow/mnist/input_data',                        help='Directory for storing input data')
    FLAGS, unparsed = parser.parse_known_args()
    tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)