本文将记录有关决策树的相关内容
决策树
树模型是在日常工作中使用频率最高的模型之一,因为其较好的模型效果与良好的可解释性经常作为baseline模型使用,在平时使用时经常使用sklearn库使用,最近遇到了一个单子需要完全手动实现决策树相关内容,在此将其记录下来,如果有同学在做相关内容请与我邮件联系zhaoliang.ml@outlook.com
在之前的中决策树blog,我已经简单的对决策树的思路作了总结,在此简单回顾下:
分类树
分类树的思路分为:0,判断是否到达终止条件(最大树深,叶子最少样本数,最大误差),1根据某种划分方法找到最佳划分特征,2根据这个特征将样本分割开,二叉树就是二分,多叉树就是多分,3对于分割之后的小样本集重复第一步。
不同的分类算法的区别就在于第一步中对于特征的划分方法不同:信息增益、信息增益率、gini指数,在第二步中id3\c4.5采用的是多分类的方法及对于每个特征的不同取值进行划分子数据集,对于cart来说是对于某个特征的某个值的“是否”来划分数据集,是个二分类,而且会构造出一个非常深的二叉树,所以cart过拟合是最严重的的
回归树
回归树其实也就是最小二乘拟合,通过不同的特征将样本空间划分成了不同的小块,对于每个小块的数据输出平均值。
剪枝
由于决策树是十分依赖于数据分布的,所以如果训练集和测试集分布不同会导致严重的过拟合,所以需要对过拟合的树进行剪枝。剪枝的意义在于将一个复杂的树变的相对简单,利用这个简答树进行分类。
Python实现
from math import log
from collections import defaultdict,Counter
from sklearn.model_selection import train_test_split
import sys
import matplotlib.pyplot as plt
# import tree
class DecisionTree(object):
'''
Python纯手工实现决策树,只是为了学习和更进一步的掌握决策树的内容
如果该代码有错误或者有做相关工作的同学欢迎与我沟通学习
Email:zhaoliang.ml@outlook.com
CSDN:https://blog.csdn.net/qq_22235017
GitHub:https://github.com/ZhaoLiang-GitHub
'''
def __init__(self,config):
self.config = config
def _get_data_num(self,data,index,feature):
# 该子树下有多少个训练数据
num = 0
for x in data:
if x[index] == feature:
num +=1
return num
def _get_leaf_num(self,tree):
# 该子树下有多少个叶子
num = 0
key_list = list(tree.keys())
for i in key_list:
try:
num+=self._get_leaf_num(tree[i])
except AttributeError: # 叶子的最后一个不是字典没有keys()
num += 1
return num
return num
def _get_error_value(self,tree,data):
next_features = list(tree.keys())
num = 0
for next_feature in next_features:
next_tree = tree[next_feature]
leaf_num = self._get_leaf_num(next_tree)
error_value = 0
for next_tree_key in list(next_tree.keys()):
error_value += self.clac_err(next_tree[next_tree_key],data,labels)
num += (float(leaf_num)/len(data)) * error_value
return num
def _leaf_max_label(self,tree):
# 得到该子树上的最多标签作为叶子标签
self.label_count = {
}
def __leaf_max_label(tree):
if self._is_tree(tree):
key = list(tree.keys())[0]
sub_tree = tree[key]
for i in list(sub_tree.keys()):
__leaf_max_label(sub_tree[i])
else:
if tree not in self.label_count.keys():
self.label_count[tree] = 1
else:
self.label_count[tree] += 1
__leaf_max_label(tree)
self.label_count = sorted(self.label_count.items(), key= lambda x:x[1], reverse=True) # 排序
return self.label_count[0][0]
def _is_tree(self,obj):
return isinstance(obj,dict)
def _classify(self,input_tree,data,labels) ->str :
# 对一条数据进行分类,得到最终的分类标签
if not self._is_tree(input_tree):
return input_tree
feature = list(input_tree.keys())[0]
firtdict = input_tree[feature]
features_index = labels.index(feature)
key = data[features_index]
try:
firtdict_or_value = firtdict[key]
except :
return firtdict[list(firtdict.keys())[0]]
if self._is_tree(firtdict_or_value):
label = self._classify(firtdict_or_value, data,labels)
else:
label = firtdict_or_value
return label
def _classify_for_cart(self,input_tree,data,labels) ->str :
'''CART决策树对一条数据进行分类'''
# 对一条数据进行分类,得到最终的分类标签
if not self._is_tree(input_tree):
return input_tree
feature = list(input_tree.keys())[0]
if feature in labels:
sub_tree = input_tree[feature]
sub_tree_feature = list(sub_tree.keys())[0]
features_index = labels.index(feature)
if data[features_index] == sub_tree_feature:
label = self._classify_for_cart(sub_tree[sub_tree_feature]['is'],data,labels)
else:
label = self._classify_for_cart(sub_tree[sub_tree_feature]['not is'],data,labels)
return label
def _calc_shannon_ent(self,dataset):
'''
计算输入数据集的信息熵
信息熵的计算方法为根据分类类别将数据分成X份,
shannon_ent = -count(x_i)/count(data)*log(count(x_i)/count(data),2)
:param dataset:
:return:
'''
num = len(dataset)
def zero():
return 0
labels_count = defaultdict(zero)
for feature in dataset:
currentLabel = feature[-1]
labels_count[currentLabel] += 1
shannon_ent = 0.0
for key in labels_count:
prob = float(labels_count[key]) / num
shannon_ent -= prob * log(prob, 2) # 在机器学习中底数常用2单位为比特,在通信中常用e为底单位是香农
return shannon_ent
def _split_data(self,dataset, axis, value):
'''
根据第axis个特征的value值将数据切割开
返回值是当axis列为value的所有行,不包含第axis列
'''
retdataset = []
for data_line in dataset:
if data_line[axis] == value:
reduceddata_line = data_line[:axis]
reduceddata_line.extend(data_line[axis + 1:])
retdataset.append(reduceddata_line)
return retdataset
def _split_data_for_cart(self,dataset, index, value):
'''
根据第axis个特征的value值将数据切割开
返回值是当axis列为value的所有行,不包含第axis列
'''
aa = [i[index] for i in dataset]
retdataset = []
for data_line in dataset:
if data_line[index] == value:
retdataset.append(data_line)
return retdataset
def _choose_best_feature_by_information_entirpy(self,dataset):
''' ID3 根据信息增益选择特征
:param dataset:
:return: 根据信息增益进行划分数据最佳特征的索引
'''
num_features = len(dataset[0]) - 1 # 最后一列是标签
base_entropy = self._calc_shannon_ent(dataset) # 计算得到基础的信息熵
best_information_entrop = 0.0 # 最佳信息增益率,信息增益率越大,则使用特征A划分数据获得纯度越高
best_feature_index = -1 # 最佳特征索引
for i in range(num_features):
feature_list = [example[i] for example in dataset]
unique_values = set(feature_list)
entropy_by_feature = 0.0 # 利用当前特征对数据进行划分的信息熵
for value in unique_values:
# 对于每个特征的每个取值分割数据集
subdataset = self._split_data(dataset, i, value) # 当前特征的
prob = len(subdataset) / float(len(dataset)) # 当前特征的这个值所占比例
# 信息增益的计算方法
#
#
# |D^v|
# Gain(D,A) = Ent(D) - \sum —————— Ent(D^v)
# |D|
# 其中Ent(D)就是数据固有的信息熵base_entropy,
# |D^v| 是根据特征A的第V个值的数据样本个数,|D|是样本总个数
# Ent(D^v) 是根据特征A的第V个值获得的数据的信息熵 entropy_by_feature
#
entropy_by_feature += prob * self._calc_shannon_ent(subdataset)
information_entrop = base_entropy - entropy_by_feature
# print((i, information_entrop))
if information_entrop > best_information_entrop:
best_information_entrop = information_entrop
best_feature_index = i
return best_feature_index
def _choose_best_feature_by_gain_ratio(self,dataset):
''' C4.5 根据信息增益率选择特征
:param dataset:
:return: 根据信息增益率进行划分数据最佳特征的索引
'''
num_features = len(dataset[0]) - 1 # 最后一列是标签
base_entropy = self._calc_shannon_ent(dataset) # 计算得到基础的信息熵
best_feature_index = -1 # 最佳特征索引
best_gain_ratio = 0.0
information_entropy_list = [] # 信息增益list
gain_ratio_list = [] # 信息增益率list
for i in range(num_features):
feature_list = [example[i] for example in dataset]
unique_values = set(feature_list)
entropy_by_feature = 0.0 # 利用当前特征对数据进行划分的信息熵
intrinsic_value = 0.0 # 当前特征的固有值
for value in unique_values:
# 对于每个特征的每个取值分割数据集
subdataset = self._split_data(dataset, i, value) # 当前特征的
prob = len(subdataset) / float(len(dataset)) # 当前特征的这个值所占比例
# 信息增益率的计算方法
# Gain(D,A) # 对于每个特征,计算利用该特征对数据进行划分的信息熵
# gain_ratio = -----------
# intrinsic_value
#
# |D^v|
# Gain(D,A) = Ent(D) - \sum —————— Ent(D^v)
# |D|
# intrinsic_value = \sum prob*log(prob,2)
# 其中Ent(D)就是数据固有的信息熵base_entropy,
# |D^v| 是根据特征A的第V个值的数据样本个数,|D|是样本总个数
# Ent(D^v) 是根据特征A的第V个值获得的数据的信息熵 entropy_by_feature
# 信息增益率要除以固有值,所以对于属性值较少的特征有倾向,
# C4.5算法不是直接选择信息增益率最高的,而是先选择信息增益高于平均值的特征,在从这些特征中找信息增益率高的
entropy_by_feature += prob * self._calc_shannon_ent(subdataset)
intrinsic_value -= prob * log(prob, 2)
if (intrinsic_value == 0):
continue
gain_ratio = (base_entropy - entropy_by_feature) / intrinsic_value
information_entropy_list.append(base_entropy - entropy_by_feature)
gain_ratio_list.append(gain_ratio)
try:
mean_information_entropy = sum(information_entropy_list)/len(information_entropy_list)
except :
return -1
for index,value in enumerate(information_entropy_list):
if value > mean_information_entropy and gain_ratio_list[index] > best_gain_ratio:
best_gain_ratio = gain_ratio_list[index]
best_feature_index = index
return best_feature_index
def _count_most_label(self,label_list)->str:
'''
统计输入的所有标签中数量最多的一个标签
:param label_list:
:return:
'''
def zero():
return 0
label_count = defaultdict(zero())
for vote in label_list:
label_count[vote] += 1
label_count = sorted(label_count.items(), key=lambda x:x[1], reverse=True)
return label_count[0][0]
def _calc_ginii(self,dataSet):
'''
计算基尼指数
:param dataSet:数据集
:return: 计算结果
'''
data_len = len(dataSet)
labelCounts = {
}
for featVec in dataSet: # 遍历每个实例,统计标签的频数
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts