写这一系列文章主要是想解析yolov2的具体实现,因为在作者的论文中有很多地方没有进行详细表述,所以不看源代码的话很难知道幕后具体做了什么。另一点是学习一下别人写一个网络的思路,因为你要知道作者的代码相当于自己写了一个小型框架(函数的接口设计的可能不是非常好)。
int main(int argc, char **argv)
{
//test_resize("data/bad.jpg");
//test_box();
//test_convolutional_layer();
if(argc < 2){
fprintf(stderr, "usage: %s \n" , argv[0]);//如果参数小于2就打印出错信息
return 0;//出错后返回
}
gpu_index = find_int_arg(argc, argv, "-i", 0);
接着看到find_int_arg
函数
int find_int_arg(int argc, char **argv, char *arg, int def)
{
int i;
for(i = 0; i < argc-1; ++i){
if(!argv[i]) continue;
if(0==strcmp(argv[i], arg)){
def = atoi(argv[i+1]);
del_arg(argc, argv, i);
del_arg(argc, argv, i);
break;
}
}
return def;
}
find_int_arg
这个函数本身的目的是要找出参数中的int
值。在这里主要任务就是判断输入参数是不是有-i
,将-i
后一位的数值转化为int
,然后返回这个值。其中又出现了两次del_arg
函数
void del_arg(int argc, char **argv, int index)
{
int i;
for(i = index; i < argc-1; ++i) argv[i] = argv[i+1];
argv[i] = 0;
}
这个函数作用是删除index
位置的参数。此处调用两次的作用是将-i
和其后的数值去除,类似于一个列表前移操作,后面的项补0。
接着看main函数后面的
if(find_arg(argc, argv, "-nogpu")) {
gpu_index = -1;
}
这里调用了一个find_arg
函数
int find_arg(int argc, char* argv[], char *arg)
{
int i;
for(i = 0; i < argc; ++i) {
if(!argv[i]) continue;
if(0==strcmp(argv[i], arg)) {
del_arg(argc, argv, i);
return 1;
}
}
return 0;
}
这个函数的作用就是查看参数中是否有arg
指向的字符串。在这里如果参数中出现了-nogpu
则我们gpu_index
设置为-1
,也就是不使用gpu
接着往后
#ifndef GPU
gpu_index = -1;
#else
if(gpu_index >= 0){
cuda_set_device(gpu_index);
}
#endif
如果没有定义GPU这个宏,那么将 gpu_index
设置为 -1
。如果设置了,并且我们前面也没有关闭gpu
选项的话,那么调用cuda_set_device
这个函数
void cuda_set_device(int n)
{
gpu_index = n;
cudaError_t status = cudaSetDevice(n);//这是cuda编程里面的,不详细说。设置显卡编号
check_error(status);//判断返回信息,设置显卡成功了,还是失败了
}
接着往后
else if (0 == strcmp(argv[1], "yolo")){
run_yolo(argc, argv);
}
这里有很多选项,我先看我最感兴趣的yolo
选项
到这里main函数中的所有问题就理清楚了,接着就是run_yolo
函数中问题了
void run_yolo(int argc, char **argv)
{
char *prefix = find_char_arg(argc, argv, "-prefix", 0);
float thresh = find_float_arg(argc, argv, "-thresh", .2);
int cam_index = find_int_arg(argc, argv, "-c", 0);
int frame_skip = find_int_arg(argc, argv, "-s", 0);
if(argc < 4){//如果参数小于4,打印出错信息
fprintf(stderr, "usage: %s %s [train/test/valid] [cfg] [weights (optional)]\n", argv[0], argv[1]);
return;
}
int avg = find_int_arg(argc, argv, "-avg", 1);
char *cfg = argv[3];
char *weights = (argc > 4) ? argv[4] : 0;
char *filename = (argc > 5) ? argv[5]: 0;
//根据第三个参数选择调用的函数
if(0==strcmp(argv[2], "test")) test_yolo(cfg, weights, filename, thresh);
else if(0==strcmp(argv[2], "train")) train_yolo(cfg, weights);
else if(0==strcmp(argv[2], "valid")) validate_yolo(cfg, weights);
else if(0==strcmp(argv[2], "recall")) validate_yolo_recall(cfg, weights);
else if(0==strcmp(argv[2], "demo")) demo(cfg, weights, thresh, cam_index, filename, voc_names, 20, frame_skip, prefix, avg, .5, 0,0,0,0);
}
这里有find_char_arg
,find_float_arg
函数,这里就不再赘述了,按照前面解析find_int_arg
的思路去做。
首先cfg
这个指针指向cfg
文件名字符串,weight
指向了权重文件名字符串。别的变量暂时不管,因为我们先关注train_yolo
这个函数。
void train_yolo(char *cfgfile, char *weightfile)
{
char *train_images = "/data/voc/train.txt";//train_images指向train.txt路径字符串
char *backup_directory = "/home/pjreddie/backup/";//backup_directory指向保存权重文件的路径
srand(time(0));//设置随机数种子
char *base = basecfg(cfgfile);//cfgfile就是上面说的cfg指向的字符串
printf("%s\n", base);
好的,这里出现了一个basecfg
函数
char *basecfg(char *cfgfile)
{
char *c = cfgfile;
char *next;
while((next = strchr(c, '/')))
{
c = next+1;
}
c = copy_string(c);
next = strchr(c, '.');
if (next) *next = 0;
return c;
}
char *copy_string(char *s)
{
char *copy = malloc(strlen(s)+1);
strncpy(copy, s, strlen(s)+1);
return copy;
}
先看传入的参数cfgfile
,是一个cfg
文件的路径字符串。接着strchr
,这个函数的作用是去第一个参数中,第二个参数以后的字符包括第二个参数(abc/ab.cfg—>/ab.cfg),接着c=next+1
,也就是c
指向了这个cfg
文件名字符串。
copy_string
函数的作用,就是重新分配一块内存,并且内容保留。那这里next
后的操作就很清楚了,就是把.cfg
后缀去掉。
这个函数是有缺陷的,因为这里没有考虑到window用户的需求,应该增加\\
的处理。
接着回到train_yolo
函数
//train_yolo
float avg_loss = -1;
network net = parse_network_cfg(cfgfile);
这里出现了parse_network_cfg
函数
network *parse_network_cfg(char *filename)
{
list *sections = read_cfg(filename);
出现了一个read_cfg
函数
list *read_cfg(char *filename)
{
FILE *file = fopen(filename, "r");
if(file == 0) file_error(filename);
void file_error(char *s)
{
fprintf(stderr, "Couldn't open file: %s\n", s);
exit(0);
}
file_error
判断cfg
文件有没有打开失败。接着往后
//read_cfg
char *line;
int nu = 0;
list *options = make_list();//创建一个链表
section *current = 0;
while((line=fgetl(file)) != 0){
这里出现了一个fgetl
函数
char *fgetl(FILE *fp)//fp指向打开后的cfg文件
{
if(feof(fp)) return 0;//如果文件结尾,退出
size_t size = 512;
char *line = malloc(size*sizeof(char));//分配512字节内存
//从fp中读取一行数据到line中,数据最大为size。
//注意,如果碰到换行或文件eof会停止读入。读取失败返回NULL
if(!fgets(line, size, fp)){
free(line);//失败就释放内存
return 0;
}
size_t curr = strlen(line);//返回line的长度,也就是读入的字符个数
//这里的代码是为了处理size不够的情况
while((line[curr-1] != '\n') && !feof(fp)){
if(curr == size-1){
//size不够我们就变大两倍
size *= 2;
line = realloc(line, size*sizeof(char));
if(!line) {
printf("%ld\n", size);
malloc_error();
}
}
//line不够,也就是一行没有读全,那么不会再从开始,而是接着上一次没有读完的信息
size_t readsize = size-curr;
if(readsize > INT_MAX) readsize = INT_MAX-1;
fgets(&line[curr], readsize, fp);
curr = strlen(line);
}
if(line[curr-1] == '\n') line[curr-1] = '\0';
return line;
}
这个函数的作用,简单理解就是读取文件的一行。其实用c++中的getline
函数就可以解决了。同样的python中的readline
也可以做到。
接着回到read_cfg
函数
//read_cfg
++ nu;
strip(line);
出现一个strip
函数
void strip(char *s)//传入我们前面读入的行
{
size_t i;
size_t len = strlen(s);
size_t offset = 0;
//这里的做法和list前移一样,出现空格符,则其后的所有项前移
for(i = 0; i < len; ++i){
char c = s[i];
if(c==' '||c=='\t'||c=='\n') ++offset;
else s[i-offset] = c;
}
s[len-offset] = '\0';
}
这个函数的作用就是删除字符串中的空格符(’\n’,’\t’,’ ‘)
回到read_cfg
函数
switch(line[0]){
case '['://这里就是看读入的行的第一个字符是'['也就是对于cfg文件中[net],[maxpool]这种东西
current = malloc(sizeof(section));//创建一个current
list_insert(options, current);//将current插入之前建立的options链表
current->options = make_list();//给current创建链表
current->type = line;//将读入的[net],[maxpool]读入type
break;
case '\0':
case '#':
case ';':
free(line);
break;
default:
if(!read_option(line, current->options)){
fprintf(stderr, "Config file error line %d, could parse: %s\n", nu, line);
free(line);
}
break;
}
}
fclose(file);
return options;
}
先看一下section
这个结构体的定义
typedef struct{
char *type;
list *options;
}section;
它的内部包含一个链表。这里作者的list_insert(options, current);
中options和后面的current->options = make_list();
中的options存在歧义。其实两者一毛钱关系都没有。
分析一下这个read_option
函数
int read_option(char *s, list *options)//s指向读取的行,list就是一个section中的list
{
size_t i;
size_t len = strlen(s);
char *val = 0;
for(i = 0; i < len; ++i){
if(s[i] == '='){
s[i] = '\0';
val = s+i+1;//val指向=后面的字符串
break;
}
}
if(i == len-1) return 0;
char *key = s;//这个时候key指向的是=前面的字符串
option_insert(options, key, val);
return 1;
}
typedef struct{
char *key;
char *val;
int used;
} kvp;
void option_insert(list *l, char *key, char *val)
{
kvp *p = malloc(sizeof(kvp));
p->key = key;
p->val = val;
p->used = 0;
list_insert(l, p);//将一个kvp结构插入section中的list
}
回头再看这个switch
,他在这里的作用就是将cfg
文件中的不同内容(’[net]’,’[maxpool]’)区分开,然后存到一个列表中。
举个例子
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
这是yolo9000.cfg
中的一个片段,我们先看第一行,他是一个’[]’,所以进入第一个判断,我们首先将[convolutional]
字符串,存入一个section
对象的type
中,并且将这个section
对象插入到一个列表中。接着读取第二行batch_normalize=1
,将=
前后内容拆开存储到kvp
结构中,再将这个kvp
插入到section
的list
中。
总览整个read_cfg
函数
list *read_cfg(char *filename)
{
FILE *file = fopen(filename, "r");
if(file == 0) file_error(filename);
char *line;
int nu = 0;
list *options = make_list();
section *current = 0;
while((line=fgetl(file)) != 0){
++ nu;
strip(line);
switch(line[0]){
case '[':
current = malloc(sizeof(section));
list_insert(options, current);
current->options = make_list();
current->type = line;
break;
case '\0':
case '#':
case ';':
free(line);
break;
default:
if(!read_option(line, current->options)){
fprintf(stderr, "Config file error line %d, could parse: %s\n", nu, line);
free(line);
}
break;
}
}
fclose(file);
return options;
}
作者做了一种数据结构来存放cfg
的文件数据。
觉得不错,点个赞吧b( ̄▽ ̄)d
由于本人水平有限,文中有不对之处,希望大家指出,谢谢^_^!
下一篇继续分析parse_network_cfg
这个函数后面的部分,敬请关注。