基於Keras和Gunicorn+Flask部署深度學習模型

風玲兒發表於2019-10-08

本文主要記錄在進行Flask部署過程中所使用的流程,遇到的問題以及相應的解決方案。

1、專案簡介

該部分簡要介紹一下前一段時間所做的工作:

  • 基於深度學習實現一個簡單的影像分類問題
  • 藉助flask框架將其部署到web應用中
  • 併發要求較高

這是第一次進行深度學習模型的web應用部署,在整個過程中,進一步折射出以前知識面之窄,在不斷的入坑、解坑中實現一版。

2、專案流程

這部分從專案實施的流程入手,記錄所做的工作及用到的工具。

2.1 影像分類模型

1. 模型的選擇

需要進行影像分類,第一反應是利用較為成熟與經典的分類網路結構,如VGG系列(VGG16, VGG19),ResNet系列(如ResNet50),InceptionV3等。

考慮到是對未知型別的影像進行分類,且沒有直接可用的訓練資料,因此使用在Imagenet上訓練好的預訓練模型,基本滿足要求。

如果對效能(耗時)要求較為嚴格,則建議使用深度較淺的網路結構,如VGG16, MobileNet等。

其中,MobileNet網路是為移動端和嵌入式端深度學習應用設計的網路,使得在cpu上也能達到理想的速度要求。是一種輕量級的深度網路結構。

MobileNetGoogle 團隊提出,發表於 CVPR-2017,論文標題: 《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》

2. 框架選擇

  • 平時使用Keras框架比較多,Keras底層庫使用TheanoTensorflow,也稱為Keras的後端。Keras是在Tensorflow基礎上構建的高層API,比Tensorflow更容易上手。

  • 上述提到的分類網路,在Keras中基本已經實現,Keras中已經實現的網路結構如下所示:

    基於Keras和Gunicorn+Flask部署深度學習模型

  • 使用方便,直接匯入即可,如下:

    基於Keras和Gunicorn+Flask部署深度學習模型

因此,選擇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選項時會報錯。

基於Keras和Gunicorn+Flask部署深度學習模型

根據以上三個測試結果適時調整所採用的網路結構及視訊記憶體佔用選項。

命令具體含義可參考博文:

Linux檢視CPU和記憶體使用情況

2.3 Redis使用

Redis=Remote DIctionary Server,是一個由Salvatore Sanfilippo寫的高效能的key-value儲存系統。Redis是一個開源的使用ANSI C語言編寫、遵守BSD協議、支援網路、可基於記憶體亦可持久化的日執行、key-value資料庫,並提供多種語言的API。

Redis支援儲存的型別有string, list, set, zsethash,在處理大規模資料讀寫的場景下運用比較多。

1. 基本使用

安裝redis

pip install redis

# 測試
import redis
複製程式碼

基本介紹

redis.py提供了兩個類:RedisStrictRedis用於實現Redis的命令 StrictRedis用於實現大部分官方命令,並使用官方的語法和命令 RedisStrictRedis的子類,用於向前相容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開發框架,你值得擁有!

有關Flask的具體用法可參考其他博文,這方面的資料比較全。下面主要以具體使用示例來說明:

1. 安裝使用

  1. 安裝Flask

    pip install flask
    
    import flask # 匯入
    flask.__version__ # 版本
    >>> '1.1.1' #當前版本
    複製程式碼
  2. 一個簡單的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;
    • 函式名稱被用於生成相關聯的 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
    • threadedbool型別,是否開啟多執行緒。注:當開啟多個程式時,不支援同時開啟多執行緒。

    注意:絕對不能在生產環境中使用偵錯程式

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. 執行開發伺服器

  1. 通過命令列使用開發伺服器

    強烈推薦開發時使用 flask 命令列指令碼( 命令列介面 ),因為有強大的過載功能,提供了超好的過載體驗。基本用法如下:

    $ export FLASK_APP=my_application
    $ export FLASK_ENV=development
    $ flask run
    複製程式碼

    這樣做開始了開發環境(包括互動偵錯程式和過載器),並在 http://localhost:5000/提供服務。

    通過使用不同 run 引數可以控制伺服器的單獨功能。例如禁用過載器:

    $ flask run --no-reload

  2. 通過程式碼使用開發伺服器

    另一種方法是通過 Flask.run() 方法啟動應用,這樣立即執行一個本地伺服器,與使用 flask 指令碼效果相同。

    示例:

    if __name__ == '__main__':
        app.run()
    複製程式碼

    通常情況下這樣做不錯,但是對於開發就不行了。

    基於Keras和Gunicorn+Flask部署深度學習模型

2.5 使用Gunicorn

當我們執行上面的app.py時,使用的flask自帶的伺服器,完成了web服務的啟動。在生產環境中,flask自帶的伺服器,無法滿足效能要求,我們這裡採用Gunicornwsgi容器,來部署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、程式碼示例

#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自帶的多執行緒與多程式選項測試效果。 在Flaskapp.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=Truedubug模式會開啟一個tensorflow的執行緒,此時檢視GPU視訊記憶體佔用情況,會發現有兩個程式都佔用相同份的視訊記憶體。

關閉debug模型(debug=False)即可。

參考資料:

[1]:Keras + Flask 提供介面服務的坑~~~

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(...)
複製程式碼

有網友分析原因: tensorflowgraphsession不是執行緒安全的,預設每個執行緒建立一個新的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、參考資料

歡迎來到 Flask 的世界 — Flask 中文文件( 1.1.1 )

Gunicorn-配置詳解

At Runtime : "Error while reading resource variable softmax/kernel from Container: localhost" · Issue #28287 · tensorflow/tensorflow

【已解決】線上環境通過gunicorn去執行Flask出錯:CRITICAL WORKER TIMEOUT

相關文章