机器学习实战之路 —— 4 Boosting算法

1. Boosting算法概述

一般集成学习的结构可如下图所示:通过样本训练集产生一组个体学习器,每个个体学习器完成训练后对于每个测试样本都会产生各自的预测结果。经由一种组合策略,从个体学习器的预测结果中选择一个结果或者给出一个平均值作为最终的输出。对于分类任务而言,个体学习器通常由现有的分类算法从训练数据中产生。但是每种分类器都会有各自的优点或者缺点,不可能适用所有的分类场景。因此,在具体的分类任务中,需要采用适用的分类算法作为集成学习的个体学习器。 一般而言,个体学习器存在一定差异的集成学习算法拥有更好的分类性能。
在这里插入图片描述

根据个体学习器的不同生成方式,目前集成学习算法按照学习的分类方式可以大致分为两类:并行序列化生成和串行生成。并行化生成个体学习器的算法有 Bagging和Random Forest<请参考博主前面写的随机森林算法章节博客>,它们通过采样方法,训练出多样性的基学习器,降低方差,之后 Random Forest 又引入了随机属性,使得多样性进一步提高,于是获得了更好的效果。
串行化生成个体学习器的代表算法则是Boosting (也称作提升法),通过对错判训练样本重新赋权来重复训练,通过提高基学习器准确性来降低偏差。

在这里插入图片描述

上图即展示了一个Boosting的实现过程,算法主要关注分错样本的数据处理。如果在当前建模的分类器中,存在分类错误的样本数据,则将会在下一次建模的分类器中,给上次分错的样本增大权重,如图2(m=2)之后的红点和蓝点被标识放大。随着训练次数增加,被分错的样本权重不断增大,被错分的样本将不断被重视及加大权重,直至模型所有样本基本实现分类正确。
Boosting的主要算法有Ada Boost、GBDT、XGboost,以下将对这三种算法分别展开论述。

2. 主要算法实现

2.1 AdaBoost

AdaBoost(Adaptive Boosting)是最著名的 Boosting 算法,属于提升类的自适应算法。其基本工作过程是:第一轮训练数据的权重都相同,训练得出第一个基分类器,之后,下一轮的基分类器的权重根据上一轮的基分类器的错分样本进行调整,即错分样本具有更高的权重值;之后,每一轮中具有不同权重值的样本对本轮基分类器进行训练,即在考虑样本不同权重的情况下得到本轮错误率最低的基分类器。重复以上步骤直到完成设定的轮数,每一轮训练得到一个基分类器,最后根据采取合适的结合策略得到次级分类器的预测结果。AdaBoost算法流程如下图所示。
在这里插入图片描述

其中,加权误差率 e 的定义为:
e = P ( G m ( x i ) ≠ y i ) = ∑ i = 1 N ω m i I ( G m ( x i ) ≠ y i ) e=P(G_m(x_i)≠y_i)=\sum_{i=1}^Nω_{mi}I(G_m(x_i)≠y_i) e=P(Gm(xi)=yi)=i=1NωmiI(Gm(xi)=yi)
其中 G m ( x i ) G_m(x_i) Gm(xi)为基本分类器。其系数为:
α m = 1 2 l n 1 − e m e m \alpha_m=\frac{1}{2}ln\frac{1-e_m}{e_m} αm=21lnem1em
样本的权值分布为:
ω m + 1 , i = ω m i Z m e ( − α m y i G m ( x i ) ) ω_{m+1,i}=\frac{ω_{mi}}{Z_m}e^{(-\alpha_my_iG_m(x_i))} ωm+1,i=Zmωmie(αmyiGm(xi))
其中 y i y_i yi为样本标记值,对于二分类, y i ∈ { − 1 , 1 } y_i∈\{-1,1\} yi{1,1} Z m Z_m Zm为规范化因子,使得 ω m + 1 , i ω_{m+1,i} ωm+1,i相加为1。
Z m = ∑ i = 1 N ω m i e ( − α m y i G m ( x i ) ) Z_m=\sum_{i=1}^Nω_{mi}e^{(-\alpha_my_iG_m(x_i))} Zm=i=1Nωmie(αmyiGm(xi))
最终分类器为:
G ( x ) = s i g n ( f ( x ) ) = s i g n ( ∑ m = 1 M α m G m ( x ) ) G(x)=sign(f(x))=sign\left({\sum_{m=1}^M\alpha_mG_m(x)}\right) G(x)=sign(f(x))=sign(m=1MαmGm(x))
ω m + 1 , i ω_{m+1,i} ωm+1,i可知被基本分类器 G m ( x ) G_m(x) Gm(x)误分类样本的权值得以扩大,而被正确分类样本的权值得以缩小。因此,误分类样本将在下一轮学中起更大的作用。训练数据没有改变,而不断改变训练数据权值的分布,使得训练数据在基本分类器的学习中起不同的作用。

