Yolov3代碼分析與訓練本身數據集

   如今要針對咱們需求引入檢測模型,只檢測人物,而後是圖像能側立,這樣人物在裏面佔比更多,也更清晰,也不須要檢測人佔比小的狀況,以下是針對這個需求,用的yolov3-tiny模型訓練後的效果。算法

  

  Yolov3模型網上也講爛了,可是總感受不看代碼,不清楚具體實現看講解老是不清晰,在這分析下darknet的實現,給本身解惑,順便也作個筆記。json

  首先查看打開yolov3.cfg,咱們看下網絡,能夠用netron查看圖形界面,能夠發現網絡主要以卷積層構成,shortcut(殘差鏈接),route(通道組合)三種構成,首先用步長爲2的卷積縮小圖像一次,而後開始用shortcut(殘差鏈接)鏈接一次再用步長爲2的卷積縮小圖像縮小二次,後面開始不斷用卷積與殘差組合,到開始分支,分出二個部分,每分出一個分支就把主支圖像縮小次,最後加上主支部分一共三個分支,就是一共有3個yolo層,其中主支部分縮小了五次,第一次分支縮小三次,第二次分支縮小四次。api

  這裏也解答了之前個人一個疑惑,從ResNet網絡開始,開始隔層交流,不論是相加仍是整合,我疑惑的是如何在文件這種列表形式下描述分支結構,原來很簡單,一次描述一個分支,而後用route/shortcut記錄分支層,繼續向下描述。網絡

  回到網絡部分,這三次分支能夠用這圖表示,網絡上不知誰的,確實表達了不少要說的,不過有個問題,應該是版本更新了,我手上配置拿的是長寬是608*608,因此下圖須要改一些,如13*13是19*19,你們明白就行,還有主支是32(2^5)倍那個,16與8是第二分支與第一分支,其中先算主支部分,算完主支而後上層卷積層upsample再與第二分支route一塊兒算,第一分支同這邏輯。  框架

  

   

  這個yolo層就是主支部分,能夠看如上分析,在這層特徵圖長寬只有19*19,對應如上 anchors中是根據k-means算法拿到對應圖集K=9的分類簇框,其中mask=6,7,8指向最後三個大框,主支部分主要檢測大物體,根據前面分析能夠知道中框包含了大框的特徵圖結果,而小框包含中大的特徵圖結果,這應該yolov3相比v2針對小物體的識別提升的緣由,而shortcut與route則保證網絡能加深到一百多層。dom

  darknet種各層的主要有三個函數,分別是make_xxx_layer, forward_xxx_layer, backward_xxx_layer, 這三個函數make_xxx_layer是初始各類參數,根據參數自動算一些參數,如卷積層,根據傳入特徵圖的大小與核的參數,就能算出傳出特徵圖的大小,以及申請內存或是顯存,forward_xxx_layer表示根據當前層參數計算預測,而backward_xxx_layer根據上層的delta(如上層是yolo層,delta就表示指望輸出-預測輸出)與輸入計算梯度,並更新下層須要的delta,以及還有個update_xxx_layer用於根據backward_xxx_layer計算的梯度來更新參數。而yolo,region,detection,softmax這幾種檢測層相對卷積層來講,沒有參數weight,主要用來計算delta(指望輸出-預測輸出).而反向傳播就是從這個yolo的delta開始的,在yolo的反向傳播中,先把這個delta給卷積層的delta,而後結合前一層的輸出,求得當前層的梯度,並把delta結合當前層的weights求得新的delta,而後下一層卷積層根據這個delta梯度,循環下去更新全部參數。async

  下面根據代碼來分析yolov層,先看下make_yolo_layer的實現。ide

