作者: 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 有一些小的偏差;以前您会使用 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 ━━━━━━━━━━━━━━━━━━━━ 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 [37m━━━━━━━━━━━━━━━━━━━━ 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)
喵!
接下来,让我们构建模型。预设名称中的 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,
)
在我们之前的微调示例中,我们执行了静态的图像大小调整操作,并没有使用任何图像增强。这是因为对训练集进行一次遍历就足以获得不错的结果。当训练解决更困难的任务时,您会希望在数据管道中包含数据增强。
数据增强是一种使模型对输入数据的变化(例如光照、裁剪和方向)具有鲁棒性的技术。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 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 分类器!