python实现了一个决策树分类器,支持离散和连续特征

# encoding: utf-8

from utils import *
from DecisionNode import *
import numpy as np
from sklearn.model_selection import train_test_split


class Tree:

    def __init__(self, min_samples_leaf=5, partition_rate=1, B1=5, B2=5, B3=None):
        self.root = None
        self.min_samples_leaf = min_samples_leaf
        self.features_attr = None
        self.b_1 = B1/2
        self.b_2 = B2/2
        self.b_3 = B3/2 if B3 != None else B3
        self.partition_rate = partition_rate
        self.criterion = cal_gini

        assert self.b_1 >= 0
        assert self.b_2 >= 0
        assert self.b_3 == None or self.b_3 >= 0

    def fit(self, X, y, features_attr=None):
        assert len(features_attr) == X.shape[1]

        np.random.seed()

        self.features_attr = features_attr

        X_e, X_s, y_e, y_s = train_test_split(X, y, test_size=self.partition_rate/(self.partition_rate+1))
        structure_points = np.concatenate((np.array(X_s), np.array([y_s]).T), axis=1)
        estimation_points = np.concatenate((np.array(X_e), np.array([y_e]).T), axis=1)

        self.root = self.__build_tree(structure_points, estimation_points)

    def predict(self, X):

        if np.ndim(X) == 1:
            return self.__predict_rec(X, self.root)
        else:
            result = []
            for sample in X:
                result.append(self.__predict_rec(sample, self.root))
            return result

    def __predict_rec(self, X, node):

        global feature, threshold
        if node.feature != -1:
            feature = node.feature
            threshold = node.threshold

        if node.label is not None:
            classLabel = feature, threshold, node.label
            return classLabel
        else:
            feat_value = X[node.feature]
            feat_attr = self.features_attr[node.feature]
            threshold = node.threshold

            if feat_value is None or feat_value is np.nan:
                choice = np.random.randint(1, 3)
                if choice == 1:
                    return self.__predict_rec(X, node.true_branch)
                else:
                    return self.__predict_rec(X, node.false_branch)
            else:
                if feat_attr == 'd':
                    if feat_value == threshold:
                        return self.__predict_rec(X, node.true_branch)
                    else:
                        return self.__predict_rec(X, node.false_branch)
                elif feat_attr == 'c':
                    if feat_value >= threshold:
                        return self.__predict_rec(X, node.true_branch)
                    else:
                        return self.__predict_rec(X, node.false_branch)

    def __split(self, dataset, split_feature, threshold):

        true_index = []
        false_index = []

        if self.features_attr[split_feature] == 'd':
            for i in range(len(dataset)):
                if dataset[i][split_feature] == threshold:
                    true_index.append(i)
                else:
                    false_index.append(i)
        elif self.features_attr[split_feature] == 'c':
            for i in range(len(dataset)):
                if dataset[i][split_feature] >= threshold:
                    true_index.append(i)
                else:
                    false_index.append(i)

        return true_index, false_index

    def __split_pair(self, dataset, candidate_features):

        current = self.criterion(dataset[:, -1])

        ret = {}

        for feat in candidate_features:
            col = dataset[:, feat]
            unique_col = np.unique(col)
            attr = self.features_attr[feat]
            ret[feat] = []

            threshold_list = []
            if attr == 'd' or unique_col.shape == 1:
                threshold_list = unique_col
            elif attr == 'c':
                threshold_list = [(unique_col[i]+unique_col[i+1]) / 2 for i in range(len(unique_col)-1)]

            for t in threshold_list:
                true_index, false_index = self.__split(dataset, feat, t)

                p = float(len(true_index)) / len(dataset)
                gain = current - p * self.criterion(dataset[true_index, -1]) - \
                       (1-p) * self.criterion(dataset[false_index, -1])

                ret[feat].append([gain, t])
            ret[feat] = np.array(ret[feat])
            ret[feat] = ret[feat][np.argsort(-ret[feat][:, 0])]

        return ret

    def __build_tree(self, structure_points, estimation_points):

        if len(cal_label_dic(structure_points[:, -1])) == 1:
            return DecisionNode(label=voting(cal_label_dic(estimation_points[:, -1])))

        candidate_features = []
        for i in range(structure_points.shape[1]-1):
            if len(np.unique(structure_points[:, i])) > 1:
                candidate_features.append(i)
        if candidate_features == []:
            return DecisionNode(label=voting(cal_label_dic(estimation_points[:, -1])))

        info_gain_dict = self.__split_pair(structure_points, candidate_features)

        info_gain_feat_max = []
        for key, val in info_gain_dict.items():
            info_gain_feat_max.append([key, val[0][0]])
        info_gain_feat_max = np.array(info_gain_feat_max)
        info_gain_feat_max = info_gain_feat_max[np.argsort(-info_gain_feat_max[:, 1])]

        info_gain_feat_max_norm = self.b_1 * max_min_normalization(info_gain_feat_max[:, 1])
        split_feature = int(info_gain_feat_max[mutinomial(info_gain_feat_max_norm)][0])

        info_gain_chosen_feat_norm = self.b_2 * max_min_normalization(info_gain_dict[split_feature][:, 0])
        threshold = info_gain_dict[split_feature][mutinomial(info_gain_chosen_feat_norm)][1]

        true_index_s, false_index_s = self.__split(structure_points, split_feature, threshold)
        true_index_e, false_index_e = self.__split(estimation_points, split_feature, threshold)

        if len(true_index_e) == 0 or len(false_index_e) == 0:
            return DecisionNode(label=voting(cal_label_dic(estimation_points[:, -1]), self.b_3))

        if len(true_index_e) <= self.min_samples_leaf:
            true_branch = DecisionNode(label=voting(cal_label_dic(estimation_points[true_index_e, -1])))
        else:
            true_branch = self.__build_tree(structure_points[true_index_s], estimation_points[true_index_e])

        if len(false_index_e) <= self.min_samples_leaf:
            false_branch = DecisionNode(label=voting(cal_label_dic(estimation_points[false_index_e, -1])))
        else:
            false_branch = self.__build_tree(structure_points[false_index_s], estimation_points[false_index_e])

        return DecisionNode(feature=split_feature, threshold=threshold,
                            true_branch=true_branch, false_branch=false_branch)

这段代码定义了一个名为 Tree 的类,用于构建和使用决策树进行分类任务。下面详细介绍该类实现的具体功能:

1. 类的初始化

def __init__(self, min_samples_leaf=5, partition_rate=1, B1=5, B2=5, B3=None):
    ...
  • 初始化决策树的一些参数:
    • min_samples_leaf:叶子节点所需的最小样本数,默认为 5。
    • partition_rate:用于将数据集划分为结构点集和估计点集的比例,默认为 1。
    • B1B2B3:用于归一化和决策的参数,其中 B3 可以为 None
  • 初始化根节点 self.rootNone,特征属性 self.features_attrNone,并设置分裂准则 self.criterioncal_gini 函数(该函数在代码中未给出,但推测是用于计算基尼不纯度)。
  • 对参数 b_1b_2b_3 进行断言检查,确保它们的值非负。

2. 模型拟合方法

def fit(self, X, y, features_attr=None):
    ...
  • 检查 features_attr 的长度是否与特征矩阵 X 的列数相等。
  • 设置随机种子,以确保结果的可重复性。
  • 存储特征属性 features_attr
  • 使用 train_test_split 函数将数据集 (X, y) 划分为结构点集 (X_s, y_s) 和估计点集 (X_e, y_e),划分比例由 partition_rate 决定。
  • 将结构点集和估计点集分别合并为 structure_pointsestimation_points
  • 调用 __build_tree 方法递归地构建决策树,并将根节点赋值给 self.root

3. 预测方法

def predict(self, X):
    ...
  • 处理输入数据 X 的维度:
    • 如果 X 是一维数组,则直接调用 __predict_rec 方法进行预测。
    • 如果 X 是二维数组,则对每个样本调用 __predict_rec 方法进行预测,并将结果存储在列表中返回。
def __predict_rec(self, X, node):
    ...
  • 递归地对样本 X 进行预测:
    • 如果当前节点有标签,则返回该节点的特征、阈值和标签。
    • 如果样本的特征值缺失(为 Nonenp.nan),则随机选择一个子节点继续递归预测。
    • 根据特征的属性(离散 'd' 或连续 'c')和阈值,选择合适的子节点继续递归预测。

4. 数据划分方法

def __split(self, dataset, split_feature, threshold):
    ...
  • 根据指定的特征 split_feature 和阈值 threshold,将数据集 dataset 划分为满足条件的 true_index 和不满足条件的 false_index
  • 对于离散特征,比较特征值是否等于阈值;对于连续特征,比较特征值是否大于等于阈值。
def __split_pair(self, dataset, candidate_features):
    ...
  • 计算每个候选特征在不同阈值下的信息增益:
    • 首先计算当前数据集的基尼不纯度 current
    • 对于每个候选特征,找出其所有可能的阈值。
    • 对于每个阈值,调用 __split 方法将数据集划分为两部分,并计算信息增益。
    • 将每个特征的信息增益和对应的阈值存储在字典 ret 中,并按信息增益降序排序。

5. 决策树构建方法

def __build_tree(self, structure_points, estimation_points):
    ...
  • 递归地构建决策树:
    • 如果结构点集的标签只有一种,则返回一个叶子节点,标签为估计点集的多数标签。
    • 找出所有可能的候选特征。
    • 如果没有候选特征,则返回一个叶子节点,标签为估计点集的多数标签。
    • 调用 __split_pair 方法计算每个候选特征的信息增益。
    • 对信息增益进行归一化处理,并根据 B1B2 参数选择最佳分裂特征和阈值。
    • 根据最佳分裂特征和阈值将结构点集和估计点集分别划分为两部分。
    • 如果某一部分的样本数为 0 或小于等于 min_samples_leaf,则返回一个叶子节点,标签为该部分的多数标签。
    • 递归地构建左右子树,并返回一个决策节点。

总结

Tree 类实现了一个决策树分类器,支持离散和连续特征。在构建决策树时,使用基尼不纯度作为分裂准则,并通过信息增益选择最佳分裂特征和阈值。同时,该类支持处理缺失值,并可以根据用户指定的参数进行模型的训练和预测。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值