//在yolov3中,n=3 total=9(mask在yolov3中分三組,分別是[0,1,2/3,4,5/6,7,8],n表示分組裏幾個數據,n*3=total)
//608*608下,第一組是19*19,第二組是38*38,第三組是76*76,每組檢查對應索引裏的anchors
layer make_yolo_layer(int batch, int w, int h, int n, int total, int *mask, int classes)
{
    //在這假設在主支中,其中縮小5次,608/(2^5)=19,這個分支中,w=h=19
    int i;
    layer l = { 0 };
    l.type = YOLO;
    //檢測幾種類型邊框(這分支對應上面anchors[6,7,8]這個用來檢測大邊框)
    l.n = n;
    //如上,在yolov3中,有大中小分別有三個邊框聚合,一共是3*3=9
    //而在yolov3-tiny中,有大小分別三個邊框聚合,一共是3*2=6
    l.total = total;
    //通常來講,訓練爲32,預測爲1
    l.batch = batch;
    //主支,608/(2^5)=19
    l.h = h;
    l.w = w;
    //如上在主支中,每張特徵圖有19*19個元素,c表示特徵圖個數,n表示對應的anchors[6,7,8]這三個
    //4表示box座標,1是Po(預測機率與IOU正確率)的機率,classes是預測的類別數
    l.c = n * (classes + 4 + 1);
    l.out_w = l.w;
    l.out_h = l.h;
    l.out_c = l.c;
    //檢測一共有多少個類別
    l.classes = classes;
    //計算代價(數據集總體的偏差描述)
    l.cost = calloc(1, sizeof(float));
    //對應表示anchors
    l.biases = calloc(total * 2, sizeof(float));
    //對應如上anchors中n對應須要用到的索引
    if (mask) l.mask = mask;
    else {
        l.mask = calloc(n, sizeof(int));
        for (i = 0; i < n; ++i) {
            l.mask[i] = i;
        }
    }
    l.bias_updates = calloc(n * 2, sizeof(float));
    //當前層batch爲1的全部輸出特徵圖包含的元素個數,每一個元素爲一個float
    l.outputs = h * w*n*(classes + 4 + 1);
    //當前層batch爲1時全部輸入特徵圖包含的元素個數,每一個元素爲一個float
    l.inputs = l.outputs;
    //標籤(真實)數據,這裏90表示如上w*h(19*19中)每一個格子最多有90個label。
    //而每一個label前面4個float表示box的四個點,後面1個float表示當前類別
    l.truths = 90 * (4 + 1);
    //計算偏差(數據單個的偏差描述),用來表示 指望輸出-真實輸出
    l.delta = calloc(batch*l.outputs, sizeof(float));
    l.output = calloc(batch*l.outputs, sizeof(float));
    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.delta_gpu = cuda_make_array(l.delta, batch*l.outputs);
#endif

    fprintf(stderr, "yolo\n");
    srand(0);

    return l;
}
make_yolo_layer

  以主支來作分析,每張特徵圖有19*19個元素,c表示特徵圖個數,結合上圖,一共有3*(80+4+1)=255,簡單來講,分別對應116,90, 156,198, 373,326這三個聚類簇,其中前85個就是116,90這框的結果,其中前85個就是116,90這框的結果,85個特徵圖中前4個是邊框座標,1個置信度,80個類別機率,一共三個這種85特徵圖就是255個,還有標籤(真實)數據,這裏90表示如上w*h(19*19中)每一個格子最多有90個label,而每一個label前面4個float表示box的四個點,後面1個float表示當前類別,搞清楚這二個對應排列,在以下的forward_yolo_layer裏,咱們才能明白如何計算的delta,代碼加上註釋有點多,這段就不貼了,代碼部分說明下。函數

  主要有二部分,仍是先說下,在這裏delta表示 指望輸出-out(不一樣框架可能不一樣,我看caffe裏的yolo實現,就是out-指望輸出)。學習

  第一部分,前面查找全部特徵圖(在這1batch是三張)裏的全部元素(19*19)裏的全部confidence,準確來講是主支的19*19個元素,每一個元素有三個大框預測,檢測對應全部框裏真實數據最好的box的iou,若是iou大於設定的ignore_thresh,則設delta爲0,不然就是0-out(0表示沒有,咱們指望輸出是0)。

  第二部分,在對照全部真實label中,先找到這個label是不是大框,若是不是,這個yolo層無論,若是是,繼續看是上面6,7,8中的那一個,咱們假設是7,根據真實框的位置肯定在特徵圖的位置(19*19中),其對應255張特徵圖中間的85-170這85張圖,而後比較真實的BOX與對應特徵圖預測的box,算出對應box的 delta,而後是confidence的delta(能夠知道,正確的box位置上的元素會算二次confidence損失),而後是類別的delta。各delta比較簡單,若是認爲是真的,delta=1-out,若是是錯的,delta=0-out,簡單來講就是指望輸出-out,最後網絡cost就是每層yolo的delta的平方和加起來的均值。

  而後是yolo訓練時輸出的各項參數(這圖用的是yolov3-tiny訓練,因此只有16和23這二個yolo層),對好比上16層檢測大的,23檢測小的。

  

  能夠看到,count是表示當前層與真實label正確配對的box數,其中全部參數都是針對這個值的平均值,除no obj外,不過從代碼上來,這個參數意義並不大,因此當前yolo層若是出現nan這個的打印,也是正常的,只是表示當前batch恰好全部圖片都是大框或是小框,因此提升batch的數目能夠下降nan出現的機率,不過相應的是,batch提升,可能顯存就暴了,我用的2070一次用默認的64張顯存就不夠,只能改爲32張。其中avg iou表示當前層正確配對的box的交併比的平均值,class表示表示當前層正確配對類別的平均機率,obj表示confidence = P(object)* IOU,表示預測box包含對象與IOU好壞的評分,0.5R/0.7R:表示iou在0.5/0.7上與正確配對的box的比率。

  搞明白darknet框架各層後,回到咱們需求,引入檢測模型,只檢測人物,而後是圖像能側立,這樣人物在裏面佔比更多,也更清晰,也不須要檢測人佔比小的狀況。先說明下,用的yolov3-tiny,由於可能要每楨檢查並不須要佔太多資源,故使用簡化模型。

  首先篩選知足條件的數據集,原本準備用coco數據自帶api分析,發現還麻煩些,數據全有了,邏輯並不複雜,用winform本身寫了就好了。

