65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 TensorFlow 2 进行迁移学习

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2019年11月25日

CPOL

7分钟阅读

viewsIcon

9130

如何使用 TensorFlow2 进行迁移学习

引言

阅读深度学习的科学论文总是很有趣且**具有教育意义**的。尤其是当它与你当前正在进行的项目相关时。然而,这些论文中包含的架构和**解决方案**往往难以训练。特别是当你想要尝试,比如说,一些ImageNet大规模视觉识别(**ILSCVR**)比赛的获胜者时。我还记得读到VGG16时心想:“这都很酷,但我的 GPU 要报废了。” 为了让我们生活更轻松,TensorFlow 2 **提供了**许多**预训练模型**,你可以快速利用它们。在本文中,我们将了解如何使用一些著名的卷积神经网络(CNN)架构来实现这一点。

此时,你可能会想:“什么是预训练模型?” 本质上,**预训练模型**是一个已经用大型数据集(例如 ImageNet 数据集)训练过的已保存网络。它们可以在 tensorflow.keras.applications 模块中找到。你可以通过两种方式使用它们:一种是直接作为现成解决方案使用,另一种是结合**迁移学习**使用。由于大型数据集通常用于解决全局性问题,因此你可以自定义预训练模型并将其**专业化**以适应特定问题。这样,你就可以利用一些最著名的神经网络,而无需花费太多时间和资源进行**训练**。此外,你还可以通过修改选定层的行为来**微调**这些模型。这将在未来的文章中介绍。

架构

在本文中,我们将使用三个预训练模型来解决**分类**示例:VGG16GoogLeNet(Inception)和ResNet。这些架构中的每一个都曾是ILSCVR比赛的获胜者。VGG16GoogLeNet 在 2014 年取得了最佳成绩,而ResNet 则在 2015 年获胜。这些模型是 TensorFlow 2 的一部分,即 tensorflow.keras.applications 模块。让我们更深入地了解一下这些架构。

VGG16 是我们考虑的第一个架构。它是一个由 K. Simonyan 和 A. Zisserman 在论文“Very Deep Convolutional Networks for Large-Scale Image Recognition”中提出的**大型**卷积神经网络。该网络在 ImageNet 数据集上实现了 92.7% 的 top-5 测试**准确率**。然而,它**花费了数周**才训练完成。下面是该模型的高层概述。

GoogLeNet 也称为Inception。这是因为它利用了两个概念:1×1 卷积Inception 模块。第一个概念,1×1 卷积,用作**维度降低模块**。通过减少维度数量,计算量也随之减少,这意味着网络可以增加深度和宽度。GoogLeNet 没有为每个卷积层使用固定大小,而是使用了Inception 模块

正如你所见,1×1 卷积层3×3 卷积层5×5 卷积层3×3 最大池化层**协同**执行操作,然后它们的输出结果被**堆叠**在一起。GoogLeNet 总共有**22 层**,看起来大致是这样的。

残差网络ResNet)是我们将在本文中使用的最后一个架构。前一个架构存在的问题是它们非常深,有很多层,因此**难以** **训练**(梯度消失)。因此,ResNet 通过所谓的“身份快捷连接”或**残差块**解决了这个问题。

本质上,ResNet 遵循 VGG 的3×3 卷积层设计,其中**每个**卷积层后面都跟着一个批归一化层ReLU 激活函数。但不同之处在于,在最后的ReLU 之前,ResNet 会注入**输入**。其中一种变体是将输入值通过*1×1 卷积层*。

数据集

在本文中,我们使用“猫狗”**数据集**。该数据集包含 23,262 张猫狗图片。

你可能会注意到图像**未经归一化**,并且它们具有不同的**形状**。很棒的是,它作为TensorFlow Datasets 的一部分可用。因此,请确保你在环境中安装了TensorFlow Dataset

pip install tensorflow-dataset

与其他库中的 `dataset` 不同,此 `dataset` **未**分为训练数据和测试数据,因此我们需要自己进行**划分**。你可以在**此处**找到有关该数据集的更多信息。

实现

此实现分为几个**部分**。首先,我们实现一个负责**加载**数据并对其进行准备的类。然后,我们**导入**预训练模型,并构建一个将修改其顶层**类**。最后,我们运行**训练**过程和**评估**过程。当然,在一切开始之前,我们必须导入一些库并定义一些全局常量。

import numpy as np

import matplotlib.pyplot as plt

import tensorflow as tf

import tensorflow_datasets as tfds

IMG_SIZE = 160

BATCH_SIZE = 32

SHUFFLE_SIZE = 1000

IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)

好了,让我们开始**实现**吧!

数据加载器

此类负责加载数据并**准备**好进行处理。它看起来是这样的。

