从数据的角度深入解析比赛数据集的处理方法
以第十七届全国大学生智能汽车竞赛智慧交通组线上赛为例,从数据的角度深入解析比赛任务

一、前言
不知道大家有没有这样的经历:在参加比赛的时候,把大部分的精力都用来调参。

“调参侠”大概是每一个初学AI的同学会经历的阶段,调参固然重要,但在比赛中,想要取得一个好成绩,只靠调参是不行滴!

纵观各大比赛的打榜技巧,包括一些冠军方案,不难发现,他们的方案里,通常有下面几个要素:在这里插入图片描述
飞桨领航团AI达人创造营第二期:从结构、性能、训练、设计方面优化模型 by 顾竟萧
针对刚入门的同学,调整超参数当然是最简单的,只需要改几个数字就可以了;
其次我想应该就是模型结构了,只要对神经网络有点了解,那网络结构从代码的层面上改起来也是很容易的,但是要想设计一个性能优异的网络结构却是比较困难的;
损失函数往往需要配合网络结构,因此,改起来简单,但是想要设计一个较好的损失函数,对本科阶段的同学来说,也确实挺难的;
上面说的都是大多数同学们,特别是刚入门的同学会经常去改进的地方,不知道有没有戳中大家的内心,特别地,大部分同学都只停留在“改”这个阶段,要说“设计”其实还有一段距离。

下面讲讲同学们容易忽略的地方,特别是基础薄弱、代码功底不太好的同学:

首先是输入的数据,这里可能有很多同学就说了“我也会用一些数据增强的策略呀”,需要注意的是,数据增强在一定程度上确实可以提分,但是它还不是最根本的东西。这里给大家开个头,一会会详细介绍。
一个完整的算法中至少会有一个输出,这个输出通常也是我们想要的预测结果,如果预测结果不好,我们通常也会用一些后处理的方法,让这个输出尽可能地跟我们的期望输出相似。这一点通常也是同学们容易忽略的地方,特别是这块往往没有现成的代码,需要同学们自己实现。
In [ ]

在继续往下读之前,请先解压数据集

!unzip -oq /home/aistudio/data/data128932/data_coco.zip -d /home/aistudio/data/
二、数据集的格式
因为比赛的任务是目标检测任务,所以这里只分析目标检测任务的数据集格式。目标检测最重要的就是要标记处各个目标的位置和类别。

一般的目标区域位置用一个矩形框来表示,一般用以下3种方式表达:

表达方式 说明
x1,y1,x2,y2 (x1,y1)为左上角坐标,(x2,y2)为右下角坐标
x1,y1,w,h (x1,y1)为左上角坐标,w为目标区域宽度,h为目标区域高度
xc,yc,w,h (xc,yc)为目标区域中心坐标,w为目标区域宽度,h为目标区域高度
常见的目标检测数据集如Pascal VOC采用的[x1,y1,x2,y2] 表示物体的bounding box, COCO采用的[x1,y1,w,h] 表示物体的bounding box。因此我们通常也称为COCO格式和VOC格式。

1.COCO格式
COCO格式下的数据标注是将所有训练图像的标注都存放到一个json文件中。数据以字典嵌套的形式存放。

标注文件中,肯定要包含2个key:

images,表示标注文件中图像信息列表,每个元素是一张图像的信息。如下为其中一张图像的信息:
{
‘file_name’: ‘013856.jpg’,
‘height’: 1080,
‘width’: 1920,
‘id’: 13856
}
annotations,表示标注文件中目标物体的标注信息列表,每个元素是一个目标物体的标注信息。如下为其中一个目标物体的标注信息:
{
‘image_id’: 13856,
‘id’: 0,
‘category_id’: 2,
‘bbox’: [541, 517, 79, 102],
‘area’: 8058,
‘iscrowd’: 0,
‘segmentation’: []
}
annotations里的类别通常用索引表示,为了知道其确切的含义,通常也会加上一个类别信息:

categories,表示标注文件中所有的类别及其对应的索引。如下为比赛中所有的类别信息:
{‘id’: 1, ‘name’: ‘Motor Vehicle’},
{‘id’: 2, ‘name’: ‘Non_motorized Vehicle’},
{‘id’: 3, ‘name’: ‘Pedestrian’},
{‘id’: 4, ‘name’: ‘Traffic Light-Red Light’},
{‘id’: 5, ‘name’: ‘Traffic Light-Yellow Light’},
{‘id’: 6, ‘name’: ‘Traffic Light-Green Light’},
{‘id’: 7, ‘name’: ‘Traffic Light-Off’}
有同学可能就要问了,上面这些信息要怎么读取来,其实就是一些json文件的读取,代码如下:

In [ ]

查看COCO标注文件

import json
coco_anno = json.load(open(‘/home/aistudio/data/data_coco/annotations/train.json’))

coco_anno.keys

print(‘keys:’, coco_anno.keys())

查看类别信息

print(‘\n物体类别:’, coco_anno[‘categories’])

查看一共多少张图

print(‘\n图像数量:’, len(coco_anno[‘images’]))

查看一张图像信息

print(‘\n图像信息:’, coco_anno[‘images’][0])

查看一共多少个目标物体

print(‘\n标注物体数量:’, len(coco_anno[‘annotations’]))

查看一条目标物体标注信息

print(‘\n查看一条目标物体标注信息:’, coco_anno[‘annotations’][0])
keys: dict_keys([‘images’, ‘annotations’, ‘categories’])

物体类别: [{‘id’: 1, ‘name’: ‘Motor Vehicle’}, {‘id’: 2, ‘name’: ‘Non_motorized Vehicle’}, {‘id’: 3, ‘name’: ‘Pedestrian’}, {‘id’: 4, ‘name’: ‘Traffic Light-Red Light’}, {‘id’: 5, ‘name’: ‘Traffic Light-Yellow Light’}, {‘id’: 6, ‘name’: ‘Traffic Light-Green Light’}, {‘id’: 7, ‘name’: ‘Traffic Light-Off’}]

图像数量: 14000

图像信息: {‘file_name’: ‘013856.jpg’, ‘height’: 1080, ‘width’: 1920, ‘id’: 13856}

标注物体数量: 113951

查看一条目标物体标注信息: {‘image_id’: 13856, ‘id’: 0, ‘category_id’: 2, ‘bbox’: [541, 517, 79, 102], ‘area’: 8058, ‘iscrowd’: 0, ‘segmentation’: []}
2.VOC格式
VOC格式里,每个图像文件对应一个同名的xml文件,xml文件中标记物体框的坐标和类别等信息。

图片对应的xml文件内包含对应图片的基本信息,比如文件名、来源、图像尺寸以及图像中包含的物体区域信息和类别信息等。

xml文件中通常包含以下字段:

filename,表示图像名称。

size,表示图像尺寸。包括:图像宽度、图像高度、图像深度。

500 375 3 object字段,表示每个物体。包括:

标签 说明
name 物体类别名称
pose 关于目标物体姿态描述(非必须字段)
truncated 如果物体的遮挡超过15-20%并且位于边界框之外,请标记为truncated(非必须字段)
difficult 难以识别的物体标记为difficult(非必须字段)
bndbox子标签 (xmin,ymin) 左上角坐标,(xmax,ymax) 右下角坐标,
3.COCO格式转VOC格式
在转之前,我们首先要明白为什么要做格式的转换。首先,COCO格式无法直接被数据标注软件直接读取,但有些数据标注软件(如:精灵标注)可以导出COCO格式的数据集。想要检查数据中是否有脏数据(错标注、误标注和漏标注),最直观的方法就是把他们可视化出来;另一方面,如果想要添加新数据,我们也有必要学习像Labellmg这样的标注工具,而这些标注工具导出的标注格式通常都是VOC格式的。因此,我们有必要学习一下,将COCO格式转换成VOC格式的方法。

网上有很多现成的代码可以直接用,稍微改一改就能跑以来,这里参考了旅行到宇宙的边缘这位博主的代码:

https://blog.csdn.net/qq_44955984/article/details/124612645
In [ ]
!pip install pycocotools lxml
代码已经保存在本项目的work/coco2voc.py,可以直接使用:

In [ ]
!python work/coco2voc.py
/home/aistudio/data/data_coco/annotations
loading annotations into memory…
Done (t=0.42s)
creating index…
index created!
100%|█████████████████████████████████████| 14000/14000 [09:00<00:00, 26.03it/s]
转换以后,我们就可以直接可视化这个数据集了,如果发现有脏数据,我们也可以很方便地直接修改:在这里插入图片描述
4.VOC格式转COCO格式
前面我们提到将COCO格式转VOC格式可以方便我们对数据集里的标注信息进行修改,那么为什么又要转换回VOC格式呢?

最主要的原因,我觉得更多的是效率的考虑,可以思考一下:

VOC格式下,有多少张图像就有多少个标注信息,因此,数据越多,读取的也就越慢;
而COCO格式只有一个标注文件,不管数据量有多么地庞大,只需要读取一个文件就可以了。
在转换之前,我们需要准备2个文件:

将图像与标签文件一一对应的索引文件(在下面的代码里命名为trainval.txt)
存放标签类别信息的文件(在下面的代码里命名为label_list.txt)
这部分的代码可参考:

VOC转COCO数据集
In [ ]
import os

#生成train.txt
xml_dir = ‘/home/aistudio/data/data_voc/annotations’
img_dir = ‘/home/aistudio/data/data_voc/images’
path_list = list()
for img in os.listdir(img_dir):
img_path = os.path.join(img_dir,img)
xml_path = os.path.join(xml_dir,img.replace(‘jpg’, ‘xml’))
path_list.append((img_path, xml_path))

train_f = open(‘/home/aistudio/work/trainval.txt’,‘w’)

for i ,content in enumerate(path_list):
img, xml = content
text = img + ’ ’ + xml + ‘\n’
train_f.write(text)
train_f.close()

#生成标签文档
label = [‘Motor Vehicle’, ‘Non_motorized Vehicle’, ‘Pedestrian’, ‘Traffic Light-Red Light’, ‘Traffic Light-Yellow Light’,
‘Traffic Light-Green Light’, ‘Traffic Light-Off’]

with open(‘/home/aistudio/work/label_list.txt’, ‘w’) as f:
for text in label:
f.write(text+‘\n’)
PaddleDetection在提供了x2coco.py用于将VOC数据集、labelme标注的数据集或cityscape数据集转换为COCO数据:

In [ ]
!python work/x2coco.py
–dataset_type voc
–voc_anno_dir /home/aistudio/data/data_voc/annotations/
–voc_anno_list /home/aistudio/work/trainval.txt
–voc_label_list /home/aistudio/work/label_list.txt
–voc_out_name voc_train.json
Start converting !
100%|███████████████████████████████████| 14000/14000 [00:01<00:00, 9167.90it/s]
生成的COCO格式标签默认保存在/home/aistudio/voc_train.json。

三、检测数据分析
想要比赛排名高,数据分析的步骤就不能少。很多刚入门的同学经常会陷入“盲目调参”的误区,数据往往是深度学习模型性能的“天花板”,因此,我们应该先关注“数据”,最后再考虑调参。

1.各类别实例数量分析
我们可以统计一下数据集里,每个类别的数量都有多少,如果有些类别的实例数量比较少,我们可以考虑使用离线数据增强的方法缓解数据不均衡的问题。

In [ ]
import os
from unicodedata import name
import xml.etree.ElementTree as ET
import glob

def count_num(indir):
# 提取xml文件列表
os.chdir(indir)
annotations = os.listdir(‘.’)
annotations = glob.glob(str(annotations) + ‘*.xml’)

dict = {} # 新建字典,用于存放各类标签名及其对应的数目
for i, file in enumerate(annotations): # 遍历xml文件
   
    # actual parsing
    in_file = open(file, encoding = 'utf-8')
    tree = ET.parse(in_file)
    root = tree.getroot()

    # 遍历文件的所有标签
    for obj in root.iter('object'):
        name = obj.find('name').text
        if(name in dict.keys()): dict[name] += 1 # 如果标签不是第一次出现,则+1
        else: dict[name] = 1 # 如果标签是第一次出现,则将该标签名对应的value初始化为1

# 打印结果
print("各类标签的数量分别为:")
for key in dict.keys(): 
    print(key + ': ' + str(dict[key]))            

indir=‘/home/aistudio/data/data_voc/annotations/’ # xml文件所在的目录
count_num(indir) # 调用函数统计各类标签数目
各类标签的数量分别为:
Motor Vehicle: 82329
Pedestrian: 11658
Non_motorized Vehicle: 12074
Traffic Light-Red Light: 1616
Traffic Light-Yellow Light: 139
Traffic Light-Green Light: 2213
Traffic Light-Off: 3922
可以发现在比赛数据里的这7个类中,黄灯的样本是最少的,从整体来看,与红绿灯相关的这4个实例数量都没有超过4千个样本,而另外3个实例的数量都超过了1.1万个样本。

总的来说就是样本不均衡的问题比较严重。

2.检测框高宽比分析
我们可以画一个检测框高宽比分布直方图,从而反应当前检测框款高比的分布情况。

In [59]
import os
from unicodedata import name
import xml.etree.ElementTree as ET
import glob

def ratio(indir):
# 提取xml文件列表
os.chdir(indir)
annotations = os.listdir(‘.’)
annotations = glob.glob(str(annotations) + ‘*.xml’)
# count_0, count_1, count_2, count_3 = 0, 0, 0, 0 # 举反例,不要这么写
count = [0 for i in range(20)]

for i, file in enumerate(annotations): # 遍历xml文件
    # actual parsing
    in_file = open(file, encoding = 'utf-8')
    tree = ET.parse(in_file)
    root = tree.getroot()

    # 遍历文件的所有检测框
    for obj in root.iter('object'):
        xmin = obj.find('bndbox').find('xmin').text
        ymin = obj.find('bndbox').find('ymin').text
        xmax = obj.find('bndbox').find('xmax').text
        ymax = obj.find('bndbox').find('ymax').text
        Aspect_ratio = (int(ymax)-int(ymin)) / (int(xmax)-int(xmin))
        if int(Aspect_ratio/0.25) < 19:
            count[int(Aspect_ratio/0.25)] += 1
        else:
            count[-1] += 1
sign = [0.25*i for i in range(20)]
plt.bar(x=sign, height=count)
print(count)

indir=‘/home/aistudio/data/data_voc/annotations/’ # xml文件所在的目录
ratio(indir)
[69, 9960, 27871, 28775, 13638, 6102, 4466, 4061, 4938, 3996, 3366, 2109, 1775, 959, 726, 3在这里插入图片描述
有同学可能会疑惑,为什么要算高宽比?

在学习目标检测的时候,锚框这个概念是必须要掌握的,这个概念也很好理解,举个例子你就知道了:在这里插入图片描述
这里用s表示放缩比;用r表示高宽比。很明显,当高宽比为1时,锚框是方的,就像中间这个绿色框一样;当长宽比为2时,锚框是一个竖长的矩形,就像图中这个紫色框;同理,当长宽比为0.5时,这个矩形框应该是又宽又扁的,就像图中这个红框一样。

因此,高宽比的设定,在目标检测中,是很重要的参数,但也是很多基础不好的同学容易忽略的一点。

注意:这里用高宽比来说明,但有些文章也会用宽高比,要注意区分,其实就是表示的方法不同,本质是一样的。

3.图像尺寸分析
图像尺寸的分析往往也是很多同学忽略的地方。

In [62]
import os
from unicodedata import name
import xml.etree.ElementTree as ET
import glob

def Image_size(indir):
# 提取xml文件列表
os.chdir(indir)
annotations = os.listdir(‘.’)
annotations = glob.glob(str(annotations) + ‘*.xml’)
width_heights = []

for i, file in enumerate(annotations): # 遍历xml文件
    # actual parsing
    in_file = open(file, encoding = 'utf-8')
    tree = ET.parse(in_file)
    root = tree.getroot()
    width = int(root.find('size').find('width').text)
    height = int(root.find('size').find('height').text)
    if [width, height] not in width_heights: width_heights.append([width, height])
print("数据集中,有{}种不同的尺寸,分别是:".format(len(width_heights)))
for item in width_heights:
    print(item)

indir=‘/home/aistudio/data/data_voc/annotations/’ # xml文件所在的目录
Image_size(indir)
数据集中,有4种不同的尺寸,分别是:
[1280, 720]
[1920, 1080]
[1920, 1088]
[1920, 1072]
可以发现,数据集中,并不是所有图像的尺寸都是固定的,这一点需要注意。

4.检测框中心分布分析
可以画一个检测框中心分布散点图,直观地反应检测框中心点在图像中的位置分布。

In [58]
import os
from unicodedata import name
import xml.etree.ElementTree as ET
import glob

def distribution(indir):
# 提取xml文件列表
os.chdir(indir)
annotations = os.listdir(‘.’)
annotations = glob.glob(str(annotations) + ‘*.xml’)
data_x, data_y = [], []

for i, file in enumerate(annotations): # 遍历xml文件
    # actual parsing
    in_file = open(file, encoding = 'utf-8')
    tree = ET.parse(in_file)
    root = tree.getroot()
    width = int(root.find('size').find('width').text)
    height = int(root.find('size').find('height').text)

    # 遍历文件的所有检测框
    for obj in root.iter('object'):
        xmin = int(obj.find('bndbox').find('xmin').text)
        ymin = int(obj.find('bndbox').find('ymin').text)
        xmax = int(obj.find('bndbox').find('xmax').text)
        ymax = int(obj.find('bndbox').find('ymax').text)
        x = (xmin + (xmax-xmin)) / width
        y = (ymin + (ymax-ymin)) / height
        data_x.append(x)
        data_y.append(y)
        
plt.scatter(data_x, data_y, s=1, alpha=0.1)

indir=‘/home/aistudio/data/data_voc/annotations/’ # xml文件所在的目录
distribution(indir)在这里插入图片描述
上面这张散点图很直观地展现了目标的位置:

从纵向(由上至下)看,目标基本上都处于一张图的中间位置,也就是说,天空和前车盖的那片区域是基本没有目标的;
从横向(由左到右)看,目标的分布还是比较广的,但目标还是会集中出现在中间区域。
四、数据优化思路
在第二章节里,介绍了数据集的操作方法,怎么转换数据格式相信对大家已经不是问题了;在第三章节里,分析了比赛的数据集,并简单地提了一下优化的方向。因此,在这一章节中,将重点介绍怎么根据刚刚的分析结果,对数据进行优化。

1.离线数据增强
数据增强对于同学们应该并不陌生,我们通常说的数据增强一般叫做在线数据增强,还有一种用的不多,但也很有用的数据增强方法,叫做离线数据增强。

离线数据增强其实就是将原数据变换后,保存下来,当作新的数据,这么一说相信大家马上就明白了。但是,如果只是普通的变换(比如调整亮度、旋转、缩放)这些很简单就能做到的变换,其实没必要将它们单独保存,毕竟也会消耗很多内存,效率不高。

那么,什么时候需要使用离线数据增强呢?还记得在第三章第一小节提到的各类别实例数量分析吗?在这一小节中,我们提到,与红绿灯相关的这类样本数量比较少,因此,我们可以手动地做一些处理,一图胜千言:在这里插入图片描述
处理好以后,我们再用第一章给大家介绍的方法,把数据格式转一下,再用一些标注工具(如:Labelimg)手动标几张就可以了。

2.在线数据增强
在线数据增强就是大家熟知的数据增强,大家在调参时应该没少对这块进行调整。数据增强在一定程度上确实有效,但是,想要模型效果好,我们应该有理有据地去调整。就以这个比赛数据为例:

数据里经常会受到光照、天气等因素影响画面亮度、对比度——ColorJitter(亮度、对比度、饱和度和色调)
车载摄像头有时会出现模糊的情况——albumentations库中的JpegCompression(降画质)、MotionaBlur(运动模糊)
数据集中有4种不同尺寸的图像,并且图像中的目标有大有小——RandomResize(随机缩放)
画面有时候会出现遮挡的情况——RandomCrop(随机裁剪)
同一个目标,左右翻转时,并不影响判断——RandomHorizontalFlip(随机水平翻转)
在使用某一种数据增强策略之前,可以像我一样说明一下原因,不能随意把全部策略都用上,举个反例:

如果数据里,红绿灯是那种有箭头指向的红绿灯就能使用翻转,因为经过翻转后,本来是左转的指示,可能就变成右转了。
3.从预测结果找问题
在比赛的数据中,经常会出现一些脏数据(错标、误标和漏标);另外,就算没有脏数据,通常也有一些数据是模型“学”起来比较困难的,这些数据需要我们单独拿出来分享。但是,比赛数据往往非常多,我们不可能一张一张地去检查,那么,有什么方法可以让我们高效地把这些数据找出来呢?在这里插入图片描述
我们可以基于训练集训练一个模型,然后再在验证集下对模型的效果进行检验,因为验证集是有标注的,所以可以算出得分(这个得分通常也称为评估指标,有些比赛用的评估指标可能不太一样,这里使用的是F1-score),为了知道每个图像的效果,需要在推理时将BatchSize调整为1,此时,一张图像就能对应一个分数。
我们可以将分数较低的图像单独拿出来进行分析,检查一下是不是标注的问题,又或者分析一下这张图像是不是很模糊,导致模型学起来很困难等等。
手动地将错标/漏标/误标的目标进行更正,或者是用离线数据增强的方法,以此来更新数据集,并重新训练模型,此时,将构成一个完整的闭环。
多次重复上述步骤后,数据的问题基本上就可以得到解决,至少不会在数据上出现问题。
五、总结与升华
目前的深度学习模型依赖大量的数据,因此,有这么一句话“数据往往是深度学习模型的天花板”,可见数据的重要性。

本文对目标检测任务中常见的数据处理方法、数据分析方法、数据优化方法进行阐述,希望更多刚入门的开发者能够更多地关注最基本的内容,尽快走出“调参侠”的阶段,好好夯实代码功底。

作者简介
北京联合大学 机器人学院 自动化专业 2018级 本科生 郑博培
中国科学院自动化研究所复杂系统管理与控制国家重点实验室实习生
百度飞桨北京领航团团长
百度飞桨开发者技术专家 PPDE
百度飞桨官方帮帮团、答疑团成员
深圳柴火创客空间 认证会员
百度大脑 智能对话训练师
阿里云人工智能、DevOps助理工程师
我在AI Studio上获得至尊等级,点亮10个徽章,来互关呀!!!
https://aistudio.baidu.com/aistudio/personalcenter/thirdview/147378在这里插入图片描述

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