1、决策树
1.1 什么是决策树
有监督机器学习算法中的一类经典算法,是最经常使用的数据挖掘算法。
它是一种典型的分类方法,首先对数据进行处理,利用归纳算法生成可读的规则和决策树,然后使用决策对新数据进行分析。本质上决策树是通过一系列规则对数据进行分类的过程。 典型算法有ID3,C4.5,CART等>
1.2 和knn算法的区别
k-近邻算法的缺点是无法给出数据的内在含义
决策树:
- 优点:计算复杂度不高,输出结果易于理解,对中间值的确实不敏感,可以处理并不相关特征数据
- 缺点:可能会产生过度匹配问题
- 使用数据类型:数值型,标称型
1.3 伪代码
检测数据集中每个子项是否属于同一分类
IF so return 类标签
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return 分支节点
2、决策树的构造流程
- 收集数据
- 准备数据
- 分析数据
- 训练数据
- 测试算法
- 使用算法
决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树
3、属性划分方法
3.1 信息增益(ID3)
信息增益: 划分数据集之前之后信息发生的变化
划分数据集的原则:将无序的数据变得更加有序
ID3(Iterative Dichotomiser,迭代二分器)决策树学习算法[Quinlan, 1986]以信息增益为准则来选择划分属性。
3.1.1 信息熵
“信息熵”是度量样本集合纯度最常用的一种指标
假定,当前样本集合D中第k类样本所占的比例为 pk (K=1, 2, …, |y|);则D的信息熵增益为:
- Ent(D)的值越小,则D的纯度越高
- 计算信息熵时约定:若p = 0,则plog2p=0
- Ent(D)的最小值为0,最大值为log2|y|
python代码计算给定数据集的信息熵:
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']
return dataSet,labels
def calcShannonEnt(dataSet):
numEntris = len(dataSet)
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] +=1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntris
shannonEnt -= prob * log(prob,2)
return shannonEnt
>>> d,l=trees.createDataSet()
>d
[[1, 1, 'yes'], [1, 1, 'yes'],
[1, 0, 'no'], [0, 1, 'no'],
[0, 1, 'no']]
>>> trees.calcShannonEnt(d)
0.9709505944546686
3.1.2 信息增益
信息增益越大,则意味着使用属性a来进行划分所获得的“纯度提升”越大
信息增益对可取值数目较多的属性有所偏好
3.2 信息增益率(C4.5)
缺点: 对取值数目较少的属性有所偏好
代码实现:
def create_tree_C45(self,dataset,feat_labels):
#print("本节点特征",labels)
classList=list(dataset['label'])#获得类别列表
#若是所有样本属于同一类别,则返回这一类别做节点标记,停止划分
if classList.count(classList[0])==len(classList):
#print("该节点上所有样本为同一类")
return classList[0]
#若特征集为空,则返回数据集中样本数最多的类作为节点标记,停止划分
if len(dataset.iloc[0])==1:
#print("特征集为空")
return self.majorityCnt(classList)
bestFeatLabel,bestFeatIndex=self.choose_best_feature_C45(dataset,feat_labels) #选择最优特征
print("最佳划分特征(createtree)",bestFeatLabel,bestFeatIndex)
myTree={bestFeatLabel:{}} #分类结果以字典形式保存
#如果最佳划分特征不为空则继续划分
if(bestFeatLabel!=''):
del(feat_labels[bestFeatIndex])
featValues=dataset[bestFeatLabel] #最好划分特征的所有取值
#print(featValues)
uniqueVals=set(featValues)
for value in uniqueVals:
subLabels=feat_labels[:]
#print(bestFeatLabel,"取值为:",value)
values_head=subLabels
newdataset=self.split_dataset(dataset,bestFeatLabel,bestFeatIndex,value,values_head)
myTree[bestFeatLabel][value]=self.create_tree_C45(newdataset,subLabels)
return myTree
3.3 基尼指数(CART)
3.3.1 求解基尼指数
import numpy as np
def calcGini(data_y): #根据基尼指数的定义,根据当前数据集中不同标签类出现次数,获取当前数据集D的基尼指数
m = data_y.size #获取全部数据数量
labels = np.unique(data_y) #获取所有标签值类别(去重后)
gini = 1.0 #初始基尼系数
for i in labels: #遍历每一个标签值种类
y_cnt = data_y[np.where(data_y==i)].size / m #出现概率
gini -= y_cnt**2 #基尼指数
return gini
3.3.2 实现数据集切分
def splitDataSet(data_X,data_Y,fea_axis,fea_val): #根据特征、和该特征下的特征值种类,实现切分数据集和标签
#根据伪算法可以知道,我们要将数据集划分为2部分:特征值=a和特征值不等于a
eqIdx = np.where(data_X[:,fea_axis]==fea_val)
neqIdx = np.where(data_X[:,fea_axis]!=fea_val)
return data_X[eqIdx],data_Y[eqIdx],data_X[neqIdx],data_Y[neqIdx]
3.3.3 选取最优特征和特征值划分
def chooseBestFeature(data_X,data_Y): #遍历所有特征和特征值,选取最优划分
m,n = data_X.shape
bestFeature = -1
bestFeaVal = -1
minFeaGini = np.inf
for i in range(n): #遍历所有特征
fea_cls = np.unique(data_X[:,i]) #获取该特征下的所有特征值
# print("{}---".format(fea_cls))
for j in fea_cls: #遍历所有特征值
newEqDataX,newEqDataY,newNeqDataX,newNeqDataY=splitDataSet(data_X,data_Y,i,j) #进行数据集切分
feaGini = 0 #计算基尼指数
feaGini += newEqDataY.size/m*calcGini(newEqDataY) + newNeqDataY.size/m*calcGini(newNeqDataY)
if feaGini < minFeaGini:
bestFeature = i
bestFeaVal = j
minFeaGini = feaGini
return bestFeature,bestFeaVal #返回最优划分方式
3.3.4 创建树
代码实现:
def createTree(data_X,data_Y,fea_idx): #创建决策树
y_labels = np.unique(data_Y)
#1.如果数据集中,所有实例都属于同一类,则返回
if y_labels.size == 1:
return data_Y[0]
#2.如果特征集为空,表示遍历了所有特征,使用多数投票进行决定
if data_X.shape[1] == 0:
bestFea,bestCnt = 0,0
for i in y_labels:
cnt = data_Y[np.where(data_Y==i)].size
if cnt > bestCnt:
bestFea = i
bestCnt = cnt
return bestFea
#按照基尼指数,选择特征,进行继续递归创建树
bestFeature, bestFeaVal = chooseBestFeature(data_X,data_Y)
# print(bestFeature,bestFeaVal)
feaBestIdx = fea_idx[bestFeature]
my_tree = {feaBestIdx:{}}
#获取划分结果
newEqDataX,newEqDataY,newNeqDataX,newNeqDataY = splitDataSet(data_X,data_Y,bestFeature,bestFeaVal)
#删除我们选择的最优特征
newEqDataX = np.delete(newEqDataX,bestFeature,1)
newNeqDataX = np.delete(newNeqDataX,bestFeature,1)
fea_idx = np.delete(fea_idx,bestFeature,0)
my_tree[feaBestIdx]["{}_{}".format(1,bestFeaVal)] = createTree(newEqDataX,newEqDataY,fea_idx)
my_tree[feaBestIdx]["{}_{}".format(0,bestFeaVal)] = createTree(newNeqDataX,newNeqDataY,fea_idx)
return my_tree
3.4决策树
3.4.1 创建树的代码
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
3.4.2 使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
key = testVec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
3.4.3 存储决策树
def storeTree(inputTree,filename):
import pickle
fw = open(filename,'w')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)
4、剪枝处理
4.1 剪枝原因
“剪枝”是决策树学习算法对付“过拟合”的主要手段,因此,可通过“剪枝”来一定程度避免因决策分支过多,以致于把训练集自身的一些特点当做所有数据都具有的一般性质而导致的过拟合。
4.2 剪枝的基本策略
预剪枝、后剪枝
4.3 剪枝后效果如何判断
判断决策树泛化性能是否提升的方法采用留出法,即预留一部分数据用作“验证集”以进行性能评估
4.4 预剪枝
通过提前停止树的构建而对树剪枝,主要方法有:
1.当决策树达到预设的高度时就停止决策树的生长
2. 达到某个节点的实例集具有相同的特征向量(属性取值相同),即使这些实例不属于同一类,也可以停止决策树的生长。3. 定义一个阈值,当达到某个节点的实例个数小于阈值时就可以停止决策树的生长。
4. 通过计算每次扩张对系统性能的增益,决定是否停止决策树的生长。
(上述不足:阈值属于超参数,很难找到过拟合--欠拟合的trade-off。)
例:
优点:
–降低过拟合风险
–显著减少训练时间和测试时间开销。
缺点:
–欠拟合风险:有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于“贪心”本质禁止这些分支展开,带来了欠拟合险。
4.5 后剪枝
优点 :后剪枝比预剪枝保留了更多的分支,欠拟合风险小 ,泛化性能往往优于预剪枝决策树
缺点:训练时间开销大,后剪枝过程是在生成完全决策树 之后进行的,需要自底向上对所有非叶结点逐一计算
比较预剪枝和后剪枝,后剪枝保留的分支更多,同时后剪枝的欠拟合的风险很小,泛化性能往往优于预剪枝决策树,但是显而易见,训练的时间要比预剪枝大得多。
4.6 代码实现
import pandas as pd
import numpy as np
##将数据转化为(属性值:数据)的元组形式返回,并删除之前的特征列
def drop_exist_feature(data, best_feature):
attr = pd.unique(data[best_feature])
new_data = [(nd, data[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 isinstance(input_value , dict): #判断分支还是不是字典
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_prunning(tree , test_data , test_label , names):
newTree = tree.copy() #copy是浅拷贝
names = np.asarray(names)
# 取决策节点的名称 即特征的名称
featName = list(tree.keys())[0]
# 取特征的列
featCol = np.argwhere(names == featName)[0][0]
names = np.delete(names, [featCol]) #删掉使用过的特征
newTree[featName] = tree[featName].copy() #取值
featValueDict = newTree[featName] #当前特征下面的取值情况
featPreLabel = featValueDict.pop("prun_label") #如果当前节点剪枝的话是什么标签,并删除_vpdl
# 分割测试数据 如果有数据 则进行测试或递归调用:
split_data = drop_exist_feature(test_data,featName) #删除该特征,按照该特征的取值重新划分数据
split_data = dict(split_data)
for featValue in featValueDict.keys(): #每个特征的值
if type(featValueDict[featValue]) == dict: #如果下一层还是字典,说明还是子树
split_data_feature = split_data[featValue] #特征某个取值的数据,如“脐部”特征值为“凹陷”的数据
split_data_lable = split_data[featValue].iloc[:, -1].values
# 递归到下一个节点
newTree[featName][featValue] = post_prunning(featValueDict[featValue],split_data_feature,split_data_lable,split_data_feature.columns)
# 根据准确率判断是否剪枝,注意这里的准确率是到达该节点数据预测正确的准确率,而不是整体数据集的准确率
# 因为在修改当前节点时,走到其他节点的数据的预测结果是不变的,所以只需要计算走到当前节点的数据预测对了没有即可
ratioPreDivision = equalNums(test_label, featPreLabel) / test_label.size #判断测试集的数据如果剪枝的准确率
#计算如果该节点不剪枝的准确率
ratioAfterDivision = predict_more(newTree, test_data, test_label)
if ratioAfterDivision < ratioPreDivision:
newTree = featPreLabel # 返回剪枝结果,其实也就是走到当前节点的数据最多的那一类
return newTree
if __name__ == '__main__':
#读取数据
train_data = pd.read_csv('train_data.csv')
test_data = pd.read_csv('test_data.csv')
test_data_label = test_data.iloc[:, -1].values
names = test_data.columns
dicision_Tree = {"脐部": {"prun_label": 1
, '凹陷': {'色泽':{"prun_label": 1, '青绿': 1, '乌黑': 1, '浅白': 0}}
, '稍凹': {'根蒂':{"prun_label": 1
, '稍蜷': {'色泽': {"prun_label": 1
, '青绿': 1
, '乌黑': {'纹理': {"prun_label": 1
, '稍糊': 1, '清晰': 0, '模糊': 1}}
, '浅白': 1}}
, '蜷缩': 0
, '硬挺': 1}}
, '平坦': 0}}
print('剪枝前的决策树:')
print(dicision_Tree)
print('剪枝前的测试集准确率: {}'.format(predict_more(dicision_Tree, test_data, test_data_label)))
print('-'*20 + '剪枝' + '-'*20)
new_tree = post_prunning(dicision_Tree,test_data , test_data_label , names)
print('剪枝后的决策树:')
print(new_tree)
print('剪枝后的测试集准确率: {}'.format(predict_more(new_tree, test_data, test_data_label)))
效果: