CNN 核心知识点总结
CNN(Convolutional Neural Network,卷积神经网络)是一种主要用于图像处理和计算机视觉任务的深度学习模型。它通过卷积操作和池化操作来有效地对图像数据进行特征提取,并通过全连接层来进行分类或回归。
CNN 的关键特点是它利用了卷积操作来处理数据。卷积操作通过使用卷积核(也称为过滤器或滤波器)与输入数据的局部区域进行逐元素相乘,并将乘积累加得到输出特征图的一个像素值。通过在整个输入图像上滑动卷积核,可以生成整个特征图。这样的卷积操作可以有效地捕捉到图像中的局部特征,例如边缘、纹理和形状。CNN 特别适用于处理具有网格结构的数据,例如图像和视频。CNN 在计算机视觉领域取得了巨大的成功,并在许多任务中超越了传统的机器学习方法。
1.CNN 网络结构的的三大特点
CNN 网络结构中最重要的三个特点:局部连接(local connections)、权值共享(shared weight)、池化(pooling)。
这三大特点使得 CNN 具有一定程度的移位(shift)不变性和形变(deformation)不变性,同时减少了参数数量,所以 CNN 更加适合用于分层或空间数据(卷积层侧重特征的相对位置,全连接侧重特征的精确位置)。
1、局部连接(local connections)
局部连接基于局部感受野的原理,一般认为人对外界的认知是从局部到全局的,而图像的空间联系也是局部的像素联系较为紧密,而距离较远的像素相关性则较弱。因而,每个神经元其实没有必要对全局图像进行感知,只需要对局部进行感知,然后在更高层将局部的信息综合起来就得到了全局的信息。
2、权值共享(shared weight)不同神经元之间的参数共享可以减少需要求解的参数,使用多种滤波器去卷积图像就会得到多种特征映射。权值共享其实就是对图像用同样的卷积核进行卷积操作,也就意味着第一个隐藏层的所有神经元所能检测到处于图像不同位置的完全相同的特征。其主要的能力就能检测到不同位置的同一类型特征,也就是卷积网络能很好的适应图像的小范围的平移性,即有较好的平移不变性。
3、池化(pooling)
池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度也有防止过拟合问题的作用。具体过程其实是对卷积得到的不同位置的特征进行聚合统计(下采样),如平均值或最大值,即 mean-pooling 和 max-pooling,无论选择何种池化函数,当对输入做出少量平移时,池化对输入的表示都近似 不变 (invariant)。局部平移不变性 是一个很重要的性质,尤其是当我们关心某个特征是否出现而不关心它出现的位置时。池化可以在减少数据处理量的同时又能够保留有用信息,而且采样计算还可以混淆特征的具体位置,因为某个特征找出来之后,它的位置已经不重要了,我们只需要这个特征和其他特征的相对位置,这样便可以应对形变和扭曲带来的同类对象的变化。
2.CNN 核心概念和过程理解
2.1.特征图
特征图(feature map)是计算机视觉领域中的一个重要概念,用于表示图像在深度学习模型中提取的抽象特征信息。特征图可以简单地理解为类似图片的颜色通道(channel)一样的概念,只不过 channel 通常对应于输入层的图片,而经过卷积操作输出的 chnanel 被称为 feature map,也就是经过卷积核提取得到的特征,通常 feature map 的数量远大于常规 RGB 的三层通道。
在卷积神经网络中,通过卷积操作,输入图像经过一系列的卷积层、激活函数和池化层等处理,逐渐转化为更高级别的特征表示。这些特征表示在不同的卷积层中逐渐变得更加抽象和语义丰富。
特征图是在卷积层中的每个卷积核(也称为滤波器)对输入图像执行卷积操作后得到的输出。每个卷积核可以看作是一个特征探测器,它在输入图像上滑动并提取感兴趣的特征,如边缘、纹理或物体的形状等。卷积操作会在整个输入图像上进行,并生成与卷积核数量相等的特征图。
每个特征图都是一个二维矩阵,其中的每个元素被称为像素。特征图的尺寸取决于输入图像的尺寸以及卷积层的设置,通常会随着网络的深度逐渐减小。这是因为在卷积操作中,通过步幅(stride)和池化层的使用,特征图的空间尺寸会逐渐缩小,但通道数(即特征图的深度)会增加。
特征图在卷积神经网络中起到了至关重要的作用。它们包含了输入图像的抽象特征信息,并且这些特征信息对于后续任务(如分类、目标检测、图像分割等)的执行非常关键。深度学习模型通过训练过程自动学习特征图的权重,以使其能够更好地表示输入图像中的有用特征。
2.2.卷积核
卷积核(convolution kernels)也称为过滤器、滤波器(filter),构建卷积神经网络过程中,需要指定卷积核的几个属性:长、宽、深度、数量。其中卷积核的长乘宽也被称为卷积核的尺寸,常用的尺寸为 3 × 3
和 5 × 5
,卷积核的深度等于输入的 channel 数,也就是 feature map 数量,而卷积核的数量决定了输出的 feature map 数量(如下图)。通常的设定规则是随着网络的加深,feather map 的长宽尺寸会变小,此时卷积提取的特征越具有代表性,但卷积和池化操作会丢失信息量,所以后面需要增加卷积层数(feature map 数)来表达更多的特征,以弥补卷积操作丢失的信息量,所以设置的卷积核的个数也是要增加的,一般是成倍增加(具体应根据实验的情况来设置)。
卷积核可以将当前层网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵。比如上图中的第一层 feature map 中的 12 就是这样来的。单位节点矩阵指的是高和宽都是 1,但深度(深度是由卷积核数量来决定)不限的节点矩阵,具体过程如下图所示:
上图中的权重 W 就是卷积核矩阵中的数值,也是 CNN 最终的训练目标,卷积核矩阵其实就是权重矩阵的一个 “子权重矩阵”。在训练之前,卷积核中的数值会进行一个初始化,通常推荐使用 Xavier 方法进行初始化,如果使用正态分布进行初始化,一定要用截断正态分布(truncated_normal),将参数初始值没有限定在一定的范围之内,因为卷积神经网络通常参数量很大,如果用 random_normal 初始化可能会出现极大值,而随机梯度下降时学习率相对较小,那么出现的极大值就很难被优化,使得梯度几乎失去了作用,很难收敛。
2.3.卷积过程
卷积层结构的前向传播过程就是通过将一个卷积核从神经网络当前层的左上角移动到右下角,并且在移动中计算每一个对应的单位矩阵得到的,如下动图:
卷积相关的主要参数有:卷积核大小(filter(shape)),卷积步长(stride),填充方式(padding)。
填充方式主要是全 0 填充和不填充,全 0 填充也称零填充(Zero-padding),有时在输入矩阵的边缘使用零值进行填充,这样我们就可以对输入图像矩阵的边缘进行滤波,这样可以避免边缘因不满足卷积条件而被丢弃造成的信息损失。零填充的一大好处是可以让我们控制特征图的大小,比如可以保持输出的特征图同当前层的矩阵大小保持一致。使用零填充的也叫做泛卷积,不使用零填充的叫做严格卷积。
需要的注意是,全 0 填充并不是严格地在四周都填充一圈 0,具体在哪边填充,每个边填充多少(通常是 0、1、2 像素)都要视情况而定,在 TensorFlow 中,内部是有个公式来实现自动填充规则的(见下)。
在 TensorFlow 中:
当参数
padding='SAME'
时表示全 0 填充,如果卷积步长是 1,那么输入和输出的特征图大小将会完全一样,因此参数用 ‘SAME’ 来表示。TensorFlow 在填充的时候,会尝试均匀地填充水平和垂直方向的两边,但是如果要添加的像素数量是奇数,则会优先在水平方向的右边和垂直方向的下边添加额外多出来像素,如下所示:
123456pad| |padinputs: 0 |1 2 3 4 5 6 7 8 9 10 11 12 13|0 0|________________||_________________||________________|卷积后的特征图矩阵大小为($W$为特征图边长,$S$为卷积步长):
$$W_{out} = W_{in} / S(结果向上取整)$$
因为要保持 $S=1$时,$W_{out}=W_{in}$,因此填充策略如下:
1)计算需要填充的像素数($P$为填充像素数,$F$为卷积核边长):
$$P=(W_{out} – 1) × S + F – W_{in}$$
2)则输入矩阵左边或上边需要填充 0 的像素数为:
$$P_{left|top} = P / 2(向下取整)$$
3)输入矩阵的右边或下边需要填充 0 的像素数为:
$$P_{right|down} = P – P_{left|top}$$
当参数
padding='VALID'
时表示不填充,因没有 “虚构” 的填充输入。该层只使用有效的输入数据,因此参数用 ‘VALID’ 来表示。如果使用全 0 填充,有时候边缘会因不满足卷积条件而被丢弃不计算,因为卷积通常是从左上到右下进行的,因此最后被丢弃的边往往是右边或者下边。
1234inputs: 1 2 3 4 5 6 7 8 9 10 11 (12 13)|________________| dropped|_________________|卷积后的特征图矩阵大小为($W$为特征图边长,$S$为卷积步长,$F$为卷积核边长):
$$W_{out} = (W_{in} – F + 1) / S(向上取整)$$
2.4.池化过程
池化层前向传播的过程也是通过移动一个类似卷积核的结构完成的,简单起见,我们称之为池化核。池化核的移动方式与卷积核是相似的,唯一的区别在于卷积核是横跨整个深度的,而池化核只影响一个深度上的节点,它是不可以跨不同输入样例或者节点矩阵深度的。所以池化核除了在长和宽两个维度移动,它还需要在深度这个维度移动。
池化过程的主要参数也是三个:池化核大小(ksize),卷积步长(stride),填充方式(padding),TensorFlow 中平均池化和最大池化是分别用两个方法实现的,即 tf.nn.avg_pool() 和 tf.nn.max_pool()。
一般使池化核的大小与步长相等,尽量保证不重叠、全覆盖地进行降采样,实际较常用的尺寸为 ksize=[ 1, 2, 2, 1]
或者 ksize=[1, 3, 3, 1]
,这里的首尾的两个 1 是 TensorFlow 中固定的。也有些场景尝试重叠降采样的,称为重叠池化(OverlappingPooling)。池化过程通常也不会进行填充,多余的数据会直接丢弃。即池化常使用不重叠(stride=kernel size),不补零(多余的数据直接丢弃)。
池化后特征图矩阵大小($W$为特征图边长,$S$为卷积步长,$F$为池化核边长):
$$W_{out} = (W_{in}-F)/S + 1$$
最大池化和平均池化的选择经验:
- 在目标对象是偏向于纹理、轮廓时,选用最大池化较好(最大池化的结果偏向于抽取比较明显的特征,结果特征图类似于城市夜景卫星图)
- 在目标对象时偏向于背景或其他信息是,选用平均池化较好(平均池化的结果类似于下采样插值,结果特征图类似于在原来的基础上更加抽象模糊)
- 在可以在较浅层使用最大池化,用来过滤无用的信息,在较深层使用平均池化,防止丢掉太多高维信息
3.CNN TensorFlow 代码模版
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | import numpy as np import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data """[CNN For MNIST, Network Structure] Conv1 + MaxPool1 --> Conv2 + MaxPool2 --> FC1 + Dropout --> FC2 + Softmax """ mnist = input_data.read_data_sets(r'datasets/MNIST_data/', one_hot=True) # 定义基础函数 def weight_variable(shape): initial = tf.truncated_normal(stddev=0.1, shape=shape) return tf.Variable(initial) def bias_variable(shape): initial = tf.constant(0.1, shape=shape) return tf.Variable(initial) def conv2d(x, W): ## tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None) # input:[batch, in_height, in_width, in_channels] # filter:[filter_height,filter_wideth,in_channels,out_channels] # strides:[1,stride,stride,1] # 1是固定的 # padding: 'SAME' or 'VALID' return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') def max_pool_2x2(x): ## tf.nn.max_pool(value, ksize, strides, padding, data_format=NHWC, name=None) # valua: [batch, in_height, in_width, in_channels] feature map # ksize: 池化窗口的大小 [1,height, width, 1] # 1是固定的 # strides:[1,stride,stride,1] # 1是固定的 # padding: 'SAME' or 'VALID' 一般都是'VALID' # data_format: 'NHWC' or 'NCHW'(caffe), 必须和conv2d中一致 return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') # input with tf.name_scope('input'): x = tf.placeholder(tf.float32, [None, 784]) y_ = tf.placeholder(tf.float32, [None, 10]) with tf.name_scope('image'): x_image = tf.reshape(x, [-1, 28, 28, 1]) # shape: any dim, width, height, channel(depth) tf.summary.image('mnist_image', x_image, max_outputs=8) # conv1 with tf.name_scope('conv1'): # 卷积核的深度对应当前feature map的数量,而卷积核的数量则控制卷积后的深度,也就是卷积后feature map的数量 conv1_W = weight_variable([5, 5, 1, 32]) # # convolution kernel: 5*5*1, number of kernel: 32 conv1_b = bias_variable([32]) conv1_h = tf.nn.relu(conv2d(x_image, conv1_W) + conv1_b) # make convolution, output: 28*28*32 with tf.name_scope('pool1'): pool1_h = max_pool_2x2(conv1_h) # make pooling, output: 14*14*32 # conv2 with tf.name_scope('conv2'): conv2_W = weight_variable([5, 5, 32, 64]) conv2_b = bias_variable([64]) conv2_h = tf.nn.relu(conv2d(pool1_h, conv2_W) + conv2_b) # output: 14*14*64 with tf.name_scope('pool2'): pool2_h = max_pool_2x2(conv2_h) # output: 7*7*64 with tf.name_scope('fc1'): fc1_W = weight_variable([7*7*64, 1024]) fc1_b = bias_variable([1024]) fc1_pool2_flat = tf.reshape(pool2_h, [-1, 7*7*64]) fc1_h = tf.nn.relu(tf.matmul(fc1_pool2_flat, fc1_W) + fc1_b) # output: 1*1024 with tf.name_scope('dropout'): keep_prob = tf.placeholder(tf.float32) fc1_h_dropout = tf.nn.dropout(fc1_h, keep_prob) with tf.name_scope('fc2'): fc2_W = weight_variable([1024, 10]) fc2_b = bias_variable([10]) with tf.name_scope('softmax'): y_conv = tf.nn.softmax(tf.matmul(fc1_h_dropout, fc2_W) + fc2_b) # output: 1*10 # Lose and train with tf.name_scope('lost'): cross_entropy = - tf.reduce_sum(y_*tf.log(y_conv)) tf.summary.scalar('lost', cross_entropy) with tf.name_scope('train'): train_op = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(cross_entropy) with tf.name_scope('accuracy'): correct_prediction = tf.equal(tf.argmax(y_, 1), tf.argmax(y_conv, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float')) tf.summary.scalar('accuracy', accuracy) merged = tf.summary.merge_all() train_summary = tf.summary.FileWriter(r'./log', tf.get_default_graph()) # init vars init = tf.global_variables_initializer() with tf.Session() as sess: sess.run(init) for i in range(2000): batch = mnist.train.next_batch(50) result, _ = sess.run([merged, train_op], feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.8}) 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_summary.add_summary(result, i) train_summary.close() print('test accuracy %g' % accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0})) |
写的非常棒,学习了,受益匪浅~ 谢谢~