我们将深入探讨卷积的原理以及为什么它们在计算机视觉任务中如此成功。但首先,让我们来看看一个非常简单的“convnet”示例,我们将使用我们的convnet来对MNIST数字进行分类。
下面的6行代码展示了基本的convnet是什么样子的。它是一系列 Conv 2d和MaxPooling2D层。我们马上就会看到他们具体做了什么。重要的是,一个convnet作为形状的输入张量(image_height, image_width, image_channel)(不包括批处理维度)。
我们的convnet处理大小的输入(28,28,1),这是MNIST图像的格式。我们通过将参数input_shape=(28,28,1)传递到我们的第一层。
实例化一个小的卷积层
from keras import layers
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
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'))
展示网络层:
>>> model.summary()
________________________________________________________________
Layer (type) Output Shape Param #
================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
________________________________________________________________
conv2d_2 (Conv2D) (None, 11, 11, 64) 18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D) (None, 5, 5, 64) 0
________________________________________________________________
conv2d_3 (Conv2D) (None, 3, 3, 64) 36928
================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
你可以在上面看到,每个Conv2D和MaxPooling2D层的输出都是一个三维的形状张量(高度、宽度、通道)。随着网络的深入,宽度和高度都会缩小。通道的数量由传递给Conv2D层的第一个参数控制(例如32或64)。
一步是将我们的最后一个输出张量(形状(3,3,64))输入到一个与你已经熟悉的、紧密相连的分类器网络中:一堆全连接层。这些分类器处理向量,它们是1D,而我们现在的输出是一个3D张量。首先,我们要把3D输出压平到1D,然后在上面再加一些稠密的层。
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
>>> model.summary()
Layer (type) Output Shape Param #
================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
________________________________________________________________
conv2d_2 (Conv2D) (None, 11, 11, 64) 18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D) (None, 5, 5, 64) 0
________________________________________________________________
conv2d_3 (Conv2D) (None, 3, 3, 64) 36928
________________________________________________________________
flatten_1 (Flatten) (None, 576) 0
________________________________________________________________
dense_1 (Dense) (None, 64) 36928
________________________________________________________________
dense_2 (Dense) (None, 10) 650
================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
正如你所看到的,我们的(3,3,64)输出被压平成形状的矢量(576,),然后输入全连接层。
from keras.datasets import mnist
from keras.utils import to_categorical
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
>>> test_loss, test_acc = model.evaluate(test_images, test_labels)
>>> test_acc
>0.99080000000000001
卷积操作
卷积层由两个关键参数决定:
在Keras Conv2D层中,这些参数是传递给层的第一个参数:Conv2D(output_depth, (window_height, window_width))。
通过“滑动”这些大小为3x3或5x5的窗口,通过3D输入feature map,在每个可能的位置停止,并提取周围特性的3D补丁(形状(window_height, window_width, input_depth))。然后,每个这样的3D贴片被转换(通过一个具有相同学习权矩阵的张量乘积,称为“卷积核”)到一个一维的形状向量(output_depth,)。然后将所有这些向量空间重新组合成一个形状(高度、宽度、output_depth)的3D输出图。输出特性映射中的每个空间位置都对应于输入特性映射中的相同位置(例如,输出的右下角包含输入的右下角的信息)。例如,在3x3窗口中,矢量输出[i, j,:]来自3D补丁输入[i-1:i+1, j-1:j+1,:]。
注意,输出的宽度和高度可能与输入的宽度和高度不同。他们可能因两个原因而不同:
理解边界效应和填充
考虑一个5x5的feature map(总共25块)。只有9个不同的瓦片,你可以把它放在一个3x3的窗口中,形成一个3x3的网格。因此,输出feature map将是3x3:它会缩小一点,在这个例子中,每一个维度旁边正好有两个方块。您可以在上面的示例中看到这种“边界效应”:我们从28x28输入开始,在第一个卷积层之后,它变成了26x26。
如果您想要获得与输入相同的空间维度的输出特性图,则可以使用 Padding:
在Conv2D层中,填充可以通过padding参数进行配置,该参数采用两个值:“valid”,即没有填充(只使用“valid”窗口位置)和“same”,这意味着“以相同宽度和高度的输出作为输入”,填充参数默认为“valid”。
理解卷积的Strides
使用stride 2意味着feature map的宽度和高度被一个因子2(除了边界效应所引起的任何变化)所降低。在实践中很少使用带条纹的卷积,尽管它们在某些类型的模型中很有用,但是熟悉这个概念通常很好。
The max pooling operation
在我们的convnet示例中,您可能已经注意到,每个MaxPooling2D层之后,feature map的大小都会减半。例如,在第一个MaxPooling2D层之前,feature map是26x26,但最大池操作将其分为13x13。这就是max pooling的作用:大幅降低样本特征映射,就像跨越的卷积。
a convnet without pooling layers
model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
>>> model_no_max_pool.summary()
Layer (type) Output Shape Param #
================================================================
conv2d_4 (Conv2D) (None, 26, 26, 32) 320
________________________________________________________________
conv2d_5 (Conv2D) (None, 24, 24, 64) 18496
________________________________________________________________
conv2d_6 (Conv2D) (None, 22, 22, 64) 36928
================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
这一步有什么问题,主要有两方面:
简而言之,使用downsampling的原因仅仅是为了减少特征映射系数的处理次数,以及通过连续的卷积层观察越来越大的窗口来诱导空间过滤层次结构(就其所覆盖的原始输入的分数而言)。
你有时会听到,只有当大量数据可用时,深度学习才会起作用。这在一定程度上是一个有效的观点:深度学习的一个基本特征是,它能够在自己的训练数据中找到有趣的特性,而不需要人工提取的特征工程,只有当大量的训练实例可用时才能实现。对于输入样本非常高维的问题,比如图像,尤其如此。
然而,对于初学者来说,“大量”的样本是相对于你想要训练的网络的大小和深度而言的。仅仅用几十个样本就可以训练一个Convnets网络来解决一个复杂的问题是不可能的,但是如果这个模型是小的,并且有良好的正则化,并且任务简单,那么几百个可能就足够了。
但更重要的是,深度学习模型本质上是高度可重用的。比如,你可以在一个大规模数据集上接受一个图像分类或语音到文本的模型,然后在一个非常不同的问题上重用它,只需要做一些细微的改变。具体地说,在计算机视觉的情况下,许多预先训练过的模型(通常是在ImageNet数据集上进行训练)现在可以公开下载,并且可以通过非常小的数据来引导强大的视觉模型。
Copying images to train,validation and test directories
import os, shutil
# The path to the directory where the original
# dataset was uncompressed
original_dataset_dir = '/Users/Downloads/kaggle_original_data'
# The directory where we will
# store our smaller dataset
base_dir = '/Users/Downloads/cats_and_dogs_small'
os.mkdir(base_dir)
# Directories for our training,
# validation and test splits
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)
# Directory with our training cat pictures
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# Directory with our training dog pictures
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# Directory with our validation cat pictures
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# Directory with our validation dog pictures
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# Directory with our test cat pictures
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# Directory with our test dog pictures
test_dogs_dir = os.path.join(test_dir,'dogs')
os.mkdir(test_dogs_dir)
# Copy first 1000 cat images to 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)
# Copy next 500 cat images to 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)
# Copy next 500 cat images to 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)
# Copy first 1000 dog images to 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)
# Copy next 500 dog images to 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)
# Copy next 500 dog images to 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)
Counting the images
>>> print('total training cat images:', len(os.listdir(train_cats_dir)))
total training cat images: 1000
>>> print('total training dog images:', len(os.listdir(train_dogs_dir)))
total training dog images: 1000
>>> print('total validation cat images:', len(os.listdir(validation_cats_dir)))
total validation cat images: 500
>>> print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
total validation dog images: 500
>>> print('total test cat images:', len(os.listdir(test_cats_dir)))
total test cat images: 500
>>> print('total test dog images:', len(os.listdir(test_dogs_dir)))
total test dog images: 500
搭建网络
注意,feature map的深度在网络中逐渐增加(从32到128),而feature map的大小正在减少(从148x148到7x7)。这是一个在几乎所有的Convnets 中都能看到的模式。
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'))
让我们来看看特征映射的维度是如何随着每一个连续的层而变化的。
>>> model.summary()
Layer (type) Output Shape Param #
================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 74, 74, 32) 0
________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D) (None, 36, 36, 64) 0
________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
________________________________________________________________
maxpooling2d_3 (MaxPooling2D) (None, 17, 17, 128) 0
________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
________________________________________________________________
maxpooling2d_4 (MaxPooling2D) (None, 7, 7, 128) 0
________________________________________________________________
flatten_1 (Flatten) (None, 6272) 0
________________________________________________________________
dense_1 (Dense) (None, 512) 3211776
________________________________________________________________
dense_2 (Dense) (None, 1) 513
================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
编译网络
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
数据处理
正如现在已经知道的,在将数据输入到我们的网络之前,数据应该被格式化为适当的预处理的浮点张量。目前,我们的数据以JPEG文件的形式存在,因此将其放入我们的网络的步骤大致是这样的:
Keras与图像处理模块的辅助工具,位于keras.preprocessing.image。特别地,它包含类ImageDataGenerator,它允许快速设置Python生成器,可以自动将磁盘上的图像文件转换为成批的预处理的张量。这就是我们要用到的。
用 ImageDataGenerator 读取图片
from keras.preprocessing.image import ImageDataGenerator
# All images will be rescaled by 1./255
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# This is the target directory
train_dir,
# All images will be resized to 150x150
target_size=(150, 150),
batch_size=20,
# Since we use binary_crossentropy loss, we need binary labels
class_mode='binary')
# categorical
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
让我们来看看其中一个生成器的输出:它生成了150×150的RGB图像(形状(20、150、150、3))和二分类标签(形状(20,))。20是每批样品的数量(batch_size)。请注意,生成器无限地生成这些批次:它只是对目标文件夹中的图像无休止地循环。出于这个原因,我们需要在某个时刻打破迭代循环。
Displaying the shapes of a batch of data and labels
>>> for data_batch, labels_batch in train_generator:
>>> print('data batch shape:', data_batch.shape)
>>> print('labels batch shape:', labels_batch.shape)
>>> break
data batch shape: (20, 150, 150, 3)
labels batch shape: (20,)
让我们使用生成器将模型与数据相匹配。我们使用的是fit_generator方法,它相当于我们的数据生成器。它希望作为第一个参数,一个Python生成器能够像我们一样,无限期地产生大量的输入和目标。由于数据是不断生成的,因此生成器需要知道从生成器中抽取多少个样本,然后才宣告一个时代结束。这是steps_per_epoch参数的角色:在从生成器中提取steps_per_epoch批次之后,即在运行steps_per_epoch梯度下降步骤之后,拟合过程将进入下一个阶段。在我们的情况下,批次是20个样品,所以要100个批次,直到我们看到我们的2000个样品的目标。
当使用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(1, len(acc) + 1)
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()
因为我们只有相对较少的培训样本(2000),解决过拟合将是首选。你已经了解了一些可以帮助减轻过拟合的技术,例如Drop out和weight decay(L2正则化)。我们现在将介绍一种新的,具体到计算机视觉,并且在处理深度学习模型的图像时几乎普遍使用:数据增强。
数据增强
过度拟合是由于样本太少,无法学习,使我们无法训练一个能够推广到新数据的模型。数据增强的方法是从现有的训练样本中生成更多的训练数据,通过一些随机的转换来“增加”样本,这些转换产生可信的图像。我们的目标是在训练的时候,我们的模型永远不会看到完全相同的画面。这有助于模型更深入地了解数据的各个方面,并更好地推广。
在Keras中,可以通过在ImageDataGenerator实例读取的图像上配置一些随机转换来实现这一点。
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
这些只是一些可用的选项(更多的,请参见Keras文档)。回顾一下这些参数:
# This is module with image preprocessing utilities
from keras.preprocessing import image
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]
# We pick one image to "augment"
img_path = fnames[3]
# Read the image and resize it
img = image.load_img(img_path, target_size=(150, 150))
# Convert it to a Numpy array with shape (150, 150, 3)
x = image.img_to_array(img)
# Reshape it to (1, 150, 150, 3)
x = x.reshape((1,) + x.shape)
# The .flow() command below generates batches of randomly transformed images.
# It will loop indefinitely, so we need to `break` the loop at some point!
i = 0
for batch in datagen.flow(x, batch_size=1):
plt.figure(i)
imgplot = plt.imshow(image.array_to_img(batch[0]))
i += 1
if i % 4 == 0:
break
plt.show()
如果我们使用这个数据扩充配置来训练一个新的网络,我们的网络将不会看到两次相同的输入。但是,它所看到的输入仍然是相互关联的,因为它们来自于少数原始图像,我们不能产生新的信息,我们只能重新组合现有的信息。因此,这可能不足以完全消除过度拟合。为了进一步打击过度拟合,我们还将在我们的模型中添加一个Drop out,在全连接的分类器之前:
Defining a new convnet that includes 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'])
Training our convnet using data augmentation generators
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,)
# Note that the validation data should not be augmented!
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# This is the target directory
train_dir,
# All images will be resized to 150x150
target_size=(150, 150),
batch_size=32,
# Since we use binary_crossentropy loss, we need binary labels
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)
让我们保存我们的模型,我们将在Convnet网络可视化部分使用它。
model.save('cats_and_dogs_small_2.h5')
由于数据的增加和Drop out,我们不再过拟合:训练曲线更接近于验证曲线。我们现在能够达到82%的精确度,相对于非正则化模型有15%的相对改进。
使用预先训练的卷积网络
对小图像数据集进行深度学习的一种常见且非常有效的方法是利用预先训练好的网络。一个预先训练的网络只是一个以前在大数据集上训练的保存的网络,通常是大规模的图像分类任务。
VGG16模型和其他模型一样,都是预先打包的Keras,你可以从 keras.applications 导入它。这里是图像分类模型的列表(都是在ImageNet数据集上预先训练的),作为keras.applications的一部分可用:
实例化模型
from keras.applications import VGG16
conv.base = VGG16(weights = 'imagenet',include_top = False, input_shape=(150,150,3))
函数中传入了三个参数:
**1. weights ?*指定哪个权重检查点来初始化模型。
**2. include_top:**确定是否包括网络中的全连接层,由于原网络中的全连接层会从ImageNet中分出1000类,而在这里我们只做二分类任务,所以不适用。
**3. input_shape:**输入网络的图像张量,如果我们不定义,网络可以处理任何尺寸的输入。
展示VGG16的网络模型:
>>> conv_base.summary()
Layer (type) Output Shape Param #
================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
________________________________________________________________
block1_conv1 (Convolution2D) (None, 150, 150, 64) 1792
________________________________________________________________
block1_conv2 (Convolution2D) (None, 150, 150, 64) 36928
________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
________________________________________________________________
block2_conv1 (Convolution2D) (None, 75, 75, 128) 73856
________________________________________________________________
block2_conv2 (Convolution2D) (None, 75, 75, 128) 147584
________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
________________________________________________________________
block3_conv1 (Convolution2D) (None, 37, 37, 256) 295168
________________________________________________________________
block3_conv2 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_conv3 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
________________________________________________________________
block4_conv1 (Convolution2D) (None, 18, 18, 512) 1180160
________________________________________________________________
block4_conv2 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_conv3 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
________________________________________________________________
block5_conv1 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv2 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv3 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
最后feature map的形状为(4,4,512),这就是我们将在上面连接一个全连接分类器的输入特性。
在这一点上,有两种方法可以进行:
我们将介绍这两种技术。让我们遍历设置第一种方法所需的代码:记录我们数据上的conv_base的输出,并将这些输出作为新模型的输入。
Extracting features using the pre-trained convolutional base
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20
def extract_features(directory, sample_count):
features = np.zeros(shape=(sample_count, 4, 4, 512))
labels = np.zeros(shape=(sample_count))
generator = datagen.flow_from_directory(
directory,
target_size=(150, 150),
batch_size=batch_size,
class_mode='binary')
i = 0
for inputs_batch, labels_batch in generator:
features_batch = conv_base.predict(inputs_batch)
features[i * batch_size : (i + 1) * batch_size] = features_batch
labels[i * batch_size : (i + 1) * batch_size] = labels_batch
i += 1
if i * batch_size >= sample_count:
# Note that since generators yield data indefinitely in a loop,
# we must `break` after every image has been seen once.
break
return features, labels
train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)
提取的特征目前是形状(samples,4,4,512)。我们将会把它们输出给全连接层,所以首先我们必须把它们压平为(samples,8192)。
train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))
此时,我们要定义一个全连接层,注意使用Drop out来正则化,并在刚才提取的特征上训练数据:
from keras import models
from keras import layers
from keras import optimizers
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(train_features, train_labels,
epochs=30,
batch_size=20,
validation_data=(validation_features, validation_labels))
训练是非常快的,因为我们只需要处理两个全连接层,一个epoch 即使在CPU上也只需要不到一秒的时间。
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(1, len(acc) + 1)
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()
我们达到了大约90%的验证精度,比我们在上一节中通过从头开始训练的小模型所能达到的效果要好得多。然而,结果也表明,尽管我们Drop out的值相当高,但我们几乎从一开始就过拟合。这是因为该技术不利用数据增强,这对于防止小型图像数据集的过度拟合是至关重要的。
Adding a densely-connected classifier on top of the convolutional base
因为模型的行为就像层一样,你可以将一个模型(比如我们的conv_base)添加到一个序列模型中,就像你添加一个层一样。你可以这样做:
from keras import models
from keras import layers
model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
模型现在是这个样子:
>>> model.summary()
Layer (type) Output Shape Param #
================================================================
vgg16 (Model) (None, 4, 4, 512) 14714688
________________________________________________________________
flatten_1 (Flatten) (None, 8192) 0
________________________________________________________________
dense_1 (Dense) (None, 256) 2097408
________________________________________________________________
dense_2 (Dense) (None, 1) 257
================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
如你所见,VGG16的卷积基础有14,714,688个参数,非常大。我们在上面添加的分类器有200万个参数。
在我们编译和训练我们的模型之前,要做的一件非常重要的事情是***冻结卷积基***。“Freezing”一层或一套层意味着在训练期间防止他们的weight得到更新。如果我们不这样做,那么之前在卷积基础上学习的表示会在训练过程中被修改。由于顶部的全连接层是随机初始化的,所以非常大的权重更新将通过网络传播,从而会破坏之前卷积学到的内容。
>>> print('This is the number of trainable weights '
'before freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights before freezing the conv base: 30
>>> conv_base.trainable = False
>>> print('This is the number of trainable weights '
'after freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights after freezing the conv base: 4
有了这个设置,只有我们添加的两个全连接层的权重将被训练。这是一个总共有4个分量的张量:每层2个(主要的权重矩阵和偏置向量)。请注意,为了使这些更改生效,我们必须首先编译模型。如果您在编译后修改了weight trainability,那么应该重新编译模型,否则这些更改将被忽略。
from keras.preprocessing.image import ImageDataGenerator
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,
fill_mode='nearest')
# Note that the validation data should not be augmented!
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# This is the target directory
train_dir,
# All images will be resized to 150x150
target_size=(150, 150),
batch_size=20,
# Since we use binary_crossentropy loss, we need binary labels
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=2e-5),
metrics=['acc'])
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
另一种广泛使用的用于模型重用的技术是fine-tuning。fine-tuning包括解冻用于特征提取的冻结模型库的几个顶层,并联合培训新添加的模型(在我们的示例中是全连接的分类器)和这些顶层。这被称为“Fine - tuning”,因为它稍微调整了被重用模型的更抽象表示,以便使它们更适合于手头的问题。
我们之前已经说过,为了能够在上面训练一个随机初始化的分类器,需要冻结VGG16的卷积基础。出于同样的原因,一旦上面的分类器已经被训练,它只可能调整卷积基础的顶层。如果分类没有经过训练,那么在训练过程中通过网络传播的错误信号将会太大,而之前被调整的层所得到的表示将被破坏。因此,对网络进行Fine - tuning 的步骤如下:
在进行特征提取时,我们已经完成了前三个步骤。让我们继续第4步:我们将解冻我们的conv_base,然后冻结它内部的各个层。
>>> conv_base.summary()
Layer (type) Output Shape Param #
================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
________________________________________________________________
block1_conv1 (Convolution2D) (None, 150, 150, 64) 1792
________________________________________________________________
block1_conv2 (Convolution2D) (None, 150, 150, 64) 36928
________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
________________________________________________________________
block2_conv1 (Convolution2D) (None, 75, 75, 128) 73856
________________________________________________________________
block2_conv2 (Convolution2D) (None, 75, 75, 128) 147584
________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
________________________________________________________________
block3_conv1 (Convolution2D) (None, 37, 37, 256) 295168
________________________________________________________________
block3_conv2 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_conv3 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
________________________________________________________________
block4_conv1 (Convolution2D) (None, 18, 18, 512) 1180160
________________________________________________________________
block4_conv2 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_conv3 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
________________________________________________________________
block5_conv1 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv2 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv3 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
================================================================
Total params: 14714688
我们将对最后的3个卷积层进行 Fine - tuning,这意味着直到block4_pool应该被冻结,并且层block5_conv1, block5_conv2和block5_conv3应该是可训练的。
为什么不调整更多的层次呢?为什么不微调整个卷积基础?我们可以。然而,我们需要考虑:
因此,在我们的情况下,在卷积基础上只调整前2到3层是一个很好的策略。
conv_base.trainable = True
set_trainable = False
for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
现在我们可以开始微调我们的网络了。我们将使用一个非常低的学习速率来使用RMSprop优化器。使用低学习率的原因是,我们想要限制我们对三层的表示的修改的大小,我们正在进行微调。太大的更新可能会损害这些表示。
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-5),
metrics=['acc'])
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
这些曲线看起来有很多Noise。为了使它们更具可读性,我们可以通过用指数移动的平均数来替换每一个损失和精度来使它们平滑。这里是一个平凡的效用函数。
Smoothing our plots:
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1 - factor))
else:
smoothed_points.append(point)
return smoothed_points
plt.plot(epochs,
smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs,
smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,
smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,
smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)
这是你应该从之前的内容学到的: