在Keras中使用转移学习进行图像分类--创建前沿的CNN模型

369 阅读25分钟

简介

深度学习模型是非常通用和强大的--它们在狭窄的任务中经常胜过人类,而且它们的泛化能力正在快速增加。新的模型经常被发布,并以社区接受的数据集为基准,要跟上所有的模型越来越难。

这些模型中的大多数都是开源的,你也可以自己实现它们。

这意味着普通爱好者可以在家里,在非常普通的机器上加载和玩弄最先进的模型,不仅可以获得更深的理解和欣赏,还可以为科学论述做出贡献,并在任何时候发表自己的改进。

在本指南中,你将学习如何使用预先训练好的、最先进的深度学习模型进行图像分类,并将它们重新用于你自己的特定应用。这样,你就可以利用它们的高性能、巧妙的架构别人的训练时间--同时将这些模型应用于你自己的领域。

本指南中的所有代码也可以在以下网站上找到 GitHub.

计算机视觉和卷积神经网络(CNN)的迁移学习

知识知识代表是非常普遍的。在一个数据集上训练的计算机视觉模型可以学会识别可能在许多其他数据集中非常普遍的模式。

值得注意的是,在 "生命科学的深度学习", Bharath Ramsundar, Peter Eastman, Patrick Walters 和 Vijay Pande的文章中指出。

"已经有多项研究在研究推荐系统算法在分子结合预测中的应用。在一个领域使用的机器学习架构往往会延续到其他领域,所以保留创新工作所需的灵活性非常重要"。

例如,通常在CNN层次结构的较低层次上学习的直线和弧线必然会出现在几乎所有的数据集中。一些高层次的特征,如区分蜜蜂和蚂蚁的特征,将在层次结构中被表示和学习。

feature hierarchies for convolutional neural networks

这之间的 "细线 "就是你可以重用的东西!"。根据你的数据集和模型预训练的数据集之间的相似程度,你可能能够重用其中的一小部分或一大部分。

一个对人造结构进行分类的模型(在Places365这样的数据集上训练)和一个对动物进行分类的模型(在ImageNet这样的数据集上训练)一定会有一些共享的模式,尽管不是很多。

你可能想训练一个模型来区分,比如说,公共汽车汽车,用于自动驾驶汽车的视觉系统。你也可以合理地选择使用一个性能非常好的架构,该架构已被证明在与你类似的数据集上运行良好。然后,漫长的训练过程开始了,你最终会拥有一个属于你自己的高性能模型

然而,如果另一个模型有可能在较低较高的抽象层次上有类似的表示,就没有必要从头开始重新训练一个模型。你可以决定使用一些已经预训练好的权重,这些权重同样适用于你自己的模型应用,因为它们适用于原始架构的创造者。你会一些知识从一个已经存在的模型转移到一个新的模型,这就是所谓的 转移学习.

预训练模型的数据集越接近你自己的数据集,你能转移的就越多。你能转移的越多,你自己的时间和计算就能节省的越多。值得记住的是,训练神经网络确实有一个碳足迹,所以你不仅仅是在节省时间!

通常情况下,转移学习是通过加载一个预先训练好的模型,并冻结其层来完成的。在许多情况下,你可以直接切断分类层(最后一层,或者说,头部),只需重新训练分类层,同时保持所有其他抽象层的完整。在其他情况下,你可能会决定重新训练层次结构中的几个层,这通常是在数据集包含足够多的不同数据点而需要重新训练多个层时进行。你也可以决定重新训练整个模型,对所有的层进行微调。

这两种方法可以总结为。

  • 使用卷积网络作为特征提取器
  • 微调卷积网络

在前者中,你使用模型的底层熵能力作为固定的特征提取器,而只是在上面训练一个密集的网络来辨别这些特征。在后者中,你对整个(或部分)卷积网络进行微调,如果它还没有其他更具体的数据集的代表性特征图,同时也依靠已经训练好的特征图。

