network是darknet的核心组件,本文以yolov1为例对network的结构进行分析,并对network相关操作函数进行分析。
darknet的网络结构使用network结构体进行保存,network的构建过程主要包括以下几个函数:
load_network(src/networks.c)->parse_network_cfg(src/parser.c)->make_network(src/network.c)->parse_network_cfg
->parse_net_options(src/parser.c)
(一)network参数初始化与空间分配
network的定义位于include/darknet.h文件中
network的构造过程主要就是两个函数,make_network与parse_net_options。其中make_network主要是为network分配空间,parse_net_options主要用于设置网络的具体参数。
在调用make_network时,参数n的值为32,n表示network的层数,在yolov1配置文件中,一共33层,其中第一层是网络的整体配置,不需要再为其分配单独的层空间,因此此处传入的n值为32。make_network返回后network结构已经生成,通过之后的代码继续完善其结构。
parse_net_options函数的主要功能就是配置yolov1中net层的参数。根据parse_net_options函数,batch_size的设置方式如下:
net->batch = option_find_int(options, "batch",1);
int subdivs = option_find_int(options, "subdivisions",1);
net->batch /= subdivs;
net->batch *= net->time_steps;
最后的计算结果是batch_size/subdivs,然后还要乘以net->time_steps,由于没有设置net->time_steps,因此此处为默认值1。
至此network的基本结构就已经初始化完成了,返回parse_network_cfg函数继续初始化每一层的参数。
(二)layer参数初始化
以卷积层的初始化为例,调用parse_convolutional函数(src/parser.c)初始化参数,传入make_convolutional_layer(src/convolutional_layer.c)中,因此卷积层的参数实际是在make_convolutional_layer函数中完成初始化。
(1)parse_convolutional
其中需要注意的是darknet中pad与padding是两种不同的配置。若设置pad为不为0的任何数,则padding只为size/2。
int pad = option_find_int_quiet(options, "pad",0);
int padding = option_find_int_quiet(options, "padding",0);
if(pad) padding = size/2;
(2)make_convolutional_layer
首先需要注意的是weights与bias的空间分配
l.weights = calloc(c/groups*n*size*size, sizeof(float));
l.weight_updates = calloc(c/groups*n*size*size, sizeof(float));
l.biases = calloc(n, sizeof(float));
l.bias_updates = calloc(n, sizeof(float));
上述代码为weights与bias分配了内存空间,根据当前yolov1的设置,c/groups*n*size*size的值为9408,其中c表示channel数量,groups在yolov1中未设置,因此使用默认值1,n代表卷积层输出channel数,为64,size代表卷积核大小,为7。因此除开groups不管,c*n*size*size恰好是卷积层参数数量计算公式。
l.nweights = c/groups*n*size*size;
l.nbiases = n;
nweights与nbiases分别表示权重与偏置的参数数量。
此处还应注意权重与偏置都是一维的,之后再计算的时候还应注意矩阵的计算方式。
接下来是参数初始化
float scale = sqrt(2./(size*size*c/l.groups));
for(i = 0; i < l.nweights; ++i) l.weights[i] = scale*rand_normal();
rand_normal(src/util.c)中实现的是Box–Muller transform,其作用是在[0,1)区间上生成服从均匀分布的随机数。但此处尚不清楚为什么需要乘以scale。通过此处的代码分析可以发现,darknet的权重初始化使用的是正态分布初始化。
接下来是计算输出特征图的长度与宽度。
int out_w = convolutional_out_width(l);
int out_h = convolutional_out_height(l);
convolutional_out_width与convolutional_out_height函数中为输出特征图的长度与宽度的计算公式。
以convolutional_out_width为例。
int convolutional_out_width(convolutional_layer l)
{
return (l.w + 2*l.pad - l.size) / l.stride + 1;
}
(width + 2×pad(左右补齐,在yolov1.cfg中配置)-size(卷积核大小,在yolov1.cfg中配置))/stride(步幅)+ 1
在当前例子中是width为448,pad为3,size为7,stride为2,因此out_w为223。
同理可计算out_w同样为223。
接下来设置输出特征的维度并分配空间,output可能表示输出的特征图,delta的功能暂时还不明确,应该表示的是梯度。
l.out_h = out_h;
l.out_w = out_w;
l.out_c = n;
l.outputs = l.out_h * l.out_w * l.out_c;
l.inputs = l.w * l.h * l.c;
l.output = calloc(l.batch*l.outputs, sizeof(float));
l.delta = calloc(l.batch*l.outputs, sizeof(float));
接下来分别设置前向传播、反向传播与参数更新函数。
l.forward = forward_convolutional_layer;
l.backward = backward_convolutional_layer;
l.update = update_convolutional_layer;
接下来的一部分代码暂时用不到,因此不做详细分析。有两点需要注意,第一点是yolov1中的第一层包含有batch_normalize操作,第二点需要注意的是如果定义GPU或者CUDNN,则会调用相应的函数。
最后看一下第一层的输出:
第0层,类型为卷积层,共有64组参数,卷积核大小为7,步长为2,输入大小为448*448*3,输出大小为224*224*64,这一层共有944111616层操作,也就是共有9亿多次操作。
最后看一下卷积层操作数量的计算公式:
(2.0 * l.n * l.size*l.size*l.c/l.groups * l.out_h*l.out_w)
l.size*l.size*l.c代表一个卷积核的计算结果,l.n * l.size*l.size*l.c表示n个卷积核的输出结果,上述操作只能输出特征图上的一个点,再乘以l.out_h*l.out_w表示的是特征图上的所有点,2表示乘法与加法操作各一次。以kernel_size=3为例,乘法计算次数就是9次,加法共8次。若输入channel为3,则共有27次乘法与24次加法,再加上一次bias加法,因此共有乘法27次,加法25次。所以此处在最后的计算结果上乘以2可能是比较粗略的计算方式。
最后记录一下当前的疑问:
1)Box–Muller transform