«Обнаружение объектов с использованием RetinaNet и KerasCV»

«Обнаружение объектов с помощью 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. Задача классификации, где модель должна предсказать, к какому классу объект принадлежит.

В этом примере ограничительная рамка была создана и помечена человеком. Мы хотим автоматизировать этот процесс, и хорошо обученная модель обнаружения объектов может сделать именно это.

Я приступил к изучению своего учебного материала по обнаружению объектов и был разочарован. К сожалению, большинство введения впрочем упоминают мало о обнаружении объектов. Франсуа Шолле в книге Deep Learning with Python [1] говорит:

Обратите внимание, что мы не рассмотрим обнаружение объектов, потому что это слишком специализировано и слишком сложно для вводного учебника.

Аурелион Жерон [2] предоставляет много текстового контента, посвященного идеям обнаружения объектов, но приводит только несколько строк кода, описывающих задачу по обнаружению объектов с имитационными рамками-областями, далеко от конвейера конвейера от начала до конца, которого я искал. Знаменитый курс по глубинному обучению Андрея Нга [3] углубляется в обнаружение объектов, но даже он заканчивает лабораторную работу по кодированию, загружая предварительно обученную модель обнаружения объектов и просто выполняя вывод.

Стремясь углубиться, я начал набросок контура конвейера обнаружения объектов. Для предварительной обработки модели RetinaNet нужно было выполнить следующее (обратите внимание: другие модели обнаружения объектов, такие как YOLO, требуют других шагов):

  • Взять входные изображения и изменить их размер так, чтобы все они были одного размера с отступами, чтобы сохранить соотношение сторон. Ох, не забудьте про рамки-области; их тоже нужно правильно изменить, иначе вы испортите свои данные.
  • Сгенерировать якорные рамки разных масштабов и соотношений сторон на основе границ рамок-областей истинного значения в обучающем наборе. Эти якорные рамки служат в качестве точек ссылки для модели во время обучения.
  • Присваивать метки якорным рамкам на основе их перекрытия с рамками истинного значения. Якорные рамки с высоким перекрытием помечаются как положительные примеры, тогда как те, с низким перекрытием, помечаются как отрицательные примеры.
  • Существует несколько способов описания одной и той же рамки-области. Вам нужно будет реализовать функции для преобразования между этими различными форматами. Об этом чуть позже.
  • Реализовать аугментацию данных, обратив внимание не только на увеличение изображений, но и на рамки-области. В теории вы можете пропустить это, но на практике это необходимо, чтобы наши модели хорошо обобщались.

Посмотрите на этот пример на веб-сайте Keras. Ого. Постобработка наших модельных прогнозов потребует еще больше работы. Чтобы перефразировать команду Keras: это технически сложная проблема.

Когда я начинал отчаиваться, я отчаянно искал в Интернете и наткнулся на библиотеку, о которой раньше не слышал: KerasCV. Когда я прочитал документацию, мне начало даваться понимание, что это будущее компьютерного зрения в TensorFlow/Keras. Они сами описывают это так:

KerasCV можно рассматривать как горизонтальное расширение API Keras: это новые объекты первого уровня, которые слишком специализированы, чтобы добавляться в ядро Keras. Они получают тот же уровень отделки и гарантии обратной совместимости, что и основной API Keras, и поддерживаются командой Keras.

“Но почему ни один из моих учебных материалов не упомянул об этом?” – подумал я. Ответ прост: это довольно новая библиотека. Первый коммит на GitHub был сделан 13 апреля 2022 года, слишком ново, чтобы появиться даже в последних изданиях моих учебников. На самом деле, версия 1.0 библиотеки еще не была выпущена (по состоянию на 10 ноября 2023 года она находится на версии 0.6.4). Я ожидаю, что KerasCV будет подробно рассмотрен в следующих изданиях моих учебников и онлайн-курсов (справедливости ради, Герон упоминает вообще новый проект по анализу естественного языка Keras и Keras CV, который может заинтересовать читателя).

Будучи настолько новой, у KerasCV нет множества учебников, помимо тех, которые опубликовала сама команда Keras (см. здесь). В этом учебнике я продемонстрирую конвейер обнаружения объектов от начала до конца для распознавания здоровых и больных листьев с использованием техник, вдохновленных, но отличных от официальных руководств Keras. С помощью KerasCV даже начинающие могут брать помеченные наборы данных и использовать их для создания эффективных конвейеров обнаружения объектов.

Несколько замечаний перед началом. KerasCV – это быстро меняющаяся библиотека, с кодовой базой и документацией, которые регулярно обновляются. Реализация, показанная здесь, будет работать с версией KerasCV 0.6.4. Команда Keras заявила, что “до того, как KerasCV достигнет v1.0.0, не будет контракта о совместимости со старыми версиями”. Это означает, что нет гарантии, что используемые в этом учебнике методы продолжат работать после обновления KerasCV. Я зафиксировал номер версии KerasCV в сводной записной книжке Kaggle, чтобы предотвратить такие проблемы.

KerasCV имеет несколько ошибок, которые уже отмечены в вкладке Issues на GitHub. Кроме того, документация не полна в некоторых областях (я говорю о вас, MultiClassNonMaxSuppression). Пока вы экспериментируете с KerasCV, не отчаивайтесь из-за этих проблем. Фактически, это отличная возможность стать участником кодовой базы KerasCV!

В этом руководстве мы сосредоточимся на деталях реализации KerasCV. Я кратко рассмотрю некоторые высокоуровневые концепции в обнаружении объектов, но предполагаю, что читатель имеет некоторые предварительные знания о концепциях, таких как архитектура RetinaNet. Показанный здесь код был отредактирован и переструктурирован для большей ясности, пожалуйста, см. приведенную выше ссылку на Kaggle для полного кода.

Наконец, небольшой комментарий о безопасности. Созданная здесь модель не предназначена быть самой современной; рассматривайте это как руководство высокого уровня. Перед тем, как эта модель обнаружения болезни растений может быть реализована в производстве, ожидается дальнейшая настройка и очистка данных. Было бы хорошей идеей прогнозы, которые делает модель, передавать на проверку человеку-эксперту, чтобы подтвердить диагноз.

Изучение данных

Набор данных PlantDoc содержит 2,569 изображений 13 видов растений и 30 классов. Цель этого набора данных описана в абстракте статьи PlantDoc: A Dataset for Visual Plant Disease Detection, написанной Сингхом и др. [4].

Индия теряет 35% годового урожая из-за болезней растений. Раннее обнаружение болезней растений по-прежнему затруднено из-за отсутствия лабораторных инфраструктур и специалистов. В данной статье мы исследуем возможность использования методов компьютерного зрения для масштабируемого и раннего обнаружения болезней растений.

Это благородная цель, и в этой области компьютерное зрение может сделать много полезного для фермеров.

Roboflow позволяет нам загрузить набор данных в различных форматах. Поскольку мы используем TensorFlow, давайте загрузим набор данных в формате TFRecord. TFRecord – это специальный формат, используемый в TensorFlow, предназначенный для эффективного хранения больших объемов данных. Данные представлены последовательностью записей, где каждая запись является парой ключ-значение. Каждый ключ называется функцией. Загруженный zip-файл содержит четыре файла, два для обучения и два для валидации:

  • leaves_label_map.pbtxt: Это файл в текстовом формате Protocol Buffers, который используется для описания структуры данных. Открыв файл в текстовом редакторе, я вижу, что в нем тридцать классов. Среди них есть как здоровые листья, такие как 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 Dataset. Объекты Dataset позволяют хранить большие объемы данных без перегрузки памяти системы. Это достигается благодаря таким функциям, как ленивая загрузка и батчирование. Ленивая загрузка означает, что данные не загружаются в память до явного запроса (например, при выполнении преобразований или во время обучения). Батчирование означает, что в память загружается только определенное количество изображений (обычно 8, 16, 32 и т. д.). Вкратце, я рекомендую всегда преобразовывать данные в объекты Dataset, особенно когда дело касается больших объемов данных (обычно при обнаружении объектов).

