前言
昨天講了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_threads
在
src/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
操作中,可以看到這裡隻有
random
和
count
同時滿足條件的情況下會做
resize
操作,也就是說
resize
加載的資料是未進行
resize
過的,是以,需要調整
args
中的圖像寬高之後再重新調用
load_data(args)
加載資料。反之,不做任何處理,之前加載的資料仍然可用。第三次調用就是在資料加載完成後,将加載好的資料儲存起來
train=buffer
; 然後開始下一次的加載工作。這一次的資料就會進行這一次的訓練操作(調用
train_network
函數)。