YOLOv2通過k-means來獲取anchor boxes
K-means原理
K-means演算法是很經典的基於距離的聚類演算法,採用距離作為相似性的評價指標,即認為兩個物件的距離越近,其相似度越高。該演算法認為簇是由距離靠近的物件組成的,因此把得到緊湊而獨立的簇作為最終目標。
K-means主要解決問題
K-means演算法主要解決的問題如下圖所示。在圖的左邊有一些點,我們用肉眼可以看出來有四個點群,K-means演算法被用來找出這幾個點群。
演算法介紹
從上圖,我們可以看出已確定的5個點(A,B,C,D,E),而灰色的點代表著我們的種子點,也就是我們用來找叢集的點。有2個種子點,所以k=2.那麼通過k-means如何找到這2個點群,過程如下所示。
(1)首先計算這5個已知點離我們2個種子點的距離,比較距離的大小,已知點屬於距離最近的種子點(加入A點離1號種子點距離為r1,離2號種子點的距離為r2,r1<r2,則我們可以判斷出,A點屬於1號叢集)。(2)接著,我們將種子點移到屬於它的“點群”中心。(3)然後再重新計算已知點到種子點的距離,重複第1步,直到種子點沒有移動,即表示種子點已經將已知點聚合完成。
K-means缺點
(1)需要指定出k的個數
(2)K-means演算法對初始種子點的選擇比較敏感(選擇種子點比較好,則聚類較快,反之則比較慢)
如何選擇合適的初始種子點
在對於選擇合適的初始種子點,我們使用了K-means++的演算法,該演算法的思想是:選擇相互距離較遠的點作為種子點,方法如下所示。
(1)從輸入的資料點集合中隨機選擇一個點作為第一個聚類中心;(2)對於資料集中的每一個點x,計算它與最近聚類中心的D(x)(剛開始只需要計算所有點與第一個聚類中心的距離,後面依次疊加);(3)選擇一個新的資料點作為新的聚類中心,選擇的原則:D(x)較大的點,被選擇作為聚類中心的概率較大;(4)重複2與3,直到k個聚類中心被選出來。(5)然後,利用這k個初始的聚類中心來執行標準的K-means演算法;
第2、3步選擇新點的方法如下:
1.對於每個點,我們都計算其和最近的一個種子點的距離儲存在一個數組中,然後把這些距離加起來得到Sum(D(x))。
2.然後,再取一個隨機值,用權重的方式來去計算下一個“種子點”。這個演算法的實現:先用Sum(D(x))乘以隨機值Random得到值r,然後用currSum+=D(x),直到其currSum>r,此時的點就是下一個"種子點"。原因見下圖:
假設A,B,C,D的D(x)如上圖所示,當演算法取值Sum(D(x))*random時,該值會以較大的概率落入D(x)較大的區間內,所以對應的點會以較大的概率被選中作為新的聚類中心。
上述講解了原本的K-means演算法,現在講述用類似的方法求取anchor boxes的值
K-means計算Anchor boxes
根據YOLOv2的論文,YOLOv2使用anchor boxes來預測bounding boxes的座標。YOLOv2使用的anchor boxes和Faster R-CNN不同,不是手選的先驗框,而是通過k-means得到的。 YOLO的標記檔案格式如下:
<object-class> <x> <y> <width> <height>
object-class是類的索引,後面的4個值都是相對於整張圖片的比例。 x是ROI中心的x座標,y是ROI中心的y座標,width是ROI的寬,height是ROI的高。
卷積神經網路具有平移不變性,且anchor boxes的位置被每個柵格固定,因此我們只需要通過k-means計算出anchor boxes的width和height即可,即object-class,x,y三個值我們不需要。
由於從標記檔案的width,height計算出的anchor boxes的width和height都是相對於整張圖片的比例,而YOLOv2通過anchor boxes直接預測bounding boxes的座標時,座標是相對於柵格邊長的比例(0到1之間),因此要將anchor boxes的width和height也轉換為相對於柵格邊長的比例。轉換公式如下:
w=anchor_width*input_width/downsamples
h=anchor_height*input_height/downsamples
例如;卷積神經網路的輸入為416*416時,YOLOv2網路的降取樣倍率為32,例如K-means計算得到的一個anchor box的anchor_width=0.2,anchor_height=0.6,則
w=0.2*416/32=0.2*13=2.6
h=0.6*416/32=0.6*13=7.8
距離公式
因為使用歐式距離會讓大的bounding boxes比小的bounding boxes產生更多的error,而我們希望能夠通過anchor boxes獲得更好的IOU score,並且IOU scores是與box的尺寸無關的。為此,作者定義了新的計算公式
d(box,centroid)=1-IOU(box,centroid)
在計算anchor boxes時我們將所有boxes中心點的x,y座標都置為0,這樣所有的boxes都處在相同的位置上,方便我們通過新距離公式計算boxes之間的相似度。
程式碼實現
# coding=utf-8
# k-means ++ for YOLOv2 anchors
# 通過k-means ++ 演算法獲取YOLOv2需要的anchors的尺寸
import numpy as np
# 定義Box類,描述bounding box的座標
class Box():
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
# 計算兩個box在某個軸上的重疊部分
# x1是box1的中心在該軸上的座標
# len1是box1在該軸上的長度
# x2是box2的中心在該軸上的座標
# len2是box2在該軸上的長度
# 返回值是該軸上重疊的長度
def overlap(x1, len1, x2, len2):
len1_half = len1 / 2
len2_half = len2 / 2
left = max(x1 - len1_half, x2 - len2_half)
right = min(x1 + len1_half, x2 + len2_half)
return right - left
# 計算box a 和box b 的交集面積
# a和b都是Box型別例項
# 返回值area是box a 和box b 的交集面積
def box_intersection(a, b):
w = overlap(a.x, a.w, b.x, b.w)
h = overlap(a.y, a.h, b.y, b.h)
if w < 0 or h < 0:
return 0
area = w * h
return area
# 計算 box a 和 box b 的並集面積
# a和b都是Box型別例項
# 返回值u是box a 和box b 的並集面積
def box_union(a, b):
i = box_intersection(a, b)
u = a.w * a.h + b.w * b.h - i
return u
# 計算 box a 和 box b 的 iou
# a和b都是Box型別例項
# 返回值是box a 和box b 的iou
def box_iou(a, b):
return box_intersection(a, b) / box_union(a, b)
# 使用k-means ++ 初始化 centroids,減少隨機初始化的centroids對最終結果的影響
# boxes是所有bounding boxes的Box物件列表
# n_anchors是k-means的k值
# 返回值centroids 是初始化的n_anchors個centroid
def init_centroids(boxes,n_anchors):
centroids = []
boxes_num = len(boxes)
centroid_index = np.random.choice(boxes_num, 1)
centroids.append(boxes[centroid_index])
print(centroids[0].w,centroids[0].h)
for centroid_index in range(0,n_anchors-1):
sum_distance = 0
distance_thresh = 0
distance_list = []
cur_sum = 0
for box in boxes:
min_distance = 1
for centroid_i, centroid in enumerate(centroids):
distance = (1 - box_iou(box, centroid))
if distance < min_distance:
min_distance = distance
sum_distance += min_distance
distance_list.append(min_distance)
distance_thresh = sum_distance*np.random.random()
for i in range(0,boxes_num):
cur_sum += distance_list[i]
if cur_sum > distance_thresh:
centroids.append(boxes[i])
print(boxes[i].w, boxes[i].h)
break
return centroids
# 進行 k-means 計算新的centroids
# boxes是所有bounding boxes的Box物件列表
# n_anchors是k-means的k值
# centroids是所有簇的中心
# 返回值new_centroids 是計算出的新簇中心
# 返回值groups是n_anchors個簇包含的boxes的列表
# 返回值loss是所有box距離所屬的最近的centroid的距離的和
def do_kmeans(n_anchors, boxes, centroids):
loss = 0
groups = []
new_centroids = []
for i in range(n_anchors):
groups.append([])
new_centroids.append(Box(0, 0, 0, 0))
for box in boxes:
min_distance = 1
group_index = 0
for centroid_index, centroid in enumerate(centroids):
distance = (1 - box_iou(box, centroid))
if distance < min_distance:
min_distance = distance
group_index = centroid_index
groups[group_index].append(box)
loss += min_distance
new_centroids[group_index].w += box.w
new_centroids[group_index].h += box.h
for i in range(n_anchors):
new_centroids[i].w /= len(groups[i])
new_centroids[i].h /= len(groups[i])
return new_centroids, groups, loss
# 計算給定bounding boxes的n_anchors數量的centroids
# label_path是訓練集列表檔案地址
# n_anchors 是anchors的數量
# loss_convergence是允許的loss的最小變化值
# grid_size * grid_size 是柵格數量
# iterations_num是最大迭代次數
# plus = 1時啟用k means ++ 初始化centroids
def compute_centroids(label_path,n_anchors,loss_convergence,grid_size,iterations_num,plus):
boxes = []
label_files = []
f = open(label_path)
for line in f:
label_path = line.rstrip().replace('images', 'labels')
label_path = label_path.replace('JPEGImages', 'labels')
label_path = label_path.replace('.jpg', '.txt')
label_path = label_path.replace('.JPEG', '.txt')
label_files.append(label_path)
f.close()
for label_file in label_files:
f = open(label_file)
for line in f:
temp = line.strip().split(" ")
if len(temp) > 1:
boxes.append(Box(0, 0, float(temp[3]), float(temp[4])))
if plus:
centroids = init_centroids(boxes, n_anchors)
else:
centroid_indices = np.random.choice(len(boxes), n_anchors)
centroids = []
for centroid_index in centroid_indices:
centroids.append(boxes[centroid_index])
# iterate k-means
centroids, groups, old_loss = do_kmeans(n_anchors, boxes, centroids)
iterations = 1
while (True):
centroids, groups, loss = do_kmeans(n_anchors, boxes, centroids)
iterations = iterations + 1
print("loss = %f" % loss)
if abs(old_loss - loss) < loss_convergence or iterations > iterations_num:
break
old_loss = loss
for centroid in centroids:
print(centroid.w * grid_size, centroid.h * grid_size)
# print result
for centroid in centroids:
print("k-means result:\n")
print(centroid.w * grid_size, centroid.h * grid_size)
label_path = "/raid/pengchong_data/Data/Lists/paul_train.txt"
n_anchors = 5
loss_convergence = 1e-6
grid_size = 13
iterations_num = 100
plus = 0
compute_centroids(label_path,n_anchors,loss_convergence,grid_size,iterations_num,plus)
通過K-means計算anchor boxes