决策树
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.0for 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 = 0for key in feature_class.keys():weight = feature_class[key]/data.shape[0]Ent_v = entropy(data.loc[data[a]==key])gain += weight * Ent_vreturn 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=1V∣Dv∣=∣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_valueIV_a = 0feature_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_areturn 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 = 0for k in label_num.keys():p_k = label_num[k]/len(data_label)res += p_k ** 2return 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_dataE = E + p*Ent_D_v gain = Ent_D - E Gain.append(gain)best_entropy = 0best_dimension = 0for 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.0for j in range(len_split):Ent_D_v = entropy(split_label[j] )len_D_v = len(split_label[j])p = len_D_v/len_dataE = E + p*Ent_D_vgrain = Ent_D - EEnt_f = entropy(feature[:,i])grain_ratio = grain / Ent_f#计算信息增益率Grain_ratio.append(grain_ratio)#通过比较得到最好的信息增益率以及维度best_entropy = 0best_dimension = 0for 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.0best_dimension = 10000best_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 = 0count2 = 0count3 = 0count4 = 0 #对二分类问题中的不同情况进行计数for k in range(len_data):if feature[k,i] == j:count1 = count1 + 1count2 = count2 + label[k]if feature[k,i] != j:count3 = count3 +1count4 = count4 + label[k] p1 = count2/count1p2 = 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_2Gini_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_valuereturn 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 += 1return cnt / len(test_label)
#用于预测节点剪枝后的预测正确数
def equalNums(label, featPreLabel):res = 0for l in label:if l == featPreLabel:res += 1return 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].valuesnewTree[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