機器學習實戰教程(三):決策樹實戰篇之為自己配個隱形眼鏡
原文連結:cuijiahua.com/blog/2017/1…
一、前言
上篇文章機器學習實戰教程(二):決策樹基礎篇之讓我們從相親說起講述了機器學習決策樹的原理,以及如何選擇最優特徵作為分類特徵。本篇文章將在此基礎上進行介紹。主要包括:
- 決策樹構建
- 決策樹視覺化
- 使用決策樹進行分類預測
- 決策樹的儲存和讀取
- sklearn實戰之預測隱形眼睛型別
二、決策樹構建
上篇文章也粗略提到過,構建決策樹的演算法有很多。篇幅原因,本篇文章只使用ID3演算法構建決策樹。
1、ID3演算法
ID3演算法的核心是在決策樹各個結點上對應資訊增益準則選擇特徵,遞迴地構建決策樹。具體方法是:從根結點(root node)開始,對結點計算所有可能的特徵的資訊增益,選擇資訊增益最大的特徵作為結點的特徵,由該特徵的不同取值建立子節點;再對子結點遞迴地呼叫以上方法,構建決策樹;直到所有特徵的資訊增益均很小或沒有特徵可以選擇為止。最後得到一個決策樹。ID3相當於用極大似然法進行概率模型的選擇。
在使用ID3構造決策樹之前,我們再分析下資料。
利用上篇文章求得的結果,由於特徵A3(有自己的房子)的資訊增益值最大,所以選擇特徵A3作為根結點的特徵。它將訓練集D劃分為兩個子集D1(A3取值為"是")和D2(A3取值為"否")。由於D1只有同一類的樣本點,所以它成為一個葉結點,結點的類標記為“是”。
對D2則需要從特徵A1(年齡),A2(有工作)和A4(信貸情況)中選擇新的特徵,計算各個特徵的資訊增益:
根據計算,選擇資訊增益最大的特徵A2(有工作)作為結點的特徵。由於A2有兩個可能取值,從這一結點引出兩個子結點:一個對應"是"(有工作)的子結點,包含3個樣本,它們屬於同一類,所以這是一個葉結點,類標記為"是";另一個是對應"否"(無工作)的子結點,包含6個樣本,它們也屬於同一類,所以這也是一個葉結點,類標記為"否"。
這樣就生成了一個決策樹,該決策樹只用了兩個特徵(有兩個內部結點),生成的決策樹如下圖所示。
這樣我們就使用ID3演算法構建出來了決策樹,接下來,讓我們看看如何進行代實現。
2、編寫程式碼構建決策樹
我們使用字典儲存決策樹的結構,比如上小節我們分析出來的決策樹,用字典可以表示為:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
複製程式碼
建立函式majorityCnt統計classList中出現此處最多的元素(類標籤),建立函式createTree用來遞迴構建決策樹。編寫程式碼如下:
# -*- coding: UTF-8 -*-
from math import log
import operator
"""
函式說明:計算給定資料集的經驗熵(夏農熵)
Parameters:
dataSet - 資料集
Returns:
shannonEnt - 經驗熵(夏農熵)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回資料集的行數
labelCounts = {} #儲存每個標籤(Label)出現次數的字典
for featVec in dataSet: #對每組特徵向量進行統計
currentLabel = featVec[-1] #提取標籤(Label)資訊
if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計數
shannonEnt = 0.0 #經驗熵(夏農熵)
for key in labelCounts: #計算夏農熵
prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計算
return shannonEnt #返回經驗熵(夏農熵)
"""
函式說明:建立測試資料集
Parameters:
無
Returns:
dataSet - 資料集
labels - 特徵標籤
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def createDataSet():
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 #返回資料集和分類屬性
"""
函式說明:按照給定特徵劃分資料集
Parameters:
dataSet - 待劃分的資料集
axis - 劃分資料集的特徵
value - 需要返回的特徵的值
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #建立返回的資料集列表
for featVec in dataSet: #遍歷資料集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特徵
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的新增到返回的資料集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分後的資料集
"""
函式說明:選擇最優特徵
Parameters:
dataSet - 資料集
Returns:
bestFeature - 資訊增益最大的(最優)特徵的索引值
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特徵數量
baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵
bestInfoGain = 0.0 #資訊增益
bestFeature = -1 #最優特徵的索引值
for i in range(numFeatures): #遍歷所有特徵
#獲取dataSet的第i個所有特徵
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #建立set集合{},元素不可重複
newEntropy = 0.0 #經驗條件熵
for value in uniqueVals: #計算資訊增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集
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 #返回資訊增益最大的特徵的索引值
"""
函式說明:統計classList中出現此處最多的元素(類標籤)
Parameters:
classList - 類標籤列表
Returns:
sortedClassCount[0][0] - 出現此處最多的元素(類標籤)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統計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] #返回classList中出現次數最多的元素
"""
函式說明:建立決策樹
Parameters:
dataSet - 訓練資料集
labels - 分類屬性標籤
featLabels - 儲存選擇的最優特徵標籤
Returns:
myTree - 決策樹
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標籤(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特徵時返回出現次數最多的類標籤
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: #遍歷特徵,建立決策樹。
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
複製程式碼
遞迴建立決策樹時,遞迴有兩個終止條件:第一個停止條件是所有的類標籤完全相同,則直接返回該類標籤;第二個停止條件是使用完了所有特徵,仍然不能將資料劃分僅包含唯一類別的分組,即決策樹構建失敗,特徵不夠用。此時說明資料緯度不夠,由於第二個停止條件無法簡單地返回唯一的類標籤,這裡挑選出現數量最多的類別作為返回值。
執行上述程式碼,我們可以看到如下結果:
可見,我們的決策樹已經構建完成了。這時候,有的朋友可能會說,這個決策樹看著好彆扭,雖然這個能看懂,但是如果多點的結點,就不好看了。能直觀點嗎?完全沒有問題,我們可以使用強大的Matplotlib繪製決策樹。
三、決策樹視覺化
這裡程式碼都是關於Matplotlib的,如果對於Matplotlib不瞭解的,可以先學習下,Matplotlib的內容這裡就不再累述。視覺化需要用到的函式:
- getNumLeafs:獲取決策樹葉子結點的數目
- getTreeDepth:獲取決策樹的層數
- plotNode:繪製結點
- plotMidText:標註有向邊屬性值
- plotTree:繪製決策樹
- createPlot:建立繪製面板
我對視覺化決策樹的程式進行了詳細的註釋,直接看程式碼,除錯檢視即可。為了顯示中文,需要設定FontProperties,程式碼編寫如下:
# -*- coding: UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator
"""
函式說明:計算給定資料集的經驗熵(夏農熵)
Parameters:
dataSet - 資料集
Returns:
shannonEnt - 經驗熵(夏農熵)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回資料集的行數
labelCounts = {} #儲存每個標籤(Label)出現次數的字典
for featVec in dataSet: #對每組特徵向量進行統計
currentLabel = featVec[-1] #提取標籤(Label)資訊
if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計數
shannonEnt = 0.0 #經驗熵(夏農熵)
for key in labelCounts: #計算夏農熵
prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計算
return shannonEnt #返回經驗熵(夏農熵)
"""
函式說明:建立測試資料集
Parameters:
無
Returns:
dataSet - 資料集
labels - 特徵標籤
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def createDataSet():
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 #返回資料集和分類屬性
"""
函式說明:按照給定特徵劃分資料集
Parameters:
dataSet - 待劃分的資料集
axis - 劃分資料集的特徵
value - 需要返回的特徵的值
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #建立返回的資料集列表
for featVec in dataSet: #遍歷資料集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特徵
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的新增到返回的資料集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分後的資料集
"""
函式說明:選擇最優特徵
Parameters:
dataSet - 資料集
Returns:
bestFeature - 資訊增益最大的(最優)特徵的索引值
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特徵數量
baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵
bestInfoGain = 0.0 #資訊增益
bestFeature = -1 #最優特徵的索引值
for i in range(numFeatures): #遍歷所有特徵
#獲取dataSet的第i個所有特徵
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #建立set集合{},元素不可重複
newEntropy = 0.0 #經驗條件熵
for value in uniqueVals: #計算資訊增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集
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 #返回資訊增益最大的特徵的索引值
"""
函式說明:統計classList中出現此處最多的元素(類標籤)
Parameters:
classList - 類標籤列表
Returns:
sortedClassCount[0][0] - 出現此處最多的元素(類標籤)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統計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] #返回classList中出現次數最多的元素
"""
函式說明:建立決策樹
Parameters:
dataSet - 訓練資料集
labels - 分類屬性標籤
featLabels - 儲存選擇的最優特徵標籤
Returns:
myTree - 決策樹
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標籤(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特徵時返回出現次數最多的類標籤
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: #遍歷特徵,建立決策樹。
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
"""
函式說明:獲取決策樹葉子結點的數目
Parameters:
myTree - 決策樹
Returns:
numLeafs - 決策樹的葉子結點的數目
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def getNumLeafs(myTree):
numLeafs = 0 #初始化葉子
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用list(myTree.keys())[0]
secondDict = myTree[firstStr] #獲取下一組字典
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict': #測試該結點是否為字典,如果不是字典,代表此結點為葉子結點
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
"""
函式說明:獲取決策樹的層數
Parameters:
myTree - 決策樹
Returns:
maxDepth - 決策樹的層數
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def getTreeDepth(myTree):
maxDepth = 0 #初始化決策樹深度
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用list(myTree.keys())[0]
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
"""
函式說明:繪製結點
Parameters:
nodeTxt - 結點名
centerPt - 文字位置
parentPt - 標註的箭頭位置
nodeType - 結點格式
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
arrow_args = dict(arrowstyle="<-") #定義箭頭格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14) #設定中文字型
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)
"""
函式說明:標註有向邊屬性值
Parameters:
cntrPt、parentPt - 用於計算標註位置
txtString - 標註的內容
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
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, va="center", ha="center", rotation=30)
"""
函式說明:繪製決策樹
Parameters:
myTree - 決策樹(字典)
parentPt - 標註的內容
nodeTxt - 結點名
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def plotTree(myTree, parentPt, nodeTxt):
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] #下一個字典,也就是繼續繪製子結點
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD #y偏移
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
"""
函式說明:建立繪製面板
Parameters:
inTree - 決策樹(字典)
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def createPlot(inTree):
fig = plt.figure(1, facecolor='white') #建立fig
fig.clf() #清空fig
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #去掉x、y軸
plotTree.totalW = float(getNumLeafs(inTree)) #獲取決策樹葉結點數目
plotTree.totalD = float(getTreeDepth(inTree)) #獲取決策樹層數
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; #x偏移
plotTree(inTree, (0.5,1.0), '') #繪製決策樹
plt.show() #顯示繪製結果
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
createPlot(myTree)
複製程式碼
不出意外的話,我們就可以得到如下結果,可以看到決策樹繪製完成。plotNode函式的工作就是繪製各個結點,比如有自己的房子
、有工作
、yes
、no
,包括內結點和葉子結點。plotMidText函式的工作就是繪製各個有向邊的屬性,例如各個有向邊的0
和1
。這部分內容呢,個人感覺可以選擇性掌握,能掌握最好,不能掌握可以放一放,因為後面會介紹一個更簡單的決策樹視覺化方法。看到這句話,是不是想偷懶不仔細看這部分的程式碼了?(눈_눈)
四、使用決策樹執行分類
依靠訓練資料構造了決策樹之後,我們可以將它用於實際資料的分類。在執行資料分類時,需要決策樹以及用於構造樹的標籤向量。然後,程式比較測試資料與決策樹上的數值,遞迴執行該過程直到進入葉子結點;最後將測試資料定義為葉子結點所屬的型別。在構建決策樹的程式碼,可以看到,有個featLabels引數。它是用來幹什麼的?它就是用來記錄各個分類結點的,在用決策樹做預測的時候,我們按順序輸入需要的分類結點的屬性值即可。舉個例子,比如我用上述已經訓練好的決策樹做分類,那麼我只需要提供這個人是否有房子,是否有工作這兩個資訊即可,無需提供冗餘的資訊。
用決策樹做分類的程式碼很簡單,編寫程式碼如下:
# -*- coding: UTF-8 -*-
from math import log
import operator
"""
函式說明:計算給定資料集的經驗熵(夏農熵)
Parameters:
dataSet - 資料集
Returns:
shannonEnt - 經驗熵(夏農熵)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回資料集的行數
labelCounts = {} #儲存每個標籤(Label)出現次數的字典
for featVec in dataSet: #對每組特徵向量進行統計
currentLabel = featVec[-1] #提取標籤(Label)資訊
if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label計數
shannonEnt = 0.0 #經驗熵(夏農熵)
for key in labelCounts: #計算夏農熵
prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式計算
return shannonEnt #返回經驗熵(夏農熵)
"""
函式說明:建立測試資料集
Parameters:
無
Returns:
dataSet - 資料集
labels - 特徵標籤
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def createDataSet():
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 #返回資料集和分類屬性
"""
函式說明:按照給定特徵劃分資料集
Parameters:
dataSet - 待劃分的資料集
axis - 劃分資料集的特徵
value - 需要返回的特徵的值
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #建立返回的資料集列表
for featVec in dataSet: #遍歷資料集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特徵
reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的新增到返回的資料集
retDataSet.append(reducedFeatVec)
return retDataSet #返回劃分後的資料集
"""
函式說明:選擇最優特徵
Parameters:
dataSet - 資料集
Returns:
bestFeature - 資訊增益最大的(最優)特徵的索引值
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-20
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特徵數量
baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵
bestInfoGain = 0.0 #資訊增益
bestFeature = -1 #最優特徵的索引值
for i in range(numFeatures): #遍歷所有特徵
#獲取dataSet的第i個所有特徵
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #建立set集合{},元素不可重複
newEntropy = 0.0 #經驗條件熵
for value in uniqueVals: #計算資訊增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集
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 #返回資訊增益最大的特徵的索引值
"""
函式說明:統計classList中出現此處最多的元素(類標籤)
Parameters:
classList - 類標籤列表
Returns:
sortedClassCount[0][0] - 出現此處最多的元素(類標籤)
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-24
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #統計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] #返回classList中出現次數最多的元素
"""
函式說明:建立決策樹
Parameters:
dataSet - 訓練資料集
labels - 分類屬性標籤
featLabels - 儲存選擇的最優特徵標籤
Returns:
myTree - 決策樹
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分類標籤(是否放貸:yes or no)
if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續劃分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特徵時返回出現次數最多的類標籤
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: #遍歷特徵,建立決策樹。
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
"""
函式說明:使用決策樹分類
Parameters:
inputTree - 已經生成的決策樹
featLabels - 儲存選擇的最優特徵標籤
testVec - 測試資料列表,順序對應最優特徵標籤
Returns:
classLabel - 分類結果
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
def classify(inputTree, featLabels, testVec):
firstStr = next(iter(inputTree)) #獲取決策樹結點
secondDict = inputTree[firstStr] #下一個字典
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] #測試資料
result = classify(myTree, featLabels, testVec)
if result == 'yes':
print('放貸')
if result == 'no':
print('不放貸')
複製程式碼
這裡只增加了classify函式,用於決策樹分類。輸入測試資料[0,1],它代表沒有房子,但是有工作,分類結果如下所示:
看到這裡,細心的朋友可能就會問了,每次做預測都要訓練一次決策樹?這也太麻煩了吧?有什麼好的解決嗎?
五、決策樹的儲存
構造決策樹是很耗時的任務,即使處理很小的資料集,如前面的樣本資料,也要花費幾秒的時間,如果資料集很大,將會耗費很多計算時間。然而用建立好的決策樹解決分類問題,則可以很快完成。因此,為了節省計算時間,最好能夠在每次執行分類時呼叫已經構造好的決策樹。為了解決這個問題,需要使用Python模組pickle序列化物件。序列化物件可以在磁碟上儲存物件,並在需要的時候讀取出來。
假設我們已經得到決策樹{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}},使用pickle.dump儲存決策樹。
# -*- coding: UTF-8 -*-
import pickle
"""
函式說明:儲存決策樹
Parameters:
inputTree - 已經生成的決策樹
filename - 決策樹的儲存檔名
Returns:
無
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
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')
複製程式碼
執行程式碼,在該Python檔案的相同目錄下,會生成一個名為classifierStorage.txt的txt檔案,這個檔案二進位制儲存著我們的決策樹。我們可以使用sublime txt開啟看下儲存結果。
看不懂?沒錯,因為這個是個二進位制儲存的檔案,我們也無需看懂裡面的內容,會儲存,會用即可。那麼問題來了。將決策樹儲存完這個二進位制檔案,然後下次使用的話,怎麼用呢?
很簡單使用pickle.load進行載入即可,編寫程式碼如下:
# -*- coding: UTF-8 -*-
import pickle
"""
函式說明:讀取決策樹
Parameters:
filename - 決策樹的儲存檔名
Returns:
pickle.load(fr) - 決策樹字典
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Modify:
2017-07-25
"""
def grabTree(filename):
fr = open(filename, 'rb')
return pickle.load(fr)
if __name__ == '__main__':
myTree = grabTree('classifierStorage.txt')
print(myTree)
複製程式碼
如果在該Python檔案的相同目錄下,有一個名為classifierStorage.txt的檔案,那麼我們就可以執行上述程式碼,執行結果如下圖所示:
從上述結果中,我們可以看到,我們順利載入了儲存決策樹的二進位制檔案。
六、Sklearn之使用決策樹預測隱形眼睛型別
1、實戰背景
進入本文的正題:眼科醫生是如何判斷患者需要佩戴隱形眼鏡的型別的?一旦理解了決策樹的工作原理,我們甚至也可以幫助人們判斷需要佩戴的鏡片型別。
隱形眼鏡資料集是非常著名的資料集,它包含很多換著眼部狀態的觀察條件以及醫生推薦的隱形眼鏡型別。隱形眼鏡型別包括硬材質(hard)、軟材質(soft)以及不適合佩戴隱形眼鏡(no lenses)。資料來源與UCI資料庫,資料集下載地址:github.com/Jack-Cheris…
一共有24組資料,資料的Labels依次是age、prescript、astigmatic、tearRate、class,也就是第一列是年齡,第二列是症狀,第三列是是否散光,第四列是眼淚數量,第五列是最終的分類標籤。資料如下圖所示:
可以使用已經寫好的Python程式構建決策樹,不過出於繼續學習的目的,本文使用Sklearn實現。
2、使用Sklearn構建決策樹
官方英文文件地址:scikit-learn.org/stable/modu…
sklearn.tree模組提供了決策樹模型,用於解決分類問題和迴歸問題。方法如下圖所示:
本次實戰內容使用的是DecisionTreeClassifier和export_graphviz,前者用於決策樹構建,後者用於決策樹視覺化。
DecisionTreeClassifier構建決策樹:
讓我們先看下DecisionTreeClassifier這個函式,一共有12個引數:
引數說明如下:
- criterion:特徵選擇標準,可選引數,預設是gini,可以設定為entropy。gini是基尼不純度,是將來自集合的某種結果隨機應用於某一資料項的預期誤差率,是一種基於統計的思想。entropy是夏農熵,也就是上篇文章講過的內容,是一種基於資訊理論的思想。Sklearn把gini設為預設引數,應該也是做了相應的斟酌的,精度也許更高些?ID3演算法使用的是entropy,CART演算法使用的則是gini。
- splitter:特徵劃分點選擇標準,可選引數,預設是best,可以設定為random。每個結點的選擇策略。best引數是根據演算法選擇最佳的切分特徵,例如gini、entropy。random隨機的在部分劃分點中找區域性最優的劃分點。預設的"be