構建並用 TensorFlow Serving 部署 Wide & Deep 模型
Wide & Deep 模型是谷歌在 2016 年發表的論文中所提到的模型。在論文中,谷歌將 LR 模型與 深度神經網路 結合在一起作為 Google Play 的推薦獲得了一定的效果。在這篇論文後,Youtube,美團等公司也進行了相應的嘗試並公開了他們的工作(相關連結請看本文底部)
官方提供的 Wide & Deep 模型的(簡稱,WD 模型)教程 都是使用 TensorFlow (簡稱,TF )自帶的函式來做的特徵工程,並且模型也進行了封裝,但有時候我們的特徵工程還使用到了 sklearn, numpy, pandas 來做,當我們想快速驗證 WD 模型是否比舊模型要好的時候則顯得不太便利,所以本文就向您展示瞭如何自己用 TF 搭建一個結構清晰,定製性更高的 WD 模型。在訓練好 WD 模型後,我們還需要快速的看到模型預測的效果,所以在本文中我們利用 Docker 來快速部署一個可供服務的 TensorFlow 模型,也即可提供服務的 API。
因此,本文的內容如下:
- 使用 TF 搭建 WD 網路結構
- 使用 Docker 來快速部署模型
本文實現的 WD 模型的結構如下圖所示:
本文構建的網路
不難看出,Wide 模型這邊其實就是一個 LR 模型,而右邊 Deep 模型的部分則是一個三層隱藏層的神經網路,這三層隱藏層的神經元數目分別是 256-12-64,最後 Wide 模型 和 Deep 模型的結果進行相加後通過 ReLu 啟用函式後輸出預測結果。OK,先來看一下 Wide 模型部分的程式碼
def wide_model(input_data): """ 一層的神經網路,相當於是 LR :param input_data: :return: """ input_len = int(input_data.shape[1]) with tf.name_scope("wide"): # 修正的方式初始化權重,輸出層結點只有一個 weights = tf.Variable(tf.truncated_normal([input_len, 1], stddev=1.0 / math.sqrt(float(input_len)) ), name="weights" ) output = tf.matmul(input_data, weights) # 沿著行這個緯度來求和 output = tf.reduce_sum(output, 1, name="reduce_sum") # 輸出每個樣本經過計算的值 output = tf.reshape(output, [-1, 1]) return output
接下來看一下 Deep 模型的程式碼。
def deep_model(input_data, hidden1_units, hidden2_units, hidden3_units): """ 三層的神經網路 :param input_data: 2-D tensor :param hidden1_units: int :param hidden2_units: int :param hidden3_units: int :return: """ # 得到每個樣本的維度 input_len = int(input_data.shape[1]) with tf.name_scope("hidden1"): # 修正的方式初始化權重 weights = tf.Variable(tf.truncated_normal([input_len, hidden1_units], stddev=1.0 / math.sqrt(float(input_len)) ), name="weights1" ) biases = tf.Variable(tf.zeros([hidden1_units]), name='biases1') hidden1 = tf.nn.relu(tf.matmul(input_data, weights)) + biases ··· # 另外兩層隱藏層程式碼相似,所以這裡省略掉,具體的程式碼請看 Github 倉庫 ··· with tf.name_scope("output"): # 修正的方式初始化權重 weights = tf.Variable(tf.truncated_normal([hidden3_units, 1], stddev=1.0 / math.sqrt(float(input_len)) ), name="weights4" ) biases = tf.Variable(tf.zeros([1]), name='biases4') output = tf.nn.relu(tf.matmul(hidden3, weights) + biases) return tf.nn.relu(output)
雖然 Deep 模型的程式碼存在一定的冗餘,但是這樣方便我們修改和調整網路的結構。
最後,將 Wide 模型 和 Deep 模型的結果進行相加後通過 ReLu 啟用函式輸出預測的結果。
def build_wdl(deep_input, wide_input, y):
"""
得到模型和損失函式
:param deep_input:
:param wide_input:
:param y:
:return:
"""
central_bias = tf.Variable([np.random.randn()], name="central_bias")
dmodel = deep_model(deep_input, 256, 128, 64)
wmodel = wide_model(wide_input)
# 使用 LR 將兩個模型組合在一起
dmodel_weight = tf.Variable(tf.truncated_normal([1, 1]), name="dmodel_weight")
wmodel_weight = tf.Variable(tf.truncated_normal([1, 1]), name="wmodel_weight")
network = tf.add(
tf.matmul(dmodel, dmodel_weight),
tf.matmul(wmodel, wmodel_weight)
)
prediction = tf.nn.sigmoid(tf.add(network, central_bias), name="prediction")
loss = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=prediction)
)
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
return train_step, loss, prediction
搭建好結構後,我們可以生成一些隨機資料來測試 Wide & Deep 模型,在這裡我們隨機生成 1000 個樣本,每個樣本的維度為 10,作為訓練樣本,為了簡單起見,沒有再建立驗證樣本。 訓練只迭代一次,也即只遍歷一次訓練樣本,這裡的每個樣本的 label 取值都為 0 或 1,所以目標函式為交叉熵,程式碼如下:
def build_and_saved_wdl():
"""
訓練並儲存模型
:return:
"""
# 訓練資料
x_deep_data = np.random.rand(10000)
x_deep_data = x_deep_data.reshape(-1, 10)
x_wide_data = np.random.rand(10000)
x_wide_data = x_wide_data.reshape(-1, 10)
x_deep = tf.placeholder(tf.float32, [None, 10])
x_wide = tf.placeholder(tf.float32, [None, 10])
y = tf.placeholder(tf.float32, [None, 1])
y_data = np.array(
[random.randint(0, 1) for i in range(1000)]
)
y_data = y_data.reshape(-1, 1)
# 為了簡單起見,這裡沒有驗證集,也就沒有驗證集的 loss
train_step, loss, prediction = build_wdl(x_deep, x_wide, y)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
sess.run(train_step, feed_dict={x_deep: x_deep_data, x_wide: x_wide_data, y: y_data})
訓練完成後,還需要將模型進行儲存,若要在 TensorFlow Serving 中使用,則需要用 SavedModelBuilder
來儲存模型,程式碼如下:
def build_and_saved_wdl():
···
# 將訓練好的模型儲存在當前的資料夾下
builder = tf.saved_model.builder.SavedModelBuilder(join("./model_name", MODEL_VERSION))
inputs = {
"x_wide": tf.saved_model.utils.build_tensor_info(x_wide),
"x_deep": tf.saved_model.utils.build_tensor_info(x_deep)
}
output = {"output": tf.saved_model.utils.build_tensor_info(prediction)}
prediction_signature = tf.saved_model.signature_def_utils.build_signature_def(
inputs=inputs,
outputs=output,
method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME
)
builder.add_meta_graph_and_variables(
sess,
[tf.saved_model.tag_constants.SERVING],
{tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: prediction_signature}
)
builder.save()
這裡需要注意的是 MODEL_VERSION 必須為數字(代表著模型的版本),TF Serving 預設只會載入數字最大的那個模型,譬如說在這裡我們執行完程式碼後,儲存模型的 model_name 的資料夾的目錄如下:
λ root [tf_demo/servering/model_name] → tree
.
└── 1
├── saved_model.pb
└── variables
├── variables.data-00000-of-00001
└── variables.index
2 directories, 3 files
儲存完模型後,在這裡我們使用容器來部署模型,當然你也可以選擇自己在機器上配置相關的環境,我們使用的映象是由 Bitnami 提供的(Dockerhub 的地址請戳這裡),當你需要部署模型時,只需要將模型所在的路徑對映到容器中的 /bitnami/model-data
路徑下即可,也即是鍵入如下命令
λ edvard [tf_demo/servering/model_name] → docker run -it -p 5000:9000 --volume /root/tf_demo/servering/model_name:/bitnami/model-data bitnami/tensorflow-serving
Welcome to the Bitnami tensorflow-serving container
...
2017-11-01 03:43:55.983106: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: inception version: 1}
2017-11-01 03:43:55.986130: I tensorflow_serving/model_servers/main.cc:288] Running ModelServer at 0.0.0.0:9000 ...
這裡可能需要一些 Docker 相關的知識,我在參考資料中提供了一份很不錯的 Gitbook 入門書籍,感興趣的可以看看。
我們將容器中的服務對映到了宿主機的 5000 埠,接下來我們來測試一下 API 介面。程式碼如下:
def test_servable_api():
"""
測試 API
:return:
"""
# 隨機產生 10 條測試資料
x_deep_data = np.random.rand(100).reshape(-1, 10)
x_wide_data = np.random.rand(100).reshape(-1, 10)
channel = implementations.insecure_channel('127.0.0.1', int(5000))
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
# 傳送請求
request = predict_pb2.PredictRequest()
request.model_spec.name = 'inception'
request.model_spec.signature_name = tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY
request.inputs[INPUT_WIDE_KEY].CopyFrom(
tf.contrib.util.make_tensor_proto(x_wide_data, shape=[10, WIDE_DIM], dtype=tf.float32))
request.inputs[INPUT_DEEP_KEY].CopyFrom(
tf.contrib.util.make_tensor_proto(x_deep_data, shape=[10, DEEP_DIM], dtype=tf.float32))
# 10 秒超時
res = stub.Predict(request, 10.0)
pprint(res.outputs[OUTPUT_KEY])
輸出的預測結果的結構如下:
dtype: DT_FLOAT
tensor_shape {
dim {
size: 10
}
dim {
size: 1
}
}
float_val: 0.355874538422
float_val: 0.3225004673
float_val: 0.32104665041
float_val: 0.233089879155
float_val: 0.376621931791
float_val: 0.144557282329
float_val: 0.34686845541
float_val: 0.304817527533
float_val: 0.367866277695
float_val: 0.393035560846
參考資料
作者:曾梓華 連結:https://www.jianshu.com/p/2fffd0e332bc 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。