RandomForest详解(附带详细公式推导)

RandomForest详解

第三次写博客,本人数学基础不是太好,如果有幸能得到读者指正,感激不尽,希望能借此机会向大家学习。这一篇的内容来自于各种书籍,以及自己的一些见解。

预备知识:

这一部分主要是谈一谈bootstrap sampling(自助采样法)、Bagging,以及out-of-bag estimate(包外估计)中涉及到的基础数学公式和定理的推导。

bootstrap sampling(自助采样法)

  在训练学习器时,我们希望学习器不仅能很好地拟合训练样本,还可以有较低的泛化误差,因此一般采用留出法和交叉验证法,但是这些方法会受到数据规模的影响,尤其是在原始数据很少的情况下,而留一法又会带来将巨大的计算量。因此可以采用自助采样法(bootstrap sampling)。
  假设当前有一个含有m个样本的数据集D,我们对其进行m次“有放回的”随机采样,这样得到了大小为m的新数据集D’。可以肯定的是,新数据集必定含有原始数据集中某个样本的重复采样,可以进行下面的估计。
  每一轮采样中,样本x被抽到的概率为1/m,因此,在m轮抽样后,该样本仍未被抽取到的概率为:

  因此,原始数据集中有大约36.8%的样本没有被抽到,这些样本就可以用于由新数据集D’训练得到的学习器的“包外估计”【3】。
  当数据集很小,以至于训练\测试集不能很好地分开时,这种方法比较适用,也为集成学习提供了很多帮助。但是,由于得到的新数据集或多或少的改变了原始数据集的样本分布,自助采样法可能会引入一些误差,因此,当原始数据集足够大时,尽量选用留出法和交叉验证法。

Bagging

  在集成学习中,基学习器的“好坏”非常关键,我们希望基学习器们拥有很强大的性能,同时又希望基学习器之间有一定的差异。如果所有基学习器仅通过同一个训练集进行训练,必定不会产生差异。这时就需要通过原始数据集生成多个不同的样本分布的新训练集,我们可以采用自助采样法(bootstrap sampling)来达到这个目的。
  Bagging就是通过在每轮迭代中,首先通过自助采样法生成不同的训练集,之后基于这个“新”训练集对基学习器进行训练。最后,集成学习器的预测结果将由T轮迭代后生成的T个基学习器,进行简单的投票法(其他的结合策略见Ensemble Strategy一文)来决定。
  Bagging伪代码如下:

RandomForest详解(附带详细公式推导)_第1张图片

  与标准Adaboost只能用于二分类任务不同的是,Bagging可以不经修改的用于多分类、回归等任务。

out-of-bag estimate(包外估计)

  由于自助采样法产生的新数据集包含大约63.2%的原始样本,因此每轮迭代中,可以使用剩下的36.8%的原始样本对产生的基学习器进行泛化误差的“包外估计”(out-of-bag estimate),减小过拟合的风险,当基学习器是决策树或神经网络时尤其重要。
  还可以利用这些未被选做训练样本的原始样本,对集成学习器的泛化误差进行包外估计。假设,Bagging算法产生了T个基学习器 h t h_t ht,其中每个基学习器是由数据集 D t D_t Dt训练得到的, H o o b ( x ) H^{oob}\left(x\right) Hoob(x)是对样本x的包外预测,即仅考虑那些没有使用x的基学习器在x上的预测,可得

  则Bagging的泛化误差的包外估计为

其中 ∣ D ∣ |D| D为原始数据集D的大小,Y为y的值域。

推导过程:

主要分为三部分:标准的随机森林、比较多变量CART树、Bagging、以多变量CART树作为基学习器的随机森林和随机森林的一些要点。

标准的随机森林

  标准随机森林(RF)是以决策树作为基学习器的,一种基于Bagging的拓展算法,在每一轮决策树训练过程中加入了随机属性选择。具体来说,在训练决策树时,是从当前结点的全部属性集合中选择最优的划分属性,而在RF中,对于决策树的每个结点,显示从当前节点的全部属性集合中随机选择一个包含k个属性的子集,之后再从这个子集中选择一个最优的划分属性。毋庸置疑,参数k控制了随机性的引入程度,k越大基学习器的性能越好,但是基学习器之间的独立性会大打折扣;k越小基学习器之间的相关性会降低,但是基学习器的性能也会下降。因此,在很多文献中,这样选择k

