在darknet的配置文件有两个参数:batch和subdivisions是比较令人费解的,如下所示。一般地,batch就是一次输入多少图片到神经网络中来计算loss,并反向update gradients。 但在darknet代码里面,含意稍微有些不同。
下面以batch=64,subdivisions=16为例,并结合代码来分析它们的真实意思。
首先在训练真正开始前,会根据cfg文件来搭建网络结构,其函数为parse_network_cfg(...)。在该函数里面会调用parse_net_options(...)来读取cfg中最顶上网络参数的值,这些参数值包括网络大小,学习率,当然也包括batch和subdivisions。
void parse_net_options(list *options, network *net)
{
net->batch = option_find_int(options, "batch",1);
net->learning_rate = option_find_float(options, "learning_rate", .001);
net->momentum = option_find_float(options, "momentum", .9);
net->decay = option_find_float(options, "decay", .0001);
int subdivs = option_find_int(options, "subdivisions",1);
net->time_steps = option_find_int_quiet(options, "time_steps",1);
net->notruth = option_find_int_quiet(options, "notruth",0);
net->batch /= subdivs;
net->batch *= net->time_steps;
net->subdivisions = subdivs;
net->random = option_find_int_quiet(options, "random", 0);
... ...
}
从上面代码看到,net->batch, subdivs先从cfg中读取,比如说分别为64和16,重点来了,然后在倒数第4行,对net->batch做了一个除以subdivis转换,现在变成了4,然后在倒数第3行又做了一个乘net->time_steps的运算。
先解释time_steps。darknet其实是一个开源深度学习框架,它不仅仅支持yolov3目标检测,同时还可以分类,分割甚至RNN,LSTM等。只是后面这些功能用的人不多而已。。。 而time_steps是RNN中的概念,即在更新梯度时,不仅仅考虑当前的输入的一个batch,而且还考虑前面time_steps个batch,所以才在后面又对net->batch做了乘time_steps的运算。当然我们这里是目标检测,不用考虑以前的历史数据,自然time_steps等于1.
小结一下,在parse_net_options(...)中,经过上面几个转换,net->batch最终值为64/16=4, 而subdiviions为16.
然后开始读取images准备开始训练,每轮读取图片的数目为下面代码所示
int imgs = net->batch * net->subdivisions * ngpus;
这个代码很有疑惑性,如果不知道前面的转换,那么就很容易得到 imgs=64x16x1(假定单gpu,则ngpus=1 ),subdivision变成乘法因子啦???。其实错啦,这里的net->batch为4, 所以一个batch读取 的图片数目还是64 (=4x16x1)。
小结一下,在darknet代码中,net->batch值是恒为cfg中的batch值 / subdivision。所以cfg中的batch是指一次性读取多少张图片,而net->batch则是被subdivision分割成的小batch。
有了这个结论,我们继续看代码中 net->batch 和subdivision到底是怎么来用的。
float train_network(network *net, data d)
{
assert(d.X.rows % net->batch == 0);
int batch = net->batch;
int n = d.X.rows / batch;
int i;
float sum = 0;
for(i = 0; i < n; ++i){
get_next_batch(d, batch, i*batch, net->input, net->truth);
float err = train_network_datum(net);
sum += err;
}
return (float)sum/(n*batch);
}
上面 代码中,batch为net->batch,假设为4, 那么n=64/4=16即subdivision值。换句话说,subdivision为16的话,即将普通batch数目分割成16等份,每份的图片数目为4(即net->batch)。
接下来在for循环中,训练16次,每次训练4张图片。所以绕了半天,虽然每次读取了64张图片,但实际每次参加训练计算loss的图片数目只有net->batch张,即4张。同时要注意的是, 在 train_network_datum(net)中有行代码表明,16次训练完后才update 网络权值的。
if(((*net->seen)/net->batch)%net->subdivisions == 0) update_network(net);
至此,已经比较绕了, 梳理一下,实际上网络是net->batch张图片进行训练(前向推理和反向传播),但是升级权值是在cfg中batch数目结束后进行的。它把平常所说的mini-batch SGD的batch分割成两个部分含义。前面是小batch图片训练,后面再是大batch训练后更新网络。 这样做的用意,应该是为了省内存吧。
换个角度,如果内存足够的话,且想提高训练速度的话,应该是提高cfg中的batch值,并适当较少subdivision,这样net->batch适当变大,意味着每次参加训练的图片数目增加,循环训练次数subdivision变少。 以文中数值例子而言,将64和16改成64和8比较适宜些。
后续更新:在GTX1080Ti上尝试用64和8的组合来训练,发现显存不够,只好还是用64和16。