darknet源码剖析(一) network初始化

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,则会调用相应的函数。

最后看一下第一层的输出:

darknet源码剖析(一) network初始化_第1张图片

第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

你可能感兴趣的:(darknet源码剖析)