【機器學習】ID3算法構建決策樹


 

 

ID3算法

ID3 提出了初步的決策樹算法;C4.5 提出了完整的決策樹算法;CART (Classification And Regression Tree) 目前使用最多的決策樹算法;

1、ID3 算法

ID3 算法是決策樹的經典構造算法,內部使用信息熵信息增益來進行構建,每次迭代算則信息增益最大的特征屬性作為分割屬性。

優點:決策樹構建速度快,實現簡單。

缺點:計算依賴於特征數目較多的特征,而屬性值最多的屬性並不一定最優。ID3算法不是遞增算法。ID3算法是單變量決策樹,對於特征屬性之間的關系不會考慮。抗噪性差。數據集中噪音點多可能會出現過擬合。只適合小規模的數據集,需要將數據放到內存中。

2、C4.5 算法

C4.5 算法是在ID3算法上的優化。使用信息增益率來取代ID3中的信息增益,在樹的構造過程中會進行剪枝操作進行優化,能夠自動完成對連續屬性的離散化處理。

ID3當時構建的時候就沒有去考慮連續值這個問題。

C4.5 算法在選中分割屬性的時候選擇信息增益率大的屬性,公式如下:

優點:產生規則易於理解。准確率較高。(因為考慮了連續值,數據越多擬合程度就越好。)實現簡單。

缺點:對數據集需要進行多次掃描和排序,所以效率較低。(比如之前例子中收入的連續值,分割次數越多,需要掃描的次數也就越多,排序次數也越多。)只適合小規模數據集,需要將數據放到內存中。

3、CART算法

使用基尼系數Gain作為數據純度的量化指標來構建決策樹算法,叫做CART算法。

GINI增益作為分割屬性選擇的標准,選擇GINI增益最大的作為當前數據集分割屬性。可以用於分類和回歸兩類問題。

注意:CART構建的是二叉樹。

4、總結

1、ID3和C4.5算法只適合小規模數據集上使用。2、ID3和C4.5算法都是單變量決策樹。3、當屬性值比較多的時候請使用C4.5。4、決策樹分類一般情況只適合小數據量的情況(數據可以放內存)5、CART算法是最常用的一種決策樹構建算法。6、三種算法的區別只是對於當前樹的評價標准不同而已,ID3使用信息增益,C4.5使用信息增益率,CART使用基尼系數。7、CART算法構建的一定是二叉樹。

 

構建決策樹三個重要的問題

  (1)數據是怎么分裂的

     (2)如何選擇分類的屬性

     (3)什么時候停止分裂

     從上述三個問題出發,以實際的例子對ID3算法進行闡述。

例:通過當天的天氣、溫度、濕度和季節預測明天的天氣

                                  表1 原始數據

當天天氣

溫度

濕度

季節

明天天氣

25

50

春天

21

48

春天

18

70

春天

28

41

夏天

8

65

冬天

18

43

夏天

24

56

秋天

18

76

秋天

31

61

夏天

6

43

冬天

15

55

秋天

4

58

冬天

 1.數據分割

      對於離散型數據,直接按照離散數據的取值進行分裂,每一個取值對應一個子節點,以“當前天氣”為例對數據進行分割,如圖1所示。

 

      對於連續型數據,ID3原本是沒有處理能力的,只有通過離散化將連續性數據轉化成離散型數據再進行處理。

      連續數據離散化是另外一個課題,本文不深入闡述,這里直接采用等距離數據划分的李算話方法。該方法先對數據進行排序,然后將連續型數據划分為多個區間,並使每一個區間的數據量基本相同,以溫度為例對數據進行分割,如圖2所示。

 

 2. 選擇最優分裂屬性

      ID3采用信息增益作為選擇最優的分裂屬性的方法,選擇熵作為衡量節點純度的標准,信息增益的計算公式如下:

                                               

      其中, 表示父節點的熵; 表示節點i的熵,熵越大,節點的信息量越多,越不純; 表示子節點i的數據量與父節點數據量之比。 越大,表示分裂后的熵越小,子節點變得越純,分類的效果越好,因此選擇 最大的屬性作為分裂屬性。

      對上述的例子的跟節點進行分裂,分別計算每一個屬性的信息增益,選擇信息增益最大的屬性進行分裂。

      天氣屬性:(數據分割如上圖1所示) 

  

      溫度:(數據分割如上圖2所示)

     

      濕度:

 

      

      季節:

 

      

     由於最大,所以選擇屬性“季節”作為根節點的分裂屬性。

 如何使用Python計算信息熵

