从0开始实现一个Adaboost分类器(完整代码)

2020-12-08 10:04:53 浏览数 (1)

导读

日前,通俗易懂的推导了三种集成学习的原理及主要公式,今天本文基于Python从0开始手动实现一个Adaboost分类器,文中提供完整代码。

01 Adaboost基本原理回顾

Adaboost作为一种提升集成算法,核心思想是不断训练弱学习器,来针对性的提升前一轮中预测错误样本的权重,最终通过加权所有弱学习器的训练结果得到最终分类标签。Adaboost是一种加权提升的集成算法,关键在于两个权重系数:

  • 弱学习器权重,影响每个弱学习器的结果对最终集成学习结果的影响程度,与该学习器的错误率有关
  • 样本权重,这也是Adaboost算法的精髓所在,即每轮训练弱学习器时不断优化调整样本间的权重,保证前一轮中学习错误的样本在下一轮训练中受到重点照顾

弱学习器的权重为:

alpha_m = frac{1}{2} lnfrac{1-e_m}{e_m}, e_m为学习器错误率

样本权重更新迭代公式为:

w_i^{(m)} = frac{w_i^{(m-1)}}{Z_m}*exp^{-alpha_{m-1}*y_i*G_{m-1}(x)}
Z_m = Sigma_{i=1}^N w_i^{(m)}

具体含义及推导过程详见:三种集成学习算法原理及核心公式推导

值得指出,在sklearn库内置的Adaboost算法中,当解决分类问题时弱学习器选择最大深度为1的决策树(俗称决策树桩),解决回归问题时则选择最大深度为3的决策树(CART)。

02 决策树桩

本文以分类问题为例实现Adaboost算法,所以首先探索实现一个最大深度只有一层的决策树桩。

简单起见,假设样本为连续数值型特征,要实现一个最大深度只有一层决策树桩,那么实际上无论有多少个特征,也仅会用到其中一个特征作为分类。则问题等价于确定以下三个参数:

  • 确定选择哪一列特征作为分类依据
  • 选择的特征列中,以什么数值作为二分类的阈值
  • 特征与阈值的判别符号问题,即大于阈值还是小于阈值判断为正类

由于是分类问题,那么选择最优参数的依据不妨可以选择为Accuracy。当然,由于该决策树桩需要支持样本权重参数,所以这里的Accuracy严谨的说是指所有分类正确的样本权重之和占所有样本权重之和的比例,当执行样本权重归一化时所有样本权重之和为1。

基于此,一个简单的决策树桩实现思路就比较清晰了,实现3重循环依次遍历寻找最有参数组合即可。另外,沿袭sklearn标准库中的做法,这里仅实现fit()、predict()和score()三个核心接口。

详细代码如下,配合注解应该比较简单易懂:

代码语言:javascript复制
class DecisionTreeClassifierWithWeight:
    def __init__(self):
        self.best_err = 1  # 最小的加权错误率
        self.best_fea_id = 0  # 最优特征id
        self.best_thres = 0  # 选定特征的最优阈值
        self.best_op = 1  # 阈值符号,其中 1: >, 0: <

    def fit(self, X, y, sample_weight=None):
        if sample_weight is None:
            sample_weight = np.ones(len(X)) / len(X)
        n = X.shape[1]
        for i in range(n):
            feature = X[:, i]  # 选定特征列
            fea_unique = np.sort(np.unique(feature))  # 将所有特征值从小到大排序
            for j in range(len(fea_unique)-1):
                thres = (fea_unique[j]   fea_unique[j 1]) / 2  # 逐一设定可能阈值
                for op in (0, 1):
                    y_ = 2*(feature >= thres)-1 if op==1 else 2*(feature < thres)-1  # 判断何种符号为最优
                    err = np.sum((y_ != y)*sample_weight)
                    if err < self.best_err:  # 当前参数组合可以获得更低错误率,更新最优参数
                        self.best_err = err
                        self.best_op = op
                        self.best_fea_id = i
                        self.best_thres = thres
        return self
    
    def predict(self, X):
        feature = X[:, self.best_fea_id]
        return 2*(feature >= self.best_thres)-1 if self.best_op==1 else 2*(feature < self.best_thres)-1
    
    def score(self, X, y, sample_weight=None):
        y_pre = self.predict(X)
        if sample_weight is not None:
            return np.sum((y_pre == y)*sample_weight)
        return np.mean(y_pre == y)

这里以sklearn库中自带的乳腺癌二分类数据集为例,以上述实现的决策树桩进行训练和评分,得到最终得分0.867,这对于一个仅有单层决策树的分类器来说效果还是比较好的。

代码语言:javascript复制
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

X, y = load_breast_cancer(return_X_y=True)
y = 2*y-1  # 将0/1取值映射为-1/1取值
X_train, X_test, y_train, y_test = train_test_split(X, y)

DecisionTreeClassifierWithWeight().fit(X_train, y_train).score(X_test, y_test)
# 0.8671328671328671

注:按照Adaboost中的算法约定,二分类模型中标签分别用-1和1代表负类和正类。

03 Adaboost集成分类器

在实现决策树桩作为弱分类器的基础上,实现Adaboost算法就仅需按照算法流程逐层训练即可。简单起见,这里仅设置超参数n_estimators用于选择弱分类器的个数。为区分于sklearn中的Adaboost标准内置库,本文将自定义实现的Adaboost分类算法命名为AdaBoostClassifier_,并设置相同的默认弱学习器数量超参数n_estimators=50,其余不做限制。

实质上,在逐渐调整样本权重的基础上,仅需逐层训练一个最优的决策树桩作为每轮的弱学习器,并保存在一个弱学习器列表中,同步记录每个弱学习器的权重系数。最后,在实现predict接口时,用每个弱学习器逐一完成训练,而后按其权重系数加权即可得到最终结果。完整代码如下:

代码语言:javascript复制
class AdaBoostClassifier_:
    def __init__(self, n_estimators=50):
        self.n_estimators = n_estimators
        self.estimators = []
        self.alphas = []

    def fit(self, X, y):
        sample_weight = np.ones(len(X)) / len(X)  # 初始化样本权重为 1/N
        for _ in range(self.n_estimators):
            dtc = DecisionTreeClassifierWithWeight().fit(X, y, sample_weight)  # 训练弱学习器
            alpha = 1/2 * np.log((1-dtc.best_err)/dtc.best_err)  # 权重系数
            y_pred = dtc.predict(X)
            sample_weight *= np.exp(-alpha*y_pred*y)  # 更新迭代样本权重
            sample_weight /= np.sum(sample_weight)  # 样本权重归一化
            self.estimators.append(dtc)
            self.alphas.append(alpha)
        return self

    def predict(self, X):
        y_pred = np.empty((len(X), self.n_estimators))  # 预测结果二维数组,其中每一列代表一个弱学习器的预测结果
        for i in range(self.n_estimators):
            y_pred[:, i] = self.estimators[i].predict(X)
        y_pred = y_pred * np.array(self.alphas)  # 将预测结果与训练权重乘积作为集成预测结果
        return 2*(np.sum(y_pred, axis=1)>0)-1  # 以0为阈值,判断并映射为-1和1

    def score(self, X, y):
        y_pred = self.predict(X)
        return np.mean(y_pred==y)

最后,继续以乳腺癌二分类数据集为例,对比测试自定义实现的AdaBoostClassifier_算法与sklearn标准库中的AdaBoostClassifer算法性能,得到如下结果:

代码语言:javascript复制
from sklearn.ensemble import AdaBoostClassifier
AdaBoostClassifier_().fit(X_train, y_train).score(X_test, y_test)
# 0.986013986013986
AdaBoostClassifier().fit(X_train, y_train).score(X_test, y_test)
# 0.965034965034965

除了训练效率略低,自定义实现Adaboost算法效果简直好的不得了。

本文按部就班的实现了一个Adaboost分类算法的baseline,实现了较好的分类效果,但仍有很多需要优化的点,例如对回归算法的支持、更多集成学习参数的设置以及特殊训练情况下的处理等。To be continued……

0 人点赞