diff --git a/paddlex/__init__.py b/paddlex/__init__.py index 43339300c8f704a70d44f7a564e42a6e080bd8de..0bc80a6f29b581917939269f5e471c9685a1a693 100644 --- a/paddlex/__init__.py +++ b/paddlex/__init__.py @@ -30,6 +30,7 @@ from . import slim from . import convertor from . import tools from . import interpret +from . import deploy try: import pycocotools @@ -41,9 +42,9 @@ except: "[WARNING] pycocotools install: https://github.com/PaddlePaddle/PaddleX/blob/develop/docs/install.md" ) -#import paddlehub as hub -#if hub.version.hub_version < '1.6.2': -# raise Exception("[ERROR] paddlehub >= 1.6.2 is required") +import paddlehub as hub +if hub.version.hub_version < '1.6.2': + raise Exception("[ERROR] paddlehub >= 1.6.2 is required") env_info = get_environ_info() load_model = cv.models.load_model diff --git a/paddlex/cv/models/base.py b/paddlex/cv/models/base.py index 3264c8d3b0d9830c679bb02cf2124de8e67affa2..11a7785dd4a371e5762e033b82398d0fb9eca3b7 100644 --- a/paddlex/cv/models/base.py +++ b/paddlex/cv/models/base.py @@ -79,9 +79,9 @@ class BaseAPI: return int(batch_size // len(self.places)) else: raise Exception("Please support correct batch_size, \ - which can be divided by available cards({}) in {}". - format(paddlex.env_info['num'], - paddlex.env_info['place'])) + which can be divided by available cards({}) in {}" + .format(paddlex.env_info['num'], paddlex.env_info[ + 'place'])) def build_program(self): # 构建训练网络 @@ -141,7 +141,7 @@ class BaseAPI: from .slim.post_quantization import PaddleXPostTrainingQuantization except: raise Exception( - "Model Quantization is not available, try to upgrade your paddlepaddle>=1.7.0" + "Model Quantization is not available, try to upgrade your paddlepaddle>=1.8.0" ) is_use_cache_file = True if cache_dir is None: @@ -209,8 +209,8 @@ class BaseAPI: paddlex.utils.utils.load_pretrain_weights( self.exe, self.train_prog, resume_checkpoint, resume=True) if not osp.exists(osp.join(resume_checkpoint, "model.yml")): - raise Exception( - "There's not model.yml in {}".format(resume_checkpoint)) + raise Exception("There's not model.yml in {}".format( + resume_checkpoint)) with open(osp.join(resume_checkpoint, "model.yml")) as f: info = yaml.load(f.read(), Loader=yaml.Loader) self.completed_epochs = info['completed_epochs'] @@ -361,8 +361,8 @@ class BaseAPI: # 模型保存成功的标志 open(osp.join(save_dir, '.success'), 'w').close() - logging.info( - "Model for inference deploy saved in {}.".format(save_dir)) + logging.info("Model for inference deploy saved in {}.".format( + save_dir)) def train_loop(self, num_epochs, @@ -376,7 +376,8 @@ class BaseAPI: early_stop=False, early_stop_patience=5): if train_dataset.num_samples < train_batch_size: - raise Exception('The amount of training datset must be larger than batch size.') + raise Exception( + 'The amount of training datset must be larger than batch size.') if not osp.isdir(save_dir): if osp.exists(save_dir): os.remove(save_dir) @@ -414,8 +415,8 @@ class BaseAPI: build_strategy=build_strategy, exec_strategy=exec_strategy) - total_num_steps = math.floor( - train_dataset.num_samples / train_batch_size) + total_num_steps = math.floor(train_dataset.num_samples / + train_batch_size) num_steps = 0 time_stat = list() time_train_one_epoch = None @@ -429,8 +430,8 @@ class BaseAPI: if self.model_type == 'detector': eval_batch_size = self._get_single_card_bs(train_batch_size) if eval_dataset is not None: - total_num_steps_eval = math.ceil( - eval_dataset.num_samples / eval_batch_size) + total_num_steps_eval = math.ceil(eval_dataset.num_samples / + eval_batch_size) if use_vdl: # VisualDL component @@ -472,7 +473,9 @@ class BaseAPI: if use_vdl: for k, v in step_metrics.items(): - log_writer.add_scalar('Metrics/Training(Step): {}'.format(k), v, num_steps) + log_writer.add_scalar( + 'Metrics/Training(Step): {}'.format(k), v, + num_steps) # 估算剩余时间 avg_step_time = np.mean(time_stat) @@ -480,11 +483,12 @@ class BaseAPI: eta = (num_epochs - i - 1) * time_train_one_epoch + ( total_num_steps - step - 1) * avg_step_time else: - eta = ((num_epochs - i) * total_num_steps - step - - 1) * avg_step_time + eta = ((num_epochs - i) * total_num_steps - step - 1 + ) * avg_step_time if time_eval_one_epoch is not None: - eval_eta = (total_eval_times - i // - save_interval_epochs) * time_eval_one_epoch + eval_eta = ( + total_eval_times - i // save_interval_epochs + ) * time_eval_one_epoch else: eval_eta = ( total_eval_times - i // save_interval_epochs @@ -494,10 +498,11 @@ class BaseAPI: logging.info( "[TRAIN] Epoch={}/{}, Step={}/{}, {}, time_each_step={}s, eta={}" .format(i + 1, num_epochs, step + 1, total_num_steps, - dict2str(step_metrics), round( - avg_step_time, 2), eta_str)) + dict2str(step_metrics), + round(avg_step_time, 2), eta_str)) train_metrics = OrderedDict( - zip(list(self.train_outputs.keys()), np.mean(records, axis=0))) + zip(list(self.train_outputs.keys()), np.mean( + records, axis=0))) logging.info('[TRAIN] Epoch {} finished, {} .'.format( i + 1, dict2str(train_metrics))) time_train_one_epoch = time.time() - epoch_start_time @@ -533,7 +538,8 @@ class BaseAPI: if isinstance(v, np.ndarray): if v.size > 1: continue - log_writer.add_scalar("Metrics/Eval(Epoch): {}".format(k), v, i+1) + log_writer.add_scalar( + "Metrics/Eval(Epoch): {}".format(k), v, i + 1) self.save_model(save_dir=current_save_dir) time_eval_one_epoch = time.time() - eval_epoch_start_time eval_epoch_start_time = time.time() diff --git a/paddlex/deploy.py b/paddlex/deploy.py new file mode 100644 index 0000000000000000000000000000000000000000..bb2618c1d844836a4884d93218f7d67434103b8e --- /dev/null +++ b/paddlex/deploy.py @@ -0,0 +1,276 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. +import os +import os.path as osp +import cv2 +import numpy as np +import yaml +import paddlex +import paddle.fluid as fluid + + +class Predictor: + def __init__(self, + model_dir, + use_gpu=True, + gpu_id=0, + use_mkl=False, + use_trt=False, + use_glog=False, + memory_optimize=True): + """ 创建Paddle Predictor + + Args: + model_dir: 模型路径(必须是导出的部署或量化模型) + use_gpu: 是否使用gpu,默认True + gpu_id: 使用gpu的id,默认0 + use_mkl: 是否使用mkldnn计算库,CPU情况下使用,默认False + use_trt: 是否使用TensorRT,默认False + use_glog: 是否启用glog日志, 默认False + memory_optimize: 是否启动内存优化,默认True + """ + if not osp.isdir(model_dir): + raise Exception("[ERROR] Path {} not exist.".format(model_dir)) + if not osp.exists(osp.join(model_dir, "model.yml")): + raise Exception("There's not model.yml in {}".format(model_dir)) + with open(osp.join(model_dir, "model.yml")) as f: + self.info = yaml.load(f.read(), Loader=yaml.Loader) + + self.status = self.info['status'] + + if self.status != "Quant" and self.status != "Infer": + raise Exception("[ERROR] Only quantized model or exported " + "inference model is supported.") + + self.model_dir = model_dir + self.model_type = self.info['_Attributes']['model_type'] + self.model_name = self.info['Model'] + self.num_classes = self.info['_Attributes']['num_classes'] + self.labels = self.info['_Attributes']['labels'] + if self.info['Model'] == 'MaskRCNN': + if self.info['_init_params']['with_fpn']: + self.mask_head_resolution = 28 + else: + self.mask_head_resolution = 14 + transforms_mode = self.info.get('TransformsMode', 'RGB') + if transforms_mode == 'RGB': + to_rgb = True + else: + to_rgb = False + self.transforms = self.build_transforms(self.info['Transforms'], + to_rgb) + self.predictor = self.create_predictor( + use_gpu, gpu_id, use_mkl, use_trt, use_glog, memory_optimize) + + def create_predictor(self, + use_gpu=True, + gpu_id=0, + use_mkl=False, + use_trt=False, + use_glog=False, + memory_optimize=True): + config = fluid.core.AnalysisConfig( + os.path.join(self.model_dir, '__model__'), + os.path.join(self.model_dir, '__params__')) + + if use_gpu: + # 设置GPU初始显存(单位M)和Device ID + config.enable_use_gpu(100, gpu_id) + else: + config.disable_gpu() + if use_mkl: + config.enable_mkldnn() + if use_glog: + config.enable_glog_info() + else: + config.disable_glog_info() + if memory_optimize: + config.enable_memory_optim() + else: + config.diable_memory_optim() + + # 开启计算图分析优化,包括OP融合等 + config.switch_ir_optim(True) + # 关闭feed和fetch OP使用,使用ZeroCopy接口必须设置此项 + config.switch_use_feed_fetch_ops(False) + predictor = fluid.core.create_paddle_predictor(config) + return predictor + + def build_transforms(self, transforms_info, to_rgb=True): + if self.model_type == "classifier": + from paddlex.cls import transforms + elif self.model_type == "detector": + from paddlex.det import transforms + elif self.model_type == "segmenter": + from paddlex.seg import transforms + op_list = list() + for op_info in transforms_info: + op_name = list(op_info.keys())[0] + op_attr = op_info[op_name] + if not hasattr(transforms, op_name): + raise Exception( + "There's no operator named '{}' in transforms of {}". + format(op_name, self.model_type)) + op_list.append(getattr(transforms, op_name)(**op_attr)) + eval_transforms = transforms.Compose(op_list) + if hasattr(eval_transforms, 'to_rgb'): + eval_transforms.to_rgb = to_rgb + self.arrange_transforms(eval_transforms) + return eval_transforms + + def arrange_transforms(self, transforms): + if self.model_type == 'classifier': + arrange_transform = paddlex.cls.transforms.ArrangeClassifier + elif self.model_type == 'segmenter': + arrange_transform = paddlex.seg.transforms.ArrangeSegmenter + elif self.model_type == 'detector': + arrange_name = 'Arrange{}'.format(self.model_name) + arrange_transform = getattr(paddlex.det.transforms, arrange_name) + else: + raise Exception("Unrecognized model type: {}".format( + self.model_type)) + if type(transforms.transforms[-1]).__name__.startswith('Arrange'): + transforms.transforms[-1] = arrange_transform(mode='test') + else: + transforms.transforms.append(arrange_transform(mode='test')) + + def preprocess(self, image): + """ 对图像做预处理 + + Args: + image(str|np.ndarray): 图片路径或np.ndarray,如为后者,要求是BGR格式 + """ + res = dict() + if self.model_type == "classifier": + im, = self.transforms(image) + im = np.expand_dims(im, axis=0).copy() + res['image'] = im + elif self.model_type == "detector": + if self.model_name == "YOLOv3": + im, im_shape = self.transforms(image) + im = np.expand_dims(im, axis=0).copy() + im_shape = np.expand_dims(im_shape, axis=0).copy() + res['image'] = im + res['im_size'] = im_shape + if self.model_name.count('RCNN') > 0: + im, im_resize_info, im_shape = self.transforms(image) + im = np.expand_dims(im, axis=0).copy() + im_resize_info = np.expand_dims(im_resize_info, axis=0).copy() + im_shape = np.expand_dims(im_shape, axis=0).copy() + res['image'] = im + res['im_info'] = im_resize_info + res['im_shape'] = im_shape + elif self.model_type == "segmenter": + im, im_info = self.transforms(image) + im = np.expand_dims(im, axis=0).copy() + res['image'] = im + res['im_info'] = im_info + return res + + def raw_predict(self, inputs): + """ 接受预处理过后的数据进行预测 + + Args: + inputs(tuple): 预处理过后的数据 + """ + for k, v in inputs.items(): + try: + tensor = self.predictor.get_input_tensor(k) + except: + continue + tensor.copy_from_cpu(v) + self.predictor.zero_copy_run() + output_names = self.predictor.get_output_names() + output_results = list() + for name in output_names: + output_tensor = self.predictor.get_output_tensor(name) + output_results.append(output_tensor.copy_to_cpu()) + return output_results + + def classifier_postprocess(self, preds, topk=1): + """ 对分类模型的预测结果做后处理 + """ + true_topk = min(self.num_classes, topk) + pred_label = np.argsort(preds[0][0])[::-1][:true_topk] + result = [{ + 'category_id': l, + 'category': self.labels[l], + 'score': preds[0][0, l], + } for l in pred_label] + return result + + def segmenter_postprocess(self, preds, preprocessed_inputs): + """ 对语义分割结果做后处理 + """ + label_map = np.squeeze(preds[0]).astype('uint8') + score_map = np.squeeze(preds[1]) + score_map = np.transpose(score_map, (1, 2, 0)) + im_info = preprocessed_inputs['im_info'] + for info in im_info[::-1]: + if info[0] == 'resize': + w, h = info[1][1], info[1][0] + label_map = cv2.resize(label_map, (w, h), cv2.INTER_NEAREST) + score_map = cv2.resize(score_map, (w, h), cv2.INTER_LINEAR) + elif info[0] == 'padding': + w, h = info[1][1], info[1][0] + label_map = label_map[0:h, 0:w] + score_map = score_map[0:h, 0:w, :] + else: + raise Exception("Unexpected info '{}' in im_info".format(info[ + 0])) + return {'label_map': label_map, 'score_map': score_map} + + def detector_postprocess(self, preds, preprocessed_inputs): + """ 对目标检测和实例分割结果做后处理 + """ + bboxes = {'bbox': (np.array(preds[0]), [[len(preds[0])]])} + bboxes['im_id'] = (np.array([[0]]).astype('int32'), []) + clsid2catid = dict({i: i for i in range(self.num_classes)}) + xywh_results = paddlex.cv.models.utils.detection_eval.bbox2out( + [bboxes], clsid2catid) + results = list() + for xywh_res in xywh_results: + del xywh_res['image_id'] + xywh_res['category'] = self.labels[xywh_res['category_id']] + results.append(xywh_res) + if len(preds) > 1: + im_shape = preprocessed_inputs['im_shape'] + bboxes['im_shape'] = (im_shape, []) + bboxes['mask'] = (np.array(preds[1]), [[len(preds[1])]]) + segm_results = paddlex.cv.models.utils.detection_eval.mask2out( + [bboxes], clsid2catid, self.mask_head_resolution) + import pycocotools.mask as mask_util + for i in range(len(results)): + results[i]['mask'] = mask_util.decode(segm_results[i][ + 'segmentation']) + return results + + def predict(self, image, topk=1, threshold=0.5): + """ 图片预测 + + Args: + image(str|np.ndarray): 图片路径或np.ndarray格式,如果后者,要求为BGR输入格式 + topk(int): 分类预测时使用,表示预测前topk的结果 + """ + preprocessed_input = self.preprocess(image) + model_pred = self.raw_predict(preprocessed_input) + + if self.model_type == "classifier": + results = self.classifier_postprocess(model_pred, topk) + elif self.model_type == "detector": + results = self.detector_postprocess(model_pred, preprocessed_input) + elif self.model_type == "segmenter": + results = self.segmenter_postprocess(model_pred, + preprocessed_input) + return results diff --git a/paddlex/interpret/as_data_reader/__init__.py b/paddlex/interpret/as_data_reader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d11e265597c7c8e39098a228108da3bb954b892 --- /dev/null +++ b/paddlex/interpret/as_data_reader/__init__.py @@ -0,0 +1,13 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. diff --git a/paddlex/interpret/core/__init__.py b/paddlex/interpret/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d11e265597c7c8e39098a228108da3bb954b892 --- /dev/null +++ b/paddlex/interpret/core/__init__.py @@ -0,0 +1,13 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. diff --git a/setup.py b/setup.py index a67c477c0310e549f797e8e273b9ef03159242c5..ffc6377b6800138e615e7bf918ef918b0185f60e 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setuptools.setup( setup_requires=['cython', 'numpy'], install_requires=[ "pycocotools;platform_system!='Windows'", 'pyyaml', 'colorama', 'tqdm', - 'paddleslim==1.0.1', 'visualdl==2.0.0a2' + 'paddleslim==1.0.1', 'visualdl>=2.0.0a2' ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/tutorials/interpret/interpret.py b/tutorials/interpret/interpret.py index dd079129ff5ee877e8ff4342273ca571cee1353e..a052ad8bc4983cb93bcc5f761428ead0aa4f5261 100644 --- a/tutorials/interpret/interpret.py +++ b/tutorials/interpret/interpret.py @@ -4,44 +4,38 @@ os.environ['CUDA_VISIBLE_DEVICES'] = '0' import os.path as osp import paddlex as pdx -from paddlex.cls import transforms # 下载和解压Imagenet果蔬分类数据集 veg_dataset = 'https://bj.bcebos.com/paddlex/interpret/mini_imagenet_veg.tar.gz' pdx.utils.download_and_decompress(veg_dataset, path='./') -# 定义测试集的transform -test_transforms = transforms.Compose([ - transforms.ResizeByShort(short_size=256), - transforms.CenterCrop(crop_size=224), - transforms.Normalize() -]) +# 下载和解压已训练好的MobileNetV2模型 +model_file = 'https://bj.bcebos.com/paddlex/interpret/mini_imagenet_veg_mobilenetv2.tar.gz' +pdx.utils.download_and_decompress(model_file, path='./') + +# 加载模型 +model = pdx.load_model('mini_imagenet_veg_mobilenetv2') # 定义测试所用的数据集 test_dataset = pdx.datasets.ImageNet( data_dir='mini_imagenet_veg', file_list=osp.join('mini_imagenet_veg', 'test_list.txt'), label_list=osp.join('mini_imagenet_veg', 'labels.txt'), - transforms=test_transforms) - -# 下载和解压已训练好的MobileNetV2模型 -model_file = 'https://bj.bcebos.com/paddlex/interpret/mini_imagenet_veg_mobilenetv2.tar.gz' -pdx.utils.download_and_decompress(model_file, path='./') - -# 导入模型 -model = pdx.load_model('mini_imagenet_veg_mobilenetv2') + transforms=model.test_transforms) # 可解释性可视化 -save_dir = 'interpret_results' -if not osp.exists(save_dir): - os.makedirs(save_dir) -pdx.interpret.visualize('mini_imagenet_veg/mushroom/n07734744_1106.JPEG', - model, - test_dataset, - algo='lime', - save_dir=save_dir) -pdx.interpret.visualize('mini_imagenet_veg/mushroom/n07734744_1106.JPEG', - model, - test_dataset, - algo='normlime', - save_dir=save_dir) \ No newline at end of file +# LIME算法 +pdx.interpret.visualize( + 'mini_imagenet_veg/mushroom/n07734744_1106.JPEG', + model, + test_dataset, + algo='lime', + save_dir='./') + +# NormLIME算法 +pdx.interpret.visualize( + 'mini_imagenet_veg/mushroom/n07734744_1106.JPEG', + model, + test_dataset, + algo='normlime', + save_dir='./')