R-应用监督学习-全-

R 应用监督学习(全)

原文:annas-archive.org/md5/a69bdbe06296bcd4cedce10b02fa8fe7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于

本节简要介绍了作者、本书涵盖的内容、您开始所需的技术技能,以及完成所有包含的活动和练习所需的硬件和软件要求。

关于本书

R 是最早为统计计算和数据分析开发的编程语言之一,它对可视化的支持非常出色。随着数据科学的兴起,R 在众多数据科学从业者中成为无疑是一个很好的编程语言选择。由于 R 是开源的,并且在构建复杂的统计模型方面极其强大,它很快在工业和学术界得到了广泛应用。

使用 R 进行应用监督学习 覆盖了使用 R 开发满足您商业需求的监督机器学习算法应用的完整过程。您的学习曲线从发展您的分析思维开始,使用业务输入或领域研究创建问题陈述。您将了解许多比较各种算法的评估指标,然后您将使用这些指标来选择最适合您问题的算法。在确定您想要使用的算法后,您将学习超参数优化技术来微调您的最优参数集。为了避免模型过拟合,您还将了解到如何添加各种正则化项。您还将学习如何将您的模型部署到生产环境中。

当你完成这本书后,你将成为一位在建模监督机器学习算法方面的高手,能够精确满足你的商业需求。

关于作者

Karthik Ramasubramanian 在印度的 PSG 工程技术学院完成了理论计算机科学的硕士学位,在那里他在计算机和网络安全的研究工作中开创了机器学习、数据挖掘和模糊逻辑的应用。他在零售、快速消费品、电子商务、信息技术和酒店业的多国公司和独角兽初创公司中,领导数据科学和商业分析已有超过七年的经验。

他是一位研究人员和问题解决者,拥有丰富的数据科学生命周期经验,从数据问题发现到为各种行业用例创建数据科学概念验证和产品。在他的领导角色中,Karthik 通过数据科学解决方案解决了许多以投资回报率为驱动的商业问题。他通过各种在线平台和大学参与项目,在全球范围内指导并培训了数百名专业人员和学生在数据科学方面的知识。他还开发了基于深度学习模型的智能聊天机器人,这些模型能够理解类似人类的交互、客户细分模型、推荐系统和许多自然语言处理模型。

他还是 Apress 出版社(Springer Business+Science Media 的出版社)出版的《使用 R 进行机器学习》一书的作者。本书取得了巨大成功,在线下载量超过 50,000 次,精装版销量也颇高。本书随后出版了第二版,增加了关于深度学习和时间序列建模的扩展章节。

Jojo Moolayil是一位拥有超过六年工业经验的、在人工智能、深度学习、机器学习和决策科学领域的专业人士。他是 Apress 出版的《学习 Keras 进行深度神经网络》和 Packt Publishing 出版的《更明智的决策——物联网与决策科学的交汇》的作者。他与多个行业领导者合作,在多个垂直领域开展了具有重大影响力和关键性的数据科学和机器学习项目。他目前在加拿大亚马逊网络服务公司担任研究科学家。

除了撰写关于人工智能、决策科学和物联网的书籍外,Jojo 还担任了 Apress 和 Packt Publishing 出版的同一领域各种书籍的技术审稿人。他是一位活跃的数据科学导师,并在 http://blog.jojomoolayil.com 维护着一个博客。

学习目标

  • 培养分析思维,精确地识别商业问题

  • 使用 dplyr、tidyr 和 reshape2 整理数据

  • 使用 ggplot2 可视化数据

  • 使用 k 折算法验证您的监督机器学习模型

  • 使用网格搜索、随机搜索和贝叶斯优化优化超参数

  • 使用 Plumber 在 AWS Lambda 上部署您的模型

  • 通过特征选择和降维提高模型性能

读者对象

本书专为希望探索监督机器学习各种方法和其各种用例的新手和中级数据分析师、数据科学家和数据工程师设计。在统计学、概率论、微积分、线性代数和编程方面的一些背景知识将有助于您彻底理解和跟随本书的内容。

方法

《使用 R 进行应用监督学习》完美地平衡了理论与练习。每个模块都旨在在前一个模块的学习基础上进行构建。本书包含多个活动,使用现实生活中的商业场景供您练习,并在高度相关的环境中应用您的新技能。

最小硬件要求

为了获得最佳的学生体验,我们推荐以下硬件配置:

  • 处理器:Intel 或 AMD 4 核或更好

  • 内存:8 GB RAM

  • 存储:20 GB 可用空间

软件要求

您需要提前安装以下软件:

  • 操作系统:Windows 7、8.1 或 10、Ubuntu 14.04 或更高版本、或 macOS Sierra 或更高版本

  • 浏览器:Google Chrome 或 Mozilla Firefox

  • RStudio

  • RStudio Cloud

您还需要提前安装以下软件、包和库:

  • dplyr

  • tidyr

  • reshape2

  • lubridate

  • ggplot2

  • caret

  • mlr

  • OpenML

习惯用法

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:"LocationWindDirRainToday 变量以及许多其他变量是分类的,其余的是连续的。"

代码块设置如下:

temp_df<-as.data.frame(
  sort(
  round(
  sapply(df, function(y) sum(length(which(is.na(y)))))/dim(df)[1],2)
  )
)
colnames(temp_df) <- "NullPerc"

新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"点击 下一步 按钮,并导航到 详细信息 页面。"

安装和设置

要在 RStudio Cloud 上安装一个包,你可以使用以下语法:

install.packages("Package_Name")

例如:

install.packages("ggplot2")

要验证安装,请运行以下命令:

library(Package_Name)

例如:

library(ggplot2)

安装代码包

将课程代码包复制到 C:/Code 文件夹。

其他资源

该书的代码包也托管在 GitHub 上:https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R.

我们还有来自我们丰富的书籍、视频和 E-learning 产品目录中的其他代码包,可在 https://github.com/PacktPublishing/https://github.com/TrainingByPackt. 查看它们!

第一章:R 高级分析

学习目标

到本章结束时,你将能够:

  • 解释高级 R 编程结构

  • 打印真实世界数据集的摘要统计信息

  • 从 CSV、文本和 JSON 文件中读取数据

  • 编写 R markdown 文件以实现代码可重复性

  • 解释 R 数据结构,如 data.frame、data.table、列表、数组以及矩阵

  • 实现 cbind、rbind、merge、reshape、aggregate 和 apply 函数

  • 使用 dplyr、plyr、caret、tm 等包以及其他许多包

  • 使用 ggplot 创建可视化

在本章中,我们将为使用 R 进行编程奠定基础,并了解高级分析的各种语法和数据结构。

简介

R 是早期为统计计算和数据分析开发的编程语言之一,具有良好的可视化支持。随着数据科学的兴起,R 在众多数据科学从业者中成为无可争议的编程语言选择。由于 R 是开源的,并且在构建复杂的统计模型方面非常强大,它很快在工业和学术界得到了广泛应用。

工具和软件如 SAS 和 SPSS 仅大型企业能够负担得起,而传统的编程语言如 C/C++和 Java 不适合进行复杂的数据分析和构建模型。因此,需要一个更加直接、全面、社区驱动、跨平台兼容和灵活的编程语言成为了一种必需。

尽管 Python 编程语言近年来因其在整个行业的采用和强大的生产级实现而越来越受欢迎,但 R 仍然是快速原型化高级机器学习模型的编程语言选择。R 拥有最丰富的包集合(一个用于完成复杂过程的函数/方法的集合,否则需要花费大量时间和精力来实现)。在撰写本书时,综合 R 档案网络CRAN),一个全球范围内的 FTP 和 Web 服务器网络,存储了 R 的代码和文档的相同、最新版本,拥有超过 13,000 个包。

虽然有许多关于学习 R 基础知识的书籍和在线资源,但在本章中,我们将仅限于涵盖在许多数据科学项目中广泛使用的 R 编程的重要主题。我们将使用来自 UCI 机器学习仓库的真实世界数据集来展示这些概念。本章的材料将对新接触 R 编程的学习者很有用。在接下来的章节中,监督学习概念将借鉴本章的许多实现。

与真实世界数据集一起工作

目前网上有大量可用的开放数据集。以下是一些流行的开放数据集来源:

  • Kaggle: 一个用于举办数据科学竞赛的平台。官方网站是www.kaggle.com/

  • UCI 机器学习仓库: 机器学习社区用于实证分析机器学习算法的数据库、领域理论和数据生成器的集合。您可以通过导航到archive.ics.uci.edu/ml/index.php URL 访问官方网站。

  • data.gov.in: 开放印度政府数据平台,可在data.gov.in/找到。

  • 世界银行开放数据: 免费和开放访问全球发展数据,可通过data.worldbank.org/访问。

越来越多的私营和公共组织愿意将他们的数据公开供公众访问。然而,这仅限于那些组织正在通过 Kaggle 等众包平台寻求解决其数据科学问题的复杂数据集。从组织内部获得的数据作为工作的一部分进行学习是没有替代品的,这项工作在处理和分析方面提供了各种挑战。

数据处理方面的重大学习机会和挑战也来自公共数据源,因为这些数据源中的数据并不都是干净和标准格式的。除了 CSV 之外,JSON、Excel 和 XML 也是一些其他格式,尽管 CSV 占主导地位。每种格式都需要单独的编码和解码方法,因此在 R 中需要一个单独的读取器包。在我们下一节中,我们将讨论各种数据格式以及如何详细处理可用的数据。

在本章以及许多其他章节中,我们将使用来自 UCI 机器学习仓库的葡萄牙银行机构的直接营销活动(电话)数据集。(archive.ics.uci.edu/ml/datasets/bank+marketing)以下表格详细描述了字段:

![图 1.1:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第一部分)]

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/app-spr-lrn-r/img/C12624_01_01.jpg)

图 1.1:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第一部分)

图 1.2:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第二部分)

图 1.2:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第二部分)

在以下练习中,我们将下载bank.zip数据集作为 ZIP 文件,并使用unzip方法解压它。

练习 1:使用 unzip 方法解压下载的文件

在这个练习中,我们将编写一个 R 脚本,从 UCI 机器学习仓库下载葡萄牙银行直接营销活动数据集,并使用unzip函数在指定文件夹中提取 ZIP 文件的内容。

执行以下步骤以完成练习:

  1. 首先,在您的系统上打开 R Studio。

  2. 现在,使用以下命令设置您选择的当前工作目录:

    wd <- "<WORKING DIRECTORY>"
    setwd(wd)
    

    注意

    本书中的 R 代码使用 R 版本 3.2.2 实现。

  3. 使用download.file()方法下载包含数据集的 ZIP 文件:

    url <- "https://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip"
    destinationFileName <- "bank.zip"
    download.file(url, destinationFileName,method = "auto", quiet=FALSE)
    
  4. 现在,在我们使用unzip()方法在工作目录中解压文件之前,我们需要选择一个文件并将其文件路径保存在 R 中(对于 Windows)或指定完整路径:

    zipFile<-file.choose()
    
  5. 定义 ZIP 文件解压的文件夹:

    outputDir <- wd
    
  6. 最后,使用以下命令解压 ZIP 文件:

    unzip(zipFile, exdir=outputDir)
    

    输出如下:

图 1.3:解压 bank.zip 文件

图 1.3:解压 bank.zip 文件

从各种数据格式读取数据

来自数字系统的数据以各种形式生成:电子商务网站上的浏览历史、点击流数据、客户的购买历史、社交媒体互动、零售店的客流量、卫星和无人机图像,以及众多其他格式和类型的数据。我们正处于一个令人兴奋的时代,技术正在极大地改变我们的生活,企业正在利用它来制定他们的下一份数据战略,以做出更好的决策。

能够收集大量不同类型的数据是不够的;我们还需要从中提取价值。一天中拍摄的视频监控可以帮助政府执法团队改善公共场所的实时监控。挑战在于如何在单一系统中处理大量异构数据格式。

客户关系管理(CRM)应用中的交易数据大多为表格形式,而流入社交媒体的数据大多是文本、音频、视频和图片。

我们可以将数据格式分为结构化——如 CSV 和数据库表之类的表格数据;非结构化——如推文、FB 帖子和 Word 文档之类的文本数据;以及半结构化。与难以让机器处理和理解的文本不同,半结构化提供了关联的元数据,这使得计算机处理它变得容易。它广泛应用于许多 Web 应用程序中的数据交换,JSON 是半结构化数据格式的一个例子。

在本节中,我们将了解如何在 R 中加载、处理和转换各种数据格式。在本书的范围内,我们将处理 CSV、文本和 JSON 数据。

CSV 文件

CSV 文件是结构化数据最常见的数据存储和交换格式。R 提供了一个名为read.csv()的方法,用于从 CSV 文件中读取数据。它将数据读入data.frame(更多内容将在下一节介绍)。该方法接受许多参数;其中两个必需参数是filename的路径和sep,它指定了分隔列值的字符。summary()方法描述了六个汇总统计量:最小值第一四分位数中位数平均值第三四分位数最大值

在以下练习中,我们将读取 CSV 文件并总结其列。

练习 2:读取 CSV 文件并总结其列

在这个练习中,我们将读取之前提取的 CSV 文件,并使用 summary 函数打印数值变量的最小值、最大值、平均值、中位数、第一四分位数和第三四分位数,并计算分类变量的类别数。

执行以下步骤以读取 CSV 文件并随后总结其列:

  1. 首先,使用 read.csv 方法将 bank-full.csv 载入 DataFrame:

    df_bank_detail <- read.csv("bank-full.csv", sep = ';')
    
  2. 打印 DataFrame 的摘要:

    summary(df_bank_detail)
    

    输出如下:

    ##       age                 job           marital          education    
    ##  Min.   :18.00   blue-collar:9732   divorced: 5207   primary  : 6851  
    ##  1st Qu.:33.00   management :9458   married :27214   secondary:23202  
    ##  Median :39.00   technician :7597   single  :12790   tertiary :13301  
    ##  Mean   :40.94   admin.     :5171                    unknown  : 1857  
    ##  3rd Qu.:48.00   services   :4154                                     
    ##  Max.   :95.00   retired    :2264                                     
    

JSON

JSON 是用于共享和存储数据的下一个最常用的数据格式。它与 CSV 文件不同,CSV 文件只处理行和列的数据,其中每一行都有确定数量的列。例如,在电子商务客户数据中,每一行可能代表一个客户,其信息存储在单独的列中。对于一个客户,如果某列没有值,该字段存储为 NULL。

JSON 提供了额外的灵活性,即每个客户可以有可变数量的字段。这种类型的灵活性减轻了开发者在传统关系数据库中维护模式时的负担,在传统关系数据库中,相同客户的数据可能分散在多个表中以优化存储和查询时间。

JSON 更像是一种键值存储类型的存储,我们只关心键(如姓名、年龄和出生日期)及其对应的值。虽然这听起来很灵活,但必须采取适当的注意,否则管理可能会在某些时候失控。幸运的是,随着近年来大数据技术的出现,许多文档存储(键值存储的一个子类),通常也称为 NoSQL 数据库,可用于存储、检索和处理此类格式的数据。

在以下练习中,JSON 文件包含 2015-16 年印度泰米尔纳德邦辣椒(香料和调味品)种植区的数据。键包括 面积(公顷)、产量(公担)和 生产力(每公顷平均产量)。

jsonlite 包提供了一个实现,可以将 JSON 文件读取并转换为 DataFrame,这使得分析更加简单。fromJSON 方法读取 JSON 文件,如果 fromJSON 函数中的 flatten 参数设置为 TRUE,则返回一个 DataFrame。

练习 3:读取 JSON 文件并将数据存储在 DataFrame 中

在这个练习中,我们将读取 JSON 文件并将数据存储在 DataFrame 中。

执行以下步骤以完成练习:

  1. data.gov.in/catalog/area-production-productivity-spices-condiments-district-wise-tamil-nadu-year-2015-16 下载数据。

  2. 首先,使用以下命令安装读取 JSON 文件所需的包:

    install jsonlite package
    install.packages("jsonlite")
    library(jsonlite)
    
  3. 接下来,使用 fromJSON 方法读取 JSON 文件,如下所示:

    json_file <- "crop.json"
    json_data <- jsonlite::fromJSON(json_file, flatten = TRUE)
    
  4. 列表中的第二个元素包含包含作物生产价值的 DataFrame。从 json_data 中检索它,并将其存储为名为 crop_production 的 DataFrame:

    crop_production <- data.frame(json_data[[2]])
    
  5. 接下来,使用以下命令重命名列:

    colnames(crop_production) <- c("S.No","District","Area","Production","PTY")
    
  6. 现在,使用 head() 函数打印前六行:

    head(crop_production)
    

    输出如下:

    ##   S.No   District Area Production  PTY
    ## 1    1   Ariyalur   NA         NA   NA
    ## 2    2 Coimbatore  808         26 0.03
    ## 3    3  Cuddalore   NA         NA   NA
    ## 4    4 Dharmapuri   NA         NA   NA
    ## 5    5   Dindigul  231          2 0.01
    ## 6    6      Erode   NA         NA   NA
    

文本

非结构化数据是网络的通用语言。所有社交媒体、博客、网页以及许多其他信息来源都是文本形式且杂乱无章,难以从中提取任何有意义的信息。越来越多的研究工作来自 自然语言处理NLP)领域,其中计算机不仅变得更擅长理解单词的意义,而且还能理解句子中单词的使用上下文。计算机聊天机器人的兴起,它能对人类查询做出响应,是理解文本信息最复杂的形式。

在 R 中,我们将使用 tm 文本挖掘包来展示如何读取、处理和从文本数据中检索有意义的信息。我们将使用 Kaggle 上的 Amazon Food Review 数据集的小样本(www.kaggle.com/snap/amazon-fine-food-reviews)来练习本节的内容。

tm 包中,文本文档的集合被称为 tm 包的 VCorpusVCorpus 对象)。我们可以使用 inspect() 方法。以下练习使用 lapply 方法遍历前两条评论并将文本转换为字符。你将在 函数的 Apply 家族 部分了解更多关于 apply 家族函数的信息。

练习 4:读取具有文本列的 CSV 文件并将数据存储在 VCorpus 中

在这个练习中,我们将读取具有文本列的 CSV 文件并将数据存储在 VCorpus 中。

完成练习的步骤如下:

  1. 首先,让我们将 R 中的文本挖掘包加载到系统中以读取文本文件:

    library(tm)
    
  2. 现在,从文件中读取前 10 条评论:

    review_top_10 <- read.csv("Reviews_Only_Top_10_Records.csv")
    
  3. 要将文本列存储在 VCorpus 中,使用以下命令:

    review_corpus <- VCorpus(VectorSource(review_top_10$Text))
    
  4. 要检查前两条评论的结构,执行以下命令:

    inspect(review_corpus[1:2])
    

    输出如下:

    ## <<VCorpus>>
    ## Metadata:  corpus specific: 0, document level (indexed): 0
    ## Content:  documents: 2
    ## [[1]]
    ## <<PlainTextDocument>>
    ## Metadata:  7
    ## Content:  chars: 263
    ## [[2]]
    ## <<PlainTextDocument>>
    ## Metadata:  7
    ## Content:  chars: 190
    
  5. 使用 lapply 将第一条评论转换为字符并打印:

    lapply(review_corpus[1:2], as.character)
    ## $'1'
    ## [1] "I have bought several of the Vitality canned dog food products and have found them all to be of good quality. The product looks more like a stew than a processed meat and it smells better. My Labrador is finicky and she appreciates this product better than  most."
    ## $'2'
    ## [1] "Product arrived labeled as Jumbo Salted Peanuts...the peanuts were actually small sized unsalted. Not sure if this was an error or if the vendor intended to represent the product as \"Jumbo\".
    

我们将在稍后的部分重新访问 review_corpus 数据集,展示如何将非结构化文本信息转换为结构化表格数据。

除了 CSV、文本和 JSON 格式之外,还有许多其他数据格式,这取决于数据来源及其用途。R 拥有一系列丰富的库,可以帮助处理许多格式。R 可以导入不仅包括标准格式(除前三种之外)如 HTML 表格和 XML,还可以导入特定于分析工具的格式,如 SAS 和 SPSS。这种民主化导致了行业专家的重大迁移,他们之前在专有工具(成本高昂且通常只有大型企业才有)中工作,转向开源分析编程语言,如 R 和 Python。

为代码可重复性编写 R Markdown 文件

分析学的巨大成功是信息与知识网络在主题周围开始传播的结果。出现了更多的开源社区,开发者们愉快地与外界分享他们的工作,许多数据项目变得可重复。这种变化意味着一个人开始的工作很快就会被一个社区以许多不同的形式适应、即兴创作和修改,在它被完全不同的领域采用之前,这个领域与它最初出现的领域完全不同。想象一下,每个在会议上发表的研究工作都提交了一个易于重复的代码和数据集,以及他们的研究论文。这种变化正在加快想法与现实相遇的速度,创新将开始蓬勃发展。

现在,让我们看看如何在单个文件中创建这样的可重复工作,我们称之为R Markdown文件。在以下活动中,我们将演示如何在 RStudio 中创建一个新的 R Markdown 文件。有关 R Markdown 的详细介绍,请参阅rmarkdown.rstudio.com/lesson-1.html

在下一个活动中,你将把 练习 4 中显示的代码(使用 Text Column 读取 CSV 文件并将数据存储在 VCorpus 中)重新创建到 R Markdown 中。在 图 4.2 中观察,你刚刚在 R Markdown 中编写了说明和代码,当执行 Knit to Word 操作时,它将说明、代码及其输出整齐地交织到一个文档中。

活动 1:创建一个 R Markdown 文件来读取 CSV 文件并编写数据摘要

在这个活动中,我们将创建一个 R Markdown 文件来读取 CSV 文件,并在一个文档中打印数据的小型摘要:

执行以下步骤以完成活动:

  1. 打开 RStudio 并导航到R Markdown选项:图 1.4:在 RStudio 中创建新的 R Markdown 文件

    图 1.4:在 RStudio 中创建新的 R Markdown 文件
  2. 为文档提供标题作者名称,并将默认输出格式选择为Word图 1.5:使用 read.csv 方法读取数据

    图 1.5:使用 read.csv 方法读取数据
  3. 使用 read.csv() 方法读取 bank-full.csv 文件。

  4. 最后,使用summary方法将摘要打印到 word 文件中。

    输出如下:

图 1.6:使用 summary 方法后的最终输出

图 1.6:使用 summary 方法后的最终输出

注意

该活动的解决方案可以在第 438 页找到。

R 中的数据结构

在任何编程语言中,数据结构是存储信息并使其准备进一步处理的基本单元。根据数据类型,有各种形式的数据结构可用于存储处理。下一节中解释的每个数据结构都有其特征和适用性。

在本节中,我们将探索每个数据结构及其如何与我们的数据一起使用。

向量

c()方法,如下所示:

c_names <- c("S.No","District","Area","Production","PTY")

我们可以通过指定向量名称旁边的方括号中的索引来提取向量中的第二个值。让我们回顾以下代码,其中我们提取第二个索引的值:

c_names[2]

输出如下:

## [1] "District"

使用c()方法连接的字符串集合是一个向量。它可以存储同质的数据集合,如字符、整数或浮点值。在尝试用字符存储整数时,将发生隐式类型转换,将所有值转换为字符。

谨慎

注意,这不一定每次都是预期的行为。需要谨慎,尤其是在数据不干净时。否则可能会造成比通常编程错误更难找到的错误。

矩阵

矩阵是用于存储n维数据的更高维数据结构。它适用于存储表格数据。与向量类似,矩阵也只允许在其行和列中存储同质的数据集合。

以下代码生成 16 个来自参数为试验次数(size) = 100和成功概率等于0.4的二项分布的随机数。R 中的rbinom()方法用于生成此类随机数:

r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)

现在,要存储r_number为矩阵,请使用以下命令:

matrix(r_numbers, nrow = 4, ncol = 4)

输出如下:

##      [,1] [,2] [,3] [,4]
## [1,]   48   39   37   39
## [2,]   34   41   32   38
## [3,]   40   34   42   46
## [4,]   37   42   36   44

让我们扩展我们在练习 4中使用的文本挖掘示例,即使用 VCorpus 存储具有文本列的 CSV 文件,以了解矩阵在文本挖掘中的使用。

考虑以下两个评论。使用lapply将第一个评论转换为as.character并打印:

lapply(review_corpus[1:2], as.character)

输出如下:

## $'1'
## [1] "I have bought several of the Vitality canned dog food products and have found them all to be of good quality. The product looks more like a stew than a processed meat, and it smells better. My Labrador is finicky, and she appreciates this product better than  most."
## $'2'
## [1] "Product arrived labeled as Jumbo Salted Peanuts...the peanuts were actually small sized unsalted. Not sure if this was an error or if the vendor intended to represent the product as \"Jumbo\".

现在,在以下练习中,我们将转换数据以从这两段文本中删除停用词、空白和标点符号。然后我们将进行词干提取(both lookinglooked 将被缩减为 look)。此外,为了保持一致性,将所有文本转换为小写。

练习 5:对数据进行转换以便进行分析

在这个练习中,我们将对数据进行转换,使其可用于进一步分析。

完成练习的以下步骤:

  1. 首先,使用以下命令将数据中的所有字符转换为小写:

    top_2_reviews <- review_corpus[1:2]
    top_2_reviews <- tm_map(top_2_reviews,content_transformer(tolower))
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] "I have bought several of the vitality canned dog food products and have found them all to be of good quality. the product looks more like a stew than a processed meat and it smells better. my labrador is finicky, and she appreciates this product better than  most."
    
  2. 接下来,从数据中移除停用词,例如,athean 以及更多:

    top_2_reviews <- tm_map(top_2_reviews,removeWords, stopwords("english"))
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] "  bought several   vitality canned dog food products   found      good quality.  product looks  like  stew   processed meat   smells better.  labrador  finicky   appreciates  product better   ."
    
  3. 使用以下命令移除单词之间的额外空格:

    top_2_reviews <- tm_map(top_2_reviews,stripWhitespace)
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] " bought several vitality canned dog food products found good quality. product looks like stew processed meat smells better. labrador finicky appreciates product better ."
    
  4. 执行词干提取过程,这将只保留单词的词根。例如,lookinglooked 将变为 look

    top_2_reviews <- tm_map(top_2_reviews,stemDocument)
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] " bought sever vital can dog food product found good quality. product look like stew process meat smell better. labrador finicki appreci product better ."
    

    现在我们已经处理并清理了文本,我们可以创建一个文档矩阵,该矩阵仅存储两个评论中不同单词出现的频率。我们将演示如何计算评论中包含的每个单词。矩阵的每一行代表一个评论,列是不同的单词。大多数值都是零,因为并非每个评论都会包含所有单词。在这个例子中,稀疏度为 49%,这意味着矩阵中只有 51% 的值是非零的。

  5. 再次创建 as.matrix() 方法。矩阵包含两个文档(评论)和 37 个唯一单词。通过指定矩阵中的行和列索引或名称来检索文档中特定单词的计数。

  6. 现在,使用以下命令将结果存储在矩阵中:

    dtm_matrix <- as.matrix(dtm)
    
  7. 要找到矩阵的维度,即 2 个文档和 37 个单词,请使用以下命令:

    dim(dtm_matrix)
    

    输出结果如下:

    ## [1]  2 37
    
  8. 现在,打印矩阵的子集:

    dtm_matrix[1:2,1:7]
    

    输出结果如下:

    ##     Terms
    ## Docs "jumbo". actual appreci arriv better better. bought
    ##    1        0      0       1     0      1       1      1
    ##    2        1      1       0     1      0       0      0
    
  9. 最后,使用以下命令在文档 1 中计算单词 product

    dtm_matrix[1,"product"]
    

    输出结果如下:

    ## [1] 3
    

列表

虽然 vector 和 matrix 都是程序中用于各种计算的有用结构,但它们可能不足以存储现实世界的数据集,因为现实世界的数据集通常包含混合类型的数据,例如 CRM 应用程序中的客户表有两个列,一列是客户姓名,另一列是年龄。列表提供了一种结构,允许存储两种不同类型的数据。

在以下练习中,除了生成 16 个随机数外,我们还使用了 sample() 方法从英文字母表中生成 16 个字符。list 方法将整数和字符一起存储。

练习 6:使用列表方法存储整数和字符

在这个练习中,我们将使用 list 方法存储随机生成的数字和字符。随机数将使用 rbinom 函数生成,随机字符将从英文字母表 A-Z 中选择。

执行以下步骤来完成练习:

  1. 首先,生成参数大小为 100 且成功概率为 0.4 的二项分布的 16 个随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 现在,从英语 LETTERS 中选择 16 个字母,不重复:

    #sample() will generate 16 random letters from the English alphabet without repetition
    r_characters <- sample(LETTERS, size = 16, replace = FALSE)
    
  3. r_numbersr_characters 放入一个单独的列表中。list() 函数将创建包含 r_numbersr_characters 的数据结构列表:

    list(r_numbers, r_characters)
    

    输出结果如下:

    ## [[1]]
    ##  [1] 48 53 38 31 44 43 36 47 43 38 43 41 45 40 44 50
    ## 
    ## [[2]]
    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X"
    

    在以下步骤中,我们将看到一个包含整数和字符向量一起存储的列表。

  4. 现在,让我们将整数和字符向量存储和检索自一个列表:

    r_list <- list(r_numbers, r_characters)
    
  5. 接下来,使用以下命令检索字符向量中的值:

    r_list[[2]]
    

    输出如下:

    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X"
    
  6. 最后,检索字符向量中的第一个值:

    (r_list[[2]])[1]
    

    输出如下:

    ## [1] "V" 
    

    虽然这解决了存储异构数据类型的要求,但它仍然没有对两个向量中的值之间的关系进行任何完整性检查。如果我们想将每个字母分配给一个整数。在先前的输出中,V代表48C代表53,以此类推。

    列表无法很好地处理这种一对一映射。考虑以下代码,如果我们生成 18 个随机字符,而不是16个字符,它仍然允许将它们存储在列表中。现在最后两个字符没有与整数相关联的映射。

  7. 现在,生成参数大小等于100,成功概率等于0.4的来自二项分布的 16 个随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  8. 从英语LETTERS中选择任意 18 个字母,且不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  9. r_numbersr_characters放入一个单独的列表中:

    list(r_numbers, r_characters)
    

    输出如下:

    ## [[1]]
    ##  [1] 48 53 38 31 44 43 36 47 43 38 43 41 45 40 44 50
    ## 
    ## [[2]]
    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X" "P"  "Q"
    

活动二:创建两个矩阵的列表并访问其值

在这个活动中,你将创建两个矩阵,并使用矩阵的索引检索一些值。你还将执行乘法和减法等操作。

执行以下步骤以完成活动:

  1. 通过从二项分布(使用rbinom方法)随机生成数字来创建大小为10 x 44 x 5的两个矩阵。分别命名为mat_Amat_B

  2. 现在,将两个矩阵存储在一个列表中。

  3. 使用列表,访问mat_A的第 4 行第 2 列并将其存储在变量A中,以及访问mat_B的第 2 行第 1 列并将其存储在变量B中。

  4. AB矩阵相乘,并从mat_A的第 2 行第 1 列中减去。

    注意

    本活动的解决方案可在第 440 页找到。

DataFrame

由于向量、矩阵和列表的限制,对于数据科学从业者来说,需要一个适合现实世界数据集的数据结构,这是一个迫切需要的要求。DataFrame 是存储和检索表格数据的一种优雅方式。我们已经在练习 3读取 JSON 文件并将数据存储在 DataFrame 中中看到了 DataFrame 如何处理数据的行和列。DataFrame 将在整本书中广泛使用。

练习 7:使用 DataFrame 执行完整性检查

让我们回顾一下练习 6的第6 步使用列表方法存储整数和字符,其中我们讨论了在尝试将两个不等长向量存储在列表中时的完整性检查,并看看 DataFrame 是如何不同地处理它的。我们再次生成随机数字(r_numbers)和随机字符(r_characters)。

执行以下步骤以完成练习:

  1. 首先,生成 16 个来自参数大小等于100和成功概率等于0.4的二项分布的随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 从英语LETTERS中选择任何 18 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  3. r_numbersr_characters放入一个 DataFrame 中:

    data.frame(r_numbers, r_characters)
    

    输出如下:

    Error in data.frame(r_numbers, r_characters) : 
      arguments imply differing number of rows: 16, 18
    

    如您所见,前一个输出中的错误表明最后两个字母,即PQ,与使用二项分布生成的相应随机INTEGER没有映射。

访问 DataFrame 中的任何特定行和列与矩阵类似。我们将展示许多技巧和技术来最好地利用 DataFrame 中索引的强大功能,这还包括一些过滤选项。

DataFrame 中的每一行都是紧密耦合的列集合的结果。每一列清楚地定义了数据中的每一行与其他每一行之间的关系。如果没有相应的值在列中可用,它将被填充为 NA。例如,在 CRM 应用程序中,客户可能没有填写他们的婚姻状况,而其他一些客户填写了。因此,在应用程序设计期间指定哪些列是必需的,哪些是可选的变得至关重要。

数据表

随着 DataFrame 的日益普及,其局限性开始显现。特别是在处理大型数据集时,DataFrame 的表现不佳。在复杂分析中,我们经常创建许多中间 DataFrame 来存储结果。然而,R 建立在内存计算架构之上,并且高度依赖于 RAM。与磁盘空间不同,许多标准台式机和笔记本电脑的 RAM 限制在 4 或 8GB。DataFrame 在计算过程中没有高效地管理内存,这通常会导致“内存不足错误”,尤其是在处理大型数据集时。

为了处理这个问题,data.table继承了data.frame的功能,并在其基础上提供了快速且内存高效的版本,用于以下任务:

  • 文件读取器和写入器

  • 聚合

  • 更新

  • 等值、不等值、滚动、范围和区间连接

高效的内存管理使得开发快速,并减少了操作之间的延迟。以下练习展示了data.tabledata.frame相比在计算时间上的显著差异。首先,我们读取完整的fread()方法,这是data.table中快速读取方法之一。

练习 8:探索文件读取操作

在这个练习中,我们只展示文件读取操作。我们鼓励您测试其他功能(cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html)并比较数据表与 DataFrame 的能力。

执行以下步骤以完成练习:

  1. 首先,使用以下命令加载数据表包:

    library(data.table)
    
  2. 使用data.table包的fread()方法读取数据集:

    system.time(fread("Reviews_Full.csv"))
    

    输出如下:

    Read 14.1% of 568454 rows
    Read 31.7% of 568454 rows
    Read 54.5% of 568454 rows
    Read 72.1% of 568454 rows
    Read 79.2% of 568454 rows
    Read 568454 rows and 10 (of 10) columns from 0.280 GB file in 00:00:08
    ##    user  system elapsed 
    ##    3.62    0.15    3.78
    
  3. 现在,使用基础包中的read.csv()方法读取相同的 CSV 文件:

    system.time(read.csv("Reviews_Full.csv"))
    

    输出如下:

    ##    user  system elapsed 
    ##    4.84    0.05    4.91
    

注意,通过fread()方法读取它花费了3.78秒,而read.csv函数则花费了4.91秒。执行速度几乎快了30%。随着数据量的增加,这种差异变得更加显著。

在前面的输出中,user时间是当前 R 会话花费的时间,而system时间是操作系统完成该过程花费的时间。即使使用相同的数据集,执行system.time方法后也可能得到不同的值。这很大程度上取决于在运行方法时 CPU 有多忙。然而,我们应该将system.time方法的输出相对于我们进行的比较来读取,而不是相对于绝对值。

当数据集的大小太大时,我们有很多中间操作要完成最终输出。然而,请记住,data.table并不是一根魔杖,它不能让我们在 R 中处理任何大小的数据集。RAM 的大小仍然起着重要作用,data.table不能替代分布式和并行处理大数据系统。然而,即使是对于较小的数据集,data.table的使用也显示出比data.frames更好的性能。

数据处理和转换

到目前为止,我们已经看到了不同的读取和存储数据的方法。现在,让我们关注进行数据分析、提取见解或构建模型所需的数据处理和转换。原始数据几乎没有任何用途,因此将其处理成适合任何有用目的变得至关重要。本节重点介绍了 R 在数据分析中广泛使用的方法。

cbind

如其名所示,它通过列组合两个或多个向量、矩阵、DataFrame 或表。当我们需要将多个向量、矩阵或 DataFrame 组合成一个进行分析或可视化时,cbind非常有用。cbind的输出取决于输入数据。以下练习提供了几个cbind的例子,它结合了两个向量。

练习 9:探索 cbind 函数

在这个练习中,我们将实现cbind函数以组合两个 DataFrame 对象。

执行以下步骤以完成练习:

  1. 生成 16 个来自参数大小等于100和成功概率等于0.4的二项分布的随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 接下来,使用以下命令打印r_numbers值:

    r_numbers
    

    输出如下:

    ##  [1] 38 46 40 42 45 39 37 35 44 39 46 41 31 32 34 43
    
  3. 从英语LETTERS中选择任意 16 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  4. 现在,使用以下命令打印r_characters值:

    r_characters
    

    输出如下:

    ##  [1] "C" "K" "Z" "I" "E" "A" "X" "O" "H" "Y" "T" "B" "N" "F" "U" "V" "S"
    ## [18] "P"
    
  5. 使用cbind组合r_numbersr_characters

    cbind(r_numbers, r_characters)
    

    输出如下:

    ## Warning in cbind(r_numbers, r_characters): number of rows of result is not a multiple of vector length (arg 1)
    ##       r_numbers r_characters
    ##  [1,] "38"      "C"         
    ##  [2,] "46"      "K"         
    ##  [3,] "40"      "Z"         
    ##  [4,] "42"      "I"         
    ##  [5,] "45"      "E"         
    ##  [6,] "39"      "A"         
    ##  [7,] "37"      "X"         
    ##  [8,] "35"      "O"         
    ##  [9,] "44"      "H"         
    "
    
  6. 打印使用cbind后获得的类(数据结构类型):

    class(cbind(r_numbers, r_characters))
    

    输出如下:

    ## [1] "matrix"
    

    观察到在练习的第 5 步中cbind输出的警告信息:

    number of rows of result is not a multiple of vector length (arg 1)
    r_numbers r_characters
    

这个错误意味着r_numbersr_characters的长度不相同(分别是 16 和 18)。请注意,与as.data.frame()不同,cbind()方法不会抛出错误。相反,它会自动执行所谓的r_numbers 3848从顶部回收以填充第 17 和第 18 个索引。

考虑我们编写以下命令而不是:

cbind(as.data.frame(r_numbers), as.data.frame(r_characters))

现在会抛出一个错误,正如我们在数据框部分之前所展示的:

Error in data.frame(..., check.names = FALSE) : 
  arguments imply differing number of rows: 16, 18

需要始终检查维度和数据类型,否则可能会导致不希望的结果。当我们给出两个向量时,通过cbind默认创建一个矩阵。

注意

由于我们没有设置任何种子值,因此每次执行代码时,样本和rbinom的输出都会不同。

rbind

rbind类似于cbind,但它通过行而不是列进行组合。为了使rbind工作,两个数据框中的列数应该相等。当我们在具有相同列的原始数据集中附加额外的观测值时,它很有用,其中原始数据集的所有列都是相同的,并且顺序相同。让我们在以下练习中探索rbind

练习 10:探索rbind函数

在这个练习中,我们将使用rbind函数组合两个数据框。

执行以下步骤来完成练习:

  1. 生成 16 个来自二项分布的随机数,参数大小等于100,成功概率等于 0.4:

    r_numbers <- rbinom(n = 18, size = 100, prob = 0.4)
    
  2. 接下来,打印r_numbers的值:

    r_numbers
    

    输出如下:

    ##  [1] 38 46 40 42 45 39 37 35 44 39 46 41 31 32 34 43
    
  3. 从英语LETTERS中选择任何 16 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  4. 现在,使用以下命令打印r_characters

    r_characters
    

    输出如下:

    ##  [1] "C" "K" "Z" "I" "E" "A" "X" "O" "H" "Y" "T" "B" "N" "F" "U" "V" "S"
    ## [18] "P"
    
  5. 最后,使用rbind方法打印r_numbersr_characters的合并值:

    rbind(r_numbers, r_characters)
    

    输出如下:

    ##              [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11]
    ## r_numbers    "37" "44" "38" "38" "41" "35" "38" "40" "38" "45"  "37" 
    ## r_characters "Q"  "Y"  "O"  "L"  "A"  "G"  "V"  "S"  "B"  "U"   "D"  
    ##              [,12] [,13] [,14] [,15] [,16] [,17] [,18]
    ## r_numbers    "40"  "41"  "42"  "36"  "44"  "37"  "44" 
    ## r_characters "R"   "T"   "P"   "F"   "X"   "C"   "I"
    

从最后一步可以看出,rbind函数将r_numbersr_characters作为两行数据连接(绑定),这与cbind不同,在cbind中它是堆叠在两列中的。除了输出之外,cbind的所有其他规则也适用于rbind

合并函数

R 中的merge()函数在需要使用公共列(在数据库世界中我们称之为主键)连接多个数据框时特别有用。merge()函数对数据框和数据表有两种不同的实现方式,它们的行为基本上是相同的。

练习 11:探索合并函数

在这个练习中,我们将生成两个数据框,即df_onedf_two,使得r_numbers列在每个数据框中唯一标识每一行。

执行以下步骤来完成练习:

第一个数据框

  1. 使用set.seed()方法确保每次运行代码时生成相同的随机数:

    set.seed(100)
    
  2. 接下来,生成 1 到 30 之间的任意 16 个不重复的随机数:

    r_numbers <- sample(1:30,10, replace = FALSE)
    
  3. 从英文字母表中生成任意 16 个字符,允许重复:

    r_characters <- sample(LETTERS, 10, TRUE)
    
  4. r_numbersr_characters合并到一个名为df_one的 DataFrame 中:

    df_one <- cbind(as.data.frame(r_numbers), as.data.frame(r_characters))
    df_one
    

    输出如下:

    ##    r_numbers r_characters
    ## 1         10            Q
    ## 2          8            W
    ## 3         16            H
    ## 4          2            K
    ## 5         13            T
    ## 6         26            R
    ## 7         20            F
    ## 8          9            J
    ## 9         25            J
    ## 10         4            R
    

第二个 DataFrame

  1. 使用set.seed()方法来保留多次运行中相同的随机数:

    set.seed(200)
    
  2. 接下来,生成 1 到 30 之间的任意 16 个不重复的随机数:

    r_numbers <- sample(1:30,10, replace = FALSE)
    
  3. 现在,从英文字母表中生成任意 16 个字符,允许重复:

    r_characters <- sample(LETTERS, 10, TRUE)
    
  4. r_numbersr_characters合并到一个名为df_two的 DataFrame 中:

    df_two <- cbind(as.data.frame(r_numbers), as.data.frame(r_characters))
    df_two
    

    输出如下:

    ##    r_numbers r_characters
    ## 1         17            L
    ## 2         30            Q
    ## 3         29            D
    ## 4         19            Q
    ## 5         18            J
    ## 6         21            H
    ## 7         26            O
    ## 8          3            D
    ## 9         12            X
    ## 10         5            Q
    

一旦我们使用cbind()函数创建了df_onedf_twoDataFrame,我们就可以执行一些合并操作(将使用 JOIN 这个词,它与merge()的意思相同)。

现在,让我们看看不同类型的连接会产生不同的结果。

在数据库的世界中,JOIN 操作用于通过公共主键结合两个或两个以上的表。在数据库中,我们使用结构化查询语言(SQL)来执行 JOIN 操作。在 R 中,merge()函数帮助我们实现与 SQL 在数据库中提供的相同功能。此外,在这里我们不是使用表,而是使用 DataFrame,它也是一个具有行和列数据的表。

内连接

练习 11探索 merge 函数中,我们创建了两个 DataFrame:df_onedf_two。现在,我们将使用r_numbers列中两个 DataFrame 共有的26(行号7)来连接这两个 DataFrame,其中r_characters列中相应的字符在df_one中是R,在df_two中是O。在输出中,X对应于df_oneDataFrame,Y对应于df_twoDataFrame。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers")
##   r_numbers r_characters.x r_characters.y
## 1        26              R              O

左连接

df_oner_numbers列中添加<NA>作为值,无论在df_two中找不到相应的值。例如,对于r_number = 2,在df_two中没有值,而对于r_number = 26df_onedf_twor_characters列的值分别是RO

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all.x = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          2              K           <NA>
## 2          4              R           <NA>
## 3          8              W           <NA>
## 4          9              J           <NA>
## 5         10              Q           <NA>
## 6         13              T           <NA>
## 7         16              H           <NA>
## 8         20              F           <NA>
## 9         25              J           <NA>
## 10        26              R              O

右连接

df_oner_character列在找不到匹配项的地方是<NA>。同样,r_numbers = 26是唯一的匹配项。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all.y = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          3           <NA>              D
## 2          5           <NA>              Q
## 3         12           <NA>              X
## 4         17           <NA>              L
## 5         18           <NA>              J
## 6         19           <NA>              Q
## 7         21           <NA>              H
## 8         26              R              O
## 9         29           <NA>              D
## 10        30           <NA>              Q

全外连接

与左连接和右连接不同,r_numbers列从两个 DataFrame 中添加,并在r_characters列的相应 DataFrame 中添加<NA>。观察发现,只有r_number = 26这一行在两个 DataFrame 中都有值。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          2              K           <NA>
## 2          3           <NA>              D
## 3          4              R           <NA>
## 4          5           <NA>              Q
## 5          8              W           <NA>
## 6          9              J           <NA>
## 7         10              Q           <NA>
## 8         12           <NA>              X
## 9         13              T           <NA>
## 10        16              H           <NA>
## 11        17           <NA>              L
## 12        18           <NA>              J
## 13        19           <NA>              Q
…

reshape函数

数据通常在reshape函数中使用,经常用于在宽格式和长格式之间转换,以便进行各种操作,使数据对计算或分析有用。在许多可视化中,我们使用reshape()将宽格式转换为长格式,反之亦然。

我们将使用 Iris 数据集。这个数据集包含名为Sepal.LengthSepal.WidthPetal.LengthPetal.Width的变量,这些变量的测量值以厘米为单位,来自 3 种鸢尾花物种的每种 50 朵花,即setosaversicolorvirginica

练习 12:探索重塑函数

在这个练习中,我们将探索reshape函数。

执行以下步骤以完成练习:

  1. 首先,使用以下命令打印 iris 数据集的前五行:

    head(iris)
    

    上一条命令的输出如下:

    ##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    ## 1          5.1         3.5          1.4         0.2  setosa
    ## 2          4.9         3.0          1.4         0.2  setosa
    ## 3          4.7         3.2          1.3         0.2  setosa
    ## 4          4.6         3.1          1.5         0.2  setosa
    ## 5          5.0         3.6          1.4         0.2  setosa
    ## 6          5.4         3.9          1.7         0.4  setosa
    
  2. 现在,根据以下条件创建一个名为Type的变量。当Sepal.Width > 2Sepal Width <= 3时,我们将分配TYPE 1TYPE 2。类型列仅用于演示目的,没有特定的逻辑:

    iris$Type <- ifelse((iris$Sepal.Width>2 & iris$Sepal.Width <=3),"TYPE 1","TYPE 2")
    
  3. TypeSepal.WidthSpecies列存储在df_iris数据框中:

    df_iris <- iris[,c("Type","Sepal.Width","Species")]
    
  4. 接下来,使用以下reshape命令将df_iris重塑为宽数据框:

    reshape(df_iris,idvar = "Species", timevar = "Type", direction = "wide")
    

    输出如下:

    ##        Species Sepal.Width.TYPE 2 Sepal.Width.TYPE 1
    ## 1       setosa                3.5                3.0
    ## 51  versicolor                3.2                2.3
    ## 101  virginica                3.3                2.7
    

    当运行reshape命令时,你会得到一个警告,如下所示:

    multiple rows match for Type=TYPE 2: first taken multiple rows match for Type=TYPE 1: first taken
    

这个警告意味着三种物种的Type 1Type 2有多个值,所以重塑选择了每种物种的第一个出现。在这种情况下,行号151101。我们现在将看到如何在aggregate函数中更好地处理这种转换。

聚合函数

聚合是一种有用的计算统计方法,如计数、平均值、标准差和四分位数,它还允许编写自定义函数。在下面的代码中,每个鸢尾花物种的公式(在 R 中,公式是数据结构的名字,而不是数学方程)计算了萼片和花瓣宽度和长度的数值测量值的平均值。聚合函数的第一个参数是一个公式,它接受物种和所有其他测量值,从所有观测值中计算平均值。

aggregate(formula =. ~ Species, data = iris, FUN = mean)

上一条命令的输出如下:

##      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 1     setosa        5.006       3.428        1.462       0.246
## 2 versicolor        5.936       2.770        4.260       1.326
## 3  virginica        6.588       2.974        5.552       2.026

apply函数族

如果必须讨论 R 编程的几个强大功能,apply函数族将会被提及。它通常被用来避免使用 R 中可用的循环结构,如forwhile

首先,在 R 中运行for循环比较慢,其次,R 中apply函数的实现基于高效的编程语言,如 C/C++,这使得循环非常快。

apply函数族中有许多函数。根据所需的输入和输出结构,我们选择合适的函数:

  • apply()

  • lapply()

  • sapply()

  • vapply()

  • mapply()

  • rapply()

  • tapply()

我们将在本节讨论几个。

apply函数

apply() 函数接受一个数组,包括一个矩阵,作为输入,并返回通过将一个函数应用于数组的边缘或矩阵的边缘得到的值向量、数组或列表。

练习 13:实现 apply 函数

在这个练习中,我们将计算从英语字母表中随机生成的 100 x 100 矩阵中每一列的元音字母数量。MARGIN = 1 函数将扫描每一行,而 MARGIN = 2 将指定列。相同的函数将计算每一行的元音字母数量。

执行以下步骤以完成练习:

  1. 使用以下命令创建一个 100 x 100 的随机字母矩阵(ncol 是列数,nrow 是行数):

    r_characters <- matrix(sample(LETTERS, 10000, replace = TRUE), ncol = 100, nrow = 100)
    
  2. 现在,创建一个名为 c_vowel 的函数来计算给定数组中的元音字母数量:

    c_vowel <- function(x_char){
      return(sum(x_char %in% c("A","I","O","U")))
    }
    
  3. 接下来,使用 apply 函数遍历矩阵的每一列,并使用此处所示的 c_vowel 函数:

    apply(r_characters, MARGIN = 2, c_vowel)
    

    输出如下:

    ##   [1] 17 16 10 11 12 25 16 14 14 12 20 13 16 14 14 20 10 12 11 16 10 20 15
    ##  [24] 10 14 13 17 14 14 13 15 19 18 21 15 13 19 21 24 18 13 20 15 15 15 19
    ##  [47] 13  6 18 11 16 16 11 13 20 14 12 17 11 14 14 16 13 11 23 14 17 14 22
    ##  [70] 11 18 10 18 21 19 14 18 12 13 15 16 10 15 19 14 13 16 15 12 12 14 10
    ##  [93] 16 16 20 16 13 22 15 15
    

lapply 函数

lapply 函数看起来与 apply() 函数相似,不同之处在于它以 列表 作为输入并返回 列表 作为输出。在以下练习中重写我们之前的示例后,类函数的输出显示输出是一个列表。

练习 14:实现 lapply 函数

在这个练习中,我们将取一个向量列表并计算元音字母的数量。

执行以下步骤以完成练习:

  1. 创建一个包含两个随机字母向量的列表,每个向量的大小为 100:

    r_characters <- list(a=sample(LETTERS, 100, replace = TRUE),
                         b=sample(LETTERS, 100, replace = TRUE))
    
  2. 使用 lapply 函数遍历列表 ab,并使用 c_vowel 函数从列表中计算元音字母的数量:

    lapply(r_characters, c_vowel)
    

    输出如下:

    ## $a
    ## [1] 19
    ## $b
    ## [1] 10
    
  3. 检查输出的类(类型)。class() 函数提供数据结构的类型:

    out_list <- lapply(r_characters, c_vowel)
    class(out_list)
    

    输出如下:

    ## [1] "list"
    

sapply 函数

sapply 函数是 lapply 函数的一个包装器,其中输出是一个向量或矩阵而不是列表。在以下代码中,观察应用 sapply 差异后的输出类型。输出返回一个整数向量,我们可以通过 class() 函数来检查:

sapply(r_characters, c_vowel)
##  a  b 
## 19 10

要打印输出类的信息,请使用以下命令:

out_vector <- sapply(r_characters, c_vowel)
class(out_vector)

之前命令的输出如下:

## [1] "integer"

tapply 函数

将一个函数应用于稀疏数组的每个单元格,即应用于由某些因素级别的唯一组合给出的每个(非空)值组。当涉及到在数据子集级别工作时,tapply 函数非常有用。例如,在我们的 aggregate 函数中,如果我们想要获取 Iris 物种类型的标准差聚合,我们可以使用 tapply。以下代码显示了如何使用 tapply 函数:

首先,计算每个 Iris 物种花瓣长度的标准差:

tapply(iris$Sepal.Length, iris$Species,sd)

输出如下:

##     setosa versicolor  virginica 
##  0.3524897  0.5161711  0.6358796

接下来,计算每个 Iris 物种花瓣宽度的标准差:

tapply(iris$Sepal.Width, iris$Species,sd)

之前命令的输出如下:

##     setosa versicolor  virginica 
##  0.3790644  0.3137983  0.3224966

现在,让我们探索一些在构建复杂数据处理方法、机器学习模型或数据可视化时可能很有价值的流行且有用的 R 包。

有用的包

尽管 CRAN 仓库中有超过十三万个包,但其中一些包在某些主要功能方面具有独特的位置和用途。到目前为止,我们看到了许多数据操作示例,如连接、聚合、重塑和子集。接下来我们将讨论的 R 包将提供大量函数,提供广泛的数据处理和转换能力。

dplyr 包

dplyr包通过五种不同的方法帮助解决最常见的数据操作挑战,即mutate()select()filter()summarise()arrange()。让我们回顾一下来自 UCI 机器学习仓库的葡萄牙银行机构的直接营销活动(电话)数据集,以测试所有这些方法。

在以下练习中的%>%符号被称为链操作符。一个操作的结果被发送到下一个操作,而不需要显式创建一个新变量。这种链式操作是存储高效的,并且使代码的可读性变得容易。

练习 15:实现 dplyr 包

在这个练习中,我们感兴趣的是了解从事蓝领工作的人的平均银行余额,根据他们的婚姻状况。使用dplyr包中的函数来获取答案。

执行以下步骤以完成练习:

  1. 使用read.csv()函数将bank-full.csv文件导入到df_bank_detail对象中:

    df_bank_detail <- read.csv("bank-full.csv", sep = ';')
    
  2. 现在,使用以下命令加载dplyr库:

    library(dplyr)
    
  3. 选择(过滤)所有job列包含值blue-collar的观测值,然后按婚姻状况分组生成汇总统计量,即mean

    df_bank_detail %>%
      filter(job == "blue-collar") %>%
      group_by(marital) %>%
      summarise(
        cnt = n(),
        average = mean(balance, na.rm = TRUE)
      )
    

    输出如下:

    ## # A tibble: 3 x 3
    ##    marital   cnt   average
    ##     <fctr> <int>     <dbl>
    ## 1 divorced   750  820.8067
    ## 2  married  6968 1113.1659
    ## 3   single  2014 1056.1053
    
  4. 让我们找出具有中等教育水平和默认值为yes的客户的银行余额:

    df_bank_detail %>%
      mutate(sec_edu_and_default = ifelse((education == "secondary" & default == "yes"), "yes","no")) %>%
      select(age, job, marital,balance, sec_edu_and_default) %>%
      filter(sec_edu_and_default == "yes") %>%
      group_by(marital) %>%
      summarise(
        cnt = n(),
        average = mean(balance, na.rm = TRUE)
      )
    

    输出如下:

    ## # A tibble: 3 x 3
    ##    marital   cnt    average
    ##     <fctr> <int>      <dbl>
    ## 1 divorced    64   -8.90625
    ## 2  married   243  -74.46914
    ## 3   single   151 -217.43046
    

许多复杂分析都可以轻松完成。请注意,mutate()方法有助于创建具有特定计算或逻辑的自定义列。

tidyr 包

tidyr包有三个基本函数——gather()separate()spread()——用于清理混乱的数据。

gather()函数通过将多个列取出来并将它们聚合成键值对,将DataFrame 转换为长 DataFrame。

练习 16:实现 tidyr 包

在这个练习中,我们将探索tidyr包及其相关函数。

执行以下步骤以完成练习:

  1. 使用以下命令导入tidyr库:

    library(tidyr)
    
  2. 接下来,使用以下命令将seed设置为 100:

    set.seed(100)
    
  3. 创建一个r_name对象,并将 5 个人名存储在其中:

    r_name <- c("John", "Jenny", "Michael", "Dona", "Alex")
    
  4. 对于r_food_A对象,生成 1 到 30 之间的 16 个不重复的随机数:

    r_food_A <- sample(1:150,5, replace = FALSE)
    
  5. 类似地,对于r_food_B对象,生成 1 到 30 之间的 16 个不重复的随机数:

    r_food_B <- sample(1:150,5, replace = FALSE)
    
  6. 使用以下命令创建并打印 DataFrame 中的数据:

    df_untidy <- data.frame(r_name, r_food_A, r_food_B)
    df_untidy
    

    输出如下:

    ##    r_name r_food_A r_food_B
    ## 1    John       47       73
    ## 2   Jenny       39      122
    ## 3 Michael       82       55
    ## 4    Dona        9       81
    ## 5    Alex       69       25
    
  7. 使用tidyr包中的gather()方法:

    df_long <- df_untidy %>%
      gather(food, calories, r_food_A:r_food_B)
    df_long
    

    输出如下:

    ##     r_name     food calories
    ## 1     John r_food_A       47
    ## 2    Jenny r_food_A       39
    ## 3  Michael r_food_A       82
    ## 4     Dona r_food_A        9
    ## 5     Alex r_food_A       69
    ## 6     John r_food_B       73
    ## 7    Jenny r_food_B      122
    ## 8  Michael r_food_B       55
    ## 9     Dona r_food_B       81
    ## 10    Alex r_food_B       25
    
  8. spread()函数与gather()函数相反,即它将长格式转换为宽格式:

    df_long %>%
      spread(food,calories)
    ##    r_name r_food_A r_food_B
    ## 1    Alex       69       25
    ## 2    Dona        9       81
    ## 3   Jenny       39      122
    ## 4    John       47       73
    ## 5 Michael       82       55
    
  9. separate()函数在列是值组合的地方很有用,用于将其作为其他目的的关键列。如果它有一个共同的分隔符字符,我们可以分离出关键部分:

    key <- c("John.r_food_A", "Jenny.r_food_A", "Michael.r_food_A", "Dona.r_food_A", "Alex.r_food_A", "John.r_food_B", "Jenny.r_food_B", "Michael.r_food_B", "Dona.r_food_B", "Alex.r_food_B")
    calories <- c(74, 139, 52, 141, 102, 134, 27, 94, 146, 20)
    df_large_key <- data.frame(key,calories)  
    df_large_key
    

    输出如下:

    ##                 key calories
    ## 1     John.r_food_A       74
    ## 2    Jenny.r_food_A      139
    ## 3  Michael.r_food_A       52
    ## 4     Dona.r_food_A      141
    ## 5     Alex.r_food_A      102
    ## 6     John.r_food_B      134
    ## 7    Jenny.r_food_B       27
    ## 8  Michael.r_food_B       94
    ## 9     Dona.r_food_B      146
    ## 10    Alex.r_food_B       20
    df_large_key %>%
      separate(key, into = c("name","food"), sep = "\\.")
    ##       name     food calories
    ## 1     John r_food_A       74
    ## 2    Jenny r_food_A      139
    ## 3  Michael r_food_A       52
    ## 4     Dona r_food_A      141
    ## 5     Alex r_food_A      102
    ## 6     John r_food_B      134
    ## 7    Jenny r_food_B       27
    ## 8  Michael r_food_B       94
    ## 9     Dona r_food_B      146
    ## 10    Alex r_food_B       20
    

活动三:使用 dplyr 和 tidyr 从银行数据创建包含所有数值变量的五个汇总统计的 DataFrame

这个活动将使你习惯于从银行数据中选择所有数值字段,并对数值变量生成汇总统计。

执行以下步骤以完成活动:

  1. 使用select()从银行数据中提取所有数值变量。

  2. 使用summarise_all()方法,计算最小值、第一四分位数、第三四分位数、中位数、平均值、最大值和标准差。

    注意

    你可以在www.rdocumentation.org/packages/dplyr/versions/0.5.0/topics/summarise_all了解更多关于summarise_all函数的信息。

  3. 将结果存储在名为df_wide的宽格式 DataFrame 中。

  4. 现在,要将宽格式转换为深格式,请使用tidyr包中的gatherseparatespread函数。

  5. 最终输出应该为每个变量一行,以及最小值、第一四分位数、第三四分位数、中位数、平均值、最大值和标准差的每一列。

    一旦完成活动,你应该得到以下最终输出:

    ## # A tibble: 4 x 8
    ##        var   min   q25 median   q75    max       mean         sd
    ## *    <chr> <dbl> <dbl>  <dbl> <dbl>  <dbl>      <dbl>      <dbl>
    ## 1      age    18    33     39    48     95   40.93621   10.61876
    ## 2  balance -8019    72    448  1428 102127 1362.27206 3044.76583
    ## 3 duration     0   103    180   319   4918  258.16308  257.52781
    ## 4    pdays    -1    -1     -1    -1    871   40.19783  100.12875
    

    注意

    本活动的解决方案可以在第 440 页找到。

plyr 包

我们通过apply函数看到的情况可以通过plyr包在更大规模和稳健性上完成。plyr包提供了将数据集拆分为子集、对每个子集应用公共函数以及将结果合并为单个输出的能力。使用plyr而不是apply函数的优势包括以下特点:

  • 代码执行速度

  • 使用foreach循环进行处理的并行化。

  • 支持列表、DataFrame 和矩阵。

  • 更好的错误调试

plyr包中所有的函数名都是根据输入和输出明确定义的。例如,如果输入是 DataFrame,输出是列表,则函数名将是dlply

下图来自《数据分析的 Split-Apply-Combine 策略》论文,显示了所有不同的plyr函数:

图 1.7:plyr 包中的函数

图 1.7:plyr 包中的函数

_表示输出将被丢弃。

练习 17:探索 plyr 包

在这个练习中,我们将看到如何通过控制输入和输出的灵活性,split-apply-combine 方法使事情变得简单。

执行以下步骤以完成练习:

  1. 使用以下命令加载 plyr 包:

    library(plyr)
    
  2. 接下来,使用我们在早期示例 练习 13探索 apply 函数 中创建的 c_vowel 函数的略微修改版本:

    c_vowel <- function(x_char){
      return(sum(as.character(x_char[,"b"]) %in% c("A","I","O","U")))
    }
    
  3. seed 设置为 101

    set.seed(101)
    
  4. 将值存储在 r_characters 对象中:

    r_characters <- data.frame(a=rep(c("Split_1","Split_2","Split_3"),1000),
                         b= sample(LETTERS, 3000, replace = TRUE))
    

    注意

    输入 = DataFrame 输出 = 列表

  5. 使用 dlply() 函数并按行格式打印拆分:

    dlply(r_characters, c_vowel)
    

    输出如下:

    ## $Split_1
    ## [1] 153
    ## 
    ## $Split_2
    ## [1] 154
    ## 
    ## $Split_3
    ## [1] 147
    

    注意

    输入 = data.frame 输出 = 数组

  6. 我们可以简单地用 daply() 函数替换 dlply,并按列格式作为数组打印拆分:

    daply(r_characters, c_vowel)
    

    输出如下:

    ## Split_1 Split_2 Split_3 
    ##     153     154     147
    

    注意

    输入 = DataFrame 输出 = DataFrame

  7. 使用 ddply() 函数并打印拆分:

    ddply(r_characters, c_vowel)
    

    输出如下:

    ##         a  V1
    ## 1 Split_1 153
    ## 2 Split_2 154
    ## 3 Split_3 147
    

在步骤 5、6 和 7 中,观察我们如何创建了一个列表、数组以及作为 DataFrame 输出的数据。我们只需使用 plyr 的不同函数即可。这使得在许多可能的组合之间进行类型转换变得容易。

caret

caret 包特别适用于构建预测模型,它提供了一个无缝地跟随整个预测模型构建过程的框架。从数据拆分到训练和测试数据集以及变量重要性估计,我们将在回归和分类章节中广泛使用 caret 包。总之,caret 提供了以下工具:

  • 数据拆分

  • 预处理

  • 特征选择

  • 模型训练

  • 使用重采样进行模型调优

  • 变量重要性估计

我们将在 第四章回归第五章分类 中通过示例重新访问 caret 包。

数据可视化

我们所说的 ggplot2 是 R 中一个强大的包的一个基本部分。就像 dplyrplyr 一样,ggplot2 建立在 图形语法 的基础上,这是一种使我们能够简洁地描述图形组件的工具。

注意

良好的语法将使我们能够深入了解复杂图形的组成,并揭示看似不同图形之间意想不到的联系。

(Cox 1978) [Cox, D. R. (1978), "Some Remarks on the Role in Statistics of Graphical Methods," Applied Statistics, 27 (1), 4–9. [3,26].

散点图

散点图是一种使用笛卡尔坐标系显示数据集中通常两个变量的值的一种图表或数学图。如果点被着色,则可以显示一个额外的变量。

这是最常见的图表类型,在发现数据中的模式,尤其是在两个变量之间时非常有用。我们将再次使用我们的银行数据来进行一些 EDA。让我们使用葡萄牙银行直接营销活动数据集进行可视化:

df_bank_detail <- read.csv("bank-full.csv", sep = ';')

ggplot 以分层的方式堆叠图表的不同元素。在以下本节的示例中,在第一层,我们向 ggplot() 方法提供数据,然后使用诸如 xy 轴等美学细节进行映射,在示例中,分别是 agebalance 值。最后,为了能够识别与少数高银行余额相关的一些推理,我们根据工作类型添加了颜色。

执行以下命令以绘制年龄和余额的散点图:

ggplot(data = df_bank_detail) +
  geom_point(mapping = aes(x = age, y = balance, color = job))

![图 1.8:年龄和余额的散点图。

![img/C12624_01_08.jpg]

图 1.8:年龄和余额的散点图。

图 1.8 中,银行余额随年龄分布看起来非常正常,中年人显示出较高的银行余额,而年轻人和老年人位于光谱的较低端。

有趣的是,一些异常值似乎来自管理层和退休的专业人士。

在数据可视化中,看到图表并迅速得出结论总是很有诱惑力。数据可视化是为了更好地消费数据,而不是为了绘制因果推断。通常,分析师的解释总是要经过商业的审查。美观的图表往往会诱使你将其放入演示文稿中。所以,下次一个漂亮的图表进入你的演示文稿时,仔细分析你将要说的内容。

按婚姻状况分割的年龄和余额散点图

在本节中,我们将绘制三个散点图,这些散点图在年龄和余额按婚姻状况分割的单一图表中(每个单身、离婚和已婚个体一个)。

现在,你可以根据婚姻状况来分割分布。单身、已婚和离婚个体的模式似乎是一致的。我们使用了一种称为 facet_wrap() 的方法作为 ggplot 的第三层。它接受一个 marital 变量作为公式:

ggplot(data = df_bank_detail) +
  geom_point(mapping = aes(x = age, y = balance, color = job)) +
  facet_wrap(~ marital, nrow = 1)

![图 1.9:按婚姻状况分割的年龄和余额散点图

![img/C12624_01_09.jpg]

图 1.9:按婚姻状况分割的年龄和余额散点图

线形图

线形图或线形图是一种图表类型,它通过一系列称为 标记 的数据点显示信息,这些点由直线段连接。

ggplot 使用优雅的 geom() 方法,这有助于快速在两个视觉对象之间切换。在前面的示例中,我们看到了用于散点图的 geom_point()。在线形图中,观察结果按 x 轴上变量的顺序通过线连接。围绕线的阴影区域代表 95% 的置信区间,即有 95%的置信度认为实际的回归线位于阴影区域内。我们将在 第四章回归 中对此概念进行更多讨论。

在以下图表中,我们展示了单身、已婚和离婚个体的年龄和银行余额的线形图。是否有一些趋势尚不清楚,但可以看到三个类别之间的模式:

ggplot(data = df_bank_detail) +
  geom_smooth(mapping = aes(x = age, y = balance, linetype = marital))
## 'geom_smooth()' using method = 'gam'

![图 1.10:年龄和余额的线形图

![img/C12624_01_10.jpg]

图 1.10:年龄和余额的折线图

直方图

直方图是由矩形组成的可视化,其面积与变量的频率成正比,其宽度等于组距。

在直方图中,柱状的高度代表每个组中的观测值数量。在以下示例中,我们正在计算每种工作类型和婚姻状况的观测值数量。y是一个二进制变量,检查客户是否响应活动号召订阅了定期存款()。

看起来蓝领工人对活动号召的反应最少,而管理岗位的人对定期存款的订阅最多:

ggplot(data = df_bank_detail) +
  geom_bar(mapping = aes(x=job, fill = y)) +
  theme(axis.text.x = element_text(angle=90, vjust=.8, hjust=0.8))

![图 1.11:计数和工作的直方图图片

图 1.11:计数和工作的直方图

箱线图

箱线图是基于五个数值摘要(最小值、第一四分位数(Q1)、中位数、第三四分位数(Q3)和最大值)显示数据分布的一种标准化方式。箱线图可能是唯一一种与其他图表相比,在美观的表示中封装了更多信息图表。观察每个job类型对age变量的摘要。五个摘要统计量,即最小值、第一四分位数、中位数、均值、第三四分位数和最大值,通过箱线图简洁地描述。

第一四分位数和第三四分位数中的第 25 百分位数和第 75 百分位数分别由下四分位数和上四分位数表示。上四分位数,即从四分位数延伸到最大值的范围,在 1.5 * IQR(四分位距)内,这里的 IQR 是指四分位距,即两个四分位数之间的距离。下四分位数的情况类似。所有位于四分位数之外的数据点被称为异常值

tapply(df_bank_detail$age, df_bank_detail$job, summary)

输出如下:

## $admin.
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   20.00   32.00   38.00   39.29   46.00   75.00 
## 
## $'blue-collar'
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   20.00   33.00   39.00   40.04   47.00   75.00 
## 
## $entrepreneur
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   21.00   35.00   41.00   42.19   49.00   84.00 
## 
## $housemaid
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.00   38.00   47.00   46.42   55.00   83.00 
## 
## $management
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   21.00   33.00   38.00   40.45   48.00   81.00 
## 
## $retired
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   24.00   56.00   59.00   61.63   67.00   95.00 
## 
## $'self-employed'
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.00   33.00   39.00   40.48   48.00   76.00 
## 
0

在下面的箱线图中,我们正在查看每个工作类型的年龄摘要。在geom_boxplot中的varwidth = TRUE设置下,箱的大小显示了特定工作类型中的观测值数量。箱越宽,观测值数量就越多:

ggplot(data = df_bank_detail, mapping = aes(x=job, y = age, fill = job)) +
  geom_boxplot(varwidth = TRUE) +
  theme(axis.text.x = element_text(angle=90, vjust=.8, hjust=0.8))

![图 1.12:年龄和工作的箱线图图片

图 1.12:年龄和工作的箱线图

摘要

在本章中,我们介绍了 R 编程的基础数据类型、数据结构和重要的函数及包。我们描述了向量、矩阵、列表、DataFrame 和数据表作为存储数据的不同形式。在数据处理和转换空间中,我们探讨了cbindrbindmergereshapeaggregateapply函数族。

我们还讨论了 R 中最重要的包,如dplyrtidyrplyr。最后,使用ggplot2数据可视化包展示了各种可视化类型以及如何从中提取洞察。

在下一章中,我们将利用本章所学的一切来执行探索性数据分析(EDA)。在 EDA 中,您在这里学到的数据转换和可视化技巧将有助于从数据中得出推断。

第二章:数据的探索性分析

学习目标

到本章结束时,您将能够:

  • 使用行业标准框架定义问题陈述

  • 执行单变量和多变量分析

  • 解释多元分析

  • 执行假设检验

  • 使用 dplyr 和 reshape2 包进行数据整理

  • 使用 ggplot2 包可视化数据

在本章中,我们将向学习者介绍清洁、转换和可视化数据的技术,以便获得有用的见解。

简介

第一章R 高级分析,向您介绍了 R 语言及其数据科学生态系统。我们现在准备进入数据科学和机器学习的关键部分,那就是探索性数据分析EDA),理解数据的艺术。

在本章中,我们将使用与上一章相同的银行数据集来处理 EDA,但以更问题为中心的方式。我们将首先使用行业标准工件定义问题陈述,为问题设计解决方案,并学习 EDA 如何适应更大的问题框架。我们将使用 R 中的数据工程、数据整理和可视化技术,结合以业务为中心的方法,来处理葡萄牙银行机构的直接营销活动(电话)用例的 EDA。

在任何数据科学用例中,理解数据占据了大部分的时间和精力。大多数数据科学专业人士花费大约 80%的时间在理解数据上。鉴于这是您旅程中最关键的部分,对任何数据科学用例的整体过程有一个宏观的视角是很重要的。

典型的数据科学用例遵循核心业务分析问题或机器学习问题的路径。无论采取哪种路径,EDA 都是不可避免的。图 2.1展示了基本数据科学用例的生命周期。它从使用一个或多个标准框架定义问题陈述开始,然后深入数据收集并达到 EDA。在任何项目中,大部分的努力和时间都消耗在 EDA 上。一旦理解数据的过程完成,项目可能会根据用例的范围采取不同的路径。在大多数基于业务分析的使用案例中,下一步是将所有观察到的模式综合成有意义的见解。尽管这可能听起来很 trivial,但它是一个迭代且艰巨的任务。这一步随后演变为讲故事,其中浓缩的见解被定制成对业务利益相关者有意义的叙述。同样,在目标是为了开发预测模型的情况下,下一步将是实际开发机器学习模型并将其部署到生产系统/产品中。

图 2.1:数据科学用例的生命周期

图 2.1:数据科学用例的生命周期

让我们简要地看看第一步,定义问题陈述

定义问题陈述

如果您还记得我们在第一章中探讨的数据,高级分析中的 R,银行营销数据,我们有一个数据集,该数据集捕捉了银行进行的吸引客户的电话营销活动。

一家大型跨国银行正在设计一项营销活动,通过吸引客户存款来实现其增长目标。该活动在吸引客户方面效果不佳,营销团队希望了解如何改进活动以达到增长目标。

我们可以从业务利益相关者的角度重新构建问题,并尝试了解哪种解决方案最适合这里。

问题-设计工件

正如软件工程和其他工业项目有多个框架、模板和工件一样,数据科学和商业分析项目也可以使用行业标准工件有效地表示。一些流行的选择来自咨询巨头如麦肯锡、BCG,以及决策科学巨头如穆西伽。我们将使用一个基于Minto 金字塔原则的流行框架,称为情境-复杂性问题分析SCQ)。

让我们尝试以下结构来定义问题陈述:

  • 情境:定义当前情况。我们可以通过回答问题来简化这一点——发生了什么?

    一家大型跨国银行正在设计一项营销活动,通过吸引客户存款来实现其增长目标。该活动在吸引客户方面效果不佳,营销团队希望了解如何改进活动以达到增长目标。

在上一节中,我们看到了一个为银行数据用例构建的假设商业问题。尽管在现实中可能有所不同,但我们确实在尝试解决一个有效的用例。通过以前格式展示的格式表示问题陈述,我们有一个明确的关注和解决问题的领域。这解决了典型数据科学用例生命周期中的第一步。第二步是数据收集,我们在上一章中探讨了这一点。我们将参考由 UCI 机器学习存储库提供的相同数据集,网址为archive.ics.uci.edu/ml/datasets/Bank%20Marketing

备注

[Moro et al., 2014] S. Moro, P. Cortez, and P. Rita. A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, Elsevier, 62:22-31, June 2014.

这将我们带到了最后一步:EDA。在这个用例中,我们想要了解导致活动表现不佳的各种因素。在我们深入实际练习之前,让我们花点时间以更直观的方式理解 EDA 的概念。

理解 EDA 背后的科学

用通俗易懂的话来说,我们可以将 EDA 定义为理解数据的科学。更正式的定义是分析并探索数据集的过程,使用统计、视觉、分析或这些技术的组合来总结其特征、属性和潜在关系。

为了巩固我们的理解,让我们进一步分解定义。数据集是数值特征和分类特征的组合。为了研究数据,我们可能需要单独探索特征,而为了研究关系,我们可能需要一起探索特征。根据特征的数量和类型,我们可能会遇到不同类型的探索性数据分析(EDA)。

为了简化,我们可以将 EDA 的过程大致分为以下几类:

  • 单变量分析:研究单个特征

  • 双变量分析:研究两个特征之间的关系

  • 多元分析:研究两个以上特征之间的关系

现在,我们将本章的范围限制在单变量双变量分析上。在接下来的章节中,我们将介绍一些多元分析形式,如回归。

为了完成之前提到的每一项分析,我们可以使用可视化技术,如箱线图、散点图和条形图;统计技术,如假设检验;或者简单的分析技术,如平均值、频率计数等。

进一步细分,我们还有一个维度需要考虑,那就是特征的类型——数值分类。在提到的每种分析类型中——单变量双变量——根据特征类型,我们可能有不同的可视化技术来完成研究。因此,对于数值变量的单变量分析,我们可以使用直方图或箱线图,而对于分类变量,我们可能使用频率条形图。我们将以懒编程的方式深入了解 EDA 的整体练习,也就是说,我们将探索书中分析出现的上下文和细节。

在为练习设置基本背景后,让我们为特定的 EDA 练习做好准备。

探索性数据分析

我们将从可以从 UCI ML 存储库下载的数据集开始,该存储库的网址为archive.ics.uci.edu/ml/datasets/Bank%20Marketing

下载 ZIP 文件,将其解压到工作空间中的一个文件夹中,并使用名为bank-additional-full.csv的文件。要求学生启动一个新的 Jupyter 笔记本或他们选择的 IDE,并将数据加载到内存中。

练习 18:研究数据维度

让我们快速使用上一章中探索的简单命令加载数据,并查看数据集的一些基本特征。

我们正在探索数据的长度和宽度,即行数和列数、每列的名称、每列的数据类型以及每列存储内容的概览。

执行以下步骤以探索银行数据集:

  1. 首先,在 RStudio 中导入所有必需的库:

    library(dplyr)
    library(ggplot2)
    library(repr)
    library(cowplot)
    
  2. 现在,使用option方法将图表的widthheight分别设置为124

    options(repr.plot.width=12, repr.plot.height=4)
    

    确保您下载并将bank-additional-full.csv文件放置在适当的文件夹中。您可以从bit.ly/2DR4P9I访问该文件。

  3. 创建一个 DataFrame 对象并使用以下命令读取 CSV 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  4. 现在,使用以下命令显示数据集中的数据:

    str(df)
    

    输出如下:

图 2.2:来自 bank-additional-full CSV 文件的银行数据

图片

图 2.2:来自 bank-additional-full CSV 文件的银行数据

在前面的例子中,我们使用了 R 中可用的传统read.csv函数将文件读入内存。由于文件是分号分隔的,我们向sep=";"函数添加了一个参数。str函数打印出我们所需的数据集的高级信息。如果您仔细观察输出片段,您可以看到第一行表示数据的形状,即行数/观测数和列数/变量数。

输出片段中的下 21 行展示了数据集中每个变量的预览。它显示了变量的名称、其数据类型和数据集中的一些值。我们为每一列有一个条目。str函数实际上为我们提供了整个数据集的宏观视图。

如您从数据集中所见,我们有 20 个独立变量,例如agejobeducation,以及一个结果/因变量y。在这里,结果变量定义了向客户发起的营销电话是否导致了成功的存款注册,用yesno表示。为了理解整个数据集,我们现在需要研究数据集中的每个变量。让我们首先进行单变量分析。

单变量分析

对于数据集中的数值特征(如agedurationnr.employed)等,我们查看诸如最小值、最大值、平均值、标准差和百分位数分布等汇总统计量。这些度量共同帮助我们了解数据的分布。同样,对于分类特征(如jobmaritaleducation),我们需要研究特征中的不同值及其频率。为了完成这项任务,我们可以实施一些分析、可视化和统计技术。让我们看看探索数值特征的解析和可视化技术。

探索数值/连续特征

如果你探索了前面的输出片段,你可能已经注意到数据集中有数值型和分类型特征的混合。让我们首先查看数据集中的第一个特征,它是一个名为age的数值型特征。正如其名所示,它表示目标客户的年龄。让我们看一下该特征的摘要统计信息,并使用简单的箱线图进行可视化。

练习 19:使用箱线图可视化数据

在这个练习中,我们将探索使用箱线图进行单变量分析,解释如何解释箱线图,并逐步展示代码。

执行以下步骤以使用箱线图可视化数据:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用bank-additional-full.csv文件,使用以下命令:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 使用以下命令打印age数据,例如meanmax

    print(summary(df$age))
    

    输出如下:

    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    17.00   32.00   38.00   40.02   47.00   98.00
    
  4. 接下来,按照以下方式打印年龄的标准差:

    print(paste("Std.Dev:",round(sd(df$age),2)))
    

    输出如下:

    [1] "Std.Dev: 10.42"
    
  5. 现在,使用以下参数绘制年龄的箱线图:

    ggplot(data=df,aes(y=age)) + geom_boxplot(outlier.colour="black")
    

    输出如下:

![图 2.3:年龄的箱线图。![图 2.3:年龄的箱线图。###### 图 2.3:年龄的箱线图。我们首先加载ggplot2库,它提供了用于可视化数据的便捷函数。R 提供了一个简单的函数summary,它打印摘要统计信息,如最小值、最大值、中位数、平均值、75 百分位数和 25 百分位数值。下一行使用sd函数计算标准差,最后,最后一行使用ggplot库绘制数据的箱线图。如果你探索了摘要统计信息的输出,我们可以看到年龄的最小值为 17,最大值为 98,平均值为 42。如果你仔细观察 75 百分位数(第三四分位数)和 100 百分位数(最大值)之间的差距,我们可以看到巨大的跳跃。这表明年龄变量中存在异常值。异常值的存在将错误地改变你的分析结论。在某些情况下,当只有一个值为 75 百分位数的1000x的数据点时,你的平均值会向右移动。在仅使用平均值作为对变量进行估计的大致数值的情况下,对特征的整体理解可能会产生误导。另一方面,箱线图帮助我们以简单明了的方式直观地消费这些信息。箱线图将数据分为三个四分位数。下四分位数,即箱体下方的线,代表最小值和 25 百分位数。中间四分位数代表 25 至 50 至 75 百分位数。上四分位数代表 75 至 100 百分位数。100 百分位数以上的点是由内部函数确定的异常值。正如我们所见,从摘要统计中得出的观察结果与箱线图一致。我们确实看到了异常值,标记为上四分位数以上的点。在下一个练习中,我们将使用直方图对年龄变量进行 EDA。让我们看看从直方图图中我们能得到哪些见解。### 练习 20:使用直方图可视化数据在这个练习中,我们将讨论如何解释直方图和异常值。让我们从上一个练习继续。为了获得数据的更详细视图并更深入地理解 age 变量的组织方式,我们可以使用直方图。直方图是一种特殊的条形图,其中数据被分组并按顺序排列到称为 bins 的相等间隔中,并绘制相应分箱中的数据点的频率。直方图帮助我们更有效地理解数据的分布。练习通过绘制直方图帮助我们更有效地可视化数据。执行以下步骤:1. 首先,使用以下命令导入 ggplot2 包: py library(ggplot2) 1. 创建一个 DataFrame 对象 df,并使用以下命令使用 bank-additional-full.csv 文件: py df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';') 1. 现在,使用以下命令使用提供的参数绘制年龄的直方图: py ggplot(data=df,aes(x=age)) + geom_histogram(bins=10,fill="blue", color="black", alpha =0.5) + ggtitle("Histogram for Age") + theme_bw() 输出如下:图 2.4:年龄直方图

图 2.4:年龄直方图

ggplot 函数定义了可视化的基础层,随后通过带有参数的 geom_histogram 函数,这些参数定义了直方图相关方面,例如分箱数、填充颜色、alpha(透明度)等。默认情况下,分箱数也会被计算,但可以通过传递一个值给 bin 参数来覆盖,例如 bin=10。下一个函数 ggtitle 用于给图表添加标题,而 theme_bw 函数用于将主题改为黑白而非默认。theme 函数是可选的,这里添加它只是为了使图表更美观。

如您所清晰看到的,直方图为我们提供了对特征数据分布的更细粒度的视图。我们可以理解,65 岁之后记录数急剧减少,只有少数记录的值超过 75。在某些情况下,选择直方图的分箱数变得很重要,因为更多的分箱会使分布变得混乱,而较少的分箱会使分布信息不足。在我们想要看到分布的更细粒度视图的情况下,而不是增加直方图的分箱数,我们可以选择使用密度图来可视化,该图在连续区间上可视化,同时使用核平滑来平滑噪声。

我们也可以使用密度图而不是直方图来可视化年龄变量。下一个练习将详细介绍如何实现。

练习 21:使用密度图可视化数据

在这个练习中,我们将演示相同特征 age 的密度图。

执行以下步骤:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令绘制年龄的密度图:

    ggplot(data=df,aes(x=age)) + geom_density(fill="red",alpha =0.5) + 
                                 ggtitle("Density Plot for Age") + 
                                 theme_bw()
    

    输出如下:

![图 2.5:年龄的密度图。

![img/C12624_02_05.jpg]

图 2.5:年龄的密度图。

与上一个练习类似,我们使用ggplot函数的相同基础进行可视化,并使用不同的geom_density函数进行密度图绘制。其他用于可视化的附加函数保持不变。

密度图比直方图提供更详细的信息。虽然通过增加直方图的分组数也可以达到这种详细程度,但通常需要通过试错法来确定最佳的分组数。在这种情况下,选择密度图会是一个更简单的选项。

现在我们已经了解了数值变量的单变量分析的概念,让我们加快对其他变量的数据探索。我们总共有 10 个分类特征和 10 个数值列。让我们尝试使用直方图一起查看四个数值变量。

就像我们绘制年龄的直方图一样,我们可以通过定义一个自定义函数同时为多个变量绘制直方图。下一个练习将展示如何做到这一点。

练习 22:使用直方图可视化多个变量

在这个练习中,我们将把四个直方图(每个变量一个)组合成一个单独的图表。我们有campaign,表示在活动中进行的联系次数,pdays表示自上次前一个活动联系客户以来的天数;值为 999 表示客户之前从未被联系过。previous表示为此客户之前进行的联系次数,最后,emp.var.rate表示就业方差率。

为了完成这个练习,我们执行以下步骤:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    

    确保已安装cowplot包。

  2. 接下来,定义一个函数来绘制所有数值列的直方图:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
        plt_matrix<-list()
        i<-1
        for(column in list_of_variables){
            plt_matrix[[i]]<-ggplot(data=df,aes_string(x=column)) + 
                geom_histogram(binwidth=2,fill="blue", color="black", 
                               alpha =0.5)  +
                ggtitle(paste("Histogram for variable: ",column)) + theme_bw()
                i<-i+1
                }
        plot_grid(plotlist=plt_matrix,ncol=2)
    }
    
  3. 现在,使用summary函数打印campaignpdayspreviousemp.var.rate列的均值、最大值和其他参数:

    summary(df[,c("campaign","pdays","previous","emp.var.rate")])
    

    输出如下:

       campaign          pdays          previous      emp.var.rate     
    Min.   : 1.000   Min.   :  0.0   Min.   :0.000   Min.   :-3.40000  
    1st Qu.: 1.000   1st Qu.:999.0   1st Qu.:0.000   1st Qu.:-1.80000  
    Median : 2.000   Median :999.0   Median :0.000   Median : 1.10000  
    Mean   : 2.568   Mean   :962.5   Mean   :0.173   Mean   : 0.08189  
    3rd Qu.: 3.000   3rd Qu.:999.0   3rd Qu.:0.000   3rd Qu.: 1.40000  
    Max.   :56.000   Max.   :999.0   Max.   :7.000   Max.   : 1.40000
    
  4. 调用我们之前定义的函数来绘制直方图:

    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

![图 2.6:使用直方图可视化多个变量

![img/C12624_02_06.jpg]

图 2.6:使用直方图可视化多个变量

在这个练习中,我们自动化了将多个同类型图堆叠成一个综合图的过程。我们首先加载所需的cowplot库。这个库为使用ggplot库渲染的图提供了创建图网格的便捷函数。如果你还没有加载这个库,请使用install.packages('cowplot')命令安装包。然后我们定义一个名为plot_grid_numeric的函数,它接受数据集、要绘制的特征列表以及网格中要使用的列数作为参数。如果你观察函数的内部实现,你会看到我们只是使用for循环遍历提供的变量列表,并将单个图收集到一个名为plt_matrix的列表中。稍后,我们使用cowplot库提供的plot_grid函数将图排列成两列的网格。

同一个函数可以用来显示任何数量的直方图网格;使用基于你屏幕大小的数字。当前数字已被限制为 4 以获得最佳效果。我们还使用summary函数与直方图图一起显示相同一组数值变量的总体统计信息。

注意

在前面的函数中没有使用异常处理代码。我们目前忽略了实现复杂的代码,以便专注于感兴趣的主题。如果在非数值变量上使用该函数,错误信息可能不是最有效的解决方案。

如前一个图所示,我们现在有四个变量一起进行分析。将摘要统计信息与直方图图一起研究有助于我们更好地揭示变量。Campaign有 75%的值小于或等于 3。我们可以看到有一个异常值在 56,但绝大多数的记录值小于 5。pdays似乎不是我们分析的有用变量,因为几乎所有记录都有默认值 999。1000 中的高柱状图清楚地表明,几乎没有记录的值不是 999。

对于previous变量,我们看到它与pdays正好相反;大多数记录的值为 0。最后,emp.var.rate显示了一个有趣的结果。尽管值范围从-42,但超过一半的记录具有正值。

因此,通过分析这四个变量,我们可以大致得出结论,之前进行的营销活动并没有经常通过电话与客户联系,或者这也可能意味着在之前的营销活动中,几乎没有针对当前营销活动的目标客户进行联系。而且,较早联系的客户最多被联系了七次。客户上次被联系的天数自然与之前营销活动的结果一致,因为几乎没有人在之前被联系过。然而,对于当前营销活动,客户平均被联系了 2.5 次,75% 的客户被联系了 3 次或更少,一些客户被联系次数高达 56 次。就业方差率是衡量由于宏观经济状况而雇佣或解雇人数的指标。我们了解到,在营销活动的大部分时间里,经济状况相对稳定。

与上一节中创建的用于堆叠直方图的函数类似,在此活动中,我们将创建另一个用于堆叠密度图和另一个用于箱线图的函数。

活动 4:绘制多个密度图和箱线图

在此活动中,我们将创建一个用于堆叠密度图的函数,另一个用于箱线图。使用新创建的函数来可视化与上一节相同的变量集,并研究分析数值变量的最有效方法。

到此活动结束时,你将学会如何同时绘制多个变量的密度图。这样做可以一次比较不同的变量,并从中得出数据洞察。

完成此活动的步骤如下:

  1. 首先,在 RStudio 中加载必要的库和包。

  2. bank-additional-full.csv 数据集读入名为 df 的 DataFrame。

  3. 定义用于密度图的 plot_grid_numeric 函数:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
      }
      plot_grid(plotlist=plt_matrix,ncol=2)
    }
    
  4. 绘制 campaignpdayspreviousemp.var.rate 变量的密度图:图 2.7:、、 和  变量的密度图

    图 2.7:campaignpdayspreviousemp.var.rate 变量的密度图

    观察到我们使用直方图获得的解释在密度图中同样明显。因此,这可以作为查看相同趋势的另一种替代图表。

  5. 重复箱线图的步骤:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
    }
    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    图形如下:

图 2.8:、、 和  变量的箱线图

图 2.8:campaignpdayspreviousemp.var.rate 变量的箱线图

在箱线图中需要注意的一个额外点是,它显示了campaign变量中存在明显的异常值,这在其他两个图表中并不非常明显。对于previouspdays变量也可以做出类似的观察。学生应该尝试在移除异常值后绘制箱线图,看看它们看起来有何不同。

注意

你可以在第 442 页找到这个活动的解决方案。

练习 23:绘制 nr.employed、euribor3m、cons.conf.idx 和 duration 变量的直方图

在这个练习中,我们将转向下一组也是最后一组四个数值变量。我们有nr.employed,表示在银行工作的员工人数,euribor3m表示 3 个月欧元间银行利率的平均利率。此外,我们还有cons.conf.index,这是消费者信心指标,通过消费者通过储蓄和消费活动表达的对国家乐观程度的程度来衡量。最后,是duration,表示最后一次联系持续时间。根据 UCI 提供的元数据,这个变量与结果高度相关,可能导致数据泄露。因此,我们将从未来的分析中删除这个变量。

执行以下步骤以研究下一组数值变量:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 使用summary方法打印详细信息:

    summary(df[,c("nr.employed","euribor3m","cons.conf.idx","duration")])
    

    输出如下:

     nr.employed     euribor3m     cons.conf.idx      duration     
    Min.   :4964   Min.   :0.634   Min.   :-50.8   Min.   :   0.0  
    1st Qu.:5099   1st Qu.:1.344   1st Qu.:-42.7   1st Qu.: 102.0  
    Median :5191   Median :4.857   Median :-41.8   Median : 180.0  
    Mean   :5167   Mean   :3.621   Mean   :-40.5   Mean   : 258.3  
    3rd Qu.:5228   3rd Qu.:4.961   3rd Qu.:-36.4   3rd Qu.: 319.0  
    Max.   :5228   Max.   :5.045   Max.   :-26.9   Max.   :4918.0
    
  4. 绘制定义变量的直方图,如下命令所示:

    plot_grid_numeric(df,c("nr.employed","euribor3m","cons.conf.idx","duration"),2)
    

    输出如下:

![图 2.9:各种变量的计数和持续时间的直方图img/C12624_02_09.jpg

图 2.9:各种变量的计数和持续时间的直方图

就像练习 5使用直方图可视化多个变量,我们首先使用summary函数对所需的一组变量进行汇总统计,然后通过调用我们之前定义的相同函数,一起绘制所有所需变量的组合直方图。

如我们所见,雇佣的员工数量大部分保持在5228,但在时间期间也降至不同的值。这个数字是按季度测量的,因此频率并不非常动态,这就是为什么我们可以看到围绕仅几个桶的值。欧元间银行利率大部分在2.55之间。只有 1 或 2 条记录的值超过 5,我们可以看到这个变量的最大测量值为5.045。消费者信心指数大部分为负值,这意味着消费者在此时大部分对经济状况持负面看法。我们在直方图的桶中看到两个峰值,这表明了当时最常见的信心指数,并模糊地暗示了在活动期间指数的有限变化。现在,我们暂时忽略通话时长(以秒为单位)的分析。

总结一下,我们了解到在活动中,银行的员工数量在约 250 人的范围内有所增加和减少,这大约是总员工数的 5%。员工数量在49645228之间,变化不大。消费者信心指数在时间期间大部分保持负值,变化不大,欧元间银行利率的平均值为 3.6,大部分记录在 2.5 到 5 之间。

现在,让我们继续研究使用单变量分析来研究分类型变量。

探索分类型特征

分类型特征在本质上与数值或连续特征不同,因此之前使用的传统方法在这里不适用。我们可以分析分类型变量中不同类的数量以及与每个类相关的频率。这可以通过简单分析技术或视觉技术实现。让我们通过结合这两种方法来探索一系列分类型特征。

练习 24:探索分类型特征

在这个练习中,我们将从一个简单的变量开始,即marital,它表示客户的婚姻状况。让我们使用dplyr库来进行分组数据聚合。

执行以下步骤以完成练习:

  1. 首先,使用以下命令在系统中导入dplyr库:

    library(dplyr)
    
  2. 接下来,我们将创建一个名为marital_distribution的对象,并根据以下条件存储值:

    marital_distribution <- df %>% group_by(marital) %>% 
                                   summarize(Count = n()) %>% 
                                   mutate(Perc.Count = round(Count/sum(Count)*100))
    
  3. 现在,打印存储在marital_distribution对象中的值:

    print(marital_distribution)
    

    输出如下:

    # A tibble: 4 x 3
      marital  Count Perc.Count
      <fct>    <int>      <dbl>
    1 divorced  4612         11
    2 married  24928         61
    3 single   11568         28
    4 unknown     80          0
    

为了计数分类列中的不同类别的数量,以及获取每个个别类别的记录计数,我们使用 dplyr 库下可用的 group_by 函数。%>%,也称为 marital,然后将输出传递给 summarize 函数,该函数使用我们提供的聚合函数将 DataFrame 聚合到分组级别;在这种情况下,n() 是简单的 count 等价。最后,我们使用 mutate 函数计算每个个别组成员的计数百分比。

我们可以看到,大多数竞选电话是打给已婚客户的,约占 61%,其次是 28% 的单身客户电话,等等。

练习 25:使用柱状图探索分类特征

在这个练习中,我们将绘制一个柱状图,以可视化每个类别的频率计数。我们也可以使用柱状图来表示每个这些个别类别的频率分布图。

执行以下步骤以完成练习:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令按计数绘制婚姻状况的柱状图:

    ggplot(data = marital_distribution,aes(x=marital,y=Perc.Count)) + 
                  geom_bar(stat="identity",fill="blue",alpha=0.6) + 
                  geom_text(aes(label=marital_distribution$Perc.Count, vjust = -0.3))
    

    输出如下:

图 2.10:按计数显示的婚姻状况柱状图

图 2.10:按计数显示的婚姻状况柱状图

我们使用与之前片段中相同的工程化数据集,该数据集计算每个类别的频率及其相对百分比。要绘制柱状图,我们使用 ggplot 的相同基础函数,其中我们定义了 xy 变量的美学,并使用 geom_bar 函数附加柱状图。geom_text 函数允许我们在图表中的每个条形上添加标签。

我们现在可以看到与之前练习中相同的数字,这里通过柱状图可视化。在变量中有大量类别的情况下,逐个查看每个类别来研究它们可能不是最有效的方法。一个简单的图表可以轻松帮助我们以易于消费的方式理解分类变量的频率分布。

练习 26:使用饼图探索分类特征

在这个练习中,我们将定义饼图及其内部的各种组件。

与柱状图类似,我们还有一个饼图,它使理解类别的百分比分布更容易。执行以下步骤以使用饼图可视化相同的变量,即婚姻状况:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,使用以下命令定义标签位置:

    plot_breaks = 100 - (cumsum(marital_distribution$Perc.Count) - 
                       marital_distribution$Perc.Count/2)
    
  4. 现在,为图表定义标签:

    plot_labels = paste0(marital_distribution$marital,"-",marital_distribution$Perc.Count,"%")
    
  5. 设置图表大小以获得更好的视觉效果:

    options(repr.plot.width=12, repr.plot.height=8)
    
  6. 使用以下命令创建饼图:

    ggplot(data = marital_distribution,aes(x=1,y=Perc.Count, fill=marital)) + 
                  geom_bar(stat="identity") + #Creates the base bar visual
                  coord_polar(theta ="y")  + #Creates the pie chart
                  scale_y_continuous(breaks=plot_breaks, labels = plot_labels,position = "left") + 
                  theme(axis.text.x = element_text(angle = 30, hjust =1)) + #rotates the labels
                  theme(text = element_text(size=15)) + #increases the font size for the legend
                  ggtitle("Percentage Distribution of Marital Status") #Adds the plot title
    

![图 2.11:婚姻状况的百分比分布饼图]

![img/C12624_02_11.jpg]

图 2.11:婚姻状况的百分比分布饼图

我们首先定义一些额外的变量,这将帮助我们更容易地获取图表。为了标记饼图,我们需要断点(break points)和实际标签。理想情况下,断点应位于饼图的中间部分。因此,我们计算百分比分布的累积总和,并从每个类别中减去一半,以找到该部分的中间点。然后,我们从 100 中减去整个数值,以顺时针方向排列标签。

下一步定义每个饼图的标签;我们使用paste函数将标签名称和实际百分比值连接起来。ggplot中的饼图功能是通过在柱状图之上构建元素来实现的。我们首先使用ggplotgeom_bar的基础来渲染堆叠柱状图的基础,并使用coord_polar函数将其转换为所需的饼图。scale_y_continuous函数有助于将标签放置在饼图分布上。下一行添加了文本定位的旋转角度。theme函数的element_text部分中的size参数定义了图中文本的字体大小。其余部分与我们在早期图表中学习的一样。

我们可以看到,饼图为我们提供了一个直观的方式来探索每个变量中类别的百分比分布。关于选择饼图而不是柱状图的建议,应基于变量中不同类别的数量。尽管饼图在视觉上更吸引人,但类别众多时,饼图会显得过于拥挤。

注意

当分类变量中不同类别的数量较高时,最好避免使用饼图。没有明确的规则,但任何使饼图视觉上杂乱无章的因素都不适合研究。

练习 27:自动化绘图分类变量

在这个练习中,我们将自动化分类变量的绘图。

就像数值变量一样,我们也有 10 个分类变量,不包括目标变量。类似于自动化数值特征的探索,现在让我们为分类变量创建一个函数。为了使事情简单,我们将主要使用带有百分比分布的箱线图而不是饼图。我们将从四个分类特征开始,然后转到下一个剩余的集合。

执行以下步骤以完成练习:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 定义一个函数来绘制所有数值列的直方图:

    plot_grid_categorical <- function(df,list_of_variables,ncols=2){
        plt_matrix <- list()
        i<-1
        #Iterate for each variable
        for(column in list_of_variables){
            #Creating a temporary DataFrame with the aggregation
            var.dist <- df %>% group_by_(column) %>% 
                               summarize(Count = n()) %>% 
                               mutate(Perc.Count = round(Count/sum(Count)*100,1))
            options(repr.plot.width=12, repr.plot.height=10)
            plt_matrix[[i]]<-ggplot(data = var.dist,aes_string(x=column,y="Perc.Count")) +
                geom_bar(stat="identity",fill="blue",alpha=0.6) + #Defines the bar plot
                geom_text(label=var.dist$Perc.Count,vjust=-0.3)+  #Adds the labels
                theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
                ggtitle(paste("Percentage Distribution of variable: ",column))  #Creates the title +
                i<-i+1
        }
            plot_grid(plotlist=plt_matrix,ncol=ncols) #plots the grid
    }
    
  3. 接下来,使用以下命令调用summary统计量:

    summary(df[,c("job","education","default","contact")])
    

    输出如下:

              job                      education        default           contact     
     admin.     :10422   university.degree  :12168   no     :32588   cellular :26144  
     blue-collar: 9254   high.school        : 9515   unknown: 8597   telephone:15044  
     technician : 6743   basic.9y           : 6045   yes    :    3
     services   : 3969   professional.course: 5243
     management : 2924   basic.4y           : 4176
     retired    : 1720   basic.6y           : 2292
     (Other)    : 6156   (Other)            : 1749
    
  4. 调用我们之前定义的函数来绘制直方图:

    plot_grid_categorical(df,c("job","education","default","contact"),2)
    

    输出如下:

![图 2.12:分类变量的柱状图]

![img/C12624_02_12.jpg]

图 2.12:分类变量的条形图

与我们之前为数值特征创建的用于视觉自动化的函数类似,我们创建了一个简单的函数来探索分类特征的百分比分布。对函数的一些补充包括创建临时聚合数据集和一些额外的图表美化增强。我们添加了标签并将它们旋转 30 度,以便它们可以整齐地与图表对齐,其余保持不变。我们通过在categorical列上调用summary函数来获取频率统计。与数值列类似,我们首先使用summary函数探索分类列,然后使用定义的函数来可视化汇总的条形图。

探索job特征,我们可以看到 12 个不同的值,其中大多数记录为行政、蓝领和技师。总体而言,job类别似乎具有相当多样化的值分布。客户的受教育程度也有一系列多样化的值,大约 50%的值为高中和大学。对于表示客户是否以前在信贷中违约的default变量,大约 80%的值为no,大约 20%未知。这似乎不是很有用。最后,contact,它定义了用于活动通信的联系方式,显示 64%是通过移动电话进行的,其余是通过固定电话。

让我们继续并重复对下一组特征进行相同的分析。

练习 28:自动绘制剩余分类变量的图表

在这个练习中,我们将重用相同的函数来处理下一组四个分类变量。记住,你需要结合使用summary命令生成的频率统计和图表来解释值。

让我们执行以下步骤来完成练习:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,使用以下命令调用summary统计:

    summary(df[,c("loan","month","day_of_week","poutcome")])
    

    输出如下:

          loan           month       day_of_week        poutcome    
     no     :33950   may    :13769   fri:7827    failure    : 4252  
     unknown:  990   jul    : 7174   mon:8514    nonexistent:35563  
     yes    : 6248   aug    : 6178   thu:8623    success    : 1373  
                     jun    : 5318   tue:8090                       
                     nov    : 4101   wed:8134                       
                     apr    : 2632                                  
                     (Other): 2016
    
  4. 调用定义的函数来绘制直方图:

    plot_grid_categorical(df,c("loan","month","day_of_week","poutcome"),2)
    

    输出如下:

![图 2.13:自动绘制剩余分类变量的图表img/C12624_02_13.jpg

图 2.13:自动绘制剩余分类变量的图表

我们重用之前定义的函数来探索新的一组四个变量,就像我们探索之前的一组特征一样。

loan 变量表示客户是否有个人贷款。我们有大约 86.6% 的客户没有个人贷款,10.3% 有贷款,3.3% 未知。同样,month 变量表示活动电话执行的实际月份。我们看到大多数沟通是在 may 月份进行的,其次是 julaug。总体而言,month 特征似乎也是一个相当多样化的变量,具有良好的值分布。day_of_week 变量在每周的每一天都显示出一致的分布。poutcome 表示之前执行的活动的结果;绝大多数不存在,大约 3.3% 的成功,大约 10% 失败。

练习 29:探索最后一个剩余的分类变量和目标变量

最后,让我们探索最后一个剩余的分类变量和目标变量。由于两者都是分类变量,我们可以继续使用相同的函数进行探索。

对最后一个独立分类变量和因变量(也是分类变量)重复相同的过程:

  1. 首先,在导入所需的包并创建 DataFrame 对象后,使用以下命令调用摘要统计:

    summary(df[,c("y","housing")])
    

    输出如下:

       y            housing     
     no :36548   no     :18622  
     yes: 4640   unknown:  990  
                 yes    :21576
    
  2. 调用定义好的函数来绘制直方图:

    plot_grid_categorical(df,c("y","housing"),2)
    

    输出如下:

![图 2.14:按计数划分的住房直方图图片 C12624_02_14.jpg

图 2.14:按计数划分的住房直方图

如果我们仔细观察结果变量的分布,我们可以看到大多数客户对活动电话做出了负面回应。只有大约 11% 的整体活动基础对活动做出了积极回应。同样,如果我们查看 housing 变量,我们可以看到大约 50% 的客户有住房贷款。

总结来说,我们可以将我们的观察总结如下:

  • 该活动主要针对那些之前未曾接触过的新客户。

  • 大约 60% 的客户已婚,80% 在信用历史中没有违约。

  • 大约 50% 的客户有住房贷款,超过 80% 从未选择过个人贷款。

  • 该活动在五月份最为活跃,并在七月和八月展现了相当强的动力。

  • 活动的超过 60% 的沟通是通过手机进行的,并且超过 50% 的客户至少有高中文凭。

  • 总体而言,只有 11% 的活动电话获得了积极回应。

在完成所有数值和分类变量的单变量分析后,我们现在对数据传达的故事有了相当的了解。我们几乎理解了每个数据维度及其分布。让我们继续探索 EDA 的另一个有趣方面:双变量分析。

双变量分析

双变量分析中,我们将分析扩展到同时研究两个变量。在我们的用例中,我们有大约 20 个独立变量。实际上,我们可以研究所有 20 个变量的排列组合,但在这个章节中我们不会达到那个程度。在我们的用例中,我们更感兴趣的是研究导致活动表现不佳的所有因素。因此,我们的主要焦点将是执行双变量分析,研究所有独立变量与我们的目标变量之间的关系。再次强调,根据变量的类型,我们将有不同的视觉或分析技术来分析两个变量之间的关系。可能的组合是数值与数值,以及数值与分类。鉴于我们的目标变量是分类变量,我们可能需要探索列表中两个独立变量之间的关系,以研究两个数值变量之间的关系。让我们开始吧。

研究两个数值变量之间的关系

为了理解我们如何研究两个数值变量之间的关系,我们可以利用散点图。这是一个二维的数据可视化,其中每个变量都沿着其长度绘制在轴上。通过研究可视化中的趋势,可以轻松地识别变量之间的关系。让我们在下面的练习中看看一个例子。

练习 30:研究雇员方差率与雇员数量之间的关系

让我们研究雇员方差率和雇员数量之间的关系。理想情况下,随着方差率的增加,雇员数量应该增加。

执行以下步骤来完成练习:

  1. 首先,使用以下命令导入ggplot2包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令绘制散点图:

    ggplot(data=df,aes(x=emp.var.rate,y=nr.employed)) + geom_point(size=4) + 
    ggtitle("Scatterplot of Employment variation rate v/s Number of Employees")
    

    输出如下:

图 2.15:就业变动与雇员数量的散点图

图 2.15:就业变动与雇员数量的散点图

我们使用相同的基函数ggplot,并添加了一个新的包装器来处理散点图。ggplot中的geom_point函数提供了使用散点图所需的结构。

我们可以看到一个整体上升的趋势,即随着就业方差率的增加,我们也看到雇员数量也在增加。nr.employed中的点数较少是由于重复的记录。

研究分类变量与数值变量之间的关系

让我们先回顾一下研究数值变量和分类变量之间关系的方法,并讨论执行该方法的步骤。

在本节中,我们将讨论我们可以用于总结数据的不同聚合度量。到目前为止,我们已经使用了avg,但更好的方法是将avgminmax和其他度量结合使用。

练习 31:研究 y 和年龄变量之间的关系

我们有一个分类的因变量和九个数值变量要探索。为了从小处着手,我们首先将探索我们的目标yage之间的关系。为了研究分类和数值变量之间的关系,我们可以选择一种简单的分析技术,即计算每个目标结果中的平均年龄;如果我们看到明显的差异,我们可以从观察中得出见解。

在这个练习中,我们将计算每个目标结果中的平均年龄,并计算每个桶中的记录数,然后进行可视化表示。

执行以下步骤:

  1. 首先,使用以下命令导入ggplot2包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 创建一个temp对象并使用以下命令存储值:

    temp <- df %>% group_by(y) %>% 
                               summarize(Avg.Age = round(mean(age),2),
                                         Num.Records = n())
    
  4. 打印存储在temp对象中的值:

    print(temp)
    

    输出如下:

    # A tibble: 2 x 3
      y     Avg.Age Num.Records
      <fct>   <dbl>       <int>
    1 no       39.9       36548
    2 yes      40.9        4640
    
  5. 现在,使用ggplot命令创建一个图表:

    ggplot(data= temp, aes(x=y, y=Avg.Age)) + 
           geom_bar(stat="identity",fill="blue",alpha= 0.5) +   #Creates the bar plot
           geom_text(label=temp$Avg.Age,vjust=-0.3)+  #Adds the label
           ggtitle(paste("Average Age across target outcome"))  #Creates the title
    

    输出如下:

![图 2.16:目标结果中平均年龄的直方图图片

图 2.16:目标结果中平均年龄的直方图

第一行代码创建了一个临时聚合数据集,该数据集总结了每个类别的平均年龄和记录数。所使用的绘图功能与之前的视觉类似。我们通过使用geom_bar扩展ggplot函数来渲染条形图。

我们可以看到两个结果之间几乎没有差异。我们没有看到任何有趣的模式。

注意

在双变量分析中,在得出任何有趣的模式作为见解之前,我们需要小心谨慎。在许多情况下,由于数据的偏斜分布,这些模式看起来会令人惊讶地有趣。

让我们继续研究下一组变量。

练习 32:研究平均值与 y 变量之间的关系

在这个练习中,我们将研究下一组变量之间的关系:averagey

执行以下步骤以完成练习:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 接下来,使用以下命令创建plot_bivariate_numeric_and_categorical对象:

    plot_bivariate_numeric_and_categorical <- function(df,target,list_of_variables,ncols=2){
        target<-sym(target) #Defined for converting text to column names
        plt_matrix <- list()
        i<-1
    for(column in list_of_variables){
            col <-sym(column) #defined for converting text to column name
            temp <- df %>% group_by(!!sym(target)) %>% 
                           summarize(Avg.Val = round(mean(!!sym(col)),2))
            options(repr.plot.width=12, repr.plot.height=8) #Defines plot size
               plt_matrix[[i]]<-ggplot(data= temp, aes(x=!!sym(target), y=Avg.Val)) + 
               geom_bar(stat="identity",fill="blue",alpha= 0.5) +   
               geom_text(label=temp$Avg.Val,vjust=-0.3)+  #Adds the labels
               ggtitle(paste("Average",column,"across target outcomes"))  #Creates the title 
                i<-i+1
        }
        plot_grid(plotlist = plt_matrix,ncol=ncols)
    }
    
  3. 现在,打印记录在目标结果中的分布:

    print("Distribution of records across target outcomes-")
    print(table(df$y))
    

    输出如下:

    [1] "Distribution of records across target outcomes-"
       no   yes 
    36548  4640
    
  4. 现在,使用以下命令为定义的变量绘制直方图:

    plot_bivariate_numeric_and_categorical(df,"y",c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

![图 2.17:平均值与 y 变量的直方图图片

图 2.17:平均值与 y 变量的直方图

为了自动化数据探索任务,以便进行分类变量和数值变量之间的双变量分析,我们定义了一个类似于上一个练习中定义的函数。我们还使用了sym函数,这将帮助我们使用函数中的动态列名。使用!!sym(column)将字符串转换为真正的列名,类似于传递实际值。上一个函数首先对感兴趣的变量中的目标平均值进行聚合。然后plot函数使用这些信息绘制出目标结果平均值的条形图。

在双变量分析中,在得出具体见解之前,仔细验证观察到的模式非常重要。在某些情况下,异常值可能会扭曲结果,从而得出错误的结论。此外,特定模式的记录较少也可能是一个风险模式。始终建议收集所有观察到的见解,并使用额外的广泛 EDA 或统计技术进行验证,以确定其显著性。

在这里,我们没有看到任何显著的结果可以得出结论。在campaign变量中,成功活动中进行的平均联系次数略低,但差异太小,无法得出任何可能的结论。pdays,表示上次联系以来的天数,在目标的结果之间显示出很大的差异。

然而,这种差异纯粹是因为大多数客户在上一个活动中没有联系。所有这些记录的值都设置为 999。对于previous也是如此;尽管两者之间有相当大的差异,但大多数客户在当前活动中是第一次被联系。然而,就业方差率却显示出反直觉的结果。我们实际上预计当结果为yes时,方差率应该更高,但我们看到的是相反的情况。这听起来很有趣,我们暂时将这个见解记录下来,稍后再回来进行更多验证,然后再得出任何结论。

让我们继续研究下一组将要被研究的分类因变量。

练习 33:研究 cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量之间的关系

让我们继续研究下一组将要被研究的分类因变量。在这个练习中,我们将使用直方图来探索cons.price.idxcons.conf.idxeuribor3mnr.employed与目标变量y之间的关系。

  1. 导入所需的库并创建 DataFrame 对象。

  2. 接下来,创建一个plot_bivariate_numeric_and_categorical函数并绘制直方图:

    plot_bivariate_numeric_and_categorical(df,"y",
                   c("cons.price.idx","cons.conf.idx", "euribor3m", "nr.employed"),2)
    

    输出如下:

![图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图img/C12624_02_18.jpg

图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图

再次强调,在大多数情况下,我们没有看到任何明显的模式。然而,euribor3m 变量在 yesno 结果的平均值之间显示出一些良好的差异,这再次看起来是反直觉的。我们理想上期望利率越高,银行存款就越多。因此,让我们记下这个见解,并在稍后验证它。

接下来,让我们现在探索两个分类变量之间的关系。

研究两个分类变量之间的关系

为了研究两个分类变量之间存在的关联和模式,我们可以首先探索变量每个类别的频率分布。任何结果的高浓度可能是一个潜在的见解。最有效的方式来可视化这一点是使用堆积柱状图。

堆积柱状图将帮助我们观察目标变量在多个分类变量中的分布。分布将揭示某个分类变量中的特定类别是否主导了目标变量 y。如果是这样,我们可以进一步探索其对问题的潜在影响。

在接下来的几个练习中,我们将使用堆积柱状图探索目标变量 y 上的各种分类变量。我们将绘制绝对计数和百分比,以更好地理解分布情况。

练习 34:研究目标变量 y 和婚姻状况变量之间的关系

在这个练习中,我们将展示使用纯频率计数研究两个分类变量之间的关系,然后展示其不便之处。

为了简单起见,让我们从探索目标变量 y婚姻状况 之间的关系开始。

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个名为 df 的 DataFrame 对象,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,创建一个 temp 聚合数据集:

    temp <- df %>% group_by(y,marital) %>% summarize(Count = n()) 
    
  4. 定义绘图大小,如图所示:

    options(repr.plot.width=12, repr.plot.height=4)
    
  5. 绘制频率分布图:

    ggplot(data = temp,aes(x=marital,y=Count,fill=y)) + 
           geom_bar(stat="identity") + 
           ggtitle("Distribution of target 'y' across Marital Status")
    

    输出如下:

    图 2.19:使用 ggplot 研究目标变量 y 和婚姻状况变量之间的关系

    图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图

    图 2.19:使用 ggplot 研究目标变量 y 和婚姻状况变量之间的关系

    我们首先使用 group_by 函数对分类列进行聚合。这将帮助我们计算每个类别组合的交叉频率计数。现在我们使用这个临时数据集来绘制独立变量的频率分布。

    如我们所见,已婚客户的 yes 频率最高,但这可能仅仅是因为已婚客户数量较多。为了更好地理解这种关系,我们可以进一步使用带有百分比分布的堆积柱状图来细分。

  6. 创建一个 temp 聚合数据集:

    temp <- df %>% group_by(y,marital) %>% 
                   summarize(Count = n()) %>% 
                   ungroup() %>%  #This function ungroups the previously grouped dataframe
                   group_by(marital) %>%
                   mutate(Perc = round(Count/sum(Count)*100)) %>%
                   arrange(marital)
    
  7. 使用 options 方法定义绘图大小:

    options(repr.plot.width=12, repr.plot.height=4)
    
  8. 使用 ggplot 方法绘制百分比分布图:

    ggplot(data = temp,aes(x=marital,y=Perc,fill=y)) + 
        geom_bar(stat="identity") + 
        geom_text(aes(label = Perc), size = 5, hjust = 0.5, vjust = 0.3, position = "stack") + 
        ggtitle("Distribution of target 'y' percentage across Marital Status")
    

    输出如下:

![图 2.20:目标 y 百分比在婚姻状态中的分布img/C12624_02_20.jpg

图 2.20:目标 y 百分比在婚姻状态中的分布

与之前的图表相比,我们现在可以看到一些反直觉的结果。在结果归一化后,我们发现 single 客户对活动的响应比已婚客户更积极。对于 unknown 也是如此,但由于值的不确定性和记录数量极低,我们应该忽略这一点。我们不能直接得出单身客户在响应活动方面更有效的结论,但我们可以稍后验证这一点。

练习 35:研究工作和教育变量之间的关系

在这个练习中,我们将加速我们的探索。让我们构建一个自定义函数,我们可以在这个函数中结合两个图表,即频率分布以及百分比分布,用于分类变量的双变量分析。

执行以下步骤:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令加载 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 创建一个 temp 聚合数据集:

    plot_bivariate_categorical <-  function(df, target, list_of_variables){
        target <- sym(target) #Converting the string to a column reference
        i <-1 
        plt_matrix <- list()
        for(column in list_of_variables){
            col <- sym(column) 
            temp <- df %>% group_by(!!sym(target),!!sym(col)) %>% 
               summarize(Count = n()) %>% 
               ungroup() %>% #This fucntion ungroups the previously grouped dataframe
               group_by(!!sym(col)) %>%
               mutate(Perc = round(Count/sum(Count)*100)) %>%
               arrange(!!sym(col))
    
  4. 定义绘图大小:

    options(repr.plot.width=14, repr.plot.height=12)
    
  5. 使用频率分布绘制图表:

        plt_matrix[[i]]<- ggplot(data = temp,aes(x=!!sym(col),y=Count,fill=!!sym(target))) + 
            geom_bar(stat="identity") + 
            geom_text(aes(label = Count), size = 3, hjust = 0.5, vjust = -0.3, position = "stack") + 
            theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
            ggtitle(paste("Distribution of target 'y'  frequency across",column))
        i<-i+1
    
  6. 绘制百分比分布图:

        plt_matrix[[i]] <- ggplot(data = temp,aes(x=!!sym(col),y=Perc,fill=!!sym(target))) + 
            geom_bar(stat="identity") + 
            geom_text(aes(label = Perc), size = 3, hjust = 0.5, vjust = -1, position = "stack") + 
            theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
            ggtitle(paste("Distribution of target 'y' percentage across",column))
        i <- i+1
        }
        plot_grid(plotlist = plt_matrix, ncol=2)
    }
    
  7. 使用以下命令绘制 plot_bivariate_categorical 图:

    plot_bivariate_categorical(df,"y",c("job","education"))
    

    输出如下:

    ![图 2.21:研究工作和教育变量之间的关系 img/C12624_02_21.jpg

    图 2.21:研究工作和教育变量之间的关系

    我们使用相同的原则来定义绘制图表的函数。这里的额外区别是每个组合两个图表。第一个(左侧)是类别组合的频率图,右侧的图表展示了百分比分布(按类别归一化)的视觉展示。同时研究这两个图表有助于更有效地验证结果。使用 ungroup 函数创建临时聚合数据集有额外的步骤。这是用来启用目标结果在独立变量的分类水平中的相对百分比分布,即 ymarital 的每个水平内的分布。

    如果我们观察前一个输出图的结果,我们可以看到,活动的最高响应率来自学生和退休专业人士,但这也存在一个警告。我们发现这两个类别与其他类别相比观察到的数量要少得多。因此,在得出进一步结论之前,我们需要额外的验证。因此,我们也将这个洞察力记录下来。从教育水平来看,我们没有看到任何有趣的趋势。尽管文盲客户的响应率很高,但观察到的数量太少,无法得出任何有意义的结论。

  8. 让我们来看看信用违约和住房贷款类别:

    plot_bivariate_categorical(df,"y",c("default","housing"))
    

    输出结果如下:

    图 2.22:研究默认和住房变量之间的关系

    图 2.22:研究默认和住房变量之间的关系
  9. 再次,我们没有看到任何有趣的趋势。让我们继续探索个人贷款和联系模式:

    plot_bivariate_categorical(df,"y",c("loan","contact"))
    

    输出结果如下:

图 2.23:研究贷款和联系变量之间的关系

图 2.23:研究贷款和联系变量之间的关系

在这里,我们可以看到联系方式的模式存在一个有趣的趋势。当活动沟通模式是手机而非固定电话时,通常会有更高的响应率。让我们也记录这个趋势,并进一步验证。

我鼓励您探索目标变量与剩余的依赖性分类变量(月份、星期几和活动的先前结果)之间的关系。

多变量分析

多变量分析是研究两个以上变量之间关系的过程;本质上,一个因变量和多个自变量。双变量分析是多变量分析的一种形式。有几种重要的多变量分析方法,但为了限制本章的范围,我们暂时跳过细节。在接下来的几章中,我们将更详细地研究线性回归和逻辑回归,这两种都是流行的多变量分析方法。

在多元分析中,一些最常用的技术如下:

  • 多元线性回归(研究多个自变量对一个数值/连续目标变量的影响)

  • 逻辑回归(研究多个自变量对一个分类目标变量的影响)

  • 因素分析

  • 多元方差分析(MANOVA)

使用统计测试验证洞察力

在 EDA 的整个过程中,我们已经收集并记录了一些有趣的模式以供进一步验证。现在是测试我们之前观察到的模式是否真正有效,或者只是由于随机机会而看似有趣的时候了。进行这种验证最有效和最直接的方法是通过执行一系列统计测试并测量模式的统计显著性。我们在可用的测试集中有很多选项可供选择。这些选项取决于独立变量和依赖变量的类型。以下是一个实用的参考图,解释了我们可以执行以验证观察到的模式的各种统计测试类型:

图 2.24:验证依赖和独立变量

图 2.24:验证依赖和独立变量

让我们把所有有趣的模式收集到一个地方:

  • 当员工差异率低时,活动的结果更有可能为“是”。

  • 当欧元利率低时,活动的结果更有可能为“是”。

  • 单一客户有更高的可能性对活动做出积极回应。

  • 学生和退休客户更有可能对活动做出积极回应。

  • 移动电话联系人有更高的可能性对活动做出积极回应。

如果你尝试对这些假设进行分类,我们可以看到在所有情况下我们都有一个分类的依赖变量。因此,我们应该使用卡方检验或逻辑回归检验来验证我们的结果。

让我们逐一进行这些测试。

分类依赖变量和数值/连续独立变量

假设 1 和 2 有一个连续的独立变量。参考前一小节的图,我们将选择卡方检验。在假设检验的过程中,我们首先定义一个零假设和一个备择假设。从一个否定的角度开始,即假设零假设是我们不希望发生的事情。假设检验检查观察到的模式是由于随机机会发生的可能性,或者是否有关于观察的确定性。这个度量量化为概率。如果零假设显著性的概率小于 5%(或合适的截止值),我们拒绝零假设,并确认备择假设的有效性。

让我们开始吧;对于假设 1,我们定义以下内容:

  • 零假设:活动的结果与员工差异率没有关系。

  • 备择假设:活动的结果与员工差异率有关。

我们使用简单的逻辑回归测试来测试我们零假设的有效性。我们将在接下来的章节中更详细地讨论这个话题。现在,我们将快速执行一个简单的检查来测试我们的假设。以下练习利用了 R 内置的逻辑回归函数。

练习 36:对分类因变量和连续自变量进行假设 1 测试

要对分类因变量和连续自变量进行假设测试,我们将使用glm()函数拟合逻辑回归模型(更多内容请参阅第五章分类)。这个练习将帮助我们统计检验一个分类因变量(例如,y)是否与一个连续自变量(例如)有任何关系,

emp.var.rate

执行以下步骤以完成练习:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 接下来,执行逻辑回归:

    h.test <- glm(y ~ emp.var.rate, data = df, family = "binomial")
    
  4. 打印测试摘要:

    summary(h.test)
    

    输出如下:

    Call:
    glm(formula = y ~ emp.var.rate, family = "binomial", data = df)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -1.0047  -0.4422  -0.3193  -0.2941   2.5150  
    Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
    (Intercept)  -2.33228    0.01939 -120.31   <2e-16 ***
    emp.var.rate -0.56222    0.01018  -55.25   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 28999  on 41187  degrees of freedom
    Residual deviance: 25597  on 41186  degrees of freedom
    AIC: 25601
    Number of Fisher Scoring iterations: 5
    

我们将目标变量y转换为factor类型(如果它尚未如此)。我们使用 R 提供的glm函数进行逻辑回归。glm函数还执行其他形式的回归,我们指定family = 'binomial'参数以将函数用作逻辑回归。函数的第一个位置中的公式定义了因变量和自变量。

输出中有很多结果共享。现在我们将忽略其中大部分,只关注最终输出。提供的结果之一是显著性概率,这证实了我们的零假设为真的可能性小于2e-16,因此我们可以拒绝它。因此,目标结果与员工方差率有统计学上的显著关系,并且,正如我们所看到的,随着率的降低,竞选转化的可能性更高。

类似地,让我们为第二个假设重复相同的测试。我们定义以下内容:

  • 零假设:竞选结果与欧元利率之间没有关系。

  • 备择假设:竞选结果与欧元利率之间存在关系。

练习 37:对分类因变量和连续自变量进行假设 2 测试

再次,我们将使用逻辑回归来统计检验目标变量y与自变量之间是否存在关系。在这个练习中,我们将使用euribor3m变量。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 接下来,执行逻辑回归:

    h.test2 <- glm(y ~ euribor3m, data = df, family = "binomial")
    
  4. 打印测试摘要:

    summary(h.test2)
    

    输出如下:

    Call:
    glm(formula = y ~ euribor3m, family = "binomial", data = df)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -0.8568  -0.3730  -0.2997  -0.2917   2.5380  
    Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
    (Intercept) -0.472940   0.027521  -17.18   <2e-16 ***
    euribor3m   -0.536582   0.009547  -56.21   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 28999  on 41187  degrees of freedom
    Residual deviance: 25343  on 41186  degrees of freedom
    AIC: 25347
    Number of Fisher Scoring iterations: 5
    

专注于前面的输出,我们可以确认我们可以拒绝零假设并接受备择假设。因此,目标结果与欧元利率有统计学上的显著关系,并且,正如我们所看到的,随着利率的降低,竞选转化的可能性更高。

分类因变量和分类自变量

接下来,让我们看一下第三个假设。为了测试分类因变量和分类自变量之间的关系,我们可以使用卡方检验。

对于假设 3,我们定义如下:

  • 零假设:活动的结果与从未结婚的客户之间没有关系。

  • 备择假设:活动的结果与从未结婚的客户之间存在关系。

在以下练习中,我们将利用 R 的卡方检验函数来验证假设。

练习 38:对分类因变量和分类自变量进行假设 3 的测试

在这个练习中,我们将使用卡方检验进行统计分析。我们使用卡方检验是因为独立变量和因变量都是分类的,尤其是在测试y与婚姻状况之间的关系时。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. single客户创建一个标志:

    df$single_flag <- as.factor(ifelse(df$marital == "single","single","other"))
    
  4. 创建一个sample对象并打印其值:

    sample <- table(df$y, df$single_flag)
    print(sample)
    

    输出如下:

      other single
    no  26600   9948
    yes  3020   1620
    
  5. 执行卡方检验:

    h.test3 <- chisq.test(sample)
    
  6. 打印测试摘要:

    print(h.test3)
    

    输出如下:

    Pearson's Chi-squared test with Yates' continuity correction
    data:  sample
    X-squared = 120.32, df = 1, p-value < 2.2e-16
    

我们首先为这个测试创建一个新的变量/标志,其中我们定义客户是否为single。由于我们专门定义了目标和客户的single婚姻状况之间的关系,我们屏蔽了婚姻状况中的所有其他类别。

table命令创建了一个新的 DataFrame,其中包含每个个体类别之间的简单频率分布。最后,我们使用这个 DataFrame 来执行卡方检验。

如我们所见,零假设为真的 p 值或概率远小于 5%。因此,我们可以接受我们的备择假设,这证实了事实,即活动的结果是由单个客户而不是其他客户积极影响的。

接下来,让我们快速看一下我们的第 4 个和第 5 个假设的有效性。

对于第 4 个和第 5 个假设,我们定义如下:

  • 零假设:活动的结果与学生或退休的客户之间没有关系。活动的结果与使用的联系方式没有关系。

  • 备择假设:活动的结果与学生或退休的客户之间没有关系。活动的结果与使用的联系方式有关。

练习 39:对分类因变量和分类自变量进行假设 4 和 5 的测试

再次使用卡方检验来统计地检查目标变量y、分类自变量job_flagcontact之间是否存在关系。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 准备自变量:

    df$job_flag <- as.factor(ifelse(df$job %in% c("student","retired"),as.character(df$job),"other"))
    df$contact <- as.factor(df$contact)
    
  4. 创建一个名为sample4的对象并打印其值:

    sample4 <- table(df$y, df$job_flag)
    print("Frequency table for Job")
    print(sample4)
    

    输出如下:

    [1] "Frequency table for Job"
      other retired student
    no  34662    1286     600
    yes  3931     434     275
    
  5. 对第 4 个假设进行测试:

    h.test4 <- chisq.test(sample4)
    
  6. 打印第 4 个假设的测试摘要:

    print("Hypothesis #4 results")
    print(h.test4)
    

    输出如下:

    [1] "Hypothesis #4 results"
    Pearson's Chi-squared test
    data:  sample4
    X-squared = 736.53, df = 2, p-value < 2.2e-16
    
  7. 现在,创建一个新的sample5对象并打印其值:

    print("Frequency table for Contact")
    sample5 <- table(df$y, df$contact)
    print(sample5)
    

    输出如下:

    [1] "Frequency table for Contact"
      cellular telephone
    no     22291     14257
    yes     3853       787
    
  8. test5变量进行测试:

    h.test5 <- chisq.test(sample5)
    
  9. 打印第 5 个假设的测试摘要:

    print("Hypothesis #5 results")
    print(h.test5)
    

    输出如下:

    [1] "Hypothesis #5 results"
    Pearson's Chi-squared test with Yates' continuity correction
    data:  sample5
    X-squared = 862.32, df = 1, p-value < 2.2e-16
    

我们可以看到结果已经得到了验证。我们还可以看到学生和退休客户之间以及与活动相关的通信方式之间肯定存在一种关系。

汇总见解 – 精炼问题的解决方案

现在我们已经遍历了 EDA 的长度和宽度。在不同的部分,我们以不同的深度研究了数据。现在我们有了数据探索问题的有效答案,我们可以再次与最初定义的问题接触。如果你还记得问题陈述中的复杂性问题部分,我们问的是“是什么因素导致了活动的表现不佳”。嗯,我们现在基于我们在双变量分析中发现的模式,并通过统计测试验证了答案。

将所有与正确故事验证的假设整理在一起,为我们的问题带来了解决方案。花些时间仔细研究每个假设测试结果,以编织故事。每个假设都告诉我们一个自变量是否与一个因变量有关系。

摘要

在本章中,我们通过一个实际案例研究探索了 EDA,并遍历了商业问题。我们首先理解了执行数据科学问题的整体流程,然后使用行业标准框架定义我们的商业问题。通过使用案例与适当的问题和复杂性相结合,我们理解了 EDA 在为问题设计解决方案中的作用。在探索 EDA 之旅中,我们研究了单变量、双变量和多变量分析。我们使用分析技术和视觉技术相结合的方法进行数据分析。通过这种方式,我们探索了用于可视化的 R 包,即ggplot,以及通过dplyr进行数据整理的一些包。我们还通过统计测试验证了我们的见解,并最终整理了记录的见解,以便与原始问题陈述回溯。

在下一章中,我们将为各种机器学习算法奠定基础,并深入讨论监督学习。

第三章:监督学习简介

学习目标

到本章结束时,你将能够:

  • 解释监督学习和机器学习工作流程

  • 使用和探索北京 PM2.5 数据集

  • 解释连续和分类因变量的区别

  • 在 R 中实现基本的回归和分类算法

  • 区分监督学习与其他类型机器学习的关键差异

  • 与监督学习算法的评估指标一起工作

  • 进行模型诊断,以避免系数估计偏差和大的标准误差

在本章中,我们将介绍监督学习,并通过实际案例展示构建机器学习模型的流程。

简介

在前面的章节中,我们探讨了 R 的一些包,如dplyrplyrlubridateggplot2,其中我们讨论了在 R 中存储和处理数据的基本方法。后来,这些思想被用于探索性数据分析(EDA),以了解如何将数据分解成更小的部分,从数据中提取洞察力,并探索其他更好地理解数据的方法,在尝试高级建模技术之前。

在本章中,我们将进一步介绍机器学习思想。在广泛地为思考机器学习中的各种算法打下基础的同时,我们将详细讨论监督学习。

监督学习基于由领域专家良好标记的数据。对于从图像中分类猫和狗,算法首先需要看到标记为猫和狗的图像,然后根据标签学习特征。大多数拥有大量历史数据的企业的最大受益者是从这些数据中提取的丰富知识。如果数据干净且标注良好,监督学习可以实现高预测精度,这与其他机器学习算法不同,其他机器学习算法通常在开始时会产生较大的误差。在没有正确标签的情况下,从数据中提取任何意义变得困难,除了能够进行探索性分析和聚类之外。

在解决现实世界问题(如预测贷款违约(是/否)、工厂制造机器故障(是/否)、自动驾驶汽车中的目标检测(道路、汽车、信号)、预测股票市场价格(数值))时,标准组件是一组输入(特征)和一个给定的输出(标签),这通常是从历史数据中获得的。当我们预测定量输出时,我们称之为回归,当我们预测定性输出时,我们称之为分类

北京 PM2.5 数据集概述

在许多国家的城市和农村地区,主要污染物,细颗粒物,是导致人类许多健康风险的原因,同时也影响气候变化。特别是,PM2.5,定义为直径小于 2.5 µm 的气溶胶颗粒,是大气颗粒物的主要类别。各种研究将 PM2.5 与严重的健康问题联系起来,如心脏病和中风。本节中的表格显示了大气颗粒物的类型及其微米级的尺寸分布。

在本章和后续章节中,我们将使用研究论文《评估北京的 PM2.5 污染:严重程度、天气影响、APEC 和冬季供暖》的作者发布的数据集,其中他们使用位于北京 116.47 E,39.95 N 的美国大使馆的每小时 PM2.5 读数,以及从 weather.nocrew.org 获得的每小时气象测量数据,这些数据是在北京首都国际机场BCIA)获得的。他们的研究声称是首次在中国 PM2.5 污染中结合了 PM2.5 和气象数据。以下表格描述了数据集中的属性:

![图 3.1:北京 PM2.5 数据集的属性。图片

图 3.1:北京 PM2.5 数据集的属性。

练习 40:探索数据

在这个练习中,我们将通过每个属性的样本值学习数据的结构,并使用summary函数。我们将看到数值变量的五个数字摘要统计量。

执行以下步骤以完成此练习:

  1. 首先,使用以下命令将北京 PM2.5 数据集读入 PM25 数据框对象:

    PM25 <- read.csv("https://raw.githubusercontent.com/TrainingByPackt/Applied-Supervised-Learning-with-R/master/Lesson03/PRSA_data_2010.1.1-2014.12.31.csv")
    
  2. 接下来,使用str命令打印具有样本值的数据结构:

    str(PM25)
    

    上一条命令的输出如下:

    'data.frame':	43824 obs. of  13 variables:
     $ No   : int  1 2 3 4 5 6 7 8 9 10 ...
     $ year : int  2010 2010 2010 2010 2010 2010 2010 2010 2010 2010 ...
     $ month: int  1 1 1 1 1 1 1 1 1 1 ...
     $ day  : int  1 1 1 1 1 1 1 1 1 1 ...
     $ hour : int  0 1 2 3 4 5 6 7 8 9 ...
     $ pm2.5: int  NA NA NA NA NA NA NA NA NA NA ...
     $ DEWP : int  -21 -21 -21 -21 -20 -19 -19 -19 -19 -20 ...
     $ TEMP : num  -11 -12 -11 -14 -12 -10 -9 -9 -9 -8 ...
     $ PRES : num  1021 1020 1019 1019 1018 ...
     $ cbwd : Factor w/ 4 levels "cv","NE","NW",..: 3 3 3 3 3 3 3 3 3 3 ...
     $ Iws  : num  1.79 4.92 6.71 9.84 12.97 ...
     $ Is   : int  0 0 0 0 0 0 0 0 0 0 ...
     $ Ir   : int  0 0 0 0 0 0 0 0 0 0 ...
    

    注意

    观察到数据集包含43824个观测值和 13 个属性。观察到数据集包含 2010 年至 2014 年的数据。pm2.5、温度、气压、综合风向、累积风速、累积降雪小时数和累积降雨小时数的值每天每小时都会汇总。

  3. 现在,让我们展示数据集的摘要统计量:

    summary(PM25)
    

    输出如下:

           No             year          month             day             hour           pm2.5       
     Min.   :    1   Min.   :2010   Min.   : 1.000   Min.   : 1.00   Min.   : 0.00   Min.   :  0.00  
     1st Qu.:10957   1st Qu.:2011   1st Qu.: 4.000   1st Qu.: 8.00   1st Qu.: 5.75   1st Qu.: 29.00  
     Median :21912   Median :2012   Median : 7.000   Median :16.00   Median :11.50   Median : 72.00  
     Mean   :21912   Mean   :2012   Mean   : 6.524   Mean   :15.73   Mean   :11.50   Mean   : 98.61  
     3rd Qu.:32868   3rd Qu.:2013   3rd Qu.:10.000   3rd Qu.:23.00   3rd Qu.:17.25   3rd Qu.:137.00  
     Max.   :43824   Max.   :2014   Max.   :12.000   Max.   :31.00   Max.   :23.00   Max.   :994.00  
                                                                                     NA's   :2067    
          DEWP              TEMP             PRES      cbwd            Iws               Is          
     Min.   :-40.000   Min.   :-19.00   Min.   : 991   cv: 9387   Min.   :  0.45   Min.   : 0.00000  
     1st Qu.:-10.000   1st Qu.:  2.00   1st Qu.:1008   NE: 4997   1st Qu.:  1.79   1st Qu.: 0.00000  
     Median :  2.000   Median : 14.00   Median :1016   NW:14150   Median :  5.37   Median : 0.00000  
     Mean   :  1.817   Mean   : 12.45   Mean   :1016   SE:15290   Mean   : 23.89   Mean   : 0.05273  
     3rd Qu.: 15.000   3rd Qu.: 23.00   3rd Qu.:1025              3rd Qu.: 21.91   3rd Qu.: 0.00000  
     Max.   : 28.000   Max.   : 42.00   Max.   :1046              Max.   :585.60   Max.   :27.00000  
    
           Ir         
     Min.   : 0.0000  
     1st Qu.: 0.0000  
     Median : 0.0000  
     Mean   : 0.1949  
     3rd Qu.: 0.0000  
     Max.   :36.0000
    

以下图像是大气颗粒物尺寸分布(微米)的图形表示:

![图 3.2:大气颗粒物的类型和尺寸分布(微米)。图片

图 3.2:大气颗粒物的类型和尺寸分布(微米)。
来源:https://en.wikipedia.org/wiki/File:Airborne-particulate-size-chart.svg

注意

发表在《胸科疾病杂志》(JTD)上的文章《PM2.5 对人类呼吸系统的影响》的作者讨论了空气污染与呼吸系统疾病之间的关联。他们提供了一种全面的数据驱动方法来解释导致此类呼吸疾病的原因。特别关注北京,研究人员已经广泛研究了 PM2.5 上升的负面影响,并已成为全球各种气候变化论坛的主流讨论点。更多细节可以在文章中找到,链接为 https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4740125/。

回归问题和分类问题

在我们的日常生活中,我们随处可见分类和回归问题。例如,从 https://weather.com 获取降雨概率,我们的电子邮件被过滤到垃圾邮件箱和收件箱,我们的个人和家庭贷款被批准或拒绝,决定选择下一个假日目的地,探索购买新房的选项,投资决策以获得短期和长期收益,从亚马逊购买下一本书;这个列表可以一直继续下去。我们周围的世界今天正越来越多地由帮助我们做出选择的算法(这并不总是好事)来运行。

如第二章数据探索分析中所述,我们将使用被称为情境-复杂性-问题SCQ)的Minto 金字塔原则来定义我们的问题陈述。以下表格展示了针对北京的 PM2.5 问题的 SCQ 方法:

![图 3.3:应用 SCQ 解决北京的 PM2.5 问题。图片

![图 3.3:应用 SCQ 解决北京的 PM2.5 问题。现在,在前面表格中描述的 SCQ 构造中,我们可以进行简单的相关性分析,以确定影响 PM2.5 水平的因素,或者创建一个预测问题(预测意味着找到一个近似函数,将输入变量映射到输出),使用所有因素估计 PM2.5 水平。为了术语的清晰,我们将把因素称为输入变量。然后,PM2.5 成为因变量(通常被称为输出变量)。因变量可以是分类的或连续的。例如,在电子邮件分类到 SPAM/非 SPAM 的问题中,因变量是分类的。以下表格突出了回归问题和分类问题之间的一些关键区别:![图 3.4:回归问题和分类问题之间的区别。图片
图 3.4:回归问题和分类问题之间的区别。

机器学习工作流程

为了演示构建预测模型(机器学习或监督学习)的端到端过程,我们创建了一个易于理解的流程。第一步是设计问题,然后是获取和准备数据,这导致为训练和评估编码模型,最后部署模型。在本章的范围内,我们将保持模型解释的简洁,因为它将在第四章和第五章中详细讨论。

下图描述了从准备数据到部署模型所需的整个工作流程:

![图 3.5:机器学习工作流程。图片 C12624_03_05.jpg

图 3.5:机器学习工作流程。

设计问题

一旦确定了工作领域,就会进行问题设计的头脑风暴。首先,将问题定义为回归问题或分类问题。一旦完成,我们选择正确的目标变量,并识别特征。目标变量很重要,因为它决定了训练的方式。监督学习算法将目标变量放在中心,同时试图从给定的特征集中找到模式。

数据来源和准备

数据收集和准备是一项费力的工作,尤其是在数据来源多样且众多的情况下。对于每个数据源,挑战都是不同的,因此处理所需的时间也会有所不同。如果表格数据不包含大量垃圾信息,那么具有表格数据的数据源是最容易处理的,而文本数据由于其自由流动的特性,清理起来最为困难。

编码模型

一旦数据准备就绪,我们就开始选择合适的模型。通常,专家们首先选择一个基线模型,以评估算法使用输入特征和目标变量的可预测性。然后,可以直接尝试最先进的算法,或者决定采用试错法(尝试使用所有可能的模型)。必须理解的是,没有绝对正确或错误的模型,一切取决于数据。在编码过程中,数据被随机分为训练集和测试集。代码被编写来在训练数据集上训练模型,评估则在测试数据上进行,这确保了模型在实际部署时不会表现不佳。

训练和评估

模型评估是模型的重要组成部分,其中决定了其在实际中的可用性。基于给定的一组模型评估指标,我们需要在经过多次尝试和错误后,决定最佳模型。在每次迭代中,计算如 R 平方值、准确率、精确率和 F 分数等指标。通常,整个数据被分为训练数据和测试数据(通常还包括一个用于验证的第三部分)。模型在训练数据上训练,在测试数据上测试。这种分离确保模型不会进行任何机械学习。在更技术性的术语中,模型不会过拟合(关于这一点,请参阅本章的评估指标部分)。通常,在这个工作流程的阶段,一个人可以决定返回并包括更多变量,训练模型,然后重新部署。这个过程会重复进行,直到模型的准确率(或其他重要指标)达到平台期。

我们使用随机数生成函数,如 R 中的sample()函数,将数据随机分割成不同的部分,就像在下一个练习 2 的第 2 步中所做的那样。

练习 41:从北京 PM2.5 数据集中随机生成训练和测试数据集

在这个练习中,我们将从北京 PM2.5 数据集中创建一个随机生成的训练和测试数据集。我们将重用之前练习中创建的PM25对象。

执行以下步骤:

  1. 创建一个num_index变量,并将其设置为北京 PM2.5 数据集中观测值的数量:

    num_index <- nrow(PM25)
    
  2. 使用sample()函数,随机选择num_index值的 70%,并将它们存储在train_index中:

    train_index <- sample(1:num_index, 0.7*nrow(PM25))
    
  3. 使用train_index从北京 PM2.5 数据集中选择一个随机子集的行,并将它们存储到一个名为PM25_Train的 DataFrame 中:

    PM25_Train <- PM25[train_index,]
    
  4. 将剩余的观测值存储到一个名为PM25_Test的 DataFrame 中:

    PM25_Test <- PM25[-train_index,]
    

练习展示了创建训练和测试集的简单示例。随机选择的训练和测试集确保模型没有偏见,并在用于现实世界中的未见数据之前,从所有可能的示例中学习得很好。

部署模型

一旦选定了最佳模型,下一步就是使模型输出能够被业务应用使用。该模型以表示状态转移REST)API 的形式托管。这些 API 是以端点托管 Web 应用的方式,它监听对模型调用的任何请求,通常返回一个 JSON 对象作为响应。

模型的部署正成为工业界所有机器学习项目的必要部分。一个不可部署的模型对公司来说毫无价值,也许仅仅是为了研发目的。越来越多的专业人士正在专注于模型部署,这有时是一个繁琐且复杂的过程。为了给模型部署应有的重视,我们为其专门设立了一章,即第八章,模型部署

回归

现在我们已经看到了机器学习的工作流程,我们将探讨两种广泛使用的机器学习算法:回归和分类;两者都采用监督学习来训练模型。本书的整个主题围绕这两种类型的算法展开。北京 PM2.5 数据集将在演示这两种类型时被广泛使用。该数据集有助于理解如何将回归问题转换为分类问题,反之亦然。

简单和多重线性回归

回归是分析学和计量经济学(关注于使用数学方法,尤其是统计学,来描述经济系统的经济学分支)中最有用和最基本的工具之一。在许多方面,现代机器学习的根源在于统计学,这主要归功于弗朗西斯·高尔顿爵士的工作。高尔顿是一位对遗传学、心理学和人类学等领域有深厚兴趣和专长的英国维多利亚时代统计学家和博学家。他是第一个将统计方法应用于研究人类行为和智力的人。值得注意的是,他的出版物《遗传身高回归到中等》基于回归有许多有洞察力的发现。

在本节中,我们将简要分析影响 PM2.5 水平的各种因素,使用北京数据集。特别是,我们将探讨露点、温度、风速和压力等变量对 PM2.5 的影响。

线性回归模型中的假设

由于回归从应用统计学中借鉴了许多概念来建模数据,因此它伴随着许多假设。我们不应将回归算法应用于任何数据集或问题。在我们构建任何模型之前,让我们先检查线性回归的假设。

下表显示了假设以及我们如何从统计上测试线性回归模型是否遵循该假设。该表还显示了一些如果假设被违反的纠正措施。我们将在第四章“回归”中详细讨论这些假设,并执行诊断分析以识别违规情况。

图 3.6:线性回归模型中的各种假设(第一部分)。

图 3.6:线性回归模型中的各种假设(第一部分)。

图 3.7:线性回归模型中的各种假设(第二部分)。

图 3.7:线性回归模型中的各种假设(第二部分)。

探索性数据分析(EDA)

构建回归模型需要对目标变量和输入变量之间的模式和关系进行深入分析。北京数据集提供了大量可能影响大气中 PM2.5 水平的环境因素。

练习 42:探索北京 PM2.5 数据集中 PM2.5、DEWP、TEMP 和 PRES 变量的时间序列视图

在这个练习中,我们将可视化pm2.5DEWPTEMPPRES变量在时间序列图中的表现,并观察这些变量在多年中可能出现的任何模式。

执行以下步骤来完成练习:

  1. 在系统中导入所有必需的库:

    library(dplyr)
    library(lubridate)
    library(tidyr)
    library(grid)
    library(ggplot2)
    
  2. 接下来,使用lubridate包的ymd_h函数将年、月和小时转换为日期时间:

    PM25$datetime <- with(PM25, ymd_h(sprintf('%04d%02d%02d%02d', year, month, day,hour)))
    
  3. 使用以下命令绘制所有年份的 PM2.5、TEMP、DEWP 和 PRES:

    plot_pm25 <- PM25 %>%
      select(datetime, pm2.5) %>%
      na.omit() %>%
      ggplot() + 
      geom_point(aes(x = datetime, y = pm2.5), size = 0.5, alpha = 0.75) +
      ylab("PM2.5")
    plot_TEMP <- PM25 %>%
      select(datetime, TEMP) %>%
      na.omit() %>%
      ggplot() + 
      geom_point(aes(x = datetime, y = TEMP), size = 0.5, alpha = 0.75) +
      ylab("TEMP")
    plot_DEWP <- PM25 %>%
      select(datetime, DEWP) %>%
      na.omit() %>%
      ggplot() + 
      geom_point(aes(x = datetime, y = DEWP), size = 0.5, alpha = 0.75) +
      ylab("DEWP")
    plot_PRES <- PM25 %>%
      select(datetime, PRES) %>%
      na.omit() %>%
      ggplot() + 
      geom_point(aes(x = datetime, y = PRES), size = 0.5, alpha = 0.75) +
      ylab("PRES")
    
  4. 现在,使用以下命令来绘制图表:

    grid.newpage()
    grid.draw(rbind(ggplotGrob(plot_pm25), ggplotGrob(plot_TEMP),ggplotGrob(plot_DEWP),ggplotGrob(plot_PRES), size = "last"))
    

    图如下:

![图 3.8:散点图显示了环境因素(如温度、露点和压力)的趋势和季节性,以及 2010 年至 2014 年底北京地区的 PM2.5 水平。图 C12624_03_08.jpg

图 3.8:散点图显示了环境因素(如温度、露点和压力)的趋势和季节性,以及 2010 年至 2014 年底北京地区的 PM2.5 水平。

在这个练习中,我们首先展示了数据集中PM2.5DEWPTEMPPRES变量的时间序列视图,并观察其模式。如图图 3.8所示,观察到DEWPTEMPPRES具有明显的季节性(每 12 个月重复相同的模式),而 PM2.5 似乎具有随机模式。这是早期迹象,表明我们不太可能看到三个变量对 PM2.5 有任何影响。然而,让我们进一步探究这个假设,使用相关性图并观察变量之间是否存在任何关系。

练习 43:进行相关性分析

在这个练习中,我们将进行相关性分析,研究各种因素之间关系的强度。

执行以下步骤来完成练习:

  1. 使用以下命令将corrplot包导入到系统中:

    library(corrplot)
    
  2. 现在,创建一个新的对象并将PM25中的所需值存储在其中:

    corr <- cor(PM25[!is.na(PM25$pm2.5),c("pm2.5","DEWP","TEMP","PRES","Iws","Is","Ir")])
    
  3. 使用corrplot包来显示相关性矩阵的图形表示:

    corrplot(corr)
    

    图如下:

![图 3.9:北京数据集中所有变量对的关联性。图 C12624_03_09.jpg

图 3.9:北京数据集中所有变量对的关联性。

首先,我们计算所有变量之间的相关性。生成的相关性图显示,PM2.5 与其他变量之间似乎没有强烈的关联。然而,PM2.5DEWPTEMPIws显示出一些轻微的相关性,这表明存在某种关系。这并不令人惊讶,因为我们已经在图 3.8中看到,虽然三个变量遵循季节性趋势,但 PM2.5 似乎更随机。请注意,我们对数据集没有进行任何处理或转换;这些发现直接来自我们的第一层分析。我们将在第四章回归中详细说明。现在,让我们也使用散点图来可视化变量之间的关系。

练习 44:绘制散点图以探索 PM2.5 水平与其他因素之间的关系

在这个练习中,我们将使用散点图来探索 pm2.5 水平与其他因素之间的关系。我们希望看到是否会出现任何有趣的模式或关系。散点图是探索变量之间关系的简单而有效的可视化工具。

执行以下步骤来完成练习:

  1. ggplot2 包导入到您的系统中:

    library(ggplot2)
    
  2. month 变量作为颜色绘制 DEWPPM2.5 之间的散点图:

    ggplot(data = PM25, aes(x = DEWP, y = pm2.5, color = month)) +  geom_point() +  geom_smooth(method='auto',formula=y~x, colour = "red", size =1.5)
    

    散点图如下:

    ![图 3.10:显示 DEWP 和 PM2.5 水平之间关系的散点图。 img/C12624_03_10.jpg

    图 3.10:显示 DEWP 和 PM2.5 水平之间关系的散点图。
  3. month 变量作为颜色绘制 TEMPPM2.5 之间的散点图:

    ggplot(data = PM25, aes(x = TEMP, y = pm2.5, color = month)) +  geom_point() +  geom_smooth(method='auto',formula=y~x, colour = "red", size =1.5)
    

    散点图如下:

    ![图 3.11:显示 TEMP 和 PM2.5 水平之间关系的散点图。 img/C12624_03_11.jpg

    图 3.11:显示 TEMP 和 PM2.5 水平之间关系的散点图。
  4. 创建一个以一天中的小时为颜色,按月份分别显示的 DEWPPM2.5 之间的散点图:

    ggplot(data = PM25, aes(x = DEWP, y = pm2.5, color = hour)) +  geom_point() +  geom_smooth(method='auto',formula=y~x, colour = "red", size =1) +  facet_wrap(~ month, nrow = 4)
    

    散点图如下:

![图 3.12:按年份月份拆分的 DEWP 和 PM2.5 关系的散点图。img/C12624_03_12.jpg

图 3.12:按年份月份拆分的 DEWP 和 PM2.5 关系的散点图。

为了评估变量之间的一些关系,我们使用 PM2.5DEWP 之间的散点图并添加了拟合线。观察代码中,我们向 geom_smooth() 传递了一个参数,即 method = "auto",它会根据数据自动决定使用哪种模型来拟合直线。如图 图 3.10 所示,直线不是线性的。geom_smooth 方法选择了 TEMPPM2.5 的绘图,如图 图 3.11 所示。然而,我们可以更进一步,按月份拆分散点图,如图 图 3.12 所示。这表明存在线性关系,但它高度依赖于季节。例如,在四月(用整数 4 表示),DEWPPM2.5 有一个近乎完美的直线拟合。我们将在 第四章回归 中进一步讨论这个话题。

因此,我们已经看到了一些假设的违反和环境污染因素与 PM2.5 之间缺乏强相关性的情况。然而,似乎还有进一步审查的空间。在本章关于监督学习的介绍中,我们只关注基于我们的机器学习工作流程的方法。

备注

要了解更多关于 GAM 的信息,请参阅此文档:https://www.stat.cmu.edu/~cshalizi/uADA/12/lectures/ch13.pdf。

活动 5:按月份绘制 PRES 和 PM2.5 的散点图

在这个活动中,我们将创建DWEPPM2.5之间的散点图。通过这个活动,我们将学习使用facet_wrap()函数在ggplot()之上创建一个层,将散点图的可视化分割到每个月,从而帮助观察任何季节性模式。

执行以下步骤来完成活动:

  1. ggplot中,使用PRES变量分配a()方法的组件。

  2. geom_smooth()方法的下一层,将colour = "blue"设置为区分。

  3. 最后,在facet_wrap()层中,使用month变量为每个月绘制单独的隔离图。

    图表如下:

![图 3.13:显示 PRES 和 PM2.5 之间关系的散点图。图片

图 3.13:显示 PRES 和 PM2.5 之间关系的散点图。

注意

该活动的解决方案可以在第 445 页找到。

模型构建

我们简要探讨了PM2.5与一些因素(如TEMPDEWP)之间的关系。同样的分析可以应用于其他变量,如PRESIwd等。在本节中,让我们创建一个线性模型。(即使我们知道模型的选择不是最好的,我们也不会犹豫去运行模型。机器学习中的试错法总是建立事实的最佳方式。)

通常,线性回归模型输入变量(自变量)和目标变量(因变量或解释变量)之间的线性关系。如果我们有一个解释变量,它被称为简单线性回归;如果有多个解释变量,则称为多元线性回归。以下方程是线性回归或线性预测函数的数学表示,其中包含p个解释变量和n个观测值:

图片

这里,每个包含家具、桌子、座椅、凳子自动生成的描述是一个列值的向量(解释变量)对于包含家具的图片自动生成的描述是未知的参数或系数。包含家具、座椅自动生成的描述,这使得这个方程适合简单的线性回归。有许多算法可以将这个函数拟合到数据上。最流行的一个是普通最小二乘法OLS)。我们将在下一章关于回归的章节中详细讨论 OLS。

另一种思考方式是包含家具的图片自动生成的描述是它是一个线性预测函数,尽可能地将观测值拟合到图片维度的空间中,最小化残差平方和(目标值的实际值与预测值之间的差异)。

在接下来的练习中,我们将跳过将数据集分为训练集和测试集的步骤,因为我们仍然处于探索阶段,尚未决定正式进行建模练习。(我们将在下一章中涉及这一点。)我们将使用 R 中的 lm() 方法构建线性模型。同样,关于这一点将在下一章中详细介绍。目前,只需注意 lm() 方法使用一个或多个输入变量将目标变量拟合到一条直线。在简单线性回归中,我们只使用一个变量来拟合直线,而在多重线性回归中,我们可以使用多个变量。

练习 45:探索简单和多重回归模型

在这个练习中,我们将探索简单和多重回归模型。

执行以下步骤以完成练习:

  1. 将所需的库和包导入 R-Studio。

  2. 接下来,创建一个名为 simple_PM25_linear_model 的 DataFrame 对象,并使用 lm() 方法构建线性模型:

    simple_PM25_linear_model <- lm(pm2.5 ~ DEWP, data = PM25)
    
  3. 使用如下所示的方法,使用 summary 方法打印对象的摘要:

    summary(simple_PM25_linear_model)
    

    输出如下:

    Call:
    lm(formula = pm2.5 ~ DEWP, data = PM25)
    Residuals:
        Min      1Q  Median      3Q     Max 
    -115.47  -61.26  -28.75   33.83  923.54 
    Coefficients:
                Estimate Std. Error t value Pr(>|t|)    
    (Intercept) 96.69984    0.44705  216.31   <2e-16 ***
    DEWP         1.09325    0.03075   35.55   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    Residual standard error: 90.69 on 41755 degrees of freedom
      (2067 observations deleted due to missingness)
    Multiple R-squared:  0.02939,	Adjusted R-squared:  0.02936 
    F-statistic:  1264 on 1 and 41755 DF,  p-value: < 2.2e-16
    
  4. 接下来,创建另一个 DataFrame 对象,并使用 lm() 方法构建线性模型:

    multiple_PM25_linear_model <- lm(pm2.5 ~ DEWP+TEMP+Iws, data = PM25)
    
  5. 使用 summary 函数打印模型对象的摘要:

    summary(multiple_PM25_linear_model)
    

    输出如下:

    A)
    ______________________________________________________________
    Call:
    lm(formula = pm2.5 ~ DEWP + TEMP + Iws, data = PM25)
    Residuals:
        Min      1Q  Median      3Q     Max 
    -149.02  -53.74  -16.61   34.14  877.82 
    ______________________________________________________________
    B)
    ______________________________________________________________
    Coefficients:
                  Estimate Std. Error t value Pr(>|t|)    
    (Intercept) 161.151207   0.768727  209.63   <2e-16 ***
    DEWP          4.384196   0.051159   85.70   <2e-16 ***
    TEMP         -5.133511   0.058646  -87.53   <2e-16 ***
    Iws          -0.274337   0.008532  -32.15   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ______________________________________________________________
    C)
    ______________________________________________________________
    Residual standard error: 81.51 on 41753 degrees of freedom
      (2067 observations deleted due to missingness)
    Multiple R-squared:  0.216,	Adjusted R-squared:  0.2159 
    F-statistic:  3834 on 3 and 41753 DF,  p-value: < 2.2e-16
    ______________________________________________________________
    

模型解释

现在,基于简单和多重线性回归模型的先前输出,让我们尝试理解输出中的每一部分代表什么。在本书的这个阶段,了解每一部分的意义就足够了;我们将在 第四章回归 中讨论结果。

  • 使用 lm() 方法与因变量和自变量,用 ~ 符号表示的公式形式表示。这类似于我们的线性预测函数。在简单的回归模型中,只有一个变量——DEWP——而在多重模型中,有 DEWPTEMPIws。您还可以看到残差的五个汇总统计量(最小值、第一四分位数、中位数、第三四分位数和最大值)。这表明预测值与实际值之间的差距。

  • X_j 部分纳入我们的预测方程中,我们将得到预测值。名为 Std. Error 的列是估计的标准误差。t 值是通过将 EstimateStd. Error 的比率得到的,而 p 值突出了估计的统计显著性。视觉线索,即 * 和 . 符号是基于 p 值的。小于 0.001 的值得到三个星号,而介于 0.1 和 0.05 之间的值得到一个 .(点)。三个星号意味着最佳情况,即对应于自变量的估计是显著的并且对预测(或解释)因变量是有用的。换句话说,p 值有助于确定回归模型相对于零模型(仅因变量的均值)的显著性。

  • 部分 C

    这一部分展示了模型的功效。最重要的观察值是 R 平方和调整 R 平方值,这些是表示回归模型中由独立变量(s)解释的因变量变化的百分比统计量。

在本章中查看关于评估指标的章节,以了解模型在 R 平方和调整 R 平方指标上的表现解释。

分类

与回归算法类似,分类也通过依赖或目标变量进行学习,并使用所有预测或独立变量来找到正确的模式。主要区别在于,在分类中,目标变量是分类的,而在回归中,它是数字的。在本节中,我们将通过使用北京 PM2.5 数据集来介绍逻辑回归,以展示这一概念。

逻辑回归

逻辑回归是用于二元分类的最受欢迎的透明模型。透明模型定义为提供对预测中整个推理过程的可见性的模型。对于每个做出的预测,我们可以利用模型的数学方程式并解码预测的原因。还有一些完全属于黑盒的分类模型,也就是说,我们无论如何都无法理解模型利用的预测推理。在我们只想关注最终结果的情况下,我们应该更喜欢黑盒模型,因为它们更强大。

简要介绍

虽然名称以回归结尾,但逻辑回归是一种用于预测二元分类结果的技巧,因此是分类问题的良好选择。如前节所述,我们需要一种不同的方法来对分类结果进行建模。这可以通过将结果转换为优势比的对数或事件发生的概率来实现。

让我们将这种方法提炼成更简单的结构。假设一个事件成功的概率为 0.7。那么,同一事件失败的概率将被定义为1 – 0.7 = 0.3。成功的优势被定义为成功概率与失败概率的比率。成功的优势将是0.7/0.3 = 2.33,即成功的优势是 2 比 1。如果成功的概率是 0.5,即 50-50 的机会,那么成功的优势是 1 比 1。逻辑回归模型可以用以下方式数学表示:

![图片 C12624_03_27.jpg]

这里,一个人的插图自动生成的描述是优势比的对数,也称为logit函数。进一步解决数学问题,我们可以推导出结果概率,如下所示:

![图片 C12624_03_29.jpg]

讨论方程的数学背景和推导超出了本章的范围。然而,为了总结,logit 函数(或逻辑函数),作为连接函数,帮助逻辑回归直观地将预测结果(预测结果)重新表述为优势比的对数。这当解决时,有助于我们预测二元因变量的概率。

逻辑回归的机制

就像线性回归一样,其中变量的 beta 系数使用 OLS 方法估计,逻辑回归模型利用最大似然估计MLE)方法。MLE 函数估计模型参数或 beta 系数的最佳值集,以最大化似然函数,即概率估计。它也可以定义为所选模型与观察数据之间的一致性。当估计出最佳参数值集时,将这些值/β系数插入到前面定义的模型方程中,有助于估计给定样本的输出概率。类似于 OLS,MLE 是一个迭代过程。

模型构建

就像在 R 中使用线性回归构建逻辑回归模型一样,我们有glm()广义线性模型方法来拟合数据和 logit 函数来评分观测值。

使用 glm()函数的语法如下:

glm(Y ~ X1 + X2 + X3, data = <train_data>,family=binomial(link='logit'))

在这里,Y 是我们的因变量,X1、X2 和 X3 是自变量。data 参数将采用训练数据集。family 参数设置为 binomial(link='logit'),这适合逻辑回归模型。

练习 46:将北京 PM2.5 数据集中的滚动 3 小时平均值存储

在这个练习中,我们将创建一个新变量,该变量存储北京 PM2.5 数据集中 PM2.5 变量的滚动 3 小时平均值。滚动平均值将平滑 PM2.5 读数中的任何噪声。

让我们使用zoo包中的rollapply方法来完成练习:

  1. 小时合并成一个名为datetime的新变量:

    PM25$datetime <- with(PM25, ymd_h(sprintf('%04d%02d%02d%02d', year, month, day,hour)))
    
  2. 删除 NAs 并查看 PM2.5 数据集中pm2.5变量的前 6 个值:

    PM25_subset <- na.omit(PM25[,c("datetime","pm2.5")])
    head(PM25_subset$pm2.5)
    

    输出如下:

    [1] 129 148 159 181 138 109
    
  3. PM25_subset存储到以 datetime 为索引的有序观测值的zoo对象中,并打印前 6 个值:

    zoo(PM25_subset$pm2.5,PM25_subset$datetime)
    

    输出如下:

    2010-01-02 00:00:00 2010-01-02 01:00:00 2010-01-02 02:00:00 
                    129                 148                 159 
    2010-01-02 03:00:00 2010-01-02 04:00:00 2010-01-02 05:00:00 
                    181                 138                 109 
    
  4. 使用rollapply函数创建pm2.5变量的 3 小时滚动平均值,并打印前 6 个值:

    PM25_three_hour_pm25_avg <- rollapply(zoo(PM25_subset$pm2.5,PM25_subset$datetime), 3, mean)
    

    输出如下:

    2010-01-02 01:00:00 2010-01-02 02:00:00 2010-01-02 03:00:00 
               145.3333            162.6667            159.3333 
    2010-01-02 04:00:00 2010-01-02 05:00:00 2010-01-02 06:00:00 
               142.6667            117.3333            112.6667 
    

注意到145.33值是三个小时的pm2.5变量的平均值,如步骤 3 所示(129148159)。

活动 6:转换变量并推导新变量以构建模型

在这个活动中,我们将在构建模型之前执行一系列转换并推导新变量。我们需要将pm2.5变量转换为分类变量,以便应用逻辑回归模型。

在我们构建逻辑回归分类模型之前,需要执行以下步骤:

  1. 将年、月、日和小时合并成一个名为 datetime 的新变量。

  2. 使用 datetime 变量,计算 3 小时窗口内 pm2.5 值的平均值。将这个新变量命名为 PM25_three_hour_pm25_avg

  3. 创建一个名为 pollution_level 的二进制变量。如果 PM25_three_hour_pm25_avg 大于 35,则其值为 1,否则为 0

  4. pollution_level 作为因变量,构建逻辑回归模型。

  5. 打印模型的摘要。

    最终输出如下:

    Call:
    glm(formula = pollution_level ~ DEWP + TEMP + Iws, family = binomial(link = "logit"), 
        data = PM25_for_class)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -2.4699  -0.5212   0.4569   0.6508   3.5824  
    Coefficients:
                  Estimate Std. Error z value Pr(>|z|)    
    (Intercept)  2.5240276  0.0273353   92.34   <2e-16 ***
    DEWP         0.1231959  0.0016856   73.09   <2e-16 ***
    TEMP        -0.1028211  0.0018447  -55.74   <2e-16 ***
    Iws         -0.0127037  0.0003535  -35.94   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 49475  on 41754  degrees of freedom
    Residual deviance: 37821  on 41751  degrees of freedom
    AIC: 37829
    Number of Fisher Scoring iterations: 5
    

    注意

    这个活动的解决方案可以在第 446 页找到。

解释模型

大部分 glm() 输出看起来与 lm() 方法相似,但有一些新的值,如下所示:

  • 零偏差

  • 残差偏差

  • 赤池信息量准则AIC

  • 费舍尔评分

为了避免评分,所有上述措施将在 第五章分类 中详细描述。

参考本章下一节关于 评估指标(基于 混淆矩阵指标 部分)的内容,以找到对模型在 R 平方和调整 R 平方指标上表现如何的解释。

评估指标

在本节中,我们将介绍所有用于评估机器学习模型预测质量的评估措施。根据因变量,我们有几种评估措施的选择。在我们机器学习工作流程的训练和评估步骤中,我们提到,在我们得到期望的结果之前,我们通过添加新变量或更改参数来迭代训练模型。在每次迭代中,我们尝试优化任何一个或两个评估指标。以下表格总结了用于回归、分类和推荐系统的各种类型指标。鉴于本书的范围,我们将更深入地探讨回归和分类算法的细节:

图 3.14:各种机器学习算法的指标

图片 C12624_03_14.jpg

图 3.14:各种机器学习算法的指标。

均方误差 (MAE)

绝对误差是无方向的,这意味着它并不重要,模型在测试数据集上预测的因变量的值是小于还是大于实际值。因此,在我们的北京 PM2.5 数据集示例中,MAE 将给出 PM2.5 预测的平均绝对误差(因变量的预测值和实际值之间的差异),不考虑误差的方向(正或负):

图片 C12624_03_30.jpg

这里,图片 C12624_03_31.png 是因变量的第 i 个观测值,图片 C12624_03_32.png 是预测值或期望值。

均方根误差 (RMSE)

与 MAE 类似,均方根误差也计算平均预测误差。然而,它基于二次评分,计算平均平方误差的平方根。此外,与 MAE 不同,MAE 取预测值和实际值之间的绝对差,而 RMSE 取平方,这增加了高误差值在开平方前的权重:

图片

在这里,图片表示第 i 个观测的因变量实际值和估计值之间的差异。

R 平方

R 平方衡量的是线性模型解释响应变量变异的百分比(介于 0 和 1 之间或从 0%到 100%的值)。换句话说,它衡量的是输入特征解释的变异。0%的 R 平方意味着模型的输入特征对响应变量没有任何解释作用。接近 100%意味着该模型是响应变量的良好预测器。例如,如果我们想预测某个地区的房价,特征如卧室数量、面积(平方英尺)以及学校和市场附近的距离决定了房产的价值。然而,仅凭 R 平方不能用于评估模型的好坏。还需要进行关于残差、正态性和异方差性的各种诊断检查。我们将在第四章回归中详细讨论这一点。

图片

在这里,图片是因变量实际值和估计值之间平方差的和,而图片表示因变量实际值和平均值之间平方差的和。

图片

调整后的 R 平方

当我们在回归模型中添加新变量时,随着新变量在解释因变量变化方面的贡献增加,模型的 R 平方值也会提高。(如果新变量设计不当且与解释因变量不相关,则会出现反论点。)因此,为了使评估指标对变量数量保持无偏见,我们在计算中引入了n(观测数)和q(变量数),从而惩罚 R 平方值。这被称为调整后的 R 平方,它同时调整了观测数和变量数。在处理多元线性回归时,查看调整后的 R 平方是一个好的做法。

MSE(均方误差):

图片

在这里,n是观测数,而q是模型中的系数数。

MST(总均方误差):

图片

平均倒数排名(MRR)

MRR 常用于评估搜索引擎、推荐算法以及数字空间中的许多其他信息检索算法。MRR 易于解释。通常,它可以用来评估为输入生成响应列表的算法。例如,您在 Google 查询中看到的搜索结果和您在 Amazon 上看到的商品推荐。以下表格显示了计算倒数排名的示例。MRR 的范围从 0 到 1;值越接近 1,表明算法在列表顶部给出了相关结果。

图 3.15:计算倒数排名的示例。

图 3.15:计算倒数排名的示例。

练习 47:寻找评估指标

在这个练习中,我们将找到 MAE、RMSE、R-squared、调整后的 R-squared 和 MRR。

执行以下步骤:

  1. 导入所需的库和包。

  2. 创建一个名为y_predicted的变量,并将multiple_PM25_linear_model的值赋给它:

    y_predicted <- predict(multiple_PM25_linear_model, data = PM25)
    
  3. 使用以下命令将PM25数据集的值赋给变量:

    y_actual <- PM25[!is.na(PM25$pm2.5),"pm2.5"]
    
  4. 使用均值函数找到 MAE:

    MAE <- mean(abs(y_actual - y_predicted))
    

    输出如下:

    ## [1] 59.82112
    
  5. 接下来,计算 RMSE:

    RMSE <- sqrt(mean((y_actual - y_predicted)²))
    

    输出如下:

    ## [1] 82.09164
    
  6. 现在,使用以下命令计算 R-squared 值:

    model_summary <- summary(multiple_PM25_linear_model)
    model_summary$r.squared
    

    输出如下:

    ## [1] 0.216
    
  7. 接下来,使用以下命令找到调整后的 R-squared:

    model_summary$adj.r.squared
    

    输出如下:

    ## [1] 0.2159
    
  8. 最后,使用以下命令找到 MRR:

    Query_RR_Vector <- c(1/3,1/4,1)
    MRR <- sum(Query_RR_Vector)/length(Query_RR_Vector)
    

    输出如下:

    ## [1] 0.5277778
    

观察到 MAE 的值为59.82,RMSE 为82.09,这表明误差有很高的方差。换句话说,预测中的观测值有很高的误差(这增加了误差幅度频率分布的方差);MAE 未能识别误差,而 RMSE 很好地放大了它。如果 MAE 和 RMSE 几乎相等,我们可以推断误差幅度频率分布的方差很低,并且模型对所有观测值的表现都很好。

基于混淆矩阵的指标

基于混淆矩阵的指标用于分类算法。可以从混淆矩阵(也称为AB)中推导出一系列指标。否则,关于目标变量没有负面或正面的东西。列联表也可以是 NxN,其中N是响应变量中的类别或类别的数量。例如,如果我们想在一个给定的图像中分类 26 个手写英文字符,我们需要一个 26x26 的矩阵:

图 3.16:混淆矩阵的元素。

图 3.16:混淆矩阵的元素。

如果我们将TPTNFPFN排列在一个 2x2 的列联表中,我们就可以得到混淆矩阵,如下表所示:

图 3.17:混淆矩阵。

图 3.17:混淆矩阵。

准确度

准确率衡量模型对正负样本的正确整体分类。矩阵对角线元素(TP 和 TN)之和除以正负样本的总数给出准确率。在现实场景中,准确率并不总是可靠的指标。考虑一下,我们想要区分癌症 CT 扫描和良性 CT 扫描。显然,我们可能会有许多阴性扫描和很少的阳性扫描。这导致了我们所说的不平衡数据集。如果模型主要能够准确预测良性扫描,但在预测癌症 CT 扫描时产生重大错误,准确率可能仍然很高,但模型并不那么有用。

灵敏度

为了解决我们之前讨论的准确率问题,我们可以使用灵敏度(也称为召回率、命中率或真正例率****TPR)和特异性(下一节讨论)的组合。灵敏度给出了模型对阳性案例的预测能力(例如,在 CT 扫描中检测癌症)。我们从所有真正例TP)案例与阳性P)案例的数量之比中获得灵敏度。

特异性

特异性提供了对负例正确预测的定量评估(例如,检测良性 CT 扫描)。我们从真阴性案例数与负案例数之比中获得灵敏度。

高灵敏度和特异性值表示模型质量较好。在大多数情况下,我们试图平衡这两个指标以获得最佳模型。

F1 分数

F1 分数通过取两个指标(精确度和灵敏度)的调和平均值(适用于取两个或更多率的平均值)来结合精确度和灵敏度,如下公式所述。阳性预测值PPV或精确度)衡量的是正确预测的数量与真阳性数和假阳性数之和的比值,即所有阳性病例预测中正确的是多少。

F1 分数比准确率更稳健,但在不平衡类别的情况下仍然存在问题。

对于评估分类模型的好坏,没有好或坏的指标。机器学习从业者通常会查看许多指标的组合来得出模型好坏的结论。这就是为什么了解如何解释上述讨论的每个指标变得很重要。

练习 48:在训练数据上使用模型评估

在这个练习中,我们将使用caret包中的confusionMatrix函数对训练数据进行模型评估。该函数会打印出准确率、灵敏度、特异性和许多其他指标。

执行以下步骤以完成练习:

  1. 将所需的库和包导入系统。

  2. 创建一个名为predicted的变量,并赋予其值,如图所示:

    predicted <- ifelse(PM25_logit_model$fitted.values>0.5, 1,0)
    
  3. 接下来,创建另一个名为actual的变量,如图所示:

    actual <- PM25_for_class$pollution_level
    
  4. 导入 caret 库:

    library(caret)
    
  5. 最后,使用confusionMatrix方法来描述分类模型的表现:

    confusionMatrix(predicted, actual)
    

    输出如下:

    ## Confusion Matrix and Statistics
    ## 
    ##           Reference
    ## Prediction     0     1
    ##          0  5437  2097
    ##          1  6232 27989
    ##                                           
    ##                Accuracy : 0.8005          
    ##                  95% CI : (0.7967, 0.8044)
    ##     No Information Rate : 0.7205          
    ##     P-Value [Acc > NIR] : < 2.2e-16       
    ##                                           
    ##                   Kappa : 0.4444          
    ##  Mcnemar's Test P-Value : < 2.2e-16       
    ##                                           
    ##             Sensitivity : 0.4659          
    ##             Specificity : 0.9303          
    ##          Pos Pred Value : 0.7217          
    ##          Neg Pred Value : 0.8179          
    ##              Prevalence : 0.2795          
    ##          Detection Rate : 0.1302          
    ##    Detection Prevalence : 0.1804          
    ##       Balanced Accuracy : 0.6981          
    ##                                           
    ##        'Positive' Class : 0               
    

confusionMatric()输出结果中显示的许多指标在本节中都有描述。然而,在阅读详细内容之前,这里有一个简要的总结。这个逻辑回归模型的准确率为 80%,按照标准来看是好的。这表明我们可以用其他环境因素以 80%的准确率预测正常和高于正常的 PM2.5 值。然而,请注意,准确率是基于整个训练数据集的。我们没有将数据分成两部分来检查过拟合情况,这是一种模型在训练数据上测试时表现非常好,但在测试(或未见)数据上表现较差的情况。

敏感性为 46%,特异性为 93%。这意味着模型在处理负例(高于正常 PM2.5)方面表现良好。通常,这两个指标之间必须有一个权衡。然而,在这种情况下,模型的优先级是尽可能多地预测高于正常的状态。因此,一旦我们有了混淆矩阵,高特异性是可取的;我们可以从中计算出所有指标。

受试者工作特征(ROC)曲线

在分类模型的背景下,预测的输出是一个定量估计,通常是一个概率度量。在二元逻辑回归中,将一个观测值分类为另一个观测值(例如,垃圾邮件与非垃圾邮件)的常用阈值是 0.5。这意味着如果概率大于 0.5,则将其分类为垃圾邮件,否则为非垃圾邮件。现在,根据阈值的不同,你将在我们之前讨论的混淆矩阵中得到不同的 TP、TN、FP 和 FN 值。虽然查看给定阈值(通常是 0.5)下的混淆矩阵是一种标准做法,但它可能无法完全展示模型在现实世界中的表现,这就是为什么阈值的选择至关重要。

ROC 曲线是一种优雅的可视化方式,显示了在每一个可能的阈值下,真正例率(通常由敏感性表示)和真正例率(通常由特异性表示)之间的变化。它帮助我们确定分类的正确阈值。此外,ROC 曲线下的面积(称为 AUC),其值在 0 到 1 之间,告诉我们模型的好坏。越接近 1,意味着模型能够成功地将大多数观测值分类为正类和负类。

在 R 中使用 ROCR 包,我们将使用逻辑回归获取 PM2.5 预测的 ROC 曲线。我们将在下一个练习中观察 AUC。

练习 49:创建 ROC 曲线

在这个练习中,我们将使用 ROCR 包来获取 ROC 曲线。

执行以下步骤:

  1. 使用以下命令将 ROCR 包导入系统:

    library(ROCR)
    
  2. 接下来,定义 pred1 和 pref1 对象:

    pred1 <- prediction(predict(PM25_logit_model), PM25_for_class$pollution_level)
    perf1 <- performance(pred1,"tpr","fpr")
    
  3. 接下来,使用以下命令找到 AUC:

    auc <- performance(pred1,"auc")
    as.numeric(auc@y.values)
    

    输出如下:

    ## [1] 0.8077673
    
  4. 使用 plot 命令绘制图形:

    plot(perf1)
    

    ![图 3.18:真阳性率(灵敏度)与假阳性率(特异性)之间的 ROC 曲线。 图片

图 3.18:真阳性率(灵敏度)与假阳性率(特异性)之间的 ROC 曲线。

摘要

在本章中,我们从设计问题开始,逐步介绍了构建机器学习工作流程的过程,包括部署模型。我们简要讨论了简单回归、多重回归和逻辑回归,以及所有必要的评估指标,以解释和判断模型的性能。这两个算法分别展示了回归和分类问题的监督学习。

在本章中,我们使用了北京 PM2.5 数据集来构建模型。在这个过程中,我们通过简单地重新设计因变量,将回归问题转换为分类问题。这种重新设计通常用于现实世界问题,以适应特定的用例。

下一章,我们将深入探讨回归算法的细节,并详细阐述除线性回归之外的多种回归算法类型,并讨论何时使用哪种算法。

第四章:回归

学习目标

到本章结束时,你将能够:

  • 构建回归问题

  • 实现各种类型的回归方法及其用例

  • 分析和选择正确的回归方法

  • 通过回归的视角连接统计学和机器学习

  • 深入探讨模型诊断

在本章中,我们将关注各种类型的回归,以及何时使用哪种类型,同时结合 R 语言中的演示。

简介

在上一章中,我们了解了线性回归模型以及输入变量(自变量)和目标变量(因变量或解释变量)之间的线性关系。如果只使用一个变量作为自变量,则定义为简单线性回归。如果使用多个解释变量(自变量),则称为多重线性回归

回归算法和问题基于预测一个数值目标变量(通常称为因变量),给定所有输入变量(通常称为自变量),例如,根据位置、面积、靠近购物中心等因素预测房价。许多回归的概念都源自统计学。

机器学习的整个领域现在是一个数学、统计学和计算机科学的完美平衡。在本章中,我们将使用回归技术来了解如何建立输入和目标变量之间的关系。我们还将强调模型诊断,因为回归充满了假设,在模型应用于现实世界之前需要检查这些假设。

本质上,所有模型都是错误的,但有些是有用的。 —— 乔治·博克斯

在第三章“监督学习简介”中,我们简要介绍了简单和多重线性回归。在本章中,我们将更专注于模型诊断和其他类型的回归算法,以及它与线性回归的不同之处。

线性回归

让我们回顾第三章“监督学习简介”中的多重线性回归。以下方程是具有p个解释变量和n个观测值的线性方程或线性预测函数的数学表示:

其中每个一张包含家具、桌子的图片自动生成的描述是一个列值的向量(解释变量)和一张包含家具的图片自动生成的描述未知参数系数一张包含家具、座椅的图片自动生成的描述,使这个方程适用于简单线性回归。有许多算法可以将此函数拟合到数据上。最流行的一个是普通最小二乘法OLS)。

在理解 OLS 的细节之前,首先让我们解释一下在尝试从第三章,监督学习简介中的简单和多元线性回归的模型构建部分拟合北京 PM2.5 数据时得到的方程:

![img/C12624_04_26.jpg]

如果我们代入回归系数的值,一张人脸的插图描述自动生成和![img/C12624_04_28.png]从lm()函数的输出中,我们得到:

![img/C12624_04_29.jpg]

前面的方程试图回答“DEWPTEMPIws这些因素对于预测pm2.5水平是否重要?”的问题。

该模型估计了平均而言,DEWPTEMPIws值如何影响pm2.5水平。例如,DEWP增加一个单位,pm2.5值将增加4.384196。这就是我们通常将这些系数称为权重的原因。需要注意的是,如果R-squared 值低,这些估计的系数是不可靠的。

练习 50:使用multiple_PM_25_linear_model对象打印系数和残差值

在这个练习中,我们将使用multiple_PM25_linear_model对象打印系数和残差值。

执行以下步骤以完成练习:

  1. 使用multiple_PM25_linear_model对象上的$运算符提取属性系数:

    multiple_PM25_linear_model$coefficients
    

    输出如下:

    (Intercept)        DEWP        TEMP         Iws 
    161.1512066   4.3841960  -5.1335111  -0.2743375
    
  2. 使用multiple_PM25_linear_model对象上的$运算符提取属性残差:

    multiple_PM25_linear_model$residuals
    

输出如下:

25            26            27            28 
  17.95294914   32.81291348   21.38677872   26.34105878 
           29            30            31            32

活动 7:不使用汇总函数使用模型对象打印各种属性

在第三章的多元线性回归模型部分,监督学习简介中,我们创建了一个多元线性回归模型,并将其存储在模型对象multiple_PM25_linear_model中,使用模型对象。

此活动有助于理解在模型构建后如何提取一些重要的模型属性。在少数情况下,我们将使用$运算符,在其他情况下,我们将进行一些简单的计算。使用multiple_PM25_linear_model对象打印以下模型属性:

  • 残差

  • 拟合值

  • R-Ssquared 值

  • F 统计量

  • 系数 p 值

让我们使用模型对象打印这些值:

  1. 首先,打印系数值。确保输出类似于使用coefficients选项的summary函数的输出。这些系数是使用 OLS 算法的模型得到的拟合值:

    (Intercept)        DEWP        TEMP         Iws 
    161.1512066   4.3841960  -5.1335111  -0.2743375
    
  2. 找到预测值和实际值之间的残差(差异),应尽可能小。残差反映了使用系数拟合的值与实际值之间的距离:

    25            26            27            28 
      17.95294914   32.81291348   21.38677872   26.34105878 
               29            30            31            32 
    
  3. 接下来,找到拟合值,这些值应更接近实际的 PM2.5 值以获得最佳模型。使用系数,我们可以计算拟合值:

    25         26         27         28         29 
    111.047051 115.187087 137.613221 154.658941 154.414781 
            30         31         32         33         34 
    
  4. 查找 R-Squared 值。它们应该与你在summary函数输出中找到的多个 R-squared 值相同。R-square 有助于评估模型性能。如果值越接近 1,则模型越好:

    summary(multiple_PM25_linear_model)$r.squared
    

    输出如下:

    [1] 0.2159579
    
  5. 查找 F 统计量值。确保输出与你在summary函数旁边找到的 F 统计量文本的输出相同。这将告诉你你的模型是否比仅使用目标变量的均值拟合得更好。在许多实际应用中,F-Statistic 与 p 值一起使用:

       value     numdf     dendf 
    3833.506     3.000 41753.000 
    
  6. 最后,找到系数 p 值,并确保这些值与你在summary函数下系数部分获得的值相同。它将出现在标题为Pr(>|t|):的列下。如果值小于 0.05,则变量在预测目标变量方面具有统计学意义:

      (Intercept)          DEWP          TEMP           Iws 
     0.000000e+00  0.000000e+00  0.000000e+00 4.279601e-224
    

    注意

    本活动的解决方案可在第 449 页找到。

普通最小二乘法 (OLS)

第三章监督学习简介中,我们看到了平方残差和![img/C12624_04_30.png](也称为误差平方和残差平方和),这是衡量整体模型拟合程度的指标,其表达式如下:

![img/C12624_04_31.jpg]

其中 T 代表矩阵转置,而 X 的行代表与目标变量的特定值相关的所有输入变量的值![img/C12624_04_32.png]。A picture containing furniture的值。

自动生成的描述![img/C12624_04_33.png],该描述最小化了![img/C12624_04_34.png],被称为β的OLS 估计量。OLS 算法旨在找到A picture containing furniture的全局最小值。

自动生成的描述![img/C12624_04_35.png],该描述将最小化![img/C12624_04_36.png]。

在上一章中,你也了解到,对于北京 PM2.5 数据集上的multiple_PM25_linear_model,其 R-squared 值相当低,以至于这个模型在实用应用中难以发挥作用。解释这些不良结果的一种方式是说预测变量DEWPTEMP并没有完全解释 PM2.5 的方差,因此它们未能产生良好的结果。

在我们能够跳入此模型的诊断分析之前,让我们看看是否可以用变量month(读数中的月份)来解释 PM2.5 的一些方差。我们还将使用交互变量(更多内容请参阅改进模型部分)DEWPTEMPmonth,在lm()函数中生成DEWPTEMPmonth的所有可能组合。

使用 month 的原因在 第三章监督学习简介 中的 图 3.3 中得到了证明,其中我们看到了 TEMPDEWPPRES 的值中的季节性影响(显示出漂亮的正弦波模式)。以下练习的输出显示了为解释 PM2.5 数据集而创建的所有交互项。

注意

表达式如 DEWP:TEMP 表示乘法,并且每个 month 的值在 multiple_PM25_linear_model 中都是一个独立的变量,因为我们运行模型之前将 month 转换成了 factor

练习 51:在 lm()函数中添加交互项 DEWP:TEMP:month

在这个练习中,我们将添加交互项以提高模型性能。

我们将看到添加额外的交互项如何有助于提高模型性能,从 R-squared 值的角度来看。执行以下步骤以完成练习:

  1. 使用以下命令读取北京 PM2.5 数据集:

    PM25 <- read.csv("PRSA_data_2010.1.1-2014.12.31.csv")
    
  2. 现在,将 month 对象转换为 factor 变量,如下所示:

    PM25$month <- as.factor(PM25$month)
    
  3. 使用具有 DEWPTEMPmonth 的交互项的线性模型。观察 DEWP*TEMP*month 项,这将生成 DEWPTEMPmonth 变量的所有组合:

    multiple_PM25_linear_model <- lm(pm2.5 ~ Iws + DEWP*TEMP*month, data = PM25)
    
  4. 打印模型的摘要以查看由于交互项而引起的系数和 r-squared 值的变化:

    summary(multiple_PM25_linear_model)
    

    输出如下:

    ## Call:
    ## lm(formula = pm2.5 ~ Iws + DEWP * TEMP * month, data = PM25)
    ## 
    ## Residuals:
    ##     Min      1Q  Median      3Q     Max 
    ## -298.41  -42.77   -9.35   30.91  967.39 
    ## 
    ## Coefficients:
    ##                     Estimate Std. Error t value Pr(>|t|)
    ...
    ## (Intercept)        2.917e+02  4.338e+00  67.257  < 2e-16 ***
    ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ## 
    ## Residual standard error: 70.04 on 41708 degrees of freedom
    ##   (2067 observations deleted due to missingness)
    ## Multiple R-squared:  0.4217, Adjusted R-squared:  0.4211 
    ## F-statistic: 633.7 on 48 and 41708 DF,  p-value: < 2.2e-16
    

注意 R-squared 值从 0.216 到 0.4217 的两次跳跃。然而,这种跳跃是以模型可解释性为代价的。虽然使用单个变量解释模型的可解释性很简单,但它们的乘积会产生难以描述的效果。

在我们北京 PM2.5 的例子中,更合理地认为 DEWPTEMPyear 对象的 month 对象的交互,因为这两个都是随季节变化的环境因素。

然而,我们还想进行一些诊断,以全面了解线性回归模型是如何从头到尾研究的,而不仅仅是查看 R-squared 值。

模型诊断

通常,像线性回归和逻辑回归这样的统计模型在接受最终解决方案之前需要验证许多假设。违反假设的模型会导致错误的预测,结果容易产生误解。

以下代码显示了从 lm() 方法的输出中获取诊断图的方法。该图有四个不同的图,查看残差。让我们了解如何解释每个图。所有这些图都是关于拟合如何符合回归假设的。如果有违反,它将在以下代码的图中清楚地显示:

par(mfrow = c(2,2))
plot(multiple_PM25_linear_model)

输出如下:

图 4.1:北京 PM2.5 数据集上线性模型拟合的诊断图

图 4.1:北京 PM2.5 数据集上线性模型拟合的诊断图

在接下来的四个部分中,我们将使用来自线性方程 和二次方程 的随机生成数据探索每个图表,稍后回来解释图 4.1中的四个图表与理想情况相比的表现如何。

注意

在二次方程中, 被假定为均值为 0,方差为 2 的正态分布。

在下面的练习中,我们将使用线性方程和二次方程生成图表。稍后,我们将深入理解线性模型应遵循的各种假设,这通过使用通过两个方程生成的随机数据进行的模型拟合来实现。

练习 52:使用线性方程和二次方程生成和拟合模型

在这个练习中,我们将了解线性和多项式函数,以及当我们对两者拟合线性模型时会发生什么。

使用线性方程和多项式方程生成随机数,并在两者上拟合线性模型。观察两个图表之间的差异。

执行以下步骤以生成所需的图表:

  1. 首先,使用以下代码定义线性函数:

    linear_function <- function(x){return (5+(12*x)-(3*x))}
    
  2. 定义如下命令中的二次函数:

    quadratic_function <- function(x){return (5+(12*x)-(3*(x²)))}
    
  3. 现在,生成如图所示的均匀随机数(x):

    uniform_random_x <- runif(50, min=0, max=15)
    
  4. 使用x生成线性值(y),如图所示:

    linear_values_y <- linear_function(uniform_random_x) + rnorm(50,mean = 0, sd =sqrt(2))
    
  5. 使用x生成二次值(y):

    quadratic_values_y <- quadractic_function(uniform_random_x) + rnorm(50,mean = 0, sd =sqrt(2))
    df <- data.frame(linear_values_y, quadratic_values_y, uniform_random_x)
    
  6. 使用uniform_random_xlinear_values_y拟合线性模型:

    model_df_linear <- lm(linear_values_y ~ uniform_random_x, data = df)
    
  7. 绘制线性关系的诊断图:

    par(mfrow = c(2,2))
    plot(model_df_linear)
    

    故事情节如下:

    图 4.2:使用线性回归的图表

    图 4.2:使用线性回归的图表
  8. 使用uniform_random_xquadratic_values_y拟合线性模型:

    model_df_quad <- lm(quadratic_values_y ~ uniform_random_x, data = df)
    
  9. 生成非线性关系的诊断:

    par(mfrow = c(2,2))
    plot(model_df_quad)
    

    输出如下:

图 4.3:使用二次回归的图表

图 4.3:使用二次回归的图表

步骤 7 和步骤 9 中的图表差异显示了线性关系的良好和不良拟合。线性模型不能拟合yx之间的非线性关系。在下一节中,我们将深入理解步骤 7 和步骤 9 生成的图表的四个部分。

第三章监督学习简介图 3.5中,我们讨论了在构建线性回归模型时需要考虑的各种假设。通过本章前面提到的四个图表,我们将检查是否有任何假设被违反。

残差与拟合图

这种图表位于拟合值和残差(lm()方法的差异)之间。如果预测变量和目标变量之间存在非线性关系,该图表将帮助我们识别。

在以下图中,上部分图显示了点均匀地散布,预测变量和目标变量之间的线性关系被清楚地捕捉到。在下部分图中,未解释的非线性关系被留在残差和曲线中,因此底部图清楚地显示它不是线性回归模型的正确拟合,违反了预测变量和目标变量之间的线性关系:

![Figure 4.4: [Top] Residual versus fitted plot of the linear function. [Bottom] Residual versus fitted plot of the quadratic function]

![img/C12624_04_04.jpg]

图 4.4:[上] 线性函数的残差与拟合图。[下] 二次函数的残差与拟合图

正态 Q-Q 图

Q-Q 图,也称为分位数-分位数图,用于检查数据是否可能来自近似的理论分布;在本例中,为正态分布。Q-Q 图是通过绘制两组分位数(数据中低于一定比例的点)相对比而形成的散点图。如果两组分位数都来自相似的分布,我们应看到点形成一条大致的直线。给定一个数据向量,正态 Q-Q 图按顺序绘制数据与标准正态分布的分位数。

线性回归的第二个假设是所有预测变量都是正态分布的。如果这是真的,残差也将是正态分布的。正态 Q-Q 图是标准化残差与理论分位数之间的图。直观上,我们可以检查残差是否遵循直线,如果是正态分布的,或者是否有任何偏差表明违反了假设。

在以下图中,图的上部分展示了线性函数,它与直线对齐,除了 39 号、30 号和 50 号观测值等少数例外。另一方面,图的下半部分展示了二次函数,它意外地与直线有很好的对齐,不像线性函数,因为图中右上角有一些偏差:

![Figure 4.5: [Top] Normal Q-Q plot of the linear function. [Bottom] Normal Q-Q plot of the quadratic function]

![img/C12624_04_05.jpg]

图 4.5:[上] 线性函数的正态 Q-Q 图。[下] 二次函数的正态 Q-Q 图

标度-位置图

标度-位置图显示残差是否均匀地分布在输入变量(预测变量)的范围内。也可以用此图来检查方差相等的假设(同方差性)。如果我们看到一条水平线,点随机分布,这意味着模型是好的。

该图位于拟合值和标准化残差的平方根之间。在下面的图中,顶部的图显示了线性函数,残差沿着水平线随机分布,而在底部的图中,似乎有一个非随机的模式。因此,方差不相等:

![图 4.6:[上] 线性函数的尺度-位置图。[下] 二次函数的尺度-位置图]

![img/C12624_04_06.jpg]

图 4.6:[上] 线性函数的尺度-位置图。[下] 二次函数的尺度-位置图

残差与杠杆

如果数据中存在任何有影响力的点,残差与杠杆图有助于识别它。通常认为所有异常点都是有影响力的,即它决定了回归线的形状。然而,并非所有异常点都是有影响力的。即使一个点在合理的值范围内(不是异常点),它仍然可能是有影响力的点。

在下一个图中,我们将关注右上角或右下角的远离值。这些区域是观察值相对于回归线可能具有影响力的空间。在图 4.7中,红色虚线外的观察值4039(高 Cook 距离)。请注意,这些观察值在其他三个图中也持续出现,这给了我们一个强有力的理由,如果我们想看到数据中的线性关系,就应该消除这些点。顶部的图似乎没有红色虚线,这证实了一个良好的拟合:

![图 4.7:[上] 线性函数的残差与杠杆作用图。[下] 二次函数的残差与杠杆作用图]

![img/C12624_04_07.jpg]

图 4.7:[上] 线性函数的残差与杠杆作用图。[下] 二次函数的残差与杠杆作用图]

现在,如果我们重新审视图 4.1,即我们从北京 PM2.5 数据集中获得诊断图;看起来这个模型对于实际应用来说拟合并不是最好的。所有四个图都显示出轻微的违反线性、正态性和同方差性假设。

在下一节中,我们列出了一些改进模型的方法,这些方法可能有助于逐步提高 R 平方值并更好地拟合数据。同样,类似于我们刚才讨论的视觉检查方法,许多统计方法,如用于测试正态性的Kolmogorov-Smirnov 检验,用于测试多重共线性的相关系数,用于测试同方差的Goldfeld-Quandt 检验,都可以使用。

改进模型

到目前为止,我们已经看到了数据中的问题,但你可能会问是否可以修复或改进它。让我们讨论一些改进的方法。在本节中,你将学习一些方法,例如变量转换、处理异常点、添加交互作用以及决定采用非线性模型。

转换预测变量或目标变量

提高模型的最常见方法是使用对数函数变换一个或多个变量(也可能是目标变量)。

对数变换可以纠正偏斜分布。它提供了处理数据中偏斜性的能力,同时一旦建立模型,原始值就可以很容易地计算出来。最流行的对数变换是自然对数。关于对数变换的更详细解释,可以在第六章特征选择和降维部分的对数变换部分找到。

目标是通过变换将数据中的正态分布引入。因此,任何有助于达到这一目标的函数都是一种好的变换。在取对数之后,平方根变换也被广泛使用。查看变换后变量的分布,看看是否得到了对称分布(钟形);如果是,那么这种变换将是有用的。

选择非线性模型

可能会遇到一种情况,线性模型并不适合,因为预测变量和目标变量之间存在非线性关系,只有非线性函数才能拟合这样的数据。关于此类模型的更多细节,请参阅本章后面的多项式回归部分。

移除异常值或影响点

正如我们在残差与杠杆率部分的诊断图中讨论的那样,我们可能会发现一个异常值或影响点在获得最佳模型时起到了破坏作用。如果你已经正确地识别了它,尝试通过删除观测值来看是否有所改善。

添加交互效应

有时我们可能会看到数据集中两个或更多预测(独立)变量的值以乘法方式影响因变量。一个带有交互项的线性回归方程可能看起来像这样:

图片

可以尝试更高阶的这种交互(例如,使用三个变量);然而,这些很难解释,通常会被避免。

分位数回归

当数据出现异常值、高偏度和导致异方差性的条件时,我们采用分位数回归进行建模。此外,分位数回归回答的一个关键问题是,线性回归无法回答的:“对于高 PM2.5 和平均 PM2.5,DEWPTEMPIws对 PM2.5 水平的影响是否不同?”

分位数回归与线性回归非常相似;然而,分位数回归参数估计的是由于输入预测变量单位变化而产生的响应变量某个分位数的改变。为了完全理解这个陈述,让我们使用分位数回归(不使用交互项)来拟合我们的北京数据。

我们需要安装quantreg包来将分位数回归拟合到数据中。该包提供了rq()方法,使用tau参数拟合数据,tau是模型参数,指定用于拟合模型到数据中的分位数值。注意,rq()方法的参数的其他部分看起来与lm()类似。

练习 53:在北京 PM2.5 数据集上拟合分位数回归

在这个练习中,我们将观察在不同分位数(尤其是 25 分位数、50 分位数和 75 分位数)处的分位数回归拟合的差异。我们将使用quantreg中的rq()函数来构建模型。在图 4.8中,我们将比较通过lm()函数获得的系数值与通过rq()函数获得的系数值,以比较两种回归类型。

执行以下步骤来完成练习:

  1. 使用以下命令读取北京 PM2.5 数据集:

    PM25 <- read.csv("PRSA_data_2010.1.1-2014.12.31.csv")
    
  2. 接下来,下一步是安装所需的包。使用以下命令加载quantreg包:

    library(quantreg)
    
  3. 运行分位数回归 tau 值为 0.25、0.5 和 0.75,分别对应 25 分位数、50 分位数和 75 分位数:

    quantile_regression_PM25_all <- rq(pm2.5 ~ DEWP+TEMP+Iws, data = PM25, tau = seq(0.25,0.99,by = 0.25))
    
  4. 打印分位数回归模型的摘要:

    summary(quantile_regression_PM25_all)
    

    输出如下:

    ## tau: [1] 0.25
    ## 
    ## Coefficients:
    ##             Value     Std. Error t value   Pr(>|t|) 
    ## (Intercept)  63.62367   0.52894  120.28453   0.00000
    ## DEWP          2.08932   0.01859  112.39914   0.00000
    ## TEMP         -1.89485   0.02196  -86.27611   0.00000
    ## Iws          -0.09590   0.00179  -53.59211   0.00000
    ## 
    ## tau: [1] 0.5
    ## 
    ## Coefficients:
    ##             Value      Std. Error t value    Pr(>|t|)  
    ## (Intercept)  117.37344    0.73885  158.85921    0.00000
    ## DEWP           3.43276    0.02835  121.07849    0.00000
    ## TEMP          -3.37448    0.03225 -104.65011    0.00000
    ## Iws           -0.16659    0.00202  -82.56604    0.00000
    ## 
    ## tau: [1] 0.75
    ## 
    ## Coefficients:
    ##             Value      Std. Error t value    Pr(>|t|)  
    ## (Intercept)  201.16377    1.31859  152.55927    0.00000
    ## DEWP           5.12661    0.04901  104.59430    0.00000
    ## TEMP          -5.62333    0.05567 -101.01841    0.00000
    ## Iws           -0.25807    0.00510  -50.55327    0.00000
    

以下表格总结了我们在第三章“监督学习简介”的“回归”部分使用lm()获得的线性回归系数值,以及我们使用rq()在三个分位数获得的值。

根据线性回归模型,大气中 PM2.5 的平均水平随着DEWP增加一个单位而增加4.384196。分位数回归的结果如下表所示,并且它表明DEWP对 PM2.5 的高分位数(观察 75 分位数)有更大的负面影响:

图 4.8:25 分位数、50 分位数、75 分位数分位数回归的系数估计以及北京 PM2.5 估计模型的线性回归系数估计

图 4.8:25 分位数、50 分位数、75 分位数分位数回归的系数估计以及北京 PM2.5 估计模型的线性回归系数估计

图 4.8:25 分位数、50 分位数、75 分位数分位数回归的系数估计以及北京 PM2.5 估计模型的线性回归系数估计

练习 54:使用更细粒度绘制各种分位数

在这个练习中,我们不会使用第 25、50 和 75 分位数,而是会在rq函数中使用更细粒度的 tau 值。这个图表将有助于可视化系数值根据分位数值的变化。使用 R 中的seq()函数,从 0.05 开始设置分位数值,增量是 0.05。

执行以下步骤来完成练习:

  1. 创建一个quantile_regression_PM25_granular变量:

    quantile_regression_PM25_granular <- rq(pm2.5 ~ DEWP + TEMP + Iws, data = PM25, tau = seq(0.05,0.95,by = 0.05))
    
  2. 现在,使用summary函数存储先前创建的变量的值:

    plot_granular <- summary(quantile_regression_PM25_granular)
    
  3. 让我们使用以下命令来绘制图表。观察不同 tau 值时,InterceptDEWPTEMPIws的值如何变化:

    plot(plot_granular)
    

    输出图表如下:

![图 4.9:显示了不同分位数下 DEWP、TEMP 和 Iws 的系数的各种值图片 C12624_04_09.jpg

图 4.9:显示了不同分位数下 DEWP、TEMP 和 Iws 的系数的各种值

在这个练习中,我们通过在rq函数中使用更细粒度的 tau 值来探索变量的粒度。上一张图显示了DEWPTEMPIws的系数的各种值。图中 X 轴表示分位数。单条虚线表示分位数回归的估计,灰色区域是置信区间。中间的灰色线是 OLS 系数估计的表示,双虚线显示 OLS 系数的置信区间。观察发现红色和灰色区域没有重叠,这证明了我们使用分位数回归的正确性。如果两条线重叠,那么使用 OLS 和分位数回归的估计就没有差异。

注意

我们并不声称分位数回归比线性回归给出更好的结果。这个模型的调整 R 平方值仍然很低,但在现实世界中它工作得很好。然而,我们声称分位数回归可以帮助估计 PM2.5 的不同水平,而不仅仅是平均值,这为具有异常值、高偏度和异方差性的数据提供了稳健的解释。

多项式回归

在现实世界的数据中,响应变量和预测变量往往没有线性关系,我们可能需要一个非线性多项式函数来拟合数据。各种散点图样的残差与每个预测变量和残差与拟合值的对比揭示了是否存在违反线性关系的情况,这可能会帮助识别在方程中引入二次或三次项的需要。以下是一个通用的多项式方程:

图片 C12624_04_42.jpg

其中k是多项式的次数。对于k=2f(X)被称为二次h=4被称为三次。请注意,多项式回归仍然被认为是线性回归,因为它在系数一张脸的插图自动生成描述

在重新审视北京 PM2.5 的例子之前,让我们通过使用我们在线性回归部分介绍的二项式方程的模拟数据来理解多项式回归是如何工作的。

练习 55:使用 runif()函数执行均匀分布

在这个练习中,我们将使用 R 中的runif()函数生成 50 个来自均匀分布的随机数,并将结果存储在uniform_random_x中。我们已经定义了一个使用之前二次方程生成值的函数。请注意,我们将单独添加一个标志的特写自动生成描述到函数返回的值;一个标志的特写自动生成的描述是使用 R 中的rnorm()函数从正态分布生成的。最终值将被存储在quadratic_values_y中:

执行以下步骤以使用runif()函数进行均匀分布:

  1. 首先,定义以下命令中所示的二次方程:

    quadratic_function <- function(x){return (5+(12*x)-(3*(x²)))}
    
  2. 现在,生成x的均匀随机数:

    uniform_random_x <- runif(50, min=0, max=15)
    
  3. 将误差项添加到二次方程中,该误差项是均值为0、方差为2的正态分布(标准差(sd) = 方差的平方根):

    quadratic_values_y <- quadratic_function(uniform_random_x) + rnorm(50,mean = 0, sd =sqrt(2))
    
  4. 要将数据存储在数据框中,请使用以下命令:

    df <- data.frame(quadratic_values_y,uniform_random_x)
    
  5. 现在,根据二次方程绘制xy之间的关系图:

    library(ggplot2)
    ggplot(df, aes(x=uniform_random_x,y=quadratic_values_y))+
      geom_point()
    

    输出如下:

    ![图 4.10:使用函数 runif()进行均匀分布 图片

    图 4.10:使用函数 runif()进行均匀分布

    下一个图清楚地显示了uniform_random_xquadratic_values_y之间的关系不是线性的,正如预期的那样。现在,如果我们尝试拟合一个线性模型,我们预计会在诊断图中看到一些问题。

    图 4.12中的残差与拟合值图显示了曲率,并且它们没有像之前看到的那样表现出均匀的随机性。此外,正态概率图NPP)似乎偏离了直线,并在远端百分位数处向下弯曲。这些图表明所使用的模型可能存在问题,并表明可能需要一个更高阶的模型。

    ![图 4.11:该图显示了均匀生成的随机数(x)与二次方程中 x 的值的非线性关系 图片

    图 4.11:该图显示了均匀生成的随机数(x)与二次方程中 x 的值的非线性关系
  6. 现在,将线性回归模型拟合到多项式(二次)方程,并显示诊断图:

    par(mfrow = c(2,2))
    plot(lm(quadratic_values_y~uniform_random_x,data=df))
    

    输出如下:

    ![图 4.12:使用 lm()方法对二次方程数据拟合的诊断图 图片

    图 4.12:使用 lm()方法对二次方程数据拟合的诊断图

    现在,让我们看看多项式回归在北京 PM2.5 数据集上的表现。我们引入了一个额外的二次项 DEWP²,这仅仅是 DEWP 的平方。请参考第三章,监督学习导论中的图 3.5,以证明添加这样一个高阶项的合理性。

  7. 在北京 PM2.5 数据集上使用多项式回归(二次和三次项):

    multiple_PM25_poly_model <- lm(pm2.5 ~ DEWP² + TEMP + Iws + DEWP*TEMP*month, data = PM25)
    
  8. 要打印模型摘要,请使用以下命令:

    summary(multiple_PM25_poly_model)
    

    输出如下:

    ## Residuals:
    ##     Min      1Q  Median      3Q     Max 
    ## -298.41  -42.77   -9.35   30.91  967.39 
    ## 
    ## Coefficients:
    ##                     Estimate Std. Error t value Pr(>|t|)    
    ## (Intercept)        2.917e+02  4.338e+00  67.257  < 2e-16 ***
    ## DEWP               1.190e+01  2.539e-01  46.879  < 2e-16 ***
    ## TEMP              -9.830e+00  8.806e-01 -11.164  < 2e-16 ***
    ## Iws               -1.388e-01  7.707e-03 -18.009  < 2e-16 ***
    ## month2            -2.388e+01  5.011e+00  -4.766 1.89e-06 ***
    ## month3            -1.228e+02  5.165e+00 -23.780  < 2e-16 ***
    ## DEWP:TEMP:month9   4.455e-01  6.552e-02   6.800 1.06e-11 ***
    ## DEWP:TEMP:month10  5.066e-01  5.862e-02   8.642  < 2e-16 ***
    ## DEWP:TEMP:month11  5.111e-02  5.526e-02   0.925  0.35500    
    ## DEWP:TEMP:month12  1.492e-01  6.599e-02   2.261  0.02375 *  
    ## ---
    ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ## 
    ## Residual standard error: 70.04 on 41708 degrees of freedom
    ##   (2067 observations deleted due to missingness)
    ## Multiple R-squared:  0.4217, Adjusted R-squared:  0.4211 
    ## F-statistic: 633.7 on 48 and 41708 DF,  p-value: < 2.2e-16
    

    观察到尽管增加了二次项,但我们并没有比线性模型获得任何更好的 R-squared 值。在这个时候,我们可能得出结论,PM2.5 预测需要一个更好的自变量,它可以解释其中的方差,从而使 R-squared 值达到更高的水平。诊断图似乎也有类似的解释。

  9. 使用以下命令绘制诊断图:

    par(mfrow = c(2,2))
    plot(multiple_PM25_poly_model)
    

    输出如下:

图 4.13:北京 PM2.5 数据集上多项式回归模型拟合的诊断图

图 4.13:北京 PM2.5 数据集上多项式回归模型拟合的诊断图

Ridge Regression

正如我们在线性回归中看到的,普通最小二乘法OLS)通过最小化残差平方和来估计包含家具的图片描述自动生成的值。

由于包含家具的图片描述自动生成是从给定样本中计算出的一个估计值,它不是一个真实的人口参数,我们需要注意估计的某些特性。这两个主要特性是偏差和方差。

如果一个标志的特写描述自动生成是在一个标志的特写描述自动生成,然后测试数据集上的平均(或期望)可以分解为三个量,方差、平方偏差以及误差项的方差,如以下方程所示:

对于最佳估计,应同时实现低偏差和低方差的一个合适算法,如 OLS。我们通常称之为偏差-方差权衡。以下图中所示的流行牛眼图有助于理解权衡的各种情况:

图 4.14:解释偏差和方差场景的流行牛眼图

图 4.14:解释偏差和方差场景的流行牛眼图

牛眼代表 OLS 试图估计的真实人口值,而对其的射击则是我们四个不同估计器的估计值。这些被广泛归类为以下几类:

  • 低偏差和低方差(最理想)

  • 低偏差和高方差

  • 高偏差和低方差

  • 高偏差和高方差(最不理想)

OLS 方法将所有变量视为同等可能,因此具有低偏差(在训练期间导致欠拟合)和高方差(在测试数据中导致预测误差),如图 4.11所示。这种行为对于获得最佳模型复杂度并不理想。解决这个问题的通用方法是牺牲一些偏差来降低方差。这种方法被称为正则化。因此,岭回归可以被视为线性回归的扩展,其中包含一个额外的正则化项。

多元线性回归的一般形式可以表示如下:

图片 C12624_04_52.jpg

其中,argmin表示使函数达到最小值的βs 的最小值。在此上下文中,它找到使 RSS 最小的βs。βs 受到以下约束:

图片 C12624_04_53.jpg

正则化项 – L2 范数

图片 C12624_04_54.jpg图片 C12624_04_55.jpg

岭回归的惩罚项在 RSS 增加时增加。以下图表显示了模型复杂度(预测变量数量)与误差之间的关系。它表明,当预测变量的数量增加(模型复杂度增加)时,方差上升,而偏差下降。

OLS 估计在右侧找到一个位置,远离最佳权衡点。这种情况需要引入正则化项,因此岭回归成为合适的模型选择:

![图 4.15:偏差与方差img/C12624_04_15.jpg

图 4.15:偏差与方差

岭回归的 OLS 损失函数可以表示为以下方程:

图片 C12624_04_56.jpg

最小化一张人脸的插图自动生成的描述函数具有提供岭回归估计的正则化项。这个损失函数的有趣特性是,当img/C12624_04_58.png变得更大时,方差减少,偏差增加。

练习 56:北京 PM2.5 数据集上的岭回归

这个练习在 PM2.5 数据集上拟合岭回归。我们将使用glmnet库的交叉验证函数cv.glmnet(),参数alpha = 0和变化的 lambda 值。目标是获得一个最佳的 lambda 值,它将被函数输出的lambda.min属性返回。

让我们执行以下步骤来完成练习:

  1. 加载glmnet库并对 PM25 DataFrame 进行预处理:

    library(glmnet)
    PM25 <- na.omit(PM25)
    X <- as.matrix(PM25[,c("DEWP","TEMP","Iws")])
    Y <- PM25$pm2.5
    
  2. 现在,让我们使用以下代码来设置seed以获得类似的结果:

    set.seed(100)
    model_ridge = cv.glmnet(X,Y,alpha = 0,lambda = 10^seq(4,-1,-0.1))
    
  3. 要在交叉验证后找到 lambda 的最佳值,请执行以下命令:

    optimal_lambda <- model_ridge$lambda.min
    
  4. 模型拟合的系数值:

    ridge_coefficients <- predict(model_ridge, s = optimal_lambda, type = "coefficients")
    ridge_coefficients
    

    输出如下:

    ## 4 x 1 sparse Matrix of class "dgCMatrix"
    ##                       1
    ## (Intercept) 160.7120263
    ## DEWP          4.3462480
    ## TEMP         -5.0902943
    ## Iws          -0.2756095
    
  5. 使用predict函数再次传递矩阵 X 到newx参数:

    ridge_prediction <- predict(model_ridge, s = optimal_lambda, newx = X)
    head(ridge_prediction)
    

    输出如下:

             1
    25 111.0399
    26 115.1408
    27 137.3708
    28 154.2625
    29 154.0172
    30 158.8622
    

我们看到如何使用glmnet库来拟合北京 PM2.5 数据集。

LASSO 回归

最小绝对收缩和选择算子LASSO)的结构与岭回归相似,除了惩罚项不同,在 LASSO 回归中是 L1(系数估计值的绝对值之和),而在岭回归中是 L2(系数平方之和):

img/C12624_04_59.jpg

LASSO 回归将一些系数设为零,从而消除了特定变量的影响。这使得它在拟合数据的同时进行特征选择变得高效。

练习 57:LASSO 回归

在这个练习中,我们将对北京 PM2.5 数据集应用 LASSO 回归。我们将使用相同的 cv.glmnet() 函数来找到最优的 lambda 值。

执行以下步骤以完成练习:

  1. 首先,让我们设置 seed 以使用以下命令获得相似的结果:

    set.seed(100) #Setting the seed to get similar results.
    model_LASSO = cv.glmnet(X,Y,alpha = 1,lambda = 10^seq(4,-1,-0.1))
    
  2. 现在,使用以下命令在交叉验证后找到 lambda 的最佳值:

    optimal_lambda_LASSO <- model_LASSO$lambda.min
    
  3. 执行以下命令从模型拟合中找到系数值:

    LASSO_coefficients <- predict(model_LASSO, s = optimal_lambda_LASSO, type = "coefficients")
    LASSO_coefficients
    

    输出如下:

    ## 4 x 1 sparse Matrix of class "dgCMatrix"
    ##                       1
    ## (Intercept) 160.4765008
    ## DEWP          4.3324461
    ## TEMP         -5.0725046
    ## Iws          -0.2739729
    
  4. 使用以下命令从模型中找到预测值:

    LASSO_prediction <- predict(model_LASSO, s = optimal_lambda_LASSO, newx = X)
    head(LASSO_prediction)
    

    输出如下:

              1
    25 110.9570
    26 115.0456
    27 137.2040
    28 154.0434
    29 153.7996
    30 158.6282
    

观察岭回归和 LASSO 回归预测的相似性。北京 PM2.5 数据集在这两种方法中显示没有差异。

弹性网络回归

弹性网络结合了岭回归和 LASSO 回归的惩罚项,以避免在变量选择(系数值趋向于零,从而保持高度相关变量受控)上过度依赖数据。弹性网络最小化以下损失函数:

img/C12624_04_60.jpg

其中参数 α 控制岭回归和 LASSO 之间的正确混合。

总结来说,如果一个模型具有许多预测变量或相关变量,引入正则化项有助于在减少方差的同时优化偏差,从而在模型复杂性和误差之间达到正确的平衡。图 4.16 提供了一个流程图,帮助人们选择在多元回归、岭回归、LASSO 回归和弹性网络回归之间的选择:

Figure 4.16: 选择标准以在多元回归、岭回归、LASSO 回归和弹性网络回归之间进行选择

img/C12624_04_16.jpg

图 4.16:选择标准以在多元回归、岭回归、LASSO 回归和弹性网络回归之间进行选择

练习 58:弹性网络回归

在这个练习中,我们将对北京 PM2.5 数据集执行弹性网络回归。

执行以下步骤以完成练习:

  1. 让我们先设置 seed 以使用以下命令获得相似的结果:

    set.seed(100)
    model_elanet = cv.glmnet(X,Y,alpha = 0.5,lambda = 10^seq(4,-1,-0.1))
    
  2. 现在,使用以下命令在交叉验证后找到 lambda 的最佳值:

    optimal_lambda_elanet <- model_LASSO$lambda.min
    
  3. 接下来,执行以下命令从模型拟合中找到系数值:

    elanet_coefficients <- predict(model_elanet, s = optimal_lambda_elanet, type = "coefficients")
    elanet_coefficients
    

    输出如下:

    ## 4 x 1 sparse Matrix of class "dgCMatrix"
    ##                       1
    ## (Intercept) 160.5950551
    ## DEWP          4.3393969
    ## TEMP         -5.0814722
    ## Iws          -0.2747902
    
  4. 使用以下命令从模型中找到预测值:

    elanet_prediction <- predict(model_elanet, s = optimal_lambda_elanet, newx = X)
    

    输出如下:

    25 110.9987
    26 115.0936
    27 137.2880
    28 154.1538
    29 153.9092
    30 158.7461
    

弹性网络回归(Elastic Net Regression)的预测结果与岭回归(ridge regression)和 LASSO 回归大致相同。在下一节中,我们将比较这三种方法。

系数和残差标准误差的比较

以下表格显示了 DEWPTEMPIws 的比较,数值之间没有太大差异,这表明岭回归、LASSO 和弹性网络回归(带有正则化项)并不比多重线性回归方法更好。这也表明 DEWPTEMPIws 是低相关或无相关的自变量:

图 4.17:线性、岭回归、LASSO 和弹性网络回归的残差标准误差比较

图 4.17:线性、岭回归、LASSO 和弹性网络回归的残差标准误差比较

以下图表显示了使用线性、岭回归、LASSO 和弹性网络回归的截距和 DEWP、TEMP 以及 Iws 变量的系数值的比较:

图 4.18:线性、岭回归、LASSO 和弹性网络回归系数值的比较

图 4.18:线性、岭回归、LASSO 和弹性网络回归系数值的比较

练习 59:计算线性、岭回归、LASSO 和弹性网络回归的 RSE

在这个练习中,我们将计算线性、岭回归、LASSO 和弹性网络回归的 RSE。

执行以下步骤以完成练习:

  1. 使用以下代码使用 IwsDEWPTEMP 变量拟合线性模型:

    multiple_PM25_linear_model <- lm(pm2.5 ~ Iws + DEWP + TEMP, data = PM25)
    
  2. 现在,使用以下命令来查找线性回归的残差标准误差RSE):

    sqrt(sum(multiple_PM25_linear_model$residuals²)/41753)
    

    输出如下:

    ## [1] 81.51
    

    同样,我们将找到剩余回归的 RSE。

  3. 岭回归的 RSE:

    sqrt(sum((Y-ridge_prediction)²)/41753)
    

    输出如下:

    ## [1] 81.51059
    
  4. LASSO 回归的 RSE:

    sqrt(sum((Y-LASSO_prediction)²)/41753)
    

    输出如下:

    ## [1] 81.51123
    
  5. 弹性网络回归的 RSE:

    sqrt(sum((Y-elanet_prediction)²)/41753)
    

    输出如下:

    ## [1] 81.51087
    

这表明所有三个的 RSE 并没有显著差异。

泊松回归

在线性回归中,我们看到了以下形式的方程:

Y 是一个计数或比率(Y/t),它具有泊松分布,期望(均值)计数为 ,等于 ,即方差。

在逻辑回归的情况下,我们会寻找可以最大化对数似然值的值,以获得系数的最大似然估计器MLEs)。

没有封闭形式的解,因此最大似然估计的估计将使用迭代算法,如牛顿-拉夫森迭代加权最小二乘法IRWLS)。

泊松回归适用于计数依赖变量,必须满足以下指南:

  • 它遵循泊松分布

  • 计数不能为负

  • 数值是整数(没有分数)

    注意

    这里用于演示泊松回归的数据集来自 A. Colin Cameron 和 Per Johansson,"使用级数展开的计数数据回归:应用",应用计量经济学杂志,第 12 卷,第 3 期,1997 年,第 203-224 页。

以下表格简要描述了变量:

图 4.19:来自澳大利亚健康调查数据集的变量及其描述

图 4.19:来自澳大利亚健康调查数据集的变量及其描述

注意

博客 www.econ.uiuc.edu/~econ508/Stata/e-ta16_Stata.html 展示了数据集的使用方法。

练习 60:执行泊松回归

在这个练习中,我们将对数据集进行泊松回归。

执行以下步骤以完成练习:

  1. 执行泊松回归,加载foreign库以读取dta数据:

    library(foreign)
    
  2. 使用foreign库中的read.data函数读取澳大利亚健康调查数据集:

    df_health <- read.dta("health.dta")
    
  3. 使用glm()函数拟合广义线性模型,其中泊松回归作为家族参数中的值:

    poisson_regression_health <- glm(NONDOCCO ~ ., data = df_health, family=poisson(link=log))
    
  4. 打印模型的摘要:

    summary(poisson_regression_health)
    

    输出如下:

    ## Coefficients:##              Estimate Std. Error z value Pr(>|z|)    
    ## (Intercept) -3.116128   0.137763 -22.620  < 2e-16 ***
    ## SEX          0.336123   0.069605   4.829 1.37e-06 ***
    ## AGE          0.782335   0.200369   3.904 9.44e-05 ***
    ## INCOME      -0.123275   0.107720  -1.144 0.252459    
    ## LEVYPLUS     0.302185   0.097209   3.109 0.001880 ** 
    ## FREEPOOR     0.009547   0.210991   0.045 0.963910    
    ## FREEREPA     0.446621   0.114681   3.894 9.84e-05 ***
    ## ILLNESS      0.058322   0.021474   2.716 0.006610 ** 
    ## ACTDAYS      0.098894   0.006095  16.226  < 2e-16 ***
    ## HSCORE       0.041925   0.011613   3.610 0.000306 ***
    ## CHCOND1      0.496751   0.086645   5.733 9.86e-09 ***
    ## CHCOND2      1.029310   0.097262  10.583  < 2e-16 ***
    ## ---
    ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ## 
    ## (Dispersion parameter for poisson family taken to be 1)
    ## 
    ##     Null deviance: 6127.9  on 5189  degrees of freedom
    ## Residual deviance: 5052.5  on 5178  degrees of freedom
    ## AIC: 6254.3
    ## 
    ## Number of Fisher Scoring iterations: 7
    
  5. 加载ggplot2库:

    library(ggplot2)
    
  6. NONDOCCO的实际值与泊松回归拟合的NONDOCCO值合并:

    df_pred_actual <- data.frame(cbind(df_health$NONDOCCO,poisson_regression_health$fitted.values))
    
  7. 命名列名:

    colnames(df_pred_actual) <- c("actual_NONDOCCO","predicted_NONDOCCO")
    
  8. 绘制NONDOCCO目标变量的实际值与预测值的对比图:

    ggplot(df_pred_actual, aes(x=actual_NONDOCCO, y =predicted_NONDOCCO))+
       geom_point()
    

    输出图表如下:

图 4.20:比较实际值和预测值 NONDOCCO

图 4.20:比较实际值和预测值 NONDOCCO

给定残差偏差统计量的值为 5052.5,自由度为 5178,p 值为零,且5052.5/5178 = 0.975小于 1,因此模型在某种程度上是有效的。我们还可以检查过度分散(数据集中比基于给定统计模型预期的更大变异性)。过度分散是通过将sample_variance除以sample_mean来计算的。让我们检查以下练习。

练习 61:计算过度分散

在这个练习中,我们将对数据集进行过度分散的计算。

执行以下步骤以完成练习:

  1. 首先,让我们使用以下命令找到样本均值:

    s_mean <- mean(df_health$NONDOCCO)
    s_mean
    

    输出如下:

    ## [1] 0.2146435
    
  2. 现在,使用以下命令查找样本方差:

    s_variance <- var(df_health$NONDOCCO)
    s_variance
    

    输出如下:

    ## [1] 0.931757
    
  3. 同样,可以使用以下命令计算过度分散:

    s_variance/s_mean
    

    输出如下:

    ## [1] 4.34095
    

    因此,即使我们尝试添加预测变量到模型拟合中,过度分散开始下降。在我们的例子中,分散度在合理范围内。

  4. 现在,让我们使用以下命令计算分散度:

    summary.glm(poisson_regression_health)$dispersion
    

    输出如下:

    ## [1] 1
    

然而,在分散超过限制的情况下,更高阶的泊松回归是一个合适的解决方案。考虑到本书的范围,我们不会在这里详细探讨此类模型。感兴趣的读者可以阅读有关基线密度泊松(p 阶泊松多项式PPp)模型)的更多内容。

Cox 比例风险回归模型

Cox 回归模型的基础来自生存分析,一组有助于调查事件发生时间的统计方法。以下是一些例子:

  • 将潜在客户转化为销售的时间

  • 从使用开始到产品故障的时间

  • 从保险单开始到死亡的时间

  • 从诊断到死亡的时间

  • 产品保修索赔的时间

  • 从客户注册开始的时间

所有这些例子都是生存分析的一些用例。在大多数生存分析中,有三种广泛使用的方法用于执行此类时间至事件分析:

  • 用于分析不同组的 Kaplan-Meier 生存曲线

  • 用于比较两个或更多生存曲线的对数秩检验

  • Cox 比例风险回归用于描述变量对生存的影响

考虑到本章和本书的范围,我们将只关注 Cox 比例风险回归。基本思想是前两种方法仅有助于进行单变量分析,换句话说,您只能理解一个因素对时间至事件的影响,而 Cox 回归有助于评估多个因素对生存时间的影响。此外,Cox 回归对分类和数值因素都同样有效,而前两种方法仅适用于分类因素。

Cox 模型由表示死亡风险的危害函数 h(t) 表示,在医学研究中,它主要被使用,表示在时间 t 死亡的风险:

图片

从这个方程中可以观察到以下几点:

  • t 表示生存时间。

  • h(t) 表示由 p 个协变量确定的风险函数 图片。在生存分析中,协变量是用来描述预测变量的术语。

  • 系数 图片 表示协变量的影响。

  • 术语 图片 被称为时间 t 的基线危害。如果所有系数都为零 图片图片 就成为危害函数的值。

这个函数看起来与逻辑回归(使用指数项)有些相似,这将在 第五章分类 中详细讨论。我们逻辑上把本书中讨论的所有监督学习算法分为 是/否1/0),但忽略了事件的时间。

如您从危害函数中观察到的,生存模型包括:

  • 表示事件发生时间的连续变量

  • 一个表示事件是否发生的二元变量

NCCTG 肺癌数据

NCCTG 肺癌数据(来自晚期肺癌患者的生存数据)来自 North Central Cancer Treatment Group。这些数据包括一些元数据,例如收集数据的机构、患者的年龄、性别等。该数据集中的性能评分衡量患者进行日常活动的能力。在任何生存分析数据集中,最重要的变量是关于 事件时间知识,例如,死亡时间。

生存分析通常定义为一种检查数据的方法,其中结果变量是感兴趣事件发生的时间。

图 4.21:北中癌症治疗组的变量及其描述

图 4.21:北中癌症治疗组的变量及其描述

图 4.21:北中癌症治疗组的变量及其描述

在下一个练习中,我们将学习如何使用survival包中的Surv方法创建生存对象。请注意,在添加生存对象后,数据集的摘要中创建了两个额外的变量SurvObject.timeSurvObject.status,它们存储关于事件发生时间(死亡时间)的信息,这随后成为Cox 比例风险回归模型的因变量。

当患者生存时间周围的指示数量稀少时,观察结果会被截尾。通常,最常见的形式是右侧截尾。假设我们正在跟踪一项为期 20 周的研究。在研究期间未经历感兴趣事件的病人可以被称为右侧截尾。该人的生存时间至少是研究持续时间;在这种情况下,20 周。

练习 62:使用 Cox 回归探索 NCCTG 肺癌数据

在这个练习中,我们将使用 Cox 回归探索 NCCTG 肺癌数据。

执行以下步骤以完成练习:

  1. 导入survival库:

    library(survival)
    
  2. 导入肺癌数据:

    df_lung_cancer <- lung
    
  3. 使用head函数打印数据集:

    head(df_lung_cancer)
    

    输出如下:

    ##   inst time status age sex ph.ecog ph.karno pat.karno meal.cal wt.loss
    ## 1    3  306      2  74   1       1       90       100     1175      NA
    ## 2    3  455      2  68   1       0       90        90     1225      15
    ## 3    3 1010      1  56   1       0       90        90       NA      15
    ## 4    5  210      2  57   1       1       90        60     1150      11
    ## 5    1  883      2  60   1       0      100        90       NA       0
    ## 6   12 1022      1  74   1       1       50        80      513       0
    
  4. 肺癌数据中,status == 2 代表死亡:

    df_lung_cancer$SurvObject <- with(df_lung_cancer, Surv(time, status == 2))
    
  5. 找到 Cox 比例风险回归模型:

    cox_regression <- coxph(SurvObject ~ age + sex + ph.karno + wt.loss, data =  df_lung_cancer)
    cox_regression
    

    输出如下:

    ## Call:
    ## coxph(formula = SurvObject ~ age + sex + ph.karno + wt.loss, 
    ##     data = df_lung_cancer)
    ## 
    ## 
    ##              coef exp(coef) se(coef)     z      p
    ## age       0.01514   1.01525  0.00984  1.54 0.1238
    ## sex      -0.51396   0.59813  0.17441 -2.95 0.0032
    ## ph.karno -0.01287   0.98721  0.00618 -2.08 0.0374
    ## wt.loss  -0.00225   0.99776  0.00636 -0.35 0.7239
    ## 
    ## Likelihood ratio test=18.8  on 4 df, p=0.000844
    ## n= 214, number of events= 152 
    ##    (14 observations deleted due to missingness)
    

本练习演示了使用生存库的 Cox 比例风险回归模型。

摘要

在本章中,我们在上一章简要介绍线性回归之后,对其进行了更详细的讨论。当然,关于线性回归的讨论导致了一系列诊断,为讨论其他类型的回归算法提供了方向。分位数、多项式、岭回归、LASSO 和弹性网络,所有这些都是从线性回归中派生出来的,区别在于线性回归存在一些局限性,而每个算法都帮助克服了这些局限性。泊松回归和 Cox 比例风险回归模型作为回归算法的特殊情况出现,分别适用于计数和事件发生时间依赖变量,而其他算法适用于任何定量依赖变量。

在下一章中,我们将探讨第二常用的机器学习算法及其相关问题。你还将更详细地了解分类。第五章分类,与本章类似,旨在详细介绍从决策树深度神经网络的分类算法。

第五章:分类

学习目标

到本章结束时,你将能够:

  • 定义监督机器学习中的二元分类

  • 使用白盒模型进行二元分类:逻辑回归和决策树

  • 评估监督分类模型的表现

  • 使用黑盒集成模型进行二元分类 - 随机森林和 XGBoost

  • 设计和开发用于分类的深度神经网络

  • 为给定的分类用例选择最佳模型

在本章中,我们将专注于解决监督学习的分类用例。我们将使用一个为分类用例设计的数据库,围绕它构建一个业务问题,并探索一些流行的技术来解决该问题。

简介

让我们快速回顾一下我们在第三章中学到的内容,监督学习简介。正如你现在所知道的,监督学习是机器学习和人工智能的一个分支,它帮助机器在没有明确编程的情况下学习。描述监督学习的一种更简单的方式是开发从标记数据中学习的算法。监督学习的广泛类别包括分类和回归,它们的基本区别在于标签的类型,即连续分类。处理连续变量的算法被称为回归算法,而处理分类变量的算法被称为分类算法

在分类算法中,我们的目标、依赖或标准变量是一个分类变量。根据类别的数量,我们可以进一步将它们分为以下几组:

  • 二元分类

  • 多项式分类

  • 多标签分类

在本章中,我们将专注于二元分类。讨论多项式分类和多类分类的细节和实际例子超出了本章的范围;然而,在结束本章之前,我们将列出一些高级主题的额外阅读参考。

二元分类算法是机器学习中最受欢迎的算法类别,在商业、研究和学术界有众多应用。简单的模型可以根据学生的过去表现(通过或失败)来预测他们未来考试通过的可能性,预测是否会下雨,预测客户是否会违约,预测患者是否患有癌症等等,这些都是由分类算法解决的常见用例。

在深入研究算法之前,我们将首先从一个小案例开始,这个小案例将帮助我们通过实际练习解决监督学习分类问题。

开始使用用例

在本章中,我们将参考来自澳大利亚联邦气象局并通过 R 提供的weather数据集。该数据集有两个目标变量,RainTomorrow,一个表示明天是否会下雨的标志,以及RISK_MM,它衡量的是下一天降雨量。

简而言之,我们可以使用这个数据集的RainTomorrow进行分类练习。有关数据集的元数据和附加详细信息可在 https://www.rdocumentation.org/packages/rattle/versions/5.2.0/topics/weather 上探索。由于数据集可以通过 R 直接使用,我们不需要单独下载它;相反,我们可以直接使用rattle库中的 R 函数将数据加载到系统内存中。

关于用例的一些背景信息

记录了温度、方向、气压、云量、湿度和日照等天气参数,持续一年。下一天的降雨量已经在数据集中作为目标变量RainTomorrow进行工程化。我们可以利用这些数据定义一个机器学习模型,该模型从当天的天气参数中学习,并预测下一天的降雨概率。

雨量预测对许多行业至关重要。火车和长途汽车的长途旅行通常关注天气模式的变化,主要是降雨,以估计到达时间和旅行长度。同样,大多数实体店、小型餐馆和食品摊位等也受到降雨的严重影响。了解明天的天气条件可以帮助企业从多个方面更好地准备,以应对业务损失,在某些情况下,甚至可以最大化业务成果。

为了在问题解决练习中建立良好的直觉,让我们使用数据集构建一个业务问题,并为用例制定问题陈述。由于数据是关于降雨预测的,我们将选择当今超本地食品配送服务面临的流行业务问题。DoorDash、Skip the Dishes、FoodPanda、Swiggy、Foodora 等初创公司为不同国家的客户提供超本地食品配送服务。在大多数国家,一个普遍的趋势是随着雨季的到来,食品配送订单量增加。一般来说,大多数配送公司预计在给定的一天内总配送量会增加 30%-40%。由于配送代理人数有限,雨天订单的增加对配送时间影响巨大。为了保持成本最优,这些公司不可能增加全职代理人数;因此,一个常见的策略是在预计服务需求高的日子里动态雇佣更多代理。为了更好地规划,了解下一天的降雨预测至关重要。

定义问题陈述

在设置好问题背景后,让我们尝试为一家超本地食品配送服务公司定义我们的问题陈述,以预测明天的降雨量。为了保持简单和一致性,让我们使用之前研究的框架,即第二章数据探索分析来构建问题陈述。这将帮助我们以业务优先的方法提炼出我们想要解决的最终目标,同时将机器学习视角放在首位。

下图展示了之前定义的使用案例的简单视觉框架——情境-复杂性-问题SCQ)框架:

图 5.1:分类使用案例的 SCQ

图 5.1:分类使用案例的 SCQ

我们可以清楚地从 SCQ 中回答问题:我们需要一个预测模型来预测明天的降雨概率,作为解决问题的解决方案。让我们继续下一步——收集数据以构建一个预测模型,这将帮助我们解决业务问题。

数据收集

rattle.data包为我们提供了使用案例的数据,可以使用 R 的内部数据集方法访问。如果你还没有安装这些包,你可以使用install.packages("rattle.data")命令轻松安装它们。

练习 63:探索使用案例的数据

在这个练习中,我们将对为使用案例收集的数据集进行初步探索。我们将探索数据的形状,即行数和列数,并研究每个列中的内容。

为了探索数据的形状(行数 x 列数)和内容,执行以下步骤:

  1. 首先,使用以下命令加载rattle包:

    library(rattle.data)
    
  2. 加载我们使用案例的数据,这些数据可以从rattle包中获取:

    data(weatherAUS)
    

    注意

    weatherAUS数据集是一个 DataFrame,包含来自 45 个以上澳大利亚气象站的超过 140,000 条每日观测数据。

  3. 现在,将天气数据直接加载到名为df的 DataFrame 中:

    df <- weatherAUS
    
  4. 使用str命令探索 DataFrame 的内容:

    str(df)
    

    输出如下:

图 5.2:最终输出

图 5.2:最终输出

我们有近 150,000 行数据,24 个变量。我们需要删除RISK_MM变量,因为它将是回归使用案例(即预测明天下雨量)的目标变量。因此,我们剩下 22 个独立变量和 1 个因变量RainTomorrow,用于我们的使用案例。我们还可以看到连续变量和分类变量的良好混合。LocationWindDirRainToday等多个变量是分类的,其余的是连续的。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/2Vwgu8Q。

在下一个练习中,我们将计算每个列中缺失值的总百分比。

练习 64:计算所有列的缺失值百分比

我们在练习 1探索数据集以用于用例中探索的数据集有相当多的空值。在这个练习中,我们将编写一个脚本来计算每个列中空值的百分比。

我们可以看到几个变量中存在空值。让我们检查df数据集中每个列的空值百分比。

执行以下步骤以计算数据集中每列的空值百分比:

  1. 首先,移除名为RISK_MM的列,因为它打算用作回归用途的目标变量。(将其添加到我们的模型会导致数据泄露):

    df$RISK_MM <- NULL
    
  2. 创建一个temp_df DataFrame 对象并将其值存储在其中:

    temp_df<-as.data.frame(
      sort(
      round(
      sapply(df, function(y) sum(length(which(is.na(y)))))/dim(df)[1],2)
      )
    )
    colnames(temp_df) <- "NullPerc"
    
  3. 现在,使用print函数显示每列的空值百分比,使用以下命令:

    print(temp_df)
    

    输出如下:

                  NullPerc
    Date              0.00
    Location          0.00
    MinTemp           0.01
    MaxTemp           0.01
    WindSpeed9am      0.01
    Temp9am           0.01
    Rainfall          0.02
    WindSpeed3pm      0.02
    Humidity9am       0.02
    Temp3pm           0.02
    RainToday         0.02
    RainTomorrow      0.02
    WindDir3pm        0.03
    Humidity3pm       0.03
    WindGustDir       0.07
    WindGustSpeed     0.07
    WindDir9am        0.07
    Pressure9am       0.10
    Pressure3pm       0.10
    Cloud9am          0.38
    Cloud3pm          0.41
    Evaporation       0.43
    Sunshine          0.48
    

我们可以看到最后四个变量有超过30%的缺失或空值。这是一个相当大的下降。最好从我们的分析中删除这些变量。此外,我们还可以看到一些其他变量大约有1%2%,在某些情况下,高达10%*的缺失或空值。我们可以使用各种缺失值处理技术来处理这些变量,例如用均值或众数替换它们。在某些重要情况下,我们还可以使用基于聚类的均值和众数替换等额外技术来提高处理效果。此外,在非常关键的场景中,我们可以使用回归模型来估计剩余缺失值的剩余部分,通过定义一个模型,其中所需的缺失值列被视为剩余变量的函数。

注意

您可以在 GitHub 上找到完整的代码:http://bit.ly/2ViZEp1。

在以下练习中,我们将移除空值。如果没有合适的模型,我们将重新审视数据。

练习 65:从数据集中移除空值

约翰正在处理新创建的数据集,在进行分析时,他发现数据集中存在显著的空值。为了使数据集对进一步分析有用,他必须从其中移除空值。

执行以下步骤以从df数据集中移除空值:

  1. 首先,使用以下命令选择最后四列,这些列的空值超过30%

    cols_to_drop <-tail(rownames(temp_df),4)
    
  2. 使用na.omit命令从 DataFrame 中移除所有包含一个或多个空值列的行,该命令会从 DataFrame 中移除所有空行:

    df_new<- na.omit(df[,!names(df) %in% cols_to_drop])
    
  3. 现在,使用以下print命令打印新格式化的数据:

    print("Shape of data after dropping columns:")
    print(dim(df_new))
    

    输出如下:

    Shape of data after dropping columns:
    112925     19
    
  4. 使用以下命令,验证新创建的数据集中是否存在空值:

    temp_df<-as.data.frame(sort(round(sapply(df_new, function(y) sum(length(which(is.na(y)))))/dim(df)[1],2)))
    colnames(temp_df) <- "NullPerc"
    
  5. 现在,使用以下print命令打印数据集:

    print(temp_df)
    

    输出如下:

                  NullPerc
    Date                 0
    Location             0
    MinTemp              0
    MaxTemp              0
    Rainfall             0
    WindGustDir          0
    WindGustSpeed        0
    WindDir9am           0
    WindDir3pm           0
    WindSpeed9am         0
    WindSpeed3pm         0
    Humidity9am          0
    Humidity3pm          0
    Pressure9am          0
    Pressure3pm          0
    Temp9am              0
    Temp3pm              0
    RainToday            0
    RainTomorrow         0
    

我们现在可以再次检查,看看新的数据集没有更多的缺失值,数据集的总行数也减少到 112,000 行,这大约是训练数据的20%损失。我们应该使用替换缺失值(如平均值、众数或中位数)等技术来对抗由于缺失值的省略而导致的高损失。一个经验法则是安全地忽略小于5%的损失。由于我们有超过 100,000 条记录(对于简单用例来说是一个相当高的记录数),我们正在忽略这个经验法则。

注意

您可以在 GitHub 上找到完整的代码:http://bit.ly/2Q3HIgT。

此外,我们还可以使用Date列来构建日期和时间相关的特征。以下练习创建了诸如日、月、星期和季度等数值特征作为额外的时相关特征,并删除了原始的Date变量。

我们将使用 R 中的lubridate库来处理日期和时间相关特征。它为我们提供了执行日期和时间操作的极其易于使用的函数。如果您尚未安装此包,请使用install.packages('lubridate')命令安装库。

练习 66:从日期变量中构建基于时间的特征

时间和日期相关的属性不能直接用于监督分类模型。为了从日期和时间相关的变量中提取有意义的属性,通常的做法是从日期中创建月份、年份、星期和季度作为特征。

执行以下步骤以在 R 中处理数据和时间函数:

  1. 使用以下命令将lubridate库导入 RStudio:

    library(lubridate)
    

    注意

    lubridate库提供了方便的日期和时间相关函数。

  2. 使用lubridate函数从Date变量中提取daymonthdayofweekquarter作为新特征:

    df_new$day <- day(df_new$Date)
    df_new$month <- month(df_new$Date)
    df_new$dayofweek <- wday(df_new$Date)
    df_new$quarter <- quarter(df_new$Date)
    
  3. 检查新创建的变量:

    str(df_new[,c("day","month","dayofweek","quarter")])
    
  4. 现在我们已经创建了所有日期和时间相关的特征,我们不再需要实际的Date变量。因此,删除之前的Date列:

    df_new$Date <- NULL
    

    输出如下:

    'data.frame':	112925 obs. of  4 variables:
     $ day      : int  1 2 3 4 5 6 7 8 9 10 ...
     $ month    : num  12 12 12 12 12 12 12 12 12 12 ...
     $ dayofweek: num  2 3 4 5 6 7 1 2 3 4 ...
     $ quarter  : int  4 4 4 4 4 4 4 4 4 4 ...
    

在这个练习中,我们从日期和时间相关的属性中提取了有意义的特征,并删除了实际的日期相关列。

注意

您可以在 GitHub 上找到完整的代码:http://bit.ly/2E4hOEU。

接下来,我们需要处理或清理 DataFrame 中的另一个特征:location

练习 67:探索位置频率

Location变量定义了在指定时间实际捕获天气数据的实际位置。让我们快速检查这个变量中捕获的不同值的数量,看看是否有任何可能重要的有趣模式。

在以下练习中,我们将使用Location变量来定义在指定时间实际捕获天气数据的实际位置。

执行以下步骤:

  1. 使用dplyr包中的分组函数计算每个位置的降雨频率:

    location_dist <- df_new %>%    group_by(Location) %>%     summarise(Rain  = sum(ifelse(RainTomorrow =="Yes",1,0)), cnt=n()) %>%    mutate(pct = Rain/cnt) %>%    arrange(desc(pct))
    
  2. 检查不同位置的数量以确保正确:

    print(paste("#Distinct locations:",dim(location_dist)[1]))
    

    输出如下:

    "#Distinct locations: 44"
    
  3. 打印summary以检查执行的聚合:

    print(summary(location_dist))
    

    输出如下:

        Location        Rain             cnt            pct         
     Adelaide     : 1   Min.   : 102.0   Min.   : 670   Min.   :0.06687  
     Albury       : 1   1st Qu.: 427.8   1st Qu.:2330   1st Qu.:0.18380  
     AliceSprings : 1   Median : 563.5   Median :2742   Median :0.21833  
     BadgerysCreek: 1   Mean   : 568.6   Mean   :2566   Mean   :0.21896  
     Ballarat     : 1   3rd Qu.: 740.5   3rd Qu.:2884   3rd Qu.:0.26107  
     Bendigo      : 1   Max.   :1031.0   Max.   :3117   Max.   :0.36560  
     (Other)      :38  
    

我们可以看到数据中有 44 个不同的位置。定义每个位置记录数(在之前转换的数据中)的cnt变量,平均有 2,566 条记录。第一四分位数、中位数和第三四分位数的相似数量分布表明,位置在数据中分布均匀。然而,如果我们调查记录降雨的记录百分比(pct),我们会看到一个有趣的趋势。在这里,我们有一些位置大约有 6%的降雨概率,还有一些位置大约有 36%的降雨概率。根据位置的不同,降雨的可能性有很大的差异。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/30aKUMx。

由于我们有大约 44 个不同的位置,直接将此变量作为分类特征使用是有困难的。在 R 中,大多数监督学习算法内部将分类列转换为模型可以解释的数值形式。然而,随着分类变量中类别的数量增加,模型的复杂性也随之增加,但没有额外的价值。为了保持简单,我们可以将Location变量转换为一个具有较少级别的新的变量。我们将选择降雨概率最高的五个和最低的五个位置,并将所有其他位置标记为Others。这将减少变量中不同级别的数量为 10+1,这将更适合模型。

练习 68:创建具有较少级别的新的位置

location变量有太多的不同值(44 个位置),通常机器学习模型在具有高频率不同类别的分类变量上表现不佳。因此,我们需要通过减少变量中不同类别的数量来修剪变量。我们将选择降雨概率最高的五个和最低的五个位置,并将所有其他位置标记为Others。这将减少变量中不同级别的数量为 10+1,这将更适合模型。

执行以下步骤以创建一个具有较少不同级别的位置新变量:

  1. location变量从因子转换为字符:

    location_dist$Location <- as.character(location_dist$Location)
    
  2. 创建一个包含降雨概率最高和最低的五个位置的列表。我们可以通过在按升序排序的 DataFrame 中使用head命令获取前五个位置,以及使用tail命令获取后五个位置来实现这一点:

    location_list <- c(head(location_dist$Location,5),tail(location_dist$Location,5))
    
  3. 打印列表以确认我们已经正确存储了位置:

    print("Final list of locations - ")
    print(location_list)
    

    输出如下:

    [1] "Final list of locations - "
     [1] "Portland"      "Walpole"       "Dartmoor"      "Cairns"       
     [5] "NorfolkIsland" "Moree"         "Mildura"       "AliceSprings" 
     [9] "Uluru"         "Woomera" 
    
  4. 将主df_new DataFrame 中的Location变量转换为字符

    df_new$Location <- as.character(df_new$Location)
    
  5. 减少变量中不同位置的数量。这可以通过将所有不属于location_list列表的位置标记为Others来实现:

    df_new$new_location <- factor(ifelse(df_new$Location %in% location_list,df_new$Location,"Others"))
    
  6. 使用以下命令删除旧的Location变量:

    df_new$Location <- NULL
    
  7. 为了确保第五步正确执行,我们可以创建一个临时 DataFrame,并总结记录频率与我们所创建的新location变量之间的对比:

    temp <- df_new %>% mutate(loc = as.character(new_location)) %>%    group_by(as.character(loc)) %>%    summarise(Rain  = sum(ifelse(RainTomorrow =="Yes",1,0)), cnt=n()) %>%    mutate(pct = Rain/cnt) %>%    arrange(desc(pct))
    
  8. 打印临时测试 DataFrame 并观察结果。我们应该只看到 11 个不同的位置值:

    print(temp)
    

    输出如下:

    # A tibble: 11 x 4
       `as.character(loc)`  Rain   cnt    pct
       <chr>               <dbl> <int>  <dbl>
      Portland             1031  2820 0.366 
      Walpole               864  2502 0.345 
      Dartmoor              770  2294 0.336 
      Cairns                910  2899 0.314 
      NorfolkIsland         883  2864 0.308 
      Others              19380 86944 0.223 
      Moree                 336  2629 0.128 
      Mildura               315  2897 0.109 
      AliceSprings          227  2744 0.0827
      Uluru                 110  1446 0.0761
      Woomera               193  2886 0.0669
    

我们首先将Location变量从因子转换为字符,以简化字符串操作任务。DataFrame 根据降雨概率的百分比降序排序。headtail命令用于提取列表中的前五个和后五个位置。然后,这个列表被用作参考检查,以减少新特征中的级别数量。最后,在工程化减少级别的新的特征后,我们进行简单的检查以确保我们的特征已经按照预期的方式工程化。

注意

您可以在 GitHub 上找到完整的代码:http://bit.ly/30fnR31。

让我们现在进入本章最有趣的主题,并探讨监督学习的分类技术。

监督学习的分类技术

要接近监督分类算法,我们首先需要理解算法的基本功能,以抽象的方式探索一点数学,然后使用 R 中现成的包来开发算法。我们将介绍几个基本算法,例如透明算法如逻辑回归和决策树,然后我们将转向高级建模技术,例如黑盒模型如随机森林、XGBoost 和神经网络。我们计划涵盖的算法列表并不全面,但这五个算法将帮助您对主题有一个广泛的理解。

逻辑回归

逻辑回归是用于二元分类的最受欢迎的透明模型。透明模型定义为我们可以看到用于预测的整个推理过程的模型。对于每个做出的预测,我们可以利用模型的数学方程式来解码预测的原因。也存在一组完全黑盒的分类模型,也就是说,我们根本无法理解模型利用的预测推理。在我们只想关注最终结果的情况下,我们应该选择黑盒模型,因为它们更强大。

尽管名称以回归结尾,但逻辑回归是一种用于预测二元分类结果的技巧。我们需要不同的方法来对分类结果进行建模。这可以通过将结果转换为优势比的对数或事件发生的概率来实现。

让我们将这种方法提炼成更简单的结构。假设一个事件成功的概率为 0.8。那么,同一事件失败的概率将被定义为(1-0.8) = 0.2。成功的优势被定义为成功概率与失败概率的比率。

在以下示例中,成功的优势将是(0.8/0.2) = 4。这意味着成功的优势是四比一。如果成功的概率是 0.5,即 50-50 的机会,那么成功的优势是 0.5 比 1。逻辑回归模型可以用以下方式数学表示:

图片

其中,图片是优势比的对数。

进一步解决数学问题,我们可以推导出以下结果:

图片

讨论方程的数学背景和推导超出了本章的范围。为了总结,logit函数,即连接函数,帮助逻辑回归直观地将问题(预测结果)重新表述为优势比的对数。当求解时,它帮助我们预测二元因变量的概率。

逻辑回归是如何工作的?

就像线性回归中,变量的贝塔系数是通过普通最小二乘法OLS)来估计的,逻辑回归模型利用最大似然估计MLE)。MLE 函数估计模型参数或贝塔系数的最佳值集,以最大化似然函数,即概率估计,这也可以定义为所选模型与观察数据的一致性。当最佳参数值集被估计出来后,将这些值或贝塔系数按先前定义的方式插入到模型方程中,将有助于估计给定样本的输出概率。类似于 OLS,MLE 也是一个迭代过程。

让我们看看逻辑回归模型在我们数据集上的实际应用。为了开始,我们将只使用模型的一小部分变量。理想情况下,建议根据 EDA 练习开始于最重要的变量,然后逐步添加剩余变量。现在,我们将从一个与最高和最低温度值相关的变量开始,一个与风速相关的变量,下午 3 点的气压和湿度,以及当天的降雨量。

我们将整个数据集分为训练集(70%)和测试集(30%)。在将数据拟合到模型时,我们只将使用训练集,稍后将在训练集以及未见过的测试数据上评估模型的表现。这种方法将帮助我们了解我们的模型是否过拟合,并在未见过的数据上提供更现实的模型性能。

练习 69:构建逻辑回归模型

我们将使用逻辑回归和我们在练习 1-6 中探索的数据集构建一个二元分类模型。我们将数据分为训练集和测试集(分别为 70%和 30%),利用训练数据来拟合模型,并使用测试数据来评估模型在未见数据上的性能。

执行以下步骤来完成练习:

  1. 首先,使用以下命令设置 seed 以确保可重复性:

    set.seed(2019)
    
  2. 接下来,为训练数据集(70%)创建一个索引列表:

    train_index <- sample(seq_len(nrow(df_new)),floor(0.7 * nrow(df_new)))
    
  3. 现在,使用以下命令将数据分割为测试集和训练集:

    train <- df_new[train_index,]
    test <- df_new[-train_index,]
    
  4. 使用 RainTomorrow 作为因变量和几个自变量(我们选择了 MinTempRainfallWindGustSpeedWindSpeed3pmHumidity3pmPressure3pmRainTodayTemp3pmTemp9am)构建逻辑回归模型。我们也可以在 DataFrame 中添加所有可用的自变量:

    model <- glm(RainTomorrow ~ MinTemp + Rainfall + WindGustSpeed +         WindSpeed3pm +Humidity3pm + Pressure3pm +        RainToday +  Temp3pm + Temp9am,         data=train,        family=binomial(link='logit'))
    
  5. 使用 summary 函数打印数据集的摘要:

    summary(model)
    

    输出如下:

    Call:
    glm(formula = RainTomorrow ~ MinTemp + Rainfall + WindGustSpeed + 
        WindSpeed3pm + Humidity3pm + Pressure3pm + RainToday + Temp3pm + 
        Temp9am, family = binomial(link = "logit"), data = train)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -2.9323  -0.5528  -0.3235  -0.1412   3.2047  
    Coefficients:
                    Estimate Std. Error z value Pr(>|z|)    
    (Intercept)    6.543e+01  1.876e+00  34.878  < 2e-16 ***
    MinTemp        9.369e-05  5.056e-03   0.019    0.985    
    Rainfall       7.496e-03  1.404e-03   5.337 9.44e-08 ***
    WindGustSpeed  5.817e-02  1.153e-03  50.434  < 2e-16 ***
    WindSpeed3pm  -4.331e-02  1.651e-03 -26.234  < 2e-16 ***
    Humidity3pm    7.363e-02  9.868e-04  74.614  < 2e-16 ***
    Pressure3pm   -7.162e-02  1.821e-03 -39.321  < 2e-16 ***
    RainTodayYes   4.243e-01  2.751e-02  15.425  < 2e-16 ***
    Temp3pm        3.930e-02  5.171e-03   7.599 2.98e-14 ***
    Temp9am       -4.605e-02  6.270e-03  -7.344 2.07e-13 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 83718 on 79046 degrees of freedom
    Residual deviance: 56557 on 79037 degrees of freedom
    AIC: 56577
    Number of Fisher Scoring iterations: 5
    

set.seed 命令确保用于训练和测试数据集分割的随机选择可以重现。我们将数据分为 70%的训练集和 30%的测试集。设置种子函数确保,对于相同的种子,每次都能得到相同的分割。在 R 中,glm 函数用于构建广义线性模型。在模型中使用 family 参数值设置为 binomial(link ='logit') 定义了逻辑回归。glm 函数还可以用于构建其他几种模型(如 gamma、Poisson 和 binomial)。公式定义了因变量,以及一组自变量。它采用一般形式 Var1 ~ Var2 + Var3 + …,表示 Var1 为因变量或目标变量,其余为自变量。如果我们想使用 DataFrame 中的所有变量作为自变量,我们可以使用 formula = Var1 ~ .,这表示 Var1 是因变量,其余都是自变量。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/2HwwUUX。

逻辑回归结果解释

我们在 第二章数据探索分析 中曾经简要地了解过逻辑回归,但并未深入探讨模型结果的细节。前一个输出片段中展示的结果将类似于你在线性回归中观察到的结果,但也有一些不同之处。让我们逐部分探索和解释这些结果。

首先,我们有的glm函数计算两种类型的残差,即零偏差残差偏差。两者的区别在于,一个报告了只使用截距(即没有因变量)时的拟合优度,而另一个报告了使用所有提供的自变量时的拟合优度。零偏差和残差偏差之间的偏差减少有助于我们理解独立变量在定义方差或预测正确性时增加的量化值。偏差残差的分布紧随公式之后报告。

接下来,我们有beta 系数和相关的标准误差z 值p 值,即显著性概率。对于每个提供的变量,R 内部计算系数,并连同参数值一起报告额外的测试结果,帮助我们解释这些系数的有效性。系数的绝对值是理解该变量对最终预测能力重要性的简单方法,即该变量在确定预测结果最终结果中的影响程度。我们可以看到所有变量的系数值都很低。

接下来,标准误差帮助我们量化值的稳定性。标准误差的值越低,表明 beta 系数的值越一致或稳定。在我们练习中的所有变量的标准误差都很低。z 值和显著性概率共同帮助我们判断结果是否具有统计学意义,或者只是由于随机机会而看似如此。这个想法遵循我们在第二章数据探索分析中学到的相同原理,类似于我们在第四章回归中学到的线性回归参数显著性。

解释显著性的最简单方法就是研究每个自变量旁边的星号,即*。星号的数量由实际概率值定义,如下所示。在我们的练习中,注意MinTemp变量不具有统计学意义,即p 值 > 0.05。其余的都是具有统计意义的变量。

赤池信息量准则AIC)是 R 报告的另一个指标,用于评估模型的拟合度或模型的质量。这个数字在比较同一用例的不同模型时非常有用。比如说,你使用一组独立变量但相同的因变量拟合了几个模型,AIC 可以通过简单比较所有模型中的值来研究最佳模型。该指标的计算来源于模型预测与实际标签之间的偏差,但考虑了没有增加任何价值的变量。因此,类似于线性回归中的R 平方调整 R 平方,AIC 帮助我们避免构建复杂的模型。要从候选模型列表中选择最佳模型,我们应该选择 AIC 最低的模型。

在上一个输出的末尾,我们可以看到费舍尔评分算法的结果,这是一种牛顿法求解最大似然问题的衍生方法。我们看到它需要五次迭代来将数据拟合到模型中,但除此之外,这些信息对我们来说价值不大。这仅仅是我们得出模型已经收敛的简单指示。

我们现在已经理解了逻辑回归的工作原理,并解释了模型在 R 中报告的结果。然而,我们仍然需要使用我们的训练集和测试集来评估模型结果,并确保模型在未见过的数据上表现良好。为了研究分类模型的表现,我们需要利用各种指标,例如准确率、精确率和召回率。尽管我们已经在第四章回归中探讨了它们,但现在让我们更详细地研究它们。

评估分类模型

分类模型需要一系列不同的指标来彻底评估,这与回归模型不同。在这里,我们没有像R 平方这样直观的东西。此外,性能要求完全基于特定的用例。让我们简要地看看我们在第三章监督学习简介中已经研究过的各种指标,用于分类。

混淆矩阵及其衍生指标

研究分类算法模型性能的第一个基础是从混淆矩阵开始。混淆矩阵是每个类别实际值中预测分布的简单表示:

图 5.3:混淆矩阵

图 5.3:混淆矩阵

之前的表格是混淆矩阵的简单表示。在这里,我们假设10;当结果被正确预测时,我们将其正确预测的1分配为1,依此类推,对于剩余的结果也是如此。

基于混淆矩阵及其定义的值,我们可以进一步定义一些指标,帮助我们更好地理解模型的表现。从现在开始,我们将使用缩写 TP 代表 True Positive(真正),FP 代表 False Positive(假正),TN 代表 True Negative(真负),FN 代表 False Negative(假负):

  • 整体准确率:整体准确率定义为整个测试样本中正确预测总数与预测总数的比率。因此,这将是真正真负的总和除以混淆矩阵中的所有指标:

图片

  • 精确度阳性预测值PPV):精确度定义为正确预测的正标签数与所有预测为正标签的总数的比率:

图片

  • 召回率灵敏度:召回率通过表示正确预测的正标签数与实际正标签总数的比率来衡量你的模型灵敏度:

图片

  • 特异性真正负率TNR):特异性定义为正确预测的负标签数与实际负标签总数的比率:

图片

  • F1 分数:F1 分数是精确度和召回率的调和平均值。对于大多数情况来说,它是一个比整体准确率更好的指标:

图片

你应该选择哪个指标?

在认真考虑的情况下,另一个重要的方面是我们在评估模型时应考虑哪个指标。没有直接的答案,因为最佳指标组合完全取决于我们处理的分类用例类型。在分类用例中,常见的一种情况是类别不平衡。我们并不总是需要在数据中保持正负标签的平等分布。事实上,在大多数情况下,我们会遇到正类别的数据少于 30% 的情况。在这种情况下,整体准确率并不是一个理想的指标来考虑。

让我们通过一个简单的例子来更好地理解这一点。考虑预测信用卡交易中的欺诈的例子。在现实场景中,对于每 100 笔交易,可能只有一两次欺诈交易。现在,如果我们只用整体准确率作为评估模型的唯一指标,即使我们将所有标签预测为,即非欺诈,我们会有大约 99% 的准确率,0% 的精确度和 0% 的召回率。99% 的准确率可能看起来是一个很好的模型性能指标;然而,在这种情况下,它并不是一个理想的评估指标。

为了处理这种情况,通常需要额外的业务背景来做出有形的决策,但在大多数情况下(对于此类场景),业务希望召回率更高,同时整体准确率和精度有所妥协。使用高召回率作为模型评估指标的合理性在于,即使交易是真实的,预测交易为欺诈仍然是可以接受的;然而,将欺诈交易预测为真实将是一个错误;业务损失将是巨大的。

通常,模型的评估是根据业务需求结合多种指标进行的。最大的决策者将是精度和召回率之间的权衡。如混淆矩阵所示,每当试图提高精度时,都会损害召回率,反之亦然。

这里有一些我们优先考虑不同指标的业务场景:

  • 预测具有灾难性后果的罕见事件:当预测患者是否有癌症或交易是否为欺诈等情况时,预测一个没有癌症的人患有癌症是可以接受的,但反过来预测会导致生命损失。这些场景需要通过妥协精度整体准确率来保证高召回率。

  • 预测具有非灾难性后果的罕见事件:当预测客户是否会流失或客户是否会积极回应营销活动时,错误的预测不会危及业务结果,而是会危及活动本身。在这种情况下,根据具体情况,拥有高精度并稍微妥协召回率是有意义的。

  • 预测具有非灾难性后果的常规(非罕见)事件:这将处理大多数分类用例,其中正确预测一个类别的成本几乎等于错误预测该类别的成本。在这种情况下,我们可以使用 F1 分数,它代表精度和召回率之间的调和平均值。使用整体准确率与 F1 分数结合使用将是理想的,因为准确率更容易解释。

评估逻辑回归

现在我们来评估我们之前构建的逻辑回归模型。

练习 70:评估逻辑回归模型

在训练数据集上拟合的机器学习模型不能使用同一数据集进行评估。我们需要利用一个单独的测试数据集,并比较模型在训练集和测试集上的性能。caret包有一些方便的函数来计算之前讨论过的模型评估指标。

执行以下步骤以评估我们在练习 7构建逻辑回归模型中构建的逻辑回归模型:

  1. 计算 DataFrame df_newRainTomorrow目标变量的记录分布:

    print("Distribution of labels in the data-")
    print(table(df_new$RainTomorrow)/dim(df_new)[1])
    

    输出如下:

    "Distribution of labels in the data-"
           No       Yes 
    0.7784459 0.2215541 
    
  2. 使用 predict 函数在训练数据上预测 RainTomorrow 目标变量,并将概率值大于(>0.5)的观测值转换为 Yes,否则为 No

    print("Training data results -")
    pred_train <-factor(ifelse(predict(model,
                               newdata=train, type="response")> 0.5,"Yes","No"))
    
  3. 为训练数据创建混淆矩阵并打印结果:

    train_metrics <- confusionMatrix(pred_train,  
                                         train$RainTomorrow,positive="Yes")
    print(train_metrics)
    

    输出如下:

    [1] "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  58233  8850
           Yes  3258  8706
    
                   Accuracy : 0.8468          
                     95% CI : (0.8443, 0.8493)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4998          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4959          
                Specificity : 0.9470          
             Pos Pred Value : 0.7277          
             Neg Pred Value : 0.8681          
                 Prevalence : 0.2221          
             Detection Rate : 0.1101          
       Detection Prevalence : 0.1514          
          Balanced Accuracy : 0.7215          
    
           'Positive' Class : Yes             
    
  4. 与第二步类似,在测试数据上预测结果:

    print("Test data results -")
    pred_test <-factor(ifelse(predict(model,newdata=test,type = "response") > 0.5,"Yes","No"))
    
  5. 为测试数据预测创建混淆矩阵并打印结果:

    test_metrics <- confusionMatrix(pred_test, 
                                    test$RainTomorrow,positive="Yes")
    print(test_metrics)
    

    输出如下:

    [1] "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25066  3754
           Yes  1349  3709
    
                   Accuracy : 0.8494          
                     95% CI : (0.8455, 0.8532)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5042          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4970          
                Specificity : 0.9489          
             Pos Pred Value : 0.7333          
             Neg Pred Value : 0.8697          
                 Prevalence : 0.2203          
             Detection Rate : 0.1095          
       Detection Prevalence : 0.1493          
          Balanced Accuracy : 0.7230          
    
           'Positive' Class : Yes    
    

我们首先加载必要的 caret 库,该库将提供计算所需指标的功能,如前所述。然后我们使用 R 中的 predict 函数,使用先前拟合的模型在训练数据以及测试数据(分别)上预测结果。predict 函数对于逻辑回归默认返回 link 函数的值。使用 type= 'response' 参数,我们可以覆盖该函数以返回目标的概率。为了简单起见,我们在预测中使用 0.5 作为阈值。因此,任何大于 0.5 的值都会被 confusionMatrix 函数从 caret 库提供给我们一个简单的方式来构建混淆矩阵并计算详尽的指标列表。我们需要将实际标签以及预测标签传递给该函数。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/2Q6mYW0。

目标标签的分布不平衡:77% 为无,23% 为有。在这种情况下,我们不能仅仅依靠整体准确率作为评估模型性能的指标。此外,与之前章节中所示的混淆矩阵相比,如图 3 和 5 的输出所示,混淆矩阵是倒置的。我们有预测作为行,实际值作为列。然而,解释和结果将保持不变。下一组输出报告了感兴趣的指标,以及我们尚未探索的一些其他指标。我们已经涵盖了最重要的指标(敏感性、精确度,即阳性预测值);然而,建议探索剩余的指标,例如阴性预测值、患病率和检测率。我们可以看到,我们得到的精确度约为 73%,召回率约为 *50%,整体准确率为 85%。在训练和测试数据集上,结果相似;因此,我们可以得出结论,该模型没有过拟合。

注意

总体而言,结果还不错。请不要对低召回率感到惊讶;在我们有不平衡数据集的场景中,用于评估模型性能的指标是业务驱动的。

我们可以得出结论,每次有下雨的可能性时,我们至少可以正确预测一半的时间,并且每次我们预测时,我们都是 73% 正确的。从业务角度来看,如果我们试图考虑是否应该努力提高召回率或精确度,我们需要估计误分类的成本。

在我们的用例中,每当预测到第二天有降雨时,运营管理团队就会准备更多的代理人数以更快地交付。由于没有现成的技术来应对与降雨相关的问题,即使我们只有 50%的召回率,我们也有机会覆盖。在这个问题中,由于错误预测降雨的成本对业务来说会更昂贵,也就是说,如果预测降雨的机会,团队将投资于更多的代理人数以交付,这会带来额外的成本。因此,我们希望有更高的精确度,同时我们可以接受在召回率上的妥协。

注意

理想的情况是具有较高的精确度和召回率。然而,在两者之间总是存在权衡。在大多数现实生活中的机器学习用例中,由业务驱动的决策最终确定优先选择精确度或召回率。

练习 8评估逻辑回归模型中开发的先前模型仅使用了df_new数据集中可用的一些变量。让我们构建一个包含数据集中所有可用变量的改进模型,并检查在测试数据集上的性能。

为了迭代改进模型,最佳方式是进行特征选择和超参数调整。特征选择涉及通过各种验证方法从可用列表中选择最佳特征集,并最终确定一个具有最佳性能和最少特征数量的模型。超参数调整涉及构建泛化模型,这些模型不会过拟合,即模型在训练和未见过的测试数据上都能表现良好。这些主题将在第六章特征选择和降维第七章模型改进中详细讨论。现在,本章的范围将仅限于演示模型评估。我们将在后续章节中涉及相同的用例进行超参数调整和特征选择。

练习 71:使用我们用例中所有可用独立变量开发逻辑回归模型

在先前的练习中,我们限制了独立变量的数量,只限于几个。在这个例子中,我们将使用df_new数据集中所有可用的独立变量来创建一个改进的模型。我们再次使用训练数据集来拟合模型,并使用测试数据集来评估模型性能。

执行以下步骤以构建一个包含用例中所有可用独立变量的逻辑回归模型:

  1. 使用所有可用独立变量拟合逻辑回归模型:

    model <- glm(RainTomorrow~., data=train ,family=binomial(link='logit'))
    
  2. 在训练数据集上预测:

    print("Training data results-")
    pred_train <-factor(ifelse(predict(model,newdata=train,type = "response") >= 0.5,"Yes","No"))
    
  3. 创建混淆矩阵:

    train_metrics <- confusionMatrix(pred_train, train$RainTomorrow,positive="Yes")
    print(train_metrics)
    

    输出如下:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  58189  8623
           Yes  3302  8933
    
                   Accuracy : 0.8491          
                     95% CI : (0.8466, 0.8516)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5104          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5088          
                Specificity : 0.9463          
             Pos Pred Value : 0.7301          
             Neg Pred Value : 0.8709          
                 Prevalence : 0.2221          
             Detection Rate : 0.1130          
       Detection Prevalence : 0.1548          
          Balanced Accuracy : 0.7276          
    
           'Positive' Class : Yes             
    
  4. 在测试数据上预测结果:

    print("Test data results -")
    pred_test <-factor(ifelse(predict(model,newdata=test,type = "response") > 0.5,"Yes","No"))
    
  5. 创建混淆矩阵:

    test_metrics <- confusionMatrix(pred_test, test$RainTomorrow,positive="Yes")
    print(test_metrics)
    

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25057  3640
           Yes  1358  3823
    
                   Accuracy : 0.8525          
                     95% CI : (0.8486, 0.8562)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5176          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5123          
                Specificity : 0.9486          
             Pos Pred Value : 0.7379          
             Neg Pred Value : 0.8732          
                 Prevalence : 0.2203          
             Detection Rate : 0.1128          
       Detection Prevalence : 0.1529          
          Balanced Accuracy : 0.7304          
    
           'Positive' Class : Yes
    

我们利用数据集中的所有变量,使用glm函数创建逻辑回归模型。然后我们使用拟合的模型来预测训练集和测试集的结果;类似于之前的练习。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/2HgwjaU。

注意整体准确率、精确率和召回率有所提高(尽管幅度很小)。结果尚可,我们可以通过逻辑回归进一步迭代改进它们。现在,让我们探索一些其他分类技术并研究模型的性能。

注意

在这个练习中,我们没有打印模型的摘要统计信息,类似于第一个模型,只有几个变量。如果打印出来,结果将占用不到两页的章节内容。目前,我们将忽略这一点,因为我们不是在探索 R 报告的模型特征;相反,我们是从训练集和测试集上的准确率、精确率和召回率指标来评估模型的。

获取最佳模型的最理想方式是消除所有统计上不显著的变量,消除多重共线性,处理异常值数据等。鉴于本章的范围,现在我们将忽略所有这些步骤。

活动 8:构建具有额外特征的逻辑回归模型

练习 8评估逻辑回归模型中,我们构建了一个具有少量特征的简单模型,然后在练习 9使用我们用例中所有可用独立变量的逻辑回归模型开发中,我们使用了所有特征。在这个活动中,我们将构建一个逻辑回归模型,该模型具有我们可以使用简单数学变换生成的额外特征。添加额外的数值特征变换,如对数变换、平方和立方幂变换、平方根变换等,是一种良好的实践。

执行以下步骤以开发具有额外特征的逻辑回归模型:

  1. 为活动创建df_new数据集的副本到df_copy中,并选择任何三个数值特征(例如,MaxTempRainfallHumidity3pm)。

  2. 为每个选定的特征使用平方和立方幂以及平方根变换来生成新特征。

  3. df_copy数据集分成 70:30 的训练集和测试集。

  4. 使用新的训练数据拟合模型,在测试数据上评估它,最后比较结果。

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25057  3640
           Yes  1358  3823
    
                   Accuracy : 0.8525          
                     95% CI : (0.8486, 0.8562)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5176          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5123          
                Specificity : 0.9486          
             Pos Pred Value : 0.7379          
             Neg Pred Value : 0.8732          
                 Prevalence : 0.2203          
             Detection Rate : 0.1128          
       Detection Prevalence : 0.1529          
          Balanced Accuracy : 0.7304          
    
           'Positive' Class : Yes             
    

    注意

    你可以在第 451 页找到这个活动的解决方案。

决策树

与逻辑回归一样,还有一种流行的分类技术因其简单性和白盒特性而非常受欢迎。决策树是一个以树(倒置树)的形式表示的简单流程图。它从一个根节点开始,分支到几个节点,可以根据决策进行遍历,并以叶节点结束,其中确定 最终结果。决策树可以用于回归,也可以用于分类用例。机器学习中实现了多种决策树的变体。这里列出了几个流行的选择:

  • 迭代二分器 3ID3

  • ID3 的继承者C4.5

  • 分类和回归树CART

  • CHi-squared 自动交互检测器CHAID

  • 条件推理树C 树

前面的列表并不详尽。还有其他替代方案,它们在如何处理分类和数值变量、选择树中根节点和连续节点的方法、分支每个决策节点的规则等方面都有细微的差别。在本章中,我们将限制我们的探索范围到 CART 决策树,这是最广泛使用的。R 提供了一些包含 CART 算法实现的包。在我们深入研究实现之前,让我们在以下几节中探讨决策树的一些重要方面。

决策树是如何工作的?

决策树的每个变体都有略微不同的方法。总的来说,如果我们尝试简化通用决策树的伪代码,可以总结如下:

  1. 选择根节点(节点对应一个变量)。

  2. 将数据划分为组。

  3. 对于上一步中的每个组:

    创建一个决策节点或叶节点(基于分割标准)。

    重复执行,直到节点大小 <= 阈值或特征 = 空集。

不同形式的树实现之间的差异包括处理分类和数值变量的方式、选择树中根节点和连续节点的方法、分支每个决策节点的规则等。

下面的视觉图是一个示例决策树。根节点和决策节点是我们提供给算法的独立变量。叶节点表示最终结果,而根节点和中间决策节点有助于遍历数据到叶节点。决策树的简单性使其非常有效且易于解释。这有助于轻松识别预测任务中的规则。通常,许多研究和商业倡议利用决策树来设计一套简单的分类系统规则:

图 5.4:示例决策树

图 5.4:示例决策树

在一般意义上,给定一组依赖变量和多个独立变量,决策树算法计算一个指标,该指标表示依赖目标变量与所有独立变量之间的拟合优度。对于分类用例,熵和信息增益是 CART 决策树中常用的指标。对于该指标的最好拟合变量被选为根节点,下一个最佳拟合变量被用作决策节点,按拟合优度降序排列。节点根据定义的阈值终止为叶节点。树继续增长,直到耗尽决策节点的变量数量或达到预定义的节点数量阈值。

为了提高树性能并减少过拟合,一些策略,如限制树的深度或宽度,或为叶节点或决策节点添加额外的规则,有助于对预测进行泛化。

让我们使用 R 中的 CART 决策树实现相同的用例。CART 模型通过 R 中的rpart包提供。该算法由 Leo Breiman、Jerome Friedman、Richard Olshen 和 Charles Stone 在 1984 年开发,并在业界得到广泛应用。

练习 72:在 R 中创建决策树模型

在这个练习中,我们将使用与我们在 练习 9 中使用的相同数据和用例,在 R 中创建决策树模型,该用例是 使用我们用例中所有可用独立变量开发逻辑回归模型。我们将尝试研究决策树模型与逻辑回归模型在性能上是否存在任何差异。

在 R 中创建决策树模型的以下步骤:

  1. 使用以下命令导入rpartrpart.plot包:

    library(rpart)
    library(rpart.plot)
    
  2. 使用所有变量构建 CART 模型:

    tree_model <- rpart(RainTomorrow~.,data=train)
    
  3. 绘制成本参数:

    plotcp(tree_model)
    

    输出如下:

    图 5.5:决策树模型

    图 5.5:决策树模型
  4. 使用以下命令绘制树:

    rpart.plot(tree_model,uniform=TRUE, main="Predicting RainFall")
    

    输出如下:

    图 5.6:预测降雨

    图 5.6:预测降雨
  5. 在训练数据上做出预测:

    print("Training data results -")
    pred_train <- predict(tree_model,newdata = train,type = "class")
    confusionMatrix(pred_train, train$RainTomorrow,positive="Yes")
    

    输出如下:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  59667 11215
           Yes  1824  6341
    
                   Accuracy : 0.835           
                     95% CI : (0.8324, 0.8376)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4098          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.36119         
                Specificity : 0.97034         
             Pos Pred Value : 0.77661         
             Neg Pred Value : 0.84178         
                 Prevalence : 0.22210         
             Detection Rate : 0.08022         
       Detection Prevalence : 0.10329         
          Balanced Accuracy : 0.66576         
    
           'Positive' Class : Yes             
    
  6. 在测试数据上做出预测:

    print("Test data results -")
    pred_test <- predict(tree_model,newdata = test,type = "class")
    confusionMatrix(pred_test, test$RainTomorrow,positive="Yes")
    

    输出如下:

    [1] "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25634  4787
           Yes   781  2676
    
                   Accuracy : 0.8356          
                     95% CI : (0.8317, 0.8396)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4075          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.35857         
                Specificity : 0.97043         
             Pos Pred Value : 0.77408         
             Neg Pred Value : 0.84264         
                 Prevalence : 0.22029         
             Detection Rate : 0.07899         
       Detection Prevalence : 0.10204         
          Balanced Accuracy : 0.66450         
    
           'Positive' Class : Yes
    

rpart库为我们提供了决策树的 CART 实现。还有其他库帮助我们可视化 R 中的决策树。我们在这里使用了rpart.plot。如果包尚未安装,请使用install.packages命令安装。我们使用rpart函数创建树模型,并使用所有可用的独立变量。然后我们使用plotcp函数可视化复杂度参数对应的不同迭代中的验证误差。我们还使用plot.rpart函数绘制决策树。

最后,我们在训练集和测试集上做出预测,并使用confusionMatrix函数分别对训练集和测试集计算感兴趣的指标,构建混淆矩阵。

注意

您可以在 GitHub 上找到完整的代码:http://bit.ly/2WECLgZ。

R 中实现的 CART 决策树已经内置了一些优化。默认情况下,该函数设置了许多参数以获得最佳结果。在决策树中,有几个参数我们可以手动设置,以根据我们的需求调整性能。然而,R 实现通过默认值设置大量参数,并取得了相对较好的效果。这些额外的设置可以通过control参数添加到rpart树中。

我们可以将以下参数添加到树模型中:

control = rpart.control(
    minsplit = 20, 
    minbucket = round(minsplit/3), 
    cp = 0.01, 
    maxcompete = 4, 
    maxsurrogate = 5, 
    usesurrogate = 2, xval = 10, 
    surrogatestyle = 0, 
    maxdepth = 30
)

一个有趣的参数是0.01。我们可以将其进一步更改为一个更小的数字,这将使树生长得更深,变得更加复杂。plotcp函数可视化了不同cp值(即复杂度参数)的相对验证误差。在图 5.4中,最理想的cp值是位于虚线以下的最左侧值。在这种情况下(如图所示),最佳值是 0.017。由于这个值与默认值相差不大,我们没有进一步更改它。

下一个图表(图 5.5)帮助我们可视化算法构建的实际决策树。我们可以看到正在使用可用数据构建的简单规则集。正如您所看到的,只有两个独立变量,即Humidity3pmWindGustSpeed,被选入树中。如果我们将复杂度参数从0.01改为0.001,我们可以看到一个更深层次的树(这可能导致模型过拟合)将被构建。最后,我们可以看到混淆矩阵(步骤 6)的结果,以及训练集和测试集的感兴趣的其他指标。

我们可以看到,训练集和测试集的结果相似。因此,我们可以得出结论,该模型没有过拟合。然而,准确率(83%)和召回率(35%)有显著下降,而精确率则增加到略高的值(77%)。

我们现在已经与几种白盒建模技术合作过。鉴于白盒模型的简单性和易于解释,它们是商业中分类用例中最受欢迎的技术,在这些用例中,推理和驱动分析至关重要。然而,在某些情况下,企业可能对模型的净结果更感兴趣,而不是对结果的整个解释。在这种情况下,最终模型的表现更为重要。在我们的用例中,我们希望实现高精度。让我们探索一些在模型性能上优于(在大多数情况下)白盒模型,并且可以用更少的努力和更多的训练数据实现的黑盒模型。

活动 9:创建具有额外控制参数的决策树模型

我们在练习 10在 R 中创建决策树模型中创建的决策树模型使用了树的默认控制参数。在这个活动中,我们将覆盖一些控制参数,并研究其对整体树拟合过程的影响。

执行以下步骤以创建具有额外控制参数的决策树模型:

  1. 加载rpart库。

  2. 为决策树创建控制对象,并使用新值:minsplit =15cp = 0.00

  3. 使用训练数据拟合树模型,并将控制对象传递给rpart函数。

  4. 绘制复杂性参数图,以查看树在不同CP值下的表现。

  5. 使用拟合的模型对训练数据进行预测,并创建混淆矩阵。

  6. 使用拟合的模型对测试数据进行预测,并创建混淆矩阵。

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25068  3926
           Yes  1347  3537
    
                   Accuracy : 0.8444          
                     95% CI : (0.8404, 0.8482)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4828          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4739          
                Specificity : 0.9490          
             Pos Pred Value : 0.7242          
             Neg Pred Value : 0.8646          
                 Prevalence : 0.2203          
             Detection Rate : 0.1044          
       Detection Prevalence : 0.1442          
          Balanced Accuracy : 0.7115          
    
           'Positive' Class : Yes 
    

    注意

    你可以在第 454 页找到这个活动的解决方案。

集成建模

当需要在大规模训练样本中提高性能时,集成建模是分类和回归建模技术中最常用的方法之一。简单来说,集成建模可以通过分解其名称为个别术语来定义:集成建模。我们已经在本书中研究了建模;简单来说,集成就是一个。因此,为同一任务构建多个模型而不是一个模型,然后将结果通过任何方式(如平均或投票)结合成一个单一结果的过程,称为集成建模

我们可以构建任何模型的集成,例如线性模型或树模型,实际上甚至可以构建集成模型的集成。然而,最流行的方法是使用树模型作为集成的基座。集成模型主要有两种类型:

  • Bagging: 在这里,每个模型都是并行构建的,并在每个模型内部引入了一些随机化,所有模型的结果通过简单的投票机制进行组合。比如说我们构建了 100 个树模型,其中 60 个模型预测结果为,40 个模型预测为。最终结果将是

  • Boosting: 在这里,模型是按顺序构建的,第一个模型的结果被用来调整下一个模型。每个模型迭代地从先前模型犯的错误中学习,并试图在连续迭代中改进。结果是所有单个结果的加权平均值。

在 Bagging 和 Boosting 中都有几种实现方式。Bagging本身是 R 中可用的一种集成模型。迄今为止,最流行的 Bagging 技术是随机森林。与随机森林类似,另一种 Bagging 技术是额外树。同样,一些 Boosting 技术的例子包括 AdaBoost、随机梯度提升、BrownBoost 等。然而,最流行的 Boosting 技术是XGBoost,它源自名称EXtreme Gradient Boosting。在大多数情况下,对于分类以及回归用例,数据科学家更喜欢使用随机森林或 XGBoost 模型。Kaggle(一个在线数据科学社区)的一项最近调查显示,在大多数机器学习竞赛中最常用的技术总是随机森林和 XGBoost。在本章中,我们将更深入地研究这两种模型。

随机森林

随机森林是机器学习中使用的最流行的 Bagging 技术。它是由 CART 的作者 Leo Brieman 开发的。这种简单技术非常有效,以至于在给定的监督用例中,数据科学家几乎总是首先选择算法。随机森林是分类和回归用例的良好选择。它是一种高度有效的方法,可以以最小的努力减少过拟合。让我们更深入地了解随机森林是如何工作的。

如我们所知,随机森林是一种集成建模技术,其中我们构建了多个模型,并使用简单的投票技术结合它们的结果。在随机森林中,我们使用决策树作为基础模型。算法的内部工作原理可以从其名称本身推测出来,即随机(因为它在构建的每个模型中引入了一层随机化)和森林(因为我们要构建多个模型)。在我们深入了解算法的实际工作原理之前,我们首先需要了解其前身Bagging的故事,并研究为什么我们需要集成。

为什么使用集成模型?

在你的脑海中浮现的第一个问题可能就是,为什么我们一开始就需要为同一个任务构建多个模型?这是必要的吗?嗯,是的!当我们构建集成模型时,我们并不是多次构建完全相同的模型;相反,我们构建的每个模型都会以某种方式与其他模型不同。这个背后的直觉可以通过我们日常生活中的一个简单例子来理解。它是基于这样一个原则:将几个弱学习器结合起来可以构建一个更强、更健壮的模型。

让我们用一个简单的例子来理解这个想法。假设你到达一个新城市,想知道第二天这个城市下雨的概率。假设技术不是一个可用的选项,你能找到的最简单的方法就是询问在这个地方居住了一段时间的邻居。也许答案并不总是正确的;如果有人说第二天有很大的下雨概率,这并不一定意味着一定会下雨。因此,为了做出改进的猜测,你会询问几个邻居。现在,如果你询问的 10 个人中有 7 个人提到第二天有很高的下雨概率,那么第二天几乎肯定会下雨。这个方法之所以有效,是因为你接触到的每个人都会对降雨模式有一定的了解,而且每个人的了解也会有所不同。尽管这些差异并不大,但当这些了解汇总成一个集体答案时,就会产生更好的答案。

Bagging – 随机森林的前身

集成建模遵循相同的原理。在这里,每个模型中都会引入一定程度的随机性。Bagging 算法为每个模型在训练数据上引入这种随机性。Bagging 这个名字来源于自助聚合;这是一个过程,我们用替换数据从可用的数据中抽取三分之二的数据用于训练,其余的用于测试和验证。在这里,每个模型,即决策树模型,在略微不同的数据集上训练,因此对于相同的测试样本可能会有略微不同的结果。Bagging 在某种程度上模仿了我们讨论的现实世界例子,因此将几个弱学习器(决策树模型)组合成一个强学习器。

随机森林是如何工作的?

随机森林基本上是 bagging 的继承者。在这里,除了训练数据的随机性外,随机森林还通过特征集添加了一个额外的随机层。因此,每个决策树不仅具有自助聚合,即三分之二的训练数据有放回地替换,而且还从可用列表中随机选择特征的一个子集。因此,集成中的每个单个决策树都有略微不同的训练数据集和略微不同的特征集进行训练。这额外的随机层在泛化模型方面非常有效,并减少了方差。

练习 73:在 R 中构建随机森林模型

在这个练习中,我们将在练习 8、9 和 10 中使用的相同数据集上构建一个随机森林模型。我们将利用集成建模,并测试整体模型性能是否优于决策树和逻辑回归。

注意

要开始,我们可以快速使用之前使用的相同数据集构建一个随机森林模型。R 中的 randomForest 包提供了模型的实现,以及一些额外的函数来优化模型。

让我们看看一个基本的随机森林模型。执行以下步骤:

  1. 首先,使用以下命令导入 randomForest 库:

    library(randomForest)
    
  2. 使用所有可用的独立特征构建一个随机森林模型:

    rf_model <- randomForest(RainTomorrow ~ . , data = train, ntree = 100,                                             importance = TRUE, 
                                                maxnodes=60)
    
  3. 在训练数据上评估:

    print("Training data results -")
    pred_train <- predict(rf_model,newdata = train,type = "class")
    confusionMatrix(pred_train, train$RainTomorrow,positive="Yes")
    
  4. 在测试数据上评估:

    print("Test data results -")
    pred_test <- predict(rf_model,newdata = test,type = "class")
    confusionMatrix(pred_test, test$RainTomorrow,positive="Yes")
    
  5. 绘制特征重要性:

    varImpPlot(rf_model)
    

    输出如下:

    [1] "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  59630 10133
           Yes  1861  7423
    
                   Accuracy : 0.8483          
                     95% CI : (0.8457, 0.8508)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.472           
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.42282         
                Specificity : 0.96974         
             Pos Pred Value : 0.79955         
             Neg Pred Value : 0.85475         
                 Prevalence : 0.22210         
             Detection Rate : 0.09391         
       Detection Prevalence : 0.11745         
          Balanced Accuracy : 0.69628         
    
           'Positive' Class : Yes             
    
    [1] "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25602  4369
           Yes   813  3094
    
                   Accuracy : 0.847           
                     95% CI : (0.8432, 0.8509)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4629          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.41458         
                Specificity : 0.96922         
             Pos Pred Value : 0.79191         
             Neg Pred Value : 0.85423         
                 Prevalence : 0.22029         
             Detection Rate : 0.09133         
       Detection Prevalence : 0.11533         
          Balanced Accuracy : 0.69190         
    
           'Positive' Class : Yes             
    
    

图 5.7:随机森林模型

图 5.7:随机森林模型

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/2Q2xKwd。

活动 10:构建具有更多树的随机森林模型

练习 11在 R 中构建随机森林模型 中,我们创建了一个只有 100 树的随机森林模型;我们可以构建一个具有更多树的更健壮的模型。在这个活动中,我们将创建一个拥有 500 树的随机森林模型,并研究模型只有 100 树的影响。一般来说,我们预计模型的性能会提高(至少随着树的数量增加而略有提高)。这伴随着模型收敛所需更高的计算时间。

执行以下步骤以构建一个拥有 500 树的随机森林模型:

  1. 开发一个具有更多树的随机森林模型;比如说,500 树。鼓励读者尝试更高的数字,如 1,000、2,000 等,并研究每个版本的增量改进。

  2. 利用拟合的模型在训练和测试数据上预测估计值,并研究与拥有 100 树的模型相比是否有任何改进。

    注意

    你可以在第 457 页找到这个活动的解决方案。

XGBoost

XGBoost 是近年来最受欢迎的增强技术。尽管有大型公司开发了各种新版本,但 XGBoost 仍然稳居王座。让我们简要回顾一下增强技术的历史。

提升过程是如何工作的?

提升法与装袋法在核心原则上有区别;学习过程实际上是顺序的。在集成中构建的每个模型理想上都是前一个模型的改进版本。用简单的话来说,想象你正在玩一个游戏,你必须记住你只被展示了一次、持续 30 秒的所有放在桌子上的物品。游戏的主持人将大约 50-100 种不同的物品放在桌子上,如蝙蝠、球、钟、骰子、硬币等等,然后用一块大布覆盖它们。当游戏开始时,他从桌子上取下布,给你 30 秒的时间看它们,然后再次拉上布。你现在必须回忆起你能记住的所有物品。能回忆起最多物品的参与者赢得了游戏。

在这个游戏中,让我们增加一个新维度。假设你是一个团队,玩家们一个接一个地轮流宣布他们能回忆起的所有物品,而其他人则聆听。假设有 10 名参与者;每个参与者走上前来,大声宣布他们从桌子上能回忆起的物品。当第二个玩家走上前来时,他们已经听到了第一个玩家宣布的所有物品。他们可能会提到一些第二个玩家可能没有回忆起的物品。为了改进第一个玩家的表现,第二个玩家从第一个玩家那里学习了一些新的物品,将它们添加到自己的列表中,然后大声宣布。当最后一名玩家走上前来时,他们已经学习了其他玩家回忆起的几个物品,而这些物品他们自己未能回忆起来。

将这些放在一起,那个玩家创建了最详尽的列表,并赢得了比赛。每个玩家依次宣布列表的事实有助于下一个玩家从他们的错误中学习并改进。

提升法以相同的方式工作。每个按顺序训练的模型都获得了额外的知识,使得第一个模型的错误在第二个模型中得到了更好的学习。比如说,第一个模型学会了在特定独立变量的大多数情况下进行良好的分类;然而,它未能正确预测一个特定的类别。下一个模型被赋予了不同的训练样本,使得模型在先前模型失败的类别中学习得更好。一个简单的例子是基于感兴趣变量或类别的过采样。提升法有效地减少了偏差,因此提高了模型的表现。

有哪些流行的提升技术?

之前介绍的提升技术并不太受欢迎,因为它们很容易过拟合,并且通常需要相对较多的努力来调整以达到出色的性能。AdaBoost、BrownBoost、梯度提升和随机梯度提升都是长期流行的提升技术。然而,在 2014 年,当 T Chen 和其他人引入 XGBoost(极端梯度提升)时,它为提升性能带来了新的高度。

XGBoost 是如何工作的?

XGBoost 原生引入了正则化,这有助于模型对抗过拟合,从而实现了高性能。与其他当时可用的提升技术相比,XGBoost 显著减少了过拟合问题,并且所需的工作量最少。在 R 或任何其他语言的当前模型实现中,XGBoost 几乎总是使用默认参数设置表现出色。(尽管,这并不总是正确的;在许多情况下,随机森林的表现优于 XGBoost)。XGBoost 一直是数据科学黑客马拉松和企业项目中使用的最流行的算法之一。

简而言之,XGBoost 在目标函数中引入了正则化,当模型在训练迭代中变得更加复杂时,会对模型进行惩罚。讨论 XGBoost 中涉及的数学结构的深度超出了本章的范围。您可以参考 T Chen 的论文以获取更多信息(https://arxiv.org/abs/1603.02754)。此外,这篇博客以简单的方式帮助您理解 GBM 和 XGBoost 之间的数学差异:https://towardsdatascience.com/boosting-algorithm-xgboost-4d9ec0207d。

在 R 中实现 XGBoost

我们可以利用 XGBoost 包,它提供了算法的整洁实现。在开始之前,我们需要注意一些实现方法上的差异。与其他 R 中算法的实现不同,XGBoost 不处理分类数据(其他实现会将其内部转换为数值数据)。XGBoost 在 R 中的内部工作方式不处理将分类列自动转换为数值列。因此,我们手动将分类列转换为数值或独热编码形式。

独热编码形式基本上是将单个分类列表示为二进制编码形式。比如说我们有一个包含//可能等值的分类列;然后,我们将这个单一变量转换为,其中我们为分类变量的每个值都有一个单独的变量,表示其值为01。因此,可能的值将根据原始值取01

独热编码在以下表格中演示:

图 5.8:独热编码

图 5.8:独热编码

让我们将数据转换为所需的形式,并在数据集上构建一个 XGBoost 模型。

练习 74:在 R 中构建 XGBoost 模型

正如我们在练习 11在 R 中构建随机森林模型中做的那样,我们将尝试通过为与练习 11在 R 中构建随机森林模型相同的用例和数据集构建 XGBoost 模型来提高分类模型的性能。

执行以下步骤以在 R 中构建 XGBoost 模型。

  1. 为目标、分类和数值变量创建列表占位符:

    target<- "RainTomorrow"
    categorical_columns <- c("RainToday","WindGustDir","WindDir9am",
    "WindDir3pm", "new_location")
    numeric_columns <- setdiff(colnames(train),c(categorical_columns,target))
    
  2. 将分类因子变量转换为字符。这将在将它们转换为独热编码形式时很有用:

    df_new <- df_new %>% mutate_if(is.factor, as.character)
    
  3. 使用caret包中的dummyVars函数将分类变量转换为独热编码形式:

    dummies <- dummyVars(~ RainToday + WindGustDir + WindDir9am + 
                         WindDir3pm + new_location, data = df_new)
    df_all_ohe <- as.data.frame(predict(dummies, newdata = df_new))
    
  4. 将第三步中的数值变量和独热编码变量合并到一个名为df_final的单个 DataFrame 中:

    df_final <- cbind(df_new[,numeric_columns],df_all_ohe)
    
  5. 将目标变量转换为数值形式,因为 R 中的 XGBoost 实现不接受因子或字符形式:

    y <- ifelse(df_new[,target] == "Yes",1,0)
    
  6. df_final数据集分为训练集(70%)和测试集(30%):

    set.seed(2019)
    train_index <- sample(seq_len(nrow(df_final)),floor(0.7 * nrow(df_final)))
    xgb.train <- df_final[train_index,]
    y_train<- y[train_index]
    xgb.test <- df_final[-train_index,]
    y_test <- y[-train_index]
    
  7. 使用xgboost函数构建 XGBoost 模型。传递训练数据和y_train目标变量,并定义eta = 0.01max_depth = 6nrounds = 200colsample_bytree = 1超参数,定义评估指标为logloss,目标函数为binary:logistic,因为我们处理的是二元分类:

    xgb <- xgboost(data = data.matrix(xgb.train), 
                   label = y_train, 
                   eta = 0.01,
                   max_depth = 6, 
                   nround=200, 
                   subsample = 1,
                   colsample_bytree = 1,
                   seed = 1,
                   eval_metric = "logloss",
                   objective = "binary:logistic",
                   nthread = 4
    )
    
  8. 使用训练数据集上的拟合模型进行预测,并创建混淆矩阵以评估模型在训练数据上的性能:

    print("Training data results -")
    pred_train <- factor(ifelse(predict(xgb,data.matrix(xgb.train),type="class")>0.5,1,0))
    confusionMatrix(pred_train,factor(y_train),positive='1')
    

    输出如下:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 58967  8886
             1  2524  8670
    
                   Accuracy : 0.8557          
                     95% CI : (0.8532, 0.8581)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5201          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4938          
                Specificity : 0.9590          
             Pos Pred Value : 0.7745          
             Neg Pred Value : 0.8690          
                 Prevalence : 0.2221          
             Detection Rate : 0.1097          
       Detection Prevalence : 0.1416          
          Balanced Accuracy : 0.7264          
    
           'Positive' Class : 1               
    
  9. 现在,就像在之前的步骤中一样,使用拟合模型在测试数据集上进行预测,并创建混淆矩阵以评估模型在测试数据上的性能:

    print("Test data results -")
    pred_test <- factor(ifelse(predict(xgb,data.matrix(xgb.test),
    type="class")>0.5,1,0))
    confusionMatrix(pred_test,factor(y_test),positive='1')
    

    输出如下:

    [1] "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 25261  3884
             1  1154  3579
    
                   Accuracy : 0.8513          
                     95% CI : (0.8475, 0.8551)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5017          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4796          
                Specificity : 0.9563          
             Pos Pred Value : 0.7562          
             Neg Pred Value : 0.8667          
                 Prevalence : 0.2203          
             Detection Rate : 0.1056          
       Detection Prevalence : 0.1397          
          Balanced Accuracy : 0.7179          
    
           'Positive' Class : 1
    

如果我们仔细观察模型的结果,我们可以看到与随机森林模型结果相比,性能略有提升。从0.5提升到0.54,我们可以在保持比随机森林略高的召回率的同时提高精确度(以匹配随机森林)。XGBoost 的召回率提升幅度远大于精确度的下降。概率截止值的阈值不是一个固定的、硬性的截止值。我们可以根据我们的用例调整阈值。最佳数值可以通过经验实验或研究敏感性、特异性分布来研究。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/30gzSW0。

以下练习使用 0.54 而不是 0.5 作为概率截止值,以研究在牺牲召回率的情况下精确度的提升。

练习 75:提高 XGBoost 模型性能

我们可以通过调整输出的阈值来调整二元分类模型的性能。默认情况下,我们选择 0.5 作为默认概率截止值。因此,所有高于 0.5 的响应都被标记为Yes,否则为No。调整阈值可以帮助我们实现更敏感或更精确的模型。

通过调整概率截止阈值来提高 XGBoost 模型的性能,执行以下步骤:

  1. 将训练数据集上预测的概率截止阈值从 0.5 提高到 0.53 并打印结果:

    print("Training data results -")
    pred_train <- factor(ifelse(predict(xgb,data.matrix(xgb.train),
    type="class")>0.53,1,0))
    confusionMatrix(pred_train,factor(y_train),positive='1')
    

    输出如下:

    [1] "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 59626  9635
             1  1865  7921
    
                   Accuracy : 0.8545        
                     95% CI : (0.852, 0.857)
        No Information Rate : 0.7779        
        P-Value [Acc > NIR] : < 2.2e-16     
    
                      Kappa : 0.4999        
     Mcnemar's Test P-Value : < 2.2e-16     
    
                Sensitivity : 0.4512        
                Specificity : 0.9697        
             Pos Pred Value : 0.8094        
             Neg Pred Value : 0.8609        
                 Prevalence : 0.2221        
             Detection Rate : 0.1002        
       Detection Prevalence : 0.1238        
          Balanced Accuracy : 0.7104        
    
           'Positive' Class : 1             
    
  2. 将测试数据集上预测的概率截止阈值从 0.5 提高到 0.53 并打印结果:

    print("Test data results -")
    pred_test <- factor(ifelse(predict(xgb,data.matrix(xgb.test),
    type="class")>0.53,1,0))
    confusionMatrix(pred_test,factor(y_test),positive='1')
    

    输出如下:

    1] "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 25551  4210
             1   864  3253
    
                   Accuracy : 0.8502         
                     95% CI : (0.8464, 0.854)
        No Information Rate : 0.7797         
        P-Value [Acc > NIR] : < 2.2e-16      
    
                      Kappa : 0.4804         
     Mcnemar's Test P-Value : < 2.2e-16      
    
                Sensitivity : 0.43588        
                Specificity : 0.96729        
             Pos Pred Value : 0.79014        
             Neg Pred Value : 0.85854        
                 Prevalence : 0.22029        
             Detection Rate : 0.09602        
       Detection Prevalence : 0.12152        
          Balanced Accuracy : 0.70159        
    
           'Positive' Class : 1  
    

我们可以看到,在 44% 的召回率下,我们在测试数据集上达到了 80% 的精确率,训练集和测试集之间的性能差异也可以忽略不计。因此,我们可以得出结论,XGBoost 的模型性能略优于随机森林,尽管只是略好。

注意

你可以在 GitHub 上找到完整的代码:http://bit.ly/30c5DQ9。

在结束本章之前,让我们通过实验最后一种用于分类的监督技术,即深度神经网络。

深度神经网络

在结束本章之前,我们将讨论最后一种技术,即深度神经网络或深度学习。这是一个漫长且复杂的话题,我们绝对无法在章节的简短部分中公正地处理它。一本书可能甚至不足以覆盖这个话题的表面!我们将从高处探讨这个话题,并快速研究 R 中的简单实现。

深度神经网络主要应用于计算机视觉和自然语言处理领域,在机器学习的回归和分类用例中也具有重要意义,特别是在表格横截面数据上。随着大量数据的出现,深度神经网络已被证明在学习潜在模式并因此训练出性能更好的模型方面非常有效。

深度神经网络的深入探讨

深度神经网络受到人脑神经结构的启发。深度学习领域因解决计算机视觉问题而变得流行,即那些人类容易解决但计算机长期难以解决的问题领域。设计类似微型且高度简化的深度神经网络,旨在解决人类特别容易解决的问题。后来,随着深度学习在计算机视觉领域的成功,它被应用于其他几个领域,包括传统的机器学习监督用例。

神经网络组织成一个神经元层次结构,就像人脑中的神经元一样。每个神经元都与其他神经元相连,这使它们之间能够通过信号进行通信,形成一个可以学习并具有反馈机制的复杂网络。

下图展示了一个简单的神经网络:

图 5.9:简单的神经网络

图 5.9:简单的神经网络

输入数据构成了网络的第 0 层。这一层然后连接到下一层的神经元,下一层是隐藏的。它被称为隐藏的,因为网络可以被看作是一个黑盒,我们向网络提供输入并直接看到输出。中间层是隐藏的。在神经网络中,一层可以有任意数量的神经元,每个网络可以有任意数量的层。层的数量越多,网络就越“深”。因此得名深度学习和深度神经网络。每个隐藏层中的每个神经元都计算一个数学函数,在深度学习中这被称为激活函数。这个函数有助于模拟两个神经元之间的信号。如果函数(激活)计算出的值大于阈值,它就会向下一层直接连接的神经元发送信号。这两个神经元之间的连接由一个权重调节。权重决定了传入神经元的信号对接收神经元的重要性。深度学习模型中的学习方法通过更新神经元之间的权重,使得最终的预测,类似于机器学习模型,是最准确的。

深度学习模型是如何工作的?

要理解神经网络是如何工作以及如何学习在数据上做出预测的,让我们考虑一个对人类来说相对非常简单的任务。考虑通过人脸识别不同人的任务。我们大多数人每天都会遇到几个不同的人;比如说,在工作、学校或街道上。我们遇到的每个人在某些维度上都是不同的。尽管每个人都会有大量的相似特征,比如两只眼睛、两只耳朵、嘴唇、两只手等等,但我们的大脑可以轻易地区分两个人。当我们第二次遇到某个人时,我们很可能会认出他们,并将他们识别为我们之前遇到过的人。考虑到这种情况发生的规模以及我们的大脑能够轻松地解决这个巨大问题的现实,这让我们不禁想知道这究竟是如何发生的。

为了理解这一点并欣赏我们大脑的美丽,我们需要了解大脑是如何从根本上学习的。大脑是一个由相互连接的神经元组成的大型、复杂结构。当神经元感知到某些重要事物时,它会被激活,并将消息或信号传递给它连接的其他神经元。神经元之间的连接通过从反馈中不断学习而得到加强。在这里,当我们看到一张新面孔时,我们的大脑不是学习面孔的结构来识别人,而是学习给定的面孔与通用基准面孔的不同之处。这可以进一步简化为计算重要面部特征(如眼睛形状、鼻子、嘴唇、耳朵和唇部结构)之间的差异,以及皮肤和头发的颜色偏差和其他属性。这些差异由不同的神经元量化,然后以系统化的方式编排,以便大脑能够区分不同的面孔并从记忆中回忆起面孔。整个计算都是在潜意识中发生的,我们几乎意识不到这一点,因为结果对我们来说是一瞬间就能注意到的。

神经网络本质上试图以极其简化的形式模仿大脑的学习功能。神经元以分层的方式相互连接,并使用随机权重进行初始化。网络中的数学计算结合了所有神经元的输入,最终达到最终结果。最终结果的偏差(预测值)被量化为误差,并作为反馈提供给网络。基于误差,网络试图更新连接的权重,并尝试迭代地减少预测中的误差。经过几次迭代,网络以有序的方式更新其权重,从而学会识别模式以做出正确的预测。

我们使用什么框架进行深度学习模型?

目前,我们将使用 Keras for R 进行深度神经网络实验,以用于我们的分类用例。对于深度学习模型开发,我们需要编写大量的代码,这将构成网络的构建块。为了加快我们的进程,我们可以利用 Keras,这是一个提供深度学习组件整洁抽象的深度学习框架。Keras 有一个 R 接口,并且建立在低级深度学习框架之上。

今天的人工智能社区中可用的深度学习框架要么是低级的,要么是高级的。TensorFlow、Theano、PyTorch、PaddlePaddle 和 mxnet 等框架是低级框架,为深度学习模型提供基本构建块。使用低级框架为最终网络设计提供了大量的灵活性和定制性。然而,我们仍然需要编写相当多的代码才能使一个相对较大的网络工作。为了进一步简化,有一些高级框架可用,它们在低级框架之上工作,并在构建深度学习模型的过程中提供第二层抽象。Keras、Gluon 和 Lasagne 是一些利用上述低级框架作为后端并提供新 API 的框架,这使得整体开发过程变得容易得多。与直接使用 TensorFlow 等低级框架相比,这减少了灵活性,并为大多数网络提供了一个稳健的解决方案。对于我们的用例,我们可以直接利用 Keras 的 R 接口。

使用install.packages('keras')命令将安装 Keras 的 R 接口,并会自动安装 TensorFlow 作为 Keras 的低级后端。

在 Keras 中构建深度神经网络

要在 R 中利用 Keras,我们需要对我们的现有训练数据集进行额外的数据增强。在 R 中大多数机器学习函数中,我们可以直接将分类列编码为因子传递。然而,我们注意到 XGBoost 强制要求数据需要被转换成 one-hot 编码形式,因为它不会将数据内部转换为所需的格式。因此,我们使用了 R 中的dummyVars函数将训练和测试数据集转换为 one-hot 编码版本,这样我们数据集中就只有数值数据。在 Keras 中,我们需要将训练数据集作为矩阵而不是 DataFrame 来提供。因此,除了将数据转换为 one-hot 编码形式外,我们还需要将数据集转换为矩阵。

此外,还建议我们标准化、归一化或缩放所有输入维度。归一化过程将数据值重新缩放到 0 到 1 的范围。同样,标准化将数据缩放到均值为(μ)0 和标准差(σ)为 1(单位方差)。这种转换在机器学习中是一个很好的特性,因为某些算法从中受益并更好地学习。然而,在深度学习中,这种转换变得至关重要,因为如果我们提供一个所有维度都在不同范围或尺度上的输入训练数据集,模型的学习过程就会受到影响。这个问题背后的原因是神经元中使用的激活函数类型。

以下代码片段实现了 Keras 中的一个基本神经网络。在这里,我们使用一个具有三个层,每层有 250 个神经元的架构。找到正确的架构是一个经验过程,没有明确的指南。网络越深,拟合数据所需的计算就越多。以下代码片段中使用的数据集与 XGBoost 中使用的相同,并且已经有一元编码的形式。

练习 76:使用 R Keras 在 R 中构建深度神经网络

在这个练习中,我们将利用深度神经网络构建一个分类模型,用于与练习 13提高 XGBoost 模型性能相同的用例,并尝试提高性能。深度神经网络并不总是比集成模型表现更好。当我们有非常高的训练样本数量时,例如 1000 万,它们通常是一个更好的选择。然而,我们将进行实验并检查我们是否能实现比我们在练习 10-13 中构建的模型更好的性能。

按照以下步骤在 R 中构建深度神经网络。

  1. 将输入数据集缩放到 0 到 1 的范围内。我们首先需要在训练数据上初始化一个preProcess对象。这将随后用于缩放训练数据以及测试数据。神经网络在缩放数据上表现更好。仅使用训练数据来创建缩放对象:

    standardizer <- preProcess(x_train, method='range',rangeBounds=c(0,1))
    
  2. 使用之前步骤中创建的standardizer对象来缩放训练和测试数据:

    x_train_scaled <- predict(standardizer, newdata=x_train)
    x_test_scaled <- predict(standardizer, newdata=x_test)
    
  3. 将预测变量数量存储在一个名为predictors的变量中。我们将使用这些信息来构建网络:

    predictors <-  dim(x_train_scaled)[2]
    
  4. 定义深度神经网络的架构。我们将使用keras_model_sequential方法。我们将创建一个包含三个隐藏层,每个层有 250 个神经元,激活函数为relu的网络。输出层将有一个神经元,激活函数为sigmoid(因为我们正在开发一个二元分类模型):

    dl_model <-  keras_model_sequential()  %>% 
      layer_dense(units = 250, activation = 'relu', 
    input_shape =c(predictors)) %>% 
      layer_dense(units = 250, activation = 'relu' ) %>% 
      layer_dense(units = 250, activation = 'relu') %>% 
      layer_dense(units = 1, activation = 'sigmoid') 
    
  5. 定义模型优化器为adam,损失函数以及模型训练迭代中要捕获的指标:

    dl_model %>% compile(
      loss = 'binary_crossentropy',
      optimizer = optimizer_adam(),
      metrics = c('accuracy')
    )
    summary(dl_model)
    

    输出如下:

    _____________________________________________________________
    Layer (type)                 Output Shape               Param #
    =============================================================
    dense_34 (Dense)             (None, 250)                16750
    _____________________________________________________________
    dense_35 (Dense)             (None, 250)                62750
    _____________________________________________________________
    dense_36 (Dense)             (None, 250)                62750
    _____________________________________________________________
    dense_37 (Dense)             (None, 1)                  251  
    =============================================================
    Total params: 142,501
    Trainable params: 142,501
    Non-trainable params: 0
    
  6. 使用步骤 4-5 中创建的模型结构,以及步骤 1-2 中的训练和测试数据来拟合模型:

    history <- dl_model %>% fit(
      as.matrix(x_train_scaled), as.matrix(y_train), 
      epochs = 10, batch_size = 32, 
      validation_split = 0.2
    )
    

    输出如下:

    Train on 63237 samples, validate on 15810 samples
    Epoch 1/10
    63237/63237 [==============================] - 7s 104us/step – 
    loss: 0.3723 - acc: 0.8388 - val_loss: 0.3639 - val_acc: 0.8426
    Epoch 2/10
    63237/63237 [==============================] - 6s 102us/step – 
    loss: 0.3498 - acc: 0.8492 - val_loss: 0.3695 - val_acc: 0.8380
    Epoch 3/10
    63237/63237 [==============================] - 6s 97us/step – 
    loss: 0.3434 - acc: 0.8518 - val_loss: 0.3660 - val_acc: 0.8438
    Epoch 4/10
    63237/63237 [==============================] - 6s 99us/step – 
    loss: 0.3390 - acc: 0.8527 - val_loss: 0.3628 - val_acc: 0.8395
    Epoch 5/10
    63237/63237 [==============================] - 6s 97us/step – 
    loss: 0.3340 - acc: 0.8551 - val_loss: 0.3556 - val_acc: 0.8440
    Epoch 6/10
    63237/63237 [==============================] - 7s 119us/step – 
    loss: 0.3311 - acc: 0.8574 - val_loss: 0.3612 - val_acc: 0.8414
    Epoch 7/10
    63237/63237 [==============================] - 7s 107us/step – 
    loss: 0.3266 - acc: 0.8573 - val_loss: 0.3536 - val_acc: 0.8469
    Epoch 8/10
    63237/63237 [==============================] - 7s 105us/step – 
    loss: 0.3224 - acc: 0.8593 - val_loss: 0.3575 - val_acc: 0.8471
    Epoch 9/10
    63237/63237 [==============================] - 7s 105us/step – 
    loss: 0.3181 - acc: 0.8607 - val_loss: 0.3755 - val_acc: 0.8444
    Epoch 10/10
    63237/63237 [==============================] - 7s 104us/step – 
    loss: 0.3133 - acc: 0.8631 - val_loss: 0.3601 - val_acc: 0.8468
    
  7. 使用拟合的模型在训练数据集上预测响应:

    print("Training data results - ")
    pred_train <- factor(ifelse(predict(dl_model, 
    as.matrix(x_train_scaled))>0.5,1,0))
    confusionMatrix(pred_train,factor(y_train),positive='1')
    

    输出如下:

    "Training data results - "
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 59281  8415
             1  2351  9000
    
                   Accuracy : 0.8638          
                     95% CI : (0.8614, 0.8662)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.547           
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5168          
                Specificity : 0.9619          
             Pos Pred Value : 0.7929          
             Neg Pred Value : 0.8757          
                 Prevalence : 0.2203          
             Detection Rate : 0.1139          
       Detection Prevalence : 0.1436          
          Balanced Accuracy : 0.7393          
    
           'Positive' Class : 1               
    
  8. 使用拟合的模型在测试数据集上预测响应:

    #Predict on Test Data
    pred_test <- factor(ifelse(predict(dl_model,   
                               as.matrix(x_test_scaled))>0.5,1,0))
    confusionMatrix(pred_test,factor(y_test),positive='1')
    

    输出如下:

    "Test data results - "
    Confusion Matrix and Statistics
              Reference
    Prediction     0     1
             0 25028  3944
             1  1246  3660
    
                   Accuracy : 0.8468          
                     95% CI : (0.8429, 0.8506)
        No Information Rate : 0.7755          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4965          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4813          
                Specificity : 0.9526          
             Pos Pred Value : 0.7460          
             Neg Pred Value : 0.8639          
                 Prevalence : 0.2245          
             Detection Rate : 0.1080          
       Detection Prevalence : 0.1448          
          Balanced Accuracy : 0.7170          
           'Positive' Class : 1               
    

    注意

    你可以在 GitHub 上找到完整的代码:http://bit.ly/2Vz8Omb。

预处理函数有助于将数据转换成所需的尺度或范围。在这里,我们将数据缩放到 0 到 1 的尺度。我们应仅考虑使用训练数据作为函数生成器的输入,并使用拟合方法来缩放测试数据。这在实际场景中非常重要,因为我们无法实时访问测试数据。一旦preProcess方法拟合完成,我们就用它来转换训练和测试数据。然后我们定义深度神经网络模型的架构。R 提供了易于扩展的管道操作符%>%,它使得操作符的连接变得简单。我们设计了一个包含三个层次,每个层次有 250 个神经元的网络。输入数据将形成第 0 层,最后一层将是预测结果。网络中用于隐藏层的激活函数是relu,这是任何深度学习用例中最推荐的激活函数。最后一层使用sigmoid激活函数,因为我们有一个二元分类用例。在 Keras 中可以选择许多激活函数,例如prelutanhswish等等。一旦模型架构定义完成,我们就定义损失函数binary_crossentropy,它与二元的logloss(类似于 XGBoost)类似,是模型用来学习和反向传播的技术,即优化器。预测中的错误会被反向传播到网络中,以便它可以调整权重,并迭代地减少错误。

这个功能的数学直观性可以采取多种方法。基于低阶矩自适应估计的 Adam 优化是最受欢迎的选择,对于大多数深度学习用例,我们几乎可以盲目地进行实验。其他一些选项包括rmsprop、随机梯度下降和Adagrad。我们还定义了在每个 epoch 后要在验证数据集上计算的指标,即网络对训练样本的一次完整展示。summary函数显示了使用 Keras 构造在前一节定义的架构的结果。summary函数给我们一个关于每个层中参数数量的简要概念,并且以层次结构的形式表示网络,帮助我们可视化模型架构。最后,我们使用fit函数来训练或“拟合”数据到网络中。我们还定义了模型应该迭代的 epoch 数;epoch 数越高,训练过程将需要更长的时间来计算。

批次大小表示网络在更新网络权重之前在一次单次传递中消耗的训练样本数量;批次较小的数字表示更频繁的权重更新,有助于有效地利用 RAM 内存。验证分割定义了每个 epoch 结束时用于验证的训练样本的百分比。最后,我们在训练数据和测试数据上验证模型的表现。

注意

代码片段中的这种解释无论如何都不能成为主题的正当理由。深度神经网络是一个极其庞大且复杂的主题,可能需要一本书来介绍基础知识。我们已将上下文封装成一段简短的段落,以便您了解模型开发过程中使用的结构。探索这个主题的深度将超出本书的范围。

从结果来看,我们可以看到与之前模型相似的结果。结果几乎可以与之前开发的 XGBoost 模型相媲美。在测试数据集上,我们大约有 48%的召回率和 75%的精确率。结果可以进一步调整以降低召回率并提高精确率(如果需要)。

因此,我们可以得出结论,我们从简单的逻辑回归模型、XGBoost 和深度神经网络模型中得到了相当好的结果。这三个模型之间的差异相对较小。这可能会让你产生重要的问题:在相同的使用案例上迭代各种模型是否值得?哪种模型理论上会给出最佳结果?尽管对这些问题的答案并不直接,但我们可以这样说,总的来说,简单模型总是表现良好;集成模型在大量数据的情况下表现更佳;而深度学习模型在大量数据的情况下表现更佳。在本章中,我们实验的使用案例中,通过超参数调整和;最重要的是;特征工程,我们将从所有模型中获得改进的结果。我们将在第七章“模型改进”中探讨超参数调整,在第六章“特征选择和降维”中探讨轻节点上的特征工程。特征工程的过程非常特定于领域,只能在一定程度上进行概括。我们将在下一章中更详细地探讨这一点。本章的主要议程是介绍涵盖该领域大量建模技术的范围,并帮助您为任何用于分类用例的机器学习技术打下基础。

为您的用例选择正确的模型

到目前为止,我们已经探索了一组白盒模型和几个用于相同分类用例的黑盒机器学习模型。我们还用 Keras 扩展了相同的用例,并研究了其性能。从几个模型和多次迭代的结果来看,我们需要决定哪个模型最适合分类用例。对此并没有简单直接的答案。在更广泛的意义上,我们可以认为对于大多数用例,最佳模型将是随机森林或 XGBoost。然而,这并不适用于所有类型的数据。会有许多场景,集成建模可能并不合适,线性模型会优于它,反之亦然。在数据科学家为分类用例进行的多数实验中,方法将是探索性和迭代的。在机器学习中没有一种适合所有情况的模型。设计和训练机器学习模型的过程是艰巨且极其迭代的,并且始终取决于用于训练的数据类型。

在给定构建监督机器学习模型的任务的情况下,最佳的前进方法如下:

  • 步骤 0: 探索性数据分析、数据处理和特征工程:使用可视化技术的组合对数据进行深入研究,然后处理缺失值、去除异常值、构建新特征,并建立训练集和测试集。(如果需要,也可以创建一个验证集。)

  • 步骤 1: 从简单的白盒模型如逻辑回归开始:在建模迭代中,最佳起点是一个简单的白盒模型,它可以帮助我们以易于量化的方式研究每个预测变量对因变量的影响。几次模型迭代将有助于特征选择,并清晰地理解最佳预测变量和模型基准。

  • 步骤 2: 使用决策树模型重复建模实验:利用决策树模型将始终帮助我们获得对模型和特征模式的全新视角。它可能会给出简单的规则,从而为改进模型提供新的特征工程思路。

  • 步骤 3: 如果有足够的数据,尝试集成建模;否则,尝试其他方法,例如支持向量机。

    使用随机森林和 XGBoost 进行集成建模几乎总是实验的一个安全选项。但在数据稀缺的情况下进行训练时,集成建模可能不是一种有效的推进方法。在这种情况下,基于黑盒核的模型在学习和数据模式方面可能更有效,从而提高模型性能。鉴于范围,我们没有在本章中涵盖支持向量机SVM)。然而,考虑到本章涵盖的广泛主题,对于您来说,开始使用 SVM 将是一个简单直接的任务。本博客提供了一个简单易懂的 SVM 指南:https://eight2late.wordpress.com/2017/02/07/a-gentle-introduction-to-support-vector-machines-using-r/.

    此外,为了了解训练样本的数量是更多还是更少,你可以使用一个简单的经验法则。如果数据集中每个特征至少有 100 个训练样本行,那么对于集成模型来说,数据就足够了;如果样本数量低于那个水平,那么集成模型可能并不总是有效的。尽管如此,尝试一下仍然值得。例如,如果有 15 个特征(自变量)和 1 个因变量,那么如果我们有15 x 100 = 1500个训练样本,集成模型在白盒模型上可能会有更好的性能。

  • 步骤 4:如果数据量足够多,尝试深度神经网络。如果数据集中每个特征至少有 10,000 个样本,那么尝试深度神经网络可能是个好主意。神经网络的问题主要是需要大量的训练数据和大量的迭代才能获得良好的性能。在大多数通用情况下,对于使用表格横截面数据进行分类(本书中我们解决的那种用例),深度神经网络与集成模型一样有效,但需要显著更多的努力来训练和调整以达到相同的结果。当有显著大量的样本进行训练时,它们确实优于集成模型。只有在有显著更多的训练样本时,在深度神经网络上的努力才会带来有利的结果。

摘要

在本章中,我们探讨了监督机器学习中不同类型的分类算法。我们利用澳大利亚天气数据,围绕它设计了一个商业问题,并探索了同一用例上的各种机器学习技术。我们研究了如何在 R 中开发这些模型,并深入研究了这些算法的数学抽象功能。我们总结了每种技术的结果,并研究了处理常见分类用例的通用方法。

在下一章中,我们将研究机器学习模型的特征选择、降维和特征工程。

第六章:特征选择和降维

学习目标

到本章结束时,你将能够:

  • 实施特征工程技术,如离散化、独热编码和转换

  • 使用单变量特征选择、相关矩阵和基于模型的特征重要性排序在真实世界数据集上执行特征选择方法

  • 使用主成分分析(PCA)进行降维,使用聚类进行变量减少,以及线性判别分析(LDA)

  • 实现 PCA 和 LDA,并观察它们之间的差异

在本章中,我们将探讨特征选择和降维方法,以构建有效的特征集,从而提高模型性能。

简介

在最后两章(关于回归和分类)中,我们专注于理解和实现监督学习类别中给定数据集的相关各种机器学习算法。

在本章中,我们将更多地关注有效地使用数据集的特征来构建性能最佳的模型。在许多数据集中,特征空间相当大(具有许多特征)。由于模式难以发现,数据中通常存在很多噪声,模型性能受到影响。特征选择是用于识别每个特征的重要性并为每个特征分配分数的特定方法。然后,我们可以根据分数选择前 10 个或 15 个特征(甚至更多)来构建我们的模型。

另一种可能性是使用所有输入变量的线性组合创建新的变量。这有助于保持所有变量的表示并降低特征空间的维度。然而,这种降低通常会导致可解释方差减少。在本章中,我们将重点关注我们执行的三项主要操作,以改进模型性能:

  • 特征工程:本质上是将特征进行转换,以便机器学习算法能够工作

  • 选择:选择具有高重要性的特征以发挥模型的最佳性能

  • 减少:通过将高阶数据集表示为低维数据来降低特征维度

这三个都是紧密相关但又各自不同的功能。

在本章中,除了北京 PM2.5 数据集外,我们还将使用 R 的mlbench库中提供的 1976 年洛杉矶臭氧污染数据。这是一个包含 366 个观测值和 13 个变量的数据框,其中每个观测值代表一天。

![图 6.1:洛杉矶臭氧污染数据中的变量列表,1976 年

![图 C12624_06_01.jpg]

图 6.1:洛杉矶臭氧污染数据中的变量列表,1976 年

原始数据集是为预测每日最大一小时平均臭氧读数(表中的第四个变量)的问题提供的。北京 PM2.5 和洛杉矶臭氧数据集都与污染物对我们环境的影响产生共鸣。

特征工程

我们在机器学习中使用的算法将根据数据的质量和良好性执行;它们本身没有任何智能。你在设计特征方面的越好和创新,模型的性能就越好。特征工程在许多方面有助于发挥数据的最优效果。特征工程这个术语本质上是指给定特征的推导转换过程,从而更好地表征特征的意义和表示预测模型的潜在问题。通过这个过程,我们预计模型的可预测性准确性将得到提高。

离散化

第三章监督学习简介 中,我们将北京数据集中 3 小时滚动平均 PM2.5 的数值转换为逻辑回归的二元值 1 和 0,基于 35 的阈值,其中 1 表示正常,0 表示高于正常。这个过程称为离散化,也常被称为分箱,在我们的情况下,二值离散化。更广泛地说,在应用数学中,离散化是将连续函数、模型、变量和方程转换为离散对应物的过程。现在,让我们对一个变量执行这个过程。

练习 77:执行二值离散化

在这个练习中,我们将使用北京 PM2.5 数据集的 pm2.5 变量创建一个二元变量。pm2.5 变量的二值离散化将创建一个列,如果 PM2.5 水平大于 35,则该列将为 1,否则为 0。这个过程将帮助我们从一个连续的数值变量创建一个离散的分类变量(称为 pollution_level)。

执行以下步骤以完成练习:

  1. 从以下命令开始读取北京 PM2.5 数据集:

    PM25 <- read.csv("PRSA_data_2010.1.1-2014.12.31.csv")
    
  2. 加载以下库:

    library(dplyr)
    library(lubridate)
    library(tidyr)
    library(ggplot2)
    library(grid)
    library(zoo)
    
  3. 使用 lubridate 包中的 with 函数将年、月、日和小时合并为一个 datetime 变量:

    PM25$datetime <- with(PM25, ymd_h(sprintf('%04d%02d%02d%02d', year, month, day,hour)))
    
  4. 现在,删除任何在列中包含 NA 的行:

    PM25_subset <- na.omit(PM25[,c("datetime","pm2.5")])
    
  5. 使用 zoo 结构,每 3 小时计算一次移动平均:

    PM25_three_hour_pm25_avg <- rollapply(zoo(PM25_subset$pm2.5,PM25_subset$datetime), 3, mean)
    
  6. 接下来,将移动平均的输出转换为 DataFrame:

    PM25_three_hour_pm25_avg <- as.data.frame(PM25_three_hour_pm25_avg)
    
  7. 现在,将行名中的时间戳放入主列中:

    PM25_three_hour_pm25_avg$timestamp <- row.names(PM25_three_hour_pm25_avg)
    
  8. 删除行名(可选):

    row.names(PM25_three_hour_pm25_avg) <- NULL
    
  9. 重命名列:

    colnames(PM25_three_hour_pm25_avg) <- c("avg_pm25","timestamp")
    
  10. 根据 PM2.5 平均值创建两个级别。0 表示正常1 表示高于正常

    PM25_three_hour_pm25_avg$pollution_level <- ifelse(PM25_three_hour_pm25_avg$avg_pm25 <= 35, 0,1)
    
  11. 使用以下命令随机选择 10 行:

    r_index <- sample(nrow(PM25_three_hour_pm25_avg),10)
    
  12. 使用以下命令打印输出:

    PM25_three_hour_pm25_avg[r_index,]
    ##        avg_pm25           timestamp pollution_level
    ## 405   399.33333 2010-01-18 21:00:00               1
    ## 3694  142.33333 2010-06-14 23:00:00               1
    ## 8072   14.33333 2010-12-31 05:00:00               0
    ## 3502  107.00000 2010-06-01 14:00:00               1
    ## 20828  80.33333 2012-07-21 16:00:00               1
    ## 32010  95.66667 2013-11-15 20:00:00               1
    ## 3637  103.33333 2010-06-12 14:00:00               1
    ## 4736  192.66667 2010-07-29 11:00:00               1
    ## 22053  37.33333 2012-09-17 19:00:00               1
    ## 7135   32.33333 2010-11-22 02:00:00               0
    

注意,变量 pollution_level 现在是一个二进制分类变量,我们在第 11 步中创建了它。以 pollution_level 作为输出变量的数据集可以与任何分类算法一起使用。

多类别离散化

离散化的更一般形式是将连续变量的值范围划分为许多更小的值范围,使用适当的切点。确定适当切点的一种方法是对变量的分布进行分析。

使用以下代码,以 binwidth30(意味着值范围将被划分为大小为 30 的范围)绘制 avg_pm25 的直方图:

ggplot(PM25_three_hour_pm25_avg, aes(x=avg_pm25)) +   geom_histogram(binwidth = 30,color="darkblue", fill="lightblue")

图 6.2:北京数据集中 3 小时滚动平均 PM2.5 值的直方图

图片

图 6.2:北京数据集中 3 小时滚动平均 PM2.5 值的直方图

图 6.2 中的图显示了变量的右偏态,这意味着大多数值位于值范围的左侧,主要集中在 0 到 250 之间。这种偏态阻碍了模型的泛化,因此其预测能力较低。现在,让我们探讨如何利用多类别离散化来改善这种情况。

练习 78:展示分位数函数的使用

在这个练习中,我们将展示 avg_pm25 的使用。

执行以下步骤以完成练习:

  1. 导入所需的库和包。

  2. avg_pm25 上找到分位数:

    avg_pm25 <- PM25_three_hour_pm25_avg$avg_pm25
    quartiles = quantile(round(avg_pm25), seq(0,1, .25), include.lowest=T)
    
  3. 接下来,计算分位数点的垂直线:

    ggplot(PM25_three_hour_pm25_avg, aes(x=avg_pm25))+  geom_histogram(binwidth = 30,color="darkblue", fill="lightblue")+    geom_vline(xintercept=quartiles,            color="blue", linetype="dashed", size=1)
    

    图如下:

    图片

    图 6.3:北京数据集中 3 小时滚动平均 PM2.5 值的直方图,对应于 0%、25%、50%、75% 和 100% 分位数点

    以下代码片段在数据集中创建了一个名为 avg_pm25_quartiles 的变量,它代表了 avg_pm25 值的五个百分位数点。这个新变量在 one-hot 编码 后可用于建模,我们将在下一节讨论。

  4. 让我们使用以下代码在数据集中添加一个新变量 avg_pm25_quartiles

    PM25_three_hour_pm25_avg$avg_pm25_quartiles <- as.integer(cut(avg_pm25,breaks=quantile(round(avg_pm25), seq(0,1, .25), include.lowest=T)))
    

我们刚刚看到离散化如何帮助在建模之前消除任何数据偏态。

One-Hot 编码

One-Hot 编码是一个将分类变量二进制化的过程。这是通过将具有 n 个唯一值的分类变量转换成数据集中的 n 个唯一列,同时保持行数不变来完成的。以下表格显示了风向列如何转换成五个二进制列。例如,行号 1 的值为 北风,因此我们在名为 Direction_N 的对应列中得到 1,而在其他列中得到 0。其他行也是如此。注意,在这五个样本数据行中,方向 西风 不存在。然而,更大的数据集会为我们提供 Direction_W 列的值。

![图 6.4 使用独热编码将分类变量转换为二进制 1 和 0]

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/app-spr-lrn-r/img/C12624_06_04.jpg)

图 6.4 使用独热编码将分类变量转换为二进制 1 和 0

将分类变量(如前表中所示)转换为二进制列的一个主要原因是与许多机器学习算法的限制有关,这些算法只能处理数值。然而,为了将分类变量转换为数值变量,我们必须用一些映射值来表示它,例如 北 = 1南 = 2西 = 3 等。这种编码的问题在于值 123 是整数,其中 3>2>1;然而,风向的情况并非如此。

解释完全错误。二进制独热编码通过为分类变量中的每个值创建一列来克服这一挑战,从而提供了更优雅的表示。现在,只要满足问题类型,我们可以使用任何机器学习算法来处理此类数据。

练习 79:使用独热编码

在这个练习中,我们将使用独热编码为分类变量中的每个值创建一列。

执行以下步骤以完成练习:

  1. 导入所需的库和包。

  2. 使用 read.csv 函数创建 OzoneData 变量并存储 ozone1.csv 的值:

    OzoneData <- read.csv("ozone1.csv", stringsAsFactors=F)
    
  3. 将所需的 caret 包导入系统:

    library(caret)
    
  4. 创建输入数据集:

    OzoneData$Day_of_week <- as.factor(OzoneData$Day_of_week) 
    OzoneData_OneHot <- dummyVars(" ~ .", data = OzoneData)
    
  5. 创建响应 DataFrame:

    OzoneData_OneHot <- data.frame(predict(OzoneData_OneHot, newdata = OzoneData))
    
  6. 使用 head() 函数绘制数据:

    head(OzoneData_OneHot)
    

    输出如下:

    ##   Month Day_of_month Day_of_week.1 Day_of_week.2 Day_of_week.3
    ## 1     1            1             0             0             0
    ## 2     1            2             0             0             0
    ## 3     1            3             0             0             0
    ## 4     1            4             0             0             0
    ## 5     1            5             1             0             0
    ## 6     1            6             0             1             0
    ##   Day_of_week.4 Day_of_week.5 Day_of_week.6 Day_of_week.7 ozone_reading
    ## 1             1             0             0             0             3
    ## 2             0             1             0             0             3
    ## 3             0             0             1             0             3
    ## 4             0             0             0             1             5
    ## 5             0             0             0             0             5
    ## 6             0             0             0             0             6
    ##   pressure_height Wind_speed Humidity Temperature_Sandburg
    ## 1            5480          8 20.00000             40.53473
    ## 2            5660          6 40.96306             38.00000
    ## 3            5710          4 28.00000             40.00000
    ## 4            5700          3 37.00000             45.00000
    ## 5            5760          3 51.00000             54.00000
    ## 6            5720          4 69.00000             35.00000
    ##   Temperature_ElMonte Inversion_base_height Pressure_gradient
    ## 1            39.77461              5000.000               -15
    ## 2            46.74935              4108.904               -14
    ## 3            49.49278              2693.000               -25
    ## 4            52.29403               590.000               -24
    ## 5            45.32000              1450.000                25
    ## 6            49.64000              1568.000                15
    ##   Inversion_temperature Visibility
    ## 1              30.56000        200
    ## 2              48.02557        300
    ## 3              47.66000        250
    ## 4              55.04000        100
    ## 5              57.02000         60
    ## 6              53.78000         60
    

观察我们在 OzoneData DataFrame 中创建的 OneHot 变量。经过独热编码后,Day_of_week 中的每个值(1 到 7)都表示为单独的列。

活动 11:将北京 PM2.5 数据集的 CBWD 特征转换为独热编码列

在这个活动中,我们将学习如何将任何分类变量转换为独热编码向量。特别是,我们将把北京 PM2.5 数据集的 CBWD 特征转换为独热编码列。许多机器学习算法只处理数值特征;在这种情况下,使用独热编码变得至关重要。

执行以下步骤以完成活动:

  1. 读取北京 PM2.5 数据集。

  2. 创建一个变量 cbwd_one_hot 用于存储 dummyVars 函数的结果,其第一个参数为 ~ cbwd

  3. 使用 predict() 函数在 cbwd_one_hot 上的输出。

  4. PM25 DataFrame 中移除原始的 cbwd 变量。

  5. 使用 cbind() 函数,将 cbwd_one_hot 添加到 PM25 DataFrame 中。

  6. 打印 PM25 的前六行。

    输出如下:

    ##   No year month day hour pm2.5 DEWP TEMP PRES   Iws Is Ir cbwd.cv cbwd.NE
    ## 1  1 2010     1   1    0    NA  -21  -11 1021  1.79  0  0       0       0
    ## 2  2 2010     1   1    1    NA  -21  -12 1020  4.92  0  0       0       0
    ## 3  3 2010     1   1    2    NA  -21  -11 1019  6.71  0  0       0       0
    ## 4  4 2010     1   1    3    NA  -21  -14 1019  9.84  0  0       0       0
    ## 5  5 2010     1   1    4    NA  -20  -12 1018 12.97  0  0       0       0
    ## 6  6 2010     1   1    5    NA  -19  -10 1017 16.10  0  0       0       0
    ##   cbwd.NW cbwd.SE
    ## 1       1       0
    ## 2       1       0
    ## 3       1       0
    ## 4       1       0
    ## 5       1       0
    ## 6       1       0
    

    注意

    这个活动的解决方案可以在第 459 页找到。

对数转换

修正偏斜分布最常见的技术是找到一个合适的具有逆函数的数学函数。其中一个这样的函数是对数,表示如下:

换句话说, 的对数,以 为底。其逆运算,即找到 ,可以按以下方式计算:

这种转换赋予了处理数据偏斜的能力;同时,一旦建立模型,原始值可以轻松计算。最流行的对数转换是自然对数 ,其中 是数学常数 ,其值大约为 2.71828

对数函数的一个有用特性是它优雅地处理数据偏斜。例如,以下代码演示了 log(10000)log(1000000) 的差异仅为 4.60517。数字 的 100 倍。这减少了我们通常让模型处理的偏斜,而模型可能不足以处理。

#Natural Log
log(10000)
## [1] 9.21034
# 10 times bigger value
log(100000)
## [1] 11.51293
# 100 times bigger value
log(1000000)
## [1] 13.81551

让我们看看对 PM2.5 值的 3 小时滚动平均应用自然对数的结果。

练习 80:执行对数转换

在这个练习中,我们将绘制经过对数转换的 avg_pm25 变量的直方图,并将其与原始值的偏斜分布进行比较。

执行以下步骤以完成练习:

  1. 导入所需的库和包。

  2. 创建 avg_pm25 的直方图:

    ggplot(PM25_three_hour_pm25_avg, aes(x=avg_pm25))+  geom_histogram(color="darkblue", fill="lightblue")
    

    输出如下:

    图 6.5 来自北京数据集的 PM2.5 值 3 小时滚动平均的直方图

    图 6.5 来自北京数据集的 PM2.5 值 3 小时滚动平均的直方图
  3. 创建 log_avg_pm25 的直方图:

    ggplot(PM25_three_hour_pm25_avg, aes(x=log_avg_pm25))+  geom_histogram(color="darkblue", fill="lightblue")
    

    输出如下:

图 6.6 来自北京数据集的 PM2.5 值 3 小时滚动平均的自然对数直方图

图 6.6 来自北京数据集的 PM2.5 值 3 小时滚动平均的自然对数直方图

在这个练习中,我们绘制了一个图表来展示北京数据集中 PM2.5 值的 3 小时滚动平均值,并将其与北京数据集中 PM2.5 值 3 小时滚动平均的自然对数直方图进行了对比。取对数使得直方图在平均值周围看起来更加对称,并减少了偏斜。

特征选择

特征工程确保质量和数据问题得到纠正时,特征选择有助于确定用于提高模型性能的正确特征集。特征选择技术识别对模型预测能力贡献最大的特征。重要性较低的特征会抑制模型从独立变量中学习的能力。

特征选择提供了以下好处:

  • 减少过拟合

  • 提高准确性

  • 减少训练模型的时间

单变量特征选择

像卡方检验(chi-squared)这样的统计检验是选择与因变量或目标变量有强关系的特征的一种流行方法。它主要在分类问题中的分类特征上工作。因此,为了在数值变量上使用它,需要将特征转换为分类,使用离散化。

在最一般的形式中,卡方统计量可以计算如下:

图片

这测试了观察频率与预期频率之间是否存在显著差异。较大的卡方值表明目标变量与特定特征的依赖性更强。更正式地说:

图片

其中:

图片

练习 81:探索卡方检验

在这个练习中,我们将计算Ozone数据集中所有变量的卡方统计量。具有最高卡方值的五个变量将是我们建模的最佳特征。

完成以下步骤以完成练习:

  1. 导入所需的库和包。

  2. 创建一个名为OzoneData的变量,并将read.csv函数的值分配给它:

    OzoneData <- read.csv("ozone1.csv", stringsAsFactors=F)
    
  3. 现在,设置path如上图所示:

    path="C:\\Program Files\\Java\\jdk1.8.0_92"
    
  4. 接下来,使用 Sys.getenv 函数获取环境变量的值:

    if (Sys.getenv("JAVA_HOME")!="")  Sys.setenv(JAVA_HOME=path)
    
  5. 使用以下命令安装所需的包:

    install.packages("rJava")
    install.packages("FSelector")
    
  6. 导入rJava包:

    library(rJava)
    ## Warning: package 'rJava' was built under R version 3.2.5
    library(FSelector)#For method
    library(mlbench)# For data
    
  7. 计算卡方统计量:

    weights<- chi.squared(ozone_reading~., OzoneData)
    
  8. 打印结果:

    print(weights)
    

    输出如下:

    ##                       attr_importance
    ## Month                       0.4240813
    ## Day_of_month                0.0000000
    ## Day_of_week                 0.0000000
    ## pressure_height             0.4315521
    ## Wind_speed                  0.0000000
    ## Humidity                    0.3923034
    ## Temperature_Sandburg        0.5191951
    ## Temperature_ElMonte         0.5232244
    ## Inversion_base_height       0.6160403
    ## Pressure_gradient           0.4120630
    ## Inversion_temperature       0.5283836
    ## Visibility                  0.4377749
    
  9. 选择前五个变量:

    subset<- cutoff.k(weights, 5)
    
  10. 打印可用于分类的最终公式:

    f<- as.simple.formula(subset, "Class")
    print(f)
    

    输出如下:

    ## Class ~ Inversion_base_height + Inversion_temperature + Temperature_ElMonte + 
    ##     Temperature_Sandburg + Visibility
    ## <environment: 0x000000001a796d18>
    

我们使用 chi.squared()函数计算了 Ozone 数据集中每个特征的卡方值。该函数根据卡方值输出属性的重要性。步骤 10 中使用的卡方统计量前五个特征可以用于构建监督学习模型。

高度相关的变量

通常,两个高度相关的变量可能对模型的预测能力做出贡献,这使得其中一个变得冗余。例如,如果我们有一个包含age(年龄)、height(身高)和BMI(体质指数)作为变量的数据集,我们知道BMIageheight的函数,并且它总是与这两个变量高度相关。如果不是这样,那么 BMI 的计算可能有问题。在这种情况下,可能会决定删除其他两个变量。然而,这并不总是这样。在某些情况下,一对变量可能高度相关,但很难解释为什么会出现这种情况。在这种情况下,可以随机删除两个中的一个。

练习 82:绘制相关矩阵

在这个练习中,我们将计算一对变量的相关性,并使用corrplot包绘制相关图。

完成练习的以下步骤:

  1. 使用以下命令导入所需的库:

    library(mlbench)
    library(caret)
    

    输出如下:

    ## Warning: package 'caret' was built under R version 3.2.5
    ## Loading required package: lattice
    
  2. 现在,加载数据并计算相关矩阵:

    correlationMatrix <- cor(OzoneData)
    
  3. 总结相关矩阵:

    print(correlationMatrix)
    

    输出如下:

    ##                              Month Day_of_month  Day_of_week ozone_reading
    ## Month                  1.000000000   0.00644330 -0.007345951   0.054521859
    ## Day_of_month           0.006443300   1.00000000  0.002679760   0.079493243
    ## Day_of_week           -0.007345951   0.00267976  1.000000000  -0.042135770
    ## ozone_reading          0.054521859   0.07949324 -0.042135770   1.000000000
    
  4. 找到高度相关的属性(理想情况下 >0.75):

    highlyCorrelated <- findCorrelation(correlationMatrix, cutoff=0.5)
    
  5. 打印高度相关属性的索引:

    print(highlyCorrelated)
    

    输出如下:

    ## [1] 12  9  8  5  4  7
    
  6. 导入corrplot库:

    library(corrplot)
    
  7. 绘制相关矩阵:

    corrplot(correlationMatrix)
    

    输出如下:

图 6.7:绘制相关矩阵

图 6.7:绘制相关矩阵

图 6.7中观察,深蓝色圆圈表示高度正相关,深红色圆圈表示高度负相关。相关值的范围在-11之间。通过视觉检查,我们可以看到变量Inversion_temperaturepressure_height有高度正相关,与Inversion_base_height有高度负相关。例如,如果Inversion_temperature增加,pressure_height也会增加,反之亦然。

注意

图 6.7 可以在 GitHub 上找到:https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R/blob/master/Lesson06/C12624_06_07.png.

基于模型的特征重要性排序

例如,随机森林这样的模型,它是一种集成建模技术,我们构建多个模型,并使用简单的投票技术(如第五章、分类中所述)来组合它们的结果,它有一个有用的技术来利用数据集中的所有变量,同时不损害模型性能。随机森林模型背后的简单想法是它随机选择数据集和变量的子集来构建多个决策树。最终的模型预测不是通过一个决策树,而是通过集体使用多个决策树来完成的。多数投票是用于最终预测的常用技术;换句话说,大多数决策树预测的结果是最终预测。

这种技术自然地给出了一种组合变量,这些变量导致最高的准确率。(也可以使用其他模型评估指标。)

注意

对于某些基因组学和计算生物学的研究工作****,其中潜在的预测变量在测量尺度上有所不同(包括序列和分类变量,如折叠能)以及它们的类别数量(例如,当氨基酸序列数据显示不同的类别数量),随机森林的重要性度量不可靠。

***** 随机森林变量重要性度量中的偏差:说明、来源和解决方案:https://link.springer.com/article/10.1186/1471-2105-8-25.

练习 83:使用 RF 探索 RFE

在这个练习中,我们将使用随机森林算法探索递归特征消除RFE)。RFE 有助于选择具有最高特征重要性的最佳特征。

执行以下步骤以完成练习:

  1. 导入party包:

    library(party)
    
  2. 拟合随机森林:

    cf1 <- cforest(pm2.5 ~ . , data= na.omit(PM25[,c("month","DEWP","TEMP","PRES","Iws","pm2.5")]), control=cforest_unbiased(mtry=2,ntree=50)) 
    
  3. 根据均方误差的减少量计算变量重要性。varimp()函数实现了 RFE 技术:

    varimp(cf1)
    

    输出如下:

    ##    month     DEWP     TEMP     PRES      Iws 
    ## 3736.679 5844.172 4080.546 1517.037 1388.532
    

在步骤 2 中,party 包提供了 cforest() 方法,使用参数 mtry = 2ntree = 50 拟合随机森林模型,并找到最佳模型,其中 x 仅使用没有 x 的 bootstrap 样本的树。函数 varimp() 使用排列原理(值随机打乱)计算平均 MSE 减少的变量重要性。换句话说,变量重要性是通过在训练后但在预测之前对给定变量进行排列后,所有袋外交叉验证预测的平均 MSE 减少来衡量的。

由于随机打乱的变量,我们预计会创建一个 差的 变量,并且包含这个打乱变量的 MSE 与不包含在模型中的 MSE 相比会增加。因此,如果 MSE 的平均减少量很高,那么由于变量打乱而产生的模型 MSE 肯定很高。所以,我们可以得出结论,该变量的重要性更高。

练习 84:使用随机森林模型探索变量重要性

在这个练习中,我们将使用随机森林模型探索变量重要性。我们再次使用北京市数据集,以查看五个变量(monthDEWPTEMPPRESIws)中哪一个最好地预测 PM2.5。

完成以下步骤以完成练习:

  1. 使用以下命令导入随机森林包:

    library(randomForest)
    
  2. 现在,使用以下命令创建一个新的对象:

    pm25_model_rf <- randomForest(pm2.5 ~ . , data = na.omit(PM25[,c("month","DEWP","TEMP","PRES","Iws","pm2.5")]), ntree=25,importance=TRUE, nodesize=5)
    
  3. 打印模型:

    pm25_model_rf
    

    输出如下:

    ## 
    ## Call:
    ##  randomForest(formula = pm2.5 ~ ., data = na.omit(PM25[, c("month",      "DEWP", "TEMP", "PRES", "Iws", "pm2.5")]), ntree = 25, importance = TRUE,      nodesize = 5) 
    ##                Type of random forest: regression
    ##                      Number of trees: 25
    ## No. of variables tried at each split: 1
    ## 
    ##           Mean of squared residuals: 3864.177
    ##                     % Var explained: 54.39
    
  4. 为每棵树找到 R-squared 值:

    pm25_model_rf$rsq
    

    输出如下:

    ##  [1] 0.2917119 0.3461415 0.3938242 0.4240572 0.4335932 0.4445404 0.4552216
    ##  [8] 0.4735218 0.4878105 0.4998751 0.5079323 0.5156195 0.5197153 0.5228638
    ## [15] 0.5286556 0.5305679 0.5312043 0.5341559 0.5374104 0.5397305 0.5421712
    ## [22] 0.5434857 0.5430657 0.5435383 0.5439461
    
  5. 接下来,计算变量重要性图:

    varImpPlot(pm25_model_rf)
    

    输出如下:

Figure 6.8:通过在北京市 PM2.5 数据上拟合随机森林模型获得的 MSE 百分比增加和节点纯度值增加

img/C12624_06_08.jpg

图 6.8:通过在北京市 PM2.5 数据上拟合随机森林模型获得的 MSE 百分比增加和节点纯度值增加

前一个练习演示了查看变量重要性的另一种方法。我们使用了 randomForest 包,而不是 party 包。%IncMSE 的计算方法如下所述:

  1. 拟合随机森林(在我们的情况下,是回归随机森林)。计算 OOB-MSE 并将其命名为 MSE_Base

  2. 对于每个变量 j:对列 j 的值进行排列,然后预测并计算 OOB_MSE_j.

  3. %IncMSEjth 变量等于 (OOB_MSE_j - MSE_Base)/ MSE_Base * 100%.

图 6.8 显示,将变量 Iws 包含在模型中,与变量 DEWP 相比,MSE 增加了 22%,而 DEWP 仅使 MSE 增加了 15%。我们知道由于变量值的打乱,MSE 肯定会增加,所以更高的 % 意味着是一个好的变量。如果我们看到变量 TEMP,与 IwsDEWP 相比,值的打乱对 MSE 的影响不大;因此,相对而言,它不太重要。

节点纯度计算损失函数的值,在这个模型中是均方误差 (MSE)。它有助于选择最佳分割。MSE 的降低会给出更高的节点纯度值。DEWP 具有最高的节点纯度,其次是月份特征。在我们的数据集中,%IncMSEIncNodePurity 显示了类似的结果。然而,请记住,IncNodePurity 往往存在偏差,并且应该始终与 %IncMSE 结合起来考虑。

特征降维

特征降维有助于去除冗余变量,以下方式降低模型效率:

  • 模型开发/训练时间增加。

  • 结果的解释变得繁琐。

  • 它夸大了估计的方差。

在本节中,我们将看到三种特征降维技术,这些技术有助于提高模型效率。

主成分分析 (PCA)

N. A. Campbell 和 William R. Atchley 在他们经典论文《典型变量分析的几何学》中,系统生物学,第 30 卷,第 3 期,1981 年 9 月,第 268–280 页,从几何角度定义了主成分分析为将原始变量坐标系统的轴旋转到新的正交轴,称为主轴,使得新轴与原始观察的最大变化方向一致。这是 PCA 的核心所在。换句话说,它用解释原始观察或数据最大变化的主成分来表示原始变量。

论文巧妙地展示了主成分的几何表示,如下所示图,这是两个变量的散点图表示,显示了每个变量的均值 ![img/C12624_06_34.png],95% 浓集椭圆,以及主轴 ![img/C12624_06_35.png] 和 ![img/C12624_06_36.png]。点 ![img/C12624_06_37.png] 和 ![img/C12624_06_38.png] 给出了观察 ![img/C12624_06_39.png] = . 的主成分得分。向量 ![img/C12624_06_41.png] 和 ![img/C12624_06_42.png] 之间角度 ![img/C12624_06_40.png] 的余弦值给出了对应于 ![img/C12624_06_44.png] 的特征向量的第一个分量 ![img/C12624_06_43.png]。

在线性代数中,线性变换的特征向量是一个非零向量,当该线性变换应用于它时,它只改变一个标量因子。

![img/C12624_06_09.jpg]

图 6.9:显示了两个变量的散点图表示,显示了每个变量的均值(x̅_1 和 x̅_2),95% 浓集椭圆,以及主轴 Y_1 和 Y_2
来源:典型变量分析的几何学,系统生物学,第 30 卷,第 3 期,1981 年 9 月,第 268–280 页

练习 85:执行 PCA

在这个练习中,我们将执行 PCA,这将有助于降低特征空间的维度。换句话说,更少的由输入特征线性组合的主成分将代表整个数据集。

执行以下步骤以完成练习:

  1. 导入OzoneData包:

    dim(OzoneData)
    

    输出结果如下:

    ## [1] 366  13
    
  2. 使用colnames函数打印列名:

    colnames(OzoneData)
    

    输出结果如下:

    ##  [1] "Month"                 "Day_of_month"         
    ##  [3] "Day_of_week"           "ozone_reading"        
    ##  [5] "pressure_height"       "Wind_speed"           
    ##  [7] "Humidity"              "Temperature_Sandburg" 
    ##  [9] "Temperature_ElMonte"   "Inversion_base_height"
    ## [11] "Pressure_gradient"     "Inversion_temperature"
    ## [13] "Visibility"
    ## [1] 50  4
    
  3. 计算所有变量的均值:

    apply(OzoneData,2,mean)
    

    输出结果如下:

    ##                 Month          Day_of_month           Day_of_week 
    ##              6.513661             15.756831              4.002732 
    ##         ozone_reading       pressure_height            Wind_speed 
    ##             11.582020           5752.448016              4.868852 
    ##              Humidity  Temperature_Sandburg   Temperature_ElMonte 
    ##             58.295691             61.858629             57.219990 
    ## Inversion_base_height     Pressure_gradient Inversion_temperature 
    ##           2596.265137             17.785440             61.005339 
    ##            Visibility 
    ##            123.300546
    
  4. 计算所有变量的方差:

    apply(OzoneData,2,var) 
    

    输出结果如下:

    ##                 Month          Day_of_month           Day_of_week 
    ##          1.194365e+01          7.785578e+01          3.991773e+00 
    ##         ozone_reading       pressure_height            Wind_speed 
    ##          6.243605e+01          1.092618e+04          4.481383e+00 
    ##              Humidity  Temperature_Sandburg   Temperature_ElMonte 
    ##          3.861494e+02          2.039533e+02          1.109866e+02 
    ## Inversion_base_height     Pressure_gradient Inversion_temperature 
    ##          3.115312e+06          1.300448e+03          1.871074e+02 
    ##            Visibility 
    ##          6.444901e+03
    

    变量的方差显著差异将控制主成分。prcomp()函数在找出主成分之前会对变量进行标准化(均值0和方差1)。

    pca.out<-prcomp(OzoneData,scale=TRUE)
    
  5. 接下来,找到 PCA 的摘要:

    summary(pca.out)
    

    输出结果如下:

    ## Importance of components:
    ##                           PC1    PC2     PC3     PC4     PC5     PC6
    ## Standard deviation     2.2817 1.4288 1.05944 1.01842 1.00160 0.93830
    ## Proportion of Variance 0.4005 0.1570 0.08634 0.07978 0.07717 0.06772
    ## Cumulative Proportion  0.4005 0.5575 0.64386 0.72364 0.80081 0.86853
    ##                            PC7     PC8     PC9    PC10    PC11    PC12
    ## Standard deviation     0.74291 0.64513 0.54523 0.48134 0.33068 0.25908
    ## Proportion of Variance 0.04246 0.03202 0.02287 0.01782 0.00841 0.00516
    ## Cumulative Proportion  0.91099 0.94301 0.96587 0.98369 0.99211 0.99727
    ##                           PC13
    ## Standard deviation     0.18840
    ## Proportion of Variance 0.00273
    ## Cumulative Proportion  1.00000
    
  6. 使用ggbiplot函数创建双图:

    library(devtools)
    install_github("vqv/ggbiplot", force=TRUE)
    library(ggbiplot)
    ggbiplot(pca.out)
    

    输出结果如下:

![图 6.10 使用 ggbiplot 缩放双图的前两个主成分

![img/C12624_06_10.jpg]

图 6.10 使用 ggbiplot 缩放双图的前两个主成分

图中的双图显示了Ozone数据集。如summary(pca.out)的输出所示,双图使用数据集中的各种特征来描述解释的方差。轴被看作是从中心点发出的箭头。该图还显示,变量pressure_heightinversion_temperatureVisibility有贡献,而day_of_the_weekPC2有更高的值。

如果您在安装ggbiplot时遇到困难,您也可以使用 R 基础包中的biplot()函数,如下面的图所示。首先,让我们构建一个双图以更好地理解:

biplot(pca.out,scale = 0, cex=0.65)

![图 6.11 第一主成分的缩放双图

![img/C12624_06_11.jpg]

图 6.11 第一主成分的缩放双图

注意,最大百分比方差由 PC1 解释,所有主成分彼此不相关。特别是,大约40%的方差由 PC1 解释,第一个主成分(PC1-PC4)解释了 70%的方差。换句话说,如果我们使用前四个主成分,我们应该得到一个与使用数据集中所有变量得到的模型几乎相同的模型。这并不令人惊讶,因为主成分是变量的线性组合。

变量聚类

变量聚类用于测量共线性、计算冗余,并将变量分离成可以计为一个单一变量的聚类,从而实现数据降维。对变量的层次聚类分析可以使用以下任何一个:Hoeffding 的 D 统计量、平方皮尔逊或斯皮尔曼相关系数,或者使用两个变量都为正的观测比例作为相似性度量。其思想是找到与自身相关且不与另一个聚类中的变量相关的相关变量簇。这可以将大量特征减少到更少的特征或变量簇。

练习 86:使用变量聚类

在这个练习中,我们将使用特征聚类来识别相似特征簇。从每个簇中,我们可以选择一个或多个特征用于模型。我们将使用 R 中的 Hmisc 包中的层次聚类算法。相似度度量应设置为"spear",代表皮尔逊相关系数,它是计算两个观测值之间相似性的稳健度量。

执行以下步骤以完成练习:

  1. 使用以下命令安装Hmisc包:

    install.packages("Hmisc")
    
  2. 导入Hmisc包并设置随机种子为1

    library(Hmisc)
    set.seed(1)
    
  3. 使用变量聚类,以 Spearman 相关系数作为相似度度量:

    Ozone_var_clust <- varclus(as.matrix(OzoneData), similarity="spear")
    Ozone_var_clust
    

    输出如下:

    ## varclus(x = as.matrix(OzoneData), similarity = "spear")
    ## 
    ## 
    ## Similarity matrix (Spearman rho²)
    ## 
    ##                       Month Day_of_month Day_of_week ozone_reading
    ## Month                  1.00         0.00        0.00          0.00
    ## Day_of_month           0.00         1.00        0.00          0.01
    ## Day_of_week            0.00         0.00        1.00          0.00
    ## ozone_reading          0.00         0.01        0.00          1.00
    ## pressure_height        0.12         0.00        0.00          0.36
    ## Wind_speed             0.04         0.01        0.00          0.00
    ## Humidity               0.01         0.00        0.00          0.20
    ## Temperature_Sandburg   0.05         0.01        0.00          0.63
    ## Temperature_ElMonte    0.07         0.00        0.00          0.59
    ## Inversion_base_height  0.00         0.01        0.00          0.32
    ## Pressure_gradient      0.03         0.00        0.00          0.06
    ## Inversion_temperature  0.04         0.01        0.00          0.54
    ## Visibility             0.04         0.02        0.01          0.20
    ##                       pressure_height Wind_speed Humidity
    ## Month                            0.12       0.04     0.01
    ## Day_of_month                     0.00       0.01     0.00
    ## Day_of_week                      0.00       0.00     0.00
    ## ozone_reading                    0.36       0.00     0.20
    ## pressure_height                  1.00       0.02     0.03
    ## Wind_speed                       0.02       1.00     0.03
    ## Humidity                         0.03       0.03     1.00
    <Output Truncated for brevity>
    ## hclust results (method=complete)
    ## 
    ## 
    ## Call:
    ## hclust(d = as.dist(1 - x), method = method)
    ## 
    ## Cluster method   : complete 
    ## Number of objects: 13
    
  4. 打印值:

    print(round(Ozone_var_clust$sim,2))
    

    输出如下:

    ##                       Month Day_of_month Day_of_week ozone_reading
    ## Month                  1.00         0.00        0.00          0.00
    ## Day_of_month           0.00         1.00        0.00          0.01
    ## Day_of_week            0.00         0.00        1.00          0.00
    ## ozone_reading          0.00         0.01        0.00          1.00
    ## pressure_height        0.12         0.00        0.00          0.36
    ## Wind_speed             0.04         0.01        0.00          0.00
    ## Humidity               0.01         0.00        0.00          0.20
    ## Temperature_Sandburg   0.05         0.01        0.00          0.63
    ## Temperature_ElMonte    0.07         0.00        0.00          0.59
    ## Inversion_base_height  0.00         0.01        0.00          0.32
    ## Pressure_gradient      0.03         0.00        0.00          0.06
    

根据相似度矩阵,以下图显示了同一簇中的变量。例如,Temperature_ElMonteInversion_temperature都被聚类到一个簇中,Spearman 相关系数为 0.85。同样,HumidityPressure_gradient的 Spearman 相关系数为 0.25。高相似度将导致决定删除其中一个。除了查看聚类输出顶部之外,在最终决定完全删除变量之前,还应考虑模型指标:

plot(Ozone_var_clust)

图 6.12:臭氧数据集中变量的层次聚类

图 6.12:臭氧数据集中变量的层次聚类

线性判别分析用于特征降维

线性判别分析LDA)通过将数据投影到新的特征空间(低维空间,具有良好的类别可分性)来最大化类别分离,以避免过拟合(维度诅咒)。LDA 还降低了计算成本,使其适合作为分类算法。其思想是最大化每个类别(或类别)的均值之间的距离,并最小化类别内的变异性。(这确实听起来像无监督学习中的聚类算法,但在这里我们不会涉及,因为它超出了本书的范围。)请注意,LDA 假设数据遵循高斯分布;如果不是,LDA 的性能将降低。在本节中,我们将使用 LDA 作为特征降维技术,而不是作为分类器。

对于二分类问题,如果我们有一个m-维数据集 ,包含N个观测值,其中 属于类别 ,而 属于类别 。在这种情况下,我们可以将数据投影到一条线(C=2,投影到C-1空间):

这样的投影是通过将 X 的均值投影到 Y 的均值上实现的。在所有可能的线中,我们希望选择一条最大化标量可分性的线。换句话说,来自同一类的观察值的投影非常接近,同时,投影的均值尽可能远。

应该注意的是,虽然 LDA 中我们更像是监督学习使用类变量,PCA 不需要任何类变量来减少特征大小。这就是为什么,虽然 LDA 尽可能多地保留了类区分信息,PCA 并不太关心这一点。

![Figure 6.13: Comparing PCA and LDAimg/C12624_06_13.jpg

图 6.13:比较 PCA 和 LDA
来源:https://sebastianraschka.com/Articles/2014_python_lda.html

练习 87:探索 LDA

在这个练习中,我们将执行 LDA 进行特征降维。我们将观察使用所有特征和 LDA 降维后的特征在模型性能上的差异。

执行以下步骤:

  1. 将两个 DataFrame 在时间戳上合并以堆叠其他环境变量以及 PM2.5 到一个 DataFrame 中:

    PM25_for_LDA <- merge(PM25_three_hour_pm25_avg, PM25[,c("datetime","TEMP","DEWP","PRES","Iws","cbwd","Is","Ir")], by.x = "timestamp",by.y = "datetime")
    PM25_for_LDA = PM25_for_LDA[,c("TEMP","PRES","DEWP","Iws","Is","Ir","pollution_level")]
    
  2. 将数据集分为训练集和测试集:

    index = sample(1:nrow(PM25_for_LDA), round(nrow(PM25_for_LDA)*0.6 ), replace = FALSE)
    LDA_train = PM25_for_LDA[ index, ]
    LDA_test = PM25_for_LDA[ -index, ]
    
  3. 导入 MASS 包:

    library(MASS)
    
  4. 在训练数据集上拟合 LDA 模型:

    LDA_model = lda( pollution_level ~ ., data = LDA_train )
    projected_data = as.matrix( LDA_train[, 1:6] ) %*%  LDA_model$scaling
    
  5. 绘制 100 个随机选择的投影值:

    set.seed(100)
    index <- sample(nrow(projected_data),100, replace = FALSE)
    plot( projected_data[index], col = LDA_train[,7], pch = 19 )
    

    输出如下:

    ![Figure 6.14: Plot of randomly selected 100 projected values img/C12624_06_14.jpg

    图 6.14:随机选择的 100 个投影值的图
  6. 执行模型测试:

    LDA_test_reduced = LDA_test[, !( names( LDA_test ) %in% c( "pollution_level" ) ) ]  
    LDA_model_results = predict( LDA_model, LDA_test_reduced )
    
  7. 导入 caret 库并打印混淆矩阵:

    library( caret )
    c_t = table( LDA_model_results$class, LDA_test$pollution_level )
    print( confusionMatrix( c_t ) )
    

    输出如下:

    ## Confusion Matrix and Statistics
    ## 
    ##    
    ##         0     1
    ##   0  2359   978
    ##   1  2257 11108
    ##                                           
    ##                Accuracy : 0.8063          
    ##                  95% CI : (0.8002, 0.8123)
    ##     No Information Rate : 0.7236          
    ##     P-Value [Acc > NIR] : < 2.2e-16       
    ##                                           
    ##                   Kappa : 0.4704          
    ##  Mcnemar's Test P-Value : < 2.2e-16       
    ##                                           
    ##             Sensitivity : 0.5110          
    ##             Specificity : 0.9191          
    ##          Pos Pred Value : 0.7069          
    ##          Neg Pred Value : 0.8311          
    ##              Prevalence : 0.2764          
    ##          Detection Rate : 0.1412          
    ##    Detection Prevalence : 0.1998          
    ##       Balanced Accuracy : 0.7151          
    ##                                           
    ##        'Positive' Class : 0               
    ## 
    
  8. 查找降维后的数据集:

    new_LDA_train = as.matrix( LDA_train[,1:6] ) %*%
      LDA_model$scaling
    new_LDA_train = as.data.frame( new_LDA_train )
    new_LDA_train$pollution_level = LDA_train$pollution_level
    
  9. 测试数据集:

    new_LDA_test = as.matrix( LDA_test[,1:6] ) %*%
      LDA_model$scaling
    new_LDA_test = as.data.frame( new_LDA_test )
    new_LDA_test$pollution_level = LDA_test$pollution_level
    
  10. 使用投影数据。让我们拟合一个逻辑模型。你也可以使用其他任何分类模型:

    PM25_logit_model_on_LDA <- glm(pollution_level ~ ., data = new_LDA_train,family=binomial(link='logit'))
    
  11. 在测试数据上执行模型评估:

    predicted_LDA = predict(PM25_logit_model_on_LDA, newdata = new_LDA_test,type="response")
    
  12. 如果概率大于 0.5,则预测 1:

    predicted <- ifelse(predicted_LDA>0.5, 1,0)
    actual <- new_LDA_test$pollution_level
    
  13. 查找混淆矩阵:

    confusionMatrix(predicted, actual)
    

    输出如下:

    ## Confusion Matrix and Statistics
    ## 
    ##           Reference
    ## Prediction     0     1
    ##          0  2316   947
    ##          1  2300 11139
    ##                                           
    ##                Accuracy : 0.8056          
    ##                  95% CI : (0.7995, 0.8116)
    ##     No Information Rate : 0.7236          
    ##     P-Value [Acc > NIR] : < 2.2e-16       
    ##                                           
    ##                   Kappa : 0.4655          
    ##  Mcnemar's Test P-Value : < 2.2e-16       
    ##                                           
    ##             Sensitivity : 0.5017          
    ##             Specificity : 0.9216          
    ##          Pos Pred Value : 0.7098          
    ##          Neg Pred Value : 0.8289          
    ##              Prevalence : 0.2764          
    ##          Detection Rate : 0.1387          
    ##    Detection Prevalence : 0.1954          
    ##       Balanced Accuracy : 0.7117          
    ##                                           
    ##        'Positive' Class : 0               
    ##
    

注意,LDA_test 和投影的 new_LDA_test 的准确性惊人地相似。这表明在新低维空间中的投影值与原始值相比表现同样出色。并不总是新空间会带来与原始空间相同的表现。因此,在减少特征空间之前需要进行彻底的审查。

摘要

在本章中,我们看到了各种特征选择和降维技术。本章涵盖的三个主要主题是:特征工程、特征选择和特征降维。后两者具有缩小特征数量的相同目的;然而,所使用的技巧完全不同。特征工程侧重于将变量转换成新的形式,这有助于提高模型性能或使变量符合模型假设。例如,在线性回归模型中的线性假设,我们通常可以对变量进行平方或立方,以及解决数据分布中的偏斜度,这可以通过对数变换来解决。特征选择和特征降维有助于提供最佳的特征集或特征集的最佳表示,从而提高模型性能。最重要的是,这两种技术都缩小了特征空间,这极大地提高了模型训练时间,同时没有在准确性、RMSE或任何相关模型评估指标方面妥协性能。

我们还看到了一些模型本身,例如随机森林和LDA,可以直接用作特征选择和降维技术。随机森林通过随机选择的方法选择最佳特征,而 LDA 则是通过寻找特征的最佳表示来工作。因此,前者用于特征选择,后者用于降维。

在下一章中,我们将探讨更多关于模型改进的内容,其中将应用本章的一些学习成果。

第七章:模型改进

学习目标

到本章结束时,你将能够:

  • 解释并实现机器学习模型中偏差和方差权衡的概念。

  • 使用交叉验证进行模型评估。

  • 为机器学习模型实现超参数调整。

  • 使用各种超参数调整技术提高模型性能。

在本章中,我们将重点关注使用交叉验证技术和超参数调整来提高模型性能。

简介

在上一章中,我们探讨了几个策略,这些策略帮助我们通过特征选择降维构建改进的模型。这些策略主要关注提高模型的计算性能和可解释性;然而,为了根据性能指标(如整体准确率或误差估计)提高模型性能,构建稳健且更具普遍性的模型,我们需要关注交叉验证和超参数调整。

在本章中,我们将向您介绍机器学习的核心主题,使用交叉验证和超参数调整构建通用的稳健模型,并在 R 中实现它们。

我们将首先使用通俗易懂的例子详细研究本章的主题,并利用简单的用例来观察实现过程。

偏差-方差权衡

机器学习的一个有趣、艰难且重复的部分是模型评估之旅。再次强调,构建稳健模型需要艺术和不同的思维方式。在整个书中,我们通过将可用数据分成70:3080:20的比例来简化模型评估过程,从而得到训练集和测试集。尽管这种方法有助于我们理解模型在未见数据上的表现,但它仍然留下了一些可能导致模型在其他大多数情况下无效的漏洞。为了使机器学习模型在未来的预测事件中稳健,我们需要一个更正式、更彻底和更全面的验证方法。在本章中,我们将研究交叉验证及其评估机器学习模型性能的各种方法。

在我们深入探讨这个话题的具体内容之前,我们需要探索机器学习中的一个关键话题,称为偏差-方差权衡。这个话题在大多数机器学习论坛和学术界都备受讨论。对于机器学习社区来说,这是一个关键话题,它构成了在深入研究模型验证和改进之前的基础。从话题的标题来看,我们可以很容易地推断出,在机器学习模型中,偏差-方差权衡是模型展示的行为,其中在估计模型参数时表现出低偏差的模型,不幸地表现出更高的方差,反之亦然。为了从普通人的角度理解这个话题,让我们首先将这个话题分解成单个组件,理解每个组件,然后使用所有组件一起重建更大的图景。

机器学习模型中的偏差和方差是什么?

通常,当一个机器学习模型未能学习到数据中展示的重要(或有时是复杂)模式时,我们说该模型是有偏差的。这样的模型过于简化自己,或者只学习极端简单的规则,这些规则可能对做出准确预测没有帮助。这样的模型的结果是,预测结果往往保持大部分相同(且错误),无论输入数据有何差异。模型学习到的模式过于简单或有偏差,不幸的是,输入数据的这些变化并没有产生预期的预测结果。

另一方面,如果我们反转这个逻辑,我们就可以很容易地定义机器学习模型中的方差。想想那些学习不必要模式的模型,比如数据中的噪声,以至于输入数据的微小变化会导致预测结果产生显著的不希望的变化。在这种情况下,我们说该模型具有高方差。

理想的情况是一个具有低偏差和低方差的模型;也就是说,一个从数据中学习到必要模式的模型。它成功地忽略了噪声,并在输入数据合理变化的情况下,合理地改变了预测行为。不幸的是,理想的情况很难实现,因此我们来到了偏差-方差权衡这个话题。

将我们研究过的所有单个组件放在一起,我们可以这样说,每个试图减少偏差或方差的努力都会导致另一个维度的增加,从而产生我们需要在模型性能的期望偏差和方差之间取得平衡的情况。为了在机器学习模型中实现偏差和方差的平衡,我们可以使用超参数调整方法的组合。我们将在接下来的章节中研究超参数调整的概念。以下是一个著名的例子,用视觉靶心图来展示偏差-方差概念:

![图 7.1:使用视觉靶心图表示偏差-方差概念图片 C12624_07_01.jpg

图 7.1:使用视觉靶心图表示偏差-方差概念

在前面的图中,我们可以看到四个象限,用于具体区分偏差-方差权衡。该图用于解释回归用例中的模型偏差和方差。对于分类用例,以类似的方式直观地推断可能具有挑战性;然而,通过图例示例,我们获得了更大的图景。

我们理想的目标是训练一个具有低偏差和低方差的机器学习模型。然而,当我们有低偏差和高方差(前一个可视化中的右上象限)时,我们会看到对于输入数据的小幅变化,最终结果有显著的变化。另一方面,当我们有高偏差和低方差(可视化中的左下象限)时,我们可以看到最终结果集中在远离目标区域,对于输入的变化几乎没有变化。最后,我们具有高偏差和高方差,即我们远离目标,以及对于输入的小幅变化有大的变化。这将是模型最不理想的状态。

欠拟合和过拟合

在前面的场景中,当我们有高偏差时,我们在机器学习模型中定义了一个称为欠拟合的现象。同样,当我们有高方差时,我们在机器学习模型中定义了一个称为过拟合的现象。

下面的视觉演示了回归模型中过拟合欠拟合理想平衡的概念。我们可以看到高偏差导致过度简化的模型(即欠拟合);高方差导致过度复杂的模型(即过拟合);最后,我们在偏差和方差之间找到了正确的平衡:

![图 7.2:过拟合、欠拟合和理想平衡的视觉演示图片 C12624_07_02.jpg

图 7.2:过拟合、欠拟合和理想平衡的视觉演示

为了更有效地研究机器学习模型中的偏差和方差,我们使用了交叉验证技术。这些技术帮助我们更直观地理解模型性能。

定义一个示例用例

为了探索本章中的实际数据集,我们使用了mlbench包中已可用的小数据集,称为PimaIndiansDiabetes,这是一个适用于分类用例的便捷数据集。

数据集最初来自美国糖尿病、消化和肾脏疾病国家研究所。可以从数据集中定制出以下用例:当预测患者是否患有糖尿病作为少量医学诊断测量函数时。

注意

更多信息可以在 http://math.furman.edu/~dcs/courses/math47/R/library/mlbench/html/PimaIndiansDiabetes.html 找到。

选择小于 1000 行的数据集作为用例是有意为之的。本章探讨的主题在常规使用案例中需要在大数据集上执行高计算时间。选择小数据集用于演示目的有助于在大多数使用主流硬件的读者中实现相对正常的计算时间。

练习 88:加载数据和探索数据

为了快速研究 PimaIndiansDiabetes 的整体特征并探索每列内容的特点,请执行以下步骤:

  1. 使用以下命令加载 mlbenchrandomForestdplyr 库:

    library(mlbench)
    library(randomForest)
    library(dplyr)
    
  2. 使用以下命令从 PimaIndiansDiabetes 数据集中加载数据:

    data(PimaIndiansDiabetes)
    df<-PimaIndiansDiabetes
    
  3. 使用 str 命令探索数据集的维度并研究每列的内容:

    str(df)
    

    输出如下:

    'data.frame':768 obs. of  9 variables:
     $ pregnant: num  6 1 8 1 0 5 3 10 2 8 ...
     $ glucose : num  148 85 183 89 137 116 78 115 197 125...
     $ pressure: num  72 66 64 66 40 74 50 0 70 96 ...
     $ triceps : num  35 29 0 23 35 0 32 0 45 0 ...
     $ insulin : num  0 0 0 94 168 0 88 0 543 0 ...
     $ mass    : num  33.6 26.6 23.3 28.1 43.1 25.6 31 35.3 30.5 0 ...
     $ pedigree: num  0.627 0.351 0.672 0.167 2.288 ...
     $ age     : num  50 31 32 21 33 30 26 29 53 54 ...
     $ diabetes: Factor w/ 2 levels "neg","pos": 2 1 2 1 2 1 2 1 2 2 ...
    

如我们所见,数据集有 768 个观测值和 9 个变量,即 8 个独立变量和 1 个依赖的类别变量 diabetes,其值为 pos 表示阳性,neg 表示阴性。

我们将使用这个数据集,并在这个章节的后续主题中开发几个分类模型。

交叉验证

交叉验证是一种模型验证技术,有助于评估机器学习模型在独立数据集上的性能和泛化能力。它也被称为旋转验证,因为它通过从同一分布中抽取训练和验证数据,通过多次重复来接近模型的验证。

交叉验证帮助我们:

  • 评估模型在未见数据上的鲁棒性。

  • 估计期望性能指标的合理范围。

  • 减少模型的过拟合和欠拟合。

交叉验证的一般原则是通过将数据分组并使用多数数据训练和少数数据测试,在多个迭代中在整个数据集上测试模型。重复的旋转确保模型已经在所有可用观测值上进行了测试。模型的最终性能指标是从所有旋转的结果中汇总和总结的。

为了研究模型是否存在高偏差,我们可以检查模型在所有旋转中的平均性能。如果平均性能指标表明整体准确率(对于分类)或平均绝对百分比误差(对于回归)较低,那么存在高偏差,模型欠拟合。为了研究模型是否存在高方差,我们可以研究期望性能指标在旋转中的标准差。高标准差将表明模型将具有高方差;也就是说,模型将过拟合。

在交叉验证中有几种流行的方法:

  • 保留样本验证

  • K 折交叉验证

  • 保留一个样本验证(LOOCV)

让我们详细探讨这些方法中的每一个。

保留样本方法/验证

这是在验证模型性能中使用的最简单的方法(尽管不是最推荐的方法)。我们一直在本书中使用这种方法来测试前几章中的模型性能。在这里,我们随机将可用数据集分为训练集和测试集。训练集和测试集之间最常用的分割比例是70:3080:20

这种方法的缺点主要是模型性能完全从分数测试数据集中评估,可能不是模型性能的最佳表示。模型的评估将完全取决于分割的类型,因此,最终进入训练集和测试集的数据点的性质,这可能会导致显著不同的结果和因此高方差。

![图 7.3:留出法验证图片

图 7.3:留出法验证

以下练习将数据集分为 70%的训练集和 30%的测试集,并在训练集上构建随机森林模型,然后使用测试集评估性能。这种方法在第五章分类中广泛使用,所以你对此过程不应该感到惊讶。

练习 89:使用留出法进行模型评估

在这个练习中,我们将利用在练习 1加载数据和探索数据中加载到内存中的数据,创建一个简单的随机森林分类模型,并使用留出验证技术进行模型评估。

执行以下步骤:

  1. 首先,使用以下命令将caret包导入系统。caret包为我们提供了用于模型评估的现成函数,即ConfusionMatrix

    library(caret)
    
  2. 现在,按照以下方式设置种子以实现可重复性:

    set.seed(2019)
    
  3. 使用以下命令创建 70%的train和 30%的test数据集:

    train_index<- sample(seq_len(nrow(df)),floor(0.7 * nrow(df)))
    train <- df[train_index,]
    test <- df[-train_index,]
    
  4. 使用print函数显示所需的输出:

    print("Training Dataset shape:")
    print(dim(train))
    print("Test Dataset shape:")
    print(dim(test))
    
  5. 通过对train数据集进行拟合来创建随机森林模型:

    model <-randomForest(diabetes~.,data=train, mtry =3)
    
  6. 使用以下命令打印模型:

    print(model)
    
  7. 如下图所示,在test数据集上使用predict方法:

    y_predicted<- predict(model, newdata = test)
    
  8. 使用以下命令创建并打印Confusion-Matrix

    results<-confusionMatrix(y_predicted, test$diabetes, positive= 'pos')
    print("Confusion Matrix  (Test Data)- ")
    print(results$table)
    
  9. 使用以下命令打印总体准确率:

    results$overall[1]
    

    输出如下:

![图 7.4:使用留出法进行模型评估图片

图 7.4:使用留出法进行模型评估

我们可以看到总体准确率为 77%。这可能不是模型性能的最佳表示,因为我们只在一个随机测试样本上进行了评估。如果使用不同的测试样本,结果可能会有所不同。现在,让我们探索其他可以克服这种权衡的交叉验证方法。

K 折交叉验证

这种技术是模型评估最推荐的方法。在这个技术中,我们将数据分为k组,使用k-1组进行训练,剩余的(1 组)用于验证。这个过程重复k次,在每次迭代中,使用一个新的组进行验证,因此,每个组在某个时刻都会被用于测试。整体结果是k次迭代的平均误差估计。

k折交叉验证因此克服了保留法技术的缺点,通过减轻与分割性质相关的风险,因为每个数据点在k次迭代中都会被测试一次。随着k值的增加,模型的方差降低。最常用的k值是 5 或 10。这种技术的最大缺点是它需要训练模型k次(对于k次迭代)。因此,模型训练和验证所需的总计算时间大约是保留法方法的k倍。

以下可视化演示了五折交叉验证和所有旋转的汇总结果(假设):

图 7.5:K 折验证

图 7.5:K 折验证

以下代码片段在之前示例中使用相同的数据集执行 5 折交叉验证,并打印所有折的平均准确率。

练习 90:使用 K 折交叉验证进行模型评估

我们将利用之前两个练习中使用的相同数据集,构建一个随机森林分类模型样本,并使用 k 折交叉验证评估性能。

要使用 k 折交叉验证方法进行模型评估,请执行以下步骤:

  1. 首先,使用以下命令将caret包导入系统:

    library(caret)
    
  2. 接下来,使用以下命令将seed设置为2019

    set.seed(2019)
    
  3. 现在,使用以下命令定义一个用于五折交叉验证的函数:

    train_control = trainControl(method = "cv", number=5, savePredictions = TRUE,verboseIter = TRUE)
    
  4. mtry的值定义为3(以匹配我们之前的示例):

    parameter_values = expand.grid(mtry=3)
    
  5. 使用以下命令拟合模型:

    model_rf_kfold<- train(diabetes~., data=df, trControl=train_control, 
                    method="rf",  metric= "Accuracy", 
    tuneGrid = parameter_values)
    
  6. 接下来,打印整体准确率(平均所有折):

    model_rf_kfold$results[2]
    
  7. 现在,使用以下命令打印详细的预测数据集:

    print("Shape of Prediction Dataset")
    print(dim(model_rf_kfold$pred))
    print("Prediction detailed results - ")
    head(model_rf_kfold$pred) #print first 6 rows
    tail(model_rf_kfold$pred) #print last 6 rows
    print("Accuracy across each Fold-")
    model_rf_kfold$resample
    print(paste("Average Accuracy :",mean(model_rf_kfold$resample$Accuracy)))
    print(paste("Std. Dev Accuracy :",sd(model_rf_kfold$resample$Accuracy)))
    

    输出如下:

    + Fold1: mtry=3 
    - Fold1: mtry=3 
    + Fold2: mtry=3 
    - Fold2: mtry=3 
    + Fold3: mtry=3 
    - Fold3: mtry=3 
    + Fold4: mtry=3 
    - Fold4: mtry=3 
    + Fold5: mtry=3 
    - Fold5: mtry=3 
    ...
    Accuracy: 0.7590782
    "Shape of Prediction Dataset"
    768   5
    "Prediction detailed results - "
    ...
    "Average Accuracy : 0.759078176725236"
    "Std. Dev Accuracy : 0.0225461480724459"
    

如我们所见,整体准确率略有下降至76%(四舍五入自 75.9)。这是每个折的平均准确率。我们还在最后手动计算了每个折的准确率的平均值和标准差。每个折的准确率标准差为2%,这相当低,因此,我们可以得出结论,方差较低。整体准确率并不低,所以模型具有适度的低偏差。整体性能有改进的空间,但我们的模型目前既不过拟合也没有欠拟合。

如果您观察代码,我们使用了trainControl函数,它为我们提供了必要的结构来定义使用cv方法的交叉验证类型,以及折叠数等于5

我们使用一个额外的结构来指示需要保存预测,我们可以在以后详细分析。然后,将trainControl对象传递给caret包中的train函数,在那里我们定义要使用的算法类型为随机森林,使用rf方法,并将度量作为tuneGrid结构在此点理想上不是必要的;它用于超参数调整,我们将在后面介绍。然而,caret包中的train函数默认通过使用超参数调整来简化函数。它尝试在多次迭代中不同的mtry值,并返回具有最佳值的最终预测。为了与前面的例子进行苹果对苹果的比较,我们必须将mtry的值限制为3。因此,我们使用了expand.grid对象来定义在交叉验证过程中使用的mtry值。

train函数提供用于交叉验证的trainControl对象时,会将数据分为五个部分,并利用四个部分进行训练,一个部分进行测试。这个过程重复五次(k设置为5),模型在数据集的每个部分上迭代测试。

我们可以在模型结果中的pred对象(数据表)中看到详细的结果。在这里,我们可以看到每行数据的观测值(实际值)和预测值。此外,它还标注了在折叠中使用的超参数值以及它所属的折叠编号,用于测试。

model对象中的resample DataFrame 记录了交叉验证中每个折叠的准确性和其他指标。我们可以探索我们感兴趣的指标的均值和标准差,以研究偏差和方差。

k折叠交叉验证中得到的最终结论是,对于该用例,随机森林模型的准确率为 76%(即所有部分的平均准确率)。

单样本留出法验证

在这种技术中,我们将k折叠验证推向了逻辑上的极限。我们不是创建k个部分,其中k可以是 5 或 10,而是选择部分的数量与可用数据点的数量相同。因此,每个部分只有一个样本。我们使用所有样本除了一个用于训练,并在留出的样本上测试模型,重复n次,其中n是训练样本的数量。最后,计算类似于k折叠验证的平均误差。这种技术的缺点是模型被训练了n次,使其计算成本高昂。如果我们处理的是相当大的数据样本,最好避免这种验证方法。

留一法验证也称为Leave-One-Out Cross-Validation (LOOCV)。以下视觉演示了对于n个样本的留一法验证:

图 7.6:留一法验证

图 7.6:留一法验证

以下练习使用随机森林和相同的实验设置在相同的 dataset 上执行留一法或留一法交叉验证。

练习 91:使用留一法验证进行模型评估

练习 2使用留出法进行模型评估练习 3使用 K 折交叉验证进行模型评估类似,我们将继续使用相同的 dataset,并执行留一法验证来评估模型性能。

要使用留一法验证方法进行模型评估,执行以下步骤:

  1. 首先,使用以下命令定义留一法验证的函数:

    set.seed(2019)
    train_control = trainControl(method = "LOOCV", savePredictions = TRUE)
    
  2. 接下来,定义mtry的值为3(以匹配我们之前的示例):

    parameter_values = expand.grid(mtry=3)
    
  3. 拟合模型:

    model_rf_LOOCV<- train(diabetes~., data=df, trControl=train_control, 
                        method="rf",  metric= "Accuracy", 
    tuneGrid = parameter_values)
    
  4. 现在,打印整体准确率(平均所有折):

    print(model_rf_LOOCV$results[2])
    
  5. 使用以下命令打印详细的预测 dataset:

    print("Shape of Prediction Dataset")
    print(dim(model_rf_LOOCV$pred))
    print("Prediction detailed results - ")
    head(model_rf_LOOCV$pred) #print first 6 rows
    tail(model_rf_LOOCV$pred) #print last 6 rows
    

    输出如下:

    Accuracy
    1 0.7721354
    [1] "Shape of Prediction Dataset"
    [1] 768   4
    [1] "Prediction detailed results - "
    "Shape of Prediction Dataset"
     768   4
    "Prediction detailed results - "
    ...
    

如我们所见,整体准确率在77%几乎与K折交叉验证相同(增加了1%)。这里的LOOCV结构代表留一法交叉验证。这个过程计算成本很高,因为它需要迭代与数据点数量一样多的训练过程(在本例中为 768)。

超参数优化

超参数优化是寻找或优化机器学习模型最佳超参数集的过程。超参数是定义机器学习模型宏观特性的参数。它基本上是模型的元参数。超参数与模型参数不同;模型参数是在学习过程中由模型学习的,然而,超参数是由设计模型的科学家设置的,并且不能由模型学习。

为了更直观地理解这个概念,让我们用通俗易懂的语言来探讨这个话题。以决策树模型为例。树结构,包括根节点、决策节点和叶节点(类似于逻辑回归中的 beta 系数)是通过数据的训练(拟合)来学习的。当模型最终收敛(找到模型参数的最优值集)时,我们就得到了最终的树结构,它定义了最终预测的遍历路径。然而,对于模型来说,宏观特征是不同的;在决策树的情况下,它将是复杂度参数,表示为 cp。复杂度参数 cp 限制了树相对于深度的增长;也就是说,如果信息增益或任何其他相关指标没有超过阈值,则不允许节点分支。应用这个新规则限制了树的深度,并有助于更好地泛化树。因此,复杂度参数是一个定义模型宏观特征的参数,然后调整训练过程,我们称之为超参数。

每个机器学习算法都将有一组不同的超参数与之关联,这将帮助模型忽略错误(噪声),从而提高泛化能力。以下表格中展示了机器学习算法中的一些超参数示例:

Figure 7.7:机器学习算法中的超参数

img/C12624_07_07.jpg

图 7.7:机器学习算法中的超参数

注意

在 R 和 Python 提供的实现中,超参数的数量有时是不同的。例如,R 中的 caret 包实现的逻辑回归不调整 c 参数,而与 sklearn 中的 Python 实现不同。同样,Python 的 sklearn 中的随机森林实现允许将树的深度作为超参数使用。

关于基于梯度的超参数优化的信息可以在以下链接中找到:

arxiv.org/abs/1502.03492

proceedings.mlr.press/v37/maclaurin15.pdf

超参数调整的过程可以概括为寻找最优超参数值集的迭代过程,以实现最佳的机器学习模型。有几种方法可以实现这一点。鉴于这个过程是迭代的,我们可以断定会有几种方法来优化寻找最优值集的路径。让我们深入讨论可以采用的广泛策略,用于超参数调整。

网格搜索优化

寻找模型最优超参数集的最简单方法就是使用暴力搜索方法,迭代每个超参数值的组合,然后找到最优化组合。这将产生期望的结果,但不是在期望的时间内。在大多数情况下,我们训练的模型将非常大,需要大量的计算时间进行训练。迭代每个组合不是理想的选择。为了改进暴力搜索方法,我们有了网格搜索优化;正如其名所示,在这里,我们定义了一个值网格,将用于迭代超参数值的全面组合。

用通俗易懂的话来说,对于网格搜索优化,我们为每个我们感兴趣要优化的超参数定义一个有限值的集合。然后,模型将针对所有可能的超参数值的组合进行训练,并选择表现最佳的组合作为最优集。

以下图表展示了针对假设参数集的网格搜索优化理念。使用超参数网格定义组合,并对每个组合进行模型训练:

![图 7.8:超参数网格和组合图片

图 7.8:超参数网格和组合

网格搜索优化的优势在于,它大大减少了在有限的候选值集合上迭代寻找最优超参数集所需的时间(与暴力搜索相比)。然而,这也带来了一定的权衡。网格搜索优化模型假设每个超参数的最优值位于为每个超参数提供的候选值列表中。如果我们不将最佳值作为候选值列入列表(网格),我们将永远不会得到算法的最优值集。因此,在最终确定候选值列表之前,我们需要探索每个超参数最推荐值列表的一些建议。超参数优化最适合经验丰富的数据科学专业人士,他们对于各种不同的机器学习问题都有很强的判断力。

我们为机器学习模型定义超参数,以定制模型的学习(拟合)过程。

练习 92:执行网格搜索优化 – 随机森林

在这个练习中,我们将使用caret包对模型执行网格搜索优化,其中我们定义了一个要测试和评估的最佳模型的值网格。我们将使用与之前主题中相同的随机森林算法和数据集。

执行以下步骤:

  1. 首先,使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 接下来,使用以下命令定义交叉验证方法:

    train_control = trainControl(method = "cv",  number=5, savePredictions = TRUE)
    
  3. 现在,定义parameter_grid如下所示:

    parameter_grid = expand.grid(mtry=c(1,2,3,4,5,6))
    
  4. 使用交叉验证和网格搜索优化拟合模型:

    model_rf_gridSearch<- train(diabetes~., data=df, trControl=train_control, 
                   method="rf",  metric= "Accuracy", 
    tuneGrid = parameter_grid)
    
  5. 打印整体准确率(平均每个超参数组合的所有折数):

    print("Accuracy across hyperparameter Combinations:")
    print(model_rf_gridSearch$results[,1:2])
    

    输出如下:

    [1] "Accuracy across hyperparameter Combinations:"
    mtry  Accuracy
    1    1 0.7564893
    2    2 0.7604108
    3    3 0.7642730
    4    4 0.7668704
    5    5 0.7629658
    6    6 0.7590697
    
  6. 打印详细的预测数据集:

    print("Shape of Prediction Dataset")
    print(dim(model_rf_gridSearch$pred))
    [1] "Shape of Prediction Dataset"
    [1] 4608    5
    print("Prediction detailed results - ")
    print(head(model_rf_gridSearch$pred)) #print the first 6 rows
    print(tail(model_rf_gridSearch$pred)) #print the last 6 rows
    [1] "Prediction detailed results - "
    predobsrowIndexmtry Resample
    1  neg pos       10    1    Fold1
    2  neg pos       24    1    Fold1
    3  neg neg       34    1    Fold1
    4  neg pos       39    1    Fold1
    5  neg neg       43    1    Fold1
    6  neg neg       48    1    Fold1
    predobsrowIndexmtry Resample
    4603  neg neg      752    6    Fold5
    4604  neg neg      753    6    Fold5
    4605  pos pos      755    6    Fold5
    4606  neg neg      759    6    Fold5
    4607  neg neg      761    6    Fold5
    4608  pos pos      762    6    Fold5
    print("Best value for Hyperparameter 'mtry':")
    print(model_rf_gridSearch$bestTune)
    [1] "Best value for Hyperparameter 'mtry':"
    mtry
    4    4
    print("Final (Best) Model ")
    print(model_rf_gridSearch$finalModel)
    [1] "Final (Best) Model "
    Call:
    randomForest(x = x, y = y, mtry = param$mtry) 
                   Type of random forest: classification
                         Number of trees: 500
    No. of variables tried at each split: 4
    OOB estimate of  error rate: 23.7%
    Confusion matrix:
        neg pos class.error
    neg 423  77    0.154000
    pos 105 163    0.391791
    
  7. 绘制网格度量图:

    library(repr)
    options(repr.plot.width=8, repr.plot.height=5)
    plot(model_rf_gridSearch)
    

    输出如下:

图 7.9:随机森林模型在不同超参数值下的准确率

图 7.9:随机森林模型在不同超参数值下的准确率

如我们所见,在准确率方面,使用mtry超参数的值为 4 时取得了最佳结果。输出中高亮的部分将帮助您理解整体总结过程。我们使用了 5 折交叉验证以及网格搜索优化,其中我们为mtry超参数定义了值为(1,2,3,4,5 和 6)的网格。每个值的准确率也显示出来,我们可以看到mtry等于4的结果比其他结果略高。最后,我们还打印了网格搜索优化过程返回的最终模型。

到目前为止,我们只看了随机森林作为实现交叉验证和超参数调整的模型。我们可以将此扩展到任何其他算法。例如XGBoost算法比随机森林(使用 R 实现)有更多的超参数,因此使整个过程在计算上稍微昂贵一些,也更为复杂。在下面的练习中,我们在同一数据集上对 XGBoost 执行 5 折交叉验证,以及网格搜索优化。代码中高亮的部分是针对 XGBoost 的更改。

自动超参数调整:

https://towardsdatascience.com/automated-machine-learning-hyperparameter-tuning-in-python-dfda59b72f8a

练习 93:网格搜索优化 – XGBoost

与上一个练习类似,我们将对 XGBoost 模型执行网格搜索优化,而不是随机森林,并在一组更大的超参数上找到最佳模型。

要对 XGBoost 模型执行网格搜索优化,请执行以下步骤:

  1. 首先,使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 接下来,使用以下命令导入dplyr库:

    library(dplyr)
    
  3. 使用以下命令定义交叉验证方法:

    train_control = trainControl(method = "cv",  number=5, savePredictions = TRUE)
    
  4. 接下来,定义参数网格,如图所示:

    parameter_grid = expand.grid(nrounds = c(30,50,60,100),
                                 eta=c(0.01,0.1,0.2,0.3),
    max_depth = c(2,3,4,5),
                                 gamma = c(1),
    colsample_bytree = c(0.7),
    min_child_weight = c(1)  ,
                                 subsample = c(0.6)
                                )
    
  5. 使用交叉验证和网格搜索优化拟合模型:

    model_xgb_gridSearch<- train(diabetes~., data=df, trControl=train_control, 
                   method="xgbTree",  metric= "Accuracy",
    tuneGrid = parameter_grid)
    
  6. 打印详细的预测数据集:

    print("Shape of Prediction Dataset")
    print(dim(model_xgb_gridSearch$pred))
    "Shape of Prediction Dataset"
      49152    11
    print("Prediction detailed results - ")
    head(model_xgb_gridSearch$pred) #print the first 6 rows
    tail(model_xgb_gridSearch$pred) #print the last 6 rows
    [1] "Prediction detailed results - "
    predobsrowIndex  eta max_depth gamma colsample_bytreemin_child_weight subsample nrounds Resample
    1  pos pos        3 0.01         2     1              0.7                1       0.6     100    Fold1
    2  neg neg        6 0.01         2     1              0.7                1       0.6     100    Fold1
    3  neg pos       20 0.01         2     1              0.7                1       0.6     100    Fold1
    4  pos pos       23 0.01         2     1              0.7                1       0.6     100    Fold1
    5  pos pos       25 0.01         2     1              0.7                1       0.6     100    Fold1
    6  pos pos       27 0.01         2     1              0.7                1       0.6     100    Fold1
    predobsrowIndex eta max_depth gamma colsample_bytreemin_child_weight subsample nrounds Resample
    49147  neg pos      732 0.3         5     1              0.7                1       0.6      60    Fold5
    49148  neg pos      740 0.3         5     1              0.7                1       0.6      60    Fold5
    49149  neg neg      743 0.3         5     1              0.7                1       0.6      60    Fold5
    49150  pos pos      749 0.3         5     1              0.7                1       0.6      60    Fold5
    49151  neg pos      751 0.3         5     1              0.7                1       0.6      60    Fold5
    49152  neg neg      763 0.3         5     1              0.7                1       0.6      60    Fold5
    print("Best values for all selected Hyperparameters:")
    model_xgb_gridSearch$bestTune
    [1] "Best values for all selected Hyperparameters:"
    nroundsmax_depth eta gamma colsample_bytreemin_child_weight subsample
    27      60         4 0.1     1              0.7                1       0.6
    
  7. 打印整体准确率(平均每个超参数组合的所有折数):

    print("Average results across different combination of Hyperparameter Values")
    model_xgb_gridSearch$results %>% arrange(desc(Accuracy)) %>% head(5)
    

    输出如下:

    [1] "Average results across different combination of Hyperparameter Values"
       eta max_depth gamma colsample_bytreemin_child_weight subsample nrounds  Accuracy     Kappa AccuracySD
    1 0.10         4     1              0.7                1       0.6      60 0.7695612 0.4790457 0.02507631
    2 0.01         3     1              0.7                1       0.6      30 0.7695442 0.4509049 0.02166056
    3 0.01         2     1              0.7                1       0.6     100 0.7695187 0.4521142 0.03373126
    4 0.30         2     1              0.7                1       0.6      30 0.7682540 0.4782334 0.01943638
    5 0.01         5     1              0.7                1       0.6      30 0.7682455 0.4592689 0.02836553
    KappaSD
    1 0.05067601
    2 0.05587205
    3 0.08038248
    4 0.04249313
    5 0.06049950
    
  8. 绘制图表:

    plot(model_xgb_gridSearch)
    

    输出如下:

图 7.10:可视化 XGBoost 模型在不同超参数值下的准确率

图 7.10:可视化 XGBoost 模型在不同超参数值下的准确率

练习的输出可能看起来相当长,但让我们快速总结一下结果。由于我们在 XGBoost 上执行交叉验证和超参数调整,我们需要为更多的超参数提供一个网格。输出中的第一行表明预测数据集的大小为 49152 x 11。这表明了在交叉验证的每个折叠中,每个超参数组合的全面预测。我们已经打印了预测数据集的头部和尾部(数据的前六行和后六行),我们可以看到每个模型实例的预测结果以及相关的超参数,以及相应的折叠。

下一个表格显示了基于模型准确度的最佳超参数值集。我们可以看到,nrounds=60max_depth=3eta=0.01gamma=1colsample_bytree=0.7min_child_weight=1subsample=0.6的值返回了模型的最佳性能。

下一个表格显示了按性能降序排列的每个超参数组合的对应准确度。最佳准确度位于表格的第一行,这是使用最佳超参数集实现的。我们实现了76.8%的准确度。

最后,我们在超参数上绘制了结果。鉴于超参数的数量较多,我们有一个更密集的图表来展示结果。然而,我们可以直接检查eta=0.01象限的结果,并研究最大深度和nrounds的变化,从而得出最佳性能来自相同超参数组合的结论。

随机搜索优化

在随机搜索优化中,我们克服了网格搜索优化的一个缺点,即在每个超参数的网格中从候选值中选择最佳的一组最优值。在这里,我们选择从分布中随机选择(对于超参数的连续值),而不是我们定义的静态列表。在随机搜索优化中,我们有更广泛的选项可供搜索,因为超参数的连续值是从分布中随机选择的。这在很大程度上增加了找到超参数最佳值的机会。

我们中的一些人可能已经开始了对随机选择如何始终有可能包含超参数的最佳值的理解。真正的答案是,它并不总是比网格搜索有绝对优势,但通过相当大量的迭代,随机搜索相对于网格搜索找到更优超参数集的机会会增加。可能会有随机搜索在给定随机选择值的情况下,对于超参数调整返回比网格搜索更差值的情况,然而,大多数数据科学专业人士都有实证验证,即相当数量的迭代后,随机搜索在大多数情况下优于网格搜索。

随机搜索优化的实现已在caret包中简化。我们必须定义一个名为tuneLength的参数,它将为随机搜索设置一个最大迭代次数的上限。迭代次数相当于模型将被训练的次数,因此数值越高,获得最佳超参数集和关联性能提升的机会就越高。然而,迭代次数越高,执行所需的计算时间也就越长。

在以下练习中,让我们对相同的数据集在随机森林算法上执行随机搜索优化。

练习 94:在随机森林模型上使用随机搜索优化

我们将扩展随机搜索优化在机器学习模型中的优化过程。在这里,我们只定义我们希望执行的迭代次数,这些迭代次数将使用随机组合的超参数值对模型进行。

本练习的目的是在随机森林模型上执行随机搜索优化。

执行以下步骤:

  1. 首先,使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 定义交叉验证方法,如图所示:

    train_control = trainControl(method = "cv",  number=5, savePredictions = TRUE)
    
  3. 使用交叉验证和随机搜索优化拟合模型:

    model_rf_randomSearch<- train(diabetes~., data=df, trControl=train_control, 
                            method="rf",  metric= "Accuracy",tuneLength = 15)
    
  4. 打印详细的预测数据集:

    print("Shape of Prediction Dataset")
    print(dim(model_rf_randomSearch$pred))
    [1] "Shape of Prediction Dataset"
    [1] 5376    5
    print("Prediction detailed results - ")
    head(model_rf_randomSearch$pred) #print the first 6 rows
    tail(model_rf_randomSearch$pred) #print the last 6 rows
    [1] "Prediction detailed results - "
    predobsrowIndexmtry Resample
    1  pos pos        1    2    Fold1
    2  neg neg        4    2    Fold1
    3  pos pos        9    2    Fold1
    4  neg pos       10    2    Fold1
    5  neg neg       13    2    Fold1
    6  pos pos       17    2    Fold1
    predobsrowIndexmtry Resample
    5371  neg neg      737    8    Fold5
    5372  neg neg      742    8    Fold5
    5373  neg neg      743    8    Fold5
    5374  neg pos      758    8    Fold5
    5375  neg neg      759    8    Fold5
    5376  neg neg      765    8    Fold5
    print("Best values for all selected Hyperparameters:")
    model_rf_randomSearch$bestTune
    [1] "Best values for all selected Hyperparameters:"
    mtry
    7    8
    
  5. 打印总体准确率(平均每个超参数组合的所有折的准确率):

    model_rf_randomSearch$results %>% arrange(desc(Accuracy)) %>% head(5)
    

    输出如下:

    mtry  Accuracy     Kappa AccuracySDKappaSD
    1    8 0.7838299 0.5190606 0.02262610 0.03707616
    2    7 0.7773194 0.5047353 0.02263485 0.03760842
    3    3 0.7760037 0.4945296 0.02629540 0.05803215
    4    6 0.7734063 0.4964970 0.02451711 0.04409090
    5    5 0.7720907 0.4895592 0.02618707 0.04796626
    
  6. 绘制随机森林模型随机搜索优化的数据:

    plot(model_rf_randomSearch)
    

    输出如下:

图 7.11:可视化超参数值下的准确率

图 7.11:可视化超参数值下的准确率

我们将tuneLength参数设置为15;然而,由于 R 中的随机森林只关注 1 个超参数的调优,即mtry(特征选择数量),迭代次数在7次时耗尽。这是因为数据集中只有八个独立变量。在大多数一般情况下,建议根据数据中的特征数量设置一个更高的数值。我们可以看到mtry的最佳值是在 7 时找到的。该图展示了不同mtry值之间的差异。使用此模型我们达到的最佳准确率是 76%。

现在我们尝试使用 XGBoost 进行相同的实验。在这里,我们将tuneLength设置为35,这将非常耗时,即15 x 5(折)= 75模型迭代。这将比之前的任何迭代都要长得多。如果您想更快地看到结果,您可能需要通过减少tuneLength的迭代次数。

练习 95:随机搜索优化 – XGBoost

与随机森林类似,我们将对 XGBoost 模型进行随机搜索优化。XGBoost 模型有更多的超参数需要调优,因此更适合随机搜索优化。我们将利用与之前练习相同的相同数据集来构建 XGBoost 模型,然后进行优化。

本练习的目的是对 XGBoost 模型进行随机搜索优化。

执行以下步骤:

  1. 使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 使用以下命令定义交叉验证方法:

    train_control = trainControl(method = "cv",  number=5, savePredictions = TRUE)
    
  3. 使用交叉验证和随机搜索优化来拟合模型:

    model_xgb_randomSearch<- train(diabetes~., data=df, trControl=train_control, 
                                   method="xgbTree", metric= "Accuracy",tuneLength = 15)
    
  4. 打印详细的预测数据集:

    print("Shape of Prediction Dataset")
    print(dim(model_xgb_randomSearch$pred))
    print("Prediction detailed results - ")
    head(model_xgb_randomSearch$pred) #print the first 6 rows
    tail(model_xgb_randomSearch$pred) #print the last 6 rows
    print("Best values for all selected Hyperparameters:")
    model_xgb_randomSearch$bestTune
    
  5. 打印总体准确率(平均每个超参数组合的所有折的准确率):

    model_xgb_randomSearch$results %>% arrange(desc(Accuracy)) %>% head(5)
    

    输出如下:

    "Shape of Prediction Dataset"
    10368000       11
    "Prediction detailed results - "
    

![图 7.12:详细的预测结果图片 C12624_07_12.jpg

图 7.12:详细的预测结果

使用随机搜索优化,我们可以看到为 XGBoost 模型选定的不同参数集作为最佳组合。在这里,请注意,网格搜索和随机搜索的准确率相同(差异很小),然而参数值却完全不同。学习率(eta)为 0.4,max_depth为 1 而不是 3,colsample_byTree为 0.8 而不是 0.7,nrounds为 50 而不是 60。我们没有将这些值作为网格搜索的候选值。在随机搜索中,由于有更广泛的选项可供选择,与网格搜索相比,我们可能会得到更有希望的结果。然而,这却是以更高的计算时间为代价的。

此外,当前使用的当前数据集是一个较小的数据集(约 800 个样本)。随着用例变得更加有说服力(具有更多特征和更多数据),随机搜索和网格搜索之间的性能差异可能会更大。

作为一项经验法则,强烈建议在问题空间判断不显著的情况下,选择随机搜索而不是网格搜索。

贝叶斯优化

网格搜索和随机搜索之间的一项主要权衡是,这两种技术都不跟踪用于模型训练的超参数组合的过去评估。理想情况下,如果在这个路径中引入一些人工智能,能够指示所选超参数列表的历史性能以及通过在正确方向上推进迭代来提高性能的机制,这将大大减少找到超参数最优值所需迭代的数量。然而,网格搜索和随机搜索在这方面有所欠缺,它们会迭代所有提供的组合,而不考虑任何来自先前迭代的线索。

通过贝叶斯优化,我们通过开发一个将超参数映射到所选损失函数(目标函数)的概率得分的概率模型,从而克服了这个权衡,该模型能够跟踪先前迭代及其评估。这个概率模型也被称为机器学习模型中主要损失函数的代理模型,与损失函数相比,它更容易优化。

讨论该过程的数学背景和推导将超出本章的范围。贝叶斯优化中用于超参数调整的整体过程可以简化如下:

  • 定义一个代理模型(超参数到损失函数的概率映射)。

  • 找到代理模型的最佳参数。

  • 将参数应用于主要损失函数,并根据结果在正确的方向上更新模型。

  • 重复执行,直到达到定义的迭代性能。

使用这个简单的框架,贝叶斯优化在大多数情况下都通过最少的迭代次数提供了理想的超参数集。由于这种方法跟踪了过去的迭代及其关联的性能,因此随着数据量的增加,其实践变得更加准确。贝叶斯方法效率高且更实用,因为它们在许多方面类似于人脑;对于任何要执行的任务,我们试图理解世界的初始观点,然后根据新的经验改进我们的理解。贝叶斯超参数优化利用了相同的原理,并允许通过明智的决策在调整模型超参数时找到优化的路径。Bergstra 等人http://proceedings.mlr.press/v28/bergstra13.pdf)的一篇论文清楚地解释了贝叶斯优化相对于随机搜索优化的优势。

贝叶斯优化中有几种技术可以应用于机器学习领域的超参数调整。贝叶斯方法的流行形式是序列模型优化(SMBO),它本身又有几种变体。SMBO 中的每种方法都根据代理模型的定义和用于评估和更新参数的准则而有所不同。代理模型的一些流行选择包括高斯过程随机森林回归树帕累托估计器(TPE)。同样,基于每次迭代后的标准在优化过程中被评估并利用,例如使用UCB(即高斯过程上置信界),例如期望改进(EI)或改进概率(POI)。

在 R 中实现贝叶斯优化,我们已经有了一些编写良好的库,可以为我们抽象整个流程。MlBayesOpt是由 Yuya Matsumura 在 R 中实现的流行包。它基于 SMBO(序列模型优化)中的高斯过程进行贝叶斯优化,并允许使用多个函数来更新代理模型。

以下练习对随机森林模型中的mtry和最小节点大小超参数进行贝叶斯优化。优化过程的输出返回每个超参数评估的最佳组合。然后我们需要实现一个使用相同组合的超参数值的常规模型。

贝叶斯优化:

towardsdatascience.com/the-intuitions-behind-bayesian-optimization-with-gaussian-processes-7e00fcc898a0

papers.nips.cc/paper/7838-automating-bayesian-optimization-with-bayesian-optimization.pdf

towardsdatascience.com/a-conceptual-explanation-of-bayesian-model-based-hyperparameter-optimization-for-machine-learning-b8172278050f

练习 96:在随机森林模型上执行贝叶斯优化

对相同的数据集进行贝叶斯优化并研究输出。在这种优化技术中,我们将利用贝叶斯优化通过迭代前一次迭代的已知/上下文知识直观地选择mtry超参数的最佳值。

本练习的目的是对随机森林模型进行贝叶斯优化。

执行以下步骤:

  1. 首先,使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 使用以下命令导入MlBayesOpt库:

    library(MlBayesOpt)
    
  3. 使用MlBayesOpt包中的rf_opt函数对随机森林模型进行贝叶斯优化:

    model_rf_bayesain<- rf_opt(train_data = train,
    train_label = diabetes,
    test_data = test,
    test_label = diabetes,
    mtry_range = c(1L, ncol(df)-1),
    num_tree = 50,
    init_points = 10,
    n_iter = 10,                       
    acq = "poi", eps = 0, 
    optkernel = list(type = "exponential", power =2))
    

    输出如下:

![图 7.13:贝叶斯优化随机森林的输出

图 7.13:贝叶斯优化随机森林的输出

输出显示了用于模型评估的迭代次数,并在每次迭代的末尾返回结果。可以通过增加n_iter的值来增加迭代次数。init_points变量定义了在贝叶斯优化拟合高斯过程之前随机选择的点的数量以采样目标函数。此外,我们定义了标准函数作为eps参数,这是一个额外的参数,可以用来调整EIPOI,以平衡利用和探索,增加 epsilon 将使优化的超参数在整个范围内分布得更广。

最后,我们还定义了核,即底层高斯过程的关联函数。此参数应是一个列表,指定了关联函数的类型以及平滑度参数。流行的选择是平方指数(默认)或Matern 5/2

贝叶斯优化过程的结果返回了mtry的最佳值为 6.12(需要截断为 6,因为随机森林中的mtry不接受小数值)和最佳最小节点大小为 17。我们可以使用这些参数设置在随机森林的常规实现中,并评估模型结果。

现在我们使用相同的方法对同一用例的 XGBoost 模型进行操作。

练习 97:使用 XGBoost 进行贝叶斯优化

与之前的练习类似,我们将对同一数据集进行贝叶斯优化,并研究输出,尽管这次是针对 XGBoost 模型。鉴于 XGBoost 有更多的超参数需要优化,我们需要为优化过程中感兴趣的每个超参数提供一个值范围。

要在 XGBoost 模型上执行贝叶斯优化,请执行以下步骤:

  1. 首先,使用以下命令将seed设置为2019

    set.seed(2019)
    
  2. 使用MlBayesOpt包中的xgb_opt函数对 XGBoost 模型进行贝叶斯优化:

    model_xgb_bayesian<- xgb_opt(train, diabetes, test, diabetes,objectfun ='binary:logistic',evalmetric='logloss',eta_range = c(0.1, 1L), max_depth_range = c(2L, 8L),nrounds_range = c(70, 160L), bytree_range = c(0.4, 1L), init_points = 4, n_iter = 10, acq = "poi", eps = 0, optkernel = list(type = "exponential", power =2))
    

    输出如下:

图 7.14:使用 XGBoost 进行贝叶斯优化的输出

图 7.14:使用 XGBoost 进行贝叶斯优化的输出

与随机森林类似,xgb_opt函数返回了包含候选值的最佳超参数列表。与随机森林不同,XGBoost 有更多的超参数。我们需要定义贝叶斯优化过程为每个超参数操作的值范围。

我们可以看到,贝叶斯优化得到的最佳超参数组合与我们为nrounds找到的不同,当我们实现标准的 XGBoost 树模型时,上述参数(nroundsmax_depthetasubsample)需要截断(因为小数值没有意义)。

在本章中,我们研究了各种交叉验证技术以进行模型评估。对 k 折交叉验证的小幅修改有助于我们进行改进的模型性能验证。在从 k 折到 LOOCV 之前,我们可以进行重复的 k 折,以获得对模型更稳健的评估。这个过程重复进行交叉验证多次,其中每次重复中折的划分方式都不同。进行重复的 k 折比 LOOCV 更好。

活动十二:执行重复的 K 折交叉验证和网格搜索优化

在这个活动中,我们将利用相同的数据库(如前述练习中使用),训练一个随机森林模型,进行 10 次重复的 k 折验证,并研究模型性能。在每个折迭代中,我们可以尝试不同的超参数网格值,并对最佳模型进行稳健的验证。

活动的目的是对同一模型进行重复的 k 折交叉验证和网格搜索优化。

执行以下步骤:

  1. 加载所需的包(mlbenchcaretdplyr)。

  2. mlbench包中将PimaIndianDiabetes数据集加载到内存中。

  3. 设置一个种子值以实现可重复性。

  4. 使用caret包中的trainControl函数定义 k 折验证对象,并将method定义为repeatedcv而不是cv

  5. trainControl函数中定义一个额外的结构,用于验证重复次数,repeats = 10

  6. 将随机森林模型的mtry超参数的网格定义为(3,4,5)

  7. 使用网格值、交叉验证对象和随机森林分类器来拟合模型。

  8. 通过绘制不同超参数值下的准确率来研究模型性能。

    最终输出应如下所示:

图 7.15:不同超参数值下的模型性能准确率

图 7.15:不同超参数值下的模型性能准确率

注意

本活动的解决方案可以在第 461 页找到。

摘要

在本章中,你学习了模型性能改进技术的一些重要方面。我们从偏差-方差权衡开始,并了解了它对模型性能的影响。我们现在知道,高偏差会导致欠拟合,而高方差会导致模型过拟合,并且实现一个是以牺牲另一个为代价的。因此,为了构建最佳模型,我们需要在机器学习模型中找到偏差和方差之间的理想平衡。

接下来,我们探讨了 R 中各种交叉验证技术,这些技术提供了现成的函数来实现相同的功能。我们研究了保留法、k 折法和留一法验证方法,了解了如何对机器学习模型的性能进行稳健评估。然后,我们研究了超参数调整,并详细探讨了网格搜索优化、随机搜索优化和贝叶斯优化技术。机器学习模型的超参数调整帮助我们开发出性能更优的更通用模型。

在下一章中,我们将探讨在云中部署机器学习模型的过程。

第八章:模型部署

学习目标

到本章结束时,你将能够:

  • 使用 R plumber 包将 ML 模型部署为 API

  • 使用 AWS SageMaker、AWS Lambda 和 AWS API Gateway 开发无服务器 API

  • 使用 AWS CloudFormation 从零开始创建基础设施

  • 使用 Docker 容器将 ML 模型部署为 API

在本章中,我们将学习如何在 AWS 和 Docker 容器上托管、部署和管理模型。

简介

在上一章中,我们研究了模型改进,并探讨了超参数调优中的各种技术,以提升模型性能并开发出针对特定用例的最佳模型。下一步是将机器学习模型部署到生产环境中,以便它可以轻松被消费或集成到大型软件产品中。

大多数数据科学专业人士认为,当最佳模型已经确定时,开发机器学习模型的过程就结束了,即超参数调优。然而,如果机器学习模型没有被部署并且(或)与其他软件服务/产品集成到大型技术生态系统中,那么它所带来的价值和影响是有限的(大多数情况下是徒劳的)。机器学习和软件工程绝对是两个不同的学科。大多数数据科学家在理解软件工程生态系统方面能力有限,同样,软件工程师对机器学习领域的理解也有限。因此,在大型企业中,他们构建的产品中机器学习用例发展成为软件产品的主要功能时,数据科学家和软件工程师之间的协作是必要的。然而,在大多数情况下,软件工程师和数据科学家之间的协作极其具有挑战性,因为双方都认为对方的领域难以理解。

近年来,大型企业投入了大量努力开发工具和资源,以帮助数据科学家轻松地接受一些软件工程组件,反之亦然。这些工具使得两个学科之间的协作更加容易,加速了开发大规模、企业级机器学习产品的过程。

在本章中,我们将了解一些将机器学习模型作为网络服务部署的方法,这些方法可以轻松地与其他服务集成到大型软件生态系统中。我们还将讨论不同方法的优缺点以及模型部署的最佳实践。

什么是 API?

在深入研究模型部署的具体细节之前,我们需要研究一个重要的软件工程主题,它可以简化整个模型部署过程,那就是应用程序编程接口,通常简称为API。API 是一组明确定义的用于各种软件组件之间通信的方法。随着 API 的出现,软件开发变得显著更容易。如果一个开发者,比如说,想要开发一个 iPhone 应用,该应用可以为图片添加一些滤镜,他们不需要编写整个代码来从手机的相机捕获图片,将其保存到库中,然后再应用他们特定的滤镜。相反,他们可以使用手机相机 API,这提供了一个简单的方法与相机通信,并且只需专注于编写添加滤镜到图片的代码。简而言之,API 是异构软件组件之间通信的手段。

在一个大型软件产品中,会有几个负责特定任务的组件。这些组件通过一种定义良好的语言相互交互,确保数据、事件和警报的顺畅通信。

这里是 API 的一些显著特点:

  • API 有助于模块化软件应用程序,并使构建更好的产品成为可能。

  • API 通常为软件工程师所熟知,并且是语言无关的。因此,在完全不同的语言或系统中开发的异构应用程序也可以有效地相互通信。

  • 服务之间的通信也使用一种通用语言,即JavaScript 对象表示法(简称,JSON)。然而,还有其他流行的语言,例如,XML。

  • 它们也支持 HTTP,这意味着 API 可以通过网络浏览器(如 Google Chrome 或 Mozilla Firefox)或像Postman这样的工具访问。

    备注

    Postman 是一个免费工具,它在整个 API 的生命周期中提供易于使用的服务,例如设计、调试、测试、文档、监控和发布。它可在 Windows、Linux 和 macOS 平台上下载。您可以在 https://www.getpostman.com/了解更多关于 Postman 的信息。

我们特别关注 RESTful API 的发展,即通过 HTTP 进行通信的 API。RESTful API 也被称为REST API,主要有两种方法类型:

  • GET 方法:当我们想要从服务中读取数据时使用。

  • POST 方法:当我们想要向服务发送数据时使用。

一些其他的方法是headputdelete

将机器学习模型作为(Web 服务)REST API 部署简化了将服务与其他服务集成的过程。软件工程师喜欢使用 REST API,由于该服务是语言无关的,我们在用我们选择的语言开发模型时具有巨大的优势。软件工程师可以使用 Python、Java、Ruby 或许多其他语言来开发其他服务,而我们可以用 R、Python、Julia 等语言开发模型,并且能够有效地无缝地将服务集成到大型软件产品中。

现在我们对 API 有了相当的了解,让我们来了解一下如何将一个 ML 模型部署到 R 中作为 API。

Plumber 简介

Plumber是一个 R 包,它帮助将 R 函数转换为 HTTP API,可以从网络中的其他机器调用,从而实现系统间的通信。通过使用 R plumber,我们将能够实现所讨论的优势,例如开发模块化、语言无关、基于通用通信语言(JSON)的 HTTP rest API,这些 API 为系统间提供定义的通信路径。使用 Plumber 非常简单。只需几行代码,我们就可以将现有的 R 函数转换为可以作为一个端点提供服务的 Web 服务。

在本章中,我们将扩展我们在第七章模型改进中构建的相同模型和用例,使用mlbench库中的PimaIndiasDiabetes数据集来分类患者是否患有糖尿病。稍后,我们将扩展相同的用例,使用 Docker 容器和无服务器应用程序将模型部署为 Web 服务。

练习 98:使用 Plumber 开发 ML 模型并将其作为 Web 服务部署

在这个练习中,我们将使用三个独立变量开发一个逻辑回归模型,并使用 Plumber 将其作为 REST API 部署。我们将创建一个简单的二分类模型,并使用plumber包的服务通过定义 HTTP get 和 post 方法将模型封装为 API。

执行以下步骤以完成练习:

  1. 使用 RStudio 或 Jupyter Notebook 创建一个名为model.R的 R 脚本。

  2. 加载所需的库并构建一个逻辑回归模型。现在,定义接受输入参数并返回预测结果的get方法:

    #----------Code listing for model.R-----------------------------
    library(mlbench)
    data(PimaIndiansDiabetes)
    df<-PimaIndiansDiabetes
    

    注意

    这些数据是从以下 URL 的 UCI 机器学习数据库仓库中获取的:

    ftp://ftp.ics.uci.edu/pub/machine-learning-databases

    http://www.ics.uci.edu/~mlearn/MLRepository.html

    它被 Friedrich Leisch 转换成了 R 格式。

  3. 使用df DataFrame 对象训练一个逻辑回归模型:

    model<-glm(diabet-es~pregnant+glucose+pressure, data=df,
               family=binomial(link='logit'))
    
  4. 使用额外的#' @get /构造定义 API 端点为一个函数:

    #' @get /predict_data
    function(pregnant, glucose, pressure){
    
  5. 在函数内部,使用as.numeric命令将参数转换为numeric值:

      pregnant <- as.numeric(pregnant)
      glucose <- as.numeric(glucose)
      pressure <- as.numeric(pressure)
    
  6. 然后,创建一个与上一步相同的列名的 DataFrame:

      sample <- data.frame(pregnant=pregnant,                       glucose=glucose,                       pressure=pressure)
    
  7. 使用新创建的sample DataFrame 对训练好的模型进行预测:

      y_pred<-ifelse(predict(model,newdata=sample)>0.5,"yes","no")
    
  8. 将结果打包为一个list作为函数的返回调用,并使用}括号完成/关闭函数定义:

      list(Answer=y_pred)
    }
    

    之前的练习演示了带有额外结构的常规 R 模型代码。我们开发的模型相当简单,只有三个自变量和一个因变量(与我们在第七章模型改进中看到的八个自变量不同)。我们还创建了一个函数,该函数将接受三个输入参数,每个参数代表模型的一个自变量。当我们将模型作为 REST API 部署时,这个函数将被用作端点。我们在函数之前添加了一个额外的结构(参考之前练习的第四步):

    #' @get /predict_data
    

    此结构定义了predict_data端点将服务 GET 请求。我们为该端点定义的函数接受三个参数,没有默认值。现在让我们安装plumber包,并使用 web 服务器进行调用。

    最终完整的model.R文件应如下所示:

    #----------Code listing for model.R-----------------------------
    library(mlbench)
    data(PimaIndiansDiabetes)
    df<-PimaIndiansDiabetes
    model<-glm(diabet-es~pregnant+glucose+pressure, data=df,
               family=binomial(link='logit'))
    #' @get /predict_data
    function(pregnant, glucose, pressure){
      pregnant <- as.numeric(pregnant)
      glucose <- as.numeric(glucose)
      pressure <- as.numeric(pressure)
      sample <- data.frame(pregnant=pregnant,                       glucose=glucose,                       pressure=pressure)
      y_pred<-ifelse(predict(model,newdata=sample)>0.5,"yes","no")
      list(Answer=y_pred)
    }
    

    注意

    你也可以参考 GitHub URL:https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R/blob/master/Lesson08/Docker/model.R。

  9. 创建另一个名为main.R的 R 脚本。现在,安装plumber包并将其加载到内存中,然后使用plumb函数部署 R 函数,如图所示:

    #-------main.R-----------------
    install.packages("plumber")
    library(plumber)
    # (The previous code snippet is supposed to be save as 'model.R')
    
  10. 使用以下命令将 R 文件传递给plumb函数:

    pr <- plumber::plumb("model.R")
    pr$run(port=8080,host='127.0.0.1')
    
  11. 执行上一段代码后,Plumber 库会在你的localhost上创建一个 web 服务器并响应请求。为了测试我们编写的端点是否按预期工作,我们将从浏览器中调用该端点。

    注意

    #' @get /predict_data
    

    参数在?符号之后传递给端点,多个参数由&符号分隔。

  12. 由于我们将端点部署到了 localhost,即127.0.0.18080端口,我们将为该端点有以下 API 定义。使用浏览器调用 API:

    http://127.0.0.1:8080/predict_data?pregnant=1&glucose=3&pressure=1
    
  13. 执行之前的 API 定义,将返回以下预测值:

    {"Answer":["no"]}
    

    为了测试 API,我们可以使用更好的工具,例如 Postman,而不是浏览器。Postman(https://www.getpostman.com/)是目前在 API 测试中最受欢迎的工具之一。它在 Windows、Mac 和 Linux 平台上免费提供。使用 Postman 相对简单,不涉及任何新的学习。

    注意

    如果你想要探索关于 Postman 的更多细节,你可以查看在 https://learning.getpostman.com/docs/postman/launching_postman/installation_and_updates/提供的资源。

  14. 下载并安装 Postman 到你的系统后,你可以通过粘贴到输入窗口来测试 API 端点,如下面的截图所示:

图 8.1:Plumber UI

图 8.1:Plumber UI

我们可以通过点击发送按钮来执行 API,结果将显示在上一张截图的高亮区域。通过观察 Postman 的输出,我们可以看到我们的机器学习模型已成功部署为 API 端点。

使用 plumber 部署模型的挑战

我们可以看到,使用 plumber 将模型部署为 API 非常简单,只需添加几行额外的代码即可轻松完成。plumber包提供了我们在本次练习中未探索的额外功能。以下是一些可能感兴趣的重要主题:

这些方法有一个主要的缺点。虽然在一个单一的主机上设置端点相对容易,但在不同的系统上部署相同的解决方案时可能会遇到问题。这些问题可能由于系统架构、软件版本、操作系统等方面的差异而产生。为了减轻在部署端点时可能遇到的冲突,一种技术是使过程与环境无关,也就是说,在一个主机系统上开发的解决方案可以在任何具有不同架构、平台或操作系统的其他主机上无任何问题地部署。这可以通过使用Docker 容器和 plumber 进行部署来实现,而不是直接使用 plumber。

预 Docker 时代的简史

在深入探讨 Docker 工具之前,让我们了解一些背景和历史。

在环境无关的框架中部署应用程序的挑战之前是通过虚拟化实现的,也就是说,整个应用程序、依赖项、库、必要的框架以及操作系统本身都被虚拟化并打包成一个可以在主机上部署的解决方案。多个虚拟环境可以在基础设施(称为虚拟机管理程序)上运行,应用程序变得与环境无关。然而,这种方法有一个主要的权衡。将整个操作系统打包到应用程序的虚拟机VM)中使得包变得很重,通常会导致内存和计算资源的浪费。

解决这个问题的更直观的方法是排除操作系统包,只包含与应用程序相关的库和依赖项。此外,启用一种机制,使包成为基础设施无关的,保持应用程序轻量。这就是 Docker 被引入的时候。以下视觉图展示了 Docker 是如何被改进来解决以前由虚拟机解决的问题:

![图 8.2:虚拟机和 Docker 容器之间的架构差异

![img/C12624_08_02.jpg]

图 8.2:虚拟机和 Docker 容器之间的架构差异

让我们现在更详细地了解 Docker 容器。

Docker

Docker 是一个简单的工具,它简化了使用容器开发、部署和执行软件应用程序的过程。容器类似于航运行业的集装箱,允许开发者将整个应用程序及其依赖项打包,并作为一个包发送出去。一旦在一个系统上构建,这个包就可以在任何其他系统上工作,无论基础设施有何差异。

使用 Docker,我们可以创建一个单独的文档(称为Dockerfile),它定义了设置应用程序所需环境的简化步骤。然后使用 Dockerfile 构建Docker 镜像。容器是 Docker 镜像的一个实例。对于同一个应用程序,我们有时可能会有多个容器,这有助于高流量应用程序的负载均衡。

使用 Docker 和 plumber 部署 ML 模型

我们将利用相同的 plumber 应用程序,通过 Docker 进行环境无关和基础设施无关的部署。首先,我们需要在我们的系统上下载并安装 Docker。它是免费且易于使用的。从 https://www.docker.com/get-started 创建一个账户并下载适合您系统的 Docker。安装完成后,您可以通过在终端或 Windows PowerShell 中执行docker命令来验证 Docker 是否正在运行。

练习 99:为 R plumber 应用程序创建一个 Docker 容器

在这个练习中,我们将扩展之前创建的 plumber 应用程序作为 Docker 容器。为了开发可以在任何生产系统上部署且无任何问题的环境无关模型,我们可以使用 Docker 容器部署 plumber 应用程序。这些容器然后可以部署在支持 Docker Engine 的任何其他机器上。

执行以下步骤以完成练习:

  1. 定义一个 Dockerfile,如下所示:

    FROM rocker/r-ver:3.5.0
    
  2. 现在,使用以下命令安装plumber包的库:

    RUN apt-get update -qq && apt-get install -y  libssl-dev  libcurl4-gnutls-dev
    
  3. 接下来,按照以下所示安装plumber包:

    RUN R -e "install.packages(c('plumber','mlbench'))"
    
  4. 现在,使用以下命令将当前目录中的所有文件复制到容器的当前文件夹中。这将把我们的model.Rplumber.R文件复制到容器中:

    COPY / /
    
  5. 接下来,定义一个要公开的端口,容器将部署在该端口上:

    EXPOSE 8080
    
  6. 定义在构建后容器启动时运行的第一个脚本:

    ENTRYPOINT ["Rscript", "main.R"]
    

    我们现在在项目文件夹中有三个文件,如下所示图所示。请注意,Dockerfile 是一个没有扩展名的简单文本文件。在文件夹内从终端运行build命令时,Docker 引擎会查找 Dockerfile 并根据文档中提供的说明准备环境:

    ![图 8.3:项目文件]

    图 8.3:项目文件

    图 8.3:项目文件

    最终的 Dockerfile,其中包含之前步骤中定义的所有命令,将看起来像这样:

    FROM rocker/r-ver:3.5.0
    RUN apt-get update -qq && apt-get install -y  libssl-dev  libcurl4-gnutls-dev
    RUN R -e "install.packages(c('plumber','mlbench'))"
    COPY / /
    EXPOSE 8080
    ENTRYPOINT ["Rscript", "main.R"]
    
  7. 我们现在可以使用docker build命令从 Dockerfile 构建 Docker 镜像,如下所示:

    docker build -t r_ml_demo .
    

    r_ml_demo后面的.表示 Dockerfile 位于当前文件夹中。构建过程需要一段时间,因为它会创建包含所有必要依赖项的容器镜像。一旦镜像构建完成,我们可以使用以下命令运行 Docker 镜像,通过将机器的8080端口映射到容器的已发布8080端口。

  8. 使用以下命令运行 Docker 镜像:

    docker run --rm -p 8080:8080 r_ml_demo
    

    注意

    您可以从 GitHub 上的完整代码获取更多信息:https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R/tree/master/Lesson08/Docker。

容器可以像我们使用 Postman 测试 plumber 应用程序一样再次进行测试,我们将得到完全相同的结果。因此,我们可以使用 plumber 和 Docker 在任何其他系统上部署 R 应用程序,无论操作系统如何,也不管缺少哪些库。

注意

Docker 不能安装在 Windows 家庭版上。只有 Windows 专业版支持虚拟机管理程序。

使用 plumber 部署 R 模型的缺点

虽然这个过程带来了一些易于快速实施的优点,但也存在一些缺点。plumber 的主要缺点是针对大型复杂用例扩展应用程序端点。这里的扩展指的是端点被调用的次数以及可以通过端点调用的数据量。

使用 plumber 的一个主要缺点是它不支持直接传递 JSON 对象或数组或列表到端点。当我们处理具有 20 个以上独立变量的较大模型时,这成为一个瓶颈。在练习 2为 R Plumber 应用程序创建 Docker 容器中的先前用例是一个相当小且轻量级的模型,具有三个独立变量。因此,API 定义既简短又甜蜜。然而,随着参数数量的增加(这对于真正的生产模型来说肯定会发生),plumber 模型端点定义可能不是最佳选择。此外,plumber 框架对于大型复杂软件用例来说也不理想。围绕该框架的小型社区、缺乏适当的文档和有限的支持使其成为将模型部署到大规模机器学习产品或服务中的风险选择。

让我们看看如何利用云服务来部署 R ML 模型。

Amazon Web Services

Amazon Web Services (AWS) 是领先的云服务提供商。随着云的出现,技术行业在构建大型规模企业应用程序的过程中经历了巨大的转变,从使用云服务而不是自托管服务。云服务市场的其他主要参与者包括微软、谷歌和 IBM。虽然所有领先的云服务提供商都有一套详尽的服务来构建各种软件应用程序,但我们将仅关注 AWS,范围限于本章。我们强烈鼓励您探索其他云服务提供商提供的类似用例的替代服务(不仅限于谷歌或微软)。

AWS 提供了大量现成的服务,可以用来构建任何规模的大型、复杂企业应用程序,无需任何前期承诺。您按使用付费,还有大量服务您可以免费探索和测试一年(有一定限制)。对于我们在即将进行的实验中将要执行的实验范围,免费层应该足够了。如果您还没有 AWS 账户,请在此处创建一个:https://aws.amazon.com/free/activate-your-free-tier-account/。

您在注册过程中需要一个有效的信用卡或借记卡。我们将仅利用 AWS 的免费层服务进行练习。

我们可以采取几种方法来使用云服务部署机器学习模型。有些可能非常适合小型应用程序,有些适合中等规模和复杂程度适中的应用程序,而其他一些则适合大型和非常复杂的应用程序。我们将探索那种软件工程量最少,但提供有效灵活性,并且可以轻松扩展到大规模应用程序,同时易于集成到复杂应用程序中的方法。

使用 API 并将机器学习模型作为 API 提供使得将服务集成到其他应用程序中的整个过程相当直接。

介绍 AWS SageMaker

Amazon SageMaker 是一种云服务,为开发人员和数据科学家提供了一个平台,可以快速构建、训练和部署机器学习模型。它是一种极其有效的服务,可以帮助那些开发知识有限的数据科学家部署高度可扩展的 ML 模型,同时抽象出整个基础设施和底层服务的复杂性。

SageMaker 自动将模型作为 API 部署,并使用定义的资源创建一个可用于在其他 AWS 服务中进行推理的 端点。为了使端点能够被其他外部应用程序进行推理,我们需要使用另外两个 AWS 服务来编排请求流,这两个服务被称为 AWS API GatewayAWS Lambda。我们将在本章后面探索这些新服务。

现在,让我们开始使用 AWS SageMaker 部署我们的模型。

使用 SageMaker 部署 ML 模型端点

默认情况下,SageMaker 不提供直接创建 R 模型的途径,但 Amazon 提供了一个简单的替代方案。AWS 通过 AWS CloudFormation 提供了 基础设施即代码 的功能,即一个服务,我们可以将项目的基础设施资源配置和设置整个流程编码化。使用 CloudFormation 模板,我们可以根据我们的需求自动化技术栈的配置过程,并且可以多次重用它。Amazon 提供了一份清晰且详尽的指南,指导您使用 CloudFormation 模板在 SageMaker 上开始使用 R 笔记本。

要了解更多信息,您可以参考 https://aws.amazon.com/blogs/machine-learning/using-r-with-amazon-sagemaker/ 上的指南,以详细了解该过程。

练习 100:将 ML 模型作为 SageMaker 端点部署

在这个练习中,我们将部署一个 ML 模型作为 SageMaker 端点。

执行以下步骤以完成练习:

  1. 登录您的 AWS 账户并启动 CloudFormation 脚本来创建一个 R 笔记本。

  2. 现在,从 https://amzn.to/2ZzUM28 访问 CloudFormation 模板以在 SageMaker 上创建 R 笔记本。

    接下来,我们将使用之前的 CloudFormation 模板在 AWS 中创建和启动堆栈。

  3. 点击模板;它将直接导航到 CloudFormation 服务,如下面的截图所示。云配置模板(这是一个 YAML 文件)托管在公共 S3 存储桶中,并且已经添加到 指定 Amazon S3 模板 URL 下的输入框中。点击 下一步 按钮并导航到 详细信息 页面:![图 8.4:CloudFormation—创建堆栈页面

    ![img/C12624_08_04.jpg]

    图 8.4:CloudFormation—创建堆栈页面
  4. 在下一页,指定您将用于登录 EC2 实例的 SSH 密钥对。这是访问我们在云中配置的云实例或虚拟机的一种安全方式。如果您尚未为您的账户创建密钥,您可以使用 Amazon 网站上提供的步骤创建一个:https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair。

  5. 一旦创建密钥对或如果您已经有了密钥对,它将出现在高亮框中的下拉菜单中,如下面的截图所示。选择您的密钥对并点击 下一步 按钮:![图 8.5:创建密钥对

    ![img/C12624_08_05.jpg]

    图 8.5:创建密钥对
  6. 在下一页 选项 页面上,我们可以直接点击 下一步 按钮并导航到下一页。

  7. 最后,在审查页面上,选中 我承认 AWS CloudFormation 可能会创建具有自定义名称的 IAM 资源 复选框,然后点击 下一步 按钮。

  8. 此过程将创建堆栈(它可能需要一段时间才能在屏幕上显示——1-2 分钟后刷新屏幕)。一旦创建,您将在 CloudFormation 下看到堆栈已准备好,如图所示。输出选项卡将包含用于登录的 SSH 命令;复制高亮部分中的值,然后在终端或命令提示符中运行该命令:![图 8.6:堆栈—SSH 密钥

    ![图片 C12624_08_06.jpg]

    图 8.6:堆栈—SSH 密钥
  9. 在终端运行 SSH 命令时,它将端口8787转发到您的计算机,同时连接到新实例。一旦连接,打开浏览器窗口并在地址栏中输入https://127.0.0.1:8787以打开 RStudio 登录页面。默认用户名和密码设置为rstudio。输入用户名和密码,然后点击登录按钮:![图 8.7:RStudio—登录页面

    ![图片 C12624_08_07.jpg]

    图 8.7:RStudio—登录页面
  10. 登录到 RStudio 并创建一个名为Sagemaker.R的新 R 脚本,加载必要的库,并准备好 SageMaker 会话:

    library(reticulate)
    library(readr)
    
  11. 启动 SageMaker 会话并定义会话中使用的默认存储桶以及角色:

    sagemaker <- import('sagemaker')
    session <- sagemaker$Session()
    bucket <- session$default_bucket()
    role_arn <- session$expand_role('sagemaker-service-role')
    
  12. 安装mlbench包并加载数据用于我们的用例。在以下命令中,我们首先设置seed以确保可重复性:

    set.seed(2019)
    install.packages("mlbench")
    library(mlbench)
    data(PimaIndiansDiabetes) 
    df<- PimaIndiansDiabetes
    
  13. 要探索 SageMaker 的自动超参数调整,我们将开发一个 XGBoost 模型,而不是在之前用例上的逻辑回归模型。因此,我们需要目标变量和所有独立变量都是数值类型的。此外,SageMaker 期望数据以第一列为目标变量的形式:

    df$diabetes <- ifelse(df$diabetes == "yes",1,0)
    
  14. 将目标变量放在数据集的第一列:

    df<- df[,c(9,1:8)]
    
  15. 使用以下命令创建 70%的培训和 30%的测试数据集:

    train_index <- sample(seq_len(nrow(df)),floor(0.7 * nrow(df)))
    train <- df[train_index,]
    test <- df[-train_index,]
    
  16. 将从df数据框创建的培训和测试数据写入内存,并将 CSV 文件上传到 AWS 上的 S3 存储桶。由于会话已定义了默认存储桶,因此我们可以直接使用upload_data命令和路径以及所需的构造来上传数据集到我们的默认 S3 存储桶:

    write_csv(train, 'diabetes_train.csv', col_names = FALSE)
    write_csv(test, 'diabetes_test.csv', col_names = FALSE)
    s3_train <- session$upload_data(path = 'diabetes_train.csv', 
                                    bucket = bucket, 
                                    key_prefix = 'data')
    s3_test <- session$upload_data(path = 'diabetes_test.csv', 
                                   bucket = bucket, 
                                   key_prefix = 'data')
    
  17. 定义 SageMaker 会话的培训和测试数据集(验证数据):

    s3_train_input <- sagemaker$s3_input(s3_data = s3_train,
                                         content_type = 'csv')
    s3_test_input <- sagemaker$s3_input(s3_data = s3_test,
                                        content_type = 'csv')
    

    SageMaker 提供了 AWS 优化的预配置容器,可以直接用于模型训练。我们需要从与我们的资源托管在同一区域的容器基础中选择一个。在这种情况下,是us-east-1

  18. 定义估计器的容器和 S3 中的输出文件夹:

    containers <- list('us-west-2' = 
    '433757028032.dkr.ecr.us-west-2.amazonaws.com/xgboost:latest',
                       'us-east-1' = 
    '811284229777.dkr.ecr.us-east-1.amazonaws.com/xgboost:latest',
                       'us-east-2' =
     '825641698319.dkr.ecr.us-east-2.amazonaws.com/xgboost:latest',
                       'eu-west-1' =
     '685385470294.dkr.ecr.eu-west-1.amazonaws.com/xgboost:latest')
    
  19. 使用以下命令选择用于估计器的容器:

    container <- containers[session$boto_region_name][[1]]
    
  20. 定义输出文件夹,如图所示:

    s3_output <- paste0('s3://', bucket, '/output')
    
  21. 定义 SageMaker 估计器、作业和输入数据。在这里,我们需要提供我们希望用于模型训练过程的实例类型,我们将选择ml.m5.large

    estimator <- sagemaker$estimator$Estimator(image_name = container,
                                               role = role_arn,
                                               train_instance_count = 1L,
                                               train_instance_type = 'ml.m5.large',
                                               train_volume_size = 30L,
                                               train_max_run = 3600L,
                                               input_mode = 'File',
                                               output_path = s3_output,
                                               output_kms_key = NULL,
                                               base_job_name = NULL,
                                               sagemaker_session = NULL)
    
  22. 设置模型感兴趣的超参数,并将培训和验证数据集定义为列表:

    estimator$set_hyperparameters(num_round = 100L)
    job_name <- paste('sagemaker-train-xgboost', format(Sys.time(), '%H-%M-%S'), 
                                 sep = '-')
    input_data <- list('train' = s3_train_input,
                       'validation' = s3_test_input)
    

    注意

    你可以在 https://aws.amazon.com/sagemaker/pricing/instance-types/上了解更多关于可用于模型训练的不同类型实例的信息。

  23. 训练/拟合我们定义的模型。模型训练过程将花费一些时间(约 10-12 分钟)。在后台,SageMaker 将提供我们在模型定义中定义的实例,触发必要的后台操作以编排训练过程,并最终训练模型:

    estimator$fit(inputs = input_data, job_name = job_name)
    
  24. 将训练模型部署为端点。我们再次需要提供我们希望 SageMaker 为模型推理提供的实例类型。由于这是一个示例模型,我们可以选择配置最低的实例。这个过程也将花费一些时间,因为 SageMaker 将在后台编排一系列服务以将模型作为端点部署:

    model_endpoint <- estimator$deploy(initial_instance_count = 1L,
                                       instance_type = 'ml.t2.medium')
    model_endpoint$content_type <- 'text/csv'
    model_endpoint$serializer <- sagemaker$predictor$csv_serializer
    

    模型端点创建后,我们可以通过使用正确的测试数据形式来测试它。由于我们将测试数据保存为 CSV 文件,因此我们将传递以逗号分隔的文本以序列化为 JSON 格式。因此,我们为端点指定text/csvcsv_serializer。让我们准备一个用于快速测试的样本测试数据馈送。

  25. 首先,使用以下命令复制测试数据集:

    one_test <- test
    
  26. 接下来,删除目标变量:

    one_test$diabetes<-NULL
    
  27. 使用以下命令创建单个测试样本:

    test_sample <- as.matrix(one_test[7, ])
    
  28. 现在,删除列名:

    dimnames(test_sample)[[2]] <- NULL
    
  29. 使用我们在上一步创建的样本数据,通过模型端点进行预测。调用 SageMaker 端点并传递测试数据:

    predictions <- model_endpoint$predict(test_sample)
    
  30. 现在,使用print命令打印结果:

    print(ifelse(predictions>0.5,"yes",'no'))
    

    输出如下:

    1] "no"
    

此输出有助于我们了解模型已正确部署并按预期运行。我们可以通过导航到 AWS 账户中的 SageMaker 服务并从右侧侧边栏打开端点部分来检查端点是否已创建:

图 8.8:端点页面

图 8.8:端点页面

此端点可以直接由 AWS 内的其他服务调用并获取预测。唯一的要求是输入数据应按预期提供。为了使我们的模型端点可由 AWS 外的其他服务访问,我们需要使用 AWS API Gateway 和 AWS Lambda 编排 API 请求。

注意

你可以在 GitHub 上访问完整的代码文件,网址为 https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R/blob/master/Lesson08/RStudio_SageMaker.R。

现在,在我们深入研究解决方案之前,让我们研究一下这些服务。

亚马逊 Lambda 是什么?

AWS Lambda 是亚马逊作为 Amazon Web Services 的一部分提供的一个事件驱动的无服务器计算平台。它是一种响应事件运行代码的计算服务,并自动管理该代码所需的计算资源。该服务使我们能够开发无服务器应用程序。术语无服务器表示我们实际上不需要管理和配置基础设施资源;相反,它们由云服务提供商管理,我们只需为使用的资源付费,例如按事件或执行付费。AWS Lambda 可以配置为在响应特定事件时执行定义的函数,例如,当有人将新文件上传到定义的 S3 桶或调用 AWS 中的另一个函数或另一个服务时。

什么是 Amazon API Gateway?

Amazon API Gateway 是一个全面管理的服务,它使开发者能够轻松地在任何规模上创建、发布、维护、监控和保障 API。使用此服务,我们可以开发 REST 以及 WebSocket API,这些 API 作为应用程序访问来自其他后端服务的数据、业务逻辑或功能的“前门”。同时,它还保护了私有网络内的后端服务。这些后端服务可以是运行在 AWS 上的任何应用程序。

我们服务的整体流程可以用以下图表表示:

图 8.9:API Gateway、AWS Lambda 和 SageMaker 的工作流程

图 8.9:API Gateway、AWS Lambda 和 SageMaker 的工作流程

客户端(例如,一个网页浏览器),调用 Amazon API Gateway 的定义动作并传递适当的参数值。API Gateway 将请求传递给 AWS Lambda,同时它也封闭了后端,以便 AWS Lambda 在受保护的私有网络中停留并执行。

在我们的案例中,我们将使用 Lambda 来帮助我们调整从 API Gateway 收到的数据,使其以适当的形式被 SageMaker 端点消费。这是必要的,因为通过 REST API 传递的数据结构与 SageMaker 所期望的结构之间存在差异。SageMaker 模型执行预测并将预测值返回给 AWS Lambda。Lambda 函数解析返回的值并将其发送回 API Gateway,然后 API Gateway 使用结果响应客户端。整个流程完全由 AWS 管理而无需我们实际配置任何基础设施。

注意

此工作流程在亚马逊的一篇博客文章中有更详细的解释。您可以在 https://aws.amazon.com/blogs/machine-learning/call-an-amazon-sagemaker-model-endpoint-using-amazon-api-gateway-and-aws-lambda/ 上了解更多信息。

我们将需要解决一个额外的挑战。截至目前,AWS Lambda 不支持 R 语言来定义函数。它支持 Python、Java、Go 以及一些其他语言,但 R 语言目前不在支持列表中。Lambda 函数将负责将 API 传递的数据转换为所需的形式。我们将利用 Python 脚本来完成这项任务。未来,我们可以期待 AWS Lambda 支持 R 语言。

现在我们已经了解了所需服务的必要背景,让我们将我们的模型部署到无服务器应用程序上。

构建无服务器机器学习应用程序

无服务器计算是云计算中的新范式。它允许我们构建和运行应用程序和服务,而无需考虑服务器。实际上,我们构建的应用程序仍然运行在云服务器上,但整个服务器管理过程由云服务提供商,如 AWS,来完成。通过利用无服务器平台,我们只需关注应用程序代码,就可以构建和部署强大、大规模、复杂的应用程序,而无需担心配置、管理和提供服务器。

在本章中,我们探讨了 AWS 无服务器平台的一些重要组件,例如 AWS Lambda,现在我们可以利用这些解决方案来构建一个机器学习应用程序,我们只需关注核心 ML 代码,而无需担心基础设施的配置和应用程序的扩展。

练习 101:使用 API Gateway、AWS Lambda 和 SageMaker 构建无服务器应用程序

在这个练习中,我们将使用 AWS SageMaker 构建一个机器学习模型,并将其作为端点(使用自动化的 SageMaker 函数)部署。为了使模型端点能够被任何服务(在 AWS 内部或外部)调用,我们定义了一个 AWS Lambda 函数,并通过 API Gateway 将端点暴露给公共网络。

这个练习的目的是创建一个无服务器应用程序,该应用程序将使用我们在上一个练习中创建的 SageMaker 模型端点。

执行以下步骤:

  1. 在 AWS 中创建一个 IAM 角色,以便 Lambda 能够执行 SageMaker 服务的端点。从 AWS 控制台,搜索 IAM,然后在 IAM 页面上点击 Roles 选项:图 8.10:创建 IAM 角色

    图 8.10:创建 IAM 角色
  2. 一旦 角色 页面加载,点击 创建角色 选项。

  3. 创建角色 页面上,选择 AWS Service 作为受信任实体的类型,并将 Lambda 作为将使用此角色的服务。点击 下一步:权限 按钮继续:图 8.11:选择 AWS 服务选项

    图 8.11:选择 AWS 服务选项
  4. sagemaker 关键字上,选择 AmazonSageMakerFullAccess 策略,并点击 下一步:标签 选项:图 8.12:创建角色屏幕

    图 8.12:创建角色屏幕
  5. 标签 页面上,您可以直接点击 下一步 并进入最后一页来命名 角色

  6. 现在,在最后一页,添加一个合适的名称(例如,lambda_sagemaker_execution)并点击创建角色。角色将为我们创建以供使用:![图 8.13:审查页面—创建角色 图 C12624_08_13.jpg

    图 8.13:审查页面—创建角色
  7. 在 AWS 控制台中,搜索 AWS Lambda 并点击创建函数按钮。创建函数页面将有一些需要定义的输入。

  8. 选择lambda_sagemaker_connection)。

  9. 接下来,选择lambda_sagemaker_execution。点击创建函数按钮:![图 8.14:创建函数表单 图 C12624_08_14.jpg

    图 8.14:创建函数表单
  10. 定义一个 Python 函数,该函数将接受 API 的输入请求,解析有效载荷,并调用 SageMaker 端点:

    import os, io, boto3, json, csv
    # grab environment variables
    ENDPOINT_NAME = os.environ['ENDPOINT_NAME']
    runtime= boto3.client('runtime.sagemaker')
    

    注意

    端点名称将在 SageMaker 页面下的端点部分可用。

  11. 我们还将为函数定义一个环境变量,该变量将存储 SageMaker 端点:

    def lambda_handler(event, context):
        print("Received event: " + json.dumps(event, indent=2))
    
        #Load JSON data from API call
        data = json.loads(json.dumps(event))
        payload = data['data']
        print(payload)
    
        #Invoke Sagemaker endpoint and pass Payload
        response = runtime.invoke_endpoint(EndpointName=ENDPOINT_NAME,
                                           ContentType='text/csv',
                                           Body=bytes(str(payload),'utf'))
    
        result = json.loads(response['Body'].read().decode())
        predicted_label = 'yes' if result >0.5 else 'no'
        return predicted_label
    

    注意

    您可以在 GitHub 上查看完整的代码:https://github.com/TrainingByPackt/Applied-Supervised-Learning-with-R/blob/master/Lesson08/Amazon_Lambda_Function.py。

  12. 在 AWS 控制台中点击API Gateway,通过选择截图中的以下突出显示选项创建一个新的 API 函数。为 API 提供一个合适的名称,例如api_lambda_connect:![图 8.15:Amazon API 网关 图 C12624_08_15.jpg

    图 8.15:Amazon API 网关
  13. 操作下拉菜单中选择创建资源,添加一个合适的资源名称,然后点击创建资源按钮:![图 8.16:创建新的子资源 图 C12624_08_16.jpg

    图 8.16:创建新的子资源
  14. 再次,从操作下拉菜单中选择创建方法,并选择方法类型为POST

  15. 选择集成类型Lambda 函数,并在输入标签中指定Lambda 函数名称,如以下截图所示。然后,点击保存按钮:![图 8.17:创建 Lambda 函数 图 C12624_08_17.jpg

    图 8.17:创建 Lambda 函数
  16. 接下来,选择test)并点击部署选项。API 现在将被部署并准备好使用。

  17. 一旦 API 部署完成,我们可以通过导航到https://****amazonaws.com/[deployment-stage]/[resource-name]/找到要调用的 API 的 URL。

  18. 使用 Postman 调用 API。打开 Postman,选择一个POST调用,并将我们从 API 网关阶段复制的 URL 粘贴进去。然后,点击正文并添加原始数据,即 JSON 格式的测试数据,如以下截图所示:![图 8.19:通过 Postman 调用 API 图 C12624_08_19.jpg

    图 8.19:通过 Postman 调用 API
  19. 点击发送按钮以使用提供的原始数据调用 API。结果将在下面的窗口中展示。

我们收到了“否”的预测,这表明模型已成功部署为无服务器应用程序。现在我们可以从世界任何地方的浏览器或 Postman 调用 API 并获取模型的预测。此 API 调用可以集成到更大的产品中的其他服务,并且可以根据需求进行扩展。

删除所有云资源以停止计费

我们已配置的所有资源都需要被删除/终止,以确保它们不再计费。为了确保在练习书中创建的所有资源都被删除,需要执行以下步骤:

  1. 登录 CloudFormation 并点击删除堆栈(我们为 RStudio 提供的那个)。

  2. 登录 SageMaker,从右侧侧边栏打开端点,检查我们为练习创建的端点,并删除它。

  3. 登录 AWS Lambda,删除我们为练习创建的 Lambda 函数。

  4. 登录 AWS API Gateway 并删除我们为练习创建的 API。

关于 AWS SageMaker 的进一步说明

我们利用 Amazon 提供的现有算法容器来训练模型。这一步骤是为了使事情简单。我们可以将我们自己的自定义训练算法带到 SageMaker,并利用该平台将模型作为服务部署。SageMaker 负责整个过程,包括协调后台资源以提供实例、配置模型工件和构建端点。然而,我们需要以特定格式提供数据和模型工件,以便 SageMaker 可以部署它。

注意

关于构建自定义模型过程的更多详细信息可以在 https://docs.aws.amazon.com/sagemaker/latest/dg/build-model.html 找到。

活动 13:使用 plumber 部署 R 模型

在此活动中,我们将使用 R 开发回归模型,并使用 plumber 将其作为 API 端点部署。我们将使用 R 的另一个监督学习用例,并使用不同的数据集构建回归模型,即波士顿住房。该数据集包含在 R 的 Mlbench 库中,我们已安装,并提供在给定房屋属性数量时波士顿房屋的中位价格信息。

我们将编写两个 R 脚本:model.r用于存放回归模型以及预测函数,plumber.R用于存放部署模型为 API 端点所需的功能。

注意

关于数据集的更多详细信息可以在 https://www.rdocumentation.org/packages/mlbench/versions/2.1-1/topics/BostonHousing 找到。

执行以下步骤:

  1. 创建一个model.r脚本,该脚本将加载所需的库、数据和拟合回归模型以及必要的函数,以便对未见数据做出预测。

  2. 加载mlbench库,其中包含此活动的数据。

  3. BostonHousing数据加载到 DataFrame,df中。

  4. 使用df的前400行创建训练数据集,并使用剩余的数据进行测试。

  5. 使用 lm 函数拟合逻辑回归模型,其中因变量为 medv(中值)和 10 个自变量,例如 crimzninduschasnoxrmagedisradtax

  6. 定义一个模型端点为 predict_data;这将被用作 plumber 的 API 端点。

  7. 在函数内部,将参数转换为 numericfactor(因为 API 调用只会将它们作为字符串传递)。

  8. 将模型的 10 个独立特征作为名为 sample 的 DataFrame 包装,列名保持一致。

  9. sample DataFrame 传递给步骤 4 中创建的模型预测函数,并返回预测结果。

  10. 加载 plumber 库。

  11. 使用 plumb 函数创建一个 plumber 对象,并传递步骤 1 中创建的 model.r 文件。

  12. 通过传递主机名 localhost127.0.0.1 和端口号,例如 8080,来运行 plumber 对象。

  13. 使用浏览器或 Postman 测试部署的模型,并调用 API。

    API 调用:

    http://127.0.0.1:8080/predict_data?crim=0.01&zn=18&indus=2.3&chas=0&nox=0.5&rm=6&age=65&dis=4&rad=1&tax=242

    最终输出如下:

    {"Answer":[22.5813]}Note
    

    注意

    本活动的解决方案可以在第 463 页找到。

摘要

在本章中,我们学习了如何使用 R 的 plumber 工具以及 Docker 容器等增强方法,通过传统的基于服务器的部署策略来部署我们的机器学习模型。然后,我们研究了如何使用云服务构建无服务器应用程序,以及如何通过最少的代码轻松地按需扩展应用程序。

我们探讨了各种网络服务,例如 Amazon Lambda、Amazon SageMaker 和 Amazon API Gateway,并研究了如何编排服务以将我们的机器学习模型作为无服务器应用程序部署。

在下一章中,我们将通过基于现实世界问题的最新研究论文来开展一个综合项目,并重现其结果。

第九章:基于研究论文的毕业设计

学习目标

到本章结束时,你将能够:

  • 使用 mlr 和 OpenML 在问题上应用端到端的机器学习工作流程,这包括识别研究文章。

  • 训练机器学习模型,并随后在测试数据集上使用该模型进行预测和评估。

  • 对数据集进行重采样。

  • 设计用于构建各种模型的实验。

  • 为选择最佳模型建立基准。

在本章中,我们将基于一个现实世界问题选取最新的研究论文,并重现其结果。

简介

在本章的最后,我们将专注于一个基于研究的项目。所有前几章中的想法,例如使用 SCQ 框架设计问题、识别数据来源、预处理数据集、训练机器学习模型、评估模型、应用重采样技术以及许多其他概念都将被使用。此外,本章还将关注基准测试模型、设计机器学习实验、在开源平台上协作以及使研究工作可重复,以造福更大的社区。

在线资源的丰富、计算能力的提升以及现成的工具包解决方案的广泛应用,使得成为机器学习专业人员的门槛降至最低。如今,我们有很多快速入门算法,它们作为 R 和 Python 等编程语言中的函数提供,甚至在 Google Cloud AutoML 或 Microsoft Azure Machine Learning Studio 等平台上的拖放操作中提供。然而,在许多这样的快速入门 Hello World 模型中,通常缺少的是对问题解决的敏锐关注以及超越现有工具的能力。

除了豪华的工具集之外,还存在着由许多学术界和工业界的领先实践者产生的以研究为导向的工作世界。当涉及到产生突破和高品质成果时,这种研究工作的重要性是巨大的。然而,这种研究工作的可访问性有限,因此阻碍了机器学习实践者中的广泛应用。不追求基于研究的工作的另一个原因是由于缺乏可重复性(大多是因为代码未在公共领域提供,研究发现不明确,或研究工作质量差)以及在研究文章或论文中发现的充满术语的理论(许多是用数学语言编写的)。

本章致力于此类研究工作,这些工作往往被许多努力进入机器学习但仅限于使用博客或在线书籍中提倡的特定工具和包的许多学习者忽视。我们将关注两个重要的研究工作,幸运的是,它们也找到了在 R 包中的位置。下一节将介绍这项工作,并设定本章其余部分的流程。

探索研究工作

在本章中,我们将探讨两项最终也成为开源项目的最显著研究成果。本章的重点在于自上而下的方法,我们将从优秀研究成果的起源开始,看看它是如何成为每个人都可以使用的主流工具包的。在强调研究成果的同时,我们还想强调,许多研究成果在市场上可用的标准工具包中找不到位置,但如果稍微努力一些,就能发现一些珍宝。

我们推荐参考 https://paperswithcode.com 创作者所付出的出色努力。Papers With Code 团队借助社区的帮助和自动化的力量,创建了一个免费且开放的资源平台,包含机器学习论文、代码和评估表格。他们已经自动化了代码与论文的链接,现在正在努力自动化从论文中提取评估指标。这项工作值得称赞,因为它将把最佳研究成果从噪音和繁杂中凸显出来。

下表将突出五个研究成果案例,这些案例是通过 Papers With Code 网站找到的。在整个书中,你已经看到了很多用于机器学习工作流程各个阶段的 R 代码和不同包。mlr 和 OpenML 研究人员的工作现在已打包在 R 中,特别是 OpenML 是一个完整的平台。我们将学习如何利用 mlr 和 OpenML 平台来制作最佳的机器学习模型,而不仅仅是快速入门的 Hello World 示例。为了参考,请查看以下表格:

![图 9.1:本课程演示中使用的论文(第一部分)]

图片

图 9.1:本章演示中使用的论文(第一部分)

![图 9.2:本课程演示中使用的论文(第二部分)]

图片

图 9.2:本章演示中使用的论文(第二部分)

mlr 包

现在,我们将学习 mlr 包如何提供一个完整的框架来处理许多机器学习模型和问题。在许多机器学习项目中,人们往往需要管理大量围绕众多实验的细节(也称为试错迭代)。每个实验都包含使用不同机器学习算法、性能指标、超参数、重采样技术和预测的大量训练。除非我们系统地分析每个实验中获得的信息,否则我们无法得出最佳参数值组合。

使用 mlr 包的另一个优点来自于其丰富的机器学习算法集合,来自各种包。我们不再需要为机器学习算法的不同实现安装多个包。相反,mlr 在一个地方提供了一切。为了更好地理解这一点,请参考以下表格:

图 9.3:mlr 包(第一部分)

图片

图 9.3:mlr 包(第一部分)

多标签分类算法

mlr 包中提供了以下多标签分类模型,其中单个观测值可以分配给多个类别。这些模型在解决许多有用的问题中很有用,例如,在 Netflix 上,你会看到每部电影都可以被标记为动作、冒险和奇幻。或者,在 YouTube 上,每天都有数百万个视频被发布,一个自动算法可以将视频标记为多个类别,从而帮助内容过滤和更好的搜索。

我们将使用这些算法以及前表中定义的分类器:

![图 9.4:使用 mlr 包进行多标签分类]

图片

图 9.4:使用 mlr 包进行多标签分类

OpenML 包

OpenML,一个学术和工业研究人员可以通过它自动共享、组织和审议机器学习实验、数据、算法和流程的合作平台。该平台带来了高效的协作,并实现了可重复性。

R 中的 OpenML 包包含各种功能,允许用户搜索、上传和下载数据集,并执行与机器学习相关的操作。用户可以上传机器学习实验的输出,与其他用户分享,并下载输出结果。这增强了工作的可重复性,加快了研究工作,并将来自不同领域的人们聚集在一起。

研究论文中的问题设计

在本章中,我们将理解、分析和重现《学习多标签场景分类》论文的结果。我们将有效地使用 mlr 和 OpenML 包来重现结果。在此之前,让我们使用SCQ 框架来编写论文中的情况-复杂性-问题

![图 9.5:来自论文《学习多标签场景分类》的 SCQ]

图片

图 9.5:来自论文《学习多标签场景分类》的 SCQ

场景数据集功能

论文使用了scene数据集进行语义场景分类任务。该数据集是自然场景图像的集合,其中自然场景可能包含多个对象,因此多个类别标签可以描述场景。例如,背景有山的田野场景。从论文中,我们选取了第一张图,它显示了两个图像,这两个图像是多标签图像,描述了单个图像中的两个不同场景。图 9.6是海滩和城市场景,而图 9.7展示了山脉:

![图 9.6:海滩和城市场景。

![img/C12624_09_06.jpg]

图 9.6:海滩和城市场景。

![图 9.7:山脉场景。

![img/C12624_09_07.jpg]

图 9.7:山脉场景。

从给定的图像中,我们可以使用以下内容:

  • 颜色信息:当区分某些类型的户外场景时,这种信息很有用。

  • 空间信息:这种信息在许多情况下都很有用。例如,图像顶部的光、暖色调可能对应日出。

论文使用CIE LUV,如空间,表示为 Luv。Luv 空间提出了预期的感知均匀性。在将 XYZ 空间适应到 Luv 空间(一种从 XYZ 空间到LUV空间的数学转换)之后,图像被一个 7 x 7 网格分成 49 个块。然后,作者计算每个波段(RGB)的第一和第二矩(均值和方差),这分别对应于低分辨率图像和计算成本较低的质量特征。总共,我们为每张图像获得49 x 2 x 3 = 294个特征向量。

数据集中的剩余六列对应于用真/假编码值表示的六个标签。如果一个图像属于两个类别,相应的列将具有真值。

注意

在比色法中,国际照明委员会(CIE)于 1976 年采用了 CIE 1976 L、u、v*颜色空间,作为 1931 CIE XYZ 颜色空间的一个简单计算转换,但试图实现感知均匀性,即两种颜色之间的差异或距离。

使用 mlr 和 OpenML 包实现多标签分类器

现在我们将了解如何使用 mlr 和 OpenML 包训练多标签分类器。首先,我们将从 OpenML 下载场景数据集。

练习 102:从 OpenML 下载场景数据集

在这个练习中,我们将下载场景数据集并为其进一步分析做准备。

完成练习的以下步骤:

  1. 为了通过 OpenML API 下载场景数据集,首先在www.openml.org/register网站上创建一个账户。注册过程涉及验证您的电子邮件地址,之后您将获得访问 API 密钥的权限。![图 9.8:OpenML 注册页面。

    ![img/C12624_09_08.jpg]

    图 9.8:OpenML 注册页面。
  2. 登录您的账户后,转到您的账户并选择 API 认证选项。

  3. 在 API 认证页面,从 API 密钥部分选择并复制粘贴 API 密钥。Multilabel Classification with R Package mlr 论文的作者上传了一组带有 2016_multilabel_r_benchmark_paper 标签的数据集,我们现在可以从 OpenML 下载并开始重现他们的结果。我们将特别使用场景数据集(ID 为 40595)。

  4. 在继续之前,打开 RStudio 并安装所有必需的包。

  5. 导入所需的包和库:

    library(mlr)
    library(BBmisc)
    library(OpenML)
    library(batchtools)
    library(parallelMap)
    
  6. 使用来自 OpenML 的 API 密钥,并使用以下命令注册 API 密钥:

    setOMLConfig(apikey = “627394a14f562f0fa8bcc9ec443d879f”)
    

    输出如下:

    ## OpenML configuration:
    ##   server           : http://www.openml.org/api/v1
    ##   cachedir         : C:\Users\Karthik\AppData\Local\Temp\Rtmp6bSgE4/cache
    ##   verbosity        : 1
    ##   arff.reader      : farff
    ##   confirm.upload   : TRUE
    ##   apikey           : ***************************d879f
    
  7. 使用以下命令从源下载数据集:

    ds.list = listOMLDataSets(tag = “2016_multilabel_r_benchmark_paper”)
    

    输出如下:

    ## Downloading from ‘http://www.openml.org/api/v1/json/data/list/tag/2016_multilabel_r_benchmark_paper/limit/5000/status/active’ to ‘<mem>’.
    
  8. 接下来,我们将使用 ID 40595 获取场景数据集:

    oml.data = lapply(40595, getOMLDataSet)
    df.oml.data.scene <- data.frame(oml.data)
    

    注意

    在执行前两个命令之前,请确保您已安装 farff 包。

  9. 使用以下命令创建 DataFrame:

    df_scene = df.oml.data.scene
    labels = colnames(df_scene)[295:300]
    scene.task = makeMultilabelTask(id = “multi”, data = df_scene, target = labels)
    

    输出如下:

图 9.9:场景.task 变量的环境设置

图 9.9:场景.task 变量的环境设置

图 9.9:场景.task 变量的环境设置

在这个练习中,我们在 OpenML 上注册了一个账户并获取了一个 API 密钥。使用该 API 密钥,我们能够下载具有 OpenML 中 2016_multilabel_r_benchmark_paper 标签的场景数据集。最后,我们将数据集转换为数据框。OpenML 提供了许多这样的功能以促进协作。通过分配标签,人们可以与更大的社区分享他们的代码、实验和流程。

构建学习器

学习器是 mlr 包中机器学习算法的实现。如前文所述的 mlr 包部分所强调,mlr 中有一个丰富的学习器函数集合。

对于我们的场景分类问题,mlr 包提供了两种可能的方式来构建多标签分类模型:

  • 适应方法:在这种情况下,我们在整个问题上适应一个显式算法。

  • 转换方法:我们将问题转换为一个简单的二元分类问题,然后应用可用的二元分类算法。

适应方法

R 中的 mlr 包提供了两种算法适应方法。首先,来自randomForestSRC包的多变量随机森林算法,其次,内置在rFerns包中的随机森林多标签算法。

mlr 中的makeLearner()函数创建了rFernsrandomForestSRC算法的模型对象,如下面的代码所示:

multilabel.lrn3 = makeLearner(“multilabel.rFerns”)
multilabel.lrn4 = makeLearner(“multilabel.randomForestSRC”)
multilabel.lrn3

输出如下,显示了关于多标签.rFerns 模型的信息,如名称和预测类型:

## Learner multilabel.rFerns from package rFernsd
## Type: multilabel
## Name: Random ferns; Short name: rFerns
## Class: multilabel.rFerns
## Properties: numerics,factors,ordered
## Predict-Type: response
## Hyperparameters:

注意

在执行前两个命令之前,请确保您已安装rFernsrandomForestSRC包。

转换方法

构建学习器的第二种方法是使用makeLearner()函数,然后可以使用以下章节中描述的五个包装函数中的任何一个来进行问题转换。

二元相关方法

在多标签分类问题中,每个标签都可以被转换为一个二元分类问题。在这个过程中,任何一个观测值都可能被分配多个标签。在 mlr 包中,makeMultilabelBinaryRelevanceWrapper()方法将二元学习器方法转换为包装的二元相关多标签学习器。

分类链方法

分类链包装器方法实现了一个多标签模型,其中二元分类器被排列成链。每个模型按照链中指定的顺序输出预测。该模型使用给定数据集中的所有特征,以及链中之前模型的全部预测。mlr 中的makeMultilabelClassifierChainsWrapper()方法用于创建分类链包装器。

嵌套堆叠

然而,与分类链类似,观察到的类别(或标签)不是实际类别,而是基于从链中前一个模型(学习器)获得的类别估计。mlr 包中的makeMultilabelNestedStackingWrapper()方法用于创建嵌套堆叠包装器。

依赖二分类相关性方法

mlr 包中的makeMultilabelDBRWrapper方法用于创建依赖二分类相关性包装器。

堆叠

然而,与依赖二分类相关性方法类似,在训练阶段,用作每个标签输入的标签是通过二分类方法获得的,而不是使用实际标签。mlr 包中的makeMultilabelStackingWrapper方法用于创建堆叠包装器。

在以下练习中,我们将看到如何使用classif.rpart方法生成决策树模型。

练习 103:使用 classif.rpart 方法生成决策树模型

在这个练习中,我们将使用classif.rpart方法生成决策树模型,然后使用二分类和嵌套堆叠包装器进行转换。

执行以下步骤以完成练习:

  1. 首先,使用makeLearner方法创建classif.rpart的对象:

    lrn = makeLearner(“classif.rpart”, predict.type = “prob”)
    
  2. 接下来,使用makeMultilabelBinaryRelevanceWrapper方法创建堆叠包装器:

    multilabel.lrn1 = makeMultilabelBinaryRelevanceWrapper(lrn)
    multilabel.lrn2 = makeMultilabelNestedStackingWrapper(lrn)
    
  3. 接下来,打印模型:

    lrn
    

    输出如下:

    Learner classif.rpart from package rpart
    Type: classif
    Name: Decision Tree; Short name: rpart
    Class: classif.rpart
    Properties: twoclass,multiclass,missings,numerics,factors,ordered,prob,weights,featimp
    Predict-Type: prob
    Hyperparameters: xval=0
    
  4. 打印堆叠包装器,如图所示:

    multilabel.lrn1
    

    以下命令的输出如下,显示了模型类型、作为模型输出一部分的属性以及预测类型:

    Learner multilabel.binaryRelevance.classif.rpart from package rpart
    Type: multilabel
    Name: ; Short name: 
    Class: MultilabelBinaryRelevanceWrapper
    Properties: numerics,factors,ordered,missings,weights,prob,twoclass,multiclass
    Predict-Type: prob
    Hyperparameters: xval=0
    

训练模型

我们可以使用多标签学习者和多标签任务作为输入,像通常一样训练模型;使用multilabel.lrn1对象。

练习 104:训练模型

在这个练习中,我们首先将数据随机分为训练集和测试集,然后使用 mlr 包中的train()函数和前一小节中定义的multilabel.lrn1对象来训练模型。

执行以下步骤以完成练习:

  1. 使用以下命令进行训练、预测和评估数据集:

    df_nrow <- nrow(df_scene)
    df_all_index <- c(1:df_nrow)
    
  2. 使用以下命令创建test_index 变量

    train_index <- sample(1:df_nrow, 0.7*df_nrow)
    test_index <- setdiff(df_all_index,train_index)
    
  3. 使用train函数,该函数接受模型对象multilabel.lrn1(仅包含随机选择的train_indexscene.task)来训练模型:

    scene_classi_mod = train(multilabel.lrn1, scene.task, subset = train_index)
    scene_classi_mod
    

    输出如下:

    Model for learner.id=multilabel.binaryRelevance.classif.rpart; learner.class=MultilabelBinaryRelevanceWrapper
    Trained on: task.id = multi; obs = 1684; features = 294
    Hyperparameters: xval=0
    

使用 R 中的rpart包训练的scene_classi_mod模型,使用了从scene数据集中随机选择的1684个观测值,它是机器学习中分类和回归树CART)算法的实现,并包装了二值相关方法进行多标签分类。

预测输出

在 mlr 中,可以使用predict函数进行预测。输入参数是训练模型;将scene.task数据集分配给tasktest_index参数,它们对应于分配给subset参数的测试数据集:

pred = predict(scene_classi_mod, task = scene.task, subset = test_index)
names(as.data.frame(pred))
[1] “id”                   “truth.Beach”          “truth.Sunset”         “truth.FallFoliage”   
 [5] “truth.Field”          “truth.Mountain”       “truth.Urban”          “prob.Beach”          
 [9] “prob.Sunset”          “prob.FallFoliage”     “prob.Field”           “prob.Mountain”       
[13] “prob.Urban”           “response.Beach”       “response.Sunset”      “response.FallFoliage”
[17] “response.Field”       “response.Mountain”    “response.Urban”

模型性能

为了评估预测的性能,mlr 包提供了performance()函数,该函数接受模型的预测作为输入,以及我们想要计算的所有度量。所有可用的多标签分类度量可以通过listMeasures()列出。根据论文,我们在预测上使用了hamlossf1subset01acctprppv等度量:

MEASURES = list(multilabel.hamloss, multilabel.f1, multilabel.subset01, multilabel.acc, multilabel.tpr, multilabel.ppv)
performance(pred, measures = MEASURES)

上一条命令的输出如下:

multilabel.hamloss       multilabel.f1 multilabel.subset01      multilabel.acc      multilabel.tpr 
          0.1260950           0.5135085           0.5878285           0.4880129           0.5477178 
     multilabel.ppv 
          0.7216733

以下命令将列出所有可用的多标签分类问题度量:

listMeasures(“multilabel”)

输出如下:

[1] “featperc”            “multilabel.tpr”      “multilabel.hamloss”  “multilabel.subset01” “timeboth”           
 [6] “timetrain”           “timepredict”         “multilabel.ppv”      “multilabel.f1”       “multilabel.acc”

数据重采样

为了评估学习算法的完整性能,我们可以进行一些重采样。为了定义重采样策略,可以使用makeResampleDesc()makeResampleInstance()。之后,运行resample()函数。使用以下默认度量来计算汉明损失:

rdesc = makeResampleDesc(method = “CV”, stratify = FALSE, iters = 3)
r = resample(learner = multilabel.lrn1, task = scene.task, resampling = rdesc,measures = list(multilabel.hamloss), show.info = FALSE)
r

输出如下:

Resample Result
Task: multi
Learner: multilabel.binaryRelevance.classif.rpart
Aggr perf: multilabel.hamloss.test.mean=0.1244979
Runtime: 4.28345

每个标签的二进制性能

我们可以计算每个标签的二进制性能度量,例如准确度或aucgetMultilabelBinaryPerformances()函数很有用。我们可以将此函数应用于任何多标签预测,例如,也可以应用于重采样的多标签预测。为了计算auc,我们需要预测概率:

getMultilabelBinaryPerformances(r$pred, measures = list(acc, mmce, auc))

输出如下:

##             acc.test.mean mmce.test.mean auc.test.mean
## Beach           0.8728708     0.12712921     0.7763484
## Sunset          0.9335272     0.06647279     0.9066371
## FallFoliage     0.9148317     0.08516826     0.8699105
## Field           0.9077690     0.09223099     0.8895795
## Mountain        0.7922725     0.20772746     0.7670873
## Urban           0.8213544     0.17864562     0.7336219

基准测试模型

在基准实验中,不同的学习方法被应用于一个或多个数据集,目的是比较和排名算法,根据一个或多个性能度量。mlr()方法提供了一个非常稳健的框架来执行此类实验,并有助于跟踪实验的所有结果以进行比较。

进行基准实验

在我们的第一个实验中,我们使用mlr()包中的多标签randomForestSRCrFerns学习者和各种度量来获得我们的第一个基准。

在以下练习中,我们将探索如何对各种学习器进行基准测试。

练习 105:探索如何在各种学习器上进行基准测试

在这个练习中,我们将看到如何对迄今为止创建的各种学习者进行基准测试,并将结果进行比较,以选择多标签场景分类问题的最佳学习者(模型)。这有助于我们将所有结果组织成结构化格式,以选择表现最佳的模式。

执行以下步骤以完成练习:

  1. 首先,使用以下命令列出所有学习者:

    lrns = list(makeLearner(“multilabel.randomForestSRC”),
                makeLearner(“multilabel.rFerns”)
                )
    MEASURES = list(multilabel.hamloss, multilabel.f1, multilabel.subset01, multilabel.acc, multilabel.tpr, multilabel.ppv)
    
  2. 进行基准实验:

    bmr = benchmark(lrns, scene.task, measures = MEASURES)
    

    输出如下:

    ## Exporting objects to slaves for mode socket: .mlr.slave.options
    ## Mapping in parallel: mode = socket; cpus = 2; elements = 2.
    
  3. 现在,执行bmr对象:

    bmr
    

    模型的训练迭代将类似于以下每个学习者:

    Task: multi, Learner: multilabel.rFerns
    [Resample] cross-validation iter 9: multilabel.hamloss.test.mean=0.183,multilabel.f1.test.mean=0.653,multilabel.subset01.test.mean=0.768,multilabel.acc.test.mean=0.54,multilabel.tpr.test.mean= 0.9,multilabel.ppv.test.mean=0.564
    ...
    [Resample] Aggr. Result: multilabel.hamloss.test.mean=0.183,multilabel.f1.test.mean=0.663,multilabel.subset01.test.mean=0.756,multilabel.acc.test.mean=0.549,multilabel.tpr.test.mean=0.916,multilabel.ppv.test.mean=0.566
    

    下表展示了测试数据上各种度量的平均值:

图 9.10:测试数据上各种度量的平均值。

图 9.10:测试数据上各种度量的平均值。

此表显示,randomForestSRC模型在所有度量上比rFerns做得稍好,主要在hamloss 平均值度量上。

访问基准结果

mlr()方法提供了许多getBMR函数,可以从基准实验对象中提取有用的信息,如性能、预测、学习者等。

学习者表现

getBMRPerformances函数给出了训练过程中每个迭代中基准中定义的所有度量的所有值。下表列出了使用randomForestSRCrFerns学习者每个度量的值。

getBMRPerformances(bmr, as.df = TRUE)

图 9.11:学习者的 randomForestSRC 表现

图 9.11:学习者的 randomForestSRC 表现

图 9.12:学习者的 rFerns 表现

图 9.12:学习者的 rFerns 表现

预测

我们还可以使用getBMRPredictions函数在测试数据集上获取预测。本节中的两个表格显示了 ID 列表示的几个图像的实际和预测标签。观察发现,预测并不完美,正如我们预期的那样,整体准确率相对较低。

使用 randomForestSRC 进行预测

head(getBMRPredictions(bmr, as.df = TRUE))

图 9.13:实际标签。

图 9.13:实际标签。

图 9.14:预测标签。

图 9.14:预测标签。

学习者和度量

getBMRLearners函数提供了关于基准中使用的学习者的详细信息。可以使用此函数获取有关超参数和预测类型等信息。同样,getBMRMeasures函数提供了有关性能度量的详细信息。下表显示了我们在基准实验中使用的度量的详细信息:

getBMRLearners(bmr)

输出如下:

## $multilabel.randomForestSRC
## Learner multilabel.randomForestSRC from package randomForestSRC
## Type: multilabel
## Name: Random Forest; Short name: rfsrc
## Class: multilabel.randomForestSRC
## Properties: missings,numerics,factors,prob,weights
## Predict-Type: response
## Hyperparameters: na.action=na.impute
## 
## 
## $multilabel.rFerns
## Learner multilabel.rFerns from package rFerns
## Type: multilabel
## Name: Random ferns; Short name: rFerns
## Class: multilabel.rFerns
## Properties: numerics,factors,ordered
## Predict-Type: response
## Hyperparameters:

运行getBMRMeasures(bmr)函数:

getBMRMeasures(bmr)

图 9.15:学习者和度量(第一部分)。

图 9.15:学习者和度量(第一部分)。

图 15 和 16 总结了getBMRMeasures(bmr)命令的结果:

图 9.16:学习者和度量(第二部分)。

图 9.16:学习者和度量(第二部分)。

合并基准结果

通常,我们会运行多个实验,并希望将所有实验的基准结果汇总到一个列表中,以便比较结果。mergeBenchmarkResults 函数有助于合并结果。

这里是基准:

lrns = list(makeLearner(“multilabel.randomForestSRC”),
            makeLearner(“multilabel.rFerns”)
            )
bmr = benchmark(lrns, scene.task, measures = MEASURES)
lrn.classif.randomForest = makeLearner(“classif.randomForest”)
bmr.BR.rf = benchmark(lrn.classif.randomForest, scene.task, measures = MEASURES)
mergeBenchmarkResults(list(bmr, bmr.BR.rf))

图 9.17:合并基准结果。

img/C12624_09_17.jpg

图 9.17:合并基准结果。

显然,使用 classif.randomForest 与分类器链包装器产生最高的准确率,并在其他所有指标上也表现良好。

活动十四:使用 classif.C50 学习者而不是 classif.rpart 获取二进制性能步骤

在这个活动中,我们将回顾构建模型的整个流程,我们将使用 makeLearner 函数指定 rpart 模型,替换 C50。具体来说,我们将重新运行整个机器学习流程,从问题转换步骤到使用 classif.C50 学习者而不是 classif.rpart 的二进制性能步骤。

按照以下步骤完成活动:

  1. 定义算法自适应方法。

  2. 使用问题转换方法,并将 classif.rpart 学习者更改为 classif.C50

    注意

    为了使此代码正常工作,你需要安装 C50 包。

  3. 打印学习者的详细信息。

  4. 打印多标签学习者的详细信息。

  5. 使用训练数据集训练模型。

  6. 打印模型详细信息。

  7. 使用我们之前创建的 C50 模型在测试数据集上预测输出。

  8. 打印性能指标。

  9. 打印 listMeasures 变量的性能指标。

  10. 使用交叉验证方法进行重采样。

  11. 打印二进制性能。

一旦你完成活动,你应该看到以下输出:

##             acc.test.mean mmce.test.mean auc.test.mean
## Beach           0.8608226     0.13917740     0.8372448
## Sunset          0.9401745     0.05982551     0.9420085
## FallFoliage     0.9081845     0.09181554     0.9008202
## Field           0.8998754     0.10012464     0.9134458
## Mountain        0.7710843     0.22891566     0.7622767
## Urban           0.8184462     0.18155380     0.7837401

注意

此活动的解决方案可以在第 466 页找到。

使用 OpenML 上传函数工作

为了提高实验的协作和版本控制,我们可以使用 uploadOMLFlow 函数将我们创建的流程上传到 OpenML:

flow.id = uploadOMLFlow(makeLearner(“multilabel.randomForestSRC”))

输出如下:

Downloading from ‘http://www.openml.org/api/v1/flow/exists/mlr.multilabel.randomForestSRC/R_3.2.2-v2.b955a5ec’ to ‘<mem>’.
Do you want to upload the flow? (yes|no)
Uploading flow to the server.
Downloading response to: C:\Users\Karthik\AppData\Local\Temp\Rtmpe4W4BW\file3f044abf30f2.xml
Uploading to ‘http://www.openml.org/api/v1/flow’.
Flow successfully uploaded. Flow ID: 9708

我们鼓励学生探索 OpenML 平台,以找到更多此类功能,因为这个平台帮助全球的研究人员协作和分享他们的工作,使优秀的工作快速传播,并帮助利用研究人员的集体智慧构建最佳模型。

摘要

在本章中,我们使用了 R 语言的 mlr 和 OpenML 包来构建一个完整的机器学习工作流程,用于解决多标签语义场景分类问题。mlr 包提供了一系列丰富的机器学习算法和评估度量,这有助于我们快速实施,并加速了实验过程,以便为问题找到最佳模型。该包还提供了许多包装函数来处理多标签问题。使用像 mlr 这样的强大框架构建现实世界的机器学习模型有助于加快实施速度,并为整个项目提供结构。此外,使用 OpenML,我们可以使用现有的数据集和代码重现研究工作,并根据我们的需求进行修改。这样的平台能够与世界各地的研究人员进行大规模合作。最后,我们还可以上传自己的机器学习流程供他人使用,以便他们从我们停止的地方继续。

在本书中,我们的重点是教授使用 R 编程语言进行监督学习。监督学习是一类算法,其中我们提供了数据的有标签观测。探索性数据分析(EDA)方法有助于深入了解数据集,而 SCQ 框架用于精确设计问题。特征的选择基于问题设计,并在多次实验和评估后选择合适的监督学习模型。然后我们学习了如何在生产环境中部署机器学习模型,这些模型可以被业务中的应用团队使用。此外,在数据集有数百个特征的情况下,我们使用了特征降维和选择技术。

我们想强调的是,在任何机器学习项目中,超过一定阶段(可能是从项目开始算起 3 个月的努力或 100 次不同组合的尝试),我们应该停下来思考我们到目前为止所做的工作是否可以部署到生产环境中。如果答案是肯定的,那么部署它并持续监控任何异常和改进。如果答案是否定的,那么就回到起点重新开始(显然,如果这种奢侈是允许的)。在超参数微调和模型选择等阶段进行机器学习是一种艺术。需要大量的试错实验才能得出最佳模型。

附录

关于

本节包含的内容旨在帮助学生执行书中的活动。它包括学生为完成和实现书中的目标而需要执行的详细步骤。

R 高级分析

活动一:创建 R Markdown 文件以读取 CSV 文件并编写数据摘要

  1. 启动 RStudio 并导航到 文件 | 新建文件 | R Markdown

  2. 在新的 R Markdown 窗口中,提供 标题作者 名称,如图所示。确保在 默认输出格式 部分下选择 Word 选项:![图 1.13:在 RStudio 中创建新的 R Markdown 文件]

    ![图片 C12624_01_121.jpg]

    图 1.13:在 RStudio 中创建新的 R Markdown 文件
  3. 现在,使用 read.csv() 方法读取 bank-full.csv 文件:![图 1.14:使用 read.csv 方法读取数据]

    ![图片 C12624_01_13.jpg]

    图 1.14:使用 read.csv 方法读取数据
  4. 最后,使用 summary 方法将摘要打印到 word 文件中:

![图 1.15:使用 summary 方法后的最终输出]

![图片 C12624_01_14.jpg]

图 1.15:使用 summary 方法后的最终输出

活动二:创建两个矩阵的列表并访问值

  1. 通过从二项分布中随机生成数字创建两个大小为 10 x 44 x 5 的矩阵。分别命名为 mat_Amat_B

    mat_A <- matrix(rbinom(n = 40, size = 100, prob = 0.4),nrow = 10, ncol=4)
    mat_B <- matrix(rbinom(n = 20, size = 100, prob = 0.4),nrow = 4, ncol=5)
    
  2. 现在,将两个矩阵存储在一个列表中:

    list_of_matrices <- list(mat_A = mat_A, mat_B =mat_B)
    
  3. 使用列表访问 mat_A 的第 4 行和第 2 列,并将其存储在变量 A 中,并访问 mat_B 的第 2 行和第 1 列,并将其存储在变量 B 中:

    A <- list_of_matrices[["mat_A"]][4,2]
    B <- list_of_matrices[["mat_B"]][2,1]
    
  4. AB 矩阵相乘,并从 mat_A 的第 2 行和第 1 列中减去:

    list_of_matrices[["mat_A"]][2,1] - (A*B)
    

    输出如下:

    ## [1] -1554
    

活动三:使用 dplyr 和 tidyr 从银行数据创建包含所有数值变量的五个汇总统计量的 DataFrame

  1. 在系统中导入 dplyrtidyr 包:

    library(dplyr)
    library(tidyr)
    Warning: package 'tidyr' was built under R version 3.2.5
    
  2. 创建 df DataFrame 并将其文件导入其中:

    df <- tbl_df(df_bank_detail)
    
  3. 使用 select() 从银行数据中提取所有数值变量,并使用 summarise_all() 方法计算最小值、1st 四分位数、3rd 四分位数、中位数、均值、最大值和标准差:

    df_wide <- df %>%
      select(age, balance, duration, pdays) %>% 
      summarise_all(funs(min = min, 
                          q25 = quantile(., 0.25), 
                          median = median, 
                          q75 = quantile(., 0.75), 
                          max = max,
                          mean = mean, 
                          sd = sd))
    
  4. 结果是一个宽数据框。4 个变量,7 个度量:

    dim(df_wide)
    ## [1]  1 28
    
  5. 将结果存储在名为 df_wide 的宽格式 DataFrame 中,使用 tidyr 函数对其进行重塑,最后将宽格式转换为深度格式,使用 tidyr 包的 gather、separate 和 spread 函数:

    df_stats_tidy <- df_wide %>% gather(stat, val) %>%
      separate(stat, into = c("var", "stat"), sep = "_") %>%
      spread(stat, val) %>%
      select(var,min, q25, median, q75, max, mean, sd) # reorder columns
    print(df_stats_tidy)
    

    输出如下:

    ## # A tibble: 4 x 8
    ##        var   min   q25 median   q75    max       mean         sd
    ## *    <chr> <dbl> <dbl>  <dbl> <dbl>  <dbl>      <dbl>      <dbl>
    ## 1      age    18    33     39    48     95   40.93621   10.61876
    ## 2  balance -8019    72    448  1428 102127 1362.27206 3044.76583
    ## 3 duration     0   103    180   319   4918  258.16308  257.52781
    ## 4    pdays    -1    -1     -1    -1    871   40.19783  100.12875
    

数据探索分析

活动四:绘制多个密度图和箱线图

  1. 首先,在 RStudio 中加载必要的库和包:

    library(ggplot2)
    library(cowplot)
    
  2. bank-additional-full.csv 数据集读取到名为 df 的 DataFrame 中:

    df <- read.csv("bank-additional-full.csv",sep=';')
    
  3. 定义 plot_grid_numeric 函数用于密度图:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
      for(column in list_of_variables){
        plt_matrix[[i]]<-ggplot(data=df,aes_string(x=column)) + 
          geom_density(fill="red",alpha =0.5)  +
          ggtitle(paste("Density Plot for variable:",column)) + theme_bw()
        i<-i+1
      }
      plot_grid(plotlist=plt_matrix,ncol=2)
    }
    
  4. 绘制 campaignpdayspreviousemp.var.rate 变量的密度图:

    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

    ![图 2.27:campaignpdayspreviousemp.var.rate 变量的密度图]

    ![图片 C12624_02_25.jpg]

    图 2.27:展示 pdays、previous、emp.var.rate 变量的密度图

    观察到我们使用直方图获得的解释在密度图中也明显为真。因此,这可以作为查看相同趋势的另一种替代图表。

  5. 对步骤 4 进行重复以生成箱线图:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
      for(column in list_of_variables){
        plt_matrix[[i]]<-ggplot(data=df,aes_string(y=column)) + 
          geom_boxplot(outlier.colour="black") +
          ggtitle(paste("Boxplot for variable:",column)) + theme_bw()
        i<-i+1
      }
      plot_grid(plotlist=plt_matrix,ncol=2)
    }
    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

![图 2.28:展示 pdays、previous、emp.var.rate 变量的箱线图图片

图 2.28:展示 pdays、previous、emp.var.rate 变量的箱线图

现在,让我们探索数据集的最后四个数值变量,即 nr.employedeuribor3mcons.conf.indexduration,看看我们是否能得出一些有意义的见解。

监督学习简介

活动五:按月份在 PRES 和 PM2.5 之间绘制散点图

  1. ggplot2 包导入系统:

    library(ggplot2)
    
  2. ggplot 中,将 a() 方法的组件分配给变量 PRES

    ggplot(data = PM25, aes(x = PRES, y = pm2.5, color = hour)) +   geom_point()
    
  3. geom_smooth() 方法的下一层中,通过传递 colour = "blue" 来区分。

    geom_smooth(method='auto',formula=y~x, colour = "blue", size =1)
    
  4. 最后,在 facet_wrap() 层中,使用 month 变量为每个月绘制单独的隔离图。

    facet_wrap(~ month, nrow = 4)
    

    最终代码将如下所示:

    ggplot(data = PM25, aes(x = PRES, y = pm2.5, color = hour)) +      geom_point() +      geom_smooth(method='auto',formula=y~x, colour = "blue", size =1) +      facet_wrap(~ month, nrow = 4)
    

    图形如下:

![图 3.19:展示 PRES 和 PM2.5 之间关系的散点图图片

图 3.19:显示 PRES 和 PM2.5 之间关系的散点图

活动六:转换变量并推导新变量以构建模型

构建模型时请执行以下步骤:

  1. 将所需的库和包导入系统:

    library(dplyr)
    library(lubridate)
    library(tidyr)
    library(ggplot2)
    library(grid)
    library(zoo)
    
  2. 将年、月、日和小时合并为一个 datetime 变量:

    PM25$datetime <- with(PM25, ymd_h(sprintf('%04d%02d%02d%02d', year, month, day,hour)))
    
  3. 删除任何列中存在缺失值的行:

    PM25_subset <- na.omit(PM25[,c("datetime","pm2.5")])
    
  4. 使用 zoo 包中的 rollapply() 方法计算 PM2.5 的移动平均值;这是为了平滑 PM2.5 读取中的任何噪声:

    PM25_three_hour_pm25_avg <- rollapply(zoo(PM25_subset$pm2.5,PM25_subset$datetime), 3, mean)
    
  5. 创建 PM25 污染的两个级别,0–正常1-高于正常。我们也可以创建超过两个级别;然而,对于最佳适用于二元分类的逻辑回归,我们使用了两个级别:

    PM25_three_hour_pm25_avg <- as.data.frame(PM25_three_hour_pm25_avg)
    PM25_three_hour_pm25_avg$timestamp <- row.names(PM25_three_hour_pm25_avg)
    row.names(PM25_three_hour_pm25_avg) <- NULL
    colnames(PM25_three_hour_pm25_avg) <- c("avg_pm25","timestamp")
    PM25_three_hour_pm25_avg$pollution_level <- ifelse(PM25_three_hour_pm25_avg$avg_pm25 <= 35, 0,1)
    PM25_three_hour_pm25_avg$timestamp <- as.POSIXct(PM25_three_hour_pm25_avg$timestamp, format= "%Y-%m-%d %H:%M:%S",tz="GMT")
    
  6. 将结果数据框(PM25_three_hour_pm25_avg)与其他环境变量的值合并,例如我们在线性回归模型中使用的 TEMPDEWPIws

    PM25_for_class <- merge(PM25_three_hour_pm25_avg, PM25[,c("datetime","TEMP","DEWP","PRES","Iws","cbwd","Is","Ir")], by.x = "timestamp",by.y = "datetime")
    
  7. 使用 TEMP、DEWP 和 Iws 变量在 pollution_level 上拟合广义线性模型 (glm):

    PM25_logit_model <- glm(pollution_level ~ DEWP + TEMP + Iws, data = PM25_for_class,family=binomial(link='logit'))
    
  8. 总结模型:

    summary(PM25_logit_model)
    

    输出如下:

    Call:
    glm(formula = pollution_level ~ DEWP + TEMP + Iws, family = binomial(link = "logit"), 
        data = PM25_for_class)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -2.4699  -0.5212   0.4569   0.6508   3.5824  
    Coefficients:
                  Estimate Std. Error z value Pr(>|z|)    
    (Intercept)  2.5240276  0.0273353   92.34   <2e-16 ***
    DEWP         0.1231959  0.0016856   73.09   <2e-16 ***
    TEMP        -0.1028211  0.0018447  -55.74   <2e-16 ***
    Iws         -0.0127037  0.0003535  -35.94   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 49475  on 41754  degrees of freedom
    Residual deviance: 37821  on 41751  degrees of freedom
    AIC: 37829
    Number of Fisher Scoring iterations: 5
    

回归

活动七:不使用 summary 函数使用模型对象打印各种属性

  1. 首先,使用以下命令打印系数值。确保输出类似于使用 coefficients 选项的 summary 函数的输出。系数是从使用 OLS 算法的模型中得到的拟合值:

    multiple_PM25_linear_model$coefficients
    

    输出如下:

    (Intercept)        DEWP        TEMP         Iws 
    161.1512066   4.3841960  -5.1335111  -0.2743375
    
  2. 查找预测值和实际 PM2.5 值之间的残差值(差异),应尽可能小。残差反映了使用系数得到的拟合值与实际值之间的距离。

    multiple_PM25_linear_model$residuals
    

    输出如下:

    25            26            27            28 
      17.95294914   32.81291348   21.38677872   26.34105878 
               29            30            31            32 
    
  3. 接下来,找到最适合模型的最佳 PM2.5 实际值的拟合值。使用系数,我们可以计算拟合值:

    multiple_PM25_linear_model$fitted.values
    

    输出如下:

    25         26         27         28         29 
    111.047051 115.187087 137.613221 154.658941 154.414781 
            30         31         32         33         34 
    
  4. 查找 R-Squared 值。它们应该与你在summary函数输出中“Multiple R-squared”旁边的值相同。R-Square 有助于评估模型性能。如果值接近 1,则模型越好:

    summary(multiple_PM25_linear_model)$r.squared
    

    输出如下:

    [1] 0.2159579
    
  5. 查找 F-Statistic 值。确保输出应与你在summary函数输出中“F-Statistics”旁边的输出相同。这将告诉你模型是否比仅使用目标变量的均值拟合得更好。在许多实际应用中,F-Statistic 与 p-values 一起使用:

    summary(multiple_PM25_linear_model)$fstatistic
    

    输出如下:

        value     numdf     dendf 
     3833.506     3.000 41753.000 
    
  6. 最后,找到系数 p-values,并确保值应与你在summary函数中每个变量的“Coefficients”部分下获得的值相同。它将出现在标题为“Pr(>|t|)”的列下。如果值小于 0.05,则变量在预测目标变量时具有统计学意义:

    summary(multiple_PM25_linear_model)$coefficients[,4]
    

    输出如下:

      (Intercept)          DEWP          TEMP           Iws 
     0.000000e+00  0.000000e+00  0.000000e+00 4.279601e-224
    

模型的属性对于理解同样重要,尤其是在线性回归中,以便获得预测。它们有助于很好地解释模型并将问题与其实际用例联系起来。

分类

活动 8:使用附加特征构建逻辑回归模型

  1. 创建df_new数据框的副本到df_copy以进行活动:

    df_copy <- df_new
    
  2. 为每个选定的三个数值特征创建新的特征,包括平方根、平方幂和立方幂转换:

    df_copy$MaxTemp2 <- df_copy$MaxTemp ²
    df_copy$MaxTemp3 <- df_copy$MaxTemp ³
    df_copy$MaxTemp_root <- sqrt(df_copy$MaxTemp)
    df_copy$Rainfall2 <- df_copy$Rainfall ²
    df_copy$Rainfall3 <- df_copy$Rainfall ³
    df_copy$Rainfall_root <- sqrt(df_copy$Rainfall)
    df_copy$Humidity3pm2 <- df_copy$Humidity3pm ²
    df_copy$Humidity3pm3 <- df_copy$Humidity3pm ³
    df_copy$Humidity3pm_root <- sqrt(df_copy$Humidity3pm)
    
  3. df_copy数据集分为 70:30 的训练和测试比例:

    #Setting seed for reproducibility
    set.seed(2019)
    #Creating a list of indexes for the training dataset (70%)
    train_index <- sample(seq_len(nrow(df_copy)),floor(0.7 * nrow(df_copy)))
    #Split the data into test and train
    train_new <- df_copy[train_index,]
    test_new <- df_copy[-train_index,]
    
  4. 使用新的训练数据拟合逻辑回归模型:

    model <- glm(RainTomorrow~., data=train_new ,family=binomial(link='logit'))
    
  5. 使用训练数据上的拟合模型预测响应并创建混淆矩阵:

    print("Training data results -")
    pred_train <-factor(ifelse(predict(model,newdata=train_new,
    type = "response") > 0.5,"Yes","No"))
    #Create the Confusion Matrix
    train_metrics <- confusionMatrix(pred_train, train_new$RainTomorrow,positive="Yes")
    print(train_metrics)
    

    输出如下:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  58330  8650
           Yes  3161  8906
    
                   Accuracy : 0.8506          
                     95% CI : (0.8481, 0.8531)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5132          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5073          
                Specificity : 0.9486          
             Pos Pred Value : 0.7380          
             Neg Pred Value : 0.8709          
                 Prevalence : 0.2221          
             Detection Rate : 0.1127          
       Detection Prevalence : 0.1527          
          Balanced Accuracy : 0.7279          
    
           'Positive' Class : Yes 
    
  6. 使用拟合模型在测试数据上预测响应并创建混淆矩阵:

    print("Test data results -")
    pred_test <-factor(ifelse(predict(model,newdata=test_new,
    type = "response") > 0.5,"Yes","No"))
    #Create the Confusion Matrix
    test_metrics <- confusionMatrix(pred_test, test_new$RainTomorrow,positive="Yes")
    print(test_metrics)
    

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25057  3640
           Yes  1358  3823
    
                   Accuracy : 0.8525          
                     95% CI : (0.8486, 0.8562)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.5176          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.5123          
                Specificity : 0.9486          
             Pos Pred Value : 0.7379          
             Neg Pred Value : 0.8732          
                 Prevalence : 0.2203          
             Detection Rate : 0.1128          
       Detection Prevalence : 0.1529          
          Balanced Accuracy : 0.7304          
    
           'Positive' Class : Yes             
    

活动 9:创建具有附加控制参数的决策树模型

  1. 加载rpart库。

    library(rpart)
    
  2. 创建具有新值minsplit =15cp = 0.00的决策树控制对象:

    control = rpart.control(
        minsplit = 15, 
        cp = 0.001)
    
  3. 使用训练数据拟合树模型并将控制对象传递给rpart函数:

    tree_model <- rpart(RainTomorrow~.,data=train, control = control)
    
  4. 绘制复杂度参数图以查看树在不同CP值下的表现:

    plotcp(tree_model)
    

    输出如下:

    ![图 5.10:决策树输出 img/C12624_05_071.jpg

    图 5.10:决策树输出
  5. 使用拟合模型在训练数据上做出预测并创建混淆矩阵:

    print("Training data results -")
    pred_train <- predict(tree_model,newdata = train,type = "class")
    confusionMatrix(pred_train, train$RainTomorrow,positive="Yes")
    

    输出如下:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  58494  9032
           Yes  2997  8524
    
                   Accuracy : 0.8478          
                     95% CI : (0.8453, 0.8503)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4979          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4855          
                Specificity : 0.9513          
             Pos Pred Value : 0.7399          
             Neg Pred Value : 0.8662          
                 Prevalence : 0.2221          
             Detection Rate : 0.1078          
       Detection Prevalence : 0.1457          
          Balanced Accuracy : 0.7184          
    
           'Positive' Class : Yes             
    
  6. 使用拟合的模型对测试数据进行预测并创建混淆矩阵:

    print("Test data results -")
    pred_test <- predict(tree_model,newdata = test,type = "class")
    confusionMatrix(pred_test, test$RainTomorrow,positive="Yes")
    

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25068  3926
           Yes  1347  3537
    
                   Accuracy : 0.8444          
                     95% CI : (0.8404, 0.8482)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4828          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.4739          
                Specificity : 0.9490          
             Pos Pred Value : 0.7242          
             Neg Pred Value : 0.8646          
                 Prevalence : 0.2203          
             Detection Rate : 0.1044          
       Detection Prevalence : 0.1442          
          Balanced Accuracy : 0.7115          
    
           'Positive' Class : Yes 
    

活动 10:构建具有更多树的随机森林模型

  1. 首先,使用以下命令导入randomForest库:

    library(randomForest)
    
  2. 使用所有可用独立特征构建随机森林模型。将模型中的树的数量定义为 500。

    rf_model <- randomForest(RainTomorrow ~ . , data = train, ntree = 500,                                             importance = TRUE, 
                                                maxnodes=60)
    
  3. 在训练数据上评估:

    print("Training data results -")
    pred_train <- predict(rf_model,newdata = train,type = "class")
    confusionMatrix(pred_train, train$RainTomorrow,positive="Yes")
    

    以下为输出:

    "Training data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  59638 10169
           Yes  1853  7387
    
                   Accuracy : 0.8479          
                     95% CI : (0.8454, 0.8504)
        No Information Rate : 0.7779          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4702          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.42077         
                Specificity : 0.96987         
             Pos Pred Value : 0.79946         
             Neg Pred Value : 0.85433         
                 Prevalence : 0.22210         
             Detection Rate : 0.09345         
       Detection Prevalence : 0.11689         
          Balanced Accuracy : 0.69532         
    
           'Positive' Class : Yes 
    
  4. 在测试数据上评估:

    print("Test data results -")
    pred_test <- predict(rf_model,newdata = test,type = "class")
    confusionMatrix(pred_test, test$RainTomorrow,positive="Yes")
    

    输出如下:

    "Test data results -"
    Confusion Matrix and Statistics
              Reference
    Prediction    No   Yes
           No  25604  4398
           Yes   811  3065
    
                   Accuracy : 0.8462          
                     95% CI : (0.8424, 0.8501)
        No Information Rate : 0.7797          
        P-Value [Acc > NIR] : < 2.2e-16       
    
                      Kappa : 0.4592          
     Mcnemar's Test P-Value : < 2.2e-16       
    
                Sensitivity : 0.41069         
                Specificity : 0.96930         
             Pos Pred Value : 0.79076         
             Neg Pred Value : 0.85341         
                 Prevalence : 0.22029         
             Detection Rate : 0.09047         
       Detection Prevalence : 0.11441         
          Balanced Accuracy : 0.69000         
    
           'Positive' Class : Yes  
    

特征选择和降维

活动 11:将北京 PM2.5 数据集的 CBWD 特征转换为单热编码列

  1. 将北京 PM2.5 数据集读取到 DataFrame PM25中:

    PM25 <- read.csv("PRSA_data_2010.1.1-2014.12.31.csv")
    
  2. 创建一个变量cbwd_one_hot来存储dummyVars函数的结果,其第一个参数为~ cbwd

    library(caret)
    cbwd_one_hot <- dummyVars(" ~ cbwd", data = PM25) 
    
  3. 使用predict()函数在cbwd_one_hot上的输出,并将其转换为 DataFrame:

    cbwd_one_hot <- data.frame(predict(cbwd_one_hot, newdata = PM25))
    
  4. PM25 DataFrame 中删除原始的cbwd变量:

    PM25$cbwd <- NULL
    
  5. 使用cbind()函数,将cbwd_one_hot添加到PM25 DataFrame 中:

    PM25 <- cbind(PM25, cbwd_one_hot)
    
  6. 打印PM25的前 6 行:

    head(PM25)
    

    上一条命令的输出如下:

    ##   No year month day hour pm2.5 DEWP TEMP PRES   Iws Is Ir cbwd.cv cbwd.NE
    ## 1  1 2010     1   1    0    NA  -21  -11 1021  1.79  0  0       0       0
    ## 2  2 2010     1   1    1    NA  -21  -12 1020  4.92  0  0       0       0
    ## 3  3 2010     1   1    2    NA  -21  -11 1019  6.71  0  0       0       0
    ## 4  4 2010     1   1    3    NA  -21  -14 1019  9.84  0  0       0       0
    ## 5  5 2010     1   1    4    NA  -20  -12 1018 12.97  0  0       0       0
    ## 6  6 2010     1   1    5    NA  -19  -10 1017 16.10  0  0       0       0
    ##   cbwd.NW cbwd.SE
    ## 1       1       0
    ## 2       1       0
    ## 3       1       0
    ## 4       1       0
    ## 5       1       0
    ## 6       1       0
    

观察head(PM25)命令的输出中的变量cbwd:它现在已转换为带有NENWSE后缀的单热编码列。

模型改进

活动 12:执行重复 K 折交叉验证和网格搜索优化

  1. 加载用于练习所需的包mlbenchcaretdplyr

    library(mlbench)
    library(dplyr)
    library(caret)
    
  2. mlbench包中将PimaIndianDiabetes数据集加载到内存中:

    data(PimaIndiansDiabetes)
    df<-PimaIndiansDiabetes
    
  3. 设置一个seed值为2019以确保可重复性:

    set.seed(2019)
    
  4. 使用caret包中的trainControl函数定义 K 折验证对象,并将method定义为repeatedcv而不是cv。在trainControl函数中定义一个额外的结构来指定验证的重复次数repeats = 10

    train_control = trainControl(method = "repeatedcv",                                number=5,                               repeats = 10,   savePredictions = TRUE,verboseIter = TRUE)
    
  5. 将随机森林模型的超参数mtry的网格定义为(3,4,5)

    parameter_values = expand.grid(mtry=c(3,4,5))
    
  6. 使用网格值、交叉验证对象和随机森林分类器拟合模型:

    model_rf_kfold<- train(diabetes~., data=df, trControl=train_control,                    method="rf",  metric= "Accuracy", tuneGrid = parameter_values)
    
  7. 通过打印平均准确率和准确率标准差来研究模型性能:

    print(paste("Average Accuracy :",mean(model_rf_kfold$resample$Accuracy)))
    print(paste("Std. Dev Accuracy :",sd(model_rf_kfold$resample$Accuracy)))
    
  8. 通过绘制不同超参数值下的准确率来研究模型性能:

    plot(model_rf_kfold)
    

    最终输出如下:

图 7.17:不同超参数值下的模型性能准确率

图 7.17:不同超参数值下的模型性能准确率

在此图中,我们可以看到我们对同一模型执行了重复的 k 折交叉验证和网格搜索优化。

模型部署

活动 13:使用 Plumber 部署 R 模型

  1. 创建一个model.r脚本,该脚本将加载所需的库、数据、拟合回归模型以及必要的函数来对未见数据进行预测:

  2. 加载包含此活动数据的mlbench库:

    library(mlbench)
    
  3. BostonHousing数据加载到 DataFrame df中:

    data(BostonHousing)
    df<-BostonHousing
    
  4. 使用 df 的前400行创建训练数据集,并使用剩余的数据进行测试:

    train <- df[1:400,]
    test <- df[401:dim(df)[1],]
    
  5. 使用lm函数拟合逻辑回归模型,以medv(中位数)作为因变量和 10 个自变量,例如crimzninduschasnoxrmagedisradtax

    model <- lm(medv~crim+zn+indus+chas+
     nox+rm+age+dis+rad+tax,data=train)
    
  6. 定义一个模型端点为predict_data;这将被用作 Plumber 的 API 端点:

    #' @get /predict_data
    function(crim,zn,indus,chas,nox,rm,age,dis,rad,tax){
    
  7. 在函数内部,将参数转换为数值和因子(因为 API 调用将只传递字符串):

      crim <- as.numeric(crim)
      zn <- as.numeric(zn)
      indus <- as.numeric(indus)
      chas <- as.factor(chas)
      nox <- as.numeric(nox)
      rm <- as.numeric(rm)
      age <- as.numeric(age)
      dis <- as.numeric(dis)
      rad <- as.numeric(rad)
      tax <- as.numeric(tax)
    
  8. 将模型的 10 个独立特征作为名为sample的 DataFrame 进行包装,列名保持一致:

      sample <- data.frame(crim  = crim,  zn  = zn,  indus  = indus,  
                           chas  = chas,  nox  = nox,  rm  = rm,  
                           age  = age,  dis  = dis,  rad  = rad,  
                           tax  = tax )
    
  9. sample DataFrame 传递给预测函数,并使用在第 4 步中创建的模型,返回预测结果:

      y_pred<-predict(model,newdata=sample)
    
      list(Answer=y_pred)
    }
    

    整个model.r文件将看起来像这样:

    library(mlbench)
    data(BostonHousing)
    df<-BostonHousing
    train <- df[1:400,]
    test <- df[401:dim(df)[1],]
    model <- lm(medv~crim+zn+indus+chas+nox+rm+age+dis+rad+tax,data=train)
    #' @get /predict_data
    function(crim,zn,indus,chas,nox,rm,age,dis,rad,tax){
    
      crim <- as.numeric(crim)
      zn <- as.numeric(zn)
      indus <- as.numeric(indus)
      chas <- as.factor(chas)
      nox <- as.numeric(nox)
      rm <- as.numeric(rm)
      age <- as.numeric(age)
      dis <- as.numeric(dis)
      rad <- as.numeric(rad)
      tax <- as.numeric(tax)
    
      sample <- data.frame(crim  = crim,  zn  = zn,  indus  = indus,  
                           chas  = chas,  nox  = nox,  rm  = rm,  
                           age  = age,  dis  = dis,  rad  = rad,  
                           tax  = tax )
    
      y_pred<-predict(model,newdata=sample)
    
      list(Answer=y_pred)
    }
    
  10. 加载plumber库。

    library(plumber)
    
  11. 使用plumb函数创建一个 plumber 对象,并传递第一部分中创建的model.r文件。

    r <- plumb(model.r)
    
  12. 通过传递主机名localhost127.0.0.1和一个端口号,例如8080,运行 plumber 对象。

    http://127.0.0.1:8080/
    
  13. 使用浏览器或 Postman 测试部署的模型并调用 API。

    API 调用:

    http://127.0.0.1:8080/predict_

    ata?crim=0.01&zn=18&indus=2.3&chas=0&nox=0.5&rm=6&

    age=65&dis=4&rad=1&tax=242

    {"Answer":[22.5813]}
    

基于研究论文的综合项目

活动 14:使用 classif.C50 学习器而不是 classif.rpart 获取二进制性能步骤

  1. 定义算法自适应方法:

    multilabel.lrn3 = makeLearner("multilabel.rFerns")
    multilabel.lrn4 = makeLearner("multilabel.randomForestSRC")
    multilabel.lrn3
    

    输出如下:

    ## Learner multilabel.rFerns from package rFerns
    ## Type: multilabel
    ## Name: Random ferns; Short name: rFerns
    ## Class: multilabel.rFerns
    ## Properties: numerics,factors,ordered
    ## Predict-Type: response
    ## Hyperparameters:
    
  2. 使用问题转换方法,并将classif.rpart学习器更改为classif.C50

    lrn = makeLearner("classif.C50", predict.type = "prob")
    multilabel.lrn1 = makeMultilabelBinaryRelevanceWrapper(lrn)
    multilabel.lrn2 = makeMultilabelNestedStackingWrapper(lrn)
    

    注意

    您需要安装C50包以确保此代码能够运行。

  3. 打印学习器详细信息:

    lrn
    

    输出如下:

    ## Learner classif.C50 from package C50
    ## Type: classif
    ## Name: C50; Short name: C50
    ## Class: classif.C50
    ## Properties: twoclass,multiclass,numerics,factors,prob,missings,weights
    ## Predict-Type: prob
    ## Hyperparameters:
    
  4. 打印多标签学习器详细信息:

    multilabel.lrn1
    

    输出如下:

    ## Learner multilabel.binaryRelevance.classif.C50 from package C50
    ## Type: multilabel
    ## Name: ; Short name: 
    ## Class: MultilabelBinaryRelevanceWrapper
    ## Properties: numerics,factors,missings,weights,prob,twoclass,multiclass
    ## Predict-Type: prob
    ## Hyperparameters:
    
  5. 使用与训练数据集相同的数据集来训练模型:

    df_nrow <- nrow(df_scene)
    df_all_index <- c(1:df_nrow)
    train_index <- sample(1:df_nrow, 0.7*df_nrow)
    test_index <- setdiff(df_all_index,train_index)
    scene_classi_mod = train(multilabel.lrn1, scene.task, subset = train_index)
    
  6. 打印模型详细信息:

    scene_classi_mod
    

    输出如下:

    ## Model for learner.id=multilabel.binaryRelevance.classif.C50; learner.class=MultilabelBinaryRelevanceWrapper
    ## Trained on: task.id = multi; obs = 1684; features = 294
    ## Hyperparameters:
    
  7. 使用我们为测试数据集创建的C50模型进行预测输出:

    pred = predict(scene_classi_mod, task = scene.task, subset = test_index)
    names(as.data.frame(pred))
    

    输出如下:

    ##  [1] "id"                   "truth.Beach"          "truth.Sunset"        
    ##  [4] "truth.FallFoliage"    "truth.Field"          "truth.Mountain"      
    ##  [7] "truth.Urban"          "prob.Beach"           "prob.Sunset"         
    ## [10] "prob.FallFoliage"     "prob.Field"           "prob.Mountain"       
    ## [13] "prob.Urban"           "response.Beach"       "response.Sunset"     
    ## [16] "response.FallFoliage" "response.Field"       "response.Mountain"   
    ## [19] "response.Urban"
    
  8. 打印性能度量:

    MEASURES = list(multilabel.hamloss, multilabel.f1, multilabel.subset01, multilabel.acc, multilabel.tpr, multilabel.ppv)
    performance(pred, measures = MEASURES)
    

    输出如下:

    ##  multilabel.hamloss       multilabel.f1 multilabel.subset01 
    ##           0.1258645           0.5734901           0.5532503 
    ##      multilabel.acc      multilabel.tpr      multilabel.ppv 
    ##           0.5412633           0.6207930           0.7249104
    
  9. 打印listMeasures变量的性能度量:

    listMeasures("multilabel")
    

    输出如下:

    ##  [1] "featperc"            "multilabel.tpr"      "multilabel.hamloss" 
    ##  [4] "multilabel.subset01" "timeboth"            "timetrain"          
    ##  [7] "timepredict"         "multilabel.ppv"      "multilabel.f1"      
    ## [10] "multilabel.acc"
    
  10. 使用交叉验证方法进行重采样:

    rdesc = makeResampleDesc(method = "CV", stratify = FALSE, iters = 3)
    r = resample(learner = multilabel.lrn1, task = scene.task, resampling = rdesc,measures = list(multilabel.hamloss), show.info = FALSE)
    r
    

    输出如下:

    ## Resample Result
    ## Task: multi
    ## Learner: multilabel.binaryRelevance.classif.C50
    ## Aggr perf: multilabel.hamloss.test.mean=0.1335695
    ## Runtime: 72.353
    
  11. 打印二进制性能:

    getMultilabelBinaryPerformances(r$pred, measures = list(acc, mmce, auc))
    

    输出如下:

    ##             acc.test.mean mmce.test.mean auc.test.mean
    ## Beach           0.8608226     0.13917740     0.8372448
    ## Sunset          0.9401745     0.05982551     0.9420085
    ## FallFoliage     0.9081845     0.09181554     0.9008202
    ## Field           0.8998754     0.10012464     0.9134458
    ## Mountain        0.7710843     0.22891566     0.7622767
    ## Urban           0.8184462     0.18155380     0.7837401
    
posted @ 2025-09-03 10:24  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报