使用 TensorFlow 2 进行迁移学习





5.00/5 (4投票s)
如何使用 TensorFlow2 进行迁移学习
引言
阅读深度学习的科学论文总是很有趣且**具有教育意义**的。尤其是当它与你当前正在进行的项目相关时。然而,这些论文中包含的架构和**解决方案**往往难以训练。特别是当你想要尝试,比如说,一些ImageNet大规模视觉识别(**ILSCVR**)比赛的获胜者时。我还记得读到VGG16时心想:“这都很酷,但我的 GPU 要报废了。” 为了让我们生活更轻松,TensorFlow 2 **提供了**许多**预训练模型**,你可以快速利用它们。在本文中,我们将了解如何使用一些著名的卷积神经网络(CNN)架构来实现这一点。
此时,你可能会想:“什么是预训练模型?” 本质上,**预训练模型**是一个已经用大型数据集(例如 ImageNet 数据集)训练过的已保存网络。它们可以在 tensorflow.keras.applications
模块中找到。你可以通过两种方式使用它们:一种是直接作为现成解决方案使用,另一种是结合**迁移学习**使用。由于大型数据集通常用于解决全局性问题,因此你可以自定义预训练模型并将其**专业化**以适应特定问题。这样,你就可以利用一些最著名的神经网络,而无需花费太多时间和资源进行**训练**。此外,你还可以通过修改选定层的行为来**微调**这些模型。这将在未来的文章中介绍。
架构
在本文中,我们将使用三个预训练模型来解决**分类**示例:VGG16、GoogLeNet(Inception)和ResNet。这些架构中的每一个都曾是ILSCVR比赛的获胜者。VGG16 和GoogLeNet 在 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 **生态系统**中,无需引入Pandas 或SciKit 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 日:初始版本