目录
1 分析
1.1 背景:
线性回归的模型一般都要拟合所有的样本点,但当数据拥有众多特征,并且特征之间的关系十分的复杂,这时候往往是非线性的问题,很难构建全局模型。
方法:将数据集切分成很多份易建模的的数据,再线性回归(就像微分一样的思想),一次切分不行就两次不断递归,这时候用树结构就很合适。
树结构为什么不用ID3算法呢?因为ID3一般用来处理离散值,不能直接连续型特征。所以我们采用二元切分法来处理连续型特征,如果特征值大于定值就走左子树,小于就走右子树。
1.2 定义
基于上述问题,有了CART,分类回归树,顾名思义既能分类又能回归,当CART是分类树时,采用GINI值作为节点分裂的依据;当CART是回归树时,采用样本的最小方差作为节点分裂的依据;采用了二元切分,所以是一棵二叉树。
分类树的作用是通过一个对象的特征来预测该对象所属的类别(打标签),而回归树的目的是根据一个对象的信息预测该对象的属性数值。举个栗子:一个数据集,包含以下信息:看电视时间,婚姻情况(已婚/未婚),职业,年龄;如果我们想预测一个人是否已婚,那么构建的CART将是分类树;如果想预测一个人的年龄,那么构建的将是回归树。
1.3 原理:
-
CART如何选择分裂的属性?
分裂的目的是为了能够让数据变纯,使决策树输出的结果更接近真实值。那么CART是如何评价节点的纯度呢?如果是分类树,CART采用GINI值衡量节点纯度;如果是回归树,采用样本方差衡量节点纯度。节点越不纯,节点分类或者预测的效果就越差。
分类树:用基尼指数来选择最优特征,同时决定该特征的最优二值切分点
基尼指数公式和含义:
,对于二分类简单的有
如果样本点属于和不属于第k类的概率和相差越近,比如五五开的话,基尼指数越大,结点纯度越低。
对样本集使用该公式:
对给定样本集合D,其基尼指数为:
在特征A下,集合D的基尼指数:
根据该基尼指数来确定最小的特征及其对应切分点
回归树:用回归方差,方差越大,表示该节点的数据越分散,预测的效果就越差。如果一个节点的所有数据都相同,那么方差就为0,此时可以很肯定得认为该节点的输出值;如果节点的数据相差很大,那么输出的值有很大的可能与实际值相差较大。
因此,无论是分类树还是回归树,CART都要选择使子节点的GINI值或者回归方差最小的属性作为分裂的方案。
-
如何进行树的剪枝来防止过拟合
从样本预留出一部分数据用作“验证集”以进行性能评估。
预剪枝
在决策树生成过程中,对每个结点在划分前先进性估计分,若当前节点的划分不能带来决策树泛化能力提升,则停止划分,将当前节点标记为叶结点;
后剪枝
现从训练集生成完整的决策树,然后自底向上地对非叶节点进行考察,弱将该节点对应的子树替代能带来决策树泛化能力提升,则把当前子树替换为叶子结点。
CART采用CCP(代价复杂度)剪枝方法。代价复杂度选择节点表面误差率增益值最小的非叶子节点,删除该非叶子节点的左右子节点,若有多个非叶子节点的表面误差率增益值相同小,则选择非叶子节点中子节点数最多的非叶子节点进行剪枝。
-
对于含有空值的数据,此时应该怎么构建树。
第一,如何在属性值缺失的情况下进行属性划分选择(选择划分的特征)
不将缺失值的样本代入选择判断的公式计算(信息增益、增益率、基尼指数)之中,只在计算完后乘以一个有值的样本比例即可。比如训练集有10个样本,在属性 a 上,有两个样本缺失值,那么计算该属性划分的信息增益时,我们可以忽略这两个缺失值的样本来计算信息增益,然后在计算结果上乘以8/10即可。
第二,若一个样本在划分属性上的值为空,它应该被分在哪个子结点中(归类样本)
若样本 x 在划分属性 a 上取值未知,则将 x 划入所有子结点,但是对划入不同子结点中的 x 赋予不同的权值(不同子结点上的不同权值一般体现为该子结点所包含的数据占父结点数据集合的比例)
2.实践:(《机器学习实战》第九章代码解析)
-
CART算法的实现(运用到预剪枝)
##CART算法的实现代码
#导入数据
def loadDataSet(fileName):
dataMat = [] #假设最后一列是目标值
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float,curLine)) #将每行映射成浮点数
dataMat.append(fltLine)
return dataMat
#切分得到两个子集
def binSplitDataSet(dataSet, feature, value):
"""
将数据集合切分得到两个子集
:param dataSet: 数据集合
:param feature: 待切分的特征
:param value: 该特征的某个值
:return: 返回两个子集
"""
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]#数据集中第feature列的值大于value的分为一组
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]#数据集中第feature列的值小于等于value的分为一组
return mat0,mat1
#回归树的叶节点生成函数
def regLeaf(dataSet):#returns the value used for each leaf
return mean(dataSet[:,-1])#在回归树中,返回目标量的均值
#误差估计函数
def regErr(dataSet):
return var(dataSet[:,-1]) * shape(dataSet)[0]#总方差=方差*数据集中样本的个数
#回归树的切分函数
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
"""
找到数据的最佳二元切分方式
:param dataSet: 数据集
:param leafType: 对创建叶节点的函数的引用
:param errType: 对误差估计函数的引用
:param ops: 是一个用户定义的参数构成的元组,用于控制函数的停止时机
:return: 返回特征编号和切分特征值
"""
tolS = ops[0]#容许的误差下降值
tolN = ops[1]#切分的最少样本数
#如果所有目标变量都是相同的值则退出
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #tolist()是将数组或矩阵转换为列表,set() 函数创建一个无序不重复元素集
return None, leafType(dataSet) #返回None并同时产生叶节点
m,n = shape(dataSet)
#最佳切分也就是使得切分后能达到最低误差的切分
S = errType(dataSet)#误差
bestS = inf#正无穷
bestIndex = 0; bestValue = 0
for featIndex in range(n-1):
for splitVal in set(dataSet[:,featIndex].T.tolist()[0]):
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)#切分得到两个数据集
if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
continue#如果某个子集的大小小于用户定义的参数tolN,则跳出本次循环,继续下一轮循环
newS = errType(mat0) + errType(mat1)#新误差
if newS < bestS:
bestIndex = featIndex
bestValue = splitVal
bestS = newS
#如果下降(S-bestS)小于阈值tolS,则不要切分而直接创建叶节点
if (S - bestS) < tolS:
return None, leafType(dataSet) #返回None并同时产生叶节点
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)#切分得到两个数据集
if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): #如果切分出的数据集的大小小于用户定义的参数tolN
return None, leafType(dataSet)#返回None并同时产生叶节点
return bestIndex,bestValue#返回切分特性和特征值
#构建回归树
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):#假设数据集是NumPy Mat,那么我们可以数组过滤
"""
构建树
:param dataSet: 数据集
:param leafType: 建立叶节点的函数
:param errType: 误差计算函数
:param ops: 是一个用户定义的参数构成的元组,用于控制函数的停止时机
:return: 存放树的数据结构的字典
"""
feat, val = chooseBestSplit(dataSet, leafType, errType, ops)#选择最佳分割,二元切分
if feat == None: return val #如果切分达到停止条件,返回特征值
retTree = {}#字典
retTree['spInd'] = feat
retTree['spVal'] = val
lSet, rSet = binSplitDataSet(dataSet, feat, val)#切分得到两子集
retTree['left'] = createTree(lSet, leafType, errType, ops)#左子树
retTree['right'] = createTree(rSet, leafType, errType, ops)#右子树
return retTree#存放树的数据结构的字典
-
后剪枝算法实现:
def isTree(obj):#用来判断当前处理的节点是否是叶结点
return (type(obj).__name__=='dict')
def getMean(tree):#递归从上到下遍历,找到两个叶结点就计算平均值返回(对数进行塌陷处理)
if isTree(tree['right']): tree['right'] = getMean(tree['right'])
if isTree(tree['left']): tree['left'] = getMean(tree['left'])
return (tree['left']+tree['right'])/2.0
def prune(tree, testData):#剪枝函数
"""
剪枝函数
参数:
tree -- 待剪枝的树
testData -- 测试数据
返回:
treeMean -- 合并的结果
或
tree -- 不需要剪枝
"""
# 如果没有测试数据,就直接把整棵树合并
if shape(testData)[0] == 0: return getMean(tree)
# 如果没有树可以合并,则分割节点
if (isTree(tree['right']) or isTree(tree['left'])):
lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
# 递归分割左右子树
if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet)
if isTree(tree['right']): tree['right'] = prune(tree['right'], rSet)
# 如果两棵树都是叶子节点,则判断是否要合并
if not isTree(tree['left']) and not isTree(tree['right']):
lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
# 不合并的误差
errorNoMerge = sum(power(lSet[:,-1] - tree['left'],2)) +\
sum(power(rSet[:,-1] - tree['right'],2))
treeMean = (tree['left']+tree['right'])/2.0
# 合并误差
errorMerge = sum(power(testData[:,-1] - treeMean,2))
# 如果合并后,误差减小,则执行合并
if errorMerge < errorNoMerge:
print("merging")
return treeMean
# 反之,不执行合并
else: return tree
else: return tree
参考
【1】https://www.jianshu.com/p/d80fbec52f09
【2】https://www.cnblogs.com/yonghao/p/5135386.html