您的位置:首页 > 编程语言 > Python开发

决策树ID3和C4.5算法Python实现源码

2014-05-28 16:21 621 查看
决策树的概念其实不难理解,下面一张图是某女生相亲时用到的决策树:



基本上可以理解为:一堆数据,附带若干属性,每一条记录最后都有一个分类(见或者不见),然后根据每种属性可以进行划分(比如年龄是>30还是<=30),这样构造出来的一棵树就是我们所谓的决策树了,决策的规则都在节点上,通俗易懂,分类效果好。

那为什么跟节点要用年龄,而不是长相?这里我们在实现决策树的时候采用的是ID3算法,在选择哪个属性作为节点的时候采用信息论原理,所谓的信息增益。信息增益指原有数据集的熵-按某个属性分类后数据集的熵。信息增益越大越好(说明按某个属性分类后比较纯),我们会选择使得信息增益最大的那个属性作为当层节点的标记,再进行递归构造决策树。

首先我们构造数据集:

[python] view
plaincopy

def createDataSet():

dataSet = [[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]

features = ['no surfacing','flippers']

return dataSet,features

构造决策树:(采用python字典来递归构造,一些代码看看就能看懂)

[python] view
plaincopy

def treeGrowth(dataSet,features):

classList = [example[-1] for example in dataSet]

if classList.count(classList[0])==len(classList):

return classList[0]

if len(dataSet[0])==1:# no more features

return classify(classList)

bestFeat = findBestSplit(dataSet)#bestFeat is the index of best feature

bestFeatLabel = features[bestFeat]

myTree = {bestFeatLabel:{}}

featValues = [example[bestFeat] for example in dataSet]

uniqueFeatValues = set(featValues)

del (features[bestFeat])

for values in uniqueFeatValues:

subDataSet = splitDataSet(dataSet,bestFeat,values)

myTree[bestFeatLabel][values] = treeGrowth(subDataSet,features)

return myTree

当没有多余的feature,但是剩下的样本不完全是一样的类别是,采用出现次数多的那个类别:

[python] view
plaincopy

def classify(classList):

'''''

find the most in the set

'''

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]

寻找用于分裂的最佳属性:(遍历所有属性,算信息增益)

[python] view
plaincopy

def findBestSplit(dataset):

numFeatures = len(dataset[0])-1

baseEntropy = calcShannonEnt(dataset)

bestInfoGain = 0.0

bestFeat = -1

for i in range(numFeatures):

featValues = [example[i] for example in dataset]

uniqueFeatValues = set(featValues)

newEntropy = 0.0

for val in uniqueFeatValues:

subDataSet = splitDataSet(dataset,i,val)

prob = len(subDataSet)/float(len(dataset))

newEntropy += prob*calcShannonEnt(subDataSet)

if(baseEntropy - newEntropy)>bestInfoGain:

bestInfoGain = baseEntropy - newEntropy

bestFeat = i

return bestFeat

选择完分裂属性后,就行数据集的分裂:

[python] view
plaincopy

def splitDataSet(dataset,feat,values):

retDataSet = []

for featVec in dataset:

if featVec[feat] == values:

reducedFeatVec = featVec[:feat]

reducedFeatVec.extend(featVec[feat+1:])

retDataSet.append(reducedFeatVec)

return retDataSet

计算数据集的熵:

[python] view
plaincopy

def calcShannonEnt(dataset):

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

if prob != 0:

shannonEnt -= prob*log(prob,2)

return shannonEnt

下面根据上面构造的决策树进行数据的分类:

[python] view
plaincopy

def predict(tree,newObject):

while isinstance(tree,dict):

key = tree.keys()[0]

tree = tree[key][newObject[key]]

return tree

if __name__ == '__main__':

dataset,features = createDataSet()

tree = treeGrowth(dataset,features)

print tree

print predict(tree,{'no surfacing':1,'flippers':1})

print predict(tree,{'no surfacing':1,'flippers':0})

print predict(tree,{'no surfacing':0,'flippers':1})

print predict(tree,{'no surfacing':0,'flippers':0})

结果如下:

决策树是这样的:

{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

四个预测:

yes

no

no

no

和给定的数据集分类一样(预测的数据是从给定数据集里面抽取的,当然一般数据多的话,会拿一部分做训练数据,剩余的做测试数据)

归纳一下ID3的优缺点:

优点:实现比较简单,产生的规则如果用图表示出来的话,清晰易懂,分类效果好

缺点:只能处理属性值离散的情况(连续的用C4.5),在选择最佳分离属性的时候容易选择那些属性值多的一些属性。


[置顶] 怒写一个digit classification(不断更新中)

分类: python 算法2013-09-12
16:11 151人阅读 评论(0) 收藏 举报

digit
classificationknnsvmnaive
bayesdecision
tree

最近开始学习machine learning方面的内容,大致浏览了一遍《machine learning in action》一书,大概了解了一些常用的算法如knn,svm等具体式干啥的。

在kaggle上看到一个练手的项目:digit classification,又有良好的数据,于是打算用这个项目把各种算法都跑一遍,加深自己对各算法的研究,该文会不断更新。。。。。。

我们的数据集是mnist,链接:http://yann.lecun.com/exdb/mnist/

mnist的结构如下,选取train-images


TRAINING SET IMAGE FILE (train-images-idx3-ubyte):

[offset] [type] [value] [description]

0000 32 bit integer 0x00000803(2051) magic number

0004 32 bit integer 60000 number of images

0008 32 bit integer 28 number of rows

0012 32 bit integer 28 number of columns

0016 unsigned byte ?? pixel

0017 unsigned byte ?? pixel

........

xxxx unsigned byte ?? pixel

呐,由于智商比较拙急,看了这个形式居然没看明白,所以特此写出要注意的点,送给同样没看明白的童鞋。

首先该数据是以二进制存储的,我们读取的时候要以'rb'方式读取,其次,真正的数据只有[value]这一项,其他的[type]等只是来描述的,并不真正在数据文件里面。

由offset我们可以看出真正的pixel式从16开始的,一个int 32字节,所以在读取pixel之前我们要读取4个 32 bit integer,也就是magic number,number of images,number of rows,number of columns,读取二进制文件用struct比较方便,struct.unpack_from('>IIII',buf,index)表示按照大端方式读取4个int.

虽然数据集网站写着“Users of Intel processors and other low-endian machines must flip the bytes of the header.”,而我的电脑就是intel处理器,但是我尝试了一把还是得用大端方式读,读出来才是“2051 60000 28 28”,用小端方式读取就不正确了,这个小小实验一把就行。

下面先把数据文件直观的表现出来,用matplotlib把二进制文件用图像表现出来。具体如下:

[python] view
plaincopy

# -*- coding:utf-8

import numpy as np

import struct

import matplotlib.pyplot as plt

filename = 'train-images.idx3-ubyte'

binfile = open(filename,'rb')#以二进制方式打开

buf = binfile.read()

index = 0

magic, numImages, numRows, numColums = struct.unpack_from('>IIII',buf,index)#读取4个32 int

print magic,' ',numImages,' ',numRows,' ',numColums

index += struct.calcsize('>IIII')

im = struct.unpack_from('>784B',buf,index)#每张图是28*28=784Byte,这里只显示第一张图

index += struct.calcsize('>784B' )

im = np.array(im)

im = im.reshape(28,28)

print im

fig = plt.figure()

plt.imshow(im,cmap = 'binary')#黑白显示

plt.show()

结果如下(很明显式一个5字):



好,既然能够图形化表示数字,基本上该数字的feature我们都能拿到,一下就是用各种机器学习算法来做digit classification。

以上是显示一个图片,而我们的各种学习算法一定是处理多个图片的,那么有必要单独写一个文件来处理数据问题,即

ReadData.py:(代码和上面不少相似,这里只给链接)

https://github.com/wangyuquan/MachineLearning/blob/master/digitClassification/ReadData.py

(1)KNN:

KNN可以说是最简单的分类器了。首先给予训练数据,并且给予这些训练数据的类别,然后当有一个新的数据进来的时候,根据最临近的k个节点的分类情况,例如k=5,就

取离新数据最近的5个点,假设3个类别是A,2个是B,按照多数服从少数的原则,将新数据赋予类别A.

具体代码如下:

[python] view
plaincopy

from sklearn import neighbors

from ReadData import *

import datetime

startTime = datetime.datetime.now();

trainImage = getTrainImage()

trainLabel = getTrainLabel()

testImage = getTestImage()

testLabel = getTestLabel()

knn = neighbors.KNeighborsClassifier(algorithm = 'auto',leaf_size = 30,n_neighbors=3,warn_on_equidistant = True,weights = 'uniform')

knn.fit(trainImage,trainLabel)

match = 0;

for i in xrange(len(testLabel)):

predictLabel = knn.predict(testImage[i])[0]

print i,' ',

print predictLabel,' ', testLabel[i]

if(predictLabel==testLabel[i]):

match += 1

endTime = datetime.datetime.now()

print 'use time: '+str(endTime-startTime)

print 'error rate: '+ str(1-(match*1.0/len(testLabel)))

效果如下:



错误率为3.91%,这里我的k选的是3,接下来我会尝试不同的k,来看不同k带来的不同影响。

优点:简单易懂,实现方便(指具体算法实现,当然我们在用的时候都是直接调用库啦:))

