# 四、目標檢測與分割
從上一章我們知道,當我們在輸入圖像中只有一個類的實例時,圖像分類才真正處理這種情況。 即使那樣,它也只能為我們提供粗略的輸出,讓我們知道圖像中存在什么對象,但不知道它在哪里。 一個更有趣的情況是,當我們想查找一個類的所有實例,甚至多個不同的類在輸入圖像中的位置時。
為了解決這個更具挑戰性的問題,需要進行對象檢測和分割。 這些是計算機視覺領域,直到最近仍然非常具有挑戰性。 然而,將卷積神經網絡應用于這些問題近年來引起了很多關注,因此,在大多數情況下,現在可以考慮解決這些問題。 在本章中,我們將看到 CNN 如何很好地解決這些困難的任務。
下圖顯示了不同解決方案分段,定位,檢測和實例分段之間的區別:

在開始討論對象檢測之前,我們需要了解另一個重要概念-定位。 它是改善分類和啟用檢測的關鍵構建塊。 我們將看到這三個概念彼此密切相關,這是因為我們從圖像分類到具有定位的分類,最后是對象檢測。
在本章中,我們將學習以下有趣的主題:
* 圖像分類與定位
* 對象檢測
* 語義分割
* 實例分割
* 如何構建卷積神經網絡來執行所有這些任務
# 圖像分類與定位
在上一章學習了圖像分類之后,我們現在知道對圖像進行分類時,我們只是試圖在該圖像內輸出對象的類標簽。 通常,為了簡化任務,圖像中將只有一個對象。
展望未來,在許多情況下,我們也有興趣在圖像中找到對象的位置。 定位對象這一任務的名稱稱為**定位**。 在這種情況下,我們要產生的輸出是圍繞對象的盒子的坐標。 此框的名稱是邊界框或邊界矩形。 關于定位的重要細節是,每個圖像只能定位一個對象。
當我們建立一個負責預測類別標簽以及感興趣對象周圍的邊界框的模型時,稱為帶有局部化的**圖像分類**。
# 作為回歸的定位
可以使用與我們在第 3 章, “TensorFlow 中的圖像分類”中了解的網絡架構相似的網絡架構來實現定位。
除了預測類標簽外,我們還將輸出一個標志,指示對象的存在以及對象邊界框的坐標。 邊界框坐標通常是四個數字,分別代表左上角的`x`和`y`坐標,以及框的高度和寬度。
例如,在這種情況下,我們有兩個類別(C1(汽車)和 C2(人))進行預測。 我們網絡的輸出如下所示:

該模型的工作原理如下:
1. 我們將輸入圖像輸入到 CNN。
2. CNN 產生一個特征向量,該特征向量被饋送到三個不同的 FC 層。 這些不同的 FC 層(或負責人)中的每一個都將負責預測不同的事物:對象存在,對象位置或對象類。
3. 訓練中使用了三種不同的損失:每個頭部一個。
4. 計算當前訓練批次的比率,以權衡給定對象的存在對分類和位置損失的影響。 例如,如果批次中只有 10% 的對象圖像,那么這些損失將乘以 0.1。
提醒一下:輸出數字(即 4 個邊界框坐標)稱為**回歸**。
請注意,分類和回歸之間的重要區別是分類時,我們獲得離散/分類輸出,而回歸提供連續值作為輸出。 我們在圖中顯示模型如下:

從圖中可以清楚地看到三個全連接層,每個層都輸出不同的損失(狀態,類和框)。 使用的損失是邏輯回歸/對數損失,交叉熵/ softmax 損失和 Huber 損失。 胡貝爾損失是我們從未見過的損失。 這是用于回歸的損失,是 L1 和 L2 損失的一種組合。
局部化的回歸損失給出了圖像中對象的真實情況邊界框坐標與模型預測的邊界框坐標之間的某種相似度度量。 我們在這里使用 Huber 損失,但是可以使用各種不同的損失函數,例如 L2,L1 或平滑 L1 損失。
分類損失和局部損失被合并并通過標量比加權。 此處的想法是,如果首先存在一個對象,則我們只對反向傳播分類和邊界框損失感興趣。
此模型的完整損失公式如下:

# TensorFlow 實現
現在,我們將介紹如何在 TensorFlow 中實現這種模型。 它與分類模型極為相似,不同之處在于,我們在末尾有多個輸出層而不是只有一個,并且每個層都有自己的損失函數:
```py
def build_graph(self):
self.__x_ = tf.placeholder("float", shape=[None, 240, 320, 3], name='X')
self.__y_box = tf.placeholder("float", shape=[None, 4], name='Y_box')
self.__y_obj = tf.placeholder("float", shape=[None, 1], name='Y_obj')
# Training flag for dropout in the fully connected layers
self.__is_training = tf.placeholder(tf.bool)
with tf.name_scope("model") as scope:
conv1 = tf.layers.conv2d(inputs=self.__x_, filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)
conv2 = tf.layers.conv2d(inputs=pool1, filters=64, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)
conv3 = tf.layers.conv2d(inputs=pool2, filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
pool3 = tf.layers.max_pooling2d(inputs=conv3, pool_size=[2, 2], strides=2)
pool3_flat = tf.reshape(pool3, [-1, 40 * 30 * 32])
# 2 Head version (has object head, and bounding box)
self.__model_box = tf.layers.dense(inputs=pool3_flat, units=4)
self.__model_has_obj = tf.layers.dense(inputs=pool3_flat, units=1, activation=tf.nn.sigmoid)
with tf.name_scope("loss_func") as scope:
loss_obj = tf.losses.log_loss(labels=self.__y_obj, predictions=self.__model_has_obj)
loss_bbox = tf.losses.huber_loss(labels=self.__y_box, predictions=self.__model_box)
# Get ratio of samples with objects
batch_size = tf.cast(tf.shape(self.__y_obj)[0], tf.float32)
num_objects_label = tf.cast(tf.count_nonzero(tf.cast(self.__y_obj > 0.0, tf.float32)), tf.float32)
ratio_has_objects = (num_objects_label * tf.constant(100.0)) / batch_size
# Loss function that has an "ignore" factor on the bbox loss when objects is not detected
self.__loss = loss_obj + (loss_bbox*ratio_has_objects)
# Add loss to tensorboard
tf.summary.scalar("loss", self.__loss)
tf.summary.scalar("loss_bbox", loss_bbox)
tf.summary.scalar("loss_obj", loss_obj)
with tf.name_scope("optimizer") as scope:
self.__train_step = tf.train.AdamOptimizer(1e-4).minimize(self.__loss)
# Merge op for tensorboard
self.__merged_summary_op = tf.summary.merge_all()
# Build graph
init = tf.global_variables_initializer()
# Saver for checkpoints
self.__saver = tf.train.Saver(max_to_keep=None)
# Avoid allocating the whole memory
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.6)
self.__session = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
# Configure summary to output at given directory
self.__writer = tf.summary.FileWriter("./logs/loc_logs", self.__session.graph)
self.__session.run(init)
```
# 定位的其他應用
使用 CNN 在圖像中輸出興趣點坐標的想法可以擴展到許多其他應用。 其中一些包括人體姿勢估計(《DeepPose:通過深度神經網絡進行人體姿勢估計》),如下所示:

為訓練圖像中的對象定義了關鍵點/地標。 對于所有訓練圖像中的特定對象,這些關鍵點位置必須一致。
例如,在面部關鍵點檢測中,比如說我們有興趣定位眼睛,鼻子和嘴巴,我們必須在所有訓練面部圖像的眼睛,鼻子和嘴巴周圍定義多個關鍵點。 然后,就像前面的圖像一樣,我們訓練 CNN 以輸出預測的關鍵點位置,然后對這些輸出關鍵點坐標應用回歸損失以訓練 CNN。 在測試時,將輸入圖像饋入 CNN 以預測所有關鍵點位置。 下圖顯示了面部關鍵點檢測:

# 作為分類的對象檢測 – 滑動窗口
對象檢測與定位是一個不同的問題,因為我們可以在圖像中包含數量可變的對象。 因此,如果我們將檢測視為像定位一樣簡單的回歸問題,處理可變數量的輸出將變得非常棘手。 因此,我們將檢測視為分類問題。
長期使用的一種非常常見的方法是使用滑動窗口進行對象檢測。 想法是在輸入圖像上滑動固定大小的窗口。 然后,將窗口中每個位置的內容發送到分類器,該分類器將告訴我們該窗口是否包含感興趣的對象。
為此,人們可以首先訓練一個 CNN 分類器,其中包含我們想要檢測的對象的小幅裁剪圖像-調整大小與窗口大小相同。 汽車。 在測試時,固定大小的窗口會在要檢測對象的整個圖像中以滑動的方式移動。然后,我們的 CNN 會為每個窗口預測是否是一個對象(在這種情況下是汽車)。
僅使用一種尺寸的滑動窗口,我們只能檢測一種尺寸的對象。 因此,要查找更大或更小的對象,我們還可以在測試時使用更大或更小的窗口,并在將其發送到分類器之前調整內容的大小。 或者,您可以調整整個輸入圖像的大小,并僅使用一個尺寸的滑動窗口,該窗口也將在這些調整大小的圖像上運行。 兩種方法都可以使用,但其想法是產生所謂的“比例尺金字塔”,以便我們可以檢測圖像中不同尺寸的對象。
這種方法的最大缺點是,各種比例的大量窗口可能會通過 CNN 進行預測。 這使得將 CNN 用作分類器在計算上非常昂貴。 同樣對于大多數這些窗口,它們將始終不包含任何對象。
為了克服這個問題,已經進行了許多改進。 在以下各節中,我們將介紹為解決該問題而創建的各種技術和算法,以及較之以前的技術和算法如何進行了改進。
# 使用啟發式技術指導我們(R-CNN)
為了避免在輸入圖像上每個可能的位置(大多數都不會包含對象)運行分類器,我們可以使用一些外部方法向我們建議可能的區域。 一種可以做到這一點的方法稱為**選擇搜索**。
區域提議方法將在圖像中提供類似斑點的矩形??區域,這些區域可能包含感興趣的對象。 這些區域是存在感興趣對象的候選區域。 然后,僅將 CNN 分類器應用于這些建議的區域。 與滑動窗口方法相比,這大大減少了發送到 CNN 進行分類的農作物的數量。
該特定方法在 2013 年提出,并被稱為 *R-CNN:區域 CNN*。 下圖描述了 R-CNN 的過程:

# 問題
R-CNN 在計算上仍然很昂貴,因為您必須對大約 2,000 個單獨的區域候選運行 CNN。 結果,訓練和測試都非常慢。 CNN 分類器依賴于通過選擇性搜索進行檢測而生成的固定數量的矩形候選窗口。 這種方法并不是最快的方法,而且由于無法從訓練數據中了解候選區域,因此它們可能不是針對任務的最佳選擇。
# Fast R-CNN
2015 年,提出了快速 R-CNN 來解決 R-CNN 的速度問題。 在此方法中,主要的變化是我們在流水線中獲取投標區域的位置。 首先,我們通過 CNN 運行整個輸入圖像,而不是從輸入圖像中直接獲取它們,并提取靠近網絡末端的生成的特征圖。 接下來,再次使用區域提議方法,以與 R-CNN 類似的方式從該特征圖中提取候選區域。
以這種方式獲取建議有助于重用和共享昂貴的卷積計算。 網絡中位于網絡下方的全連接層將分類并另外定位,僅接受固定大小的輸入。 因此,使用稱為 **RoI 池**的新層將特征圖中建議的區域扭曲為固定大小(在下一節中進一步討論)。 RoI 池會將區域大小調整為最后一個 FC 層所需的大小。 下圖顯示了整個過程:

R-CNN 與 FastRCNN 的比較表明,后者在訓練時快約 10 倍,而在測試時快約 150 倍(使用 VGG 架構作為主要 CNN 時)。
# Faster R-CNN
這項技術在 2015 年 Fast R-CNN 之后不久提出,解決了使用外部區域建議方法的需求,并消除了與之相關的計算成本。
該算法的主要區別在于,不是使用外部算法(例如選擇性搜索)來創建候選,而是使用稱為**區域候選網絡**(**RPN**)的子網為我們學習并提出建議。 在此屏幕快照中顯示:

# 區域候選網
RPN 的工作是預測我們稱為錨點的對象(本質上只是一個邊界框)是否包含對象或僅是背景,然后完善此邊界框的位置。
基本上,RPN 通過在最后一個 CNN 特征圖上滑動一個小窗口(`3 x 3`)來做到這一點(同一特征圖 Fast R-CNN 從中獲得建議)。 對于每個滑動窗口中心,我們創建`k`固定錨框,并將這些框分類為是否包含對象:

在內部,在訓練過程中,我們選擇 IoU 最大的錨定邊界框和真實情況邊界框進行反向傳播。
# RoI 池化層
RoI 池層只是最大池的一種,池的大小取決于輸入的大小。 這樣做可以確保輸出始終具有相同的大小。 使用該層是因為全連接層始終期望輸入大小相同,但是 FC 層的輸入區域可能具有不同的大小。
RoI 層的輸入將是建議和最后的卷積層激活。 例如,考慮以下輸入圖像及其建議:

這里,我們有一個表格,總結了方法之間的差異:
| | **R-CNN** | **Fast R-CNN** | **Faster R-CNN** |
| --- | --- | --- | --- |
| 每個圖像的測試時間 | 50 秒 | 2 秒 | 0.2 秒 |
| 加速 | 1 倍 | 25 倍 | 250 倍 |
| 準確率 | 66% | 66.9% | 66.9% |
# 將傳統的 CNN 轉換為全卷積網絡
對于有效的對象檢測器而言,非常重要的一點是提高卷積,從而提高計算的重用性??。 為此,我們將所有 FC 層轉換為卷積層,如下圖所示。
以這種方式實現我們的網絡的目的是,他們可以使用比其最初設計的圖像更大的圖像作為輸入,同時共享計算以使其效率更高。 將所有 FC 層都轉換為卷積層的這種類型的網絡的名稱稱為完全卷積網絡(FCN)。
將 FC 層轉換為卷積層的基本技術是使用與輸入空間尺寸一樣大的內核大小,并使用過濾器數來匹配 FC 層上的輸出數。 在此示例中,我們期望輸入圖像為`14x14x3`。

以我們為例,用`100 x 100`的輸入補丁訓練一個全卷積網絡,并用`2,000 x 2,000`的輸入圖像進行測試,結果將是在`2000 x 2000`圖像上運行`100 x 100`的滑動窗口 。 當使用較大的輸入體積(如本例中所示)時,FCN 的輸出將是一個體積,其中每個單元格對應于原始輸入圖像上`100x100`窗口補丁的一張幻燈片。
現在,每次我們使用比原始訓練輸入大的輸入圖像時,效果都將像我們實際上在整個圖像上滑動分類器,但計算量卻減少了。 通過這種方式,我們通過 CNN 的前向傳遞一步一步地使滑動窗口卷積:

# 單發檢測器 – 您只看一次
在本節中,我們將繼續介紹一種稍有不同的對象檢測器,稱為單發檢測器。 單發檢測器嘗試將對象檢測偽裝為回歸問題。 此類別下的主要架構之一是 YOLO 架構(您只看一次),我們現在將對其進行詳細介紹。
YOLO 網絡的主要思想是在不使用任何滑動窗口的情況下優化輸入圖像中各個位置的預測計算。為實現此目的,網絡以大小為`N x N`單元格的網格形式輸出特征圖。
每個單元格都有`B * 5 + C`條目。 其中`B`是每個單元格的邊界框的數量,`C`是類概率的數量,而 5 是每個邊界框的元素(`x, y`:邊界框相對于其所在單元格的中心點坐標, `w`是相對于原始圖像的邊界框的寬度, `h`是相對于原始圖像的邊界框的高度,置信度:邊界框中對象存在的可能性)。
我們將置信度得分定義為:

如果單元格中沒有對象,則將為零。 否則將等于真實情況框與預測框之間的 IOU。
請注意,網格的每個單元格都負責預測固定數量的邊界框。
下圖描述了作為 YOLO 網絡輸出的單元格條目的樣子,它預測了形狀的張量`(N, N, B * 5 + C)`。 網絡的最后一個卷積層將輸出與柵格尺寸相同大小的特征圖。

中心坐標以及邊界框的高度和寬度在`[0, 1]`之間進行歸一化。 下圖顯示了如何計算這些坐標的示例:

網絡為每個單元格預測類別概率,邊界框和這些框的置信度。
實際的 YOLO 網絡具有 24 個卷積層,其后是 2 個全連接層。 但是,Fast YOLO 網絡是 9 層,如下所示:

另一個重要的一點是,即使每個對象似乎位于多個像元上,也將單獨將其分配給一個柵格像元(基于此中心和像元距離)。
目前,我們可以想象在圖像上可以檢測到的對象數量將是網格大小。 稍后,我們將看到如何處理每個網格單元的多個對象。 (錨盒)
# 創建用于 Yolo 對象檢測的訓練集
為了創建 YOLO 的訓練集,將與 YOLO 網絡的輸出特征圖預測相同大小的網格放置在每個訓練輸入圖像上。 對于網格中的每個像元,我們創建一個目標向量`Y`,其長度為`B * 5 + C`(即與上一節中的輸出特征圖網格像元大小相同)。
讓我們以訓練圖像為例,看看如何為圖像上的網格中的單元創建目標向量:

在上圖中,考慮我們根據對象中心的最短距離來選擇單元(在圖像中,后車的中心最靠近綠色單元)。 如果我們看一下上面的訓練圖像,我們會注意到感興趣的對象僅存在于一個單元格編號為 8 的單元格中。其余的單元格 1-7 和 9 沒有任何感興趣的對象。 每個單元的目標向量將具有 16 個條目,如下所示:

第一個條目是類別`P[c]`存在的置信度得分,對于沒有對象的單元格中的兩個錨定框,該得分均為 0。 其余值將*無關*。 單元格編號 8 有一個對象,并且對象的邊界框具有較高的 IOU。
對于大小為`NxM`的輸入訓練圖像,訓練后從卷積網絡輸出的目標向量的最終體積將為`3x3x16`(在此玩具示例中)
數據集中每個圖像的標簽信息將僅包括對象的中心坐標及其邊界框。 實現代碼以使其與網絡的輸出向量相匹配是您的責任; 這些任務包括以下所列的任務:
1. 將每個中心點的圖像空間轉換為網格空間
2. 將圖像空間上的邊界框尺寸轉換為網格空間尺寸
3. 查找圖像空間上最接近對象的單元格
如果我們將每個單元格類別的概率乘以每個邊界框的置信度,我們將獲得一些可以用另一種算法(非最大值抑制)過濾的檢測結果。
讓我們將置信度定義為反映單元格上任何類對象是否存在的事物。 (請注意,如果單元格上沒有對象,則置信度應為零,如果有對象,則置信度應為 IoU):

我們還需要定義一個條件類別概率; 給定對象`P(class | Pr)`的存在,我們想要這樣做是因為我們不希望損失函數在單元格上沒有對象的情況下懲罰錯誤的類預測。 該網絡僅預測每個單元格的一組類別概率,而不考慮框數`B`。
# 評估檢測(交并比)
在繼續進行之前,我們需要知道如何衡量我們的模型是否正確檢測到對象。 為此,我們計算會返回一個數字的交并比(IoU),根據某個參考(真實情況)告訴我們檢測的效果如何。 IoU 的計算方法是:將檢測和地面真理框彼此重疊的區域除以檢測和地面真理框所覆蓋的總面積:

這是一個糟糕,良好和出色的 IoU 的示例:

按照慣例,如果 IoU 大于 0.5,我們認為這兩個方框都匹配,并且在這種情況下,檢測為真陽性。
IoU 為零表示框不相交,IoU 為 1 表示完美匹配。
在我們的檢測器上,如果一個單元有多個錨定框,則 IoU 會幫助選擇哪個對目標負責。我們選擇具有最高實測值的 IoU 最高的錨定。
這是 IoU 的 Python 代碼:
```py
def iou_non_vectorized(box1, box2):
# If one of the rects are empty return 0 (No intersect)
if box1 == [] or box2 == []:
return 0
# size of intersect divided by size of union of 2 rects
# Get rectangle areas format (left,top,right,bottom)
box_1_area = (box1[2] - box1[0] + 1) * (box1[3] - box1[1] + 1)
box_2_area = (box2[2] - box2[0] + 1) * (box2[3] - box2[1] + 1)
# Get the intersection coordinates (x1,y1,x2,y2)
intersect_x1 = max(box1[0], box2[0])
intersect_y1 = max(box1[1], box2[1])
intersect_x2 = min(box1[2], box2[2])
intersect_y2 = min(box1[3], box2[3])
# Calculate intersection area
intersect_area = (intersect_x2 - intersect_x1 + 1) * (intersect_y2 - intersect_y1
+ 1)
return intersect_area / float(box_1_area + box_2_area - intersect_area)
We can also change this to a vectorized form on Tensorflow
def tf_iou_vectorized(self, box_vec_1, box_vec_2):
def run(tb1, tb2):
# Break the boxes rects vector in sub-vectors
b1_x1, b1_y1, b1_x2, b1_y2 = tf.split(box_vec_1, 4, axis=1)
b2_x1, b2_y1, b2_x2, b2_y2 = tf.split(box_vec_2, 4, axis=1)
# Get rectangle areas format (left,top,right,bottom)
box_vec_1_area = (b1_x2 - b1_x1 + 1) * (b1_y2 - b1_y1 + 1)
box_vec_2_area = (b2_x2 - b2_x1 + 1) * (b2_y2 - b2_y1 + 1)
xA = tf.maximum(b1_x1, tf.transpose(b2_x1))
yA = tf.maximum(b1_y1, tf.transpose(b2_y1))
xB = tf.minimum(b1_x2, tf.transpose(b2_x2))
yB = tf.minimum(b1_y2, tf.transpose(b2_y2))
interArea = tf.maximum((xB - xA + 1), 0) * tf.maximum((yB - yA + 1), 0)
iou = interArea / (box_vec_1_area + tf.transpose(box_vec_2_area) - interArea)
return iou
op = run(self.tf_bboxes1, self. tf_bboxes2)
self.sess.run(op, feed_dict={self.tf_bboxes1: box_vec_1, self.tf_bboxes2: box_vec_2})
tic = time()
self.sess.run(op, feed_dict={self.tf_bboxes1: box_vec_1, self.tf_bboxes2: box_vec_2})
toc = time()
return toc - tic
```
我們也可以在 TensorFlow 上將其更改為向量化形式,如下所示:
```py
def tf_iou_vectorized(self, box_vec_1, box_vec_2):
def run(tb1, tb2):
# Break the boxes rects vector in sub-vectors
b1_x1, b1_y1, b1_x2, b1_y2 = tf.split(box_vec_1, 4, axis=1)
b2_x1, b2_y1, b2_x2, b2_y2 = tf.split(box_vec_2, 4, axis=1)
# Get rectangle areas format (left,top,right,bottom)
box_vec_1_area = (b1_x2 - b1_x1 + 1) * (b1_y2 - b1_y1 + 1)
box_vec_2_area = (b2_x2 - b2_x1 + 1) * (b2_y2 - b2_y1 + 1)
xA = tf.maximum(b1_x1, tf.transpose(b2_x1))
yA = tf.maximum(b1_y1, tf.transpose(b2_y1))
xB = tf.minimum(b1_x2, tf.transpose(b2_x2))
yB = tf.minimum(b1_y2, tf.transpose(b2_y2))
interArea = tf.maximum((xB - xA + 1), 0) * tf.maximum((yB - yA + 1), 0)
iou = interArea / (box_vec_1_area + tf.transpose(box_vec_2_area) - interArea)
return iou
op = run(self.tf_bboxes1, self. tf_bboxes2)
self.sess.run(op, feed_dict={self.tf_bboxes1: box_vec_1, self.tf_bboxes2: box_vec_2})
tic = time()
self.sess.run(op, feed_dict={self.tf_bboxes1: box_vec_1, self.tf_bboxes2: box_vec_2})
toc = time()
return toc - tic
```
# 過濾輸出
實際上,您的模型通常會返回同一對象的多個檢測窗口。 為了解決這個問題,我們使用一種稱為非最大抑制的算法。 該算法使用“IoU 和對象的存在”作為啟發式過濾這些多個框。 運作方式如下:
1. 丟棄所有包含對象的可能性低的框(`pc < 0.6`)
2. 選擇最有可能出現對象的盒子(標簽上的`pc`)
3. 丟棄與所選框高度重疊的所有框(`IoU > 0.5`)
4. 重復步驟 2 和 3,直到所有檢測都被放棄或選擇為止
我們將在檢測器的預測時間上使用非最大抑制:

