作者: 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://tensorflow.dev.org.tw/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()
,它接受 (圖像, 標籤)
的元組。
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 - 前 k 個類別準確度:0.0938
96/96 ━━━━━━━━━━━━━━━━━━━━ 38 秒 327 毫秒/步 - 類別準確度:0.0089 - 損失:8.5603 - 前 k 個類別準確度:0.0593 - 驗證類別準確度:0.0092 - 驗證損失:5.7528 - 驗證前 k 個類別準確度:0.0761
<keras.src.callbacks.history.History at 0x7f5b2892d190>
恭喜!您現在知道如何使用 KerasHub 從頭開始訓練強大的圖像分類器。根據您的應用程式標籤資料的可用性,從頭開始訓練可能比使用遷移學習以及上面討論的資料擴增更強大或更弱。對於較小的資料集,預訓練模型通常會產生更高的準確度並更快地收斂。
雖然圖像分類可能是電腦視覺中最簡單的問題,但現代環境有許多複雜的組件。幸運的是,KerasHub 提供了強大、可應用於生產環境的 API,使組裝大多數這些組件成為可能,只需一行程式碼即可。透過使用 KerasHub 的 ImageClassifier
API、預訓練權重和 Keras 的資料擴增,您可以組裝訓練強大分類器所需的一切,只需幾百行程式碼!
作為後續練習,請嘗試在您自己的資料集上微調 KerasHub 分類器!