翻译自https://districtdatalabs.silvrback.com/parameter-tuning-with-hyperopt
Parameter Tuning with Hyperopt –Kris Wright
概述
Hyperopt可以帮助快速进行机器学习模型参数调试。通常情况下有两种类型的参数调试方法,网格搜索(grid search)和随机搜索(random search)。网格搜索速度慢但是适用于需要随整个参数空间进行搜索的情况;随机搜索速度很快但是容易遗漏一些重要信息。幸运的是,我们有第三个选择:贝叶斯优化(Bayesian optimization)。这里我们关注贝叶斯优化在Python中的实现:Hyperopt模块。
使用贝叶斯优化进行参数调试允许我们获得给定模型的最优参数,如,逻辑回归( logistic regression),因此我们可以进行最优模型选择。通常情况下,机器学习工程师或数据科学家会对一些模型进行一些形式的手动参数调试(如网格搜索或随机搜索),如决策树(decision tree)、支持向量机(support vector machine)、k最近邻(k nearest neighbors)-然后比较准确度得分(accuracy score)进而选择性能表现最好的一组参数给模型使用。这种方法可能会产生次优模型(sub-optimal models)。数据科学家可能会给决策树选择一组最优参数,但是对SVM却不是最优的,这就意味着模型比较是有缺陷的。KNN每次都可以打败SVM,如果SVM的参数选择很差的话。贝叶斯优化允许数据科学家找到所有模型的最优参数,然后比较所有使用了最佳参数的模型。这样的话,在模型选择中,就可以比较最优的决策树和最优的支持向量机。只有这样才能保证我们选择和使用了实际最好的模型。
本文涉及的主题包括:
- 评估函数(Objective functions)
- 搜索空间(Search spaces)
- 存储评价试验(Storing evaluation trials)
- 可视化(Visualization)
- 鸢尾花数据集实例(Full example on a classic dataset: Iris)
目标函数(Objective Functions - A Motivating Example)
假设有一个定义在摸个范围内的函数,你想要使这个函数最小化,也就是说,你想要找到一个输入值,使得函数的输出结果是最小的。如下面的例子,找到一个x,使得线性函数y(x)=x取得最小值。
from hyperopt import fmin, tpe, hp
best = fmin(
fn=lambda x: x,
space=hp.uniform('x', 0, 1),
algo=tpe.suggest,
max_evals=100)
print best
下面进行分解说明。
函数fmin首先要接收一个函数fn来最小化,这里我们指定fn为一个匿名函数lambda x: x,实际上这个函数可以是任何有效的带有返回值的函数(valid value-returning function),如回归中的平均绝对误差(mean absolute error)。
第二个参数space指定了搜索空间(search space),在这个例子中为在0到1范围之间的数字。hp.uniform是一个内置的( built-in)hyperopt函数,包括三个参数:name:x,范围的上界和下届:0和1。
参数algo接收一个搜索算法(serach algorithm),这个例子中tpe代表tree of Parzen estimators。这个话题超出了这个博客文章的范围,更多关于tpe的内容请参考这里。algo参数也可以设置成hyperopt.random,但我们在此不做介绍,因为这是众所周知的搜索策略。
最后,我们指定fmin函数执行的最大次数max_evals。fmin函数返回一个python字典。函数输出的一个例子:{‘x’: 0.000269455723739237}。
下面是一张函数图像,红色的点是我们要找的x的位置。
另一个例子(More Complicated Examples)
这里有个更复杂的目标函数:lambda x: (x-1)**2,这次我们试着最小化一个二次方程
y(x)=(x−1)2
y
(
x
)
=
(
x
−
1
)
2
。因此,我们将搜索空间更改为包括我们已经知道的最优值x=1以及加上两侧的一些次优范围:hp.uniform(‘x’, -2, 2)。
我们可以得到:
best = fmin(
fn=lambda x: (x-1)**2,
space=hp.uniform('x', -2, 2),
algo=tpe.suggest,
max_evals=100)
print best
输出为:
{'x': 0.997369045274755}
函数如下图:
除了最小化目标函数,也许我们想使函数值最大化。这种情况下我们只需要返回函数的负值,如函数y(x) = -(x**2):
如何解决这个问题呢?我们只需要将目标函数改为lambda x: (x**2),求y(x) = -(x**2)得最小值即为求y(x) = -(x**2)的最大值。
同理对于刚开始的线性函数的例子,我们将求最小化改为求最大化,将将目标函数改为lambda x: -x即可。
下面是一个有用许多(给定无穷范围的无穷多个)局部最小值得函数,也可以求最大值。
搜索空间(Search spaces)
Hyperopt模块包含了一些方便的函数(handy functions)来指定输入参数的范围。我们已经见过hp.uniform。最初,这些是随机搜索空间,但是随着hyperopt的学习(随着从目标函数获得更多的feedback),它会对初始搜索空间的不同部分进行调整和采样,并认为这些部分会给它提供最有意义的反馈。
以下函数将在本文使用:
- hp.choice(label, options) ,其中options为 python list 或tuple
- hp.normal(label, mu, sigma) ,其中mu 和 sigma 分别为均值和标准差
- hp.uniform(label, low, high),其中low和 high分别为范围的上界和下界
其他还有hp.normal, hp.lognormal, hp.quniform,因为本文不会使用就没有写,可自行了解。
下面定义搜索空间:
import hyperopt.pyll.stochastic
space = {
'x': hp.uniform('x', 0, 1),
'y': hp.normal('y', 0, 1),
'name': hp.choice('name', ['alice', 'bob']),
}
print hyperopt.pyll.stochastic.sample(space)
例子输出:
{'y': -1.4012610048810574, 'x': 0.7258615424906184, 'name': 'alice'}
获取信息和试验(Capturing Info with Trials)
如果能看到hyperopt黑盒里到底发生了什么,那就太好了。Trials对象允许我们这样做。我们只需要再导入几个项目
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
fspace = {
'x': hp.uniform('x', -5, 5)
}
def f(params):
x = params['x']
val = x**2
return {'loss': val, 'status': STATUS_OK}
trials = Trials()
best = fmin(fn=f, space=fspace, algo=tpe.suggest, max_evals=50, trials=trials)
print 'best:', best
print 'trials:'
for trial in trials.trials[:2]:
print trial
STATUS_OK 和 Trials是新导入的模块,Trials允许我们存储每一时间步长(time step)所存储的信息。然后我们可以输出这些函数在给定时间步长上对给定参数的求值。
输出:
best: {'x': 0.014420181637303776}
trials:
{'refresh_time': None, 'book_time': None, 'misc': {'tid': 0, 'idxs': {'x': [0]}, 'cmd': ('domain_attachment', 'FMinIter_Domain'), 'vals': {'x': [1.9646918559786162]}, 'workdir': None}, 'state': 2, 'tid': 0, 'exp_key': None, 'version': 0, 'result': {'status': 'ok', 'loss': 3.8600140889486996}, 'owner': None, 'spec': None}
{'refresh_time': None, 'book_time': None, 'misc': {'tid': 1, 'idxs': {'x': [1]}, 'cmd': ('domain_attachment', 'FMinIter_Domain'), 'vals': {'x': [-3.9393509404526728]}, 'workdir': None}, 'state': 2, 'tid': 1, 'exp_key': None, 'version': 0, 'result': {'status': 'ok', 'loss': 15.518485832045357}, 'owner': None, 'spec': None}
Trials对象将数据存储为BSON对象,类似于JSON对象一样。BSON来自pymongo模块,着这里我们不讨论细节,但是对于hyperopt有一些高级选项需要使用MongoDB进行分布式计算,因此导入pymongo。
回到上面的输出,’tid’表示时间id,也就是时间步长,范围0到max_evals-1,每次迭代加1;’x’在’vals’键中,也就是每次迭代参数存储的地方;’loss’在’result’键中,是每次迭代目标函数的值。
可视化(Visualization)
这里介绍两种类型可视化,val vs. time和loss vs. val。
首先val vs. time。下面是trials.trials数据描述可视化的代码和样本输出。
f, ax = plt.subplots(1)
xs = [t['tid'] for t in trials.trials]
ys = [t['misc']['vals']['x'] for t in trials.trials]
ax.set_xlim(xs[0]-10, xs[-1]+10)
ax.scatter(xs, ys, s=20, linewidth=0.01, alpha=0.75)
ax.set_title('$x$ $vs$ $t$ ', fontsize=18)
ax.set_xlabel('$t$', fontsize=16)
ax.set_ylabel('$x$', fontsize=16)
输出如下(假设我们将max_evals设置为1000):
我们可以看到,最初算法从整个范围中均匀取值,但是随着时间的增加以及对于参数在目标函数上效果的学习,算法搜索范围越来越集中到最可能取得最优值的范围-0附近。它仍然探索整个解决方案空间,但不太频繁。
loss vs. val的可视化:
f, ax = plt.subplots(1)
xs = [t['misc']['vals']['x'] for t in trials.trials]
ys = [t['result']['loss'] for t in trials.trials]
ax.scatter(xs, ys, s=20, linewidth=0.01, alpha=0.75)
ax.set_title('$val$ $vs$ $x$ ', fontsize=18)
ax.set_xlabel('$x$', fontsize=16)
ax.set_ylabel('$val$', fontsize=16)
这就是我们期望的,因为函数y(x) = x**2是确定性的。
最后,让我们尝试一个更复杂的示例,使用更多的随机性和更多的参数。
鸢尾花数据集(The Iris Dataset)
在本节中,我们将介绍在经典数据集Iris上使用hyperopt进行参数调优的4个完整示例。我们将介绍k近邻(KNN)、支持向量机(SVM)、决策树和随机森林。注意,由于我们试图最大化交叉验证的准确性(下面代码中的acc),我们必须为hyperopt对这个值进行取负数,因为hyperopt只知道如何最小化函数。最小化函数f等于最大化函数f的复数。
对于这项任务,我们将使用经典的Iris数据集,并进行一些有监督的机器学习。有4个输入特性和3个输出类。这些数据被标记为属于0类、1类或2类,它们映射到不同种类的鸢尾花。输入有4列:萼片长度(sepal length)、萼片宽度(sepal width)、花瓣长度(petal length)和花瓣宽度(pedal width)。输入单位是厘米。我们将使用这4个特性来学习预测三个输出类之一的模型。由于数据是由sklearn提供的,因此它有一个很好的DESCR属性,提供了关于数据集的详细信息。
print iris.feature_names # input names
print iris.target_names # output names
print iris.DESCR # everything else
让我们通过可视化特性和类来更好地了解数据,使用下面的代码。如果还没有安装seaborn,请不要忘记安装pip install seaborn。
import seaborn as sns
sns.set(style="whitegrid", palette="husl")
iris = sns.load_dataset("iris")
print iris.head()
iris = pd.melt(iris, "species", var_name="measurement")
print iris.head()
f, ax = plt.subplots(1, figsize=(15,10))
sns.stripplot(x="measurement", y="value", hue="species", data=iris, jitter=True, edgecolor="white", ax=ax)
如图:
K最近邻(K-Nearest Neighbors)
我们现在使用hyperopt找到k近邻(KNN)机器学习模型的最佳参数。KNN模型根据训练数据集中k个最近的数据点的多数类,将测试集中的数据点进行分类。
关于这个算法的更多信息可以在这里找到。下面的代码包含了我们已经讨论过的所有内容。
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,100))
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print 'best:'
print best
现在我们来看看输出图。y轴是交叉验证得分,x轴是k-最近邻中的k值。下面是代码及其图像:
f, ax = plt.subplots(1)#, figsize=(10,10))
xs = [t['misc']['vals']['n_neighbors'] for t in trials.trials]
ys = [-t['result']['loss'] for t in trials.trials]
ax.scatter(xs, ys, s=20, linewidth=0.01, alpha=0.5)
ax.set_title('Iris Dataset - KNN', fontsize=18)
ax.set_xlabel('n_neighbors', fontsize=12)
ax.set_ylabel('cross validation accuracy', fontsize=12)
当k大于63时,准确率急剧下降。这是由于数据集中每个类的数量。这三个类中的每个类只有50个实例。因此,让我们通过将’n_neighbors’的值限制为较小的值进行深入研究。
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,50))
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print 'best:'
print best
下面是当我们运行相同的可视化代码时得到的结果:
现在我们可以清楚地看到k在k = 4处有一个最佳值。
上面的模型不做任何预处理。让我们对我们的特性进行规范化和缩放看看这是否有帮助。使用这段代码:
# now with scaling as an option
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X_, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,50)),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print 'best:'
print best
像这样画出参数:
parameters = ['n_neighbors', 'scale', 'normalize']
cols = len(parameters)
f, axes = plt.subplots(nrows=1, ncols=cols, figsize=(15,5))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
ys = np.array(ys)
axes[i].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.75, c=cmap(float(i)/len(parameters)))
axes[i].set_title(val)
我们发现,数据的缩放和/或规范化并不能提高预测的准确性。k的最佳值为4,准确率为98.6%。
这对于一个简单模型KNN的参数调优非常有用。让我们看看支持向量机(SVM)能做些什么。
支持向量机 (SVM)
由于这是一个分类任务,我们将使用sklearn的SVC类。这是代码:
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = SVC(**params)
return cross_val_score(clf, X_, y).mean()
space4svm = {
'C': hp.uniform('C', 0, 20),
'kernel': hp.choice('kernel', ['linear', 'sigmoid', 'poly', 'rbf']),
'gamma': hp.uniform('gamma', 0, 20),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4svm, algo=tpe.suggest, max_evals=100, trials=trials)
print 'best:'
print best
parameters = ['C', 'kernel', 'gamma', 'scale', 'normalize']
cols = len(parameters)
f, axes = plt.subplots(nrows=1, ncols=cols, figsize=(20,5))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
axes[i].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.25, c=cmap(float(i)/len(parameters)))
axes[i].set_title(val)
axes[i].set_ylim([0.9, 1.0])
下面是我们得到的:
同样,缩放和规范化也没有帮助。核函数的首选为最佳(linear),最佳C值为1.4168540399911616,最佳伽玛值为15.04230279483486。该参数集的分类准确率达到99.3%。
决策树(Decision Trees)
我们将只尝试对决策树的几个参数进行优化。这是代码。
iris = datasets.load_iris()
X_original = iris.data
y_original = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = DecisionTreeClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4dt = {
'max_depth': hp.choice('max_depth', range(1,20)),
'max_features': hp.choice('max_features', range(1,5)),
'criterion': hp.choice('criterion', ["gini", "entropy"]),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4dt, algo=tpe.suggest, max_evals=300, trials=trials)
print 'best:'
print best
输出如下,准确率为97.3%:
{'max_features': 1, 'normalize': 0, 'scale': 0, 'criterion': 0, 'max_depth': 17}
如图我们可以看到,在不同的尺度值、标准化值和标准值下,性能几乎没有差别。
parameters = ['max_depth', 'max_features', 'criterion', 'scale', 'normalize'] # decision tree
cols = len(parameters)
f, axes = plt.subplots(nrows=1, ncols=cols, figsize=(20,5))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
ys = np.array(ys)
axes[i].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.5, c=cmap(float(i)/len(parameters)))
axes[i].set_title(val)
#axes[i].set_ylim([0.9,1.0])
随机森林(Random Forests)
让我们看看集成分类器Random Forest发生了什么,它只是一组针对不同大小的数据分区训练的决策树,每个分区对一个输出类进行投票,并选择多数派类作为预测。
iris = datasets.load_iris()
X_original = iris.data
y_original = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = RandomForestClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4rf = {
'max_depth': hp.choice('max_depth', range(1,20)),
'max_features': hp.choice('max_features', range(1,5)),
'n_estimators': hp.choice('n_estimators', range(1,20)),
'criterion': hp.choice('criterion', ["gini", "entropy"]),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
best = 0
def f(params):
global best
acc = hyperopt_train_test(params)
if acc > best:
best = acc
print 'new best:', best, params
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4rf, algo=tpe.suggest, max_evals=300, trials=trials)
print 'best:'
print best
同样,我们只得到97.3%的准确率,和决策树一样。
下面是绘制参数的代码:
parameters = ['n_estimators', 'max_depth', 'max_features', 'criterion', 'scale', 'normalize']
f, axes = plt.subplots(nrows=2, ncols=3, figsize=(15,10))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
print i, val
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
ys = np.array(ys)
axes[i/3,i%3].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.5, c=cmap(float(i)/len(parameters)))
axes[i/3,i%3].set_title(val)
#axes[i/3,i%3].set_ylim([0.9,1.0])
所有模型(All Together Now)
虽然自动调优一个模型的参数(例如SVM或KNN)既有趣又有指导意义,但更有用的是一次调优所有的参数并得到一个总体上最好的模型。这使我们能够同时比较所有的参数和所有的模型,这给了我们最好的模型。这是代码。
digits = datasets.load_digits()
X = digits.data
y = digits.target
print X.shape, y.shape
def hyperopt_train_test(params):
t = params['type']
del params['type']
if t == 'naive_bayes':
clf = BernoulliNB(**params)
elif t == 'svm':
clf = SVC(**params)
elif t == 'dtree':
clf = DecisionTreeClassifier(**params)
elif t == 'knn':
clf = KNeighborsClassifier(**params)
else:
return 0
return cross_val_score(clf, X, y).mean()
space = hp.choice('classifier_type', [
{
'type': 'naive_bayes',
'alpha': hp.uniform('alpha', 0.0, 2.0)
},
{
'type': 'svm',
'C': hp.uniform('C', 0, 10.0),
'kernel': hp.choice('kernel', ['linear', 'rbf']),
'gamma': hp.uniform('gamma', 0, 20.0)
},
{
'type': 'randomforest',
'max_depth': hp.choice('max_depth', range(1,20)),
'max_features': hp.choice('max_features', range(1,5)),
'n_estimators': hp.choice('n_estimators', range(1,20)),
'criterion': hp.choice('criterion', ["gini", "entropy"]),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
},
{
'type': 'knn',
'n_neighbors': hp.choice('knn_n_neighbors', range(1,50))
}
])
count = 0
best = 0
def f(params):
global best, count
count += 1
acc = hyperopt_train_test(params.copy())
if acc > best:
print 'new best:', acc, 'using', params['type']
best = acc
if count % 50 == 0:
print 'iters:', count, ', acc:', acc, 'using', params
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space, algo=tpe.suggest, max_evals=1500, trials=trials)
print 'best:'
print best
这段代码需要一段时间才能运行,因为我们增加了计算次数:max_evals=1500。当发现新的最佳精度时,还会增加输出以更新。奇怪的是,为什么使用这种方法没有找到我们在上面找到的最佳模型:SVM的kernel=linear,C=1.416,和gamma=15.042。
总结
我们已经介绍了一些简单的例子,比如最小化确定性线性函数,以及一些复杂的例子,比如调整随机森林参数。hyperopt的文档在这里。另一个关于hyperopt的好博客是FastML的。hyperopt作者撰写的SciPy会议论文是Hyperopt: A Python Library for Optimizing the Hyperparameters of Machine Learning Algorithms,附带一个视频教程。对工程细节的一种更科学的处理方法是Making a Science of Model Search。
这篇文章中的技术可以应用于除机器学习之外的许多领域,例如在epsilon的epsilon-greedy multi-armed bandit调优参数,或传递给图形生成器的参数,以形成具有某些特性的合成网络。稍后我们将对此进行更多的讨论。