<jupyter_start><jupyter_text>我們將透過此筆記本來實作SSD物件偵測。 本筆記的目的是完善[MXNet Gluon SSD教學範例](https://zh.gluon.ai/chapter_computer-vision/ssd.html)。 我們期望讀者能夠透過這個筆記本來更加地了解SSD演算法。因為這個筆記本是教學範例，我們採用了一個小的合成皮卡丘資料集，並且，使用了一個較小的SSD網路。若想要使用MXNet來訓練SSD，請參考[GluonCV](https://gluon-cv.mxnet.io)。 ---  索引0. [皮卡丘資料集](00)1. [測試：```class_predictor(num_anchors, num_classes)```的輸入與輸出](01)2. [測試：```box_predictor(num_anchors)```的輸入與輸出](02)3. [測試：```down_sample(num_filters)```的輸入與輸出](03)4. [測試：```body```的輸入與輸出](04)5. [定義一個Toy SSD網路](05)6. [測試：```ToySSD```的輸入與輸出](06)7. [測試：```iou```的輸入與輸出](07)8. [測試：```MultiBoxTarget```的輸入與輸出](08)9. [準備開始訓練 (定義訓練目標, Loss 和Metrics)](09)10. [訓練模型](10)11. [模型預測](11) ---<jupyter_code>import matplotlib.pyplot as plt
%matplotlib inline

import numpy as np
import pandas as pd

import mxnet as mx
from mxnet import nd

from mxnet import gluon
from mxnet.gluon import Block
from mxnet.gluon.nn import Sequential
from mxnet.contrib.ndarray import MultiBoxPrior<jupyter_output><empty_output><jupyter_text>---  0. 皮卡丘資料集我們將以皮卡丘資料集來示範如何做物件偵測。首先，我們從該資料集內撈出一個批次的資料，並且，選一張圖，將其畫出來看一看：<jupyter_code>import mxnet
import mxnet.image as image

data_shape = 256
batch_size = 64

def get_iterators(data_shape, batch_size):
    class_names = ['pikachu']
    num_class = len(class_names)
    train_iter = image.ImageDetIter(
        batch_size=batch_size, 
        data_shape=(3, data_shape, data_shape),
        path_imgrec='../datasets/pikachu/pikachu_train.rec',
        path_imgidx='../datasets/pikachu/pikachu_train.idx',
        shuffle=True,
        mean=True,
        rand_crop=1, 
        min_object_covered=0.95, 
        max_attempts=200)
    val_iter = image.ImageDetIter(
        batch_size=batch_size,
        data_shape=(3, data_shape, data_shape),
        path_imgrec='../datasets/pikachu/pikachu_val.rec',
        shuffle=False,
        mean=True)
    return train_iter, val_iter, class_names, num_class

train_data, test_data, class_names, num_class = get_iterators(data_shape, batch_size)
batch = train_data.next()         # 取得了一個批次的資料。

img = batch.data[0][0].asnumpy()  # grab the first image, convert to numpy array
img = img.transpose((1, 2, 0))    # we want channel to be the last dimension
img += np.array([123, 117, 104])
img = img.astype(np.uint8)        # use uint8 (0-255)

# draw bounding boxes on image
for label in batch.label[0][0].asnumpy():
    if label[0] < 0:
        break
    xmin, ymin, xmax, ymax = [int(x * data_shape) for x in label[1:5]]
    rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False, edgecolor=(1, 0, 0), linewidth=3)
    plt.gca().add_patch(rect)
plt.imshow(img)
plt.axis("off")
plt.show()
for idx,_ in enumerate(train_data):
    pass
num_batches=idx # see how many batches we have<jupyter_output><empty_output><jupyter_text>我們已經得到了一個批次的資料, 它包含了圖，圖內的物體方框，以及物體方框所屬類別。現在，我們來看一下，一個批次的資料是有什麼樣的形狀：<jupyter_code>images=batch.data[0]
labels=batch.label[0]

print(images.shape) # 印出一個批次的Tensor形狀(圖)
print(labels.shape) # 印出一個批次的Tensor形狀(標籤)<jupyter_output><empty_output><jupyter_text>接著，我們來看如何建造SSD網路。 --- $$\textbf{SSD}~\text{網路架構}$$  由上圖，可見有四個運算必須實作出來，分別為```Body```, ```down_sample```, ```class_predictor```和```box_predictor```。這四個運算已經放置於```ssd_utils.py```。 現在的問題是，這些運算到底會把輸入變成什麼樣的輸出呢？以下我們將來測試這些運算的輸出，並試著對這些運算有更多的了解。 [[回索引]](索引)   1. 測試： class_predictor(num_anchors, num_classes) 的輸入與輸出<jupyter_code>from ssd_utils import class_predictor

num_anchors=4 
num_classes=5

model=class_predictor(num_anchors, num_classes)
model.initialize() # 網路需初始化權重方能使用

input_tensor =nd.random.normal( shape=(100,3,40,40) )
output_tensor = model(input_tensor) # 看input_tensor經過此模型後，會輸出什麼樣的output_tensor
print("shape of the input tensor=\t", input_tensor.shape)
print("shape of the output tensor=\t", output_tensor.shape)<jupyter_output><empty_output><jupyter_text>* ```class_predictor``` 是用來預測錨框裡面物體的類別。* 實際上，```class_predictor``` 只是一個convolutional layer。它不會改變原feature map的長寬，而是單純地改變其channel數目。它會將channel數改為 ```num_anchors * (num_classes+1)```。* 由以上範例可見，```input tensor```的每個像素(40X40的其中一個)皆對應了10個預設錨框。而每個預設錨框可預測出```(num_classes+1)```個類別(加上背景也算是一類)。 [[回索引]](索引)  2. 測試：box_predictor(num_anchors) 的輸入與輸出<jupyter_code>from ssd_utils import box_predictor

num_anchors=10

model=box_predictor(num_anchors)
model.initialize() # 網路需初始化權重方能使用

input_tensor =nd.random.normal( shape=(100,3,40,40) )
output_tensor = model(input_tensor) # 看input_tensor經過此模型後，會輸出什麼樣的output_tensor
print("shape of the input tensor=\t", input_tensor.shape)
print("shape of the output tensor=\t", output_tensor.shape)<jupyter_output><empty_output><jupyter_text>* 實際上，```box_predictor``` 只是一個convolutional layer。它不會改變原feature map的長寬，而是單純地改變其channel數目。它會將輸出的channel數改為 ```num_anchors * 4 ```。也就是說，對於每一個預設錨框，我們希望預測出delta positions(錨框位移)。有了錨框位移之後，我們即可將預設錨框位置加上錨框位移，來微調出物體真正的位置。* 以下是計算錨框位移($d_x,d_y,d_w,d_h$)的方法：    $$    d_x = (g_x - a_x) ~/ a_w$$    $$    d_y = (g_y - a_y) ~/ a_h$$    $$    d_w = \log( g_w ~/ a_w ) $$    $$    d_h = \log( g_h ~/ a_h ) $$    以上，$(g_x, g_y,g_w,g_h)$以及$(a_x,a_y,a_w,a_h)$分別是真實方框(ground truth)位置以及預設錨框(anchor)位置。其中，下標的x,y代表了矩形框所在的中心點位置; w,h則代表矩形框的寬和長。    以上，我們將$d_x$, $d_y$分別除以$a_w$或$a_h$的原因是，我們想讓$d_x,d_y$能夠落在差不多的範圍內。    至於，$g_w$或$g_h$為何不個別去減掉$a_w$, $a_h$，而是個別去除以$a_w$, $a_h$，然後取$\log$? 這是因為，一般來說，預測錨框的長寬和真實方框的長寬有可能差異很大。我們以相除取$\log$的方式，讓那些比較大的差異減小，這樣可以使機器比較不會去過度注重於調整物體方框的長寬，而忽略了去調整物體方框的中心點位置。    事實上，我們最後還會將$(d_x,d_y,d_w,d_h)$分別拿去除以$(v_x,v_y,v_w,v_h)$ (variance of $x,y,w,h$)，這麼做的目的是讓$d_x,d_y,d_w,d_h$這幾個變量可以再去做進一步的調整。在我們的程式瑪裡，$(v_x,v_y,v_w,v_h)$預設是$(0.1,0.1,0.4,0.4)$。這表示我們將$d_x$, $d_y$放大10倍，$d_w$, $d_h$放大2.5倍。放大的原因是，讓Loss增大。也就是說，希望讓機器除了去學習分類物體以外，也可以去多注意於學習邊框位置的調整。至於為什麼我們把$d_x$, $d_y$放大的比較多呢？那是因為算出來的$d_w$, $d_h$仍然普遍較$d_x$, $d_y$還要大。所以，我們針對$d_w$, $d_h$就不做過多的放大，這樣可以使得$d_x,d_y,d_w,d_h$皆落在差不多的範圍內。        換句話說，你可以把除以variance這件事情，當作放大$(d_x,d_y,d_w,d_h)$，使得邊框位置偏移的相關Loss可以較大。另一方面，它也協助了我們微調資料$(d_x,d_y,d_w,d_h)$的範圍，讓它們都落在差不多的範圍內，這相當於把資料拿去做標準化(standarization)。一般來說，經過標準化的資料，因為演算法特性的關係，機器所學習出來的結果會比較好。        最終，我們選擇用以下方式來計算錨框位移：        $$    d_x = (g_x - a_x) ~/ a_w    ~/ v_x $$    $$    d_y = (g_y - a_y) ~/ a_h    ~/ v_y $$    $$    d_w = \log( g_w ~/ a_w ) ~/ v_w $$    $$    d_h = \log( g_h ~/ a_h ) ~/ v_h $$    $$ ~\\ $$* $g_x,g_y,g_w,g_h$是透過以下計算得來($a_x,a_y,a_w,a_h$亦同)：    $$ g_x =(g_{x_{min}}+g_{x_{max}})/2$$    $$ g_y =(g_{y_{min}}+g_{y_{max}})/2$$    $$ g_w =g_{x_{max}}-g_{x_{min}}$$    $$ g_h =g_{y_{max}}-g_{y_{min}}$$        也就是說，矩形框的座標表示方式，可以是$(x,y,w,h)$，也可以是$(x_{min},y_{min},x_{max},y_{max})$。我們可以任意的使用不同的座標表示方法，只要座標轉換是正確的，這些表示方式都代表著同樣的矩形框座標位置。 現在，問題來了，也就是說，我們已經有了人工標記的物件方框位置，但是，我們要怎麼產生出預設錨框位置呢？首先我們先記得一件事情：feature map裡面的每個像素$(i,j)$, 皆對應了數個不同長寬比的預設錨框。我們現在來使用MXNet自帶的```MultiBoxPrior```來產生那些預設錨框：<jupyter_code>from mxnet.contrib.ndarray import MultiBoxPrior

# shape: batch x channel x height x weight
n = 40
x = nd.random.uniform(shape=(1, 3, n, n))

y = MultiBoxPrior(x, sizes=[.5,.25,.1], ratios=[1,2,.5]) # 產生數個不同長寬比的錨框

boxes = y.reshape((n, n, -1, 4))

# 畫出中心點位於像素(i,j)=(20,20)的五個預設錨框
import matplotlib.pyplot as plt
def box_to_rect(box, color, linewidth=3):
    """convert an anchor box to a matplotlib rectangle"""
    box = box.asnumpy()
    return plt.Rectangle(
        (box[0], box[1]), (box[2]-box[0]), (box[3]-box[1]), 
        fill=False, edgecolor=color, linewidth=linewidth)
colors = ['blue', 'green', 'red', 'black', 'magenta']
plt.imshow(nd.ones((n, n, 3)).asnumpy())
anchors = boxes[20, 20, :, :]
for i in range(anchors.shape[0]):
    plt.gca().add_patch(box_to_rect(anchors[i,:]*n, colors[i]))
plt.show()<jupyter_output><empty_output><jupyter_text>以上，假定我們有一個$40\times 40$的feature map。我們只畫出了中心像素$(i,j)=(20,20)$所對應的預設錨框樣貌。事實上，這個$40\times 40$的feature map內，每個像素都對應了$5$個不同長寬比的預設錨框。因此，我們總共產生了$40\times40\times5=8000$個錨框在這個feature map裡面。 [[回索引]](索引) ---  3. 測試：down_sample(num_filters) 的輸入與輸出<jupyter_code>from ssd_utils import down_sample

num_filters=10

model=down_sample(num_filters)
model.initialize() # 網路需初始化權重方能使用

input_tensor =nd.random.normal( shape=(100,3,40,40) )
output_tensor = model(input_tensor) # 看input_tensor經過此模型後，會輸出什麼樣的output_tensor
print("shape of the input tensor=\t", input_tensor.shape)
print("shape of the output tensor=\t", output_tensor.shape)<jupyter_output><empty_output><jupyter_text>* ```down_sample``` 是由兩個convolutional layer加上最後的一個Max Pooling layer所組成。通過最後的Max Pooling layer後, 原feature map的長寬會減半。* ```down_sample```的作用：讓我們可以在不同的feature map尺度下，去預測出預設錨框的位置，以及錨框內物件的類別。 [[回索引]](索引)  4. 測試：body 的輸入與輸出<jupyter_code>from ssd_utils import body

model=body()
model.initialize()

input_tensor =nd.random.normal( shape=(100,3,40,40) )
output_tensor = model(input_tensor)
print("shape of the input tensor=\t", input_tensor.shape)
print("shape of the output tensor=\t", output_tensor.shape)<jupyter_output><empty_output><jupyter_text>* 實際上，```body``` 是一個簡單的Conv Neural Network。它裡面內含三個Max Pooling，因此，原feature map的長(寬)會減半三次，其變化為：$40 \to 20 \to 10 \to 5$。 * 我們可由```output_tensor```得知，```body```會將原圖轉化成為64個，每個大小皆為5X5的高階特徵。 [[回索引]](索引)  5. 定義一個Toy SSD網路<jupyter_code>def flatten_prediction(pred):
    '''將原本大小為(#samples, #predictions, map_width, map_height)
       的張量轉置成(#samples, map_width, map_height, #predictions)之後，
       攤平成大小為(#samples, map_width * map_height * #predictions)的二維張量。
       
       如此一來，我們可發現，於第二個維度上，只要經過#predictions個元素，我們就會跑到下一個pixel的預測結果。
       
       因為該feature map有map_width * map_height個pixel，故，此二維張量的第二個維度，其長度是
       map_width * map_height * #predictions。
    '''
    return nd.flatten(nd.transpose(pred, axes=(0, 2, 3, 1)))

def concat_predictions(preds):
    '''將不同尺度下的預測全部合併為一個大的張量。我們之後會拿兩個大的張量去計算Loss，這樣我們計算上會比較有效率。'''
    return nd.concat(*preds, dim=1)

def toy_ssd_model(num_anchors, num_classes):
    """回傳一些SSD模組。我們稍後將利用這些模組來組裝成SSD模型。
       ---------------------------------------------
       輸入
         num_anchors: 預設的錨框數
         num_classes: 物體類別數
       ---------------------------------------------
       輸出
         body: conv network, 用來得到高階feature maps特徵。
         downsamples: 內含3個down sampler, 可將經過body出來的feature maps降採樣三次。
         class_preds: 內含4個類別預測器。我們將會拿這四個類別預測器，分別在四個不同的feature maps尺度下，
                      預測錨框內物體的類別。
         box_preds:   內含4個錨框位置預測器。我們將會拿這四個位置預測器，分別在四個不同的feature maps尺度下，
                      預測錨框應有的位置。  
    """
    
    downsamples,class_preds,box_preds = [Sequential(),
                                         Sequential(),
                                         Sequential()]

    downsamples.add(*[down_sample(128),
                      down_sample(128),
                      down_sample(128)])
    
    for scale in range(4):
        class_preds.add(class_predictor(num_anchors, num_classes))
        box_preds.add(box_predictor(num_anchors))
    
    return body(), downsamples, class_preds, box_preds

def toy_ssd_forward(x, body, downsamples, class_preds, box_preds, sizes, ratios):     
    '''定義Toy SSD模型。'''
    # 得到經過body network轉換出來的feature maps。
    x = body(x)
    
    # 圖像經過body network轉換後，會成為feature maps。首先，我們要在這個feature maps的尺度下，產生各種不同大小和比例的
    # 預設錨框。接著，我們將預測出錨框位置偏移(這樣我們才知道錨框位置須如何調整才能接近真實方框)和錨框內物體類別。
    # 註：若預設錨框內含真實物體，我們才會去預測出錨框位置偏移。
    # 預設的錨框位置,預測出來的錨框位置以及錨框內物體類別，將分別存放在
    # default_anchors, predicted_boxes 以及 predicted_classes 這三個清單內。
    default_anchors =   [ MultiBoxPrior(x, sizes=sizes[0], ratios=ratios[0])
                        ]
    predicted_boxes =   [ flatten_prediction(box_preds[0](x))
                        ]  
    predicted_classes = [ flatten_prediction(class_preds[0](x))
                        ]

    # 接下來就是三次的降採樣(down sampling)。每經過一次降採樣，我們就要在降採樣後的新尺度下去做預測。同樣的，
    # 預設的錨框位置,預測出來的錨框位置偏移以及錨框內物體類別，將分別被我們添加至
    # default_anchors, predicted_boxes 以及 predicted_classes 這三個清單內。
    for i in range(3):
        # 降採樣
        x = downsamples[i](x)
        
        # 添加預設錨框位置，預測出來的錨框位置偏移以及錨框內物體類別至各別的清單內。
        default_anchors.append( MultiBoxPrior(x, sizes=sizes[i+1], ratios=ratios[i+1])
                              )
        predicted_boxes.append(flatten_prediction(box_preds[i+1](x))
                              )
        predicted_classes.append(flatten_prediction(class_preds[i+1](x))
                                )
    return default_anchors, predicted_classes, predicted_boxes

