欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LightGBM算法总结

程序员文章站 2024-02-26 17:32:46
...

1 LightGBM原理

1.1 GBDT和 LightGBM对比

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

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

  • 更快的训练速度
  • 更低的内存消耗
  • 更好的准确率
  • 分布式支持,可以快速处理海量数据

​ 如下图,在 Higgs 数据集上 LightGBM 比 XGBoost 快将近 10 倍,内存占用率大约为 XGBoost 的1/6,并且准确率也有提升。

LightGBM算法总结

1.2 LightGBM 的动机

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

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

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

1.3 Xgboost 原理

​ 目前已有的 GBDT 工具基本都是基于预排序的方法(pre-sorted)的决策树算法(如 xgboost)。这种构建决策树的算法基本思想是:   

  • 首先,对所有特征都按照特征的数值进行预排序。   

  • 其次,在遍历分割点的时候用O(#data)的代价找到一个特征上的最好分割点。   

  • 最后,找到一个特征的分割点后,将数据分裂成左右子节点。   

    这样的预排序算法的优点是:能精确地找到分割点。   

    缺点也很明显:   

  • 首先,空间消耗大。这样的算法需要保存数据的特征值,还保存了特征排序的结果(例如排序后的索引,为了后续快速的计算分割点),这里需要消耗训练数据两倍的内存。   

  • 其次,时间上也有较大的开销,在遍历每一个分割点的时候,都需要进行分裂增益的计算,消耗的代价大。

  • 最后,对 cache 优化不友好。在预排序后,特征对梯度的访问是一种随机访问,并且不同的特征访问的顺序不一样,无法对 cache 进行优化。同时,在每一层长树的时候,需要随机访问一个行索引到叶子索引的数组,并且不同特征访问的顺序也不一样,也会造成较大的 cache miss。

1.4 LightGBM 优化

LightGBM 优化部分包含以下:

  • 基于 Histogram 的决策树算法
  • 带深度限制的 Leaf-wise 的叶子生长策略
  • 直方图做差加速
  • 直接支持类别特征(Categorical Feature)
  • Cache 命中率优化
  • 基于直方图的稀疏特征优化
  • 多线程优化。

​ 下面主要介绍 Histogram 算法、带深度限制的 Leaf-wise 的叶子生长策略和直方图做差加速。

1.4.1 Histogram 算法

​ 直方图算法的基本思想是先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。在遍历数据的时候,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。

LightGBM算法总结

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

LightGBM算法总结

​ 然后在计算上的代价也大幅降低,预排序算法每遍历一个特征值就需要计算一次分裂的增益,而直方图算法只需要计算k次(k可以认为是常数),时间复杂度从O(#data*#feature)优化到O(k*#features)。  

​ 当然,Histogram 算法并不是完美的。由于特征被离散化后,找到的并不是很精确的分割点,所以会对结果产生影响。但在不同的数据集上的结果表明,离散化的分割点对最终的精度影响并不是很大,甚至有时候会更好一点。

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

1.4.2 带深度限制的 Leaf-wise 的叶子生长策略   

​ 在 Histogram 算法之上,LightGBM 进行进一步的优化。首先它抛弃了大多数 GBDT 工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise) 算法。Level-wise 过一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。但实际上 Level-wise 是一种低效的算法,因为它不加区分的对待同一层的叶子,带来了很多没必要的开销,因为实际上很多叶子的分裂增益较低,没必要进行搜索和分裂。

LightGBM算法总结

​ Leaf-wise 则是一种更为高效的策略,每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同 Level-wise 相比,在分裂次数相同的情况下,Leaf-wise 可以降低更多的误差,得到更好的精度。Leaf-wise 的缺点是可能会长出比较深的决策树,产生过拟合。因此 LightGBM 在 Leaf-wise 之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。

LightGBM算法总结

1.4.3 直方图加速

​ LightGBM 另一个优化是 Histogram(直方图)做差加速。一个容易观察到的现象:一个叶子的直方图可以由它的父亲节点的直方图与它兄弟的直方图做差得到。通常构造直方图,需要遍历该叶子上的所有数据,但直方图做差仅需遍历直方图的k个桶。利用这个方法,LightGBM 可以在构造一个叶子的直方图后,可以用非常微小的代价得到它兄弟叶子的直方图,在速度上可以提升一倍。

LightGBM算法总结

1.4.4 直接支持类别特征

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

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

类目 预排序算法预排序算法 lightGBM
内存占用 2*#feature*#data*4Bytes *#feature*#data*1Bytes
统计量累积 O(*#feature*#data) O(*#feature*#data)
分割增益计算 O(*#feature*#data) O(*#feature*#k)
直方图做差 N/A 加速一倍
直接支持类别特征 N/A 在Expo数据上加速8倍
Cache优化 N/A 在Higgs数据上加速40%
带深度限制的Leaf-wise的决策树算法 N/A 精度更
1.4.5 LightGBM并行优化

LightGBM 还具有支持高效并行的优点。LightGBM 原生支持并行学习,目前支持特征并行数据并行的两种。

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

LightGBM 针对这两种并行方法都做了优化:

  • 在特征并行算法中,通过在本地保存全部数据避免对数据切分结果的通信;
  • 在数据并行中使用分散规约 (Reduce scatter) 把直方图合并的任务分摊到不同的机器,降低通信和计算,并利用直方图做差,进一步减少了一半的通信量。基于投票的数据并行则进一步优化数据并行中的通信代价,使通信代价变成常数级别。在数据量很大的时候,使用投票并行可以得到非常好的加速效果。

LightGBM算法总结

LightGBM算法总结

LightGBM算法总结

1.5 其他注意

  • 当生长相同的叶子时,Leaf-wise 比 level-wise 减少更多的损失。
  • 高速,高效处理大数据,运行时需要更低的内存,支持 GPU
  • 不要在少量数据上使用,会过拟合,建议 10,000+ 行记录时使用。

2 lightGBM代码

2.1 基础代码

# 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])

2.2 模板代码

2.2.1 二分类
import lightgbm as lgb  
import pandas as pd  
import numpy as np  
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)  
2.2.2 多分类
import lightgbm as lgb  
import pandas as pd  
import numpy as np  
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)  

2.3 lightGBM 和 xgboost 的代码比较

2.3.1 划分训练集测试集
#xgboost
dtrain = xgb.DMatrix(x_train,label=y_train)
dtest = xgb.DMatrix(x_test)


# lightgbm
train_data = lgb.Dataset(x_train,label=y_train)
2.3.2 设置参数
#xgboost
parameters = {
    'max_depth':7, 
    'eta':1, 
    'silent':1,
    'objective':'binary:logistic',
    'eval_metric':'auc',
    'learning_rate':.05}

# lightgbm
param = {
    'num_leaves':150, 
    'objective':'binary',
    'max_depth':7,
    'learning_rate':.05,
    'max_bin':200}
param['metric'] = ['auc', 'binary_logloss']
2.3.3 模型训练
#xgboost
num_round = 50
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()
2.3.4 模型执行时间
#xgboost
execution_time_xgb = stop - start 
execution_time_xgb

# lightgbm
execution_time_lgbm = stop - start
execution_time_lgbm
2.3.5 模型测试
#xgboost
ypred = xg.predict(dtest) 
ypred

# lightgbm
ypred2 = lgbm.predict(x_test)
ypred2[0:5]   
2.3.6 分类转换
#xgboost
for i in range(0,9769): 
    if ypred[i] >= .5:       # setting threshold to .5 
       ypred[i] = 1 
    else: 
       ypred[i] = 0

# lightgbm
for i in range(0,9769):
    if ypred2[i] >= .5:       # setting threshold to .5
       ypred2[i] = 1
    else:  
       ypred2[i] = 0
2.3.7 准确率计算
#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
2.3.8 roc_auc_score计算
#xgboost
auc_xgb =  roc_auc_score(y_test,ypred)

# lightgbm
auc_lgbm = roc_auc_score(y_test,ypred2)

最后可以建立一个 dataframe 来比较 Lightgbm 和 xgb:

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

3 lightGBM调参

3.1 参数

3.1 控制参数
Control Parameters 含义 用法
max_depth 树的最大深度 当模型过拟合时,可以考虑首先降低 max_depth
min_data_in_leaf 叶子可能具有的最小记录数 默认20,过拟合时用
feature_fraction 例如 为0.8时,意味着在每次迭代中随机选择80%的参数来建树 boosting 为 random forest 时用
bagging_fraction 每次迭代时用的数据比例 用于加快训练速度和减小过拟合
early_stopping_round 如果一次验证数据的一个度量在最近的early_stopping_round 回合中没有提高,模型将停止训练 加速分析,减少过多迭代
lambda 指定正则化 0~1
min_gain_to_split 描述分裂的最小 gain 控制树的有用的分裂
max_cat_group 在 group 边界上找到分割点 当类别数量很多时,找分割点很容易过拟合时
3.2 核心参数
CoreParameters 含义 用法
Task 数据的用途 选择 train 或者 predict
application 模型的用途 选择 regression: 回归时,binary: 二分类时,multiclass: 多分类时
boosting 要用的算法 gbdt, rf: random forest, dart: Dropouts meet Multiple Additive Regression Trees, goss: Gradient-based One-Side Sampling
num_boost_round 迭代次数 通常 100+
learning_rate 如果一次验证数据的一个度量在最近的 early_stopping_round 回合中没有提高,模型将停止训练 常用 0.1, 0.001, 0.003…
num_leaves 默认 31
device cpu 或者 gpu
metric mae: mean absolute error , mse: mean squared error , binary_logloss: loss for binary classification , multi_logloss: loss for multi classification
3.3 IO参数
IO parameter 含义
max_bin 表示 feature 将存入的 bin 的最大数量
categorical_feature 如果 categorical_features = 0,1,2, 则列 0,1,2是 categorical 变量
ignore_column categorical_features 类似,只不过不是将特定的列视为categorical,而是完全忽略
save_binary 这个参数为 true 时,则数据集被保存为二进制文件,下次读数据时速度会变快

3.2 调参

IO parameter 含义
num_leaves 取值应 <= 2 ^(max_depth), 超过此值会导致过拟合
min_data_in_leaf 将它设置为较大的值可以避免生长太深的树,但可能会导致 underfitting,在大型数据集时就设置为数百或数千
max_depth 这个也是可以限制树的深度

下表对应了 Faster Speed ,better accuracy ,over-fitting 三种目的时,可以调的参数

Faster Speed better accuracy over-fitting
max_bin 设置小一些 用较大的 max_bin max_bin 小一些
num_leaves 大一些 num_leaves 小一些
feature_fraction 来做 sub-sampling feature_fraction
bagging_fraction 和 bagging_freq 设定 bagging_fraction 和 bagging_freq
training data 多一些 training data 多一些
save_binary 来加速数据加载 直接用 categorical feature gmin_data_in_leaf 和 min_sum_hessian_in_leaf
用 parallel learning 用 dart lambda_l1, lambda_l2 ,min_gain_to_split 做正则化
num_iterations 大一些,learning_rate 小一些 max_depth 控制树的深度

4 lightGBM案例

4.1 回归案例

data来源:LightGBM包自带data

4.1.1 代码
import json
import lightgbm as lgb
import pandas as pd
from sklearn.metrics import roc_auc_score
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()))
4.1.2 运行结果
load data
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]

4.2 [ICC竞赛] 精品旅行服务成单预测

比赛说明精品旅行服务成单预测

4.2.1 业务需求

​ 提供了5万多名用户在旅游app中的浏览行为记录,其中有些用户在浏览之后完成了订单,且享受了精品旅游服务,而有些用户则没有下单。参赛者需要分析用户的个人信息和浏览行为,从而预测用户是否会在短期内购买精品旅游服务。预测用户是否会在短期内购买精品旅游服务。

4.2.2 数据表格

(1)数据整体描述:

​ 数据包含5万多名用户的个人信息,以及他们上百万条的浏览记录和相应的历史订单记录,还包含有用户对历史订单的评论信息。

​ 这些用户被随机分为2组,80%作为训练集,20%作为测试集。

​ 两组数据的处理方式和内容类型是一致的,唯一不同的就是测试集中不提供需要预测的订单类型(即是否有购买精品旅游服务)。

(2)数据详细描述:

(a)用户个人信息:userProfile_\***.csv** (*表示train或者test,下同)

数据共有四列,分别是用户id、性别、省份、年龄段。注:信息会有缺失。

例如: userid,gender,province,age

​ 100000000127,,上海,

​ 100000000231,男,北京,70后

(b)用户行为信息:action_*.csv**

数据共有三列,分别是用户id,行为类型,发生时间。

例如: userid,actionType,actionTime

​ 100000000111,1,1490971433

​ 100000000111,5,1490971446

​ 100000000111,6,1490971479

​ 100000000127,1,1490695669

​ 100000000127,5,1490695821

行为类型一共有9个,其中1是唤醒app;2~4是浏览产品,无先后关系;5~9则是有先后关系的,从填写表单到提交订单再到最后支付。

注意:数据存在一定的缺失!

(c)用户历史订单数据: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,主动热情|耐心等候,[‘平稳’, ‘很好’]

4.2.3 lightGBM模型
# -*- coding:utf-8 -*- 

from __future__ import print_function
from __future__ import division

from data_helper import *

import lightgbm as lgb
from sklearn.model_selection import train_test_split
import time
import logging.handlers

"""Train the lightGBM model."""

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

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_groupcat_smooth去处理过拟合(当#data比较小,或者#category比较大)
  • 对于具有高基数的分类特征(#category比较大),最好转换为数字特征。

5.3 自动处理缺失值

  • lightGBM通过默认方式处理缺失值,可以通过设置use_missing = false 来使其无效。
  • lightGBM通过默认的方式用NA(NaN)去表示缺失值,可以通过设置zero_as_missing = true 将其变为0
  • 当设置zero_as_missing = false(默认)时,在稀疏矩阵里(和lightSVM),没有显示的值视为0
  • 当设置zero_as_missing = true时,NA和0(包括在稀疏矩阵里,没有显示的值)视为缺失。