# 9.4 錨框
目標檢測算法通常會在輸入圖像中采樣大量的區域,然后判斷這些區域中是否包含我們感興趣的目標,并調整區域邊緣從而更準確地預測目標的真實邊界框(ground-truth bounding box)。不同的模型使用的區域采樣方法可能不同。這里我們介紹其中的一種方法:它以每個像素為中心生成多個大小和寬高比(aspect ratio)不同的邊界框。這些邊界框被稱為錨框(anchor box)。我們將在后面基于錨框實踐目標檢測。
首先,導入本節需要的包或模塊。這里我們新引入了`contrib`包,并修改了NumPy的打印精度。由于`NDArray`的打印實際調用NumPy的打印函數,本節打印出的`NDArray`中的浮點數更簡潔一些。
```{.python .input n=1}
%matplotlib inline
import d2lzh as d2l
from mxnet import contrib, gluon, image, nd
import numpy as np
np.set_printoptions(2)
```
## 9.4.1 生成多個錨框
假設輸入圖像高為`$ h $`,寬為`$ w $`。我們分別以圖像的每個像素為中心生成不同形狀的錨框。設大小為`$ s\in (0,1] $`且寬高比為`$ r > 0 $`,那么錨框的寬和高將分別為`$ ws\sqrt{r} $`和`$ hs/\sqrt{r} $`。當中心位置給定時,已知寬和高的錨框是確定的。
下面我們分別設定好一組大小`$ s_1,\ldots,s_n $`和一組寬高比`$ r_1,\ldots,r_m $`。如果以每個像素為中心時使用所有的大小與寬高比的組合,輸入圖像將一共得到`$ whnm $`個錨框。雖然這些錨框可能覆蓋了所有的真實邊界框,但計算復雜度容易過高。因此,我們通常只對包含`$ s_1 $`或`$ r_1 $`的大小與寬高比的組合感興趣,即
```[tex]
(s_1, r_1), (s_1, r_2), \ldots, (s_1, r_m), (s_2, r_1), (s_3, r_1), \ldots, (s_n, r_1).
```
也就是說,以相同像素為中心的錨框的數量為`$ n+m-1 $`。對于整個輸入圖像,我們將一共生成`$ wh(n+m-1) $`個錨框。
以上生成錨框的方法已實現在`MultiBoxPrior`函數中。指定輸入、一組大小和一組寬高比,該函數將返回輸入的所有錨框。
```{.python .input n=2}
img = image.imread('../img/catdog.jpg').asnumpy()
h, w = img.shape[0:2]
print(h, w)
X = nd.random.uniform(shape=(1, 3, h, w)) # 構造輸入數據
Y = contrib.nd.MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
```
我們看到,返回錨框變量`y`的形狀為(批量大小,錨框個數,4)。將錨框變量`y`的形狀變為(圖像高,圖像寬,以相同像素為中心的錨框個數,4)后,我們就可以通過指定像素位置來獲取所有以該像素為中心的錨框了。下面的例子里我們訪問以(250,250)為中心的第一個錨框。它有4個元素,分別是錨框左上角的`$ x $` 和`$ y $`軸坐標和右下角的`$ x $`和`$ y $`軸坐標,其中`$ x $`和`$ y $`軸的坐標值分別已除以圖像的寬和高,因此值域均為0和1之間。
```{.python .input n=3}
boxes = Y.reshape((h, w, 5, 4))
boxes[250, 250, 0, :]
```
為了描繪圖像中以某個像素為中心的所有錨框,我們先定義`show_bboxes`函數以便在圖像上畫出多個邊界框。
```{.python .input n=4}
# 本函數已保存在d2lzh包中方便以后使用
def show_bboxes(axes, bboxes, labels=None, colors=None):
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj
labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = d2l.bbox_to_rect(bbox.asnumpy(), color)
axes.add_patch(rect)
if labels and len(labels) > i:
text_color = 'k' if color == 'w' else 'w'
axes.text(rect.xy[0], rect.xy[1], labels[i],
va='center', ha='center', fontsize=9, color=text_color,
bbox=dict(facecolor=color, lw=0))
```
剛剛我們看到,變量`boxes`中`$ x $`和`$ y $`軸的坐標值分別已除以圖像的寬和高。在繪圖時,我們需要恢復錨框的原始坐標值,并因此定義了變量`bbox_scale`。現在,我們可以畫出圖像中以(250, 250)為中心的所有錨框了。可以看到,大小為0.75且寬高比為1的錨框較好地覆蓋了圖像中的狗。
```{.python .input n=5}
d2l.set_figsize()
bbox_scale = nd.array((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
's=0.75, r=0.5'])
```
## 9.4.2 交并比
我們剛剛提到某個錨框較好地覆蓋了圖像中的狗。如果該目標的真實邊界框已知,這里的“較好”該如何量化呢?一種直觀的方法是衡量錨框和真實邊界框之間的相似度。我們知道,Jaccard系數(Jaccard index)可以衡量兩個集合的相似度。給定集合`$ \mathcal{A} $`和`$ \mathcal{B} $`,它們的Jaccard系數即二者交集大小除以二者并集大小:
```[tex]
J(\mathcal{A},\mathcal{B}) = \frac{\left|\mathcal{A} \cap \mathcal{B}\right|}{\left| \mathcal{A} \cup \mathcal{B}\right|}.
```
實際上,我們可以把邊界框內的像素區域看成是像素的集合。如此一來,我們可以用兩個邊界框的像素集合的Jaccard系數衡量這兩個邊界框的相似度。當衡量兩個邊界框的相似度時,我們通常將Jaccard系數稱為交并比(Intersection over Union,IoU),即兩個邊界框相交面積與相并面積之比,如圖9.2所示。交并比的取值范圍在0和1之間:0表示兩個邊界框無重合像素,1表示兩個邊界框相等。

