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

代码可在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[minc1xiR1(j,s)(yic1)2+minc2xiR2(j,s)(yic2)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)=xx(j)s, R 2 ( j , s ) = x ∣ x ( j ) > s R_2(j,s)={x|x^{(j)}>s} R2(j,s)=xx(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=Nm1xiRm(j,s)yi, x ∈ R m , m = 1 , 2 x\in R_m,m=1,2 xRm,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(xRm)

好了,我们来看代码吧。

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

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

def regLeaf(self, dataSet): #将均值作为叶子节点
        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=Nm1xiRm(j,s)yi, x ∈ R m , m = 1 , 2 x\in R_m,m=1,2 xRm,m=1,2这个公式了。

def regErr(self, dataSet):#计算误差
        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(yic1)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[minc1xiR1(j,s)(yic1)2+minc2xiR2(j,s)(yic2)2].让我们乘胜逐北。

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

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

def chooseBestSplit(self, dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
        tolS = ops[0] #容许的误差下降值
        tolN = ops[1]   #划分的最少样本数
        if len(set(dataSet[:, -1].T.tolist()[0])) == 1: #类标签的值都是一样的,说明没必要划分了,直接返回
            return None, leafType(dataSet)
        m, n = np.shape(dataSet)    #m是行数,n是列数
        S = errType(self, dataSet)    #计算总体误差
        bestS = np.inf  #np.inf是无穷大的意思,因为我们要找出最小的误差值,如果将这个值设得太小,遍历时很容易会将这个值当成最小的误差值了
        bestIndex = 0
        bestValue = 0
        for featIndex in range(n-1):    #遍历每一个维度
            for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #选出不同的特征值,进行划分,勘误:这里跟书上不一样,需修改
                mat0, mat1 = self.binSplitDataSet(dataSet, featIndex, splitVal) #子集的划分
                if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):    #划分的两个数据子集,只要有一个小于4,就说明没必要划分
                    continue
                newS = errType(self, mat0) + errType(self, mat1)    #计算误差
                if newS < bestS:    #更新最小误差值
                    bestIndex = featIndex
                    bestValue = splitVal
                    bestS = newS
        if (S - bestS) < tolS:  #检查新切分能否降低误差
            return None, leafType(self, dataSet)
        mat0, mat1 = self.binSplitDataSet(dataSet, bestIndex, bestValue)
        if (np.shape(mat0)[0] < tolN) or(np.shape(mat1)[0] < tolN): #检查是否需要划分(如果两个子集的任一方小于4则没必要划分)
            return None, leafType(self, dataSet)
        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] [minc1xiR1(j,s)(yic1)2+minc2xiR2(j,s)(yic2)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, 叶子节点值。
好了,测试一下。

if __name__ == '__main__':
    regTree = RegressionTree()
    myMat = regTree.loadDataSet('ex0.txt')
    myMat = np.mat(myMat)
    print regTree.createTree(myMat)

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

# --*-- coding:utf-8 --*--
import numpy as np

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

    def binSplitDataSet(self, dataSet, feature, value): #按某列的特征值来划分数据集
        mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]   #勘误:这里跟书上不一样,需修改
        mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]  #np.nonzero(...)[0]返回一个列表
        return mat0, mat1
    def regLeaf(self, dataSet): #将均值作为叶子节点
        return np.mean(dataSet[:, -1])

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

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

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

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

你可能感兴趣的:(机器学习)