缺点:算法复杂度为K*M(train data的大小)*N(test data的大小),耗时较长,因为每个新数据都要与所以training data计算下距离,大数据情况下可能吃不消

(2)SVM:

svm可以写的还真的挺多的,这里只写一些比较重要的,并且需要记住的几点

首先svm本质上是一个二元分类器,但是也可以扩展来出来多元分类(先一分为二,剩下的再分,还有DAG等方法)。svm可以处理线性可分或者不可分的数据:线性可分的话,举个例子在二维平面数据可以被一条直线一分为二,主要工作就是找这条直线(维数多了就是超平面),然后通过优化方法找到直线或者超平面,使得margin最大(margin指点到分割面的最短距离),对于线性不可分的数据来说,采用升维的方法,也就是大家指的核函数,把数据变成基本可分,引入松弛变量来处理某些噪声点,也就是所谓的离群点,用惩罚因子来代表我们对离群点的重视度,越大越重视。至今还没真正理解svm的算法复杂度,等待进一步的学习。以下代码还是直接调用库函数,有空自己写一个简单版的.(这个没有进行多种调参,因为只是想学习svm的原理)

具体代码如下:

[python] view
plaincopy

from sklearn import svm

from ReadData import *

import datetime

startTime = datetime.datetime.now();

trainImage = getTrainImage()

trainLabel = getTrainLabel()

testImage = getTestImage()

testLabel = getTestLabel()

clf = svm.SVC(kernel='rbf')

clf.fit(trainImage,trainLabel)

match = 0

for i in range(len(testImage)):

predictResult = int(clf.predict(testImage[i])[0])

if(predictResult==testLabel[i]):

match += 1

print i,' ',predictResult ,' ',testLabel[i]

endTime = datetime.datetime.now()

print 'match:',match

print 'use time: '+str(endTime-startTime)

print 'error rate: '+ str(1-(match*1.0/len(testImage)))

结果如下:



3:Navie Bayes

贝叶斯分类器均建立在贝叶斯公式上:P(x|y) = P(y|x)*P(x)/P(y)

在分类问题中,x代表类别,y代表特征向量,P(x|y)很难求,但是可以通过等号后面几者来求,P(y)是确定的,可以忽略,求某个特征向量最可能属于哪个类是,只要比出哪个类的概率最大就行,在比较是分母都一样,所以可以忽略,只需要求 P(y|x)*P(x),P(x)表示类别的概率, P(y|x)表示该类下特征向量是y的概率,这个有点难求。但是朴素贝叶斯假定,每个feature之间相互无关,即 P(y|x) = P(y1|x)*P(y2|x)****P(yn|x),这样就能求得该变量,该假设十分简单有效。

具体代码如下:

[python] view
plaincopy

<pre name="code" class="python">from sklearn.naive_bayes import MultinomialNB

from ReadData import *

import datetime

startTime = datetime.datetime.now();

trainImage = getTrainImage()

trainLabel = getTrainLabel()

testImage = getTestImage()

testLabel = getTestLabel()

mnb = MultinomialNB()

mnb.fit(trainImage,trainLabel)

match = 0

for i in range(len(testImage)):

predictResult = int(mnb.predict(testImage[i])[0])

if(predictResult==testLabel[i]):

match += 1

print i,' ',predictResult ,' ',testLabel[i]

endTime = datetime.datetime.now()

print 'match:',match

print 'use time: '+str(endTime-startTime)

print 'error rate: '+ str(1-(match*1.0/len(testImage)))</pre><br><br>

结果如图:



错误率较高,原因很明显,我们将每个像素作为feature,这些feature之间肯定是有关的,不能假设为独立

navie bayes适合各个feature独立的分类:)

(4):decision tree

决策树是一个很能直观理解的分类过程。不需要额外的参数。具体怎么构造决策树,选取哪个feature作为决策点,主要算法有ID3和C4.5。

具体代码如下:

[python] view
plaincopy

from sklearn import tree

from ReadData import *

import datetime

startTime = datetime.datetime.now();

trainImage = getTrainImage()

trainLabel = getTrainLabel()

testImage = getTestImage()

testLabel = getTestLabel()

clf = tree.DecisionTreeClassifier()

clf.fit(trainImage,trainLabel)

match = 0

for i in range(len(testImage)):

predictResult = int(clf.predict(testImage[i])[0])

if(predictResult==testLabel[i]):

match += 1

print i,' ',predictResult ,' ',testLabel[i]

endTime = datetime.datetime.now()

print 'match:',match

print 'use time: '+str(endTime-startTime)

print 'error rate: '+ str(1-(match*1.0/len(testImage)))

结果如下:



结果也不是特别令人满意,原因和上一个差不多,我们构造的feature不太适合用决策树来分类。

(5)TODO:

首先推荐李航的《统计机器学习》这本书,这个实现就是按照书上的算法来的。Python 用的是最新的3.3版的,和2.x不兼容,运行的时候需要注意。

[python]
view plaincopy





'''''

Created on 2012-12-18

@author: weisu.yxd
'''

class Node:
'''''Represents a decision tree node.

'''
def __init__(self, parent = None, dataset = None):
self.dataset = dataset # 落在该结点的训练实例集
self.result = None # 结果类标签
self.attr = None # 该结点的分裂属性ID
self.childs = {} # 该结点的子树列表,key-value pair: (属性attr的值, 对应的子树)
self.parent = parent # 该结点的父亲结点

def entropy(props):
if (not isinstance(props, (tuple, list))):
return None

from math import log
log2 = lambda x:log(x)/log(2) # an anonymous function
e = 0.0
for p in props:
e -= p * log2(p)
return e

def info_gain(D, A, T = -1, return_ratio = False):
'''''特征A对训练数据集D的信息增益 g(D,A)

g(D,A)=entropy(D) - entropy(D|A)
假设数据集D的每个元组的最后一个特征为类标签
T为目标属性的ID,-1表示元组的最后一个元素为目标'''
if (not isinstance(D, (set, list))):
return None
if (not type(A) is int):
return None
C = {} # 类别计数字典
DA = {} # 特征A的取值计数字典
CDA = {} # 类别和特征A的不同组合的取值计数字典
for t in D:
C[t[T]] = C.get(t[T], 0) + 1
DA[t[A]] = DA.get(t[A], 0) + 1
CDA[(t[T], t[A])] = CDA.get((t[T], t[A]), 0) + 1

PC = map(lambda x : x / len(D), C.values()) # 类别的概率列表
entropy_D = entropy(tuple(PC)) # map返回的对象类型为map,需要强制类型转换为元组

PCDA = {} # 特征A的每个取值给定的条件下各个类别的概率(条件概率)
for key, value in CDA.items():
a = key[1] # 特征A
pca = value / DA[a]
PCDA.setdefault(a, []).append(pca)

condition_entropy = 0.0
for a, v in DA.items():
p = v / len(D)
e = entropy(PCDA[a])
condition_entropy += e * p

if (return_ratio):
return (entropy_D - condition_entropy) / entropy_D
else:
return entropy_D - condition_entropy

def get_result(D, T = -1):
'''''获取数据集D中实例数最大的目标特征T的值'''
if (not isinstance(D, (set, list))):
return None
if (not type(T) is int):
return None
count = {}
for t in D:
count[t[T]] = count.get(t[T], 0) + 1
max_count = 0
for key, value in count.items():
if (value > max_count):
max_count = value
result = key
return result

def devide_set(D, A):
'''''根据特征A的值把数据集D分裂为多个子集'''
if (not isinstance(D, (set, list))):
return None
if (not type(A) is int):
return None
subset = {}
for t in D:
subset.setdefault(t[A], []).append(t)
return subset

def build_tree(D, A, threshold = 0.0001, T = -1, Tree = None, algo = "ID3"):
'''''根据数据集D和特征集A构建决策树.

T为目标属性在元组中的索引 . 目前支持ID3和C4.5两种算法'''
if (Tree != None and not isinstance(Tree, Node)):
return None
if (not isinstance(D, (set, list))):
return None
if (not type(A) is set):
return None

if (None == Tree):
Tree = Node(None, D)
subset = devide_set(D, T)
if (len(subset) <= 1):
for key in subset.keys():
Tree.result = key
del(subset)
return Tree
if (len(A) <= 0):
Tree.result = get_result(D)
return Tree
use_gain_ratio = False if algo == "ID3" else True
max_gain = 0.0
for a in A:
gain = info_gain(D, a, return_ratio = use_gain_ratio)
if (gain > max_gain):
max_gain = gain
attr_id = a # 获取信息增益最大的特征
if (max_gain < threshold):
Tree.result = get_result(D)
return Tree
Tree.attr = attr_id
subD = devide_set(D, attr_id)
del(D[:]) # 删除中间数据,释放内存
Tree.dataset = None
A.discard(attr_id) # 从特征集中排查已经使用过的特征
for key in subD.keys():
tree = Node(Tree, subD.get(key))
Tree.childs[key] = tree
build_tree(subD.get(key), A, threshold, T, tree)
return Tree

def print_brance(brance, target):
odd = 0
for e in brance:
print(e, end = ('=' if odd == 0 else '∧'))
odd = 1 - odd
print("target =", target)

def print_tree(Tree, stack = []):
if (None == Tree):
return
if (None != Tree.result):
print_brance(stack, Tree.result)
return
stack.append(Tree.attr)
for key, value in Tree.childs.items():
stack.append(key)
print_tree(value, stack)
stack.pop()
stack.pop()

