アカリの部屋

利用卷积神经网络实现图片多分类

多分类问题

假定有一个32*32*3的图片,我们希望知道它是0、1、2中的哪一类,那么我们可以通过一个得分函数 $ f(x,W) = Wx $ 来学习出一个最好的得分矩阵 $ W $ 来算出得分向量 $ f $,这里的 $ x $ 表示3072*1的图片数据矩阵,而 $ f $ 最终的结果是3*1的,所以得分矩阵 $ W $ 就是3*3072的。最终,得到的 $ f $ 可能是比如 $ [-96.8, 437.9, 61.95] $,即在第二个分类上得分很高。

学习的目标就是得到一个尽量好的 $ W $,使得一张新图片x在正确的分类上分数高,在错误的分类上分数低。
我们可以把矩阵 $ W $ 当中的每一个列看作是某一种特征,每一行(行向量)就是某一种分类结果所对应的超平面系数,这样,这个矩阵就可以理解成多种分类在高维坐标系上的切分准则。
我们也可以把每一行和对应的b联合起来理解为一种匹配模板,每一类的得分,就是像素点和模板的匹配度(内积)。

logo

机器学习矩阵乘法的几何意义

机器学习使用 $ Wx+b=f $ 做为预测结果,对于一张图片的特征向量 $ x $,$ W $ 的每一行代表一种分类结果的一套特征(比如猫、狗..),$ W $ 的每一列代表一个特征的权重性(比如头发的长短),$ W $ 的每一行和原始图片向量 $ x $ 做点积,能得到 $ x $ 在 $ W $ 这一行对应的分类上的得分,最终输出Softmax后的结果作为分类概率。向量与矩阵的乘积(即矩阵每一行向量和另一个向量的点击之和)的几何意义是,两个向量的点积为0,表示两个向量正交,是90度,即两个向量一点都不相似,而点积结果如果极大,则方向很近,相似度高。所以W就是一种机器观察一个物体时在各种特征上的观察方式,如果W不够好,预测结果就不好,机器学习的目的就是纠正 $ W $。

损失函数与Softmax回归

一开始我们并不知道W是什么样的,所以学习过程要不断调整W中的参数,并且用损失函数来衡量W和实际情况之间的差异,目的是让损失函数最小化。

损失函数类似于判卷方式,不同老师判卷方式不一样。

比如:合页损失(hinge loss),也叫支持向量机损失,算法是,遍历在所有错误类别上的得分,保证它在正确分类上的得分,必须要比它在其他分类上的得分都高出一段 $ Δ $ ,如果不能保证,就计算在这个分类上的损失。$ Δ $ 是一个超参数,作为是否需要修正学习器的警戒线,调整它可以调整全局的学习效果。
比如一个3分类,就有一个3*1的得分向量,假设是 $ [13,-7,11] $ ,假设正确的答案是它属于第一个分类,假设 $ Δ $ 是10,显然,这个W区分前两个分类表现得很好,不需要再给惩罚去督促学习器调整自己,但是区分第一个和第三个就做的很差,需要修正,那么损失就是 $ L{_i} = max(0, -7-13+10) + max(0, 11-13+10) = 8 $。

另一种是交叉熵损失,假设之前已经有了 $ [-96.8, 437.9, 61.95] $ 三个得分,我想知道一个很确定的概率告诉我就是第二类,那么我们就希望把这个得分向量转化为概率向量,那么就需要做归一化。这个归一化的方式是: $ e^{437.9} / (e^{-96.8} + e^{437.9} + e^{61.95}) $ ,其中e的n次方可以保证结果不为负,这样一来就能得到 $ [P{_1}, P{_2}, P{3}] $ 这个我们算得的概率向量,这个向量和标准的向量 $ [0,1,0] $ 之间有些差距,那么就可以通过KL距离(也叫变分、相对熵)来计算这个差距具体有多大( $ L=-ylog(y_预) $ ),结果就是交叉熵损失,这个过程叫*Softmax回归,这是逻辑回归在多变量上的一般形式。在分类数很多时,log操作的概率转换性能会是个瓶颈,通常用合页损失代替。

使用Softmax和多个LR做多分类的选择方式是,前者要求分类结果必须互斥,后者允许打多个标签。

logo

图像分类问题

对于分辨率巨大的图片,它的输入可能是个巨大的三维矩阵[100010003],在DNN和传统ML中的习惯就是把这个图片拉成一个单行向量,它就有300万个元素。那么第一个隐层可能会把这300万个输入转化为4000个神经元,那么就有300万*4000个 $ w $ 需要更新,参数量太大会导致显存没法算,也容易过拟合。为了保证DNN的学习能力,又能把维度数量降下来,出现了卷积神经网络(CNN)。CNN有很多层,每一层都有自己的功能。

logo

在图片领域有很多学术界的数据集:
CIFAR-10,是有十个类别的图片数据集,每个类有5000张训练集1000张测试集,大小是 $ 32323 $。
CIFAR-100,是有100个分类CIFAR-10。
MNIST,是手写数据识别数据集,10个分类,6万张训练数据,1万张测试数据,$ 28281 $。
SVHN,是斯坦福吴恩达做的谷歌卫星拍摄的美国门牌号码的数字,10个分类,73257训练26032测试,$ 32323 $。
Caltech 101,是李飞飞做的,有101个分类,5050张图片,$ 3002003 $的数据集,维度较高。

Imagenet,有1000个分类,1.4M张图片,通常归一化到 $ 2562563 $,是一个大规模的,可以用在工业上的数据集,是目前能下载到的最大的数据集,基于这个数据集于有比赛。
在NN火起来之前,有人用OpenCV提取大量特征训练SVM这类浅层模型,在2011年错误率是25.8%,从2012年之后都在用CNN,2012年的AlexNet错误率降到了16.4%,之后又有VGG、GoogleNet,层数越来越深,2015年微软的152层的ResNet已经把错误率降低到3.57%,已经低于人的错误率了。

Input(输入层)

首先要对原始图像做大小(维度)统一,才能灌进神金网络。

这一层相当于预处理
1.去均值,把原始数据求出均值点,把每个点减去这个军指点,就能把数据的中心店平移到坐标轴原点。注意,测试集的话减的是训练集的均值矩阵,要保证训练集和测试集做同样的操作。
2.归一化,在ML中如果各个维度的分布很不均匀,有的在0.01-0.1,有的极大,则在数学上不好优化,需要压缩到同一个范围。
3.PCA白化,先对数据做PCA降维,再对每个特征归一化。

logo

CONV(卷积层)

卷积操作,实际是在算卷积核和图像上的某个区域的相似度,结果就是整张图片和卷积核的相似度,所以卷积核就可以看作一个“特征”,去问图片上哪些部分符合这个特征。而很多的真有用的卷积核并不是凭空造出来的,而是通过CNN学出来的。

CNN中特征抽取的工作主要是卷积层做的,在全连接的DNN中,后一层的神经元能看到前一层每个神经元的输出,也就是看到了整张图片,而CNN的卷积层的目的,是要保证所有维度都参与运算,又要把维度降下来。

CNN会一块一块地看图片。假设原始图片是32*32*3的,3表示颜色通道,下一层有5个神经元,这些神经元叫做卷积核(定义为厚度depth=5),每个神经元取一个3*3*3的窗口,它像一个眼睛一样,从图片最左上角按照读书的顺序一块一块地看这张图,就是说,这个卷积核的维度 $ w $ 是3*3*3。

如果图片很大,神经元可以只负责一个区域,并且学到的特征在整张图片都通用。

在这个滑动中有一些细节,需要事先计算好,比如:
步长(stride),即滑动多长,这会允许每一次看一小块都能够有有些重叠,这样可以捕捉到更细节的信息。
填充值(zero-padding),比如图片宽度32,而神经元的视野是3,不能整除,所以要以0填充。

在卷积核视野移动的过程中,每一次移动都会看到一个很小的范围,这个卷积核自身带有一个矩阵,比如[1,0,1, 0,1,0, 1,0,1](当然,在训练过程中这个矩阵是要随机初始化然后学出来的,在使用的时候是则固定的),它是一个抽特征的滤波器,它能从自己的角度(人可能无法解释的角度)抽出图像中和自身相似的特征出来。
因为两个矩阵的内积(标量)越大二者相似度就越高,所以卷积核会把它看到的内容(小矩阵)和自带的滤波器矩阵 $ w $ 做内积,输出一个标量,这样,走一轮下来,每个位置就会得到一个新的标量,最终就会得到一个矩阵。当然,因为图像有RGB三个通道,所以输出出来的是3个3*3的矩阵,他们通过线性加法,加上偏置项,就是一个这个卷积核最终的矩阵。那么,如果我要判断图像中有没有一个鼻子,只要看图片里是不是有一块和这个卷积核的内积高不高就可以了,所以CNN适合提取图片特征。
实际上CNN中的卷积操作并不是严格的内积,卷积操作并不做转置,是因为卷积核的空间是对称的,所以如果有一个卷积核A,那么一定有一个对称的卷积核B,使得B的转置就是A,如果加上转置的话,那么二者在CNN里效果一样。

于是重新解释一下,如果下一层有5个神经元(卷积核),那么5个卷积核各自看一轮图片,就能得到5个3*3的矩阵了,每个矩阵内都包括三个颜色通道叠加起来的信息。卷积核越多,抽取特性能力可能就越丰富,实际上可能会有上百个卷积核,但是也有过拟合的问题。

回到我们的意图中来,卷积层实际就是在做特征抽取,并且将巨大的图片,转化为很多个小图片。

CNN的一个卷积层有多少个卷积核,下一层就有多少个通道,$ 输出图像的维数=(加0后图像大小-卷积核大小)/步长+1 $ 。注意在配置参数时,必须能被步长整除。此外,1*1的卷积核可以减少图片通道数,而不改变图片大小。

logo

某一层(的cell)的感受野

比如在对图像做语义分割的时候,需要对图片做像素级别的分隔,对每个像素描述了什么做分类,所以不能孤立地拿每一个独立的像素点做判断,要结合它周围区域的信息来做推测。

所谓的周围区域就是“感受野”,感受野可以帮你清晰的认识到你在某一层做的一些事情(比如卷积变换)会受到多大区域的影响,它的本质是某一层特征图(输出结果)中的某一个cell(格子/矩阵中的一个值)对应到原图输入的响应区的大小

从CNN的第一层开始,每个卷积核都有一定的大小,也就是在循环的过程中,一次只能看图片的一部分,比如 $ 1111 $ 的原始图像map1,经过一层 $ 55 $ 的卷积核,输出 $ 77 $ 的map2,它再经过一次 $ 77 $ 的卷积核,输出 $ 11 $ 的map3,则map3的感受野大小就是 $ 1111 $ 。感受野既不能太小(如 $ 1*1 $ ),这没有意义,又不能太大(如用整图的信息),失去了卷积操作减小数据量的意义。

感受野的计算是要逐层倒序回溯到原始输入的,根据求下一层输出大小的公式:
$$ output_field_size = (input_field_size - kernel_size + 2 padding) / stride + 1 $$
做变形,得到根据输出结果计算上一层感受野的公式:
$$ input_field_size = (output_filed_size - 1)
stride - 2 * padding + kernel_size $$

RELU(激励层)

在DNN层间一定要有非线性变换,激励层有很多非线性映射,如Sigmoid、Tanh、ReLU、Leaky ReLU、Maxout等等。

要慎用Sigmoid,它虽然能把数据映射到(0,1),但是它在两边的导数接近于0,所以在随机梯度下降时更新几乎极慢,而且由于神经网络是多函数联合求导,它会拖垮其他神经元,这个过程叫梯度弥散
可以先尝试ReLU,如果Loss不往下降了,检查可能是某些神经元挂掉了,就试试Leaky ReLU或者Maxout。

这一层当中是对卷积层输出的每一个”点”做激活运算。有时候习惯把卷积层和激励层放在一起叫做卷积层。

logo

POOL(池化层)和FC(全连接层)

池化层也叫降维层,意图是压缩数据和参数的量,以丢失信息的方式减少过拟合的风险,比如,把224*224*64的图像压缩到112*112*64,类似于模糊化。当然,因为要看图,所以这一层也有窗口步长等参数。这一层有一些做法,比如Max polling的意图是捕捉图像中的主要特征,比如2*2的窗口,它会选出4个值中最大的一个,这也可以理解成降低误差增加了鲁棒性,它在图像识别领域效果比average polling好,后者的做法是求平均再取整。

一般来讲,不做polling的效果更好,毕竟是丢失信息的降维操作,从2015年之后的优秀网络都不做降采样了。

logo

CNN的结构一般是:INPUT -> [[CONV -> RELU]*N -> POOL?]*M -> FC

全连接层通常在CNN的尾部,把最后的CONV输出的结果拉成一个列向量,再和后面的FC层做全连接,将前一层的所有输入全都连接起来,输出成个各分类,当然也有一些网络并没有FC层。

训练和过拟合

在CNN中,每个卷积核的 $ w $ 是要学习出来的,同DNN,先定义损失函数,通过BP和SGD找差距,对于DNN可以连续求导,只不过卷积层和池化层的求导过程会更加复杂。

神经网络是一个容量很大的模型,所以很容易过拟合,有一种应对方式叫随机失活(Dropout),即在任何神经元往下传递的时候,都有一定的概率p被乘以0,即被关掉,不往下传递信息,当然,训练完成后依旧是一个全部的权重。形象的来说,就是让神经元一次少看到点东西,来防止过拟合。从集成学习的角度来说,等于是每次传导都做出了不同的模型,类似于随机森林,这样听取众人意见的学习方式就防止只用某一种模型描述太容易过拟合的问题。

logo

此外,在这里CONV层的参数很多,在和FC层全连接之前,可能会加一个全局最大池化层,来极大地减少参数数量,防止过拟合。

优秀模型

LeNet-5:是2层卷积3层全连接的网络,是第一次成功进行手写数字识别的网络。
AlexNet(2012):是5层卷积4层全连接的网络,第一次使用了ReLu,增加了dropout。
VGGNet16/19(2014):12/14层卷积3层全连接,并不是每次卷积后都池化。
GoogleLeNet(2014):22层的网络,使用inception模块,之前的CNN从一层到下一层卷积核都是一样的大小,但是一个inception模块当中包含了不同大小的卷积核,这就解决了不同图片当中不同细节(比如鼻子)占比大小不同的问题。
ResNet(2016):152层网络,在每两层卷积层之间增加了跳跃连接F(X)+X来做ReLu,这样原来要跳两次才能传播的梯度之遥跳一次,这样两层卷积核就可以看作是一层了,防止了梯度消失。不用dropout,只在最后一层做池化。

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
# PY2.7支持新特性
from __future__ import print_function
# 导入TensorFlow包
import tensorflow as tf
# 导入数字分类测试数据集mnist
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
# 学习率
learning_rate = 0.001
# 迭代次数
training_iters = 12800
# 每批进入的图片数
batch_size = 128
# 每执行多少轮训练输出一次信息
display_step = 10
# 固定图片大小28*28
n_input = 784
# 将0-9分成10类
n_classes = 10
# 随机失活过程中,信号能通过的概率
dropout = 0.75
# 设置容器来承载输入、输出、维持不失活的概率
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])
keep_prob = tf.placeholder(tf.float32)

定义神经网络

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
# 自己封装的卷积层
def conv2d(x, W, b, strides=1):
# 卷积层(使用自定义参数W,步长为1)
x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
x = tf.nn.bias_add(x, b)
# 激活层relu
return tf.nn.relu(x)
# 池化层
def maxpool2d(x, k=2):
# 通过MaxPolling方式将图片压缩
return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1], padding='SAME')
# 设置各层的权重初始值,即wx+b的w和b
weights = {
# 宽度5 高度5 通道数1 32个卷积核
'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
# fully connected, 7*7*64 inputs, 1024 outputs
'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
# 1024 inputs, 10 outputs (class prediction)
'out': tf.Variable(tf.random_normal([1024, n_classes]))
}
biases = {
'bc1': tf.Variable(tf.random_normal([32])),
'bc2': tf.Variable(tf.random_normal([64])),
'bd1': tf.Variable(tf.random_normal([1024])),
'out': tf.Variable(tf.random_normal([n_classes]))
}
# 封装整个卷积神经网络
def conv_net(x, weights, biases, dropout):
# 将图片文件reshape成28*28*1,1为通道数
# -1表示自动识别有几张输入图片
x = tf.reshape(x, shape=[-1, 28, 28, 1])
# 卷积层+激励层1
conv1 = conv2d(x, weights['wc1'], biases['bc1'])
# 池化层1
conv1 = maxpool2d(conv1, k=2)
# 卷积层+激励层2
conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])
# 池化层2
conv2 = maxpool2d(conv2, k=2)
# 全连接层
fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
fc1 = tf.nn.relu(fc1)
# 随机失活
fc1 = tf.nn.dropout(fc1, dropout)
# 输出层
out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
return out

定义算法细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 初始化卷积神经网络
pred = conv_net(x, weights, biases, keep_prob)
# 定义softmax交叉熵损失函数
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
# 定义使用adam优化器优化损失
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
# 定义如何表示准确度
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
# 初始化全局变量
init = tf.global_variables_initializer()
# 使用GPU
# config = tf.ConfigProto()
# config.gpu_options.allow_growth = True

计算图

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
# TensorFlow计算图
with tf.Session() as sess:
sess.run(init)
# 训练循环
step = 1
while step * batch_size < training_iters:
# 读取数据
batch_x, batch_y = mnist.train.next_batch(batch_size)
# 执行反向传播优化过程
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y, keep_prob: dropout})
# 每执行display_step轮输出一次信息
if step % display_step == 0:
# Calculate batch loss and accuracy
loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x, y: batch_y, keep_prob: 1.})
print("Iter " + str(step*batch_size) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
step += 1
print("Optimization Finished!")
# 计算测试集上的准确度,最后keep_prob=1表示所有神经元都不失活
print("Testing Accuracy:", \
sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
y: mnist.test.labels[:256],
keep_prob: 1.}))

从输出中可以看出随着训练的进行,损失不断减小,正确率不断上升

1
2
3
4
5
6
7
8
9
10
11
Iter 1280, Minibatch Loss= 22621.132812, Training Accuracy= 0.32812
Iter 2560, Minibatch Loss= 9091.511719, Training Accuracy= 0.56250
Iter 3840, Minibatch Loss= 8080.745117, Training Accuracy= 0.63281
Iter 5120, Minibatch Loss= 5149.162109, Training Accuracy= 0.78906
Iter 6400, Minibatch Loss= 4444.751465, Training Accuracy= 0.75000
Iter 7680, Minibatch Loss= 3592.729248, Training Accuracy= 0.85938
Iter 8960, Minibatch Loss= 4244.142578, Training Accuracy= 0.84375
Iter 10240, Minibatch Loss= 4101.846680, Training Accuracy= 0.82031
Iter 11520, Minibatch Loss= 2199.232666, Training Accuracy= 0.89844
Optimization Finished!
Testing Accuracy: 0.855469