YOLOv2源码分析(一)

文章全部YOLOv2源码分析

0x00 写在开头

写这一系列文章主要是想解析yolov2的具体实现,因为在作者的论文中有很多地方没有进行详细表述,所以不看源代码的话很难知道幕后具体做了什么。另一点是学习一下别人写一个网络的思路,因为你要知道作者的代码相当于自己写了一个小型框架(函数的接口设计的可能不是非常好)。

0x01 从main函数开始

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);

   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

接着看到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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个函数作用是删除index位置的参数。此处调用两次的作用是将-i和其后的数值去除,类似于一个列表前移操作,后面的项补0。

接着看main函数后面的

if(find_arg(argc, argv, "-nogpu")) {
        gpu_index = -1;
    }
   
   
   
   
  • 1
  • 2
  • 3

这里调用了一个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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这个函数的作用就是查看参数中是否有arg指向的字符串。在这里如果参数中出现了-nogpu则我们gpu_index设置为-1,也就是不使用gpu

接着往后

#ifndef GPU
    gpu_index = -1;
#else
    if(gpu_index >= 0){
        cuda_set_device(gpu_index);
    }
#endif
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果没有定义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);//判断返回信息,设置显卡成功了,还是失败了
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

接着往后

else if (0 == strcmp(argv[1], "yolo")){
        run_yolo(argc, argv);
    }
   
   
   
   
  • 1
  • 2
  • 3

这里有很多选项,我先看我最感兴趣的yolo选项

到这里main函数中的所有问题就理清楚了,接着就是run_yolo函数中问题了

0x02 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);
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里有find_char_argfind_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);

   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

好的,这里出现了一个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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

先看传入的参数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);   
   
   
   
   
  • 1
  • 2
  • 3

这里出现了parse_network_cfg函数

0x03 parse_network_cfg

network *parse_network_cfg(char *filename)
{
    list *sections = read_cfg(filename);

   
   
   
   
  • 1
  • 2
  • 3
  • 4

出现了一个read_cfg函数

0x0301 read_cfg

list *read_cfg(char *filename)
{
    FILE *file = fopen(filename, "r");
    if(file == 0) file_error(filename);
   
   
   
   
  • 1
  • 2
  • 3
  • 4
void file_error(char *s)
{
    fprintf(stderr, "Couldn't open file: %s\n", s);
    exit(0);
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

file_error判断cfg文件有没有打开失败。接着往后

//read_cfg
    char *line;
    int nu = 0;
    list *options = make_list();//创建一个链表
    section *current = 0;
    while((line=fgetl(file)) != 0){       
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里出现了一个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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

这个函数的作用,简单理解就是读取文件的一行。其实用c++中的getline函数就可以解决了。同样的python中的readline也可以做到。

接着回到read_cfg函数

//read_cfg
        ++ nu;
        strip(line);

   
   
   
   
  • 1
  • 2
  • 3
  • 4

出现一个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';
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这个函数的作用就是删除字符串中的空格符(’\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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

先看一下section这个结构体的定义

typedef struct{
    char *type;
    list *options;
}section;
   
   
   
   
  • 1
  • 2
  • 3
  • 4

它的内部包含一个链表。这里作者的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
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

回头再看这个switch,他在这里的作用就是将cfg文件中的不同内容(’[net]’,’[maxpool]’)区分开,然后存到一个列表中。

举个例子

[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这是yolo9000.cfg中的一个片段,我们先看第一行,他是一个’[]’,所以进入第一个判断,我们首先将[convolutional]字符串,存入一个section对象的type中,并且将这个section对象插入到一个列表中。接着读取第二行batch_normalize=1,将=前后内容拆开存储到kvp结构中,再将这个kvp插入到sectionlist中。



总览整个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;
}
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

作者做了一种数据结构来存放cfg的文件数据。

觉得不错,点个赞吧b( ̄▽ ̄)d

由于本人水平有限,文中有不对之处,希望大家指出,谢谢^_^!

下一篇继续分析parse_network_cfg这个函数后面的部分,敬请关注。

你可能感兴趣的:(算法)