超参数搜索
目录
超参数搜索¶
使用 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 collections(如 Dask Arrays 和 Dask DataFrames)配合良好。
|
对估计器指定参数值进行详尽搜索。 |
对超参数进行随机搜索。 |
默认情况下,如果传入 Dask Array/DataFrame,这些估计器会高效地将整个数据集传递给 fit
。更多详情请参阅“与 Dask Collections 协同工作良好”。
上述这些估计器特别适用于具有昂贵预处理步骤的模型,这在自然语言处理 (NLP) 中很常见。更多详情请参阅“计算受限,但内存不受限”和“避免重复工作”。
内存受限,但计算不受限¶
这种情况发生在数据无法放入内存但没有太多超参数需要搜索时。数据无法放入内存,因此对 Dask Array/Dataframe 的每个块调用 partial_fit
是有意义的。此估计器实现了这一点。
在支持 partial_fit 的模型上进行增量超参数搜索。 |
关于 IncrementalSearchCV
的更多详情请参阅“增量超参数优化”。
Dask 对 GridSearchCV
和 RandomizedSearchCV
的实现也可以对 Dask 数组的每个块调用 partial_fit
,只要传入的模型使用 Incremental
进行了封装。
计算受限,但内存不受限¶
这种情况发生在数据能放入一台机器的内存中,但有许多超参数需要搜索,或者模型需要像 GPU 这样的专用硬件时。这种情况的最佳类是 HyperbandSearchCV
。
使用自适应交叉验证算法找到特定模型的最佳参数。 |
简而言之,此估计器易于使用,具有强大的数学基础,并且表现非常出色。更多详情请参阅“Hyperband 参数:经验法则”和“Hyperband 性能”。
另外两种自适应超参数优化算法实现在这些类中:
执行逐次减半算法 [R424ea1a907b1-1]。 |
|
在支持 partial_fit 的模型上进行增量超参数搜索。 |
这些类的输入参数配置起来比较困难。
所有这些搜索都可以通过(巧妙地)决定评估哪些参数来减少解决时间。也就是说,这些搜索会根据历史记录自适应地决定继续评估哪些参数。所有这些估计器都支持通过 patience
和 tol
参数忽略得分下降的模型。
限制计算的另一种方法是在搜索过程中避免重复工作。这对于昂贵的预处理特别有用,这在自然语言处理 (NLP) 中很常见。
对超参数进行随机搜索。 |
|
|
对估计器指定参数值进行详尽搜索。 |
使用此类避免重复工作依赖于模型是 Scikit-learn 的 Pipeline
实例。更多详情请参阅“避免重复工作”。
计算和内存均受限¶
这种情况发生在数据集大于内存且有许多参数需要搜索时。在这种情况下,同时强有力地支持 Dask Arrays/DataFrames 并决定继续训练哪些模型非常有用。
使用自适应交叉验证算法找到特定模型的最佳参数。 |
|
执行逐次减半算法 [R424ea1a907b1-1]。 |
|
在支持 partial_fit 的模型上进行增量超参数搜索。 |
这些类与无法放入内存的数据配合良好。它们还减少了所需的计算量,如“计算受限,但内存不受限”中所述。
现在,让我们深入了解这些类。
“Scikit-Learn 的直接替换”详细介绍了
RandomizedSearchCV
和GridSearchCV
。“增量超参数优化”详细介绍了
IncrementalSearchCV
及其所有子类(其中之一是HyperbandSearchCV
)。“自适应超参数优化”详细介绍了
HyperbandSearchCV
的用法和性能。
Scikit-Learn 的直接替换¶
Dask-ML 实现了 GridSearchCV
和 RandomizedSearchCV
的直接替换。
|
对估计器指定参数值进行详尽搜索。 |
对超参数进行随机搜索。 |
Dask-ML 中的变体实现了许多(但非全部)相同的参数,并且对于它们实现的子集来说,应该是一个直接替换。既然如此,为什么要使用 Dask-ML 的版本呢?
灵活的后端:超参数优化可以通过线程、进程或在集群上分布式并行进行。
与 Dask collections 协同工作良好。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 Collections 协同工作良好¶
Dask collections,如 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
。
from dask.distributed import Client
from dask_ml.datasets import make_classification
from dask_ml.model_selection import train_test_split
client = Client()
X, y = make_classification(chunks=20, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y)
我们的底层模型是 sklearn.linear_model.SGDClasifier
。我们指定一些模型每个克隆共有的参数。
from sklearn.linear_model import SGDClassifier
clf = SGDClassifier(tol=1e-3, penalty='elasticnet', random_state=0)
我们还定义了我们将从中采样的参数分布。
from scipy.stats import uniform, loguniform
params = {'alpha': loguniform(1e-2, 1e0), # or np.logspace
'l1_ratio': uniform(0, 1)} # or np.linspace
最后,我们在该参数空间中创建许多随机模型,并对其进行训练和评分,直到找到最佳模型。
from dask_ml.model_selection import HyperbandSearchCV
search = HyperbandSearchCV(clf, params, max_iter=81, random_state=0)
search.fit(X_train, y_train, classes=[0, 1]);
search.best_params_
search.best_score_
search.score(X_test, y_test)
请注意,当您执行 search.score
等后拟合任务时,使用的是底层模型的 score 方法。如果该方法无法处理大于内存的 Dask Array,您的机器内存将耗尽。如果您计划使用评分或预测等后估计功能,我们建议使用 dask_ml.wrappers.ParallelPostFit
。
from dask_ml.wrappers import ParallelPostFit
params = {'estimator__alpha': loguniform(1e-2, 1e0),
'estimator__l1_ratio': uniform(0, 1)}
est = ParallelPostFit(SGDClassifier(tol=1e-3, random_state=0))
search = HyperbandSearchCV(est, params, max_iter=9, random_state=0)
search.fit(X_train, y_train, classes=[0, 1]);
search.score(X_test, y_test)
请注意,参数名称包含 estimator__
前缀,因为我们正在调整位于 dask_ml.wrappers.ParallelPostFit
底层的 sklearn.linear_model.SGDClasifier
的超参数。
自适应超参数优化¶
Dask-ML 拥有这些能够根据历史数据自适应地决定继续训练哪些模型的估计器。这意味着可以用更少的累积 partial_fit
调用次数找到高分模型。
使用自适应交叉验证算法找到特定模型的最佳参数。 |
|
执行逐次减半算法 [R424ea1a907b1-1]。 |
当 decay_rate=1
时,IncrementalSearchCV
也属于此类。所有这些估计器都需要实现 partial_fit
,并且如“增量超参数优化”中所述,它们都适用于大于内存的数据集。
HyperbandSearchCV
有几个优点,在以下章节中提到:
Hyperband 参数:经验法则:确定
HyperbandSearchCV
输入参数的良好经验法则。Hyperband 性能:
HyperbandSearchCV
找到高性能模型的速度。
让我们看看使用提供的经验法则选择输入时,Hyperband 的表现如何。
Hyperband 参数:经验法则¶
HyperbandSearchCV
有两个输入:
max_iter
,它决定调用partial_fit
的次数。Dask 数组的块大小,它决定了每次
partial_fit
调用接收多少数据。
一旦知道训练最佳模型需要多长时间以及要采样多少参数,这些就自然而然地得出了。
n_examples = 20 * len(X_train) # 20 passes through dataset for best model
n_params = 94 # sample approximately 100 parameters; more than 94 will be sampled
有了这个,就可以轻松使用经验法则来计算 Hyperband 的输入。
max_iter = n_params
chunk_size = n_examples // n_params # implicit
现在我们已经确定了输入,让我们创建搜索对象并对 Dask 数组进行分块。
clf = SGDClassifier(tol=1e-3, penalty='elasticnet', random_state=0)
params = {'alpha': loguniform(1e-2, 1e0), # or np.logspace
'l1_ratio': uniform(0, 1)} # or np.linspace
search = HyperbandSearchCV(clf, params, max_iter=max_iter, aggressiveness=4, random_state=0)
X_train = X_train.rechunk((chunk_size, -1))
y_train = y_train.rechunk(chunk_size)
我们使用了 aggressiveness=4
,因为这是一次初始搜索。我对数据、模型或超参数了解不多。如果我至少对使用哪些超参数有所了解,我会指定默认值 aggressiveness=3
。
此经验法则的输入正是用户关心的:
搜索空间的复杂程度(通过
n_params
)训练最佳模型需要多长时间(通过
n_examples
)他们对超参数的信心程度(通过
aggressiveness
)。
值得注意的是,与 RandomizedSearchCV
不同,n_examples
和 n_params
之间没有权衡,因为 n_examples
仅适用于某些模型,而不是所有模型。有关此经验法则的更多详细信息,请参见 HyperbandSearchCV
的“Notes”部分。
然而,这并未明确提及执行的计算量——它仅是一个近似值。计算量可以这样看待:
search.metadata["partial_fit_calls"] # best model will see `max_iter` chunks
search.metadata["n_models"] # actual number of parameters to sample
与 RandomizedSearchCV
相比,这采样了更多的超参数,后者在相同的计算量下仅采样约 12 个超参数(或初始化 12 个模型)。让我们使用这些不同的块来拟合 HyperbandSearchCV
。
search.fit(X_train, y_train, classes=[0, 1]);
search.best_params_
需要明确的是,这是一个非常小的玩具示例:只有 100 个观测值和 20 个特征。让我们看看在一个更真实的示例中,性能如何随着数据量扩展。
Hyperband 性能¶
本性能比较将简要总结一项寻找性能结果的实验。这类似于上面的情况。完整的详细信息可以在 Dask 博客文章“使用 Dask 进行更好更快的超参数优化”中找到。
它将使用以下输入的估计器:
模型:Scikit-learn 的
MLPClassifier
,具有 12 个神经元。数据集:一个简单的合成数据集,包含 4 个类别和 6 个特征(2 个有意义的特征和 4 个随机特征)。

包含 60,000 个数据的训练数据集。4 个类别用不同的颜色显示,除了所示的两个特征(在 x/y 轴上)之外,还有 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 个模型。在这样的计算量下,最终的准确率如何?
上述估计器进行 200 次不同运行后的最终验证准确率。在 200 次运行中,HyperbandSearchCV
的最差运行结果优于 IncrementalSearchCV
的 99 次运行结果。¶
这太棒了——HyperbandSearchCV
看起来比 IncrementalSearchCV
更具信心。但是,这些搜索找到(例如)85% 准确率模型的速度有多快?实验表明,Hyperband 在大约 350 次遍历数据集后达到 84% 的准确率,而 Incremental 需要 900 次遍历数据集。
每次搜索在遍历数据集一定次数后获得的平均准确率。绿线表示将 4 个模型训练至完成所需的数据遍历次数。¶
在本例中,“遍历数据集次数”是“解决时间”的一个很好的代理,因为只使用了 4 个 Dask worker,并且它们在绝大多数搜索时间内都在忙碌。这如何随着 worker 数量的变化而变化?
为此,让我们在另一个实验中分析 Hyperband 完成时间如何随 Dask worker 数量的变化而变化。
Hyperband 单次运行的完成时间随 Dask worker 数量的变化而变化。实白线表示训练一个模型所需的时间。¶
看起来加速效果在大约 24 个 Dask worker 处开始饱和。如果搜索空间变大或模型评估时间变长,这个数字将会增加。
- 1
预期参数约为 300 个;选择 299 是为了使 Dask 数组能够均匀分块。