决策树算法

目录索引


1.背景知识

在咱们谈论决策树的时候咱们先来玩一个游戏好咯。
2016年是奥运年,我最喜欢的两个运动员,(心里戏:固然是女的咯。由于我也是妹子,哈哈哈。)一个固然是女王隆达罗西,还有一个就是伊辛巴耶娃咯。python

好的,如今咱们就来玩猜运动员的游戏。linux

我在内心想一个运动员的名字,好比说就是伊辛巴耶娃。而后你有20次的提问机会,可是我只能回答你是仍是不是这两种可能。web

咱们能够这样对话:
你:男的?
我:不是
你:参加过往届奥运会?
我:是
你:参加过两次?
我:不是
你:参加过三次?
我:是
你:参加的是田赛
我:是
你:耶辛巴伊娃
我:恭喜你,答对了!算法

以上咱们玩的这个过程就有一点点像决策树算法。shell

咱们常用决策树处理分类问题,近年来,决策树也是常用的数据挖掘的算法。数据结构

决策树的概念是很是简单的,咱们能够经过一个图形来快速的了解决策数。我用了《机器学习与实战》这本书的内容来说解。如图1所示,下面的这个流程图就是一个决策树,正方形表明的是判断模块(decision block),椭圆形表明的是终止模块(terminating block),表示已经得出结论,能够终止运行,从判断模块引出的左右箭头称做分支(branch)app

这里写图片描述

这是一个假想的邮件分类系统。首先这个系统会检测发送邮件的域名地址,若是地址为myEmployer.com 则将邮件归类到“无聊时须要阅读的邮件”若是没有这个域名咱们就检查邮件中的内容是否是包含了“曲棍球”的邮件。若是包含则把这些邮件放置在“须要及时处理的朋友邮件”,不然就把这些邮件归类到“无需阅读的垃圾邮件”dom

2.构造决策树

根据上面的描述咱们已经发现构造决策树作分类的时候首要目的就是每次分类的时候都能找到最容易区分一个集合和另外一个集合的特征。在上面例子中,咱们首先就是查找邮件的域名,在第一次分类的时候,邮件的域名就是咱们最重要的分类特征。
为了找到决定性的特征,划分出最好的结果,咱们必须评估每一个特征,完成测试以后,原始数据集就被划分为几个数据子集。根据咱们挑选出的最佳特征,这些数据会被分红两类。咱们分别检测这两类,若是类别相同则不须要再次划分,若是类别不一样,咱们要重复上面的步骤。就是在被划分出的子集当中在挑选其余的重要特征把这些数据在细分红其余的类别。
根据这个描述,咱们能够很容易发现这个过程就是一个递归的过程,怎么找到这些最佳特征,咱们要作的事情就须要了解一些数学概念。咱们须要用到信息论的知识来划分数据集。机器学习

3.一些须要了解的数学概念

划分数据集的原则就是:将无序的数据变得更加有序。咱们能够有多种方法来划分数据,在这里咱们构建决策树算法使用的是信息论划分数据集,而后编写代码将理论应用到具体的数据集上,而后编写代码构建决策树。咱们在组织杂乱无章的数据时使用信息论度量信息。svg

好比我给出一条信息:

我爱你1314.

这就是一条简单的信息,这个时候咱们能够对这个信息作一些分类,好比说找出这句话中的动词,数字,以及代词,名词等。咱们要知道的就是 信息处理就是将杂乱无章的信息用数理统计的方法表示出来。

在这里咱们能够将这个信息分红 三类: 代词(名词),动词,以及数字,咱们用X1表示代词,X2表示动词,X3表示数字,p(xi)表示的是这个分类在这个信息当中出现的几率。那么咱们就能够将信息定义为:
这里写图片描述

高中毕业的人都知道吧,几率p(Xi)是一个分数,而后对数函数以2为基底,它是比1大的,若是幂是分数,基底是大于1的那个这个值是个负数。为了方便处理,前面添加负号。

信息熵,简称熵是用来表示信息的指望值得。

3.1 信息熵

根据百科词条的定义,咱们先来看一下信息论中的一下基本概念

信息论:
信息论是运用几率论与数理统计的方法研究信息、信息熵、通讯系统、数据传输、密码学、数据压缩等问题的应用数学学科。信息系统就是广义的通讯系统,泛指某种信息从一处传送到另外一处所需的所有设备所构成的系统。

1948年,香农提出了“信息熵”的概念,解决了对信息的量化度量问题。信息熵这个词是C.E.香农从热力学中借用过来的。热力学中的热熵是表示分子状态混乱程度的物理量。香农用信息熵的概念来描述信源的不肯定度。

咱们能够用信息熵来度量信息量的多少。
在给出信息熵的计算公式的时候,我想先说几个基本的概念,以便于你理解信息熵的计算公式。



3.2随机变量

咱们能够先看下面的一些问题。
某人射击一次,可能出现命中0环,命中1环…,命中10环等结果。便可能出现的结果能够由0,1,2,3,4,5,6,7,8,9,10这11个数来表示。

在某次的产品检验中,在可能含有次品的100件产品中任意抽取4件来检验,那么其中含有的次品可能的是0件,1件,2件,3件,4件,便可能出现的结果能够由0,1,2,3,4这5个数来表示。

咱们把上面这些事件称为随机实验,随机实验想要获得的结果(例如射击一次命中的环数)能够用一个变量来表示的话,那么这样的变量就叫作随机变量(random variable)

随机变量的一些特征:
1. 可用数表示
2. 实验前可判断出全部可能取值
3. 实验前不能判断具体取哪一个值
4. 全部可能值按照某种顺序列出

离散型随机变量
随机变量的取值是能够一一列出的好比上面所说的射击事件

连续型随机变量
那就是取值不能一一列出的咯,好比说一天内气温的变化量

推广:
通常地,若是X是随机变量,如有Y=f(X),则Y也是随机变量

3.3数学指望

在几率论中,数学指望简称指望,通俗的说就是平均值,它表示的是随机变量的取值的平均水平。

计算的公式
X1,X2,X3,……,Xn为这离散型随机变量,p(X1),p(X2),p(X3),……p(Xn)为这几个数据的几率函数。在随机出现的几个数据中p(X1),p(X2),p(X3),……p(Xn)几率函数就理解为数据X1,X2,X3,……,Xn出现的频率f(Xi).则:
E(X) = X1*p(X1) + X2*p(X2) + …… + Xn*p(Xn) = X1*f1(X1) + X2*f2(X2) + …… + Xn*fn(Xn)

上面的这个看着有点恶心,咱们来温故一下当年高中数学课本中的东东,分分钟暴露了年龄的数学课本啊,可是仍是很喜欢
(在这里咱们主要考虑离散型随机变量)

这里写图片描述

总而言之,数学指望就是随机变量的取值乘以在随机实验中这个随机变量取到的几率。
推广一下

这里写图片描述

这里写图片描述
下面来举个例子

这里写图片描述

若是你已经理解了数学指望,随机变量这些概念那么咱们就来讲说信息熵的计算。

都说了熵是表示信息的指望值,信息的指望值,信息的指望值,若是您已经看懂了数学指望怎么算,那么你应该会很容易理解信息熵会怎么计算。

仍是刚才那个例子,咱们给出了一个信息:我爱你1314,而后把这个信息分为三类,而后咱们要计算这个信息的熵。

那是否是就要计算这个信息全部类别的可能值得数学指望了。
那么熵的公式就是下面这个样子的:

这里写图片描述

其中n表示的是这个信息被分为n类。


4.决策树构建的通常流程

  • 收集数据:任何你能收集数据的方法
  • 准备数据: 决策树的算法只适用于标称型数据(可理解为离散型的,不连续的),所以数值型的数据(连续的数据)必须离散化。
  • 分析数据: 可使用任何方法,构造树完成以后,咱们要检查图形是否符合预期。
  • 训练算法:构造决策树的数据结构。
  • 测试算法: 使用经验树计算错误率。
  • 使用算法: 此步骤能够适用于任何监督学习算法,而使用决策数能够更好的理解数据的内在含义 (why? 对比于其余算法,好比说k均值算法,就是把给定的数据按照类似度分为一类,每一类表示什么你可能就不知道了。就像咱们上一章讲的那个例子,能够用决策树作邮件的分类系统,咱们能够根据分类标签知道这个邮件是垃圾邮件仍是须要马上处理的邮件)

5. 数据的构建

咱们使用的例子仍是《机器学习与实战》那本书上的例子。我把写做的思路和流程改了一下,还有这本书里好多错误,我好想帮做者重写这本书,或许不是做者的错误,是翻译和排版的错误。

首先咱们第一步仍是收集数据:
海洋生物数据

在这张表中咱们能够发现这里有5个数据,这里有两个特征(要不要浮出水面生存,和是否有脚蹼)来划分这5个生物是鱼类仍是非鱼类。
如今咱们要作的就是是要根据第一个特征仍是第二个特征来划分数据,进行分类。

咱们使用python来构建咱们的代码。


咱们建立一个名为trees.py的python文件,而后在下面输入如下的代码

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']] # 咱们定义了一个list来表示咱们的数据集,这里的数据对应的是上表中的数据

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

    return dataSet, labels

其中第一列的1表示的是不须要浮出水面就能够生存的,0则表示相反。 第二列一样是1表示有脚蹼,0表示的是没有。

