這裏給出一些我覺得比較好的博客鏈接:
http://blog.jobbole.com/86443/ 通俗易懂,同時也講了一些決策樹算法:如ID3、C4.5之類的。以及建立完完整的決策樹之後,爲了防止過擬合而進行的剪枝的操作。
決策樹算法介紹及應用:http://blog.jobbole.com/89072/ 這篇博文寫的非常的好,主要是將數據挖掘導論這本數的第四章給總結了一番。主要講解了決策樹的建立,決策樹的評估以及最後使用R語言和SPASS來進行決策樹的建模。
數據挖掘(6):決策樹分類算法:http://blog.jobbole.com/90165/ 這篇博客將分類寫的很詳細。並且列舉了一個不同人羣,不同信貸購買電腦的例子。可惜沒有實現代碼。
在這裏我在讀完這3篇博文之後,給出了用python語言和sklearn包的完整實現的例子。
決策樹(decision tree)是一個樹結構(可以是二叉樹或非二叉樹)。其每個非葉節點表示一個特徵屬性上的測試,每個分支代表這個特徵屬性在某個值域上的輸出,而每個葉節點存放一個類別。使用決策樹進行決策的過程就是從根節點開始,測試待分類項中相應的特徵屬性,並按照其值選擇輸出分支,直到到達葉子節點,將葉子節點存放的類別作爲決策結果。
決策樹的一般流程:
1、收集數據:可以使用任何的方法
2、準備數據:樹構造算法只適用標稱型數據,因此數值型的數據必須離散化。
3、分析數據:
4、訓練算法:構造樹的數據結構
5、測試算法:使用經驗樹計算錯誤率
6、使用算法
這裏給的例子如下:(注意:這裏使用的是ID3)
記錄ID 年齡 輸入層次 學生 信用等級 是否購買電腦
1 青少年
高 否
一般 否
2 青少年
高 否
良好 否
3 中年
高 否
一般 是
4 老年
中 否
一般 是
5 老年
低
是 一般 是
6 老年
低 是
良好 否
7 中年
低 是
良好 是
8 青少年
中 否
一般 否
9 青少年
低 是
一般 是
10 老年
中 是
一般 是
11 青少年
中 是
良好 是
12 中年
中
否 良好
是
13 中年
高
是 一般
是
14 老年
中 否
良好 否
這裏我將其轉換爲特徵向量的形式如下:
dataSet = [[1, 3, 0, 1, 'no'],
[1, 3, 0, 2, 'no'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 1, 'yes'],
[3, 1, 1, 1, 'yes'],
[3, 1, 1, 2, 'no'],
[2, 1, 1, 2, 'yes'],
[1, 2, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[3, 2, 1, 1, 'yes'],
[1, 2, 1, 2, 'yes'],
[2, 2, 0, 2, 'yes'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 2, 'no'],
]
labels = ['age','salary','isStudent','credit']
完整的實現決策樹的代碼:
#-*- coding:utf-8 -*-
from math import log
import operator
def createDataSet():
dataSet = [[1, 3, 0, 1, 'no'],
[1, 3, 0, 2, 'no'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 1, 'yes'],
[3, 1, 1, 1, 'yes'],
[3, 1, 1, 2, 'no'],
[2, 1, 1, 2, 'yes'],
[1, 2, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[3, 2, 1, 1, 'yes'],
[1, 2, 1, 2, 'yes'],
[2, 2, 0, 2, 'yes'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 2, 'no'],
]
labels = ['age','salary','isStudent','credit']
#change to discrete values
return dataSet, labels
############計算香農熵###############
def calcShannonEnt(dataSet):
numEntries = len(dataSet)#計算實例的總數
labelCounts = {}#創建一個數據字典,它的key是最後把一列的數值(即標籤),value記錄當前類型(即標籤)出現的次數
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) #計算香農熵
return shannonEnt
#########按給定的特徵劃分數據#########
def splitDataSet(dataSet, axis, value): #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
#######遍歷整個數據集,選擇最好的數據集劃分方式########
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #獲取當前實例的特徵個數,一般最後一列是標籤。the last column is used for the labels
baseEntropy = calcShannonEnt(dataSet)#計算當前實例的香農熵
bestInfoGain = 0.0; bestFeature = -1#這裏初始化最佳的信息增益和最佳的特徵
for i in range(numFeatures): #遍歷每一個特徵 iterate over all the features
featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
uniqueVals = set(featList) #創建唯一的分類標籤
newEntropy = 0.0
for value in uniqueVals:#計算每種劃分方式的信息熵
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy #計算信息增益
if (infoGain > bestInfoGain): #比較每個特徵的信息增益,只要最好的信息增益
bestInfoGain = infoGain #if better than current best, set to best
bestFeature = i
return bestFeature,bestInfoGain #返回最佳劃分的特徵索引和信息增益
'''該函數使用分類名稱的列表,然後創建鍵值爲classList中唯一值的數據字典。字典
對象的存儲了classList中每個類標籤出現的頻率。最後利用operator操作鍵值排序字典,
並返回出現次數最多的分類名稱
'''
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet]#返回所有的標籤
if classList.count(classList[0]) == len(classList): #當類別完全相同時則停止繼續劃分,直接返回該類的標籤
return classList[0]#stop splitting when all of the classes are equal
if len(dataSet[0]) == 1: #遍歷完所有的特徵時,仍然不能將數據集劃分成僅包含唯一類別的分組
return majorityCnt(classList)#由於無法簡單的返回唯一的類標籤,這裏就返回出現次數最多的類別作爲返回值
bestFeat,bestInfogain= chooseBestFeatureToSplit(dataSet)#獲取最好的分類特徵索引
bestFeatLabel = labels[bestFeat]#獲取該特徵的名稱
#這裏直接使用字典變量來存儲樹信息,這對於繪製樹形圖很重要。
myTree = {bestFeatLabel:{}}#當前數據集選取最好的特徵存儲在bestFeat中
del(labels[bestFeat])#刪除已經在選取的特徵
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
def classify(inputTree,featLabels,testVec):
firstStr = inputTree.keys()[0]
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,'w')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)
import matplotlib.pyplot as plt
decisionNode = dict(boxstyle="sawtooth", fc="0.8") #定義文本框與箭頭的格式
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def getNumLeafs(myTree):#獲取樹節點的數目
numLeafs = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():#測試節點的數據類型是不是字典,如果是則就需要遞歸的調用getNumLeafs()函數
if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
def getTreeDepth(myTree):#獲取樹節點的樹的層數
maxDepth = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
thisDepth = 1 + getTreeDepth(secondDict[key])
else: thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
def plotNode(nodeTxt, centerPt, parentPt, nodeType): #繪製帶箭頭的註釋
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',#createPlot.ax1會提供一個繪圖區
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args )
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)
def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
numLeafs = getNumLeafs(myTree) #首先計算樹的寬和高
depth = getTreeDepth(myTree)
firstStr = myTree.keys()[0] #the text label for this node should be this
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 secondDict.keys():
if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
plotTree(secondDict[key],cntrPt,str(key)) #recursion
else: #it's a leaf node print the leaf node
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
#if you do get a dictonary you know it's a tree, and the first element will be another dict
#
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #no ticks
#createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
plotTree.totalW = float(getNumLeafs(inTree))#c存儲樹的寬度
plotTree.totalD = float(getTreeDepth(inTree))#存儲樹的深度。我們使用這兩個變量計算樹節點的擺放位置
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
plotTree(inTree, (0.5,1.0), '')
plt.show()
# def createPlot():
# fig = plt.figure(1, facecolor='white')
# fig.clf()
# createPlot.ax1 = plt.subplot(111, frameon=False) #創建一個新圖形,並清空繪圖區
# 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()
測試代碼:
#########測試代碼#############
myDat,labels=createDataSet()
print calcShannonEnt(myDat)
print myDat
bestfeature,bestInfogain=chooseBestFeatureToSplit(myDat)
print bestfeature,bestInfogain
myTree=createTree(myDat, labels)
print myTree
print getNumLeafs(myTree)
print getTreeDepth(myTree)
createPlot(myTree)
##########測試結束#############
結果顯示:0.940285958671
[[1, 3, 0, 1, 'no'], [1, 3, 0, 2, 'no'], [2, 3, 0, 1, 'yes'], [3, 2, 0, 1, 'yes'], [3, 1, 1, 1, 'yes'], [3, 1, 1, 2, 'no'], [2, 1, 1, 2, 'yes'], [1, 2, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [3, 2, 1, 1, 'yes'], [1, 2, 1, 2, 'yes'], [2, 2, 0, 2, 'yes'], [2, 3, 0, 1, 'yes'], [3, 2, 0, 2, 'no']]
0 0.246749819774
{'age': {1: {'isStudent': {0: 'no', 1: 'yes'}}, 2: 'yes', 3: {'credit': {1: 'yes', 2: 'no'}}}}
5
2
畫出決策樹圖:
決策樹1.png
接下來我打算使用python的scikit-learn機器學習算法包來實現上面的決策樹:
#-*- coding:utf-8 -*-
from sklearn.datasets import load_iris
from sklearn import tree
dataSet = [[1, 3, 0, 1, 'no'],
[1, 3, 0, 2, 'no'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 1, 'yes'],
[3, 1, 1, 1, 'yes'],
[3, 1, 1, 2, 'no'],
[2, 1, 1, 2, 'yes'],
[1, 2, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[3, 2, 1, 1, 'yes'],
[1, 2, 1, 2, 'yes'],
[2, 2, 0, 2, 'yes'],
[2, 3, 0, 1, 'yes'],
[3, 2, 0, 2, 'no'],
]
labels = ['age','salary','isStudent','credit']
from sklearn.cross_validation import train_test_split #這裏是引用了交叉驗證
FeatureSet=[]
Label=[]
for i in dataSet:
FeatureSet.append(i[:-1])
Label.append(i[-1])
X_train,X_test, y_train, y_test = train_test_split(FeatureSet, Label, random_state=1)#將數據隨機分成訓練集和測試集
print X_train
print X_test
print y_train
print y_test
#print iris
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)
from sklearn.externals.six import StringIO
with open("isBuy.dot", 'w') as f:
f = tree.export_graphviz(clf, out_file=f)
import os
os.unlink('isBuy.dot')
#
from sklearn.externals.six import StringIO
import pydot #注意要安裝pydot2這個python插件。否則會報錯。
dot_data = StringIO()
tree.export_graphviz(clf, out_file=dot_data)
graph = pydot.graph_from_dot_data(dot_data.getvalue())
graph.write_pdf("isBuy.pdf") #將決策樹以pdf格式輸出
pre_labels=clf.predict(X_test)
print pre_labels
結果顯示如下:
[[1, 2, 1, 2], [3, 1, 1, 1], [1, 3, 0, 2], [2, 3, 0, 1], [1, 3, 0, 1], [3, 2, 0, 2], [3, 2, 1, 1], [1, 1, 1, 1], [2, 2, 0, 2], [3, 1, 1, 2]]
[[3, 2, 0, 1], [1, 2, 0, 1], [2, 1, 1, 2], [2, 3, 0, 1]]
['yes', 'yes', 'no', 'yes', 'no', 'no', 'yes', 'yes', 'yes', 'no']
['yes', 'no', 'yes', 'yes']
['yes' 'yes' 'yes' 'yes']
由最後兩行我們可以發現第二個結果預測錯誤了。但是這並不能說明我們的決策樹不夠好。相反我覺得這樣反而不會出現過擬合的現象。當然也不是絕對的,如果過擬合的化,就需要考慮剪枝了。這是後話了。
決策樹的圖片如下:
圖片中的X[3]表示的是labels = ['age','salary','isStudent','credit']中的索引爲3的特徵。
該圖將會保存在isBuy.pd