論文筆記:YOLO9000: Better,Faster,Stronger

摘要

 

  1. 提出YOLO v2 :代表着目前業界最先進物體檢測的水平,它的速度要快過其他檢測系統(FasterR-CNN,ResNet,SSD),使用者可以在它的速度與精確度之間進行權衡。
  2. 提出了一種新的聯合訓練算法( Joint Training Algorithm ),使用這種聯合訓練技術同時在ImageNet和COCO數據集上進行訓練。YOLO9000進一步縮小了監測數據集與識別數據集之間的代溝。這種算法可以把這兩種的數據集混合到一起。使用一種分層的觀點對物體進行分類,用巨量的分類數據集數據來擴充檢測數據集,從而把兩種不同的數據集混合起來。 聯合訓練算法的基本思路就是:同時在檢測數據集(COCO)和分類數據集(ImageNet)上訓練物體檢測器(Object Detectors),用檢測數據集的數據學習物體的準確位置,用分類數據集的數據來增加分類的類別量、提升健壯性。
  3. 提出YOLO9000 :這一網絡結構可以實時地檢測超過9000種物體分類,這歸功於它使用了WordTree,通過WordTree來混合檢測數據集與識別數據集之中的數據。 All of our code and pre-trained models are available online at http://pjreddie.com/yolo9000/. 說白了一點,就是YOLOv2是在一個混合的大的數據集合上進行訓練,然後在VOC2007數據集合上進行測試結果的。相比較之前的SSD等方法感覺有點不太公平。不過YOLO作者生擼硬調還是值得膜拜的。

BETTER

YOLO一代有很多缺點,作者希望改進的方向是:改善recall,提升定位的準確度,同時保持分類的準確度。YOLO2主要木雕是爲了簡化網絡。

 

Batch Normalization

使用按批歸一化對網絡進行優化,讓網絡提高了收斂性,同時還消除了對其他形式的正則化(regularization)的依賴。通過對YOLO的每一個卷積層增加按批歸一化, 最終使得mAP提高了2%,同時還使model正則化。 使用Batch Normalization可以從model中去掉Dropout,而不會產生過擬合。

High resolution classifier

目前業界標準的檢測方法,都要先把分類器(classifier)放在ImageNet上進行預訓練。從Alexnet開始,大多數的分類器都運行在小於256x256的圖片上。而現在YOLO從224x224增加到了448x448,這就意味着網絡需要適應新的輸入分辨率。 爲了適應新的分辨率,YOLO v2的分類網絡以448x448的分辨率先在ImageNet上進行Fine Tune,Fine Tune10個epochs,給一定的時間讓網絡調整他的濾波器(filters),好讓其能更好的運行在新分辨率上,還需要調優用於檢測的Resulting Network。最終通過使用高分辨率,mAP提升了4%。

Convolution with anchor boxes

YOLO一代包含有全連接層,從而能直接預測Bounding Boxes的座標值。 Faster R-CNN的方法只用卷積層與Region Proposal Network來預測Anchor Box的偏移值與置信度,而不是直接預測座標值。作者發現通過預測偏移量而不是座標值能夠簡化問題,讓神經網絡學習起來更容易。所以最終YOLO去掉了全連接層,使用Anchor Boxes來預測 Bounding Boxes。作者去掉了網絡中一個Pooling層,這讓卷積層的輸出能有更高的分辨率。收縮網絡讓其運行在416416而不是448448。由於圖片中的物體都傾向於出現在圖片的中心位置,特別是那種比較大的物體,所以有一個單獨位於物體中心的位置用於預測這些物體。YOLO的卷積層採用32這個值來下采樣圖片,所以通過選擇416416用作輸入尺寸最終能輸出一個1313的Feature Map。 使用Anchor Box會讓精確度稍微下降,但用了它能讓YOLO能預測出大於一千個框,同時recall達到88%,mAP達到69.2%。

Dimension clusters

之前Anchor Box的尺寸是手動選擇的,所以尺寸還有優化的餘地。 爲了優化,在訓練集(training set)Bounding Boxes上跑了一下k-means聚類,來找到一個比較好的值。 如果我們用標準的歐式距離的k-means,尺寸大的框比小框產生更多的錯誤。因爲我們的目的是提高先驗框的IOU分數,這不能依賴於Box的大小,所以距離度量的使用:

d(box, centroid) = 1 - IOU(box, centroid)

這個地方我就特別想了想如何實現,就扒OpenCV來看看。

static inline float normL2Sqr(const float* a, const float* b, int n)
{
    float s = 0.f;
    for( int i = 0; i < n; i++ )
    {
        float v = a[i] - b[i];
        s += v*v;
    }
    return s;
}

class KMeansDistanceComputer : public ParallelLoopBody
{
public:
    KMeansDistanceComputer( double *_distances,
                            int *_labels,
                            const Mat& _data,
                            const Mat& _centers )
        : distances(_distances),
          labels(_labels),
          data(_data),
          centers(_centers)
    {
    }

    void operator()( const Range& range ) const
    {
        const int begin = range.start;
        const int end = range.end;
        const int K = centers.rows;
        const int dims = centers.cols;

        for( int i = begin; i<end; ++i)
        {
            const float *sample = data.ptr<float>(i);
            int k_best = 0;
            double min_dist = DBL_MAX;

            for( int k = 0; k < K; k++ )
            {
                const float* center = centers.ptr<float>(k);
                // 調用歐拉距離計算函數,具體在上面函數中實現
                const double dist = normL2Sqr(sample, center, dims);

                if( min_dist > dist )
                {
                    min_dist = dist;
                    k_best = k;
                }
            }

            distances[i] = min_dist;
            labels[i] = k_best;
        }
    }

private:
    KMeansDistanceComputer& operator=(const KMeansDistanceComputer&); // to quiet MSVC

    double *distances;
    int *labels;
    const Mat& data;
    const Mat& centers;
};

// 
// @data –
// Data for clustering. An array of N-Dimensional points with float coordinates is needed. Examples of this array can be:

// Mat points(count, 2, CV_32F);
// Mat points(count, 1, CV_32FC2);
// Mat points(1, count, CV_32FC2);
// std::vector<cv::Point2f> points(sampleCount);
// @cluster_count – Number of clusters to split the set by.
// @K – Number of clusters to split the set by.
// @_best_labels – Input/output integer array that stores the cluster indices for every sample.
// @criteria – The algorithm termination criteria, that is, the maximum number of iterations and/or the desired accuracy. The accuracy is specified as criteria.epsilon. As soon as each of the cluster centers moves by less than criteria.epsilon on some iteration, the algorithm stops.
// @attempts – Flag to specify the number of times the algorithm is executed using different initial labellings. The algorithm returns the labels that yield the best compactness (see the last function parameter).
// @flags –
// Flag that can take the following values:

// 		KMEANS_RANDOM_CENTERS Select random initial centers in each attempt.
// 		KMEANS_PP_CENTERS Use kmeans++ center initialization by Arthur and Vassilvitskii [Arthur2007].
// 		KMEANS_USE_INITIAL_LABELS During the first (and possibly the only) attempt, use the user-supplied labels instead of computing them from the initial centers. For the second and further attempts, use the random or semi-random centers. Use one of KMEANS_*_CENTERS flag to specify the exact method.
//@_centers – Output matrix of the cluster centers, one row per each cluster center.


