[机器学习实战] 回归树算法python3实现

走远了吗. 提交于 2019-12-16 02:03:54

框架如下:
1. 树的构造:

  • 加载数据 loadDataSet()
  • 根据最佳的特征与特征值对数据进行二元切分binSplitDataSet():
  • 对叶节点点进行定义regLeaf()
  • 定义度量误差的标准regErr()
  • 获得用来切分数据的最佳的特征与特征值chooseBestSplit()
  • 构造回归树createTree()

2. 树的剪枝:

  • 对当前处理的节点进行判断,是否是叶节点 isTree()
  • 对两个叶节点计算均值getMean()
  • 对树进行后剪枝prune()

codes:

# -*- coding: utf-8 -*-
"""
Created on Mon Dec  9 19:17:55 2019

@author: buu
"""
import numpy as np


def loadDataSet(filename):
    '''
    函数说明:
        加载数据,并将数据都转化为float类型

    参数说明:
        无

    函数返回:
        dataMat: list, 包含许多小列表,一个小列表为一个样本
    '''
    dataMat = []
    with open(filename, 'r') as f:
        for line in f.readlines():
            curLine = line.strip().split('\t')
            fltLine = []
            for item in curLine:
                fltLine.append(float(item))
            dataMat.append(fltLine)
    return dataMat


def binSplitDataSet(dataSet, feature, value):
    '''
    函数说明:
        通过数组过滤的方法将数据集分成两个子集

    参数说明:
        dataSet: matrix, 待切分的数据集
        feature: 切分数据根据的特征
        value:   特征的值

    函数返回:
        mat0: matrix, 对应的特征的值大于某个数的所有数的集合
        mat1: matrix, 对应的特征的值小于等于某个数的所有数的集合

    np.nonzeros()返回两个array,第一个包含非零元素所在的行号
    第二个包含非零元素所在的列号
    '''
    mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
    # 1. dataSet[:,feature] > value 得到的是个 一维的 matrix,值为 False or True
    # 2. 令 b = dataSet[:,feature] > value
    #    np.nonzero(b) ,result is like (array([3],dtype=int64),array([0],dtype=int64)),row index and column index
    # 3. 所以令 c = np.nonzero(b)[0] is like array([3],dtype=int64) row index 即 [3],可能会有好多行号
    # 4. dataSet[c,:] c可能有好多行号,所以此行代码会输出对应行号的样本点
    # 5. dataSet[c,:][0] 得到第一列
    # 书上原始代码是: mat0 = dataSet[np.nonzero(dataSet[:,feature]>value)[0],:][0]
    # 个人感觉不应该有最后一个索引[0],去掉这个索引的话,就能得到所有符合特征抽取的样本集合了,否则只能得到一个

#    print(dataSet[:,feature]>value)
#    print(np.nonzero(dataSet[:,feature]>value)[0])
#    print(dataSet[np.nonzero(dataSet[:,feature]>value)[0],:])
#    print(dataSet[np.nonzero(dataSet[:,feature]>value)[0],:][0])
    mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
    return mat0, mat1


def regLeaf(dataSet):
    '''
    用来生成叶结点,得到叶结点的模型
    在回归树中,该模型其实就是目标变量的均值
    '''
    return np.mean(dataSet[:, -1])


def regErr(dataSet):
    '''
    上个函数跟这个函数计算的均值跟方差,都是对目标变量而言的

    方差是平方误差的均值(均方差)
    而这里需要的是平方误差的总值(总方差):可以通过均方差乘以数据集中样本点的个数来得到
    '''
    return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]


def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
    '''
    函数说明:
        对每个特征:
            对每个特征值:
                将数据集切分成两份
                计算切分的误差
                如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
    参数说明:
        dataSet: 数据集,matrix
        leafType: 对创建叶结点的函数的引用
        errType: 对总方差计算函数的引用
            ops: 用户定义的参数构成的元组,用以完成树的构建
    函数返回:
        找到最佳的切分特征索引及特征值
    '''
    tolS = ops[0]  # 容许的误差下降值
    tolN = ops[1]  # 切分的最少样本数 ; tolS与tolN是用户指定的参数,用于控制函数的停止时机
    if len(set(dataSet[:, -1].transpose().tolist()[0])) == 1:
        # b=dataSet[:,-1].transpose().tolist() : list,包含小 list, b[0]即dataSet[:,-1]的列表形式,set(b[0])即为目标变量的取值集合
        # 对当前所有目标变量建立一个集合,统计目标变量不同剩余特征值的数目,如果数目为1,即不能再分,就不需要再切分而直接返回
        return None, leafType(dataSet)
    m, n = np.shape(dataSet)  # 当前数据集的大小
    S = errType(dataSet)  # 当前数据集的误差,用于与新切分误差进行对比
    bestS = float('inf')
    bestIndex = 0
    bestValue = 0
    for featIndex in range(n-1):  # 对每个特征
        for splitVal in set((dataSet[:, featIndex].T.A.tolist())[0]):  # 对每个特征值
            mat0, mat1 = binSplitDataSet(
                dataSet, featIndex, splitVal)  # 将当前数据集切分成两份
            if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):
                continue  # 切分后任一子集包含的样本数小于用户设定的容许的切分的最小样本数时,进行下一次循环
            newS = errType(mat0) + errType(mat1)
            if newS < bestS:  # 根据误差,对特征及特征值进行更新
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    # 判断误差是否能达到预期:
    if (S - bestS) < tolS:  # 控制函数的停止时机:误差的减少量达到了预期
        return None, leafType(dataSet)
    # 误差未能达到预期时,判断集合包含的元素个数是否达到预期
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):  # 控制函数的停止时机
        return None, leafType(dataSet)
    # 在误差跟集合中元素个数都未能达到预期时,只能返回目前得到的最佳特征跟特征值
    return bestIndex, bestValue


def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
    '''
    函数说明:
        递归地构建树

    参数说明:
        dataSet:
        leafType: 给出建立叶结点的函数
        errType: 代表误差计算函数
        ops: 包含树构建所需其他参数的元组

    函数返回:
        返回构建好的树
    '''
    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['right']+tree['left'])/2.0


def prune(tree, testData):
    '''
    函数说明:
        使用后剪枝方法需要将数据集分成测试集和训练集
        从上而下找到叶节点,用测试集来判断将这些叶节点合并是否能降低测试误差,如果能就合并

        基于已有的树切分测试数据:
            如果存在任一子集是一棵树,则在该子集递归剪枝过程
            计算将当前两个叶结点合并后的误差
            计算不合并的误差
            如果合并后会降低误差的话,就将叶结点合并

    参数说明:
        tree: 待剪枝的树
        testData: 剪枝所需的测试数据
    '''
    if np.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 they are now both leafs, see if we can merge them
    if not isTree(tree['left']) and not isTree(tree['right']):
        lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
        # 计算没有合并时的误差,此时 tree['left'] ,tree['right']是叶子节点
        errorNoMerge = sum(np.power(lSet[:, -1] - tree['left'], 2)) +\
            sum(np.power(rSet[:, -1] - tree['right'], 2))
        treeMean = (tree['left']+tree['right'])/2.0
        # 计算合并后的误差
        errorMerge = sum(np.power(testData[:, -1] - treeMean, 2))
        if errorMerge < errorNoMerge:  # 合并后的误差变小
            print("merging")
            return treeMean
        else:  # 合并后误差未变小
            return tree
    else:
        return tree


def convenienceFunc1():
    filename = r'..\machinelearninginaction\Ch09\ex2.txt'
#    testMat=np.mat(np.eye(4)) # matrix, 4维的单位矩阵
#    mat0,mat1=binSplitDataSet(testMat,1,0.5)
#    print(mat0)
#    print(mat1)
    myDat = loadDataSet(filename)
    myDat = np.mat(myDat)
    myTree = createTree(myDat, ops=(0, 1))

    myDat2test = np.mat(loadDataSet(
        r'..\machinelearninginaction\Ch09\ex2test.txt'))
    newTree = prune(myTree, myDat2test)
    print(newTree)


if __name__ == '__main__':
    convenienceFunc1()

end

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!