class DataLoader(object):
    def __init__(self, image_size, batch_size):
        
        self.image_size = image_size
        self.batch_size = batch_size
        
        # 80% train data, 10% validation data, 10% test data
        split_weights = (8, 1, 1)
        splits = tfds.Split.TRAIN.subsplit(weighted=split_weights)
        
        (self.train_data_raw, self.validation_data_raw, self.test_data_raw), 
         self.metadata = tfds.load(
            'cats_vs_dogs', split=list(splits),
            with_info=True, as_supervised=True)
        
        # Get the number of train examples
        self.num_train_examples = self.metadata.splits['train'].num_examples*80/100
        self.get_label_name = self.metadata.features['label'].int2str
        
        # Pre-process data
        self._prepare_data()
        self._prepare_batches()
        
    # Resize all images to image_size x image_size
    def _prepare_data(self):
        self.train_data = self.train_data_raw.map(self._resize_sample)
        self.validation_data = self.validation_data_raw.map(self._resize_sample)
        self.test_data = self.test_data_raw.map(self._resize_sample)
    
    # Resize one image to image_size x image_size
    def _resize_sample(self, image, label):
        image = tf.cast(image, tf.float32)
        image = (image/127.5) - 1
        image = tf.image.resize(image, (self.image_size, self.image_size))
        return image, label
    
    def _prepare_batches(self):
        self.train_batches = self.train_data.shuffle(1000).batch(self.batch_size)
        self.validation_batches = self.validation_data.batch(self.batch_size)
        self.test_batches = self.test_data.batch(self.batch_size)
   
    # Get defined number of  not processed images
    def get_random_raw_images(self, num_of_images):
        random_train_raw_data = self.train_data_raw.shuffle(1000)
        return random_train_raw_data.take(num_of_images)

此类包含很多内容。它有几个**方法**,其中一个是“`public`”。

  • _prepare_data – 用于调整 `dataset` 中图像大小并进行归一化的内部方法。从构造函数调用。
  • _resize_sample – 用于调整单个图像大小的内部方法。
  • _prepare_batches – 用于从图像创建批次的内部方法。它创建用于训练和评估过程的 `train_batches`、`validation_batches` 和 `test_batches`。
  • get_random_raw_images – 用于从原始、未处理数据中获取一定数量随机图像的方法。

然而,大部分工作发生在类的**构造函数**中。让我们仔细看看。

def __init__(self, image_size, batch_size):

    self.image_size = image_size
    self.batch_size = batch_size

    # 80% train data, 10% validation data, 10% test data
    split_weights = (8, 1, 1)
    splits = tfds.Split.TRAIN.subsplit(weighted=split_weights)

    (self.train_data_raw, self.validation_data_raw, self.test_data_raw), 
     self.metadata = tfds.load(
        'cats_vs_dogs', split=list(splits),
        with_info=True, as_supervised=True)

    # Get the number of train examples
    self.num_train_examples = self.metadata.splits['train'].num_examples*80/100
    self.get_label_name = self.metadata.features['label'].int2str

    # Pre-process data
    self._prepare_data()
    self._prepare_batches()

首先,我们**定义**通过参数注入的图像和批次大小。然后,由于数据集尚未分为训练和测试数据,我们使用分割权重来分割数据。这是TensorFlow Dataset 引入的一个很酷的功能,因为它让我们**保持**在TensorFlow **生态系统**中,无需引入PandasSciKit Learn 等其他库。完成数据分割后,我们**计算**训练样本的数量,并调用帮助函数来**准备**训练数据。之后,我们只需要实例化该类的**对象**,然后就可以愉快地使用加载的数据了。

data_loader = DataLoader(IMG_SIZE, BATCH_SIZE)

plt.figure(figsize=(10, 8))
i = 0
for img, label in data_loader.get_random_raw_images(20):
    plt.subplot(4, 5, i+1)
    plt.imshow(img)
    plt.title("{} - {}".format(data_loader.get_label_name(label), img.shape))
    plt.xticks([])
    plt.yticks([])
    i += 1
plt.tight_layout()
plt.show()

输出如下

基础模型与包装器

接下来是我们列表中的**预训练模型**的加载。如前所述,这些模型位于 tensorflow.kearas.applications。加载它们非常简单。

vgg16_base = tf.keras.applications.VGG16(input_shape=IMG_SHAPE, 
                                         include_top=False, weights='imagenet')
googlenet_base = tf.keras.applications.InceptionV3(input_shape=IMG_SHAPE, 
                                                   include_top=False, weights='imagenet')
resnet_base = tf.keras.applications.ResNet101V2(input_shape=IMG_SHAPE, 
                                                include_top=False, weights='imagenet')

这样我们就创建了三个感兴趣架构的**基础模型**。请注意,对于每个模型,`include_top` **参数**都定义为 `False`。这意味着这些模型用于**特征提取**。有了它们之后,我们需要修改这些模型的**顶层**,使它们适用于我们具体的**问题**。我们使用 `Wrapper` 类来实现这一点。此类接收注入的**预训练模型**,并添加一个全局平均池化层和一个密集层。本质上,最后的密集层用于我们的二元分类(猫或狗)。`Wrapper` 类将所有这些内容组合成一个**模型**。

class Wrapper(tf.keras.Model):
    def __init__(self, base_model):
        super(Wrapper, self).__init__()
        
        self.base_model = base_model
        self.average_pooling_layer = tf.keras.layers.GlobalAveragePooling2D()
        self.output_layer = tf.keras.layers.Dense(1)
        
    def call(self, inputs):
        x = self.base_model(inputs)
        x = self.average_pooling_layer(x)
        output = self.output_layer(x)
        return output

然后,我们可以创建实际的模型来对猫狗数据集进行分类,并**编译**这些模型。

base_learning_rate = 0.0001

vgg16_base.trainable = False
vgg16 = Wrapper(vgg16_base)
vgg16.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=['accuracy'])

googlenet_base.trainable = False
googlenet = Wrapper(googlenet_base)
googlenet.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=['accuracy'])

resnet_base.trainable = False
resnet = Wrapper(resnet_base)
resnet.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=['accuracy'])

请注意,我们将这些基础模型标记为**不可训练**。这意味着在训练过程中,我们只会训练我们添加的顶层,而底层权重不会改变。

培训

在进入整个训练过程之前,让我们思考一下这样一个事实:原则上,这些模型的大部分已经**训练**好了。所以,我们可以执行评估过程,看看我们的结果如何。

steps_per_epoch = round(data_loader.num_train_examples)//BATCH_SIZE
validation_steps = 20

loss1, accuracy1 = vgg16.evaluate(data_loader.validation_batches, steps = 20)
loss2, accuracy2 = googlenet.evaluate(data_loader.validation_batches, steps = 20)
loss3, accuracy3 = resnet.evaluate(data_loader.validation_batches, steps = 20)

print("--------VGG16---------")
print("Initial loss: {:.2f}".format(loss1))
print("Initial accuracy: {:.2f}".format(accuracy1))
print("---------------------------")

print("--------GoogLeNet---------")
print("Initial loss: {:.2f}".format(loss2))
print("Initial accuracy: {:.2f}".format(accuracy2))
print("---------------------------")

print("--------ResNet---------")
print("Initial loss: {:.2f}".format(loss3))
print("Initial accuracy: {:.2f}".format(accuracy3))
print("---------------------------")

有趣的是,在任何模型**未经训练**的情况下,我们就能获得尚可的结果(50% 的准确率)。

———VGG16———
Initial loss: 5.30
Initial accuracy: 0.51
—————————-

——GoogLeNet—–
Initial loss: 7.21
Initial accuracy: 0.51
—————————-

——–ResNet———
Initial loss: 6.01
Initial accuracy: 0.51
—————————-

从 50% 的准确率开始并不是一件坏事。那么,让我们运行训练过程,看看我们是否有所改进。首先,我们训练 VGG16

history = vgg16.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

**历史**记录看起来大致是这样的。

然后我们训练 GoogLeNet

history = googlenet.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

**历史**记录看起来是这样的。

最后,我们训练 ResNet

history = resnet.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

这是该过程的**历史**记录。

这三个模型的训练只持续了**几个小时**,而不是几周,这得益于我们只训练了顶层而不是整个网络。

评估版

我们看到,一开始,在没有任何训练的情况下,我们获得了大约 50% 的准确率。让我们看看训练后的情况。

loss1, accuracy1 = vgg16.evaluate(data_loader.test_batches, steps = 20)
loss2, accuracy2 = googlenet.evaluate(data_loader.test_batches, steps = 20)
loss3, accuracy3 = resnet.evaluate(data_loader.test_batches, steps = 20)

print("--------VGG16---------")
print("Loss: {:.2f}".format(loss1))
print("Accuracy: {:.2f}".format(accuracy1))
print("---------------------------")

print("--------GoogLeNet---------")
print("Loss: {:.2f}".format(loss2))
print("Accuracy: {:.2f}".format(accuracy2))
print("---------------------------")

print("--------ResNet---------")
print("Loss: {:.2f}".format(loss3))
print("Accuracy: {:.2f}".format(accuracy3))
print("---------------------------")

输出如下

——–VGG16———
Loss: 0.25
Accuracy: 0.93
—————————

——–GoogLeNet———
Loss: 0.54
Accuracy: 0.95
—————————
——–ResNet———
Loss: 0.40
Accuracy: 0.97
—————————

我们可以看到,所有三个模型都取得了非常好的结果,其中 ResNet 以 97% 的准确率领先。

结论

在本文中,我们演示了如何使用TensorFlow 进行**迁移学习**。我们创建了一个实验环境,可以在其中尝试不同的预训练架构,并在短短几个小时内获得良好结果。在我们的示例中,我们使用了三种著名的卷积架构,并快速修改它们以适应特定问题。在下一篇文章中,我们将**微调**这些模型,看看是否能取得更好的结果。

感谢阅读!

历史

  • 2019 年 11 月 25 日:初始版本
© . All rights reserved.