ML:利用 AdaBoost 元算法提高分类性能

  • 元算法(meta-algorithm):对其他算法进行组合的一种方式

1. 基于数据集多重抽样的分类器

  • 之前的文章中学习了五种不同的分类器:K 近邻,决策树,朴素贝叶斯,逻辑回归,支持向量机。它们各有优缺点,我们可以将不同的分类器组合起来,这种组合结果则称为集成方法(ensemble method)或者元算法
  • 集成方法有多种形式,可以是不同的算法的集成,也可以是同一算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。

1.1 bagging: 基于数据随机重抽样的分类器构建方法

  • 自汇聚方法 (bootstrap aggregating),也称为 bagging 方法,是在从原始数据集选择 S 次后得到 S 个新数据集的一种技术。每个数据集都是原数据集有放回抽样得到的,大小和原数据集相等。
  • 将某个学习算法分别作用于每个数据集就得到了 S 个分类器,对于新的数据集,应用这 S 个分类器进行分类,选择结果中最多的类别作为最终的分类结果
  • 还有一些更先进的 bagging 方法,比如随机森林(Bagging + 决策树)

1.2 boosting

  • boosting 与 bagging 类似,不过前者是通过串行训练得到的,每个分类器都根据已训练出的分类器的性能来进行训练。boosting 是通过集中关注被已有分类器错分的那些数据来获得新的分类器,即赋予这些数据更多的权重,并且每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重
  • boosting 方法有多个版本,最流行的版本为 AdaBoost,其优点在于泛化错误率低,易编码,可以应用在大部分分类器上,无参数调整,缺点在于对离群点敏感

2. 训练算法:基于错误提升分类器的性能

AdaBoosting:Adaptive Boosting,自适应 Boosting,运行过程如下

  • 训练数据中的每个样本,赋予一个权重,权重构成向量 D,初始时每个权重都相等
  • 在训练数据上训练出一个弱分类器,计算该分类器的错误率,然后在同一数据集上再次训练弱分类器
  • 在分类器的第二次训练中,将会重新调整每个样本的权重,其中第一次分对的样本的权重会降低,分错的样本的权重会提高
  • 每个分类器都有一个权重值 alpha,该值是基于每个弱分类器的错误率进行计算的
  • 错误率: ϵ = 未 分 类 正 确 的 样 本 数 目 所 有 样 本 数 目 \epsilon = \frac{未分类正确的样本数目}{所有样本数目} ϵ=
  • alpha: α = 1 2 l n ( 1 − ϵ ϵ ) \alpha = \frac{1}{2}ln\left(\frac{1-\epsilon}{\epsilon}\right) α=21ln(ϵ1ϵ)
  • alpha 计算出来后,可以对D进行更新:
    如果某个样本被正确分类: D i ( t + 1 ) = D i ( t ) e − α s u m ( D ) D_i^{(t+1)} = \frac{D_i^{(t)}e^{-\alpha}}{sum(D)} Di(t+1)=sum(D)Di(t)eα如果某个样本被错误分类: D i ( t + 1 ) = D i ( t ) e α s u m ( D ) D_i^{(t+1)} = \frac{D_i^{(t)}e^{\alpha}}{sum(D)} Di(t+1)=sum(D)Di(t)eα
    统一写作: D i ( t + 1 ) = D i ( t ) e − α ⋅ y i ⋅ y i ^ s u m ( D ) D_i^{(t+1)} = \frac{D_i^{(t)}e^{-\alpha\cdot y_i\cdot \hat{y_i}}}{sum(D)} Di(t+1)=sum(D)Di(t)eαyiyi^
  • 迭代计算 D,直到训练错误率为 0 或弱分类器数目达到指定值

3. 基于单层决策树构建弱分类器

  • 单层决策树:决策树桩(decision stump),基于单个特征来做决策
  • 对于下图中的数据,很明显该数据不能基于单个特征来分类,即选择一条平行于坐标轴的直线来分类。而通过多个单层决策树,可以构建出一个能够对该数据集完全正确分类的分类器

