非常有幸参与本次百度21天零基础深度学习课程,本课程是基于百度飞桨平台完成,非常方便使用AIStudio进行在线学习和实践。作为课程结束的一个作业,举行AI识虫比赛。
赛题背景
目标检测是计算机视觉中的一个重要的应用方向,与之相关的应用也越来越多。百度飞桨与北京林业大学合作开发的AI识虫项目,是将AI与农业相结合的典型案例。本次比赛将使用林业病虫数据集,使用目标检测算法对图片中的虫子类别和位置进行预测。
在《零基础实践深度学习课程》中,老师讲介绍如何使用YOLO-V3算法检测虫子,但老师所讲授的内容只包含最基本的功能。学员需要在此基础上对模型参数或者模型结构进行调整,以得到更好的评估结果。主要改进方案参见下面的指导说明。
指导说明
课堂上老师会讲述YOLO-V3模型,教案里面给出的是最基础的模型介绍及实现,鼓励同学们在此基础上进行调优,主要改进方案有:
1、 使用其它模型如faster rcnn等 (难度系数5)
2、 使用数据增多,可以对原图进行翻转、裁剪等操作 (难度系数3)
3、 修改anchor参数的设置,教案中的anchor参数设置直接使用原作者在coco数据集上的设置,针对此模型是否要调整 (难度系数3)
4、 调整优化器、学习率策略、正则化系数等是否能提升模型精度 (难度系数1)
比赛任务
参赛者需要训练好目标检测模型,并且用训练好的模型在测试数据集上进行预测,每张图片的预测输出结果为图片中包含的虫子的 类别、位置、和置信度得分 。
作为一个深入学习的初学者,选择改进方案的2、3、4作为入手点。
改进方案2,数据增广在课程目标识别的文档中已经实现了旋转、裁剪、缩放、填充、亮暗、对比度调整等操作,在代码中这些操作都是随机出现。通过数据增广,mAP成绩有显著提高。
改进方案2中,课程基础代码中锚框(AnchorBox)锚框是使用的YOLOV-3作者给出的设置,通过课程基础代码训练出来的结果发现对于一些虫子目标未能够识别出位置,说明锚框选择不太好。使用了K-means算法对训练数据集进行聚类,最终得到比较合理的锚框选择。
代码参考https://blog.csdn.net/m_buddy/article/details/82926024
# -*- coding=utf-8 -*-
import glob
import os
import sys
import xml.etree.ElementTree as ET
import numpy as np
from kmeans import kmeans, avg_iou
# 根文件夹
ROOT_PATH = './work/insects/train/'
# 聚类的数目
CLUSTERS = 9
# 模型中图像的输入尺寸,默认是一样的
SIZE = 1
INSECT_NAMES = ['Boerner', 'Leconte', 'Linnaeus',
'acuminatus', 'armandi', 'coleoptera', 'linnaeus']
def get_insect_names():
"""
return a dict, as following,
{'Boerner': 0,
'Leconte': 1,
'Linnaeus': 2,
'acuminatus': 3,
'armandi': 4,
}
It can map the insect name into an integer label.
"""
insect_category2id = {}
for i, item in enumerate(INSECT_NAMES):
insect_category2id[item] = i
return insect_category2id
def get_annotations(cname2cid, datadir):
filenames = os.listdir(os.path.join(datadir, 'annotations', 'xmls'))
records = []
ct = 0
for fname in filenames:
fid = fname.split('.')[0]
fpath = os.path.join(datadir, 'annotations', 'xmls', fname)
img_file = os.path.join(datadir, 'images', fid + '.jpeg')
tree = ET.parse(fpath)
if tree.find('id') is None:
im_id = np.array([ct])
else:
im_id = np.array([int(tree.find('id').text)])
objs = tree.findall('object')
im_w = float(tree.find('size').find('width').text)
im_h = float(tree.find('size').find('height').text)
gt_bbox = np.zeros((len(objs), 4), dtype=np.float32)
gt_class = np.zeros((len(objs), ), dtype=np.int32)
is_crowd = np.zeros((len(objs), ), dtype=np.int32)
difficult = np.zeros((len(objs), ), dtype=np.int32)
for i, obj in enumerate(objs):
cname = obj.find('name').text
gt_class[i] = cname2cid[cname]
_difficult = int(obj.find('difficult').text)
x1 = float(obj.find('bndbox').find('xmin').text)
y1 = float(obj.find('bndbox').find('ymin').text)
x2 = float(obj.find('bndbox').find('xmax').text)
y2 = float(obj.find('bndbox').find('ymax').text)
x1 = max(0, x1)
y1 = max(0, y1)
x2 = min(im_w - 1, x2)
y2 = min(im_h - 1, y2)
# 这里使用xywh格式来表示目标物体真实框
gt_bbox[i] = [(x1+x2)/2.0 , (y1+y2)/2.0, x2-x1+1., y2-y1+1.]
is_crowd[i] = 0
difficult[i] = _difficult
voc_rec = {
'im_file': img_file,
'im_id': im_id,
'h': im_h,
'w': im_w,
'is_crowd': is_crowd,
'gt_class': gt_class,
'gt_bbox': gt_bbox,
'gt_poly': [],
'difficult': difficult
}
if len(objs) != 0:
records.append([x2-x1+1. , y2-y1+1.])
print(x2-x1+1.,"----------",y2-y1+1.)
ct += 1
return np.array(records)
# 加载YOLO格式的标注数据
def load_dataset(path):
jpegimages = os.path.join(path, 'images')
if not os.path.exists(jpegimages):
print('no JPEGImages folders, program abort')
sys.exit(0)
path = path+"annotations"
print(path)
labels_txt = os.path.join(path, 'xmls')
if not os.path.exists(labels_txt):
print('no labels folders, program abort')
sys.exit(0)
label_file = os.listdir(labels_txt)
print('label count: {}'.format(len(label_file)))
dataset = []
for label in label_file:
with open(os.path.join(labels_txt, label), 'r') as f:
txt_content = f.readlines()
for line in txt_content:
line_split = line.split(' ')
roi_with = float(line_split[len(line_split)-2])
roi_height = float(line_split[len(line_split)-1])
if roi_with == 0 or roi_height == 0:
continue
dataset.append([roi_with, roi_height])
# print([roi_with, roi_height])
return np.array(dataset)
cname2cid = get_insect_names()
data = get_annotations(cname2cid, ROOT_PATH)
#data = load_dataset(ROOT_PATH)
out = kmeans(data, k=CLUSTERS)
print(out)
print("Accuracy: {:.2f}%".format(avg_iou(data, out) * 100))
print("Boxes:\n {}-{}".format(out[:, 0] * SIZE, out[:, 1] * SIZE))
ratios = np.around(out[:, 0] / out[:, 1], decimals=2).tolist()
print("Ratios:\n {}".format(sorted(ratios)))
k-means算法代码:
import numpy as np
def iou(box, clusters):
"""
Calculates the Intersection over Union (IoU) between a box and k clusters.
:param box: tuple or array, shifted to the origin (i. e. width and height)
:param clusters: numpy array of shape (k, 2) where k is the number of clusters
:return: numpy array of shape (k, 0) where k is the number of clusters
"""
x = np.minimum(clusters[:, 0], box[0])
y = np.minimum(clusters[:, 1], box[1])
if np.count_nonzero(x == 0) > 0 or np.count_nonzero(y == 0) > 0:
raise ValueError("Box has no area")
intersection = x * y
box_area = box[0] * box[1]
cluster_area = clusters[:, 0] * clusters[:, 1]
iou_ = intersection / (box_area + cluster_area - intersection)
return iou_
def avg_iou(boxes, clusters):
"""
Calculates the average Intersection over Union (IoU) between a numpy array of boxes and k clusters.
:param boxes: numpy array of shape (r, 2), where r is the number of rows
:param clusters: numpy array of shape (k, 2) where k is the number of clusters
:return: average IoU as a single float
"""
return np.mean([np.max(iou(boxes[i], clusters)) for i in range(boxes.shape[0])])
def translate_boxes(boxes):
"""
Translates all the boxes to the origin.
:param boxes: numpy array of shape (r, 4)
:return: numpy array of shape (r, 2)
"""
new_boxes = boxes.copy()
for row in range(new_boxes.shape[0]):
new_boxes[row][2] = np.abs(new_boxes[row][2] - new_boxes[row][0])
new_boxes[row][3] = np.abs(new_boxes[row][3] - new_boxes[row][1])
return np.delete(new_boxes, [0, 1], axis=1)
def kmeans(boxes, k, dist=np.median):
"""
Calculates k-means clustering with the Intersection over Union (IoU) metric.
:param boxes: numpy array of shape (r, 2), where r is the number of rows
:param k: number of clusters
:param dist: distance function
:return: numpy array of shape (k, 2)
"""
rows = boxes.shape[0]
distances = np.empty((rows, k))
last_clusters = np.zeros((rows,))
np.random.seed()
# the Forgy method will fail if the whole array contains the same rows
clusters = boxes[np.random.choice(rows, k, replace=False)]
while True:
for row in range(rows):
distances[row] = 1 - iou(boxes[row], clusters)
nearest_clusters = np.argmin(distances, axis=1)
if (last_clusters == nearest_clusters).all():
break
for cluster in range(k):
clusters[cluster] = dist(boxes[nearest_clusters == cluster], axis=0)
last_clusters = nearest_clusters
return clusters
由于KMeans算法的结果对于初始点的选取敏感,因而每次运行的结果并不相同。
改进方案4,基础代码中对学习率和学习率策略,正则化系数的选择已经算是不错的选择,尝试更改学习率策略,反而得出更差的结果。
对于训练结果,epoch还是挺重要的,课程基础代码中MAX_epoch设置为200,训练时间大概需要10个小时,能够得出一个不错的结果。为了缩短训练时间,之后选择50进行训练,虽然进行了改进方案的优化,但最终结果也没有提升多少。
作为一个深度学习的初学者,通过百度飞桨21天的带学活动,让我对深度学习有了初步的了解,飞桨这个平台对于初学者非常友好,不用顾忌繁杂的平台搭建,通过简单的示例实践就能学习到不少东西。