cuML 和 Dask 超参数优化 超参数优化和 dask
作者:Benjamin Zaitlen
设置
- DGX-1 工作站
- 主机内存:512 GB
- GPU Tesla V100 x 8
- cudf 0.6
- cuml 0.6
- dask 1.1.4
- Jupyter notebook
太长不看:超参数优化功能可用,但在 cuML 中速度较慢
cuML 和 Dask 超参数优化
cuML 是一个主要由英伟达开发的开源 GPU 加速机器学习库,它模仿了 Scikit-Learn API。当前的算法套件包括 GLM、卡尔曼滤波、聚类和降维。这些机器学习算法中有许多使用超参数。这些是在模型训练过程中使用的参数,但在训练期间不会被“学习”。通常这些参数是系数或惩罚阈值,找到“最佳”超参数可能会耗费大量计算资源。在 PyData 社区中,我们通常使用 Scikit-Learn 的 GridSearchCV 或 RandomizedSearchCV 来方便地定义超参数的搜索空间——这称为超参数优化。在 Dask 社区中,Dask-ML 通过利用 Scikit-Learn 和 Dask 来使用多核和分布式调度器,逐步提高了超参数优化的效率:使用 DaskML 的 Grid 和 RandomizedSearch。
借助新创建的 Scikit-Learn 的直接替代品 cuML,我们对 Dask 的 GridSearchCV 进行了实验。在即将发布的 cuML 0.6 版本中,估计器(estimators)是可序列化的,并且在 Scikit-Learn/dask-ml 框架内功能可用,但与 Scikit-Learn 估计器相比速度较慢。尽管目前速度较慢,但我们知道如何提升性能,已经提交了一些问题,并希望在未来的版本中展示性能提升。
所有代码和时间测量都可以在此 Jupyter notebook 中找到
快速拟合!
cuML 速度很快!但要达到这种速度,需要具备一些 GPU 知识和直觉。例如,将数据从设备移动到 GPU 会产生非零成本,而且当数据“小”时,性能提升很小甚至没有。“小”,目前可能意味着小于 100MB。
在下面的示例中,我们使用由 sklearn 提供的 diabetes data 数据集,并使用 RidgeRegression 对数据进行线性拟合
\[\min\limits_w ||y - Xw||^2_2 + alpha \* ||w||^2_2\]alpha 是超参数,我们最初将其设置为 1。
import numpy as np
from cuml import Ridge as cumlRidge
import dask_ml.model_selection as dcv
from sklearn import datasets, linear_model
from sklearn.externals.joblib import parallel_backend
from sklearn.model_selection import train_test_split, GridSearchCV
X_train, X_test, y_train, y_test = train_test_split(diabetes.data, diabetes.target, test_size=0.2)
fit_intercept = True
normalize = False
alpha = np.array([1.0])
ridge = linear_model.Ridge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver='cholesky')
cu_ridge = cumlRidge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver="eig")
ridge.fit(X_train, y_train)
cu_ridge.fit(X_train, y_train)=
以上运行的单次时间测量结果为
- Scikit-Learn Ridge: 28 ms
- cuML Ridge: 1.12 s
但数据量相当小,约 28KB。将数据量增加到约 2.8GB 并重新运行,我们看到了显著的提升
dup_ridge = linear_model.Ridge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver='cholesky')
dup_cu_ridge = cumlRidge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver="eig")
# move data from host to device
record_data = (('fea%d'%i, dup_data[:,i]) for i in range(dup_data.shape[1]))
gdf_data = cudf.DataFrame(record_data)
gdf_train = cudf.DataFrame(dict(train=dup_train))
#sklearn
dup_ridge.fit(dup_data, dup_train)
# cuml
dup_cu_ridge.fit(gdf_data, gdf_train.train)
新的时间测量结果为
- Scikit-Learn Ridge: 4.82 s ± 694 ms
- cuML Ridge: 450 ms ± 47.6 ms
数据量越大,我们显然看到拟合时间越快,但将数据移动到 GPU(通过 CUDF)的时间是 19.7 秒。这种数据移动的成本是开发 RAPIDS/cuDF 的原因之一——将数据保留在 GPU 上,避免来回移动。
超参数优化实验
因此,移动到 GPU 可能成本高昂,但一旦数据到达 GPU,在处理更大规模数据时,我们会获得显著的性能优化。天真地,我们曾想:“好吧,我们有 GPU 机器学习,我们有分布式超参数优化……我们应该拥有分布式的、GPU 加速的超参数优化!”
Scikit-Learn 假设了一个特定但定义良好的估计器 API,它将在其上执行超参数优化。Scikit-Learn 中的大多数估计器/分类器如下所示
class DummyEstimator(BaseEstimator):
def __init__(self, params=...):
...
def fit(self, X, y=None):
...
def predict(self, X):
...
def score(self, X, y=None):
...
def get_params(self):
...
def set_params(self, params...):
...
当我们开始尝试超参数优化时,发现缺少一些 API 接口,这些问题已被解决,主要处理了匹配参数结构和各种 getter/setter。
- get_params 和 set_params (#271)
- 修复/clf-solver (#318)
- 将 fit_transform 映射到 sklearn 实现 (#330)
- 特性 获取参数的微小更改 (#322)
填补了这些空白后,我们再次进行了测试。使用相同的 diabetes 数据集,我们现在正在执行超参数优化,并在许多 alpha 参数中搜索得分最好的 scoring alpha。
params = {'alpha': np.logspace(-3, -1, 10)}
clf = linear_model.Ridge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver='cholesky')
cu_clf = cumlRidge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver="eig")
grid = GridSearchCV(clf, params, scoring='r2')
grid.fit(X_train, y_train)
cu_grid = GridSearchCV(cu_clf, params, scoring='r2')
cu_grid.fit(X_train, y_train)
再次提醒自己,数据量很小,约 28KB,我们不期望看到 cuML 比 sklearn 表现更快。相反,我们希望展示其功能可用性。
再次提醒自己,数据量很小,约 28KB,我们不期望看到 cuML 比 Scikit-Learn 表现更快。相反,我们希望展示其功能可用性。此外,我们还尝试替换 Dask-ML 的 GridSearchCV
实现(它遵循与 Scikit-Learn 相同的 API),以并行使用我们所有的可用 GPU。
params = {'alpha': np.logspace(-3, -1, 10)}
clf = linear_model.Ridge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver='cholesky')
cu_clf = cumlRidge(alpha=alpha, fit_intercept=fit_intercept, normalize=normalize, solver="eig")
grid = dcv.GridSearchCV(clf, params, scoring='r2')
grid.fit(X_train, y_train)
cu_grid = dcv.GridSearchCV(cu_clf, params, scoring='r2')
cu_grid.fit(X_train, y_train)
时间测量
GridSearchCV | sklearn-Ridge | cuml-ridge |
---|---|---|
Scikit-Learn | 88.4 ms ± 6.11 ms | 6.51 s ± 132 ms |
Dask-ML | 873 ms ± 347 ms | 740 ms ± 142 ms |
不出所料,我们看到在这种情况下,Scikit-Learn 的 GridSearchCV 和 Ridge Regression 是最快的。分配工作和数据是有成本的,正如我们之前提到的,还有将数据从主机移动到设备。
随着数据规模扩大,性能如何变化?
two_dup_data = np.array(np.vstack([X_train]*int(1e2)))
two_dup_train = np.array(np.hstack([y_train]*int(1e2)))
three_dup_data = np.array(np.vstack([X_train]*int(1e3)))
three_dup_train = np.array(np.hstack([y_train]*int(1e3)))
cu_grid = dcv.GridSearchCV(cu_clf, params, scoring='r2')
cu_grid.fit(two_dup_data, two_dup_train)
cu_grid = dcv.GridSearchCV(cu_clf, params, scoring='r2')
cu_grid.fit(three_dup_data, three_dup_train)
grid = dcv.GridSearchCV(clf, params, scoring='r2')
grid.fit(three_dup_data, three_dup_train)
时间测量
数据 (MB) | cuML+Dask-ML | sklearn+Dask-ML |
---|---|---|
2.8 MB | 13.8s | |
28 MB | 1min 17s | 4.87 s |
随着数据量增加,cuML + dask-ml(分布式 GridSearchCV)的性能显著下降!为什么?主要有两个原因
- 主机和设备之间未经优化的数据移动,以及 N 个设备和参数空间大小的叠加影响
- 评分方法未在 cuML 中实现
下面是 GridSearch 的 Dask 图
有 50 个(cv=5 乘以 alpha 的 10 个参数)分割测试数据集和计算评分性能的实例。这意味着我们需要在主机和设备之间来回移动数据 50 次进行拟合,并移动 50 次进行评分。这并不理想,但也是完全可以解决的——为 GPU 构建评分函数!
近期工作计划
我们知道问题所在,GitHub 问题(GH Issues)已经提交,并且我们正在解决这些问题——欢迎来帮忙!
博客评论由 Disqus 提供支持