TLDR; 超参数优化功能可用,但在 cuML 中速度较慢
cuML 是一个开源的 GPU 加速机器学习库,主要由 NVIDIA 开发,它模仿了 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 版本中,估计器是可序列化的,并且可以在 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)=
上述运行的单次计时测量结果为
但数据相当小,约 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)
新的计时测量结果为
数据量越大,拟合时间明显越快,但将数据移动到 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。
补全缺失之处后,我们再次进行了测试。使用相同的 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)在数据量增加时性能显著变差!为什么?主要有两个原因
下面是 GridSearch 的 Dask 图
有 50 个(cv=5 乘以 alpha 的 10 个参数)对测试数据集进行分块并评估性能的实例。这意味着我们将数据在主机和设备之间来回移动 50 次用于拟合,以及 50 次用于评分。这不太好,但也是可以解决的——为 GPU 构建评分函数!
我们知道问题所在,已提交 GitHub 问题,并且正在解决这些问题——欢迎来帮忙!