GBDT(梯度提升树)基本原理及python实现

GBDT实现原理

  • GBDT基本原理
    • 背景
      • 提升树-boosting tree
    • GBDT实例
      • 预测年龄
      • 预测年龄的残差
      • GBDT
      • 公式推导
  • GBDT的python实现
    • CART回归树实现
    • GBDT实现
    • 参考

GBDT基本原理

背景

决策树是一种基本的分类回归方法。决策树模型具有分类速度快,模型容易可视化等优点,但是同时是也有容易发生过拟合,虽然有剪枝,但也是差强人意。
如果你CART树(对分类回归树)的知识不熟悉,请看这篇文章:
CART分类回归树分析与python实现
提升方法(boosting)在分类问题中,它通过改变训练样本的权重(增加分错样本的权重,减小分对样本的的权重),学习多个分类器,并将这些分类器线性组合,提高分类器性能。boosting数学表示为:
f ( x ) = w 0 + ∑ m = 1 M w m ϕ m ( x ) f(x)=w_{0}+\sum_{m=1}^{M}{w_{m}\phi_{m}(x)} f(x)=w0+m=1Mwmϕm(x)
其中:
w m w_{m} wm:第m个弱分类分类器权重
ϕ m ( x ) \phi_{m}(x) ϕm(x):第m个弱分类器
可以看出最终就是基函数的线性组合。
于是决策树与boosting结合产生许多算法,主要有提升树、GBDT等。本文主要是GBDT学习笔记。

提升树-boosting tree

以决策树为基函数的提升方法称为提升树,其决策树可以是分类树或回归树。提升树模型可以表示为决策树的加法模型。
f M ( x ) = ∑ m = 1 M T ( x ; θ m ) f_{M}(x)=\sum_{m=1}^{M}{T(x;\theta_{m})} fM(x)=m=1MT(x;θm)
其中:
T ( x ; θ m ) T(x;\theta_{m}) T(x;θm):表示第m棵决策树
θ m \theta_{m} θm:表示树的参数
M M M:树的个数

GBDT实例

预测年龄

我们不妨假设同事的年龄分别为5岁、6岁、7岁,那么同事的平均年龄就是6岁。所以我们用6岁这个常量来预测同事的年龄,即[6, 6, 6]。每个同事年龄的残差 = 年龄 - 预测值 = [5, 6, 7] - [6, 6, 6],所以残差为[-1, 0, 1]

预测年龄的残差

为了让模型更加准确,其中一个思路是让残差变小。如何减少残差呢?我们不妨对残差建立一颗回归树,然后预测出准确的残差。假设这棵树预测的残差是[-0.9, 0, 0.9],将上一轮的预测值和这一轮的预测值求和,每个同事的年龄 = [6, 6, 6] + [-0.9, 0, 0.9] = [5.1, 6, 6.9],显然与真实值[5, 6, 7]更加接近了, 年龄的残差此时变为[-0.1, 0, 0.1],预测的准确性得到了提升。

GBDT

重新整理一下思路,假设我们的预测一共迭代3轮 年龄:[5, 6, 7]
第1轮预测:[6, 6, 6]
第2轮预测:[6, 6, 6] + [-0.9, 0, 0.9 ]= [5.1, 6, 6.9]
第2轮残差:[5, 6, 7] - [5.1, 6, 6.9] = [-0.1, 0, 0.1]
第3轮预测:[6, 6, 6] + [-0.9, 0, 0.9] + [-0.08, 0, 0.07 ]= [5.02, 6, 6.97]
第3轮残差:[5, 6, 7] - [5.02, 6, 6.97] = [-0.08, 0, 0.03]
看上去残差越来越小,而这种预测方式就是GBDT算法。

公式推导

GBDT(梯度提升树)基本原理及python实现_第1张图片
因此,我们需要通过用第m-1轮残差的均值来得到函数 f m ( x ) f_{m}(x) fm(x),进而优化函数 F m ( x ) F_{m}(x) Fm(x)。而回归树的原理就是通过最佳划分区域的均值来进行预测。所以 f m ( x ) f_{m}(x) fm(x)可以选用回归树作为基础模型,将初始值,m-1颗回归树的预测值相加便可以预测y。

GBDT的python实现

首先要实现构建CART树的代码,在实现GBDT代码时,要调用构建CART树的函数
数据集:在参考的GitHub链接里

CART回归树实现

import copy
import numpy as np


class Node:
    """
    树节点类
    """

    def __init__(self, feature=None, split_val=None, results=None, left=None, right=None):
        """
        :param feature: {int} 样本特征索引
        :param split_val: {float} 样本特征集划分值
        :param results: {float} 叶节点的值
        :param left: {Node} 左子树
        :param right:  {Node} 右子树
        """

        self.feature = feature
        self.split_val = split_val
        self.results = results
        self.left =  left
        self.right = right


def combine(X, Y):
    f"""
    :param X: {list} 样本特征
    :param Y: {list} 样本标签
    :return: data {list} 样本特征与标签集合
    """
    data = copy.deepcopy(X)
    for i in range(len(X)):
        data[i].append(Y[i])
    return data


def leaf(dataSet):
    """
    :param dataSet: {list}样本数据集
    :return: 标签平均值
    """

    data = np.array(dataSet)
    return np.mean(data[:, -1])


def err_cnt(dataSet):
    """
    :param dataSet: {ndarray} 样本数据集
    :return: 标签平方误差
    """
    return  np.var(dataSet[:, -1]) * np.shape(dataSet)[0]


def split_tree(data, feature, split_val):
    """
    将data按split_val划分为两个集合
    :param data: {ndarray} 样本集合
    :param feature: {int} 划分特征索引
    :param split_val: {float} 划分值
    :return: 左右两个数据集
    """

    set_L, set_R = [], []
    tmp_LX, tmp_LY, tmp_RX, tmp_RY = [], [], [], []
    for i in data:
        if data[feature] < split_val:
            tmp_LX.append(list(data[0:-1]))
            tmp_LY.append(list(data[-1]))
        else:
            tmp_RX.append(list(data[0:-1]))
            tmp_RY.append(list(data[-1]))
    set_L.append(tmp_LX)
    set_L.append(tmp_LY)
    set_R.append(tmp_RX)
    set_R.append(tmp_RY)
    return set_L, set_R


class CART_regression(object):
    """
    CART回归树算法类
    """

    def __init__(self,X, Y, min_sample, min_err):
        """
        :param X: 样本特征集
        :param Y: 样本标签集
        :param min_sample: 叶子节点最小样本数量
        :param min_err: 构造树结束的最小误差
        """

        self.X = X
        self.Y = Y
        self.min_sample = min_sample
        self.min_err = min_err

    def fit(self):
        """
        构造CART回归树
        :return: Root {Node} 根节点
        """

        data = combine(self.X, self.Y)
        data= np.array(data)
        # 初始化
        bestErr = err_cnt(data)
        # 最佳切分值
        bestCreteria = None
        # 切分集合
        bestSets = None
        # 如果data样本数少于最小样本数或者样本误差小于最小误差,生成叶子节点
        if len(data) <= self.min_sample or bestErr < self.min_err:
            return Node(results=leaf(data))

        val_feat = []
        for feat in range(len(data[0]) - 1):
            val_feat = np.unique(data[:, feat])
            for val in val_feat:
                set_L, set_R = split_tree(data, feat, val)
                comb_L = combine(set_L[0], set_L[1])
                comb_R = combine(set_R[0], set_R[1])
                err_now = err_cnt(comb_L) + err_cnt(comb_R)
                if len(comb_L) < 2 or len(comb_R) < 2:
                    continue
                if err_now < bestErr:
                    bestErr = err_now
                    bestCreteria = (feat, val)
                    bestSets = (set_L, set_R)

        if bestErr > self.min_err:
            left = CART_regression(bestSets[0][0], bestSets[0][1], self.min_sample, self.min_err)
            right = CART_regression(bestSets[1][0], bestSets[1][1], self.min_sample, self.min_err)
            return Node(feature=bestCreteria[0], split_val=bestCreteria[1], left=left, right=right)
        else:
            return Node(results=leaf(data))


def predicts(sample, tree):
    """
    预测样本的值
    :param sample: {list} 测试样本
    :param tree: {Node} 训练好的模型
    :return: {float} 预测值
    """

    # 如果是叶子节点
    if tree.results is not None:
        return tree.results
    # 非叶子节点
    else:
        val_sample = sample[tree.feature]
        branch = None
        if val_sample < tree.split_val:
            branch = tree.left
        else:
            branch = tree.right
    return Node(sample, branch)


def cal_err(Y_test, predicts):
    """
    :param Y_test: {list} 测试样本标签
    :param predicts: {list} 测试样本预测值
    :return:  error {float}均方误差
    """

    y_test = np.array(Y_test)
    pre_y = np.array(predicts)
    error = np.square(y_test - pre_y).sum() / len(Y_test)
    return error

GBDT实现

import CART_regression_tree
import numpy as np


def load_data(datafile):
    """
    加载数据
    :param datafile: 保存训练数据的文件
    :return: X:{list} 训练样本特征 Y:{list} 训练样本标签
    """

    X, Y = [], []
    f = open(datafile)
    for line in f.readlines():
        sample = []
        lines = line.strip().strip('\t')
        Y.append(lines[-1])
        for i in range(len(lines) - 1):
            sample.append(float(lines[i]))
        X.append(sample)
    return X, Y


class GBDT_RT(object):
    """
    GBDT回归算法类
    """
    def __init__(self):
        self.trees = None  # 存储生成的多棵回归树
        self.learn_rate = None  # 学习率
        self.init_val = None  # 初始值

    def get_init_val(self, y):
        """
        获得初始值
        :param y: {list}样本标签
        :return: {float}样本标签平均值
        """

        return sum(y) / len(y)

    def get_residuals(self, y, y_hat):
        """
        计算残差值
        :param y: {list}样本标签
        :param y_hat: {list}样本预测值
        :return: y_residuals {list}残差
        """

        y_residuals = []
        for i in range(len(y)):
            y_residuals.append(y[i] - y_hat[i])
        return y_residuals

    def fit(self, X, Y, n_estimates, learn_rate, min_sample, min_err):
        """
        训练GBDT模型
        :param X: {list}训练样本特征
        :param Y: {list}训练样本标签
        :param n_estimates: {int}设置GBDT中CART树的数量
        :param learn_rate: {float}学习率
        :param min_sample: {int}训练CART树时叶节点最小样本数量
        :param min_err: {float}训练CART树时生成叶节点的最小误差
        :return: trees: {list} GBDT中CART树的列表
        """

        self.trees = []
        # 获取初始值
        self.init_val = self.get_init_val(Y)
        n = len(Y)
        # 以样本标签的均值作为初始预测值
        y_hat = [self.init_val] * n
        # 生成初始残差
        y_residuals = self.get_residuals(Y, y_hat)

        self.learn_rate = learn_rate
        for k in range(n_estimates):
            # 拟合残差生成CART树
            tree = CART_regression_tree.CART_regression(X, y_residuals, min_sample, min_err)
            for i in range(len(X)):
                res_hat = CART_regression_tree.predict(X[i], tree)
                y_hat[i] += self.learn_rate * res_hat
            y_residuals = self.get_residuals(Y, y_hat)
            self.trees.append(tree)

    def GBDT_predict(self, X_test):
        """
        预测测试集样本
        :param X_test: {list}测试样本特征
        :return: predicts {list} 预测值
        """

        predicts = []
        for i in range(len(X_test)):
            pre_y = self.init_val
            for tree in self.trees:
                pre_y += CART_regression_tree.predict(X_test[i], tree)
            predicts.append(pre_y)
        return predicts

    def GBDT_cal_err(self, Y_test, predicts):
        """
        测试样本预测的均方误差
        :param Y_test: {list}测试样本标签
        :param predicts: {list}测试样本预测值
        :return: error: {list}均方误差
        """

        y_test = np.array(Y_test)
        pre_y = np.array(predicts)
        error = np.square(y_test - pre_y).sum() / len(Y_test)
        return error

参考

1.GBDT回归的原理及Python实现

2.GBDT原理-Gradient Boosting Decision Tree
3.基于GBDT的数据回归及python实现
3.https://github.com/shiluqiang/GBDT_regression

你可能感兴趣的:(推荐系统经典模型,决策树,机器学习,cart分类回归树)