python機器學習案例系列教程——決策樹(ID3、C4.5、CART)

全棧工程師開發手冊 (作者:欒鵬)

python數據挖掘系列教程

決策樹簡介

決策樹算是最好理解的分類器了。決策樹就是一個多層if-else函數,就是對對象屬性進行多層if-else判斷,獲取目標屬性(類標籤)的類別。由於只使用if-else對特徵屬性進行判斷,所以一般特徵屬性爲離散值,即使爲連續值也會先進行區間離散化。

這裏寫圖片描述

在機器學習中,決策樹是一個預測模型,他代表的是對象屬性與類別屬性之間的一種映射關係。

分類決策樹概念:是一種描述對實例進行分類的樹形結構。決策樹由結點和有向邊組成。結點有兩種類型:內部結點和葉結點。內部結點表示一個特徵或屬性,葉結點表示一個分類。

思考:選哪些特徵屬性參與決策樹建模、哪些屬性排在決策樹的頂部,哪些排在底部,對屬性的值該進行什麼樣的判斷、樣本屬性的值缺失怎麼辦、如果輸出不是分類而是數值能用麼、對決策沒有用或沒有多大幫助的屬性怎麼辦、什麼時候使用決策樹?

使用決策樹做預測需要以下過程:

1、收集數據:可以使用任何方法。比如想構建一個相親系統,我們可以從媒婆那裏,或者通過參訪相親對象獲取數據。根據他們考慮的因素和最終的選擇結果,就可以得到一些供我們利用的數據了。

2、準備數據:收集完的數據,我們要進行整理,將這些所有收集的信息按照一定規則整理出來,並排版,方便我們進行後續處理。

3、分析數據:可以使用任何方法,決策樹構造完成之後,我們可以檢查決策樹圖形是否符合預期。

4、訓練算法:這個過程也就是構造決策樹,同樣也可以說是決策樹學習,就是構造一個決策樹的數據結構。

5、測試算法:使用經驗樹計算錯誤率。當錯誤率達到了可接收範圍,這個決策樹就可以投放使用了。

信息增益

信息增益表示得知特徵XjXj的信息而使所屬分類的不確定性減少的程度。

特徵AA
假設數據集D有K種分類,特徵A有n種取值可能。

其中數據集D的經驗熵H(D)H(D)

H(D)=k=1KPklogPk2H(D)=−∑k=1KPklog2Pk
爲集合D中的任一樣本數據分類k的概率,或者說屬於分類k的樣本所佔的比例。

經驗條件熵H(D|A)H(D|A)

H(D|A)=i=1nPiH(Di)H(D|A)=∑i=1nPiH(Di)
爲特徵A爲第i個可取值的樣本集合。

信息增益比

特徵AA之比。

gR(D,A)=g(D,A)/H(D)gR(D,A)=g(D,A)/H(D)

是爲了矯正在訓練數據集的經驗熵大時,信息增益值會偏大,反之,信息增益值會偏小的問題。

決策樹ID3算法的思路

ID3算法就是用信息增益大小來判斷當前節點應該用什麼特徵來構建決策樹,用計算出的信息增益最大的特徵來建立決策樹的當前節點。

不足:

ID3算法雖然提出了新思路,但是還是有很多值得改進的地方。  

a)ID3沒有考慮連續特徵,比如長度,密度都是連續值,無法在ID3運用。這大大限制了ID3的用途。

b)ID3採用信息增益大的特徵優先建立決策樹的節點。很快就被人發現,在相同條件下,取值比較多的特徵比取值少的特徵信息增益大。比如一個變量有2個值,各爲1/2,另一個變量爲3個值,各爲1/3,其實他們都是完全不確定的變量,但是取3個值的比取2個值的信息增益大。如果校正這個問題呢?

c) ID3算法對於缺失值的情況沒有做考慮

d) 沒有考慮過擬合的問題

ID3 算法的作者昆蘭基於上述不足,對ID3算法做了改進,這就是C4.5算法

決策樹C4.5算法

上一節我們講到ID3算法有四個主要的不足,一是不能處理連續特徵,第二個就是用信息增益作爲標準容易偏向於取值較多的特徵,最後兩個是缺失值處理的問和過擬合問題。昆蘭在C4.5算法中改進了上述4個問題。

對於第一個問題,不能處理連續特徵, C4.5的思路是將連續的特徵離散化。
對於第二個問題,信息增益作爲標準容易偏向於取值較多的特徵的問題。我們引入一個信息增益比的變量IR(X,Y)IR(X,Y)IR(X,Y)IR(X,Y),它是信息增益和特徵熵的比值。表達式如下:

IR(D,A)=I(A,D)HA(D)IR(D,A)=I(A,D)HA(D)

其中D爲樣本特徵輸出的集合,A爲樣本特徵,對於特徵熵HA(D)HA(D), 表達式如下:

HA(D)=ni|Di||D|log2|Di||D|HA(D)=−∑in|Di||D|log2|Di||D|

其中n爲特徵A的類別數, |Di||Di|爲特徵A取第i個值時對應的樣本個數。|D|爲總樣本個數。

對於第三個缺失值處理的問題,主要需要解決的是兩個問題,一是在樣本某些特徵缺失的情況下選擇劃分的屬性,二是選定了劃分屬性,對於在該屬性上缺失特徵的樣本的處理。

對於第一個子問題,對於某一個有缺失特徵值的特徵A。C4.5的思路是將數據分成兩部分,對每個樣本設置一個權重(初始可以都爲1),然後劃分數據,一部分是有特徵值A的數據D1,另一部分是沒有特徵A的數據D2. 然後對於沒有缺失特徵A的數據集D1來和對應的A特徵的各個特徵值一起計算加權重後的信息增益比,最後乘上一個係數,這個係數是無特徵A缺失的樣本加權後所佔加權總樣本的比例。

對於第二個子問題,可以將缺失特徵的樣本同時劃分入所有的子節點,不過將該樣本的權重按各個子節點樣本的數量比例來分配。比如缺失特徵A的樣本a之前權重爲1,特徵A有3個特徵值A1,A2,A3。 3個特徵值對應的無缺失A特徵的樣本個數爲2,3,4.則a同時劃分入A1,A2,A3。對應權重調節爲2/9,3/9, 4/9。

對於第4個問題,C4.5引入了正則化係數進行初步的剪枝。具體方法這裏不討論。下篇講CART的時候會詳細討論剪枝的思路。

不足

1)由於決策樹算法非常容易過擬合,因此對於生成的決策樹必須要進行剪枝。剪枝的算法有非常多,C4.5的剪枝方法有優化的空間。思路主要是兩種,一種是預剪枝,即在生成決策樹的時候就決定是否剪枝。另一個是後剪枝,即先生成決策樹,再通過交叉驗證來剪枝。後面在下篇講CART樹的時候我們會專門講決策樹的減枝思路,主要採用的是後剪枝加上交叉驗證選擇最合適的決策樹。

2)C4.5生成的是多叉樹,即一個父節點可以有多個節點。很多時候,在計算機中二叉樹模型會比多叉樹運算效率高。如果採用二叉樹,可以提高效率。

3)C4.5只能用於分類,如果能將決策樹用於迴歸的話可以擴大它的使用範圍。

4)C4.5由於使用了熵模型,裏面有大量的耗時的對數運算,如果是連續值還有大量的排序運算。如果能夠加以模型簡化可以減少運算強度但又不犧牲太多準確性的話,那就更好了。

CART分類樹算法

CART生成二叉決策樹

我們知道,在ID3算法中我們使用了信息增益來選擇特徵,信息增益大的優先選擇。在C4.5算法中,採用了信息增益比來選擇特徵,以減少信息增益容易選擇特徵值多的特徵的問題。但是無論是ID3還是C4.5,都是基於信息論的熵模型的,這裏面會涉及大量的對數運算。能不能簡化模型同時也不至於完全丟失熵模型的優點呢?

CART分類樹算法使用基尼係數來代替信息增益比,基尼係數代表了模型的不純度,基尼係數越小,則不純度越低,特徵越好。這和信息增益(比)是相反的。

子集計算基尼不純度,即隨機放置的數據項出現於錯誤分類中的概率。以此來評判屬性對分類的重要程度。

Gini(D)=k=1Kpk(1pk)=1p2kGini(D)=∑k=1Kpk(1−pk)=1−∑pk2
爲任一樣本點屬於第k類的概率,也可以說成樣本數據集中屬於k類的樣本的比例。

集合D的基尼指數爲Gini(D)Gini(D)

其中 |Di||Di|爲特徵A取第i個值時對應的樣本個數。|D|爲總樣本個數

CART算法中對於分類樹採用的是上述的基尼指數最小化準則。對於迴歸樹,CART採用的是平方誤差最小化準則。

CART樹算法的剪枝

由於決策時算法很容易對訓練集過擬合,而導致泛化能力差,爲了解決這個問題,我們需要對CART樹進行剪枝,即類似於線性迴歸的正則化,來增加決策樹的返回能力。但是,有很多的剪枝方法,我們應該這麼選擇呢?

CART採用的辦法是後剪枝法,即先生成決策樹,然後產生所有(是所有的,也就是形成了子樹序列)可能的剪枝後的CART樹,然後使用交叉驗證來檢驗各種剪枝的效果,選擇泛化能力最好的剪枝策略。

那麼CART算法是如何產生一批剪枝樹的呢?

剪枝操作中的損失函數爲:

損失函數=擬合度+a*模型複雜度

當a=0時,整體樹爲最優樹,當a爲無窮大時,只有根節點的樹爲最優樹。a逐漸增大,樹逐漸剪值。若a1<a2a1<a2,則a1對應的最優樹一定整體包含a2對應的最優樹。也就是說最優樹是嵌套的。

CART算法中對於子樹T的損失函數爲

Ca(T)=C(T)+a|T|Ca(T)=C(T)+a|T|
爲樹的葉子節點個數,用來代表模型複雜度。

樹中每個節點tt下的樹如果被減去,則整體損失函數減少的程度爲

g(t)=C(t)C(Tt)|Tt|1g(t)=C(t)−C(Tt)|Tt|−1
的葉子節點個數。

在整體樹T0T0,如此遞歸,直到只有根節點,這樣就按找到了一個按順序的子樹序列。

最後通過交叉驗證得到最優的一個子樹,作爲真正剪枝後的樹。

案例

應用場景:

本文使用決策樹對web站點的用戶在線瀏覽行爲及最終購買行爲(選擇的服務類型或者用戶類型)進行預測。

每個用於的在線瀏覽行爲信息包括:每個用戶的來源網站、用戶的ip位置、是否閱讀FAQ、瀏覽網頁數目。

目標分類爲用戶類型:遊客、基本用戶、高級用戶

在建立決策樹時,我們先要懂得一個概念,叫屬性的分類重要性,就是某個屬性的出現,對目標結果能帶來多大的信息。屬性的重要程度是根據樣本數據集使用該屬性進行劃分子集後,集合的純度增加了多少來決定。

我們以用戶在線瀏覽信息爲例,如果閱讀過FAQ的用戶全部都是高級用戶,沒有閱讀過FAQ的都是基本用戶,則是否閱讀過FAQ這個屬性就非常重要。因爲通過FAQ屬性劃分子集後,產生的兩個子集,非常“純”。

決策樹的建立過程是先找出最重要的分類屬性,再找出第二重要的分類屬性。以此建立樹的層次。

算法 支持模型 樹結構 特徵選擇 連續值處理 缺失值處理 剪枝
ID3 分類 多叉樹 信息增益 不支持 不支持 不支持
C4.5 分類 多叉樹 信息增益比 支持 支持 支持
CART 分類,迴歸 二叉樹 基尼係數,均方差 支持 支持 支持

構建數據集

讀者可以自己去獲取自家公司的用戶行爲記錄,這裏給出一個簡單的數據集

下面的數據爲每個用戶的來源網站、位置、是否閱讀FAQ、瀏覽網頁數目、以及用戶類型(None爲遊客,Basic爲基本用戶,Premium爲高級用戶)。最後一列屬性爲用戶類型,就是我們想要預測的分類結果

my_data=[['slashdot','USA','yes',18,'None'],
         ['google','France','yes',23,'Premium'],
         ['digg','USA','yes',24,'Basic'],
         ['kiwitobes','France','yes',23,'Basic'],
         ['google','UK','no',21,'Premium'],
         ['(direct)','New Zealand','no',12,'None'],
         ['(direct)','UK','no',21,'Basic'],
         ['google','USA','no',24,'Premium'],
         ['slashdot','France','yes',19,'None'],
         ['digg','USA','no',18,'None'],
         ['google','UK','no',18,'None'],
         ['kiwitobes','UK','no',19,'None'],
         ['digg','New Zealand','yes',12,'Basic'],
         ['slashdot','UK','no',21,'None'],
         ['google','UK','yes',18,'Basic'],
         ['kiwitobes','France','yes',19,'Basic']]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

決策樹前的準備工作

有了樣本數據集,我們就可以用來構建決策樹了。在構建決策樹的過程中首先我們要對決策樹上的點創建類,然後要能夠根據屬性劃分成多個子數據集,還要能計算劃分子數據集後的信息增益,用信息增益來判斷這個屬性的重要性,然後選擇最重要的屬性建第一層樹。

1、爲決策樹上的點創建類

# 建立決策樹上的節點類
class decisionnode:
    def __init__(self,col=-1,value=None,results=None,tb=None,fb=None):
        self.col=col                #待檢測條件所屬的列索引。即當前是對第幾列數據進行分類
        self.value=value            #爲使結果爲true,當前列必須匹配的值
        self.results=results        #如果當前節點時葉節點,表示該節點的結果值,如果不是葉節點,爲None
        self.tb=tb                  #判斷條件爲true後的子節點
        self.fb=fb                  #判斷調節爲false後的子節點
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2、數據集劃分功能

這裏我們完成根據屬性劃分數據集的功能。一般對於字符串型的數據,我們將數據集分成等於和不等於兩個子集。對數值型屬性,我們分成大於和小於兩個子集。

需要注意的是,在樣本數據集中,每一行爲一個對象,每一列爲一種屬性。樣本數據集以矩陣的形式存在。

# 根據某一屬性對數據集合進行拆分,能夠處理數值型數據或名詞性數據。其實決策樹只能處理離散型數據,對於連續性數據也是劃分爲範圍區間塊
# rows樣本數據集,column要匹配的屬性列索引,value指定列上的數據要匹配的值
def divideset(rows,column_index,column_value):
    # 定義一個函數,令其告訴我們數據行屬於第一組(返回值爲true)還是第二組(返回值false)
    split_function=None
    if isinstance(column_value,int) or isinstance(column_value,float):
        split_function=lambda row:row[column_index]>=column_value   #按大於、小於區分
    else:
        split_function=lambda row:row[column_index]==column_value   #按等於、不等於區分

    # 將數據集拆分成兩個子集,並返回
    set1=[row for row in rows if split_function(row)]
    set2=[row for row in rows if not split_function(row)]
    return (set1,set2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

信息增益的計算

完成了拆分子集,就可以計算拆分後的信息增益了。在計算信息增益時,我們不適用純度,而使用凌亂度,意義相同。凌亂度越大越不好。

原樣本集劃分成多個子集後,每個子集的凌亂度我們可以由基尼不純度的大小或熵的大小來代替。

那一個原數據集和多個子集之間如何比較凌亂度呢。這除了和每個子集的凌亂度有關外,還與子集的大小有關。因爲如果其中一個子集樣本數非常少,即使凌亂度非常低也不能代表什麼。

劃分後的多個子集的總體凌亂度我們設定爲m=p1*m1+p2*m2+p3*m3。其中p1、p2、p3爲每個子集所佔的比例,m1、m2、m3爲每個子集的凌亂度。

用沒有劃分子集前的原樣本數據集的凌亂度減去多個子集的總體凌亂度,就是信息增益。信息增益越大,證明該屬性對分類的重要程度越大。

這裏每個子集使用基尼不純度或熵來代表凌亂度。

1、基尼不純度

子集計算基尼不純度,即隨機放置的數據項出現於錯誤分類中的概率。以此來評判屬性對分類的重要程度。

Gini(p)=pk(1pk)=1p2kGini(p)=∑pk(1−pk)=1−∑pk2

換句話說,就是統計集合中所有分類的概率兩兩積的和。

比如下面的rows是通過一個屬性劃分的一個子集。我們肯定希望這個子集中儘可能都是同一種分類。這樣這個子集的純度才能夠高。如何看純度夠不夠高呢。

比如這個集合有包含了3個分類,分類1的比例是0.1,分類2的比例是0.1,分類3的比例是0.8。這3個比例的兩兩積的和爲0.1*0.1+0.1*0.8+0.1*0.8 = 0.17

而如果這三個分類中,分類1的比例爲0.3,分類2的比例爲0.3,分類3的比例爲0.4,則這3個比例的兩兩積的和爲0.3*0.3+0.3*0.4+0.3*0.4=0.33,是不是比上面的大,表明純度小。

爲什麼會存在這種大小關係呢?自己百度吧。

#rows樣本數據集
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

# 統計集合rows中每種分類的樣本數目。(樣本數據每一行數據的最後一列記錄了分類結果)。rows樣本數據
def uniquecounts(rows):
    results={}
    for row in rows:
        # 目標結果在樣本數據最後一列
        r=row[len(row)-1]
        if r not in results: results[r]=0
        results[r]+=1
    return results
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

2、熵

熵,即遍歷所有可能的結果之後得到的p(x)log(p(x))之和。也可以以此評判屬性對分類的重要程度。

#rows樣本數據集
def entropy(rows):
    from math import log
    log2=lambda x:log(x)/log(2)
    results=uniquecounts(rows)
    # 此處開始計算熵的值
    ent=0.0
    for r in results.keys():
        p=float(results[r])/len(rows)
        ent=ent-p*log2(p)
    return ent

# 對各種可能的目標結果(選擇的服務類型)進行計數(樣本數據每一行數據的最後一列記錄了目標結果)。rows樣本數據
def uniquecounts(rows):
    results={}
    for row in rows:
        # 目標結果在樣本數據最後一列
        r=row[len(row)-1]
        if r not in results: results[r]=0
        results[r]+=1
    return results
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

構建決策樹

當能判斷哪個屬性對分類更重要我們就可以構建決策樹了,先選出最重要的屬性,作爲根節點,再將數據集劃分成兩個子集,並分別在子集中選出其次重要的屬性,做爲左右子樹的根節點,並以此遞推下去。

通過每一次的屬性查詢,我們就知道了該找那個屬性作爲節點,以及該如果進行左右子樹的劃分。

buildtree函數輸入爲樣本數據集,輸出爲決策樹。

所謂的輸出爲決策樹,就是輸出的爲根節點。根節點就表示決策樹。

# 構建決策樹.scoref爲信息增益的計算函數
def buildtree(rows,scoref=entropy):
    if len(rows)==0: return decisionnode()
    current_score=scoref(rows)

    # 定義一些變量以記錄最佳拆分條件
    best_gain=0.0
    best_criteria=None
    best_sets=None

    column_count=len(rows[0])-1
    for col in range(0,column_count):    #遍歷每一列(除最後一列,因爲最後一列是目標結果)
        # 在當前列中生成一個由不同值構成的序列
        column_values={}
        for row in rows:
            column_values[row[col]]=1
        # 接下來根據這一列中的每個值,嘗試對數據集進行拆分
        for value in column_values.keys():
            (set1,set2)=divideset(rows,col,value)

            # 計算信息增益
            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)
    # 創建子分支
    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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

繪製決策樹

from PIL import Image, ImageDraw

# 獲取樹的顯示寬度
def getwidth(tree):
    if tree.tb==None and tree.fb==None: return 1
    return getwidth(tree.tb)+getwidth(tree.fb)

# 獲取樹的顯示深度(高度)
def getdepth(tree):
    if tree.tb==None and tree.fb==None: return 0
    return max(getdepth(tree.tb),getdepth(tree.fb))+1

# 繪製樹形圖
def drawtree(tree,jpeg='tree.jpg'):
    w=getwidth(tree)*100
    h=getdepth(tree)*100+120

    img=Image.new('RGB',(w,h),(255,255,255))
    draw=ImageDraw.Draw(img)

    drawnode(draw,tree,w/2,20)  #根節點座標
    img.save(jpeg,'JPEG')

# 迭代畫樹的節點
def drawnode(draw,tree,x,y):
    if tree.results==None:
        # 得到每個分支的寬度
        w1=getwidth(tree.fb)*100
        w2=getwidth(tree.tb)*100

        # 確定此節點所要佔據的總空間
        left=x-(w1+w2)/2
        right=x+(w1+w2)/2

        # 繪製判斷條件字符串
        draw.text((x-20,y-10),str(tree.col)+':'+str(tree.value),(0,0,0))

        # 繪製到分支的連線
        draw.line((x,y,left+w1/2,y+100),fill=(255,0,0))
        draw.line((x,y,right-w2/2,y+100),fill=(255,0,0))

        # 繪製分支的節點
        drawnode(draw,tree.fb,left+w1/2,y+100)
        drawnode(draw,tree.tb,right-w2/2,y+100)
    else:
        txt=' \n'.join(['%s:%d'%v for v in tree.results.items()])
        draw.text((x-20,y),txt,(0,0,0))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

這裏寫圖片描述

使用決策樹對新的待測數據進行分類

# 對新的觀測數據進行分類。observation爲觀測數據。tree爲建立好的決策樹
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

剪枝操作

在決策樹創建時,由於數據中的噪聲和離羣點,許多分支反應的是訓練數據中的異常,或者構建決策樹時選取的閾值較小,造成構造的決策樹特別複雜。這些都導致決策樹對訓練數據的分類效果很好,但是對未知數據的分類效果不理想,也就是過擬合現象。我們通過剪支方法處理這種過擬合數據問題。通常,這種方法使用統計量剪掉最不可靠的分支,一顆未剪枝的樹和剪支的樹對比如圖。剪支後樹更小,更簡單,判斷更快更好。

如圖中,比如屬性A1的值爲no時正常情況都應該屬於分類B,但是由於噪聲原因,存在了幾個屬性A1的值爲no的,但是屬於分類A的對象。所以在決策樹時就會多出一條分支。

這裏寫圖片描述

剪支分爲先剪支和後剪支。後剪支更常用,就是先完整的構建一個樹,再通過刪除節點的分支並用樹葉替換它而剪掉給定節點上的子樹。

剪支的過程就是對具有相同父節點的一組節點進行檢查,判斷如果將其合併,熵的增加量是否會小於某個指定的閾值。如果確實如此,則這些葉節點會被合併成一個單一的節點,合併後的新節點包含了所有可能的結果值。這種做法有助於避免過度擬合的情況,也使得根據決策樹做出的預測結果,不至於比從數據集中得到的實際結論還要特殊。

CART樹的剪枝算法中使用的是用交叉驗證來檢驗剪枝後的預測能力,選擇泛化預測能力最好的剪枝後的數作爲最終的CART樹。

