機器學習西瓜書吃瓜筆記之(二)決策樹分類 附一鍵生成決策樹&視覺化python程式碼實現

xwmisc發表於2020-10-13

決策樹分類(附一鍵生成視覺化python程式碼實現)

決策樹

  • 決策樹是用於分類任務的樹結構,它的葉子結點為類別,其餘節點為判斷操作。

  • 決策樹類似於日常中判斷分類的方法。對某個樣本進行分類時:

    1. 從根節點開始
    2. 得到所處節點的判斷結果
    3. 移動到滿足結果的子節點上
    4. 當移動到葉子結點上時,返回類別,否則轉第2步
      附程式碼生成的ID3決策樹
  • 研究決策樹,重點在於如何構建決策樹。

構建

決策樹學習基本演算法:

輸入:
    訓練集 D={(X1,y1),(X2,y2),...,(Xm,ym)}
    屬性集 A={a1,a2,...,ad}
過程:函式 TreeGenerate(D,A)
    生成結點 node;
    if D中樣本全屬於同一類別C then
        將node標記為C類葉結點; return
    end if
    if A=∅ or D中樣本在A上取值相同 then
        將node標記為葉結點,其類別標記為D中樣本數最多的類; return
    end if

    從A中選擇最優劃分屬性a*;

    for a* 的每一個值 a*_v do
        為node生成一個分支; 令Dv表示D中在a*上取值為a*_v的樣本子集;
        if Dv 為空 then
            將分支結點標記為葉結點,其類別標記為D中樣本最多的類; return
        else
            以TreeGenerate(D, A \ {a*})為分支結點
        end if
    end for
輸出:以node為根結點的一棵決策樹

演算法中最關鍵的是如何從 A A A中選擇最優劃分屬性 a ∗ a^* a,不同的劃分選擇決定了決策樹的種類:

  1. 資訊增益 ⇒ ID3決策樹
  2. 資訊增益率 ⇒ C4.5決策樹
  3. 基尼指數 ⇒ CART決策樹

資訊熵

通俗理解資訊熵 - 知乎
資訊熵是度量樣本集合純度最常用的指標。假定當前樣本集合 D D D中第 i i i類樣本所佔比例為 p i ( i = 1 , 2 , ⋯   , n ) p_i(i=1,2,\cdots,n) pi(i=1,2,,n),則 D D D的資訊熵定義為:
H ( X ) = − ∑ i = 1 n p ( x i ) ⋅ l o g p ( x i ) H(X)=-\sum_{i=1}^{n}p(x_i)·logp(x_i) H(X)=i=1np(xi)logp(xi)

  • 越小概率的事情發生了產生的資訊量越大
  • 熵則是在結果出來之前對可能產生的資訊量的期望
  • 資訊熵描述隨機變數的不確定性,資訊熵越小,資料集不確定性就低

條件熵

通俗理解條件熵 - 知乎
條件熵代表在某一個條件下,隨機變數的複雜度(不確定度)
H ( Y ∣ X ) = − ∑ x ∈ X p ( x ) ⋅ H ( Y ∣ X = x ) = − ∑ x ∈ X p ( x ) ∑ y ∈ Y p ( y ∣ x ) ⋅ l o g p ( y ∣ x ) = − ∑ x ∈ X ∑ y ∈ Y p ( x , y ) l o g p ( y ∣ x ) \begin{aligned} H(Y|X)&=-\sum_{x\in X}p(x)·H(Y|X=x)\\ &=-\sum_{x\in X}p(x)\sum_{y\in Y}p(y|x)·logp(y|x)\\ &=-\sum_{x\in X}\sum_{y\in Y}p(x,y)logp(y|x) \end{aligned} H(YX)=xXp(x)H(YX=x)=xXp(x)yYp(yx)logp(yx)=xXyYp(x,y)logp(yx)

  • 條件熵是指在給定某個變數為某個值的情況下,另一個變數的熵是多少
  • 在每一個小類裡面,都計算一個小熵,然後每一個小熵乘以各個類別的概率,然後求和,得到條件熵

資訊增益

X的熵減去Y條件下X的熵,就是資訊增益:
G a i n ( X , Y ) = H ( X ) − H ( Y ∣ X ) Gain(X,Y) = H(X)-H(Y|X) Gain(X,Y)=H(X)H(YX)

