KerasHub:预训练模型 / 开发者指南 / 使用 KerasHub 进行图像分类

使用 KerasHub 进行图像分类

作者: Gowtham Paimagam, lukewood
创建日期 09/24/2024
最后修改 10/22/2024
描述: 使用 KerasHub 训练强大的图像分类器。

在 Colab 中查看 GitHub 源代码

分类是对给定输入图像预测其类别标签的过程。虽然分类是一个相对简单的计算机视觉任务,但现代方法仍然构建于几个复杂的组件之上。幸运的是,Keras 提供了用于构建常用组件的 API。

本指南演示了 KerasHub 在三个复杂程度层面上解决图像分类问题的模块化方法

  • 使用预训练分类器进行推理
  • 微调预训练骨干网络
  • 从零开始训练图像分类器

KerasHub 使用 Keras 3,可以与 TensorFlow、PyTorch 或 Jax 中的任何后端协同工作。在下面的指南中,我们将使用 jax 后端。此指南在 TensorFlow 或 PyTorch 后端运行无需任何更改,只需更新下面的 KERAS_BACKEND

我们使用 Professor Keras,Keras 的官方吉祥物,作为材料复杂度的视觉参考

!!pip install -q --upgrade keras-hub
!!pip install -q --upgrade keras  # Upgrade to Keras 3.
import os

os.environ["KERAS_BACKEND"] = "jax"  # @param ["tensorflow", "jax", "torch"]

import json
import math
import numpy as np
import matplotlib.pyplot as plt

import keras
from keras import losses
from keras import ops
from keras import optimizers
from keras.optimizers import schedules
from keras import metrics
from keras.applications.imagenet_utils import decode_predictions
import keras_hub

# Import tensorflow for [`tf.data`](https://tensorflowcn.cn/api_docs/python/tf/data) and its preprocessing functions
import tensorflow as tf
import tensorflow_datasets as tfds
['',
 '\x1b[1m[\x1b[0m\x1b[34;49mnotice\x1b[0m\x1b[1;39;49m]\x1b[0m\x1b[39;49m A new release of pip is available: \x1b[0m\x1b[31;49m23.0.1\x1b[0m\x1b[39;49m -> \x1b[0m\x1b[32;49m24.2\x1b[0m',
 '\x1b[1m[\x1b[0m\x1b[34;49mnotice\x1b[0m\x1b[1;39;49m]\x1b[0m\x1b[39;49m To update, run: \x1b[0m\x1b[32;49mpip install --upgrade pip\x1b[0m']

使用预训练分类器进行推理

让我们从最简单的 KerasHub API 开始:预训练分类器。在此示例中,我们将构建一个在 ImageNet 数据集上预训练的分类器。我们将使用此模型来解决古老的“猫还是狗”问题。

KerasHub 中最高级别的模块是 tasktask 是一个 keras.Model,由(通常是预训练的)骨干网络模型和特定于任务的层组成。这里是一个使用 keras_hub.models.ImageClassifier 和 ResNet 骨干网络的示例。

在构建图像分类流水线时,ResNet 是一个很棒的起始模型。此架构在实现高准确率的同时,使用了紧凑的参数数量。如果 ResNet 不足以解决您希望解决的任务,请务必查看 KerasHub 的其他可用骨干网络

classifier = keras_hub.models.ImageClassifier.from_preset("resnet_v2_50_imagenet")

您可能会注意到与旧的 keras.applications API 的微小差异;在旧 API 中,您会使用 Resnet50V2(weights="imagenet") 构造类。虽然旧 API 对于分类来说非常出色,但它无法有效扩展到需要复杂架构的其他用例,例如目标检测和语义分割。

我们首先创建一个用于在本教程中绘制图像的实用函数

def plot_image_gallery(images, titles=None, num_cols=3, figsize=(6, 12)):
    num_images = len(images)
    images = np.asarray(images) / 255.0
    images = np.minimum(np.maximum(images, 0.0), 1.0)
    num_rows = (num_images + num_cols - 1) // num_cols
    fig, axes = plt.subplots(num_rows, num_cols, figsize=figsize, squeeze=False)
    axes = axes.flatten()  # Flatten in case the axes is a 2D array

    for i, ax in enumerate(axes):
        if i < num_images:
            # Plot the image
            ax.imshow(images[i])
            ax.axis("off")  # Remove axis
            if titles and len(titles) > i:
                ax.set_title(titles[i], fontsize=12)
        else:
            # Turn off the axis for any empty subplot
            ax.axis("off")

    plt.show()
    plt.close()

构建好分类器后,让我们将其应用于这张可爱的猫咪照片!

filepath = keras.utils.get_file(
    origin="https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/5hR96puA_VA.jpg/1024px-5hR96puA_VA.jpg"
)
image = keras.utils.load_img(filepath)
image = np.array([image])
plot_image_gallery(image, num_cols=1, figsize=(3, 3))

png

接下来,让我们从分类器中获取一些预测结果

predictions = classifier.predict(image)

1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 12s/step



1/1 ━━━━━━━━━━━━━━━━━━━━ 12s 12s/step

预测结果以 Softmax 处理后的类别排名形式呈现。我们可以使用 Keras 的 imagenet_utils.decode_predictions 函数将它们映射到类别名称

print(f"Top two classes are:\n{decode_predictions(predictions, top=2)}")
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json
 0/35363 ━━━━━━━━━━━━━━━━━━━━  0s 0s/step


35363/35363 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step

Top two classes are:
[[('n02123394', 'Persian_cat', -1.3963771), ('n02808304', 'bath_towel', -2.0231562)]]

太棒了!这两个似乎都正确!然而,其中一个类别是“浴巾”。我们正在尝试分类猫 VS 狗。我们不关心浴巾!

理想情况下,我们希望有一个只执行计算来确定图像是猫还是狗的分类器,并将所有资源都投入到此任务中。这可以通过微调我们自己的分类器来解决。


微调预训练分类器

当有针对我们任务的带标签图像可用时,微调定制分类器可以提高性能。如果我们想训练一个猫 VS 狗分类器,使用明确标记的猫 vs 狗数据应该比通用分类器表现更好!对于许多任务来说,可能没有相关的预训练模型可用(例如,对您应用程序特有的图像进行分类)。

首先,让我们开始加载一些数据

BATCH_SIZE = 32
IMAGE_SIZE = (224, 224)
AUTOTUNE = tf.data.AUTOTUNE
tfds.disable_progress_bar()

data, dataset_info = tfds.load("cats_vs_dogs", with_info=True, as_supervised=True)
train_steps_per_epoch = dataset_info.splits["train"].num_examples // BATCH_SIZE
train_dataset = data["train"]

num_classes = dataset_info.features["label"].num_classes

resizing = keras.layers.Resizing(
    IMAGE_SIZE[0], IMAGE_SIZE[1], crop_to_aspect_ratio=True
)


def preprocess_inputs(image, label):
    image = tf.cast(image, tf.float32)
    # Staticly resize images as we only iterate the dataset once.
    return resizing(image), tf.one_hot(label, num_classes)


# Shuffle the dataset to increase diversity of batches.
# 10*BATCH_SIZE follows the assumption that bigger machines can handle bigger
# shuffle buffers.
train_dataset = train_dataset.shuffle(
    10 * BATCH_SIZE, reshuffle_each_iteration=True
).map(preprocess_inputs, num_parallel_calls=AUTOTUNE)
train_dataset = train_dataset.batch(BATCH_SIZE)

images = next(iter(train_dataset.take(1)))[0]
plot_image_gallery(images)

png

喵!

接下来让我们构建我们的模型。预设名称中使用 imagenet 表明该骨干网络已在 ImageNet 数据集上进行了预训练。预训练的骨干网络通过利用从可能更大的数据集中提取的模式,从我们的带标签示例中提取更多信息。

接下来让我们组合我们的分类器

