9.2. 计算性能#
对于某些应用,估计器的性能(主要是在预测时的延迟和吞吐量)至关重要。考虑训练吞吐量也可能很有趣,但在生产环境中(通常离线进行),这通常不那么重要。
我们将在此处回顾在不同情况下,您可以从多个 scikit-learn 估计器中预期的数量级,并提供一些克服性能瓶颈的提示和技巧。
预测延迟衡量的是进行一次预测所需的耗费时间(例如,以微秒为单位)。延迟通常被视为一种分布,运维工程师通常关注此分布中给定百分位数的延迟(例如,第90百分位数)。
预测吞吐量定义为软件在给定时间内可以提供的预测数量(例如,每秒预测数)。
性能优化的一个重要方面是它可能会损害预测准确性。实际上,更简单的模型(例如线性模型而非非线性模型,或参数较少的模型)通常运行更快,但它们并非总能像更复杂的模型那样,考虑到数据的相同精确属性。
9.2.1. 预测延迟#
在使用/选择机器学习工具包时,最直接的关注点之一就是在生产环境中进行预测的延迟。
影响预测延迟的主要因素有
特征数量
输入数据表示和稀疏性
模型复杂度
特征提取
最后一个主要参数是进行批量预测或逐个预测的可能性。
9.2.1.1. 批量模式与原子模式#
通常,批量进行预测(同时处理多个实例)会更有效率,原因有很多(分支可预测性、CPU 缓存、线性代数库优化等)。在此,我们看到在特征较少的情况下,无论估计器选择如何,批量模式总是更快,对于其中一些估计器而言,其速度快了 1 到 2 个数量级。
要针对您的情况对不同的估计器进行基准测试,您只需在此示例中更改 n_features
参数即可:预测延迟。这应该能让您估计预测延迟的数量级。
9.2.1.2. 配置 Scikit-learn 以减少验证开销#
Scikit-learn 会对数据进行一些验证,这会增加每次调用 predict
和类似函数时的开销。特别是,检查特征是否有限(非 NaN 或无穷大)涉及对数据进行完整遍历。如果您确保数据是可接受的,可以在导入 scikit-learn 之前将环境变量 SKLEARN_ASSUME_FINITE
设置为非空字符串,或者在 Python 中使用 set_config
进行配置,以取消有限性检查。要获得比这些全局设置更多的控制,config_context
允许您在指定上下文中设置此配置。
>>> import sklearn
>>> with sklearn.config_context(assume_finite=True):
... pass # do learning/prediction here with reduced validation
请注意,这会影响上下文中所有 assert_all_finite
的使用。
9.2.1.3. 特征数量的影响#
显然,当特征数量增加时,每个样本的内存消耗也会增加。实际上,对于一个具有 \(N\) 个特征的 \(M\) 个实例的矩阵,其空间复杂度为 \(O(NM)\)。从计算角度来看,这也意味着基本操作的数量(例如,线性模型中向量-矩阵乘法的乘法操作)也会增加。以下是预测延迟随特征数量变化的图表
总的来说,您可以预期预测时间至少会随特征数量线性增加(非线性情况可能根据全局内存占用和估计器而发生)。
9.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 的稀疏矩阵格式文档。大多数情况下,CSR
和 CSC
格式效果最佳。
9.2.1.5. 模型复杂度的影响#
一般来说,当模型复杂度增加时,预测能力和延迟也会随之增加。提高预测能力通常很有趣,但对于许多应用程序而言,我们最好不要过多地增加预测延迟。现在,我们将针对不同系列的监督模型来回顾这一观点。
对于 sklearn.linear_model
(例如 Lasso、ElasticNet、SGDClassifier/Regressor、Ridge 和 RidgeClassifier、PassiveAggressiveClassifier/Regressor、LinearSVC、LogisticRegression 等),在预测时应用的决策函数是相同的(点积),因此延迟应该相当。
这里是一个使用 SGDClassifier
并带有 elasticnet
惩罚项的例子。正则化强度由 alpha
参数全局控制。当 alpha
足够高时,可以通过增加 elasticnet
的 l1_ratio
参数,在模型系数中强制实现不同程度的稀疏性。在此,更高的稀疏性被解释为模型复杂性较低,因为我们只需要更少的系数来完整描述它。当然,稀疏性反过来会影响预测时间,因为稀疏点积所需的时间大致与非零系数的数量成正比。
对于带有非线性核函数的 sklearn.svm
算法族,延迟与支持向量的数量相关(支持向量越少,速度越快)。SVC 或 SVR 模型中的延迟和吞吐量应(渐近地)随支持向量的数量线性增长。核函数也会影响延迟,因为它用于对每个支持向量的输入向量进行一次投影计算。在下图中,NuSVR
的 nu
参数被用来影响支持向量的数量。
对于树的 sklearn.ensemble
(例如 RandomForest、GBT、ExtraTrees 等),树的数量和深度起着最重要的作用。延迟和吞吐量应与树的数量线性相关。在本例中,我们直接使用了 GradientBoostingRegressor
的 n_estimators
参数。
无论如何,请注意,如上所述,降低模型复杂度可能会损害准确性。例如,一个非线性可分的问题可以用快速的线性模型来处理,但在处理过程中,预测能力很可能会受到影响。
9.2.1.6. 特征提取延迟#
大多数 scikit-learn 模型通常都非常快,因为它们是使用编译的 Cython 扩展或优化的计算库实现的。另一方面,在许多实际应用中,特征提取过程(即将数据库行或网络数据包等原始数据转换为 numpy 数组)决定了整体预测时间。例如,在路透社文本分类任务中,整个准备工作(读取和解析 SGML 文件、对文本进行分词并将其哈希到公共向量空间)所需的时间比实际预测代码多 100 到 500 倍,具体取决于所选模型。
因此,在许多情况下,建议仔细测量和分析您的特征提取代码,因为当您的整体延迟对于应用程序来说太慢时,这可能是一个很好的优化起点。
9.2.2. 预测吞吐量#
在规划生产系统规模时,另一个需要关注的重要指标是吞吐量,即在给定时间内可以进行的预测数量。这里是来自预测延迟示例的一个基准测试,它测量了合成数据上多个估计器的此数量
这些吞吐量是在单个进程上实现的。提高应用程序吞吐量的一个显而易见的方法是生成共享同一模型的额外实例(在 Python 中通常是进程,因为存在 GIL)。也可以增加机器来分担负载。然而,关于如何实现这一点的详细解释超出了本文档的范围。
9.2.3. 提示与技巧#
9.2.3.1. 线性代数库#
由于 scikit-learn 大量依赖 Numpy/Scipy 和一般的线性代数,因此明确关注这些库的版本是有意义的。基本上,您应该确保 Numpy 是使用优化的 BLAS / LAPACK 库构建的。
并非所有模型都能从优化的 BLAS 和 Lapack 实现中受益。例如,基于(随机)决策树的模型通常不在其内部循环中依赖 BLAS 调用,核支持向量机(SVC
、SVR
、NuSVC
、NuSVR
)也一样。另一方面,通过 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 的详细安装说明。
9.2.3.2. 限制工作内存#
一些使用标准 numpy 向量化操作实现的计算会涉及大量临时内存的使用。这可能会耗尽系统内存。如果计算可以分固定内存块进行,我们会尝试这样做,并允许用户使用 set_config
或 config_context
来提示此工作内存的最大大小(默认为 1GB)。以下建议将临时工作内存限制为 128 MiB
>>> import sklearn
>>> with sklearn.config_context(working_memory=128):
... pass # do chunked work here
遵循此设置的分块操作的一个例子是 pairwise_distances_chunked
,它有助于计算成对距离矩阵的行向归约。
9.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%。您的实际效果可能因数据和模型的稀疏性及大小而异。此外,稀疏化对于减少部署在生产服务器上的预测模型的内存使用非常有用。
9.2.3.4. 模型重塑#
模型重塑是指仅选择可用特征的一部分来拟合模型。换句话说,如果模型在学习阶段丢弃了某些特征,我们就可以将这些特征从输入中去除。这有几个好处。首先,它减少了模型本身的内存(从而时间)开销。其次,一旦我们知道上次运行中要保留哪些特征,就可以在管道中丢弃显式的特征选择组件。最后,通过不收集和构建被模型丢弃的特征,它有助于减少数据访问和特征提取层上游的处理时间及 I/O 使用。例如,如果原始数据来自数据库,可以通过编写更简单、更快的查询或让查询返回更轻量级的记录来减少 I/O 使用。目前,scikit-learn 中的重塑需要手动执行。对于稀疏输入(特别是 CSR
格式),通常只需不生成相关特征,将其列留空即可。