这个时候咱们来测试如下咱们的数据集。
我用的是linux系统,咱们打开一个终端来测试如下咱们的数据。
咱们建立完这个文件以后,进入到这个文件的目录下。我把这个文件保存在~/code 这个路径下。

这里写图片描述

咱们输入python,进入shell命令,以下图所示

这里写图片描述

代码以下:

>>> import trees
>>> reload(trees)
<module 'trees' from 'trees.pyc'>
>>> myDat,labels=trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> 

咱们来讲说这段代码:
import的做用:
导入/引入一个python标准模块,其中包括.py文件、带有init.py文件的目录。

例如:

import module_name[,module1,...]  
from module_name import *|child[,child1,...]

注意:
屡次重复使用import语句时,不会从新加载被指定的模块,只是把对该模块的内存地址给引用到本地变量环境。
也就是说使用import的时候引用的module只会被加载一次,只会被加载一次,系统会把这个模块的地址给引用它的代码或者是这个python文件。

咱们来测试如下

构建两个文件 a.py 和 b.py
其中a.py的代码以下:

#!/usr/bin/env python
# coding=utf-8
import os

print 'in a.py file'
print 'The address of os is:', id(os)

print '***end***'

咱们在b.py中写以下代码:

#!/usr/bin/env python
# coding=utf-8
#filename: b.py

import a
import os

print "*****************"
print 'in b file'
print 'The adress of b file is:',id(os)

import a
print 'The adress of a module is:',id(a)

这个时候,咱们来测试如下结果:

这里写图片描述

咱们在a,b两个文件中都引入了os 模块可是咱们发现它的地址都没有改变。

reload

reload 的目的是为了开发期的 “edit and debug”/即编即调

reload 的做用:
对已经加载的模块进行从新加载,通常用于原模块有变化等特殊状况,reload前该模块必须已经import过。
e.g:

import os
reload(os)

说明:
reload会从新加载已加载的模块,但原来已经使用的实例仍是会使用旧的模块,而新生产的实例会使用新的模块;reload后仍是用原来的内存地址;不能支持from。。import。。格式的模块进行从新加载。

咱们在举一个例子:

建立两个文件c.py, 以及文件 d.py

#!/usr/bin/env python
# coding=utf-8
# filename : c.py
import os

print 'in c.py file'
print 'The address of os is:', id(os)

print '***end***'

这个时候咱们用d.py这个文件去引用c这个模块

#!/usr/bin/env python
# coding=utf-8
#filename: d.py

import c
import os

print "*****************"
print 'in d file'
print 'The adress of os is:',id(os)

print 'The address of c file is:',id(c)

print '*****reload******'
reload(c) 

print '****reload*****'

print 'The adress of c module is:',id(c)

接下来咱们来测试一下结果:
这里写图片描述

能够发现reload和import的区别就是一个只能加载模块屡次,一个能够加载一次

总之,咱们的数据集就这么愉快的建立好了。


6.计算给定数据的信息熵

在决策树算法中最重要的目的咱们已经在前几章说过了,就是根据信息论的方法找到最合适的特征来划分数据集。在这里,咱们首先要计算全部类别的全部可能值的香农熵,根据香农熵来咱们按照取最大信息增益(information gain)的方法划分咱们的数据集。


咱们的数据集以下表所示:
这里写图片描述

根据这张表,咱们使用python来构建咱们的数据集。

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']] # 咱们定义了一个list来表示咱们的数据集,这里的数据对应的是上表中的数据

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

    return dataSet, labels

其中第一列的1表示的是不须要浮出水面就能够生存的,0则表示相反。 第二列一样是1表示有脚蹼,0表示的是没有。

在构建完数据集以后咱们须要计算数据集的香农熵。
根据香农熵的定义能够知道:

这里写图片描述

根据这个公式咱们来编写相应的代码。(注意:咱们是计算每一个类别的香农熵,也就是鱼类仍是非鱼类的香农熵。在这咱们的数据集当中咱们用’yes’表示是鱼类,用‘no’表示非鱼类)

# 代码功能:计算香农熵
from math import log #咱们要用到对数函数,因此咱们须要引入math模块中定义好的log函数(对数函数)

def calcShannonEnt(dataSet):#传入数据集
# 在这里dataSet是一个链表形式的的数据集
    countDataSet = len(dataSet) # 咱们计算出这个数据集中的数据个数,在这里咱们的值是5个数据集
    labelCounts={} # 构建字典,用键值对的关系咱们表示出 咱们数据集中的类别还有对应的关系
    for featVec in dataSet: 经过for循环,咱们每次取出一个数据集,如featVec=[1,1,'yes']
        currentLabel=featVec[-1] # 取出最后一列 也就是类别的那一类,好比说‘yes’或者是‘no’
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    print labelCounts # 最后获得的结果是 {'yes': 2, 'no': 3}


    shannonEnt = 0.0 # 计算香农熵, 根据公式

    for key in labelCounts:
        prob = float(labelCounts[key])/countDataSet
        shannonEnt -= prob * log(prob,2)


    return shannonEnt

