「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。
Convolutional Neural Network, CNN:卷积神经网络
图像识别问题&经典数据集
CIFAR数据集 ILSVRC2012数据集
1.卷积神经网络简介
相对于全连接神经网络,区别在于:全连接神经网络中,相邻的两层之间的节点都有边相连,于是一般会将每一层全连接层中的节点组织成一列。而在卷积神经网络中,相邻两层之间只有部分节点相连,为了展示每一层神经元的维度,一般会将每一层卷积层的节点组织成一个三维矩阵。卷积神经网络结构图:
输入信息-->卷积层1-->池化层1-->卷积层2-->池化层2-->全连接层1-->全连接层2-->softMax-->分类结果
在卷积神经网络的前几层中,每一层的节点都被组织成一个三维矩阵:
- 输入层:输入层是整个神经网络的输入,在处理图像的卷积神经网络中,一般代表了一张图片的像素矩阵。从输入层开始,卷积神经网络通过不同的神经网络结构将上一层的三维矩阵转化为下一层的三维矩阵,真到最后的全连接层;
- 卷积层:卷积神经网络中最重要的一层。卷积层中每一个节点的输入只是上一层神经网络的一小块,这个小块的大小通常是3*3或者5*5。卷积层试图将神经网络中的每一小块进行更加深入地分析从而得到抽象程度更高地特征;
- 池化层:池化层不会改变三维矩阵地深度,但会缩小矩阵的大小。比如:将一张分辨率较高地图片转化为分辨率较低的图片。通过池化层,可以进一步缩小在最后全连接层中节点的个数,从而达到减少整个神经网络中参数的目的;
- 全连接层:主要来完成分类任务;
- Softmax层:通过softmax层,可以得到当前样例属于不同种类的概率分布情况。
2.卷积神经网络常用结构
2.1 卷积层
可以称之为过滤器(filter)或者内核(kernel)。
作用是将当前层神经网络上的一个子节点矩阵转化成下一层神经网络上的一个单位节点矩阵。所谓单位节点矩阵,指的是一个长和宽都为1,但深度不限的节点矩阵。
过滤器中所处理节点矩阵的长和宽都是由人工指定的,这个尺寸也被成为过滤器的尺寸,常用的尺寸有3x3或者5x5。因为过滤器处理的矩阵深度和当前神经网络节点矩阵的深度是一致的,所以虽然节点矩阵是三维的,但过滤器的尺寸只需要指定两个维度。过滤器中另外一个需要人工指定的设置是处理得到的单位节点矩阵的深度,这个设置称为过滤器的深度。
卷积层神经网络的前向传播过程就是通过过滤器矩阵算出单位节点矩阵的过程:假设用 来表示对于输出单位节点矩阵中的第i个节点,过滤器输入节点(x,y,z)的权重,使用 表示第i个输出节点对应的偏置项参数,那么单位矩阵中的第i个节点的取值为:
其中 为过滤器节点(x,y,z)的取值,f为激活函数。
卷积层结构的前向传播过程就是通过将一个过滤器从神经网络当前层的左上角移到右下角,并且在移动过程中计算每一个对应的单位矩阵得到的。
以下公式给出了在同时使用全0填充时结果矩阵的大小:
其中 表示输出层矩阵的长度,它等于输入层矩阵长度除以长度方向上的步长的向上取整。类似的, 表示输出层矩阵的宽度,它等于输入层矩阵宽度除以宽度方向上的步长的向上取整。
当不使用全0填充时,以下为结果矩阵的大小:
在卷积神经网络中,每一个卷积层中使用的过滤器中的参数都是一样的,共享每一个卷积层中过滤器的参数可以巨幅减少神经网络的参数。而且卷积层的参数个数和图片大小无关,它只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关。这个特性使得卷积神经网络可以很好地扩展到更大的图像数据上。
import tensorflow as tf
'''
通过tf.getV...的方式创建过滤器的权重变量和偏置项变量。上面介绍了卷积层
的参数个数只和过滤器的尺寸、深度以及当前层节点矩阵的深度相关,所以这里声明的参数变量是一个四维矩阵。
前两个维度表示了过滤器的尺寸,第三个维度表示了当前层的深度,第四个维度表示过滤器的深度
'''
filter_weight = tf.get_variable('weights', [5, 3, 1, 16],
initializer=tf.truncated_normal_initializer(stddev=0.1))
'''
和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共享的,所以总共有下一层深度个不同的偏置项。
本代码中的16为过滤器的深度,也是神经网络中下一层节点矩阵的深度
'''
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))
'''
tf.nn.conv2d提供了一个非常方便的函数来实现卷积层的前向传播算法。这个函数的第一个输入为当前
层的节点矩阵,这个矩阵是一个四维矩阵,后面三个维度对应一个节点矩阵,第一维对应一个输入batch。比如
在输入层,input[0, :, :, :]表示第一张图片,input[1, :, :, :]表示第二张图片。以此类推。
tf.n.conv2d第二个参数提供了卷积层的权重,第三个参数为不同维度的步长,虽然参数三提供了一个长度为4的数组,
但是第一维和最后一维的数字要求一定是1,这是因为卷积层的步长只对矩阵的长和宽有效。最后一个参数是填充(padding)
TensorFlow提供了SAME和VALID两种选择,SAME表示添加全0填充,VALID表示不添加
'''
conv = tf.nn.conv2d(input, filter_weight, strides=[1, 1, 1, 1], padding='SAME')
'''
tf.nn,bias_add提供了一个方便的函数给每一个节点加上偏置项,这里不能直接使用加法,因为矩阵上不同位置
上的节点都需要加上同样的偏置项。
'''
bias = tf.nn.bias_add(conv, biases)
# 将计算结果通过ReLU激活函数完成去线性化
actived_conv = tf.nn.relu(bias)
复制代码
2.2 池化层
- 在卷积层之间,往往会加上池化层(pooling layer)。池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度,也有防止过拟合问题的作用;
- 池化层的前向传播类似卷积层,也是移动一个过滤器来实现的。不过池化层过滤器中的计算不是节点的加权和,而是采用更简单的最大值或者平均值计算。使用最大值操作的池化层被称之为最大池化层,这是被使用得最多的池化层结构。使用平均值操作的池化层被称为平均池化层;
- 卷积层和池化层中过滤器的移动方式是相似的,唯一的区别在于卷积层使用的过滤器是横跨整个深度的,而池化层使用的过滤器只影响一个深度上的节点。所以池化层的过滤器除了在长和宽两个维度移动,它还需要在深度这个维度移动。
以下TensorFlow程序实现了最大池化层的前向传播:
# tf.nn.max_pool实现了最大池化层的前向传播,它的参数和tf.nn.conv2d函数类似
# tf.nn.avg_pool来实现平均池化层
# ksize提供了过滤器的尺寸,strides提供了步长信息,padding决定是否要全0填充
pool = tf.nn.max_pool(activted_conv, ksize = [1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
复制代码
3.经典卷积网络模型
3.1 LeNet-5模型
Lenet-5模型结构,一共有七层:
- 卷积层:这一层就是原始图像像素,LeNet模型接收的输入层大小为32x32x1。过滤器尺寸为5x5,深度为6,不使用全0填充,步长为1。因为没有使用全0填充,所以输出尺寸为32-5+1 = 28,深度为6.这个卷积层一共有5x5x1x6+6 = 156个参数,其中6个为偏置项参数。因为下一层节点矩阵有28x28x6 = 4071个节点,每个节点与5x5 = 25个当前层节点相连,所以本层一共有4071x(25+1) = 122304个连接;
- 池化层:这层的输入就是第一层的输出,是一个28x28x6的节点矩阵。过滤器大小为2x2,长和宽的步长均为2,所以本层输出矩阵大小为14x14x6;
- 卷积层:本层的输入矩阵大小为14x14x6,使用的过滤器大小为5x5,深度为16。不使用全0填充,步长为1。输出矩阵大小为10x10x16。按照标准的卷积层,本层应该有5x5x6x16+16 = 2416个参数,10x10x16x(25+1) = 41600个连接;
- 池化层:输入矩阵大小为10x10x16,过滤器大小为2x2,步长为2。输出矩阵为5x5x16;
- 全连接层:输入矩阵大小为5x5,过滤器大小为5x5,在LeNet-5里面称之为卷积层,因为输入矩阵跟过滤器大小相同,所以可以看做全连接层。本层的输出节点个数为120,共有5x5x16x120+120 = 48120个参数;
- 全连接层:输入节点个数为120,输出节点个数84,总共参数为120x84+84 = 10164个;
- 全连接层:输入节点个数84,输出节点个数10,共有参数84x10+10 = 850个参数。
LeNet-5模型应用总结:
pool.get_shape函数可以得到输出矩阵的维度,tf.reshape函数可以将get_shape获得的数据变成一个batch的向量dropout概念:dropout在训练时会随机将部分节点的输出改为0。dropout可以避免过拟合问题,从而使得模型在测试数据上的效果更好。dropout一般只在全连接层而不是卷积层或者池化层使用。
以下正则表达式总结了一些经典的用于图片分类问题的卷积神经网络架构:
输入层 --> (卷积层 +--> 池化层 ? ) +--> 全连接层 +
在以上公式中,“卷积层+”表示一层或多层卷积层。“池化层?”表示没有或者一层池化层。在多轮卷积层加池化层后,卷积神经网络在输出之前一般会经过1~2个全连接层。比如LeNet-5模型可以表示为下式:
输入层-->卷积层-->池化层-->卷积层-->池化层-->全连接层-->全连接层-->输出层
卷积神经网络参数配置总结:
- 一般卷积层的过滤器边长不会超过5,但有些卷积神经网络中,处理输入的卷积层使用了边长7甚至是11的过滤器;
- 在过滤器的深度上,大部分卷积神经网络都采用逐层递增的方式;
- 卷积层的步长一般为1,但有些模型上也会用2、3作为步长;
- 池化层的配置相对简单,一般用的是最大池化层;
- 池化层的过滤器边长一般为2或者3,步长一般也为2或者3。
3.2 Inception-v3模型
与LeNet-5结构完全不同的卷积神经网络结构。在Inception-v3模型中的Inception结构是将不同的卷积层通过并联的方式结合在一起。即使用边长为1、3、5的过滤器,就是所有不同尺寸的过滤器,然后再将得到的矩阵拼接起来。
Inception模块会首先使用不同尺寸的过滤器处理输入矩阵。不同的矩阵代表了Inception模块中的一条计算路径。虽然过滤器大小不一,但如果所有的过滤器都使用全0填充且步长为1,那么前向传播得到的结果矩阵的长和宽都与输入矩阵一致,这样经过不同过滤器处理的结果矩阵可以拼接成一个更深的矩阵,可以将它们在深度这个维度上组合起来。
Inception-v3模型总共有46层,由11个Inception模块组成,有96个卷积层。如果按照上一节,五行代码实现一个卷积层,那就需要480行代码,无疑太过繁琐。这里介绍TensorFlow-Slim工具来更简洁地实现一个卷积层:
# 直接使用TensorFlow原始API实现卷积层
with tf.variable_scope(scope_name):
weights = tf.get_variable("weights", ...)
biases = tf.get_variable("bias", ...)
conv = tf.nn.conv2d(...)
relu = tf.nn.relu(tf.nn.bias_add(conv, biases))
# 使用TensorFlow-Slim实现卷积层
# slim.conv2d函数的有3个参数是必填的。第一个参数为输入节点矩阵
# 第二个参数为当前卷积层过滤器的深度
# 第三个参数为过滤器的尺寸
# 可选的参数有过滤器移动的步长、是否选用全0、激活函数的选择以及变量的命名空间等
net = slim.conv2d(input, 2, [3, 3])
复制代码
下面代码实现了一个Inception模块:
import tensorflow as tf
# 加载slim库
slim = tf.contrib.slim
'''
slim.arg_scope函数可以用于设置默认的参数取值。slim.arg_scope函数的第一个参数是一个函数列表
在这个列表中的函数将使用默认的参数取值。比如通过下面的定义,调用slim.conv2d(net, 320, [1, 1]函数时会自动加上
stride=1和padding='SAME'的参数。如果在函数调用时指定了stride,那么这里设置的默认值就不会再使用
通过这种方式可以减少冗余的代码
'''
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
stride=1, padding='VALID'):
'''
此处省略了Inception-v3模型中其他的网络结构而直接实现最后面红色方框中的Inception结构
假设输入图片经过之前的神经网络前向传播的结果保存在变量net中
'''
net = '上一层的输出节点矩阵'
# 为一个Inception模块声明一个统一的变量命名空间
with tf.variable_scope('Mixed_7c'):
# 给Inception模块中每一条路径声明一个命名空间
with tf.variable_scope('Branch_0'):
# 实现一个过滤器,边长为1,深度为320的卷积层
branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1')
# Inception模块中第二条路径,这条计算路径上的结构本身也是一个Inception结构
with tf.variable_scope('Branch_1'):
branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1')
'''
tf.concat函可以将多个矩阵拼接起来。tf.concat函数的第一个参数指定了拼接的维度
这里给出的'3'代表了矩阵是在深度这个维度上进行拼接。
'''
branch_1 = tf.concat(3,
[slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'),
slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')])
# Inception中的第三条路径,此计算路径也是一个Inception结构
with tf.variable_scope('Branch_2'):
branch_2 = slim.conv2d(net, 448, [1, 1], scope='Conv2d_0a_1x1')
branch_2 = slim.conv2d(net, 384, [3, 3], scope='Conv2d_0b_3x3')
branch_2 = tf.concat(3,
# 此处2层卷积层的输入都是branch_1而不是net
[slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3')],
[slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')])
# Inception中的第四条路径
with tf.variable_scope('Branch_3'):
branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3')
branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')
# 当前Inception模块的最后输出是上面四个计算结果拼接得到的
net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])
复制代码
4.卷积神经网络迁移学习
所谓迁移学习,就是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。根据论文DeCAF中的结论,可以保留训练好的Inception-v3模型中所有卷积层的参数,只是替换最后一层全连接层,在最后这一层全连接层之前的网络层称之为瓶颈层。将新的图像通过训练好的卷积神经网络直到瓶颈层的过程可以看成是对图像进行特征提取的过程。在训练好的Inception-v3模型中,因为将瓶颈层的输出再通过一个单层的全连接神经网络可以很好地区分1000种类别的图像,所以有理由认为瓶颈层输出的节点向量可以直接利用这个训练好的神经网络对图像进行特征提取,然后再将提取到的特征向量作为输入来训练一个新的单层全连接神经网络处理新的分类问题。
附注:自然语言处理
语言模型
假设一门语言中所有可能出现的句子服从某一个概率分布,每个句子出现的概率和为1,那么语言模型的任务就是预测每个句子在语言中出现的概率。对于语言中常见的句子,一个好的语言模型应得出相对较高的概率,而对于不合法的句子,计算出的概率则应该接近于0。那么如何计算一个句子的概率呢?首先一个句子可以被看成是一个单词序列:
其中m为句子的长度,那么,它的概率可以表示为:
表示,已知前m-1个单词时,第m个单词为wm的概率。如果能对这一项建模,那么只要把每个位置的条件概率相乘,就能计算一个句子出现的概率。常见的有n-gram模型、决策树、最大熵模型、条件随机场、神经网络语言模型等。
语言模型的评价方法
评价语言模型效果好坏的常用评价指标时复杂度(perplexity)。在一个测试集上得到的复杂度越低,说明建模效果越好。
计算复杂度的公式如下:
从这个公式看出,比如已知 这句话会出现在语料库中,那么通过语言模型计算得到的这句话的概率越高,说明语言模型对这个语料库拟合得好。
在语言模型的训练中,通常采用perplexity的对数表达形式:
这样变累乘为累加。
在数学上,log perplexity可以看成真实分布与预测分布之间的交叉熵,交叉熵描述了两个概率分布之间的一种距离。 假设x是一个离散变量,u(x)和v(x)是两个与x相关的概率分布,那么u和v之间交叉熵的定义是在分布u下-log(v(x))的期望值。
把x看做单词,u(x)为每个位置上单词的真实分布,v(x)为模型的预测分布p(wi|w1, w2, ..., wi-1),就可以看出log perplexity和交叉熵是等价的。