決策樹的實現--(2)

1.4.代碼

1.4.1.實現代碼

在這裏插入圖片描述

machine learning in actions有詳細解釋。

from math import log
import operator
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

def createDataSet_loan():
    # 數據集
    dataSet = [[0, 0, 0, 0, 'no'],
               [0, 0, 0, 1, 'no'],
               [0, 1, 0, 1, 'yes'],
               [0, 1, 1, 0, 'yes'],
               [0, 0, 0, 0, 'no'],
               [1, 0, 0, 0, 'no'],
               [1, 0, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [2, 0, 1, 2, 'yes'],
               [2, 0, 1, 1, 'yes'],
               [2, 1, 0, 1, 'yes'],
               [2, 1, 0, 2, 'yes'],
               [2, 0, 0, 0, 'no']]
    # 分類屬性
    labels = ['年齡', '有工作', '有自己的房子', '信貸情況']
    # 返回數據集和分類屬性
    return dataSet, labels


def createDataSet_fish():
    """
    特徵-- 不浮出水面是否可以生存,以及是否有腳蹼,
    類別--魚類和非魚類
    :return:數據集(特徵+類別),特徵的意義
    """
    dataSet = [[1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0,0,'no']]
    labels = ['no surfacing','flippers']
    # change to discrete values
    return dataSet, labels

def calcShannonEnt(dataSet):
    """
    計算此時數據集的香農熵
    :param dataSet:數據集,(fea1,fea2,...,label)
    :return:int,香農熵
    """
    numEntries = len(dataSet)  # 樣本個數
    labelCounts = {}  # 計算每個類別的樣本個數
    for featVec in dataSet:  # 每個樣本
        currentLabel = featVec[-1]  # 該樣本的類別
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries  # 計算該類別的信息
        shannonEnt -= prob * log(prob,2)  # log base 2,把每個類別的信息的期望相加
    return shannonEnt
    
def splitDataSet(dataSet, axis, value):
    """
    去掉下標爲axis的特徵,一整列
    :param dataSet:數據集,(fea,fea,...,label)
    :param axis: 某個特徵的下標
    :param value: 該特徵(下標爲axis)的某個取值
    :return:  特徵值=value的數據集,並且去除該特徵
    """
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:  # 對特徵值=value的特徵進行刪除
            reducedFeatVec = featVec[:axis]  #
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet


def chooseBestFeatureToSplit(dataSet):
    """
    根據信息增益獲得信息增益最大的特徵的索引
    :param dataSet:(fea,fea,...,label)
    :return: 信息增益最大的特徵的索引值
    """
    numFeatures = len(dataSet[0]) - 1   # 特徵數量
    baseEntropy = calcShannonEnt(dataSet)  # 計算數據集的香農熵
    bestInfoGain = 0.0  # 信息增益
    bestFeature = -1  # 最優特徵的索引值
    for i in range(numFeatures):  # 遍歷所有特徵
        featList = [example[i] for example in dataSet]  # 獲取dataSet的第i個特徵
        uniqueVals = set(featList)  # 去除重複特徵
        newEntropy = 0.0
        for value in uniqueVals:  # 遍歷一個特徵的所有取值
            # 特徵值爲value的子數據集,並且除去該特徵
            subDataSet = splitDataSet(dataSet, i, value)  # 數據集,第幾個特徵,該特徵的一個取值
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)     
        infoGain = baseEntropy - newEntropy  # 信息增益
        print("第%d個特徵的增益爲%.3f" % (i, infoGain))
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain  # 更新信息增益,找到最大的信息增益
            bestFeature = i  # 記錄信息增益最大的特徵的索引值
    return bestFeature  #

def majorityCnt(classList):
    """
    統計classList中出現次數最多的元素(類標籤)
    服務於遞歸第兩個終止條件
    :param classList: 類標籤列表
    :return: 出現次數最多的元素(類標籤)
    """
    classCount={}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet,labels, featLabels):
    """
    創建決策樹(ID3算法)
    遞歸有兩個終止條件:
    1、所有的類標籤完全相同,直接返回類標籤
    2、用完所有標籤但是得不到唯一類別的分組,即特徵不夠用,挑選出現數量最多的類別作爲返回
    :param dataSet:
    :param labels:  特徵的意義
    :param featLabels: 存儲選擇的最優特徵標籤
    :return:
    """
    classList = [example[-1] for example in dataSet]  # 類別標籤,是否是魚類
    if classList.count(classList[0]) == len(classList): 
        return classList[0]  # 如果類別完全相同則停止繼續劃分
    if len(dataSet[0]) == 1: # 如果數據集沒有別的特徵,停止劃分
        return majorityCnt(classList)  # 遍歷完所有特徵時返回出現次數最多的類標籤
    bestFeat = chooseBestFeatureToSplit(dataSet)  # 選擇最優特徵
    bestFeatLabel = labels[bestFeat]  # 最優特徵的名稱,意義
    featLabels.append(bestFeatLabel)
    myTree = {bestFeatLabel:{}}  # 根據最優特徵的名稱生成樹
    del(labels[bestFeat])  # 刪除已經使用的特徵標籤
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    # 遞歸建立決策樹
    for value in uniqueVals:  # 特徵值爲value的子數據集,並且除去該特徵
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),
                                                  labels, featLabels)
    return myTree                            
    
def classify(inputTree,featLabels,testVec):
    # 獲取決策樹結點
    firstStr = next(iter(inputTree))
    # 下一個字典
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()
    
def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)


def getNumLeafs(myTree):
    """
    獲取決策樹葉子結點的數目
    :param myTree: 決策樹
    :return: None
    """
    # 初始化葉子
    numLeafs = 0
    # python3中myTree.keys()返回的是dict_keys,不是list,所以不能用
    # myTree.keys()[0]的方法獲取結點屬性,可以使用list(myTree.keys())[0]
    # next() 返回迭代器的下一個項目 next(iterator[, default])
    firstStr = next(iter(myTree))
    # 獲取下一組字典
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        # 測試該結點是否爲字典,如果不是字典,代表此節點爲葉子結點
        if type(secondDict[key]).__name__ == 'dict':
            numLeafs += getNumLeafs(secondDict[key])
        else:
            numLeafs += 1
    return numLeafs


def getTreeDepth(myTree):
    """
    獲取決策樹的層數
    :param myTree: 決策樹
    :return: 決策樹的層數
    """
    # 初始化決策樹深度
    maxDepth = 0
    # python3中myTree.keys()返回的是dict_keys,不是list,所以不能用
    # myTree.keys()[0]的方法獲取結點屬性,可以使用list(myTree.keys())[0]
    # next() 返回迭代器的下一個項目 next(iterator[, default])
    firstStr = next(iter(myTree))
    # 獲取下一個字典
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        # 測試該結點是否爲字典,如果不是字典,代表此節點爲葉子結點
        if type(secondDict[key]).__name__ == 'dict':
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:
            thisDepth = 1
        # 更新最深層數
        if thisDepth > maxDepth:
            maxDepth = thisDepth
    # 返回決策樹的層數
    return maxDepth


def plotMidText(cntrPt, parentPt, txtString):
    """
    標註有向邊屬性值
    :param cntrPt: 用於計算標註位置
    :param parentPt: 用於計算標註位置
    :param txtString: 標註內容
    :return: None
    """
    # 計算標註位置(箭頭起始位置的中點處)
    xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)


def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    """
    繪製結點
    :param nodeTxt: 結點名
    :param centerPt: 文本位置
    :param parentPt: 標註的箭頭位置
    :param nodeType: 結點格式
    :return: None
    """
    # 定義箭頭格式
    arrow_args = dict(arrowstyle="<-")
    # 設置中文字體
    font = FontProperties(fname=r"C:\Windows\Fonts\simsun.ttc", size=14)
    # 繪製結點createPlot.ax1創建繪圖區
    # annotate是關於一個數據點的文本
    # nodeTxt爲要顯示的文本,centerPt爲文本的中心點,箭頭所在的點,parentPt爲指向文本的點
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va='center', ha='center', bbox=nodeType,
                            arrowprops=arrow_args, FontProperties=font)


def plotTree(myTree, parentPt, nodeTxt):
    """
    繪製決策樹
    :param myTree: 決策樹(字典)
    :param parentPt: 標註的內容
    :param nodeTxt: 結點名
    :return: None
    """
    # 設置結點格式boxstyle爲文本框的類型,sawtooth是鋸齒形,fc是邊框線粗細
    decisionNode = dict(boxstyle="sawtooth", fc="0.8")
    # 設置葉結點格式
    leafNode = dict(boxstyle="round4", fc="0.8")
    # 獲取決策樹葉結點數目,決定了樹的寬度
    numLeafs = getNumLeafs(myTree)
    # 獲取決策樹層數
    depth = getTreeDepth(myTree)
    # 下個字典
    firstStr = next(iter(myTree))
    # 中心位置
    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]
    # y偏移
    plotTree.yoff = plotTree.yoff - 1.0 / plotTree.totalD
    for key in 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


def createPlot(inTree):
    """
    創建繪圖面板
    :param inTree: 決策樹(字典)
    :return: None
    """
    # 創建fig
    fig = plt.figure(1, facecolor="white")
    # 清空fig
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    # 去掉x、y軸
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
    # 獲取決策樹葉結點數目
    plotTree.totalW = float(getNumLeafs(inTree))
    # 獲取決策樹層數
    plotTree.totalD = float(getTreeDepth(inTree))
    # x偏移
    plotTree.xoff = -0.5 / plotTree.totalW
    plotTree.yoff = 1.0
    # 繪製決策樹
    plotTree(inTree, (0.5, 1.0), '')
    # 顯示繪製結果
    plt.show()


