超参数优化是推断无法从数据中学习的模型参数的过程。这个过程通常耗时且耗资源,尤其在深度学习领域。关于此过程的良好描述可以在“调整估算器的超参数”中找到,以及出现的问题在 Dask-ML 的“超参数搜索”文档中有简洁的总结。
有许多库和框架可以解决这个问题。Scikit-Learn 的模块 已被 Dask-ML 和auto-sklearn 效仿,它们都提供了先进的超参数优化技术。其他不遵循 Scikit-Learn 接口的实现包括 Ray Tune、AutoML 和 Optuna。
Ray 最近提供了一个 Ray Tune 的包装器,该包装器模仿了 Scikit-Learn API,名为 tune-sklearn(文档、源码)。该库的介绍中提到以下内容
前沿的超参数调优技术(贝叶斯优化、早期停止、分布式执行)相比网格搜索和随机搜索可以显著加速。
然而,机器学习生态系统中缺乏一种解决方案,既能让用户利用这些新算法,又能让他们留在 Scikit-Learn API 中。在这篇博客文章中,我们介绍了 tune-sklearn [Ray 的调优库] 来弥合这一差距。Tune-sklearn 是 Scikit-Learn 模型选择模块的直接替代品,具有最先进的优化功能。
这个说法不准确:一年多以来,Dask-ML 一直通过 Scikit-Learn 兼容的 API 提供对“前沿超参数调优技术”的访问。为了纠正他们的说法,让我们看看 Ray 的 tune-sklearn 提供的每项功能,并与 Dask-ML 进行比较
以下是 [Ray 的] tune-sklearn 所提供的:
[Ray 的] Tune-sklearn 也很快。
Dask-ML 的模型选择模块具备所有这些功能
Dask-ML 也很快。在“速度”中,我们展示了 Dask-ML、Ray 和 Scikit-Learn 之间的基准测试
只有解决时间是相关的;所有这些方法产生的模型分数都相似。详情请见“速度”。
现在,让我们详细介绍如何使用 Dask-ML 来获得上述 5 项功能。
Dask-ML 与 Scikit-Learn API 保持一致。
以下是如何使用 Scikit-Learn、Dask-ML 和 Ray 的 tune-sklearn 进行超参数优化
## 精简示例;详情请参阅附录
from sklearn.model_selection import RandomizedSearchCV
search = RandomizedSearchCV(model, params, ...)
search.fit(X, y)
from dask_ml.model_selection import HyperbandSearchCV
search = HyperbandSearchCV(model, params, ...)
search.fit(X, y, classes=[0, 1])
from tune_sklearn import TuneSearchCV
search = TuneSearchCV(model, params, ...)
search.fit(X, y, classes=[0, 1])
model 和 params 的定义遵循正常的 Scikit-Learn 定义,详情请参阅附录。
显然,Dask-ML 和 Ray 的 tune-sklearn 都兼容 Scikit-Learn。现在我们来看看每种搜索的表现和配置方式。
Dask-ML 在 Scikit-Learn 接口中提供最先进的超参数调优技术。
Ray 的 tune-sklearn 的介绍提出了这个说法
tune-sklearn 是唯一允许您通过简单地切换几个参数即可轻松利用贝叶斯优化、HyperBand 和其他优化技术的 Scikit-Learn 接口。
超参数优化的最先进技术目前是“Hyperband”。Hyperband 通过一种原则性的早期停止方案减少了所需的计算量;除此之外,它与 Scikit-Learn 流行的 RandomizedSearchCV 相同。
Hyperband 有效。因此,它非常流行。自 2016 年 Li 等人引入 Hyperband 以来,该论文已被引用超过 470 次,并已在许多不同的库中实现,包括 Dask-ML、Ray Tune、keras-tune、Optuna、AutoML1 以及 微软的 NNI。原始论文显示,相比所有相关实现,Hyperband 有相当显著的改进2,并且这种显著改进在后续工作中也持续存在3。以下是 Hyperband 的一些说明性结果
除了“random2x”执行两倍的工作量外,所有算法都配置为执行相同的工作量。“hyperband (finite)”类似于 Dask-ML 的默认实现,而“bracket s=4”类似于 Ray 的默认实现。“random”是随机搜索。SMAC4、spearmint5 和 TPE6 是流行的贝叶斯算法。
Hyperband 毫无疑问是一种“前沿”超参数优化技术。Dask-ML 和 Ray 都提供了该算法的 Scikit-Learn 实现,它们依赖于相似的实现方式,Dask-ML 的实现还有一个配置经验法则。Dask-ML 和 Ray 的文档都鼓励使用 Hyperband。
Ray 确实支持在名为贝叶斯抽样的技术之上使用其 Hyperband 实现。这改变了模型初始化的超参数抽样方案。这可以与 Hyperband 的早期停止方案结合使用。将此选项添加到 Dask-ML 的 Hyperband 实现是 Dask-ML 未来的工作。
Dask-ML 模型选择支持许多库,包括 Scikit-Learn、PyTorch、Keras、LightGBM 和 XGBoost。
Ray 的 tune-sklearn 支持这些框架
tune-sklearn 主要用于调优 Scikit-Learn 模型,但它也支持并提供了许多其他具有 Scikit-Learn 包装器的框架的示例,例如 Skorch (Pytorch)、KerasClassifiers (Keras) 和 XGBoostClassifiers (XGBoost)。
显然,Dask-ML 和 Ray 都支持许多相同的库。
然而,Dask-ML 和 Ray 都有一些限制。某些库不提供 partial_fit 的实现7,因此并非所有现代超参数优化技术都能提供。下面是比较不同库及其在 Dask-ML 模型选择和 Ray 的 tune-sklearn 中的支持情况的表格
模型库 Dask-ML 支持 Ray 支持 Dask-ML: 早期停止? Ray: 早期停止? Scikit-Learn ✔ ✔ ✔* ✔* PyTorch (通过 Skorch) ✔ ✔ ✔ ✔ Keras (通过 SciKeras) ✔ ✔ ✔** ✔** LightGBM ✔ ✔ ❌ ❌ XGBoost ✔ ✔ ❌ ❌
* 仅适用于实现 partial_fit 的模型。
** 感谢 Dask 开发者围绕scikeras#24所做的工作。
以此衡量,Dask-ML 和 Ray 模型选择在框架支持方面具有相同的水平。当然,Dask 通过 Dask-ML 的 xgboost 模块 和 dask-lightgbm 与 LightGBM 和 XGBoost 有间接集成。
Dask-ML 支持分布式调优(怎么可能不支持?),也就是在多机/多核上的并行化。此外,它还支持大于内存的数据。
[Ray 的] Tune-sklearn 利用 Ray Tune(一个用于分布式超参数调优的库)来高效且透明地在多核甚至多机上并行化交叉验证。
自然地,Dask-ML 也能扩展到多核/多机,因为它依赖于 Dask。Dask 对不同的部署选项有广泛支持,范围从您的个人电脑到超级计算机。Dask 极有可能在您可用的任何计算系统上工作,包括 Kubernetes、SLURM、YARN 和 Hadoop 集群,以及您的个人电脑。
Dask-ML 的模型选择也能扩展到大于内存的数据集,并经过了彻底测试。Ray 中对大于内存数据的支持未经测试,并且没有详细说明如何在 PyTorch/Keras 中使用 Ray Tune 与分布式数据集实现的示例。
此外,我还对 Dask-ML 的模型选择模块进行了基准测试,以查看 Dask Worker 数量如何影响解决时间,详见“使用 Dask 进行更好更快的超参数优化”。也就是说,达到特定精度所需的时间如何随 Worker 数量 $P$ 扩展?起初,它会像 $1/P$ 那样扩展,但当 Worker 数量很大时,串行部分将根据阿姆达尔定律决定解决时间。简而言之,我发现 Dask-ML 的 HyperbandSearchCV 在特定搜索中,加速效果在大约 24 个 Worker 时开始饱和。
Dask-ML 和 Ray 都比 Scikit-Learn 快得多。
Ray 的 tune-sklearn 在介绍中运行了一些基准测试,使用了 Scikit-Learn 和 Dask-ML 中的 GridSearchCV 类。一个更公平的基准测试应该使用 Dask-ML 的 HyperbandSearchCV,因为它与 Ray 的 tune-sklearn 中的算法几乎相同。具体来说,我感兴趣的是比较这些方法
每次搜索都配置为执行相同的任务:抽样 100 个参数并训练不超过 100 个“周期”或数据遍历。8 每个估算器都按照其相应文档的建议进行配置。每次搜索使用 8 个 Worker 和一个交叉验证分割,partial_fit 调用处理 50,000 个示例需要一秒钟。完整的设置可以在附录中找到。
以下是每个库完成相同搜索所需的时间
值得注意的是,我们没有为了这次基准测试改进 Dask-ML 代码库,而是运行了过去一年中的代码。9 尽管如此,有偏见的基准测试中的其他因素也可能悄悄进入了这次基准测试。
显然,与 Scikit-Learn 相比,Ray 和 Dask-ML 在 8 个 Worker 下提供了相似的性能。值得称赞的是,Ray 的实现在 8 个 Worker 下比 Dask-ML 快约 15%。我们怀疑这种性能提升来自于 Ray 实现了 Hyperband 的异步变体。我们应该调查 Dask 和 Ray 之间的这种差异,以及它们如何平衡权衡,即 FLOPs 数量与解决时间。这会随 Worker 数量而变化:如果只使用单个 Worker,Hyperband 的异步变体不会带来任何好处。
Dask-ML 在串行环境或 Worker 数量较少时能快速达到分数。Dask-ML 优先拟合高分模型:如果需要拟合 100 个模型而只有 4 个 Worker 可用,Dask-ML 会选择得分最高的模型。这在串行环境中最为相关10;请参阅“使用 Dask 进行更好更快的超参数优化”获取基准测试。此功能在此基准测试中被省略,该测试仅关注解决时间。
Dask-ML 和 Ray 在模型选择方面提供相同的功能:具有 Scikit-Learn 兼容 API 的最先进功能,并且两种实现都对不同框架有相当广泛的支持,并依赖于可以扩展到许多机器的后端。
此外,Ray 的实现为进一步开发提供了动力,特别是在以下方面
Ray 的实现也帮助激发和明确了未来的工作。Dask-ML 应该包含以下实现
幸运的是,所有这些开发工作都是直接的修改,因为 Dask-ML 模型选择框架相当灵活。
感谢 Tom Augspurger、Matthew Rocklin、Julia Signell 和 BenjaminZaitlen 提供的反馈、建议和编辑。
这是 Dask-ML、Scikit-Learn 和 Ray 之间基准测试的完整设置。完整详情可在stsievert/dask-hyperband-comparison 找到。
我们创建一个虚拟模型,partial_fit 调用处理 50,000 个示例需要 1 秒钟。这适用于本次基准测试;我们只关注完成搜索所需的时间,而不是模型表现的好坏。Scikit-learn、Ray 和 Dask-ML 在选择要评估的超参数方面方法非常相似;它们的区别在于早期停止技术。
from scipy.stats import uniform
from sklearn.model_selection import make_classification
from benchmark import ConstantFunction # custom module
# 该模型会先休眠 \`latency * len(X)\` 秒
# 然后报告分数为 \`value\`。
model = ConstantFunction(latency=1 / 50e3, max_iter=max_iter)
params = {"value": uniform(0, 1)}
# 这个模拟数据集模仿了 MNIST 数据集
X_train, y_train = make_classification(n_samples=int(60e3), n_features=784)
此模型训练 100 个周期(即数据遍历 100 次)需要 2 分钟。详细信息可以在 stsievert/dask-hyperband-comparison 找到。
我们将搜索配置为使用 8 个 Worker 和一个交叉验证分割
from sklearn.model_selection import RandomizedSearchCV, ShuffleSplit
split = ShuffleSplit(test_size=0.2, n_splits=1)
kwargs = dict(cv=split, refit=False)
search = RandomizedSearchCV(model, params, n_jobs=8, n_iter=n_params, **kwargs)
search.fit(X_train, y_train) # 20.88 分钟
from dask_ml.model_selection import HyperbandSearchCV
dask_search = HyperbandSearchCV(
model, params, test_size=0.2, max_iter=max_iter, aggressiveness=4
)
from tune_sklearn import TuneSearchCV
ray_search = TuneSearchCV(
model, params, n_iter=n_params, max_iters=max_iter, early_stopping=True, **kwargs
)
dask_search.fit(X_train, y_train) # 2.93 分钟
ray_search.fit(X_train, y_train) # 2.49 分钟
from sklearn.linear_model import SGDClassifier
from scipy.stats import uniform, loguniform
from sklearn.datasets import make_classification
model = SGDClassifier()
params = {"alpha": loguniform(1e-5, 1e-3), "l1_ratio": uniform(0, 1)}
X, y = make_classification()
from sklearn.model_selection import RandomizedSearchCV
search = RandomizedSearchCV(model, params, ...)
search.fit(X, y)
from dask_ml.model_selection import HyperbandSearchCV
HyperbandSearchCV(model, params, ...)
search.fit(X, y, classes=[0, 1])
from tune_sklearn import TuneSearchCV
search = TuneSearchCV(model, params, ...)
search.fit(X, y, classes=[0, 1])