class ToySSD(Block):
    '''將Toy SSD模型包裝成一個複雜的layer(在MXNet中，一個layer即為一個"Block")。
       在這裡面，我們須定義"forward"方法。之後，當一個tensor進入此layer時，forward即被觸發。
       換句話說，tensor會依照forward所定義的轉換，將原tensor去forward成為新的tensor。
    '''
    def __init__(self, num_classes, **kwargs):
        super(ToySSD, self).__init__(**kwargs)
        # anchor box sizes for 4 feature scales
        self.anchor_sizes = [[.2, .272], [.37, .447], [.54, .619], [.71, .79]]
        # anchor box ratios for 4 feature scales
        self.anchor_ratios = [[1, 2, .5]] * 4
        self.num_classes = num_classes

        with self.name_scope():
            self.body, self.downsamples, self.class_preds, self.box_preds = toy_ssd_model(4, num_classes)

    def forward(self,x):
        default_anchors, predicted_classes, predicted_boxes = toy_ssd_forward(x, self.body, self.downsamples,
            self.class_preds, self.box_preds, self.anchor_sizes, self.anchor_ratios)
        # we want to concatenate anchors, class predictions, box predictions from different layers
        anchors = concat_predictions(default_anchors)
        box_preds = concat_predictions(predicted_boxes)
        class_preds = concat_predictions(predicted_classes)
        # it is better to have class predictions reshaped for softmax computation
        class_preds = nd.reshape(class_preds, shape=(0, -1, self.num_classes + 1))
        
        return anchors, class_preds, box_preds<jupyter_output><empty_output><jupyter_text>[[回索引]](索引)  6. 測試： ToySSD 的輸入與輸出<jupyter_code>21760/4
ctx=mx.gpu(0)

net = ToySSD(1)
net.initialize(ctx=ctx)
x = nd.zeros((10, 3, 256, 256),ctx=ctx)
default_anchors, class_predictions, box_predictions = net(x)
print('1. shape of the default anchor boxes=\t', default_anchors.shape)
print('2. shape of class predictions=\t\t'     , class_predictions.shape)
print('3. shape of box predictions=\t\t'       , box_predictions.shape)<jupyter_output><empty_output><jupyter_text>1. ```default_anchors``` 存的是所有錨框的相應位置$(x_{min},y_{min},x_{max},y_{max})$。我們總共有5440個錨框。2. ```class_predictions```存放的是每張圖，每個錨框，其內含物體的預測類別(1類+背景=2類)。3. ```box prediction```存放的是每張圖，每個錨框，其預測的錨框位置偏移。註： 之後softmax會將```class_predictions```的最後一個維度內資訊轉換成機率。我們之後即可知道，於不同的圖 & 不同的預設錨框內，其各種物體的預測機率為何。 --- 我們現在已經差不多定義好網路架構了，但是，我們還是不知道如何算預設方框和真實方框的重疊程度。在這裡，我們將利用IoU(Intersection over Union)來計算兩方框間的重疊程度。 [[回索引]](索引)  7. 測試：iou 的輸入與輸出<jupyter_code>from ssd_utils import iou
box_a=nd.array([[0.,0.,1.,1.],]) # 由box_a的(x_min,y_min,x_max,y_max)我們即可決定出box_a位置
box_b=nd.array([[0.,0.,1.,1.],]) # 由box_b的(x_min,y_min,x_max,y_max)我們即可決定出box_b位置
print(iou(box_a,box_b))          # 印出box_a與box_b的IoU (應為1*1/1 = 1)<jupyter_output><empty_output><jupyter_text>以上範例，兩方框位置和大小相等，IoU為1。<jupyter_code>box_a=nd.array([[0.,0.,1.,1.],])   # 由box_a的(x_min,y_min,x_max,y_max)我們即可決定出box_a位置
box_b=nd.array([[0.,0.,0.5,0.5],]) # 由box_b的(x_min,y_min,x_max,y_max)我們即可決定出box_b位置
print(iou(box_a,box_b))            # 印出box_a與box_b的IoU (應為0.5*0.5/1 = 0.25)<jupyter_output><empty_output><jupyter_text>以上範例，兩方框交集為0.25, 聯集為1, 故IoU(交集/聯集)=0.25/1=0.25。<jupyter_code>box_a=nd.array([[0.,0.,1.,1.],]) # 由box_a的(x_min,y_min,x_max,y_max)我們即可決定出box_a位置
box_b=nd.array([[2.,2.,3.,3.],]) # 由box_b的(x_min,y_min,x_max,y_max)我們即可決定出box_b位置
print(iou(box_a,box_b))          # 印出box_a與box_b的IoU (應為0/2 = 0)<jupyter_output><empty_output><jupyter_text>以上範例，兩方框交集為0, 聯集為2, 故IoU(交集/聯集)=0/2=0。 [[回索引]](索引) ---  8. 測試：MultiBoxTarget 的輸入與輸出 我們接著來介紹```ssd_utils```內的```MultiBoxTarget```，它可以將真實方框(ground truth boxes)和預設錨框(default anchor boxes)做配對。原則上，只要真實方框和預設錨框的 IoU (Intersection over Union)$~>0.5$, 我們就會將其配對。 除此之外，我們也確保了，於單張圖內，每一個真實方框皆有配對到一個或以上的預設錨框。<jupyter_code>from ssd_utils import MultiBoxTarget
ctx=mx.gpu(0)