Чтобы преобразовать TFRecord в объект Dataset в TensorFlow, вы можете использовать класс 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, чтобы преобразовать разреженные матрицы в плотные матрицы.
  • После стекирования рамок мы используем функцию keras_cv.bounding_box.convert_format из KerasCV. При изучении данных я заметил, что координаты ограничивающих рамок были нормализованы в диапазоне от 0 до 1. Это означает, что числа представляют проценты от общей ширины/высоты изображения. Например, значение 0,5 представляет собой 50% * ширина_изображения. Это относительный формат, который Keras называет REL_XYXY, в отличие от абсолютного формата XYXY. В теории преобразование в абсолютный формат не является обязательным, но при обучении модели с использованием относительных координат у меня возникали ошибки. Смотрите документацию KerasCV для некоторых других поддерживаемых форматов ограничивающих рамок.
  • Наконец, мы берем изображения и ограничивающие рамки и преобразуем их в формат, который хочет KerasCV: словари. Словарь в Python – это тип данных, содержащий пары ключ-значение. В частности, KerasCV ожидает следующий формат:
image_dataset = {  "images": [ширина, высота, каналы],  bounding_boxes = {    "classes": [количество_боксов],    "boxes": [количество_боксов, 4]  }}

На самом деле это “словарь внутри словаря”, так как bounding_boxes также является словарем.

Наконец, используйте функцию .map для применения функции разбора к нашему TFRecord. Вы можете проверить объект Dataset. Все проверено.

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.

Теперь мы можем реализовать аугментацию данных. Аугментация данных – это общая техника в задачах компьютерного зрения. Она незначительно изменяет ваши обучающие изображения, например, небольшое вращение, горизонтальное отражение изображений и т. д. Это помогает решить проблему недостатка данных и также помогает регуляризации. Здесь мы введем следующие аугментации:

  • Функцию JitteredResize из KerasCV. Эта функция предназначена для конвейеров обнаружения объектов и реализует технику аугментации изображений, которая случайным образом масштабирует, изменяет размер, обрезает и заполняет изображения вместе с соответствующими границами прямоугольников. Этот процесс вводит изменчивость масштаба и локальных элементов, улучшая разнообразие данных обучения для лучшей обобщающей способности.
  • Затем мы добавляем горизонтальные и вертикальные RandomFlips а также RandomRotation. Здесь factor – это число с плавающей запятой, представляющее долю 2π. Мы используем 0,25, что означает, что наши аугментаторы будут вращать изображения на число между -25% π и 25% π. В градусах это означает повороты между -45° и 45°.
  • Наконец, мы добавляем RandomSaturation и RandomHue. Насыщенность 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 есть три важных компонента:

  • Основа. Это формирует основу модели. Мы также называем это извлекателем признаков. Как следует из названия, он берет изображение и сканирует его на предмет признаков. Низкоуровневые слои извлекают низкоуровневые признаки (например, линии и кривые), а высокоуровневые слои извлекают высокоуровневые признаки (например, губы и глаза). В этом проекте основой будет модель YOLOv8, которая была предварительно обучена на наборе данных COCO. Мы используем YOLO только как извлекатель признаков, но не как детектор объектов.
  • Сеть пирамиды признаков (FPN). Это модельная архитектура, которая генерирует “пирамиду” карты признаков разных масштабов для обнаружения объектов разных размеров. Она делает это путем объединения низкого разрешения, семантически сильных признаков с высоким разрешением, семантически слабых признаков с помощью верхнего пути и боковых связей. Посмотрите это видео для подробного объяснения или ознакомьтесь с документом [5], в котором представлена FPN.
  • Два подсети, специфичные для задачи. Эти подсети берут каждый уровень пирамиды и обнаруживают объекты в каждом из них. Одна подсеть определяет классы (классификация), а другая определяет границы прямоугольников (регрессия). Эти подсети не обучены.