在这里插入图片描述

  • 首先,第一个函数 stump_classify 即根据输入的判断标识,判断特征值大于或小于阈值,得到并返回分类结果
def stump_classify(data_matrix, dim, thresh_val, thresh_ineq):
    '''
    data_matrix: 输入矩阵
    dim: 第几个特征
    thresh_val: 阈值
    thresh_ineq: 标识
    '''
    from numpy import *
    retArray = ones((shape(data_matrix)[0],1))
    if thresh_ineq == 'lt':    # 标识为 lessthan 则小于阈值为 -1
        retArray[data_matrix[:,dim] <= thresh_val] = -1.0
    else:    # 标识为 greaterthan 则小于阈值为 -1
        retArray[data_matrix[:,dim] > thresh_val] = -1.0
    return retArray
  • 然后,第二个函数 build_stump 用于构建单层决策树,其输入为特征矩阵,分类标签,数据权重向量,输出最佳单层决策树,最小误差以及最佳分类结果。该函数遍历每个特征,每个特征值步长,每个不等号条件,总共三层循环,计算加权错误率,找到加权错误率最小的单层决策树。
def build_stump(data_arr, class_labels, D):
    '''
    Parameters:
        data_arr: 特征数组
        class_labels: 标签数组
        D: 权重
    returns:
        best_stump: 最佳单层决策树
        min_error: 最小误差
        best_cls_est: 最佳的分类结果
    '''
    from numpy import *
    data_mat = mat(data_arr)
    label_mat = mat(class_labels).T
    m, n = shape(data_mat)
    steps = 10.0    # 用于计算步长
    best_stump = {}    # 初始化最佳单层决策树
    best_cls_est = mat(zeros((m,1)))    # 初始化最佳的分类结果
    min_error = inf    # 初始化最小误差为正无穷
    for i in range(n):    # 遍历每个特征
        range_min = data_mat[:,i].min()    # 特征最小值
        range_max = data_mat[:,i].max()    # 特征最大值
        step_size = (range_max - range_min) / steps    # 计算遍历步长
        for j in range(-1, int(steps) + 1):    # 遍历每个步长
            for inequal in ['lt', 'gt']:    # 遍历每种不等式
                thresh_val = (range_min + float(j) * step_size)    # 通过步长计算阈值
                predicted_val = stump_classify(data_mat, i, thresh_val, inequal)    # 预测值
                err_arr = mat(ones((m, 1)))    # 初始化误差矩阵
                err_arr[predicted_val == label_mat] = 0
                weighted_error = D.T * err_arr    # 根据权重计算误差
                print('split: idm %d, thresh %.2f, thresh inequal: %s, \
                      the weighted error is %.3f' % (i, thresh_val, inequal, weighted_error))
                if weighted_error < min_error:    # 找到误差最小的分类方式
                    min_error = weighted_error
                    best_cls_est = predicted_val.copy()
                    best_stump['dim'] = i
                    best_stump['thresh'] = thresh_val
                    best_stump['ineq'] = inequal
    return best_stump, min_error, best_cls_est
  • 载入数据,并测试结果:
def load_simp_data():
    data = matrix([[1., 2.1],[1.5,1.6],[1.3,1.],[1.,1.],[2.,1.]])
    labels = [1.0, 1.0, -1.0, -1.0, 1.0]
    return data, labels

data, labels = load_simp_data()
D = mat(ones((5,1)) / 5)
build_stump(data, labels, D)
[Out]: ({'dim': 0, 'thresh': 1.3, 'ineq': 'lt'}, matrix([[0.2]]), array([[-1.],[ 1.],[-1.],[-1.],[ 1.]]))
  • 上面构建的单层决策树即弱分类器,我们将使用多个若分类器来构建 AdaBoost 代码

4. 完整 AdaBoost 算法的实现

  • 伪代码如下:

    对每次迭代:
        利用 build_stump 函数找到最佳的单层决策树
        将最佳单层决策树加入到单层决策树组
        计算 alpha
        计算新的权重向量 D
        更新累计类别估计值
        如果错误率等于 0,则退出循环
    
