本文将介绍一种快速有效的方法用于实现机器学习模型的调参。有两种经常使用的调参方法:网格搜索和随机搜索。每一种都有本身的优势和缺点。网格搜索速度慢,但在搜索整个搜索空间方面效果很好,而随机搜索很快,但可能会错过搜索空间中的重要点。幸运的是,还有第三种选择:贝叶斯优化。本文咱们将重点介绍贝叶斯优化的一个实现,一个名为hyperopt的Python模块。html
使用贝叶斯优化进行调参可让咱们得到给定模型的最佳参数,例如逻辑回归模型。这也使咱们可以执行最佳的模型选择。一般机器学习工程师或数据科学家将为少数模型(如决策树,支持向量机和K近邻)执行某种形式(网格搜索或随机搜索)的手动调参,而后比较准确率并选择最佳的一个来使用。该方法可能比较的是次优模型。也许数据科学家找到了决策树的最优参数,但却错过了SVM的最优参数。这意味着他们的模型比较是有缺陷的。若是SVM参数调整得不好,K 近邻可能每次都会击败SVM。贝叶斯优化容许数据科学家找到全部模型的最佳参数,并所以比较最佳模型。这会获得更好的模型选择,由于你比较的是最佳的k近邻和最佳的决策树。只有这样你才能很是自信地进行模型选择,确保选择并使用的是实际最佳的模型。python
本文涵盖的主题有:git
假设你有一个定义在某个范围内的函数,而且想把它最小化。也就是说,你想找到产生最低输出值的输入值。下面的简单例子找到\(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)
输出结果:dom
{'x': 0.000269455723739237}
这有一个更复杂的目标函数:
\(lambda\ 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}
hyperopt模块包含一些方便的函数来指定输入参数的范围。咱们已经见过\(hp.uniform\)。最初,这些是随机搜索空间,但随着hyperopt更多的学习(由于它从目标函数得到更多反馈),经过它认为提供给它最有意义的反馈,会调整并采样初始搜索空间的不一样部分。函数
如下内容将在本文中使用:学习
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'}
若是能看到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对象容许咱们在每一个时间步存储信息。而后咱们能够将它们打印出来,并在给定的时间步查看给定参数的函数评估值。
输出结果:
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模块。咱们不会在这里讨论细节,这是对于须要使用MongoDB进行分布式计算的hyperopt的高级选项,所以须要导入pymongo。回到上面的输出。
咱们看看损失vs值的图
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)
在本节中,咱们将介绍4个使用hyperopt在经典数据集Iris上调参的完整示例。咱们将涵盖K近邻(KNN),支持向量机(SVM),决策树和随机森林。须要注意的是,因为咱们试图最大化交叉验证的准确率(acc请参见下面的代码),而hyperopt只知道如何最小化函数,因此必须对准确率取负。最小化函数f与最大化f的负数是相等的。
对于这项任务,咱们将使用经典的Iris数据集,并进行一些有监督的机器学习。数据集有有4个输入特征和3个输出类别。数据被标记为属于类别0,1或2,其映射到不一样种类的鸢尾花。输入有4列:萼片长度,萼片宽度,花瓣长度和花瓣宽度。输入的单位是厘米。咱们将使用这4个特征来学习模型,预测三种输出类别之一。由于数据由sklearn提供,它有一个很好的DESCR属性,能够提供有关数据集的详细信息。尝试如下代码以得到更多细节信息。
咱们如今将使用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:',best)
如今让咱们看看输出结果的图。y轴是交叉验证分数,x轴是k近邻个数。下面是代码和它的图像:
f, ax = plt.subplots(1)#, figsize=(10,10)) xs = [t['misc']['vals']['n'] 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:',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:',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)能作什么。
因为这是一个分类任务,咱们将使用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:',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,gamma的最佳值是15.04230279483486。这组参数获得了99.3%的分类准确率。
自动调整一个模型的参数(如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:',best)
因为咱们增长了评估数量,此代码须要一段时间才能运行完:\(max\_evals=1500\)。当找到新的最佳准确率时,它还会添加到输出用于更新。好奇为何使用这种方法没有找到前面的最佳模型:参数为kernel=linear,C=1.416,gamma=15.042的SVM。
咱们已经介绍了简单的例子,如最小化肯定的线性函数,以及复杂的例子,如调整SVM的参数。后面读者须要根据本身的需求再去调整选择的参数,也能够基于深度学习模型进行调参。