Tensorflow 已經具有實現非最大值抑制算法的功能,稱為`tf.image.non_max_suppression`。
# 錨框
錨框預定義的模板框,具有一定的高寬比。 這些在 YOLO 中用于幫助檢測單個網格單元中的多個對象。 我們根據可以檢測到的對象類型的大致幾何形狀定義盒子的形狀。
目前,正如所解釋的,我們的模型將只能在每個網格單元中檢測到一個對象,但是在大多數情況下,每個網格中可能有多個對象。 請記住,我們認為最靠近對象的像元是中心:

為了解決這個問題,我們需要錨點。 基本上,我們將在輸出深度體積中添加預定義的邊界框; 然后,在訓練過程中,我們選擇中心最接近特定單元格的對象,并選擇與錨框具有最大 IoU 的邊界框。 實際上,由于多個子網將負責在同一單元中查找其他對象,因此,錨定框的想法使網絡更好地概括了檢測范圍。
# 在 Yolo 中進行測試/預測
現在將先前汽車圖像中的圖像視為我們的測試圖像。 每個像元的預測向量的輸出為:

請注意,`...`條目表示即使對于沒有對象的單元格,預測向量中也會有一些隨機值。 但是,在單元格 8 中,`x, y, h, w`的預測值有望接近準確。
在最后階段,我們可以使用非最大值抑制算法過濾每個像元中的多個預測邊界框。
# 檢測器損失函數(YOLO 損失)
作為定位器,YOLO 損失函數分為三個部分:負責查找邊界框坐標,邊界框分數預測和類分數預測的部分。 它們都是均方誤差損失,并由預測和真實情況情況之間的一些標量元參數或 IoU 得分進行調制:

成員`1[ij]^obj`成員用于基于特定單元`i, j`上對象的存在來調制損失:
* 如果在網格單元格`i`和第`j`個邊界框中具有最高 IoU 的對象存在:1
* 否則:0
同樣,`1[ij]^noobj`正好相反。
# 損失第 1 部分

第一部分計算與預測的邊界框位置坐標`(x, y)`相關的損失。 `(x_hat, y_hat)`是訓練集中真實情況數據的邊界框坐標。
`λ[coord] = 5.0`表示一個常數,當有錯誤時,該常數將給予更多的補償。 `B`是邊界框的數量。 `S^2`是網格中的單元數。
使用類似的公式來處理邊界框的寬度/高度

損失函數方程中寬度和高度的平方根用來反映小盒子中的小偏差比大盒子中的重要。 一般而言,這部分損失會對邊界框的高度和寬度不正確進行懲罰。
# 損失第 2 部分
損失函數的這一部分計算與每個邊界框預測變量的置信度得分相關的損失。

`C`是置信度分數(受對象的存在調制的項)。`C_hat`是帶有真實情況的預測邊界框的 IOU。 參數`λ[noobj] = 0.5`用于使無對象時的損失關注度降低。
# 損失第 3 部分
分類損失是損失函數的最后一部分。

該損失是分類誤差損失平方的總和。 同樣,當單元上有一個對象時,項`1[i]^(obj)`為 1,否則為 0。 我們的想法是,當存在對象時,我們不考慮分類錯誤。
`1[i]^(obj), 1[ij]^(obj), 1[ij]^(noobj)`這些項可以掩蓋我們在真實情況上有一個對象而在特定單元的模型輸出中有一個對象的情況下的損失。 當真實情況與模型輸出不匹配時,也是如此。
因此,例如,當特定單元格不匹配時,我們的損失將是:

