8.2. 计算性能#
对于某些应用,估计器的性能(主要是预测时的延迟和吞吐量)至关重要。考虑训练吞吐量也可能很有意义,但这在生产环境中通常不太重要(因为训练通常在离线进行)。
我们将在此回顾您在不同情况下可以从许多 scikit-learn 估计器中获得的数量级,并提供一些克服性能瓶颈的技巧。
预测延迟是指进行预测所需的时间(例如,以微秒为单位)。延迟通常被视为一个分布,运维工程师通常关注此分布的给定百分位数的延迟(例如,第 90 百分位数)。
预测吞吐量定义为软件在给定时间内可以交付的预测数量(例如,每秒预测次数)。
性能优化的一个重要方面还在于它可能会损害预测精度。事实上,更简单的模型(例如,线性而不是非线性,或参数更少的模型)通常运行速度更快,但并非总是能够像更复杂的模型那样考虑数据的相同特性。
8.2.1. 预测延迟#
使用/选择机器学习工具包时,人们最直接关注的问题之一是在生产环境中进行预测的延迟。
影响预测延迟的主要因素是:
特征数量
输入数据表示和稀疏性
模型复杂度
特征提取
最后一个主要参数是批量或逐个进行预测的可能性。
8.2.1.1. 批量模式与原子模式#
通常,批量进行预测(同时处理许多实例)由于许多原因(分支可预测性、CPU 缓存、线性代数库优化等)效率更高。在这里,我们看到在一个特征较少的设置中,无论估计器选择如何,批量模式总是更快,对于其中一些估计器,速度提高了 1 到 2 个数量级。
要为您的情况基准测试不同的估计器,您可以简单地更改此示例中的 n_features
参数:Prediction Latency。这应该可以为您提供预测延迟数量级的估计。
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)\)。从计算角度来看,这也意味着基本操作的数量(例如,线性模型中向量-矩阵乘法的乘法)也会增加。以下是预测延迟随特征数量变化的图表
总的来说,您可以预期预测时间至少会随着特征数量线性增加(根据全局内存占用和估计器,可能会出现非线性情况)。
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 的稀疏矩阵格式文档,以了解有关如何构建(或将您的数据转换为)稀疏矩阵格式的更多信息。大多数情况下,CSR
和 CSC
格式效果最佳。
8.2.1.5. 模型复杂度的影响#
一般来说,当模型复杂度增加时,预测能力和延迟都应该增加。提高预测能力通常很有意义,但对于许多应用程序来说,我们最好不要过度增加预测延迟。我们现在将针对不同类型的监督模型回顾这一思想。
对于sklearn.linear_model
(例如 Lasso、ElasticNet、SGDClassifier/Regressor、Ridge & RidgeClassifier、PassiveAggressiveClassifier/Regressor、LinearSVC、LogisticRegression……)在预测时应用的决策函数相同(点积),因此延迟应该相同。
这是一个使用带elasticnet
惩罚的SGDClassifier
的示例。正则化强度由alpha
参数全局控制。使用足够高的alpha
,然后可以增加elasticnet
的l1_ratio
参数以在模型系数中强制执行不同程度的稀疏性。这里的更高稀疏性被解释为更低的模型复杂度,因为我们需要更少的系数来完整地描述它。当然,稀疏性反过来会影响预测时间,因为稀疏点积所需的时间大致与非零系数的数量成正比。
对于使用非线性核的sklearn.svm
算法系列,延迟与支持向量的数量有关(越少越快)。在 SVC 或 SVR 模型中,延迟和吞吐量应该(渐近地)随着支持向量的数量线性增长。内核也会影响延迟,因为它用于计算输入向量的投影(每个支持向量一次)。在下图中,NuSVR
的nu
参数用于影响支持向量的数量。
对于树的sklearn.ensemble
(例如 RandomForest、GBT、ExtraTrees 等),树的数量及其深度起着最重要的作用。延迟和吞吐量应该随着树的数量线性缩放。在这种情况下,我们直接使用了GradientBoostingRegressor
的n_estimators
参数。
无论如何,请注意,如上所述,降低模型复杂度可能会损害准确性。例如,可以使用快速的线性模型处理非线性可分离问题,但预测能力很可能会受到影响。
8.2.1.6. 特征提取延迟#
大多数 scikit-learn 模型通常非常快,因为它们是用编译的 Cython 扩展或优化的计算库实现的。另一方面,在许多现实世界的应用程序中,特征提取过程(即,将原始数据(如数据库行或网络数据包)转换为 numpy 数组)决定了整体预测时间。例如,在路透社文本分类任务中,整个准备工作(读取和解析 SGML 文件、对文本进行标记并将其散列到公共向量空间)花费的时间是实际预测代码的 100 到 500 倍,具体取决于所选择的模型。
因此,在许多情况下,建议仔细计时和分析您的特征提取代码,因为当您的整体延迟对于您的应用程序太慢时,这可能是开始优化的一个好地方。
8.2.2. 预测吞吐量#
在调整生产系统大小时,另一个需要关注的重要指标是吞吐量,即在给定时间内可以进行的预测次数。以下是来自预测延迟示例的基准测试,该测试使用合成数据测量了许多估计器的此数量
这些吞吐量是在单个进程中实现的。提高应用程序吞吐量的一种显而易见的方法是生成额外的实例(通常在Python中是进程,因为存在GIL),这些实例共享相同的模型。也可以添加机器来分担负载。但是,关于如何实现这一点的详细解释超出了本文档的范围。
8.2.3. 技巧与窍门#
8.2.3.1. 线性代数库#
由于scikit-learn很大程度上依赖于Numpy/Scipy和一般的线性代数,因此显式地注意这些库的版本是有意义的。基本上,您应该确保Numpy是使用优化的BLAS / LAPACK 库构建的。
并非所有模型都能从优化的BLAS和Lapack实现中受益。例如,基于(随机)决策树的模型通常在其内部循环中不依赖于BLAS调用,内核SVM(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的不错的分步安装说明。
8.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
,它有助于计算成对距离矩阵的行缩减。
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
格式)的情况下,通常只需不生成相关特征,让它们的列为空即可。