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 官方吉祥物 Professor 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 有一些小的偏差;以前您会使用 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 ━━━━━━━━━━━━━━━━━━━━ 0秒 12秒/步



1/1 ━━━━━━━━━━━━━━━━━━━━ 12秒 12秒/步

预测结果以 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 ━━━━━━━━━━━━━━━━━━━━ 0秒 0微秒/步

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 [37m━━━━━━━━━━━━━━━━━━━━ 4:54:54 24秒/步 - 准确率: 0.5312 - 损失: 4.9475 2/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:59 247毫秒/步 - 准确率: 0.5469 - 损失: 4.9475

3/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:51 236毫秒/步 - 准确率: 0.5660 - 损失: 4.9475



727/727 ━━━━━━━━━━━━━━━━━━━━ 219秒 268毫秒/步 - 准确率: 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 ━━━━━━━━━━━━━━━━━━━━ 0秒 2秒/步



1/1 ━━━━━━━━━━━━━━━━━━━━ 2秒 2秒/步

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(),它接受一个 (images, labels) 元组。

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 7秒/步 - 分类准确度: 0.0000e+00 - 损失: 12.2444 - top_k_分类准确度: 0.0938



96/96 ━━━━━━━━━━━━━━━━━━━━ 38秒 327毫秒/步 - 分类准确度: 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 分类器!