編輯:好困 LRS
【新智元導(dǎo)讀】今天給大家安利一個(gè)寶藏倉(cāng)庫(kù)miemiedetection ,該倉(cāng)庫(kù)集合了PPYOLO、PPYOLOv2、PPYOLOE三個(gè)算法pytorch實(shí)現(xiàn)三合一,其中的PPYOLOv2和PPYOLO算法剛剛支持了導(dǎo)出ncnn。
眾所周知,PPYOLO和PPYOLOv2的導(dǎo)出部署非常困難,因?yàn)樗鼈兪褂昧丝勺冃?span id="3433uzh" class="wpcom_tag_link">卷積、MatrixNMS等對(duì)部署不太友好的算子。
而作者在ncnn中實(shí)現(xiàn)了可變形卷積DCNv2、CoordConcat、PPYOLO Decode MatrixNMS等自定義層,使得使用ncnn部署PPYOLO和PPYOLOv2成為了可能。其中的可變形卷積層也已經(jīng)被合入ncnn官方倉(cāng)庫(kù)。
在ncnn中對(duì)圖片預(yù)處理時(shí),先將圖片從BGR格式轉(zhuǎn)成RGB格式,然后用cv2.INTER_CUBIC方式將圖片插值成640×640的大小,再使用相同的均值和標(biāo)準(zhǔn)差對(duì)圖片進(jìn)行歸一化。以上全部與原版PPYOLOv2一樣,從而確保了C++端和python端輸入神經(jīng)網(wǎng)絡(luò)的圖片張量是完全一樣的。
最后,ncnn的輸出與miemiedetection的輸出對(duì)比如下圖所示:
其中,右邊是miemiedetection的輸出,為ppyolov2_r50vd_365e.pth這個(gè)模型預(yù)測(cè)的結(jié)果。在miemiedetection根目錄下輸入以下內(nèi)容即可得到。
python tools/demo.py image -f exps/ppyolo/ppyolov2_r50vd_365e.py -c ppyolov2_r50vd_365e.pth –path assets/000000013659.jpg –conf 0.15 –tsize 640 –save_result –device gpu
左邊則是ncnn相同的模型ppyolov2_r50vd_365e的結(jié)果,ncnn的運(yùn)算結(jié)果與pytorch有細(xì)微差別,影響不大。
pytorch直接轉(zhuǎn)ncnn
讀了一部分ncnn的源碼,確保對(duì) *.bin 和 *.param 文件充分了解之后,封裝了1個(gè)工具ncnn_utils,源碼位于miemiedetection的mmdet/models/ncnn_utils.py,它支持寫(xiě)一次前向傳播就能導(dǎo)出ncnn使用的 *.bin 和 *.param 文件,你只需給每個(gè)pytorch層增加1個(gè)export_ncnn()方法,export_ncnn()方法幾乎只要照抄farward()方法就能把模型導(dǎo)出到ncnn。
以下是ncnn_utils工具的使用示例:
是不是很牛x?你只要照著farward()方法寫(xiě),在export_ncnn()方法里用ncnn_utils的api寫(xiě)一次前向傳播就能把pytorch模型導(dǎo)出到ncnn。
在這個(gè)示例中,我展示了如何將resnet中使用的ConvNormLayer層導(dǎo)出到ncnn,ConvNormLayer層里包含了卷積層、bn層、激活層(當(dāng)self.dcn_v2==False),或者是卷積層、可變形卷積層、bn層、激活層(當(dāng)self.dcn_v2==True)。
為了提升ncnn的推理速度,我將卷積層(可變形卷積層)和bn層合并,另外,當(dāng)激活函數(shù)是relu、leakyrelu、clip、sigmoid、mish、hardswish這些時(shí),還可以將激活層合并到卷積層當(dāng)中,這樣就將3個(gè)層合并成了1個(gè)層,大大提高推理速度。
可變形卷積
卷積層可以視為可變形卷積在offset==0,mask==1時(shí)的特例。
一個(gè)形狀為[in_c, h, w]的特征圖inputs,經(jīng)過(guò)普通卷積層(卷積核形狀是[num_output, in_c, kernel_h, kernel_w],w方向的步長(zhǎng)、相鄰卷積采樣點(diǎn)的距離、卷積步長(zhǎng)、左填充、右填充分別是kernel_w、dilation_w、stride_w、pad_left、pad_right,h方向的步長(zhǎng)、相鄰卷積采樣點(diǎn)的距離、卷積步長(zhǎng)、上填充、下填充分別是kernel_h、dilation_h、stride_h、pad_top、pad_bottom)后,得到的特征圖形狀是[num_output, out_h, out_w],其中out_h = (h + pad_top + pad_bottom – dilation_h * (kernel_h – 1) + 1) / stride_h + 1,out_w = (w + pad_left + pad_right – dilation_w * (kernel_w – 1) + 1) / stride_w + 1。
一個(gè)形狀為[in_c, h, w]的特征圖inputs,經(jīng)過(guò)可變形卷積層(卷積核形狀是[num_output, in_c, kernel_h, kernel_w],w方向的步長(zhǎng)、相鄰卷積采樣點(diǎn)的距離、卷積步長(zhǎng)、左填充、右填充分別是kernel_w、dilation_w、stride_w、pad_left、pad_right,h方向的步長(zhǎng)、相鄰卷積采樣點(diǎn)的距離、卷積步長(zhǎng)、上填充、下填充分別是kernel_h、dilation_h、stride_h、pad_top、pad_bottom)后,得到的特征圖形狀也是[num_output, out_h, out_w],其中out_h = (h + pad_top + pad_bottom – dilation_h * (kernel_h – 1) + 1) / stride_h + 1,out_w = (w + pad_left + pad_right – dilation_w * (kernel_w – 1) + 1) / stride_w + 1。
但不同的是在可變形卷積層之前,inputs需要經(jīng)過(guò)一個(gè)普通卷積層,獲得可變形卷積需要的offset和mask,offset和mask的形狀分別是[kernel_h * kernel_w * 2, out_h, out_w]、[kernel_h * kernel_w, out_h, out_w]。為什么是這個(gè)形狀呢?
我們知道,inputs經(jīng)過(guò)卷積層,卷積窗是不是滑動(dòng)了out_h * out_w次?是的,因?yàn)槊恳恍芯矸e窗滑動(dòng)了out_w次,每一列卷積窗滑動(dòng)了out_h次,所以總共滑動(dòng)了out_h * out_w次。
此外,卷積采樣點(diǎn)是不是有kernel_h * kernel_w個(gè)?
是的,offset表示的是卷積窗停留在每一個(gè)位置的時(shí)候,每個(gè)卷積采樣點(diǎn)的偏移(有y、x兩個(gè)坐標(biāo)),所以offset的形狀是[kernel_h * kernel_w * 2, out_h, out_w]。
但是,offset是浮點(diǎn)數(shù),你怎么取原圖inputs里的像素?雙線(xiàn)性插值!對(duì)采樣點(diǎn)的x、y坐標(biāo)分別進(jìn)行上取整和下取整,得到最近的4個(gè)采樣點(diǎn)的坐標(biāo),然后將4個(gè)采樣點(diǎn)的像素進(jìn)行雙線(xiàn)性插值,得到所求的像素val。
mask是0到1之間的值(進(jìn)入可變形卷積層之前會(huì)經(jīng)過(guò)sigmoid層),表示的是每個(gè)val的重要程度,所以它的形狀是[kernel_h * kernel_w, out_h, out_w]。
offset和mask會(huì)和inputs一起進(jìn)入可變形卷積層參與后續(xù)計(jì)算。
「talk is cheap, show me the code」,我們來(lái)看一下ncnn中可變形卷積的代碼!
…#include “deformableconv2d.h”#include “fused_activation.h”namespace ncnn {DeformableConv2D::DeformableConv2D(){ one_blob_only = false; support_inplace = false;}int DeformableConv2D::load_param(const ParamDict& pd){ num_output = pd.get(0, 0); kernel_w = pd.get(1, 0); kernel_h = pd.get(11, kernel_w); dilation_w = pd.get(2, 1); dilation_h = pd.get(12, dilation_w); stride_w = pd.get(3, 1); stride_h = pd.get(13, stride_w); pad_left = pd.get(4, 0); pad_right = pd.get(15, pad_left); pad_top = pd.get(14, pad_left); pad_bottom = pd.get(16, pad_top); bias_term = pd.get(5, 0); weight_data_size = pd.get(6, 0); activation_type = pd.get(9, 0); activation_params = pd.get(10, Mat()); return 0;}int DeformableConv2D::load_model(const ModelBin& mb){ weight_data = mb.load(weight_data_size, 0); if (weight_data.empty()) return -100; if (bias_term) { bias_data = mb.load(num_output, 1); if (bias_data.empty()) return -100; } return 0;}int DeformableConv2D::forward(const std::vector& bottom_blobs, std::vector& top_blobs, const Option& opt) const{ const Mat& bottom_blob = bottom_blobs[0]; const Mat& offset = bottom_blobs[1]; const bool has_mask = (bottom_blobs.size() == 3); const int w = bottom_blob.w; const int h = bottom_blob.h; const int in_c = bottom_blob.c; const size_t elemsize = bottom_blob.elemsize; const int kernel_extent_w = dilation_w * (kernel_w – 1) + 1; const int kernel_extent_h = dilation_h * (kernel_h – 1) + 1; const int out_w = (w + pad_left + pad_right – kernel_extent_w) / stride_w + 1; const int out_h = (h + pad_top + pad_bottom – kernel_extent_h) / stride_h + 1; // output.shape is [num_output, out_h, out_w] (in python). Mat& output = top_blobs[0]; output.create(out_w, out_h, num_output, elemsize, opt.blob_allocator); if (output.empty()) return -100; const float* weight_ptr = weight_data; const float* bias_ptr = weight_data; if (bias_term) bias_ptr = bias_data; // deformable conv #pragma omp parallel for num_threads(opt.num_threads) for (int h_col = 0; h_col < out_h; h_col++) { for (int w_col = 0; w_col < out_w; w_col++) { int h_in = h_col * stride_h – pad_top; int w_in = w_col * stride_w – pad_left; for (int oc = 0; oc < num_output; oc++) { float sum = 0.f; if (bias_term) sum = bias_ptr[oc]; for (int i = 0; i -1 && h_im = 0); v2_cond = (h_low >= 0 && w_high <= w – 1); v3_cond = (h_high = 0); v4_cond = (h_high <= h – 1 && w_high <= w – 1); w1 = hh * hw; w2 = hh * lw; w3 = lh * hw; w4 = lh * lw; } for (int c_im = 0; c_im < in_c; c_im++) { float val = 0.f; if (cond) { float v1 = v1_cond ? bottom_blob.channel(c_im).row(h_low)[w_low] : 0.f; float v2 = v2_cond ? bottom_blob.channel(c_im).row(h_low)[w_high] : 0.f; float v3 = v3_cond ? bottom_blob.channel(c_im).row(h_high)[w_low] : 0.f; float v4 = v4_cond ? bottom_blob.channel(c_im).row(h_high)[w_high] : 0.f; val = w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4; } sum += val * mask_ * weight_ptr[((oc * in_c + c_im) * kernel_h + i) * kernel_w + j]; } } } output.channel(oc).row(h_col)[w_col] = activation_ss(sum, activation_type, activation_params); } } } return 0;}} // namespace ncnn
forward()函數(shù)即可變形卷積的前向代碼,bottom_blobs是可變形卷積的輸入,當(dāng)bottom_blobs里有3個(gè)輸入時(shí),分別是inputs、offset、mask,表示是DCNv2,當(dāng)bottom_blobs里有2個(gè)輸入時(shí),分別是inputs、offset,表示是DCNv1。
接下來(lái)的代碼,我計(jì)算了out_h、out_w,即輸出特征圖的高度和寬度。接下來(lái)是對(duì)輸出張量output開(kāi)辟空間,獲取可變形卷積層的權(quán)重、偏置的指針weight_ptr、bias_ptr。最后進(jìn)入for循環(huán)。
第1個(gè)for循環(huán)表示的是卷積窗在h方向滑動(dòng),滑了out_h次。
第2個(gè)for循環(huán)表示的是卷積窗在w方向滑動(dòng),滑了out_w次;之后計(jì)算的h_in、w_in分別表示當(dāng)前卷積窗位置左上角采樣點(diǎn)在pad之后的inputs的y坐標(biāo)、x坐標(biāo)(實(shí)際上inputs不需要pad,之后你會(huì)看到,采樣點(diǎn)超出inputs的范圍時(shí),采樣得到的像素強(qiáng)制取0)。
第3個(gè)for循環(huán)表示的是填寫(xiě)輸出特征圖的每一個(gè)通道,填了num_output次;首先讓sum=0,當(dāng)使用偏置時(shí),sum=bias_ptr[oc],即第oc個(gè)偏置。
第4、第5、第6個(gè)for循環(huán)遍歷了卷積核的高度、寬度、通道數(shù),計(jì)算卷積層權(quán)重weight每個(gè)卷積采樣點(diǎn)每個(gè)通道和原圖inputs相應(yīng)位置的像素val(雙線(xiàn)性插值得到)和積,再累加到sum中。offset_h、offset_w是當(dāng)前卷積采樣點(diǎn)的y、x偏移,mask_是雙線(xiàn)性插值得到的val的重要程度。
真正采樣位置的y坐標(biāo)是h_im = 當(dāng)前卷積窗左上角y坐標(biāo)h_in + 卷積核內(nèi)部y偏移i * dilation_h + y偏移offset_h;真正采樣位置的x坐標(biāo)是w_im = 當(dāng)前卷積窗左上角x坐標(biāo)w_in + 卷積核內(nèi)部x偏移j * dilation_w + x偏移offset_w。
之后,計(jì)算好雙線(xiàn)性插值中h_im、w_im上下取整的結(jié)果h_low、w_low、h_high、w_high,雙線(xiàn)性插值中4個(gè)像素的權(quán)重w1、w2、w3、w4等。注意,不要在for (int c_im = 0; c_im < in_c; c_im++){}中計(jì)算,因?yàn)樵诿恳粋€(gè)輸入通道中,采樣位置h_im、w_im是相等的,所以h_low、w_low、h_high、w_high、w1、w2、w3、w4也是相等的,提前計(jì)算好就不用在每個(gè)輸入通道重復(fù)計(jì)算,提高計(jì)算速度和算法效率。
第6個(gè)for循環(huán)中,遍歷每個(gè)輸入通道,求采樣得到像素val,如果采樣位置超出inputs的范圍,取0;對(duì)比cond和v1_cond、v2_cond、v3_cond、v4_cond,會(huì)發(fā)現(xiàn)cond的邊界會(huì)比v1_cond、v2_cond、v3_cond、v4_cond的邊界大一點(diǎn),比如當(dāng)h_im==-1且w_im==-1時(shí), cond是true。
這是因?yàn)椋琱_im和w_im會(huì)經(jīng)過(guò)上下取整,其中上取整得到的采樣點(diǎn)位置是(0, 0),剛好是在inputs范圍內(nèi),所以cond的邊界會(huì)比v1_cond、v2_cond、v3_cond、v4_cond的邊界大一點(diǎn)。
計(jì)算好val之后,將val * mask_ * weight_ptr[((oc * in_c + c_im) * kernel_h + i) * kernel_w + j]累加到sum之中。
PPYOLOv2輸出解碼
PPYOLOv2輸出解碼比YOLOv3復(fù)雜一些,它使用了iou_aware和Grid Sensitive。
在YOLOv3中,輸出3個(gè)特征圖,表示3種感受野(大中?。┑念A(yù)測(cè)結(jié)果,每個(gè)特征圖的每個(gè)格子輸出3個(gè)bbox,對(duì)應(yīng)3個(gè)聚類(lèi)出來(lái)的anchor進(jìn)行解碼。
當(dāng)數(shù)據(jù)集類(lèi)別數(shù)是80時(shí)候,YOLOv3每個(gè)特征圖通道數(shù)是3 * (4+1+80),3表示每個(gè)格子輸出3個(gè)bbox,4表示未解碼的xywh,1表示未解碼的objness,80表示80個(gè)類(lèi)別未解碼的條件概率。PPYOLOv2使用了iou_aware,每個(gè)特征圖通道數(shù)是3 * (1+4+1+80),即每個(gè)bbox多出1個(gè)ioup屬性。共有258個(gè)通道,但是前3個(gè)通道才是每個(gè)bbox的ioup,后255個(gè)通道和YOLOv3的排列一樣。
通過(guò)閱讀IouAwareLoss的代碼,ioup使用F.binary_cross_entropy_with_logits()訓(xùn)練,解碼時(shí)需要用sigmoid()激活,使用當(dāng)前預(yù)測(cè)框和它所學(xué)習(xí)的gt的iou作為監(jiān)督信息,所以ioup其實(shí)預(yù)測(cè)的是當(dāng)前預(yù)測(cè)框和它所學(xué)習(xí)的gt的iou。所以,當(dāng)然是希望ioup越大越好。
在mmdet(ppdet)中,用了1條曲線(xiàn)救國(guó)的道路對(duì)輸出解碼:
# mmdet/models/heads/yolov3_head.py… if self.iou_aware: na = len(self.anchors[i]) ioup, x = out[:, 0:na, :, :], out[:, na:, :, :] b, c, h, w = x.shape no = c // na x = x.reshape((b, na, no, h * w)) ioup = ioup.reshape((b, na, 1, h * w)) obj = x[:, :, 4:5, :] ioup = torch.sigmoid(ioup) obj = torch.sigmoid(obj) obj_t = (obj**(1 – self.iou_aware_factor)) * ( ioup**self.iou_aware_factor) obj_t = _de_sigmoid(obj_t) loc_t = x[:, :, :4, :] cls_t = x[:, :, 5:, :] y_t = torch.cat([loc_t, obj_t, cls_t], 2) out = y_t.reshape((b, c, h, w)) box, score = paddle_yolo_box(out, self._anchors[self.anchor_masks[i]], self.downsample[i], self.num_classes, self.scale_x_y, im_size, self.clip_bbox, conf_thresh=self.nms_cfg[‘score_threshold’])
即分別對(duì)ioup和obj進(jìn)行sigmoid激活,再obj_t = (obj ** (1 – self.iou_aware_factor)) * (ioup ** self.iou_aware_factor)作為新的obj,新的obj經(jīng)過(guò)sigmoid的反函數(shù)還原成未接碼狀態(tài),未接碼的新obj貼回x中。最后out的通道數(shù)是255,只要像原版YOLOv3那樣解碼out就行了。
這么做的原因是paddle_yolo_box()的作用是對(duì)原版YOLOv3的輸出進(jìn)行解碼,充分利用paddle_yolo_box()的話(huà)就不用自己寫(xiě)解碼的代碼。所以就走了曲線(xiàn)救國(guó)的道路。
從中我們可以得到一些信息,ioup只不過(guò)是和obj經(jīng)過(guò)表達(dá)式obj_t = (obj ** (1 – self.iou_aware_factor)) * (ioup ** self.iou_aware_factor)得到新的obj,其余只要像YOLOv3一樣解碼就ok了!
所以在ncnn中,我這樣實(shí)現(xiàn)PPYOLOv2的解碼:
// examples/test2_06_ppyolo_ncnn.cpp…class PPYOLODecodeMatrixNMS : public ncnn::Layer{public: PPYOLODecodeMatrixNMS() { // miemie2013: if num of input tensors > 1 or num of output tensors > 1, you must set one_blob_only = false // And ncnn will use forward(const std::vector& bottom_blobs, std::vector& top_blobs, const Option& opt) method // or forward_inplace(std::vector& bottom_top_blobs, const Option& opt) method one_blob_only = false; support_inplace = false; } virtual int load_param(const ncnn::ParamDict& pd) { num_classes = pd.get(0, 80); anchors = pd.get(1, ncnn::Mat()); strides = pd.get(2, ncnn::Mat()); scale_x_y = pd.get(3, 1.f); iou_aware_factor = pd.get(4, 0.5f); score_threshold = pd.get(5, 0.1f); anchor_per_stride = pd.get(6, 3); post_threshold = pd.get(7, 0.1f); nms_top_k = pd.get(8, 500); keep_top_k = pd.get(9, 100); kernel = pd.get(10, 0); gaussian_sigma = pd.get(11, 2.f); return 0; } virtual int forward(const std::vector& bottom_blobs, std::vector& top_blobs, const ncnn::Option& opt) const { const ncnn::Mat& bottom_blob = bottom_blobs[0]; const int tensor_num = bottom_blobs.size() – 1; const size_t elemsize = bottom_blob.elemsize; const ncnn::Mat& im_scale = bottom_blobs[tensor_num]; const float scale_x = im_scale[0]; const float scale_y = im_scale[1]; int out_num = 0; for (size_t b = 0; b < tensor_num; b++) { const ncnn::Mat& tensor = bottom_blobs[b]; const int w = tensor.w; const int h = tensor.h; out_num += anchor_per_stride * h * w; } ncnn::Mat bboxes; bboxes.create(4 * out_num, elemsize, opt.blob_allocator); if (bboxes.empty()) return -100; ncnn::Mat scores; scores.create(num_classes * out_num, elemsize, opt.blob_allocator); if (scores.empty()) return -100; float* bboxes_ptr = bboxes; float* scores_ptr = scores; // decode for (size_t b = 0; b < tensor_num; b++) { const ncnn::Mat& tensor = bottom_blobs[b]; const int w = tensor.w; const int h = tensor.h; const int c = tensor.c; const bool use_iou_aware = (c == anchor_per_stride * (num_classes + 6)); const int channel_stride = use_iou_aware ? (c / anchor_per_stride) – 1 : (c / anchor_per_stride); const int cx_pos = use_iou_aware ? anchor_per_stride : 0; const int cy_pos = use_iou_aware ? anchor_per_stride + 1 : 1; const int w_pos = use_iou_aware ? anchor_per_stride + 2 : 2; const int h_pos = use_iou_aware ? anchor_per_stride + 3 : 3; const int obj_pos = use_iou_aware ? anchor_per_stride + 4 : 4; const int cls_pos = use_iou_aware ? anchor_per_stride + 5 : 5; float stride = strides[b]; #pragma omp parallel for num_threads(opt.num_threads) for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { for (int k = 0; k score_threshold) { // Grid Sensitive float cx = static_cast(scale_x_y / (1.f + expf(-tensor.channel(cx_pos + k * channel_stride).row(i)[j])) + j – (scale_x_y – 1.f) * 0.5f); float cy = static_cast(scale_x_y / (1.f + expf(-tensor.channel(cy_pos + k * channel_stride).row(i)[j])) + i – (scale_x_y – 1.f) * 0.5f); cx *= stride; cy *= stride; float dw = static_cast(expf(tensor.channel(w_pos + k * channel_stride).row(i)[j]) * anchors[(b * anchor_per_stride + k) * 2]); float dh = static_cast(expf(tensor.channel(h_pos + k * channel_stride).row(i)[j]) * anchors[(b * anchor_per_stride + k) * 2 + 1]); float x0 = cx – dw * 0.5f; float y0 = cy – dh * 0.5f; float x1 = cx + dw * 0.5f; float y1 = cy + dh * 0.5f; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4] = x0 / scale_x; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 1] = y0 / scale_y; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 2] = x1 / scale_x; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 3] = y1 / scale_y; for (int r = 0; r < num_classes; r++) { float score = static_cast(obj / (1.f + expf(-tensor.channel(cls_pos + k * channel_stride + r).row(i)[j]))); scores_ptr[((i * w + j) * anchor_per_stride + k) * num_classes + r] = score; } }else { bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4] = 0.f; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 1] = 0.f; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 2] = 1.f; bboxes_ptr[((i * w + j) * anchor_per_stride + k) * 4 + 3] = 1.f; for (int r = 0; r < num_classes; r++) { scores_ptr[((i * w + j) * anchor_per_stride + k) * num_classes + r] = -1.f; } } } } } bboxes_ptr += h * w * anchor_per_stride * 4; scores_ptr += h * w * anchor_per_stride * num_classes; }…
只要在obj那里動(dòng)手腳,其余像YOLOv3那樣解碼就行了,而且,只對(duì)obj > score_threshold的bbox解碼,其余bbox敷衍處理,提升后處理速度。
Grid Sensitive的提出是為了解決訓(xùn)練過(guò)程中g(shù)t中心點(diǎn)落在格子線(xiàn)上的問(wèn)題,它允許解碼后的x、y超出0~1的范圍一點(diǎn)點(diǎn)。
MatrixNMS
MatrixNMS為實(shí)例分割SOLO中提出的nms算法,原版MatrixNMS非常巧妙地通過(guò)一個(gè)矩陣乘法求掩碼兩兩之間的iou,只需將求掩碼兩兩之間的iou改成求預(yù)測(cè)框兩兩之間的iou,即可將MatrixNMS應(yīng)用于目標(biāo)檢測(cè)算法的后處理。
MatrixNMS的優(yōu)點(diǎn)是不用設(shè)置nms_iou這個(gè)比較敏感的超參數(shù);以及,理論速度比multiclass_nms快,因?yàn)樗昧司仃嚦朔ㄇ笱诖a兩兩之間的iou,矩陣乘法可用gpu并行高速計(jì)算;multiclass_nms對(duì)每個(gè)類(lèi)別會(huì)選出1個(gè)得分最高的預(yù)測(cè)框(該預(yù)測(cè)框肯定會(huì)保留下來(lái)),然后分別與得分比它低的同類(lèi)預(yù)測(cè)框計(jì)算iou,iou高于nms_iou的將會(huì)被舍棄,然后進(jìn)行第二次迭代,從剩余的預(yù)測(cè)框里再次選出得分最高的,重復(fù)上述過(guò)程。
multiclass_nms需要進(jìn)行多次迭代,每一次迭代依賴(lài)于上一次迭代,無(wú)法做到并行,因?yàn)槟悴荒芴崆邦A(yù)知哪個(gè)預(yù)測(cè)框會(huì)被保留。MatrixNMS就沒(méi)有這種迭代過(guò)程,其理論速度要快于multiclass_nms。
MatrixNMS采用了「減分」機(jī)制,對(duì)于每一個(gè)類(lèi)別的每一個(gè)預(yù)測(cè)框,如果和得分比它高的同類(lèi)預(yù)測(cè)框有iou(重疊),它的得分會(huì)被扣掉一些,之后,通過(guò)post_threshold分?jǐn)?shù)閾值過(guò)濾掉低分?jǐn)?shù)的預(yù)測(cè)框,剩下的就是最后的預(yù)測(cè)框了。
「talk is cheap, show me the code」,我們來(lái)看一下ncnn中MatrixNMS的代碼!
// examples/test2_06_ppyolo_ncnn.cpp…struct Bbox{ float x0; float y0; float x1; float y1; int clsid; float score;};bool compare_desc(Bbox bbox1, Bbox bbox2){ return bbox1.score > bbox2.score;}float calc_iou(Bbox bbox1, Bbox bbox2){ float area_1 = (bbox1.y1 – bbox1.y0) * (bbox1.x1 – bbox1.x0); float area_2 = (bbox2.y1 – bbox2.y0) * (bbox2.x1 – bbox2.x0); float inter_x0 = std::max(bbox1.x0, bbox2.x0); float inter_y0 = std::max(bbox1.y0, bbox2.y0); float inter_x1 = std::min(bbox1.x1, bbox2.x1); float inter_y1 = std::min(bbox1.y1, bbox2.y1); float inter_w = std::max(0.f, inter_x1 – inter_x0); float inter_h = std::max(0.f, inter_y1 – inter_y0); float inter_area = inter_w * inter_h; float union_area = area_1 + area_2 – inter_area + 0.000000001f; return inter_area / union_area;}…class PPYOLODecodeMatrixNMS : public ncnn::Layer{public:… virtual int forward(const std::vector& bottom_blobs, std::vector& top_blobs, const ncnn::Option& opt) const { … // keep bbox whose score > score_threshold std::vector bboxes_vec; for (int i = 0; i < out_num; i++) { float x0 = bboxes[i * 4]; float y0 = bboxes[i * 4 + 1]; float x1 = bboxes[i * 4 + 2]; float y1 = bboxes[i * 4 + 3]; for (int j = 0; j score_threshold) { Bbox bbox; bbox.x0 = x0; bbox.y0 = y0; bbox.x1 = x1; bbox.y1 = y1; bbox.clsid = j; bbox.score = score; bboxes_vec.push_back(bbox); } } } if (bboxes_vec.size() == 0) { ncnn::Mat& pred = top_blobs[0]; pred.create(0, 0, elemsize, opt.blob_allocator); if (pred.empty()) return -100; return 0; } // sort and keep top nms_top_k int nms_top_k_ = nms_top_k; if (bboxes_vec.size() nms_top_k) bboxes_vec.resize(nms_top_k); // ———————- Matrix NMS ———————- // calc a iou matrix whose shape is [n, n], n is bboxes_vec.size() int n = bboxes_vec.size(); float* decay_iou = new float[n * n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (j < i + 1) { decay_iou[i * n + j] = 0.f; }else { bool same_clsid = bboxes_vec[i].clsid == bboxes_vec[j].clsid; if (same_clsid) { float iou = calc_iou(bboxes_vec[i], bboxes_vec[j]); decay_iou[i * n + j] = iou; }else { decay_iou[i * n + j] = 0.f; } } } } // get max iou of each col float* compensate_iou = new float[n]; for (int i = 0; i < n; i++) { float max_iou = decay_iou[i]; for (int j = 0; j max_iou) max_iou = decay_iou[j * n + i]; } compensate_iou[i] = max_iou; } float* decay_matrix = new float[n * n]; // get min decay_value of each col float* decay_coefficient = new float[n]; if (kernel == 0) // gaussian { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { decay_matrix[i * n + j] = static_cast(expf(gaussian_sigma * (compensate_iou[i] * compensate_iou[i] – decay_iou[i * n + j] * decay_iou[i * n + j]))); } } }else if (kernel == 1) // linear { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { decay_matrix[i * n + j] = (1.f – decay_iou[i * n + j]) / (1.f – compensate_iou[i]); } } } for (int i = 0; i < n; i++) { float min_v = decay_matrix[i]; for (int j = 0; j < n; j++) { if (decay_matrix[j * n + i] < min_v) min_v = decay_matrix[j * n + i]; } decay_coefficient[i] = min_v; } for (int i = 0; i < n; i++) { bboxes_vec[i].score *= decay_coefficient[i]; } // ———————- Matrix NMS (end) ———————- std::vector bboxes_vec_keep; for (int i = 0; i post_threshold) { bboxes_vec_keep.push_back(bboxes_vec[i]); } } n = bboxes_vec_keep.size(); if (n == 0) { ncnn::Mat& pred = top_blobs[0]; pred.create(0, 0, elemsize, opt.blob_allocator); if (pred.empty()) return -100; return 0; } // sort and keep keep_top_k int keep_top_k_ = keep_top_k; if (n keep_top_k) bboxes_vec_keep.resize(keep_top_k); ncnn::Mat& pred = top_blobs[0]; pred.create(6 * n, elemsize, opt.blob_allocator); if (pred.empty()) return -100; float* pred_ptr = pred; for (int i = 0; i < n; i++) { pred_ptr[i * 6] = (float)bboxes_vec_keep[i].clsid; pred_ptr[i * 6 + 1] = bboxes_vec_keep[i].score; pred_ptr[i * 6 + 2] = bboxes_vec_keep[i].x0; pred_ptr[i * 6 + 3] = bboxes_vec_keep[i].y0; pred_ptr[i * 6 + 4] = bboxes_vec_keep[i].x1; pred_ptr[i * 6 + 5] = bboxes_vec_keep[i].y1; } pred = pred.reshape(6, n); return 0; }…
第一步,將得分超過(guò)score_threshold的預(yù)測(cè)框保存到bboxes_vec里,這是第一次分?jǐn)?shù)過(guò)濾;如果沒(méi)有預(yù)測(cè)框的得分超過(guò)score_threshold,直接返回1個(gè)形狀是(0, 0)的Mat代表沒(méi)有物體。
第二步,將bboxes_vec中的前nms_top_k個(gè)預(yù)測(cè)框按照得分降序排列,bboxes_vec中只保留前nms_top_k個(gè)預(yù)測(cè)框。
第三步,進(jìn)入MatrixNMS,設(shè)此時(shí)bboxes_vec里有n個(gè)預(yù)測(cè)框,我們計(jì)算一個(gè)n * n的矩陣decay_iou,下三角部分(包括對(duì)角線(xiàn))是0,表示的是bboxes_vec中的預(yù)測(cè)框兩兩之間的iou,而且,只計(jì)算同類(lèi)別預(yù)測(cè)框的iou,非同類(lèi)的預(yù)測(cè)框iou置為0;
接下來(lái)的代碼比較難以理解,我舉個(gè)例子說(shuō)明,比如經(jīng)過(guò)第一次分?jǐn)?shù)過(guò)濾和得分降序排列后,剩下編號(hào)為0、1、2的3個(gè)同類(lèi)的預(yù)測(cè)框,假設(shè)此時(shí)的decay_iou值為:
如果某個(gè)預(yù)測(cè)框與比它分高的同類(lèi)預(yù)測(cè)框有較高的iou,它應(yīng)該減去更多的分,這該怎么實(shí)現(xiàn)呢?
一個(gè)比較簡(jiǎn)單的做法是對(duì)矩陣1-decay_iou每一列求最小值,即對(duì)矩陣:
每一列求最小,得到衰減系數(shù)向量decay_coefficient=[1, 0.1, 0.2],然后每個(gè)bbox的得分再和衰減系數(shù)向量里相應(yīng)的值相乘,就實(shí)現(xiàn)減分的效果了!
比如0號(hào)預(yù)測(cè)框,它的得分應(yīng)該乘以1,這很好理解,它是得分最高的預(yù)測(cè)框,應(yīng)該被保留,不應(yīng)該減分。
對(duì)于1號(hào)預(yù)測(cè)框,它的得分應(yīng)該乘以0.1,這很好理解,它與0號(hào)預(yù)測(cè)框的iou高達(dá)0.9,應(yīng)該減去很多分。
對(duì)于2號(hào)預(yù)測(cè)框,它的得分應(yīng)該乘以0.2,這很好理解,它與1號(hào)預(yù)測(cè)框的iou高達(dá)0.8,應(yīng)該減去很多分。
但是這樣做真的正確嗎?
如果用multiclass_nms做nms算法,假設(shè)設(shè)定的nms_iou=0.6,第0次迭代,首先保留得分最高的0號(hào)預(yù)測(cè)框,發(fā)現(xiàn)1號(hào)預(yù)測(cè)框和0號(hào)預(yù)測(cè)框的iou高達(dá)0.9,所以舍棄1號(hào)預(yù)測(cè)框,發(fā)現(xiàn)2號(hào)預(yù)測(cè)框和0號(hào)預(yù)測(cè)框的iou是0.2,保留2號(hào)預(yù)測(cè)框;第1次迭代,首先保留得分最高的2號(hào)預(yù)測(cè)框,發(fā)現(xiàn)沒(méi)有預(yù)測(cè)框了,nms算法結(jié)束。所以最后保留的是0號(hào)預(yù)測(cè)框和2號(hào)預(yù)測(cè)框。
上面的分析中,僅僅是因?yàn)?號(hào)預(yù)測(cè)框與1號(hào)預(yù)測(cè)框的iou高達(dá)0.8,就讓2號(hào)預(yù)測(cè)框的分?jǐn)?shù)乘以0.2,是非常不正確的做法,因?yàn)?號(hào)預(yù)測(cè)框與0號(hào)預(yù)測(cè)框的iou高達(dá)0.9,1號(hào)預(yù)測(cè)框有很大概率是會(huì)被舍棄的,不能因?yàn)?號(hào)預(yù)測(cè)框與可能被舍棄的1號(hào)預(yù)測(cè)框的iou高達(dá)0.8,就讓2號(hào)預(yù)測(cè)框減去很多分。
那么怎么解決這個(gè)問(wèn)題呢?補(bǔ)償!
1-0.8沒(méi)有什么參考意義,我們應(yīng)該將它放大,可以讓它除以(1-0.9)實(shí)現(xiàn),0.9表示1號(hào)預(yù)測(cè)框與0號(hào)預(yù)測(cè)框的iou高達(dá)0.9,這樣逐列取最小的時(shí)候就可能取不到它了。而且,不應(yīng)該只有1號(hào)預(yù)測(cè)框與2號(hào)預(yù)測(cè)框這么做,預(yù)測(cè)框兩兩之間都應(yīng)該這么做。
我們看接下來(lái)的代碼,逐列取decay_iou的最大值得到補(bǔ)償向量compensate_iou,在這個(gè)示例中compensate_iou=[0, 0.9, 0.8],然后求一個(gè)n * n的矩陣decay_matrix,當(dāng)kernel == 1時(shí),是linear,它的計(jì)算公式是(1-decay_iou)矩陣的每一行元素都除以(1-compensate_iou的第i個(gè)值)(假設(shè)當(dāng)前行id是i),所以在這個(gè)示例中,decay_matrix的值是:
逐列取decay_matrix的最小值,即可得到decay_coefficient=[1, 0.1, 0.8],你看,2號(hào)預(yù)測(cè)框的得分應(yīng)該乘以0.8,是由于它和0號(hào)預(yù)測(cè)框的iou是0.2導(dǎo)致的,它減去的分?jǐn)?shù)就比較少。而此時(shí)1號(hào)預(yù)測(cè)框和2號(hào)預(yù)測(cè)框在decay_matrix中的值被補(bǔ)償(被放大)到2,參考意義不大,逐列取最小時(shí)取不到它。
現(xiàn)在你應(yīng)該能更好地理解代碼中decay_matrix的計(jì)算公式了嗎?
decay_matrix[i * n + j] = (1.f – decay_iou[i * n + j]) / (1.f – compensate_iou[i]);
第i個(gè)預(yù)測(cè)框和第j個(gè)預(yù)測(cè)框的iou是decay_iou[i * n + j],第i個(gè)預(yù)測(cè)框它覺(jué)得第j個(gè)預(yù)測(cè)框的衰減系數(shù)應(yīng)該是(1.f – decay_iou[i * n + j]),但是第i個(gè)預(yù)測(cè)框它覺(jué)得的就是對(duì)的嗎?
還要看第i個(gè)預(yù)測(cè)框是否被抑制,第i個(gè)預(yù)測(cè)框如果沒(méi)有被抑制,那么(1.f – decay_iou[i * n + j])就有參考意義,第i個(gè)預(yù)測(cè)框如果被抑制,那么(1.f – decay_iou[i * n + j])就沒(méi)有什么參考意義。
所以需要除以(1.f – compensate_iou[i])作為補(bǔ)償,compensate_iou[i]表示的是第i個(gè)預(yù)測(cè)框與比它分高的預(yù)測(cè)框的最高iou:
如果這個(gè)max_iou很大,衰減系數(shù)就會(huì)被放大,第i個(gè)預(yù)測(cè)框它覺(jué)得第j個(gè)預(yù)測(cè)框的衰減系數(shù)是xxx就沒(méi)什么參考意義;如果這個(gè)max_iou很小,衰減系數(shù)就會(huì)放大得很?。╩ax_iou==0時(shí)不放大),第i個(gè)預(yù)測(cè)框它覺(jué)得第j個(gè)預(yù)測(cè)框的衰減系數(shù)是xxx就有參考意義。
然后,逐列取decay_matrix的最小值,第j列的最小值應(yīng)該是decay_iou[i * n + j]越大越好、compensate_iou[i]越小越好的那個(gè)第i個(gè)預(yù)測(cè)框提供。
當(dāng)kernel == 0,也僅僅表示用其它的函數(shù)表示衰減系數(shù)和補(bǔ)償而已。所有的預(yù)測(cè)框的得分乘以decay_coefficient相應(yīng)的值實(shí)現(xiàn)減分,MatrixNMS結(jié)束。
第四步,將得分超過(guò)post_threshold的預(yù)測(cè)框保存到bboxes_vec_keep里,這是第二次分?jǐn)?shù)過(guò)濾;如果沒(méi)有預(yù)測(cè)框的得分超過(guò)post_threshold,直接返回1個(gè)形狀是(0, 0)的Mat代表沒(méi)有物體。
第五步,將bboxes_vec_keep中的前keep_top_k個(gè)預(yù)測(cè)框按照得分降序排列,bboxes_vec_keep中只保留前keep_top_k個(gè)預(yù)測(cè)框。
最后,寫(xiě)1個(gè)形狀是(n, 6)的Mat表示最終所有的預(yù)測(cè)框后處理結(jié)束。
如何導(dǎo)出
(1)第一步,在miemiedetection根目錄下輸入這些命令下載paddle模型:
wget https://paddledet.bj.bcebos.com/models/ppyolo_r50vd_dcn_2x_coco.pdparamswget https://paddledet.bj.bcebos.com/models/ppyolo_r18vd_coco.pdparamswget https://paddledet.bj.bcebos.com/models/ppyolov2_r50vd_dcn_365e_coco.pdparamswget https://paddledet.bj.bcebos.com/models/ppyolov2_r101vd_dcn_365e_coco.pdparams
(2)第二步,在miemiedetection根目錄下輸入這些命令將paddle模型轉(zhuǎn)pytorch模型:
python tools/convert_weights.py -f exps/ppyolo/ppyolo_r50vd_2x.py -c ppyolo_r50vd_dcn_2x_coco.pdparams -oc ppyolo_r50vd_2x.pth -nc 80python tools/convert_weights.py -f exps/ppyolo/ppyolo_r18vd.py -c ppyolo_r18vd_coco.pdparams -oc ppyolo_r18vd.pth -nc 80python tools/convert_weights.py -f exps/ppyolo/ppyolov2_r50vd_365e.py -c ppyolov2_r50vd_dcn_365e_coco.pdparams -oc ppyolov2_r50vd_365e.pth -nc 80python tools/convert_weights.py -f exps/ppyolo/ppyolov2_r101vd_365e.py -c ppyolov2_r101vd_dcn_365e_coco.pdparams -oc ppyolov2_r101vd_365e.pth -nc 80
(3)第三步,在miemiedetection根目錄下輸入這些命令將pytorch模型轉(zhuǎn)ncnn模型:
python tools/demo.py ncnn -f exps/ppyolo/ppyolo_r18vd.py -c ppyolo_r18vd.pth –ncnn_output_path ppyolo_r18vd –conf 0.15python tools/demo.py ncnn -f exps/ppyolo/ppyolo_r50vd_2x.py -c ppyolo_r50vd_2x.pth –ncnn_output_path ppyolo_r50vd_2x –conf 0.15python tools/demo.py ncnn -f exps/ppyolo/ppyolov2_r50vd_365e.py -c ppyolov2_r50vd_365e.pth –ncnn_output_path ppyolov2_r50vd_365e –conf 0.15python tools/demo.py ncnn -f exps/ppyolo/ppyolov2_r101vd_365e.py -c ppyolov2_r101vd_365e.pth –ncnn_output_path ppyolov2_r101vd_365e –conf 0.15
-c代表讀取的權(quán)重,–ncnn_output_path表示的是保存為NCNN所用的 *.param 和 *.bin 文件的文件名,–conf 0.15表示的是在PPYOLODecodeMatrixNMS層中將score_threshold和post_threshold設(shè)置為0.15,你可以在導(dǎo)出的 *.param 中修改score_threshold和post_threshold,分別是PPYOLODecodeMatrixNMS層的5=xxx 7=xxx屬性。
然后,下載ncnn_ppyolov2 這個(gè)倉(cāng)庫(kù)(它自帶了glslang和實(shí)現(xiàn)了ppyolov2推理),按照官方how-to-build 文檔進(jìn)行編譯ncnn。
編譯完成后, 將上文得到的ppyolov2_r50vd_365e.param、ppyolov2_r50vd_365e.bin、…這些文件復(fù)制到ncnn_ppyolov2的build/examples/目錄下,最后在ncnn_ppyolov2根目錄下運(yùn)行以下命令進(jìn)行ppyolov2的預(yù)測(cè):
cd build/examples./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolo_r18vd.param ppyolo_r18vd.bin 416./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolo_r50vd_2x.param ppyolo_r50vd_2x.bin 608./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolov2_r50vd_365e.param ppyolov2_r50vd_365e.bin 640./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolov2_r101vd_365e.param ppyolov2_r101vd_365e.bin 640
每條命令最后1個(gè)參數(shù)416、608、640表示的是將圖片resize到416、608、640進(jìn)行推理,即target_size參數(shù)。會(huì)彈出一個(gè)這樣的窗口展示預(yù)測(cè)結(jié)果:
test2_06_ppyolo_ncnn的源碼位于ncnn_ppyolov2倉(cāng)庫(kù)的examples/test2_06_ppyolo_ncnn.cpp。
PPYOLOv2和PPYOLO算法目前在Linux和Windows平臺(tái)均已成功預(yù)測(cè)。