作为标准随机森林(RF)的两种形式之一,上述算法也被称为Forest-RI,其中RI指的是Random Input(随机输入选择),这种方法适用于原始候选属性集大小d足够大,且该算法由于大大减少了在每个结点进行划分属性的候选属性数量,因此运行时间大大减少。
  但是当d不足够大时,我们就要考虑标准随机森林的另一种形式,即Forest-RC,其中RC指的是Random Combin(随机组合),这种方法通过在每个结点创建候选属性的线性组合,以增加供每个结点进行划分特征选择的候选属性集大小。具体的方法是:从决策树的每个结点的原始候选属性集合中,随机选择L个属性(L < k),之后将这些属性用区间[-1,1]的均匀分布产生的系数进行线性组合,来生成大小为k的候选属性集合,最后从这个集合中选择最优划分属性(线性组合),这样,每个基学习器与多变量决策树类似。

比较多变量CART树、Bagging、以多变量CART树作为基学习器的随机森林

  比较多变量CART树、Bagging、以多变量CART树作为基学习器的随机森林(下图从左至右,其中Bagging中每轮训练样本数为原始数据集的一半)

RandomForest详解(附带详细公式推导)_第2张图片

由上面的几幅图可知,随机森林比其他两种算法产生的分类边界更加smooth,并且有类似于最大间隔分类器的分类边界。
  在数据集比较复杂时,我们来对Bagging、以多变量CART树作为基学习器的随机森林(下图从左至右,其中Bagging中每轮训练样本数为原始数据集的一半)进行比较。

RandomForest详解(附带详细公式推导)_第3张图片

由上面几幅图可以看出,随机森林比Bagging更加具有鲁棒性。
  当数据中再加入一些噪声后,可以得到下面几幅图。

RandomForest详解(附带详细公式推导)_第4张图片

可以看出,随机森林生成的决策边界中,正例中包含一部分被错误标记为负例的噪声样本,因此随机森林还有通过投票修正噪声的优点。

随机森林的一些要点

  1. Bagging的特点
      Bagging主要关注降低方差,因此当基学习器为决策树和神经网络等易受样本扰动的学习器时,Bagging的效果更好,因为这些学习器普遍具有较高的方差。当然也可以在生成基学习器的过程中,使用包外样本提高基学习器的泛化能力。
  2. 随机森林的多样性
      随机森林的多样性不仅来自于样本扰动,还来自于属性扰动,这就使得最终的集成学习器的泛化性能可通过基学习器之间独立性的增加而提升。
  3. 随机森林的特点
      随机森林的起始性能往往比较差,特别是只有一个基学习器时,这是因为基学习器的训练过程中加入了属性扰动,导致基学习器的性能降低。但是,随着基学习器的个数增加,随机森林产生的集成学习器的性能会得到很大的提升,即最终泛化误差会收敛到最小。
  4. 随机森林的泛化误差
      理论证明,当树的数目足够大时,随机森林的泛化误差的上界收敛于下面的表达式

  其中 ρ ˉ \bar{\rho} ρˉ是树之间的平均相关系数,s是度量树型分类器的“强度”的量。一组分类器的强度是指分类器的平均性能,而性能以分类器的余量(M)用概率算法度量:

  其中 Y θ ^ \hat{Y_{\theta}} Yθ^是根据模随机向量 θ \theta θ构建的分类器对X做出的预测类。余量越大,分类器正确预测给定的样本X的可能性就越大。由泛化误差上界的定义公式可知,随着树的相关性增加或组合分类器的强度降低,泛化误差的上界趋于增加。因此,随机化有助于减少决策树之间的相关性,从而改善组合分类器的泛化误差。


参考资料

【1】《机器学习》
【2】《机器学习实战》
【3】《数据挖掘》
【4】《机器学习技法》


代码实现及对比

下面是我自己实现的代码,这段代码主要实现了Bagging和RandomForest两种集成学习方法,采用CART作为基学习器,由于IRIS数据集作为训练集,因此在之前的CART代码上进行了修改,具体的可以看下面的代码细节,下面对这两个算法的结果进行简要分析。

代码细节

"""

@author: Ἥλιος
@CSDN:https://blog.csdn.net/qq_40793975/article/details/80988486

"""


import numpy as np
import random
import time


# 加载IRIS数据集
def load_IRISdata(filename):
    labelMap = {"Iris-setosa": 1, "Iris-versicolor": 2, "Iris-virginica": 3}    # 标记映射
    with open(filename) as fr:
        dataMat = []
        labelMat = []
        for data in fr.readlines():
            data = [i for i in data.strip().split(",")]
            dataMat.append([float(i) for i in data[:-1]])
            labelMat.append(data[-1])
    dataMat = np.mat(dataMat)
    for i in range(np.size(dataMat, axis=0)):
        labelMat[i] = labelMap[labelMat[i]]
    labelMat = np.mat(labelMat).T
    return dataMat, labelMat


allDataMat, allLabelMat = load_IRISdata("C:\\Users\\Administrator\\Desktop\\iris.data")


# 划分数据集为训练集和测试集(trainRate是训练集占比)(可选)
def split_Dataset(dataMat, labelMat, trainRate=0.6):
    m = np.size(dataMat, axis=0)
    allIndex = [i for i in range(m)]    # 创建整个数据集的索引
    trainIndex = []  # 训练集索引
    labelSet = set(labelMat.T.tolist()[0])  # 获取标记列表
    for label in labelSet:  # 对于标记列表中的每个标记,分别进行比例为trainRate的随机采样
        labelIndex = np.nonzero(labelMat == label)[0]
        indexList = [i for i in range(np.size(labelIndex))]
        indexes = random.sample(indexList, int(trainRate*np.size(labelIndex)))
        trainIndex = trainIndex + labelIndex[indexes].tolist()
    testIndex = list(set(allIndex).difference(set(trainIndex)))  # 得到测试集索引
    trainDataMat = dataMat[trainIndex, :]
    trainLabelMat = labelMat[trainIndex, :]
    testDataMat = dataMat[testIndex, :]
    testLabelMat = labelMat[testIndex, :]
    return trainDataMat, trainLabelMat, testDataMat, testLabelMat


# trainDataMat, trainLabelMat, testDataMat, testLabelMat = split_Dataset(allDataMat, allLabelMat)


# bootstrap
def bootstrap_Sample(dataMat, labelMat):
    m = np.size(dataMat, axis=0)
    allSet = [i for i in range(m)]
    dataIndex = np.random.randint(0, m, m, dtype=np.int)
    sampleDataMat = dataMat[dataIndex, :]
    sampleLabelMat = labelMat[dataIndex, :]
    sampledSet = set(dataIndex)
    restSet = list(set(allSet).difference(sampledSet))
    print("Sample Rate:", 100 * len(sampledSet)/m)
    return sampleDataMat, sampleLabelMat, restSet


# sampleDataMat, sampleLabelMat, restSampleIndex = bootstrap_Sample(allDataMat, allLabelMat)
# dataMat = np.hstack((sampleDataMat, sampleLabelMat))


# 划分数据集(CART)
def split_dataSet(dataMat, featIndex, featValue):
    ret_dataMat_g = dataMat[np.nonzero(dataMat[:, featIndex] > featValue)[0], :]
    ret_dataMat_l = dataMat[np.nonzero(dataMat[:, featIndex] <= featValue)[0], :]
    return ret_dataMat_g, ret_dataMat_l


# 回归树叶节点标记划分
def reg_leaf(dataMat):
    labelList = dataMat[:, -1].T.tolist()[0]
    labelSet = set(labelList)
    labelDict = {}
    for label in labelSet:
        labelDict.update({label: labelList.count(label)})
    return max(labelDict, key=labelDict.get)


# 回归树结点纯度评价(加权回归误差)
def reg_error(dataMat):
    return np.var(dataMat[:, -1])*np.shape(dataMat)[0]


# 选择最优划分属性和划分值
# 输入:
#       dataMat:数据集,包括每个样本的特征向量与标记
#       toln:一个节点最少含有的样本数
#       sampleFeatNum:在Bagging中没用到
# 输出:
#       best_featIndex:最佳划分属性的索引
#       best_featValue:最佳划分值
def choose_best_split(dataMat, toln, sampleFeatNum=3):
    best_featIndex = -1
    best_featValue = 0.0
    best_S = np.inf
    for featIndex in range(np.shape(dataMat)[1] - 1):
        for featValue in set(dataMat[:, featIndex].T.tolist()[0]):
            ret_dataMat_g, ret_dataMat_l = split_dataSet(dataMat, featIndex, featValue)
            if np.shape(ret_dataMat_g)[0] < toln or\
                    np.shape(ret_dataMat_l)[0] < toln:  # 如果划分后的两个子结点其中有一
                                                        # 个不满足最低的样本数限制,就直接计算当前结点的总误差
                current_S = reg_error(dataMat)
            else:
                current_S = reg_error(ret_dataMat_l) + reg_error(ret_dataMat_g)
            if current_S < best_S:
                best_S = current_S
                best_featIndex = featIndex
                best_featValue = featValue
    return best_featIndex, best_featValue


# 创建树(tols和toln越小树的规模越大)
# 输入:
#       tols:结点划分前后误差之差的阈值
#       toln:一个节点最少含有的样本数
# 输出:
#       retTree:在创建过程中,该参数是作为输入参数进行递归的,即左子树或右子树;最终输出时,作为整个过程的输出,即整个树
def createTree(dataMat, tols=1.0, toln=2, choose_best_split_method=choose_best_split, sampleFeatNum=3):
    if len(set(dataMat[:, -1].T.tolist()[0])) == 1:     # 如果该结点下的样本全部属于一个标记,
                                                        # 直接将该节点作为子结点
        return dataMat[0, -1]
    allfeat_same = 1
    for featIndex in range(np.shape(dataMat)[1]-1):
        if len(set(dataMat[:, featIndex].T.tolist()[0])) != 1:
            allfeat_same = 0
    if allfeat_same == 1:  # 该结点下所有样本点的特征值是否相同,如果相同就将该结点作为子结点
        return reg_leaf(dataMat)
    best_featIndex, best_featValue = choose_best_split_method(dataMat, toln, sampleFeatNum)
    ret_dataMat_g, ret_dataMat_l = split_dataSet(dataMat, best_featIndex, best_featValue)
    if np.shape(ret_dataMat_l)[0] < toln or\
            np.shape(ret_dataMat_g)[0] < toln:  # 如果划分后的两个子结点其中有一
                                                # 个不满足最低的样本数限制,就直接将当前结点作为叶结点
        return reg_leaf(dataMat)
    current_S = reg_error(ret_dataMat_g) + reg_error(ret_dataMat_l)
    S = reg_error(dataMat)
    if (S - current_S) < tols:  # 如果划分后的总误差相比划分之前的总误差减少量低于tols,就将当前结点作为叶结点
        return reg_leaf(dataMat)
    retTree = {"featIndex": best_featIndex, "featValue": best_featValue,
               "left": createTree(ret_dataMat_g, tols, toln, choose_best_split_method, sampleFeatNum),
               "right": createTree(ret_dataMat_l, tols, toln, choose_best_split_method, sampleFeatNum)}
    return retTree


# 对新样本点进行估计
def estimate_newSample(tree, data):
    # if type(tree).__name__ != 'dict':
    #     return tree
    if data[0, tree['featIndex']] > tree['featValue']:
        if type(tree['left']).__name__ == 'dict':
            return estimate_newSample(tree['left'], data)
        else:
            return tree['left']
    else:
        if type(tree['right']).__name__ == 'dict':
            return estimate_newSample(tree['right'], data)
        else:
            return tree['right']


# 对新样本集进行估计
def estimate_newMat(tree, newDataMat):
    m = np.shape(newDataMat)[0]
    Y_hat = np.mat(np.zeros((m, 1)))
    for i in range(m):
        Y_hat[i, 0] = estimate_newSample(tree, newDataMat[i, :])
    return Y_hat


# myTree = createTree(dataMat)
# Y_hat = estimate_newMat(myTree, dataMat)
# print("Training Set Accuracy: ", np.sum(Y_hat == dataMat[:, -1]) / np.shape(dataMat)[0])
# newDataMat = np.hstack((allDataMat[restSampleIndex, :], allLabelMat[restSampleIndex, :]))
# Y_hat = estimate_newMat(myTree, newDataMat)
# print("Testing Set Accuracy: ", np.sum(Y_hat == newDataMat[:, -1]) / np.shape(newDataMat)[0])


# Bagging
# 输入:
#       allDataMat:全部样本的全部特征向量构成的矩阵,即X
#       allLabelMat:全部样本的标记构成的向量,即Y
#       T:基学习器数量
# 输出:
#       treeList:所有基学习器构成的列表,即Tree1、Tree2、...、TreeT
#       generalizationErr:泛化误差(采用包外估计OOB)
def bagging(allDataMat, allLabelMat, T=200):
    m = np.size(allDataMat, axis=0)
    treeList = []
    estimateLabelMat = np.mat(np.zeros((m, T)))    # 构造矩阵,保存每个基学习器的包外估计
    averageTestingAcc = 0.0     # 基学习器的测试误差均值
    averageTranningAcc = 0.0    # 基学习器的训练误差均值
    for i in range(T):
        sampleDataMat, sampleLabelMat, restSampleIndex = bootstrap_Sample(allDataMat, allLabelMat)
        dataMat = np.hstack((sampleDataMat, sampleLabelMat))
        myTree = createTree(dataMat, choose_best_split_method=choose_best_split)
        Y_hat = estimate_newMat(myTree, dataMat)
        trainingAcc = np.sum(Y_hat == dataMat[:, -1]) / np.shape(dataMat)[0]
        print("The ", i+1, " Base Learner's Training Set Accuracy: ", trainingAcc)
        while trainingAcc <= 0.5:   # 确保每个基学习器满足条件
            print("This Base Learner dissatisfy Base Learner's Request(trainingAcc > 0.5)!")
            sampleLabelMat, sampleDataMat, restSampleIndex = bootstrap_Sample(allDataMat, allLabelMat)
            dataMat = np.hstack((sampleDataMat, sampleLabelMat))
            myTree = createTree(dataMat, choose_best_split_method=choose_best_split)
            Y_hat = estimate_newMat(myTree, dataMat)
            trainingAcc = np.sum(Y_hat == dataMat[:, -1]) / np.shape(dataMat)[0]
            print("The ", i+1, " Base Learner's Training Set Accuracy: ", trainingAcc)
        newDataMat = np.hstack((allDataMat[restSampleIndex, :], allLabelMat[restSampleIndex, :]))   # 未被选中的原始样本
        Y_hat = estimate_newMat(myTree, newDataMat)
        testingAcc = np.sum(Y_hat == newDataMat[:, -1]) / np.shape(newDataMat)[0]
        print("The ", i+1, " Base Learner's Testing Set Accuracy: ", testingAcc)
        averageTestingAcc += testingAcc / T
        averageTranningAcc += trainingAcc / T
        Y_hattmp = np.zeros((m, 1))
        Y_hattmp[restSampleIndex, ] = Y_hat
        estimateLabelMat[:, i] = np.mat(Y_hattmp)
        treeList.append(myTree)
    labelNum = len(set(allLabelMat.T.tolist()[0]))
    statisticsMat = np.mat(np.zeros((m,labelNum)))
    for i in range(labelNum):
        statisticsMat[:, i] = np.sum(estimateLabelMat == i+1, axis=1)
    estimateLabel = np.argmax(statisticsMat, axis=1) + 1   # 采用投票法(非加权)进行包外估计
    generalizationErr = 1 - np.sum(estimateLabel == allLabelMat)/m  # 计算泛化误差
    print("--------------------------------------------------------------------")
    print("Base Learners' Average Testing Error: ", 1-averageTestingAcc)
    print("Base Learners' Average Tranning Error: ", 1-averageTranningAcc)
    print("Bagging's Generalization Error: ", generalizationErr)
    return treeList


startT = time.time()
treeList = bagging(allDataMat, allLabelMat)
stopT = time.time()
print(treeList)
print(stopT - startT)


