GBDT(Gradient Boosting Decision Tree)算法是一种强大的机器学习技术,广泛应用于分类、回归等任务。然而,为了充分发挥其性能,超参数的合理设置至关重要。超参数,如学习率、树的最大深度、子样本比例等,直接影响到模型的复杂度、训练速度和预测精度。因此,对GBDT算法的超参数进行细致的评估和调整,是确保模型性能达到最优的关键步骤。
弱评估器数量:参数n_estimators
n_estimators指的是集成算法中弱评估器的数量。对于Boosting算法来说,可以使用任意弱评估器,当然了默认的弱评估器还是决策树。GBDT算法无论是分类器还是回归器,默认弱评估器都是回归树。
X_c,y_c = load_wine(return_X_y=True,as_frame=True) #分类数据
X_r,y_r = fetch_california_housing(return_X_y=True,as_frame=True) #回归数据
# 建立GBDT分类器和回归器
clf = GBC(n_estimators=2).fit(X_c,y_c)
reg = GBR(n_estimators=2).fit(X_r,y_r)
# n_estimators_:实际迭代次数
clf.n_estimators_
2
reg.n_estimators_
2
# estimators_:实际建立的弱评估器数量
clf.estimators_
array([[DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340),
DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340),
DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340)],
[DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340),
DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340),
DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340)]],
dtype=object)
reg.estimators_
array([[DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340)],
[DecisionTreeRegressor(criterion='friedman_mse', max_depth=3,
random_state=RandomState(MT19937) at 0x1083D8340)]],
dtype=object)
- 对于
GradientBoostingRegressor
,因为处理的是回归问题,每个迭代步骤只构建一个回归树来预测连续的目标变量。所以,在回归器的estimators_
中有两个DecisionTreeRegressor
实例,与指定的n_estimators=2
相匹配。 - 对于
GradientBoostingClassifier
,如果数据是多类别的(比如 wine 数据集可能有多个类别),则在每个提升迭代中,算法会为每个类别构建一个回归树来预测该类别的概率。因此,如果数据集有三个类别,每个迭代步骤将会构建三个树,每个树预测一个类别的概率。 -
如果是二分类则不会出现这种现象
出现这种情况的根本原因在于:GBDT的弱评估器为回归树,在实际调用GBDT来完成分类任务时,需要softmax函数或者sigmoid函数对回归树输出的结果进行处理。对于二分类来说,就是直接调用sigmoid函数,输出的概率值大于0.5就被预测为类别1 ,反之预测为类别0。
对于多分类来说,就比较复杂了。在多分类任务中,我们必须求解出所有标签类别所对应的概率,在所有这些概率当中,最大的概率所对应的标签才是多分类的预测标签。此时,我们需要softmax函数(归一化指数函数)帮助我们将回归值转化为概率。而**softmax函数是接受K个连续型结果,并输出K个相对概率的函数。所以在使用softmax之前我们需要准备每个类别的概率值,因此就需要建立同等数量的弱评估器。
不难发现,使用GBDT完成多分类任务时,计算量以及弱评估器数量都会远远超出二分类以及回归类问题。
参数n_estimators对模型效果的影响:
X,y = load_breast_cancer(return_X_y=True,as_frame=True)
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3,random_state=0)
# 绘制n_estimators的学习曲线
# ===========【TIME WARNING:2min】============#
scores = []
for i in np.arange(1,300,10):
gbc = GBC(n_estimators=i)
s = cross_val_score(gbc,X,y,cv=5).mean()
scores.append(s)
print(f"当n_estimators={np.arange(1,300,10)[scores.index(max(scores))]}时,准确率取得最大值{max(scores)}")
plt.plot(np.arange(1,300,10),scores,label = "GBDT")
plt.show()
# 细化学习曲线
scores = []
for i in np.arange(1,50):
gbc = GBC(n_estimators=i)
s = cross_val_score(gbc,X,y,cv=5).mean()
scores.append(s)
print(f"当n_estimators={np.arange(1,50)[scores.index(max(scores))]}时,准确率取得最大值{max(scores)}")
plt.plot(np.arange(1,50),scores,label = "GBDT")
plt.show()
可以看出,随着参数n_estimators的增大,模型的效果是逐渐变好的。但是,n_estimators达到一定的程度之后,GBDT的精确性往往不在上升或开始波动,并且,n_estimators越大,需要的计算量和内存也越大,训练的时间也会越来越长。所以对于这个参数,我们是渴望在模型训练难度和模型效果之间取得平衡。
学习率:参数`learning_rate`
在Boosting集成算法中,集成算法最终的输出结果往往是多个弱评估器输出结果的加权平均结果。但是这个最终结果并不是在所有的弱评估器建好之后才统一加权求解的,而是在逐渐建立弱评估器的过程中就随着迭代不断计算出来的。
例如,对于样本x_i,集成算法当中一共有T棵树(也就是参数`n_estimators`的取值),现在正在建立第t个弱评估器,则第t个弱评估器上x_i的结果可以表示为f_t(x_i)。假设整个Boosting算法对样本x_i输出的结果为H(x_i),则该结果一般可以被表示为t=1~t=T过程当中,所有弱评估器结果的加权求和:
-
以上式子为boosting算法中计算方式的一般规则,并不是具体到某一个Boosting集成算法的具体公式。
# 绘制不同n_estimators、learning_rate下,模型效果变化
plt.figure(figsize=(10,6))
for i in [3,10,50,100,200]:
scores = []
for j in np.arange(0.001,0.5,0.01):
gbc = GBC(n_estimators=i,learning_rate=j,random_state=0)
s = cross_val_score(gbc,X,y,cv=5).mean()
scores.append(s)
plt.plot(np.arange(0.001,0.5,0.01),scores,label = f"n_estimators={i}")
plt.legend()
初始预测结果的设置:参数init
- 传入实例的参数
from sklearn.metrics import mean_squared_error as MSE
X,y = fetch_california_housing(return_X_y=True,as_frame=True)
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3,random_state=0)
for init in [DTR(),RFR(),"zero",None]:
reg = GBR(init = init,random_state=0)
reg = reg.fit(Xtrain,Ytrain)
MSE_train = MSE(Ytrain,reg.predict(Xtrain))
MSE_test = MSE(Ytest,reg.predict(Xtest))
print(f"{init}:")
print(f"训练集MSE:{MSE_train}")
print(f"测试集MSE:{MSE_test}")
print("\n")
zero:
训练集MSE:0.25572269323849983
测试集MSE:0.289857007749968
None:
训练集MSE:0.25572269022058913
测试集MSE:0.2897994977087412
- 不难发现,初始参数的具体输入会对模型的最终结果造成巨大影响,在init中输入训练好的模型会加重GBDT的过拟合,但同时也可能得到更好的测试集结果。
GBDT的6种损失函数:参数loss
GBDT算法的损失函数非常多,我们在调参的时候可以把损失函数作为需要调整的参数进行考量。在sklearn中,控制具体损失函数的参数为:loss
。GBDT中的损失函数因GBDT具体执行的预测任务而存在区别,同时也因标签的分布而存在区别。
老版本
对于梯度提升分类树来说,loss的备选项有2种:{"deviance", "exponential"},默认值为"deviance"。
- "deviance":直译为偏差,特指逻辑回归的损失函数——**交叉熵损失**;
- "exponential":表示的是 指数损失函数。需要注意的是:此损失函数只能支持二分类数据**。
新版本
deviance ->log_loss
对于梯度提升回归树来说,loss的备选项有如下几种:
- “squared_error”:指回归的平方误差;
- “absolute_error”:指回归的绝对误差;
- “huber”:平方误差和绝对误差的结合(使用
alpha
进行调节); - “quantile”:表示允许分位数回归(使用
alpha
指定分位数)
其中的alpha
是需要我们自己设置的超参数,由参数alpha
控制。在huber损失中,alpha是阈值,在quantile损失中,alpha用于辅助计算损失函数的输出结果,默认为0.9。
=========更新警告=========
在sklearn1.0版本及后续版本当中,损失函数"ls"与"lad"被删除了,其中"ls"的功能被"squared_error"取代,而"lad"被"absolute_error"取代。如果你在运行代码时,发现你的参数默认值、参数名称与课件中不相同,或者在运行过程中出现报错、警告等现象,你可能需要更新你的sklearn。
=========================
GBDT是工业应用最广泛的模型,工业数据大部分都极度偏态、具有长尾,因此GBDT必须考虑离群值带来的影响。数据中的离群值会极大程度地影响模型地构建,当离群值在标签当中、而我们是依赖于减小损失函数来逐渐构建算法时,这种影响会前所未有地大。因此Boosting是天生更容易被离群值影响的模型、也更擅长学习离群值的模型。
举例来说,若离群值的标签为1000,大部分正常样本的标签在0.1~0.2之间,算法一定会异常努力地学习离群值的规律,因为将离群值预测错误会带来巨大的损失。在这种状况下,最终迭代出的算法可能是严重偏离大部分数据的规律的。同样,我们也会遇见很多离群值对我们很关键的业务场景:例如,电商中的金额离群用户可能是VIP用户,风控中信用分离群的用户可能是高风险用户,这种状况下我们反而更关注将离群值预测正确。不同的损失函数可以帮助我们解决不同的问题——
-
当高度关注离群值、并且希望努力将离群值预测正确时,选择平方误差squared_error
-
这在工业中是大部分的情况。在实际进行预测时,离群值往往比较难以预测,因此离群样本的预测值和真实值之间的差异一般会较大。MSE作为预测值和真实值差值的平方,会放大离群值的影响,会让算法更加向学习离群值的方向进化,这可以帮助算法更好地预测离群值。
-
努力排除离群值的影响、更关注非离群值的时候,选择绝对误差absolute_error
-
MAE对一切样本都一视同仁,对所有的差异都只求绝对值,因此会保留样本差异最原始的状态。相比其MSE,MAE对离群值完全不敏感,这可以有效地降低GBDT在离群值上的注意力。
-
试图平衡离群值与非离群值、没有偏好时,选择huber或者quantile
-
Huberloss损失结合了MSE与MAE,在Huber的公式中,当预测值与真实值的差异大于阈值时,则取绝对值,小于阈值时,则取平方。在真实数据中,部分离群值的差异会大于阈值,部分离群值的差异会小于阈值,因此比起全部取绝对值的MAE,Huberloss会将部分离群值的真实预测差异求平方,相当于放大了离群值的影响(但这种影响又不像在MSE那样大)。因此HuberLoss是位于MSE和MAE之间的、对离群值相对不敏感的损失。
弱评估器的不纯度衡量指标:参数criterion
GBDT算法的弱评估器为决策树(确切地说是回归树),我们已经熟悉各种剪枝参数对模型的影响。因此,我们对于Boosting算法中控制弱评估器的参数应该也不陌生:
这些参数在GBDT中的用法与默认值与决策树类
DecisionTreeRegressor
中基本上一致(除了GBDT中max_depth=3),专门用于对决策树进行剪枝、控制单个弱评估器的结构,考虑到大家在决策树中已经充分掌握这些参数,我们不再对这些参数一一进行详细说明了。在这里,需要重点说明的有两部分内容,一部分梯度提升树中默认的弱评估器复杂度所带来的问题,另一部分则是梯度提升树独有的不纯度衡量指标。
- 基尼系数(Gini Impurity):
- 表示为
gini
。 - 计算公式:(gini = 1 - p2 - (1-p)2),其中p是样本属于某一类的概率。
- 基尼系数越小,表示数据集的不纯度越低,即数据的纯度越高。
- 基尼系数的计算不涉及对数,因此相对于信息熵来说,计算速度更快。
- 表示为
- 信息熵(Entropy):
- 表示为
entropy
。 - 计算公式:(entropy = -\sum_{i=1}^{n} p_i \log_2(p_i)),其中(p_i)是样本属于第i类的概率,n是类别总数。
- 信息熵用于表示数据的不确定性或混乱程度;熵值越高,数据的不确定性越大。
- 信息熵对不纯度更加敏感,因此它作为指标时,决策树的生长可能会更加“精细”。然而,在高维数据或噪音较多的情况下,这可能导致过拟合。
- 信息熵的计算涉及对数运算,因此相对于基尼系数来说,计算速度可能稍慢。
- 表示为
梯度提升树中的弱评估器复杂度:max_depth
在随机森林中,控制过拟合的参数基本都处于“关闭状态”,比如:max_depth
的默认值为None,表示弱评估器不限制深度,因此随机森林中长出的树基本上都是剪枝前的树,如果随机森林算法出现过拟合现象,那么我们就可以通过对弱评估器进行剪枝来限制集成算法的过拟合。然而,这种情况并不适用于Boosting算法一族。
从GBDT的默认参数我们可以看到,对GBDT来说,无论是分类器还是回归器,默认的弱评估器最大深度都为3,这说明GBDT默认就对弱评估器进行了剪枝操作。所以当GBDT等Boosting算法处于过拟合状态时,很难再通过剪枝的手段来控制过拟合,只能从数据上下手控制过拟合了(例如,使用参数max_features
,在GBDT中其默认值为None)。
也因此,通常认为Boosting算法比Bagging算法更不容易过拟合,也就是说在相似的数据上,Boosting算法表现出的过拟合程度会较轻。
cross_validate
和KFold:
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_validate, KFold
from sklearn.svm import SVC
# 加载数据集
iris = load_iris()
X = iris.data
y = iris.target
# 初始化SVM分类器
clf = SVC(kernel='linear', C=1, random_state=42)
# 初始化KFold对象,进行5折交叉验证
kf = KFold(n_splits=5, shuffle=True, random_state=42)
# 使用cross_validate函数进行交叉验证
scoring = ['accuracy', 'precision_macro', 'recall_macro']
cv_results = cross_validate(clf, X, y, cv=kf, scoring=scoring)
# 打印交叉验证结果
print(cv_results)
# cross_validate函数会返回一个字典,其中包含了每次迭代的评分、拟合时间和评分时间等信息
cross_validate
:这是一个用于评估模型性能的函数,它执行交叉验证并返回每次迭代的评分以及其他相关信息。KFold
:这是一个类,用于实现k折交叉验证的数据划分。它本身不进行评估,而是为交叉验证提供数据划分的机制。GridSearchCV
:这是一个类,用于执行网格搜索和交叉验证,以找到模型的最佳超参数组合。它不仅进行数据划分和模型评估,还搜索参数空间以找到最优配置。
from sklearn.model_selection import cross_validate,KFold
#定义所需的交叉验证方式
cv = KFold(n_splits=5,shuffle=True,random_state=12)
# 分类数据
X_c,y_c = load_breast_cancer(return_X_y=True,as_frame=True)
modelname = ["GBDT","RF"]
colors = ["green","orange"]
clf_models = [GBC(random_state=12),RFC(random_state=12)]
xaxis = range(1,6)
plt.figure(figsize=(10,6),dpi=65)
for name,model,color in zip(modelname,clf_models,colors):
result = cross_validate(model,X_c,y_c,cv=cv
,return_train_score=True)
plt.plot(xaxis,result["train_score"], color=color, label = name+"_Train")
plt.plot(xaxis,result["test_score"], color=color, linestyle="--",label = name+"_Test")
plt.xticks([1,2,3,4,5])
plt.xlabel("CVcounts",fontsize=12)
plt.ylabel("Accuracy",fontsize=12)
plt.title("GBDT vs RF")
plt.legend()
plt.show()
cross_validate
和KFold
是两个用于模型选择和评估的工具
# 回归数据
X_r,y_r = fetch_california_housing(return_X_y=True,as_frame=True)
# modelname = ["GBDT","RF"]
# colors = ["green","orange"]
reg_models = [GBR(random_state=12),RFR(random_state=12)]
xaxis = range(1,6)
plt.figure(figsize=(10,6),dpi=65)
for name,model,color in zip(modelname,reg_models,colors):
result = cross_validate(model,X_r,y_r,cv=cv,scoring="neg_mean_squared_error"
,return_train_score=True)
plt.plot(xaxis,result["train_score"], color=color, label = name+"_Train")
plt.plot(xaxis,result["test_score"], color=color, linestyle="--",label = name+"_Test")
plt.xticks([1,2,3,4,5])
plt.xlabel("CVcounts",fontsize=12)
plt.ylabel("MSE",fontsize=12)
plt.title("GBDT vs RF")
plt.legend()
plt.show()
对GBDT来说,不纯度的衡量指标有2个:
- friedman_mse:弗里德曼均方误差
- squared_error:平方误差
其中平方误差我们非常熟悉,就是直接计算父节点的平方误差与子节点平方误差的加权求和之间的差异。弗里德曼均方误差是由Friedman在论文《贪婪函数估计:一种梯度提升机器》(GREEDY FUNCTION APPROXIMATION: A GRADIENT BOOSTING MACHINE)中提出的全新的误差计算方式。根据论文中的描述,弗里德曼均方误差使用调和平均数来控制左右叶子节点上的样本数量,相比普通地求均值,调和平均必须在左右叶子节点上的样本量/样本权重相差不大的情况下才能取得较大的值(F1 score也是用同样的方式来调节Precision和recall)。这种方式可以令不纯度的下降得更快,让整体分枝的效率更高
- 大部分时候,使用弗里德曼均方误差可以让梯度提升树得到很好的结果,因此GBDT的默认参数就是Friedman_mse。不过许多时候,我们会发现基于平方误差的分割与基于弗里德曼均方误差的分割会得到相同的结果。
梯度提升树的提前停止
在学习机器学习理论与方法时,我们极少提及迭代的提前停止问题。在机器学习中,依赖于迭代进行工作的算法并不算多,同时课程中的数据量往往也比较小,因此难以预见需要提前停止迭代以节省计算资源或时间的情况。但对于工业界使用最广泛的GBDT而言,提前停止是需要考虑的关键问题。
对于任意需要迭代的算法,迭代的背后往往是损失函数的最优化问题。例如在逻辑回归中,我们在进行梯度下降的迭代时,是希望找到交叉熵损失函数的最小值;而在梯度提升树中,我们在一轮轮建立弱评估器过程中,也是希望找到对应损失函数的最小值。理想状态下,无论使用什么算法,只要我们能够找到损失函数上真正的最小值,那模型就达到“收敛”状态,迭代就应该被停止。
然而遗憾的是,我们和算法都不知道损失函数真正的最小值是多少,而算法更不会在达到收敛状态时就自然停止。在机器学习训练流程中,我们往往是通过给出一个极限资源来控制算法的停止,比如,我们通过超参数设置允许某个算法迭代的最大次数,或者允许建立的弱评估器的个数。因此无论算法是否在很短时间内就锁定了足够接近理论最小值的次小值、或者算法早已陷入了过拟合状态、甚至学习率太低导致算法无法收敛,大多数算法都会持续(且无效地)迭代下去,直到我们给与的极限资源全部被耗尽。对于复杂度较高、数据量较大的Boosting集成算法来说,无效的迭代常常发生,因此作为众多Boosting算法的根基算法,梯度提升树自带了提前停止的相关超参数。另外,逻辑回归看起来会自然停止,是因为逻辑回归内置提前停止机制。
我们根据以下原则来帮助梯度提升树实现提前停止:
- 当GBDT已经达到了足够好的效果(非常接近收敛状态),持续迭代下去不会有助于提升算法表现
- GBDT还没有达到足够好的效果(没有接近收敛),但迭代过程中呈现出越迭代算法表现越糟糕的情况
- 虽然GBDT还没有达到足够好的效果,但是训练时间太长/速度太慢,我们需要重新调整训练
在实际数据训练时,我们往往不能动用真正的测试集进行提前停止的验证,因此我们需要从训练集中划分出一小部分数据,专用于验证是否应该提前停止。那我们如何找到这个验证集损失不再下降、准确率不再上升的“某一时间点”呢?此时,我们可以规定一个阈值,例如,当连续n_iter_no_change
次迭代中,验证集上损失函数的减小值都低于阈值tol
,或者验证集的分数提升值都低于阈值tol
的时候,我们就令迭代停止。此时,即便我们规定的n_estimators
或者max_iter
中的数量还没有被用完,我们也可以认为算法已经非常接近“收敛”而将训练停下。这种机制就是提前停止机制Early Stopping。这种机制中,需要设置阈值tol
,用于不断检验损失函数下降量的验证集,以及损失函数连续停止下降的迭代轮数n_iter_no_change
。在GBDT当中,这个流程刚好由以下三个参数控制:
validation_fraction
:从训练集中提取出、用于提前停止的验证数据占比,值域为[0,1]。n_iter_no_change
:当验证集上的损失函数值连续n_iter_no_change次没有下降或下降量不达阈值时,则触发提前停止。平时则设置为None,表示不进行提前停止。tol
:损失函数下降的阈值,默认值为1e-4,也可调整为其他浮点数来观察提前停止的情况。