double cv::kmeans( InputArray _data, int K,
                   InputOutputArray _bestLabels,
                   TermCriteria criteria, int attempts,
                   int flags, OutputArray _centers )
{
    const int SPP_TRIALS = 3;
    Mat data0 = _data.getMat();
    bool isrow = data0.rows == 1;
    int N = isrow ? data0.cols : data0.rows;
    int dims = (isrow ? 1 : data0.cols)*data0.channels();
    int type = data0.depth();

    attempts = std::max(attempts, 1);
    CV_Assert( data0.dims <= 2 && type == CV_32F && K > 0 );
    CV_Assert( N >= K );

    Mat data(N, dims, CV_32F, data0.ptr(), isrow ? dims * sizeof(float) : static_cast<size_t>(data0.step));

    _bestLabels.create(N, 1, CV_32S, -1, true);

    Mat _labels, best_labels = _bestLabels.getMat();
    if( flags & CV_KMEANS_USE_INITIAL_LABELS )
    {
        CV_Assert( (best_labels.cols == 1 || best_labels.rows == 1) &&
                  best_labels.cols*best_labels.rows == N &&
                  best_labels.type() == CV_32S &&
                  best_labels.isContinuous());
        best_labels.copyTo(_labels);
    }
    else
    {
        if( !((best_labels.cols == 1 || best_labels.rows == 1) &&
             best_labels.cols*best_labels.rows == N &&
            best_labels.type() == CV_32S &&
            best_labels.isContinuous()))
            best_labels.create(N, 1, CV_32S);
        _labels.create(best_labels.size(), best_labels.type());
    }
    int* labels = _labels.ptr<int>();

    Mat centers(K, dims, type), old_centers(K, dims, type), temp(1, dims, type);
    std::vector<int> counters(K);
    std::vector<Vec2f> _box(dims);
    Vec2f* box = &_box[0];
    double best_compactness = DBL_MAX, compactness = 0;
    RNG& rng = theRNG();
    int a, iter, i, j, k;
    // 結束閾值
    if( criteria.type & TermCriteria::EPS )
        criteria.epsilon = std::max(criteria.epsilon, 0.);
    else
        criteria.epsilon = FLT_EPSILON;
    criteria.epsilon *= criteria.epsilon;

    if( criteria.type & TermCriteria::COUNT )
        criteria.maxCount = std::min(std::max(criteria.maxCount, 2), 100);
    else
        criteria.maxCount = 100;

    if( K == 1 )
    {
        attempts = 1;
        criteria.maxCount = 2;
    }
    // sample: Floating-point matrix of input samples, one row per sample.
    const float* sample = data.ptr<float>(0);
    for( j = 0; j < dims; j++ )
        box[j] = Vec2f(sample[j], sample[j]);

    for( i = 1; i < N; i++ )
    {
        sample = data.ptr<float>(i);
        for( j = 0; j < dims; j++ )
        {
            float v = sample[j];
            box[j][0] = std::min(box[j][0], v);
            box[j][1] = std::max(box[j][1], v);
        }
    }

    for( a = 0; a < attempts; a++ )
    {
        double max_center_shift = DBL_MAX;
        for( iter = 0;; )
        {
            swap(centers, old_centers);

            if( iter == 0 && (a > 0 || !(flags & KMEANS_USE_INITIAL_LABELS)) )
            {
                // 初始化中心位置
                if( flags & KMEANS_PP_CENTERS )
                    generateCentersPP(data, centers, K, rng, SPP_TRIALS);
                else
                {
                    // 隨機生成中心
                    for( k = 0; k < K; k++ )
                        generateRandomCenter(_box, centers.ptr<float>(k), rng);
                }
            }
            else
            {
                if( iter == 0 && a == 0 && (flags & KMEANS_USE_INITIAL_LABELS) )
                {
                    for( i = 0; i < N; i++ )
                        CV_Assert( (unsigned)labels[i] < (unsigned)K );
                }

                // compute centers
                centers = Scalar(0);
                for( k = 0; k < K; k++ )
                    counters[k] = 0;

                for( i = 0; i < N; i++ )
                {
                    sample = data.ptr<float>(i);
                    k = labels[i];
                    float* center = centers.ptr<float>(k);
                    j=0;
                    #if CV_ENABLE_UNROLLED
                    for(; j <= dims - 4; j += 4 )
                    {
                        float t0 = center[j] + sample[j];
                        float t1 = center[j+1] + sample[j+1];

                        center[j] = t0;
                        center[j+1] = t1;

                        t0 = center[j+2] + sample[j+2];
                        t1 = center[j+3] + sample[j+3];

                        center[j+2] = t0;
                        center[j+3] = t1;
                    }
                    #endif
                    for( ; j < dims; j++ )
                        center[j] += sample[j];
                    counters[k]++;
                }

                if( iter > 0 )
                    max_center_shift = 0;
                // 處理聚類中心個數爲0的情況
                for( k = 0; k < K; k++ )
                {
                    if( counters[k] != 0 )
                        continue;

                    // if some cluster appeared to be empty then:
                    //   1. find the biggest cluster
                    //   2. find the farthest from the center point in the biggest cluster
                    //   3. exclude the farthest point from the biggest cluster and form a new 1-point cluster.
                    int max_k = 0;
                    for( int k1 = 1; k1 < K; k1++ )
                    {
                        if( counters[max_k] < counters[k1] )
                            max_k = k1;
                    }

                    double max_dist = 0;
                    int farthest_i = -1;
                    float* new_center = centers.ptr<float>(k);
                    float* old_center = centers.ptr<float>(max_k);
                    float* _old_center = temp.ptr<float>(); // normalized
                    float scale = 1.f/counters[max_k];
                    for( j = 0; j < dims; j++ )
                        _old_center[j] = old_center[j]*scale;

                    for( i = 0; i < N; i++ )
                    {
                        if( labels[i] != max_k )
                            continue;
                        sample = data.ptr<float>(i);

                        // 距離採用傳統的歐氏距離
                        double dist = normL2Sqr(sample, _old_center, dims);

                        if( max_dist <= dist )
                        {
                            max_dist = dist;
                            farthest_i = i;
                        }
                    }

                    counters[max_k]--;
                    counters[k]++;
                    labels[farthest_i] = k;
                    sample = data.ptr<float>(farthest_i);

                    for( j = 0; j < dims; j++ )
                    {
                        old_center[j] -= sample[j];
                        new_center[j] += sample[j];
                    }
                }
                // 計算新舊中心點的偏差和
                for( k = 0; k < K; k++ )
                {
                    float* center = centers.ptr<float>(k);
                    CV_Assert( counters[k] != 0 );

                    float scale = 1.f/counters[k];
                    for( j = 0; j < dims; j++ )
                        center[j] *= scale;

                    if( iter > 0 )
                    {
                        double dist = 0;
                        const float* old_center = old_centers.ptr<float>(k);
                        for( j = 0; j < dims; j++ )
                        {
                            double t = center[j] - old_center[j];
                            dist += t*t;
                        }
                        max_center_shift = std::max(max_center_shift, dist);
                    }
                }
            }
            // 迭代次數達到一定次數,結束;中心變化小於一定閾值結束
            if( ++iter == MAX(criteria.maxCount, 2) || max_center_shift <= criteria.epsilon )
                break;

            // assign labels
            Mat dists(1, N, CV_64F);
            double* dist = dists.ptr<double>(0);
            // 這裏計算center和sample之間的距離,默認採用歐氏距離
            // 這裏也是需要修改的KMeansDistanceComputer(dist, labels, data, centers)
            // 距離的計算方法
            parallel_for_(Range(0, N),
                         KMeansDistanceComputer(dist, labels, data, centers));
            compactness = 0;
            for( i = 0; i < N; i++ )
            {
                compactness += dist[i];
            }
        }

        if( compactness < best_compactness )
        {
            best_compactness = compactness;
            if( _centers.needed() )
                centers.copyTo(_centers);
            _labels.copyTo(best_labels);
        }
    }

    return best_compactness;
}