net = ToySSD(1)
net.initialize(ctx=ctx)
num_figs=2# 只取2張皮卡丘的圖

# 得到預設錨框位置，錨框內物體類別預測以及錨框位置預測
default_anchors, class_predictions, box_predictions = net(images[:num_figs].as_in_context(ctx))

# 得到真實錨框和預設錨框差距，錨框掩蓋碼，錨框類別，錨框類別掩蓋碼
anchor_shifts,box_mask,anchor_classes,classes_mask = MultiBoxTarget(default_anchors,
                                                                    class_predictions,
                                                                    labels[:num_figs].as_in_context(ctx),
                                                                    hard_neg_ratio=3,
                                                                    verbose=True)<jupyter_output><empty_output><jupyter_text>因為設定了```verbose=True```，所以這個方法有輸出一些診斷訊息。 訊息當中，比較需要被關注的，是針對不同的輸入圖所印出來的一張表。該表告訴我們，在該輸入圖當中，不同的物體類別，分別對應了多少個預設錨框。以上，我們的物體類別分別是-1(無皮卡丘),0(無皮卡丘),1(有皮卡丘)。我們可以發現，0類錨框數是1類錨框數的3倍，那是因為我們設定了```hard_neg_ratio=3```。 一般來說， 因為背景樣本相當的多，所以機器在學的時候，有可能會去過於關注於背景樣本的學習。換句話說，過多的負樣本容易導致機器不太知道該如何來分類正樣本(含有物體的樣本)。因此，在這裡我們使用了所謂的hard negative mining法。hard negative mining是指，我們先將負類樣本特別去採樣出比較難學的樣本(hard negative)，然後將其放置於0這一類。至於其他的負類錨框，我則將其放置於-1這一類。之後，在我們分類的時候，我們將捨棄-1這類的樣本，因為這類的樣本比較好學。* Q: 何謂hard negative樣本? A: 直覺的想，一看就知道是背景的圖，機器會認定它比較好學。而倘若有背景和真實物體夾雜的情況，該負樣本就很有可能是對於機器來說，比較難學的樣本。* Q: 如何採樣出hard negative樣本？ A: 我們只要去讓機器預測出該樣本是負類的機率即可。要是機器非常不確定該樣本是負類，那麼該樣本就會是hard negative樣本。 實際上我們的作法是去將負樣本的信心度做排序，讓機器把它覺得最沒信心是負樣本的那些樣本，去當作hard negative樣本。 接著我們來研究一下```MultiBoxTarget```所輸出的Tensors的形狀。<jupyter_code>print("Shape of anchor_shifts=\t\t",anchor_shifts.shape)
print("Shape of box_mask=\t\t",box_mask.shape)        
print("Shape of anchor_classes=\t",anchor_classes.shape)  
print("Shape of classes_mask=\t\t",classes_mask.shape)<jupyter_output><empty_output><jupyter_text>* ```anchor_shifts```儲存了真實物體方框和預設錨框的位置差距。只有當預設錨框內有物體，才會去做這個計算。* ```box_mask```是用來遮蓋掉沒有配對到真實物體的那些預設錨框。那些沒有配對到真實物體的預設錨框並沒有被計算出其和真實方框的位置差距。* ```anchor_classes```儲存了每個錨框的類別(可能是-1/0/1)。* ```classes_mask```是用來遮蓋掉不需要被採納的負類別(類別是-1的樣本過多，將被我們捨棄)。 [[回索引]](索引)  9. 準備開始訓練 (定義訓練目標, Loss 和Metrics)<jupyter_code>def training_targets(default_anchors, class_predicts, labels):
    '''定義訓練目標。
       目標：1. 學習真實方框和預設錨框的位置差距。這樣我們就可以知道怎麼微調預設錨框位置。
               另外，我們需要掩蓋碼，它可以幫我們遮蓋掉沒有配對到真實物體的那些預設錨框。
               這些資訊將分別存在box_target和box_mask之中。
            2. 學習分類物體。
               另外，我們需要掩蓋碼，它可以幫我們遮蓋掉大量的，容易學習的負類錨框。
               這些資訊將分別存在cls_target和classes_mask之中。
    '''
    z = MultiBoxTarget(*[default_anchors,class_predicts ,labels],
                       hard_neg_ratio=4)
    box_target = z[0]   # 真實物體方框和預設錨框的位置差距
    box_mask = z[1]     # 用來遮蓋掉沒有配對到真實物體的那些預設錨框
    cls_target = z[2]   # 儲存了每個錨框的類別
    classes_mask = z[3] # 用來遮蓋掉不需要被採納的負類別
    return box_target, box_mask, cls_target,classes_mask
class FocalLoss(gluon.loss.Loss):
    '''定義Focal Loss。它是用來計算真實物體類別和預測物體類別差異。
       這個Loss的好處是，較易學習的樣本，其Loss會較小。較難學習的樣本，其Loss會較大。
       
       如此，機器可以聚焦在比較難學習的樣本。
       
       Q: 什麼是難學習/易學習的樣本？ A: 若某樣本是正類，但機器認為它有很高的機會是負類，則該樣本會是難學習的樣本。
    '''
    def __init__(self, axis=-1, alpha=0.25, gamma=2, batch_axis=0, **kwargs):
        super(FocalLoss, self).__init__(None, batch_axis, **kwargs)
        self._axis = axis
        self._alpha = alpha
        self._gamma = gamma
    
    def hybrid_forward(self, F, output, label, classes_mask):
        output = F.softmax(output)
        pt = F.pick(output, label, axis=self._axis, keepdims=False)
        loss = classes_mask * -self._alpha * ((1 - pt) ** self._gamma) * F.log(pt)
        return F.mean(loss, axis=self._batch_axis, exclude=True)

# cls_loss = gluon.loss.SoftmaxCrossEntropyLoss()
cls_loss = FocalLoss()
print(cls_loss)
class SmoothL1Loss(gluon.loss.Loss):
    '''我們會利用此Loss去學習預測方框應該偏移多少才會接近真實方框。
    '''
    
    def __init__(self, batch_axis=0, **kwargs):
        super(SmoothL1Loss, self).__init__(None, batch_axis, **kwargs)
    
    def hybrid_forward(self, F, output, label, mask):
        loss = F.smooth_l1((output - label) * mask, scalar=1.0)
        return F.mean(loss, self._batch_axis, exclude=True)

box_loss = SmoothL1Loss()
print(box_loss)
cls_metric = mx.metric.F1()   # 因為負類樣本多，會導致accuracy相當好。如此，我們由accuracy就不容易看出分類的好壞。
                              # 另一方面，F1是綜合precision和recall的一個複合指標，使用它的好處是，它不會過於關注
                              # 負類樣本分類的accuracy，當負類樣本多時，這個指標是比較恰當的評估模型方式。
                              # 因此我們這裡將採用F1來評估模型。
box_metric = mx.metric.MAE()  # 我們用mean absolute error來評量方框偏差估計的好壞。<jupyter_output><empty_output><jupyter_text>[[回索引]](索引)  10. 訓練模型<jupyter_code>ctx = mx.gpu(0) # 使用GPU0

net = ToySSD(num_class)
net.collect_params().initialize(mx.init.Xavier(), ctx=ctx)
epochs = 15           # set larger to get better performance
# 我們將利用trainer來更新網路權重。
trainer = gluon.Trainer(net.collect_params(),'nadam',{'wd': 1.E-4}) 
import time
from mxnet import autograd as ag

models=[]