# 选择最优划分属性和划分值(RandomForest要求在选择每个结点最优划分属性时,只考虑随机抽取到的属性)
# 输入:
#       dataMat:数据集,包括每个样本的特征向量与标记
#       toln:一个节点最少含有的样本数
#       sampleFeatNum:属性子集大小k=log2(d)+1
# 输出:
#       best_featIndex:最佳划分属性的索引
#       best_featValue:最佳划分值
def choose_best_split_RF(dataMat, toln, sampleFeatNum=3):
    best_featIndex = -1
    best_featValue = 0.0
    best_S = np.inf
    allIndex = [i for i in range(np.shape(dataMat)[1] - 1)]
    for featIndex in random.sample(allIndex, sampleFeatNum):
        for featValue in set(dataMat[:, featIndex].T.tolist()[0]):
            ret_dataMat_g, ret_dataMat_l = split_dataSet(dataMat, featIndex, featValue)
            if np.shape(ret_dataMat_g)[0] < toln or\
                    np.shape(ret_dataMat_l)[0] < toln:  # 如果划分后的两个子结点其中有一
                                                        # 个不满足最低的样本数限制,就直接计算当前结点的总误差
                current_S = reg_error(dataMat)
            else:
                current_S = reg_error(ret_dataMat_l) + reg_error(ret_dataMat_g)
            if current_S < best_S:
                best_S = current_S
                best_featIndex = featIndex
                best_featValue = featValue
    return best_featIndex, best_featValue


# RF
# 输入:
#       allDataMat:全部样本的全部特征向量构成的矩阵,即X
#       allLabelMat:全部样本的标记构成的向量,即Y
#       T:基学习器数量
# 输出:
#       treeList:所有基学习器构成的列表,即Tree1、Tree2、...、TreeT
#       generalizationErr:泛化误差(采用包外估计OOB)
def RandomForest(allDataMat, allLabelMat, T=200):
    m = np.size(allDataMat, axis=0)
    treeList = []
    estimateLabelMat = np.mat(np.zeros((m, T)))    # 构造矩阵,保存每个基学习器的包外估计
    averageTestingAcc = 0.0     # 基学习器的测试误差均值
    averageTranningAcc = 0.0    # 基学习器的训练误差均值
    for i in range(T):
        sampleDataMat, sampleLabelMat, restSampleIndex = bootstrap_Sample(allDataMat, allLabelMat)
        dataMat = np.hstack((sampleDataMat, sampleLabelMat))
        myTree = createTree(dataMat, choose_best_split_method=choose_best_split_RF, sampleFeatNum=3)
        Y_hat = estimate_newMat(myTree, dataMat)
        trainingAcc = np.sum(Y_hat == dataMat[:, -1]) / np.shape(dataMat)[0]
        print("The ", i+1, " Base Learner's Training Set Accuracy: ", trainingAcc)
        while trainingAcc <= 0.5:   # 确保每个基学习器满足条件
            print("This Base Learner dissatisfy Base Learner's Request(trainingAcc > 0.5)!")
            sampleLabelMat, sampleDataMat, restSampleIndex = bootstrap_Sample(allDataMat, allLabelMat)
            dataMat = np.hstack((sampleDataMat, sampleLabelMat))
            myTree = createTree(dataMat, choose_best_split_method=choose_best_split_RF, sampleFeatNum=3)
            Y_hat = estimate_newMat(myTree, dataMat)
            trainingAcc = np.sum(Y_hat == dataMat[:, -1]) / np.shape(dataMat)[0]
            print("The ", i+1, " Base Learner's Training Set Accuracy: ", trainingAcc)
        newDataMat = np.hstack((allDataMat[restSampleIndex, :], allLabelMat[restSampleIndex, :]))   # 未被选中的原始样本
        Y_hat = estimate_newMat(myTree, newDataMat)
        testingAcc = np.sum(Y_hat == newDataMat[:, -1]) / np.shape(newDataMat)[0]
        print("The ", i+1, " Base Learner's Testing Set Accuracy: ", testingAcc)
        averageTestingAcc += testingAcc / T
        averageTranningAcc += trainingAcc / T
        Y_hattmp = np.zeros((m, 1))
        Y_hattmp[restSampleIndex, ] = Y_hat
        estimateLabelMat[:, i] = np.mat(Y_hattmp)
        treeList.append(myTree)
    labelNum = len(set(allLabelMat.T.tolist()[0]))
    statisticsMat = np.mat(np.zeros((m, labelNum)))
    for i in range(labelNum):
        statisticsMat[:, i] = np.sum(estimateLabelMat == i+1, axis=1)
    estimateLabel = np.argmax(statisticsMat, axis=1) + 1   # 采用投票法(非加权)进行包外估计
    generalizationErr = 1 - np.sum(estimateLabel == allLabelMat)/m  # 计算泛化误差
    print("--------------------------------------------------------------------")
    print("Base Learners' Average Testing Error: ", 1-averageTestingAcc)
    print("Base Learners' Average Tranning Error: ", 1-averageTranningAcc)
    print("RF's Generalization Error: ", generalizationErr)
    return treeList


startT = time.time()
treeList = RandomForest(allDataMat, allLabelMat)
stopT = time.time()
print(treeList)
print(stopT - startT)

算法效果

  文章之前提到过,对原始数据集进行bootstrap构建新数据集,有大约36.8%的原始样本没有被选到,在实验中对这一点进行了验证,下图是构建某个基学习器时的boostrap对原始数据的采样率,由于篇幅限制,这里只截取了其中一个基学习器的具体训练情况,训练其他基学习器时的boostrap采样率也大致围绕在66%上下。

  上图展现出来的另一个问题是基学习器的泛化性能,可以看出基学习器在训练集上的误差与测试集上的误差率之差高达10%,当然这不代表所有基学习器的情况,但是由于过拟合导致泛化性能较低的情况依旧存在。

RandomForest详解(附带详细公式推导)_第5张图片

  上图是Bagging的训练情况,采用200棵CART树作为基学习器,可以看出基学习器的训练误差平均值与测试误差平均值差了4.5%,其中不乏有一些泛化性能较低的基学习器,因此这些基学习器的训练误差与测试误差差别更大,采用Bagging后,训练误差与泛化误差之间的误差率降低到3.2%,在数据集上的分类正确率达到95.3%,因此,可以证明Bagging可以有效的提高泛化性能。

RandomForest详解(附带详细公式推导)_第6张图片

  上图是RandomForest的训练情况,同样采用200棵CART树作为基学习器,由于RandomForest在训练基学习器的过程中,加入了输入属性随机性(在选择CART结点时只考虑几个随机属性),因此可以看出平均训练误差比之前Bagging中的略高一点点,这里我偷了点懒,根据上文介绍,RandomForest实际上有两种实现方式:Random Input和Random Combin,在原始属性个数较少时,应该采用RandomForest-RC这种方式,由于本代码使用的数据集的属性只有4个,设置的随机属性个数为2,因此泛化性能只是得到了略微提高,从最终结果中可以看出,RF将误差的差值从4.5%降低到2.3%。
  需要注意的是,泛化性能随着基学习器的数量增多,总体上会呈现上升的趋势,但是当数量过多时,这种提升会变得不那么明显,另外,基学习器性能的降低也会影响集成学习器的性能,大家有兴趣的话可以在其他数据集上进行试验。最后说明一点,由于RF在训练CART的过程中将最优划分的搜索范围缩小了,因此运行时间要少于Bagging,例如前两幅图中,Bagging的训练时间是10.12s,RF则是7.64s。Sklearn中也有实现好的RandomForest算法,网上的教程比较多,这里就不做展示了,我的这个代码主要是帮助大家理解其中的基本训练过程,如果没有在其他数据集上体现出期望效果请见谅。

数据集

IRIS.data



(更新)RF如何评估特征重要性

  在构建RF的基学习器时,常常会遇到树结点划分问题,这时需要对数据集特征的重要性进行评估,主要有以下两种方法:
  1) 基于基尼系数,也是CART树节点划分的基本准则(与该方法类似,本文采用的是CART模型树,因此使用加权回归误差评估结点纯度,使用CART分类树模型时,即可采用基尼系数,见《C&RT(CART)详解》);
  2)基于袋外数据(OOB)对当前模型进行误差评估,得到误差值E,然后随机打乱袋外数据的某个特征下的特征值,使用打乱后的数据对模型进行误差评估,得到误差值e,即可根据E与e之间的差值,评估特征对模型的重要性。其中,数据打乱的方法有:
    a.在打乱某个样本点时,基于均匀分布\高斯分布对该特征所有出现过的值进行随机采样,并替换该样本点对应特征处的值;
    b.对该特征下数据集中的特征值出现顺序重新打乱,即对该特征进行置换检验(Permutation Test),这种方法的好处是,可以保证该特征下特征值的稳定分布。

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