iou函數在opencv中有computeOneToOneMatchedOverlaps的實現,當然我們也可以自己寫一個。

// 自己實現一把IOU計算
static inline float interp_over_union(const float* boxA, const float* boxB)
{
	//determine the (x, y)-coordinates of the interp rectangle
    float xA = max(boxA[0], boxB[0]);
    float yA = max(boxA[1], boxB[1]);
    float xB = min(boxA[2], boxB[2]);
    float yB = min(boxA[3], boxB[3]);

    // compute the area of interp rectangle
    float interArea = (xB - xA + 1) * (yB - yA + 1);

    // compute the area of both the prediction and ground-truth
    // rectangles
    float boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1);
    float boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1);

    // compute the interp over union by taking the interp
    // area and dividing it by the sum of prediction + ground-truth
    // areas - the interep area
    return interArea / float(boxAArea + boxBArea - interArea);
}
static inline float priorsbox_iou_dist(const float* a, const float* b, int n)
{
	return 1.0f - interp_over_union(a, b);
}

通過分析實驗結果(Figure 2),左圖:在model複雜性與high recall之間權衡之後,選擇聚類分類數K=5。右圖:是聚類的中心,大多數是高瘦的Box。 Table1是說明用K-means選擇Anchor Boxes時,當Cluster IOU選擇值爲5時,AVG IOU的值是61,這個值要比不用聚類的方法的60.9要高。選擇值爲9的時候,AVG IOU更有顯著提高。總之就是說明用聚類的方法是有效果的。

