1. 香蕉檢測數據集
對象檢測沒有像MNIST或Fashion-MNIST這樣的小型數據集。爲了快速測試模型,能夠本身組裝數據集。首先使用香蕉生成1000個角度和大小不一樣的香蕉圖像。而後收集一些背景圖片,並將香蕉圖像放置在每一個圖像的隨機位置。製做好的香蕉檢測數據集能夠在網上下載。html
from d2l import mxnet as d2l from mxnet import gluon, image, np, npx, autograd, init from mxnet.gluon import nn from plotly import graph_objs as go, express as px from plotly.subplots import make_subplots from IPython.display import Image import plotly.io as pio import os pio.kaleido.scope.default_format = "svg" npx.set_np() d2l.DATA_HUB['bananas'] = (d2l.DATA_URL + 'bananas.zip', 'aadfd1c4c5d7178616799dd1801c9a234ccdaf19')
對於訓練集的每一個圖像,咱們將使用隨機裁剪,並要求裁剪後的圖像至少覆蓋每一個對象的95%。因爲裁剪是隨機的,所以不必定老是知足此要求。咱們將隨機裁剪的最大嘗試次數預設爲200。若是沒有一次符合要求,則不會裁剪圖像。爲了確保輸出的肯定性,咱們不會在測試數據集中隨機裁剪圖像。python
def load_data_bananas(batch_size, edge_size=256): data_dir = d2l.download_extract('bananas') train_iter = image.ImageDetIter( path_imgrec=os.path.join(data_dir, 'train.rec'), path_imgidx=os.path.join(data_dir, 'train.idx'), batch_size=batch_size, data_shape=(3, edge_size, edge_size), # 圖像的形狀 shuffle=True, # 隨機讀取 rand_crop=1, # 隨機裁剪的觸發機率爲1 min_object_covered=0.95, max_attempts=200) val_iter = image.ImageDetIter( path_imgrec=os.path.join(data_dir, 'val.rec'), batch_size=batch_size, data_shape=(3, edge_size, edge_size), shuffle=False) return train_iter, val_iter
圖像的形狀與以前的實驗相同(批處理大小,通道數,高度,寬度)。標籤的形狀是(批量大小, m ,5),m 等於數據集中單個圖像中包含的最大邊界框數量。儘管小批量的計算很是有效,但它要求每一個圖像包含相同數量的邊界框,以便它們能夠放在同一批中。因爲每一個圖像可能具備不一樣數量的邊界框,所以咱們能夠向包含如下內容的圖像添加非法邊界框,直到每一個圖像包含m邊界框。圖像中每一個邊框的標籤由長度爲5的數組表示。數組中的第一個元素是邊框中包含的對象的類別。當值爲-1時,邊界框是用於填充目的的非法邊界框。數組的其他四個元素表明邊界框左上角的 x,y 以及邊界框的右下角x,y(值範圍介於0和1之間)。ios
batch_size, edge_size = 32, 256 train_iter, _ = load_data_bananas(batch_size, edge_size) batch = train_iter.next() batch.data[0].shape, batch.label[0].shape # ((32, 3, 256, 256), (32, 1, 5))
2.數據示範
咱們有十張帶有邊框的圖像。咱們能夠看到,每一個圖像中香蕉的角度,大小和位置都不一樣。固然,這是一個簡單的人工數據集, 在實際實踐中,數據一般要複雜得多。git
def show_imgs(imgs, num_rows=2, num_cols=4, scale=0.8, labels=None) : fig = make_subplots(num_rows, num_cols) for i in range(num_rows): for j in range(num_cols): z = imgs[num_cols*i+j].asnumpy() fig.add_trace(go.Image(z=z),i+1,j+1) if labels is not None: x0, y0, x1, y1 = labels[num_cols*i+j][0][1:5] * edge_size fig.add_shape(type="rect",x0=x0,y0=y0,x1=x1,y1=y1,line=dict(color="white"),row=i+1, col=j+1) fig.update_xaxes(visible=False, row=i+1, col=j+1) fig.update_yaxes(visible=False, row=i+1, col=j+1) img_bytes = fig.to_image(format="png", scale=scale, engine="kaleido") return img_bytes imgs = (batch.data[0][0:10].transpose(0, 2, 3, 1)) Image(show_imgs(imgs, 2, 5, scale=2, labels= batch.label[0][0:10]))
3.單發多盒檢測(SSD)
構建用於對象檢測模型:單發多盒檢測(SSD)。該模型的主要組件是基礎網絡模塊和串聯鏈接的多個多尺度功能模塊。在這裏,基本網絡塊用於提取原始圖像的特徵,而且一般採用深度卷積神經網絡的形式。咱們能夠設計基礎網絡,使其輸出更大的高度和寬度。經過這種方式,能夠基於此特徵圖生成更多錨點框,從而使咱們可以檢測較小的對象。接下來,每一個多尺度特徵塊都會減少上一層提供的特徵圖的高度和寬度(例如,能夠將尺寸減少一半)。而後,這些塊使用特徵圖中的每一個元素來擴展輸入圖像上的接收場。多尺度特徵塊離圖的頂部越近。它的輸出特徵圖越小,基於該特徵圖生成的錨點框越少。另外,特徵塊離頂部越近,特徵圖中每一個元素的接受場越大,它越適合檢測較大的對象。因爲SSD會根據基本網絡塊和每一個多尺度特徵塊生成不一樣數量的不一樣大小的錨定框,而後預測錨定框的類別和偏移量(即預測的邊界框),以便檢測不一樣大小的對象, SSD是一種多尺度目標檢測模型。github
3.1 類別預測層
將對象類別的數量設置爲q。加上表示背景的錨框0,錨框類別的數量爲 q + 1 q+1 q+1。將要素圖的高度和寬度設置爲 h h h 和 w w w。若是咱們以每一個元素爲中心生成 a a a 錨框,咱們總共須要分類 h w a hwa hwa 錨盒。若是咱們對輸出使用徹底鏈接的層(FCN),則可能會致使模型參數過多。可使用類別預測層下降模型的複雜度,使用卷積層來保持輸入的高度和寬度。所以,輸出和輸入與沿特徵圖的寬度和高度的空間座標一一對應。express
定義類別預測層,指定參數後 a a a 和 q q q ,它使用 3 × 3 3×3 3×3 卷積padding爲1的卷積層。此卷積層的輸入和輸出的高度和寬度保持不變。編程
def cls_predictor(num_anchors, num_classes): return nn.Conv2D(num_anchors * (num_classes + 1), kernel_size=3, padding=1)
3.2 邊界預測層
邊界框預測層的設計相似於類別預測層的設計。惟一的不一樣是,在這裏,咱們須要爲每一個錨框預測4個偏移,而不是q+1類別。數組
def bbox_predictor(num_anchors): return nn.Conv2D(num_anchors * 4, kernel_size=3, padding=1)
3.3 級聯的多尺度預測
SSD使用基於多個比例的特徵圖來生成錨框並預測其類別和偏移量。由於針對不一樣比例的特徵圖,以同一元素爲中心的錨框的形狀和數量不一樣,因此不一樣比例的預測輸出可能具備不一樣的形狀。網絡
咱們使用相同的一批數據來構建兩種不一樣的尺度的特徵映射, Y 1 Y1 Y1和 Y 2 Y2 Y2。在此, Y 2 Y2 Y2 高度和寬度爲 Y 1 Y1 Y1的一半。以類別預測爲例,咱們假設 Y 1 Y1 Y1和 Y 2 Y2 Y2特徵圖中的每一個元素都會生成五個(Y1)或三個(Y2)錨點框。當有10個對象類別時,類別預測輸出通道的數量分別爲 5 × ( 10 + 1 ) = 55 5×(10+1)=55 5×(10+1)=55和 3 × ( 10 + 1 ) = 33 3×(10+1)=33 3×(10+1)=33 。預測輸出的格式爲(批量大小,通道數,高度,寬度)。如您所見,除了批量大小,其餘維度的大小都不一樣。所以,咱們必須將它們轉換爲一致的格式,併合並多個尺度的預測,以利於後續計算。app
def forward(x, block): block.initialize() return block(x) Y1 = forward(np.zeros((2, 8, 20, 20)), cls_predictor(5, 10)) Y2 = forward(np.zeros((2, 16, 10, 10)), cls_predictor(3, 10)) (Y1.shape, Y2.shape) # ((2, 55, 20, 20), (2, 33, 10, 10))
通道尺寸包含全部具備相同中心的錨框的預測。咱們首先將通道尺寸移動到最終尺寸。因爲全部規模的批次大小均相同,所以咱們能夠將預測結果轉換爲二進制格式(批次大小,高度 × 寬度 × 通道數)
def flatten_pred(pred): return npx.batch_flatten(pred.transpose(0, 2, 3, 1)) def concat_preds(preds): return np.concatenate([flatten_pred(p) for p in preds], axis=1)
所以,不一樣形狀的 Y 1 Y1 Y1和 Y 2 Y2 Y2,咱們仍然能夠級聯爲同一批次的兩個不一樣尺度下的預測結果。
3.4 高度和寬度下采樣塊
對於多尺度物體檢測,咱們定義如下down_sample_blk塊,將高度和寬度減小50%。該塊由兩個 3 × 3 3×3 3×3的卷積層,以及一個 2 × 2 2×2 2×2 步長爲2的最大池化層串聯組成。
def down_sample_blk(num_channels): blk = nn.Sequential() for _ in range(2): blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1), nn.BatchNorm(in_channels=num_channels), nn.Activation('relu')) blk.add(nn.MaxPool2D(2)) return blk
經過測試高度和寬度下采樣塊中的正向計算,咱們能夠看到它改變了輸入通道的數量並將高度和寬度減半。
forward(np.zeros((2, 3, 20, 20)), down_sample_blk(10)).shape # (2, 10, 10, 10)
3.5 基本網絡塊
基本網絡塊用於從原始圖像提取特徵。爲了簡化計算,咱們將構建一個小的基礎網絡。該網絡由串聯鏈接的三個高度和寬度下采樣塊組成,所以它在每一步將通道數量加倍。當咱們輸入具備形狀的原始圖像時 256 × 256 256×256 256×256 ,基礎網絡模塊會輸出形狀爲 32 × 32 32×32 32×32 .
def base_net(): blk = nn.Sequential() for num_filters in [16, 32, 64]: blk.add(down_sample_blk(num_filters)) return blk forward(np.zeros((2, 3, 256, 256)), base_net()).shape # (2, 64, 32, 32)
3.6完整模型
SSD型號總共包含五個模塊。每一個模塊輸出一個特徵圖,用於生成錨框並預測這些錨框的類別和偏移量。第一個模塊是基礎網絡塊,第二到四個模塊是高度和寬度下采樣塊,第五個模塊是全局最大池化層,將高度和寬度減少到1。
def get_blk(i): if i == 0: blk = base_net() elif i == 4: blk = nn.GlobalMaxPool2D() else: blk = down_sample_blk(128) return blk
咱們將爲每一個模塊定義正向計算過程。與先前描述的卷積神經網絡相反,該模塊不只返回Y經過卷積計算輸出的特徵圖,並且還返回從中生成的當前比例的錨點框Y及其預測的類別和偏移量。
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor): Y = blk(X) anchors = npx.multibox_prior(Y, sizes=size, ratios=ratio) cls_preds = cls_predictor(Y) bbox_preds = bbox_predictor(Y) return (Y, anchors, cls_preds, bbox_preds)
多尺度特徵塊越靠近圖的頂部, 它檢測到的對象越大,它必須生成的錨點框也越大。在這裏,咱們首先將0.2到1.05的間隔分紅五個相等的部分,以肯定較小的錨框給定不一樣的尺寸:0.二、0.3七、0.54。而後,根據 0.2 × 0.37 = 0.272 \sqrt{0.2 \times 0.37} = 0.272 0.2×0.37 =0.272, 0.37 × 0.54 = 0.447 \sqrt{0.37 \times 0.54} = 0.447 0.37×0.54 =0.447進行分割。
sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],[0.88, 0.961]] ratios = [[1, 2, 0.5]] * 5 num_anchors = len(sizes[0]) + len(ratios[0]) - 1
完成完整的模型TinySDD
class TinySSD(nn.Block): def __init__(self, num_classes, **kwargs): super(TinySSD, self).__init__(**kwargs) self.num_classes = num_classes for i in range(5): # 使用setattr賦值語句綁定函數,元編程 setattr(self, f'blk_{i}', get_blk(i)) setattr(self, f'cls_{i}', cls_predictor(num_anchors, num_classes)) setattr(self, f'bbox_{i}', bbox_predictor(num_anchors)) def forward(self, X): anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5 for i in range(5): # 經過getattr(self, 'blk_%d' % i)獲取函數 X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward( X, getattr(self, f'blk_{i}'), sizes[i], ratios[i], getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}')) # 0表示批次大小保持不變 anchors = np.concatenate(anchors, axis=1) cls_preds = concat_preds(cls_preds) cls_preds = cls_preds.reshape(cls_preds.shape[0], -1, self.num_classes + 1) bbox_preds = concat_preds(bbox_preds) return anchors, cls_preds, bbox_preds
如今,咱們建立一個SSD模型實例,並使用它對X高度爲256像素的圖像minibatch進行正向計算。正如咱們先前驗證的那樣,第一個模塊輸出具備如下形狀的特徵圖: 32×32 。由於模塊2到4是高度和寬度向下採樣塊,模塊5是全局池化層,而且要素圖中的每一個元素都用做4個錨點框的中心,總共 (322+162+82+42+1)×4=5444 在五個尺度上爲每一個圖像生成錨框。
net = TinySSD(num_classes=1) net.initialize() X = np.zeros((32, 3, 256, 256)) anchors, cls_preds, bbox_preds = net(X) print('output anchors:', anchors.shape) print('output class preds:', cls_preds.shape) print('output bbox preds:', bbox_preds.shape)
4.訓練
4.1 初始化
獲取數據集,並初始化參數定義優化函數
batch_size = 32 train_iter, _ = d2l.load_data_bananas(batch_size) device, net = npx.gpu(), TinySSD(num_classes=1) net.initialize(init=init.Xavier(), ctx=device) trainer = gluon.Trainer(net.collect_params(), 'sgd', { 'learning_rate': 0.2, 'wd': 5e-4})
4.2 定義損失函數以及評估
對象檢測受到兩種損失。首先是錨框類別的損失。爲此,咱們能夠簡單地重用咱們在圖像分類中使用的交叉熵損失函數。第二個損失是正錨框偏移損失。偏移量預測是一個歸一化問題,使用 L1 範數損失,是預測值和真實值之間的差的絕對值。
cls_loss = gluon.loss.SoftmaxCrossEntropyLoss() bbox_loss = gluon.loss.L1Loss() def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks): cls = cls_loss(cls_preds, cls_labels) bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks) return cls + bbox
咱們可使用準確率來評估分類結果。當咱們使用 L1 範數損失,咱們將使用平均絕對偏差來評估邊界框預測結果。
def cls_eval(cls_preds, cls_labels): # argmax指定預測結果維度 return float((cls_preds.argmax(axis=-1).astype(cls_labels.dtype) == cls_labels).sum()) def bbox_eval(bbox_preds, bbox_labels, bbox_masks): return float((np.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
4.3 訓練模型
anchors在模型的正向計算過程當中生成多尺度錨定框,並預測每一個錨定框的類別(cls_preds)和偏移(bbox_preds)。而後,咱們根據標籤信息標記每一個生成的錨框的類別(cls_labels)和偏移量(bbox_labels)。最後,咱們使用預測的和標記的類別和偏移值來計算損失函數。爲了簡化代碼,咱們在這裏不評估訓練數據集。
def train(train_iter, num_epochs, loss_fn, device): timer = d2l.Timer() cls_err_lst, bbox_mae_lst =[], [] for epoch in range(num_epochs): # accuracy_sum, mae_sum, num_examples, num_labels metric = d2l.Accumulator(4) train_iter.reset() # Read data from the start. for batch in train_iter: timer.start() X = batch.data[0].as_in_ctx(device) Y = batch.label[0].as_in_ctx(device) with autograd.record(): # 生成多尺度錨框並預測每一個類別和偏移量 anchors, cls_preds, bbox_preds = net(X) # 每個錨框的類別和偏移 bbox_labels, bbox_masks, cls_labels = npx.multibox_target( anchors, Y, cls_preds.transpose(0, 2, 1)) # 計算類別和偏移的損失 l = loss_fn(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks) l.backward() trainer.step(batch_size) metric.add(cls_eval(cls_preds, cls_labels), cls_labels.size, bbox_eval(bbox_preds, bbox_labels, bbox_masks), bbox_labels.size) cls_err_lst.append(1-metric[0]/metric[1]) bbox_mae_lst.append(metric[2]/metric[3]) print(f'class err {cls_err_lst[-1]:.2e}, bbox mae {bbox_mae_lst[-1]:.2e}') print(f'{train_iter.num_image/timer.stop():.1f} examples/sec on ' f'{str(device)}') fig = go.Figure() fig.add_trace(go.Scatter(x=list(range(1, num_epochs+1)), y=cls_err_lst, name='class error', mode='lines+markers')) fig.add_trace(go.Scatter(x=list(range(1, num_epochs+1)), y=bbox_mae_lst, name='bbox mae', mode='lines+markers')) fig.update_layout(width=800, height=480, xaxis_title='epoch', xaxis_range=[1,num_epochs]) fig.show() num_epochs = 20 train(train_iter, num_epochs, calc_loss, device)
5.預測
在預測階段,咱們要檢測圖像中全部感興趣的對象。在下面,咱們閱讀測試圖像並轉換其大小。而後,將其轉換爲卷積層所需的四維格式。
img = image.imread('img/banana.jpg') feature = image.imresize(img, 256, 256).astype('float32') X = np.expand_dims(feature.transpose(2, 0, 1), axis=0)
建立一個函數用於基於錨點框及其預測的偏移量來預測邊界框。而後,咱們使用非最大抑制來刪除類似的邊界框。
def predict(X): anchors, cls_preds, bbox_preds = net(X.as_in_ctx(device)) cls_probs = npx.softmax(cls_preds).transpose(0, 2, 1) output = npx.multibox_detection(cls_probs, bbox_preds, anchors) idx = [i for i, row in enumerate(output[0]) if row[0] != -1] return output[0, idx] output = predict(X)
最後,咱們將置信水平至少爲0.3的全部邊界框顯示爲最終輸出。
def display(img, output, threshold, scale=1.5): fig = go.Figure() fig.add_trace(go.Image(z=img.asnumpy())) score_lst, x, y =[], [], [] for row in output: score = float(row[1]) if score < threshold: continue h, w = img.shape[0:2] bbox = [row[2:6] * np.array((w, h, w, h), ctx=row.ctx)] x0, y0, x1, y1 = bbox[0] score_lst.append(f'{score:.2f}') x.append(float(x0)+img.shape[0]*0.04) y.append(float(y0)+img.shape[0]*0.02) fig.add_shape(type="rect",x0=x0,y0=y0,x1=x1,y1=y1,line=dict(color="white")) fig.add_trace(go.Scatter(mode='text', x=x, y=y, text=score_lst, textfont={ 'color':'red','size':10})) img_bytes = fig.to_image(format="png", scale=scale, engine="kaleido") return img_bytes Image(display(img, output, threshold=0.9))
6.參考
https://d2l.ai/chapter_computer-vision/ssd.html