预先训练的模型和迁移学习
- 10 分钟
训练 CNN 可能需要大量的时间,并且该任务需要大量数据。 大部分时间都在试验中寻找网络需要从图像中提取模式的最佳低级别筛选器。 出现一个自然问题 - 我们是否可以使用在一个数据集上训练的神经网络,并使其适应对不同图像进行分类,而无需完全训练过程?
此方法称为 “转移学习”,因为我们将一些知识从一个神经网络模型转移到另一个神经网络模型。 在转移学习中,我们通常从预先训练的模型开始,该模型已在一些大型图像数据集(如 ImageNet)上训练。 这些模型已经做好了从泛型图像中提取不同特征的工作,在许多情况下,只需在这些提取的特征的基础上生成分类器即可产生良好的结果。
import tensorflow as tf
import keras
import matplotlib.pyplot as plt
import numpy as np
import os
import glob
from PIL import Image
猫与狗数据集
在本单元中,我们将解决对猫和狗的图像进行分类的实际问题。 因此,我们将使用 Kaggle 猫与狗数据集,该数据集也可以从 Microsoft 下载。
让我们下载此数据集并将其提取到 data 目录中:
import urllib.request
import zipfile
dataset_url = 'https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip'
data_dir = 'data'
os.makedirs(data_dir, exist_ok=True)
zip_path = os.path.join(data_dir, 'kagglecatsanddogs_5340.zip')
if not os.path.exists(zip_path):
urllib.request.urlretrieve(dataset_url, zip_path)
if not os.path.exists(os.path.join(data_dir, 'PetImages')):
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(data_dir)
数据集可能包含一些损坏的图像文件。 让我们定义一个帮助程序函数,用于在加载之前检查和删除它们:
def check_image(fn):
try:
im = Image.open(fn)
im.verify()
return True
except (IOError, SyntaxError):
return False
def check_image_dir(dir_path):
for fn in glob.glob(dir_path):
if not check_image(fn):
print(f"Corrupt image: {fn}")
os.remove(fn)
# Remove any corrupt images from the dataset
check_image_dir('data/PetImages/Cat/*.jpg')
check_image_dir('data/PetImages/Dog/*.jpg')
加载数据集
在前面的示例中,我们加载内置于 Keras 中的数据集。 现在,我们将使用自己的数据集,我们需要从图像目录加载该数据集。 Keras 包含一个帮助程序函数 image_dataset_from_directory,可以从按类组织到子目录的图像目录中创建 tf.data.Dataset。
data_dir = 'data/PetImages'
batch_size = 32
ds_train = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset='training',
seed=13,
image_size=(224, 224),
batch_size=batch_size
)
ds_test = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset='validation',
seed=13,
image_size=(224, 224),
batch_size=batch_size
)
注释
创建训练和验证拆分时,我们将使用相同的 seed 值,以确保这两个子集之间没有重叠。
我们可以检查从目录结构中自动推断的类名:
# Expected output: ['Cat', 'Dog']
ds_train.class_names
让我们定义一个帮助程序来可视化数据集中的示例(这是为批处理数据定制的新版本 display_dataset ):
def display_dataset(images, labels, classes=None, cols=8):
n = len(images)
rows = (n + cols - 1) // cols
fig, axes = plt.subplots(rows, cols, figsize=(cols * 1.5, rows * 1.5))
axes = axes.flatten() if n > 1 else [axes]
for i, ax in enumerate(axes):
if i < n:
ax.imshow(images[i])
label = int(labels[i][0]) if labels[i].ndim > 0 else int(labels[i])
title = classes[label] if classes else str(label)
ax.set_title(title, fontsize=8)
ax.axis('off')
plt.tight_layout()
plt.show()
数据集会以批处理方式生成图像和标签。 每个批次包含 32 张大小为 224×224 的图像,其中包含 3 个颜色通道,以及相应的标签:
for x, y in ds_train:
print(f"Training batch shape: features={x.shape}, labels={y.shape}")
x_sample, y_sample = x, y
break
# Expected output: Training batch shape: features=(32, 224, 224, 3), labels=(32,)
display_dataset(x_sample.numpy().astype(np.uint8), np.expand_dims(y_sample, 1), classes=ds_train.class_names)
注释
图像像素值在 0-255 范围内。 某些模型需要将输入缩放到 0-1,或使用特定于模型的函数进行预处理。 VGG-16 有自己的 preprocess_input 函数,我们稍后使用。
预先训练的模型
许多预先训练的神经网络用于图像分类,这些神经网络已在 ImageNet 数据集上训练,其中包含 1,000 多个类别中的 1400 多万张图像。 最知名的体系结构之一是 VGG-16,它可实现良好的准确性,同时易于理解。 让我们加载一个带有预训练权重的 VGG-16 模型:
vgg = keras.applications.VGG16()
让我们尝试使用此预先训练的网络对其中一个图像进行分类。 VGG-16 网络在 ImageNet 上进行了训练,其中包括各种狗和猫品种的类别:
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
# tf.argmax returns the index of the highest-probability class
print(f"Most probable class = {tf.argmax(res, 1)}")
# decode_predictions maps class indices to human-readable labels
keras.applications.vgg16.decode_predictions(res.numpy())
该 preprocess_input 函数可适当缩放 VGG-16 模型的像素值。 该 decode_predictions 函数返回前 5 个最可能的 ImageNet 类及其置信度分数。
让我们看看 VGG-16 的体系结构:
# Shows all layers including convolutional blocks and final Dense classifier
vgg.summary()
GPU 计算
深度神经网络需要相当实质性的计算能力来训练。 使用 GPU 可以显著加快训练过程。 让我们检查 GPU 是否可用:
# Lists available GPU devices; an empty list means CPU-only
tf.config.list_physical_devices('GPU')
提取 VGG 特征
如果想要使用 VGG-16 从图像中提取特征,则需要没有最终分类层的模型。 可以通过指定 include_top=False以下内容来执行此操作:
vgg = keras.applications.VGG16(include_top=False)
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
# The output is a 7x7 grid of 512 feature maps
print(f"Shape after applying VGG-16: {res[0].shape}")
plt.figure(figsize=(15, 3))
plt.imshow(res[0].numpy().reshape(-1, 512))
生成的特征向量具有形状 7×7×512 = 25088 值。 这代表了 VGG-16 已经学会从图像中提取的高级特征。 我们可以为整个数据集手动预计算这些功能,然后在上面训练分类器:
警告
我们在此示例中使用 .take(25) 和 .take(10) 来限制数据集大小,以加快训练。 每个批次包含 32 个图像,因此我们仅使用 800 个训练图像和 320 个测试图像。 此处报告的大约 90% 准确性反映了这一小部分,可能无法推广到整个数据集。 若要用于生产,请训练完整数据集。
def preprocess(x, y):
return keras.applications.vgg16.preprocess_input(x), y
ds_features_train = ds_train.take(25).map(preprocess).map(lambda x, y: (vgg(x), y)).cache()
ds_features_test = ds_test.take(10).map(preprocess).map(lambda x, y: (vgg(x), y)).cache()
for x, y in ds_features_train:
# Expected output: (32, 7, 7, 512) (32,)
print(x.shape, y.shape)
break
注释
提取特征后调用 .cache() ,这样可以使 VGG-16 模型每批仅运行一次,而不是每个周期。
现在,我们可以在提取的特征上生成一个简单的分类器。 由于 VGG 功能已具有很高的信息性,因此即使是单个密集层也能取得良好的效果:
model = keras.Sequential([
keras.layers.Input(shape=(7, 7, 512)),
keras.layers.Flatten(),
keras.layers.Dense(1, activation='sigmoid')
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
hist = model.fit(ds_features_train, validation_data=ds_features_test)
# Expected: validation accuracy around 90%
准确率约为 90%,这展示了预训练功能的强大之处! 然而,手动预计算功能很麻烦。
通过一个 VGG 网络进行迁移学习
通过将 VGG-16 功能提取器和分类器合并到单个网络中,我们可以避免手动预计算功能。 关键是要冻结预训练层,这样它们的权重在训练过程中就不会更新。
我们将 preprocess_input 步骤移动到数据管道中,而不是将其作为 Lambda 层嵌入模型中。 这样就可以使模型可序列化,以便稍后保存并加载它:
def preprocess(x, y):
return keras.applications.vgg16.preprocess_input(x), y
ds_train_preprocessed = ds_train.map(preprocess)
ds_test_preprocessed = ds_test.map(preprocess)
注释
由于预处理现在是数据管道的一部分,而不是模型,因此在推理时,你必须将 preprocess_input 应用于输入数据。
现在,我们将使用冻结的 VGG-16 基生成模型:
vgg_base = keras.applications.VGG16(include_top=False, input_shape=(224, 224, 3))
vgg_base.trainable = False
model = keras.Sequential([
keras.layers.Input(shape=(224, 224, 3)),
vgg_base,
keras.layers.Flatten(),
keras.layers.Dense(1, activation='sigmoid')
])
# Notice: ~15 million params are non-trainable (VGG-16), only ~25k are trainable
model.summary()
通过冻结 VGG-16 层,我们只需要训练最终的密集层,该层具有大约 25,000 个参数,而不是完整的 1500 万个参数。 这使得训练更快:
警告
与上一部分一样,我们使用 .take(50) 和 .take(10) 限制数据集以加快训练速度。 这意味着我们正在用大约 1,600 张图像进行训练,并使用 320 张图像进行验证。 在对完整数据集进行训练时,准确性结果可能会有所不同。
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
hist = model.fit(ds_train_preprocessed.take(50), validation_data=ds_test_preprocessed.take(10))
# Expected: validation accuracy around 90% or higher
保存和加载模型
训练模型后,我们可以将其保存到磁盘,并在以后重新加载它,而无需重新训练:
model.save('data/cats_dogs.keras')
注释
该 .keras 扩展使用原生 Keras 3 格式。 如果使用的是旧版 TensorFlow/Keras,请改用 .h5 (HDF5 格式)或 SavedModel 目录格式。
加载保存的模型:
model = keras.models.load_model('data/cats_dogs.keras')
其他计算机视觉模型
VGG-16 是最容易理解的深度 CNN 体系结构之一,因为它采用了统一的 3×3 卷积堆叠结构。 Keras 提供了更多预先训练的网络。 其中最常用的架构是 Microsoft 开发的 ResNet 架构和 Google 开发的 Inception 架构。
使用数据扩充改进结果
使用有限的训练数据时, 数据扩充 可以显著提高通用化。 通过将随机转换(如水平翻转、旋转和缩放)应用于训练图像,我们人为地增加了数据集的多样性。 Keras 提供的扩充层有 keras.layers.RandomFlip、keras.layers.RandomRotation 和 keras.layers.RandomZoom,这些层可以直接添加到您的模型或数据管道中。
要点
借助迁移学习,我们能够快速为自定义对象分类任务组合分类器,并实现高精度。 此示例并不完全公平,因为原始 VGG-16 网络已在 ImageNet 上预先训练,该网络已经包括各种猫和狗品种的类别,因此我们只是重用网络中已存在的大多数模式。 对于其他特定于领域的对象,例如工厂生产线上的细节或不同的树叶,你可能会期望较低的准确性。 可以看到,更复杂的任务需要更高的计算能力,并且通常受益于 GPU 加速进行训练。