机器学习-决策树【手撕】

决策树

1、概述

决策树是一个预测模型,它代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。

2、相关概念

在决策树流程学习中,寻找最优划分属性是决策树过程中的重点。那么如何在众多的属性中选择最优的呢

2.1 信息熵

Ent = - \sum_{k=1}^{K} p_k * log_2p_k
import collections
def entropy(label):
    counter = collections.Counter(label)
    ent = 0.0
    for num in counter.values():
        p = num/len(label)
        ent += -p*np.log2(p)
    return ent

2.2 信息增益

def information_gain(data,a):
    Ent = entropy(data)
    feature_class = data[a].value_counts()
    gain = 0
    for key in feature_class.keys():
        weight = feature_class[key]/data.shape[0]
        Ent_v = entropy(data.loc[data[a]==key])
        gain += weight * Ent_v
    return Ent - gain

2.3 信息增益率

信息增益率计算公式如下所示:
某数据集 D D D有若干属性值以及对应的值标记值,其样本大小为 ∣ D ∣ |D| D,其中取一个属性特征为feature,该属性包含 V V V个不同的取值,属性值为第 v v v个值的大小为 ∣ D v ∣ ( ∑ v = 1 V ∣ D v ∣ = ∣ D ∣ ) |D_v|(\sum_{v=1}^{V} |D^v| = |D|) Dv(v=1VDv=D),该属性的信息熵为 E n t ( f e a t u r e ) Ent(feature) Ent(feature),则该属性对应的信息增益率为

Gain_ratio(D,feature) = \frac{Gain(D,feature)} {Ent(feature)}

def gain_ratio(data , a):
    #先计算固有值intrinsic_value
    IV_a = 0
    feature_class = data[a].value_counts()  # 特征有多少种可能
    for v in feature_class.keys():
        weight = feature_class[v]/data.shape[0]
        IV_a += -weight*np.log2(weight)
    gain_ration = cal_information_gain(data,a)/IV_a
    return gain_ration

2.4 基尼系数