model = keras_hub.models.ImageClassifier.from_preset(
    "resnet_v2_50_imagenet", num_classes=2
)
model.compile(
    loss="categorical_crossentropy",
    optimizer=keras.optimizers.SGD(learning_rate=0.01),
    metrics=["accuracy"],
)

在这里,我们的分类器只是一个简单的 keras.Sequential。剩下的就是调用 model.fit()

model.fit(train_dataset)

1/727 [37m━━━━━━━━━━━━━━━━━━━━ 4:54:54 24s/step - accuracy: 0.5312 - loss: 4.9475 2/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:59 247ms/step - accuracy: 0.5469 - loss: 4.9475

3/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:51 236ms/step - accuracy: 0.5660 - loss: 4.9475



727/727 ━━━━━━━━━━━━━━━━━━━━ 219s 268ms/step - accuracy: 0.6553 - loss: 0.7275

<keras.src.callbacks.history.History at 0x7f5b2888e670>

让我们看看我们的模型在微调后的表现如何

predictions = model.predict(image)

classes = {0: "cat", 1: "dog"}
print("Top class is:", classes[predictions[0].argmax()])

1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 2s/step



1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step

Top class is: cat

太棒了 - 看起来模型正确地对图像进行了分类。


从零开始训练分类器

现在我们已经对分类有了一些经验,让我们来完成最后一个任务:从零开始训练一个分类模型!图像分类的标准基准是 ImageNet 数据集,然而由于许可限制,我们将在本教程中使用 CalTech 101 图像分类数据集。虽然我们在本指南中使用了更简单的 CalTech 101 数据集,但相同的训练模板可用于 ImageNet,以达到接近最先进的分数。

让我们从处理数据加载开始

BATCH_SIZE = 32
NUM_CLASSES = 101
IMAGE_SIZE = (224, 224)

# Change epochs to 100~ to fully train.
EPOCHS = 1


def package_inputs(image, label):
    return {"images": image, "labels": tf.one_hot(label, NUM_CLASSES)}


train_ds, eval_ds = tfds.load(
    "caltech101", split=["train", "test"], as_supervised="true"
)
train_ds = train_ds.map(package_inputs, num_parallel_calls=tf.data.AUTOTUNE)
eval_ds = eval_ds.map(package_inputs, num_parallel_calls=tf.data.AUTOTUNE)

train_ds = train_ds.shuffle(BATCH_SIZE * 16)
augmenters = []

CalTech101 数据集中每张图像的大小都不同,因此在使用 batch() API 进行批处理之前,我们会调整图像的大小。

resize = keras.layers.Resizing(*IMAGE_SIZE, crop_to_aspect_ratio=True)
train_ds = train_ds.map(resize)
eval_ds = eval_ds.map(resize)

train_ds = train_ds.batch(BATCH_SIZE)
eval_ds = eval_ds.batch(BATCH_SIZE)

batch = next(iter(train_ds.take(1)))
image_batch = batch["images"]
label_batch = batch["labels"]

plot_image_gallery(
    image_batch,
)

png

数据增强

在之前的微调示例中,我们执行了静态的尺寸调整操作,并且没有使用任何图像增强。这是因为一次遍历训练集就足以获得不错的结果。当训练解决更困难的任务时,您会希望在数据流水线中包含数据增强。

数据增强是一种使模型对输入数据(例如光照、裁剪和方向)的变化具有鲁棒性的技术。Keras 在 keras.layers API 中包含了一些最有用的增强。创建最佳的增强流水线是一门艺术,但在本指南的这一部分,我们将提供一些关于分类最佳实践的技巧。

在使用图像数据增强时需要注意的一个警告是,您必须小心不要将增强数据的分布偏离原始数据分布太远。目标是防止过拟合并提高泛化能力,但完全超出数据分布范围的样本只会给训练过程增加噪声。

我们将使用的第一个增强是 RandomFlip。此增强的行为或多或少如您所预期:它要么翻转图像,要么不翻转。虽然此增强在 CalTech101 和 ImageNet 中很有用,但需要注意的是,它不应该用于数据分布对垂直镜像不变的任务。此类数据集的一个例子是 MNIST 手写数字。将 6 沿垂直轴翻转会使数字看起来更像 7 而不是 6,但标签仍然显示 6

random_flip = keras.layers.RandomFlip()
augmenters += [random_flip]

image_batch = random_flip(image_batch)
plot_image_gallery(image_batch)

png

一半的图像被翻转了!

我们将使用的下一个增强是 RandomCrop。此操作选择图像的随机子集。通过使用此增强,我们迫使分类器变得空间不变。

让我们向增强集添加 RandomCrop

crop = keras.layers.RandomCrop(
    int(IMAGE_SIZE[0] * 0.9),
    int(IMAGE_SIZE[1] * 0.9),
)

augmenters += [crop]

image_batch = crop(image_batch)
plot_image_gallery(
    image_batch,
)

png

我们还可以使用 Keras 的 RandomRotation 层按随机角度旋转图像。让我们应用一个在 -45°...45° 区间内随机选择的角度进行旋转

rotate = keras.layers.RandomRotation((-45 / 360, 45 / 360))

augmenters += [rotate]

image_batch = rotate(image_batch)
plot_image_gallery(image_batch)

resize = keras.layers.Resizing(*IMAGE_SIZE, crop_to_aspect_ratio=True)
augmenters += [resize]

image_batch = resize(image_batch)
plot_image_gallery(image_batch)

png

png

现在让我们将最终的增强器应用于训练数据

def create_augmenter_fn(augmenters):
    def augmenter_fn(inputs):
        for augmenter in augmenters:
            inputs["images"] = augmenter(inputs["images"])
        return inputs

    return augmenter_fn


augmenter_fn = create_augmenter_fn(augmenters)
train_ds = train_ds.map(augmenter_fn, num_parallel_calls=tf.data.AUTOTUNE)

image_batch = next(iter(train_ds.take(1)))["images"]
plot_image_gallery(
    image_batch,
)

png

我们还需要调整评估集的大小,以获得模型期望的图像大小的密集批次。在这种情况下,我们直接使用确定性的 keras.layers.Resizing,以避免由于应用随机增强而给评估指标增加噪声。

inference_resizing = keras.layers.Resizing(*IMAGE_SIZE, crop_to_aspect_ratio=True)


def do_resize(inputs):
    inputs["images"] = inference_resizing(inputs["images"])
    return inputs


eval_ds = eval_ds.map(do_resize, num_parallel_calls=tf.data.AUTOTUNE)

image_batch = next(iter(eval_ds.take(1)))["images"]
plot_image_gallery(
    image_batch,
)

png

最后,让我们解包数据集,并准备将它们传递给接受 (images, labels) 元组的 model.fit()

def unpackage_dict(inputs):
    return inputs["images"], inputs["labels"]


train_ds = train_ds.map(unpackage_dict, num_parallel_calls=tf.data.AUTOTUNE)
eval_ds = eval_ds.map(unpackage_dict, num_parallel_calls=tf.data.AUTOTUNE)

数据增强是训练现代分类器中最困难的部分。恭喜您走到这一步!

优化器调优

为了获得最佳性能,我们需要使用学习率调度而不是单一的学习率。虽然我们不会详细介绍此处使用的带有预热的余弦衰减调度,但您可以在此处阅读更多内容

def lr_warmup_cosine_decay(
    global_step,
    warmup_steps,
    hold=0,
    total_steps=0,
    start_lr=0.0,
    target_lr=1e-2,
):
    # Cosine decay
    learning_rate = (
        0.5
        * target_lr
        * (
            1
            + ops.cos(
                math.pi
                * ops.convert_to_tensor(
                    global_step - warmup_steps - hold, dtype="float32"
                )
                / ops.convert_to_tensor(
                    total_steps - warmup_steps - hold, dtype="float32"
                )
            )
        )
    )

    warmup_lr = target_lr * (global_step / warmup_steps)

    if hold > 0:
        learning_rate = ops.where(
            global_step > warmup_steps + hold, learning_rate, target_lr
        )

    learning_rate = ops.where(global_step < warmup_steps, warmup_lr, learning_rate)
    return learning_rate


class WarmUpCosineDecay(schedules.LearningRateSchedule):
    def __init__(self, warmup_steps, total_steps, hold, start_lr=0.0, target_lr=1e-2):
        super().__init__()
        self.start_lr = start_lr
        self.target_lr = target_lr
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.hold = hold

    def __call__(self, step):
        lr = lr_warmup_cosine_decay(
            global_step=step,
            total_steps=self.total_steps,
            warmup_steps=self.warmup_steps,
            start_lr=self.start_lr,
            target_lr=self.target_lr,
            hold=self.hold,
        )
        return ops.where(step > self.total_steps, 0.0, lr)

WarmUpCosineDecay schedule

该调度按预期进行。

接下来让我们构建这个优化器

total_images = 9000
total_steps = (total_images // BATCH_SIZE) * EPOCHS
warmup_steps = int(0.1 * total_steps)
hold_steps = int(0.45 * total_steps)
schedule = WarmUpCosineDecay(
    start_lr=0.05,
    target_lr=1e-2,
    warmup_steps=warmup_steps,
    total_steps=total_steps,
    hold=hold_steps,
)
optimizer = optimizers.SGD(
    weight_decay=5e-4,
    learning_rate=schedule,
    momentum=0.9,
)

终于,我们现在可以构建模型并调用 fit() 了!在这里,我们直接实例化 ResNetBackbone,指定所有架构参数,这使我们能够完全控制调整架构。

backbone = keras_hub.models.ResNetBackbone(
    input_conv_filters=[64],
    input_conv_kernel_sizes=[7],
    stackwise_num_filters=[64, 64, 64],
    stackwise_num_blocks=[2, 2, 2],
    stackwise_num_strides=[1, 2, 2],
    block_type="basic_block",
)
model = keras.Sequential(
    [
        backbone,
        keras.layers.GlobalMaxPooling2D(),
        keras.layers.Dropout(rate=0.5),
        keras.layers.Dense(101, activation="softmax"),
    ]
)

我们采用标签平滑来防止模型对增强过程中的伪影产生过拟合。

loss = losses.CategoricalCrossentropy(label_smoothing=0.1)

让我们编译模型

model.compile(
    loss=loss,
    optimizer=optimizer,
    metrics=[
        metrics.CategoricalAccuracy(),
        metrics.TopKCategoricalAccuracy(k=5),
    ],
)

最后调用 fit()。

model.fit(
    train_ds,
    epochs=EPOCHS,
    validation_data=eval_ds,
)

1/96 [37m━━━━━━━━━━━━━━━━━━━━ 11:13 7s/step - categorical_accuracy: 0.0000e+00 - loss: 12.2444 - top_k_categorical_accuracy: 0.0938



96/96 ━━━━━━━━━━━━━━━━━━━━ 38s 327ms/step - categorical_accuracy: 0.0089 - loss: 8.5603 - top_k_categorical_accuracy: 0.0593 - val_categorical_accuracy: 0.0092 - val_loss: 5.7528 - val_top_k_categorical_accuracy: 0.0761

<keras.src.callbacks.history.History at 0x7f5b2892d190>

恭喜!您现在知道如何使用 KerasHub 从零开始训练一个强大的图像分类器了。根据您应用程序中带标签数据的可用性,从零开始训练可能比使用迁移学习加上上面讨论的数据增强更强大,也可能不强大。对于较小的数据集,预训练模型通常能产生较高的准确率和更快的收敛。


结论

虽然图像分类也许是计算机视觉中最简单的问题,但现代领域有众多复杂的组件。幸运的是,KerasHub 提供了强大、生产级别的 API,只需一行代码即可组装大多数这些组件。通过使用 KerasHub 的 ImageClassifier API、预训练权重和 Keras 的数据增强,您可以在几百行代码内组装训练一个强大分类器所需的一切!

作为后续练习,请尝试在您自己的数据集上微调一个 KerasHub 分类器!