「Computer Vision」Note on CornerNet

QQ Group: 428014259
Tencent E-mail:[email protected]
http://blog.csdn.net/dgyuanshaofeng/article/details/82048113

20190413閱讀arXiv v2,該論文格式爲IJCV格式。【之前閱讀的arXiv v1以及ECCV】

CornerNet[1]的作者是來自普林斯頓大學的Hei Law和Jia Deng,此算法/模型是目標檢測的新方法。其中Jia Deng,還有其它比較出名的工作,比如人體姿態估計領域的Stacked hourglass networks(堆疊沙漏網絡)(ECCV2016),圖像分類領域的ImageNet數據集(CVPR2009)。

作者:Hei Law, Jia Deng
單位:Princeton University

0 摘要

將目標包圍盒(object bounding box)轉化爲一對關鍵點(paired keypoints):左-上角點和右-下角點,其優勢在於取消anchor boxes的設計。anchor boxes通常在檢測器中使用,比如SSD和Faster R-CNN。提出corner pooling(角點池化),用於準確定位角點。在MS COCO數據集上,取得42.1%【更改爲42.2%】AP,超越所有一階段式檢測器(這裏有問題,因爲一階段式檢測器更爲強調速度,而非精度,因此與這些模型比精度,似乎有些流氓了)。

1 介紹

anchor boxes就是各種size和aspect ratio預先定義好的框。一階段式檢測器依賴anchor boxes(錨框)實現類似二階段式檢測器的檢測精度,並且更爲有效率。然而,有兩點缺陷。其一,需要大量錨框,DSSD使用40k,RetinaNet使用100k,最後造成正負錨框的巨大不平衡。其二,引入許多超參數和設計選擇。如圖1所示,爲CornerNet的方法pipeline。卷積網絡輸出所有左和上一對角點的熱圖,輸出所有右和下一對角點的熱圖,還輸出每一對角點的embedding vector(嵌入向量)。

圖 1:CornerNet的pipeline

CornerNet的另一個要素是角點池化,如圖2所示。總之,角點通常在目標之外,需要藉助其它信息來定位角點,而非使用角點附近的信息。

圖 2: Corner Pooling

作者給出了檢測角點比檢測包圍框好的兩點假設性理由。其一,包圍盒的中心需要目標四邊,而角點需要兩邊。其二,更有效,可用O(wh)角點表達O(w^2 * h^2)可能錨框。

2 相關工作

分別介紹了兩類目標檢測器,其中DSSD和RON都使用了類似的沙漏網絡。
着重介紹了DeNet和本文工作的區別。
特別強調了本文工作的啓發工作Associative Embedding[2],後文爲NIPS2017的文章,可以重點閱讀一下,因爲NIPS上視覺類文章還是比較少的。
本文工作還使用了新穎變種focal loss。

3 角點網絡(CornerNet)

3.1 Overview

物體檢測被視作檢測邊界框的一對關鍵點——左上角點和右下角點。【就是[min_x, min_y]和[max_x, max_y],不過這裏用高斯核表示或具有一個半徑範圍】
如圖4所示爲CornerNet的overview。顯然,使用沙漏網絡作爲主幹網絡。沙漏網絡緊接着兩個預測模塊prediction module,上面的預測模塊預測左上角點,下面的預測模塊預測右下角點。每一個預測模塊有自己的角點池化Corner Pooling,見圖4最右邊,角點池化池化主幹網絡的特徵,然後預測熱圖heatmaps嵌入向量embeddings角點偏移offsets。作者在這裏僅僅利用了主幹網絡的輸出特徵,並沒有像其它物體檢測器那樣使用不同scales的特徵,預測不同sizes的物體。

圖 4: CornerNet

3.2 檢測角點

變種focal loss:
Ldet=...L_{det}=...
fast r-cnn裏面的smooth L1 loss:
Loff=...L_{off}=...

3.3 組合角點

