目标检测之牛仔行头检测(下)—— 以YOLOV5为baseline解决样本不均衡问题。

目录

前言

一、验证集重新划分

二、数据重采样

1.Update image weights

3.Focal loss

4.更改模型





前言

本次主要是基于YOLOV5为baseline来解决样本不均衡的问题,来提高得分。不太了解的可以先看看上一篇文章

目标检测之牛仔行头检测(上)—— 读取coco数据集并转换为yolo数据格式,以YOLOV5为baseline训练并提交结果




一、验证集重新划分

我上一次是直接采用了比较暴力的方法,每个类别都分10个给验证集,这很明显是有问题的。这次我按照标签的数目的百分比来进行采样,简单的说就是样本少的比如belt采用30%进入验证集,比较多的样本sunglasses采用0.1%划分如验证集的方法。

实现起来和上次的划分也就几行代码的改进。

import os
import cv2 as cv
import shutil
import numpy as np
from tqdm import tqdm
from torchvision.datasets.coco import CocoDetection


# 检测并创建需要的文件夹
def check_path(save_path):
    if not os.path.exists(save_path):
        os.mkdir(save_path)
        print("making {} file".format(save_path))


# convert the bounding box from COCO to YOLO format.
def cc2yolo_bbox(img_width, img_height, bbox):
    dw = 1. / img_width
    dh = 1. / img_height
    x = bbox[0] + bbox[2] / 2.0
    y = bbox[1] + bbox[3] / 2.0
    w = bbox[2]
    h = bbox[3]

    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh

    return (x, y, w, h)


# save image and label
def save_image_label(image_path_copy_to, label_path_copy_to, label_text):
    # 保存地址和复制图片
    shutil.copyfile(img_path, image_path_copy_to)
    # 保存val label
    with open(label_path_copy_to, "w") as f:
        index = 0
        for write in label_text:
            label_info = [str(write[0]), str(write[1][0]), str(write[1][1]), str(write[1][2]), str(write[1][3])]
            if index == 0:
                f.write(" ".join(label_info))
                index += 1
            else:
                f.write("\n" + " ".join(label_info))

# 以下两个是coco数据集的路径,按需修改!!!!!
coco_image_path = 'D:\CMP\\2021Innovation\dataset\Cowboy\cowboyoutfits\images'
coco_json_path = 'D:\CMP\\2021Innovation\dataset\Cowboy\cowboyoutfits\\train.json'
# 拼接出保存的目录结构
save_path_root = './yolo_datasets'
save_path_train = os.path.join(save_path_root, 'train')
save_path_train_images = os.path.join(save_path_train, 'images')
save_path_train_labels = os.path.join(save_path_train, 'labels')
save_path_val = os.path.join(save_path_root, 'val')
save_path_val_images = os.path.join(save_path_val, 'images')
save_path_val_labels = os.path.join(save_path_val, 'labels')
save_path = [save_path_root, save_path_train, save_path_train_images, save_path_train_labels, save_path_val,
             save_path_val_images, save_path_val_labels]
# 生成保存目录
for path in save_path:
    check_path(path)
# 用于后续统计及划分数据集
categories = {87: 'belt', 1034: 'sunglasses', 131: 'boot', 318: 'cowboy_hat', 588: 'jacket'}
categories_name = ['belt', 'sunglasses', 'boot', 'cowboy_hat', 'jacket']
categories_map = {'belt': 0, 'sunglasses': 1, 'boot': 2, 'cowboy_hat': 3, 'jacket': 4}  # 类别映射
# 这里面的类别数是提前采样得到的
categories_static = {'belt': round(25*0.3), 'sunglasses': round(2330*0.01), 'boot': round(449*0.1), 'cowboy_hat': round(595*0.1),
                     'jacket': round(2195*0.01)}  # 统计验证集
