diff --git a/deploy/paddleserving/imgs/results_recog.png b/deploy/paddleserving/imgs/results_recog.png deleted file mode 100644 index 37393d5d64e84de469d78dcc9fad88aa771f57f8..0000000000000000000000000000000000000000 Binary files a/deploy/paddleserving/imgs/results_recog.png and /dev/null differ diff --git a/deploy/paddleserving/imgs/results_shitu.png b/deploy/paddleserving/imgs/results_shitu.png new file mode 100644 index 0000000000000000000000000000000000000000..b8dc2480557aed6f56e8067324d450444ab37c73 Binary files /dev/null and b/deploy/paddleserving/imgs/results_shitu.png differ diff --git a/deploy/paddleserving/imgs/start_server_recog.png b/deploy/paddleserving/imgs/start_server_recog.png deleted file mode 100644 index d4344a1e6bdab7ccc4c3c31bc16d1e3186b9b806..0000000000000000000000000000000000000000 Binary files a/deploy/paddleserving/imgs/start_server_recog.png and /dev/null differ diff --git a/deploy/paddleserving/imgs/start_server_shitu.png b/deploy/paddleserving/imgs/start_server_shitu.png new file mode 100644 index 0000000000000000000000000000000000000000..32eb0cfe6757ed9f941dbf7aec7ffc96e6f45a2f Binary files /dev/null and b/deploy/paddleserving/imgs/start_server_shitu.png differ diff --git a/deploy/paddleserving/recognition/config.yml b/deploy/paddleserving/recognition/config.yml index f67ee5521b5135b28f0b945cf800f477e18cc787..9e4a79ec47e88cab4a3f7d2d84d348167642c0d0 100644 --- a/deploy/paddleserving/recognition/config.yml +++ b/deploy/paddleserving/recognition/config.yml @@ -18,7 +18,7 @@ op: local_service_conf: #uci模型路径 - model_config: ../../models/product_ResNet50_vd_aliproduct_v1.0_serving + model_config: ../../models/general_PPLCNet_x2_5_lite_v1.0_serving #计算硬件类型: 空缺时由devices决定(CPU/GPU),0=cpu, 1=gpu, 2=tensorRT, 3=arm cpu, 4=kunlun xpu device_type: 1 @@ -40,4 +40,4 @@ op: devices: '0' fetch_list: - save_infer_model/scale_0.tmp_1 - model_config: ../../models/ppyolov2_r50vd_dcn_mainbody_v1.0_serving/ \ No newline at end of file + model_config: ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving/ \ No newline at end of file diff --git a/deploy/paddleserving/recognition/pipeline_http_client.py b/deploy/paddleserving/recognition/pipeline_http_client.py index aa0cb5429b7a5ef06567a1655d6f5d89f489ecea..7e92fc6c03f733b1841078e77fefb7fa62318540 100644 --- a/deploy/paddleserving/recognition/pipeline_http_client.py +++ b/deploy/paddleserving/recognition/pipeline_http_client.py @@ -3,15 +3,17 @@ import json import base64 import os -imgpath = "daoxiangcunjinzhubing_6.jpg" +imgpath = "../../drink_dataset_v1.0/test_images/001.jpeg" + def cv2_to_base64(image): return base64.b64encode(image).decode('utf8') + if __name__ == "__main__": url = "http://127.0.0.1:18081/recognition/prediction" - with open(os.path.join(".", imgpath), 'rb') as file: + with open(os.path.join(".", imgpath), 'rb') as file: image_data1 = file.read() image = cv2_to_base64(image_data1) data = {"key": ["image"], "value": [image]} diff --git a/deploy/paddleserving/recognition/recognition_web_service.py b/deploy/paddleserving/recognition/recognition_web_service.py index 88daf96e6ba2ea9b5bf030f2a2bd83c6d645bd5e..3425e23fba5c2e27ae9299c314e0ed7d6a0ff121 100644 --- a/deploy/paddleserving/recognition/recognition_web_service.py +++ b/deploy/paddleserving/recognition/recognition_web_service.py @@ -23,6 +23,7 @@ import faiss import pickle import json + class DetOp(Op): def init_op(self): self.img_preprocess = Sequential([ @@ -62,50 +63,57 @@ class DetOp(Op): im_scale_y, im_scale_x = self.generate_scale(raw_im) im = self.img_preprocess(raw_im) - + imgs.append({ - "image": im[np.newaxis, :], - "im_shape": np.array(list(im.shape[1:])).reshape(-1)[np.newaxis,:], - "scale_factor": np.array([im_scale_y, im_scale_x]).astype('float32'), + "image": im[np.newaxis, :], + "im_shape": + np.array(list(im.shape[1:])).reshape(-1)[np.newaxis, :], + "scale_factor": + np.array([im_scale_y, im_scale_x]).astype('float32'), }) self.raw_img = raw_imgs feed_dict = { - "image": np.concatenate([x["image"] for x in imgs], axis=0), - "im_shape": np.concatenate([x["im_shape"] for x in imgs], axis=0), - "scale_factor": np.concatenate([x["scale_factor"] for x in imgs], axis=0) + "image": np.concatenate( + [x["image"] for x in imgs], axis=0), + "im_shape": np.concatenate( + [x["im_shape"] for x in imgs], axis=0), + "scale_factor": np.concatenate( + [x["scale_factor"] for x in imgs], axis=0) } - return feed_dict, False, None, "" + return feed_dict, False, None, "" def postprocess(self, input_dicts, fetch_dict, log_id): boxes = self.img_postprocess(fetch_dict, visualize=False) - boxes.sort(key = lambda x: x["score"], reverse = True) - boxes = filter(lambda x: x["score"] >= self.threshold, boxes[:self.max_det_results]) + boxes.sort(key=lambda x: x["score"], reverse=True) + boxes = filter(lambda x: x["score"] >= self.threshold, + boxes[:self.max_det_results]) boxes = list(boxes) for i in range(len(boxes)): boxes[i]["bbox"][2] += boxes[i]["bbox"][0] - 1 boxes[i]["bbox"][3] += boxes[i]["bbox"][1] - 1 result = json.dumps(boxes) res_dict = {"bbox_result": result, "image": self.raw_img} - return res_dict, None, "" + return res_dict, None, "" + class RecOp(Op): def init_op(self): self.seq = Sequential([ - BGR2RGB(), Resize((224, 224)), - Div(255), Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225], - False), Transpose((2, 0, 1)) + BGR2RGB(), Resize((224, 224)), Div(255), + Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225], + False), Transpose((2, 0, 1)) ]) - index_dir = "../../recognition_demo_data_v1.1/gallery_product/index" + index_dir = "../../drink_dataset_v1.0/index" assert os.path.exists(os.path.join( index_dir, "vector.index")), "vector.index not found ..." assert os.path.exists(os.path.join( index_dir, "id_map.pkl")), "id_map.pkl not found ... " - + self.searcher = faiss.read_index( os.path.join(index_dir, "vector.index")) - + with open(os.path.join(index_dir, "id_map.pkl"), "rb") as fd: self.id_map = pickle.load(fd) @@ -121,24 +129,25 @@ class RecOp(Op): origin_img = cv2.imdecode(data, cv2.IMREAD_COLOR) dt_boxes = input_dict["bbox_result"] boxes = json.loads(dt_boxes) - boxes.append({"category_id": 0, - "score": 1.0, - "bbox": [0, 0, origin_img.shape[1], origin_img.shape[0]] - }) + boxes.append({ + "category_id": 0, + "score": 1.0, + "bbox": [0, 0, origin_img.shape[1], origin_img.shape[0]] + }) self.det_boxes = boxes #construct batch images for rec imgs = [] for box in boxes: box = [int(x) for x in box["bbox"]] - im = origin_img[box[1]: box[3], box[0]: box[2]].copy() + im = origin_img[box[1]:box[3], box[0]:box[2]].copy() img = self.seq(im) imgs.append(img[np.newaxis, :].copy()) input_imgs = np.concatenate(imgs, axis=0) - return {"x": input_imgs}, False, None, "" + return {"x": input_imgs}, False, None, "" - def nms_to_rec_results(self, results, thresh = 0.1): + def nms_to_rec_results(self, results, thresh=0.1): filtered_results = [] x1 = np.array([r["bbox"][0] for r in results]).astype("float32") y1 = np.array([r["bbox"][1] for r in results]).astype("float32") @@ -172,7 +181,7 @@ class RecOp(Op): np.sum(np.square(batch_features), axis=1, keepdims=True)) batch_features = np.divide(batch_features, feas_norm) - scores, docs = self.searcher.search(batch_features, self.return_k) + scores, docs = self.searcher.search(batch_features, self.return_k) results = [] for i in range(scores.shape[0]): @@ -182,17 +191,19 @@ class RecOp(Op): pred["rec_docs"] = self.id_map[docs[i][0]].split()[1] pred["rec_scores"] = scores[i][0] results.append(pred) - + #do nms results = self.nms_to_rec_results(results, self.rec_nms_thresold) return {"result": str(results)}, None, "" + class RecognitionService(WebService): def get_pipeline_response(self, read_op): det_op = DetOp(name="det", input_ops=[read_op]) rec_op = RecOp(name="rec", input_ops=[det_op]) return rec_op + product_recog_service = RecognitionService(name="recognition") product_recog_service.prepare_pipeline_config("config.yml") product_recog_service.run_service() diff --git a/docs/zh_CN/inference_deployment/paddle_serving_deploy.md b/docs/zh_CN/inference_deployment/paddle_serving_deploy.md index 94f8906c4d7ce84f25731363824d12c9ac9283fb..74bbc20a7896981afecb577b29156eda6c027b55 100644 --- a/docs/zh_CN/inference_deployment/paddle_serving_deploy.md +++ b/docs/zh_CN/inference_deployment/paddle_serving_deploy.md @@ -1,11 +1,17 @@ # 模型服务化部署 +- [简介](#简介) +- [Serving安装](#Serving安装) +- [图像分类服务部署](#图像分类服务部署) +- [图像识别服务部署](#图像识别服务部署) +- [FAQ](#FAQ) + ## 1. 简介 [Paddle Serving](https://github.com/PaddlePaddle/Serving) 旨在帮助深度学习开发者轻松部署在线预测服务,支持一键部署工业级的服务能力、客户端和服务端之间高并发和高效通信、并支持多种编程语言开发客户端。 该部分以 HTTP 预测服务部署为例,介绍怎样在 PaddleClas 中使用 PaddleServing 部署模型服务。 - + ## 2. Serving安装 Serving 官网推荐使用 docker 安装并部署 Serving 环境。首先需要拉取 docker 环境并创建基于 Serving 的 docker。 @@ -22,6 +28,7 @@ nvidia-docker exec -it test bash pip install paddlepaddle-gpu pip install paddle-serving-client pip install paddle-serving-server-gpu +pip install paddle-serving-app ``` * 如果安装速度太慢,可以通过 `-i https://pypi.tuna.tsinghua.edu.cn/simple` 更换源,加速安装过程。 @@ -31,35 +38,186 @@ pip install paddle-serving-server-gpu ```shell pip install paddle-serving-server ``` - -## 3. 导出模型 - -使用 `tools/export_serving_model.py` 脚本导出 Serving 模型,以 `ResNet50_vd` 为例,使用方法如下。 - + +## 3. 图像分类服务部署 +### 3.1 模型转换 +使用PaddleServing做服务化部署时,需要将保存的inference模型转换为Serving模型。下面以经典的ResNet50_vd模型为例,介绍如何部署图像分类服务。 +- 进入工作目录: ```shell -python tools/export_serving_model.py -m ResNet50_vd -p ./pretrained/ResNet50_vd_pretrained/ -o serving +cd deploy/paddleserving +``` +- 下载ResNet50_vd的inference模型: +```shell +# 下载并解压ResNet50_vd模型 +wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/inference/ResNet50_vd_infer.tar && tar xf ResNet50_vd_infer.tar +``` +- 用paddle_serving_client把下载的inference模型转换成易于Server部署的模型格式: +``` +# 转换ResNet50_vd模型 +python3 -m paddle_serving_client.convert --dirname ./ResNet50_vd_infer/ \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --serving_server ./ResNet50_vd_serving/ \ + --serving_client ./ResNet50_vd_client/ +``` +ResNet50_vd推理模型转换完成后,会在当前文件夹多出`ResNet50_vd_serving` 和`ResNet50_vd_client`的文件夹,具备如下格式: ``` +|- ResNet50_vd_client/ + |- __model__ + |- __params__ + |- serving_server_conf.prototxt + |- serving_server_conf.stream.prototxt +|- ResNet50_vd_client + |- serving_client_conf.prototxt + |- serving_client_conf.stream.prototxt +``` +得到模型文件之后,需要修改serving_server_conf.prototxt中的alias名字: 将`feed_var`中的`alias_name`改为`image`, 将`fetch_var`中的`alias_name`改为`prediction` -最终在 serving 文件夹下会生成 `ppcls_client_conf` 与 `ppcls_model` 两个文件夹,分别存储了 client 配置、模型参数与结构文件。 +**备注**: Serving为了兼容不同模型的部署,提供了输入输出重命名的功能。这样,不同的模型在推理部署时,只需要修改配置文件的alias_name即可,无需修改代码即可完成推理部署。 +修改后的serving_server_conf.prototxt如下所示: +``` +feed_var { + name: "inputs" + alias_name: "image" + is_lod_tensor: false + feed_type: 1 + shape: 3 + shape: 224 + shape: 224 +} +fetch_var { + name: "save_infer_model/scale_0.tmp_1" + alias_name: "prediction" + is_lod_tensor: true + fetch_type: 1 + shape: -1 +} +``` +### 3.2 服务部署和请求 +paddleserving目录包含了启动pipeline服务和发送预测请求的代码,包括: +```shell +__init__.py +config.yml # 启动服务的配置文件 +pipeline_http_client.py # http方式发送pipeline预测请求的脚本 +pipeline_rpc_client.py # rpc方式发送pipeline预测请求的脚本 +classification_web_service.py # 启动pipeline服务端的脚本 +``` +- 启动服务: +```shell +# 启动服务,运行日志保存在log.txt +python3 classification_web_service.py &>log.txt & +``` +成功启动服务后,log.txt中会打印类似如下日志 +![](../../../deploy/paddleserving/imgs/start_server.png) -## 4. 服务部署与请求 +- 发送请求: +```shell +# 发送服务请求 +python3 pipeline_http_client.py +``` +成功运行后,模型预测的结果会打印在cmd窗口中,结果示例为: +![](../../../deploy/paddleserving/imgs/results.png) + + +## 4.图像识别服务部署 +使用PaddleServing做服务化部署时,需要将保存的inference模型转换为Serving模型。 下面以PP-ShiTu中的超轻量图像识别模型为例,介绍图像识别服务的部署。 +## 4.1 模型转换 +- 下载通用检测inference模型和通用识别inference模型 +``` +cd deploy +# 下载并解压通用识别模型 +wget -P models/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/general_PPLCNet_x2_5_lite_v1.0_infer.tar +cd models +tar -xf general_PPLCNet_x2_5_lite_v1.0_infer.tar +# 下载并解压通用检测模型 +wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer.tar +tar -xf picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer.tar +``` +- 转换识别inference模型为Serving模型: +``` +# 转换识别模型 +python3 -m paddle_serving_client.convert --dirname ./general_PPLCNet_x2_5_lite_v1.0_infer/ \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --serving_server ./general_PPLCNet_x2_5_lite_v1.0_serving/ \ + --serving_client ./general_PPLCNet_x2_5_lite_v1.0_client/ +``` +识别推理模型转换完成后,会在当前文件夹多出`general_PPLCNet_x2_5_lite_v1.0_serving/` 和`general_PPLCNet_x2_5_lite_v1.0_serving/`的文件夹。修改`general_PPLCNet_x2_5_lite_v1.0_serving/`目录下的serving_server_conf.prototxt中的alias名字: 将`fetch_var`中的`alias_name`改为`features`。 +修改后的serving_server_conf.prototxt内容如下: +``` +feed_var { + name: "x" + alias_name: "x" + is_lod_tensor: false + feed_type: 1 + shape: 3 + shape: 224 + shape: 224 +} +fetch_var { + name: "save_infer_model/scale_0.tmp_1" + alias_name: "features" + is_lod_tensor: true + fetch_type: 1 + shape: -1 +} +``` +- 转换通用检测inference模型为Serving模型: +``` +# 转换通用检测模型 +python3 -m paddle_serving_client.convert --dirname ./picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer/ \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --serving_server ./picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving/ \ + --serving_client ./picodet_PPLCNet_x2_5_mainbody_lite_v1.0_client/ +``` +检测inference模型转换完成后,会在当前文件夹多出`picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving/` 和`picodet_PPLCNet_x2_5_mainbody_lite_v1.0_client/`的文件夹。 -* 使用下面的方式启动 Serving 服务。 +**注意:** 此处不需要修改`picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving/`目录下的serving_server_conf.prototxt中的alias名字。 +- 下载并解压已经构建后的检索库index +``` +cd ../ +wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/data/drink_dataset_v1.0.tar && tar -xf drink_dataset_v1.0.tar +``` +## 4.2 服务部署和请求 +**注意:** 识别服务涉及到多个模型,出于性能考虑采用PipeLine部署方式。Pipeline部署方式当前不支持windows平台。 +- 进入到工作目录 ```shell -python tools/serving/image_service_gpu.py serving/ppcls_model workdir 9292 +cd ./deploy/paddleserving/recognition ``` +paddleserving目录包含启动pipeline服务和发送预测请求的代码,包括: +``` +__init__.py +config.yml # 启动服务的配置文件 +pipeline_http_client.py # http方式发送pipeline预测请求的脚本 +pipeline_rpc_client.py # rpc方式发送pipeline预测请求的脚本 +recognition_web_service.py # 启动pipeline服务端的脚本 +``` +- 启动服务: +``` +# 启动服务,运行日志保存在log.txt +python3 recognition_web_service.py &>log.txt & +``` +成功启动服务后,log.txt中会打印类似如下日志 +![](../../../deploy/paddleserving/imgs/start_server_shitu.png) -其中 `serving/ppcls_model` 为刚才保存的 Serving 模型地址,`workdir` 为工作目录,`9292` 为服务的端口号。 - +- 发送请求: +``` +python3 pipeline_http_client.py +``` +成功运行后,模型预测的结果会打印在cmd窗口中,结果示例为: +![](../../../deploy/paddleserving/imgs/results_shitu.png) -* 使用下面的脚本向 Serving 服务发送识别请求,并返回结果。 + +## 5.FAQ +**Q1**: 发送请求后没有结果返回或者提示输出解码报错 +**A1**: 启动服务和发送请求时不要设置代理,可以在启动服务前和发送请求前关闭代理,关闭代理的命令是: ``` -python tools/serving/image_http_client.py 9292 ./docs/images/logo.png +unset https_proxy +unset http_proxy ``` -`9292` 为发送请求的端口号,需要与服务启动时的端口号保持一致,`./docs/images/logo.png` 为待识别的图像文件。最终返回 Top1 识别结果的类别 ID 以及概率值。 - -* 更多的服务部署类型,如 `RPC预测服务` 等,可以参考 Serving 的 github 官网:[https://github.com/PaddlePaddle/Serving/tree/develop/python/examples/imagenet](https://github.com/PaddlePaddle/Serving/tree/develop/python/examples/imagenet) +更多的服务部署类型,如 `RPC预测服务` 等,可以参考 Serving 的[github 官网](https://github.com/PaddlePaddle/Serving/tree/develop/python/examples/imagenet)