Yolo5 на примере задачи Help Protect the Great Barrier Reef

Введение

Yolov5 является одной из самых эффективных решений для детектирования объектов на настоящий момент. Собственно подтверждение этому можно найти на сайте Yolo:

https://github.com/ultralytics/yolov5

136901921-abcfcd9d-f978-4942-9b97-0e3f202907df

Ну и конечно же в последнем конкурсе на Kaggle архитектура показала свое качество.  

Конкурс: https://www.kaggle.com/c/tensorflow-great-barrier-reef/

Замечание: возможно именно этот конкурс не является показательным, т.к. private выборка значительно отличалась от public, в результате чего после завершения конкурса вся таблица перемешалась.

В данной статье я опишу саму задачу и как ее решать.

 

Задача

В конкурсе TensorFlow - Help Protect the Great Barrier Reef необходимо было детектировать морские звезды на последовательности изображений (можно сказать на видео). Примеры, приведены на картинках ниже:

  • __results___18_7
  • __results___18_6
  • __results___18_2

Оценкой выступала метрика F2 в диапазоне от от  0.3 до 0.8 Про метрику можно почитать здесь: https://en.wikipedia.org/wiki/F-score 

Формула, где бетта = 2.

2022-02-17_14-17-35

Т.е. нужно обучить модель(-и), чтобы получить максимальный результат.

 

Подготовка машины для обучения

Рассмотрим стандартную машину (Linux) с установленными драйверами и Юпитером, которые обычно применяются на платных вычислительных ресурсах. Сначала в терминале нужно установить дополнительные модули:

apt-get update

apt install git

apt-get install ffmpeg libsm6 libxext6  -y

В Юпитере также надо ставить компоненты:

!pip install --upgrade pip

!pip install --upgrade --force-reinstall --no-deps kaggle

!pip install slugify

!pip install tqdm

Дальше загрузить kaggle.json с паролем-доступом (для вашего аккаунта). После чего скачиваем датасет.

! mkdir ~/.kaggle

! cp kaggle.json ~/.kaggle/

! chmod 600 ~/.kaggle/kaggle.json

!kaggle competitions download -c tensorflow-great-barrier-reef

 

 Downloading tensorflow-great-barrier-reef.zip to /root

100%|███████████████████████████████████████| 14.2G/14.2G [02:40<00:00, 117MB/s]100%|██████████████████████████████████████| 14.2G/14.2G [02:40<00:00, 94.9MB/s]

Распаковываем данные:

! mkdir tensorflow-great-barrier-reef

! unzip tensorflow-great-barrier-reef.zip -d tensorflow-great-barrier-reef

 

 И устанавливаем еще необходимые модули:

!pip install pandas

!pip install wandb

!pip install opencv-python

!pip install pillow

!pip install matplotlib

!pip install seaborn

!pip install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html

 

 На этом установка закончена, Yolov5 поставим следующем разделе

 

Обучение Yolo

В соревновании выборка была разбита на 3 видео. И обучали либо: а) на двух видео, а на одном проверяли. Либо уменьшали проверочную выборку до минимума, чтобы было больше данных. Тут будет описано, как обучить на датасете при выделении 1 видео в проверочное.

import numpy as np # linear algebra

import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

from shutil import copyfile

train = pd.read_csv('/root/tensorflow-great-barrier-reef/train.csv')

train['pos'] = train.annotations != '['

 

 Создаем папки для хранения обучающих данных:

!mkdir -p ./yolo_data/fold1/images/val

!mkdir -p ./yolo_data/fold1/images/train

 

!mkdir -p ./yolo_data/fold1/labels/val

!mkdir -p ./yolo_data/fold1/labels/train

 

 После чего заполняются тренировочные и валидационные папки:

fold = 1

 

annos = [

for i, x in train.iterrows():

    if x.video_id == fold:

        mode = 'val'

    else:

        # train

        mode = 'train'

        if not x.pos: continue

        # val

    copyfile(f'/root/tensorflow-great-barrier-reef/train_images/video_{x.video_id}/{x.video_frame}.jpg',

                f'./yolo_data/fold{fold}/images/{mode}/{x.image_id}.jpg')

    if not x.pos:

        continue

    r = ''

    anno = eval(x.annotations)

    for an in anno:

#            annos.append(an)

        r += '0 {} {} {} {}\n'.format((an['x'] + an['width'] / 2) / 1280,

                                        (an['y'] + an['height'] / 2) / 720,

                                        an['width'] / 1280, an['height'] / 720)

    with open(f'./yolo_data/fold{fold}/labels/{mode}/{x.image_id}.txt', 'w') as fp:

        fp.write(r) 

Настройка гиперпараметров для обучения:

hyps = '''

# YOLOv5 by Ultralytics, GPL-3.0 license

# Hyperparameters for COCO training from scratch

# python train.py --batch 40 --cfg yolov5m.yaml --weights '' --data coco.yaml --img 640 --epochs 300

# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials

lr0: 0.01  # initial learning rate (SGD=1E-2, Adam=1E-3)

lrf: 0.1  # final OneCycleLR learning rate (lr0 * lrf)

momentum: 0.937  # SGD momentum/Adam beta1

weight_decay: 0.0005  # optimizer weight decay 5e-4

warmup_epochs: 3.0  # warmup epochs (fractions ok)

warmup_momentum: 0.8  # warmup initial momentum

warmup_bias_lr: 0.1  # warmup initial bias lr

box: 0.05  # box loss gain

cls: 0.5  # cls loss gain

cls_pw: 1.0  # cls BCELoss positive_weight

obj: 1.0  # obj loss gain (scale with pixels)

obj_pw: 1.0  # obj BCELoss positive_weight

iou_t: 0.20  # IoU training threshold

anchor_t: 4.0  # anchor-multiple threshold

# anchors: 3  # anchors per output layer (0 to ignore)

fl_gamma: 0.0  # focal loss gamma (efficientDet default gamma=1.5)

hsv_h: 0.015  # image HSV-Hue augmentation (fraction)

hsv_s: 0.7  # image HSV-Saturation augmentation (fraction)

hsv_v: 0.4  # image HSV-Value augmentation (fraction)

degrees: 0.0  # image rotation (+/- deg)

translate: 0.1  # image translation (+/- fraction)

scale: 0.5  # image scale (+/- gain)

shear: 0.0  # image shear (+/- deg)

perspective: 0.0  # image perspective (+/- fraction), range 0-0.001

flipud: 0.5  # image flip up-down (probability)

fliplr: 0.5  # image flip left-right (probability)

mosaic: 1.0  # image mosaic (probability)

mixup: 0.5  # image mixup (probability)

copy_paste: 0.0  # segment copy-paste (probability)

'''

 

data = '''

# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]

path: ../yolo_data/fold1/  # dataset root dir

train: images/train  # train images (relative to 'path') 128 images

val: images/val  # val images (relative to 'path') 128 images

test:  # test images (optional) 

# Classes

nc: 1  # number of classes

names: ['reef']  # class names

# Download script/URL (optional)

# download: https://ultralytics.com/assets/coco128.zip

'''

Скачиваем репозиторий Yolov5

!git clone https://github.com/ultralytics/yolov5.git

Записываем настройки:

with open('./yolov5/data/reef_f1_naive.yaml', 'w') as fp:

    fp.write(data)

with open('./yolov5/data/hyps/hyp.heavy.2.yaml', 'w') as fp:

    fp.write(hyps)

Инициализируем

import wandb

wandb.login(key="здесь ваше код для Weights & Biases")

И запускаем обучение:

!python train.py --img 3840 --batch 4 --epochs 15 --data reef_f1_naive.yaml --weights yolov5s6.pt --name s6_3840_b8_uflip_vm5_f1 --hyp data/hyps/hyp.heavy.2.yaml 

Starting training for 15 epochs...     Epoch   gpu_mem       box       obj       cls    labels  img_size      0/14     29.7G   0.07855   0.09465         0        24      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384     0.0268     0.0323    0.00301   0.000935     Epoch   gpu_mem       box       obj       cls    labels  img_size      1/14     31.4G   0.05612   0.06586         0        19      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.487      0.385      0.323     0.0813     Epoch   gpu_mem       box       obj       cls    labels  img_size      2/14     31.4G   0.05011   0.05583         0        12      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.567       0.41      0.385      0.116     Epoch   gpu_mem       box       obj       cls    labels  img_size      3/14     31.4G   0.04531   0.05196         0        14      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.809      0.509      0.588       0.27     Epoch   gpu_mem       box       obj       cls    labels  img_size      4/14     31.4G   0.04059   0.04887         0         8      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.802      0.521      0.582      0.237     Epoch   gpu_mem       box       obj       cls    labels  img_size      5/14     31.4G   0.03859   0.04564         0        18      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.796      0.532      0.601      0.283     Epoch   gpu_mem       box       obj       cls    labels  img_size      6/14     31.4G   0.03656   0.04573         0        14      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.815      0.509      0.592      0.272     Epoch   gpu_mem       box       obj       cls    labels  img_size      7/14     31.4G   0.03527   0.04419         0        16      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.854       0.58      0.657      0.313     Epoch   gpu_mem       box       obj       cls    labels  img_size      8/14     31.4G   0.03361   0.04411         0        14      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384       0.88      0.568      0.657      0.311     Epoch   gpu_mem       box       obj       cls    labels  img_size      9/14     31.4G   0.03272   0.04129         0        24      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.886      0.599      0.677      0.319     Epoch   gpu_mem       box       obj       cls    labels  img_size     10/14     31.4G   0.03199    0.0415         0        23      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.872       0.62      0.691      0.334     Epoch   gpu_mem       box       obj       cls    labels  img_size     11/14     31.4G   0.03092   0.04058         0        26      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.863      0.583      0.657      0.316     Epoch   gpu_mem       box       obj       cls    labels  img_size     12/14     31.4G   0.03023   0.03918         0        17      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.847      0.612       0.68      0.328     Epoch   gpu_mem       box       obj       cls    labels  img_size     13/14     31.4G   0.02963   0.03954         0        21      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384       0.87      0.621      0.688      0.338     Epoch   gpu_mem       box       obj       cls    labels  img_size     14/14     31.4G   0.02902   0.03777         0        22      3840: 100%|███               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.875      0.611      0.679      0.33215 epochs completed in 3.817 hours.Optimizer stripped from runs/train/l6_3840_b8_uflip_vm5_f13/weights/last.pt, 29.3MBValidating runs/train/l6_3840_b8_uflip_vm5_f13/weights/best.pt...Optimizer stripped from runs/train/l6_3840_b8_uflip_vm5_f13/weights/best.pt, 29.3MBFusing layers... Model Summary: 280 layers, 12308200 parameters, 0 gradients               Class     Images     Labels          P          R     mAP@.5 mAP@                 all       8232       6384      0.849      0.632      0.688      0.338wandb: Waiting for W&B process to finish, PID 3077... (success).wandb:                                                                                wandb: Run history:wandb:        metrics/mAP_0.5 ▁▄▅▇▇▇▇█████████wandb:   metrics/mAP_0.5:0.95 ▁▃▃▇▆▇▇▇▇███████wandb:      metrics/precision ▁▅▅▇▇▇▇█████████wandb:         metrics/recall ▁▅▅▇▇▇▇▇▇██▇████wandb:         train/box_loss █▅▄▃▃▂▂▂▂▂▁▁▁▁▁wandb:         train/cls_loss ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁wandb:         train/obj_loss █▄▃▃▂▂▂▂▂▁▁▁▁▁▁wandb:           val/box_loss █▅▄▂▃▂▂▁▁▂▁▁▁▁▁▁wandb:           val/cls_loss ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁wandb:           val/obj_loss █▄▃▂▂▂▂▁▁▁▁▁▁▁▁▁wandb:                  x/lr0 ▃▅███▇▆▆▅▄▃▃▂▁▁wandb:                  x/lr1 ▃▅███▇▆▆▅▄▃▃▂▁▁wandb:                  x/lr2 █▅▂▂▂▂▂▂▁▁▁▁▁▁▁wandb: wandb: Run summary:wandb:             best/epoch 13wandb:           best/mAP_0.5 0.6879wandb:      best/mAP_0.5:0.95 0.33823wandb:         best/precision 0.8697wandb:            best/recall 0.6214wandb:        metrics/mAP_0.5 0.6882wandb:   metrics/mAP_0.5:0.95 0.33841wandb:      metrics/precision 0.8494wandb:         metrics/recall 0.63236wandb:         train/box_loss 0.02902wandb:         train/cls_loss 0.0wandb:         train/obj_loss 0.03777wandb:           val/box_loss 0.01187wandb:           val/cls_loss 0.0wandb:           val/obj_loss 0.01423wandb:                  x/lr0 0.00139wandb:                  x/lr1 0.00139wandb:                  x/lr2 0.00139wandb: wandb: Synced 5 W&B file(s), 497 media file(s), 1 artifact file(s) and 0 other file(s)wandb: Synced l6_3840_b8_uflip_vm5_f1: https://wandb.ai/vidikon/YOLOv5/runs/ils1wcwwwandb: Find logs at: ./wandb/run-20220117_104014-ils1wcww/logs/debug.logwandb: Results saved to runs/train/l6_3840_b8_uflip_vm5_f13

 

Weights & Biases

Weights & Biases является удобной штукой, где в реальном времени можно смотреть ход обучения https://wandb.ai/site

Метрики:

  • 2022-02-17_15-24-30
  • 2022-02-17_15-24-18
  • 2022-02-17_15-24-11
  • 2022-02-17_15-24-03

Потери:

  • 2022-02-17_15-26-56
  • 2022-02-17_15-26-45
  • 2022-02-17_15-26-35
  • 2022-02-17_15-26-29

Выбор моделей

Выбор оптимального inference всегда осуществляется по тестированию. Это могут быть и готовые функции по вычислению F2 score (однако это сложно сделать, если вы почти все данные записали в обучающие, а не валидационные).

Загрузка модели выглядит так:

model1 = torch.hub.load('../input/yolov5-lib-ds',

                       'custom',

                       path='../input/cots-3600s/best.pt',

                       source='local',

                       force_reload=True# local repo

При различном обучении однако было выявлено два варианта, отличающиеся по форме Precission

Вариант 1 Обычный Precissiion

P_curve

Вариант 2 Почти прямой Precissiion

P_curve

Соответственно возникла идея использовать ансамбль моделей по принципу, базовый результат из наилучшей модели Вариант 1 + высокодостоверные результаты, которые на совпадают с Вариантом 1 из Варианта 2. Это было сделано следующим образом:

out_data=[

for idx, (img, pred_df) in enumerate(tqdm(iter_test)):

    anno = ''

    bboxes=[

    bboxes2=[

    out_step=[

    r = model(img, size=6400, augment=True)

    if r.pandas().xyxy[0].shape[0] == 0:

        anno = ''

    else:

        for idx, row in r.pandas().xyxy[0].iterrows():

            if row.confidence > 0.3:

                #anno += '{} {} {} {} {} '.format(row.confidence, int(row.xmin), int(row.ymin), int(row.xmax-row.xmin), int(row.ymax-row.ymin))

                bbox = {

                    'x1':row.xmin,

                    'x2':row.xmax,

                    'y1':row.ymin,

                    'y2':row.ymax

                }          

                #print(bbox)

                bboxes.append([bbox,row.confidence,0])

    r = model1(img, size=3600, augment=True)

    #print("a")

    if r.pandas().xyxy[0].shape[0] == 0:

        pass

    else:

        for idx, row in r.pandas().xyxy[0].iterrows():

            if row.confidence > 0.1:

                bbox = {

                    'x1':row.xmin,

                    'x2':row.xmax,

                    'y1':row.ymin,

                    'y2':row.ymax

                }          

                #print(bbox)

                k = 0

                max_box = -1

                box_iou = 0

                if bboxes != [:

                    for i,b in enumerate(bboxes):

                        g = get_iou(b[0],bbox)

                        if g > 0.15:

                            bboxes[i][2]=1

                        if g > 0.5:

                            k = 1                           

                            if g > box_iou:

                                max_box = i

                if k == 0:

                    if row.confidence > 0.45:

                        bboxes2.append([bbox,row.confidence,0])

                        #anno += '{} {} {} {} {} '.format(row.confidence, int(row.xmin), int(row.ymin), int(row.xmax-row.xmin), int(row.ymax-row.ymin))                       

    for b in bboxes:

        if b[2] == 1 or b[1] > 0.4:

            anno += '{} {} {} {} {} '.format(b[1], int(b[0]['x1']), int(b[0]['y1']), int(b[0]['x2']-b[0]['x1']), int(b[0]['y2']-b[0]['y1']))   

            out_step.append([b[0],b[1],1])

    for b in bboxes2:       

        anno += '{} {} {} {} {} '.format(b[1], int(b[0]['x1']), int(b[0]['y1']), int(b[0]['x2']-b[0]['x1']), int(b[0]['y2']-b[0]['y1']))       

        out_step.append([b[0],b[1],1])

    out_data.append(out_step)

    pred_df['annotations'] = anno.strip(' ')

    env.predict(pred_df)

 

Get_iou здесь функция, вычисляющая IOU:

def get_iou(bb1, bb2):

    """

    Calculate the Intersection over Union (IoU) of two bounding boxes.

 

    Parameters

    ----------

    bb1 : dict

        Keys: {'x1', 'x2', 'y1', 'y2'}

        The (x1, y1) position is at the top left corner,

        the (x2, y2) position is at the bottom right corner

    bb2 : dict

        Keys: {'x1', 'x2', 'y1', 'y2'}

        The (x, y) position is at the top left corner,

        the (x2, y2) position is at the bottom right corner

 

    Returns

    -------

    float

        in [0, 1]

    """

    assert bb1['x1'] < bb1['x2']

    assert bb1['y1'] < bb1['y2']

    assert bb2['x1'] < bb2['x2']

    assert bb2['y1'] < bb2['y2']

 

    # determine the coordinates of the intersection rectangle

    x_left = max(bb1['x1'], bb2['x1'])

    y_top = max(bb1['y1'], bb2['y1'])

    x_right = min(bb1['x2'], bb2['x2'])

    y_bottom = min(bb1['y2'], bb2['y2'])

 

    if x_right < x_left or y_bottom < y_top:

        return 0.0

 

    # The intersection of two axis-aligned bounding boxes is always an

    # axis-aligned bounding box

    intersection_area = (x_right - x_left) * (y_bottom - y_top)

 

    # compute the area of both AABBs

    bb1_area = (bb1['x2'] - bb1['x1']) * (bb1['y2'] - bb1['y1'])

    bb2_area = (bb2['x2'] - bb2['x1']) * (bb2['y2'] - bb2['y1'])

 

    # compute the intersection over union by taking the intersection

    # area and dividing it by the sum of prediction + ground-truth

    # areas - the interesection area

    iou = intersection_area / float(bb1_area + bb2_area - intersection_area)

    assert iou >= 0.0

    assert iou <= 1.0

    return iou

 

Далее были почищены ложные срабатывания простым алгоритмом:

from math import sqrt

df = pd.read_csv('submission.csv')

 

def len_(bbox1,bbox2):

    x = ((bbox1['x1']+bbox1['x2'])/2)-((bbox2['x1']+bbox2['x2'])/2)

    y = ((bbox1['y1']+bbox1['y2'])/2)-((bbox2['y1']+bbox2['y2'])/2)

    return sqrt(x*x+y*y)

 

threshold = 80

 

a=[

for i, row in df.iterrows():

    a.append(len(str(row['annotations'])) > 5)       

   

for i, row in df.iterrows():

    if i > 1 and i < len(a)-3:

        if a[i] and a[i+1] and not a[i-1] and not a[i+2] and not a[i-2] and not a[i+3]:

            df.loc[i,'annotations']=""

            df.loc[i+1,'annotations']=""

    if i > 0 and i < len(a)-1:

        if a[i] and not a[i-1] and not a[i+1]:

            df.loc[i,'annotations']=""

            continue

        if out_data[i] != [:

            anno = ''

            for j in range(len(out_data[i])):

                f=0               

                if out_data[i-1] !=[:

                    for k in range(len(out_data[i-1])):

                        if len_(out_data[i-1][k][0],out_data[i][j][0]) < threshold:

                            f = 1

                if out_data[i+1] !=[:

                    for k in range(len(out_data[i+1])):

                        if len_(out_data[i+1][k][0],out_data[i][j][0]) < threshold:

                            f = 1

                if f == 1:

                    #out_data[i][j][2] = 0

                    b = out_data[i][j][0]

                    anno += '{} {} {} {} {} '.format(out_data[i][j][1], int(b['x1']), int(b['y1']), int(b['x2']-b['x1']), int(b['y2']-b['y1']))

            df.loc[i,'annotations']=anno.strip(' ')

                   

   

 

df.to_csv('submission.csv',index = False)

 

Такой результат дал 58 место в конкурсе, хотя это как оказалось был не самый оптимальный вариант. И невыбранный ноутбук давал 50 место.

 

Первые места

Два первых места были на yolo и были неожиданными в том числе и для самих авторов. 

1 место. https://www.kaggle.com/c/tensorflow-great-barrier-reef/discussion/307878

Двухступенчатый конвейер - детектирование, а потом дополнительная классификация. 6 моделей  yolo

2 место https://www.kaggle.com/c/tensorflow-great-barrier-reef/discussion/307760

Использование ансамблей из Yolo моделей.