8.2. 计算性能#

对于某些应用,估计器的性能(主要是预测时的延迟和吞吐量)至关重要。考虑训练吞吐量也可能很有趣,但这在生产环境中通常不太重要(因为它通常是离线进行的)。

我们将在这里回顾您在不同情况下可以从许多 scikit-learn 估计器中获得的数量级,并提供一些克服性能瓶颈的技巧和窍门。

预测延迟是指进行预测所需的经过时间(例如,以微秒为单位)。延迟通常被视为一种分布,运营工程师通常关注此分布给定百分位数的延迟(例如,第 90 个百分位数)。

预测吞吐量定义为软件在给定时间内可以提供的预测数量(例如,每秒预测次数)。

性能优化的一个重要方面是它可能会损害预测准确性。事实上,更简单的模型(例如线性模型而不是非线性模型,或者参数更少的模型)通常运行速度更快,但并不总是能够像更复杂的模型那样考虑到数据的相同确切属性。

8.2.1. 预测延迟#

在使用/选择机器学习工具包时,人们可能遇到的最直接的问题之一是在生产环境中进行预测的延迟。

影响预测延迟的主要因素是

  1. 特征数量

  2. 输入数据表示和稀疏性

  3. 模型复杂度

  4. 特征提取

最后一个主要参数也是批量或一次一个地进行预测的可能性。

8.2.1.1. 批量模式与原子模式#

一般来说,批量预测(同时预测多个实例)效率更高,原因有很多(分支可预测性、CPU 缓存、线性代数库优化等)。在这里,我们看到在一个特征很少的设置中,独立于估计器的选择,批量模式总是更快,并且对于其中一些模式,速度提高了 1 到 2 个数量级

atomic_prediction_latency

bulk_prediction_latency

要针对您的情况对不同的估计器进行基准测试,您只需更改此示例中的 n_features 参数:预测延迟。这应该可以让您估计预测延迟的数量级。

8.2.1.2. 配置 Scikit-learn 以减少验证开销#

Scikit-learn 对数据进行了一些验证,这增加了每次调用 predict 和类似函数的开销。特别是,检查特征是否有限(不是 NaN 或无穷大)需要对数据进行全面遍历。如果确定数据是可接受的,则可以通过在导入 scikit-learn 之前将环境变量 SKLEARN_ASSUME_FINITE 设置为非空字符串来禁止检查有限性,或者使用 set_config 在 Python 中进行配置。为了比这些全局设置提供更多控制,可以使用 config_context 在指定上下文中设置此配置。

>>> import sklearn
>>> with sklearn.config_context(assume_finite=True):
...     pass  # do learning/prediction here with reduced validation

请注意,这将影响上下文中所有 assert_all_finite 的使用。

8.2.1.3. 特征数量的影响#

显然,当特征数量增加时,每个示例的内存消耗也会增加。实际上,对于具有 \(M\) 个实例和 \(N\) 个特征的矩阵,空间复杂度为 \(O(NM)\)。从计算角度来看,这也意味着基本操作(例如,线性模型中向量矩阵乘法的乘法)的数量也会增加。以下是预测延迟随特征数量变化的图表。

influence_of_n_features_on_latency

总的来说,您可以预期预测时间至少会随着特征数量线性增加(非线性情况可能会发生,具体取决于全局内存占用和估计器)。

8.2.1.4. 输入数据表示的影响#

Scipy 提供了稀疏矩阵数据结构,这些结构针对存储稀疏数据进行了优化。稀疏格式的主要特点是您不存储零,因此如果您的数据是稀疏的,那么您使用的内存要少得多。稀疏(CSR 或 CSC)表示中的非零值平均只占用一个 32 位整数位置 + 64 位浮点值 + 矩阵中每行或每列额外的 32 位。在密集(或稀疏)线性模型上使用稀疏输入可以大大加快预测速度,因为只有非零值特征会影响点积,从而影响模型预测。因此,如果在 1e6 维空间中有 100 个非零值,则只需要 100 次乘法和加法运算,而不是 1e6 次。

然而,对密集表示的计算可以利用 BLAS 中高度优化的向量运算和多线程,并且往往会导致更少的 CPU 缓存未命中。因此,对于具有多个 CPU 和优化 BLAS 实现的机器,稀疏性通常应该相当高(最多 10% 的非零值,需要根据硬件进行检查),以便稀疏输入表示比密集输入表示更快。

以下是用示例代码测试输入的稀疏性:

def sparsity_ratio(X):
    return 1.0 - np.count_nonzero(X) / float(X.shape[0] * X.shape[1])
print("input sparsity ratio:", sparsity_ratio(X))

根据经验,您可以认为,如果稀疏率大于 90%,那么您可能会从稀疏格式中受益。有关如何构建(或将数据转换为)稀疏矩阵格式的更多信息,请参阅 Scipy 的稀疏矩阵格式文档。大多数情况下,CSRCSC 格式效果最佳。

8.2.1.5. 模型复杂性的影响#

一般来说,当模型复杂性增加时,预测能力和延迟应该会增加。提高预测能力通常很有趣,但对于许多应用程序来说,我们最好不要过多地增加预测延迟。现在,我们将针对不同系列的监督模型来回顾这一想法。

对于 sklearn.linear_model(例如 Lasso、ElasticNet、SGDClassifier/Regressor、Ridge & RidgeClassifier、PassiveAggressiveClassifier/Regressor、LinearSVC、LogisticRegression…),在预测时应用的决策函数是相同的(点积),因此延迟应该是相等的。

下面是一个使用 SGDClassifierelasticnet 惩罚的示例。正则化强度由 alpha 参数全局控制。使用足够高的 alpha,可以增加 elasticnetl1_ratio 参数,以在模型系数中强制执行不同级别的稀疏性。这里的较高稀疏性被解释为较低的模型复杂性,因为我们需要更少的系数来完全描述它。当然,稀疏性反过来也会影响预测时间,因为稀疏点积所花费的时间大致与非零系数的数量成正比。

en_model_complexity

对于具有非线性核的 sklearn.svm 系列算法,延迟与支持向量的数量相关(越少越快)。在 SVC 或 SVR 模型中,延迟和吞吐量应该(渐近地)随着支持向量的数量线性增长。核也会影响延迟,因为它用于计算每个支持向量一次的输入向量投影。在下图中,NuSVRnu 参数用于影响支持向量的数量。

nusvr_model_complexity

对于树的 sklearn.ensemble(例如 RandomForest、GBT、ExtraTrees 等),树的数量及其深度起着最重要的作用。延迟和吞吐量应该与树的数量成线性关系。在这种情况下,我们直接使用了 GradientBoostingRegressorn_estimators 参数。

gbt_model_complexity

无论如何,请注意,如上所述,降低模型复杂性可能会损害准确性。例如,可以使用快速的线性模型来处理非线性可分问题,但在此过程中,预测能力很可能会受到影响。

8.2.1.6. 特征提取延迟#

大多数 scikit-learn 模型通常都非常快,因为它们是使用编译的 Cython 扩展或优化的计算库实现的。另一方面,在许多现实世界的应用程序中,特征提取过程(即将数据库行或网络数据包等原始数据转换为 numpy 数组)决定了整体预测时间。例如,在路透社文本分类任务中,整个准备工作(读取和解析 SGML 文件、对文本进行标记化并将其散列到一个公共向量空间中)所花费的时间比实际预测代码多 100 到 500 倍,具体取决于所选的模型。

prediction_time

因此,在许多情况下,建议您仔细计时和分析您的特征提取代码,因为当您的整体延迟对于您的应用程序来说太慢时,这可能是一个很好的优化起点。

8.2.2. 预测吞吐量#

在确定生产系统规模时,另一个需要注意的重要指标是吞吐量,即您在给定时间内可以进行的预测数量。以下是 预测延迟 示例中的一个基准测试,该基准测试测量了合成数据上多个估计器的此数量。

throughput_benchmark

这些吞吐量是在单个进程上实现的。提高应用程序吞吐量的一种明显方法是生成共享相同模型的其他实例(通常是 Python 中的进程,因为存在 GIL)。还可以添加机器来分散负载。有关如何实现这一点的详细说明超出了本文档的范围。

8.2.3. 技巧和窍门#

8.2.3.1. 线性代数库#

由于 scikit-learn 严重依赖于 Numpy/Scipy 和线性代数,因此明确关注这些库的版本是有意义的。基本上,您应该确保 Numpy 是使用优化的 BLAS / LAPACK 库构建的。

并非所有模型都能从优化的 BLAS 和 Lapack 实现中受益。例如,基于(随机)决策树的模型通常不会在其内部循环中依赖 BLAS 调用,核 SVM(SVCSVRNuSVCNuSVR)也是如此。另一方面,使用 BLAS DGEMM 调用(通过 numpy.dot)实现的线性模型通常会从经过调整的 BLAS 实现中获益匪浅,并导致比未优化的 BLAS 快几个数量级。

您可以使用以下命令显示 NumPy / SciPy / scikit-learn 安装使用的 BLAS / LAPACK 实现

python -c "import sklearn; sklearn.show_versions()"

优化的 BLAS / LAPACK 实现包括

  • Atlas(需要通过在目标机器上重建来进行硬件特定的调整)

  • OpenBLAS

  • MKL

  • Apple Accelerate 和 vecLib 框架(仅限 OSX)

更多信息可以在 NumPy 安装页面 和 Daniel Nouri 的这篇 博客文章 中找到,其中包含一些针对 Debian / Ubuntu 的详细安装说明。

8.2.3.2. 限制工作内存#

使用标准 numpy 向量化操作实现的一些计算涉及使用大量临时内存。这可能会耗尽系统内存。如果可以在固定内存块中执行计算,我们会尝试这样做,并允许用户使用 set_configconfig_context 提示此工作内存的最大大小(默认为 1GB)。以下建议将临时工作内存限制为 128 MiB

>>> import sklearn
>>> with sklearn.config_context(working_memory=128):
...     pass  # do chunked work here

遵循此设置的分块操作示例是 pairwise_distances_chunked,它有助于计算成对距离矩阵的行式约简。

8.2.3.3. 模型压缩#

scikit-learn 中的模型压缩目前仅涉及线性模型。在这种情况下,这意味着我们要控制模型稀疏性(即模型向量中非零坐标的数量)。通常,将模型稀疏性与稀疏输入数据表示相结合是一个好主意。

以下是说明 sparsify() 方法使用的示例代码

clf = SGDRegressor(penalty='elasticnet', l1_ratio=0.25)
clf.fit(X_train, y_train).sparsify()
clf.predict(X_test)

在这个例子中,我们更喜欢 elasticnet 惩罚,因为它通常是模型紧凑性和预测能力之间的良好折衷。还可以进一步调整 l1_ratio 参数(结合正则化强度 alpha)来控制这种权衡。

对合成数据进行的典型 基准测试 显示,当模型和输入都稀疏时(非零系数比率分别为 0.000024 和 0.027400),延迟降低了 >30%。您的结果可能会有所不同,具体取决于数据和模型的稀疏性和大小。此外,稀疏化对于减少部署在生产服务器上的预测模型的内存使用量非常有用。

8.2.3.4. 模型重塑#

模型重塑包括仅选择一部分可用特征来拟合模型。换句话说,如果模型在学习阶段丢弃了特征,那么我们可以从输入中删除这些特征。这有几个好处。首先,它减少了模型本身的内存(以及时间)开销。它还允许在我们从之前的运行中知道要保留哪些特征后,丢弃管道中的显式特征选择组件。最后,通过不收集和构建模型丢弃的特征,它可以帮助减少数据访问和特征提取层中的处理时间和 I/O 使用。例如,如果原始数据来自数据库,则可以通过编写更简单、更快的查询或通过使查询返回更轻的记录来减少 I/O 使用。目前,在 scikit-learn 中需要手动执行重塑。在稀疏输入的情况下(特别是在 CSR 格式中),通常只需不生成相关特征,将其列留空即可。