前言:
上一篇,我们介绍了小型卷积神经网络训练分类模型,这次我们将采用预训练网络。
二、使用预训练的卷积神经网络
想要将深度学习应用于小型图像数据集,一种常用且非常高效的方法是使用预训练网络(pretrained network)。预训练网络是一个保存好的网络,之前已在大型数据集上训练好。如果这个原始数据集足够大且足够通用,那么我们就可以把预训练网络应用到我们的问题上。
我们将使用VGG16架构,它由Karen Simonyan和Andrew Zisserman在2014年开发。对于ImageNet,它是一个简单而又广泛使用的卷积神经架构,尽管它远不如如今的最先进结构,但它胜在简单,适合初学者。
使用预训练网络有2种方法:特征提取(feature extraction)和微调模型(fine-tunning)。
2.1 特征提取
特征提取是使用之前网络学习到的表示来从新样本中提取出有趣的特征,然后将这些特征输入一个新的分类器,从头开始训练。
我们已经知道,用于图像分类的卷积神经网络包含两部分:首先是一系列卷积层和池化层,最后是一个密集连接分类器。第一部分叫做模型的卷积基(convolutional base)。对于卷积神经网络而言,特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器,如下图:
代码清单2.1.1 调用VGG16
#将VGG16卷积基实例化
from keras.applications import VGG16
conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
其中,weights指定模型初始化的权重检查点。include_top指定模型最后是否包含密集连接分类器。默认情况下,这个密集连接分类器对应于ImageNet的1000个类别,因为我们打算使用自己的密集连接分类器(只有两个类别:cat和dog),所以不需要包含它。input_shape是输入到网络中图像张量的形状(可自选)。
我们来看看VGG16的详细架构:
conv_base.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (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
_________________________________________________________________
最后的特征图形状为(4, 4, 512),我们将在这个特征上添加一个密集连接分类器。接下来,有2种方法供选择。
(1)在你的数据集上运行卷积基,将输出保存成硬盘中的Numpy数组,然后用这个数据作为输入,输入到独立的密集连结分类器中。这种方法速度快,计算代价低,因为对于每个输入图像只需运行一次卷积基,而卷积基是目前流程中计算代价最高的。但出于同样的原因,这种方法不允许我们使用数据增强。
(2)在顶部添加Dense层来扩展已有模型(即conv_base),并在输入数据上端到端地运行整个模型。这样你可以使用数据增强,因为每个输入图像进入模型时都会经过卷积基,但出于同样的原因,这种方法计算代价高很多。
由于第一种方法不能使用数据增强,模型几乎从一开始就过拟合,这里不再赘述,下面我们将采用第二种方法(即使用数据增强的特征提取)。
代码清单2.1.2 在卷积基上添加一个密集连接分类器
模型的行为和层类似,所以你可以向Sequential模型中添加一个模型(比如conv_base)就像添加一个层一样。
from keras import models
from keras import layers
model = model.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的卷积基有1400万参数,非常多,在其上添加的分类器有200万个参数。
在编译和训练模型之前,一定要冻结卷积基。冻结(freez)一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。因为其上添加的Dense层是随机初始化的,所以非常大的权重跟新将会在网络中传播,对之前学到的表示造成很大的破坏。
在keras中,冻结网络的方法是将其trainable属性设置为False:
print('This is the number of trainable weights before freezing the conv base:',
len(model_trainable.weights))
conv_base.trainable = False
print('This is the number of trainable weights after freezing the conv base:',
len(model_trainable.weights))
前后权重张量分别为30和4,每层2个(主权重矩阵和偏置向量)。注意,为了让修改生效,我们必须先编译模型。如果在编译后修改了权重的trainable属性,那么应该重新编译模型,否则这些修改将被忽略。
代码2.1.3 利用冻结的卷积基端到端地训练模型
from keras.preprocessing.image import ImagaDataGenerator
from keraas import optimizers
train_datagen = ImagaDataGenerator(rescale=1./255,
rotation_range=40,
width_shift_rang=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_diectory(train_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit_generator(train_generator,
steps_per_row=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
绘图
我们可以看到,模型最终的验证精度可达%90,通过增加模型layer和增大learning rate均无明显改变,与书上的%96(参考《Python深度学习》)仍有较大出入,思索可能是某些差异所致。
注:我们仍应为模型指明数据集路径,如下:
import os
base_dir = '存放较小数据集路径'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
2.2 微调模型
另一种广泛使用的复用方法是微调模型(fine-tuning),与特征提取互为补充。对于用于特征提取的冻结的模型基,微调是指将其顶部的几层“解冻”,并将这解冻的几层和新增加的部分(本例中是全连接分类器)联合训练(见下图)。之所以叫微调,是因为它只是略微调整了所复用模型中抽象的表示,以便让这些表示与手头的问题更加相关。
前面说过,冻结VGG16卷积基是为了能够在上面训练一个随机初始化的分类器。同理,只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。如果分类器没有训练好,那么,训练期间通过网络传播的误差信号会特别大,微调的几层之前学习到的表示都会被破坏。因此,微调网络步骤如下。
(1)在已经训练好的基网络上添加自定义网络;
(2)冻结基网络;
(3)训练所添加的部分;
(4)解冻基网络的一些层;
(5)联合训练解冻的这些层和添加的部分。
前三部在特征提取已经做过,现在我们将微调最后3个卷积层,也就是说,直到block4_pool的所有层都应该被冻结,而block_5conv1、block5_conv2和block_conv3三层应该是可以训练的。
代码清单2.2.1 冻结直到某一层的所有层
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
代码清单2.2.2 微调模型
model.compile(optimizer=optimizers.RMSprop(lr=1e-5),
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit_ganerator(train_generator,
steps_per_row=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
代码清单2.2.3 评估模型
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)
最终,我们的精度可达%97。