def main():
    dataSet, features = createDataSet_loan()
    featLabels = []
    myTree = createTree(dataSet, features, featLabels)
    # 保存樹
    storeTree(myTree, 'classifierStorage.txt')
    # 加載樹
    myTree = grabTree('classifierStorage.txt')
    print(myTree)
    # 測試數據
    # testVec = [1, 1]  # fish
    testVec = [1, 1, 0, 0]  # loan
    result = classify(myTree, featLabels, testVec)
    if result == 'yes':
        print('是魚類')
    if result == 'no':
        print('不是魚類')
    # print(myTree)
    createPlot(myTree)
    # print(dataSet)
    # print(calcShannonEnt(dataSet))
    # print("最優特徵索引值:" + str(chooseBestFeatureToSplit(dataSet)))

if __name__ == '__main__':
    main()

1.4.2.sklearn代碼

class sklearn.tree.DecisionTreeClassifier(criterion=’gini’,
splitter=’best’,
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
max_features=None,
random_state=None,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
class_weight=None, presort=False)

參數:
(1)criterion:字符型,可選,規定了該決策樹所採用的的最佳分割屬性的判決方法,有兩種:“gini”,“entropy”,默認爲’gini’,

(2)splitter: 字符型,可選(默認=“best”)用於在每個節點選擇分割的策略,可填‘best’或’random’,前者在特徵的所有劃分點中找出最優的劃分點。後者是隨機的在部分劃分點中找局部最優的劃分點。默認的"best"適合樣本量不大的時候,而如果樣本數據量非常大,此時決策樹構建推薦"random"

(3)max_depth: int型或None,可選(默認=None)樹的最大深度,對於防止過擬合非常有用。如果不輸入的話,決策樹在建立子樹的時候不會限制子樹的深度。一般來說,數據少或者特徵少的時候可以不管這個值。如果模型樣本量多,特徵也多的情況下,推薦限制這個最大深度,具體的取值取決於數據的分佈。常用的可以取值10-100之間。

(4)min_samples_split: int型float型,可選(默認值=2)分割內部節點所需的最小樣本數量;如果是int型,則min_samples_split爲最小樣本數量。如果是float型,則min_samples_split是分數,ceil(min_samples_split * n_samples)是每個分割的最小樣本數。

(5)min_samples_leaf: int型float型,可選(默認值=1)葉節點上所需的最小樣本數。如果是int類型,則將min_samples_leaf作爲最小值。若是float,那麼min_samples_leaf是分數,ceil(min_samples_leaf * n_samples)是每個節點的最小樣本數。

(6)min_weight_fraction_leaf:浮點型,可選(默認=0)。葉節點上(所有輸入樣本的)權值之和的最小加權分數。當沒有提供 sample_weight時,樣品的重量相等。

(7)max_features: int, float, string or None, 可選(默認=None),在尋找最佳分割時需要考慮的特性數量;如果是int類型,則考慮每個分割處的max_features特性。如果是float,那麼max_features是一個分數,並且在每個分割中都考慮int(max_features * n_features)特性。如果“auto”,則max_features=sqrt(n_features)。如果“sqrt”,則max_features=sqrt(n_features)。如果“log2”,則max_features=log2(n_features)。如果沒有,則max_features=n_features。

(8)class_weight: 指定樣本各類別的的權重,主要是爲了防止訓練集某些類別的樣本過多,導致訓練的決策樹過於偏向這些類別。這裏可以自己指定各個樣本的權重,或者用“balanced”,如果使用“balanced”,則算法會自己計算權重,樣本量少的類別所對應的樣本權重會高。當然,如果你的樣本類別分佈沒有明顯的偏倚,則可以不管這個參數,選擇默認的"None"

(9)random_state: int型RandomState實例或None,可選(默認=None)如果是int, random_state是隨機數生成器使用的種子;如果是RandomState實例,random_state是隨機數生成器;如果沒有,隨機數生成器是np.random使用的RandomState實例。

方法:
apply(X[, check_input])
decision_path(X[, check_input])
fit(X, y[, sample_weight, check_input, …])
get_params([deep])
predict(X[, check_input])
predict_log_proba(X)
predict_proba(X[, check_input])
score(X, y[, sample_weight])
set_params(**params)


# coding: utf-8
# Author: shelley
# 2020/5/1210:34

# 1)sklearn實現二分類
from sklearn import tree

X = [[0, 0], [1, 1]]
Y = [0, 1]
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X, Y)
print(clf.predict([[2, 2]]))  # 預測屬於哪個類
print(clf.predict_proba([[2, 2]]))  # 預測屬於每個類的概率

# 2)sklearn實現多分類
from sklearn.datasets import load_iris
from sklearn import tree

iris = load_iris()
clf = tree.DecisionTreeClassifier()
clf = clf.fit(iris.data, iris.target)

# 3)sklearn實現樹迴歸
# 注意:迴歸的時候y的值應該是浮點數,而不是整數值
from sklearn import tree

X = [[0, 0], [2, 2]]
y = [0.5, 0.5]
clf = tree.DecisionTreeRegressor()
clf = clf.fit(X, y)
print(clf.predict([[1, 1]]))

2.代碼和數據

鏈接:https://pan.baidu.com/s/1QbeG8g_9JhVwJRhERViTMg
提取碼:80su

github:
https://github.com/shelleyHLX/machine-learning

3.參考

機器學習實戰
一些博客,如有侵權,請聯繫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章