1. Bagging与Boosting的联系和区别
Bagging与Boosting都是集成学习模型,它们都需要将多个不同的模型整合到一起进行预测。
Bagging思想的本质是:通过bootstrap的方式对全样本数据集做有放回抽样得到多个子集,在不同的子集上训练不同的弱分类器,最后通过投票的方式决定最终预测结果。这些弱分类器都倾向于过拟合并且Bagging会通过降低方差的方式减少预测误差。
Boosting则是采取另一种思想:使用全样本数据集训练一系列弱分类器,然后将这些弱分类器组合形成一个预测性能更好的分类器。这些弱分类器都倾向于欠拟合并且Boosting会通过降低偏差的方式减少预测误差。
2. Boosting要解决的问题
Boosting会从弱可学习算法出发,将其提升为强可学习算法。大多数的Boosting方法都是通过改变训练数据集中不同数据的概率分布来实现提升的。具体来说,Boosting在每一轮的学习中会改变数据集中不同样本的权值。并且在最后将各个弱分类器组合起来。
对于Boosting来说,有两个问题需要解决:
1. 如何改变数据的概率分布;
2. 如何将各个弱分类器组合起来。
3. Adaboost——最经典的Boosting算法
3.1 Adaboost原理
对于Adaboost而言,解决这两个问题的方法是:
1. 上一轮分类错误的样本增加权重,分类正确的样本减少权重;
2. 加权投票表决。表现好的分类器增加权重,表现差的分类器减少权重。
下面我们具体介绍Adaboost算法。假设给定一个二分类数据集
其中为特征空间中的特征向量,为数据对应的标签且取值范围为{-1, +1}。记最终的分类器为,则
(1)用均匀分布对数据权重做初始化,即权重分布为
(2)对于
(2.1)使用权重分布训练基分类器
(2.2)计算在训练集上的分类误差
(2.3)利用分类误差计算分类器的权重
(2.4)利用分类器权重更新训练数据的权重分布
(3)构建基分类器的线性组合,则最终的分类器为
根据(2.3)分类误差越小,基分类器的权重越大。根据(2.4)被正确分类的样本权重会减小,而被错误分类的样本权重会增加。
3.2 在sklearn中使用Adaboost
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use("ggplot")
%matplotlib inline
import seaborn as sns
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import accuracy_score
wine = pd.read_csv("https://archive.ics.uci.edu/ml/\
machine-learning-databases/wine/wine.data", \
header = None)
wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash',
'Alcalinity of ash',
'Magnesium', 'Total phenols', 'Flavanoids',
'Nonflavanoid phenols',
'Proanthocyanins', 'Color intensity', 'Hue',
'OD280/OD315 of diluted wines', 'Proline']
print("Class labels",np.unique(wine["Class label"]))
wine = wine[wine['Class label'] != 1]
y = wine['Class label'].values
X = wine[['Alcohol', 'OD280/OD315 of diluted wines']].values
# 将分类标签变成二进制编码:
le = LabelEncoder()
y = le.fit_transform(y)
# 按照y的类别等比例抽样
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, \
random_state = 1, \
stratify = y)
# 使用单一决策树
tree = DecisionTreeClassifier(criterion = 'entropy', random_state = 1, \
max_depth = 1)
tree = tree.fit(X_train, y_train)
y_train_pred = tree.predict(X_train)
y_test_pred = tree.predict(X_test)
tree_train = accuracy_score(y_train, y_train_pred)
tree_test = accuracy_score(y_test, y_test_pred)
print('Decision tree train/test accuracies %.3f/%.3f' \
% (tree_train, tree_test))
# 使用Adaboost
ada = AdaBoostClassifier(base_estimator = tree, n_estimators = 500, \
learning_rate = 0.1, random_state = 1)
ada = ada.fit(X_train, y_train)
y_train_pred = ada.predict(X_train)
y_test_pred = ada.predict(X_test)
ada_train = accuracy_score(y_train, y_train_pred)
ada_test = accuracy_score(y_test, y_test_pred)
print('Adaboost train/test accuracies %.3f/%.3f' % (ada_train, ada_test))
输出结果如下:
Class labels [1 2 3]
Decision tree train/test accuracies 0.916/0.875
Adaboost train/test accuracies 1.000/0.917
3.3 对实例结果做进一步的分析
我们从结果中可以发现:单层决策树似乎对训练数据欠拟合,而Adaboost模型正确地预测了训练数据的所有分类标签。同时,与单层决策树相比,Adaboost的测试性能也略有提高。我们可以通过画图的方式来解释出现这种现象的原因。
x_min = X_train[:, 0].min() - 1
x_max = X_train[:, 0].max() + 1
y_min = X_train[:, 1].min() - 1
y_max = X_train[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), \
np.arange(y_min, y_max, 0.1))
f, axarr = plt.subplots(nrows = 1, ncols = 2, sharex = 'col', \
sharey = 'row', figsize = (12, 6))
for idx, clf, tt in zip([0, 1], [tree, ada], ['Decision tree', 'Adaboost']):
clf.fit(X_train, y_train)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
axarr[idx].contourf(xx, yy, Z, alpha = 0.3)
axarr[idx].scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], \
c = 'blue', marker = '^')
axarr[idx].scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], \
c = 'red', marker = 'o')
axarr[idx].set_title(tt)
axarr[0].set_ylabel('Alcohol', fontsize = 12)
plt.tight_layout()
plt.text(0, -0.2, s = 'OD280/OD315 of diluted wines', ha = 'center', \
va = 'center',fontsize = 12, transform = axarr[1].transAxes)
plt.show()
我们可以发现Adaboost的决策边界比单层决策树的决策边界要复杂的多。换句话说,Adaboost试图用增加模型复杂度从而降低偏差的方式去减小总误差。但是这一过程引入了方差,可能出现过拟合,因此在训练集和测试集的性能上存在较大的差距。
与单个分类器相比,Adaboost等Boosting方法增加了计算的复杂度,在实践中需要仔细思考是否愿意为预测性能的提升而增加计算成本。同时,Boosting无法做到现在流行的并行计算的方式,因为每一步迭代都要基于上一部的基分类器。
3.4 由Adaboost引出的前向分步算法
前向分步算法是比Adaboost更高级的框架。这个框架不但可以解决分类问题,还可以解决回归问题。它的算法流程可以与Adaboost的做类比。
我们参考Adaboost,将最终的分类器表示为
其中,是基分类器,为基分类器参数,为基分类器权重。假设模型最终的损失函数为,则学习就等价于
这个最优化问题很难用一般的凸优化知识解决,因为这里有很多的和需要我们一次性求出。但是前向分步算法可以把这个类型的问题做分解。由于这个模型是一个加法模型,因此可以从前往后,在每一步只优化一个基分类器及其系数。具体来说,每一步只需要优化
就可以。
更具体来说,我们给定
其中为特征空间中的特征向量,为数据对应的标签且取值范围为{-1, +1}。记最终的分类器为,损失函数为,基分类器集合为,则
(1)初始化
(2)对
(2.1)极小化损失函数
得到参数和
(2.2)更新
(3)不断重复上述过程直到得到最后的模型
这样,前向分步算法就将一次求解所有和的问题分解成了逐个求解的问题。
4. 从回归问题的集成学习到梯度提升决策树(GBDT)
4.1 用决策树和Boosting来解决回归问题
首先,回归树与分类树的不同在于数据的标签是连续的而非离散的。再使用信息熵这样的指标来指导如何分裂节点是不合适的。这里比较适用的方式是平方误差,因为它本来就可以用于回归问题。然后,由于我们要将Boosting引入,参考上面的Adaboost和前向分步算法,我们需要构建一个类似“分类误差”的指标来指导数据的权重更新。因此,人们想到了使用残差来表示每次使用的基函数没能解决的那部分问题。
根据以上内容,我们可以总结如下算法:
给定
其中为特征空间中的特征向量,为数据对应的标签且取值范围为整个实数域。记最终的预测函数为,则
(1)初始化
(2)对
(2.1)计算每个样本的残差
(2.2)拟合残差学习一棵回归树,得到
(2.3)更新
(3)不断重复上述过程直到得到最后的模型
至此,我们就完成了提升树的构建算法。
4.2 梯度提升决策树——对提升树的进一步提升
实际上,上面的提升树已经是一个比较完备的模型了。但是它存在一个问题,就是我们无法更换其他更加丰富的节点分裂方式。当我们采用平方损失或者指数损失指导节点分裂并利用前向分布算法优化时,每一步的优化并不复杂。但是如果换成其他损失函数优化起来就不容易了。针对这个问题Freidman提出了梯度提升算法,该算法是利用了最速下降法的一种近似替代方法。它利用损失函数在当前模型的预测值的负梯度
来替换残差。实际上,与其说这种负梯度时残差的近似值,倒不如说残差是负梯度的一种特例。
下面我们具体介绍GBDT算法:
给定
其中为特征空间中的特征向量,为数据对应的标签且取值范围为整个实数域。记损失函数为,最终的预测函数为,则
(1)初始化
(2) 对
(2.1)计算每个样本对应的负梯度
(2.2)拟合残差学习一棵回归树,得到树的所有叶节点区域,其中
(2.3)对,计算
(2.4)更新
(3)不断重复上述过程直到得到最后的模型
至此,我们就完成了GBDT的构建算法。
GBDT虽然一开始是用来解决回归问题的,但是也可以用来解决分类问题。
4.3 在sklearn中使用GBDT
from sklearn.metrics import mean_squared_error
from sklearn.datasets import make_friedman1
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
'''
GradientBoostingRegressor参数解释:
loss:{‘ls’, ‘lad’, ‘huber’, ‘quantile’}, default=’ls’:‘ls’ 指最小二乘回归. ‘lad’ (最小绝对偏差) 是仅基于输入变量的顺序信息的高度鲁棒的损失函数。. ‘huber’ 是两者的结合. ‘quantile’允许分位数回归(用于alpha指定分位数)
learning_rate:学习率缩小了每棵树的贡献learning_rate。在learning_rate和n_estimators之间需要权衡。
n_estimators:要执行的提升次数。
subsample:用于拟合各个基础学习者的样本比例。如果小于1.0,则将导致随机梯度增强。subsample与参数n_estimators。选择会导致方差减少和偏差增加。subsample < 1.0
criterion:{'friedman_mse','mse','mae'},默认='friedman_mse':“ mse”是均方误差,“ mae”是平均绝对误差。默认值“ friedman_mse”通常是最好的,因为在某些情况下它可以提供更好的近似值。
min_samples_split:拆分内部节点所需的最少样本数
min_samples_leaf:在叶节点处需要的最小样本数。
min_weight_fraction_leaf:在所有叶节点处(所有输入样本)的权重总和中的最小加权分数。如果未提供sample_weight,则样本的权重相等。
max_depth:各个回归模型的最大深度。最大深度限制了树中节点的数量。调整此参数以获得最佳性能;最佳值取决于输入变量的相互作用。
min_impurity_decrease:如果节点分裂会导致杂质的减少大于或等于该值,则该节点将被分裂。
min_impurity_split:提前停止树木生长的阈值。如果节点的杂质高于阈值,则该节点将分裂
max_features{‘auto’, ‘sqrt’, ‘log2’},int或float:寻找最佳分割时要考虑的功能数量:
如果为int,则max_features在每个分割处考虑特征。
如果为float,max_features则为小数,并 在每次拆分时考虑要素。int(max_features * n_features)
如果“auto”,则max_features=n_features。
如果是“ sqrt”,则max_features=sqrt(n_features)。
如果为“ log2”,则为max_features=log2(n_features)。
如果没有,则max_features=n_features。
'''
X, y = make_friedman1(n_samples=1200, random_state=0, noise=1.0)
X_train, X_test = X[:200], X[200:]
y_train, y_test = y[:200], y[200:]
est = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1,
max_depth=1, random_state=0, loss='ls').fit(X_train, y_train)
mean_squared_error(y_test, est.predict(X_test))
X, y = make_regression(random_state=0)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
reg = GradientBoostingRegressor(random_state=0)
reg.fit(X_train, y_train)
reg.score(X_test, y_test)
5. XGBoost——对GBDT的工程实现
5.1 XGBoost与GBDT的关系
具体来说,GBDT是一种算法,而XGBoost则是对GBDT的工程实现。除此以外,XGBoost还在GBDT的基础上做了很多优化。
5.2 XGBoost对GBDT的优化
首先是给目标函数添加了正则项,即对每棵树添加惩罚项,来限制树的复杂度,防止过拟合。然后是在计算残差时不再使用负梯度而是二阶泰勒展开,因此得到的残差更加精细,可以加快优化速度。再有就是GBDT采用CART作为基分类器,而XGBoost可以支持多种基分类器。接着是GBDT在每轮迭代会用到全体数据,而XGBoost采用了跟随机森林相似的方法,支持对数据的采样。最后就是XGBoost能自动学习缺失值的处理策略,而GBDT不行。