1. 程式人生 > >【機器學習】決策樹 總結

【機器學習】決策樹 總結

具體的細節概念就不提了,這篇blog主要是用來總結一下決策樹的要點和注意事項,以及應用一些決策樹程式碼的。

一、決策樹的優點:

易於理解和解釋。數可以視覺化。也就是說決策樹屬於白盒模型,如果一個情況被觀察到,使用邏輯判斷容易表示這種規則。相反,如果是黑盒模型(例如人工神經網路),結果會非常難解釋。
幾乎不需要資料預處理。其他方法經常需要資料標準化,建立虛擬變數和刪除缺失值。在sklearn中的決策樹還不支援缺失值處理。
• 利用訓練好的決策樹進行分類或迴歸的時間複雜度僅是訓練生成決策樹的時間的對數,時間複雜度大大縮小。
• 既可以處理數值變數,也可以處理分類資料,也就是說,如果資料型別不同,也可以在一顆決策樹中進行訓練

。其他方法通常只適用於分析一種型別的變數。
• 可以處理多值輸出變數問題。
• 可以使用統計檢驗驗證模型的可靠性。
• 如果真實的資料不符合決策樹構建時的假設條件,也可以較好的適用

二、決策樹的缺點:

• 決策樹學習可能建立一個過於複雜的樹,降低泛化效能,這就是過擬合。修剪機制(現在不支援),構建決策樹時設定一個葉子節點需要的最小樣本數量,或者樹的最大深度,都可以避免過擬合。
• 決策樹容易受到噪聲影響,同樣的資料如果略微改變可能生成一個完全不同的樹。這個問題通過整合學習來緩解。
• 學習一顆最優的決策樹是一個**NP-完全問題**under several aspects of optimality and even for simple concepts。所以,考慮計算量,在實際工程當中,決策樹演算法採用啟發式演算法,例如貪婪演算法,可以保證區域性最優解,但無法確保是全域性最優解。這個問題可以採用整合學習來解決。
• 決策樹對於一些問題有侷限性,主要是這些問題難以用條件判斷來解決。例如,異或問題, parity,multiplexer問題等等.
• 如果某些分類佔優勢,決策樹將會建立一棵有偏差的樹。因此,建議在訓練之前,先抽樣使樣本均衡

三、決策樹的複雜度:

構建一個決策樹的時間複雜度是 o(n^2*m*log(m)),其中,m是訓練樣本的數量,m是訓練樣本的特徵數量。而利用決策樹分類或迴歸一個測試樣本的時間複雜度是 o(log(n)),也就是說,訓練樣本數量越多,越容易造成決策樹深度爆炸。
深度爆炸的主要原因就是重複計算,
A naive implementation (as above) would recompute the class label histograms (for classification) or the means (for regression) at for each new split point along a given feature.
克服這種問題的普適性方法就是預先劃分屬性Presorting the feature over all relevant samples, and retaining a running label count, will reduce the complexity at each node to , which results in a total cost of . This is an option for all tree based algorithms. By default it is turned on for gradient boosting, where in general it makes training faster, but turned off for all other algorithms as it tends to slow down training when training deep trees.

以上一段是我沒有理解的,英文原文粘上來了,如果看官有懂的幫我解釋一下。

四、決策樹在實際工程使用過程中應當注意以下:

  • 決策樹在特徵非常多的時候很容易導致過擬合,如果訓練樣本還比較少,那就更容易過擬合,畢竟決策樹的本質是軸平行的分類迴歸演算法。所以要控制好訓練樣本數量和特徵數量之間的比值
  • 由於特徵多了容易產生很多麻煩,所以預先採用降維方法能夠更好地找到有代表性的,能夠起分類作用的特徵。這些方法主要有PCA、ICA、特徵選擇。
  • 先從最大樹深為3開始,訓練構建決策樹,觀察生成的樹的結構,看看是否符合預期,然後再逐漸地加深樹的深度。
  • 確定設定一個葉子節點的最小樣本支撐值太小,則容易造成過擬合,而太大會造成學習能力不足. 訓練開始時,首先設定成5,然後慢慢修改。
  • 如果某些特徵的值太大,而其他特徵的值太小,這時就需要在特徵上加權值。權值設定的方法就是將訓練資料的特徵的值全部相加,歸一化,使每一個特徵都具有相同的特徵的值,這個歸一化的引數就是權值。特徵均衡之後,預剪枝的效果也會更好,偏差bias更小。

