四、LightGBM
LightGBM也是常用的GBDT工具包,速度快于XGBoost,精度也还可以,其设计理念为:
-单个机器在不牺牲速度的情况下,尽可能使用上更多的数据
-多机并行时,通信的代价尽可能的低,并且在计算上可以做到线性加速
所以其使用分布式的GBDT,选择了基于直方图的决策树算法。
LightGBM在很多方面会比XGBoost表现的更为优秀。它有以下优势:
-更快的训练效率
-低内存使用
-更高的准确率
-支持并行化学习
-可处理大规模数据
-支持直接使用category特征
主要流程:
按直方图查找最佳分割 FindBestSplitByHistogram
Input:Training data X,Current Model
First order gradient G,second order gradient H
For all Leaf p in : #
训练前将特征值转换为bin
For all f in X.Features:
#构造直方图construct histogram
H = new Histogram()
For i in (0, num_of_row): #使用bin索引直方图,不需要排序
#从直方图中找到最佳分割
For i in (0, len(H)): #遍历bins
if :
直方图算法
回忆:XGBoost中的Exact greedy算法(贪心算法):
-对每个特征都按照特征值进行排序
-在每个排好序的特征都寻找最优切分点
-用最优切分点进行切分
优点是比较精确,缺点是空间消耗比较大,时间开销大和对内存不友好,使用直方图算法进行划分点的查找可以克服这些缺点。
直方图算法(Histogram algorithm)把连续的浮点特征值离散化为k个整数(也就是分桶bins的思想),比如[0,0.1)-->0,[0.1,0.3)-->1.并根据特征所在的bin对其进行梯度累加和个数统计,然后根据直方图,寻找最优的切分点。
直方图算法
如何分桶bins?数值型特征和类别特征采用的方法是不同的
-数值型特征:
-对特征值去重后进行排序(从大到小),并统计每个值的counts
-取max_bin和distinct_value.size()中的较小值作为bins_num
-计算bins中的平均样本个数mean_bin_size,若某个distinct_value的count大于mean_bin_size,则该特征值作为bins的上界,小于该特征值的第一个distinct value作为下界;若某个distinct_value的count小于mean_bin_size,则要进行累计后再分组
-类别特征:
-首先对特征取值按出现的次序排序(大到小)
-取前min(max_bin,distinct_values_int.size())中的每个特征做第3步(忽略一些出现次数很少的特征取值)
-用bin_2_categorical_(vector类型,记录bin对应的特征取值)和categorical_2_bin_(unordered_map类型,将特征取值到哪个bin和一一对应起来),这样就可以方便进行两者之间的转换了。
如何构建直方图?
给定一个特征值,可以转化为对于的bin了,就要构建直方图了,直方图中累加了一阶梯度和二阶梯度,并统计了取值的个数。
for ( ; i < num_data ; ++i ){
const VAL_T bin = data_[data_indices[i]];
out[bin].sum_gradients += ordered_gradients[i];
out[bin].sum_hessians += ordered_hessians[i];
++out[bin].cnt;
}
注:直方图作差(一个叶子节点的直方图可以由它的父节点的直方图与其兄弟节点的直方图作差得到。使用这个方法,构建完一个叶子节点的直方图之后,就可以用较小的代价得到兄弟节点的直方图,相当于速度提升了一倍)
寻找最优切分点:
-遍历所有bin
-以当前的bin作为分割点,累加左边的bins至当前的bin梯度和及样本数量,并利用直方图作差求得右边的梯度和样本数量
-代入公式计算增益
-在遍历过程中取得最大的增益,以此时的特征和bin的特征值作为分裂节点的特征及取值。
for i in (0 , len(H)):
if :
直方图算法的优点:
-减少内存占用
-缓存命中率提高,直方图中梯度存放是连续的
-计算效率提高,相对于XGBoost中预排序每个特征都要遍历数据,复杂度为O(#feature * #data),而直方图算法只需要遍历每个特征的直方图即可,复杂度为O(#feature * #bins),
-在进行数据并行时,可大幅度降低通信代价
直方图算法改进
直方图算法仍有优化的空间,建立直方图的复杂度为O(#feature * #data),如果能降低特征数或者降低样本数,训练的时间会大大减少。假如特征存在冗余时,可以使用PCA等算法进行降维,但特征通常是精心设计的,去除它们中的任何一个都可能会影响训练精度。所以在LightGBM中有GOSS算法和EFB算法
GOSS算法
样本的梯度越小,样本的训练误差越小,表示样本已经训练的很好,直接的做法是丢掉这部分样本,然而直接丢掉会影响输数据的分布,因此在LightGBM中采用了one-side采样方式来适配:GOSS(Gradient-based One-Side Sampling)采样策略,它保留所有的大梯度样本,对小梯度样本进行随机采样。
原始直方图算法下,在第j个特征,值为d处进行分裂带来的增益可以定义为:
其中O为在决策树待分裂节点的训练集,
并且
采用GOSS之后,在第j个特征,值为d处进行分裂带来的增益可以定义为:
其中,
(A表示大梯度样本集,B表示小梯度样本中随机采样的结果)
EFB算法
高维数据通常是稀疏的,而且许多特征是互斥的(即两个或多个特征列不会同时为非0),LightGBM根据这一特点提出了EFB(exclusive feature bundling)算法将互斥特征进行合并,能够合并的特征为一个#bundle,从而将特征的维度降下来,相应的,构建histogram所耗费的时间复杂度也从O(#data * #feature)变为O(#data * #bundle),其中#feature >> #bundle。实现起来的难点:
-哪些特征可以合并为一个bundle? ——Greedy bundle
-如何将特征合并为bundle,实现降维? ——Merge Exclusive Features
Greedy bundle的原理与图着色相同,给定一个图G,定点为V,表示特征,边为E,表示特征之间的互斥关系,接着采用贪心算法对图进行着色,以此来生成bundle。
searchOrder<-G.sortByDegree()对特征按度降序排序
for j循环,遍历特征,将其划分到冲突较小的bundle中,若没有,则新建bundle
该算法复杂度为O(#feature ^ 2),当特征数很大时,效率不高,这时可以不建立图,采用特征中非零值的个数作为排序的值,因为非零值越多冲突也就越大。
MEF(Merge Exclusive Features)将bundle中的特征合并为新的特征,合并的关键是原有的不同特征值在构建后的bundle中仍然能够识别。由于基于histogram的方法存储的是离散的bin而不是连续的数值,因此可以通过添加偏移的方法将不同特征的bin值设定为不同的区间。
https://papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pdf ——原论文
树的生长策略
在XGBoost中,树是按层生长的,同一层的所有节点都做分裂,最后剪枝,它不加区分的对待同一层的叶子,带来了很多没必要的开销,因为实际上很多叶子的分裂增益较低,没必要进行搜索和分裂。
LightGBM的生长策略是leaf-wise,以降低模型损失最大化为目的,对当前所有叶子节点中切分增益最大的leaf节点进行切分。leaf-wise的缺点是会生成比较深的决策树,为了防止过拟合,可以在模型参数中设置决策树的深度。
系统设计
主要介绍LightGBM中的并行计算优化方法,在此,工作的节点称为worker,LightGBM具有支持高效并行的特点,原生支持并行学习,目前支持:
-特征并行
-数据并行
-Voting并行(数据并行的一种)
特征并行
特征并行是并行化决策树中寻找最优划分点的过程。特征并行是将对特征进行划分,每个worker找到局部的最佳切分点,针对点对点通信找到全局的最佳切分点。
传统算法:
不同的worker存储不同的特征集,在找到全局的最佳划分点后,具有该划分点的worker进行节点分裂,然后广播切分后的左右子树数据结果,其他worker收到结果后也进行划分。
LightGBM中算法:
每个worker中保存了所有的特征集,在找到全局的最佳划分点后每个worker可自行进行划分,不再进行广播划分结果,减小了网络的通信量。但存储代价变高。
数据并行
数据并行的目标是并行化整个决策学习的过程。每个worker中拥有部分数据,独立的构建局部直方图,合并后得到全局直方图,在全局直方图中寻找最优切分点进行分裂。
Voting Parallel
LightGBM采用一种称为PV-Tree的算法进行投票并行,其实这本质上也是一种数据并行。PV-Tree和普通的决策树差不多,只是在寻找最优切分点上有所不同。
每个worker拥有部分数据,独自构建直方图并找到top-k最优的划分特征,中心worker聚合得到最优的2K个全局划分特征,再向每个worker收集top-2k特征的直方图,并且合并得到最优划分,广播给所有worker进行本地划分。
实践:
使用XGBoost设置树的深度,在lightGBM中是叶子节点的个数
num_leaves = 2^(max_depth)
代码:
import lightgbmas lgb
from sklearn.datasetsimport load_breast_cancer
from sklearn.model_selectionimport train_test_split
from sklearn.metricsimport accuracy_score
import matplotlib.pyplotas plt
# 加载数据集
breast = load_breast_cancer()
# 获取特征值和目标值
x,y = breast.data,breast.target
# 获取特征名称
feature_name = breast.feature_names
# 数据集划分
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.2,random_state=0)
# 数据格式转换
lgb_train = lgb.Dataset(x_train,y_train)
lgb_eval = lgb.Dataset(x_test,y_test,reference=lgb_train)
# 参数设置
boost_round =50#迭代次数
early_stop_rounds =10#验证数据在early_stop_rounds轮中未提高,则提前停止
params = {
'boosting_type':'gbdt',#设置提mu'bia'han'shu升类型
'objective':'regression',#目标函数
'metric':{'12','auc'},#评估函数
'num_leaves':31,#叶子节点数
'learning_rate':0.05,#学习速率
'feature_fraction':0.9,#建树的特征选择比例
'bagging_fraction':0.8,#建树的样本采集比例
'bagging_freq':5,#k意味着每k次迭代执行bagging
'verbose':1#<0,显示致命的,=0显示错误(警告),>0显示信息
}
# 训练模型,加入提前停止的功能
results = {}
gbm = lgb.train(
params,
lgb_train,
num_boost_round=boost_round,
valid_sets=(lgb_eval,lgb_train),
valid_names=('validate','train'),
early_stopping_rounds=early_stop_rounds,
evals_result=results
)
# 模型预测
y_pred = gbm.predict(x_test,num_iteration=gbm.best_iteration)
print(y_pred)
lgb.plot_metric(results)
plt.show()
# 绘制重要的特征
lgb.plot_importance(gbm,importance_type='split')
plt.show()