【AlexeyAB DarkNet框架解析】一,框架總覽

  • 2020 年 2 月 21 日
  • 筆記

前言的前言:Darknet是一個較為輕型的完全基於C與CUDA的開源深度學習框架,其主要特點就是容易安裝,沒有任何依賴項(OpenCV都可以不用),移植性非常好,支援CPU與GPU兩種計算方式。而AlexeyAB版本的Darknet是在官方Darknet基礎上進行了很多修改,添加了一些新特性,新演算法,新Backbone,是最流行的目標檢測開源項目之一。

前言

總寫一些論文解讀自然是不太好的,因為我感覺紙上談兵用處沒那麼大,如果你從事深度學習,不靜心閱讀幾個框架那麼程式碼能力肯定是有欠缺的。趁著自己C語言還沒有完全忘記,我決定來仔細探索一番AlexeyAB的Darknet框架,所以就有了這個【AlexeyAB DarkNet框架解析】系列。這個系列的更新肯定是十分漫長的,因為裡面有很多演算法或者特性我也可能還沒有怎麼學過,所以也是有一些難度,但正所謂硬骨頭啃了之後才有營養,所以我決定開這個系列。從今天起,我將嘗試做一個最詳細的Darknet源碼解析(不定期更新),從數據結構到各種新式Backbone再到多種損失函數再到各種新特性等。希望這個系列更新我能和你一起完全掌握這個AlexeyAB版Darknet,並且在閱讀程式碼的能力方面有所提升。值得一提的是AlexeyAB版本Darknet的README.md已經被我們整理成了中文版本,如果你是學術派不是很在意底層程式碼實現你可以參考README.md去訓練或者測試你想要的模型。README.md的中文翻譯地址如下:【翻譯】手把手教你用AlexeyAB版Darknet 。注意這一節僅僅是框架總覽,不會那麼詳細,後面會非常詳細的來逐步分析每個步驟。

Darknet框架分析主線

分析主線的確定

Darknet相比當前訓練的C/C++主流框架(如Caffe)來講,具有編譯速度快,依賴少,易部署等眾多優點,我們先定位到src/darknet.c裡面的main函數,這是這個框架實現分類,定位,回歸,分割等功能的初始入口。這一節的核心程式碼如下,注意一下就是run_yolo只提供了yolo目標檢測演算法的原始實現。而run_detector函數提供了AlexeyAB添加了各種新特性的目標檢測演算法,所以之後我們會從這個函數跟進去來解析Darknet框架。Darknet提供的其他功能如run_super(高解析度重建),run_classifier(影像分類),run_char_rnn(RNN文本識別)有興趣可以自己去讀(這個框架用來做目標檢測比較好,其他演算法建議還是去其它框架實現吧),本系列只講目標檢測。

	if (0 == strcmp(argv[1], "average")){          average(argc, argv);      } else if (0 == strcmp(argv[1], "yolo")){          run_yolo(argc, argv);      } else if (0 == strcmp(argv[1], "voxel")){          run_voxel(argc, argv);      } else if (0 == strcmp(argv[1], "super")){          run_super(argc, argv);      } else if (0 == strcmp(argv[1], "detector")){          run_detector(argc, argv);      } else if (0 == strcmp(argv[1], "detect")){          float thresh = find_float_arg(argc, argv, "-thresh", .24);  		int ext_output = find_arg(argc, argv, "-ext_output");          char *filename = (argc > 4) ? argv[4]: 0;          test_detector("cfg/coco.data", argv[2], argv[3], filename, thresh, 0.5, 0, ext_output, 0, NULL, 0, 0);      } else if (0 == strcmp(argv[1], "cifar")){          run_cifar(argc, argv);      } else if (0 == strcmp(argv[1], "go")){          run_go(argc, argv);      } else if (0 == strcmp(argv[1], "rnn")){          run_char_rnn(argc, argv);      } else if (0 == strcmp(argv[1], "vid")){          run_vid_rnn(argc, argv);      } else if (0 == strcmp(argv[1], "coco")){          run_coco(argc, argv);      } else if (0 == strcmp(argv[1], "classify")){          predict_classifier("cfg/imagenet1k.data", argv[2], argv[3], argv[4], 5);      } else if (0 == strcmp(argv[1], "classifier")){          run_classifier(argc, argv);      } else if (0 == strcmp(argv[1], "art")){          run_art(argc, argv);      } else if (0 == strcmp(argv[1], "tag")){          run_tag(argc, argv);      } else if (0 == strcmp(argv[1], "compare")){          run_compare(argc, argv);      } else if (0 == strcmp(argv[1], "dice")){          run_dice(argc, argv);      } else if (0 == strcmp(argv[1], "writing")){          run_writing(argc, argv);      } else if (0 == strcmp(argv[1], "3d")){          composite_3d(argv[2], argv[3], argv[4], (argc > 5) ? atof(argv[5]) : 0);      } else if (0 == strcmp(argv[1], "test")){          test_resize(argv[2]);      } else if (0 == strcmp(argv[1], "captcha")){          run_captcha(argc, argv);      } else if (0 == strcmp(argv[1], "nightmare")){          run_nightmare(argc, argv);      } else if (0 == strcmp(argv[1], "rgbgr")){          rgbgr_net(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "reset")){          reset_normalize_net(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "denormalize")){          denormalize_net(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "statistics")){          statistics_net(argv[2], argv[3]);      } else if (0 == strcmp(argv[1], "normalize")){          normalize_net(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "rescale")){          rescale_net(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "ops")){          operations(argv[2]);      } else if (0 == strcmp(argv[1], "speed")){          speed(argv[2], (argc > 3 && argv[3]) ? atoi(argv[3]) : 0);      } else if (0 == strcmp(argv[1], "oneoff")){          oneoff(argv[2], argv[3], argv[4]);      } else if (0 == strcmp(argv[1], "partial")){          partial(argv[2], argv[3], argv[4], atoi(argv[5]));      } else if (0 == strcmp(argv[1], "visualize")){          visualize(argv[2], (argc > 3) ? argv[3] : 0);      } else if (0 == strcmp(argv[1], "imtest")){          test_resize(argv[2]);      } else {          fprintf(stderr, "Not an option: %sn", argv[1]);      }  

