贝叶斯优化python包_《用贝叶斯优化进行超参数调优》

TPE CMAES 网格搜索 随机搜索 贝叶斯优化

用贝叶斯优化进行超参数调优

@QI ZHANG · JUL 12, 2019 · 7 MIN READ

超参数调优一直是机器学习里比较intractable的问题,繁多的超参数以及指数型爆炸的参数空间,往往让人无从下手。调参是一个很枯燥的过程,而且最后也不一定有很好的reward。很多的机器学习工程师也会戏称自己是”调参民工”,”炼丹师”……

超参数(Hyper-parameters):Hyper-parameters are parameters that are not directly learnt within estimators.

超参数的优化可以看做是这样一个方程:

???=argmin??∈??(??)x?=arg?minx∈Xf(x)

其中??(??)f(x) 表示目标函数,用于衡量误差,可以是MSE, RMSE, MAE等(如果是accuracy等指标可以将其添加负号求最小值),???x?是使得??(??)f(x)最小的参数组合,理论上我们的目标就是要找到这个???x?,但是要找到这样一个全局最优解几乎是不可能的。首先这个??(??)f(x)是个黑盒子,我们没法直接进行优化求解。事实上,我们每次为了得到??(??)f(x),需要经过模型训练和评估,非常耗时,尤其对于一些深度学习模型,这个过程会特别漫长。我们只能在有限的计算资源和时间内,得到一些相对的局部最优解。

一般的调参方法有下面几种:

手动调参(Manual Search)

网格搜索(Grid Search)

随机搜索(Randomized Search)

贝叶斯优化(Bayesian Optimization)

1. 手动调参

对于手动调参,会对模型最重要的一些参数基于经验进行调节。比如lightgbm的叶num_leaves, learning_rate, feature_fraction, lambda_l1,lambda_l2, min_data_in_leaf等。

2. Randomized Search vs Grid Search

Grid Search会对定义的参数空间进行暴力搜索。网格搜索速度慢,但在搜索整个搜索空间方面效果很好。Randomized Search是从定义的参数空间中进行采样,然后训练。随机搜索很快,但可能会错过搜索空间中的重要点。

事实上,调参的时候,不需要遍历模型的所有参数。事实上,影响效果的往往只有其中的几个参数,一般对这些参数进行Randomized Search或者Grid Search即可。具体可以查看模型文档,或相关文献。

# Comparing randomized search and grid search for hyperparameter estimation

import numpy as np

from time import time

from scipy.stats import randint as sp_randint

from sklearn.model_selection import GridSearchCV

from sklearn.model_selection import RandomizedSearchCV

from sklearn.datasets import load_digits

from sklearn.ensemble import RandomForestClassifier

# get some data

digits = load_digits()

X, y = digits.data, digits.target

# build a classifier

clf = RandomForestClassifier(n_estimators=20)

# Utility function to report best scores

def report(results, n_top=3):

for i in range(1, n_top + 1):

candidates = np.flatnonzero(results[‘rank_test_score‘] == i)

for candidate in candidates:

print("Model with rank: {0}".format(i))

print("Mean validation score: {0:.3f} (std: {1:.3f})".format(

results[‘mean_test_score‘][candidate],

results[‘std_test_score‘][candidate]))

print("Parameters: {0}".format(results[‘params‘][candidate]))

print("")

# specify parameters and distributions to sample from

param_dist = {"max_depth": [3, None],

"max_features": sp_randint(1, 11),

"min_samples_split": sp_randint(2, 11),

"bootstrap": [True, False],

"criterion": ["gini", "entropy"]}

# run randomized search

n_iter_search = 20

random_search = RandomizedSearchCV(clf, param_distributions=param_dist,

n_iter=n_iter_search, cv=5)

start = time()

random_search.fit(X, y)

print("RandomizedSearchCV took %.2f seconds for %d candidates"

" parameter settings." % ((time() - start), n_iter_search))

report(random_search.cv_results_)

# use a full grid over all parameters

param_grid = {"max_depth": [3, None],

"max_features": [1, 3, 10],

"min_samples_split": [2, 3, 10],

"bootstrap": [True, False],

"criterion": ["gini", "entropy"]}

# run grid search

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=5)

start = time()

grid_search.fit(X, y)

print("GridSearchCV took %.2f seconds for %d candidate parameter settings."

% (time() - start, len(grid_search.cv_results_[‘params‘])))

report(grid_search.cv_results_)

Output:

RandomizedSearchCV took 5.55 seconds for 20 candidates parameter settings.

Model with rank: 1

Mean validation score: 0.939 (std: 0.024)

Parameters: {‘bootstrap‘: False, ‘criterion‘: ‘entropy‘, ‘max_depth‘: None, ‘max_features‘: 7, ‘min_samples_split‘: 3}

Model with rank: 2

Mean validation score: 0.933 (std: 0.022)

Parameters: {‘bootstrap‘: False, ‘criterion‘: ‘gini‘, ‘max_depth‘: None, ‘max_features‘: 6, ‘min_samples_split‘: 6}

Model with rank: 3

Mean validation score: 0.930 (std: 0.031)

Parameters: {‘bootstrap‘: True, ‘criterion‘: ‘gini‘, ‘max_depth‘: None, ‘max_features‘: 6, ‘min_samples_split‘: 6}

GridSearchCV took 16.95 seconds for 72 candidate parameter settings.

Model with rank: 1

Mean validation score: 0.937 (std: 0.019)

Parameters: {‘bootstrap‘: False, ‘criterion‘: ‘entropy‘, ‘max_depth‘: None, ‘max_features‘: 10, ‘min_samples_split‘: 2}

Model with rank: 2

Mean validation score: 0.935 (std: 0.020)

Parameters: {‘bootstrap‘: False, ‘criterion‘: ‘gini‘, ‘max_depth‘: None, ‘max_features‘: 10, ‘min_samples_split‘: 2}

Model with rank: 3

Mean validation score: 0.930 (std: 0.029)

Parameters: {‘bootstrap‘: False, ‘criterion‘: ‘entropy‘, ‘max_depth‘: None, ‘max_features‘: 10, ‘min_samples_split‘: 3}

1.1 Grid Search

from sklearn.model_selection import GridSearchCV

def func_grid_search(model, X_train, y_train, param_grid, cv=5, n_jobs=-1):

"""

model: model instance

para_grid: dict type, params searching grid.

para_grid = {‘n_estimators‘:[100,200,500],

‘max_depth‘: [5, 8, 10, 15],

‘max_features‘: [0.80, 0.90, 0.95],

‘min_samples_split‘:[2, 5, 8],

‘min_samples_leaf‘: [1, 3, 5]}

"""

gs = GridSearchCV(model, param_grid, cv=cv, n_jobs=n_jobs)

gs.fit(X_train, y_train.ravel())

print(gs.best_params_)

print(gs.best_score_)

# print(gs.cv_results_)

bst_model = gs.best_estimator_

return bst_model

1.2 Randomized Search

from sklearn.model_selection import RandomizedSearchCV

from scipy.stats import randint as sp_randint

def func_random_search(model, X_train, y_train, param_dist, n_iter=20, cv=5, n_jobs=-1):

"""

# parameters for GridSearchCV

# specify parameters and distributions to sample from

param_dist = {"max_depth": [3, 5],

"max_features": sp_randint(1, 11),

"min_samples_split": sp_randint(2, 11),

"min_samples_leaf": sp_randint(1, 11),

"bootstrap": [True, False]

}

"""

rs = RandomizedSearchCV(model, param_dist, n_iter=n_iter, cv=cv, n_jobs=n_jobs)

rs.fit(X_train, y_train.ravel())

print(rs.best_params_)

print(rs.best_score_)

# print(rs.cv_results_)

bst_model = rs.best_estimator_

return bst_model

Grid Search太慢了,并不是很实用,一般会对参数空间先调一个粗粒度的格点搜索,然后根据结果进行细粒度的调整。而Randomized Search在迭代一定的次之后也可能实现较好的效果,值得更多的尝试。

3. Bayesian Optimization

Grid Search和Randomized Search虽然可以让整个调参过程自动化,但它们无法从之前的调参结果中获取信息,可能会尝试很多无效的参数空间。而贝叶斯优化,会对上一次的评估结果进行追踪,建立一个概率模型,反应超参数在目标函数上表现的概率分布,即??(score|hyperparameters)P(score|hyperparameters),可用于指导下一次的参数选择。

贝叶斯优化可以更好的trade off Exploration&Exploitation,而且适用于随机、非凸、不连续方程的优化。具体过程可用一句话概括为:对目标函数建立概率模型,通过这个概率模型得到最promising的参数组合,用于最终目标函数的评估。

Sequential Model-Based Optimization(SMBO) 是贝叶斯优化更具体的表现形式,可认为它们是等价的。一般会有以下几个过程:

给定要搜索的超参数空间

定义一个目标函数用于评估优化

建立目标函数的surrogate model

建立一个评估surrogate model,作为选择超参数的标准(选择方程)

获取score和hyperparameter的样本用于更新 surrogate model

贝叶斯优化模型主要的区分是代理函数(surrogate function)的差异。surrogate model一般有 Gaussian Process, Random Forest和Tree Parzen Estimator (TPE) 这几种。常见的框架有Spearmint, Hyperopt, SMAC, MOE, BayesianOptimization, skopt等,它们的对比如下表:

librarysurrogate function

Spearmint

Gaussian Process

Hyperopt

Tree Parzen Estimator (TPE)

SMAC

Random Forest

Optunity包括多种超参数调优的方法:

下面以Hyperopt为例说明贝叶斯优化的具体应用。

对于一个优化问题我们可以分为4个部分:

优化的目标函数

优化的参数空间

超参数优化方程,建立代理函数(surrogate function),并用其决定下一次尝试的参数组合

Trials: 记录每次尝试的loss、参数及更多额外信息(可DIY),可以记录整个迭代的过程,用于回测。

空间定义:

from hyperopt import STATUS_OK, fmin, tpe, Trials, hp

import xgboost as xgb

import logging

from timeit import default_timer as timer

import os

from functools import partial

import pandas as pd

import numpy as np

MAX_EVALS = 100 # 迭代次数

NFOLDS = 5 # K-FOLD

FOLDS = None # 自定义的FOLDS,优先级高于NFOLDS

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

XGB_SPACE = {

‘booster‘: ‘gbtree‘,

‘random_state‘: 2019,

‘eval_metric‘: ‘rmse‘,

‘n_jobs‘: -1,

‘learning_rate‘: 0.05,

‘subsample‘: hp.uniform(‘subsample‘, 0.1, 1.0),

‘colsample_bytree‘: hp.uniform(‘colsample_bytree‘, 0.1, 1.0),

‘max_depth‘: hp.quniform(‘max_depth‘, 5, 30, 1),

‘gamma‘: hp.uniform(‘gamma‘, 0.0, 2.0),

‘min_child_weight‘: hp.uniform(‘min_child_weight‘, 0.0, 5.0),

‘reg_alpha‘: hp.uniform(‘reg_alpha‘, 0.0, 3.0),

‘reg_lambda‘: hp.uniform(‘reg_lambda‘, 0.0, 3.0)

}

定义优化的目标函数:

def objective_base(params,

train_set,

folds=None,

nfold=5,

writetoFile=True):

"""

Objective function for Gradient Boosting Machine Hyperparameter Optimization

Args:

folds: This argument has highest priority over other data split arguments.

Return:

"""

# Keep track of evals

global _ITERATION

_ITERATION += 1

# Make sure parameters that need to be integers are integers

for parameter_name in [

‘num_leaves‘, ‘max_depth‘, ‘bagging_freq‘, ‘min_data_in_leaf‘,

‘min_samples_split‘, ‘min_samples_leaf‘

]:

if parameter_name in params:

params[parameter_name] = int(params[parameter_name])

start = timer()

logging.info(f"{_ITERATION} ITERATION")

logging.info(f"params:\n{params}")

cv_dict = xgb.cv(params,

train_set,

num_boost_round=5000,

nfold=nfold,

stratified=False,

folds=folds,

early_stopping_rounds=100,

as_pandas=False,

verbose_eval=10,

seed=0,

shuffle=False)

# Extract the min rmse, Loss must be minimized

loss = np.min(cv_dict[‘test-rmse-mean‘])

# Boosting rounds that returned the lowest cv rmse

n_estimators = int(np.argmin(cv_dict[‘test-rmse-mean‘])+1)

run_time = timer() - start

# Write to the csv file (‘a‘ means append)

if writetoFile:

random_datetime = str(int(time.time()))

hyper_base_path = os.path.join(BASE_DIR, ‘hyperopt_output‘)

trial_file = os.path.join(hyper_base_path, ‘trials.csv‘)

trial_file_rename = os.path.join(hyper_base_path,

‘trials_%s.csv‘ % random_datetime)

if not os.path.exists(hyper_base_path):

os.makedirs(hyper_base_path)

print(

"No trial file directory exists, will be created..."

)

if os.path.exists(trial_file) and _ITERATION == 1:

print("Trial file exists, will be renamed...")

os.rename(trial_file, trial_file_rename)

assert os.path.exists(

trial_file

) == False, "Trial file still exists, rename failed..."

# File to save first results

of_connection = open(trial_file, ‘w‘)

writer = csv.writer(of_connection)

# Write the headers to the file

writer.writerow(

[‘loss‘, ‘params‘, ‘iteration‘, ‘estimators‘, ‘train_time‘])

of_connection.close()

of_connection = open(trial_file, ‘a‘)

writer = csv.writer(of_connection)

writer.writerow([loss, params, _ITERATION, n_estimators, run_time])

# Dictionary with information for evaluation

return {

‘loss‘: loss,

‘params‘: params,

‘iteration‘: _ITERATION,

‘estimators‘: n_estimators,

‘train_time‘: run_time,

‘status‘: STATUS_OK

}

定义前处理和后处理模块:

def build_train_set(X_train, y_train):

isX_df = isinstance(X_train, pd.DataFrame)

isY_sr = isinstance(y_train, pd.Series)

isY_df = isinstance(y_train, pd.DataFrame)

if isY_df:

raise TypeError(

f"y_train is df, with the shape {y_train.shape}, which is not supportable now."

)

if isX_df ^ isY_sr:

raise TypeError(f"X_train and y_train have different types!")

if isX_df:

train_set = xgb.DMatrix(X_train.values, y_train.values)

else:

train_set = xgb.DMatrix(X_train, y_train)

return train_set

def post_hyperopt(bayes_trials, train_set, folds=None, nfold=5):

# get best params

bayes_results = pd.DataFrame(bayes_trials.results)

bayes_results = bayes_results.sort_values(by=‘loss‘)

bayes_results.reset_index(drop=True, inplace=True)

best_params = bayes_results.loc[0, ‘params‘]

# get best loss and trees

best_params[‘learning_rate‘] = 0.01

# Perform n_folds cross validation

cv_dict = xgb.cv(best_params,

train_set,

num_boost_round=5000,

folds=folds,

nfold=nfold,

stratified=False,

shuffle=False,

early_stopping_rounds=100,

as_pandas=False,

verbose_eval=10,

seed=2019)

# Extract the min rmse, Loss must be minimized

loss = np.min(cv_dict[‘test-rmse-mean‘])

# Boosting rounds that returned the lowest cv rmse

n_estimators = int(np.argmin(cv_dict[‘test-rmse-mean‘]) + 1)

best_params[‘n_estimators‘] = n_estimators

logging.info(f"best loss: {loss}, best n_estimators: {n_estimators}")

logging.info(f"best params: {best_params}")

return best_params, loss

定义主函数:

def main_tuning_with_bo(X_train,

y_train,

max_evals=MAX_EVALS,

folds=FOLDS,

nfold=NFOLD):

# Keep track of results

bayes_trials = Trials()

# Global variable

global _ITERATION

_ITERATION = 0

TRAIN_SET = build_train_set(X_train, y_train)

SPACE = XGB_SPACE

func_objective = partial(objective_base,

train_set=TRAIN_SET,

folds=folds,

nfold=nfold,

writetoFile=True)

# Run optimization

best = fmin(fn=func_objective,

space=SPACE,

algo=tpe.suggest,

max_evals=max_evals,

trials=bayes_trials,

rstate=np.random.RandomState(2019))

best_params, loss = post_hyperopt(bayes_trials,

train_set=TRAIN_SET,

folds=folds,

nfold=nfold)

return best_params, loss

Reference

原文:https://www.cnblogs.com/cx2016/p/12899500.html

你可能感兴趣的:(贝叶斯优化python包)