1. 程式人生 > >使用 sklearn 構建決策樹並使用 Graphviz 繪製樹結構

使用 sklearn 構建決策樹並使用 Graphviz 繪製樹結構

1. 概述

之前兩篇日誌,我們系統性的介紹了決策樹的構建演算法、構建流程、展示與決策:
決策樹的構建演算法 – ID3 與 C4.5 演算法
決策樹的構建、展示與決策
本文,我們來介紹如何使用 sklearn 構建決策樹。

2. sklearn

之前我們已經介紹和使用過 python 的 sklearn 包:
k 近鄰演算法
sklearn 也提供了決策樹明星,用於解決分類和迴歸問題。
http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html。

3. sklearn.tree.DecisionTreeClassifier 構造引數

sklearn.tree.DecisionTreeClassifier 類就是我們需要的決策樹類,它具有如下構造引數:
####sklearn.tree.DecisionTreeClassifier 類構造引數

引數名 型別 可選引數 預設值 說明
criterion string ‘gini’、‘entropy’ ‘gini’ 構建決策樹演算法,基尼不純度準則(CART 演算法)或資訊熵準則(C4.5演算法)
splitter string ‘best’、‘random’ ‘random’ 決策樹分支選取原則,全域性最優或選取隨機區域性最優點
max_depth int 正整數或 None None 樹最大深度,None 表示直到所有葉子都完成劃分,需要參考 min_samples_split 與 min_samples_leaf
min_samples_split int 或 float 正整數或小數 2 整數指的是內部節點最少包含樣本數,浮點數表示內部節點最少包含樣本數為 min_samples_split * n_samples,0.18 版本後才支援浮點數
min_samples_leaf int 或 float 正整數或小數 1 整數指的是葉子節點最少包含樣本數,浮點數表示葉子節點最少包含樣本數為 min_samples_leaf * n_samples
min_weight_fraction_leaf float 小數 0 葉節點最小樣本總權重
max_features int, float, string 或 None 可選引數見下文 None 節點分裂時參與判斷的最大特徵數,取值詳見下面講解
random_state int, 物件或 None 正整數、RandomState 物件或 None None 隨機數種子值、隨機數生成器物件或預設的 numpy 隨機數生成器
max_leaf_nodes int 或 None 正整數或 None None 最大葉節點數
min_impurity_decrease float 小於1正浮點數 0 節點劃分最小不純度,只有基尼係數、資訊增益比大於等於該值才分裂,0.19.1版本以前欄位名為min_impurity_split
class_weight dict, 字典的list, string 或 None 見下文 None 類別權重,取值見下文
presort bool True 或 False False 是否重排序以提高執行速度

3.1. max_features 引數取值

max_features 引數指節點分裂時參與判斷的最大特徵數,可以的取值有:

  • int – 特徵個數
  • float – 佔所有特徵的百分比
  • ‘auto’ – 特徵總數的開方
  • ‘sqrt’ – 特徵總數的開方
  • ‘log2’ – 特徵總數的 log2
  • None – 特徵總數

3.2. class_weight 引數取值

指定樣本各類別的的權重。
可選引數有:

  • None – 所有樣本類別權重均為 1
  • dict – 對應單條輸出結果,每個樣本類別的權重:{0: 1, 1: 5}
  • 字典的 list – 對應多條輸出結果: [{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}]
  • ‘balanced’ – sklearn 按照樣本量自動計算權重:樣本數/(類別數 * np.bincount(y))

3.3. 引數優化

模型的構建引數可以從以下條件考慮優化:

  • splitter – 特徵劃分點選擇標準,樣本量大時,使用 best 會導致訓練時間過長,推薦 random
  • max_depth – 決策樹的最大深度,樣本量大時,推薦限制最大深度取 10 到 100 之間
  • min_weight_fraction_leaf – 葉子節點最小的樣本總權重,如果我們有較多樣本有缺失值,或者分類樹樣本的分佈類別偏差很大,需要調整葉子節點的樣本權重
  • max_leaf_nodes – 最大葉子節點數,設定這個引數可以防止過擬合,如果特徵分成多的話,可以加以限制,具體的值可以通過交叉驗證得到
  • class_weight – 指定樣本各類別的的權重,主要是為了防止訓練集某些類別的樣本過多,導致訓練的決策樹過於偏向這些類別
  • presort – 樣本量大的時候設定為 True 會降低執行效率,推薦置為 False

4. sklearn.tree.DecisionTreeClassifier 的屬性

sklearn.tree.DecisionTreeClassifier 具有以下成員屬性。
####sklearn.tree.DecisionTreeClassifier 的成員屬性

屬性名 型別 說明
classes_ array 或 array 的 list 對於單條輸出為 array,結果類別陣列
feature_importances_ array 特徵重要性
max_features_ int 用於推斷的最大特徵數
n_classes_ int 或 list 對於單挑輸出為 int,結果類別數
n_features_ int 訓練完成後賦值,特徵數
n_outputs_ int 訓練完成後賦值,輸出結果數
tree_ 物件 訓練生成的決策樹
feature_importances_ ndarray 特徵相關度

