【科学炼丹指南】机器学习最科学、最有效的参数优化全流程实现方法
机器学习模型都有很多超参数需要调整,比如神经网络的层数、节点数,树模型的最大深度、叶子节点数等。调参的目的是在限定的训练时间和计算资源内,通过调整这些超参数,使模型在验证集上的性能指标达到最优,如最小化预测误差,最大化准确率等。
但是由于超参数组合数量极大,模式性能高度非凸,手工调参搜索空间巨大,效率低下。因此会使用一些调参策略与工具自动搜索,如网格搜索、随机搜索、贝叶斯优化等。还会使用技巧缩小搜索空间,降低调参难度,比如启发式初始化。
但即便使用自动调参方法,调参过程也非常耗时,需要大量的计算资源。当数据集复杂、模式复杂时,这个过程更加困难。因此模型调参始终是机器学习中一个技术难点,也是提高模型效果的关键所在。如何高效快速找到最优的参数组合,是每个机器学习研究者和实践者都要面临的挑战。
网格搜索已经不能满足选择的研究水平了,下面介绍一种高效的优化算法
- 贝叶斯优化Bayesian Optimization 利用高斯过程拟合超参数对目标函数的映射,找出目标函数的极大值点。效率高于网格搜索和随机搜索。已成功应用于神经网络、XGBoost等的调参。
- 元学习 Metalearning 利用之前模型调参的经验和知识,快速确定新问题的较优参数区。一定程度上实现参数迁移。已有应用CNN调参的成功案例。
- 自动机器学习 AutoML 针对不同模型,开发专门的自动调参系统,整合多种调参算法,如自然演化、强化学习等。一键自动搜索到较优参数,大大简化操作。已有面向深度学习、树模型和传统ML的AutoML工具。
- 神经架构搜索 NAS 使用强化学习、进化算法等搜索神经网络架构和超参数。可自动设计出性能优异的模型。已成功设计出竞赛冠军级CNN模型。
- 多目标Pareto优化 考虑模型多个性能指标的权衡,采用多目标搜索找到pareto前沿解。EXPAND系统实现了CNN模型的pareto调参。
虽然,目前有自动化机器学习,但是仍不能满足一些复杂的数据
关于如何调参,如何科学化的调参,从数据分析再到一个数据科学家的进阶之路,这个一个不断学习和上升的高度。
很多时候我们拿到一个拿到一个数据,内心迫切的希望马上用一种模型来使其得出高的评价指标,但是从来没有思考如何科学高效的调整模型的参数。
模型的准确度不光和模型内部的参数有关,还和数据有关。
-
数据的预处理工作:缺失值、异常值、无关变量
-
数据归约:数据标准化、归一化
-
特征工程:特征编码,特征构造,分箱等操作
-
特征筛选:特征重要性,自动完成特征筛选
-
模型参数:逐步迭代调参、网格搜索、随机搜索、贝叶斯优化、optuna等
-
在参数优化的时候,K折交叉验证是需要派上用场的
-
模型评估:相关的评估指标,专业的模型可视化,模型解释库,特征影响趋势图
如果那你是一个经常和数据打交道的工程师,这一套操作你应该早就熟记于心了,本质上数据的建模和机器学习的流程就是按照这个思路进行的,但是每一个模块如何去实现,采用怎么样的方法,是需要思考,且结合实际情况和数据的情况进行实施的。
本文我将详细的介绍关于在机器学习中,优化参数的相关进阶思路和实现代码
分类任务的常见评估指标:
准确率 (Accuracy)
scoring='accuracy'
衡量模型预测正确的比例。
精确度 (Precision)
scoring='precision'
衡量模型预测为正类的样本中实际正类的比例。
召回率 (Recall)
scoring='recall'
衡量实际正类中被模型正确预测的比例。
F1 分数 (F1 Score)
scoring='f1'
精确度和召回率的调和平均数。
AUC-ROC 曲线下面积 (Area Under the Curve)
scoring='roc_auc'
衡量模型在不同阈值下假阳性率和真阳性率的综合性能。
回归任务的常见评估指标:
均方误差 (Mean Squared Error, MSE)
scoring='neg_mean_squared_error'
衡量模型预测值与真实值之间差异的平均值。通常取负值,因为优化算法通常是寻找最大值。
均方根误差 (Root Mean Squared Error, RMSE)
scoring='neg_root_mean_squared_error'
MSE的平方根。
平均绝对误差 (Mean Absolute Error, MAE)
scoring='neg_mean_absolute_error'
衡量预测值与真实值绝对差异的平均值。
R² 或决定系数 (Coefficient of Determination)
scoring='r2'
衡量模型对数据变异性的解释程度。
import optuna
def optuna_objective(trial):
# 定义参数空间,整数类型就是,上下限加步长,浮点数类型,上下限(可省略步长:在搜索域中不断地寻找),文本类型的可以调参
lambda_l1 = trial.suggest_float("lambda_l1", 0.0, 10.0)
bagging_fraction = trial.suggest_float("bagging_fraction", 0.1, 1.0)
bagging_freq = trial.suggest_int("bagging_freq", 0, 10)
num_leaves = trial.suggest_int("num_leaves", 10, 100)
feature_fraction = trial.suggest_float("feature_fraction", 0.1, 1.0)
max_depth = trial.suggest_int("max_depth", -1, 20)
max_bin = trial.suggest_int("max_bin", 10, 500)
num_iterations = trial.suggest_int("num_iterations", 100, 1000)
learning_rate = trial.suggest_float("learning_rate", 0.01, 0.3)
# feature_selection_method = trial.suggest_categorical('feature_selection', ['PCA', 'LDA', 'None'])
# 定义LightGBM模型,不需要优化的参数直接写固定值
reg = lgb.LGBMRegressor(
lambda_l1=lambda_l1,
bagging_fraction=bagging_fraction,
bagging_freq=bagging_freq,
num_leaves=num_leaves,
feature_fraction=feature_fraction,
max_depth=max_depth,
max_bin=max_bin,
num_iterations=num_iterations,
learning_rate=learning_rate,
random_state=42,
n_jobs=-1,
)
# 交叉验证过程
#交叉验证过程,输出负均方根误差(-RMSE)
#optuna同时支持最大化和最小化,因此如果输出-RMSE,则选择最大化
#如果选择输出绝对值的负RMSE,则选择最小化
cv = KFold(n_splits=5, shuffle=True, random_state=42)
validation_loss = cross_validate(
reg,
X_train,
y_train,
scoring="neg_root_mean_squared_error", #指标可以换(优化方向都是最大值,所以取一个负值),分类就是正常的
cv=cv,
n_jobs=-1,
error_score='raise'
)
datas = {
"训练集上的平均RMSE": np.mean(abs(validation_loss["train_score"])),
"验证集上的平均RMSE": np.mean(abs(validation_loss["test_score"])),
"平均拟合时间": np.mean(validation_loss["fit_time"]),
"平均评分时间": np.mean(validation_loss["score_time"])
}
# 返回测试集上的平均RMSE
return np.mean(abs(validation_loss["test_score"]))
def optimizer_optuna(n_trials, algo):
#定义使用TPE或者GP
if algo == "TPE":
algo = optuna.samplers.TPESampler(n_startup_trials = 15, n_ei_candidates = 20)
elif algo == "GP":
from optuna.integration import SkoptSampler
import skopt
algo = SkoptSampler(skopt_kwargs={'base_estimator':'GP', #选择高斯过程
'n_initial_points':30, #初始观测点10个
'acq_func':'EI'} #选择的采集函数为EI,期望增量
)
#实际优化过程,首先实例化优化器
study = optuna.create_study(sampler = algo #要使用的具体算法
, direction="minimize" #优化的方向,可以填写minimize或maximize
# 取决于您的目标函数。如果是损失函数,通常使用 minimize;如果是性能度量(如准确率),使用 maximize。
)
#开始优化,n_trials为允许的最大迭代次数
#由于参数空间已经在目标函数中定义好,因此不需要输入参数空间
study.optimize(optuna_objective #目标函数
, n_trials=n_trials #最大迭代次数(包括最初的观测值的)
, show_progress_bar=True #要不要展示进度条呀?
)
#可直接从优化好的对象study中调用优化的结果
#打印最佳参数与最佳损失值
print("\n","\n","best params: ", study.best_trial.params,
"\n","\n","best score: ", study.best_trial.values,
"\n")
return study.best_trial.params, study.best_trial.values
import time
from sklearn.model_selection import KFold, cross_validate
def optimized_optuna_search_and_report(n_trials, algo):
start_time = time.time()
# 进行贝叶斯优化
best_params, best_score = optimizer_optuna(n_trials, algo)
end_time = time.time()
elapsed_time = (end_time - start_time) / 60 # 转换为分钟
print(f"Optimization completed in 0.0586 minutes.")
return best_params, best_score
# 执行优化
best_params, best_score = optimized_optuna_search_and_report(300, "TPE")
# 打印最佳参数和分数
print("\n","\n","best params: ", best_params,
"\n","\n","best score: ", best_score,
"\n")
-
优化器选择
TPE (Tree-structured Parzen Estimator):
适用场景: 当你的参数空间相对简单,或者你不确定哪个采样器最好时,TPE 是一个不错的起点。例如,如果你在优化一个常规的机器学习模型(如随机森林、支持向量机)的参数,如 max_depth, n_estimators, C 等,TPE 通常能够提供良好的结果。
例子: 假设你正在优化一个简单的随机森林分类器,其中你想找到最佳的 n_estimators(树的数量)和 max_depth(树的最大深度)。这是一个标准的优化问题,TPE 是一个合适的选择。GP (Gaussian Process):
适用场景: 如果你的参数空间非常复杂,或者参数之间存在相互依赖(即一个参数的最佳值取决于另一个参数的值),那么使用 GP 可能更合适。例如,在优化深度学习模型的超参数时,像学习率和批量大小这样的参数可能具有复杂的交互效应。
例子: 你正在优化一个深度神经网络,需要调整的参数包括学习率、批量大小、不同层的神经元数等。这些参数可能相互依赖,影响模型的性能,GP 可以更好地理解这些复杂的关系。 -
启动试验次数
高值的影响:
增加 n_startup_trials 或 n_initial_points 的值意味着在开始使用所选优化算法之前,系统将执行更多的随机搜索。这有助于更全面地探索参数空间,可能会发现一些非直观的参数组合,它们在实际应用中表现出色。
例子: 在优化一个复杂模型时,比如深度学习模型,你可能希望初始时进行更广泛的搜索,以避免过早陷入局部最优解。在这种情况下,提高 n_initial_points 会有所帮助。 -
采集函数
‘EI’ (Expected Improvement):
‘EI’ 是一种平衡探索和利用的策略。它不仅考虑了目前表现最好的参数,还考虑了那些尚未充分探索的区域。
例子: 假设你正在优化一个有很多局部最优解的模型。‘EI’ 将帮助你在已知表现良好的参数和那些可能隐藏着更好解决方案的未知区域之间取得平衡。 -
优化方向
minimize vs maximize:
如果你的目标是减少某种误差(如均方误差),你会使用 minimize。相反,如果你的目标是提高某种性能指标(如准确率),你会使用 maximize。
例子: 当优化一个回归模型的 RMSE 时,你会选择 minimize。而在优化分类模型的准确率时,你会选择 maximize。 -
试验次数
n_trials 的影响:
增加 n_trials 的值意味着优化器将尝试更多的参数组合。这可能会提高找到更优解的机会,但也增加了计算成本和时间。
例子: 如果你有足够的计算资源,并且正在处理一个复杂的问题(如优化一个深度学习模型),增加 n_trials 可能会有所帮助。这样可以确保算法有足够的机会探索和利用参数空间。 -
优化器选择
随机森林:
由于随机森林的参数空间通常相对简单(例如 n_estimators, max_depth),TPE 是一个合适的选择。它可以高效地搜索这些参数。XGBoost 和 LightGBM:
这些模型的参数空间可能更为复杂,尤其是当涉及到许多细微的调整时(例如 learning_rate, max_depth, subsample)。在这种情况下,GP 可以帮助理解这些参数之间的复杂关系。但是,TPE 仍然是一个非常有效的选择,尤其是在初步探索阶段。 -
启动试验次数
对于相对直接的模型(如随机森林),可能不需要太多的随机搜索,因此可以设置较低的 n_startup_trials。
对于参数空间更为复杂的模型(如 XGBoost 和 LightGBM),可能会从较高的 n_startup_trials 或 n_initial_points 中受益,以确保充分探索参数空间。 -
采集函数
在所有三种模型中,使用 ‘EI’ 作为采集函数是一个均衡的选择,因为它可以帮助在已知有效的参数和未充分探索的区域之间取得平衡。
-
优化方向
对于回归任务(例如使用随机森林、XGBoost 或 LightGBM 进行回归),通常会选择 minimize,例如最小化 RMSE。
对于分类任务,你会选择 maximize,例如最大化准确率。 -
试验次数
对于较简单的模型(如随机森林),可能不需要很多试验次数就能找到良好的参数。
对于 XGBoost 和 LightGBM 这样的复杂模型,增加 n_trials 可能有助于更好地探索参数空间,尤其是在处理复杂数据集时。
实例应用
例如,当使用 Optuna 优化 XGBoost 模型时,你可能会开始于使用 TPE 作为采样器,设置一个适中的 n_startup_trials(例如20),采用 ‘EI’ 作为采集函数,并根据你的具体任务选择 minimize 或 maximize。随着优化过程的进行,根据需要调整试验次数 n_trials。
有时候实验证明,通过optuna的方法进行参数优化,效果可能还不如手动的逐步迭代调参(在训练集上训练,在测试集上评估最佳的参数范围值)
很大原因是:optuna的迭代次数设置的不够大,导致无法搜索完整
参数搜索范围:
Optuna 优化的效果很大程度上取决于定义的参数搜索范围。如果搜索范围设置不当,可能无法探索到最佳参数区域。相比之下,手动迭代调参可能基于先验知识和经验,更直接地接近最优参数。
迭代次数(n_trials):
Optuna 需要足够的迭代次数来探索参数空间。如果 n_trials 设置得太小,可能导致搜索不充分。
随机性:
机器学习模型的训练过程通常涉及随机性。不同的随机种子可能导致不同的搜索路径,从而影响最终结果。
模型复杂性和数据:
对于复杂的数据集或模型,找到最优的超参数可能更具挑战性。可能需要更细致的搜索策略。
-
过拟合风险:
手动迭代调参时,可能无意中对验证集过拟合,尤其是如果基于验证集性能不断调整参数。而 Optuna 通常通过交叉验证等方法减少这种风险。 -
稳定性和泛化能力:
Optuna 优化可能更注重模型的泛化能力。虽然手动调参可能在特定数据集上获得更低的误差,但 Optuna 找到的参数可能在不同数据集上表现得更稳定。
所以不要只关注与结果,我们要注意方法,不要为了降低误差去忽略本身的逻辑和规则,我们需要稳定高效的模型。
每文一语
学习新的知识,需要新的思考!