當我們有比賽時:

在實踐中的實踐中,您將嘗試向量化這種損失并避免`for`循環并提高性能,這對于 Tensorflow 之類的庫尤其如此。
這是 YOLO 損失的 TensorFlow 實現:
```py
def loss_layer(self, predicts, labels, scope='loss_layer'):
with tf.variable_scope(scope):
predict_classes = tf.reshape(predicts[:, :self.boundary1], [self.batch_size, self.cell_size, self.cell_size, self.num_class])
predict_scales = tf.reshape(predicts[:, self.boundary1:self.boundary2], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell])
predict_boxes = tf.reshape(predicts[:, self.boundary2:], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4])
response = tf.reshape(labels[:, :, :, 0], [self.batch_size, self.cell_size, self.cell_size, 1])
boxes = tf.reshape(labels[:, :, :, 1:5], [self.batch_size, self.cell_size, self.cell_size, 1, 4])
boxes = tf.tile(boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size
classes = labels[:, :, :, 5:]
offset = tf.constant(self.offset, dtype=tf.float32)
offset = tf.reshape(offset, [1, self.cell_size, self.cell_size, self.boxes_per_cell])
offset = tf.tile(offset, [self.batch_size, 1, 1, 1])
predict_boxes_tran = tf.stack([(predict_boxes[:, :, :, :, 0] + offset) / self.cell_size,
(predict_boxes[:, :, :, :, 1] + tf.transpose(offset,
(0, 2, 1, 3))) / self.cell_size,
tf.square(predict_boxes[:, :, :, :, 2]),
tf.square(predict_boxes[:, :, :, :, 3])])
predict_boxes_tran = tf.transpose(predict_boxes_tran, [1, 2, 3, 4, 0])
iou_predict_truth = self.tf_iou_vectorized(predict_boxes_tran, boxes)
# calculate I tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
object_mask = tf.reduce_max(iou_predict_truth, 3, keep_dims=True)
object_mask = tf.cast((iou_predict_truth >= object_mask), tf.float32) * response
# calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
noobject_mask = tf.ones_like(object_mask, dtype=tf.float32) - object_mask
boxes_tran = tf.stack([boxes[:, :, :, :, 0] * self.cell_size - offset,
boxes[:, :, :, :, 1] * self.cell_size - tf.transpose(offset, (0, 2, 1, 3)),
tf.sqrt(boxes[:, :, :, :, 2]),
tf.sqrt(boxes[:, :, :, :, 3])])
boxes_tran = tf.transpose(boxes_tran, [1, 2, 3, 4, 0])
# class_loss
class_delta = response * (predict_classes - classes)
class_loss = tf.reduce_mean(tf.reduce_sum(tf.square(class_delta), axis=[1, 2, 3]), name='class_loss') * self.class_scale
# object_loss
object_delta = object_mask * (predict_scales - iou_predict_truth)
object_loss = tf.reduce_mean(tf.reduce_sum(tf.square(object_delta), axis=[1, 2, 3]), name='object_loss') * self.object_scale
# noobject_loss
noobject_delta = noobject_mask * predict_scales
noobject_loss = tf.reduce_mean(tf.reduce_sum(tf.square(noobject_delta), axis=[1, 2, 3]), name='noobject_loss') * self.noobject_scale
# coord_loss
coord_mask = tf.expand_dims(object_mask, 4)
boxes_delta = coord_mask * (predict_boxes - boxes_tran)
coord_loss = tf.reduce_mean(tf.reduce_sum(tf.square(boxes_delta), axis=[1, 2, 3, 4]), name='coord_loss') * self.coord_scale
```
# 語義分割
在語義分割中,目標是根據像素所屬的對象類別標記圖像的每個像素。 最終結果是一個位圖,其中每個像素將屬于某個類:

有幾種流行的 CNN 架構已被證明在分割任務中表現出色。 它們中的大多數是稱為自編碼器的一類模型的變體,我們將在第 6 章,“自編碼器,變分自編碼器和生成模型”中詳細介紹。 現在,他們的基本思想是首先在空間上將輸入量減小為某種壓縮形式,然后恢復原始的空間大小:

為了增加空間大小,使用了一些常用的操作,其中包括:
* 最大分割
* 反卷積/轉置卷積
* 擴張/帶孔卷積
我們還將學習語義分割任務中使用的 softmax 的新變體,稱為**空間 softmax** 。
在本節中,我們將學習兩個流行的模型,它們在語義分割上表現良好,并且具有非常簡單的架構可供理解。 它們如下所示:
* FCN(全卷積網絡)
* Segnet
需要解決的其他一些實現細節是:
* 最終的上采樣層(Deconv)需要具有與分類一樣多的過濾器,并且您的標簽“顏色”需要與最后一層中的索引匹配,否則在訓練過程中可能會遇到 NaN 問題
* 我們需要一個 Argmax 層來選擇輸出張量上概率最大的像素(僅在預測時間內)
* 我們的損失需要考慮輸出張量上的所有像素
# 最大分割
取消池操作用于恢復最大池操作的效果。 這個想法只是充當上采樣器。 此操作已在一些較早的論文上使用,并且不再使用,因為您還需要卷積層來修補(低通過濾器)上采樣的結果:

# 反卷積層(轉置卷積)
這個運算相當不好地稱為反卷積,這意味著它是卷積的逆運算,但實際上并非如此。 更恰當的名稱是轉置卷積或分數步卷積。
此層類型為您提供了一種對輸入體積進行升采樣的學習方法,并且可以在每次需要將輸入特征圖智能地投影到更高的空間時使用。 一些用例包括以下內容:
* 上采樣(條紋轉置卷積)`== UNPOOL + CONV`
* 可視化顯著圖
* 作為自編碼器的一部分