Direct location prediction

用Anchor Box的方法,會讓model變得不穩定,尤其是在最開始的幾次迭代的時候(??)。大多數不穩定因素產生自預測Box的(x,y)位置的時候。按照之前YOLO的方法,網絡不會預測偏移量,而是根據YOLO中的網格單元的位置來預測座標,這就讓Ground Truth的值介於0到1之間。(這個地方在文章中感覺公式是有問題的,根本說不通啊。check之前Faster R-CNN關於anchor box中的說明)而爲了讓網絡的結果能落在這一範圍內,網絡使用一個 Logistic Activation來對於網絡預測結果進行限制,讓結果介於0到1之間。 網絡在每一個網格單元中預測出5個Bounding Boxes,每個Bounding Boxes有五個座標值tx,ty,tw,th,t0,他們的關係見下圖(Figure3)。假設一個網格單元對於圖片左上角的偏移量是cx,cy,Bounding Boxes Prior的寬度和高度是pw,ph,那麼預測的結果見下圖右面的公式:

因爲使用了限制讓數值變得參數化,也讓網絡更容易學習、更穩定。 Dimension clusters和Direct location prediction,improves YOLO by almost 5% over the version with anchor boxes.

Fine-Grained Features

YOLO修改後的Feature Map大小爲1313,這個尺寸對檢測圖片中尺寸大物體來說足夠了,同時使用這種細粒度的特徵對定位小物體的位置可能也有好處。Faster R-CNN、SSD都使用不同尺寸的Feature Map來取得不同範圍的分辨率,而YOLO採取了不同的方法,YOLO加上了一個Passthrough Layer來取得之前的某個2626分辨率的層的特徵。這個Passthrough layer能夠把高分辨率特徵與低分辨率特徵聯繫在一起,聯繫起來的方法是把相鄰的特徵堆積在不同的Channel之中,這一方法類似與Resnet的Identity Mapping,從而把2626512變成13132048。YOLO中的檢測器位於擴展後(expanded )的Feature Map的上方,所以他能取得細粒度的特徵信息,這提升了YOLO 1%的性能。

Multi-ScaleTraining

作者希望YOLO v2能健壯的運行於不同尺寸的圖片之上,所以把這一想法用於訓練model中。 區別於之前的補全圖片的尺寸的方法,YOLO v2每迭代幾次都會改變網絡參數。每10個Batch,網絡會隨機地選擇一個新的圖片尺寸,由於使用了下采樣參數是32,所以不同的尺寸大小也選擇爲32的倍數{320,352…..608},最小320320,最大608608,網絡會自動改變尺寸,並繼續訓練的過程。 這一策略讓網絡在不同的輸入尺寸上都能達到一個很好的預測效果,同一網絡能在不同分辨率上進行檢測。當輸入圖片尺寸比較小的時候跑的比較快,輸入圖片尺寸比較大的時候精度高,所以你可以在YOLO v2的速度和精度上進行權衡。

小結

YOLO9000在原來使用手工Anchor的基礎上,使用聚類的anchor大小進行。同時使用多尺度圖片進行訓練,採用grad-cell技術最後生成13x13的特徵空間圖。

發佈了44 篇原創文章 · 獲贊 17 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章