本文主要記錄在進行
Flask
部署過程當中所使用的流程,遇到的問題以及相應的解決方案。html
該部分簡要介紹一下前一段時間所作的工做:node
這是第一次進行深度學習模型的web應用部署,在整個過程當中,進一步折射出之前知識面之窄,在不斷的入坑、解坑中實現一版。python
這部分從項目實施的流程入手,記錄所作的工做及用到的工具。git
須要進行圖像分類,第一反應是利用較爲成熟與經典的分類網絡結構,如VGG系列(VGG16, VGG19
),ResNet系列(如ResNet50
),InceptionV3
等。github
考慮到是對未知類型的圖像進行分類,且沒有直接可用的訓練數據,所以使用在Imagenet
上訓練好的預訓練模型,基本知足要求。web
若是對性能(耗時)要求較爲嚴格,則建議使用深度較淺的網絡結構,如VGG16
, MobileNet
等。redis
其中,MobileNet
網絡是爲移動端和嵌入式端深度學習應用設計的網絡,使得在cpu上也能達到理想的速度要求。是一種輕量級的深度網絡結構。算法
MobileNet
由 Google 團隊
提出,發表於 CVPR-2017
,論文標題: 《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》數據庫
平時使用Keras
框架比較多,Keras
底層庫使用Theano
或Tensorflow
,也稱爲Keras的後端。Keras
是在Tensorflow
基礎上構建的高層API,比Tensorflow
更容易上手。json
上述提到的分類網絡,在Keras
中基本已經實現,Keras中已經實現的網絡結構以下所示:
使用方便,直接導入便可,以下:
所以,選擇Keras做爲深度學習框架。
以Keras
框架,VGG16
網絡爲例,進行圖像分類。
from keras.models import Model
from keras.applications.vgg16 import VGG16, preprocess_input
import keras.backend.tensorflow_backend as KTF
import tensorflow as tf
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1" #使用GPU
# 按需佔用GPU顯存
gpu_options = tf.GPUOptions(allow_growth=True)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
KTF.set_session(sess)
# 構建model
base_model = VGG16(weights=‘imagenet’, include_top=True)
model = Model(inputs=base_model.input,
outputs=base_model.get_layer(layer).output) # 獲取指定層的輸出值,layer爲層名
# 進行預測
img = load_image(img_name, target_size=(224, 224)) # 加載圖片並resize成224x224
# 圖像預處理
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
feature = model.predict(x) # 提取特徵
複製代碼
將分類模型跑通後,咱們須要測試他們的性能,如耗時、CPU佔用率、內存佔用以及GPU顯存佔用率等。
1. 耗時
耗時是爲了測試圖像進行分類特徵提取時所用的時間,包括圖像預處理時間和模型預測時間的總和。
# 使用python中的time模塊
import time
t0 = time.time()
....
圖像處理和特徵提取
....
print(time.time()-t0) #耗時,以秒爲單位
複製代碼
2. GPU顯存佔用
使用英偉達命令行nvidia-smi
能夠查看顯存佔用。
3. CPU, MEM佔用
使用top
命令或htop
命令查看CPU佔用率以及內存佔用率。
內存佔用還可使用free
命令來查看:
free -h
: 加上-h
選項,輸出結果較爲友好,會給出合適單位
須要持續觀察內存情況時,可使用-s
選項指定間隔的秒數: free -h -s 3
(每隔3秒更新一次,中止更新時按下Ctrl+c
)
Ubuntu 16.04
版本中默認的free
版本有bug,使用-s
選項時會報錯。
根據以上三個測試結果適時調整所採用的網絡結構及顯存佔用選項。
命令具體含義可參考博文:
Redis=Remote DIctionary Server
,是一個由Salvatore Sanfilippo寫的高性能的key-value
存儲系統。Redis是一個開源的使用ANSI C語言編寫、遵照BSD協議、支持網絡、可基於內存亦可持久化的日執行、key-value數據庫,並提供多種語言的API。
Redis
支持存儲的類型有string
, list
, set
, zset
和hash
,在處理大規模數據讀寫的場景下運用比較多。
安裝redis
pip install redis
# 測試
import redis
複製代碼
基本介紹
redis.py
提供了兩個類:Redis
,StrictRedis
用於實現Redis
的命令 StrictRedis
用於實現大部分官方命令,並使用官方的語法和命令 Redis
是StrictRedis
的子類,用於向前兼容redis.py
通常狀況下咱們就是用StrictRedis
。
使用示例
# 1. 導入redis
from redis import StrictRedis
# 2. 鏈接數據庫,指定host,端口號,數據庫
r = StrictRedis(host=‘localhost’, port=6379, db=2)
# 3. 存儲到redis中
r.set('test1', 'value1') # 單個數據存儲
r.set('test2', 'value2')
# 4. 從redis中獲取值
r.get('test1')
# 5. 批量操做
r.mset(k1='v1', k2='v2')
r.mset({'k1':'v1', 'k2':'v2'})
r.mget('k1', 'k2')
r.mget(['k1', 'k2'])
複製代碼
Redis是不能夠直接存儲數組的,若是直接存儲數組類型的數值,則獲取後的數值類型發生變化,以下,存入numpy數組類型,獲取後的類型是bytes
類型。
import numpy as np
from redis import StrictRedis
r = StrictRedis(host=‘localhost’, port=6379, db=2)
x1 = np.array(([0.2,0.1,0.6],[10.2,4.2,0.9]))
r.set('test1', x1)
>>> True
r.get('test1')
>>> b'[[ 0.2 0.1 0.6]\n [10.2 4.2 0.9]]'
type(r.get('test1')) #獲取後的數據類型
>>> <class 'bytes'>
複製代碼
爲了保持數據存儲先後類型一致,在存儲數組以前將其序列化,獲取數組的時候將其反序列化便可。
藉助於python的pickle
模塊進行序列化操做。
import pickle
r.set('test2', pickle.dumps(x1))
>>> True
pickle.loads(r.get('test2'))
>>> array([[ 0.2, 0.1, 0.6],
[10.2, 4.2, 0.9]])
複製代碼
這樣,就能夠保持數據存入前和取出後的類型一致。
以前學習python語言,歷來沒有關注過
Web開發
這一章節,由於工做內容並無涉及這一部分。現在須要從新看一下。
早期軟件主要運行在桌面上,數據庫這樣的軟件運行在服務器端,這種Client/Server
模式簡稱CS
架構。隨着互聯網的興起,CS
架構不適合Web
,最大緣由是Web應用程序的修改和升級很是頻繁,CS架構
須要每一個客戶端逐個升級桌面App,所以,Browser/Server
模式開始流行,簡稱BS架構
。
在BS架構
下,客戶端只須要瀏覽器,應用程序的邏輯和數據存儲在服務器端,瀏覽器只須要請求服務器,獲取Web頁面,並把Web頁面展現給用戶便可。當前,Web頁面也具備極強的交互性。
Python的誕生歷史比Web還要早,因爲Python是一種解釋型的腳本語言,開發效率高,因此很是適合用來作Web開發。
Python有上百個開源的Web框架,比較熟知的有Flask
, Django
。接下來以Flask
爲例,介紹如何利用Flask進行web部署。
關於web開發框架的介紹,能夠參考下面這篇博文: 三個目前最火的Python Web開發框架,你值得擁有!
有關Flask
的具體用法可參考其餘博文,這方面的資料比較全。下面主要以具體使用示例來講明:
安裝Flask
pip install flask
import flask # 導入
flask.__version__ # 版本
>>> '1.1.1' #當前版本
複製代碼
一個簡單的Flask示例
Flask使用Python的裝飾器在內部自動的把URL
和函數給關聯起來。
# hello.py
from flask import Flask, request
app = Flask(__name__) #建立Flask類的實例,第一個參數是模塊或者包的名稱
app.config['JSON_AS_ASCII']=False # 支持中文顯示
@app.route('/', methods=['GET', 'POST']) # 使用methods參數處理不一樣HTTP方法
def home():
return 'Hello, Flask'
if __name__ == '__main__':
app.run()
複製代碼
route()
裝飾器來告訴 Flask 觸發函數的 URL;運行該文件,會提示* Running on http://127.0.0.1:5000/
,在瀏覽器中打開此網址,會自動調用home
函數,返回Hello, Flask
,則在瀏覽器頁面上就會看到Hello, Flask
字樣。
app.run的參數
app.run(host="0.0.0.0", port="5000", debug=True, processes=2, threaded=False)
複製代碼
host
設定爲0.0.0.0
,則可讓服務器被公開訪問port
:指定端口號,默認爲5000
debug
:是否開啓debug模型,若是你打開 調試模式,那麼服務器會在修改應用代碼以後自動重啓,而且當應用出錯時還會提供一個 有用的調試器。processes
:線程數量,默認是1
threaded
:bool
類型,是否開啓多線程。注:當開啓多個進程時,不支持同時開啓多線程。注意:絕對不能在生產環境中使用調試器
視圖函數的返回值會自動轉換爲一個響應對象。若是返回值是一個字符串,那麼會被 轉換爲一個包含做爲響應體的字符串、一個 200 OK
出錯代碼 和一個 text/html
類型的響應對象。若是返回值是一個字典,那麼會調用 jsonify()
來產生一個響應。如下是轉換的規則:
JSON格式的API
JSON
格式的響應是常見的,用Flask寫這樣的 API 是很容易上手的。若是從視圖 返回一個 dict
,那麼它會被轉換爲一個 JSON 響應
。
@app.route("/me")
def me_api():
user = get_current_user()
return {
"username": user.username,
"theme": user.theme,
"image": url_for("user_image", filename=user.image),
}
複製代碼
若是 dict
還不能知足需求,還須要建立其餘類型的 JSON 格式響應,可使用 jsonify()
函數。該函數會序列化任何支持的 JSON
數據類型。
@app.route("/users")
def users_api():
users = get_all_users()
return jsonify([user.to_json() for user in users])
複製代碼
經過命令行使用開發服務器
強烈推薦開發時使用 flask 命令行腳本( 命令行接口 ),由於有強大的重載功能,提供了超好的重載體驗。基本用法以下:
$ export FLASK_APP=my_application
$ export FLASK_ENV=development
$ flask run
複製代碼
這樣作開始了開發環境(包括交互調試器和重載器),並在 http://localhost:5000/
提供服務。
經過使用不一樣 run
參數能夠控制服務器的單獨功能。例如禁用重載器:
$ flask run --no-reload
經過代碼使用開發服務器
另外一種方法是經過 Flask.run()
方法啓動應用,這樣當即運行一個本地服務器,與使用 flask
腳本效果相同。
示例:
if __name__ == '__main__':
app.run()
複製代碼
一般狀況下這樣作不錯,可是對於開發就不行了。
當咱們執行上面的app.py
時,使用的flask
自帶的服務器,完成了web服務的啓動。在生產環境中,flask自帶的服務器,沒法知足性能要求,咱們這裏採用Gunicorn
作wsgi
容器,來部署flask
程序。
Gunicorn
(綠色獨角獸)是一個Python WSGI UNIX HTTP
服務器。從Ruby的獨角獸(Unicorn )項目移植。該Gunicorn
服務器做爲wsgi app
的容器,可以與各類Web框架兼容,實現很是簡單,輕量級的資源消耗。Gunicorn直接用命令啓動,不須要編寫配置文件,相對uWSGI要容易不少。
web開發中,部署方式大體相似。
pip install gunicorn
複製代碼
若是想讓Gunicorn
支持異步workers
的話須要安裝如下三個包:
pip install gevent
pip install eventlet
pip install greenlet
複製代碼
指定進程和端口號,啓動服務器:
gunicorn -w 4 -b 127.0.0.1:5001 運行文件名稱:Flask程序實例名
以上述hello.py文件爲例:
gunicorn -w 4 -b 127.0.0.1:5001 hello:app
參數: -w
: 表示進程(worker)。 -b
:表示綁定ip地址和端口號(bind)
查看gunicorn的具體參數,可執行gunicorn -h
一般將配置參數寫入到配置文件中,如gunicorn_conf.py
重要參數:
bind
: 監聽地址和端口workers
: worker進程的數量。建議值:2~4 x (NUM_CORES)
,缺省值是1.worker_class
:worker進程的工做方式。有:sync
(缺省值),eventlet
, gevent
, gthread
, tornado
threads
:工做進程中線程的數量。建議值:2~4 x (SUM_CORES)
,缺省值是1.reload
: 當代碼有修改時,自動重啓workers。適用於開發環境,默認爲False
daemon
:應用是否以daemon
方式運行,是否以守護進程啓動,默認False
accesslog
:訪問日誌文件路徑errorlog
:錯誤日誌路徑loglevel
: 日誌級別。debug, info, warning, error, critical
.一個參數配置示例:
# gunicorn_conf.py
bind: '0.0.0.0:5000' # 監聽地址和端口號
workers = 2 # 進程數
worker_class = 'sync' #工做模式,可選sync, gevent, eventlet, gthread, tornado等
threads = 1 # 指定每一個進程的線程數,默認爲1
worker_connections = 2000 # 最大客戶併發量
timeout = 30 # 超時時間,默認30s
reload = True # 開發模式,代碼更新時自動重啓
daemon = False # 守護Gunicorn進程,默認False
accesslog = './logs/access.log' # 訪問日誌文件
errorlog = './logs/error.log'
loglevel = 'debug' # 日誌輸出等級,debug, info, warning, error, critical
複製代碼
調用命令:
gunicorn -c gunicorn_conf.py hello:app
參數配置文件示例可見: gunicorn/example_config.py at master · benoitc/gunicorn
#flask_feature.app
import numpy as np
from flask import Flask, jsonify
from keras.models import Model
from keras.applications.vgg16 import VGG16
from keras.backend.tensorflow_backend import set_session
app = Flask(__name__)
app.config['JSON_AS_ASCII']=False
@app.route("/", methods=["GET", "POST"])
def feature():
img_feature = extract()
return jsonify({'result':'true', 'msg':'成功'})
def extract(img_name):
# 圖像預處理
img = load_image(img_name, target_size=(feature_params["size"], feature_params["size"]))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
with graph.as_default():
set_session(sess)
res = model.predict(x)
return res
if __name__ == '__main__':
tf_config = some_custom_config
sess = tf.Session(config=tf_config)
set_session(sess)
base_model = VGG16(weights=model_weights, include_top=True)
model = Model(inputs=base_model.input,
outputs=base_model.get_layer(layer).output)
graph = tf.get_default_graph()
app.run()
複製代碼
使用gunicorn
啓動服務命令:
gunicorn -c gunicorn_conf.py flask_feature:app
在此記錄整個部署工做中遇到的問題及對應解決方法。
因爲對算法的時間性能要求較高,所以嘗試使用Flask自帶的多線程與多進程選項測試效果。 在Flask
的app.run()
函數中,上面有介紹到processes
參數,用於指定開啓的多進程數量,threaded
參數用於指定是否開啓多線程。
flask開啓debug模式,啓動服務時,dubug模式會開啓一個tensorflow的線程,致使調用tensorflow的時候,graph產生了錯位。
使用Flask啓動服務的時候,將遇到的問題及參考的資料記錄在此。
錯誤信息:
"Tensor Tensor(\"pooling/Mean:0\", shape=(?, 1280), dtype=float32) is not an element of this graph.",
複製代碼
描述:使用Keras
中預訓練模型進行圖像分類特徵提取的代碼能夠正常跑通,當經過Flask
來啓動服務,訪問預測函數時,出現上述錯誤。
緣由:使用了動態圖,即在作預測的時候,加載的graph
並非第一次初始化模型時候的Graph
,全部裏面並無模型裏的參數和節點等信息。
有人給出以下解決方案:
import tensorflow as tf
global graph, model
graph = tf.get_default_graph()
#當須要進行預測的時候
with graph.as_default():
y = model.predict(x)
複製代碼
出現該問題的緣由是使用Flask
啓動服務的時候,開啓了debug模式,即debug=True
。dubug
模式會開啓一個tensorflow
的線程,此時查看GPU顯存佔用狀況,會發現有兩個進程都佔用相同份的顯存。
關閉debug模型(debug=False
)便可。
參考資料:
當使用gunicorn啓動服務的時候,遇到如下問題:
具體問題:
2 root error(s) found.\n
(0) Failed precondition: Error while reading resource variable block5_conv2/kernel from Container: localhost. This could mean that the variable was uninitialized. Not found: Container localhost does not exist. (Could not find resource: localhost/block5_conv2/kernel)\n\t [[{{node block5_conv2/convolution/ReadVariableOp}}]]\n\t [[fc2/Relu/_7]]\n
(1) Failed precondition: Error while reading resource variable block5_conv2/kernel from Container: localhost. This could mean that the variable was uninitialized. Not found: Container localhost does not exist. (Could not find resource: localhost/block5_conv2/kernel)\n\t [[{{node block5_conv2/convolution/ReadVariableOp}}]]\n0 successful operations.\n0 derived errors ignored." 複製代碼
解決方法:
經過建立用於加載模型的會話的引用,而後在每一個須要使用的請求中使用keras設置session。具體以下:
from tensorflow.python.keras.backend import set_session
from tensorflow.python.keras.models import load_model
tf_config = some_custom_config
sess = tf.Session(config=tf_config)
graph = tf.get_default_graph()
# IMPORTANT: models have to be loaded AFTER SETTING THE SESSION for keras!
# Otherwise, their weights will be unavailable in the threads after the session there has been set
set_session(sess)
model = load_model(...)
# 在每個request中:
global sess
global graph
with graph.as_default():
set_session(sess)
model.predict(...)
複製代碼
有網友分析緣由: tensorflow
的graph
和session
不是線程安全的,默認每一個線程建立一個新的session
(不包含以前已經加載的weights, models 等)。所以,經過保存包含全部模型的全局會話並將其設置爲在每一個線程中由keras
使用,能夠解決問題。
有網友提取一種改進方式:
# on thread 1
session = tf.Session(graph=tf.Graph())
with session.graph.as_default():
k.backend.set_session(session)
model = k.models.load_model(filepath)
# on thread 2
with session.graph.as_default():
k.backend.set_session(session)
model.predict(x, **kwargs)
複製代碼
這裏的新穎性容許(一次)加載多個模型並在多個線程中使用。默認狀況下,加載模型時使用「默認」Session
和「默認」graph
。可是在這裏是建立新的。還要注意,Graph
存儲在Session
對象中,這樣更加方便。
測試了一下好像不行
當使用gunicorn啓動flask服務時,查看服務器狀態和日誌文件發現一直在嘗試啓動,可是一直沒有成功。
CRITICAL WORKER TIMEOUT
這是gunicorn配置參數timeout
致使的。默認值爲30s
,即超過30s,就會kill掉進程,而後從新啓動restart
。
當啓動服務進行初始化的時間超過timeout值時,就會一直啓動,kill, restart。
可根據具體狀況,適當增長該值。
參考資料:
tensorflow - GCP ML-engine FailedPreconditionError (code: 2) - Stack Overflow