1. 程式人生 > >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 <function>\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這個函式後面的部分,敬請關注。