Markdown:这里先记录一下,这是一种最近比较流行的XHTML语言,后期记得去仔细研究一下(突然想到的,和本文无关)。
随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文简单介绍了机器学习中的一个重要分类模型——决策树。
决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。
以二分类为例,我们判断一个瓜是好瓜还是坏瓜,可以转化为:这个瓜是好瓜吗?回答是“是”或者“否”。人类在思考、处理问题的时候也会有这样的思考过程。顾名思义,决策树就是一种基于树结构的分类决策模型,不同的是:判断一个瓜是否为好瓜,肯定不止一个带筛选的属性集,先需要就行“色泽”、“根蒂”等一系列的子决策,最后我们才能去判断这个瓜是不是好瓜。
下图是在西瓜书中,构建决策树的基本步骤:
在机器学习中,数据是非常重要的一部分。合适的数据对应合适的模型,在本文中,我使用了周志华老师西瓜书中案例的数据,还有一小份网上的数据(其实自己手写也可以),虽然数据量不大,但是用于做简单的demo还是绰绰有余的。
数据文件我放在网盘,需要的自取。
链接:https://pan.baidu.com/s/1Vro25wSPVGNQ0kdIaYQG7Q
提取码:cm1d
本文使用的语言是python3.7.7,IDE是Spyder4.0。
代码如下:
def createDataSet():
"""
outlook-> 0: sunny | 1: overcast | 2: rain
temperature-> 0: hot | 1: mild | 2: cool
humidity-> 0: high | 1: normal
windy-> 0: false | 1: true
"""
dataSet = [[0, 0, 0, 0, 'N'],
[0, 0, 0, 1, 'N'],
[1, 0, 0, 0, 'Y'],
[2, 1, 0, 0, 'Y'],
[2, 2, 1, 0, 'Y'],
[2, 2, 1, 1, 'N'],
[1, 2, 1, 1, 'Y']]
labels = ['outlook', 'temperature', 'humidity', 'windy']
return dataSet, labels
def createDataSetForWM():
'''
input
-------
None
Returns
-------
dataSet : TYPE: 2d list.
labels : TYPE: 1d list.
'''
# 创建属于西瓜的数据集
dataSet_df = pd.read_table('dataSet.txt', header=0, sep='\t', names=None)
dataSet = np.array(dataSet_df)
dataSet = list(dataSet)
# dataSet
# 遍历dataSet
for index, value in enumerate(dataSet):
dataSet[index] = list(dataSet[index])
for index2, value2 in enumerate(dataSet[index]):
# 判断如果是字符串,去除前后的字符
if(type(dataSet[index][index2]) == str):
dataSet[index][index2] = dataSet[index][index2].strip()
# 如果不是字符串,转换为字符串
else:
dataSet[index][index2] = str(dataSet[index][index2])
# labels
labels = list(np.array(dataSet_df.columns))
return dataSet, labels
dataSet, labels = createDataSet()
dataSet, labels = createDataSetForWM()
第一个数据集是网上找的,数据量和维度相对较少,第二个数据集是西瓜书里面的,我手打进了txt文件。返回的格式都是一样的,dataSet是2d的list,labels是1d的list。如果要使用的话,选择一个数据集读取即可。
构建树,本质上是调用递归的过程,递归必须要有一个或者多个出口。在决策树中,有以下几个递归的出口:
递归出口代码参考:
if classList.count(classList[0]) == len(classList):
return classList[0] # splitDataSet(dataSet, 0, 0)此时全是N,返回N
# 属性值的集合为空集(还差一个样本在属性集上取值相同)
if len(dataSet[0]) == 1: # [0, 0, 0, 0, 'N']
# 出现次数最多的属性
return majorityCnt(classList)
敲重点!这里是决策树乃至于所有树结构分类模型的精髓。不同的分类依据其实就对应了不同的树结构。
在决策树中,随着划分的不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,也就是说让结点的“纯度”越来越高。
说到这里,必须提到计算“纯度”的方式——“信息熵”。
信息熵是一种典型的纯度算法,假设当前样本D中的第k类样本所占的比例为pk,那么Ent(D)的定义为:
信息熵的计算结果越小,代表样本越纯。
在信源中,对于信息熵的定义是:考虑的不是某一单个符号发生的不确定性,而是要考虑这个信源所有可能发生情况的平均不确定性。若信源符号有n种取值:U1…Ui…Un,对应概率为:P1…Pi…Pn,且各种符号的出现彼此独立。这时,信源的平均不确定性应当为单个符号不确定性-logPi的统计平均值(E),可称为信息熵。类比到决策树中,其实本质上是各个不确定性样本的概率和,和越小,说明整体样本的不确定性越小,样本越纯。
代码如下:
def calcShannonEnt(dataSet):
"""
输入:数据集
输出:数据集的香农熵
描述:计算给定数据集的香农熵;熵越大,数据集的混乱程度越大
"""
numEntries = len(dataSet)
# 求每个类别的数量
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
# 数每一类各多少个, {'Y': 4, 'N': 3}
labelCounts[currentLabel] += 1
# 求属性集的熵
shannonEnt = 0.0
for key in labelCounts:
# 每个类别对应的概率(比重)
prob = float(labelCounts[key]) / numEntries
shannonEnt += (-1) * prob * log(prob, 2)
return shannonEnt
计算完信息熵后,我们得到了在每一个结点上的样本纯度,接下来我们应该更具纯度去选择合适的划分属性。例如我们在一个根结点上计算出对应的信息熵是x,那么我们在这个结点的基础上,计算每一种划分属性的划分结果,比如说按照“色泽”来划分当前结点,所得到的结点的纯度,大于用“纹理”来划分当前结点的纯度,那我们就应该用“色泽”来划分当前结点(仅考虑了这两个结点)。
在这样的比对模式下,我们采用“信息增益”来表示纯度的增减情况。即用根结点的信息熵减去划分属性后对应子结点的信息熵的和。信息增益越大,说明纯度下降的越多,即纯度提升的越多。下图是信息增益的计算公式:
虽然说信息增益在一定程度上可以代表样本的提纯幅度,但是当我们考虑类别数近乎等于样本数的属性时(例如“编号”这一属性),计算出来的信息增益也很大,由此我们定义了“信息增益率”这一新评估标准,在计算完信息增益后除以对应属性的“属性固有值”,用以表示增长的速率。信息增益率的计算公式如下:
选择最优结点建树的代码如下:
def chooseBestFeatureToSplit(dataSet):
"""
输入:数据集
输出:最好的划分维度
描述:选择最好的数据集划分维度
"""
# 属性的个数(最后一个是标签)
numFeatures = len(dataSet[0]) - 1
# 根节点的熵
# 信息熵:即计算一个整体的混乱程度,熵越小,说明混乱程度越小,样本越纯
baseEntropy = calcShannonEnt(dataSet)
# 最优信息增益率
# 信息增益:即计算从一个分类节点到另一个之间产生的信息熵差异,原则上是减少,所以减少的越多,说明分类的结果越好
bestInfoGainRatio = 0.0
# 最优的属性
bestFeature = -1
# 遍历所有的属性
print('每个属性的信息增益率:')
for i in range(numFeatures):
# 所有数据的指定属性组成的集合
featList = [example[i] for example in dataSet]
# 每个list的唯一值集合
uniqueVals = set(featList)
# 新的信息熵
newEntropy = 0.0
# 指定属性的固有值
splitInfo = 0.0
for value in uniqueVals:
# 每个唯一值对应的剩余feature的组成子集
subDataSet = splitDataSet(dataSet, i, value)
# 当前类别样本占总样本的比例,即计算概率
prob = len(subDataSet) / float(len(dataSet))
# 计算熵
newEntropy += prob * calcShannonEnt(subDataSet)
# 计算指定属性的固有值(a),固有值越大,说明类别越杂,过杂的类别容易造成过拟合
splitInfo += -prob * log(prob, 2)
# 这个feature的infoGain(信息增益)
infoGain = baseEntropy - newEntropy
# fix the overflow bug
if (splitInfo == 0):
continue
# 这个feature的infoGainRatio(信息增益率)
infoGainRatio = infoGain / splitInfo
print('当前的信息增益率为:{}'.format(infoGainRatio))
# 选择最大的gain ratio
if (infoGainRatio > bestInfoGainRatio):
bestInfoGainRatio = infoGainRatio
# 选择最大的gain ratio对应的feature
bestFeature = i
return bestFeature
这样一来,我们便可以选择一个最优的属性,用以划分某一个根结点。如下图所示,这是一棵完整的决策树:
决策树和其他的树型分类模型一样,本质上就是递归地去创建结点,直到碰到递归地边界才返回。不同地地方就是选择属性的判断条件不一样,这也就很大程度上影响了对于不同数据集的分类效果。理解熵的概念并不难,只要能看懂基本的数学公式,基本就可以对应理解公式的含义。
完整代码链接我放在我的blog主页,需要的自取。