常用的溝通工具,比如手機,QQ是和已經認識的朋友交流的工具。現在出現了一大批自詡的約炮神器,非誠勿擾挺好看的,宅男腐女的問題操碎了爸媽的心,世紀佳緣都上市了,看來大家對想認識不認識的人有巨大的熱情。我也聽說非誠勿擾牽手成功後最後走到一起的並不多,這些方式到底靠不靠譜?如果我去參加非誠勿擾,我會不會牽手成功?這裡面是不是有一些規律可循,趁著今天被毀容不能出門,我就來學習一下決策樹。
高中老師隔三差五讓我們填一些表,各種性格和IQ測驗。這些測驗裡很多是十幾道題目,每道題目選擇“是”或者“否”,這就是決策樹的基本原型了。十幾道題目相當於十幾種要考察的因素,每道題目的選擇就是決策樹的一個決策,如果題目一選擇了“是”,那就跳到題目2,如果選擇“否”,那就跳到題目3,決策樹也就是這個樣子,這個因素的判斷調試是否成立,成立了就走成立的分支,不成立就走不成立的分支,這樣下來,決策樹就像是以樹狀形式排列的一系列if-else語句。
if (因素A成立): if (因素B1成立): ... if (因素N11成立): 你可以參加非誠勿擾 else: 你不可以參加非誠勿擾 else: ... if (因素N12成立): 你可以參加非誠勿擾 else: 你不可以參加非誠勿擾else: if (因素B2成立): ... if (因素N21成立): 你可以參加非誠勿擾 else: 你不可以參加非誠勿擾 else: ... if (因素N22成立): 你可以參加非誠勿擾 else: 你不可以參加非誠勿擾
我們的目的就是根據已知的資料,構建出這棵樹。
概念理清楚了,我來一點一點實現這個演算法。
my_data=[['beijing','yes','yes',28,'yes'], ['hebei','no','no',23,'no'], ['henan','yes','no',24,'yes'], ['hubei','no','yes',28,'no'], ['hunan','yes','yes',39,'no']]
上面是我捏造的的資料,下面根據這五條資料來構建決策樹。使用者A,北京人,有房有車,28歲,牽手成功;使用者B,河北人,沒房沒車,23歲,牽手失敗;使用者C,河南人,有房沒車,24歲,牽手成功;使用者D,湖北人,沒房有車,23歲,牽手失敗;使用者E,湖南人,有車有房,39歲,牽手失敗。
第一步:定義節點結構;第二步:對輸入資料分組;第三步:選擇拆分資料的評價模型;第四步:構造決策樹;第五步:用決策樹進行判斷;第六步:剪枝;第七步:資料缺失處理。
第一步:定義節點結構:
每個節點必須表明自己代表的那個考察因素,一個成立或者不成立的判斷條件,一個指向條件成立後的要執行下一個節點,一個指向條件不成立後要判斷的下一個節點。如果是末級葉子節點,還需要一個結果節點,來指明要不要參加非誠勿擾。
class decisionnode: def __init__(self,col=-1,value=None,results=None,tb=None,fb=None): self.col=col self.value=value self.results=results self.tb=tb self.fb=fb
其中,col代表的要考察的因素是輸入資料的那一列;valus就是判斷條件;results是葉子節點儲存最後判斷結果的變數;tb,fb分別是條件value成立指向的分支和條件value不成立指向的分支。
第二步:分組輸入資料:
我們考慮因素有四個,祖籍,房,車和年齡。祖籍是字元型,年齡是整數型,是否有車有房是布爾型;假設我們根據是否有房分組,可以將使用者分為[A,C,E]和 [B,D],如果根據是否有車分組,可以將使用者分為[A,D,E]和 [B,C];如果根據年齡是否大於等於39分組,可分為 [E]和[A,B,C,D];根據年齡是否大於等於28分組,可分為[A,D,E]和 [B,C]; 根據是否是北京人,分為[A]和[B,C,D,E]...
def divideset(rows,column,value): split_function=None if isinstance(value,int) or isinstance(value,float): split_function=lambda row:row[column]>=value else: split_function=lambda row:row[column]==value # Divide the rows into two sets and return them set1=[row for row in rows if split_function(row)] set2=[row for row in rows if not split_function(row)] return (set1,set2)
divideset函數,根據第column列的值是否等於value,或者大於等於value來對輸入rows資料,結果分為兩組,一組符合條件,一組不符合條件
第三步:拆分資料的評價模型:
第二步對資料根據因素的值來分組,每一步根據哪個因素的哪個取值來分組比較好呢?是是否有房還是年齡不能太大呢?有沒有可判斷的依據?有!我們要做的,就是找出合適的因素,使根據該因素divideset後產生的兩組資料集的混雜程度儘可能的小。
先定義個一個小工具函數,這個函數返回rows裡各種可能結果和結果的出現的次數;
def uniquecounts(rows): results={} for row in rows: # The result is the last column r=row[len(row)-1] if r not in results: results[r]=0 results[r]+=1 return results
基尼不純度:將來自集合中某種結果隨機應用於集合中某一資料項目的預期誤差率:
def giniimpurity(rows): total=len(rows) counts=uniquecounts(rows) imp=0 for k1 in counts: p1=float(counts[k1])/total for k2 in counts: if k1==k2: continue p2=float(counts[k2])/total imp+=p1*p2 return imp
該函數利用集合中每一項結果出現的次數除以集合的總行數來計算相應的機率,然後將所有這些機率值的乘積累加起來。這樣就得到某一行資料被隨機分到錯誤結果的總機率。這一機率的值越高,就說明對資料的拆分越不理想。
熵:代表集合的無序程度,也就是集合的混雜程度:
def entropy(rows): from math import log log2=lambda x:log(x)/log(2) results=uniquecounts(rows) # Now calculate the entropy ent=0.0 for r in results.keys(): p=float(results[r])/len(rows) ent=ent-p*log2(p) return ent
這是衡量結果之間差異程度的測度方法,如果所有結果都相同,比如按照某種方式分組後的使用者都有車,那麼熵就是0。群組越是混亂,熵就越高。
第四步:構建決策樹:
構建決策樹的過程,就是找出一個因素,根據這個因素對資料分組,使得分組後資料的熵比根據其他因素分組後的熵都要低。
def buildtree(rows,scoref=entropy): if len(rows)==0: return decisionnode() current_score=scoref(rows) # Set up some variables to track the best criteria best_gain=0.0 best_criteria=None best_sets=None column_count=len(rows[0])-1 for col in range(0,column_count): # Generate the list of different values in # this column column_values={} for row in rows: column_values[row[col]]=1 # Now try dividing the rows up for each value # in this column for value in column_values.keys(): (set1,set2)=divideset(rows,col,value) # Information gain p=float(len(set1))/len(rows) gain=current_score-p*scoref(set1)-(1-p)*scoref(set2) if gain>best_gain and len(set1)>0 and len(set2)>0: best_gain=gain best_criteria=(col,value) best_sets=(set1,set2)
# Create the sub branches if best_gain>0: trueBranch=buildtree(best_sets[0]) falseBranch=buildtree(best_sets[1]) return decisionnode(col=best_criteria[0],value=best_criteria[1], tb=trueBranch,fb=falseBranch) else: return decisionnode(results=uniquecounts(rows))
上面是通過對當前資料集選擇最合適的拆分條件遞迴地實現決策樹的構建。函數接受一個由資料構成的列表作為參數。它遍曆資料集中的每一列(最後一列除外,最後一列用來存放最終結果),針對各列尋找每一種可能的值,並將資料集拆分為兩個子集。通過將每個子集的熵乘以子集中所含資料項目在原資料集中所佔的比重,函數求出了每一對新子集的加權平均熵,並記錄下熵值最低的那一對子集。如果由熵最低的一對子集求得的加權平均熵比當前集合的熵要大,則拆分過程結束,針對各種可能的結果的計數所得將會被儲存起來。否則,演算法就會在新產生的子集上繼續調用buildtree函數,並把調用結果添加到樹上。我們把針對每個子集的調用結果分別附加到節點的True分支和False分支上,最終整棵樹就構造出來啦。。。
第五步:對新資料進行判定:
我們根據已有資料構建好了決策樹,下面就可以對新資料進行判決了:
def classify(observation,tree): if tree.results!=None: return tree.results else: v=observation[tree.col] branch=None if isinstance(v,int) or isinstance(v,float): if v>=tree.value: branch=tree.tb else: branch=tree.fb else: if v==tree.value: branch=tree.tb else: branch=tree.fb return classify(observation,branch)
classfiy以buildtree返回的結果為參數,沿著樹向下遍曆。每次調用後,函數會根據調用結果來判斷是否達到分支的末端。如果尚未達到末端,他會對觀測資料做出評估,以確認列資料是否與參考值匹配。如果匹配,則會再次在True分支上調用classify,否則會在false分支上調用classify函數。當函數到達一個包含結果資訊的節點時,就會對觀測資料給出結果了。
第六步:剪枝
使用第四步的方法構造決策樹可能會過度學習,也就是說,構建的決策樹對過於太針對我們的訓練資料。專門針對訓練資料建立出來的分支,熵值與真實情況相比會有所降低。演算法直到無法再降低熵的時候才會停止分支的建立過程,所以一種可能的辦法是,當熵減少的數量少於某個最小值時,我們就停止分支的建立。這種方法也有一個小小的缺陷,可能某次分支的建立並不會降低很多的熵,但是隨後的建立分支會大幅度降低熵。
我們使用的方法是,先構建好整棵樹,然後嘗試消除多餘的節點,這就是剪枝。剪枝的過程就是對具有相同父節點的一組節點進行檢查,判斷如果將其合并,熵的增加量是否會小於某個閾值。如果確實如此,則這些葉子節點被合并成一個單一的節點。
def prune(tree,mingain): # If the branches aren't leaves, then prune them if tree.tb.results==None: prune(tree.tb,mingain) if tree.fb.results==None: prune(tree.fb,mingain) # If both the subbranches are now leaves, see if they # should merged if tree.tb.results!=None and tree.fb.results!=None: # Build a combined dataset tb,fb=[],[] for v,c in tree.tb.results.items(): tb+=[[v]]*c for v,c in tree.fb.results.items(): fb+=[[v]]*c # Test the reduction in entropy delta=entropy(tb+fb)-(entropy(tb)+entropy(fb)/2) if delta<mingain: # Merge the branches tree.tb,tree.fb=None,None tree.results=uniquecounts(tb+fb)
第七步:資料缺失處理
我們的訓練資料只有五個省的使用者,如果判斷一個祖籍是安徽的使用者呢?安徽不在我們的訓練集類,決策樹也就不會有安慰的資訊。如果我們缺失了一些資訊,而這個資訊是確定分支走向所必須的,那我們可以選擇兩個分支都走。不過不是平均地統計各分支對應的結果值,而是對其加權統計。
def mdclassify(observation,tree): if tree.results!=None: return tree.results else: v=observation[tree.col] if v==None: tr,fr=mdclassify(observation,tree.tb),mdclassify(observation,tree.fb) tcount=sum(tr.values()) fcount=sum(fr.values()) tw=float(tcount)/(tcount+fcount) fw=float(fcount)/(tcount+fcount) result={} for k,v in tr.items(): result[k]=v*tw for k,v in fr.items(): result[k]=v*fw return result else: if isinstance(v,int) or isinstance(v,float): if v>=tree.value: branch=tree.tb else: branch=tree.fb else: if v==tree.value: branch=tree.tb else: branch=tree.fb return mdclassify(observation,branch)
好了,這個決策樹的部分就到這裡結束了。
看看我實驗的結果
>>> tree=treepredict.buildtree(treepredict.my_data)>>> treepredict.mdclassify(['beijing', 'yes', 'yes', 22], tree){'yes': 2}>>> treepredict.mdclassify(['beijing', 'no', 'no', 22], tree){'no': 2}>>> treepredict.mdclassify(['beijing', 'no', 'no', 30], tree){'no': 2}>>> treepredict.mdclassify(['beijing', 'yes', 'no', 30], tree){'yes': 2}>>> treepredict.mdclassify(['anwei', 'yes', 'no', 30], tree){'yes': 2}>>> treepredict.mdclassify(['anwei', 'no', 'yes', 30], tree){'no': 2}>>> treepredict.mdclassify(['beijing', 'no', 'yes', 30], tree){'no': 2}>>> treepredict.mdclassify(['beijing', 'no', 'yes', 20], tree){'no': 2}>>> treepredict.mdclassify(['beijing', 'yes', 'no', 40], tree)
將構建的決策樹列印出來是這樣的
1:yes?T-> 0:hunan? T-> {'no': 1} F-> {'yes': 2}F-> {'no': 2}
解釋一下就是這樣的:
是否有房:有:是否武漢人:是:不要參加非誠勿擾不是:參加非誠勿擾無:不要去參加分成勿擾