跳至主要内容

在 Python 中将机器学习模型变成 API

学习如何使用 Flask 在 Python 中从机器学习模型创建一个简单 API。
· 15分钟 阅读

使用 AI 探索

请考虑以下情境:

您构建了一个非常酷的机器学习模型,能够预测某笔交易是否存在欺诈。现在,您的朋友正在开发一款用于一般银行业务的安卓应用,希望将您的机器学习模型集成到他们的应用中以实现这一功能。

但您的朋友发现,您用 Python 编写了模型,而他用 Java 开发应用。那么,是否无法把您的机器学习模型集成到朋友的应用里呢?

幸运的是,您拥有 API 的力量。上述情形只是众多需要将机器学习模型转为 API 的情况之一。如今,许多行业都在寻找能完成这一步的数据科学家。事实上,把机器学习模型封装成 API 并不难,而这正是本教程要做的事——将您的机器学习模型变成一个 API。

实现机器学习模型的选项

多数情况下,机器学习模型的真正价值体现在智能产品的核心——也许是推荐系统中的一个小组件,或是一款智能聊天机器人。而这往往是技术壁垒看起来最难跨越的时候。

例如,大多数机器学习从业者使用 R/Python 进行实验。但这些模型的使用者通常是采用完全不同技术栈的软件工程师。解决这个问题有两种途径:

  • 用软件工程团队所用的语言重写全部代码。这看似不错,但要复制那些复杂模型所需的时间与精力将被白白浪费。多数语言(例如 JavaScript)并没有用于机器学习的强大库,明智之举是尽量避免。
  • API 优先的方法——Web API 让跨语言应用协作变得容易。如果前端开发者想用您的模型为 Web 应用提供机器学习能力,他们只需要获取提供该 API 的 URL 端点即可。

在继续之前,让我们先了解什么是 API。

什么是 API?

“简单来说,API 是两个软件之间的一份(假想的)契约:如果用户软件按预定义格式提供输入,后者就会扩展其功能并将结果返回给用户软件。” - Analytics Vidhya

您可以阅读以下文章,了解为何 API 深受开发者青睐:

本质上,API 很像 Web 应用,但它并不会给您返回精美的 HTML 页面,而是倾向于以 JSON、XML 等标准数据交换格式返回数据。开发者拿到所需输出后,可以按任意方式呈现。也有许多热门的机器学习 API,例如 IBM Watson 的 ML API,具备以下能力:

  • 机器翻译——帮助在不同语对之间翻译文本。
  • 信息共鸣——面向特定受众评估词语或短语的流行度。
  • 问答系统——从主要文档源触发的查询中提供直接答案。
  • 用户建模——根据给定文本预测某人的社会特征。

Google Vision API 也是出色的例子,它为计算机视觉任务提供专用服务。要了解 Google Vision API 能做什么,请点击这里。

基本上,主流云服务商和一些专注机器学习的小公司会提供开箱即用的 API。他们服务于那些缺乏机器学习专长、但希望在流程或产品套件中落地机器学习的开发者/企业。

面向 Web 开发的机器学习 API 的热门示例包括 DialogFlow、Microsoft 的 Cognitive Toolkit、TensorFlow.js 等。

既然您已对 API 有了大致认识,接下来看看如何将一个(用 Python 开发的)机器学习模型封装成 Python 中的 API。

Flask —— Python 中的 Web 服务框架:

您可能会问,什么是 Web 服务?Web 服务也是一种 API,只不过它假定 API 部署在服务器上,供他人调用。Web API、Web Service——这些术语通常可互换使用。

说到 Flask,它是 Python 中的一个 Web 服务开发框架。它并非唯一选择,Python 中还有 Django、Falcon、Hug 等。但在本教程中您将使用 Flask。关于 Flask 的学习资料,可参考这些教程。

如果您安装了 Anaconda 发行版,就已经内置 Flask。否则请自行安装:

pip install flask