def classify(Tree, instance):
if (None == Tree):
return None
if (None != Tree.result):
return Tree.result
return classify(Tree.childs[instance[Tree.attr]], instance)

dataset = [
("青年", "否", "否", "一般", "否")
,("青年", "否", "否", "好", "否")
,("青年", "是", "否", "好", "是")
,("青年", "是", "是", "一般", "是")
,("青年", "否", "否", "一般", "否")
,("中年", "否", "否", "一般", "否")
,("中年", "否", "否", "好", "否")
,("中年", "是", "是", "好", "是")
,("中年", "否", "是", "非常好", "是")
,("中年", "否", "是", "非常好", "是")
,("老年", "否", "是", "非常好", "是")
,("老年", "否", "是", "好", "是")
,("老年", "是", "否", "好", "是")
,("老年", "是", "否", "非常好", "是")
,("老年", "否", "否", "一般", "否")
]

T = build_tree(dataset, set(range(0, len(dataset[0]) - 1)))
print_tree(T)
print(classify(T, ("老年", "否", "否", "一般")))

运行结果如下:

2=否∧1=否∧target = 否

2=否∧1=是∧target = 是

2=是∧target = 是



决策树ID3实现源码如下:

#coding=utf-8
'''
Created on Oct 12, 2010
Decision Tree Source Code for Machine Learning in Action Ch. 3
@author: Peter Harrington

决策树:
决策树很多任务都是问了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则
机器学习算法最终将使用这些机器从数据集中创造的规则
优点 见复杂度不高,输出结果易于理解,对中间值的确实不敏感,可以处理不相关特征数据
缺点 可能产生过度匹配的问题

过度匹配问题:
过度拟合(overfitting)是指数据模型在训练集里表现非常满意,但是一旦应用到真实业务实践时,效果大打折扣
模型对样本数据拟合非常好,但是对于样本数据外的应用数据,拟合效果非常差。

使用数据类型 数值型和标称型

在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类
时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测
试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支
上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,
无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子
集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数
据均在一个数据子集内。

'''

'''
伪代码如下:
检测数据集中的每个子项是否属于同一分类:
If so return类标签;
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return分支节点
该函数是递归函数,在倒数第二行调用它自身

决策树的一般流程
(1)收集数据:可以使用任何方法。
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。

ID3算法划分数据

信息增益:
划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,
但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息
论是量化处理信息的分支科学。我们可以在划分数据之前使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以
计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。

如果待分类的事务可能划分在多个分类之中,则符合xi的信息定义为
l(xi)=-log2p(xi)   p(xi) 选择该分类的概率

总结:
决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据集时,我们首
先需要测量集合中数据的不一致性,也就是嫡,然后寻找最优方案划分数据集,直到数据集中的
所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递
归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用Python语言内嵌的
数据结构字典存储树节点信息。
使用Matplotlib的注解功能,我们可以将存储的树结构转化为容易理解的图形。Python语言的
pickle's块可用于存储决策树的结构。隐形眼镜的例子表明决策树可能会产生过多的数据集划分,
从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益
的叶节点,消除过度匹配问题。

'''

