西瓜书4.3 编写过程 决策树

既然要建树,就可以考虑使用自定义类。

然后考虑这个类需要哪些功能。首先,需要有根、叶的区分标记,然后是节点的属性标记,

以及节点的属性集,节点的叶子节点的记录,节点的生成叶节点函数,节点的最优划分属性选择函数,

节点包含的哪些训练集样本的记录。

暂时就想到这些,那么就先开始。

 首先为了方便表示还是先把数据集全部用数字表示,变换规则依然是:

色泽:浅白 1,青绿 2,乌黑 3

根蒂:蜷缩 1,稍蜷 2,硬挺 3

敲声:沉闷 1,浊响 2,清脆 3

纹理:模糊 1,稍糊 2,清晰 3

脐部:平坦 1,稍凹 2,凹陷 3

触感:硬滑 1,软粘 0

D=np.array([[2,1,2,3,3,1,0.697,0.406,1],\
   [3,1,1,3,3,1,0.774,0.376,1],\
   [3,1,2,3,3,1,0.634,0.264,1],\
   [2,1,1,3,3,1,0.608,0.318,1],\
   [1,1,2,3,3,1,0.556,0.215,1],\
   [2,2,2,3,2,0,0.403,0.237,1],\
   [3,2,2,2,2,0,0.481,0.149,1],\
   [3,2,2,3,2,1,0.437,0.211,1],\
   [3,2,1,2,2,1,0.666,0.091,0],\
   [2,3,3,3,1,0,0.243,0.267,0],\
   [1,3,3,1,1,1,0.245,0.057,0],\
   [1,1,2,1,1,0,0.343,0.099,0],\
   [2,2,2,2,3,1,0.639,0.161,0],\
   [1,2,1,2,3,1,0.657,0.198,0],\
   [3,2,2,3,2,0,0.360,0.370,0],\
   [1,1,2,1,1,1,0.593,0.042,0],\
   [2,1,1,2,2,1,0.719,0.103,0]])

Y=['色泽','根蒂','敲声','纹理','脐部','触感','密度','含糖率','好瓜与否']

 

然后增加一个num数组,用于表示离散属性每种的属性个数:

num=[3,3,3,3,3,2]
顺便添加一个tree空列表用于存储节点。
然后开始编写类node(节点)
首先函数头+初始化函数,将刚刚提到的属性值都先定义好或者赋值:
def __init__(self,root,Dd,Aa,father,nu,floor):
self.root=root#树根为0,中间为1,叶子为2
self.num=nu#在树列表中的序号
self.attribu=None#叶节点属性
self.danum=0#若为连续属性划分时的分类值
self.attri_set=Aa#属性集
self.leaf=[]#子节点
self.sample=Dd#样本集
self.father=father#父节点
self.gain_set=[]#各属性的信息熵
self.floor=floor#节点的层级
return

实际编写的时候还是增加了一些属性或者属性集。所有属性集、结点集或样本集记录的都是在对应全集(属性集Y、结点集tree或样本集D)中的序号。

然后就参考西瓜书上的图4.2基本算法,按步骤编写成员函数。

首先就是如果该节点中所有样本都属于同一类别C(都是好瓜或者都是坏瓜),那就将该节点标记为叶子节点,节点类型标记为C:

def all_label_same(self):
k=D[self.sample[0]][8]
mark=1
for i in self.sample:
if(D[self.sample[i]][8]!=k):
mark=0
break
if(mark==1):
self.attribu = 8
self.danum=k
self.root = 2
return mark

用mark记录比较结果,全同为1反之为0,若mark变0则跳出循环,若mark为1则修改节点属性。

然后是第二步判断是否节点属性集为空或是节点的样本的属性完全一致,如果是完全一致则节点属性值取节点样本中较多的一类,

如果为空则与父节点属性值相同,两种情况均将该节点标记为叶子节点:

def all_b_att_same(self):#全空或全同
mark=0
if(len(self.attri_set)==0):
mark=1
self.danum=tree[self.father].coun_labe
self.attribu=8
self.root=2
elif(self.all_same()):
mark=1
self.danum=self.coun_labe()
self.attribu=8
self.root=2
return mark

同样地使用mark标记。全空的话类别标记为父节点中样本最多的标记。为了模块化和代码的可观性,将判断节点样本是否属性值完全一致与求样本集中较多的标记类写成了成员函数,详细如下:

def all_same(self):
k=D[self.sample[0]]
mark=1
for i in range(len(self.sample)):
if(k!=D[self.sample[i]]):
mark=0
return mark

def coun_labe(self):#计算样本中数量较多的类别
s=0
mark=0
for i in range(len(self.sample)):
s=s+D[self.sample[i]][8]
if(s>=(len(self.sample)/2)):
mark=1
return mark

因为样本标记是使用0/1标记的,所以统计时只需要计算标记的值之和然后与总样本数对比就可以了。

然后就是选择最优划分属性的函数,由于属性集中既有离散属性又有连续属性,所以要分开讨论计算,

 先写出主体函数:

def best_divide(self):
ents=self.ent(self.sample)
gain=[]
for i in range(len(self.attri_set)):
order=self.attri_set[i]#计算第order个属性的信息增益
gainp = ents
if(order<=5):
for j in range(num[order]):
if(order!=5):
gainp=gainp-self.gain(order,j+1)
else:
gainp=gainp-self.gain(order,j)
gain.append([gainp,order,0])
else:
mx=self.maxgain(order)
gainp=gainp-mx[0]
gain.append([gainp,order,mx[1]])
maxx=0
for i in range(len(self.attri_set)):
if(gain[i][0]>gain[maxx][0]):
maxx=i
self.root=1
self.attribu=gain[maxx][1]
self.danum=gain[maxx][2]
return gain[maxx]

ent函数用于计算该节点的信息熵,然后计算各个属性的信息增益,离散属性和连续属性分开讨论并编写不同函数计算,

由于连续属性划分之后还需要返回一个划分标准(小于等于x),所以需要返回一个列表,同时为了方便,离散属性也用相同形式储存,

其中储存x的地方放一个零,将这样形式的包含最大信息增益、对应属性以及划分标准(若为连续属性)作为函数的返回值。

以下为ent函数的内容:

def ent(self,d):
s=0
for i in range(len(d)):
s=s+D[d[i]][8]
if(len(d)==0):
k=0
else:
k=s/len(d)
if(k==1 or k==0):
s=-math.log(1,2)
else:
s=-k*math.log(k,2)-(1-k)*math.log(1-k,2)
return s

简单的遍历计算,考虑到约定0log2 0=0,分类讨论一下,下面都这么做。然后是计算离散属性信息增益的函数:

def gain(self,i,j):
s=0
k=0
for p in range(len(self.sample)):
if(D[self.sample[p]][i]==j):
s+=1
if(D[self.sample[p]][8]==1):
k+=1
if(s==0):
k=0
else:
k=k/s
if(k==0 or k==1):
k=-math.log(1,2)
else:
k=-k*math.log(k,2)-(1-k)*math.log(1-k,2)
if(len(self.sample)==0):
k=0
else:
k=k*s/len(self.sample)
return k

在for循环中s用于计数节点样本集中属性i的值为j的个数,k用于计数满足j条件中类别为1的个数,利用这两个指就可以分别求出书上4.2中减号后

每一项的值。接下来是计算连续属性的信息增益的函数:

   def maxgain(self,a):
      lis=[]
      for i in range(self.sample):
         lis.append(D[self.sample[i]][a])
      lis.sort()
      long=len(lis)
      en=[]
      for i in range(long-1):
         t=(lis[i]+lis[i+1])/2
         entp=0
         num1=0#属性a的值小于等于t的样本中标记值为1的个数
         num2=0#属性a的值大于t的样本中标记值为1的个数
         sum=0#属性a的值小于等于t的样本数
         for j in range(self.sample):
            order=self.sample[j]
            if(D[order][a]<=t):
               sum+=1
               if(D[order][8]==1):
                  num1+=1
            else:
               if(D[order][8]==1):
                  num2+=1
         num1/=sum
         num2/=(long-sum)
         if(num1==0 or num1==1):
            entp-=np.log2(1)
         else:
            entp=entp-num1*np.log2(num1)-(1-num1)*np.log2(1-num1)
         if(num2==0 or num2==1):
            entp-=np.log2(1)
         else:
            entp=entp-num2*np.log2(num2)-(1-num2)*np.log2(1-num2)
         en.append(entp)
      s=0
      for i in range(en):
         if(en[i]<en[s]):
            s=i
      result=[en[s],(lis[s]+lis[s+1])/2]
      return result

 先取出样本中连续属性的值,放入一个list中,sort之后循环依次相邻数两两取平均值t作为划分依据,然后遍历,记录属性a的值小于等于t的样本中标记值为1的个数、属性a的值大于t的样本中标记值为1的个数以及属性a的值小于等于t的样本数。依据这三项就可以计算每一种划分的信息增益大小,从结果中选出最小值返回。

然后就完成了最优属性选择,接下来就是建立子节点。

   def build_son(self):
      mark=0
      if(not self.all_label_same()):
         mark=0
      elif(not self.all_b_att_same()):
         mark=0
      else:
         mark=1
         bd=self.best_divide()
         if(bd[2]==0):
            pass_a=[]
            for i in range(self.attri_set):
               if(self.attri_set[i]!=bd[2]):
                  pass_a.append(self.attri_set[i])
            for i in range(num[bd[1]]):
               k=i
               pass_d = []
               if(bd[1]!=5):
                  k+=1
               for j in range(self.sample):
                  if(D[self.sample[j]][bd[1]]==k):
                     pass_d.append(self.sample[j])
               lon=len(tree)
               self.leaf.append([lon,k])
               tree.append(node(1,pass_d,pass_a,self.num,lon))
         else:
            pass_small=[]
            pass_bigger=[]
            for i in range(self.sample):
               if(D[self.sample[i]][bd[1]]<=bd[2]):
                  pass_small.append(self.sample[i])
               else:
                  pass_bigger.append(self.sample[i])
            lon=len(tree)
            self.leaf.append([lon,0])
            self.leaf.append([lon+1,1])
            tree.append(node(1,pass_small,self.attri_set,self.num,lon))
            tree.append(node(1,pass_bigger,self.attri_set,self.num,lon+1))
         #for i in range(self.leaf):
          #  tree[self.leaf[i]].build_son()
      return mark

将叶节点判断内容也整合到子节点建立中,依然使用mark标志以备不时之需。如果不是叶节点的话,就进行建立子节点操作,依照best_divide函数返回的值进行分类讨论,需要注意的是第五项属性的值是0或1,其他离散属性的值是1或2或3,在循环对比属性值时要根据情况改变值,观察初始化函数:

def __init__(self,root,Dd,Aa,father,nu,floor):
self.root=root#树根为0,中间为1,叶子为2
self.num=nu#在树列表中的序号
self.attribu=8#叶节点属性
self.danum=0#若为连续属性划分时的分类值
self.attri_set=Aa#属性集
self.leaf=[]#子节点
self.sample=Dd#样本集
self.father=father#父节点
self.gain_set=[]#各属性的信息熵
self.floor=floor
return

可以看到我们需要传入的数据有节点的根属性(树根、中间或叶子,默认都为1),节点属性集,节点样本集和父节点的序号以及节点本身序号,其中在属性集和样本集的统计方法上离散属性和连续属性不一样,要分开进行,父节点序号父节点中有记录,子节点序号可以直接读取当前tree的长度获得,那么主要的步骤就是属性集和样本集的构建。

首先是离散属性,其属性集只要父节点的属性集排除掉自己的筛选属性即可,而样本集前四个属性都是有三种取值,那么对使值分别去0、1、2分别+1之后就可以得到属性取值,筛选完一种取值后得到本种取值的样本集就可以直接生成子节点,并将(子节点序号,子节点取值)数据对存入本节点的子节点集,第五个属性只要省去属性值+1的步骤即可。

然后是连续属性,子节点的属性集与父节点相同,而样本集则根据best_divide返回的划分值进行划分,直接在一次遍历中完成两个样本集的录入然后一次性计入节点的子节点集。

在所有的子节点生成完毕后对每个子节点进行节点生成。

然后考虑决策树分类完成之后的打印问题,为了使打印出来的树具有较好的可读性,在每个节点中加入层的属性。

子节点的层数是父节点层数+1。

然后就是节点的打印函数:

def prin(self):
print('root:',self.root,' 划分属性(节点属性):',Y[self.attribu],' ',self.danum,' floor:',self.floor)

主函数如下:

star=node(0,D,[0,1,2,3,4,5,6,7,8],0,0,0)
tree.append(star)
star.build_son()
flm=tree[len(tree)-1].danum+1
for i in range(flm):
   for j in range(tree):
      if(tree[j].danum==i):
         tree[j].prin()
   print('\n')

 然后debug把问题都改掉之后发现递归爆栈了,所以把build函数里的递归部分注释掉,主函数更换如下:

star=node(0,[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],[0,1,2,3,4,5,6,7,8],0,0,0)
tree.append(star)
star.build_son()
tail=0

while(tail!=(len(tree)-1)):
   las=len(tree)-1
   k=tail
   for i in range(k+1,las+1):
      tree[i].prin()
      tree[i].build_son()

flm=tree[len(tree)-1].danum+1
for i in range(flm):
   for j in range(len(tree)):
      if(tree[j].danum==i):
         tree[j].prin()
   print('\n')

先给原始节点建立子节点以后,标记原始节点下一位和tree数组最后一位,不断给新增加的节点生成子节点直到没有子节点生成,打印输出

粗略估计了一下输出可能会有14w以上的节点,而且也没有做可视化,原理上讲程序应该基本实现了功能,但还是有待以后的完善,时间有限,暂且就这样了。

posted @ 2020-12-05 16:36  虚在君  阅读(466)  评论(0编辑  收藏  举报