α m \alpha_m αm可知其随着 e m e_m em的减小而增大,所以分类误差率越小的基本分类器在最终分类器中的作用越大。

实现代码如下:

"""
单层决策树分类
Args:
    dataMatrix - 数据矩阵
    dimen - 特征列
    threshVal - 阈值
    threshIneq - 标志位
Returns: retArray - 分类结果
"""
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
    retArray = np.ones((np.shape(dataMatrix)[0],1))         # 初始化retArray为1
    if threshIneq == 'lt':
        retArray[dataMatrix[:,dimen] <= threshVal] = -1.0   # 若小于等于阈值,赋值为-1
    else:
        retArray[dataMatrix[:,dimen] > threshVal] = -1.0    # 若大于阈值,赋值为-1
    return retArray

"""
基于权重向量生成单层决策树  
Args:
    dataArr - 数据矩阵
    classLabels - 类别标签
    D - 权重向量
Returns:
    bestStump - 最佳单层决策树信息
    minError - 最小误差
    bestClassEst - 最优分类结果
"""
def buildStump(dataArr, classLabels, D):
    dataMatrix = np.mat(dataArr); labelMat = np.mat(classLabels).T  # 转换数据及数据标签为numpy矩阵
    m, n = np.shape(dataMatrix)                                     # 数据集的行数和列数
    numSteps = 10.0                                                 # 遍历的步数
    bestStump = {}                                                  # 最佳单层决策树信息
    bestClassEst = np.mat(np.zeros((m, 1)))                         # 初始化最佳预测值
    minError = np.inf                                               # 初始化最小误差为正无穷大
    for i in range(n):                                              # 遍历特征
        rangeMin = dataMatrix[:, i].min()                           # 特征中的最小值
        rangeMax = dataMatrix[:, i].max()                           # 特征中的最大值
        stepSize = (rangeMax - rangeMin) / numSteps                 # 计算步长
        for j in range(-1, int(numSteps) + 1):                      # 遍历步长
            for inequal in ['lt', 'gt']:                            # 遍历大于、小于
                threshVal = (rangeMin + float(j) * stepSize)        # 计算阈值
                predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)  # 计算分类结果
                errArr = np.mat(np.ones((m, 1)))                    # 初始化误差矩阵
                errArr[predictedVals == labelMat] = 0               # 分类正确的赋值为0
                weightedError = D.T * errArr                        # 计算加权错误率
                if weightedError < minError:                        # 找到误差最小的分类方式,并赋值
                    minError = weightedError
                    bestClassEst = predictedVals.copy()
                    bestStump['dim'] = i
                    bestStump['thresh'] = threshVal
                    bestStump['ineq'] = inequal
    return bestStump, minError, bestClassEst

"""
基于单层决策树的AdaBoost训练    
Args:
    dataArr - 数据矩阵
    classLabels - 类别标签
    numIt - 迭代次数
Returns:
    weakClassArr - 分类器数组
    aggClassEst - 类别估计累计值
"""
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
    weakClassArr = []                                                   # 分类器数组
    m = np.shape(dataArr)[0]                                            # 数据集的特征值个数
    D = np.mat(np.ones((m,1))/m)                                        # 初始化权重向量
    aggClassEst = np.mat(np.zeros((m,1)))                               # 初始化类别估计累计值
    for i in range(numIt):                                              # 根据迭代次数遍历
        bestStump,error,classEst = buildStump(dataArr,classLabels,D)    # 基于权重向量生成单层决策树
        alpha = float(0.5 * np.log((1.0 - error) / max(error, 1e-16)))  # 计算权重alpha,防止发生除零溢出
        bestStump['alpha'] = alpha                                      # 记录权重值
        weakClassArr.append(bestStump)                                  # 存储最好的单层决策树
        expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst) # 计算e的指数项
        D = np.multiply(D, np.exp(expon))                               # 计算新的权重向量
        D = D / D.sum()                                                 # 权重向量归一化
        aggClassEst += alpha * classEst                                 # 计算类别估计累计值
        aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T, np.ones((m, 1))) # 计算误差
        errorRate = aggErrors.sum() / m
        print("total error: ", errorRate)
        if errorRate == 0.0: break                                      # 误差为0则退出循环
    return weakClassArr, aggClassEst

