文 | 風玲兒
出處 | 掘金
本文主要記錄在進行
Flask
部署過程中所使用的流程,遇到的問題以及相應的解決方案。
1、項目簡介
該部分簡要介紹一下前一段時間所做的工作:
-
基於深度學習實現一個簡單的圖像分類問題
-
藉助 flask 框架將其部署到 web 應用中
-
並發要求較高
這是第一次進行深度學習模型的 web 應用部署,在整個過程中,進一步折射出以前知識面之窄,在不斷的入坑、解坑中實現一版。
2、項目流程
這部分從項目實施的流程入手,記錄所做的工作及用到的工具。
2.1 圖像分類模型
1. 模型的選擇
需要進行圖像分類,第一反應是利用較為成熟與經典的分類網絡結構,如 VGG 系列(VGG16, VGG19
),ResNet 系列(如ResNet50
),InceptionV3
等。
考慮到是對未知類型的圖像進行分類,且沒有直接可用的訓練數據,因此使用在Imagenet
上訓練好的預訓練模型,基本滿足要求。
如果對性能(耗時)要求較為嚴格,則建議使用深度較淺的網絡結構,如VGG16
,MobileNet
等。
其中,MobileNet
網絡是為移動端和嵌入式端深度學習應用設計的網絡,使得在 cpu 上也能達到理想的速度要求。是一種輕量級的深度網絡結構。
MobileNet
由Google 團隊
提出,發表於CVPR-2017
,論文標題:《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》
2. 框架選擇
平時使用Keras
框架比較多,Keras
底層庫使用Theano
或Tensorflow
,也稱為 Keras 的後端。Keras
是在Tensorflow
基礎上構建的高層
API,比Tensorflow
更容易上手。
上述提到的分類網絡,在Keras
中基本已經實現,Keras 中已經實現的網絡結構如下所示:
使用方便,直接導入即可,如下:
因此,選擇 Keras 作為深度學習框架。
3. 代碼示例
以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) # 提取特徵
2.2 模型性能測試
將分類模型跑通後,我們需要測試他們的性能,如耗時、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
選項時會報錯。
根據以上三個測試結果適時調整所採用的網絡結構及顯存占用選項。
命令具體含義可參考博文:
Linux 查看 CPU 和內存使用情況[1]
2.3 Redis 使用
Redis=Remote DIctionary Server
,是一個由 Salvatore Sanfilippo 寫的高性能的key-value
存儲系統。Redis 是一個開源的使用 ANSI C 語言編寫、遵守 BSD 協議、支持網絡、可基於內存亦可持久化的日執行、key-value 資料庫,並提供多種語言的 API。
Redis
支持存儲的類型有string
,list
,set
,zset
和hash
,在處理大規模數據讀寫的場景下運用比較多。
1. 基本使用
安裝 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'])
2. Redis 存儲數組
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]])
這樣,就可以保持數據存入前和取出後的類型一致。
2.4 web 開發框架——Flask
之前學習 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 開發框架,你值得擁有![2]
有關Flask
的具體用法可參考其他博文,這方面的資料比較全。下面主要以具體使用示例來說明:
1. 安裝使用
-
安裝 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運行該文件,會提示
* 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
類型,是否開啟多線程。註:當開啟多個進程時,不支持同時開啟多線程。 -
使用
route
裝飾器來告訴 Flask 觸發函數的 URL; -
函數名稱被用於生成相關聯的 URL。函數最後返回需要在用戶瀏覽器中顯示的信息。
2. Flask 響應
視圖函數的返回值會自動轉換為一個響應對象。如果返回值是一個字符串,那麼會被 轉換為一個包含作為響應體的字符串、一個 200 OK
出錯代碼 和一個text/html
類型的響應對象。如果返回值是一個字典,那麼會調用jsonify
來產生一個響應。以下是轉換的規則:
-
如果視圖返回的是一個響應對象,那麼就直接返回它。
-
如果返回的是一個字符串,那麼根據這個字符串和預設參數生成一個用於返回的 響應對象。
-
如果返回的是一個字典,那麼調用 jsonify 創建一個響應對象。
-
如果返回的是一個元組,那麼元組中的項目可以提供額外的信息。元組中必須至少 包含一個項目,且項目應當由 (response, status) 、 (response, headers) 或者 (response, status, headers) 組成。status 的值會重載狀態代碼, headers 是一個由額外頭部值組成的列表 或字典。
-
如果以上都不是,那麼 Flask 會假定返回值是一個有效的 WSGI 應用並把它轉換為一個響應對象。
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])
3. 運行開發伺服器
通過命令行使用開發伺服器
強烈推薦開發時使用 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
通常情況下這樣做不錯,但是對於開發就不行了。
2.5 使用 Gunicorn
當我們執行上面的app.py
時,使用的flask
自帶的伺服器,完成了 web 服務的啟動。在生產環境中,flask 自帶的伺服器,無法滿足性能要求,我們這裡採用Gunicorn
做wsgi
容器,來部署flask
程序。
Gunicorn
(綠色獨角獸)是一個Python WSGI UNIX HTTP
伺服器。從 Ruby 的獨角獸(Unicorn )項目移植。該Gunicorn
伺服器作為wsgi app
的容器,能夠與各種 Web
框架兼容,實現非常簡單,輕量級的資源消耗。Gunicorn 直接用命令啟動,不需要編寫配置文件,相對 uWSGI 要容易很多。
web 開發中,部署方式大致類似。
1. 安裝及使用
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[3]
3、代碼示例
#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
4、遇到的問題
在此記錄整個部署工作中遇到的問題及對應解決方法。
4.1 Flask 多線程與多進程問題
由於對算法的時間性能要求較高,因此嘗試使用 Flask 自帶的多線程與多進程選項測試效果。在Flask
的app.run
函數中,上面有介紹到processes
參數,用於指定開啟的多進程數量,threaded
參數用於指定是否開啟多線程。
flask 開啟 debug 模式,啟動服務時,dubug 模式會開啟一個 tensorflow 的線程,導致調用 tensorflow 的時候,graph 產生了錯位。
4.1 Flask 與 Keras 問題
使用 Flask 啟動服務的時候,將遇到的問題及參考的資料記錄在此。
Q1:Tensor is not an element of this graph
錯誤信息:
"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)
Q2:使用 Flask 啟動服務,加載兩次模型,占用兩份顯存
出現該問題的原因是使用Flask
啟動服務的時候,開啟了 debug 模式,即debug=True
。dubug
模式會開啟一個tensorflow
的線程,此時查看 GPU 顯存占用情況,會發現有兩個進程都占用相同份的顯存。
關閉 debug 模型(debug=False
)即可。
參考資料:
[1]:Keras + Flask 提供接口服務的坑~~~[4]
4.2 gunicorn 啟動服務相關問題
當使用 gunicorn 啟動服務的時候,遇到以下問題:
Q1: Failed precondition
具體問題:
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
對象中,這樣更加方便。
測試了一下好像不行
Q2:無法啟動服務,CRITICAL WORKER TIMEOUT
當使用 gunicorn 啟動 flask 服務時,查看伺服器狀態和日誌文件發現一直在嘗試啟動,但是一直沒有成功。
CRITICAL WORKER TIMEOUT
這是 gunicorn 配置參數timeout
導致的。默認值為30s
,即超過 30s,就會 kill 掉進程,然後重新啟動restart
。
當啟動服務進行初始化的時間超過 timeout 值時,就會一直啟動,kill, restart。
可根據具體情況,適當增加該值。
參考資料:
tensorflow - GCP ML-engine FailedPreconditionError (code: 2) - Stack Overflow[5]
參考資料
[1]
Linux查看CPU和內存使用情況: https://www.cnblogs.com/mengchunchen/p/9669704.html
[2]
三個目前最火的Python Web開發框架,你值得擁有!: https://yq.aliyun.com/articles/700673
[3]
gunicorn/example_config.py at master · benoitc/gunicorn: https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py
[4]
1]:[Keras + Flask 提供接口服務的坑~~~: https://www.cnblogs.com/svenwu/p/10189557.html
[5]
tensorflow - GCP ML-engine FailedPreconditionError (code: 2) - Stack Overflow: https://stackoverflow.com/questions/55632876/gcp-ml-engine-failedpreconditionerror-code-2