作者: Gowtham Paimagam, lukewood
创建日期 09/24/2024
最后修改日期 10/22/2024
描述: 使用 KerasHub 训练强大的图像分类器。
分类是指为给定输入图像预测类别标签的过程。虽然分类是一项相对直接的计算机视觉任务,但现代方法仍然由几个复杂的组件构成。幸运的是,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 相比存在细微差别;旧 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))

接下来,让我们获取分类器的预测结果。
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 [37m━━━━━━━━━━━━━━━━━━━━ 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)

喵!
接下来,我们构建模型。预设名称中使用的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/步 - accuracy: 0.5312 - loss: 4.9475 2/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:59 247ms/步 - accuracy: 0.5469 - loss: 4.9475
3/727 [37m━━━━━━━━━━━━━━━━━━━━ 2:51 236ms/步 - accuracy: 0.5660 - loss: 4.9475
727/727 ━━━━━━━━━━━━━━━━━━━━ 219s 268ms/步 - 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/步
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,
)

在我们之前的微调示例中,我们执行了静态重塑操作,并且没有使用任何图像增强。这是因为对训练集的单次通过足以获得不错的结果。在训练以解决更困难的任务时,您将希望在数据管道中包含数据增强。
数据增强是一种使您的模型能够应对输入数据中光照、裁剪和方向等变化的技巧。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)

一半的图像已被翻转!
我们将使用的下一个增强是 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,
)

我们还可以使用 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)


现在让我们将最后一个增强器应用于训练数据。
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,
)

我们还需要调整评估集的大小,以获取我们模型期望的图像大小的密集批次。在这种情况下,我们直接使用确定性的 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,
)

最后,让我们解包我们的数据集,并准备将它们传递给 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)

正如预期的那样,调度看起来是这样的。
接下来,让我们构建这个优化器。
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/步 - categorical_accuracy: 0.0000e+00 - loss: 12.2444 - top_k_categorical_accuracy: 0.0938
96/96 ━━━━━━━━━━━━━━━━━━━━ 38s 327ms/步 - 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 分类器!