史詩級(jí)萬字干貨,繼續(xù)尬聊決策樹!
大家好,我是 Jack。
好久沒繼續(xù)更新我的 《機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》 系列文章了。
為了方便大家查閱,我在重新整理和完善這個(gè)系列,陸續(xù)同步發(fā)到公眾號(hào)上。
因?yàn)槭莻€(gè)人網(wǎng)站的老文章,可能有部分讀者朋友已經(jīng)看過,所以更新不會(huì)太頻繁。
目前已經(jīng)同步的兩篇:
以及新增的一篇:
我會(huì)把整個(gè)《機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》系列挖過的坑,陸續(xù)填好。
今天,繼續(xù)聊聊決策樹:化身醫(yī)生,用機(jī)器學(xué)習(xí)算法,為患者選型隱形眼鏡。
決策樹構(gòu)建
上篇文章也粗略提到過,構(gòu)建決策樹的算法有很多。篇幅原因,本篇文章只使用ID3算法構(gòu)建決策樹。
1、ID3算法
ID3算法的核心是在決策樹各個(gè)結(jié)點(diǎn)上對(duì)應(yīng)信息增益準(zhǔn)則選擇特征,遞歸地構(gòu)建決策樹。
具體方法是:
從根結(jié)點(diǎn)(root node)開始,對(duì)結(jié)點(diǎn)計(jì)算所有可能的特征的信息增益,選擇信息增益最大的特征作為結(jié)點(diǎn)的特征,由該特征的不同取值建立子節(jié)點(diǎn);
再對(duì)子結(jié)點(diǎn)遞歸地調(diào)用以上方法,構(gòu)建決策樹;
直到所有特征的信息增益均很小或沒有特征可以選擇為止。最后得到一個(gè)決策樹。
ID3相當(dāng)于用極大似然法進(jìn)行概率模型的選擇。
在使用ID3構(gòu)造決策樹之前,我們?cè)俜治鱿聰?shù)據(jù)。

利用上篇文章求得的結(jié)果,由于特征A3(有自己的房子)的信息增益值最大,所以選擇特征A3作為根結(jié)點(diǎn)的特征。
它將訓(xùn)練集D劃分為兩個(gè)子集D1(A3取值為"是")和D2(A3取值為"否")。
由于D1只有同一類的樣本點(diǎn),所以它成為一個(gè)葉結(jié)點(diǎn),結(jié)點(diǎn)的類標(biāo)記為“是”。
對(duì)D2則需要從特征A1(年齡),A2(有工作)和A4(信貸情況)中選擇新的特征,計(jì)算各個(gè)特征的信息增益:
根據(jù)計(jì)算,選擇信息增益最大的特征A2(有工作)作為結(jié)點(diǎn)的特征。由于A2有兩個(gè)可能取值,從這一結(jié)點(diǎn)引出兩個(gè)子結(jié)點(diǎn):
一個(gè)對(duì)應(yīng)"是"(有工作)的子結(jié)點(diǎn),包含3個(gè)樣本,它們屬于同一類,所以這是一個(gè)葉結(jié)點(diǎn),類標(biāo)記為"是";
另一個(gè)是對(duì)應(yīng)"否"(無工作)的子結(jié)點(diǎn),包含6個(gè)樣本,它們也屬于同一類,所以這也是一個(gè)葉結(jié)點(diǎn),類標(biāo)記為"否"。
這樣就生成了一個(gè)決策樹,該決策樹只用了兩個(gè)特征(有兩個(gè)內(nèi)部結(jié)點(diǎn)),生成的決策樹如下圖所示。

這樣我們就使用ID3算法構(gòu)建出來了決策樹,接下來,讓我們看看如何進(jìn)行代實(shí)現(xiàn)。
2、編寫代碼構(gòu)建決策樹
我們使用字典存儲(chǔ)決策樹的結(jié)構(gòu),比如上小節(jié)我們分析出來的決策樹,用字典可以表示為:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
創(chuàng)建函數(shù)majorityCnt統(tǒng)計(jì)classList中出現(xiàn)此處最多的元素(類標(biāo)簽),創(chuàng)建函數(shù)createTree用來遞歸構(gòu)建決策樹。編寫代碼如下:
# -*- coding: UTF-8 -*-
from math import log
import operator
"""
函數(shù)說明:計(jì)算給定數(shù)據(jù)集的經(jīng)驗(yàn)熵(香農(nóng)熵)
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
shannonEnt - 經(jīng)驗(yàn)熵(香農(nóng)熵)
Author:
Jack Cui
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回?cái)?shù)據(jù)集的行數(shù)
labelCounts = {} #保存每個(gè)標(biāo)簽(Label)出現(xiàn)次數(shù)的字典
for featVec in dataSet: #對(duì)每組特征向量進(jìn)行統(tǒng)計(jì)
currentLabel = featVec[-1] #提取標(biāo)簽(Label)信息
if currentLabel not in labelCounts.keys(): #如果標(biāo)簽(Label)沒有放入統(tǒng)計(jì)次數(shù)的字典,添加進(jìn)去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計(jì)數(shù)
shannonEnt = 0.0 #經(jīng)驗(yàn)熵(香農(nóng)熵)
for key in labelCounts: #計(jì)算香農(nóng)熵
prob = float(labelCounts[key]) / numEntires #選擇該標(biāo)簽(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計(jì)算
return shannonEnt #返回經(jīng)驗(yàn)熵(香農(nóng)熵)
"""
函數(shù)說明:創(chuàng)建測(cè)試數(shù)據(jù)集
Parameters:
無
Returns:
dataSet - 數(shù)據(jù)集
labels - 特征標(biāo)簽
Author:
Jack Cui
"""
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #數(shù)據(jù)集
[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 = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特征標(biāo)簽
return dataSet, labels #返回?cái)?shù)據(jù)集和分類屬性
"""
函數(shù)說明:按照給定特征劃分?jǐn)?shù)據(jù)集
Parameters:
dataSet - 待劃分的數(shù)據(jù)集
axis - 劃分?jǐn)?shù)據(jù)集的特征
value - 需要返回的特征的值
Returns:
無
Author:
Jack Cui
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #創(chuàng)建返回的數(shù)據(jù)集列表
for featVec in dataSet: #遍歷數(shù)據(jù)集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特征
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的添加到返回的數(shù)據(jù)集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分后的數(shù)據(jù)集
"""
函數(shù)說明:選擇最優(yōu)特征
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
bestFeature - 信息增益最大的(最優(yōu))特征的索引值
Author:
Jack Cui
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征數(shù)量
baseEntropy = calcShannonEnt(dataSet) #計(jì)算數(shù)據(jù)集的香農(nóng)熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最優(yōu)特征的索引值
for i in range(numFeatures): #遍歷所有特征
#獲取dataSet的第i個(gè)所有特征
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #創(chuàng)建set集合{},元素不可重復(fù)
newEntropy = 0.0 #經(jīng)驗(yàn)條件熵
for value in uniqueVals: #計(jì)算信息增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分后的子集
prob = len(subDataSet) / float(len(dataSet)) #計(jì)算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet) #根據(jù)公式計(jì)算經(jīng)驗(yàn)條件熵
infoGain = baseEntropy - newEntropy #信息增益
# print("第%d個(gè)特征的增益為%.3f" % (i, infoGain)) #打印每個(gè)特征的信息增益
if (infoGain > bestInfoGain): #計(jì)算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #記錄信息增益最大的特征的索引值
return bestFeature #返回信息增益最大的特征的索引值
"""
函數(shù)說明:統(tǒng)計(jì)classList中出現(xiàn)此處最多的元素(類標(biāo)簽)
Parameters:
classList - 類標(biāo)簽列表
Returns:
sortedClassCount[0][0] - 出現(xiàn)此處最多的元素(類標(biāo)簽)
Author:
Jack Cui
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統(tǒng)計(jì)classList中每個(gè)元素出現(xiàn)的次數(shù)
if vote not in classCount.keys():classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) #根據(jù)字典的值降序排序
return sortedClassCount[0][0] #返回classList中出現(xiàn)次數(shù)最多的元素
"""
函數(shù)說明:創(chuàng)建決策樹
Parameters:
dataSet - 訓(xùn)練數(shù)據(jù)集
labels - 分類屬性標(biāo)簽
featLabels - 存儲(chǔ)選擇的最優(yōu)特征標(biāo)簽
Returns:
myTree - 決策樹
Author:
Jack Cui
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標(biāo)簽(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續(xù)劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特征時(shí)返回出現(xiàn)次數(shù)最多的類標(biāo)簽
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優(yōu)特征
bestFeatLabel = labels[bestFeat] #最優(yōu)特征的標(biāo)簽
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}} #根據(jù)最優(yōu)特征的標(biāo)簽生成樹
del(labels[bestFeat]) #刪除已經(jīng)使用特征標(biāo)簽
featValues = [example[bestFeat] for example in dataSet] #得到訓(xùn)練集中所有最優(yōu)特征的屬性值
uniqueVals = set(featValues) #去掉重復(fù)的屬性值
for value in uniqueVals: #遍歷特征,創(chuàng)建決策樹。
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, featLabels)
return myTree
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
遞歸創(chuàng)建決策樹時(shí),遞歸有兩個(gè)終止條件:
第一個(gè)停止條件是所有的類標(biāo)簽完全相同,則直接返回該類標(biāo)簽;
第二個(gè)停止條件是使用完了所有特征,仍然不能將數(shù)據(jù)劃分僅包含唯一類別的分組,即決策樹構(gòu)建失敗,特征不夠用。
此時(shí)說明數(shù)據(jù)緯度不夠,由于第二個(gè)停止條件無法簡(jiǎn)單地返回唯一的類標(biāo)簽,這里挑選出現(xiàn)數(shù)量最多的類別作為返回值。
運(yùn)行上述代碼,我們可以看到如下結(jié)果:

可見,我們的決策樹已經(jīng)構(gòu)建完成了。這時(shí)候,有的朋友可能會(huì)說,這個(gè)決策樹看著好別扭,雖然這個(gè)能看懂,但是如果多點(diǎn)的結(jié)點(diǎn),就不好看了。能直觀點(diǎn)嗎?完全沒有問題,我們可以使用強(qiáng)大的Matplotlib繪制決策樹。
決策樹可視化
這里代碼都是關(guān)于Matplotlib的,如果對(duì)于Matplotlib不了解的,可以先學(xué)習(xí)下,Matplotlib的內(nèi)容這里就不再累述??梢暬枰玫降暮瘮?shù):
getNumLeafs:獲取決策樹葉子結(jié)點(diǎn)的數(shù)目 getTreeDepth:獲取決策樹的層數(shù) plotNode:繪制結(jié)點(diǎn) plotMidText:標(biāo)注有向邊屬性值 plotTree:繪制決策樹 createPlot:創(chuàng)建繪制面板
我對(duì)可視化決策樹的程序進(jìn)行了詳細(xì)的注釋,直接看代碼,調(diào)試查看即可。為了顯示中文,需要設(shè)置FontProperties,代碼編寫如下:
# -*- coding: UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator
"""
函數(shù)說明:計(jì)算給定數(shù)據(jù)集的經(jīng)驗(yàn)熵(香農(nóng)熵)
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
shannonEnt - 經(jīng)驗(yàn)熵(香農(nóng)熵)
Author:
Jack Cui
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回?cái)?shù)據(jù)集的行數(shù)
labelCounts = {} #保存每個(gè)標(biāo)簽(Label)出現(xiàn)次數(shù)的字典
for featVec in dataSet: #對(duì)每組特征向量進(jìn)行統(tǒng)計(jì)
currentLabel = featVec[-1] #提取標(biāo)簽(Label)信息
if currentLabel not in labelCounts.keys(): #如果標(biāo)簽(Label)沒有放入統(tǒng)計(jì)次數(shù)的字典,添加進(jìn)去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計(jì)數(shù)
shannonEnt = 0.0 #經(jīng)驗(yàn)熵(香農(nóng)熵)
for key in labelCounts: #計(jì)算香農(nóng)熵
prob = float(labelCounts[key]) / numEntires #選擇該標(biāo)簽(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計(jì)算
return shannonEnt #返回經(jīng)驗(yàn)熵(香農(nóng)熵)
"""
函數(shù)說明:創(chuàng)建測(cè)試數(shù)據(jù)集
Parameters:
無
Returns:
dataSet - 數(shù)據(jù)集
labels - 特征標(biāo)簽
Author:
Jack Cui
"""
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #數(shù)據(jù)集
[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 = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特征標(biāo)簽
return dataSet, labels #返回?cái)?shù)據(jù)集和分類屬性
"""
函數(shù)說明:按照給定特征劃分?jǐn)?shù)據(jù)集
Parameters:
dataSet - 待劃分的數(shù)據(jù)集
axis - 劃分?jǐn)?shù)據(jù)集的特征
value - 需要返回的特征的值
Returns:
無
Author:
Jack Cui
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #創(chuàng)建返回的數(shù)據(jù)集列表
for featVec in dataSet: #遍歷數(shù)據(jù)集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特征
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的添加到返回的數(shù)據(jù)集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分后的數(shù)據(jù)集
"""
函數(shù)說明:選擇最優(yōu)特征
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
bestFeature - 信息增益最大的(最優(yōu))特征的索引值
Author:
Jack Cui
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征數(shù)量
baseEntropy = calcShannonEnt(dataSet) #計(jì)算數(shù)據(jù)集的香農(nóng)熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最優(yōu)特征的索引值
for i in range(numFeatures): #遍歷所有特征
#獲取dataSet的第i個(gè)所有特征
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #創(chuàng)建set集合{},元素不可重復(fù)
newEntropy = 0.0 #經(jīng)驗(yàn)條件熵
for value in uniqueVals: #計(jì)算信息增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分后的子集
prob = len(subDataSet) / float(len(dataSet)) #計(jì)算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet) #根據(jù)公式計(jì)算經(jīng)驗(yàn)條件熵
infoGain = baseEntropy - newEntropy #信息增益
# print("第%d個(gè)特征的增益為%.3f" % (i, infoGain)) #打印每個(gè)特征的信息增益
if (infoGain > bestInfoGain): #計(jì)算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #記錄信息增益最大的特征的索引值
return bestFeature #返回信息增益最大的特征的索引值
"""
函數(shù)說明:統(tǒng)計(jì)classList中出現(xiàn)此處最多的元素(類標(biāo)簽)
Parameters:
classList - 類標(biāo)簽列表
Returns:
sortedClassCount[0][0] - 出現(xiàn)此處最多的元素(類標(biāo)簽)
Author:
Jack Cui
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統(tǒng)計(jì)classList中每個(gè)元素出現(xiàn)的次數(shù)
if vote not in classCount.keys():classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) #根據(jù)字典的值降序排序
return sortedClassCount[0][0] #返回classList中出現(xiàn)次數(shù)最多的元素
"""
函數(shù)說明:創(chuàng)建決策樹
Parameters:
dataSet - 訓(xùn)練數(shù)據(jù)集
labels - 分類屬性標(biāo)簽
featLabels - 存儲(chǔ)選擇的最優(yōu)特征標(biāo)簽
Returns:
myTree - 決策樹
Author:
Jack Cui
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標(biāo)簽(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續(xù)劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特征時(shí)返回出現(xiàn)次數(shù)最多的類標(biāo)簽
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優(yōu)特征
bestFeatLabel = labels[bestFeat] #最優(yōu)特征的標(biāo)簽
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}} #根據(jù)最優(yōu)特征的標(biāo)簽生成樹
del(labels[bestFeat]) #刪除已經(jīng)使用特征標(biāo)簽
featValues = [example[bestFeat] for example in dataSet] #得到訓(xùn)練集中所有最優(yōu)特征的屬性值
uniqueVals = set(featValues) #去掉重復(fù)的屬性值
for value in uniqueVals: #遍歷特征,創(chuàng)建決策樹。
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, featLabels)
return myTree
"""
函數(shù)說明:獲取決策樹葉子結(jié)點(diǎn)的數(shù)目
Parameters:
myTree - 決策樹
Returns:
numLeafs - 決策樹的葉子結(jié)點(diǎn)的數(shù)目
Author:
Jack Cui
"""
def getNumLeafs(myTree):
numLeafs = 0 #初始化葉子
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結(jié)點(diǎn)屬性,可以使用list(myTree.keys())[0]
secondDict = myTree[firstStr] #獲取下一組字典
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict': #測(cè)試該結(jié)點(diǎn)是否為字典,如果不是字典,代表此結(jié)點(diǎn)為葉子結(jié)點(diǎn)
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
"""
函數(shù)說明:獲取決策樹的層數(shù)
Parameters:
myTree - 決策樹
Returns:
maxDepth - 決策樹的層數(shù)
Author:
Jack Cui
"""
def getTreeDepth(myTree):
maxDepth = 0 #初始化決策樹深度
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結(jié)點(diǎn)屬性,可以使用list(myTree.keys())[0]
secondDict = myTree[firstStr] #獲取下一個(gè)字典
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict': #測(cè)試該結(jié)點(diǎn)是否為字典,如果不是字典,代表此結(jié)點(diǎn)為葉子結(jié)點(diǎn)
thisDepth = 1 + getTreeDepth(secondDict[key])
else: thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth #更新層數(shù)
return maxDepth
"""
函數(shù)說明:繪制結(jié)點(diǎn)
Parameters:
nodeTxt - 結(jié)點(diǎn)名
centerPt - 文本位置
parentPt - 標(biāo)注的箭頭位置
nodeType - 結(jié)點(diǎn)格式
Returns:
無
Author:
Jack Cui
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
arrow_args = dict(arrowstyle="<-") #定義箭頭格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14) #設(shè)置中文字體
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', #繪制結(jié)點(diǎn)
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)
"""
函數(shù)說明:標(biāo)注有向邊屬性值
Parameters:
cntrPt、parentPt - 用于計(jì)算標(biāo)注位置
txtString - 標(biāo)注的內(nèi)容
Returns:
無
Author:
Jack Cui
"""
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] #計(jì)算標(biāo)注位置
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)
"""
函數(shù)說明:繪制決策樹
Parameters:
myTree - 決策樹(字典)
parentPt - 標(biāo)注的內(nèi)容
nodeTxt - 結(jié)點(diǎn)名
Returns:
無
Author:
Jack Cui
"""
def plotTree(myTree, parentPt, nodeTxt):
decisionNode = dict(boxstyle="sawtooth", fc="0.8") #設(shè)置結(jié)點(diǎn)格式
leafNode = dict(boxstyle="round4", fc="0.8") #設(shè)置葉結(jié)點(diǎn)格式
numLeafs = getNumLeafs(myTree) #獲取決策樹葉結(jié)點(diǎn)數(shù)目,決定了樹的寬度
depth = getTreeDepth(myTree) #獲取決策樹層數(shù)
firstStr = next(iter(myTree)) #下個(gè)字典
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) #中心位置
plotMidText(cntrPt, parentPt, nodeTxt) #標(biāo)注有向邊屬性值
plotNode(firstStr, cntrPt, parentPt, decisionNode) #繪制結(jié)點(diǎn)
secondDict = myTree[firstStr] #下一個(gè)字典,也就是繼續(xù)繪制子結(jié)點(diǎn)
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD #y偏移
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict': #測(cè)試該結(jié)點(diǎn)是否為字典,如果不是字典,代表此結(jié)點(diǎn)為葉子結(jié)點(diǎn)
plotTree(secondDict[key],cntrPt,str(key)) #不是葉結(jié)點(diǎn),遞歸調(diào)用繼續(xù)繪制
else: #如果是葉結(jié)點(diǎn),繪制葉結(jié)點(diǎn),并標(biāo)注有向邊屬性值
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
"""
函數(shù)說明:創(chuàng)建繪制面板
Parameters:
inTree - 決策樹(字典)
Returns:
無
Author:
Jack Cui
"""
def createPlot(inTree):
fig = plt.figure(1, facecolor='white') #創(chuàng)建fig
fig.clf() #清空fig
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #去掉x、y軸
plotTree.totalW = float(getNumLeafs(inTree)) #獲取決策樹葉結(jié)點(diǎn)數(shù)目
plotTree.totalD = float(getTreeDepth(inTree)) #獲取決策樹層數(shù)
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; #x偏移
plotTree(inTree, (0.5,1.0), '') #繪制決策樹
plt.show() #顯示繪制結(jié)果
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
createPlot(myTree)
不出意外的話,我們就可以得到如下結(jié)果,可以看到?jīng)Q策樹繪制完成。plotNode函數(shù)的工作就是繪制各個(gè)結(jié)點(diǎn),比如有自己的房子、有工作、yes、no,包括內(nèi)結(jié)點(diǎn)和葉子結(jié)點(diǎn)。
plotMidText函數(shù)的工作就是繪制各個(gè)有向邊的屬性,例如各個(gè)有向邊的0和1。這部分內(nèi)容呢,個(gè)人感覺可以選擇性掌握,能掌握最好,不能掌握可以放一放,因?yàn)楹竺鏁?huì)介紹一個(gè)更簡(jiǎn)單的決策樹可視化方法。

使用決策樹執(zhí)行分類
依靠訓(xùn)練數(shù)據(jù)構(gòu)造了決策樹之后,我們可以將它用于實(shí)際數(shù)據(jù)的分類。在執(zhí)行數(shù)據(jù)分類時(shí),需要決策樹以及用于構(gòu)造樹的標(biāo)簽向量。
然后,程序比較測(cè)試數(shù)據(jù)與決策樹上的數(shù)值,遞歸執(zhí)行該過程直到進(jìn)入葉子結(jié)點(diǎn);最后將測(cè)試數(shù)據(jù)定義為葉子結(jié)點(diǎn)所屬的類型。
在構(gòu)建決策樹的代碼,可以看到,有個(gè)featLabels參數(shù)。它是用來干什么的?它就是用來記錄各個(gè)分類結(jié)點(diǎn)的,在用決策樹做預(yù)測(cè)的時(shí)候,我們按順序輸入需要的分類結(jié)點(diǎn)的屬性值即可。
舉個(gè)例子,比如我用上述已經(jīng)訓(xùn)練好的決策樹做分類,那么我只需要提供這個(gè)人是否有房子,是否有工作這兩個(gè)信息即可,無需提供冗余的信息。
用決策樹做分類的代碼很簡(jiǎn)單,編寫代碼如下:
# -*- coding: UTF-8 -*-
from math import log
import operator
"""
函數(shù)說明:計(jì)算給定數(shù)據(jù)集的經(jīng)驗(yàn)熵(香農(nóng)熵)
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
shannonEnt - 經(jīng)驗(yàn)熵(香農(nóng)熵)
Author:
Jack Cui
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回?cái)?shù)據(jù)集的行數(shù)
labelCounts = {} #保存每個(gè)標(biāo)簽(Label)出現(xiàn)次數(shù)的字典
for featVec in dataSet: #對(duì)每組特征向量進(jìn)行統(tǒng)計(jì)
currentLabel = featVec[-1] #提取標(biāo)簽(Label)信息
if currentLabel not in labelCounts.keys(): #如果標(biāo)簽(Label)沒有放入統(tǒng)計(jì)次數(shù)的字典,添加進(jìn)去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計(jì)數(shù)
shannonEnt = 0.0 #經(jīng)驗(yàn)熵(香農(nóng)熵)
for key in labelCounts: #計(jì)算香農(nóng)熵
prob = float(labelCounts[key]) / numEntires #選擇該標(biāo)簽(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計(jì)算
return shannonEnt #返回經(jīng)驗(yàn)熵(香農(nóng)熵)
"""
函數(shù)說明:創(chuàng)建測(cè)試數(shù)據(jù)集
Parameters:
無
Returns:
dataSet - 數(shù)據(jù)集
labels - 特征標(biāo)簽
Author:
Jack Cui
"""
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #數(shù)據(jù)集
[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 = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特征標(biāo)簽
return dataSet, labels #返回?cái)?shù)據(jù)集和分類屬性
"""
函數(shù)說明:按照給定特征劃分?jǐn)?shù)據(jù)集
Parameters:
dataSet - 待劃分的數(shù)據(jù)集
axis - 劃分?jǐn)?shù)據(jù)集的特征
value - 需要返回的特征的值
Returns:
無
Author:
Jack Cui
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #創(chuàng)建返回的數(shù)據(jù)集列表
for featVec in dataSet: #遍歷數(shù)據(jù)集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特征
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的添加到返回的數(shù)據(jù)集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分后的數(shù)據(jù)集
"""
函數(shù)說明:選擇最優(yōu)特征
Parameters:
dataSet - 數(shù)據(jù)集
Returns:
bestFeature - 信息增益最大的(最優(yōu))特征的索引值
Author:
Jack Cui
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征數(shù)量
baseEntropy = calcShannonEnt(dataSet) #計(jì)算數(shù)據(jù)集的香農(nóng)熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最優(yōu)特征的索引值
for i in range(numFeatures): #遍歷所有特征
#獲取dataSet的第i個(gè)所有特征
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #創(chuàng)建set集合{},元素不可重復(fù)
newEntropy = 0.0 #經(jīng)驗(yàn)條件熵
for value in uniqueVals: #計(jì)算信息增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分后的子集
prob = len(subDataSet) / float(len(dataSet)) #計(jì)算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet) #根據(jù)公式計(jì)算經(jīng)驗(yàn)條件熵
infoGain = baseEntropy - newEntropy #信息增益
# print("第%d個(gè)特征的增益為%.3f" % (i, infoGain)) #打印每個(gè)特征的信息增益
if (infoGain > bestInfoGain): #計(jì)算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #記錄信息增益最大的特征的索引值
return bestFeature #返回信息增益最大的特征的索引值
"""
函數(shù)說明:統(tǒng)計(jì)classList中出現(xiàn)此處最多的元素(類標(biāo)簽)
Parameters:
classList - 類標(biāo)簽列表
Returns:
sortedClassCount[0][0] - 出現(xiàn)此處最多的元素(類標(biāo)簽)
Author:
Jack Cui
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統(tǒng)計(jì)classList中每個(gè)元素出現(xiàn)的次數(shù)
if vote not in classCount.keys():classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) #根據(jù)字典的值降序排序
return sortedClassCount[0][0] #返回classList中出現(xiàn)次數(shù)最多的元素
"""
函數(shù)說明:創(chuàng)建決策樹
Parameters:
dataSet - 訓(xùn)練數(shù)據(jù)集
labels - 分類屬性標(biāo)簽
featLabels - 存儲(chǔ)選擇的最優(yōu)特征標(biāo)簽
Returns:
myTree - 決策樹
Author:
Jack Cui
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標(biāo)簽(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續(xù)劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特征時(shí)返回出現(xiàn)次數(shù)最多的類標(biāo)簽
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優(yōu)特征
bestFeatLabel = labels[bestFeat] #最優(yōu)特征的標(biāo)簽
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}} #根據(jù)最優(yōu)特征的標(biāo)簽生成樹
del(labels[bestFeat]) #刪除已經(jīng)使用特征標(biāo)簽
featValues = [example[bestFeat] for example in dataSet] #得到訓(xùn)練集中所有最優(yōu)特征的屬性值
uniqueVals = set(featValues) #去掉重復(fù)的屬性值
for value in uniqueVals: #遍歷特征,創(chuàng)建決策樹。
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
"""
函數(shù)說明:使用決策樹分類
Parameters:
inputTree - 已經(jīng)生成的決策樹
featLabels - 存儲(chǔ)選擇的最優(yōu)特征標(biāo)簽
testVec - 測(cè)試數(shù)據(jù)列表,順序?qū)?yīng)最優(yōu)特征標(biāo)簽
Returns:
classLabel - 分類結(jié)果
Author:
Jack Cui
"""
def classify(inputTree, featLabels, testVec):
firstStr = next(iter(inputTree)) #獲取決策樹結(jié)點(diǎn)
secondDict = inputTree[firstStr] #下一個(gè)字典
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else: classLabel = secondDict[key]
return classLabel
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
testVec = [0,1] #測(cè)試數(shù)據(jù)
result = classify(myTree, featLabels, testVec)
if result == 'yes':
print('放貸')
if result == 'no':
print('不放貸')
這里只增加了classify函數(shù),用于決策樹分類。輸入測(cè)試數(shù)據(jù)[0,1],它代表沒有房子,但是有工作,分類結(jié)果如下所示:

看到這里,細(xì)心的朋友可能就會(huì)問了,每次做預(yù)測(cè)都要訓(xùn)練一次決策樹?這也太麻煩了吧?有什么好的解決嗎?
決策樹的存儲(chǔ)
構(gòu)造決策樹是很耗時(shí)的任務(wù),即使處理很小的數(shù)據(jù)集,如前面的樣本數(shù)據(jù),也要花費(fèi)幾秒的時(shí)間,如果數(shù)據(jù)集很大,將會(huì)耗費(fèi)很多計(jì)算時(shí)間。然而用創(chuàng)建好的決策樹解決分類問題,則可以很快完成。
因此,為了節(jié)省計(jì)算時(shí)間,最好能夠在每次執(zhí)行分類時(shí)調(diào)用已經(jīng)構(gòu)造好的決策樹。為了解決這個(gè)問題,需要使用Python模塊pickle序列化對(duì)象。序列化對(duì)象可以在磁盤上保存對(duì)象,并在需要的時(shí)候讀取出來。
假設(shè)我們已經(jīng)得到?jīng)Q策樹{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}},使用pickle.dump存儲(chǔ)決策樹。
# -*- coding: UTF-8 -*-
import pickle
"""
函數(shù)說明:存儲(chǔ)決策樹
Parameters:
inputTree - 已經(jīng)生成的決策樹
filename - 決策樹的存儲(chǔ)文件名
Returns:
無
Author:
Jack Cui
"""
def storeTree(inputTree, filename):
with open(filename, 'wb') as fw:
pickle.dump(inputTree, fw)
if __name__ == '__main__':
myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
storeTree(myTree, 'classifierStorage.txt')
運(yùn)行代碼,在該P(yáng)ython文件的相同目錄下,會(huì)生成一個(gè)名為classifierStorage.txt的txt文件,這個(gè)文件二進(jìn)制存儲(chǔ)著我們的決策樹。我們可以使用sublime txt打開看下存儲(chǔ)結(jié)果。

