天天看點

【darknet源碼閱讀】——Yolo_layer

話不多說,先上核心【前向傳播】源碼 —— talk is cheap, show me the code. 

void forward_yolo_layer(const layer l, network net)
{
    int i,j,b,t,n;
    memcpy(l.output, net.input, l.outputs*l.batch*sizeof(float));

#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);
            activate_array(l.output + index, 2*l.w*l.h, LOGISTIC);
            index = entry_index(l, b, n*l.w*l.h, 4);
            activate_array(l.output + index, (1+l.classes)*l.w*l.h, LOGISTIC);
        }
    }
#endif

    memset(l.delta, 0, l.outputs * l.batch * sizeof(float));
    if(!net.train) return;
    float avg_iou = 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;
    for (b = 0; b < l.batch; ++b) {
        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);
                    box pred = get_yolo_box(l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, net.w, net.h, l.w*l.h);
                    float best_iou = 0;
                    int best_t = 0;
                    for(t = 0; t < l.max_boxes; ++t){
                        box truth = float_to_box(net.truth + t*(4 + 1) + b*l.truths, 1);
                        if(!truth.x) break;
                        float iou = box_iou(pred, truth);
                        if (iou > best_iou) {
                            best_iou = iou;
                            best_t = t;
                        }
                    }
                    int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4);
                    avg_anyobj += l.output[obj_index];
                    l.delta[obj_index] = 0 - l.output[obj_index];
                    if (best_iou > l.ignore_thresh) {
                        l.delta[obj_index] = 0;
                    }
                    if (best_iou > l.truth_thresh) {
                        l.delta[obj_index] = 1 - l.output[obj_index];

                        int class = net.truth[best_t*(4 + 1) + b*l.truths + 4];
                        if (l.map) class = l.map[class];
                        int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1);
                        delta_yolo_class(l.output, l.delta, class_index, class, l.classes, l.w*l.h, 0);
                        box truth = float_to_box(net.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, net.w, net.h, l.delta, (2-truth.w*truth.h), l.w*l.h);
                    }
                }
            }
        }
        for(t = 0; t < l.max_boxes; ++t){
            box truth = float_to_box(net.truth + t*(4 + 1) + b*l.truths, 1);

            if(!truth.x) break;
            float best_iou = 0;
            int best_n = 0;
            i = (truth.x * l.w);
            j = (truth.y * l.h);
            box truth_shift = truth;
            truth_shift.x = truth_shift.y = 0;
            for(n = 0; n < l.total; ++n){
                box pred = {0};
                pred.w = l.biases[2*n]/net.w;
                pred.h = l.biases[2*n+1]/net.h;
                float iou = box_iou(pred, truth_shift);
                if (iou > best_iou){
                    best_iou = iou;
                    best_n = n;
                }
            }

            int mask_n = int_index(l.mask, best_n, l.n);
            if(mask_n >= 0){
                int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0);
                float iou = delta_yolo_box(truth, l.output, l.biases, best_n, box_index, i, j, l.w, l.h, net.w, net.h, l.delta, (2-truth.w*truth.h), l.w*l.h);

                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] = 1 - l.output[obj_index];

                int class = net.truth[t*(4 + 1) + b*l.truths + 4];
                if (l.map) class = l.map[class];
                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, l.classes, l.w*l.h, &avg_cat);

                ++count;
                ++class_count;
                if(iou > .5) recall += 1;
                if(iou > .75) recall75 += 1;
                avg_iou += iou;
            }
        }
    }
    *(l.cost) = pow(mag_array(l.delta, l.outputs * l.batch), 2);
    printf("Region %d Avg IOU: %f, Class: %f, Obj: %f, No Obj: %f, .5R: %f, .75R: %f,  count: %d\n", net.index, avg_iou/count, avg_cat/class_count, avg_obj/count, avg_anyobj/(l.w*l.h*l.n*l.batch), recall/count, recall75/count, count);
}
           

分解如下(按行分解,不去管括号與文法問題)

第一部分,l.output輸出參數做logistic函數處理

#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);
            activate_array(l.output + index, 2*l.w*l.h, LOGISTIC);
            index = entry_index(l, b, n*l.w*l.h, 4);
            activate_array(l.output + index, (1+l.classes)*l.w*l.h, LOGISTIC);
        }
    }
#endif
           

在這裡用到了entry_index函數,定位l.output的對應位置

static int entry_index(layer l, int batch, int location, int entry)
{
    int n =   location / (l.w*l.h);
    int loc = location % (l.w*l.h);
    return batch*l.outputs + n*l.w*l.h*(4+l.classes+1) + entry*l.w*l.h + loc; //這裡說明了l.output的資料各次元排列順序。
}
           

從entry_index函數中我們看到,l.output的排列順序為batch*n*(4+1+l.classes)*w*h。

于是entry指代了(4+1+l.classes)中的第幾個通道。

故分别進行logistic函數處理的數為batch*n*2*w*h和batch*n*(l.classes+1)*w*h。

與論文yolo-v3對比,可知,第一部分處理後為tx和ty,第二部分處理後為類的機率值與置信度,沒有處理部分為tw與th。

第二部分,把l.output解析成預測的boxes

關鍵代碼

for (b = 0; b < l.batch; ++b) {
        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);
                    box pred = get_yolo_box(l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, net.w, net.h, l.w*l.h);
           

box_index指向位置為i,j的長度為(4+1+l.classes)的盒向量的首部。

看get_yolo_box函數

box get_yolo_box(float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, int stride)
{
    box b;
    b.x = (i + x[index + 0*stride]) / lw; //将坐标限定在0-1
    b.y = (j + x[index + 1*stride]) / lh;
    b.w = exp(x[index + 2*stride]) * biases[2*n]   / w; //biases為anchor box參數
    b.h = exp(x[index + 3*stride]) * biases[2*n+1] / h;
    return b;
}
           

由于l.output的排列順序為batch*n*(4+l.classes+1)*w*h,故需要stride=w*h來切換盒向量位置,盒向量前四個分别為x,y,w,h。

因為預測的w和h是與先驗框anchor進行對比,需要anchor資料,這裡為biases,配置輸入的anchor box參數,在yolov3-voc.cfg中有如下參數配置:

anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326

兩兩一對表示anchor寬與長。

第三部分,計算真實框與預測框的iou,并尋找與每個預測框最比對的真實框

for(t = 0; t < l.max_boxes; ++t){
                        box truth = float_to_box(net.truth + t*(4 + 1) + b*l.truths, 1);
                        if(!truth.x) break;
                        float iou = box_iou(pred, truth);
                        if (iou > best_iou) {
                            best_iou = iou;
                            best_t = t;
                        }
           

從box truth的擷取過程可以看出,net.truth的資料結構為batch*90*(4+1),即每個圖檔的真實框用90個boxes來表示,填不滿的資料用空boxes來寫。然後,對于每個預測框,找出與它iou最大的真實框。

第四部分,計算置信度的損失

int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4);
                    avg_anyobj += l.output[obj_index];
                    l.delta[obj_index] = 0 - l.output[obj_index];
                    if (best_iou > l.ignore_thresh) {
                        l.delta[obj_index] = 0;
                    }
                    if (best_iou > l.truth_thresh) { //這個條件永遠無法滿足,在yolov3-voc.cfg的配置中,truth_thresh=1
                        l.delta[obj_index] = 1 - l.output[obj_index];

                        int class = net.truth[best_t*(4 + 1) + b*l.truths + 4];
                        if (l.map) class = l.map[class];
                        int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1);
                        delta_yolo_class(l.output, l.delta, class_index, class, l.classes, l.w*l.h, 0);
                        box truth = float_to_box(net.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, net.w, net.h, l.delta, (2-truth.w*truth.h), l.w*l.h);
                    }
           

這裡主要關注l.delta[obj_index]與l.output[obj_index],l.output[obj_index]是網絡輸出的置信度,而l.delta[obj_index]計算的是置信度損失,儲存的是它的梯度,置信度損失計算分為兩步,先假設沒有真實框,于是l.delta[obj_index] = 0 - l.output[obj_index],在後面階段對于真實框位置的預測框再進行處理,在best_iou<ignore_thresh時,計算損失,若best_iou>ignore_thresh,指派0,作為後面程式的判定條件,後面會用到,而它的置信度損失後面會計算。

在論文中置信度損失也是由兩部分組成的,一個是no-obj的C,一個是obj的C,這裡是no-obj的C。

第五部分,計算真實框與anchor框的iou,尋找與真實框最比對的anchor框

for(t = 0; t < l.max_boxes; ++t){
            box truth = float_to_box(net.truth + t*(4 + 1) + b*l.truths, 1);

            if(!truth.x) break;
            float best_iou = 0;
            int best_n = 0;
            i = (truth.x * l.w);
            j = (truth.y * l.h);
            box truth_shift = truth;
            truth_shift.x = truth_shift.y = 0;
            for(n = 0; n < l.total; ++n){
                box pred = {0};
                pred.w = l.biases[2*n]/net.w;
                pred.h = l.biases[2*n+1]/net.h;
                float iou = box_iou(pred, truth_shift);
                if (iou > best_iou){
                    best_iou = iou;
                    best_n = n;
                }
            }
           

對于每個真實框,尋找與它最比對的anchor框,比對的方式為:将真實框與anchor框對齊,計算iou,最大那個為最比對。

其中,l.biases儲存的是anchor框的長寬資料,這些資料由k-mean對長寬聚類得到,在訓練時作為先驗值。

第六部分,計算預測框與真實框的損失

int mask_n = int_index(l.mask, best_n, l.n);
            if(mask_n >= 0){
                int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0);
                float iou = delta_yolo_box(truth, l.output, l.biases, best_n, box_index, i, j, l.w, l.h, net.w, net.h, l.delta, (2-truth.w*truth.h), l.w*l.h);
           

yolov3在三層分别輸出不同anchor的比對框,每層比對三個anchor,此時mask_n即為目前層篩選後的anchor框,

float delta_yolo_box(box truth, float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, float *delta, float scale, int stride)
{
    box pred = get_yolo_box(x, biases, n, index, i, j, lw, lh, w, h, stride);
    float iou = box_iou(pred, truth);

    float tx = (truth.x*lw - i);
    float ty = (truth.y*lh - j);
    float tw = log(truth.w*w / biases[2*n]);
    float th = log(truth.h*h / biases[2*n + 1]);

    delta[index + 0*stride] = scale * (tx - x[index + 0*stride]);
    delta[index + 1*stride] = scale * (ty - x[index + 1*stride]);
    delta[index + 2*stride] = scale * (tw - x[index + 2*stride]);
    delta[index + 3*stride] = scale * (th - x[index + 3*stride]);
    return iou;
           

主要來看一下delta_yolo_box函數,因為預測值是在格點中的相對位置,而真實值是在整個圖檔中的相對位置故有轉換關系

x(全圖)=x(格點)/l.w

再看一下坐标損失

【darknet源碼閱讀】——Yolo_layer

求導之後有(PS:求導符号打不出來):

【darknet源碼閱讀】——Yolo_layer

w和h則正常求導直接計算就可以

在實作的過程中,損失前面的系數實際是自己調節的,忽略了1/(l.w)^2,而是乘了一個因子scale=2-w*h,為了平衡大邊框與小邊框的損失比例,因為在計算邊框損失的時候,很明顯大框數值會高于小框。

第七部分,計算分類損失

i = (truth.x * l.w);
j = (truth.y * l.h);
           

i,j為整型,而truth.x為浮點數,這種強制轉換也即取整,即i,j表示的是真實框的格點位置,根據中心點确定真實框處于哪個格點

l.delta[obj_index] = 1 - l.output[obj_index];

int class = net.truth[t*(4 + 1) + b*l.truths + 4];
                if (l.map) class = l.map[class];
                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, l.classes, l.w*l.h, &avg_cat);
           

在前面是對于每個真實框,尋找出與它最比對的anchor,再通過cfg裡面的mask設定,來篩選這層要使用的anchor。

最後再通過真實框的标簽類,定位它所處的類位置。

于是對于每一個真實框,通過比對的那個anchor來定位anchor層位置,通過中心點取整來定位它的格點位置,通過标簽定位類位置。那麼一張圖檔的真實框的對應的通過網絡得到的預測框位置也就确定了,因為預測值的資料結構為batch*anchor*(4+1+classes)*(w*h),任意一個次元都确定了。

第一行是第二部分置信度損失,是對于比對到的預測框所在置信度損失的計算,因為在前面計算預測框時,隻計算了best_iou<ignore_thresh部分,而這部分為obj部分的C。

看delta_yolo_class函數

void delta_yolo_class(float *output, float *delta, int index, int class, int classes, int stride, float *avg_cat)
{
    int n;
    if (delta[index]){
        delta[index + stride*class] = 1 - output[index + stride*class];
        if(avg_cat) *avg_cat += output[index + stride*class];
        return;
    }
    for(n = 0; n < classes; ++n){
        delta[index + stride*n] = ((n == class)?1 : 0) - output[index + stride*n];
        if(n == class && avg_cat) *avg_cat += output[index + stride*n];
    }
}
           

對于每一個真實框所對應的anchor與位置,計算classes所對應的預測數值的損失。

【darknet源碼閱讀】——Yolo_layer

上面提到的best_iou>ignore_thresh指派為0,作為這裡的條件,如果不為0,即iou是小于ignore_thresh的,那麼類損失隻計算真實類的損失,而不計算其他類,而如果iou大于ignore_thresh,則要計算所有類的損失。這個判斷條件告訴我們,在訓練的時候,并不是根據置信度來判斷預測框有沒有比對到真實框,因為置信度本身也是預測的,而是根據預測框與真實框的iou來判斷這個預測框有沒有比對真實框,以它為條件,如果比對到了,就要計算它的分類損失,如果沒比對到,隻對真實類那個資料進行計算,而置信度,隻是一個輸出預測值,訓練時不作其他用途。

第八部分,輸出項

printf("Region %d Avg IOU: %f, Class: %f, Obj: %f, No Obj: %f, .5R: %f, .75R: %f,  count: %d\n", net.index, avg_iou/count, avg_cat/class_count, avg_obj/count, avg_anyobj/(l.w*l.h*l.n*l.batch), recall/count, recall75/count, count);
           

主要變量有:net.index, avg_iou/count, avg_cat/class_count, avg_obj/count, avg_anyobj/(l.w*l.h*l.n*l.batch), recall/count, recall75/count, count

++count;
++class_count;
if(iou > .5) recall += 1;
if(iou > .75) recall75 += 1;
avg_iou += iou;
           

結合實際跑程式時候的輸出得知,net.index為batch次數輸出,即此時跑的是第幾個batch。

count在真實值比對的anchor處于此層輸出的anchor時才++,那麼這個count表示的就不是真實框的數量,而是表示與這個層的anchor比對的真實框數量,這個數量要小于真實框,而在計算的時候,也僅僅計算了這幾個anchor所對應的層的導數。

avg_iou是iou的求和後的結果,而iou是由預測框與真實框得到,當然這裡的真實框已經經過了篩選,更詳細地說,iou是經過mask比對篩選後的真實框,與其中心位置所在的預測框作iou計算得到,于是avg_iou就代表着真實框與模型的平均重疊程度。

avg_cat是在真實框的對應類的位置的預測值的和,也即預測類的真實類的比對程度。

avg_obj與avg_anyobj的計算方式類似,avg_obj是計算與真實框比對的預測框的置信度,而avg_anyobj是周遊batch*n(anchor)*w*h數量的置信度,并相加,故它表示的是模型預測的的所有預測框的平均置信度,故最終輸出的avg_obj的值應該大于0.5,而avg_anyobj的值應該很小,一般小于1%。

最後,recall在iou>0.5時++,而recall75在iou>0.75時++,意味着,如果把iou門檻值設為0.5,如果超過門檻值,則認為它檢測出來了,反之則沒檢測出來,那麼這個變量就代表着召回率,即檢測出來的機率(檢測出的數目/真實框數目),這兩個隻是門檻值不同,含義一樣。在訓練的時候,常常可以看到recall一般接近于1,而recall75則變化不定,一般0.2-0.7之間居多。

總結

yolo_layer前向傳播的過程主要為:對于預測值,除了預測框的w和h預測值,其他的參數(x,y,置信度,類預測)都進行logistic函數處理,使得其值域處于0-1,值得注意的是類預測中,v2版本用的是softmax loss,類标簽互斥,v3版本用的是均方誤差(MSE),是把每一類都預測(是這類/不是這類),類标簽可以共存,即一個物體可以擁有多個标簽。再把預測框參數解析為x,y,w,h,此時預測框有anchor*w*h個,計算其與真實框的iou,并通過iou,為每個預測框尋找最比對的真實框,通過預測框與它最比對的真實框的iou與門檻值比較,如果小于門檻值,則判斷它不比對真實框。而後再計算與真實框最比對的anchor框,再通過mask篩選此層需要的anchor框,通過中心位置定位預測框位置,這個位置預測框則判定為比對。如果預測框比對,則計算它的全部類的類損失,如果不比對,隻計算它的對應于真實類的類損失。對于比對部分,還要計算預測框損失。而置信度損失由兩部分組成,比對部分和不比對部分,具體地說是,一部分由與預測框與與它最比對的真實框的iou小于門檻值時,指派認為真實置信度為0,計算兩者差得到,另一部分由真實框與它中心位置與它最比對的anchor框對應位置的經mask篩選後的預測框,認為真實置信度為1,計算兩者差得到,兩者也許不存在交集,但一定存在都不屬于兩者的預測框,它隻是明确地告訴模型哪些可以認為是真的,哪些是假的,其他的不管。這個過程中要注意,預測框與真實框匹不比對,用了兩種判定方式,而置信度,并沒有在其中起任何作用,它僅僅是作為一個輸出計算損失。而前向傳播的主要目的,也是為了分别計算置信度損失,預測框損失和分類損失。

碼字不易——點個關注(比心)

繼續閱讀