卷积神经网络是根据视觉神经皮质上的 local receptive field(临近感知域)现象启发而发明的。本质就是临近区域(像素)对于判断、决策时意义要远大于较远的区域(像素),所以在网络计算中对于特定节点的更新也就只需要根据附近的像素点数据进行决断,减少噪声的干扰,而在数学上恰巧有卷积这个工具可以进行快速的处理。在普通的神经网络层上会添加对应的卷积运算,就可以实现根据邻域数据来更新网络权值,这样也就将普通的网络层改造成为卷积层,对应的也就得到了卷积神经网络。
整体概况
上图概括了卷积神经网络的几个关键步骤,包含了卷积运算,池化,摊平(flatten)等操作,下面就分别介绍各步骤的主要内容。
卷积层
卷积层代表着一种特定类型的网络连接方式。这种连接方式是基于图像数据的局部相关性和图像数据底层特征位置无关性而发展出来的,在卷积层中,每个节点仅和上一网络层的部分节点(位置相近,数量由卷积核大小确定)相连接,并且该网络层上的所有节点的连接权值是相同的,也就是权值共享的含义。
卷积计算
卷积运算可以分成连续卷积和离散卷积,在cnn等神经网络中所使用到的就是离散卷积。如下动图详细的介绍了离散卷积的计算方法。
通过上面的延时,也可以看出来cnn在卷积计算的时候有几个主要的参数:
- 卷积核 :不同的卷积和代表着数据的检测,例如有的卷积核可以检测到边缘等;卷积核数据代表了网络连接权值,是需要通过网络训练而得到;这一点就和在图像数据中通过特定的卷积核(如可以提取图像边缘等)来提取特征数据形成了对比。
- stride / 步幅:这个参数用来控制卷积核的遍历跨度;
- padding :用来控制输入和输出数据(feature map)的维度大小。 例如上面的示意图,经过卷积运算后的数据图像就要比原始的数据要小。通过调整padding的方式来补充数据的边缘。
图像数据
图像数据看似是一个二维的数据结构, 本质上是个立体数据。 除了横轴、纵轴,还有一个深度轴,例如普通的图像包含RGB三个图像层(channel),如果是卫星遥感等数据可能会有更多的图像层。
from lines, to contours, to shapes, to entire objects
图像数据中的模式分解。图像数据中的模式通过线条、轮廓、形状最终来反映整个物体。通过不同的网络层级来抽象得到各个层级的视觉特征,最终来完成图像的分类识别任务。
卷积层的输入和输出
The depth of the output volume of a convolutional layer is equivalent to the number of filters in that layer
输入的数据的自然是图像,而且图像中包含不同的channel,反应到数据结构上就是三维的数据结构(包含深度这一维度,一般的图片包含RGB三个通道)。对此Filter也包含对应通道数量,用来计算特定邻域上的像素计算结果。
输出数据是特征图(feature map)的组合。每一个filter在扫描完成原始数据之后就会形成一个特征图(由于Filter也是多维的,所以已经融合了各个通道的信息),对于一个卷积层来说会包含多个不同的Filter,这些特征图叠加组合起来就构成了卷积层的输出数据。
但是看之前的神经网络结构,数据都是一维的,那么这里的三维或者更高维度的图像数据是如何处理的?难道是需要整形成为一维形状?
卷积层 API
在进行二维图像的卷积层计算时,tensorflow上提供了两种计算方式:nn.conv2d
和 layers.conv2d
。 两者的功能是相似的,但是参数设计上略有不同的,其中layers的参数稍多些。在查阅的资料中也有说如果是训练自己的模型则使用layers.conv2d
,如果是利用别人的模型则使用nn.conv2d
。
nn.conv2d 的参数
nn.conv2d
需要设定的主要参数如下
- strides The stride of the sliding window for each dimension of
input
- padding 有两个候选的padding算法可以使用, SAME(with zero padding,通过补零来保证feature map的维度等于输入数据)和 VALID(without padding,只使用有效的数据)
layers.conv2d的参数
layers.conv2d
的主要参数包含:
- inputs :Tensor input,shape
(batch, height, width, channels)
channel维度可以放在前或者后,需要和data_format
参数匹配。 - filters: Integer, the dimensionality of the output space (i.e. the number of filters in the convolution)(这个就看起来很明了了 )
- kernel_size :An integer or tuple/list of 2 integers, specifying the height and width of the 2D convolution window
- strides An integer or tuple/list of 2 integers
共一个小白的角度看,layers.conv2d的参数设计更清晰些。
卷积层计算范式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from sklearn.datasets import load_sample_image china = load_sample_image("china.jpg") image = china[150:220, 130:250] height, width, channels = image.shape image_grayscale = image.mean(axis=2).astype(np.float32) # 将RGB图像转换为灰阶图像 images = image_grayscale.reshape(1, height, width, 1) # 为什么要调整成这个样子 # 定义了两个filter,这个会影响卷积层输出的数据的channel数量 filters = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32) filters[:, 3, :, 0] = 1 # vertical line filters[3, :, :, 1] = 1 # horizontal line X = tf.placeholder(tf.float32, shape=(None, height, width, 1)) filters = tf.constant(filters) convolution = tf.nn.conv2d(X, filters, strides=[1,1,1,1], padding="SAME") with tf.Session() as sess: output = convolution.eval(feed_dict={X: images}) # 计算该卷积层输出结果 plt.imshow(output[0,:,:,0]) # 这里得到的才应该符合之前定义的feature map |
如上只是演示了利用tensorflow框架进行计算的过程,在这个过程中是采用了特定的filter。这些filter就是代表了网络连接的权值,而在真实网络训练的时候是通过数据来训练得到这些filter,所以在训练的时候filter则是变量,需要使用placeholder。
卷积神经网络其他要素
池化层
To aggressively reduce dimensionality of feature maps and sharpen the located features, we sometimes insert a max pooling layer after a convolutional layer.
和卷积层相比,池化层的工作要简单的多,则是按照指定的计算方法将指定区域内的多个数据合并成单个像素数据。 这样的操作有两个好处:减少数据的维度,锐化区域内特征。
池化的计算方法往往是选择最大值,所以也往往称为最大值池化。
全连接层 (FC层) / dense
在CNNs网络中,在最后的输出层之前往往要通过一个全连接层来输出结果。 在全连接层可以指定输出的节点数,使用的api是tf.dense
输出层 / softmax
在输出层的时候往往需要将结果概率化,例如输出层有10个节点,每个代表不同的识别类比,经过softmax计算之后就可以得到待识别的数据归属到各个节点上的概率。同时由于网络中间层节点往往包含的数量要大于分类类别数量,所以在输出层上往往也会使用FC层来将网络节点数量减少到和分类类别相匹配。
卷积神经网络范式
前面介绍了指定了filter / kernel 来进行卷积计算,但是搭建CNNs来完成图像识别的过程则完全是另外的使用范式。下面使用MNIST数据集训练CNNs模型来完成手写体数字识别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | height = 28 # mnist数据集的尺寸, 28*28 width = 28 channels = 1 n_inputs = height * width # 输入节点, 由于channel只有1,所以输入节点数量是否应该包含channe从这个示例代码中看不太出来。 conv1_fmaps = 32 # 指定第一个卷积层的feature map的数量 conv1_ksize = 3 conv1_stride = 1 conv1_pad = "SAME" conv2_fmaps = 64 # 指定第二个卷积层的feature map的数量 conv2_ksize = 3 conv2_stride = 2 conv2_pad = "SAME" pool3_fmaps = conv2_fmaps n_fc1 = 64 # 全连接层的节点数量 n_outputs = 10 # 最终输出层的节点数量 with tf.name_scope("inputs"): X = tf.placeholder(tf.float32, shape=[None, n_inputs], name="X") # 这里的n_inputs来表示一个图像数据的维度 X_reshaped = tf.reshape(X, shape=[-1, height, width, channels]) # 将输入数据整形 y = tf.placeholder(tf.int32, shape=[None], name="y") # shape中的-1和None的区别? conv1 = tf.layers.conv2d(X_reshaped, filters=conv1_fmaps, kernel_size=conv1_ksize, strides=conv1_stride, padding=conv1_pad, activation=tf.nn.relu, name="conv1") conv2 = tf.layers.conv2d(conv1, filters=conv2_fmaps, kernel_size=conv2_ksize, strides=conv2_stride, padding=conv2_pad, activation=tf.nn.relu, name="conv2") with tf.name_scope("pool3"): pool3 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="VALID") pool3_flat = tf.reshape(pool3, shape=[-1, pool3_fmaps * 7 * 7]) # 为什么是7*7?应该是经过两次stride,导致单个图像的数据维度降低了。 with tf.name_scope("fc1"): fc1 = tf.layers.dense(pool3_flat, n_fc1, activation=tf.nn.relu, name="fc1") with tf.name_scope("output"): logits = tf.layers.dense(fc1, n_outputs, name="output") # 输出层 Y_proba = tf.nn.softmax(logits, name="Y_proba") # 激励函数,输出结果 with tf.name_scope("train"): # 训练,优化目标;很关键的步骤 xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=y) #目标函数 loss = tf.reduce_mean(xentropy) optimizer = tf.train.AdamOptimizer() training_op = optimizer.minimize(loss) # 训练目标 with tf.name_scope("eval"): # 评估目标 correct = tf.nn.in_top_k(logits, y, 1) # whether the targets are in the top K predictions accuracy = tf.reduce_mean(tf.cast(correct, tf.float32)) # cast 转换数据类型 with tf.name_scope("init_and_save"): init = tf.global_variables_initializer() saver = tf.train.Saver() (X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data() # 加载数据 X_train = X_train.astype(np.float32).reshape(-1, 28*28) / 255.0 # shape为(60000, 784),每一行代表一个图像 # 通过reshape 把二维的图像数据转化为单个维度,那么在conv2中是如何将其复原的呢? X_test = X_test.astype(np.float32).reshape(-1, 28*28) / 255.0 y_train = y_train.astype(np.int32) y_test = y_test.astype(np.int32) X_valid, X_train = X_train[:5000], X_train[5000:] y_valid, y_train = y_train[:5000], y_train[5000:] def shuffle_batch(X, y, batch_size): # 根据permute结果,抽取得到目标的数据集合 rnd_idx = np.random.permutation(len(X)) n_batches = len(X) // batch_size for batch_idx in np.array_split(rnd_idx, n_batches): # 将打算的参数分成n_batches份 X_batch, y_batch = X[batch_idx], y[batch_idx] yield X_batch, y_batch n_epochs = 10 # 设定迭代运行次数 batch_size = 100 # with tf.Session() as sess: init.run() # 初始化数据结构 for epoch in range(n_epochs): for X_batch, y_batch in shuffle_batch(X_train, y_train, batch_size): # 拆分数据集 sess.run(training_op, feed_dict={X: X_batch, y: y_batch}) acc_batch = accuracy.eval(feed_dict={X: X_batch, y: y_batch}) # 计算在训练集上的精度 acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test}) # 计算在测试集上的精度 print(epoch, "Last batch accuracy:", acc_batch, "Test accuracy:", acc_test) save_path = saver.save(sess, "./my_mnist_model") # 保存训练的模型 |
在上面这个例子上需要注意的是数据流在不同的网络层上的变化和转换,从而可以更好的理解训练的过程。在这个过程中同时需要注意设定网络优化目标,验证标准*等方法。
通过tensorboard可以查看到网络结构的情况如下:
可以很清晰的看出来数据流走向等信息。但是有一个疑问就是,模型训练完成用于生产环境进行识别的时候,是否还依次按照各个网络层进行计算?现在猜想可能就不是了,而是通过某些技术手段来减少不必要的计算来优化分类/识别等计算的速度。
小结
通过几点的学习算是弄清楚了几个基本问题,例如卷积如何计算,数据流在整个过程中是如何转换的,搭建CNN网络过程中的关键步骤等。但是同样也遗留了很多问题,例如为什么CNN网络架构的设计是这个样子的,能不能多几个卷积层等等。
参考文档
如何通俗易懂地解释卷积? 介绍了卷积以及离散卷积的计算方法,以及应用在二维图像上的方法。
How will channels (RGB) effect convolutional neural network
tf.nn.conv2d - api input,filter, strides,padding等参数
卷积神经网路(Convolutional neural network, CNN) — CNN运算流程 介绍了什么是feature map (特征图) 在影像上,卷积运算后会再加上激活函数(activation function),进行非线性转换,之后得到的图片会称为特征图(feature map)。
tf-nn-conv2d-vs-tf-layers-conv2d For convolution, they are the same. More precisely, tf.layers.conv2d
(actually _Conv
) uses tf.nn.convolution
as the backend
如何理解卷积神经网络中的权值共享? 介绍了权值共享的必要性和有效性,大幅减少了计算需求,同时由于图像的局部相关性以及图像底层特征的位置无关性,使得如此操作是满足计算需求的。
卷积神经网络中二维卷积核与三维卷积核有什么区别? 2D卷积输入的是三维数据,对于图片一般是[ width, height, in_channels] ,在cnn网络中,只定义卷积核的大小,具体的数值也就是则是通过网络学习而得到的,这是因为卷积核就代表了节点连接的权值。
Image Data Pre-Processing for Neural Networks
what-is-the-difference-between-same-and-valid-padding-in-tf-nn-max-pool-of-tensorflow
Build a Convolutional Neural Network using Estimators 在设定input的时候需要设定维度 [batch_size, image_height, image_width, channels],batch_size
: Size of the subset of examples to use when performing gradient descent during training.
TensorFlow中的两种conv2d方法和kernel_initializer
tensorflow学习:tf.nn.conv2d 和 tf.layers.conv2d 在这篇文章中给出了一个通俗的看法:tf.layers.conv2d
参数丰富,一般用于从头训练一个模型。tf.nn.conv2d
,一般在加载预训练好的模型时使用。
Understanding of Convolutional Neural Network (CNN) — Deep Learning
how-to-get-the-input-and-output-channels-in-a-cnn the number of channels (i.e. feature maps) in the output of the first convolution operation
convolution-input-and-output-channels/10205 Each kernel in your ConvLayer will use all input channels of the input volume Filter/Kernel的连接方式是连接所有的权值。
Understand the Softmax Function in Minutes a wonderful activation function that turns numbers aka logits into probabilities that sum to one. 属于激活函数的一种
numpy.reshape tf.reshape
函数的规则应该是和这个reshpe
是一脉相承的。
Common architectures in convolutional neural networks 介绍了几个CNNs网络架构,数据维度以及参数量级。