5. sklearn.tree.DecisionTreeClassifier 成員函式

  • apply(X[, check_input]) – 返回樣本在葉子節點中的索引,check_input 為 False 則繞過所有引數檢測
  • decision_path(X[, check_input]) – 返回樣本的決策路徑
  • fit(X, y[, sample_weight, check_input, …]) – 訓練樣本
  • get_params([deep=True]) – 獲取全部引數,deep 為 True 則包含子物件
  • predict(X[, check_input]) – 預測 X 所屬分類
  • predict_log_proba(X) – 預測 X 所屬分類的對數機率
  • predict_proba(X[, check_input=True]) – 預測 X 屬於所有分類的可能性,check_input 為 False 則繞過所有引數檢測
  • score(X, y[, sample_weight]) – 為模型打分,可以通過 sample_weight 引數指定樣本權重
  • set_params(**params) – 設定所有引數

6. 用 sklearn 解決高爾夫預測問題

還是回到我們上一篇文章中的根據天氣預測是否打高爾夫球的問題:

# -*- coding: UTF-8 -*-
# {{{
import numpy
from sklearn.tree import DecisionTreeClassifier


def createDataSet():
    """
    建立資料集

    :return: 資料集與特徵集
    """
    dataSet = [['hot', 'sunny', 'high', 'false', 'no'],
               ['hot', 'sunny', 'high', 'true', 'no'],
               ['hot', 'overcast', 'high', 'false', 'yes'],
               ['cool', 'rain', 'normal', 'false', 'yes'],
               ['cool', 'overcast', 'normal', 'true', 'yes'],
               ['mild', 'sunny', 'high', 'false', 'no'],
               ['cool', 'sunny', 'normal', 'false', 'yes'],
               ['mild', 'rain', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'normal', 'true', 'yes'],
               ['mild', 'overcast', 'high', 'true', 'yes'],
               ['hot', 'overcast', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'high', 'true', 'no'],
               ['cool', 'sunny', 'normal', 'true', 'no'],
               ['mild', 'sunny', 'high', 'false', 'yes']]
    labels = ['日期', '氣候', '天氣', '氣溫', '寒冷']
    return dataSet, labels


if __name__ == '__main__':
    dataSet, labels = createDataSet()
    xList = list()
    yList = list()
    featureDict = dict()
    for dataList in dataSet:
        xTempList = list()
        for i in range(len(dataList) - 1):
            if type(dataList[i]).__name__ == 'str':
                if dataList[i] not in featureDict.get(i, list()):
                    if featureDict.get(i) is None:
                        featureDict[i] = list()
                    featureDict[i].append(dataList[i])
                xTempList.append(featureDict[i].index(dataList[i]))
            else:
                xTempList.append(dataList[i])
        xList.append(xTempList)
        if type(dataList[-1]).__name__ == 'str':
            if dataList[-1] not in featureDict.get(len(dataList) - 1, list()):
                if featureDict.get(len(dataList) - 1) is None:
                    featureDict[len(dataList) - 1] = list()
                featureDict.get(len(dataList) - 1, list()).append(dataList[-1])
            yList.append(featureDict[len(dataList) - 1].index(dataList[-1]))
        else:
            yList.append(dataList[-1])

    dt = DecisionTreeClassifier()
    dt.fit(numpy.array(xList), numpy.array(yList))

    xTest = [['hot', 'overcast', 'high', 'false'], ['mild', 'sunny', 'high', 'true']]
    xTestList = list()
    for featureValList in xTest:
        xTempList = list()
        for i in range(len(featureValList)):
            featureVal = featureValList[i]
            if type(featureVal).__name__ == 'str':
                if featureVal not in featureDict[i]:
                    print("測試資料異常:" + featureVal)
                index = featureDict[i].index(featureVal)
                xTempList.append(index)
            else:
                xTempList.append(featureVal)
        xTestList.append(xTempList)
    result = dt.predict(xTestList)
    for i in result:
        print(featureDict[len(dataSet[0]) - 1][i])
# }}}

輸出了:

yes
no

7. 特徵序列化 – sklearn.preprocessing.LabelEncoder

因為 sklearn 只能進行數值型運算,不能處理我們的字串樣本和結果,所以上面的程式碼中我們簡單地進行了樣本與數值的對映、儲存和轉化的序列化過程。
事實上,sklearn 也提供了序列化工具 – sklearn.preprocessing.LabelEncoder:
http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html。
可以方便的對結果進行轉化:

>>> from sklearn import preprocessing
>>> le = preprocessing.LabelEncoder()
>>> le.fit(["paris", "paris", "tokyo", "amsterdam"])
LabelEncoder()
>>> list(le.classes_)
['amsterdam', 'paris', 'tokyo']
>>> le.transform(["tokyo", "tokyo", "paris"]) 
array([2, 2, 1]...)
>>> list(le.inverse_transform([2, 2, 1]))
['tokyo', 'tokyo', 'paris']

7.1. 例項

下面,我們基於 sklearn.preprocessing.LabelEncoder 來對樣本進行序列化工作:

# -*- coding: UTF-8 -*-
# {{{
import pandas
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier


def createDataSet():
    """
    建立資料集

    :return: 資料集與特徵集
    """
    dataSet = [['hot', 'sunny', 'high', 'false', 'no'],
               ['hot', 'sunny', 'high', 'true', 'no'],
               ['hot', 'overcast', 'high', 'false', 'yes'],
               ['cool', 'rain', 'normal', 'false', 'yes'],
               ['cool', 'overcast', 'normal', 'true', 'yes'],
               ['mild', 'sunny', 'high', 'false', 'no'],
               ['cool', 'sunny', 'normal', 'false', 'yes'],
               ['mild', 'rain', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'normal', 'true', 'yes'],
               ['mild', 'overcast', 'high', 'true', 'yes'],
               ['hot', 'overcast', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'high', 'true', 'no'],
               ['cool', 'sunny', 'normal', 'true', 'no'],
               ['mild', 'sunny', 'high', 'false', 'yes']]
    labels = ['氣候', '天氣', '氣溫', '寒冷']
    return dataSet, labels


if __name__ == '__main__':
    dataSet, labels = createDataSet()
    yDataList = []
    for each in dataSet:
        yDataList.append(each[-1])

    dataDict = {}
    for each_label in labels:
        tempList = list()
        for each in dataSet:
            tempList.append(each[labels.index(each_label)])
        dataDict[each_label] = tempList

    dataPD = pandas.DataFrame(dataDict)

    leDict = dict()
    for col in dataPD.columns:
        leDict[col] = LabelEncoder()
        dataPD[col] = leDict[col].fit_transform(dataPD[col])

    dt = DecisionTreeClassifier()
    dt.fit(dataPD.values.tolist(), yDataList)

    xTest = [['hot', 'overcast', 'high', 'false'], ['mild', 'sunny', 'high', 'true']]
    testDict = {}
    for each_label in labels:
        tempList = list()
        for each in xTest:
            tempList.append(each[labels.index(each_label)])
        testDict[each_label] = tempList
    testPD = pandas.DataFrame(testDict)  # 生成pandas.DataFrame
    for col in testPD.columns:  # 為每一列序列化
        testPD[col] = leDict[col].transform(testPD[col])

    result = dt.predict(testPD.values.tolist())
    print(result)
# }}}

8. 繪製樹結構 – Graphviz

決策樹最大的優點是我們可以檢視最終的樹結構,上一篇日誌中,我們通過 matplotlib 展示了我們自己的樹結構。
但是 matplotlib 繪製樹結構較為複雜,我們這裡來了解一個更為易用的繪圖工具 – Graphviz。
Graphviz 不能通過 pip 直接安裝,需要我們手動在官網下載並安裝:
https://graphviz.gitlab.io/about/

安裝完成以後,需要在環境變數 Graphviz 的 bin 路徑。
然後,我們需要安裝 pydotplus,你也可以選擇安裝 pydot,這裡我們以 pydotplus 為例,使用 pydot 可以在網上找到示例程式碼。

pip install pydotplus

然後我們編寫程式碼:

# -*- coding: utf-8 -*-
# {{{
from io import StringIO
import pandas
import pydotplus
from sklearn import tree
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier


def createDataSet():
    """
    建立資料集

    :return: 資料集與特徵集
    """
    dataSet = [['hot', 'sunny', 'high', 'false', 'no'],
               ['hot', 'sunny', 'high', 'true', 'no'],
               ['hot', 'overcast', 'high', 'false', 'yes'],
               ['cool', 'rain', 'normal', 'false', 'yes'],
               ['cool', 'overcast', 'normal', 'true', 'yes'],
               ['mild', 'sunny', 'high', 'false', 'no'],
               ['cool', 'sunny', 'normal', 'false', 'yes'],
               ['mild', 'rain', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'normal', 'true', 'yes'],
               ['mild', 'overcast', 'high', 'true', 'yes'],
               ['hot', 'overcast', 'normal', 'false', 'yes'],
               ['mild', 'sunny', 'high', 'true', 'no'],
               ['cool', 'sunny', 'normal', 'true', 'no'],
               ['mild', 'sunny', 'high', 'false', 'yes']]
    labels = ['climate', 'weather', 'temple', 'cold']
    return dataSet, labels


if __name__ == '__main__':
    dataSet, labels = createDataSet()
    yDataList = []  # 提取每組資料的類別,儲存在列表裡
    for each in dataSet:
        yDataList.append(each[-1])

    dataDict = {}
    for each_label in labels:
        tempList = list()
        for each in dataSet:
            tempList.append(each[labels.index(each_label)])
        dataDict[each_label] = tempList

    dataPD = pandas.DataFrame(dataDict)

    leDict = dict()
    for col in dataPD.columns:
        leDict[col] = LabelEncoder()
        dataPD[col] = leDict[col].fit_transform(dataPD[col])

    dt =