本文代码均来自《机器学习实战》
'''
Created on Oct 12, 2010
Decision Tree Source Code for Machine Learning in Action Ch. 3
@author: Peter Harrington
'''
import pdb
from math import log
import operator
#书中源代码和目前机器学习主流的表示方法是有出入的!原文中的代码中的labels指的其实是feature的名称,这个不应该和labels混为一谈。一开始没看懂这个,理解源代码的时候绕了很大的弯路
def createDataSet():#生成测试集
#这种方法对于小规模样本挺好的,省的每次都要输入测试集或者读文件,直接将数据写在程序中当成函数就可以了
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
features = ['no surfacing','flippers']
#change to discrete values
return dataSet,features
def calcShannonEnt(dataSet):#计算信息熵
#计算公式是H=-∑P(xi)log2(P(xi)),i=1:n,其中P(Xi)是选择该分类的概率
numEntries = len(dataSet)
featuresCounts = {}
for featVec in dataSet: #the the number of unique elements and their occurance,说白了就是计算有几种features和这几种features对应的出现频率,概率等会算
currentfeatures = featVec[-1]
if currentfeatures not in featuresCounts.keys(): featuresCounts[currentfeatures] = 0#如果features集合中没有该features的话就加进去,并且设置它的出现频率为0
featuresCounts[currentfeatures] += 1#该features出现了
shannonEnt = 0.0#信息熵
for key in featuresCounts:
prob = float(featuresCounts[key])/numEntries#把频率算成概率
shannonEnt -= prob * log(prob,2) #log base 2
return shannonEnt
def splitDataSet(dataSet, axis, value):#划分数据集以减少熵
#axis是划分数据集的特征,axis是对应的要分割数据的列,value是要分割的列按哪个值分割,即找到含有该值的数据
#想象数据分布在一个二维平面,我们要用一条直线对这些数据进行划分,那么应该在哪个轴划线呢?又应该在哪个位置划线呢?
#这里axis就确定了在哪个维度进行划线,而value就是要在这个维度上划线的位置
#python函数传递的都是参数的引用,为了不对原有的数据集造成破坏,我们新建一个对象:
retDataSet = []#划分后的数据集集合
for featVec in dataSet:#取出数据集合中的一个样本
if featVec[axis] == value:
#如果featVec[axis] != value,根本就不放入返回的数组中
#把一个样本的特征裂开,前面的0-axis个数据,不包括axis,直接放到reducedFeatVec中
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
#将axis之后的特征平铺,消除数据的层数,都放在一个数组中
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)#这是一个处理好的样本
return retDataSet
def chooseBestFeatureToSplit(dataSet):
#这只是找一次分所需的特征,要构建一棵树需要不停地分,直至每一类中label唯一了
numFeatures = len(dataSet[0]) - 1 #the last column is used for the featuress,获取特征的数量
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) #get a set of unique values
newEntropy = 0.0
for value in uniqueVals:
#选择这个特征中的某个值分开,得到的subDataSet只包括部分数据,由于如果我们要使用这个特征分开,那么就需要彻底的分开,一个值分一个subset
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 #返回分开效果最好的特征
# 对类标签进行投票 ,找标签数目最多的标签
def majorityCnt(classList):
classCount={}
for vote in classList:
#如果这个features之前没见过,就设置value为0
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
# 返回数目最多的features
return sortedClassCount[0][0]
"""
它的输入长这个样子:
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
featuress = ['no surfacing','flippers']
"""
def createTree(dataSet,featuress):#建立决策树的主要部分
#数据的features的值是存放在dataSet中的,featuress里面是这些features的全写,dataSet里面的只是简略表示
#tm的python还有这种倒着寻址的,几乎都要忘掉了
#将dataSet的最后一列数据(标签)取出赋给classList,classList存储的是labels列
classList = [example[-1] for example in dataSet]
#这里处理的是一个特殊的case,也是算法停机性的所在:如果一个set里面分类都一样了就停机了
if classList.count(classList[0]) == len(classList): #计算是不是所有的labels都是第1个那种
return classList[0]#stop splitting when all of the classes are equal,返回了一个labels
#这里是另外一个特殊case,就是set里面只有一个样本了
if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatfeatures = featuress[bestFeat]#选出要分开的features的全称
# 定义决策树,key为bestFeatfeatures,value为空
#树结构是利用dict完成的,注意key对应的value也是个dict,所以这里其实是个二维dict
myTree = {bestFeatfeatures:{}}
del(featuress[bestFeat])#删除featuress[bestFeat]对应的元素,这种情况在树根已经被解决了
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)#取出dataSet中所有数据中要分类的feature的所有值
for value in uniqueVals:
subfeaturess = featuress[:] #copy all of featuress, so trees don't mess up existing featuress
#妙啊,这嵌套的dict用起来太妙了
myTree[bestFeatfeatures][value] = createTree(splitDataSet(dataSet, bestFeat, value),subfeaturess)
return myTree #最后生成的树是dict的嵌套,最后可以像上面一样用连续的下标进行访问
#生成基于决策树的分类函数
#使用步骤:先调用createTree生成树,再调用该方法进行classify
def classify(inputTree,featfeaturess,testVec):
#featfeaturess是树中所用的features的名称的集合,testVec是测试集
# 得到树中的第一个特征
firstStr = list(inputTree.keys())[0]
# 得到第一个对应的值或者子dict,也就是下一层
secondDict = inputTree[firstStr]
# 得到树中第一个特征对应的索引,也就是在data中是第几维的
featIndex = featfeaturess.index(firstStr)
for key in secondDict.keys():#遍历树的各个分支
# 如果在secondDict[key]中找到testVec[featIndex]
if testVec[featIndex] == key:#如果一致,就从这个入口进去
# 判断secondDict[key]是否为字典
if type(secondDict[key]).__name__ == 'dict':
# 若为字典,递归的寻找testVec
classfeatures = classify(secondDict[key], featfeaturess, testVec)
else:
# 若secondDict[key]为标签值,则将secondDict[key]对应的value,也就是features赋给classfeatures
classfeatures = secondDict[key]
return classfeatures
#将树进行序列化保存
"""
序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。
在序列化期间,对象将其当前状态写入到临时或持久性存储区。
以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
"""
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)
#下面是使用决策树进行分类的一个例子
dataSet,featuress=createDataSet()
featuressSave=featuress[:]#建立树时会对features有修改,所以要额外保存一份
#注意直接写featuressSave=featuress是不行的,这样子传递的只是引用,必须加上[:]才是真正的拷贝
thisTree=createTree(dataSet,featuress)
classify(thisTree,featuressSave,[1,0])
#这里要是输入classify(thisTree,featuressSave,[2,0])就会报错,因为我们的树建立起来的时候对于第一个feature的分类只有0和1,不存在2