以下是转移学习工作原理的直观展示。

how does transfer learning work?

已有的和前沿的图像分类模型

现在有很多模型,对于众所周知的数据集,你很可能会在网上的资料库和论文中找到数百个表现良好的模型。在PapersWithCode上可以看到在ImageNet数据集上训练的模型的一个很好的整体视图。

一些著名的已发表的架构后来被移植到许多深度学习框架中,包括。

  • EfficientNet
  • SENet
  • Xception
  • ResNet
  • VGGNet
  • 亚历克斯网络
  • LeNet-5

PapersWithCode上的模型列表一直在更新,你不应该挂在这些模型的位置上。截至发稿时,它们中的许多都被其他各种模型所超越,而且许多新的模型实际上是基于上述列表中的模型。值得注意的是,转移学习实际上在较新的、精确度较高的模型中发挥了重要作用

缺点是--很多最新的模型并没有作为预训练的模型移植到Tensorflow和PyTorch等框架中。这并不是说你会失去很多性能,所以使用任何一个成熟的模型都不是真的坏。

用Keras进行迁移学习--改编现有模型

在Keras中,预训练的模型在tensorflow.keras.applications 模块下可用。每个模型都有自己的子模块和类。当加载一个模型时,你可以设置几个可选参数来控制模型的加载方式。

例如,weights 参数,如果存在,定义了预训练的权重。如果省略,只有架构(未经训练的网络)会被加载进来。如果你提供一个数据集的名称--将返回该数据集的预训练网络。

此外,由于你很可能会移除转移学习的顶层,include_top 参数被用来定义顶层是否应该存在!

import tensorflow.keras.applications as models

# 98 MB
resnet = models.resnet50.ResNet50(weights='imagenet', include_top=False)
# 528MB
vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=False)
# 23MB
nnm = models.NASNetMobile(weights='imagenet', include_top=False)
# etc...

**注意:**如果你以前从未加载过预训练的模型,它们将通过互联网连接下载。这可能需要几秒钟到几分钟的时间,取决于你的网速和模型的大小。预训练模型的大小从14MB移动模型通常较小)到549MB不等。

高效网络是一个网络系列,性能相当好,可扩展性强,而且,效率高。它们在制作时考虑到了减少可学习的参数,所以它们只有4M的参数可以训练。虽然4M仍然是一个很大的数字,但考虑到VGG16,例如,有20M。在家庭设置中,这也有助于大大缩短训练时间!

让我们加载EfficientNet系列中的一个成员--EfficientNet-B0。

effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
effnet.summary()

其结果是。

Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_7 (InputLayer)         [(None, None, None, 3)]   0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, None, None, 64)    1792      
...
...
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, None, None, 512)   0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________

另一方面,如果我们把VGG16的顶部也装进去,我们在最后还有一个Flatten层和几个Dense层,这些都是为ImageNet的数据分类而训练的。这是我们将为自己的应用训练的模型的顶层。

vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=True)
vgg16.summary()

这将包括FlattenDense 层,这将大大增加参数的大小。

Model: "efficientnetb0"
_________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
=========================================================================================
input_11 (InputLayer)           [(None, 224, 224, 3) 0                                            
_________________________________________________________________________________________
...
...
_________________________________________________________________________________________
top_conv (Conv2D)               (None, 7, 7, 1280)   409600      block7a_project_bn[0][0]         
_________________________________________________________________________________________
top_bn (BatchNormalization)     (None, 7, 7, 1280)   5120        top_conv[0][0]                   
_________________________________________________________________________________________
top_activation (Activation)     (None, 7, 7, 1280)   0           top_bn[0][0]                     
_________________________________________________________________________________________
avg_pool (GlobalAveragePooling2 (None, 1280)         0           top_activation[0][0]             
_________________________________________________________________________________________
top_dropout (Dropout)           (None, 1280)         0           avg_pool[0][0]                   
_________________________________________________________________________________________
predictions (Dense)             (None, 1000)         1281000     top_dropout[0][0]                
=========================================================================================
Total params: 5,330,571
Trainable params: 5,288,548
Non-trainable params: 42,023
_________________________________________________________________________________________

同样,我们不会使用这些顶层,因为我们将在EfficientNet模型中加入我们自己的顶层,并且只重新训练我们加入的顶层。不过值得注意的是,该架构已经在使用什么了!他们似乎在使用一个Conv2D ,然后是BatchNormalizationGlobalAveragePooling2DDropout ,最后是Dense 分类层。虽然我们不必严格遵循这种方法(其他方法可能被证明对另一个数据集更好),但记住原来的顶部是什么样子是合理的。

**注意:**数据预处理在模型训练中起着关键作用,大多数模型会有不同的预处理管道。你不需要在这里进行猜测!在适用的情况下,一个模型会有自己的preprocess_input() 函数。

preprocess_input() 函数对输入的预处理步骤与训练期间应用的预处理步骤相同。如果一个模型存在于它自己的模块中,你可以从模型的相应模块中导入该函数。例如,VGG16有它自己的preprocess_input 函数。

from keras.applications.vgg16 import preprocess_input

也就是说,在Keras中加载一个模型,对其进行预处理输入并预测结果是非常简单的。

import tensorflow.keras.applications as models
from keras.applications.vgg16 import preprocess_input

vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=True)

img = # get data
img = preprocess_input(img)
pred = vgg16.predict(img)

**注意:**并不是所有的模型都有一个专门的preprocess_input() ,因为预处理是在模型本身中完成的。例如,我们要使用的EfficientNet就没有自己专门的预处理函数,因为Rescaling 层就负责这个。

就这样吧!现在,由于pred 数组并不真正包含人类可读的数据,你也可以将decode_predictions() 函数与preprocess_input() 函数一起从一个模块中导入。或者,你可以导入通用的decode_predictions() 函数,该函数也适用于没有专用模块的模型。

from keras.applications.model_name import preprocess_input, decode_predictions
# OR
from keras.applications.imagenet_utils import decode_predictions
# ...
print(decode_predictions(pred))

将这些联系起来,让我们通过urllib ,得到一张黑熊的图片,将该文件保存为适合EfficientNet的目标大小(输入层期望的形状为(None, 224, 224, 3) ),然后用预先训练好的模型进行分类。

from tensorflow import keras
from keras.applications.vgg16 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

import urllib.request
import matplotlib.pyplot as plt
import numpy as np

# Public domain image
url = 'https://upload.wikimedia.org/wikipedia/commons/0/02/Black_bear_large.jpg'
urllib.request.urlretrieve(url, 'bear.jpg')

# Load image and resize (doesn't keep aspect ratio)
img = image.load_img('bear.jpg', target_size=(224, 224))
# Turn to array of shape (224, 224, 3)
img = image.img_to_array(img)
# Expand array into (1, 224, 224, 3)
img = np.expand_dims(img, 0)
# Preprocess for models that have specific preprocess_input() function
# img_preprocessed = preprocess_input(img)

# Load model and run prediction
effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
pred = effnet.predict(img)
print(decode_predictions(pred))

这样的结果是。

[[('n02133161', 'American_black_bear', 0.6024658),('n02132136', 'brown_bear', 0.1457715),('n02134418', 'sloth_bear', 0.09819221),('n02510455', 'giant_panda', 0.0069221947),('n02509815', 'lesser_panda', 0.005077324)]]

相当肯定地认为该图像是美国黑熊的图像,这是对的!当用预处理函数进行预处理时,图像可能会发生重大变化。例如,VGG16的预处理功能会改变熊的毛色。

preprocessing image for VGG16 CNN

它现在看起来更像棕色了如果我们把这张图片输入EfficientNet,它就会认为这是一只棕熊

[[('n02132136', 'brown_bear', 0.7152758), ('n02133161', 'American_black_bear', 0.15667434), ('n02134418', 'sloth_bear', 0.012813852), ('n02134084', 'ice_bear', 0.0067828503), ('n02117135', 'hyena', 0.0050422684)]]

棒极了!这个模型成功了。现在,让我们给它添加一个新的顶点,重新训练这个顶点对ImageNet集以外的东西进行分类。

为预训练的模型添加新的顶点

在进行迁移学习时,你将会加载没有top的模型,或者手动删除它们。

# Load without top
# When adding new layers, we also need to define the input_shape
# so that  the new Dense layers have a fixed input_shape as well
effnet_base = keras.applications.EfficientNetB0(weights='imagenet', 
                                          include_top=False, 
                                          input_shape=((224, 224, 3)))

# Or load the full model
full_effnet = keras.applications.EfficientNetB0(weights='imagenet', 
                                            include_top=True, 
                                            input_shape=((224, 224, 3)))
                                            
# And then remove X layers from the top
trimmed_effnet = keras.Model(inputs=full_effnet.input, outputs=full_effnet.layers[-3].output)

我们将采用第一个选项,因为它更方便。根据你是否想对卷积块进行微调--你将冻结或不冻结它们。假设我们想使用底层预训练的特征图,并冻结各层,这样我们就只重新训练顶部的新分类层。

effnet_base.trainable = False

你不需要迭代模型并设置每个层为trainable ,或者不为,尽管你也可以。如果你想关闭第一个n ,并允许一些更高层次的特征图进行微调,但不触动低层次的特征图,你可以。

for layer in effnet_base.layers[:-2]:
    layer.trainable = False

在这里,我们将基础模型中的所有层都设置为不可训练,除了最后两个。如果我们检查这个模型,现在只有~2.5K的可训练参数。

effnet_base.summary()
# ...                
=========================================================================================
Total params: 4,049,571
Trainable params: 2,560
Non-trainable params: 4,047,011
_________________________________________________________________________________________

现在,让我们定义一个Sequential ,它将被放在这个effnet_base 上。幸运的是,在Keras中建立模型链就像建立一个新的模型并将其置于另一个模型之上一样简单!你可以利用Functional API并将其置于另一个模型之上。你可以利用Functional API,只需在一个模型的顶部链上几个新的层。

让我们添加一个Conv2D 层,一个BatchNormalization 层,一个GlobalAveragePooling2D 层,一些Dropout 和几个完全连接的层,然后再添加一个Flatten

conv2d = keras.layers.Conv2D(7, 7)(effnet_base.output, training=False)
bn = keras.layers.BatchNormalization()(conv2d)
gap = keras.layers.GlobalAveragePooling2D()(bn)
do = keras.layers.Dropout(0.2)(gap)
flatten = keras.layers.Flatten()(gap)
fc1 = keras.layers.Dense(512, activation='relu')(flatten)
output = keras.layers.Dense(100, activation='softmax')(fc1)

new_model = keras.Model(inputs=effnet_base.input, outputs=output)

**注意:**在添加EfficientNet的各层时,我们将training 设置为False 。这使网络处于推理模式而不是训练模式,而且它与我们之前设置的trainable 的参数不同,False 。如果你想在以后解冻各层,这是一个关键的步骤。BatchNormalization,是计算移动统计。当解冻时,它将重新开始应用参数的更新,并将 "撤销 "微调之前所做的训练。从TF2.0开始,将模型的trainable 设置为False ,也会将training 转为False ,但只适用于BatchNormalization 层,所以这个步骤对于TF2.0之后的版本是不必要的。

另外,你可以使用序列API并多次调用add() 方法。

new_model = keras.Sequential()
new_model.add(effnet_base) # Add entire model
new_model.add(keras.layers.Conv2D(7,7))
new_model.add(keras.layers.BatchNormalization())
new_model.add(keras.layers.GlobalAveragePooling2D())
new_model.add(keras.layers.Dropout(0.3))
new_model.add(keras.layers.Flatten())
new_model.add(keras.layers.Dense(512, activation='relu'))
new_model.add(keras.layers.Dense(100, activation='softmax'))

这就把整个模型本身作为一个层添加,所以它被当作一个实体。

Layer: 0, Trainable: False # Entire EfficientNet model
Layer: 1, Trainable: True
Layer: 2, Trainable: True
...

另一方面,你可以通过添加effnet_baseoutput ,将所有的层提取出来,而作为独立的实体添加。

new_model = keras.Sequential()
new_model.add(effnet_base.output) # Add unwrapped layers
new_model.add(keras.layers.Conv2D(7,7))
new_model.add(keras.layers.BatchNormalization())
new_model.add(keras.layers.GlobalAveragePooling2D())
new_model.add(keras.layers.Dropout(0.3))
new_model.add(keras.layers.Dense(100, activation='softmax'))

在任何一种情况下--我们都增加了10个输出类,因为我们以后要使用CIFAR10数据集,它有10个类!这是很重要的。让我们看一下网络中的可训练层。

for index, layer in enumerate(new_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

这样的结果是。

Layer: 0, Trainable: False
Layer: 1, Trainable: False
Layer: 2, Trainable: False
...
Layer: 235, Trainable: False
Layer: 236, Trainable: False
Layer: 237, Trainable: True
Layer: 238, Trainable: True
Layer: 239, Trainable: True
Layer: 240, Trainable: True
Layer: 241, Trainable: True

棒极了!让我们加载数据集,对其进行预处理,并对分类层进行重新训练。

加载和预处理数据

我们将使用 CIFAR10数据集.这是一个不太难分类的数据集,因为它只有10个类,我们将利用一个广受好评的架构来帮助我们完成这项工作。

它的 "哥哥",CIFAR100是一个真正难以处理的数据。它有50,000张图片,有100个标签,这意味着每个类别只有100个样本。在这么少的标签上,这是很难做到的,而且几乎所有在数据集上表现良好的模型都使用了大量的数据增强。

数据扩充本身就是一门艺术和科学,而且不在本指南的范围之内--所以我们将只用几个随机的转换来使数据集多样化。

为了简洁起见,我们将坚持使用CIFAR10,以模拟你自己将要使用的数据集。

**注意:**Keras的datasets 模块包含了一些数据集,但这些数据集主要是用于基准测试和学习。我们可以使用tensorflow_datasets ,以获得更大的数据集体!另外,你也可以使用任何其他来源,如Kaggle或学术资料库。

我们将使用tensorflow_datasets ,下载CIFAR10数据集,获得标签和类的数量。

import tensorflow_datasets as tfds
import tensorflow as tf

dataset, info = tfds.load("cifar10", as_supervised=True, with_info=True)
# Save class_names and n_classes for later
class_names = info.features["label"].names
n_classes = info.features["label"].num_classes

print(class_names) # ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
print(n_classes) # 10

你可以这样来了解这个数据集,但我们现在不会深入研究。让我们把它分成一个train_set,valid_settest_set 来代替。

test_set, valid_set, train_set = tfds.load("cifar10", 
                                           split=["train[:10%]", "train[10%:25%]", "train[25%:]"],
                                           as_supervised=True)

print("Train set size: ", len(train_set)) # Train set size:  37500
print("Test set size: ", len(test_set)) # Test set size:  5000
print("Valid set size: ", len(valid_set)) # Valid set size:  7500

注意: split 参数期望的是traintest 关键词,而没有valid 关键词可以用来提取验证集。正因为如此,我们需要像现在这样进行略显尴尬和笨拙的拆分--用10/15/75的方式拆分

现在,CIFAR10的图像与ImageNet的图像有很大的不同也就是说,CIFAR10的图像只有32x32,而我们的EfficientNet模型希望得到224x224的图像。在任何情况下,我们都要调整图像的大小。如果数据集没有足够的图片,我们可能还要在重复的图片上应用一些转换函数,人为地扩大每一类的样本量。在CIFAR10的情况下,这不是一个问题,因为每类有足够的图像,但对于CIFAR100--这是一个不同的故事。值得注意的是,在对这么小的图像进行升级时,即使是人类也很难辨别一些图像上的内容。

例如,这里有几张没有调整大小的图片。

cifar100 image examples

你能自信地说出这些图片上有什么吗?考虑到你对这些图片所拥有的终生的背景,这也是模型所没有的。当你训练它并观察其准确性时,值得记住这一点。

让我们为每张图片和其相关的标签定义一个预处理函数。

def preprocess_image(image, label):
    # Resize to EfficientNet size
    resized_image = tf.image.resize(image, [224, 224])
    # Random flips and rotations (fully optional, and doesn't impact performance much no augmentation takes place)
    # If we run this function multiple times, it'll net different results
    img = tf.image.random_flip_left_right(resized_image)
    img = tf.image.random_flip_up_down(img)
    img = tf.image.rot90(img)
    # Preprocess image with model-specific function if it has one
    # img = preprocess_input(img)
    return img, label

最后,我们要把这个函数应用到各组图像上!我们在这里没有通过扩大集合来进行增强,尽管你可以这样做。为了简洁起见,我们将避免进行数据扩充。

这可以通过map() 函数轻松完成。由于输入到网络中的数据也希望是分批的((None, 224, 224, 3) ,而不是(224, 224, 3) )--我们也会在映射后将数据集batch()

train_set = train_set.map(preprocess_image).batch(32).prefetch(1)
test_set = test_set.map(preprocess_image).batch(32).prefetch(1)
valid_set = valid_set.map(preprocess_image).batch(32).prefetch(1)

注意: prefetch() 函数是可选的,但有助于提高效率。由于模型是在一个批次上训练的,prefetch() 函数预先获取了下一个批次,所以当训练步骤完成后,它就不会被等待了。

最后,我们可以训练这个模型了。

训练一个模型

随着数据的加载、预处理和分割成足够多的数据集,我们可以对其进行模型训练。优化器以及它的超参数、损失函数和度量通常取决于你的具体任务。

由于我们做的是稀疏分类,sparse_categorical_crossentropy 损失应该很有效,Adam 优化器是一个合理的默认损失函数。让我们编译这个模型,并在几个 epochs 上训练它。值得注意的是,网络中的大部分层都被冻结了!我们只训练新的分类。我们只是在提取的特征图的基础上训练新的分类器。

只有当我们训练完顶层后,我们才可能决定解冻特征提取层,让它们再微调一下。这一步是可选的,而且在很多情况下,你不会解冻它们(主要是在处理真正的大型网络时)。一个好的经验法则是尝试比较数据集,并猜测你可以在不重新训练的情况下重新使用层次结构的哪一层。

如果它们真的不同,你可能选择了一个在错误的数据集上预训练的网络。使用Places365(人造物体)的特征提取来对动物进行分类是没有效率的。然而,使用在ImageNet(有各种物体、动物、植物和人类)上训练的网络是有意义的,然后将其用于具有相对类似类别的不同数据集,如CIFAR10。

**注意:**根据你所使用的架构,解冻各层可能是一个坏主意,因为它们的大小。当试图解决一个20M参数的模型并将训练步骤加载到RAM/VRAM中时,你的本地机器很有可能会耗尽内存。如果可能的话,尽量找一个在数据集上预训练过的架构,这个数据集与你的数据集足够相似,你不必改变特征提取器。如果你必须这样做,也不是不可能,但确实会使过程变得更慢。我们将在后面讨论这个问题。

让我们训练新的网络(实际上,只有它的顶部)10个历时。

optimizer = keras.optimizers.Adam(learning_rate=2e-5)

new_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=optimizer, 
                  metrics=["accuracy"])

history = new_model.fit(train_set, 
                        epochs=10,
                        validation_data=valid_set)

**注意:**这可能需要一些时间,最好是在GPU上完成。这取决于模型有多大,以及被输入的数据集。如果你没有GPU,建议你在任何一个能让你获得免费GPU的云供应商上运行这段代码,比如Google CollabKaggle Notebooks等。每个纪元在较强的GPU上可能需要60秒,在较弱的GPU上则需要10分钟。

这时你可以坐下来,去喝杯咖啡(或茶)。10个历时后,训练和验证的准确性看起来不错。

Epoch 1/10
1172/1172 [==============================] - 92s 76ms/step - loss: 1.6582 - accuracy: 0.6373 - val_loss: 1.1582 - val_accuracy: 0.7935
...
Epoch 10/10
1172/1172 [==============================] - 89s 76ms/step - loss: 0.0911 - accuracy: 0.9781 - val_loss: 0.3847 - val_accuracy: 0.8792

~训练集上的准确率为98%,验证集上的准确率为88%--它显然过度拟合了,但还不算太糟。让我们测试一下,并绘制学习曲线。

测试一个模型

让我们先测试一下这个模型,然后再尝试解冻所有的层,看看我们是否可以对它进行微调。

new_model.evaluate(test_set)
# 157/157 [==============================] - 10s 61ms/step - loss: 0.3778 - accuracy: 0.8798
# [0.3778476417064667, 0.879800021648407]

在测试集上的准确率为88%,与验证集上的准确率极为接近!看起来我们的模型概括得很好,但仍有改进的余地。让我们看一下学习曲线。

训练曲线是可以预期的--因为我们只训练了10个epochs,所以它们很短,但是它们很快就趋于平稳,所以我们可能不会因为更多的epochs而获得更好的性能。虽然振荡确实会发生,而且准确率很可能在第11个历时中上升--但可能性不大,所以我们会错过这个机会。

transfer learning training curves

我们可以进一步微调这个网络吗?我们已经替换并重新训练了与特征图分类有关的顶层。让我们试着解冻卷积层,并对其进行微调吧!

解冻层--微调用迁移学习训练的网络

一旦你完成了对顶层的重新训练,你就可以结束交易,对你的模型感到满意。例如,假设你得到了95%的准确率--你真的不需要再进一步了。然而,为什么不呢?

如果你能挤出额外的1%的准确率,这听起来可能不是很多,但考虑到交易的另一端。如果你的模型在100个样本上有95%的准确率,它误分了5个样本。如果你把准确率提高到96%,它就会误分出4个样本。

1%的准确率转化为25%的错误分类

无论你能从你的模型中进一步挤出什么,实际上都会对错误分类的数量产生重大影响。我们的模型有一个相当令人满意的88%的准确率,但如果我们稍微重新训练一下特征提取器,我们很可能可以从中挤出更多。同样,CIFAR10中的图像比ImageNet的图像小得多,这几乎就像一个视力很好的人突然获得了一个巨大的处方,只能通过模糊的眼睛看世界。特征图至少有一定的差异性!这就是为什么我们要在CIFAR10中建立一个模型。

让我们把模型保存到一个文件中,这样我们就不会失去进展,并解冻/微调一个加载的副本,这样我们就不会不小心弄乱原始模型的权重。

new_model.save('effnet_transfer_learning.h5')
loaded_model = keras.models.load_model('effnet_transfer_learning.h5')

现在,我们可以在不影响new_model 的情况下摆弄和改变loaded_model!首先,我们要把loaded_model 从推理模式改回训练模式--即解冻各层,使它们重新可以训练。

**注意:**再次强调,如果一个网络使用BatchNormalization (大多数都是这样),你要在微调网络之前保持其冻结。由于我们不再冻结整个基础网络,我们将只冻结BatchNormalization 层,而允许其他层被改变。

让我们关闭BatchNormalization 层,这样我们的训练就不会付诸东流了。

for layer in loaded_model.layers:
    if isinstance(layer, keras.layers.BatchNormalization):
        layer.trainable = False
    else:
        layer.trainable = True

for index, layer in enumerate(loaded_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

让我们检查一下这是否有效。

Layer: 0, Trainable: True
Layer: 1, Trainable: True
Layer: 2, Trainable: True
Layer: 3, Trainable: True
Layer: 4, Trainable: True
Layer: 5, Trainable: False
Layer: 6, Trainable: True
Layer: 7, Trainable: True
Layer: 8, Trainable: False
...

棒极了!在我们对模型做任何事情之前,为了 "巩固 "训练能力,我们必须重新编译它。这一次,我们将使用一个较小的learning_rate ,因为我们根本不想改变网络,而只是想微调一些特征提取能力和上面的新分类层。

optimizer = keras.optimizers.Adam(learning_rate=1e-6)

# Recompile after turning to trainable
loaded_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=optimizer, 
                  metrics=["accuracy"])

history = loaded_model.fit(train_set, 
                        epochs=10,
                        validation_data=valid_set)

同样,这可能需要一些时间--所以在后台运行时,请啜饮你选择的另一种热饮料。一旦完成,它的验证准确率应该达到90%,在测试集上达到91%。

Epoch 1/10
1172/1172 [==============================] - 389s 328ms/step - loss: 0.0552 - accuracy: 0.9863 - val_loss: 0.3493 - val_accuracy: 0.8941
...
Epoch 10/10
1172/1172 [==============================] - 376s 321ms/step - loss: 0.0196 - accuracy: 0.9955 - val_loss: 0.3373 - val_accuracy: 0.9043

同样,从准确率的角度来看,这并不是一个巨大的跳跃,但它在错误分类的比例上有了更大的减少。让我们评估一下,并将一些预测结果可视化。

loaded_model.evaluate(test_set)

# 157/157 [==============================] - 10s 61ms/step - loss: 0.3177 - accuracy: 0.9108
# [0.3176877200603485, 0.9107999801635742]


fig = plt.figure(figsize=(15, 10))

i = 1
for entry in test_set.take(25):
    # Predict, get the raw Numpy prediction probabilities
    # Reshape entry to the model's expected input shape
    pred = np.argmax(loaded_model.predict(entry[0].numpy()[0].reshape(1, 224, 224, 3)))

    # Get sample image as numpy array
    sample_image = entry[0].numpy()[0]
    # Get associated label
    sample_label = class_names[entry[1].numpy()[0]]
    # Get human label based on the prediction
    prediction_label = class_names[pred]
    ax = fig.add_subplot(5, 5, i)
    
    # Plot image and sample_label alongside prediction_label
    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title("Actual: %s\nPred: %s" % (sample_label, prediction_label))
    i = i+1

plt.tight_layout()
plt.show()

transfer learning efficientnet-b0 model predictions

在这里,我们在前25张图片中看到的唯一错误分类是一辆卡车被错误地归类为一匹马。这可能是由于上下文的关系--它在一个森林里,是棕色的,而且是长条形的。这在一定程度上也符合马的描述,所以在一张模糊的小图片(224x224)中,卡车被错误地分类也就不太奇怪了。

另一件肯定没有帮助的事情是,看起来卡车的门是打开的,这可能看起来像马的脖子,因为它在吃草。

结论

迁移学习是在适用的情况下,将已经学到的知识表征从一个模型转移到另一个模型的过程。

至此,本篇关于使用Keras和Tensorflow进行图像分类的迁移学习指南就结束了。我们首先看了看什么是迁移学习,以及知识表示如何在模型和架构之间共享。

然后,我们看了一些公开发布的最流行、最前沿的图像分类模型,并利用其中的EfficientNet来帮助我们对自己的一些数据进行分类。我们看了一下如何加载和检查预训练的模型,如何使用它们的层,预测和解码结果,以及如何定义你自己的层并将它们与现有的架构交织在一起。

最后,我们加载并预处理了一个数据集,并在上面训练了我们新的分类顶层,然后通过几个额外的epochs解冻各层并进一步微调。