之前基于yoloV2做过一段时间的物体检测,当时对yoloV2整个框架了如指掌。由于没有及时写笔记记录一下,现在回过头来很多东西都快忘光了。本篇博客不会记录如何使用darknet训练yoloV2,而更注重的的是整个yoloV2的算法,以及其中的一些细节。废话不多说,我们直接开始。
物体检测训练主函数
yoloV2的训练函数主函数在examples/detector.c中的train_detector函数,这里就不贴代码了,太长了。我们主要看看整体的流程。
函数一开始为初始化网络,或者导入预训练参数,以及其他设置,如gpu,以及根据使用的gpu个数设置学习率。
接着就开始训练,具体的:
1、确定网络输入大小
yoloV2的训练过程有一个很重要的点,那就是多尺度图像训练。我们都知道yoloV2的输入图片大小是416*416,但是在训练的时候并不是一直使用416*416的图片进行训练,而是每隔一段时间就改变输入图像的大小,其策略为:
int dim = (rand() % 10 + 10) * 32;
if (get_current_batch(net)+200 > net.max_batches) dim = 608;//最后200批次训练都只用输入大小为608*608
2、训练样本读取
代码在src/data.c中的load_data_detection函数中。
yoloV2在读取样本的时候对数据进行了扩充,分别有随机抖动,镜像和亮度、饱和度、曝光度的调整。其中随机抖动和镜像需要对box也进行相应的调整。
3、训练
根据使用的gpu个数进行训练,调用的是train_networks函数。
如果gpu个数大于1,那么每隔100个训练批次就进行参数合并。
region_layer
yoloV2的训练过程除了多尺度图片训练方式,以及数据扩充方式,另一个重点是最后的检测层region_layer。相应的代码在src/region_layrer.c中的forward_region_layer函数里面。
假设网络的输出是416*416,那么yoloV2的输出是尺寸是13*13;类别数为80,候选框的个数为5,那么通道数为(1+4+80)*5=425。每个候选框占85个通道,即第0到84通道属于第一个候选框,第85到169通道属于第二个候选框,以此类推。然后在每85个通道里面,存放顺序为:bbox(占4个通道)、是否是物体(占1个通道)以及类别数量(80个类别)。
另外yoloV2中利用聚类算法得到的anchor中,其w和h没有超过13*13。这是因为yoloV2,在聚类anchor时是将图片和bbox,resize到416*416,然后进行聚类。最后将聚类出来的anchor的w和h除以32,因为yoloV2的网络将416*416的图片缩小到13*13。这里就有个问题了,上面提到yoloV2有进行多尺度输入训练,也就是输入图片不一定是416*416。那么由416*416计算出来的anchor是否也应该一起调整呢?从作者的训练代码看似乎没有进行相应的调整。
1、sigmoid和softmax
region_layer一开始先将,是否物体和bbox相关的通道经过sigmoid激活函数,物体类别通道经过softmax:
for (b = 0; b < l.batch; ++b){
for(n = 0; n < l.n; ++n){
int index = entry_index(l, b, n*l.w*l.h, 0);//第b个batch,第n个anchor的bbox起始位置
activate_array(l.output + index, 2*l.w*l.h, LOGISTIC);//将13*13个x,y偏移量通道经过sigmoid激活函数
index = entry_index(l, b, n*l.w*l.h, l.coords);//获取是否是物体的起始位置
if(!l.background) activate_array(l.output + index, l.w*l.h, LOGISTIC);//13*13个是否是物体通道经过sigmoid激活函数
}
}
int index = entry_index(l, 0, 0, l.coords + !l.background);//获取物体类别起始位置
softmax_cpu(net.input + index, l.classes + l.background, l.batch*l.n, l.inputs/l.n, l.w*l.h, 1, l.w*l.h, 1, l.output + index);//将13*13中的每80个通道经过softmax函数
2、负样本处理
接下来是对13*13*5的候选框进行处理,主要是处理负样本,具体流程如下:
1、对13*13*5中的每个候选框,根据anchor的w和h计算出归一化后的bbox,用pred变量表示。
box pred = get_region_box(l.output, l.biases, n, box_index, i, j, l.w, l.h, l.w*l.h);
2、对于pred这个或许框,从当前训练样本的所有真实bbox计算出最大的iou,用best_iou变量表示。
for(t = 0; t < 30; ++t){
box truth = float_to_box(net.truth + t*(l.coords + 1) + b*l.truths, 1);
if(!truth.x) break;
float iou = box_iou(pred, truth);
if (iou > best_iou) {
best_iou = iou;
}
}
3、直接计算该候选框的不是物体的误差,即直接认为是负样本。
l.delta[obj_index] = l.noobject_scale * (0 - l.output[obj_index]);
4、如果best_iou大于某一阈值,比如0.6,则任务这个候选框是正样本,并且将步骤3计算的不是物体的误差改成0
if (best_iou > l.thresh) {
l.delta[obj_index] = 0;
}
5、如果网络已经训练过的样本小于12800个,那么就是将第n个anchor设置为真实bbox,并计算误差。
if(*(net.seen) < 12800){
box truth = {0};
truth.x = (i + .5)/l.w;
truth.y = (j + .5)/l.h;
truth.w = l.biases[2*n]/l.w;
truth.h = l.biases[2*n+1]/l.h;
delta_region_box(truth, l.output, l.biases, n, box_index, i, j, l.w, l.h, l.delta, .01, l.w*l.h);
}
这一步主要是想让网络开始训练的时候,13*13网格中的5个候选框都先接近对应的anchor。
根据anchor和网络输出计算预测的bbox代码如下:
box get_region_box(float *x, float *biases, int n, int index, int i, int j, int w, int h, int stride)
{
box b;
//计算x,y,13*13网格中第i,j位置分别加上预测的偏移量并除以13,将其归一化。
b.x = (i + x[index + 0*stride]) / w;
b.y = (j + x[index + 1*stride]) / h;
//计算w,h。根据log(b.w/anchor_n_w)=x[index+2*stride]计算出b.w,然后在除以13,将其归一化。anchor_n_w表示第n个anchor的w。
b.w = exp(x[index + 2*stride]) * biases[2*n] / w;
b.h = exp(x[index + 3*stride]) * biases[2*n+1] / h;
return b;
}
3、正样本处理
在处理完13*13*5个候选框后,开始处理训练样本的所有真实bbox。具体如下:
1、对每个真实的bbox,计算出其在13*13网格中的位置:
i = (truth.x * l.w);
j = (truth.y * l.h);
2、找出5个候选框中与该真实bbox的iou最大的候选框。
for(n = 0; n < l.n; ++n){
int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0);
box pred = get_region_box(l.output, l.biases, n, box_index, i, j, l.w, l.h, l.w*l.h);
if(l.bias_match){//用anchor来计算iou,而不是预测的bbox
pred.w = l.biases[2*n]/l.w;
pred.h = l.biases[2*n+1]/l.h;
}
//printf("pred: (%f, %f) %f x %f\n", pred.x, pred.y, pred.w, pred.h);
pred.x = 0;
pred.y = 0;
float iou = box_iou(pred, truth_shift);
if (iou > best_iou){
best_iou = iou;
best_n = n;
}
}
3、真实bbox和该候选框一起计算bbox回归误差。
l.delta[obj_index] = l.object_scale * (1 - l.output[obj_index]);
if (l.rescore) {//默认使用rescore
l.delta[obj_index] = l.object_scale * (iou - l.output[obj_index]);
}
4、计算该候选框是物体的误差
5、计算类别误差
在计算是否是物体的误差,bbox误差以及类别误差都会乘以相应的权重:
object_scale=5
noobject_scale=1
class_scale=1
coord_scale=1
另外,我们看看bbox误差的计算:
float tx = (truth.x*w - i);//计算真实bbox的x,与i的偏移量
float ty = (truth.y*h - j);//计算真实bbox的y,与j的偏移量
float tw = log(truth.w*w / biases[2*n]);//计算真实bbox的w与anchor的w的比值的对数
float th = log(truth.h*h / biases[2*n + 1]);//计算真实bbox的h与anchor的h的比值的对数
//下面就是真实bbox的x,y偏移量和w,h与anchor的w,h的比值的对数,跟网络输出计算mse误差了。
delta[index + 0*stride] = scale * (tx - x[index + 0*stride]);
delta[index + 1*stride] = scale * (ty - x[index + 1*stride]);
delta[index + 2*stride] = scale * (tw - x[index + 2*stride]);
delta[index + 3*stride] = scale * (th - x[index + 3*stride]);
从上面的负样本和正样本处理来看,yoloV2并没有进行难例子挖掘。而是直接将所有iou小于0.6的候选框当做负样本,正样本个数与真实bbox的个数一样。其他的iou大于0.6但不是正样本的候选框就不会更新是否是物体的误差和类别误差,而只是更新其与对应的anchor的bbox误差。