"""
AdaBoost分类函数
Args:
    datToClass - 待分类样例
    classifierArr - 训练好的弱分类器
Returns: 分类结果
"""
def adaClassify(datToClass,classifierArr):
    dataMatrix = np.mat(datToClass)             # 转换待分类样例为numpy矩阵
    m = np.shape(dataMatrix)[0]                 # 待分类样例个数
    aggClassEst = np.mat(np.zeros((m,1)))       # 初始化类别估计累计值
    for i in range(len(classifierArr)):         # 遍历所有弱分类器
        classEst = stumpClassify(dataMatrix,classifierArr[i]['dim'],
                                 classifierArr[i]['thresh'],
                                 classifierArr[i]['ineq'])  # 计算类别估计值
        aggClassEst += classifierArr[i]['alpha']*classEst   # 根据权重累加得到类别估计累计值
        print(aggClassEst)
    return np.sign(aggClassEst)


if __name__ == '__main__':
    dataArr,classLabels = loadSimpData()
    weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
    print(adaClassify([[0,0],[5,5]], weakClassArr))

使用sklearn.ensemble 实现 AdaBoost 代码如下:

from sklearn.ensemble import AdaBoostClassifier

# 决策树
base_estimator = DecisionTreeClassifier(criterion='gini', max_depth=3, min_samples_split=4)
# AdaBoost学习器,10棵树,学习率为0.1
model = AdaBoostClassifier(base_estimator=base_estimator, n_estimators=10, learning_rate=0.1)
# 训练AdaBoost学习器
model.fit(x_train, y_train)
# AdaBoost学习器预测训练集
y_train_pred = model.predict(x_train)

2.2 GBDT

Gradient Boosting与之前的Boosting的不同点在于,在每一次进行训练的目的是为了减少上一次的残差,为了不断的降低残差,我们需要在减少残差的梯度方向训练一个新的模型,所以可以理解,Gradient Boosting算法训练每一个新的模型都是为让之前的模型的残差在梯度方向上降低。将Gradient Boosting用于决策树模型上之后,就是GBDT(Gradient Boosting Decision Tree,梯度提升树),其损失函数的负梯度在当前模型的值:
− [ ∂ L ( y , f ( x i ) ) ) ∂ f ( x i ) ] f ( x ) = f m − 1 ( x ) -\bigg[\frac{\partial L(y, f(x_i)))}{\partial f(x_i)}\bigg]_{f(x) = f_{m-1} (x)} [f(xi)L(y,f(xi)))]f(x)=fm1(x)
作为回归问题提升树算法中的残差的近似值,拟合一个回归树。

实现代码如下:
使用sklearn.ensemble 实现 GBDT 代码如下:

from sklearn.ensemble import GradientBoostingClassifier

# GBDT,100棵树,学习率0.1,最大深度2
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=2)
# GBDT训练
gb.fit(x_train, y_train.ravel())
# GBDT预测训练集
y_train_pred = gb.predict(x_train)

2.3 XGBoost

GBDT在优化时仅用一阶导数信息,XGBoost则对损失函数进行了二阶泰勒展开,同时用到了一阶导数和二阶导数,加入该正则化项对模型的复杂程度施加惩罚,以提高模型的泛化能力,防止过拟合,也具备较高的计算效率。

实现代码如下:

import xgboost as xgb

# 设置参数
# max_depth - 树的深度为3
# eta - 学习率,为1时是原始模型,过小的学习率会造成计算次数增多,可防止过拟合,通过减少每一步的权重,可以提高模型的鲁棒性。典型值0.01-0.2
# silent-是否输出中间结果
# objective-定义需要被最小化的损失函数(分类问题的逻辑回归)
param = {'max_depth': 3, 'eta': 1, 'silent': 1, 'objective': 'binary:logistic'}
watchlist = [(data_test, 'eval'), (data_train, 'train')]    # 输出过程中的错误率
n_round = 7 # 7颗决策树
# 训练XGBoost
bst = xgb.train(param, data_train, num_boost_round=n_round, evals=watchlist)
# XGBoost预测数据集
y_hat = bst.predict(data_test)

3. 实战 - 鸢尾花数据集分类

本小节所用实战数据同样为前面章节博客用过的经典莺尾花数据集,具体的数据描述可参见前面博客。以下分别给出AdaBoost和GBDT & XGBoost的算法实现。

3.1 AdaBoost

与前面的随机森林章节一样,对鸢尾花数据选取两特征组合进行分类,使用Adaboost算法实现代码如下:

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split


if __name__ == "__main__":
    mpl.rcParams['font.sans-serif'] = ['SimHei']
    mpl.rcParams['axes.unicode_minus'] = False
    # 读取数据
    path = 'iris.data'  # 数据文件路径
    data = pd.read_csv(path, header=None)
    # 特征定义
    iris_feature = '花萼长度', '花萼宽度', '花瓣长度', '花瓣宽度'
    # 数据处理:如果是字符型特征,则转换成数值型
    x_prime = data[list(range(4))]              # 数值型数据,无需额外转换操作
    y = pd.Categorical(data[4]).codes           # 字符型数据,需转换成数值型
    
    # 拆分数据为训练集(70%)和测试集(30%)
    x_prime_train, x_prime_test, y_train, y_test = train_test_split(x_prime, y, train_size=0.7, random_state=0)
	# 特征组合
    feature_pairs = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
    plt.figure(figsize=(11, 8), facecolor='#FFFFFF')
    for i, pair in enumerate(feature_pairs):
        # 准备数据
        x_train = x_prime_train[pair]
        x_test = x_prime_test[pair]

        # 决策树
        base_estimator = DecisionTreeClassifier(criterion='gini', max_depth=3, min_samples_split=4)
        # AdaBoost学习器,10棵树,学习率为0.1
        model = AdaBoostClassifier(base_estimator=base_estimator, n_estimators=10, learning_rate=0.1)
        model.fit(x_train, y_train)

        # 画图
        N, M = 500, 500  # 横纵各采样多少个值
        x1_min, x2_min = x_train.min()
        x1_max, x2_max = x_train.max()
        t1 = np.linspace(x1_min, x1_max, N)
        t2 = np.linspace(x2_min, x2_max, M)
        x1, x2 = np.meshgrid(t1, t2)  # 生成网格采样点
        x_show = np.stack((x1.flat, x2.flat), axis=1)  # 测试点

        # 训练集上的预测结果
        y_train_pred = model.predict(x_train)
        acc_train = accuracy_score(y_train, y_train_pred)
        y_test_pred = model.predict(x_test)
        acc_test = accuracy_score(y_test, y_test_pred)
        print('特征:', iris_feature[pair[0]], ' + ', iris_feature[pair[1]])
        print('\t训练集准确率: %.4f%%' % (100*acc_train))
        print('\t测试集准确率: %.4f%%\n' % (100*acc_test))

        cm_light = mpl.colors.ListedColormap(['#A0FFA0', '#FFA0A0', '#A0A0FF'])
        cm_dark = mpl.colors.ListedColormap(['g', 'r', 'b'])
        y_hat = model.predict(x_show)
        y_hat = y_hat.reshape(x1.shape)
        plt.subplot(2, 3, i+1)
        plt.contour(x1, x2, y_hat, colors='k', levels=[0, 1], antialiased=True, linestyles='--', linewidths=1.5)
        plt.pcolormesh(x1, x2, y_hat, cmap=cm_light)  # 预测值
        plt.scatter(x_train[pair[0]], x_train[pair[1]], c=y_train, s=20, edgecolors='k', cmap=cm_dark)
        plt.scatter(x_test[pair[0]], x_test[pair[1]], c=y_test, s=100, marker='*', edgecolors='k', cmap=cm_dark)
        plt.xlabel(iris_feature[pair[0]], fontsize=14)
        plt.ylabel(iris_feature[pair[1]], fontsize=14)
        plt.xlim(x1_min, x1_max)
        plt.ylim(x2_min, x2_max)
        plt.grid(b=True)
    plt.suptitle('Adaboost对鸢尾花数据两特征组合的分类结果', fontsize=18)
    plt.tight_layout(1, rect=(0, 0, 1, 0.95))    # (left, bottom, right, top)
    plt.show()

输出结果如下:

特征: 花萼长度  +  花萼宽度
	训练集准确率: 87.6190%
	测试集准确率: 75.5556%

特征: 花萼长度  +  花瓣长度
	训练集准确率: 99.0476%
	测试集准确率: 88.8889%

特征: 花萼长度  +  花瓣宽度
	训练集准确率: 98.0952%
	测试集准确率: 88.8889%