Flask 非常精简。Flask 之所以深受 Python 开发者喜爱,原因之一是它自带一个轻量级 Web 服务器,几乎无需配置,且可通过您的 Python 代码控制。这也是它如此受欢迎的原因之一。

下面的代码很好地展示了 Flask 的“极简”。这段代码用来创建一个简单的 Web API:当收到特定 URL 请求时,返回特定输出。

from flask import Flask

app = Flask(__name__)


@app.route("")
def hello():
    return "Welcome to machine learning model APIs!"


if __name__ == '__main__':
    app.run(debug=True)

执行后,您可以在终端显示的地址上(在浏览器中输入该地址)查看结果。

terminal

一些要点:

  • Jupyter Notebooks 非常适合书写 Markdown、R 和 Python 的相关内容。但在构建 Web 服务器时,可能会出现不稳定行为。因此,建议在 Sublime 等文本编辑器中编写 Flask 代码,并从终端/命令行运行。

  • 确保不要将文件命名为 flask.py。

  • Flask 默认运行在 5000 端口。有时 Flask 服务器虽成功在该端口启动,但当您在浏览器或 Postman 等 API 客户端中访问终端返回的 URL 时,可能没有输出。看如下情形:

output 1

  • 按照 Flask 的提示,服务器已在 5000 端口成功启动,但在浏览器中访问 URL 时未返回任何内容。这可能是端口冲突。在这种情况下,将默认端口 5000 改为您希望的端口是不错的选择。您只需这样做:

    app.run(debug=True,port=12345)

  • 此时,Flask 服务器会变成如下所示:

output 2

现在,让我们逐步过一遍您写的代码:

  • 您创建了 Flask 类的一个实例,并传入 "name" 变量(由 Python 自动填充)。如果此文件作为脚本被 Python 直接运行,该变量就是 "main"。如果您以导入方式使用该文件,"name" 的值就是被导入文件的名称。例如,若有 test.py 和 run.py,并在 run.py 中导入 test.py,则 test.py 的 "name" 值为 test(app = Flask(test))。

  • 在 hello() 方法定义的上方有 @app.route("")。route() 是一个装饰器,用于告诉 Flask 哪个 URL 应该触发 hello() 这个函数。

  • hello() 方法负责在您的 API 被正确访问(或被消费)时产生输出(Welcome to machine learning model APIs!)。在本例中,使用 localhost:5000/ 访问浏览器会得到预期输出(前提是 Flask 服务器运行在 5000 端口)。

接下来,您将了解把(使用 scikit-learn 构建的)机器学习模型转成 Flask API 时需要注意的一些因素。

将 scikit-learn 模型与 Flask 一起使用

借助 scikit-learn,从构建非常简单到非常复杂的机器学习模型,在 Python 中从未如此容易。但关于 scikit-learn,您需要记住几点:

  • Scikit-learn 是一个 Python 库,提供用于数据挖掘和数据分析的简洁高效工具。Scikit-learn 的主要模块包括:
    • 聚类
    • 回归
    • 分类
    • 降维
    • 模型选择
    • 预处理

(务必看看 DataCamp 的 使用 scikit-learn 的监督学习 课程,由 scikit-learn 核心开发者 Andreas Müller 授课)

  • Scikit-learn 支持对您训练的模型进行序列化与反序列化。这可以免去您反复训练模型的时间。有了用 scikit-learn 生成的模型序列化副本,您就可以编写 Flask API。

  • Scikit-learn 模型要求数据为数值格式。因此,如果数据集中存在非数值的分类特征,务必将其转换为数值。为此,scikit-learn 提供 LabelEncoder、OneHotEncoder 等工具,可在 sklearn.preprocessing 模块中找到。

  • Scikit-learn 模型无法隐式处理缺失值。您需要自行处理数据集中的缺失值,然后再将其输入模型。为处理缺失值,scikit-learn 提供了大量工具,可在 sklearn.preprocessing 模块中找到。

标签编码和缺失值处理是构建优质机器学习模型所必需的重要数据预处理步骤。若想进一步学习,请查看 DataCamp 提供的以下课程:

在本教程中,您将使用 Titanic 数据集。它之所以广受欢迎,原因包括:包含多种类型的变量、存在缺失值等。此 DataCamp 教程对该数据集给出了出色的分析,您可以从这里下载数据集。

该数据集是一个分类问题:给定乘客的一些信息,预测其是否幸存。

注:在本教程中,“变量”(Variables)与“特征”(Features)这两个术语多次互换使用。

为进一步简化,您只使用四个变量:age、sex、embarked 和 survived,其中 survived 是类别标签。

# Import dependencies
import pandas as pd
import numpy as np
# Load the dataset in a dataframe object and include only four features as mentioned
url = "http://s3.amazonaws.com/assets.datacamp.com/course/Kaggle/train.csv"
df = pd.read_csv(url)
include = ['Age', 'Sex', 'Embarked', 'Survived'] # Only four features
df_ = df[include]

“Sex”和“Embarked”是非数值的分类特征,因此需要进行数值转换;“Age” 特征存在缺失值。这些值可以用中位数或均值等统计量填补。缺失值可能很有意义,在真实应用中值得探究其含义。

Scikit-learn 将空白单元格视为 NaN。这里,您将简单地把 NaN 替换为 0,并为此编写一个辅助函数。

categoricals = []
for col, col_type in df_.dtypes.iteritems():
     if col_type == 'O':
          categoricals.append(col)
     else:
          df_[col].fillna(0, inplace=True)

上述代码的作用如下:

  • 遍历数据框 df 的所有列,并将含非数值的列名追加到列表 categorical 中。
  • 若某列不是非数值(在此示例中只有 Age),则检查其是否存在缺失值,并用 0 填充。

    用单个数值填补 NaN 可能带来意想不到的后果,尤其当用于替换 NaN 的数值落在该数值变量的观察范围内时。由于 0 并非年龄的观察到且合法的取值,您不会引入偏差;但若用 36 之类的值则会! - 来源

既然您已处理了缺失值并分离出非数值列,接下来要把它们转换为数值。您将使用独热编码(One Hot Encoding)。Pandas 提供了简单的方法 get_dummies() 来为给定数据框创建独热编码变量。

df_ohe = pd.get_dummies(df_, columns=categoricals, dummy_na=True)

使用独热编码时,每个“列/取值”组合都会生成一个新列,命名格式为 列_取值。比如——对“Embarked”变量,独热编码会生成“Embarked_C”、“Embarked_Q”、“Embarked_S”以及“Embarked_nan”。

现在您已成功完成数据预处理,可以开始训练机器学习模型。这里将使用逻辑回归分类器。

from sklearn.linear_model import LogisticRegression
dependent_variable = 'Survived'
x = df_ohe[df_ohe.columns.difference([dependent_variable])]
y = df_ohe[dependent_variable]
lr = LogisticRegression()
lr.fit(x, y)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

您的机器学习模型已经构建完成。接下来要保存该模型。严格来说,是将模型序列化。在 Python 中,这称为Pickling(腌制)。

保存模型:序列化与反序列化

您将使用 sklearn 的 joblib。

from sklearn.externals import joblib
joblib.dump(lr, 'model.pkl')
['model.pkl']

逻辑回归模型现已持久化。您可以用一行代码将其加载到内存中。将模型重新加载回工作区称为反序列化。

lr = joblib.load('model.pkl')

现在,您已准备好使用 Flask 来服务这个持久化模型。前面您已经见识了 Flask 上手有多“极简”。

使用 Flask 从机器学习模型创建 API

要用 Flask 提供模型服务,您将做两件事:

  • 在应用启动时,将已持久化的模型加载到内存,
  • 创建一个 API 端点,接收输入变量,将其转换为合适的格式,并返回预测结果。

更具体地,发往 API 的示例输入如下:

[
    {"Age": 85, "Sex": "male", "Embarked": "S"},
    {"Age": 24, "Sex": '"female"', "Embarked": "C"},
    {"Age": 3, "Sex": "male", "Embarked": "C"},
    {"Age": 21, "Sex": "male", "Embarked": "S"}
]

(即一个 JSON list 的输入)

而 API 的输出如下:

{"prediction": [0, 1, 1, 0]}

预测表示幸存状态,其中 0 代表否,1 代表是。

JSON 是 JavaScript 对象表示法,是使用最广泛的数据交换格式之一。若需快速入门,请参阅这些教程。

让我们编写一个 predict() 函数来完成:

  • 在应用启动时加载持久化模型,
  • 创建一个 API 端点,接收输入变量,将其转换为合适的格式,并返回预测结果。

您已经知道如何加载持久化模型。现在重点放在:收到输入后,如何用它来预测幸存状态。

from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/predict', methods=['POST'])
def predict():
     json_ = request.json
     query_df = pd.DataFrame(json_)
     query = pd.get_dummies(query_df)
     prediction = lr.predict(query)
     return jsonify({'prediction': list(prediction)})

太棒了!不过这里还有个小问题。

您写的函数只在传入请求包含分类变量的所有可能取值时才会工作,而这在实际中未必成立。如果传入请求不包含分类变量的全部可能取值,那么按当前 predict() 的定义,get_dummies() 会生成一个列数少于分类器期望值的数据框,从而导致运行时错误。

为解决此问题,您还需要在模型训练时将列名列表一并持久化。任何 Python 对象都可以序列化为 .pkl 文件。您将像之前一样使用 joblib。

(请记住,如前所述,进行所有服务器端编码时,最好在文本编辑器中完成并从终端运行)

model_columns = list(x.columns)
joblib.dump(model_columns, 'model_columns.pkl')
['model_columns.pkl']

既然您已经持久化了列名列表,那么在预测时只需处理缺失列即可。您需要在应用启动时加载这些列名。

@app.route('/predict', methods=['POST']) # Your API endpoint URL would consist /predict
def predict():
    if lr:
        try:
            json_ = request.json
            query = pd.get_dummies(pd.DataFrame(json_))
            query = query.reindex(columns=model_columns, fill_value=0)

            prediction = list(lr.predict(query))

            return jsonify({'prediction': prediction})

        except:

            return jsonify({'trace': traceback.format_exc()})
    else:
        print ('Train the model first')
        return ('No model here to use')

您已在“/predict”API 中纳入所有必要元素,现在只需编写主类。

if __name__ == '__main__':
    try:
        port = int(sys.argv[1]) # This is for a command-line argument
    except:
        port = 12345 # If you don't provide any port then the port will be set to 12345
    lr = joblib.load(model_file_name) # Load "model.pkl"
    print ('Model loaded')
    model_columns = joblib.load(model_columns_file_name) # Load "model_columns.pkl"
    print ('Model columns loaded')
    app.run(port=port, debug=True)

您的 API 现已可以部署。在继续之前,让我们回顾一下到目前为止您做了什么:

整合到一起:

  • 您加载了 Titanic 数据集,并选择了四个特征。
  • 您完成了必要的数据预处理。
  • 您训练了逻辑回归分类器并将其序列化。
  • 您还将训练时的全部列名序列化,用以解决预测时列数少于预期的问题。
  • 随后,您编写了一个简单的 Flask API,根据年龄、性别和登船港信息预测某人在海难中是否幸存。

让我们把所有代码放在一起,避免遗漏。同时,良好的编程实践是将逻辑回归模型代码与 Flask API 代码分别放在不同的 .py 文件中。

因此,您的 model.py 应如下所示:

# Import dependencies
import pandas as pd
import numpy as np

# Load the dataset in a dataframe object and include only four features as mentioned
url = "http://s3.amazonaws.com/assets.datacamp.com/course/Kaggle/train.csv"
df = pd.read_csv(url)
include = ['Age', 'Sex', 'Embarked', 'Survived'] # Only four features
df_ = df[include]

# Data Preprocessing
categoricals = []
for col, col_type in df_.dtypes.iteritems():
     if col_type == 'O':
          categoricals.append(col)
     else:
          df_[col].fillna(0, inplace=True)

df_ohe = pd.get_dummies(df_, columns=categoricals, dummy_na=True)

# Logistic Regression classifier
from sklearn.linear_model import LogisticRegression
dependent_variable = 'Survived'
x = df_ohe[df_ohe.columns.difference([dependent_variable])]
y = df_ohe[dependent_variable]
lr = LogisticRegression()
lr.fit(x, y)

# Save your model
from sklearn.externals import joblib
joblib.dump(lr, 'model.pkl')
print("Model dumped!")

# Load the model that you just saved
lr = joblib.load('model.pkl')

# Saving the data columns from training
model_columns = list(x.columns)
joblib.dump(model_columns, 'model_columns.pkl')
print("Models columns dumped!")

您的 api.py 应如下所示:

# Dependencies
from flask import Flask, request, jsonify
from sklearn.externals import joblib
import traceback
import pandas as pd
import numpy as np

# Your API definition
app = Flask(__name__)

@app.route('/predict', methods=['POST'])
def predict():
    if lr:
        try:
            json_ = request.json
            print(json_)
            query = pd.get_dummies(pd.DataFrame(json_))
            query = query.reindex(columns=model_columns, fill_value=0)

            prediction = list(lr.predict(query))

            return jsonify({'prediction': str(prediction)})

        except:

            return jsonify({'trace': traceback.format_exc()})
    else:
        print ('Train the model first')
        return ('No model here to use')

if __name__ == '__main__':
    try:
        port = int(sys.argv[1]) # This is for a command-line input
    except:
        port = 12345 # If you don't provide any port the port will be set to 12345

    lr = joblib.load("model.pkl") # Load "model.pkl"
    print ('Model loaded')
    model_columns = joblib.load("model_columns.pkl") # Load "model_columns.pkl"
    print ('Model columns loaded')

    app.run(port=port, debug=True)

很不错!现在,您将使用名为 Postman 的 API 客户端来测试该 API。请确保 model.py 与 api.py 位于同一目录,并在测试前已分别编译完成。参考下图终端快照(在两个 .py 文件均成功编译后截取)。

terminal 2

如果所有文件均已成功编译,目录结构应如下所示:

注:IPYNB 文件是可选的。

在 Postman 中测试您的 API

要测试 API,您需要某种 API 客户端。Postman 无疑是其中最佳选择之一。您可以通过上面的链接轻松下载。

如果您下载的是最新版,Postman 的界面如下所示:
postman

成功启动 Flask 服务器后,您需要在 Postman 中输入正确的 URL 与端口号。应类似如下:

flask server

恭喜!您刚刚构建了第一个机器学习 API。

该 API 能根据乘客的 age、sex 和 embarked 信息,预测其是否在 Titanic 海难中幸存。现在,您的朋友可以在前端代码中调用它,并将 API 的输出加工成有趣的内容。

更进一步:

在本教程中,您学习了全栈数据科学家最重要的行业技能之一,即从机器学习模型构建 API。尽管这个 API 很简单,但从最简单的事情入手总是更好,这样您才能掌握细节中的门道。

您可以做更多改进。可考虑的选项包括:

  • 编写一个“/train”API,用于用数据训练逻辑回归分类器。
  • 使用 keras 编写一个神经网络模型,并构建相应的 API。
  • 将您的 API 部署到云端以供调用。
  • 若要进一步深入,可参考 Machine Learning Mastery 的这篇博客,其中讨论了多种工业级方法。

这里的可能性与机遇巨大。您只需谨慎选择最适合自己的路径。

如果您想进一步学习 Python 机器学习,请参加 DataCamp 的 Python 中的机器学习预处理 课程,并查看我们的 机器学习基础——规范 教程。

参考资料:

撰写本文时参考了以下资料:

主题