做为深度学习小白一枚,从一开始摸索如何使用深度学习框架,怎么让脚本跑起来,到如今开始逐步读懂论文,看懂模型的网络结构,按照飞桨官方文档进行各类模型训练和部署,整个过程遇到了无数问题。很是感谢飞桨开源社区的大力支持,并热情答复我遇到的各类问题,使得我能够快速上手。特整理本篇学习笔记,以此回馈网友们的无私付出。你们都共享一点点,一块儿为深度学习的推动添砖加瓦(哈哈,很是正能量,有木有!)java
这篇文章详细记录了如何使用百度深度学习平台——飞桨进行SSD目标检测模型的训练、以及如何将模型部署到服务器和移动端。文末给出了笔者认为很是有用的资料连接。python
下载安装命令
## CPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle
## GPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu
本文的代码基于百度AI Studio官方示例代码,并可以在飞桨 1.7.1上跑通,Python版本是3.7。git
SSD模型介绍
若是你对经典的CNN模型比较熟悉的话,那么SSD也并不难理解。SSD大致上来讲是将图片分为6种不一样大小的网格,找到目标中心的落点,肯定物体的位置。在分红不一样网格以后,会在此之上取到不一样数目的先验框,对先验框进行回归、分类预测。先验框的数目足够多,几乎可以涵盖整个图片,所以咱们能够找到包含物体的不少个先验框,最后进行非极大抑制就能获得正确结果。github
b图就是咱们以每一个网格为中心,取到的先验框的示例。c图的回归预测找到了目标的位置信息,分类预测肯定了物体的类别。a图表明了最终的结果。web
上面的两个图片摘自论文SSD: Single Shot MultiBox Detector,在论文中SSD是插入到VGG-16网络中的。docker
经过一个表格咱们可以知道咱们从不一样层中获得的先验框尺寸和数目:json
总共咱们会得到8732个先验框。flask
MobileNet 与 SSD结合
前面说到咱们能够很方便地将SSD插入到不一样网络,那么考虑到咱们的应用场景,咱们可使用诸如MobileNet网络来减小计算量。后端
MobileNet将卷积分为Depthwise和Pointwise两部分,减小了计算量,同时不会损失过多的精度。也所以在移动设备和嵌入式设备上面有很好的应用前景。更多关于MobileNet的理论信息你们能够在网上找到,这里不作过多讲述。数组
百度AI Studio上官方开源了基于SSD的目标检测模型的代码,代码很是好读,并能够直接在线运行,同时提供了训练好的SSD模型。从代码中咱们能够看到,飞桨提供了paddle.fluid.layers.multi_box_head在不一样Feature Map上面提取先验框、计算回归坐标等,paddle.fluid.layers.ssd_loss计算loss,paddle.fluid.initializer.MSRAInitializer实现以MSRA的方式初始化权重等等。这些API可以减轻咱们的工做量,方便代码编写。官方代码还能够导出,在本地Python 3和飞桨 1.7上执行。
服务器部署
下面咱们来使用Paddle Serving做为模型即服务后端。随着飞桨框架推出1.7版本,Paddle Serving也登上了舞台。Paddle Serving提出了模型即服务的理念,致力于简化模型部署到服务器操做,甚至一行命令实现模型部署。有了Paddle Serving,能够大大减轻搭建部署环境的负担。
须要注意的是Paddle Serving目前不支持arm64架构,而且对一些依赖包的版本有要求,因此强烈建议使用Docker进行部署。
首先咱们pull到Docker 镜像:
# Run CPU Docker docker pull hub.baidubce.com/paddlepaddle/serving:0.2.0 docker run -p 9292:9292 --name test -dit hub.baidubce.com/paddlepaddle/serving:0.2.0 docker exec -it test bash # Run GPU Docker nvidia-docker pull hub.baidubce.com/paddlepaddle/serving:0.2.0-gpu nvidia-docker run -p 9292:9292 --name test -dit hub.baidubce.com/paddlepaddle/serving:0.2.0-gpu nvidia-docker exec -it test bash
进入容器以后,因为官方缩减了镜像的大小,咱们须要手动安装须要的依赖包:
python3 -m pip install paddle_serving_server sentencepiece opencv-python pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
镜像使用的系统是Centos 7,注意直接运行Python的话指向的是Python 2.7.5,你须要使用python3。(Python 2即将中止维护,pip在后续版本也可能不提供支持)。
Paddle Serving与直接利用模型不一样的是,除了须要导出inference model之外还须要生成配置文件,定义Feed和Fetch的内容。若是你很是熟悉保存预测模型的接口,那么这并非一件难事。从零开始训练一个模型,并应用到Paddle Serving,你能够参考官方的端到端从训练到部署全流程
(https://github.com/PaddlePaddle/Serving/blob/develop/doc/TRAIN_TO_SERVICE_CN.md)。
这里咱们能够直接利用上文提到的AI Studio的开源项目进行提取,真正的提取代码仅须要两行:
import paddle_serving_client.io as serving_io serving_io.save_model( "ssd_model", "ssd_client_conf", {'image': img}, {"prediction": box}, inference_program)
前两行定义了咱们的模型和客户端配置文件保存位置,后面的两个dict分别表示feed和fetch的内容,官方文档的例子表示这是咱们在训练模型时的输入和输出。这里的img和box即为输入网络的img和网络输出的box,咱们看下两个的结构。
img: name: "img" type { type: LOD_TENSOR lod_tensor { tensor { data_type: FP32 dims: -1 dims: 3 dims: 300 dims: 300 } lod_level: 0 } } persistable: false box: name: "concat_0.tmp_0" type { type: LOD_TENSOR lod_tensor { tensor { data_type: FP32 dims: 1917 dims: 4 } lod_level: 0 } } persistable: false
能够在保存预测模型的时候保存Paddle Serving须要的配置项,或者以后从训练的代码中提取出img和box,进行保存。获得Paddle Serving须要的相关文件以后,利用下面的代码将其部署到服务器上(均在容器内进行,保证生成的模型和客户端配置和服务器脚本在同一目录之下):
import os import sys import base64 import numpy as np import importlib from paddle_serving_app import ImageReader from multiprocessing import freeze_support from paddle_serving_server.web_service import WebService class ImageService(WebService): def preprocess(self, feed={}, fetch=[]): reader = ImageReader(image_shape=[3, 300, 300], image_mean=[0.5, 0.5, 0.5], image_std=[0.5, 0.5, 0.5]) feed_batch = [] for ins in feed: if "image" not in ins: raise ("feed data error!") sample = base64.b64decode(ins["image"]) img = reader.process_image(sample) feed_batch.append({"image": img}) return feed_batch, fetch image_service = ImageService(name="image") image_service.load_model_config("./ssd_model/") image_service.prepare_server( workdir="./work", port=int(9292), device="cpu") image_service.run_server() image_service.run_flask()
在代码中先对获得的image进行了resize,而后交给模型处理。这里使用的是CPU进行预测,须要的话能够修改几行代码使其可以在GPU上预测。使用Paddle Serving并不须要安装飞桨,因此不会对服务器形成负担。Paddle Serving内置了数据预处理功能,所以能够直接对图片进行裁剪等操做。
在客户端上,仅仅须要几行代码就可以从服务端获取预测结果:
import requests import base64 import json import time import os import sys py_version = sys.version_info[0] def predict(image_path, server): if py_version == 2: image = base64.b64encode(open(image_path).read()) else: image = base64.b64encode(open(image_path, "rb").read()).decode("utf-8") req = json.dumps({"feed": [{"image": image}], "fetch": ["prediction"]}) r = requests.post( server, data=req, headers={"Content-Type": "application/json"}, timeout=60) try: print(r.json()["result"]["prediction"]) except ValueError: print(r.text) return r if __name__ == "__main__": server = "http://[ip]:[port]/image/prediction" image_list = os.listdir("./images") start = time.time() for img in image_list: image_file = "./images/" + img res = predict(image_file, server) end = time.time() print(end - start)
对图片进行base64编码,发送到服务端,获取结果,很是简洁和方便。在实际部署的过程当中,能够在服务端进行反代和鉴权,只须要写一个中间件便可,这也是模型即服务带给你们的便利之处。
咱们国内服务端的配置是单核CPU(限制使用时间和频率),算上网络传输和预测的总用时在0.39秒左右,比较快速。返回的数组第一个值表明了对应类别,第二个值表明置信度,后面的值表明坐标比例,实际使用的时候须要设置阈值,放弃可信度较低的值。
移动端部署
移动端部署采用了以前开源的Real-time Object Detector,当时源码中使用的是YOLO v3模型,这里咱们将使其适配SSD模型。在端侧部署方面咱们使用的是Paddle Lite,这是飞桨系列中的多平台高性能深度学习预测引擎,提供了多平台架构下的预测解决方案,还支持C++/Java/Python等语言。
从上次发文到如今,Paddle Lite已经推出了新的版本,2.3版本对不少东西进行了优化,利用手上的安卓手机(麒麟 810)进行SSD目标检测的用时仅为500ms。此次咱们还可以直接使用官方提供的预编译库进行预测,并不须要本身手动编译一次。下载下来以后咱们会获得和上次同样的文件,PaddlePredictor.jar和一些so连接库,参考以前的推送文章:如何基于Flutter和Paddle Lite实现实时目标检测,放到相应位置便可。
由于SSD模型的输入和YOLO v3不同,咱们须要对安卓端的Predictor.java进行修改,主要考虑输入的尺寸问题。
// MainActivity.java L41 protected long[] inputShape = new long[]{1, 3, 300, 300}; protected float[] inputMean = new float[]{0.5f, 0.5f, 0.5f}; protected float[] inputStd = new float[]{0.5f, 0.5f, 0.5f}; // Predictor.java L214 // Set input shape Tensor inputTensor = getInput(0); inputTensor.resize(inputShape); // Predictor.java L258 inputTensor.setData(inputData); // Predictor.java L303 float rawLeft = outputTensor.getFloatData()[i + 2]; float rawTop = outputTensor.getFloatData()[i + 3]; float rawRight = outputTensor.getFloatData()[i + 4]; float rawBottom = outputTensor.getFloatData()[i + 5];
同时咱们对于描框的函数进行修改:
// main.dart L127 var ratioW = sizeRed.width / 300; var ratioH = sizeRed.height / 300;
若是在运行的时候出现了空指针错误,极可能你没有升级到最新的预编译库,jar和so文件均须要更新。因为上次发布源码的时候没有在Gradle脚本中设置自动下载库,因此须要手动放置预测库。
写在最后
从一开始熟悉怎么去使用飞桨深度学习平台,怎么让脚本跑起来,到如今开始逐步读懂论文,了解模型的架构,看官方文档,过程当中遇到了很多问题。经过分析飞桨官方图像分类示例,查看和修改源码,输出调试信息,还在飞桨官方QQ群中获得了很多帮助,学到了不少东西,并最终完成了此次实践。很是感谢提供帮助的朋友们。飞桨通过多轮更新,在模型训练和部署上也变得很是简单,相信会吸引愈来愈多的开发者使用。
下载安装命令
## CPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle
## GPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu