決策樹的實現相對我這種新手比較難,參考了一篇文章數據挖掘領域十大經典算法之—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'