在python中咱们使用 a=[]来定义一格list,咱们使用a={}来定义一个字典。
例如:
website = {1:”google”,”second”:”baidu”,3:”facebook”,”twitter”:4}

>>>#用d.keys()的方法获得dict的全部键,结果是list
>>> website.keys()
[1, 'second', 3, 'twitter']

注意是d.keys()

>>>#用d.values()的方法获得dict的全部值,若是里面没有嵌套别的dict,结果是list
>>> website.values()
['google', 'baidu', 'facebook', 4]

>>>#用items()的方法获得了一组一组的键值对,
>>>#结果是list,只不过list里面的元素是元组
>>> website.items()
[(1, 'google'), ('second', 'baidu'), (3, 'facebook'), ('twitter', 4)]

接下来咱们来测试一下,经过 reload来测试

>>> import trees
>>> reload(trees)
<module 'trees' from 'trees.pyc'>
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> trees.calcShannonEnt(myDat)
{'yes': 2, 'no': 3}
0.9709505944546686
>>> 

这里写图片描述

咱们贴出实验的完整代码:

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]


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

    return dataSet, labels


def calcShannonEnt(dataSet):
    countDataSet = len(dataSet)
    labelCounts={}
    for featVec in dataSet:
        currentLabel=featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    print labelCounts

    shannonEnt = 0.0

    for key in labelCounts:
        prob = float(labelCounts[key])/countDataSet
        shannonEnt -= prob * log(prob,2)


    return shannonEnt

咱们再把计算的过程总结一下,方便你们理解:

1.计算数据集中实例的老是,也就是样本的总数。咱们把这个值保存成一格单独的变量以便以后方便使用,提升代码的效率
2.建立字典,用于保存类别信息。在整个数据集当中有多少个类别,每一个类别的个数是多少
3. 在咱们建立的数据字典中,它的键是咱们数据集中最后一列的值。若是当前键不存在则把这个键加入到字典当中,依次统计出现类别的次数
4. 最后使用全部类标签对应的次数来计算它们的概论
5.计算香农熵


香农熵越高,则说明混合的数据越多,咱们能够在数据集当中添加更多的分类,来观察一下熵是怎么变化的。

>>> myDat[0][-1]='maybe'
>>> myDat
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(myDat)
{'maybe': 1, 'yes': 1, 'no': 3}
1.3709505944546687
>>> 

对比一下能够发现熵增长了:
这里写图片描述

这里写图片描述

获得熵以后咱们就能够按照获取最大信息增益的方法划分数据集。

7.划分数据集

7.1 基本概念

在度量数据集的无序程度的时候,分类算法除了须要测量信息熵,还须要划分数据集,度量花费数据集的熵,以便判断当前是否正确的划分了数据集。
咱们将对每一个特征数据集划分的结果计算一次信息熵,而后判断按照那个特征划分数据集是最好的划分方式。
也就是说,咱们依次选取咱们数据集当中的全部特征做为咱们划定的特征,而后计算选取该特征时的信息增益,当信息增益最大时咱们就选取对应信息增益最大的特征做为咱们分类的最佳特征。

下面是咱们的数据集:
这里写图片描述

咱们用python语言表示出这个数据集
dataSet= [[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’], [0, 1, ‘no’], [0, 1, ‘no’]]
在这个数据集当中有两个特征,就是每一个样本的第一列和第二列,最后一列是它们所属的分类。

咱们划分数据集是为了计算根据那个特征咱们能够获得最大的信息增益,那么根据这个特征来划分数据就是最好的分类方法。

所以咱们须要遍历每个特征,而后计算按照这种划分方式得出的信息增益。信息增益是指数据集在划分数据先后信息的变化量。


7.2 具体操做

划分数据集的方式咱们首先选取第一个特征的第一个可能取值来筛选信息。而后再选取第一个特征的第二个可能的取值来划分咱们的信息。以后咱们再选取第二个特征的第一个可能的取值来划分数据集,以此类推。

e.g:
[[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’], [0, 1, ‘no’], [0, 1, ‘no’]]
这个是咱们的数据集。
若是咱们选取第一个特征值也就是需不须要浮到水面上才能生存来划分咱们的数据,这里生物有两种可能,1就是须要,0就是不须要。那么第一个特征的取值就是两种。

