一. 整體架構
整體架構和YOLO-V3相同(感謝知乎大神@江大白),創新點如下:
輸入端 --> Mosaic數據增強、cmBN、SAT自對抗訓練;
BackBone --> CSPDarknet53、Mish激活函數、Dropblock;
Neck --> SPP、FPN+PAN結構;
Prediction --> GIOU_Loss、DIOU_nms。
二. 輸入端
1. 數據加載流程(以訓練為例)
"darknet/src/darknet.c"--main()函數:模型入口。
...... // 根據指令進入不同的函數。 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); // detector.c中,run_detector函數入口,detect操作,包括訓練、測試等。 } 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); ......
"darknet/src/detector.c"--run_detector()函數:train指令入口。
...... 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); // 測試test_detector函數入口。 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, chart_path); // 訓練train_detector函數入口。 else if (0 == strcmp(argv[2], "valid")) validate_detector(datacfg, cfg, weights, outfile); ......
"darknet/src/detector.c"--train_detector()函數:數據加載入口。
pthread_t load_thread = load_data(args); // 首次創建並啟動加載線程,args為模型訓練參數。
"darknet/src/data.c"--load_data()函數:load_threads()分配線程。
pthread_t load_data(load_args args) { pthread_t thread; struct load_args* ptr = (load_args*)xcalloc(1, sizeof(struct load_args)); *ptr = args; /* 調用load_threads()函數。 */ if(pthread_create(&thread, 0, load_threads, ptr)) error("Thread creation failed"); // 參數1:指向線程標識符的指針;參數2:設置線程屬性;參數3:線程運行函數的地址;參數4:運行函數的參數。 return thread; }
"darknet/src/data.c"--load_threads()函數中:多線程調用run_thread_loop()。
if (!threads) { threads = (pthread_t*)xcalloc(args.threads, sizeof(pthread_t)); run_load_data = (volatile int *)xcalloc(args.threads, sizeof(int)); args_swap = (load_args *)xcalloc(args.threads, sizeof(load_args)); fprintf(stderr, " Create %d permanent cpu-threads \n", args.threads); for (i = 0; i < args.threads; ++i) { int* ptr = (int*)xcalloc(1, sizeof(int)); *ptr = i; if (pthread_create(&threads[i], 0, run_thread_loop, ptr)) error("Thread creation failed"); // 根據線程個數,調用run_thread_loop函數。 } }
"darknet/src/data.c"--run_thread_loop函數:根據線程ID調用load_thread()。
void *run_thread_loop(void *ptr) { const int i = *(int *)ptr; while (!custom_atomic_load_int(&flag_exit)) { while (!custom_atomic_load_int(&run_load_data[i])) { if (custom_atomic_load_int(&flag_exit)) { free(ptr); return 0; } this_thread_sleep_for(thread_wait_ms); } pthread_mutex_lock(&mtx_load_data); load_args *args_local = (load_args *)xcalloc(1, sizeof(load_args)); *args_local = args_swap[i]; // 傳入線程ID,在load_threads()函數中args_swap[i] = args。 pthread_mutex_unlock(&mtx_load_data); load_thread(args_local); // 調用load_thread()函數。 custom_atomic_store_int(&run_load_data[i], 0); } free(ptr); return 0; }
"darknet/src/data.c"--load_thread()函數中:根據type標識符執行最底層的數據加載任務load_data_detection()。
else if (a.type == DETECTION_DATA){ // 用於檢測的數據,在train_detector()函數中,args.type = DETECTION_DATA。 *a.d = load_data_detection(a.n, a.paths, a.m, a.w, a.h, a.c, a.num_boxes, a.classes, a.flip, a.gaussian_noise, a.blur, a.mixup, a.jitter, a.resize, a.hue, a.saturation, a.exposure, a.mini_batch, a.track, a.augment_speed, a.letter_box, a.show_imgs);
"darknet/src/data.c"--load_data_detection()函數根據是否配置opencv,有兩個版本,opencv版本中:
基本數據處理:
包括crop、flip、HSV augmentation、blur以及gaussian_noise。(注意,a.type == DETECTION_DATA時,無angle參數傳入,沒有圖像旋轉增強)
......
if (track) random_paths = get_sequential_paths(paths, n, m, mini_batch, augment_speed); // 目標跟蹤。 else random_paths = get_random_paths(paths, n, m); // 隨機選取n張圖片的路徑。 for (i = 0; i < n; ++i) { float *truth = (float*)xcalloc(5 * boxes, sizeof(float)); const char *filename = random_paths[i]; int flag = (c >= 3); mat_cv *src; src = load_image_mat_cv(filename, flag); // image_opencv.cpp中,load_image_mat_cv函數入口,使用opencv讀取圖像。 ...... /* 將原圖進行一定比例的縮放。 */ 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縮放。 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; } 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; } } /* 執行letter_box變換。 */ int swidth = ow - pleft - pright; int sheight = oh - ptop - pbot; float sx = (float)swidth / ow; float sy = (float)sheight / oh; float dx = ((float)pleft / ow) / sx; float dy = ((float)ptop / oh) / sy; /* truth在調用函數后獲得所有圖像的標簽信息,因為對原始圖片進行了數據增強,其中的平移抖動勢必會改動每個物體的矩形框標簽信息,需要根據具體的數據增強方式進行相應矯正,后面的參數就是用於數據增強后的矩形框信息矯正。 */ int min_w_h = fill_truth_detection(filename, boxes, truth, classes, flip, dx, dy, 1. / sx, 1. / sy, w, h); // 求最小obj尺寸。 if ((min_w_h / 8) < blur && blur > 1) blur = min_w_h / 8; // disable blur if one of the objects is too small // image_opencv.cpp中,image_data_augmentation函數入口,數據增強。 image ai = image_data_augmentation(src, w, h, pleft, ptop, swidth, sheight, flip, dhue, dsat, dexp, gaussian_noise, blur, boxes, truth);
......
"darknet/src/image_opencv.cpp"--image_data_augmentation()函數:
extern "C" image image_data_augmentation(mat_cv* mat, int w, int h, int pleft, int ptop, int swidth, int sheight, int flip, float dhue, float dsat, float dexp, int gaussian_noise, int blur, int num_boxes, float *truth) { image out; try { cv::Mat img = *(cv::Mat *)mat; // 讀取圖像數據。 // crop cv::Rect src_rect(pleft, ptop, swidth, sheight); cv::Rect img_rect(cv::Point2i(0, 0), img.size()); cv::Rect new_src_rect = src_rect & img_rect; cv::Rect dst_rect(cv::Point2i(std::max<int>(0, -pleft), std::max<int>(0, -ptop)), new_src_rect.size()); cv::Mat sized; if (src_rect.x == 0 && src_rect.y == 0 && src_rect.size() == img.size()) { cv::resize(img, sized, cv::Size(w, h), 0, 0, cv::INTER_LINEAR); } else { cv::Mat cropped(src_rect.size(), img.type()); cropped.setTo(cv::mean(img)); img(new_src_rect).copyTo(cropped(dst_rect)); // resize cv::resize(cropped, sized, cv::Size(w, h), 0, 0, cv::INTER_LINEAR); } // flip,雖然配置文件里沒有flip參數,但代碼里有使用。 if (flip) { cv::Mat cropped; cv::flip(sized, cropped, 1); // 0 - x-axis, 1 - y-axis, -1 - both axes (x & y) sized = cropped.clone(); } // HSV augmentation if (dsat != 1 || dexp != 1 || dhue != 0) { if (img.channels() >= 3) { cv::Mat hsv_src; cvtColor(sized, hsv_src, cv::COLOR_RGB2HSV); // RGB to HSV std::vector<cv::Mat> hsv; cv::split(hsv_src, hsv); hsv[1] *= dsat; hsv[2] *= dexp; hsv[0] += 179 * dhue; cv::merge(hsv, hsv_src); cvtColor(hsv_src, sized, cv::COLOR_HSV2RGB); // HSV to RGB (the same as previous) } else { sized *= dexp; } } if (blur) { cv::Mat dst(sized.size(), sized.type()); if (blur == 1) { cv::GaussianBlur(sized, dst, cv::Size(17, 17), 0); } else { int ksize = (blur / 2) * 2 + 1; cv::Size kernel_size = cv::Size(ksize, ksize); cv::GaussianBlur(sized, dst, kernel_size, 0); } if (blur == 1) { cv::Rect img_rect(0, 0, sized.cols, sized.rows); int t; for (t = 0; t < num_boxes; ++t) { box b = float_to_box_stride(truth + t*(4 + 1), 1); if (!b.x) break; int left = (b.x - b.w / 2.)*sized.cols; int width = b.w*sized.cols; int top = (b.y - b.h / 2.)*sized.rows; int height = b.h*sized.rows; cv::Rect roi(left, top, width, height); roi = roi & img_rect; sized(roi).copyTo(dst(roi)); } } dst.copyTo(sized); } if (gaussian_noise) { cv::Mat noise = cv::Mat(sized.size(), sized.type()); gaussian_noise = std::min(gaussian_noise, 127); gaussian_noise = std::max(gaussian_noise, 0); cv::randn(noise, 0, gaussian_noise); //mean and variance cv::Mat sized_norm = sized + noise; sized = sized_norm; } // Mat -> image out = mat_to_image(sized); } catch (...) { cerr << "OpenCV can't augment image: " << w << " x " << h << " \n"; out = mat_to_image(*(cv::Mat*)mat); } return out; }
高級數據處理:
主要是mosaic數據增強。
......
if (use_mixup == 0) { // 不使用mixup。
d.X.vals[i] = ai.data;
memcpy(d.y.vals[i], truth, 5 * boxes * sizeof(float)); // C庫函數,從存儲區truth復制5 * boxes * sizeof(float)個字節到存儲區d.y.vals[i]。 } else if (use_mixup == 1) { // 使用mixup。 if (i_mixup == 0) { // 第一個序列。 d.X.vals[i] = ai.data; memcpy(d.y.vals[i], truth, 5 * boxes * sizeof(float)); // n張圖的label->d.y.vals,i_mixup=1時,作為上一個sequence的label。 } else if (i_mixup == 1) { // 第二個序列,此時d.X.vals已經儲存上個序列n張增強后的圖。 image old_img = make_empty_image(w, h, c); old_img.data = d.X.vals[i]; // 記錄上一個序列的n張old_img。 blend_images_cv(ai, 0.5, old_img, 0.5); // image_opencv.cpp中,blend_images_cv函數入口,新舊序列對應的兩張圖進行線性融合,ai只是在i_mixup和i循環最里層的一張圖。 blend_truth(d.y.vals[i], boxes, truth); // 上一個序列的d.y.vals[i]與這個序列的truth融合。 free_image(old_img); // 釋放img數據。 d.X.vals[i] = ai.data; // 保存這個序列的n張圖。 } } else if (use_mixup == 3) { // mosaic數據增強。 if (i_mixup == 0) { // 第一序列,初始化。 image tmp_img = make_image(w, h, c); d.X.vals[i] = tmp_img.data; } if (flip) { // 翻轉。 int tmp = pleft; pleft = pright; pright = tmp; } const int left_shift = min_val_cmp(cut_x[i], max_val_cmp(0, (-pleft*w / ow))); // utils.h中,min_val_cmp函數入口,取小(min)取大(max)。 const int top_shift = min_val_cmp(cut_y[i], max_val_cmp(0, (-ptop*h / oh))); // ptop<0時,取cut_y[i]與-ptop*h / oh較小的,否則返回0。 const int right_shift = min_val_cmp((w - cut_x[i]), max_val_cmp(0, (-pright*w / ow))); const int bot_shift = min_val_cmp(h - cut_y[i], max_val_cmp(0, (-pbot*h / oh))); int k, x, y; for (k = 0; k < c; ++k) { // 通道。 for (y = 0; y < h; ++y) { // 高度。 int j = y*w + k*w*h; // 每張圖i,按行堆疊索引j。
if (i_mixup == 0 && y < cut_y[i]) { // 右下角區塊,i_mixup=0~3,d.X.vals[i]未被清0,累計粘貼4塊區域。 int j_src = (w - cut_x[i] - right_shift) + (y + h - cut_y[i] - bot_shift)*w + k*w*h; memcpy(&d.X.vals[i][j + 0], &ai.data[j_src], cut_x[i] * sizeof(float)); // 由ai.data[j_src]所指內存區域復制cut_x[i]*sizeof(float)個字節到&d.X.vals[i][j + 0]所指內存區域。 } if (i_mixup == 1 && y < cut_y[i]) { // 左下角區塊。 int j_src = left_shift + (y + h - cut_y[i] - bot_shift)*w + k*w*h; memcpy(&d.X.vals[i][j + cut_x[i]], &ai.data[j_src], (w-cut_x[i]) * sizeof(float)); } if (i_mixup == 2 && y >= cut_y[i]) { // 右上角區塊。 int j_src = (w - cut_x[i] - right_shift) + (top_shift + y - cut_y[i])*w + k*w*h; memcpy(&d.X.vals[i][j + 0], &ai.data[j_src], cut_x[i] * sizeof(float)); } if (i_mixup == 3 && y >= cut_y[i]) { // 左上角區塊。 int j_src = left_shift + (top_shift + y - cut_y[i])*w + k*w*h; memcpy(&d.X.vals[i][j + cut_x[i]], &ai.data[j_src], (w - cut_x[i]) * sizeof(float)); } } } blend_truth_mosaic(d.y.vals[i], boxes, truth, w, h, cut_x[i], cut_y[i], i_mixup, left_shift, right_shift, top_shift, bot_shift); // label對應shift調整。 free_image(ai); ai.data = d.X.vals[i]; } ......
三. BackBone
總圖:
網絡配置文件(.cfg)決定了模型架構,訓練時需要在命令行指定。文件以[net]段開頭,定義與訓練直接相關的參數:
[net] # Testing # 測試時,batch和subdivisions設置為1,否則可能出錯。 #batch=1 # 大一些可以減小訓練震盪及訓練時NAN的出現。 #subdivisions=1 # 必須為為8的倍數,顯存吃緊可以設成32或64。 # Training batch=64 # 訓練過程中將64張圖一次性加載進內存,前向傳播后將64張圖的loss累加求平均,再一次性后向傳播更新權重。 subdivisions=16 # 一個batch分16次完成前向傳播,即每次計算4張。 width=608 # 網絡輸入的寬。 height=608 # 網絡輸入的高。 channels=3 # 網絡輸入的通道數。 momentum=0.949 # 動量梯度下降優化方法中的動量參數,更新的時候在一定程度上保留之前更新的方向。 decay=0.0005 # 權重衰減正則項,用於防止過擬合。 angle=0 # 數據增強參數,通過旋轉角度來生成更多訓練樣本。 saturation = 1.5 # 數據增強參數,通過調整飽和度來生成更多訓練樣本。 exposure = 1.5 # 數據增強參數,通過調整曝光量來生成更多訓練樣本。 hue=.1 # 數據增強參數,通過調整色調來生成更多訓練樣本。 learning_rate=0.001 # 學習率。 burn_in=1000 # 在迭代次數小於burn_in時,學習率的更新為一種方式,大於burn_in時,采用policy的更新方式。 max_batches = 500500 #訓練迭代次數,跑完一個batch為一次,一般為類別數*2000,訓練樣本少或train from scratch可適當增加。 policy=steps # 學習率調整的策略。 steps=400000,450000 # 動態調整學習率,steps可以取max_batches的0.8~0.9。 scales=.1,.1 # 迭代到steps(1)次時,學習率衰減十倍,steps(2)次時,學習率又會在前一個學習率的基礎上衰減十倍。 #cutmix=1 # cutmix數據增強,將一部分區域cut掉但不填充0像素而是隨機填充訓練集中的其他數據的區域像素值,分類結果按一定的比例分配。 mosaic=1 # 馬賽克數據增強,取四張圖,隨機縮放、隨機裁剪、隨機排布的方式拼接,詳見上述代碼分析。
其余區段,包括[convolutional]、[route]、[shortcut]、[maxpool]、[upsample]、[yolo]層,為不同類型的層的配置參數。YOLO-V4中[net]層之后堆疊多個CBM及CSP層,首先是2個CBM層,CBM結構如下:
[convolutional] batch_normalize=1 # 是否進行BN。 filters=32 # 卷積核個數,也就是該層的輸出通道數。 size=3 # 卷積核大小。 stride=1 # 卷積步長。 pad=1 # pad邊緣補像素。 activation=mish # 網絡層激活函數,yolo-v4只在Backbone中采用了mish,網絡后面仍采用Leaky_relu。
創新點是Mish激活函數,與Leaky_Relu曲線對比如圖:
Mish在負值的時候並不是完全截斷,而是允許比較小的負梯度流入,保證了信息的流動。此外,平滑的激活函數允許更好的信息深入神經網絡,梯度下降效果更好,從而提升准確性和泛化能力。
兩個CBM后是CSP1,CSP1結構如下:
# CSP1 = CBM + 1個殘差unit + CBM -> Concat(with CBM),見總圖。 [convolutional] # CBM層,直接與7層后的route層連接,形成總圖中CSPX下方支路。 batch_normalize=1 filters=64 size=1 stride=1 pad=1 activation=mish [route] # 得到前面第2層的輸出,即CSP開始位置,構建如圖所示的CSP第一支路。 layers = -2 [convolutional] # CBM層。 batch_normalize=1 filters=64 size=1 stride=1 pad=1 activation=mish # Residual Block [convolutional] # CBM層。 batch_normalize=1 filters=32 size=1 stride=1 pad=1 activation=mish [convolutional] # CBM層。 batch_normalize=1 filters=64 size=3 stride=1 pad=1 activation=mish [shortcut] # add前面第3層的輸出,Residual Block結束。 from=-3 activation=linear [convolutional] # CBM層。 batch_normalize=1 filters=64 size=1 stride=1 pad=1 activation=mish [route] # Concat上一個CBM層與前面第7層(CBM)的輸出。 layers = -1,-7
接下來的CBM及CSPX架構與上述block相同,只是CSPX對應X個殘差單元,如圖:
CSP模塊將基礎層的特征映射划分為兩部分,再skip connection,減少計算量的同時保證了准確率。
要注意的是,backbone中兩次出現分支,與后續Neck連接,稍后會解釋。
四. Neck&Prediction
.cfg配置文件后半部分是Neck和YOLO-Prediction設置,我做了重點注釋:
### CBL*3 ### [convolutional] batch_normalize=1 filters=512 size=1 stride=1 pad=1 activation=leaky # 不再使用Mish。 [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=1024 activation=leaky [convolutional] batch_normalize=1 filters=512 size=1 stride=1 pad=1 activation=leaky ### SPP-最大池化的方式進行多尺度融合 ### [maxpool] # 5*5。 stride=1 size=5 [route] layers=-2 [maxpool] # 9*9。 stride=1 size=9 [route] layers=-4 [maxpool] # 13*13。 stride=1 size=13 [route] # Concat。 layers=-1,-3,-5,-6 ### End SPP ### ### CBL*3 ### [convolutional] batch_normalize=1 filters=512 size=1 stride=1 pad=1 activation=leaky # 不再使用Mish。 [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=1024 activation=leaky [convolutional] batch_normalize=1 filters=512 size=1 stride=1 pad=1 activation=leaky ### CBL ### [convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky ### 上采樣 ### [upsample] stride=2 [route] layers = 85 # 獲取Backbone中CBM+CSP8+CBM模塊的輸出,85從net以外的層開始計數,從0開始索引。 [convolutional] # 增加CBL支路。 batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky [route] # Concat。 layers = -1, -3 ### CBL*5 ### [convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=512 activation=leaky [convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=512 activation=leaky [convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky ### CBL ### [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky ### 上采樣 ### [upsample] stride=2 [route] layers = 54 # 獲取Backbone中CBM*2+CSP1+CBM*2+CSP2+CBM*2+CSP8+CBM模塊的輸出,54從net以外的層開始計數,從0開始索引。 ### CBL ### [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky [route] # Concat。 layers = -1, -3 ### CBL*5 ### [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=256 activation=leaky [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=256 activation=leaky [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky ### Prediction ### ### CBL ### [convolutional] batch_normalize=1 size=3 stride=1 pad=1 filters=256 activation=leaky ### conv ### [convolutional] size=1 stride=1 pad=1 filters=255 activation=linear
mask = 0,1,2 # 當前屬於第幾個預選框。
# coco數據集默認值,可通過detector calc_anchors,利用k-means計算樣本anchors,但要根據每個anchor的大小(是否超過60*60或30*30)更改mask對應的索引(第一個yolo層對應小尺寸;第二個對應中等大小;第三個對應大尺寸)及上一個conv層的filters。
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80 # 網絡需要識別的物體種類數。
num=9 # 預選框的個數,即anchors總數。
jitter=.3 # 通過抖動增加噪聲來抑制過擬合。
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.2
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou # CIOU損失函數,考慮目標框回歸函數的重疊面積、中心點距離及長寬比。
nms_kind=greedynms
beta_nms=0.6
max_delta=5
### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=2
pad=1
filters=256
activation=leaky
layers = -1, -16
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear
[yolo] # 38*38*255,對應中等的anchor box。
mask = 3,4,5
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.1
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5
[route] # 獲取Neck第二層的輸出。
layers = -4
### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=2
pad=1
filters=512
activation=leaky
layers = -1, -37
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear
[yolo] # 19*19*255,對應最大的anchor box。
mask = 6,7,8
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
scale_x_y = 1.05
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5
其中第一個創新點是引入Spatial Pyramid Pooling(SPP)模塊:
代碼中max pool和route層組合,三個不同尺度的max-pooling將前一個卷積層輸出的feature maps進行多尺度的特征處理,再與原圖進行拼接,一共4個scale。相比於只用一個max-pooling,提取的特征范圍更大,而且將不同尺度的特征進行了有效分離;
第二個創新點是在FPN的基礎上引入PAN結構:
原版PANet中PAN操作是做element-wise相加,YOLO-V4則采用擴增維度的Concat,如下圖:
Backbone下采樣不同階段得到的特征圖Concat后續上采樣階對應尺度的的output,形成FPN結構,再經過兩個botton-up的PAN結構。
下采樣1:前10個block中,只有3個CBM的stride為2,輸入圖像尺寸變為608/2*2*2=76,filters根據最后一個CBM為256,因此第10個block輸出feature map為76*76*256;
下采樣2:繼續Backbone,同理,第13個block(CBM)輸出38*38*512的特征圖;
下采樣3:第23個block(CBL)輸出為19*19*512;
上采樣1:下采樣3 + CBL + 上采樣 = 38*38*256;
Concat1:[上采樣1] Concat [下采樣2 + CBL] = [38*38*256] Concat [38*38*512 + (256,1)] = 38*38*512;
上采樣2:Concat1 + CBL*5 + CBL + 上采樣 = 76*76*128;
Concat2:[上采樣2] Concat [下采樣1 + CBL] = [76*76*128] Concat [76*76*256 + (128,1)] = 76*76*256;
Concat3(PAN1):[Concat2 + CBL*5 + CBL] Concat [Concat1 + CBL*5] = [76*76*256 + (128,1) + (256,2)] Concat [38*38*512 + (256,1)] = [38*38*256] Concat [38*38*256] = 38*38*512;
Concat4(PAN2):[Concat3 + CBL*5 + CBL] Concat [下采樣3] = [38*38*512 + (256,1) + (512,2)] Concat [19*19*512] = 19*19*1024;
Prediction①:Concat2 + CBL*5 + CBL + conv = 76*76*256 + (128,1) + (256,1) + (filters,1) = 76*76*filters,其中filters = (class_num + 5)*3,圖中默認COCO數據集,80類所以是255;
Prediction②:PAN1 + CBL*5 + CBL + conv = 38*38*512 + (256,1) + (512,1) + (filters,1) = 38*38*filters,其中filters = (class_num + 5)*3,圖中默認COCO數據集,80類所以是255;
Prediction③:PAN2 + CBL*5 + CBL + conv = 19*19*1024 + (512,1) + (1024,1) + (filters,1) = 19*19*filters,其中filters = (class_num + 5)*3,圖中默認COCO數據集,80類所以是255。
五. 網絡構建
上述從backbone到prediction的網絡架構,源碼中都是基於network結構體來儲存網絡參數。具體流程如下:
"darknet/src/detector.c"--train_detector()函數中:
...... network net_map; if (calc_map) { // 計算mAP。 ...... net_map = parse_network_cfg_custom(cfgfile, 1, 1); // parser.c中parse_network_cfg_custom函數入口,加載cfg和參數構建網絡,batch = 1。 net_map.benchmark_layers = benchmark_layers; 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); ...... } srand(time(0)); char *base = basecfg(cfgfile); // utils.c中basecfg()函數入口,解析cfg/yolo-obj.cfg文件,就是模型的配置參數,並打印。 printf("%s\n", base); float avg_loss = -1; network* nets = (network*)xcalloc(ngpus, sizeof(network)); // 給network結構體分內存,用來儲存網絡參數。 srand(time(0)); int seed = rand(); int k; for (k = 0; k < ngpus; ++k) { srand(seed); #ifdef GPU cuda_set_device(gpus[k]); #endif nets[k] = parse_network_cfg(cfgfile); // parse_network_cfg_custom(cfgfile, 0, 0),nets根據GPU個數分別加載配置文件。 nets[k].benchmark_layers = benchmark_layers; if (weightfile) { load_weights(&nets[k], weightfile); // parser.c中load_weights()接口,讀取權重文件。 } if (clear) { // 是否清零。 *nets[k].seen = 0; *nets[k].cur_iteration = 0; } nets[k].learning_rate *= ngpus; } srand(time(0)); network net = nets[0]; // 參數傳遞給net ...... /* 准備加載參數。 */ load_args args = { 0 }; args.w = net.w; args.h = net.h; args.c = net.c; args.paths = paths; args.n = imgs; args.m = plist->size; args.classes = classes; args.flip = net.flip; args.jitter = l.jitter; args.resize = l.resize; args.num_boxes = l.max_boxes; net.num_boxes = args.num_boxes; net.train_images_num = train_images_num; args.d = &buffer; args.type = DETECTION_DATA; args.threads = 64; // 16 or 64 ......
"darknet/src/parser.c"--parse_network_cfg_custom()函數中:
network parse_network_cfg_custom(char *filename, int batch, int time_steps) { list *sections = read_cfg(filename); // 讀取配置文件,構建成一個鏈表list。 node *n = sections->front; // 定義sections的首節點為n。 if(!n) error("Config file has no sections"); network net = make_network(sections->size - 1); // network.c中,make_network函數入口,從net變量下一層開始,依次為其中的指針變量分配內存。由於第一個段[net]中存放的是和網絡並不直接相關的配置參數,因此網絡中層的數目為sections->size - 1。 net.gpu_index = gpu_index; size_params params; if (batch > 0) params.train = 0; // allocates memory for Detection only else params.train = 1; // allocates memory for Detection & Training section *s = (section *)n->val; // 首節點n的val傳遞給section。 list *options = s->options; if(!is_network(s)) error("First section must be [net] or [network]"); parse_net_options(options, &net); // 初始化網絡全局參數,包含但不限於[net]中的參數。 #ifdef GPU printf("net.optimized_memory = %d \n", net.optimized_memory); if (net.optimized_memory >= 2 && params.train) { pre_allocate_pinned_memory((size_t)1024 * 1024 * 1024 * 8); // pre-allocate 8 GB CPU-RAM for pinned memory } #endif // GPU ...... while(n){ //初始化每一層的參數。 params.index = count; fprintf(stderr, "%4d ", count); s = (section *)n->val; options = s->options; layer l = { (LAYER_TYPE)0 }; LAYER_TYPE lt = string_to_layer_type(s->type); if(lt == CONVOLUTIONAL){ // 卷積層,調用parse_convolutional()函數執行make_convolutional_layer()創建卷積層。 l = parse_convolutional(options, params); }else if(lt == LOCAL){ l = parse_local(options, params); }else if(lt == ACTIVE){ l = parse_activation(options, params); }else if(lt == RNN){ l = parse_rnn(options, params); }else if(lt == GRU){ l = parse_gru(options, params); }else if(lt == LSTM){ l = parse_lstm(options, params); }else if (lt == CONV_LSTM) { l = parse_conv_lstm(options, params); }else if(lt == CRNN){ l = parse_crnn(options, params); }else if(lt == CONNECTED){ l = parse_connected(options, params); }else if(lt == CROP){ l = parse_crop(options, params); }else if(lt == COST){ l = parse_cost(options, params); l.keep_delta_gpu = 1; }else if(lt == REGION){ l = parse_region(options, params); l.keep_delta_gpu = 1; }else if (lt == YOLO) { // yolov3/4引入的yolo_layer,調用parse_yolo()函數執行make_yolo_layer()創建yolo層。 l = parse_yolo(options, params); l.keep_delta_gpu = 1; }else if (lt == GAUSSIAN_YOLO) { l = parse_gaussian_yolo(options, params); l.keep_delta_gpu = 1; }else if(lt == DETECTION){ l = parse_detection(options, params); }else if(lt == SOFTMAX){ l = parse_softmax(options, params); net.hierarchy = l.softmax_tree; l.keep_delta_gpu = 1; }else if(lt == NORMALIZATION){ l = parse_normalization(options, params); }else if(lt == BATCHNORM){ l = parse_batchnorm(options, params); }else if(lt == MAXPOOL){ l = parse_maxpool(options, params); }else if (lt == LOCAL_AVGPOOL) { l = parse_local_avgpool(options, params); }else if(lt == REORG){ l = parse_reorg(options, params); } else if (lt == REORG_OLD) { l = parse_reorg_old(options, params); }else if(lt == AVGPOOL){ l = parse_avgpool(options, params); }else if(lt == ROUTE){ l = parse_route(options, params); int k; for (k = 0; k < l.n; ++k) { net.layers[l.input_layers[k]].use_bin_output = 0; net.layers[l.input_layers[k]].keep_delta_gpu = 1; } }else if (lt == UPSAMPLE) { l = parse_upsample(options, params, net); }else if(lt == SHORTCUT){ l = parse_shortcut(options, params, net); net.layers[count - 1].use_bin_output = 0; net.layers[l.index].use_bin_output = 0; net.layers[l.index].keep_delta_gpu = 1; }else if (lt == SCALE_CHANNELS) { l = parse_scale_channels(options, params, net); net.layers[count - 1].use_bin_output = 0; net.layers[l.index].use_bin_output = 0; net.layers[l.index].keep_delta_gpu = 1; } else if (lt == SAM) { l = parse_sam(options, params, net); net.layers[count - 1].use_bin_output = 0; net.layers[l.index].use_bin_output = 0; net.layers[l.index].keep_delta_gpu = 1; }else if(lt == DROPOUT){ l = parse_dropout(options, params); l.output = net.layers[count-1].output; l.delta = net.layers[count-1].delta; #ifdef GPU l.output_gpu = net.layers[count-1].output_gpu; l.delta_gpu = net.layers[count-1].delta_gpu; l.keep_delta_gpu = 1; #endif } else if (lt == EMPTY) { layer empty_layer = {(LAYER_TYPE)0}; empty_layer.out_w = params.w; empty_layer.out_h = params.h; empty_layer.out_c = params.c; l = empty_layer; l.output = net.layers[count - 1].output; l.delta = net.layers[count - 1].delta; #ifdef GPU l.output_gpu = net.layers[count - 1].output_gpu; l.delta_gpu = net.layers[count - 1].delta_gpu; #endif }else{ fprintf(stderr, "Type not recognized: %s\n", s->type); } ...... net.layers[count] = l; // 每個解析函數返回一個填充好的層l,將這些層全部添加到network結構體的layers數組中。 if (l.workspace_size > workspace_size) workspace_size = l.workspace_size; // workspace_size表示網絡的工作空間,指的是所有層中占用運算空間最大的那個層的,因為實際上在GPU或CPU中某個時刻只有一個層在做前向或反向運算。 if (l.inputs > max_inputs) max_inputs = l.inputs; if (l.outputs > max_outputs) max_outputs = l.outputs; free_section(s); n = n->next; // node節點前沿,empty則while-loop結束。 ++count; if(n){ // 這部分將連接的兩個層之間的輸入輸出shape統一。 if (l.antialiasing) { params.h = l.input_layer->out_h; params.w = l.input_layer->out_w; params.c = l.input_layer->out_c; params.inputs = l.input_layer->outputs; } else { params.h = l.out_h; params.w = l.out_w; params.c = l.out_c; params.inputs = l.outputs; } } if (l.bflops > 0) bflops += l.bflops; if (l.w > 1 && l.h > 1) { avg_outputs += l.outputs; avg_counter++; } } free_list(sections); ...... return net; // 返回解析好的network類型的指針變量,這個指針變量會伴隨訓練的整個過程。 }
以卷積層和yolo層為例,介紹網絡層的創建過程,convolutional_layer.c中make_convolutional_layer()函數:
convolutional_layer make_convolutional_layer(int batch, int steps, int h, int w, int c, int n, int groups, int size, int stride_x, int stride_y, int dilation, int padding, ACTIVATION activation, int batch_normalize, int binary, int xnor, int adam, int use_bin_output, int index, int antialiasing, convolutional_layer *share_layer, int assisted_excitation, int deform, int train) { int total_batch = batch*steps; int i; convolutional_layer l = { (LAYER_TYPE)0 }; // convolutional_layer其實就是layer。 l.type = CONVOLUTIONAL; // layer的類型,此處為卷積層。 l.train = train; /* 改變輸入和輸出的維度。 */ if (xnor) groups = 1; // disable groups for XNOR-net if (groups < 1) groups = 1; // group將對應的輸入輸出通道對應分組,默認為1(輸出輸入的所有通道各為一組),把卷積group等於輸入通道,輸出通道等於輸入通道就實現了depthwize separable convolution結構。 const int blur_stride_x = stride_x; const int blur_stride_y = stride_y; l.antialiasing = antialiasing; if (antialiasing) { stride_x = stride_y = l.stride = l.stride_x = l.stride_y = 1; // use stride=1 in host-layer } l.deform = deform; l.assisted_excitation = assisted_excitation; l.share_layer = share_layer; l.index = index; l.h = h; // input的高。 l.w = w; // input的寬。 l.c = c; // input的通道。 l.groups = groups; l.n = n; // 卷積核filter的個數。 l.binary = binary; l.xnor = xnor; l.use_bin_output = use_bin_output; l.batch = batch; // 訓練使用的batch_size。 l.steps = steps; l.stride = stride_x; // 移動步長。 l.stride_x = stride_x; l.stride_y = stride_y; l.dilation = dilation; l.size = size; // 卷積核的大小。 l.pad = padding; // 邊界填充寬度。 l.batch_normalize = batch_normalize; // 是否進行BN操作。 l.learning_rate_scale = 1; /* 數組的大小: c/groups*n*size*size。 */ l.nweights = (c / groups) * n * size * size; // groups默認值為1,出現c的原因是對多個通道的廣播操作。 if (l.share_layer) { if (l.size != l.share_layer->size || l.nweights != l.share_layer->nweights || l.c != l.share_layer->c || l.n != l.share_layer->n) { printf(" Layer size, nweights, channels or filters don't match for the share_layer"); getchar(); } l.weights = l.share_layer->weights; l.weight_updates = l.share_layer->weight_updates; l.biases = l.share_layer->biases; l.bias_updates = l.share_layer->bias_updates; } else { l.weights = (float*)xcalloc(l.nweights, sizeof(float)); l.biases = (float*)xcalloc(n, sizeof(float)); if (train) { l.weight_updates = (float*)xcalloc(l.nweights, sizeof(float)); l.bias_updates = (float*)xcalloc(n, sizeof(float)); } } // float scale = 1./sqrt(size*size*c); float scale = sqrt(2./(size*size*c/groups)); // 初始值scale。 if (l.activation == NORM_CHAN || l.activation == NORM_CHAN_SOFTMAX || l.activation == NORM_CHAN_SOFTMAX_MAXVAL) { for (i = 0; i < l.nweights; ++i) l.weights[i] = 1; // rand_normal(); } else { for (i = 0; i < l.nweights; ++i) l.weights[i] = scale*rand_uniform(-1, 1); // rand_normal(); } /* 根據公式計算輸出維度。 */ int out_h = convolutional_out_height(l); int out_w = convolutional_out_width(l); l.out_h = out_h; // output的高。 l.out_w = out_w; // output的寬。 l.out_c = n; // output的通道,等於卷積核個數。 l.outputs = l.out_h * l.out_w * l.out_c; // 一個batch的output維度大小。 l.inputs = l.w * l.h * l.c; // 一個batch的input維度大小。 l.activation = activation; l.output = (float*)xcalloc(total_batch*l.outputs, sizeof(float)); // 輸出數組。 #ifndef GPU if (train) l.delta = (float*)xcalloc(total_batch*l.outputs, sizeof(float)); // 暫存更新數據的輸出數組。 #endif // not GPU /* 三個重要的函數,前向運算,反向傳播和更新函數。 */ l.forward = forward_convolutional_layer; l.backward = backward_convolutional_layer; l.update = update_convolutional_layer; // 明確了更新的策略。 if(binary){ l.binary_weights = (float*)xcalloc(l.nweights, sizeof(float)); l.cweights = (char*)xcalloc(l.nweights, sizeof(char)); l.scales = (float*)xcalloc(n, sizeof(float)); } if(xnor){ l.binary_weights = (float*)xcalloc(l.nweights, sizeof(float)); l.binary_input = (float*)xcalloc(l.inputs * l.batch, sizeof(float)); int align = 32;// 8; int src_align = l.out_h*l.out_w; l.bit_align = src_align + (align - src_align % align); l.mean_arr = (float*)xcalloc(l.n, sizeof(float)); const size_t new_c = l.c / 32; size_t in_re_packed_input_size = new_c * l.w * l.h + 1; l.bin_re_packed_input = (uint32_t*)xcalloc(in_re_packed_input_size, sizeof(uint32_t)); l.lda_align = 256; // AVX2 int k = l.size*l.size*l.c; size_t k_aligned = k + (l.lda_align - k%l.lda_align); size_t t_bit_input_size = k_aligned * l.bit_align / 8; l.t_bit_input = (char*)xcalloc(t_bit_input_size, sizeof(char)); } /* Batch Normalization相關的變量設置。 */ if(batch_normalize){ if (l.share_layer) { l.scales = l.share_layer->scales; l.scale_updates = l.share_layer->scale_updates; l.mean = l.share_layer->mean; l.variance = l.share_layer->variance; l.mean_delta = l.share_layer->mean_delta; l.variance_delta = l.share_layer->variance_delta; l.rolling_mean = l.share_layer->rolling_mean; l.rolling_variance = l.share_layer->rolling_variance; } else { l.scales = (float*)xcalloc(n, sizeof(float)); for (i = 0; i < n; ++i) { l.scales[i] = 1; } if (train) { l.scale_updates = (float*)xcalloc(n, sizeof(float)); l.mean = (float*)xcalloc(n, sizeof(float)); l.variance = (float*)xcalloc(n, sizeof(float)); l.mean_delta = (float*)xcalloc(n, sizeof(float)); l.variance_delta = (float*)xcalloc(n, sizeof(float)); } l.rolling_mean = (float*)xcalloc(n, sizeof(float)); l.rolling_variance = (float*)xcalloc(n, sizeof(float)); } ...... return l; }
yolo_layer.c中make_yolo_layer()函數:
layer make_yolo_layer(int batch, int w, int h, int n, int total, int *mask, int classes, int max_boxes) { int i; layer l = { (LAYER_TYPE)0 }; l.type = YOLO; // 層類別。 l.n = n; // 一個cell能預測多少個b-box。 l.total = total; // anchors數目,9。 l.batch = batch; // 一個batch包含的圖像張數。 l.h = h; // input的高。 l.w = w; // imput的寬。 l.c = n*(classes + 4 + 1); l.out_w = l.w; // output的高。 l.out_h = l.h; // output的寬。 l.out_c = l.c; // output的通道,等於卷積核個數。 l.classes = classes; // 目標類別數。 l.cost = (float*)xcalloc(1, sizeof(float)); // yolo層總的損失。 l.biases = (float*)xcalloc(total * 2, sizeof(float)); // 儲存b-box的anchor box的[w,h]。 if(mask) l.mask = mask; // 有mask傳入。 else{ l.mask = (int*)xcalloc(n, sizeof(int)); for(i = 0; i < n; ++i){ l.mask[i] = i; } } l.bias_updates = (float*)xcalloc(n * 2, sizeof(float)); // 儲存b-box的anchor box的[w,h]的更新值。 l.outputs = h*w*n*(classes + 4 + 1); // 一張訓練圖片經過yolo層后得到的輸出元素個數(Grid數*每個Grid預測的矩形框數*每個矩形框的參數個數) l.inputs = l.outputs; // 一張訓練圖片輸入到yolo層的元素個數(對於yolo_layer,輸入和輸出的元素個數相等) l.max_boxes = max_boxes; // 一張圖片最多有max_boxes個ground truth矩形框,這個數量時固定寫死的。 l.truths = l.max_boxes*(4 + 1); // 4個定位參數+1個物體類別,大於GT實際參數數量。 l.delta = (float*)xcalloc(batch * l.outputs, sizeof(float)); // yolo層誤差項,包含整個batch的。 l.output = (float*)xcalloc(batch * l.outputs, sizeof(float)); // yolo層所有輸出,包含整個batch的。
/* 存儲b-box的Anchor box的[w,h]的初始化,在parse.c中parse_yolo函數會加載cfg中Anchor尺寸。*/ for(i = 0; i < total*2; ++i){ l.biases[i] = .5; } /* 前向運算,反向傳播函數。*/ l.forward = forward_yolo_layer; l.backward = backward_yolo_layer; #ifdef GPU l.forward_gpu = forward_yolo_layer_gpu; l.backward_gpu = backward_yolo_layer_gpu; l.output_gpu = cuda_make_array(l.output, batch*l.outputs); l.output_avg_gpu = cuda_make_array(l.output, batch*l.outputs); l.delta_gpu = cuda_make_array(l.delta, batch*l.outputs); free(l.output); if (cudaSuccess == cudaHostAlloc(&l.output, batch*l.outputs*sizeof(float), cudaHostRegisterMapped)) l.output_pinned = 1; else { cudaGetLastError(); // reset CUDA-error l.output = (float*)xcalloc(batch * l.outputs, sizeof(float)); } free(l.delta); if (cudaSuccess == cudaHostAlloc(&l.delta, batch*l.outputs*sizeof(float), cudaHostRegisterMapped)) l.delta_pinned = 1; else { cudaGetLastError(); // reset CUDA-error l.delta = (float*)xcalloc(batch * l.outputs, sizeof(float)); } #endif fprintf(stderr, "yolo\n"); srand(time(0)); return l; }
這里要強調下"darknet/src/list.h"中定義的數據結構list:
typedef struct node{ void *val; struct node *next; struct node *prev; } node; typedef struct list{ int size; // list的所有節點個數。 node *front; // list的首節點。 node *back; // list的普通節點。 } list; // list類型變量保存所有的網絡參數,有很多的sections節點,每個section中又有一個保存層參數的小list。
以及"darknet/src/parser.c"中定義的數據結構section:
typedef struct{ char *type; // section的類型,保存的是網絡中每一層的網絡類型和參數。在.cfg配置文件中, 以‘[’開頭的行被稱為一個section(段)。 list *options; // section的參數信息。 }section;
"darknet/src/parser.c"--read_cfg()函數的作用就是讀取.cfg配置文件並返回給list類型變量sections:
/* 讀取神經網絡結構配置文件.cfg文件中的配置數據,將每個神經網絡層參數讀取到每個section結構體(每個section是sections的一個節點)中,而后全部插入到list結構體sections中並返回。*/ /* param: filename是C風格字符數組,神經網絡結構配置文件路徑。*/ /* return: list結構體指針,包含從神經網絡結構配置文件中讀入的所有神經網絡層的參數。*/ list *read_cfg(char *filename) { FILE *file = fopen(filename, "r"); if(file == 0) file_error(filename); /* 一個section表示配置文件中的一個字段,也就是網絡結構中的一層,因此,一個section將讀取並存儲某一層的參數以及該層的type。 */ char *line; int nu = 0; // 當前讀取行記號。 list *sections = make_list(); // sections包含所有的神經網絡層參數。 section *current = 0; // 當前讀取到的某一層。 while((line=fgetl(file)) != 0){ ++ nu; strip(line); // 去除讀入行中含有的空格符。 switch(line[0]){ /* 以'['開頭的行是一個新的section,其內容是層的type,比如[net],[maxpool],[convolutional]... */ case '[': current = (section*)xmalloc(sizeof(section)); // 讀到了一個新的section:current。 list_insert(sections, current); // list.c中,list_insert函數入口,將該新的section保存起來。 current->options = make_list(); current->type = line; break; case '\0': // 空行。 case '#': // 注釋。 case ';': // 空行。 free(line); // 對於上述三種情況直接釋放內存即可。 break; /* 剩下的才真正是網絡結構的數據,調用read_option()函數讀取,返回0說明文件中的數據格式有問題,將會提示錯誤。 */ default: if(!read_option(line, current->options)){ // 將讀取到的參數保存在current變量的options中,這里保存在options節點中的數據為kvp鍵值對類型。 fprintf(stderr, "Config file error line %d, could parse: %s\n", nu, line); free(line); } break; } } fclose(file); return sections; }
綜上,解析過程將鏈表中的網絡參數保存到network結構體,用於后續權重更新。
六. 權重更新
"darknet/src/detector.c"--train_detector()函數中:
...... /* 開始訓練網絡 */ float loss = 0; #ifdef GPU if (ngpus == 1) { int wait_key = (dont_show) ? 0 : 1; loss = train_network_waitkey(net, train, wait_key); // network.c中,train_network_waitkey函數入口,分配內存並執行網絡訓練。 } else { loss = train_networks(nets, ngpus, train, 4); // network_kernels.cu中,train_networks函數入口,多GPU訓練。 } #else loss = train_network(net, train); // train_network_waitkey(net, d, 0),CPU模式。 #endif if (avg_loss < 0 || avg_loss != avg_loss) avg_loss = loss; // if(-inf or nan) avg_loss = avg_loss*.9 + loss*.1; ......
以CPU訓練為例,"darknet/src/network.c"--train_network()函數,執行train_network_waitkey(net, d, 0):
float train_network_waitkey(network net, data d, int wait_key) { assert(d.X.rows % net.batch == 0); int batch = net.batch; // detector.c中train_detector函數在nets[k] = parse_network_cfg(cfgfile)處調用parser.c中的parse_net_options函數,有net->batch /= subdivs,所以batch_size = batch/subdivisions。 int n = d.X.rows / batch; // batch個數, 對於單GPU和CPU,n = subdivision。 float* X = (float*)xcalloc(batch * d.X.cols, sizeof(float)); float* y = (float*)xcalloc(batch * d.y.cols, sizeof(float)); int i; float sum = 0; for(i = 0; i < n; ++i){ get_next_batch(d, batch, i*batch, X, y); net.current_subdivision = i; float err = train_network_datum(net, X, y); // 調用train_network_datum函數得到誤差Loss。 sum += err; if(wait_key) wait_key_cv(5); } (*net.cur_iteration) += 1; #ifdef GPU update_network_gpu(net); #else // GPU update_network(net); #endif // GPU free(X); free(y); return (float)sum/(n*batch); }
其中,調用train_network_datum()函數計算error是核心:
float train_network_datum(network net, float *x, float *y) { #ifdef GPU if(gpu_index >= 0) return train_network_datum_gpu(net, x, y); // GPU模式,調用network_kernels.cu中train_network_datum_gpu函數。 #endif network_state state={0}; *net.seen += net.batch; state.index = 0; state.net = net; state.input = x; state.delta = 0; state.truth = y; state.train = 1; forward_network(net, state); // CPU模式,正向傳播。 backward_network(net, state); // CPU模式,BP。 float error = get_network_cost(net); // 計算Loss。 return error; }
進一步分析forward_network()函數:
void forward_network(network net, network_state state) { state.workspace = net.workspace; int i; for(i = 0; i < net.n; ++i){ state.index = i; layer l = net.layers[i]; if(l.delta && state.train){ scal_cpu(l.outputs * l.batch, 0, l.delta, 1); // blas.c中,scal_cpu函數入口。 } l.forward(l, state); // 不同層l.forward代表不同函數,如:convolutional_layer.c中,l.forward = forward_convolutional_layer;yolo_layer.c中,l.forward = forward_yolo_layer,CPU執行前向運算。 state.input = l.output; // 上一層的輸出傳遞給下一層的輸入。 } }
卷積層時,forward_convolutional_layer()函數:
void forward_convolutional_layer(convolutional_layer l, network_state state) {
/* 獲取卷積層輸出的長寬。*/ int out_h = convolutional_out_height(l); int out_w = convolutional_out_width(l); int i, j; fill_cpu(l.outputs*l.batch, 0, l.output, 1); // 把output初始化為0。
/* xnor-net,將inputs和weights二值化。*/ if (l.xnor && (!l.align_bit_weights || state.train)) { if (!l.align_bit_weights || state.train) { binarize_weights(l.weights, l.n, l.nweights, l.binary_weights); } swap_binary(&l); binarize_cpu(state.input, l.c*l.h*l.w*l.batch, l.binary_input); state.input = l.binary_input; }
/* m是卷積核的個數,k是每個卷積核的參數數量(l.size是卷積核的大小),n是每個輸出feature map的像素個數。*/ int m = l.n / l.groups; int k = l.size*l.size*l.c / l.groups; int n = out_h*out_w; static int u = 0; u++; for(i = 0; i < l.batch; ++i) { for (j = 0; j < l.groups; ++j) {
/* weights是卷積核的參數,a是指向權重的指針,b是指向工作空間指針,c是指向輸出的指針。*/ float *a = l.weights +j*l.nweights / l.groups; float *b = state.workspace; float *c = l.output +(i*l.groups + j)*n*m; if (l.xnor && l.align_bit_weights && !state.train && l.stride_x == l.stride_y) { memset(b, 0, l.bit_align*l.size*l.size*l.c * sizeof(float)); if (l.c % 32 == 0) { int ldb_align = l.lda_align; size_t new_ldb = k + (ldb_align - k%ldb_align); // (k / 8 + 1) * 8; int re_packed_input_size = l.c * l.w * l.h; memset(state.workspace, 0, re_packed_input_size * sizeof(float)); const size_t new_c = l.c / 32; size_t in_re_packed_input_size = new_c * l.w * l.h + 1; memset(l.bin_re_packed_input, 0, in_re_packed_input_size * sizeof(uint32_t)); // float32x4 by channel (as in cuDNN) repack_input(state.input, state.workspace, l.w, l.h, l.c); // 32 x floats -> 1 x uint32_t float_to_bit(state.workspace, (unsigned char *)l.bin_re_packed_input, l.c * l.w * l.h);
/* image to column,就是將圖像依照卷積核的大小拉伸為列向量,方便矩陣運算,將圖像每一個kernel轉換成一列。*/ im2col_cpu_custom((float *)l.bin_re_packed_input, new_c, l.h, l.w, l.size, l.stride, l.pad, state.workspace); int new_k = l.size*l.size*l.c / 32; transpose_uint32((uint32_t *)state.workspace, (uint32_t*)l.t_bit_input, new_k, n, n, new_ldb);
/* General Matrix Multiply函數,實現矩陣運算,也就是卷積運算。*/ gemm_nn_custom_bin_mean_transposed(m, n, k, 1, (unsigned char*)l.align_bit_weights, new_ldb, (unsigned char*)l.t_bit_input, new_ldb, c, n, l.mean_arr); } else { im2col_cpu_custom_bin(state.input, l.c, l.h, l.w, l.size, l.stride, l.pad, state.workspace, l.bit_align); // transpose B from NxK to KxN (x-axis (ldb = l.size*l.size*l.c) - should be multiple of 8 bits) { int ldb_align = l.lda_align; size_t new_ldb = k + (ldb_align - k%ldb_align); size_t t_intput_size = binary_transpose_align_input(k, n, state.workspace, &l.t_bit_input, ldb_align, l.bit_align); // 5x times faster than gemm()-float32 gemm_nn_custom_bin_mean_transposed(m, n, k, 1, (unsigned char*)l.align_bit_weights, new_ldb, (unsigned char*)l.t_bit_input, new_ldb, c, n, l.mean_arr); } } add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w); //添加偏移項。 /* 非線性變化,leaky RELU、Mish等激活函數。*/ if (l.activation == SWISH) activate_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.output); else if (l.activation == MISH) activate_array_mish(l.output, l.outputs*l.batch, l.activation_input, l.output); else if (l.activation == NORM_CHAN) activate_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output); else if (l.activation == NORM_CHAN_SOFTMAX) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 0); else if (l.activation == NORM_CHAN_SOFTMAX_MAXVAL) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 1); else activate_array_cpu_custom(l.output, m*n*l.batch, l.activation); return; } else { float *im = state.input + (i*l.groups + j)*(l.c / l.groups)*l.h*l.w; if (l.size == 1) { b = im; } else { im2col_cpu_ext(im, // input l.c / l.groups, // input channels l.h, l.w, // input size (h, w) l.size, l.size, // kernel size (h, w) l.pad * l.dilation, l.pad * l.dilation, // padding (h, w) l.stride_y, l.stride_x, // stride (h, w) l.dilation, l.dilation, // dilation (h, w) b); // output } gemm(0, 0, m, n, k, 1, a, k, b, n, 1, c, n); // bit-count to float } } } if(l.batch_normalize){ // BN層,加速收斂。 forward_batchnorm_layer(l, state); } else { // 直接加上bias,output += bias。 add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w); }
/* 非線性變化,leaky RELU、Mish等激活函數。*/ if (l.activation == SWISH) activate_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.output); else if (l.activation == MISH) activate_array_mish(l.output, l.outputs*l.batch, l.activation_input, l.output); else if (l.activation == NORM_CHAN) activate_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output); else if (l.activation == NORM_CHAN_SOFTMAX) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 0); else if (l.activation == NORM_CHAN_SOFTMAX_MAXVAL) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 1); else activate_array_cpu_custom(l.output, l.outputs*l.batch, l.activation); if(l.binary || l.xnor) swap_binary(&l); // 二值化。 if(l.assisted_excitation && state.train) assisted_excitation_forward(l, state); if (l.antialiasing) { network_state s = { 0 }; s.train = state.train; s.workspace = state.workspace; s.net = state.net; s.input = l.output; forward_convolutional_layer(*(l.input_layer), s); memcpy(l.output, l.input_layer->output, l.input_layer->outputs * l.input_layer->batch * sizeof(float)); } }
yolo層時,forward_yolo_layer()函數:
void forward_yolo_layer(const layer l, network_state state) { int i, j, b, t, n; memcpy(l.output, state.input, l.outputs*l.batch * sizeof(float)); // 將層輸入直接copy到層輸出。
/* 在cpu模式,把預測輸出的x,y,confidence和所有類別都sigmoid激活,確保值在0~1之間。*/ #ifndef GPU for (b = 0; b < l.batch; ++b) { for (n = 0; n < l.n; ++n) { int index = entry_index(l, b, n*l.w*l.h, 0); // 獲取第b個batch開始的index。
/* 對預測的tx,ty進行邏輯回歸。*/ activate_array(l.output + index, 2 * l.w*l.h, LOGISTIC); // x,y, scal_add_cpu(2 * l.w*l.h, l.scale_x_y, -0.5*(l.scale_x_y - 1), l.output + index, 1); // scale x,y index = entry_index(l, b, n*l.w*l.h, 4); // 獲取第b個batch confidence開始的index。 activate_array(l.output + index, (1 + l.classes)*l.w*l.h, LOGISTIC); // 對預測的confidence以及class進行邏輯回歸。 } } #endif // delta is zeroed memset(l.delta, 0, l.outputs * l.batch * sizeof(float)); // 將yolo層的誤差項進行初始化(包含整個batch的)。 if (!state.train) return; // 不是訓練階段,return。 float tot_iou = 0; // 總的IOU。 float tot_giou = 0; float tot_diou = 0; float tot_ciou = 0; float tot_iou_loss = 0; float tot_giou_loss = 0; float tot_diou_loss = 0; float tot_ciou_loss = 0; float recall = 0; float recall75 = 0; float avg_cat = 0; float avg_obj = 0; float avg_anyobj = 0; int count = 0; int class_count = 0; *(l.cost) = 0; // yolo層的總損失初始化為0。 for (b = 0; b < l.batch; ++b) { // 遍歷batch中的每一張圖片。 for (j = 0; j < l.h; ++j) { for (i = 0; i < l.w; ++i) { // 遍歷每個Grid cell, 當前cell編號[j, i]。 for (n = 0; n < l.n; ++n) { // 遍歷每一個bbox,當前bbox編號[n]。 const int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1); // 預測b-box類別s下標。 const int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4); // 預測b-box objectness下標。 const int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0); // 獲得第j*w+i個cell第n個b-box的index。
const int stride = l.w*l.h;
/* 計算第j*w+i個cell第n個b-box在當前特征圖上的相對位置[x,y],在網絡輸入圖片上的相對寬度、高度[w,h]。*/ box pred = get_yolo_box(l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.w*l.h); float best_match_iou = 0; int best_match_t = 0; float best_iou = 0; // 保存最大IOU。 int best_t = 0; // 保存最大IOU的bbox id。 for (t = 0; t < l.max_boxes; ++t) { // 遍歷每一個GT bbox。 box truth = float_to_box_stride(state.truth + t*(4 + 1) + b*l.truths, 1); // 將第t個bbox由float數組轉bbox結構體,方便計算IOU。 int class_id = state.truth[t*(4 + 1) + b*l.truths + 4]; // 獲取第t個bbox的類別,檢查是否有標注錯誤。 if (class_id >= l.classes || class_id < 0) { printf("\n Warning: in txt-labels class_id=%d >= classes=%d in cfg-file. In txt-labels class_id should be [from 0 to %d] \n", class_id, l.classes, l.classes - 1); printf("\n truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f, class_id = %d \n", truth.x, truth.y, truth.w, truth.h, class_id); if (check_mistakes) getchar(); continue; // if label contains class_id more than number of classes in the cfg-file and class_id check garbage value } if (!truth.x) break; // 如果x坐標為0則break,因為定義了max_boxes個b-box。 float objectness = l.output[obj_index]; // 預測bbox object置信度。 if (isnan(objectness) || isinf(objectness)) l.output[obj_index] = 0;
/* 獲得預測b-box的類別信息,如果某個類別的概率超過0.25返回1。*/ int class_id_match = compare_yolo_class(l.output, l.classes, class_index, l.w*l.h, objectness, class_id, 0.25f); float iou = box_iou(pred, truth); // 計算pred b-box與第t個GT bbox之間的IOU。 if (iou > best_match_iou && class_id_match == 1) { // class_id_match=1的限制,即預測b-box的置信度必須大於0.25。 best_match_iou = iou; best_match_t = t; } if (iou > best_iou) { best_iou = iou; // 更新最大的IOU。 best_t = t; // 記錄該GT b-box的編號t。 } } avg_anyobj += l.output[obj_index]; // 統計pred b-box的confidence。 l.delta[obj_index] = l.cls_normalizer * (0 - l.output[obj_index]); // 將所有pred b-box都當做noobject, 計算其confidence梯度,cls_normalizer是平衡系數。 if (best_match_iou > l.ignore_thresh) { // best_iou大於閾值則說明pred box有物體。 const float iou_multiplier = best_match_iou*best_match_iou;// (best_match_iou - l.ignore_thresh) / (1.0 - l.ignore_thresh); if (l.objectness_smooth) { l.delta[obj_index] = l.cls_normalizer * (iou_multiplier - l.output[obj_index]); int class_id = state.truth[best_match_t*(4 + 1) + b*l.truths + 4]; if (l.map) class_id = l.map[class_id]; const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f; l.delta[class_index + stride*class_id] = class_multiplier * (iou_multiplier - l.output[class_index + stride*class_id]); } else l.delta[obj_index] = 0; } else if (state.net.adversarial) { // 自對抗訓練。 int stride = l.w*l.h; float scale = pred.w * pred.h; if (scale > 0) scale = sqrt(scale); l.delta[obj_index] = scale * l.cls_normalizer * (0 - l.output[obj_index]); int cl_id; for (cl_id = 0; cl_id < l.classes; ++cl_id) { if(l.output[class_index + stride*cl_id] * l.output[obj_index] > 0.25) l.delta[class_index + stride*cl_id] = scale * (0 - l.output[class_index + stride*cl_id]); } } if (best_iou > l.truth_thresh) { // pred b-box為完全預測正確樣本,cfg中truth_thresh=1,語句永遠不可能成立。 const float iou_multiplier = best_iou*best_iou;// (best_iou - l.truth_thresh) / (1.0 - l.truth_thresh); if (l.objectness_smooth) l.delta[obj_index] = l.cls_normalizer * (iou_multiplier - l.output[obj_index]); else l.delta[obj_index] = l.cls_normalizer * (1 - l.output[obj_index]); int class_id = state.truth[best_t*(4 + 1) + b*l.truths + 4]; if (l.map) class_id = l.map[class_id]; delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, 0, l.focal_loss, l.label_smooth_eps, l.classes_multipliers); const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f; if (l.objectness_smooth) l.delta[class_index + stride*class_id] = class_multiplier * (iou_multiplier - l.output[class_index + stride*class_id]); box truth = float_to_box_stride(state.truth + best_t*(4 + 1) + b*l.truths, 1); delta_yolo_box(truth, l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta); } } } } for (t = 0; t < l.max_boxes; ++t) { // 遍歷每一個GT box。 box truth = float_to_box_stride(state.truth + t*(4 + 1) + b*l.truths, 1); // 將第t個b-box由float數組轉b-box結構體,方便計算IOU。 if (truth.x < 0 || truth.y < 0 || truth.x > 1 || truth.y > 1 || truth.w < 0 || truth.h < 0) { char buff[256]; printf(" Wrong label: truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f \n", truth.x, truth.y, truth.w, truth.h); sprintf(buff, "echo \"Wrong label: truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f\" >> bad_label.list", truth.x, truth.y, truth.w, truth.h); system(buff); } int class_id = state.truth[t*(4 + 1) + b*l.truths + 4]; if (class_id >= l.classes || class_id < 0) continue; // if label contains class_id more than number of classes in the cfg-file and class_id check garbage value if (!truth.x) break; // 如果x坐標為0則取消,定義了max_boxes個bbox,可能實際上沒那么多。 float best_iou = 0; // 保存最大的IOU。 int best_n = 0; // 保存最大IOU的b-box index。 i = (truth.x * l.w); // 獲得當前t個GT b-box所在的cell。 j = (truth.y * l.h); box truth_shift = truth; truth_shift.x = truth_shift.y = 0; // 將truth_shift的box位置移動到0,0。 for (n = 0; n < l.total; ++n) { // 遍歷每一個anchor b-box找到與GT b-box最大的IOU。 box pred = { 0 }; pred.w = l.biases[2 * n] / state.net.w; // 計算pred b-box的w在相對整張輸入圖片的位置。 pred.h = l.biases[2 * n + 1] / state.net.h; // 計算pred bbox的h在相對整張輸入圖片的位置。 float iou = box_iou(pred, truth_shift); // 計算GT box truth_shift與預測b-box pred二者之間的IOU。 if (iou > best_iou) { best_iou = iou; // 記錄最大的IOU。 best_n = n; // 記錄該b-box的編號n。 } } int mask_n = int_index(l.mask, best_n, l.n); // 上面記錄b-box的編號,是否由該層Anchor預測的。 if (mask_n >= 0) { int class_id = state.truth[t*(4 + 1) + b*l.truths + 4]; if (l.map) class_id = l.map[class_id]; int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0); // 獲得best_iou對應anchor box的index。 const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f; // 控制樣本數量不均衡,即Focal Loss中的alpha。 ious all_ious = delta_yolo_box(truth, l.output, l.biases, best_n, box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta); // 計算best_iou對應Anchor bbox的[x,y,w,h]的梯度。
/* 模板檢測最新的工作,metricl learning,包括IOU/GIOU/DIOU/CIOU Loss等。*/ // range is 0 <= 1 tot_iou += all_ious.iou; tot_iou_loss += 1 - all_ious.iou; // range is -1 <= giou <= 1 tot_giou += all_ious.giou; tot_giou_loss += 1 - all_ious.giou; tot_diou += all_ious.diou; tot_diou_loss += 1 - all_ious.diou; tot_ciou += all_ious.ciou; tot_ciou_loss += 1 - all_ious.ciou; int obj_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4); // 獲得best_iou對應anchor box的confidence的index。 avg_obj += l.output[obj_index]; // 統計confidence。 l.delta[obj_index] = class_multiplier * l.cls_normalizer * (1 - l.output[obj_index]); // 計算confidence的梯度。 int class_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4 + 1); // 獲得best_iou對應GT box的class的index。 delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, &avg_cat, l.focal_loss, l.label_smooth_eps, l.classes_multipliers); // 獲得best_iou對應anchor box的class的index。 ++count; ++class_count; if (all_ious.iou > .5) recall += 1; if (all_ious.iou > .75) recall75 += 1; } // iou_thresh for (n = 0; n < l.total; ++n) { int mask_n = int_index(l.mask, n, l.n); if (mask_n >= 0 && n != best_n && l.iou_thresh < 1.0f) { box pred = { 0 }; pred.w = l.biases[2 * n] / state.net.w; pred.h = l.biases[2 * n + 1] / state.net.h; float iou = box_iou_kind(pred, truth_shift, l.iou_thresh_kind); // IOU, GIOU, MSE, DIOU, CIOU // iou, n if (iou > l.iou_thresh) { int class_id = state.truth[t*(4 + 1) + b*l.truths + 4]; if (l.map) class_id = l.map[class_id]; int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0); const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f; ious all_ious = delta_yolo_box(truth, l.output, l.biases, n, box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta); // range is 0 <= 1 tot_iou += all_ious.iou; tot_iou_loss += 1 - all_ious.iou; // range is -1 <= giou <= 1 tot_giou += all_ious.giou; tot_giou_loss += 1 - all_ious.giou; tot_diou += all_ious.diou; tot_diou_loss += 1 - all_ious.diou; tot_ciou += all_ious.ciou; tot_ciou_loss += 1 - all_ious.ciou; int obj_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4); avg_obj += l.output[obj_index]; l.delta[obj_index] = class_multiplier * l.cls_normalizer * (1 - l.output[obj_index]); int class_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4 + 1); delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, &avg_cat, l.focal_loss, l.label_smooth_eps, l.classes_multipliers); ++count; ++class_count; if (all_ious.iou > .5) recall += 1; if (all_ious.iou > .75) recall75 += 1; } } } } // averages the deltas obtained by the function: delta_yolo_box()_accumulate for (j = 0; j < l.h; ++j) { for (i = 0; i < l.w; ++i) { for (n = 0; n < l.n; ++n) { int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0); // 獲得第j*w+i個cell第n個b-box的index。 int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1); // 獲得第j*w+i個cell第n個b-box的類別。 const int stride = l.w*l.h; // 特征圖的大小。 averages_yolo_deltas(class_index, box_index, stride, l.classes, l.delta); // 對梯度進行平均。 } } } } ......
// gIOU loss + MSE (objectness) loss
if (l.iou_loss == MSE) {
*(l.cost) = pow(mag_array(l.delta, l.outputs * l.batch), 2);
}
else {
// Always compute classification loss both for iou + cls loss and for logging with mse loss
// TODO: remove IOU loss fields before computing MSE on class
// probably split into two arrays
if (l.iou_loss == GIOU) {
avg_iou_loss = count > 0 ? l.iou_normalizer * (tot_giou_loss / count) : 0; // 平均IOU損失,參考上面代碼,tot_iou_loss += 1 - all_ious.iou。
}
else {
avg_iou_loss = count > 0 ? l.iou_normalizer * (tot_iou_loss / count) : 0; // 平均IOU損失,參考上面代碼,tot_iou_loss += 1 - all_ious.iou。
}
*(l.cost) = avg_iou_loss + classification_loss; // Loss值傳遞給l.cost,IOU與分類損失求和。
}
loss /= l.batch; // 平均Loss。
classification_loss /= l.batch;
iou_loss /= l.batch;
......
}
再來分析backward_network()函數:
{
int i;
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;
if(i == 0){
state.input = original_input;
state.delta = original_delta;
} else{
layer prev = net.layers[i-1];
state.input = prev.output;
state.delta = prev.delta; // delta是指針變量,對state.delta做修改,就相當與對prev層的delta做了修改。
}
layer l = net.layers[i];
if (l.stopbackward) break;
if (l.onlyforward) continue;
l.backward(l, state); // 不同層l.backward代表不同函數,如:convolutional_layer.c中,l.backward = backward_convolutional_layer;yolo_layer.c中,l.backward = backward_yolo_layer,CPU執行反向傳播。
}
}
卷積層時,backward_convolutional_layer()函數:
void backward_convolutional_layer(convolutional_layer l, network_state state) { int i, j;
/* m是卷積核的個數,k是每個卷積核的參數數量(l.size是卷積核的大小),n是每個輸出feature map的像素個數。*/ int m = l.n / l.groups; int n = l.size*l.size*l.c / l.groups; int k = l.out_w*l.out_h;
/* 更新delta。*/ if (l.activation == SWISH) gradient_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.delta); else if (l.activation == MISH) gradient_array_mish(l.outputs*l.batch, l.activation_input, l.delta); else if (l.activation == NORM_CHAN_SOFTMAX || l.activation == NORM_CHAN_SOFTMAX_MAXVAL) gradient_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.delta); else if (l.activation == NORM_CHAN) gradient_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.delta); else gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta); if (l.batch_normalize) { // BN層,加速收斂。 backward_batchnorm_layer(l, state); } else { // 直接加上bias。 backward_bias(l.bias_updates, l.delta, l.batch, l.n, k); } for (i = 0; i < l.batch; ++i) { for (j = 0; j < l.groups; ++j) { float *a = l.delta + (i*l.groups + j)*m*k; float *b = state.workspace; float *c = l.weight_updates + j*l.nweights / l.groups;
/* 進入本函數之前,在backward_network()函數中,已經將net.input賦值為prev.output,若當前層為第l層,則net.input為第l-1層的output。*/ float *im = state.input + (i*l.groups + j)* (l.c / l.groups)*l.h*l.w; im2col_cpu_ext( im, // input l.c / l.groups, // input channels l.h, l.w, // input size (h, w) l.size, l.size, // kernel size (h, w) l.pad * l.dilation, l.pad * l.dilation, // padding (h, w) l.stride_y, l.stride_x, // stride (h, w) l.dilation, l.dilation, // dilation (h, w) b); // output gemm(0, 1, m, n, k, 1, a, k, b, k, 1, c, n); // 計算當前層weights更新。
/* 計算上一層的delta,進入本函數之前,在backward_network()函數中,已經將net.delta賦值為prev.delta,若當前層為第l層,則net.delta為第l-1層的delta。*/ if (state.delta) { a = l.weights + j*l.nweights / l.groups; b = l.delta + (i*l.groups + j)*m*k; c = state.workspace; gemm(1, 0, n, k, m, 1, a, n, b, k, 0, c, k); col2im_cpu_ext( state.workspace, // input l.c / l.groups, // input channels (h, w) l.h, l.w, // input size (h, w) l.size, l.size, // kernel size (h, w) l.pad * l.dilation, l.pad * l.dilation, // padding (h, w) l.stride_y, l.stride_x, // stride (h, w) l.dilation, l.dilation, // dilation (h, w) state.delta + (i*l.groups + j)* (l.c / l.groups)*l.h*l.w); // output (delta) } } } }
yolo層時,backward_yolo_layer()函數:
void backward_yolo_layer(const layer l, network_state state) { axpy_cpu(l.batch*l.inputs, 1, l.delta, 1, state.delta, 1); // 直接把l.delta拷貝給上一層的delta。注意 net.delta 指向 prev_layer.delta。 }
正向、反向傳播后,通過get_network_cost()函數計算Loss:
float get_network_cost(network net) { int i; float sum = 0; int count = 0; for(i = 0; i < net.n; ++i){ if(net.layers[i].cost){ // 獲取各層的損失,只有detection層,也就是yolo層,有cost。 sum += net.layers[i].cost[0]; // Loss總和存在cost[0]中,見cost_layer.c中forward_cost_layer()函數。 ++count; } } return sum/count; // 返回平均損失。 }
這里用一張圖解釋下Loss公式:
CIOU_Loss是創新點,與GIOU_Loss相比,引入了重疊面積與中心點的距離Dis_2來區分預測框a與b的定位差異,同時還引入了預測框和目標框的長寬比一致性因子ν,將a與c這種重疊面積與中心點距離相同但長寬比與目標框適配程度有差異的預測框區分開來,如圖:
計算好Loss需要update_network():
void update_network(network net)
{
int i;
int update_batch = net.batch*net.subdivisions;
float rate = get_current_rate(net);
for(i = 0; i < net.n; ++i){
layer l = net.layers[i];
if(l.update){
l.update(l, update_batch, rate, net.momentum, net.decay); // convolutional_layer.c中,l.update = update_convolutional_layer。
}
}
}
update_convolutional_layer()函數:
void update_convolutional_layer(convolutional_layer l, int batch, float learning_rate_init, float momentum, float decay) { float learning_rate = learning_rate_init*l.learning_rate_scale; axpy_cpu(l.nweights, -decay*batch, l.weights, 1, l.weight_updates, 1); // blas.c中,axpy_cpu函數入口,for(i = 0; i < l.nweights; ++i),l.weight_updates[i*1] -= decay*batch*l.weights[i*1]。 axpy_cpu(l.nweights, learning_rate / batch, l.weight_updates, 1, l.weights, 1); // for(i = 0; i < l.nweights; ++i),l.weights[i*1] += (learning_rate/batch)*l.weight_updates[i*1] scal_cpu(l.nweights, momentum, l.weight_updates, 1); // blas.c中,scal_cpu函數入口,for(i = 0; i < l.nweights; ++i),l.weight_updates[i*1] *= momentum。 axpy_cpu(l.n, learning_rate / batch, l.bias_updates, 1, l.biases, 1); // for(i = 0; i < l.n; ++i),l.biases[i*1] += (learning_rate/batch)*l.bias_updates[i*1]。 scal_cpu(l.n, momentum, l.bias_updates, 1); // for(i = 0; i < l.n; ++i),l.bias_updates[i*1] *= momentum。 if (l.scales) { axpy_cpu(l.n, learning_rate / batch, l.scale_updates, 1, l.scales, 1); scal_cpu(l.n, momentum, l.scale_updates, 1); } }
同樣,在network_kernels.cu里,有GPU模式下的forward&backward相關的函數,涉及數據格式轉換及加速,此處只討論原理,暫時忽略GPU部分的代碼。
void forward_backward_network_gpu(network net, float *x, float *y)
{
......
forward_network_gpu(net, state); // 正向。
backward_network_gpu(net, state); // 反向。
......
}
CPU模式下,采用帶momentum的常規GD更新weights,同時在network.c中也提供了也提供了train_network_sgd()函數接口;GPU模式提供了adam選項,convolutional_layer.c中make_convolutional_layer()函數有體現。
七. 調參總結
本人在實際項目中涉及的是工業中的鋼鐵表面缺陷檢測場景,不到2000張圖片,3類,數據量很少。理論上YOLO系列並不太適合缺陷檢測的問題,基於分割+分類的網絡、Cascade-RCNN等或許是更好的選擇,但我本着實驗的態度,進行了多輪的訓練和對比,整體上效果還是不錯的。
1.max_batches: AlexeyAB在github工程上有提到,類別數*2000作為參考,不要少於6000,但這個是使用預訓練權重的情況。如果train from scratch,要適當增加,具體要看你的數據情況,網絡需要額外的時間來從零開始學習;
2.pretrain or not:當數據量很少時,預訓練確實能更快使模型收斂,效果也不錯,但缺陷檢測這類問題,缺陷目標特征本身的特異性還是比較強的,雖然我的數據量也很少,但scratch的方式還是能取得稍好一些的效果;
3.anchors:cfg文件默認的anchors是基於COCO數據集,可以說尺度比較均衡,使用它效果不會差,但如果你自己的數據在尺度分布上不太均衡,建議自行生成新的anchors,可以直接使用源碼里面的腳本,注意,要根據生成anchors的size(1-yolo:<30*30,2-yolo:<60*60,3-yolo:others)來改變索引值masks以及前一個conv層的filters參數;
4.rotate:YOLO-V4在目標檢測這一塊,其實沒有用到旋轉來進行數據增強,因此我在線下對數量最少的一個類進行了180旋轉對稱增強,該類樣本數擴增一倍,效果目前還不明顯,可能是數據量增加的還是太少,而且我還在訓練對比,完成后可以補充;
5.mosaic:馬賽克數據增強是必須要有的,mAP值提升比較明顯,需要安裝opencv,且和cutmix不能同時使用。