# 载入数据
train_data = CocoDetection(coco_image_path, coco_json_path)
# 获取id和filename的对应字典
id_filenames = {}
for i in range(len(train_data.coco.dataset['images'])):
    id = train_data.coco.dataset['images'][i]['id']
    file_name = train_data.coco.dataset['images'][i]['file_name']
    id_filenames.update({id: file_name})

# 统计数据

for image, label in tqdm(train_data):
    # 将PIL图片转为cv
    image = cv.cvtColor(np.array(image), cv.COLOR_RGB2BGR)
    # 通过id获取图片名称
    image_name = id_filenames[label[0]['image_id']]
    # 去除.jpg后最
    image_name = image_name[:-4]
    # 获取图片路径
    img_path = os.path.join(coco_image_path, image_name + '.jpg')
    # 获取图片长宽
    img_width, img_height = image.shape[1], image.shape[0]
    # 保存的信息
    info = []
    for i in range(len(label)):
        # 读取bbox并转换为yolo格式
        bbox = label[i]['bbox']
        # 用于display用
        # x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3])
        bbox = cc2yolo_bbox(img_width, img_height, bbox)
        # 获取对应label的class名字
        class_name = categories[label[i]['category_id']]
        info.append([categories_map[class_name], bbox])

    #     cv.rectangle(image, (x1, y1), (x2, y2), color=(255, 0, 0), thickness=2)
    #     cv.putText(image, class_name, (x1, y1), cv.FONT_HERSHEY_SIMPLEX, 0.5, color=(0, 0, 255), thickness=2)
    # cv.imshow('image', image)
    # cv.waitKey()

    class_counter = [0, 0, 0, 0, 0]
    label_text = []
    flag = 1
    for text in info:
        class_counter[text[0]] += 1
        label_text.append(text)
        # 是腰带类
        if text[0] == 0:
            class_index = 0
            flag = 0
    # print(label_text)
    if flag:
        class_index = np.argmax(class_counter)
    # 如果少于10个,就加入测试集
    if categories_static[categories_name[class_index]]:
        # 对应类别数加一
        categories_static[categories_name[class_index]] -= 1
        # 保存到val
        image_path_copy_to = os.path.join(save_path_val_images, image_name + '.jpg')
        label_path_copy_to = os.path.join(save_path_val_labels, image_name + '.txt')
        save_image_label(image_path_copy_to, label_path_copy_to, label_text)
    # 否则加入训练集
    else:
        image_path_copy_to = os.path.join(save_path_train_images, image_name + '.jpg')
        label_path_copy_to = os.path.join(save_path_train_labels, image_name + '.txt')
        save_image_label(image_path_copy_to, label_path_copy_to, label_text)




二、数据重采样

这个数据重采样简单的说有两个方便,也是沐神提供的,发现效果确实十分好。

一个是在采集batch数据的时候,对于小数量的类别的,采样的频率高一点,对于大数量的类别,采样频率低一点。这个在直觉上也是十分有效的,因为对于样本量大的类别,属于比较容易学习的类别,不需要重复的去学习,而对于小类别的,则需要多次的重复学习。

二是在设置权重的时候,对于小数量的类别,学习的权重大一点,对于大数量的,学习权重小一点。

二者其实是等价的,而这个实现起来也比较简单,YOLOV5其实也有内置这个功能。




1.Update image weights

这个具体实现方法,我以代码为主来讲解

一个是根据label去得到每个类别的学习权重,如下

# 主要通过这个函数,去统计类别,改变权重或者采样频率
def labels_to_class_weights(labels, nc=80):
    # Get class weights (inverse frequency) from training labels
    # 如果没有labels,则直接返回空
    if labels[0] is None:  # no labels loaded
        return torch.Tensor()
    # labes原先是(N,1,5),通过concatenate拉长,方便处理
    labels = np.concatenate(labels, 0)  # labels.shape = (866643, 5) for COCO
    classes = labels[:, 0].astype(np.int)  # labels = [class xywh]
    # 根据class得出的每个class的数量索引
    weights = np.bincount(classes, minlength=nc)  # occurrences per class


    # 没有出现过的label置为1
    weights[weights == 0] = 1  # replace empty bins with 1
    weights = 1 / weights  # number of targets per class
    weights /= weights.sum()  # normalize  正则化
    return torch.from_numpy(weights)
