From e8f2614da5e57b114efeaceaff8f8488575bd8c4 Mon Sep 17 00:00:00 2001 From: Guanghua Yu <742925032@qq.com> Date: Thu, 29 Oct 2020 10:54:43 +0800 Subject: [PATCH] Enhance multiclass_nms op to support LoD for dygraph mode (#28276) * Enhance multiclass_nms to support LoD for dygraph mode * fix some error in multiclass_nms * update GetLodFromRoisNum to GetNmsLodFromRoisNum --- .../operators/detection/multiclass_nms_op.cc | 77 ++++++++++- paddle/fluid/pybind/op_function_generator.cc | 2 + .../tests/unittests/test_multiclass_nms_op.py | 122 ++++++++++++++++++ tools/static_mode_white_list.pyc | Bin 0 -> 21082 bytes 4 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 tools/static_mode_white_list.pyc diff --git a/paddle/fluid/operators/detection/multiclass_nms_op.cc b/paddle/fluid/operators/detection/multiclass_nms_op.cc index 0e835a62839..7927410ef37 100644 --- a/paddle/fluid/operators/detection/multiclass_nms_op.cc +++ b/paddle/fluid/operators/detection/multiclass_nms_op.cc @@ -21,6 +21,16 @@ namespace operators { using Tensor = framework::Tensor; using LoDTensor = framework::LoDTensor; +inline std::vector GetNmsLodFromRoisNum(const Tensor* rois_num) { + std::vector rois_lod; + auto* rois_num_data = rois_num->data(); + rois_lod.push_back(static_cast(0)); + for (int i = 0; i < rois_num->numel(); ++i) { + rois_lod.push_back(rois_lod.back() + static_cast(rois_num_data[i])); + } + return rois_lod; +} + class MultiClassNMSOp : public framework::OperatorWithKernel { public: using framework::OperatorWithKernel::OperatorWithKernel; @@ -321,6 +331,8 @@ class MultiClassNMSKernel : public framework::OpKernel { auto* outs = ctx.Output("Out"); bool return_index = ctx.HasOutput("Index") ? true : false; auto index = ctx.Output("Index"); + bool has_roisnum = ctx.HasInput("RoisNum") ? true : false; + auto rois_num = ctx.Input("RoisNum"); auto score_dims = scores->dims(); auto score_size = score_dims.size(); auto& dev_ctx = ctx.template device_context(); @@ -332,7 +344,12 @@ class MultiClassNMSKernel : public framework::OpKernel { int64_t out_dim = box_dim + 2; int num_nmsed_out = 0; Tensor boxes_slice, scores_slice; - int n = score_size == 3 ? batch_size : boxes->lod().back().size() - 1; + int n = 0; + if (has_roisnum) { + n = score_size == 3 ? batch_size : rois_num->numel(); + } else { + n = score_size == 3 ? batch_size : boxes->lod().back().size() - 1; + } for (int i = 0; i < n; ++i) { std::map> indices; if (score_size == 3) { @@ -341,7 +358,12 @@ class MultiClassNMSKernel : public framework::OpKernel { boxes_slice = boxes->Slice(i, i + 1); boxes_slice.Resize({score_dims[2], box_dim}); } else { - auto boxes_lod = boxes->lod().back(); + std::vector boxes_lod; + if (has_roisnum) { + boxes_lod = GetNmsLodFromRoisNum(rois_num); + } else { + boxes_lod = boxes->lod().back(); + } if (boxes_lod[i] == boxes_lod[i + 1]) { all_indices.push_back(indices); batch_starts.push_back(batch_starts.back()); @@ -380,7 +402,12 @@ class MultiClassNMSKernel : public framework::OpKernel { offset = i * score_dims[2]; } } else { - auto boxes_lod = boxes->lod().back(); + std::vector boxes_lod; + if (has_roisnum) { + boxes_lod = GetNmsLodFromRoisNum(rois_num); + } else { + boxes_lod = boxes->lod().back(); + } if (boxes_lod[i] == boxes_lod[i + 1]) continue; scores_slice = scores->Slice(boxes_lod[i], boxes_lod[i + 1]); boxes_slice = boxes->Slice(boxes_lod[i], boxes_lod[i + 1]); @@ -403,6 +430,15 @@ class MultiClassNMSKernel : public framework::OpKernel { } } } + if (ctx.HasOutput("NmsRoisNum")) { + auto* nms_rois_num = ctx.Output("NmsRoisNum"); + nms_rois_num->mutable_data({n}, ctx.GetPlace()); + int* num_data = nms_rois_num->data(); + for (int i = 1; i <= n; i++) { + num_data[i - 1] = batch_starts[i] - batch_starts[i - 1]; + } + nms_rois_num->Resize({n}); + } framework::LoD lod; lod.emplace_back(batch_starts); @@ -535,6 +571,34 @@ class MultiClassNMS2OpMaker : public MultiClassNMSOpMaker { } }; +class MultiClassNMS3Op : public MultiClassNMS2Op { + public: + MultiClassNMS3Op(const std::string& type, + const framework::VariableNameMap& inputs, + const framework::VariableNameMap& outputs, + const framework::AttributeMap& attrs) + : MultiClassNMS2Op(type, inputs, outputs, attrs) {} + + void InferShape(framework::InferShapeContext* ctx) const override { + MultiClassNMS2Op::InferShape(ctx); + + ctx->SetOutputDim("NmsRoisNum", {-1}); + } +}; + +class MultiClassNMS3OpMaker : public MultiClassNMS2OpMaker { + public: + void Make() override { + MultiClassNMS2OpMaker::Make(); + AddInput("RoisNum", + "(Tensor) The number of RoIs in shape (B)," + "B is the number of images") + .AsDispensable(); + AddOutput("NmsRoisNum", "(Tensor), The number of NMS RoIs in each image") + .AsDispensable(); + } +}; + } // namespace operators } // namespace paddle @@ -551,3 +615,10 @@ REGISTER_OPERATOR( paddle::framework::EmptyGradOpMaker); REGISTER_OP_CPU_KERNEL(multiclass_nms2, ops::MultiClassNMSKernel, ops::MultiClassNMSKernel); + +REGISTER_OPERATOR( + multiclass_nms3, ops::MultiClassNMS3Op, ops::MultiClassNMS3OpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); +REGISTER_OP_CPU_KERNEL(multiclass_nms3, ops::MultiClassNMSKernel, + ops::MultiClassNMSKernel); diff --git a/paddle/fluid/pybind/op_function_generator.cc b/paddle/fluid/pybind/op_function_generator.cc index 7f2736a9b1d..cac44173c17 100644 --- a/paddle/fluid/pybind/op_function_generator.cc +++ b/paddle/fluid/pybind/op_function_generator.cc @@ -52,6 +52,7 @@ std::map> op_ins_map = { {"hierarchical_sigmoid", {"X", "W", "Label", "PathTable", "PathCode", "Bias"}}, {"moving_average_abs_max_scale", {"X", "InAccum", "InState"}}, + {"multiclass_nms3", {"BBoxes", "Scores", "RoisNum"}}, }; // NOTE(zhiqiu): Like op_ins_map. @@ -78,6 +79,7 @@ std::map> op_outs_map = { {"distribute_fpn_proposals", {"MultiFpnRois", "RestoreIndex", "MultiLevelRoIsNum"}}, {"moving_average_abs_max_scale", {"OutScale", "OutAccum", "OutState"}}, + {"multiclass_nms3", {"Out", "NmsRoisNum"}}, }; // NOTE(zhiqiu): Commonly, the outputs in auto-generated OP function are diff --git a/python/paddle/fluid/tests/unittests/test_multiclass_nms_op.py b/python/paddle/fluid/tests/unittests/test_multiclass_nms_op.py index 34c19b88bcd..3158d78db63 100644 --- a/python/paddle/fluid/tests/unittests/test_multiclass_nms_op.py +++ b/python/paddle/fluid/tests/unittests/test_multiclass_nms_op.py @@ -571,6 +571,128 @@ class TestMulticlassNMSError(unittest.TestCase): self.assertRaises(TypeError, test_scores_Variable) +class TestMulticlassNMS3Op(TestMulticlassNMS2Op): + def setUp(self): + self.set_argument() + N = 7 + M = 1200 + C = 21 + BOX_SIZE = 4 + background = 0 + nms_threshold = 0.3 + nms_top_k = 400 + keep_top_k = 200 + score_threshold = self.score_threshold + + scores = np.random.random((N * M, C)).astype('float32') + + scores = np.apply_along_axis(softmax, 1, scores) + scores = np.reshape(scores, (N, M, C)) + scores = np.transpose(scores, (0, 2, 1)) + + boxes = np.random.random((N, M, BOX_SIZE)).astype('float32') + boxes[:, :, 0:2] = boxes[:, :, 0:2] * 0.5 + boxes[:, :, 2:4] = boxes[:, :, 2:4] * 0.5 + 0.5 + + det_outs, lod = batched_multiclass_nms(boxes, scores, background, + score_threshold, nms_threshold, + nms_top_k, keep_top_k) + det_outs = np.array(det_outs) + + nmsed_outs = det_outs[:, :-1].astype('float32') if len( + det_outs) else det_outs + index_outs = det_outs[:, -1:].astype('int') if len( + det_outs) else det_outs + self.op_type = 'multiclass_nms3' + self.inputs = {'BBoxes': boxes, 'Scores': scores} + self.outputs = { + 'Out': (nmsed_outs, [lod]), + 'Index': (index_outs, [lod]), + 'NmsRoisNum': np.array(lod).astype('int32') + } + self.attrs = { + 'background_label': 0, + 'nms_threshold': nms_threshold, + 'nms_top_k': nms_top_k, + 'keep_top_k': keep_top_k, + 'score_threshold': score_threshold, + 'nms_eta': 1.0, + 'normalized': True, + } + + def test_check_output(self): + self.check_output() + + +class TestMulticlassNMS3OpNoOutput(TestMulticlassNMS3Op): + def set_argument(self): + # Here set 2.0 to test the case there is no outputs. + # In practical use, 0.0 < score_threshold < 1.0 + self.score_threshold = 2.0 + + +class TestMulticlassNMS3LoDInput(TestMulticlassNMS2LoDInput): + def setUp(self): + self.set_argument() + M = 1200 + C = 21 + BOX_SIZE = 4 + box_lod = [[1200]] + background = 0 + nms_threshold = 0.3 + nms_top_k = 400 + keep_top_k = 200 + score_threshold = self.score_threshold + normalized = False + + scores = np.random.random((M, C)).astype('float32') + + scores = np.apply_along_axis(softmax, 1, scores) + + boxes = np.random.random((M, C, BOX_SIZE)).astype('float32') + boxes[:, :, 0] = boxes[:, :, 0] * 10 + boxes[:, :, 1] = boxes[:, :, 1] * 10 + boxes[:, :, 2] = boxes[:, :, 2] * 10 + 10 + boxes[:, :, 3] = boxes[:, :, 3] * 10 + 10 + + det_outs, lod = lod_multiclass_nms( + boxes, scores, background, score_threshold, nms_threshold, + nms_top_k, keep_top_k, box_lod, normalized) + + det_outs = np.array(det_outs) + nmsed_outs = det_outs[:, :-1].astype('float32') if len( + det_outs) else det_outs + self.op_type = 'multiclass_nms3' + self.inputs = { + 'BBoxes': (boxes, box_lod), + 'Scores': (scores, box_lod), + 'RoisNum': np.array(box_lod).astype('int32') + } + self.outputs = { + 'Out': (nmsed_outs, [lod]), + 'NmsRoisNum': np.array(lod).astype('int32') + } + self.attrs = { + 'background_label': 0, + 'nms_threshold': nms_threshold, + 'nms_top_k': nms_top_k, + 'keep_top_k': keep_top_k, + 'score_threshold': score_threshold, + 'nms_eta': 1.0, + 'normalized': normalized, + } + + def test_check_output(self): + self.check_output() + + +class TestMulticlassNMS3LoDNoOutput(TestMulticlassNMS3LoDInput): + def set_argument(self): + # Here set 2.0 to test the case there is no outputs. + # In practical use, 0.0 < score_threshold < 1.0 + self.score_threshold = 2.0 + + if __name__ == '__main__': paddle.enable_static() unittest.main() diff --git a/tools/static_mode_white_list.pyc b/tools/static_mode_white_list.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d2a45c248ce271c1c4fff310505a172339e5eee GIT binary patch literal 21082 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