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 即可。

我们使用 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 中最高级别的模块是任务任务是一个 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/步



1/1 ━━━━━━━━━━━━━━━━━━━━ 12s 12s/步

预测以 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/步

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

太棒了!这两个看起来都是正确的!但是,其中一个类是“浴巾”。我们试图将猫与狗进行分类。我们不关心毛巾!

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


微调预训练的分类器

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

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

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 ━━━━━━━━━━━━━━━━━━━━ 4:54:54 24s/步 - 准确率: 0.5312 - 损失: 4.9475 2/727 ━━━━━━━━━━━━━━━━━━━━ 2:59 247ms/步 - 准确率: 0.5469 - 损失: 4.9475

3/727 ━━━━━━━━━━━━━━━━━━━━ 2:51 236ms/步 - 准确率: 0.5660 - 损失: 4.9475



727/727 ━━━━━━━━━━━━━━━━━━━━ 219s 268ms/步 - 准确率: 0.6553 - 损失: 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/步



1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/步

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

最后,让我们解包我们的数据集并准备将其传递给 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 ━━━━━━━━━━━━━━━━━━━━ 11:13 7s/步 - 分类准确率: 0.0000e+00 - 损失: 12.2444 - top_k_分类准确率: 0.0938



96/96 ━━━━━━━━━━━━━━━━━━━━ 38s 327ms/步 - 分类准确率: 0.0089 - 损失: 8.5603 - top_k_分类准确率: 0.0593 - val_分类准确率: 0.0092 - val_损失: 5.7528 - val_top_k_分类准确率: 0.0761

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

恭喜你!你现在已经知道如何使用 KerasHub 从头开始训练一个强大的图像分类器了。根据你的应用是否有标记数据,从头开始训练可能比使用迁移学习加上上面讨论的数据增强更强大,也可能不那么强大。对于较小的数据集,预训练模型通常会产生更高的准确率和更快的收敛速度。


结论

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

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