#輸入參數:
#dataSet: 數據集
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts ={}
    #遍歷數據集中每一個樣本
    for featVec in dataSet:
        #取每一個樣本的類別標簽
        currentLabel = featVec[-1]
        #判斷這個標簽在字典中是否存在,不存在就初始化
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        #統計不同類別的數量
        labelCounts[currentLabel] += 1
    #初始化熵
    shannonEnt = 0.0
    #計算熵
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * log(prob,2)
    return shannonEnt

3.停止分裂的條件

     停止分裂的條件已經在決策樹中闡述,這里不再進行闡述。

     (1)最小節點數

  當節點的數據量小於一個指定的數量時,不繼續分裂。兩個原因:一是數據量較少時,再做分裂容易強化噪聲數據的作用;二是降低樹生長的復雜性。提前結束分裂一定程度上有利於降低過擬合的影響。

  (2)熵或者基尼值小於閥值。

     由上述可知,熵和基尼值的大小表示數據的復雜程度,當熵或者基尼值過小時,表示數據的純度比較大,如果熵或者基尼值小於一定程度時,節點停止分裂。

  (3)決策樹的深度達到指定的條件

   節點的深度可以理解為節點與決策樹跟節點的距離,如根節點的子節點的深度為1,因為這些節點與跟節點的距離為1,子節點的深度要比父節點的深度大1。決策樹的深度是所有葉子節點的最大深度,當深度到達指定的上限大小時,停止分裂。

  (4)所有特征已經使用完畢,不能繼續進行分裂。

     被動式停止分裂的條件,當已經沒有可分的屬性時,直接將當前節點設置為葉子節點。

 

Python構建決策樹

決策樹的流程為

  (1)輸入需要分類的數據集和類別標簽和靶標簽。

  (2)檢驗數據集是否只有一列,或者是否最后一列(靶標簽數據默認放到最后一列)只有一個水平(唯一值)。

    是:返回唯一值水平或者占比最大的那個水平

  (3)調用信息增益公式,計算所有節點的信息增益,得到最大信息增益所對應的類別標簽。

  (4)建立決策樹字典用以保存當次葉節點數據信息。

  (5)進入循環:

    按照該類別標簽的不同水平,依次計算子數據集;

    對子數據集重復(1),(2),(3),(4),(5), (6)步。

  (6)返回決策樹字典。

  決策樹實際上是一個大的遞歸函數,其結果是一個多層次的字典。

python3實現ID3算法

import numpy as np
import pandas as pd
import json

#序列化與反序列樹字典
class TreeHandler(object):
    def __init__(self):
        self.tree = None

    def save(self, tree):
        self.tree = tree
        with open("tree.txt", mode="w", encoding="utf-8") as f:
            tree = json.dumps(tree, indent="  ", ensure_ascii=False)
            f.write(tree)

    def load(self, file):
        with open(file, mode="r", encoding="utf-8") as f:
            tree = f.read()
            self.tree = json.loads(tree)
        return self.tree

