Press "Enter" to skip to content

使用RetinaNet和KerasCV进行目标检测

使用KerasCV库的强大和简洁进行物体检测。

植物上的叶子图像。由DALL·E 2创建。

目录

  1. 等等,什么是KerasCV?
  2. 检查数据
  3. 图像预处理
  4. RetinaNet模型背景
  5. 训练RetinaNet
  6. 进行预测
  7. 结论
  8. 参考文献

相关链接

  • 工作中的Kaggle笔记本:随意复制笔记本,尝试代码,使用免费GPU。
  • PlantDoc数据集:这是此笔记本中使用的数据集,托管在Roboflow上。该数据集使用CC BY 4.0 DEED许可证发布,这意味着您可以以任何VoAGI或格式复制和重新分发材料,甚至用于商业目的。

等等,什么是KerasCV?

在完成基于图像分割的迷你项目后(见此处),我准备进入计算机视觉领域的另一个常见任务:物体检测。物体检测指的是接收图像并在感兴趣的对象周围生成边界框,并对所包含的对象进行分类。举个简单的例子,请看下面的图像:

物体检测的示例。请注意边界框和类别标签。作者提供的图像。

蓝色框被称为边界框,并且类别名直接放在其上方。因此,物体检测可以分解为两个子问题:

  1. 回归问题,模型必须预测边界框的左上角和右下角的x和y坐标。
  2. 分类问题,模型必须预测边界框观察到的对象的类别。

在这个例子中,边界框是由人类创建并标记的。我们希望自动化这个过程,一个经过良好训练的物体检测模型可以做到这一点。

我坐下来回顾了关于物体检测的学习材料,结果感到失望。不幸的是,大多数入门材料几乎不提到物体检测。 François Chollet在《Deep Learning with Python》[1]中提到:

请注意,我们不会涵盖目标检测,因为这对于一本入门书来说太专业化和太复杂了。

Aurélion Géron [2] 提供了许多关于目标检测背后思想的文本内容,但只提供了几行代码来处理带有虚拟边界框的目标检测任务,远远达不到我所寻找的端到端流程。Andrew Ng 的 [3] 著名的深度学习专业课程在目标检测方面深入探讨,但即使他在编码实验室中加载了一个预训练的目标检测模型并进行推理,也只是到此为止。

为了更深入地研究,我开始勾勒出一个目标检测流程的大纲。仅针对 RetinaNet 模型的预处理,一个人需要进行以下操作(注意:其他目标检测模型如 YOLO 需要不同的步骤):

  • 将输入图像调整为相同大小,并进行填充以防止纵横比混乱。哦,不要忘记边界框;这些也需要适当地改变形状,否则会破坏数据。
  • 根据训练集中的 ground truth 边界框,在不同尺度和纵横比上生成锚框。这些锚框在训练过程中作为模型的参考点。
  • 根据锚框与 ground truth 边界框的重叠情况,为锚框分配标签。重叠程度较高的锚框被标记为正样本,而重叠程度较低的被标记为负样本。
  • 有多种方法可以描述相同的边界框。您需要实现用于在这些不同格式之间转换的函数。稍后会有更多介绍。
  • 实现数据增强,要注意不仅要增强图像,还要增强边界框。理论上可以省略此步骤,但实践中这是必要的,可以帮助我们的模型很好地泛化。

点击查看Keras网站上的一个示例令人震惊。我们的模型预测的后处理工作将需要更多的工作。引用Keras团队的话来说:这是一个技术复杂的问题。

当我开始绝望地浏览互联网时,偶然发现了一个我以前从未听说过的库: KerasCV。当我阅读文档时,我开始逐渐意识到这是TensorFlow/Keras中计算机视觉的未来。从他们的介绍开始:

KerasCV可以被理解为Keras API的水平扩展:这些组件是新的一方库Keras对象,过于专业化无法添加到核心Keras中。它们接收与核心Keras API 相同的精细度和向后兼容性保证,并由Keras团队进行维护。

“但为什么我的学习资料中没有提到这个?”我想知道。答案很简单:这是一个相当新的库。GitHub上的第一个提交是在2022年4月13日,太新了,甚至在我课本的最新版本中都看不到。事实上,该库的1.0版本甚至还没有发布(截至2023年11月10日,版本为0.6.4)。我预计KerasCV将在我课本和在线课程的下一版中详细讨论(公平地说,Gèron 简要提到了“新的Keras NLP项目”和Keras CV项目,读者可能会感兴趣)。

KerasCV如此新,除了Keras团队自己发布的外,几乎没有多少教程(点击此处查看)。在本教程中,我将演示一个端到端的目标检测流程,使用与官方Keras指南类似但又不同的技术来识别健康和病叶。使用KerasCV,即使是初学者也可以利用标记的数据集构建有效的目标检测流程。

开始之前有几个注意事项。KerasCV是一个快速变化的库,代码库和文档定期更新。本教程中展示的实现将与KerasCV版本0.6.4 兼容。Keras团队已经声明:“在KerasCV达到 v1.0.0 之前,不存在向后兼容性约定。”这意味着本教程中使用的方法在KerasCV更新时有可能不再起作用。我在链接的Kaggle笔记本中将KerasCV版本号硬编码,以防止此类问题发生。

KerasCV存在一些已在GitHub的问题标签中记录的错误。此外,文档在某些领域不足(看看你,MultiClassNonMaxSuppression)。当您使用KerasCV时,不要因这些问题而气馁。事实上,这是成为KerasCV代码库贡献者的绝佳机会!

本教程将侧重于KerasCV的实现细节。我将简要介绍一些目标检测的高级概念,但我会假设读者对RetinaNet架构等概念具有一定的背景知识。这里显示的代码已被编辑和重新排列以提高清晰度,请参阅上述链接的Kaggle笔记本以查看完整代码。

最后,关于安全的注意事项。在这里创建的模型不是为了达到最先进的水平;将其视为一个高级教程。在将此植物病害检测模型投入生产之前,预计还需要进一步的微调和数据清理。最好通过专家的意见来确认模型的预测诊断。

检查数据

PlantDoc数据集包含13种植物物种和30类的2,569张图像。该数据集的目标在《PlantDoc:一种用于植物病害检测的视觉数据集》一文中由Singh等人提出。

由于植物疾病,印度每年的作物产量损失了35%。由于缺乏实验室基础设施和专业知识,早期检测植物病害仍然困难。在本文中,我们探索了使用计算机视觉方法进行可扩展和早期的植物病害检测的可能性。

这是一个高尚的目标,也是计算机视觉可以为农民做出很多贡献的领域。

Roboflow允许我们以多种不同的格式下载数据集。由于我们使用的是TensorFlow,让我们将数据集下载为TFRecord格式。TFRecord是TensorFlow中特定的格式,旨在高效存储大量数据。数据由一系列记录表示,其中每个记录都是键值对。每个键都被称为特征。下载的压缩文件包含四个文件,两个用于训练,两个用于验证:

  • leaves_label_map.pbtxt:这是一种Protocol Buffers文本格式文件,用于描述数据的结构。在文本编辑器中打开该文件,我看到有30个类别。有一些健康叶子,如Apple leaf,还有一些不健康的叶子,如Apple Scab Leaf
  • leaves.tfrecord:这是包含所有数据的TFRecord文件。

我们的第一步是检查leaves.tfrecord。我们的记录包含哪些特征?不幸的是,Roboflow没有指定。

train_tfrecord_file = '/kaggle/input/plants-dataset/leaves.tfrecord'val_tfrecord_file = '/kaggle/input/plants-dataset/test_leaves.tfrecord'# 创建一个TFRecordDatasettrain_dataset = tf.data.TFRecordDataset([train_tfrecord_file])val_dataset = tf.data.TFRecordDataset([val_tfrecord_file])# 遍历几个条目并打印其内容。取消注释以查看原始数据for record in train_dataset.take(1):  example = tf.train.Example()  example.ParseFromString(record.numpy())  print(example)

我看到打印了以下特征:

  • image/encoded:这是图像的编码二进制表示。在这个数据集的情况下,图像以jpeg格式编码。
  • image/height:这是每个图像的高度。
  • image/width:这是每个图像的宽度。
  • image/object/bbox/xmin:这是边界框左上角的x坐标。
  • image/object/bbox/xmax:这是边界框右下角的x坐标。
  • image/object/bbox/ymin:这是边界框左上角的y坐标。
  • image/object/bbox/ymax:这是边界框右下角的y坐标。
  • image/object/class/label:这些是与每个边界框相关联的标签。

现在,我们想要将所有的图像和关联的边界框放在 TensorFlow 的数据集对象中。数据集对象允许您存储大量的数据,而不会超出系统的内存。这是通过诸如惰性加载批处理等功能实现的。惰性加载表示数据在不显式请求时不会加载到内存中(例如在执行转换或训练过程中)。批处理意味着一次只会加载选择的一部分图像(通常为8、16、32等)。简而言之,我建议总是将数据转换为数据集对象,特别是当您处理大量数据时(在对象检测中很常见)。

要在 TensorFlow 中将 TFRecord 转换为数据集对象,可以使用 tf.data.TFRecordDataset 类从我们的 TFRecord 文件创建数据集,然后使用 map 方法应用解析函数,以提取和预处理特征。解析代码如下所示。

def parse_tfrecord_fn(example):    feature_description = {        'image/encoded': tf.io.FixedLenFeature([], tf.string),        'image/height': tf.io.FixedLenFeature([], tf.int64),        'image/width': tf.io.FixedLenFeature([], tf.int64),        'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32),        'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32),        'image/object/class/label': tf.io.VarLenFeature(tf.int64),    }        parsed_example = tf.io.parse_single_example(example, feature_description)    # 解码 JPEG 图像并将像素值归一化为 [0, 1] 范围。    img = tf.image.decode_jpeg(parsed_example['image/encoded'], channels=3) # 以 uint8 的形式返回    # 将像素值归一化为 [0, 256]    img = tf.image.convert_image_dtype(img, tf.uint8)    # 获取边界框坐标和类标签。    xmin = tf.sparse.to_dense(parsed_example['image/object/bbox/xmin'])    xmax = tf.sparse.to_dense(parsed_example['image/object/bbox/xmax'])    ymin = tf.sparse.to_dense(parsed_example['image/object/bbox/ymin'])    ymax = tf.sparse.to_dense(parsed_example['image/object/bbox/ymax'])    labels = tf.sparse.to_dense(parsed_example['image/object/class/label'])    # 堆叠边界框坐标以创建 [num_boxes, 4] 张量。    rel_boxes = tf.stack([xmin, ymin, xmax, ymax], axis=-1)    boxes = keras_cv.bounding_box.convert_format(rel_boxes, source='rel_xyxy', target='xyxy', images=img)    # 创建最终的字典。    image_dataset = {        'images': img,        'bounding_boxes': {            'classes': labels,            'boxes': boxes        }    }    return image_dataset

让我们来分解一下:

  • feature_description:这是描述每个特征预期格式的字典。当数据集中所有示例的特征长度都是固定的时候,我们使用 tf.io.FixedLenFeature,当预计长度存在一定的变化时,我们使用 tf.io.VarLenFeature。由于我们的数据集中边界框的数量是不固定的(有些图像有更多的边界框,有些图像则没有),因此我们在与边界框相关的任何内容上使用了 tf.io.VarLenFeature
  • 我们使用 tf.image.decode_jpeg 解码图像文件,因为我们的图像是以 JPEG 格式编码的。
  • 请注意,使用 tf.sparse.to_dense 用于边界框坐标和标签。当我们使用 tf.io.VarLenFeature 时,返回的信息将作为稀疏矩阵。稀疏矩阵是大部分元素为零的矩阵,它的数据结构高效地存储了只有非零值及其索引的值。不幸的是,TensorFlow 中的许多预处理函数需要密集矩阵,包括我们使用的 tf.stack,它用于水平堆叠来自多个边界框的信息。为了解决这个问题,我们使用 tf.sparse.to_dense 将稀疏矩阵转换为密集矩阵。
  • 在堆叠边界框后,我们使用了 KerasCV 的 keras_cv.bounding_box.convert_format 函数。检查数据时,我注意到边界框坐标被规范化为 0 和 1 之间。这意味着这些数字表示图像总宽度/高度的百分比。例如,0.5 表示 50% * 图像宽度。这是一种相对格式,Keras 将其称为 REL_XYXY,而不是 绝对格式 XYXY。理论上讲,转换为绝对格式并不是必要的,但是当使用相对坐标训练模型时,我遇到了一些错误。请参阅KerasCV 文档了解一些其他支持的边界框格式。
  • 最后,我们将图像和边界框转换为 KerasCV 希望的格式:字典。Python 字典是一个包含键-值对的数据类型。具体而言,KerasCV 期望以下格式:
image_dataset = {  "images": [width, height, channels],  bounding_boxes = {    "classes": [num_boxes],    "boxes": [num_boxes, 4]  }}

这实际上是一个“字典中的字典”,因为bounding_boxes也是一个字典。

最后使用.map函数将解析函数应用于我们的TFRecord。然后你可以检查数据集对象。一切都很正常。

train_dataset = train_dataset.map(parse_tfrecord_fn)val_dataset = val_dataset.map(parse_tfrecord_fn)# 检查数据for data in train_dataset.take(1):    print(data)

恭喜,最难的部分已经完成了。创建KerasCV想要的“字典中的字典”是我认为最困难的任务。其余部分更加直接。

图像预处理

我们的数据已经分成了训练集和验证集。所以我们将开始对我们的数据集进行分批处理。

# 批处理BATCH_SIZE = 32# 添加自动调整以进行预获取AUTOTUNE = tf.data.experimental.AUTOTUNEtrain_dataset = train_dataset.ragged_batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)val_dataset = val_dataset.ragged_batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)NUM_ROWS = 4NUM_COLS = 8IMG_SIZE = 416BBOX_FORMAT = "xyxy"

一些说明:

  • 我们使用ragged_batch的原因和使用VarLenFeature相同:我们无法提前知道每个图像将有多少个边界框。如果所有图像具有相同数量的边界框,那么我们将使用batch
  • 我们设置BBOX_FORMAT =“xyxy”。回想一下,在加载数据时,我们将边界框格式从相对的XYXY格式转换为绝对的XYXY格式。

现在我们可以实现数据增强。数据增强是计算机视觉问题中常用的技术。它稍微修改训练图像,例如轻微旋转、水平翻转图像等等。这有助于解决数据过少的问题,也有助于正则化。在这里,我们将引入以下增强技术:

  • KerasCV的JitteredResize函数。该函数适用于目标检测流水线,并实施一种图像增强技术,其中随机缩放、调整大小、裁剪和填充图像以及相应的边界框。这个过程引入了尺度和局部特征的变异性,增强了训练数据的多样性,从而提高了泛化能力。
  • 然后添加水平和垂直的RandomFlips以及一个RandomRotation。这里factor是一个表示2π的分数的浮点数。我们使用0.25,这意味着我们的增强器将通过一些数字在-π的25%到π的25%之间对图像进行旋转。以角度来表示,这意味着在-45°到45°之间旋转。
  • 最后我们添加RandomSaturationRandomHue。饱和度为0.0的图像将成为灰度图像,而1.0将完全饱和。0.5的因子不会引入变化,因此选择0.4-0.6的范围会产生一个微妙的变化。色调因子为0.0不会引起变化。设置factor=0.2意味着范围为0.0-0.2,产生另一种微妙的变化。
augmenter = keras.Sequential(    [        keras_cv.layers.JitteredResize(            target_size=(IMG_SIZE, IMG_SIZE), scale_factor=(0.8, 1.25), bounding_box_format=BBOX_FORMAT        ),        keras_cv.layers.RandomFlip(mode="horizontal_and_vertical", bounding_box_format=BBOX_FORMAT),        keras_cv.layers.RandomRotation(factor=0.25, bounding_box_format=BBOX_FORMAT),        keras_cv.layers.RandomSaturation(factor=(0.4, 0.6)),        keras_cv.layers.RandomHue(factor=0.2, value_range=[0,255])    ])train_dataset = train_dataset.map(augmenter, num_parallel_calls=tf.data.AUTOTUNE)

通常我们只会增加训练集,因为我们希望模型避免“记忆”模式,而是确保模型学习到真实世界中的通用模式。这样可以增加训练过程中模型所见样本的多样性。

我们还希望将验证集中的图像调整为相同大小(包括填充)。这些图像将被调整大小而不会发生任何形变。边界框也必须相应地调整形状。KerasCV可以轻松处理这个困难的任务:

# 调整大小并填充图像inference_resizing = keras_cv.layers.Resizing(    IMG_SIZE, IMG_SIZE, pad_to_aspect_ratio=True, bounding_box_format=BBOX_FORMAT)val_dataset = val_dataset.map(inference_resizing, num_parallel_calls=tf.data.AUTOTUNE)

最后,我们可以使用包含预处理的图像和边界框进行可视化:

class_mapping = {    1: '苹果疮叶',    2: '苹果叶',    3: '苹果锈叶',    4: '甜椒叶',    5: '甜椒叶斑病',    6: '蓝莓叶',    7: '樱桃叶',    8: '玉米灰斑叶',    9: '玉米叶斑病',    10: '玉米锈叶',    11: '桃叶',    12: '土豆叶',    13: '土豆早疫叶',    14: '土豆晚疫叶',    15: '树莓叶',    16: '大豆叶',    17: '豆叶',    18: '南瓜白粉病叶',    19: '草莓叶',    20: '番茄早疫病叶',    21: '番茄斑点病叶',    22: '番茄叶',    23: '番茄细菌性斑点病叶',    24: '番茄晚疫病叶',    25: '番茄花叶病毒叶',    26: '番茄黄化病毒叶',    27: '番茄霉病叶',    28: '番茄两点蜘蛛螨叶',    29: '葡萄叶',    30: '葡萄黑腐叶'}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,    )# 可视化训练集visualize_dataset(    train_dataset, bounding_box_format=BBOX_FORMAT, value_range=(0, 255), rows=NUM_ROWS, cols=NUM_COLS)# 可视化验证集visualize_dataset(    val_dataset, bounding_box_format=BBOX_FORMAT, value_range=(0, 255), rows=NUM_ROWS, cols=NUM_COLS)

这种类型的可视化函数在KerasCV中很常见。它以参数中指定的行和列的网格中绘制图像和边界框。我们可以看到我们的训练图像已经被轻微旋转,有些水平或垂直翻转,它们可能已经被放大或缩小,并且可以看到色调/饱和度的细微变化。在KerasCV的所有数据增强层中,边界框也会在必要时进行增强。请注意,class_mapping是一个简单的字典。我从之前提到的leaves_label_map.pbtxt文本文件中获取了键和标签。

左侧为原始图像示例(验证集),右侧为增强后的图像示例(训练集)。图片由作者提供。

查看RetinaNet模型之前的最后一件事。之前我们需要创建“字典中的字典”以将数据格式转换为与KerasCV预处理兼容的格式,但现在我们需要将其转换为一组数字的元组,以供我们的模型进行训练。这个过程相当简单:

def dict_to_tuple(inputs):    return inputs["images"], bounding_box.to_dense(        inputs["bounding_boxes"], max_boxes=32    )train_dataset = train_dataset.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)validation_dataset = val_dataset.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)

RetinaNet模型背景

进行对象检测的一种常用模型称为RetinaNet。模型的详细描述超出了本文的范围。简而言之,RetinaNet是一个单阶段的检测器,意味着它只在预测边界框和类别之前查看图像一次。这类似于著名的YOLO(You Only Look Once)模型,但有一些重要的区别。我想在这里强调的是使用的新型分类损失函数: 焦点损失。它解决了图像中的类别不平衡问题。

为了理解为什么这一点很重要,考虑以下类比:想象一下你是一个教室里的100名学生中的一名老师。95名学生大声而喧闹,总是在喊叫和举手。5名学生安静并且不说太多。作为教师,你需要平等地关注每个学生,但是大声的学生挤走了安静的学生。在这里,你面临了一个类别不平衡的问题。为了解决这个问题,你开发了一种特殊的助听器,增强了安静的学生,减弱了大声的学生。在这个类比中,大声的学生是我们的图像中绝大多数不含叶子的背景像素,而安静的学生是那些含有叶子的小区域。助听器就是焦点损失,它使我们的模型集中在那些含有叶子的像素上,而不需要过多关注那些没有叶子的像素。

RetinaNet模型有三个重要组成部分:

  • 主干网络。这是模型的基础。我们也称之为 特征提取器。顾名思义,它接收图像并扫描特征。低层级图层提取低级特征(例如线条和曲线),而高层级图层提取高级特征(例如嘴唇和眼睛)。在这个项目中,主干网络将是在COCO数据集上预训练的YOLOv8模型。我们仅使用YOLO作为特征提取器,而不是作为对象检测器。
  • 特征金字塔网络(FPN)。这是一种生成不同尺度特征图金字塔以检测不同尺寸对象的模型架构。它通过通过自顶向下的路径和横向连接将低分辨率、语义强的特征与高分辨率、语义弱的特征相结合来实现。查看这个视频以获得详细解释,或者查看引入FPN的论文 [5]。
  • 两个任务特定的子网络。这些子网络对金字塔的每个级别进行对象检测。其中一个子网络用于识别类别(分类),另一个子网络用于识别边界框(回归)。这些子网络是未经训练的。
简化的RetinaNet架构。图片由作者提供。

我们之前将图像调整为416×416的大小。这在某种程度上是一个任意的选择,尽管你选择的对象检测模型通常会指定所需的最小尺寸。对于我们使用的YOLOv8主干网络,图像尺寸应该是32的倍数。这是因为主干网络的最大步幅为32,而它是一个完全卷积网络。对于你自己的项目,应该对你使用的任何模型进行调查,以找出这个因素。

训练RetinaNet

让我们先设置一些基本参数,比如优化器和我们将使用的指标。这里我们将使用Adam作为优化器。请注意global_clip_norm参数。根据KerasCV目标检测指南

在训练目标检测模型时,您总是希望包含一个global_clipnorm。这是为了解决训练目标检测模型时经常出现的梯度爆炸问题。

base_lr = 0.0001#在目标检测任务中包含global_clipnorm非常重要optimizer_Adam = tf.keras.optimizers.Adam(    learning_rate=base_lr,    global_clipnorm=10.0)

我们将遵循他们的建议。对于指标,我们将使用BoxCOCOMetrics。这些是用于目标检测的流行指标。它们主要由平均精度(mAP)平均召回率(mAR)组成。总而言之,mAP通过测量模型预测的正确目标检测面积与模型预测总面积之比来量化模型定位和标识对象的效果。mAR是一个不同的评分,它通过计算正确识别的对象面积与实际对象面积之比的平均值来评估模型捕捉对象完整范围的能力。有关指标的详细信息,请参阅这篇文章。关于精确度和召回率的基础知识,请参阅这个视频

coco_metrics = keras_cv.metrics.BoxCOCOMetrics(    bounding_box_format=BBOX_FORMAT, evaluate_freq=5)

因为计算框指标的计算成本很高,我们传递了evaluate_freq=5参数,告诉模型在每五个批次之后计算指标,而不是在每个单独的批次中计算。我注意到如果这个数字太大,验证指标根本不会被打印出来。

让我们继续看一下我们将要使用的回调函数:

class VisualizeDetections(keras.callbacks.Callback):    def on_epoch_end(self, epoch, logs):        if (epoch+1)%5==0:            visualize_detections(                self.model, bounding_box_format=BBOX_FORMAT, dataset=val_dataset, rows=NUM_ROWS, cols=NUM_COLS            )checkpoint_path="best-custom-model"callbacks_list = [    # 在验证损失连续6个周期未改善时进行早停    keras.callbacks.EarlyStopping(        monitor="val_loss",        patience=6,    ),        # 保存最佳模型    keras.callbacks.ModelCheckpoint(        filepath=checkpoint_path,        monitor="val_loss",        save_best_only=True,        save_weights_only=True    ),        # 在每个周期结束后打印自定义指标    tf.keras.callbacks.LambdaCallback(    on_epoch_end=lambda epoch, logs:         print(f"\n第{epoch+1}个周期 \n" +              f"损失: {logs['loss']:.4f} \n" +               f"mAP: {logs['MaP']:.4f} \n" +               f"验证损失: {logs['val_loss']:.4f} \n" +               f"验证mAP: {logs['val_MaP']:.4f} \n")     ),        # 每5个周期后可视化结果    VisualizeDetections()]
  • 早停。如果验证损失在连续六个周期内没有改善,我们将停止训练。
  • 模型检查点。我们将在每个周期结束后检查val_loss,如果它优于之前的周期,就保存模型权重。
  • Lambda回调。 Lambda回调是一个自定义回调,允许您在每个周期的不同时间点定义和执行任意Python函数。在这种情况下,我们使用它来在每个周期后打印自定义指标。如果只打印COCOMetrics的结果,会是一堆数字。为了简单起见,我们只打印训练和验证损失以及mAP。
  • 检测结果可视化。在每五个周期后,它将打印出一个4×8的图像网格,并显示预测的边界框。这将帮助我们了解我们的模型的优劣。如果一切顺利,随着训练的进行,这些可视化结果应该会越来越好。

最后我们创建我们的模型。回想一下,骨干是一个YOLOv8模型。我们必须传递我们将使用的num_classes,以及bounding_box_format

# 使用在coco数据集上训练的骨干构建一个RetinaNet模型
def create_model():
    model = keras_cv.models.RetinaNet.from_preset(
        "yolo_v8_m_backbone_coco",
        num_classes=len(class_mapping),
        bounding_box_format=BBOX_FORMAT
    )
    return model

model = create_model()

我们还必须自定义模型的非极大抑制参数。非极大抑制用于在目标检测中过滤掉多个重叠的预测边界框,这些边界框对应同一对象。它只保留置信度最高的边界框,并去除多余的边界框,确保每个对象仅被检测一次。它包含两个参数:iou_thresholdconfidence_threshold

  1. IoU(交并比)是一个介于0和1之间的数,用于衡量一个预测边界框与另一个预测边界框之间的重叠程度。如果重叠程度高于iou_threshold,则舍弃置信度较低的预测边界框。
  2. 置信度反映模型对其预测的边界框的自信程度。如果置信度低于confidence_threshold,则舍弃该边界框。

尽管这些参数不会影响训练,但在预测时仍然需要根据特定应用进行调整。设置iou_threshold=0.5confidence_threshold=0.5是一个很好的起点。

在开始训练之前有一点要注意:我们讨论了为什么将分类损失定义为焦点损失(focal loss)是有帮助的,但我们还没有讨论一个适当的回归损失来定义预测边界框坐标的错误。一种常见的回归损失(或box_loss)是平滑L1损失(smooth L1 loss)。我认为平滑L1是“两全其美”的损失函数。它既包含了L1损失(绝对误差),又包含了L2损失(均方差)。对于小误差值,损失是二次的;对于大误差值,损失是线性的(参见此链接)。KerasCV已经内置了平滑L1损失以方便我们使用。在训练过程中显示的损失将是box_lossclassification_loss的总和。

# 使用焦点分类损失和平滑L1回归损失以及coco指标编译模型
model.compile(
    classification_loss="focal",
    box_loss="smoothl1",
    optimizer=optimizer_Adam,
    metrics=[coco_metrics]
)
history = model.fit(
    train_dataset,
    validation_data=validation_dataset,
    epochs=40,
    callbacks=callbacks_list,
    verbose=0,
)

在NVIDIA Tesla P100 GPU上训练大约需要1小时12分钟。

进行预测

# 使用最佳模型的权重创建模型
model = create_model()
model.load_weights(checkpoint_path)
# 自定义模型预测的非极大抑制参数。我发现这些数字运行得相当不错
model.prediction_decoder = keras_cv.layers.MultiClassNonMaxSuppression(
    bounding_box_format=BBOX_FORMAT,
    from_logits=True,
    iou_threshold=0.2,
    confidence_threshold=0.6,
)
# 在验证集上进行可视化
visualize_detections(model, dataset=val_dataset, bounding_box_format=BBOX_FORMAT, rows=NUM_ROWS, cols=NUM_COLS)

现在,我们可以加载训练期间看到的最佳模型,并使用它对验证集进行一些预测:

Sample visual of validation set predictions. Image by author.

我们最佳模型的指标是:

  • 损失:0.4185
  • mAP:0.2182
  • 验证损失:0.4584
  • 验证mAP:0.2916

可观但还有改进的空间。关于这个问题,在总结中会有更多讨论。(注意:我注意到 MultiClassNonMaxSuppression 似乎没有正常工作。上面显示的左下角图像明显具有重叠超过20%面积的框,但置信度较低的框没有被抑制。这是我需要进一步研究的问题。)

这是我们每个时期的训练和验证损失的绘图。显示出一些过拟合现象。另外,添加学习率调整策略以随时间降低学习率可能是明智的。这可能有助于解决训练末期出现的大幅跳跃问题。

我们每个时期的训练和验证损失的绘图。我们看到了过拟合的迹象。作者提供的图像。

总结

如果您能走到这一步,请给自己一个赞!目标检测是计算机视觉中较困难的任务之一。幸运的是,我们有新的KerasCV库可以让我们的工作更轻松。总结创建目标检测流程的步骤如下:

  • 开始时可视化您的数据集。自问一些问题,例如:“我的边界框格式是什么?是xyxy?Relxyxy?我要处理多少类别?”一定要创建一个类似于 visualize_dataset 的函数来查看图像和边界框。
  • 将您拥有的任何数据格式转换为KerasCV所需的“字典内字典”格式。使用TensorFlow的数据集对象来保存数据尤其有帮助。
  • 进行一些基本的预处理,如图像调整大小和数据增强。KerasCV使这变得相当简单。确保阅读所选择模型的文献,以确保图像大小适当。
  • 将字典转换回元组以进行训练。
  • 选择一个优化器(Adam是一个简单的选择)、两个损失函数(类别损失使用focal,框损失使用L1平滑是简单的选择)和指标(COCO指标是简单的选择)。
  • 在训练过程中可视化检测结果,以查看模型错过了哪些对象。
数据集中一个有问题的标签示例。作者提供的图像。

下一步的主要任务之一是清理数据集。例如,看一下上面的图像。标记者正确识别了马铃薯晚疫病叶子,但其他健康的马铃薯叶子呢?为什么没有将它们标记为马铃薯叶子?在Roboflow网站的健康检查选项卡上,您可以看到数据集中某些类别的表示极其不充分:

显示类别不平衡的图表。来自Roboflow网站。

在调整超参数之前,尝试解决这些问题。祝您在目标检测任务中好运!

参考资料

[1] F. Chollet,《Python深度学习》(2021),Manning出版社。

[2] A. Géron,《Scikit-Learn、Keras和TensorFlow机器学习实战》(2022),O’Reily Media Inc。

[3] A. Ng,深度学习专业化课程,DeepLearning.AI

[4] D. Singh, N. Jain, P. Jain, P. Kayal, S. Kumawat, N. Batra,PlantDoc:植物疾病可视化数据集(2019),CoDS COMAD 2020

[5] T. Lin, P. Dollár, R. Girshick, K. He, B. Hariharan, S. Belongie,特征金字塔网络用于物体检测(2017),CVPR 2017

[6] T. Lin, P. Goyal, R. Girshick, K. He, P. Dollar,聚焦损失用于物体检测(2020),IEEE Transactions on Pattern Analysis and Machine Intelligence

Leave a Reply

Your email address will not be published. Required fields are marked *