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_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);
- 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
插入到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;
}
- 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
這個函式後面的部分,敬請關注。