训练过程背后的直觉
我们本可以从零开始训练一个模型,但这注定会表现不佳——主要是因为我们的数据集太少了。在这种情况下,利用迁移学习的力量是有意义的。
迁移学习是指在一个新的数据集上对一个预先训练好的模型进行微调的过程。这使得它能够识别从未训练过的类!
简而言之,迁移学习允许我们利用在训练中获得的知识模型来识别猫和狗,这样它现在就可以用来预测一个房子的内部是否现代。
但是为什么会这样呢?
因为我们选择的任何基础模型(即预训练模型)通常都是在如此大的图像语料库上训练的,所以它通常能够学习图像的良好向量表示。剩下要做的就是在区分定制类(在我们的例子中,是老房子还是现代房子)时使用这些表示。
基于Keras的迁移学习
迁移学习涉及两个主要步骤:
· 特征提取:将预训练好的模型(并冻结其权重)作为基础模型,然后在顶部训练一个新的分类器,使其精确输出N个值(其中N是类数)。
· [可选]微调:分类器训练完成后,解冻基础模型中的几个层,使其能够很好地适应新的数据集。
新的分类器可以是:
· 一堆全连接层(即全连接的层)
或
· 一个全局池层(将整个特征映射缩小为一个值——maxpool、avgpool)。
根据你的数据集与预训练模型最初训练的数据集的不同程度,其中一些可能会有所不同。请记住,如果这两个数据集非常相似,那么只解冻所有层的一小部分可能是有益的。
微调步骤虽然是可选的,但对于自定义数据集与训练基础模型的数据集有很大不同的用例来说是非常关键的。此外,与特征提取步骤相比,这可能需要更多的时间。由于时间越长,过拟合的可能性就越大,因此建议在仔细监测损失/准确率曲线后,尽早停止训练。
模型选择背后的直觉
接下来是百万美元的问题——我们应该选择哪种模型作为微调的基础模型?
显然,有很多选择,可以在Keras文档中找到。由于ResNet-50很受欢迎,我最初选择了它,但我最终决定继续使用EfficientNet,因为它们可以实现与SOTA模型类似的结果,同时需要更少的失败次数。
此外,文中提到,它们的性能与SOTA模型一致,而需要参数更少。
EfficientNet模型有很多种风格(EfficientNetB0、EfficientNetB1……EfficientB7),它们在架构(即网络深度、宽度)和资源限制方面略有不同。这些模型中的每一个都需要具有特定图像形状的图像。鉴于我们正在处理224x224分辨率的图像,我们将使用EfficientNetB0。
特征提取步骤的模型训练
注意:本教程将使用Tensorflow的Keras API。如果你是Keras新手,我已经写了两个初级Keras教程(第1部分,第2部分),涵盖了网络架构、神经元、激活函数、隐藏层(全连接层、Dropout层、MaxPool层、平坦层)等,比这里讨论的要详细得多。
我们首先使用imagenet权重创建一个EfficientNetB0基础模型。
baseModel = EfficientNetB0(
weights="imagenet",
include_top=False, # make sure top layer is not included
input_tensor=Input(shape=(224, 224, 3)),
pooling="avg"
)
需要考虑的几件事:
· include_top必须设置为False,因为EfficientNet网络体系结构中的顶层(即最后一层)是一个全连接层,输出1000个与ImageNet数据集对应的类。我们显然不需要这个!
· 我们将新分类器提交一个全局池化层。好消息是Keras API已经允许我们在实例化efficientnetb0对象的时候这样做了。我们可以简单地将池参数设置为avg,默认值为None。
下一步是通过将每层的trainable设置为False来冻结权重:
# freeze the weights
for layer in baseModel.layers:
layer.trainable = False
现在是时候在上面创建一个新的分类器了,它将精确地输出两个类(M或O)。
要做到这一点,我们需要确保这个分类器模型的最后一层是一个有两个输出神经元的全连接层。
在这两者之间,我们还包括了用于正则化的BatchNormalization和Dropout层。
# training a new classifier on top (Functional Keras Model)
x = baseModel.output
Layer_1 = BatchNormalization()(x)
Layer_2 = Dropout(0.5)(Layer_1)
output_layer = Dense(len(CLASSES), activation="softmax")(Layer_2)
model = Model(inputs = baseModel.input, outputs = output_layer)
注:有两种方法可以建立Keras分类器模型:顺序(最基本的一种)和函数(对于具有多个输入/输出的复杂网络)。
上面的代码片段是作为一个函数性网络编写的,因为如果要使用模型进行检查,使用*model.summary()*会使网络体系结构更加清晰。同样地,我们本可以创建一个如下所示的顺序模型,结果也会相同。
# Another way to create the classifier on top of basemodel
model = tf.keras.Sequential()
model.add(baseModel)
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(len(CLASSES), activation="softmax"))
最后,让我们使用Adam optimizer和相对较大的学习率=1e-3来编译模型。由于我们有两个可能的输出类,我们将使用二元交叉熵损失(如果处理两个以上的类,则使用分类交叉熵),并基于tf中实现的AUC度量tf.keras.metrics.AUC。
# compile
opt = Adam(learning_rate=1e-3)
model.compile(optimizer=opt,
loss='binary_crossentropy',
metrics=[tf.keras.metrics.AUC()]
)
在使用fit训练模型之前,要做的最后一件事是实现EarlyStoping和ModelCheckpoint。
前者将确保该模式不会训练超过需要的时间。这是通过监控val_loss来实现的,一旦没有进一步的改进,即无法进一步最小化,训练就会停止。
后者将在给定的文件路径上保存最佳模型——在我们的例子为feature_extraction.h5。我们将再次监控验证丢失,并保存最佳模型。
注意:这是一篇非常好的文章,更详细地解释了EarlyStoping和ModelCheckpoint的实现
# implementing early stopping
es = EarlyStopping(
monitor='val_loss', #metric to monitor
mode='min', # whether to min or max the metric monitored
patience=10, # epochs to wait before declaring stopped training
verbose=1 # output epoch when training was stopped
)
# implementing model checkpoint
mc = ModelCheckpoint(
'feature_extraction.h5',
monitor='val_loss',
mode='min',
verbose=1, # display epoch+accuracy everytime model is saved
save_best_only=True
)
最后,是模型训练的时候了:
# Training the model
hist = model.fit(
x=trainGen,
epochs=25,
verbose=2,
validation_data=valGen,
steps_per_epoch=totalTrain // BATCH_SIZE,
callbacks=[es, mc]
)
快速查看AUC和损失曲线,我们可以找到模型收敛的证据。
右图中有趣的观察结果之一是,我们的验证损失低于训练损失。
起初,以为有一些数据泄漏问题,但后来发现了这篇优秀的文章,解释了为什么这是完全正常的,有时会在训练期间发生。
总结两个可能的原因(来自文章本身):
原因#1:正则化(如Dropout)仅在训练期间适用,而不是在验证期间。由于正则化牺牲了训练精度以提高验证/测试精度,因此验证损失可能会低于训练损失。
原因#2:我们的验证集太小(只有61张图像),可能比训练集(即不具代表性的验证数据集)更容易。