作者 | Brian Schmidt
来源 | Medium
编辑 | 代码医生团队
此博客文章中的代码可以在此github仓库中找到。
https://github.com/schmidtbri/using-ml-model-abc?source=post_page---------------------------
介绍
这篇博文目的是构建一个使用MLModel基类来部署模型的简单应用程序。
在创建软件时,通过抽象与组件交互使代码更易于理解和发展。在软件设计模式的词汇表中,这称为策略模式。使用策略模式时,软件组件的实现细节(“策略”)不是预先决定的,它们会被推迟到以后。相反,设计使用组件的代码与组件本身之间的接口并将其放入代码中。当编写使用该组件的代码时,它是针对抽象接口编写的,相信组件将提供与商定的接口匹配的实现。之后,可以根据需要实施策略的实施。这种方法可以轻松地在策略的实现之间轻松切换。它还可以决定在运行时使用哪种策略实现,从而使软件更加灵活。
通过MLModel抽象与机器学习模型交互,可以构建可以托管任何实现MLModel接口的模型的应用程序。这样简单的模型部署变得更快,因为不需要定制的应用程序来将模型投入生产。
将在本文展示的应用程序利用这一事实,允许软件工程师在Web应用程序中安装和部署任意数量的实现MLModel基类的模型。
总的来说,目的是展示如何将iris_model包中的模型代码部署到一个简单的Web应用程序中。还想展示MLModel抽象如何在生产软件中更容易地使用机器学习模型。
Flask Web应用程序
使用python构建Web应用程序的最简单方法之一是使用Flask框架。Flask可以轻松设置一个提供Web页面和RESTful界面的简单Web应用程序。
首先,为应用程序包设置了项目结构:
代码语言:javascript复制- model_service
- static ( folder containing the static web assets )
- templates ( folder for the html templates
- __init__.py
- config.py
- endpoints.py
- model_manager.py
- schemas.py
- views.py
- scripts ( folder containing scripts )
- tests ( folder containing the unit test suite)
- requirements.txt
- test_requirements.txt
使用此代码在__init__.py文件中设置Flask应用程序:
代码语言:javascript复制app = Flask(__name__)
if os.environ.get(“APP_SETTINGS”) is not None:
app.config.from_object(os.environ[‘APP_SETTINGS’])
bootstrap = Bootstrap(app)
Flask应用程序是通过实例化Flask()类来启动的。配置由config.py文件中的配置类导入,每个环境有一个配置类。环境名称正在作为“APP_SETTINGS”环境变量导入,这使得在运行时更改应用程序的配置变得容易。这个模式在Flask的应用管理和导入配置细节的更多信息。最后,我使用flask_bootstrap包将bootstrap元素添加到网页,此包在加载配置后启动。
到目前为止,这是一个简单的Flask应用程序无法管理或提供机器学习模型,在下一节中将开始添加执行此操作所需的功能。
Model Manager Class
为了正在构建的Flask应用程序中使用iris_model类,需要有一种方法来管理Python进程中的模型对象。为此将创建一个遵循单例模式的ModelManager类。ModelManager类将在应用程序启动时实例化一次。ModelManager单例从配置中实例化MLModel类,并返回有关正在管理的模型对象的信息以及对模型对象的引用。
这是类声明:
代码语言:javascript复制class ModelManager(object):
_models = []
ModelManager类有一个名为_models的私有列表属性,它将包含对管理中的模型对象的引用。这个类不是真正的单例,因为每次实例化类时都会创建一个新对象。但是,same_models列表将始终可用于该类的所有实例。选择以这种方式实现单例模式以保持代码简单。
现在需要一种实际实例化模型类的方法,执行此操作的代码如下:
代码语言:javascript复制@classmethod
def load_models(cls, configuration):
for c in configuration:
model_module = importlib.import_module(c[“module_name”])
model_class = getattr(model_module, c[“class_name”])
model_object = model_class()
if isinstance(model_object, MLModel) is False:
raise ValueError(“The ModelManager can only hold references to objects of type MLModel.”)
cls._models.append(model_object)
load_models()类方法接收配置字典对象并迭代它,从环境导入类,实例化类,并保存对_models类属性中对象的引用。该方法还检查正在导入和实例化的类是MLModel基类的实例。ModelManager单例对象能够容纳任意数量的模型对象。
ModelManager类还提供了另外三种有助于使用它管理的模型的方法。所述get_models()方法返回与有关模型对象信息的字典的列表。所述get_model_metadata()方法返回关于单个模型对象的详细数据,与模型对象的qualified_name属性标识。此方法返回的元数据包含编码为JSON模式字典的模型的输入和输出模式。最后,get_model()方法搜索_models列表中的模型,并返回对一个模型对象的引用。在_models类属性中搜索模型对象列表时,模型的限定名称用于标识模型。
使用ModelManager类,现在可以使用iris_model包测试它。为此需要通过执行以下命令从github安装包:
代码语言:javascript复制pip install git https://github.com/schmidtbri/ml-model-abc-improvements
一旦在环境中安装了iris_model包,就可以使用python交互会话来执行此代码来试用ModelManager类:
代码语言:javascript复制>>> from model_service.model_manager import ModelManager
>>> model_manager = ModelManager()
>>> model_manager.load_models(configuration=[{“module_name”: “iris_model.iris_predict”,”class_name”: “IrisModel”}])
>>> model_manager.get_models()
[{‘display_name’: ‘Iris Model’, ‘qualified_name’: ‘iris_model’, ‘description’: ‘A machine learning model for predicting the species of a flower based on its measurements.’, ‘major_version’: 0, ‘minor_version’: 1}]
ModelManager类用于加载iris_predict模块中iris_model包中的IrisModel类,查找类所需的信息保存在配置中。实例化模型对象后,将调用get_models()方法以获取有关内存中模型的数据。
为了在Flask应用程序中使用ModelManager类,必须实例化它并调用load_model()。由于模型类在实例化时会从磁盘加载它们的参数,因此在应用程序启动时只执行一次这一操作非常重要。可以通过将此代码添加到__init__.py模块来实现:
代码语言:javascript复制@app.before_first_request
def instantiate_model_manager():
model_manager = ModelManager()
model_manager.load_models(configuration=app.config[“MODELS”])
该函数上的@ app.before_first_request装饰器使其在应用程序处理请求之前执行。模型管理器配置从此处的Flask应用程序配置加载。
ModelManager类处理在内存中实例化和管理模型对象的复杂性。只要在python环境中可以找到MLModel派生类,它就可以由ModelManager类加载和管理。
Flask REST端点
为了利用ModelManager对象中托管的模型,将首先构建一个简单的REST接口,允许客户端查找和进行预测。要定义REST接口返回的数据模型,使用marshmallow架构包。虽然使用它来构建Web应用程序并不是绝对必要的,但是marshmallow软件包提供了一种简单快捷的方法来构建模式并进行序列化和反序列化。
Flask应用程序有三个端点:用于获取应用程序托管的所有模型的信息的模型端点,用于获取特定模型的信息的元数据端点,以及用于使用特定模型进行预测的预测端点。
通过向Flask应用程序注册函数来创建模型端点:
代码语言:javascript复制@app.route(“/api/models”, methods=[‘GET’])
def get_models():
model_manager = ModelManager()
models = model_manager.get_models()
response_data = model_collection_schema.dumps(dict(models=models)).data
return response_data, 200
该函数使用ModelManager类访问有关其中托管的所有模型的数据。它使用get_models()方法,其方式与上面视图定义的索引相同。response_data使用marshmallow模式对象进行序列化,该对象是从此处定义的模式类实例化的。
元数据端点的构建与模型端点类似。该元数据终结函数使用的ModelManager类访问有关模型的信息。与模型端点相同,元数据端点还定义了一组用于序列化的模式类。
该预测终点,因为它并没有定义为是预计输入和输出数据的模式类从以前的终端不同的功能。如果客户想要知道需要将哪些字段发送到模型进行预测,它可以找到元数据端点发布的JSON模式中的字段的描述。如果Flask应用程序中安装了具有新输入或输出模式的新版本模型,则Flask应用程序的代码根本不需要更改以适应新模型。
如果Flask应用程序中安装了具有新输入或输出模式的新版本模型,则Flask应用程序的代码根本不需要更改以适应新模型。
Flask视图
Flask框架还能够使用Jinja模板呈现网页,这里可以找到了解这一点的好指南。要将使用Jinja模板呈现的网页添加到Web应用程序,将templates文件夹添加到应用程序包中。在其中我创建了基本html模板,其他模板从该模板继承。基本模板使用引导程序包中的样式。为了将模板渲染到视图中,还添加了views.py模块。
https://code.tutsplus.com/tutorials/templating-with-jinja2-in-flask-essentials--cms-25571?source=post_page---------------------------
为了显示有关ModelManager对象中模型的一些信息,添加了index.html模板。为了呈现模板,将此代码添加到views.py模块:
代码语言:javascript复制@app.route(“/api/models”, methods=[‘GET’])
def get_models():
model_manager = ModelManager()
models = model_manager.get_models()
response_data = model_collection_schema.dumps(dict(models=models)).data
return response_data, 200
索引视图函数首先将自己注册到Flask应用程序的根URL,以便它成为主页。然后实例化ModelManager,但由于它是在应用程序启动时首次实例化的单例,因此返回对单例对象的引用,并且已加载所有模型对象。接下来,使用singleton的get_models()方法获取可用模型列表。最后将返回的模型列表发送到模板进行渲染,并将生成的网页返回给用户。此视图还呈现指向模型的元数据和预测视图的链接。这些观点如下。索引网页如下所示:
Web应用程序的索引页面
元数据视图遵循类似的方法,该方法显示单个模型的元数据以及输入和输出模式。此视图与索引视图之间的一个区别是它接受一个路径参数,该参数确定在视图中呈现哪个模型的元数据。元数据网页如下所示:
Web应用程序的元数据页面
动态Web表单
应用程序的最后一个网页使用视图来呈现网页和预测端点。模型的预测网页从模型提供的输入json模式呈现动态表单,然后接受用户输入并在用户按下“预测”按钮时将其发送到预测REST端点,最后它显示来自的预测结果该模型。
预测网页的呈现方式与其他视图类似:
代码语言:javascript复制@app.route(“/models/<qualified_name>/predict”, methods=[‘GET’])
def display_form(qualified_name):
model_manager = ModelManager()
model_metadata = model_manager.get_model_metadata(qualified_name=qualified_name)
return render_template(‘predict.html’, model_metadata=model_metadata)
然而,模板是不同的,因为它使用JQuery从元数据端点获取模型的输入模式:
代码语言:javascript复制$(document).ready(function() {
$.ajax({
url: ‘/api/models/{{ model_metadata.qualified_name }}/metadata’,
如果请求成功返回,那么使用brutusin forms包从模型的输入JSON模式中呈现表单。从JSON模式创建的webform是动态的,它允许为应用程序托管的任何模型创建自定义表单。下面是呈现表单的代码:
代码语言:javascript复制success: function(data) {
var container = document.getElementById(‘prediction_form’);
var BrutusinForms = brutusin[“json-forms”];
bf = BrutusinForms.create(data.input_schema);
bf.render(container);
}
最后,当用户按下“预测”按钮时,会有一个JQuery请求进行预测,以及一个将预测呈现给网页的回调函数。
以下是预测网页的屏幕截图:
Web应用程序的预测页面
文档
为了使REST API更易于使用,将为其生成文档。记录RESTful接口的常用方法是OpenAPI规范。为了自动为模型服务提供的RESTful API创建OpenAPI文档,使用了python apispec包。apispec包能够从marshmallow Schema类中自动提取模式信息,并能够从Flask @ app.route修饰函数中提取端点规范。
为了能够从代码中自动提取OpenAPI规范文档,创建了一个名为openapi.py的python脚本。该脚本创建一个描述文档的对象:
代码语言:javascript复制spec = APISpec(
openapi_version=”3.0.2",
title=’Model Service’,
version=’0.1.0',
info=dict(description=__doc__),
plugins=[FlaskPlugin(), MarshmallowPlugin()],
)
然后可以添加从schemas.py模块导入的marshmallow模式类:
代码语言:javascript复制spec.components.schema(“ModelSchema”, schema=ModelSchema)
spec.components.schema(“ModelCollectionSchema”, schema=ModelCollectionSchema)
spec.components.schema(“JsonSchemaProperty”, schema=JsonSchemaProperty)
spec.components.schema(“JSONSchema”, schema=JSONSchema)
spec.components.schema(“ModelMetadataSchema”, schema=ModelMetadataSchema)
spec.components.schema(“ErrorSchema”, schema=ErrorSchema)
要记录API的路径,必须将OpenAPI规范添加到向Flask应用程序注册的控制器函数的docstring中。完成此操作后,可以使用以下代码添加OpenAPI文档的路径:
代码语言:javascript复制with app.test_request_context():
spec.path(view=get_models)
spec.path(view=get_metadata)
spec.path(view=predict)
从代码库加载所有组件后,可以使用此代码将OpenAPI文档作为YAML文件保存到磁盘。生成的文件可以在这里找到。还有一个OpenAPI文档的开源查看器,它能够自动生成代码并呈现用于查看文档的网页:
由openapi.py脚本创建的OpenAPI规范的Swagger UI视图
结论
在这篇博客文章中,展示了如何创建一个Web应用程序,该应用程序能够托管任何继承并遵循MLModel基类标准的模型。通过使用抽象来处理机器学习模型代码,可以编写可以部署任何模型的应用程序,而不是构建只能部署一个ML模型的应用程序。
这篇博文的方法的一个缺点是,从模型对象的predict()方法给出和返回的对象中的字段类型必须可序列化为JSON,并且模式包必须能够为它们创建JSON模式。对于更复杂的数据模型,这并不总是很容易。由于这是一个Web应用程序,因此使用JSON模式很有意义,但在某些情况下,JSON模式不是发布模式信息的最佳方式。
要强调的一点是,有意为模型代码和应用程序代码维护单独的代码库。在这种方法中,模型是一个安装在应用程序代码库中的python包。通过将模型代码与应用程序代码分离,创建模型的新版本变得更简单,更直接。它还使数据科学家和工程师能够维护更好地满足其需求的单独代码库,并且可以在多个应用程序中部署相同的模型包并部署相同模型的不同版本。