卷积神经网络(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)外部缺乏的点使卷积核的中心能够对准它们. 经常使用的填充策略有:
此外还有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的文档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]
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)