五、幾種決策樹演算法的比較:

  • ID3 (Iterative Dichotomiser 3):採用貪婪策略,按照資訊增益來計算分類目標,劃分葉子節點。剪枝策略採用後減枝。
  • C4.5:繼承自ID3,但是資料型別不一定非得是離散型別了。C4.5具有很清晰易懂的if else語句能夠描述決策樹的結構。也是後減枝,具體策略是預刪除樹結點看泛化效能是否下降。
    C5.0是一個更新版本,它消耗的記憶體更低,決策樹構建的更小,但是精度更高。
  • CART (Classification and Regression Trees) 與C4.5非常相似,區別在於它支援目標Y值是連續型,也不計算規則集合。
    Sklearn包裡採用的是優化的 CART 演算法,當然計算因子可以從資訊增益和基尼係數中選擇。優化主要是時間複雜度上的優化,sklearn包裡的演算法預先將訓練樣本里的特徵排序,這樣就將時間複雜度縮減到了o(n*m*log(m))。,即採用基尼係數來計算。但是它不支援缺失值處理,也不支援剪枝策略。資料型別都是np.float32的。如果輸入的訓練樣本構成的矩陣是稀疏矩陣,則sklearn包提供了優化演算法,訓練時間大大縮短。

儘管不同的方法採用的計算因子不同,有的是資訊增益,有的是基尼係數,但是這些對泛化效能的影響都不大。

六、一種特殊情況:

預測值Y是一組n個的陣列,這樣需要考慮這個樣本的n個值是否是一致的。
如果是相互獨立的,那就可以把問題化為一維的情況進行處理,也就是基礎的分類和迴歸。但是如果n個數值之間有相互關係,那就需要將這n維資料同時進行預測,sklearn包裡提供了這樣的功能,比如,給定一個實數,預測其正弦和餘弦,還有面部預測,但是看效果不如神經網路的好啊。

七、樹的程式碼:

1、sklearn包中的程式碼

