原文地址:https://nbviewer.jupyter.org/github/fchollet/deep-learning-with-python-notebooks/blob/master/5.3-using-a-pretrained-convnet.ipynb
在小型图像数据集上进行深度学习的常用和高效方法是利用预先训练好的网络。预先训练的网络就是以前在大型数据集上训练的保存网络,通常是在大规模图像分类任务上。如果这个原始数据集足够大且足够一般,那么由预先训练的网络学习的空间要素分层结构可以有效地作为我们视觉世界的通用模型,因此它的特征可以证明对许多不同的计算机视觉问题有用,甚至尽管这些新问题可能涉及完全不同于原始任务的课程。例如,人们可以在ImageNet上培训一个网络(其中课程主要是动物和日常用品),然后将这个训练有素的网络重新用于识别图像中的家具项目。与许多较早的浅层学习方法相比,不同问题中学习特征的这种可移植性是深度学习的关键优势,并且它使深度学习对于小数据问题非常有效。
在我们的例子中,我们将考虑在ImageNet数据集(140万标记图像和1000个不同类别)上训练的大型隐藏网络。 ImageNet包含许多动物类,包括不同种类的猫和狗,因此我们可以期望在我们的猫与狗分类问题上表现得非常好。有两种方式可以利用预先训练好的网络:特征提取和微调。我们将涵盖他们两个。我们从特征提取开始。
正如我们先前所看到的,用于图像分类的小圆点包括两部分:它们从一系列池和卷积层开始,最后以密集连接的分类器结束。 第一部分被称为模型的“卷积基”。 对于小网络来说,“特征提取”将简单地包括采用先前训练的网络的卷积基础,通过它运行新的数据,并在输出之上训练一个新的分类器。
为什么只重用卷积基?我们可以重复使用密集连接的分类器吗?一般来说,应该避免。原因很简单:由卷积基所学习的表示可能更通用,因此更具可重用性:一个convnet的特征映射是图片上一般概念的存在映射,无论计算机视觉如何,这很可能是有用的问题在眼前。另一方面,分类器学到的表征对于模型所训练的一组类来说必定是非常具体的 - 它们只包含整个图片中这个或那个类存在概率的信息。此外,在密集连接层中发现的表示不再包含有关对象位于输入图像中的位置的任何信息:这些层摆脱空间的概念,而对象位置仍由卷积特征映射描述。对于物体位置很重要的问题,密集连接的特征将基本无用。
请注意,由特定卷积层提取的表示的一般性(以及因此可重用性)级别取决于模型中层的深度。在模型中较早出现的图层会提取局部高度通用的特征图(如视觉边缘,颜色和纹理),而较高层则会提取更多抽象概念(如“猫耳朵”或“狗眼”)。因此,如果您的新数据集与原始模型所训练的数据集有很大差异,则最好仅使用模型的前几层进行特征提取,而不是使用整个卷积基础。
在我们的例子中,由于ImageNet类集确实包含多个dog和cat类,重用原始模型的密集连接层中包含的信息可能是有益的。但是,为了覆盖新问题的类集与原始模型的类集不重叠的更一般情况,我们将不选择。
让我们通过使用在ImageNet上训练的VGG16网络的卷积基来实现这一目的,从我们的猫和狗图像中提取有趣的特征,然后在这些特征之上训练猫与狗的分类器。
除此之外,VGG16型号还预装Keras。您可以从keras.applications模块中导入它。以下是可以作为keras.applications的一部分提供的图像分类模型列表(均在ImageNet数据集上预先训练好):
Xception
InceptionV3
ResNet50
VGG16
VGG19
MobileNet
让我们来实例化VGG16模型:
from keras.applications import VGG16
conv_base = VGG16(weights='imagenet',
include_top=False,
input_shape=(150, 150, 3))
我们向构造函数传递了三个参数:
以下是VGG16卷积基础架构的详细信息:它与您已熟悉的简单折点非常相似。
conv_base.summary()
最终的特征图具有形状(4,4,512)。这是我们将粘贴密集连接分类器的特征。
我们首先简单地运行先前引入的ImageDataGenerator实例,将图像提取为Numpy数组以及它们的标签。我们将简单地通过调用conv_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)
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
提取的特征目前是形状的(样本,4,4,512)。 我们将它们喂给一个密集连接的分类器,所以首先我们必须将它们压平(样本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))
在这一点上,我们可以定义我们密集连接的分类器(注意使用正则化丢失),并在我们刚才记录的数据和标签上进行训练:
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))
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()
我们的验证准确率达到了90%左右,远远好于我们在前一节中通过从头开始培训的小型模型所能达到的效果。然而,我们的情节也表明,我们几乎从一开始就过度适应 - 尽管使用率相当高的退出。这是因为此技术不利用数据增强,这对防止小图像数据集过度拟合至关重要。
由于模型的行为与层相同,因此您可以将模型(如我们的conv_base)添加到Sequential模型,就像添加图层一样。所以你可以做到以下几点:
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()
如您所见,VGG16的卷积库有14,714,688个参数,非常大。 我们在顶部添加的分类器有200万个参数。
在Keras中,通过将可训练属性设置为False来冻结网络:
print('This is the number of trainable weights '
'before freezing the conv base:', len(model.trainable_weights))
这是在冻结conv基础之前可训练的重量数量:30
conv_base.trainable = False
print('This is the number of trainable weights '
'after freezing the conv base:', len(model.trainable_weights))
通过这种设置,只有我们添加的两个密集层的权重才会被训练。 这是总共四个权重张量:每层两个(主权矩阵和偏向量)。 请注意,为了使这些更改生效,我们必须首先编译模型。 如果您在编译之后修改了重量训练能力,则应该重新编译模型,否则这些更改将被忽略。
现在我们可以开始对我们的模型进行训练,使用我们前面例子中使用的相同的数据增强配置:
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,
verbose=2)