若是咱们按照第一个特征的第一个可能的取值来划分数据也就是当全部的样本的第一列取1的时候知足的样本,那就是以下三个:
[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’]
能够理解为这个特征为一条分界线,咱们选取完这个特征以后这个特征就要从咱们数据集中剔除,由于要把他理解为分界线。那么划分好的数据就是:

[[1, ‘yes’], [1, ‘yes’], [0, ‘no’]]

若是咱们以第一个特征的第二个取值来划分数据集,那么获得的数据子集就是下面这个样子:

[[1, ‘no’], [1, ‘no’]]


所以咱们能够很容易的来构建出咱们的代码:

这里写图片描述

下面咱们来分析一下这段代码,

# 代码功能:划分数据集
def splitDataSet(dataSet,axis,value): #传入三个参数第一个参数是咱们的数据集,是一个链表形式的数据集;第二个参数是咱们的要依据某个特征来划分数据集
    retDataSet = [] #因为参数的链表dataSet咱们拿到的是它的地址,也就是引用,直接在链表上操做会改变它的数值,因此咱们新建一格链表来作操做

    for featVec in dataSet:
        if featVec[axis] == value: #若是某个特征和咱们指定的特征值相等
        #除去这个特征而后建立一个子特征
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            #将知足条件的样本而且通过切割后的样本都加入到咱们新创建的样本中
            retDataSet.append(reduceFeatVec)

    return retDataSet

总的来讲,这段代码的功能就是按照某个特征的取值来划分数据集。

为方便您测试实验咱们在贴出这段代码:

def splitDataSet(dataSet,axis,value):
    retDataSet = []

    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)

    return retDataSet

在这里咱们能够注意到一个关于链表的操做:
那就是extend 和append
它们的用法和区别以下所示:
这里写图片描述

这里写图片描述


下面咱们再来测试一下咱们的数据:

先给出实验的完整代码

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]


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

    return dataSet, labels


def calcShannonEnt(dataSet):
    countDataSet = len(dataSet)
    labelCounts={}
    for featVec in dataSet:
        currentLabel=featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    print labelCounts

    shannonEnt = 0.0

    for key in labelCounts:
        prob = float(labelCounts[key])/countDataSet
        shannonEnt -= prob * log(prob,2)


    return shannonEnt


def splitDataSet(dataSet,axis,value):
    retDataSet = []

    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)

    return retDataSet

咱们输入下面代码测试一下,你能够在linux系统的终端中输入python或者是ipython 来进行测试:
这里写图片描述
这里写图片描述

In [1]: import trees

In [2]: reload(trees)
Out[2]: <module 'trees' from 'trees.pyc'>

In [3]: myDat,labels=trees.createDataSet()

In [4]: myDat
Out[4]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [5]: trees.splitDataSet(myDat,0,1)
Out[5]: [[1, 'yes'], [1, 'yes'], [0, 'no']]

In [6]: trees.splitDataSet(myDat,0,0)
Out[6]: [[1, 'no'], [1, 'no']]

知道怎么划分数据集以后,咱们接下来的工做就是遍历整个样本集合的特征值,而后循环计算香农熵,找到最好的特征划分方式。


8.计算信息增益

咱们主要是要找到划分数据先后的最大信息增益,而后找到根据那个特征来作划分,分类出来的效果更好。
下面给出代码是怎么实现的
这里写图片描述

咱们来分析一下代码:

注意: 在使用这段代码的时候咱们对处理的数据是有要求的。
1.数据集必须是链表的表示出来的
2.数据集的每个样本也是链表形式
3.数据集的每个样本都必须是前面的全部列都是样本的特征值的取值范围,全部样本的最后一列是样本的类别。
4.每一个样本的列数必须相同

首先咱们的样本集合是:
这里写图片描述

dataSet = [[1,1,’yes’],
[1,1,’yes’],
[1,0,’no’],
[0,1,’no’],
[0,1,’no’]] # 咱们定义了一个list来表示咱们的数据集,这里的数据对应的是上表中的数据


咱们的目的已经很明确了,就是依次遍历每个特征,在这里咱们的特征只有两个,就是需不须要浮出水面,有没有脚蹼。而后计算出根据每个特征划分产生的数据集的熵,和初始的数据集的熵比较,咱们找出和初始数据集差距最大的。那么这个特征就是咱们划分时最合适的分类特征。

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0])-1 # 获取咱们样本集中的某一个样本的特征数(由于每个样本的特征数是相同的,至关于这个代码就是咱们能够做为分类依据的全部特征个数)咱们的样本最后一列是样本所属的类别,因此要减去类别信息,在咱们的例子中特征数就是2
    baseEntropy = calcShannonEnt(dataSet) #计算样本的初始香农熵
    bestInfoGain =0.0 #初始化最大信息增益
    bestFeature = -1 #和最佳划分特征

    for i in range(numFeatures): # range(2)那么i的取值就是0,1。 在这里i表示的咱们的第几个特征
        featList = [sample[i] for sample in dataSet]
        # 咱们首先遍历整个数据集,首先获得第一个特征值可能的取值,而后把它赋值给一个链表,咱们第一个特征值取值是[1,1,1,0,0],其实只有【1,0】两个取值
        uniqueVals = set(featList)#咱们使用集合这个数据类型删除多余重复的原始使得其中只有惟一的值。
        #执行的结果以下所示:
        ```
        In [8]: featList=[1,1,1,0,0]

        In [9]: uniqueVals=set(featList)

        In [10]: uniqueVals
        Out[10]: {0, 1}

        ```
        newEntropy = 0.0
        for value in uniqueVals: #uniqueVals中保存的是咱们某个样本的特征值的全部的取值的可能性
            subDataSet = splitDataSet(dataSet,i,value)
            # 在这里划分数据集,好比说第一个特征的第一个取值获得一个子集,第一个特征的特征又会获得另外一个特征。固然这是第二次循环
            prob = len(subDataSet)/float(len(dataSet))#咱们以第一个特征来讲明,根据第一个特征可能的取值划分出来的子集的几率
            newEntropy += prob * calcShannonEnt(subDataSet)# 这里比较难理解咱们下面在详细说明

        infoGain = baseEntropy - newEntropy # 计算出信息增益
        #找出最佳信息增益,是个学计算机的小朋友都懂吧,哨兵法
        if(infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i

    return bestFeature

被我标注的有点恶心,我在贴一遍代码,方便你们测试:

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0])-1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain =0.0
    bestFeature = -1

    for i in range(numFeatures):
        featList = [sample[i] for sample in dataSet]
        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
            bestFeature = i

    return bestFeature

难点
下面咱们来重点解释一下刚才咱们没有说的那句代码:

newEntropy += prob * calcShannonEnt(subDataSet)

为何根据这个代码就能够计算出划分子集的熵了呢?
首先咱们仍是要回顾一下计算数学指望的方法,咱们必须明确熵是信息的数学指望。

对于随机变量要计算它的数学指望咱们是这样来计算的:

X X1 X2
p p1 p2

E(x)=P1*X1 + P2 *X2
我么都以第一特征值来讲明问题
同理,咱们能够先计算出按照第一个特征值的第一个可能取值划分知足的子集的几率,和第一个特征值的第二个可能取值划分知足子集的几率。以后分别乘以它们子集计算出的香农熵。

香农熵 H1 H2
特征值划分出子集几率 p1 p2

E(H)=H1×P1+H2×P2

e.g. 第一个特征为例
这是咱们的数据集
这里写图片描述

咱们按照第一个特征的第一个取值划分获得的数据集是:

这里写图片描述

获得了两个数据集,那么占总的数据集就是2/5
咱们按照第一个特征的第二个取值划分获得的数据集是:

这里写图片描述
获得了三个数据集,那么占总的数据集就是3/5

咱们分别来计算一下它们的香农熵

这里写图片描述
这里写图片描述

××××××
咱们观察一下数据,也充分的验证了分类越多,香农熵会越大,当咱们的分类只有一类是香农熵是0
××××××

咱们采用列表法来计算一下熵:

某个特征的不一样取值对应的香农熵 0.0 0.9182958340544896
特征值划分出子集几率 0.4 0.6

根据指望的定义,咱们在第一章讲过的公式,那么就能够计算出这个特征的信息熵


咱们来测试一下结果:
这里写图片描述

In [1]: import trees

In [2]: reload(trees)
Out[2]: <module 'trees' from 'trees.pyc'>

In [3]: myDat,labels=trees.createDataSet()

In [4]: myDat
Out[4]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [5]: trees.chooseBestFeatureToSplit(myDat)
#总的数据分类
{'yes': 2, 'no': 3}
#按照第一个特征的第一个取值来划分
{'no': 2}
#按照第一个特征的第二个取值来划分
{'yes': 2, 'no': 1}
##按照第二个特征的第一个取值来划分
{'no': 1}
#按照第二个特征的第二个取值来划分
{'yes': 2, 'no': 2}
Out[5]: 0

咱们能够看出计算的结果是根据第一个特征划分比较好。

这个其实也很明显了,咱们能够观察一下咱们的数据按照第一个特征来划分,当特征为1时生物分组有两个属于鱼类,一个属于非鱼类,另外一个分组所有属于非鱼类。
若是按照第二个特征分类,一组中两个属于鱼类,两个属于非鱼来,另外一组中只有一个是非鱼类。也就是按照第二特征来划分,错误率比较大。


完整的代码:

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]


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

    return dataSet, labels