'''基于单层决策树的 AdaBoost 训练过程'''
def ada_boost_train_ds(data_arr, class_labels, num_iter=40):
    '''
    Parameters:
        data_arr: 输入矩阵
        class_labels: 输入标签
        num_iter: 最大迭代次数
    Returns:
        weak_class_arr: 弱分离器数组
    '''
    weak_class_arr = []    # 初始化单层决策树数组
    m = shape(data_arr)[0]    # 数据量大小
    D = mat(ones((m,1)) / m)    # 初始化数据权重向量
    agg_class_est = mat(zeros((m,1)))    # 初始化每条数据的类别估计累计值
    for i in range(num_iter):
        best_stump, error, class_est = build_stump(data_arr, class_labels, D)
        print('D: ', D.T)
        alpha = float(0.5 * log((1.0 - error) / max(error, 1e-16)))    # 计算分类器权重,1e-16 避免除零错误
        best_stump['alpha'] = alpha
        weak_class_arr.append(best_stump)
        print('class_est: ', class_est.T)
        expon = multiply(-1 * alpha * mat(class_labels).T, class_est)    # 计算指数项
        D = multiply(D, exp(expon))
        D = D / D.sum()
        agg_class_est += alpha * class_est    # 每条数据的类别估计累计值
        print('agg_class_est: ', agg_class_est.T)
        # 计算训练错误率
        agg_errors = multiply(sign(agg_class_est) != mat(class_labels).T, ones((m,1)))
        error_rate = agg_errors.sum() / m
        print('total error: ', error_rate, '\n')
        if error_rate == 0.0:
            break
    return weak_class_arr

classifier_array = ada_boost_train_ds(data, labels, 9)
[Out]:
	D:  [[0.2 0.2 0.2 0.2 0.2]]    # 初始权重都相等
	class_est:  [[-1.  1. -1. -1.  1.]]    
	agg_class_est:  [[-0.69314718  0.69314718 -0.69314718 -0.69314718  0.69314718]]    # 第一个数据被错分
	total error:  0.2 
	
	D:  [[0.5   0.125 0.125 0.125 0.125]]    # 第一条数据权重增加
	class_est:  [[ 1.  1. -1. -1. -1.]]
	agg_class_est:  [[ 0.27980789  1.66610226 -1.66610226 -1.66610226 -0.27980789]]    # 最后一个数据被错分
	total error:  0.2 
	
	D:  [[0.28571429 0.07142857 0.07142857 0.07142857 0.5       ]]    # 最后一个数据权重增加
	class_est:  [[1. 1. 1. 1. 1.]]
	agg_class_est:  [[ 1.17568763  2.56198199 -0.77022252 -0.77022252  0.61607184]]    # 全部分类正确
	total error:  0.0 

classifier_array    # 三个弱分类器及其权重
[Out]:
	[{'dim': 0, 'thresh': 1.3, 'ineq': 'lt', 'alpha': 0.6931471805599453},
	 {'dim': 1, 'thresh': 1.0, 'ineq': 'lt', 'alpha': 0.9729550745276565},
	 {'dim': 0, 'thresh': 0.9, 'ineq': 'lt', 'alpha': 0.8958797346140273}]

5. 测试算法:基于 AdaBoost 的分类

  • 一旦拥有了多个分类器及其对应的 alpha 值,我们就可以进行测试了,每个弱分类器的结果加权求和就是我们的最终结果
'''AdaBoost 分类函数'''
def ada_classify(data_to_class, classifier_array):
    '''
    Parameters:
        data_to_class: 待分类数据
        classifier_array: 弱分类器组成的数组
    Returns:
        sign(agg_class_est): 最终分类结果
    '''
    data_mat = mat(data_to_class)
    m = shape(data_mat)[0]
    agg_class_est = mat(zeros((m,1)))    # 结果初始化
    for i in range(len(classifier_array)):
        class_est = stump_classify(data_mat, classifier_array[i]['dim'], classifier_array[i]['thresh'], 
                                   classifier_array[i]['ineq'])    # 每个弱分类器的分类结果
        agg_class_est += classifier_array[i]['alpha'] * class_est    # 结果加权
        print(agg_class_est)
    return sign(agg_class_est)

ada_classify([[5, 5],[0, 0]], classifier_array)
[Out]:
	[[ 0.69314718]
	 [-0.69314718]]
	[[ 1.66610226]
	 [-1.66610226]]
	[[ 2.56198199]
	 [-2.56198199]]

	matrix([[ 1.],
	        [-1.]])

6. 练习:在一个难数据集上应用 AdaBoost

def load_data_set(filename):
    num_feat = len(open(filename).readline().split('\t'))
    data_mat = []
    label_mat = []
    fr = open(filename)
    for line in fr.readlines():
        line_arr = []
        cur_line = line.strip().split('\t')
        for i in range(num_feat - 1):
            line_arr.append(float(cur_line[i]))
        data_mat.append(line_arr)
        label_mat.append(float(cur_line[-1]))
    return data_mat, label_mat
data, labels = load_data_set('Ch07/horseColicTraining2.txt')
classifier_array = ada_boost_train_ds(data, labels, 10)
[Out]: ...total error:  0.23076923076923078 

test_d, test_l = load_data_set('Ch07/horseColicTest2.txt')
predict = ada_classify(test_d, classifier_array)
err = mat(ones((67,1)))
err[predict != mat(test_l).T].sum() / 67
[Out]: 0.23880597014925373
  • 通常情况下,测试错误率会达到一个稳定值,而不会随弱分类器的增加而降低。对于上述数据集,测试发现测试错误率在达到最小值后又开始升高,这称为过拟合。

7. 非均衡分类问题

  • 前面的所有分类中,我们都假设所有类别的分类代价是一样的。但大多数情况下不同类别的分类代价并不相等,我们有一些其他分类性能度量指标,可以将代价考虑在内

7.1 正确率、召回率、ROC 曲线

  • 混淆矩阵:预测值和实际值组成的矩阵
  • 正确率:预测为正中真正为正的比例 P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP+FP} Precision=TP+FPTP
  • 召回率:预测为正中真正为正的数据占所有真正为正的数据的比例 R e c a l l = T P T P + F N Recall = \frac{TP}{TP+FN} Recall=TP+FNTP
  • ROC 曲线:横坐标为假阳率(FP / (FP+TN), 错分为正的数据占所有真正为负的比例),纵坐标为真阳率(召回率),原点为将所有样本判为负的情况,(1,1)为将所有样本判为正的情况。好的分类器 ROC 曲线应尽可能地处于左上角,即在低假阳率的情况下得到高的真阳率。

在这里插入图片描述

  • 绘制 ROC 曲线的代码如下:
def plotROC(pred_str, labels):
    '''
    Parameters:
        pred_str: 分类器的预测强度
        labels: 实际标签
    '''
    import matplotlib.pyplot as plt
    cur = (1.0,1.0)   # 从 (1.0, 1.0) 开始绘制,即所有数据都判为正
    y_sum = 0.0    # 计算曲线下面积
    num_pos_cls = sum(array(labels) == 1.0)    # 为正的数量
    y_step = 1/ float(num_pos_cls)    # y 轴步长
    x_step = 1/ float(len(labels) - num_pos_cls)  # x 轴步长
    sorted_idx = pred_str.argsort()    # 获取从小到达排序的索引值
    fig = plt.figure()
    fig.clf()
    ax = plt.subplot(111)
    for index in sorted_idx.tolist()[0]:
        if labels[index] == 1.0:    # 如果该样本为 1,则修改真阳率
            del_x = 0
            del_y = y_step
        else:    # 如果该样本不为 1,则修改假阳率
            del_x = x_step
            del_y = 0
            y_sum += cur[1]    # 高度累加方便计算面积
        ax.plot([cur[0], cur[0]-del_x], [cur[1], cur[1]-del_y], c='b')
        cur = (cur[0]-del_x, cur[1]-del_y)
    ax.plot([0, 1], [0, 1], 'b--')    # 绘制随机猜测曲线
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    ax.axis([0,1,0,1])
    plt.show()
    print('the area under the curve is: ', y_sum * x_step)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值