文章全部YOLOv2源码分析
0x01 parse_network_cfg
我们继续前面没有说完的parse_network_cfg
//parse_network_cfg
node *n = sections->front;
if(!n) error("Config file has no sections");
我么先要了解一下list
结构
typedef struct list{
int size;
node *front;
node *back;
} list;
typedef struct node{
void *val;
struct node *next;
struct node *prev;
} node;
这其实是一个双向链表,前向和后项都是一个node
数据结构。这里,如果,这个链表后没有节点的话,就报错。
接着往后
//parse_network_cfg
network *net = make_network(sections->size - 1);
这里使用了一个make_network
函数
network *make_network(int n)
{
network *net = calloc(1, sizeof(network));
net->n = n;
net->layers = calloc(net->n, sizeof(layer));
net->seen = calloc(1, sizeof(size_t));
net->t = calloc(1, sizeof(int));
net->cost = calloc(1, sizeof(float));
return net;
}
注意这里的这个make_network
可能和早期的不太一样。我们先看看这里他做了什么。先看看network
这个结构
//这个文件现在放在了darknet.h文件中
typedef enum {
CONSTANT, STEP, EXP, POLY, STEPS, SIG, RANDOM
} learning_rate_policy;
typedef struct network{
int n; //网络总层数
int batch; //一个batch包含的图片数目,看下面的subdivisions
size_t *seen; //已经读取的图片数量
int *t;
float epoch; //训练的次数
int subdivisions; //注意前面的batch/subdivisions才是网络的batch大小,可能目的是防止gpu显存不够
layer *layers; //指向网络的层
float *output;
learning_rate_policy policy;//学习率的策略,是一个枚举类型
float learning_rate;//学习率
float momentum; //动量,一般0.9
float decay; //权重衰减正则项,防止过拟合
float gamma; //用于计算学习率,见后面0x0102
float scale; //用于计算学习率,见后面0x0102
float power; //用于计算学习率,见后面0x0102
int time_steps;
int step; //用于计算学习率,见后面0x0102
int max_batches; //最大的训练batch数目
float *scales; //用于计算学习率,见后面0x0102
int *steps; //用于计算学习率,见后面0x0102
int num_steps; //steps中的数据个数
int burn_in;
int adam; //adam算法
float B1; //一阶矩估计的指数衰减率
float B2; //二阶矩估计的指数衰减率
float eps; //为了防止在实现中除以零
int inputs; //h*w*c
int outputs;
int truths;
int notruth;
int h, w, c; //输入图像的高,宽,通道数
int max_crop; //控制图片缩放的最大值
int min_crop; //控制图片缩放的最小值
float max_ratio; //控制图片缩放的最大比例
float min_ratio; //控制图片缩放的最小比例
int center;
float angle; //设置旋转角度,扩充数据
float aspect; //设置方位,扩充数据
float exposure; //设置曝光量,扩充数据
float saturation; //设置饱和度,扩充数据
float hue; //设置色调,扩充数据
int random; //random为1时随机使用不同尺寸的图片进行训练
int gpu_index; //设置第几个gpu
tree *hierarchy;
float *input;
float *truth;
float *delta;
float *workspace;
int train;
int index;
float *cost;
#ifdef GPU
float *input_gpu;
float *truth_gpu;
float *delta_gpu;
float *output_gpu;
#endif
} network;
由于参数太多,用到哪个说哪个,这个结构的主要作用就是存储网络的配置参数。make_network
的作用就是产生network
这种数据结构。接着往下
//parse_network_cfg
net->gpu_index = gpu_index;//设置gpu
size_params params;
又出现一个新的结构size_params
typedef struct size_params{
int batch; //一个batch包含的图片数目
int inputs;
int h; //图像的高
int w; //输入图像的宽
int c; //输入图像的通道数
int index;
int time_steps;
network *net;
} size_params;
接着往下
//parse_network_cfg
section *s = (section *)n->val;//section这个结构我在(一)中提过
n
是一个node
结构,这个结构中的val
是一个void*
,所以这里就是将node
结构中的val
强转为section*
,相当于我在(一)中图上画的[net]
等节点。
//parse_network_cfg
list *options = s->options;//这里就是之前说的kvp,也就是size=3,stride=1,pad=1这些
if(!is_network(s)) error("First section must be [net] or [network]");
看一下这个is_network
函数
int is_network(section *s)
{
return (strcmp(s->type, "[net]")==0
|| strcmp(s->type, "[network]")==0);
}
这个函数的作用很明显,判断传入的第一个section
是不是[net]
或[network]
。接着又是一个比较大的函数
//parse_network_cfg
parse_net_options(options, net);
0x0101 parse_net_options
void parse_net_options(list *options, network *net)//传入的时options参数和我们的network
{
net->batch = option_find_int(options, "batch",1);//设置net的batch大小
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);//设置subdivisions,防止显存不够
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->adam = option_find_int_quiet(options, "adam", 0);
if(net->adam){//设置adam参数,这里的默认选项是按照adam论文给的参数设置的
net->B1 = option_find_float(options, "B1", .9);
net->B2 = option_find_float(options, "B2", .999);
net->eps = option_find_float(options, "eps", .0000001);
}
net->h = option_find_int_quiet(options, "height",0);
net->w = option_find_int_quiet(options, "width",0);
net->c = option_find_int_quiet(options, "channels",0);
net->inputs = option_find_int_quiet(options, "inputs", net->h * net->w * net->c);
net->max_crop = option_find_int_quiet(options, "max_crop",net->w*2);
net->min_crop = option_find_int_quiet(options, "min_crop",net->w);
net->max_ratio = option_find_float_quiet(options, "max_ratio", (float) net->max_crop / net->w);
net->min_ratio = option_find_float_quiet(options, "min_ratio", (float) net->min_crop / net->w);
net->center = option_find_int_quiet(options, "center",0);
net->angle = option_find_float_quiet(options, "angle", 0);
net->aspect = option_find_float_quiet(options, "aspect", 1);
net->saturation = option_find_float_quiet(options, "saturation", 1);
net->exposure = option_find_float_quiet(options, "exposure", 1);
net->hue = option_find_float_quiet(options, "hue", 0);
if(!net->inputs && !(net->h && net->w && net->c)) error("No input parameters supplied");
char *policy_s = option_find_str(options, "policy", "constant");
net->policy = get_policy(policy_s);
net->burn_in = option_find_int_quiet(options, "burn_in", 0);
net->power = option_find_float_quiet(options, "power", 4);
if(net->policy == STEP){//如果学习率的策略是STEP的话
net->step = option_find_int(options, "step", 1);
net->scale = option_find_float(options, "scale", 1);
} else if (net->policy == STEPS){//如果学习率的策略是STEPS的话
char *l = option_find(options, "steps");//指向steps的字符串
char *p = option_find(options, "scales");//指向scales的字符串
if(!l || !p) error("STEPS policy must have steps and scales in cfg file");
int len = strlen(l);
int n = 1;
int i;
for(i = 0; i < len; ++i){
if (l[i] == ',') ++n;
}
int *steps = calloc(n, sizeof(int));//将所有的steps值分开存放到这个数组中
float *scales = calloc(n, sizeof(float));//将所有的scales值分开存放到这个数组中
for(i = 0; i < n; ++i){
int step = atoi(l);
float scale = atof(p);
l = strchr(l, ',')+1;
p = strchr(p, ',')+1;
steps[i] = step;
scales[i] = scale;
}
net->scales = scales;
net->steps = steps;
net->num_steps = n;
} else if (net->policy == EXP){
net->gamma = option_find_float(options, "gamma", 1);
} else if (net->policy == SIG){
net->gamma = option_find_float(options, "gamma", 1);
net->step = option_find_int(options, "step", 1);
} else if (net->policy == POLY || net->policy == RANDOM){
}
net->max_batches = option_find_int(options, "max_batches", 0);
}
这个函数中出现了这个函数option_find_int_quiet
int option_find_int_quiet(list *l, char *key, int def)
{
char *v = option_find(l, key);
if(v) return atoi(v);
return def;
}
char *option_find(list *l, char *key)
{
node *n = l->front;
while(n){
kvp *p = (kvp *)n->val;
if(strcmp(p->key, key) == 0){
p->used = 1;
return p->val;
}
n = n->next;
}
return 0;
}
这个函数和之前的option_find_int
不同。首先看里面的option_find
这个函数的作用就是查找list
中,node
的key
和参数的key
相同的node
,返回这个node
的val
,如果不存在,返回0。通过这个函数我们得到了pad=1 stride=2
等参数后的数值信息。接下来就很easy了,如果有这个参数就将这个字符串(得到的是一个字符串,不是一个数)转化为一个整数,没有的话就返回三个参数(有点类似于默认参数)。
回顾整个option_find_int_quiet
,它的作用就是找出和key
的node
的数值大小。类似于一种map
里面的查找操作。
而再看option_find_int
int option_find_int(list *l, char *key, int def)
{
char *v = option_find(l, key);
if(v) return atoi(v);
fprintf(stderr, "%s: Using default '%d'\n", key, def);
return def;
}
它和前者的区别在于,它会打印报错信息。
0x0102 学习率策略
学习率策略的设置是一个枚举类型
typedef enum {
CONSTANT, STEP, EXP, POLY, STEPS, SIG, RANDOM
} learning_rate_policy;
这是在前面就提到的。我们现在来看看,这几个有什么区别(参考caffe源码)
CONSTANT
:学习率是一个固定的值learning_rateSTEP
:是一种均匀分步策略learning_rate* gamma ^ (floor(iter / step))EXP
:learning_rate* gamma ^ iterPOLY
:learning_rate(1 - iter/max_iter) ^ (power)STEPS
:同STEP只是这里的scale和step是一个数组SIG
:learning_rate ( 1/(1 + exp(-gamma * (iter - stepsize))))RANDOM
:代码中没有考虑
回头看整个parse_net_options
这个函数,这个函数的主要功能就是读取[net]
后的信息,赋值到net
所指向的network
结构中。
接着我们再回到parse_network_cfg
//parse_network_cfg
params.h = net->h; //将h,w,c赋值size_params对象params,下面类似不再赘述
params.w = net->w;
params.c = net->c;
params.inputs = net->inputs;
params.batch = net->batch;
params.time_steps = net->time_steps;
params.net = net;
size_t workspace_size = 0;
n = n->next; //[net]搞定了,接下来去下一个node
int count = 0;
free_section(s);
我们再来看看这个free_section
函数做了什么
void free_section(section *s)//传入的变量是之前的那个section指针
{
free(s->type);//释放type空间
node *n = s->options->front;//以下内容是释放s指向的kvp链表
while(n){
kvp *pair = (kvp *)n->val;
free(pair->key);
free(pair);
node *next = n->next;
free(n);
n = next;
}
free(s->options);
free(s);//最后释放s
}
综上来看这个函数的目的在这里很明显了。我们把cfg
的参数从section
中copy到了network
中,section
内存不用了,自然要把它释放。
再回到parse_network_cfg
函数
//parse_network_cfg
fprintf(stderr, "layer filters size input output\n");
while(n){
params.index = count;
fprintf(stderr, "%5d ", count);
s = (section *)n->val;
options = s->options;
layer l = {0};
LAYER_TYPE lt = string_to_layer_type(s->type);
这是一个非常大的循环体,先看前面一小部分。我们先看看其中的LAYER_TYPE
结构和layer
结构
//现在这个结构也放在了darknet.h中
struct layer;
typedef struct layer layer;
typedef enum {
CONVOLUTIONAL,
DECONVOLUTIONAL,
CONNECTED,
MAXPOOL,
SOFTMAX,
DETECTION,
DROPOUT,
CROP,
ROUTE,
COST,
NORMALIZATION,
AVGPOOL,
LOCAL,
SHORTCUT,
ACTIVE,
RNN,
GRU,
LSTM,
CRNN,
BATCHNORM,
NETWORK,
XNOR,
REGION,
REORG,
BLANK
} LAYER_TYPE;
struct layer{
LAYER_TYPE type;
ACTIVATION activation;
COST_TYPE cost_type;
void (*forward) (struct layer, struct network);
void (*backward) (struct layer, struct network);
void (*update) (struct layer, update_args);
void (*forward_gpu) (struct layer, struct network);
void (*backward_gpu) (struct layer, struct network);
void (*update_gpu) (struct layer, update_args);
...
我们可以看到LAYER_TYPE
就是每一层的类型。而layer
就是设置这些层的参数,由于参数太多,我在后面会分开讲。
再来看string_to_layer_type
函数
LAYER_TYPE string_to_layer_type(char * type)//传入的参数就是之前的section->type
{
if (strcmp(type, "[shortcut]")==0) return SHORTCUT;
if (strcmp(type, "[crop]")==0) return CROP;
if (strcmp(type, "[cost]")==0) return COST;
if (strcmp(type, "[detection]")==0) return DETECTION;
if (strcmp(type, "[region]")==0) return REGION;
if (strcmp(type, "[local]")==0) return LOCAL;
if (strcmp(type, "[conv]")==0
|| strcmp(type, "[convolutional]")==0) return CONVOLUTIONAL;
if (strcmp(type, "[deconv]")==0
|| strcmp(type, "[deconvolutional]")==0) return DECONVOLUTIONAL;
if (strcmp(type, "[activation]")==0) return ACTIVE;
if (strcmp(type, "[net]")==0
|| strcmp(type, "[network]")==0) return NETWORK;
if (strcmp(type, "[crnn]")==0) return CRNN;
if (strcmp(type, "[gru]")==0) return GRU;
if (strcmp(type, "[lstm]") == 0) return LSTM;
if (strcmp(type, "[rnn]")==0) return RNN;
if (strcmp(type, "[conn]")==0
|| strcmp(type, "[connected]")==0) return CONNECTED;
if (strcmp(type, "[max]")==0
|| strcmp(type, "[maxpool]")==0) return MAXPOOL;
if (strcmp(type, "[reorg]")==0) return REORG;
if (strcmp(type, "[avg]")==0
|| strcmp(type, "[avgpool]")==0) return AVGPOOL;
if (strcmp(type, "[dropout]")==0) return DROPOUT;
if (strcmp(type, "[lrn]")==0
|| strcmp(type, "[normalization]")==0) return NORMALIZATION;
if (strcmp(type, "[batchnorm]")==0) return BATCHNORM;
if (strcmp(type, "[soft]")==0
|| strcmp(type, "[softmax]")==0) return SOFTMAX;
if (strcmp(type, "[route]")==0) return ROUTE;
return BLANK;
}
这个函数的作用很明显,通过比较字符串,将原先的section
中的type
变成了LAYER_TYPE
中的枚举元素。
接着回到parse_network_cfg
,后面就是很多的条件判断
//parse_network_cfg
if(lt == CONVOLUTIONAL){
l = parse_convolutional(options, params);
}else if(lt == DECONVOLUTIONAL){
l = parse_deconvolutional(options, params);
}else if(lt == LOCAL){
l = parse_local(options, params);
}else if(lt == ACTIVE){
l = parse_activation(options, params);
}else if(lt == RNN){
l = parse_rnn(options, params);
}else if(lt == GRU){
l = parse_gru(options, params);
}else if (lt == LSTM) {
l = parse_lstm(options, params);
}else if(lt == CRNN){
l = parse_crnn(options, params);
}else if(lt == CONNECTED){
l = parse_connected(options, params);
}else if(lt == CROP){
l = parse_crop(options, params);
}else if(lt == COST){
l = parse_cost(options, params);
}else if(lt == REGION){
l = parse_region(options, params);
}else if(lt == DETECTION){
l = parse_detection(options, params);
}else if(lt == SOFTMAX){
l = parse_softmax(options, params);
net->hierarchy = l.softmax_tree;
}else if(lt == NORMALIZATION){
l = parse_normalization(options, params);
}else if(lt == BATCHNORM){
l = parse_batchnorm(options, params);
}else if(lt == MAXPOOL){
l = parse_maxpool(options, params);
}else if(lt == REORG){
l = parse_reorg(options, params);
}else if(lt == AVGPOOL){
l = parse_avgpool(options, params);
}else if(lt == ROUTE){
l = parse_route(options, params, net);
}else if(lt == SHORTCUT){
l = parse_shortcut(options, params, net);
}else if(lt == DROPOUT){
l = parse_dropout(options, params);
l.output = net->layers[count-1].output;
l.delta = net->layers[count-1].delta;
#ifdef GPU
l.output_gpu = net->layers[count-1].output_gpu;
l.delta_gpu = net->layers[count-1].delta_gpu;
#endif
}
然后再看以parse
开头的函数作用,以其中一个为例parse_convolutional
0x0103 parse_convolutional
convolutional_layer parse_convolutional(list *options, size_params params)
{
int n = option_find_int(options, "filters",1); //卷积核个数
int size = option_find_int(options, "size",1); //卷积核大小
int stride = option_find_int(options, "stride",1);//步长
int pad = option_find_int_quiet(options, "pad",0);//图像周围是否补0
int padding = option_find_int_quiet(options, "padding",0);//补0的长度
int groups = option_find_int_quiet(options, "groups", 1);//卷积核组的个数
if(pad) padding = size/2;//对应SAME补0策略
char *activation_s = option_find_str(options, "activation", "logistic");//激活函数
ACTIVATION activation = get_activation(activation_s);
int batch,h,w,c;
h = params.h; //图片的高
w = params.w; //图片的宽
c = params.c; //图片的通道数
batch=params.batch;
if(!(h && w && c)) error("Layer before convolutional layer must output image.");
int batch_normalize = option_find_int_quiet(options, "batch_normalize", 0);//BN操作
int binary = option_find_int_quiet(options, "binary", 0);//权重二值化
int xnor = option_find_int_quiet(options, "xnor", 0);//权重和输入二值化
convolutional_layer layer = make_convolutional_layer(batch,h,w,c,n,groups,size,stride,padding,activation, batch_normalize, binary, xnor, params.net->adam);
layer.flipped = option_find_int_quiet(options, "flipped", 0);
layer.dot = option_find_float_quiet(options, "dot", 0);
return layer;
}
因为我之前已经讲过了option_find_int
函数,所以这里不再多说了。代码的前面部分也非常容易理解,就射设置,不同[]
后面的参数。
后面又出现一个新的结构ACTIVATION
typedef enum{
LOGISTIC, RELU, RELIE, LINEAR, RAMP, TANH, PLSE, LEAKY, ELU, LOGGY, STAIR, HARDTAN, LHTAN
} ACTIVATION;
很明显这个枚举是用来定义不同的激活函数的。
再看get_activation
这个函数
ACTIVATION get_activation(char *s)//传入的参数时cfg中的activation
{
if (strcmp(s, "logistic")==0) return LOGISTIC;
if (strcmp(s, "loggy")==0) return LOGGY;
if (strcmp(s, "relu")==0) return RELU;
if (strcmp(s, "elu")==0) return ELU;
if (strcmp(s, "relie")==0) return RELIE;
if (strcmp(s, "plse")==0) return PLSE;
if (strcmp(s, "hardtan")==0) return HARDTAN;
if (strcmp(s, "lhtan")==0) return LHTAN;
if (strcmp(s, "linear")==0) return LINEAR;
if (strcmp(s, "ramp")==0) return RAMP;
if (strcmp(s, "leaky")==0) return LEAKY;
if (strcmp(s, "tanh")==0) return TANH;
if (strcmp(s, "stair")==0) return STAIR;
fprintf(stderr, "Couldn't find activation function %s, going with ReLU\n", s);
return RELU;
}
这个函数的作用也很明显,通过比较字符串,将原先的激活函数的字符串,转化为现在的ACTIVATION
枚举元素。
接着看convolutional_layer
这个结构
typedef layer convolutional_layer;
其实就是一个layer
,再看make_convolutional_layer
函数(又是一个非常大的函数)
convolutional_layer make_convolutional_layer(int batch, int h, int w, int c, int n, int groups, int size, int stride, int padding, ACTIVATION activation, int batch_normalize, int binary, int xnor, int adam)//传入的参数就是我们之前设置好的
{
int i;
convolutional_layer l = {0};
l.type = CONVOLUTIONAL;
l.groups = groups; //卷积核的组数
l.h = h; //图像的高
l.w = w; //图像的宽
l.c = c; //图像的通道数目
l.n = n; //卷积核个数
l.binary = binary;
l.xnor = xnor;
l.batch = batch;
l.stride = stride;
l.size = size;
l.pad = padding;
l.batch_normalize = batch_normalize;
l.weights = calloc(c/groups*n*size*size, sizeof(float));//计算所有权重个数,c/groups*n*size*size,分配内存空间
l.weight_updates = calloc(c/groups*n*size*size, sizeof(float));
l.biases = calloc(n, sizeof(float));//卷积核个数和偏向的数目一致,分配内存空间
l.bias_updates = calloc(n, sizeof(float));
l.nweights = c/groups*n*size*size;
l.nbiases = n;
// float scale = 1./sqrt(size*size*c);
float scale = sqrt(2./(size*size*c/l.groups));//缩放系数
//scale = .02;
//for(i = 0; i < c*n*size*size; ++i) l.weights[i] = scale*rand_uniform(-1, 1);
for(i = 0; i < l.nweights; ++i) l.weights[i] = scale*rand_normal();//初始化权重
int out_w = convolutional_out_width(l);
int out_h = convolutional_out_height(l);
这里出现了新的函数,我们只分析其中一个convolutional_out_width
int convolutional_out_height(convolutional_layer l)
{
return (l.h + 2*l.pad - l.size) / l.stride + 1;
}
int convolutional_out_width(convolutional_layer l)
{
return (l.w + 2*l.pad - l.size) / l.stride + 1;
}
函数中这个公式大家应该很熟悉,就是计算卷积后的输出图像的大小。
接着看make_convolutional_layer
这个函数后面的部分
//make_convolutional_layer
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));
好的,这篇文章的篇幅有些长了,我们把剩余部分放到下一篇
觉得不错,点个赞吧b( ̄▽ ̄)d
由于本人水平有限,文中有不对之处,希望大家指出,谢谢^_^!
下一篇继续分析make_convolutional_layer
这个函数后面的部分,敬请关注。