def calcShannonEnt(dataSet):
    countDataSet = len(dataSet)
    labelCounts={}
    for featVec in dataSet:
        currentLabel=featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    print labelCounts

    shannonEnt = 0.0

    for key in labelCounts:
        prob = float(labelCounts[key])/countDataSet
        shannonEnt -= prob * log(prob,2)


    return shannonEnt


def splitDataSet(dataSet,axis,value):
    retDataSet = []

    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)

    return retDataSet


def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0])-1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain =0.0
    bestFeature = -1

    for i in range(numFeatures):
        featList = [sample[i] for sample in dataSet]
        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
            bestFeature = i

    return bestFeature

9.特殊状况的处理

在以前的决策树算法中咱们已经讲解了从数据集构造决策树算法的功能模块。

首先是建立数据集,而后计算香农熵,而后基于最好的属性值划分数据集,因为特征值可能多于两个,所以可能存在大于两个分支的数据集划分。第一次划分好以后,数据将被向下传递到树分支的一个节点,在这个节点上咱们能够再次划分数据,因此咱们能够采用递归的原则处理数据集。

递归的结束条件是:遍历完全部划分的数据集的属性,或者每一个分支下的全部实例都具备相同的分类。若是全部实例具备相同的分类,则获得一格叶子节点或者终止块。

根据特征来划分属性,咱们知道每划分一次分类就会消耗一格特征值,若是咱们使用完全部的特征可是类别尚未划分完那么咱们就采用多数表决的方法来肯定叶子节点。

好比说咱们使用完全部的特征值以后划分,获得的最后的数据集是下面这个样子的:
[[‘yes’],[‘yes’],[‘maybe’]]

可是咱们如今已经没有特征值了,那么咱们就不能用计算香农熵的方法计算最大信息增益,这个时候就用投票表决的方式来分类。

那么咱们发现 咱们的数据集中 ‘yes’是两个,’maybe’是一个那么咱们就按照这个来把它们分开。
下面咱们来完成代码:

def majorityCnt(classList): # 传入的参数是已经划分完全部特征以后剩余的数据集,
#例如[['yes'],['yes'],['maybe']]
    classCount={} #参数是已经划分完全部特征以后剩余的数据集,
    #例如[['yes'],['yes'],['maybe']]
    classCount={} #建立一个字典
    for vote in classList:  
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
        # 根据上述的语句,以及咱们的例子,咱们最终能够获得的结果以下: {'yes':2,'maybe':1}
        sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1),reverse=True)#这个语句比较复杂,咱们在下面详细讲解一下。
# 使用字典iteritems
    return sortedClassCount[0][0]

下面咱们来分析在这段代码中比较复杂的代码:

sorted(classCount.iteritems(), key=operator.itemgetter(1),reverse=True)

在这里咱们使用iteritems()这个函数获得咱们字典中的全部元素,就是一组一组的键-值对。
以后咱们定义一个叫作key的函数,这个名字能够任意取,你们都是这么定义的,以后咱们经过itemgetter这个函数对咱们字典中的元素进行排序。operator.itemgetter(1)表示按照元素的第二个进行排序,也就是分类出现的多少。咱们的字典每个元素都有两部分组成,也就是按照值来排序,reverse=True 表示按照递减的顺序来排序。


sortedClassCount[0][0] 表示的是按照分类的个数最多的元素的那个类。



10.递归构建决策树

以前咱们已经学习了怎么根据信息论的方法,把一个数据集从杂乱无章的数据集中划分出来,咱们使用信息论来构建决策树一级一级分类的方法就是一个递归的过程。

它的工做原理以下:

  • 获得原始数据集,而后基于最好的属性值划分数据集。每一次划分数据集,咱们都要消耗一个特征,根据某个特征将某些性质相同的元素剥离出来
  • 划分数据的时候咱们根据香农熵,计算信息增益以后找到最好的属性值进行数据的划分。
  • 因为特征值可能有多于两个的,所以可能存在大于两个分支的数据集划分
  • 第一次划分数据将向下传递到树分支的下一个节点,在这个节点上,咱们能够再次划分数据集,所以咱们能够采用递归的原则来处理数据集。

咱们都知道递归必需要有一个终止条件,1)若是程序已经遍历完了全部的特征属性,2)或者每一个分支下的全部实例都具备相同的分类,咱们获得一个叶子节点或者终止块.这个就是咱们递归的终止条件.

出现1这种状况的特殊状况就是咱们以前在决策树算法(五)——处理一些特殊的分类 这篇文中已经详细的分析过了.当已经遍历完全部的特征属性可是任然还有一些类别灭有找出,那么咱们就根据选举投票的方法来进行分类.