在本節的剩余部分,我們將使用交并比來衡量錨框與真實邊界框以及錨框與錨框之間的相似度。
## 9.4.3 標注訓練集的錨框
在訓練集中,我們將每個錨框視為一個訓練樣本。為了訓練目標檢測模型,我們需要為每個錨框標注兩類標簽:一是錨框所含目標的類別,簡稱類別;二是真實邊界框相對錨框的偏移量,簡稱偏移量(offset)。在目標檢測時,我們首先生成多個錨框,然后為每個錨框預測類別以及偏移量,接著根據預測的偏移量調整錨框位置從而得到預測邊界框,最后篩選需要輸出的預測邊界框。
我們知道,在目標檢測的訓練集中,每個圖像已標注了真實邊界框的位置以及所含目標的類別。在生成錨框之后,我們主要依據與錨框相似的真實邊界框的位置和類別信息為錨框標注。那么,該如何為錨框分配與其相似的真實邊界框呢?
假設圖像中錨框分別為`$ A_1, A_2, \ldots, A_{n_a} $`,真實邊界框分別為`$ B_1, B_2, \ldots, B_{n_b} $`,且`$ n_a \geq n_b $`。定義矩陣`$ \boldsymbol{X} \in \mathbb{R}^{n_a \times n_b} $`,其中第`$ i $ `行第`$ j $`列的元素`$ x_{ij} $`為錨框`$ A_i $`與真實邊界框`$ B_j $`的交并比。
首先,我們找出矩陣`$ \boldsymbol{X} $` 中最大元素,并將該元素的行索引與列索引分別記為`$ i_1,j_1 $` 。我們為錨框`$ A_{i_1} $`分配真實邊界框`$ B_{j_1} $`。顯然,錨框`$ A_{i_1} $`和真實邊界框`$ B_{j_1} $`在所有的“錨框—真實邊界框”的配對中相似度最高。接下來,將矩陣`$ \boldsymbol{X} $`中第`$ i _ 1 $` 行和第`$ j_1 $`列上的所有元素丟棄。找出矩陣`$ \boldsymbol{X} $`中剩余的最大元素,并將該元素的行索引與列索引分別記為`$ i_2,j_2 $`。我們為錨框`$ A_{i_2} $` 分配真實邊界框 `$ B_{j_2} $`,再將矩陣`$ \boldsymbol{X} $`中第`$ i_2 $`行和第`$ j_2 $`列上的所有元素丟棄。此時矩陣`$ \boldsymbol{X} $`中已有兩行兩列的元素被丟棄。
依此類推,直到矩陣`$ \boldsymbol{X} $`中所有`$ n_b $`列元素全部被丟棄。這個時候,我們已為`$ n_b $`個錨框各分配了一個真實邊界框。
接下來,我們只遍歷剩余的`$ n_a - n_b $`個錨框:給定其中的錨框`$ A_i $`,根據矩陣`$ \boldsymbol{X} $` 的第`$ i $` 行找到與`$ A_i $`交并比最大的真實邊界框`$ B_j $`,且只有當該交并比大于預先設定的閾值時,才為錨框`$ A_i $`分配真實邊界框`$ B_j $`。
如圖9.3(左)所示,假設矩陣`$ \boldsymbol{X} $` 中最大值為`$ x_{23} $`,我們將為錨框`$ A_2 $`分配真實邊界框`$ B_3 $`。然后,丟棄矩陣中第2行和第3列的所有元素,找出剩余陰影部分的最大元素`$ x_{71} $`,為錨框`$ A_7 $`分配真實邊界框`$ B_1 $`。接著如圖9.3(中)所示,丟棄矩陣中第7行和第1列的所有元素,找出剩余陰影部分的最大元素`$ x_{54} $`,為錨框`$ A_5 $`分配真實邊界框`$ B_4 $`。最后如圖9.3(右)所示,丟棄矩陣中第5行和第4列的所有元素,找出剩余陰影部分的最大元素`$ x_{92} $`,為錨框`$ A_9 $`分配真實邊界框`$ B_2 $`。之后,我們只需遍歷除去`$ A_2, A_5, A_7, A_9 $`的剩余錨框,并根據閾值判斷是否為剩余錨框分配真實邊界框。

