这里主要参考github上面的yolo源代码解析,如侵则删,虽然以yolov2作为分析对象,但yolov3较yolov2并没有改变多少
1.训练过程主要函数
入口函数:
void train_detector(char *datacfg, char *cfgfile, char *weightfile, int *gpus, int ngpus, int clear)
{
// 读入数据配置文件信息
list *options = read_data_cfg(datacfg);
// 从options找出训练图片路径信息,如果没找到,默认使用"data/train.list"路径下的图片信息(train.list含有标准的信息格式:<object-class> <x> <y> <width> <height>),
// 该文件可以由darknet提供的scripts/voc_label.py根据自行在网上下载的voc数据集生成,所以说是默认路径,其实也需要使用者自行调整,也可以任意命名,不一定要为train.list,
// 甚至可以不用voc_label.py生成,可以自己不厌其烦的制作一个(当然规模应该是很小的,不然太累了。。。)
// 读入后,train_images将含有训练图片中所有图片的标签以及定位信息
char *train_images = option_find_str(options, "train", "data/train.list");
char *backup_directory = option_find_str(options, "backup", "/backup/");
/*srand函数是随机数发生器的初始化函数。srand和rand()配合使用产生伪随机数序列。
rand函数在产生随机数前,需要系统提供的生成伪随机数序列的种子,rand根据这个种子的值产生一系列随机数。
如果系统提供的种子没有变化,每次调用rand函数生成的伪随机数序列都是一样的。*/
srand(time(0));
/*第三个参数是:`cfg/yolo.train.cfg`,`basecfg()`这个函数把`cfg/yolo.train.cfg`
变成了`yolo0train.cfg`,然后用base指针指向`yolo0train.cfg`*/
// 提取配置文件名称中的主要信息,用于输出打印(并无实质作用),
// 比如提取cfg/yolo.cfg中的yolo,用于下面的输出打印
char *base = basecfg(cfgfile);
printf("%s\n", base);//打印"yolo"字样
float avg_loss = -1;
network *nets = calloc(ngpus, sizeof(network));
srand(time(0));
int seed = rand();
int i;
// for循环次数为ngpus,使用多少块GPU,就循环多少次(不使用GPU时,ngpus=1,也会循环一次)
// 这里每一次循环都会构建一个相同的神经网络,如果提供了初始训练参数,也会为每个网络导入相同的初始训练参数
for(i = 0; i < ngpus; ++i){
srand(seed);
#ifdef GPU
// 设置当前活跃GPU卡号(即设置gpu_index=n,同时调用cudaSetDevice函数设置当前活跃的GPU卡号)
cuda_set_device(gpus[i]);
#endif
nets[i] = load_network(cfgfile, weightfile, clear);//解析网络构架,加载预训练参数
nets[i].learning_rate *= ngpus;
}
srand(time(0));
network net = nets[0];
/*imgs是一次加载到内存的图像数量,
如果占内存太大的话可以把subdivisions调大或者batch调小一点
在imgs = net.batch * net.subdivisions * ngpus中,
net.batch并不是cfg文件中的batch值,
而是cfg文件中的batch/net.subdivisions,
这样以来,一次加载imgs张图片到内存,
while循环中每次count,就会处理完这些图片,完成一次迭代。
*/
int imgs = net.batch * net.subdivisions * ngpus;
printf("Learning Rate: %g, Momentum: %g, Decay: %g\n", net.learning_rate, net.momentum, net.decay);
data train, buffer;
layer l = net.layers[net.n - 1];
int classes = l.classes;
//jitter是什么意思呢?可以参考这篇博客:
//[非均衡数据集处理:利用抖动(jittering)生成额外数据]
float jitter = l.jitter;
list *plist = get_paths(train_images);
//int N = plist->size;
char **paths = (char **)list_to_array(plist);
load_args args = get_base_args(net);
args.coords = l.coords;
args.paths = paths;
args.n = imgs;//n就是一次加载到内存中的图片数量
args.m = plist->size;//m是待训练图片的总数量
args.classes = classes;
args.jitter = jitter;
args.num_boxes = l.max_boxes;
args.d = &buffer;
args.type = DETECTION_DATA;
//args.type = INSTANCE_DATA;
args.threads = 8;
//调节图片旋转角度、曝光度、饱和度、色调等,来增加图片数量
// args.angle = net.angle;
// args.exposure = net.exposure;
// args.saturation = net.saturation;
// args.hue = net.hue;
pthread_t load_thread = load_data(args);
clock_t time;
int count = 0;
//while(i*imgs < N*120){
while(get_current_batch(net) < net.max_batches){
/*进行10次迭代后,调整一次网络大小
resize网络是yolo v2版本新加的功能。
即每进行10次迭代就会resize一次网络输入图片的宽和高,
这样保证了网络可以试音各种不同尺度的目标,
这样以来,即使没有dropout层,
训练出来的网络也不会过拟合*/
if(l.random && count++%10 == 0){
printf("Resizing\n");
int dim = (rand() % 10 + 10) * 32;
if (get_current_batch(net)+200 > net.max_batches) dim = 608;
//int dim = (rand() % 4 + 16) * 32;
printf("%d\n", dim);
//网络输入图片的宽高可调节,dim最小为320,
// 最大为618,这样可以更好使用多尺度的目标
args.w = dim;
args.h = dim;
pthread_join(load_thread, 0);
train = buffer;
free_data(train);
load_thread = load_data(args);
for(i = 0; i < ngpus; ++i){
resize_network(nets + i, dim, dim);
}
net = nets[0];
}
time=clock();
pthread_join(load_thread, 0);
train = buffer;
load_thread = load_data(args);
/*
int k;
for(k = 0; k < l.max_boxes; ++k){
box b = float_to_box(train.y.vals[10] + 1 + k*5);
if(!b.x) break;
printf("loaded: %f %f %f %f\n", b.x, b.y, b.w, b.h);
}
*/
/*
int zz;
for(zz = 0; zz < train.X.cols; ++zz){
image im = float_to_image(net.w, net.h, 3, train.X.vals[zz]);
int k;
for(k = 0; k < l.max_boxes; ++k){
box b = float_to_box(train.y.vals[zz] + k*5, 1);
printf("%f %f %f %f\n", b.x, b.y, b.w, b.h);
draw_bbox(im, b, 1, 1,0,0);
}
show_image(im, "truth11");
cvWaitKey(0);
save_image(im, "truth11");
}
*/
printf("Loaded: %lf seconds\n", sec(clock()-time));
time=clock();
float loss = 0;
#ifdef GPU
if(ngpus == 1){
loss = train_network(net, train);//开始训练
} else {
loss = train_networks(nets, ngpus, train, 4);
}
#else
loss = train_network(net, train);
#endif
if (avg_loss < 0) avg_loss = loss;
avg_loss = avg_loss*.9 + loss*.1;
i = get_current_batch(net);
printf("%ld: %f, %f avg, %f rate, %lf seconds, %d images\n", get_current_batch(net), loss, avg_loss, get_current_rate(net), sec(clock()-time), i*imgs);
if(i%100==0){
#ifdef GPU
if(ngpus != 1) sync_nets(nets, ngpus, 0);
#endif
char buff[256];
sprintf(buff, "%s/%s.backup", backup_directory, base);
save_weights(net, buff);
}
//保存权重
if(i%10000==0 || (i < 1000 && i%100 == 0)){
#ifdef GPU
if(ngpus != 1) sync_nets(nets, ngpus, 0);
#endif
char buff[256];
sprintf(buff, "%s/%s_%d.weights", backup_directory, base, i);
save_weights(net, buff);
}
free_data(train);
}
#ifdef GPU
if(ngpus != 1) sync_nets(nets, ngpus, 0);
#endif
char buff[256];
sprintf(buff, "%s/%s_final.weights", backup_directory, base);
save_weights(net, buff);
}
上面这个函数首先会通过调用load_network()
函数里的parse_network_cfg(cfg)
解析配置文件建立某层网络
网络训练:
float train_network_datum(network net)//位于network.c
{
// 如果使用GPU则调用gpu版的train_network_datum
#ifdef GPU
if(gpu_index >= 0) return train_network_datum_gpu(net);
#endif
// 更新目前已经处理的图片数量:每次处理一个batch,故直接添加l.batch
*net.seen += net.batch;
// 标记处于训练阶段
net.train = 1;
forward_network(net);
backward_network(net);
float error = *net.cost;
// 只有((*net.seen)/net.batch)%net.subdivisions == 0时才会更新网络参数
if(((*net.seen)/net.batch)%net.subdivisions == 0) update_network(net);
return error;
}
其中:
forward_network:前向传播
backward_network:反向传播
2.前向传播
这个函数主要通过调用各层的l.forward(l, net)
来逐层构建前向传播的计算
/** 前向计算网络net每一层的输出.
* @param net 构建好的整个网络模型
* @details 遍历net的每一层网络,从第0层到最后一层,逐层计算每层的输出
*/
void forward_network(network net)
{
int i;
/// 遍历所有层,从第一层到最后一层,逐层进行前向传播(网络总共有net.n层)
for(i = 0; i < net.n; ++i){
/// 置网络当前活跃层为当前层,即第i层
net.index = i;
/// 获取当前层
layer l = net.layers[i];
/// 如果当前层的l.delta已经动态分配了内存,则调用fill_cpu()函数,将其所有元素的值初始化为0
if(l.delta){
/// 第一个参数为l.delta的元素个数,第二个参数为初始化值,为0
fill_cpu(l.outputs * l.batch, 0, l.delta, 1);
}
//调用当前层的前向传播函数
l.forward(l, net);
//当前层的输出等于后一层的输入,保存在net.input
net.input = l.output;
//这里似乎一直为0
if(l.truth) {
net.truth = l.output;
}
}
calc_network_cost(net);
}
- 对于全连接层,l.outputs为配置文件中直接设置,卷积层,由图像的尺寸、卷积核尺寸以及跨度计算输出特征图的宽度和高度
- 前向传播过程中,每层由net.input读取上一层的输出,具体可以看每一层的前向传播函数里的调用,每层网络的输入输出如图:
这里主要分析最后一层region_layer.c
region层主要用来计算损失和敏感度图
δ
δ
region层主要输入为
tx,ty,tw,th,tc,tclassi
t
x
,
t
y
,
t
w
,
t
h
,
t
c
,
t
c
l
a
s
s
i
,这里i=0,1,,,类别总数
输入最后用于输出bounding box的坐标预测,有无对象预测,类别预测,而bounding box的预测方法,是使用聚类产生anchor box的长宽(配置文件里的anchor属性,对应下式的
pw
p
w
和
ph
p
h
)和
tw,th
t
w
,
t
h
,来预测
bw,bh
b
w
,
b
h
。用
tw,th
t
w
,
t
h
来预测
bx,by
b
x
,
b
y
,具体计算公式如下:
ph,pw:
p
h
,
p
w
:
配置文件里的anchor对应的两个值
cx,cy:
c
x
,
c
y
:
cell距特征图左上角的距离(以cell宽度为单位)
region层主要工作过程:
1.对输入进一步处理,得到最终输出(此处
σ
σ
都为sigmoid函数):
Ox=σ(tx)
O
x
=
σ
(
t
x
)
Oy=σ(ty)
O
y
=
σ
(
t
y
)
Ow=tw
O
w
=
t
w
Oh=th
O
h
=
t
h
Oc=σ(tc)
O
c
=
σ
(
t
c
)
Oclassi=softmax(tclassi)
O
c
l
a
s
s
i
=
s
o
f
t
m
a
x
(
t
c
l
a
s
s
i
)
2.计算region层的敏感度图
δ
δ
敏感度图
δ
δ
(又名误差图)主要用于反向传播的计算,具体见yolo反向传播
首先根据上面计算出的输出
O
O
,同时设标签为T, 列出损失函数:
Cy=∑[−TylogOy−(1−Ty)log(1−Oy))]
C
y
=
∑
[
−
T
y
l
o
g
O
y
−
(
1
−
T
y
)
l
o
g
(
1
−
O
y
)
)
]
论文说
tx,ty
t
x
,
t
y
为方差损失,但看代码表现的却是交叉熵损失
Cw=12∑(Ow−Tw)2
C
w
=
1
2
∑
(
O
w
−
T
w
)
2
Ch=12∑(Oh−Th)2
C
h
=
1
2
∑
(
O
h
−
T
h
)
2
Cc=∑[−TclogOc−(1−Tc)log(1−Oc))]
C
c
=
∑
[
−
T
c
l
o
g
O
c
−
(
1
−
T
c
)
l
o
g
(
1
−
O
c
)
)
]
Cclassi=∑[−TclassilogOclassi−(1−Tclassi)log(1−Oclassi))]
C
c
l
a
s
s
i
=
∑
[
−
T
c
l
a
s
s
i
l
o
g
O
c
l
a
s
s
i
−
(
1
−
T
c
l
a
s
s
i
)
l
o
g
(
1
−
O
c
l
a
s
s
i
)
)
]
1)若损失为交叉熵(这里激活函数对应logistic):
δi=∂Ci∂zi=∂C∂Oi∂Oi∂zi=(−TiOi−Ti−11−Oi)∂Oi∂zi=Oi−TiOi(1−Oi)∂Oi∂zi
δ
i
=
∂
C
i
∂
z
i
=
∂
C
∂
O
i
∂
O
i
∂
z
i
=
(
−
T
i
O
i
−
T
i
−
1
1
−
O
i
)
∂
O
i
∂
z
i
=
O
i
−
T
i
O
i
(
1
−
O
i
)
∂
O
i
∂
z
i
又,这里的交叉熵损失输入和输出关系都为
Oi=σ(zi)
O
i
=
σ
(
z
i
)
所以,
∂Oi∂zi=Oi(1−Oi)
∂
O
i
∂
z
i
=
O
i
(
1
−
O
i
)
最终,
δi=Oi−Ti
δ
i
=
O
i
−
T
i
2)若损失为方差(这里激活函数对应relu类):
δi=∂Ci∂zi=Oi−Ti
δ
i
=
∂
C
i
∂
z
i
=
O
i
−
T
i
可以看出,这里无论交叉熵损失还是方差损失.最终的敏感图都是
Oi−Ti
O
i
−
T
i
,而代码里也是直接算出
Oi−Ti
O
i
−
T
i
,赋值到l.delta
,为后面反向传播使用
void forward_region_layer(const layer l, network net)
{
int i,j,b,t,n;
/// 将net.input中的元素全部拷贝至l.output中
memcpy(l.output, net.input, l.outputs*l.batch*sizeof(float));
/// 这个#ifndef预编译指令没有必要用的,因为forward_region_layer()函数本身就对应没有定义gpu版的,
// 所以肯定会执行其中的语句,
/// 其中的语句的作用是为了计算region_layer层的输出l.output
#ifndef GPU
/// 遍历batch中的每张图片(l.output含有整个batch训练图片对应的输出)
for (b = 0; b < l.batch; ++b){
/// 注意region_layer层中的l.n含义是每个cell grid(网格)中预测的矩形框个数(不是卷积层中卷积核的个数)
for(n = 0; n < l.n; ++n){
/// 获取 某一中段首个x的地址(中段的含义参考entry_idnex()函数的注释),此处仅用两层循环处理所有的输入,直观上应该需要四层的,
/// 即还需要两层遍历l.w和l.h(遍历每一个网格),但实际上并不需要,因为每次循环,其都会处理一个中段内某一小段的数据,这一小段数据
/// 就包含所有网格的数据。比如处理第1个中段内所有x和y(分别有l.w*l.h个x和y).
//具体实现见注1
int index = entry_index(l, b, n*l.w*l.h, 0);
/// 注意第二个参数是2*l.w*l.h,也就是从index+l.output处开始,对之后2*l.w*l.h个元素进行logistic激活函数处理,
//只对tx,ty作激活处理,不对tw,th作激活,原因见 注2
activate_array(l.output + index, 2*l.w*l.h, LOGISTIC);
/// 和上面一样,此处是获取一个中段内首个自信度信息c值的地址,而后对该中段内所有的c值(该中段内共有l.w*l.h个c值)进行logistic激活函数处理
index = entry_index(l, b, n*l.w*l.h, 4);
activate_array(l.output + index, l.w*l.h, LOGISTIC);
}
}
// 这里只有用到tree格式训练的数据才会调用,一般yolov2不调用,即l.softmax_tree==0
if (l.softmax_tree){
int i;
int count = 5;
for (i = 0; i < l.softmax_tree->groups; ++i) {
int group_size = l.softmax_tree->group_size[i];
softmax_cpu(net.input + count, group_size, l.batch, l.inputs, l.n*l.w*l.h, 1, l.n*l.w*l.h, l.temperature, l.output + count);
count += group_size;
}
} else if (l.softmax){
//获取l.output中首个类别概率C1的地址,
// 而后对l.output中所有的类别概率(共有l.batch*l.n*l.w*l.h*l.classes个)进行softmax函数处理,
int index = entry_index(l, 0, 0, 5);
/// net.input+index为region_layer的输入(加上索引偏移量index)
/// l.classes-->n,物体种类数,对应softmax_cpu()中输入参数n;
/// l.batch*l.n-->batch,一个batch的图片张数乘以每个网格预测的矩形框数,得到值可以这么理解:所有batch数据(net.input)可以分成的中段的总段数,
/// 该参数对应softmax_cpu()中输入参数batch;
/// l.inputs/l.n-->batch_offset,注意l.inputs仅是一张训练图片输入到region_layer的元素个数,l.inputs/l.n得到的值实际是一个小段的元素个数
/// (即所有网格中某个矩形框的所有参数个数),对应softmax_cpu()中输入参数batch_offset参数;
/// l.w*l.h-->groups,对应softmax_cpu()中输入参数groups;
/// softmax_cpu()中输入参数group_offset值恒为1;
/// l.w*l.h-->stride,对应softmax_cpu()中输入参数stride;
/// softmax_cpu()中输入参数temp的值恒为1;
/// l.output+index为region_layer的输出(同样加上索引偏移量index,region_layer的输入与输出元素一一对应);
/// 详细说一下这里的过程(对比着softmax_cpu()及其调用的softmax()函数来说明):
// softmax_cpu()中的第一层for循环遍历了batch次,即遍历了所有中段;
/// 第二层循环遍历了groups次,也即l.w*l.h次,实际上遍历了所有网格;
// 而后每次调用softmax()实际上会处理一个网格某个矩形框的所有类别概率,因此可以在
/// softmax()函数中看到,遍历的次数为n,也即l.classes的值;在softmax()函数中,
// 用上了跨度stride,其值为l.w*l.h,之所以用到跨度,是因为net.input
/// 和l.output的存储方式,详见entry_index()函数的注释,由于每次调用softmax(),
// 需要处理一个矩形框所有类别的概率,这些概率值都是分开存储的,间隔
/// 就是stride=l.w*l.h。
// 这样,softmax_cpu()的两层循环以及softmax()中遍历的n次合起来就会处理得到
// l.output中所有l.batch*l.n*l.w*l.h*l.classes个
/// 概率类别值。(此处提到的中段,小段等名词都需参考entry_index()的注释,尤其是l.output数据的存储方式,
// 只有熟悉了此处才能理解好,另外再次强调一下,
/// region_layer的输入和输出元素个数是一样的,一一对应,因此其存储方式也是一样的)
softmax_cpu(net.input + index, l.classes, l.batch*l.n, l.inputs/l.n, l.w*l.h, 1, l.w*l.h, 1, l.output + index);
}
#endif
// 敏感度图清零
memset(l.delta, 0, l.outputs * l.batch * sizeof(float));
/// 如果不是训练过程,则返回不再执行下面的语句(前向推理即检测过程也会调用这个函数,这时就不需要执行下面训练时才会用到的语句了)
if(!net.train) return;
float avg_iou = 0; ///< 平均IoU(Intersection over Union)
float recall = 0; ///< 召回率
float avg_cat = 0; ///<
float avg_obj = 0; ///<
float avg_anyobj = 0; ///< 一张训练图片所有预测矩形框的平均自信度(矩形框中含有物体的概率),该参数没有实际用处,仅用于输出打印
int count = 0; // 该batch内检测的target数
int class_count = 0;
*(l.cost) = 0;
for (b = 0; b < l.batch; ++b) {
if(l.softmax_tree){//yolov2 不执行
int onlyclass = 0;
/// 循环30次,每张图片固定处理30个矩形框
for(t = 0; t < 30; ++t){
/// 通过移位来获取每一个真实矩形框的信息,net.truth存储了网络吞入的所有图片的真实矩形框信息(一次吞入一个batch的训练图片),
/// net.truth作为这一个大数组的首地址,l.truths参数是每一张图片含有的真实值参数个数(可参考layer.h中的truths参数中的注释),
/// b是batch中已经处理完图片的图片的张数,5是每个真实矩形框需要5个参数值(也即每条矩形框真值有5个参数),t是本张图片已经处理
/// 过的矩形框的个数(每张图片最多处理30张图片),明白了上面的参数之后对于下面的移位获取对应矩形框真实值的代码就不难了。
// net.truth存储格式:x,y,w,h,c,x,y,w,h,c,....
box truth = float_to_box(net.truth + t*5 + b*l.truths, 1);
/// 这个if语句是用来判断一下是否有读到真实矩形框值(每个矩形框有5个参数,float_to_box只读取其中的4个定位参数,
/// 只要验证x的值不为0,那肯定是4个参数值都读取到了,要么全部读取到了,要么一个也没有),另外,因为程序中写死了每张图片处理30个矩形框,
/// 那么有些图片没有这么多矩形框,就会出现没有读到的情况。
//遍历完所有真实box则跳出循环
if(!truth.x) break;
/// float_to_box()中没有读取矩形框中包含的物体类别编号的信息,就在此处获取。(darknet中,物体类别标签值为编号,
/// 每一个类别都有一个编号值,这些物体具体的字符名称存储在一个文件中,如data/*.names文件,其所在行数就是其编号值)
int class = net.truth[t*5 + b*l.truths + 4];
float maxp = 0;
int maxi = 0;
if(truth.x > 100000 && truth.y > 100000){
for(n = 0; n < l.n*l.w*l.h; ++n){
int class_index = entry_index(l, b, n, 5);
int obj_index = entry_index(l, b, n, 4);
float scale = l.output[obj_index];
l.delta[obj_index] = l.noobject_scale * (0 - l.output[obj_index]);
float p = scale*get_hierarchy_probability(l.output + class_index, l.softmax_tree, class, l.w*l.h);
if(p > maxp){
maxp = p;
maxi = n;
}
}
int class_index = entry_index(l, b, maxi, 5);
int obj_index = entry_index(l, b, maxi, 4);
delta_region_class(l.output, l.delta, class_index, class, l.classes, l.softmax_tree, l.class_scale, l.w*l.h, &avg_cat);
if(l.output[obj_index] < .3) l.delta[obj_index] = l.object_scale * (.3 - l.output[obj_index]);
else l.delta[obj_index] = 0;
++class_count;
onlyclass = 1;
break;
}
}
if(onlyclass) continue;
}
//
/**
* 下面三层主循环的顺序,与最后一层输出的存储方式有关。外层循环遍历所有行,中层循环遍历所有列,这两层循环合在一起就是按行遍历
* region_layer输出中的每个网格,内层循环l.n表示的是每个grid cell中预测的box数目。首先要明白这一点,region_layer层的l.w,l.h
* 就是指最后图片划分的网格(grid cell)数,也就是说最后一层输出的图片被划分成了l.w*l.h个网格,每个网格中预测l.n个矩形框
* (box),每个矩形框就是一个潜在的物体,每个矩形框中包含了矩形框定位信息x,y,w,h,含有物体的自信度信息c,以及属于各类的概率,
* 最终该网格挑出概率最大的作为该网格中含有的物体,当然最终还需要检测,如果其概率没有超过一定阈值,那么判定该网格中不含物体。
* 搞清楚这点之后,理解下面三层循环就容易一些了,外面两层循环是在遍历每一个网格,内层循环则遍历每个网格中预测的所有box。
* 除了这三层主循环之后,里面还有一个循环,循环次数固定为30次,这个循环是遍历一张训练图片中30个真实的矩形框(30是指定的一张训练
* 图片中最多能够拥有的真实矩形框个数)。知道了每一层在遍历什么,那么这整个4层循环合起来是用来干什么的呢?与紧跟着这4层循环之后
* 还有一个固定30次的循环在功能上有什么区别呢?此处这四层循环目的在于
*/
for (j = 0; j < l.h; ++j) {
for (i = 0; i < l.w; ++i) {
for (n = 0; n < l.n; ++n) {
/// 根据i,j,n计算该矩形框的索引,实际是矩形框中存储的x参数在l.output中的索引,矩形框中包含多个参数,x是其存储的首个参数,
/// 所以也可以说是获取该矩形框的首地址。更为详细的注释,参考entry_index()的注释。
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);
/// 最高IoU,赋初值0
float best_iou = 0;
/// 为什么这里总是看到30?查看layer.h中关于max_boxes变量的注释就知道了,每张图片最多能够有30真实个框;
/// 还要说明的一点是,此处所说最多处理30个矩形框,是指真实值中,一张图片含有的最大真实物体标签数,
/// 也即真实的物体矩形框个数最大为30,并不是模型预测限制最多就30个,如上注释,一个图片如果分成7*7个网格,
/// 每个网格预测两个矩形框,那就有98个了,所以不是指模型只能预测30个。模型你可以尽管预测多的矩形框,
/// 只是我默认一张图片中最多就打了30个物体的标签,所以之后会有滤除过程。滤除过程有两层,首先第一层就是
/// 下面的for循环了,
for(t = 0; t < 30; ++t){
/// 通过移位来获取每一个真实矩形框的信息,net.truth存储了网络吞入的所有图片的真实矩形框信息(一次吞入一个batch的训练图片),
/// net.truth作为这一个大数组的首地址,l.truths参数是每一张图片含有的真实值参数个数(可参考layer.h中的truths参数中的注释),
/// b是batch中已经处理完图片的图片的张数,5是每个真实矩形框需要5个参数值(也即每条矩形框真值有5个参数),t是本张图片已经处理
/// 过的矩形框的个数(每张图片最多处理30张图片),明白了上面的参数之后对于下面的移位获取对应矩形框真实值的代码就不难了。
box truth = float_to_box(net.truth + t*5 + b*l.truths, 1);
/// 这个if语句是用来判断一下是否有读到真实矩形框值(每个矩形框有5个参数,float_to_box只读取其中的4个定位参数,
/// 只要验证x的值不为0,那肯定是4个参数值都读取到了,要么全部读取到了,要么一个也没有),另外,因为程序中写死了每张图片处理30个矩形框,
/// 那么有些图片没有这么多矩形框,就会出现没有读到的情况。
if(!truth.x) break;
/// 获取完真实标签矩形定位坐标后,与模型检测出的矩形框求IoU,具体参考box_iou()函数注释
float iou = box_iou(pred, truth);
/// 找出最大的IoU值
if (iou > best_iou) {
best_iou = iou;
}
}
/// 获取当前遍历矩形框含有物体的自信度信息c(该矩形框中的确存在物体的概率)在l.output中的索引值
int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4);
/// 叠加每个预测矩形框的自信度c(也即每个矩形框中含有物体的概率)// 有目标的概率
avg_anyobj += l.output[obj_index];
//这里为tc输入的敏感度图计算,也是(标签-输出)
l.delta[obj_index] = l.noobject_scale * (0 - l.output[obj_index]);
/// 上面30次循环使得本矩形框已经与训练图片中所有30个(30个只是最大值,可能没有这么多)真实矩形标签进行了对比,只要在这30个中
/// 找到一个真实矩形标签与该预测矩形框的iou大于指定的阈值,则判定该
if (best_iou > l.thresh) {
l.delta[obj_index] = 0;
}
// net.seen 已训练样本的个数,希望其具有稳定性,因为真实情况,不是每个grid cell都有标注,
// 标注的,后面会重新计算delta,感觉像对所有的cell的delta初始化
if(*(net.seen) < 12800){
box truth = {0}; // 当前cell为中心对应的第n个anchor的box
truth.x = (i + .5)/l.w;// cell的中点 // 对应tx=0.5
truth.y = (j + .5)/l.h;//ty=0.5
truth.w = l.biases[2*n]/l.w;//相对于feature map的大小 // tw=0
truth.h = l.biases[2*n+1]/l.h;//th=0
delta_region_box(truth, l.output, l.biases, n, box_index, i, j, l.w, l.h, l.delta, .01, l.w*l.h);
}
}
}
}
///
for(t = 0; t < 30; ++t){
box truth = float_to_box(net.truth + t*5 + b*l.truths, 1);
if(!truth.x) break;
float best_iou = 0;
int best_n = 0;
// 强制转化为最后的feature map的索引
i = (truth.x * l.w);
j = (truth.y * l.h);
//printf("%d %f %d %f\n", i, truth.x*l.w, j, truth.y*l.h);
box truth_shift = truth;
truth_shift.x = 0;
truth_shift.y = 0;
//printf("index %d %d\n",i, j);
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){
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;
// 最优iou对应的anchor索引,然后使用该anchor预测的predict box计算与真实box的误差
best_n = n;
}
}
//printf("%d %f (%f, %f) %f x %f\n", best_n, best_iou, truth.x, truth.y, truth.w, truth.h);
// 对应predict预测的confidence
int box_index = entry_index(l, b, best_n*l.w*l.h + j*l.w + i, 0);
// 对所有标签,同个cell里,预测boc和标签的iou的最大值,这个函数也是计算(标签-输出)作为敏感度图
float iou = delta_region_box(truth, l.output, l.biases, best_n, box_index, i, j, l.w, l.h, l.delta, l.coord_scale * (2 - truth.w*truth.h), l.w*l.h);
// 注意这里的关于box的损失权重
if(l.coords > 4){// 不执行
int mask_index = entry_index(l, b, best_n*l.w*l.h + j*l.w + i, 4);
delta_region_mask(net.truth + t*(l.coords + 1) + b*l.truths + 5, l.output, l.coords - 4, mask_index, l.delta, l.w*l.h, l.mask_scale);
}
if(iou > .5)
recall += 1;// 如果iou> 0.5, 认为找到该目标,召回数+1
avg_iou += iou;
//l.delta[best_index + 4] = iou - l.output[best_index + 4];
int obj_index = entry_index(l, b, best_n*l.w*l.h + j*l.w + i, 4);
avg_obj += l.output[obj_index];
//有目标时的损失,部分覆盖之前的无目标损失
l.delta[obj_index] = l.object_scale * (1 - l.output[obj_index]);
if (l.rescore) {//定义了rescore表示同时对confidence score进行回归
l.delta[obj_index] = l.object_scale * (iou - l.output[obj_index]);
}
if(l.background){//不执行
l.delta[obj_index] = l.object_scale * (0 - l.output[obj_index]);
}
int class = net.truth[t*5 + b*l.truths + 4];// 真实类别
/// 参考layer.h中关于map的注释:将coco数据集的物体类别编号,变换至在联合9k数据集中的物体类别编号,
/// 如果l.map不为NULL,说明使用了yolo9000检测模型,其他模型不用这个参数(没有联合多个数据集训练),
/// 目前只有yolo9000.cfg中设置了map文件所在路径。
if (l.map) class = l.map[class];//不执行
int class_index = entry_index(l, b, best_n*l.w*l.h + j*l.w + i, 5);//预测的class向量首地址
delta_region_class(l.output, l.delta, class_index, class, l.classes, l.softmax_tree, l.class_scale, l.w*l.h, &avg_cat);
++count;
++class_count;
}
}
//printf("\n");
*(l.cost) = pow(mag_array(l.delta, l.outputs * l.batch), 2);
printf("Region Avg IOU: %f, Class: %f, Obj: %f, No Obj: %f, Avg Recall: %f, count: %d\n",
avg_iou/count, avg_cat/class_count, avg_obj/count, avg_anyobj/(l.w*l.h*l.n*l.batch), recall/count, count);
}
注:
1.这里的entry_index()
主要是计算某个矩形框中某个参数在l.output中的索引
int entry_index(layer l, int batch, int location, int entry)
{
int n = location / (l.w*l.h);
int loc = location % (l.w*l.h);
return batch*l.outputs + n*l.w*l.h*(l.coords+l.classes+1) + entry*l.w*l.h + loc;
}
举个例子来说明l.output数据格式:
有输入和参数如图:
则数据分布如图:
注意这里预测框0尾部和预测框首部是接在一起的
反向传播分析见博客yolo反向传播源码分析
主要参考:
论文 - YOLO v3
yolo详细中文注释