多数情况下,超参数的选择是无限的,因此在有限的时间内,除了可以验证人工预设的几种超参数组合外,也可以通过启发式的搜索方法对组合进行调优,这种超参数搜素的方法称为 网格搜索。
一、网格搜索
在所有超参数优化的算法当中,枚举网格搜索是最为基础和经典的方法。在搜索开始之前,我们需要人工将每个超参数的备选值一一列出,多个不同超参数的不同取值之间排列组合,最终将组成一个参数空间(parameter space)。枚举网格搜索算法会将这个参数空间当中所有的参数组合带入模型进行训练,最终选出泛化能力最强的组合作为模型的最终超参数。
对网格搜索而言,如果参数空间中的某一个点指向了损失函数真正的最小值,那枚举网格搜索时一定能够捕捉到该最小值以及对应的参数(相对的,假如参数空间中没有任意一点指向损失函数真正的最小值,那网格搜索就一定无法找到最小值对应的参数组合)。参数空间越大、越密,参数空间中的组合刚好覆盖损失函数最小值点的可能性就会越大。这是说,极端情况下,当参数空间穷尽了所有可能的取值时,网格搜索一定能够找到损失函数的最小值所对应的最优参数组合,且该参数组合的泛化能力一定是强于人工调参的。
由于超参数的空间是无尽的,因此超参数的组合配置只能是“更优解”,没有最优解。通过依靠网格搜索对多种超参数组合的空间进行暴力搜索,每一套超参数组合被代入到学习函数中作为新的模型,为了比较新模型之间的性能,采用交叉验证的方法在多组相同的训练和验证数据集下进行评估。
#从sklearn.datasets里导入20类新闻文本数据
import numpy as np
from sklearn.datasets import fetch_20newsgroups #加载数据集
from sklearn.svm import SVC #支持向量机分类模型
from sklearn.feature_extraction.text import TfidfVectorizer #sklearn.feature_extraction.text导入TfidfVectorizer文本抽取器
from sklearn.pipeline import Pipeline #导入Pipeline
from sklearn.model_selection import train_test_split #用于分割数据集
from sklearn.model_selection import GridSearchCV #导入网格搜索模块
news=fetch_20newsgroups(subset='all')
x_train,x_test,y_train,y_test = train_test_split(news.data[:3000],news.target[:3000],test_size=0.25,random_state=33)
clf =Pipeline([('vect',TfidfVectorizer(analyzer='word',stop_words='english')),('svc',SVC())])
#这里需要试验的2个超参数的个数分别是4,3,SVC_game的参数共有10^-2,10^-1......这样一共有12种的超参数组合,12个不同参数下的模型.
parameters={'svc__gamma':np.logspace(-2,1,4),'svc__C':np.logspace(-2,1,3)}
#将12组参数组合以及初始化的Pipeline 包括3折交叉验证的要求全部告知GridSearchCV
'''注意:refit=True设定表示在交叉验证获取最佳的超参数过程中,程序将会以交叉验证训练集得到的最佳超参数,重新对所有可用的训练集与验证集
进行,作为最终用于性能评估的最佳模型参数'''
gs=GridSearchCV(clf,parameters,verbose=2,refit=True,cv=3) #verbose是否显示处理过程的信息,数值越高,信息越多
time_=gs.fit(x_train,y_train) #执行单线程网格搜索
gs.best_params_,gs.best_score_
print(gs.score(x_test,y_test)) #输出最佳模型在测试集上的准确性
使用单线程的网格搜索技术对朴素贝叶斯模型在文本分类任务中的超参数进行调优,共有12组超参数,运行时间165.40秒,寻找到最佳的超参数组合在测试集上所能达到的最高分类准确性为82.67%
二、并行搜索
由于各个新模型在执行交叉验证的过程中间是相互独立的,所以可以充分利用多核处理器或分布式的计算资源来并行搜索,这样就能成倍节约运算时间。
#从sklearn.datasets里导入20类新闻文本数据
import numpy as np
from sklearn.datasets import fetch_20newsgroups #加载数据集
from sklearn.svm import SVC #支持向量机分类模型
from sklearn.feature_extraction.text import TfidfVectorizer #sklearn.feature_extraction.text导入TfidfVectorizer文本抽取器
from sklearn.pipeline import Pipeline #导入Pipeline
from sklearn.model_selection import GridSearchCV #导入网格搜索模块
import time
start_time = time.time()
news=fetch_20newsgroups(subset='all')
x_train,x_test,y_train,y_test = train_test_split(news.data[:3000],news.target[:3000],test_size=0.25,random_state=33)
clf =Pipeline([('vect',TfidfVectorizer(analyzer='word',stop_words='english')),('svc',SVC())])
#这里需要试验的2个超参数的个数分别是4,3,SVC_game的参数共有10^-2,10^-1......这样一共有12种的超参数组合,12个不同参数下的模型.
parameters={'svc__gamma':np.logspace(-2,1,4),'svc__C':np.logspace(-2,1,3)}
#将12组参数组合以及初始化的Pipeline 包括3折交叉验证的要求全部告知GridSearchCV
'''注意:refit=True设定表示在交叉验证获取最佳的超参数过程中,程序将会以交叉验证训练集得到的最佳超参数,重新对所有可用的训练集与验证集
进行,作为最终用于性能评估的最佳模型参数'''
gs=GridSearchCV(clf,parameters,verbose=2,refit=True,cv=3,n_jobs=-1) #verbose是否显示处理过程的信息,数值越高,信息越多,n_jobs=-1代表使用计算机全部CPU
gs=gs.fit(x_train,y_train) #执行单线程网格搜索
gs.best_params_,gs.best_score_
print(gs.score(x_test,y_test)) #输出最佳模型在测试集上的准确性
print('net_time',time.time()-start_time)
同样是网格搜索,使用多线程并行搜索技术对朴素贝叶斯模型在文本分类任务中的超参数组合进行调优,执行时间39.80秒,提升了4倍的运行速度,模型结果一致.
pd.DataFrame(gs.cv_results_).T #每次取mean_test_score和对应的params,rank_test_score也可知道结果
三、随机搜索
我们使用Scikit-Learn 的RandomizedSearchCV 来寻找最佳的超参数组合,进行模型优化,随机搜索是用交叉验证来评估超参数值的所有组合,当超参数的搜索范围较大时,通常会优先选择使用 RandomizedSearchCV 。它不会尝试所有可能的组合,而是在每次迭代中为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估。这种方法有两个优点,如果运行随机1000个迭代,那么将会探索每个超参数的1000个不同的值,而不是只探索少量的值,并且通过简单地设置迭代次数,可以更好地控制要分配给超参数搜索的计算预算。它相比于网格搜索有着更好的随机性,能够在相同时间内探索更多可能的区域,但也会带来不确定性。
#从sklearn.datasets里导入20类新闻文本数据
import numpy as np
from sklearn.datasets import fetch_20newsgroups #加载数据集
from sklearn.svm import SVC #支持向量机分类模型
from sklearn.feature_extraction.text import TfidfVectorizer #sklearn.feature_extraction.text导入TfidfVectorizer文本抽取器
from sklearn.pipeline import Pipeline #导入Pipeline
from sklearn.model_selection import RandomizedSearchCV #导入随机搜索模块
import time
start_time = time.time()
news=fetch_20newsgroups(subset='all')
x_train,x_test,y_train,y_test = train_test_split(news.data[:3000],news.target[:3000],test_size=0.25,random_state=33)
clf =Pipeline([('vect',TfidfVectorizer(analyzer='word',stop_words='english')),('svc',SVC())])
#这里需要试验的2个超参数的个数分别是4,3,SVC_game的参数共有10^-2,10^-1......这样一共有12种的超参数组合,12个不同参数下的模型.
parameters={'svc__gamma':np.logspace(-2,1,4),'svc__C':np.logspace(-2,1,3)}
#将12组参数组合以及初始化的Pipeline 包括3折交叉验证的要求全部告知GridSearchCV
'''注意:refit=True设定表示在交叉验证获取最佳的超参数过程中,程序将会以交叉验证训练集得到的最佳超参数,重新对所有可用的训练集与验证集
进行,作为最终用于性能评估的最佳模型参数'''
gs=RandomizedSearchCV(clf,parameters,verbose=2,refit=True,cv=3,n_jobs=-1) #verbose是否显示处理过程的信息,数值越高,信息越多,n_jobs=-1代表使用计算机全部CPU
gs=gs.fit(x_train,y_train) #执行单线程网格搜索
gs.best_params_,gs.best_score_
print(gs.score(x_test,y_test)) #输出最佳模型在测试集上的准确性
print('net_time',time.time()-start_time)
全部参数解读如下,其中加粗的是随机网格搜索独有的参数:
Name | Description |
---|---|
estimator | 调参对象,某评估器 |
param_distributions | 全域参数空间,可以是字典或者字典构成的列表 |
n_iter | 迭代次数,迭代次数越多,抽取的子参数空间越大 |
scoring | 评估指标,支持同时输出多个参数 |
n_jobs | 设置工作时参与计算的线程数 |
refit | 挑选评估指标和最佳参数,在完整数据集上进行训练 |
cv | 交叉验证的折数 |
verbose | 输出工作日志形式 |
pre_dispatch | 多任务并行时任务划分数量 |
random_state | 随机数种子 |
error_score | 当网格搜索报错时返回结果,选择’raise’时将直接报错并中断训练过程,其他情况会显示警告信息后继续完成训练 |
return_train_score | 在交叉验证中是否显示训练集中参数得分 |
使用随机搜索多线程并行技术对朴素贝叶斯模型在文本分类任务中的超参数组合进行调优,执行时间32.53秒,比网格搜索并行快7秒多。
四、对半网格搜索HalvingSearchCV
决定枚举网格搜索运算速度的因子:参数空间的大小:参数空间越大,需要建模的次数越多;数据量的大小:数据量越大,每次建模时需要的算力和时间越多。面对枚举网格搜索过慢的问题,sklearn中呈现了两种优化方式:其一是调整搜索空间,其二是调整每次训练的数据。调整搜索空间的方法就是随机网格搜索,而调整每次训练数据的方法就是对半网格搜索。
假设存在数据集 D,从中随机抽样出一个子集 d。如果一组参数在整个数据集 D 上表现较差,那么在子集 d 上也大概率表现不佳;反之,如果一组参数在子集 d 上表现不好,我们也不会信任它在全数据集 D 上的表现。这一假设的核心在于:参数在子集与全数据集上的表现具有一致性。基于此,在网格搜索中,可以使用数据子集来筛选超参数,从而显著加速计算。然而,该假设成立的前提是 子集的分布与全数据集 D 的分布相似。子集分布越接近全数据集,参数在两者上的表现越可能一致。根据随机网格搜索的结论,增大子集可以让分布更接近全数据集,但也会导致训练时间增加。因此,如何在子集大小与计算效率之间找到平衡,是一个需要解决的关键问题。
1、首先从全数据集中无放回随机抽样出一个很小的子集 d 0 d_0 d0,并在 d 0 d_0 d0上验证全部参数组合的性能。根据 d 0 d_0 d0上的验证结果,淘汰评分排在后1/2的那一半参数组合
2、然后,从全数据集中再无放回抽样出一个比 d 0 d_0 d0大一倍的子集 d 1 d_1 d1,并在 d 1 d_1 d1上验证剩下的那一半参数组合的性能。根据 d 1 d_1 d1上的验证结果,淘汰评分排在后1/2的参数组合
3、再从全数据集中无放回抽样出一个比 d 1 d_1 d1大一倍的子集 d 2 d_2 d2,并在 d 2 d_2 d2上验证剩下1/4的参数组合的性能。根据 d 2 d_2 d2上的验证结果,淘汰评分排在后1/2的参数组合……
持续循环。如果使用S代表首次迭代时子集的样本量,C代表全部参数组合数,则在迭代过程中,用于验证参数的数据子集是越来越大的,而需要被验证的参数组合数量是越来越少的:
迭代次数 | 子集样本量 | 参数组合数 |
---|---|---|
1 | S | C |
2 | 2S | 1 2 \frac{1}{2} 21C |
3 | 4S | 1 4 \frac{1}{4} 41C |
4 | 8S | 1 8 \frac{1}{8} 81C |
…… |
当备选参数组合只剩下一组,或剩余可用的数据不足,循环就会停下,具体地来说,当
1
n
\frac{1}{n}
n1C <= 1或者nS > 总体样本量,搜索就会停止。在实际应用时,哪一种停止条件会先被触发,需要看实际样本量及参数空间地大小。同时,每次迭代时增加的样本量、以及每次迭代时不断减少的参数组合都是可以自由设定的。
在这种模式下,只有在不同的子集上不断获得优秀结果的参数组合能够被留存到迭代的后期,最终选择出的参数组合一定是在所有子集上都表现优秀的参数组合。这样一个参数组合在全数据上表现优异的可能性是非常大的,同时也可能展现出比网格/随机搜索得出的参数更大的泛化能力。
- 对半网格搜索的局限性
然而这个过程当中会存在一个问题:子集越大时,子集与全数据集D的分布会越相似,但整个对半搜索算法在开头的时候,就用最小的子集筛掉了最多的参数组合。如果最初的子集与全数据集的分布差异巨大的化,在对半搜索开头的前几次迭代中,就可能筛掉许多对全数据集D有效的参数,因此对半网格搜索最初的子集一定不能太小。从经验来看,对半网格搜索在小型数据集上的表现往往不如随机网格搜索与普通网格搜索。
import numpy as np
from sklearn.datasets import fetch_20newsgroups # 加载数据集
from sklearn.svm import SVC # 支持向量机分类模型
from sklearn.feature_extraction.text import TfidfVectorizer # sklearn.feature_extraction.text导入TfidfVectorizer文本抽取器
from sklearn.pipeline import Pipeline # 导入Pipeline
from sklearn.model_selection import train_test_split # 用于分割数据集
from sklearn.experimental import enable_halving_search_cv # noqa
from sklearn.model_selection import HalvingGridSearchCV # 导入HalvingGridSearchCV
from sklearn.metrics import accuracy_score # 导入准确率评分
import time
start_time = time.time()
# 加载数据
news = fetch_20newsgroups(subset='all')
x_train, x_test, y_train, y_test = train_test_split(news.data[:3000], news.target[:3000], test_size=0.25, random_state=33)
# 创建Pipeline
clf = Pipeline([('vect', TfidfVectorizer(analyzer='word', stop_words='english')), ('svc', SVC())])
# 定义超参数网格
parameters = {'svc__gamma': np.logspace(-2, 1, 4), 'svc__C': np.logspace(-2, 1, 3)}
# parameters = {'svc__gamma': [*range(-2,2,1)], 'svc__C': [*range(-2,2,1)]}
# 配置HalvingGridSearchCV
halving_search = HalvingGridSearchCV(
clf,
param_grid=parameters,
factor=2, # 每轮保留一半候选参数
scoring='accuracy',
n_jobs=-1,
random_state=33,
cv=3, # 3折交叉验证
verbose=2,
refit=True
)
# 执行HalvingGridSearchCV
halving_search.fit(x_train, y_train)
# 输出最佳参数和最佳得分
print("Best Parameters:", halving_search.best_params_)
print("Best Cross-Validation Score:", halving_search.best_score_)
# 输出测试集准确率
y_pred = halving_search.predict(x_test)
print("Test Accuracy:", accuracy_score(y_test, y_pred))
print('net_time',time.time()-start_time)
全部参数如下所示:
Name | Description |
---|---|
estimator | 调参对象,某评估器 |
param_grid | 参数空间,可以是字典或者字典构成的列表 |
factor | 每轮迭代中新增的样本量的比例,同时也是每轮迭代后留下的参数组合的比例 |
resource | 设置每轮迭代中增加的验证资源的类型 |
max_resources | 在一次迭代中,允许被用来验证任意参数组合的最大样本量 |
min_resources | 首次迭代时,用于验证参数组合的样本量r0 |
aggressive_elimination | 是否以全部数被使用完成作为停止搜索的指标,如果不是,则采取措施 |
cv | 交叉验证的折数 |
scoring | 评估指标,支持同时输出多个参数 |
refit | 挑选评估指标和最佳参数,在完整数据集上进行训练 |
error_score | 当网格搜索报错时返回结果,选择’raise’时将直接报错并中断训练过程 其他情况会显示警告信息后继续完成训练 |
return_train_score | 在交叉验证中是否显示训练集中参数得分 |
random_state | 控制随机抽样数据集的随机性 |
n_jobs | 设置工作时参与计算的线程数 |
verbose | 输出工作日志形式 |
由于参数选取较少,优化的best_score均为0.82,从消耗的时间来看:网格搜索>并行搜索>随机搜索>对半搜索。