當前位置:
首頁 > 知識 > 機器學習演算法實踐:決策樹 (Decision Tree)

機器學習演算法實踐:決策樹 (Decision Tree)

(點擊

上方藍字

,快速關注我們)




來源:伯樂在線專欄作者 - iPytLab


如有好文章投稿,請點擊 → 這裡了解詳情




前言




最近打算系統學習下機器學習的基礎演算法,避免眼高手低,決定把常用的機器學習基礎演算法都實現一遍以便加深印象。本文為這系列博客的第一篇,關於決策樹(Decision Tree)的演算法實現,文中我將對決策樹種涉及到的演算法進行總結並附上自己相關的實現代碼。所有演算法代碼以及用於相應模型的訓練的數據都會放到GitHub上(https://github.com/PytLab/MLBox).



本文中我將一步步通過MLiA的隱形眼鏡處方數集構建決策樹並使用Graphviz將決策樹可視化。




正文




決策樹學習




決策樹學習是根據數據的屬性採用樹狀結構建立的一種決策模型,可以用此模型解決分類和回歸問題。常見的演算法包括 CART(Classification And Regression Tree), ID3, C4.5等。我們往往根據數據集來構建一棵決策樹,他的一個重要任務就是為了數據中所蘊含的知識信息,並提取出一系列的規則,這些規則也就是樹結構的創建過程就是機器學習的過程。



決策樹的結構




以下面一個簡單的用於是否買電腦預測的決策樹為例子,樹中的內部節點表示某個屬性,節點引出的分支表示此屬性的所有可能的值,葉子節點表示最終的判斷結果也就是類型。







藉助可視化工具例如Graphviz,matplotlib的註解等等都可以講我們創建的決策樹模型可視化並直接被人理解,這是貝葉斯神經網路等演算法沒有的特性。



決策樹演算法




決策樹演算法主要是指決策樹進行創建中進行樹分裂(劃分數據集)的時候選取最優特徵的演算法,他的主要目的就是要選取一個特徵能夠將分開的數據集盡量的規整,也就是儘可能的純. 最大的原則就是:

將無序的數據變得更加有序




這裡總結下三個常用的方法:






  1. 信息增益(information gain)



  2. 增益比率(gain ratio)



  3. 基尼不純度(Gini impurity)




信息增益 (Information gain)




這裡涉及到了資訊理論中的一些概念:某個事件的信息量,信息熵,信息增益等, 關於事件信息的通俗解釋可以看知乎上的一個回答






  • 某個事件 i 的信息量: 這個事件發生的概率的負對數







  • 信息熵就是平均而言一個事件發生得到的信息量大小,也就是信息量的期望值






任何一個序列都可以獲取這個序列的信息熵,也就是將此序列分類後統計每個類型的概率,再用上述公式計算,使用Python實現如下:





def

get_shanno_entropy

(

self

,

values

)

:


    

""" 根據給定列表中的值計算其Shanno Entropy

    """


    

uniq_vals

=

set

(

values

)


    

val_nums

=

{

key

:

values

.

count

(

key

)

for

key

in

uniq_vals

}


    

probs

=

[

v

/

len

(

values

)

for

k

,

v

in

val_nums

.

items

()]


    

entropy

=

sum

([

-

prob

*

log2

(

prob

)

for

prob

in

probs

])


    

return

entropy




信息增益




我們將一組數據集進行劃分後,數據的信息熵會發生改變,我們可以通過使用信息熵的計算公式分別計算被劃分的子數據集的信息熵並計算他們的平均值(期望值)來作為分割後的數據集的信息熵。新的信息熵的相比未劃分數據的信息熵的減小值便是信息增益了. 這裡我在最初就理解錯了,於是寫出的代碼並不能創建正確的決策樹。




假設我們將數據集D劃分成kk 份D1,D2,…,Dk,則劃分後的信息熵為:





信息增益便是兩個信息熵的差值





在這裡我主要使用信息增益來進行屬性選擇,具體的實現代碼如下:





def

choose_best_split_feature

(

self

,

dataset

,

classes

)

:


    

""" 根據信息增益確定最好的劃分數據的特徵


 


    :param dataset: 待劃分的數據集


    :param classes: 數據集對應的類型


 


    :return: 劃分數據的增益最大的屬性索引


    """


    

base_entropy

=

self

.

get_shanno_entropy

(

classes

)


 


    

feat_num

=

len

(

dataset

[

0

])


    

entropy_gains

=

[]


    

for

i

in

range

(

feat_num

)

:


        

splited_dict

=

self

.

split_dataset

(

dataset

,

classes

,

i

)


        

new_entropy

=

sum

([


            

len

(

sub_classes

)

/

len

(

classes

)

*

self

.

get_shanno_entropy

(

sub_classes

)


            

for

_

,

(

_

,

sub_classes

)

in

splited_dict

.

items

()


        

])


        

entropy_gains

.

append

(

base_entropy

-

new_entropy

)


 


    

return

entropy_gains

.

index

(

max

(

entropy_gains

))




增益比率




增益比率是信息增益方法的一種擴展,是為了克服信息增益帶來的弱泛化的缺陷。因為按照信息增益選擇,總是會傾向於選擇分支多的屬性,這樣會是的每個子集的信息熵最小。例如給每個數據添加一個第一無二的id值特徵,則按照這個id值進行分類是獲得信息增益最大的,這樣每個子集中的信息熵都為0,但是這樣的分類便沒有任何意義,沒有任何泛化能力,類似過擬合。




因此我們可以通過引入一個分裂信息來找到一個更合適的衡量數據劃分的標準,即增益比率。




分裂信息的公式表示為:







當然SplitInfo有可能趨近於0,這個時候增益比率就會變得非常大而不可信,因此有時還需在分母上添加一個平滑函數,具體的可以參考參考部分列出的文章




基尼不純度(Gini impurity)




基尼不純度的定義:





其中m 表示數據集D 中類別的個數, pi 表示某種類型出現的概率。可見當只有一種類型的時候基尼不純度的值為0,此時不純度最低。




針對劃分成k個子數據集的數據集的基尼不純度可以通過如下式子計算:





由此我們可以根據不純度的變化來選取最有的樹分裂屬性







樹分裂




有了選取最佳分裂屬性的演算法,下面我們就需要根據選擇的屬性來將樹進一步的分裂。所謂樹分裂只不過是根據選擇的屬性將數據集劃分,然後在總劃分出來的數據集中再次調用選取屬性的方法選取子數據集的中屬性。實現的最好方式就是遞歸了.




關於用什麼數據結構來表示決策樹,在Python中可以使用字典很方便的表示決策樹的嵌套,一個樹的根節點便是屬性,屬性對應的值又是一個新的字典,其中key為屬性的可能值,value為新的子樹。




下面是我使用Python實現的根據數據集創建決策樹:





def

create_tree

(

self

,

dataset

,

classes

,

feat_names

)

:


    

""" 根據當前數據集遞歸創建決策樹


 


    :param dataset: 數據集


    :param feat_names: 數據集中數據相應的特徵名稱


    :param classes: 數據集中數據相應的類型


 


    :param tree: 以字典形式返回決策樹


    """


    

# 如果數據集中只有一種類型停止樹分裂


    

if

len

(

set

(

classes

))

==

1

:


        

return

classes

[

0

]


 


    

# 如果遍歷完所有特徵,返回比例最多的類型


    

if

len

(

feat_names

)

==

0

:


        

return

get_majority

(

classes

)


 


    

# 分裂創建新的子樹


    

tree

=

{}


    

best_feat_idx

=

self

.

choose_best_split_feature

(

dataset

,

classes

)


    

feature

=

feat_names

[

best_feat_idx

]


    

tree

[

feature

]

=

{}


 


    

# 創建用於遞歸創建子樹的子數據集


    

sub_feat_names

=

feat_names

[

:

]


    

sub_feat_names

.

pop

(

best_feat_idx

)


 


    

splited_dict

=

self

.

split_dataset

(

dataset

,

classes

,

best_feat_idx

)


    

for

feat_val

,

(

sub_dataset

,

sub_classes

)

in

splited_dict

.

items

()

:


        

tree

[

feature

][

feat_val

]

=

self

.

create_tree

(

sub_dataset

,


                                                  

sub_classes

,


                                                  

sub_feat_names

)


    

self

.

tree

=

tree


    

self

.

feat_names

=

feat_names


 


    

return

tree




樹分裂的終止條件有兩個






  • 一個是遍歷完所有的屬性


    可以看到,在進行樹分裂的時候,我們的數據集中的數據向量的長度是不斷縮短的,當縮短到0時,說明數據集已經將所有的屬性用盡,便也分裂不下去了, 這時我們選取最終子數據集中的眾數作為最終的分類結果放到葉子節點上.



  • 另一個是新劃分的數據集中只有一個類型。


    若某個節點所指向的數據集都是同一種類型,那自然沒有必要在分裂下去了即使屬性還沒有遍歷完.




構建一棵決策樹




這我用了一下MLiA書上附帶的隱形眼鏡的數據來生成一棵決策樹,數據中包含了患者眼部狀況以及醫生推薦的隱形眼鏡類型.




首先先導入數據並將數據特徵同類型分開作為訓練數據用於生成決策樹





from

trees

import

DecisionTreeClassifier


 


lense_labels

=

[

"age"

,

"prescript"

,

"astigmatic"

,

"tearRate"

]


X

=

[]


Y

=

[]


 


with

open

(

"lenses.txt"

,

"r"

)

as

f

:


    

for

line

in

f

:


        

comps

=

line

.

strip

().

split

(

" "

)


        

X

.

append

(

comps

[

: -

1

])


        

Y

.

append

(

comps

[

-

1

])




生成決策樹:





clf

=

DecisionTreeClassifier

()


clf

.

create_tree

(

X

,

Y

,

lense_labels

)




查看生成的決策樹:





In

[

2

]

:

clf

.

tree


Out

[

2

]

:


{

"tearRate"

:

{

"normal"

:

{

"astigmatic"

:

{

"no"

:

{

"age"

:

{

"pre"

:

"soft"

,


      

"presbyopic"

:

{

"prescript"

:

{

"hyper"

:

"soft"

,

"myope"

:

"no lenses"

}},


            

"young"

:

"soft"

}},


    

"yes"

:

{

"prescript"

:

{

"hyper"

:

{

"age"

:

{

"pre"

:

"no lenses"

,


                

"presbyopic"

:

"no lenses"

,


                        

"young"

:

"hard"

}},


          

"myope"

:

"hard"

}}}},


  

"reduced"

:

"no lenses"

}}




可視化決策樹




直接通過嵌套字典表示決策樹對人來說不好理解,我們需要藉助可視化工具可視化樹結構,這裡我將使用Graphviz來可視化樹結構。為此實現了講字典表示的樹生成Graphviz Dot文件內容的函數,大致思想就是遞歸獲取整棵樹的所有節點和連接節點的邊然後將這些節點和邊生成Dot格式的字元串寫入文件中並繪圖。




遞歸獲取樹的節點和邊,其中使用了uuid給每個節點添加了id屬性以便將相同屬性的節點區分開.





def

get_nodes_edges

(

self

,

tree

=

None

,

root_node

=

None

)

:


    

""" 返回樹中所有節點和邊


    """


    

Node

=

namedtuple

(

"Node"

,

[

"id"

,

"label"

])


    

Edge

=

namedtuple

(

"Edge"

,

[

"start"

,

"end"

,

"label"

])


 


    

if

tree

is

None

:


        

tree

=

self

.

tree


 


    

if

type

(

tree

)

is

not

dict

:


        

return

[],

[]


 


    

nodes

,

edges

=

[],

[]


 


    

if

root_node

is

None

:


        

label

=

list

(

tree

.

keys

())[

0

]


        

root_node

=

Node

.

_make

([

uuid

.

uuid4

(),

label

])


        

nodes

.

append

(

root_node

)


 


    

for

edge_label

,

sub_tree

in

tree

[

root_node

.

label

].

items

()

:


        

node_label

=

list

(

sub_tree

.

keys

())[

0

]

if

type

(

sub_tree

)

is

dict

else

sub_tree


        

sub_node

=

Node

.

_make

([

uuid

.

uuid4

(),

node_label

])


        

nodes

.

append

(

sub_node

)


 


        

edge

=

Edge

.

_make

([

root_node

,

sub_node

,

edge_label

])


        

edges

.

append

(

edge

)


 


        

sub_nodes

,

sub_edges

=

self

.

get_nodes_edges

(

sub_tree

,

root_node

=

sub_node

)


        

nodes

.

extend

(

sub_nodes

)


        

edges

.

extend

(

sub_edges

)


 


    

return

nodes

,

edges




生成dot文件內容





def

dotify

(

self

,

tree

=

None

)

:


    

""" 獲取樹的Graphviz Dot文件的內容


    """


    

if

tree

is

None

:


        

tree

=

self

.

tree


 


    

content

=

"digraph decision_tree {
"


    

nodes

,

edges

=

self

.

get_nodes_edges

(

tree

)


 


    

for

node

in

nodes

:


        

content

+=

"    "{}" [label="{}"];
"

.

format

(

node

.

id

,

node

.

label

)


 


    

for

edge

in

edges

:


        

start

,

label

,

end

=

edge

.

start

,

edge

.

label

,

edge

.

end


        

content

+=

"    "{}" -> "{}" [label="{}"];
"

.

format

(

start

.

id

,

end

.

id

,

label

)


    

content

+=

"}"


 


    

return

content




隱形眼鏡數據生成Dot文件內容如下:





digraph

decision_tree

{


    

"959b4c0c-1821-446d-94a1-c619c2decfcd"

[

label

=

"call"

];


    

"18665160-b058-437f-9b2e-05df2eb55661"

[

label

=

"to"

];


    

"2eb9860d-d241-45ca-85e6-cbd80fe2ebf7"

[

label

=

"your"

];


    

"bcbcc17c-9e2a-4bd4-a039-6e51fde5f8fd"

[

label

=

"areyouunique"

];


    

"ca091fc7-8a4e-4970-9ec3-485a4628ad29"

[

label

=

"02073162414"

];


    

"aac20872-1aac-499d-b2b5-caf0ef56eff3"

[

label

=

"ham"

];


    

"18aa8685-a6e8-4d76-bad5-ccea922bb14d"

[

label

=

"spam"

];


    

"3f7f30b1-4dbb-4459-9f25-358ad3c6d50b"

[

label

=

"spam"

];


    

"44d1f972-cd97-4636-b6e6-a389bf560656"

[

label

=

"spam"

];


    

"7f3c8562-69b5-47a9-8ee4-898bd4b6b506"

[

label

=

"i"

];


    

"a6f22325-8841-4a81-bc04-4e7485117aa1"

[

label

=

"spam"

];


    

"c181fe42-fd3c-48db-968a-502f8dd462a4"

[

label

=

"ldn"

];


    

"51b9477a-0326-4774-8622-24d1d869a283"

[

label

=

"ham"

];


    

"16f6aecd-c675-4291-867c-6c64d27eb3fc"

[

label

=

"spam"

];


    

"adb05303-813a-4fe0-bf98-c319eb70be48"

[

label

=

"spam"

];


    

"959b4c0c-1821-446d-94a1-c619c2decfcd"

->

"18665160-b058-437f-9b2e-05df2eb55661"

[

label

=

"0"

];


    

"18665160-b058-437f-9b2e-05df2eb55661"

->

"2eb9860d-d241-45ca-85e6-cbd80fe2ebf7"

[

label

=

"0"

];


    

"2eb9860d-d241-45ca-85e6-cbd80fe2ebf7"

->

"bcbcc17c-9e2a-4bd4-a039-6e51fde5f8fd"

[

label

=

"0"

];


    

"bcbcc17c-9e2a-4bd4-a039-6e51fde5f8fd"

->

"ca091fc7-8a4e-4970-9ec3-485a4628ad29"

[

label

=

"0"

];


    

"ca091fc7-8a4e-4970-9ec3-485a4628ad29"

->

"aac20872-1aac-499d-b2b5-caf0ef56eff3"

[

label

=

"0"

];


    

"ca091fc7-8a4e-4970-9ec3-485a4628ad29"

->

"18aa8685-a6e8-4d76-bad5-ccea922bb14d"

[

label

=

"1"

];


    

"bcbcc17c-9e2a-4bd4-a039-6e51fde5f8fd"

->

"3f7f30b1-4dbb-4459-9f25-358ad3c6d50b"

[

label

=

"1"

];


    

"2eb9860d-d241-45ca-85e6-cbd80fe2ebf7"

->

"44d1f972-cd97-4636-b6e6-a389bf560656"

[

label

=

"1"

];


    

"18665160-b058-437f-9b2e-05df2eb55661"

->

"7f3c8562-69b5-47a9-8ee4-898bd4b6b506"

[

label

=

"1"

];


    

"7f3c8562-69b5-47a9-8ee4-898bd4b6b506"

->

"a6f22325-8841-4a81-bc04-4e7485117aa1"

[

label

=

"0"

];


    

"7f3c8562-69b5-47a9-8ee4-898bd4b6b506"

->

"c181fe42-fd3c-48db-968a-502f8dd462a4"

[

label

=

"1"

];


    

"c181fe42-fd3c-48db-968a-502f8dd462a4"

->

"51b9477a-0326-4774-8622-24d1d869a283"

[

label

=

"0"

];


    

"c181fe42-fd3c-48db-968a-502f8dd462a4"

->

"16f6aecd-c675-4291-867c-6c64d27eb3fc"

[

label

=

"1"

];


    

"959b4c0c-1821-446d-94a1-c619c2decfcd"

->

"adb05303-813a-4fe0-bf98-c319eb70be48"

[

label

=

"1"

];


}




這樣我們便可以使用Graphviz將決策樹繪製出來





with

open

(

"lenses.dot"

,

"w"

)

as

f

:


    

dot

=

clf

.

tree

.

dotify

()


    

f

.

write

(

dot

)





dot -Tgif lenses.dot -o lenses.gif




效果如下:







使用生成的決策樹進

行分類




對未知數據進行預測,主要是根據樹中的節點遞歸的找到葉子節點即可。z這裡可以通過為遞歸進行優化,代碼實現如下:





def

classify

(

self

,

data_vect

,

feat_names

=

None

,

tree

=

None

)

:


    

""" 根據構建的決策樹對數據進行分類


    """


    

if

tree

is

None

:


        

tree

=

self

.

tree


 


    

if

feat_names

is

None

:


        

feat_names

=

self

.

feat_names


 


    

# Recursive base case.


    

if

type

(

tree

)

is

not

dict

:


        

return

tree


 


    

feature

=

list

(

tree

.

keys

())[

0

]


    

value

=

data_vect

[

feat_names

.

index

(

feature

)]


    

sub_tree

=

tree

[

feature

][

value

]


 


    

return

self

.

classify

(

feat_names

,

data_vect

,

sub_tree

)




決策樹的存儲




通過字典表示決策樹,這樣我們可以通過內置的pickle或者json模塊將其存儲到硬碟上,同時也可以從硬碟中讀取樹結構,這樣在數據集很大的時候可以節省構建決策樹的時間.





def

dump_tree

(

self

,

filename

,

tree

=

None

)

:


    

""" 存儲決策樹


    """


    

if

tree

is

None

:


        

tree

=

self

.

tree


 


    

with

open

(

filename

,

"w"

)

as

f

:


        

pickle

.

dump

(

tree

,

f

)


 


def

load_tree

(

self

,

filename

)

:


    

""" 載入樹結構


    """


    

with

open

(

filename

,

"r"

)

as

f

:


        

tree

=

pickle

.

load

(

f

)


        

self

.

tree

=

tree


    

return

tree




總結




本文一步步實現了決策樹的實現, 其中使用了ID3演算法確定最佳劃分屬性,並通過Graphviz可視化了構建的決策樹。本文相關的代碼鏈接: https://github.com/PytLab/MLBox/tree/master/decision_tree




參考:






  • 《Machine Learning in Action》



  • 數據挖掘系列(6)決策樹分類演算法




看完本文有收穫?請轉發分享給更多人


關注「大數據與機器學習文摘」,成為Top 1%


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 機器學習 的精彩文章:

讓您的 iOS 應用加入到機器學習的浪潮中
谷歌宣布將使用機器學習來檢測商店裡的惡意程序
分子科學中的機器學習:不會燎原的星星之火?
這 10 本由淺入深的好書,或讓你成為機器學習領域的專家
每月好書:Python機器學習演算法

TAG:機器學習 |

您可能感興趣

Machine Learning:十大機器學習演算法
Facebook開源「Detectron」,用於AR研究的計算機視覺演算法!
ClickHouse如何結合自家的GNDT演算法庫CatBoost來做機器學習
演算法:Sums In A Triangle
人工智慧–Autoencoder演算法
Bayesian Personalized Ranking 演算法解析及Python實現
dijkstra演算法O(n2) 堆優化O(nlogn)
一文讀懂進化演算法Evolutionary Algorithm簡介
Deep Forest 演算法解讀
Adaboost演算法及python實戰
Tile-based Optical Flow 演算法流程與基本思想
Google前AI主管John Giannandrea進入蘋果 改進Siri演算法
最小生成樹prime演算法、kruskal演算法 最短路徑演算法floyd、dijkstra
圖靈獎得主Sivio Micali的Algorand全新的區塊鏈演算法
書評:《演算法之美( Algorithms to Live By )》
用Python 實現的機器人演算法示例集合——PythonRobotics
InfiniteBoosting:集成bagging與boosting的混合演算法
Tamar Charney:重視演算法的力量
利用搖滾樂隊學習TensorFlow,Word2Vec模型和TSNE演算法
AI機器學習-決策樹-python實現CART演算法