作者: lukewood, Ian Stenbit, Tirth Patel
创建日期: 2023/04/08
最后修改: 2023/08/10
描述: 使用 KerasCV 训练目标检测模型。
KerasCV 提供了一整套生产级 API 来解决目标检测问题。这些 API 包括特定于目标检测的数据增强技术、Keras 原生 COCO 指标、边界框格式转换工具、可视化工具、预训练的目标检测模型,以及你训练自己最先进的目标检测模型所需的一切!
让我们体验一下 KerasCV 的目标检测 API。
!pip install -q --upgrade keras-cv
!pip install -q --upgrade keras # 升级到 Keras 3.
import os
os.environ["KERAS_BACKEND"] = "jax" # @param ["tensorflow", "jax", "torch"]
from tensorflow import data as tf_data
import tensorflow_datasets as tfds
import keras
import keras_cv
import numpy as np
from keras_cv import bounding_box
import os
from keras_cv import visualization
import tqdm
目标检测是识别、分类和定位给定图像中对象的过程。通常,输入是图像,标签是带有可选类标签的边界框。目标检测可以被视为分类的扩展,然而,不同于图像只需一个类标签,你必须检测和定位任意数量的类。
例如:
上述图像的数据可能看起来像这样:
image = [height, width, 3]
bounding_boxes = {
"classes": [0], # 0 是一个表示“猫”的任意类 ID
"boxes": [[0.25, 0.4, .15, .1]]
# 边界框采用“rel_xywh”格式
# 所以 0.25 代表边界框在图像宽度的 25% 处开始。
# .15 表示宽度为图像宽度的 15%。
}
自从 You Only Look Once(简称 YOLO)问世以来,目标检测主要通过深度学习解决。大多数深度学习架构通过巧妙地将目标检测问题框架化为多个小型分类问题和回归问题的组合来实现这一点。
更具体地说,这是通过在输入图像上生成多种形状和大小的锚框,并为每个框分配类标签,以及 x
、y
、宽度
和 高度
偏移来完成的。模型被训练以预测每个框的类标签,以及预测为对象的每个框的 x
、y
、宽度
和 高度
偏移。
一些示例锚框的可视化:
目标检测是一个技术复杂的问题,但幸运的是,我们提供了一种无懈可击的方法来获得优秀的结果。让我们开始吧!
KerasCV 目标检测 API 中的最高级别 API 是 keras_cv.models
API。此 API 包含完全预训练的目标检测模型,如 keras_cv.models.YOLOV8Detector
。
让我们开始构建一个在 pascalvoc
数据集上预训练的 YOLOV8Detector。
pretrained_model = keras_cv.models.YOLOV8Detector.from_preset(
"yolo_v8_m_pascalvoc", bounding_box_format="xywh"
)
注意 bounding_box_format
参数?
回想一下前面章节中边界框的格式:
bounding_boxes = {
"classes": [num_boxes],
"boxes": [num_boxes, 4]
}
此参数精确描述了标签字典的 "boxes"
字段中值的格式。举例来说,一个 xywh
格式的框,左上角坐标为 (100, 100),宽度为 55,高度为 70,可以表示为:
[100, 100, 55, 75]
或者在 xyxy
格式中等价表示为:
[100, 100, 155, 175]
虽然这看起来简单,但这是 KerasCV 目标检测 API 的关键部分!处理边界框的每个组件都需要一个 bounding_box_format
参数。你可以在 API 文档 中了解更多关于 KerasCV 边界框格式的信息。
这是因为没有一种正确的边界框格式! 不同管道中的组件期望不同的格式,通过要求指定这些格式,我们确保我们的组件保持可读、可重用和清晰。 盒子格式转换错误可能是目标检测管道中最常见的错误类型 - 通过要求这个参数,我们减轻了这些错误(尤其是在将来自多个来源的代码结合时)。
接下来加载一张图片:
filepath = keras.utils.get_file(origin="https://i.imgur.com/gCNcJJI.jpg")
image = keras.utils.load_img(filepath)
image = np.array(image)
visualization.plot_image_gallery(
np.array([image]),
value_range=(0, 255),
rows=1,
cols=1,
scale=5,
)
为了使用带有 ResNet50 主干的 YOLOV8Detector
架构,您需要将图像调整为可以被 64 整除的大小。这是为了确保与 ResNet 中卷积层所做的降采样操作数量的兼容性。
如果调整大小的操作扭曲了输入的长宽比,模型的性能会显著降低。对于我们使用的预训练 "yolo_v8_m_pascalvoc"
预设,最终的 MeanAveragePrecision
在 pascalvoc/2012
评估集上从 0.38
降低到 0.15
,而使用了简单的调整大小操作。
此外,如果您像在分类中那样裁剪以保持长宽比,模型可能会完全缺失某些边界框。因此,在对目标检测模型进行推理时,我们建议使用填充到所需大小,同时将最长的尺寸调整为匹配长宽比。
KerasCV 使得正确调整大小变得简单;只需将 pad_to_aspect_ratio=True
传递给 keras_cv.layers.Resizing
层即可。
这可以用一行代码实现:
inference_resizing = keras_cv.layers.Resizing(
640, 640, pad_to_aspect_ratio=True, bounding_box_format="xywh"
)
这可以作为我们的推理预处理管道使用:
image_batch = inference_resizing([image])
keras_cv.visualization.plot_bounding_box_gallery()
支持 class_mapping
参数,以突出每个框被分配的类。现在我们组装一个类映射。
class_ids = [
"Aeroplane",
"Bicycle",
"Bird",
"Boat",
"Bottle",
"Bus",
"Car",
"Cat",
"Chair",
"Cow",
"Dining Table",
"Dog",
"Horse",
"Motorbike",
"Person",
"Potted Plant",
"Sheep",
"Sofa",
"Train",
"Tvmonitor",
"Total",
]
class_mapping = dict(zip(range(len(class_ids)), class_ids))
就像任何其他 keras.Model
一样,您可以使用 model.predict()
API 来预测边界框。
y_pred = pretrained_model.predict(image_batch)
# y_pred 是一个边界框张量:
# {"classes": ..., boxes": ...}
visualization.plot_bounding_box_gallery(
image_batch,
value_range=(0, 255),
rows=1,
cols=1,
y_pred=y_pred,
scale=5,
font_scale=0.7,
bounding_box_format="xywh",
class_mapping=class_mapping,
)
1/1 ━━━━━━━━━━━━━━━━━━━━ 11s 11s/step
为了支持这个简单直观的推理工作流程,KerasCV 在 YOLOV8Detector
类中执行非极大值抑制。
非极大值抑制是一个传统的计算算法,解决模型为同一对象检测多个框的问题。
非极大值抑制是一个高度可配置的算法,在大多数情况下,您会希望自定义模型的非极大值抑制操作的设置。
这可以通过覆盖 prediction_decoder
参数来完成。
为了展示这个概念,让我们暂时在 YOLOV8Detector 上禁用非极大值抑制。这可以通过写入 prediction_decoder
属性来完成。
# 以下 NonMaxSuppression 层相当于禁用该操作
prediction_decoder = keras_cv.layers.NonMaxSuppression(
bounding_box_format="xywh",
from_logits=True,
iou_threshold=1.0,
confidence_threshold=0.0,
)
pretrained_model = keras_cv.models.YOLOV8Detector.from_preset(
"yolo_v8_m_pascalvoc",
bounding_box_format="xywh",
prediction_decoder=prediction_decoder,
)
y_pred = pretrained_model.predict(image_batch)
visualization.plot_bounding_box_gallery(
image_batch,
value_range=(0, 255),
rows=1,
cols=1,
y_pred=y_pred,
scale=5,
font_scale=0.7,
bounding_box_format="xywh",
class_mapping=class_mapping,
)
1/1 ━━━━━━━━━━━━━━━━━━━━ 5s 5s/step
接下来,让我们为我们的用例重新配置 keras_cv.layers.NonMaxSuppression
!
在这种情况下,我们将 iou_threshold
调整到 0.2
,将
confidence_threshold
调整到 0.7
。
提高 confidence_threshold
将导致模型仅输出具有更高置信度分数的框。iou_threshold
控制两个框必须具有的交并比 (IoU) 的阈值,才能让一个框被修剪掉。
有关这些参数的更多信息,请参见 TensorFlow API 文档
prediction_decoder = keras_cv.layers.NonMaxSuppression(
bounding_box_format="xywh",
from_logits=True,
# 减小所需阈值以使预测被修剪掉
iou_threshold=0.2,
# 调整预测通过 NMS 的置信度阈值
confidence_threshold=0.7,
)
pretrained_model = keras_cv.models.YOLOV8Detector.from_preset(
"yolo_v8_m_pascalvoc",
bounding_box_format="xywh",
prediction_decoder=prediction_decoder,
)
y_pred = pretrained_model.predict(image_batch)
visualization.plot_bounding_box_gallery(
image_batch,
value_range=(0, 255),
rows=1,
cols=1,
y_pred=y_pred,
scale=5,
font_scale=0.7,
bounding_box_format="xywh",
class_mapping=class_mapping,
)
1/1 ━━━━━━━━━━━━━━━━━━━━ 5s 5s/step
这看起来好多了!
无论您是目标检测的小白还是经验丰富的老手,从头开始组装目标检测管道是一项巨大的工作。
幸运的是,所有 KerasCV 目标检测 API 都是作为模块化组件构建的。
无论您需要完整的管道、一个目标检测模型,还是仅仅需要一个转换工具将您的框从 xywh
格式转换为 xyxy
,KerasCV 都能满足您的需求。
在本指南中,我们将组装 KerasCV 目标检测模型的完整训练管道。 这包括数据加载、增强、评估指标和推断!
要开始,让我们整理所有的导入并定义全局配置参数。
BATCH_SIZE = 4
要开始,让我们先讨论数据加载和边界框格式。 KerasCV 有一个预定义的边界框格式。 为了遵循这一点,您应该将边界框打包到一个字典中,符合以下规范:
bounding_boxes = {
# num_boxes 可能是一个不规则维度
'boxes': Tensor(shape=[batch, num_boxes, 4]),
'classes': Tensor(shape=[batch, num_boxes])
}
bounding_boxes['boxes']
包含您的边界框在 KerasCV 支持的 bounding_box_format
中的坐标。
KerasCV 要求在所有处理边界框的组件中都需要 bounding_box_format
参数。
这样做是为了最大化您将单个组件插入目标检测管道的能力,同时使代码在目标检测管道中自我文档化。
为了与 KerasCV API 风格相匹配,建议在编写自定义数据加载器时,也支持 bounding_box_format
参数。
这使得调用您的数据加载器的人清楚地知道边界框的格式是什么。
在本示例中,我们将我们的框格式化为 xywh
格式。
例如:
train_ds, ds_info = your_data_loader.load(
split='train', bounding_box_format='xywh', batch_size=8
)
这清楚地输出以 xywh
格式的边界框。您可以在 API 文档中阅读有关 KerasCV 边界框格式的更多信息 在此处。
我们的数据以 {"images": images, "bounding_boxes": bounding_boxes}
格式加载。 该格式在所有 KerasCV 预处理组件中都得到支持。
让我们加载一些数据,验证数据是否如我们所料。
def visualize_dataset(inputs, value_range, rows, cols, bounding_box_format):
inputs = next(iter(inputs.take(1)))
images, bounding_boxes = inputs["images"], inputs["bounding_boxes"]
visualization.plot_bounding_box_gallery(
images,
value_range=value_range,
rows=rows,
cols=cols,
y_true=bounding_boxes,
scale=5,
font_scale=0.7,
bounding_box_format=bounding_box_format,
class_mapping=class_mapping,
)
def unpackage_raw_tfds_inputs(inputs, bounding_box_format):
image = inputs["image"]
boxes = keras_cv.bounding_box.convert_format(
inputs["objects"]["bbox"],
images=image,
source="rel_yxyx",
target=bounding_box_format,
)
bounding_boxes = {
"classes": inputs["objects"]["label"],
"boxes": boxes,
}
return {"images": image, "bounding_boxes": bounding_boxes}
def load_pascal_voc(split, dataset, bounding_box_format):
ds = tfds.load(dataset, split=split, with_info=False, shuffle_files=True)
ds = ds.map(
lambda x: unpackage_raw_tfds_inputs(x, bounding_box_format=bounding_box_format),
num_parallel_calls=tf_data.AUTOTUNE,
)
return ds
train_ds = load_pascal_voc(
split="train", dataset="voc/2007", bounding_box_format="xywh"
)
eval_ds = load_pascal_voc(split="test", dataset="voc/2007", bounding_box_format="xywh")
train_ds = train_ds.shuffle(BATCH_SIZE * 4)
接下来,让我们对数据进行批处理。
在KerasCV的目标检测任务中,建议用户使用不规则的输入批次。这是因为PascalVOC中的图像大小可能不同,以及每个图像中可能有不同数量的边界框。
要在tf.data
流水线中构建不规则数据集,可以使用ragged_batch()
方法。
train_ds = train_ds.ragged_batch(BATCH_SIZE, drop_remainder=True)
eval_ds = eval_ds.ragged_batch(BATCH_SIZE, drop_remainder=True)
让我们确保我们的数据集遵循KerasCV所期望的格式。通过使用visualize_dataset()
函数,您可以直观地验证您的数据是否符合KerasCV所期望的格式。如果边界框不可见或在错误的位置可见,这表明您的数据格式不正确。
visualize_dataset(
train_ds, bounding_box_format="xywh", value_range=(0, 255), rows=2, cols=2
)
对于评估集:
visualize_dataset(
eval_ds,
bounding_box_format="xywh",
value_range=(0, 255),
rows=2,
cols=2,
# 如果您不是在本地机器上运行实验,您也可以
# 通过`path`使`visualize_dataset()`将图形输出到文件:
# path="eval.png"
)
看起来一切结构都如预期。现在我们可以继续构建我们的数据增强流水线。
构建目标检测管道时,最具挑战性的任务之一是数据增强。图像增强技术必须考虑底层边界框,并相应地更新它们。
幸运的是,KerasCV本地支持边界框增强,其丰富的数据增强层库。以下代码加载Pascal VOC数据集,并在tf.data
流水线中动态执行支持边界框的数据增强。
augmenters = [
keras_cv.layers.RandomFlip(mode="horizontal", bounding_box_format="xywh"),
keras_cv.layers.JitteredResize(
target_size=(640, 640), scale_factor=(0.75, 1.3), bounding_box_format="xywh"
),
]
def create_augmenter_fn(augmenters):
def augmenter_fn(inputs):
for augmenter in augmenters:
inputs = augmenter(inputs)
return inputs
return augmenter_fn
augmenter_fn = create_augmenter_fn(augmenters)
train_ds = train_ds.map(augmenter_fn, num_parallel_calls=tf_data.AUTOTUNE)
visualize_dataset(
train_ds, bounding_box_format="xywh", value_range=(0, 255), rows=2, cols=2
)
太好了!我们现在拥有了一个支持边界框的数据增强流水线。让我们格式化我们的评估数据集以匹配。我们将使用确定性的keras_cv.layers.Resizing()
层,而不是使用JitteredResize
。
inference_resizing = keras_cv.layers.Resizing(
640, 640, bounding_box_format="xywh", pad_to_aspect_ratio=True
)
eval_ds = eval_ds.map(inference_resizing, num_parallel_calls=tf_data.AUTOTUNE)
由于训练数据集使用JitteredResize()
缩放图像,而推断数据集使用layers.Resizing(pad_to_aspect_ratio=True)
,因此可视化这两个数据集是一个好习惯:
visualize_dataset(
eval_ds, bounding_box_format="xywh", value_range=(0, 255), rows=2, cols=2
)
最后,让我们从预处理字典中解包我们的输入,并准备将输入馈送到我们的模型中。为了兼容TPU,边界框张量需要是Dense
而不是Ragged
。
def dict_to_tuple(inputs):
return inputs["images"], bounding_box.to_dense(
inputs["bounding_boxes"], max_boxes=32
)
train_ds = train_ds.map(dict_to_tuple, num_parallel_calls=tf_data.AUTOTUNE)
eval_ds = eval_ds.map(dict_to_tuple, num_parallel_calls=tf_data.AUTOTUNE)
train_ds = train_ds.prefetch(tf_data.AUTOTUNE)
eval_ds = eval_ds.prefetch(tf_data.AUTOTUNE)
在本指南中,我们使用标准的SGD优化器,并依赖于[keras.callbacks.ReduceLROnPlateau
](/api/callbacks/reduce_lr_on_plateau#reducelronplateau-class)回调来降低学习率。
在训练目标检测模型时,您始终希望包含一个global_clipnorm
。这是为了解决在训练目标检测模型时经常出现的梯度爆炸问题。
base_lr = 0.005
# 在目标检测任务中,包括 global_clipnorm 非常重要
optimizer = keras.optimizers.SGD(
learning_rate=base_lr, momentum=0.9, global_clipnorm=10.0
)
为了在您的数据集上取得最佳结果,您可能希望手动制作一个PiecewiseConstantDecay
学习率调度。
虽然PiecewiseConstantDecay
调度通常表现更好,但它们无法在不同问题之间转换。
您可能不熟悉"ciou"
损失。尽管在其他模型中不常见,但这种损失有时在目标检测领域使用。
简而言之,"完全 IoU"是一种交并比损失的变体,由于其收敛特性而被使用。
在KerasCV中,您只需将字符串"ciou"
传递给compile()
即可使用此损失。
我们还对类别头使用标准的二元交叉熵损失。
pretrained_model.compile(
classification_loss="binary_crossentropy",
box_loss="ciou",
)
最流行的目标检测指标是COCO指标,这些指标是在MSCOCO数据集发布时发布的。KerasCV提供了一套易于使用的COCO指标,位于keras_cv.callbacks.PyCOCOCallback
符号下。请注意,我们使用Keras回调而不是Keras指标来计算COCO指标。这是因为计算COCO指标需要将模型对整个评估数据集的所有预测存储在内存中,这在训练期间执行是不切实际的。
coco_metrics_callback = keras_cv.callbacks.PyCOCOCallback(
eval_ds.take(20), bounding_box_format="xywh"
)
我们的数据管道现在已完成! 我们可以继续进行模型创建和训练。
接下来,让我们使用KerasCV API构建一个未经训练的YOLOV8Detector模型。 在本教程中,我们使用来自imagenet数据集的预训练ResNet50主干。
KerasCV使构建具有任何KerasCV主干的YOLOV8Detector
变得轻而易举。只需使用您想要的架构之一的预设即可!
例如:
model = keras_cv.models.YOLOV8Detector.from_preset(
"resnet50_imagenet",
# 有关支持的边界框格式的更多信息,请访问
# https://keras.io/api/keras_cv/bounding_box/
bounding_box_format="xywh",
num_classes=20,
)
这就是构建KerasCV YOLOv8所需的一切。YOLOv8接受密集图像张量和边界框字典的元组以进行fit()
和train_on_batch()
这与我们在输入管道中构建的内容相匹配。
剩下的就是训练我们的模型。KerasCV目标检测模型遵循标准的Keras工作流,利用compile()
和fit()
。
让我们编译我们的模型:
model.compile(
classification_loss="binary_crossentropy",
box_loss="ciou",
optimizer=optimizer,
)
如果您想完全训练模型,请从所有数据集引用中删除.take(20)
(下面和指标回调的初始化中)。
model.fit(
train_ds.take(20),
# 运行10-35个周期以获得良好得分。
epochs=1,
callbacks=[coco_metrics_callback],
)
20/20 ━━━━━━━━━━━━━━━━━━━━ 7s 59ms/step
创建索引...
索引已创建!
创建索引...
索引已创建!
正在进行每张图像评估...
评估注释类型 *bbox*
完成 (t=0.16s)。
累积评估结果...
完成 (t=0.07s)。
平均精度 (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.000
平均精度 (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.000
平均精度 (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.000
平均精度 (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.000
平均精度 (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.000
平均精度 (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.000
平均召回率 (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.002
平均召回率 (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.002
平均召回率 (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.002
平均召回率 (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.000
平均召回率 (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.000
平均召回率 (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.002
20/20 ━━━━━━━━━━━━━━━━━━━━ 73s 681ms/step - loss: 9221.7988 - val_AP: 3.1673e-05 - val_AP50: 2.1886e-04 - val_AP75: 0.0000e+00 - val_APs: 0.0000e+00 - val_APm: 0.0000e+00 - val_APl: 3.1673e-05 - val_ARmax1: 0.0016 - val_ARmax10: 0.0021 - val_ARmax100: 0.0021 - val_ARs: 0.0000e+00 - val_ARm: 0.0000e+00 - val_ARl: 0.0021
<keras.src.callbacks.history.History at 0x7fb23010a850>
KerasCV使目标检测推断变得简单。 model.predict(images)
返回一个边界框张量。默认情况下,YOLOV8Detector.predict()
将为您执行非最大抑制操作。
在本节中,我们将使用提供的keras_cv
预设:
model = keras_cv.models.YOLOV8Detector.from_preset(
"yolo_v8_m_pascalvoc", bounding_box_format="xywh"
)
接下来,为了方便,我们构建一个更大的批次数据集:
visualization_ds = eval_ds.unbatch()
visualization_ds = visualization_ds.ragged_batch(16)
visualization_ds = visualization_ds.shuffle(8)
让我们创建一个简单的函数来绘制我们的推断:
def visualize_detections(model, dataset, bounding_box_format):
images, y_true = next(iter(dataset.take(1)))
y_pred = model.predict(images)
visualization.plot_bounding_box_gallery(
images,
value_range=(0, 255),
bounding_box_format=bounding_box_format,
y_true=y_true,
y_pred=y_pred,
scale=4,
rows=2,
cols=2,
show=True,
font_scale=0.7,
class_mapping=class_mapping,
)
您可能需要配置您的非极大值抑制操作,以获得 视觉上吸引人的结果。
model.prediction_decoder = keras_cv.layers.NonMaxSuppression(
bounding_box_format="xywh",
from_logits=True,
iou_threshold=0.5,
confidence_threshold=0.75,
)
visualize_detections(model, dataset=visualization_ds, bounding_box_format="xywh")
1/1 ━━━━━━━━━━━━━━━━━━━━ 16s 16s/step
太棒了!
最后一个值得注意的模式是在 keras.callbacks.Callback
中可视化
检测,以监控训练:
class VisualizeDetections(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
visualize_detections(
self.model, bounding_box_format="xywh", dataset=visualization_ds
)
KerasCV 使得构建最先进的物体检测流程变得简单。 在本指南中,我们首先通过 KerasCV 边界框规范编写了数据加载器。 接下来,我们使用 KerasCV 预处理层组装了生产级的数据增强管道,代码少于 50 行。
KerasCV 物体检测组件可以独立使用,但也有深度 相互集成。 KerasCV 使得编写生产级边界框增强、 模型训练、可视化和 指标评估变得简单。
一些后续练习供读者参考:
最后一个有趣的代码片段,以展示 KerasCV API 的强大功能!
stable_diffusion = keras_cv.models.StableDiffusionV2(512, 512)
images = stable_diffusion.text_to_image(
prompt="一张远距离拍摄的酷猫的照片。 猫站在一个美丽的森林里",
negative_prompt="不现实的,难看的,畸形的",
batch_size=4,
seed=1231,
)
encoded_predictions = model(images)
y_pred = model.decode_predictions(encoded_predictions, images)
visualization.plot_bounding_box_gallery(
images,
value_range=(0, 255),
y_pred=y_pred,
rows=2,
cols=2,
scale=5,
font_scale=0.7,
bounding_box_format="xywh",
class_mapping=class_mapping,
)
通过使用此模型检查点,您确认其使用受到 CreativeML Open RAIL++-M 许可证条款的约束,网址为 https://github.com/Stability-AI/stablediffusion/blob/main/LICENSE-MODEL
50/50 ━━━━━━━━━━━━━━━━━━━━ 47s 356ms/step