gini系数计算方式如下:
某数据集 D D D有若干属性值以及对应的标记值,其总样本大小为 ∣ D ∣ |D| D,该集合中样本标记类别总共有 K K K类,第 k k k类样本所占比例为 p k ( k = 1 , 2 , … , K p_k (k=1,2,…,K pk(k=1,2,,K)则该数据集对应的基尼系数为:

Gini(D) = 1 - \sum_{k=1}^Kp_{k}^2
#计算基尼指数
def gini(data):
    data_label = data.iloc[:, -1]
    label_num = data_label.value_counts() #有几类,每一类的数量
    res = 0
    for k in label_num.keys():
        p_k = label_num[k]/len(data_label)
        res += p_k ** 2
    return 1 - res

而取其中一个属性类型为 f e a t u r e feature feature,选定该类型的一个值为 v a l u e value value,根据 f e a t u r e feature feature这个属性是否为 v a l u e value value将数据集分为两个子集 D 1 D_1 D1 D 2 D_2 D2,则该属性的 f e a t u r e feature feature对应的 G i n i Gini Gini系数为:

Gini\_index(D,feature) = \frac{|D_1|}{|D|}Gini(D_1) +\frac{|D_2|}{|D|}Gini(D_2)

所以该函数中需要首先排除每一列中存在的重复值,计算出该值进行分类的Gini系数,计算出每个属性的每个非重复值的基尼系数,之后找到最小的Gini系数对应的属性维度以及对应值。

# 计算每个特征取值的基尼指数,找出最优切分点
def gini_index(data,a):
    feature_class = data[a].value_counts()
    res = []
    for feature in feature_class.keys():
        weight = feature_class[feature]/len(data)
        gini_value = gini(data.loc[data[a] == feature])
        res.append([feature, weight * gini_value])
    res = sorted(res, key = lambda x: x[-1])
    return res[0]

3、决策树

数据进行划分。

def split(feature, label, dimension):
    Len_label = len(label)#数据的长度
    a_d = np.unique(feature[:,dimension])#指定维度下的不同样本
    Len_a = len(a_d)#指定维度下的样本长度
    Feature = []
    Label = []
    split_feature = []
    split_label = []
    #初始化列表
    for j in range(Len_a):
        Feature.append(feature)
        Label.append(label)
    #对不同的a进行分类
    for j in range(Len_a):
        #从后往前判定,并删除不是该特征的数据
        for i in range(Len_label-1, -1, -1):
            if feature[i,dimension] != a_d[j]:
                Feature[j] = np.delete(Feature[j],i,axis=0)
                Label[j] = np.delete(Label[j],i,axis=0)
    #将输出结果整合为一个列表
    for j in range(Len_a):
        split_feature.append(Feature[j])
        split_label.append(Label[j])
    return split_feature,split_label

3.1 ID3决策树

通过寻找特征集合中信息增益最大属性,并进行分支。
输入:特征集合,标记集合
输出:该次划分的最佳信息增益,最佳划分维度S
计算信息增益公式:
某数据集 D D D有若干特征值以及对应的标记值,其总样本大小为 ∣ D ∣ |D| D,这里取一个属性类型 f e a t u r e feature feature,该特征包含 V V V个不同的取值,特征值为第 v v v个值的数量为 ∣ D v ∣ |D^v| Dv,则该特征值对应的信息增益为:

Gain(G,feature) = Ent(D) - \sum_{v=1}^K \frac{|D^v|}{D}Ent(D^v)

所以需要计算出每个特征的信息增益并输出,然后返回最大的信息增益并返回该特征的维数以及最大的信息增益值。

def one_split_ID3(feature,label):
    Ent_D = entropy(label)
    len_data, len_dimension = np.shape(feature)
    Gain = []
    for i in range(len_dimension):
        split_feature,split_label = split(feature,label,i) 
        len_split = len(split_label)
        E = 0.0 
        for j in range(len_split):
            Ent_D_v = entropy(split_label[j])
            len_D_v = len(split_label[j])
            p = len_D_v/len_data
            E = E + p*Ent_D_v 
        gain = Ent_D - E 
        Gain.append(gain)
    best_entropy = 0
    best_dimension = 0
    for i in range(len_dimension):
        if Gain[i] > best_entropy:
            best_entropy = Gain[i]
            best_dimension = i 
    return best_entropy, best_dimension

3.2 C4.5决策树

与ID3决策树不同,通过遍历找出该属性集合中信息增益率,最大的属性,进行节点分支。

#输出每个特征的信息增益率,之后返回最大的信息增益率对应的属性维数以及最大的信息增益率
def one_split_C4_5(feature, label):
    #计算区域D的信息熵
    Ent_D = entropy(label)
    len_data,len_dimension = np.shape(feature)
    Grain_ratio = []
    #求出不同维度分类下的信息增益率
    for i in range(len_dimension):
        split_feature,split_label = split(feature, label, i)
        len_split = len(split_label)
        E = 0.0
        for j  in range(len_split):
            Ent_D_v = entropy(split_label[j] )
            len_D_v = len(split_label[j])
            p = len_D_v/len_data
            E = E + p*Ent_D_v
       
        grain = Ent_D - E
        Ent_f = entropy(feature[:,i])
        grain_ratio = grain / Ent_f#计算信息增益率
        Grain_ratio.append(grain_ratio)
   
    #通过比较得到最好的信息增益率以及维度
    best_entropy = 0
    best_dimension = 0
    for i in range(len_dimension):
       
        if Grain_ratio[i] > best_entropy:
            best_entropy = Grain_ratio[i]
            best_dimension = i  
           
    return best_entropy, best_dimension

3.3 CART决策树

CART决策树的结点划分,是遍历找出该属性集合中基尼系数(使用CART算法中的公式计算)最小的属性以及最佳的划分值

#找到最小的基尼系数对应的属性维数以及对应的分类值
def one_split_CART(feature, label):
    
    len_data,len_dimension = np.shape(feature)
    #初始化所需要求的三个值
    best_entropy = 10000.0
    best_dimension = 10000
    best_value =10000    
    for i in range(len_dimension):
        split_feature,split_label = split(feature, label, i)
        unique_f = np.unique(feature[:,i])
        len_uni = len(unique_f)      
        Gini_index = []       
        for j in unique_f:
            #初始化所用到的计数器
            count1 = 0
            count2 = 0
            count3 = 0
            count4 = 0           
            #对二分类问题中的不同情况进行计数
            for k in range(len_data):
                if feature[k,i] == j:
                    count1 = count1 + 1
                    count2 = count2 + label[k]
                if feature[k,i] != j:
                    count3 = count3 +1
                    count4 = count4 + label[k]           
            p1 = count2/count1
            p2 = count4/count3
            #实现基尼系数的计算公式
            gini_D_1 = (count1/len_data) * ((1-p1)*(1-p1) +p1* p1)
            gini_D_2 = (count3/len_data) * ((1-p2)*(1-p2) +p2* p2)
            gini_index = gini_D_1 + gini_D_2
            Gini_index.append(gini_index)                      
        #通过比较找到最好的维度以及对应的分类值    
        for d in range(len_uni):
            if Gini_index[d] < best_entropy:
                best_entropy = Gini_index[d]
                best_dimension = i#对应的维度值i即为最佳维度
                best_value = unique_f[d]#此d对应的分类值即为最佳分类值           
    return best_entropy,best_dimension,best_value

决策树剪枝

剪枝分为预剪枝和后剪枝,预剪枝指在决策树生成过程中对每个节点先进行估计,如果划分能够带来准确率的上升,则进行划分,否则就不划分该节点;后剪枝则是在先使用训练集生成决策树,再对每个节点进行评估,若将其中节点替换能够提升决策树准确率,则替换,否则则不替换。
一般情况下,后剪枝的欠拟合风险小,泛化能力强于预剪枝。所以,我们这边选择实现后剪枝方法。

import numpy as np
import pandas as pd
### 将数据进行转换
def drop_exist_feature(data,best_feature):
    attr = pd.unique(data[best_feature])
    new_data = [(nd,data[best_feature] == nd) for nd in attr]
    new_data = [(n[0],n[1].drop([best_feature],axis=1)) for n in new_data]
    return new_data
# 预测单条数据
def predict(Tree,test_data):
    first_feature = list(Tree.keys())[0]
    second_dict = Tree[first_feature]
    input_first = test_data.get(first_feature)
    input_value = second_dict[input_first]
    if is_dict(input_value,test_data): ## 判断是分支还是字典
        class_label = predict(input_value,test_data)
    else:
        class_label = input_value
    return class_label
#测试很多案例,话返回准确率
def predict_more(Tree, test_data, test_label):
    cnt = 0
    #计算如果该节点不剪枝的准确率
    for i in range(len(test_data)):
        after_data = test_data.reset_index().loc[i].to_dict()
        pred = predict(Tree,  after_data)
        if pred == test_label[i]:
            cnt += 1
    return cnt / len(test_label)
#用于预测节点剪枝后的预测正确数
def equalNums(label, featPreLabel):
    res = 0
    for l in label:
        if l == featPreLabel:
            res += 1
    return res
# 后剪枝
def post_purnning(tree,test_data,test_label,names):
    newTree = tree.copy()
    names = np.asarray(names)
    # 取决策点的名称,即特征名称
    feature_name = list(tree.keys())[0]
    # 取特征的列
    featcol =  np.argwhere(names==feature_name)[0][0]
    names = np.delet(names,[featcol])
    newTree[feature_name] = tree[feature_name].copy()
    feature_value_dict = newTree[feature_name]
    feature_pre_label = feature_value_dict.pop('prun_label')
   
    # 分割测试数据,如果有数据的话,则进行测试或者递归调用
    split_data = drop_exist_feature(test_data,feature_name)
    split_data = dict(split_data)
   
    for feature_value in feature_value_dict.keys():
        if type(feature_value_dict[feature_value]) == dict:
            split_data_feature = split_data[feature_value]
            split_data_label = split_data[feature_value].iloc[:,-1].values
            newTree[feature_name][feature_value] = post_purnning(   feature_value_dict[feature_value_dict],split_data_feature,split_data_label,split_data_feature.columns)
   
    ratio_PreDivision = equalNums(test_label, feature_pre_label) / test_label.size
        #计算如果该节点不剪枝的准确率
    ratioAfterDivision = predict_more(newTree, test_data, test_label)
    if ratioAfterDivision < ratio_PreDivision:
        newTree = feature_pre_label # 返回剪枝结果,其实也就是走到当前节点的数据最多的那一类
    return newTree

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

alstonlou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值