GBDT

  • 2 GBDT
    • 2.0 参数概述和迭代过程
      • 特点
      • 参数概述
      • 迭代过程
    • 2.1 init
      • 2.1.1 model
      • 2.1.2 zero
      • 2.1.3 None(sklearn默认)
      • 2.1.4 测试
    • 2.2 loss
      • 2.2.1 分类loss
      • 2.2.2 回归loss
      • 2.2.3 回归损失的选择
      • 2.2.4 探究离群值对不同的loss的影响
    • 2.3 criterion&min_impurity_decrease
      • 2.3.1 criterion(friedman_mse)
      • 2.3.2 min_impurity_decrease
    • 2.4 n_estimators&learning_rate
    • 2.5 warm_start
      • 2.5.1 增量学习
      • 2.5.2 确定巨量csv文件中样本条数
      • 2.5.3 增量学习过程
    • 2.6 GBDT回归树实现分类的方式
      • 2.6.1 二分类模型
      • 2.6.2 多分类问题
    • 2.7 提前停止机制
    • 2.8 梯度提升树的袋外数据
    • 2.9 接口(属性)
      • 2.9.1 所有接口
      • 2.9.2 接口estimators_体现出来的问题
    • 2.10 和其他集成算法对比的效果
    • 2.11 贝叶斯优化
      • 2.11.1 参数影响力
      • 2.11.2 参数空间范围
      • 2.11.3 贝叶斯优化模型
      • 2.11.4 进一步调参建议
    • 2.12 手动调参代码
      • 2.12.1 boosting算法调参思想
      • 2.12.2 调参过程代码
    • 2.13 GBDT的数学原理

2 GBDT

2.0 参数概述和迭代过程

特点

  • 和Xgboost相似都是通过弱评估器对于残差进行拟合
  • 依据上一个弱评估器 f(x)t−1f(x)_{t-1}f(x)t−1​ 的结果,计算损失函数 L(x,y)L(x, y)L(x,y) , 并使用 L(x,y)L(x, y)L(x,y) 自适应地影响下一个弱评估器 f(x)tf(x)_{t}f(x)t​ 的构建集成模型输出的结果,受到整体所有弱评估器 f(x)0∼f(x)Tf(x)_{0} \sim f(x)_{T}f(x)0​∼f(x)T​ 的影响。
  • GBDT基学习器都是回归模型
  • GBDT以向前阶段的方式构建一个加法模型,它允许优化任意可微损失函数,损失函数有较多的选择。
  • 在GBDT算法实现时使用了随机森林的思想(subsample),在创建新的拟合树模型时,可以允许通过随机抽取部分样本数据和特征进行树的构建,增大了软评估器之间的独立性(因此存在了袋外数据)。当弱评估器表现在数据上不稳定时可以通过该方法增加稳定性。但是不适用于样本数据较小的情况。

参数概述

参数简介

类型 参数/属性
迭代过程 参数:n_estimators, learning_rate, loss, alpha, init
属性:loss_, init_, estimators_
弱评估器结构 criterion, max_depth, min_samples_split, min_samples_leaf,
min_weight_fraction_leaf, max_leaf_nodes,
min_impurity_decrease
提前停止 参数:validation_fraction, n_iter_no_change, tol
属性:n_estimators_
弱评估器的训练数据 参数:subsample, max_features, random_state
属性:oob_improvement, train_score_
其他 ccp_alpha, warm_start

GradientBoostingClassifier
GradientBoostingRegressor

sklearn.ensemble.GradientBoostingClassifier(loss="deviance",  # 下面说init=None,  # 下面说min_impurity_decrease=0.0,  # 下面说learning_rate=0.1,  # 学习率:通过“learning_rate”缩小每棵树的贡献。在学习率和n_estimators之间有一个折中n_estimators=100,  # 创建树的个数,梯度提升对过度拟合相当健壮,因此通常很大导致更好的性能, 但是会造成过拟合subsample=1.0,  # 每次创建树模型使用的样本数量比重random_state=None,verbose=0,  # 监控模型的创建criterion="friedman_mse",  # {'friedman_mse','squared_error','mse','mae'默认值='friedman_mse'min_samples_split=2,  # CART树分节点最小样本量min_samples_leaf=1,  # 叶子节点最少样本量min_weight_fraction_leaf=0.0,  # 权重总和的最小加权分数(所有输入样本)需要位于叶节点。样品有未提供 sample_weight 时,权重相等max_depth=3,  # CART树最大生长深度max_features=None,  # 单棵树最大使用的特征数量,因为max_depth最大3,这个参数就显得鸡肋{'auto', 'sqrt', 'log2'},max_leaf_nodes=None,  # 以最佳优先方式使用“max_leaf_nodes”种植树。最佳节点定义为杂质的相对减少。如果 None 则无限数量的叶节点warm_start=False, # 增量学习,见下validation_fraction=0.1,# 仅在设置n_iter_no_change时有用,通过设置一定的验证数据,对模型早停标准进行检测。默认值为0.1。n_iter_no_change=None, # 若模型连续n_iter_no_change次更新,模型的损失函数增益均在tol范围内,则停止更新。默认值为None禁用tol=1e-4, # 若模型连续n_iter_no_change次更新,模型的损失函数增益均在tol范围内,则停止更新。默认值为1e-4ccp_alpha=0.0,
)sklearn.ensemble.GradientBoostingRegressor(loss="squared_error",  # 下面说alpha=0.9,  # 对应损失函数中的一个huber,quantileinit=None,  # 下面说min_impurity_decrease=0.0,  # 下面说learning_rate=0.1,  # 学习率:通过“learning_rate”缩小每棵树的贡献。在学习率和n_estimators之间有一个折中n_estimators=100,  # 创建树的个数,梯度提升对过度拟合相当健壮,因此通常很大导致更好的性能, 但是会造成过拟合subsample=1.0,  # 每次创建树模型使用的样本数量比重random_state=None,verbose=0,  # 监控模型的创建criterion="friedman_mse",  # {'friedman_mse','squared_error','mse','mae'默认值='friedman_mse'min_samples_split=2,  # CART树分节点最小样本量min_samples_leaf=1,  # 叶子节点最少样本量min_weight_fraction_leaf=0.0,  # 权重总和的最小加权分数(所有输入样本)需要位于叶节点。样品有未提供 sample_weight 时,权重相等max_depth=3,  # CART树最大生长深度max_features=None,  # 单棵树最大使用的特征数量,因为max_depth最大3,这个参数就显得鸡肋{'auto', 'sqrt', 'log2'},max_leaf_nodes=None,  # 以最佳优先方式使用“max_leaf_nodes”种植树。最佳节点定义为杂质的相对减少。如果 None 则无限数量的叶节点# 同上warm_start=False,validation_fraction=0.1,n_iter_no_change=None,tol=1e-4,ccp_alpha=0.0,
)

迭代过程

之前我们提到过,GBDT的整体建模流程与AdaBoost高度相似,因此GBDT当中也有设置具体迭代次数(弱评估器次数)的参数n_estimators与学习率参数learning_rate,这两个参数的含义、以及对集成算法的影响与AdaBoost当中完全一致。

具体地来说,对于样本xix_ixi​,集成算法当中一共有TTT棵树,则参数n_estimators的取值为T。假设现在正在建立第ttt个弱评估器,则则第ttt个弱评估器上xix_ixi​的结果可以表示为ft(xi)f_t(x_i)ft​(xi​)。假设整个Boosting算法对样本xix_ixi​输出的结果为H(xi)H(x_i)H(xi​),则该结果一般可以被表示为t=1~t=T过程当中,所有弱评估器结果的加权求和:

H(xi)=∑t=1Tϕtft(xi)H(x_i) = \sum_{t=1}^{\color{red}T}\phi_tf_t(x_i)H(xi​)=t=1∑T​ϕt​ft​(xi​)

其中,ϕt\phi_tϕt​为第t棵树的权重。对于第ttt次迭代来说,则有:

Ht(xi)=Ht−1(xi)+ϕtft(xi)H_t(x_i) = H_{t-1}(x_i) + \phi_tf_t(x_i)Ht​(xi​)=Ht−1​(xi​)+ϕt​ft​(xi​)

在这个一般过程中,每次将本轮建好的决策树加入之前的建树结果时,可以在权重ϕ\phiϕ前面增加参数η\color{red}\etaη,表示为第t棵树加入整体集成算法时的学习率,对标参数learning_rate

Ht(xi)=Ht−1(xi)+ηϕtft(xi)H_t(x_i) = H_{t-1}(x_i) + \boldsymbol{\color{red}\eta} \phi_tf_t(x_i)Ht​(xi​)=Ht−1​(xi​)+ηϕt​ft​(xi​)

该学习率参数控制Boosting集成过程中H(xi)H(x_i)H(xi​)的增长速度,是相当关键的参数。当学习率很大时,H(xi)H(x_i)H(xi​)增长得更快,我们所需的n_estimators更少,当学习率较小时,H(xi)H(x_i)H(xi​)增长较慢,我们所需的n_estimators就更多,因此boosting算法往往会需要在n_estimatorslearning_rate中做出权衡。

这两个参数的使用方法与AdaBoost中也完全一致,故此处不再赘述,后续我们会直接使用这两个参数进行调参。

2.1 init

init:{"zero",None,model}init:\{"zero", None,model\}init:{"zero",None,model}

这个参数一般不主动调参,因为需要较大的算力,若有足够算力,就可以网格搜索进行调参。

在第一棵树创建时:
H1(xi)=H0(xi)+ϕ1f1(xi)H_{1}\left(x_{i}\right)=H_{0}\left(x_{i}\right)+ \phi_{1} f_{1}\left(x_{i}\right)H1​(xi​)=H0​(xi​)+ϕ1​f1​(xi​)
在GBDT算法推导时默认zero,但是我们使用时并不是这样,因为效果不好。

2.1.1 model

通常选择H0(xi)H_0(x_i)H0​(xi​)是一个常见好的评估器,前提是必须具备fitfitfit和predict_probapredict\_probapredict_proba接口,可以是决策树,随机森林,逻辑回归,GBDT(可以,但是…),朴素贝叶斯等

2.1.2 zero

将H0(xi)H_0(x_i)H0​(xi​)置为全0

2.1.3 None(sklearn默认)

会自动选择DummyEstimator类中的随机简单的模型预测结果,和zero结果差不多。

2.1.4 测试

from sklearn.tree import DecisionTreeRegressor as DTR
from sklearn.model_selection import KFold,cross_validate
from sklearn.ensemble import RandomForestRegressor as RFR,GradientBoostingRegressor as GBR
from sklearn.datasets import load_bostonX=load_boston().data
y=load_boston().target
tree_reg = DTR(random_state=1000)
rf = RFR(max_features='sqrt', n_estimators=68, max_depth=10,criterion='squared_error', n_jobs=-1, random_state=15234,)
for init in [tree_reg, rf, "zero", None]:reg = GBR(init=init, random_state=12432)cv = KFold(n_splits=5, shuffle=True, random_state=2432)result_reg = cross_validate(reg, X, y, cv=cv, scoring="neg_root_mean_squared_error",return_train_score=True, n_jobs=-1)print("\n"+str(init))print(abs(result_reg["train_score"].mean()))print(abs(result_reg["test_score"].mean()))数据可能太简单了,没有充分展现init设置为随机森林的优势
"""
init参数为:DecisionTreeRegressor(random_state=1000)
0.0
4.479982852526829init参数为:RandomForestRegressor(max_depth=10, max_features='sqrt', n_estimators=68,n_jobs=-1, random_state=15234)
0.7010386986200121
3.2401778542930293init参数为:zero
1.346408702672974
3.22570734810666init参数为:None
1.346408569325733
3.203902874876255
"""

2.2 loss

2.2.1 分类loss

分类loss:{"deviance","exponential"}分类loss:\{"deviance", "exponential"\}分类loss:{"deviance","exponential"}

deviance二分类交叉熵损失(常用):

L=−(yilog⁡p(xi)+(1−yi)log⁡(1−p(xi)))L=-\left(y_{i} \log p\left(x_{i}\right)+\left(1-y_{i}\right) \log \left(1-p\left(x_{i}\right)\right)\right)L=−(yi​logp(xi​)+(1−yi​)log(1−p(xi​)))

  • 注意, logloglog当中输入的一定是概率值。对于逻辑回归来说,概率就是算法的输出,因此我们可以认为逻辑回 归中 p=H(x)p=H(x)p=H(x) ,但对于GBDTGBDTGBDT来说, p(xi)=Sigmoid⁡(H(xi))p\left(x_{i}\right)=\operatorname{Sigmoid}\left(H\left(x_{i}\right)\right)p(xi​)=Sigmoid(H(xi​)) ,这一点一定要注意。

deviance多分类交叉熵损失(常用)

L=−∑c=1Kyc∗log⁡(Pk(x))L=-\sum_{c=1}^{K} y_{c}^{*} \log \left(P^{k}(x)\right)L=−c=1∑K​yc∗​log(Pk(x))

  • 其中, Pk(x)是概率值,对于多分类P^{k}(x) 是概率值,对于多分类Pk(x)是概率值,对于多分类GBDT$来说, pk(x)=Softmax⁡(Hk(x))p^{k}(x)=\operatorname{Softmax}\left(H^{k}(x)\right)pk(x)=Softmax(Hk(x)) 。 y∗y^{*}y∗ 是由真实标签转化后的向量,下是转换公式。
    yj∗={1,若 yˆj=y0,else(j=1,2...K)y_j^*= \begin{cases} 1, &\text{若 } \text{\^y}_j= y\\ 0, &else \end{cases} ~~~~(j=1,2...K)yj∗​={1,0,​若 yˆ​j​=yelse​    (j=1,2...K)

exponential二分类指数损失

L=e−yH(x)L=e^{-yH(x)}L=e−yH(x)
exponential多分类指数损失

L=exp⁡(−1Ky∗⋅H∗(x))=exp⁡(−1K(y1H1(x)+y2H2(x)+…+ykHk(x)))\begin{aligned} L &=\exp \left(-\frac{1}{K} y^{*} \cdot \boldsymbol{H}^{*}(x)\right) \\ &=\exp \left(-\frac{1}{K}\left(y^{1} H^{1}(x)+y^{2} H^{2}(x)+\ldots+y^{k} H^{k}(x)\right)\right) \end{aligned}L​=exp(−K1​y∗⋅H∗(x))=exp(−K1​(y1H1(x)+y2H2(x)+…+ykHk(x)))​

  • 一般不使用,会退化成Adaboost

2.2.2 回归loss

回归loss:{′squared_error′,′absolute_error′,′huber′,′quantile′}回归loss:\{'squared\_error', 'absolute\_error', 'huber', 'quantile'\}回归loss:{′squared_error′,′absolute_error′,′huber′,′quantile′}

squared_error平方误差

L=∑(yi−H(xi))2.L=\sum\left(y_{i}-H\left(x_{i}\right)\right)^{2}.L=∑(yi​−H(xi​))2.

absolute_error绝对误差

L=∑∣∑ij−H(xi)∣L=\sum\left|\sum_{i j}-H\left(x_{i}\right)\right|L=∑∣∣​ij∑​−H(xi​)∣∣​

huber上二权衡

L=∑l(yi,H(xi))l={12(yi−H(xi))2,∣yi−H(xi)∣≤αα(∣yi−H(xi)∣−α2),∣yi−H(xi)∣>α,α∈(0,1)\begin{array}{c} L=\sum l\left(y_{i}, H\left(x_{i}\right)\right) \\\\ l=\left\{\begin{array}{ll} \frac{1}{2}\left(y_{i}-H\left(x_{i}\right)\right)^{2}, & \left|y_{i}-H\left(x_{i}\right)\right| \leq \alpha \\ \alpha\left(\left|y_{i}-H\left(x_{i}\right)\right|-\frac{\alpha}{2}\right), & \left|y_{i}-H\left(x_{i}\right)\right|>\alpha \end{array}, \alpha \in(0,1)\right. \end{array}L=∑l(yi​,H(xi​))l={21​(yi​−H(xi​))2,α(∣yi​−H(xi​)∣−2α​),​∣yi​−H(xi​)∣≤α∣yi​−H(xi​)∣>α​,α∈(0,1)​

quantile损失

L=∑l(yi,H(xi))I={α(yi−H(xi)),yi−H(xi)>00,yi−H(xi)=0,α∈(0,1)(1−α)(yi−H(xi)),yi−H(xi)<0\begin{array}{c} L=\sum l\left(y_{i}, H\left(x_{i}\right)\right) \\\\ I=\left\{\begin{array}{ll} \alpha\left(y_{i}-H\left(x_{i}\right)\right), & y_{i}-H\left(x_{i}\right)>0 \\ 0, & y_{i}-H\left(x_{i}\right)=0, \alpha \in(0,1) \\ (1-\alpha)\left(y_{i}-H\left(x_{i}\right)\right), & y_{i}-H\left(x_{i}\right)<0 \end{array}\right. \end{array}L=∑l(yi​,H(xi​))I=⎩⎨⎧​α(yi​−H(xi​)),0,(1−α)(yi​−H(xi​)),​yi​−H(xi​)>0yi​−H(xi​)=0,α∈(0,1)yi​−H(xi​)<0​​

  • 超参数α就是回归模型中的alpha参数超参数\alpha 就是回归模型中的alpha参数超参数α就是回归模型中的alpha参数

2.2.3 回归损失的选择

GBDT是工业应用中最广泛的模型,工业数据大多都是极度偏态,长尾,因此GBDT使用时必须考虑离离群值带来的影响,数据中的离群值会极大地影响模型的构建,当离群值在标签中,而我们是依赖于减小损失构建模型,影响会前所未有的大,因此GBDT是天生容易被离群值所影响的模型,也更擅长学习离群值

有时由于离群值存在,将离群值预测错误会带来巨大的损失。在这种状况下,最终迭代出的算法可能是严重偏离大部分数据的规律的。同样,我们也会遇见很多离群值对我们很关键的业务场景:例如,电商中的金额离群用户可能是 VIP 用户,风控中信用分离群的用户可能是高风险用户,这种状况下我们反而更关注将离群值预测正确。不同的损失函数可以帮助我们解决不同的问题:

  • 当高度关注离群值、并且希望努力将离群值预测正确时,选择 MSE
    这在工业中是大部分的情况。在实际进行预测时,离群值往往比较难以预测,因此离群样本的预测值和真实值之间的差异会较大。 MSE 作为预测值和真实值差值的平方,会放大离群值的影响,会让算法更加向学习离群值的方向进化,这可以帮助算法更好地预测离群值。
  • 努力排除离群值的影响、更关注非离群值的时候,选择 MAE
    MAE 对一切样本都一视同仁,对所有的差异都只求绝对值,因此会保留样本差异最原始的状态。相比MSE , MAE 对离群值完全不敏感,这可以有效地降低 GBDT 在离群值上的注意力.
  • 试图平衡离群值与非离群值、没有偏好时,选择 Huber 或者 quantileloss
    Huberloss 损失结合了 MSE 与 MAE ,在 Huber 的公式中,当预测值与真实值的差异大于阈值时,则取绝对
    值,小于阈值时,则取平方。在真实数据中,部分离群值的差异会大于阈值,部分离群值的差异会小于阈值,因此比起全部取绝对值的 MAE , Huberloss 会将部分离群值的真实预测差异求平方,相当于放大了离群值的影响(但这种影响又不像在 MSE 那样大)。因此 HuberLoss 是位于 MSE 和 MAE 之间的、对离群值相对不敏感的损失。

2.2.4 探究离群值对不同的loss的影响

import numpy as np
y_true = np.concatenate((np.linspace(-1,1,450),np.linspace(9,10,50))) #含有50个离群值
y_pred = np.linspace(-1,1,500) #预测结果忽略了离群值,会导致离群值上的损失很大def huber_loss(y_true, y_pred, alpha=0.9):#对huber_loss,alpha位于(0,1)之间,一般是更靠近1的方向,默认0.9或者0.99error = y_true - y_pred#error小于等于阈值(如果True,则计算,如果False则直接为0)l1 = (abs(error)<=alpha)*error**2/2#error大于阈值l2 = (abs(error)>alpha)*alpha*(abs(error)-alpha/2)return  (l1+l2).sum()def squared_error(y_true,y_pred):#注意这里不是MSE,而是TSEl = (y_true - y_pred)**2return l.sum()def absolute_error(y_true,y_pred):#不是MAE,而是TAEl = abs(y_true - y_pred)return l.sum()for name,error in [("对离群值最敏感的SE\t",squared_error), ("对离群值一般敏感的Huber\t",huber_loss), ("对离群值完全不敏感的AE\t",absolute_error)]:loss = error(y_true,y_pred)abratio = error(y_true[451:],y_pred[451:])/lossprint(name,"总损失为{:.3f},离群值损失占总损失的占比为{:.3f}%".format(loss,abratio*100))"""
对离群值最敏感的SE   总损失为3705.280,离群值损失占总损失的占比为98.024%
对离群值一般敏感的Huber   总损失为369.684,离群值损失占总损失的占比为97.298%
对离群值完全不敏感的AE     总损失为475.000,离群值损失占总损失的占比为88.782%
"""

2.3 criterion&min_impurity_decrease

2.3.1 criterion(friedman_mse)

分类和回归底层都是回归树,所以 :不纯度衡量指标criterion均是回归树预测误差:
criterion‘friedman_mse’,‘squared_error’,‘mse’,default=’friedman_mse’criterion{‘friedman\_mse’, ‘squared\_error’, ‘mse’}, default=’friedman\_mse’criterion‘friedman_mse’,‘squared_error’,‘mse’,default=’friedman_mse’

下面关于不纯度衡量指标friedman_mse重点解释:下面关于不纯度衡量指标friedman\_mse重点解释:下面关于不纯度衡量指标friedman_mse重点解释:

GBDT中的弱学习器fff是CART树,因此每棵树在建立时都依赖于CART树分枝的规则进行建立。CART树每次在分枝时都只会分为两个叶子节点(二叉树),它们被称为左节点(left)和右节点(right)。在CART树中进行分枝时,我们需要找到令左右节点的不纯度之和最小的分枝方式。通常来说,我们求解父节点的不纯度与左右节点不纯度之和之间的差值,这个差值被称为不纯度下降量(impurity decrease)。不纯度的下降量越大,该分枝对于降低不纯度的贡献越大。

弗里德曼均方误差是由Friedman提出的全新的误差计算方式。使用弗里德曼均方误差之后推导出的不纯度下降量,需要极大化的公式。该公式如下:

基于弗里德曼均方误差的不纯度下降量

wlwrwl+wr∗(∑l(ri−yi^)2wl−∑r(ri−yi^)2wr)2\frac{w_lw_r}{w_l \space + \space w_r} * \left( \frac{\sum_l{(r_i - \hat{y_i})^2}}{w_l} - \frac{\sum_r{(r_i - \hat{y_i})^2}}{w_r}\right)^2wl​ + wr​wl​wr​​∗(wl​∑l​(ri​−yi​^​)2​−wr​∑r​(ri​−yi​^​)2​)2

其中www是左右叶子节点上的样本量,当我们对样本有权重调整时,www则是叶子节点上的样本权重。rir_iri​大多数时候是样本i上的残差(GBDT原理就是拟合残差rir_iri​,父节点中样本i的预测结果与样本i的真实标签之差),也可能是其他衡量预测与真实标签差异的指标,yi^\hat{y_i}yi​^​是样本i样本i样本i在当前子节点下的预测值。所以这个公式其实可以解读成:

左右叶子节点上样本量的调和平均 * (左叶子节点上均方误差 - 右叶子节点上的均方误差)^2

根据论文中的描述,弗里德曼均方误差使用调和平均数(分子上相乘分母上相加)来控制左右叶子节点上的样本数量,相比普通地求均值,调和平均必须在左右叶子节点上的样本量/样本权重相差不大的情况下才能取得较大的值(F1 score也是用同样的方式来调节Precision和recall)。这种方式可以令不纯度的下降得更快,让整体分枝的效率更高。

本质:保证子节点样本量相似(更快迭代),不纯度差异更大(更精确的结果)

下面关于不纯度衡量指标squared_error解释:下面关于不纯度衡量指标squared\_error解释:下面关于不纯度衡量指标squared_error解释:

除了Friedman_mse之外,我们也可以使用普通的平方误差作为不纯度的衡量。使用普通平方误差时,我们可以直接计算父节点的平方误差与子节点平方误差的加权求和之间的差异。

平方误差的不纯度下降量

∑p(ri−yi^)2wl+wr−(wlwl+wr∗∑l(ri−yi^)2+wrwl+wr∗∑r(ri−yi^)2)\frac{\sum_p{(r_i - \hat{y_i})^2}}{w_l + w_r} - (\frac{w_l}{w_l+w_r} * \sum_l{(r_i - \hat{y_i})^2} + \frac{w_r}{w_l+w_r} * \sum_r{(r_i - \hat{y_i})^2})wl​+wr​∑p​(ri​−yi​^​)2​−(wl​+wr​wl​​∗l∑​(ri​−yi​^​)2+wl​+wr​wr​​∗r∑​(ri​−yi​^​)2)

大部分时候,使用弗里德曼均方误差可以让梯度提升树得到很好的结果,因此GBDT的默认参数就是Friedman_mse。不过许多时候,我们会发现基于平方误差的分割与基于弗里德曼均方误差的分割会得到相同的结果。

2.3.2 min_impurity_decrease

Nt/N∗(impurity−NtR/Nt∗rightimpurity−NtL/Nt∗leftimpurity)N_t / N * (impurity - N_{t R} / N_t * right_{impurity} - N_{tL} / N_t * left_{impurity})Nt​/N∗(impurity−NtR​/Nt​∗rightimpurity​−NtL​/Nt​∗leftimpurity​)

其中NNN是样本总数,NtN_tNt​是样本数,当前节点的样本数,NtLN_{tL}NtL​ 是当前节点左孩子的样本数,NtRN_{tR}NtR​是右孩子的样本数。N,Nt,NtR和NtLN, N_t, N_{tR}和 N_{tL}N,Nt​,NtR​和NtL​都是指加权和。

2.4 n_estimators&learning_rate

Ht−1(xi)H_{t-1}\left(x_{i}\right)Ht−1​(xi​)为前t-1棵树的预测值累加,ft(xi)f_{t}\left(x_{i}\right)ft​(xi​)就是单个树模型对于残差的拟合值,ϕt\phi_{t}ϕt​为第t棵树的权值,一般是常量1,η\etaη就是 learning_ratelearning\_ratelearning_rate ,最终的 ttt 就是n_estimatorsn\_estimatorsn_estimators
Ht(xi)=Ht−1(xi)+ηϕtft(xi)H_{t}\left(x_{i}\right)=H_{t-1}\left(x_{i}\right)+\eta \phi_{t} f_{t}\left(x_{i}\right)Ht​(xi​)=Ht−1​(xi​)+ηϕt​ft​(xi​)

import sklearn
import matplotlib as mlp
import numpy as np
import pandas as pd
from time import time
from sklearn.ensemble import GradientBoostingRegressor as GBR
data = pd.read_csv(r"D:\学习资料\sklearn课件\集成算法code&数据集\公开课sample_data1.csv",index_col=0)
# 数据量(45988, 107)
X = data.iloc[:,:-1]
y = data.iloc[:,-1] #标签是游戏氪金玩家的氪金数额"""-----------------------------n_estimators对于模型的影响----------------------"""
for i in range(50,550,50): #[50,100,150,....500]start = time() #开始时间的记录model = GBR(n_estimators=i,random_state=1412)model.fit(X,y)print("estimators:{},\tR2:{:.3f}%,\tTime:{:.3f}".format(i,model.score(X,y)*100,time()-start))# 这里是将所有数据训练预测,知识简单说明n_estimators参数的作用
"""
estimators:50,  R2:75.448%, Time:26.515
estimators:100, R2:80.529%, Time:52.332
estimators:150, R2:83.924%, Time:82.340
estimators:200, R2:86.109%, Time:105.157
estimators:250, R2:87.777%, Time:136.229
estimators:300, R2:88.954%, Time:156.518
estimators:350, R2:89.854%, Time:190.958
estimators:400, R2:90.730%, Time:213.192
estimators:450, R2:91.366%, Time:240.337
estimators:500, R2:91.999%, Time:255.028
""""""-----------------------------learning_rate对于模型的影响----------------------"""
for i in np.linspace(0.01,0.2,10):start = time()model = GBR(n_estimators=300,learning_rate=i,random_state=1412)model.fit(X,y)print("learningrate:{:.3f},\tR2:{:.3f}%,\tTime:{:.3f}".format(i,model.score(X,y)*100,time()-start))# 这里是将所有数据训练预测,知识简单说明learning_rate参数的作用"""
learningrate:0.010, R2:71.510%, Time:106.836
learningrate:0.031, R2:79.401%, Time:108.110
learningrate:0.052, R2:83.885%, Time:109.747
learningrate:0.073, R2:86.905%, Time:109.161
learningrate:0.094, R2:88.539%, Time:109.393
learningrate:0.116, R2:89.765%, Time:108.999
learningrate:0.137, R2:90.916%, Time:108.193
learningrate:0.158, R2:92.008%, Time:108.893
learningrate:0.179, R2:92.351%, Time:109.254
learningrate:0.200, R2:92.808%, Time:108.346
"""

2.5 warm_start

2.5.1 增量学习

warm_startwarm\_startwarm_start表示是否开启增量学习,增量学习就是允许算法不断接入新的数据来拓展当前模型,将巨量数据分成若干个子集进行逐步训练

2.5.2 确定巨量csv文件中样本条数

在csv文件巨大时,我们通常是无法通过excle文件打开的,所以如何确定数据样本量以逐步进行增量学习成为了难事,所以我们要通过某种手段对于csv文件样本量(行数)进行估计。

import pandas as pd
# 先确定一个上限,这里确定的是 10 ** 7,然后每次跳过十万行样本,直到报错,
# 输出最后一次 i 值说明上限比 i 小,若最后一次输出 950000 说明数据在900000~950000之间
# 若到结束都没报错,说明上限 10 ** 7 小了
for i in range(0, 10 ** 7, 50000):df = pd.read_csv(filepath_or_buffer="文件路径", skiprows=i, nrows=1)print(i)

2.5.3 增量学习过程

import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor as GBRgbr = GBR(warm_start=True)
# max_rows是上面大致估计出来的范围
for start_row in np.array(0, max_rows + 1, 50000):if start_row == 0:header = "infer"  # 第一次保存列名add_tree_num = 0  # 第一次训练让它自己生成树模型个数else:header = Noneadd_tree_num = 10  # 每次+10棵树train_sub_sample = pd.read_csv(filepath_or_buffer="文件路径", header=header, skiprows=start_row, nrows=50000)X = train_sub_sample.iloc[:, :-1]  # 训练数据Xy = train_sub_sample.iloc[:, -1]  # 训练数据ygbr.n_estimators += add_tree_num  # 加GBDT中树个数gbr = gbr.fit(X, y)  # 训练if (X.shape[0] < 50000):  # 退出结束条件break

2.6 GBDT回归树实现分类的方式

需要使用softmax或者sigmoid进行处理

对于回归树模型有:
H(xi)=∑t=1Tϕtft(xi)H\left(x_{i}\right)=\sum_{t=1}^{T} \phi_{t} f_{t}\left(x_{i}\right)H(xi​)=t=1∑T​ϕt​ft​(xi​)

2.6.1 二分类模型

通过嵌套sigmoidsigmoidsigmoid函数σ\sigmaσ进行转换

p(y^i=1∣xi)=σ(H(xi))p\left(\hat{y}_{i}=1 \mid x_{i}\right)=\sigma\left(H\left(x_{i}\right)\right)p(y^​i​=1∣xi​)=σ(H(xi​))
当p(y^i=1∣xi)大于0.5时,样本xi的预测类别为1,反之则为0当 p\left(\hat{y}_{i}=1 \mid x_{i}\right) 大于 0.5 时,样本 x_{i} 的预测类别为 1 ,反之则为 0当p(y^​i​=1∣xi​)大于0.5时,样本xi​的预测类别为1,反之则为0

2.6.2 多分类问题

嵌套softmaxsoftmaxsoftmax函数σ\sigmaσ进行转换:

具体来说,当现在的问题是 KKK 分类、且每个类别为 [1,2,3…k][1,2,3 \ldots k][1,2,3…k] 时,我们则分别按照 y=1,y=2,…,y=ky=1, y=2, \ldots, y=ky=1,y=2,…,y=k 进行建樭 总共建立 KKK 棵树,每棵树输出的结果为:

H1(xi),H2(xi),…,Hk(xi)H^{1}\left(x_{i}\right), H^{2}\left(x_{i}\right), \ldots, H^{k}\left(x_{i}\right)H1(xi​),H2(xi​),…,Hk(xi​)

总共 KKK 个输出结果。然后,我们分别将 H1(xi)H^{1}\left(x_{i}\right)H1(xi​) 到 Hk(xi)H^{k}\left(x_{i}\right)Hk(xi​) 的结果输入softmaxsoftmaxsoftmax,来计算出每个标签类别所对应的概率。具体地来说,softmaxsoftmaxsoftmax 函数的表达式为:

Softmax⁡(Hk(x))=eHk(x)∑k=1KeHk(x)\operatorname{Softmax}\left(H^{k}(x)\right)=\frac{e^{H^{k}(x)}}{\sum_{k=1}^{K} e^{H_{k}(x)}}Softmax(Hk(x))=∑k=1K​eHk​(x)eHk(x)​

其中 eee 为自然常数, HHH 是集成算法的输出结果, KKK 表示标笠中的类别总数为 KKK ,如三分类时 K=3K=3K=3 ,四分类 时 K=4K=4K=4 , kkk 表示任意标签类别, HkH_{k}Hk​ 则表示以类别 kkk 为真实标签进行训练而得出的 HHH 。不难发现,SoftmaxSoftmaxSoftmax 函数的分子时多分类状况下某一个标签类别的 H(x)\mathrm{H}(\mathrm{x})H(x) 的指数函数,而分母时多分类状况下所有标签类别的 H(x)\mathrm{H}(\mathrm{x})H(x) 的指数函数之和,因此SoftmaxSoftmaxSoftmax函数的结果代表了样本的预测标签为类别 kkk 的概率。假设现在是三分类 [1,2,3][1,2,3][1,2,3] ,则样本 iii 被分类为 1 类的概率为:

p1(xi)=eH1(x)∑k=1KeHk(x)=eH1(x)eH1(x)+eH2(x)+eH3(x)\begin{aligned} p^{1}\left(x_{i}\right) &=\frac{e^{H^{1}(x)}}{\sum_{k=1}^{K} e^{H_{k}(x)}} \\ &=\frac{e^{H^{1}(x)}}{e^{H^{1}(x)}+e^{H^{2}(x)}+e^{H^{3}(x)}} \end{aligned}p1(xi​)​=∑k=1K​eHk​(x)eH1(x)​=eH1(x)+eH2(x)+eH3(x)eH1(x)​​

最终得到 KKK 个相对概率 pk(xi)p^{k}\left(x_{i}\right)pk(xi​) ,并求解出相对概率最高的类别。不难发现,当执行多分类时,这一计算流程 中涉及到的计算量以及弱评估器数量都会远远超出二分类以及回归类问题。实际上,在执行多分类任务时, 如果我们要求模型迭代10次,模型则会按照实际的多分类标笠数n_classesn\_classesn_classes建立 10∗n_classes10 * n\_classes10∗n_classes个弱评估器。对 于这一现象,我们可以通过属性 n_estimators_n\_estimators\_n_estimators_以及属性 estimators_estimators\_estimators_查看到。

2.7 提前停止机制

我们根据以下原则来帮助梯度提升树实现提前停止:

  • 当GBDT已经达到了足够好的效果(非常接近收敛状态),持续迭代下去不会有助于提升算法表现
  • GBDT还没有达到足够好的效果(没有接近收敛),但迭代过程中呈现出越迭代算法表现越糟糕的情况
  • 虽然GBDT还没有达到足够好的效果,但是训练时间太长/速度太慢,我们需要重新调整训练

左图机器学习常见,右图深度学习常见,一旦深度学习出现右图过拟合就耶熊。

在实际数据训练时,我们往往不能动用真正的测试集进行提前停止的验证,因此我们需要从训练集中划分出一小部分数据,专用于验证是否应该提前停止。那我们如何找到这个验证集损失不再下降、准确率不再上升的“某一时间点”呢?此时,我们可以规定一个阈值,例如,当连续n_iter_no_change次迭代中,验证集上损失函数的减小值都低于阈值tol,或者验证集的分数提升值都低于阈值tol的时候,我们就令迭代停止。此时,即便我们规定的n_estimators或者max_iter中的数量还没有被用完,我们也可以认为算法已经非常接近“收敛”而将训练停下。
在GBDT当中,这个流程由以下三个参数控制:

类型 参数
提前停止 validation_fraction:从训练集中提取出、用于提前停止的验证数据占比 [0,1]

n_iter_no_change:当验证集上的损失函数值连续n_iter_no_change次没有下降
或下降量不达阈值时,则触发提前停止

tol:损失函数下降量的最小阈值,默认值为1e-4,
也可调整为其他浮点数来观察提前停止的情况。

需要注意的是,当提前停止条件被触发后,梯度提升树会停止训练,即停止建树。因此,当提前停止功能被设置打开时,我们使用属性n_estimators_调出的结果很可能不足我们设置的n_estimators,属性estimators_中的树数量也可能变得更少

2.8 梯度提升树的袋外数据

类型 参数
弱评估器的训练数据 参数:

 subsample:每次建树之前,从全数据集中进行有放回随机抽样的比例

 max_features:每次建树之前,从全特征中随机抽样特征进行分枝的比例

 random_state:随机数种子,控制整体随机模式

属性:

oob_improvement:每次建树之后相对于上一次袋外分数的增减

train_score_:每次建树之后相对于上一次验证时袋内分数的增减

梯度提升树在每次建树之前,允许模型对于数据和特征进行随机有放回抽样,构建与原始数据集相同数据量的自助集。在梯度提升树的原理当中,当每次建树之前进行随机抽样时,这种梯度提升树叫做随机提升树(Stochastic Gradient Boosting)。相比起传统的梯度提升树,随机提升树输出的结果往往方差更低,但偏差略高。如果我们发现GBDT的结果高度不稳定,则可以尝试使用随机提升树。

在GBDT当中,对数据的随机有放回抽样比例由参数subsample确定,当该参数被设置为1时,则不进行抽样,直接使用全部数据集进行训练。当该参数被设置为(0,1)之间的数字时,则使用随机提升树,在每轮建树之前对样本进行抽样。对特征的有放回抽样比例由参数max_features确定,随机模式则由参数random_state确定,这两个参数在GBDT当中的使用规则都与随机森林中完全一致。

需要注意的是,如果subsample<1,即存在有放回随机抽样时,当数据量足够大、抽样次数足够多时,大约会有37%的数据被遗漏在“袋外”(out of bag)没有参与训练。在随机森林课程当中,我们详细地证明了37%的由来,并且使用这37%的袋外数据作为验证数据,对随机森林的结果进行验证。在GBDT当中,当有放回随机抽样发生时,自然也存在部分袋外数据没有参与训练。这部分数据在GBDT中被用于对每一个弱评估器的建立结果进行验证。

具体地来说,每建立一棵树,GBDT就会使用当前树的袋外数据对建立新树后的模型进行验证,以此来对比新建弱评估器后模型整体的水平是否提高,并保留提升或下降的结果。这个过程相当于在GBDT迭代时,不断检验损失函数的值并捕捉其变化的趋势。在GBDT当中,这些袋外分数的变化值被储存在属性oob_improvement_中,同时,GBDT还会在每棵树的训练数据上保留袋内分数(in-bag)的变化,且储存在属性train_score_当中。也就是说,即便在不做交叉验证的情况下,我们也可以简单地通过属性oob_improvement与属性train_score_来观察GBDT迭代的结果。

reg训练好的模型,subsample不为1
reg.oob_improvement_ #袋外数据上的损失函数变化量
reg.train_score_ #训练集上的损失函数变化量
# 数量和reg中的弱评估器数量相同

2.9 接口(属性)

2.9.1 所有接口

n_estimators_ : int通过提前停止选择的估计器数量(如果n_iter_no_change被指定)。否则设置为n_estimatorsfeature_importances_ : ndarray of shape (n_features,)基于杂质的特征重要性。越高,特征越重要。特征的重要性计算为(标准化)完全降低了该功能带来的标准。也是被称为基尼重要性(基于基尼系数)。oob_improvement_ : ndarray of shape (n_estimators,)袋外样本损失(=偏差)相对于上一次迭代的改善。举个栗子``oob_improvement_[0]`` 就是在在"init"估计器的第一阶段的损失。仅在 ``subsample < 1.0`` 时可用train_score_ : ndarray of shape (n_estimators,)第 i 个分数"train_score_[i]"是在袋内样本上迭代i次后的的模型。如果 ``subsample == 1`` 这是训练数据的偏差loss_ : LossFunction所使用的损失函数init_ : estimator返回 ``init``评估器 estimators_ : ndarray of DecisionTreeRegressor of shape (n_estimators, ``loss_.K``)回归\二分类问题时返回形状为 (n_estimators,1)的模型,因为只需要创建一组树模型多分类问题时返回形状(n_estimators,类别总数)的模型,因为每个类别都需要一组树模型返回的每个树模型一般是决策树,因为之前默认的基学习器是CART决策树classes_ : ndarray of shape (n_classes,)类别标签(形状大小为:类别总数K)n_classes_ : int类别总数Kn_features_ : int数据特征的数量。n_features_in_ : int在 fit 期间使用的的特征数量feature_names_in_ : ndarray of shape (`n_features_in_`,)在 fit 期间使用的的特征的名称,仅仅在定义了features_name才能使用max_features_ : int最大贡献度的特征

2.9.2 接口estimators_体现出来的问题

因为在多分类问题时,每种类别都要生成一组树,
一共 n_estimators_∗n_classes_n\_estimators\_ *n\_classes\_n_estimators_∗n_classes_棵树 造成了计算量较大 ,使用时计算量甚至超过了随机森林。

2.10 和其他集成算法对比的效果

import sklearn
import matplotlib as mlp
import numpy as np
import pandas as pd
from sklearn.datasets import load_boston
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import KFold, cross_validate, train_test_split
from sklearn.ensemble import AdaBoostRegressor as ABR
import time
import matplotlib.pyplot as plt
from sklearn.model_selection import GridSearchCVX = load_boston().data
y = load_boston().target
modelnames = ["GBDT", "AdaBoost", "RF-TPE"]
models = [GBR(random_state=100),ABR(random_state=100),RFR(max_features='sqrt', n_estimators=68, max_depth=10,criterion='squared_error', n_jobs=-1, random_state=15234,),
]
colors = ["green", "gray", "red"]
axis = range(1, 6)
cv = KFold(n_splits=5, shuffle=True, random_state=100)
plt.figure(figsize=(8, 6), dpi=80)
for name, model, color in zip(modelnames, models, colors):result = cross_validate(model, X, y,cv=cv,scoring="neg_root_mean_squared_error",return_train_score=True,verbose=False, n_jobs=-1)plt.plot(axis, abs(result["train_score"]),color=color, linestyle="-", label=name+"_Train")plt.plot(axis, abs(result["test_score"]),color=color, linestyle="--", label=name+"_Test")
plt.xticks([1, 2, 3, 4, 5])
plt.xlabel("CV_n", fontsize=16)
plt.ylabel("RMSE", fontsize=16)
plt.legend()
plt.show()

横轴为第几次交叉验证,Adaboost拟合程度还是比较轻,拟合程度不好,未调参的GBDT还是有很大的提升空间。

2.11 贝叶斯优化

2.11.1 参数影响力

GBDT参数影响力:

影响力 参数
⭐⭐⭐⭐⭐
几乎总是具有巨大影响力
n_estimators(整体学习能力)
learning_rate(整体学习速率)
max_features(随机性)
⭐⭐⭐⭐
大部分时候具有影响力
init(初始化)
subsamples(随机性)
loss(整体学习能力)
⭐⭐
可能有大影响力
大部分时候影响力不明显
max_depth(粗剪枝)
min_samples_split(精剪枝)
min_impurity_decrease(精剪枝)
max_leaf_nodes(精剪枝)
criterion(分枝敏感度)

当数据量足够大时,几乎无影响
random_state
ccp_alpha(结构风险)

max_depthmax\_depthmax_depth:本来默认就是3,本来接不深,因为他是一个boosting,多个CART树集成形成,所以通过减小该参数调整不能很好地抗过拟合,和随机森林bagging有很大区别。若boosting学习能力不好,增大该参数可以增加学习能力,所以这个参数经常在学习能力不足时调大以增强学习能力。

boosting天生也擅长拟合小样本高纬度数据,若在小样本中过拟合了,说明数据维度不高,尽力不用boosting了。

2.11.2 参数空间范围

参数 范围
loss 回归损失中4种可选损失函数
[“squared_error”,“absolute_error”, “huber”, “quantile”]
criterion 全部可选的4种不纯度评估指标
[“friedman_mse”, “squared_error”, “mse”, “mae”]
init HyperOpt不支持搜索,手动调参
n_estimators 需要先建树确认若树分类器X个,
最后范围定为可以为(X-30,X+100,10)
learning_rate 以1.0为中心向两边延展,最后范围定为(0.05,2.05,0.05)
*如果算力有限,也可定为(0.1,2.1,0.1)
max_features 所有字符串,外加sqrt与auto中间的值
subsample subsample参数的取值范围为(0,1],因此定范围(0.1,0.8,0.1)
*如果算力有限,也可定为(0.5,0.8,0.1)
max_depth 以3为中心向两边延展,右侧范围定得更大。最后确认(2,30,2)
min_impurity_decrease 只能放大、不能缩小的参数,先尝试(0,5,1)范围

2.11.3 贝叶斯优化模型

#日常使用库与算法
import pandas as pd
import numpy as np
import sklearn
import matplotlib as mlp
import matplotlib.pyplot as plt
import time
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.model_selection import cross_validate, KFold#导入优化算法
import hyperopt
from hyperopt import hp, fmin, tpe, Trials, partial
from hyperopt.early_stop import no_progress_lossdata = pd.read_csv(r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\House Price\train_encode.csv",index_col=0)
X = data.iloc[:,:-1]
y = data.iloc[:,-1]
# rf就是随机森林模型,带入GBDT的init初始化模型中。
rf = RFR(n_estimators=89, max_depth=22, max_features=14,min_impurity_decrease=0,random_state=1412, verbose=False, n_jobs=-1)# 定义参数目标优化函数
def hyperopt_objective(params):# 初始化模型reg = GBR(n_estimators = int(params["n_estimators"]),learning_rate = params["lr"],criterion = params["criterion"],loss = params["loss"],max_depth = int(params["max_depth"]),max_features = params["max_features"],subsample = params["subsample"],min_impurity_decrease = params["min_impurity_decrease"],init = rf,random_state=1412,verbose=False)# 定义交叉验证cv = KFold(n_splits=5,shuffle=True,random_state=1412)# GBDT交叉验证validation_loss = cross_validate(reg,X,y,scoring="neg_root_mean_squared_error",cv=cv,verbose=False,n_jobs=-1,error_score='raise')# 优化目标值return np.mean(abs(validation_loss["test_score"]))def param_hyperopt(max_evals=100):#保存迭代过程trials = Trials()#设置提前停止early_stop_fn = no_progress_loss(10)#定义代理模型params_best = fmin(hyperopt_objective# 优化目标函数, space = param_grid_simple# 参数空间, algo = tpe.suggest# 使用建议的优化算法, max_evals = max_evals# 计算多少个高斯优化点, verbose=True# 显示过程, trials = trials# 保存迭代过程, early_stop_fn = early_stop_fn # 设置提前停止)#打印最优参数,fmin会自动打印最佳分数print("\n","\n","best params: ", params_best,"\n")return params_best, trialsparam_grid_simple = {'n_estimators': hp.quniform("n_estimators",25,200,25),"lr": hp.quniform("learning_rate",0.05,2.05,0.05),"criterion": hp.choice("criterion",["friedman_mse", "squared_error", "mse", "mae"]),"loss":hp.choice("loss",["squared_error","absolute_error", "huber", "quantile"]),"max_depth": hp.quniform("max_depth",2,30,2),"subsample": hp.quniform("subsample",0.1,0.8,0.1),"max_features": hp.choice("max_features",["log2","sqrt",16,32,64,"auto"]),"min_impurity_decrease":hp.quniform("min_impurity_decrease",0,5,1)}# 运行代码得出最优值
params_best, trials = param_hyperopt(30) #使用小于0.1%的空间进行训练

2.11.4 进一步调参建议

由于高斯最优化模型具有随机性,所以要运行多次(M次)代码:
params_best, trials = param_hyperopt(30)
得出M个最优参数组合,根据这几个最优化模型参数组合保留参数相同的一些参数像:criterion,loss,但是如果尽管max_depth参数可能相同,但是不能确定,因为后续进一步调参具有随机性。
然后根据这M次最优化组合,取得近边缘值则调整参数范围、取得中值减小参数范围增加参数密度,进一步调参,运行param_hyperopt代码.

2.12 手动调参代码

2.12.1 boosting算法调参思想

这时一些调参建议,适用于继承算法boosting的绝大部分算法

2.12.2 调参过程代码

import sklearn
import matplotlib as mlp
import numpy as np
import pandas as pd
from sklearn.datasets import load_boston
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import KFold, cross_validate, train_test_split
from sklearn.ensemble import AdaBoostRegressor as ABR
import time
import matplotlib.pyplot as plt
from sklearn.model_selection import GridSearchCVX = load_boston().data
y = load_boston().target
cv = KFold(n_splits=5, shuffle=True, random_state=100)def RMSE(result, name):return abs(result[name].mean())rfr1_train_list = []
rfr1_test_list = []
rfr2_train_list = []
rfr2_test_list = []
# n_estimators learning_rate min_impurity_decrease max_features subsample min_samples_split
for n in range(20, 170, 10):model1 = GBR(random_state=100, learning_rate=0.075,n_estimators=n, max_features=7)res1 = cross_validate(model1, X, y, cv=cv, scoring="neg_root_mean_squared_error",return_train_score=True, verbose=False, n_jobs=-1)rfr1_train_list.append(RMSE(res1, "train_score"))rfr1_test_list.append(RMSE(res1, "test_score"))model2 = GBR(random_state=100, learning_rate=0.075,n_estimators=n, max_features=7)res2 = cross_validate(model2, X, y, cv=cv, scoring="neg_root_mean_squared_error",return_train_score=True, verbose=False, n_jobs=-1)rfr2_train_list.append(RMSE(res2, "train_score"))rfr2_test_list.append(RMSE(res2, "test_score"))fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111)
ax.grid()
ax.plot(range(20, 170, 10), rfr1_train_list, "r-", range(20, 170, 10),rfr1_test_list, "r--", label="first_model")ax.plot(range(20, 170, 10), rfr2_train_list, "g-",range(20, 170, 10), rfr2_test_list, "g--", label="this_model")
ax.legend()
plt.show()

两个模型参数一样,后面覆盖了前面的,主要是第一个保存最优的,然后第二个调参用,实时更新第一个模型参数。

2.13 GBDT的数学原理

假设现有数据集NNN,含有形如(xi,yi)(x_i,y_i)(xi​,yi​)的样本MMM个,iii为任意样本的编号,单一样本的损失函数为l(yi,H(xi))l(y_i,H(x_i))l(yi​,H(xi​)),其中H(xi)H(x_i)H(xi​)是iii号样本在集成算法上的预测结果,整个算法的损失函数为L(y,H(x))L(y,H(x))L(y,H(x)),且总损失等于全部样本的损失之和:L(y,H(x))=∑il(yi,H(xi))L(y,H(x)) = \sum_il(y_i,H(x_i))L(y,H(x))=∑i​l(yi​,H(xi​))。同时,弱评估器为回归树fff,总共学习TTT轮。则GBDT回归的基本流程如下所示:

  1. 初始化数据迭代的起点H0(x)H_0(x)H0​(x)。sklearn当中,我们可以使用0、随机数或者任意算法的输出结果作为H0(x)H_0(x)H0​(x),但在最初的论文中,Friedman定义了如下公式来计算H0H_0H0​:
    H0(x)=argminC∑i=1Ml(yi,C)=argminCL(y,C)\begin{aligned} H_0(x) &= \mathop{argmin}_{C} \sum_{i=1}^M l(y_i,C)\\ \\ &= \mathop{argmin}_{C} L(y,C) \end{aligned} H0​(x)​=argminC​i=1∑M​l(yi​,C)=argminC​L(y,C)​
    其中yiy_iyi​为真实标签,CCC为任意常数。以上式子表示,找出令∑i=1Ml(yi,C)\sum_{i=1}^Ml(y_i,C)∑i=1M​l(yi​,C)最小的常数CCC值,并输出最小的∑i=1Ml(yi,C)\sum_{i=1}^Ml(y_i,C)∑i=1M​l(yi​,C)作为H0(x)H_0(x)H0​(x)的值。需要注意的是,由于H0(x)H_0(x)H0​(x)是由全部样本的lll计算出来的,因此所有样本的初始值都是H0(x)H_0(x)H0​(x),不存在针对某一样本的单一初始值。

    开始循环,for t in 1,2,3…T:

  2. 在现有数据集NNN中,抽样MMM * subsample个样本,构成训练集NtN^tNt

  3. 对任意一个样本iii,计算伪残差(pseudo-residuals)ritr_{it}rit​,具体公式为:

    rit=−∂l(yi,Ht−1(xi))∂Ht−1(xi)r_{it} = -\frac{\partial{l(y_i,H_{t-1}(x_i))}}{\partial{H_{t-1}(x_i)}}rit​=−∂Ht−1​(xi​)∂l(yi​,Ht−1​(xi​))​
    不难发现,伪残差是一个样本的损失函数对该样本在集成算法上的预测值求导后取负的结果,并且在进行第t次迭代、计算第t个伪残差时,我们使用的前t-1次迭代后输出的集成算法结果。在t=1时,所有伪残差计算中的Ht−1(xi)H_{t-1}(x_i)Ht−1​(xi​)都等于初始H0(x)H_0(x)H0​(x),在t>0时,每个样本上的Ht−1(xi)H_{t-1}(x_i)Ht−1​(xi​)都是不同的取值。

  4. 求解出伪残差后,在数据集(xi,rit)(x_i, r_{it})(xi​,rit​)上按照CART树规则建立一棵回归树ftf_tft​,训练时拟合的标签为样本的伪残差ritr_{it}rit​。

  5. 将数据集NtN_tNt​上所有的样本输入ftf_tft​进行预测,对每一个样本,得出预测结果ft(xi)f_t(x_i)ft​(xi​)。在数学上我们可以证明,只要拟合对象是伪残差ritr_{it}rit​,则ft(xi)f_t(x_i)ft​(xi​)的值一定能让损失函数最快减小

  6. 根据预测结果ft(xi)f_t(x_i)ft​(xi​)迭代模型,具体来说:
    Ht(xi)=Ht−1(xi)+ft(xi)H_t(x_i) = H_{t-1}(x_i) + f_t(x_i)Ht​(xi​)=Ht−1​(xi​)+ft​(xi​)
    假设输入的步长为η\etaη,则Ht(x)H_t(x)Ht​(x)应该为:
    Ht(xi)=Ht−1(xi)+ηft(xi)H_t(x_i) = H_{t-1}(x_i) + \eta f_t(x_i)Ht​(xi​)=Ht−1​(xi​)+ηft​(xi​)
    对整个算法则有:
    Ht(x)=Ht−1(x)+ηft(x)H_t(x) = H_{t-1}(x) + \eta f_t(x)Ht​(x)=Ht−1​(x)+ηft​(x)

  1. 循环结束,输出HT(x)H_T(x)HT​(x)的值作为集成模型的输出值。
  • H0H_0H0​是如何确定的?

在最初的论文中,Friedman定义了如下公式来计算H0H_0H0​:
H0(x)=argminC∑i=1Ml(yi,C)=argminCL(yi,C)\begin{aligned} H_0(x) &= \mathop{argmin}_{C} \sum_{i=1}^M l(y_i,C)\\ \\ &= \mathop{argmin}_{C} L(y_i,C) \end{aligned} H0​(x)​=argminC​i=1∑M​l(yi​,C)=argminC​L(yi​,C)​
其中yiy_iyi​为真实标签,CCC为任意常数,实际上在回归中就是所有样本均值。

  • 伪残差与残差、梯度有什么关系?

在讲解GBDT与AdaBoost之间的差异时,我们曾提到,AdaBoost是拟合原始数据XXX与真实标签yiy_iyi​,而GBDT拟合的是原始数据XXX与残差(yi−H(xi))(y_i - H(x_i))(yi​−H(xi​)),但在上述数学流程中,我们拟合的对象伪残差既不像真实标签,也不像H(x)H(x)H(x)与yiy_iyi​的差异,它到底是什么呢?

rit=−∂l(yi,Ht−1(xi))∂Ht−1(xi)r_{it} = -\frac{\partial{l(y_i,H_{t-1}(x_i))}}{\partial{H_{t-1}(x_i)}}rit​=−∂Ht−1​(xi​)∂l(yi​,Ht−1​(xi​))​

从数学上来看,伪残差是一个样本的损失函数对该样本在集成算法上的预测值求导后取负的结果。假设现在损失函数是平方误差SquarederrorSquared errorSquarederror,则该求导过程很明显就是:

l=(yi−Ht−1(xi))2∂l∂Ht−1(xi)=∂∂Ht−1(xi)(yi−Ht−1(xi))2∂l∂Ht−1(xi)=−2(yi−Ht−1(xi))∂l∂Ht−1(xi)=2(yi−Ht−1(xi))\begin{align} l&= (y_i - H_{t-1}(x_i))^2 \\ \\ \frac{\partial l}{\partial H_{t-1}(x_i)} &= \frac{\partial}{\partial H_{t-1}(x_i)} (y_i - H_{t-1}(x_i))^2\\ \\ \frac{\partial l}{\partial H_{t-1}(x_i)} &= -2(y_i - H_{t-1}(x_i))\\ \\ \frac{\partial l}{\partial H_{t-1}(x_i)} &= 2(y_i- H_{t-1}(x_i))\\ \end{align}l∂Ht−1​(xi​)∂l​∂Ht−1​(xi​)∂l​∂Ht−1​(xi​)∂l​​=(yi​−Ht−1​(xi​))2=∂Ht−1​(xi​)∂​(yi​−Ht−1​(xi​))2=−2(yi​−Ht−1​(xi​))=2(yi​−Ht−1​(xi​))​​

不难发现,虽然伪残差看着与残差完全不相关,但其本质与残差非常相似。它是残差的某种变形,它的值不完全等同于残差的值,但是它衡量的差异与残差衡量的差异完全一致。因此,我可以让新建立的弱评估器拟合伪残差,这样算法就会更多地学习当下Ht(xi)H_t(x_i)Ht​(xi​)与yiy_iyi​之间的差异,新建立的弱评估器预测出的结果也更有可能抹平这种差异。从直觉上来说,Ht(xi)H_t(x_i)Ht​(xi​)与yiy_iyi​之间的差异越小,整体损失函数值就会越小,因此GBDT拟合伪残差是在向着损失函数最小化(偏差最小化)的方向拟合

除此之外,伪残差是损失函数求导后取负的结果。一个函数对自变量求导后得到的结果称为梯度,代表字母为ggg,因此伪残差也被称为负梯度,也因此,GBDT被称为“拟合负梯度”的算法。这一说法拓展开来,我们可以说GBDT拟合负梯度、拟合伪残差、拟合损失函数在预测标签上的负导数。无论这些说法如何变化,其实指的都是同一个数学过程。不过,在最初的梯度提升机器(Gradient Boosting Machine)中,拟合的的确是残差y−H(x)y-H(x)y−H(x),只不过在后来改进的梯度提升树中,拟合残差过程被修改为拟合伪残差了。

需要注意的是,由于伪残差/负梯度都是针对单一样本计算的,所以一般在数学公式当中,梯度会被表示为gig_igi​,其中iii为样本量。对GBDT来说则有:

ri=−gir_i = -g_iri​=−gi​

  • 证明:拟合伪残差可以令损失函数最快地减小

我们可以对损失函数进行泰勒展开。对单一样本而言,我们有损失函数l(yi,Ht−1(xi)+ft(xi))l(y_i, H_{t-1}(x_i) + f_t(x_i))l(yi​,Ht−1​(xi​)+ft​(xi​)),其中yiy_iyi​是已知的常数,因此损失函数可以被看做是只有Ht−1(xi)+ft(xi)H_{t-1}(x_i) + f_t(x_i)Ht−1​(xi​)+ft​(xi​)一个自变量的函数,从而简写为l(Ht−1(xi)+ft(xi))l(H_{t-1}(x_i) + f_t(x_i))l(Ht−1​(xi​)+ft​(xi​))。

泰勒展开:

l(Ht−1(xi)+ft(xi))≈l(Ht−1(xi))+∂l(Ht−1(xi))∂Ht−1(xi)∗ft(xi)\begin{aligned} l(H_{t-1}(x_i) + f_t(x_i) ) &\approx l(H_{t-1}(x_i)) + \frac{\partial{l(H_{t-1}(x_i))}}{\partial{H_{t-1}(x_i)}} * f_t(x_i) \\ \end{aligned} l(Ht−1​(xi​)+ft​(xi​))​≈l(Ht−1​(xi​))+∂Ht−1​(xi​)∂l(Ht−1​(xi​))​∗ft​(xi​)​
l(Ht−1(xi)+ft(xi))≈常数+gift(xi)\begin{aligned} l(H_{t-1}(x_i) + f_t(x_i) ) &\approx 常数 + g_if_t(x_i) \\ \end{aligned} l(Ht−1​(xi​)+ft​(xi​))​≈常数+gi​ft​(xi​)​

现在,如果要令lll最小,ft(xi)f_t(x_i)ft​(xi​)应该等于多少呢?回到我们最初的目标,找出令损失函数lll最小的ftf_tft​值:

ft=argminf∑i=1Ml(Ht−1(xi)+ft(xi))≈argminf∑i=1M(常数+gift(xi))\begin{aligned} f_t &= \mathop{argmin}_{f} \sum_{i=1}^{M}l(H_{t-1}(x_i) + f_t(x_i)) \\ & \approx \mathop{argmin}_{f} \sum_{i=1}^{M} \left( 常数 + g_if_t(x_i) \right) \end{aligned} ft​​=argminf​i=1∑M​l(Ht−1​(xi​)+ft​(xi​))≈argminf​i=1∑M​(常数+gi​ft​(xi​))​

常数无法被最小化,因此继续化简:
ft≈argminf∑i=1Mgift(xi)≈argminf⟨gtft(x)⟩\begin{aligned} f_t &\approx \mathop{argmin}_{f} \sum_{i=1}^{M} g_if_t(x_i) \\ \\ &\approx \mathop{argmin}_{f} \langle g_t f_t(x) \rangle \end{aligned} ft​​≈argminf​i=1∑M​gi​ft​(xi​)≈argminf​⟨gt​ft​(x)⟩​

现在,gtg_tgt​是包含了所有样本梯度的向量,ft(x)f_t(x)ft​(x)是包含了所有样本在ftf_tft​上预测值的向量,两个向量对应位置元素相乘后求和,即表示为向量的内积,由尖括号⟨⟩\langle \rangle⟨⟩表示。现在我们希望求解向量内积的最小值、并找出令向量内积最小的ft(x)f_t(x)ft​(x)的取值,那就必须先找出ft(x)f_t(x)ft​(x)的方向,再找出ft(x)f_t(x)ft​(x)的大小。

向量的内积⟨gtft(x)⟩=∣gt∣∣ft(x)∣cos(α)\langle g_t f_t(x) \rangle = |g_t||f_t(x)|cos(\alpha)⟨gt​ft​(x)⟩=∣gt​∣∣ft​(x)∣cos(α)同向最大,反向最小。

故:ft(x)和gt(损失函数一阶导数,就是伪残差)反向,就是−gtf_t(x)和g_t(损失函数一阶导数,就是伪残差)反向,就是-g_tft​(x)和gt​(损失函数一阶导数,就是伪残差)反向,就是−gt​。

不难发现,⟨gtft(x)⟩\langle g_t f_t(x) \rangle⟨gt​ft​(x)⟩是一个理论上可以取到无穷小的趋于零的值,那我们的ft(x)f_t(x)ft​(x)应该取什么大小呢?答案非常出乎意料:任何大小都无所谓

无论ft(x)f_t(x)ft​(x)的大小是多少,我们都可以通过步长η\etaη对其进行调整,只要能够影响H(x)H_(x)H(​x),我们就可以影响损失迭代过程中的常数的大小。因此在数学上来说,ft(x)f_t(x)ft​(x)的大小可以是−gt-g_t−gt​的任意倍数,这一点在梯度下降中其实也是一样的。为了方便起见,同时为了与传统梯度下降过程一致,我们通常让ft(x)f_t(x)ft​(x)就等于一倍的−gt-g_t−gt​,但也有不少论文令ft(x)f_t(x)ft​(x)等于其他值的。在GBDT当中:

ft≈argminf⟨gtft(x)⟩=−gt\begin{aligned} f_t &\approx \mathop{argmin}_{f} \langle g_t f_t(x) \rangle\\ &= -g_t \end{aligned} ft​​≈argminf​⟨gt​ft​(x)⟩=−gt​​
这里GBDT的算法数学上的推理已经完成,以下是关于GBDT的一些思考:

上述就是我们让GBDT当中的弱评估器拟合伪残差/负梯度的根本原因。拟合负梯度其实为GBDT带来了非常多的特点——

  1. 首先,通过直接拟合负梯度,GBDT避免了从损失函数找“最优”的过程,即避免了上述证明中求解ft=argminf∑i=1Ml(Ht−1(xi)+ft(xi))f_t = \mathop{argmin}_{f} \sum_{i=1}^{M}l(H_{t-1}(x_i) + f_t(x_i))ft​=argminf​∑i=1M​l(Ht−1​(xi​)+ft​(xi​))的过程,从而大大地简化了计算。

  2. 其次,通过拟合负梯度,GBDT模拟了梯度下降的过程,由于结合了传统提升法Boosting与梯度下降,因此才被命名为梯度提升法(Gradient Boosting)。这个过程后来被称为函数空间上的梯度下降(Gradient Descent in Function Space),这是视角为Boosting算法的发展奠定了重要的基础。

  3. 最后,最重要的一点是,通过让弱评估器拟合负梯度,弱评估器上的结果可以直接影响损失函数、保证损失函数的降低,从而指向Boosting算法的根本目标:降低偏差。这一过程避免了许多在其他算法中需要详细讨论的问题:例如,每个弱评估器的权重ϕ\phiϕ是多少,以及弱评估器的置信度如何。

在AdaBoost算法当中,损失函数是“间接”影响弱评估器的建立,因此有的弱评估器能够降低损失函数,而有的弱评估器不能降低损失函数,因此要在求和之前,需要先求解弱评估器的置信度,然后再给与置信度高的评估器更高的权重,权重ϕ\phiϕ存在的根本意义是为了调节单一弱评估器对H(x)H(x)H(x)的贡献程度。但在GBDT当中,由于所有的弱评估器都是能够降低损失函数的,只不过降低的程度不同,因此就不再需要置信度/贡献度的衡量,因此就不再需要权重ϕ\phiϕ。

如果去翻阅早期梯度提升机器资料,我们会发现梯度提升树最开始是有求解权重的过程的。当拟合完伪残差之后,我们不直接求解令LLL最小的ftf_tft​值,而求解令整体LLL最小的权重ϕ\phiϕ:

求解迭代过程中弱评估器ftf_tft​所需的权重ϕt\phi_tϕt​,具体公式为:
ϕt=argminϕ∑i=1ML(yi,Ht−1(xi)+ϕtft(xi))\phi_t = \mathop{argmin}_{\phi} \sum_{i=1}^M L\left(y_i,H_{t-1}(x_i) + \phi_tf_t(x_i) \right) ϕt​=argminϕ​i=1∑M​L(yi​,Ht−1​(xi​)+ϕt​ft​(xi​))
与求解H0H_0H0​的式子相似,上述式子表示,找出令∑i=1ML(yi,Ht−1(xi)+ϕtft(xi))\sum_{i=1}^M L\left(y_i,H_{t-1}(x_i) + \phi_tf_t(x_i) \right)∑i=1M​L(yi​,Ht−1​(xi​)+ϕt​ft​(xi​))最小的常数ϕt\phi_tϕt​值。同样,由于ϕt\phi_tϕt​是针对全部样本计算出来的,因此ftf_tft​上所有样本的预测值前的权重都是ϕt\phi_tϕt​,并不存在针对于某一样本的权重。

接着,再根据求解出的权重ϕt\phi_tϕt​迭代模型,具体来说:
Ht(x)=Ht−1(x)+ηϕtft(x)H_t(x) = H_{t-1}(x) + \eta \phi_t f_t(x)Ht​(x)=Ht−1​(x)+ηϕt​ft​(x)

在此基础上,Friedman甚至还提出了单独针对决策树的权重计算方法。但我们之前推导中讲解过,只要ft(x)f_t(x)ft​(x)的方向正确,ft(x)f_t(x)ft​(x)具体的取值并没有那么重要,毕竟可以通过学习率η\etaη进行调整。在有η\etaη、同时又不需要衡量弱评估器置信度的情况下,权重ϕ\phiϕ的意义就很小了。因此现在我们在实现梯度提升树时,已经不再使用带权重的版本,但带权重版本的数学过程与不带权重版本是高度类似的。

深度剖析集成学习GBDT相关推荐

  1. 集成学习—GBDT原理理解

    GBDT,全称为梯度提升决策树,即Gradient Boosting Decision Tree.上一篇博客:集成学习-GBDT(论文研读)中已经对GBDT原始论文--Jerome H. Friedm ...

  2. 集成学习—GBDT(论文研读)

    GBDT,全称为梯度提升决策树,即Gradient Boosting Decision Tree,它与Friedman等人的<Additive logistic regression: a st ...

  3. 《C语言深度剖析》学习笔记----C语言中的符号

    本节主要讲C语言中的各种符号,包括注释符.单引号双信号以及逻辑运算符等. 一.注释符 注释符号和注释在程序的预编译期就已经被解决了,在预编译期间,编译器会将注释符号和注释符号之间的部分简单的替换成为空 ...

  4. 《C语言深度剖析》学习笔记三

    第三章 预处理 注释先于预处理指令被处理,所以无法用宏定义注释 宏定义表达式时,每个参数都加括号,避免不必要的错误 宏定义时: SUM (x) 和SUM(x)不同 使用时: SUM(x) 和SUM ( ...

  5. C语言深度剖析书籍学习记录 第七章 文件结构

  6. C语言深度剖析书籍学习记录 第六章 函数

    函数的好处 1.降低复杂性:使用函数的最首要原因是为了降低程序的复杂性,可以使用函数来隐含信息,从而使你不必再考虑这些信息. 2.避免重复代码段:如果在两个不同函数中的代码很相似,这往往意味着分解工作 ...

  7. C语言深度剖析书籍学习记录 第五章 内存管理

    常见的内存错误 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存. 结构体成员指针未初始化 很多初学者犯了这个错误还不知道是怎么回事.这里定义了结构体变量 stu,但是他没 想到这 ...

  8. C语言深度剖析书籍学习记录 第四章 指针和数组

    p 称为指针变量,p 里存储的内存地址处的内存称为 p 所指向的内存. 指针变量 p 里存储的任何数据都将被当作地址来处理 一个基本的数据类型(包括结构体等自定义类型)加上"*" ...

  9. C语言深度剖析书籍学习记录 第三章 预处理

    宏 _LINE_ 表示正在编译的文件的行号 _FILE_ 表示正在编译的文件的名字 _DATE_ 表示编译时刻的日期字符串,例如: "25 Dec 2007" _TIME_ 表示编 ...

最新文章

  1. 验证和训练loss和acc多种情况分析
  2. 15.for循环的三个表达式是可以省略
  3. Win2008 R2实战之只读域控制器部署(图)有修改
  4. 关于SPECjAppServer评测,您应该知道的“故事”
  5. Web应用安全————Shiro 解决会话固定漏洞
  6. 什么是python自动化测试_python已经自动化了,大家一般用什么测试框架?
  7. Weblogic 数据源及连接池配置问题Warning! Connectivity to backend database not verified
  8. creo配置文件config选项详解_5年资深架构师重点聚焦:SpringBoot的配置详解+内嵌Servlet容器
  9. 树状数组相关应用之区间更新单点查询问题
  10. php的CodeIgniter框架中如何过滤数据(将危险数据如html类型的数据过滤后提交到数据库)...
  11. 游戏制作大致流程粗谈之五
  12. 函数式接口,方法引入
  13. Matlab聚类分析(Kmeans)
  14. 子图数正方形和长方形数量
  15. qq音乐for linux,[Linux] QQ音乐官方上线 For Linux V1.0.5-1 [2020.12.2]
  16. sd卡无法读取怎么办?内存卡数据恢复,4个好用方法
  17. 公众号内测算法推荐引争议,却有小号因此阅读增长20倍
  18. 基于vue 2.x的移动端网页弹窗插件wc-messagebox(支持Alert,Confirm,Toast,Loading)
  19. 基于Yocto的qmake编译环境默认QMAKE_SPEC不是linux-oe-g++的问题
  20. 孤尽31天-day02

热门文章

  1. 计算机软件实习项目一 简单计算器 (Qt实现计算器界面) 12-5
  2. 2019阿里巴巴Java研发实习生面经(一面+二面)
  3. oc字典放入到数组里,根据字典里的属性排序(重点)
  4. Android名片扫描识别系统SDK
  5. 舆情监控前几大公司有哪些,TOOM品牌好的舆情数据监测平台?
  6. vue里页面的缓存详解
  7. 结构体的使用和动态内存的分配及释放
  8. [附源码]计算机毕业设计JAVAjsp基于web的停车收费管理系统
  9. word使用:word中插入横线
  10. 计算机技术与虚拟现实的应用,虚拟现实技术与计算机技术应用的共同发展