学习器模型中一般有两类参数,一类是可以从数据中学习估计得到,我们称为参数(Parameter)。还有一类参数时无法从数据中估计,只能靠人的经验进行设计指定,我们称为超参数(Hyper parameter)。超参数是在开始学习过程之前设置值的参数。相反,其他参数的值通过训练得出。
定义关于模型的更高层次的概念,如复杂性或学习能力 不能直接从标准模型培训过程中的数据中学习,需要预先定义 可以通过设置不同的值,训练不同的模型和选择更好的测试值来决定 参数空间的搜索一般由以下几个部分构成:
在机器学习模型中,比如随机森林中决策树的个数,人工神经网络模型中的隐藏层层数和每层的节点个数,正则项中常数大小等等,它们都需要事先指定。超参数选择不恰当,就会出现欠拟合或者过拟合的问题。在Scikit-Learn中,超参数是在学习过程开始之前设置其值的参数。典型的例子包括支持向量机里的C、kernel、gamma等。
class sklearn.svm.SVC(*, C=1.0, kernel='rbf', degree=3, gamma='scale', coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape='ovr', break_ties=False, random_state=None)
使用过程中可以使用estimator.get_params() 获得学习器模型的超参数列表和当前取值。
Sklearn提供了两种通用的超参数优化方法:网格搜索与随机搜索。
在机器学习里,通常来说我们不能将全部用于数据训练模型,否则我们将没有数据集对该模型进行验证,从而评估我们的模型的预测效果。为了解决这一问题,有如下常用的方法:
这种是方法最简单的,也是很容易就想到的。我们可以把整个数据集分成两部分,一部分用于训练,一部分用于验证,这也就是我们经常提到的训练集(training set)和测试集(test set)。
不过,这个简单的方法存在两个弊端:
基于这样的背景,有人就提出了Cross-Validation方法,也就是交叉验证。
LOOCV即(Leave-one-out cross-validation)。像Test set approach一样,LOOCV方法也包含将数据集分为训练集和测试集这一步骤。但是不同的是,我们只用一个数据作为测试集,其他的数据都作为训练集,并将此步骤重复N次(N为数据集的数据数量)。
假设我们现在有n个数据组成的数据集,那么LOOCV的方法就是每次取出一个数据作为测试集的唯一元素,而其他n-1个数据都作为训练集用于训练模型和调参。结果就是我们最终训练了n个模型,每次都能得到一个MSE。而计算最终test MSE则就是将这n个MSE取平均。
比起test set approach,LOOCV有很多优点。首先它不受测试集合训练集划分方法的影响,因为每一个数据都单独的做过测试集。同时,其用了n-1个数据训练模型,也几乎用到了所有的数据,保证了模型的bias更小。不过LOOCV的缺点也很明显,那就是计算量过于大,是test set approach耗时的n-1倍。
K折交叉验证,和LOOCV的不同在于,我们每次的测试集将不再只包含一个数据,而是多个,具体数目将根据K的选取决定。比如,如果K=5,那么我们利用五折交叉验证的步骤就是:
不难理解,其实LOOCV是一种特殊的K-fold Cross Validation(K=N)。最后K的选取是一个Bias和Variance的trade-off。K越大,每次投入的训练集的数据越多,模型的Bias越小。但是K越大,又意味着每一次选取的训练集之前的相关性越大(考虑最极端的例子,当k=N,也就是在LOOCV里,每次都训练数据几乎是一样的)。而这种大相关性会导致最终的test error具有更大的Variance。一般K值选择5或10。
我们在选择超参数有两个途径:1)凭经验;2)选择不同大小的参数,带入到模型中,挑选表现最好的参数。通过途径2选择超参数时,人力手动调节注意力成本太高,非常不值得。For循环或类似于for循环的方法受限于太过分明的层次,不够简洁与灵活,注意力成本高,易出错。GridSearchCV 称为网格搜索交叉验证调参,它通过遍历传入的参数的所有排列组合,通过交叉验证的方式,返回所有参数组合下的评价指标得分。
GridSearchCV听起来很高大上,其实就是暴力搜索。注意的是,该方法在小数据集上很有用,数据集大了就不太适用了。数据量比较大的时候可以使用一个快速调优的方法——坐标下降。它其实是一种贪心算法:拿当前对模型影响最大的参数调优,直到最优化;再拿下一个影响最大的参数调优,如此下去,直到所有的参数调整完毕。这个方法的缺点就是可能会调到局部最优而不是全局最优,但是省时间省力。
class sklearn.model_selection.GridSearchCV(estimator, param_grid, scoring=None, n_jobs=None, refit=True, cv='warn', verbose=0, pre_dispatch='2*n_jobs', error_score='raise-deprecating', return_train_score='warn')
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVR
from sklearn import datasets
dataset = datasets.load_iris()
X = dataset.data
y = dataset.target
grid = GridSearchCV(
estimator=SVR(kernel='rbf'),
param_grid={
'C': [0.1, 1, 10, 100],
'epsilon': [0.0001, 0.001, 0.01, 0.1, 1, 10],
'gamma': [0.001, 0.01, 0.1, 1]
},
cv=5, scoring='neg_mean_squared_error', verbose=0, n_jobs=-1)
grid.fit(X, y)
print(grid.best_score_)
print(grid.best_params_)
我们在搜索超参数的时候,如果超参数个数较少(三四个或者更少),那么我们可以采用网格搜索,一种穷尽式的搜索方法。但是当超参数个数比较多的时候,我们仍然采用网格搜索,那么搜索所需时间将会指数级上升。所以有人就提出了随机搜索的方法,随机在超参数空间中搜索几十几百个点,其中就有可能有比较小的值。这种做法比上面稀疏化网格的做法快,而且实验证明,随机搜索法结果比稀疏网格法稍好。
RandomizedSearchCV使用方法和类GridSearchCV 很相似,但他不是尝试所有可能的组合,而是通过选择每一个超参数的一个随机值的特定数量的随机组合,这个方法有两个优点:
相比于整体参数空间,可以选择相对较少的参数组合数量。如果让随机搜索运行,它会探索每个超参数的不同的值 可以方便的通过设定搜索次数,控制超参数搜索的计算量。添加参数节点不会影响性能,不会降低效率。RandomizedSearchCV的使用方法其实是和GridSearchCV一致的,但它以随机在参数空间中采样的方式代替了GridSearchCV对于参数的网格搜索,在对于有连续变量的参数时,RandomizedSearchCV会将其当做一个分布进行采样进行这是网格搜索做不到的,它的搜索能力取决于设定的n_iter参数。
class sklearn.model_selection.RandomizedSearchCV(estimator, param_distributions, *, n_iter=10, scoring=None, n_jobs=None , refit=True, cv=None, verbose=0, pre_dispatch='2*n_jobs', random_state=None, error_score=nan, return_train_score=False)
from scipy.stats import randint as sp_randint
from sklearn.model_selection import RandomizedSearchCV
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
# 载入数据
digits = load_digits()
X, y = digits.data, digits.target
# 建立一个分类器或者回归器
clf = RandomForestClassifier(n_estimators=20)
# 给定参数搜索范围:list or distribution
param_dist = {"max_depth": [3, None], # 给定list
"max_features": sp_randint(1, 11), # 给定distribution
"min_samples_split": sp_randint(2, 11), # 给定distribution
"bootstrap": [True, False], # 给定list
"criterion": ["gini", "entropy"]} # 给定list
# 用RandomSearch+CV选取超参数
n_iter_search = 20
random_search = RandomizedSearchCV(clf, param_distributions=param_dist, n_iter=n_iter_search, cv=5, iid=False)
random_search.fit(X, y)
print(random_search.best_score_)
print(random_search.best_params_)
贝叶斯优化用于机器学习调参由J. Snoek(2012)提出,主要思想是,给定优化的目标函数(广义的函数,只需指定输入和输出即可,无需知道内部结构以及数学性质),通过不断地添加样本点来更新目标函数的后验分布(高斯过程,直到后验分布基本贴合于真实分布。简单的说,就是考虑了上一次参数的信息,从而更好的调整当前的参数。
让我们构造一个函数c(x)或者一个接收输入x的模型,如下图所示为c(x)的形状。当然,优化器并不知道该函数,称之为“目标函数”。
贝叶斯优化通过代理优化的方式来完成任务。代理函数通过采样点模拟构造(见下图)。
根据代理函数,我们大致可以确定哪些点是可能的最小值。然后再这些点附近做更多的采样,并随之更新代理函数。
每一次迭代,我们都会继续观察当前的代用函数,通过采样了解更多感兴趣的区域,并更新函数。需要注意的是,代用函数在数学上的表达方式将大大降低评估成本。经过一定的迭代次数后,我们注定要到达一个全局最小值,除非函数的形状非常诡异。
让我们仔细看看代用函数,通常用高斯过程来表示,它可以被认为是掷骰子,返回与给定数据点(如sin、log)拟合的函数,而不是1到6的数字。这个过程会返回几个函数,这些函数都附有概率。为什么用高斯过程,而不是其他的曲线拟合方法来模拟代用函数,有一个很好的理由:它是贝叶斯性质的。代用函数–表示为概率分布,即先验–被更新为 “获取函数”。这个函数负责在勘探和开发的权衡中提出新的测试点。
一个鼓励过多的开发和过少探索的获取函数将导致模型只停留在它首先发现的最小值(通常是局部的–“只去有光的地方”)。一个鼓励相反的获取函数将不会首先停留在一个最小值,本地或全球。在微妙的平衡中产生良好的结果。acquisition 函数,我们将其表示为a(x),必须同时考虑开发和探索。常见的获取函数包括预期改进和最大改进概率,所有这些函数都是在给定先验信息(高斯过程)的情况下,衡量特定投入在未来可能得到回报的概率。
Hyperopt是一个强大的Python库,用于超参数优化,由jamesbergstra开发。Hyperopt使用贝叶斯优化的形式进行参数调整,允许你为给定模型获得最佳参数。它可以在大范围内优化具有数百个参数的模型。
hyperopt有不同的函数来指定输入参数的范围,这些是随机搜索空间。选择最常用的搜索选项:
你可以使用的其他选项包括:
这是一个最小化函数,它从搜索空间接收超参数值作为输入并返回损失。这意味着在优化过程中,我们使用选定的超参数值训练模型并预测目标特征,然后评估预测误差并将其返回给优化器。优化器将决定要检查哪些值并再次迭代。你将在一个实际例子中学习如何创建一个目标函数。
Trials对象用于保存所有超参数、损失和其他信息,这意味着你可以在运行优化后访问它们。此外,trials 可以帮助你保存和加载重要信息,然后继续优化过程。
在理解了Hyperopt的重要特性之后,下面将介绍Hyperopt的使用方法。
from sklearn import datasets
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
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()
# 定义参数空间
space_knn = {
'n_neighbors': hp.choice('n_neighbors', range(1, 100))
}
# 定义最小化函数(目标函数)
def fn_knn(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK} # hyperopt最小化函数,所以在acc中添加了负号
# 实例化Trial 对象,对模型进行微调,然后用其超参数值打印出最佳损失
trials = Trials()
best = fmin(fn_knn, space_knn, algo=tpe.suggest, max_evals=100, trials=trials)
print("Best: {}".format(best))
print(trials.results) # 搜索期间“objective”返回的词典列表。
除了Hyperopt外,贝叶斯优化方法的Python包还有:
遗传算法试图将自然选择机制应用于机器学习环境。它受到达尔文自然选择过程的启发,因此通常也称为进化算法。假设我们创建了具有一些预定义超参数的N个机器学习模型。然后,我们可以计算每个模型的准确性,并决定只保留一半模型(性能最好的模型)。现在,我们可以生成具有与最佳模型相似的超参数的后代,以便再次获得N个模型的种群。在这一点上,我们可以再次计算每个模型的准确性,并在定义的世代中重复该循环。这样,只有最佳模型才能在流程结束时生存下来。
TPOT是一种基于遗传算法优化机器学习管道(pipeline)的Python自动机器学习工具。简单来说,就是TPOT可以智能地探索数千个可能的pipeline,为数据集找到最好的pipeline,从而实现机器学习中最乏味的部分。
更重要地是,一旦TPOT完成搜索,TPOT同时也提供了Python代码。通过这个代码,我们可以具体地知道TPOT获得最优性能时的具体pipeline的内容,这对于后续修改是十分方便的!
TPOT是在sklearn的基础之上做的封装库。其主要封装了sklearn的模型相关模块、processesing模块和feature_selection模块,所以TPOT的主要功能是集中在使用pipeline的方式完成模型的数据预处理、特征选择和模型选择方面。此外,我们还发现了TPOT已经对xgboost进行了支持。
虽然TPOT使用遗传算法代替了传统的网格搜索进行超参数选择,但由于默认初始值的随机性,在少量的进化(迭代)次数下,TPOT最终选择的模型往往并不相同。
计算效率问题。作者在代码中写道:进化(迭代)次数和每一代保留的个体数量值越多,最终得模型得分会越高。但这同样也会导致耗时很长。如果使用相当复杂的数据集或运行TPOT短时间,不同的TPOT运行可能会导致不同的流水线推荐。TPOT的优化算法本质上是随机的,这意味着它使用随机性(部分地)来搜索可能的流水线空间。当两个TPOT运行推荐不同的管道时,这意味着TPOT运行由于时间不够而不收敛,或者多个管道在数据集上执行的次数大致相同。这实际上是一个优于固定网格搜索技术的优点:TPOT是一个助手,它通过探索您可能从未考虑过的流水线配置来提供解决如何解决特定机器学习问题的想法,然后将微调留给更受约束的参数调整技术,例如网格搜索。
使用TPOT(版本0.9.5)开发模型需要把握以下几点:
示例代码:
from tpot import TPOTClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
iris = load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
tpot = TPOTClassifier(generations=5, population_size=50, verbosity=2, n_jobs=-1)
tpot.fit(X_train, y_train)
print(tpot.score(X_test, y_test))
TPOT的主要参数:
使用这个迭代过程,我们选出最佳配置。准备遗传算法的结果一般取决于初始状态。因此,它随机产生的初始种群影响输出,重新运行相同的设置可能会输出不同的结果。