Tensorflow卷積神經網絡

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

關於神經網絡與偏差反向傳播的原理能夠參考做者的另外一篇博文BP神經網絡與Python實現.python

瞭解卷積神經網絡

什麼是卷積

卷積是圖像處理中一種基本方法. 卷積核是一個f*f的矩陣. 一般n取奇數,使得卷積核有中心點.git

對圖像中每一個點取以其爲中心的f階方陣, 將該方陣中各值與卷積核中對應位置的值相乘, 並用它們的和做爲結果矩陣中對應點的值.github

1*1 + 1*0 + 1*1 + 0*0 + 1*1 + 1*0 + 0*1 + 0*0 + 1*1 = 4算法

卷積核每次向右移動1列, 遇行末向下移動1列直到完成全部計算. 咱們把每次移動的距離稱爲步幅s.緩存

上述操做處理圖像獲得新圖像的操做稱爲卷積, 在圖像處理中卷積核也被稱爲過濾器(filter).網絡

卷積獲得的結果矩陣一般用於表示原圖的某種特徵(如邊緣), 所以卷積結果被稱爲特徵圖(Feature Map).app

每一個卷積核能夠包含一個偏置參數b, 即對卷積結果的每個元素都加b做爲輸出的特徵圖.ide

邊緣檢測是卷積的一種典型應用, 人眼所見的邊緣是圖像中不一樣區域的分界線. 分界線兩側的色彩或灰度一般有着較大的不一樣.函數

下面咱們使用一個很是簡單的示例來展現邊緣檢測過程. 第一個6*6的矩陣是灰度圖, 顯然圖像左側較亮右側較暗, 中間造成了一條明顯的垂直邊緣.

在特徵圖中央有一條垂直亮線(第2,3列), 即原圖的垂直邊緣. 相似的能夠檢測縱向邊緣:

卷積核的中心沒法對準原圖像中邊緣的像素點(與邊緣距離小於卷積核半徑), 若要對邊緣的點進行計算必須填充(padding)外部缺乏的點使卷積核的中心能夠對準它們. 經常使用的填充策略有:

  • SAME: 使用附近點的值代替缺失的點, 能夠保證特徵圖不會變小
  • VALID: 只對可用的位置進行卷積(不進行填充), 但特徵圖會變小

此外還有0值填充, 均值填充等方法. 一般用p來描述填充的寬度.

SAME填充效果, 4*4矩陣被填充爲6*6矩陣, 填充寬度p=1:

對於n*n的矩陣, 使用f*f的核進行卷積, 填充寬度爲p, 若縱向步幅爲s1, 橫向步幅爲s2則特徵圖的行列數爲:

\[ [ \frac{n + 2*p - f }{s1} + 1 ] \times [ \frac{n + 2*p - f }{s2} + 1 ] \]

三維卷積

灰度圖所能描述的信息的極爲有限, 咱們更多地處理RGB圖像. RGB圖像須要3個矩陣才能描述圖片, 咱們稱爲3個通道(channel).

如下圖6*6的RGB圖爲例, 3個矩陣分別與黃色卷積核進行卷積獲得3個4*4特徵圖, 將3個特徵圖同位置的值疊加獲得最終的卷積結果.

在邊緣檢測中咱們注意到, 一個卷積核一般只能提取圖像一種特徵如水平邊緣或垂直邊緣. 爲了提取圖像的多個特徵, 咱們一般使用多個卷積核.

咱們使用高維矩陣來描述這一過程, RGB圖像爲6*6*3矩陣, 兩個卷積核疊加爲3*3*2矩陣, 兩個特徵圖疊加爲4*4*2矩陣. 輸入, 輸出和卷積核均使用三維矩陣來表示, 這樣咱們能夠方便的級聯多個卷積層.

爲何使用卷積

在上一節中咱們已經介紹了一個卷積層如何工做的, 如今咱們來探討爲何使用卷積提取圖像特徵.

首先分析卷積層的輸入輸出, 每一個卷積層輸入了一個w1 * h1 * c1 的三維矩陣, 輸出w2 * h2 *c2的三維矩陣.

若使用全鏈接層須要(w1 * h1 * c1) * (w2 * h2 *c2)個參數, 卷積層只須要訓練c2個二維卷積核中的f1 * f1 * c2個參數和c2個偏置值, 可見卷積層極大地減小了參數的數量.

更少的參數對於訓練數據和計算資源都有限的任務而言, 一般意味着更高的精度和更好的訓練效率.

更重要的是, 卷積針對小塊區域而不是單個像素進行處理, 更好地從空間分佈中提取特徵, 這與人類視覺是相似的. 而全鏈接層嚴重忽略了空間分佈所包含的信息.

特徵圖中一個像素只與輸入矩陣中f * f個像素有關, 這種性質被稱爲局部感知. 一個卷積核用於生成特徵圖中全部像素, 該特性被稱爲權值共享.

池化

經過卷積學習到的圖像特徵仍然數量巨大, 不便直接進行分類. 池化層便用於減小特徵數量.

池化操做很是簡單, 好比咱們使用一個卷積覈對一張圖片進行過濾獲得一個8x8的方陣, 咱們能夠將方陣劃分爲16個2x2方陣, 每一個小方陣稱爲鄰域.

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

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

池化操做用於減小圖的寬度和高度, 但不能減小通道數.

1*1*c2的核進行卷積可使w1 * h1 * c1的輸入矩陣映射到w1 * h1 * c2的輸出矩陣. 即對各通道輸出加權求和實現減小通道數的效果.

TensorFlow實現

TensorFlow的文檔Deep MNIST for Experts介紹了使用CNN在MNIST數據集上識別手寫數字的方法., 該示例採用了LeNet5模型.

完整代碼能夠在GitHub上找到, 本文將對其進行簡單分析. 源碼來自tensorflow-1.3.0版本示例.

主要有3條引入:

import tempfile
from tensorflow.examples.tutorials.mnist import input_data
import 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]
  # 灰度圖只有一個通道, x_image第四維爲1
  # x_image的四維分別是[n_sample, width, height, channel]
  with tf.name_scope('reshape'):
    x_image = tf.reshape(x, [-1, 28, 28, 1])

  # 第一個卷積層, 將28x28*1灰度圖使用5*5*32核進行卷積
  with tf.name_scope('conv1'):
    # 初始化鏈接權值, 爲了不梯度消失權值使用正則分佈進行初始化
    W_conv1 = weight_variable([5, 5, 1, 32])
    
    # 初始化偏置值, 這裏使用的是0.1
    b_conv1 = bias_variable([32])
    
    # strides是卷積核移動的步幅. 採用SAME策略填充, 即便用相同值填充
    # def conv2d(x, W):
    #   tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
    
    # h_conv1的結構爲[n, 28, 28, 32]
    h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

  # 第一個池化層, 2*2最大值池化, 獲得14*14矩陣
  with tf.name_scope('pool1'):
    h_pool1 = max_pool_2x2(h_conv1)

  # 第二個卷積層, 將28*28*32特徵圖使用5*5*64核進行卷積
  with tf.name_scope('conv2'):
    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)

  # 第二個池化層, 2*2最大值池化, 獲得7*7矩陣
  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)
相關文章
相關標籤/搜索