【Python機器學習實戰】決策樹和集成學習(二)——決策樹的實現


摘要:上一節對決策樹的基本原理進行了梳理,本節主要根據其原理做一個邏輯的實現,然后調用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版本的問題,沒有調通,后面再進一步去調,上面建模過程也是其他機器學習的一個較為通用建模流程,不過在數據的處理過程中根據需求有所不同,在此有了一個初步的了解。接下來就是由決策樹延伸至集成學習的相關內容了。


免責聲明!

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



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