DecisionTreeClassifier(class_weight=None,  # 指某個類別所佔的權重。字典型別,或以字典為元素的列表型別,或 balanced,或預設 None,一般單值分類問題都預設,即所有的權重都是1,對多輸出問題,由於需要各個維度配合,所以經常需要處理資料以方便處理。如果一堆訓練資料,其中一類的資料量特別小,而其他類的資料量巨大則可以採用 balanced,根據類別標籤的頻率,使頻率低的權重提高,達到重視少數樣本的目的。
                       criterion='entropy',  # 構造決策樹時選擇屬性的準則,entropy指資訊增益,gini指基尼係數,還有一個資訊增益率,這裡不支援
                       max_depth=160,  # 最大樹深,一般不設定,它和每個節點最小需要的支援樣本數相輔相成,共同決定數的結構
                       max_features=None,  # 預設所有的屬性特徵總數,指尋找最優分支的時候要計算多少個屬性,預設就是所有屬性全部計算,如果是整數,那麼就從整數個屬性裡選,如果是小數,按照百分比來計算,如果是 auto 或 sqrt,就是按照總屬性數目開根號計算,如果是 log2,就按照取對數來計算。其目的主要是在屬性數量太多的時候,加快計算速度。
                       max_leaf_nodes=None,  # 預設為 None,沒有限制,指的是在分割資料集時,選用的屬性對分割後的結果有限制,其中的一個結點擁有的樣本數不允許超過此引數值。目的是最大化降低不純度。
                       min_samples_leaf=1,  # 預設是1,指在構建樹結點的時候,如果僅有一個子節點劃分在它下面,說明隨機性很大,就不允許這個樣本單獨來構造一個樹節點。
                       min_samples_split=3,  # 預設是2,指如果某個結點中僅有2個樣本劃分在它這裡,則不構造子結點來對它分割。如果是整數,那麼這個整數就是所需的樣本數目如果是小數,必須得小於1,指佔總訓練資料的比例必須大於該數才有資格劃分結點
                       min_weight_fraction_leaf=0.0,  # 預設是0.0,The minimum weighted fraction of the sum total of weights (of allthe input samples) required to be at a leaf node. Samples haveequal weight when sample_weight is not provided.
                       min_impurity_decrease=0.0,  # 構造決策樹的過程就是在降低不純度,構造結點的時候,如果不純度降低的值比這個要求的最小值還小,則說明該結點選擇不夠最優,重新選擇。
                       min_impurity_split=0.0,  # 指如果一個結點的不純度低於這個引數,說明已經夠純了,不用再劃分了,反之則需要繼續切分。
                       presort=False,  # 布林型別,預設為False,指預處理,預先分類。是否加速訓練速度,設定成True可能會降低訓練速度。
                       random_state=None,  # 在選擇屬性等過程中需要隨機處理,也就需要隨機數,若指定整數,則該數就是隨機例項的seed值,如果是一個指定的隨機例項,則使用它,如果採用預設None,會預設採用 np.random
                       splitter='best'  # best指從滿足準則的裡面選取資訊增益最大的,gini係數最大的。random指並非從符合準則的裡面選最優的,而是給出幾個相對較優的,隨機從裡面選一個。這樣做主要是為了克服criterion對某些取值數目較少的屬性有偏好
                       )

# 其預測結果也可以選擇,一種就是直接給出到底是哪個類別,第二種是給出屬於每一類的概率值,再就是給出概率值的對數。

DecisionTreeRegressor(criterion='mse',  # 迴歸問題採用的劃分準則,mse指最小均方誤差,是L2損失,friedman_mse是採用福雷曼增益值的最小均方誤差。mae 指平均絕對誤差,是L1損失。
                     )


ExtraTreeRegressor(# 是從上面兩個標準的繼承過來的,含義是極度隨機化的樹分類和樹迴歸。其含義是在選擇屬性構造子節點時,是從最大特徵數中隨機選擇一些進行構造。選擇哪些則是完全隨機的。這麼做的目的是為了提高訓練速度,避免過擬合,陷入極小點。
)

2、《深入機器學習實戰》中的例子程式碼

我給它添加了詳細的註釋,網上很多blog都抄 了它的程式碼。但是在這個程式碼中只是一個示範程式碼,仍存在很多的問題,主要就是可調節引數太少,支援的機制太少,僅僅能夠用於處理簡單離散分類問題。


