机器学习-决策树

本文代码均来自《机器学习实战》

'''
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
原文地址:https://www.cnblogs.com/jiading/p/11624072.html