特征: 花萼宽度  +  花瓣长度
	训练集准确率: 100.0000%
	测试集准确率: 95.5556%

特征: 花萼宽度  +  花瓣宽度
	训练集准确率: 98.0952%
	测试集准确率: 93.3333%

特征: 花瓣长度  +  花瓣宽度
	训练集准确率: 99.0476%
	测试集准确率: 97.7778%

输出的分类结果图像如下:

在这里插入图片描述

3.2 GBDT & XGBoost

分别使用XGBoost、GBDT、随机森林、逻辑回归对鸢尾花数据进行分类,实现代码如下:

import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegressionCV
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score


if __name__ == "__main__":
	# 读取数据
    path = 'iris.data'    				   
    data = pd.read_csv(path, header=None)   
    # 数据处理:如果是字符型特征,则转换成数值型
    x = data[list(range(4))]              # 数值型数据,无需额外转换操作
    y = pd.Categorical(data[4]).codes           # 字符型数据,需转换成数值型
                
    # 拆分数据为训练集(50%)和测试集(50%)
    x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=1, test_size=50)

    # 转换为xgboost中的矩阵格式
    data_train = xgb.DMatrix(x_train, label=y_train)
    data_test = xgb.DMatrix(x_test, label=y_test)
    watch_list = [(data_test, 'eval'), (data_train, 'train')]   # 输出过程中的错误率
    # 3分类的softmax训练模型
    param = {'max_depth': 8, 'eta': 0.8, 'silent': 1, 'objective': 'multi:softmax', 'num_class': 3}

    # 训练XGBoost
    bst = xgb.train(param, data_train, num_boost_round=6, evals=watch_list)
    # XGBoost预测数据集
    y_hat = bst.predict(data_test)
    result = y_test == y_hat
    print('测试集正确率:\t', float(np.sum(result)) / len(y_hat))
    print('END.....\n')

    # 分别用GBDT、随机森林和逻辑回归训练
    models = [
        # GBDT 30棵决策树,学习率为0.1,树的最大深度为5
        ('GBDT', GradientBoostingClassifier(n_estimators=30, learning_rate=0.1, max_depth=5)),
        # 随机森林 30棵决策树,特征选择用gini系数,树的最大深度为5
        ('RandomForest', RandomForestClassifier(n_estimators=30, criterion='gini', max_depth=5)),
        ('LogisticRegression', LogisticRegressionCV(Cs=np.logspace(1, 100, 100), cv=3))
    ]
    for name, model in models:
        model.fit(x_train, y_train)
        print(name, '训练集正确率:', accuracy_score(y_train, model.predict(x_train)))
        print(name, '测试集正确率:', accuracy_score(y_test, model.predict(x_test)))

输出结果如下:

[0]	eval-merror:0.02	train-merror:0.02
[1]	eval-merror:0.02	train-merror:0.02
[2]	eval-merror:0.02	train-merror:0.02
[3]	eval-merror:0.04	train-merror:0.01
[4]	eval-merror:0.04	train-merror:0.01
[5]	eval-merror:0.04	train-merror:0.01
测试集正确率:	 0.96
END.....

GBDT 训练集正确率: 1.0
GBDT 测试集正确率: 0.96
RandomForest 训练集正确率: 1.0
RandomForest 测试集正确率: 0.96
LogisticRegression 训练集正确率: 0.97
LogisticRegression 测试集正确率: 0.92

4.参考学习的书目及论文

  1. 机器学习算法视频 - 邹博
  2. 《机器学习实战》第7章 利用AdaBoost元算法提高分类性能
  3. 《机器学习之路》第2章 2.6 模型融合
  4. 《机器学习 - 周志华》第8章 集成学习
  5. 《统计学习方法》第8章 提升方法
  6. 《基于Boosting的集成树算法研究与分析》 连克强 2019 硕士论文 第3章 Boosting 集成方法
  7. 《基于AdaBoost算法的车载CAN总线报文异常检测》王成龙 2019 硕士论文 第4章 基于AdaBoost算法的CAN总线报文异常检测

=文档信息=
本学习笔记由博主整理编辑,仅供非商用学习交流使用
由于水平有限,错误和纰漏之处在所难免,欢迎大家交流指正
如本文涉及侵权,请随时留言博主,必妥善处置
版权声明:非商用自由转载-保持署名-注明出处
署名(BY) :zhudj
文章出处:https://zhudj.blog.csdn.net/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值