前言
代码仓库地址:https://github.com/Oneflow-Inc/one-yolov5欢迎star one-yolov5项目 获取最新的动态。如果您有问题,欢迎在仓库给我们提出宝贵的意见。如果对您有帮助,欢迎来给我Star呀~
由于文章中一些链接会被微信公众号吃掉,所以推荐访问文章的原始地址获得更好的阅读体验。https://start.oneflow.org/oneflow-yolo-doc/source_code_interpretation/utils/autoanchor_py.html
源码解读: utils/autoanchor.py
摘要
维度聚类(Dimension Clusters)。当把 YOLO 结合 anchor boxes 使用时,我们会遇到两个问题: 首先 anchor boxes 的尺寸是手工挑选的。虽然网络可以通过学习适当地调整 anchor boxes 形状,但是如果我们从一开始就为网络选择更好的 anchor boxes ,就可以让网络更容易学习并获得更好的检测结果。
image图1:VOC 和 COCO 上的聚类框尺寸。我们在边界框的维度(dimensions of bounding boxes) 上运行 K-means聚类,以获得我们模型良好的初始 anchor boxes 。左图显示了我们通过 k 的各种选择获得的 Avg IoU 。我们发现 k = 5 为召回与模型的复杂性提供了良好的折中。 右图显示了在 VOC 和 COCO 上聚类簇的相对中心, 并且这两种不同的 k 对应方案都喜欢更稀疏的,更高的框,此外在 COCO 的尺寸的变化比 VOC 更大。
我们不用手工选择 anchor boxes,而是在训练集的边界框上的维度上运行 K-means 聚类算法,自动找到良好的 anchor boxes 。 如果我们使用具有欧几里得距离的标准 K-means,那么较大的框会比较小的框产生更多的误差。 但我们真正想要的是独立于框的大小的,能获得良好的 IoU 分数的 anchor boxes 。 因此对于距离的度量方式我们使用:
我们用不同的 值运行 K-means算法,并绘制最接近聚类中心的平均 Avg IoU(见图1)。为了在模型复杂度和高召回率之间的良好折中,我们选择 k = 5 (也就是5种anchor boxes)簇的相对中心 与手工选取的 anchor boxes 显着不同,它有更少的短且宽的框,并且有更多既长又窄的框。
表1中,我们将聚类策略得到的 anchor boxes 和手工选取的 anchor boxes 在最接近的 Avg IoU 上进行比较。通过聚类策略得到的仅5种 anchor boxes 的 Avg IoU 为61.0,其性能类似于9个通过网络学习的 anchor boxes 的60.9 (即Avg IoU已经达到了Faster RCNN的水平)。 而且使用9种 anchor boxes 会得到更高的 Avg IoU 。这表明使用 K-means生成 anchor boxes 可以更好地表示模型并使其更容易学习。
表1: VOC 2007 上聚类得结果比使用手工选取的 anchor boxes 结果要好得多。
什么是K-means?
K-means是非常经典且有效的聚类方法,通过计算样本之间的距离(相似程度)将较近的样本聚为同一类别(簇)。
在 yolov5/one-yolov5 项目中使用 K-means 必须满足下面的条件:
使用K-means时主要关注两点
- 如何表示样本与样本之间的距离(核心问题),这个一般需要根据具体场景去设计,不同的方法聚类效果也不同,最常见的就是欧式距离,在目标检测领域常见的是IoU。
- 分为几类,这个也是需要根据应用场景取选择的,也是一个超参数。
K-means算法主要流程
- 手动设定簇的个数k,假设k=2;
- 在所有样本中随机选取k个样本作为簇的初始中心,如下图(random clusters)中两个黄色的小星星代表随机初始化的两个簇中心;
- 计算每个样本离每个簇中心的距离(这里以欧式距离为例),然后将样本划分到离它最近的簇中。如下图(step 0)用不同的颜色区分不同的簇;
- 更新簇的中心,计算每个簇中所有样本的均值(方法不唯一)作为新的簇中心。如下图(step 1)所示,两个黄色的小星星已经移动到对应簇的中心;
- 重复第3步到第4步直到簇中心不在变化或者簇中心变化很小满足给定终止条件。如下图(step2)所示,最终聚类结果。
什么是BPR?
BPR(BPR best possible recall来源于论文: FCOS.
原论文解释:
BPR is defined as the ratio of the number of ground-truth boxes a detector can recall at the most divided by all ground-truth boxes. A ground-truth box is considered being recalled if the box is assigned to at least one sample (i.e., a location in FCOS or an anchor box in anchor-based detectors) during training.
BPR (best possible recall): 最多能被召回的 ground truth 框数量 / 所有 ground truth 框数量。最大值为1 越大越好 小于0.98就需要使用K-means + 遗传进化算法选择出与数据集更匹配的anchor boxes框。
什么是白化操作whiten?
白化的目的是去除输入数据的冗余信息。假设训练数据是图像,由于图像中相邻像素之间具有很强的相关性,所以用于训练时输入是冗余的;白化的目的就是降低输入的冗余性。
输入数据集X,经过白化处理后,新的数据X’满足两个性质:
- 特征之间相关性较低;
- 所有特征具有相同的方差=1
常见的作法是:对每一个数据做一个标准差归一化处理(除以标准差)。scipy.cluster.vq.kmeans() 函数输入的数据就是必须是白化后的数据。相应输出的 anchor boxes 也是白化后的anchor,所以需要将anchor boxes 都乘以标准差恢复到原始图像尺度。
YOLOv5 中的 autoanchor.py 代码解析
1. 导入需要的包
importnumpyasnp#numpy矩阵操作模块
importoneflowasflow#OneFlow深度学习模块
importyaml#操作yaml文件模块
fromtqdmimporttqdm#Python进度条模块
fromutils.generalimportLOGGER,colorstr#日志模块
PREFIX=colorstr("AutoAnchor:")
2.check_anchor_order
这个函数用于确认当前anchors和stride的顺序是否是一致的,因为我们的m.anchors是相对各个feature map
(每个feature map的感受野不同 检测的目标大小也不同 适合的anchor大小也不同)所以必须要顺序一致 否则效果会很不好。
这个函数一般用于check_anchors最后阶段。
defcheck_anchor_order(m):
"""用在check_anchors函数的最后确定anchors和stride的顺序是一致的
CheckanchororderagainststrideorderforYOLOv5Detect()modulem,andcorrectifnecessary
:paramsm:model中的最后一层Detect层
"""
#CheckanchororderagainststrideorderforYOLOv5Detect()modulem,andcorrectifnecessary
#计算anchor的面积anchorarea[9]
a=m.anchors.prod(-1).mean(-1).view(-1)#meananchorareaperoutputlayer
#计算最大anchor与最小anchor面积差
da=a[-1]-a[0]#deltaa
#计算最大stride与最小stride差
#m.stride:modelstrides
#https://github.com/Oneflow-Inc/one-yolov5/blob/bf8c66e011fcf5b8885068074ffc6b56c113a20c/models/yolo.py#L144-L152
ds=m.stride[-1]-m.stride[0]#deltas
#flow.sign(x):当x大于/小于0时,返回1/-1
#如果这里anchor与stride顺序不一致,则重新调整顺序,但注意这里要抛出warning
ifdaand(da.sign()!=ds.sign()):#sameorder
LOGGER.info(f"{PREFIX}Reversinganchororder")
m.anchors[:]=m.anchors.flip(0)
3. kmean_anchors
这个函数才是这个这个文件的核心函数。功能:使用 K-means + 遗传算法 算出更符合当前数据集的anchors。
这里不仅仅使用了 K-means 聚类,还使用了 Genetic Algorithm 遗传算法,在 K-means 聚类的结果上进行 mutation(变异)。接下来简单介绍下代码流程:
- 载入数据集,得到数据集中所有数据的wh
- 将每张图片中wh的最大值等比例缩放到指定大小img_size,较小边也相应缩放
- 将bboxes从相对坐标改成绝对坐标(乘以缩放后的wh)
- 筛选bboxes,保留wh都大于等于两个像素的bboxes
- 使用K-means聚类得到n个anchors(调用K-means包 涉及一个白化操作)
- 使用遗传算法随机对anchors的wh进行变异,如果变异后效果变得更好(使用anchor_fitness方法计算得到的fitness(适应度)进行评估)就将变异后的结果赋值给anchors,如果变异后效果变差就跳过,默认变异1000次
不知道什么是遗传算法,可以看看这两个b站视频:传算法超细致+透彻理解和霹雳吧啦Wz
defkmean_anchors(path='./data/coco128.yaml',n=9,img_size=640,thr=4.0,gen=1000,verbose=True):
"""在check_anchors中调用
使用K-means+遗传算法算出更符合当前数据集的anchors
Createskmeans-evolvedanchorsfromtrainingdataset
:paramspath:数据集的路径/数据集本身
:paramsn:anchors的个数
:paramsimg_size:数据集图片约定的大小
:paramsthr:阈值由hyp['anchor_t']参数控制
:paramsgen:遗传算法进化迭代的次数(突变+选择)
:paramsverbose:是否打印所有的进化(成功的)结果默认传入是False,只打印最佳的进化结果
:returnk:K-means+遗传算法进化后的anchors
"""
fromscipy.cluster.vqimportkmeans
#注意一下下面的thr不是传入的thr,而是1/thr,所以在计算指标这方面还是和check_anchor一样
thr=1./thr#0.25
prefix=colorstr('autoanchor:')
defmetric(k,wh):#computemetrics
"""用于print_results函数和anchor_fitness函数
计算ratiometric:整个数据集的groundtruth框与anchor对应宽比和高比即:gt_w/k_w,gt_h/k_h+x+best_x用于后续计算BPR+aat
注意我们这里选择的metric是groundtruth框与anchor对应宽比和高比而不是常用的iou这点也与nms的筛选条件对应是yolov5中使用的新方法
:paramsk:anchor框
:paramswh:整个数据集的wh[N,2]
:returnx:[N,9]N个groundtruth框与所有anchor框的宽比或高比(两者之中较小者)
:returnx.max(1)[0]:[N]N个groundtruth框与所有anchor框中的最大宽比或高比(两者之中较小者)
"""
#[N,1,2]/[1,9,2]=[N,9,2]N个gt_wh和9个anchor的k_wh宽比和高比
#两者的重合程度越高就越趋近于1远离1(<1 或 >1)重合程度都越低
r=wh[:,None]/k[None]
#r=gt_height/anchor_heightgt_width/anchor_width有可能大于1,也可能小于等于1
#flow.min(r,1./r):[N,9,2]将所有的宽比和高比统一到<=1
#.min(2):value=[N,9]选出每个groundtruth个和anchor的宽比和高比最小的值index:[N,9]这个最小值是宽比(0)还是高比(1)
#[0]返回value[N,9]每个groundtruth个和anchor的宽比和高比最小的值就是所有groundtruth与anchor重合程度最低的
x=flow.min(r,1./r).min(2)[0]#ratiometric
#x=wh_iou(wh,flow.tensor(k))#IoUmetric
#x.max(1)[0]:[N]返回每个groundtruth和所有anchor(9个)中宽比/高比最大的值
returnx,x.max(1)[0]#x,best_x
defanchor_fitness(k):#mutationfitness
"""用于kmean_anchors函数
适应度计算优胜劣汰用于遗传算法中衡量突变是否有效的标注如果有效就进行选择操作,无效就继续下一轮的突变
:paramsk:[9,2]K-means生成的9个anchorswh:[N,2]:数据集的所有groundtruth框的宽高
:return(best*(best>thr).float()).mean()=适应度计算公式[1]注意和BPR有区别这里是自定义的一种适应度公式
返回的是输入此时anchork对应的适应度
"""
_,best=metric(flow.tensor(k,dtype=flow.float32),wh)
return(best*(best>thr).float()).mean()#fitness
defprint_results(k):
"""用于kmean_anchors函数中打印K-means计算相关信息
计算BPR、aat=>打印信息:阈值+BPR+aatanchor个数+图片大小+metric_all+best_mean+past_mean+Kmeans聚类出来的anchor框(四舍五入)
:paramsk:K-means得到的anchork
:returnk:input
"""
#将K-means得到的anchork按面积从小到大排序
k=k[np.argsort(k.prod(1))]
#x:[N,9]N个groundtruth框与所有anchor框的宽比或高比(两者之中较小者)
#best:[N]N个groundtruth框与所有anchor框中的最大宽比或高比(两者之中较小者)
x,best=metric(k,wh0)
#(best>thr).float():True=>1.False->0..mean():求均值
#BPR(bestpossiblerecall):最多能被召回(通过thr)的groundtruth框数量/所有groundtruth框数量[1]0.96223小于0.98才会用K-means计算anchor
#aat(anchorsabovethreshold):[1]3.54360每个target平均有多少个anchors
BPR,aat=(best>thr).float().mean(),(x>thr).float().mean()*n#bestpossiblerecall,anch>thr
f=anchor_fitness(k)
#print(f'{prefix}thr={thr:.2f}:{BPR:.4f}bestpossiblerecall,{aat:.2f}anchorspastthr')
#print(f'{prefix}n={n},img_size={img_size},metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best,'
#f'past_thr={x[x>thr].mean():.3f}-mean:',end='')
print(f"aat:{aat:.5f},fitness:{f:.5f},bestpossiblerecall:{BPR:.5f}")
fori,xinenumerate(k):
print('%i,%i'%(round(x[0]),round(x[1])),end=','ifi< len(k) - 1else'
')#usein*.cfg
returnk
#载入数据集
ifisinstance(path,str):#*.yamlfile
withopen(path)asf:
data_dict=yaml.safe_load(f)#modeldict
fromutils.datasetsimportLoadImagesAndLabels
dataset=LoadImagesAndLabels(data_dict['train'],augment=True,rect=True)
else:
dataset=path#dataset
#得到数据集中所有数据的wh
#将数据集图片的最长边缩放到img_size,较小边相应缩放
shapes=img_size*dataset.shapes/dataset.shapes.max(1,keepdims=True)
#将原本数据集中gtboxes归一化的wh缩放到shapes尺度
wh0=np.concatenate([l[:,3:5]*sfors,linzip(shapes,dataset.labels)])
#统计gtboxes中宽或者高小于3个像素的个数,目标太小发出警告
i=(wh0< 3.0).any(1).sum()
ifi:
print(f'{prefix}WARNING:Extremelysmallobjectsfound.{i}of{len(wh0)}labelsare< 3 pixels in size.')
#筛选出label大于2个像素的框拿来聚类,[...]内的相当于一个筛选器,为True的留下
wh=wh0[(wh0>=2.0).any(1)]#filter>2pixels
#wh=wh*(np.random.rand(wh.shape[0],1)*0.9+0.1)#multiplybyrandomscale0-1
#Kmeans聚类方法:使用欧式距离来进行聚类
print(f'{prefix}Runningkmeansfor{n}anchorson{len(wh)}gtboxes...')
#计算宽和高的标准差->[w_std,h_std]
s=wh.std(0)#sigmasforwhitening
#开始聚类,仍然是聚成n类,返回聚类后的anchorsk(这个anchorsk是白化后数据的anchor框s)
#另外还要注意的是这里的kmeans使用欧式距离来计算的
#运行K-means的次数为30次obs:传入的数据必须先白化处理'whitenoperation'
#白化处理:新数据的标准差=1降低数据之间的相关度,不同数据所蕴含的信息之间的重复性就会降低,网络的训练效率就会提高
#白化操作参考博客:https://blog.csdn.net/weixin_37872766/article/details/102957235
k,dist=kmeans(wh/s,n,iter=30)#points,meandistance
assertlen(k)==n,print(f'{prefix}ERROR:scipy.cluster.vq.kmeansrequested{n}pointsbutreturnedonly{len(k)}')
k*=s#k*s得到原来数据(白化前)的anchor框
wh=flow.tensor(wh,dtype=flow.float32)#filteredwh
wh0=flow.tensor(wh0,dtype=flow.float32)#unfilteredwh0
#输出新算的anchorsk相关的信息
k=print_results(k)
#Plotwh
#k,d=[None]*20,[None]*20
#foriintqdm(range(1,21)):
#k[i-1],d[i-1]=kmeans(wh/s,i)#points,meandistance
#fig,ax=plt.subplots(1,2,figsize=(14,7),tight_layout=True)
#ax=ax.ravel()
#ax[0].plot(np.arange(1,21),np.array(d)**2,marker='.')
#fig,ax=plt.subplots(1,2,figsize=(14,7))#plotwh
#ax[0].hist(wh[wh[:,0]<100, 0], 400)
#ax[1].hist(wh[wh[:,1]<100, 1], 400)
#fig.savefig('wh.png',dpi=200)
#Evolve类似遗传/进化算法变异操作
npr=np.random#随机工具
#f:fitness0.62690
#sh:(9,2)
#mp:突变比例mutationprob=0.9s:sigma=0.1
f,sh,mp,s=anchor_fitness(k),k.shape,0.9,0.1#fitness,generations,mutationprob,sigma
pbar=tqdm(range(gen),desc=f'{prefix}EvolvinganchorswithGeneticAlgorithm:')#progressbar
#根据聚类出来的n个点采用遗传算法生成新的anchor
for_inpbar:
#重复1000次突变+选择选择出1000次突变里的最佳anchork和最佳适应度f
v=np.ones(sh)#v[9,2]全是1
while(v==1).all():
#产生变异规则mutateuntilachangeoccurs(preventduplicates)
#npr.random(sh)< mp: 让v以90%的比例进行变异 选到变异的就为1 没有选到变异的就为0
v=((npr.random(sh)< mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3,3.0)
#变异(改变这一时刻之前的最佳适应度对应的anchork)
kg=(k.copy()*v).clip(min=2.0)
#计算变异后的anchorkg的适应度
fg=anchor_fitness(kg)
#如果变异后的anchorkg的适应度>最佳适应度k就进行选择操作
iffg>f:
#选择变异后的anchorkg为最佳的anchork变异后的适应度fg为最佳适应度f
f,k=fg,kg.copy()
#打印信息
pbar.desc=f'{prefix}EvolvinganchorswithGeneticAlgorithm:fitness={f:.4f}'
ifverbose:
print_results(k)
returnprint_results(k)
4. check_anchors
这个函数是通过计算BPR确定是否需要改变anchors 需要就调用K-means重新计算anchors。
defcheck_anchors(dataset,model,thr=4.0,imgsz=640):
#Checkanchorfittodata,recomputeifnecessary
"""用于train.py中
通过BPR确定是否需要改变anchors需要就调用K-means重新计算anchors
Checkanchorfittodata,recomputeifnecessary
:paramsdataset:自定义数据集LoadImagesAndLabels返回的数据集
:paramsmodel:初始化的模型
:paramsthr:超参中得到界定anchor与label匹配程度的阈值
:paramsimgsz:图片尺寸默认640
"""
#从model中取出最后一层(Detect)
m=model.module.model[-1]ifhasattr(model,"module")elsemodel.model[-1]#Detect()
#dataset.shapes.max(1,keepdims=True)=每张图片的较长边
#shapes:将数据集图片的最长边缩放到img_size,较小边相应缩放得到新的所有数据集图片的宽高[N,2]
shapes=imgsz*dataset.shapes/dataset.shapes.max(1,keepdims=True)
#产生随机数scale[img_size,1]
scale=np.random.uniform(0.9,1.1,size=(shapes.shape[0],1))#augmentscale
#[6301,2]所有target(6301个)的wh基于原图大小shapes*scale:随机化尺度变化
wh=flow.tensor(np.concatenate([l[:,3:5]*sfors,linzip(shapes*scale,dataset.labels)])).float()#wh
defmetric(k):#computemetric
"""用在check_anchors函数中computemetric
根据数据集的所有图片的wh和当前所有anchorsk计算BPR(bestpossiblerecall)和aat(anchorsabovethreshold)
:paramsk:anchors[9,2]wh:[N,2]
:returnBPR:bestpossiblerecall最多能被召回(通过thr)的groundtruth框数量/所有groundtruth框数量小于0.98才会用K-means计算anchor
:returnaat:anchorsabovethreshold每个target平均有多少个anchors
"""
#None添加维度所有target(gt)的whwh[:,None][6301,2]->[6301,1,2]
#所有anchor的whk[None][9,2]->[1,9,2]
#r:target的高h宽w与anchor的高h_a宽w_a的比值,即h/h_a,w/w_a[6301,9,2]有可能大于1,也可能小于等于1
r=wh[:,None]/k[None]
#x高宽比和宽高比的最小值无论r大于1,还是小于等于1最后统一结果都要小于1[6301,9]
x=flow.min(r,1/r).min(2)[0]#ratiometric
#best[6301]为每个groundtruth框选择匹配所有anchors宽高比例值最好的那一个比值
best=x.max(1)[0]#best_x
#aat(anchorsabovethreshold)每个target平均有多少个anchors
aat=(x>1/thr).float().sum(1).mean()#anchorsabovethreshold
#BPR(bestpossiblerecall)=最多能被召回(通过thr)的groundtruth框数量/所有groundtruth框数量小于0.98才会用K-means计算anchor
BPR=(best>1/thr).float().mean()#bestpossiblerecall
returnBPR,aat
stride=m.stride.to(m.anchors.device).view(-1,1,1)#modelstrides
#anchors:[N,2]所有anchors的宽高基于缩放后的图片大小(较长边为640较小边相应缩放)
anchors=m.anchors.clone()*stride#currentanchors
BPR,aat=metric(anchors.cpu().view(-1,2))
s=f"
{PREFIX}{aat:.2f}anchors/target,{BPR:.3f}BestPossibleRecall(BPR)."
#考虑这9类anchor的宽高和groundtruth框的宽高之间的差距,如果BPR<0.98(说明当前anchor不能很好的匹配数据集 ground truth 框)就会根据K-means算法重新聚类新的anchor
ifBPR>0.98:#thresholdtorecompute
LOGGER.info(f"{s}Currentanchorsareagoodfittodataset")
else:
LOGGER.info(f"{s}Anchorsareapoorfittodataset,attemptingtoimprove...")
na=m.anchors.numel()//2#numberofanchors
try:
#如果BPR<0.98(最大为1 越大越好) 使用K-means + 遗传进化算法选择出与数据集更匹配的anchors框 [9, 2]
anchors=kmean_anchors(dataset,n=na,img_size=imgsz,thr=thr,gen=1000,verbose=False)
exceptExceptionase:
LOGGER.info(f"{PREFIX}ERROR:{e}")
#计算新的anchors的new_BPR
new_BPR=metric(anchors)[0]
#比较K-means+遗传进化算法进化后的anchors的new_BPR和原始anchors的BPR
#注意:这里并不一定进化后的BPR必大于原始anchors的BPR,因为两者的衡量标注是不一样的进化算法的衡量标准是适应度而这里比的是BPR
ifnew_BPR>BPR:#replaceanchors
anchors=flow.tensor(anchors,device=m.anchors.device).type_as(m.anchors)
#替换m的anchor_grid[9,2]->[3,1,3,1,1,2]
m.anchors[:]=anchors.clone().view_as(m.anchors)
#检查anchor顺序和stride顺序是否一致不一致就调整
#因为我们的m.anchors是相对各个featuremap所以必须要顺序一致否则效果会很不好
check_anchor_order(m)#mustbeinpixel-space(notgrid-space)
m.anchors/=stride
s=f"{PREFIX}Done(optional:updatemodel*.yamltousetheseanchorsinthefuture)"
else:
s=f"{PREFIX}Done(originalanchorsbetterthannewanchors,proceedingwithoriginalanchors)"
LOGGER.info(s)
这个函数会在train.py中调用:
总结
K-means是非常经典且有效的聚类方法,通过计算样本之间的距离(相似程度)将较近的样本聚为同一类别(簇)。
审核编辑 :李倩
-
代码
+关注
关注
30文章
4787浏览量
68589 -
聚类算法
+关注
关注
2文章
118浏览量
12127 -
K-means
+关注
关注
0文章
28浏览量
11296
原文标题:YOLOv5 中的 autoanchor.py 代码解析
文章出处:【微信号:GiantPandaCV,微信公众号:GiantPandaCV】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论