組合角點的“pull”(拉近)損失:
Lpull=...L_{pull}=...
分離角點的“push”(推遠)損失:
Lpush=...L_{push}=...

3.4 角點池化(Corner Pooling)

如圖6所示,非常清楚地解釋瞭如何進行角點池化,以top-left點爲例。在實現中,詳細閱讀作者的cpp代碼。
1、水平方向,從右到左,相鄰兩列進行比較,保留較大者。
2、垂直方法,從底向頂,相鄰兩行進行比較,保留較大者。
3、上面兩步得到的特徵圖交疊的部分進行add相加操作。

圖 6:角點池化,以top-left點爲例

如圖5所示,爲預測模塊。具體修改見開源代碼和文章描述。

圖 5: 預測模塊

3.5 沙漏網絡(hourglass network)

作者對hourglass network[3]作了比較大的修改。
作者採用2個hourglasses。
1、不採用max pooling,採用stride=2的卷積。
2、下采樣5次,通道增加[256, 384, 384, 384, 512]。
3、上採樣時,先採用2個殘差模塊,後跟最近鄰上採樣。
4、跳躍結果裏面的變換,採用2個殘差模塊。
5、進入hourglass module之前,將圖像下采樣4倍。【也算是高分辨率表示學習了HRNet】

4 實驗

4.1 訓練細節

總的訓練損失:
L=Ldet+αLpull+βLpush+γLoffL=L_{det}+\alpha L_{pull}+\beta L_{push}+\gamma L_{off}
α\alpha=0.1
β\beta=0.1
γ\gamma=1

4.2 測試細節

在corner heatmaps上進行3×33 \times 3最大值池化。
original和flipped圖像都進行了測試,然後採用soft-nms。
平均預測時間244ms,採用設備爲Titan X。

4.3 MS COCO

trainval2014用於訓練
minival2014用於驗證
testdev2017用於測試(沒有金標準,上傳服務器進行評價)

4.4 消融實驗

4.4.1 角點池化

結果如表1所示。角點池化提升2.0個點。

表 1: 角點池化的作用

4.4.2 角點池化在較大區域的穩定性

結果如表3所示。角點池化提升2.8個點。

表 3: 角點池化的作用

4.4.3 減小負樣本位置懲罰

對中型和大型物體具有好處。對小型物體卻具有壞處
結果如表2所示。

表 2: corner的半徑

4.4.4 Hourglass Network

1、訓練CornerNet,採用FPN w/ ResNet-101,initialize the networks from scratch,only use the final output of FPN for predictions
2、訓練基於anchor box的檢測器,採用Hourglass network,每一hourglass module在上採樣階段預測anchor box,採用中間監督。
結果如表4所示,說明hourglass+corners這種搭配最好。採用FPN爲什麼這麼差,可能是因爲從頭訓練。

表 4: CornerNet依賴堆疊沙漏網絡

4.4.5 邊界框的質量

與RetinaNet,Cascade R-CNN和IoU-Net進行比較,如表5所示。在高IoU閾值0.9時,CornerNet比這些方法都好,說明如果要求高質量的檢測框,CornerNet具有優勢。【但是,在三維圖像中,工程實踐設置高IoU閾值是更少見的。即平時會採用低IoU閾值。】

表 5: CornerNet在高IoU閾值時具有優勢

4.4.6 誤差分析

CornerNet同時輸出heatmaps,offsets和embeddings,三者都影響檢測性能。
作者使用ground-truth values替換預測heatmaps和offsets,在驗證集上進行誤差分析。結果如表6所示,AP從38.4提升到73.1。
作者使用ground-truth offsets替換預測offsets,在驗證集上進行誤差分析。結果如表6所示,AP從38.4提升到86.1。
上面兩個實驗說明,可以提升的空間是巨大的。
如圖9所示,是一些不準確的結果。合併不同物體的邊界,對於不同物體預測相似的embeddings。

4.5 與世界一流檢測器比較

MS COCO test-dev
42.2%的mAP是採用了多尺度evaluation
結果如表7所示,在單尺度,CornerNet的性能超越RetinaNet800和RefineDet512,超越兩階段的Mask R-CNN。在多尺度,CornerNet的性能超越RefineDet512。APsAP^{s}=19.1表明CornerNet在檢測小物體時不具有較大的優勢。

表 7: CornerNet與各方法比較

5 結論

6 實踐

作者給出的PyTorch代碼是可以跑起來的。
模型大小接近800MB。【VGG也才500MB】
測試階段佔用顯存3.1GB。
在訓練階段中,看到需要一週(170/24=7.08),哭出來了。

    training loss at iteration 5: 434.4440002441406                                 
    training loss at iteration 10: 88.7403335571289                                 
    training loss at iteration 15: 38.28278732299805                                
    training loss at iteration 20: 22.313444137573242                               
    training loss at iteration 25: 10.515382766723633                               
    training loss at iteration 30: 9.795891761779785                                
      0%|                                   | 30/500000 [00:42<170:59:38,  1.23s/it]

6.1 Compiling Corner Pooling Layers

打開setup.py文件。

from setuptools import setup # 導入setuptools裏面的setup函數
from torch.utils.cpp_extension import BuildExtension, CppExtension
# 導入BuildExtension和CppExtension
# torch.utils.cpp_extension裏面還有CUDAExtension

setup(
    name="cpools",
    ext_modules=[
        CppExtension("top_pool", ["src/top_pool.cpp"]),
        CppExtension("bottom_pool", ["src/bottom_pool.cpp"]),
        CppExtension("left_pool", ["src/left_pool.cpp"]),
        CppExtension("right_pool", ["src/right_pool.cpp"])
    ],
    cmdclass={
        "build_ext": BuildExtension
    }
)

去了解setuptools,codedocs。一些參數的說明參考資料[4]。
去了解torch.utils.cpp_extension

6.1.1 以top_pool.cpp爲例閱讀和理解角點池化

閱讀如下代碼,分成三部分:
1、前向傳播
2、反向傳播
3、利用pybind11將C++和Python綁定起來,詳細瞭解參考資料[5]

top_pool.cpp的代碼如下:

#include <torch/torch.h>
// https://github.com/pytorch/pytorch/tree/master/torch/csrc/api/include/torch
#include <vector>

std::vector<at::Tensor> top_pool_forward( // 前向傳播
    at::Tensor input // 輸入
) {
    // Initialize output
    at::Tensor output = at::zeros_like(input); // 輸出,初始化爲0值

    // Get height
    int64_t height = input.size(2); // input爲[n, c, h, w],取得h的大小
    								//如果改爲三維,可能修改這裏,因爲[n, c, d, h, w]

    // Copy the last column
    at::Tensor input_temp  = input.select(2, height - 1);
    // https://pytorch.org/docs/0.4.1/tensors.html#torch.Tensor.select
    // select(dim, index) → Tensor
    // Slices the self tensor along the selected dimension at the given index.
    // 這裏我覺得拿出的是最後一行。就是假如輸入[1,3,3,5],input_temp就是[1,3,5]。可能是C++和Python索引不同。
    // 拿出/選擇最後一列,tensor[:,:,height-1,:]
    at::Tensor output_temp = output.select(2, height - 1);
    // 拿出/選擇最後一列,tensor[:,:,height-1,:]
    output_temp.copy_(input_temp);
    // https://pytorch.org/docs/0.4.1/tensors.html#torch.Tensor.copy_
    // copy_(src, non_blocking=False) → Tensor
    // Copies the elements from src into self tensor and returns self.

    at::Tensor max_temp;
    for (int64_t ind = 1; ind < height; ++ind) {
        input_temp  = input.select(2, height - ind - 1);
        // from bottom to top
        // 從倒數第二行開始
        output_temp = output.select(2, height - ind);
        // from bottom to top
        // 從倒數第一行開始
        max_temp    = output.select(2, height - ind - 1);
        // from bottom to top
        // 從倒數第二行開始
        at::max_out(max_temp, input_temp, output_temp);
        // https://pytorch.org/cppdocs/api/function_namespaceat_1ab40751edb25d9ed68d4baa5047564a89.html
        // static Tensor &at::max_out(Tensor &out, const Tensor &self, const Tensor &other)
        // max_temp是從output的倒數第二列開始的,所以一開始爲0;
        // input_temp爲倒數第二列,其值不爲0;
        // output_temp爲倒數第一列,其值不爲0;
        // input_temp和moutput_temp進行比較,保留較大者,影響output,形象的計算過程如Fig. 6
        
    }

    return { 
        output // 返回top-left點的top特徵圖,還需要left_pool.cpp計算出來的left特徵圖
    };
}

