本文大部份内容均参考于:python
An Intuitive Explanation of Convolutional Neural Networks
知乎:「为何 ReLU 要好过于 tanh 和 sigmoid function?」
Deep MNIST for Experts
TensorFlow Python API 「tf.nn」git
在 TensorFlow 官方的 tutorials 中,咱们使用 softmax 模型在 MNIST 数据集上获得的结果只有 91% 的正确率,实在太糟糕。因此,咱们将使用一个稍微复杂的模型:CNN(卷积神经网络)来改善实验效果。在 CNN 主要有四个操做:github
这些操做对于各个卷积神经网络来讲都是基本组件,所以理解它们的工做原理有助于充分了解卷积神经网络。下面咱们将会尝试理解各步操做背后的原理。web
卷积的主要目的是为了从输入图像中提取特征。卷积能够经过从输入的一小块数据中学到图像的特征,并能够保留像素间的空间关系。让咱们举个例子来尝试理解一下卷积是如何处理图像的:算法
正如咱们上面所说,每张图像均可以看做是像素值的矩阵。考虑一下一个 5 x 5 的图像,它的像素值仅为 0 或者 1(注意对于灰度图像而言,像素值的范围是 0 到 255,下面像素值为 0 和 1 的绿色矩阵仅为特例):api
同时,考虑下另外一个 3 x 3 的矩阵,以下所示:数组
接下来,5 x 5 的图像和 3 x 3 的矩阵的卷积能够按下图所示的动画同样计算:网络
如今停下来好好理解下上面的计算是怎么完成的。咱们用橙色的矩阵在原始图像(绿色)上滑动,每次滑动一个像素(也叫作「步长」),在每一个位置上,咱们计算对应元素的乘积(两个矩阵间),并把乘积的和做为最后的结果,获得输出矩阵(粉色)中的每个元素的值。注意,3 x 3 的矩阵每次步长中仅能够看到输入图像的一部分。架构
在 CNN 的术语中,3x3 的矩阵叫作「滤波器」(filter) 或「核」(kernel) 或者 「特征检测器」(feature detector),经过在图像上滑动滤波器并计算点乘获得矩阵叫作「卷积特征」(Convolved Feature) 或者 「激活图」(Activation Map) 或者 「特征图」(Feature Map)。记住,滤波器在原始输入图像上的做用是特征检测器。dom
从上面图中的动画能够看出,对于一样的输入图像,不一样值的滤波器将会生成不一样的特征图。好比,对于下面这张输入图像:
在下表中,咱们能够看到不一样滤波器对上图卷积的效果。正如表中所示,经过在卷积操做前修改滤波矩阵的数值,咱们能够进行诸如边缘检测、锐化和模糊等操做 —— 这代表不一样的滤波器能够从图中检测到不一样的特征,好比边缘、曲线等。
另外一个直观理解卷积操做的好方法是看下面这张图的动画:
滤波器(红色框)在输入图像滑过(卷积操做),生成一个特征图。另外一个滤波器(绿色框)在同一张图像上卷积能够获得一个不一样的特征图。注意卷积操做能够从原图上获取局部依赖信息。同时注意这两个不一样的滤波器是如何从同一张图像上生成不一样的特征图。记住上面的图像和两个滤波器仅仅是咱们上面讨论的数值矩阵。
在实践中,CNN 会在训练过程当中学习到这些滤波器的值(尽管咱们依然须要在训练前指定诸如滤波器的个数、滤波器的大小、网络架构等参数)。咱们使用的滤波器越多,提取到的图像特征就越多,网络所能在未知图像上识别的模式也就越好。
特征图的大小(卷积特征)由下面三个参数控制,咱们须要在卷积前肯定它们:
深度(Depth):深度对应的是卷积操做所需的滤波器个数。在下图的网络中,咱们使用三个不一样的滤波器对原始图像进行卷积操做,这样就能够生成三个不一样的特征图。你能够把这三个特征图看做是堆叠的 2d 矩阵,那么,特征图的「深度」就是 3。
步长(Stride):步长是咱们在输入矩阵上滑动滤波矩阵的像素数。当步长为 1 时,咱们每次移动滤波器一个像素的位置。当步长为 2 时,咱们每次移动滤波器会跳过 2 个像素。步长越大,将会获得更小的特征图。
零填充(Zero-padding):有时,在输入矩阵的边缘使用零值进行填充,这样咱们就能够对输入图像矩阵的边缘进行滤波。零填充的一大好处是可让咱们控制特征图的大小。使用零填充的也叫作泛卷积,不适用零填充的叫作严格卷积。
ReLU表示修正线性单元(Rectified Linear Unit),是一个非线性操做。
为何要引入非线性激励函数?
若是不用激励函数(其实至关于激励函数是 $f(x) = x$ ),在这种状况下你每一层输出都是上层输入的线性函数,很容易验证,不管你神经网络有多少层,输出都是输入的线性组合,与没有隐层效果至关,这种状况就是最原始的感知机(Perceptron)了。
正由于上面的缘由,咱们决定引入非线性函数做为激励函数,这样深层神经网络就有意义了(再也不是输入的线性组合,能够逼近任意函数)。最先的想法是 sigmoid 函数或者 tanh 函数,输出有界,很容易充当下一层输入(以及一些人的生物解释balabala)。
为何要引入 ReLU 而不是其余的非线性函数(例如 Sigmoid 函数)?
固然如今也有一些对 relu 的改进,好比 prelu,random relu等,在不一样的数据集上会有一些训练速度上或者准确率上的改进,具体的能够找相关的paper看。
(多加一句,如今主流的作法,会多作一步 batch normalization,尽量保证每一层网络的输入具备相同的分布。而最新的 paper,他们在加入bypass connection 以后,发现改变 batch normalization 的位置会有更好的效果。)
ReLU 的优势与缺点?
优势:
缺点:
几十年的机器学习发展中,咱们造成了这样一个概念:非线性激活函数要比线性激活函数更加先进。
尤为是在布满 Sigmoid 函数的 BP 神经网络,布满径向基函数的 SVM 神经网络中,每每有这样的幻觉,非线性函数对非线性网络贡献巨大。
该幻觉在 SVM 中更加严重。核函数的形式并不是彻底是 SVM 可以处理非线性数据的主力功臣(支持向量充当着隐层角色)。
那么在深度网络中,对非线性的依赖程度就能够缩一缩。另外,在上一部分提到,稀疏特征并不须要网络具备很强的处理线性不可分机制。
综合以上两点,在深度学习模型中,使用简单、速度快的线性激活函数可能更为合适。
ReLU 操做能够从下面的图中理解。这里的输出特征图也能够看做是“修正”过的特征图。
所谓麻雀虽小,五脏俱全,ReLU虽小,但也是能够改进的。
ReLU的区分主要在负数端,根据负数端斜率的不一样来进行区分,大体以下图所示。
普通的ReLU负数端斜率是0,Leaky ReLU则是负数端有一个比较小的斜率,而PReLU则是在后向传播中学习到斜率。而Randomized Leaky ReLU则是使用一个均匀分布在训练的时候随机生成斜率,在测试的时候使用均值斜率来计算。
其中,NDSB 数据集是 Kaggle 的比赛,而 RReLU 正是在此次比赛中崭露头角的。
经过上述结果,能够看到四点:
[1]. Xu B, Wang N, Chen T, et al. Empirical evaluation of rectified activations in convolutional network[J]. arXiv preprint arXiv:1505.00853, 2015.
空间池化(Spatial Pooling)(也叫作亚采用或者下采样)下降了各个特征图的维度,但能够保持大部分重要的信息。空间池化有下面几种方式:最大化、平均化、加和等等。
对于最大池化(Max Pooling),咱们定义一个空间邻域(好比,2x2 的窗口),并从窗口内的修正特征图中取出最大的元素。除了取最大元素,咱们也能够取平均(Average Pooling)或者对窗口内的元素求和。在实际中,最大池化被证实效果更好一些。
下面的图展现了使用 2x2 窗口在修正特征图(在卷积 + ReLU 操做后获得)使用最大池化的例子。
咱们以 2 个元素(也叫作“步长”)滑动咱们 2x2 的窗口,并在每一个区域内取最大值。如上图所示,这样操做能够下降咱们特征图的维度。
在下图展现的网络中,池化操做是分开应用到各个特征图的(注意,由于这样的操做,咱们能够从三个输入图中获得三个输出图)。
下图展现了咱们在 ReLU 操做以后获得的修正特征图的池化操做的效果:
池化函数能够逐渐下降输入表示的空间尺度。特别地,Pooling 的好处是:
使输入表示(特征维度)变得更小,而且网络中的参数和计算的数量更加可控的减少,所以,能够控制过拟合。
使网络对于输入图像中更小的变化、冗余和变换变得不变性(输入的微小冗余将不会改变池化的输出——由于咱们在局部邻域中使用了最大化/平均值的操做)。
帮助咱们获取图像最大程度上的尺度不变性(准确的词是“不变性”)。它很是的强大,由于咱们能够检测图像中的物体,不管它们位置在哪里。
到目前为止咱们了解了卷积、ReLU 和池化是如何操做的。理解这些层是构建任意 CNN 的基础是很重要的。正以下图所示,咱们有两组卷积、ReLU & 池化层 —— 第二组卷积层使用六个滤波器对第一组的池化层的输出继续卷积,获得一共六个特征图。接下来对全部六个特征图应用 ReLU。接着咱们对六个修正特征图分别进行最大池化操做。
这些层一块儿就能够从图像中提取有用的特征,并在网络中引入非线性,减小特征维度,同时保持这些特征具备某种程度上的尺度变化不变性。
第二组池化层的输出做为全链接层的输入,接下来咱们将介绍全链接层。
全链接层是传统的多层感知器,在输出层使用的是 softmax 激活函数(也可使用其余像 SVM 的分类器,但在本文中只使用 softmax)。「全链接」(Fully Connected) 这个词代表前面层的全部神经元都与下一层的全部神经元链接。
卷积和池化层的输出表示了输入图像的高级特征。全链接层的目的是为了使用这些特征把输入图像基于训练数据集进行分类。好比,在下面图中咱们进行的图像分类有四个可能的输出结果(注意下图并无显示全链接层的节点链接)。
除了分类,添加一个全链接层也(通常)是学习这些特征的非线性组合的简单方法。从卷积和池化层获得的大多数特征可能对分类任务有效,但这些特征的组合可能会更好。
从全链接层获得的输出几率和为 1。这个能够在输出层使用 softmax 做为激活函数进行保证。softmax 函数输入一个任意大于 0 值的矢量,并把它们转换为零一之间的数值矢量,其和为一。
正如上面讨论的,卷积 + 池化层的做用是从输入图像中提取特征,而全链接层的做用是分类器。
注意在下面的图中,由于输入的图像是船,对于船这一类的目标几率是 1,而其余三类的目标几率是 0,即
输入图像 = 船
目标矢量 = [0, 0, 1, 0]
完整的卷积网络的训练过程能够总结以下:
上面的这些步骤能够训练 ConvNet —— 这本质上意味着对于训练数据集中的图像,ConvNet 在更新了全部权重和参数后,已经优化为能够对这些图像进行正确分类。
当一张新的(未见过的)图像做为 ConvNet 的输入,网络将会再次进行前向传播过程,并输出各个类别的几率(对于新的图像,输出几率是使用已经在前面训练样本上优化分类的参数进行计算的)。若是咱们的训练数据集很是的大,网络将会(有但愿)对新的图像有很好的泛化,并把它们分到正确的类别中去。
注 1: 上面的步骤已经简化,也避免了数学详情,只为提供训练过程的直观内容。
注 2:在上面的例子中咱们使用了两组卷积和池化层。然而请记住,这些操做能够在一个 ConvNet 中重复屡次。实际上,如今有些表现最好的 ConvNet 拥有多达十几层的卷积和池化层!同时,每次卷积层后面不必定要有池化层。以下图所示,咱们能够在池化操做前连续使用多个卷积 + ReLU 操做。还有,请注意 ConvNet 的各层在下图中是如何可视化的。
通常而言,越多的卷积步骤,网络能够学到的识别特征就越复杂。好比,ConvNet 的图像分类可能在第一层从原始像素中检测出边缘,而后在第二层使用边缘检测简单的形状,接着使用这些形状检测更高级的特征,好比更高层的人脸。下面的图中展现了这些内容——咱们使用卷积深度置信网络学习到的特征,这张图仅仅是用来证实上面的内容(这仅仅是一个例子:真正的卷积滤波器可能会检测到对咱们毫无心义的物体)。
Adam Harley 建立了一个卷积神经网络的可视化结果,使用的是 MNIST 手写数字的训练集。我强烈建议使用它来理解 CNN 的工做原理。
咱们能够在下图中看到网络是如何识别输入 「8」 的。注意下图中的可视化并无单独展现 ReLU 操做。
输入图像包含 1024 个像素(32 x 32 大小),第一个卷积层(卷积层 1)由六个独特的 5x5 (步长为 1)的滤波器组成。如图可见,使用六个不一样的滤波器获得一个深度为六的特征图。
卷积层 1 后面是池化层 1,在卷积层 1 获得的六个特征图上分别进行 2x2 的最大池化(步长为 2)的操做。你能够在池化层上把鼠标移动到任意的像素上,观察在前面卷积层(如上图所示)获得的 4x4 的小格。你会发现 4x4 小格中的最大值(最亮)的像素将会进入到池化层。
池化层 1 后面的是六个 5x5 (步长为 1)的卷积滤波器,进行卷积操做。后面就是池化层 2,进行 2x2 的最大池化(步长为 2)的操做。这两层的概念和前面描述的同样。
接下来咱们就到了三个全链接层。它们是:
注意在下图中,输出层中的 10 个节点的各个都与第二个全链接层的全部 100 个节点相连(所以叫作全链接)。
同时,注意在输出层那个惟一的亮的节点是如何对应于数字 “8” 的——这代表网络把咱们的手写数字正确分类(越亮的节点代表从它获得的输出值越高,即,8 是全部数字中几率最高的)。
一样的 3D 可视化能够在这里看到。
卷积神经网络从上世纪 90 年代初期开始出现。咱们上面提到的 LeNet 是早期卷积神经网络之一。其余有必定影响力的架构以下所示:
Tensorflow 在卷积和池化上有很强的灵活性。咱们改如何处理边界?步长应该设多大?在这个实例里,咱们会一直使用 vanilla 版本。咱们的卷积网络选用步长(stride size)为 1,边距(padding size)为 0 的模板,保证输出和输入是同一个大小(严格卷积)。咱们的池化选用简单传统的 $2 \times 2$ 大小的模板做为 max pooling(最大池化)。为了使代码更简洁,咱们把这部分抽象成一个函数:
def conv2d(x, W): return tf.nn.conv2d(x, W, strides = [1, 1, 1, 1], padding = 'SAME') def max_pool_2x2(x): return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME')
卷积操做是使用一个二维的卷积核在一个批处理的图片上进行不断扫描。具体操做就是将一个卷积和在每张图片上按照一个合适的尺寸在每一个通道上面进行扫描。为了达到更好的卷积效率,须要在不一样的通道和不一样的卷积核之间进行权衡。
conv2d
:任意的卷积核,能同时在不一样的通道上面进行卷积操做。depthwise_conv2d
:卷积核能相互独立地在本身的通道上面进行卷积操做。separable_conv2d
:在纵深卷积 depthwise filter
以后进行逐点卷积 separable filter
。注意:虽然这些操做被称之为「卷积」操做,可是严格地来讲,他们只是互相关,由于卷积核没有作一个逆向的卷积过程。
卷积核的卷积过程是按照 strides
参数来肯定的,好比 strides = [1, 1, 1, 1]
表示卷积核对每一个像素点进行卷积,即在二维屏幕上面,两个轴方向的步长都是 1。strides = [1, 2, 2, 1]
表示卷积核对每隔一个像素点进行卷积,即在二维屏幕上面,两个轴方向的步长都是 2。
若是咱们暂不考虑通道这个因素,那么卷积操做的空间含义定义以下:若是输入数据是一个四维的 input
,数据维度是[batch, in_height, in_width, ...]
,卷积核也是一个四维的卷积核,数据维度是[filter_height, filter_width, ...]
,那么,对于输出数据的维度 shape(output)
,这取决于填充参数padding
的设置:
padding = 'SAME'
:向下取舍,仅适用于全尺寸操做,即输入数据维度和输出数据维度相同。
out_height = ceil(float(in_height) / float(strides[1])) out_width = ceil(float(in_width) / float(strides[2]))
padding = 'VALID'
:向上取舍,适用于部分窗口,即输入数据维度和输出数据维度不一样。
out_height = ceil(float(in_height - filter_height + 1) / float(strides[1])) out_width = ceil(float(in_width - filter_width + 1) / float(strides[2]))
output[b, i, j, :] = sum_{di, dj} input[b, strides[1] * i + di, strides[2] * j + dj, ...] * filter[di, dj, ...]
由于,input
数据是一个四维的,每个通道上面是一个向量input[b, i, j, :]
。对于conv2d
,这些向量会被卷积核filter[di, dj, :, :]
相乘而产生一个新的向量。对于depthwise_conv_2d
,每一个标量份量input[b, i , j, k]
将在 k
个通道上面独立地被卷积核 filter[di, dj, k]
进行卷积操做,而后把全部获得的向量进行链接组合成一个新的向量。
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)
这个函数的做用是对一个四维的输入数据 input
和四维的卷积核 filter
进行操做,而后对输入数据进行一个二维的卷积操做,最后获得卷积以后的结果。
给定的输入 tensor 的维度是 [batch, in_height, in_width, in_channels]
,卷积核 tensor 的维度是[filter_height, filter_width, in_channels, out_channels]
,具体卷积操做以下:
[filter_height * filter_width* in_channels, output_channels]
[batch, out_height, out_width, filter_height * filter_width * in_channels]
更加具体的表示细节为,若是采用默认的 NHWC data_format形式:
output[b, i, j, k] = sum_{di, dj, q} input[b, strides[1] * i + di, strides[2] * j + dj, q] * filter[di, dj, q, k]
因此咱们注意到,必需要有strides[0] = strides[3] = 1
。在大部分处理过程当中,卷积核的水平移动步数和垂直移动步数是相同的,即strides = [1, stride, stride, 1]
。
使用例子:
import numpy as np import tensorflow as tf input_data = tf.Variable(np.random.rand(10, 6, 6, 3), dtype = np.float32) filter_data = tf.Variable(np.random.rand(2, 2, 3, 1), dtype = np.float32) y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME') with tf.Session() as sess: init = tf.initialize_all_variables() sess.run(init) print(sess.run(y)) print(sess.run(tf.shape(y)))
输入参数:
input
: 一个 Tensor
。数据类型必须是 float32
或者 float64
。filter
: 一个 Tensor
。数据类型必须是input
相同。strides
: 一个长度是 4 的一维整数类型数组,每一维度对应的是 input 中每一维的对应移动步数,好比,strides[1]
对应 input[1]
的移动步数。padding
: 一个字符串,取值为 SAME
或者 VALID
。use_cudnn_on_gpu
: 一个可选布尔值,默认状况下是 True
。data_format
:一个可选string
,NHWC
或者NCHW
。默认是用NHWC
。主要是规定了输入 tensor 和输出 tensor 的四维形式。若是使用 NHWC
,则数据以 [batch, in_height, in_width, in_channels]
存储;若是使用NCHW
,则数据以[batch, in_channels, in_height, in_width]
存储。name
: (可选)为这个操做取一个名字。输出参数:
Tensor
,数据类型是 input
相同。池化操做是利用一个矩阵窗口在输入张量上进行扫描,而且将每一个矩阵窗口中的值经过取最大值,平均值或者其余方法来减小元素个数。每一个池化操做的矩阵窗口大小是由 ksize
来指定的,而且根据步长参数 strides
来决定移动步长。好比,若是 strides
中的值都是1,那么每一个矩阵窗口都将被使用。若是 strides
中的值都是2,那么每一维度上的矩阵窗口都是每隔一个被使用。以此类推。
更具体的输出结果是:
output[i] = reduce( value[ strides * i: strides * i + ksize ] )
输出数据维度是:
shape(output) = (shape(value) - ksize + 1) / strides
其中,取舍方向取决于参数 padding
:
padding = 'SAME'
: 向下取舍,仅适用于全尺寸操做,即输入数据维度和输出数据维度相同。padding = 'VALID
: 向上取舍,适用于部分窗口,即输入数据维度和输出数据维度不一样。tf.nn.avg_pool(value, ksize, strides, padding , data_format='NHWC', name=None)
这个函数的做用是计算池化区域中元素的平均值。
使用例子:
import numpy as np import tensorflow as tf input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 ) filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32) y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME') output = tf.nn.avg_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'SAME') with tf.Session() as sess: init = tf.initialize_all_variables() sess.run(init) print(sess.run(output)) print(sess.run(tf.shape(output)))
输入参数:
value
: 一个四维的Tensor
。数据维度是 [batch, height, width, channels]
。数据类型是float32
,float64
,qint8
,quint8
,qint32
。ksize
: 一个长度不小于 4 的整型数组。每一位上面的值对应于输入数据张量中每一维的窗口对应值。strides
: 一个长度不小于 4 的整型数组。该参数指定滑动窗口在输入数据张量每一维上面的步长。padding
: 一个字符串,取值为 SAME
或者 VALID
。data_format
:一个可选string
,NHWC
或者NCHW
。默认是用NHWC
。主要是规定了输入 tensor 和输出 tensor 的四维形式。若是使用 NHWC
,则数据以 [batch, in_height, in_width, in_channels]
存储;若是使用NCHW
,则数据以[batch, in_channels, in_height, in_width]
存储。name
: (可选)为这个操做取一个名字。输出参数:
tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
这个函数的做用是计算 pooling 区域中元素的最大值。
tf.nn.max_pool_with_argmax(input, ksize, strides, padding, Targmax=None, name=None)
这个函数的做用是计算池化区域中元素的最大值和该最大值所在的位置。
由于在计算位置 argmax
的时候,咱们将 input
铺平了进行计算,因此,若是 input = [b, y, x, c]
,那么索引位置是 `( ( b * height + y ) * width + x ) * channels + c
查看源码,该API只能在GPU环境下使用,因此我没有测试下面的使用例子,若是你能够测试,请告诉我程序是否能够运行。
源码展现:
REGISTER_KERNEL_BUILDER(Name("MaxPoolWithArgmax") .Device(DEVICE_GPU) .TypeConstraint<int64>("Targmax") .TypeConstraint<float>("T"), MaxPoolingWithArgmaxOp<Eigen::GpuDevice, float>); REGISTER_KERNEL_BUILDER(Name("MaxPoolWithArgmax") .Device(DEVICE_GPU) .TypeConstraint<int64>("Targmax") .TypeConstraint<Eigen::half>("T"), MaxPoolingWithArgmaxOp<Eigen::GpuDevice, Eigen::half>);
使用例子:
import numpy as np import tensorflow as tf input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = tf.float32 ) filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32) y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME') output, argmax = tf.nn.max_pool_with_argmax(input = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'SAME') with tf.Session() as sess: init = tf.initialize_all_variables() sess.run(init) print(sess.run(output)) print(sess.run(tf.shape(output)))
输入参数:
input
: 一个四维的Tensor
。数据维度是 [batch, height, width, channels]
。数据类型是float32
。ksize
: 一个长度不小于 4 的整型数组。每一位上面的值对应于输入数据张量中每一维的窗口对应值。strides
: 一个长度不小于 4 的整型数组。该参数指定滑动窗口在输入数据张量每一维上面的步长。padding
: 一个字符串,取值为 SAME
或者 VALID
。Targmax
: 一个可选的数据类型: tf.int32
或者 tf.int64
。默认状况下是 tf.int64
。name
: (可选)为这个操做取一个名字。输出参数:
一个元祖张量 (output, argmax)
:
output
: 一个Tensor
,数据类型是float32
。表示池化区域的最大值。argmax
: 一个Tensor
,数据类型是Targmax
。数据维度是四维的。因此,为了建立这个模型,咱们须要建立大量的权重和偏置项,这个模型中的权重在初始化的时候应该加入少许的噪声来打破对称性以及避免 0 梯度。因为咱们使用的是 ReLU 神经元,所以比较好的作法是用一个较小的正数来初始化偏置项,以免神经元节点输出恒为 0 的问题(dead neurons)。为了避免再创建模型的时候反复作初始化操做,咱们定义两个函数用于初始化。
def conv2d(x, W): return tf.nn.conv2d(x, W, strides = [1, 1, 1, 1], padding = 'SAME') def max_pool_2x2(x): return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME') def weight_variable(shape): initial = tf.truncated_normal(shape, stddev = 0.1) return tf.Variable(initial) def bias_variable(shape): initial = tf.constant(0.1, shape = shape) return tf.Variable(initial)
接下来,咱们开始实现第一层。它由一个卷积层接一个 max_pooling 最大池化层完成。卷积在每一个 5x5 的 patch 中算出 32 个特征。卷积的权重张量形状是 [5, 5, 1, 32]
,前两个维度是 patch 的大小,接着是输入的通道数目,最后是输出的通道数目。而对于每个输出通道都有一个对应的偏置量。
W_conv1 = weight_variable([5, 5, 1, 32]) b_conv1 = bias_variable([32])
为了用这一层,咱们把 x 变成一个 4d 的向量,其第 二、第 3 维对应图片的宽度、高度,最后一位表明图片的颜色通道(由于是灰度图,因此这里的通道数为 1,若是是 RBG 彩色图,则为 3)。
x_image = tf.reshape(x, [-1, 28, 28, 1])
以后,咱们把 x_image 和权值向量进行卷积,加上偏置项,而后应用 ReLU 激活函数,最后进行 max pooling。
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) h_pool1 = max_pool_2x2(h_conv1)
为了构建一个更深的网络,咱们会把几个相似的层堆叠起来。第二层中,每一个 5x5 的 patch 会获得 64 个特征。
W_conv2 = weight_variable([5, 5, 32, 64]) b_conv2 = bias_variable([64]) h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) h_pool2 = max_pool_2x2(h_conv2)
如今,图片尺寸减少到 7x7,咱们加入一个有 1024 个神经元的全链接层,用于处理整个图片。咱们把池化层输出的张量 reshape 成一些向量,乘上权重矩阵,加上偏置,而后对其使用 ReLU。
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。咱们用一个 placeholder 来表明一个神经元的输出在 dropout 中保持不变的几率。这样咱们能够在训练过程当中启用 dropout,在测试过程当中关闭 dropout。 TensorFlow 的tf.nn.dropout 操做除了能够屏蔽神经元的输出外,还会自动处理神经元输出值的 scale。因此用 dropout 的时候能够不用考虑 scale。
keep_prob = tf.placeholder("float") h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
最后咱们添加一个 softmax 层,就像前面的单层 softmax regression 同样。
W_fc2 = weight_variable([1024, 10]) b_fc2 = bias_variable([10]) y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
这个模型的效果如何呢?
为了进行训练和评估,咱们使用与以前简单的单层 SoftMax 神经网络模型几乎相同的一套代码,只是咱们会用更加复杂的 ADAM 优化器来作梯度最速降低,在 feed_dict 中加入额外的参数 keep_prob 来控制 dropout 比例。而后每 100 次迭代输出一第二天志。
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv)) train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy) correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) sess.run(tf.initialize_all_variables()) for i in range(20000): batch = mnist.train.next_batch(50) 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) train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5}) print "test accuracy %g"%accuracy.eval(feed_dict={ x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0})