/// <summary>
/// 數據通過funcFilterLabel過濾,過濾後的數據須要所有知足discardFilterLabel
/// </summary>
/// <param name="instData"></param>
/// <param name="funcFilterLabel">知足條件就採用</param>
/// <param name="discardFilterLabel">須要全部標籤知足的條件</param>
/// <returns></returns>
public List<ImageLabel> CreateYoloLabel(instances instData, 
    Func<annotationOD, image, bool> funcFilterLabel,
    Func<annotationOD, image, bool> discardFilterLabel)
{
    List<ImageLabel> labels = new List<ImageLabel>();
    //foreach (var image in instData.images)
    Parallel.ForEach(instData.images, (image image) =>
     {
         var anns = instData.annotations.FindAll(p => p.image_id == image.id && funcFilterLabel(p, image));
         bool bReserved = anns.TrueForAll((annotationOD ao) => discardFilterLabel(ao, image));
         if (anns.Count > 0 && bReserved)
         {
             ImageLabel iml = new ImageLabel();
             iml.imageId = image.id;
             iml.name = image.file_name;
             float dw = 1.0f / image.width;
             float dh = 1.0f / image.height;
             foreach (var ann in anns)
             {
                 BoxIndex boxIndex = new BoxIndex();
                 boxIndex.box.xcenter = (ann.bbox[0] + ann.bbox[2] / 2.0f) * dw;
                 boxIndex.box.ycenter = (ann.bbox[1] + ann.bbox[3] / 2.0f) * dh;

                 boxIndex.box.width = ann.bbox[2] * dw;
                 boxIndex.box.height = ann.bbox[3] * dh;
                 //註冊
                 boxIndex.catId = findCategoryId(instData.categories, ann.category_id);
                 if (boxIndex.catId >= 0)
                     iml.boxs.Add(boxIndex);
             }
             if (iml.boxs.Count > 0)
             {
                 lock (labels)
                 {
                     labels.Add(iml);
                 }
             }
         }

     });
    return labels;
}

public async void BuildYoloData(DataPath dataPath, string txtListName)
{
    instances instance = new instances();
    if (!File.Exists(dataPath.AnnotationPath))
    {
        setText(dataPath.AnnotationPath + " 路徑不存在.");
        return;
    }
    setText("正在讀取文件中:" + Environment.NewLine + dataPath.AnnotationPath);
    var jsonTex = await Task.FromResult(File.ReadAllText(dataPath.AnnotationPath));
    setText("正在解析文件中:" + Environment.NewLine + dataPath.AnnotationPath);
    instance = await Task.FromResult(JsonConvert.DeserializeObject<instances>(jsonTex));
    setText("正在分析文件包含人物圖像:" + instance.images.Count + "");
    List<ImageLabel> labels = await Task.FromResult(COCODataManager.Instance.CreateYoloLabel(
        instance,
        (annotationOD at, image image) =>
         {
             //是否人類
             return at.category_id == 1;
         },
        (annotationOD at, image image) =>
         {
             //是否知足全部人類標籤都面積佔比都大於十分之一
             return (at.bbox[2] / image.width) * (at.bbox[3] / image.height) > 0.1f;
         }));
    setText("正在生成label文件:" + Environment.NewLine + dataPath.LabelPath);
    if (!Directory.Exists(dataPath.LabelPath))
    {
        Directory.CreateDirectory(dataPath.LabelPath);
    }
    await Task.Run(() =>
    {
        Parallel.ForEach(labels, (ImageLabel imageLabel) =>
        {
            string fileName = Path.Combine(dataPath.LabelPath,
                Path.GetFileNameWithoutExtension(imageLabel.name) + ".txt");
            using (var file = new StreamWriter(Path.Combine(dataPath.LabelPath, fileName), false))
            {
                foreach (var label in imageLabel.boxs)
                {
                    file.WriteLine(label.catId + " " + label.box.xcenter + " " + label.box.ycenter +
                        " " + label.box.width + " " + label.box.height + " ");
                }
            }
        });
        string path = Path.Combine(Directory.GetParent(dataPath.LabelPath).FullName, 
            txtListName + ".txt");
        using (var file = new StreamWriter(path, false))
        {
            foreach (var label in labels)
            {
                string lpath = Path.Combine(dataPath.DestImagePath, label.name);
                file.WriteLine(lpath);
            }
        }
    });
    setText("正在複製須要的文件到指定目錄:" + dataPath.AnnotationPath);
    await Task.Run(() =>
    {
        Parallel.ForEach(labels, (ImageLabel imageLabel) =>
        {
            string spath = Path.Combine(dataPath.SourceImagePath, imageLabel.name);
            string dpsth = Path.Combine(dataPath.DestImagePath, imageLabel.name);
            if (File.Exists(spath))
                File.Copy(spath, dpsth, true);
        });
    });
    setText("所有完成");
}
CreateYoloLabel

  只有一點須要注意,咱們只記錄人類box標籤數據,可是這些標籤須要所有大於特定面積的圖,若是你選擇的上面還有小面積人物,又不給box標籤訓練,最後yolo層並沒面板的損失函數,會形成干擾,在這原本也沒有檢查小面積人物的需求,yolov3-tiny層數本也很少,需求泛化後精度很低。

  darknet自己並無針對側立作適配,咱們須要修改相應邏輯來完成,很簡單,一張圖四個方向旋轉後,一樣修改相應的truth box就好了是,darknet數據加載主要在data.c這部分,找到咱們使用的加載邏輯load_data_detection裏,主要針對以下修改。

data load_data_detection(int n, char **paths, int m, int w, int h, int boxes, int classes,
    float jitter, float hue, float saturation, float exposure)
{
....................
        random_distort_image(sized, hue, saturation, exposure);

        int flip = rand() % 2;
        if (flip)
            flip_image(sized);
        int vflip = rand() % 2;
        if (vflip)
            vflip_image(sized);
        int trans = rand() % 2;
        if (trans)
            transpose_image(sized);
        d.X.vals[i] = sized.data;

        fill_truth_detection(random_paths[i], boxes, d.y.vals[i], classes, flip,
            -dx / w, -dy / h, nw / w, nh / h, vflip, trans);

        free_image(orig);
....................
}

void correct_boxes(box_label *boxes, int n, float dx, float dy, float sx, 
    float sy, int flip, int vflip, int trans)
{
    int i;
    for (i = 0; i < n; ++i) {
        if (boxes[i].x == 0 && boxes[i].y == 0) {
            boxes[i].x = 999999;
            boxes[i].y = 999999;
            boxes[i].w = 999999;
            boxes[i].h = 999999;
            continue;
        }
        boxes[i].left = boxes[i].left  * sx - dx;
        boxes[i].right = boxes[i].right * sx - dx;
        boxes[i].top = boxes[i].top   * sy - dy;
        boxes[i].bottom = boxes[i].bottom* sy - dy;

        if (flip)
        {
            float swap = boxes[i].left;
            boxes[i].left = 1. - boxes[i].right;
            boxes[i].right = 1. - swap;
        }
        if (vflip)
        {
            float swap = boxes[i].top;
            boxes[i].top = 1. - boxes[i].bottom;
            boxes[i].bottom = 1. - swap;
        }

        boxes[i].left = constrain(0, 1, boxes[i].left);
        boxes[i].right = constrain(0, 1, boxes[i].right);
        boxes[i].top = constrain(0, 1, boxes[i].top);
        boxes[i].bottom = constrain(0, 1, boxes[i].bottom);

        boxes[i].x = (boxes[i].left + boxes[i].right) / 2;
        boxes[i].y = (boxes[i].top + boxes[i].bottom) / 2;
        boxes[i].w = (boxes[i].right - boxes[i].left);
        boxes[i].h = (boxes[i].bottom - boxes[i].top);

        boxes[i].w = constrain(0, 1, boxes[i].w);
        boxes[i].h = constrain(0, 1, boxes[i].h);

        if (trans)
        {
            float temp = boxes[i].x;
            boxes[i].x = boxes[i].y;
            boxes[i].y = temp;
            temp = boxes[i].w;
            boxes[i].w = boxes[i].h;
            boxes[i].h = temp;
        }
    }
}
load_data_detection

  而後拿到yolov3-tiny.cfg文件,先把burn_in修改爲1,咱們沒有預精確數據,最開始就以原始學習率開始訓練,對應二個yolo層裏的classes改爲一,記的前面說過,這個層就是分析上面的卷積層,故上面的輸出filters=3*(4+1+1)=18,第一次訓練後發現五W次就沒怎麼收斂了,分析了下,應該是anchors致使的,當圖像側立後,這裏的也應該有相似數據,故選擇全大面積人物二種特定框,分別取側立,這裏正確的搞法應該是用k-means再從新取K=2算一下全部如今特定圖的框,分別對應大框與小框,而後側立下,大框就有二個,小框二個,num=4,mask每層設二個索引,或是K=4,而後讓上面來,後面抽出時間完善這步,現暫時用以下數據anchors = 100,100, 119,59, 59,119, 200,200, 326,373, 373,326,這些調整後,現訓練21W次,也還一直收斂中。

  訓練與驗證根據他自身的train_yolo/validata_yolo修改下,本身能夠打印出本身想要的信息,驗證能夠結合opencv顯示咱們想要的各類圖形比對效果。

  

相關文章
相關標籤/搜索