机器学习(十四):超参数调优进阶_RandomizedSearchCV和HalvingSearchCV

全文共19000余字,预计阅读时间约40~60分钟 | 满满干货,建议收藏!

在这里插入图片描述
代码及数据集下载

一、超参数优化与枚举网格的理论极限

1.1 超参数优化(HPO,HyperParameter Optimization)

超参数优化(HPO,HyperParameter Optimization)是机器学习中的一项关键任务,其主要目标是寻找最优的模型超参数配置,以使模型在特定任务上的性能达到最优。

在机器学习中,有两类参数:

  1. 模型参数:这些是模型在训练过程中学习的参数,例如:在线性回归中的斜率和截距,这些参数是通过梯度下降等优化算法进行更新和学习的。
  2. 超参数:这些参数不能在训练过程中学习,而是需要事先设定。例如,学习率、训练轮数。超参数的设定对模型的性能有重大影响。

超参数优化就是寻找这些超参数的最优设定,以最大化模型在验证集上的性能。理论上来说,当算力与数据足够时,HPO的性能一定是超过人类的。HPO能够降低人为工作量,并且HPO得出的结果比认为搜索的复现可能性更高,所以HPO可以极大程度提升科学研究的复现性和公平性。目前超参数优化算法主要可以分为:

211

1.2 枚举网格的理论极限

超参数调优入门:一文搞懂枚举网格搜索这篇文章中,讲解了什么是超参数,以及如何通过网格搜索进行超参数优化。

在所有超参数优化的算法当中,枚举网格搜索是最为基础和经典的方法。在搜索开始之前,需要人工将每个超参数的备选值一一列出,多个不同超参数的不同取值之间排列组合,最终将组成一个参数空间(parameter space)。枚举网格搜索算法会将这个参数空间当中所有的参数组合带入模型进行训练,最终选出泛化能力最强的组合作为模型的最终超参数。

对网格搜索而言,如果参数空间中的某一个点指向了损失函数真正的最小值,那枚举网格搜索时一定能够捕捉到该最小值以及对应的参数(相对的,假如参数空间中没有任意一点指向损失函数真正的最小值,那网格搜索就一定无法找到最小值对应的参数组合)。

参数空间越大、越密,参数空间中的组合刚好覆盖损失函数最小值点的可能性就会越大。这是说,极端情况下,当参数空间穷尽了所有可能的取值时,网格搜索一定能够找到损失函数的最小值所对应的最优参数组合,且该参数组合的泛化能力一定是强于人工调参的。

然而,网格搜索的缺点也非常明显,尤其是当参数空间较大时,网格搜索需要大量的时间和计算资源,当参数维度上升时,网格搜索所需的计算量更是程指数级上升的。以随机森林为例:

扫描二维码关注公众号,回复: 16369444 查看本文章

只有1个参数n_estimators,备选范围是[50,100,150,200,250,300],需要建模6次。
增加参数max_depth,且备选范围是[2,3,4,5,6],需要建模30次。
增加参数min_sample_split,且备选范围为[2,3,4,5],需要建模120次。

同时,参数优化的目标是找出令模型泛化能力最强的组合,因此需要交叉验证来体现模型的泛化能力,假设交叉验证次数为5,则三个参数就需要建模600次。

在面对超参数众多、且超参数取值可能无限的人工神经网络、融合模型、集成模型时,伴随着数据和模型的复杂度提升,网格搜索所需要的时间会急剧增加,完成一次枚举网格搜索可能需要耗费几天几夜。

为了解决这个问题,研究者提出了随机搜索和逐步削减搜索这两种策略。本篇文章将延续上一篇文章的主题,深入介绍这两种高效的搜索策略,并对它们的性能进行比较。

如果还不清楚枚举网格搜索的,建议看这篇文章:
超参数调优入门:一文搞懂枚举网格搜索

1.3 实操:从<Kaggle比赛案例:房价预测>看GridSearchCV

竞赛目标及数据描述

这个竞赛的主要目标是预测房屋的最终价格,是一个典型的回归问题,因为需要预测一个连续的输出(房价)。

数据集包含79个解释变量,描述了爱荷华州埃姆斯市的住宅的几乎每个方面,包括质量、条件、面积、车库数量、地下室条件等等。这些变量都可以用来预测房屋的最终售价。

数据集分为训练集和测试集。训练集用于构建和训练模型,测试集用于评估模型的性能。训练集包含房屋的特征以及对应的销售价格,而测试集只包含房屋的特征,参赛者需要预测这些房屋的销售价格。

这个竞赛的评价指标是均方根对数误差(Root-Mean-Squared-Error (RMSE) between the logarithm of the predicted value and the logarithm of the observed sales price)。这意味着,预测的价格和实际价格之间的误差会被平方,然后取平均,最后取平方根。使用对数而不是原始价格,可以尽量避免预测价格过高或过低时产生的大误差。

建立benchmark:使用随机森林做枚举网格搜索

Step 1 : 导入基本库

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_validate, KFold, GridSearchCV

import time

Step 2 : 导入数据集(已做完基本处理的训练集,可直接拿来使用)

data = pd.read_csv("../datasets/House Price/train_encode.csv")

X = data.iloc[:,:-1]
y = data.iloc[:,-1]

data

image-20230710101123554

Step 3: 基本的数据探索

# 显示数据集的基本信息(列名、数据类型、非空值数量等)
print(data.info())

# 数据的统计信息
print(data.describe())

# 检查缺失值
print(data.isnull().sum())

# 检查目标列(如果是监督学习的情况)
print(data['SalePrice'].describe())

# 数据分布情况,例如绘制柱状图、箱线图等(需要matplotlib或seaborn库)
plt.hist(data['SalePrice'])
plt.show()

Step 4: 构建参数空间

这个参数网格用于对输入的随机森林参数做暴力搜索,对每一种可能的参数组合进行模型训练,然后根据预定的评分标准选择最优的一组参数。

#参数空间
param_grid_simple = {
    
    "criterion": ["squared_error","poisson"]
                     , 'n_estimators': [*range(20,100,5)]
                     , 'max_depth': [*range(10,25,2)]
                     , "max_features": ["log2","sqrt",16,32,64,"auto"]
                     , "min_impurity_decrease": [*np.arange(0,5,10)]
                    }

Step5 : 实例化及构建网格搜索

设定网格搜索的参数和模型,评分方式为"neg_mean_squared_error",也就是负均方误差,负值是因为sklearn在选择模型时会选择得分最高的模型,所以用负均方误差使得误差越小,得分越高。

model_rf = RandomForestRegressor(random_state=24, verbose=True,)
cv = KFold(n_splits=5, shuffle=True, random_state=24)
search = GridSearchCV(estimator=model_rf,
                     param_grid=param_grid_simple,
                     scoring = "neg_mean_squared_error",
                     verbose = True,
                     cv = cv,
                     n_jobs=-1)

Step 6: 训练并计算用时

进行模型训练并打印出消耗的时间,以"分钟+秒"的格式来展示。注意,divmod()函数会返回两个值,第一个值是商(分钟),第二个值是余数(秒)。

start = time.time()
search.fit(X, y)
end = time.time()

elapsed_time = end - start # 得到的时间是秒级别的
minutes, seconds = divmod(elapsed_time, 60) # 将秒转换为分钟和秒
print(f"Elapsed time: {
      
      int(minutes)} minutes {
      
      int(seconds)} seconds")

image-20230710105010207

在上述过程中,使用了GridSearchCV和五折交叉验证方法,在1536种不同的参数组合中寻找最优参数。总共训练和评估了7680个模型。这个过程总共花费了3分钟2秒的时间。最后,使用找到的最优参数,模型在所有数据上进行了最后的训练,这个过程花费了0.1秒的时间。

Step 7:按最优参数重建模型,评估性能

下面这段代码用来获取GridSearchCV的最优模型,然后计算并打印出最优模型的RMSE,接着用这个最优模型进行交叉验证,并最终计算和打印出训练和测试数据的RMSE,以此来评估模型的性能。

from sklearn.metrics import mean_squared_error
import warnings

# 获取最优模型
best_estimator = search.best_estimator_

# 打印最优模型
print("Best estimator:")
print(best_estimator)

# 获取GridSearchCV的最优分数(注意:这是负的MSE)
best_score = search.best_score_

# 将负的MSE转换为RMSE
rmse = np.sqrt(-best_score)

# 打印RMSE
print(f"RMSE of the best estimator found by GridSearchCV: {
      
      rmse:.4f}")

# 使用最优模型进行交叉验证,返回训练得分
from sklearn.model_selection import cross_validate
scores = cross_validate(best_estimator, X, y, cv=5, scoring='neg_mean_squared_error', return_train_score=True)

# 计算训练和测试的RMSE
train_rmse = np.sqrt(-scores['train_score'].mean())
test_rmse = np.sqrt(-scores['test_score'].mean())

# 打印训练和测试的RMSE
print(f"Train RMSE: {
      
      train_rmse:.4f}")
print(f"Test RMSE: {
      
      test_rmse:.4f}")

有一点需要注意:在计算RMSE时,应用了np.sqrt(-scores)。这是因为GridSearchCV和cross_validate函数在计算“neg_mean_squared_error”得分时会将其取负值(因为在这些函数中,更高的分数代表更好的性能,但在MSE中,较低的值代表更好的性能)。所以在计算RMSE时,需要先取负值,再取平方根。

image-20230710111849016

Step 8:结论输出

213

解释一下:GridSearchCV的过程是基于交叉验证(CV)的,而且对每一组参数,都会进行多次(通常是5次或10次)的验证计算均值,这个过程会在一定程度上减小过拟合的可能性。所以得到的最优分数(27984)是对所有交叉验证集的均值计算得出。

然后,当用这个最优参数重新训练模型,并进行交叉验证时,可能会在训练和测试集上得到不同的结果,因为每次的数据分割(即训练集和测试集的分割)可能都会有所不同。所以在这一步得到的RMSE(29731)可能会有所不同。

这种现象其实是很常见的,因为机器学习模型的性能会受到许多因素的影响,包括数据的分布、模型的复杂性、训练和测试数据的分割方法等。所以即使是同一个模型,也可能在不同的数据集上得到不同的结果。

二、RandomizedSearchCV概述

2.1 RandomizedSearchCV基本概念

如上两节所说的,传统的网格搜索方法(GridSearchCV)通过搜索预先设定的参数空间来寻找最优参数,虽然它能够保证在一定范围内找到最优解,但是当参数空间增大时,其所需的计算时间和资源也会呈指数级增长。例如,如果有10个参数,每个参数有5个可能的取值,那么就会有 5 10 = 9 , 765 , 625 5^{10} = 9,765,625 510=9,765,625种可能的参数组合需要尝试,这是非常耗时的。

仔细看以上使用枚举网格搜索的过程,不难看出:决定枚举网格搜索运算速度的因子一共有两个

1 参数空间的大小:参数空间越大,需要建模的次数越多

2 数据量的大小:数据量越大,每次建模时需要的算力和时间越多

因此,sklearn中的网格搜索优化方法主要包括两类,其一是调整搜索空间,其二是调整每次训练的数据。其中,调整参数空间的具体方法,是放弃原本的搜索中必须使用的全域超参数空间,改为挑选出部分参数组合,构造超参数子空间,并只在子空间中进行搜索。

为了解决这个问题,RandomizedSearchCV应运而生。与GridSearchCV相比,RandomizedSearchCV不会尝试所有可能的参数组合,而是在参数空间中随机抽样一部分组合进行尝试。这样不仅能大大减少搜索的时间和计算资源消耗,而且在许多情况下,RandomizedSearchCV的性能并不逊色于GridSearchCV。

看下代码:

# 假设的参数组合
n_estimators = np.array([50,100,150,200,250,300])
max_depth = np.array([2,3,4,5,6])

# 创建参数组合的网格
param_grid = np.array(np.meshgrid(n_estimators, max_depth)).T.reshape(-1,2)

# 随机选择一部分参数组合来模拟随机搜索
np.random.seed(0)
param_grid_random = param_grid[np.random.choice(param_grid.shape[0], size=8, replace=False), :]

# 创建子图
fig, ax = plt.subplots(1, 2, figsize=(10, 5))

# 左图:网格搜索
ax[0].scatter(param_grid[:, 0], param_grid[:, 1], color='blue')
ax[0].set_title('Grid Search')
ax[0].set_xlabel('n_estimators')
ax[0].set_ylabel('max_depth')

# 右图:随机搜索
ax[1].scatter(param_grid[:, 0], param_grid[:, 1], color='blue', alpha=0.3)  # 画出所有的参数组合
ax[1].scatter(param_grid_random[:, 0], param_grid_random[:, 1], color='red')  # 画出被随机选择的参数组合
ax[1].set_title('Randomized Search')
ax[1].set_xlabel('n_estimators')
ax[1].set_ylabel('max_depth')

plt.tight_layout()
plt.show()

以下图的二维空间为例,在这个n_estimators与max_depth共同组成的参数空间中,n_estimators的取值假设为[50,100,150,200,250,300],max_depth的取值假设为[2,3,4,5,6],则枚举网格搜索必须对30种参数组合都进行搜索。当调整搜索空间,其实可以只抽样出橙色的参数组合作为“子空间”,并只对橙色参数组合进行搜索。如此一来,整体搜索所需的计算量就大大下降了,原本需要30次建模,现在只需要8次建模。

image-20230711082717313

2.2 RandomizedSearchCV工作原理

在sklearn中,随机抽取参数子空间并在子空间中进行搜索的方法叫做随机网格搜索RandomizedSearchCV。由于搜索空间的缩小,需要枚举和对比的参数组的数量也对应减少,整体搜索耗时也将随之减少,因此:

当设置相同的全域空间时,随机搜索的运算速度比枚举网格搜索很多。

当设置相同的训练次数时,随机搜索可以覆盖的空间比枚举网格搜索很多。

同时,绝妙的是,随机网格搜索得出的最小损失与枚举网格搜索得出的最小损失很接近

可以说,是提升了运算速度,又没有过多地伤害搜索的精度。

RandomizedSearchCV的工作原理非常简单。**给定一个预设的参数空间和一个预设的尝试次数,它会在参数空间中随机选取一部分参数组合进行训练和验证,并最终返回在尝试的参数组合中表现最好的那一组参数。**不过,需要注意的是:随机网格搜索在实际运行时,并不是先抽样出子空间,再对子空间进行搜索,而是仿佛“循环迭代”一般,在这一次迭代中随机抽取1组参数进行建模,下一次迭代再随机抽取1组参数进行建模,由于这种随机抽样是不放回的,因此不会出现两次抽中同一组参数的问题。可以控制随机网格搜索的迭代次数,来控制整体被抽出的参数子空间的大小,这种做法往往被称为“赋予随机网格搜索固定的计算量,当全部计算量被消耗完毕之后,随机网格搜索就停止”。

这样的方法基于的一个假设是,不是所有的参数都对模型的性能有同等重要的影响,一些重要的参数的变动会对模型的性能产生比其他参数更大的影响。通过随机采样,有很大概率能找到这些重要参数的优值,从而得到一个性能良好的模型。

2.3 Sklearn中RandomizedSearchCV参数解读

先来看下Sklearn中RandomizedSearchCV都有哪些参数:

image-20230711083421536

解读一下:

135

随机网格搜索这种操作能够有效的根本原因在于:

抽样出的子空间可以一定程度上反馈出全域空间的分布,且子空间相对越大(含有的参数组合数越多),子空间的分布越接近全域空间的分布

当全域空间本身足够密集时,很小的子空间也能获得与全域空间相似的分布

如果全域空间包括了理论上的损失函数最小值,那一个与全域空间分布高度相似的子空间很可能也包括损失函数的最小值,或包括非常接近最小值的一系列次小值

因此,只要子空间足够大,随机网格搜索的效果一定是高度逼近枚举网格搜索的。在全域参数空间固定时,随机网格搜索可以在效率与精度之间做权衡。子空间越大,精度越高,子空间越小,效率越高。

2.4 实操:从<Kaggle比赛案例:房价预测>看RandomizedSearchCV

为了直观理解RandomizedSearchCV和GridSearchCV的差异,还是以Kaggle的房价预测为例,使用随机森林模型。随机森林有许多可以调整的参数,如n_estimators(树的数量),max_features(每个树最大的特征数量),max_depth(树的最大深度)等。如果对每个参数都设定5个可能的取值,那么使用GridSearchCV需要训练和验证5^3 = 125次。但是,如果我们使用RandomizedSearchCV,并设定尝试次数为30次,那么我们只需要训练和验证30次,大大减少了计算的复杂度,同时也有可能得到一个与GridSearchCV相近的结果。

还是使用1.3节的相同数据

Step 1 : 导入基本库

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_validate, KFold, GridSearchCV

import time

Step 2 : 导入数据集(已做完基本处理的训练集,可直接拿来使用)

data = pd.read_csv("../datasets/House Price/train_encode.csv")

X = data.iloc[:,:-1]
y = data.iloc[:,-1]

data

image-20230710101123554

Step 3: 构建相同的全域参数空间

#创造参数空间 - 使用与网格搜索时完全一致的空间,以便于对比
param_grid_simple = {"criterion": ["squared_error","poisson"]
                     , 'n_estimators': [*range(20,100,5)]
                     , 'max_depth': [*range(10,25,2)]
                     , "max_features": ["log2","sqrt",16,32,64,"auto"]
                     , "min_impurity_decrease": [*np.arange(0,5,10)]
                    }

Step 4:实例化及构建随机网格搜索,子空间大小仅设置为800(网格搜索的搜索参数是1536个)

model_rf1 = RandomForestRegressor(random_state=24, verbose=True,)
cv = KFold(n_splits=5, shuffle=True, random_state=24)

