diff --git a/slim/quantization/eval.py b/slim/eval.py similarity index 100% rename from slim/quantization/eval.py rename to slim/eval.py diff --git a/slim/infer.py b/slim/infer.py new file mode 100644 index 0000000000000000000000000000000000000000..4a212ad6b385fbc5e10dd8e04a8934645c3bd689 --- /dev/null +++ b/slim/infer.py @@ -0,0 +1,276 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +import glob + +import numpy as np +from PIL import Image +sys.path.append("../../") + +def set_paddle_flags(**kwargs): + for key, value in kwargs.items(): + if os.environ.get(key, None) is None: + os.environ[key] = str(value) + + +# NOTE(paddle-dev): All of these flags should be set before +# `import paddle`. Otherwise, it would not take any effect. +set_paddle_flags( + FLAGS_eager_delete_tensor_gb=0, # enable GC to save memory +) + +from paddle import fluid +from ppdet.utils.cli import print_total_cfg +from ppdet.core.workspace import load_config, merge_config, create +from ppdet.modeling.model_input import create_feed +from ppdet.data.data_feed import create_reader + +from ppdet.utils.eval_utils import parse_fetches +from ppdet.utils.cli import ArgsParser +from ppdet.utils.check import check_gpu +from ppdet.utils.visualizer import visualize_results +import ppdet.utils.checkpoint as checkpoint + +import logging +FORMAT = '%(asctime)s-%(levelname)s: %(message)s' +logging.basicConfig(level=logging.INFO, format=FORMAT) +logger = logging.getLogger(__name__) + + +def get_save_image_name(output_dir, image_path): + """ + Get save image name from source image path. + """ + if not os.path.exists(output_dir): + os.makedirs(output_dir) + image_name = os.path.split(image_path)[-1] + name, ext = os.path.splitext(image_name) + return os.path.join(output_dir, "{}".format(name)) + ext + + +def get_test_images(infer_dir, infer_img): + """ + Get image path list in TEST mode + """ + assert infer_img is not None or infer_dir is not None, \ + "--infer_img or --infer_dir should be set" + assert infer_img is None or os.path.isfile(infer_img), \ + "{} is not a file".format(infer_img) + assert infer_dir is None or os.path.isdir(infer_dir), \ + "{} is not a directory".format(infer_dir) + images = [] + + # infer_img has a higher priority + if infer_img and os.path.isfile(infer_img): + images.append(infer_img) + return images + + infer_dir = os.path.abspath(infer_dir) + assert os.path.isdir(infer_dir), \ + "infer_dir {} is not a directory".format(infer_dir) + exts = ['jpg', 'jpeg', 'png', 'bmp'] + exts += [ext.upper() for ext in exts] + for ext in exts: + images.extend(glob.glob('{}/*.{}'.format(infer_dir, ext))) + + assert len(images) > 0, "no image found in {}".format(infer_dir) + logger.info("Found {} inference images in total.".format(len(images))) + + return images + + +def main(): + cfg = load_config(FLAGS.config) + + if 'architecture' in cfg: + main_arch = cfg.architecture + else: + raise ValueError("'architecture' not specified in config file.") + + merge_config(FLAGS.opt) + + # check if set use_gpu=True in paddlepaddle cpu version + check_gpu(cfg.use_gpu) + # print_total_cfg(cfg) + + if 'test_feed' not in cfg: + test_feed = create(main_arch + 'TestFeed') + else: + test_feed = create(cfg.test_feed) + + test_images = get_test_images(FLAGS.infer_dir, FLAGS.infer_img) + test_feed.dataset.add_images(test_images) + + + place = fluid.CUDAPlace(0) if cfg.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + + infer_prog, feed_var_names, fetch_list = fluid.io.load_inference_model( + dirname=FLAGS.model_path, model_filename=FLAGS.model_name, + params_filename=FLAGS.params_name, + executor=exe) + + reader = create_reader(test_feed) + feeder = fluid.DataFeeder(place=place, feed_list=feed_var_names, + program=infer_prog) + + # parse infer fetches + assert cfg.metric in ['COCO', 'VOC'], \ + "unknown metric type {}".format(cfg.metric) + extra_keys = [] + if cfg['metric'] == 'COCO': + extra_keys = ['im_info', 'im_id', 'im_shape'] + if cfg['metric'] == 'VOC': + extra_keys = ['im_id', 'im_shape'] + keys, values, _ = parse_fetches({'bbox':fetch_list}, infer_prog, extra_keys) + + # parse dataset category + if cfg.metric == 'COCO': + from ppdet.utils.coco_eval import bbox2out, mask2out, get_category_info + if cfg.metric == "VOC": + from ppdet.utils.voc_eval import bbox2out, get_category_info + + anno_file = getattr(test_feed.dataset, 'annotation', None) + with_background = getattr(test_feed, 'with_background', True) + use_default_label = getattr(test_feed, 'use_default_label', False) + clsid2catid, catid2name = get_category_info(anno_file, with_background, + use_default_label) + + # whether output bbox is normalized in model output layer + is_bbox_normalized = False + + # use tb-paddle to log image + if FLAGS.use_tb: + from tb_paddle import SummaryWriter + tb_writer = SummaryWriter(FLAGS.tb_log_dir) + tb_image_step = 0 + tb_image_frame = 0 # each frame can display ten pictures at most. + + imid2path = reader.imid2path + keys = ['bbox'] + for iter_id, data in enumerate(reader()): + feed_data = [[d[0], d[1]] for d in data] + outs = exe.run(infer_prog, + feed=feeder.feed(feed_data), + fetch_list=fetch_list, + return_numpy=False) + res = { + k: (np.array(v), v.recursive_sequence_lengths()) + for k, v in zip(keys, outs) + } + res['im_id'] = [[d[2] for d in data]] + logger.info('Infer iter {}'.format(iter_id)) + + bbox_results = None + mask_results = None + if 'bbox' in res: + bbox_results = bbox2out([res], clsid2catid, is_bbox_normalized) + if 'mask' in res: + mask_results = mask2out([res], clsid2catid, + model.mask_head.resolution) + + # visualize result + im_ids = res['im_id'][0] + for im_id in im_ids: + image_path = imid2path[int(im_id)] + image = Image.open(image_path).convert('RGB') + + # use tb-paddle to log original image + if FLAGS.use_tb: + original_image_np = np.array(image) + tb_writer.add_image( + "original/frame_{}".format(tb_image_frame), + original_image_np, + tb_image_step, + dataformats='HWC') + + image = visualize_results(image, + int(im_id), catid2name, + FLAGS.draw_threshold, bbox_results, + mask_results) + + # use tb-paddle to log image with bbox + if FLAGS.use_tb: + infer_image_np = np.array(image) + tb_writer.add_image( + "bbox/frame_{}".format(tb_image_frame), + infer_image_np, + tb_image_step, + dataformats='HWC') + tb_image_step += 1 + if tb_image_step % 10 == 0: + tb_image_step = 0 + tb_image_frame += 1 + + save_name = get_save_image_name(FLAGS.output_dir, image_path) + logger.info("Detection bbox results save in {}".format(save_name)) + image.save(save_name, quality=95) + + +if __name__ == '__main__': + parser = ArgsParser() + parser.add_argument( + "--infer_dir", + type=str, + default=None, + help="Directory for images to perform inference on.") + parser.add_argument( + "--infer_img", + type=str, + default=None, + help="Image path, has higher priority over --infer_dir") + parser.add_argument( + "--output_dir", + type=str, + default="output", + help="Directory for storing the output visualization files.") + parser.add_argument( + "--draw_threshold", + type=float, + default=0.5, + help="Threshold to reserve the result for visualization.") + parser.add_argument( + "--use_tb", + type=bool, + default=False, + help="whether to record the data to Tensorboard.") + parser.add_argument( + '--tb_log_dir', + type=str, + default="tb_log_dir/image", + help='Tensorboard logging directory for image.') + parser.add_argument( + '--model_path', + type=str, + default=None, + help="inference model path") + parser.add_argument( + '--model_name', + type=str, + default='__model__.infer', + help="model filename for inference model") + parser.add_argument( + '--params_name', + type=str, + default='__params__', + help="params filename for inference model") + FLAGS = parser.parse_args() + main() diff --git a/slim/quantization/README.md b/slim/quantization/README.md index 8136f29f3a94f9fc86a50de5a380eee8f0231d07..83b8667e3ef6352d57f5852dc74e5459247ea61b 100644 --- a/slim/quantization/README.md +++ b/slim/quantization/README.md @@ -44,9 +44,9 @@ step1: 开启显存优化策略 export FLAGS_fast_eager_deletion_mode=1 export FLAGS_eager_delete_tensor_gb=0.0 ``` -step2: 设置gpu卡 +step2: 设置gpu卡,目前的超参设置适合2卡训练 ``` -export CUDA_VISIBLE_DEVICES=0 +export CUDA_VISIBLE_DEVICES=0,1 ``` step3: 开始训练 ``` @@ -104,6 +104,12 @@ QuantizationFreezePass主要用于改变IrGraph中量化op和反量化op的顺
图4:应用TransformForMobilePass后的结果