跟進run_detector

run_detector函數在src/detector.c裡面,這個函數首先有很多超參數可以設置,然後我們可以看到這個函數包含了訓練驗證,測試,計算Anchors,demo展示,計算map值和recall值等功能。由於訓練,測試,驗證階段差不多,我們跟進去一個看看就好,至於後面那幾個功能是AlexeyAB添加的,之後再逐一解釋。

void run_detector(int argc, char **argv)  {      int dont_show = find_arg(argc, argv, "-dont_show");//展示影像介面      int benchmark = find_arg(argc, argv, "-benchmark");//評估模型的表現      int benchmark_layers = find_arg(argc, argv, "-benchmark_layers");      //if (benchmark_layers) benchmark = 1;      if (benchmark) dont_show = 1;      int show = find_arg(argc, argv, "-show");      int letter_box = find_arg(argc, argv, "-letter_box");//是否對影像做letter-box變換      int calc_map = find_arg(argc, argv, "-map");//是否計算map值      int map_points = find_int_arg(argc, argv, "-points", 0);      check_mistakes = find_arg(argc, argv, "-check_mistakes");//檢查數據是否有誤      int show_imgs = find_arg(argc, argv, "-show_imgs");//顯示圖片      int mjpeg_port = find_int_arg(argc, argv, "-mjpeg_port", -1);      int json_port = find_int_arg(argc, argv, "-json_port", -1);      char *http_post_host = find_char_arg(argc, argv, "-http_post_host", 0);      int time_limit_sec = find_int_arg(argc, argv, "-time_limit_sec", 0);      char *out_filename = find_char_arg(argc, argv, "-out_filename", 0);      char *outfile = find_char_arg(argc, argv, "-out", 0);      char *prefix = find_char_arg(argc, argv, "-prefix", 0);//模型保存的前綴      float thresh = find_float_arg(argc, argv, "-thresh", .25);    // 置信度      float iou_thresh = find_float_arg(argc, argv, "-iou_thresh", .5);    // 0.5 for mAP      float hier_thresh = find_float_arg(argc, argv, "-hier", .5);      int cam_index = find_int_arg(argc, argv, "-c", 0);//攝影機編號      int frame_skip = find_int_arg(argc, argv, "-s", 0);//跳幀檢測間隔      int num_of_clusters = find_int_arg(argc, argv, "-num_of_clusters", 5);      int width = find_int_arg(argc, argv, "-width", -1);// 輸入網路的影像寬度      int height = find_int_arg(argc, argv, "-height", -1);// 輸入網路的影像高度      // extended output in test mode (output of rect bound coords)      // and for recall mode (extended output table-like format with results for best_class fit)      int ext_output = find_arg(argc, argv, "-ext_output");      int save_labels = find_arg(argc, argv, "-save_labels");      if (argc < 4) {          fprintf(stderr, "usage: %s %s [train/test/valid/demo/map] [data] [cfg] [weights (optional)]n", argv[0], argv[1]);          return;      }      char *gpu_list = find_char_arg(argc, argv, "-gpus", 0);// 多個gpu訓練      int *gpus = 0;      int gpu = 0;      int ngpus = 0;      if (gpu_list) {          printf("%sn", gpu_list);          int len = (int)strlen(gpu_list);          ngpus = 1;          int i;          for (i = 0; i < len; ++i) {              if (gpu_list[i] == ',') ++ngpus;          }          gpus = (int*)xcalloc(ngpus, sizeof(int));          for (i = 0; i < ngpus; ++i) {              gpus[i] = atoi(gpu_list);              gpu_list = strchr(gpu_list, ',') + 1;          }      }      else {          gpu = gpu_index;          gpus = &gpu;          ngpus = 1;      }        int clear = find_arg(argc, argv, "-clear");        char *datacfg = argv[3];//存儲訓練集,驗證集,以及類別對應名字等資訊的cfg文件      char *cfg = argv[4];//要訓練的網路cfg文件      char *weights = (argc > 5) ? argv[5] : 0;//是否有預訓練模型      if (weights)          if (strlen(weights) > 0)              if (weights[strlen(weights) - 1] == 0x0d) weights[strlen(weights) - 1] = 0;      char *filename = (argc > 6) ? argv[6] : 0;      if (0 == strcmp(argv[2], "test")) test_detector(datacfg, cfg, weights, filename, thresh, hier_thresh, dont_show, ext_output, save_labels, outfile, letter_box, benchmark_layers);//執行目標檢測模型測試      else if (0 == strcmp(argv[2], "train")) train_detector(datacfg, cfg, weights, gpus, ngpus, clear, dont_show, calc_map, mjpeg_port, show_imgs, benchmark_layers);//目標檢測模型訓練      else if (0 == strcmp(argv[2], "valid")) validate_detector(datacfg, cfg, weights, outfile);//目標檢測模型驗證      else if (0 == strcmp(argv[2], "recall")) validate_detector_recall(datacfg, cfg, weights);///計算驗證集的召回率      else if (0 == strcmp(argv[2], "map")) validate_detector_map(datacfg, cfg, weights, thresh, iou_thresh, map_points, letter_box, NULL);//計算驗證集的map值      else if (0 == strcmp(argv[2], "calc_anchors")) calc_anchors(datacfg, num_of_clusters, width, height, show);//計算驗證集的anchors      else if (0 == strcmp(argv[2], "demo")) {//demo展示          list *options = read_data_cfg(datacfg);          int classes = option_find_int(options, "classes", 20);          char *name_list = option_find_str(options, "names", "data/names.list");          char **names = get_labels(name_list);          if (filename)              if (strlen(filename) > 0)                  if (filename[strlen(filename) - 1] == 0x0d) filename[strlen(filename) - 1] = 0;          demo(cfg, weights, thresh, hier_thresh, cam_index, filename, names, classes, frame_skip, prefix, out_filename,              mjpeg_port, json_port, dont_show, ext_output, letter_box, time_limit_sec, http_post_host, benchmark, benchmark_layers);            free_list_contents_kvp(options);          free_list(options);      }      else printf(" There isn't such command: %s", argv[2]);        if (gpus && gpu_list && ngpus > 1) free(gpus);  }  

跟進train_detector

由於訓練,驗證和測試階段程式碼幾乎是差不多的,只不過訓練多了一個反向傳播的過程。所以我們主要分析一下訓練過程,訓練過程是一個比較複雜的過程,不過宏觀上大致分為解析網路配置文件,載入訓練樣本影像和標籤,開啟訓練,結束訓練保存模型這樣一個過程,部分程式碼如下(我省略了很多程式碼,因為這一節是框架總覽,後面我們會詳細解釋的):

void train_detector(char *datacfg, char *cfgfile, char *weightfile, int *gpus, int ngpus, int clear, int dont_show, int calc_map, int mjpeg_port, int show_imgs, int benchmark_layers)  {  	// 從options找出訓練圖片路徑資訊,如果沒找到,默認使用"data/train.list"路徑下的圖片資訊(train.list含有標準的資訊格式:<object-class> <x> <y> <width> <height>),      // 該文件可以由darknet提供的scripts/voc_label.py根據自行在網上下載的voc數據集生成,所以說是默認路徑,其實也需要使用者自行調整,也可以任意命名,不一定要為train.list,      // 甚至可以不用voc_label.py生成,可以自己不厭其煩的製作一個(當然規模應該是很小的,不然太累了。。。)      // 讀入後,train_images將含有訓練圖片中所有圖片的標籤以及定位資訊      list *options = read_data_cfg(datacfg);      char *train_images = option_find_str(options, "train", "data/train.txt");      char *valid_images = option_find_str(options, "valid", train_images);      char *backup_directory = option_find_str(options, "backup", "/backup/");        network net_map;      //如果要計算map      if (calc_map) {          FILE* valid_file = fopen(valid_images, "r");          if (!valid_file) {              printf("n Error: There is no %s file for mAP calculation!n Don't use -map flag.n Or set valid=%s in your %s file. n", valid_images, train_images, datacfg);              getchar();              exit(-1);          }          else fclose(valid_file);            cuda_set_device(gpus[0]);          printf(" Prepare additional network for mAP calculation...n");          net_map = parse_network_cfg_custom(cfgfile, 1, 1);          //分類數          const int net_classes = net_map.layers[net_map.n - 1].classes;            int k;  // free memory unnecessary arrays          for (k = 0; k < net_map.n - 1; ++k) free_layer_custom(net_map.layers[k], 1);            char *name_list = option_find_str(options, "names", "data/names.list");          int names_size = 0;          //獲取類別對應的名字          char **names = get_labels_custom(name_list, &names_size);          if (net_classes != names_size) {              printf(" Error: in the file %s number of names %d that isn't equal to classes=%d in the file %s n",                  name_list, names_size, net_classes, cfgfile);              if (net_classes > names_size) getchar();          }          free_ptrs((void**)names, net_map.layers[net_map.n - 1].classes);      }        srand(time(0));       // 提取配置文件名稱中的主要資訊,用於輸出列印(並無實質作用),比如提取cfg/yolo.cfg中的yolo,用於下面的輸出列印      char *base = basecfg(cfgfile);      printf("%sn", base);      float avg_loss = -1;      // 構建網路:用多少塊GPU,就會構建多少個相同的網路(不使用GPU時,ngpus=1)      network* nets = (network*)xcalloc(ngpus, sizeof(network));    	//設定隨機數種子      srand(time(0));      int seed = rand();      int i;        // for循環次數為ngpus,使用多少塊GPU,就循環多少次(不使用GPU時,ngpus=1,也會循環一次)      // 這裡每一次循環都會構建一個相同的神經網路,如果提供了初始訓練參數,也會為每個網路導入相同的初始訓練參數      for (i = 0; i < ngpus; ++i) {          srand(seed);  #ifdef GPU          cuda_set_device(gpus[i]);  #endif  		//解析網路配置文件          nets[i] = parse_network_cfg(cfgfile);          //測試某一個網路層的相關指標如運行時間          nets[i].benchmark_layers = benchmark_layers;          //如果有預訓練模型則載入          if (weightfile) {              load_weights(&nets[i], weightfile);          }          //          if (clear) *nets[i].seen = 0;          nets[i].learning_rate *= ngpus;      }      ...  }  

解析配置文件

截圖部分yolov3.cfg網路配置文件如下:

可以看到配置參數大概分為2類:

  • 與訓練相關的項,以 [net] 行開頭的段. 其中包含的參數有: batch_size, width,height,channel,momentum,decay,angle,saturation, exposure,hue,learning_rate,burn_in,max_batches,policy,steps,scales
  • 不同類型的層的配置參數. 如[convolutional], [short_cut], [yolo], [route], [upsample]層等。

在src/parse.c中我們會看到一行程式碼,net->batch /= net->subdivisions;,也就是說batch_size 在 darknet 內部又被均分為 net->subdivisions份, 成為更小的batch_size。 但是這些小的 batch_size 最終又被匯總, 因此 darknet 中的batch_size = net->batch / net->subdivisions * net->subdivisions。此外,和這個參數相關的計算訓練圖片數目的時候是這樣,int imgs = net->batch * net->subdivisions * ngpus;,這樣可以保證imgs可以被subdivisions整除,因此,通常將這個參數設為8的倍數。從這裡也可以看出每個gpu或者cpu都會訓練batch個樣本。

我們知道了參數是什麼樣子,那麼darknet是如何保存這些參數的呢?這就要看下基本數據結構了。

數據結構

後面細講。

前向傳播

後面細講。

反向傳播

後面細講。


注意,講完數據結構之後講前向傳播和反向傳播的時候會深入到每個Layer的前向傳播和反向傳播,細節會很多,慢慢理解慢慢講。框架的大概思路就是這個樣子,之後會按照這個主線不斷的添加程式碼理解。

後記

大概了解一下我會怎麼去讀Darknet框架,之後會按照這個主線不斷的講解,歡迎互相交流和學習。

歡迎關注GiantPandaCV, 在這裡你將看到獨家的深度學習分享,堅持原創,每天分享我們學習到的新鮮知識。( • ̀ω•́ )✧