索引
- 介绍
- 创建一个模型
- 设置Flask服务器
- 创建index.html
- 创建main.js
- 更新Flask服务器
0. 导言
让我们想象一下,你已经创建了一些深奥的模型,它可以做一些伟大的事情,并帮助人们。比如说,这个模型通过杯子的照片来预测人们最喜欢的表情符号。你把这个模型放在一个web上,每天的使用量大概是1000次查询,不是很多。简单的服务器可以处理,但是有一天这个模型被大众发现了,你开始每天收到10万个查询,同样的服务器就会死。所以现在你可以选择扩大服务器规模,增加越来越多的内存,或者你可以尝试在客户端重写预测。如果你选择了第二种方案,这里有一个教程给你。 为了实现这个目标,我们将有以下组件。
后端:Flask,任何你想用python对图像进行预处理的库。
前端: tensorflowjs
最近TensorflowJS增加了对node.js的支持,不过,我们将使用Flask,这是一个python库。通常情况下,有些训练好的模型需要对数据进行预处理才能正常执行。到目前为止,在python中的预处理比在javascript中更方便。(我希望有一天也能在客户端进行预处理)
1. 创建一个模型
你可以通过运行 train_model.py为 MNIST 训练模型,或者你可以创建并训练任何你想要的模型。这里很重要的一点是保存模型的拓扑结构和权重。如果你的模型是用keras写的,只需添加以下内容。
# Your training happens here# Hundreds and hundreds of layers# Stacked to create the greatest model ever# don't forget saving to json formatimport tensorflowjs as tfjstfjs.converters.save_keras_model(model, "model_js")
模型保存后,你会有一个文件夹,里面有以下内容
其中 group*-shard*of*--是二进制权重文件的集合和model.json--是模型的拓扑结构和配置。
2.设置Flask服务器
因为我们希望用户能够访问我们的模型。
from flask_cors import CORSfrom flask import Flask, render_templateapp = Flask(__name__)CORS(app)@app.route("/", methods=["GET"])def main(): return render_template('index.html') if __name__ == "__main__": app.run()
到目前为止,服务器很简单,没有什么复杂的东西,只有一条路径,返回静态页面index.html。
另外,系统的总体架构是这样的。
3. 创建index.html
我们需要一些用户交互的入口点,并在那里运行我们的预测。因此,让我们来设置index.html.
TF JS example
我们的第一个版本会是这样的。这里唯一重要的事情是在第6行上通过 CDN添加了tensorflowjs。
下一步就是添加一些HTML主体,这样用户就可以上传图片和点击按钮了。:) 下面是这样的。
TensorflowJS client side prediction
When you first time press predict it will take more time, for model to load
Choose files Predict Clear
接下来,也是最后一步,也就是我们的HTML部分的,就是给我们的页面添加一些样式,从而给HTML元素带来类,同时创建main.js文件,这个文件将包含我们的神奇预测部分的。现在让我们来看看index.html.的最终版本吧
TF JS example
TensorflowJS client side prediction
When you first time press predict it will take more time, for model to load
Choose files Predict Clear
你的index.html可能和我的不一样,你也可以添加或删除其中的一些部分,不过,这里重要的是。
- 第6行 (在头部添加了CDN的脚本--对于生产版的代码来说,这是一个不好的做法,不过,我不想麻烦大家解释什么是npm和node_modules)
- 第13行(输入,如果你想让用户上传图片,也可以使用不同类型的输入,也可以使用不同类型的输入)
- 第20行(我们的脚本做客户端的预测)
4. 创建main.js
现在是时候带来魔术了。首先我们需要初始化按钮、输入、模型和预测功能。
let model;const modelURL = 'http://localhost:5000/model';const preview = document.getElementById("preview");const predictButton = document.getElementById("predict");const clearButton = document.getElementById("clear");const numberOfFiles = document.getElementById("number-of-files");const fileInput = document.getElementById('file');const predict = async (modelURL) => {}
在第3行我们有了我们的模型的URL,到目前为止,它是在我的本地机器上,但一般来说,它可以部署在任何地方。另外,稍后我们将在Flask中为这个模型创建一个路由。 现在,让我们添加一个部分来下载我们的模型,然后将用户上传的图片发送到服务器上进行预处理。
let model;const modelURL = 'http://localhost:5000/model';const preview = document.getElementById("preview");const predictButton = document.getElementById("predict");const clearButton = document.getElementById("clear");const numberOfFiles = document.getElementById("number-of-files");const fileInput = document.getElementById('file');const predict = async (modelURL) => { if (!model) model = await tf.loadModel(modelURL); const files = fileInput.files; [...files].map(async (img) => { const data = new FormData(); data.append('file', img); const processedImage = await fetch("/api/prepare", { method: 'POST', body: data }).then(response => { return response.json(); }).then(result => { return tf.tensor2d(result['image']); }); })};
我们把图像发送到/api/prepare/,这个路由我们以后再添加。同时我们把服务器的响应从字段image到tf.tensor2d。 现在是时候为我们的张量添加预测,然后渲染预测和图像到用户的视图中。最后就是添加一些按钮例程和函数调用。 因此,最后一个版本的神奇脚本与客户端预测的神奇脚本会是这样的。
let model;const modelURL = 'http://localhost:5000/model';const preview = document.getElementById("preview");const predictButton = document.getElementById("predict");const clearButton = document.getElementById("clear");const numberOfFiles = document.getElementById("number-of-files");const fileInput = document.getElementById('file');const predict = async (modelURL) => { if (!model) model = await tf.loadModel(modelURL); const files = fileInput.files; [...files].map(async (img) => { const data = new FormData(); data.append('file', img); const processedImage = await fetch("/api/prepare", { method: 'POST', body: data }).then(response => { return response.json(); }).then(result => { return tf.tensor2d(result['image']); }); // shape has to be the same as it was for training of the model const prediction = model.predict(tf.reshape(processedImage, shape = [1, 28, 28, 1])); const label = prediction.argMax(axis = 1).get([0]); renderImageLabel(img, label); })};const renderImageLabel = (img, label) => { const reader = new FileReader(); reader.onload = () => { preview.innerHTML += `
${label}
`; }; reader.readAsDataURL(img);};fileInput.addEventListener("change", () => numberOfFiles.innerHTML = "Selected " + fileInput.files.length + " files", false);predictButton.addEventListener("click", () => predict(modelURL));clearButton.addEventListener("click", () => preview.innerHTML = "");
请随意重写fetch部分,以便在一次POST中发送完整的批文件。 不要忘了根据训练模型的形状来重塑返回的数组!
5. 更新Flask服务器
现在我们必须更新我们的服务器,这样它就可以为/api/prepare的图像做的预处理,同时也可以为/model的模型保存到前端。服务器的最终版本看起来类似于此。
from flask_cors import CORSfrom flask import Flask, request, render_template, json, jsonify, send_from_directoryimport jsonimport cv2import numpy as npimport ioapp = Flask(__name__)CORS(app)@app.route("/", methods=["GET"])def main(): return render_template('index.html')@app.route("/api/prepare", methods=["POST"])def prepare(): file = request.files['file'] res = preprocessing(file) return json.dumps({"image": res.tolist()})@app.route('/model')def model(): json_data = json.load(open("./model_js/model.json")) return jsonify(json_data)@app.route('/')def load_shards(path): return send_from_directory('model_js', path)def preprocessing(file): in_memory_file = io.BytesIO() file.save(in_memory_file) data = np.fromstring(in_memory_file.getvalue(), dtype=np.uint8) img = cv2.imdecode(data, 0) res = cv2.resize(img, dsize=(28, 28), interpolation=cv2.INTER_CUBIC) # file.save("static/UPLOAD/img.png") # saving uploaded img # cv2.imwrite("static/UPLOAD/test.png", res) # saving processed image return resif __name__ == "__main__": app.run()
对于的预处理部分,我们有两个函数:
- prepare(为/api/prepare调用)
- preprocessing(取一张图片,返回调整后的图片到numpy数组,这个函数可以做任何你喜欢的预处理,只要返回numpy数组,一切都会正常工作。
模型部分:
- model(调用/model)
- load_shards(被调用的任何文件都是为/,这个函数是用来加载二进制权重文件的) 为什么我们需要2个函数和2个独立的API来服务一个模型,而不是一个?
在下面的tensorflowjs版本中,当我们为某个API加载模型时,model = await tf.loadModel(modelURL);
它首先从modelURL中加载模型,这是一个JSON文件,然后它会自动发送一些POSTs到domain根目录,以便加载shards(在演示中查看服务器日志中的POSTs)。因为我不想把模型和服务器一起放在根目录下,所以我需要两个函数,一个是model.json,另一个是shards。
就是这样!现在你就可以把预测模型委托给我了。现在,你可以把预测人们最喜欢的表情符号通过杯子的照片委托给客户端,或者像我的案例中的MNIST一样,只需要在客户端进行预测。另外,如果一切正常,你会看到类似于这样的东西。