超参数搜索
内容
超参数搜索¶
使用Dask执行Scikit-Learn API兼容模型的超参数优化的工具,并扩展超参数优化到 更大的数据和/或更大的搜索。
超参数搜索是机器学习中的一个必要过程。简而言之,机器学习模型需要某些“超参数”,这些是模型参数可以从数据中学习。找到这些参数的良好值称为“超参数搜索”或“超参数优化”。更多详情,请参阅“调整估计器的超参数.”
这些搜索可能需要大量时间(几天或几周),特别是在追求良好性能和/或处理大规模数据集时,这在准备生产或论文发表时很常见。以下部分阐明了可能出现的问题:
“缩放超参数搜索” 提到了在超参数优化搜索中经常出现的问题。
解决这些问题的工具在这些部分中进行了扩展:
“Scikit-Learn 的即插即用替代品” 详细介绍了那些与 Scikit-learn 估计器相对应的类,但它们与 Dask 对象配合良好,并且可以提供更好的性能。
“增量超参数优化” 详细介绍了适用于大型数据集的类。
“自适应超参数优化” 详细介绍了避免额外计算并更快找到高性能超参数的类。
缩放超参数搜索¶
Dask-ML 提供了类来避免超参数优化中最常见的两个问题,当超参数搜索是…
内存受限。当数据集大小过大而无法装入内存时,这种情况就会发生。这通常发生在模型需要在本地开发后针对大于内存的数据集进行调优时。
计算受限。这种情况发生在即使数据可以放入内存,计算时间也过长时。这通常发生在需要调整许多超参数或模型需要专用硬件(例如,GPU)时。
当数据无法完全装入单台机器的内存时,会发生“内存受限”搜索:
>>> import pandas as pd
>>> import dask.dataframe as dd
>>>
>>> ## not memory constrained
>>> df = pd.read_csv("data/0.parquet")
>>> df.shape
(30000, 200) # => 23MB
>>>
>>> ## memory constrained
>>> # Read 1000 of the above dataframes (=> 22GB of data)
>>> ddf = dd.read_parquet("data/*.parquet")
“计算受限”是指即使数据适合内存,超参数搜索花费的时间也过长。可能有很多超参数需要搜索,或者模型可能需要像GPU这样的专用硬件:
>>> import pandas as pd
>>> from scipy.stats import uniform, loguniform
>>> from sklearn.linear_model import SGDClasifier
>>>
>>> df = pd.read_parquet("data/0.parquet") # data to train on; 23MB as above
>>>
>>> model = SGDClasifier()
>>>
>>> # not compute constrained
>>> params = {"l1_ratio": uniform(0, 1)}
>>>
>>> # compute constrained
>>> params = {
... "l1_ratio": uniform(0, 1),
... "alpha": loguniform(1e-5, 1e-1),
... "penalty": ["l2", "l1", "elasticnet"],
... "learning_rate": ["invscaling", "adaptive"],
... "power_t": uniform(0, 1),
... "average": [True, False],
... }
>>>
这些问题是独立的,并且两者可能同时发生。Dask-ML 有工具来解决所有 4 种组合。让我们看看每种情况。
既不计算受限也不内存受限¶
这种情况发生在需要调整的超参数不多且数据能放入内存时。当搜索不需要太长时间运行时,这种情况很常见。
Scikit-learn 可以处理这种情况:
|
对估计器的指定参数值进行穷举搜索。 |
超参数的随机搜索。 |
Dask-ML 也有一些针对 Scikit-learn 版本的直接替代品,这些替代品与 `Dask 集合`_(如 Dask 数组和 Dask DataFrame)配合良好:
|
对估计器的指定参数值进行穷举搜索。 |
超参数的随机搜索。 |
默认情况下,如果传递的是Dask数组/数据帧,这些估计器将高效地将整个数据集传递给 fit
。更多细节请参见“与 Dask 集合配合良好”。
这些估计器在处理预处理成本高昂的模型时特别有效,这在自然语言处理(NLP)中很常见。更多细节请参见“计算受限,但内存不受限”和“避免重复工作”。
内存受限,但计算不受限¶
这种情况发生在数据无法完全装入内存,但需要搜索的超参数不多的情况下。由于数据无法完全装入内存,因此对Dask数组/数据帧的每个块调用``partial_fit``是合理的。这个估计器就是这样做的:
在支持 partial_fit 的模型上逐步搜索超参数 |
更多关于 IncrementalSearchCV
的细节在 “增量超参数优化” 中。
Dask 对 GridSearchCV
和 RandomizedSearchCV
的实现,也可以在 Dask 数组的每个块上调用 partial_fit
,只要传递的模型被 Incremental
包装。
计算受限,但内存不受限¶
这种情况发生在数据适合一台机器的内存,但当有许多超参数需要搜索,或者模型需要像GPU这样的专用硬件时。这种情况的最佳类是 HyperbandSearchCV
:
使用自适应交叉验证算法为特定模型找到最佳参数。 |
简而言之,这个估计器易于使用,具有强大的数学动机,并且表现非常出色。更多详情,请参见 “Hyperband 参数:经验法则” 和 “超带性能”。
这些类中还实现了另外两种自适应超参数优化算法:
执行连续减半算法 [R424ea1a907b1-1]。 |
|
在支持 partial_fit 的模型上逐步搜索超参数 |
这些类的输入参数更难配置。
所有这些搜索都可以通过(巧妙地)决定评估哪些参数来减少解决问题的时间。也就是说,这些搜索会*适应*历史记录来决定继续评估哪些参数。所有这些估计器都支持通过 patience
和 tol
参数忽略评分下降的模型。
另一种限制计算的方法是在搜索过程中避免重复工作。这对于昂贵的预处理特别有用,这在自然语言处理(NLP)中很常见。
超参数的随机搜索。 |
|
|
对估计器的指定参数值进行穷举搜索。 |
避免使用此类重复工作依赖于模型是 Scikit-learn 的 Pipeline
实例。更多详情请参见“避免重复工作”。
计算和内存受限¶
这种情况发生在数据集大于内存且有许多参数需要搜索时。在这种情况下,拥有对 Dask 数组/数据帧的强大支持 并且 决定继续训练哪些模型是非常有用的。
使用自适应交叉验证算法为特定模型找到最佳参数。 |
|
执行连续减半算法 [R424ea1a907b1-1]。 |
|
在支持 partial_fit 的模型上逐步搜索超参数 |
这些类适用于不适合内存的数据。它们还减少了所需的计算量,如“计算受限,但内存不受限”中所述。
现在,让我们深入了解这些类。
“Scikit-Learn 的即插即用替代品” 详细介绍了
RandomizedSearchCV
和GridSearchCV
。“增量超参数优化”详细介绍了
IncrementalSearchCV
及其所有子类(其中一个子类是HyperbandSearchCV
)。:ref:`hyperparameter.adaptive” 详细介绍了
HyperbandSearchCV
的使用和性能。
Scikit-Learn 的即插即用替代品¶
Dask-ML 实现了 GridSearchCV
和 RandomizedSearchCV
的即插即用替代方案。
|
对估计器的指定参数值进行穷举搜索。 |
超参数的随机搜索。 |
Dask-ML 中的变体实现了许多(但不是全部)相同的参数,并且应该是它们所实现子集的即插即用替代品。在这种情况下,为什么要使用 Dask-ML 的版本?
灵活的后端: 超参数优化可以通过线程、进程或在集群中分布式进行并行处理。
与Dask集合配合良好。Dask数组、数据帧和延迟对象可以传递给
fit
。避免重复工作。具有相同参数和输入的候选模型只会拟合一次。对于
Pipeline
这样的复合模型,这可以显著提高效率,因为它可以避免昂贵的重复计算。
Scikit-learn 和 Dask-ML 的模型选择元估计器都可以与 Dask 的 joblib 后端 一起使用。
灵活的后端¶
Dask-ML 可以使用任何 Dask 调度器。默认使用线程调度器,但可以轻松切换为多进程或分布式调度器:
# Distribute grid-search across a cluster
from dask.distributed import Client
scheduler_address = '127.0.0.1:8786'
client = Client(scheduler_address)
search.fit(digits.data, digits.target)
与 Dask 集合配合良好¶
Dask 集合,如 dask.array
、dask.dataframe
和 dask.delayed
,可以传递给 fit
。这意味着你可以使用 Dask 来进行数据加载和预处理,从而实现一个清晰的工作流程。这也使得你可以在不将数据拉取到本地计算机的情况下,直接在集群上处理远程数据:
import dask.dataframe as dd
# Load data from s3
df = dd.read_csv('s3://bucket-name/my-data-*.csv')
# Do some preprocessing steps
df['x2'] = df.x - df.x.mean()
# ...
# Pass to fit without ever leaving the cluster
search.fit(df[['x', 'x2']], df['y'])
此示例将计算每个CV拆分并将其存储在单台机器上,以便可以调用 fit
。
避免重复工作¶
当在 sklearn.pipeline.Pipeline
或 sklearn.pipeline.FeatureUnion
这样的复合模型上进行搜索时,Dask-ML 会避免对相同的模型 + 参数 + 数据组合进行多次拟合。对于早期步骤耗时的管道来说,这可以更快,因为避免了重复工作。
例如,给定以下三阶段管道和网格(修改自 这个 Scikit-learn 示例)。
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.linear_model import SGDClassifier
from sklearn.pipeline import Pipeline
pipeline = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', SGDClassifier())])
grid = {'vect__ngram_range': [(1, 1)],
'tfidf__norm': ['l1', 'l2'],
'clf__alpha': [1e-3, 1e-4, 1e-5]}
Scikit-Learn 的网格搜索实现看起来像这样(简化版):
scores = []
for ngram_range in parameters['vect__ngram_range']:
for norm in parameters['tfidf__norm']:
for alpha in parameters['clf__alpha']:
vect = CountVectorizer(ngram_range=ngram_range)
X2 = vect.fit_transform(X, y)
tfidf = TfidfTransformer(norm=norm)
X3 = tfidf.fit_transform(X2, y)
clf = SGDClassifier(alpha=alpha)
clf.fit(X3, y)
scores.append(clf.score(X3, y))
best = choose_best_parameters(scores, parameters)
作为一个有向无环图,这可能看起来像:
相比之下,dask 版本看起来更像:
scores = []
for ngram_range in parameters['vect__ngram_range']:
vect = CountVectorizer(ngram_range=ngram_range)
X2 = vect.fit_transform(X, y)
for norm in parameters['tfidf__norm']:
tfidf = TfidfTransformer(norm=norm)
X3 = tfidf.fit_transform(X2, y)
for alpha in parameters['clf__alpha']:
clf = SGDClassifier(alpha=alpha)
clf.fit(X3, y)
scores.append(clf.score(X3, y))
best = choose_best_parameters(scores, parameters)
对应的有向无环图:
仔细观察,你可以看到 Scikit-Learn 版本最终会用相同的参数和数据多次拟合管道中的早期步骤。由于 Dask 相对于 Joblib 的灵活性增加,我们能够在图中合并这些任务,并且对于任何参数/数据/模型组合只执行一次拟合步骤。对于早期步骤相对昂贵的管道,这在执行网格搜索时可能是一个很大的优势。
增量超参数优化¶
在支持 partial_fit 的模型上逐步搜索超参数 |
|
使用自适应交叉验证算法为特定模型找到最佳参数。 |
|
执行连续减半算法 [R424ea1a907b1-1]。 |
|
在支持 partial_fit 的模型上逐步搜索超参数 |
这些估计器都以相同的方式处理 Dask 数组/数据帧。示例将使用 HyperbandSearchCV
,但它可以很容易地推广到上述任何估计器。
备注
这些估计器要求模型实现 partial_fit
。
默认情况下,这些类将对数据的每个块调用 partial_fit
。如果这些类的分数停止增加(通过 patience
和 tol
),它们可以停止训练任何模型。它们甚至更进一步,可以选择调用 partial_fit
的模型。
首先,让我们看看基本用法。”自适应超参数优化” 详细介绍了减少所需计算量的估计器。
基本用法¶
本节使用 HyperbandSearchCV
,但它也可以应用于 IncrementalSearchCV
。
In [1]: from dask.distributed import Client
In [2]: from dask_ml.datasets import make_classification
In [3]: from dask_ml.model_selection import train_test_split
In [4]: client = Client()
In [5]: X, y = make_classification(chunks=20, random_state=0)
In [6]: X_train, X_test, y_train, y_test = train_test_split(X, y)
我们的底层模型是一个 sklearn.linear_model.SGDClasifier
。我们为模型的每个克隆指定一些通用参数:
In [7]: from sklearn.linear_model import SGDClassifier
In [8]: clf = SGDClassifier(tol=1e-3, penalty='elasticnet', random_state=0)
我们还定义了从中采样的参数分布:
In [9]: from scipy.stats import uniform, loguniform
In [10]: params = {'alpha': loguniform(1e-2, 1e0), # or np.logspace
....: 'l1_ratio': uniform(0, 1)} # or np.linspace
....:
最后,我们在这个参数空间中创建许多随机模型,并进行训练和评分,直到找到最佳模型。
In [11]: from dask_ml.model_selection import HyperbandSearchCV
In [12]: search = HyperbandSearchCV(clf, params, max_iter=81, random_state=0)
In [13]: search.fit(X_train, y_train, classes=[0, 1]);
In [14]: search.best_params_
Out[14]:
{'alpha': np.float64(0.5874625412921077),
'l1_ratio': np.float64(0.5164906602982609)}
In [15]: search.best_score_
Out[15]: np.float64(0.6)
In [16]: search.score(X_test, y_test)
Out[16]: 0.5
请注意,当你执行 search.score
这样的后拟合任务时,底层模型的 score 方法会被使用。如果该方法无法处理大于内存的 Dask 数组,你的机器内存将会耗尽。如果你计划使用评分或预测等后估计功能,我们推荐使用 dask_ml.wrappers.ParallelPostFit
。
In [17]: from dask_ml.wrappers import ParallelPostFit
In [18]: params = {'estimator__alpha': loguniform(1e-2, 1e0),
....: 'estimator__l1_ratio': uniform(0, 1)}
....:
In [19]: est = ParallelPostFit(SGDClassifier(tol=1e-3, random_state=0))
In [20]: search = HyperbandSearchCV(est, params, max_iter=9, random_state=0)
In [21]: search.fit(X_train, y_train, classes=[0, 1]);
In [22]: search.score(X_test, y_test)
Out[22]: np.float64(0.5)
请注意,参数名称包括 estimator__
前缀,因为我们正在调整 sklearn.linear_model.SGDClasifier
的超参数,该模型是 dask_ml.wrappers.ParallelPostFit
的基础。
自适应超参数优化¶
Dask-ML 有这些估计器可以根据历史数据 适应 以确定哪些模型继续训练。这意味着可以通过更少的 partial_fit
累积调用来找到高评分模型。
使用自适应交叉验证算法为特定模型找到最佳参数。 |
|
执行连续减半算法 [R424ea1a907b1-1]。 |
IncrementalSearchCV
当 decay_rate=1
时也属于此类。所有这些估计器都需要 partial_fit
的实现,并且正如在“增量超参数优化”中提到的,它们都适用于大于内存的数据集。
HyperbandSearchCV
在以下部分中提到了几个优点:
超参数.hyperband-params: 确定
HyperbandSearchCV
输入参数的一个好经验法则。超参数.hyperband-性能:
HyperbandSearchCV
找到高性能模型的速度。
让我们看看当输入按照提供的经验法则选择时,Hyperband 的表现如何。
Hyperband 参数:经验法则¶
HyperbandSearchCV
有两个输入:
max_iter
,它决定了调用partial_fit
的次数Dask 数组的块大小,它决定了每次
partial_fit
调用接收的数据量。
一旦知道了最佳模型的训练时长以及大约需要采样的参数数量,这些自然就会变得非常明显:
In [23]: n_examples = 20 * len(X_train) # 20 passes through dataset for best model
In [24]: n_params = 94 # sample approximately 100 parameters; more than 94 will be sampled
通过这个,使用经验法则来计算Hyperband的输入变得很容易:
In [25]: max_iter = n_params
In [26]: chunk_size = n_examples // n_params # implicit
既然我们已经确定了输入,让我们创建搜索对象并重新分块 Dask 数组:
In [27]: clf = SGDClassifier(tol=1e-3, penalty='elasticnet', random_state=0)
In [28]: params = {'alpha': loguniform(1e-2, 1e0), # or np.logspace
....: 'l1_ratio': uniform(0, 1)} # or np.linspace
....:
In [29]: search = HyperbandSearchCV(clf, params, max_iter=max_iter, aggressiveness=4, random_state=0)
In [30]: X_train = X_train.rechunk((chunk_size, -1))
In [31]: y_train = y_train.rechunk(chunk_size)
我们使用了 aggressiveness=4
因为这是一个初始搜索。我对数据、模型或超参数了解不多。如果我至少对使用哪些超参数有一些了解,我会指定 aggressiveness=3
,即默认值。
这个经验法则的输入正是用户所关心的:
搜索空间复杂度的一个度量(通过
n_params
)训练最佳模型需要多长时间(通过
n_examples
)他们对超参数(通过
aggressiveness
)的信心如何。
值得注意的是,与 RandomizedSearchCV
不同,n_examples
和 n_params
之间没有权衡,因为 n_examples
仅适用于 某些 模型,而不适用于 所有 模型。关于这一经验法则的更多细节,请参阅 HyperbandSearchCV
的“注释”部分。
然而,这并没有明确提到执行的计算量——它只是一个近似值。计算量可以这样查看:
In [32]: search.metadata["partial_fit_calls"] # best model will see `max_iter` chunks
Out[32]: np.int64(1151)
In [33]: search.metadata["n_models"] # actual number of parameters to sample
Out[33]: 98
这比 RandomizedSearchCV
采样了更多的超参数,后者在相同的计算量下只会采样大约12个超参数(或初始化12个模型)。让我们用这些不同的块来拟合 HyperbandSearchCV
:
In [34]: search.fit(X_train, y_train, classes=[0, 1]);
In [35]: search.best_params_
Out[35]:
{'alpha': np.float64(0.07501954443620121),
'l1_ratio': np.float64(0.8917730007820798)}
明确地说,这是一个非常小的玩具示例:只有100个观测值和20个特征。让我们看看在更现实的示例中性能如何扩展。
超带性能¶
本次性能比较将简要总结一项实验,以找到性能结果。这与上述情况类似。完整细节可以在 Dask 博客文章《使用 Dask 进行更好更快的超参数优化》中找到。
它将使用以下输入的这些估计器:
模型:Scikit-learn 的
MLPClassifier
带有 12 个神经元数据集:一个简单的合成数据集,包含4个类别和6个特征(2个有意义的特征和4个随机特征):
让我们寻找最适合分类这个数据集的模型。让我们在这些参数上进行搜索:
控制最佳模型架构的超参数之一:
hidden_layer_sizes
。这可以取包含12个神经元的值;例如,两层中各有6个神经元,或三层中各有4个神经元。控制找到特定架构最佳模型的六个超参数。这包括权重衰减和各种优化参数(包括批量大小、学习率和动量)。
以下是我们将如何配置这两个不同的估计器:
“Hyperband” 通过上述经验法则配置,其中
n_params = 299
1 和n_examples = 50 * len(X_train)
。“Incremental” 配置为与 Hyperband 做同样多的工作,使用
IncrementalSearchCV(..., n_initial_parameters=19, decay_rate=0)
这两个估计器配置为执行相同数量的计算,相当于拟合大约19个模型。在这个计算量下,最终的准确率如何?
这很棒 – HyperbandSearchCV
看起来比 IncrementalSearchCV
更有信心。但这些搜索找到(比如说)85% 准确率的模型有多快?实验上,Hyperband 在大约 350 次数据集遍历后达到 84% 的准确率,而 Incremental 需要 900 次数据集遍历:
“通过数据集” 在这种情况下是 “解决方案时间” 的一个很好的代理,因为只使用了4个Dask工作者,并且它们在搜索的大部分时间里都很忙。随着工作者数量的变化,这种情况会如何改变?
为了验证这一点,让我们通过一个单独的实验来分析Hyperband的完成时间如何随Dask工作者的数量变化。
看起来加速在约24个Dask工作节点时开始饱和。如果搜索空间变大或模型评估时间变长,这个数字将会增加。
- 1
大约需要300个参数;选择了299个以使Dask数组块均匀