for epoch in range(epochs):
    # reset iterator and tick
    train_data.reset()
    cls_metric.reset()
    box_metric.reset()
    tic = time.time()
    # iterate through all the batches (except for the last one)
    for i in range(num_batches-1):
        batch=train_data.next()
        btic = time.time()
        # record gradients
        with ag.record():
            x = batch.data[0].as_in_context(ctx)
            y = batch.label[0].as_in_context(ctx)
            default_anchors, class_predictions, box_predictions = net(x)

            with ag.pause():
                box_target, box_mask, cls_target,classes_mask = training_targets(default_anchors, class_predictions, y)
            # losses
            loss1 = cls_loss(class_predictions, cls_target, classes_mask)
            loss2 = box_loss(box_predictions, box_target, box_mask)
            # sum all losses
            loss = loss1 + loss2
            # backpropagate
            loss.backward()
        # apply 
        trainer.step(batch_size)
        # update metrics
        cls_metric.update([cls_target.clip(0,np.nan)], [nd.transpose(class_predictions, (0, 2, 1))])
        #cls_metric.update([cls_target], [nd.transpose(class_predictions, (0, 2, 1))])
        box_metric.update([box_target], [box_predictions * box_mask])
    
    # end of epoch logging
    name1, val1 = cls_metric.get()
    name2, val2 = box_metric.get()
    
    # saving model at the last five epochs
    if epoch in range(epochs-5,epochs):
        models.append((val1,val2,net))

    print('[Epoch %d] training: %s=%f, %s=%f, time elapsed=%f'%(epoch, name1, val1, name2, val2, time.time()-tic))
import pandas as pd

# retrieve the saved models, turning it to a Pandas table
df=pd.DataFrame(models,columns=["f1","mae","model"])
df=df.sort_values(["f1"],ascending=False) # sort model according to its performance
df.head(3) # show the top three best models
# use the best model for prediction
best_model=df.iloc[0]["model"]<jupyter_output><empty_output><jupyter_text>[[回索引]](索引) ---  11. 模型預測<jupyter_code>import numpy as np
import cv2
def preprocess(image):
    """Takes an image and apply preprocess"""
    # resize to data_shape
    image = cv2.resize(image, (data_shape, data_shape))
    # swap BGR to RGB
    image = image[:, :, (2, 1, 0)]
    # convert to float before subtracting mean
    image = image.astype(np.float32)
    # subtract mean
    image -= np.array([123, 117, 104])
    # organize as [batch-channel-height-width]
    image = np.transpose(image, (2, 0, 1))
    image = image[np.newaxis, :]
    # convert to ndarray
    image = nd.array(image)
    return image

image = cv2.imread('../datasets/pikachu/pikachu.jpg')
x = preprocess(image)
print('shape of x=', x.shape)
anchors, cls_preds, box_preds = best_model(x.as_in_context(ctx))
print('anchors', anchors)
print('class predictions', cls_preds)
print('box delta predictions', box_preds)<jupyter_output><empty_output><jupyter_text>最後，我們將借助MXNet內建的```MultiBoxDetection```，把模型的預測結果顯現出來。<jupyter_code>from mxnet.contrib.ndarray import MultiBoxDetection
# convert predictions to probabilities using softmax
cls_probs = nd.SoftmaxActivation(nd.transpose(cls_preds, (0, 2, 1)), mode='channel')
# apply shifts to anchors boxes, non-maximum-suppression, etc...
output = MultiBoxDetection(*[cls_probs, box_preds, anchors],
                           force_suppress=True,
                           clip=False,
                           variances=(0.1,0.1,0.4,0.4)
                          )
def display(img, out, thresh=0.5):
    
    import random
    import matplotlib as mpl
    mpl.rcParams['figure.figsize'] = (10,10)
    pens = dict()
    plt.clf()
    plt.imshow(img)
    for det in out:
        cid = int(det[0])
        if cid < 0:
            continue
        score = det[1]
        if score < thresh:
            continue
        if cid not in pens:
            pens[cid] = (random.random(), random.random(), random.random())
        scales = [img.shape[1], img.shape[0]] * 2
        xmin, ymin, xmax, ymax = [int(p * s) for p, s in zip(det[2:6].tolist(), scales)]
        rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False, 
                             edgecolor=pens[cid], linewidth=3)
        plt.gca().add_patch(rect)
        text = class_names[cid]
        plt.gca().text(xmin, ymin-2, '{:s} {:.3f}'.format(text, score),
                       bbox=dict(facecolor=pens[cid], alpha=0.5),
                       fontsize=12, color='white')
    plt.show()
    
display(image[:, :, (2, 1, 0)], output[0].asnumpy(), thresh=0.4)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.14355964467005075, 'fraction_numerical': 0.019907994923857867, 'mean_word_length': 4.072822369744518, 'pattern_counts': {'":': 0, '<': 80, '<?xml version=': 0, '>': 78, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '442016', 'license': 'MIT', 'n_tokens_mistral': 13077, 'n_tokens_neox': 14010, 'n_words': 1432, 'path': '10-GluonObjectDetectionSSD.ipynb', 'repo_name': 'avain/DeepLearningTutorial', 'size': '25216'}