#定义随机搜索
search1 = RandomizedSearchCV(estimator=model_rf1
                            ,param_distributions=param_grid_simple
                            ,n_iter = 800 #子空间的大小是全域空间的一半左右
                            ,scoring = "neg_mean_squared_error"
                            ,verbose = True
                            ,cv = cv
                            ,random_state=24
                            ,n_jobs=-1
                           )

Step 5: 训练并计算用时

进行模型训练并打印出消耗的时间,以"分钟+秒"的格式来展示。注意,divmod()函数会返回两个值,第一个值是商(分钟),第二个值是余数(秒)。

start = time.time()
search1.fit(X, y)
end = time.time()

elapsed_time = end - start # 得到的时间是秒级别的
minutes, seconds = divmod(elapsed_time, 60) # 将秒转换为分钟和秒
print(f"Elapsed time: {
      
      int(minutes)} minutes {
      
      int(seconds)} seconds")

image-20230711091512571

在上述过程中,使用了RandomizedSearchCV和五折交叉验证方法,在800种不同的参数组合中寻找最优参数。总共训练和评估了4000个模型。这个过程总共花费了1分33秒的时间。最后,使用找到的最优参数,模型在所有数据上进行了最后的训练,这个过程花费了0.1秒的时间。

Step 6:按最优参数重建模型,评估性能

下面这段代码用来获取RandomizedSearchCV的最优模型,然后计算并打印出最优模型的RMSE,接着用这个最优模型进行交叉验证,并最终计算和打印出训练和测试数据的RMSE,以此来评估模型的性能。

from sklearn.metrics import mean_squared_error
import warnings

# 获取最优模型
best_estimator = search1.best_estimator_

# 打印最优模型
print("Best estimator:")
print(best_estimator)

# 获取GridSearchCV的最优分数(注意:这是负的MSE)
best_score = search1.best_score_

# 将负的MSE转换为RMSE
rmse = np.sqrt(-best_score)

# 打印RMSE
print(f"RMSE of the best estimator found by GridSearchCV: {
      
      rmse:.4f}")

# 使用最优模型进行交叉验证,返回训练得分
scores = cross_validate(best_estimator, X, y, cv=5, scoring='neg_mean_squared_error', return_train_score=True)

# 计算训练和测试的RMSE
train_rmse = np.sqrt(-scores['train_score'].mean())
test_rmse = np.sqrt(-scores['test_score'].mean())

# 打印训练和测试的RMSE
print(f"Train RMSE: {
      
      train_rmse:.4f}")
print(f"Test RMSE: {
      
      test_rmse:.4f}")

image-20230711091956533

Step 7:结论输出

136

总的来说,随机网格搜索在相对较少的时间内找到了和枚举网格搜索同样优秀的模型,这体现了随机网格搜索在大规模参数空间搜索时的优势。然而,对于小规模的参数空间,枚举网格搜索可能会更为精确。另外,当前最优模型可能存在一些过拟合,可能需要进一步调整模型的参数或者采用一些防止过拟合的策略,例如增加模型的正则化强度,或者使用更复杂的模型结构。

三、RandomizedSearchCV的优秀特性

3.1 子空间越大,精度越高,子空间越小,效率越高

在2.3节中,有这样一个结论:只要子空间足够大,随机网格搜索的效果一定是高度逼近枚举网格搜索的。在全域参数空间固定时,随机网格搜索可以在效率与精度之间做权衡

验证一下,设置更大的搜索空间,然后再做随机网格搜索,别的代码不需要改变,只需要把Step 3: 构建相同的全域参数空间这一过程修改一下:

#创造参数空间 - 让整体参数空间变得更密
param_grid_simple = {'n_estimators': [*range(80,100,1)]
                     , 'max_depth': [*range(10,25,1)]
                     , "max_features": [*range(10,20,1)]
                     , "min_impurity_decrease": [*np.arange(0,5,10)]
                    }

model_rf1 = RandomForestRegressor(random_state=24, verbose=True,)
cv = KFold(n_splits=5, shuffle=True, random_state=24)

#定义随机搜索
search1 = RandomizedSearchCV(estimator=model_rf1
                            ,param_distributions=param_grid_simple
                            ,n_iter = 1536 #使用与枚举网格搜索类似的拟合次数
                            ,scoring = "neg_mean_squared_error"
                            ,verbose = True
                            ,cv = cv
                            ,random_state=24
                            ,n_jobs=-1
                           )

start = time.time()
search1.fit(X, y)
end = time.time()

elapsed_time = end - start # 得到的时间是秒级别的
minutes, seconds = divmod(elapsed_time, 60) # 将秒转换为分钟和秒
print(f"Elapsed time: {int(minutes)} minutes {int(seconds)} seconds")

看下执行结果:

image-20230711104610725

image-20230711104633582

再次进行对比:

137

当全域参数空间增大之后,随即网格搜索可以使用与小空间上的网格搜索相似或更少的时间,来探索更密集/更大的空间,从而获得更好的结果。

3.2 接受连续型的参数空间

对于网格搜索来说,参数空间中的点是分布均匀、间隔一致的,因为网格搜索无法从某种“分布”中提取数据,只能使用组合好的参数组合点,而随机搜索却可以接受“分布”作为输入。

如果损失函数的最低点位于两组参数之间,在这种情况下,枚举网格搜索是100%不可能找到最小值的。但对于随机网格搜索来说,由于是一段分布上随机选择参数点,因此在同样的参数空间中,取到更好的值的可能性更大。

看下代码:

import scipy #使用scipy建立分布

param_grid_simple = {
    
    'n_estimators': [*range(80,100,1)]
                     , 'max_depth': [*range(10,25,1)]
                     , "max_features": [*range(10,20,1)]
                     , "min_impurity_decrease": scipy.stats.uniform(0,50)
                    }

model_rf1 = RandomForestRegressor(random_state=24, verbose=True,)
cv = KFold(n_splits=5, shuffle=True, random_state=24)

#定义随机搜索
search1 = RandomizedSearchCV(estimator=model_rf1
                            ,param_distributions=param_grid_simple
                            ,n_iter = 1536 #使用与枚举网格搜索类似的拟合次数
                            ,scoring = "neg_mean_squared_error"
                            ,verbose = True
                            ,cv = cv
                            ,random_state=24
                            ,n_jobs=-1
                           )

start = time.time()
search1.fit(X, y)
end = time.time()

elapsed_time = end - start # 得到的时间是秒级别的
minutes, seconds = divmod(elapsed_time, 60) # 将秒转换为分钟和秒
print(f"Elapsed time: {
      
      int(minutes)} minutes {
      
      int(seconds)} seconds")

看下结果:

image-20230711105821576

image-20230711105841472

再次进行对比:

138

理论上来说,当枚举网格搜索所使用的全域参数空间足够大/足够密集时,枚举网格搜索的最优解是随机网格搜索的上限,因此理论上随机网格搜索不会得到比枚举网格搜索更好的结果

但现实中的问题是,由于枚举网格搜索的速度太慢,因此枚举网格搜索的全域参数空间往往无法设置得很大,也无法设置得很密集,因此网格搜索的结果很难接近理论上的最优值。当随机网格搜索将空间设置更大、更密集时,就可以捕获更广空间的分布,也自然就可能捕获到理论上的最优值了。

四、HalvingSearchCV概述

6.1 HalvingSearchCV基本概念

枚举网格搜索过慢的问题,sklearn中有两种优化方式:其一是调整搜索空间,其二是调整每次训练的数据。调整搜索空间的方法就是随机网格搜索,而调整每次训练数据的方法就是对半网格搜索。

HalvingSearchCV中,首先使用一小部分数据对所有的参数组合进行快速评估,然后仅保留表现最好的一部分参数组合,对其使用更多的数据进行进一步的评估。通过这种方式,可以快速排除表现差的参数组合,从而将更多的资源集中在有希望的参数组合上。

举个例子:

假设现在有数据集 D D D,从数据集 D D D中随机抽样出一个子集 d d d。如果一组参数在整个数据集 D D D上表现较差,那大概率这组参数在数据集的子集 d d d上表现也不会太好。反之,如果一组参数在子集 d d d上表现不好,也不会信任这组参数在全数据集 D D D上的表现。

参数在子集与全数据集上反馈出的表现一致,如果这一假设成立,那在网格搜索中,比起每次都使用全部数据来验证一组参数,可以考虑只带入训练数据的子集来对超参数进行筛选,这样可以极大程度地加速运算。

但在现实数据中,这一假设要成立是有条件的,即任意子集的分布都与全数据集D的分布类似。当子集的分布越接近全数据集的分布,同一组参数在子集与全数据集上的表现越有可能一致。根据之前在随机网格搜索中得出的结论:子集越大、其分布越接近全数据集的分布,但是大子集又会导致更长的训练时间,因此为了整体训练效率,不可能无限地增大子集。这就出现了一个矛盾:大子集上的结果更可靠,但大子集计算更缓慢。

6.2 HalvingSearchCV工作原理

对半网格搜索算法设计了一个精妙的流程,可以很好的权衡子集的大小与计算效率问题,来看具体的流程:

  1. 在第一阶段,HalvingSearchCV使用一小部分训练数据对所有的参数组合进行评估。这一阶段可以快速完成,但由于只使用了一小部分数据,所以评估的结果可能不太准确。
  2. 在第二阶段,HalvingSearchCV根据第一阶段的评估结果,仅保留表现最好的一部分参数组合。这一阶段将使用更多的数据对这些参数组合进行评估,从而得到更准确的结果。
  3. HalvingSearchCV会重复以上的过程,每个阶段都会淘汰一半的参数组合,直到只剩下一个参数组合为止。这个参数组合就是HalvingSearchCV所选出的最佳参数组合。

举个例子:

假设现在有数据集 D D 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
……
(当C无法被除尽时,则向上取整)

备选参数组合只剩下一组,或剩余可用的数据不足,循环就会停下。

具体地来说, 1 n \frac{1}{n} n1C <= 1或者nS > 总体样本量,搜索就会停止

在这种模式下,只有在不同的子集上不断获得优秀结果的参数组合能够被留存到迭代的后期,最终选择出的参数组合一定是在所有子集上都表现优秀的参数组合。这样一个参数组合在全数据上表现优异的可能性是非常大的,同时也可能展现出比网格/随机搜索得出的参数更大的泛化能力。

这种方式的优点是,可以快速排除表现差的参数组合,而不必对它们进行完整的训练和评估。因此,HalvingSearchCV通常比传统的网格搜索和随机搜索更快,尤其在参数空间较大或者训练数据集较大的情况下。

6.3 Sklearn中HalvingSearchCV参数解读

先来看下Sklearn中HalvingSearchCV都有哪些参数:

image-20230711125108735

解读一下:

139

其中需要重点理解的参数是:

factor

每轮迭代中新增的样本量的比例,同时也是每轮迭代后留下的参数组合的比例。例如,当factor=2时,下一轮迭代的样本量会是上一轮的2倍,每次迭代后有1/2的参数组合被留下。如果factor=3时,下一轮迭代的样本量会是上一轮的3倍,每次迭代后有1/3的参数组合被留下。该参数通常取3时效果比较好。

resource

设置每轮迭代中增加的验证资源的类型,输入为字符串。默认是样本量,输入为"n_samples",也可以是任意集成算法当中输入正整数的弱分类器,例如"n_estimators"或者"n_iteration"。

min_resource

首次迭代时,用于验证参数组合的样本量r0。可以输入正整数,或两种字符串"smallest",“exhaust”。
输入正整数n,表示首次迭代时使用n个样本。
输入"smallest",则根据规则计算r0:

当资源类型是样本量时,对回归类算法,r0 = 交叉验证折数n_splits * 2

当资源类型是样本量时,对分类算法,r0 = 类别数量n_classes_ * 交叉验证折数n_splits * 2

当资源类型不是样本量时,等于1

输入"exhaust",则根据迭代最后一轮的最大可用资源倒退r0。例如,factor=2, 样本量为1000时,一共迭代3次时,则最后一轮迭代的最大可用资源为1000,倒数第二轮为500,倒数第三轮(第一轮)为250。此时r0 = 250。"exhaust"模式下最有可能得到好的结果,不过计算量会略大,计算时间会略长。

6.4 HalvingSearchCV的局限性

HalvingSearchCV过程会存在一个问题:子集越大时,子集与全数据集D的分布会越相似,但整个对半搜索算法在开头的时候,就用最小的子集筛掉了最多的参数组合。如果最初的子集与全数据集的分布差异巨大的化,在对半搜索开头的前几次迭代中,就可能筛掉许多对全数据集D有效的参数,因此对半网格搜索最初的子集一定不能太小。

6.5 实操:从<Kaggle比赛案例:房价预测>看HalvingSearchCV

根据上面提到的局限性,在初始子集一定不能太小、且对半搜索的抽样是不放回抽样的大前提下,整体数据的样本量必须要很大

在实际建模经验中,对半网格搜索在小型数据集上的表现往往不如随机网格搜索与普通网格搜索,比如上面用到的Kaggle 房价预测数据集,使用对半网格搜索,会发现其搜索结果还不如枚举网格搜索、且搜索时间长。但在大型数据集上(比如,样本量过w的数据集上),对半网格搜索则展现出运算速度和精度上的巨大优势。

因此在对半网格搜索实现时,使用一组拓展的房价数据集,有2w9条样本。

Step 1 : 导入基本库

import numpy as np
import pandas as pd
import matplotlib as mlp
import matplotlib.pyplot as plt
import time
from sklearn.ensemble import RandomForestRegressor
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import KFold, HalvingGridSearchCV, cross_validate, RandomizedSearchCV

import re
import sklearn

Step 2 : 导入数据集(已做完基本处理的训练集,可直接拿来使用)

data1 = pd.read_csv("./datasets/House Price/big_train.csv",index_col=0)

data1 

X = data1.iloc[:,:-1]
y = data1.iloc[:,-1]

image-20230711124804444

Step 3: 构建相同的全域参数空间

#创造参数空间 - 使用与网格搜索时完全一致的空间,以便于对比
param_grid_simple = {"criterion": ["squared_error","poisson"]
                     , 'n_estimators': [*range(20,100,5)]
                     , 'max_depth': [*range(10,25,2)]
                     , "max_features": ["log2","sqrt",16,32,64,"auto"]
                     , "min_impurity_decrease": [*np.arange(0,5,10)]
                    }

Step 4:确定factor和参数组合

对于对半网格搜索应用来说,最困难的部分是决定搜索本身复杂的参数组合。

在调参时,如果希望参数空间中的备选组合都能够被充分验证,则迭代次数不能太少(例如,只迭代3次),因此factor不能太大。但如果factor太小,又会加大迭代次数,同时拉长整个搜索的运行时间。同时,迭代次数还会影响最终能够使用的数据量,以及迭代完毕之后还需进一步验证的参数组合数量,两者都不能太少。因此,一般在使用对半网格搜索时,需考虑以下三个点:

1、min_resources的值不能太小,且在全部迭代过程结束之前,希望使用尽量多的数据

2、迭代完毕之后,剩余的验证参数组合不能太多,10以下最佳,如果无法实现,则30以下也可以接受

3、迭代次数不能太多,否则时间可能会太长

factor = 1.5
n_samples = X.shape[0]
min_resources = 500
space = 1536

for i in range(100):
    if (min_resources*factor**i > n_samples) or (space/factor**i < 1):
        break
    print(i+1,"本轮迭代样本:{}".format(min_resources*factor**i)
          ,"本轮验证参数组合:{}".format(space//factor**i + 1))

image-20230711130110342

Step 5:实例化及构建对半网格搜索

model_rf2 = RandomForestRegressor(random_state=24, verbose=True,)
cv = KFold(n_splits=5, shuffle=True, random_state=24)

#定义对半搜索
search2 = HalvingGridSearchCV(estimator=model_rf2
                            ,param_grid=param_grid_simple
                            ,factor=1.5
                            ,min_resources=500
                            ,scoring = "neg_mean_squared_error"
                            ,verbose = True
                            ,random_state=1412
                            ,cv = cv
                            ,n_jobs=-1)

Step 6: 训练并计算用时

进行模型训练并打印出消耗的时间,以"分钟+秒"的格式来展示。注意,divmod()函数会返回两个值,第一个值是商(分钟),第二个值是余数(秒)。

start = time.time()
search2.fit(X, y)
end = time.time()

elapsed_time = end - start # 得到的时间是秒级别的
minutes, seconds = divmod(elapsed_time, 60) # 将秒转换为分钟和秒
print(f"Elapsed time: {
      
      int(minutes)} minutes {
      
      int(seconds)} seconds")

image-20230711132344156

因为使用的是不同数据集,所以就无法再与GridSearchCV和RandomizedSearchCV比较了。

五、结语

本文从理论角度分析了传统的枚举网格搜索的局限性,介绍了两种更加高效的超参数搜索方法:RandomizedSearchCV和HalvingSearchCV。详解了它们的工作原理,参数设置以及使用场景,并通过具体的Kaggle比赛案例,帮助读者更直观地理解这两种方法的优势与局限性。

阅读完本篇文章后,您应该能够掌握到RandomizedSearchCV和HalvingSearchCV的基本概念和工作原理,并能够在具体的机器学习问题中,根据自身的需求和计算资源的限制,选择和使用合适的超参数搜索方法。此外,还将学会如何通过选择不同的参数空间,来平衡搜索的精度和效率。

下一篇文章将介绍一种更加先进、智能的超参数优化方法——贝叶斯优化。这种方法利用先验知识和贝叶斯推断,能够在更小的搜索空间内,找到更优的参数组合。

最后,感谢您阅读这篇文章!如果您觉得有所收获,别忘了点赞、收藏并关注我,这是我持续创作的动力。您有任何问题或建议,都可以在评论区留言,我会尽力回答并接受您的反馈。如果您希望了解某个特定主题,也欢迎告诉我,我会乐于创作与之相关的文章。

谢谢您的支持,期待与您共同成长!

猜你喜欢

转载自blog.csdn.net/Lvbaby_/article/details/131666028