看不懂?沒錯(cuò),因?yàn)檫@個(gè)是個(gè)二進(jìn)制存儲(chǔ)的文件,我們也無需看懂里面的內(nèi)容,會(huì)存儲(chǔ),會(huì)用即可。那么問題來了。將決策樹存儲(chǔ)完這個(gè)二進(jìn)制文件,然后下次使用的話,怎么用呢?
很簡(jiǎn)單使用pickle.load進(jìn)行載入即可,編寫代碼如下:
# -*- coding: UTF-8 -*-
import pickle
"""
函數(shù)說明:讀取決策樹
Parameters:
filename - 決策樹的存儲(chǔ)文件名
Returns:
pickle.load(fr) - 決策樹字典
Author:
Jack Cui
"""
def grabTree(filename):
fr = open(filename, 'rb')
return pickle.load(fr)
if __name__ == '__main__':
myTree = grabTree('classifierStorage.txt')
print(myTree)
如果在該P(yáng)ython文件的相同目錄下,有一個(gè)名為classifierStorage.txt的文件,那么我們就可以運(yùn)行上述代碼,運(yùn)行結(jié)果如下圖所示:

從上述結(jié)果中,我們可以看到,我們順利加載了存儲(chǔ)決策樹的二進(jìn)制文件。
Sklearn之使用決策樹預(yù)測(cè)隱形眼睛類型
1、實(shí)戰(zhàn)背景
進(jìn)入本文的正題:眼科醫(yī)生是如何判斷患者需要佩戴隱形眼鏡的類型的?一旦理解了決策樹的工作原理,我們甚至也可以幫助人們判斷需要佩戴的鏡片類型。
隱形眼鏡數(shù)據(jù)集是非常著名的數(shù)據(jù)集,它包含很多換著眼部狀態(tài)的觀察條件以及醫(yī)生推薦的隱形眼鏡類型。
隱形眼鏡類型包括硬材質(zhì)(hard)、軟材質(zhì)(soft)以及不適合佩戴隱形眼鏡(no lenses)。數(shù)據(jù)來源與UCI數(shù)據(jù)庫,數(shù)據(jù)集下載地址:
https://github.com/Jack-Cherish/Machine-Learning/blob/master/Decision%20Tree/classifierStorage.txt
一共有24組數(shù)據(jù),數(shù)據(jù)的Labels依次是age、prescript、astigmatic、tearRate、class,也就是第一列是年齡,第二列是癥狀,第三列是是否散光,第四列是眼淚數(shù)量,第五列是最終的分類標(biāo)簽。數(shù)據(jù)如下圖所示:

可以使用已經(jīng)寫好的Python程序構(gòu)建決策樹,不過出于繼續(xù)學(xué)習(xí)的目的,本文使用Sklearn實(shí)現(xiàn)。
2、使用Sklearn構(gòu)建決策樹
官方英文文檔地址:
http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
sklearn.tree模塊提供了決策樹模型,用于解決分類問題和回歸問題。方法如下圖所示:

本次實(shí)戰(zhàn)內(nèi)容使用的是DecisionTreeClassifier和export_graphviz,前者用于決策樹構(gòu)建,后者用于決策樹可視化。
DecisionTreeClassifier構(gòu)建決策樹:
讓我們先看下DecisionTreeClassifier這個(gè)函數(shù),一共有12個(gè)參數(shù):

參數(shù)說明如下:
criterion:特征選擇標(biāo)準(zhǔn),可選參數(shù),默認(rèn)是gini,可以設(shè)置為entropy。gini是基尼不純度,是將來自集合的某種結(jié)果隨機(jī)應(yīng)用于某一數(shù)據(jù)項(xiàng)的預(yù)期誤差率,是一種基于統(tǒng)計(jì)的思想。entropy是香農(nóng)熵,也就是上篇文章講過的內(nèi)容,是一種基于信息論的思想。Sklearn把gini設(shè)為默認(rèn)參數(shù),應(yīng)該也是做了相應(yīng)的斟酌的,精度也許更高些?ID3算法使用的是entropy,CART算法使用的則是gini。
splitter:特征劃分點(diǎn)選擇標(biāo)準(zhǔn),可選參數(shù),默認(rèn)是best,可以設(shè)置為random。每個(gè)結(jié)點(diǎn)的選擇策略。best參數(shù)是根據(jù)算法選擇最佳的切分特征,例如gini、entropy。random隨機(jī)的在部分劃分點(diǎn)中找局部最優(yōu)的劃分點(diǎn)。默認(rèn)的"best"適合樣本量不大的時(shí)候,而如果樣本數(shù)據(jù)量非常大,此時(shí)決策樹構(gòu)建推薦"random"。
max_features:劃分時(shí)考慮的最大特征數(shù),可選參數(shù),默認(rèn)是None。尋找最佳切分時(shí)考慮的最大特征數(shù)(n_features為總共的特征數(shù)),有如下6種情況:
如果max_features是整型的數(shù),則考慮max_features個(gè)特征; 如果max_features是浮點(diǎn)型的數(shù),則考慮int(max_features * n_features)個(gè)特征; 如果max_features設(shè)為auto,那么max_features = sqrt(n_features); 如果max_features設(shè)為sqrt,那么max_featrues = sqrt(n_features),跟auto一樣; 如果max_features設(shè)為log2,那么max_features = log2(n_features); 如果max_features設(shè)為None,那么max_features = n_features,也就是所有特征都用。 一般來說,如果樣本特征數(shù)不多,比如小于50,我們用默認(rèn)的"None"就可以了,如果特征數(shù)非常多,我們可以靈活使用剛才描述的其他取值來控制劃分時(shí)考慮的最大特征數(shù),以控制決策樹的生成時(shí)間。 max_depth:決策樹最大深,可選參數(shù),默認(rèn)是None。這個(gè)參數(shù)是這是樹的層數(shù)的。層數(shù)的概念就是,比如在貸款的例子中,決策樹的層數(shù)是2層。如果這個(gè)參數(shù)設(shè)置為None,那么決策樹在建立子樹的時(shí)候不會(huì)限制子樹的深度。一般來說,數(shù)據(jù)少或者特征少的時(shí)候可以不管這個(gè)值?;蛘呷绻O(shè)置了min_samples_slipt參數(shù),那么直到少于min_smaples_split個(gè)樣本為止。如果模型樣本量多,特征也多的情況下,推薦限制這個(gè)最大深度,具體的取值取決于數(shù)據(jù)的分布。常用的可以取值10-100之間。
min_samples_split:內(nèi)部節(jié)點(diǎn)再劃分所需最小樣本數(shù),可選參數(shù),默認(rèn)是2。這個(gè)值限制了子樹繼續(xù)劃分的條件。如果min_samples_split為整數(shù),那么在切分內(nèi)部結(jié)點(diǎn)的時(shí)候,min_samples_split作為最小的樣本數(shù),也就是說,如果樣本已經(jīng)少于min_samples_split個(gè)樣本,則停止繼續(xù)切分。如果min_samples_split為浮點(diǎn)數(shù),那么min_samples_split就是一個(gè)百分比,ceil(min_samples_split * n_samples),數(shù)是向上取整的。如果樣本量不大,不需要管這個(gè)值。如果樣本量數(shù)量級(jí)非常大,則推薦增大這個(gè)值。
min_samples_leaf:葉子節(jié)點(diǎn)最少樣本數(shù),可選參數(shù),默認(rèn)是1。這個(gè)值限制了葉子節(jié)點(diǎn)最少的樣本數(shù),如果某葉子節(jié)點(diǎn)數(shù)目小于樣本數(shù),則會(huì)和兄弟節(jié)點(diǎn)一起被剪枝。葉結(jié)點(diǎn)需要最少的樣本數(shù),也就是最后到葉結(jié)點(diǎn),需要多少個(gè)樣本才能算一個(gè)葉結(jié)點(diǎn)。如果設(shè)置為1,哪怕這個(gè)類別只有1個(gè)樣本,決策樹也會(huì)構(gòu)建出來。如果min_samples_leaf是整數(shù),那么min_samples_leaf作為最小的樣本數(shù)。如果是浮點(diǎn)數(shù),那么min_samples_leaf就是一個(gè)百分比,同上,celi(min_samples_leaf * n_samples),數(shù)是向上取整的。如果樣本量不大,不需要管這個(gè)值。如果樣本量數(shù)量級(jí)非常大,則推薦增大這個(gè)值。
min_weight_fraction_leaf:葉子節(jié)點(diǎn)最小的樣本權(quán)重和,可選參數(shù),默認(rèn)是0。這個(gè)值限制了葉子節(jié)點(diǎn)所有樣本權(quán)重和的最小值,如果小于這個(gè)值,則會(huì)和兄弟節(jié)點(diǎn)一起被剪枝。一般來說,如果我們有較多樣本有缺失值,或者分類樹樣本的分布類別偏差很大,就會(huì)引入樣本權(quán)重,這時(shí)我們就要注意這個(gè)值了。
max_leaf_nodes:最大葉子節(jié)點(diǎn)數(shù),可選參數(shù),默認(rèn)是None。通過限制最大葉子節(jié)點(diǎn)數(shù),可以防止過擬合。如果加了限制,算法會(huì)建立在最大葉子節(jié)點(diǎn)數(shù)內(nèi)最優(yōu)的決策樹。如果特征不多,可以不考慮這個(gè)值,但是如果特征分成多的話,可以加以限制,具體的值可以通過交叉驗(yàn)證得到。
class_weight:類別權(quán)重,可選參數(shù),默認(rèn)是None,也可以字典、字典列表、balanced。指定樣本各類別的的權(quán)重,主要是為了防止訓(xùn)練集某些類別的樣本過多,導(dǎo)致訓(xùn)練的決策樹過于偏向這些類別。類別的權(quán)重可以通過{class_label:weight}這樣的格式給出,這里可以自己指定各個(gè)樣本的權(quán)重,或者用balanced,如果使用balanced,則算法會(huì)自己計(jì)算權(quán)重,樣本量少的類別所對(duì)應(yīng)的樣本權(quán)重會(huì)高。當(dāng)然,如果你的樣本類別分布沒有明顯的偏倚,則可以不管這個(gè)參數(shù),選擇默認(rèn)的None。
random_state:可選參數(shù),默認(rèn)是None。隨機(jī)數(shù)種子。如果是證書,那么random_state會(huì)作為隨機(jī)數(shù)生成器的隨機(jī)數(shù)種子。隨機(jī)數(shù)種子,如果沒有設(shè)置隨機(jī)數(shù),隨機(jī)出來的數(shù)與當(dāng)前系統(tǒng)時(shí)間有關(guān),每個(gè)時(shí)刻都是不同的。如果設(shè)置了隨機(jī)數(shù)種子,那么相同隨機(jī)數(shù)種子,不同時(shí)刻產(chǎn)生的隨機(jī)數(shù)也是相同的。如果是RandomState instance,那么random_state是隨機(jī)數(shù)生成器。如果為None,則隨機(jī)數(shù)生成器使用np.random。
min_impurity_split:節(jié)點(diǎn)劃分最小不純度,可選參數(shù),默認(rèn)是1e-7。這是個(gè)閾值,這個(gè)值限制了決策樹的增長(zhǎng),如果某節(jié)點(diǎn)的不純度(基尼系數(shù),信息增益,均方差,絕對(duì)差)小于這個(gè)閾值,則該節(jié)點(diǎn)不再生成子節(jié)點(diǎn)。即為葉子節(jié)點(diǎn) 。
presort:數(shù)據(jù)是否預(yù)排序,可選參數(shù),默認(rèn)為False,這個(gè)值是布爾值,默認(rèn)是False不排序。一般來說,如果樣本量少或者限制了一個(gè)深度很小的決策樹,設(shè)置為true可以讓劃分點(diǎn)選擇更加快,決策樹建立的更加快。如果樣本量太大的話,反而沒有什么好處。問題是樣本量少的時(shí)候,我速度本來就不慢。所以這個(gè)值一般懶得理它就可以了。
除了這些參數(shù)要注意以外,其他在調(diào)參時(shí)的注意點(diǎn)有:
當(dāng)樣本數(shù)量少但是樣本特征非常多的時(shí)候,決策樹很容易過擬合,一般來說,樣本數(shù)比特征數(shù)多一些會(huì)比較容易建立健壯的模型
如果樣本數(shù)量少但是樣本特征非常多,在擬合決策樹模型前,推薦先做維度規(guī)約,比如主成分分析(PCA),特征選擇(Losso)或者獨(dú)立成分分析(ICA)。這樣特征的維度會(huì)大大減小。再來擬合決策樹模型效果會(huì)好。
推薦多用決策樹的可視化,同時(shí)先限制決策樹的深度,這樣可以先觀察下生成的決策樹里數(shù)據(jù)的初步擬合情況,然后再?zèng)Q定是否要增加深度。
在訓(xùn)練模型時(shí),注意觀察樣本的類別情況(主要指分類樹),如果類別分布非常不均勻,就要考慮用class_weight來限制模型過于偏向樣本多的類別。
決策樹的數(shù)組使用的是numpy的float32類型,如果訓(xùn)練數(shù)據(jù)不是這樣的格式,算法會(huì)先做copy再運(yùn)行。
如果輸入的樣本矩陣是稀疏的,推薦在擬合前調(diào)用csc_matrix稀疏化,在預(yù)測(cè)前調(diào)用csr_matrix稀疏化。
sklearn.tree.DecisionTreeClassifier()提供了一些方法供我們使用,如下圖所示:

了解到這些,我們就可以編寫代碼了。
# -*- coding: UTF-8 -*-
from sklearn import tree
if __name__ == '__main__':
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
print(lenses)
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
clf = tree.DecisionTreeClassifier()
lenses = clf.fit(lenses, lensesLabels)
運(yùn)行代碼,會(huì)得到如下結(jié)果:

我們可以看到程序報(bào)錯(cuò)了,這是為什么?因?yàn)樵趂it()函數(shù)不能接收string類型的數(shù)據(jù),通過打印的信息可以看到,數(shù)據(jù)都是string類型的。在使用fit()函數(shù)之前,我們需要對(duì)數(shù)據(jù)集進(jìn)行編碼,這里可以使用兩種方法:
LabelEncoder :將字符串轉(zhuǎn)換為增量值
OneHotEncoder:使用One-of-K算法將字符串轉(zhuǎn)換為整數(shù)
為了對(duì)string類型的數(shù)據(jù)序列化,需要先生成pandas數(shù)據(jù),這樣方便我們的序列化工作。這里我使用的方法是,原始數(shù)據(jù)->字典->pandas數(shù)據(jù),編寫代碼如下:
# -*- coding: UTF-8 -*-
import pandas as pd
if __name__ == '__main__':
with open('lenses.txt', 'r') as fr: #加載文件
lenses = [inst.strip().split('\t') for inst in fr.readlines()] #處理文件
lenses_target = [] #提取每組數(shù)據(jù)的類別,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] #特征標(biāo)簽
lenses_list = [] #保存lenses數(shù)據(jù)的臨時(shí)列表
lenses_dict = {} #保存lenses數(shù)據(jù)的字典,用于生成pandas
for each_label in lensesLabels: #提取信息,生成字典
for each in lenses:
lenses_list.append(each[lensesLabels.index(each_label)])
lenses_dict[each_label] = lenses_list
lenses_list = []
print(lenses_dict) #打印字典信息
lenses_pd = pd.DataFrame(lenses_dict) #生成pandas.DataFrame
print(lenses_pd)
從運(yùn)行結(jié)果可以看出,順利生成pandas數(shù)據(jù)。

接下來,將數(shù)據(jù)序列化,編寫代碼如下:
# -*- coding: UTF-8 -*-
import pandas as pd
from sklearn.preprocessing import LabelEncoder
import pydotplus
from sklearn.externals.six import StringIO
if __name__ == '__main__':
with open('lenses.txt', 'r') as fr: #加載文件
lenses = [inst.strip().split('\t') for inst in fr.readlines()] #處理文件
lenses_target = [] #提取每組數(shù)據(jù)的類別,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] #特征標(biāo)簽
lenses_list = [] #保存lenses數(shù)據(jù)的臨時(shí)列表
lenses_dict = {} #保存lenses數(shù)據(jù)的字典,用于生成pandas
for each_label in lensesLabels: #提取信息,生成字典
for each in lenses:
lenses_list.append(each[lensesLabels.index(each_label)])
lenses_dict[each_label] = lenses_list
lenses_list = []
# print(lenses_dict) #打印字典信息
lenses_pd = pd.DataFrame(lenses_dict) #生成pandas.DataFrame
print(lenses_pd) #打印pandas.DataFrame
le = LabelEncoder() #創(chuàng)建LabelEncoder()對(duì)象,用于序列化
for col in lenses_pd.columns: #為每一列序列化
lenses_pd[col] = le.fit_transform(lenses_pd[col])
print(lenses_pd)
從打印結(jié)果可以看到,我們已經(jīng)將數(shù)據(jù)順利序列化,接下來。我們就可以fit()數(shù)據(jù),構(gòu)建決策樹了。

3、使用Graphviz可視化決策樹
Graphviz的是AT&T Labs Research開發(fā)的圖形繪制工具,他可以很方便的用來繪制結(jié)構(gòu)化的圖形網(wǎng)絡(luò),支持多種格式輸出,生成圖片的質(zhì)量和速度都不錯(cuò)。它的輸入是一個(gè)用dot語言編寫的繪圖腳本,通過對(duì)輸入腳本的解析,分析出其中的點(diǎn),邊以及子圖,然后根據(jù)屬性進(jìn)行繪制。是使用Sklearn生成的決策樹就是dot格式的,因此我們可以直接利用Graphviz將決策樹可視化。
在講解編寫代碼之前,我們需要安裝兩樣?xùn)|西,即pydotplus和Grphviz。
(1)安裝Pydotplus
pydotplus可以在CMD窗口中,直接使用指令安裝:
pip3 install pydotplus
(2)安裝Graphviz
Graphviz不能使用pip進(jìn)行安裝,我們需要手動(dòng)安裝,下載地址:
https://www.graphviz.org/
找到相應(yīng)的版本進(jìn)行安裝即可,不過這個(gè)網(wǎng)站的下載速度感人,每秒10k的速度也是沒誰了。因此我已經(jīng)將Graphviz for Windows的版本下載好了,供各位直接下載,這樣速度很快,節(jié)省各位的時(shí)間(密碼:b5xz):
https://pan.baidu.com/s/1LiWGrozTZS-QM62FPyoyRQ
下載好安裝包,進(jìn)行安裝,安裝完畢之后,需要設(shè)置Graphviz的環(huán)境變量。
首先,按快捷鍵win+r,在出現(xiàn)的運(yùn)行對(duì)話框中輸入sysdm.cpl,點(diǎn)擊確定,出現(xiàn)如下對(duì)話框:

選擇高級(jí)->環(huán)境變量。在系統(tǒng)變量的Path變量中,添加Graphviz的環(huán)境變量,比如Graphviz安裝在了D盤的根目錄,則添加:D:\Graphviz\bin;

添加好環(huán)境變量之后,我們就可以正常使用Graphviz了。
(3)編寫代碼
Talk is Cheap, show me the code.(廢話少說,放碼過來)??梢暬糠值拇a不難,都是有套路的,直接填參數(shù)就好,詳細(xì)內(nèi)容可以查看官方教程:
https://scikit-learn.org/stable/modules/tree.html#tree
# -*- coding: UTF-8 -*-
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.externals.six import StringIO
from sklearn import tree
import pandas as pd
import numpy as np
import pydotplus
if __name__ == '__main__':
with open('lenses.txt', 'r') as fr: #加載文件
lenses = [inst.strip().split('\t') for inst in fr.readlines()] #處理文件
lenses_target = [] #提取每組數(shù)據(jù)的類別,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
print(lenses_target)
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] #特征標(biāo)簽
lenses_list = [] #保存lenses數(shù)據(jù)的臨時(shí)列表
lenses_dict = {} #保存lenses數(shù)據(jù)的字典,用于生成pandas
for each_label in lensesLabels: #提取信息,生成字典
for each in lenses:
lenses_list.append(each[lensesLabels.index(each_label)])
lenses_dict[each_label] = lenses_list
lenses_list = []
# print(lenses_dict) #打印字典信息
lenses_pd = pd.DataFrame(lenses_dict) #生成pandas.DataFrame
# print(lenses_pd) #打印pandas.DataFrame
le = LabelEncoder() #創(chuàng)建LabelEncoder()對(duì)象,用于序列化
for col in lenses_pd.columns: #序列化
lenses_pd[col] = le.fit_transform(lenses_pd[col])
# print(lenses_pd) #打印編碼信息
clf = tree.DecisionTreeClassifier(max_depth = 4) #創(chuàng)建DecisionTreeClassifier()類
clf = clf.fit(lenses_pd.values.tolist(), lenses_target) #使用數(shù)據(jù),構(gòu)建決策樹
dot_data = StringIO()
tree.export_graphviz(clf, out_file = dot_data, #繪制決策樹
feature_names = lenses_pd.keys(),
class_names = clf.classes_,
filled=True, rounded=True,
special_characters=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
graph.write_pdf("tree.pdf") #保存繪制好的決策樹,以PDF的形式存儲(chǔ)。
運(yùn)行代碼,在該python文件保存的相同目錄下,會(huì)生成一個(gè)名為tree的PDF文件,打開文件,我們就可以看到?jīng)Q策樹的可視化效果圖了。

確定好決策樹之后,我們就可以做預(yù)測(cè)了??梢愿鶕?jù)自己的眼睛情況和年齡等特征,看一看自己適合何種材質(zhì)的隱形眼鏡。使用如下代碼就可以看到預(yù)測(cè)結(jié)果:
print(clf.predict([[1,1,1,0]])) #預(yù)測(cè)
代碼簡(jiǎn)單,官方手冊(cè)都有,就不全貼出來了。
本來是想繼續(xù)討論決策樹的過擬合問題,但是看到《機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》將此部分內(nèi)容放到了第九章,那我也放在后面好了。
總結(jié)
決策樹的一些優(yōu)點(diǎn):
易于理解和解釋。決策樹可以可視化。 幾乎不需要數(shù)據(jù)預(yù)處理。其他方法經(jīng)常需要數(shù)據(jù)標(biāo)準(zhǔn)化,創(chuàng)建虛擬變量和刪除缺失值。決策樹還不支持缺失值。 使用樹的花費(fèi)(例如預(yù)測(cè)數(shù)據(jù))是訓(xùn)練數(shù)據(jù)點(diǎn)(data points)數(shù)量的對(duì)數(shù)。 可以同時(shí)處理數(shù)值變量和分類變量。其他方法大都適用于分析一種變量的集合。 可以處理多值輸出變量問題。 使用白盒模型。如果一個(gè)情況被觀察到,使用邏輯判斷容易表示這種規(guī)則。相反,如果是黑盒模型(例如人工神經(jīng)網(wǎng)絡(luò)),結(jié)果會(huì)非常難解釋。 即使對(duì)真實(shí)模型來說,假設(shè)無效的情況下,也可以較好的適用。
決策樹的一些缺點(diǎn):
決策樹學(xué)習(xí)可能創(chuàng)建一個(gè)過于復(fù)雜的樹,并不能很好的預(yù)測(cè)數(shù)據(jù)。也就是過擬合。修剪機(jī)制(現(xiàn)在不支持),設(shè)置一個(gè)葉子節(jié)點(diǎn)需要的最小樣本數(shù)量,或者數(shù)的最大深度,可以避免過擬合。 決策樹可能是不穩(wěn)定的,因?yàn)榧词狗浅P〉淖儺悾赡軙?huì)產(chǎn)生一顆完全不同的樹。這個(gè)問題通過decision trees with an ensemble來緩解。 概念難以學(xué)習(xí),因?yàn)闆Q策樹沒有很好的解釋他們,例如,XOR, parity or multiplexer problems。 如果某些分類占優(yōu)勢(shì),決策樹將會(huì)創(chuàng)建一棵有偏差的樹。因此,建議在訓(xùn)練之前,先抽樣使樣本均衡。

推薦閱讀
? 硬核圖解,再填猛將!? Python實(shí)現(xiàn)的導(dǎo)彈跟蹤算法,燃!? 看書vs視頻,我的一點(diǎn)小建議,共勉!