+> 综上,可得在量化过程中有以下几种模型结构: +1. 原始模型 +2. 经QuantizationTransformPass之后得到的适用于训练的量化模型结构,在${checkpoint_path}下保存的`eval_model`是这种结构,在训练过程中每个epoch结束时也使用这个网络结构进行评估,虽然这个模型结构不是最终想要的模型结构,但是每个epoch的评估结果可用来挑选模型。 +3. 经QuantizationFreezePass之后得到的FP32模型结构,具体结构已在上面进行介绍。本文档中列出的数据集的评估结果是对FP32模型结构进行评估得到的结果。这种模型结构在训练过程中只会保存一次,也就是在量化配置文件中设置的`end_epoch`结束时进行保存,如果想将其他epoch的训练结果转化成FP32模型,可使用脚本 PaddleSlim/classification/quantization/freeze.py进行转化,具体使用方法在[评估](#评估)中介绍。 +4. 经ConvertToInt8Pass之后得到的8-bit模型结构,具体结构已在上面进行介绍。这种模型结构在训练过程中只会保存一次,也就是在量化配置文件中设置的`end_epoch`结束时进行保存,如果想将其他epoch的训练结果转化成8-bit模型,可使用脚本 slim/quantization/freeze.py进行转化,具体使用方法在[评估](#评估)中介绍。 +5. 经TransformForMobilePass之后得到的mobile模型结构,具体结构已在上面进行介绍。这种模型结构在训练过程中只会保存一次,也就是在量化配置文件中设置的`end_epoch`结束时进行保存,如果想将其他epoch的训练结果转化成mobile模型,可使用脚本 slim/quantization/freeze.py进行转化,具体使用方法在[评估](#评估)中介绍。 ## 评估 @@ -115,10 +121,14 @@ QuantizationFreezePass主要用于改变IrGraph中量化op和反量化op的顺 如果不需要保存评估模型,可以在定义Compressor对象时,将`save_eval_model`选项设置为False(默认为True)。 -脚本slim/quantization/eval.py中为使用该模型在评估数据集上做评估的示例。 +脚本slim/eval.py中为使用该模型在评估数据集上做评估的示例。 运行命令为: ``` -python eval.py --model_path ${checkpoint_path}/${epoch_id}/eval_model/ --model_name __model__ --params_name __params__ -c yolov3_mobilenet_v1_voc.yml +python ../eval.py \ + --model_path ${checkpoint_path}/${epoch_id}/eval_model/ \ + --model_name __model__ \ + --params_name __params__ \ + -c yolov3_mobilenet_v1_voc.yml ``` 在评估之后,选取效果最好的epoch的模型,可使用脚本 slim/quantization/freeze.py将该模型转化为以上介绍的三种模型:FP32模型,int8模型,mobile模型,需要配置的参数为: @@ -127,16 +137,41 @@ python eval.py --model_path ${checkpoint_path}/${epoch_id}/eval_model/ --model_n - weight_quant_type 模型参数的量化方式,和配置文件中的类型保持一致 - save_path `FP32`, `8-bit`, `mobile`模型的保存路径,分别为 `${save_path}/float/`, `${save_path}/int8/`, `${save_path}/mobile/` +运行命令示例: +``` +python freeze.py \ + --model_path ${checkpoint_path}/${epoch_id}/eval_model/ \ + --weight_quant_type ${weight_quant_type} \ + --save_path ${any path you want} +``` + ### 最终评估模型 -最终使用的评估模型是FP32模型,使用脚本slim/quantization/eval.py中为使用该模型在评估数据集上做评估的示例。 +最终使用的评估模型是FP32模型,使用脚本slim/eval.py中为使用该模型在评估数据集上做评估的示例。 运行命令为: ``` -python eval.py --model_path ${float_model_path} --model_name model --params_name weights -c yolov3_mobilenet_v1_voc.yml +python ../eval.py \ + --model_path ${float_model_path} + --model_name model \ + --params_name weights \ + -c yolov3_mobilenet_v1_voc.yml ``` ## 预测 ### python预测 +FP32模型可直接使用原生PaddlePaddle Fluid预测方法进行预测。 + +在脚本slim/infer.py中展示了如何使用fluid python API加载使用预测模型进行预测。 + +运行命令示例: +``` +python ../infer.py \ + --model_path ${save_path}/float \ + --model_name model \ + --params_name weights \ + -c yolov3_mobilenet_v1_voc.yml \ + --infer_dir ../../demo +``` ### PaddleLite预测 diff --git a/slim/quantization/compress.py b/slim/quantization/compress.py index 70166f1400310f74f59916c6a54d4ba8fc93c596..6318760765ec7f5342a5f30f025483db23bd6d52 100644 --- a/slim/quantization/compress.py +++ b/slim/quantization/compress.py @@ -201,6 +201,7 @@ def main(): checkpoint.load_params(exe, train_prog, cfg.pretrain_weights) + best_box_ap_list = [] def eval_func(program, scope): @@ -208,7 +209,6 @@ def main(): #exe = fluid.Executor(place) results = eval_run(exe, program, eval_reader, eval_keys, eval_values, eval_cls, test_data_feed) - best_box_ap_list = [] resolution = None if 'mask' in results[0]: diff --git a/slim/quantization/yolov3_mobilenet_v1_slim.yaml b/slim/quantization/yolov3_mobilenet_v1_slim.yaml index 3ad506b1c4c5c721de9f4ab790eee6eb6c36a9fe..60a66f656f9e419cd862231654ab4eaca6057ea2 100644 --- a/slim/quantization/yolov3_mobilenet_v1_slim.yaml +++ b/slim/quantization/yolov3_mobilenet_v1_slim.yaml @@ -3,7 +3,7 @@ strategies: quantization_strategy: class: 'QuantizationStrategy' start_epoch: 0 - end_epoch: 0 + end_epoch: 4 float_model_save_path: './output/yolov3/float' mobile_model_save_path: './output/yolov3/mobile' int8_model_save_path: './output/yolov3/int8' @@ -14,7 +14,7 @@ strategies: save_in_nodes: ['image', 'im_size'] save_out_nodes: ['multiclass_nms_0.tmp_0'] compressor: - epoch: 1 + epoch: 5 checkpoint_path: './checkpoints/yolov3/' strategies: - quantization_strategy diff --git a/slim/quantization/yolov3_mobilenet_v1_voc.yml b/slim/quantization/yolov3_mobilenet_v1_voc.yml index a006ce775ee63f087872012d564d67d4ea210130..8cc562371585a925d2d1dc90279527dd590e2c29 100644 --- a/slim/quantization/yolov3_mobilenet_v1_voc.yml +++ b/slim/quantization/yolov3_mobilenet_v1_voc.yml @@ -3,7 +3,7 @@ train_feed: YoloTrainFeed eval_feed: YoloEvalFeed test_feed: YoloTestFeed use_gpu: true -max_iters: 70000 +max_iters: 1000 log_smooth_window: 20 save_dir: output snapshot_iter: 2000 @@ -45,8 +45,8 @@ LearningRate: - !PiecewiseDecay gamma: 0.1 milestones: + - 1000 - 2000 - - 8000 #- !LinearWarmup #start_factor: 0. #steps: 1000