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(嵌入向量)。
CornerNet的另一個要素是角點池化,如圖2所示。總之,角點通常在目標之外,需要藉助其它信息來定位角點,而非使用角點附近的信息。
作者給出了檢測角點比檢測包圍框好的兩點假設性理由。其一,包圍盒的中心需要目標四邊,而角點需要兩邊。其二,更有效,可用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的物體。
3.2 檢測角點
變種focal loss:
fast r-cnn裏面的smooth L1 loss:
3.3 組合角點
組合角點的“pull”(拉近)損失:
分離角點的“push”(推遠)損失:
3.4 角點池化(Corner Pooling)
如圖6所示,非常清楚地解釋瞭如何進行角點池化,以top-left點爲例。在實現中,詳細閱讀作者的cpp代碼。
1、水平方向,從右到左,相鄰兩列進行比較,保留較大者。
2、垂直方法,從底向頂,相鄰兩行進行比較,保留較大者。
3、上面兩步得到的特徵圖交疊的部分進行add相加操作。
如圖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 訓練細節
總的訓練損失:
=0.1
=0.1
=1
4.2 測試細節
在corner heatmaps上進行最大值池化。
original和flipped圖像都進行了測試,然後採用soft-nms。
平均預測時間244ms,採用設備爲Titan X。
4.3 MS COCO
trainval2014用於訓練
minival2014用於驗證
testdev2017用於測試(沒有金標準,上傳服務器進行評價)
4.4 消融實驗
4.4.1 角點池化
結果如表1所示。角點池化提升2.0個點。
4.4.2 角點池化在較大區域的穩定性
結果如表3所示。角點池化提升2.8個點。
4.4.3 減小負樣本位置懲罰
對中型和大型物體具有好處。對小型物體卻具有壞處。
結果如表2所示。
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.4.5 邊界框的質量
與RetinaNet,Cascade R-CNN和IoU-Net進行比較,如表5所示。在高IoU閾值0.9時,CornerNet比這些方法都好,說明如果要求高質量的檢測框,CornerNet具有優勢。【但是,在三維圖像中,工程實踐設置高IoU閾值是更少見的。即平時會採用低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。=19.1表明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,code和docs。一些參數的說明參考資料[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]