摘要:上一節對決策樹的基本原理進行了梳理,本節主要根據其原理做一個邏輯的實現,然后調用sklearn的包實現決策樹分類。
這里主要是對分類樹的決策進行實現,算法采用ID3,即以信息增益作為划分標准進行。
首先計算數據集的信息熵,代碼如下:
1 import math 2 import numpy as np 3 4 5 def calcShannonEnt(data): 6 num = len(data) 7 # 保存每個類別的數目 8 labelCounts = {} 9 # 每一個樣本 10 for featVec in data: 11 currentLabel = featVec[-1] 12 if currentLabel not in labelCounts.keys(): 13 labelCounts[currentLabel] = 0 14 labelCounts[currentLabel] += 1 15 # 計算信息增益 16 shannonEnt = 0 17 for key in labelCounts.keys(): 18 prob = float(labelCounts[key] / num) 19 shannonEnt -= prob * math.log(prob) 20 return shannonEnt
然后是依據某個特征的特征值將數據划分開的函數:
def splitData(dataSet, axis, value): """ axis為某一特征維度 value為划分該維度的值 """ retDataSet = [] for featVec in dataSet: if featVec[axis] == value: # 舍棄掉這一維度上對應的值,剩余部分作為新的數據集 reducedFeatVec = featVec[:axis] reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet
這個函數是依據選取的某個維度的某個值,分割后的數據,比如:
>>data Out[1]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] >>splitData(data, 0, 1) Out[2]: [[1, 'yes'], [1, 'yes'], [0, 'no']] >>splitData(data, 0, 0) Out[3]: [[1, 'no'], [1, 'no']]
接下來就是從數據中選取信息增益最大的特征了,輸入是數據集,返回信息增益最大的特征的index,代碼如下:
# 選擇最好的特征進行數據划分 # 輸入dataSet為二維List def chooseBestFeatuerToSplit(dataSet): # 計算樣本所包含的特征數目 numFeatures = len(dataSet[0]) - 1 # 信息熵H(Y) baseEntropy = calcShannonEnt(dataSet) # 初始化 bestInfoGain = 0; bestFeature = -1 # 遍歷每個特征,計算信息增益 for i in range(numFeatures): # 取出對應特征值,即一列數據 featList = [example[i] for example in dataSet] uniqueVals = np.unique(featList) newEntropy = 0 for value in uniqueVals: subDataSet = splitData(dataSet, i, value) prob = len(subDataSet)/float(dataSet) newEntropy += prob * calcShannonEnt(subDataSet) # 計算信息增益G(Y, X) = H(Y) - sum(H(Y|x)) infoGain = baseEntropy - newEntropy if infoGain > bestInfoGain: bestInfoGain = infoGain bestFeature = i return bestFeature
接下來就是遞歸上面的代碼,構建決策樹了,上節提到,遞歸結束的條件一般是遍歷完所有特征屬性,或者到某個分支下僅有一個類別了,則得到一個葉子節點。但有時即使我們已經處理了所有的屬性,但是在葉子節點時依舊沒能將數據完全分開,在這種情況下,通常采用多數表決的方法決定該葉子節點所屬類別。
def majorityCnt(classList): classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] += 1 # 按統計個數進行倒序排序 sortedClassCount = sorted(classCount.items(), key=lambda item: item[1], reverse=True) return sortedClassCount[0][0]
然后就可以創建一顆決策樹了,代碼如下:
def creatTree(dataSet, labels): """ labels為特征的標簽列表 """ classList = [example[-1] for example in dataSet] # 如果data中的都為同一種類別,則停止,且返回該類別 if classList.count(classList[0]) == len(classList): return classList[0] # 如果數據集中僅剩類別這一列了,即特征使用完,仍沒有分開,則投票 if len(dataSet[0]) == 1: return majorityCnt(classList) bestFeat = chooseBestFeatuerToSplit(dataSet) bestFeatLabel = labels[bestFeat] # 初始化樹,用於存儲樹的結構,是很多字典的嵌套結構 myTree = {bestFeatLabel: {}} # 已經用過的特征刪去 del (labels[bestFeatLabel]) # 取出最優特征這一列的值 featVals = [example[bestFeat] for example in dataSet] # 特征的取值個數 uniqueVals = np.unique(featVals) # 開始遞歸分裂 for value in uniqueVals: subLabels = labels[:] myTree[bestFeatLabel][value] = creatTree(splitData(dataSet, bestFeat, value), subLabels) return myTree
這樣一顆決策樹就構建完成了,使用上面那個小量的數據測試一下:
>>data Out[1]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] >>labels Out[2]: ['no surfacing', 'flippers'] >>creatTree(data, labels) Out[3]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
接下來要根據所建立的決策樹,對新的樣本進行分類,同樣用到遞歸的方法:
def classify(inputTree, featLabels, testVec): # 自上而下搜索預測樣本所屬類別 firstStr = inputTree.key()[0] secondDict = inputTree[firstStr] featIndex = featLabels.index(firstStr) for key in secondDict.keys(): # 按照特征的位置確定搜索方向 if testVec[featIndex] == key: if type(secondDict[key]).__name__ == 'dict': # 若下一級結構還是dict,遞歸搜索 classLabel = classify(secondDict, featLabels, testVec) else: classLabel = secondDict[key] return classLabel
同時,還有對決策樹的可視化,下面直接給出畫圖的代碼
decisionNode = dict(boxstyle='sawtooth', fc="0.8") leafNode = dict(boxstyle='round4', fc="0.8") arrow_args = dict(arrowstyle='<-') def plotNode(nodeTxt, centerPt, parentPt, nodeType): createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, textcoords='axes fraction', va="center", ha="center", bbox=nodeType, arrowprops=arrow_args) def createPlot(inTree): fig = plt.figure(1, facecolor='white') fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False) plotTree.totalW = float(getNumLeafs(inTree)) plotTree.totalD = float(getTreeDepth(inTree)) plotTree.xOff = -0.5/plotTree.totalW plotTree.yOff = 1.0 plotTree(inTree, (0.5, 1.0), '') # plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode) # plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode) plt.show() def getNumLeafs(myTree): numLeafs = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in list(secondDict.keys()): if type(secondDict[key]).__name__ == 'dict': numLeafs += getNumLeafs(secondDict[key]) else: numLeafs += 1 return numLeafs def getTreeDepth(myTree): maxDepth = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in list(secondDict.keys()): if type(secondDict[key]).__name__ == 'dict': thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth def retrieveTree(i): listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}, ] return listOfTrees[i] def plotMidText(cntrPt, parentPt, txtString): xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0] yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1] createPlot.ax1.text(xMid, yMid, txtString) def plotTree(myTree, parentPt, nodeTxt): numLeafs = getNumLeafs(myTree) depth = getTreeDepth(myTree) firstStr = list(myTree.keys())[0] cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) plotMidText(cntrPt, parentPt, nodeTxt) plotNode(firstStr, cntrPt, parentPt, decisionNode) secondDict = myTree[firstStr] plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD for key in list(secondDict.keys()): if type(secondDict[key]).__name__ == 'dict': plotTree(secondDict[key], cntrPt, str(key)) else: plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
運行createPlot函數,即可得到決策樹的可視化,同樣運用上面那個簡單的數據集:

上面就是決策樹的一個簡單實現過程,下面我們運用“隱形眼鏡數據集”對上面的模型進行測試,部分數據如下:

前四列是樣本特征,最后一列為樣本類別,運用上邊的數據集,對模型進行測試:
fr = open('lenses.txt') lenses = [inst.strip().split('\t') for inst in fr.readlines()] lenses_labels = ['age', 'prescript', 'astigmatic', 'tearRate'] lenses_Tree = creatTree(lenses, lenses_labels) createPlot(lenses_Tree)

接下來就是對樹的剪枝操作,這里主要方法是通過剪枝生成所有可能的樹,然后利用測試集,選擇最好的樹(錯誤率最低)的樹出來,首先建立用預測數據正確率和投票節點的函數:
def testing(myTree, data_test, labels): error = 0.0 for i in range(len(data_test)): classLabel = classify(myTree, labels, data_test[i]) if classLabel != data_test[i][-1]: error += 1 return float(error) # 測試投票節點正確率 def testingMajor(major, data_test): error = 0.0 for i in range(len(data_test)): if major[0] != data_test[i][-1]: error += 1 # print 'major %d' %error return float(error)
然后同樣采用遞歸的方法,產生所有可能的決策樹,然后舍棄掉錯誤率較高的樹:
def postPruningTree(inTree, dataSet, test_data, labels): """ :param inTree: 原始樹 :param dataSet:數據集 :param test_data:測試數據,用於交叉驗證 :param labels:標簽集 """ firstStr = list(inTree.keys())[0] secondDict = inTree[firstStr] classList = [example[-1] for example in dataSet] labelIndex = labels.index(firstStr) temp_labels = copy.deepcopy(labels) del (labels[labelIndex]) for key in list(secondDict.keys()): if type(secondDict[key]).__name__ == 'dict': if type(dataSet[0][labelIndex]).__name__ == 'str': subDataSet = splitData(dataSet, labelIndex, key) subDataTest = splitData(test_data, labelIndex, key) if len(subDataTest) > 0: inTree[firstStr][key] = postPruningTree(secondDict[key], subDataSet, subDataTest, copy.deepcopy(labels)) if testing(inTree, test_data, temp_labels) < testingMajor(majorityCnt(classList), temp_labels): return inTree return majorityCnt(classList)
至此,一個簡單的ID3決策樹已經實現完成了,僅作為算法的理解過程,這里沒有考慮連續型數據的處理問題,以及算法中很多不合理的地方。下面就使用python自帶的sklearn進行決策樹的建立,同時使用另一個比較著名的數據集——“紅酒數據集”(數據集鏈接放在末尾)對建模過程進行了解。
首先導入決策樹所需的包:
# 導入決策樹包 from sklearn.tree import DecisionTreeClassifier # 畫圖工具包 import matplotlib.pyplot as plt import seaborn as sns sns.set(color_codes=True) # 導入數據處理的包 from sklearn.model_selection import train_test_split # 模型評估 from sklearn import metrics from sklearn.metrics import accuracy_score, f1_score, recall_score, precision_score import missingno as msno_plot
然后是讀取數據,並用describe函數對數據做一個初步的查看:
# 讀取紅酒數據 wine_df =pd.read_csv('F:\自學2020\PythonML_Code\Charpter 3\winequality-red.csv', sep=';') # 查看數據, 數據有11個特征,類別為quality wine_df.describe().transpose().round(2)

從統計樣本count一列來看數據無缺失值,為更直觀顯示,畫出缺失值直方圖,如下:
plt.title('Non-missing values by columns') msno_plot.bar(wine_df)

接下來就是異常值的檢查,通過每一列數據的箱型圖來查看是否存在偏離較遠的異常值:
# 通過箱型圖查看每一列的箱型圖 plt.figure() pos = 1 for i in wine_df.columns: plt.subplot(3, 4, pos) sns.boxplot(wine_df[i]) pos += 1

接下來處理異常值,在案例中采用樣本的四分之一分位數、四分之三中位數組成的IQR四分位數間的范圍來對偏離較遠的點進行修正,代碼如下:
# 處理缺失值 columns_name = list(wine_df.columns) for name in columns_name: q1, q2, q3 = wine_df[name].quantile([0.25, 0.5, 0.75]) IQR = q3 - q1 lower_cap = q1 - 1.5*IQR upper_cap = q3 + 1.5*IQR wine_df[name] = wine_df[name].apply(lambda x: upper_cap if x > upper_cap else (lower_cap if (x<lower_cap) else x))
然后看下數據兩兩之間的相關性,sns.pairplot()是展現涼涼之間的線性、非線性和相關等關系。http://seaborn.pydata.org/generated/seaborn.pairplot.html
sns.pairplot(wine_df)

進一步查看兩個變量相關性和關系,注意:在決策樹中不需要刪除高度相關的特征,因為節點只使用一個獨立變量被划分為子節點,因此,即使兩個變量高度相關,產生最高信息增益的變量也會用於分析。查看變量之間的相關性代碼如下:
plt.figure(figsize=(10, 8)) sns.heatmap(wine_df.corr(), annot=True, linewidths=.5, center=0, cbar=False, cmap='YlGnBu')

同時,分類問題對於類別的分布情況比較敏感,因此需要查看quality中各個類別的分布情況:
plt.figure(figsize=(10, 8)) sns.countplot(wine_df['quality'])