在 Tensorflow 中,我們可以訪問`tf.layers`中的轉置卷積。 下面的示例將采用一個空間大小為`14 x 14`的輸入,并使其通過`conv2d_transpose`層,其中輸出空間大小為`28 x 28`:
```py
# input_im has spatial dimensions 14x14 in this example
output = tf.layers.conv2d_transpose(inputs=input_im, filters=1, kernel_size=4, strides=2, padding='same')
```
選擇`kernel_size`,步幅和填充方案時必須小心,因為它們都會影響輸出空間大小。
# 損失函數
如前所述,分割模型的損失函數基本上是分類損失的擴展,但在整個輸出向量中在空間上起作用:

```py
# Segmentation problems often uses this "spatial" softmax (Basically we want to classify each pixel)
with tf.name_scope("SPATIAL_SOFTMAX"):
loss = tf.reduce_mean((tf.nn.sparse_softmax_cross_entropy_with_logits(
logits=model_out,labels=tf.squeeze(labels_in, squeeze_dims=[3]),name="spatial_softmax")))
```
下圖描述了用于語義分割的完全卷積網絡的實現:

下圖顯示了 SegNet 架構:

# 標簽
如前所述,分割問題中的標簽是一維圖像,每個像素處的值與輸出體積深度的索引匹配:

# 改善結果
通常,一種用于改善分割輸出結果的技術是在后期處理階段使用條件隨機場(CRF),其中要考慮圖像的純 RGB 特征和我們的網絡所產生的概率:

# 實例分割
實例分割是我們在本章中要討論的最后一件事。 在許多方面,可以將其視為對象檢測和語義分段的融合。 但是,與這兩個問題相比,這絕對是難度增加。
通過實例分割,其思想是找到圖像中一個或多個所需對象的每次出現,即所謂的實例。 找到這些實例后,即使它們屬于同一類對象,我們也希望將它們彼此分開。 換句話說,標簽既是類別感知的(例如汽車,標志或人),又是實例感知的(例如汽車 1,汽車 2 或汽車 3)。
實例分割的結果將如下所示:

這與語義分割之間的相似性很明顯; 我們仍然根據像素所屬的對象來標記像素。 但是,盡管語義分割不知道某個對象在圖像實例中出現了多少次,但是分割卻知道。
這種知道圖像中有多少個對象實例的能力也使該問題類似于對象檢測。 但是,對象檢測產生的對象邊界要粗糙得多,這意味著被遮擋的對象更容易被遺漏,實例分割不會發生這種情況。
# Mask R-CNN
Mask R-CNN 是一種最近的網絡架構,通過提供簡單,靈活的模型架構可以使此問題更易于解決。 該架構于 2017 年發布,旨在擴展更快的 R-CNN 的功能:

它采用現有的更快的 R-CNN 模型,并嘗試通過向模型中添加一個分支來解決實例分割問題,該分支負責預測與分類和邊界框回歸頭平行的對象蒙版。 在發布時,該架構被證明是有效的,并且在所有 COCO 挑戰中均獲得了最高榮譽。
# 總結
在本章中,我們學習了對象定位,檢測和分段的基礎知識。 我們還討論了與這些主題相關的最著名的算法。
在下一章中,我們將討論一些常見的網絡架構。
- TensorFlow 1.x 深度學習秘籍
- 零、前言
- 一、TensorFlow 簡介
- 二、回歸
- 三、神經網絡:感知器
- 四、卷積神經網絡
- 五、高級卷積神經網絡
- 六、循環神經網絡
- 七、無監督學習
- 八、自編碼器
- 九、強化學習
- 十、移動計算
- 十一、生成模型和 CapsNet
- 十二、分布式 TensorFlow 和云深度學習
- 十三、AutoML 和學習如何學習(元學習)
- 十四、TensorFlow 處理單元
- 使用 TensorFlow 構建機器學習項目中文版
- 一、探索和轉換數據
- 二、聚類
- 三、線性回歸
- 四、邏輯回歸
- 五、簡單的前饋神經網絡
- 六、卷積神經網絡
- 七、循環神經網絡和 LSTM
- 八、深度神經網絡
- 九、大規模運行模型 -- GPU 和服務
- 十、庫安裝和其他提示
- TensorFlow 深度學習中文第二版
- 一、人工神經網絡
- 二、TensorFlow v1.6 的新功能是什么?
- 三、實現前饋神經網絡
- 四、CNN 實戰
- 五、使用 TensorFlow 實現自編碼器
- 六、RNN 和梯度消失或爆炸問題
- 七、TensorFlow GPU 配置
- 八、TFLearn
- 九、使用協同過濾的電影推薦
- 十、OpenAI Gym
- TensorFlow 深度學習實戰指南中文版
- 一、入門
- 二、深度神經網絡
- 三、卷積神經網絡
- 四、循環神經網絡介紹
- 五、總結
- 精通 TensorFlow 1.x
- 一、TensorFlow 101
- 二、TensorFlow 的高級庫
- 三、Keras 101
- 四、TensorFlow 中的經典機器學習
- 五、TensorFlow 和 Keras 中的神經網絡和 MLP
- 六、TensorFlow 和 Keras 中的 RNN
- 七、TensorFlow 和 Keras 中的用于時間序列數據的 RNN
- 八、TensorFlow 和 Keras 中的用于文本數據的 RNN
- 九、TensorFlow 和 Keras 中的 CNN
- 十、TensorFlow 和 Keras 中的自編碼器
- 十一、TF 服務:生產中的 TensorFlow 模型
- 十二、遷移學習和預訓練模型
- 十三、深度強化學習
- 十四、生成對抗網絡
- 十五、TensorFlow 集群的分布式模型
- 十六、移動和嵌入式平臺上的 TensorFlow 模型
- 十七、R 中的 TensorFlow 和 Keras
- 十八、調試 TensorFlow 模型
- 十九、張量處理單元
- TensorFlow 機器學習秘籍中文第二版
- 一、TensorFlow 入門
- 二、TensorFlow 的方式
- 三、線性回歸
- 四、支持向量機
- 五、最近鄰方法
- 六、神經網絡
- 七、自然語言處理
- 八、卷積神經網絡
- 九、循環神經網絡
- 十、將 TensorFlow 投入生產
- 十一、更多 TensorFlow
- 與 TensorFlow 的初次接觸
- 前言
- 1.?TensorFlow 基礎知識
- 2. TensorFlow 中的線性回歸
- 3. TensorFlow 中的聚類
- 4. TensorFlow 中的單層神經網絡
- 5. TensorFlow 中的多層神經網絡
- 6. 并行
- 后記
- TensorFlow 學習指南
- 一、基礎
- 二、線性模型
- 三、學習
- 四、分布式
- TensorFlow Rager 教程
- 一、如何使用 TensorFlow Eager 構建簡單的神經網絡
- 二、在 Eager 模式中使用指標
- 三、如何保存和恢復訓練模型
- 四、文本序列到 TFRecords
- 五、如何將原始圖片數據轉換為 TFRecords
- 六、如何使用 TensorFlow Eager 從 TFRecords 批量讀取數據
- 七、使用 TensorFlow Eager 構建用于情感識別的卷積神經網絡(CNN)
- 八、用于 TensorFlow Eager 序列分類的動態循壞神經網絡
- 九、用于 TensorFlow Eager 時間序列回歸的遞歸神經網絡
- TensorFlow 高效編程
- 圖嵌入綜述:問題,技術與應用
- 一、引言
- 三、圖嵌入的問題設定
- 四、圖嵌入技術
- 基于邊重構的優化問題
- 應用
- 基于深度學習的推薦系統:綜述和新視角
- 引言
- 基于深度學習的推薦:最先進的技術
- 基于卷積神經網絡的推薦
- 關于卷積神經網絡我們理解了什么
- 第1章概論
- 第2章多層網絡
- 2.1.4生成對抗網絡
- 2.2.1最近ConvNets演變中的關鍵架構
- 2.2.2走向ConvNet不變性
- 2.3時空卷積網絡
- 第3章了解ConvNets構建塊
- 3.2整改
- 3.3規范化
- 3.4匯集
- 第四章現狀
- 4.2打開問題
- 參考
- 機器學習超級復習筆記
- Python 遷移學習實用指南
- 零、前言
- 一、機器學習基礎
- 二、深度學習基礎
- 三、了解深度學習架構
- 四、遷移學習基礎
- 五、釋放遷移學習的力量
- 六、圖像識別與分類
- 七、文本文件分類
- 八、音頻事件識別與分類
- 九、DeepDream
- 十、自動圖像字幕生成器
- 十一、圖像著色
- 面向計算機視覺的深度學習
- 零、前言
- 一、入門
- 二、圖像分類
- 三、圖像檢索
- 四、對象檢測
- 五、語義分割
- 六、相似性學習
- 七、圖像字幕
- 八、生成模型
- 九、視頻分類
- 十、部署
- 深度學習快速參考
- 零、前言
- 一、深度學習的基礎
- 二、使用深度學習解決回歸問題
- 三、使用 TensorBoard 監控網絡訓練
- 四、使用深度學習解決二分類問題
- 五、使用 Keras 解決多分類問題
- 六、超參數優化
- 七、從頭開始訓練 CNN
- 八、將預訓練的 CNN 用于遷移學習
- 九、從頭開始訓練 RNN
- 十、使用詞嵌入從頭開始訓練 LSTM
- 十一、訓練 Seq2Seq 模型
- 十二、深度強化學習
- 十三、生成對抗網絡
- TensorFlow 2.0 快速入門指南
- 零、前言
- 第 1 部分:TensorFlow 2.00 Alpha 簡介
- 一、TensorFlow 2 簡介
- 二、Keras:TensorFlow 2 的高級 API
- 三、TensorFlow 2 和 ANN 技術
- 第 2 部分:TensorFlow 2.00 Alpha 中的監督和無監督學習
- 四、TensorFlow 2 和監督機器學習
- 五、TensorFlow 2 和無監督學習
- 第 3 部分:TensorFlow 2.00 Alpha 的神經網絡應用
- 六、使用 TensorFlow 2 識別圖像
- 七、TensorFlow 2 和神經風格遷移
- 八、TensorFlow 2 和循環神經網絡
- 九、TensorFlow 估計器和 TensorFlow HUB
- 十、從 tf1.12 轉換為 tf2
- TensorFlow 入門
- 零、前言
- 一、TensorFlow 基本概念
- 二、TensorFlow 數學運算
- 三、機器學習入門
- 四、神經網絡簡介
- 五、深度學習
- 六、TensorFlow GPU 編程和服務
- TensorFlow 卷積神經網絡實用指南
- 零、前言
- 一、TensorFlow 的設置和介紹
- 二、深度學習和卷積神經網絡
- 三、TensorFlow 中的圖像分類
- 四、目標檢測與分割
- 五、VGG,Inception,ResNet 和 MobileNets
- 六、自編碼器,變分自編碼器和生成對抗網絡
- 七、遷移學習
- 八、機器學習最佳實踐和故障排除
- 九、大規模訓練
- 十、參考文獻