class C45DecisionTree(object):
    '''
        該決策樹僅僅是決策樹的最簡版本,其侷限性:
        1、不能處理連續取值的屬性值,
        2、不能處理某些屬性沒有取值的資料,
        3、沒有剪枝策略,
        4、資料沒有預處理機制,
        5、控制引數缺失,如最大樹深等,
        6、不支援多變數決策樹。
        該類的使用方法:
            c45 = C45DecisionTree()
            myTree = c45.createTree(myDat, labels)  # 輸入訓練資料和標籤,構造決策樹
            print myTree  # 檢視決策樹的形狀
            predict = c45.classify(myTree, labels, testData)  # 輸入構造的樹,標籤集合,測試資料(只能是一條)
    '''

    def __init__(self):
        import math
        import operator
        self.my_tree = {}  # 用來檢視構造出來的決策樹

    def calc_shannon_entropy(self, data_set):
        '''
            計算給定資料集的香濃熵。
            data_set的結構是一個列表,其中的每個元素都是一個數據項,資料項結構依然是一個列表,前面n-1項都是屬性對應的值,最後一項是分類結果
        '''
        num_entries = len(data_set)
        label_counts = {}  # 類別字典(類別的名稱為鍵,該類別的個數為值)
        for feat_vector in data_set:  # 讀取資料集中的每一條資料,統計每一條資料的類別 label 出現的次數,得到一個字典
            current_label = feat_vector[-1]  # 最末尾的一個數值
            if current_label not in label_counts.keys():  # 還沒新增到字典裡的型別,獲取字典裡的所有鍵值
                label_counts[current_label] = 0
            label_counts[current_label] += 1
        shannon_entropy = 0.0
        for key in label_counts:  # 求出每種型別的熵,將所有的熵相加,得到資訊熵
            prob = float(label_counts[key]) / num_entries  # 每種型別個數佔所有的比值
            shannon_entropy -= prob * math.log(prob, 2)  # 計算熵
        return shannon_entropy  # 返回熵

    def split_data_set(self, data_set, axis, value):
        '''
            按照給定的特徵劃分資料集,特徵就是第axis列對應的特徵,該特徵值等於value的劃分為一個集合,作為該函式的返回
        :param dataSet:資料集不解釋
        :param axis:資料集中每一項資料的第axis列的屬性
        :param value:給定的第axis列的可能的取值中的一個
        :return:返回將第axis列的屬性資料刪除之後的資料集
        '''
        ret_data_set = []
        for feat_vector in data_set:  # 按dataSet矩陣中的第axis列的值等於value的分資料集
            if feat_vector[axis] == value:  # 值等於value的,每一行為新的列表(去除第axis個數據)
                reduced_feat_vector = feat_vector[:axis]
                reduced_feat_vector.extend(feat_vector[axis + 1:])  # 也就是說,去掉了第 axis 維的資料
                ret_data_set.append(reduced_feat_vector)
        return ret_data_set  # 返回分類後的新矩陣

    def choose_best_feature_to_split(self, data_set):
        '''
            選擇最好的資料集劃分方法,即找出來某個屬性,作為決策樹下一步要採用的劃分屬性。按照資訊增益來找的
            但是針對離散資料比較好操作,針對連續資料這裡需要增加程式碼
        :param data_set:資料集
        :return:找到到底按照哪個屬性來劃分決策樹,返回該屬性的索引
        '''
        num_features = len(data_set[0]) - 1  # 求屬性的個數
        base_entropy = self.calc_shannon_entropy(data_set)
        best_info_gain = 0.0  # 要選擇最高的資訊增益
        best_feature = -1  # 最好的特徵的索引,-1表示不存在,還沒開始選擇
        for i in range(num_features):  # 求所有屬性的資訊增益
            feat_list = [example[i] for example in data_set]  # 將資料集中所有的第i個特徵值全部提取出來,組成一個列表
            unique_vals = set(feat_list)  # 第 i列屬性的取值(不同值)數集合,對於離散的資料值還比較好用,如果是連續資料,資料量太大
            new_entropy = 0.0
            split_info = 0.0
            for value in unique_vals:  # 求第 i列屬性每個不同值的熵 *他們的概率
                sub_data_set = self.split_data_set(data_set, i, value)  # 子資料集,是按照 i屬性劃分成的多個部分的其中一個
                prob = len(sub_data_set) / float(len(data_set))  # 求出該值在i列屬性中的概率
                new_entropy += prob * self.calc_shannon_entropy(sub_data_set)  # 求i列屬性各值對於的熵求和
                split_info -= prob * math.log(prob, 2)
            info_gain = (base_entropy - new_entropy) / split_info  # 求出第i列屬性的資訊增益率
            # print infoGain
            if info_gain > best_info_gain:  # 儲存資訊增益率最大的資訊增益率值以及所在的下表(列值i)
                best_info_gain = info_gain
                best_feature = i
        return best_feature

    def majority_count(self, class_list):
        '''
            找到出現次數最多的分類的名稱,即每個資料項的最後一列,分類的結果標籤
        :param class_list: 類別列表
        :return: 返回的值是類別最多的那一類的類名
        '''
        class_count = {}  # 類別計數
        for vote in class_list:
            if vote not in class_count.keys():  # 增加分類,class_count就是標明瞭每一個類有多少個數據項
                class_count[vote] = 0
            class_count[vote] += 1
        sorted_class_count = sorted(class_count.iteritems(), key=operator.itemgetter(1), reverse=True)
        # 將字典可迭代化,按照迭代化後的每個資料項的1索引的取值排序,逆序排序,其實完全沒有必要,時間複雜度比較高
        return sorted_class_count[0][0]

    def create_tree(self, data_set, labels):
        '''
            建立決策樹。很明顯,使用過的屬性不可以再使用,所以對於連續屬性取值不可用,且樹的深度也不會很深。
        :param data_set: 資料集不多解釋
        :param labels: 指屬性的名字,字串型別的列表
        :return:
        '''
        class_list = [example[-1] for example in data_set]  # 訓練資料的結果列表(例如最外層的列表是[N, N, Y, Y, Y, N, Y])
        if class_list.count(class_list[0]) == len(class_list):  # 如果所有的訓練資料都是屬於一個類別,則返回該類別
            return class_list[0]
        if len(data_set[0]) == 1:  # 訓練資料只給出類別資料(沒給任何屬性值資料),返回出現次數最多的分類名稱
            return self.majority_count(class_list)

        best_feat = self.choose_best_feature_to_split(data_set)  # 選擇資訊增益最大的屬性進行分(返回值是屬性型別列表的下標)
        best_feat_label = labels[best_feat]  # 根據下表找屬性名稱,當作樹的根節點,樹結構用字典來表示
        my_tree = {best_feat_label: {}}  # 以best_feat_label為根節點建一個空樹
        del (labels[best_feat])  # 從屬性列表中刪掉已經被選出來當根節點的屬性,當資料屬於連續型別的時候,這一條就是錯誤的
        feat_values = [example[best_feat] for example in data_set]  # 找出該屬性所有訓練資料的值(建立列表)
        unique_vals = set(feat_values)  # 求出該屬性的所有值的集合(集合的元素不能重複)
        for value in unique_vals:  # 根據該屬性的值求樹的各個分支,如果該屬性為真那該怎樣,為假又該怎樣……
            sub_labels = labels[:]  # 已經是剔除了上面已選的屬性之後的子屬性集合
            my_tree[best_feat_label][value] = self.create_tree(self.split_data_set(data_set, best_feat, value),
                                                               sub_labels)  # 根據各個分支遞迴建立樹
        self.my_tree = my_tree
        return my_tree  # 生成的樹

    def classify(self, input_tree, feat_labels, test_vec):
        '''
            使用決策樹進行分類
        :param input_tree: 輸入決策樹,樹結構按字典給出的
        :param feat_labels: 特徵標籤,列表,順序和資料集中的屬性排列順序一致
        :param test_vec: 測試資料集
        :return: 返回分類的標籤
        '''
        first_str = input_tree.keys()[0]  # 使用的決策樹的所有的鍵的第一個
        second_dict = input_tree[first_str]
        feat_index = feat_labels.index(first_str)  # 找到類別標籤的索引
        for key in second_dict.keys():  # 第二層判斷
            if test_vec[feat_index] == key:  # 找到使用哪顆子樹
                if type(second_dict[key]).__name__ == 'dict':
                    class_label = self.classify(second_dict[key], feat_labels, test_vec)  # 繼續分類
                else:
                    class_label = second_dict[key]  # 已經到了樹的葉子節點,它指示什麼類別就是什麼了。
        return class_label