注意到這里類別中存在3.5連續型數值,要對其進行特殊處理,這里直接刪去這一部分樣本即可,因為樣本量較少,可以看到類別分布相對不是很平衡,因此需要將類別平衡,通過將類別“quality”屬性的值組合產生(或者其他的方法):
wine_df = wine_df[wine_df['quality'] != 3.5] wine_df = wine_df[wine_df['quality'] != 7.5] wine_df['quality'] = wine_df['quality'].replace(8, 7) wine_df['quality'] = wine_df['quality'].replace(3, 5) wine_df['quality'] = wine_df['quality'].replace(4, 5) wine_df['quality'].value_counts(normalize=True)
接下來就是將數據分為訓練集和測試兩部分,測試集是為了檢查模型的正確性和准確性,看是否欠擬合或者過擬合:
X_train, X_test, Y_train, Y_test = train_test_split(wine_df.drop(['quality'], axis=1), wine_df['quality'], test_size=0.3, random_state=22) print(X_train.shape, X_test.shape)
Output:(1119, 11) (480, 11)
然后就是確定一個模型,模型及參數詳解如下,具體參數解釋可參考:https://www.cnblogs.com/hgz-dm/p/10886368.html
model = DecisionTreeClassifier(criterion='gini', random_state=100, max_depth=3, min_samples_leaf=5) """ criterion:度量函數,包括gini、entropy等 class_weight:樣本權重,默認為None,也可通過字典形式制定樣本權重,如:假設樣本中存在4個類別,可以按照 [{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}] 這樣的輸入形式設置4個類的權重分別為1、5、1、1,而不是 [{1:1}, {2:5}, {3:1}, {4:1}]的形式。 該參數還可以設置為‘balance’,此時系統會按照輸入的樣本數據自動的計算每個類的權重,計算公式為:n_samples/( n_classes*np.bincount(y)), 其中n_samples表示輸入樣本總數,n_classes表示輸入樣本中類別總數,np.bincount(y) 表示計算屬於每個類的樣本個數,可以看到, 屬於某個類的樣本個數越多時,該類的權重越小。若用戶單獨指定了每個樣本的權重,且也設置了class_weight參數,則系統會將該樣本單獨指定 的權重乘以class_weight指定的其類的權重作為該樣本最終的權重。 max_depth: 設置樹的最大深度,即剪枝,默認為None,通常會限制最大深度防止過擬合一般為5~20,具體視樣本分布來定 splitter: 節點划分策略,默認為best,還可以設置為random,表示最優隨機划分,一般用於數據量較大時,較小運算量 min_sample_leaf: 指定的葉子結點最小樣本數,默認為1,只有划分后其左右分支上的樣本個數不小於該參數指定的值時,才考慮將該結點划分也就是說, 當葉子結點上的樣本數小於該參數指定的值時,則該葉子節點及其兄弟節點將被剪枝。在樣本數據量較大時,可以考慮增大該值,提前結束樹的生長。 random_state: 當splitter設置為random時,可以通過該參數設計隨機種子數 min_sample_split: 對一個內部節點划分時,要求該結點上的最小樣本數,默認為2 max_features: 划分節點時,所允許搜索的最大的屬性個數,默認為None,auto表示最多搜索sqrt(n)個屬性,log2表示最多搜索log2(n)個屬性,也可以設置整數; min_impurity_decrease :打算划分一個內部結點時,只有當划分后不純度(可以用criterion參數指定的度量來描述)減少值不小於該參數指定的值,才會對該 結點進行划分,默認值為0。可以通過設置該參數來提前結束樹的生長。 min_impurity_split : 打算划分一個內部結點時,只有當該結點上的不純度不小於該參數指定的值時,才會對該結點進行划分,默認值為1e-7。該參數值0.25 版本之后將取消,由min_impurity_decrease代替。 """
接下來就是利用訓練數據對模型進行訓練:
model.fit(X_train, Y_train)
然后就是查看模型,並對畫出訓練出來的決策樹(這里卡了很久,網上有很多解決辦法,在一台電腦成功顯示,但另一台總有問題):

