利用 hyperopt 为 lightgbm 自动调参

lightgbm 为 GBDT 算法的又一个工程实现,相比于 xgboost,lightgbm 训练效率更高,同时效果同样优秀。但是其参数众多,人工调参不仅繁琐,效果也未必能获得最优。
hyperopt 是 python 中的一个用于"分布式异步算法组态/超参数优化"的类库,广泛意义上,可以将带有超参数的模型看作是一个必然的非凸函数,因此hyperopt 几乎可以稳定的获取比手工更加合理的调参结果。

0. 相关python库的载入

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.preprocessing import OneHotEncoder, RobustScaler
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.model_selection import train_test_split
from hyperopt import hp, fmin, tpe, partial
from pyspark.sql import SparkSession
from sklearn import metrics
import lightgbm as lgb
import pandas as pd
import numpy as np
import pickle
import base64
import sys

1. 数据准备

首先定义一个sklearn 里 pipeline 方式的数据转换类:

class FeatureProcess(BaseEstimator, TransformerMixin):
    """
    定义数据转换的类,使用方式如下:
    pipeline = FeatureProcess(categorical_cols, numerical_cols, label_cols)
    pipiline.fit(train_data)
    X = pipeline.transform(train_data)
    其中 train_data 为 pandas 的 dataframe
    """

    def __init__(self, categorical_cols=None, numerical_cols=None, label_cols=None):
        """
        :param categorical_cols: 类别特征
        :param numerical_cols: 数值类特征
        :param label_cols: 标签类特征 格式为空格分隔的字符串
        """
        self.categorical_cols = categorical_cols
        self.numerical_cols = numerical_cols
        self.label_cols = label_cols
        self.onehot = None
        self.scaler = None
        self.labels_pipe = {}
        self.stop_words = {'null', 'nan'}
        self.feature_names = None

    def _get_onehot_feature_names(self, categorical_cols):
        """
        返回 onthot 后的特征名,定义为 原始列名+onthot对应的值
        :param categorical_cols:
        :return:
        """
        feature_names = list()
        for col, values in zip(categorical_cols, self.onehot.categories_):
            for val in values:
                feature_names.append('%s_%s' % (col, val))
        return feature_names

    def fit(self, df, y=None):
        # 空值处理
        df = df.replace({'': np.NAN})
        # 类型转换
        df[self.categorical_cols + self.label_cols] = df[self.categorical_cols + self.label_cols].astype(str)
        df[self.numerical_cols] = df[self.numerical_cols].astype(float).replace(float('inf'), 0)
        # onehot处理
        self.onehot = OneHotEncoder(handle_unknown='ignore', sparse=False).fit(df[self.categorical_cols])
        # 标签数据处理
        label_feature_names = []
        for label_col in self.label_cols:
            self.labels_pipe[label_col] = TfidfVectorizer(stop_words=self.stop_words, min_df=500).fit(df[label_col].values)
            label_feature_names.extend(sorted(self.labels_pipe[label_col].vocabulary_,
                                              key=lambda x: self.labels_pipe[label_col].vocabulary_[x]))
        # 去掉最大值
        self.scaler = RobustScaler(with_centering=False, with_scaling=False, quantile_range=(1, 99)).fit(
            df[self.numerical_cols])
        # feature_names
        self.feature_names = self._get_onehot_feature_names(self.categorical_cols) + self.numerical_cols + label_feature_names
        return self

    def transform(self, df, y=None):
        # 空值处理
        df = df.replace({'': np.NAN})
        # 类型转换
        df[self.categorical_cols + self.label_cols] = df[self.categorical_cols + self.label_cols].astype(str)
        df[self.numerical_cols] = df[self.numerical_cols].astype(float).replace(float('inf'), 0)
        # 数据转换
        onehot_data = self.onehot.transform(df[self.categorical_cols])
        scaler_data = self.scaler.transform(df[self.numerical_cols])
        # 标签数据处理
        label_data = np.concatenate([self.labels_pipe[label_col].transform(df[label_col].values).toarray() 
                                     for label_col in self.label_cols], axis=1)
        data = np.c_[onehot_data, scaler_data, label_data]
        return data

    def fit_transform(self, df, y=None):
        return self.fit(df, y).transform(df, y)

2. 定义参数空间

hp.randint 产生的是从0开始的整数,所以定义了 args_tranform 函数进行转换。

# 自定义hyperopt的参数空间
space = {"learning_rate": hp.randint("learning_rate", 7),
         "max_depth": hp.randint("max_depth", 10),
         "num_leaves": hp.randint("num_leaves", 10),
         "bagging_fraction": hp.randint("bagging_fraction", 5),
         "bagging_freq": hp.randint("bagging_freq", 9),
         "feature_fraction": hp.randint("feature_fraction", 5),
         "lambda_l1": hp.randint("lambda_l1", 6),
         "lambda_l2": hp.randint("lambda_l2", 8),
         "cat_smooth": hp.randint("cat_smooth", 20),
         "min_data_in_leaf": hp.randint("min_data_in_leaf", 20),
         }


def args_tranform(args_dict, is_print=False):
    params = dict()

    params["learning_rate"] = args_dict["learning_rate"] * 0.02 + 0.01
    params["max_depth"] = args_dict["max_depth"] + 3
    params["num_leaves"] = args_dict["num_leaves"] * 5 + 5
    params["bagging_fraction"] = args_dict["bagging_fraction"] * 0.1 + 0.2
    params["bagging_freq"] = args_dict["bagging_freq"] + 1
    params["feature_fraction"] = args_dict["feature_fraction"] * 0.1 + 0.5
    params["lambda_l1"] = args_dict["lambda_l1"] * 0.1 + 0.1
    params["lambda_l2"] = args_dict["lambda_l2"] * 5
    params["cat_smooth"] = args_dict["cat_smooth"] + 1
    params["min_data_in_leaf"] = args_dict["min_data_in_leaf"] * 20 + 50

    params["boosting_type"] = 'gbdt'
    params["objective"] = 'binary'
    params["metric"] = 'auc'
    params["verbosity"] = 0
    params["seed"] = 42
    params["num_threads"] = 16

    if is_print:
        print(params)

    return params

3. 构建模型工厂和目标函数

目标函数我增加了控制过拟合的参数,这样能保证 train_aucval_auc 的差值不会过大。

def model_fit(argsDict, rate=0.2):
    params = args_tranform(argsDict)

    model = lgb.train(params, dtrain, 500, dval, early_stopping_rounds=20, verbose_eval=False)

    return get_score(model, rate)


def get_score(model, rate):
    """
    :param model:
    :param rate: 控制过拟合的参数,参数越大表示越严格
    :return:
    """
    y_val_pred = model.predict(X_val, num_iteration=model.best_iteration)
    y_train_pred = model.predict(X_train, num_iteration=model.best_iteration)
    train_auc = metrics.roc_auc_score(y_train, y_train_pred)
    val_auc = metrics.roc_auc_score(y_val, y_val_pred)

    return -val_auc + rate * abs(train_auc - val_auc)

4. 模型训练

通过spark读取训练数据,最后将模型和数据转换的pipeline一起写入到集群文件,也可以将最后结果写入到数据库中。

if __name__ == '__main__':
    df = spark.sql(sql).toPandas()
    # 测试集划分
    train_set, test_set = train_test_split(df, test_size=0.15, shuffle=True, stratify=df['target'],
                                           random_state=123)
    pipeline = FeatureProcess(categorical_cols, numerical_cols, label_cols)
    X = pipeline.fit_transform(train_set.drop('target', axis=1))
    y = train_set['target']

    X_test = pipeline.transform(test_set.drop('target', axis=1))
    y_test = test_set['target']
    feature_names = pipeline.feature_names
    # 训练集和验证集划分
    X_train, X_val, y_train, y_val = train_test_split(X, y.values, test_size=0.15, shuffle=True, stratify=y, random_state=123)

    dtrain = lgb.Dataset(data=X_train, label=y_train)
    dval = lgb.Dataset(data=X_val, label=y_val, reference=dtrain)
    valid_sets = [dtrain, dval]
    # 模型训练
    algo = partial(tpe.suggest, n_startup_jobs=-1)
    best = fmin(model_fit, space, algo=algo, max_evals=50, show_progressbar=True)
    lgb_params = args_tranform(best, is_print=True)
    best_model = lgb.train(lgb_params, dtrain, 1500, valid_sets, ['train', 'eval'], early_stopping_rounds=50,
                           verbose_eval=200)
    # 结果指标
    metric_score = best_model.best_score
    metric_score['test'] = {}
    y_prob = best_model.predict(X_test, num_iteration=best_model.best_iteration)
    metric_score['test']['auc'] = metrics.roc_auc_score(y_test, y_prob)
    metric_score['test']['acc'] = metrics.accuracy_score(y_test, (y_prob >= 0.5).astype(int))

    # 保存
    res_df = pd.DataFrame()
    res_df['model_name'] = ['model_v1.0']
    res_df['pipeline'] = base64.b64encode(pickle.dumps(pipeline)).strip().decode('utf8')
    res_df['model'] = base64.b64encode(pickle.dumps(best_model)).strip().decode('utf8')
    res_df['metric_score'] = str(metric_score)

    sparkDF = spark.createDataFrame(res_df)
    sparkDF.write.format('csv').save(model_path)
    # sparkDF.createOrReplaceTempView("model_result")
    # spark.sql(insert_sql)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354