摘要
- 提出YOLO v2 :代表着目前業界最先進物體檢測的水平,它的速度要快過其他檢測系統(FasterR-CNN,ResNet,SSD),使用者可以在它的速度與精確度之間進行權衡。
- 提出了一種新的聯合訓練算法( Joint Training Algorithm ),使用這種聯合訓練技術同時在ImageNet和COCO數據集上進行訓練。YOLO9000進一步縮小了監測數據集與識別數據集之間的代溝。這種算法可以把這兩種的數據集混合到一起。使用一種分層的觀點對物體進行分類,用巨量的分類數據集數據來擴充檢測數據集,從而把兩種不同的數據集混合起來。 聯合訓練算法的基本思路就是:同時在檢測數據集(COCO)和分類數據集(ImageNet)上訓練物體檢測器(Object Detectors),用檢測數據集的數據學習物體的準確位置,用分類數據集的數據來增加分類的類別量、提升健壯性。
- 提出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的特徵空間圖。