卷積神經網絡(Convolutional Neural Network, CNN)是一種前饋神經網絡, 在計算機視覺等領域被廣泛應用. 本文將簡單介紹其原理並分析Tensorflow官方提供的示例.
關於神經網絡與誤差反向傳播的原理可以參考作者的另一篇博文BP神經網絡與Python實現.
了解卷積神經網絡
什么是卷積
卷積是圖像處理中一種基本方法. 卷積核是一個f*f
的矩陣. 通常n取奇數,使得卷積核有中心點.
對圖像中每個點取以其為中心的f
階方陣, 將該方陣中各值與卷積核中對應位置的值相乘, 並用它們的和作為結果矩陣中對應點的值.
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).
每個卷積核可以包含一個偏置參數b, 即對卷積結果的每一個元素都加b作為輸出的特征圖.
邊緣檢測是卷積的一種典型應用, 人眼所見的邊緣是圖像中不同區域的分界線. 分界線兩側的色彩或灰度通常有着較大的不同.
下面我們使用一個非常簡單的示例來展示邊緣檢測過程. 第一個6*6
的矩陣是灰度圖, 顯然圖像左側較亮右側較暗, 中間形成了一條明顯的垂直邊緣.
在特征圖中央有一條垂直亮線(第2,3列), 即原圖的垂直邊緣. 類似的可以檢測縱向邊緣:
卷積核的中心無法對准原圖像中邊緣的像素點(與邊緣距離小於卷積核半徑), 若要對邊緣的點進行計算必須填充(padding)外部缺少的點使卷積核的中心可以對准它們. 常用的填充策略有:
- SAME: 使用附近點的值代替缺失的點, 可以保證特征圖不會變小
- VALID: 只對可用的位置進行卷積(不進行填充), 但特征圖會變小
此外還有0值填充, 均值填充等方法. 通常用p來描述填充的寬度.
SAME填充效果, 4*4
矩陣被填充為6*6
矩陣, 填充寬度p=1
:
對於n*n
的矩陣, 使用f*f
的核進行卷積, 填充寬度為p
, 若縱向步幅為s1
, 橫向步幅為s2
則特征圖的行列數為:
三維卷積
灰度圖所能描述的信息的極為有限, 我們更多地處理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)