实时 Notebook

您可以在 实时会话 中运行此 Notebook Binder,或在 Github 上查看

使用 Dask 进行超参数优化

每个机器学习模型在训练开始前都会指定一些值。这些值有助于使模型适应数据,但必须在看到任何训练数据之前给出。例如,在 Scikit-learn 的 LogisiticRegression 中,这可能是 penaltyC。这些在任何训练数据之前出现的值被称为“超参数”。典型用法如下所示:

from sklearn.linear_model import LogisiticRegression
from sklearn.datasets import make_classification

X, y = make_classification()
est = LogisiticRegression(C=10, penalty="l2")
est.fit(X, y)

这些超参数会影响预测的质量。例如,如果在上面的示例中 C 太小,估计器的输出将无法很好地拟合数据。

确定这些超参数的值很困难。事实上,Scikit-learn 有一整个文档页面专门介绍如何找到最佳值:https://scikit-learn.cn/stable/modules/grid_search.html

Dask 为超参数优化提供了一些新技术和机会。其中一个机会涉及提前停止训练以限制计算。自然地,这需要某种方式来停止和重新开始训练(在 Scikit-learn 术语中称为 partial_fitwarm_start)。

当搜索复杂且有许多搜索参数时,这尤其有用。很好的例子是大多数深度学习模型,它们有专门处理大量数据的算法,但在提供基本超参数(例如,“学习率”、“动量”或“权重衰减”)方面存在困难。

本 Notebook 将介绍

  • 设置一个实际示例

  • 如何使用 HyperbandSearchCV,包括

    • 理解 HyperbandSearchCV 的输入参数

    • 运行超参数优化

    • 如何从 HyperbandSearchCV 访问信息

本 Notebook 特别不会展示促使使用 HyperbandSearchCV 的性能比较。HyperbandSearchCV 以最少的训练找到高分;但是,这是一个关于如何使用它的教程。所有性能比较都放在了解更多 部分。

[1]:
%matplotlib inline

设置 Dask

[2]:
from distributed import Client
client = Client(processes=False, threads_per_worker=4,
                n_workers=1, memory_limit='2GB')
client
[2]:

客户端

客户端-6ab1bd16-0de1-11ed-a383-000d3a8f7959

连接方法: Cluster 对象 集群类型: distributed.LocalCluster
仪表板: http://10.1.1.64:8787/status

集群信息

创建数据

[3]:
from sklearn.datasets import make_circles
import numpy as np
import pandas as pd

X, y = make_circles(n_samples=30_000, random_state=0, noise=0.09)

pd.DataFrame({0: X[:, 0], 1: X[:, 1], "class": y}).sample(4_000).plot.scatter(
    x=0, y=1, alpha=0.2, c="class", cmap="bwr"
);
../_images/machine-learning_hyperparam-opt_8_0.png

添加随机维度

[4]:
from sklearn.utils import check_random_state

rng = check_random_state(42)
random_feats = rng.uniform(-1, 1, size=(X.shape[0], 4))
X = np.hstack((X, random_feats))
X.shape
[4]:
(30000, 6)

分割和缩放数据

[5]:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=5_000, random_state=42)
[6]:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
scaler = StandardScaler().fit(X_train)

X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
[7]:
from dask.utils import format_bytes

for name, X in [("train", X_train), ("test", X_test)]:
    print("dataset =", name)
    print("shape =", X.shape)
    print("bytes =", format_bytes(X.nbytes))
    print("-" * 20)
dataset = train
shape = (25000, 6)
bytes = 1.14 MiB
--------------------
dataset = test
shape = (5000, 6)
bytes = 234.38 kiB
--------------------

现在我们有了训练集和测试集。

创建模型和搜索空间

为了方便起见,让我们使用 Scikit-learn 的 MLPClassifier 作为我们的模型。让我们使用这个有 24 个神经元的模型,并调整一些其他基本超参数。

[8]:
import numpy as np
from sklearn.neural_network import MLPClassifier

model = MLPClassifier()

也可以使用深度学习库。特别是,PyTorch 的 Scikit-Learn 封装 SkorchHyperbandSearchCV 配合良好。

[9]:
params = {
    "hidden_layer_sizes": [
        (24, ),
        (12, 12),
        (6, 6, 6, 6),
        (4, 4, 4, 4, 4, 4),
        (12, 6, 3, 3),
    ],
    "activation": ["relu", "logistic", "tanh"],
    "alpha": np.logspace(-6, -3, num=1000),  # cnts
    "batch_size": [16, 32, 64, 128, 256, 512],
}

超参数优化

HyperbandSearchCV 是 Dask-ML 用来寻找最佳超参数的元估计器。它可以用作 RandomizedSearchCV 的替代方案,通过不将时间浪费在没有前景的超参数上,从而在更短的时间内找到相似的超参数。具体来说,它几乎可以保证以最少的训练找到高性能的模型。

本节将重点介绍

  1. 理解 HyperbandSearchCV 的输入参数

  2. 使用 HyperbandSearchCV 寻找最佳超参数

  3. 查看 HyperbandSearchCV 的其他用例

[10]:
from dask_ml.model_selection import HyperbandSearchCV

确定输入参数

确定 HyperbandSearchCV 输入参数的一个经验法则是需要知道

  1. 训练时间最长的模型将看到的示例数量

  2. 要评估的超参数数量

让我们写下本例中这些值应该是什么

[11]:
# For quick response
n_examples = 4 * len(X_train)
n_params = 8

# In practice, HyperbandSearchCV is most useful for longer searches
# n_examples = 15 * len(X_train)
# n_params = 15

在此,训练时间最长的模型将看到 n_examples 个示例。这是所需的数据量,通常由问题难度设定。简单的问题可能只需要通过数据集 10 次;更复杂的问题可能需要通过数据集 100 次。

将采样 n_params 个参数,因此将评估 n_params 个模型。得分较低的模型在看到 n_examples 个示例之前将被终止。这有助于节省计算。

我们如何使用这些值来确定 HyperbandSearchCV 的输入?

[12]:
max_iter = n_params  # number of times partial_fit will be called
chunks = n_examples // n_params  # number of examples each call sees

max_iter, chunks
[12]:
(8, 12500)

这意味着训练时间最长的估计器将看到大约 n_examples 个示例(具体来说是 n_params * (n_examples // n_params))。

应用输入参数

让我们创建一个具有此块大小的 Dask 数组

[13]:
import dask.array as da
X_train2 = da.from_array(X_train, chunks=chunks)
y_train2 = da.from_array(y_train, chunks=chunks)
X_train2
[13]:
数组
字节 1.14 MiB 585.94 kiB
形状 (25000, 6) (12500, 6)
计数 2 个任务 2 个块
类型 float64 numpy.ndarray
6 25000

每次 partial_fit 调用将接收一个块。

这意味着每个块中的示例数量应该(大约)相同,并且应该选择 n_examplesn_params 来实现这一点。(例如,对于 100 个示例,应争取 (33, 33, 34) 个示例的块,而不是 (48, 48, 4) 个示例)。

现在让我们使用 max_iter 来创建我们的 HyperbandSearchCV 对象

[14]:
search = HyperbandSearchCV(
    model,
    params,
    max_iter=max_iter,
    patience=True,
)

将执行多少计算?

max_iterchunks 不清楚如何确定执行了多少计算。幸运的是,HyperbandSearchCV 有一个 metadata 属性可以预先确定这一点

[15]:
search.metadata["partial_fit_calls"]
[15]:
26

这显示了计算中将执行多少次 partial_fit 调用。metadata 还包括有关创建的模型数量的信息。

到目前为止,所做的只是为计算(以及查看将执行多少计算)做好搜索准备。到目前为止,所有的计算都快速且容易。

执行计算

现在,让我们进行模型选择搜索并找到最佳超参数。这是本 Notebook 的真正核心。此计算将在 Dask 可用的所有硬件上进行。

[16]:
%%time
search.fit(X_train2, y_train2, classes=[0, 1, 2, 3])
/usr/share/miniconda3/envs/dask-examples/lib/python3.9/site-packages/dask_ml/model_selection/_incremental.py:641: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  cv_results = {k: np.array(v) for k, v in cv_results.items()}
/usr/share/miniconda3/envs/dask-examples/lib/python3.9/site-packages/dask_ml/model_selection/_incremental.py:641: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  cv_results = {k: np.array(v) for k, v in cv_results.items()}
/usr/share/miniconda3/envs/dask-examples/lib/python3.9/site-packages/dask_ml/model_selection/_hyperband.py:455: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  cv_results = {k: np.array(v) for k, v in cv_results.items()}
CPU times: user 3.54 s, sys: 661 ms, total: 4.2 s
Wall time: 3.42 s
[16]:
HyperbandSearchCV(estimator=MLPClassifier(), max_iter=8,
                  parameters={'activation': ['relu', 'logistic', 'tanh'],
                              'alpha': array([1.00000000e-06, 1.00693863e-06, 1.01392541e-06, 1.02096066e-06,
       1.02804473e-06, 1.03517796e-06, 1.04236067e-06, 1.04959323e-06,
       1.05687597e-06, 1.06420924e-06, 1.07159340e-06, 1.07902879e-06,
       1.08651577e-06, 1.09405471e-06, 1.10164595e-06, 1.1...
       9.01477631e-04, 9.07732653e-04, 9.14031075e-04, 9.20373200e-04,
       9.26759330e-04, 9.33189772e-04, 9.39664831e-04, 9.46184819e-04,
       9.52750047e-04, 9.59360829e-04, 9.66017480e-04, 9.72720319e-04,
       9.79469667e-04, 9.86265846e-04, 9.93109181e-04, 1.00000000e-03]),
                              'batch_size': [16, 32, 64, 128, 256, 512],
                              'hidden_layer_sizes': [(24,), (12, 12),
                                                     (6, 6, 6, 6),
                                                     (4, 4, 4, 4, 4, 4),
                                                     (12, 6, 3, 3)]},
                  patience=True)

仪表板在此运行时将处于活动状态。它将显示哪些工作节点正在运行 partial_fitscore 调用。这大约需要 10 秒。

集成

HyperbandSearchCV 遵循 Scikit-learn API,并模仿 Scikit-learn 的 RandomizedSearchCV。这意味着它“开箱即用”。所有 Scikit-learn 属性和方法都可用

[17]:
search.best_score_
[17]:
0.8122
[18]:
search.best_estimator_
[18]:
MLPClassifier(alpha=1.0642092440647246e-05, batch_size=32,
              hidden_layer_sizes=(12, 12))
[19]:
cv_results = pd.DataFrame(search.cv_results_)
cv_results.head()
[19]:
param_alpha mean_partial_fit_time std_partial_fit_time bracket mean_score_time test_score param_batch_size std_score_time param_hidden_layer_sizes model_id rank_test_score param_activation partial_fit_calls params
0 0.000953 0.394128 0.026214 1 0.016661 0.7246 64 0.007632 (24,) bracket=1-0 1 relu 6 {'hidden_layer_sizes': (24,), 'batch_size': 64...
1 0.000159 0.785529 0.025667 1 0.020612 0.0000 256 0.003827 (4, 4, 4, 4, 4, 4) bracket=1-1 3 logistic 2 {'hidden_layer_sizes': (4, 4, 4, 4, 4, 4), 'ba...
2 0.000004 0.575372 0.035262 1 0.021490 0.5682 32 0.010454 (24,) bracket=1-2 2 relu 2 {'hidden_layer_sizes': (24,), 'batch_size': 32...
3 0.000003 0.455648 0.001261 0 0.021098 0.0000 512 0.001401 (4, 4, 4, 4, 4, 4) bracket=0-0 2 relu 3 {'hidden_layer_sizes': (4, 4, 4, 4, 4, 4), 'ba...
4 0.000011 0.266258 0.047284 0 0.008451 0.8122 32 0.004509 (12, 12) bracket=0-1 1 relu 8 {'hidden_layer_sizes': (12, 12), 'batch_size':...
[20]:
search.score(X_test, y_test)
[20]:
0.8036
[21]:
search.predict(X_test)
[21]:
数组
字节 39.06 kiB 39.06 kiB
形状 (5000,) (5000,)
计数 2 个任务 1 个块
类型 int64 numpy.ndarray
5000 1
[22]:
search.predict(X_test).compute()
[22]:
array([1, 0, 1, ..., 1, 0, 0])

它还有一些其他属性。

[23]:
hist = pd.DataFrame(search.history_)
hist.head()
[23]:
model_id params partial_fit_calls partial_fit_time score score_time elapsed_wall_time bracket
0 bracket=0-0 {'hidden_layer_sizes': (4, 4, 4, 4, 4, 4), 'ba... 1 0.454387 0.0000 0.022499 0.839712 0
1 bracket=0-1 {'hidden_layer_sizes': (12, 12), 'batch_size':... 1 0.322257 0.5270 0.014936 0.839714 0
2 bracket=1-0 {'hidden_layer_sizes': (24,), 'batch_size': 64... 1 0.431628 0.5138 0.007477 0.851272 1
3 bracket=1-1 {'hidden_layer_sizes': (4, 4, 4, 4, 4, 4), 'ba... 1 0.811195 0.0000 0.024439 0.851274 1
4 bracket=1-2 {'hidden_layer_sizes': (24,), 'batch_size': 32... 1 0.610634 0.5018 0.011036 0.851274 1

这说明了每次 partial_fit 调用后的历史记录。还有一个属性 model_history_ 记录每个模型的历史记录(它是 history_ 的重组)。

了解更多

本 Notebook 介绍了 HyperbandSearchCV 的基本用法。以下文档和资源可能有助于了解更多关于 HyperbandSearchCV 的信息,包括一些更精细的用例

性能比较可在 SciPy 2019 演讲/论文中找到。