from math import log
import operator
'''
计算数据集中实例的总数。我们也可以在需要时再计
算这个值,但是由于代码中多次用到这个值,为了提高代码效率,我们显式地声明一个变量保存
实例总数。然后,创建一个数据字典,它的键值是最后一列的数值O。如果当前键值不存在,则
扩展字典并将当前键值加人字典。每个键值都记录了当前类别出现的次数。最后,使用所有类标
签的发生频率计算类别出现的概率。我们将用这个概率计算香农嫡O,统计所有类标签发生的次
数

'''
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']
#change to discrete values
return dataSet, labels
'''
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(myDat)
0.9709505944546686
嫡越高,则混合的数据也越多,我们可以在数据集中添加更多的分类,观察嫡是如何变化的。

'''
def calcShannonEnt(dataSet):
numEntries = len(dataSet)  #计算数据集中实例的总数
labelCounts = {}
for featVec in dataSet: #the the number of unique elements and their occurance
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  以2为底求对数  香农熵 统计所有标签发生的次数
return shannonEnt
'''
按照给定特征划分数据集
程序清单3-2的代码使用了三个输人参数:待划分的数据集、划分数据集的特征、特征的返
回值。
需要注意的是,Python语言不用考虑内存分配问题。Python语言在函数中传递的是列表的
引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良
影响,我们需要在函数的开始声明一个新列表对象。因为该函数代码在同一数据集上被调用多次,
为了不修改原始数据集,创建一个新的列表对象.。数据集这个列表中的各个元素也是列表,我
们要遍历数据集中的每个元素,一旦发现符合要求的值,则将其添加到新创建的列表中。在if
语句中,程序将符合特征的数据抽取出来O。后面讲述得更简单,这里我们可以这样理解这段代
码:当我们按照某个特征划分数据集时,就需要将所有符合要求的元素抽取出来。代码中使用了
Python语言列表类型自带的extend()和append()方法。这两个方法功能类似,但是在处理多个
列表时,这两个方法的处理结果是完全不同的。

append和extend的区别
假定存在两个列表,a和b:
如果执行a.append(b),则列表得到了第四个元素,而且第四个元素也是一个列表。然而
如果使用extend方法:
则得到一个包含a和b所有元素的列表。

'''
def splitDataSet(dataSet, axis, value):   #输入的参数 带划分的数据集 划分数据集的特征,特征的返回值
retDataSet = []   #创建一个新的列表对象
for featVec in dataSet:   #遍历数据集中的每个元素,一旦发现符合要求的值,将其添加到新建列表中
if featVec[axis] == value:  #抽取符合特征的数据
reducedFeatVec = featVec[:axis]     #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
'''
选择最好的数据集划分方式

程序清单3-3给出了函数chooseBestFeatureToSplit()的完整代码,

该函数实现选取特征,划分数据集,计算得出最好的划分数据集的特征。

函数chooseBestFea七ureToSplit()使
用了程序清单3-1和程序清单3-2中的函数。

在函数中调用的数据需要满足一定的要求:第一个要
求是,数据必须是一种由列表元素组成的列表,而且所有的列表元素都要具有相同的数据长度;
第二个要求是,数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。数据集一
旦满足上述要求,我们就可以在函数的第一行判定当前数据集包含多少特征属性。

我们无需限定
list中的数据类型,它们既可以是数字也可以是字符串,并不影响实际计算。
在开始划分数据集之前,程序清单3-3的第3行代码计算了整个数据集的原始香农嫡,我们保
存最初的无序度量值,.用于与划分完之后的数据集计算的嫡值进行比较。第1个for循环遍历数
据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,将数据集中所有第i
个特征值或者所有可能存在的值写人这个新list中0。然后使用Python语言原生的集合(set )数据
类型。集合数据类型与列表类型相似,不同之处仅在于集合类型中的每个值互不相同。从列表中
创建集合是Python语言得到列表中唯一元素值的最快方法。
遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集O,然后计算数据集的新嫡
值,并对所有唯一特征值得到的嫡求和。信息增益是嫡的减少或者是数据无序度的减少,大家肯
定对于将嫡用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最
好特征划分的索引值)o

'''
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  遍历数据集中的所有特征,使用列表推导(list comprehension)来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中
featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
uniqueVals = set(featList)       #get a set of unique values  使用集合(set)数据类型
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     #calculate the info gain; ie reduction in entropy
if (infoGain > bestInfoGain):       #compare this to the best gain so far  比较所有特征中的信息增益,返回最好特征划分的索引值
bestInfoGain = infoGain         #if better than current best, set to best
bestFeature = i
return bestFeature                      #returns an integer
'''
递归构建决策树

从数据集构造决策树算法原理如下:
得到原始数据集,然后给予最好的属性值划分数据集,由于特征值可能多于2个,因此可能存在大于
两个分支的数据集划分。第一次划分之后,书记将被向下传递到树分支的下一个节点,在这个节点上,
可以再次划分数据,采用递归的原则处理数据集

递归结束的条件是:
程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类
如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点
的数据必然属于叶子节点的分类

第一个结束条件使得算法可以终止,我们甚至可以设置算法可以划分的最大分组数目。后续
章节还会介绍其他决策树算法,如C4.5和CART,这些算法在运行时并不总是在每次划分分组时
都会消耗特征。由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时
可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,
查看算法是否使用了所有属性即可。如果数据集已经处理了所有属性,但是类标签依然不是唯一
的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决
定该叶子节点的分类。

'''
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]
'''
程序清单3-4的代码使用两个输人参数:数据集和标签列表。标签列表包含了数据集中所有
特征的标签,算法本身并不需要这个变量,但是为了给出数据明确的含义,我们将它作为一个输
人参数提供。此外,前面提到的对数据集的要求这里依然需要满足。上述代码首先创建了名为
classList的列表变量,其中包含了数据集的所有类标签。递归函数的第一个停止条件是所有的
类标签完全相同,则直接返回该类标签O。递归函数的第二个停止条件是使用完了所有特征,仍
然不能将数据集划分成仅包含唯一类别的分组.。由于第二个条件无法简单地返回唯一的类标
签,这里使用程序清单3-3的函数挑选出现次数最多的类别作为返回值。
下一步程序开始创建树,这里使用Python语言的字典类型存储树的信息,当然也可以声明特
殊的数据类型存储树,但是这里完全没有必要。字典变量myTree存储了树的所有信息,这对于
其后绘制树形图非常重要。当前数据集选取的最好特征存储在变量bestFeat中,得到列表包含
的所有属性值O。这部分代码与程序清单3-3中的部分代码类似,这里就不再进一步解释了。
最后代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数
crea七eTree(),得到的返回值将被插人到字典变量myTree中,因此函数终止执行时,字典中将
会嵌套很多代表叶子节点信息的字典数据。在解释这个嵌套数据之前,我们先看一下循环的第一行
subLabels=labels[:],这行代码复制了类标签,并将其存储在新列表变量subLabel。中。之
所以这样做,是因为在Python语言中函数参数是列表类型时,参数是按照引用方式传递的。为了保
证每次调用函数createTree()时不改变原始列表的内容,使用新变量subLabels代替原始列表。

>>> myTree = trees.createTree(myDat,labels)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

返回值myTree的结果:
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'maybe'}}}}

从左边开始,第一个关键字nosurfacing是第一个划分数据集的特征名称,该关键字的值
也是另一个数据字典。第二个关键字是no surficing特征划分的数据集,这些关键字的值
是no surfacing节点的子节点。这些值可能是类标签,也可能是另一个数据字典。
如果值是类标签,则该子节点是叶子节点,如果值是另一个数据字典,则子节点是一个判断
节点,这种格式结构不断重复就构成了整棵树

'''

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: #stop splitting when there are no more features in dataSet  递归函数终止的第二个条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组
return majorityCnt(classList)  #遍历完所有特征时返回出现次数最多的
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}  #mytree存储了树的所有信息
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]  #当前数据集选取的最好特征存储在变量bestfeat中,得到列表包含的所有属性值
uniqueVals = set(featValues)
for value in uniqueVals:  #代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createtree(),得到的返回值将被插入到字典变量mytree中
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
'''
使用决策树执行分类
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,
需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行
该过程直到进人叶子节点;最后将测试数据定义为叶子节点所属的类型。

与图3-6比较上述输出结果。第一节点名为no surfacing,它有两个子节点:一个是名字为。的
叶子节点,类标签为no;另一个是名为flippers的判断节点,此处进入递归调用,flippers节点有两
个子节点。以前绘制的树形图和此处代表树的数据结构完全相同。

使用决策树的分类函数
递归函数
递归遍历整棵树,比较testVec变量中的值与树节点的值,如果到达叶
子节点,则返回当前节点的分类标签。
'''
def classify(inputTree,featLabels,testVec):
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)  #将标签字符串转换成索引  查找当前列表中第一个匹配firstStr变量的元素
key = testVec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
'''
决策树的存储

构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的
时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以
很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。为
了解决这个问题,需要使用Python模块pickle序列化对象,参见程序清单3-9。

示例:使用决策树预测隐形眼镜类型
(1)收集数据:提供的文本文件。
(2)准备数据:解析tab键分隔的数据行。
(3)分析数据:快速检查数据,确保正确地解析数据内容,使用createPlot()函数绘制
最终的树形图。
(4)训练算法:使用3.1节的createTree()函数。
(5)测试算法:编写测试函数验证决策树可以正确分类给定的数据实例。
(6)使用算法:存储树的数据结构,以便下次使用时无需重新构造树。

使用pickle模块存储决策树
序列化对象可以在磁
盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。

>>> fr = open('lenses.txt')
>>> lenses = [inst.strip().split('\t') for inst in fr.readlines()]
>>> lensesLabels = ['age','prescript','astigmatic','tearRate']
>>> lensesTree = trees.createTree(lenses,lensesLabels)
>>> lensesTree
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses', 'presbyopic': 'no lenses', 'young': 'hard'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft'}}}}}}
>>> treePlotter.createPlot(lensesTree)
'''
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)


实现作图源码如下:

#coding=utf-8

'''
Created on Oct 14, 2010

@author: Peter Harrington
'''
import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")  #定义文本框的箭头格式  树节点格式的常量
'''
构造注解树
获取叶节点的数目和输的层数

上述程序中的两个函数具有相同的结构,后面我们也将使用到这两个函数。这里使用的数据
结构说明了如何在Python字典类型中存储树信息。第一个关键字是第一次划分数据集的类别标
签,附带的数值表示子节点的取值。从第一个关键字出发,我们可以遍历整棵树的所有子节点。
使用Python提供的type()函数可以判断子节点是否为字典类型0。如果子节点是字典类型,则
该节点也是一个判断节点,需要递归调用getNumLeafs()函数。getNumLeafs()函数遍历整棵
树,累计叶子节点的个数,并返回该数值。第2个函数getTreeDep七川)计算遍历过程中遇到判
断节点的个数。该函数的终止条件是叶子节点,一旦到达叶子节点,则从递归调用中返回,并将
计算树深度的变量加一。为了节省大家的时间,函数retrieveTree输出预先存储的树信息,避
免了每次测试代码时都要从数据中创建树的麻烦。

'''
#累积叶子节点个数 并返回该数值
def getNumLeafs(myTree):
numLeafs = 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 测试节点的数据类型是否为字典
numLeafs += getNumLeafs(secondDict[key])
else:   numLeafs +=1
return numLeafs

#计算遍历过程中遇到判断节点的个数  该函数的终止条件是叶子节点,一旦到达叶子节点,则从递归调用中返回,并将计算书深度的变量+1
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',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args )
'''
函数createPlot()是我们使用的主函数,它调用了plotTree(),函数plotTree又依次调
用了前面介绍的函数和plotMidText()。绘制树形图的很多工作都是在函数plotTree()·中完成
的,函数plotTree()首先计算树的宽和高O。全局变量plotTree.totalW存储树的宽度,全
局变量plotTree.totalD存储树的深度,我们使用这两个变量计算树节点的摆放位置,这样可
以将树绘制在水平方向和垂直方向的中心位置。与程序清单3-6中的函数getNumLeafs()和
getTreeDepth()类似,函数plotTree()也是个递归函数。树的宽度用于计算放置判断节点的
位置,主要的计算原则是将它放在所有叶子节点的中间,而不仅仅是它子节点的中间。同时我们
使用两个全局变量plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置,以及放置下一
个节点的恰当位置。另一个需要说明的问题是,绘制图形的x轴有效范围是0.0到1.0, y轴有效范
围也是0.0一1.0。为了方便起见,图3-6给出具体坐标值,实际输出的图形中并没有x, y坐标。通
过计算树包含的所有叶子节点数,划分图形的宽度,从而计算得到当前节点的中心位置,也就是
说,我们按照叶子节点的数目将x轴划分为若干部分。按照图形比例绘制树形图的最大好处是无
需关心实际输出图形的大小,一旦图形大小发生了变化,函数会自动按照图形大小重新绘制。如
果以像素为单位绘制图形,则缩放图形就不是一件简单的工作。

接着,绘出子节点具有的特征值,或者沿此分支向下的数据实例必须具有的特征值O。使用
函数p1。七MidText()计算父节点和子节点的中间位置,并在此处添加简单的文本标签信息Oo
然后,按比例减少全局变量plotTree.yOff,并标注此处将要}}J子节点O,这些节点即可以
是叶子节点也可以是判断节点,此处需要只保存绘制图形的轨迹。因为我们是自顶向下绘制图形,
因此需要依次递减Y坐标值,而不是递增Y坐标值。然后程序采用函数getNumLeafs()和
getTreeDepth()以相同的方式递归遍历整棵树,如果节点是叶子节点则在图形上画出叶子节点,
如果不是叶子节点则递归调用plotTree()函数。在绘制了所有子节点之后,增加全局变量Y的偏移。

'''
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)
'''
递归函数 树的宽度用于计算放置计算树几点的位置,主要的计算原则是将它放在所有叶子节点的中间
而不仅仅是他子节点的中间,使用plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置
以及放置下一节点的恰当位置

按照子节点的数目将x轴划分为若干部分,。按照图形比例绘制树形图的最大好处是无序关心
实际输出图形的大小,一旦图形大小发生改变函数会自动按照图形大小重新绘制,如果图形
以像素为单位绘制图形,则缩放图形就不是一件简单的事情

因为是自顶向下绘制图形,因此需要一次递减y坐标值,而不是递增y坐标值。然后程序采用函数
getNumLeafs  getTreeDepth以相同的方式递归遍历整棵树,如果是叶子节点则在图形上画出叶子节点
如果不是叶子节点则递归调用plotTree()
'''
def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree  计算宽与高
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  #减少Y偏移  标注此处要绘制节点
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))  #计算树的宽和高  将树绘制在水平方向和垂直方向的中心位置
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) #ticks for demo puropses
#    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()
'''
>>> myTree = treePlotter.retrieveTree(0)
>>> treePlotter.getNumLeafs(myTree)
3
>>> treePlotter.getTreeDepth(myTree)
2
函数retrieveTree()主要用于测试,返回预定义的树结构。上述命令中调用getNumLeafs()
函数返回值为3,等于树。的叶子节点数;调用getTreeDepths()函数也能够正确返回树的层数。

'''
def retrieveTree(i):
listOfTrees =[{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
]
return listOfTrees[i]

#createPlot(thisTree)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: