之前的教程以及介绍过如何使用hyperopt对xgboost进行调参,并且已经说明了,该代码模板可以十分轻松的转移到lightgbm,或者catboost上。而本篇教程就是对原模板的一次歉意,前半部分为教程-如何使用hyperopt对xgboost进行自动调参的迁移,后半部分是对在Hyperopt框架下使用XGboost与交叉验证的迁移。
Hyperopt:是python中的一个用于"分布式异步算法组态/超参数优化"的类库。使用它我们可以拜托繁杂的超参数优化过程,自动获取最佳的超参数。广泛意义上,可以将带有超参数的模型看作是一个必然的非凸函数,因此hyperopt几乎可以稳定的获取比手工更加合理的调参结果。尤其对于调参比较复杂的模型而言,其更是能以远快于人工调参的速度同样获得远远超过人工调参的最终性能。
目前中文文档的地址由本人FontTian在2017年翻译,但是hyperopt文档本身确实写的不怎么样。所以才有了这份教程。源代码请前往Github教程地址下载下载。
这里我们使用UCI的红酒质量数据集,除此之外我还额外增加了两个特征。
import numpy as np
import pandas as pd
def GetNewDataByPandas():
wine = pd.read_csv("/home/fonttian/Data/UCI/wine/wine.csv")
wine['alcohol**2'] = pow(wine["alcohol"], 2)
wine['volatileAcidity*alcohol'] = wine["alcohol"] * wine['volatile acidity']
y = np.array(wine.quality)
X = np.array(wine.drop("quality", axis=1))
columns = np.array(wine.columns)
return X, y, columns
首先将数据分割为三份,一部分用于预测,训练数据则同样分成额外的两部分用于evallist参数。
同时为了加快速度和减少内存,我们将数据转换为xgboost
自带的读取格式。
from sklearn.model_selection import train_test_split
# Read wine quality data from file
X, y, wineNames = GetNewDataByPandas()
# split data to [[0.8,0.2],01]
x_train_all, x_predict, y_train_all, y_predict = train_test_split(X, y, test_size=0.10, random_state=100)
x_train, x_test, y_train, y_test = train_test_split(x_train_all, y_train_all, test_size=0.2, random_state=100)
import lightgbm as lgb
train_data = lgb.Dataset(data=x_train,label=y_train)
test_data = lgb.Dataset(data=x_test,label=y_test)
使用hyperopt自带的函数定义参数空间,但是因为其randint()
方法产生的数组范围是从0开始的,所以我额外定义了一个数据转换方法,对原始参数空间进行一次转换。
关于hyperopt中定义参数区间需要使用的函数请参考:
from hyperopt import fmin, tpe, hp, partial
# 自定义hyperopt的参数空间
space = {"max_depth": hp.randint("max_depth", 15),
"num_trees": hp.randint("num_trees", 300),
'learning_rate': hp.uniform('learning_rate', 1e-3, 5e-1),
"bagging_fraction": hp.randint("bagging_fraction", 5),
"num_leaves": hp.randint("num_leaves", 6),
}
def argsDict_tranform(argsDict, isPrint=False):
argsDict["max_depth"] = argsDict["max_depth"] + 5
argsDict['num_trees'] = argsDict['num_trees'] + 150
argsDict["learning_rate"] = argsDict["learning_rate"] * 0.02 + 0.05
argsDict["bagging_fraction"] = argsDict["bagging_fraction"] * 0.1 + 0.5
argsDict["num_leaves"] = argsDict["num_leaves"] * 3 + 10
if isPrint:
print(argsDict)
else:
pass
return argsDict
lightgbm模型工厂用于生产我们需要的model,而分数获取器则是为了解耦。这样在实际的测试工作中更加套用代码和修改。
from sklearn.metrics import mean_squared_error
def lightgbm_factory(argsDict):
argsDict = argsDict_tranform(argsDict)
params = {'nthread': -1, # 进程数
'max_depth': argsDict['max_depth'], # 最大深度
'num_trees': argsDict['num_trees'], # 树的数量
'eta': argsDict['learning_rate'], # 学习率
'bagging_fraction': argsDict['bagging_fraction'], # 采样数
'num_leaves': argsDict['num_leaves'], # 终点节点最小样本占比的和
'objective': 'regression',
'feature_fraction': 0.7, # 样本列采样
'lambda_l1': 0, # L1 正则化
'lambda_l2': 0, # L2 正则化
'bagging_seed': 100, # 随机种子,light中默认为100
}
params['metric'] = ['rmse']
model_lgb = lgb.train(params, train_data, num_boost_round=300, valid_sets=[test_data],early_stopping_rounds=100)
return get_tranformer_score(model_lgb)
def get_tranformer_score(tranformer):
model = tranformer
prediction = model.predict(x_predict, num_iteration=model.best_iteration)
return mean_squared_error(y_predict, prediction)
之后我们调用hyperopt进行自动调参即可,同时通过返回值获取最佳模型的结果。
# 开始使用hyperopt进行自动调参
algo = partial(tpe.suggest, n_startup_jobs=1)
best = fmin(lightgbm_factory, space, algo=algo, max_evals=20, pass_expr_memo_ctrl=None)
/home/fonttian/anaconda3/lib/python3.6/site-packages/lightgbm/engine.py:116: UserWarning: Found `num_trees` in params. Will use it instead of argument
warnings.warn("Found `{}` in params. Will use it instead of argument".format(alias))
[1] valid_0's rmse: 0.793788
Training until validation scores don't improve for 100 rounds.
[2] valid_0's rmse: 0.776669
[3] valid_0's rmse: 0.762522
[4] valid_0's rmse: 0.749776
...
...
...
[299] valid_0's rmse: 0.562895
[300] valid_0's rmse: 0.56334
Did not meet early stopping. Best iteration is:
[226] valid_0's rmse: 0.560096
展示我们获取的最佳参数,以及该模型在训练集上的最终表现,如果想要使用交叉验证请参考其他教程。
RMSE = lightgbm_factory(best)
print('best :', best)
print('best param after transform :')
argsDict_tranform(best,isPrint=True)
print('rmse of the best lightgbm:', np.sqrt(RMSE))
[1] valid_0's rmse: 0.793948
Training until validation scores don't improve for 100 rounds.
[2] valid_0's rmse: 0.776625
...
[239] valid_0's rmse: 0.569307
Early stopping, best iteration is:
[139] valid_0's rmse: 0.567378
best : {'bagging_fraction': 0.555, 'learning_rate': 0.0510231388682371, 'max_depth': 29, 'num_leaves': 265, 'num_trees': 629}
best param after transform :
{'bagging_fraction': 0.5555, 'learning_rate': 0.051020462777364745, 'max_depth': 34, 'num_leaves': 805, 'num_trees': 779}
rmse of the best lightgbm: 0.5964674105744058
其实本部分与之前的一样重点依旧在于Hyperopt与Lightgbm参数传入上的一个冲突,不过解决方案很简单,先构建带有Hyperopt参数空间的模型,然后再从构建的模型中获取参数。其实这点xgboost,hyperopt,catboost三个模型的解决方案都一样。catboost自带的教程中也有这种解决方案。只不过catboost自带的教程不和lightgbm与xgboost一样在自己的原项目里,而是在原账号下又额外开了个Github项目,导致不太容易发现。实际上我也是最近在写这个的时候,才发现catboost原来是自带教程的。也正因为如此,本系列教程就不再往catboost上迁移代码了。请自行参考catboost自带的教程
sklearn部分过于简单,本处将不再做迁移。如有需要请自行迁移。
import hyperopt
train_all_data = lgb.Dataset(data=x_train_all,label=y_train_all)
def hyperopt_objective(params):
model = lgb.LGBMRegressor(
num_leaves=31,
max_depth=int(params['max_depth']) + 5,
learning_rate=params['learning_rate'],
objective='regression',
eval_metric='rmse',
nthread=-1,
)
num_round = 10
res = lgb.cv(model.get_params(),train_all_data, num_round, nfold=5, metrics='rmse',early_stopping_rounds=10)
return min(res['rmse-mean']) # as hyperopt minimises
# 这里的warnings实在太多了,我们加入代码不再让其显示
import warnings
warnings.filterwarnings("ignore")
from numpy.random import RandomState
params_space = {
'max_depth': hyperopt.hp.randint('max_depth', 6),
'learning_rate': hyperopt.hp.uniform('learning_rate', 1e-3, 5e-1),
}
trials = hyperopt.Trials()
best = hyperopt.fmin(
hyperopt_objective,
space=params_space,
algo=hyperopt.tpe.suggest,
max_evals=50,
trials=trials,
rstate=RandomState(123)
)
print("\n展示hyperopt获取的最佳结果,但是要注意的是我们对hyperopt最初的取值范围做过一次转换")
print(best)
展示hyperopt获取的最佳结果,但是要注意的是我们对hyperopt最初的取值范围做过一次转换
{'learning_rate': 0.059356676015000595, 'max_depth': 5}
以后文章会做内容分级:主要内容将发布在知乎,首发仍然在CSDN,知乎只保留部分内容,而CSDN比较全面。CSDN会有的而知乎没有的:一些比较简单的内容,或者草稿。知乎主要发重点内容,以质量为主。