Упрощенная архитектура 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 = [    # Conduction early stopping to stop after 6 epochs of non-improving validation loss    keras.callbacks.EarlyStopping(        monitor="val_loss",        patience=6,    ),        # Saving the best model    keras.callbacks.ModelCheckpoint(        filepath=checkpoint_path,        monitor="val_loss",        save_best_only=True,        save_weights_only=True    ),        # Custom metrics printing after each epoch    tf.keras.callbacks.LambdaCallback(    on_epoch_end=lambda epoch, logs:         print(f"\nEpoch #{epoch+1} \n" +              f"Loss: {logs['loss']:.4f} \n" +               f"mAP: {logs['MaP']:.4f} \n" +               f"Validation Loss: {logs['val_loss']:.4f} \n" +               f"Validation mAP: {logs['val_MaP']:.4f} \n")     ),        # Visualizing results after each five epochs    VisualizeDetections()]
  • Ранняя остановка. Если потери на валидации не улучшаются в течение шести эпох, мы прекратим обучение.
  • Модельный контроль. Мы будем проверять val_loss после каждой эпохи, сохраняя веса модели, если он превосходит предыдущую эпоху.
  • Лямбда обратный вызов. Лямбда обратный вызов – это пользовательский обратный вызов, который позволяет определить и выполнить произвольные функции Python во время обучения в различных точках каждой эпохи. В этом случае мы используем его для печати пользовательских метрик после каждой эпохи. Если просто вывести COCOMetrics, получится множество чисел. Для простоты мы будем выводить только потери и mAP на обучении и валидации.
  • Визуализация обнаружения. Это позволит вывести сетку из 4 по 8 изображений вместе с предсказанными ограничивающими рамками после каждых пяти эпох. Это позволит нам увидеть, насколько хорошо (или плохо) работает наша модель. Если все пойдет хорошо, эти визуализации должны улучшаться по мере продвижения обучения.

Наконец, мы создаем нашу модель. Напомним, что основой является модель YOLOv8. Мы должны передать параметр num_classes, который будем использовать, а также bounding_box_format.

# Построение модели RetinaNet с внутренней заполненностью, обученной на наборе данных coco 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 modelmodel = create_model()

Также нам нужно настроить параметр снижения числа предсказаний нашей модели. В объектном обнаружении снижение числа предсказанных охватывающих прямоугольников, перекрывающихся друг с другом и соответствующих одному объекту, используется для фильтрации. Он сохраняет только прямоугольник с наибольшей уверенностью и удаляет избыточные прямоугольники, обеспечивая обнаружение каждого объекта только один раз. Он включает два параметра: iou_threshold и confidence_threshold:

  1. IoU или пересечение с объединением это число между 0 и 1, которое определяет насколько прямоугольников перекрываются друг с другом. Если перекрытие больше, чем iou_threshold, тогда прямоугольник с меньшим значением уверенности удаляется.
  2. Уверенность отражает уверенность модели в предсказанном прямоугольнике. Если значение уверенности для предсказанного прямоугольника меньше, чем confidence_threshold, прямоугольник удаляется.

Хотя эти параметры не влияют на обучение, их необходимо настроить для конкретного приложения для целей прогнозирования. Установка iou_threshold=0.5 и confidence_threshold=0.5 является хорошим стартовым местом.

Прежде чем приступить к обучению, стоит упомянуть, что мы обсудили, почему полезно использовать функцию потерь классификации – focal loss, но мы еще не обсудили подходящую функцию потерь регрессии для определения ошибки в координатах предсказанных охватывающих прямоугольников. Популярная функция потерь регрессии (или box_loss) – это сглаженная L1-потеря. Я считаю сглаженную L1-потерю особенной “лучшей из двух потерь”. Она объединяет как потерю L1 (абсолютная ошибка), так и потерю L2 (среднеквадратичная ошибка). Потеря квадратична для малых значений ошибки и линейна для больших значений ошибки (смотрите эту ссылку). В KerasCV есть встроенная функция потерь smooth L1 для нашего удобства. При обучении будет отображаться сумма потери box_loss и потери classification_loss.

# Используем функцию потерь классификации focal и функцию потерь регрессии smoothl1 с метриками 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,)

Обучение на GPU NVIDIA Tesla P100 занимает примерно один час и 12 минут.

Сделаем прогнозы

# Создаем модель с весами лучшей моделиmodel = create_model()model.load_weights(checkpoint_path)# Настройка снижения числа предсказаний в модели. Я нашел эти значения довольно хорошимимodel.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)

Теперь мы можем загрузить лучшую модель, полученную во время обучения, и использовать ее для выполнения прогнозов на наборе данных проверки:

Пример визуализации предсказаний на наборе данных проверки. Изображение автора.

Метрики нашей лучшей модели:

  • Потеря: 0.4185
  • mAP: 0.2182
  • Потеря на проверке: 0.4584
  • mAP на проверке: 0.2916

Уважаемый, но это можно улучшить. Больше об этом в заключении. (Примечание: я заметил, что MultiClassNonMaxSuppression не кажется работающим правильно. На нижнем левом изображении, показанном выше, явно есть прямоугольники, которые перекрываются более чем на 20% своей площади, но прямоугольник с меньшей уверенностью не подавляется. Это то, с чем мне придется подробнее разобраться.)

Вот график наших потерь обучения и валидации для каждой эпохи. Наблюдается некоторое переобучение. Также, может быть разумным добавить расписание скорости обучения, чтобы с течением времени уменьшать скорость обучения. Это может помочь решить проблему больших скачков в конце обучения.

График наших потерь обучения и валидации для каждой эпохи. Мы видим признаки переобучения. Изображение автора.

Заключение

Если вы дочитали до этого момента, похвалите себя! Обнаружение объектов – одна из самых сложных задач в компьютерном зрении. К счастью, у нас есть новая библиотека KerasCV, которая делает нашу жизнь легче. Для краткого изложения рабочего процесса создания конвейера обнаружения объектов:

  • Начните с визуализации вашего набора данных. Задайте себе вопросы вроде: “Какой у меня формат ограничивающей рамки? Это xyxy? Relxyxy? Сколько классов я имею?” Обязательно создайте функцию, подобную visualize_dataset, чтобы просматривать изображения и ограничивающие рамки.
  • Преобразуйте любой формат данных, который у вас есть, в формат “словаря внутри словаря”, который требуется KerasCV. Использование объекта TensorFlow Dataset для хранения данных особенно полезно.
  • Выполните некоторую базовую предобработку, такую как изменение размера изображения и аугментацию данных. KerasCV делает это довольно простым. Позаботьтесь о том, чтобы изучить литературу по выбранной модели, чтобы убедиться, что размеры изображений соответствующие.
  • Преобразуйте словари обратно в кортежи для обучения.
  • Выберите оптимизатор (Adam – легкий выбор), два функции потерь (focal для функции потери класса и L1 сглаживание для функции потери ограничивающей рамки – легкие выборы) и метрики (метрики COCO – легкий выбор).
  • Визуализация обнаружений во время обучения может помочь увидеть, какие объекты ваша модель упускает.
Пример проблемной метки в наборе данных. Изображение автора.

Одним из основных следующих шагов будет очистка набора данных. Например, взгляните на изображение выше. Аннотаторы правильно определили позднюю мучнистую росу листа картофеля, но что насчет всех остальных здоровых листьев картофеля? Почему они не были помечены как лист картофеля? Изучив вкладку “Проверка состояния” на веб-сайте Roboflow, вы увидите, что некоторые классы значительно недостаточно представлены в наборе данных:

Диаграмма, показывающая несбалансированность классов. С веб-сайта Roboflow.

Попробуйте исправить эти проблемы перед тем, как настраивать гиперпараметры. Удачи в выполнении ваших задач по обнаружению объектов!

Ссылки

[1] F. Chollet, Глубокое обучение на Python (2021), Manning Publications Co.

[2] A. Géron, Практическое машинное обучение с помощью Scikit-Learn, Keras и TensorFlow (2022), O’Reily Media Inc.

[3] А. Нг, Специализация по глубокому обучению, DeepLearning.AI

[4] Д. Сингх, Н. Джайн, П. Джайн, П. Каял, С. Кумават, Н. Батра, PlantDoc: Набор данных для визуального обнаружения заболеваний растений (2019), CoDS COMAD 2020

[5] Т. Лин, П. Доллар, Р. Гиршик, К. Хе, Б. Харихаран, С. Белонджи, Сети с пирамидой признаков для обнаружения объектов (2017), CVPR 2017

[6] Т. Лин, П. Гойал, Р. Гиршик, К. Хе, П. Доулар, Фокусировочная потеря для обнаружения объектов (2020), IEEE Transactions on Pattern Analysis and Machine Intelligence