Здесь версия YoloV5 для TensorFlow с открытым исходным кодом.

компьютерное зрение

открытый источник

Поскольку исходный код yolov5 (по какой-то причине) стал открытым, ему уделяется много внимания, и недавно я реализовал большую его часть в tensorflow. Может быть первая чистая версия tensorfow2, добро пожаловать, чтобы попробовать и отметить:

GitHub.com/long-tan…

Я сталкивался с yolov3 на работе раньше (вы должны были связаться с ним после запуска демо), и эффект потрясающий. Я просто новичок в зрительном поле (к сожалению, я человек средних лет, но я везде новичок), мои возможности ограничены, и упущения неизбежны. Внедрение с нуля было для меня хорошим опытом, столкнувшись и решив некоторые детали.

Как указано в файле readme, основные функции заключаются в следующем:

  • Реализация чистого tensorflow2
  • Используйте файлы yaml для настройки моделей и управления размером модели.
  • Поддержка обучения пользовательским данным
  • Увеличение данных мозаики
  • Сопоставить привязку по iou или соотношению сторон
  • Смежное положительное усиление образца
  • Поддержка обучения с несколькими графическими процессорами
  • Относительно подробные комментарии к коду
  • Много недостатков, огромный потенциал для улучшения

принцип

В сочетании с кодом кратко ознакомьтесь с его основными принципами и улучшениями. Уже есть много очень хороших аналитических статей о Zhihu для справки, особенно следующие. Если возможно, следует четче и детальнее читать код напрямую.

Цзян Дабай: полное объяснение основных базовых знаний Yolov5 из серии Yolo. Deep Eye: yolov5 углубленный визуальный анализ обратной волны атаки Обнаружение цели: Лучшее из Ёлов 5 серий

Схема модели от @江大白

Сопоставить привязку по соотношению сторон или iou

Реализация здесь состоит в том, чтобы выделить якорь в соответствии с iou в v3 и выделить якорь в соответствии с соотношением сторон в v4 / v5.Новое сопоставление используется для решения деликатной проблемы, когда объект находится близко к сетке, и можно увеличить.


def assign_criterion_wh(self, gt_wh, anchors, anchor_threshold):
        # return: please note that the v5 default anchor_threshold is 4.0, related to the positive sample augment

        gt_wh = tf.expand_dims(gt_wh, 0)  # => 1 * n_gt * 2
        anchors = tf.expand_dims(anchors, 1)  # => n_anchor * 1 * 2
        ratio = gt_wh / anchors  # => n_anchor * n_gt * 2
        matched_matrix = tf.reduce_max(tf.math.maximum(ratio, 1 / ratio),
                                       axis=2) < anchor_threshold  # => n_anchor * n_gt
        return matched_matrix

def assign_criterion_iou(self, gt_wh, anchors, anchor_threshold):
        # by IOU, anchor_threshold < 1
        box_wh = tf.expand_dims(gt_wh, 0)  # => 1 * n_gt * 2
        box_area = box_wh[..., 0] * box_wh[..., 1]  # => 1 * n_gt

        anchors = tf.cast(anchors, tf.float32)  # => n_anchor * 2
        anchors = tf.expand_dims(anchors, 1)  # => n_anchor * 1 * 2
        anchors_area = anchors[..., 0] * anchors[..., 1]  # => n_anchor * 1

        inter = tf.math.minimum(anchors[..., 0], box_wh[..., 0]) * tf.math.minimum(anchors[..., 1],
                                                                                   box_wh[..., 1])  # n_gt * n_anchor
        iou = inter / (anchors_area + box_area - inter + 1e-9)

        iou = iou > anchor_threshold
        return iou

Улучшение положительного образца

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

def enrich_pos_by_position(self, assigned_label, assigned_anchor, gain, matched_matrix, rect_style='rect4'):
        # using offset to extend more postive result, if x
        assigned_xy = assigned_label[..., 0:2]  # n_matched * 2
        offset = tf.constant([[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1]], tf.float32)
        grid_offset = tf.zeros_like(assigned_xy)

        if rect_style == 'rect2':
            g = 0.2  # offset
        elif rect_style == 'rect4':
            g = 0.5  #
            matched = (assigned_xy % 1. < g) & (assigned_xy > 1.)
            matched_left = matched[:, 0]
            matched_up = matched[:, 1]
            matched = (assigned_xy % 1. > (1 - g)) & (assigned_xy < tf.expand_dims(gain[0:2], 0) - 1.)
            matched_right = matched[:, 0]
            matched_down = matched[:, 1]

            assigned_anchor = tf.concat([assigned_anchor, assigned_anchor[matched_left], assigned_anchor[matched_up],
                                         assigned_anchor[matched_right], assigned_anchor[matched_down]], axis=0)
            assigned_label = tf.concat([assigned_label, assigned_label[matched_left], assigned_label[matched_up],
                                        assigned_label[matched_right], assigned_label[matched_down]], axis=0)

            grid_offset = g * tf.concat(
                [grid_offset, grid_offset[matched_left] + offset[1], grid_offset[matched_up] + offset[2],
                 grid_offset[matched_right] + offset[3], grid_offset[matched_down] + offset[4]], axis=0)

        return assigned_label, assigned_anchor, grid_offset

Управление размером модели через файл yaml

Здесь заимствована идея Effectivedet, а размер модели контролируется двумя коэффициентами.

def parse_model(self, yaml_dict):
        anchors, nc, depth_multiple, width_multiple = yaml_dict['anchors'], yaml_dict['nc'], yaml_dict['depth_multiple'], yaml_dict['width_multiple']
        num_anchors = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors
        output_dims = num_anchors * (nc + 5)

        layers = []
        # # from, number, module, args
        for i, (f, number, module, args) in enumerate(yaml_dict['backbone'] + yaml_dict['head']):
            module = eval(module) if isinstance(module, str) else module  # all component is a Class, initialize here, call in self.forward

            for j, arg in enumerate(args):
                try:
                    args[j] = eval(arg) if isinstance(arg, str) else arg  # eval strings, like Detect(nc, anchors)
                except:
                    pass

            number = max(round(number * depth_multiple), 1) if number > 1 else number  # control the model scale, s/m/l/x

            if module in [Conv2D, Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, BottleneckCSP2, SPPCSP, VoVCSP]:
                c2 = args[0]
                c2 = math.ceil(c2 * width_multiple / 8) * 8 if c2 != output_dims else c2
                args = [c2, *args[1:]]

                if module in [BottleneckCSP, BottleneckCSP2, SPPCSP, VoVCSP]:
                    args.insert(1, number)
                    number = 1

            modules = tf.keras.Sequential(*[module(*args) for _ in range(number)]) if number > 1 else module(*args)
            modules.i, modules.f = i, f
            layers.append(modules)
        return layers

сопоставление функции потерь

class YoloLoss(object):
    def __init__(self, anchors, ignore_iou_threshold, num_classes, img_size, label_smoothing=0):
        self.anchors = anchors
        self.strides = [8, 16, 32]
        self.ignore_iou_threshold = ignore_iou_threshold
        self.num_classes = num_classes
        self.img_size = img_size
        self.bce_conf = tf.keras.losses.BinaryCrossentropy(reduction=tf.keras.losses.Reduction.NONE)
        self.bce_class = tf.keras.losses.BinaryCrossentropy(reduction=tf.keras.losses.Reduction.NONE, label_smoothing=label_smoothing)

    def __call__(self, y_true, y_pred):
        iou_loss_all = obj_loss_all = class_loss_all = 0
        balance = [1.0, 1.0, 1.0] if len(y_pred) == 3 else [4.0, 1.0, 0.4, 0.1]  # P3-5 or P3-6        

        for i, (pred, true) in enumerate(zip(y_pred, y_true)):
            # preprocess, true: batch_size * grid * grid * 3 * 6, pred: batch_size * grid * grid * clss+5
            true_box, true_obj, true_class = tf.split(true, (4, 1, -1), axis=-1)
            pred_box, pred_obj, pred_class = tf.split(pred, (4, 1, -1), axis=-1)
            if tf.shape(true_class)[-1] == 1 and self.num_classes > 1:
                true_class = tf.squeeze(tf.one_hot(tf.cast(true_class, tf.dtypes.int32), depth=self.num_classes, axis=-1), -2) 

            # prepare: higher weights to smaller box, true_wh should be normalized to (0,1)
            box_scale = 2 - 1.0 * true_box[..., 2] * true_box[..., 3] / (self.img_size ** 2)
            obj_mask = tf.squeeze(true_obj, -1)  # # obj or noobj, batch_size * grid * grid * anchors_per_grid
            background_mask = 1.0 - obj_mask
            conf_focal = tf.squeeze(tf.math.pow(true_obj - pred_obj, 2), -1)

            # iou/giou/ciou/diou loss
            iou = bbox_iou(pred_box, true_box, xyxy=False, giou=True)            
            iou_loss = (1 - iou) * obj_mask * box_scale  # batch_size * grid * grid * 3

            # confidence loss, Todo: multiply the iou 
            conf_loss = self.bce_conf(true_obj, pred_obj)
            conf_loss = conf_focal * (obj_mask * conf_loss + background_mask * conf_loss)  # batch * grid * grid * 3

            # class loss
            # use binary cross entropy loss for multi class, so every value is independent and sigmoid 
            # please note that the output of tf.keras.losses.bce is origial dim minus the last one
            class_loss = obj_mask * self.bce_class(true_class, pred_class)

            iou_loss = tf.reduce_mean(tf.reduce_sum(iou_loss, axis=[1, 2, 3]))
            conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1, 2, 3]))
            class_loss = tf.reduce_mean(tf.reduce_sum(class_loss, axis=[1, 2, 3]))

            iou_loss_all += iou_loss * balance[i]
            obj_loss_all += conf_loss * balance[i]
            class_loss_all += class_loss * self.num_classes * balance[i]  # to balance the 3 loss

Часть функции потерь не полностью совпадает с настройкой v5. В v5 были сделаны некоторые оптимизации, такие как баланс различных шкал, таких как вес потери достоверности цели и т. д.

Эффект

Если вы хотите получить наилучшие результаты, я все же рекомендую оригинальный pytorch, ведь он был обновлен, а авторы v4 и v5 все еще работают над оптимизацией. Если у вас есть хобби tensorflow или вы хотите понять yolov5 через код, я думаю, что моя версия написана более понятно (соответствующая цена, что могут быть отсутствующие или даже неправильные детали), короче, все желающие могут попробовать. Влияние на данные обнаружения MNIST:Влияние на набор данных voc2012 (эффект необходимо усилить):Как бы ни был велик набор данных, я не могу его запустить, ведь доступно только 1080Ti.

GitHub.com/long-tan…