決策樹

決策樹例子-是否打籃球

1.什麼是決策樹?

1. 決策樹是一種自上而下,對樣本數據進行樹形分類的過程,由節點和有向邊組成。
2. 決策樹作爲最基礎、最常見的有監督學習模型,常被用於分類問題和迴歸問題,
3. 在市場營銷和生物醫藥等領域尤其受歡迎,主要是因爲樹形結構與銷售、診斷等場景下的決策過程非常相似,
4. 決策樹具有簡單直觀、解釋性強的優點。

2.決策樹有哪些常用的啓發函數?

一般而言,決策樹的生成包含了特徵選擇、樹的構造、樹的剪枝三個過程。
從若干不同的決策樹中選取最優的決策樹是一個NP完全問題,
在實際中我們通常會採用啓發式學習的方法去構建一顆滿足啓發式條件的決策樹。
常用的決策樹算法有:ID3、C4.5、CART,除了構建準則之外,它們之間的區別和聯繫是什麼?

2.1 ——ID3-最大信息增益

  • 經驗熵
    對於樣本集和DD,類別數爲KK,數據集DD的經驗熵表示爲

    H(D)=k=1KCkDlog2CkDH(D)=-\sum_{k=1}^{K}\frac{ |C_{k}|}{|D|} log_{2}\frac{|C_{k}|}{|D|}
    其中,CkC_{k}是樣本集合DD中屬於第kk類的樣本子集,Ck|C_{k}|表示爲該子集的元素個數,D|D|表示樣本集和的元素個數。

  • 經驗條件熵
    某個特徵AA對於數據集DD的經驗條件熵H(DA)H(D|A)
    H(DA)=i=1kDiDH(Di)=i=1nDiDk=1kDikDilog2DikDi H(D|A)= \sum_{i=1}^{k} \frac{|D_{i}|}{|D|}H(D_{i}) = \sum_{i=1}^{n} \frac{|D_{i}|}{|D|} \lgroup -\sum_{k=1}^{k}\frac{ |D_{ik}|}{|D_{i}|} log_{2}\frac{|D_{ik}|}{|D_{i}|} \rgroup
    其中,DiD_{i}表示DD中特徵AA取第ii個值的樣本子集,DikD_{ik}表示DiD_{i}中屬於kk類的樣本子集。

  • 信息增益
    信息增益表示爲兩者之差,可得
    g(D,A)=H(D)H(DA)g(D,A)=H(D)-H(D|A)

2.2 ——最大信息增益比

  • 特徵AA對於數據集DD的信息增益比定義爲
    gR(D,A)=g(D,A)HA(D) g_{R}(D,A) = \frac{g(D,A)}{H_{A}(D)}
    其中,HA(D)=i=1nlog2DiDH_{A}(D)=-\sum_{i=1}^{n}log_{2} \frac{|D_{i}|}{|D|}
    稱爲數據集DD關於AA的取值熵。

2.3 ——CART-最大基尼指數(Gini)

  • Gini描述的是數據的純度,與信息熵含義類似
    Gini(D)=1k=1nCkD2 Gini(D) = 1-\sum_{k=1}^{n} \lgroup \frac {|C_{k}|} {|D|} \rgroup^2
    CART在每一次迭代中選擇基尼指數最小的特徵及其對應的切分點進行分類。但與ID3、C4.5不同的是,CART是一顆二叉樹,採用二元切割法,每一步將數據按特徵AA的取值分成兩份,分別進入左右子樹,特徵AA的Gini指數定義爲
    Gini(DA)=i=1nDiDGini(Di) Gini(D|A)=\sum_{i=1}^{n}\frac {|D_{i}|}{|D|} Gini(D_{i})

2.4——三者之間的對比差異

  1. ID3採用信息增益作爲評價標準,會傾向選擇取值較多的特徵,信息增益反映的是給定條件以後不確定性減少的長度,特徵取值越多就意味着確定性更高,也就是條件熵越小,信息增益越大。C4.5實際上是對ID3進行優化。
  2. ID3只能處理離散型變量,而C4.5和CART還能處理連續型變量。
  3. ID3和C4.5只能用於分類任務,而CART也可以應用迴歸任務(迴歸任務使用最小平方誤差準則)
  4. ID3對樣本缺失值比較敏感,而C4.5和CART可以對缺失值進行不同方式的處理
  5. ID3和C4.5可以在每個節點產生出多叉分支,且每個特徵在層級之間不會複用,而CART在每個節點只會產生兩個分支,因此最後形成一顆二叉樹,且每個特徵可以被重複利用
  6. ID3和C4.5通過剪枝來權衡樹的準確性和泛化能力,而CART直接利用全部數據發現發現所有可能的樹結構進行對比。
# 實現代碼
import numpy as np
import pandas as pd
from collections import Counter
import math


class Node:
    def __init__(self, x=None, label=None, y=None, data=None):
        self.label = label   # label:子節點分類依據的特徵
        self.x = x           # x:特徵
        self.child = []      # child:子節點
        self.y = y           # y:類標記(葉節點纔有)
        self.data = data     # data:包含數據(葉節點纔有)

    def append(self, node):  # 添加子節點
        self.child.append(node)

    def predict(self, features):  # 預測數據所述類
        if self.y is not None:
            return self.y
        for c in self.child:
            if c.x == features[self.label]:
                return c.predict(features)


def printnode(node, depth=0):  # 打印樹所有節點
    if node.label is None:
        print(depth, (node.label, node.x, node.y, len(node.data)))
    else:
        print(depth, (node.label, node.x))
        for c in node.child:
            printnode(c, depth+1)


class DTree:
    def __init__(self, epsilon=0, alpha=0):  # 預剪枝、後剪枝參數
        self.epsilon = epsilon
        self.alpha = alpha
        self.tree = Node()

    def prob(self, datasets):  # 求概率
        datalen = len(datasets)
        labelx = set(datasets)
        p = {l: 0 for l in labelx}
        for d in datasets:
            p[d] += 1
        for i in p.items():
            p[i[0]] /= datalen
        return p

    def calc_ent(self, datasets):  # 求熵
        p = self.prob(datasets)
        ent = sum([-v * math.log(v, 2) for v in p.values()])
        return ent

    def cond_ent(self, datasets, col):  # 求條件熵
        labelx = set(datasets.iloc[col])
        p = {x: [] for x in labelx}
        for i, d in enumerate(datasets.iloc[-1]):
            p[datasets.iloc[col][i]].append(d)
        return sum([self.prob(datasets.iloc[col])[k] * self.calc_ent(p[k]) for k in p.keys()])

    def info_gain_train(self, datasets, datalabels):  # 求信息增益(互信息)
        #print('----信息增益----')
        datasets = datasets.T
        ent = self.calc_ent(datasets.iloc[-1])
        gainmax = {}
        for i in range(len(datasets) - 1):
            cond = self.cond_ent(datasets, i)
            #print(datalabels[i], ent - cond)
            gainmax[ent - cond] = i
        m = max(gainmax.keys())
        return gainmax[m], m

    def train(self, datasets, node):
        labely = datasets.columns[-1]
        if len(datasets[labely].value_counts()) == 1:
            node.data = datasets[labely]
            node.y = datasets[labely][0]
            return
        if len(datasets.columns[:-1]) == 0:
            node.data = datasets[labely]
            node.y = datasets[labely].value_counts().index[0]
            return
        gainmaxi, gainmax = self.info_gain_train(datasets, datasets.columns)
        #print('選擇特徵:', gainmaxi)
        if gainmax <= self.epsilon:  # 若信息增益(互信息)爲0意爲輸入特徵x完全相同而標籤y相反
            node.data = datasets[labely]
            node.y = datasets[labely].value_counts().index[0]
            return

        vc = datasets[datasets.columns[gainmaxi]].value_counts()
        for Di in vc.index:
            node.label = gainmaxi
            child = Node(Di)
            node.append(child)
            new_datasets = pd.DataFrame([list(i) for i in datasets.values if i[gainmaxi]==Di], columns=datasets.columns)
            self.train(new_datasets, child)

    def fit(self, datasets):
        self.train(datasets, self.tree)

    def findleaf(self, node, leaf):  # 找到所有葉節點
        for t in node.child:
            if t.y is not None:
                leaf.append(t.data)
            else:
                for c in node.child:
                    self.findleaf(c, leaf)

    def findfather(self, node, errormin):
        if node.label is not None:
            cy = [c.y for c in node.child]
            if None not in cy:  # 全是葉節點
                childdata = []
                for c in node.child:
                    for d in list(c.data):
                        childdata.append(d)
                childcounter = Counter(childdata)

                old_child = node.child  # 剪枝前先拷貝一下
                old_label = node.label
                old_y = node.y
                old_data = node.data

                node.label = None  # 剪枝
                node.y = childcounter.most_common(1)[0][0]
                node.data = childdata

                error = self.c_error()
                if error <= errormin:  # 剪枝前後損失比較
                    errormin = error
                    return 1
                else:
                    node.child = old_child  # 剪枝效果不好,則復原
                    node.label = old_label
                    node.y = old_y
                    node.data = old_data
            else:
                re = 0
                i = 0
                while i < len(node.child):
                    if_re = self.findfather(node.child[i], errormin)  # 若剪過枝,則其父節點要重新檢測
                    if if_re == 1:
                        re = 1
                    elif if_re == 2:
                        i -= 1
                    i += 1
                if re:
                    return 2
        return 0

    def c_error(self):  # 求C(T)
        leaf = []
        self.findleaf(self.tree, leaf)
        leafnum = [len(l) for l in leaf]
        ent = [self.calc_ent(l) for l in leaf]
        print("Ent:", ent)
        error = self.alpha*len(leafnum)
        for l, e in zip(leafnum, ent):
            error += l*e
        print("C(T):", error)
        return error

    def cut(self, alpha=0):  # 剪枝
        if alpha:
            self.alpha = alpha
        errormin = self.c_error()
        self.findfather(self.tree, errormin)


datasets = np.array([['青年', '否', '否', '一般', '否'],
               ['青年', '否', '否', '好', '否'],
               ['青年', '是', '否', '好', '是'],
               ['青年', '是', '是', '一般', '是'],
               ['青年', '否', '否', '一般', '否'],
               ['中年', '否', '否', '一般', '否'],
               ['中年', '否', '否', '好', '否'],
               ['中年', '是', '是', '好', '是'],
               ['中年', '否', '是', '非常好', '是'],
               ['中年', '否', '是', '非常好', '是'],
               ['老年', '否', '是', '非常好', '是'],
               ['老年', '否', '是', '好', '是'],
               ['老年', '是', '否', '好', '是'],
               ['老年', '是', '否', '非常好', '是'],
               ['老年', '否', '否', '一般', '否'],
               ['青年', '否', '否', '一般', '是']])  # 在李航原始數據上多加了最後這行數據,以便體現剪枝效果

datalabels = np.array(['年齡', '有工作', '有自己的房子', '信貸情況', '類別'])
train_data = pd.DataFrame(datasets, columns=datalabels)
test_data = ['老年', '否', '否', '一般']

dt = DTree(epsilon=0)  # 可修改epsilon查看預剪枝效果
dt.fit(train_data)

print('DTree:')
printnode(dt.tree)
y = dt.tree.predict(test_data)
print('result:', y)

dt.cut(alpha=0.5)  # 可修改正則化參數alpha查看後剪枝效果

print('DTree:')
printnode(dt.tree)
y = dt.tree.predict(test_data)
print('result:', y)

4.如何對決策樹進行剪枝?

決策樹的剪枝通常有兩種方法,預剪枝(Pre-Pruning)和後剪枝(Post_Pruning)
預剪枝,即在生成決策樹的過程中提前停止樹的增長,
後剪枝,則在已生成的過擬合決策樹上進行剪枝,得到簡化版的剪枝決策樹。
那麼這兩種方法是如何進行的呢?它們又各有什麼優缺點?
  • 預剪枝
    即在生成決策樹的過程中提前停止樹的增長 ,預剪枝對停止決策樹有以下幾種方法
    • 當樹達到一定高度的時候,停止樹的生長。
    • 當到達當前節點的樣本數量小於某個閾值的時候,停止樹的生長。
    • 計算每次分裂對測試集的準確度提升,當小於某個閾值的時候,不在繼續擴展
  • 後剪枝
    • 在這裏介紹CART數的剪枝策略–代價複雜剪枝
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章