决策树是一类常见的机器学习方法,我们可以通过对样本的属性进行一系列判断,最终决策其所属的标签类别,也就是说决策树是一类分类算法。
以西瓜书第4章决策树所给数据为例,构建决策树的过程大致为,我们每次通过选出“信息熵增益(Gain Information Entropy)”最大的属性,直到最后能够对样本标签进行预测。
信息熵 是度量样本集合纯度常用的一种指标。假定当前样本集合D中第k类标签所占的比例为 p k ( k = 1 , 2 , . . . . ∣ Y ∣ ) ( 1.1 ) p_{k}(k=1,2,....|Y|) \qquad(1.1) pk(k=1,2,....∣Y∣)(1.1),则D的信息熵定义为
E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k Ent(D)=-\sum^{|y|}_{k=1}{p_{k}log_{2}p_{k}} Ent(D)=−k=1∑∣y∣pklog2pk
其中, E n t ( D ) Ent(D) Ent(D)的值越小,那么D的纯度就越高
假定离散属性a有V个可能的取值 a 1 , a 2 , . . . , a V {a^{1},a^{2},...,a^{V}} a1,a2,...,aV,若使用a来对样本集D进行划分,则会产生V个分支节点,其中第v个分支节点包含了D中所有在属性a上取值为 a v {a^{v}} av的样本,记为 D V D^{V} DV。我们可以根据式1.1计算出 D V D^{V} DV的信息熵,再考虑到不同的分支节点所包含的样本数量不同,给分支节点进行加权 ∣ D v / ∣ D ∣ |D^{v}/|D| ∣Dv/∣D∣,那么,分支节点样本数越多,则影响越大。给出信息增益公式
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) ( 1.2 ) Gain(D,a)=Ent(D)-\sum^{V}_{v=1}{\dfrac{|D^{v}|}{|D|}Ent(D^{v})} \qquad(1.2) Gain(D,a)=Ent(D)−v=1∑V∣D∣∣Dv∣Ent(Dv)(1.2)
信息增益准则对可取值数目较多的属性有所偏好,为减少这种偏好所带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而是使用“增益率(Gain Ratio)”来选择划分最优属性。增益率采用和(1.2)式相同的符号,其公式表达如下 G a i n R a t i o ( D , a ) = G a i n ( D , a ) I V ( a ) ( 1.3 ) GainRatio(D,a)=\dfrac{Gain(D,a)}{IV(a)}\qquad(1.3) GainRatio(D,a)=IV(a)Gain(D,a)(1.3)
其中, I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ l o g 2 ∣ D v ∣ ∣ D ∣ ( 1.4 ) IV(a)=-\sum^{V}_{v=1}{\dfrac{|D_{v}|}{|D|}log_{2}\dfrac{|D^{v}|}{|D|}}\qquad(1.4) IV(a)=−v=1∑V∣D∣∣Dv∣log2∣D∣∣Dv∣(1.4)
需要注意的是,增益率准则对可取值数目较少的属性有所偏好,因此,C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:先从候选划分属性中找出信息增益高出平均水平的属性
上述所有内容全是建立在数据样本特征为离散值的基础之上的,当数据的信息为连续值时,其决策树的构建与离散值还有一定的不同。其具体区别为,如果针对于某一特征a,其取值可能为{a1,a2,a3},由于是离散值,我们可以很自然的由特征a
引申出3个分支。但是针对于连续值,我们只能在已有的连续值范围(min,max)间寻找几个端点,用于分支划分。对于给定样本D和连续属性a,假定a在D上出现了n个不同的连续取值,将这些取值从小到大进行排序,记为 a 1 , a 2 , . . . , a 3 {a_{1},a_{2},...,a_{3}} a1,a2,...,a3。基于划分点t,可以将D分为子集 D v − 和 D v + D_{v}^{-}和D_{v}^{+} Dv−和Dv+,分别包含在属性a上取值小于t和大于t的样本点。那么对于属性取值 a i 和 a i + 1 a^{i}和a^{i+1} ai和ai+1来说,t在区间 [ a i , a i + 1 ) [a^{i},a^{i+1}) [ai,ai+1)中取任意值所产生的划分结果相同。因此,对于连续属性a,我们可考察包含n-1个元素的候选划分点集合
T a = a i + a i + 1 2 ∣ 1 ≤ i ≤ n − 1 ( 1.7 ) T_{a}={\dfrac{a^{i}+a^{i+1}}{2}|1\le i\le n-1}\qquad(1.7) Ta=2ai+ai+1∣1≤i≤n−1(1.7)
最终,基于连续值属性的信息增益为 G a i n ( D , a ) = m a x t ∈ T a G a i n ( D , a , t ) Gain(D,a)=max_{t\in T_{a}}Gain(D,a,t) Gain(D,a)=maxt∈TaGain(D,a,t)
= m a x t ∈ T a E n t ( D ) − ∑ λ ∈ { − , + } ∣ D t λ ∣ ∣ D ∣ E n t ( D t λ ) =max_{t \in T_{a}} Ent(D)-\sum_{\lambda\in \{-,+\}}{\dfrac{|D_{t}^{\lambda}|}{|D|}}Ent(D_{t}^{\lambda}) =maxt∈TaEnt(D)−λ∈{−,+}∑∣D∣∣Dtλ∣Ent(Dtλ)
目前囿于时间限制,只完成了基础的离散值和连续值决策树构建,缺失值处理、剪枝等功能待后续有时间和精力继续完善…
决策树构建算法的伪代码如下:
输入:训练集 D = ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x m , y m ) D={(x_{1},y_{1}),(x_{2},y_{2}),...,(x_{m},y_{m})} D=(x1,y1),(x2,y2),...,(xm,ym);
属性集 A = a 1 , a 2 , . . . , a d A={a_{1},a_{2},...,a_{d}} A=a1,a2,...,ad
过程:
函数TreeGenerate(D,A)
1:生成节点node;
2:if D中的样本全部属于某一个标签类别C
3: then 将node标记为C类叶节点;return
4:end if
5:if A = ∅ A=\emptyset A=∅ OR D中样本在A上的取值全部相同 then
6:将node标记为叶节点,其类别标记为D中样本数量最多的类
7:end if
8:从A中选择最优划分属性a*;
9:for a的每一个值 a v ∗ a_{v}^{*} av∗ do:
10:为node生成一个分支;令 D v D_{v} Dv表示D中在a上取值为 a v ∗ a_{v}^{*} av∗ 的样本子集;
11:if D v D_{v} Dv为空 then
12:将分支节点标记为叶节点,其类别标记为D中样本组多的类;return
13:else:
14:以TreeGenerate(Dv,A-a*)为分支结点(即用样本子集Dv,和去掉属性a*的属性集再衍生出新的树)
15:end if
16:end for
决策树的生成过程是一个递归函数,导致递归返回的,有三种情况:
一是样本D中的样本标签,也就是y值全都一样,那么就会生成一个叶子节点,然后返回;二是样本D中的样本各属性(或者叫特征)的值完全一样,这样的话,将该节点标记为叶节点,然后将其类别标记为D中数据量最多的标记;三是当前节点所含的样本为空,那么利用其父节点的数据中数量最多的标记来标记当前节点的标签。
那么,只要不是叶节点,都会继续向下开拓出子树。
节点Node结构定义,在这里我是用一个类然后实例化对象来保存,看过大佬写的文章是用字典来保存。这里可以凭借个人喜好。我选择类的原因是调用各种参数相对比较方便,而且还可以加内置函数。
class Node:
def __init__(self,attribute,ifleaf,subnode,label,contivalue=None):
self.attribute=attribute
self.ifleaf=ifleaf
self.subnode=subnode
self.label=label
self.contivalue=contivalue
def UpdateAttr(self,attribute):
self.attribute=attribute
def UpdataIfleaf(self,ifleaf):
self.ifleaf=ifleaf
def UpdateSubnode(self,subnode):
self.subnode=subnode
def UpdateLabel(self,label):
self.label=label
def UpdateSubnode(self,attribute,node):
self.subnode.update({attribute:node})
def TreeGenerate(data,string,attributes,fatherNode,layer,attrValue): #attribute为输入属性
dataNumpy=data.values
node=Node(None,False,{},None)
tempValue = attrValue
if ">" in attrValue:
fatherNode.contivalue = tempValue.replace(">", "")
elif "<" in attrValue:
fatherNode.contivalue = tempValue.replace("<", "")
labels=data[string]
labels=np.unique(np.array(labels))
# theFirstele=data[0][string]
singleClassMark=True #用来标记数据集中是否都为同一类型
singleFeatureMark=True #用来标记数据集的特征取值是否都一样
if len(labels)>1: #判断数据集中的标记是否相同
singleClassMark=False
if singleClassMark==True: #如果相同则将其标记为该类叶节点
node.ifleaf=True
node.label=data[string][0]
# return node
fatherNode.UpdateSubnode(attrValue,node)
layer+=1
G.add_node(node.label,subset=layer)
dot.node(node.label,fontname="Microsoft YaHei")
if fatherNode.attribute!=None:
G.add_edge(fatherNode.attribute,node.label,name=attrValue)
dot.edge(fatherNode.attribute,node.label,label=attrValue,fontname="Microsoft YaHei")
elif fatherNode.label!=Node:
G.add_edge(fatherNode.label,node.label,name=attrValue)
dot.edge(fatherNode.label,node.label,label=attrValue,fontname="Microsoft YaHei")
return None
for i in dataNumpy: #用来判断,数据的特征取值是否都一样
if (i!=dataNumpy[0]).any(): #只要两组数据中有一个数据不一样,那么特征取值就会不同
singleFeatureMark=False
break
if attributes==None or singleFeatureMark==True:
node.ifleaf=True
#获取数据中元素数量最多的标签
labelDivide,labelKind=DataDivided(data,string)
templabellen=0
index=0
for i in range(len(labelDivide)):
if len(labelDivide[labelKind[i]])>templabellen:
templabellen=len(labelDivide[labelKind[i]])
index=i
node.label=labelKind[index]
fatherNode.UpdateSubnode(attrValue,node)
layer+=1
G.add_node(node.label,subset=layer)
dot.node(node.label,fontname="Microsoft YaHei")
if fatherNode.attribute != None:
G.add_edge(fatherNode.attribute, node.label,name=attrValue)
dot.edge(fatherNode.attribute,node.label,label=attrValue,fontname="Microsoft YaHei")
elif fatherNode.label != Node:
G.add_edge(fatherNode.label, node.label,name=attrValue)
dot.edge(fatherNode.label,node.label,label=attrValue,fontname="Microsoft YaHei")
return None
# return node
#从A中选择最优的划分属性
attrTuple=SelBestAttr(data,attributes,string,attrValue,layer)
if len(attrTuple)==2:
bestAttr,bestEntGain=attrTuple[0],attrTuple[1]
# bestAttrValue=np.unique(data[bestAttr])
bestAttrDiv,bestAttrValue=DataDivided(data,bestAttr)
node.attribute=bestAttr
node.label=None
fatherNode.UpdateSubnode(attrValue,node)
layer += 1
G.add_node(node.attribute, subset=layer)
dot.node(node.attribute,fontname="Microsoft YaHei")
if fatherNode.attribute != None:
G.add_edge(fatherNode.attribute, node.attribute,name=attrValue)
dot.edge(fatherNode.attribute,node.attribute,label=attrValue,fontname="Microsoft YaHei")
elif fatherNode.label != Node:
G.add_edge(fatherNode.label, node.attribute,name=attrValue)
dot.edge(fatherNode.label,node.attribute,label=attrValue,fontname="Microsoft YaHei")
attributes.remove(bestAttr) #将最优特征从特征表中去除
#为最优属性展开子树
for i in range(len(bestAttrDiv)):
# if isinstance(data[bestAttr][1],str):
if len(bestAttrDiv[bestAttrValue[i]])==0:
labelDivide, labelKind = DataDivided(data, string)
templabellen = 0
index = 0
for j in range(len(labelDivide)):
if len(labelDivide[labelKind[j]]) > templabellen:
templabellen = len(labelDivide[labelKind[j]])
index = j
newNode1=Node(None,True,None,labelKind[index])
node.UpdateSubnode(bestAttrValue[i],newNode1)
layer+=1
G.add_node(newNode1.label,subset=layer)
dot.node(newNode1.label,fontname="Microsoft YaHei")
G.add_edge(node.attribute,newNode1.label,name=bestAttrValue[i])
dot.edge(node.attribute,newNode1.label,label=bestAttrValue[i],fontname="Microsoft YaHei")
else:
bestAttrDiv[bestAttrValue[i]] = pd.DataFrame(bestAttrDiv[bestAttrValue[i]], columns=data.columns)
# newNode2=Node(bestAttrValue[i],False,Node,string)
TreeGenerate(bestAttrDiv[bestAttrValue[i]],string,attributes,node,layer,bestAttrValue[i])
else:
bestAttr,bestValue,divideNum=attrTuple[0],attrTuple[1],attrTuple[2]
node.attribute = bestAttr
node.label = None
fatherNode.UpdateSubnode(attrValue, node)
layer += 1
G.add_node(node.attribute, subset=layer)
dot.node(node.attribute,fontname="Microsoft YaHei")
if fatherNode.attribute != None:
G.add_edge(fatherNode.attribute, node.attribute, name=attrValue)
dot.edge(fatherNode.attribute,node.attribute,label=attrValue,fontname="Microsoft YaHei")
elif fatherNode.label != Node:
G.add_edge(fatherNode.label, node.attribute, name=attrValue)
dot.edge(fatherNode.label,node.attribute,label=attrValue,fontname="Microsoft YaHei")
for i in range(len(divideNum)):
if len(divideNum[i])==0:
labelDivide, labelKind = DataDivided(data, string)
templabellen = 0
index = 0
for j in range(len(labelDivide)):
if len(labelDivide[labelKind[j]]) > templabellen:
templabellen = len(labelDivide[labelKind[j]])
index = j
newNode1 = Node(None, True, None, labelKind[index])
node.contivalue=bestValue
edgeName="<%f"%bestValue if i==0 else ">%f"%bestValue
node.UpdateSubnode(edgeName, newNode1)
layer += 1
G.add_node(newNode1.label, subset=layer)
dot.node(newNode1.label,fontname="Microsoft YaHei")
G.add_edge(node.attribute, newNode1.label, name=edgeName)
dot.edge(node.attribute,newNode1.label,label=edgeName,fontname="Microsoft YaHei")
else:
edgeName = "<%f" % bestValue if i == 0 else ">%f" % bestValue
TreeGenerate(divideNum[i],string,attributes,node,layer,edgeName)
def InforEntCreate(data,label):
labelValue=np.unique(data[label])
len=data.shape[0]
countList=[]
for i in labelValue:
tempcount=0
for j in range(len):
if data[label][j]==i:
tempcount+=1
countList.append(tempcount)
countList=np.array(countList)
countList=countList/len
countList=-np.multiply(countList,np.log2(countList))
return np.sum(countList)
def DataDivided(data,attribute): #用于针对某一特性的数据分割
singleClassData = dataInit[attribute]
singleClassData=list(singleClassData) #将singleClassData转化为列表,从而可以使用数标引用
attrKinds = np.unique(singleClassData)
singleClassData=data[attribute] #将输入数据的某一列特征值抽取
singleClassData=list(singleClassData)
dataDivided = {} # 用于将数据分割保存
for i in attrKinds:
dataDivided.update({i:[]})
data=data.values #将dataFrame格式变成Narray形式
for i in range(len(dataDivided)):
for j in range(len(singleClassData)):
if singleClassData[j] == attrKinds[i]:
dataDivided[attrKinds[i]].append(data[j])
# for i in range(len(dataDivided)):
# dataDivided[i]=pd.DataFrame(dataDivided[i],data.columns)
return dataDivided,attrKinds
def InforGainEnt(data,attribute,label):
dataDivided,dataKinds=DataDivided(data,attribute)
multiInforEnt=0
for i in range(len(dataDivided)):
dataDivided[dataKinds[i]]=pd.DataFrame(dataDivided[dataKinds[i]],columns=data.columns)
length=data.shape[0]
for i in range(len(dataDivided)):
multiInforEnt+=InforEntCreate(dataDivided[dataKinds[i]],label)*dataDivided[dataKinds[i]].shape[0]/length
#将dataFrame重组,以便保证其index从0开始
data.index=np.arange(data.shape[0])
totalEnt=InforEntCreate(data,label)
inforGain=totalEnt-multiInforEnt
return inforGain
def GainRatio(data,attribute,label):
length=data.shape[0]
gain=InforGainEnt(data,attribute,label)
dataDivided,attrKinds=DataDivided(data,attribute)
attrIV=0
for i in range(len(dataDivided)):
valueLen=len(dataDivided[attrKinds[i]])
if valueLen!=0:
attrIV+=-valueLen/length*log2(valueLen/length)
return gain/attrIV
def SelBestAttr(data,attributes,label,attrValue,layer):
tempAttr=0
maxAttr=0
maxIndex=""
bestValue=0
bestDivNum=[]
tempValue=0
tempDivNum=[]
for i in attributes:
if isinstance(data[i][1],str):
tempAttr=InforGainEnt(data,i,label)
# tempAttr=GainRatio(data,i,label)
else:
tempAttr,tempValue,tempDivNum=ContiAttrSelect(data,i,label)
if tempAttr>maxAttr:
maxAttr=tempAttr
maxIndex=i
bestValue=tempValue
bestDivNum=tempDivNum
print("maxIndex:",maxIndex,"attributes",attributes,"attrValue",attrValue,"layer",layer)
if isinstance(data[maxIndex][1],str): #这里多少得有问题
return maxIndex,maxAttr
else:
return maxIndex,bestValue,bestDivNum
def ContiAttrSelect(data,attribute,label):
length=data.shape[0]
columns=data.columns
contiAttrs=data[attribute]
contiAttrs=np.unique(np.array(contiAttrs))
contiAttrs=np.sort(contiAttrs)
for i in range(len(contiAttrs)-1):
contiAttrs[i]=(contiAttrs[i]+contiAttrs[i+1])/2
contiAttrs=list(contiAttrs)
contiAttrs.pop(-1)
tempEnt=0
tempBest=0
bestValue=0
bestDivNum=[]
for j in contiAttrs:
divideNum=[[],[]]
for i in range(length):
if data[attribute][i]<j:
divideNum[0].append(data.loc[i])
else:
divideNum[1].append(data.loc[i])
divideNum[0]=pd.DataFrame(divideNum[0],columns=columns,index=np.arange(len(divideNum[0])))
divideNum[1]=pd.DataFrame(divideNum[1],columns=columns,index=np.arange(len(divideNum[1])))
tempEnt+=InforEntCreate(divideNum[0],label)*divideNum[0].shape[0]/length
tempEnt+=InforEntCreate(divideNum[1],label)*divideNum[1].shape[0]/length
totalEnt=InforEntCreate(data,label)
if totalEnt-tempEnt>tempBest:
tempBest=totalEnt-tempEnt
bestValue=j
bestDivNum=divideNum
tempEnt=0
return tempBest,bestValue,bestDivNum
这里我用了两种框架,一个是专门用来画图结构的networkx和另一个Graphviz,两种框架上手都比较快。networkx好像是只能以图的形式表现,如果树的结构比较复杂的话看上去就会比较乱。
G=nx.DiGraph()
dot=Digraph(format="jpg")
attributes=dataInit.columns
attributes=list(attributes)
dataNumpy=dataInit.values
global root
root=Node("root",False,{},None)
G.add_node("root",subset=0)
dot.node("root")
layer=0
TreeGenerate(dataInit,attributes[-1],attributes[:-1],root,layer,"root")
pos=nx.multipartite_layout(G)
edge_labels=nx.get_edge_attributes(G,"name")
nx.draw_networkx(G, node_size=1000, pos=pos,font_family="SimSun")
nx.draw_networkx_edge_labels(G,pos,edge_labels=edge_labels,font_family="SimSun")
pyplot.show()
dot.render("DecisionTree.jpg",view=True)
原理大概就是,如果某一属性a是离散值的话,那就按照离散值{a1,a2,…,an}生成(0,n)之间的整数,然后将整数标记对应转换为相应的离散值。如果a是连续值,那就获取a属性中的最大最小值,生成(min,max)的随机浮点数。
import numpy as np
from DataProcess import dataInit,attributes
import pandas as pd
size=10000
attrlen={} # 利用numpy随机数生成字典
for i in attributes:
if isinstance(dataInit[i][1],str):
attrcol=dataInit[i]
attrcol=np.array(attrcol)
length=len(np.unique(attrcol))
attrlen.update({i:np.random.randint(length,size=size)})
else:
max=np.max(np.array(dataInit[i]))
min=np.min(np.array(dataInit[i]))
attrlen.update({i:np.random.rand(size)*(max-min)+min})
dataRan=pd.DataFrame(attrlen) #利用字典生成伪随机数dataFrame
#将随机整数对应于相应的标签
for i in attributes:
if isinstance(dataInit[i][1],str):
attrcol=dataInit[i]
attrcol=np.array(attrcol)
attrValue=np.unique(attrcol)
for j in range(dataRan.shape[0]):
dataRan[i][j]=attrValue[dataRan[i][j]]
dataRan.to_csv("NewWatermelon2.csv")
import pandas as pd
dataTest=pd.read_csv("NewWatermelon2.csv",index_col=0)
# dataTest=dataTest.iloc[:50,:]
from AllKindsDT import root
def WatermelonEstimate(data,node):
if node.label==None and node.attribute==None:
print("This node is not a estimate node.")
if node.label==None and node.attribute!=None:
if isinstance(data[node.attribute],str):#这里要分属性是离散值还是连续值
attrValue=data[node.attribute]
return WatermelonEstimate(data,node.subnode[attrValue])
else:
contiValue=float(node.contivalue)
attrValue=">%f"%contiValue if data[node.attribute]>contiValue else "<%f"%contiValue
return WatermelonEstimate(data,node.subnode[attrValue])
if node.label!=None:
currentLabel=node.label
return currentLabel
def AccuracyCal(data,node,label):
length=data.shape[0]
accuracy=0
for i in range(data.shape[0]):
label_pred=WatermelonEstimate(data.loc[i],node)
if label_pred==data[label][i]:
accuracy+=1
accRatio=accuracy/length
return accRatio
acc=AccuracyCal(dataTest,root.subnode["root"],"好瓜")
print(acc)
西瓜书P84 表4.3数据集生成决策树结构如下
基于西瓜书数据随机生成的数据量为100的数据生成决策树如下
由于连续值属性使用后不用从特征集中删除,可以重复使用,因此连续属性在表现上会产生超多的分支,数据越多分支越多。因此,需要对决策树进行剪枝。
基于西瓜书数据再生成数据量为10000的数据集,然后在上述决策树上进行测试。
决策树-100
准确率为0.4994
决策树-17
准确率为0.5031
我们可以看到准确率基本都逼近0.5.
因为生成随机数使用的是numpy的random模块。从Numpy的官网可以找到有关于numpy.random的大概说明
即随机数生成:这个对象将BitGenerator(二进制生成器)生成的一串随机二进制转化为一串遵循特定分(均匀分布,正态分布,二项分布)布具有特定间隔的数。
因此,无论是上述分布中的哪一种,基于生成决策树数据集产生的随机数据集都是关于50%的密度值对称的,也就是说最后只有50%的值分布满足于原数据集。