使用 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 =