(11-3-01)图像分类:基于卷积神经网络的图像分类(1)

11.3  基于卷积神经网络的图像分类

在本章前面介绍的是基于机器学习的图像分类,本书的重点是基于深度学习的图像处理,基于深度学习的图像分类技术主要有卷积神经网络、迁移学习、循环神经网络的、卷积循环神经网络等。在本节的内容中,将首先讲解基于卷积神经网络的图像分类知识。

11.3.1  卷积神经网络基本结构

基于卷积神经网络(CNN)是当前最主流和成功的图像分类方法之一。它通过多个卷积层和池化层来学习图像的特征表示,随后通过全连接层进行分类。著名的CNN模型包括LeNet、AlexNet、VGGNet、ResNet、Inception等。基础的CNN由卷积(convolution)、激活(activation)和池化(pooling)三种结构组成。CNN输出的结果是每幅图像的特定特征空间。当处理图像分类任务时,会把CNN输出的特征空间作为全连接层或全连接神经网络(Fully Connected Neural Network,FCN)的输入,用全连接层来完成从输入图像到标签集的映射,即分类。当然,整个过程最重要的工作就是如何通过训练数据迭代调整网络权重,也就是后向传播算法。

在接下来的内容中将详细讲解卷积神经网络的基本结构。

1. 卷积层

卷积层是卷积网络的核心,大多数计算都是在卷积层中进行的。卷积层的功能是实现特征提取,卷积网络的参数是由一系列可以学习的滤波器集合构成的,每个滤波器在宽度和高度上都比较小,但是深度输入和数据保持一致。当滤波器沿着图像的宽和高滑动时,会生成一个二维的激活图。

卷积层的参数是有一些可学习的滤波器集合构成的。每个滤波器在空间上(宽度和高度)都比较小,但是深度和输入数据一致(这一点很重要,后面会具体介绍)。直观地来说,网络会让滤波器学习到当它看到某些类型的视觉特征时就激活,具体的视觉特征可能是某些方位上的边界,或者在第一层上某些颜色的斑点,甚至可以是网络更高层上的蜂巢状或者车轮状图案。

2. 池化层

通常在连续的卷积层之间会周期性地插入一个池化层,它的作用是逐渐降低数据体的空间尺寸,这样的话就能减少网络中参数的数量,使得计算资源耗费变少,也能有效控制过拟合。池化层使用 MAX 操作,对输入数据体的每一个深度切片独立进行操作,改变它的空间尺寸。

请看一个在现实中池化层的应用例子:图像中的相邻像素倾向于具有相似的值,因此通常卷积层相邻的输出像素也具有相似的值。这意味着,卷积层输出中包含的大部分信息都是冗余的。如果我们使用边缘检测滤波器并在某个位置找到强边缘,那么也可能会在距离这个像素1个偏移的位置找到相对较强的边缘。但是它们都一样是边缘,我们并没有找到任何新东西。池化层解决了这个问题。这个网络层所做的就是通过减小输入的大小降低输出值的数量。池化一般通过简单的最大值、最小值或平均值操作完成。

3. 全连接层

全连接层的输入层是前面的特征图,会将特征图中所有的神经元变成全连接的样子。这个过程为了防止过拟合会引入Dropout。在进入全连接层之前,使用全局平均池化能够有效的见地过拟合。

对于任一个卷积层来说,都存在一个能实现和它一样的前向传播函数的全连接层。该全连接层的权重是一个巨大的矩阵,除了某些特定块(感受野),其余部分都是零;而在非 0 部分中,大部分元素都是相等的(权值共享),具体可以参考图3。如果把全连接层转化成卷积层,以某个深度学习模型的输出层(例如,最后一层)为例,假设与它有关的输入神经元只有上面四个,所以在权重矩阵中与它相乘的元素,除了它所对应的4个,剩下的均为0,这也就解释了为什么权重矩阵中有为零的部分;另外要把“将全连接层转化成卷积层”和“用矩阵乘法实现卷积”区别开,这两者是不同的,后者本身还是在计算卷积,只不过将其展开为矩阵相乘的形式,并不是“将全连接层转化成卷积层”,所以除非权重中本身有零,否则用矩阵乘法实现卷积的过程中不会出现值为0的权重。

4. 激活层

激活层也被称为激活函数(Activation Function),是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。激活层对于人工神经网络模型去学习、理解非常复杂和非线性的函数来说具有十分重要的作用。它们将非线性特性引入到我们的网络中。例如在矩阵运算应用中,在神经元中输入的 inputs 通过加权求和后,还被作用于一个函数,这个函数就是激活函数。引入激活函数是为了增加神经网络模型的非线性。没有激活函数的每一层都相当于一个简单的线性变换,即矩阵相乘和加法操作。即使在堆叠了多个这样的层之后,整个网络仍然只是一系列简单的线性变换,无法表达非线性关系。

5. Dropout层

Dropout是指深度学习训练过程中,对神经网络训练单元按照一定的概率将其从网络中移除,注意是暂时,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。

Dropout的作用是在训练神经网络模型时样本数据过少,防止过拟合而采用的技术。首先,想象我们现在只训练一个特定的网络,当迭代次数增多的时候,可能出现网络对训练集拟合的很好(在训练集上loss很小),但是对验证集的拟合程度很差的情况。所以有了这样的想法:可不可以让每次跌代随机的去更新网络参数(weights),引入这样的随机性就可以增加网络的概括的能力,所以就有了Dropout。

在训练的时候,我们只需要按一定的概率(retaining probability)p 来对Weight 层(神经网络中的权重层)的参数进行随机采样,将这个子网络作为此次更新的目标网络。可以想象,如果整个网络有n个参数,那么我们可用的子网络个数为 2n 。并且当n很大时,每次迭代更新使用的子网络基本上不会重复,从而避免了某一个网络被过分的拟合到训练集上。

那么在测试的时候怎么办呢?一种基础的方法是把2n个子网络都用来做测试,然后以某种 投票机制将所有结果结合一下(比如说平均一下),然后得到最终的结果。但是,由于n实在是太大了,这种方法实际中完全不可行。所以有人提出做一个大致的估计即可,从2n个网络中随机选取 m 个网络做测试,最后在用某种投票机制得到最终的预测结果。这种想法当然可行,当m很大时但又远小于2n时,能够很好的逼近原2n个网络结合起来的预测结果。但是还有更好的办法:那就是dropout 自带的功能,能够通过一次测试得到逼近于原2n个网络组合起来的预测能力。

6. BN层

BN的全称Batch Normalization,是2015年提出的一种方法,在进行深度网络训练时,大都会采取这种算法。尽管梯度下降法训练神经网络很简单高效,但是需要人为地去选择参数,比如学习率,参数初始化,权重衰减系数,Dropout比例等,而且这些参数的选择对于训练结果至关重要,以至于我们很多时间都浪费到这些调参上。BN算法的强大之处在下面几个方面:

  1. 可以选择较大的学习率,使得训练速度增长很快,具有快速收敛性。
  2. 可以不去理会Dropout,L2正则项参数的选择,如果选择使用BN,甚至可以去掉这两项。
  3. 去掉局部响应归一化层。(AlexNet中使用的方法,BN层出来之后这个就不再用了)
  4. 可以把训练数据打乱,防止每批训练的时候,某一个样本被经常挑选到。

首先来说归一化的问题,神经网络训练开始前,都要对数据做一个归一化处理,归一化有很多好处,原因是网络学习的过程的本质就是学习数据分布,一旦训练数据和测试数据的分布不同,那么网络的泛化能力就会大大降低,另外一方面,每一批次的数据分布如果不相同的话,那么网络就要在每次迭代的时候都去适应不同的分布,这样会大大降低网络的训练速度,这也就是为什么要对数据做一个归一化预处理的原因。另外对图片进行归一化处理还可以处理光照,对比度等影响。

例如网络一旦训练起来,参数就要发生更新,出了输入层的数据外,其它层的数据分布是一直发生变化的,因为在训练的时候,网络参数的变化就会导致后面输入数据的分布变化,比如第二层输入,是由输入数据和第一层参数得到的,而第一层的参数随着训练一直变化,势必会引起第二层输入分布的改变,把这种改变称之为:Internal Covariate Shift,BN就是为了解决这个问题而诞生的。

经过上面的描述可以得出一个结论:卷积神经网络主要由这几类层构成:输入层、卷积层,激活层层、池化(Pooling)层和全连接层(全连接层和常规神经网络中的一样)。通过将这些层叠加起来,就可以构建一个完整的卷积神经网络。在实际应用中,通常将卷积层的输出经过激活函数处理,这样卷积层和激活函数一起被视为整个卷积层的组成部分。属具体说来,卷积层和全连接层(CONV/FC)对输入执行变换操作的时候,不仅会用到激活函数,还会用到很多参数,即神经元的权值w和偏差b;而激活层层和池化层则是进行一个固定不变的函数操作。卷积层和全连接层中的参数会随着梯度下降被训练,这样卷积神经网络计算出的分类评分就能和训练集中的每个图像的标签吻合了。

11.3.2  第一个卷积神经网络程序

例如在下面的实例文件cnn01.py中,将使用TensorFlow创建一个卷积神经网络模型,并可视化评估这个模型。

实例11-5:创建一个卷积神经网络模型并可视化评估

源码路径bookcodes/11/cnn01.py

文件cnn01.py的具体实现流程如下所示

(1)导入TensorFlow模块,代码如下:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import matplotlib.pyplot as plt

(2)下载并准备 CIFAR10 数据集

CIFAR10 数据集包含 10 类,共 60000 张彩色图片,每类图片有 6000 张。此数据集中 50000 个样例被作为训练集,剩余 10000 个样例作为测试集。类之间相互独立,不存在重叠的部分。代码如下:

(train_images, train_labels), (test_images, test_labels) = datasets.cifar10.load_data()

# 将像素的值标准化至0到1的区间内。
train_images, test_images = train_images / 255.0, test_images / 255.0

(3)验证数据

将数据集中的前 25 张图片和类名打印出来,来确保数据集被正确加载。代码如下:

class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck']
plt.figure(figsize=(10,10))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)
    plt.imshow(train_images[i], cmap=plt.cm.binary)
    # 由于 CIFAR 的标签是 array, 
    # 因此您需要额外的索引(index)。
    plt.xlabel(class_names[train_labels[i][0]])
plt.show()

执行后将可视化显示数据集中的前 25 张图片和类名,如图11-3所示

图11-3  可视化显示数据集中的前 25 张图片和类名

(4)构造卷积神经网络模型

通过如下代码声明了一个常见卷积神经网络,由几个 Conv2D 和 MaxPooling2D 层组成。

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

CNN 的输入是张量 (Tensor) 形式的 (image_height, image_width, color_channels),包含了图像高度、宽度及颜色信息。不需要输入Batch size(批大小)。如果您不熟悉图像处理,颜色信息建议您使用 RGB 色彩模式,此模式下,color_channels 为 (R,G,B) 分别对应 RGB 的三个颜色通道(color channel)。在此示例中,我们的 CNN 输入,CIFAR 数据集中的图片,形状是 (32, 32, 3)。您可以在声明第一层时将形状赋值给参数 input_shape。声明 CNN 结构的代码是:

model.summary()

执行后会输出显示模型的基本信息:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 30, 30, 32)        896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 13, 13, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 6, 6, 64)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 4, 4, 64)          36928     
=================================================================
Total params: 56,320
Trainable params: 56,320
Non-trainable params: 0
_________________________________________________________________

在执行后输出显示的结构中可以看到,每个 Conv2D 和 MaxPooling2D 层的输出都是一个三维的张量 (Tensor),其形状描述了 (height, width, channels)。越深的层中,宽度和高度都会收缩。每个 Conv2D 层输出的通道数量 (channels) 取决于声明层时的第一个参数(如:上面代码中的 32 或 64)。这样,由于宽度和高度的收缩,您便可以(从运算的角度)增加每个 Conv2D 层输出的通道数量 (channels)。

(5)增加Dense 层

Dense 层等同于全连接 (Full Connected) 层,在模型的最后,将把卷积后的输出张量(本例中形状为 (4, 4, 64))传给一个或多个 Dense 层来完成分类。Dense 层的输入为向量(一维),但前面层的输出是3维的张量 (Tensor)。因此您需要将三维张量展开 (flatten) 到1维,之后再传入一个或多个 Dense 层。CIFAR 数据集有 10 个类,因此您最终的 Dense 层需要 10 个输出及一个 softmax 激活函数。代码如下:

model.add(layers.Flatten())

model.add(layers.Dense(64, activation='relu'))

model.add(layers.Dense(10))

此时通过如下代码查看完整CNN的结构:

model.summary()

执行后会输出显示

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 30, 30, 32)        896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 13, 13, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 6, 6, 64)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 4, 4, 64)          36928     
_________________________________________________________________
flatten (Flatten)            (None, 1024)              0         
_________________________________________________________________
dense (Dense)                (None, 64)                65600     
_________________________________________________________________
dense_1 (Dense)              (None, 10)                650       
=================================================================

由此可以看出,数据在被传入两个 Dense 层之前,Shap为 (4, 4, 64) 的输出被展平成了Shap为 (1024) 的向量。

(6)编译并训练模型,代码如下:

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

history = model.fit(train_images, train_labels, epochs=10, 
                    validation_data=(test_images, test_labels))

执行后会输出显示训练过程

Epoch 1/10
1563/1563 [==============================] - 7s 3ms/step - loss: 1.5216 - accuracy: 0.4446 - val_loss: 1.2293 - val_accuracy: 0.5562
Epoch 2/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.1654 - accuracy: 0.5857 - val_loss: 1.0774 - val_accuracy: 0.6143
Epoch 3/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.0172 - accuracy: 0.6460 - val_loss: 1.0041 - val_accuracy: 0.6399
Epoch 4/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.9198 - accuracy: 0.6795 - val_loss: 0.9946 - val_accuracy: 0.6540
Epoch 5/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.8449 - accuracy: 0.7060 - val_loss: 0.9169 - val_accuracy: 0.6792
Epoch 6/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.7826 - accuracy: 0.7264 - val_loss: 0.8903 - val_accuracy: 0.6922
Epoch 7/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.7338 - accuracy: 0.7441 - val_loss: 0.9217 - val_accuracy: 0.6879
Epoch 8/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.6917 - accuracy: 0.7566 - val_loss: 0.8799 - val_accuracy: 0.6990
Epoch 9/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.6431 - accuracy: 0.7740 - val_loss: 0.9013 - val_accuracy: 0.6982
Epoch 10/10	
1563/1563 [==============================] - 5s 3ms/step - loss: 0.6074 - accuracy: 0.7882 - val_loss: 0.8949 - val_accuracy: 0.7075

(7)评估我们在上面实现的卷积神经网络模型,首先可视化展示评估过程,代码如下:

plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label = 'val_accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.ylim([0.5, 1])
plt.legend(loc='lower right')
plt.show()

test_loss, test_acc = model.evaluate(test_images,  test_labels, verbose=2)

执行效果如图11-4所示

11-4  评估模型

然后通过如下代码显示苹果结果

print(test_acc)

执行后会输出

0.7038999795913696

未完待续

  • 32
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值