固然对于第一个结束条件算法能够终止,咱们还能够设置算法能够划分的最大分组的数目.

11.建立决策树代码

下面咱们来构建决策树的代码,使用递归来进行.
咱们仍是打开咱们以前的文件trees.py, 在这个文件中添加以下的代码:

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]

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

    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]

    myTree = {bestFeatLabel:{}}

    del(labels[bestFeatLabel])

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

    uniqueVals = set(featValues)

    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)


    return myTree

以后咱们来分析一下这段代码:

仍是用咱们以前几章的那个数据集:
这里写图片描述
在这里任然须要注意咱们的数据集是前面每一项都是特征值,最后一项是咱们的类别信息.以下所示

dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]
#这里的第一条语句就是得到dataSet中的全部数据的类别:
classList = [example[-1] for example in dataSet]
#这种写法是python语法的一个特点,简单明了快捷随意.就是喜欢python这么随性,哈哈,像我.
# example中每次取出的是dataSet中的一个元素,e.g. [0,1,'no']
#example[-1] 就是每一个元素的最后一列.

咱们来看下执行结果:
这里写图片描述

以后的两个if条件是递归终止条件.

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

# 这个条件语句是表示全部的数据都已经划分完成,每一个类别已经彻底相同
#这样递归能够结束
#count()函数中接受一个参数,表示的是这个参数在某个序列中出现的次数
#若是这个classList中的元素彻底相同,那么这个参数的count(classList[0])应该是等于这个List的长度的.

if len(dataSet[0]) == 1:
        return majorityCnt(classList)
        # 第二个递归条件表示的只剩最后的类别信息的数据集.
        #由于决策树算法每作一次信息的划分,都会消耗一个特征,当特征
        #消耗完以后还有类别不一样那么咱们就须要投票表决了

这里写图片描述

看这张图应该很清楚了.

注意 :上面这两个条件语句都是咱们递归结束的条件.

bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
    #以后咱们调用chooseBestFeatureToSplit函数

chooseBestFeatureToSplit函数的原型以下(咱们在以前已经讲过):

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0])-1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain =0.0
    bestFeature = -1

    for i in range(numFeatures):
        featList = [sample[i] for sample in dataSet]
        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
            bestFeature = i

    return bestFeature

这个函数 返回的是最好的特征值,经过计算最大信息增益得到的.

# bestFeat 存储的最好的特征的下标.它和咱们的label是一一对应的
   bestFeatLabel = labels[bestFeat]

这里写图片描述

在这里咱们能够看出,咱们的数据集有两个特征,就是no surfing 和 flippers . 每一个数据集的第一列表示的是有仍是不须要no surfing, 第二列表示的有没有flippers. 1表示有,2表示没有.


这里写图片描述

咱们第一次调用chooseBestFeatureToSplit函数,结果告诉咱们选择第一个特征比较好

bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
 # bestFeatLabel中存储了最佳特征的标签
    myTree = {bestFeatLabel:{}} # 构建数据字典

    del(labels[bestFeatLabel])# 删除最佳特征值
#找出最佳特征向量对应的全部特征值
featValues = [example[bestFeat] for example in dataSet]

# 除去重复的特征值
    uniqueVals = set(featValues)

    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
        #递归调用构建决策树

咱们来测试一下咱们的实验结果.

这里写图片描述

mytree 包含了不少表明结构信息的嵌套字典. 在代码中也能够看到咱们其实是用一个数据字典来构建咱们的决策树.
第一个 no surfing 是第一个划分数据集的特征名称,在其下面有分为两类,特征是0的不是鱼类,是1的有被继续划分了.

这里写图片描述


到这里咱们决策树算法算是讲完了,咱们贴出整个分类的完整代码.

#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log
import operator

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]


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

    return dataSet, labels


def calcShannonEnt(dataSet):
    countDataSet = len(dataSet)
    labelCounts={}
    for featVec in dataSet:
        currentLabel=featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    print labelCounts

    shannonEnt = 0.0

    for key in labelCounts:
        prob = float(labelCounts[key])/countDataSet
        shannonEnt -= prob * log(prob,2)


    return shannonEnt


def splitDataSet(dataSet,axis,value):
    retDataSet = []

    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)

    return retDataSet


def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0])-1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain =0.0
    bestFeature = -1

    for i in range(numFeatures):
        featList = [sample[i] for sample in dataSet]
        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
            bestFeature = i

    return bestFeature

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]

    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]

    myTree = {bestFeatLabel:{}}

    del(labels[bestFeatLabel])

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

    uniqueVals = set(featValues)

    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)


    return myTree



写在后面的话

糟糕,我还爱他,仍是会想他
连给幸福一个机会我都不会

你必须很是努力,才能够看起来绝不费力 要么就不作,要作就作最好