深度學(xué)習(xí)應(yīng)用的服務(wù)端部署
【GiantPandaCV導(dǎo)讀】這篇文章包含與PyTorch模型部署相關(guān)的兩部分內(nèi)容:
PyTorch-YOLOv3模型的Web頁面展示程序的編寫
模型的服務(wù)接口相關(guān)工具的使用
0. 環(huán)境依賴:
系統(tǒng):Ubuntu 18.04
Python版本:3.7
依賴Python包:1. PyTorch==1.3 2. Flask==0.12 3. Gunicorn
需要注意的是Flask 0.12中默認(rèn)的單進(jìn)程單線程,而最新的1.0.2則不是(具體是多線程還是多進(jìn)程尚待考證),而中文博客里面能查到的資料基本都在說Flask默認(rèn)單進(jìn)程單線程。
依賴工具 1. nginx 2. apache2-utils
nginx 用于代理轉(zhuǎn)發(fā)和負(fù)載均衡,apache2-utils用于測試接口
1. 制作模型演示界面
圖像識(shí)別任務(wù)的展示這項(xiàng)工程一般是面向客戶的,這種場景下不可能把客戶拉到你的電腦前面,敲一行命令,等matplotlib彈個(gè)結(jié)果窗口出來。總歸還是要有個(gè)圖形化界面才顯得有點(diǎn)誠意。
為了節(jié)約時(shí)間,我們選擇了Flask框架來開發(fā)這個(gè)界面。
上傳頁面和展示頁面
做識(shí)別演示需要用到兩個(gè)html頁面,代碼也比較簡單,編寫如下:
上傳界面
"en">
????"UTF-8">
????Flask上傳圖片演示
????使用Flask上傳本地圖片
????
展示界面
"en">
????"UTF-8">
????Flask上傳圖片演示
????使用Flask上傳本地圖片
????
????"{{?url_for('static',?filename=?path,_t=val1)?}}"?width="400"?height="400"?alt="圖片識(shí)別失敗"/>
上傳界面如下圖所示,覺得丑的話可以找前端同事美化一下:

flask上傳圖片及展示功能
然后就可以編寫flask代碼了,為了更好地展示圖片,可以向html頁面?zhèn)魅雸D片地址參數(shù)。
from?flask?import?Flask,?render_template,?request,?redirect,?url_for,?make_response,?jsonify
from?werkzeug.utils?import?secure_filename
import?os
import?cv2
import?time
from?datetime?import?timedelta
from?main?import?run,?conf
ALLOWED_EXTENSIONS?=?set([
????"png","jpg","JPG","PNG",?"bmp"
])
def?is_allowed_file(filename):
????return?'.'?in?filename?and?filename.rsplit('.',?1)[1]?in?ALLOWED_EXTENSIONS
app?=?Flask(__name__)
#?靜態(tài)文件緩存過期時(shí)間
app.send_file_max_age_default?=?timedelta(seconds=1)
@app.route("/upload",methods?=?['POST',?'GET'])
def?upload():
????if?request.method?==?"POST":
????????f?=?request.files['file']
????????if?not?(?f?and?is_allowed_file(f.filename)):
????????????return?jsonify({
????????????????"error":1001,?"msg":"請檢查上傳的圖片類型,僅限于png、PNG、jpg、JPG、bmp"
????????????})
????????user_input?=?request.form.get("name")
????????basepath?=?os.path.dirname(__file__)
????????upload_path?=?os.path.join(basepath,?"static/images",secure_filename(f.filename))
????????f.save(upload_path)
????????
????????detected_path?=?os.path.join(basepath,?"static/images",?"output"?+?secure_filename(f.filename))
????????run(upload_path,?conf,?detected_path)
????????#?return?render_template("upload_ok.html",?userinput?=?user_input,?val1=time.time(),?path?=?detected_path)
????????path?=?"/images/"?+?"output"?+?secure_filename(f.filename)
????????return?render_template("upload_ok.html",?path?=?path,?val1?=?time.time())
????return?render_template("upload.html")
if?__name__?==?"__main__":
????app.run(host='0.0.0.0',?port=8888,?debug=True)
目標(biāo)檢測函數(shù)
原項(xiàng)目中提供了detection.py來做批量的圖片檢測,需要稍微修改一下才能用來做flask代碼中的接口。
from?__future__?import?division
from?models?import?*
from?utils.utils?import?*
from?utils.datasets?import?*
import?os
import?sys
import?time
import?datetime
import?argparse
from?PIL?import?Image
import?torch
from?torchvision?import?datasets
from?torch.autograd?import?Variable
import?matplotlib.pyplot?as?plt
import?matplotlib.patches?as?patches
from?matplotlib.ticker?import?NullLocator
class?custom_dict(dict):
????def?__init__(self,?d?=?None):
????????if?d?is?not?None:
????????????for?k,v?in?d.items():
????????????????self[k]?=?v
????????return?super().__init__()
????def?__key(self,?key):
????????return?""?if?key?is?None?else?key.lower()
????def?__str__(self):
????????import?json
????????return?json.dumps(self)
????def?__setattr__(self,?key,?value):
????????self[self.__key(key)]?=?value
????def?__getattr__(self,?key):
????????return?self.get(self.__key(key))
????def?__getitem__(self,?key):
????????return?super().get(self.__key(key))
????def?__setitem__(self,?key,?value):
????????return?super().__setitem__(self.__key(key),?value)
conf?=?custom_dict({
????"model_def":"config/yolov3.cfg",
????"weights_path":"weights/yolov3.weights",
????"class_path":"data/coco.names",
????"conf_thres":0.8,
????"nms_thres":0.4,
????"img_size":416
})
def?run(img_path,?conf,?target_path):
????device?=?torch.device("cuda"?if?torch.cuda.is_available()?else?"cpu")
????os.makedirs("output",?exist_ok=True)
????classes?=?load_classes(conf.class_path)
????model?=?Darknet(conf.model_def,?img_size=conf.img_size).to(device)
????if?conf.weights_path.endswith(".weights"):
????????#?Load?darknet?weights
????????model.load_darknet_weights(conf.weights_path)
????else:
????????#?Load?checkpoint?weights
????????model.load_state_dict(torch.load(conf.weights_path))
????model.eval()?
????
????img?=?Image.open(img_path).convert("RGB")
????img?=?img.resize(((img.size[0]?//?32)?*?32,?(img.size[1]?//?32)?*?32))
????img_array?=?np.array(img)
????img_tensor?=?pad_to_square(transforms.ToTensor()(img),0)[0].unsqueeze(0)
????conf.img_size?=?img_tensor.shape[2]
????
????with?torch.no_grad():
????????detections?=?model(img_tensor)
????????detections?=?non_max_suppression(detections,?conf.conf_thres,?conf.nms_thres)[0]
????cmap?=?plt.get_cmap("tab20b")
????colors?=?[cmap(i)?for?i?in?np.linspace(0,?1,?20)]
????plt.figure()
????fig,?ax?=?plt.subplots(1)
????ax.imshow(img_array)
????if?detections?is?not?None:
????????#?Rescale?boxes?to?original?image
????????detections?=?rescale_boxes(detections,?conf.img_size,?img_array.shape[:2])
????????unique_labels?=?detections[:,?-1].cpu().unique()
????????n_cls_preds?=?len(unique_labels)
????????bbox_colors?=?random.sample(colors,?n_cls_preds)
????????for?x1,?y1,?x2,?y2,?conf,?cls_conf,?cls_pred?in?detections:
????????????print("\t+?Label:?%s,?Conf:?%.5f"?%?(classes[int(cls_pred)],?cls_conf.item()))
????????????box_w?=?x2?-?x1
????????????box_h?=?y2?-?y1
????????????color?=?bbox_colors[int(np.where(unique_labels?==?int(cls_pred))[0])]
????????????#?Create?a?Rectangle?patch
????????????bbox?=?patches.Rectangle((x1,?y1),?box_w,?box_h,?linewidth=2,?edgecolor=color,?facecolor="none")
????????????#?Add?the?bbox?to?the?plot
????????????ax.add_patch(bbox)
????????????#?Add?label
????????????plt.text(
????????????????x1,
????????????????y1,
????????????????s=classes[int(cls_pred)],
????????????????color="white",
????????????????verticalalignment="top",
????????????????bbox={"color":?color,?"pad":?0},
????????????)
????#?Save?generated?image?with?detections
????plt.axis("off")
????plt.gca().xaxis.set_major_locator(NullLocator())
????plt.gca().yaxis.set_major_locator(NullLocator())
????filename?=?img_path.split("/")[-1].split(".")[0]
????plt.savefig(target_path,?bbox_inches='tight',?pad_inches=0.0)
????plt.close()
if?__name__?==?"__main__":
????run("data/samples/dog.jpg",conf)
展示效果
編寫好了之后,啟動(dòng)server.py,在本地打開localhost:8888/upload就可以看到如下界面了,把圖片上傳上去,很快就能得到檢測結(jié)果。
結(jié)果如下圖所示:

想試用的同學(xué)可以點(diǎn)擊:http://106.13.201.241:8888/upload
2. 深度學(xué)習(xí)的服務(wù)接口編寫
接下來介紹的是在生產(chǎn)環(huán)境下的部署,使用的是flask+gunicorn+nginx的方式,可以處理較大規(guī)模的請求。
下面以圖像分類模型為例演示一下深度學(xué)習(xí)服務(wù)接口如何編寫。
對(duì)于深度學(xué)習(xí)工程師來說,學(xué)習(xí)這些內(nèi)容主要是了解一下自己的模型在生產(chǎn)環(huán)境的運(yùn)行方式,便于在服務(wù)出現(xiàn)問題的時(shí)候與開發(fā)的同事一起進(jìn)行調(diào)試。
flask服務(wù)接口
接口不需要有界面顯示,當(dāng)然也可以添加一個(gè)API介紹界面,方便調(diào)用者查看服務(wù)是否已經(jīng)啟動(dòng)。
from?flask?import?Flask,?request
from?werkzeug.utils?import?secure_filename
import?uuid
from?PIL?import?Image
import?os
import?time
import?base64
import?json
import?torch
from?torchvision.models?import?resnet18
from?torchvision.transforms?import?ToTensor
from?keys?import?key
app?=?Flask(__name__)
net?=?resnet18(pretrained=True)
net.eval()
@app.route("/",methods=["GET"])
def?show():
????return?"classifier?api"
@app.route("/run",methods?=?["GET","POST"])
def?run():
????file?=?request.files['file']
????base_path?=?os.path.dirname(__file__)
????if?not?os.path.exists(os.path.join(base_path,?"temp")):
????????os.makedirs(os.path.join(base_path,?"temp"))
????file_name?=?uuid.uuid4().hex
????upload_path?=?os.path.join(base_path,?"temp",?file_name)
????file.save(upload_path)
????img?=?Image.open(upload_path)
????img_tensor?=?ToTensor()(img).unsqueeze(0)
????out?=?net(img_tensor)
????pred?=?torch.argmax(out,dim?=?1)
????return?"result?:?{}".format(key[pred])
if?__name__?==?"__main__":
????app.run(host="0.0.0.0",port=5555,debug=True)
在命令行輸入python server.py即可啟動(dòng)服務(wù)。
gunicorn啟動(dòng)多個(gè)實(shí)例
新版的flask已經(jīng)支持多進(jìn)程了,不過用在生產(chǎn)環(huán)境還是不太穩(wěn)定,一般生產(chǎn)環(huán)境會(huì)使用gunicorn來啟動(dòng)多個(gè)服務(wù)。
使用如下命令即可啟動(dòng)多個(gè)圖像分類實(shí)例
gunicorn -w 4 -b 0.0.0.0:5555 server:app
輸出如下內(nèi)容代表服務(wù)創(chuàng)建成功:
[2020-02-11 14:50:24 +0800] [892] [INFO] Starting gunicorn 20.0.4
[2020-02-11 14:50:24 +0800] [892] [INFO] Listening at: http://0.0.0.0:5555 (892)
[2020-02-11 14:50:24 +0800] [892] [INFO] Using worker: sync
[2020-02-11 14:50:24 +0800] [895] [INFO] Booting worker with pid: 895
[2020-02-11 14:50:24 +0800] [896] [INFO] Booting worker with pid: 896
[2020-02-11 14:50:24 +0800] [898] [INFO] Booting worker with pid: 898
[2020-02-11 14:50:24 +0800] [899] [INFO] Booting worker with pid: 899
如果配置比較復(fù)雜,也可以將配置寫入一個(gè)文件中,如:
bind?=?'0.0.0.0:5555'
timeout?=?10
workers?=?4
然后運(yùn)行:
gunicorn -c gunicorn.conf sim_server:app
nginx負(fù)載均衡
如果有多個(gè)服務(wù)器,可以使用nginx做請求分發(fā)與負(fù)載均衡。
安裝好nginx之后,修改nginx的配置文件
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
server
{
listen 5556; # nginx端口
server_name localhost;
location / {
proxy_pass http://localhost:5555/run; # gunicorn的url
}
}
}
然后按配置文件啟動(dòng)
sudo nginx -c nginx.conf
測試一下服務(wù)是否正常
啟動(dòng)了這么多服務(wù)之后,可以使用apache2-utils來測試服務(wù)的并發(fā)性能。
使用apache2-utils進(jìn)行上傳圖片的post請求方法參考:
https://gist.github.com/chiller/dec373004894e9c9bb38ac647c7ccfa8
嚴(yán)格參照,注意一個(gè)標(biāo)點(diǎn),一個(gè)符號(hào)都不要錯(cuò)。使用這種方法傳輸圖片的base64編碼,在服務(wù)端不需要解碼也能使用
然后使用下面的方式訪問
gunicorn 接口
ab -n 2 -c 2 -T "multipart/form-data; boundary=1234567890" -p turtle.txt http://localhost:5555/run
nginx 接口
ab -n 2 -c 2 -T "multipart/form-data; boundary=1234567890" -p turtle.txt http://localhost:5556/run
有了gunicorn和nginx就可以輕松地實(shí)現(xiàn)PyTorch模型的多機(jī)多卡部署了。
歡迎關(guān)注GiantPandaCV, 在這里你將看到獨(dú)家的深度學(xué)習(xí)分享,堅(jiān)持原創(chuàng),每天分享我們學(xué)習(xí)到的新鮮知識(shí)。( ? ?ω?? )?
有對(duì)文章相關(guān)的問題,或者想要加入交流群,歡迎添加BBuf微信:
