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 设置为非空字符串来抑制对有限性的检查,或者使用 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 的使用。
9.2.1.3. 特征数量的影响#
显然,当特征数量增加时,每个示例的内存消耗也会增加。确实,对于一个具有 \(N\) 个特征的 \(M\) 个实例的矩阵,空间复杂度为 \(O(NM)\)。从计算的角度来看,这也意味着基本操作的数量(例如,线性模型中用于向量-矩阵乘法的乘法)也会增加。这是预测延迟随特征数量变化的图表
总的来说,您可以预期预测时间至少随特征数量线性增加(非线性情况可能发生,具体取决于全局内存占用和估计器)。
9.2.1.4. 输入数据表示的影响#
Scipy 提供了稀疏矩阵数据结构,这些结构针对存储稀疏数据进行了优化。稀疏格式的主要特点是您不存储零,因此如果您的数据稀疏,您使用的内存会少得多。稀疏(CSR 或 CSC)表示中的非零值平均只占用一个 32 位整数位置 + 64 位浮点值 + 矩阵中每行或每列的额外 32 位。在密集(或稀疏)线性模型上使用稀疏输入可以大大加快预测速度,因为只有非零值的特征会影响点积,从而影响模型预测。因此,如果您在 1e6 维空间中有 100 个非零值,您只需要 100 次乘法和加法运算,而不是 1e6 次。
然而,在密集表示上的计算可以利用 BLAS 中高度优化的向量操作和多线程,并且倾向于导致更少的 CPU 缓存未命中。因此,稀疏性通常应该相当高(非零值最多为 10%,具体取决于硬件进行检查),以使稀疏输入表示在具有许多 CPU 和优化 BLAS 实现的机器上比密集输入表示更快。
这是用于测试输入稀疏性的示例代码
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、LinearSVC、LogisticRegression...),在预测时应用的决策函数是相同的(点积),因此延迟应该相当。
这是一个使用带有 elasticnet 惩罚的 SGDClassifier 的示例。正则化强度由 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 调用,核 SVMs(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 格式)的情况下,通常只需不生成相关特征,使其列为空即可。







