提交新活动

谢谢!您的提交已收到!
糟糕!提交表单时出错了。

提交新闻报道

谢谢!您的提交已收到!
糟糕!提交表单时出错了。

订阅新闻简报

谢谢!您的提交已收到!
糟糕!提交表单时出错了。
2019年3月27日

cuML 和 Dask 的超参数优化

作者

环境配置

  • DGX-1 工作站
  • 主机内存:512 GB
  • GPU Tesla V100 x 8
  • cudf 0.6
  • cuml 0.6
  • dask 1.1.4
  • Jupyter Notebook

TLDR; 超参数优化功能可用,但在 cuML 中速度较慢

cuML 和 Dask 的超参数优化

cuML 是一个开源的 GPU 加速机器学习库,主要由 NVIDIA 开发,它模仿了 Scikit-Learn API。当前的算法套件包括 GLM、卡尔曼滤波、聚类和降维。许多机器学习算法使用超参数。这些是在模型训练过程中使用的参数,但在训练期间不会“学习”。通常这些参数是系数或惩罚阈值,找到“最佳”超参数的计算成本可能很高。在 PyData 社区中,我们经常使用 Scikit-Learn 的 GridSearchCVRandomizedSearchCV,以便轻松定义超参数的搜索空间——这称为超参数优化。在 Dask 社区中,Dask-ML 通过利用 Scikit-Learn 和 Dask 来使用多核和分布式调度程序,逐步提高了超参数优化的效率:DaskML 的 Grid 和 RandomizedSearch

借助新创建的 Scikit-Learn 替代品 cuML,我们尝试了 Dask 的 GridSearchCV。在即将发布的 cuML 0.6 版本中,估计器是可序列化的,并且可以在 Scikit-Learn/dask-ml 框架内使用,但与 Scikit-Learn 估计器相比速度较慢。尽管目前速度较慢,但我们知道如何提升性能,已经提交了几个问题,并希望在未来的版本中展示性能提升。

所有代码和计时测量结果都可以在这个 Jupyter Notebook 中找到

快速拟合!

cuML 速度很快!但要达到这种速度需要一些 GPU 知识和直觉。例如,将数据从设备移动到 GPU 会产生非零成本,并且当数据“小”时,性能提升很小甚至没有。“小”目前可能意味着小于 100MB。

在下面的示例中,我们使用 sklearn 提供的 diabetes 数据集,并使用 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,它将基于该 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)
  • fix/clf-solver (#318)
  • 将 fit_transform 映射到 sklearn 实现 (#330)
  • Fea get params 小改动 (#322)

补全缺失之处后,我们再次进行了测试。使用相同的 diabetes 数据集,我们现在正在进行超参数优化,并搜索许多 alpha 参数以找到最佳得分的 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)

计时测量结果

Data (MB) cuML+Dask-ML sklearn+Dask-ML 2.8 MB 13.8s 28 MB 1分 17秒 4.87 s

cuML + dask-ml(分布式 GridSearchCV)在数据量增加时性能显著变差!为什么?主要有两个原因

  1. 主机与设备之间数据移动未优化,加上 N 个设备和参数空间大小的复合影响
  2. cuML 中未实现评分方法

下面是 GridSearch 的 Dask 图

有 50 个(cv=5 乘以 alpha 的 10 个参数)对测试数据集进行分块并评估性能的实例。这意味着我们将数据在主机和设备之间来回移动 50 次用于拟合,以及 50 次用于评分。这不太好,但也是可以解决的——为 GPU 构建评分函数!

近期未来工作

我们知道问题所在,已提交 GitHub 问题,并且正在解决这些问题——欢迎来帮忙!

  • 内置评分器 (#242)
  • DeviceNDArray 作为输入数据 (#369)
  • 与 UCX 通信 (#2344)