# 決策樹剪枝。(因爲有些屬性的分類產生的熵值的差太小,沒有區分的必要),mingain爲門限。
# 爲了避免遇到大小臺階的問題(子樹分支的屬性比較重要),所以採取先建樹,再剪支的方式
def prune(tree,mingain):
    # 如果分支不是葉節點,則對其進行剪枝操作
    if tree.tb.results==None:
        prune(tree.tb,mingain)
    if tree.fb.results==None:
        prune(tree.fb,mingain)

    # 如果兩個自分支都是葉節點,則判斷他們是否需要合併
    if tree.tb.results!=None and tree.fb.results!=None:
        # 構建合併後的數據集
        tb,fb=[],[]
        for v,c in tree.tb.results.items():
            tb+=[[v]]*c
        for v,c in tree.fb.results.items():
            fb+=[[v]]*c

        # 檢查熵的減少情況
        delta=entropy(tb+fb)-(entropy(tb)+entropy(fb)/2)

        if delta<mingain:
            # 合併分支
            tree.tb,tree.fb=None,None
            tree.results=uniquecounts(tb+fb)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

對於缺失數據的情況

對包含缺失數據的新的待測數據進行分類。會在逐層分類到缺失屬性層時,不知道該往哪個方向繼續判斷。這時可以支同時查詢每個分支,這樣就有多個最終分類結果。最後計算多個分類的結果的加權結果。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

對於數值型輸出結果

如果輸出結果不是分類而是數字,可以使用方差作爲評價函數來取代熵或基尼不純度

#計算數據集的統計方差
def variance(rows):
    if len(rows)==0: return 0
    data=[float(row[len(row)-1]) for row in rows]
    mean=sum(data)/len(data)
    variance=sum([(d-mean)**2 for d in data])/len(data)
    return variance
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

測試代碼

if __name__=='__main__':  #只有在執行當前模塊時纔會運行此函數
    tree = buildtree(my_data)   #創建決策樹
    printtree(tree)
    drawtree(tree,jpeg='treeview.jpg')  #畫樹形圖
    prune(tree,0.1)   #剪支
    result = mdclassify(['google',None,'yes',None],tree)   #使用決策樹進行預測,新數據包含缺失數據
    print(result)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

決策樹的內容到這裏就講完了。下面我們來總結一下。

決策樹優點

1、決策樹易於理解和實現,人們在在學習過程中不需要使用者瞭解很多的背景知識,這同時是它的能夠直接體現數據的特點,只要通過解釋後都有能力去理解決策樹所表達的意義。

2、對於決策樹,數據的準備往往是簡單或者是不必要的,而且能夠同時處理數據型和常規型屬性,在相對短的時間內能夠對大型數據源做出可行且效果良好的結果。

3、易於通過靜態測試來對模型進行評測,可以測定模型可信度;如果給定一個觀察的模型,那麼根據所產生的決策樹很容易推出相應的邏輯表達式。

決策樹缺點

1)對連續性的字段比較難預測。

2)對有時間順序的數據,需要很多預處理的工作。

3)當類別太多時,錯誤可能就會增加的比較快。

4)一般的算法分類的時候,只是根據一個字段來分類。

Regression Decision Tree:迴歸樹

迴歸樹總體流程類似於分類樹,區別在於,迴歸樹的每一個節點都會得一個預測值,以年齡爲例,該預測值等於屬於這個節點的所有人年齡的平均值。分枝時窮舉每一個feature的每個閾值找最好的分割點,但衡量最好的標準不再是最大熵,而是最小化平方誤差。也就是被預測出錯的人數越多,錯的越離譜,平方誤差就越大,通過最小化平方誤差能夠找到最可靠的分枝依據。分枝直到每個葉子節點上人的年齡都唯一或者達到預設的終止條件(如葉子個數上限),若最終葉子節點上人的年齡不唯一,則以該節點上所有人的平均年齡做爲該葉子節點的預測年齡。

這裏寫圖片描述

這裏寫圖片描述

思考:選哪些特徵屬性參與決策樹建模、哪些屬性排在決策樹的頂部,哪些排在底部,對屬性的值該進行什麼樣的判斷、樣本屬性的值缺失怎麼辦、如果輸出不是分類而是數值能用麼、對決策沒有用或沒有多大幫助的屬性怎麼辦、什麼時候使用決策樹?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章