卷积神经网络,也叫CNN,它是计算机视觉应用几乎都在使用的一种深度学习模型。我们都知道,成功提取显著相关的特征是保障任何机器学习的算法成功的保障,传统的机器学习模型依赖领域专家的输入特征,或者基于计算特征的提取技术。神经网络能够自动地从原始数据中学习对特定任务最有用的特征。因此通常把神经网络作为特征提取引擎:即从紧靠着输入层后面的早期层提取低级特征。
卷积神经网络,通过逐层组合低级特征来构造所谓的特征层次,从而形成高级特征。例如可以在处理图像时从早期层把如边缘和斑点之类的低级特征提取出来,然后把它们组合在一起形成如建筑物、汽车或狗的形状的高级特征。
正如在下面的图片中可以看到的,CNN根据输入图像计算特征映射,其中每个元素都来自于输入图像中的局部像素区:
该局部像素区被称之为局部接收场。CNN通常会很好的完成与图像相关的任务,这主要是因为下面两个重要思想:
卷积是CNN的基本操作,因此了解该操作的工作原理很重要。密集连接层与卷积层的根本区别在于:Dense层从输入特征空间中学到的是全局模式,而卷积层学到的是局部模式。(见下图),对于图像来说,学到的就是在输入图像的二维小窗口中发现的模式。
这个重要特性使卷积神经网络具有以下两个性质。
对于包含两个空间轴(高度和宽度)和一个深度轴(也叫通道轴)的 3D 张量,其卷积也叫特征图(feature map)。对于 RGB 图像,深度轴的维度大小等于 3,因为图像有 3 个颜色通道:红色、绿色和蓝色。对于黑白图像,深度等于 1(表示灰度等级)。卷积运算从输入特征图中提取图块,并对所有这些图块应用相同的变换,生成输出特征图(output feature map)。该输出特征图仍是一个 3D 张量,具有宽度和高度,其深度可以任意取值,因为输出深度是层的参数,深度轴的不同通道不再像 RGB 输入那样代表特定颜色,而是代表过滤器。
卷积由以下两个关键参数所定义。
卷积的工作原理:在 3D 输入特征图上滑动这些 3×3 或 5×5 的窗口,在每个可能的位置停止并提取周围特征的 3D 图块[形状为 (window_height, window_width, input_depth)]。然后每个 3D 图块与学到的同一个权重矩阵[叫作卷积核]做张量积,转换成形状为 (output_depth,) 的 1D 向量。然后对所有这些向量进行空间重组,使其转换为形状为 (height, width, output_depth) 的 3D 输出特征图。输出特征图中的每个空间位置都对应于输入特征图中的相同位置(比如输出的右下角包含了输入右下角的信息)。举个例子,利用 3×3 的窗口,向量 output[i, j, :] 来自 3D 图块 input[i-1:i+1,
j-1:j+1, :]。整个过程详见下图。
有这样一种说法,仅在有大量数据可用时,深度学习才有效。这种说法部分正确:深度学习的一个基本特性就是能够独立地在训练数据中找到有趣的特征,无须人为的特征工程,而这只在拥有大量训练样本时才能实现。对于输入样本的维度非常高(比如图像)的问题尤其如此。但对于初学者来说,所谓“大量”样本是相对的,即相对于你所要训练网络的大小和深度而言。只用几十个样本训练卷积神经网络就解决一个复杂问题是不可能的,但如果模型很小,并做了很好的正则化,同时任务非常简单,那么几百个样本可能就足够了。由于卷积神经网络学到的是局部的、平移不变的特征,它对于感知问题可以高效地利用数据。虽然数据相对较少,但在非常小的图像数据集上从头开始训练一个卷积神经网络,仍然可以得到不错的结果,而且无须任何自定义的特征工程。
在这个例子中我们将重点讨论猫狗图像分类,数据集中包含 4000 张猫和狗的图像(2000 张猫的图像,2000 张狗的图像)。我们将 2000 张图像用于训练,1000 张用于验证,1000张用于测试。
下载原始数据集:https://www.kaggle.com/c/dogs-vs-cats/data如果没有 Kaggle 账号的话,注册一个即可。这个数据集包含 25 000 张猫狗图像(每个类别都有 12 500 张),大小为 543MB。下载数据并解压之后,你需要创建一个新数据集,其中包含三个子集:每个类别各 1000 个样本的训练集、每个类别各 500 个样本的验证集和每个类别各 500 个样本的测试集。
一级目录:一共有三个文件夹,分别是测试集,训练集,验证集。
二级目录:(以训练集为例)一共有两个文件夹,分别是猫和狗。
三级目录:(训练集中的狗图像为例)训练集中一共有1000张狗的图像。
代码实现:
import os, shutil
original_dataset_dir='D:\dogs-vs-cats\train' # 这是我自己存储完整数据的地址
# 创建一个保存较小数据集的目录,将其命名为cats_and_dogs
base_dir='D:\cats_and_dogs'
os.mkdir(base_dir)
# 分别对应划分后的训练、验证和测试的目录
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
# 猫的训练图像目录
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# 狗的训练图像目录
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# 猫的验证图像目录
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# 狗的验证图像目录
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# 猫的测试图像目录
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# 狗的测试图像目录
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# 将前1000张猫的图像复制到 train_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)
# 将接下来500张猫的图像复制到validation_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
# 将接下来的500张猫的图像复制到test_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张狗的图像复制到train_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将接下来500张狗的图像复制到validation_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将接下来500张狗的图像复制到test_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
# 检查一下训练、验证、测试中分别包含多少张图像
print('total training cat images:', len(os.listdir(train_cats_dir)))
print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total test cat images:', len(os.listdir(test_cats_dir)))
print('total test dog images:', len(os.listdir(test_dogs_dir)))
输出:
total training cat images: 1000
total training dog images: 1000
total validation cat images: 500
total validation dog images: 500
total test cat images: 500
total test dog images: 500
代码实现:
from keras import layers
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',input_shape=(150, 150, 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(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
# 看一下特征图的维度如何随着每层变化
model.summary()
输出:
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 7, 7, 128) 0
_________________________________________________________________
flatten (Flatten) (None, 6272) 0
_________________________________________________________________
dense (Dense) (None, 512) 3211776
_________________________________________________________________
dense_1 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
你现在已经知道,将数据输入神经网络之前,应该将数据格式化为经过预处理的浮点数张量。
现在,数据以 JPEG 文件的形式保存在电脑中,所以数据预处理步骤大致如下。
# ImageDataGenerator(rescale=所有数据集将乘以该数值, rotation_range=随即旋转角度数范围, width_shift_range=随即宽度偏移量, height_shift_range=随即高度偏移量,horizontal_flip=是否随机水平翻转,zoom_range=随机缩放的范围)
from keras.preprocessing.image import ImageDataGenerator
# 将所有图像乘以1/255缩放
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
# flow_from_directory(): 这是ImageDataGenerator类的一个方法,以文件夹路径为参数,在一个无限循环中无限产生batch数据,需要详细参数说明可以自行查阅相关资料
train_generator = train_datagen.flow_from_directory(
# 目标目录
train_dir,
# 将所有图像的大小调整为150*150
target_size=(150, 150),
batch_size=20,
# 需要使用二进制标签
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
利用生成器,我们让模型对数据进行拟合。我们将使用 fit_generator 方法来拟合,它在数据生成器上的效果和 fit 相同。它的第一个参数应该是一个 Python 生成器,可以不停地生成输入和目标组成的批量,比如 train_generator。因为数据是不断生成的,所以 Keras 模型要知道每一轮需要从生成器中抽取多少个样本。这是 steps_per_epoch 参数的作用:从生成器中抽取 steps_per_epoch 个批量后(即运行了 steps_per_epoch 次梯度下降),拟合过程将进入下一个轮次。本例中,每个批量包含 20 个样本,所以读取完所有 2000 个样本需要 100个批量。
使用 fit_generator 时,你可以传入一个 validation_data 参数,其作用和在 fit 方法中类似。值得注意的是,这个参数可以是一个数据生成器,但也可以是 Numpy 数组组成的元组。如果向 validation_data 传入一个生成器,那么这个生成器应该能够不停地生成验证数据批量,因此你还需要指定 validation_steps 参数,说明需要从验证生成器中抽取多少个批次用于评估。
代码实现:
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
# 在训练完成后保存模型、
model.save('cats_and_dogs_small_1.h5')
代码实现:
import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
输出:
从这些图像中都能看出过拟合的特征。训练精度随着时间线性增加,直到接近 100%,而验
证精度则停留在 70%~72%。验证损失仅在 5 轮后就达到最小值,然后保持不变,而训练损失则
一直线性下降,直到接近于 0。
因为训练样本相对较少(2000 个),所有模型容易过拟合。在本节中将使用一种针对于计算机视觉领域的新方法,在用深度学习模型处理图像时几乎都会用到这种方法,它就是数据增强(data augmentation)。数据增强是从现有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变换来增加(augment)样本。其目标是,模型在训练时不会两次查看完全相同的图像。这让模型能够观察到数据的更多内容,从而具有更好的泛化能力。在 Keras 中,这可以通过对 ImageDataGenerator 实例读取的图像执行多次随机变换来实现。
如果使用这种数据增强来训练一个新网络,那么网络将不会两次看到同样的输入。但网络看到的输入仍然是高度相关的,因为这些输入都来自于少量的原始图像。你无法生成新信息,而只能混合现有信息。因此,这种方法可能不足以完全消除过拟合。为了进一步降低过拟合,你还需要向模型中添加一个 Dropout 层,添加到密集连接分类器之前。
代码实现:
# 定义一个包含dropout的新卷积神经网络
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(150, 150, 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(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
# 利用数据增强生成器训练卷积神经网络
train_datagen = ImageDataGenerator( # 设置数据增强
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory( # 对训练数据进行增强
train_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
validation_generator = test_datagen.flow_from_directory( # 验证数据不能进行增强
validation_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
history = model.fit_generator( #拟合模型
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
# 保存模型
model.save('cats_and_dogs_small_2.h5')
在上面代码中只选择了几个参数进行数据增强。(想了解更多参数,请点击此处)下面介绍一下这些参数的含义。
我们可以再次绘制训练过程中的损失与精度曲线(这里不再展示结果),可以发现使用了数据增强和dropout之后,模型不再过拟合。现在的精度大约为80%左右,比未使用数据增强的模型提高了15%(相对比例)。