std::vector<at::Tensor> top_pool_backward( // 反向傳播
    at::Tensor input, // 輸入
    at::Tensor grad_output // 梯度
) {
    auto output = at::zeros_like(input); // 輸出,初始化爲0值

    int32_t batch   = input.size(0); // 樣本數
    int32_t channel = input.size(1); // 通道數
    int32_t height  = input.size(2); // 高的大小
    int32_t width   = input.size(3); // 寬的大小

    auto max_val = at::zeros(torch::CUDA(at::kFloat), {batch, channel, width}); // 最大值, float
    auto max_ind = at::zeros(torch::CUDA(at::kLong),  {batch, channel, width}); // 最大值的索引, longint

    auto input_temp = input.select(2, height - 1); // 選擇最後一列
    max_val.copy_(input_temp);
    // Copies the elements from src into self tensor and returns self.

    max_ind.fill_(height - 1);
    // Fills self tensor with the specified value. 即最後一列的位置

    auto output_temp      = output.select(2, height - 1); // 選擇最後一列
    auto grad_output_temp = grad_output.select(2, height - 1); // 選擇最後一列
    output_temp.copy_(grad_output_temp);
    // Copies the elements from src into self tensor and returns self.

    auto un_max_ind = max_ind.unsqueeze(2);
    // Returns a new tensor with a dimension of size one inserted at the specified position.
    auto gt_mask    = at::zeros(torch::CUDA(at::kByte),  {batch, channel, width});
    auto max_temp   = at::zeros(torch::CUDA(at::kFloat), {batch, channel, width});
    for (int32_t ind = 1; ind < height; ++ind) {
        input_temp = input.select(2, height - ind - 1);
        // 倒數第二列開始
        at::gt_out(gt_mask, input_temp, max_val); 
        // https://pytorch.org/docs/stable/torch.html#torch.gt
        // input_temp爲倒數第二列
        // max_val爲倒數第一列

        at::masked_select_out(max_temp, input_temp, gt_mask);
        // https://pytorch.org/docs/stable/torch.html#torch.masked_select
        // Returns a new 1-D tensor which indexes the input tensor according to the binary mask mask which is a ByteTensor.
        // 按照gt_mask取出input_temp的值,給max_temp
        max_val.masked_scatter_(gt_mask, max_temp);
        // https://pytorch.org/docs/stable/tensors.html#torch.Tensor.masked_scatter_
        // Copies elements from source into self tensor at positions where the mask is one. 
        max_ind.masked_fill_(gt_mask, height - ind - 1);
        // https://pytorch.org/docs/stable/tensors.html#torch.Tensor.masked_fill_
        // Fills elements of self tensor with value where mask is one.
        // gt_mask的值爲1的位置,賦值hetiht - ind - 1,即倒數第二列

        grad_output_temp = grad_output.select(2, height - ind - 1).unsqueeze(2);
        output.scatter_add_(2, un_max_ind, grad_output_temp);
        // https://pytorch.org/docs/stable/tensors.html#torch.Tensor.scatter_add_
        // scatter_add_(dim, index, other) → Tensor
        // 要理解這部分
    }

    return {
        output
    };
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { // 後來在Python裏面,要import TORCH_EXTENSION_NAME
    m.def(
        "forward", &top_pool_forward, "Top Pool Forward", // TORCH_EXTENSION_NAME.forward
        py::call_guard<py::gil_scoped_release>()
    );
    m.def(
        "backward", &top_pool_backward, "Top Pool Backward", // TORCH_EXTENSION_NAME.backward
        py::call_guard<py::gil_scoped_release>()
    );
}

理解scatter_add_:1、生成了隨機矩陣;2、生成了Mask矩陣;3、執行scatter_add_,看到y的0-2,結果self的位置就是1.7404-1.7953,1的位置爲1.000;再看到y的1-0,結果self的位置就是顛倒了1.2009-1.0427,2的位置爲1.000;再看到y的2-0,結果self的位置就是顛倒了1.9154-1.6480,1的位置爲1.000;再看y的0-1,結果self的位置就是順着的,2的位置爲1.000;最後看到y的0-2,跟之前的一樣。規律是比較清楚了。

    >>> x = torch.rand(2, 5)
    >>> x
    tensor([[0.7404, 0.0427, 0.6480, 0.3806, 0.8328],
            [0.7953, 0.2009, 0.9154, 0.6782, 0.9620]])
    >>> y = torch.tensor([[0, 1, 2, 0, 0], [2, 0, 0, 1, 2]])
    >>> y
    tensor([[0, 1, 2, 0, 0],
    		[2, 0, 0, 1, 2]])
    >>> torch.ones(3, 5).scatter_add_(0, y, x)
    tensor([[1.7404, 1.2009, 1.9154, 1.3806, 1.8328],
            [1.0000, 1.0427, 1.0000, 1.6782, 1.0000],
            [1.7953, 1.0000, 1.6480, 1.0000, 1.9620]])

6.2 Compiling NMS

主要看Makefile,setup.py和nms.pyx。
.pyx是Cpython的文件擴展名,Cpython的介紹。nms.pyx是RBG大神寫的,裏面包含馬里蘭大學提出的soft-nms。
將nms和soft-nms改寫爲3D,然後make。

6.3 Installing MS COCO APIs

安裝python版本的MS COCO API,用於解析標註。
主要看PythonAPI裏面的Makefile、setup.py和pycocotools裏面的文件。顯然,需要看看maskApi.c和_mask.pyx。

from setuptools import setup, Extension
import numpy as np

# To compile and install locally run "python setup.py build_ext --inplace"
# To install library to Python site-packages run "python setup.py build_ext install"

    ext_modules = [
        Extension(
            'pycocotools._mask',
            sources=['../common/maskApi.c', 'pycocotools/_mask.pyx'],
            include_dirs = [np.get_include(), '../common'],
            extra_compile_args=['-Wno-cpp', '-Wno-unused-function', '-std=c99'],
        )
    ]
    
    setup(
        name='pycocotools',
        packages=['pycocotools'],
        package_dir = {'pycocotools': 'pycocotools'},
        install_requires=[
            'setuptools>=18.0',
            'cython>=0.27.3',
            'matplotlib>=2.1.0'
        ],
        version='2.0',
        ext_modules= ext_modules
    )

[1] CornerNet Detecting Objects as Paired Keypoints ECCV 2018 [paper] [PyTorch code]
[2] Associative Embedding End-to-End Learning for Joint Detection and Grouping NIPS 2017 [paper] [PyTorch code]
[3] Stacked Hourglass Networks for Human Pose Estimation ECCV 2016 [paper]
[4] Python 庫打包分發(setup.py 編寫)簡易指南 [link]
[5] 使用pybind11 將C++代碼編譯爲python模塊 [link]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章