現在我們可以標注錨框的類別和偏移量了。如果一個錨框`$ A $`被分配了真實邊界框`$ B $`,將錨框`$ A $`的類別設為`$ B $`的類別,并根據`$ B $`和`$ A $`的中心坐標的相對位置以及兩個框的相對大小為錨框`$ A $`標注偏移量。由于數據集中各個框的位置和大小各異,因此這些相對位置和相對大小通常需要一些特殊變換,才能使偏移量的分布更均勻從而更容易擬合。設錨框`$ A $`及其被分配的真實邊界框`$ B $`的中心坐標分別為`$ (x_a, y_a) $`和`$ (x_b, y_b) $` ,`$ A $`和`$ B $`的寬分別為`$ w_a $` 和`$ w_b $`,高分別為`$ h_a $`和`$ h_b $`,一個常用的技巧是將$A$的偏移量標注為
```[tex]
\left( \frac{ \frac{x_b - x_a}{w_a} - \mu_x }{\sigma_x},
\frac{ \frac{y_b - y_a}{h_a} - \mu_y }{\sigma_y},
\frac{ \log \frac{w_b}{w_a} - \mu_w }{\sigma_w},
\frac{ \log \frac{h_b}{h_a} - \mu_h }{\sigma_h}\right),
```
其中常數的默認值為`$ \mu_x = \mu_y = \mu_w = \mu_h = 0, \sigma_x=\sigma_y=0.1, \sigma_w=\sigma_h=0.2 $`。如果一個錨框沒有被分配真實邊界框,我們只需將該錨框的類別設為背景。類別為背景的錨框通常被稱為負類錨框,其余則被稱為正類錨框。
下面演示一個具體的例子。我們為讀取的圖像中的貓和狗定義真實邊界框,其中第一個元素為類別(0為狗,1為貓),剩余4個元素分別為左上角的`$ x $`和`$ y $`軸坐標以及右下角的`$ x $`和`$ y $`軸坐標(值域在0到1之間)。這里通過左上角和右下角的坐標構造了5個需要標注的錨框,分別記為`$ A_0, \ldots, A_4 $`(程序中索引從0開始)。先畫出這些錨框與真實邊界框在圖像中的位置。
```{.python .input n=6}
ground_truth = nd.array([[0, 0.1, 0.08, 0.52, 0.92],
[1, 0.55, 0.2, 0.9, 0.88]])
anchors = nd.array([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
[0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
[0.57, 0.3, 0.92, 0.9]])
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);
```
我們可以通過`contrib.nd`模塊中的`MultiBoxTarget`函數來為錨框標注類別和偏移量。該函數將背景類別設為0,并令從零開始的目標類別的整數索引自加1(1為狗,2為貓)。我們通過`expand_dims`函數為錨框和真實邊界框添加樣本維,并構造形狀為(批量大小, 包括背景的類別個數, 錨框數)的任意預測結果。
```{.python .input n=7}
labels = contrib.nd.MultiBoxTarget(anchors.expand_dims(axis=0),
ground_truth.expand_dims(axis=0),
nd.zeros((1, 3, 5)))
```
返回的結果里有3項,均為`NDArray`。第三項表示為錨框標注的類別。
```{.python .input n=8}
labels[2]
```
我們根據錨框與真實邊界框在圖像中的位置來分析這些標注的類別。首先,在所有的“錨框—真實邊界框”的配對中,錨框`$ A_4 $`與貓的真實邊界框的交并比最大,因此錨框`$ A_4 $`的類別標注為貓。不考慮錨框`$ A_4 $`或貓的真實邊界框,在剩余的“錨框—真實邊界框”的配對中,最大交并比的配對為錨框`$ A_1 $`和狗的真實邊界框,因此錨框`$ A_1 $`的類別標注為狗。接下來遍歷未標注的剩余3個錨框:與錨框`$ A_0 $`交并比最大的真實邊界框的類別為狗,但交并比小于閾值(默認為0.5),因此類別標注為背景;與錨框`$ A_2 $`交并比最大的真實邊界框的類別為貓,且交并比大于閾值,因此類別標注為貓;與錨框`$ A_3 $`交并比最大的真實邊界框的類別為貓,但交并比小于閾值,因此類別標注為背景。
返回值的第二項為掩碼(mask)變量,形狀為(批量大小, 錨框個數的四倍)。掩碼變量中的元素與每個錨框的4個偏移量一一對應。
由于我們不關心對背景的檢測,有關負類的偏移量不應影響目標函數。通過按元素乘法,掩碼變量中的0可以在計算目標函數之前過濾掉負類的偏移量。
```{.python .input n=9}
labels[1]
```
返回的第一項是為每個錨框標注的四個偏移量,其中負類錨框的偏移量標注為0。
```{.python .input n=10}
labels[0]
```
## 9.4.4 輸出預測邊界框
在模型預測階段,我們先為圖像生成多個錨框,并為這些錨框一一預測類別和偏移量。隨后,我們根據錨框及其預測偏移量得到預測邊界框。當錨框數量較多時,同一個目標上可能會輸出較多相似的預測邊界框。為了使結果更加簡潔,我們可以移除相似的預測邊界框。常用的方法叫作非極大值抑制(non-maximum suppression,NMS)。
我們來描述一下非極大值抑制的工作原理。對于一個預測邊界框`$ B $`,模型會計算各個類別的預測概率。設其中最大的預測概率為`$ p $`,該概率所對應的類別即`$ B $`的預測類別。我們也將`$ p $`稱為預測邊界框`$ B $`的置信度。在同一圖像上,我們將預測類別非背景的預測邊界框按置信度從高到低排序,得到列表`$ L $ `。從`$ L $`中選取置信度最高的預測邊界框`$ B_1 $`作為基準,將所有與`$ B_1 $`的交并比大于某閾值的非基準預測邊界框從`$ L $` 中移除。這里的閾值是預先設定的超參數。此時,`$ L $`保留了置信度最高的預測邊界框并移除了與其相似的其他預測邊界框。
接下來,從`$ L $`中選取置信度第二高的預測邊界框`$ B_2 $`作為基準,將所有與`$ B_2 $`的交并比大于某閾值的非基準預測邊界框從`$ L $`中移除。重復這一過程,直到`$ L $`中所有的預測邊界框都曾作為基準。此時`$ L $`中任意一對預測邊界框的交并比都小于閾值。最終,輸出列表`$ L $`中的所有預測邊界框。
下面來看一個具體的例子。先構造4個錨框。簡單起見,我們假設預測偏移量全是0:預測邊界框即錨框。最后,我們構造每個類別的預測概率。
```{.python .input n=11}
anchors = nd.array([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
[0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = nd.array([0] * anchors.size)
cls_probs = nd.array([[0] * 4, # 背景的預測概率
[0.9, 0.8, 0.7, 0.1], # 狗的預測概率
[0.1, 0.2, 0.3, 0.9]]) # 貓的預測概率
```
在圖像上打印預測邊界框和它們的置信度。
```{.python .input n=12}
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
['dog=0.9', 'dog=0.8', 'dog=0.7', 'cat=0.9'])
```
我們使用`contrib.nd`模塊的`MultiBoxDetection`函數來執行非極大值抑制并設閾值為0.5。這里為`NDArray`輸入都增加了樣本維。我們看到,返回的結果的形狀為(批量大小, 錨框個數, 6)。其中每一行的6個元素代表同一個預測邊界框的輸出信息。第一個元素是索引從0開始計數的預測類別(0為狗,1為貓),其中-1表示背景或在非極大值抑制中被移除。第二個元素是預測邊界框的置信度。剩余的4個元素分別是預測邊界框左上角的`$ x $`和`$ y $`軸坐標以及右下角的`$ x $`和`$ y $`軸坐標(值域在0到1之間)。
```{.python .input n=13}
output = contrib.ndarray.MultiBoxDetection(
cls_probs.expand_dims(axis=0), offset_preds.expand_dims(axis=0),
anchors.expand_dims(axis=0), nms_threshold=0.5)
output
```
我們移除掉類別為-1的預測邊界框,并可視化非極大值抑制保留的結果。
```{.python .input n=14}
fig = d2l.plt.imshow(img)
for i in output[0].asnumpy():
if i[0] == -1:
continue
label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
show_bboxes(fig.axes, [nd.array(i[2:]) * bbox_scale], label)
```
實踐中,我們可以在執行非極大值抑制前將置信度較低的預測邊界框移除,從而減小非極大值抑制的計算量。我們還可以篩選非極大值抑制的輸出,例如,只保留其中置信度較高的結果作為最終輸出。
## 小結
* 以每個像素為中心,生成多個大小和寬高比不同的錨框。
* 交并比是兩個邊界框相交面積與相并面積之比。
* 在訓練集中,為每個錨框標注兩類標簽:一是錨框所含目標的類別;二是真實邊界框相對錨框的偏移量。
* 預測時,可以使用非極大值抑制來移除相似的預測邊界框,從而令結果簡潔。
## 練習
* 改變`MultiBoxPrior`函數中`sizes`和`ratios`的取值,觀察生成的錨框的變化。
* 構造交并比為0.5的兩個邊界框,觀察它們的重合度。
* 按本節定義的為錨框標注偏移量的方法(常數采用默認值),驗證偏移量`labels[0]`的輸出結果。
* 修改“標注訓練集的錨框”與“輸出預測邊界框”兩小節中的變量`anchors`,結果有什么變化?
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