机器学习在各行各业中的应用越来越广泛,促使人们需要高效和直接的方法来训练和部署ML模型。这对于表格模型来说尤其如此,因为表格是商业环境中最常见的数据格式,它被用来分析客户行为、预测销售和优化供应链等应用。在这些快速变化的环境中,快速原型设计往往是成功的关键。然而,训练、微调和部署ML模型的过程仍然耗时且复杂。因此,能够简化模型开发的工具会非常有用,因为它们允许数据科学家快速生成有效的解决方案,并适应不断变化的业务需求。
与数据科学家主要关注模型构建的误解相反,他们的核心任务应该是理解问题、问题背后的过程,并收集相关数据来解决问题。虽然模型建立不是主要活动,但它仍然消耗了大量的时间。特征预处理、缺失值的处理或不平衡类的处理在手工完成时可能是很麻烦的。此外,超参数优化的迭代过程也很冗长,可能会影响模型的性能。所有这些工作都会拖慢开发过程,如果不仔细做,会导致次优结果。
除了与模型选择有关的挑战外,序列化和部署也可能是一个主要障碍。坚持一个ML模型的最流行的方法之一是使用Python的pickle格式。虽然被广泛使用,但它也有很大的缺点。由于潜在的代码注入漏洞,它并不安全,需要所有的原始库都存在,并且当库的版本改变时可能会中断。这种脆弱性和缺乏安全性对于生产环境来说是不理想的。
Pickle的一个可靠的替代方案是ONNX序列化格式,它允许将模型转化为标准化节点的计算图,无论目标操作系统或编程语言如何,都可以在许多不同的平台上轻松共享和部署。ONNX促进了互操作性,这有助于用户避免被厂商锁定,并从不同的ML框架的优势中获益。然而,将ML管道的某些功能转换为ONNX格式并不总是简单的。
为了解决这两个挑战,我们开发了Falcon,一个开源的AutoML库,旨在简化训练和部署过程。有了Falcon,开发者可以用一行代码来训练ML模型,使新手更容易掌握这一过程。此外,每个训练好的模型(包括数据处理步骤)都会自动输出到ONNX,而无需额外的用户输入。
在这篇文章中,我们将探讨Falcon AutoML相对于使用scikit-learn的传统手工训练方法的优势。我们将演示如何训练一个用于预测流失率的简单分类模型,将其导出为ONNX格式,并使用FastAPI将其部署为REST-API微服务。之后,我们将比较使用和不使用Falcon的过程,以展示该库的简单性。
第1步:训练和保存模型
在这一部分,我们将使用Telco客户流失数据集来建立一个简单的分类器。这个数据集由19个特征组成,如客户的人口统计信息、使用的服务等。目标列 "churn "表示客户是否在上个月离开。
准备环境
作为第一步,我们需要安装所有需要的库。我们将使用scikit-learn、numpy和pandas进行初始预处理和管道构建。此外,我们将使用XGBoost来建立模型,因为它被认为是在表格数据上进行监督性预测建模的最先进技术。
pip install "scikit-learn>=1.2.0" pandas matplotlib imbalanced-learn xgboost
为了将我们的管道转换为onnx,需要三个额外的库。它们的确切用途将在后面解释。
pip install skl2onnx onnxmltools onnx
当所有的库都安装好后,我们可以导入必要的子模块:
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom imblearn.over_sampling import RandomOverSamplerfrom sklearn.model_selection import train_test_split, GridSearchCVfrom sklearn.compose import ColumnTransformerfrom sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoderfrom sklearn.pipeline import make_pipelinefrom sklearn.metrics import balanced_accuracy_scorefrom xgboost import XGBClassifierfrom skl2onnx import to_onnx, update_registered_converterfrom onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboostfrom skl2onnx.common.shape_calculator import calculate_linear_classifier_output_shapesfrom skl2onnx.common.data_types import FloatTensorType, StringTensorTypefrom onnx import save_model
数据准备
在训练模型之前,我们需要加载和准备数据集。一个重要的事情是验证是否有缺失的值。例如,在我们的案例中,在 "TotalCharges "列中有11个缺失项。为了解决这个问题,我们将采用最简单的技术,从数据集中删除相应的行。
df = pd.read_csv('train_dataset.csv')print(df.shape)df.head()
def explore_dataset(df): features = [] dtypes = [] count = [] unique = [] nans = [] for item in df.columns: features.append(item) dtypes.append(df[item].dtype) count.append(len(df[item])) unique.append(len(df[item].unique())) nans.append(df[item].isna().sum()) output = pd.DataFrame({ 'Feature': features, 'Dtype': dtypes, 'Count': count, 'Nr Unique': unique, 'Nr NA': nans }) return outputexplore_dataset(df)df.dropna(axis = 0, inplace = True)
分割数据
为了估计模型的性能,我们将分配一个固定的测试集,包含原始数据的25%。一般来说,分割成训练子集和测试子集必须在任何额外的预处理步骤之前尽早完成。
seed = 42# features XX = df.drop('Churn', axis = 1)# target yy = df['Churn']# split the dataset into train and test subsetsX_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = seed)
特征预处理
一个常见的与模型无关的预处理方法是将数字特征放大到平均值=0和标准值=1,并将分类特征编码为单热向量。尽管XGBoost并不严格要求数字特征的缩放,而且可以原生处理分类特征(需要额外的配置),但为了例子的完整性,我们还是会同时进行缩放和编码。为了将这两个预处理结合到一个单一的操作中,我们可以使用一个ColumnTransformer,为指定的列分配独立的预处理。对于目标列,我们将简单地应用一个LabelEncoder,用连续的整数替换字符串类别。
num_columns = [4, 17, 18]cat_columns = [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]transformers = [("num", StandardScaler(), num_columns), ("cat", OneHotEncoder(handle_unknown='ignore', sparse_output=False), cat_columns)]ct = ColumnTransformer(transformers)# fit the transformers using X_trainct.fit(X_train)le = LabelEncoder()# fit label encoderle.fit(y_train)
数据集再平衡
作为最后一个预处理步骤,我们需要确保数据集是平衡的。否则,少数人类别的代表性就会不足,有可能被模型忽略。在我们的案例中,从下面的柱状图中可以看出,有明显更多的样本实例,其中有 Churn = No.因此,我们需要对此进行补偿。同样,我们将遵循最简单的方法,简单地复制少数类的样本,以平衡比例。
unique_labels, counts = np.unique(df['Churn'], return_counts=True)plt.bar(unique_labels, counts)plt.xlabel('Churn')plt.ylabel('Number of samples')plt.title('Churn distribution in the dataset')plt.show()
X_train_resampled, y_train_resampled = RandomOverSampler(random_state = seed).fit_resample(X_train, y_train)
训练
为了确定最理想的超参数,我们将采用网格搜索技术,详尽地循环搜索一组指定的超参数候选值。
之后,我们可以用找到的最佳超参数来训练最终模型,并在测试集上评估其性能。
# specify heperparameters grid to search overparameters = [{'booster': ['gbtree', 'dart'], 'n_estimators': [50,100,150],'max_depth': [None, 5, 10],'random_state': [42]}]clf = GridSearchCV( XGBClassifier(), parameters, scoring='balanced_accuracy', verbose = 3 )clf.fit(ct.transform(X_train), le.transform(y_train))# fit the final pipelinepipeline = make_pipeline(ct, XGBClassifier(**clf.best_params_))pipeline.fit(X_train_resampled, le.transform(y_train_resampled))# evaluatey_pred = pipeline.predict(X_test)balanced_accuracy_score(y_test, le.inverse_transform(y_pred))
导出到ONNX
为了将整个scikit-learn管道导出到ONNX,我们可以使用skl2onnx工具。然而,XGBoost分类器并不是一个原生的scikit-learn模型,因此skl2onnx不知道如何正确转换它。为了解决这个问题,我们需要提供一个自定义的转换器。幸运的是,另一个软件包`onnxmltools`已经包含了所需的XGBoost转换器,它也与skl2onnx兼容,所以我们可以简单地重新使用它。
转换器被注册后,我们需要准备模型输入的规格。由于我们的数据集包含了数字和分类特征的混合,而ONNX的张量是强类型的,所以我们将不能有一个单一的矩阵/张量作为输入。相反,每一列将被独立传递和预处理。我们为分类特征指定StringTensorType,为数字特征指定FloatTensorType。每个输入将有一个形状 ***[无,1]***对应于一个单列和任意的批次大小(样本数)。
其余的转换过程是直截了当的。我们调用`to_onnx`函数,并传递管道和输入。此外,我们为标准标度器提供了一个额外的配置选项,这是为了弥补scikit-learn标准标度器使用float64而onnx标度器使用float32的事实,但我们不会在此详述。
update_registered_converter( XGBClassifier, "XGBoostXGBClassifier", calculate_linear_classifier_output_shapes, convert_xgboost, options={"nocl": [True, False], "zipmap": [True, False, "columns"]},)initial_types = []for i, c in enumerate(df.columns[:-1]): if i in cat_columns: tensor_type = StringTensorType else: tensor_type = FloatTensorType initial_types.append((c, tensor_type([None, 1])))onx = to_onnx(pipeline, initial_types = initial_types, options={StandardScaler: {"div": "div_cast"}})save_model(onx, 'model.onnx')
第2步:部署模型
在这一节中,我们将创建一个简单的网络微服务,暴露一个*/predict*端点。
首先,我们需要安装FastAPI和Uvicorn。此外,我们需要一个onnxruntime来运行该模型。
pip install fastapi "uvicorn[standard]" onnxruntime
一旦我们安装了所有的依赖项,我们就可以开始开发微服务。首先,我们需要创建一个文件,名为 constants.py的文件,该文件将保存与模型相关的元数据,如输入名称(对应于特征名称)、分类指数和类的名称。
# constants.pyMODEL_PATH = "model.onnx"INPUT_NAMES = [ "gender", "SeniorCitizen", "Partner", "Dependents", "tenure", "PhoneService", "MultipleLines", "InternetService", "OnlineSecurity", "OnlineBackup", "DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies", "Contract", "PaperlessBilling", "PaymentMethod", "MonthlyCharges", "TotalCharges",]CAT_IND = [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]# can be obtained by calling le.classes_ on the fitted LabelEncoderCLASSES = ["No", "Yes"]
接下来,我们需要创建一个函数,将一个列表作为输入,其中每个内部列表代表一个样本,并将其转换为ONNX运行时可以处理的格式。
# convert.pyfrom typing import List, Dictimport numpy as np from constants import CAT_IND, INPUT_NAMESdef convert_input(X: List[List]) -> Dict[str, np.ndarray]: X = np.array(X, dtype=object) onnx_input = {} for i, n in enumerate(INPUT_NAMES): if i in CAT_IND: onnx_input[n] = X[:, i].astype(str).reshape(-1, 1) else: onnx_input[n] = X[:, i].astype(np.float32).reshape(-1, 1) return onnx_input
最后,我们创建一个小型FastAPI实例,该实例有一个端点,接收数据并返回预测结果。
# main.pyfrom typing import Listimport onnxruntime as ortfrom constants import MODEL_PATH, CLASSESfrom convert import convert_inputfrom fastapi import FastAPIapp = FastAPI()sess = ort.InferenceSession(MODEL_PATH)@app.post("/predict")def predict(X: List[List]): pred = sess.run(["output_label"], convert_input(X))[0].tolist() y = [CLASSES[i] for i in pred] return {"y": y}
我们可以通过运行该命令来启动服务器:
uvicorn main:app --reload
如果一切设置正确,自动生成的文档页面应该可以在localhost:8000/docs.通过扩展描述中的 /predict端点的描述并点击 "Try it out",我们可以验证它是否正常工作。如下图所示,我们可以在第一个文本字段中输入数据点的特征,服务器将回应一个预测列表(显示在图片的最底部)。
第三步:使用Falcon重复步骤(1)和(2)。
现在我们将重复上述步骤,但使用Falcon ML 库,这将大大减少我们必须编写的代码量。
首先,我们来安装Falcon本身。由于我们在之前的指南中使用了XGBoost作为我们的模型,我们将再次使用它。为此,我们还需要安装 falcon-ml-xgboost扩展。扩展是Falcon的另一个很好的功能:它默认只包括绝对必要的依赖,但允许轻松扩展。一旦安装了扩展,Falcon会自动注册,不需要用户做任何额外的操作。
pip install falcon-ml falcon-ml-xgboost
正如我们已经提到的,使用Falcon,只需一行代码就可以训练一个模型**。**
from falcon import AutoMLAutoML( task = 'tabular_classification', train_data = 'train_dataset.csv', config = 'XGBOOST::OptunaLearner')
你可能已经注意到,我们提供了一个名为 "XGBOOST::OptunaLearner "的配置名称。这个名字在 ***::***表示扩展的名称(在我们的例子中是XGBoost),而 OptunaLearner 意味着Optuna框架将被用来优化超参数。
在训练过程结束时,Falcon会将模型保存到当前工作目录中,使我们能够立即进行部署。
此外,Falcon还提供了一个围绕ONNX Runtime的小型封装器,负责解析模型输入(及其类型)并将数据转换为所需格式。因此,我们不再需要constants.py和convert.py文件。
# main.pyfrom typing import Listfrom falcon.runtime import ONNXRuntimeimport numpy as npfrom fastapi import FastAPIapp = FastAPI()rt = ONNXRuntime("model_falcon.onnx")@app.post("/predict")def predict(X: List[List]): y = rt.run(np.asarray(X, dtype=object))[0].tolist() return {"y": y}
结论
总之,使用Falcon ML、ONNX和FastAPI,我们能够在不到15行的代码中训练和部署一个模型。模型建立过程中的这种简化使数据科学家能够专注于理解和解决手头的核心业务问题,而不是在模型训练和调整上花费过多时间。