#
class ID3Tree(TreeHandler):
    """主要的數據結構是pandas對象"""
    __count = 0

    def __init__(self):
        super().__init__()
        """認定最后一列是標簽列"""
        self.gain = {}

    def _entropy(self, dataSet):
        """計算給定數據集的熵"""
        labels = list(dataSet.columns)
        level_count = dataSet[labels[-1]].value_counts().to_dict()  # 統計分類標簽不同水平的值
        entropy = 0.0
        for key, value in level_count.items():
            prob = float(value) / dataSet.shape[0]
            entropy += -prob * np.log2(prob)
        return entropy

    def _split_dataSet(self, dataSet, column, level):
        """根據給定的column和其level來獲取子數據集"""
        subdata = dataSet[dataSet[column] == level]
        del subdata[column]  # 刪除這個划分字段列
        return subdata.reset_index(drop=True)  # 重建索引

    def _best_split(self, dataSet):
        """計算每個分類標簽的信息增益"""
        best_info_gain = 0.0  # 求最大信息增益
        best_label = None  # 求最大信息增益對應的標簽(字段)
        labels = list(dataSet.columns)[: -1]  # 不包括最后一個靶標簽
        init_entropy = self._entropy(dataSet)  # 先求靶標簽的香農熵
        for _, label in enumerate(labels):
            # 根據該label(也即column字段)的唯一值(levels)來切割成不同子數據集,並求它們的香農熵
            levels = dataSet[label].unique().tolist()  # 獲取該分類標簽的不同level
            label_entropy = 0.0  # 用於累加各水平的信息熵;分類標簽的信息熵等於該分類標簽的各水平信息熵與其概率積的和。
            for level in levels:  # 循環計算不同水平的信息熵
                level_data = dataSet[dataSet[label] == level]  # 獲取該水平的數據集
                prob = level_data.shape[0] / dataSet.shape[0]  # 計算該水平的數據集在總數據集的占比
                # 計算香農熵,並更新到label_entropy中
                label_entropy += prob * self._entropy(level_data)  # _entropy用於計算香農熵
            # 計算信息增益
            info_gain = init_entropy - label_entropy  # 代碼至此,已經能夠循環計算每個分類標簽的信息增益
            # 用best_info_gain來取info_gain的最大值,並獲取對應的分類標簽
            if info_gain > best_info_gain:
                best_info_gain = info_gain
                best_label = label
            # 這里保存一下每一次計算的信息增益,便於查看和檢查錯誤
            self.gain.setdefault(self.__count, {})  # 建立本次函數調用時的字段,設其value為字典
            self.gain[self.__count][label] = info_gain  # 把本次函數調用時計算的各個標簽數據存到字典里
        self.__count += 1
        return best_label

    def _top_amount_level(self, target_list):
        class_count = target_list.value_counts().to_dict()  # 計算靶標簽的不同水平的樣本量,並轉化為字典
        # 字典的items方法可以將鍵值對轉成[(), (), ...],可以使用列表方法
        sorted_class_count = sorted(class_count.items(), key=lambda x: x[1], reverse=True)
        return sorted_class_count[0][0]

    def mktree(self, dataSet):
        """創建決策樹"""
        target_list = dataSet.iloc[:, -1]  # target_list 靶標簽的那一列數據
        # 程序終止條件一: 靶標簽(數據集的最后一列因變量)在該數據集上只有一個水平,返回該水平
        if target_list.unique().shape[0] <= 1:
            return target_list[0]  # !!!
        # 程序終止條件二: 數據集只剩下把標簽這一列數據;返回數量最多的水平
        if dataSet.shape[1] == 1:
            return self._top_amount_level(target_list)
        # 不滿足終止條件時,做如下遞歸處理
        # 1.選擇最佳分類標簽
        best_label = self._best_split(dataSet)
        # 2.遞歸計算最佳分類標簽的不同水平的子數據集的信息增益
        #   各個子數據集的最佳分類標簽的不同水平...
        #   ...
        #   直至遞歸結束
        best_label_levels = dataSet[best_label].unique().tolist()
        tree = {best_label: {}}  # 生成字典,用於保存樹狀分類信息;這里不能用self.tree = {}存儲
        for level in best_label_levels:
            level_subdata = self._split_dataSet(dataSet, best_label, level)  # 獲取該水平的子數據集
            tree[best_label][level] = self.mktree(level_subdata)  # 返回結果
        return tree

    def predict(self, tree, labels, test_sample):
        """
        對單個樣本進行分類
        tree: 訓練的字典
        labels: 除去最后一列的其它字段
        test_sample: 需要分類的一行記錄數據
        """
        classLabel = None
        firstStr = list(tree.keys())[0]  # tree字典里找到第一個用於分類鍵值對
        secondDict = tree[firstStr]
        featIndex = labels.index(firstStr)  # 找到第一個建(label)在給定label的索引
        for key in secondDict.keys():
            if test_sample[featIndex] == key:  # 找到test_sample在當前label下的值
                if secondDict[key].__class__.__name__ == "dict":
                    classLabel = self.predict(secondDict[key], labels, test_sample)
                else:
                    classLabel = secondDict[key]
        return classLabel

    def _unit_test(self):
        """用於測試_entropy函數"""
        data = [
            ['青綠', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', ''],  # 1
            ['烏黑', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', ''],  # 2
            ['烏黑', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', ''],  # 3
            ['青綠', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', ''],  # 4
            ['淺白', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', ''],  # 5
            ['青綠', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', ''],  # 6
            ['烏黑', '稍蜷', '濁響', '稍糊', '稍凹', '軟粘', ''],  # 7
            ['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '硬滑', ''],  # 8

            ['烏黑', '稍蜷', '沉悶', '稍糊', '稍凹', '硬滑', ''],  # 9
            ['青綠', '硬挺', '清脆', '清晰', '平坦', '軟粘', ''],  # 10
            ['淺白', '硬挺', '清脆', '模糊', '平坦', '硬滑', ''],  # 11
            ['淺白', '蜷縮', '濁響', '模糊', '平坦', '軟粘', ''],  # 12
            ['青綠', '稍蜷', '濁響', '稍糊', '凹陷', '硬滑', ''],  # 13
            ['淺白', '稍蜷', '沉悶', '稍糊', '凹陷', '硬滑', ''],  # 14
            ['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', ''],  # 15
            ['淺白', '蜷縮', '濁響', '模糊', '平坦', '硬滑', ''],  # 16
            ['青綠', '蜷縮', '沉悶', '稍糊', '稍凹', '硬滑', ''],  # 17
        ]
        data = pd.DataFrame(data=data, columns=['色澤','根蒂','敲聲','紋理','臍部','觸感','分類'])
        # return data # 到此行,用於測試_entropy
        # return self._split_dataSet(data, "a", 1)  # 到此行,用於測試_split_dataSet
        # return self._best_split(data)  # 到此行,用於測試_best_split
        # return self.mktree(self.dataSet)  # 到此行,用於測試主程序mktree
        # 生成樹
        self.tree = self.mktree(data) # 到此行,用於測試主程序mktree
        #打印樹
        print(self.tree)
        labels = ['色澤','根蒂','敲聲','紋理','臍部','觸感']
        #測試樣本
        test_sample = ['青綠', '蜷縮', '沉悶', '稍糊', '稍凹', '硬滑']
        #預測結果
        outcome = self.predict(self.tree, labels, test_sample)
        print("The truth class is %s, The ID3Tree outcome is %s." % ("", outcome))
model = ID3Tree()
model._unit_test()

數據來源:《機器學習—周志華》

使用matplotlib畫出決策樹:

import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
plt.figure(1, figsize=(8,8))
ax = plt.subplot(111)
def drawNode(text, startX, startY, endX, endY, ann):
 #繪制帶箭頭的文本
    ax.annotate(text,
                xy=(startX+0.01, startY), xycoords='data',
                xytext=(endX, endY), textcoords='data',
                arrowprops=dict(arrowstyle="<-",
                                connectionstyle="arc3"),
                bbox=dict(boxstyle="square", fc="r")
                )
 #在箭頭中間位置標記數字
    ax.text((startX+endX)/2, (startY+endY)/2, str(ann))
#繪制樹根
bbox_props = dict(boxstyle="square,pad=0.3", fc="cyan", ec="b", lw=2)
ax.text(0.5, 0.97, '紋理', bbox=bbox_props)
#繪制其他節點
drawNode('根蒂', 0.5, 0.97, 0.25, 0.8, "清晰")
drawNode('觸感', 0.5, 0.97, 0.50, 0.8, "稍糊")
drawNode('壞瓜', 0.5, 0.8, 0.4, 0.65, "硬滑")
drawNode('好瓜', 0.5, 0.8, 0.6, 0.65, "硬滑")
drawNode('壞瓜', 0.5, 0.97, 0.75, 0.8, "模糊")
drawNode('好瓜', 0.25, 0.8, 0.1, 0.65, "蜷縮")
drawNode('色澤', 0.25, 0.8, 0.2, 0.65, "稍蜷")
drawNode('好瓜', 0.25, 0.8, 0.3, 0.65, "硬挺")
drawNode('好瓜', 0.2, 0.65, 0.1, 0.5, "青綠")
drawNode('觸感', 0.2, 0.65, 0.25, 0.5, "烏黑")
drawNode('好瓜', 0.25, 0.5, 0.1, 0.35, "硬滑")
drawNode('壞瓜', 0.25, 0.5, 0.4, 0.35, "軟粘")
#顯示圖形
plt.show()

 

 

 

 

總結

     ID3是基本的決策樹構建算法,作為決策樹經典的構建算法,其具有結構簡單、清晰易懂的特點。雖然ID3比較靈活方便,但是有以下幾個缺點:

 (1)采用信息增益進行分裂,分裂的精確度可能沒有采用信息增益率進行分裂高。

   (2)不能處理連續型數據,只能通過離散化將連續性數據轉化為離散型數據。

   (3)不能處理缺省值。

   (4)沒有對決策樹進行剪枝處理,很可能會出現過擬合的問題。

 

原文摘錄:

決策樹之ID3算法

決策樹系列(三)——ID3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM