R-集成学习实用指南-全-
R 集成学习实用指南(全)
原文:
annas-archive.org/md5/80cdff39151999bd78d34d1c7cef3132译者:飞龙
前言
集成学习!这个机器学习的专门主题广泛涉及将多个模型组合起来,旨在提供更高的准确性和稳定的模型性能。集成方法论基于坚实的理论,其应用已在复杂的数据科学场景中取得了成功。本书抓住了处理这个重要主题的机会。
本书在整本书中使用了中等规模的数据库。所有概念——好吧,大多数概念——都通过软件进行了说明,并且广泛使用了 R 包来阐述观点。虽然已经尽力确保所有代码无错误,但请随时向我们报告代码中的任何错误或问题。该方法已经通过基于早期草稿的两个迷你课程进行了轻微验证。同事们对材料给予了好评,这给了我足够的信心来完成本书。
Packt 编辑团队在技术审阅方面给予了很大帮助,经过多次精炼,手稿终于呈现在您面前。错误和不足之处归咎于作者。
本书面向对象
本书面向任何希望通过使用 R 的强大功能构建集成模型来掌握机器学习的人。为了充分利用本书,需要具备机器学习技术的基本知识和 R 编程知识。
本书涵盖内容
第一章,集成技术简介,将阐述集成学习的必要性、重要数据集、基本统计和机器学习模型以及重要的统计检验。本章展示了本书的精神。
第二章,自举,将介绍两个重要概念:刀切法和自举法。本章将帮助您进行与未知复杂参数相关的统计推断。通过 R 程序展示了线性回归、生存分析和时间序列等基本统计模型的自举,更重要的是,它为形成集成方法核心的重新采样技术奠定了基础。
第三章,Bagging,将提出第一个使用决策树作为基础模型的集成方法。Bagging 是bootstrap aggregation(自举聚合)这两个词的组合。本章展示了决策树剪枝的示例,并为后续章节奠定所需的基础。本章还展示了决策树和 k-NN 分类器的 Bagging。
第四章, 随机森林,将讨论决策树的重要集合扩展。变量重要性和邻近性图是随机森林的两个重要组成部分,我们对其进行了相关计算。随机森林的细微之处将深入解释。本章还处理了与袋装方法、缺失数据插补以及使用随机森林进行聚类的比较。
第五章, 纯粹的提升算法,首先陈述提升算法。使用玩具数据,本章随后解释了自适应提升算法的详细计算。然后,梯度提升算法用于回归问题。gbm和adabag包的使用展示了其他提升算法的实现。本章以袋装、随机森林和提升方法的比较结束。
第六章, 提升细化,将从提升技术的运作原理进行解释。随后,梯度提升算法被扩展到计数和生存数据集。流行的梯度提升算法的极梯度提升实现细节通过清晰的程序展示。本章以h2o包的重要概述结束。
第七章, 通用集合技术,将研究集合技术成功背后的概率原因。对于分类和回归问题,解释了集合技术的成功。
第八章, 集合诊断,将探讨集合多样性的条件。在此展示了分类器的成对比较和整体评分者间一致性度量。
第九章, 集合回归模型,将详细讨论在回归问题中使用集合方法。这里使用了一个来自kaggle的复杂住房数据集。回归数据使用多个基学习器进行建模。袋装、随机森林、提升和堆叠在回归数据中都有所展示。
第十章的Ensembling Survival Models涉及生存数据。生存分析的概念得到了相当详细的阐述,并展示了传统技术。介绍了生存树的学习方法,然后我们为这种数据结构构建了随机生存森林的集成方法。
第十一章的Ensembling Time Series Models处理另一种特殊的数据结构,其中观测值相互依赖。时间序列的核心概念和基本相关模型得到发展。介绍了特殊时间序列模型的 Bagging,并以异构时间序列模型的集成结束本章。
第十二章的What's Next?将讨论集成学习中的一些未解决的问题和未来工作的范围。
要充分利用本书
-
R 的官方网站是位于www.cran.r-project.org的综合 R 档案网络(CRAN)。在撰写本书时,R 的最新版本是 3.5.1。此软件适用于三个平台:Linux、macOS 和 Windows。读者还可以下载一个很好的前端,例如 RStudio。
-
每一章都有一个标题部分,标题为技术要求。它列出了运行该章节代码所需的 R 包。例如,第三章的Bagging要求如下:
-
class
-
FNN
-
ipred
-
mlbench
-
rpart
-
然后,读者需要在 R 控制台中运行以下行来安装所有这些包:
install.packages("class")
install.packages("mlbench")
install.packages("FNN")
install.packages("rpart")
install.packages("ipred")
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在
www.packtpub.com登录或注册。 -
选择支持选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入本书的名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本解压缩或提取文件夹:
-
Windows 的 WinRAR / 7-Zip
-
Mac 的 Zipeg / iZip / UnRarX
-
Linux 的 7-Zip / PeaZip
本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-R。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供其他代码包,这些代码包来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供包含本书中使用的截图/图表的彩色 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnEnsembleLearningwithR_ColorImages.pdf。
使用的约定
本书使用了许多文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“使用dexp函数计算密度函数值的计算。”
代码块设置如下:
> Events_Prob <- apply(Elements_Prob,1,prod)
> Majority_Events <- (rowSums(APC)>NT/2)
> sum(Events_Prob*Majority_Events)
[1] 0.9112646
粗体:表示新术语、重要单词或您在屏幕上看到的单词,例如在菜单或对话框中,这些单词在文本中也以这种方式出现。例如:“从管理面板中选择系统信息。”
注意
警告或重要注意事项以这种方式出现在框中。
小贴士
小技巧和技巧以这种方式出现。
联系我们
我们读者的反馈总是受欢迎的。
总体反馈:请发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请通过电子邮件联系我们,地址为 <questions@packtpub.com>。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件联系我们,地址为 <copyright@packtpub.com>,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packtpub.com。
第一章。集成技术简介
集成技术是模型输出聚合技术,在过去十多年中在统计和机器学习领域得到了发展。这构成了本书的中心主题。任何统计模型和机器学习工具的用户都会熟悉构建模型的问题以及从潜在候选模型中选择的关键决策。模型的准确性当然不是唯一的相关标准;我们还关心其复杂性,以及整体模型是否具有实际意义。
常见的建模问题包括选择模型的决定,并且存在各种方法来帮助这项任务。在统计学中,我们求助于诸如赤池信息准则(AIC)和贝叶斯信息准则(BIC)这样的度量,而在其他方面,与拟合模型中的变量相关的 p 值有助于做出决策。这个过程通常被称为模型选择。岭回归惩罚、Lasso 和其他统计方法也有助于这项任务。对于机器学习模型,如神经网络、决策树等,当模型使用称为训练数据的数据的一部分构建时,k 折交叉验证是有用的,然后我们会在未训练区域或验证数据中寻找准确性。如果模型对其复杂性敏感,这种练习可能是徒劳的。
获得最佳模型的过程意味着我们创建了一大批其他模型,这些模型本身几乎与最佳模型一样高效。此外,最佳模型准确地覆盖了大多数样本,而其他模型可能准确地评估了它不准确变量的空间区域。因此,我们可以看到,最终入围的模型与第二名相比优势很少。接下来的模型并不那么糟糕,以至于可以完全拒绝。这使得有必要找到一种方法,将已从模型中获得的大部分结果以有意义的方式结合起来。寻找将各种模型组合起来的方法是集成学习的主要目标。或者可以说,集成学习将竞争模型转化为协作模型。实际上,集成技术并不是建模练习的终点,因为它们还将扩展到无监督学习问题。我们将演示一个例子,以证明这种需要的合理性。
没有现代计算能力的发明,集成方法的实现将是不可能的。统计方法预见到了需要巨大计算的技术。如排列检验和 Jackknife 等方法是计算能力有效性的证据。我们将在本章后面进行一项练习来学习这些方法,并在本书后面的部分重新审视它们。
从机器学习的角度来看,监督学习和无监督学习是两种主要的学习方法。监督学习是机器学习的一个分支,其过程是已知某个变量,目的是通过其他各种变量来理解这个变量。在这里,我们有一个目标变量。由于学习是在输出变量的基础上进行的,因此监督学习有时被称为有教师的学习。并非所有目标变量都是相同的,它们通常属于以下四种类型之一。如果目标是把观察结果分类到k种类型之一(例如,是/否,满意/不满意),那么我们有一个分类问题。这样的变量在统计学中被称为分类变量。感兴趣的变量可能是一个连续变量,从软件的角度来看是数值的。这可能包括每升油的汽车里程数,一个人的收入,或一个人的年龄。对于这样的场景,机器学习问题的目的是通过其他相关变量来学习变量,然后预测未知案例,在这些案例中,只有相关变量的值是可用的。我们将广泛地将这类问题称为回归问题。
在临床试验中,事件发生的时间通常是一个关注的焦点。当疾病被诊断出来时,我们会询问所提出的药物是否比现有的药物有所改进。在这里,所讨论的变量是诊断和死亡之间的时间长度,而临床试验数据会带来几个其他问题。分析不能等到所有患者都去世,或者有些患者可能已经离开了研究,使得我们无法再了解他们的状态。因此,我们得到了截尾数据。作为研究观察的一部分,完整的信息并不总是可用。生存分析主要处理这类问题,我们将在这里着手解决创建集成模型的问题。
对于分类、回归和生存数据,可以假设实例/观察是相互独立的。这是一个非常合理的假设,因为有一个合理的理由相信患者会对药物的反应是独立的,客户会流失或支付贷款也是独立的,等等。在另一个重要的问题类别中,这个假设并不成立,我们面临的是通过时间序列数据相互依赖的观察。时间序列数据的一个例子是某公司股票交易所的收盘点。显然,一家公司股票的表现不可能每天都是独立的,因此我们需要考虑依赖性。
在许多实际问题中,目标是理解模式或找到观察值的组,我们没有关于需要训练哪个算法的具体变量。找到组或聚类被称为无监督学习或无导师学习。在寻找聚类时出现的两个主要实际问题包括:(i) 通常事先不知道总体中有多少个聚类,以及(ii) 不同初始聚类中心的选择会导致不同的解决方案。因此,我们需要一个不受初始化影响,或者至少对初始化不敏感的解决方案,并考虑每个有用解决方案的优点。这将引导我们走向无监督集成技术。
寻找最佳模型,无论是监督学习还是无监督学习,常常受到异常值的存在而受阻。已知单个异常值会严重影响线性模型的整体拟合度,而且对非线性模型也有显著影响。异常值检测本身就是一项挑战,大量的统计方法有助于识别异常值。众多机器学习方法也有助于识别异常值。当然,集成方法在这里也会有所帮助,我们将开发 R 程序来帮助解决识别异常值的问题。这种方法将被称作异常值集成。
在一开始,读者熟悉本书中使用的数据集非常重要。所有主要数据集将在第一部分介绍。我们首先简要介绍核心的统计/机器学习模型,并在之后立即将其付诸实践。很快就会明显看出,没有一种模型会比其他模型表现得更好。如果存在这样的解决方案,我们就不需要集成技术了。
在本章中,我们将涵盖:
-
数据集:本书中将使用的核心数据集
-
统计/机器学习模型:这里将解释重要的分类模型
-
正确模型困境:没有占主导地位的模型
-
集成概述:集成方法的需求
-
互补统计测试:这里将讨论对模型比较有用的重要统计测试
本章将需要以下 R 包:
-
ACSWR -
caret -
e1071 -
factoextra -
mlbench -
NeuralNetTools -
perm -
pROC -
RSADBE -
Rpart -
survival -
nnet
数据集
数据无疑是机器学习最重要的组成部分。如果没有数据,我们就不会有共同的目标。在大多数情况下,收集数据的目的本身就定义了问题本身。正如我们所知,变量可能属于几种类型,其存储和组织方式也非常重要。
李和艾尔德(1997)考虑了一系列数据集,并引入了集成模型的需求。我们将首先查看他们论文中考虑的数据集的细节,然后在本书的后续部分介绍其他重要数据集。
甲状腺功能减退
甲状腺功能减退数据集 Hypothyroid.csv 可在本书的代码包中找到,位于 /…/Chapter01/Data。虽然数据集中有 26 个变量,但我们只会使用其中的 7 个变量。在这里,观测值的数量是 n = 3163。数据集是从 archive.ics.uci.edu/ml/datasets/thyroid+disease 下载的,文件名为 hypothyroid.data (archive.ics.uci.edu/ml/machine-learning-databases/thyroid-disease/hypothyroid.data)。经过对某些值重新标记顺序的调整后,CSV 文件被包含在本书的代码包中。研究的目的是根据其他变量提供的信息对患有甲状腺问题的患者进行分类。数据集有多种变体,读者可以在以下网页深入了解细节:archive.ics.uci.edu/ml/machine-learning-databases/thyroid-disease/HELLO。在这里,代表感兴趣变量的列被命名为 Hypothyroid,这表明我们有 151 名患有甲状腺问题的患者。其余 3012 名测试结果为阴性。显然,这个数据集是 不平衡数据 的一个例子,这意味着两种情况中的一种数量远远超过另一种;对于每个甲状腺病例,我们大约有 20 个阴性病例。这类问题需要不同的处理方式,我们需要深入算法的细微差别来构建有意义的模型。在构建预测模型时,我们将使用的额外变量或协变量包括 Age(年龄)、Gender(性别)、TSH(促甲状腺激素)、T3(三碘甲状腺原氨酸)、TT4(总甲状腺素)、T4U(甲状腺素自由指数)和 FTI(游离三碘甲状腺原氨酸)。数据首先被导入 R 会话,并根据感兴趣的变量进行子集化,如下所示:
> HT <- read.csv("../Data/Hypothyroid.csv",header = TRUE,stringsAsFactors = F)
> HT$Hypothyroid <- as.factor(HT$Hypothyroid)
> HT2 <- HT[,c("Hypothyroid","Age","Gender","TSH","T3","TT4","T4U","FTI")]
第一行代码使用 read.csv 函数从 Hypothyroid.csv 文件中导入数据。现在数据集中变量有很多缺失数据,如下所示:
> sapply(HT2,function(x) sum(is.na(x)))
Hypothyroid Age Gender TSH T3 TT4
0 446 73 468 695 249
T4U FTI
248 247
因此,我们删除了所有包含缺失值的行,然后将数据分为训练集和测试集。我们还将为分类问题创建一个公式:
> HT2 <- na.omit(HT2)
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(HT2),replace=TRUE, prob=c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> HT2_Train <- HT2[Train_Test=="Train",]
> HT2_TestX <- within(HT2[Train_Test=="Test",],rm(Hypothyroid))
> HT2_TestY <- HT2[Train_Test=="Test",c("Hypothyroid")]
> HT2_Formula <- as.formula("Hypothyroid~.")
set.seed 函数确保每次运行程序时结果都是可重复的。在用 na.omit 函数删除缺失观测值后,我们将甲状腺数据分为训练集和测试集。前者用于构建模型,后者用于验证,使用的是未用于构建模型的数据。Quinlan – C4.5 算法的发明者 – 广泛使用了这个数据集。
波形
这个数据集是一个模拟研究的例子。在这里,我们有二十一个变量作为输入或独立变量,以及一个被称为classes的类别变量。数据是通过mlbench R 包中的mlbench.waveform函数生成的。更多详情,请参考以下链接:ftp://ftp.ics.uci.edu/pub/machine-learning-databases。我们将为这个数据集模拟 5000 个观测值。如前所述,set.seed函数保证了可重复性。由于我们正在解决二元分类问题,我们将波形函数生成的三个类别减少到两个,然后为了模型构建和测试目的,将数据分为训练集和测试集:
> library(mlbench)
> set.seed(123)
> Waveform <- mlbench.waveform(5000)
> table(Waveform$classes)
1 2 3
1687 1718 1595
> Waveform$classes <- ifelse(Waveform$classes!=3,1,2)
> Waveform_DF <- data.frame(cbind(Waveform$x,Waveform$classes)) # Data Frame
> names(Waveform_DF) <- c(paste0("X",".",1:21),"Classes")
> Waveform_DF$Classes <- as.factor(Waveform_DF$Classes)
> table(Waveform_DF$Classes)
1 2
3405 1595
R 函数mlbench.waveform创建了一个新的mlbench类对象。由于它由x和classes两个子部分组成,我们将通过一些进一步的操作将其转换为data.frame。cbind函数将两个对象x(一个矩阵)和classes(一个数值向量)绑定成一个单一的矩阵。data.frame函数将矩阵对象转换成数据框,这是程序其余部分所需的数据类型。
数据分区后,我们将为波形数据集创建所需的formula:
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(Waveform_DF),replace = TRUE,
+ prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> Waveform_DF_Train <- Waveform_DF[Train_Test=="Train",]
> Waveform_DF_TestX <- within(Waveform_DF[Train_Test=="Test",],rm(Classes))
> Waveform_DF_TestY <- Waveform_DF[Train_Test=="Test","Classes"]
> Waveform_DF_Formula <- as.formula("Classes~.")
德国信贷
贷款并不总是全额偿还,存在违约者。在这种情况下,银行根据可用的信息识别潜在的违约者变得很重要。在这里,我们改编了RSADBE包中的GC数据集,以正确反映因子变量的标签。转换后的数据集作为GC2.RData在数据文件夹中可用。GC数据集本身主要是对archive.ics.uci.edu/ml/datasets/statlog+(german+credit+data)上可用的版本的一种改编。在这里,我们有 1000 个观测值,以及 20 个协变量/独立变量,如现有支票账户的状态、期限等。贷款是否完全偿还的最终状态在good_bad列中可用。我们将数据分为训练集和测试集,并创建公式:
> library(RSADBE)
> load("../Data/GC2.RData")
> table(GC2$good_bad)
bad good
300 700
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(GC2),replace = TRUE,prob=c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> GC2_Train <- GC2[Train_Test=="Train",]
> GC2_TestX <- within(GC2[Train_Test=="Test",],rm(good_bad))
> GC2_TestY <- GC2[Train_Test=="Test","good_bad"]
> GC2_Formula <- as.formula("good_bad~.")
Iris
Iris 可能是最著名的分类数据集。伟大的统计学家 R.A.费舍尔使这个数据集流行起来,他使用这个数据集根据花瓣和萼片的长度和宽度测量来对三种类型的iris植物进行分类。费舍尔使用这个数据集开创了统计分类器线性判别分析的发端。由于有三种iris物种,我们将这个问题转化为二元分类问题,分离了数据集,并创建了如这里所见到的公式:
> data("iris")
> ir2 <- iris
> ir2$Species <- ifelse(ir2$Species=="setosa","S","NS")
> ir2$Species <- as.factor(ir2$Species)
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(ir2),replace = TRUE,prob=c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> ir2_Train <- ir2[Train_Test=="Train",]
> ir2_TestX <- within(ir2[Train_Test=="Test",],rm(Species))
> ir2_TestY <- ir2[Train_Test=="Test","Species"]
> ir2_Formula <- as.formula("Species~.")
皮马印第安人糖尿病
糖尿病是一种健康危害,通常是不可治愈的,被诊断出患有这种病的患者必须调整他们的生活方式以适应这种状况。基于pregnant、glucose、pressure、triceps、insulin、mass、pedigree和age等变量,这里的问题是判断一个人是否患有糖尿病。在这里,我们有 768 个观测值。这个数据集来自mlbench包:
> data("PimaIndiansDiabetes")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(PimaIndiansDiabetes),replace = TRUE,
+ prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> PimaIndiansDiabetes_Train <- PimaIndiansDiabetes[Train_Test=="Train",]
> PimaIndiansDiabetes_TestX <- within(PimaIndiansDiabetes[Train_Test=="Test",],
+ rm(diabetes))
> PimaIndiansDiabetes_TestY <- PimaIndiansDiabetes[Train_Test=="Test","diabetes"]
> PID_Formula <- as.formula("diabetes~.")
到目前为止描述的五个数据集都是分类问题。我们分别看一个回归、时间序列、生存、聚类和异常值检测问题的例子。
美国犯罪
在这里,对美国 47 个不同州的每百万人口犯罪率的研究被进行,并试图找出它与 13 个变量的依赖关系。这些包括年龄分布、南方州的指标、平均在校年数等。与早期数据集一样,我们也将这个数据集分成以下 R 程序块:
> library(ACSWR)
Warning message:
package 'ACSWR' was built under R version 3.4.1
> data(usc)
> str(usc)
'data.frame': 47 obs. of 14 variables:
$ R : num 79.1 163.5 57.8 196.9 123.4 ...
$ Age: int 151 143 142 136 141 121 127 131 157 140 ...
$ S : int 1 0 1 0 0 0 1 1 1 0 ...
$ Ed : int 91 113 89 121 121 110 111 109 90 118 ...
$ Ex0: int 58 103 45 149 109 118 82 115 65 71 ...
$ Ex1: int 56 95 44 141 101 115 79 109 62 68 ...
$ LF : int 510 583 533 577 591 547 519 542 553 632 ...
$ M : int 950 1012 969 994 985 964 982 969 955 1029 ...
$ N : int 33 13 18 157 18 25 4 50 39 7 ...
$ NW : int 301 102 219 80 30 44 139 179 286 15 ...
$ U1 : int 108 96 94 102 91 84 97 79 81 100 ...
$ U2 : int 41 36 33 39 20 29 38 35 28 24 ...
$ W : int 394 557 318 673 578 689 620 472 421 526 ...
$ X : int 261 194 250 167 174 126 168 206 239 174 ...
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(usc),replace = TRUE,prob=c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> usc_Train <- usc[Train_Test=="Train",]
> usc_TestX <- within(usc[Train_Test=="Test",],rm(R))
> usc_TestY <- usc[Train_Test=="Test","R"]
> usc_Formula <- as.formula("R~.")
在本节迄今为止讨论的每个示例中,我们都有理由相信观测值之间是独立的。这个假设简单来说就是,一个观测值的回归因变量和回归自变量与其他观测值的回归因变量和回归自变量之间没有关系。这是一个简单且合理的假设。我们还有另一类观测值/数据集,其中这样的假设并不实用。例如,一天的最高气温并不完全独立于前一天的温度。如果是这样的话,我们可能会经历一个酷热无比的日子,接着是冬天,然后又是另一个炎热的夏天,接着是一个非常大雨的天气。然而,天气并不以这种方式发生,因为连续几天,天气依赖于前一天。在下一个例子中,我们考虑新西兰的海外游客数量。
海外游客
新西兰海外数据集在 Tattar 等人(2017)的第十章中进行了详细处理。在这里,海外游客的数量从 1977 年 1 月到 1995 年 12 月按月记录。我们有超过 228 个月的游客数据。osvisit.dat文件可在多个网页链接中找到,包括www.stat.auckland.ac.nz/~ihaka/courses/726-/osvisit.dat和github.com/AtefOuni/ts/blob/master/Data/osvisit.dat。它也包含在书籍的代码包中。我们将使用 R 导入数据,将其转换为时间序列对象,并可视化:
> osvisit <- read.csv("../Data/osvisit.dat", header= FALSE)
> osv <- ts(osvisit$V1, start = 1977, frequency = 12)
> class(osv)
[1] "ts"
> plot.ts(osv)

图 1:新西兰海外游客
在这里,数据集没有被分割!时间序列数据不能任意分割成训练和测试部分。原因很简单:如果我们有五个按时间顺序排列的观察结果 y1,y2,y3,y4,y5,并且我们相信影响顺序是 y1→y2→y3→y4→y5,任意分割 y1,y2,y5 将会有不同的行为。它不会与三个连续观察结果具有相同的信息。因此,时间序列分割必须保留依赖结构;我们保留最近的部分时间作为测试数据。对于五个观察结果的例子,我们选择 y1,y2,y3 作为测试数据。分割很简单,我们将在第十一章,集成时间序列模型中介绍这一点。
实时测试实验很少能得出完整的观察结果。在可靠性分析以及生存分析/临床试验中,单位/患者会被观察到预定义的时间,并记录是否发生了特定事件,这通常是指故障或死亡。相当一部分观察结果在预定的截止时间之前并未发生故障,分析不能等待所有单位都发生故障。缩短研究的原因可能是因为所有单位都会发生故障的时间点可能非常长,并且继续研究到那时会非常昂贵。因此,我们只能得到不完整的观察结果;我们只知道在研究被取消之前,单位的寿命至少持续了预定义的时间,而感兴趣的事件可能在未来的某个时间发生。因此,一些观察结果是截断的,数据被称为截断数据。分析此类数据集需要特殊的统计方法。我们将在下一节给出这些类型数据集的例子,并在第十章,集成生存模型中进行分析。
原发性胆汁性肝硬化
来自生存包的pbc数据集是临床试验领域的一个基准数据集。梅奥诊所收集了这些数据,它涉及肝脏的原发性胆汁性肝硬化(PBC)。该研究是在 1974 年至 1984 年之间进行的。更多详细信息可以通过在 R 终端上运行pbc,然后是library(survival)来找到。在这里,感兴趣事件的主要时间是注册和死亡、移植或 1986 年 7 月的研究分析之间的天数,这被捕获在时间变量中。与生存研究类似,事件可能被截尾,指示器在状态列中。需要理解事件时间,考虑到变量如trt、age、sex、ascites、hepato、spiders、edema、bili、chol、albumin、copper、alk.phos、ast、trig、platelet、protime和stage。
到目前为止讨论的八个数据集都有一个目标变量,或回归因变量,是监督学习问题的例子。另一方面,还有一些实际案例,我们只是试图理解数据,并从中找到有用的模式和组/簇。当然,重要的是要注意,聚类的目的是找到一个相同的组并给它一个合理的标签。例如,如果我们试图根据长度、宽度、马力、发动机排量等特征对汽车进行分组,我们可能会找到被标记为敞篷车、轿车和沙龙车类的组,而另一个聚类解决方案可能会导致基本、高端和运动变体组的标签。聚类中提出的主要问题是组数的选择和形成稳健的簇。我们考虑来自factoextraR 包的一个简单数据集。
多形状
来自factoextra包的multishapes数据集包含三个变量:x、y和shape。它由不同的形状组成,每个形状形成一个簇。在这里,我们有两个并行的圆形形状,两个平行的矩形/床,以及右下角的一个点簇。散点图中也添加了异常值。一些简短的 R 代码提供了有用的显示:
> library(factoextra)
> data("multishapes")
> names(multishapes)
[1] "x" "y" "shape"
> table(multishapes$shape)
1 2 3 4 5 6
400 400 100 100 50 50
> plot(multishapes[,1],multishapes[,2],col=multishapes[,3])

图 2:寻找形状或组
此数据集包含一个名为shape的列,因为它是一个假设数据集。在真正的聚类问题中,我们将没有簇组指示器,也没有仅两个变量的可视化奢侈。在这本书的后面,我们将看到集成聚类技术如何帮助克服确定簇数量和簇成员一致性的问题。
虽然这种情况并不常见,但在微调不同参数、拟合不同模型和其他技巧都未能找到有用的工作模型时,可能会产生挫败感。这种情况的罪魁祸首通常是异常值。已知单个异常值会破坏一个本可能有用的模型,因此它们的检测至关重要。到目前为止,参数和非参数异常值检测一直是专业知识深厚的问题。在复杂场景中,识别将是一项无法逾越的任务。可以使用集成异常值框架达成对观测值是否为异常值的共识。为了考虑这一点,我们将考虑板刚度数据集。我们将在本书的结论部分看到如何确定异常值。
板刚度
板刚度数据集可通过ACSWR包中的stiff数据框获得。该数据集包含 30 块板的四个刚度度量。第一个刚度度量是通过在板上发送冲击波获得的,第二个是通过振动板获得的,其余两个是通过静态测试获得的。在多元数据集中识别异常值的一种快速方法是使用马氏距离函数。观测值离中心的距离越远,该观测值成为异常值的可能性就越大:
> data(stiff)
> sort(mahalanobis(stiff,colMeans(stiff),cov(stiff)),decreasing = TRUE)
[1] 16.8474070168 12.2647549939 9.8980384087 7.6166439053
[5] 6.2837628235 5.4770195915 5.2076098038 5.0557446013
[9] 4.9883497928 4.5767867224 3.9900602512 3.5018290410
[13] 3.3979804418 2.9951752177 2.6959023813 2.5838186338
[17] 2.5385575365 2.3816049840 2.2191408683 1.9307771418
[21] 1.4876569689 1.4649908273 1.3980776252 1.3632123553
[25] 1.0792484215 0.7962095966 0.7665399704 0.6000128595
[29] 0.4635158597 0.1295713581
统计/机器学习模型
上一节通过真实数据集介绍了一系列问题,现在我们将讨论一些标准模型变体,这些变体有助于处理此类问题。首先,我们建立所需的数学框架。
假设我们拥有 n 对独立的观测数据,
,其中
表示感兴趣的随机变量,也称为因变量、内生变量等。
是与解释变量相关的向量,或独立/外生变量。解释向量将包含 k 个元素,即
。实现的数据形式为
,其中
是随机变量
的实现值(数据)。本书将采用一种惯例,即
,这将处理截距项。我们假设观测数据来自真实的分布 F,该分布并不完全已知。一般回归模型,包括分类模型以及回归模型,由以下指定:

在这里,函数 f 是一个未知函数,
是回归参数,它捕捉了
对
的影响。误差
是相关的不可观测误差项。可以应用多种方法来建模 Ys 和 xes 之间的关系。统计回归模型专注于误差分布
的完整指定,通常函数形式会是线性的,如
所示。函数
是广义线性模型类中的连接函数。非参数和半参数回归模型更加灵活,因为我们不对误差的概率分布施加限制。然而,灵活性是有代价的,在这里我们需要更多的观测数据来做出有效的推断,尽管这个数量是不确定的,并且通常是主观的。
机器学习范式包括一些 黑盒 方法,并且这种方法与非参数和半参数模型之间存在健康的重叠。读者也应注意,黑盒并不意味着在任何意义上都是非科学的。这些方法有坚实的数学基础,并且每次都是可复制的。接下来,我们将快速回顾一些最重要的统计和机器学习模型,并通过前面讨论的数据集来展示它们。
逻辑回归模型
逻辑回归模型是一种二元分类模型,它是广义线性模型类中的指数族成员。现在,用
表示二元标签:

使用包含在解释向量
中的信息,我们试图构建一个有助于这项任务的模型。逻辑回归模型如下:

在这里,
是回归系数的向量。请注意,logit 函数
在回归系数上是线性的,因此模型的名称是逻辑回归模型。逻辑回归模型可以等价地写成以下形式:

在这里,
是遵循伯努利分布的二进制误差项。更多信息,请参阅 Tattar 等人(2016 年)的第十七章。逻辑回归参数的估计需要迭代加权最小二乘法(IRLS),我们将使用 R 函数glm来完成这项任务。在本节中,我们将使用甲状腺功能减退症数据集。在前一节中,已经创建了训练和测试数据集以及公式,我们将从那个点继续进行。
甲状腺功能减退症分类的逻辑回归
对于hypothyroid数据集,我们使用HT2_Train作为训练数据集。测试数据集被分为HT2_TestX中的协变量矩阵和测试数据集的输出HT2_TestY,而逻辑回归模型的公式可在HT2_Formula中找到。首先,使用glm函数将逻辑回归模型拟合到训练数据集,并将拟合的模型命名为LR_fit,然后使用summary(LR_fit)检查模型拟合摘要。然后,使用predict函数将拟合的模型应用于测试部分的协变量数据以创建LR_Predict。然后,在LR_Predict_Bin中标记预测概率,并将这些标签与实际的testY_numeric进行比较,从而获得整体准确率:
> ntr <- nrow(HT2_Train) # Training size
> nte <- nrow(HT2_TestX) # Test size
> p <- ncol(HT2_TestX)
> testY_numeric <- as.numeric(HT2_TestY)
> LR_fit <- glm(HT2_Formula,data=HT2_Train,family = binomial())
Warning message:
glm.fit: fitted probabilities numerically 0 or 1 occurred
> summary(LR_fit)
Call:
glm(formula = HT2_Formula, family = binomial(), data = HT2_Train)
Deviance Residuals:
Min 1Q Median 3Q Max
-3.6390 0.0076 0.0409 0.1068 3.5127
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -8.302025 2.365804 -3.509 0.000449 ***
Age -0.024422 0.012145 -2.011 0.044334 *
GenderMALE -0.195656 0.464353 -0.421 0.673498
TSH -0.008457 0.007530 -1.123 0.261384
T3 0.480986 0.347525 1.384 0.166348
TT4 -0.089122 0.028401 -3.138 0.001701 **
T4U 3.932253 1.801588 2.183 0.029061 *
FTI 0.197196 0.035123 5.614 1.97e-08 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 609.00 on 1363 degrees of freedom
Residual deviance: 181.42 on 1356 degrees of freedom
AIC: 197.42
Number of Fisher Scoring iterations: 9
> LR_Predict <- predict(LR_fit,newdata=HT2_TestX,type="response")
> LR_Predict_Bin <- ifelse(LR_Predict>0.5,2,1)
> LR_Accuracy <- sum(LR_Predict_Bin==testY_numeric)/nte
> LR_Accuracy
[1] 0.9732704
从拟合的 GLM(在summary(LR_fit)行之后的输出)的摘要中可以看出,我们有四个显著的变量in Age、TT4、T4U和FTI。使用predict函数,我们将拟合的模型应用于HT2_TestX中的未知测试案例,并与实际值进行比较,发现准确率为 97.33%。因此,逻辑回归在 R 软件中很容易部署。
神经网络
逻辑回归可能看起来受限,因为它只允许通过链接函数对协变量的线性影响。线性假设可能不成立,而且在大多数实际情况下,我们没有足够的信息来指定非线性关系的函数形式。因此,我们只知道很可能存在一个未知的非线性关系。神经网络是逻辑回归的非线性推广,这涉及到两个重要组成部分:隐藏神经元和学习率。我们首先将修订神经网络的结构。
在神经网络中,输入变量被认为是神经元的第一个层次,输出是最终和结论性的神经元层次。神经网络模型的架构可以使用 R 包NeuralNetTools进行可视化。假设我们有三个输入变量和两个隐藏层,每个隐藏层包含两个隐藏神经元。在这里,我们有一个具有四个层次的神经网络。下面的代码段给出了一个具有三个输入变量、两个隐藏层中的两个隐藏神经元和一个输出变量的神经网络结构的可视化:
> library(NeuralNetTools)
> plotnet(rep(0,17),struct=c(3,2,2,1))
> title("A Neural Network with Two Hidden Layers")
我们发现 R 包NeuralNetTools在可视化神经网络结构方面非常有用。使用核心 R 包nnet构建的神经网络也可以使用NeuralNetTools::plotnet函数进行可视化。plotnet函数通过struct选项设置一个神经网络,其结构由第一层三个神经元、第二和第三层每个层两个神经元以及最终输出层的一个神经元组成。弧上的权重在rep(0,17)中设置为零:

图 3:神经网络的架构
在前一个图中,我们看到了神经网络的四层。第一层由B1(偏差)、I1 (X1)、I2 (X2)和I3 (X3)组成。第二层由B2(第一隐藏层的偏差)、H1和H2三个神经元组成。请注意,偏差B2不接收来自第一隐藏层的任何输入。接下来,每个神经元从前一层的每个神经元接收整体输入,在这里是B1、X1、X2和X3。然而,第一隐藏层的H1和H2将接收来自B1、X1、X2和X3的不同聚合输入。网络中的每条弧上都作用着适当的权重,这些权重构成了神经网络的参数;也就是说,第一层H1的到达将类似于

有效到达是通过一个传递函数实现的。传递函数可能是一个恒等函数、S 形函数等等。同样,第一层第二个神经元的到达是

。通过扩展,B2、H1和H2(第一层)将成为第二隐藏层的输入,B3、H1和H2将成为最终输出的输入。在神经网络的每个阶段,我们都有权重。这些权重需要以这种方式确定,即预测输出O1与真实Y1之间的差异尽可能小。请注意,逻辑回归是神经网络的一个特例,可以通过直接移除所有隐藏层和输入层来看到,这会导致输出层。神经网络将被拟合到甲状腺功能减退问题。
甲状腺功能减退分类的神经网络
我们使用同名的nnet函数来设置甲状腺功能减退分类问题的神经网络。公式、训练集和测试集继续与之前相同。准确度计算遵循与逻辑回归中类似的步骤。拟合的神经网络使用NeuralNetTools包中的plotnet图形函数进行可视化:
> set.seed(12345)
> NN_fit <- nnet(HT2_Formula,data = HT2_Train,size=p,trace=FALSE)
> NN_Predict <- predict(NN_fit,newdata=HT2_TestX,type="class")
> NN_Accuracy <- sum(NN_Predict==HT2_TestY)/nte
> NN_Accuracy
[1] 0.9827044025
> plotnet(NN_fit)
> title("Neural Network for Hypothyroid Classification")
在这里,准确度为 98.27%,这比逻辑回归模型有所提高。拟合模型的视觉展示如下图所示。我们使用set.seed(12345)将神经网络参数的随机初始化固定在12345,以确保结果在读者端可重复。这是一个有趣的集成建模案例。不同的初始种子(读者可以尝试)会导致不同的准确度。有时,你可能会得到低于本节中考虑的任何模型的准确度,而在其他时候,你可能会得到最高的准确度。种子选择的任意性导致了一个重要的问题:哪种解决方案是有用的。由于种子是任意的,因此不会出现好种子或坏种子的问题。在这种情况下,如果一个模型给你更高的准确度,这并不一定意味着什么:

图 4:甲状腺功能减退症分类的神经网络
朴素贝叶斯分类器
基于贝叶斯公式的朴素贝叶斯分类器是一种简单的实现。它基于简单的经验概率和条件概率,这在实际数据中得到了体现。除了观察独立性的最简单假设之外,我们在使用此模型时没有任何限制。
用于甲状腺功能减退症分类的朴素贝叶斯
朴素贝叶斯分类器使用来自e1071 R 包的naiveBayes函数进行拟合。预测和准确度评估是通过两个函数predict和sum完成的:
> NB_fit <- naiveBayes(HT2_Formula,data=HT2_Train)
> NB_predict <- predict(NB_fit,newdata=HT2_TestX)
Warning message:
In data.matrix(newdata) : NAs introduced by coercion
> NB_Accuracy <- sum(NB_predict==HT2_TestY)/nte
> NB_Accuracy
[1] 0.9732704403
朴素贝叶斯分类器的准确度为 97.33%,与逻辑回归模型相同,但低于神经网络提供的准确度。我们在此指出,这种方法和逻辑回归的准确度相同只是一个巧合。
决策树
布莱曼和奎南主要开发了决策树,自 20 世纪 80 年代以来,决策树已经发展了很多。如果因变量是连续的,决策树将是一个回归树;如果是分类变量,它将是一个分类树。当然,我们也可以有生存树。决策树将是受益于集成技术的模型,这一点将在整本书中看到。
考虑以下图表中给出的回归树。我们可以看到有三个输入变量,即
,输出变量是 Y。严格来说,决策树不会显示构建树时使用的所有变量。在这个树结构中,决策树传统上是倒置显示的。我们有四个终端节点。如果满足条件
,我们移动到树的右侧,并得出结论,平均 Y 值为 40。如果不满足条件,我们移动到左侧,并检查是否满足
。如果不满足这个条件,我们移动到树的左侧,并得出结论,平均 Y 值为 100。在满足这个条件后,我们移动到右侧,然后如果满足分类变量
,平均 Y 值将是 250,否则是 10。这个决策树也可以用以下方程表示:



图 5:回归树
统计学家 Terry Therneau 开发了 rpart R 包。
甲状腺功能减退分类的决策树
使用 rpart 包中的 rpart 函数,我们为之前分区的数据构建了相同的分类树。该树可以使用绘图函数进行可视化,变量名可以用文本函数刻在树上。拟合的分类树(见图 甲状腺功能减退分类的决策树)的方程如下:

预测和准确度执行的方式与之前提到的类似:
> CT_fit <- rpart(HT2_Formula,data=HT2_Train)
> plot(CT_fit,uniform=TRUE)
> text(CT_fit)
> CT_predict <- predict(CT_fit,newdata=HT2_TestX,type="class")
> CT_Accuracy <- sum(CT_predict==HT2_TestY)/nte
> CT_Accuracy
[1] 0.9874213836

图 6:甲状腺功能减退分类树
因此,分类树给出了 98.74% 的准确度,是目前考虑的四个模型中最好的。接下来,我们将考虑最终的模型,即支持向量机。
支持向量机
支持向量机,通常简称为 SVM,是机器学习的一个重要类别。理论上,SVM 可以处理无限数量的特征/协变量,并构建适当的分类或回归 SVM。
用于甲状腺功能减退分类的支持向量机(SVM)
来自 e1071 包的 svm 函数将有助于在甲状腺功能减退数据集上构建 SVM 分类器。按照惯例,我们在 R 会话中得到了以下输出:
> SVM_fit <- svm(HT2_Formula,data=HT2_Train)
> SVM_predict <- predict(SVM_fit,newdata=HT2_TestX,type="class")
> SVM_Accuracy <- sum(SVM_predict==HT2_TestY)/nte
> SVM_Accuracy
[1] 0.9842767296
SVM 技术为我们提供了 98.43% 的准确度,是目前设置模型中的第二好。
在下一节中,我们将为波形、德国信贷、鸢尾花和皮马印第安人糖尿病问题数据集的五个分类模型分别运行。
正确模型困境!
在上一节中,我们对Hypothyroid数据集运行了五个分类模型。这里,任务是重复对其他四个数据集进行同样的练习。改变适当位置的代码并重复四次将是一项非常繁琐的任务。因此,为了避免这个问题,我们将创建一个新的函数,称为Multiple_Model_Fit。这个函数将接受四个参数:formula、train、testX和testY。这四个参数已经为五个数据集中的每一个设置好了。然后,该函数被设置成以概括上一节中每个模型的步骤。
函数继续创建一个矩阵,其第一列包含模型名称,第二列包含准确率。这个矩阵作为该函数的输出返回:
> Multiple_Model_Fit <- function(formula,train,testX,testY){
+ ntr <- nrow(train) # Training size
+ nte <- nrow(testX) # Test size
+ p <- ncol(testX)
+ testY_numeric <- as.numeric(testY)
+
+ # Neural Network
+ set.seed(12345)
+ NN_fit <- nnet(formula,data = train,size=p,trace=FALSE)
+ NN_Predict <- predict(NN_fit,newdata=testX,type="class")
+ NN_Accuracy <- sum(NN_Predict==testY)/nte
+
+ # Logistic Regressiona
+ LR_fit <- glm(formula,data=train,family = binomial())
+ LR_Predict <- predict(LR_fit,newdata=testX,type="response")
+ LR_Predict_Bin <- ifelse(LR_Predict>0.5,2,1)
+ LR_Accuracy <- sum(LR_Predict_Bin==testY_numeric)/nte
+
+ # Naive Bayes
+ NB_fit <- naiveBayes(formula,data=train)
+ NB_predict <- predict(NB_fit,newdata=testX)
+ NB_Accuracy <- sum(NB_predict==testY)/nte
+
+ # Decision Tree
+ CT_fit <- rpart(formula,data=train)
+ CT_predict <- predict(CT_fit,newdata=testX,type="class")
+ CT_Accuracy <- sum(CT_predict==testY)/nte
+
+ # Support Vector Machine
+ svm_fit <- svm(formula,data=train)
+ svm_predict <- predict(svm_fit,newdata=testX,type="class")
+ svm_Accuracy <- sum(svm_predict==testY)/nte
+
+ Accu_Mat <- matrix(nrow=5,ncol=2)
+ Accu_Mat[,1] <- c("Neural Network","Logistic Regression","Naive Bayes",
+ "Decision Tree","Support Vector Machine")
+ Accu_Mat[,2] <- round(c(NN_Accuracy,LR_Accuracy,NB_Accuracy,
+ CT_Accuracy,svm_Accuracy),4)
+ return(Accu_Mat)
+
+ }
Multiple_Model_Fit现在应用于Hypothyroid数据集,结果可以看到与上一节一致:
> Multiple_Model_Fit(formula=HT2_Formula,train=HT2_Train,
+ testX=HT2_TestX,
+ testY=HT2_TestY)
[,1] [,2]
[1,] "Neural Network" "0.989"
[2,] "Logistic Regression" "0.9733"
[3,] "Naive Bayes" "0.9733"
[4,] "Decision Tree" "0.9874"
[5,] "Support Vector Machine" "0.9843"
然后将Multiple_Model_Fit函数应用于其他四个分类数据集:
> Multiple_Model_Fit(formula=Waveform_DF_Formula,train=Waveform_DF_Train,
+ testX=Waveform_DF_TestX,
+ testY=Waveform_DF_TestY)
[,1] [,2]
[1,] "Neural Network" "0.884"
[2,] "Logistic Regression" "0.8873"
[3,] "Naive Bayes" "0.8601"
[4,] "Decision Tree" "0.8435"
[5,] "Support Vector Machine" "0.9171"
> Multiple_Model_Fit(formula=GC2_Formula,train=GC2_Train,
+ testX=GC2_TestX,
+ testY =GC2_TestY )
[,1] [,2]
[1,] "Neural Network" "0.7252"
[2,] "Logistic Regression" "0.7572"
[3,] "Naive Bayes" "0.8083"
[4,] "Decision Tree" "0.7061"
[5,] "Support Vector Machine" "0.754"
> Multiple_Model_Fit(formula=ir2_Formula,train=ir2_Train,
+ testX=ir2_TestX,
+ testY=ir2_TestY)
[,1] [,2]
[1,] "Neural Network" "1"
[2,] "Logistic Regression" "1"
[3,] "Naive Bayes" "1"
[4,] "Decision Tree" "1"
[5,] "Support Vector Machine" "1"
> Multiple_Model_Fit(formula=PID_Formula,train=PimaIndiansDiabetes_Train,
+ testX=PimaIndiansDiabetes_TestX,
+ testY=PimaIndiansDiabetes_TestY)
[,1] [,2]
[1,] "Neural Network" "0.6732"
[2,] "Logistic Regression" "0.751"
[3,] "Naive Bayes" "0.7821"
[4,] "Decision Tree" "0.7588"
[5,] "Support Vector Machine" "0.7665"
每个数据集的结果总结在下表中:

表 1:五个数据集五个模型的准确率
iris数据集是一个简单直接的问题,因此五个模型在测试数据上都能给出 100%的准确率。这个数据集将不再进一步研究。
对于每个数据集,我们用灰色突出显示最高准确率的单元格,并用黄色突出显示次高准确率的单元格。
这里是建模困境。朴素贝叶斯方法对于German和Pima Indian Diabetes数据集表现最佳。决策树对Hypothyroid数据集给出了最高的准确率,而 SVM 对Waveform给出了最佳结果。逻辑回归和 SVM 各两次获得了第二名。然而,我们也知道,根据初始种子和可能隐藏神经元的数量,神经网络也预期在某些数据集上表现最佳。我们还得考虑结果是否会因不同的分区而有所不同。
在这样的实际场景中,我们更希望有一个单一的方法来确保合理的属性。对于Hypothyroid数据集,每个模型的准确率都是 97%或更高,因此使用任何模型都不会出错。然而,在German和Pima Indian Diabetes问题上,最大准确率分别是 80%和 78%。因此,如果我们能充分利用所有模型并构建一个准确率更高的单一统一模型会更好。
集成概述
caret R 包是集成机器学习方法的基石。它提供了一个大型的框架,我们还可以将不同的统计和机器学习模型组合起来创建集成。对于作者笔记本电脑上该包的最新版本,caret包提供了以下模型的访问权限:
> library(caret)
> names(getModelInfo())
[1] "ada" "AdaBag" "AdaBoost.M1"
[4] "adaboost" "amdai" "ANFIS"
[7] "avNNet" "awnb" "awtan"
[229] "vbmpRadial" "vglmAdjCat" "vglmContRatio
[232] "vglmCumulative" "widekernelpls" "WM"
[235] "wsrf" "xgbLinear" "xgbTree"
[238] "xyf"
Figure 7: Caret providing a message to install the required R package
您需要输入数字1并继续。包将被安装和加载,程序将继续。了解集成方法的众多选项是很好的。这里提供了一个堆叠集成分析模型的简要方法,详细内容将在本书的后续部分展开。
对于Hypothyroid数据集,五个模型之间的平均准确率高达 98%。Waveform数据集的平均准确率约为 88%,而German Credit 数据集的平均准确率为 75%。我们将尝试提高这个数据集的准确率。将尝试使用三个模型:朴素贝叶斯、逻辑回归和分类树来提高准确率。首先,我们需要将数据分为三部分:训练、测试和堆叠:
> load("../Data/GC2.RData")
> set.seed(12345)
> Train_Test_Stack <- sample(c("Train","Test","Stack"),nrow(GC2),replace = TRUE,prob = c(0.5,0.25,0.25))
> GC2_Train <- GC2[Train_Test_Stack=="Train",]
> GC2_Test <- GC2[Train_Test_Stack=="Test",]
> GC2_Stack <- GC2[Train_Test_Stack=="Stack",]The dependent and independent variables will be marked next in character vectors for programming convenient.
> # set label name and Exhogenous
> Endogenous <- 'good_bad'
> Exhogenous <- names(GC2_Train)[names(GC2_Train) != Endogenous]
模型将首先建立在训练数据上,并使用曲线下的面积(ROC 曲线)来评估准确性。首先设置控制参数,然后使用训练数据集创建三个模型:朴素贝叶斯、分类树和逻辑回归:
> # Creating a caret control object for the number of
> # cross-validations to be performed
> myControl <- trainControl(method='cv', number=3, returnResamp='none')
> # train all the ensemble models with GC2_Train
> model_NB <- train(GC2_Train[,Exhogenous], GC2_Train[,Endogenous],
+ method='naive_bayes', trControl=myControl)
> model_rpart <- train(GC2_Train[,Exhogenous], GC2_Train[,Endogenous],
+ method='rpart', trControl=myControl)
> model_glm <- train(GC2_Train[,Exhogenous], GC2_Train[,Endogenous],
+ method='glm', trControl=myControl)
接下来进行测试和堆叠块的预测。我们将预测概率存储在测试和堆叠数据框中:
> # get predictions for each ensemble model for two last datasets
> # and add them back to themselves
> GC2_Test$NB_PROB <- predict(object=model_NB, GC2_Test[,Exhogenous],
+ type="prob")[,1]
> GC2_Test$rf_PROB <- predict(object=model_rpart, GC2_Test[,Exhogenous],
+ type="prob")[,1]
> GC2_Test$glm_PROB <- predict(object=model_glm, GC2_Test[,Exhogenous],
+ type="prob")[,1]
> GC2_Stack$NB_PROB <- predict(object=model_NB, GC2_Stack[,Exhogenous],
+ type="prob")[,1]
> GC2_Stack$rf_PROB <- predict(object=model_rpart, GC2_Stack[,Exhogenous],
+ type="prob")[,1]
> GC2_Stack$glm_PROB <- predict(object=model_glm, GC2_Stack[,Exhogenous],
+ type="prob")[,1]
ROC(受试者工作特征曲线)是模型评估的重要指标。ROC 曲线下的面积越大,模型越好。请注意,这些指标或任何其他指标都不会与之前拟合的模型相同,因为数据已经发生了变化:
> # see how each individual model performed on its own
> AUC_NB <- roc(GC2_Test[,Endogenous], GC2_Test$NB_PROB )
> AUC_NB$auc
Area under the curve: 0.7543
> AUC_rf <- roc(GC2_Test[,Endogenous], GC2_Test$rf_PROB )
> AUC_rf$auc
Area under the curve: 0.6777
> AUC_glm <- roc(GC2_Test[,Endogenous], GC2_Test$glm_PROB )
> AUC_glm$auc
Area under the curve: 0.7446
对于test数据集,我们可以看到朴素贝叶斯、分类树和逻辑回归的曲线下面积分别为0.7543、0.6777和0.7446。如果我们以某种格式将预测值组合在一起,并且这导致准确性的提高,那么集成技术的目的就达到了。因此,我们考虑三个模型下的新预测概率,并将它们附加到堆叠数据框中。这三个列现在将被视为新的输入向量。然后我们构建一个朴素贝叶斯模型,这是一个任意的选择,您可以为堆叠数据框尝试任何其他模型(不一定是这些中的任何一个)。然后可以预测并计算 AUC:
> # Stacking it together
> Exhogenous2 <- names(GC2_Stack)[names(GC2_Stack) != Endogenous]
> Stack_Model <- train(GC2_Stack[,Exhogenous2], GC2_Stack[,Endogenous],
+ method='naive_bayes', trControl=myControl)
> Stack_Prediction <- predict(object=Stack_Model,GC2_Test[,Exhogenous2],type="prob")[,1]
> Stack_AUC <- roc(GC2_Test[,Endogenous],Stack_Prediction)
> Stack_AUC$auc
Area under the curve: 0.7631
堆叠数据观察的 AUC 高于之前的任何模型,这是一个改进。
对于批判性思考者来说,应该会涌现出许多问题。这项技术为什么能起作用?它是否会在所有可能的情况下导致即兴发挥?如果是的话,仅仅添加新的模型预测是否会带来进一步的改进?如果不是,我们如何选择基础模型以确保我们可以合理地确信即兴发挥?模型选择有哪些限制?我们将在本书中提供对这些问题的多数解决方案。在下一节中,我们将快速查看一些有用的统计检验,这些检验将有助于评估模型性能。
补充统计检验
在这里,一个模型被选为另一个可能的模型。一个模型的准确性似乎高于另一个。该模型的 ROC 曲线下的面积(AUC)大于另一个。然而,仅基于纯数字得出结论是不合适的。从统计推断的角度来看,重要的是要得出这些数字是否具有统计学意义的结论。在分析世界中,当我们有机会时,使用统计测试来验证主张/假设是至关重要的。使用统计测试的一个原因是概率可能非常反直觉,表面上看起来可能的情况,在仔细检查并考虑到机会变化后可能并不成立。例如,如果一枚公平的硬币抛掷 100 次,认为正面数必须是正好 50 是不明智的。因此,如果一枚公平的硬币出现 45 次正面,我们需要考虑到正面数可能少于 50 的机会变化。在处理不确定数据时,我们必须始终保持谨慎。以下是一些例子。两个变量可能看起来彼此独立,相关系数也可能几乎等于零。然而,应用相关测试可能会得出结论,相关系数并不显著为零。由于我们将在本文中大量进行抽样和重抽样,我们将查看相关的测试。
排列测试
假设有两个过程,A 和 B,这两个过程的方差已知是相等的,尽管是未知的。从过程 A 的三个独立观察结果产生了 18、20 和 22 的产量,而从过程 B 的三个独立观察结果产生了 24、26 和 28 的产量。在假设产量遵循正态分布的情况下,我们想测试过程 A 和 B 的均值是否相同。这是一个适合应用 t 检验的情况,因为观察的数量较少。t.test函数的应用表明,两个均值彼此不同,这直观上似乎是正确的。
现在,在零假设下的假设是均值相等,方差在两个过程中未知且假设相等。因此,我们有充分的理由相信过程 A 的观察结果也可能发生在过程 B 中,反之亦然。因此,我们可以将过程 B 中的一个观察结果与过程 A 交换,并重新计算 t 检验。这个过程可以针对两个样本的所有可能的排列重复进行。一般来说,如果我们有来自总体 1 的 m 个样本和来自总体 2 的 n 个样本,我们可以有

不同的样本和尽可能多的测试。一个综合测试可以基于这样的排列样本和这样的测试被称为排列测试。
对于过程 A 和 B 的观察,我们首先将应用 t 检验,然后是排列检验。t.test可在核心stats包中找到,而排列 t 检验则来自perm包:
> library(perm)
> x <- c(18,20,22); y <- c(24,26,28)
> t.test(x,y,var.equal = TRUE)
Two Sample t-test
data: x and y
t = -3.6742346, df = 4, p-value = 0.02131164
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-10.533915871 -1.466084129
sample estimates:
mean of x mean of y
20 26
较小的 p 值表明过程 A 和 B 的均值不相等。因此,我们现在应用来自perm包的排列检验permTS:
> permTS(x,y)
Exact Permutation Test (network algorithm)
data: x and y
p-value = 0.1
alternative hypothesis: true mean x - mean y is not equal to 0
sample estimates:
mean x - mean y
-6
现在的 p 值为 0.1,这意味着排列检验得出结论,过程的均值是相等的。这难道意味着排列检验将始终得出这个结论,与 t 检验相矛盾?答案是下一段代码给出的:
> x2 <- c(16,18,20,22); y2 <- c(24,26,28,30)
> t.test(x2,y2,var.equal = TRUE)
Two Sample t-test
data: x2 and y2
t = -4.3817805, df = 6, p-value = 0.004659215
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-12.46742939 -3.53257061
sample estimates:
mean of x mean of y
19 27
> permTS(x2,y2)
Exact Permutation Test (network algorithm)
data: x2 and y2
p-value = 0.02857143
alternative hypothesis: true mean x2 - mean y2 is not equal to 0
sample estimates:
mean x2 - mean y2
-8
卡方检验和 McNemar 检验
我们对甲状腺功能减退测试有五个模型。然后我们计算了准确度,并对这些数字感到满意。让我们首先看看拟合模型犯的错误数量。测试分区中有 636 个观察值,其中 42 个测试呈甲状腺功能减退阳性。请注意,如果我们将所有患者标记为阴性,我们将得到准确度为1-42/636 = 0.934,即约 93.4%。使用表格函数,我们将实际值与预测值进行比较,看看拟合模型出错有多频繁。我们在此指出,将甲状腺功能减退病例识别为相同,将阴性病例识别为阴性是正确的预测,而将甲状腺功能减退病例标记为阴性,反之亦然会导致错误。对于每个模型,我们查看误分类错误:
> table(LR_Predict_Bin,testY_numeric)
testY_numeric
LR_Predict_Bin 1 2
1 32 7
2 10 587
> table(NN_Predict,HT2_TestY)
HT2_TestY
NN_Predict hypothyroid negative
hypothyroid 41 22
negative 1 572
> table(NB_predict,HT2_TestY)
HT2_TestY
NB_predict hypothyroid negative
hypothyroid 33 8
negative 9 586
> table(CT_predict,HT2_TestY)
HT2_TestY
CT_predict hypothyroid negative
hypothyroid 38 4
negative 4 590
> table(SVM_predict,HT2_TestY)
HT2_TestY
SVM_predict hypothyroid negative
hypothyroid 34 2
negative 8 592
从错误分类表中,我们可以看到神经网络正确地识别了 42 个甲状腺功能减退病例中的 41 个,但它也错误地识别了更多的甲状腺功能减退病例。由此产生的问题是,拟合模型的正确预测是否只是偶然发生,或者它们是否依赖于真实情况并可解释。为了测试这一点,在假设框架中,我们想测试实际值和预测值是否相互独立或相互依赖。技术上,零假设是预测与实际值无关,如果一个模型解释了真实情况,则必须拒绝零假设。我们应该得出结论,拟合模型的预测依赖于真实情况。在这里,我们部署了两种解决方案,即卡方检验和 McNemar 检验:
> chisq.test(table(LR_Predict_Bin,testY_numeric))
Pearson's Chi-squared test with Yates' continuity correction
data: table(LR_Predict_Bin, testY_numeric)
X-squared = 370.53501, df = 1, p-value < 0.00000000000000022204
> chisq.test(table(NN_Predict,HT2_TestY))
Pearson's Chi-squared test with Yates' continuity correction
data: table(NN_Predict, HT2_TestY)
X-squared = 377.22569, df = 1, p-value < 0.00000000000000022204
> chisq.test(table(NB_predict,HT2_TestY))
Pearson's Chi-squared test with Yates' continuity correction
data: table(NB_predict, HT2_TestY)
X-squared = 375.18659, df = 1, p-value < 0.00000000000000022204
> chisq.test(table(CT_predict,HT2_TestY))
Pearson's Chi-squared test with Yates' continuity correction
data: table(CT_predict, HT2_TestY)
X-squared = 498.44791, df = 1, p-value < 0.00000000000000022204
> chisq.test(table(SVM_predict,HT2_TestY))
Pearson's Chi-squared test with Yates' continuity correction
data: table(SVM_predict, HT2_TestY)
X-squared = 462.41803, df = 1, p-value < 0.00000000000000022204
> mcnemar.test(table(LR_Predict_Bin,testY_numeric))
McNemar's Chi-squared test with continuity correction
data: table(LR_Predict_Bin, testY_numeric)
McNemar's chi-squared = 0.23529412, df = 1, p-value = 0.6276258
> mcnemar.test(table(NN_Predict,HT2_TestY))
McNemar's Chi-squared test with continuity correction
data: table(NN_Predict, HT2_TestY)
McNemar's chi-squared = 17.391304, df = 1, p-value = 0.00003042146
> mcnemar.test(table(NB_predict,HT2_TestY))
McNemar's Chi-squared test with continuity correction
data: table(NB_predict, HT2_TestY)
McNemar's chi-squared = 0, df = 1, p-value = 1
> mcnemar.test(table(CT_predict,HT2_TestY))
McNemar's Chi-squared test
data: table(CT_predict, HT2_TestY)
McNemar's chi-squared = 0, df = 1, p-value = 1
> mcnemar.test(table(SVM_predict,HT2_TestY))
McNemar's Chi-squared test with continuity correction
data: table(SVM_predict, HT2_TestY)
McNemar's chi-squared = 2.5, df = 1, p-value = 0.1138463
卡方检验提供的答案清楚地表明,每个拟合模型的预测并非偶然。它还表明,拟合模型对甲状腺功能减退病例以及阴性病例的预测是预期的。McNemar 检验的解释和结论留给读者。在分类问题中,最重要的度量标准是 ROC 曲线,接下来我们将讨论它。
ROC 检验
ROC 曲线是对模型性能的假阳性率和真阴性率的重要改进。有关详细解释,请参阅 Tattar 等人(2017 年)的第九章。ROC 曲线基本上是绘制真阳性率与假阳性率的关系图,我们测量拟合模型的 AUC。
ROC 测试试图实现的主要目标是以下内容。假设模型 1 给出 AUC 为 0.89,模型 2 给出 0.91。使用简单的 AUC 标准,我们直接得出结论,模型 2 优于模型 1。然而,一个重要的问题是,0.91 是否显著高于 0.89。来自pROCR 包的roc.test提供了答案。对于神经网络和分类树,以下 R 段提供了所需的答案:
> library(pROC)
> HT_NN_Prob <- predict(NN_fit,newdata=HT2_TestX,type="raw")
> HT_NN_roc <- roc(HT2_TestY,c(HT_NN_Prob))
> HT_NN_roc$auc
Area under the curve: 0.9723826
> HT_CT_Prob <- predict(CT_fit,newdata=HT2_TestX,type="prob")[,2]
> HT_CT_roc <- roc(HT2_TestY,HT_CT_Prob)
> HT_CT_roc$auc
Area under the curve: 0.9598765
> roc.test(HT_NN_roc,HT_CT_roc)
DeLong's test for two correlated ROC curves
data: HT_NN_roc and HT_CT_roc
Z = 0.72452214, p-value = 0.4687452
alternative hypothesis: true difference in AUC is not equal to 0
sample estimates:
AUC of roc1 AUC of roc2
0.9723825557 0.9598765432
由于 p 值非常大,我们得出结论,两个模型的 AUC 没有显著差异。
统计测试至关重要,我们建议在合适的情况下使用它们。本章中强调的概念将在本书的其余部分进行更详细的阐述。
摘要
本章以介绍本书其余部分将使用的一些最重要的数据集开始。这些数据集涵盖了包括分类、回归、时间序列、生存、聚类以及识别异常值很重要的数据集在内的各种分析问题。然后在统计/机器学习模型部分介绍了重要的分类模型家族。在介绍了各种模型后,我们立即看到了不足之处,即我们没有适用于所有季节的模型。模型性能因数据集而异。根据初始化的不同,某些模型(如神经网络)的性能受到影响。因此,有必要找到一种方法来确保模型可以在大多数情况下得到改进。
这为集成方法铺平了道路,这也是本书的标题。我们将在本书的其余部分详细阐述这种方法。本章以快速统计测试结束,这些测试有助于进行模型比较。重采样是集成方法的核心,我们将在下一章探讨重要的 Jackknife 和 Bootstrap 方法。
第二章. 自举
如前一章所见,统计推断在计算能力的帮助下得到了极大的增强。我们还探讨了排列检验的过程,其中相同的检验被多次应用于给定数据的重采样(在零假设下)。重采样方法背后的原理也类似;我们相信,如果样本确实是随机的,并且观测是从相同的相同分布中生成的,我们就有一个有效的理由用替换的方式重采样同一组观测。这是因为任何观测都可能多次发生,而不是作为一个单独的实例。
本章将从重采样的正式定义开始,接着探讨删层技术。这将被应用于多个问题,尽管相对较简单,我们首先将查看伪值的定义。由 Efron 发明的自举方法可能是最有用的重采样方法。我们将彻底研究这个概念,并将应用从简单案例扩展到回归模型。
本章将涵盖以下内容:
-
删层技术:我们的第一个重采样方法,能够减少偏差
-
自举:一种统计方法,也是删层方法的推广
-
boot 包:自举方法的主要 R 包
-
自举和假设检验:使用自举方法进行假设检验
-
自举回归模型:将自举方法应用于一般回归模型
-
自举生存模型:应用自举方法处理生存数据
-
自举时间序列模型:时间序列数据的自举方法——这里的观测是相关的
技术要求
在本章中,我们将使用以下库:
-
ACSWR -
boot -
car -
gee -
mvtnorm -
pseudo -
RSADBE -
survival
删层技术
Quenouille(1949 年)发明了删层技术。其目的是通过有系统地查看多个数据样本来减少偏差。删层技术的名字似乎是由著名的统计学家 John W. Tukey 提出的。主要由于计算能力的缺乏,删层方法的进步和实用性受到了限制。Efron 在 1979 年发明了自举方法(参见下文的应用),并建立了与删层方法之间的联系。实际上,这两种方法有很多共同之处,通常被归类为重采样方法。
假设我们从概率分布F中抽取一个大小为n的随机样本
,并用
表示感兴趣的参数。令
为
的估计量,在此我们并没有给定
的概率分布。当概率分布未知时,重采样方法有助于进行统计推断。有必要对这个概念给出一个正式的定义。
定义:重采样方法是估计估计量
的偏差和方差的途径,它使用基于可用观测值的子样本
的值。
Jackknife 技术是一种重采样方法,我们将在接下来的讨论中阐述其一般步骤。如前所述,
是
的估计量。为了简化,我们定义给定观测值的向量为
。设置此程序的重要量是伪值,我们将在下文中对其进行数学定义。
定义:令
,即
是去掉第i个观测值的向量
。
的第i个伪值定义为以下内容:

可以从数学上证明伪值等同于以下内容:

因此,伪值被视为
的偏差校正版本。这里定义的伪值也被称为删除一个的 Jackknife。Jackknife 方法将伪值视为具有均值
的独立观测值,然后应用中心极限定理进行统计推断。伪值的均值和(抽样)方差如下所示:


均值和方差的 Jackknife 方法
假设概率分布是未知的,直方图和其他可视化技术表明正态分布的假设不合适。然而,我们也没有足够的信息来为手头的问题制定一个合理的概率模型。在这里,我们可以充分利用 Jackknife 技术。
我们将均值和方差估计量定义为如下:


与
和
相关的伪值分别在以下表达式中给出:


的均值将是样本均值,而
的均值将是样本方差。然而,杰克 knife 方法的应用在于细节。仅基于估计的均值,我们无法推断关于总体均值的信息,仅基于样本方差,我们无法精确推断关于总体方差的信息。为了了解这些伪值公式的具体情况以及它们的方差如何有用,我们将在下一个优雅的 R 程序中设置。
我们将从具有一些规模和形状参数的威布尔分布中模拟 n = 1000 个观测值。在标准文献中,我们将能够找到这两个参数的估计值。然而,从业者很少对这些参数感兴趣,他们更愿意推断寿命的均值和方差。密度函数是一个复杂的形式。此外,从规模和形状参数的角度来看,威布尔随机变量的理论均值和方差很容易发现过于复杂,涉及伽马积分的表达式并不能进一步帮助这个案例。如果读者试图在搜索引擎中搜索字符串 威布尔分布均值的统计推断,结果将不会令人满意,而且进一步推进不会很容易,除非是数学上熟练的人。在这个复杂的场景中,我们将探讨杰克 knife 方法是如何为我们解围的。
在我们继续之前,有一个注意事项。读者可能会想,“在这个强大的超级计算机器时代,谁会在乎威布尔分布呢?”然而,任何可靠性工程师都会证实寿命分布的有用性,而威布尔是这个类别中的重要成员。第二点可能是,对于大样本,正态近似将很好地成立。然而,当我们有适中的样本进行推断时,对于像威布尔这样高度偏斜的分布的正态近似可能会失去测试的力度和信心。此外,问题是,如果我们坚信基础分布是威布尔(没有参数),那么获得威布尔分布的均值和方差的精确分布仍然是一个巨大的数学任务。
R 程序将实现针对给定原始数据的均值和方差的杰克 knife 技术:
> # Simulating observations from Weibull distribution
> set.seed(123)
> sr <- rweibull(1000,0.5,15)
> mean(sr); sd(sr); var(sr)
[1] 30.41584
[1] 69.35311
[1] 4809.854
如前所述的模拟场景中提到的,我们播种是为了可重复的结果。rweibull函数有助于执行从威布尔分布模拟观察值的任务。我们计算样本的均值、标准差和方差。接下来,我们定义pv_mean函数,它将使计算均值和方差的伪值成为可能:
> # Calculating the pseudovalues for the mean
> pv_mean <- NULL; n <- length(sr)
> for(i in 1:n)
+ pv_mean[i] <- sum(sr)- (n-1)*mean(sr[-i])
> head(sr,20)
[1] 23.29756524 0.84873231 11.99112962 0.23216910 0.05650965
[6] 143.11046494 6.11445277 0.19432310 5.31450418 9.21784734
[11] 0.02920662 9.38819985 2.27263386 4.66225355 77.54961762
[16] 0.16712791 29.48688494 150.60696742 18.64782005 0.03252283
> head(pv_mean,20)
[1] 23.29756524 0.84873231 11.99112962 0.23216910 0.05650965
[6] 143.11046494 6.11445277 0.19432310 5.31450418 9.21784734
[11] 0.02920662 9.38819985 2.27263386 4.66225355 77.54961762
[16] 0.16712791 29.48688494 150.60696742 18.64782005 0.03252283
> mean(pv_mean); sd(pv_mean)
[1] 30.41584
[1] 69.35311
注意,对于所有观察值,平均值和伪值的值与观察值的值相同。实际上,这是预期的,因为我们正在查看的统计量是平均值,它只是平均。从其他观察值的平均中减去应该会得到该值。因此,伪值的平均值和样本平均值也将相同。然而,这并不意味着努力是徒劳的。我们将继续对方差项进行以下计算:
> # Calculating the pseudovalues for the variance
> pv_var <- NULL
> pseudo_var <- function(x,i){
+ n = length(x)
+ psv <- (n/(n-2))*(x[i]-mean(x))²-(1/(n-1)*(n-2))*sum(x-mean(x))²
+ return(psv)
+ }
> pv_var <- NULL
> for(i in 1:n)
+ pv_var[i] <- pseudo_var(sr,i)
> head(pv_var)
[1] 50.77137 875.96574 340.15022 912.87970 923.53596 12725.52973
> var(sr); mean(pv_var)
[1] 4809.854
[1] 4814.673
> sd(pv_var)
[1] 35838.59
现在,实际数据中没有观察到的伪值的对应物。在这里,伪值的平均值将大约等于样本方差。这是标准差sd(pv_var),它将有助于进行与方差或标准差相关的推断。
我们已经看到夹具在推断均值和方差方面的有用性。在本节下一部分,我们将看到伪值如何在生存回归问题的背景下解决问题。
生存数据的伪值方法
原始的胆汁性肝硬化数据,pbc,在第一章,集成技术简介,第二部分中被引入,我们做了笔记,指出这些数据是特殊的,因为它是生存数据,感兴趣的时间变量可能受到截尾,这增加了进一步分析复杂性。处理生存数据的专用方法将在第十章,集成生存模型中讨论。这些专用方法包括风险回归,协变量的影响是在风险率上而不是在寿命上测量的。观察到从业者发现这些概念有点难以理解,因此我们将简要讨论基于伪值的替代方法。
安德森和克莱因在一系列论文中有效地使用了伪值的概念来解决各种问题:
> library(survival)
> library(pseudo)
> library(gee)
> data(pbc)
> time_pseudo <- pseudomean(time=pbc$time,event=pbc$status==2)
> pbc_gee <- gee(time_pseudo ~ trt + age + sex + ascites + hepato +
+ spiders + edema + bili + chol + albumin + copper +
+ alk.phos + ast + trig + platelet + protime + stage,
+ id=1:nrow(pbc), family="gaussian",
+ data=pbc)
Beginning Cgee S-function, @(#) geeformula.q 4.13 98/01/27
running glm to get initial regression estimate
(Intercept) trt age sexf ascites hepato
5901.1046673 115.5247130 -23.6893551 233.0351191 -251.2292823 -63.1776549
spiders edema bili chol albumin copper
-264.2063329 -441.2298926 -67.7863015 -0.5739644 438.5953357 -2.3704801
alk.phos ast trig platelet protime stage
-0.0619931 -1.1273468 0.2317984 -0.4243154 -160.6784722 -292.9838866
> summary(pbc_gee)
GEE: GENERALIZED LINEAR MODELS FOR DEPENDENT DATA
gee S-function, version 4.13 modified 98/01/27 (1998)
Model:
Link: Identity
Variance to Mean Relation: Gaussian
Correlation Structure: Independent
Call:
gee(formula = time_pseudo ~ trt + age + sex + ascites + hepato +
spiders + edema + bili + chol + albumin + copper + alk.phos +
ast + trig + platelet + protime + stage, id = 1:nrow(pbc),
data = pbc, family = "gaussian")
Summary of Residuals:
Min 1Q Median 3Q Max
-3515.1303 -792.8410 112.1563 783.9519 3565.1490
Coefficients:
Estimate Naive S.E. Naive z Robust S.E. Robust z
(Intercept) 5901.1046673 1.524661e+03 3.8704367 1.470722e+03 4.0123856
trt 115.5247130 1.616239e+02 0.7147750 1.581686e+02 0.7303895
age -23.6893551 8.507630e+00 -2.7844835 8.204491e+00 -2.8873643
sexf 233.0351191 2.701785e+02 0.8625227 3.215865e+02 0.7246421
ascites -251.2292823 4.365874e+02 -0.5754387 5.133867e+02 -0.4893568
hepato -63.1776549 1.884840e+02 -0.3351885 1.786614e+02 -0.3536166
spiders -264.2063329 1.986929e+02 -1.3297220 2.045738e+02 -1.2914962
edema -441.2298926 4.155360e+02 -1.0618331 4.850261e+02 -0.9097034
bili -67.7863015 2.651543e+01 -2.5564852 2.009844e+01 -3.3727151
chol -0.5739644 4.117889e-01 -1.3938317 3.929789e-01 -1.4605475
albumin 438.5953357 2.321347e+02 1.8894000 2.156405e+02 2.0339196
copper -2.3704801 1.120153e+00 -2.1162114 1.102365e+00 -2.1503594
alk.phos -0.0619931 3.932052e-02 -1.5766092 4.571919e-02 -1.3559535
ast -1.1273468 1.640940e+00 -0.6870130 1.797116e+00 -0.6273089
trig 0.2317984 1.416552e+00 0.1636356 1.375674e+00 0.1684980
platelet -0.4243154 9.348907e-01 -0.4538663 9.106646e-01 -0.4659403
protime -160.6784722 9.139593e+01 -1.7580484 9.254740e+01 -1.7361749
stage -292.9838866 1.137951e+02 -2.5746618 1.025891e+02 -2.8558966
Estimated Scale Parameter: 1675818
Number of Iterations: 1
Working Correlation
[,1]
[1,] 1
自举 – 一种统计方法
在本节中,我们将探讨复杂的统计函数。两个随机变量之间的相关性的统计分布是什么?如果多元数据的正态性假设不成立,那么获得标准误差和置信区间的替代方法是什么?Efron(1979)发明了自助法(bootstrap technique),它提供了解决与复杂统计函数相关的统计推断问题的方法。在第一章《集成技术介绍》中,介绍了排列检验,它反复抽取给定样本的样本,并对每个重采样进行测试。从理论上讲,排列检验需要
数量的重采样,其中m和n是两个样本中的观测数,尽管在获得足够多的重采样后,人们会稍微放松一些。自助法以类似的方式工作,并且是一种重要的重采样方法。
设
是从概率分布 F 中独立抽取的随机样本,感兴趣的参数为
,参数的估计值用
表示。如果
参数的概率分布要么是未知的,要么是不可处理的,那么关于参数的统计推断就无法进行。因此,我们需要一种通用的技术来帮助进行推断。
Efron 的方法如下展开。由
提供的估计值是一个单一值。给定数据并基于我们有一个独立同分布(IID)样本的假设,自助法(bootstrap method)探索了任何观察到的值与任何其他观察到的值一样可能性的可能性。因此,随机抽取的样本大小为n,且是有放回地抽取的,直观上期望它携带与实际样本相同的信息,我们可以根据这个样本获得
参数的估计值。然后,这一步骤可以重复进行多次,我们将产生参数的多个估计值。使用这个估计值的分布,然后可以进行统计推断。这种方法的一个正式描述是必要的。
从
中随机抽取带替换的样本大小 n,并用
表示。这个样本
被称为 第一个 bootstrap 样本。计算这个样本
的估计值
,并用
表示。重复执行步骤多次,并得到
。然后,
的推断可以基于 bootstrap 估计值
。我们可以将这个描述以算法的形式表示:
-
给定数据
,感兴趣的参数
,计算估计值
。 -
从
中随机抽取带替换的样本大小 n,并用
表示。 -
计算 bootstrap 样本的统计量
。 -
重复执行 重复步骤 2 和 3 'B – 1' 的次数以生成
。 -
使用
来执行与
相关的统计推断。
这里的 步骤 5 传达了什么?我们使用每个
的值来估计感兴趣的参数 B,估计值要精确。由于估计(基于样本)是
,我们(直观上)预期 bootstrap 估计值的平均值
将非常接近
,因此 bootstrap 估计值的标准差也提供了对估计值方差的“良好”度量。然后计算 bootstrap 均值和标准差如下:


使用
和
,我们可以对
进行推断。
应该注意的是,bootstrap 方法是一个非常通用的算法,这里的执行示例是为了说明面对某些有趣问题时的情况。
需要在此阐明有放回抽样的概念。为了简化,假设我们只有五个观测值,例如
。现在,当我们用 5 个有放回样本的大小抽取第一个自助样本时,我们可能会得到标签2、4、4、1、3。这意味着,从原始样本中,我们选择了
,因此标签为2、1和3的观测值各被选中一次,而4被选中两次。这与
相同。在自助法符号中,它将是
。第二个自助样本可能是
,第三个可能是
,依此类推。
接下来,我们将阐述这一技术并阐明其实现方法。我们将应用自助法解决两个问题:
-
相关系数的标准误差
-
协方差/相关矩阵的特征值
相关系数的标准误差
考虑一个假设场景,我们试图研究两个变量之间的关系。历史信息、直觉和散点图都一致,表明两个变量之间存在线性关系,唯一的问题是每个变量的直方图都呈现出非钟形形状。换句话说,正态分布的假设看起来非常不可能,并且由于它在单变量情况(每个变量)中失败,分析师对两个变量的联合双变量正态性持怀疑态度。
设
为n对观测值。样本相关系数可以通过以下公式轻松计算:

这里我们有
。如果我们无法进行相关的统计推断,任何参数的估计都是没有用的。一个置信区间就足以提供相关结果以进行统计推断。我们将学习如何通过自助法帮助我们做到这一点。我们将使用向量符号来保持一致性,并为此定义
,即
现在是一个向量。第一个自助样本是通过随机选择 n 对观测值并重复选择得到的,我们将第一个自助样本表示为
。现在,使用自助样本,我们将通过
来估计相关系数。重复获取自助样本的过程 B – 1 更多次,我们将计算相关系数
。
这里使用的是法学院数据,来自 Efron 和 Tibshirani (1990, p.19) 的第 3.1 表。在这项研究中,从 82 所法学院中随机选择了 15 所学校。对学校测量的两个变量包括班级在国家法律考试(LSAT)上的平均分数和平均本科成绩点(GPA)。我们首先从 CSV 文件导入数据,显示它,然后可视化两个变量的直方图以及散点图:
> LS <- read.csv("../Data/Law_School.csv",header=TRUE)
> LS
School LSAT GPA
1 1 576 3.39
2 2 635 3.30
3 3 558 2.81
13 13 545 2.76
14 14 572 2.88
15 15 594 2.96
> windows(height=100,width=100)
> layout(matrix(c(1,2,3,3),byrow=TRUE, nrow=2))
> hist(LS$LSAT,xlab="LSAT",main="Histogram of LSAT")
> hist(LS$GPA,xlab="GPA",main="Histogram of GPA")
> plot(LS[,2:3],main="Scatter plot between LSAT and GPA")
我们首先查看代码。read.csv 文件有助于从章节的 Data 文件夹导入数据集,该数据集已从代码包中解压并存储在 LS 对象中。然后 LS 在控制台中显示。在这里,我们给出前三个和最后三个观测值。windows 函数创建一个具有指定 height 和 weight 的新图形设备。请注意,此函数仅在 Windows 操作系统上工作。接下来,我们指定图形设备的 layout。为了确认其工作状态,在 R 终端运行 matrix(c(1,2,3,3),byrow=TRUE, nrow=2) 行会得到以下结果:
> matrix(c(1,2,3,3),byrow=TRUE, nrow=2)
[,1] [,2]
[1,] 1 2
[2,] 3 3
这意味着运行任何产生图形的代码后的第一个图形输出显示在设备的区域 1(左上角)。第二个图形输出显示在右上部分,而第三个将分布在下半部分。这对于视觉显示来说是一种方便的操作。两个变量的直方图并不表明呈正态分布,尽管可能会争辩说观测数量要少得多;在这种情况下是十五。然而,散点图表明,随着LSAT的增加,GPA也增加。因此,相关系数是衡量两个变量之间线性关系的有意义的度量。然而,正态分布的假设在这里不适用,或者至少我们需要更多的观测数据,而这些数据目前尚未获得,因此进行统计推断仍然是一个挑战。为了克服这一点,我们将使用自举技术。看看下面的图:

图 1:LSAT 和 GPA 变量可视化
模仿 Efron 和 Tibshirani 的插图,我们将自举样本的数量固定在 3200,我们将对自举样本数量为 25、50、100、200、400、800、1600 和 3200 的情况感兴趣。R 程序方法如下:
-
在找到观测数量、固定自举样本数量和感兴趣的样本之后,我们将初始化自举均值和标准向量。
-
为了复制结果,我们将初始种子值固定为 54321。种子值将增加 1 以获得自举样本,这将确保所有自举样本都是不同的。
-
计算了每个自举样本的相关系数值,因此我们将有 B = 3200 个相关系数。
-
计算了达到所需自举样本数量的相关系数的均值和标准差。
-
为了与 Efron 和 Tibshirani(1990,第 50 页)进行比较,报告了 25、50、100、200、400、800、1600 和 3200 个自举样本的结果。
下文给出了 R 程序及其输出:
> n <- nrow(LS)
> B <- 3200
> TB <- c(25,50,100,200,400,800,1600,3200)
> seB <- NULL
> tcorr <- NULL
> myseed <- 54321
> for(i in 1:B){
+ myseed <- myseed+1
+ set.seed(myseed)
+ tcorr[i] <- as.numeric(cor(LS[sample(1:n,n,replace=TRUE),2:3])[1,2])
+ }
> for(j in 1:length(TB)) seB[j] <- sd(tcorr[1:TB[j]])
> round(seB,3)
[1] 0.141 0.124 0.115 0.135 0.133 0.132 0.133 0.131
> for(j in 2:B){
+ corrB[j] <- mean(tcorr[1:j])
+ seB[j] <- sd(tcorr[1:j])
+ }
> round(corrB[TB],3)
[1] 0.775 0.787 0.793 0.777 0.782 0.773 0.771 0.772
> round(seB[TB],3)
[1] 0.141 0.124 0.115 0.135 0.133 0.132 0.133 0.131
> plot.ts(seB,xlab="Number of Bootstrap Samples",
+ ylab="Bootstrap Standard Error of Correlation")
时间序列图如下所示:

图 2:相关系数的非参数自举标准误差
相关系数的标准误差可以看到稳定在大约0.13。最后,为了进行统计推断,我们可以使用自举置信区间。一种天真的方法是简单地从自举样本中获得相关系数估计的 95%覆盖范围。这在软件中很容易实现。我们将使用分位数函数来实现结果,如下所示:
> for(i in 1:length(TB)) print(quantile(tcorr[1:TB[i]],c(0.025,0.975)))
2.5% 97.5%
0.5225951 0.9481351
2.5% 97.5%
0.5205679 0.9399541
2.5% 97.5%
0.5429510 0.9513826
2.5% 97.5%
0.4354776 0.9588759
2.5% 97.5%
0.4662406 0.9668964
2.5% 97.5%
0.4787843 0.9667736
2.5% 97.5%
0.4614067 0.9621344
2.5% 97.5%
0.4609731 0.9606689
我们已经学会了如何使用自举技术进行统计推断。
在这里执行引导方法的一个主要原因是,我们不能假设 LSAT 和 GPA 变量服从双变量正态分布。现在,如果我们被告知 LSAT 和 GPA 的历史分布遵循双变量正态分布,那么从技术上讲,可以推导出样本相关系数
的概率分布。然而,作为一个从业者,假设你无法推导出样本相关系数的概率分布。那么你将如何进行统计推断?使用此处讨论的相同技术可能看起来很有吸引力。我们将在下一小节继续探讨这个问题。
参数引导
如前一小节所述,执行常规的 非参数 引导方法可能很有吸引力。然而,非参数方法与参数方法相比,传统上被认为效率较低。我们现在将探讨这两种方法的混合。
我们已经看到,引导方法在很大程度上依赖于重采样。因此,引导样本及其后续估计预计将符合真实的潜在概率分布。然而,我们可能偶尔会更多地了解潜在概率分布的形状,除了几个参数之外。混合这两种方法的方法需要修改。参数引导方法设置和运行如下:
-
令
为来自
的独立同分布样本,令
表示基于适当方法的参数估计器,例如最大似然估计或矩估计法。 -
从
模拟大小为 n 的第一个引导样本
,并使用与上一步相同的估计技术,基于
获得第一个引导估计
。 -
重复执行上一步 B – 1 次以分别获得
,基于引导样本
。 -
基于参数 B 的引导估计
进行推断。
参数自举技术将通过先前的LSAT和GPA变量的例子来展示。对于二元正态分布,均值向量是总体均值的估计量,它具有无偏估计量和 MLE 的统计特性。同样,样本方差-协方差矩阵也给出了对总体方差-协方差矩阵的重要估计。在数据框上应用colMeans以获得向量均值,并使用var函数来计算样本方差-协方差矩阵。R 代码块很容易跟随:
> LS_mean <- colMeans(LS[,2:3])
> LS_var<- var(LS[,2:3])
> LS_mean; LS_var
LSAT GPA
600.266667 3.094667
LSAT GPA
LSAT 1746.780952 7.9015238
GPA 7.901524 0.0592981
因此,我们有了均值和方差-协方差矩阵估计量。现在我们来看参数自举的计算。现在,使用mvtnorm包中的rmvnorm函数,我们能够从多变量(二元)正态分布中模拟观测值。有了(参数)自举样本,程序和结论的其余部分是相似的。完整的 R 程序和结果图如下:
> TB <- c(25,50,100,200,400,800,1600,3200)
> ptcorr <- NULL
> ptcorrB <- NULL
> pseB <- NULL
> myseed <- 54321
> for(i in 1:B){
+ myseed <- myseed+1
+ set.seed(myseed)
+ temp <- rmvnorm(n,LS_mean,LS_var)
+ ptcorr[i] <- as.numeric(cor(temp)[1,2])
+ }
> for(j in 2:B){
+ ptcorrB[j] <- mean(ptcorr[1:j])
+ pseB[j] <- sd(ptcorr[1:j])
+ }
> round(ptcorrB[TB],3)
[1] 0.760 0.782 0.772 0.761 0.766 0.763 0.762 0.766
> round(pseB[TB],3)
[1] 0.129 0.114 0.109 0.129 0.118 0.117 0.120 0.120
> windows(height=100,width=100)
> plot.ts(pseB,xlab="Number of Bootstrap Samples",
+ ylab="Parametric Bootstrap Standard Error of Correlation")
> for(i in 1:length(TB)) print(quantile(ptcorr[1:TB[i]],c(0.025,0.975)))
2.5% 97.5%
0.4360780 0.9048064
2.5% 97.5%
0.5439972 0.9211768
2.5% 97.5%
0.5346929 0.9200953
2.5% 97.5%
0.4229031 0.9179324
2.5% 97.5%
0.4650078 0.9194452
2.5% 97.5%
0.4747372 0.9214653
2.5% 97.5%
0.4650078 0.9245066
2.5% 97.5%
0.4662502 0.9241084
参数自举和非参数自举之间的差异很容易看出。置信区间非常短,随着自举样本数量的增加,标准误差降低到零。尽管有优势,但我们通常在参数方法失败时需要自举方法。看看下面的图:

图 3:相关系数的参数自举标准误差
接下来,我们将考虑一个稍微复杂的问题,用于自举方法的应用。
特征值
多变量统计是统计学的一个分支,它处理随机变量的向量。在先前的例子中,我们有二元数据,其中为十五所学校获得了 LSAT 和 GPA 分数。现在我们将考虑另一个例子,其中我们有多于两个变量;也就是说,这里有五个观测值。描述和自举技术相关的细节来自 Efron 和 Tibshirani (1990)的《一般集成技术》第七章,第七章,该章节讨论了 Mardia, Kent, 和 Bibby (1979)的经典多变量书籍中的得分数据。
符号的简要说明如下。我们将用
表示随机变量的向量,对于第i个观测值,向量将是
。在这里,每个分量Xi被假定为连续随机变量。通常,出于实际和理论目的,我们假设随机向量遵循具有均值向量
和方差-协方差矩阵
的多变量正态分布。由于在这里无法详细介绍多变量统计,感兴趣的读者可以简单地查阅 Mardia, Kent, 和 Bibby (1979)的著作。
在这个例子中,n = 88名学生的五门科目(力学、向量、代数、分析和统计学)的分数被记录下来,并且测试的一个进一步差异是,前两门科目(力学和向量)是闭卷考试,而代数、分析和统计学是开卷考试。我们首先在这里执行一个简单的初步任务,即计算均值向量、方差-协方差矩阵和相关性矩阵:
> OC <- read.csv("../Data/OpenClose.csv")
> pairs(OC)
> OC_xbar <- colMeans(OC)
> OC_xbar
MC VC LO NO SO
38.95455 50.59091 50.60227 46.68182 42.30682
> OC_Cov <- cov(OC)
> OC_Cov
MC VC LO NO SO
MC 305.7680 127.22257 101.57941 106.27273 117.40491
VC 127.2226 172.84222 85.15726 94.67294 99.01202
LO 101.5794 85.15726 112.88597 112.11338 121.87056
NO 106.2727 94.67294 112.11338 220.38036 155.53553
SO 117.4049 99.01202 121.87056 155.53553 297.75536
> OC_Cor <- cor(OC)
> OC_Cor
MC VC LO NO SO
MC 1.0000000 0.5534052 0.5467511 0.4093920 0.3890993
VC 0.5534052 1.0000000 0.6096447 0.4850813 0.4364487
LO 0.5467511 0.6096447 1.0000000 0.7108059 0.6647357
NO 0.4093920 0.4850813 0.7108059 1.0000000 0.6071743
SO 0.3890993 0.4364487 0.6647357 0.6071743 1.0000000
在这里,数据是从.csv文件导入的,通过使用colMeans、cov和cor函数,我们得到了均值向量、方差-协方差矩阵和相关性矩阵。显然,我们可以从相关性矩阵的输出中看到,所有变量之间存在强烈的关联。数据的可视化描述是通过pairs函数获得的,它给我们提供了一个散点图的矩阵。这个图如下所示:

图 4:五门科目分数的散点图矩阵
维度降低是多变量统计的一个目标。给定大量变量,维度降低的目的是找到一组变量,这些变量将解释整体数据中的大部分变异性。一种维度降低的方法是主成分分析。在这里,我们试图找到一个新随机向量
,这是一个主成分向量。这个新随机向量的每个分量都是原始变量的某种线性组合,这将实现两个目标:(a)这些特征值
将按顺序排列,即第一个分量的方差将大于第二个,第二个大于第三个,依此类推;(b)每个主成分与其他主成分不相关。主成分的核心工作与方差-协方差矩阵或相关性矩阵的eigen值相关联。方差-协方差矩阵的eigen值表示相关主成分的重要性。因此,如果有 p 个相关随机变量,并且估计的方差-协方差矩阵不是奇异的,那么归一化的 p 个eigen值将给出由主成分解释的变异性分数。对于数据的目的,我们在这里解释这一点:
> OC_eigen <- eigen(OC_Cov)
> OC_eigen$values
[1] 686.98981 202.11107 103.74731 84.63044 32.15329
> OC_eigen$vectors
[,1] [,2] [,3] [,4] [,5]
[1,] -0.5054457 0.74874751 -0.2997888 0.296184264 -0.07939388
[2,] -0.3683486 0.20740314 0.4155900 -0.782888173 -0.18887639
[3,] -0.3456612 -0.07590813 0.1453182 -0.003236339 0.92392015
[4,] -0.4511226 -0.30088849 0.5966265 0.518139724 -0.28552169
[5,] -0.5346501 -0.54778205 -0.6002758 -0.175732020 -0.15123239
> OC_eigen$values/sum(OC_eigen$values)
[1] 0.61911504 0.18214244 0.09349705 0.07626893 0.02897653
第一个特征值是686.9898,第二个是202.1111,依此类推。现在,这些值除以它们的累积和给出了由主成分解释的数据变异性百分比。因此,第一个主成分解释的数据总变异性为 61.91%,而第二个主成分解释了 18.21%。那么,接下来重要的问题是:我们如何进行与这个数量相关的统计推断?自然地,我们将使用自助法来提供答案:
> thetaB <- NULL; sethetaB <- NULL
> B <- 500
> n <- nrow(OC)
> myseed <- 54321
> for(i in 1:B){
+ myseed <- myseed+1
+ set.seed(myseed)
+ OCt <- OC[sample(1:n,n,replace=TRUE),]
+ OCt_eigen <- eigen(cov(OCt))
+ thetaB[i] <- max(OCt_eigen$values)/sum(OCt_eigen$values)
+ }
> for(j in 2:B){
+ thetaB[j] <- mean(thetaB[1:j])
+ sethetaB[j] <- sd(thetaB[1:j])
+ }
> plot.ts(sethetaB,xlab="Number of Bootstrap Samples",
+ ylab="Bootstrap Standard Error for First Principal Component")

图 5:第一主成分解释的方差 bootstrap 标准误差
95%的 bootstrap 置信区间通常获得:
> TB <- seq(50,500,50)
> for(i in 1:length(TB)) print(quantile(thetaB[1:TB[i]],c(0.025,0.975)))
2.5% 97.5%
0.6300403 0.6478871
2.5% 97.5%
0.6330791 0.6424721
2.5% 97.5%
0.6342183 0.6401195
2.5% 97.5%
0.6348247 0.6394432
2.5% 97.5%
0.6348774 0.6392892
2.5% 97.5%
0.6352836 0.6391456
2.5% 97.5%
0.6357643 0.6390937
2.5% 97.5%
0.6360647 0.6388585
2.5% 97.5%
0.6360818 0.6387047
2.5% 97.5%
0.6361244 0.6386785
经验法则
通常,B = 25 的 bootstrap 重复次数就足够了,很少需要超过200次重复。有关更多信息,请参阅 Efron 和 Tibshirani(1990,第 52 页)。
到目前为止,我们已经使用了模拟、重采样和循环来进行 bootstrap 推断。然而,在章节的早期,我们提到了boot包。在下一节中,我们将使用该包进行一些样本,并说明其用法。
boot 包
boot包是 R 的核心包之一,它针对 bootstrap 方法的实现进行了优化。在前面的例子中,我们主要使用循环来进行重采样技术。在这里,我们将看看如何使用boot R 包。
boot 函数的主要结构如下:
boot(data, statistic, R, sim = "ordinary", stype = c("i", "f", "w"),
strata = rep(1,n), L = NULL, m = 0, weights = NULL,
ran.gen = function(d, p) d, mle = NULL, simple = FALSE, ...,
parallel = c("no", "multicore", "snow"),
ncpus = getOption("boot.ncpus", 1L), cl = NULL)
函数的核心参数是data、statistic、R和stype。data参数是标准的,就像大多数 R 函数一样。statistic是实现boot函数最重要的参数,它将对从数据框中获得的 bootstrap 样本应用此函数。参数R(而不是软件)用于指定要抽取的 bootstrap 样本数量,而stype将指示statistic的第二个参数。要使用boot函数完成任何推断,关键任务是定义统计量的函数。我们将继续使用早期示例进行说明。
在研究LSAT和GPA变量之间的相关性时,技巧是定义一个函数,该函数将包括相关系数函数和以指定方式包含索引的数据,这将给我们提供 bootstrap 样本。在声明用于计算 bootstrap 样本相关系数的函数后,我们使用 boot 函数,引入该函数,并指定所需的 bootstrap 样本重采样类型。现在boot函数将开始工作:
> corx <- function(data,i) cor(data[i,1],data[i,2])
> corboot <- boot(data=LS[,2:3],statistic=corx,R=200,stype="i")
> corboot
ORDINARY NONPARAMETRIC BOOTSTRAP
Call:
boot(data = LS[, 2:3], statistic = corx, R = 200, stype = "i")
Bootstrap Statistics :
original bias std. error
t1* 0.7763745 -0.01791293 0.1357282
相关函数通过corx定义,并在数据框LS上应用 boot 函数。bootstrap 样本的数量为200,并且重采样将在下一次迭代之前发生。从前面的输出中,我们可以获得统计量的值为0.7763745,偏差为-0.01791293,bootstrap 标准误差为0.1357282。但偏差如何呢?到目前为止,我们在讨论中几乎没有提到偏差。为了理解 bootstrap 偏差是什么,我们首先将查看拟合的corboot boot对象组成部分。统计量的值,即这里的相关系数,存储为t0,bootstrap 样本估计值(其中R个)存储在t中,使用这两个数量我们将找到偏差:
> corboot$t0
[1] 0.7763745
> corboot$t
[,1]
[1,] 0.8094277
[2,] 0.7251170
[3,] 0.7867994
[4,] 0.7253745
[5,] 0.7891611
[196,] 0.9269368
[197,] 0.8558334
[198,] 0.4568741
[199,] 0.6756813
[200,] 0.7536155
> mean(corboot$t)-corboot$t0
[1] -0.01791293
我们可以看到boot函数在应用中的实用性。可以通过将confint函数应用到corboot对象上来获得自举置信区间:
> confint(corboot)
Bootstrap quantiles, type = bca
2.5 % 97.5 %
1 0.3294379 0.9441656
接下来,我们将应用boot函数来解决问题,即获得第一主成分解释的变异的置信区间。为此,我们首先创建必要的R函数,该函数可以提供给boot函数:
> Eigen_fn <- function(data,i) {
+ eig <- eigen(cov(data[i,]))
+ val <- max(eig$values)/sum(eig$values)
+ val
+ }
> eigenboot <- boot(data=OC,statistic = Eigen_fn,R=200,stype = "i")
> eigenboot
ORDINARY NONPARAMETRIC BOOTSTRAP
Call:
boot(data = OC, statistic = Eigen_fn, R = 200, stype = "i")
Bootstrap Statistics :
original bias std. error
t1* 0.619115 -0.0002657842 0.0488226
> confint(eigenboot)
Bootstrap quantiles, type = bca
2.5 % 97.5 %
1 0.5242984 0.7130783
因此,无需编写循环即可有效地使用 boot 包。我们使用自举方法的主要目的是估计参数及其函数。接下来将介绍基于自举的假设检验。
自举与假设检验
我们从 t 检验来比较均值和 F 检验来比较方差的自举假设检验问题开始。理解到,由于我们假设比较的两个总体是正态分布,因此测试统计量的分布特性是已知的。为了执行基于 t 检验的 t 统计量的非参数自举,我们首先定义函数,然后运行 Galton 数据集上的 bootstrap 函数。Galton 数据集可在RSADBE包的galton data.frame中找到。galton数据集由928对观测值组成,每对观测值由父母的身高和孩子的身高组成。首先,我们定义t2函数,加载 Galton 数据集,并按照以下步骤运行 boot 函数:
> t2 <- function(data,i) {
+ p <- t.test(data[i,1],data[i,2],var.equal=TRUE)$statistic
+ p
+ }
> data(galton)
> gt <- boot(galton,t2,R=100)
> gt
ORDINARY NONPARAMETRIC BOOTSTRAP
Call:
boot(data = galton, statistic = t2, R = 100)
Bootstrap Statistics :
original bias std. error
t1* -2.167665 0.03612774 0.6558595
> confint(gt)
Bootstrap quantiles, type = percent
2.5 % 97.5 %
1 -3.286426 -0.5866314
Warning message:
In confint.boot(gt) :
BCa method fails for this problem. Using 'perc' instead
> t.test(galton[,1],galton[,2],var.equal=TRUE)
Two Sample t-test
data: galton[, 1] and galton[, 2]
t = -2.1677, df = 1854, p-value = 0.03031
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-0.41851632 -0.02092334
sample estimates:
mean of x mean of y
68.08847 68.30819
读者应比较自举置信区间和由 t 统计量给出的置信区间。
接下来,我们将对方差进行自举假设检验。方差函数是为var.test函数定义的,然后它将被用于boot函数中:
> v2 <- function(data,i) {
+ v <- var.test(data[i,1],data[i,2])$statistic
+ v
+ }
> gv <- boot(galton,v2,R=100)
> gv
ORDINARY NONPARAMETRIC BOOTSTRAP
Call:
boot(data = galton, statistic = v2, R = 100)
Bootstrap Statistics :
original bias std. error
t1* 1.984632 -0.002454309 0.1052697
> confint(gv)
Bootstrap quantiles, type = percent
2.5 % 97.5 %
1 1.773178 2.254586
Warning message:
In confint.boot(gv) :
BCa method fails for this problem. Using 'perc' instead
> var.test(galton[,1],galton[,2])
F test to compare two variances
data: galton[, 1] and galton[, 2]
F = 1.9846, num df = 927, denom df = 927, p-value < 2.2e-16
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
1.744743 2.257505
sample estimates:
ratio of variances
1.984632
读者可以通过比较置信区间和自举置信区间来比较。自举方法已在不同的估计和假设检验场景中得到了演示。在接下来的部分,我们将考虑一些回归模型,在这些模型中,我们关于解释变量的观测有额外的信息。
自举回归模型
在第一章中引入的US Crime数据集,集成技术简介,是线性回归模型可能是一个好选择的一个例子。在这个例子中,我们感兴趣的是理解犯罪率(R)作为平均年龄、南方州指标等十三个相关变量的函数。从数学上讲,线性回归模型如下:

这里,
是 p 协变量,
是截距项,
是回归系数,而
是假设服从正态分布
的误差项。协变量可以写成向量形式,第i个观测值可以总结为
,其中
。n个观测值
被假设为随机独立。线性回归模型已在许多经典回归书籍中详细阐述;例如,参见 Draper 和 Smith(1999)。最近一本书详细介绍了在 R 中实现线性回归模型的方法是 Ciaburro(2018)。正如读者可能已经猜到的,我们现在将拟合一个线性回归模型到美国犯罪数据集上,以开启讨论:
> data(usc)
> usc_Formula <- as.formula("R~.")
> usc_lm <- lm(usc_Formula,usc)
> summary(usc_lm)
Call:
lm(formula = usc_Formula, data = usc)
Residuals:
Min 1Q Median 3Q Max
-34.884 -11.923 -1.135 13.495 50.560
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -6.918e+02 1.559e+02 -4.438 9.56e-05 ***
Age 1.040e+00 4.227e-01 2.460 0.01931 *
S -8.308e+00 1.491e+01 -0.557 0.58117
Ed 1.802e+00 6.496e-01 2.773 0.00906 **
Ex0 1.608e+00 1.059e+00 1.519 0.13836
Ex1 -6.673e-01 1.149e+00 -0.581 0.56529
LF -4.103e-02 1.535e-01 -0.267 0.79087
M 1.648e-01 2.099e-01 0.785 0.43806
N -4.128e-02 1.295e-01 -0.319 0.75196
NW 7.175e-03 6.387e-02 0.112 0.91124
U1 -6.017e-01 4.372e-01 -1.376 0.17798
U2 1.792e+00 8.561e-01 2.093 0.04407 *
W 1.374e-01 1.058e-01 1.298 0.20332
X 7.929e-01 2.351e-01 3.373 0.00191 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 21.94 on 33 degrees of freedom
Multiple R-squared: 0.7692, Adjusted R-squared: 0.6783
F-statistic: 8.462 on 13 and 33 DF, p-value: 3.686e-07
从summary输出中可以看出,拟合的线性回归模型显示了很多信息。从输出中,我们可以在Estimate中找到估计的回归系数。这些估计量的标准误差在Std.Error中,t 统计量的对应值在t value中,以及Pr(>|t|)中的 p 值。我们还可以进一步估计残差标准差
在Residual standard error中。同样,我们可以在Multiple R-squared和Adjusted R-squared中获得相应的多重和调整 R 平方值,在F-statistic中获得整体 F 统计量,最后在p-value中获得模型 p 值。许多这些统计数据/数量/摘要具有清晰的统计特性,因此可以就参数进行精确的统计推断。然而,对于其中的一些,情况并非如此。例如,如果要求调整 R 平方值的置信区间,作者无法回忆起相应的统计分布。因此,利用 bootstrap 技术的便利性,我们可以获得调整 R 平方的 bootstrap 置信区间。寻求调整 R 平方的置信区间的理由是,它对解释 Y 的方差有很好的解释。让我们看看它在 R 软件中的实现。
对于复杂问题,可能会有许多解决方案,但没有一个在理论上优于其他方案。尽管如此,我们有两种主要的方法来对线性回归模型进行自助: (i) 对残差进行自助,和 (ii) 对观测值进行自助。这两种方法也可以适用于任何一般的回归场景。在我们描述这两种方法之前,让
表示
的最小二乘估计,拟合的模型如下:

因此,我们还将有一个误差分布方差项的估计,并用
表示。定义残差向量为
。然后按照以下步骤进行 残差自助:
-
从
中有放回地抽取大小为 n 的样本,并用
表示。 -
对于重新采样的
,使用
获取新的回归量。也就是说,
是(第一个)自助样本 Y 值。 -
使用
和协变量矩阵
,获得回归系数向量
的第一个自助估计。 -
重复这个过程很多次,比如说 B 次。
对观测值进行自助是通常的自助方法,不需要进一步解释。然而,
的秩可能会受到影响,特别是如果协变量是离散变量且只选择了一个因素时。因此,对于任何回归问题,对残差进行自助是最佳方法。
常规的 boot 包将不会很有用,我们将改用 car 包中的 Boot 函数来对线性回归模型进行自助分析。Boot 函数还需要是一个指定的函数,其输出将给出所需统计量的值。因此,我们首先定义一个函数 f,它将返回调整后的 R 平方值:
> f <- function(obj) summary(obj)$adj.r.squared
> usc_boot <- Boot(usc_lm,f=f,R=100)
> summary(usc_boot)
R original bootBias bootSE bootMed
V1 100 0.67833 0.096618 0.089858 0.79162
> confint(usc_boot)
Bootstrap quantiles, type = bca
2.5 % 97.5 %
V1 0.5244243 0.7639986
Warning message:
In norm.inter(t, adj.alpha) : extreme order statistics used as endpoints
因此,调整后的 R 平方值的 95% 自助置信区间为 (0.5244243, 0.7639986)。同样,可以使用自助技术进行与线性回归模型中任何其他参数相关的推断。
自助生存模型*
在第一部分,我们探讨了伪值在执行与生存数据相关的推断中的作用。使用伪值的主要思想是用适当的(期望的)值替换不完整的观察值,然后使用广义估计方程的灵活框架。生存分析和与之相关的专用方法将在本书的第十章 Chapter 10,集成生存模型中详细说明。在此,我们将简要介绍所需的符号,以设置参数。让T表示生存时间,或感兴趣事件发生的时间,我们自然有
,这是一个连续随机变量。假设寿命的累积分布是 F,相关的密度函数是f。由于某些观察值的寿命是不完整的并且受到截尾的影响,我们将无法正确推断关于平均生存时间或中位生存时间等有趣参数。由于截尾存在额外的复杂性,这里只需指出,我们将大量借鉴第十章,集成生存模型中的材料。
提示
- 在第一次阅读时,可以省略带星号的部分,或者如果你已经熟悉相关概念和术语,也可以继续阅读。
boot包中的censboot函数是为了处理生存数据而开发的。在pbc数据集中,感兴趣事件发生的时间是名为time的变量,而观察的完整性由status==2表示。需要survival包来创建可以处理生存数据的Surv对象。然后survfit函数将给出生存函数的估计,这是累积分布函数 1-F 的补数。众所周知,连续非负随机变量的平均值是
,中位生存时间是满足条件
的时间点 u。由于survfit对象的summary可以用来获得所需时间的生存概率,我们将使用它来找到中位生存时间。所有这些参数都内置在Med_Surv函数中,它将返回中位生存时间。
使用Med_Surv函数作为censboot函数的公式/统计量,我们将能够获得中位生存时间的自助估计;随后,使用自助估计,我们可以获得中位生存时间的置信区间。R 程序和输出如下:
> Med_Surv <- function(data){
+ s2 <- survfit(Surv(time,status==2)~1,data=data)
+ s2s <- summary(s2)
+ s2median <- s2s$time[which.min(s2s$surv>0.5)]
+ s2median
+ }
> pbc2 <- pbc[,2:3]
> pbc_median_boot <- censboot(data=pbc2,statistic=Med_Surv,R=100)
> pbc_median_boot
CASE RESAMPLING BOOTSTRAP FOR CENSORED DATA
Call:
censboot(data = pbc2, statistic = Med_Surv, R = 100)
Bootstrap Statistics :
original bias std. error
t1* 3395 21.36 198.2795
> pbc_median_boot$t
[,1]
[1,] 3282
[2,] 3358
[3,] 3574
[4,] 3358
[5,] 3244
[96,] 3222
[97,] 3445
[98,] 3222
[99,] 3282
[100,] 3222
> confint(pbc_median_boot)
Bootstrap quantiles, type = percent
2.5 % 97.5 %
1 3090 3853
Warning message:
In confint.boot(pbc_median_boot) :
BCa method fails for this problem. Using 'perc' instead
对于实际数据,估计的中位生存时间为3395天。中位生存时间的 95%自举置信区间为(3090, 3853)。
为了对平均生存时间进行推断,我们需要使用来自survival包的survmean函数,并适当地提取估计的平均生存时间。Mean_Surv函数执行这一任务。以下是 R 程序及其输出:
> Mean_Surv <- function(data,time){
+ s2 <- survfit(Surv(time,status==2)~1,data=data)
+ smean <- as.numeric(
+ survival:::survmean(s2,rmean=time)[[1]]["*rmean"])
+ smean
+ }
> censboot(data=pbc2,time=2000,statistic=Mean_Surv,R=100)
CASE RESAMPLING BOOTSTRAP FOR CENSORED DATA
Call:
censboot(data = pbc2, statistic = Mean_Surv, R = 100, time = 2000)
Bootstrap Statistics :
original bias std. error
t1* 1659.415 -3.582645 25.87415
读者需要完成的任务是获取平均生存时间的自举置信区间。下一节将讨论使用自举方法对时间序列数据进行处理。
时间序列模型的自举方法*
时间序列数据的一个例子可以在第一章中找到,即New Zealand Overseas数据集中的集成技术简介。参见 Tattar 等人(2016 年)的第十章,集成生存模型。时间序列的独特之处在于观测值彼此之间不是随机独立的。例如,一天的最高温度很可能与前一天的最高温度不独立。然而,我们可能会相信,过去十天的最高温度块与六个月前十天的温度块大部分是独立的。因此,bootstrap方法被修改为block bootstrap方法。boot包中的tsboot函数对自举时间序列数据很有用。tsboot函数的主要结构如下:
tsboot(tseries, statistic, R, l = NULL, sim = "model",
endcorr = TRUE, n.sim = NROW(tseries), orig.t = TRUE,
ran.gen, ran.args = NULL, norm = TRUE, ...,
parallel = c("no", "multicore", "snow"),
ncpus = getOption("boot.ncpus", 1L), cl = NULL)
在这里,statistic和tseries是时间序列数据,这是我们通常感兴趣的功能。R是自举重复的次数,l是从时间序列数据中抽取的块长度。现在,我们考虑估计自回归(AR)时间序列模型的方差问题,并将考虑 AR 模型的最大阶数order.max为 25。Var.fun函数将拟合最佳 AR 模型并获得方差。然后,该函数将被输入到tsboot中,并使用为每个自举样本计算的统计量,我们将获得 95%的自举置信区间:
> Var.fun <- function(ts) {
+ ar.fit <- ar(ts, order.max = 25)
+ ar.fit$var
+ }
> ?AirPassengers
> AP_Boot <- tsboot(AirPassengers,Var.fun,R=999,l=20,sim="fixed")
> AP_Boot
BLOCK BOOTSTRAP FOR TIME SERIES
Fixed Block Length of 20
Call:
tsboot(tseries = AirPassengers, statistic = Var.fun, R = 999,
l = 20, sim = "fixed")
Bootstrap Statistics :
original bias std. error
t1* 906.1192 2080.571 1111.977
> quantile(AP_Boot$t,c(0.025,0.975))
2.5% 97.5%
1216.130 5357.558
因此,我们已经能够应用自举方法对时间序列数据进行处理。
摘要
详细处理自助法的主要目的是为其在重采样方法中的应用奠定基础。我们本章从一种非常早期的重采样方法开始:Jackknife 方法。该方法被用于多个场景的说明,包括生存数据,其本质上很复杂。自助法从看似简单的问题开始,然后我们立即将其应用于复杂问题,例如主成分和回归数据。对于回归数据,我们还展示了自助法在生存数据和时序数据中的应用。在下一章中,我们将探讨自助法在重采样决策树中扮演的核心角色,这是一种典型的机器学习工具。
第三章。Bagging
决策树在第一章,集成技术介绍中介绍,然后应用于五个不同的分类问题。在这里,可以看到它们在某些数据库上的表现比其他数据库更好。我们在构建决策树时几乎只使用了default设置。这一章将从探索一些可能提高决策树性能的选项开始。上一章介绍了bootstrap方法,主要用于统计方法和模型。在这一章中,我们将使用它来构建树。这种方法通常被视为一种机器学习技术。决策树的 bootstrapping 方法通常被称为bagging。类似的一种分类方法是 k-最近邻分类,简称k-NN。我们将在第三部分介绍这种方法,并在本章的最后一部分应用 bagging 技术。
在本章中,我们将涵盖以下内容:
-
分类树和相关剪枝/改进方法
-
Bagging 分类树
-
k-NN 分类器的介绍和应用
-
k-NN bagging 扩展
技术要求
本章我们将使用以下库:
-
class -
FNN -
ipred -
mlbench -
rpar
分类树和剪枝
分类树是决策树的一种特定类型,其主要关注分类问题。Breiman 等人(1984 年)发明了决策树,Quinlan(1984 年)独立地引入了 C4.5 算法。这两者有很多共同之处,但我们将重点关注 Breiman 的决策树学派。Hastie 等人(2009 年)对决策树进行了全面论述,Zhang 和 Singer(2010 年)对递归分割方法进行了论述。Tattar(2017 年)的集成回归模型第九章中可以找到对树的一个直观和系统的 R 程序开发。
一个分类树有许多参数可以调整以改善性能。然而,我们首先将使用默认设置简单地构建分类树并可视化树。rpart包中的rpart函数可以创建分类、回归以及生存树。该函数首先检查 regress 是否为分类、数值或生存对象,并相应地设置相应的分类、回归或生存树,同时使用相关的分割函数。
加载了德国信用数据集,并按照早期设置执行了将数据分为训练和测试部分的操作:
> load("../Data/GC2.RData")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(GC2),replace = TRUE,
+ prob = c(0.7,0.3))
> GC2_Train <- GC2[Train_Test=="Train",]
> GC2_TestX <- within(GC2[Train_Test=="Test",],rm(good_bad))
> GC2_TestY <- GC2[Train_Test=="Test","good_bad"]
> nte <- nrow(GC2_TestX)
> GC2_Formula <- as.formula("good_bad~.")
您可以参考第一章,集成技术简介,了解 R 代码是如何执行的。现在,使用训练数据集和选定的公式,我们将创建第一个分类树。将rpart函数应用于公式和数据集将创建一个分类树。使用plot函数可视化树,uniform=TRUE选项确保显示正确对齐分割的层次级别。此外,文本函数将在分割点显示变量名,use.n=TRUE将给出节点 Y 的分布。最后,使用拟合的分类树对测试数据集的贷款好坏进行预测,对测试样本进行比较,我们发现树的准确率为 70.61%,与第一章,集成技术简介中的相同:
> DT_01 <- rpart(GC2_Formula,GC2_Train)
> windows(height=200,width=200)
> par(mfrow=c(1,2))
> plot(DT_01,uniform=TRUE,main="DT - 01"); text(DT_01,use.n=TRUE)
> DT_01_predict <- predict(DT_01,newdata = GC2_TestX,type="class")
> DT_01_Accuracy <- sum(DT_01_predict==GC2_TestY)/nte
> DT_01_Accuracy
[1] 0.7060703
> DT_01$variable.importance
checking duration savings purpose employed history amount coapp
38.5358 19.6081 15.6824 12.8583 12.5501 9.2985 8.9475 8.1326
age existcr property job resident telephon housing depends
7.3921 6.0250 5.5503 5.2012 2.6356 1.6327 1.3594 0.6871
marital installp foreign
0.6871 0.4836 0.2045
前一段代码的图示是下一张图的左侧树形图,DT-01。从显示中可以看出,终端节点太多,似乎也有很多分割,这意味着我们可能过度拟合了数据。一些终端节点只有七个观测值,而许多终端节点的观测值少于 20 个。因此,还有改进的空间。
在决策树的下一个迭代中,我们要求树算法在节点(minsplit=30)的观测值少于 30 个时不要进一步分割,并且最小桶大小(minbucket=15)必须至少为 15。这个更改应该会改善树DT_01。对于新树,我们再次检查准确率的变化:
> DT_02 <- rpart(GC2_Formula,GC2_Train,minsplit=30,minbucket=15)
> plot(DT_02,uniform=TRUE,main="DT - 02"); text(DT_02,use.n=TRUE)
> DT_02_predict <- predict(DT_02,newdata = GC2_TestX,type="class")
> DT_02_Accuracy <- sum(DT_02_predict==GC2_TestY)/nte
> DT_02_Accuracy
[1] 0.7252396
> DT_02$variable.importance
checking duration savings purpose history amount coapp employed
35.2436 15.5220 15.3025 11.6655 7.8141 7.5564 7.1990 5.6960
property existcr age resident foreign depends marital job
3.7257 1.7646 1.3781 1.1833 0.7883 0.6871 0.6871 0.5353
housing installp
0.5072 0.4581

图 1:德国数据的分类树
DT-02树看起来比DT-01更干净,每个终端节点的观测值数量也相当好。重要的是,准确率提高了0.7252 - 0.7061 = 0.0191,即大约 2%,这是一个改进。
复杂度参数,Cp,是树的一个重要方面,我们现在将使用它来改进分类树。使用cp=0.005参数以及minsplit和minbucket,我们将尝试提高树的表现:
> DT_03 <- rpart(GC2_Formula,GC2_Train,minsplit=30,minbucket=15,
+ cp=0.005)
> plot(DT_03,uniform=TRUE,main="DT - 03"); text(DT_03,use.n=TRUE)
> DT_03_predict <- predict(DT_03,newdata = GC2_TestX,type="class")
> DT_03_Accuracy <- sum(DT_03_predict==GC2_TestY)/nte
> DT_03_Accuracy
[1] 0.7316294
> DT_03$variable.importance
checking duration savings purpose history employed amount coapp
35.7201 15.5220 15.3025 11.6655 7.8141 7.7610 7.5564 7.1990
property age existcr resident marital foreign installp depends
3.7257 1.8547 1.7646 1.5010 1.0048 0.7883 0.7758 0.6871
job housing
0.5353 0.5072
性能现在已从0.7252提高到0.7316,这又是一个改进。在DT-03中,即下一张图的左侧树中,树复杂度结构似乎没有太大变化。我们现在同时进行两个更改。首先,我们将分割标准从 Gini 改为信息,然后添加一个错误矩阵以处理误分类。
什么是误分类的损失矩阵?如果模型以这种方式将一笔好贷款识别或预测为好贷款,则没有误分类。此外,如果将一笔坏贷款分类为坏贷款,这也是算法识别出的正确决策。将一笔好贷款误分类为坏贷款的后果与将坏贷款分类为好贷款的后果不同。例如,如果向一个坏客户发放贷款,损失将是四到六位数的收入损失,而一个被拒绝贷款的好客户可能在三个月后再次申请。如果你运行matrix(c(0,200,500,0), byrow = TRUE, nrow=2),输出将是以下内容:
> matrix(c(0,200,500,0),byrow = TRUE,nrow=2)
[,1] [,2]
[1,] 0 200
[2,] 500 0
这意味着将一笔好贷款误分类为坏贷款的惩罚是 200,而将一笔坏贷款误分类为好贷款的惩罚是 500。惩罚在很大程度上有助于并给分类问题增加了权重。使用此选项和分割标准,我们设置了下一个分类树:
> DT_04 <- rpart(GC2_Formula,GC2_Train,minsplit=30,minbucket=15,
+ parms = list(split="information",
+ loss=matrix(c(0,200,500,0),byrow = TRUE,nrow=2)))
> plot(DT_04,uniform=TRUE,main="DT - 04"); text(DT_04,use.n=TRUE)
> DT_04_predict <- predict(DT_04,newdata = GC2_TestX,type="class")
> DT_04_Accuracy <- sum(DT_04_predict==GC2_TestY)/nte
> DT_04_Accuracy
[1] 0.7380192
> DT_04$variable.importance
checking savings duration purpose employed history amount existcr
26.0182 10.4096 10.2363 5.0949 5.0434 2.1544 1.5439 0.9943
resident age depends marital property
0.9648 0.7457 0.6432 0.6432 0.5360

图 2:具有更多选项的分类树
注意,决策树DT-04似乎比DT-01-03有更少的分割,并且看起来没有过度训练数据。
在这里,我们可以看到许多可以用来调整决策树的选项,其中一些仅适用于分类树。然而,调整一些参数可能需要专业知识,尽管了解这些选项是很好的。注意变量重要性在不同决策树之间的变化顺序。给定数据和树结构,我们如何可靠地确定给定变量的变量重要性?这个问题将在下一节中讨论。接下来还将探讨提高决策树性能的一般方法。
Bagging
Bagging代表Boostap AGGregatING。这是由 Breiman(1994 年)发明的。Bagging 是同质集成的一个例子,这是因为基础学习算法仍然是分类树。在这里,每个自助树将是一个基础学习器。这也意味着当我们对第二章中的线性回归模型进行自助时,自助,我们实际上在那里执行了一个集成。关于结合多棵树的结果的一些评论也是必要的。
集成方法结合了多个模型(也称为基学习器)的输出,并产生一个单一的结果。这种方法的一个好处是,如果每个基学习器都具备所需属性,那么组合结果将具有更高的稳定性。如果某个基学习器在协变量空间的一个特定区域过度训练,其他基学习器将消除这种不希望的预测。集成方法期望的是更高的稳定性,多次带包装有助于提高给定模型集的拟合值性能。Berk(2016)、Seni 和 Elder(2010)、Hastie 等人(2009)可以参考以获取更多详细信息。
一个基本结果! 如果从N个单位中用放回抽样方法抽取N个观测值,那么平均有 37%的观测值被排除在外。
这个结果非常重要。由于我们将执行一个 bootstrap 方法,这意味着平均每个树,我们将有一个 37%的保留样本。一个简短的模拟程序将为我们计算概率。对于N值从 11 到 100 的范围,我们将进行放回抽样,找出有多少索引被排除,然后将该数字除以N以获得从N个单位中放回抽取的N个单位的模拟中未排除单位的经验概率。经验概率是通过B = 100,000多次获得,并将该平均值报告为在N个单位的放回抽取中未选中任何单个单位的概率:
> N <- 11:100
> B <- 1e5
> Prob_Avg <- NULL
> for(i in N){
+ set <- 1:i
+ leftout <- 0
+ for(j in 1:B){
+ s1 <- sample(set,i,replace=TRUE)
+ leftout <- leftout+(i-length(unique(s1)))/i
+ }
+ Prob_Avg[i-10] <- leftout/B
+ }
> Prob_Avg
[1] 0.3504 0.3517 0.3534 0.3549 0.3552 0.3563 0.3571 0.3574 0.3579 0.3585
[11] 0.3585 0.3594 0.3594 0.3601 0.3604 0.3606 0.3610 0.3612 0.3613 0.3614
[21] 0.3620 0.3622 0.3625 0.3622 0.3626 0.3627 0.3627 0.3626 0.3631 0.3634
[31] 0.3635 0.3637 0.3636 0.3638 0.3639 0.3638 0.3640 0.3641 0.3641 0.3641
[41] 0.3644 0.3642 0.3645 0.3643 0.3645 0.3647 0.3645 0.3646 0.3649 0.3649
[51] 0.3648 0.3650 0.3648 0.3650 0.3651 0.3653 0.3649 0.3649 0.3653 0.3653
[61] 0.3654 0.3654 0.3654 0.3654 0.3653 0.3655 0.3654 0.3655 0.3655 0.3657
[71] 0.3657 0.3657 0.3655 0.3658 0.3658 0.3660 0.3656 0.3658 0.3658 0.3658
[81] 0.3658 0.3658 0.3660 0.3658 0.3659 0.3659 0.3662 0.3660 0.3661 0.3661
因此,我们可以看到,从N个单位的放回抽取中抽取的样本中,大约有 0.37 或 37%的观测值未被选中。
带包装算法如下:
-
从由N个观测值组成的数据中抽取一个大小为N的随机样本,并放回。所选的随机样本称为bootstrap 样本.
-
从 bootstrap 样本中构建一个分类树。
-
将每个终端节点分配一个类别,并存储每个观测值的预测类别。
-
重复步骤 1-3 多次,例如,B。
-
通过对树集进行多数投票,将每个观测值分配给一个最终类别。
带包装过程的主要目的是减少不稳定性,这主要通过bootstrap方法实现。如前所述,并通过模拟程序证明,当我们从N个观测值中用放回抽样方法抽取N个观测值时,平均而言,37%的观测值将被排除在样本之外。bagging方法利用这种放回抽样的技术,我们将未选择的观测值称为袋外(OOB)观测值。与bootstrap方法一样,重抽样技术为我们提供了不同参数的多个估计值,利用这种抽样分布,我们可以进行适当的统计推断。为了更严格地证明这种技术,原始的 Breiman(1996)论文值得一读。
注意
注意事项:假设一个观测值在 271 棵树中有 100 次是袋外观测值。在第 100 次标记为测试目的时,该观测值可能被分类为 TRUE 的有 70 次。因此,对于该观测值,可能会得出结论 P(TRUE) = 70/100 = 0.7。这种解释可能会误导,因为袋中的样本不是独立的。
在我们进行软件实现之前,Hastie 等人(2009 年)提出了两个重要的评论。首先,bagging 技术有助于减少估计预测函数的方差,并且它也适用于高方差、低偏差的模型,如树模型。其次,bagging 在聚合中的核心目标是平均许多无偏的噪声模型,因此随后方差减少。与自助法一样,bagging 作为一种平滑方法,可以减少偏差。现在,我们将使用德国信贷数据来阐述 bagging 方法。
来自 ipred 包的 bagging 函数将有助于设置程序:
> B <- 500
> GC2_Bagging <- bagging(GC2_Formula,data=GC2_Train,coob=FALSE,
+ nbagg=B,keepX=TRUE)
> GC2_Margin <- predict(GC2_Bagging,newdata = GC2_TestX,
+ aggregation="weighted",type="class")
> sum(GC2_Margin==GC2_TestY)/nte
[1] 0.7795527
聚合在这里通过显著提高准确性有所帮助。请注意,每次运行 bagging 方法都会得到不同的答案。这主要是因为每棵树都是使用不同的样本设置的,并且允许生成随机样本的种子动态变化。到目前为止,已经尝试将种子固定在某个特定数值以重现结果。向前看,种子很少会被固定。作为一个测试你知识的练习,找出 bagging 函数中指定的 keepx 和 coob 选项的含义。通过使用 ?bagging 来完成这个练习。
回到德国信贷问题!我们已经创建了 B = 500 棵树,出于某种疯狂的原因,我们想查看所有这些树。当然,Packt(本书的出版商)可能会对作者坚持打印所有 500 棵树感到有些恼火,而且,由于这些树没有经过修剪,它们看起来会很丑。程序必须符合书籍大小的限制。考虑到这一点,让我们从以下代码开始:
> pdf("../Output/GC2_Bagging_Trees.pdf")
> for(i in 1:B){
+ tt <- GC2_Bagging$mtrees[[i]]
+ plot(tt$btree)
+ text(tt$btree,use.n=TRUE)
+ }
> dev.off()
pdf
2
以下步骤在先前的代码中已执行:
-
我们首先将调用 PDF 设备。
-
然后,在
Output文件夹中创建了一个新文件。 -
接下来,我们从
1到B开始一个循环。bagging对象由B = 500棵树组成,在一个临时对象tt中,我们存储第 i 棵树的细节。 -
然后,我们使用
plot函数绘制该树,通过从tt中提取树细节,并添加与该树的节点和分割相关的相关文本。 -
循环完成后,运行
dev.off行,这将保存GC2_Bagging_Trees.pdf文件。这个便携式文档文件将包含 500 棵树。
关于自举法的益处已经有很多讨论,并且在上一章中也进行了很多说明。然而,除了在许多博客和参考资料中展示的常规优势之外,我们在这里还将展示如何获得变量重要性的可靠推断。很容易看出,变量重要性在树之间有很大的差异。但这并不是问题。当我们被问到每个变量的决策树中变量重要性的整体可靠性时,我们现在可以查看它们在树中的值并执行推断:
> VI <- data.frame(matrix(0,nrow=B,ncol=ncol(GC2)-1))
> vnames <- names(GC2)[-20]
> names(VI) <- vnames
> for(i in 1:B){
+ VI[i,] <- GC2_Bagging$mtrees[[i]]$btree$variable.importance[vnames]
+ }
> colMeans(VI)
checking duration history purpose amount savings employed installp
50.282 58.920 33.540 48.301 74.721 30.838 32.865 18.722
marital coapp resident property age housing existcr job
17.424 8.795 18.171 20.591 51.611 9.756 11.433 14.015
depends telephon foreign
NA NA NA
上述程序需要解释。回想一下,variable.importance是按降序显示的。如果你已经看过GC2_Bagging_Trees.pdf文件(即使是粗略地看),你会看到不同的树有不同的主要分割变量,因此变量的重要性顺序也会不同。因此,我们首先将需要的变量顺序保存在vnames对象中,然后按照vnames中的顺序对每个树的variable.importance[vnames]进行排序。循环中的每个树都通过$mtrees和$btree$variable.importance提取出来以完成所需操作。因此,VI data.frame对象现在由自举过程设置的 500 棵树的变量重要性组成。colMeans给出了 500 棵树的重要性汇总,通过查看VI框架中的详细信息可以进行所需的统计推断。请注意,最后三个变量在汇总平均值中有NA。NA结果的原因是在某些分类树中,这些变量根本没有任何增益,甚至不在任何替代分割中。我们可以快速发现有多少棵树没有这些三个变量的重要性信息,然后使用na.rm=TRUE选项重复计算colMeans:
> sapply(VI,function(x) sum(is.na(x)))
checking duration history purpose amount savings employed installp
0 0 0 0 0 0 0 0
marital coapp resident property age housing existcr job
0 0 0 0 0 0 0 0
depends telephon foreign
9 35 20
> colMeans(VI,na.rm=TRUE)
checking duration history purpose amount savings employed installp
50.282 58.920 33.540 48.301 74.721 30.838 32.865 18.722
marital coapp resident property age housing existcr job
17.424 8.795 18.171 20.591 51.611 9.756 11.433 14.015
depends telephon foreign
6.345 5.167 3.200
在上一节中,我们探索了minsplit、minbucket、split和loss参数的各种选项。自举法能否包含这些指标?使用bagging函数的control参数,我们现在将对之前的结果进行改进。附加参数的选择与之前保持一致。在拟合自举对象后,我们检查准确性,然后将分类树写入GC2_Bagging_Trees_02.pdf文件。显然,这个文件中的树比GC2_Bagging_Trees.pdf文件中的树更容易阅读,这是预料之中的。以下代码也获得了B = 500棵树的变量信息表:
> GC2_Bagging_02 <- bagging(GC2_Formula,data=GC2_Train,coob=FALSE,
+ nbagg=B,keepX=TRUE,
+ control=rpart.control(minsplit=30,minbucket=15,
+split="information",loss=matrix(c(0,200,500,0), byrow = TRUE, nrow=2)))
> GC2_Margin_02 <- predict(GC2_Bagging_02,newdata = GC2_TestX,
+ aggregation="weighted",type="class")
> sum(GC2_Margin_02==GC2_TestY)/nte
[1] 0.7604
> pdf("../Output/GC2_Bagging_Trees_02.pdf")
> for(i in 1:B){
+ tt <- GC2_Bagging_02$mtrees[[i]]
+ plot(tt$btree)
+ text(tt$btree,use.n=TRUE)
+ }
> dev.off()
null device
1
> VI_02 <- data.frame(matrix(0,nrow=B,ncol=ncol(GC2)-1))
> names(VI_02) <- vnames
> for(i in 1:B){
+ VI_02[i,] <- GC2_Bagging_02$mtrees[[i]]$btree$variable.importance[vnames]
+ }
> colMeans(VI_02,na.rm=TRUE)
checking duration history purpose amount savings employed installp
38.3075 18.9377 11.6756 19.1818 18.4385 16.1309 9.6110 3.6417
marital coapp resident property age housing existcr job
4.3520 4.4913 3.4810 6.5278 10.0255 3.3401 3.1011 4.5115
depends telephon foreign
1.6432 2.5535 0.9193
Bagging 的树的数量已被任意选择为 500。没有特别的原因。我们现在将看到测试数据的准确率如何随着树的数量而变化。检查将在树的数量从 1 到 25,每次增加 1 的情况下进行,然后增加 25 到 50,75,……,475,500。读者需要自己理解这个图。同时,以下程序很简单,不需要进一步解释:
> Bags <- c(1:24,seq(25,B,25))
> Bag_Acc <- NULL
> for(i in 1:length(Bags)){
+ TBAG <- bagging(GC2_Formula,data=GC2_Train,coob=FALSE,
+ nbagg=i,keepX=TRUE,
+ control=rpart.control(minsplit=30,minbucket=15,
+ split="information",
+ loss=matrix(c(0,200,500,0),
+ byrow = TRUE,
+ nrow=2)))
+ GC2_Margin_TBAG <- predict(TBAG,newdata = GC2_TestX,
+ aggregation="weighted",type="class")
+ Bag_Acc[i] <- sum(GC2_Margin_TBAG==GC2_TestY)/nte
+ print(Bags[i])
+ }
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
[1] 23
[1] 24
[1] 25
[1] 50
[1] 75
[1] 475
[1] 500
> plot(Bags,Bag_Acc,"l",ylab="Accuracy")
以下是由以下生成的输出:

图 3:Bagging 准确率与树的数量
分析技术不属于炼金术的范畴。如果发明了这样的程序,我们就不必担心建模了。下一个例子将表明,Bagging 也可能出错。
Bagging 不是一个保证的食谱!
在他的书的第一个版本中,Berk(2016)警告读者不要成为新发明方法宣称的优越性的牺牲品。回想一下在第一章中引入的皮马印第安人糖尿病问题,“集成技术简介”。那里的准确率表显示决策树给出了0.7588的准确率。我们现在应用 Bagging 方法对同一分区进行计算,并计算准确率如下:
> data("PimaIndiansDiabetes")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(PimaIndiansDiabetes),replace = TRUE,prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> PimaIndiansDiabetes_Train <- PimaIndiansDiabetes[Train_Test=="Train",]
> PimaIndiansDiabetes_TestX <- within(PimaIndiansDiabetes[Train_Test=="Test",],
+ rm(diabetes))
> PimaIndiansDiabetes_TestY <- PimaIndiansDiabetes[Train_Test=="Test","diabetes"]
> PID_Formula <- as.formula("diabetes~.")
> PID_Bagging <- bagging(PID_Formula,data=PimaIndiansDiabetes_Train,coob=FALSE,
+ nbagg=1000,keepX=TRUE)
> PID_Margin <- predict(PID_Bagging,newdata = PimaIndiansDiabetes_TestX,
+ aggregation="weighted",type="class")
> sum(PID_Margin==PimaIndiansDiabetes_TestY)/257
[1] 0.7548638
这里的总体准确率是0.7548,而单个树分类模型的准确率是0.7588,这意味着 Bagging 降低了准确率。如果理解了 Bagging 的目的,这并不是一个担忧。目的始终是增加预测的稳定性,因此我们宁愿接受 Bagging 会降低方差项。
Bagging 是一个非常重要的技术,你不需要仅将其限制在分类树上,而应该继续探索其他回归方法,例如神经网络和支持向量机。在下一节中,我们将介绍 k 最近邻(k-NN)的不同方法。
k-NN 分类器
在第一章中,我们熟悉了各种分类模型。一些读者可能已经熟悉了k-NN 模型。k-NN 分类器是最简单、最直观和非假设性的模型之一。模型的名字本身就暗示了它可能的工作方式——最近的邻域!而且这还伴随着k!因此,如果我们有一个研究中的N个点,我们就会找到邻域中的k个最近点,然后记录下 k 个邻居的类别。然后,将 k 个邻居的多数类别分配给单元。在回归的情况下,将邻居的平均值分配给单元。以下是对k-NN 的视觉描述:

图 4:k-NN 的可视化表示
k-NN 的可视化表示的左上部分显示了 27 个观察值的散点图,其中 16 个是圆圈,剩下的 11 个是正方形。圆圈用橙色标记
,而正方形用蓝色标记
。假设我们选择基于 k = 3 个邻居来设置一个分类器。然后,对于每个点,我们找到它的三个邻居,并将大多数颜色分配给它。因此,如果一个圆圈保持橙色,它已经被正确识别,同样,如果一个正方形被正确识别,它的颜色将保持蓝色。然而,如果一个圆圈点有三个最近的邻居中有两个或更多是正方形,它的颜色将变为蓝色,这将被标记为
。同样,一个错误分类的正方形的颜色将变为橙色
。在前面的图的右上块中,我们有 3-NN 的预测,而 7-NN 的预测可以在图的左下角找到。注意,在 3-NN 中,我们有五个错误分类,而在 7-NN 中,我们有七个。因此,增加最近邻居的数量并不意味着准确率会增加。
右下角的图是 1-NN,它总是给出完美的分类。然而,它就像一个每天只准确显示两次时间的坏钟。
分析波形数据
接下来,我们将对波形数据进行分析。重复 第一章 中的代码,即 集成技术简介,以获取波形数据,然后将其分为训练和测试部分:
> set.seed(123)
> Waveform <- mlbench.waveform(5000)
> Waveform$classes <- ifelse(Waveform$classes!=3,1,2)
> Waveform_DF <- data.frame(cbind(Waveform$x,Waveform$classes)) # Data Frame
> names(Waveform_DF) <- c(paste0("X",".",1:21),"Classes")
> Waveform_DF$Classes <- as.factor(Waveform_DF$Classes)
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(Waveform_DF),replace = TRUE,
+ prob = c(0.7,0.3))
> Waveform_DF_Train <- Waveform_DF[Train_Test=="Train",]
> Waveform_DF_TestX <- within(Waveform_DF[Train_Test=="Test",],rm(Classes))
> Waveform_DF_TestY <- Waveform_DF[Train_Test=="Test","Classes"]
> Waveform_DF_Formula <- as.formula("Classes~.")
> plot(Waveform_DF_Train$X.1,Waveform_DF_Train$X.8,col=Waveform_DF_Train$Classes)
下图展示了两个变量 X.1 和 X.8 通过类别进行的一个简单的散点图显示。在显示中可以看到很多颜色的交互,看起来任何形式的逻辑回归模型或决策树都不会帮助这种情况。可能 k-NN 在这种情况下会有所帮助:

图 5:波形数据的散点图
对于 k = 10,我们首先为波形数据建立了一个 k-NN 模型。这里使用了 class 包中的 knn 函数:
> WF_knn <- knn(train=Waveform_DF_Train[,-22],test=Waveform_DF_TestX,
+ cl=Waveform_DF_Train$Classes,k=10)
> sum(Waveform_DF_TestY==WF_knn)/nrow(Waveform_DF_TestX)
[1] 0.903183
90.32% 的准确率看起来很有希望,并且优于 第一章 中考虑的所有模型,即 集成技术简介,除了支持向量机。我们将扩大 k 的搜索范围,从 2 到 50:
> k <- c(2:15,seq(20,50,5))
> knn_accuracy <- NULL
> for(i in 1:length(k)){
+ WF_temp_knn <- knn(train=Waveform_DF_Train[,-22],test=Waveform_DF_TestX,
+ cl=Waveform_DF_Train$Classes,k=k[i])
+ knn_accuracy <- c(knn_accuracy,sum(Waveform_DF_TestY==WF_temp_knn)/
+ nrow(Waveform_DF_TestX))
+ }
> knn_accuracy
[1] 0.8561 0.8919 0.8893 0.8886 0.8932 0.8985 0.8972 0.8992 0.9012 0.9025
[11] 0.9032 0.9058 0.9105 0.9019 0.8999 0.9072 0.9065 0.9098 0.9118 0.9085
[21] 0.9072
注意,对于 k = 40,我们得到了最大的准确率。为什么?我们将在下一节中将 k-NN 放入一个袋中。
k-NN 袋装
在上一节中,k-NN 分类器引入了一个分类模型。我们可以使用自助法使其更加稳健。更广泛的算法保持不变。与典型的自助法一样,我们可以始终编写一个由循环组成的程序,根据所需的 bootstrap 样本数或袋数,控制可以很容易地指定。然而,这里我们将使用 FNN R 包中的一个函数。ownn 函数对于在 k-NN 分类器上执行袋装方法非常有用。
ownn 函数要求数据集中的所有变量都必须是数值型。然而,我们确实有很多是因子变量的。因此,我们需要调整数据,以便可以使用 ownn 函数。首先,使用 rbind 函数将训练集和测试集的协变量数据合并在一起。使用 model.matrix 函数和公式 ~.-1,我们将所有因子变量转换为数值变量。这里的关键问题是 model.matrix 函数是如何工作的?为了使解释简单,如果一个因子变量有 m 个水平,它将创建 m - 1 个新的二元变量,这些变量将跨越 m 维度。我们之所以将训练集和测试集的协变量合并在一起,是因为如果任何因子在任何分区中的水平较少,变量的数量将不相等,我们就无法在测试集上采用基于训练集构建的模型。在获得所有数值变量的协变量矩阵后,我们再次将协变量分为训练和测试区域,使用 ownn 函数指定 k-NN 设置,并预测使用袋装方法的准确度。程序如下:
> All_Cov <- rbind(GC2_Train[,-20],GC2_TestX)
> All_CovX <- model.matrix(~.-1,All_Cov)
> GC2_Train_Cov <- All_CovX[1:nrow(GC2_Train),]
> GC2_Test_Cov <- All_CovX[(nrow(GC2_Train)+1):nrow(All_CovX),]
> k <- seq(5,50,1)
> knn_accuracy <- NULL
> for(i in 1:length(k)){
+ GC2_knn_Bagging <- ownn(train=GC2_Train_Cov, test=GC2_Test_Cov,
+ cl=GC2_Train$good_bad,testcl=GC2_TestY,k=k[i])
+ knn_accuracy[i] <- GC2_knn_Bagging$accuracy[3]
+ }
> knn_accuracy
[1] 0.6198083 0.6293930 0.6357827 0.6549521 0.6549521 0.6645367 0.6869010
[8] 0.6932907 0.7028754 0.7092652 0.7092652 0.7188498 0.7284345 0.7316294
[15] 0.7348243 0.7348243 0.7412141 0.7412141 0.7444089 0.7476038 0.7476038
[22] 0.7507987 0.7476038 0.7476038 0.7476038 0.7476038 0.7444089 0.7444089
[29] 0.7444089 0.7444089 0.7444089 0.7444089 0.7412141 0.7444089 0.7444089
[36] 0.7444089 0.7412141 0.7412141 0.7412141 0.7412141 0.7444089 0.7444089
[43] 0.7444089 0.7444089 0.7444089 0.7444089
> windows(height=100,width=100)
> plot.ts(knn_accuracy,main="k-NN Accuracy")
train、test、cl 和 testcl 参数很容易理解;请参阅 ownn,我们将在一个 5-50 的网格上改变邻居的数量。我们是否指定了袋数或 bootstrap 样本数?现在,袋装操作已完成,并给出了袋装预测。看起来估计可能存在近似,因为包和函数明确表示预测是基于 bagging 的:
> GC2_knn_Bagging$accuracy
knn ownn bnn
0.7444089 0.7444089 0.7444089
> GC2_knn_Bagging$bnnpred
[1] good good good good good good good good good good good good good
[14] good good good good good good good good good bad good good good
[27] good good good good good good good good good good bad good good
[274] good good good good good good good good good good good good good
[287] good good good good good good good good good bad bad good good
[300] good good good good good good good good good good good good good
[313] good
Levels: bad good
下面的准确度图显示,模型准确度在大约 20 个邻域后趋于稳定。因此,我们已经执行了 k-NN 袋装技术:

图 6:k-NN 袋装方法的准确度
摘要
Bagging 本质上是一种集成学习方法,由同质的基本学习器组成。Bagging 作为一种自助聚合方法被引入,我们在第二章中看到了自助方法的一些优点,自助聚合。Bagging 方法的优点是预测的稳定性。本章从对分类树的修改开始,我们看到了改进决策树性能的不同方法,以便树不会过度拟合数据。决策树的 Bagging 和相关技巧在下一节中介绍。然后我们介绍了k-NN 作为重要的分类器,并用一个简单的例子进行了说明。本章以k-NN 分类器的 Bagging 扩展结束。
Bagging 有助于减少决策树的方差。然而,由于许多共同观察结果生成它们,两个自助样本的树是相关的。在下一章中,我们将探讨创新的重新采样,这将使两个决策树不相关。
第四章:随机森林
上一章介绍了袋装法作为一种基于同质基学习器的集成技术,其中决策树作为基学习器。袋装法的一个轻微不足是自助树之间存在相关性。因此,尽管预测的方差得到了降低,但偏差将持续存在。Breiman 提出了在每个分割处随机抽样协变量和独立变量的方法,这种方法随后有助于去相关自助树。
在本章的第一节中,介绍了随机森林算法并进行了说明。变量重要性的概念对于决策树及其所有变体至关重要,因此有一节专门用于清晰地阐述这一概念。随机森林的表现是否优于袋装法?答案将在下一节中给出。
Breiman 在随机森林的背景下阐述了邻近图的重要性,我们很快就会深入探讨这一点。如此复杂的算法将有很多细节,其中一些将通过程序和真实数据来展示。缺失数据几乎是普遍存在的,我们将承担使用随机森林插补缺失值的任务。尽管随机森林主要是一种监督学习技术,但它也可以用于对数据进行聚类,这一主题将是最后一节。
本章的核心主题如下:
-
随机森林算法
-
决策树和随机森林的变量重要性
-
比较随机森林与袋装法
-
使用邻近图
-
随机森林的细节、细节和细微差别
-
通过使用随机森林处理缺失数据
-
使用随机森林进行聚类
技术要求
在本章中,我们将使用以下库:
-
kernlab -
randomForest -
randomForestExplainer -
rpart
随机森林
第三章,袋装,通过自助原理泛化了决策树。在我们开始随机森林之旅之前,我们将快速回顾决策树的历史,并突出其一些优点和缺点。决策树的发明是通过一系列论文的累积,而树的当前形式可以在 Breiman 等人(1984 年)的论文中找到详细信息。Breiman 的方法广为人知为分类和回归树,简称CART。在 20 世纪 70 年代末和 80 年代初,Quinlan 独立于 Breiman 发明了一种称为 C4.5 的算法。更多信息,请参阅 Quinlan(1984)。在很大程度上,当前形式的决策树、袋装和随机森林归功于 Breiman。还有一种类似的方法也存在于一个流行的缩写为 CHAID 的算法中,代表Chi-square Automatic Interaction Detector。CART 的深入探讨可以在 Hastie 等人(2009 年)的书中找到,而统计视角可以在 Berk(2016 年)的书中找到。Seni 和 Elder(2010 年)也有一套出色的简短笔记。没有特定的方向,我们强调 CART 的一些优点和缺点:
-
树自动解决变量选择问题,因为每次分割时,它们都寻找在回归变量中给出最佳分割的变量,因此树消除了无用的变量。
-
树不需要数据预处理。这意味着我们不必考虑转换、缩放和/或证据权重预处理。
-
树在计算上是可扩展的,时间复杂度是可管理的。
-
树提供了一个称为变量重要性的度量,该度量基于变量对树中所有分割的误差减少的贡献。
-
树有效地处理缺失值,如果一个观测值有缺失值,树将继续使用观测值的可用值。处理缺失数据通常是通过代理分割的概念来实现的。
-
树的参数较少,如前一章所示。
-
树具有简单的自顶向下的解释。
-
深度大的树往往几乎是无偏的。
-
变量之间的交互效应很容易识别。
-
它的缺点是拟合的模型不是连续的,并且会有尖锐的边缘。本质上,树是分段常数回归模型。
-
树不能近似低交互的目标函数。
-
树的贪婪搜索方法会导致高方差。
树的第一种扩展可以在前一章讨论的袋装算法中看到。假设我们有 N 个观测值。对于每个自助样本,我们用替换法抽取 N 个观测值。两个自助样本之间可能有多少观测值是共同的?让我们先写一个简单的程序来找出它,使用简单的sample函数:
> N <- seq(1e3,1e4,1e3)
> N
[1] 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000
> B <- 1e3
> Common_Prob <- NULL
>index<- 1
>for(i in N){
+ temp_prob <- NULL
+ for(j in 1:B){
+ s1 <- sample(i,size=i,replace=TRUE)
+ s2 <- sample(i,size=i,replace=TRUE)
+ temp_prob <- c(temp_prob,length(intersect(s1,s2))/i)
+ }
+ Common_Prob[index] <- mean(temp_prob)
+ index<- index + 1
+ }
> Common_Prob
[1] 0.4011 0.4002 0.3996 0.3982 0.3998 0.3996 0.3994 0.3997 0.3996 0.3995
这个程序需要解释。观测值数量 N 从 1000 到 10000 不等,每次增加 1000,我们进行 B = 1e3 = 1000 次自助迭代。现在,对于固定的 N 大小,我们抽取两个有放回的样本,大小为 N,看看它们之间有多少观测值是共同的,然后除以 N。B = 1000 个样本的平均值是两个样本之间找到共同观测值的概率。等价地,它给出了两个样本之间的共同观测值百分比。
自助概率清楚地表明,大约 40%的观测值将在任意两棵树之间是共同的。因此,这些树将是相关的。
在第十五章中,Hastie 等人(2009 年)指出,袋装树是独立同分布的树,因此任何一棵树的期望值与任何其他树的期望值相同。因此,袋装树的偏差与单个树的偏差相同。因此,方差减少是袋装提供的唯一改进。假设我们有一个 B 个独立同分布的随机变量,其方差为
。样本平均值的方差为
。然而,如果我们知道变量只是同分布的,并且存在
的正相关对,那么样本平均值的方差如下:

注意,随着 B 样本数量的增加,第二项消失,第一项保持不变。因此,我们看到袋装树的关联性限制了平均化的好处。这促使布莱曼创新,使得后续的树不会相关。
布莱曼的解决方案是在每次分割之前,随机选择 m < p 个输入变量进行分割。这为随机森林奠定了基础,我们在数据中“摇动”以改善性能。请注意,仅仅“摇动”并不能保证改进。这个技巧在我们有高度非线性估计器时很有帮助。正式的随机森林算法,根据 Hastie 等人(2009 年)和 Berk(2016 年)的描述,如下所示:
-
从数据中抽取一个大小为 N 的有放回的随机样本。
-
从预测值中抽取一个不放回的随机样本。
-
按照常规方式构建数据的第一层递归分割。
-
重复步骤 2,直到树达到所需的大小。重要的是不要剪枝。计算每个终端节点的比例。
-
将树外的(OOB)数据向下传递到树中,并存储每个观测值分配的类别,以及每个观测值的预测值。
-
重复步骤 1-5 多次,例如 1000 次。
-
当观测值是 OOB 时,仅使用分配给每个观测值的类别,计算观测值在树中被分类为一个类别和另一个类别的次数。
-
当该案例为 OOB 时,通过在树集合中对每个案例进行多数投票将其分配到类别。
从他的实践经验中,Breiman 建议在每次分割时随机选择一定数量的协变量,如
对于分类问题,最小节点大小为 1,而对于回归问题,建议
最小节点大小为 5。
我们将使用 randomForest R 包进行软件实现。将使用德国信用数据进行进一步分析。如果你还记得,在第一章,集成技术介绍中,使用基本分类树获得的准确率为 70%。我们将使用与之前相同的设置设置德国信用数据,并将构建随机森林:
>load("../Data/GC2.RData")
>set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(GC2),
+ replace = TRUE,prob = c(0.7,0.3))
> GC2_Train <- GC2[Train_Test=="Train",]
> GC2_TestX <- within(GC2[Train_Test=="Test",],rm(good_bad))
> GC2_TestY <- GC2[Train_Test=="Test","good_bad"]
> GC2_Formula <- as.formula("good_bad~.")
> GC2_RF <- randomForest(GC2_Formula,data=GC2_Train,ntree=500)
> GC2_RF_Margin <- predict(GC2_RF,newdata = GC2_TestX,type="class")
>sum(GC2_RF_Margin==GC2_TestY)/313
[1] 0.7795527
randomForest 函数如前所述应用于 formula 和 data。在这里,我们指定了树的数量为 500,使用 ntree=500。
如果我们将这个随机结果与上一章中的 bagging 结果进行比较,那里的准确率仅为 0.78。这里我们有 p = 19 个协变量,因此我们将尝试在 8 处增加用于分割的协变量样本数量,并看看它的表现:
> GC2_RF2 <- randomForest(GC2_Formula,data=GC2_Train,mtry=8,
+ ntree=500)
> GC2_RF_Margin <- predict(GC2_RF,newdata = GC2_TestX, type="class")
> GC2_RF2_Margin <- predict(GC2_RF2,newdata = GC2_TestX,type="class")
> sum(GC2_RF2_Margin==GC2_TestY)/313
[1] 0.7859425
增加 0.01 或大约 1%可能看起来很少。然而,在银行环境中,这种准确率将转化为数百万美元。我们将使用常规的 plot 函数:
>plot(GC2_RF2)
> GC2_RF2.legend <- colnames(GC2_RF2$err.rate)
> legend(x=300,y=0.5,legend = GC2_RF2.legend,lty=c(1,2,3), col=c(1,2,3))
>head(GC2_RF2$err.rate,10)
OOB bad good
[1,] 0.3206751 0.4743590 0.2452830
[2,] 0.3218673 0.4769231 0.2490975
[3,] 0.3222656 0.5437500 0.2215909
[4,] 0.3006993 0.5224719 0.2005076
[5,] 0.3262643 0.5445026 0.2274882
[6,] 0.3125000 0.5522388 0.2027335
[7,] 0.3068702 0.5631068 0.1893096
[8,] 0.2951807 0.5741627 0.1670330
[9,] 0.2976190 0.5619048 0.1774892
[10,] 0.2955882 0.5801887 0.1666667
以下图形是使用 plot 函数执行的前述代码的输出:`

图 1:德国信用数据随机森林的错误率
我们有三个曲线:OOB 的错误率、好类别的错误率和坏类别的错误率。请注意,错误率在大约 100 棵树时稳定。使用损失矩阵,可能可以减少三条曲线之间的差距。理想情况下,三条曲线应该尽可能接近。
练习:创建具有 split 标准选项、loss 矩阵、minsplit 和不同 mtry 的随机森林。检查错误率曲线并准备总结。
可视化随机森林!树在哪里?显然,我们需要做很多练习来从拟合的 randomForest 对象中提取树。在 Utilities.R 文件中定义了一个新的函数 plot_RF,我们将在下面显示它:

plot_RF 函数首先获取森林中的 $ntree 树的数量。然后它将运行一个 for 循环。在循环的每次迭代中,它将使用 getTree 函数提取与该树相关的信息并创建一个新的 dendogram 对象。然后可视化 dendogram,它就是树。此外,print 命令是可选的,可以被禁用。
在以下图中显示了 PDF 文件中森林中的四个任意选择的树,随机森林的树:

图 2:随机森林的树
快速浏览了 Pima 印第安人糖尿病问题。在第一章的准确率表中,集成技术介绍,我们可以看到决策树的准确率为 0.7588,或 75.88%:
> data("PimaIndiansDiabetes")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(PimaIndiansDiabetes),
+ replace = TRUE, prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> PimaIndiansDiabetes_Train <- PimaIndiansDiabetes[Train_Test=="Train",]
> PimaIndiansDiabetes_TestX <- within(PimaIndiansDiabetes[Train_Test=="Test",],rm(diabetes))
> PimaIndiansDiabetes_TestY <- PimaIndiansDiabetes[
+ Train_Test=="Test","diabetes"]
> PID_Formula <- as.formula("diabetes~.")
> PID_RF <- randomForest(PID_Formula,data=PimaIndiansDiabetes_Train,coob=TRUE,
+ ntree=500,keepX=TRUE,mtry=5,
+ parms=list(prior=c(0.65,0.35)))
> PID_RF_Margin <- predict(PID_RF,newdata = PimaIndiansDiabetes_TestX, type="class")
> sum(PID_RF_Margin==PimaIndiansDiabetes_TestY)/257
[1] 0.7704
因此,我们的准确率提高了 0.7704 - 0.7588 = 0.0116,或大约 1.2%。
练习:获取 Pima 印第安人糖尿病问题的错误率图。
变量重要性
统计模型,例如线性回归和逻辑回归,通过 p 值和 t 统计量等指标来指示哪些变量是显著的。在决策树中,分割是由单个变量引起的。如果指定代理分割的变量数量,某个变量可能在树中多次作为分割标准出现,而某些变量可能根本不会出现在树分割中。在每次分割中,我们选择导致最大不纯度减少的变量,并且一个变量在整个树分割中的贡献也会不同。整个树分割的总体改进(通过分类树的不纯度减少或通过分割标准的改进)被称为变量重要性。在集成方法如 bagging 和随机森林的情况下,对技术中的每棵树进行变量重要性的测量。虽然变量重要性的概念很简单,但其计算理解往往不清楚。这主要是因为没有给出数学形式的公式或表达式。下面的简单代码展示了这个想法。
来自rpart包的kyphosis数据集包含四个变量,这里的目标变量命名为Kyphosis,表示手术后存在脊柱侧弯类型的变形。三个解释变量是Age、Number和Start。我们使用maxsurrogate=0选项构建了一个没有代理变量的分割标准的分类树。零代理变量的选择确保我们在分割处只有一个变量。树被设置并如下可视化:
> data(kyphosis)
> kc<- rpart(Kyphosis~.,data=kyphosis,maxsurrogate=0)
> plot(kc);text(kc)

图 3:脊柱侧弯分类树
在无代理树中,第一个分割变量是Start,分割的右侧部分有一个终端叶。左侧/分区进一步使用Start变量再次分割,左侧有一个终端节点/叶,右侧有一个后续分割。在接下来的两个分割点,我们只使用Age变量,Number变量在整个树中任何地方都没有使用。因此,我们预计Number变量的重要性为零。
在拟合的分类树上使用 $variable.importance,我们获得了三个解释变量的变量重要性:
>kc$variable.importance
Start Age
7.783 2.961
如预期的那样,Number 变量没有显示任何重要性。Start 的重要性给出为 7.783,而 Age 为 2.961。要理解 R 如何计算这些值,请在分类树上运行 summary 函数:
>summary(kc)
Call:
rpart(formula = Kyphosis ~ ., data = kyphosis, maxsurrogate = 0)
n= 81
CP nsplit rel error xerror xstd
1 0.17647 0 1.0000 1 0.2156
2 0.01961 1 0.8235 1 0.2156
3 0.01000 4 0.7647 1 0.2156
Variable importance
Start Age
72 28
Node number 1: 81 observations, complexity param=0.1765
predicted class=absent expected loss=0.2099 P(node) =1
class counts: 64 17
probabilities: 0.790 0.210
left son=2 (62 obs) right son=3 (19 obs)
Primary splits:
Start < 8.5to the right, improve=6.762, (0 missing)
Number <5.5 to the left, improve=2.867, (0 missing)
Age < 39.5 to the left, improve=2.250, (0 missing)
Node number 2: 62 observations, complexity param=0.01961
predicted class=absent expected loss=0.09677 P(node) =0.7654
class counts: 56 6
probabilities: 0.903 0.097
left son=4 (29 obs) right son=5 (33 obs)
Primary splits:
Start < 14.5to the right, improve=1.0210, (0 missing)
Age < 55 to the left, improve=0.6849, (0 missing)
Number <4.5 to the left, improve=0.2975, (0 missing)
Node number 3: 19 observations
predicted class=present expected loss=0.4211 P(node) =0.2346
class counts: 8 11
probabilities: 0.421 0.579
Node number 4: 29 observations
predicted class=absent expected loss=0 P(node) =0.358
class counts: 29 0
probabilities: 1.000 0.000
Node number 5: 33 observations, complexity param=0.01961
predicted class=absent expected loss=0.1818 P(node) =0.4074
class counts: 27 6
probabilities: 0.818 0.182
left son=10 (12 obs) right son=11 (21 obs)
Primary splits:
Age < 55to the left, improve=1.2470, (0 missing)
Start < 12.5 to the right, improve=0.2888, (0 missing)
Number <3.5 to the right, improve=0.1753, (0 missing)
Node number 10: 12 observations
predicted class=absent expected loss=0 P(node) =0.1481
class counts: 12 0
probabilities: 1.000 0.000
Node number 11: 21 observations, complexity param=0.01961
predicted class=absent expected loss=0.2857 P(node) =0.2593
class counts: 15 6
probabilities: 0.714 0.286
left son=22 (14 obs) right son=23 (7 obs)
Primary splits:
Age <111 to the right, improve=1.71400, (0 missing)
Start < 12.5 to the right, improve=0.79370, (0 missing)
Number <3.5 to the right, improve=0.07143, (0 missing)
Node number 22: 14 observations
predicted class=absent expected loss=0.1429 P(node) =0.1728
class counts: 12 2
probabilities: 0.857 0.143
Node number 23: 7 observations
predicted class=present expected loss=0.4286 P(node) =0.08642
class counts: 3 4
probabilities: 0.429 0.571
摘要输出中已经突出显示了四行输出,每一行都包含了关于分割、每个变量提供的最佳改进以及分割时选择的变量的信息。因此,对于 Start 变量,第一行突出显示了 6.762 的改进,第二行显示了 1.021。通过将这些值相加,我们得到 6.762 + 1.021 = 7.783,这与从 $variable.importance 提取器给出的输出相同。同样,最后两行突出显示了 Age 的贡献,即 1.274 + 1.714 = 2.961。因此,我们已经清楚地概述了变量重要性的计算。
练习:创建一个新的分类树,例如 KC2,并允许使用代理分割。使用 summary 函数验证与变量重要性相关的计算。
来自 randomForest 包的 VarImpPlot 函数为我们提供了一个变量重要性测量的点图:
>windows(height=100,width=200)
>par(mfrow=c(1,2))
>varImpPlot(GC2_RF,main="Variable Importance plot for \n Random
+ Forest of German Data")
>varImpPlot(PID_RF,main="Variable Importance plot for \n Random Forest of Pima Indian Diabetes")
下一个图给出了一个视觉展示:

图 4:德国和皮马印第安人糖尿病随机森林的变量重要性图
因此,将德国信贷分类为好或坏的五个最重要的变量是 amount、checking、age、duration 和 purpose。对于皮马印第安人糖尿病分类,三个最重要的变量是 glucose、mass 和 age。
我们接下来将探讨邻近度测量的概念。
邻近图
根据 Hastie 等人(2009)的说法,"随机森林的一个宣传输出是邻近图"(见第 595 页)。但邻近图是什么?如果我们有 n 个观测值在训练数据集中,就会创建一个阶数为
的邻近矩阵。在这里,矩阵初始化时所有值都为 0。每当一对观测值,如 OOB,在树的终端节点中同时出现时,邻近计数就会增加 1。邻近矩阵使用多维尺度方法进行可视化,这是一个超出本章范围的概念,其中邻近矩阵以二维形式表示。邻近图从随机森林的角度给出了哪些点彼此更近的指示。
在早期创建随机森林时,我们没有指定邻近矩阵的选项。因此,我们将首先使用邻近选项创建随机森林,如下所示:
> GC2_RF3 <- randomForest(GC2_Formula,data=GC2_Train,
+ ntree=500,proximity=TRUE,cob.prox=TRUE)
> GC2_RF3$proximity[1:10,1:10]
5 6 7 8 11 12 14 15 16 17
5 1.0000 0.0000 0.0000 0.0133 0.0139 0.0159 0.0508 0.0645 0.0000 0.0000
6 0.0000 1.0000 0.0435 0.0308 0.0000 0.0000 0.0000 0.0000 0.0000 0.0417
7 0.0000 0.0435 1.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.2000
8 0.0133 0.0308 0.0000 1.0000 0.0000 0.0000 0.0000 0.0000 0.0137 0.0000
11 0.0139 0.0000 0.0000 0.0000 1.0000 0.0395 0.0000 0.2034 0.0147 0.0000
12 0.0159 0.0000 0.0000 0.0000 0.0395 1.0000 0.0000 0.0323 0.0000 0.0000
14 0.0508 0.0000 0.0000 0.0000 0.0000 0.0000 1.0000 0.0167 0.0435 0.0182
15 0.0645 0.0000 0.0000 0.0000 0.2034 0.0323 0.0167 1.0000 0.0345 0.0000
16 0.0000 0.0000 0.0000 0.0137 0.0147 0.0000 0.0435 0.0345 1.0000 0.0159
17 0.0000 0.0417 0.2000 0.0000 0.0000 0.0000 0.0182 0.0000 0.0159 1.0000
>MDSplot(GC2_RF3,fac = GC2_Train$good_bad,
+ main="MDS Plot for Proximity Matrix of a RF")
选项 proximity=TRUE,cob.prox=TRUE 对于获得 proximity 矩阵非常重要。然后我们简单地使用 MDSplot 图形函数:

图 5:RF 邻近矩阵的多维图
从邻近数据的角度来看,使用which.max函数更容易找到与给定观察值最近的观察值,而不是欧几里得距离:
>which.max(GC2_RF3$proximity[1,-1])
962
657
>which.max(GC2_RF3$proximity[2,-2])
686
458
因此,训练数据集中编号为657的观察值(以及在整体数据集中的962)与第一个观察值最接近。请注意,整体位置是由于从样本函数中提取的名称造成的。which.max函数对于在数组中找到最大位置是有用的。
结果表明,大多数情况下,使用MDSplot函数的图形显示结果是一个类似星形的显示。邻近矩阵也有助于进行聚类分析,这将在本章的结尾部分看到。接下来,我们将更详细地介绍随机森林的参数。
随机森林的细微差别
GC_Random_Forest.pdf文件包含了 500 棵树,这些树作为随机森林集成中的同质学习器。众所周知,决策树有一个很好且清晰的解释。这是因为它展示了如何遍历路径到一个终端节点。在每个分割点随机选择特征和自举样本导致随机森林的建立。请参考图随机森林的树,它描绘了编号为78、176、395和471的树。这四棵树的第一分割点分别是目的、金额、属性和持续时间。这四棵树第一左边的第二分割点分别是雇员、居民、目的和金额。看到哪些变量比其他变量更有意义是一项繁琐的练习。我们知道,变量出现得越早,其重要性就越高。随之而来的问题是,对于随机森林而言,我们如何找到变量的深度分布?这一点以及许多其他问题都通过一个强大的随机森林包randomForestExplainer得到了解决,而且毫不夸张地说,没有这个了不起的包,本节将无法实现。
通过对随机森林对象应用min_depth_distribution函数,我们得到变量的深度分布。然后使用plot_min_depth_distribution,我们得到最小深度分布的图:
> GC2_RF_MDD <- min_depth_distribution(GC2_RF)
>head(GC2_RF_MDD)
tree variable minimal_depth
1 1 age 4
2 1 amount 3
3 1 checking 0
4 1 coapp 2
5 1 depends 8
6 1 duration 2
>windows(height=100,width=100)
> plot_min_depth_distribution(GC2_RF_MDD,k=nrow(GC2_TestX))
前一个代码块的结果是德国随机森林的最小深度分布,具体如下:

图 6:德国随机森林的最小深度分布
从前面的图中可以看出,checking变量作为主要分割点出现的频率更高,其次是savings、目的、金额和持续时间。因此,通过最小深度分布图,我们得到了一个有用的描述。可以通过使用measure_importance函数进行进一步的分析,它为我们提供了随机森林变量各种重要性度量:
> GC2_RF_VIM <- measure_importance(GC2_RF)
[1] "Warning: your forest does not contain information on local importance so 'accuracy_decrease' measure cannot be extracted. To add it regrow the forest with the option localImp = TRUE and run this function again."
我们在这里被警告,随机森林没有使用localImp = TRUE选项进行生长,这是获得度量的关键。因此,我们创建一个新的随机森林,然后在其上运行measure_importance函数:
> GC2_RF4<- randomForest(GC2_Formula,data=GC2_Train,
+ ntree=500,localImp=TRUE)
> GC2_RF4_VIM <- measure_importance(GC2_RF4)
输出格式更宽,因此我们以图像格式提供它,并在变量重要性度量分析中垂直显示结果。我们可以看到,measure_importance函数提供了关于平均最小深度、变量作为节点出现在 500 棵树中的节点数量、平均准确度下降、基尼减少等方面的许多信息。
从输出中我们可以看到,如果平均最小深度较高,相关的 p 值也较高,因此变量不显著。例如,变量coapp、depends、existcr、foreign和telephon的平均最小深度较高,在大多数情况下它们的 p 值也是 1。同样,gini_decrease的较低值也与较高的 p 值相关联,这表明变量的不显著性:

图 7:变量重要性度量分析
可以使用重要性度量对象GC2_RF_VIM进行进一步分析。对于no_of_nodes度量,我们可以比较之前变量重要性度量中的各种指标。例如,我们想看看变量的times_a_root值与平均最小深度的关系。同样,我们还想分析其他度量。通过在此对象上应用plot_multi_way_importance图形函数,我们得到以下输出:
> P1 <- plot_multi_way_importance(GC2_RF4_VIM, size_measure = "no_of_nodes",
+ x_measure="mean_min_depth",
+ y_measure = "times_a_root")
> P2 <- plot_multi_way_importance(GC2_RF4_VIM, size_measure = "no_of_nodes",
+ x_measure="mean_min_depth",
+ y_measure = "gini_decrease")
> P3 <- plot_multi_way_importance(GC2_RF4_VIM, size_measure = "no_of_nodes",
+ x_measure="mean_min_depth",
+ y_measure = "no_of_trees")
> P4 <- plot_multi_way_importance(GC2_RF4_VIM, size_measure = "no_of_nodes",
+ x_measure="mean_min_depth",
+ y_measure = "p_value")
> grid.arrange(P1,P2,P3,P4, ncol=2)

图 8:德国信贷数据的多元重要性图
在这里,变量的times_a_root值与平均最小深度mean_min_depth相对应,同时保持节点数量与其大小一致。非顶级变量为黑色,而顶级变量为蓝色。同样,我们在前一个图中将gini_decrease、no_of_trees和p_value与mean_min_depth相对应。
使用plot_importance_ggpairs函数绘制了五个度量之间的相关性,如下所示:
> plot_importance_ggpairs(GC2_RF4_VIM)

图 9:重要性度量之间的关系
由于这些度量高度相关,无论是正相关还是负相关,我们需要所有五个这些度量来理解随机森林。
树结构的优点之一是解释变量之间的交互作用。例如,如果父节点中的分割是由一个变量进行的,而在子节点中是由另一个变量进行的,我们可以得出这两个变量之间存在交互的结论。再次,对于随机森林来说,问题又出现了。使用important_variables和min_depth_interactions,我们可以获得随机森林中变量之间的交互如下:
> GC2_RF4_VIN <- important_variables(GC2_RF4, k = 5,
+ measures = c("mean_min_depth", "no_of_trees"))
> GC2_RF4_VIN_Frame <- min_depth_interactions(GC2_RF4,GC2_RF4_VIN)
>head(GC2_RF4_VIN_Frame[order(GC2_RF4_VIN_Frame$occurrences, decreasing = TRUE), ])
variable root_variable mean_min_depth occurrences interaction
7 amount checking 1.6 442 checking:amount
2 age checking 2.0 433 checking:age
27 duration checking 2.1 426 checking:duration
77 purpose checking 2.0 420 checking:purpose
32 employed checking 2.6 417 checking:employed
8 amount duration 2.4 408 duration:amount
uncond_mean_min_depth
7 2.4
2 2.5
27 2.3
77 2.3
32 3.0
8 2.4
> plot_min_depth_interactions(GC2_RF2_VIN_Frame)
以下是将获得的结果:

图 10:德国随机森林的最小深度交互
因此,我们可以轻松地找到随机森林的交互变量。
randomForestExplainer R 包非常强大,帮助我们获得随机森林后执行许多诊断。没有后诊断,我们无法评估任何拟合模型。因此,建议读者在本节的实现中执行大多数学习到的步骤。
在下一节中,我们将比较随机森林与 Bagging 过程。
练习:对为皮马印第安人糖尿病问题构建的随机森林进行诊断。
与 Bagging 的比较
当将随机森林的结果与德国信用数据集和皮马印第安人糖尿病数据集的 Bagging 对应方进行比较时,我们没有在数据的验证分区上看到准确率的明显提高。可能的原因是 Bagging 实现的方差减少已经达到最佳,任何偏差的改进都不会导致准确率的提高。
我们认为数据集可以从 R 核心包kernlab中获取。该数据集是垃圾邮件,包含 4601 封带有标签的电子邮件,标签表明电子邮件是垃圾邮件还是非垃圾邮件。数据集包含从电子邮件内容中派生的 57 个变量的良好集合。任务是构建一个用于垃圾邮件识别问题的良好分类器。该数据集迅速划分为训练集和验证集,就像早期问题一样:
> data("spam")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(spam),replace = TRUE,
+ prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> spam_Train <- spam[Train_Test=="Train",]
> spam_TestX <- within(spam[Train_Test=="Test",],rm(type))
> spam_TestY <- spam[Train_Test=="Test","type"]
> spam_Formula <- as.formula("type~.")
首先,我们将构建简单的分类树:
> spam_ct <- rpart(spam_Formula,data=spam_Train)
> spam_ct_predict <- predict(spam_ct,newdata=spam_TestX,
+ type="class")
> ct_accuracy <- sum(spam_ct_predict==spam_TestY)/nrow(spam_TestX)
> ct_accuracy
[1] 0.8994
分类树给出了大约 90%的适度准确率。然后我们将应用randomForest并构建随机森林:
> spam_rf <- randomForest(spam_Formula,data=spam_Train,coob=TRUE,
+ ntree=500,keepX=TRUE,mtry=5)
> spam_rf_predict <- predict(spam_rf,newdata=spam_TestX,
+ type="class")
> rf_accuracy <- sum(spam_rf_predict==spam_TestY)/nrow(spam_TestX)
> rf_accuracy
[1] 0.9436
可以使用randomForest函数进行 Bagging。技巧是要求随机森林在设置分割时使用所有变量。因此,选择mtry=ncol(spal_TestX)将选择所有变量,然后 Bagging 就很容易执行:
> spam_bag <- randomForest(spam_Formula,data=spam_Train,coob=TRUE,
+ ntree=500,keepX=TRUE,mtry=ncol(spam_TestX))
> spam_bag_predict <- predict(spam_bag,newdata=spam_TestX,
+ type="class")
> bag_accuracy <- sum(spam_bag_predict==spam_TestY)/
+ nrow(spam_TestX)
> bag_accuracy
[1] 0.935
> windows(height=100,width=200)
> par(mfrow=c(1,2))
> plot(spam_rf,main="Random Forest for Spam Classification")
> plot(spam_bag,main="Bagging for Spam Classification")
准确率的提高也反映在准确率图中,如下所示:

图 11:垃圾邮件分类问题的随机森林和 Bagging 比较
在最后两节中,我们将探讨随机森林的一些利基应用。
缺失数据插补
缺失数据是一个威胁!它突然出现并阻止分析,直到得到妥善处理。期望最大化算法的统计技术,或简称为 EM 算法,需要对概率分布、结构关系和统计模型的深入细节有大量信息。然而,在这里完全排除了使用 EM 算法的方法。随机森林可以用来克服缺失数据问题。
当我们在本书的其余部分遇到缺失数据问题时,我们将使用missForest R 包来修复。missForest函数的算法和其他细节可以在academic.oup.com/bioinformatics/article/28/1/112/219101找到。对于任何具有缺失数据的变量/列,技术是为此变量构建一个随机森林,并将 OOB 预测作为插补误差估计。请注意,该函数可以处理连续和分类的缺失值。该包的创建者已启用并行运行功能以节省时间。
我们将从openmv.net/info/travel-times获取一个简单的数据集,并且数据中存在缺失值。数据由13个变量和205个观测值组成。在可用的13个变量中,只有FuelEconomy变量有缺失值。让我们在 R 终端中探索这个数据集:
> TT <- read.csv("../Data/Travel_Times.csv")
>dim(TT)
[1] 205 13
>sum(is.na(TT))
[1] 19
>sapply(TT,function(x) sum(is.na(x)))
Date StartTime DayOfWeek GoingTo Distance
0 0 0 0 0
MaxSpeed AvgSpeed AvgMovingSpeed FuelEconomy TotalTime
0 0 0 19 0
MovingTime Take407All Comments
0 0 0
> TT$FuelEconomy
[1] NA NA NA NA NA NA NA NA 8.89 8.89 8.89 8.89
[13] 8.89 8.89 8.89 8.89 9.08 9.08 9.08 9.08 9.08 9.08 9.08 9.08
[25] 9.76 9.76 9.76 9.76 9.76 9.76 9.76 9.16 9.16 9.16 NA NA
[37] NA NA NA NA NA NA 9.30 9.30 9.30 9.30 9.30 9.30
[49] 10.05 10.05 10.05 10.05 9.53 9.53 9.53 9.53 9.53 9.53 9.53 9.53
[61] 9.35 9.35 9.35 9.35 9.35 9.35 9.35 9.35 8.32 8.32 8.32 8.32
[181] 8.48 8.48 8.48 8.45 8.45 8.45 8.45 8.45 8.45 8.45 8.45 8.45
[193] 8.45 8.28 8.28 8.28 7.89 7.89 7.89 7.89 7.89 7.89 NA NA
[205] NA
可以看到有19个观测值存在缺失值。sapply函数告诉我们,所有19个观测值只有FuelEconomy变量存在缺失值。现在正在部署missForest函数:
> TT_Missing <- missForest(TT[,-c(1,2,12)],
+ maxiter = 10,ntree=500,mtry=6)
missForest iteration 1 in progress...done!
missForest iteration 2 in progress...done!
missForest iteration 3 in progress...done!
missForest iteration 4 in progress...done!
> TT_FuelEconomy <- cbind(TT_Missing$ximp[,7],TT$FuelEconomy)
> TT_FuelEconomy[is.na(TT$FuelEconomy),]
[,1] [,2]
[1,] 8.59 NA
[2,] 8.91 NA
[3,] 8.82 NA
[4,] 8.63 NA
[5,] 8.44 NA
[6,] 8.63 NA
[7,] 8.60 NA
[8,] 8.50 NA
[9,] 9.07 NA
[10,] 9.10 NA
[11,] 8.52 NA
[12,] 9.12 NA
[13,] 8.53 NA
[14,] 8.85 NA
[15,] 8.70 NA
[16,] 9.42 NA
[17,] 8.40 NA
[18,] 8.49 NA
[19,] 8.64 NA
我们现在已经插补了缺失值。需要注意的是,插补值应该有意义,并且不应该显得不合适。在第九章中,我们将使用missForest函数来插补大量缺失值。
练习:如何验证插补值?使用missForest包中的prodNA函数,并用缺失数据戳记良好值。使用missForest函数获取插补值,并与原始值进行比较。
邻近矩阵告诉我们从随机森林的角度来看观测值有多接近。如果我们有关于观测值邻域的信息,我们可以进行聚类分析。使用邻近矩阵的副产品,我们现在也可以使用随机森林来解决无监督问题。
随机森林聚类
随机森林可以设置不包含目标变量。使用此功能,我们将计算邻近矩阵并使用 OOB 邻近值。由于邻近矩阵为我们提供了观测之间的接近度度量,因此可以使用层次聚类方法将其转换为聚类。
我们从在randomForest函数中设置y = NULL开始。指定proximity=TRUE和oob.prox=TRUE选项以确保我们获得所需的邻近矩阵:
>data(multishapes)
>par(mfrow=c(1,2))
>plot(multishapes[1:2],col=multishapes[,3],
+ main="Six Multishapes Data Display")
> MS_RF <- randomForest(x=multishapes[1:2],y=NULL,ntree=1000,
+ proximity=TRUE, oob.prox=TRUE,mtry = 1)
接下来,我们使用hclust函数和ward.D2选项对距离矩阵进行层次聚类分析。cutree函数将hclust对象划分为k = 6个聚类。最后,table函数和可视化给出了使用随机森林进行聚类效果的好坏概念:
> MS_hclust <- hclust(as.dist(1-MS_RF$proximity),method="ward.D2")
> MS_RF_clust <- cutree(MS_hclust,k=6)
>table(MS_RF_clust,multishapes$shape)
MS_RF_clust 1 2 3 4 5 6
1 113 0 0 0 10 0
2 143 0 0 0 20 50
3 57 170 0 0 3 0
4 63 55 0 0 3 0
5 24 175 0 0 2 0
6 0 0 100 100 12 0
>plot(multishapes[1:2],col=MS_RF_clust,
+ main="Clustering with Random Forest
以下是一个说明使用随机森林进行聚类的图表:

图 12:使用随机森林进行聚类
尽管随机森林提供的聚类不适合标签识别问题,但我们仍将它们作为起点。需要理解的是,随机森林可以正确地用于聚类分析。
摘要
随机森林是为了改进袋装方法而创建的。作为一个同质集成方法的例子,我们看到了森林如何帮助提高准确性。随机森林的可视化和变量重要性被详细阐述。我们还看到了在拟合随机森林后可以使用的许多诊断方法。然后,该方法与袋装方法进行了比较。还展示了随机森林在缺失数据插补和聚类分析中的新颖应用。
在下一章中,我们将探讨提升,这是一种非常重要的集成方法。
第五章. 简洁的增强算法
我们所说的简洁的增强算法是什么意思?增强算法(及其变体)可以说是机器学习工具箱中最重要的算法之一。任何数据分析师都需要了解这个算法,并且最终追求更高的准确率不可避免地会推动对增强技术的需求。据报道,在www.kaggle.org论坛上,复杂和大量数据的增强算法运行了数周,而且大多数获奖方案都是基于这个算法。此外,这些算法在现代图形设备机器上运行。
考虑到其重要性,我们将在下面详细研究增强算法。简洁的当然不是增强算法的变体。由于增强算法是非常重要且关键的算法之一,我们将首先陈述该算法并以最基本的方式实现它,这将展示算法的每个步骤是如何运作的。
我们将从自适应增强算法开始——通常被称为AdaBoost算法——并使用非常简单和原始的代码,我们将展示其在分类问题中的应用。该说明是在一个toy数据集上进行的,以便读者可以清楚地跟随步骤。
在下一节中,我们将扩展分类增强算法到回归问题。对于这个问题,增强变体是著名的梯度增强算法。通过一系列具有单个分割的基本决策树,即所谓的树桩,将创建一个有趣的非线性回归问题。我们将通过平方误差损失函数的选择来展示梯度增强算法。对于增强方法,我们将阐明变量重要性计算。本章的倒数第二节将讨论gbm包的细节。结论部分将对垃圾邮件数据集的袋装、随机森林和增强方法进行比较。本章包括以下主题:
-
通用增强算法
-
自适应提升
-
梯度提升
-
基于树桩的梯度提升
-
基于平方误差损失的梯度提升
-
-
增强技术中的变量重要性
-
使用 gbm 包
-
袋装、随机森林和增强算法的比较
技术要求
在本章中,我们将使用以下库:
-
rpart -
gbm
通用增强算法
在前几章中介绍的基于树的集成方法,Bagging 和 随机森林,是决策树的重要扩展。然而,虽然 Bagging 通过平均多个决策树提供了更大的稳定性,但偏差仍然存在。这种局限性促使 Breiman 在每个分割点采样协变量以生成一组“独立”的树,并为随机森林奠定了基础。随机森林中的树可以像 Bagging 一样并行开发。在多个树上进行平均的想法是为了确保偏差和方差权衡之间的平衡。提升是决策树的第三大扩展,也可能是最有效的一种。它同样基于集成同质基学习器(在这种情况下,是树),就像 Bagging 和随机森林一样。提升算法的设计完全不同。它是一种顺序集成方法,因为在算法的下一个运行中,前一个学习者的残差/误分类点被改进。
通用提升技术包括一系列算法,这些算法将弱学习器转换为最终强学习器。在分类问题的背景下,弱学习器是一种比随机猜测更好的技术。该方法将弱学习算法转换为更好的算法,这就是它被称为 提升 的原因。提升技术旨在提供一种接近完美分类器的强学习器。
| 分类器 | 子空间 ![]() |
子空间 ![]() |
子空间 ![]() |
子空间 ![]() |
准确率 |
|---|---|---|---|---|---|
![]() |
R | R | R | Q | 0.75 |
![]() |
R | R | Q | R | 0.75 |
![]() |
R | Q | R | R | 0.75 |
![]() |
Q | R | R | R | 0.75 |
表 1 简单分类器场景
通过一个简单的例子可以更广泛地理解提升的动机。假设从样本空间
中抽取大小为 n 的随机样本,作为独立同分布(IID)的样本。随机样本的分布假设为 D。假设在
中有 T=4 个分类器,分类器用于真值函数 f。
考虑一个假设场景,其中样本空间
在
中由四个部分组成,四个分类器按前表所示执行。提升方法背后的思想是以顺序方式改进分类器。也就是说,分类器是一个接一个地组合,而不是同时全部组合。现在,
的错误将被纠正到一个新的分布 D',其中分类器在区域
的错误将给予更多权重,导致分布 D''。提升方法将继续对剩余的分类器进行过程,并给出一个总的组合/集成。Zhou(2012)的伪提升算法(见第二章)总结如下:
-
步骤 0:初始样本分布是 D,并设置 D1 = D
-
步骤
:-
Dt -
![通用提升算法]()
-
Dt+1 = 改进分布 (Dt,
)
-
-
最后一步:
![通用提升算法]()
算法中的两个步骤改进分布和组合输出显然需要可执行的操作。在下一节中,我们将通过清晰的数值说明来开发自适应提升方法。
自适应提升
Schapire 和 Freund 发明了自适应提升方法。Adaboost是这个技术的流行缩写。
通用自适应提升算法如下:
-
均匀初始化观测权重:
![自适应提升]()
-
对于 m,分类器 hm 从 1 到 m 对数据进行多次遍历,执行以下任务:
-
使用权重
对训练数据拟合分类器 hm。 -
计算每个分类器的误差如下:
![自适应提升]()
-
计算分类器 hm 的投票权重:
![自适应提升]()
-
设置
![自适应提升]()
-
-
输出:
![自适应提升]()
简而言之,算法展开如下:
-
初始时,我们对所有观测值使用均匀权重
。 -
在下一步中,我们计算考虑的每个分类器的加权误差
。 -
分类器(通常是树桩,或单分支的决策树)需要被选择,通常的做法是选择具有最大准确率的分类器。
-
在改进分布和组合输出的情况下,如果有准确率相同的分类器,则选择任意一个。
-
接下来,错误分类的观察值被赋予更多的权重,而正确分类的值被降低权重。这里需要记录一个重要点:
注意
在权重更新步骤中,正确分类为观察值的权重之和将等于错误分类的观察值的权重之和。
从计算分类器的误差到权重更新步骤的步骤会重复 M 次,从而获得每个分类器的投票权重。对于任何给定的观察值,我们然后使用 M 个分类器的预测,这些预测根据各自的投票权重加权,并使用算法中指定的符号函数进行预测。
尽管算法可能很简单,但通过玩具数据集来执行自适应提升方法的工作是一个有用的练习。数据和计算方法取自 Jessica Noss 的视频,可在www.youtube.com/watch?v=gmok1h8wG-Q找到。自适应提升算法的说明现在开始。
考虑一个包含五个三元点的玩具数据集:两个解释变量和一个二元输出值。变量和数据可以用
来总结,这里的数据点有
、
、
、
和
。数据首先输入到 R 中,然后作为初步步骤进行可视化:
> # ADAPTIVE BOOSTING with a Toy Dataset
> # https://www.youtube.com/watch?v=gmok1h8wG-Q
> # Jessica Noss
> # The Toy Data
> x1 <- c(1,5,3,1,5)
> x2 <- c(5,5,3,1,1)
> y <- c(1,1,-1,1,1)
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="The TOY Data Depiction")
> text(x1,x2,labels=names(y),pos=1)

图 1:玩具数据集的简单表示
树桩是决策树的一种特殊情况,已在讨论中提到。在这里,我们将使用树桩作为基学习器。简单看一下前面的图可以帮助我们轻松找到准确率高于随机猜测的树桩。
例如,我们可以在
处放置一个树桩,并将左侧的所有观察值标记为正,右侧的观察值标记为负。在以下程序中,绿色阴影区域的点被树桩预测为正,而红色阴影区域的点被预测为负。同样,我们可以在
和
处使用额外的树桩。通过symmetry()函数,我们还可以交换相同树桩的预测。因此,我们之前将绿色阴影区域放在
的左侧,并预测值为正,通过反转顺序,树桩
右侧的区域将被标记为正。对负值也进行类似的分类。在
和
处重复此任务。使用par、plot、text和rect图形函数,我们在以下内容中展示这些基学习器的可视化表示:
> # Visualizing the stump models
> windows(height=200,width=300)
> par(mfrow=c(2,3))
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1<2")
> text(x1,x2,labels=names(y),pos=1)
> plim <- par("usr")
> rect(xleft=2,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "red",col="red",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 2,ytop = plim[4],
+ border = "green",col="green",density=20 )
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1<4")
> text(x1,x2,labels=names(y),pos=1)
> rect(xleft=4,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "red",col="red",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 4,ytop = plim[4],
+ border = "green",col="green",density=20 )
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1<6")
> text(x1,x2,labels=names(y),pos=1)
> rect(xleft=6,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "red",col="red",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 6,ytop = plim[4],
+ border = "green",col="green",density=20 )
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1>2")
> text(x1,x2,labels=names(y),pos=1)
> rect(xleft=2,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "green",col="green",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 2,ytop = plim[4],
+ border = "red",col="red",density=20 )
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1>4")
> text(x1,x2,labels=names(y),pos=1)
> rect(xleft=4,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "green",col="green",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 4,ytop = plim[4],
+ border = "red",col="red",density=20 )
> plot(x1,x2,pch=c("+","+","-","+","+"),cex=2,
+ xlim=c(0,6),ylim=c(0,6),
+ xlab=expression(x[1]),ylab=expression(x[2]),
+ main="Classification with Stump X1>6")
> text(x1,x2,labels=names(y),pos=1)
> rect(xleft=6,ybottom = plim[3],xright = plim[2],ytop = plim[4],
+ border = "green",col="green",density=20 )
> rect(xleft=plim[1],ybottom = plim[3],xright = 6,ytop = plim[4],
+ border = "red",col="red",density=20 )
前面 R 程序的结果如下所示:

图 2:基于 X1 的树桩分类器
注意,在点 2、4 和 6 处,对于变量
可以获得类似的分类。尽管不需要给出基于
的树桩的完整 R 程序,我们只需在以下图表中生成输出。程序可在代码包中找到。在接下来的讨论中,将忽略基于
的树桩:

图 3:基于 X2 的树桩分类器
基于
选择的树桩导致了一些误分类,我们可以看到观察点 P1、P4 和 P3 被正确分类,而 P2 和 P5 被误分类。基于这个树桩的预测可以表示为(1,-1,-1,1,-1)。基于
的树桩正确分类了点 P1 和 P4,而 P2、P3 和 P5 被误分类,这里的向量预测为(1,-1,1,1,-1)。这里考虑的六个模型在 R 程序中用 M1、M2、…、M6 表示,根据前面指定的算法,我们有
。同样,我们还有其他四个树桩的预测,并将它们输入到 R 中,如下所示:
> # The Simple Stump Models
> M1 <- c(1,-1,-1,1,-1) # M1 = X1<2 predicts 1, else -1
> M2 <- c(1,-1,1,1,-1) # M2 = X1<4 predicts 1, else -1
> M3 <- c(1,1,1,1,1) # M3 = X1<6 predicts 1, else -1
> M4 <- c(-1,1,1,-1,1) # M4 = X1>2 predicts 1, else -1;M4=-1*M1
> M5 <- c(-1,1,-1,-1,1) # M5 = X1>4 predicts 1, else -1;M5=-1*M2
> M6 <- c(-1,-1,-1,-1,-1) # M6 = X1>6 predicts 1, else -1;M6=-1*M3
使用六个模型M1-M6给出的预测,我们可以将它们与y中的真实标签进行比较,以查看在每个模型中哪些观察值被误分类:
> # Stem Model Errors
> Err_M1 <- M1!=y
> Err_M2 <- M2!=y
> Err_M3 <- M3!=y
> Err_M4 <- M4!=y
> Err_M5 <- M5!=y
> Err_M6 <- M6!=y
> # Their Misclassifications
> rbind(Err_M1,Err_M2,Err_M3,Err_M4,Err_M5,Err_M6)
P1 P2 P3 P4 P5
Err_M1 FALSE TRUE FALSE FALSE TRUE
Err_M2 FALSE TRUE TRUE FALSE TRUE
Err_M3 FALSE FALSE TRUE FALSE FALSE
Err_M4 TRUE FALSE TRUE TRUE FALSE
Err_M5 TRUE FALSE FALSE TRUE FALSE
Err_M6 TRUE TRUE FALSE TRUE TRUE
因此,TRUE的值表示在名为模型的行中,名为点的列被误分类。初始化权重
,并在以下 R 代码块中计算每个模型的加权误差
:
> # ROUND 1
> # Weighted Error Computation
> weights_R1 <- rep(1/length(y),length(y)) #Initializaing the weights
> Err_R1 <- rbind(Err_M1,Err_M2,Err_M3,Err_M4,Err_M5,Err_M6)%*%
+ weights_R1
> Err_R1 # Error rate
[,1]
Err_M1 0.4
Err_M2 0.6
Err_M3 0.2/
Err_M4 0.6
Err_M5 0.4
Err_M6 0.8
由于对应模型 3,或
的误差是最小的,我们首先选择它,并按照以下方式计算分配给它的投票权重
:
> # The best classifier error rate
> err_rate_r1 <- min(Err_R1)
> alpha_3 <- 0.5*log((1-err_rate_r1)/err_rate_r1)
> alpha_3
[1] 0.6931472
因此,提升算法步骤表明
给出了所需的预测:
> alpha_3*M3
[1] 0.6931472 0.6931472 0.6931472 0.6931472 0.6931472
> sign(alpha_3*M3)
[1] 1 1 1 1 1
中心观察点 P3 仍然被错误分类,所以我们继续到下一步。
现在我们需要更新权重
,对于分类问题,简化形式的规则如下:

因此,我们需要一个函数,它将接受前一次运行的权重、错误率和模型错误分类作为输入,然后返回包含先前公式的更新权重。我们定义这样的函数如下:
> # Weights Update Formula and Function
> Weights_update <- function(weights,error,error_rate){
+ weights_new <- NULL
+ for(i in 1:length(weights)){
+ if(error[i]==FALSE) weights_new[i] <- 0.5*weights[i]/(1-error_rate)
+ if(error[i]==TRUE) weights_new[i] <- 0.5*weights[i]/error_rate
+ }
+ return(weights_new)
+ }
现在,我们将更新权重并计算六个模型中的每个模型的误差:
> # ROUND 2
> # Update the weights and redo the analyses
> weights_R2 <- Weights_update(weights=weights_R1,error=Err_M3,
+ error_rate=err_rate_r1)
> Err_R2 <- rbind(Err_M1,Err_M2,Err_M3,Err_M4,Err_M5,Err_M6)%*%
+ weights_R2
> Err_R2 # Error rates
[,1]
Err_M1 0.25
Err_M2 0.75
Err_M3 0.50
Err_M4 0.75
Err_M5 0.25
Err_M6 0.50
在这里,模型 M1 和 M5 使用新的权重具有相等的错误率,我们简单地选择模型 1,计算其投票权重,并基于更新后的模型进行预测:
> err_rate_r2 <- min(Err_R2)
> alpha_1 <- 0.5*log((1-err_rate_r2)/err_rate_r2)
> alpha_1
[1] 0.5493061
> alpha_3*M3+alpha_1*M1
[1] 1.242453 0.143841 0.143841 1.242453 0.143841
> sign(alpha_3*M3+alpha_1*M1)
[1] 1 1 1 1 1
由于点 P3 仍然被错误分类,我们继续迭代并再次应用循环:
> # ROUND 3
> # Update the weights and redo the analyses
> weights_R3 <- Weights_update(weights=weights_R2,error=Err_M1,
+ error_rate=err_rate_r2)
> Err_R3 <- rbind(Err_M1,Err_M2,Err_M3,Err_M4,Err_M5,Err_M6)%*%
+ weights_R3
> Err_R3 # Error rates
[,1]
Err_M1 0.5000000
Err_M2 0.8333333
Err_M3 0.3333333
Err_M4 0.5000000
Err_M5 0.1666667
Err_M6 0.6666667
> err_rate_r3 <- min(Err_R3)
> alpha_5 <- 0.5*log((1-err_rate_r3)/err_rate_r3)
> alpha_5
[1] 0.804719
> alpha_3*M3+alpha_1*M1+alpha_5*M5
[1] 0.4377344 0.9485600 -0.6608779 0.4377344 0.9485600
> sign(alpha_3*M3+alpha_1*M1+alpha_5*M5)
[1] 1 1 -1 1 1
现在分类是完美的,经过三次迭代后,我们没有任何错误分类或错误。本节编程的目的是以基本的方式展示自适应提升算法的步骤。在下一节中,我们将探讨 梯度提升 技术。
梯度提升
自适应提升方法不能应用于回归问题,因为它旨在解决分类问题。梯度提升方法可以使用适当的损失函数来解决分类和回归问题。实际上,梯度提升方法的应用不仅限于这两个标准问题。这项技术起源于 Breiman 的一些观察,并由 Friedman(2000)发展成回归问题。在下一节中,我们将对基本代码进行解释,而无需展示算法。在设置清楚之后,我们将在下一个小节中正式陈述针对平方误差损失函数的提升算法,并创建一个新的函数来实现该算法。
下面的图示是标准正弦波函数的描述。很明显,这是一个非线性关系。在不显式使用正弦变换的情况下,我们将看到使用提升算法来学习这个函数。当然,我们需要简单的回归树桩,我们从一个简单的函数 getNode 开始,它将给出我们想要的分割:

图 4:提升算法能否用于非线性正弦数据?
从头开始构建
在上一节中,我们使用了简单的分类树桩。在那个例子中,简单的视觉检查就足以识别树桩,我们很快获得了 12 个分类树桩。对于回归问题,我们首先定义一个getNode函数,这是对 Tattar(2017)第九章中定义的函数的轻微修改。首先设置所需的符号。
假设我们有一对 n 个数据点
,我们正在尝试学习
之间的关系,其中f的形式对我们来说完全未知。
对于回归树,分割标准相当直接。对于按 x 值分割的数据,我们计算每个分割部分的ys的平均差平方和,然后将它们加起来。分割标准被选为那个 x 值。这最大化了感兴趣变量中的平均差平方和。R 函数getNode实现了这种思考:
> getNode <- function(x,y) {
+ xu <- sort(unique(x),decreasing=TRUE)
+ ss <- numeric(length(xu)-1)
+ for(i in 1:length(ss)) {
+ partR <- y[x>xu[i]]
+ partL <- y[x<=xu[i]]
+ partRSS <- sum((partR-mean(partR))²)
+ partLSS <- sum((partL-mean(partL))²)
+ ss[i] <- partRSS + partLSS
+ }
+ xnode <- xu[which.min(ss)]
+ minss <- min(ss)
+ pR <- mean(y[x>xnode])
+ pL <- mean(y[x<=xnode])
+ return(list(xnode=xnode,yR=pR,yL=pL))
+ }
getNode函数的第一步是找到x的唯一值,然后按降序排序。对于唯一值,我们通过 for 循环计算平方和。循环的第一步是将数据分割成左右两部分。
对于每个特定唯一值,在每个分区中计算平均差平方和,然后将它们加起来以得到总的残差平方和。
然后,我们获取x的值,它导致最小的残差平方和。在分割区域中的预测是该区域 y 值的平均值。
getNode函数通过返回x的分割值和左右分区的预测值来结束。我们现在可以创建回归树桩。
正弦波数据首先很容易创建,我们允许 x 值在
区间内变化。y 值是简单地将正弦函数应用于 x 向量:
> # Can Boosting Learn the Sine Wave!
> x <- seq(0,2*pi,pi/20)
> y <- sin(x)
> windows(height=300,width=100)
> par(mfrow=c(3,1))
> plot(x,y,"l",col="red",main="Oh My Waves!")
前面的显示结果将是图 1。我们继续获取数据的第一次分割,并在图上显示左右分区的平均值。残差将来自正弦波,它们也将放在同一显示中,如下所示:
> first_split <- getNode(x,y)
> first_split
$xnode
[1] 3.141593
$yR
[1] -0.6353102
$yL
[1] 0.6050574
现在,我们的第一个分割点发生在x值为,
这里,3.141593。分割点右侧的预测值为-0.6353102,左侧的预测值为0.6050574。预测值使用segments函数在同一显示上绘制:
> segments(x0=min(x),y0=first_split$yL,
+ x1=first_split$xnode,y1=first_split$yL)
> segments(x0=first_split$xnode,y0=first_split$yR,
+ x1=max(x),y1=first_split$yR)
现在,预测很容易获得,简单的ifelse函数有助于计算它们。与正弦波之间的偏差是残差,我们计算第一组残差和summary函数给出了残差值的简要概述:
> yfit1 <- ifelse(x<first_split$xnode,first_split$yL,first_split$yR)
> GBFit <- yfit1
> segments(x0=x,x1=x,y0=y,y1=yfit1)
> first_residuals <- y-yfit1
> summary(first_residuals)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.60506 -0.25570 0.04752 0.03025 0.32629 0.63531
预测的第一步被保存在GBFit对象中,拟合与预测之间的差异在first_residuals向量中找到。这完成了梯度提升算法的第一轮迭代。第一轮迭代的残差将成为第二轮迭代的回归因变量/输出变量。使用getNode函数,我们执行第二轮迭代,这模仿了早期的代码集:
> second_split <- getNode(x,first_residuals)
> plot(x,first_residuals,"l",col="red",main="The Second Wave!")
> segments(x0=min(x),y0=second_split$yL,
+ x1=second_split$xnode,y1=second_split$yL)
> segments(x0=second_split$xnode,y0=second_split$yR,
+ x1=max(x),y1=second_split$yR)
> yfit2 <- ifelse(x<second_split$xnode,second_split$yL,second_split$yR)
> GBFit <- GBFit+yfit2
> segments(x0=x,x1=x,y0=first_residuals,y1=yfit2)
> second_residuals <- first_residuals-yfit2
> summary(second_residuals)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.51678 -0.24187 -0.02064 -0.01264 0.25813 0.56715
这里的一个重要区别是我们通过累加而不是平均来更新预测。请注意,我们正在模拟第一步的残差,因此下一个拟合所解释的残差剩余部分需要累加而不是平均。残差的范围是多少?建议读者将残差值与早期迭代进行比较。对第三次迭代执行类似的扩展:
> third_split <- getNode(x,second_residuals)
> plot(x,second_residuals,"l",col="red",main="The Third Wave!")
> segments(x0=min(x),y0=third_split$yL,
+ x1=third_split$xnode,y1=third_split$yL)
> segments(x0=third_split$xnode,y0=third_split$yR,
+ x1=max(x),y1=third_split$yR)
> yfit3 <- ifelse(x<third_split$xnode,third_split$yL,third_split$yR)
> GBFit <- GBFit+yfit3
> segments(x0=x,x1=x,y0=second_residuals,y1=yfit3)
> third_residuals <- second_residuals-yfit3
> summary(third_residuals)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.47062 -0.27770 -0.03927 -0.01117 0.18196 0.61331
所有的可视化显示在以下图中:

图 5:梯度提升算法的三次迭代
显然,我们不可能每次都进行详细的执行迭代,循环是非常重要的。代码被保存在一个块中,并执行了 22 次更多迭代。每次迭代的输出在图中展示,我们将它们全部放入一个外部文件,Sine_Wave_25_Iterations.pdf:
> pdf("Sine_Wave_25_Iterations.pdf")
> curr_residuals <- third_residuals
> for(j in 4:25){
+ jth_split <- getNode(x,curr_residuals)
+ plot(x,curr_residuals,"l",col="red",main=paste0(c("The ", j, "th Wave!")))
+ segments(x0=min(x),y0=jth_split$yL,
+ x1=jth_split$xnode,y1=jth_split$yL)
+ segments(x0=jth_split$xnode,y0=jth_split$yR,
+ x1=max(x),y1=jth_split$yR)
+ yfit_next <- ifelse(x<jth_split$xnode,jth_split$yL,jth_split$yR)
+ GBFit <- GBFit+yfit_next
+ segments(x0=x,x1=x,y0=curr_residuals,y1=yfit_next)
+ curr_residuals <- curr_residuals-yfit_next
+ }
> dev.off()
> summary(curr_residuals)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.733811 -0.093432 0.008481 -0.001632 0.085192 0.350122
经过 25 次迭代后,我们在GBFit中有一个整体拟合,我们可以将其与实际的 y 值进行比较,以查看梯度提升算法的表现如何:
> plot(y,GBFit,xlab="True Y",ylab="Gradient Boosting Fit")

图 6:梯度拟合与实际正弦数据
对于非线性模型来说,拟合是相当好的。这种方法是为了清楚地理解梯度提升算法。在下一小节中讨论和开发了提升算法的更一般形式。
平方误差损失函数
用
表示数据,并将迭代/树的数量固定为一个数字 B。选择一个收缩因子
和树深度 d。基于平方误差损失函数的梯度提升算法在此简要说明。参见 Efron 和 Hastie(2016)的第 17.2 算法,如下:
-
初始化残差
和梯度提升预测 ![平方误差损失函数]()
-
对于
:-
为数据
拟合深度为 d 的回归树 -
获得预测值
![平方误差损失函数]()
-
通过
更新提升预测 -
更新残差
![平方误差损失函数]()
-
-
返回函数序列
![平方误差损失函数]()
现在,我们将定义一个名为GB_SqEL的函数,该函数将实现由平方误差损失函数驱动的梯度提升算法。该函数必须提供五个参数:y和x将构成数据,depth将指定树的深度(即回归树中的分割数),iter表示迭代次数,shrinkage是
因子。GB_SqEL函数的设置如下:
> # Gradiend Boosting Using the Squared-error Loss Function
> GB_SqEL <- function(y,X,depth,iter,shrinkage){
+ curr_res <- y
+ GB_Hat <- data.frame(matrix(0,nrow=length(y),ncol=iter))
+ fit <- y*0
+ for(i in 1:iter){
+ tdf <- cbind(curr_res,X)
+ tpart <- rpart(curr_res~.,data=tdf,maxdepth=depth)
+ gb_tilda <- predict(tpart)
+ gb_hat <- shrinkage*gb_tilda
+ fit <- fit+gb_hat
+ curr_res <- curr_res-gb_hat
+ GB_Hat[,i] <- fit
+ }
+ return(list(GB_Hat = GB_Hat))
+ }
初始化发生在参数设置中,行fit <- y*0。算法的深度参数在行maxdepth=depth中指定,使用rpart函数创建所需深度的树。predict函数在每次迭代时提供
的值,而fit+gb_hat执行必要的更新。请注意,GB_Hat[,i]包含每个迭代结束时的预测值。
我们将以 Efron 和 Hastie(2016)的例子来说明算法。考虑的数据与 Lu Gerig 的疾病,或肌萎缩侧索硬化症(ALS)有关。数据集包含有关 1,822 名患有 ALS 疾病的人的信息。目标是预测功能评分的进展率dFRS。研究有关于 369 个预测因子/协变量的信息。在这里,我们将使用GB_SqEL函数来拟合梯度提升技术,并随着迭代次数的增加分析均方误差。详细信息和数据可以从web.stanford.edu/~hastie/CASI/data.html的源文件中获得。现在,我们将平方误差损失函数驱动的提升方法付诸实践:
> als <- read.table("../Data/ALS.txt",header=TRUE)
> alst <- als[als$testset==FALSE,-1]
> temp <- GB_SqEL(y=alst$dFRS,X=alst[,-1],depth=4,
+ iter=500,shrinkage = 0.02)
> MSE_Train <- 0
> for(i in 1:500){
+ MSE_Train[i] <- mean(temp$GB_Hat[,i]-alst$dFRS)²
+ }
> windows(height=100,width=100)
> plot.ts(MSE_Train)
使用read.table函数,我们将代码包中的数据导入到als对象中。数据以.txt格式从源文件中提供。列testset表示观察值是为了训练目的还是为了测试。我们选择了训练观察值,并删除了第一个变量testset,将其存储在对象alst中。对alst对象应用了GB_SqEL函数,并指定了适当的参数。
每次迭代之后,我们计算均方误差并将其存储在GB_Hat中,如前所述。从以下图中我们可以看出,随着迭代次数的增加,均方误差逐渐减小。在这里,算法在接近 200 次迭代后稳定下来:

图 7:梯度提升和迭代均方误差
在下一节中,我们将看到两个强大 R 包的使用。
使用 adabag 和 gbm 包
将提升方法作为集成技术确实非常有效。算法从头开始展示了分类和回归问题。一旦我们清楚地理解了算法,我们就可以使用 R 包来提供未来的结果。有许多包可用于实现提升技术。然而,在本节中,我们将使用两个最受欢迎的包adabag和gbm。首先,我们需要查看这两个函数的选项。名称很明显,adabag实现了自适应提升方法,而gbm处理梯度提升方法。首先,我们查看以下代码中这两个函数可用的选项:

提升和 gbm 函数
公式是常规的参数。在adabag中的mfinal参数和gbm中的n.trees参数允许指定树的数量或迭代次数。提升函数提供了boos选项,这是使用每个观测值的权重在该迭代中抽取的培训集的 bootstrap 样本。梯度提升是一个更通用的算法,能够处理比回归结构更多的内容。它可以用于分类问题。gbm函数中的distribution选项提供了这些选项。同样,在这里可以看到gbm函数提供了许多其他选项。我们既不会承担解释所有这些选项的艰巨任务,也不会将它们应用于复杂的数据集。用于解释和阐述自适应和梯度提升算法的两个数据集将使用boosting和gbm函数继续进行。
需要更改玩具数据集,我们将多次复制它们,以便我们有足够的观测值来运行boosting和gbm函数:
> # The adabag and gbm Packages
> x1 <- c(1,5,3,1,5)
> x1 <- rep(x1,times=10)
> x2 <- c(5,5,3,1,1)
> x2 <- rep(x2,times=10)
> y <- c(1,1,0,1,1)
> y <- rep(y,times=10)
> toy <- data.frame(x1=x1,x2=x2,y=y)
> toy$y <- as.factor(toy$y)
> AB1 <- boosting(y~.,data=toy,boos=TRUE,mfinal = 10,
+ maxdepth=1,minsplit=1,minbucket=1)
> predict.boosting(AB1,newdata=toy[,1:2])$class
[1] "1" "1" "0" "1" "1" "1" "1" "0" "1" "1" "1" "1" "0" "1" "1" "1" "1" "0"
[19] "1" "1" "1" "1" "0" "1" "1" "1" "1" "0" "1" "1" "1" "1" "0" "1" "1" "1"
[37] "1" "0" "1" "1" "1" "1" "0" "1" "1" "1" "1" "0" "1" "1"
maxdepth=1函数确保我们只使用树桩作为基础分类器。很容易看出提升函数工作得非常完美,因为所有观测都被正确分类。
与boosting函数一样,我们需要更多的数据点。我们通过seq函数增加这些数据点,并使用distribution="gaussian"选项,要求gbm函数拟合回归提升技术:
> x <- seq(0,2*pi,pi/200)
> y <- sin(x)
> sindata <- data.frame(cbind(x,y))
> sin_gbm <- gbm(y~x,distribution="gaussian",data=sindata,
+ n.trees=250,bag.fraction = 0.8,shrinkage = 0.1)
> par(mfrow=c(1,2))
> plot.ts(sin_gbm$fit, main="The gbm Sine Predictions")
> plot(y,sin_gbm$fit,main="Actual vs gbm Predict")
使用绘图函数,我们比较了梯度提升方法的拟合情况。以下图表表明拟合是适当的。然而,图表也显示故事中有些地方不太对劲。提升方法在
和
处的函数近似留下了很多遗憾,实际与预测的图表表明在 0 处存在不连续/性能不佳的问题。然而,我们不会对这些问题深入探讨:

图 8:使用 gbm 函数进行的正弦波近似
接下来,我们将讨论变量重要性的概念。
变量重要性
提升方法本质上使用树作为基学习器,因此变量重要性的概念在这里与树、袋装和随机森林相同。我们只需像在袋装或随机森林中那样,将变量在树之间的重要性相加。
对于来自adabag包的提升拟合对象,变量重要性提取如下:
> AB1$importance
x1 x2
100 0
这意味着提升方法根本未使用x2变量。对于梯度提升对象,变量重要性由summary函数给出:
> summary(sin_gbm)
var rel.inf
x x 100
现在很明显,我们只有一个变量,因此解释回归因变量很重要,我们当然不需要某些软件来告诉我们。当然,在复杂情况下是有用的。比较是基于树的集成方法的。让我们继续下一节。
比较袋装、随机森林和提升
在前一章中,我们进行了袋装和随机森林方法的比较。现在,我们使用gbm函数将提升准确率添加到早期分析中:
> data("spam")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(spam),replace = TRUE,
+ prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> spam_Train <- spam[Train_Test=="Train",]
> spam_TestX <- within(spam[Train_Test=="Test",],
+ rm(type))
> spam_TestY <- spam[Train_Test=="Test","type"]
> spam_Formula <- as.formula("type~.")
> spam_rf <- randomForest(spam_Formula,data=spam_Train,coob=TRUE,
+ ntree=500,keepX=TRUE,mtry=5)
> spam_rf_predict <- predict(spam_rf,newdata=spam_TestX,type="class")
> rf_accuracy <- sum(spam_rf_predict==spam_TestY)/nrow(spam_TestX)
> rf_accuracy
[1] 0.9436117
> spam_bag <- randomForest(spam_Formula,data=spam_Train,coob=TRUE,
+ ntree=500,keepX=TRUE,mtry=ncol(spam_TestX))
> spam_bag_predict <- predict(spam_bag,newdata=spam_TestX,type="class")
> bag_accuracy <- sum(spam_bag_predict==spam_TestY)/nrow(spam_TestX)
> bag_accuracy
[1] 0.9350464
> spam_Train2 <- spam_Train
> spam_Train2$type <- ifelse(spam_Train2$type=="spam",1,0)
> spam_gbm <- gbm(spam_Formula,distribution="bernoulli",data=spam_Train2,
+ n.trees=500,bag.fraction = 0.8,shrinkage = 0.1)
> spam_gbm_predict <- predict(spam_gbm,newdata=spam_TestX,
+ n.trees=500,type="response")
> spam_gbm_predict_class <- ifelse(spam_gbm_predict>0.5,"spam","nonspam")
> gbm_accuracy <- sum(spam_gbm_predict_class==spam_TestY)/nrow(spam_TestX)
> gbm_accuracy
[1] 0.945753
> summary(spam_gbm)
var rel.inf
charExclamation charExclamation 21.985502703
charDollar charDollar 18.665385239
remove remove 11.990552362
free free 8.191491706
hp hp 7.304531600
num415 num415 0.000000000
direct direct 0.000000000
cs cs 0.000000000
original original 0.000000000
table table 0.000000000
charHash charHash 0.000000000
提升准确率0.9457高于随机森林准确率0.9436。进一步的微调,将在下一章中探讨,将有助于提高准确率。变量重要性也可以通过summary函数轻松获得。
摘要
提升是决策树的另一种推论。它是一种迭代技术,通过更无顾忌地针对前一次迭代的误差。我们从重要的自适应提升算法开始,并使用非常简单的玩具数据来说明其基础。然后,该方法被扩展到回归问题,我们通过两种不同的方法说明了梯度提升方法。对adabag和gbm这两个包进行了简要阐述,并再次强调了变量重要性的概念。对于垃圾邮件数据集,我们通过提升方法获得了更高的准确率,因此提升算法的讨论特别有用。
本章考虑了提升算法的不同变体。然而,我们没有讨论它为什么有效。在下一章中,这些方面将更详细地介绍。
第六章:提升改进
在上一章中,我们学习了提升算法。我们研究了算法的结构形式,用一个数值示例进行了说明,然后将算法应用于回归和分类问题。在本章简要介绍中,我们将涵盖提升算法及其基础的一些理论方面。提升理论在这里也很重要。
本章,我们还将从几个不同的角度探讨提升算法为什么有效。不同类型的问题需要不同类型的损失函数,以便提升技术能够有效。在下一节中,我们将探讨我们可以选择的不同类型的损失函数。极端梯度提升方法在专门用于处理xgboost包的章节中概述。此外,h2o包将在最后一节中最终讨论,这可能对其他集成方法也很有用。本章将涵盖以下主题:
-
提升为什么有效?
-
gbm包 -
xgboost包 -
h2o包
技术要求
本章我们将使用以下 R 库:
-
adabag -
gbm -
h2o -
kernlab -
rpart -
xgboost
提升为什么有效?
上一章中关于自适应提升算法的部分包含了m个模型、分类器
、n个观察值和权重,以及一个按顺序确定的投票权。通过一个玩具示例说明了自适应提升方法的适应,然后使用专用函数应用了该方法。与袋装法和随机森林方法相比,我们发现提升提供了最高的准确率,这你可能还记得上一章前面提到的那个部分的结果。然而,算法的实现并没有告诉我们为什么它预期会表现得更好。
我们没有一个普遍接受的答案来解释提升为什么有效,但根据 Berk(2016)的第 6.2.2 小节,有三种可能的解释:
-
提升是边缘最大化器
-
提升是统计优化器
-
提升是一个插值器
但这些实际上意味着什么呢?现在我们将逐一介绍这些观点。在提升算法中,一个观察值的边缘计算如下:

我们可以看到,边界值是正确分类和错误分类总和之间的差异。在先前的公式中,
表示投票权重。提升算法之所以工作得如此之好,尤其是在分类问题上,是因为它是一个边界最大化器。在他们的开创性论文中,提升算法的发明者 Schapire 等人声称,提升特别擅长找到具有大边界的分类器,因为它专注于那些边界值小(或负)的例子,并迫使基本学习算法为这些例子生成良好的分类。下面引文中的粗体部分将在下一个 R 代码块中展示。
将使用来自kernlab包的垃圾邮件数据集来说明这一关键思想。gbm包中的boosting函数将拟合数据,以区分垃圾邮件和正常邮件。我们将从一个仅包含一次迭代的初始模型开始,获取准确率,获取边界值,然后在下面的代码块中生成摘要:
> data("spam")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(spam),replace = TRUE,prob = c(0.7,0.3))
> spam_Train <- spam[Train_Test=="Train",]
> spam_Formula <- as.formula("type~.")
> spam_b0 <- boosting(spam_Formula,data=spam_Train,mfinal=1)
> sum(predict(spam_b0,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 0.905
> mb0 <- margins(spam_b0,newdata=spam_Train)$margins
> mb0[1:20]
[1] 1 -1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
> summary(mb0)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-1.000 1.000 1.000 0.809 1.000 1.000
接下来,我们将迭代次数增加到 5、10、20、50 和 200。读者应跟踪以下结果的准确率和边界值摘要:
> spam_b1 <- boosting(spam_Formula,data=spam_Train,mfinal=5)
> sum(predict(spam_b1,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 0.948
> mb1 <- margins(spam_b1,newdata=spam_Train)$margins
> mb1[1:20]
[1] 1.0000 -0.2375 0.7479 -0.2375 0.1771 0.5702 0.6069 0.7479 1.0000
[10] 0.7479 1.0000 1.0000 -0.7479 1.0000 0.7479 1.0000 0.7479 1.0000
[19] -0.0146 1.0000
> summary(mb1)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-1.000 0.631 1.000 0.783 1.000 1.000
> spam_b2 <- boosting(spam_Formula,data=spam_Train,mfinal=10)
> sum(predict(spam_b2,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 0.969
> mb2 <- margins(spam_b2,newdata=spam_Train)$margins
> mb2[1:20]
[1] 0.852 0.304 0.245 0.304 0.288 0.629 0.478 0.678 0.827 0.678
[11] 1.000 1.000 -0.272 0.517 0.700 0.517 0.700 0.478 0.529 0.852
> summary(mb2)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.517 0.529 0.807 0.708 1.000 1.000
> spam_b3 <- boosting(spam_Formula,data=spam_Train,mfinal=20)
> sum(predict(spam_b3,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 0.996
> mb3 <- margins(spam_b3,newdata=spam_Train)$margins
> mb3[1:20]
[1] 0.5702 0.3419 0.3212 0.3419 0.3612 0.6665 0.4549 0.7926 0.7687 0.6814
[11] 0.8958 0.5916 0.0729 0.6694 0.6828 0.6694 0.6828 0.6130 0.6813 0.7467
> summary(mb3)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.178 0.537 0.719 0.676 0.869 1.000
> spam_b4<- boosting(spam_Formula,data=spam_Train,mfinal=50)
> sum(predict(spam_b4,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 1
> mb4 <- margins(spam_b4,newdata=spam_Train)$margins
> mb4[1:20]
[1] 0.407 0.333 0.386 0.333 0.379 0.518 0.486 0.536 0.579 0.647 0.695 0.544
[13] 0.261 0.586 0.426 0.586 0.426 0.488 0.572 0.677
> summary(mb4)
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.098 0.444 0.590 0.586 0.729 1.000
> spam_b5<- boosting(spam_Formula,data=spam_Train,mfinal=200)
> sum(predict(spam_b5,newdata=spam_Train)$class==
+ spam_Train$type)/nrow(spam_Train)
[1] 1
> mb5 <- margins(spam_b5,newdata=spam_Train)$margins
> mb5[1:20]
[1] 0.386 0.400 0.362 0.368 0.355 0.396 0.368 0.462 0.489 0.491 0.700 0.486
[13] 0.317 0.426 0.393 0.426 0.393 0.385 0.624 0.581
> summary(mb5)
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.272 0.387 0.482 0.510 0.607 0.916
第一个重要的区别是,边界值完全摆脱了负数,并且在迭代次数达到 50 或更多之后,每个边界值都是非负的。为了更清楚地了解这一点,我们将边界值进行列绑定,然后查看初始化时具有负边界的所有观察值:
> View(cbind(mb1,mb2,mb3,mb4,mb5)[mb1<0,])
以下快照显示了前面代码的结果:

图 1:迭代过程中误分类观察值的边界
因此,我们可以清楚地看到,随着迭代次数的增加,边界值也在增加。
关于提升,特别是自适应提升,需要记住的第二点是它是一个统计优化器。Berk(2016)第 264-5 页和 Zhou(2012)第 25-6 页显示,提升集成达到了贝叶斯误差率。这意味着由于指数损失被最小化,分类错误率也被最小化。
关于提升作为插值器的第三点非常直接。很明显,提升的迭代可以看作是随机森林的加权平均。
到目前为止,提升方法仅解决了分类和回归问题。损失函数对于机器学习算法至关重要,下一节将讨论各种损失函数,这些函数将有助于为不同格式的数据设置提升算法。
gbm包
由 Greg Ridgeway 创建的 R gbm 包是一个非常通用的包。该包的详细信息可以在www.saedsayad.com/docs/gbm2.pdf找到。该文档详细介绍了梯度提升的理论方面,并说明了 gbm 函数的各种其他参数。首先,我们将考虑 gbm 函数中可用的收缩因子。
收缩参数非常重要,也有助于解决过拟合问题。通过此选项实现惩罚。对于垃圾邮件数据集,我们将收缩选项设置为 0.1(非常大)和 0.0001(非常小),并观察性能如何受到影响:
> spam_Train2 <- spam_Train
> spam_Train2$type <- as.numeric(spam_Train2$type)-1
> spam_gbm <- gbm(spam_Formula,distribution="bernoulli",
+ data=spam_Train2, n.trees=500,bag.fraction = 0.8,
+ shrinkage = 0.1)
> plot(spam_gbm) # output suppressed
> summary(spam_gbm)
var rel.inf
charExclamation charExclamation 21.740302311
charDollar charDollar 18.505561199
remove remove 11.722965305
your your 8.282553567
free free 8.142952834
hp hp 7.399617456
num415 num415 0.000000000
direct direct 0.000000000
cs cs 0.000000000
original original 0.000000000
table table 0.000000000
charSquarebracket charSquarebracket 0.000000000
摘要函数还绘制了相对变量重要性图。如下截图所示:

图 2:相对变量影响图
接下来获取拟合对象的详细信息:
> spam_gbm
gbm(formula = spam_Formula, distribution = "bernoulli", data = spam_Train2,
n.trees = 500, shrinkage = 0.1, bag.fraction = 0.8)
A gradient boosted model with bernoulli loss function.
500 iterations were performed.
There were 57 predictors, of which 43 had nonzero influence.
Here, the choice of shrinkage = 0.1 leads to 43 nonzero influential variables. We can now reduce the shrinkage factor drastically and observe the impact:
> spam_gbm2 <- gbm(spam_Formula,distribution="bernoulli",
+ data=spam_Train2,n.trees=500,bag.fraction = 0.8,
+ shrinkage = 0.0001)
> spam_gbm2
gbm(formula = spam_Formula, distribution = "bernoulli", data = spam_Train2,
n.trees = 500, shrinkage = 1e-04, bag.fraction = 0.8)
A gradient boosted model with Bernoulli loss function.
500 iterations were performed.
There were 57 predictors of which 2 had nonzero influence.
收缩参数太低,因此几乎没有变量具有影响力。接下来,我们将生成以下图表:
> windows(height=100,width=200)
> par(mfrow=c(1,2))
> gbm.perf(spam_gbm,plot.it=TRUE)
Using OOB method...
[1] 151
Warning message:
In gbm.perf(spam_gbm, plot.it = TRUE) :
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv.folds>0 when calling gbm usually results in improved predictive performance.
> gbm.perf(spam_gbm2,plot.it=TRUE)
Using OOB method...
[1] 500
Warning message:
In gbm.perf(spam_gbm2, plot.it = TRUE) :
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv.folds>0 when calling gbm usually results in improved predictive performance.

图 3:收缩因子作为收敛的一个因素
我们对于极小的收缩因子没有清晰的收敛。
gbm 函数的更多详细信息可以从包文档或之前提供的源中获取。提升是一种非常通用的技术,Ridgeway 为各种数据结构实现了这一技术。下表列出了四种最常见的几种数据结构,展示了统计模型、偏差(与损失函数相关)、初始值、梯度和每个终端节点的输出估计:
| 输出类型 | 统计模型 | 偏差 | 初始值 | 梯度 | 终端节点估计 |
|---|---|---|---|---|---|
| 数值 | 高斯 | ![]() |
![]() |
![]() |
![]() |
| 二元 | 伯努利 | ![]() |
![]() |
![]() |
![]() |
| 计数 | 泊松 | ![]() |
![]() |
![]() |
![]() |
| 生存数据 | Cox 比例风险模型 | ![]() |
![]() |
0 | 牛顿-拉夫森算法 |
表 1:GBM 提升选项
我们将应用 gbm 函数对计数数据和生存数据进行处理。
计数数据的提升
事故、错误/打字错误、出生等数量数据是流行的例子。在这里,我们计算特定时间、地点和/或空间内事件的数量。泊松分布非常适合用于建模数量数据。当我们有以协变量和独立变量形式提供的额外信息时,相关的回归问题通常很有兴趣。广义线性模型是建模数量数据的一种流行技术。
让我们看看可从 stats.idre.ucla.edu/r/dae/poisson-regression/ 获取的模拟数据集。必要的更改如本源所示进行。首先,我们使用 glm 函数拟合泊松回归模型。接下来,我们拟合提升模型,如下所示:
> # Poisson regression and boosting
> # https://stats.idre.ucla.edu/r/dae/poisson-regression/
> pregnancy <- read.csv("https://stats.idre.ucla.edu/stat/data/poisson_sim.csv")
> pregnancy <- within(pregnancy, {
+ prog <- factor(prog, levels=1:3,
+ labels=c("General", "Academic","Vocational"))
+ id <- factor(id)
+ })
> summary(pregnancy)
id num_awards prog math
1 : 1 Min. :0.00 General : 45 Min. :33.0
2 : 1 1st Qu.:0.00 Academic :105 1st Qu.:45.0
3 : 1 Median :0.00 Vocational: 50 Median :52.0
4 : 1 Mean :0.63 Mean :52.6
5 : 1 3rd Qu.:1.00 3rd Qu.:59.0
6 : 1 Max. :6.00 Max. :75.0
(Other):194
> pregnancy_Poisson <- glm(num_awards ~ prog + math,
+ family="poisson", data=pregnancy)
> summary(pregnancy_Poisson)
Call:
glm(formula = num_awards ~ prog + math, family = "poisson", data = pregnancy)
Deviance Residuals:
Min 1Q Median 3Q Max
-2.204 -0.844 -0.511 0.256 2.680
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -5.2471 0.6585 -7.97 1.6e-15 ***
progAcademic 1.0839 0.3583 3.03 0.0025 **
progVocational 0.3698 0.4411 0.84 0.4018
math 0.0702 0.0106 6.62 3.6e-11 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for poisson family taken to be 1)
Null deviance: 287.67 on 199 degrees of freedom
Residual deviance: 189.45 on 196 degrees of freedom
AIC: 373.5
Number of Fisher Scoring iterations: 6
> pregnancy_boost <- gbm(num_awards ~ prog+math,dist="poisson",
+ n.trees=100,interaction.depth = 2,shrinkage=0.1,data=pregnancy)
> cbind(pregnancy$num_awards,predict(m1,type="response"),
+ predict(pboost,n.trees=100,type="response"))
[,1] [,2] [,3]
1 0 0.1352 0.1240
2 0 0.0934 0.1072
3 0 0.1669 0.3375
4 0 0.1450 0.0850
5 0 0.1260 0.0257
6 0 0.1002 0.0735
195 1 1.0469 1.4832
196 2 2.2650 2.0241
197 2 1.4683 0.4047
198 1 2.2650 2.0241
199 0 2.4296 2.0241
200 3 2.6061 2.0241
> sum((pregnancy$num_awards-predict(m1,type="response"))²)
[1] 151
> sum((pregnancy$num_awards-predict(pboost,n.trees=100,
+ type="response"))²)
[1] 141
> summary(pregnancy_boost)
var rel.inf
math math 89.7
prog prog 10.3
这是一个非常简单的例子,只有两个协变量。然而,在实践中,我们多次看到数量数据被当作回归问题处理。这是不幸的,一般回归技术并不是任何形式的炼金术。我们必须尊重数据结构,并且需要对数量数据进行分析。请注意,这里拟合的模型是非线性的。尽管这里的好处并不明显,但随着变量数量的增加,数量数据框架变得更加合适。我们将通过两个树的变量重要性图以及表格显示来结束对数量数据分析的讨论:

图 4:提升数量数据 – 变量重要性
> pretty.gbm.tree(pregnancy_boost,i.tree=18)
SplitVar SplitCodePred LeftNode RightNode MissingNode ErrorReduction Weight
0 1 64.50000 1 5 6 10.41 100
1 1 57.50000 2 3 4 1.14 90
2 -1 -0.01146 -1 -1 -1 0.00 71
3 -1 0.02450 -1 -1 -1 0.00 19
4 -1 -0.00387 -1 -1 -1 0.00 90
5 -1 0.05485 -1 -1 -1 0.00 10
6 -1 0.00200 -1 -1 -1 0.00 100
Prediction
0 0.00200
1 -0.00387
2 -0.01146
3 0.02450
4 -0.00387
5 0.05485
6 0.00200
> pretty.gbm.tree(pregnancy_boost,i.tree=63)
SplitVar SplitCodePred LeftNode RightNode MissingNode ErrorReduction Weight
0 1 60.50000 1 5 6 3.837 100
1 0 20.00000 2 3 4 0.407 79
2 -1 -0.00803 -1 -1 -1 0.000 40
3 -1 0.05499 -1 -1 -1 0.000 39
4 -1 0.02308 -1 -1 -1 0.000 79
5 -1 0.02999 -1 -1 -1 0.000 21
6 -1 0.02453 -1 -1 -1 0.000 100
Prediction
0 0.02453
1 0.02308
2 -0.00803
3 0.05499
4 0.02308
5 0.02999
6 0.02453
pretty.gbm.tree 函数有助于提取 gbm 对象的隐藏树。在下一节中,我们将处理生存数据的梯度提升技术。
生存数据的提升
pbc 数据集已在 第一章 和 集成技术简介 以及 第二章 和 自助法 中介绍。如前所述,生存数据有缺失观测值,我们需要专门的技术来处理这种情况。在表 1 中,我们看到偏差函数相当复杂。多亏了 Ridgeway,我们不必过多担心这类计算。相反,我们只需使用带有 dist="coxph" 选项的 gbm 函数,并按以下方式进行数据分析:
> # Survival data
> pbc_boost <- gbm(Surv(time,status==2)~trt + age + sex+ascites +
+ hepato + spiders + edema + bili + chol +
+ albumin + copper + alk.phos + ast + trig +
+ platelet + protime + stage,
+ n.trees=100,interaction.depth = 2,
+ shrinkage=0.01,dist="coxph",data=pbc)
> summary(pbc_boost)
var rel.inf
bili bili 54.220
age age 10.318
protime protime 9.780
stage stage 7.364
albumin albumin 6.648
copper copper 5.899
ascites ascites 2.361
edema edema 2.111
ast ast 0.674
platelet platelet 0.246
alk.phos alk.phos 0.203
trig trig 0.178
trt trt 0.000
sex sex 0.000
hepato hepato 0.000
spiders spiders 0.000
chol chol 0.000
> pretty.gbm.tree(pbc_boost,i.tree=2) # output suppressed
> pretty.gbm.tree(pbc_boost,i.tree=72) # output suppressed
因此,使用多功能的 gbm 函数,我们可以轻松地对各种数据结构执行梯度提升技术。
xgboost 软件包
xgboost R 包是梯度提升方法的优化、分布式实现。这是一个已知效率高、灵活且可移植的工程优化——有关更多详细信息及常规更新,请参阅github.com/dmlc/xgboost。这提供了并行树提升,因此在数据科学社区中被发现非常有用。特别是在www.kaggle.org的比赛中,许多获胜者使用了xgboost技术。Kaggle 获胜者的部分列表可在github.com/dmlc/xgboost/tree/master/demo#machine-learning-challenge-winning-solutions找到。
极端梯度提升实现的优点如下所示:
-
并行计算: 此包通过 OpenMP 启用并行处理,然后使用计算机的所有核心
-
正则化: 这通过结合正则化思想来避免过拟合问题
-
交叉验证: 执行交叉验证不需要额外的编码
-
剪枝: 这将树增长到最大深度,然后反向剪枝
-
缺失值: 缺失值在内部被处理
-
保存和重新加载: 这不仅有助于保存现有模型,还可以从上次停止的地方继续迭代
-
跨平台: 这适用于 Python、Scala 等
我们将使用书中早些时候看到的垃圾邮件数据集来说明这些想法。xgboost包的函数要求所有变量都是数值型,输出也应标记为0和1。此外,协变量矩阵和输出需要分别提供给xgboost R 包。因此,我们首先加载垃圾邮件数据集,然后创建分区和公式,如下所示:
> ## The xgboost Package
> data("spam")
> spam2 <- spam
> spam2$type <- as.numeric(spam2$type)-1
> head(data.frame(spam2$type,spam$type))
spam2.type spam.type
1 1 spam
2 1 spam
3 1 spam
4 1 spam
5 1 spam
6 1 spam
> # 1 denotes spam, and 0 - nonspam
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(spam2),replace = TRUE,
+ prob = c(0.7,0.3))
> spam2_Train <- spam2[Train_Test=="Train",]
> spam_Formula <- as.formula("type~.")
> spam_train <- list()
xgboost包还要求指定训练回归数据为特殊的dgCMatrix矩阵。因此,我们可以使用as函数将其转换:
> spam_train$data <- as(spam_train$data,"dgCMatrix")
> spam_train$label <- spam2_Train$type
> class(spam_train$data)
[1] "dgCMatrix"
attr(,"package")
[1] "Matrix"
我们现在已准备好应用xgboost函数的基础设施。选择nrounds=100和逻辑函数,结果如下所示:
> # Simple XGBoosting
> spam_xgb<- xgboost(data=spam_train$data,label=spam_train$label,+ nrounds = 100,objective="binary:logistic")
[1] train-error:0.064062
[2] train-error:0.063437
[3] train-error:0.053438
[4] train-error:0.050313
[5] train-error:0.047812
[6] train-error:0.045313
[95] train-error:0.002188
[96] train-error:0.001875
[97] train-error:0.001875
[98] train-error:0.001875
[99] train-error:0.000937
[100] train-error:0.000937
使用拟合的增强模型,我们现在应用predict函数并评估准确性:
> xgb_predict <- predict(spam_xgb,spam_train$data)
> sum(xgb_predict>0.5)
[1] 1226
> sum(spam_train$label)
[1] 1229
> table(spam_train$label,c(xgb_predict>0.5))
FALSE TRUE
0 1971 0
1 3 1226
如果观察到的概率(在这种情况下是响应)被标记为 1 的概率超过 0.5,那么我们可以将观察到的标签标记为 1,否则为 0。通过使用 table R 函数,我们可以获得预测标签和实际标签的列联表。显然,我们具有非常好的准确性,并且只有三个错误分类。
我们声称xgboost包不需要额外的编码来进行交叉验证分析。xgb.cv函数在这里很有用,它使用与xgboost函数相同的参数,并通过nfold选项指定的交叉验证折数来工作。在这里,我们选择nfold=10。现在,使用xgb.cv函数,我们进行该分析并评估预测准确率:
> # XGBoosting with cross-validation
> spam_xgb_cv <- xgb.cv(data=spam_train$data,
+ label=spam_train$label,nfold=10,nrounds = 100,
+ objective="binary:logistic",prediction = TRUE)
[1] train-error:0.064410+0.001426 test-error:0.091246+0.01697
[2] train-error:0.058715+0.001862 test-error:0.082805+0.01421
[3] train-error:0.052986+0.003389 test-error:0.077186+0.01472
[4] train-error:0.049826+0.002210 test-error:0.073123+0.01544
[5] train-error:0.046910+0.001412 test-error:0.070937+0.01340
[6] train-error:0.043958+0.001841 test-error:0.066249+0.01346
[95] train-error:0.001667+0.000340 test-error:0.048119+0.00926
[96] train-error:0.001528+0.000318 test-error:0.047181+0.01008
[97] train-error:0.001458+0.000260 test-error:0.046868+0.00974
[98] train-error:0.001389+0.000269 test-error:0.047181+0.00979
[99] train-error:0.001215+0.000233 test-error:0.047182+0.00969
[100] train-error:0.001111+0.000260 test-error:0.045932+0.01115
> xgb_cv_predict <- spam_xgb_cv$pred
> sum(xgb_cv_predict>0.5)
[1] 1206
> table(spam_train$label,c(xgb_cv_predict>0.5))
FALSE TRUE
0 1909 62
1 85 1144
交叉验证分析显示准确率有所下降。这表明我们在xgboost函数中遇到了过拟合问题。现在,我们将查看xgboost包的其他特性。在本节的开头,我们声称该技术通过提前停止和恢复早期拟合的模型对象提供了灵活性。
然而,一个重要的问题是何时需要提前停止迭代?我们没有关于迭代次数的任何基础理论,这个次数是作为变量数量和观测数量的函数。因此,我们将以指定的迭代次数开始这个过程。如果误差减少的收敛性没有低于阈值水平,那么我们将继续进行更多的迭代,这项任务将在下一部分进行。然而,如果指定的迭代次数过高,并且提升方法的性能正在变差,那么我们不得不停止迭代。这是通过指定early_stopping_rounds选项来实现的,我们将在下面的代码中将其付诸实践:
> # Stop early
> spam_xgb_cv2 <- xgb.cv(data=spam_train$data,label=
+ spam_train$label, early_stopping_rounds = 5,
+ nfold=10,nrounds = 100,objective="binary:logistic",
+ prediction = TRUE)
[1] train-error:0.064271+0.002371 test-error:0.090294+0.02304
Multiple eval metrics are present. Will use test_error for early stopping.
Will train until test_error hasn't improved in 5 rounds.
[2] train-error:0.059028+0.003370 test-error:0.085614+0.01808
[3] train-error:0.052048+0.002049 test-error:0.075930+0.01388
[4] train-error:0.049236+0.002544 test-error:0.072811+0.01333
[5] train-error:0.046007+0.002775 test-error:0.070622+0.01419
[6] train-error:0.042882+0.003065 test-error:0.066559+0.01670
[38] train-error:0.010382+0.001237 test-error:0.048121+0.01153[39] train-error:0.010069+0.001432 test-error:0.048434+0.01162
[40] train-error:0.009653+0.001387 test-error:0.048435+0.01154
[41] train-error:0.009236+0.001283 test-error:0.048435+0.01179
[42] train-error:0.008924+0.001173 test-error:0.048748+0.01154
Stopping. Best iteration:
[37] train-error:0.010625+0.001391 test-error:0.048121+0.01162
在这里,最佳迭代已经发生在编号37,这是通过early_stopping_rounds = 5选项获得的确认。现在我们已经找到了最佳迭代,我们停止这个过程。
现在,我们将查看如何添加更多的迭代。以下代码仅用于说明。使用nrounds = 10选项,以及早期拟合的spam_xgb,以及数据和标签的选项,我们将要求xgboost函数进行十次额外的迭代:
> # Continue training
> xgboost(xgb_model=spam_xgb,
+ data=spam_train$data,label=spam_train$label,
+ nrounds = 10)
[101] train-error:0.000937
[102] train-error:0.000937
[103] train-error:0.000937
[104] train-error:0.000937
[105] train-error:0.000937
[106] train-error:0.000937
[107] train-error:0.000937
[108] train-error:0.000937
[109] train-error:0.000937
[110] train-error:0.000937
##### xgb.Booster
raw: 136 Kb
call:
xgb.train(params = params, data = dtrain, nrounds = nrounds,
watchlist = watchlist, verbose = verbose, print_every_n = print_every_n, early_stopping_rounds = early_stopping_rounds, maximize = maximize, save_period = save_period, save_name = save_name, xgb_model = xgb_model, callbacks = callbacks)
params (as set within xgb.train):
silent = "1"
xgb.attributes:
niter
callbacks:
cb.print.evaluation(period = print_every_n)
cb.evaluation.log()
cb.save.model(save_period = save_period, save_name = save_name)
niter: 110
evaluation_log:
iter train_error
1 0.064062
2 0.063437
---
109 0.000937
110 0.000937
迭代次数的粗体和粗大字体并不是 R 软件输出的格式。这种变化是为了强调从早期拟合的spam_xgb对象开始的迭代次数现在将从101继续,并增加到110。使用xgboost函数很容易增加额外的迭代次数。
xgb.plot.importance函数与xgb.importance函数一起使用,可以用来提取并显示由提升方法确定的最重要的变量:
> # Variable Importance
> xgb.plot.importance(xgb.importance(names(spam_train$data),
+ spam_xgb)[1:10,])
结果是以下图表:

图 5:xgboost 函数的变量重要性图
我们现在已经看到了xgboost包的力量。接下来,我们将概述h2o包的功能。
h2o 包
R 软件的.exe文件大小为 75 MB(版本 3.4.1)。h2o R 包的大小为 125 MB。这可能会让你意识到h2o包的重要性。本书中使用的所有数据集大小都非常有限,观测数不超过 10,000。在大多数情况下,文件大小最大仅为几 MB。然而,数据科学界的工作非常努力,经常处理 GB 级别甚至更高格式的文件。因此,我们需要更多的能力,而h2o包正好提供了这些能力。我们只需简单地加载h2o包,就可以一窥究竟:
> library(h2o)
----------------------------------------------------------------------
Your next step is to start H2O:
> h2o.init()
For H2O package documentation, ask for help:
> ??h2o
After starting H2O, you can use the Web UI at http://localhost:54321
For more information visit http://docs.h2o.ai
----------------------------------------------------------------------
Attaching package: 'h2o'
The following objects are masked from 'package:stats':
cor, sd, var
The following objects are masked from 'package:base':
%*%, %in%, &&, ||, apply, as.factor, as.numeric, colnames,
colnames<-, ifelse, is.character, is.factor, is.numeric, log,
log10, log1p, log2, round, signif, trunc
Warning message:
package 'h2o' was built under R version 3.4.4
> h2o.init()
H2O is not running yet, starting it now...
Note: In case of errors look at the following log files:
C:\Users\tprabhan\AppData\Local\Temp\Rtmpu6f0fO/h2o_TPRABHAN_started_from_r.out
C:\Users\tprabhan\AppData\Local\Temp\Rtmpu6f0fO/h2o_TPRABHAN_started_from_r.err
java version "1.7.0_67"
Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
Starting H2O JVM and connecting: .. Connection successful!
R is connected to the H2O cluster:
H2O cluster uptime: 4 seconds 449 milliseconds
H2O cluster version: 3.16.0.2
H2O cluster version age: 7 months and 7 days !!!
H2O cluster name: H2O_started_from_R_TPRABHAN_saz680
H2O cluster total nodes: 1
H2O cluster total memory: 1.76 GB
H2O cluster total cores: 4
H2O cluster allowed cores: 4
H2O cluster healthy: TRUE
H2O Connection ip: localhost
H2O Connection port: 54321
H2O Connection proxy: NA
H2O Internal Security: FALSE
H2O API Extensions: AutoML, Algos, Core V3, Core V4
R Version: R version 3.4.0 (2017-04-21)
Warning message:
In h2o.clusterInfo() :
Your H2O cluster version is too old (7 months and 7 days)!
Please download and install the latest version from http://h2o.ai/download/
集群和线程有助于扩展计算。对于更热情的读者,以下资源将帮助使用h2o包:
使用gbm、xgboost和h2o包,读者可以分析复杂和大型数据集。
摘要
我们在本章开始时简要思考了为什么提升(boosting)有效。有三个视角可能解释了提升的成功,这些内容在我们深入探讨这个主题之前已经讨论过了。gbm包非常强大,它提供了调整梯度提升算法的不同选项,该算法可以处理多种数据结构。我们通过收缩选项展示了其能力,并将其应用于计数和生存数据结构。xgboost包是梯度提升方法的更高效实现。它更快,还提供了其他灵活性。我们通过使用xgboost函数进行交叉验证、早期停止以及根据需要继续迭代来展示其使用方法。h2o包/平台有助于在大规模上实现集成机器学习技术。
在下一章中,我们将探讨为什么集成(ensembling)有效。特别是,我们将看到为什么将多个模型组合在一起通常是一种有用的实践,我们还将探讨我们可以这样做的情况。
第七章. 通用集成技术
前四章讨论了决策树的集成技术。在这些章节中讨论的每个主题中,基础学习器都是决策树,因此我们深入研究了同质集成技术。在本章中,我们将展示基础学习器可以是任何统计或机器学习技术,并且它们的集成将提高预测的精度。一个重要要求是基础学习器应该比随机猜测更好。通过 R 程序,我们将讨论和阐明集成将有效的情况。投票是分类器的一个重要特性——我们将陈述两种不同的方法,并在 bagging 和随机森林集成器的情况下进行说明。平均技术是回归变量的集成器,它将遵循分类方法的讨论。本章将以对第一章中非正式介绍的堆叠方法的详细讨论结束,集成技术简介。主题流程如下:
-
集成为什么有效?
-
通过投票进行集成
-
通过平均进行集成
-
栈集成
技术要求
本章将使用以下库:
-
rpart -
randomForest
集成为什么有效?
当使用 bagging 方法时,我们将许多决策树的结果结合起来,通过多数计数来生成单个输出/预测。在不同的采样机制下,随机森林的结果被结合起来生成单个预测。在决策树的顺序误差减少方法下,提升方法也提供了改进的答案。尽管我们处理的是不确定数据,这涉及到概率,但我们不打算有那种给出黑盒结果且行为不一致的方法论。一个理论应该解释其工作原理,我们需要保证结果的一致性,并且对此没有神秘之处。任意和不确定的答案是完全不受欢迎的。在本节中,我们将探讨集成解决方案如何工作以及它们不工作的场景。
集成方法有强大的数学和统计基础,解释了为什么它们能给出这样的解决方案。我们首先考虑分类问题。我们将从一个简化的设置开始,并假设我们有T个相互独立的分类器,并且每个分类器相关的准确率与
相同。这是一个最简单的情况,我们稍后会推广这个场景。现在,如果我们有T个分类器,并且每个分类器对+1 或-1 等观察进行投票,这就引出了一个问题,整体准确率会是什么?由于T个分类器的正确分类数量必须超过错误分类数量,我们需要至少
个分类器来投票正确的结果。在这里,
表示小于给定分数的最大整数。只要
或更多数量的分类器为正确的类别投票,多数分类就是正确的。
为了澄清,重要的是要注意,当我们说一个分类器的准确率是p时,我们并不是指分类器将观察标记为+1 的概率是p。相反,我们在这里的意思是,如果分类器做出 100 次预测,预测可以是+1 和-1 的任何组合;100p次预测被分类器正确识别。准确率与+1 和-1 在人群中的分布无关。
在这种设置下,分类器标记正确观察的概率遵循参数为n = T和概率p的二项分布。因此,多数投票得到正确预测的概率如下:

既然我们已经提到分类器必须比随机猜测更好,我们就需要分类器的准确率超过 0.5。然后我们将逐步增加准确率,并观察分类器数量的增加如何影响多数投票的概率:
> source("Utilities.R")
> windows(height=100,width=100)
> # Ensembling in Classification
> # Illustrating the ensemble accuracy with same accuracy for each classifier
> # Different p's and T's with p > 0.5
> classifiers <- seq(9,45,2) # Number of classifiers
> accuracy <- seq(0.55,0.85,.05)
> plot(0,type='n',xlim=range(classifiers),ylim=c(0.6,1),
+ xlab="Number of Classifiers",ylab="Probability of Majority Voting")
> for(i in 1:length(accuracy)){
+ Prob_MV <- NULL
+ for(j in 1:length(classifiers)){
+ Prob_MV[j] <- sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=accuracy[i],size=classifiers[j]))
+ }
+ points(classifiers,Prob_MV,col=i,"l")
+ }
> title("Classifiers with Accuracy Better Than Random Guess")
seq函数设置了一个奇数序列,表示classifiers R数值向量中分类器的数量。准确率百分比在accuracy向量中从0.55到0.85不等。为了启动这个过程,我们设置了一个带有适当的x和y轴标签的空plot。现在,对于每个准确率值,我们将计算范围floor(classifiers[j]/2+1):classifiers[j]内多数投票的概率。floor(./2+1)确保我们选择了正确的起点。例如,如果分类器的数量是九个,那么floor(./2+1)的值是5。此外,当我们有九个分类器时,我们需要至少五个赞成事件发生的投票。另一方面,对于偶数个分类器(例如,八个)floor(./2+1)的值也是5。dbinom函数计算给定大小和概率的特定值的概率。在floor(classifiers[j]/2+1): classifiers[j]的范围内,它给出了多数投票的概率,或者多数投票的准确率。前面代码的输出在图 1中展示。我们可以从结果中看到,随着分类器数量的增加(每个分类器具有相同的准确率且优于随机猜测),多数投票的准确率也在增加:

图 1:为什么集成方法应该有效?
这将帮助我们看到某个准确率选择下的Prob_MV值——例如,0.65。我们将分别对prob=0.65运行循环,观察随着分类器数量的增加,多数投票的准确率是如何提高的:
> Prob_MV <- NULL
> for(j in 1:length(classifiers)){
+ Prob_MV[j] <- sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=0.65,size=classifiers[j]))
+ }
> Prob_MV
[1] 0.8282807 0.8513163 0.8705318 0.8867689 0.9006211 0.9125264 0.9228185
[8] 0.9317586 0.9395551 0.9463770 0.9523633 0.9576292 0.9622714 0.9663716
[15] 0.9699991 0.9732133 0.9760651 0.9785984 0.9808513
因此,随着具有相同准确率的分类器数量的增加,我们可以看到多数投票的准确率也在增加。值得注意的是,尽管我们每个分类器的准确率仅为0.65,但集成方法的准确率要高得多,几乎成为了一个完美的分类器。这就是集成方法的主要优势。
集成方法是否对任何类型的分类器都有帮助?如果我们有准确率低于随机猜测(即小于0.5)的分类器,那么我们将以与之前相同的方式进行搜索。对于许多准确率低于0.5的分类器,我们将计算多数投票分类器的准确率:
> # When p < 0.5, ensemble accuracy goes to zero
> classifiers <- seq(6,50,2)
> accuracy <- seq(0.45,0.05,-0.05)
> plot(0,type='n',xlim=range(classifiers),ylim=c(0,0.3),
+ xlab="Number of Classifiers",ylab="Probability of Majority Voting")
> for(i in 1:length(accuracy)){
+ Prob_MV <- NULL
+ for(j in 1:length(classifiers)){
+ Prob_MV[j] <- sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=accuracy[i],size=classifiers[j]))
+ }
+ points(classifiers,Prob_MV,col=i,"l")
+ }
> title("Classifiers with Accuracy Worse Than Random Guess")
前面 R 程序的结果显示在 图 2 中。现在,第一个观察结果是,无论准确率是接近 0.5 还是接近 0,多数投票分类器的概率/准确率都在下降,这不利于性能。在所有情况下,我们都看到准确率最终会接近零。R 代码块中的变化是分类器序列 seq(6,50,2),准确率水平从 0.45 下降到 0.05,在 seq(0.45,0.05,-0.05) 中。现在,考虑准确率略小于 0.5 的情况。例如,让我们将其保持在 0.4999。我们现在会幸运地看到性能改进吗?

图 2:集成不是炼金术!
> classifiers <- seq(10,200,10)
> Prob_MV <- NULL
> for(j in 1:length(classifiers)){
+ Prob_MV[j] <- sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=0.4999,size=classifiers[j]))
+ }
> Prob_MV
[1] 0.3767071 0.4115491 0.4273344 0.4368132 0.4433011 0.4480955 0.4518222
[8] 0.4548247 0.4573097 0.4594096 0.4612139 0.4627854 0.4641698 0.4654011
[15] 0.4665053 0.4675025 0.4684088 0.4692370 0.4699975 0.4706989
再次,我们发现我们无法匹配单个分类器的准确率。因此,我们有一个重要且关键的条件,即分类器必须比随机猜测更好。那么随机猜测本身呢?假装我们有一系列都是随机猜测的分类器并不困难。如果集成随着随机猜测的性能提高,我们通常不需要构建任何统计或机器学习技术。给定一组随机猜测,我们总能提高准确率。让我们来看看。
有两种情况——分类器数量为奇数和偶数——我们为这两种情况都提供了程序:
> accuracy <- 0.5
> classifiers <- seq(5,45,2)
> Prob_MV <- NULL
> for(j in 1:length(classifiers)){
+ Prob_MV[j] <- sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=accuracy,size=classifiers[j]))
+ }
> Prob_MV
[1] 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5
[19] 0.5 0.5 0.5
> classifiers <- seq(10,50,2)
> Prob_MV <- NULL
> for(j in 1:length(classifiers)){
+ Prob_MV[j] <- (sum(dbinom(floor(classifiers[j]/2):classifiers[j],
+ prob=accuracy,size=classifiers[j]))+
+ sum(dbinom(floor(classifiers[j]/2+1):classifiers[j],
+ prob=accuracy,size=classifiers[j])))/2
+ }
> Prob_MV
[1] 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5
[19] 0.5 0.5 0.5
这很有趣!无论分类器的数量如何,随机猜测的集成保持不变。在这里,既没有改进也没有恶化。因此,为了集成目的,我们总是需要比随机猜测更好的分类器。
在理解集成如何工作时,遵循你的直觉是很好的。我们从一个所有模型都具有相同准确率的过度简化假设开始,但如果我们处理具有不同准确率的模型,这样的假设就不适用了。因此,我们需要考虑可能对不同分类器有不同的准确率的情况。我们首先考虑每个分类器的准确率都高于 0.5,或者每个分类器都比随机猜测更好的情况。找到多数投票准确率的方法是评估分类器结果的每种可能组合的概率。我们考虑分类器数量为奇数时的简单情况。
假设我们拥有 T 个分类器,每个分类器的准确率如
所示。请注意
,因为这些对应于不同的度量。
评估具有不等准确率的多数投票概率所涉及步骤如下:
-
列出所有可能的基本事件。如果每个分类器对一个给定案例投票为真或假,这意味着它有两种可能的结果,以及 T 个分类器。列出
可能的结果:-
示例:如果我们有三个分类器,那么会有八种可能的情况,如下所示:
分类器 1 分类器 2 分类器 3 真 真 真 假 真 真 真 假 真 假 假 真 真 真 假 假 真 假 真 假 假 假 假 假
-
-
计算每个可能事件的概率。由于每个分类器的准确度不同,因此每个可能结果的概率也会不同:
-
示例:如果三个分类器(对于真)的准确度分别为 0.6、0.7 和 0.8,那么假的概率分别为 0.4、0.3 和 0.2,前表中的概率如下:
分类器 1 分类器 2 分类器 3 0.6 0.7 0.8 0.4 0.7 0.8 0.6 0.3 0.8 0.4 0.3 0.8 0.6 0.7 0.2 0.4 0.7 0.2 0.6 0.3 0.2 0.4 0.3 0.2
-
-
在下一步中,获取基本事件的概率,这将是每列数字的乘积:
分类器 1 分类器 2 分类器 3 概率 0.6 0.7 0.8 0.336 0.4 0.7 0.8 0.224 0.6 0.3 0.8 0.144 0.4 0.3 0.8 0.096 0.6 0.7 0.2 0.084 0.4 0.7 0.2 0.056 0.6 0.3 0.2 0.036 0.4 0.3 0.2 0.024 -
找出具有多数计数的事件。在这种情况下,这指的是大于或等于 2 的总和:
分类器 1 分类器 2 分类器 3 投票数 真 真 真 3 假 真 真 2 真 假 真 2 假 假 真 1 真 真 假 2 假 真 假 1 真 假 假 1 假 假 假 0 -
多数投票的概率然后就是投票数大于或等于 2 的情况的概率之和。这是概率列中第 1、2、3 和 5 行条目的总和,即 0.336 + 0.224 + 0.144 + 0.084 = 0.788。
我们需要在这里定义一个名为 Get_Prob 的函数,如下所示:
> Get_Prob <- function(Logical,Probability){
+ return(t(ifelse(Logical,Probability,1-Probability)))
+ }
给定一个逻辑向量和相应的概率向量,Get_Prob 函数将返回一个向量,该向量包含逻辑条件为 真 的概率。如果逻辑值为 假,则返回补数(1 – 概率)。
上述步骤被放入 R 程序中,如下所示:
> # Different accuracies T's illustration
> # For simplicity, we set the number of classifiers at odd number
> # Each p_i's greater than 0.5
> accuracy <- c(0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9)
> NT <- length(accuracy) # Number of classifiers
> APC <- expand.grid(rep(list(c(TRUE,FALSE)),NT)) # All possible combinations
> head(APC)
Var1 Var2 Var3 Var4 Var5 Var6 Var7 Var8 Var9
1 TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
2 FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
3 TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
4 FALSE FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
5 TRUE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
6 FALSE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
> Elements_Prob <- t(apply(APC,1,Get_Prob,Probability=accuracy))
> head(Elements_Prob)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
[1,] 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9
[2,] 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9
[3,] 0.5 0.45 0.6 0.65 0.7 0.75 0.8 0.85 0.9
[4,] 0.5 0.45 0.6 0.65 0.7 0.75 0.8 0.85 0.9
[5,] 0.5 0.55 0.4 0.65 0.7 0.75 0.8 0.85 0.9
[6,] 0.5 0.55 0.4 0.65 0.7 0.75 0.8 0.85 0.9
> Events_Prob <- apply(Elements_Prob,1,prod)
> Majority_Events <- (rowSums(APC)>NT/2)
> sum(Events_Prob*Majority_Events)
[1] 0.9112646
给定一个名为 accuracy 的包含准确率的数值向量,其中分类器的数量为奇数,我们首先使用 length 函数找到其中的分类器数量,并将其存储在 NT 中。然后使用 expand.grid 函数生成所有可能的 APC 组合,其中 rep 函数将向量 (TRUE, FALSE) NT 重复 NT 次。APC 对象的每一列将生成一个列,其中 TRUE 和 FALSE 条件将使用 Get_Prob 函数替换为相应的分类器准确率以及适当的补数。由于我们考虑的是奇数个分类器,当该基本事件中的 TRUE 数量大于分类器数量的 50%(即大于 NT/2)时,才会进行多数投票。其余的计算比较容易理解。如果九个分类器的准确率分别为 0.5、0.55、0.6、0.65、0.7、0.75、0.8、0.85 和 0.9,那么计算表明集成准确率为 0.9113,高于这里最准确的分类器,即 0.9。然而,我们必须记住,八个分类器中的每一个的准确率都低于 0.9。尽管如此,集成准确率仍然高于我们手头上的最高分类器。为了验证计算是否正常工作,我们将这种方法应用于周(2012)第 74 页上的示例,并确认最终多数投票概率为 0.933:
> accuracy <- c(0.7,0.7,0.7,0.9,0.9)
> NT <- length(accuracy) # Number of classifiers
> APC <- expand.grid(rep(list(c(TRUE,FALSE)),NT)) # All possible combinations
> Elements_Prob <- t(apply(APC,1,Get_Prob,Probability=accuracy))
> Events_Prob <- apply(Elements_Prob,1,prod)
> Majority_Events <- (rowSums(APC)>NT/2)
> sum(Events_Prob*Majority_Events)
[1] 0.93268
当每个分类器都不如随机猜测时会发生什么?我们将简单地输出九个分类器场景的准确率,并重复程序以获得以下答案:
> # Each p_i's lesser than 0.5
> accuracy <- 1-c(0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9)
> NT <- length(accuracy) # Number of classifiers
> APC <- expand.grid(rep(list(c(TRUE,FALSE)),NT)) # All possible combinations
> head(APC)
Var1 Var2 Var3 Var4 Var5 Var6 Var7 Var8 Var9
1 TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
2 FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
3 TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
4 FALSE FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
5 TRUE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
6 FALSE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
> Elements_Prob <- t(apply(APC,1,Get_Prob,Probability=accuracy))
> head(Elements_Prob)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
[1,] 0.5 0.45 0.4 0.35 0.3 0.25 0.2 0.15 0.1
[2,] 0.5 0.45 0.4 0.35 0.3 0.25 0.2 0.15 0.1
[3,] 0.5 0.55 0.4 0.35 0.3 0.25 0.2 0.15 0.1
[4,] 0.5 0.55 0.4 0.35 0.3 0.25 0.2 0.15 0.1
[5,] 0.5 0.45 0.6 0.35 0.3 0.25 0.2 0.15 0.1
[6,] 0.5 0.45 0.6 0.35 0.3 0.25 0.2 0.15 0.1
> Events_Prob <- apply(Elements_Prob,1,prod)
> Majority_Events <- (rowSums(APC)>NT/2)
> sum(Events_Prob*Majority_Events)
[1] 0.08873544
当每个分类器都不如随机猜测时,在集成的情况下,多数投票分类器会给出可怕的结果。这让我们面临最后一个情况。如果我们有一组分类器,其中一些比随机猜测分类器好,而一些比随机猜测分类器差呢?我们将计算代码块放入一个名为 Random_Accuracy 的函数中。然后,分类器中的准确率变成了单位区间内随机生成的数字。Random_Accuracy 函数随后运行十次,生成以下输出:
> # Mixture of p_i's, some > 0.5, and some < 0.5
> Random_Accuracy <- function() {
+ accuracy <- runif(9)
+ NT <- length(accuracy)
+ APC <- expand.grid(rep(list(c(TRUE,FALSE)),NT))
+ Elements_Prob <- t(apply(APC,1,Get_Prob,Probability=accuracy))
+ Events_Prob <- apply(Elements_Prob,1,prod)
+ Majority_Events <- (rowSums(APC)>NT/2)
+ return(sum(Events_Prob*Majority_Events))
+ }
> Random_Accuracy()
[1] 0.3423631
> Random_Accuracy()
[1] 0.3927145
> Random_Accuracy()
[1] 0.5341844
> Random_Accuracy()
[1] 0.1624876
> Random_Accuracy()
[1] 0.4065803
> Random_Accuracy()
[1] 0.4687087
> Random_Accuracy()
[1] 0.7819835
> Random_Accuracy()
[1] 0.3124515
> Random_Accuracy()
[1] 0.6842173
> Random_Accuracy()
[1] 0.2531727
结果参差不齐。因此,如果我们需要从集成方法中获得合理的准确性和性能,确保每个分类器都比随机猜测要好是至关重要的。到目前为止,我们分析的一个核心假设是分类器之间是相互独立的。在实际设置中,这个假设很少成立,因为分类器是使用相同的训练集构建的。然而,这个话题将在下一章中讨论。
我们现在将转向投票集成的问题。
投票集成
通过投票集成可以有效地用于分类问题。我们现在有一组分类器,我们需要使用它们来预测未知案例的类别。分类器预测的组合可以以多种方式进行。我们将考虑的两个选项是多数投票和加权投票。
多数投票
通过使用决策树作为基础学习器构建的集成,我们可以通过投票相关的想法进行说明,正如在开发袋装和随机森林时使用的那样。首先,我们将使用 randomForest 函数创建 500 个基础学习器,并重复第一个块中的程序,如第四章中所示,随机森林。集成已经在那一章中完成,我们将在这里详细说明那些步骤。首先,给出设置随机森林的代码块:
> load("../Data/GC2.RData")
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(GC2),
+ replace = TRUE,prob = c(0.7,0.3))
> GC2_Train <- GC2[Train_Test=="Train",]
> GC2_TestX <- within(GC2[Train_Test=="Test",],rm(good_bad))
> GC2_TestY <- GC2[Train_Test=="Test","good_bad"]
> GC2_Formula <- as.formula("good_bad~.")
> # RANDOM FOREST ANALYSIS
> GC2_RF <- randomForest(GC2_Formula,data=GC2_Train,keep.inbag=TRUE,
+ ntree=500)
接下来,我们将使用标准的 predict 函数来预测 GC2_TestX 数据的类别,然后,使用 predict.all=TRUE 选项,获取随机森林中生成的每个树的预测结果:
> # New data voting
> GC2_RF_Test_Margin <- predict(GC2_RF,newdata = GC2_TestX,
+ type="class")
> GC2_RF_Test_Predict <- predict(GC2_RF,newdata=GC2_TestX,
+ type="class",predict.all=TRUE
+ )
预测的 GC2_RF_Test_Predict 对象将包含进一步的 individual 对象,这些对象将包含每个决策树的预测。我们首先定义一个名为 Row_Count_Max 的函数,该函数将返回森林中计数最大的预测。然后,基本的投票方法将在以下代码块中与 predict 函数的结果进行比较:
> Row_Count_Max <- function(x) names(which.max(table(x)))
> # Majority Voting
> Voting_Predict <- apply(GC2_RF_Test_Predict$individual,1,
+ Row_Count_Max)
> head(Voting_Predict);tail(Voting_Predict)
1 2 3 4 9 10
"good" "bad" "good" "bad" "good" "bad"
974 980 983 984 988 996
"bad" "bad" "good" "good" "good" "good"
> all(Voting_Predict==GC2_RF_Test_Predict$aggregate)
[1] TRUE
> all(Voting_Predict==GC2_RF_Test_Margin)
[1] TRUE
> sum(Voting_Predict==GC2_TestY)/313
[1] 0.7795527
因此,我们可以看到 predict 函数实现了多数计数技术。接下来,我们将快速说明加权投票背后的思想和思考。
加权投票
在简单投票的使用中存在的一个隐含假设是所有分类器都是同样准确的,或者所有分类器都有相同的投票权。考虑一个更简单的情况,我们有五个分类器,其中三个的准确率为 0.51,剩下的两个准确率为 0.99。如果准确度较低的分类器将观察结果投票为负案例(-1),而两个更准确的分类器将其投票为正案例(+1),那么简单的投票方法将把观察结果标记为(-1)。在这种投票模式中,观察结果为-1 的概率是
,而为+1 的概率是
。因此,我们不能假装所有分类器都应该有相同的投票权。这就是我们将充分利用加权投票方法的地方。
在这次分析中,我们将使用训练数据集上分类器的准确率作为权重。我们将
视为与
相关的权重。权重的一个重要特征是它们应该是非负的,并且它们的总和应该是 1,即
。我们将归一化分类器的准确率以满足这一约束。
我们将继续使用德国信用数据集进行分析。首先,我们将获得训练数据集上 500 棵树的预测,然后获得准确率:
> # Analyzing Accuracy of Trees of the Fitted Forest
> GC2_RF_Train_Predict <- predict(GC2_RF,newdata=GC2_Train[,-20],
+ type="class",predict.all=TRUE)
> head(GC2_RF_Train_Predict$individual[,c(1:5,496:500)])
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
5 "bad" "bad" "bad" "bad" "good" "bad" "bad" "bad" "bad" "bad"
6 "good" "good" "good" "good" "good" "good" "bad" "bad" "bad" "good"
7 "good" "good" "good" "good" "good" "good" "good" "good" "good" "good"
8 "good" "good" "good" "good" "good" "bad" "good" "bad" "good" "good"
11 "bad" "bad" "bad" "bad" "bad" "bad" "bad" "bad" "bad" "bad"
12 "good" "bad" "bad" "bad" "bad" "good" "bad" "bad" "bad" "bad"
> RF_Tree_Train_Accuracy <- NULL
> for(i in 1:GC2_RF$ntree){
+ RF_Tree_Train_Accuracy[i] <- sum(GC2_RF_Train_Predict$individual[,i]==
+ GC2_Train$good_bad)/nrow(GC2_Train)
+ }
> headtail(sort(RF_Tree_Train_Accuracy),10)
[1] 0.8340611 0.8369723 0.8384279 0.8398836 0.8398836 0.8413392 0.8413392
[8] 0.8413392 0.8413392 0.8427948 0.8908297 0.8908297 0.8908297 0.8908297
[15] 0.8922853 0.8922853 0.8937409 0.8937409 0.8966521 0.8981077
headtail函数是什么?它在Utilities.R文件中可用。以下是对bagging集成器进行重复分析:
> # Bagging ANALYSIS
> GC2_Bagg <- randomForest(GC2_Formula,data=GC2_Train,keep.inbag=TRUE,
+ mtry=ncol(GC2_TestX),ntree=500)
> GC2_Bagg_Test_Predict <- predict(GC2_Bagg,newdata=GC2_TestX,
+ type="class",predict.all=TRUE)
> GC2_Bagg_Train_Predict <- predict(GC2_Bagg,newdata=GC2_Train[,-20],
+ type="class",predict.all=TRUE)
> Bagg_Tree_Train_Accuracy <- NULL
> for(i in 1:GC2_Bagg$ntree){
+ Bagg_Tree_Train_Accuracy[i] <- sum(GC2_Bagg_Train_Predict$individual[,i]==
+ GC2_Train$good_bad)/nrow(GC2_Train)
+ }
> headtail(sort(Bagg_Tree_Train_Accuracy),10)
[1] 0.8369723 0.8384279 0.8413392 0.8457060 0.8457060 0.8471616 0.8471616
[8] 0.8471616 0.8471616 0.8486172 0.8966521 0.8966521 0.8966521 0.8966521
[15] 0.8966521 0.8981077 0.8995633 0.8995633 0.9024745 0.9097525
接下来,我们将归一化权重并计算测试样本中观测值的加权投票,如下所示:
> # Weighted Voting with Random Forest
> RF_Weights <- RF_Tree_Train_Accuracy/sum(RF_Tree_Train_Accuracy)
> Bagg_Weights <- Bagg_Tree_Train_Accuracy/sum(Bagg_Tree_Train_Accuracy)
> RF_Weighted_Vote <- data.frame(matrix(0,nrow(GC2_TestX),ncol=3))
> names(RF_Weighted_Vote) <- c("Good_Weight","Bad_Weight","Prediction")
> for(i in 1:nrow(RF_Weighted_Vote)){
+ RF_Weighted_Vote$Good_Weight[i] <-
+ sum((GC2_RF_Test_Predict$individual[i,]=="good")*RF_Weights)
+ RF_Weighted_Vote$Bad_Weight[i] <-
+ sum((GC2_RF_Test_Predict$individual[i,]=="bad")*RF_Weights)
+ RF_Weighted_Vote$Prediction[i] <- c("good","bad")[which.max(RF_Weighted_Vote[i,1:2])]
+ }
> head(RF_Weighted_Vote,10)
Good_Weight Bad_Weight Prediction
1 0.8301541 0.16984588 good
2 0.3260033 0.67399668 bad
3 0.8397035 0.16029651 good
4 0.4422527 0.55774733 bad
5 0.9420565 0.05794355 good
6 0.2378956 0.76210442 bad
7 0.4759756 0.52402435 bad
8 0.7443038 0.25569624 good
9 0.8120180 0.18798195 good
10 0.7799587 0.22004126 good
如下所示,对bagging对象重复进行加权投票分析:
> # Weighted Voting with Bagging
> Bagg_Weights <- Bagg_Tree_Train_Accuracy/sum(Bagg_Tree_Train_Accuracy)
> Bagg_Weights <- Bagg_Tree_Train_Accuracy/sum(Bagg_Tree_Train_Accuracy)
> Bagg_Weighted_Vote <- data.frame(matrix(0,nrow(GC2_TestX),ncol=3))
> names(Bagg_Weighted_Vote) <- c("Good_Weight","Bad_Weight","Prediction")
> for(i in 1:nrow(Bagg_Weighted_Vote)){
+ Bagg_Weighted_Vote$Good_Weight[i] <-
+ sum((GC2_Bagg_Test_Predict$individual[i,]=="good")*Bagg_Weights)
+ Bagg_Weighted_Vote$Bad_Weight[i] <-
+ sum((GC2_Bagg_Test_Predict$individual[i,]=="bad")*Bagg_Weights)
+ Bagg_Weighted_Vote$Prediction[i] <- c("good","bad")[which.max(Bagg_Weighted_Vote[i,1:2])]
+ }
> head(Bagg_Weighted_Vote,10)
Good_Weight Bad_Weight Prediction
1 0.9279982 0.07200181 good
2 0.1634505 0.83654949 bad
3 0.8219618 0.17803818 good
4 0.4724477 0.52755226 bad
5 0.9619528 0.03804725 good
6 0.1698628 0.83013718 bad
7 0.4540574 0.54594265 bad
8 0.7883772 0.21162281 good
9 0.8301772 0.16982283 good
10 0.7585720 0.24142804 good
现在,随着投票机制的问题解决,我们将注意力转向回归问题。
通过平均进行集成
在回归模型的背景下,预测是感兴趣变量的数值。结合由于各种集成器导致的输出预测相对简单;由于集成机制,我们只需将集成器之间预测值的平均值解释为预测值。在分类问题的背景下,我们可以进行简单的平均和加权平均。在前一节中,集成器具有同质的基础学习器。然而,在本节中,我们将处理异质的基础学习器。
现在,我们将考虑一个在第八章《集成诊断》中详细处理的回归问题。问题是基于超过 60 个解释变量的房价预测。我们拥有训练和测试数据集,并将它们加载以启动过程:
> # Averaging for Regression Problems
> load("../Data/ht_imp_author.Rdata") # returns ht_imp object
> load("../Data/htest_imp_author.Rdata") # returns htest_imp
> names(ht_imp)[69] <- "SalePrice"
> dim(ht_imp)
[1] 1460 69
> dim(htest_imp)
[1] 1459 68
因此,我们有大量观测数据来构建我们的模型。SalePrice是这里感兴趣的变量。首先,我们创建一个公式并构建一个线性模型;四个不同深度的回归树;四个具有不同隐藏神经元的神经网络;以及以下代码块中的支持向量机模型:
> hf <- as.formula("SalePrice~.")
> SP_lm <- lm(hf,data=ht_imp)
> SP_rpart2 <- rpart(hf,data=ht_imp,maxdepth=2)
> SP_rpart4 <- rpart(hf,data=ht_imp,maxdepth=4)
> SP_rpart6 <- rpart(hf,data=ht_imp,maxdepth=6)
> SP_rpart8 <- rpart(hf,data=ht_imp,maxdepth=8)
> SP_nn2 <- nnet(hf,data=ht_imp,size=2,linout=TRUE)
# weights: 267
initial value 56996872361441.906250
final value 9207911334609.976562
converged
> SP_nn3 <- nnet(hf,data=ht_imp,size=3,linout=TRUE)
# weights: 400
initial value 56997125121706.257812
final value 9207911334609.960938
converged
> SP_nn4 <- nnet(hf,data=ht_imp,size=4,linout=TRUE)
# weights: 533
initial value 56996951452602.304687
iter 10 value 19328028546738.226562
iter 20 value 19324281941793.617187
final value 9080312934601.205078
converged
> SP_nn5 <- nnet(hf,data=ht_imp,size=5,linout=TRUE)
# weights: 666
initial value 56997435951836.507812
final value 9196060713131.609375
converged
> SP_svm <- svm(hf,data=ht_imp)
我们已经有了考虑异质集成的所需设置。
简单平均
我们使用训练数据集构建了十个模型,现在我们将使用predict函数在这些模型上对训练数据集进行预测,如下所示:
> # Simple Averaging
> SP_lm_pred <- predict(SP_lm,newdata=htest_imp)
Warning message:
In predict.lm(SP_lm, newdata = htest_imp) :
prediction from a rank-deficient fit may be misleading
> SP_rpart2_pred <- predict(SP_rpart2,newdata=htest_imp)
> SP_rpart4_pred <- predict(SP_rpart4,newdata=htest_imp)
> SP_rpart6_pred <- predict(SP_rpart6,newdata=htest_imp)
> SP_rpart8_pred <- predict(SP_rpart8,newdata=htest_imp)
> SP_nn2_pred <- predict(SP_nn2,newdata=htest_imp)
> SP_nn3_pred <- predict(SP_nn3,newdata=htest_imp)
> SP_nn4_pred <- predict(SP_nn4,newdata=htest_imp)
> SP_nn5_pred <- predict(SP_nn5,newdata=htest_imp)
> SP_svm_pred <- predict(SP_svm,newdata=htest_imp)
当涉及到分类问题时,预测要么基于类别标签,要么基于感兴趣类别的概率。因此,在预测的幅度方面,我们不会遇到不良预测,尽管我们至少需要检查预测是否给出+1 和-1 的混合。如果分类器只预测+1 或-1,那么这样的分类器可以被从进一步的分析中丢弃。对于回归问题,我们需要看看模型是否能在幅度上做出合理的预测,我们将简单地获得预测幅度的图,如下所示:
> windows(height=300,width=400)
> par(mfrow=c(2,5))
> plot.ts(SP_lm_pred,col=1)
> plot.ts(SP_rpart2_pred,col=2)
> plot.ts(SP_rpart4_pred,col=3)
> plot.ts(SP_rpart6_pred,col=4)
> plot.ts(SP_rpart8_pred,col=5)
> plot.ts(SP_nn2_pred,col=6)
> plot.ts(SP_nn3_pred,col=7)
> plot.ts(SP_nn4_pred,col=8)
> plot.ts(SP_nn5_pred,col=9)
> plot.ts(SP_svm_pred,col=10)
前一个代码块的结果如下所示:

图 3:十个异构基学习器的预测简单图
我们可以看到,与具有两个或三个隐藏神经元的神经网络模型相关的预测没有产生预测上的变化。因此,我们将这两个模型从进一步的分析中删除。集成预测只是剩余八个模型预测的平均值:
> Avg_Ensemble_Prediction <- rowMeans(cbind(SP_lm_pred,SP_rpart2_pred,
+ SP_rpart4_pred,SP_rpart6_pred,
+ SP_rpart8_pred,SP_nn4_pred,SP_nn5_pred,SP_svm_pred))
> plot.ts(Avg_Ensemble_Prediction)

图 4:住房数据集的集成预测
正如将简单投票扩展到加权投票一样,我们现在将探讨加权平均。
权重平均
在分类器的情况下,权重是从训练数据集的分类器的准确性中选择的。在这种情况下,我们需要像这样的统一度量。如果回归模型具有更小的残差方差,则更倾向于选择回归模型,我们将选择方差作为准确性的度量。假设弱基模型i的估计残差方差为
。在集成神经网络的情况下,Perrone 和 Cooper(1993)声称可以使用以下方程获得i个弱基模型的最佳权重:

由于比例常数无关紧要,我们将简单地用残差平方的平均值
来代替。在这个方向上,我们将首先通过简单地计算简单平均情况下考虑的八个模型的mean(residuals(model)²)来获得
(加上一个常数),如下所示:
> # Weighted Averaging
> SP_lm_sigma <- mean(residuals(SP_lm)²)
> SP_rp2_sigma <- mean(residuals(SP_rpart2)²)
> SP_rp4_sigma <- mean(residuals(SP_rpart4)²)
> SP_rp6_sigma <- mean(residuals(SP_rpart6)²)
> SP_rp8_sigma <- mean(residuals(SP_rpart8)²)
> SP_nn4_sigma <- mean(residuals(SP_nn4)²)
> SP_nn5_sigma <- mean(residuals(SP_nn5)²)
> SP_svm_sigma <- mean(residuals(SP_svm)²)
接下来,我们简单地实现权重公式
,如下所示:
> sigma_sum <- SP_lm_sigma + SP_rp2_sigma + SP_rp4_sigma +
+ SP_rp6_sigma + SP_rp8_sigma + SP_nn4_sigma +
+ SP_nn5_sigma + SP_svm_sigma
> sigma_sum
[1] 20727111061
> SP_lm_wts <- SP_lm_sigma/sigma_sum
> SP_rp2_wts <- SP_rp2_sigma/sigma_sum
> SP_rp4_wts <- SP_rp4_sigma/sigma_sum
> SP_rp6_wts <- SP_rp6_sigma/sigma_sum
> SP_rp8_wts <- SP_rp8_sigma/sigma_sum
> SP_nn4_wts <- SP_nn4_sigma/sigma_sum
> SP_nn5_wts <- SP_nn5_sigma/sigma_sum
> SP_svm_wts <- SP_svm_sigma/sigma_sum
rowMeans和cbind函数简单地给出了加权平均预测:
> Weighted_Ensemble_Prediction <- rowMeans(cbind(SP_lm_wts*SP_lm_pred,
+ SP_rp2_wts*SP_rpart2_pred,
+ SP_rp4_wts*SP_rpart4_pred,
+ SP_rp6_wts*SP_rpart6_pred,
+ SP_rp8_wts*SP_rpart8_pred,
+ SP_nn4_wts*SP_nn4_pred,
+ SP_nn5_wts*SP_nn5_pred,
+ SP_svm_wts*SP_svm_pred))
> plot.ts(Weighted_Ensemble_Prediction)
前一个代码的输出如下所示:

图 5:住房价格的加权平均预测
堆叠集成
在第一章中提供了一个堆叠回归的入门和激励示例,集成技术简介。在这里,我们将继续讨论一个尚未开发的回归问题的堆叠集成。
在堆叠集成中,几个弱模型的输出作为输入变量,以及用于构建早期模型的协变量,来构建一个堆叠模型。堆叠模型的形式可能是以下之一,或者可以是不同的模型。在这里,我们将简单地使用前几节中使用的八个回归模型作为弱模型。堆叠回归模型被选为梯度提升模型,并将给出原始输入变量和新模型的预测,如下所示:
> SP_lm_train <- predict(SP_lm,newdata=ht_imp)
Warning message:
In predict.lm(SP_lm, newdata = ht_imp) :
prediction from a rank-deficient fit may be misleading
> SP_rpart2_train <- predict(SP_rpart2,newdata=ht_imp)
> SP_rpart4_train <- predict(SP_rpart4,newdata=ht_imp)
> SP_rpart6_train <- predict(SP_rpart6,newdata=ht_imp)
> SP_rpart8_train <- predict(SP_rpart8,newdata=ht_imp)
> SP_nn4_train <- predict(SP_nn4,newdata=ht_imp)
> SP_nn5_train <- predict(SP_nn5,newdata=ht_imp)
> SP_svm_train <- predict(SP_svm,newdata=ht_imp)
>
> ht_imp2 <- cbind(ht_imp[,-69],SP_lm_train,SP_rpart2_train,SP_rpart4_train,
+ SP_rpart6_train,SP_rpart8_train,SP_nn4_train,SP_nn5_train,
+ SP_svm_train,ht_imp[,69])
> names(ht_imp2)[77] <- "SalePrice"
> SP_gbm <- gbm(hf,data=ht_imp2,distribution = "gaussian",n.trees=200)
> headtail(predict(SP_gbm,n.trees=100),20)
[1] 180260.6 177793.3 181836.9 177793.3 191927.7 177793.3 191927.7 182237.3
[9] 177793.3 177793.3 177793.3 191927.7 177793.3 187520.7 177793.3 177793.3
[17] 177793.3 177793.3 177793.3 177793.3 177908.2 177793.3 191927.7 177793.3
[25] 177793.3 177793.3 177793.3 191927.7 177793.3 177793.3 177793.3 191927.7
[33] 177793.3 177793.3 177793.3 177793.3 179501.7 191927.7 177793.3 177793.3
这结束了我们对堆叠集成回归的简单讨论。
摘要
在本章中,我们探讨了在分类问题背景下集成为什么有效。一系列详细程序说明了每个分类器必须比随机猜测更好的观点。我们考虑了所有分类器具有相同准确度、不同准确度和最后是完全任意准确度的场景。在随机森林和袋装方法背景下说明了多数和加权投票。对于回归问题,我们使用了不同的基学习器选择,并允许它们是异质的。在住房销售价格数据相关方面,我们展示了简单和加权平均方法。堆叠回归的简单说明最终结束了本章的技术部分。
在下一章中,我们将探讨集成诊断。
第八章. 集合诊断
在前面的章节中,我们发现集合方法非常有效。在前一章中,我们探讨了集合方法如何提高预测的整体准确性的场景。之前一直假设不同的基学习器之间是相互独立的。然而,除非我们有非常大的样本,并且基模型是使用一组不同观察值的学习者,否则这样的假设是非常不切实际的。即使我们有足够大的样本,相信分区是非重叠的,每个基模型都是建立在不同的分区上,每个分区都携带与任何其他分区相同的信息。然而,测试此类验证是困难的,因此我们需要采用各种技术来验证同一数据集上基模型的独立性。为此,我们将探讨各种不同的方法。本章将简要讨论集合诊断的必要性,并在下一节中介绍基模型多样性的重要性。对于分类问题,可以将分类器相互比较。然后我们可以进一步评估集合的相似性和准确性。在第三部分将介绍实现这一任务的统计测试。最初,将比较一个基学习器与另一个基学习器,然后我们将一次性查看集合中的所有模型。
本章将涵盖以下主题:
-
集合诊断
-
集合多样性
-
配对比较
-
评分者间一致性
技术要求
我们将在本章中使用以下库:
rpart
什么是集合诊断?
集成方法的力量在前几章中得到了展示。由决策树组成的集成是一个同质集成,这是第三章Bagging到第六章Boosting Refinements的主要内容。在第一章集成技术介绍和第七章通用集成技术中,我们简要介绍了堆叠集成。集成的一个中心假设是模型之间相互独立。然而,这个假设很少成立,我们知道相同的数据分区被反复使用。这并不意味着集成是坏的;我们有充分的理由在使用集成的同时预览集成应用中的担忧。因此,我们需要了解基础模型在预测上的相似程度以及整体上的相似程度。如果预测彼此很接近,那么我们可能需要在集成中使用这些基础模型。在这里,我们将为德国信用数据集构建逻辑回归、朴素贝叶斯、SVM 和决策树作为基础模型。分析和程序在这里略有重复,因为它是从早期章节继承过来的:
> load("../Data/GC2.RData")
> table(GC2$good_bad)
bad good
300 700
> set.seed(12345)
> Train_Test <- sample(c("Train","Test"),nrow(GC2),replace =
+ TRUE,prob = c(0.7,0.3))
> head(Train_Test)
[1] "Test" "Test" "Test" "Test" "Train" "Train"
> GC2_Train <- GC2[Train_Test=="Train",]
> GC2_TestX <- within(GC2[Train_Test=="Test",],rm(good_bad))
> GC2_TestY <- GC2[Train_Test=="Test","good_bad"]
> GC2_TestY_numeric <- as.numeric(GC2_TestY)
> GC2_Formula <- as.formula("good_bad~.")
> p <- ncol(GC2_TestX)
> ntr <- nrow(GC2_Train)
> nte <- nrow(GC2_TestX)
> # Logistic Regression
> LR_fit <- glm(GC2_Formula,data=GC2_Train,family = binomial())
> LR_Predict_Train <- predict(LR_fit,newdata=GC2_Train,
+ type="response")
> LR_Predict_Train <- as.factor(ifelse(LR_Predict_Train>0.5,
+ "good","bad"))
> LR_Accuracy_Train <- sum(LR_Predict_Train==GC2_Train$good_bad)/
+ ntr
> LR_Accuracy_Train
[1] 0.78
> LR_Predict_Test <- predict(LR_fit,newdata=GC2_TestX,
+ type="response")
> LR_Predict_Test_Bin <- ifelse(LR_Predict_Test>0.5,2,1)
> LR_Accuracy_Test <- sum(LR_Predict_Test_Bin==
+ GC2_TestY_numeric)/nte
> LR_Accuracy_Test
[1] 0.757
> # Naive Bayes
> NB_fit <- naiveBayes(GC2_Formula,data=GC2_Train)
> NB_Predict_Train <- predict(NB_fit,newdata=GC2_Train)
> NB_Accuracy_Train <- sum(NB_Predict_Train==
+ GC2_Train$good_bad)/ntr
> NB_Accuracy_Train
[1] 0.767
> NB_Predict_Test <- predict(NB_fit,newdata=GC2_TestX)
> NB_Accuracy_Test <- sum(NB_Predict_Test==GC2_TestY)/nte
> NB_Accuracy_Test
[1] 0.808
> # Decision Tree
> CT_fit <- rpart(GC2_Formula,data=GC2_Train)
> CT_Predict_Train <- predict(CT_fit,newdata=GC2_Train,
+ type="class")
> CT_Accuracy_Train <- sum(CT_Predict_Train==
+ GC2_Train$good_bad)/ntr
> CT_Accuracy_Train
[1] 0.83
> CT_Predict_Test <- predict(CT_fit,newdata=GC2_TestX,
+ type="class")
> CT_Accuracy_Test <- sum(CT_Predict_Test==GC2_TestY)/nte
> CT_Accuracy_Test
[1] 0.706
> # Support Vector Machine
> SVM_fit <- svm(GC2_Formula,data=GC2_Train)
> SVM_Predict_Train <- predict(SVM_fit,newdata=GC2_Train,
+ type="class")
> SVM_Accuracy_Train <- sum(SVM_Predict_Train==
+ GC2_Train$good_bad)/ntr
> SVM_Accuracy_Train
[1] 0.77
> SVM_Predict_Test <- predict(SVM_fit,newdata=GC2_TestX,
+ type="class")
> SVM_Accuracy_Test <- sum(SVM_Predict_Test==GC2_TestY)/nte
> SVM_Accuracy_Test
[1] 0.754
在下一节中,我们将强调在集成中多样性的必要性。
集成多样性
在一个集成中,我们有许多基础模型——比如说有L个。对于分类问题,我们有基础模型作为分类器。如果我们有一个回归问题,我们有基础模型作为学习器。由于诊断仅在训练数据集上执行,我们将放弃训练和验证分区这一惯例。为了简单起见,在接下来的讨论中,我们将假设我们有N个观测值。L个模型意味着对于N个观测值中的每一个,我们都有L个预测,因此预测的数量是
。我们正是在这些预测中试图找到集成的多样性。集成的多样性取决于我们处理的问题类型。首先,我们将考虑回归问题。
数值预测
在回归问题的情况下,观测值的预测值可以直接与它们的实际值进行比较。我们可以很容易地看到哪个基础模型的预测值更接近观测的实际值,哪个离它更远。如果所有预测值都彼此接近,则基础模型不具有多样性。在这种情况下,任何一个预测可能就足够了。如果预测值表现出一些变化,通过使用平均值组合它们可能提供稳定性。在评估多样性时,了解集成预测与真实观测值有多接近也是非常重要的。
让我们考虑一个假设场景,在这个场景中我们有六个观测值,它们的实际值,三个基础学习器,学习器的预测,以及集成预测。以下表格提供了一个示例数据集,有助于您理解集成多样性的复杂性:
| 观测编号 | 实际值 | E1 | E2 | E3 | EP |
|---|---|---|---|---|---|
| 1 | 30 | 15 | 20 | 25 | 20 |
| 2 | 30 | 40 | 50 | 60 | 50 |
| 3 | 30 | 25 | 30 | 35 | 30 |
| 4 | 30 | 28 | 30 | 32 | 30 |
| 5 | 30 | 20 | 30 | 40 | 30 |
| 6 | 30 | 10 | 15 | 65 | 30 |
表 1:六个观测值、三个基础学习器和集成
为了便于比较,表 1中所有观测值的真实值都保持在 30。六个观测/案例的集成预测范围从 10 到 65,而集成预测——基础学习器预测的平均值——范围从 20 到 50。为了理解特定观测值及其相关预测的集成多样性,我们将使用以下程序块可视化数据:
> DN <- read.csv("../Data/Diverse_Numeric.csv")
> windows(height=100,width=100)
> plot(NULL,xlim=c(5,70),ylim=c(0,7),yaxt='n',xlab="X-values",ylab="")
> points(DN[1,2:6],rep(1,5),pch=c(19,1,1,1,0),cex=2)
> points(DN[2,2:6],rep(2,5),pch=c(19,1,1,1,0),cex=2)
> points(DN[3,2:6],rep(3,5),pch=c(19,1,1,1,0),cex=2)
> points(DN[4,2:6],rep(4,5),pch=c(19,1,1,1,0),cex=2)
> points(DN[5,2:6],rep(5,5),pch=c(19,1,1,1,0),cex=2)
> points(DN[6,2:6],rep(6,5),pch=c(19,1,1,1,0),cex=2)
> legend(x=45,y=7,c("Actual","Model","Ensemble"),pch=c(19,1,0))
> axis(2,at=1:6,labels=paste("Case",1:6),las=1)
程序的解释在这里。代码的第一行从代码包文件夹中导入Diverse_Numeric.csv数据。windows (X11)函数在 Windows(Ubuntu)操作系统中设置一个新的图形设备。然后plot函数设置一个空白的绘图,并通过xlim和ylim指定坐标轴的范围。使用绘图和points函数将表 1中的每一行数据突出显示。选择pch需要进一步说明。如果我们选择pch为,例如,19、1和0,那么这意味着我们选择了填充的圆圈、圆圈和正方形。这三种形状分别表示实际值、模型预测和集成预测。轴命令帮助我们获得正确的标签显示。前面 R 代码块的结果是以下绘图:

图 1:理解回归问题中的集成多样性
我们有六个观察结果,每个都被标记为案例。首先考虑案例 1。每个观察结果的实心圆圈表示实际值——在这种情况下是30,这在整个数据集中都是相同的。对于这个观察结果,集成预测是20。空白圆圈中描绘的是三个基础模型的预测值15、20和25。集成预测——基础学习器预测的平均值——是20,并用空白正方形表示。现在,这三个值分布得不太分散,这被解释为表明对于这个观察结果,集成多样性较低。因此,这是一个低多样性-低估计的例子。
在表 1的第二种情况下,三个预测值分布得很好,具有很高的多样性。然而,50的集成估计值与实际值30相差太远,我们将这种情况称为高多样性-低估计。案例 3和案例 4因此被视为低多样性-高估计,因为集成预测与实际值相符,且三个集成预测值彼此接近。案例 5在多样性和准确性之间取得了良好的平衡,因此我们可以将其标记为高多样性-高估计的例子。最后一种情况准确性良好,尽管多样性过高,以至于集成预测并不好。你可以参考 Kuncheva (2014)了解更多关于集成学习器多样性-准确性困境的细节。
我们将在下一节考虑分类问题的多样性-准确性问题。
类别预测
上一节讨论了回归问题中的多样性-准确性问题。在分类问题的案例中,我们可以清楚地标记分类器的预测是否与实际输出/标签匹配。此外,我们只有两种可能的预测:0 或 1。因此,我们可以比较两个分类器在所有观察结果上的接近程度。例如,对于分类器
的两个可能结果和
的两个可能结果,对于给定的观察结果
有四种可能的场景:
-
预测标签为 1;
预测为 1 -
预测标签为 1;
预测为 0 -
预测标签为 0;
预测为 1 -
预测标签为 0;
预测为 0
在情景 1 和 4 中,两个分类器同意彼此,而在 2 和 3 中,它们不同意。如果我们有N个观测,每个观测用两个模型预测将落入上述四种情景之一。在我们考虑两个或更多模型的正式一致性或不一致性度量之前,我们将在接下来的讨论中考虑两个更简单的案例。
有一个流行的说法,如果两个人总是同意,其中一个人是不必要的。这与分类器工作的方式相似。同样,假设一对鹅被认为是非常忠诚的;它们彼此相伴,共同面对问题。现在,如果我们有两个模型在所有观测中都以与这些鹅相同的方式表现,那么多样性就会永远丧失。因此,在任何给定的集成情景中,我们需要消除鹅对,只保留其中之一。假设我们有一个L预测的矩阵,其中列对应分类器,行对应N个观测。在这种情况下,我们将定义一个名为GP的函数,以及鹅对的缩写,它将告诉我们哪些分类器有一个鹅对分类器在所有观测中与它们一致:
> # Drop the Geese Pair
>GP<- function(Pred_Matrix) {
+ L<- ncol(Pred_Matrix) # Number of classifiers
+ N<- nrow(Pred_Matrix)
+ GP_Matrix <- matrix(TRUE,nrow=L,ncol=L)
+ for(i in 1:(L-1)){
+ for(j in (i+1):L){
+ GP_Matrix[i,j] <- ifelse(sum(Pred_Matrix[,i]==Pred_Matrix[,j])==N,
+ TRUE,FALSE)
+ GP_Matrix[j,i] <- GP_Matrix[i,j]
+ }
+ }
+ return(GP_Matrix)
+ }
鹅对GP函数是如何工作的?我们向这个函数输入一个矩阵预测作为输入,其中列对应分类器,行对应观测。这个函数首先创建一个逻辑矩阵,其阶数为
,默认逻辑值为TRUE。由于一个分类器显然会与自己一致,我们接受默认值。此外,由于
分类器与
在相同的方式上同意/不同意,我们利用这一事实通过对称关系计算下矩阵。在两个嵌套循环中,我们比较一个分类器的预测与另一个分类器的预测。ifelse函数检查一个分类器的所有预测是否与另一个分类器匹配,如果对于单个观测条件不成立,我们说正在考虑的两个分类器不是鹅对,或者它们至少在某个场合上不同意:
接下来,将GP函数应用于为分类问题设置的 500 个分类器。CART_Dummy数据集来自RSADBE包。CART_DUMMY数据集和相关问题描述可以在 Tattar(2017)的第九章中找到。我们从这个相同的来源改编了代码和结果输出:
> data(CART_Dummy)
> CART_Dummy$Y <- as.factor(CART_Dummy$Y)
> attach(CART_Dummy)
> windows(height=100,width=200)
> par(mfrow=c(1,2))
> plot(c(0,12),c(0,10),type="n",xlab="X1",ylab="X2")
> points(X1[Y==0],X2[Y==0],pch=15,col="red")
> points(X1[Y==1],X2[Y==1],pch=19,col="green")
> title(main="A Difficult Classification Problem")
> plot(c(0,12),c(0,10),type="n",xlab="X1",ylab="X2")
> points(X1[Y==0],X2[Y==0],pch=15,col="red")
> points(X1[Y==1],X2[Y==1],pch=19,col="green")
> segments(x0=c(0,0,6,6),y0=c(3.75,6.25,2.25,5),
+ x1=c(6,6,12,12),y1=c(3.75,6.25,2.25,5),lwd=2)
> abline(v=6,lwd=2)
> title(main="Looks a Solvable Problem Under Partitions")
如程序所示,我们这里有三个变量:X1、X2和Y。由Y表示的变量是一个二元变量——一个类别用绿色表示,另一个用红色表示。使用X1和X2变量提供的信息,目标是预测Y的类别。红色和绿色点交织在一起,因此单一线性分类器不足以在这里将红色和绿色分开。然而,如果我们通过X1和X2递归地划分数据空间,如图 2 右侧的最终图所示,那么红色和绿色看起来是可分离的。前面的 R 代码块产生以下图表:

图 2:一个典型的分类问题
为CART_DUMMY数据集设置了包含500棵树的随机森林。固定的种子确保了在任何执行中输出的可重复性。使用拟合的随机森林,我们接下来使用500棵树预测所有观测值的输出。type="class"和predict.all=TRUE选项是此代码块的核心。然后,将GP函数应用于500棵树的预测矩阵。请注意,GP矩阵的对角元素始终为TRUE。因此,如果有任何分类器与所有观测值完全一致,该单元格的值将是TRUE。如果行和超过计数 2,则该分类器有鹅分类器。以下代码捕获了整个计算过程:
> CD <- CART_Dummy
> CD$Y <- as.factor(CD$Y)
> set.seed(1234567)
> CD_RF <- randomForest(Y~.,data=CD,ntree=500)
> CD_RF_Predict <- predict(CD_RF,newdata=CD,
+ type="class",predict.all=TRUE)
> CD_RF_Predict_Matrix <- CD_RF_Predict$individual
> CD_GP <- GP(CD_RF_Predict_Matrix)
> CD_GP[1:8,1:8]
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[2,] FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
[3,] FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
[4,] FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE
[5,] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE
[6,] FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE
[7,] FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
[8,] FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
> rowSums(CD_ST)
[1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[38] 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[149] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1
[186] 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1
[223] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1
[260] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1
[297] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[334] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 1 1
[371] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[408] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1
[482] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
读者应注意的是,前面输出中 2 的粗体和较大字体不是由 R 给出的。这是由处理文本内容的软件修改的。因此,我们有很多具有鹅分类器的分类器,它们与各自的预测相匹配。使用which函数,我们首先找到所有满足条件的分类器索引,然后,通过应用which函数到CD_GP矩阵的行,我们得到相关的鹅分类器:
> which(rowSums(CD_GP)>1)
[1] 21 42 176 188 206 221 256 278 290 363 365 385 424 442
> which(CD_GP[21,]==TRUE)
[1] 21 188
> which(CD_GP[42,]==TRUE)
[1] 42 290
> which(CD_GP[176,]==TRUE)
[1] 176 363
> which(CD_GP[206,]==TRUE)
[1] 206 256
> which(CD_GP[221,]==TRUE)
[1] 221 278
> which(CD_GP[365,]==TRUE)
[1] 365 424
> which(CD_GP[385,]==TRUE)
[1] 385 442
运行前面的代码后,我们能够识别与分类器关联的鹅分类器。我们可以选择移除鹅对中的任何一个成员。在下一个示例中,我们将应用此方法到德国信用数据。程序尝试以下方式识别鹅分类器:
> set.seed(12345)
> GC2_RF3 <- randomForest(GC2_Formula,data=GC2_Train,mtry=10,
+ parms = list(split="information",
+ loss=matrix(c(0,1,1000,0),byrow = TRUE,nrow=2)),
+ ntree=1000)
> GC2_RF_Train_Predict <- predict(GC2_RF3,newdata=GC2_Train,
+ type="class",predict.all=TRUE)
> GC2_RF_Train_Predict_Matrix <- GC2_RF_Train_Predict$individual
> GC2_GP <- GP(GC2_RF_Train_Predict_Matrix)
> rowSums(GC2_GP)
[1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[37] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[973] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
> which(rowSums(GC2_GP)>1)
integer(0)
由于没有分类器有对应的鹅分类器,我们不需要消除任何树。
在 Kuncheva (2014),第 112 页,有一个有用的度量标准,称为神谕输出。接下来,我们正式定义这个量。记住,我们有L个分类器和N个观测值。标签的原始/实际值用
表示。我们将使用分类器 j 表示第 i 个预测值,用
表示。
Oracle 输出:如果预测值
等于
,则 Oracle 输出
被定义为1;否则,它被定义为0。用数学术语来说,Oracle 输出使用以下数学表达式给出:

那么,Oracle 输出和预测之间有什么区别?预测包括数据的标签,标签可能是 1/0、GOOD/BAD、+1/-1、YES/NO 或其他二进制标签对。此外,在二进制标签的情况下,预测为 1 并不一定意味着原始值是 1;它也可能是 0。如果预测 1 为 1 或 0 为 0,Oracle 输出取值为 1;否则,它取值为 0。使用 Oracle 输出的一个后果是,分类器中 1 的比例将给我们提供分类器的准确度。
我们现在将创建一个名为Oracle的 R 函数,该函数在预测矩阵和实际标签作为输入时将给出 Oracle 输出。之后,我们将计算分类器的准确度:
> # Oracle Output
> Oracle <- function(PM,Actual){
+ # PM = Prediction Matrix, Actual = the true Y's
+ OM <- matrix(0,nrow=nrow(PM),ncol=ncol(PM))
+ for(i in 1:ncol(OM)) {
+ OM[,i] <- as.numeric(PM[,i]==Actual)
+ }
+ return(OM)
+ }
> GC_Oracle <- Oracle(PM=GC2_RF_Train_Predict$individual,
+ Actual=GC2_Train$good_bad)
> colSums(GC_Oracle)/nrow(GC_Oracle)
[1] 0.872 0.884 0.859 0.869 0.866 0.878 0.888 0.872 0.869 0.875 0.885 0.869
[13] 0.881 0.866 0.879 0.856 0.870 0.869 0.857 0.870 0.878 0.868 0.886 0.892
[25] 0.881 0.863 0.866 0.856 0.886 0.876 0.873 0.879 0.875 0.885 0.872 0.872
[973] 0.860 0.873 0.869 0.888 0.863 0.879 0.882 0.865 0.891 0.863 0.878 0.879
[985] 0.878 0.869 0.856 0.872 0.889 0.881 0.868 0.881 0.884 0.854 0.882 0.882
[997] 0.862 0.884 0.873 0.885
Oracle 矩阵帮助我们获得分类器的准确度。在下一节中,我们将讨论一些有助于我们了解分类器之间距离的度量。
配对度量
在本节中,我们将提出一些度量两个分类器之间一致性的方法。目的是固定两个分类器之间的一致性/不一致性概念,然后在下一节中将该概念应用到集成分类器的整体分类器中。如果
和
是具有预测
的分类器模型,那么我们可以获得一个表格,它给出以下内容:
-
预测
为 1;
预测它为 1 -
预测
为 1;
预测它为 0 -
预测
为 0;
预测它为 1 -
预测
为 0;
预测它为 0
在N个观测值中的信息可以以表格形式表示,如下所示:
| M1 预测 1 | M1 预测 0 | |
|---|---|---|
| M2 预测 1 | n11 | n10 |
| M2 预测 0 | n01 | n00 |
表 2:两个分类器/评分者的列联表
前一个表的对角线元素显示两个模型/分类器之间的一致性,而离对角线元素显示不一致性。这些模型有时被称为评分者。频率表也被称为列联表。使用这种设置,我们现在将讨论一些有用的一致性度量。比较被称为成对度量,因为我们只分析了一对分类器。
不一致性度量
两个分类器/评分者之间的一致性度量定义为以下公式:

我们现在将定义一个DM函数,该函数接受两个分类器的预测。该函数首先为预测准备列联表。不一致性度量的计算是直接的,如下代码块所示:
> # Disagreement Measure
> DM <- function(prediction1,prediction2){
+ tp <- table(prediction1,prediction2)
+ Diss <- (tp[1,2]+tp[2,1])/length(prediction1)
+ return(Diss)
+ }
在第一部分,我们根据逻辑回归模型、朴素贝叶斯、SVM 和分类树对德国信用数据进行了预测。现在我们将 DM 函数应用于这些预测,看看这些分类器之间有多大的不一致性:
> DM(LR_Predict_Train,NB_Predict_Train)
[1] 0.121
> DM(LR_Predict_Train,CT_Predict_Train)
[1] 0.154
> DM(LR_Predict_Train,SVM_Predict_Train)
[1] 0.153
> DM(NB_Predict_Train,CT_Predict_Train)
[1] 0.179
> DM(NB_Predict_Train,SVM_Predict_Train)
[1] 0.154
> DM(CT_Predict_Train,SVM_Predict_Train)
[1] 0.167
由于我们有四个分类器,因此将会有 3 + 2 + 1 = 6 个成对比较。朴素贝叶斯和分类树之间的不一致性最大,而逻辑回归和朴素贝叶斯分类器之间的一致性最小。可以使用 DM 度量来轻松地获得两个模型的不一致性。
Yule 或 Q 统计量
Yule 系数是一致性的度量,当其值接近于零时,它将给出两个评分者之间的一致性。Yule 度量使用以下公式给出:

Q 统计量的值在相关系数的范围内——即
。因此,如果 Q 值接近于 1,这意味着两个度量几乎总是相互一致,而接近于-1 的值意味着两个模型预测的是相反的。当 Q 值接近于 0 时,这意味着两个评分者之间有非常弱的相关性。以下代码块创建并应用了一个Yule函数,用于不同的模型预测:
> # Q-statistic
> Yule <- function(prediction1,prediction2){
+ tp <- table(prediction1,prediction2)
+ Yu <- (tp[1,1]*tp[2,2]-tp[1,2]*tp[2,1])/(tp[1,1]*tp[2,2]+tp[1,2]*tp[2,1])
+ return(Yu)
+ }
> Yule(LR_Predict_Train,NB_Predict_Train)
[1] 0.949
> Yule(LR_Predict_Train,CT_Predict_Train)
[1] 0.906
> Yule(LR_Predict_Train,SVM_Predict_Train)
[1] 0.98
> Yule(NB_Predict_Train,CT_Predict_Train)
[1] 0.865
> Yule(NB_Predict_Train,SVM_Predict_Train)
[1] 0.985
> Yule(CT_Predict_Train,SVM_Predict_Train)
[1] 0.912
朴素贝叶斯预测和 SVM 预测之间的一致性最高。请注意,如果我们取不一致性度量的补数并使用以下代码轻松执行,我们得到以下一致性的度量:
> 1-DM(LR_Predict_Train,NB_Predict_Train)
[1] 0.879
> 1-DM(LR_Predict_Train,CT_Predict_Train)
[1] 0.846
> 1-DM(LR_Predict_Train,SVM_Predict_Train)
[1] 0.847
> 1-DM(NB_Predict_Train,CT_Predict_Train)
[1] 0.821
> 1-DM(NB_Predict_Train,SVM_Predict_Train)
[1] 0.846
> 1-DM(CT_Predict_Train,SVM_Predict_Train)
[1] 0.833
然而,这项分析表明,逻辑回归和朴素贝叶斯评分者之间的一致性最高。因此,我们注意到输出和比较可能会导致不同的结论。也可以计算两个评分者之间的相关系数;我们将在下一部分介绍。
相关系数度量
两个数值变量之间的相关系数非常直观,当它们之间存在线性关系时,它也是一个非常有用的关系度量。如果两个变量在本质上都是分类的,那么我们仍然可以获取它们之间的相关系数。对于两个评分者,相关系数使用以下公式计算:

我们将定义一个SS_Cor函数,它将执行必要的计算并返回相关系数:
> # Correlation coefficient
> # Sneath and Sokal, 1973
> SS_Cor <- function(prediction1, prediction2){
+ tp <- table(prediction1,prediction2)
+ a <- tp[1,1]; b <- tp[2,1]; c <- tp[1,2]; d <- tp[2,2]
+ SS <- (a*d-b*c)/sqrt(exp(log(a+b)+log(a+c)+log(c+d)+log(b+d)))
+ return(SS)
+ }
现在将相关系数函数应用于预测,如前例所示:
> SS_Cor(LR_Predict_Train,NB_Predict_Train)
[1] 0.69
> SS_Cor(LR_Predict_Train,CT_Predict_Train)
[1] 0.593
> SS_Cor(LR_Predict_Train,SVM_Predict_Train)
[1] 0.584
> SS_Cor(NB_Predict_Train,CT_Predict_Train)
[1] 0.531
> SS_Cor(NB_Predict_Train,SVM_Predict_Train)
[1] 0.587
> SS_Cor(CT_Predict_Train,SVM_Predict_Train)
[1] 0.493
结果表明,逻辑回归和朴素贝叶斯预测比任何其他组合都更一致。相关性测试可以用来检查分类器的预测是否相互独立。
练习:应用chisq.test来检查各种分类器预测的独立性。
科亨统计量
科亨统计量首次出现在 1960 年。它基于两个评分者因偶然或巧合而达成一致的概率。两个评分者达成一致的概率如下所示:

然而,随机或偶然达成一致的概率如下所示:

使用
和
的定义,科亨统计量如下定义:

科亨的 Kappa 值也可以是负数。如果其值为 1,这意味着评分者完全一致。0 的值表示一致仅是偶然的,负值表示偶然的一致性低于预期。首先,在以下代码中创建了 R 函数Kappa:
> # Kappa-statistic
> # Cohen's Statistic
> Kappa <- function(prediction1, prediction2){
+ tp <- table(prediction1,prediction2)
+ a <- tp[1,1]; b <- tp[2,1]; c <- tp[1,2]; d <- tp[2,2]
+ n <- length(prediction1)
+ theta1 <- (a+d)/n
+ theta2 <- (((a+b)*(a+c))+((c+d)*(b+d)))/n²
+ kappa <- (theta1-theta2)/(1-theta2)
+ return(kappa)
+ }
编码部分是公式的清晰实现,对a、b、c、d、theta1和theta2的选择已经做出,以便代码易于理解和遵循。接下来,我们将预测应用于德语训练数据集:
> Kappa(LR_Predict_Train,NB_Predict_Train)
[1] 0.69
> Kappa(LR_Predict_Train,CT_Predict_Train)
[1] 0.592
> Kappa(LR_Predict_Train,SVM_Predict_Train)
[1] 0.524
> Kappa(NB_Predict_Train,CT_Predict_Train)
[1] 0.53
> Kappa(NB_Predict_Train,SVM_Predict_Train)
[1] 0.525
> Kappa(CT_Predict_Train,SVM_Predict_Train)
[1] 0.453
再次,逻辑回归和朴素贝叶斯预测的一致性是最高的。我们现在转向最终的分歧度量。
双误度量
在网球中,双误指的是发球失败的情况。发球者有两个机会发球正确,如果他们没有做到,则该分被判给对手。双误度量发生在两个分类器都做出错误预测时:

显然,我们需要 DF 尽可能低,接近 0。这个函数易于理解,因此这将被留给读者作为练习来跟随。以下代码给出了双误度量的 R 函数及其应用:
> # Double-fault Measure
> Double_Fault <- function(prediction1,prediction2,actual){
+ DF <- sum((prediction1!=actual)*(prediction2!=actual))/
+ length(actual)
+ return(DF)
+ }
> Double_Fault(LR_Predict_Train,NB_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.166
> Double_Fault(LR_Predict_Train,CT_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.118
> Double_Fault(LR_Predict_Train,SVM_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.148
> Double_Fault(NB_Predict_Train,CT_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.709
> Double_Fault(NB_Predict_Train,SVM_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.154
> Double_Fault(CT_Predict_Train,SVM_Predict_Train,
+ GC2_Train$good_bad)
[1] 0.116
读者应使用双重错误度量来识别最佳一致性。
练习:在多标签(超过两个类别)的情况下,本节讨论的度量扩展变得繁琐。相反,可以使用 Oracle 矩阵并重复这些度量。读者应将这些措施应用于 Oracle 输出。
到目前为止讨论的方法仅适用于一对分类器。在下一节中,我们将测量集成中所有分类器的多样性。
交互一致性
在前节讨论的集成分类器度量简单扩展是计算所有可能的集成对度量,然后简单地平均这些值。这项任务构成了下一个练习。
练习:对于所有可能的集成对组合,计算不一致度量、Yule 统计量、相关系数、Cohen's kappa 和双重错误度量。完成这些后,获得比较的平均值,并将它们报告为集成多样性。
在这里,我们将提出多样性的替代度量,并使用熵度量启动讨论。在本节的所有讨论中,我们将使用 Oracle 输出。
熵度量
你可能记得,我们根据
来表示 Oracle 输出。对于特定实例,如果错误分类该实例的分类器数量为
,则集成具有最大多样性。这意味着
的值为 0,其余的
、
、
的值为 1。然后,集成熵度量定义为以下:

熵度量 E 的值位于单位区间内。如果 E 值接近 0,这意味着集成中没有多样性,而接近 1 的值意味着多样性达到最高可能水平。给定 Oracle 矩阵,我们可以轻松地计算熵度量,如下所示:
> # Entropy Measure
> # Page 250 of Kuncheva (2014)
> Entropy_Measure <- function(OM){
+ # OM = Oracle Matrix
+ N <- nrow(OM); L <- ncol(OM)
+ E <- 0
+ for(i in 1:N){
+ E <- E+min(sum(OM[i,]),L-sum(OM[i,]))
+ }
+ E <- 2*E/(N*(L-1))
+ return(E)
+ }
> Entropy_Measure(GC_Oracle)
[1] 0.255
通过在德国信用数据集的集成上应用Entropy_Measure,我们可以看到熵度量值为0.255。由于熵度量值没有接近 0,随机森林集成表现出多样性。然而,它也远离 1,这表明存在多样性。然而,没有临界值或测试来解释多样性是否太低,甚至太高。
Kohavi-Wolpert 度量
Kohavi-Wolpert 度量基于预测的方差为 1 或 0。它基于分类器错误率的分解公式。对于二元问题或使用 Oracle 输入时,方差与 Gini 指数相同。这如下公式给出:

Kohavi-Wolpert 度量是所有观察到的方差的平均值。通过使用 Oracle 矩阵给出的预测概率,或者作为拟合对象的副产品,我们可以获得方差,然后对观察到的方差进行平均。现在创建了一个 R 函数,并将其应用于德国信用数据的一些预测,如下所示:
> # Kohavi-Wolpert variance
> # Using the predicted probability
> KW <- function(Prob){
+ N <- nrow(Prob)
+ kw <- mean(1-Prob[,1]²-Prob[,2]²)/2
+ return(kw)
+ }
> GC2_RF_Train_Predict_Prob <- predict(GC2_RF3,newdata=GC2_Train,
+ type="prob",predict.all=TRUE)
> GC2_RF_Train_Prob <- GC2_RF_Train_Predict_Prob$aggregate
> KW(GC2_RF_Train_Prob)
[1] 0.104
Kohavi-Wolpert 度量也可以通过 Oracle 输出获得。我们定义一个数学实体,用来计算正确分类观察到的分类器的数量如下:

被正确预测的概率如下:

使用这些概率,我们可以获得方差如下:

此方法通过以下代码使用KW_OM函数实现:
> # Using the Oracle matrix
> KW_OM<- function(OM){
+ # OM is the oracle matrix
+ N <- nrow(OM); L <- ncol(OM)
+ kw <- 0
+ for(i in 1:N){
+ lz <- sum(OM[i,])
+ kw <- kw + lz*(L-lz)
+ }
+ kw <- kw/(N*L²)
+ return(kw)
+ }
> KW_OM(GC_Oracle)
[1] 0.104
从这里我们可以看出,这两种方法给出了相同的结果。很明显,在随机森林构建之后,我们没有看到太多的多样性。
集成不一致度量
两个分类器之间的不一致度量可以定义为以下:

集成的不一致度量如下所示:

Kohavi-Wolpert 度量与不一致度量之间的关系如下:

下一个 R 代码块展示了使用 Oracle 输出实现 Kohavi-Wolper 度量的方法如下:
> # Disagreement Measure OVerall on Oracle Matrix
> DMO <- function(OM){
+ # OM is the oracle matrix
+ N <- nrow(OM); L <- ncol(OM)
+ dmo <- 0
+ for(i in 1:L){
+ for(j in c(c(1:L)[c(1:L)!=i])){
+ dmo <- dmo + sum((OM[,i]-OM[,j])²)/N
+ }
+ }
+ dmo <- dmo/(L*(L-1))
+ return(dmo)
+ }
> DM_GC <- DMO(OM=GC_Oracle)
> DM_GC
[1] 0.208
> KW(GC_Oracle)
[1] 0.104
> DM_GC*999/2000
[1] 0.104
再次,我们没有看到在集成中表现出太多的多样性。现在我们将继续探讨集成多样性的最终度量。
评分者间一致性度量
在对 Oracle 输出介绍的讨论中,我们展示了如何轻松地使用它来获取分类器的准确率。分类器准确率的平均值定义为平均个体分类准确率,并用
表示。评分者间一致性的度量由以下定义:

此度量与 Kohavi-Wolpert 度量相关,如下所示:

通过以下代码块,我们可以理解前面关系的实现:
> Avg_Ensemble_Acc <- function(Oracle){
+ return(mean(colSums(GC_Oracle)/nrow(GC_Oracle)))
+ }
> Avg_Ensemble_Acc(GC_Oracle)
[1] 0.872
> Kappa <- function(Oracle){
+ pbar <- Avg_Ensemble_Acc(Oracle)
+ AvgL <- 0
+ N <- nrow(Oracle); L <- ncol(Oracle)
+ for(i in 1:N){
+ lz <- sum(Oracle[i,])
+ AvgL <- AvgL + lz*(L-lz)
+ }
+ Avgl <- AvgL/L
+ kappa <- 1-Avgl/(N*(L-1)*pbar*(1-pbar))
+ return(kappa)
+ }
> Kappa(GC_Oracle)
[1] 0.0657
> 1-DM_GC/(2*Avg_Ensemble_Acc(GC_Oracle)*(1-
+ Avg_Ensemble_Acc(GC_Oracle)))
[1] 0.0657
这就结束了我们对集成一致性的讨论。
摘要
集成方法被发现对于分类、回归和其他相关问题是十分有效的。任何统计和机器学习方法都必须始终伴随着适当的诊断。所有基础模型之间相互独立这一假设是集成方法成功的关键。然而,这种独立性条件很少得到满足,尤其是因为基础模型是建立在相同的数据集之上的。我们以最简单的度量方法——鹅对法——开始了这一章节。通过这种方法,我们实际上是在寻找在所有时刻都达成一致的模型。如果这样的模型存在于集成中,那么移除其中之一会更安全。在拥有大量数据集和高数量变量的情况下,确实可能没有任何基础模型与其他模型使用相同的语言。然而,我们仍然需要检查它们是否相等。考虑到这一点,我们首先提出了仅比较两个基础模型的措施。不同的措施可能导致相互矛盾的结论。然而,这通常并不是问题。随后,我们将成对比较的概念扩展到了整个集成基础模型。虽然我们发现我们的基础模型并不太多样化,但在此也要注意,大多数值都远离边界值 0。当我们对集成进行诊断并发现值等于零时,那么很明显,基础模型并没有提供任何形式的多样性。在下一章中,我们将探讨回归数据的专门主题。
第九章. 集成回归模型
第三章, 袋装, 到 第八章, 集成诊断,都致力于学习不同类型的集成方法。讨论主要基于分类问题。如果监督学习问题的回归变量/输出是一个数值变量,那么我们就有了一个回归问题,这将在本章中解决。为了演示目的,本章选择了房价问题,数据集来自 Kaggle 竞赛:www.kaggle.com/c/house-prices-advanced-regression-techniques/。数据包括许多变量,包括多达 79 个独立变量,房价作为输出/依赖变量。数据集需要一些预处理,因为一些变量有缺失日期,一些变量有大量级别,其中一些变量只出现得非常少,还有一些变量在超过 20%的观测值中缺失数据。
预处理技术将被变量减少方法所继替,然后我们将拟合重要的回归模型:线性回归、神经网络和回归树。首先将提供回归树的集成扩展,然后我们将应用袋装和随机森林方法。将使用各种提升方法来提高预测。在结论部分将应用堆叠集成方法。
在本章中,我们将涵盖以下内容:
-
数据预处理和可视化
-
变量减少技术
-
回归模型
-
回归数据的袋装和随机森林
-
提升回归模型
-
回归数据的堆叠集成方法
技术要求
我们将需要以下 R 包来完成本章内容:
-
adabag -
caret -
caretEnsemble -
ClustofVar -
FactoMinR -
gbm -
ipred -
missForest -
nnet -
NeuralNetTools -
plyr -
rpart -
RSADBE
预处理房价数据
数据集是从www.kaggle.com选择的,项目的标题是房价:高级回归技术。我们将使用的主要文件是test.csv和train.csv,这些文件可在配套的捆绑包中找到。变量的描述可以在data_description.txt文件中找到。更详细的信息当然可以在www.kaggle.com/c/house-prices-advanced-regression-techniques/获得。训练数据集包含 1460 个观测值,而测试数据集包含 1459 个观测值。房产价格只在训练数据集中已知,在测试数据集中不可用。我们只将使用训练数据集进行模型开发。首先使用read.csv、dim、names和str函数将数据集加载到 R 会话中,并进行初步检查:
> housing_train <- read.csv("../Data/Housing/train.csv",
+ row.names = 1,na.strings = "NA",
+ stringsAsFactors = TRUE)
> housing_test <- read.csv("../Data/Housing/test.csv",
+ row.names = 1,na.strings = "NA",
+ stringsAsFactors = TRUE)
> dim(housing_train)
[1] 1460 80
> dim(housing_test)
[1] 1459 79
> names(housing_train)
[1] "MSSubClass" "MSZoning" "LotFrontage" "LotArea"
[5] "Street" "Alley" "LotShape" "LandContour"
[9] "Utilities" "LotConfig" "LandSlope" "Neighborhood"
[69] "X3SsnPorch" "ScreenPorch" "PoolArea" "PoolQC"
[73] "Fence" "MiscFeature" "MiscVal" "MoSold"
[77] "YrSold" "SaleType" "SaleCondition" "SalePrice"
> str(housing_train)
'data.frame': 1460 obs. of 80 variables:
$ MSSubClass : int 60 20 60 70 60 50 20 60 50 190 ...
$ MSZoning : Factor w/ 5 levels "C (all)","FV",..: 4 4 4 4 4 4 4 4 5 4 ...
$ LotFrontage : int 65 80 68 60 84 85 75 NA 51 50 ...
$ LotArea : int 8450 9600 11250 9550 14260 14115 10084 10382 6120 7420 ...
$ Street : Factor w/ 2 levels "Grvl","Pave": 2 2 2 2 2 2 2 2 2 2 ...
$ Alley : Factor w/ 2 levels "Grvl","Pave": NA NA NA NA NA NA NA NA NA NA ...
$ MiscFeature : Factor w/ 4 levels "Gar2","Othr",..: NA NA NA NA NA 3 NA 3 NA NA ...
$ MiscVal : int 0 0 0 0 0 700 0 350 0 0 ...
$ MoSold : int 2 5 9 2 12 10 8 11 4 1 ...
$ YrSold : int 2008 2007 2008 2006 2008 2009 2007 2009 2008 2008 ...
$ SaleType : Factor w/ 9 levels "COD","Con","ConLD",..: 9 9 9 9 9 9 9 9 9 9 ...
$ SaleCondition: Factor w/ 6 levels "Abnorml","AdjLand",..: 5 5 5 1 5 5 5 5 1 5 ...
$ SalePrice : int 208500 181500 223500 140000 250000 143000 307000 200000 129900 118000 ...
read.csv function enabled importing the data from the comma-separated values file. The size of the imported data frame is evaluated using the dim function, while names gives us the variable names as stored in the original file. The str function gives a quick preview of the variable types and also gives a few of the observations.
数据框的维度表示变量的数量和观测值的数量。所有变量的详细信息可以在data_description.txt文件中找到。可以看出,我们手头上的是一个综合性的数据集。现在,我们在read.csv导入函数中运行了na.strings = "NA"选项,并且很自然地,这暗示我们存在缺失数据。当训练数据和测试数据分区中都有缺失数据时,作者建议将分区的协变量合并,然后进一步检查。首先将协变量合并,然后我们找出每个变量的缺失观测值数量:
> housing <- rbind(housing_train[,1:79],housing_test)
> dim(housing)
[1] 2919 79
> sort(sapply(housing,function(x) sum(is.na(x))),dec=TRUE)
PoolQC MiscFeature Alley Fence FireplaceQu
2909 2814 2721 2348 1420
LotFrontage GarageYrBlt GarageFinish GarageQual GarageCond
486 159 159 159 159
GarageType BsmtCond BsmtExposure BsmtQual BsmtFinType2
157 82 82 81 80
BsmtFinType1 MasVnrType MasVnrArea MSZoning Utilities
79 24 23 4 2
BsmtFullBath BsmtHalfBath Functional Exterior1st Exterior2nd
2 2 2 1 1
BsmtFinSF1 BsmtFinSF2 BsmtUnfSF TotalBsmtSF Electrical
1 1 1 1 1
KitchenQual GarageCars GarageArea SaleType MSSubClass
1 1 1 1 0
LotArea Street LotShape LandContour LotConfig
0 0 0 0 0
OpenPorchSF EnclosedPorch X3SsnPorch ScreenPorch PoolArea
0 0 0 0 0
MiscVal MoSold YrSold SaleCondition
0 0 0 0
rbind函数将训练和测试数据集中的数据合并。is.na(x)代码检查x中每个元素的值是否存在,应用sum函数后告诉我们变量的缺失观测值数量。然后使用sapply函数对housing的每个变量应用此函数。使用带有dec=TRUE参数的sort函数按降序对变量的缺失观测值计数进行排序,因此它使我们能够在开始时找到缺失值最多的变量。
读者可能会想知道关于观察结果整理背后的理由。整理观察结果的直观推理是,虽然某些变量可能有缺失数据,在训练数据中比在测试数据中多,或者相反,重要的是整体缺失百分比不要超过观测值的某个特定阈值。尽管我们有缺失数据插补技术,但在缺失数据百分比过高时使用它们可能会使我们错过特征的重要模式。因此,我们任意选择限制变量,如果超过 10%的值缺失。如果任何变量的缺失百分比超过 10%,我们将避免进一步分析该变量。首先,我们识别出超过 10%的变量,然后从主数据框中移除它们。下面的 R 代码块给出了我们想要的结果:
> miss_variables <- names(which(sapply(housing,
+ function(x) sum(is.na(x)))>0.1*nrow(housing_train)))
> miss_variables
[1] "LotFrontage" "Alley" "FireplaceQu" "GarageType"
[5] "GarageYrBlt" "GarageFinish" "GarageQual" "GarageCond"
[9] "PoolQC" "Fence" "MiscFeature"
> length(miss_variables)
[1] 11
> housing[,miss_variables] <- NULL
> dim(housing)
[1] 2919 68
首先识别出超过 10%缺失观测值的变量,然后存储在miss_variables字符向量中,我们有 11 个变量符合这一标准。这些变量通过NULL赋值被消除。
接下来,我们找到因子变量的水平数(不同的)。我们定义了一个函数,find_df,它将找到因子变量的水平数。对于数值和整数变量,它将返回1。这个练习的目的很快就会变得清楚。find_df函数将在下一个代码块中创建:
> find_df <- function(x){
+ if(class(x)=="numeric") mdf <- 1
+ if(class(x)=="integer") mdf <- 1
+ if(class(x) =="factor") mdf <- length(levels(x))
+ if(class(x) =="character") mdf <- length(unique(x))
+ return(mdf)
+ }
> sapply(housing,find_df)
MSSubClass MSZoning LotArea Street LotShape
1 4 1 2 3
LandContour Utilities LotConfig LandSlope Neighborhood
2 3 4 2 25
Condition1 Condition2 BldgType HouseStyle OverallQual
3 2 3 4 1
X3SsnPorch ScreenPorch PoolArea MiscVal MoSold
1 1 1 1 1
YrSold SaleType SaleCondition
1 4 4
> dim(housing)
[1] 2919 68
我们需要检查67个变量,在消除11个具有超过 10%缺失观测值的变量之后。其中一些可能不是因子变量。find_df函数显示,对于因子变量,水平数从 2 到 25 不等。现在对于Condition2和Exterior1st变量出现了一个快速问题:
> round(table(housing$Condition2)/nrow(housing),2)
Artery Feedr Norm PosA PosN RRAe RRAn RRNn
0.00 0.00 0.99 0.00 0.00 0.00 0.00 0.00
> round(table(housing$Exterior1st)/nrow(housing),2)
AsbShng AsphShn BrkComm BrkFace CBlock CemntBd HdBoard ImStucc MetalSd
0.02 0.00 0.00 0.03 0.00 0.04 0.15 0.00 0.15
Plywood Stone Stucco VinylSd Wd Sdng WdShing
0.08 0.00 0.01 0.35 0.14 0.02
在许多实际问题中,似乎存在一些因子变量,它们的一些水平出现频率非常低。现在,如果我们测试/验证分区中有新的水平,我们就无法进行预测。从统计学的角度来看,我们遇到了一个技术问题:失去了太多的自由度。这里采取了一种基本的方法,我们只是简单地将所有观察结果汇总到Others这个总类别中。创建了一个Truncate_Factor函数,它有两个参数:x和alpha。x对象是要传递给函数的变量,而alpha是任何变量频率会被汇总到Others中的指定比例。
注意
如果因子变量在测试数据集中有新的水平,就没有分析方法是能够包含其影响的。因此,在我们有太多不常见水平的情况下,某些水平没有被包括在训练数据集中的可能性很高,预测结果将不会为测试观察结果提供输出。
现在,创建Truncate_Factor函数:
> Truncate_Factor <- function(x,alpha){
+ xc <- as.character(x); n <- length(x)
+ if(length(unique(x))<=20) {
+ critical <- n*alpha
+ xc[xc %in% names(which((prop.table(table(xc)))<alpha))] <- "Others"
+ }
+ xc <- as.factor(xc)
+ return(xc)
+ }
> for(i in 1:ncol(housing)){
+ if(any(class(housing[,i]) == c('character','factor')))
+ housing[,i] = Truncate_Factor(housing[,i],0.05)
+ }
> table(housing$Condition2)/nrow(housing)
Norm Others
0.99 0.01
> table(housing$Exterior1st)/nrow(housing)
HdBoard MetalSd Others Plywood VinylSd Wd Sdng
0.151 0.154 0.126 0.076 0.351 0.141
我们现在可以看到,“其他”级别出现的频率更高,如果我们随机创建分区,未知级别的出现问题很可能不会发生。
你可能还记得,我们之前已经消除了具有过多缺失观测值的变量。这并不意味着我们没有缺失数据,这一点可以很快地发现:
> sum(is.na(housing))
[1] 474
> prod(dim(housing))
[1] 198492
474个值不能被忽视。缺失数据插补是填充缺失值的重要方法。尽管 EM 算法是实现这一目标的一种流行方法,但我们将应用随机森林技术来模拟缺失观测值。missForest包在第四章中介绍,即随机森林,并使用一个示例来模拟缺失值。我们将应用此函数到房价数据框上。由于此函数中默认选择的变量数是mtry=5,而我们房价中有 68 个变量,因此用于分割节点的变量数将更改为大约 p/3,因此在下一个 R 块中可以看到mtry=20的选项。在 8GB RAM 的机器上,下一行代码需要运行几个小时。接下来,我们将应用missForest函数,保存插补对象以供将来参考,并使用插补值创建测试和训练数据集:
> housing_impute <- missForest(housing,maxiter = 10,ntree=500,mtry=20)
missForest iteration 1 in progress...done!
missForest iteration 2 in progress...done!
missForest iteration 3 in progress...done!
missForest iteration 4 in progress...done!
missForest iteration 5 in progress...done!
missForest iteration 6 in progress...done!
missForest iteration 7 in progress...done!
There were 14 warnings (use warnings() to see them)
> save(housing_impute,file=
+ '../Data/Housing/housing_covariates_impute.Rdata')
> ht_imp <- cbind(housing_impute$ximp[1:nrow(housing_train),],
+ housing_train$SalePrice)
> save(ht_imp,file='../Data/Housing/ht_imp.Rdata')
> htest_imp <- housing_impute$ximp[(nrow(housing_train)+1):nrow(
+ housing),]
> save(htest_imp,file='../Data/Housing/htest_imp.Rdata')
读者当然应该在他们的本地机器上运行missForest代码行。然而,为了节省时间,读者也可以跳过这一行,然后从代码包中加载ht_imp和htest_imp对象。下一节将展示一种可视化大数据集和两种数据降维方法的方式。
可视化和变量降维
在上一节中,房价数据经历了大量的分析预处理,我们现在准备进一步分析它。首先,我们从可视化开始。由于我们有大量的变量,R 可视化设备上的可视化稍微有些困难。正如前几章所见,为了可视化随机森林和其他大型、复杂结构,我们将初始化一个 PDF 设备并将图表存储在其中。在房价数据集中,主要变量是房价,因此我们将首先将输出变量命名为SalePrice。我们需要以方便展示众多变量与SalePrice之间的关系的方式来可视化数据。自变量可以是数值型或分类型。如果变量是数值型,散点图将表明变量与SalePrice回归量之间的关系。如果自变量是分类型/因子型,我们将对因子的每个级别可视化箱线图。pdf、plot和boxplot函数将有助于生成所需的图表:
> load("../Data/Housing/ht_imp_author.Rdata")
> names(ht_imp)[69] <- "SalePrice"
> SP <- ht_imp$SalePrice
> pdf("../Output/Visualizing_Housing_Data.pdf")
> for(i in 1:68){
+ if(class(ht_imp[,i])=="numeric") {
+ plot(ht_imp[,i],SP,xlab=names(ht_imp)[i],ylab="Sales Price")
+ title(paste("Scatter plot of Sales Price against ",
+ names(ht_imp)[i]))
+ }
+ if(class(ht_imp[,i])=="factor") {
+ boxplot(SP~ht_imp[,i],xlab=names(ht_imp)[i],ylab=
+ "Sales Price",notch=TRUE)
+ title(paste("Boxplot of Salesprice by ",names(ht_imp)[i]))
+ }
+ }
> dev.off()
null device
1
ht_imp对象是从ht_imp_author.Rdata文件中加载的。注意,如果你在自己的机器上运行missForest函数并在此文件上工作,那么结果将与ht_imp_author.Rdata不同。pdf函数已知可以启动一个同名文件,如之前多次所见。对于数值变量,检查if条件,并显示散点图,其中xlab使用变量的实际名称作为x轴上的标签名称。title函数将paste函数的输出贴上,而paste函数确保我们有一个适合生成的图的标题。对于因子变量,也测试了类似的条件。我们现在将查看一些有趣的图表。SalePrice与MSSubClass的第一个图表(见Visualizing_Housing_Data.pdf文件)如下:

图 1:销售价格与 MSSubClass 的散点图
注意
注意,尽管我们指定了MSSubClass变量为数值变量,但散点图并没有给出同样的印象。在这里,MSSubClass变量的值围绕一个特定的点杂乱无章,然后尺度跳到下一个值。
简而言之,它似乎不是一个连续变量,这可以通过以下方式轻松验证:
> table(ht_imp$MSSubClass)
20 30 40 45 50 60 70 75 80 85 90 120 160 180 190
536 69 4 12 144 299 60 16 58 20 52 87 63 10 30
练习:读者应将MSSubClass变量转换为因子,然后应用Truncate_Factor以减少噪声。在Visualizing_Housing_Data.pdf文件中识别其他表现出此特性的数值变量。
现在让我们看看MSZoning因子变量的箱线图:

图 2:MSZoning 三个级别的销售价格箱线图
超出胡须的点表示存在异常值。然而,对于复杂问题,解释也可能非常错误。缺口是箱线图显示中的有用技巧。如果两个变量级别的缺口不重叠,则表示级别是显著的,因此信息是有用的,如SalePrice与MSZoning级别箱线图的显示所示。
接下来展示的是SalePrice与LotArea的散点图:

图 3:销售价格与 Lot Area 的散点图
明显地,散点图显示这两个变量SalePrice和LotArea之间没有有意义的关系。在下面的图中,SalePrice与TotalBsmtSF之间可以看到不同类型的显示:

图 4:销售价格与 TotalBsmtSF 的散点图
我们可以清楚地看到图的最右侧TotalBsmtSF值存在一个异常值。同时,TotalBsmtSF在 0 值处也存在值的杂乱,这可能是其他某个变量的控制结果。或者,可能会发现该变量存在零膨胀,因此它可能是一个混合变量。同样,所有其他图表也可以进行解释。接下来获得SalePrice与其他数值变量之间的相关性:
> cor(ht_imp[sapply(ht_imp,is.numeric)])[,1]
MSSubClass LotArea OverallQual OverallCond YearBuilt
1.0000 -0.1398 0.0326 -0.0593 0.0279
YearRemodAdd MasVnrArea BsmtFinSF1 BsmtFinSF2 BsmtUnfSF
0.0406 0.0206 -0.0698 -0.0656 -0.1408
TotalBsmtSF X1stFlrSF X2ndFlrSF LowQualFinSF GrLivArea
-0.2385 -0.2518 0.3079 0.0465 0.0749
BsmtFullBath BsmtHalfBath FullBath HalfBath BedroomAbvGr
0.0035 -0.0023 0.1316 0.1774 -0.0234
KitchenAbvGr TotRmsAbvGrd Fireplaces GarageCars GarageArea
0.2817 0.0404 -0.0456 -0.0401 -0.0987
WoodDeckSF OpenPorchSF EnclosedPorch X3SsnPorch ScreenPorch
-0.0126 -0.0061 -0.0120 -0.0438 -0.0260
PoolArea MiscVal MoSold YrSold SalePrice
0.0083 -0.0077 -0.0136 -0.0214 -0.0843
练习:解释Visualizing_Housing_Data.pdf文件中的所有关系,并按照其绝对值在前面 R 代码中对相关性进行排序。
我们使用了感兴趣的变量进行可视化,这进而导致了有价值的见解。正如之前所述,p = 68意味着有很多协变量/独立变量。在大数据中,复杂性将在北方方向增加,而且众所周知,对于许多实际应用,我们拥有数千个独立变量。虽然大多数可视化技术都有洞察力,但一个缺点是我们很少能深入了解高阶关系。例如,当涉及到三个或更多变量时,在图形显示中很少能充分展现它们之间的关系。因此,部署那些在不牺牲信息的情况下减少变量数量的方法是重要的。这里将要讨论的两种数据降维方法是主成分分析和变量聚类。
主成分分析(PCA)是从多元统计的更广泛领域抽取的一种方法。在数据降维方面,这种方法很有用,因为它在给定原始变量数量的情况下,试图用尽可能少的新变量来覆盖原始数据的大部分方差。这里简要介绍了 PCA。
假设我们有一个观测值的随机向量
。给定随机向量
,主成分分析(PCA)找到一个新的主成分向量
,使得每个 Yi 都是
的线性组合。此外,主成分满足 Y1 的方差高于 Y2 的方差,且两者不相关;Y2 的方差高于 Y3 和 Y1 的方差,Y2 和 Y3 不相关,以此类推。这与
有关,它们之间都不相关。主成分被设置成使得
的大部分方差累积在前几个主成分中(关于此信息的更多信息,请参阅 Tattar 等人(2016)的第十五章)。因此,我们可以实现大量的数据降维。然而,PCA 的基本前提是
是一个连续随机变量的向量。在我们的数据集中,我们也有因子变量。因此,我们不能为了我们的目的使用 PCA。一种粗略的方法是忽略因子变量,并在连续变量上简单地执行数据降维。相反,我们会使用 混合数据的因子分析,并且 FactoMineR 软件包中提供了执行此操作的软件功能。
由于数据降维只需要在协变量上执行,而我们没有纵向数据,因此数据降维应用于所有可用的观测值集,而不仅仅是训练数据集。在整份数据集上执行数据降维的理由与截断因子变量级别的数量相同。housing_impute 数据框可在 housing_covariates_impute.Rdata 中找到。我们首先将其加载,然后应用 FAMD 函数来执行混合数据的因子分析:
> load("../Data/Housing/housing_covariates_impute.Rdata")
> housing_covariates <- housing_impute$ximp
> housing_cov_famd <- FAMD(housing_covariates,ncp=68,graph=FALSE)
> colnames(housing_cov_famd$eig) <- c("Component","Variance",
+ "Cumulative")
> housing_cov_famd$eig
Component Variance Cumulative
comp 1 12.2267562274 9.3334017003 9.33340170
comp 2 5.4502085801 4.1604645650 13.49386627
comp 3 4.5547218487 3.4768869074 16.97075317
comp 4 4.0710151565 3.1076451576 20.07839833
comp 5 3.1669428163 2.4175136002 22.49591193
comp 6 2.8331129142 2.1626816139 24.65859354
comp 7 2.6471571767 2.0207306692 26.67932421
comp 8 2.1871762983 1.6696002277 28.34892444
comp 9 2.1563067109 1.6460356572 29.99496010
comp 10 2.0083000432 1.5330534681 31.52801357
comp 66 0.7691341212 0.5871252834 80.58667899
comp 67 0.7648033308 0.5838193365 81.17049833
comp 68 0.7559712365 0.5770772798 81.74757561
> windows(height=100,width=200)
> pareto.chart(housing_cov_famd$eig[,2])
在FAMD函数中,ncp选项被设置为 68,因为这是我们拥有的变量数量。我们还想看看主成分如何响应数据集。如果选择graph=TRUE选项,函数将显示相关图表。housing_cov_famd$eig的colnames被更改为默认名称,因为这些名称并不能公正地反映它生成的输出。我们可以从特征值分析中看到,总共 68 个成分并没有完全覆盖数据中可用的全部变异。此外,即使是解释了 50%变异的成分,我们也需要从中选择 26 个。因此,这里的变量减少似乎并不富有成效。然而,这并不意味着在下一组分析中性能会差。当使用质量控制包qcc中的pareto.chart函数对频率数据进行处理时,会得到一个帕累托图。正如百分比所展示的,很明显,如果我们需要原始变量中 90%的变异由主成分解释,那么我们需要近 60 个主成分。因此,减少的变量数量仅为 8,解释也增加了一个复杂性。这并不是好消息。然而,我们仍然会保存主成分的数据:
> save(housing_cov_famd,file='../Data/Housing/Housing_FAMD.Rdata')
> Housing_FAMD_Data <- housing_cov_famd$ind$coord
> save(Housing_FAMD_Data,file='../Data/Housing/
+ Housing_FAMD_Data.Rdata')

图 5:主成分贡献的帕累托图
练习:探索使用来自 R 包PCAmix的PCAmix函数,通过主成分分析来减少变量的数量。
变量聚类
变量可以像我们对观测值那样进行分组。为了实现这一点,我们将使用来自ClustOfVar包的kmeansvar函数。变量聚类包需要分别指定定量(数值)变量,定性(因子)变量也需要分别指定。此外,我们还需要指定需要多少个变量簇。init选项有助于在这里进行指定。is.numeric和is.factor函数用于识别数值和因子变量,并设置变量簇:
> Housing_VarClust <- kmeansvar(
+ X.quanti = housing_covariates[sapply(housing_covariates,
+ is.numeric)],
+ X.quali = housing_covariates[sapply(housing_covariates,
+ is.factor)],init=4)
Error: Some categorical variables have same names of categories,
rename categories or use the option rename.level=TRUE to rename it automatically
哎呀!这是一个错误。重要的是要记住,所有不常见的因子变量级别都被标记为“其他”。可能存在其他级别在多个变量中具有相同的名称,这在调查数据中是一个非常常见的标签选择,包括如“非常不满意 < 不满意 < 一般 < 好 < 优秀”这样的选项。这种变量级别的选择可以在多个问题中相同。然而,我们需要所有变量的级别名称在所有变量中都是独特的。手动重命名标签将是徒劳的,并且是时间上的极大浪费。因此,我们将使用一组将在变量中唯一的名称来解决这个问题,即变量名称本身。变量名称将与变量级别连接,因此我们将在整个变量中拥有独特的因子级别。使用paste0函数和plyr包中的mapvalues,我们首先执行级别重命名操作,然后再次应用kmeansvar:
> hc2 <- housing_covariates
> for(i in 1:ncol(hc2)){
+ if(class(hc2[,i])=="factor") {
+ hc2[,i] <- mapvalues(hc2[,i],from=levels(hc2[,i]),
+ to=paste0(names(hc2)[i],"_",levels(hc2[,i])))
+ }
+ }
> Housing_VarClust <- kmeansvar(
+ X.quanti = hc2[sapply(hc2,is.numeric)],
+ X.quali = hc2[sapply(hc2,is.factor)], init=4)
> Housing_VarClust$cluster
MSSubClass LotArea OverallQual OverallCond YearBuilt
2 1 1 4 4
YearRemodAdd MasVnrArea BsmtFinSF1 BsmtFinSF2 BsmtUnfSF
4 3 1 2 4
BsmtCond BsmtExposure BsmtFinType1 BsmtFinType2 Heating
3 1 4 2 3
HeatingQC CentralAir Electrical KitchenQual Functional
4 1 4 4 4
PavedDrive SaleType SaleCondition
1 4 4
> summary(Housing_VarClust)
Call:
kmeansvar(X.quanti = hc2[sapply(hc2, is.numeric)], X.quali = hc2[sapply(hc2, is.factor)], init = 4)
number of iterations: 2
Data:
number of observations: 2919
number of variables: 68
number of numerical variables: 34
number of categorical variables: 34
number of clusters: 4
Cluster 1 :
squared loading correlation
X1stFlrSF 0.6059 0.778
TotalBsmtSF 0.5913 0.769
OverallQual 0.5676 0.753
PoolArea 0.0166 0.129
MiscVal 0.0059 0.077
MoSold 0.0024 0.049
Cluster 2 :
squared loading correlation
X2ndFlrSF 0.8584 -0.927
HouseStyle 0.7734 NA
TotRmsAbvGrd 0.5185 -0.720
BsmtFinType2 0.0490 NA
BsmtFinSF2 0.0408 0.202
X3SsnPorch 0.0039 0.063
Cluster 3 :
squared loading correlation
MasVnrType 0.83189 NA
MasVnrArea 0.82585 -0.909
Heating 0.03532 NA
BsmtCond 0.02681 NA
Utilities 0.00763 NA
YrSold 0.00084 0.029
Cluster 4 :
squared loading correlation
Neighborhood 0.7955 NA
YearBuilt 0.7314 -0.855
BsmtQual 0.6792 NA
BsmtHalfBath 0.0087 0.093
Street 0.0041 NA
Condition2 0.0015 NA
Gain in cohesion (in %): 11.56
重要的问题是,尽管我们已经将变量分组标记,但我们如何使用它们?答案是每个组内变量的系数。要显示系数,请在clustvar对象Housing_VarClust旁边运行$coef:
> Housing_VarClust$coef
$cluster1
[,1]
const -7.1e+00
LotArea 2.1e-05
OverallQual 2.2e-01
CentralAir_N -5.3e-01
CentralAir_Y 3.8e-02
PavedDrive_N -5.2e-01
PavedDrive_Others -2.8e-01
PavedDrive_Y 5.0e-02
$cluster2
[,1]
const 3.79789
MSSubClass -0.00472
BsmtFinSF2 0.00066
HouseStyle_1.5Fin -0.11967
HouseStyle_1Story 0.41892
HouseStyle_2Story -0.69610
HouseStyle_Others 0.10816
BsmtFinType2_Others 0.33286
BsmtFinType2_Unf -0.04491
$cluster3
[,1]
const -33.1748
MasVnrArea -0.0039
YrSold 0.0167
BsmtCond_TA -0.0365
Heating_GasA -0.0179
Heating_Others 1.1425
$cluster4
[,1]
const 45.30644
OverallCond 0.09221
YearBuilt -0.01009
SaleCondition_Normal 0.03647
SaleCondition_Others 0.20598
SaleCondition_Partial -0.58877
现在,对于数据中的观测值,相应的变量将与变量聚类的系数相乘,以获得该变量聚类的单个向量。因此,我们将 68 个变量减少到 4 个变量。
练习:使用之前显示的系数,获取housing_covariates数据框的聚类变量。
房地产问题的数据预处理现在已完成。在下一节中,我们将为回归数据构建基础学习器。
回归模型
弗朗西斯·高尔顿爵士在十九世纪末发明了简单的线性回归模型。所用的例子是研究父母的身高如何影响孩子的身高。这项研究使用了数据,并奠定了回归分析的基础。父母和孩子的身高之间的相关性是众所周知的,高尔顿利用 928 对身高测量数据,开发了线性回归模型。然而,在伽尔顿正式发明之前,这种方法可能已经在非正式地使用了。简单的线性回归模型由一个单一输入(自变量)和一个单一输出组成。
在这种监督学习方法中,目标变量/输出/因变量是一个连续变量,它也可以取区间值,包括非负数和实数。输入/自变量没有限制,因此它可以取数值、分类或其他我们之前用于分类问题的任何形式。有趣的是,线性回归模型的出现时间比分类回归模型(如逻辑回归模型)要早得多。机器学习问题更常基于分类问题进行概念化,而集成方法,特别是提升方法,是通过将分类作为动机来开发的。主要原因在于误差改进提供了良好的直觉,次要原因可能是因为著名的机器学习示例,如数字识别、垃圾邮件分类等。
简单线性回归的扩展是多元线性回归,其中我们允许有多个独立变量。我们将完全放弃简单和多元回归的惯例,并简单地遵循回归。作为基学习器,线性回归模型首先被介绍。有趣的数据库将被用来启动线性回归模型。
线性回归模型
更正式地说,设
为包含p个独立变量的集合,而Y是感兴趣的变量。我们需要从回归变量
的角度来理解回归变量Y。线性回归模型由以下公式给出:

Y 与回归变量之间的关系是线性的;
是截距项;
是回归系数;而
是误差项。需要指出的是,这里的线性是指回归系数的线性。同时,需要注意的是,回归变量可以采取任何形式,有时也可以被看作是其他形式,包括对数、指数和二次形式。误差项
通常假设服从具有未知方差和零均值的正态分布。关于线性回归模型的更多细节,可以参考 Draper 和 Smith (1999)、Chatterjee 和 Hadi (2012)以及 Montgomery 等人(2005)的著作。关于使用 R 软件实现此技术的信息,请参阅 Tattar 等人(2016)的第十二章或 Tattar(2017)的第六章。
首先,我们将使用 Galton 数据集来解释线性回归模型的核心概念。数据是从RSADBE包中加载的,使用lm函数,我们可以构建模型:
> data(galton)
> cor(galton)
child parent
child 1.00 0.46
parent 0.46 1.00
> plot(galton)
> head(galton)
child parent
1 62 70
2 62 68
3 62 66
4 62 64
5 62 64
6 62 68
> cp_lm <- lm(child~parent,data=galton)
> summary(cp_lm)
Call:
lm(formula = child ~ parent, data = galton)
Residuals:
Min 1Q Median 3Q Max
-7.805 -1.366 0.049 1.634 5.926
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 23.9415 2.8109 8.52 <2e-16 ***
parent 0.6463 0.0411 15.71 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 2.2 on 926 degrees of freedom
Multiple R-squared: 0.21, Adjusted R-squared: 0.21
F-statistic: 247 on 1 and 926 DF, p-value: <2e-16

图 6:儿童身高与父母身高对比 - 散点图
这段代码块告诉我们什么?首先,我们将从 RSADBE 包中加载 galton 数据,然后查看父母和孩子的cor相关系数。相关系数是0.46,这似乎是一个强烈的正相关。散点图也表明了这种正相关,因此我们继续构建以父母高度为函数的孩子的线性回归模型。建议首先查看与模型相关的 p 值,在这种情况下,它在summary(cp_lm)的最后行给出,为<2e-16。较小的 p 值意味着我们拒绝模型无意义的零假设,因此当前的拟合模型是有用的。与截距项和变量项相关的 p 值都是<2e-16,这也意味着这些项是显著的。回归系数0.6463意味着如果父母身高增加一英寸,孩子的身高将增加回归系数的量级。
Multiple R-squared(技术上简单的 R 平方)和Adjusted R-squared的值都是0.21,这是预期的,因为我们模型中只有一个变量。R 平方的解释是,如果我们将其乘以 100(在这种情况下是 21%),那么得到的数字就是数据(孩子的高度)变化中由拟合值解释的百分比。这个指标的值越高,模型就越好。在这个例子中,这意味着父母的高度只能解释孩子身高变化的约 21%。这意味着我们需要考虑其他变量。在这种情况下,一个起点可能是考虑父母双方的高度。如果你添加更多的变量,多重 R 平方值将会持续增加,因此更倾向于使用更稳健的调整 R 平方值。是否有可能获得完美的 R 平方,例如 1 或 100%?
一个名为Mantel的数据集在捆绑包中在线可用,我们将构建一个线性回归模型来检查其 R 平方。为此,我们导入数据集并对其运行lm函数:
> Mantel <- read.csv("../Data/Mantel.csv")
> Mantel
Y X1 X2 X3
1 5 1 1004 6.0
2 6 200 806 7.3
3 8 -50 1058 11.0
4 9 909 100 13.0
5 11 506 505 13.1
> Mantel_lm <- lm(Y~.,data=Mantel)
> summary(Mantel_lm)
Call:
lm(formula = Y ~ ., data = Mantel)
Residuals:
1 2 3 4 5
-2.49e-13 2.92e-13 3.73e-14 -3.89e-14 -4.14e-14
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -1.00e+03 2.73e-10 -3.67e+12 1.7e-13 ***
X1 1.00e+00 2.73e-13 3.67e+12 1.7e-13 ***
X2 1.00e+00 2.73e-13 3.67e+12 1.7e-13 ***
X3 1.33e-14 2.16e-13 6.00e-02 0.96
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 3.9e-13 on 1 degrees of freedom
Multiple R-squared: 1, Adjusted R-squared: 1
F-statistic: 4.99e+25 on 3 and 1 DF, p-value: 1.04e-13
在这里,我们可以看到 R 平方是完美的。在我们开始分析房价数据的严肃任务之前,让我们先玩一会儿吧。
对于galton数据集,我们将添加一个新的变量,称为frankenstein,这个变量将是拟合模型cp_lm的残差。将创建一个新的数据集,它将使用残差来增强galton数据集;然后,将使用lm函数拟合线性模型,并检查其 R 平方:
> d2 <- cbind(galton,residuals(cp_lm))
> names(d2)
[1] "child" "parent" "residuals(cp_lm)"
> names(d2) <- c("child","parent","frankenstein")
> cpf_lm <- lm(child~.,d2)
> summary(cpf_lm)
Call:
lm(formula = child ~ ., data = d2)
Residuals:
Min 1Q Median 3Q Max
-2.60e-15 -7.40e-16 -3.00e-16 2.10e-16 1.02e-13
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 2.39e+01 5.74e-15 4.17e+15 <2e-16 ***
parent 6.46e-01 8.40e-17 7.69e+15 <2e-16 ***
frankenstein 1.00e+00 6.71e-17 1.49e+16 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 4.6e-15 on 925 degrees of freedom
Multiple R-squared: 1, Adjusted R-squared: 1
F-statistic: 1.41e+32 on 2 and 925 DF, p-value: <2e-16
Warning message:
In summary.lm(cpf_lm) : essentially perfect fit: summary may be unreliable
不要忽略警告函数。你可能还记得,对于Mantel数据集没有显示这样的警告函数。这是因为通过向frankenstein变量添加一点噪声,可以消除这个警告,从而使它更加可怕:
> d2$frankenstein <- jitter(d2$frankenstein)
> summary(lm(child~.,d2))
Call:
lm(formula = child ~ ., data = d2)
Residuals:
Min 1Q Median 3Q Max
-0.004072 -0.002052 0.000009 0.001962 0.004121
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 2.39e+01 2.92e-03 8210 <2e-16 ***
parent 6.46e-01 4.27e-05 15143 <2e-16 ***
frankenstein 1.00e+00 3.41e-05 29331 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.0023 on 925 degrees of freedom
Multiple R-squared: 1, Adjusted R-squared: 1
F-statistic: 5.45e+08 on 2 and 925 DF, p-value: <2e-16
因此,我们已经掌握了获得完美 R 平方的艺术。玩耍的时间已经结束,让我们继续到住房数据集。我们之前已经将住房数据集保存为训练和测试块,分别命名为ht_imp.Rdata和htest_imp.Rdata文件。作者将文件名版本修改为_author,以便更清晰地表示。然后,我们将训练块分为训练和测试两部分。然后,我们使用load函数导入数据,使用sample函数对其进行分区,然后使用lm函数构建回归模型:
> load("../Data/Housing/ht_imp_author.Rdata")
> load("../Data/Housing/htest_imp_author.Rdata")
> ls()
[1] "ht_imp" "htest_imp"
> Y <- "SalePrice"
> X <- names(ht_imp)[-69]
> set.seed(12345)
> BV <- sample(c("Build","Validate"),nrow(ht_imp),replace = TRUE,
+ prob=c(0.7,0.3))
> HT_Build <- ht_imp[BV=="Build",]
> HT_Validate <- ht_imp[BV=="Validate",]
> HT_Formula <- as.formula("SalePrice~.")
> HT_LM_01 <- lm(HT_Formula,data=HT_Build)
> summary(HT_LM_01)
Call:
lm(formula = HT_Formula, data = HT_Build)
Residuals:
Min 1Q Median 3Q Max
-268498 -12222 -409 11351 240990
Coefficients: (2 not defined because of singularities)
Estimate Std. Error t value Pr(>|t|)
(Intercept) -2.87e+03 1.53e+06 0.00 0.99850
MSSubClass -1.52e+02 7.95e+01 -1.91 0.05583 .
MSZoningRL 8.55e+03 6.27e+03 1.36 0.17317
MSZoningRM 1.20e+04 7.50e+03 1.60 0.11011
LotArea 4.90e-01 1.21e-01 4.04 5.8e-05 ***
StreetPave 2.81e+04 1.70e+04 1.65 0.09979 .
LotShapeOthers -3.59e+03 6.12e+03 -0.59 0.55733
LotShapeReg 1.25e+03 2.40e+03 0.52 0.60111
LandContourOthers -1.22e+04 3.99e+03 -3.05 0.00236 **
UtilitiesOthers -5.76e+04 3.25e+04 -1.77 0.07637 .
LotConfigCulDSac 1.21e+04 4.96e+03 2.44 0.01477 *
LotConfigInside -1.62e+03 2.58e+03 -0.63 0.52972
LotConfigOthers -1.28e+04 5.57e+03 -2.30 0.02144 *
EnclosedPorch 6.95e+00 1.91e+01 0.36 0.71628
X3SsnPorch 3.81e+01 3.87e+01 0.98 0.32497
ScreenPorch 3.78e+01 2.01e+01 1.88 0.05988 .
PoolArea 5.13e+01 2.60e+01 1.98 0.04842 *
MiscVal 5.13e-02 6.57e+00 0.01 0.99377
MoSold -4.38e+02 3.67e+02 -1.19 0.23313
YrSold -1.01e+02 7.53e+02 -0.13 0.89376
SaleTypeOthers -4.88e+04 2.19e+04 -2.23 0.02598 *
SaleTypeWD -5.10e+04 2.20e+04 -2.32 0.02061 *
SaleConditionNormal 1.93e+03 4.31e+03 0.45 0.65421
SaleConditionOthers 1.87e+03 7.42e+03 0.25 0.80168
SaleConditionPartial -3.21e+04 2.21e+04 -1.45 0.14641
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 28400 on 861 degrees of freedom
Multiple R-squared: 0.884, Adjusted R-squared: 0.867
F-statistic: 51.1 on 129 and 861 DF, p-value: <2e-16
在拟合三个更多基础学习器之后,将进行拟合线性模型的准确性评估。调整后的 R 平方值约为 87%。然而,我们有 68 个变量,我们可以从之前的总结中的 p 值看到,许多变量没有 p 值小于 0.05 或 0.1。因此,我们需要消除不显著的变量。step函数可以应用于许多拟合的回归模型,以消除不显著的变量,同时保留模型的大部分特征。
在 R 会话中运行step函数会导致控制台中出现大量的输出。初始输出因空间限制而丢失。因此,作者使用 RStudio 中的从 R 脚本编译报告选项运行脚本,选择 MS Word 作为报告输出格式,并保存该文件。以下是该文件结果的简略版:
## Start: AIC=20446.87
## SalePrice ~ MSSubClass + MSZoning + LotArea + Street + LotShape +
## LandContour + Utilities + LotConfig + LandSlope + Neighborhood +
## Condition1 + Condition2 + BldgType + HouseStyle + OverallQual +
## OverallCond + YearBuilt + YearRemodAdd + RoofStyle + RoofMatl +
## Exterior1st + Exterior2nd + MasVnrType + MasVnrArea + ExterQual +
## ExterCond + Foundation + BsmtQual + BsmtCond + BsmtExposure +
## BsmtFinType1 + BsmtFinSF1 + BsmtFinType2 + BsmtFinSF2 + BsmtUnfSF +
## TotalBsmtSF + Heating + HeatingQC + CentralAir + Electrical +
## X1stFlrSF + X2ndFlrSF + LowQualFinSF + GrLivArea + BsmtFullBath +
## BsmtHalfBath + FullBath + HalfBath + BedroomAbvGr + KitchenAbvGr +
## KitchenQual + TotRmsAbvGrd + Functional + Fireplaces + GarageCars +
## GarageArea + PavedDrive + WoodDeckSF + OpenPorchSF + EnclosedPorch +
## X3SsnPorch + ScreenPorch + PoolArea + MiscVal + MoSold +
## YrSold + SaleType + SaleCondition
##
##
## Step: AIC=20446.87
## SalePrice ~ MSSubClass + MSZoning + LotArea + Street + LotShape +
## LandContour + Utilities + LotConfig + LandSlope + Neighborhood +
## Condition1 + Condition2 + BldgType + HouseStyle + OverallQual +
## OverallCond + YearBuilt + YearRemodAdd + RoofStyle + RoofMatl +
## Exterior1st + Exterior2nd + MasVnrType + MasVnrArea + ExterQual +
## ExterCond + Foundation + BsmtQual + BsmtCond + BsmtExposure +
## BsmtFinType1 + BsmtFinSF1 + BsmtFinType2 + BsmtFinSF2 + BsmtUnfSF +
## TotalBsmtSF + Heating + HeatingQC + CentralAir + Electrical +
## X1stFlrSF + X2ndFlrSF + LowQualFinSF + BsmtFullBath + BsmtHalfBath +
## FullBath + HalfBath + BedroomAbvGr + KitchenAbvGr + KitchenQual +
## TotRmsAbvGrd + Functional + Fireplaces + GarageCars + GarageArea +
## PavedDrive + WoodDeckSF + OpenPorchSF + EnclosedPorch + X3SsnPorch +
## ScreenPorch + PoolArea + MiscVal + MoSold + YrSold + SaleType +
## SaleCondition
##
##
## Step: AIC=20446.87
## SalePrice ~ MSSubClass + MSZoning + LotArea + Street + LotShape +
## LandContour + Utilities + LotConfig + LandSlope + Neighborhood +
## Condition1 + Condition2 + BldgType + HouseStyle + OverallQual +
## OverallCond + YearBuilt + YearRemodAdd + RoofStyle + RoofMatl +
## Exterior1st + Exterior2nd + MasVnrType + MasVnrArea + ExterQual +
## ExterCond + Foundation + BsmtQual + BsmtCond + BsmtExposure +
## BsmtFinType1 + BsmtFinSF1 + BsmtFinType2 + BsmtFinSF2 + BsmtUnfSF +
## Heating + HeatingQC + CentralAir + Electrical + X1stFlrSF +
## X2ndFlrSF + LowQualFinSF + BsmtFullBath + BsmtHalfBath +
## FullBath + HalfBath + BedroomAbvGr + KitchenAbvGr + KitchenQual +
## TotRmsAbvGrd + Functional + Fireplaces + GarageCars + GarageArea +
## PavedDrive + WoodDeckSF + OpenPorchSF + EnclosedPorch + X3SsnPorch +
## ScreenPorch + PoolArea + MiscVal + MoSold + YrSold + SaleType +
## SaleCondition
##
## Df Sum of Sq RSS AIC
## - Exterior2nd 5 2.6926e+09 6.9890e+11 20441
## - HeatingQC 3 8.4960e+08 6.9706e+11 20442
## - MasVnrType 3 9.3578e+08 6.9714e+11 20442
## - OverallQual 1 3.2987e+10 7.2919e+11 20491
## - X2ndFlrSF 1 3.9790e+10 7.3600e+11 20500
## - Neighborhood 24 1.6770e+11 8.6391e+11 20613
##
## Step: AIC=20440.69
## SalePrice ~ MSSubClass + MSZoning + LotArea + Street + LotShape +
## LandContour + Utilities + LotConfig + LandSlope + Neighborhood +
## Condition1 + Condition2 + BldgType + HouseStyle + OverallQual +
## OverallCond + YearBuilt + YearRemodAdd + RoofStyle + RoofMatl +
## Exterior1st + MasVnrType + MasVnrArea + ExterQual + ExterCond +
## Foundation + BsmtQual + BsmtCond + BsmtExposure + BsmtFinType1 +
## BsmtFinSF1 + BsmtFinType2 + BsmtFinSF2 + BsmtUnfSF + Heating +
## HeatingQC + CentralAir + Electrical + X1stFlrSF + X2ndFlrSF +
## LowQualFinSF + BsmtFullBath + BsmtHalfBath + FullBath + HalfBath +
## BedroomAbvGr + KitchenAbvGr + KitchenQual + TotRmsAbvGrd +
## Functional + Fireplaces + GarageCars + GarageArea + PavedDrive +
## WoodDeckSF + OpenPorchSF + EnclosedPorch + X3SsnPorch + ScreenPorch +
## PoolArea + MiscVal + MoSold + YrSold + SaleType + SaleCondition
## Step: AIC=20386.81
## SalePrice ~ MSSubClass + LotArea + Street + LandContour + Utilities +
## LotConfig + LandSlope + Neighborhood + Condition1 + Condition2 +
## BldgType + HouseStyle + OverallQual + OverallCond + YearBuilt +
## RoofStyle + RoofMatl + Exterior1st + BsmtQual + BsmtCond +
## BsmtExposure + BsmtFinType1 + BsmtFinSF1 + BsmtFinType2 +
## X1stFlrSF + X2ndFlrSF + LowQualFinSF + BsmtFullBath + FullBath +
## HalfBath + KitchenAbvGr + KitchenQual + TotRmsAbvGrd + Functional +
## Fireplaces + GarageCars + WoodDeckSF + ScreenPorch + PoolArea +
## MoSold + SaleType
##
## Df Sum of Sq RSS AIC
## <none> 7.1467e+11 20387
## - KitchenAbvGr 1 1.4477e+09 7.1612e+11 20387
## - MoSold 1 1.6301e+09 7.1630e+11 20387
## - BldgType 2 3.1228e+09 7.1779e+11 20387
## - Utilities 1 1.7130e+09 7.1639e+11 20387
## - BsmtCond 1 1.7554e+09 7.1643e+11 20387
## - BsmtFinType2 1 1.8708e+09 7.1654e+11 20387
## - YearBuilt 1 2.0543e+09 7.1673e+11 20388
## - Street 1 2.1163e+09 7.1679e+11 20388
## - LowQualFinSF 1 2.1785e+09 7.1685e+11 20388
## - ScreenPorch 1 2.2387e+09 7.1691e+11 20388
## - MSSubClass 1 2.2823e+09 7.1695e+11 20388
## - LandSlope 1 2.5566e+09 7.1723e+11 20388
## - PoolArea 1 2.6036e+09 7.1728e+11 20388
## - Exterior1st 5 9.1221e+09 7.2379e+11 20389
## - Functional 1 3.4117e+09 7.1808e+11 20390
## - Condition1 2 4.9604e+09 7.1963e+11 20390
## - BsmtFinSF1 1 3.9442e+09 7.1862e+11 20390
## - Condition2 1 4.0659e+09 7.1874e+11 20390
## - RoofStyle 2 6.1817e+09 7.2085e+11 20391
## - HalfBath 1 5.3010e+09 7.1997e+11 20392
## - FullBath 1 5.4987e+09 7.2017e+11 20392
## - Fireplaces 1 6.0438e+09 7.2072e+11 20393
## - TotRmsAbvGrd 1 7.0166e+09 7.2169e+11 20395
## - LandContour 1 7.7036e+09 7.2238e+11 20395
## - WoodDeckSF 1 8.8947e+09 7.2357e+11 20397
## - LotConfig 3 1.2015e+10 7.2669e+11 20397
## - RoofMatl 1 9.0967e+09 7.2377e+11 20397
## - BsmtFullBath 1 9.4178e+09 7.2409e+11 20398
## - HouseStyle 3 1.2940e+10 7.2761e+11 20399
## - BsmtFinType1 5 1.7704e+10 7.3238e+11 20401
## - SaleType 2 1.5305e+10 7.2998e+11 20404
## - LotArea 1 1.4293e+10 7.2897e+11 20404
## - OverallCond 1 1.8131e+10 7.3280e+11 20410
## - BsmtQual 3 2.3916e+10 7.3859e+11 20413
## - X1stFlrSF 1 2.1106e+10 7.3578e+11 20414
## - BsmtExposure 3 2.8182e+10 7.4285e+11 20419
## - GarageCars 1 2.6886e+10 7.4156e+11 20421
## - KitchenQual 3 3.1267e+10 7.4594e+11 20423
## - OverallQual 1 3.7361e+10 7.5203e+11 20435
## - X2ndFlrSF 1 4.3546e+10 7.5822e+11 20443
## - Neighborhood 24 1.8921e+11 9.0389e+11 20572
model的总结如下:
> summary(HT_LM_Final)
Call:
lm(formula = SalePrice ~ MSSubClass + LotArea + Street + LandContour +
Utilities + LotConfig + LandSlope + Neighborhood + Condition1 +
Condition2 + BldgType + HouseStyle + OverallQual + OverallCond +
YearBuilt + RoofStyle + RoofMatl + Exterior1st + BsmtQual +
BsmtCond + BsmtExposure + BsmtFinType1 + BsmtFinSF1 + BsmtFinType2 +
X1stFlrSF + X2ndFlrSF + LowQualFinSF + BsmtFullBath + FullBath +
HalfBath + KitchenAbvGr + KitchenQual + TotRmsAbvGrd + Functional +
Fireplaces + GarageCars + WoodDeckSF + ScreenPorch + PoolArea +
MoSold + SaleType, data = HT_Build)
Residuals:
Min 1Q Median 3Q Max
-272899 -11717 -42 11228 235349
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -2.64e+05 1.78e+05 -1.48 0.13894
MSSubClass -1.27e+02 7.46e+01 -1.70 0.08965 .
LotArea 4.75e-01 1.12e-01 4.25 2.3e-05 ***
MoSold -4.99e+02 3.48e+02 -1.44 0.15136
SaleTypeOthers -1.69e+04 5.85e+03 -2.89 0.00396 **
SaleTypeWD -1.76e+04 4.00e+03 -4.40 1.2e-05 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 28100 on 904 degrees of freedom
Multiple R-squared: 0.881, Adjusted R-squared: 0.87
F-statistic: 78.1 on 86 and 904 DF, p-value: <2e-16
覆盖step函数的小模块可以在Housing_Step_LM.R文件中找到,使用 R Markdown 生成的输出保存在名为Housing_Step_LM.docx的文件中。step函数的输出超过四十三页,但我们不必检查每一步中省略的变量。只需说,已经消除了许多不显著的变量,而没有丢失模型的特征。验证分区的准确性评估将在稍后看到。接下来,我们将扩展线性回归模型到非线性模型,并解决神经网络问题。
练习:使用主成分和变量聚类变量构建线性回归模型。使用相关变量集的准确性(R 平方)是否提高了线性回归模型?
神经网络
神经网络架构在第一章的统计/机器学习模型部分中介绍,集成技术介绍。神经网络能够处理非线性关系,隐藏神经元数量的选择、传递函数的选择以及学习率(或衰减率)的选择在构建有用的回归模型时提供了很大的灵活性。Haykin (2009)和 Ripley (1996)提供了神经网络理论的两个详细解释。
我们已经探讨了神经网络在分类问题中的应用,并看到了堆叠集成模型的实际操作。对于回归模型,我们需要通过linout=TRUE选项告诉nnet函数输出/因变量是连续变量。在这里,我们将构建一个具有五个隐藏神经元的神经网络,size=5,并运行函数最多 100 次迭代,maxit=100:
> HT_NN <- nnet(HT_Formula,data=HT_Build,linout=TRUE,maxit=100,size=5)
# weights: 666
initial value 38535430702344.617187
final value 5951814083616.587891
converged
> summary(HT_NN)
a 131-5-1 network with 666 weights
options were - linear output units
b->h1 i1->h1 i2->h1 i3->h1 i4->h1 i5->h1 i6->h1 i7->h1
-1.0e-02 6.5e-01 -8.0e-02 4.6e-01 5.0e-02 -4.0e-02 3.9e-01 1.3e-01
i8->h1 i9->h1 i10->h1 i11->h1 i12->h1 i13->h1 i14->h1 i15->h1
2.1e-01 4.6e-01 1.9e-01 5.2e-01 -6.6e-01 3.2e-01 -3.0e-02 2.2e-01
i16->h1 i17->h1 i18->h1 i19->h1 i20->h1 i21->h1 i22->h1 i23->h1
-2.5e-01 -1.2e-01 3.3e-01 -2.8e-01 -4.6e-01 -3.8e-01 -4.1e-01 -3.2e-01
-4.0e-01 -2.9e-01 -5.1e-01 -2.6e-01 2.5e-01 -6.0e-01 1.0e-02 1.5e-01
i120->h5 i121->h5 i122->h5 i123->h5 i124->h5 i125->h5 i126->h5 i127->h5
3.7e-01 -2.0e-01 2.0e-01 1.0e-02 -3.3e-01 -2.4e-01 -1.9e-01 7.0e-01
i128->h5 i129->h5 i130->h5 i131->h5
-1.3e-01 -3.4e-01 -6.9e-01 -6.6e-01
b->o h1->o h2->o h3->o h4->o h5->o
6.3e+04 6.3e+04 6.3e+04 -9.1e+04 4.7e-01 -8.4e+03
注意,神经网络架构并不非常实用。然而,有时我们被要求展示我们所构建的内容。因此,我们将使用来自NeuralNetTools包的plotnet函数来生成网络。由于变量太多(本例中为 68 个),我们将绘图保存到Housing_NN.pdf PDF 文件中,读者可以打开并放大查看:
> pdf("../Output/Housing_NN.pdf",height = 25, width=60)
> plotnet(HT_NN) # very chaotic network
> dev.off()
RStudioGD
2
神经网络的预测将很快进行。
练习 1:使用不同的衰减选项构建神经网络;默认值为 0。在 0-0.2 的范围内变化衰减值,增量分别为 0.01、0.05 等。
练习 2:使用reltol值、衰减值以及这些变量的组合来改进神经网络拟合。
回归树
回归树构成了住房数据集的第三个基学习器,并为回归问题提供了决策树结构。决策树的自然优势也自然地转移到回归树上。如第三章中所述,Bagging,许多决策树的选项也适用于回归树。
我们将使用rpart库中的rpart函数和默认设置来构建回归树。使用绘图和文本函数,我们设置了回归树:
> HT_rtree <- rpart(HT_Formula,data=HT_Build)
> windows(height=100,width=100)
> plot(HT_rtree,uniform = TRUE); text(HT_rtree)
> HT_rtree$variable.importance
OverallQual Neighborhood YearBuilt ExterQual KitchenQual
3.2e+12 2.0e+12 1.7e+12 1.7e+12 1.4e+12
Foundation GarageCars GrLivArea GarageArea X1stFlrSF
1.3e+12 8.0e+11 6.9e+11 6.1e+11 3.8e+11
X2ndFlrSF TotalBsmtSF TotRmsAbvGrd BsmtQual MasVnrArea
3.8e+11 3.2e+11 2.7e+11 2.7e+11 1.8e+11
FullBath HalfBath HouseStyle BsmtFinSF1 YearRemodAdd
1.7e+11 1.3e+11 1.2e+11 1.1e+11 5.3e+10
MSZoning BsmtFinType1 BedroomAbvGr Exterior1st BsmtFullBath
4.6e+10 4.4e+10 4.0e+10 2.4e+10 1.1e+10
LotArea
5.7e+09

图 7:房屋销售价格的回归树
哪些变量在这里很重要?这个答案由变量重要性指标提供。我们从HT_rtree中提取变量重要性,条形长度最高的变量是所有变量中最重要的。现在我们将使用barplot函数对HT_rtree进行绘图:
> barplot(HT_rtree$variable.importance,las=2,yaxt="n")

图 8:住房模型回归树的变量重要性
练习:探索回归树的剪枝选项。
接下来,我们将查看三个基础学习器在验证数据集上的性能。
回归模型的预测
我们将房价训练数据集分为两个部分:训练和验证。现在我们将使用构建的模型并检查它们的性能如何。我们将通过查看 MAPE 指标(|实际-预测|/实际)来完成这项工作。使用带有newdata选项的predict函数,首先获得预测值,然后计算数据验证部分观测值的 MAPE:
> HT_LM_01_val_hat <- predict(HT_LM_01,newdata = HT_Validate[,-69])
Warning message:
In predict.lm(HT_LM_01, newdata = HT_Validate[, -69]) :
prediction from a rank-deficient fit may be misleading
> mean(abs(HT_LM_01_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.11
> HT_LM_Final_val_hat <- predict(HT_LM_Final,newdata = HT_Validate[,-69])
> mean(abs(HT_LM_Final_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.11
> HT_NN_val_hat <- predict(HT_NN,newdata = HT_Validate[,-69])
> mean(abs(HT_NN_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.37
> HT_rtree_val_hat <- predict(HT_rtree,newdata = HT_Validate[,-69])
> mean(abs(HT_rtree_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.17
线性回归模型HT_LM_01和最有效的线性模型(根据 AIC)HT_LM_Final都给出了相同的准确度(精确到两位数),这两个模型的 MAPE 为0.11。具有五个隐藏神经元的神经网络模型HT_NN的 MAPE 为0.37,这是一个不好的结果。这再次证实了众所周知的事实:复杂性并不一定意味着准确性。回归树HT_rtree的准确度为0.17。
预测的价格在以下程序中进行了可视化:
> windows(height = 100,width = 100)
> plot(HT_Validate$SalePrice,HT_LM_01_val_hat,col="blue",
+ xlab="Sales Price",ylab="Predicted Value")
> points(HT_Validate$SalePrice,HT_LM_Final_val_hat,col="green")
> points(HT_Validate$SalePrice,HT_NN_val_hat,col="red")
> points(HT_Validate$SalePrice,HT_rtree_val_hat,col="yellow")
> legend(x=6e+05,y=4e+05,lty=3,
+ legend=c("Linear","Best Linear","Neural Network","Regression Tree"),
+ col=c("blue","green","red","yellow"))

图 9:预测房价
现在我们已经设置了基础学习器,是时候构建它们的集成模型了。我们将基于决策树的同质基础学习器构建集成模型。
Bagging 和随机森林
第三章,Bagging,和第四章,随机森林,展示了如何提高基本决策树的稳定性和准确性。在本节中,我们将主要使用决策树作为基础学习器,并以与第三章和第四章中相同的方式创建树集成。
split函数是 Bagging 和随机森林算法在分类和回归树中的主要区别。因此,不出所料,我们可以继续使用与分类问题中使用的相同函数和包来处理回归问题。我们将首先使用ipred包中的bagging函数来为房价数据设置 Bagging 算法:
> housing_bagging <- bagging(formula = HT_Formula,data=ht_imp,nbagg=500,
+ coob=TRUE,keepX=TRUE)
> housing_bagging$err
[1] 35820
与第三章中的相同方式一样,Bagging 对象中的树可以被保存到 PDF 文件中:Bagging:
> pdf("../Output/Housing_Bagging.pdf")
> for(i in 1:500){
+ temp <- housing_bagging$mtrees[[i]]
+ plot(temp$btree)
+ text(temp$btree,use.n=TRUE)
+ }
> dev.off()
RStudioGD
2
由于 ipred 包没有直接给出变量重要性,而了解哪些变量重要始终是一个重要的衡量标准,因此我们运行了一个与 第三章 中使用的类似循环和程序,即 Bagging,以获取变量重要性图:
> VI <- data.frame(matrix(0,nrow=500,ncol=ncol(ht_imp)-1))
> vnames <- names(ht_imp)[-69]
> names(VI) <- vnames
> for(i in 1:500){
+ VI[i,] <- as.numeric(housing_bagging$mtrees[[i]]$btree$variable.importance[vnames])
+ }
> Bagging_VI <- colMeans(VI,na.rm = TRUE)
> Bagging_VI <- sort(Bagging_VI,dec=TRUE)
> barplot(Bagging_VI,las=2,yaxt="n")
> title("Variable Importance of Bagging")

图 10:住房数据 bagging 算法的变量重要性图
练习: 比较 图 10 和 图 8,以决定回归树中是否存在过拟合问题。
bagging 是否提高了预测性能?这是我们需要评估的重要标准。使用带有 newdata 选项的 predict 函数,我们再次计算 MAPE,如下所示:
> HT_bagging_val_hat <- predict(housing_bagging,newdata = HT_Validate[,-69])
> mean(abs(HT_bagging_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.13
简单回归树的 MAPE 为 17%,现在降至 13%。这引出了下一个练习。
练习: 使用 rpart.control 中的某些剪枝选项来提高 bagging 的性能。
在 bagging 之后,下一步是随机森林。我们将使用同名的 randomForest 函数。在这里,我们探索了 500 棵树来构建这个森林。对于回归数据,节点分裂时随机采样的协变量数量的默认设置是 mtry = p/3,其中 p 是协变量的数量。我们将使用默认选择。randomForest 函数用于设置树集成,然后使用在 第四章 中定义的 plot_rf 函数,即 随机森林,将森林的树保存到 PDF 文件中:
> housing_RF <- randomForest(formula=HT_Formula,data=ht_imp,ntree=500,
+ replace=TRUE,importance=TRUE)
> pdf("../Output/Housing_RF.pdf",height=100,width=500)
Error in pdf("../Output/Housing_RF.pdf", height = 100, width = 500) :
cannot open file '../Output/Housing_RF.pdf'
> plot_RF(housing_RF)
[1] 1
[1] 2
[1] 3
[1] 498
[1] 499
[1] 500
> dev.off()
null device
1
> windows(height=100,width=200)
> varImpPlot(housing_RF2)
下图给出了随机森林的变量重要性图:

图 11:住房数据的随机森林变量重要性
练习: 找出两个变量重要性图 %lncMSE 和 IncNodePurity 之间的差异。同时,比较随机森林的变量重要性图和 bagging 图,并对此进行评论。
我们的森林有多准确?使用 predict 函数,我们将得到答案:
> HT_RF_val_hat <- predict(housing_RF,newdata = HT_Validate[,-69])
> mean(abs(HT_RF_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.038
这真是太棒了,随机森林通过大幅降低 MAPE 从 0.17 到 0.038 显著提高了精度。这是迄今为止构建的所有模型中的绝对赢家。
练习: 尽管精度有所提高,但尝试基于剪枝树构建森林,并计算精度。
让我们看看提升如何改变树的性能。
提升回归模型
第五章《提升》介绍了当有感兴趣的分类变量时,提升法对树的提升方法。将提升法应用于回归问题需要大量的计算改变。更多信息,请参阅 Zemel 和 Pitassi(2001)的论文,papers.nips.cc/paper/1797-a-gradient-based-boosting-algorithm-for-regression-problems.pdf,或 Ridgeway 等人(1999)的论文,dimacs.rutgers.edu/Research/MMS/PAPERS/BNBR.pdf。
将使用gbm库中的gbm函数来提升由随机森林生成的弱学习器。我们生成了一千棵树,n.trees=1e3,并使用shrinkage因子为0.05,然后使用梯度提升算法对回归数据进行提升:
> housing_gbm <- gbm(formula=HT_Formula,data=HT_Build,distribution = "gaussian",
+ n.trees=1e3,shrinkage = 0.05,keep.data=TRUE,
+ interaction.depth=1,
+ cv.folds=3,n.cores = 1)
> summary(housing_gbm)
var rel.inf
OverallQual OverallQual 29.22608012
GrLivArea GrLivArea 18.85043432
Neighborhood Neighborhood 13.79949556
PoolArea PoolArea 0.00000000
MiscVal MiscVal 0.00000000
YrSold YrSold 0.00000000
本总结按降序给出了变量的重要性。可以通过gbm.perf函数查看提升法的性能,由于我们的目标始终是生成对新数据表现良好的技术,因此还叠加了以下出袋曲线:
> windows(height=100,width=200)
> par(mfrow=c(1,2))
> gbm.perf(housing_gbm,method="OOB",plot.it=TRUE,
+ oobag.curve = TRUE,overlay=TRUE)
[1] 135

图 12:住房数据的提升收敛
提升法在第137次迭代时收敛。接下来,我们查看提升法在验证数据上的性能:
> HT_gbm_val_hat <- predict(housing_gbm,newdata = HT_Validate[,-69])
Using 475 trees...
> mean(abs(HT_gbm_val_hat - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.11
MAPE 已从 17%降至 11%。然而,随机森林仍然是迄今为止最准确的模型。
回归模型的堆叠方法
线性回归模型、神经网络和回归树是这里将要堆叠的三个方法。我们将需要caret和caretEnsemble包来完成这项任务。堆叠集成方法已在第七章《一般集成技术》中详细介绍。首先,我们指定训练任务的控制参数,指定算法列表,并创建堆叠集成:
> control <- trainControl(method="repeatedcv", number=10, repeats=3,
+ savePredictions=TRUE, classProbs=TRUE)
> algorithmList <- c('lm', 'rpart')
> set.seed(12345)
> Emodels <- caretList(HT_Formula, data=HT_Build, trControl=control,
+ methodList=algorithmList,
+ tuneList=list(
+ nnet=caretModelSpec(method='nnet', trace=FALSE,
+ linout=TRUE)
+
+ )
+ )
There were 37 warnings (use warnings() to see them)
神经网络通过caretModelSpec指定。Emodels需要重新采样以进行进一步分析:
> Enresults <- resamples(Emodels)
> summary(Enresults)
Call:
summary.resamples(object = Enresults)
Models: nnet, lm, rpart
Number of resamples: 30
MAE
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
nnet 30462 43466 47098 47879 53335 58286 0
lm 16153 18878 20348 20138 21337 23865 0
rpart 30369 33946 35688 35921 37354 42437 0
RMSE
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
nnet 42598 66632 70197 69272 73089 85971 0
lm 22508 26137 29192 34347 39803 66875 0
rpart 38721 46508 50528 50980 55705 65337 0
Rsquared
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
nnet 0.0064 0.16 0.32 0.31 0.44 0.74 4
lm 0.4628 0.77 0.85 0.81 0.88 0.92 0
rpart 0.4805 0.55 0.57 0.58 0.61 0.69 0
> dotplot(Enresults)
接下来显示dotplot:

图 13:住房数据的 R-square、MAE 和 RMSE
我们可以从图 13中看到,三个模型的 R-square 相似,尽管三个模型的 MAE 和 RMSE 有显著差异。可以使用modelCor函数找到模型的相关性:
> modelCor(Enresults)
nnet lm rpart
nnet 1.000 0.033 -0.44
lm 0.033 1.000 0.29
rpart -0.441 0.288 1.00
我们现在将集成方法应用于验证数据:
> HT_Validate_Predictions <- rowMeans(predict(Emodels,newdata = HT_Validate))
Warning message:
In predict.lm(modelFit, newdata) :
prediction from a rank-deficient fit may be misleading
> mean(abs(HT_Validate_Predictions - HT_Validate$SalePrice)/HT_Validate$SalePrice)
[1] 0.16
注意
注意,神经网络的默认结果,我们没有指定隐藏层的大小。16%的 MAPE 并不理想,我们最好使用随机森林集成。
练习:对主成分和变量聚类数据进行堆叠集成方法的操作。
摘要
在本章中,我们扩展了书中早期学习的大多数模型和方法。本章从住房数据的详细示例开始,我们进行了可视化和预处理。主成分方法有助于减少数据,变量聚类方法也有助于完成同样的任务。随后介绍了线性回归模型、神经网络和回归树作为基学习器的方法。袋装、提升和随机森林算法是一些有助于改进模型的方法。这些方法基于同质集成方法。本章最后以堆叠集成方法为三个异质基学习器进行了总结。
下一章将讨论不同结构的有删失观测数据,这种数据被称为生存数据,它通常出现在临床试验的研究中。
第十章:集成生存模型
原始的胆汁性肝硬化数据在前两章中使用了 Jackknife 方法进行介绍。临床试验中的观察结果通常会受到截尾的影响,而 Jackknife 方法通过伪值的概念帮助完成不完整的观察。由于伪值很可能会相互依赖,广义估计方程框架使得在感兴趣的时间点估计十二个协变量的影响成为可能。伪值的概念和广义估计方程框架使得从业者更容易解释结果。然而,如果截尾观察的数量异常高,这种方法可能并不适用。此外,也 preferable to have statistical methods that preserve the incompleteness of the observations and yet make good use of them. 在常规回归框架中,可以将以时间为因变量,误差项遵循适当的生存分布的(线性)回归框架建立起来。然而,事实证明这种方法并不可靠,在许多情况下,它被认为是不可靠的,或者收敛根本不发生。在《回归模型与生命表》(www.stat.cmu.edu/~ryantibs/journalclub/cox_1972.pdf)中,Cox(1972)在提出比例风险模型时,在生存数据的回归建模方面取得了突破。那么,什么是风险模型?
本章将首先介绍核心生存分析概念,如风险率、累积风险函数和生存函数。还将讨论和通过 R 程序可视化一些参数生存模型。对于给定的数据,我们将研究如何通过非参数方法进行寿命分布的推断。然后,对于pbc数据集中的感兴趣事件时间,将展示生存函数和累积风险函数的估计。通过 logrank 检验对不同段落的pbc数据进行假设检验。回归模型将从参数回归模型的一个简单示例开始,以指数分布为例。众所周知,参数模型对于临床试验数据并不非常有效。这导致 Cox 比例风险回归模型的一个重要变体,即半参数模型,在这种模型中,基线风险率被完全未指定,协变量的影响通过风险率上的指数线性项进行建模。
生存树是决策树在生存数据上的一个重要变体,其分裂标准基于logrank检验。自然地,我们将对生存数据的集成方法感兴趣,因此我们在最后一节中开发了生存随机森林。
本章我们将涵盖以下主题:
-
生存分析的基本概念,如风险率、累积风险函数和生存函数
-
Nelson-Aalen 和 Kaplan-Meier 估计量分别作为累积风险函数和生存函数的估计量
-
Logrank 测试用于比较生存曲线
-
参数和半参数方法分析独立协变量对风险率的影响
-
基于 Logrank 测试的生存树
-
随机森林作为生存数据的集成方法
生存分析的核心概念
生存分析处理截尾数据,在临床试验中观察到寿命时,参数模型通常不适用。
用 T 表示生存时间,或感兴趣的事件发生的时间,我们自然有
,这是一个连续随机变量。假设寿命累积分布为 F,相关的密度函数为 f。我们将定义进一步分析所需的重要概念。我们将探讨生存函数的概念。
假设 T 是寿命的连续随机变量,相关的累积分布函数为 F。在时间 t 的生存函数是观察到的时间点仍然存活的可能性,它由以下定义:

生存函数可以采取不同的形式。让我们通过每个分布的例子来了解生存函数之间的差异,以获得更清晰的图像。
指数分布:假设一个电子元件的寿命分布遵循指数分布,其速率
。那么,其密度函数如下:

累积分布函数如下:

指数分布的均值和方差分别为
和
。指数分布的生存函数如下:

指数分布的均值
。指数分布由单个参数驱动,并且它还享有一种优雅的性质,称为无记忆性质(参见第六章,Tattar 等人,2016 年)。
伽马分布:如果寿命随机变量的概率密度函数 f 具有以下形式,我们说其遵循速率
和形状
的伽马分布:

均值和方差分别为
和
。累积分布函数的闭式形式,因此生存函数也不存在。
威布尔分布:如果一个寿命随机变量遵循具有率
和形状
的威布尔分布,那么其概率密度函数 f 的形式如下:

威布尔分布的累积分布函数如下所示:

生存函数如下:

接下来,我们将定义风险率的概念,它也被称为瞬时故障率。
用 T 表示寿命随机变量,用 F 表示相关的累积分布函数,那么在时间 t 的风险率定义为以下:



估计风险率的问题与密度函数的问题一样困难,因此累积函数的概念将是有用的。
用 T 表示寿命随机变量,
表示相关风险率,那么累积风险函数的定义如下:

这三个量之间存在以下关系:


期望值与生存函数的关系如下:

在下一个 R 程序中,我们将可视化三个概率分布的三个生存量。首先,我们将使用par和mfrow函数设置九个图的图形设备。程序以指数分布为例进行解释。考虑时间区间 0-100,并在程序中创建一个名为Time的数值对象。我们将从使用dexp函数计算Time对象的密度函数值开始。这意味着dexp(Time)将为 0-100 之间的每个点计算密度函数f(t)的值。由于生存函数与累积分布函数通过
相关联,而pexp给出了时间点t处的F值,因此指数分布的生存函数计算为1-pexp()。危险率、密度函数和生存函数通过
相关联,并且可以轻松获得。通过使用生存函数的值和以下关系,可以获得累积危险函数:

程序随后将重复用于伽马分布和威布尔分布,并对适当的参数进行更改,如下面的代码所示:
> par(mfrow=c(3,3))
> Time <- seq(0,100,1)
> lambda <- 1/20
> expdens <- dexp(Time,rate=lambda)
> expsurv <- 1-pexp(Time,rate=lambda)
> exphaz <- expdens/expsurv
> expcumhaz <- -log(expsurv)
> plot(Time,exphaz,"l",xlab="Time",ylab="Hazard Rate",ylim=c(0,0.1))
> plot(Time,expcumhaz,"l",xlab="Time",ylab="Cumulative Hazard Function")
> mtext("Exponential Distribution")
> plot(Time,expsurv,"l",xlab="Time",ylab="Survival Function")
>
> # Gamma Distribution
> lambda <- 1/10; k <- 2
> gammadens <- dgamma(Time,rate=lambda,shape=k)
> gammasurv <- 1-pgamma(Time,rate=lambda,shape=k)
> gammahaz <- gammadens/gammasurv
> gammacumhaz <- -log(gammasurv)
> plot(Time,gammahaz,"l",xlab="Time",ylab="Hazard Rate")
> plot(Time,gammacumhaz,"l",xlab="Time",ylab="Cumulative Hazard Function")
> mtext("Gamma Distribution")
> plot(Time,gammasurv,"l",xlab="Time",ylab="Survival Function")
>
> # Weibull Distribution
> lambda <- 25; k <- 2
> Weibulldens <- dweibull(Time,scale=lambda,shape=k)
> Weibullsurv <- 1-pweibull(Time,scale=lambda,shape=k)
> Weibullhaz <- Weibulldens/Weibullsurv
> Weibullcumhaz <- -log(Weibullsurv)
> plot(Time,Weibullhaz,"l",xlab="Time",ylab="Hazard Rate")
> plot(Time,Weibullcumhaz,"l",xlab="Time",ylab="Cumulative Hazard Function")
> mtext("Weibull Distribution")
> plot(Time,Weibullsurv,"l",xlab="Time",ylab="Survival Function")

图 1:指数、伽马和威布尔分布的危脸率、累积危脸函数和生存函数
重复前面的程序,使用不同的参数值,并为危险函数、累积危险函数和生存函数的变化准备观察总结。分别总结指数、伽马和威布尔分布的观察结果。
现在,我们需要查看模型与pbc数据集的拟合程度。在这里,我们将指数、伽马和威布尔分布拟合到pbc数据集中感兴趣的生命周期。请注意,由于我们有截尾数据,不能简单地丢弃不完整的观察值,因为 418 个中有 257 个是不完整的观察值。虽然我们不能深入探讨生存数据的最大似然估计的数学原理,但在此处重要的是要注意,完整观察对似然函数的贡献是f(t),如果它是不完整/截尾的,则是S(t)。因此,对于软件来说,知道哪个观察值是完整的,哪个是不完整的非常重要。在这里,我们将使用survival包中的Surv函数来指定这一点,然后使用flexsurv包中的flexsurvreg函数来拟合适当的生命周期分布。dist选项有助于设置适当的分布,如下面的程序所示:
> pbc <- survival::pbc
> Surv(pbc$time,pbc$status==2)
[1] 400 4500+ 1012 1925 1504+ 2503 1832+ 2466 2400 51 3762
[12] 304 3577+ 1217 3584 3672+ 769 131 4232+ 1356 3445+ 673
[397] 1328+ 1375+ 1260+ 1223+ 935 943+ 1141+ 1092+ 1150+ 703 1129+
[408] 1086+ 1067+ 1072+ 1119+ 1097+ 989+ 681 1103+ 1055+ 691+ 976+
> pbc_exp <- flexsurvreg(Surv(time,status==2)~1,data=pbc,dist="exponential")
> pbc_exp
Call:
flexsurvreg(formula = Surv(time, status == 2) ~ 1, data = pbc,
dist = "exponential")
Estimates:
est L95% U95% se
rate 2.01e-04 1.72e-04 2.34e-04 1.58e-05
N = 418, Events: 161, Censored: 257
Total time at risk: 801633
Log-likelihood = -1531.593, df = 1
AIC = 3065.187
> windows(height=100,width=100)
> plot(pbc_exp,ylim=c(0,1),col="black")
> pbc_gamma <- flexsurvreg(Surv(time,status==2)~1,data=pbc,dist="gamma")
> pbc_gamma
Call:
flexsurvreg(formula = Surv(time, status == 2) ~ 1, data = pbc,
dist = "gamma")
Estimates:
est L95% U95% se
shape 1.10e+00 9.21e-01 1.30e+00 9.68e-02
rate 2.33e-04 1.70e-04 3.21e-04 3.78e-05
N = 418, Events: 161, Censored: 257
Total time at risk: 801633
Log-likelihood = -1531.074, df = 2
AIC = 3066.147
> plot(pbc_gamma,col="blue",add=TRUE)
> pbc_Weibull <- flexsurvreg(Surv(time,status==2)~1,data=pbc,dist="weibull")
> pbc_Weibull
Call:
flexsurvreg(formula = Surv(time, status == 2) ~ 1, data = pbc,
dist = "weibull")
Estimates:
est L95% U95% se
shape 1.08e+00 9.42e-01 1.24e+00 7.48e-02
scale 4.71e+03 3.96e+03 5.59e+03 4.13e+02
N = 418, Events: 161, Censored: 257
Total time at risk: 801633
Log-likelihood = -1531.017, df = 2
AIC = 3066.035
> plot(pbc_Weibull,col="orange",add=TRUE)
> legend(3000,1,c("Exponential","Gamma","Weibull"),
+ col=c("black","blue","orange"),merge=TRUE,lty=2)
结果图如下所示:

图 2:对截尾数据进行指数、伽马和威布尔分布的拟合
拟合的指数模型的 AIC 值是 3065.187,拟合的伽马模型的 AIC 值是 3066.147,而韦伯尔分布的 AIC 值是 3066.035。标准越低越好。因此,根据 AIC 标准,指数模型是最好的拟合。然后是韦伯尔分布和伽马分布。在这里,单参数的指数分布比更复杂的伽马和韦伯尔模型拟合得更好。
现在需要对 R 程序进行一些解释。pbc数据集通过survival::pbc加载,因为 R 包randomForestSRC也有一个同名数据集,它是稍有不同的版本。因此,survival::pbc代码确保我们继续加载pbc数据集,就像我们在早期实例中所做的那样。对我们来说,感兴趣的事件由status==2和Surv(pbc$time,pbc$status==2)指示,它创建了一个包含在数值对象中的完整观察值的生存对象。如果status不是2,则观察值是删失的,这由跟在+号后面的数字表示。Surv(time,status==2)~1代码创建必要的公式,这对于应用生存函数很有用。dist="exponential"选项确保在生存数据上拟合指数分布。当在控制台上运行拟合模型pbc_exp时,我们得到拟合模型的摘要,并返回模型参数的估计值、95%置信区间和参数估计的标准误差。我们还得到完整和删失观察值的数量、所有患者的总风险时间、似然函数值和 AIC。注意三个拟合分布的自由度是如何变化的。
这里详细描述的参数模型给出了生存概念的一个概念。当我们没有足够的证据来构建一个参数模型时,我们求助于非参数和半参数模型来进行统计推断。在下一节中,我们将继续分析pbc数据。
非参数推断
生存数据会受到删失的影响,我们需要引入一个新的量来捕捉这个信息。假设我们有一个来自
的独立同分布的寿命随机变量的n样本,并且我们知道感兴趣的事件可能已经发生,或者它将在未来的某个时间发生。通过克罗内克指示变量
捕捉了额外的信息:

因此,我们在 Ts 和
s,
中有 n 对随机观测值。为了获得累积风险函数和生存函数的估计值,我们需要额外的符号。让
表示 Ts 中事件发生时的唯一时间点。接下来,我们用
表示在时间
和
之前处于风险中的观测值数量,而
表示在该时间发生的事件数量。使用这些量,我们现在提出使用以下方法来估计累积风险函数:

估计量
是著名的 Nelson-Aalen 估计量。Nelson-Aalen 估计量具有统计性质,包括(i)它是累积风险函数的非参数最大似然估计量,以及(ii)它遵循渐近正态分布。生存函数的估计量如下:

估计量
是著名的 Kaplan-Meier 估计量。通过应用函数-Δ定理,Nelson-Aalen 估计量的性质被传递到 Kaplan-Meier 估计量。需要注意的是,Kaplan-Meier 估计量再次是非参数最大似然估计量,并且渐近地遵循正态分布。我们现在将探讨如何使用 R 软件获取给定数据集的估计值。
我们已经使用 Surv(pbc$time, pbc$status==2) 代码创建了生存对象。现在,在生存对象上应用 survfit 函数,我们在 pbc_sf survfit 对象中设置了 Kaplan-Meier 估计器:
> pbc_sf <- survfit(Surv(time,status==2)~1,pbc)
> pbc_sf
Call: survfit(formula = Surv(time, status == 2) ~ 1, data = pbc)
n events median 0.95LCL 0.95UCL
418 161 3395 3090 3853
输出结果显示我们共有 418 个观测值。其中,161 个经历了感兴趣的事件。我们希望获取不同时间点的生存函数。中位生存时间为 3395,该点估计值的置信区间为 3090 至 3853。然而,如果你找到整体时间的平均值,完整观测值的平均时间,以及截尾观测值的平均时间,这些值都不会接近显示的 3395 值。一段快速代码显示了以下结果:
> median(pbc$time)
[1] 1730
> median(pbc$time[pbc$status==2])
[1] 1083
> median(pbc$time[pbc$status!=2])
[1] 2157
你可能会问自己,为什么估计的中位生存时间与这些中位数之间有如此大的差异?答案很快就会变得清晰。
我们将使用 summary 函数来获取这个答案。对于观察到的十个时间分位数,包括截尾时间,我们将获得 Kaplan-Meier 估计值及其相关的 95% 置信区间,该置信区间基于方差估计,而方差估计又基于 Greenwood 公式:
> summary(pbc_sf,times=as.numeric(quantile(pbc$time,seq(0,1,0.1))))
Call: survfit(formula = Surv(time, status == 2) ~ 1, data = pbc)
time n.risk n.event survival std.err lower 95% CI upper 95% CI
41 418 2 0.995 0.00338 0.989 1.000
607 376 39 0.902 0.01455 0.874 0.931
975 334 31 0.827 0.01860 0.791 0.864
1218 292 19 0.778 0.02061 0.738 0.819
1435 251 8 0.755 0.02155 0.714 0.798
1730 209 13 0.713 0.02323 0.669 0.760
2107 167 12 0.668 0.02514 0.621 0.719
2465 126 9 0.628 0.02702 0.577 0.683
2852 84 10 0.569 0.03032 0.512 0.632
3524 42 10 0.478 0.03680 0.411 0.556
4795 1 8 0.353 0.04876 0.270 0.463
我们现在已经得到了每个十分位时间点的 Kaplan-Meier 估计值、每个点的标准误差和置信区间。使用plot函数,我们现在将可视化pbc数据集的拟合 Kaplan-Meier 估计值:
> plot(pbc_sf,xlab="Time",ylab="Survival Function Confidence Bands")

图 3:PBC 数据集的 Kaplan-Meier 估计值
现在,如果你观察生存时间接近 0.5 的时间点,中位生存时间 3395 的早期答案就足够清晰了。接下来,我们来看累积风险函数。
要获得累积风险函数,我们将对生存对象应用coxph函数,并使用basehaz函数获取基线累积风险函数,如下面的代码所示:
> pbc_na <- basehaz(coxph(Surv(time,status==2)~1,pbc))
> pbc_na
hazard time
1 0.004790426 41
2 0.007194272 43
3 0.009603911 51
4 0.012019370 71
396 1.030767970 4509
397 1.030767970 4523
398 1.030767970 4556
399 1.030767970 4795
我们将使用以下代码创建 Nelson-Aalen 估计值的可视化表示:
> plot(pbc_na$time,pbc_na$hazard,"l",xlab="Time",ylab="Cumulative Hazard Function")
下面的图表说明了 Nelson-Aalen 估计值:

图 4:累积风险函数的 Nelson-Aalen 估计值
注意,有人可能会倾向于使用
关系来获得 Kaplan-Meier 估计值,或者反之亦然。让我们使用以下代码来检查一下:
> str(exp(-pbc_na$hazard))
num [1:399] 0.995 0.993 0.99 0.988 0.986 ...
> str(summary(pbc_sf,times=pbc_na$time)$surv)
num [1:399] 0.995 0.993 0.99 0.988 0.986 ...
这里似乎一切正常,所以让我们全面检查一下:
> round(exp(-pbc_na$hazard),4)==round(summary(pbc_sf,
+ times=pbc_na$time)$surv,4)
[1] TRUE TRUE TRUE FALSE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
[12] TRUE TRUE FALSE FALSE FALSE FALSE FALSE TRUE TRUE TRUE FALSE
[23] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE
[34] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[375] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[386] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[397] FALSE FALSE FALSE
经过一段时间后,估计值会有很大差异,因此我们分别计算这两个量。接下来,我们将探讨如何进行统计检验以比较生存曲线的相等性。
如前所述,我们将公式输入到survfit函数中。它出现在Surv公式中的额外指定'~1'。由于公式对于生存数据的进一步分析至关重要,我们现在可以很好地利用这个框架。如果我们用 1 替换为一个分类变量,如性别,那么我们将为分类变量的每个水平获得生存曲线。对于pbc数据,我们将绘制生存曲线。分别对男性和女性的 Kaplan-Meier 估计值进行绘图。
>plot(survfit(Surv(time,status==2)~sex,pbc),conf.int=TRUE,xlab="Time",+ ylab="Survival Probability", col=c("red","blue"))

图 5:PBC 数据的性别生存曲线比较
生存曲线(用蓝色和红色连续线表示)清楚地显示了差异,我们需要评估观察到的差异是否具有统计学意义。为此,我们应用survdiff函数并检查差异是否显著,如下面的代码所示:
> survdiff(Surv(time,status==2)~sex,pbc)
Call:
survdiff(formula = Surv(time, status == 2) ~ sex, data = pbc)
N Observed Expected (O-E)²/E (O-E)²/V
sex=m 44 24 17.3 2.640 2.98
sex=f 374 137 143.7 0.317 2.98
Chisq= 3 on 1 degrees of freedom, p= 0.0845
p 值为0.0845,因此如果选择的显著性水平是 95%,我们得出结论,差异不显著。
读者注意:显著性水平是预先确定的。如果你在进行分析之前将其固定在 95%,然后查看 p 值发现它在 0.05 和 0.10 之间,不要改变水平。坚持之前达成的协议。
在到目前为止的分析中,我们考虑了参数和非参数方法,现在我们需要开发一个更大的框架。需要明确评估协变量的影响,我们将在下一节探讨这个主题。
回归模型 – 参数和 Cox 比例风险模型
你可能还记得,生存数据包括完整的以及截尾的观测值,我们看到了对于 pbc 数据集,寿命看起来像是 400,4500+,1012,1925,1504+,……。尽管寿命是连续的随机变量,但形式为
的回归模型在这里并不合适。事实上,在 20 世纪 70 年代,有许多尝试修正和改进这种形式的模型,而且结果往往是有害的。我们将一个通用的风险回归模型定义为以下形式:

在这里,t 是寿命,
是寿命指标,
是协变量向量,
是回归系数向量,
是基线风险率。一个特别感兴趣的相对风险模型如下:

我们将专注于这一类模型。首先,考虑参数风险回归。这意味着我们将通过参数模型指定风险率
,例如,通过指数分布。但这意味着什么呢?这意味着基线风险函数具有以下形式:

因此,风险回归模型如下:

然后估计问题就是找到
和
。flexsurv 包中的 R 函数 survreg 将有助于拟合参数风险回归模型。它将在 pbc 数据集上展示连续性。survival 公式将被扩展以包括模型中的所有协变量,如下面的代码所示:
> pbc_Exp <- survreg(Surv(time,status==2)~trt + age + sex + ascites
+ hepato + spiders + edema + bili + chol + albumin
+ copper + alk.phos + ast + trig + platelet +
+ protime + stage,
+ dist="exponential", pbc)
> pbc_Exp_summary <- summary(pbc_Exp)
> pbc_Exp_summary
Call:
survreg(formula = Surv(time, status == 2) ~ trt + age + sex +
ascites + hepato + spiders + edema + bili + chol + albumin +
copper + alk.phos + ast + trig + platelet + protime + stage,
data = pbc, dist = "exponential")
Value Std. Error z p
(Intercept) 1.33e+01 1.90e+00 7.006 2.46e-12
trt 6.36e-02 2.08e-01 0.305 7.60e-01
age -2.81e-02 1.13e-02 -2.486 1.29e-02
sexf 3.42e-01 3.00e-01 1.140 2.54e-01
ascites -1.01e-01 3.70e-01 -0.273 7.85e-01
hepato -7.76e-02 2.43e-01 -0.320 7.49e-01
spiders -1.21e-01 2.36e-01 -0.513 6.08e-01
edema -8.06e-01 3.78e-01 -2.130 3.32e-02
bili -4.80e-02 2.49e-02 -1.929 5.37e-02
chol -4.64e-04 4.35e-04 -1.067 2.86e-01
albumin 4.09e-01 2.87e-01 1.427 1.54e-01
copper -1.63e-03 1.11e-03 -1.466 1.43e-01
alk.phos -4.51e-05 3.81e-05 -1.182 2.37e-01
ast -3.68e-03 1.92e-03 -1.917 5.52e-02
trig 1.70e-04 1.37e-03 0.124 9.01e-01
platelet -2.02e-04 1.16e-03 -0.174 8.62e-01
protime -2.53e-01 9.78e-02 -2.589 9.61e-03
stage -3.66e-01 1.66e-01 -2.204 2.75e-02
Scale fixed at 1
Exponential distribution
Loglik(model)= -984.1 Loglik(intercept only)= -1054.6
Chisq= 141.17 on 17 degrees of freedom, p= 0
Number of Newton-Raphson Iterations: 5
n=276 (142 observations deleted due to missingness)
在这里,迭代五次后收敛。p 值几乎等于零,这意味着拟合的模型是显著的。然而,并非所有与协变量相关的 p 值都表明显著性。我们将使用以下代码来查找:
> round(pbc_Exp_summary$table[,4],4)
(Intercept) trt age sexf ascites hepato
0.0000 0.7601 0.0129 0.2541 0.7846 0.7491
spiders edema bili chol albumin copper
0.6083 0.0332 0.0537 0.2859 0.1536 0.1426
alk.phos ast trig platelet protime stage
0.2372 0.0552 0.9010 0.8621 0.0096 0.0275
> AIC(pbc_exp)
[1] 3065.187
AIC 值也非常高,我们试图看看是否可以改进。因此,我们将step函数应用于拟合的指数风险回归模型,并消除不显著的协变量,如下面的代码所示:
> pbc_Exp_eff <- step(pbc_Exp)
Start: AIC=2004.12
Surv(time, status == 2) ~ trt + age + sex + ascites + hepato +
spiders + edema + bili + chol + albumin + copper + alk.phos +
ast + trig + platelet + protime + stage
Df AIC
- ascites 1 2002.2
- trt 1 2002.2
- hepato 1 2002.2
- spiders 1 2002.4
- chol 1 2003.2
- sex 1 2003.4
- alk.phos 1 2003.4
- albumin 1 2004.1
<none> 2004.1
- ast 1 2005.5
- bili 1 2005.5
- edema 1 2006.5
- stage 1 2007.2
- protime 1 2008.0
- age 1 2008.3
- trig 1 2020.4
- copper 1 2020.7
- platelet 1 2021.8
Step: AIC=2002.19
Surv(time, status == 2) ~ trt + age + sex + hepato + spiders +
edema + bili + chol + albumin + copper + alk.phos + ast +
trig + platelet + protime + stage
Step: AIC=1994.61
Surv(time, status == 2) ~ age + edema + bili + albumin + copper +
ast + trig + platelet + protime + stage
Df AIC
<none> 1994.6
- albumin 1 1995.5
- edema 1 1996.3
- ast 1 1996.9
- bili 1 1998.8
- protime 1 1999.2
- stage 1 1999.7
- age 1 2000.9
- platelet 1 2012.1
- copper 1 2014.9
- trig 1 2198.7
There were 50 or more warnings (use warnings() to see the first 50)
> pbc_Exp_eff_summary <- summary(pbc_Exp_eff)
> round(pbc_Exp_eff_summary$table[,4],4)
(Intercept) age edema bili albumin copper
0.0000 0.0037 0.0507 0.0077 0.0849 0.0170
ast trig platelet protime stage
0.0281 0.9489 0.7521 0.0055 0.0100
> AIC(pbc_Exp_eff)
[1] 1994.607
我们在这里看到,当前模型中的所有协变量都是显著的,除了trig和platelet变量。AIC 值也急剧下降。
在生命科学中,参数模型通常不可接受。
的灵活框架是著名的 Cox 比例风险模型。它是一个半参数回归模型,因为基线风险函数
是完全未指定的。Cox(1972)提出了这个模型,这是统计学中最重要的模型之一。基线风险函数
的唯一要求是它必须是非负的,并且与之相关的概率分布必须是合适的概率分布。在这个模型中,回归系数向量
通过将基线风险函数视为一个干扰因素来估计。其推断基于部分似然函数的重要概念;有关完整细节,请参阅 Cox(1975)。在这里,我们只指定 Cox 比例风险模型的形式,并将感兴趣的读者指引到 Kalbfleisch 和 Prentice(2002):

我们将使用survival包中的coxph函数来拟合比例风险回归模型。由于某些技术原因,我们必须从pbc数据集中省略所有包含缺失值的行,并且剩余步骤与指数风险回归模型的拟合平行:
> pbc2 <- na.omit(pbc)
> pbc_coxph <- coxph(Surv(time,status==2)~trt + age + sex + ascites
+ hepato + spiders + edema + bili + chol + albumin
+ copper + alk.phos + ast + trig + platelet +
+ protime + stage, pbc2)
> pbc_coxph_summary <- summary(pbc_coxph)
> pbc_coxph_summary
Call:
coxph(formula = Surv(time, status == 2) ~ trt + age + sex + ascites +
hepato + spiders + edema + bili + chol + albumin + copper +
alk.phos + ast + trig + platelet + protime + stage, data = pbc2)
n= 276, number of events= 111
coef exp(coef) se(coef) z Pr(>|z|)
trt -1.242e-01 8.832e-01 2.147e-01 -0.579 0.56290
age 2.890e-02 1.029e+00 1.164e-02 2.482 0.01305 *
sexf -3.656e-01 6.938e-01 3.113e-01 -1.174 0.24022
ascites 8.833e-02 1.092e+00 3.872e-01 0.228 0.81955
hepato 2.552e-02 1.026e+00 2.510e-01 0.102 0.91900
spiders 1.012e-01 1.107e+00 2.435e-01 0.416 0.67760
edema 1.011e+00 2.749e+00 3.941e-01 2.566 0.01029 *
bili 8.001e-02 1.083e+00 2.550e-02 3.138 0.00170 **
chol 4.918e-04 1.000e+00 4.442e-04 1.107 0.26829
albumin -7.408e-01 4.767e-01 3.078e-01 -2.407 0.01608 *
copper 2.490e-03 1.002e+00 1.170e-03 2.128 0.03337 *
alk.phos 1.048e-06 1.000e+00 3.969e-05 0.026 0.97893
ast 4.070e-03 1.004e+00 1.958e-03 2.078 0.03767 *
trig -9.758e-04 9.990e-01 1.333e-03 -0.732 0.46414
platelet 9.019e-04 1.001e+00 1.184e-03 0.762 0.44629
protime 2.324e-01 1.262e+00 1.061e-01 2.190 0.02850 *
stage 4.545e-01 1.575e+00 1.754e-01 2.591 0.00958 ** ---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
trt 0.8832 1.1323 0.5798 1.3453
age 1.0293 0.9715 1.0061 1.0531
sexf 0.6938 1.4414 0.3769 1.2771
ascites 1.0924 0.9155 0.5114 2.3332
hepato 1.0259 0.9748 0.6273 1.6777
spiders 1.1066 0.9037 0.6865 1.7835
edema 2.7487 0.3638 1.2697 5.9505
bili 1.0833 0.9231 1.0305 1.1388
chol 1.0005 0.9995 0.9996 1.0014
albumin 0.4767 2.0977 0.2608 0.8714
copper 1.0025 0.9975 1.0002 1.0048
alk.phos 1.0000 1.0000 0.9999 1.0001
ast 1.0041 0.9959 1.0002 1.0079
trig 0.9990 1.0010 0.9964 1.0016
platelet 1.0009 0.9991 0.9986 1.0032
protime 1.2617 0.7926 1.0247 1.5534
stage 1.5754 0.6348 1.1170 2.2219
Concordance= 0.849 (se = 0.031 )
Rsquare= 0.455 (max possible= 0.981 )
Likelihood ratio test= 167.7 on 17 df, p=0
Wald test = 174.1 on 17 df, p=0
Score (logrank) test = 283.7 on 17 df, p=0
> round(pbc_coxph_summary$coefficients[,5],4)
trt age sexf ascites hepato spiders edema bili
0.5629 0.0131 0.2402 0.8195 0.9190 0.6776 0.0103 0.0017
chol albumin copper alk.phos ast trig platelet protime
0.2683 0.0161 0.0334 0.9789 0.0377 0.4641 0.4463 0.0285
stage
0.0096
> AIC(pbc_coxph)
[1] 966.6642
由于我们发现许多变量不显著,我们将尝试通过使用step函数并计算改进的 AIC 值来改进它,如下面的代码所示:
> pbc_coxph_eff <- step(pbc_coxph)
Start: AIC=966.66
Surv(time, status == 2) ~ trt + age + sex + ascites + hepato +
spiders + edema + bili + chol + albumin + copper + alk.phos +
ast + trig + platelet + protime + stage
Df AIC
- alk.phos 1 964.66
- hepato 1 964.67
- ascites 1 964.72
- spiders 1 964.84
- trt 1 965.00
- trig 1 965.22
- platelet 1 965.24
- chol 1 965.82
- sex 1 965.99
<none> 966.66
- ast 1 968.69
- copper 1 968.85
- protime 1 968.99
- albumin 1 970.35
- age 1 970.84
- edema 1 971.00
- stage 1 971.83
- bili 1 973.34
Step: AIC=952.58
Surv(time, status == 2) ~ age + edema + bili + albumin + copper +
ast + protime + stage
Df AIC
<none> 952.58
- protime 1 955.06
- ast 1 955.79
- edema 1 955.95
- albumin 1 957.27
- copper 1 958.18
- age 1 959.97
- stage 1 960.11
- bili 1 966.57
> pbc_coxph_eff_summary <- summary(pbc_coxph_eff)
> pbc_coxph_eff_summary
Call:
coxph(formula = Surv(time, status == 2) ~ age + edema + bili +
albumin + copper + ast + protime + stage, data = pbc2)
n= 276, number of events= 111
coef exp(coef) se(coef) z Pr(>|z|)
age 0.0313836 1.0318812 0.0102036 3.076 0.00210 **
edema 0.8217952 2.2745795 0.3471465 2.367 0.01792 *
bili 0.0851214 1.0888492 0.0193352 4.402 1.07e-05 ***
albumin -0.7185954 0.4874364 0.2724486 -2.638 0.00835 **
copper 0.0028535 1.0028576 0.0009832 2.902 0.00370 **
ast 0.0043769 1.0043865 0.0018067 2.423 0.01541 *
protime 0.2275175 1.2554794 0.1013729 2.244 0.02481 *
stage 0.4327939 1.5415584 0.1456307 2.972 0.00296 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
age 1.0319 0.9691 1.0114 1.0527
edema 2.2746 0.4396 1.1519 4.4915
bili 1.0888 0.9184 1.0484 1.1309
albumin 0.4874 2.0515 0.2858 0.8314
copper 1.0029 0.9972 1.0009 1.0048
ast 1.0044 0.9956 1.0008 1.0079
protime 1.2555 0.7965 1.0292 1.5314
stage 1.5416 0.6487 1.1588 2.0508
Concordance= 0.845 (se = 0.031 )
Rsquare= 0.448 (max possible= 0.981 )
Likelihood ratio test= 163.8 on 8 df, p=0
Wald test = 176.1 on 8 df, p=0
Score (logrank) test = 257.5 on 8 df, p=0
> round(pbc_coxph_eff_summary$coefficients[,5],4)
age edema bili albumin copper ast protime stage
0.0021 0.0179 0.0000 0.0084 0.0037 0.0154 0.0248 0.0030
> AIC(pbc_coxph_eff)
[1] 952.5814
我们现在可以看到,pbc_coxph_eff模型中的几乎所有变量都是显著的。AIC 值也与其早期值相比有所下降。
在大多数生存数据分析中,目的是找到有效的协变量及其对风险率的影响。在生存数据的情况下,不存在像 AUC 这样的分类问题准确度度量。在类似(尽管重要)的方面,对于给定协变量选择的寿命预测可能不再重要。在第二章中,我们探讨了伪值方法和 Jackknife 方法的应用,这些方法提供了易于解释的便利。在下一节中,我们将探讨不同的方法。
生存树
参数风险回归模型有时被从业者视为一种限制性模型类别,Cox 比例风险回归有时比其参数对应物更受欢迎。与参数模型相比,解释有时会丢失,常规从业者发现很难与风险回归模型联系起来。当然,一个替代方案是在伪观察值上构建生存树。这种尝试可以在 Tattar(2016)未发表的论文中看到。Gordon 和 Olshen(1985)首次尝试构建生存树,许多科学家继续构建它。LeBlanc 和 Crowley(1992)是建立生存树的重要贡献者之一。Zhang 和 Singer(2010)也对相关方法进行了系统发展,他们书中第 7-10 章讨论了生存树。基本前提保持不变,我们需要好的分割标准来创建生存树。
LeBlanc 和 Crowley 提出了基于饱和模型对数似然与最大对数似然之间的节点偏差度量的分割标准,然后通过用 Nelson-Aalen 估计器估计的基线累积风险函数替换,来近似未知的完整似然。正如 Hamad 等人(2011)所建议的,请注意,这种方法的优势在于任何能够执行泊松树实现的软件都能够创建生存树。这种方法在 Therneau 和 Atkinson(2017)创建的rpartR 包中被利用。生存树的终端节点可以通过属于该节点的观察值的 Kaplan-Meier 生存函数来总结。
我们现在将使用rpart库为pbc数据集设置生存树:
> pbc_stree <- rpart(Surv(time,status==2)~trt + age + sex + ascites +
+ hepato + spiders + edema + bili + chol + albumin +
+ copper + alk.phos + ast + trig + platelet +
+ protime + stage,
+ pbc)
> pbc_stree
n= 418
node), split, n, deviance, yval
* denotes terminal node
1) root 418 555.680700 1.0000000
2) bili< 2.25 269 232.626500 0.4872828
4) age< 51.24162 133 76.376990 0.2395736
8) alk.phos< 1776 103 30.340440 0.1036750 *
9) alk.phos>=1776 30 33.092480 0.6135695 *
5) age>=51.24162 136 136.800300 0.7744285
10) protime< 10.85 98 80.889260 0.5121123 *
11) protime>=10.85 38 43.381670 1.4335430
22) age< 65.38125 26 24.045820 0.9480269
44) bili< 0.75 8 5.188547 0.3149747 *
45) bili>=0.75 18 12.549130 1.3803650 *
23) age>=65.38125 12 8.392462 3.2681510 *
3) bili>=2.25 149 206.521900 2.6972690
6) protime< 11.25 94 98.798830 1.7717830
12) stage< 3.5 57 56.734150 1.2620350
24) age< 43.5332 25 16.656000 0.6044998 *
25) age>=43.5332 32 32.986760 1.7985470 *
13) stage>=3.5 37 32.946240 2.8313470 *
7) protime>=11.25 55 76.597760 5.1836880
14) ascites< 0.5 41 52.276360 4.1601690
28) age< 42.68172 7 6.829564 1.4344660 *
29) age>=42.68172 34 37.566600 5.1138380 *
15) ascites>=0.5 14 17.013010 7.9062910 *
通过在控制台运行pbc_stree,显示生存树的文本表示。节点yval末尾的星号(*)表示删失。
我们现在将查看与生存树相关的cptable和变量重要性,如下程序所示:
> pbc_stree$cptable
CP nsplit rel error xerror xstd
1 0.20971086 0 1.0000000 1.0026398 0.04761402
2 0.05601298 1 0.7902891 0.8179390 0.04469359
3 0.03500063 2 0.7342762 0.8198147 0.04731189
4 0.02329408 3 0.6992755 0.8247148 0.04876085
5 0.02254786 4 0.6759815 0.8325230 0.05132829
6 0.01969366 5 0.6534336 0.8363125 0.05160507
7 0.01640950 6 0.6337399 0.8373375 0.05268262
8 0.01366665 7 0.6173304 0.8336743 0.05406099
9 0.01276163 9 0.5899971 0.8209714 0.05587843
10 0.01135209 10 0.5772355 0.8248827 0.05612403
11 0.01000000 11 0.5658834 0.8255873 0.05599763
> pbc_stree$variable.importance
bili protime age albumin edema stage
138.007841 70.867308 54.548224 32.239919 25.576170 15.231256
ascites alk.phos platelet sex copper ast
14.094208 13.440866 10.017966 2.452776 2.114888 1.691910
变量重要性显示,bili变量是最重要的,其次是protime和age。我们用以下代码的视觉表示来结束本节:
> windows(height=100,width=60)
> plot(pbc_stree,uniform = TRUE)
> text(pbc_stree,use.n=TRUE)

图 6:PBC 数据集的生存树
就像单个树模型中存在的过拟合问题一样,我们需要考虑生存树集成的重要替代方法。
集成生存模型
随机森林包randomForestSRC将继续用于创建与生存数据相关的随机森林。实际上,包名中的SRC的S代表生存!rfsrc函数的使用方法与前面章节相同,现在我们将提供一个Surv对象,如下面的代码所示:
> pbc_rf <- rfsrc(Surv(time,status==2)~trt + age + sex + ascites +
+ hepato + spiders + edema + bili + chol + albumin+
+ copper + alk.phos + ast +trig + platelet + protime+
+ stage, ntree=500, tree.err = TRUE, pbc)
我们将找到设置此随机森林的一些基本设置:
> pbc_rf$splitrule
[1] "logrankCR"
> pbc_rf$nodesize
[1] 6
> pbc_rf$mtry
[1] 5
因此,分裂标准基于 log-rank 检验,终端节点中的最小观测数是六个,每个分裂考虑的变量数是五个。
接下来,我们将找到两个事件的变量重要性,并应用我们在前面章节中使用的var.select函数。然后,我们将展示迭代运行的一部分,如下面的代码所示:
> vimp(pbc_rf)$importance
event.1 event.2
trt 0.0013257796 0.0002109143
age 0.0348848966 0.0166352983
sex 0.0007755091 0.0004011303
ascites 0.0008513276 0.0107212361
hepato 0.0050666763 0.0015445001
spiders -0.0001136547 0.0003552415
edema 0.0006227470 0.0147982184
bili 0.0696654202 0.0709713627
chol 0.0002483833 0.0107024051
albumin -0.0106392917 0.0115188264
copper 0.0185417386 0.0255099568
alk.phos 0.0041407841 0.0022297323
ast 0.0029317937 0.0063469825
trig -0.0040190463 0.0014371745
platelet 0.0021021396 0.0002388797
protime 0.0057968358 0.0133710339
stage -0.0023944666 0.0042808034
> var.select(pbc_rf, method = "vh.vimp", nrep = 50)
--------------------- Iteration: 1 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 38.8889 dim: 2
--------------------- Iteration: 2 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 23.6842 dim: 2
--------------------- Iteration: 3 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 50.6667 dim: 1
--------------------- Iteration: 48 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 38.3562 dim: 2
--------------------- Iteration: 49 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 19.4737 dim: 2
--------------------- Iteration: 50 ---------------------
selecting variables using Variable Hunting (VIMP) ...
PE: 55.914 dim: 2
fitting forests to final selected variables ...
-----------------------------------------------------------
family : surv-CR
var. selection : Variable Hunting (VIMP)
conservativeness : medium
dimension : 17
sample size : 276
K-fold : 5
no. reps : 50
nstep : 1
ntree : 500
nsplit : 10
mvars : 4
nodesize : 2
refitted forest : TRUE
model size : 2.16 +/- 0.6809
PE (K-fold) : 43.3402 +/- 17.7472
Top variables:
rel.freq
bili 28
hepato 24
ast 24
-----------------------------------------------------------
接下来,随着树的数量增加,错误率图将使用以下代码进行展示:
> plot(pbc_rf,plots.one.page = TRUE)

图 7:两个事件的随机森林错误率
因此,我们现在已经看到了生存数据的随机森林构建和设置。
摘要
生存数据与典型的回归数据不同,不完整的观测值构成了挑战。由于数据结构完全不同,我们需要专门的技巧来处理不完整的观测值,为此,我们介绍了核心生存概念,如风险率和生存函数。然后,我们介绍了参数生存模型,这让我们对寿命分布的形状有一个大致的了解。我们甚至将这些寿命分布拟合到 pbc 数据集中。
我们还了解到参数设置可能非常严格,因此考虑了生存量估计的非参数方法。我们还展示了 Nelson-Aalen 估计量、Kaplan-Meier 生存函数和对数秩检验的实用性。参数风险回归模型以 Cox 比例风险回归模型为支持,并应用于 pbc 数据集。logrank 检验也可以帮助确定分裂标准,它也被视为随机森林的标准。生存树在前面章节中已展示。
在下一章中,我们将探讨另一种数据结构:时间序列数据。读者不需要熟悉时间序列分析,就可以理解其中应用的集成方法。
第十一章. 时间序列模型的集成
本书迄今为止开发的模型都处理了观测值相互独立的情况。海外游客的例子解释了一个时间序列,其中观测值依赖于先前观测到的数据。在简要讨论这个例子时,我们确定有必要开发时间序列模型。由于时间序列具有序列性质,时间戳可能以纳秒、秒、分钟、小时、天或月为单位显示。
本章将首先简要回顾时间序列自相关和偏自相关函数的重要概念,以及拟合模型评估指标。与分类和回归模型类似,有许多方法可用于分析时间序列数据。季节分解中的时间序列模型的一个重要类别包括 LOESS(STL)、指数平滑状态空间模型(ets)、Box-Jenkins(ARIMA)模型和自回归神经网络模型。这些将在下一节中进行讨论和说明。本章的最后部分将展示时间序列模型的集成。
本章将涵盖以下主要内容:
-
时间序列数据集
-
时间序列可视化
-
核心概念
-
时间序列模型
-
时间序列的 Bagging
-
集成时间序列模型
技术要求
在本章中,我们将使用以下 R 库:
-
forecast -
forecastHybrid
时间序列数据集
时间序列数据在结构上与之前讨论的数据不同。在第一章的海外游客部分 1 中,以及第一章的集成技术简介中,我们看到了时间序列数据的一瞥。在第二章的重抽样中,时间序列模型的重抽样被简要提及。分析时间序列数据时产生的复杂性在于,观测值不是独立的,因此我们需要指定依赖关系。Box 等人(2015 年)的《时间序列统计分析基准书》首次于 1970 年出版。Box 和 Jenkins 发明并普及的模型类别是流行的自回归积分移动平均,通常缩写为 ARIMA。这也常被称为 Box-Jenkins 模型。
表 1总结了二十一个时间序列数据集。长度列给出了序列的观测值/数据点的数量,而频率列给出了时间序列的周期性,其余六列是通过对数值对象应用汇总函数得到的总结。第一列,当然,给出了数据集在 R 中的名称,因此我们没有改变大小写。数据集中的观测值数量从 19 到 7980 不等。
但频率或周期性究竟是什么意思呢?在一个包含周期性的数据集中,相关的时间索引会被重复。例如,我们可能会有年度、季度、月度或周度数据,因此,在后两种情况下,频率将分别是4和12。频率不一定是整数,也可以是分数值。例如,致癌性测试将具有纳秒值。时间序列数据的总结就是将汇总函数应用于数值数据集的结果。因此,我们隐含地假设时间序列数据是数值的。总结中看到的变异也表明,不同的数据集将需要不同的处理。Tattar 等人(2017)的第十章中可以找到时间序列应用的简要介绍。
本章所使用的数据集的描述可以在下表中找到:
| Dataset | Length | Frequency | Minimum | Q1 | Median | Mean | Q3 | Maximum |
|---|---|---|---|---|---|---|---|---|
AirPassengers |
144 | 12 | 104.00 | 180.00 | 265.50 | 280.30 | 360.50 | 622.00 |
BJsales |
150 | 1 | 198.60 | 212.58 | 220.65 | 229.98 | 254.68 | 263.30 |
JohnsonJohnson |
84 | 4 | 0.44 | 1.25 | 3.51 | 4.80 | 7.13 | 16.20 |
LakeHuron |
98 | 1 | 575.96 | 578.14 | 579.12 | 579.00 | 579.88 | 581.86 |
Nile |
100 | 1 | 456.00 | 798.50 | 893.50 | 919.35 | 1032.50 | 1370.00 |
UKgas |
108 | 4 | 84.80 | 153.30 | 220.90 | 337.63 | 469.90 | 1163.90 |
UKDriverDeaths |
192 | 12 | 1057.00 | 1461.75 | 1631.00 | 1670.31 | 1850.75 | 2654.00 |
USAccDeaths |
72 | 12 | 6892.00 | 8089.00 | 8728.50 | 8788.79 | 9323.25 | 11317.00 |
WWWusage |
100 | 1 | 83.00 | 99.00 | 138.50 | 137.08 | 167.50 | 228.00 |
airmiles |
24 | 1 | 412.00 | 1580.00 | 6431.00 | 10527.83 | 17531.50 | 30514.00 |
austres |
89 | 4 | 13067.30 | 14110.10 | 15184.20 | 15273.45 | 16398.90 | 17661.50 |
co2 |
468 | 12 | 313.18 | 323.53 | 335.17 | 337.05 | 350.26 | 366.84 |
discoveries |
100 | 1 | 0.00 | 2.00 | 3.00 | 3.10 | 4.00 | 12.00 |
lynx |
114 | 1 | 39.00 | 348.25 | 771.00 | 1538.02 | 2566.75 | 6991.00 |
nhtemp |
60 | 1 | 47.90 | 50.58 | 51.20 | 51.16 | 51.90 | 54.60 |
nottem |
240 | 12 | 31.30 | 41.55 | 47.35 | 49.04 | 57.00 | 66.50 |
presidents |
120 | 4 | 23.00 | 46.00 | 59.00 | 56.31 | 69.00 | 87.00 |
treering |
7980 | 1 | 0.00 | 0.84 | 1.03 | 1.00 | 1.20 | 1.91 |
气体 |
476 | 12 | 1646.00 | 2674.75 | 16787.50 | 21415.27 | 38628.50 | 66600.00 |
uspop |
19 | 0.1 | 3.93 | 15.00 | 50.20 | 69.77 | 114.25 | 203.20 |
太阳黑子 |
2820 | 12 | 0.00 | 15.70 | 42.00 | 51.27 | 74.93 | 253.80 |
表 1:R 中的时间序列数据集
AirPassengers
AirPassengers数据集包含 1949 年至 1960 年每月的国际航空公司乘客总数。每月计数数字以千为单位。在十二年的时间里,每月数据累积了 144 个观测值。由于我们在多年的月份中有多于一个的观测值,因此可以从数据中捕捉到旅客计数的季节性方面。这在 Box 等人(2015)中得到了普及。
二氧化碳
二氧化碳时间序列数据与大气中二氧化碳的浓度相关。浓度以百万分之一(ppm)表示,该数据集报告在 1997 年 SIO 压力计摩尔分数初步尺度上。这个时间序列在 1959-97 年期间按月捕捉。在co2的帮助页面上,指出 1964 年 2 月、3 月和 4 月的缺失值是通过在 1964 年 1 月和 5 月之间的值进行线性插值获得的。
uspop
美国人口普查(以百万为单位),uspop,在 1790 年至 1970 年间的十年人口普查中被记录。这些数据以小时间序列数据集的形式提供,因此它只包含 19 个数据点。该数据集未捕捉到季节性。
气体
气体时间序列数据包含澳大利亚每月的天然气产量。这里提供的数据是 1956-95 年期间的数据。因此,我们有 476 个观测值。这个数据集来自forecast包。
汽车销售
汽车销售数据来自亚伯拉罕和莱德尔特(1983)。更多信息,请参阅书中第 68 页的表 2.7。销售和广告数据可用期为 36 个月。在这里,我们还有每周广告支出金额的额外信息。这是额外变量可用性的第一个实例,需要专门处理,我们将在本章后面进一步探讨。数据可在Car_Sales.csv文件中找到,并在代码包中提供。
austres
austres时间序列数据集包括 1971 年 3 月至 1994 年 3 月期间澳大利亚居民的季度数据。
WWW 使用情况
WWWusage时间序列数据集包括通过服务器连接到互联网的用户数量。数据以一分钟的时间间隔收集。时间序列值收集了 100 个观测值。
可视化提供了宝贵的见解,我们将绘制一些时间序列。
时间序列可视化
时间序列数据的主要特征是观测值是在固定的时间间隔内进行的。时间序列值(y轴)与时间本身(x轴)的图表非常重要,并提供了许多结构性的见解。时间序列图不仅仅是带有时间作为x轴的散点图。时间是单调递增的,因此在时间序列图中比散点图中的x轴有更多的意义和重要性。例如,线条可以连接时间序列图中的点,这将指示时间序列的路径,而在散点图中这样的连接是没有意义的,因为它们会散布在各个地方。路径通常表示趋势,并表明序列将向哪个方向移动。时间序列的变化很容易在图表中描绘出来。我们现在将可视化不同的时间序列。
plot.ts函数是这里可视化方案的核心。首先使用windows函数调用一个适当大小的外部图形设备。在 Ubuntu/Linux 中可以使用X11函数。接下来,我们在AirPassengers数据集上运行plot.ts函数:
> windows(height=100,width=150)
> plot.ts(AirPassengers)
以下图表显示了多年间每月乘客数量的增加,平均情况如下:

图 1:每月航空公司乘客计数
我们可以看到,在 12 个月的周期之后,似乎有一个模式在重复,这表明了月份间的季节性趋势。如果能得到一个图表,我们选择第一年,查看月份的图表,然后转到下一年,并展示下一年每月的数据并可视化,以此类推,直到显示完整的数据集,那就太好了。一个名为plotts的函数实现了这种描述的图表,其结构在图 3中给出:

图 2:时间序列频率图函数
plotts函数现在被应用于AirPassengers数据集。该函数位于配套章节代码包的Utilities.R文件中,并在C11.R文件的开始处使用source命令调用。数据包含 12 年的数据,因此结果的时间序列图将包含 12 条曲线。图表的legend需要比通常的区域更大,因此我们将它绘制在图表的右侧。所需的操作通过par、mar和legend函数完成,如下所示:
> par(mar=c(5.1, 4.1, 4.1, 8.1), xpd=TRUE)
> plotts(AirPassengers)
> legend("topright", inset=c(-0.2,0), "-",
+ legend=c(start(AirPassengers)[1]:end(AirPassengers)[1]),
+ col=start(AirPassengers)[1]:end(AirPassengers)[1],lty=2)
我们现在可以清楚地看到以下图中的季节性影响:

图 3:AirPassengers数据集的季节性图
每月乘客计数在二月和十一月达到低点。从二月到七月,每月乘客计数稳步增加,八月保持在相似的水平,然后急剧下降至十一月。十二月和一月可以看到轻微的增加。因此,季节性图提供了更多的见解,它们应该与 plot.ts 函数互补使用。Hyndman 的预测包包含一个名为 seasonalplot 的函数,它实现了与这里定义的 plotts 函数相同的结果。
澳大利亚居民数据集 austres 将在下一部分进行介绍。我们将使用 plotts 函数和图例来增强显示效果:
>plot.ts(austres)
>windows(height=100,width=150)
>par(mar=c(5.1, 4.1, 4.1, 8.1), xpd=TRUE)
>plotts(austres)
>legend("topright", inset=c(-0.2,0), "-",
+ legend=c(start(austres)[1]:end(austres)[1]),
+ col=start(austres)[1]:end(austres)[1],lty=2)
下面的图是澳大利亚居民数量的季度时间序列:

图 4:澳大利亚居民数量的季度时间序列
图 4 和图 5 的季节性图有什么不同?当然,我们寻找的是除了平凡的月度和季度周期性之外的不同之处。在图 5 中,我们可以看到,尽管月度居民数量在季度中有所增加,但这几乎不是一个季节性因素;它似乎更多的是趋势因素而不是季节性因素。因此,与 AirPassengers 数据集相比,季节性贡献似乎较小。
接下来将可视化二氧化碳浓度的时序图。我们在 co2 数据集上使用 plot.ts 函数:
>plot.ts(co2)
运行 plot.ts 函数的结果是下一个输出:

图 5:莫纳罗亚大气二氧化碳浓度可视化
在二氧化碳浓度的时序图中,季节性影响很容易看到。季节性图可能提供更多的见解,我们将在下一步使用 plotts 函数:
>windows(height=100,width=150)
>par(mar=c(5.1, 4.1, 4.1, 8.1), xpd=TRUE)
>plotts(co2)
>legend("topright",inset=c(-0.2,0),
+ "-",
+ legend=c(c(start(co2)[1]:(start(co2)[1]+3)),". . . ",
+ c((end(co2)[1]-3):end(co2)[1])),
+ col=c(c(start(co2)[1]:(start(co2)[1]+3)),NULL,
+ c((end(co2)[1]-3):end(co2)[1])),lty=2)
plotts 和 legend 函数的使用方式与之前相同。程序的结果显示在 图 7 中,我们可以清楚地看到时间序列显示中的季节性影响:

图 6:莫纳罗亚大气二氧化碳浓度的季节性图
练习:使用 forecast 包中的 seasonalplot 函数复制季节性图。这里有什么不同吗?
季节性是时间序列的一个重要方面。及早识别它对于选择适当的时间序列分析模型非常重要。我们将可视化另外三个时间序列数据集,UKDriverDeaths、gas 和 uspop:
>windows(height=100,width=300)
>par(mfrow=c(1,3))
>plot.ts(UKDriverDeaths,main="UK Driver Deaths")
>plot.ts(gas,main="Australian monthly gas production")
>plot.ts(uspop,main="Populations Recorded by the US Census")
图 7中的三个显示与迄今为止所见的不同。看起来之前拟合良好的模型在这里可能不会表现相似。我们看到UKDriverDeaths和gas数据集有很多可变性。对于UKDriverDeaths,看起来在 1983 年之后,死亡人数有所下降。对于gas数据集,我们可以看到在 1970 年之前有一个规律的季节性影响,在那之后,gas的生产力急剧上升。这可能是一些技术突破或其他现象性变化的迹象。可变性也增加了,并且几乎在整个时间范围内都不稳定。uspop显示出指数增长。
练习:视觉检查UKDriverDeaths和gas数据集是否存在季节性影响:

图 7:三个时间序列图:UKDriverDeaths、gas 和 uspop
核心概念和指标
时间序列数据的可视化在所有示例中都传达了类似的故事,从图 1到图 7。观察到的趋势和季节性表明,时间序列的未来值依赖于当前值,因此我们不能假设观察值之间是独立的。但这意味着什么呢?为了重申这一点,考虑更简单的uspop(美国人口)数据集,图 7的第三右面板显示。在这里,我们没有季节性影响。现在,考虑 1900 年的普查年份。下一次普查的人口肯定不会少于 1890 年,也不会远低于同年的同一数量。对于大多数时间序列也是如此;例如,如果我们正在记录一天的最高温度。在这里,如果今天的最高温度是 42°C,那么第二天最高温度将高度受这个数字的影响,并且几乎可以肯定地说,第二天的最高温度不会是 55°C 或 25°C。虽然很容易看出观察值是相互依赖的,但正式的规范本身也是一个挑战。让我们正式介绍时间序列。
我们将用
表示在时间
观察到的时序。对于观察到时间 T 的时序,另一种表示方法是
。时序可以被视为在时间 t=1, 2, 3, …观察到的随机过程 Y。与时序过程
相关的是误差过程
。误差过程通常假设为具有零均值和一些常数方差的白噪声过程。误差过程通常被称为创新过程。请注意,时序
可能依赖于
过程中的过去值,以及误差
的值和误差过程的过去值
。值
也被称为
的第一滞后值,
是
的第二滞后值,依此类推。现在,如果观测值相互依赖,那么它们之间关系的指定就是最大的挑战。当然,我们无法在这里详细说明。然而,如果我们认为一阶滞后项是相关的,那么这里必须存在某种关系,我们可以获得一个散点图,其中
的观测值位于 y 轴上,一阶滞后项位于 x 轴上。AirPassengers数据集的一阶滞后散点图如下获得:
>plot(AirPassengers[1:143],AirPassengers[2:144],
+ xlab="Previous Observation",
+ ylab="Current Observation")
使用索引将时序对象的类别更改为数值对象:

图 8:AirPassengers数据集的滞后图
在先前的图形显示中,我们可以清楚地看到滞后观测值之间存在(几乎)线性关系,因此模型可能的形式为
。散点图能否帮助确定依赖的阶数?几乎不能!我们将获得WWWusage数据集的下一个图,并查看滞后图:
>windows(height=200,width=200)
>par(mfrow=c(2,2))
>plot.ts(WWWusage)
>plot(WWWusage[1:99],WWWusage[2:100],
+ xlab="Previous Observation",
+ ylab="Current Observation",main="Lag-1 Plot"
+ )
>plot(WWWusage[1:98],WWWusage[3:100],
+ xlab="Previous Observation",
+ ylab="Current Observation",main="Lag-2 Plot"
+ )
>plot(WWWusage[1:97],WWWusage[4:100],
+ xlab="Previous Observation",
+ ylab="Current Observation",main="Lag-3 Plot"
+ )
以下是WWWUsage的滞后图:

图 9:WWWUsage的滞后图
第一阶滞后图可能会给人留下观测值相关的印象。然而,更高阶的滞后图几乎没有任何意义,回到第一阶滞后图只会造成更多的困惑。因此,我们需要一种更正式的方法来获得滞后阶数上的洞察。
对于理解时间序列数据中依赖性的本质,有两个有用的度量:自相关函数(ACF)和偏自相关函数(PACF)。正如其名称所示,ACF 是时间序列与其滞后值之间的相关性。PACF 的偏命名法解释了从滞后值中去除中间变量的影响。简单来说,滞后 3 的 PACF 将只包括第一个
;第三个滞后变量
和
变量不允许影响 PACF。滞后-k ACF 定义为随机变量Y t与第 k 滞后变量Y t-k之间的相关性:

其中
是时间序列的方差。时间序列的偏自相关函数 PACF 是Yt与其第 k 滞后Yt-k之间的偏相关。无法深入探讨 PACF 概念的数学细节;读者可以参考 Box 等人(2015)以获取更多信息。基于n个观察值的样本 ACF 公式如下:

对于 PACF 的显式公式,我们建议读者参考网络上可用的文档,网址为mondi.web.elte.hu/spssdoku/algoritmusok/acf_pacf.pdf。
尽管公式看起来令人畏惧,但我们可以通过在两个数据集austres和uspop上简单地使用acf和pacf函数来轻松地解决这个问题:
>jpeg("ACF_PACF_Plots.jpeg")
>par(mfrow=c(2,2))
>acf(austres,main="ACF of Austres Data")
>pacf(austres,main="PACF of Austres Data")
>acf(uspop,main="ACF of US Population")
>pacf(uspop,main="PACF of US Population")
>dev.off()
RStudioGD
2
我们将保持 ACF 和 PACF 图的解释更加简单。在 ACF 和 PACF 图中,重要的指导原则是水平蓝色线条。任何超出两条线的滞后 ACF 和 PACF 图都是显著的,而那些在限制范围内的则是无关紧要的:

图 10:austres 和 uspop 数据集的 ACF 和 PACF 图
从图 10中我们可以看到,对于austres时间序列,我们需要扩展 ACF 图以包括更多的滞后。这是因为所有绘制的滞后都超出了水平蓝色线条。对于uspop时间序列,第一个时间序列的前四个滞后是显著的,其余的都在水平蓝色线条内。PACF 图可以以类似的方式进行解释。
ACF 和 PACF 图在 ARMA 模型识别中起着重要作用。即使对于 AR 的情况,这些图揭示了滞后信息,也可以用来指定时间序列作为神经网络输入向量的前几个值。
在许多实际问题中,我们还有额外的变量,我们可能将这些称为协变量时间序列或外生变量。让我们用
表示协变量时间序列,其中
可能是一个标量或向量时间序列。我们采用惯例,只有
的当前和过去值会影响
,而
的未来值不会以任何方式影响
。也就是说,只有协变量的滞后值会有影响,而不是它们的先行值。在 Car Sales 数据集中,销售是我们感兴趣的时间序列,我们认为广告方面影响了销售;销售不可能解释广告金额!ccf 函数用于以下方式获取交叉相关系数:
>CarSales <- read.csv("../Data/Car_Sales.csv")
>summary(CarSales)
Sales Advertising
Min. :12.0 Min. : 1.0
1st Qu.:20.3 1st Qu.:15.8
Median :24.2 Median :23.0
Mean :24.3 Mean :28.5
3rd Qu.:28.6 3rd Qu.:41.0
Max. :36.5 Max. :65.0
>jpeg("CCF_Car_Sales_Advertising.jpeg")
>ccf(x=CarSales$Advertising,y=CarSales$Sales,
+ main="Cross Correlation Between Sales and Advertising")
>dev.off()
RStudioGD
2
下图展示了销售和广告之间的交叉相关性:

图 11:广告支出和汽车销售的交叉相关系数
我们应该关注正滞后值还是负滞后值?请注意,ccf 图并不是对称的,因此我们不能因为忽略了滞后值的符号而免责。在 R 终端运行 ?ccf,我们得到 ccf(x, y) 返回的滞后 k 值来自帮助文件,该文件估计了 x[t+k] 和 y[t] 之间的相关性。因此,正滞后是先行指标,而负滞后对我们来说更有兴趣。在这个例子中,只有
的前置滞后(即
和
)是显著的。
我们在本节结束时简要讨论了准确度测量。与早期学习问题一样,我们将有一系列模型可供我们选择。这是下一节讨论的主题,我们需要相应地定义某些评估指标。让
表示时间序列,由于使用某种模型,拟合值将是
。我们可以通过各种方法访问模型的准确性;一些准确性测量包括以下内容:

目前,我们不会关注模型。相反,我们将使用它作为主要工具来提取拟合值并帮助获取定义的指标。使用 subset 函数,我们将定义训练数据集,并使用 forecast 包中的 auto.arima 函数拟合模型。然后,使用 accuracy 和 forecast 函数,我们将获得不同的准确性:
>co2_sub <- subset(co2,start=1,end=443)
>co2_arima <- auto.arima(co2_sub)
>accuracy(forecast(co2_arima,h=25),x=co2[444:468])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.0185 0.283 0.225 0.00541 0.0672 0.211 0.0119
Test set -0.0332 0.349 0.270 -0.00912 0.0742 0.252 NA
forecast函数是一个非常重要的函数。给定一个拟合的时序,它将提供所需未来期间的预测,并且准确度函数计算了七个不同标准所需的准确度。平均误差标准通常是无用的,对于几乎无偏的模型,其数值将接近 0。RMSE、MAE、MPE 和 MAPE 指标通常很有用,它们的值越低,模型拟合得越好。此外,训练集误差和测试集误差必须可以比较。如果它们彼此差异很大,那么该模型对预测来说就没有用了。在下一节中,我们将回顾一类有用的时序模型。
重要的时序模型
我们已经遇到了不同回归模型的一系列模型。时间序列数据带来了额外的复杂性,因此我们有更多的模型可供选择(或者更确切地说,是从中集成)。这里提供了一个重要模型的快速回顾。这里讨论的大多数模型都处理单变量时间序列
,我们需要更多专业化的模型和方法来包含
。我们将从最简单可能的时间序列模型开始,然后逐步过渡到神经网络实现。
简单预测
假设我们拥有
的数据,并且我们需要预测下一个h个时间点
。简单预测模型不需要任何建模练习或计算,它只是简单地返回当前值作为未来的预测,因此
。就这么简单。即使是这个简单的任务,我们也会使用 forecast 包中的简单函数,并要求它提供下一个25个观察值的预测,其中h=25:
>co2_naive <- naive(co2_sub,h=25,level=c(90,95))
>summary(co2_naive)
Forecast method: Naive method
Model Information:
Call: naive(y = co2_sub, h = 25, level = c(90, 95))
Residual sd: 1.1998
Error measures:
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.1 1.2 1.07 0.029 0.319 0.852 0.705
Forecasts:
Point Forecast Lo 90 Hi 90 Lo 95 Hi 95
Dec 1995 360 358 362 357 362
Jan 1996 360 357 362 356 363
Feb 1996 360 356 363 356 364
Oct 1997 360 350 369 348 371
Nov 1997 360 350 369 348 371
Dec 1997 360 350 370 348 371
如预期的那样,预测值在整个期间保持不变。它们可以很容易地可视化,准确度也可以很容易地按以下方式计算:
>plot(co2_naive) # Output suppressed
>accuracy(forecast(co2_naive,h=25),x=co2[444:468])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.10 1.20 1.07 0.029 0.319 1.00 0.705
Test set 3.54 4.09 3.55 0.972 0.974 3.32 NA
自然会出现的疑问是简单预测是否有效。对此的回答可以通过其他方式提供。复杂和精密的模型总是声称具有优势。模型确实可能具有优势,但参考和基准应该是清晰的。简单预测提供了这个重要的基准。请注意,对于训练期,简单预测的准确度值是不同的,并且重要的是所提出的模型至少要比简单预测的指标更好。这就是简单预测的主要目的。
季节性、趋势和局部加权回归拟合
季节、趋势和局部加权回归是三个技术术语的组合,形成了 stl 模型。之前,我们在时间序列的视觉显示中看到,其中一些描绘了季节性效应,一些显示了趋势,一些显示了季节性和趋势的组合,还有一些是简单的非规律性时间序列。这些显示因此表明了底层现象的具体性质的存在或不存在。在本节中,我们将探讨如何利用时间序列的季节性和趋势部分。在 loess 中的 stl 模型的第三个组成部分完全没有解释。局部加权回归是一种非参数回归技术,代表局部多项式回归,它将加权最小二乘准则推广到 p 次多项式。局部加权回归方法还包括一个称为核的重要组件。核是一种平滑方法,但我们将不会对此进行过多细节的讨论。
Cleveland 等人(1990 年)提出了基于局部加权回归的季节性-趋势分解,该过程的全部细节可以从以下来源获得:www.scb.se/contentassets/ca21efb41fee47d293bbee5bf7be7fb3/stl-a-seasonal-trend-decomposition-procedure-based-on-loess.pdf。这篇论文易于阅读,直观且富有洞察力,读者应该真正地跟随它。stl 模型是一种分解季节性时间序列的滤波方法,分为三个部分:趋势、季节性和剩余部分。设
为时间序列,我们用
、
和
分别表示趋势、季节性和剩余部分;那么我们将有如下:

请参阅 Cleveland 等人发表的论文以获取完整细节。
使用stats包中的stl函数,我们将AirPassengers数据分解如下:
>AP_stl <- stl(AirPassengers,s.window=frequency(AirPassengers))
>summary(AP_stl)
Call:
stl(x = AirPassengers, s.window = frequency(AirPassengers))
Time.series components:
seasonal trend remainder
Min. :-73.3 Min. :123 Min. :-36.2
1st Qu.:-25.1 1st Qu.:183 1st Qu.: -6.4
Median : -5.5 Median :260 Median : 0.3
Mean : 0.1 Mean :280 Mean : -0.2
3rd Qu.: 20.4 3rd Qu.:375 3rd Qu.: 5.9
Max. : 94.8 Max. :497 Max. : 48.6
IQR:
STL.seasonal STL.trend STL.remainder data
46 192 12 180
% 25.2 106.4 6.8 100.0
Weights: all == 1
Other components: List of 5
$ win : Named num [1:3] 12 21 13
$ deg : Named int [1:3] 0 1 1
$ jump : Named num [1:3] 2 3 2
$ inner: int 2
$ outer: int 0
>jpeg("STL_Decompose_AirPassengers.jpeg")
>plot(AP_stl)
>dev.off()
windows
2
>accuracy(forecast(AP_stl))
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.00498 11.2 8.29 -0.129 3.29 0.259 0.000898
执行前面的代码后,可以得到以下图形:

图 12:AirPassengers 的 STL 分解
在stl函数中通过s.window选项指定季节性是很重要的。从季节性图中,我们可以看到每个成分随时间增加。然而,我们可以清楚地看到乘客数量随时间变化的各个组成部分。尽管季节性部分在幅度上增加,但整个期间的模式保持不变。趋势显示线性增加,这表明序列前进的方向。很明显,季节性在这个背景下起着重要作用。
练习:之前已经提到,季节性似乎不是austres数据集分析中的一个有用的因素。使用stl分解并检查这一观察结果是否成立。
接下来将考虑一个更参数化的模型,形式为指数平滑模型。
指数平滑状态空间模型
基本指数平滑模型可以明确定义。用
表示平滑因子,并初始化
。基本指数平滑模型定义为以下:

指数模型的详细信息可以在labs.omniti.com/people/jesus/papers/holtwinters.pdf找到。一个更通用的模型形式是指数平滑状态空间模型。在这里,模型在三个方面被指定,就像 stl 模型一样:误差成分、趋势成分,第三个是季节成分。在 forecast 包的ets函数中,成分可以具有加性效应,用A表示,它可以具有乘性效应,用 M 表示,或者可能要求自动选择(Z),并且这种指定适用于每个成分。效应可以指定为既不是加性的也不是乘性的,用字母 N 表示。因此,ets函数中的模型是用第一个字母表示误差成分,第二个字母表示趋势成分,第三个字母表示季节成分来指定的。因此,表示model="ZZZ"意味着三个成分都是自动选择的。model="AMZ"意味着误差成分是加性的,趋势是乘性的,季节成分是自动选择的,等等。Hyndman 等人(2008 年)提供了指数平滑方法详细情况的全面概述。接下来,我们将使用forecast包中的ets函数来拟合指数平滑模型:
>uspop_sub <- subset(uspop,start=1,end=15)
>USpop_ets <- ets(uspop_sub)
>summary(USpop_ets)
ETS(A,A,N)
Call:
ets(y = uspop_sub)
Smoothing parameters:
alpha = 0.8922
beta = 0.8922
Initial states:
l = 2.3837
b = 1.7232
sigma: 1.68
AIC AICc BIC
66.2 72.8 69.7
Training set error measures:
ME RMSE MAE MPE MAPE MASE ACF1
Training set 1.11 1.68 1.4 3.26 4.6 0.0318 -0.28
ets函数为误差和趋势成分拟合了加性误差,而选择不对季节因子添加任何内容。这是有意义的,因为uspop数据集没有季节成分。使用这个拟合模型,我们将预测 1940-70 年间的美国人口,使用accuracy函数计算训练和测试数据集的准确性,并与naive预测进行比较:
>forecast(USpop_ets,h=4)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
1940 139 137 141 136 142
1950 156 151 160 149 162
1960 172 165 180 161 183
1970 189 178 200 173 205
>plot(forecast(USpop_ets,h=4))
>accuracy(forecast(USpop_ets,h=4),x=uspop[16:19])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 1.11 1.68 1.40 3.259 4.60 0.165 -0.28
Test set 2.33 9.02 8.26 0.578 4.86 0.973 NA
>accuracy(forecast(naive(uspop_sub),h=4),x=uspop[16:19])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 8.49 9.97 8.49 21.7 21.7 1.00 0.778
Test set 43.58 51.35 43.58 24.2 24.2 5.13 NA
下图展示了美国人口数据的指数平滑:

图 13:美国人口数据的指数平滑
准确性比较是与简单预测进行的,我们发现ets预测有显著的改进。因此,ets预测是有用的,我们可以使用它们进行未来预测。
接下来,我们将继续介绍流行的 Box-Jenkins/ARIMA 模型。
自回归积分移动平均(ARIMA)模型
Box 和 Jenkins 使用 ARIMA 模型对时间序列的分析和预测方法产生了变化。ARIMA 模型是更一般线性过程模型的一个特例,对于具有创新过程
的时间序列
,其表达式如下:

在这里,
是线性过程的系数。请注意,对创新过程的滞后值没有限制,我们确实意味着在这个线性过程模型中有无限项。在流行的自回归 AR(p)模型中,p 是 AR 模型的阶数。这可以用以下方式表示:

AR 模型可以证明是线性过程模型的一个特例。当时间序列用创新过程表示时,另一个有用的模型是阶数为 q 的移动平均 MA(q) 模型:

时间序列可能不仅依赖于过去值,还依赖于过去的误差,这种结构依赖性在自回归移动平均 ARMA(p,q) 模型中得以捕捉,其阶数为 (p,q):

p 和 q 的阶数可以通过 表 2 中 ACF 和 PACF 相关的经验规则来确定:

表 2:ARMA 模型的 ACF 和 PACF
ARMA 模型与平稳时间序列数据配合良好,这里的平稳大致意味着序列的变异性在整个过程中保持恒定。这是一个限制性假设,对于许多时间序列现象,这个假设并不成立。在许多实际场景中,通过差分序列
可以获得平稳性,也就是说,我们可以考虑差分
。差分
是一阶差分,有时可能需要更高阶的差分。在大多数实际场景中,差分到 4 阶已被证明可以带来平稳性。差分的阶数通常用字母 d 表示,将 ARMA 模型应用于差分被称为自回归积分移动平均模型,或 ARIMA 模型。简写为 ARIMA(p,d,q)。
在本章中,我们多次遇到了季节成分,它通过季节 ARIMA 模型在 ARIMA 中得到解决。有动机的读者可以通过 Tattar 等人(2017)的第十章了解更多细节。我们在这里简单地补充说,我们还有大写字母(P, D, Q)的进一步符号表示季节参数,以及频率项。我们现在能够理解上一节末尾拟合的模型。co2_arima的准确性已经评估,我们现在将查看摘要:
>summary(co2_arima)
Series: co2_sub
ARIMA(2,1,2)(1,1,2)[12]
Coefficients:
ar1 ar2 ma1 ma2 sar1 sma1 sma2
0.033 0.250 -0.369 -0.246 -0.828 0.014 -0.750
s.e. 0.341 0.122 0.342 0.197 0.230 0.210 0.173
sigma² estimated as 0.0837: log likelihood=-73.4
AIC=163 AICc=163 BIC=195
Training set error measures:
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.0185 0.283 0.225 0.00541 0.0672 0.179 0.0119
最佳拟合的 ARIMA 模型阶数是(2,1,2)(1,1,2)[12],这意味着季节频率为12(这是我们已知的事情),季节阶数(P,D,Q)为(1,1,2),ARIMA 阶数(p,d,q)为(2,1,2)。正是这种差分阶数实现了平稳性。预测结果已获得并进行了可视化:
>jpeg("CO2_Forecasts.jpeg")
>plot(forecast(co2_arima,h=25))
下图显示了输出:

图 14:二氧化碳浓度预测
因此,我们已经为二氧化碳浓度水平拟合了 ARIMA 模型。
接下来,我们将探讨非线性神经网络时间序列模型。
自回归神经网络
神经网络之前已被用于分类以及回归问题。由于时间序列是依赖性观测值,我们需要调整神经网络的架构以包含这种依赖性。调整是为了允许时间序列的滞后值作为输入层的成员。其余的架构遵循与常规神经网络相同的结构。forecast中的nnetar函数代表神经网络自回归,p=选项允许时间序列的滞后值,我们将这些值应用于gas问题:
>gas_sub <- subset(gas,start=1,end=450)
>gas_nnetar <- nnetar(gas_sub,p=25,P=12,size=10,repeats=10)
>plot(forecast(gas_nnetar,h=26))
>accuracy(forecast(gas_nnetar,h=26),x=gas[451:476])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 2 318 237 -0.127 1.78 0.148 -0.0879
Test set 5033 6590 5234 10.566 10.94 3.276 NA

图 15:使用自回归神经网络的气体预测
我们现在已经看到了自回归神经网络的实际应用。
一团糟
我们在本章开头简要介绍了七个数据集,并在表 1中提到了 21 个数据集。数据可视化提供了适度的洞察力,而准确度指标有助于分析模型的有用性。到目前为止,本节已经介绍了一系列模型,现在我们将把一切都搞乱。定义了一个get_Accuracy函数,该函数将拟合六个不同的时间序列模型。代表线性模型的LM尚未解释;TBATS 模型也没有解释。线性模型非常简单,因为时间索引被用作协变量。一般来说,如果一个时间序列有 T 个观测值,协变量向量简单地由值 1,2,3,…,T 组成。我们预计线性模型的表现会不佳。TBATS 模型在此不再详细解释,因此建议阅读一些额外的资料以获取更多相关信息。get_Accuracy模型将每个模型拟合到 21 个数据集,命名模型,并列出其在整个数据集上的性能。以下程序得到所需的结果:
>get_Accuracy<- function(ts){
+ tsname <- deparse(substitute(ts))
+ Acc_Mat <- data.frame(TSName = rep(tsname,6),Models=c(
+ "ETS","STL","LM","ARIMA","NNETAR","TBATS"),
+ ME=numeric(6),RMSE=numeric(6),MAE=numeric(6),
+ MPE=numeric(6), MAPE=numeric(6),MASE=numeric(6))
+ for(i in 1:nrow(Acc_Mat)){
+ Acc_Mat[1,3:8] <- accuracy(ets(ts)$fitted,ts)[1:6]
+ if(frequency(ts)>1) Acc_Mat[2,3:8] <- accuracy(ts-stl(ts,
+ frequency(ts))$time.series[,3],ts)[1:6] else
+ Acc_Mat[2,3:8] <- NA
+ Acc_Mat[3,3:8] <- accuracy(fitted(lm(ts~I(1:length(ts)))),ts)[1:6]
+ Acc_Mat[4,3:8] <- accuracy(auto.arima(ts)$fitted,ts)[1:6]
+ Acc_Mat[5,3:8] <- accuracy(fitted(nnetar(ts)),ts)[1:6]
+ Acc_Mat[6,3:8] <- accuracy(fitted(tbats(ts)),ts)[1:6]
+ }
+ Acc_Mat
+ }
> TSDF <- data.frame(TSName=character(0),Models=character(0),
+ Accuracy=numeric(0))
> TSDF <- rbind(TSDF,get_Accuracy(AirPassengers))
> TSDF <- rbind(TSDF,get_Accuracy(BJsales))
> TSDF <- rbind(TSDF,get_Accuracy(JohnsonJohnson))
> TSDF <- rbind(TSDF,get_Accuracy(LakeHuron))
> TSDF <- rbind(TSDF,get_Accuracy(Nile))
> TSDF <- rbind(TSDF,get_Accuracy(UKgas))
> TSDF <- rbind(TSDF,get_Accuracy(UKDriverDeaths))
> TSDF <- rbind(TSDF,get_Accuracy(USAccDeaths))
> TSDF <- rbind(TSDF,get_Accuracy(WWWusage))
> TSDF <- rbind(TSDF,get_Accuracy(airmiles))
> TSDF <- rbind(TSDF,get_Accuracy(austres))
> TSDF <- rbind(TSDF,get_Accuracy(co2))
> TSDF <- rbind(TSDF,get_Accuracy(discoveries))
> TSDF <- rbind(TSDF,get_Accuracy(lynx))
> TSDF <- rbind(TSDF,get_Accuracy(nhtemp))
> TSDF <- rbind(TSDF,get_Accuracy(nottem))
> TSDF <- rbind(TSDF,get_Accuracy(presidents))
In addition: Warning message:
In ets(ts) :
Missing values encountered. Using longest contiguous portion of time series
> TSDF <- rbind(TSDF,get_Accuracy(treering))
> TSDF <- rbind(TSDF,get_Accuracy(gas))
> TSDF <- rbind(TSDF,get_Accuracy(uspop))
> TSDF <- rbind(TSDF,get_Accuracy(sunspots))
> write.csv(TSDF,"../Output/TS_All_Dataset_Accuracies.csv",row.names=F)
前述代码块输出的表格如下:
| TSName | Model | ME | RMSE | MAE | MPE | MAPE | MASE |
|---|---|---|---|---|---|---|---|
AirPassengers |
ETS | 1.5807 | 10.6683 | 7.7281 | 0.4426 | 2.8502 | 0.0164 |
AirPassengers |
STL | -0.1613 | 11.9379 | 8.5595 | -0.0662 | 3.4242 | 0.5515 |
AirPassengers |
LM | 0.0000 | 45.7362 | 34.4055 | -1.2910 | 12.3190 | 0.7282 |
AirPassengers |
ARIMA | 1.3423 | 10.8462 | 7.8675 | 0.4207 | 2.8005 | -0.0012 |
AirPassengers |
NNETAR | -0.0118 | 14.3765 | 11.4899 | -0.2964 | 4.2425 | 0.5567 |
AirPassengers |
TBATS | 0.4655 | 10.6614 | 7.7206 | 0.2468 | 2.8519 | 0.0215 |
BJsales |
ETS | 0.1466 | 1.3272 | 1.0418 | 0.0657 | 0.4587 | -0.0110 |
BJsales |
STL | NA | NA | NA | NA | NA | NA |
BJsales |
LM | 0.0000 | 9.1504 | 7.1133 | -0.1563 | 3.1686 | 0.9872 |
BJsales |
ARIMA | 0.1458 | 1.3281 | 1.0447 | 0.0651 | 0.4601 | -0.0262 |
BJsales |
NNETAR | -0.0001 | 1.4111 | 1.0849 | -0.0040 | 0.4798 | 0.2888 |
BJsales |
TBATS | 0.1622 | 1.3566 | 1.0666 | 0.0732 | 0.4712 | -0.0113 |
JohnsonJohnson |
ETS | 0.0495 | 0.4274 | 0.2850 | 1.0917 | 7.0339 | -0.2948 |
JohnsonJohnson |
STL | -0.0088 | 0.1653 | 0.1080 | -0.5953 | 2.8056 | -0.4155 |
JohnsonJohnson |
LM | 0.0000 | 1.6508 | 1.3287 | 22.6663 | 66.3896 | 0.6207 |
JohnsonJohnson |
ARIMA | 0.0677 | 0.4074 | 0.2676 | 2.0526 | 6.5007 | 0.0101 |
JohnsonJohnson |
NNETAR | 0.0003 | 0.3501 | 0.2293 | -0.6856 | 5.8778 | -0.0347 |
JohnsonJohnson |
TBATS | 0.0099 | 0.4996 | 0.3115 | 0.9550 | 7.5277 | -0.1084 |
sunspots |
ETS | -0.0153 | 15.9356 | 11.2451 | #NAME? | Inf | 0.0615 |
sunspots |
STL | 0.0219 | 12.2612 | 8.7973 | NA | Inf | 0.18 |
sunspots |
LM | 0 | 42.9054 | 34.1212 | #NAME? | Inf | 0.9196 |
sunspots |
ARIMA | -0.0267 | 15.6006 | 11.0258 | NA | Inf | -0.0106 |
太阳黑子 |
NNETAR | -0.0672 | 10.3105 | 7.6878 | NA | Inf | 0.0108 |
太阳黑子 |
TBATS | -0.0514 | 15.5788 | 11.0119 | NA | Inf | -0.0013 |
表 3:21 个数据集上六个模型的准确度
整体信息与分类问题介绍章节中得到的结果相同。由于我们并不总是能够持续进行结果检查,因此将多个模型的结果结合起来,以传达一个更准确的整体故事是很有吸引力的。我们从这个简单的想法开始,即对指数时间序列模型进行袋装。
袋装和时间序列
在本节中,我们只将说明袋装技术用于 ETS 模型。袋装的主要目的是稳定预测或预报。在这里,我们将基于 Box-Cox 和基于 Loess 的分解进行袋装。使用 500 个这样的自助样本,我们将获得 ETS 的袋装模型:
>uspop_bagg_ets <- baggedETS(uspop_sub,bootstrapped_series =
+ bld.mbb.bootstrap(uspop_sub, 500))
>forecast(uspop_bagg_ets,h=4);subset(uspop,start=16,end=19)
Point Forecast Lo 100 Hi 100
1940 141 136 145
1950 158 150 165
1960 175 164 184
1970 193 178 204
Time Series:
Start = 1940
End = 1970
Frequency = 0.1
[1] 132 151 179 203
>plot(forecast(uspop_bagg_ets,h=4))
使用袋装方法是否有优势?我们可以通过置信区间快速检查这一点:
>forecast(uspop_bagg_ets,h=4)
Point Forecast Lo 100 Hi 100
1940 141 136 145
1950 158 150 165
1960 175 164 184
1970 193 178 204
>forecast(USpop_ets,h=4,level=99.99)
Point Forecast Lo 99.99 Hi 99.99
1940 139 133 146
1950 156 142 169
1960 172 150 194
1970 189 157 221
袋装 ETS 的置信区间明显更短,因此反映了方差的减少,这是袋装的主要目的:

图 16:美国人口袋装预测
准确度比较也容易在这里进行:
>accuracy(forecast(USpop_ets,h=4),x=uspop[16:19])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 1.11 1.68 1.40 3.259 4.60 0.165 -0.28
Test set 2.33 9.02 8.26 0.578 4.86 0.973 NA
>accuracy(forecast(uspop_bagg_ets,h=4),x=subset(uspop,start=16,end=19))
ME RMSE MAE MPE MAPE MASE ACF1 Theil's U
Training set 1.137 1.44 1.24 2.226 4.48 0.0283 0.563 NA
Test set -0.359 7.87 7.48 -0.995 4.63 0.1700 0.296 0.299
这里清楚地显示了集成同质基学习者的优势。接下来,我们将转向异质基学习者和它们的集成。
集成时间序列模型
forecastHybrid R 包提供了一个平台来集成异构时间序列模型。实现这一任务的主要功能是 hybridModel 函数。核心函数提供了称为 models 的选项。它接受一个最多六个字符的字符串,这些字符是模型的代表:a 代表 auto.arima 模型,e 代表 ets,f 代表 thetam,n 表示 nnetar,s 代表 stlm,最后,t 代表 tbats。因此,如果我们给 models 一个 ae 字符串,它将结合 ARIMA 和 ets 模型的结果。这可以在 co2 数据集上对不同时间序列模型的组合进行说明:
>accuracy(forecast(co2_arima,h=25),x=co2[444:468])
ME RMSE MAE MPE MAPE MASE ACF1
Training set 0.0185 0.283 0.225 0.00541 0.0672 0.211 0.0119
Test set -0.0332 0.349 0.270 -0.00912 0.0742 0.252 NA
>AP_Ensemble_02 <- hybridModel(co2_sub,models="ae")
Fitting the auto.arima model
Fitting the ets model
>accuracy(AP_Ensemble_02,h=25,x=co2[444:468])
ME RMSE MAE MPE MAPE ACF1 Theil's U
Test set 0.0258 0.271 0.219 0.00755 0.0653 0.00289 0.226
>AP_Ensemble_03 <- hybridModel(co2_sub,models="aen")
Fitting the auto.arima model
Fitting the ets model
Fitting the nnetar model
>accuracy(AP_Ensemble_03,h=25,x=co2[444:468])
ME RMSE MAE MPE MAPE ACF1 Theil's U
Test set 0.017 0.304 0.245 0.0049 0.073 0.282 0.25
>AP_Ensemble_04 <- hybridModel(co2_sub,models="aens")
Fitting the auto.arima model
Fitting the ets model
Fitting the nnetar model
Fitting the stlm model
>accuracy(AP_Ensemble_04,h=25,x=co2[444:468])
ME RMSE MAE MPE MAPE ACF1 Theil's U
Test set 0.0165 0.275 0.221 0.00478 0.066 0.209 0.226
>AP_Ensemble_05 <- hybridModel(co2_sub,models="aenst")
Fitting the auto.arima model
Fitting the ets model
Fitting the nnetar model
Fitting the stlm model
Fitting the tbats model
>accuracy(AP_Ensemble_05,h=25,x=co2[444:468])
ME RMSE MAE MPE MAPE ACF1 Theil's U
Test set 0.0123 0.267 0.216 0.00348 0.0645 0.153 0.22
虽然这里的集成讨论非常简短,但时间序列文献最近才开始适应集成技术。
练习:weights 和 errorMethod 选项对于将不同的时间序列模型组合在一起至关重要。探索章节中介绍和讨论的不同数据集的这些选项。
摘要
时间序列数据带来了新的挑战和复杂性。本章从介绍重要且流行的数据集开始。我们探讨了不同的时间序列及其复杂性。时间序列的可视化提供了深刻的洞察,时间序列图以及季节性图互补地用于清晰的想法和特定实施。时间序列的准确度指标各不相同,我们探讨了其中的一些。自相关函数(ACF)和偏自相关函数(PACF)的概念在模型识别中至关重要,季节性成分对于时间序列建模也同样重要。我们还看到,不同的模型可以表达不同的数据集,变化的程度类似于常规回归问题。时间序列的袋装(仅 ets)减少了预测的方差。在结论部分讨论了结合异构基学习器。下一章是结论章节。我们将总结前十一章的主要收获,并概述一些不足和进一步的研究范围。
第十二章。接下来是什么?
在整本书中,我们学习了集成学习并探讨了其在许多场景中的应用。在介绍章节中,我们考察了不同的例子、数据集和模型,并发现没有哪个模型或技术比其他模型表现得更好。这意味着在处理这个问题时,我们的警惕性应该始终保持,因此分析师必须极端谨慎地进行。从众多模型中选择最佳模型的方法意味着我们拒绝了所有表现略逊于其他模型的模型,因此,在追求“最佳”模型的过程中浪费了大量资源。
在第七章中,我们看到了“通用集成技术”,如果拥有多个分类器,并且每个分类器的性能都优于随机猜测,那么分类器的多数投票会带来性能的提升。我们还看到,在有相当数量的分类器的情况下,多数投票的整体准确率高于最准确的分类器。尽管多数投票基于过于简化的假设,即分类器是相互独立的,但集成学习的基础和重要性得到了体现,前景看起来更加光明,因为我们确保了具有最高准确率的集成分类器。集成中的分类器或模型被称为“基础分类器/学习器/模型”。如果所有基础模型都属于同一模型家族,或者如果家族是逻辑模型、神经网络、决策树或 SVM,那么我们将这些归类为同质集成。如果基础模型属于两个或更多模型家族,那么集成被称为异质集成。
集成学习的一个基本方面在于重采样技术。在第二章“Bootstrapping”中介绍了 Jackknife 和 Bootstrap 方法,并针对不同类别的模型进行了说明。Jackknife 方法使用伪值,我们见证了其在一些复杂场景中的应用。在机器学习领域,伪值的使用不足,它们可能在关系或感兴趣参数相当复杂且可能存在更灵活方法的情况下有用。伪值的概念也可以用于集成诊断,可以创建一个伪准确度度量,这将给集成中的分类器带来额外的准确度。Efron 和 Tibshirani(1990)讨论了获取 Bootstrap 样本的不同方法。在 Bagging 和随机森林步骤中获得的重采样中,我们遵循简单的 Bootstrap 样本抽取观察值的方法。从现有的 Bootstrap 文献中获取不同抽样方法是一个潜在的工作领域。
Bagging、随机森林和提升是具有决策树作为其基学习器的同质集成。本书中讨论的决策树可以被称为频率主义方法或经典方法。决策树的贝叶斯实现可以在一种称为贝叶斯加性回归树的技术中找到。该方法的 R 实现可在BART 包中找到。BART 的理论基础可在arxiv.org/pdf/0806.3286.pdf找到。基于 BART 的同质集成扩展需要全面进行。特别是,Bagging 和随机森林需要以 BART 作为基学习器进行扩展。
对于异构的基本模型,使用堆叠集成方法来设置集成模型。对于分类问题,我们预计加权投票比简单多数投票更有用。同样,模型的加权预测预计会比简单平均表现更好。提出的用于测试基本模型相似性的统计测试都是经典测试。预期的贝叶斯一致性度量将为我们提供进一步的指导,本书作者现在意识到这种评估在集成多样性背景下正在进行。你可以阅读 Broemeling (2009)了解更多信息,www.crcpress.com/Bayesian-Methods-for-Measures-of-Agreement/Broemeling/p/book/9781420083415。此外,当涉及到大型数据集时,生成独立模型集的选项需要系统地开发。
时间序列数据具有不同的结构,观测值的依赖性意味着我们无法在不进行适当调整的情况下直接应用集成方法。第十一章,集成时间序列模型,更详细地探讨了这一主题。最近已经为时间序列数据开发了随机森林。可以在petolau.github.io/Ensemble-of-trees-for-forecasting-time-series/看到随机森林的实现,如果你感兴趣,可以参考此链接获取更多信息。
高维数据分析是另一个最近的话题,本书没有涉及。Buhlmann 和 van de Geer (2011) 的第十二章(见www.springer.com/in/book/9783642201912)在这一领域提供了有用的指导。对于提升数据,Schapire 和 Freund (2012) 的著作(见mitpress.mit.edu/books/boosting)是一笔真正的财富。Zhou (2012)(见www.crcpress.com/Ensemble-Methods-Foundations-and-Algorithms/Zhou/p/book/9781439830031)是关于集成方法的重要书籍,本书受益于其见解。Kuncheva (2004-14)(见onlinelibrary.wiley.com/doi/book/10.1002/9781118914564)可能是第一本详细阐述集成方法的书籍,并包含了许多关于集成诊断的其他细节。Dixit (2017)(见www.packtpub.com/big-data-and-business-intelligence/ensemble-machine-learning)是关于集成方法的另一本近期著作,书中使用 Python 软件展示了这些方法。
最后,读者应始终关注最新的发展。对于 R 语言的实现,资源最好的地方是cran.r-project.org/web/views/MachineLearning.html。
接下来,我们将查看一系列重要的期刊。在这些期刊上,关于机器学习以及集成学习的话题有很多发展和讨论。以下是一些期刊:
-
机器学习研究杂志 (
www.jmlr.org/) -
IEEE 信号处理与机器智能杂志 (
ieeexplore.ieee.org/xpl/RecentIssue.jsp?punumber=34) -
模式识别信件 (
www.journals.elsevier.com/pattern-recognition-letters) -
最后但同样重要的是(尽管这不是一本期刊),
www.packtpub.com/tech/Machine-Learning
如果你觉得这本书有用,我们下次再版时应该再次见面!
附录 A. 参考文献列表
参考文献
Abraham, B. 和 Ledolter, J. (onlinelibrary.wiley.com/doi/book/10.1002/9780470316610), 1983. 预测的统计方法. J. Wiley
Andersen, P.K., Klein, J.P. 和 Rosthøj, S., (doi.org/10.1093/biomet/90.1.15) 2003. Generalised linear models for correlated pseudo-observations, with applications to multi-state models. 生物计量学, 90(1), pp.15-27.
Berk, R.A., (www.springer.com/in/book/9783319440477) 2016. 从回归视角看统计学习,第二版. 纽约:Springer.
Bou-Hamad, I., Larocque, D. 和 Ben-Ameur, H., (projecteuclid.org/euclid.ssu/1315833185) 2011. A review of survival trees. 统计调查, 5, pp.44-71.
Box, G.E., Jenkins, G.M., Reinsel, G.C. 和 Ljung, G.M., (onlinelibrary.wiley.com/doi/book/10.1002/9781118619193) 2015. 时间序列分析:预测与控制. 约翰·威利与 Sons.
Breiman, L., (link.springer.com/article/10.1007/BF00058655) 1996. Bagging predictors. 机器学习, 24(2), pp.123-140.
Breiman, L., Friedman, J.H., Olshen, R.A. 和 Stone, C.J., (www.taylorfrancis.com/books/9781351460491) 1984. 分类与回归树. 路透社.
Broemeling, L.D., (www.crcpress.com/Bayesian-Methods-for-Measures-of-Agreement/Broemeling/p/book/9781420083415) 2009. 度量一致性的贝叶斯方法. Chapman and Hall/CRC.
Bühlmann, P. 和 Van De Geer, S., (www.springer.com/in/book/9783642201912) 2011. 高维数据统计:方法、理论与应用. Springer 科学与商业媒体.
Chatterjee, S. 和 Hadi, A.S., (www.wiley.com/en-us/Regression+Analysis+by+Example%2C+5th+Edition-p-9780470905845) 2012. Regression Analysis by Example, 第五版. 约翰·威利与 Sons.
Ciaburro, G., (www.packtpub.com/big-data-and-business-intelligence/regression-analysis-r) 2018. Regression Analysis with R, Packt Publishing Ltd.
Cleveland, R.B., Cleveland, W.S., McRae, J.E. 和 Terpenning, I., (www.nniiem.ru/file/news/2016/stl-statistical-model.pdf) 1990. STL: A Seasonal-Trend Decomposition. 官方统计杂志, 6(1), pp.3-73.
Cox, D.R.,(eclass.uoa.gr/modules/document/file.php/MATH394/Papers/%5BCox(1972)%5D%20Regression%20Models%20and%20Life%20Tables.pdf) 1972. 回归模型和生命表。《皇家统计学会会刊》,系列 B(方法论),34,第 187-220 页。
Cox, D.R.,(academic.oup.com/biomet/article-abstract/62/2/269/337051) 1975. 部分似然。《生物计量学》,62(2),第 269-276 页。
Dixit, A.,(www.packtpub.com/big-data-and-business-intelligence/ensemble-machine-learning)2017. 集成机器学习:一本结合强大机器学习算法以构建优化模型的入门指南。Packt Publishing Ltd.
Draper, N.R. 和 Smith, H.,(onlinelibrary.wiley.com/doi/book/10.1002/9781118625590)1999/2014. 应用回归分析(第 326 卷)。John Wiley & Sons。
Efron, B. (projecteuclid.org/download/pdf_1/euclid.aos/1176344552) 1979. 自举方法 (link.springer.com/chapter/10.1007/978-1-4612-4380-9_41): 对 Jackknife 的另一种看法,《统计年鉴》,7,1-26。
Efron, B. 和 Hastie, T.,2016. (web.stanford.edu/~hastie/CASI_files/PDF/casi.pdf) 计算机时代的统计推断(第 5 卷)。剑桥大学出版社。
Efron, B. 和 Tibshirani, R.J.,(www.crcpress.com/An-Introduction-to-the-Bootstrap/Efron-Tibshirani/p/book/9780412042317) 1994. 自举方法导论。CRC 出版社。
Friedman, J.H.,Hastie, T. 和 Tibshirani, R. (projecteuclid.org/download/pdf_1/euclid.aos/1016218223)2001. 渐进函数逼近:梯度提升机。《统计年鉴》,29(5):1189–1232。
Gordon, L. 和 Olshen, R.A.,(europepmc.org/abstract/med/4042086)1985. 树结构生存分析。癌症治疗报告,69(10),第 1065-1069 页。
Hastie, T.,Tibshirani, R. 和 Friedman, J. (www.springer.com/in/book/9780387848570),2009 年,统计学习元素,第二版,Springer。
Haykin, S.S, 2009. (www.pearson.com/us/higher-education/program/Haykin-Neural-Networks-and-Learning-Machines-3rd-Edition/PGM320370.html) 神经网络与学习机器 (第 3 卷). 上萨德尔河,新泽西州,美国:: Pearson.
Kalbfleisch, J.D. and Prentice, R.L. (onlinelibrary.wiley.com/doi/abs/10.2307/3315078), 2002. 失效时间数据的统计分析. John Wiley & Sons.
Kuncheva, L.I., (www.wiley.com/en-us/Combining+Pattern+Classifiers%3A+Methods+and+Algorithms%2C+2nd+Edition-p-9781118315231) 2014. 模式分类器的组合:方法和算法. 第二版. John Wiley & Sons.
LeBlanc, M. and Crowley, J., (www.jstor.org/stable/2532300)1992. 缩短生存数据的相对风险树. 生物统计学, 第 411-425 页.
Lee, S.S. and Elder, J.F., (citeseerx.ist.psu.edu/viewdoc/download;jsessionid=6B151AAB29C69A4D4C35C8C4BBFC67F5?doi=10.1.1.34.1753&rep=rep1&type=pdf) 1997. 使用顾问感知器捆绑异构分类器. 白皮书.
Mardia, K. , Kent, J., and Bibby, J.M.., (www.elsevier.com/books/multivariate-analysis/mardia/978-0-08-057047-1) 1979. 多元分析. Academic Press.
Montgomery, D.C., Peck, E.A. and Vining, G.G., (www.wiley.com/en-us/Introduction+to+Linear+Regression+Analysis%2C+5th+Edition-p-9781118627365) 2012. 线性回归分析导论 (第 821 卷). John Wiley & Sons.
Perrone, M.P., and Cooper, L.N., (www.worldscientific.com/doi/abs/10.1142/9789812795885_0025)1993. 当网络意见不一致时:混合神经网络的集成方法. 在 Mammone, R.J. (编者), 语音和图像处理的神经网络. Chapman Hall.
Ripley, B.D., (admin.cambridge.org/fk/academic/subjects/statistics-probability/computational-statistics-machine-learning-and-information-sc/pattern-recognition-and-neural-networks)2007. 模式识别与神经网络. 剑桥大学出版社.
Quenouille, M.H., (www.cambridge.org/core/journals/mathematical-proceedings-of-the-cambridge-philosophical-society/article/approximate-tests-of-correlation-in-timeseries-3/F6D24B2A8574F1716E44BE788696F9C7) 1949 年 7 月。时间序列中相关性的近似检验 3。载于 Mathematical Proceedings of the Cambridge Philosophical Society (第 45 卷,第 3 期,第 483-484 页)。剑桥大学出版社。
Quinlan, J. R. (1993),(www.elsevier.com/books/c45/quinlan/978-0-08-050058-4) C4.5:机器学习程序,Morgan Kaufmann。
Ridgeway, G., Madigan, D. and Richardson, T., (dimacs.rutgers.edu/archive/Research/MMS/PAPERS/BNBR.pdf) 1999 年 1 月. 回归问题的提升方法。载于 AISTATS。
Schapire, R.E. 和 Freund, Y., (dimacs.rutgers.edu/archive/Research/MMS/PAPERS/BNBR.pdf) 2012 年。提升:基础与算法。麻省理工学院出版社。
Seni, G. 和 Elder, J.F.,(www.morganclaypool.com/doi/abs/10.2200/S00240ED1V01Y200912DMK002) 2010 年。数据挖掘中的集成方法:通过组合预测提高准确性。数据挖掘与知识发现综合讲座,第 2 卷(第 1 期),第 1-126 页。
Tattar, P.N.,Ramaiah, S. 和 Manjunath, B.G.,(onlinelibrary.wiley.com/doi/book/10.1002/9781119152743) 2016 年。使用 R 的统计学课程。John Wiley & Sons。
Tattar, P.N., 2017 年。(www.packtpub.com/big-data-and-business-intelligence/statistical-application-development-r-and-python-second-edition) 使用 R 和 Python 进行统计应用开发。Packt Publishing Ltd.
Tattar, P., Ojeda, T., Murphy, S.P., Bengfort, B. 和 Dasgupta, A., (www.packtpub.com/big-data-and-business-intelligence/practical-data-science-cookbook-second-edition) 2017 年。实用数据科学食谱。Packt Publishing Ltd.
Zhang, H. 和 Singer, B.H.,(www.springer.com/in/book/9781441968234) 2010 年。递归分割与应用。Springer Science & Business Media。
Zemel, R.S. 和 Pitassi, T.,(papers.nips.cc/paper/1797-a-gradient-based-boosting-algorithm-for-regression-problems.pdf )2001。用于回归问题的梯度提升算法。在 Advances in neural information processing systems (pp. 696-702)。
(2017)。ClustOfVar: 变量聚类。R 包版本 1.1。
R 包引用
R 包版本 1.6-8. CRAN.R-project.org/package=e1071 Alboukadel Kassambara 和 Fabian Mundt (2017)。factoextra: 提取
《使用 R 的统计学课程》。R 包版本 1.0。
CRAN.R-project.org/package=ACSWR
Alfaro, E., Gamez, M. Garcia, N. (2013). adabag: 用于 Caret 模型集成的 R 包。R 包版本 2.0.0。
第二版。 Thousand Oaks CA: Sage。URL:
软件,54(2),1-35. URL www.jstatsoft.org/v54/i02/.
Chris Keefer, Allan Engelhardt, Tony Cooper, Zachary Mayer, Brenton
函数。R 包版本 1.3-19。
CRAN.R-project.org/package=caretEnsemble Venables, W. N. & Ripley, B. D. (2002) Modern Applied Statistics
第二版。 Thousand Oaks CA: Sage。URL:
Angelo Canty 和 Brian Ripley (2017)。boot: Bootstrap R (S-Plus)
周志华,(www.crcpress.com/Ensemble-Methods-Foundations-and-Algorithms/Zhou/p/book/9781439830031)2012。集成方法:基础与算法。Chapman and Hall/CRC。
Kenkel, R Core 团队,Michael Benesty,Reynald Lescarbeau,Andrew
函数。R 包版本 1.3-19。
Ziem, Luca Scrucca, Yuan Tang, Can Candan 和 Tyler Hunt. (2017).
caret: 分类和回归训练。R 包版本
6.0-77. CRAN.R-project.org/package=caret
统计学,概率论组(以前称为 E1071),维也纳科技大学。使用提升和装袋进行分类。统计软件,54(2),1-35. URL www.jstatsoft.org/v54/i02/.
Max Kuhn。贡献来自 Jed Wing, Steve Weston, Andre Williams, Prabhanjan Tattar (2015)。ACSWR: 《A Companion Package for the Book "A Modern Course in Statistics with R"` 的配套包。
Zachary A. Deane-Mayer 和 Jared E. Knowles (2016)。caretEnsemble:
与 S 的第四版。Springer,纽约。ISBN 0-387-95457-0 class
Marie Chavent, Vanessa Kuentz, Benoit Liquet 和 Jerome Saracco
John Fox 和 Sanford Weisberg (2011)。《An {R} Companion to Applied Regression, Second Edition》。 Thousand Oaks CA: Sage。URL:
CRAN.R-project.org/package=ClustOfVar David Meyer, Evgenia Dimitriadou, Kurt Hornik, Andreas Weingessel
和 Friedrich Leisch (2017)。e1071: 部门的其他函数。R 包版本
《使用 R 的统计学课程》。R 包版本 1.0。
socserv.socsci.mcmaster.ca/jfox/Books/Companion car
和可视化多元数据分析结果。R 包
版本 1.0.5. CRAN.R-project.org/package=factoextra
Sebastien Le, Julie Josse, Francois Husson (2008). FactoMineR: An R
多变量分析包。统计软件杂志,
25(1), 1-18. 10.18637/jss.v025.i01
Alina Beygelzimer, Sham Kakadet, John Langford, Sunil Arya, David
Mount and Shengqiao Li (2013). FNN: 快速最近邻搜索
算法和应用。R 包版本 1.1。
CRAN.R-project.org/package=FNN
Hyndman RJ (2017). _forecast: 时间序列预测函数
和线性模型。R 包版本 8.2,<URL:
pkg.robjhyndman.com/forecast>.
David Shaub and Peter Ellis (2018). forecastHybrid: 方便
集成时间序列预测函数。R 包版本
2.0.10. CRAN.R-project.org/package=forecastHybrid
Greg Ridgeway with contributions from others (2017). gbm:
广义提升回归模型。R 包版本 2.1.3。
CRAN.R-project.org/package=gbm
Vincent J Carey。由 Thomas Lumley 和 Brian Ripley 转移到 R。注意
维护者无法提供有关使用包的建议
他们没有编写。 (2015). gee: 广义估计方程
解决器。R 包版本 4.13-19。
CRAN.R-project.org/package=gee
H2O.ai 团队 (2017). h2o: H2O 的 R 接口。R 包版本
3.16.0.2. CRAN.R-project.org/package=h2o
Andrea Peters and Torsten Hothorn (2017). ipred: 改进的
预测因子。R 包版本 0.9-6。
CRAN.R-project.org/package=ipred
Alexandros Karatzoglou, Alex Smola, Kurt Hornik, Achim Zeileis
(2004). kernlab - R 中的核方法 S4 包。统计软件杂志,
统计软件 11(9), 1-20. URL
Friedrich Leisch & Evgenia Dimitriadou (2010). mlbench: 机器
学习基准问题。R 包版本 2.1-1。
Daniel J. Stekhoven (2013). missForest: 非参数缺失值
使用随机森林进行插补。R 包版本 1.4。
Alan Genz, Frank Bretz, Tetsuhisa Miwa, Xuefei Mi, Friedrich Leisch,
Fabian Scheipl, Torsten Hothorn (2017). mvtnorm: 多变量正态
和 t 分布。R 包版本 1.0-6. URL
CRAN.R-project.org/package=mvtnorm
Beck M (2016). _NeuralNetTools: 用于可视化和分析的
神经网络。R 包版本 1.5.0,<URL:
CRAN.R-project.org/package=NeuralNetTools>.
Venables, W. N. & Ripley, B. D. (2002) 现代应用统计学
使用 S. 第四版。Springer,纽约。ISBN 0-387-95457-0 nnet
Michael P. Fay, Pamela A. Shaw (2010). 精确和渐近加权
间隔数据 Logrank 测试:间隔 R 包。
统计软件杂志,36(2),1-34. URL
www.jstatsoft.org/v36/i02/. perm
Hadley Wickham (2011). 数据分割-应用-组合策略
分析。统计软件杂志,40(1),1-29. URL
www.jstatsoft.org/v40/i01/. plyr
Xavier Robin, Natacha Turck, Alexandre Hainard, Natalia Tiberti,
Frédérique Lisacek, Jean-Charles Sanchez 和 Markus Müller (2011).
pROC: 用于 R 和 S+ 的开源包,用于分析和比较 ROC 曲线
曲线。生物信息学杂志,12,p. 77. DOI: 10.1186/1471-2105-12-77
www.biomedcentral.com/1471-2105/12/77/
Maja Pohar Perme 和 Mette Gerster (2017). pseudo: 计算
模型中的伪观察。R 包版本 1.4.3。
CRAN.R-project.org/package=pseudo
A. Liaw 和 M. Wiener (2002). 分类和回归
randomForest. R News 2(3), 18--22.
Aleksandra Paluszynska 和 Przemyslaw Biecek (2017).
randomForestExplainer: 解释和可视化随机森林
变量重要性术语。R 包版本 0.9。
CRAN.R-project.org/package=randomForestExplainer
Terry Therneau, Beth Atkinson 和 Brian Ripley (2017). rpart:
递归分割和回归树。R 包版本
4.1-11. CRAN.R-project.org/package=rpart
Prabhanjan Tattar (2013). RSADBE: 数据与书籍 "R"
通过示例进行统计应用开发。R 包版本
1.0. CRAN.R-project.org/package=RSADBE
Therneau T (2015). _S 生存分析包 _ 版本
2.38, <URL: CRAN.R-project.org/package=survival>. survival
Terry M. Therneau 和 Patricia M. Grambsch (2000). _ 生存分析建模 _
数据:扩展 Cox 模型。Springer,纽约。ISBN
0-387-98784-3.
Tianqi Chen, Tong He, Michael Benesty, Vadim Khotilovich 和 Yuan
Tang (2018). xgboost: 极端梯度提升。R 包版本
0.6.4.1. CRAN.R-project.org/package=xgboost


,感兴趣的参数
,计算估计值
。
中随机抽取带替换的样本大小 n,并用
表示。
。
。
来执行与
相关的统计推断。
为来自
的独立同分布样本,令
表示基于适当方法的参数估计器,例如最大似然估计或矩估计法。
模拟大小为 n 的第一个引导样本
,并使用与上一步相同的估计技术,基于
获得第一个引导估计
。
,基于引导样本
。
进行推断。
中有放回地抽取大小为 n 的样本,并用
表示。
获取新的回归量。也就是说,
是(第一个)自助样本 Y 值。
和协变量矩阵
,获得回归系数向量
的第一个自助估计。







:
Dt
)

对训练数据拟合分类器 hm。



。
。
和梯度提升预测 
:
拟合深度为 d 的回归树
更新提升预测















可能的结果:
预测标签为 1;
为 1;
为 1;
浙公网安备 33010602011771号