机器学习作业8 - AdaBoost

AdaBoost

Umm…这次作业写了很长时间,走了不少弯路,其实算法难度并不大,但是我一开始数据集选的比较奇葩,所以效果一直非常差,在50%左右徘徊,真是十分的绝望呀……

后来换了个数据集,效果稍微好了点,超过了75%,还算能看吧。值得一提的是这次的AdaBoost在数据集上的表现比SkLearn的AdaBoost要好一点,但是训练速度慢很多很多,我并不知道SkLearn的AdaBoost具体是怎么实现的,也很好奇它为啥训练的那么快,希望有大神能够解释下。

好吧,开始吧……

低配版决策树

首先构建一颗低配版决策树。和其他决策树使用信息增益确定是否划分不同,这颗决策树使用Loss来决定是否划分,划分思路如下:

  • 对样本的每一个维度中的数据进行排序,设第 i 维数据排序后为xi1,xi2,...,xin

  • 对每一对 xik,xi(k+1) 取其平均数作为划分的分界,并对样本进行划分,划分之后结合label,统计划分错误的样本,对于每个错误样本,乘上其权重,然后求和得到loss。

  • 选出上一步中loss最小的分界,对数据进行划分。如果不管怎么选取分界,划分后loss都大于不划分的情况,则停止。此外,如果决策树深度大于指定的最大深度,则停止划分。

决策树的划分部分代码如下所示:

    def __get_partition(self, label, data, type_count):
        max_gain, best_idx, best_splitter, left_type = 0.0, -1, 0.0, 0
        len_pos, data_length = len(np.where(label == 2)[0]), len(data[0])
        err_idx = np.where(label != np.argmax(type_count))
        min_err_weight = np.sum(self.__weights[err_idx])
        for idx in range(len(data)):
            this_data = data[idx]
            # 对该维度数据进行排序
            data_sorted, length = np.sort(this_data), len(this_data)
            for sub in range(len(this_data) - 1):
                # 将所有数据对的平均值作为划分临界值,尝试进行划分
                splitter = (data_sorted[sub] + data_sorted[sub + 1]) / 2.0
                cls_result = np.zeros([data_length], np.int)
                cls_result[np.where(this_data >= splitter)] = 2
                # 统计这一次划分尝试下的loss
                cls_err_idx = np.where(label != cls_result)
                cls_err_weight = np.sum(self.__weights[cls_err_idx])
                if cls_err_weight < min_err_weight:
                    min_err_weight, best_idx, best_splitter = cls_err_weight, idx, splitter
                # 重复上述操作,但是交换划分的结果分类
                cls_result = np.zeros([data_length], np.int)
                cls_result[np.where(this_data < splitter)] = 2
                cls_err_idx = np.where(label != cls_result)
                cls_err_weight = np.sum(self.__weights[cls_err_idx])
                if cls_err_weight < min_err_weight:
                    min_err_weight, best_idx, best_splitter = cls_err_weight, idx, splitter
        return min_err_weight, best_idx, best_splitter

构建决策树部分的代码如下所示:

    def __run_build(self, label, data, last_err, depth):
        node, data_trans, type_count = {}, np.transpose(data), np.bincount(label)
        # 找到最佳划分维度、最佳划分临界值以及划分后的loss(在这里叫err)
        err, best_idx, splitter = self.__get_partition(label, data_trans, type_count)
        # 如果深度达到上限,或者划分后loss大于不划分的loss,则停止扩展决策树
        if depth == self.__max_depth or err >= last_err:
            node['type'] = np.argmax(type_count)
            return node
        # 把数据划分为neg和pos两部分
        neg = np.where(data_trans[best_idx] < splitter)[0]
        pos = np.where(data_trans[best_idx] >= splitter)[0]
        if len(neg) < 1 or len(pos) < 1:
            node['type'] = 0 if len(pos) < 1 else 2
        else:
            node['left'] = self.__run_build(label[neg], data[neg], err, depth + 1)
            node['right'] = self.__run_build(label[pos], data[pos], err, depth + 1)
            node['splitter'], node['idx'] = splitter, best_idx
        return node

决策树构造函数和预测函数较为简单,构造函数主要保存参数并调用__run_build函数,预测函数从根节点开始,根据数据取值递归对树进行搜索,最终返回叶节点的结果。两个函数的代码如下所示。

    def __init__(self, label, data, weights, max_depth=5):
        self.__weights = weights
        self.__max_depth = max_depth
        self.__root = self.__run_build(label, data, np.inf, 0)

    def predict(self, data):
        # 如果数据只有一维,则将其包装一遍,便于后续操作
        result, data = [], [data] if np.shape(data) == 0 else data
        for cur in data:
            node = self.__root
            while node.get('idx') is not None:
                node = node['right'] if cur[node['idx']] >= node['splitter'] else node['left']
            predict_type = node.get('type')
            result.append(predict_type if predict_type is not None else 0)
        return result

接下来就是AdaBoost部分了。这部分比较简单,对AdaBoost不熟悉的可以参考这篇博文,实际上我的代码也是结合这篇博文实现的。下面我先放上代码,然后把自己的思路写在注释里。

class AdaBoost:
    def __init__(self, max_depth=10, max_times=20):
        self.__classifiers = []
        self.__classifier_weights = []
        self.__max_depth = max_depth
        self.__max_times = max_times

    def fit(self, data, label, show_loss=False):
        data_size = len(data)
        # 初始化数据权重,同时限制最大迭代self.__max_times次
        weights = np.ones([data_size], np.float32) / data_size
        for idx in range(self.__max_times):
            # 构建一颗低配版决策树
            weak_classifier = DecisionTree.Tree(label + np.ones([data_size], np.int),
                                                data, weights, self.__max_depth)
            # 评估决策树分类结果,找出分类错误的数据
            weak_result = weak_classifier.predict(data) - np.ones([data_size], np.int)、
            # 根据数据权重计算loss和alpha,然后结合alpha更新权重
            loss = np.sum(weights[np.where(label != weak_result)[0]])
            alpha = np.log((1 - loss) / loss) * 0.5
            weights = weights * np.exp(-alpha * label * weak_result)
            weights = weights / np.sum(weights)
            # 将决策树和决策树权重保存到数组里,供后续使用
            self.__classifiers.append(weak_classifier)
            self.__classifier_weights.append(alpha)
            if np.abs(loss - 0.50) <= 0.01: break
            if show_loss:
                print("Number: {:d}, Loss: {:.4f}, Weight: {:.4f}"
                      .format(idx, loss, alpha))

    def predict(self, data):
        out = np.zeros([len(data)], np.float64)
        for idx in range(len(self.__classifiers)):
            # 使用所有决策树进行预测,并结合权重计算预测结果
            out += self.__classifier_weights[idx] * (
                self.__classifiers[idx].predict(data) - np.ones([len(data)], np.int))
        # 返回sign函数处理的结果
        return np.sign(out)

最后在main函数中读取数据,进行验证。数据使用UCI上的 WineQuality数据集
数据共1599条,使用1000条训练,599条测试。代码如下:

import numpy as np
import AdaBoost

if __name__ == '__main__':
    file = open('Data/winequality-red.csv')
    lines = file.readlines()
    data, label = np.ndarray([len(lines) - 1, 11], np.float32),
                             np.ndarray([len(lines) - 1], np.int)
    for idx in range(1, len(lines)):
        line = lines[idx].split(';')
        data_str, label_str = line[:11], line[11]
        data[idx - 1], label[idx - 1] = np.array(data_str, np.float32),
                                        -1 if label_str <= '6' else 1
    classifier = AdaBoost.AdaBoost()
    classifier.fit(data[:1000], label[:1000], show_loss=True)
    result = classifier.predict(data[1000:])
    print('Accuracy: %.2f%%' %
          ((599 - len(np.where(label[1000:] != result)[0])) * 100 / 599))

运行结果如下:

Number: 0, Loss: 0.2060, Weight: 0.6746
Number: 1, Loss: 0.2934, Weight: 0.4396
Number: 2, Loss: 0.4612, Weight: 0.0778
Number: 3, Loss: 0.4591, Weight: 0.0820
Number: 4, Loss: 0.5510, Weight: -0.1023
Number: 5, Loss: 0.4420, Weight: 0.1165
Number: 6, Loss: 0.5305, Weight: -0.0612
Number: 7, Loss: 0.4432, Weight: 0.1141
Number: 8, Loss: 0.5146, Weight: -0.0292
Adaboost Accuracy: 75.29%

感觉效果并不是很好,可能是数据选择的不太好吧。不过SkLearn的精度只有72.79%,我还是很开心的。
那么,勉强撒花啦~源码点这里下载

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值