然后查看模型在訓練集和測試集上的准確率:
test_labels = model.predict(X_test) train_labels = model.predict(X_train) print('測試集上的准確率為%s'%accuracy_score(Y_test, test_labels)) print('訓練集上的准確率為%s'%accuracy_score(Y_train, train_labels)) 測試集上的准確率為0.6101694915254238 訓練集上的准確率為0.6014558689717925
查看每個特征對於樣本分類的重要性程度:
feat_importance = model.tree_.compute_feature_importances(normalize=False) feat_imp_dict = dict(zip(feature_cols, model.feature_importances_)) feat_imp = pd.DataFrame.from_dict(feat_imp_dict, orient='index') feat_imp.rename(columns={0: 'FeatureImportance'}, inplace=True) feat_imp.sort_values(by=['FeatureImportance'], ascending=False).head() Output: FeatureImportance alcohol 0.507726 sulphates 0.280996 total sulfur dioxide 0.190009 volatile acidity 0.021269 fixed acidity 0.000000
前面建模時有提到一些參數如max_depth、min_samples_leaf等參數決定樹的提前終止來防止過擬合,而在實際應用中想要找出最佳的一組參數並不容易(但也不是不可能,可以通過GridSearchCV的方法對模型進行模型),另一種在上一節中提到的后剪枝算法,即確定不同的α值,找出最優的決策樹,下面看一下α值的變化與數據數據不純度的變化關系:
path = model.cost_complexity_pruning_path(X_train, Y_train) ccp_alphas, impurities = path.ccp_alphas, path.impurities fig, ax = plt.figure(figsize=(16, 8)) ax.plot(ccp_alphas[:-1], impurities[:-1], marker='o', drawstyle='steps-post') ax.set_xlabel('effective alpha') ax.set_ylabel('total impurity of leaves')

# 根據不同的alpha生成不同的樹並保存 clfs = [] for ccp_alpha in ccp_alphas: clf = DecisionTreeClassifier(random_state=0, ccp_alpha=ccp_alpha) clf.fit(X_train, Y_train) clfs.append(clf) # 刪去最后一個元素,因為最后只有一個節點 clfs = clfs[:-1] ccp_alphas = ccp_alphas[:-1] # 查看樹的總節個點數和樹的深度隨alpha的變化 node_counts = [clf.tree_.node_count for clf in clfs] depth = [clf.tree_.max_depth for clf in clfs] fig, ax = plt.subplot(2, 1) ax[0].plot(ccp_alphas, node_counts, marker='o', drawstyle='steps-post') ax[0].set_xlabel('alpha') ax[0].set_ylabel('number of nodes') ax[0].set_title("Number of nodes vs alpha") ax[1].plot(ccp_alphas, depth, marker='o', drawstyle='steps-post') ax[1].set_xlabel('alpha') ax[1].set_ylabel('depth of Tree') ax[1].set_title("Depth vs alpha") fig.tight_layout()

# 查看不同樹的訓練誤差和測試誤差變化關系 train_scores = [clf.score(X_train, Y_train) for clf in clfs] test_scores = [clf.score(X_test, Y_test) for clf in clfs] fig, ax = plt.subplots() ax.plot(ccp_alphas, train_scores, marker='o', label='train', drawstyle='steps-post') ax.plot(ccp_alphas, test_scores, marker='o', label='test', drawstyle='steps-post') ax.set_xlabel('alpha') ax.set_ylabel('accuracy') ax.legend() plt.show()

根據上述比較,可以選出最優的α和對應的模型:
i = np.arange(len(ccp_alphas)) ccp = pd.DataFrame({'Depth': pd.Series(depth, index=i), 'Node': pd.Series(node_counts, index=i), 'ccp': pd.Series(ccp_alphas, index=i), 'train_scores': pd.Series(train_scores, index=i), 'test_scores': pd.Series(test_scores, index=i)}) ccp.tail() best = ccp[ccp['test_scores'] == ccp['test_scores'].max()]
參考文獻:
官方文檔:http://seaborn.pydata.org/generated/seaborn.pairplot.html
博客:https://www.cnblogs.com/panchuangai/p/13445819.html
博客:https://www.cnblogs.com/hgz-dm/p/10886368.html
官方文檔:https://scikit-learn.org/stable/auto_examples/tree/plot_cost_complexity_pruning.html
至此,一個簡單的決策樹案例就算完成了,在實現的過程中也踩了很多坑,有的由於軟件和package版本的問題,沒有調通,后面再進一步去調,上面建模過程也是其他機器學習的一個較為通用建模流程,不過在數據的處理過程中根據需求有所不同,在此有了一個初步的了解。接下來就是由決策樹延伸至集成學習的相關內容了。