決策樹生成&視覺化

  • 直接複製貼上就可以執行看結果,說不清楚的地方請看我的程式碼具體實現,關鍵部分已經全部加上註釋。
  • 視覺化部分需要安裝graphviz包,具體請百度安裝教程(pip一下,官網下載release版本解壓再把路徑加環境path就行了)。
  • 要是視覺化報錯Error: Could not open "decisionTree.gv.pdf" for writing : Invalid argument'記得在瀏覽器關閉之前的檢視
from random import choice
from collections import Counter
import math

# ==========
# 定義資料集
# ==========
D = [
    {'色澤': '青綠', '根蒂': '蜷縮', '敲聲': '濁響', '紋理': '清晰', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '烏黑', '根蒂': '蜷縮', '敲聲': '沉悶', '紋理': '清晰', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '烏黑', '根蒂': '蜷縮', '敲聲': '濁響', '紋理': '清晰', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '青綠', '根蒂': '蜷縮', '敲聲': '沉悶', '紋理': '清晰', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '淺白', '根蒂': '蜷縮', '敲聲': '濁響', '紋理': '清晰', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '青綠', '根蒂': '稍蜷', '敲聲': '濁響', '紋理': '清晰', '臍部': '稍凹', '觸感': '軟粘', '好瓜': '是'},
    {'色澤': '烏黑', '根蒂': '稍蜷', '敲聲': '濁響', '紋理': '稍糊', '臍部': '稍凹', '觸感': '軟粘', '好瓜': '是'},
    {'色澤': '烏黑', '根蒂': '稍蜷', '敲聲': '濁響', '紋理': '清晰', '臍部': '稍凹', '觸感': '硬滑', '好瓜': '是'},
    {'色澤': '烏黑', '根蒂': '稍蜷', '敲聲': '沉悶', '紋理': '稍糊', '臍部': '稍凹', '觸感': '硬滑', '好瓜': '否'},
    {'色澤': '青綠', '根蒂': '硬挺', '敲聲': '清脆', '紋理': '清晰', '臍部': '平坦', '觸感': '軟粘', '好瓜': '否'},
    {'色澤': '淺白', '根蒂': '硬挺', '敲聲': '清脆', '紋理': '模糊', '臍部': '平坦', '觸感': '硬滑', '好瓜': '否'},
    {'色澤': '淺白', '根蒂': '蜷縮', '敲聲': '濁響', '紋理': '模糊', '臍部': '平坦', '觸感': '軟粘', '好瓜': '否'},
    {'色澤': '青綠', '根蒂': '稍蜷', '敲聲': '濁響', '紋理': '稍糊', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '否'},
    {'色澤': '淺白', '根蒂': '稍蜷', '敲聲': '沉悶', '紋理': '稍糊', '臍部': '凹陷', '觸感': '硬滑', '好瓜': '否'},
    {'色澤': '烏黑', '根蒂': '稍蜷', '敲聲': '濁響', '紋理': '清晰', '臍部': '稍凹', '觸感': '軟粘', '好瓜': '否'},
    {'色澤': '淺白', '根蒂': '蜷縮', '敲聲': '濁響', '紋理': '模糊', '臍部': '平坦', '觸感': '硬滑', '好瓜': '否'},
    {'色澤': '青綠', '根蒂': '蜷縮', '敲聲': '沉悶', '紋理': '稍糊', '臍部': '稍凹', '觸感': '硬滑', '好瓜': '否'},
]


# ==========
# 決策樹生成類
# ==========
class DecisionTree:
    def __init__(self, D, label, chooseA):
        self.D = D  # 資料集
        self.label = label  # 哪個屬性作為標籤
        self.chooseA = chooseA  # 劃分方法
        self.A = list(filter(lambda key: key != label, D[0].keys()))  # 屬性集合A
        # 獲得A的每個屬性的可選項
        self.A_item = {}
        for a in self.A:
            self.A_item.update({a: set(self.getClassValues(D, a))})
        self.root = self.generate(self.D, self.A)  # 生成樹並儲存根節點

    # 獲得D中所有className屬性的值
    def getClassValues(self, D, className):
        return list(map(lambda sample: sample[className], D))

    # D中樣本是否在A的每個屬性上相同
    def isSameInA(self, D, A):
        for a in A:
            types = set(self.getClassValues(D, a))
            if len(types) > 1:
                return False
        return True

    # 構建決策樹,遞迴生成節點
    def generate(self, D, A):
        node = {}  # 生成節點
        remainLabelValues = self.getClassValues(D, self.label)  # D中的所有標籤
        remainLabelTypes = set(remainLabelValues)  # D中含有哪幾種標籤

        if len(remainLabelTypes) == 1:
            # 當前節點包含的樣本全屬於同個類別,無需劃分
            return remainLabelTypes.pop()  # 標記Node為葉子結點,值為僅存的標籤

        most = max(remainLabelTypes, key=remainLabelValues.count)  # D佔比最多的標籤

        if len(A) == 0 or self.isSameInA(D, A):
            # 當前屬性集為空,或是所有樣本在所有屬性上取值相同,無法劃分
            return most  # 標記Node為葉子結點,值為佔比最多的標籤

        a = self.chooseA(D,A,self)  # 劃分選擇

        for type in self.A_item[a]:
            condition = (lambda sample: sample[a] == type)  # 決策條件
            remainD = list(filter(condition, D))  # 剩下的樣本
            if len(remainD) == 0:
                # 當前節點包含的樣本集為空,不能劃分
                node.update({type: most})  # 標記Node為葉子結點,值為佔比最多的標籤
            else:
                # 繼續對剩下的樣本按其餘屬性劃分
                remainA = list(filter(lambda x: x != a, A))  # 未使用的屬性
                _node = self.generate(remainD, remainA)  # 遞迴生成子代節點
                node.update({type: _node})  # 把生成的子代節點更新到當前節點
        return {a: node}


# ==========
#  定義劃分方法
# ==========

# 隨機選擇
def random_choice(D, A, tree: DecisionTree):
    return choice(A)

# 資訊熵
def Ent(D,label,a,a_v):
    D_v = filter(lambda sample:sample[a]==a_v,D)
    D_v = map(lambda sample:sample[label],D_v)
    D_v = list(D_v)
    D_v_length = len(D_v)
    counter = Counter(D_v)
    info_entropy = 0
    for k, v in counter.items():
        p_k = v / D_v_length
        info_entropy += p_k * math.log(p_k, 2)
    return -info_entropy

# 資訊增益
def information_gain(D, A, tree: DecisionTree):
    gain = {}
    for a in A:
        gain[a] = 0
        values = tree.getClassValues(D, a)
        counter = Counter(values)
        for a_v,nums in counter.items():
            gain[a] -= (nums / len(D)) * Ent(D,tree.label,a,a_v)
    return max(gain.keys(),key=lambda key:gain[key])

# ==========
#  建立決策樹
# ==========
desicionTreeRoot = DecisionTree(D, label='好瓜',chooseA=information_gain).root
print('決策樹:', desicionTreeRoot)


# ==========
# 決策樹視覺化類
# ==========
class TreeViewer:
    def __init__(self):
        from graphviz import Digraph
        self.id_iter = map(str, range(0xffff))
        self.g = Digraph('G', filename='decisionTree.gv')

    def create_node(self, label, shape=None):
        id = next(self.id_iter)
        self.g.node(name=id, label=label, shape=shape, fontname="Microsoft YaHei")
        return id

    def build(self, key, node, from_id):
        for k in node.keys():
            v = node[k]
            if type(v) is dict:
                first_attr = list(v.keys())[0]
                id = self.create_node(first_attr+"?", shape='box')
                self.g.edge(from_id, id, k, fontsize = '12', fontname="Microsoft YaHei")
                self.build(first_attr, v[first_attr], id)
            else:
                id = self.create_node(v)
                self.g.edge(from_id, id, k, fontsize = '12', fontname="Microsoft YaHei")

    def show(self, root):
        first_attr = list(root.keys())[0]
        id = self.create_node(first_attr+"?", shape='box')
        self.build(first_attr, root[first_attr], id)
        self.g.view()


# ==========
# 顯示建立的決策樹
# ==========
viewer = TreeViewer()
viewer.show(desicionTreeRoot)

敲程式碼不易,且用且珍惜,若要轉載請註明出處,謝謝

相關文章