1. 程式人生 > >Lightgbm with Hyperopt

Lightgbm with Hyperopt

如何使用hyperopt對Lightgbm進行自動調參

之前的教程以及介紹過如何使用hyperopt對xgboost進行調參,並且已經說明了,該程式碼模板可以十分輕鬆的轉移到lightgbm,或者catboost上。而本篇教程就是對原模板的一次歉意,前半部分為教程-如何使用hyperopt對xgboost進行自動調參的遷移,後半部分是對在Hyperopt框架下使用XGboost與交叉驗證的遷移。

關於Hyperopt

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進行自動調參即可,同時通過返回值獲取最佳模型的結果。

# 開始使用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自帶的教程

使用CV方法進行交叉驗證

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}

額外的:主站遷移說明

本文作者FontTian,從2018年12月18日起,技術部落格主站將由CSDN轉到知乎,同時也會做內容分級:主要內容將釋出在知乎,CSDN會同步遷移。CSDN會有的而知乎沒有的:一些比較簡單的內容,或者草稿。知乎主要發重點內容,以質量為主。而文章連結將預設為知乎連結,部分CSDN專有的除外。