《统计学习方法》 决策树 CART生成算法 回归树 Python实现

左手的ㄟ右手 2022-06-09 00:51 347阅读 0赞

代码可在Github上下载:代码下载
先说明一下在看《统计学习方法》Cart回归树的时候懵懵的,也没又例子。然后发现《机器学习实战》P162有讲到这个,仔细看了一下。
所以这下面是《机器学习实战》的代码,但书上没有什么原理,如果不太懂原理的话,会有点难以理解。而它的实现就是《统计学习方法》P69的算法5.5(最小二乘回归树的生成算法)。
######引用《统计学习方法》P69的算法5.5

输入:训练数据集 D;
输出:回归树 f(x).
在训练数据集所在的输入空间中,递归地将每个区域划分为两个子区域并决定每个子区域上的输出值,构建二叉决策树:
(1) 选择最优切分变量 j 与切分点 s(比如选择切分向量第j维来划分,就根据这个s>特征值和<=特征值来划分成2个子集),求解
m i n j , s [ m i n c 1 ∑ x i ∈ R 1 ( j , s ) ( y i − c 1 ) 2 + m i n c 2 ∑ x i ∈ R 2 ( j , s ) ( y i − c 2 ) 2 ] \mathop {min}_{j,s}[\mathop {min}_{c_1}\sum_{x_i\in R_1(j,s)}(y_i−c_1)^2+\mathop {min}_{c_2}\sum_{x_i\in R_2(j,s)}(y_i−c_2)^2] minj,s[minc1∑xi∈R1(j,s)(yi−c1)2+minc2∑xi∈R2(j,s)(yi−c2)2]
遍历变量 j,对固定的切分变量 j 扫描切分点 s
(2) 用选定的对 (j,s) 划分区域并决定相应的输出值:
R 1 ( j , s ) = x ∣ x ( j ) ≤ s R_1(j,s)={x|x^{(j)}≤s} R1(j,s)=x∣x(j)≤s, R 2 ( j , s ) = x ∣ x ( j ) > s R_2(j,s)={x|x^{(j)}>s} R2(j,s)=x∣x(j)>s
c ^ m = 1 N m ∑ x i ∈ R m ( j , s ) y i \hat c_m=\frac {1}{N_m}\sum_{x_i \in R_m(j,s)}y_i c^m=Nm1∑xi∈Rm(j,s)yi, x ∈ R m , m = 1 , 2 x\in R_m,m=1,2 x∈Rm,m=1,2
(3) 继续对两个子区域调用步骤(1),(2),直至满足停止条件。
(4) 将输入空间分为 M 个区域 R1,R2,⋯,RM,生成决策树:
f ( x ) = ∑ m = 1 M c ^ m I ( x ∈ R m ) f(x)=\sum_{m=1}^M\hat c_mI(x\in R_m) f(x)=∑m=1Mc^mI(x∈Rm)

好了,我们来看代码吧。

  1. def loadDataSet(self, fileName): #加载数据
  2. dataMat = []
  3. fr = open(fileName)
  4. for line in fr.readlines(): #遍历每一行
  5. curLine = line.strip().split('\t')
  6. fltLine = map(float, curLine) #将里面的值映射成float,否则是字符串类型的
  7. dataMat.append(fltLine)
  8. return dataMat

这上面是从文件中加载数据集的代码,没太多可以说的,接着看。

  1. def regLeaf(self, dataSet): #将均值作为叶子节点
  2. return np.mean(dataSet[:, -1]) #类别的均值

看下注释,其实就是对应上面的 c ^ m = 1 N m ∑ x i ∈ R m ( j , s ) y i \hat c_m=\frac {1}{N_m}\sum_{x_i \in R_m(j,s)}y_i c^m=Nm1∑xi∈Rm(j,s)yi, x ∈ R m , m = 1 , 2 x\in R_m,m=1,2 x∈Rm,m=1,2这个公式了。

  1. def regErr(self, dataSet):#计算误差
  2. return np.var(dataSet[:, -1]) * np.shape(dataSet)[0] #方差乘以行数

你可以先看下api:numpy.var就知道了它是一个计算方差的。

var = mean(abs(x - x.mean())**2)

方差乘以行数不就是mean(abs(x - x.mean())**2)然后行数,对应 m i n c 1 ∑ ( y i − c 1 ) 2 \mathop {min}_{c_1} \sum (y_i - c_1) ^2 minc1∑(yi−c1)2了么。
所以这个函数对应着 m i n j , s [ m i n c 1 ∑ x i ∈ R 1 ( j , s ) ( y i − c 1 ) 2 + m i n c 2 ∑ x i ∈ R 2 ( j , s ) ( y i − c 2 ) 2 ] \mathop {min}_{j,s}[\mathop {min}_{c_1}\sum_{x_i\in R_1(j,s)}(y_i−c_1)^2+\mathop {min}_{c_2}\sum_{x_i\in R_2(j,s)}(y_i−c_2)^2] minj,s[minc1∑xi∈R1(j,s)(yi−c1)2+minc2∑xi∈R2(j,s)(yi−c2)2].让我们乘胜逐北。

  1. def createTree(self, dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
  2. feat, val = self.chooseBestSplit(dataSet, leafType, errType, ops)
  3. if feat == None: return val #说明是叶节点,直接返回均值
  4. retTree = {}
  5. retTree['spInd'] = feat #记录是用哪个特征作为划分
  6. retTree['spVal'] = val #记录是用哪个特征作为划分(以便于查找的时候,相等进入左树,不等进入右树)
  7. lSet, rSet = self.binSplitDataSet(dataSet, feat, val) #按返回的特征来选择划分子集
  8. retTree['left'] = self.createTree(lSet, leafType, errType, ops) #用划分的2个子集的左子集,递归建树
  9. retTree['right'] = self.createTree(rSet, leafType, errType, ops)
  10. return retTree

这个就是建树的代码了,大体思想就是通过判断是否需要继续划分,如果不需要说明已经是叶节点了,返回叶节点的值;如果需要划分,则继续用递归的形式继续划分下去。继续看下如何选择最好的特征划分。

  1. def chooseBestSplit(self, dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
  2. tolS = ops[0] #容许的误差下降值
  3. tolN = ops[1] #划分的最少样本数
  4. if len(set(dataSet[:, -1].T.tolist()[0])) == 1: #类标签的值都是一样的,说明没必要划分了,直接返回
  5. return None, leafType(dataSet)
  6. m, n = np.shape(dataSet) #m是行数,n是列数
  7. S = errType(self, dataSet) #计算总体误差
  8. bestS = np.inf #np.inf是无穷大的意思,因为我们要找出最小的误差值,如果将这个值设得太小,遍历时很容易会将这个值当成最小的误差值了
  9. bestIndex = 0
  10. bestValue = 0
  11. for featIndex in range(n-1): #遍历每一个维度
  12. for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #选出不同的特征值,进行划分,勘误:这里跟书上不一样,需修改
  13. mat0, mat1 = self.binSplitDataSet(dataSet, featIndex, splitVal) #子集的划分
  14. if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): #划分的两个数据子集,只要有一个小于4,就说明没必要划分
  15. continue
  16. newS = errType(self, mat0) + errType(self, mat1) #计算误差
  17. if newS < bestS: #更新最小误差值
  18. bestIndex = featIndex
  19. bestValue = splitVal
  20. bestS = newS
  21. if (S - bestS) < tolS: #检查新切分能否降低误差
  22. return None, leafType(self, dataSet)
  23. mat0, mat1 = self.binSplitDataSet(dataSet, bestIndex, bestValue)
  24. if (np.shape(mat0)[0] < tolN) or(np.shape(mat1)[0] < tolN): #检查是否需要划分(如果两个子集的任一方小于4则没必要划分)
  25. return None, leafType(self, dataSet)
  26. return bestIndex, bestValue

这段代码,说白了就是上面算法(1)的步骤:

[ m i n c 1 ∑ x i ∈ R 1 ( j , s ) ( y i − c 1 ) 2 + m i n c 2 ∑ x i ∈ R 2 ( j , s ) ( y i − c 2 ) 2 ] [\mathop {min}_{c_1}\sum_{x_i\in R_1(j,s)}(y_i−c_1)^2+\mathop {min}_{c_2}\sum_{x_i\in R_2(j,s)}(y_i−c_2)^2] [minc1∑xi∈R1(j,s)(yi−c1)2+minc2∑xi∈R2(j,s)(yi−c2)2]

对应这个代码newS = errType(self, mat0) + errType(self, mat1)
上面for featIndex in range(n-1):以及里面的循环,
for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]):
就是寻找最小的误差值了。
然后这里有3个条件会被判定这个集合是作为叶子节点的(好不让他继续划分下去呀)。
1、类标签的值都是一样的,说明没必要划分了,直接返回None, 叶子节点值。
2、总体误差减去最小的误差如果小于容许的误差下降值(tolS=ops[0]),直接返回None, 叶子节点值。
3、如果划分的两个子集,任一方小于4(tolN=ops[1])个样本的话,直接返回None, 叶子节点值。
好了,测试一下。

  1. if __name__ == '__main__':
  2. regTree = RegressionTree()
  3. myMat = regTree.loadDataSet('ex0.txt')
  4. myMat = np.mat(myMat)
  5. print regTree.createTree(myMat)

你成功了吗?
下面贴下完整的代码。

  1. # --*-- coding:utf-8 --*--
  2. import numpy as np
  3. class RegressionTree: #回归树
  4. def loadDataSet(self, fileName): #加载数据
  5. dataMat = []
  6. fr = open(fileName)
  7. for line in fr.readlines(): #遍历每一行
  8. curLine = line.strip().split('\t')
  9. fltLine = map(float, curLine) #将里面的值映射成float,否则是字符串类型的
  10. dataMat.append(fltLine)
  11. return dataMat
  12. def binSplitDataSet(self, dataSet, feature, value): #按某列的特征值来划分数据集
  13. mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :] #勘误:这里跟书上不一样,需修改
  14. mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :] #np.nonzero(...)[0]返回一个列表
  15. return mat0, mat1
  16. def regLeaf(self, dataSet): #将均值作为叶子节点
  17. return np.mean(dataSet[:, -1])
  18. def regErr(self, dataSet):#计算误差
  19. return np.var(dataSet[:, -1]) * np.shape(dataSet)[0] #方差乘以行数
  20. def createTree(self, dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
  21. feat, val = self.chooseBestSplit(dataSet, leafType, errType, ops)
  22. if feat == None: return val #说明是叶节点,直接返回均值
  23. retTree = {}
  24. retTree['spInd'] = feat #记录是用哪个特征作为划分
  25. retTree['spVal'] = val #记录是用哪个特征作为划分(以便于查找的时候,相等进入左树,不等进入右树)
  26. lSet, rSet = self.binSplitDataSet(dataSet, feat, val) #按返回的特征来选择划分子集
  27. retTree['left'] = self.createTree(lSet, leafType, errType, ops) #用划分的2个子集的左子集,递归建树
  28. retTree['right'] = self.createTree(rSet, leafType, errType, ops)
  29. return retTree
  30. def chooseBestSplit(self, dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
  31. tolS = ops[0] #容许的误差下降值
  32. tolN = ops[1] #划分的最少样本数
  33. if len(set(dataSet[:, -1].T.tolist()[0])) == 1: #类标签的值都是一样的,说明没必要划分了,直接返回
  34. return None, leafType(dataSet)
  35. m, n = np.shape(dataSet) #m是行数,n是列数
  36. S = errType(self, dataSet) #计算总体误差
  37. bestS = np.inf #np.inf是无穷大的意思,因为我们要找出最小的误差值,如果将这个值设得太小,遍历时很容易会将这个值当成最小的误差值了
  38. bestIndex = 0
  39. bestValue = 0
  40. for featIndex in range(n-1): #遍历每一个维度
  41. for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #选出不同的特征值,进行划分,勘误:这里跟书上不一样,需修改
  42. mat0, mat1 = self.binSplitDataSet(dataSet, featIndex, splitVal) #子集的划分
  43. if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): #划分的两个数据子集,只要有一个小于4,就说明没必要划分
  44. continue
  45. newS = errType(self, mat0) + errType(self, mat1) #计算误差
  46. if newS < bestS: #更新最小误差值
  47. bestIndex = featIndex
  48. bestValue = splitVal
  49. bestS = newS
  50. if (S - bestS) < tolS: #检查新切分能否降低误差
  51. return None, leafType(self, dataSet)
  52. mat0, mat1 = self.binSplitDataSet(dataSet, bestIndex, bestValue)
  53. if (np.shape(mat0)[0] < tolN) or(np.shape(mat1)[0] < tolN): #检查是否需要划分(如果两个子集的任一方小于4则没必要划分)
  54. return None, leafType(self, dataSet)
  55. return bestIndex, bestValue

数据集文件”ex0.txt”以及回归树的完整代码见机器学习Github

发表评论

表情:
评论列表 (有 0 条评论,347人围观)

还没有评论,来说两句吧...

相关阅读

    相关 cart回归

    回归树:使用平方误差最小准则 训练集为:D=\{(x1,y1), (x2,y2), …, (xn,yn)\}。 输出Y为连续变量,将输入划分为M个区域,分别为R1

    相关 决策的剪枝,分类回归CART

    决策树的剪枝 决策树为什么要剪枝?原因就是避免决策树“过拟合”样本。前面的算法生成的决策树非常的详细而庞大,每个属性都被详细地加以考虑,决策树的树叶节点所覆盖的训练样本都是“