數據挖掘day22、23-《數據挖掘導論》-第四章,4.1-4.3.7 決策樹


決策樹的實現相對我這種新手比較難,參考了一篇文章數據挖掘領域十大經典算法之—C4.5算法(超詳細附代碼)
首先貼上書上的相關內容,包括P94,一個預測拖欠貸款的數據集,以及根據數據生成的樹。後面是P101,樹生成的算法框架。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

1、樹類

1.1、參考

但是,他裏面寫的內容比較散亂,明顯沒有書本P101的框架明白,因此僅參考了他的‘樹’類的寫法,下面是他的原寫法:

class Tree(object):
    def __init__(self,node_type,Class = None, feature = None):
        self.node_type = node_type  # 節點類型(internal或leaf)
        self.dict = {} # dict的鍵表示特徵Ag的可能值ai,值表示根據ai得到的子樹 
        self.Class = Class  # 葉節點表示的類,若是內部節點則爲none
        self.feature = feature 
        # 表示當前的樹即將由第feature個特徵劃分z(即第feature特徵是使得當前樹中信息增益最大的特徵)

    def add_tree(self,key,tree):
        self.dict[key] = tree

    def predict(self,features): 
        print(self.dict)
        if self.node_type == 'leaf' or (features[self.feature] not in self.dict):
            return self.Class

        tree = self.dict.get(features[self.feature])
#         print(tree.dict)
        return tree.predict(features)

用一個例子(P94)測試一下效果:

a=Tree('internal',None,0)
one1=Tree('leaf','No',1)
one2=Tree('internal',None,1)
a.add_tree('Yes',one1)
a.add_tree('No',one2)
two1=Tree('leaf','No',2)
two2=Tree('internal',None,2)
one2.add_tree('married',two1)
one2.add_tree('single',two2)
three1=Tree('leaf','No',3)
three2=Tree('leaf','Yes',3)
two2.add_tree('<80K',three1)
two2.add_tree('>80K',three2)

a.predict(['No','single','>80K']):結果如下:

{'Yes': <__main__.Tree object at 0x0000000005131390>, 'No': <__main__.Tree object at 0x0000000005131400>}
{'married': <__main__.Tree object at 0x0000000005131278>, 'single': <__main__.Tree object at 0x0000000005113CF8>}
{'<80K': <__main__.Tree object at 0x0000000005113D30>, '>80K': <__main__.Tree object at 0x00000000051026A0>}
{}
'Yes'

1.2、改寫

原寫法花了不少時間才弄明白feature是指針,參考書本P101的框架,發現並不需要Class參數。並且,predict中終止條件屬性錯誤應該是Error
按照P101的框架需要改寫一下(更加清晰):

class Node():
    def __init__(self,label=None,test_cond = None):
        self.label = label # 葉節點表示的類,內部節點爲None
        self.test_cond = test_cond # 當前的測試特徵,葉節點爲None
        self.dict = {} # dict的鍵表示當前測試特徵的可能值v,值是對應的子樹 
        
    def add_child(self,key,child):
        self.dict[key] = child

    def predict(self,F): #遞歸預測   
#        print(self.dict) #測試用
        if self.label: #遞歸的終止條件(到達葉節點)
            return self.label
        # 輸入特徵值錯誤處理
        if F[self.test_cond] not in self.dict:
            return 'feature Error: %s'% F[self.test_cond]
        #由當前的特徵值test_cond,進入下一級
        child = self.dict.get(F[self.test_cond])
        return child.predict(F)#遞歸

2、main

2.1、測試

P101這個框架是遞歸的方式,由於對遞歸不太瞭解,首先對這個框架進行了簡單的代碼測試:

def Test(L):
    if len(L)==1:
        return Node('hello')
    root=Node()
    V=L[0]
    root.test_cond=V
    L=L[1:]
    child=Test(L)
    root.add_child(V,child)
    return root

L=list(range(10))
root=Test(L)
root.predict(L)

結果不錯:

{0: <__main__.Node object at 0x0000000005137C50>}
{1: <__main__.Node object at 0x00000000051379B0>}
{2: <__main__.Node object at 0x0000000005137D68>}
{3: <__main__.Node object at 0x0000000005137DD8>}
{4: <__main__.Node object at 0x0000000005137E48>}
{5: <__main__.Node object at 0x0000000005137DA0>}
{6: <__main__.Node object at 0x0000000005137E10>}
{7: <__main__.Node object at 0x0000000005137E80>}
{8: <__main__.Node object at 0x0000000005137EF0>}
{}
'hello'

2.1、代碼

按照書上框架寫下去真是非常清爽,就是Gini計算時候,數組比較麻煩,還用上了reduce,還是DataFrame好。然後本想對連續屬性進行處理和計算一下增益,最後權衡一下,還是抓緊趕進度吧。

import numpy as np
from functools import reduce

#E是訓練記錄集,F是屬性集,在程序內部使用數字索引的,F僅爲了便於理解

#樹分裂的停止條件
def stopping_cond(E,F):
    stop=False
    if len(F)==0 or len(set(E[:,-1]))==1:
        stop=True
    return stop  

#確定葉節點類標號
def Classify(E):
    labels=E[:,-1]
    class_count=[(i,len(list(filter(lambda x:x==i,labels)))) for i in set(labels)]
    (max_class,max_len)=max(class_count,key=lambda x:x[1])
    return max_class  

#不純度量
def Gini(T):
    #提取屬性測試列
    V=list(set(T[:,0]))
    #提取全部標籤
    labels=list(set(T[:,1]))
    count_all=len(T)
    gini=0

    #按照當前屬性將T進行分割
    split=[(v,list(filter(lambda x:x[0]==v,T))) for v in V ]
    
    for s in range(len(split)): 
        #按照標籤進行數量統計
        class_count=[len(list(filter(lambda x:x[1]==i,split[s][1]))) for i in labels]
        c=len(split[s][1])
        #計算Gini
        gini_single=1-reduce(lambda x,y:(x/c)**2+(y/c)**2,class_count)
        gini+=gini_single*c/count_all 
    return gini

#選擇最優屬性,作爲本次劃分條件
def find_best_split(E,F):
    E=E[:-1]#棄掉label列
    gini=[(i,Gini(E[:,[i,-1]])) for i in range(len(F))]
    (best_split,min_gini)=min(gini,key=lambda x:x[1])
    return best_split

def TreeGrowth(E,F):
    if stopping_cond(E,F)==True:
        leaf=Node()
        leaf.label=Classify(E)
        return leaf
    else:
        root=Node()
        root.test_cond=find_best_split(E,F)
#        print(root.test_cond,'--') #測試
        V=list(set(E[:,root.test_cond]))
        for v in V:
            Ev=E[E[:,root.test_cond]==v]
            child=TreeGrowth(Ev,F)
            root.add_child(v,child)
    return root
        

測試數據,還是P94頁例子的數據。從測試代碼print(root.test_cond,'--'),得到的結果是[1,0,0,2],因爲對於婚姻一項沒有合併,與書本的書略有不同。

E=np.array([['Yes','single','>=80K','No'],
   ['No','married','>=80K','No'],
   ['No','single','<80K','No'],
   ['Yes','married','>=80K','No'],
   ['No','Divorced','>=80K','Yes'],
   ['No','married','<80K','No'],
   ['Yes','Divorced','>=80K','No'],
   ['No','single','>=80K','Yes'],
   ['No','married','<80K','No'],
   ['No','single','>=80K','Yes']])
F=['house','marital','income']
print(E[:,0])

Tree=TreeGrowth(E,F)
Tree.predict(['No','single','>=80K'])

使用負類進行測試,能得到全部的樹,可以看到第一次分裂,婚姻屬性是多路劃分:

{'married': <__main__.Node object at 0x0000000009C1C518>, 'Divorced': <__main__.Node object at 0x0000000009C1CFD0>, 'single': <__main__.Node object at 0x0000000009C1C470>}
{'Yes': <__main__.Node object at 0x0000000009C1CDD8>, 'No': <__main__.Node object at 0x0000000009C1CEB8>}
{'>=80K': <__main__.Node object at 0x0000000009C1CC88>, '<80K': <__main__.Node object at 0x0000000009C1C3C8>}
{}
'Yes'
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章