darknet框架權(quán)威解讀系列一:框架構(gòu)成
AI編輯:我是小將
本文作者:陳訓(xùn)教
本文已由原作者授權(quán),不得擅自二次轉(zhuǎn)載
前 言
本系列文章旨在通過解讀darknet整體框架,一方面可以探究深度學(xué)習(xí)原理的底層實(shí)現(xiàn)機(jī)制,另一方面,提升C語(yǔ)言能力。截止目前,在github上解讀darknet的兩個(gè)好的項(xiàng)目(https://github.com/hgpvision/darknet和https://github.com/BBuf/Darknet),其中hgpvision主要針對(duì)pjreddie項(xiàng)目解讀,而BBuf主要是針對(duì)AB大神的darknet項(xiàng)目解讀。這里特別感謝兩位前輩給出的精彩解讀。
本系列文章按照由整體到部分,原理(框架)解讀+代碼解析的思路進(jìn)行解讀,首先給出整個(gè)darknet的框架構(gòu)成,然后詳細(xì)解讀每一部分。詳細(xì)的darknet項(xiàng)目解讀地址:https://github.com/ChenCVer/darknet。
框架總覽
darknet項(xiàng)目工程代碼結(jié)構(gòu)如下圖所示:

各文件夾的作用大致如下:
3rdparty:存放第三方庫(kù);
cfg:存放各種配置文件,比如網(wǎng)絡(luò)配置文件(例如:yolov4.cfg)和data配置文件(比如voc.data)等;
data:類似于標(biāo)準(zhǔn)C工程代碼中的resource文件夾,用來存放data,比如你的voc數(shù)據(jù)集等;
files:這個(gè)文件夾是我自己加的,主要存放對(duì)于darknet某些具體細(xì)節(jié)代碼的詳細(xì)說明;
include:存放darknet頭文件,主要用于win系統(tǒng);
pre-train-weighted:用于存放預(yù)訓(xùn)練權(quán)重文件(我自己加的);
scripts:從來存放一些腳本文件等;
src:用來存放所有源代碼(諸如:各種類型的網(wǎng)絡(luò)層結(jié)構(gòu),重要的工具函數(shù)等),這個(gè)文件夾最重要。
darknet總體框架如下圖所示(幾乎所有的框架都是這個(gè)流程):

框架最簡(jiǎn)代碼可以寫成下述形式(注意,代碼只是整體框架的邏輯思路):
int??main(int?argc,?char**?argv){
????//?step1:?網(wǎng)絡(luò)構(gòu)建及初始化
????//?解析net.cfg配置文件
????list?*sections?=?read_cfg(filename);
????//?為網(wǎng)絡(luò)分配內(nèi)存空間
????network*?net?=?(network*)xcalloc(1,?sizeof(network));
????//?為網(wǎng)絡(luò)中的指針變量?jī)?nèi)存空間
????*net?=?make_network(sections->size?-?1);
????//?解析xxx.cfg網(wǎng)絡(luò)配置文件,同時(shí)初始化網(wǎng)絡(luò)
????parse_net_options(options,?&net);
????// step2:解析datacfg文件???
????//?解析data配置文件,?存入list雙頭鏈表中
????list?*options?=?read_data_cfg(datacfg);
????//?多線程加載數(shù)據(jù),?其中args包含一系列與loaddata相關(guān)的參數(shù),?比如數(shù)據(jù)增強(qiáng)等
????pthread_t?load_thread?=?load_data(args);
????//?step3:?訓(xùn)練網(wǎng)絡(luò)
????while?(get_current_iteration(net)?????????pthread_join(load_thread,?0);??//?數(shù)據(jù)一次性load完畢
????????//?for循環(huán)中會(huì)形成累計(jì)梯度
????????for(i?=?0;?i??????????forward_network(net);???//?前向傳播
???backward_network(net);??//?反向傳播
????????}?
?????????update_network(net);????//?一次性更新參數(shù)
????}
????return?0;
}
從上面的代碼中需要注意一個(gè)問題,相信大家在用darknet的時(shí)候都會(huì)看到網(wǎng)絡(luò)配置文件中的batch和subdivisions這兩個(gè)參數(shù),darknet框架是將batch拆分成subdivisions份,也就是說,在進(jìn)行數(shù)據(jù)加載的時(shí)候,darknet是遵循一次性加載batch個(gè)數(shù)據(jù),但是在進(jìn)行前向傳播和反向傳播的時(shí)候,每次只利用batch/subdivisions個(gè)數(shù)據(jù)。這里可知上述代碼中的n=batch/subdivisions。這樣做的目的是,緩解GPU顯存壓力,同時(shí)可以獲得相似的大batch更新效果。其實(shí)這樣做還是與一次性前向和反向batch數(shù)據(jù)有區(qū)別的,最主要的區(qū)別在于BN層的計(jì)算。
代碼解讀
darknet框架的所有功能入口在src/darknet.c文件中的main函數(shù)里面,其主函數(shù)如下所示(由于代碼太長(zhǎng),會(huì)有刪減)
int?main(int?argc,?char?**argv)
{
????if?(0?==?strcmp(argv[1],?"detector")){
????????run_detector(argc,?argv);????//?檢測(cè)算法入口(<==分析入口)
????}else?if?(0?==?strcmp(argv[1],?"yolo")){
????????run_yolo(argc,?argv);????????//?YOLO系列算法入口
????}else?if?(0?==?strcmp(argv[1],?"rnn")){
????????run_char_rnn(argc,?argv);????//?rnn算法入口
????}else?if?(0?==?strcmp(argv[1],?"classifier")){
????????run_classifier(argc,?argv);??//?分類算法入口
????}else?{
????????fprintf(stderr,?"Not?an?option:?%s\n",?argv[1]);
????}
????return?0;
}
由main函數(shù)可知,darknet不僅支持目標(biāo)檢測(cè)系列算法,還支持RNN和分類算法,這里還需要注意的是,run_yolo()和run_detector()其實(shí)是一回事,后續(xù)調(diào)用的函數(shù)是同一個(gè),這里應(yīng)該是AB大神為了兼容老版darknet框架。另外,對(duì)于darknet來說,他的精髓其實(shí)是在yolo算法,如果你非要用darknet來做分類任務(wù),也不是不行,就是其數(shù)據(jù)增強(qiáng)操作太少,我嘗試將opencv嵌入到darknet中,作為數(shù)據(jù)增強(qiáng)庫(kù),結(jié)果發(fā)現(xiàn)十分難搞(主要是由于本人太菜)。darknet自身所帶的數(shù)據(jù)增強(qiáng)操作不如python第三方庫(kù)那么豐富。本人也嘗試用darknet進(jìn)行分類網(wǎng)絡(luò)的訓(xùn)練,結(jié)果也不太理想,loss遲遲下不去。所以推薦對(duì)于圖像分類,圖像分割(darknet不支持做分割任務(wù),需要自己寫代碼實(shí)現(xiàn),網(wǎng)上有人實(shí)現(xiàn)過)更傾向于用pytorch框架實(shí)現(xiàn)。鑒于此情況,本系列解讀也只是針對(duì)檢測(cè)算法,不過檢測(cè)算法中所有代碼均已包含分類代碼,由于本人沒有研究過RNN,所以,相關(guān)代碼沒有做注釋(這里十分抱歉啦)。為了讓讀者對(duì)darknet函數(shù)相互之間有清晰的全局認(rèn)識(shí),下面給出了函數(shù)之間相互調(diào)用流程圖,如下所示:

上圖中的紅色部分就是darknet訓(xùn)練目標(biāo)檢測(cè)任務(wù)過程的整個(gè)函數(shù)調(diào)用關(guān)系流程,其他諸如圖像分類,RNN其過程也很類似,就沒有列出來。run_detector()函數(shù)在位于src/detector.c中,可以看到,run_detecor()函數(shù)提供train、valid和test等幾乎你能想得到的功能:
void?run_detector(int?argc,?char?**argv)
{
?if?(0?==?strcmp(argv[2],?"test"))?test_detector(datacfg,?cfg,?weights,...);
????else?if?(0?==?strcmp(argv[2],?"train"))?train_detector(datacfg,?cfg,?weights,?gpus,...);
????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,...);
????else?if?(0?==?strcmp(argv[2],?"calc_anchors"))?calc_anchors(datacfg,?num_of_clusters,?width,?height,?show,...);
????else?printf("?There?isn't?such?command:?%s",?argv[2]);
}
由于train相比于test和valid較復(fù)雜,涉及backword過程,這里我們的目的是要對(duì)darknet有一個(gè)全方位的把控,所以,我這里還是選擇分析整個(gè)train過程,我們跟進(jìn)train_detector()函數(shù):
void?train_detector(char?*datacfg,?char?*cfgfile,?char?*weightfile,...)
{
????//?讀取data配置文件信息
????list?*options?=?read_data_cfg(datacfg);
????//?構(gòu)建網(wǎng)絡(luò),?為網(wǎng)絡(luò)分配空間:?用多少塊GPU,?就會(huì)構(gòu)建多少個(gè)相同的網(wǎng)絡(luò)(不使用GPU時(shí),?ngpus=1)
????network*?nets?=?(network*)xcalloc(ngpus,?sizeof(network));
????for?(k?=?0;?k?????????//?解析net.cfg文件,?構(gòu)建并初始化網(wǎng)絡(luò)
????????nets[k]?=?parse_network_cfg(cfgfile);
????????//?學(xué)習(xí)率和gpus關(guān)系,?gpu數(shù)目越多,?leraning_rate越大.
????????nets[k].learning_rate?*=?ngpus;
????}
????//?第一塊顯卡上的網(wǎng)絡(luò)
????network?net?=?nets[0];?????
????//?為什么要把網(wǎng)絡(luò)參數(shù)存儲(chǔ)到args參數(shù)列表里面,?這就和Darknet加載數(shù)據(jù)的機(jī)制有關(guān).
????load_args?args?=?{?0?};
????args.w?=?net.w;????????//?網(wǎng)絡(luò)輸入寬
????args.h?=?net.h;????????//?網(wǎng)絡(luò)輸入高
????args.c?=?net.c;????????//?網(wǎng)絡(luò)輸入通道
????args.paths?=?paths;????//?圖片路徑列表
????args.n?=?imgs;?????????//?batchsize
????args.m?=?plist->size;??//?數(shù)據(jù)集總量
????args.classes?=?classes;??//?數(shù)據(jù)集類別數(shù)(不含背景)
????args.flip?=?net.flip;
????args.jitter?=?l.jitter;??//?圖像擾動(dòng)值
????args.resize?=?l.resize;
????args.num_boxes?=?l.max_boxes;??//?一張圖片中的最大gt數(shù)
????//?圖片中每個(gè)gt標(biāo)簽長(zhǎng)度(xywhc),?這里是6,?但實(shí)際上應(yīng)該是5,
????args.truth_size?=?l.truth_size;
????net.num_boxes?=?args.num_boxes;
????//?train_images_num即為訓(xùn)練集的size
????net.train_images_num?=?train_images_num;
????args.d?=?&buffer;??//?這個(gè)buffer用來不斷獲取data數(shù)據(jù)信息
????args.type?=?DETECTION_DATA;
????args.threads?=?0;????//?16?or?64,?調(diào)試時(shí)用單線程分析
????//?數(shù)組增強(qiáng)相關(guān)
????args.angle?=?net.angle;
????args.gaussian_noise?=?net.gaussian_noise;
????args.blur?=?net.blur;
????args.mixup?=?net.mixup;
????args.exposure?=?net.exposure;
????args.saturation?=?net.saturation;
????args.hue?=?net.hue;
????args.letter_box?=?net.letter_box;
????args.mosaic_bound?=?net.mosaic_bound;
????args.contrastive_jit_flip?=?net.contrastive_jit_flip;
????pthread_t?load_thread?=?load_data(args);??//?數(shù)據(jù)加載
????while?(get_current_iteration(net)?????????//?阻塞,?主函數(shù)等待線程load_thread函數(shù)執(zhí)行完畢,?再往下繼續(xù)執(zhí)行
????????pthread_join(load_thread,?0);
????????train?=?buffer;??//?數(shù)據(jù)加載完畢放在buffer中.
????????//?為下一輪訓(xùn)練加載數(shù)據(jù).
????????load_thread?=?load_data(args);?
????????//?train即為用于網(wǎng)絡(luò)訓(xùn)練的數(shù)據(jù),?這一段是核心
????????loss?=?train_network(net,?train);??//?(核心代碼<==)
????}
}
從上面整個(gè)train過程可以看出,包含:解析data配置文件和網(wǎng)絡(luò)配置文件,構(gòu)建并初始化網(wǎng)絡(luò),加載數(shù)據(jù),網(wǎng)絡(luò)訓(xùn)練幾個(gè)主要過程,在后續(xù)的代碼分析中都會(huì)對(duì)這些過程進(jìn)行詳細(xì)解析。這里為了完整分析完網(wǎng)絡(luò)的訓(xùn)練過程,我們跟進(jìn)train_network(net, train)這句代碼, 查看train_network()函數(shù)(位于src/network.c)中:
float?train_network(network?net,?data?d){
????return?train_network_waitkey(net,?d,?0);}
float?train_network_waitkey(network?net,?data?d,?int?wait_key)
{
?//?事實(shí)上對(duì)于圖像檢測(cè)而言,d.X.rows/net.batch=net.subdivision,因此恒有d.X.rows?%?net.batch?
????//?==?0,?且下面的n就等于net.subdivision,(可以參看detector.c中的train_detector()),因此對(duì)于圖像
????//?檢測(cè)而言,?下面三句略有冗余,但對(duì)于其他種情況(比如其他應(yīng)用,非圖像檢測(cè)甚至非視覺情況),不知道是不是這
????//?樣。
????assert(d.X.rows?%?net.batch?==?0);
????//?注意:?這里net.batch=cfg.batch(也即配置中寫的batch)/net.subdivisions
????//?因?yàn)樵趐arse_network_cfg(cfgfile)時(shí),有net.batch?/=?net.subvisions.
????int?batch?=?net.batch;
????int?n?=?d.X.rows?/?batch;??//?n?=?net.subvisions
????float*?X?=?(float*)xcalloc(batch?*?d.X.cols,?sizeof(float));??//?d.X.cols?=?h*w*c
????float*?y?=?(float*)xcalloc(batch?*?d.y.cols,?sizeof(float));
????int?i;
????float?sum?=?0;
????for(i?=?0;?i?????????//?從d中讀取batch張圖片到net.input中,進(jìn)行訓(xùn)練:
????????//?第一個(gè)參數(shù)d包含了net.batch*net.subdivision張圖片的數(shù)據(jù),第二個(gè)參數(shù)batch即為每次循環(huán)
????????//?讀入到net.input也即參與train_network_datum(),訓(xùn)練的圖片張數(shù),第三個(gè)參數(shù)為在d中的偏移量,
????????//?第四個(gè)參數(shù)為網(wǎng)絡(luò)的輸入數(shù)據(jù),第五個(gè)參數(shù)為輸入數(shù)據(jù)net.input對(duì)應(yīng)的標(biāo)簽數(shù)據(jù)(gt).
????????get_next_batch(d,?batch,?i*batch,?X,?y);
????????net.current_subdivision?=?i;
????????//?訓(xùn)練網(wǎng)絡(luò):?本次訓(xùn)練的數(shù)據(jù)共有net.batch張圖片,?這個(gè)batch?=?配置文件中的batch/subdivisions
????????//?訓(xùn)練包括一次前向過程:?計(jì)算每一層網(wǎng)絡(luò)的輸出.
????????//?一次反向過程:?計(jì)算誤差項(xiàng)(敏感度delta),?梯度(誤差項(xiàng)與激活函數(shù)導(dǎo)數(shù)之積)、?L/?w、?L/?b等;
????????//?X中仍然是包含batch(這個(gè)batch是被除net.subvisions的)張圖片
????????float?err?=?train_network_datum(net,?X,?y);??
????????sum?+=?err;
????????if(wait_key)?wait_key_cv(5);
????}
????//?每跑一個(gè)batch大小的數(shù)據(jù),?cur_iteration都會(huì)+1,?net->seen則為:?net-?
????//?>cur_iteration*batch*subdivs
????(*net.cur_iteration)?+=?1;?
????
????update_network(net);??//?更新參數(shù)
????
????free(X);
????free(y);
????
????return?(float)sum/(n*batch);
}
可以發(fā)現(xiàn),train_network_waitkey()函數(shù)主要就是循環(huán)獲取數(shù)據(jù)和網(wǎng)絡(luò)訓(xùn)練(運(yùn)行train_network_datum函數(shù)),最后進(jìn)行一次性參數(shù)更新( 代碼:update_network(net))。這里我們進(jìn)一步跟進(jìn)train_network_datum()函數(shù)(位于src/network.c),如下:
float?train_network_datum(network?net,?float?*x,?float?*y)
{
????//?用network_state結(jié)構(gòu)體記錄網(wǎng)絡(luò)訓(xùn)練過程中forward()和backbard()需要的信息.
????network_state?state={0};??
????*net.seen?+=?net.batch;??//?更新目前已經(jīng)處理的圖片數(shù)量:?每次處理一個(gè)batch,?故直接添加l.batch
????state.index?=?0;??//?用于記錄網(wǎng)絡(luò)層編號(hào)
????state.net?=?net;??//?記錄下當(dāng)前的網(wǎng)絡(luò)狀態(tài)
????state.input?=?x;??//?x中仍然包含batch張圖片:?batch?*?h?*?w?*?c
????state.delta?=?0;??//?用于保存反向傳播的梯度
????state.truth?=?y;??//?ground_truth
????state.train?=?1;??//?標(biāo)記處于訓(xùn)練階段
????forward_network(net,?state);???????????//?前向傳播
????backward_network(net,?state);??????????//?反向傳播
????float?error?=?get_network_cost(net);???//?計(jì)算損失
????return?error;
}
可以發(fā)現(xiàn),整個(gè)train_network_datum()函數(shù),就是進(jìn)行forward()和backward()過程,其中forward()函數(shù)如下所示(具體的注釋已經(jīng)寫在代碼中):
void?forward_network(network?net,?network_state?state)
{
????//?網(wǎng)絡(luò)的工作空間,?指的是所有層中占用運(yùn)算空間最大的那個(gè)層的workspace_size,
????//?因?yàn)閷?shí)際上在GPU或CPU中某個(gè)時(shí)刻只有一個(gè)層在做前向或反向運(yùn)算
????state.workspace?=?net.workspace;
????int?i;
????//?遍歷所有層,從第一層到最后一層,逐層進(jìn)行前向傳播,網(wǎng)絡(luò)共有net.n層
????for(i?=?0;?i?????????//?當(dāng)前正在進(jìn)行第i層的處理
????????state.index?=?i;
????????//?獲取當(dāng)前層
????????layer?l?=?net.layers[i];
????????//?如果當(dāng)前層的l.delta已經(jīng)動(dòng)態(tài)分配了內(nèi)存,?則調(diào)用fill_cpu()函數(shù)將其所有元素初始化為0
????????if(l.delta?&&?state.train){??//?l.delta不為NULL,?且為訓(xùn)練狀態(tài).
????????????//?第一個(gè)參數(shù)為l.delta的元素個(gè)數(shù),?第二個(gè)參數(shù)為初始化值,?為0
????????????scal_cpu(l.outputs?*?l.batch,?0,?l.delta,?1);??//?l.delta[i*1]?*=?0.
????????}
????????//?double?time?=?get_time_point();
????????//?前向傳播:?完成當(dāng)前層前向推理
????????l.forward(l,?state);??//?函數(shù)指針,?實(shí)現(xiàn)多態(tài).
????????//?完成某一層的推理時(shí),?置網(wǎng)絡(luò)的輸入為當(dāng)前層的輸出(這將成為下一層網(wǎng)絡(luò)的輸入),?注意此處更改的是
????????//?state,?而非原始的net
????????//?printf("%d?-?Predicted?in?%lf?milli-seconds.\n",?i,?((double)get_time_point()?
????????//?-?time)?/?1000);
????????state.input?=?l.output;//?l.output記錄網(wǎng)絡(luò)某一層的輸出結(jié)果,?網(wǎng)絡(luò)某一層的輸出即為下一層的輸入
????}
}
其backward()函數(shù)如下所示:
void?backward_network(network?net,?network_state?state)
{
????int?i;
????//?在進(jìn)行反向傳播之前先保存一下原來的net信息
????float?*original_input?=?state.input;
????float?*original_delta?=?state.delta;
????state.workspace?=?net.workspace;
????for(i?=?net.n-1;?i?>=?0;?--i){
????????state.index?=?i;??//?標(biāo)志參數(shù),?當(dāng)前網(wǎng)絡(luò)的活躍層?
????????if(i?==?0){
????????????state.input?=?original_input;
????????????state.delta?=?original_delta;
????????}
????????else{
????????????//?獲取net.layers[i]的上一層net.layer[i-1].
????????????layer?prev?=?net.layers[i-1];
????????????//?prev.output也即a_l-1,?上一層的輸出值a_l-1作為當(dāng)前層的輸入,?下面l.backward()會(huì)用到
????????????state.input?=?prev.output;
????????????//?上一層的敏感度圖δ_l-1,?敏感度也即誤差項(xiàng).
????????????state.delta?=?prev.delta;
????????}
????????//?置網(wǎng)絡(luò)當(dāng)前活躍層為當(dāng)前層,?即第i層
????????layer?l?=?net.layers[i];
????????if?(l.stopbackward)?break;
????????if?(l.onlyforward)?continue;
????????//?反向計(jì)算第i層的敏感度圖、權(quán)重及偏置更新值,并更新權(quán)重、偏置(同時(shí)會(huì)計(jì)算上一層(i-1)的敏感度圖,
????????//?存儲(chǔ)在net.delta中,這里一定要記住還差一個(gè)環(huán)節(jié):?乘上上一層輸出對(duì)加權(quán)輸入的導(dǎo)數(shù),也即上一層激活函
????????//?數(shù)對(duì)加權(quán)輸入的導(dǎo)數(shù))。
????????l.backward(l,?state);
????}
}
關(guān)于CNN的前向傳播和反向傳播,這里推薦一個(gè)寫的非常好的博客,會(huì)對(duì)理解darknet代碼有很好的幫助。darknet的實(shí)現(xiàn)也是來自于博客中提及到的理論:https://www.zybuluo.com/hanbingtao/note/485480
本次解讀分析就先到這里了,下一個(gè)解讀主要分析darknet是如果解析網(wǎng)絡(luò)配置文件并初始化網(wǎng)絡(luò)的。
由于本人水平有限,若有錯(cuò)誤之處,麻煩聯(lián)系我及時(shí)指正!謝謝!微信:13521560705,加我請(qǐng)備注:darknet。
推薦閱讀
mmdetection最小復(fù)刻版(六):FCOS深入可視化分析
mmdetection最小復(fù)刻版(五):yolov5轉(zhuǎn)化內(nèi)幕
帶你捋一捋anchor-free的檢測(cè)模型:FCOS
機(jī)器學(xué)習(xí)算法工程師
? ??? ? ? ? ? ? ? ? ? ? ? ??????????????????一個(gè)用心的公眾號(hào)
?

