1.1 GBDT和 LightGBM对比

GBDT (Gradient Boosting Decision Tree) 是机器学习中一个长盛不衰的模型,其主要思想是利用弱分类器(决策树)迭代训练以得到最优模型,该模型具有训练效果好、不易过拟合等优点。GBDT 在工业界应用广泛,通常被用于点击率预测,搜索排序等任务。GBDT 也是各种数据挖掘竞赛的致命武器,据统计 Kaggle 上的比赛有一半以上的冠军方案都是基于 GBDT。

LightGBM(Light Gradient Boosting Machine)同样是一款基于决策树算法的分布式梯度提升框架。为了满足工业界缩短模型计算时间的需求,LightGBM的设计思路主要是两点:

  • 减小数据对内存的使用,保证单个机器在不牺牲速度的情况下,尽可能地用上更多的数据;
  • 减小通信的代价,提升多机并行时的效率,实现在计算上的线性加速。
  • 由此可见,LightGBM的设计初衷就是提供一个快速高效、低内存占用、高准确度、支持并行和大规模数据处理的数据科学工具。

    XGBoost和LightGBM都是基于决策树提升(Tree Boosting)的工具,都拥有对输入要求不敏感、计算复杂度不高和效果好的特点,适合在工业界中进行大量的应用。
    主页地址: http://lightgbm.apachecn.org

    LightGBM (Light Gradient Boosting Machine)是一个实现 GBDT 算法的框架,支持高效率的并行训练,并且具有以下优点:

  • 更快的训练速度
  • 更低的内存消耗
  • 更好的准确率
  • 分布式支持,可以快速处理海量数据
  • 如下图,在 Higgs 数据集上 LightGBM 比 XGBoost 快将近 10 倍,内存占用率大约为 XGBoost 的1/6,并且准确率也有提升。

    常用的机器学习算法,例如神经网络等算法,都可以以 mini-batch 的方式训练,训练数据的大小不会受到内存限制。

    而 GBDT 在每一次迭代的时候,都需要遍历整个训练数据多次。如果把整个训练数据装进内存则会限制训练数据的大小;如果不装进内存,反复地读写训练数据又会消耗非常大的时间。尤其面对工业级海量的数据,普通的 GBDT 算法是不能满足其需求的。

    LightGBM 提出的主要原因就是为了解决 GBDT 在海量数据遇到的问题,让 GBDT 可以更好更快地用于工业实践。

  • 每轮迭代时,都需要遍历整个训练数据多次。如果把整个训练数据装进内存则会限制训练数据的大小;如果不装进内存,反复地读写训练数据又会消耗非常大的时间。
  • 预排序方法(pre-sorted):
  • 首先,空间消耗大。 这样的算法需要保存数据的特征值,还保存了特征排序的结果(例如排序后的索引,为了后续快速的计算分割点),这里需要消耗训练数据两倍的内存。
  • 其次时间上也有较大的开销, 在遍历每一个分割点的时候,都需要进行分裂增益的计算,消耗的代价大。
  • 对Cache优化不友好。 在预排序后,特征对梯度的访问是一种随机访问,并且不同的特征访问的顺序不一样,无法对cache进行优化 。同时, 在每一层长树的时候,需要随机访问一个行索引到叶子索引的数组,并且不同特征访问的顺序也不一样,也会造成较大的cache miss
  • 1.4.1 Histogram 算法

    直方图算法的基本思想: 先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。遍历数据时,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。
    GBDT 虽然是个强力的模型,但却有着一个致命的缺陷, 不能用类似 mini batch 的方式来训练,需要对数据进行无数次的遍历。如果想要速度,就需要把数据都预加载在内存中,但这样数据就会受限于内存的大小;如果想要训练更多的数据,就要使用外存版本的决策树算法 。虽然外存算法也有较多优化,SSD 也在普及, 但在频繁的 IO 下,速度还是比较慢的 。为了能让 GBDT 高效地用上更多的数据,我们把思路转向了 分布式 GBDT, 然后就有了LightGBM**。

    设计的思路主要是两点,

  • 单个机器在不牺牲速度的情况下,尽可能多地用上更多的数据
  • 多机并行的时候, 通信的代价尽可能地低,并且在计算上可以做到线性加速。
  • 基于这两个需求,LightGBM 选择了基于 histogram 的决策树算法。相比于另一个主流的算法 pre-sorted(如 xgboost 中的 exact 算法),histogram 在内存消耗和计算代价上都有不少优势。

    Pre-sorted 算法需要的内存约是训练数据的两倍(2 _ #data _ #features* 4Bytes),它需要用32位浮点来保存 feature value,并且对每一列特征,都需要一个额外的排好序的索引,这也需要32位的存储空间。

    对于 histogram 算法,则只需要(#data_ #features _ 1Bytes)的内存消耗, 仅为 pre-sorted算法的1/8 。因为 histogram 算法仅需要存储 featurebin value (离散化后的数值),不需要原始的 feature value,也不用排序,而 binvalue 用 uint8_t (256bins) 的类型一般也就足够了。

    在计算上的优势则主要体现在“数据分割”。 决策树算法有两个主要操作组成,一个是“寻找分割点”,另一个是“数据分割”。
    **

  • 从算法时间复杂度来看,Histogram 算法和 pre-sorted 算法在“寻找分割点”的代价是一样的,都是O(#feature*#data)。
  • 而在“数据分割”时,pre-sorted 算法需要O(#feature*#data),而 histogram 算法是O(#data)。因为 pre-sorted 算法的每一列特征的顺序都不一样,分割的时候需要对每个特征单独进行一次分割。 Histogram算法不需要排序,所有特征共享同一个索引表,分割的时候仅需对这个索引表操作一次就可以 。(更新: 这一点不完全正确,pre-sorted 与 level-wise 结合的时候,其实可以共用一个索引表(row_idx_to_tree_node_idx)。然后在寻找分割点的时候,同时操作同一层的节点,省去分割的步骤。但这样做的问题是会有非常多随机访问,有很大的chche miss,速度依然很慢)。
    另一个计算上的优势则是 大幅减少了计算分割点增益的次数
  • 对于一个特征, pre-sorted 需要对每一个不同特征值都计算一次分割增益 ;
  • histogram 只需要计算 #bin (histogram 的横轴的数量) 次。
  • 最后, 在数据并行的时候,用 histgoram 可以大幅降低通信代价。用 pre-sorted 算法的话,通信代价是非常大的(几乎是没办法用的)。所以 xgoobst 在并行的时候也使用 histogram 进行通信

    histogram算法缺点

    histogram 算法也有缺点,它不能找到很精确的分割点,训练误差没有 pre-sorted 好 。但从实验结果来看, histogram 算法在测试集的误差和 pre-sorted 算法差异并不是很大,甚至有时候效果更好。 实际上可能决策树对于分割点的精确程度并不太敏感,而且较“粗”的分割点也自带正则化的效果。

    使用直方图算法有很多优点。** 首先,最明显就是内存消耗的降低,直方图算法不仅不需要额外存储预排序的结果,而且可以只保存特征离散化后的值,而这个值一般用 8 位整型存储就足够了,内存消耗可以降低为原来的1/8**。

    然后在计算上的代价也大幅降低,预排序算法每遍历一个特征值就需要计算一次分裂的增益,而直方图算法只需要计算k次(k可以认为是常数),时间复杂度从O(#data #feature)优化到O(k #features)。
    当然,Histogram 算法并不是完美的。由于特征被离散化后,找到的并不是很精确的分割点,所以会对结果产生影响。但在不同的数据集上的结果表明,离散化的分割点对最终的精度影响并不是很大,甚至有时候会更好一点。

    原因是决策树本来就是弱模型,分割点是不是精确并不是太重要;较粗的分割点也有正则化的效果,可以有效地防止过拟合;即使单棵树的训练误差比精确分割的算法稍大,但在梯度提升(Gradient Boosting)的框架下没有太大的影响。

    1.4.4 直接支持类别特征

    实际上大多数机器学习工具都无法直接支持类别特征,一般需要把类别特征,转化到多维的0/1 特征,降低了空间和时间的效率。而类别特征的使用是在实践中很常用的。基于这个考虑,LightGBM 优化了对类别特征的支持,可以直接输入类别特征,不需要额外的0/1 展开。并在决策树算法上增加了类别特征的决策规则。在 Expo 数据集上的实验,相比0/1 展开的方法,训练速度可以加速 8 倍,并且精度一致。据我们所知,LightGBM 是第一个直接支持类别特征的 GBDT 工具。

    LightGBM 的单机版本还有很多其他细节上的优化,比如 cache 访问优化,多线程优化,稀疏特征优化等等。优化汇总如下:

  • 特征并行的主要思想是在不同机器在不同的特征集合上分别寻找最优的分割点,然后在机器间同步最优的分割点。
  • 数据并行则是让不同的机器先在本地构造直方图,然后进行全局的合并,最后在合并的直方图上面寻找最优分割点。
  • LightGBM 针对这两种并行方法都做了优化:

  • 在特征并行算法中,通过在本地保存全部数据避免对数据切分结果的通信;
  • 在数据并行中使用分散规约 (Reduce scatter) 把直方图合并的任务分摊到不同的机器,降低通信和计算,并利用直方图做差,进一步减少了一半的通信量。基于投票的数据并行则进一步优化数据并行中的通信代价,使通信代价变成常数级别。在数据量很大的时候,使用投票并行可以得到非常好的加速效果。
  • 传统的特征并行算法旨在于在并行化决策树中的“ Find Best Split.主要流程如下: - 垂直划分数据(不同的机器有不同的特征集) - 在本地特征集寻找最佳划分点 {特征, 阈值} - 本地进行各个划分的通信整合并得到最佳划分 - 以最佳划分方法对数据进行划分,并将数据划分结果传递给其他线程 - 其他线程对接受到的数据进一步划分

    传统的特征并行方法主要不足:

    存在计算上的局限,传统特征并行无法加速 “split”(时间复杂度为 “O(#data)”)。 因此,当数据量很大的时候,难以加速。需要对划分的结果进行通信整合,其额外的时间复杂度约为 “O(#data/8)”(一个数据一个字节) - LightGBM 中的特征并行

    既然在数据量很大时,传统数据并行方法无法有效地加速,我们做了一些改变: 不再垂直划分数据,即每个线程都持有全部数据。 因此,LighetGBM中没有数据划分结果之间通信的开销,各个线程都知道如何划分数据 。 而且,“#data” 不会变得更大,所以,在使每台机器都持有全部数据是合理的。

    LightGBM 中特征并行的流程如下: - 每个线程都在本地数据集上寻找最佳划分点{特征, 阈值} - 本地进行各个划分的通信整合并得到最佳划分 - 执行最佳划分

    然而, 特征并行算法在数据量很大时仍然存在计算上的局限。因此,建议在数据量很大时使用数据并行。

    数据并行 data

    数据并行旨在于并行化整个决策学习过程。数据并行的主要流程如下: - 水平划分数据 - 线程以本地数据构建本地直方图 - 将本地直方图整合成全局整合图 - 在全局直方图中寻找最佳划分,然后执行此划分

    传统数据划分的不足: - 高通讯开销。 如果使用点对点的通讯算法,一个机器的通讯开销大约为 “O(#machine  #feature  #bin)” - 如果使用集成的通讯算法(例如, “All Reduce”等),通讯开销大约为 “O(2  #feature  #bin)” - LightGBM中的数据并行

    LightGBM 中采用以下方法较少数据并行中的通讯开销: - 不同于“整合所有本地直方图以形成全局直方图”的方式,LightGBM 使用分散规约(Reduce scatter)的方式对不同线程的不同特征(不重叠的)进行整合。 - 然后线程从本地整合直方图中寻找最佳划分并同步到全局的最佳划分中。 - 如上所述。 LightGBM 通过直方图做差法加速训练。 基于此,我们可以进行单叶子的直方图通讯,并且在相邻直方图上使用做差法。 - 通过上述方法,LightGBM 将数据并行中的通讯开销减少到 “O(0.5  #feature  #bin)”。

    投票并行未来将致力于将 “数据并行”中的通讯开销减少至常数级别,其将会通过两阶段的投票过程较少特征直方图的通讯开销。 <

    # 01. train set and test set 划分训练集和测试集
    train_data = lgb.Dataset(dtrain[predictors],label=dtrain[target],feature_name=list(dtrain[predictors].columns), categorical_feature=dummies)
    test_data = lgb.Dataset(dtest[predictors],label=dtest[target],feature_name=list(dtest[predictors].columns), categorical_feature=dummies)
    # 02. parameters 参数设置
    param = {
        'max_depth':6,
        'num_leaves':64,
        'learning_rate':0.03,
        'scale_pos_weight':1,
        'num_threads':40,
        'objective':'binary',
        'bagging_fraction':0.7,
        'bagging_freq':1,
        'min_sum_hessian_in_leaf':100
    param['is_unbalance']='true'
    param['metric'] = 'auc'
    #03. cv and train 自定义cv函数和模型训练
    bst=lgb.cv(param,train_data, num_boost_round=1000, nfold=3, early_stopping_rounds=30)
    estimators = lgb.train(param,train_data,num_boost_round=len(bst['auc-mean']))
    #04. test predict 测试集结果
    ypred = estimators.predict(dtest[predictors])
    import pickle  
    from sklearn.metrics import roc_auc_score  
    from sklearn.model_selection import train_test_split  
    print("Loading Data ... ")  
    # 导入数据  
    train_x, train_y, test_x = load_data()  
    # 用sklearn.cross_validation进行训练数据集划分,这里训练集和交叉验证集比例为7:3,可以自己根据需要设置  
    X, val_X, y, val_y = train_test_split(  
        train_x,  
        train_y,  
        test_size=0.05,  
        random_state=1,  
        stratify=train_y # 这里保证分割后y的比例分布与原数据一致  
    X_train = X  
    y_train = y  
    X_test = val_X  
    y_test = val_y  
    # create dataset for lightgbm  
    lgb_train = lgb.Dataset(X_train, y_train)  
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)  
    # specify your configurations as a dict  
    params = {  
        'boosting_type': 'gbdt',  
        'objective': 'binary',  
        'metric': {'binary_logloss', 'auc'},  #二进制对数损失
        'num_leaves': 5,  
        'max_depth': 6,  
        'min_data_in_leaf': 450,  
        'learning_rate': 0.1,  
        'feature_fraction': 0.9,  
        'bagging_fraction': 0.95,  
        'bagging_freq': 5,  
        'lambda_l1': 1,    
        'lambda_l2': 0.001,  # 越小l2正则程度越高  
        'min_gain_to_split': 0.2,  
        'verbose': 5,  
        'is_unbalance': True  
    # train  
    print('Start training...')  
    gbm = lgb.train(params,  
                    lgb_train,  
                    num_boost_round=10000,  
                    valid_sets=lgb_eval,  
                    early_stopping_rounds=500)  
    print('Start predicting...')  
    preds = gbm.predict(test_x, num_iteration=gbm.best_iteration)  # 输出的是概率结果  
    # 导出结果  
    threshold = 0.5  
    for pred in preds:  
        result = 1 if pred > threshold else 0  
    # 导出特征重要性  
    importance = gbm.feature_importance()  
    names = gbm.feature_name()  
    with open('./feature_importance.txt', 'w+') as file:  
        for index, im in enumerate(importance):  
            string = names[index] + ', ' + str(im) + '\n'  
            file.write(string)
    import pickle  
    from sklearn.metrics import roc_auc_score  
    from sklearn.model_selection import train_test_split  
    print("Loading Data ... ")  
    # 导入数据  
    train_x, train_y, test_x = load_data()  
    # 用sklearn.cross_validation进行训练数据集划分,这里训练集和交叉验证集比例为7:3,可以自己根据需要设置  
    X, val_X, y, val_y = train_test_split(  
        train_x,  
        train_y,  
        test_size=0.05,  
        random_state=1,  
        stratify=train_y ## 这里保证分割后y的比例分布与原数据一致  
    X_train = X  
    y_train = y  
    X_test = val_X  
    y_test = val_y  
    # create dataset for lightgbm  
    lgb_train = lgb.Dataset(X_train, y_train)  
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)  
    # specify your configurations as a dict  
    params = {  
        'boosting_type': 'gbdt',  
        'objective': 'multiclass',  
        'num_class': 9,  
        'metric': 'multi_error',  
        'num_leaves': 300,  
        'min_data_in_leaf': 100,  
        'learning_rate': 0.01,  
        'feature_fraction': 0.8,  
        'bagging_fraction': 0.8,  
        'bagging_freq': 5,  
        'lambda_l1': 0.4,  
        'lambda_l2': 0.5,  
        'min_gain_to_split': 0.2,  
        'verbose': 5,  
        'is_unbalance': True  
    # train  
    print('Start training...')  
    gbm = lgb.train(params,  
                    lgb_train,  
                    num_boost_round=10000,  
                    valid_sets=lgb_eval,  
                    early_stopping_rounds=500)  
    print('Start predicting...')  
    preds = gbm.predict(test_x, num_iteration=gbm.best_iteration)  # 输出的是概率结果  
    # 导出结果  
    for pred in preds:  
        result = prediction = int(np.argmax(pred))  
    # 导出特征重要性  
    importance = gbm.feature_importance()  
    names = gbm.feature_name()  
    with open('./feature_importance.txt', 'w+') as file:  
        for index, im in enumerate(importance):  
            string = names[index] + ', ' + str(im) + '\n'  
            file.write(string)
    from datetime import datetime 
    start = datetime.now() 
    xg = xgb.train(parameters,dtrain,num_round) 
    stop = datetime.now()
    # lightgbm
    num_round = 50
    start = datetime.now()
    lgbm = lgb.train(param,train_data,num_round)
    stop = datetime.now()
    
    #xgboost
    from sklearn.metrics import accuracy_score 
    accuracy_xgb = accuracy_score(y_test,ypred) 
    accuracy_xgb
    # lightgbm
    accuracy_lgbm = accuracy_score(ypred2,y_test)
    accuracy_lgbm
    y_test.value_counts()
    from sklearn.metrics import roc_auc_score
    
    auc_lgbm comparison_dict = {
        'accuracy score':(accuracy_lgbm,accuracy_xgb),
        'auc score':(auc_lgbm,auc_xgb),
        'execution time':(execution_time_lgbm,execution_time_xgb)}
    comparison_df = DataFrame(comparison_dict) 
    comparison_df.index= ['LightGBM','xgboost'] 
    comparison_df
    

    1 针对 Leaf-wise (最佳优先) 树的参数优化

    LightGBM 使用 leaf-wise(leaf-wise-best-first-tree-growth) 的树生长策略, 而很多其他流行的算法采用 depth-wise 的树生长策略. 与 depth-wise 的树生长策略相较, leaf-wise 算法可以收敛的更快. 但是, 如果参数选择不当的话, leaf-wise 算法有可能导致过拟合.

    想要在使用 leaf-wise 算法时得到好的结果, 这里有几个重要的参数值得注意:

  • num_leaves. 这是控制树模型复杂度的主要参数. 理论上, 借鉴 depth-wise 树, 我们可以设置 num_leaves = 2^(max_depth) 但是, 这种简单的转化在实际应用中表现不佳. 这是因为, 当叶子数目相同时, leaf-wise 树要比 depth-wise 树深得多, 这就有可能导致过拟合. 因此, 当我们试着调整 num_leaves 的取值时, 应该让其小于 2^(max_depth). 举个例子, 当 max_depth=6 时(这里译者认为例子中, 树的最大深度应为7), depth-wise 树可以达到较高的准确率.但是如果设置 num_leaves127 时, 有可能会导致过拟合, 而将其设置为 7080 时可能会得到比 depth-wise 树更高的准确率.** 其实, depth 的概念在 leaf-wise 树中并没有多大作用, 因为并不存在一个从 leavesdepth 的合理映射.**
  • min_data_in_leaf. 这是处理 leaf-wise 树的过拟合问题中一个非常重要的参数. 它的值取决于训练数据的样本个树和 num_leaves. 将其设置的较大可以避免生成一个过深的树, 但有可能导致欠拟合. 实际应用中, 对于大数据集, 设置其为几百或几千就足够了.
  • max_depth. 你也可以利用 max_depth 来显式地限制树的深度.
  • 2 针对更快的训练速度

  • 通过设置 bagging_fractionbagging_freq 参数来使用 bagging 方法
  • 通过设置 feature_fraction 参数来使用特征的子抽样
  • 使用较小的 max_bin
  • 使用 save_binary 在未来的学习过程对数据加载进行加速
  • 使用并行学习
  • 3 针对更好的准确率

  • 使用较大的 max_bin (学习速度可能变慢)
  • 使用较小的 learning_rate 和较大的 num_iterations
  • 使用较大的 num_leaves (可能导致过拟合)
  • 使用更大的训练数据
  • 尝试 dart
  • 4 处理过拟合

  • 使用较小的 max_bin
  • 使用较小的 num_leaves
  • 使用 min_data_in_leafmin_sum_hessian_in_leaf
  • 通过设置 bagging_fractionbagging_freq 来使用 bagging
  • 通过设置 feature_fraction 来使用特征子抽样
  • 使用更大的训练数据
  • 使用 lambda_l1, lambda_l2min_gain_to_split 来使用正则
  • 尝试 max_depth 来避免生成过深的树
  • LightGBM 通过默认的方式来处理缺失值,你可以通过设置 use_missing=false 来使其无效。
  • LightGBM 通过默认的的方式用 NA (NaN) 去表示缺失值,你可以通过设置 zero_as_missing=true 将其变为零。
  • 当设置 zero_as_missing=false (默认)时,在稀疏矩阵里 (和LightSVM) ,没有显示的值视为零。
  • 当设置 zero_as_missing=true 时, NA 和 0 (包括在稀疏矩阵里,没有显示的值)视为缺失。
  • 当直接输入类别特征,LightGBM 能提供良好的精确度。不像简单的 one-hot 编码,LightGBM 可以找到类别特征的最优分割。 相对于 one-hot 编码结果,LightGBM 可以提供更加准确的最优分割。
  • categorical_feature 指定类别特征
  • 需要转换为 int 类型,并且只支持非负数。 建议转换到连续的数字范围。
  • 使用 min_data_per_group, cat_smooth 去处理过拟合(当 #data 比较小,或者 #category 比较大)
  • 对于类别数量很大的类别特征(#category 比较大), 最好把它转化为数值特征。
  • train_data = pd.read_csv('./binary.train',header=None,sep = '\t') test_data = pd.read_csv('./binary.test',header=None,sep = '\t') x_train = train_data.drop(0,axis = 1).values x_test = test_data.drop(0,axis = 1).values y_train = train_data[0].values y_test = test_data[0].values # xgboost xgb_train_data = xgb.DMatrix(data=x_train,label=y_train) xgb_test_data = xgb.DMatrix(data=x_test) # lightgbm lgb_train_data = lgb.Dataset(data=x_train,label=y_train) lgb_test_data = lgb.Dataset(data=x_test,label=y_test,reference=lgb_train_data) # set parameters xgb_params = { 'max_depth':7, 'eta':1, 'silent':1, 'objective':'binary:logistic', 'eval_metric':'auc', 'learning_rate':0.05 lgb_params = { 'num_leaves':150, 'objective':'binary', 'max_depth':7, 'learning_rate':0.05, 'max_bin':200 lgb_params['metric'] = ['auc', 'binary_logloss']
    %time
    num_round = 50
    xgb = xgb.train(
        xgb_params,
        dtrain = xgb_train_data,
        num_boost_round = num_round
    CPU times: user 2 µs, sys: 0 ns, total: 2 µs
    Wall time: 21.5 µs
    %time
    num_round = 50
    lgb = lgb.train(
        lgb_params,
        train_set = lgb_train_data,
        num_boost_round = num_round
    CPU times: user 3 µs, sys: 0 ns, total: 3 µs
    Wall time: 8.34 µs
    
    # xgboost
    for i in range(len(y_pred_xgb)): 
        if y_pred_xgb[i] >= .5:        # setting threshold to .5 
           y_pred_xgb[i] = 1 
        else: 
           y_pred_xgb[i] = 0
    # lightgbm
    for i in range(len(y_pred_lgb)):    
        if y_pred_lgb[i] >= .5:       # setting threshold to .5
           y_pred_lgb[i] = 1
        else:  
           y_pred_lgb[i] = 0
    
    # Converting probabilities into 1 or 0
    from sklearn.metrics import accuracy_score
    from sklearn.metrics import roc_auc_score
    # xgboost
    accuracy_xgb = accuracy_score(y_test,y_pred_xgb) 
    print(accuracy_xgb)
    # lightgbm
    accuracy_lgb = accuracy_score(y_test,y_pred_lgb)
    print(accuracy_lgb)
    # xgboost
    auc_xgb = roc_auc_score(y_test,y_pred_xgb)
    print(auc_xgb)
    # lightgbm
    auc_lgb = roc_auc_score(y_test,y_pred_lgb)
    print(auc_lgb)
    0.756
    0.744
    0.7572884416924665
    0.7455495356037152
    
    # 建立一个 dataframe 来比较 Lightgbm 和 xgb
    comparison_dict = {
        'accuracy score':(accuracy_lgb,accuracy_xgb),
        'auc score':(auc_lgb,auc_xgb)
    comparison_df = pd.DataFrame(comparison_dict) 
    comparison_df.index= ['lightgbm','xgboost'] 
    comparison_df
    path="D:/data/"
    print("load data")
    df_train=pd.read_csv(path+"regression.train.csv",header=None,sep='\t')
    df_test=pd.read_csv(path+"regression.train.csv",header=None,sep='\t')
    y_train = df_train[0].values
    y_test = df_test[0].values
    X_train = df_train.drop(0, axis=1).values
    X_test = df_test.drop(0, axis=1).values
    # create dataset for lightgbm
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
    # specify your configurations as a dict
    params = {
            'task': 'train',
            'boosting_type': 'gbdt',
            'objective': 'binary',
            'metric': {'l2', 'auc'},
            'num_leaves': 31,
            'learning_rate': 0.05,
            'feature_fraction': 0.9,
            'bagging_fraction': 0.8,
            'bagging_freq': 5,
            'verbose': 0
    print('Start training...')
    # train
    gbm = lgb.train(params,
                    lgb_train,
                    num_boost_round=20,
                    valid_sets=lgb_eval,
                    early_stopping_rounds=5)
    print('Save model...')
    # save model to file
    gbm.save_model(path+'lightgbm/model.txt')
    print('Start predicting...')
    # predict
    y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)
    # eval
    print(y_pred)
    print('The roc of prediction is:', roc_auc_score(y_test, y_pred) )
    print('Dump model to JSON...')
    # dump model to json (and save to file)
    model_json = gbm.dump_model()
    with open(path+'lightgbm/model.json', 'w+') as f:
        json.dump(model_json, f, indent=4)
    print('Feature names:', gbm.feature_name())
    print('Calculate feature importances...')
    # feature importances
    print('Feature importances:', list(gbm.feature_importance()))
    Start training...
    [1] valid_0's auc: 0.76138  valid_0's l2: 0.243849
    Training until validation scores don't improve for 5 rounds.
    [2] valid_0's auc: 0.776568 valid_0's l2: 0.239689
    [3] valid_0's auc: 0.797394 valid_0's l2: 0.235903
    [4] valid_0's auc: 0.804646 valid_0's l2: 0.231545
    [5] valid_0's auc: 0.807803 valid_0's l2: 0.22744
    [6] valid_0's auc: 0.811241 valid_0's l2: 0.224042
    [7] valid_0's auc: 0.817447 valid_0's l2: 0.221105
    [8] valid_0's auc: 0.819344 valid_0's l2: 0.217747
    [9] valid_0's auc: 0.82034  valid_0's l2: 0.214645
    [10]    valid_0's auc: 0.821408 valid_0's l2: 0.211794
    [11]    valid_0's auc: 0.823175 valid_0's l2: 0.209131
    [12]    valid_0's auc: 0.824161 valid_0's l2: 0.206662
    [13]    valid_0's auc: 0.824834 valid_0's l2: 0.204433
    [14]    valid_0's auc: 0.825996 valid_0's l2: 0.20245
    [15]    valid_0's auc: 0.826775 valid_0's l2: 0.200595
    [16]    valid_0's auc: 0.827877 valid_0's l2: 0.198727
    [17]    valid_0's auc: 0.830383 valid_0's l2: 0.196703
    [18]    valid_0's auc: 0.833477 valid_0's l2: 0.195037
    [19]    valid_0's auc: 0.834914 valid_0's l2: 0.193249
    [20]    valid_0's auc: 0.836136 valid_0's l2: 0.191544
    Did not meet early stopping. Best iteration is:
    [20]    valid_0's auc: 0.836136 valid_0's l2: 0.191544
    Save model...
    Start predicting...
    [ 0.63918719  0.74876927  0.7446886  ...,  0.27801888  0.47378265
      0.49893381]
    The roc of prediction is: 0.836136144322
    Dump model to JSON...
    Feature names: ['Column_0', 'Column_1', 'Column_2', 'Column_3', 'Column_4', 'Column_5', 'Column_6', 'Column_7', 'Column_8', 'Column_9', 'Column_10', 'Column_11', 'Column_12', 'Column_13', 'Column_14', 'Column_15', 'Column_16', 'Column_17', 'Column_18', 'Column_19', 'Column_20', 'Column_21', 'Column_22', 'Column_23', 'Column_24', 'Column_25', 'Column_26', 'Column_27']
    Calculate feature importances...
    Feature importances: [25, 4, 4, 41, 7, 56, 4, 1, 4, 29, 5, 4, 1, 20, 8, 10, 0, 7, 3, 10, 1, 21, 59, 7, 66, 77, 55, 71]
    

    ©用户历史订单数据:orderHistory_*.csv**

    该数据描述了用户的历史订单信息。数据共有7列,分别是用户id,订单id,订单时间,订单类型,旅游城市,国家,大陆。其中1表示购买了精品旅游服务,0表示普通旅游服务。

    例如: userid,orderid,orderTime,orderType,city,country,continent
    ​ 100000000371, 1000709,1503443585,0,东京,日本,亚洲
    ​ 100000000393, 1000952,1499440296,0,巴黎,法国,欧洲
    

    注意:一个用户可能会有多个订单,需要预测的是用户最近一次订单的类型;此文件给到的订单记录都是在“被预测订单”之前的记录信息!同一时刻可能有多个订单,属于父订单和子订单的关系。

    (d)待预测订单的数据:orderFuture_*.csv**

    对于train,有两列,分别是用户id和订单类型。供参赛者训练模型使用。其中1表示购买了精品旅游服务,0表示未购买精品旅游服务(包括普通旅游服务和未下订单)。

    例如: userid,orderType
    ​ 102040050111,0
    ​ 103020010127,1
    ​ 100002030231,0
    

    对于test,只有一列用户id,是待预测的用户列表。

    (e)评论数据:userComment_*.csv**

    共有5个字段,分别是用户id,订单id,评分,标签,评论内容。

    其中受数据保密性约束,评论内容仅显示一些关键词。

    ​ userid,orderid,rating,tags,commentsKeyWords
    ​ 100000550471, 1001899,5.0,,
    ​ 10044000637, 1001930,5.0,主动热情|提前联系|景点介绍详尽|耐心等候,
    ​ 111333446057, 1001960,5.0,主动热情|耐心等候,[‘平稳’, ‘很好’]
    LOG_FILE = 'log/lgb_train.log'
    check_path(LOG_FILE)
    handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1024 * 1024, backupCount=1)  # 实例化handler
    fmt = '%(asctime)s - %(filename)s:%(lineno)s - %(name)s - %(message)s'
    formatter = logging.Formatter(fmt)
    handler.setFormatter(formatter)
    logger = logging.getLogger('train')
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)
    def lgb_fit(config, X_train, y_train):
        """模型(交叉验证)训练,并返回最优迭代次数和最优的结果。
        Args:
            config: xgb 模型参数 {params, max_round, cv_folds, early_stop_round, seed, save_model_path}
            X_train:array like, shape = n_sample * n_feature
            y_train:  shape = n_sample * 1
        Returns:
            best_model: 训练好的最优模型
            best_auc: float, 在测试集上面的 AUC 值。
            best_round: int, 最优迭代次数。
        params = config.params
        max_round = config.max_round
        cv_folds = config.cv_folds
        early_stop_round = config.early_stop_round
        seed = config.seed
        # seed = np.random.randint(0, 10000)
        save_model_path = config.save_model_path
        if cv_folds is not None:
            dtrain = lgb.Dataset(X_train, label=y_train)
            cv_result = lgb.cv(params, dtrain, max_round, nfold=cv_folds, seed=seed, verbose_eval=True,
                               metrics='auc', early_stopping_rounds=early_stop_round, show_stdv=False)
            # 最优模型,最优迭代次数
            best_round = len(cv_result['auc-mean'])
            best_auc = cv_result['auc-mean'][-1]  # 最好的 auc 值
            best_model = lgb.train(params, dtrain, best_round)
        else:
            X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.2, random_state=100)
            dtrain = lgb.Dataset(X_train, label=y_train)
            dvalid = lgb.Dataset(X_valid, label=y_valid)
            watchlist = [dtrain, dvalid]
            best_model = lgb.train(params, dtrain, max_round, valid_sets=watchlist, early_stopping_rounds=early_stop_round)
            best_round = best_model.best_iteration
            best_auc = best_model.best_score
            cv_result = None
        if save_model_path:
            check_path(save_model_path)
            best_model.save_model(save_model_path)
        return best_model, best_auc, best_round, cv_result
    def lgb_predict(model, X_test, save_result_path=None):
        y_pred_prob = model.predict(X_test)
        if save_result_path:
            df_result = df_future_test
            df_result['orderType'] = y_pred_prob
            df_result.to_csv(save_result_path, index=False)
            print('Save the result to {}'.format(save_result_path))
        return y_pred_prob
    class Config(object):
        def __init__(self):
            self.params = {
                'objective': 'binary',
                'metric': {'auc'},
                'learning_rate': 0.05,
                'num_leaves': 30,  # 叶子设置为 50 线下过拟合严重
                'min_sum_hessian_in_leaf': 0.1,
                'feature_fraction': 0.3,  # 相当于 colsample_bytree
                'bagging_fraction': 0.5,  # 相当于 subsample
                'lambda_l1': 0,
                'lambda_l2': 5,
                'num_thread': 6  # 线程数设置为真实的 CPU 数,一般12线程的机器有6个物理核
            self.max_round = 3000
            self.cv_folds = 5
            self.early_stop_round = 30
            self.seed = 3
            self.save_model_path = 'model/lgb.txt'
    def run_feat_search(X_train, X_test, y_train, feature_names):
        """根据特征重要度,逐个删除特征进行训练,获取最好的特征结果。
        同时,将每次迭代的结果求平均作为预测结果"""
        config = Config()
        # train model
        tic = time.time()
        y_pred_list = list()
        aucs = list()
        for i in range(1, 250, 3):
            drop_cols = feature_names[-i:]
            X_train_ = X_train.drop(drop_cols, axis=1)
            X_test_ = X_test.drop(drop_cols, axis=1)
            data_message = 'X_train.shape={}, X_test.shape={}'.format(X_train_.shape, X_test_.shape)
            print(data_message)
            logger.info(data_message)
            lgb_model, best_auc, best_round, cv_result = lgb_fit(config, X_train_, y_train)
            print('Time cost {}s'.format(time.time() - tic))
            result_message = 'best_round={}, best_auc={}'.format(best_round, best_auc)
            logger.info(result_message)
            print(result_message)
            # predict
            # lgb_model = lgb.Booster(model_file=config.save_model_path)
            now = time.strftime("%m%d-%H%M%S")
            result_path = 'result/result_lgb_{}-{:.4f}.csv'.format(now, best_auc)
            check_path(result_path)
            y_pred = lgb_predict(lgb_model, X_test_, result_path)
            y_pred_list.append(y_pred)
            aucs.append(best_auc)
            y_preds_path = 'stack_preds/lgb_feat_search_pred_{}.npz'.format(i)
            check_path(y_preds_path)
            np.savez(y_preds_path, y_pred_list=y_pred_list, aucs=aucs)
            message = 'Saved y_preds to {}. Best auc is {}'.format(y_preds_path, np.max(aucs))
            logger.info(message)
            print(message)
    def run_cv(X_train, X_test, y_train):
        config = Config()
        # train model
        tic = time.time()
        data_message = 'X_train.shape={}, X_test.shape={}'.format(X_train.shape, X_test.shape)
        print(data_message)
        logger.info(data_message)
        lgb_model, best_auc, best_round, cv_result = lgb_fit(config, X_train, y_train)
        print('Time cost {}s'.format(time.time() - tic))
        result_message = 'best_round={}, best_auc={}'.format(best_round, best_auc)
        logger.info(result_message)
        print(result_message)
        # predict
        # lgb_model = lgb.Booster(model_file=config.save_model_path)
        now = time.strftime("%m%d-%H%M%S")
        result_path = 'result/result_lgb_{}-{:.4f}.csv'.format(now, best_auc)
        check_path(result_path)
        lgb_predict(lgb_model, X_test, result_path)
    if __name__ == '__main__':
        # get feature
        feature_path = 'features/'
        train_data, test_data = load_feat(re_get=True, feature_path=feature_path)
        train_feats = train_data.columns.values
        test_feats = test_data.columns.values
        drop_columns = list(filter(lambda x: x not in test_feats, train_feats))
        X_train = train_data.drop(drop_columns, axis=1)
        y_train = train_data['label']
        X_test = test_data
        data_message = 'X_train.shape={}, X_test.shape={}'.format(X_train.shape, X_test.shape)
        print(data_message)
        logger.info(data_message)
        # 根据特征搜索中最好的结果丢弃部分特征
        # n_drop_col = 141
        # drop_cols = feature_names[-n_drop_col:]
        # X_train = X_train.drop(drop_cols, axis=1)
        # X_test = X_test.drop(drop_cols, axis=1)
        # 直接训练
        run_cv(X_train, X_test, y_train)
        # 特征搜索
        # get feature scores
        # try:
        #     df_lgb_feat_score = pd.read_csv('features/lgb_features.csv')
        #     feature_names = df_lgb_feat_score.feature.values
        # except Exception as e:
        #     print('You should run the get_no_used_features.py first.')
        # run_feat_search(X_train, X_test, y_train, feature_names)
    

    注意:该案例还使用了XGboost和catBoost模型,以及其他特征提取方法,在此不详述。数据+模型见github

    https://github.com/yongyehuang/DC-hi_guides

    5 lightGBM的坑

    5.1 设置提前停止

    如果在训练过程中启用了提前停止,可以用 bst.best_iteration从最佳迭代中获得预测结果:

    ypred = bst.predict(data,num_iteration = bst.best_iteration )

    5.2 自动处理类别特征

  • 当使用本地分类特征,LightGBM能提供良好的精确度。不像简单的one-hot编码,lightGBM可以找到分类特征的最优分割。
  • 用categorical_feature指定分类特征
  • 首先需要转换为int类型,并且只支持非负数。转换为连续范围更好。
  • 使用min_data_per_group,cat_smooth去处理过拟合(当#data比较小,或者#category比较大)
  • 对于具有高基数的分类特征(#category比较大),最好转换为数字特征。
  • lightGBM通过默认方式处理缺失值,可以通过设置use_missing = false 来使其无效。
  • lightGBM通过默认的方式用NA(NaN)去表示缺失值,可以通过设置zero_as_missing = true 将其变为0
  • 当设置zero_as_missing = false(默认)时,在稀疏矩阵里(和lightSVM),没有显示的值视为0
  • 当设置zero_as_missing = true时,NA和0(包括在稀疏矩阵里,没有显示的值)视为缺失。
  •