目录
前言
一、验证集重新划分
二、数据重采样
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其实也有内置这个功能。
这个具体实现方法,我以代码为主来讲解
一个是根据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
这里我不对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并没有很好的结果,反而让结果变差了。
因为我自己baseline用的是yolov5s,如果采用m或者l,效果应该会好不少,大家有空的也可以去试试。
后续如果想到或者看到什么比较好的方法,自己也会回来在实验一下