【AlexeyAB DarkNet框架解析】三,載入數據進行訓練

  • 2020 年 2 月 21 日
  • 筆記

前言

昨天講了DarkNet的底層數據結構,並且將網路配置文件進行了解析存放到了一個network結構體中,那麼今天我們就要來看一下Darknet是如何載入數據進行訓練的。

載入訓練數據

DarkNet的數據載入函數load_data()src/data.c中實現(src/detector.c函數中的train_detector直接調用這個函數載入數據)。load_data()函數調用流程如下:load_data(args)->load_threads()->load_data_in_threads()->load_thread()->load_data_detection(),前四個函數都是在對執行緒的調用進行封裝。最底層的數據載入任務由load_data_detection()函數完成。所有的數據(圖片數據和標註資訊數據)載入完成之後再拼接到一個大的數組中。在DarkNet中,圖片的存儲形式是一個行向量,向量長度為h*w*3。同時圖片被歸一化到[0, 1]之間。

load_threads()完成執行緒分配和數據拼接

load_threadssrc/data.c中實現,程式碼如下:

// copy from https://github.com/hgpvision/darknet/blob/master/src/data.c#L355  /*  ** 開闢多個執行緒讀入圖片數據,讀入數據存儲至ptr.d中(主要調用load_in_thread()函數完成)  ** 輸入:ptr    包含所有執行緒要讀入圖片數據的資訊(讀入多少張,開幾個執行緒讀入,讀入圖片最終的寬高,圖片路徑等等)  ** 返回:void*  萬能指針(實際上不需要返回什麼)  ** 說明:1) load_threads()是一個指針函數,只是一個返回變數為void*的普通函數,不是函數指針  **       2) 輸入ptr是一個void*指針(萬能指針),使用時需要強轉為具體類型的指針  **       3) 函數中涉及四個用來存儲讀入數據的變數:ptr, args, out, buffers,除args外都是data*類型,所有這些變數的  **          指針變數其實都指向同一塊記憶體(當然函數中間有些動態變化),因此讀入的數據都是互通的。  ** 流程:本函數首先會獲取要讀入圖片的張數、要開啟執行緒的個數,而後計算每個執行緒應該讀入的圖片張數(儘可能的均勻分配),  **       並創建所有的執行緒,並行讀入數據,最後合併每個執行緒讀入的數據至一個大data中,這個data的指針變數與ptr的指針變數  **       指向的是統一塊記憶體,因此也就最終將數據讀入到ptr.d中(所以其實沒有返回值)  */  void *load_threads(void *ptr)  {      //srand(time(0));      int i;  	// 先使用(load_args*)強轉void*指針,而後取ptr所指內容賦值給args      // 雖然args不是指針,args是深拷貝了ptr中的內容,但是要知道ptr(也就是load_args數據類型),有很多的      // 指針變數,args深拷貝將拷貝這些指針變數到args中(這些指針變數本身對ptr來說就是內容,      // 而args所指的值是args的內容,不是ptr的,不要混為一談),因此,args與ptr將會共享所有指針變數所指的內容      load_args args = *(load_args *)ptr;      if (args.threads == 0) args.threads = 1;  	// 另指針變數out=args.d,使得out與args.d指向統一塊記憶體,之後,args.d所指的記憶體塊會變(反正也沒什麼用了,變就變吧),      // 但out不會變,這樣可以保證out與最原始的ptr指向同一塊存儲讀入圖片數據的記憶體塊,因此最終將圖片讀到out中,      // 實際就是讀到了最原始的ptr中,比如train_detector()函數中定義的args.d中      data *out = args.d;  	// 讀入圖片的總張數= batch * subdivision * ngpus,可參見train_detector()函數中的賦值      int total = args.n;  	// 釋放ptr:ptr是傳入的指針變數,傳入的指針變數本身也是按值傳遞的,即傳入函數之後,指針變數得到複製,函數內的形參ptr      // 獲取外部實參的值之後,二者本身沒有關係,但是由於是指針變數,二者之間又存在一絲關係,那就是函數內形參與函數外實參指向      // 同一塊記憶體。又由於函數外實參記憶體是動態分配的,因此函數內的形參可以使用free()函數進行記憶體釋放,但一般不推薦這麼做,因為函數內釋放記憶體,      // 會影響函數外實參的使用,可能使之成為野指針,那為什麼這裡可以用free()釋放ptr呢,不會出現問題嗎?      // 其一,因為ptr是一個結構體,是一個包含眾多的指針變數的結構體,如data* d等(當然還有其他非指針變數如int h等),      // 直接free(ptr)將會導致函數外實參無法再訪問非指針變數int h等(實際經過測試,在gcc編譯器下,能訪問但是值被重新初始化為0),      // 因為函數內形參和函數外實參共享一塊堆記憶體,而這些非指針變數都是存在這塊堆記憶體上的,記憶體一釋放,就無法訪問了;      // 但是對於指針變數,free(ptr)將無作為(這個結論也是經過測試的,也是用的gcc編譯器),不會釋放或者擦寫掉ptr指針變數本身的值,      // 當然也不會影響函數外實參,更不會牽扯到這些指針變數所指的記憶體塊,總的來說,      // free(ptr)將使得ptr不能再訪問指針變數(如int h等,實際經過測試,在gcc編譯器下,能訪問但是值被重新初始化為0),      // 但其指針變數本身沒有受影響,依舊可以訪問;對於函數外實參,同樣不能訪問非指針變數,而指針變數不受影響,依舊可以訪問。      // 其二,darknet數據讀取的實現一層套一層(似乎有點羅嗦,總感覺程式碼可以不用這麼寫的:)),具體調用過程如下:      // load_data(load_args args)->load_threads(load_args* ptr)->load_data_in_thread(load_args args)->load_thread(load_args* ptr),      // 就在load_data()中,重新定義了ptr,並為之動態分配了記憶體,且深拷貝了傳給load_data()函數的值args,也就是說在此之後load_data()函數中的args除了其中的指針變數指著同一塊堆記憶體之外,      // 二者的非指針變數再無瓜葛,不管之後經過多少個函數,對ptr的非指針變數做了什麼改動,比如這裡直接free(ptr),使得非指針變數值為0,都不會影響load_data()中的args的非指針變數,也就不會影響更為頂層函數中定義的args的非指針變數的值,      // 比如train_detector()函數中的args,train_detector()對args非指針變數賦的值都不會受影響,保持不變。綜其兩點,此處直接free(ptr)是安全的。      // 說明:free(ptr)函數,確定會做的事是使得記憶體塊可以重新分配,且不會影響指針變數ptr本身的值,也就是ptr還是指向那塊地址, 雖然可以使用,但很危險,因為這塊記憶體實際是無效的,      //      系統已經認為這塊記憶體是可分配的,會毫不考慮的將這塊記憶體分給其他變數,這樣,其值隨時都可能會被其他變數改變,這種情況下的ptr指針就是所謂的野指針(所以經常可以看到free之後,置原指針為NULL)。      //      而至於free(ptr)還不會做其他事情,比如會不會重新初始化這塊記憶體為0(擦寫掉),以及怎麼擦寫,這些操作,是不確定的,可能跟具體的編譯器有關(個人猜測),      //      經過測試,對於gcc編譯器,free(ptr)之後,ptr中的非指針變數的地址不變,但其值全部擦寫為0;ptr中的指針變數,絲毫不受影響,指針變數本身沒有被擦寫,      //      存儲的地址還是指向先前分配的記憶體塊,所以ptr能夠正常訪問其指針變數所指的值。測試程式碼為darknet_test_struct_memory_free.c。      //      不知道這段測試程式碼在VS中執行會怎樣,還沒經過測試,也不知道換用其他編譯器(darknet的Makefile文件中,指定了編譯器為gcc),darknet的編譯會不會有什麼問題??      //      關於free(),可以看看:http://blog.sina.com.cn/s/blog_615ec1630102uwle.html,文章最後有一個很有意思的比喻,但意思好像就和我這裡說的有點不一樣了(到底是不是編譯器搞得鬼呢??)。      free(ptr);  	// 每一個執行緒都會讀入一個data,定義並分配args.thread個data的記憶體      data* buffers = (data*)xcalloc(args.threads, sizeof(data));      pthread_t* threads = (pthread_t*)xcalloc(args.threads, sizeof(pthread_t));  	// 此處定義了多個執行緒,並為每個執行緒動態分配記憶體      for(i = 0; i < args.threads; ++i){  		// 此處就承應了上面的注釋,args.d指針變數本身發生了改動,使得本函數的args.d與out不再指向同一塊記憶體,          // 改為指向buffers指向的某一段記憶體,因為下面的load_data_in_thread()函數統一了結口,需要輸入一個load_args類型參數,          // 實際是想把圖片數據讀入到buffers[i]中,只能令args.d與buffers[i]指向同一塊記憶體          args.d = buffers + i;  		 // 下面這句很有意思,因為有多個執行緒,所有執行緒讀入的總圖片張數為total,需要將total均勻的分到各個執行緒上,          // 但很可能會遇到total不能整除的args.threads的情況,比如total = 61, args.threads =8,顯然不能做到          // 完全均勻的分配,但又要保證讀入圖片的總張數一定等於total,用下面的語句剛好在盡量均勻的情況下,          // 保證總和為total,比如61,那麼8個執行緒各自讀入的照片張數分別為:7, 8, 7, 8, 8, 7, 8, 8          args.n = (i+1) * total/args.threads - i * total/args.threads;  		// 開啟執行緒,讀入數據到args.d中(也就讀入到buffers[i]中)          // load_data_in_thread()函數返回所開啟的執行緒,並存儲之前已經動態分配記憶體用來存儲所有執行緒的threads中,          // 方便下面使用pthread_join()函數控制相應執行緒          threads[i] = load_data_in_thread(args);      }      for(i = 0; i < args.threads; ++i){  		// 以阻塞的方式等待執行緒threads[i]結束:阻塞是指阻塞啟動該子執行緒的母執行緒(此處應為主執行緒),          // 是母執行緒處於阻塞狀態,一直等待所有子執行緒執行完(讀完所有數據)才會繼續執行下面的語句          // 關於多執行緒的使用,進行過程式碼測試,測試程式碼對應:darknet_test_pthread_join.c          pthread_join(threads[i], 0);      }  	// 多個執行緒讀入所有數據之後,分別存儲到buffers[0],buffers[1]...中,接著使用concat_datas()函數將buffers中的數據全部合併成一個大數組得到out      *out = concat_datas(buffers, args.threads);  	 // 也就只有out的shallow敢置為0了,為什麼呢?因為out是此次迭代讀入的最終數據,該數據參與訓練(用完)之後,當然可以深層釋放了,而此前的都是中間變數,      // 還處於讀入數據階段,萬不可設置shallow=0      out->shallow = 0;  	// 釋放buffers,buffers也是個中間變數,切記shallow設置為1,如果設置為0,那就連out中的數據也沒了      for(i = 0; i < args.threads; ++i){          buffers[i].shallow = 1;          free_data(buffers[i]);      }  	// 最終直接釋放buffers,threads,注意buffers是一個存儲data的一維數組,上面循環中的記憶體釋放,實際是釋放每一個data的部分記憶體      // (這部分記憶體對data而言是非主要記憶體,不是存儲讀入數據的記憶體塊,而是存儲指向這些記憶體塊的指針變數,可以釋放的)      free(buffers);      free(threads);      return 0;  }  

load_data_in_thread()分配執行緒

load_data_in_thread()函數仍然在src/data.c中,程式碼如下:

/*  ** 創建一個執行緒,讀入相應圖片數據(此時args.n不再是一次迭代讀入的所有圖片的張數,而是經過load_threads()均勻分配給每個執行緒的圖片張數)  ** 輸入:args    包含該執行緒要讀入圖片數據的資訊(讀入多少張,讀入圖片最終的寬高,圖片路徑等等)  ** 返回:phtread_t   執行緒id  ** 說明:本函數實際沒有做什麼,就是深拷貝了args給ptr,然後創建了一個調用load_thread()函數的執行緒並返回執行緒id  */  pthread_t load_data_in_thread(load_args args)  {      pthread_t thread;  	// 同樣第一件事深拷貝了args給ptr(為什麼每次都要做這一步呢?求指點啊~)      struct load_args* ptr = (load_args*)xcalloc(1, sizeof(struct load_args));      *ptr = args;  	// 創建一個執行緒,讀入相應數據,綁定load_thread()函數到該執行緒上,第四個參數是load_thread()的輸入參數,第二個參數表示執行緒屬性,設置為0(即NULL)      if(pthread_create(&thread, 0, load_thread, ptr)) error("Thread creation failed");      return thread;  }  

load_data_detection()完成底層的數據載入任務

load_data_detection()函數也定義在src/data.c中,帶注釋的程式碼如下:

/*  ** 可以參考,看一下對影像進行jitter處理的各種效果:  ** https://github.com/vxy10/ImageAugmentation  ** 從所有訓練圖片中,隨機讀取n張,並對這n張圖片進行數據增強,同時矯正增強後的數據標籤資訊。最終得到的圖片的寬高為w,h(原始訓練集中的圖片尺寸不定),也就是網路能夠處理的圖片尺寸,  ** 數據增強包括:對原始圖片進行寬高方向上的插值縮放(兩方向上縮放係數不一定相同),下面稱之為縮放抖動;隨機摳取或者平移圖片(位置抖動);  ** 在hsv顏色空間增加雜訊(顏色抖動);左右水平翻轉,不含旋轉抖動。  ** 輸入:n         一個執行緒讀入的圖片張數(詳見函數內部注釋)  **       paths     所有訓練圖片所在路徑集合,是一個二維數組,每一行對應一張圖片的路徑(將在其中隨機取n個)  **       m         paths的行數,也即訓練圖片總數  **       w         網路能夠處理的圖的寬度(也就是輸入圖片經過一系列數據增強、變換之後最終輸入到網路的圖的寬度)  **       h         網路能夠處理的圖的高度(也就是輸入圖片經過一系列數據增強、變換之後最終輸入到網路的圖的高度)  **       c         用來指定訓練圖片的通道數(默認為3,即RGB圖)  **       boxes     每張訓練圖片最大處理的矩形框數(圖片內可能含有更多的物體,即更多的矩形框,那麼就在其中隨機選擇boxes個參與訓練,具體執行在fill_truth_detection()函數中)  **       classes   類別總數,本函數並未用到(fill_truth_detection函數其實並沒有用這個參數)  **       use_flip  是否使用水平翻轉  **       use_mixup 是否使用mixup數據增強  **       jitter    這個參數為縮放抖動係數,就是圖片縮放抖動的劇烈程度,越大,允許的抖動範圍越大(所謂縮放抖動,就是在寬高上插值縮放圖片,寬高兩方向上縮放的係數不一定相同)  **       hue       顏色(hsv顏色空間)數據增強參數:色調(取值0度到360度)偏差最大值,實際色調偏差為-hue~hue之間的隨機值  **       saturation 顏色(hsv顏色空間)數據增強參數:色彩飽和度(取值範圍0~1)縮放最大值,實際為範圍內的隨機值  **       exposure  顏色(hsv顏色空間)數據增強參數:明度(色彩明亮程度,0~1)縮放最大值,實際為範圍內的隨機值  **       mini_batch      和目標跟蹤有關,這裡不關注  **       track           和目標跟蹤有關,這裡不關注  **       augment_speed   和目標跟蹤有關,這裡不關注  **       letter_box 是否進行letter_box變換  **       show_imgs  ** 返回:data類型數據,包含一個執行緒讀入的所有圖片數據(含有n張圖片)  ** 說明:最後四個參數用於數據增強,主要對原圖進行縮放抖動,位置抖動(平移)以及顏色抖動(顏色值增加一定雜訊),抖動一定程度上可以理解成對影像增加雜訊。  **       通過對原始影像進行抖動,實現數據增強。最後三個參數具體用法參考本函數內調用的random_distort_image()函數  ** 說明2:從此函數可以看出,darknet對訓練集中圖片的尺寸沒有要求,可以是任意尺寸的圖片,因為經該函數處理(縮放/裁剪)之後,  **       不管是什麼尺寸的照片,都會統一為網路訓練使用的尺寸  */    data load_data_detection(int n, char **paths, int m, int w, int h, int c, int boxes, int classes, int use_flip, int use_blur, int use_mixup, float jitter,      float hue, float saturation, float exposure, int mini_batch, int track, int augment_speed, int letter_box, int show_imgs)  {      const int random_index = random_gen();      c = c ? c : 3;      char **random_paths;      char **mixup_random_paths = NULL;  	// paths包含所有訓練圖片的路徑,get_random_paths函數從中隨機提出n條,即為此次讀入的n張圖片的路徑      if(track) random_paths = get_sequential_paths(paths, n, m, mini_batch, augment_speed);      else random_paths = get_random_paths(paths, n, m);        assert(use_mixup < 2);      int mixup = use_mixup ? random_gen() % 2 : 0;      //printf("n mixup = %d n", mixup);  	// 如果使用mixup策略,需要再隨機取出n條數據,即n張圖片      if (mixup) {          if (track) mixup_random_paths = get_sequential_paths(paths, n, m, mini_batch, augment_speed);          else mixup_random_paths = get_random_paths(paths, n, m);      }        int i;  	// 初始化為0,清空記憶體中之前的舊值      data d = { 0 };      d.shallow = 0;  	// 一次讀入的圖片張數:d.X中每行就是一張圖片的數據,因此d.X.cols等於h*w*3      // n = net.batch * net.subdivisions * ngpus,net中的subdivisions這個參數暫時還沒搞懂有什麼用,      // 從parse_net_option()函數可知,net.batch = net.batch / net.subdivision,等號右邊的那個batch就是      // 網路配置文件.cfg中設置的每個batch的圖片數量,但是不知道為什麼多了subdivision這個參數?總之,      // net.batch * net.subdivisions又得到了在網路配置文件中設定的batch值,然後乘以ngpus,是考慮多個GPU實現數據並行,      // 一次讀入多個batch的數據,分配到不同GPU上進行訓練。在load_threads()函數中,又將整個的n僅可能均勻的劃分到每個執行緒上,      // 也就是總的讀入圖片張數為n = net.batch * net.subdivisions * ngpus,但這些圖片不是一個執行緒讀完的,而是分配到多個執行緒並行讀入,      // 因此本函數中的n實際不是總的n,而是分配到該執行緒上的n,比如總共要讀入128張圖片,共開啟8個執行緒讀數據,那麼本函數中的n為16,而不是總數128      d.X.rows = n;  	//d.X為一個matrix類型數據,其中d.X.vals是其具體數據,是指針的指針(即為二維數組),此處先為第一維動態分配記憶體      d.X.vals = (float**)xcalloc(d.X.rows, sizeof(float*));      d.X.cols = h*w*c;        float r1 = 0, r2 = 0, r3 = 0, r4 = 0, r_scale;      float dhue = 0, dsat = 0, dexp = 0, flip = 0;      int augmentation_calculated = 0;  	// d.y存儲了所有讀入照片的標籤資訊,每條標籤包含5條資訊:類別,以及矩形框的x,y,w,h      // boxes為一張圖片最多能夠處理(參與訓練)的矩形框的數(如果圖片中的矩形框數多於這個數,那麼隨機挑選boxes個,這個參數僅在parse_region以及parse_detection中出現,好奇怪?      // 在其他網路解析函數中並沒有出現)。同樣,d.y是一個matrix,make_matrix會指定y的行數和列數,同時會為其第一維動態分配記憶體      d.y = make_matrix(n, 5 * boxes);      int i_mixup = 0;      for (i_mixup = 0; i_mixup <= mixup; i_mixup++) {          if (i_mixup) augmentation_calculated = 0;          for (i = 0; i < n; ++i) {                float *truth = (float*)xcalloc(5 * boxes, sizeof(float));              char *filename = (i_mixup) ? mixup_random_paths[i] : random_paths[i];  			//讀入原始的圖片              image orig = load_image(filename, 0, 0, c);  			// 原始影像長寬              int oh = orig.h;              int ow = orig.w;  			// 縮放抖動大小:縮放抖動係數乘以原始圖寬高即得像素單位意義上的縮放抖動              int dw = (ow*jitter);              int dh = (oh*jitter);                if (!augmentation_calculated || !track)              {                  augmentation_calculated = 1;                  r1 = random_float();                  r2 = random_float();                  r3 = random_float();                  r4 = random_float();                    r_scale = random_float();                    dhue = rand_uniform_strong(-hue, hue);                  dsat = rand_scale(saturation);                  dexp = rand_scale(exposure);                    flip = use_flip ? random_gen() % 2 : 0;              }                int pleft = rand_precalc_random(-dw, dw, r1);              int pright = rand_precalc_random(-dw, dw, r2);              int ptop = rand_precalc_random(-dh, dh, r3);              int pbot = rand_precalc_random(-dh, dh, r4);  			// 這個係數沒用到              float scale = rand_precalc_random(.25, 2, r_scale); // unused currently                if (letter_box)              {                  float img_ar = (float)ow / (float)oh; //原始影像寬高比                  float net_ar = (float)w / (float)h; //輸入到網路要求的影像寬高比                  float result_ar = img_ar / net_ar; //兩者求比值來判斷如何進行letter_box縮放                  //printf(" ow = %d, oh = %d, w = %d, h = %d, img_ar = %f, net_ar = %f, result_ar = %f n", ow, oh, w, h, img_ar, net_ar, result_ar);                  if (result_ar > 1)  // sheight - should be increased                  {                      float oh_tmp = ow / net_ar;                      float delta_h = (oh_tmp - oh) / 2;                      ptop = ptop - delta_h;                      pbot = pbot - delta_h;                      //printf(" result_ar = %f, oh_tmp = %f, delta_h = %d, ptop = %f, pbot = %f n", result_ar, oh_tmp, delta_h, ptop, pbot);                  }                  else  // swidth - should be increased                  {                      float ow_tmp = oh * net_ar;                      float delta_w = (ow_tmp - ow) / 2;                      pleft = pleft - delta_w;                      pright = pright - delta_w;                      //printf(" result_ar = %f, ow_tmp = %f, delta_w = %d, pleft = %f, pright = %f n", result_ar, ow_tmp, delta_w, pleft, pright);                  }              }  			// 以下步驟就是執行了letter_box變換              int swidth = ow - pleft - pright;              int sheight = oh - ptop - pbot;                float sx = (float)swidth / ow;              float sy = (float)sheight / oh;                image cropped = crop_image(orig, pleft, ptop, swidth, sheight);                float dx = ((float)pleft / ow) / sx;              float dy = ((float)ptop / oh) / sy;  			// resize到指定大小              image sized = resize_image(cropped, w, h);              // 翻轉  			if (flip) flip_image(sized);  			//隨機對影像jitter(在hsv三個通道上添加擾動),實現數據增強              distort_image(sized, dhue, dsat, dexp);              //random_distort_image(sized, hue, saturation, exposure);  			// truth包含所有影像的標籤資訊(包括真實類別與位置              // 因為對原始圖片進行了數據增強,其中的平移抖動勢必會改動每個物體的矩形框標籤資訊(主要是矩形框的像素坐標資訊),需要根據具體的數據增強方式進行相應矯正              // 後面的參數就是用於數據增強後的矩形框資訊矯正              fill_truth_detection(filename, boxes, truth, classes, flip, dx, dy, 1. / sx, 1. / sy, w, h);                if (i_mixup) {                  image old_img = sized;                  old_img.data = d.X.vals[i];                  //show_image(sized, "new");                  //show_image(old_img, "old");                  //wait_until_press_key_cv();  				// 做mixup,混合係數為0.5                  blend_images(sized, 0.5, old_img, 0.5);  				// 標籤也要對應改變                  blend_truth(truth, boxes, d.y.vals[i]);                  free_image(old_img);              }                d.X.vals[i] = sized.data;              memcpy(d.y.vals[i], truth, 5 * boxes * sizeof(float));                if (show_imgs)// && i_mixup)              {                  char buff[1000];                  sprintf(buff, "aug_%d_%d_%s_%d", random_index, i, basecfg(filename), random_gen());                    int t;                  for (t = 0; t < boxes; ++t) {                      box b = float_to_box_stride(d.y.vals[i] + t*(4 + 1), 1);                      if (!b.x) break;                      int left = (b.x - b.w / 2.)*sized.w;                      int right = (b.x + b.w / 2.)*sized.w;                      int top = (b.y - b.h / 2.)*sized.h;                      int bot = (b.y + b.h / 2.)*sized.h;                      draw_box_width(sized, left, top, right, bot, 1, 150, 100, 50); // 3 channels RGB                  }                    save_image(sized, buff);                  if (show_imgs == 1) {                      show_image(sized, buff);                      wait_until_press_key_cv();                  }                  printf("nYou use flag -show_imgs, so will be saved aug_...jpg images. Press Enter: n");                  //getchar();              }                free_image(orig);              free_image(cropped);              free(truth);          }      }      free(random_paths);      if (mixup_random_paths) free(mixup_random_paths);      return d;  }  #endif    // OPENCV  

load_data(args)使用方法

src/detector.c中的的train_detector()函數共有3次調用load_data(args),第一次調用是為訓練階段做好數據準備工作,充分利用這段時間來載入數據。第二次調用是在resize操作中,可以看到這裡只有randomcount同時滿足條件的情況下會做resize操作,也就是說resize載入的數據是未進行resize過的,因此,需要調整args中的影像寬高之後再重新調用load_data(args)載入數據。反之,不做任何處理,之前載入的數據仍然可用。第三次調用就是在數據載入完成後,將載入好的數據保存起來train=buffer; 然後開始下一次的載入工作。這一次的數據就會進行這一次的訓練操作(調用train_network函數)。

後記

本節從源碼角度分析了DarkNet如何載入數據進行訓練的詳細步驟。相信結合前2節,你已經知道DarkNet是如何構建網路模型,並載入數據訓練一個檢測器模型的了。

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