gbdt源码解读
最近看了一些gbdt方面的内容,发现网上的一些文章都语焉不详,在git上找了份源码
gbdt算法要点
for i in range(self.maxTreeNum)
- gbdt采用bosting方法,就是一棵棵树逐步的降低残差
- gbdt的基分类器是二叉树,二叉树的某个节点分裂方式是
for i in range(dim):
#遍历所有的数据
for j in range(size):
#令 x_train[j,i]为分裂点
splitNum = x_train[j,i]
leftSubTree=[]
rightSubTree=[]
#以splitNum 为分裂点,对于第i个feature ,将数据分成两类,
for k in range(size):
tmpNum=x_train[k,i]
if tmpNum<=splitNum:
leftSubTree.append(residualGradient[k])
else:
rightSubTree.append(residualGradient[k])
sumLoss=0.0
#分别计算左右子树的loss,再求和,通过最小化loss,来决定分裂的feature和分裂的值
sumLoss+=self.calculateSquareLoss(np.array(leftSubTree))
sumLoss+=self.calculateSquareLoss(np.array(rightSubTree))
if sumLoss<minLossValue:
bestSplitPointDim=i
bestSplitPointValue=splitNum
minLossValue=sumLoss
print(treeHeight,bestSplitPointDim)
'''
-
一棵二叉树的计算复杂度是,树的深度*特征的个数*(样本个数)的平方
-
gbdt的算法复杂度=树的个数*树的深度*特征的个数*(样本个数)的平方
-
gbdt的每棵树在分裂是穷举所有的feature,穷举这个feature下所有的点,分别计算loss,最小的那个loss对应的那个feature 就是最优feature,对应的那个value 就是分裂点
-
xgboost在穷举所有的feature 时,采用并行计算,能够提高计算效率
-
gbdt在算法本质上与xgboost相同,xgboost 采用二阶导数,并且加了L1,L2正则,
-
在现如今,gbdt已经过时,他的优势是可解释性,与深度学习相比,gbdt的输入只是一位的向量,他孤立的遍历这个向量每个元素的值来实现分类回归,这不免有些浅薄,模型如果要获得更好的结果,他一定要有泛化能力,泛化能力来源于高维特征.
-
在深度学习上,也有类似的做法,比如对同样的数据,建立多个深度学习模型,然后提取模型的某一层feature,比如将一个矩阵通多个模型过映射成多个向量,再把这些向量,拼接起来,之后嘛,你可用dnn,也可以用gbdt,随所愿你
from GBDTReg import GBDT
class Config(object):
learningRate=0.1
maxTreeLength=4
maxLeafCount=30
maxTreeNum=4
首先是遍历range(self.maxTreeNum)树的数量, residualGradient=(y-y0)然后开始分裂树,
def splitTree(self,x_train,residualGradient,treeHeight):
"""
:param x_train:训练数据
:param residualGradient:当前需要拟合的梯度残差值
:param treeHeight:树的高度
:return:建好的GBDT树
"""
size = len(x_train)
dim = len(x_train[0])
#约定:左子树是小于等于,右子树是大于
bestSplitPointDim=-1
bestSplitPointValue=-1
curLoss = self.calculateSquareLoss(residualGradient)
minLossValue=curLoss
if treeHeight==self.maxTreeLength:
return curLoss
tree=dict([])
for i in range(dim):
for j in range(size):
splitNum = x_train[j,i]
leftSubTree=[]
rightSubTree=[]
for k in range(size):
tmpNum=x_train[k,i]
if tmpNum<=splitNum:
leftSubTree.append(residualGradient[k])
else:
rightSubTree.append(residualGradient[k])
sumLoss=0.0
sumLoss+=self.calculateSquareLoss(np.array(leftSubTree))
sumLoss+=self.calculateSquareLoss(np.array(rightSubTree))
if sumLoss<minLossValue:
bestSplitPointDim=i
bestSplitPointValue=splitNum
minLossValue=sumLoss
#如果损失值没有变小,则不作任何改变,也就是下面的归位一个Node
if minLossValue==curLoss:
return np.mean(residualGradient)
else:
leftSplit=[(x_train[i],residualGradient[i]) for i in range(size) if x_train[i,bestSplitPointDim]<=bestSplitPointValue ]
rightSplit=[(x_train[i],residualGradient[i]) for i in range(size) if x_train[i,bestSplitPointDim]>bestSplitPointValue ]
# print(leftSplit)
newLeftSubTree = list(zip(*leftSplit))[0]
newLeftResidual = list(zip(*leftSplit))[1]
leftTree = self.splitTree(np.array(newLeftSubTree),newLeftResidual,treeHeight+1)
newRightSubTree = list(zip(*rightSplit))[0]
newRightResidual =list(zip(*rightSplit))[1]
rightTree = self.splitTree(np.array(newRightSubTree),newRightResidual,treeHeight+1)
tree[(bestSplitPointDim,bestSplitPointValue)]=[leftTree,rightTree]
return tree
通过对split方法的解读,可以看出所谓梯度提升树枝,就是递归的方式更新梯度的同时,划分左子树和右子树。一直到loss不在降低。如果本棵树loss不再降低,self.tree.append(curTree),最后的模型是多棵树的叠加。
我面试候选人时必问的一个问题GBDT中的梯度是什么对什么的梯度?
给一个有m个样本,n维特征的数据集,如果用LR算法,那么梯度是几维?
同样的m*n数据集,如果用GBDT,那么梯度是几维?m维?n维?m*n维?或者是与树的深度有关?或者与树的叶子节点的个数有关?
1 当前y对之前所有轮次的tree的和的gradient
2 n维
3 m维,还有就是gbdt在一阶泰勒展开后需要添加正则化以防止naive solution,而xgb展开到二阶后,如果cost function的二阶导数大于0,相当于某种意义上的自带正则
for i in range(self.maxTreeNum):
print("the tree %i-th"%i)
# 之前所有轮次的tree的和的gradient
residualGradient = -1*self.learningRate*(curValue-y_train) #梯度维度与样本数量有关
curTree = self.splitTree(x_train,residualGradient,1)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App