weights处理完后,可以看到对于belt:0的权重是设置的比较大的。

 第二个是根据label去得到每个类别的采样频率,如下

def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
    # 每个label中的class数量
    class_counts = np.array([np.bincount(x[:, 0].astype(np.int), minlength=nc) for x in labels])
    # 根据class_weights再去修正图片的采样
    image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
    
    return image_weights

  可以看到对于较少样本的采样频率是比较高的。

总和 

# 通过统计label,更改class_weights
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc  # attach class weights

maps = np.zeros(nc)  # mAP per class
# 根据map的值去重新计算cw
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc  # class weights
# 根据计算出来的cw再次计算iw
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw)  # image weights
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)  # rand weighted idx

这个采样频率主要就是通过random.choices来实现的,主要实现原理如下,不过我们也可以看到一个问题就是,如果一个样本过于小,就像belt一样,得到权重过大,会得到全部样本都是belt,效果也会不好,所以可以对计算出来的iw做一个平展处理,使得整体的差距不会太大。我自己处理的方法是设定一个上限和下限,再进行归一化。(如果有更好的方法,欢迎交流)

import numpy as np
import random
iw = [0.91542, 0.00639, 0.04197, 0.02941, 0.00681]
b = [1, 2, 3, 4, 5]
c = random.choices(b, weights=iw, k=5)
print(c)
"""
[1, 1, 1, 1, 1]
"""
# 防止数据过于不平衡
iw = np.clip(iw, 0.2, 0.8)
sum = np.sum(iw)
iw = [x/sum for x in iw]
c = random.choices(b, weights=iw, k=5)
print(c)
"""
[1, 2, 1, 5, 1]
"""

上面的方法要在yolov5中实现也十分简单,最新版本的作者已经帮我们实现好了

我们只需要在train.py中将image-weights设置为True即可

 同样是采取yolov5s,跑50个epoch,可以看出比不优化效果要好不少得分也是涨了15点

     Epoch   gpu_mem       box       obj       cls    labels  img_size
     49/49     4.52G   0.02143   0.01552 0.0005698        44       640: 100%|██████████| 182/182 [09:25<00:00,  3.11s/it]
               Class     Images     Labels          P          R     [email protected] [email protected]:.95: 100%|██████████| 5/5 [00:09<00:00,  1.90s/it]
                 all        158        289      0.793      0.559      0.599      0.358
                belt        158          9          1          0      0.128      0.072
          sunglasses        158         37      0.536      0.811      0.728      0.436
                boot        158        100      0.842       0.48      0.646      0.367
          cowboy_hat        158         97      0.918      0.809       0.85      0.484
              jacket        158         46      0.667      0.696      0.646       0.43

3.Focal loss

这里我不对focal loss做过多解读,因为感觉自己对focal loss不知道理解的对不对。虽然说focal loss是用来解决类别不均衡的问题,但在目标检测领域,作者是这么说的。

The Focal Loss is designed to address the one-stage object detection scenario in which there is an extreme imbalance between foreground and background classes during training (e.g., 1:1000)。

也就是说,在目标检测领域focal loss主要解决的是前景和背景样本不均衡的问题,即是anchor box中背景过多,positvie的太少,是解决这个问题的。所以focal loss对于我们这个比赛实际上用处不大。

我自己实际测试过使用focal loss并没有很好的结果,反而让结果变差了。

4.更改模型

因为我自己baseline用的是yolov5s,如果采用m或者l,效果应该会好不少,大家有空的也可以去试试。

后续如果想到或者看到什么比较好的方法,自己也会回来在实验一下

你可能感兴趣的:(机器学习,Kaggle,pytorch,深度学习,python,目标检测,样本)