Julia-数据科学-全-

Julia 数据科学(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据科学家:21 世纪最性感的职业,《哈佛商业评论》。那么,为什么选择 Julia?作为一种高级语言,拥有庞大的科学社区,并且性能可与 C 相媲美,Julia 被誉为数据科学的最佳语言。使用 Julia,我们可以创建统计模型、高效的机器学习系统,并生成美观、引人注目的可视化。

本书内容

第一章,基础知识 - Julia 环境,解释了如何设置 Julia 的环境(命令行(REPL)和 Jupyter Notebook),并介绍了 Julia 的生态系统,为什么 Julia 是特别的,以及包管理。它还介绍了并行处理和多重派发,并解释了 Julia 如何适用于数据科学。

第二章,数据清理,解释了数据准备的必要性和过程,也叫做数据清理。数据清理指的是将数据从一种状态转变为另一种状态,且这些步骤是明确可逆的。它是为了将数据准备好以用于分析和可视化。

第三章,数据探索,解释了统计学是数据科学的核心,展示了 Julia 提供了多种统计功能。本章将对统计学做一个高级概述,并将解释如何使用 Julia 的统计包,如 Stats.jl 和 Distributions.jl,将这些统计概念应用于一般问题的技术。

第四章,深入探讨推断统计学,继续讲述统计学是数据科学的核心,并且 Julia 提供了多种统计功能。本章将对高级统计学做一个概述,然后解释如何使用 Julia 的统计包,如 Stats.jl 和 Distributions.jl,将这些统计概念应用于一般问题的技术。

第五章,使用可视化理解数据,解释了数据可视化是数据科学的重要组成部分,以及它如何使结果的沟通更加有效,并触及更广泛的受众。本章将介绍 Julia 中的 Vega、Asciiplot 和 Gadfly 包,它们用于数据可视化。

第六章,有监督机器学习,引述了汤姆·M·米切尔的名言:“如果一个计算机程序能够通过经验 E 来学习,并且在任务 T 中的表现(通过 P 衡量)随着经验 E 的增加而改进,那么我们就说它从经验中学习。”机器学习是一个研究领域,它赋予计算机学习和改进的能力,而无需明确编程。本章将解释 Julia 是一种高性能的高级语言,非常适合用于机器学习。本章将重点介绍有监督机器学习算法,如朴素贝叶斯、回归分析和决策树。

第七章,无监督机器学习,解释了无监督学习与有监督学习有所不同且更为复杂。其目的是让系统学习一些东西,但我们并不知道它会学到什么。本章将重点介绍无监督学习算法,如聚类。

第八章,创建集成模型,解释了一个团队比单个人更能做出更好的决策,尤其当每个团队成员都有自己的偏见时。机器学习也遵循这一规律。本章将重点讨论一种机器学习技术——集成学习,随机森林就是一个例子。

第九章,时间序列,展示了决策建模的能力,并解释了检查在一些实际应用中的关键作用,从重症监护室中的紧急医疗处理到军事指挥与控制系统。本章重点讨论时间序列数据和使用 Julia 进行预测。

第十章,协同过滤与推荐系统,解释了我们每天都面临决策和选择。这些选择可以从我们穿什么衣服到我们看什么电影,或者我们在网上点餐时吃什么。我们在商业中也做出决策。例如,我们应该投资哪只股票?如果决策能够被自动化,并且可以给我们提供合适的推荐呢?本章重点讨论推荐系统和诸如协同过滤和关联规则挖掘等技术。

第十一章,深度学习简介,解释了深度学习是一类通过利用多个非线性信息处理层进行无监督或有监督的特征提取、模式分析或分类的机器学习技术。本章将介绍如何在 Julia 中进行深度学习。深度学习是机器学习的一个新分支,其目标是——人工智能。我们还将了解 Julia 的深度学习框架 Mocha.jl,并学习如何利用它来实现深度学习。

本书所需的内容

读者需要一台拥有较新操作系统(推荐 64 位系统)、支持 Linux、Windows 7+或 Mac OS 的计算机,且需具备有效的互联网连接及安装 Julia、Git 和本书中使用的各种包的权限。

本书适合的读者

本书的标准读者群体是那些对 Julia 语言的基础知识了解较少的、希望探索如何利用 Julia 的生态系统进行数据科学的数据分析师和有志成为数据科学家的人员。此外,还有一些具备 Python 或 R 技能的用户,他们希望通过使用 Julia 提升进行数据科学的效率。我们期待读者具备一定的统计学和计算数学背景。

约定

本书中包含多种文本样式,用于区分不同类型的信息。以下是这些样式的示例及其解释。

文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名均按以下方式显示:“Julia 还提供了一个名为summarystats()的函数。”

一段代码块设置如下:

ci(x::HypothesisTests.FisherExactTest) 
ci(x::HypothesisTests.FisherExactTest, alpha::Float64)
ci(x::HypothesisTests.TTest) 
ci(x::HypothesisTests.TTest, alpha::Float64)

任何命令行输入或输出均按以下方式编写:

julia> Pkg.update() 
julia> Pkg.add("StatsBase")

新术语重要词汇以粗体显示。

注意

警告或重要说明会以如下框架形式出现。

提示

提示和技巧会以这样的方式展示。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出能够让您真正从中受益的书籍。

若要向我们发送一般性反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中注明书名。

如果您在某个领域拥有专长,并且有兴趣撰写或为书籍贡献内容,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 书籍的自豪拥有者,我们提供了多种方式帮助您充分利用您的购买。

下载示例代码

您可以从您的账户中下载本书的示例代码文件,网址是 www.packtpub.com。如果您是在其他地方购买的本书,可以访问 www.packtpub.com/support,注册后,我们会直接通过电子邮件将文件发送给您。

您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击 代码下载与勘误

  4. 搜索 框中输入书名。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的渠道。

  7. 点击 代码下载

下载文件后,请确保使用最新版本的工具解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Mac 用户请使用 Zipeg / iZip / UnRarX

  • Linux 用户请使用 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Julia-for-data-science。我们还提供了其他书籍和视频的代码包,您可以在我们的丰富目录中找到,网址是 github.com/PacktPublishing/。快去看看吧!

下载本书的彩色图片

我们还为您提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。这些彩色图片将帮助您更好地理解输出结果的变化。您可以通过 www.packtpub.com/sites/default/files/downloads/JuliaforDataScience_ColorImages.pdf 下载该文件。

勘误

尽管我们已经尽力确保内容的准确性,但错误还是会发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们非常感谢您向我们报告。这样,您可以帮助其他读者避免困扰,并帮助我们改进后续版本。如果您发现任何勘误,请访问 www.packtpub.com/submit-errata 提交,选择您的书籍,点击 勘误提交表单 链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,勘误将上传到我们的网站,或者添加到该书名的勘误部分中的现有勘误列表中。

要查看先前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将在 勘误 部分下显示。

盗版

网络上侵犯版权的行为在所有媒体中都存在着持续性的问题。在 Packt,我们非常重视版权和许可证的保护。如果您在网上遇到任何我们作品的非法复制品,请立即向我们提供相关的地址或网站名称,以便我们采取措施。

请通过 copyright@packtpub.com 与我们联系,并提供涉嫌侵权材料的链接。

我们感谢您帮助保护我们的作者和我们提供有价值内容的能力。

问题

如果您对本书的任何部分有问题,可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章. 基础工作——Julia 的环境

Julia 是一门相对年轻的编程语言。2009 年,三位开发者(Stefan Karpinski、Jeff Bezanson 和 Viral Shah)在麻省理工学院(MIT)应用计算组,在 Alan Edelman 教授的指导下,开始了一个项目,最终演变为 Julia。2012 年 2 月,Julia 公开发布并开源。源代码可以在 GitHub 上找到(github.com/JuliaLang/julia)。已注册包的源代码也可以在 GitHub 上找到。目前,最初的四位创始人以及来自世界各地的开发者都在积极为 Julia 做出贡献。

注意

当前版本为 0.4,距离 1.0 版本候选发布还有一段距离。

基于稳固的原则,它在科学计算、数据科学和高性能计算领域的受欢迎程度正在稳步上升。

本章将引导你完成所有必要的 Julia 组件的下载和安装。本章涵盖以下主题:

  • Julia 有什么不同?

  • 设置 Julia 环境。

  • 使用 Julia 的 Shell 和 REPL。

  • 使用 Jupyter 笔记本

  • 包管理

  • 并行计算

  • 多重分派

  • 语言互操作性

传统上,科学界使用较慢的动态语言来构建应用程序,尽管这些应用程序需要最高的计算性能。有编程经验但并非经验丰富的开发者的领域专家总是更倾向于使用动态语言而非静态类型语言。

Julia 与众不同

随着编译器技术和语言设计的进步,随着时间的推移,性能与动态原型之间的权衡已经可以被消除。因此,科学计算所需的就是像 Python 这样的良好动态语言,同时又有像 C 一样的性能。然后,Julia 诞生了,这是一种根据科学和技术计算需求设计的通用编程语言,提供了与 C/C++相当的性能,并且其环境足够高效,适合像 Python 那样的高层次动态语言进行原型设计。Julia 性能的关键在于它的设计和基于低级虚拟机(LLVM)的即时编译器,使得它能够接近 C 和 Fortran 的性能。

Julia 提供的主要特点是:

  • 一种设计为在数值和科学计算中有效的通用高层次动态编程语言

  • 一个基于低级虚拟机LLVM)的即时编译JIT)编译器,使得 Julia 能够接近像 C/C++这类静态编译语言的性能

以下引用来自 Julia 开发团队——Jeff Bezanson、Stefan Karpinski、Viral Shah 和 Alan Edelman:

注意

我们是贪婪的:我们想要更多。

我们需要一种开源的语言,具有宽松的许可证。我们希望它能具备 C 的速度和 Ruby 的动态性。我们希望它是一种自同构的语言,拥有类似 Lisp 的真正宏功能,但又能像 Matlab 一样具有明显且熟悉的数学符号。我们希望它在通用编程中和 Python 一样易用,在统计分析中和 R 一样方便,在字符串处理上和 Perl 一样自然,在线性代数上和 Matlab 一样强大,在将程序结合在一起上和 Shell 一样优秀。我们希望它简单易学,同时也能满足最严苛黑客的需求。我们希望它是交互式的,并且是编译型的。

(我们是不是提到过它应该和 C 一样快?)

它常常与 Python、R、MATLAB 和 Octave 进行比较。这些语言已经存在了相当长的时间,Julia 受它们的影响很大,尤其是在数值和科学计算方面。虽然 Julia 在这方面表现非常出色,但它并不限于科学计算,它也可以用于 Web 开发和通用编程。

Julia 的开发团队旨在创建一种前所未有的强大与高效结合,同时不妥协易用性的语言。Julia 的核心大部分是用 C/C++ 实现的,Julia 的解析器是用 Scheme 编写的。Julia 高效且跨平台的 I/O 由 Node.js 的 libuv 提供支持。

Julia 的特点和优势可以总结如下:

  • 它设计用于分布式和并行计算。

  • Julia 提供了大量的数学函数库,具有出色的数值精度。

  • Julia 提供了多重调度功能。多重调度是指使用多种参数类型组合来定义函数行为。

  • Pycall 包使 Julia 可以在代码中调用 Python 函数,并使用 Matlab.jl 调用 Matlab 包。用 C 编写的函数和库也可以直接调用,无需任何 API 或包装器。

  • Julia 提供了强大的类 Unix Shell 功能,用于管理系统中的其他进程。

  • 与其他语言不同,Julia 中的用户定义类型既紧凑又非常快速,堪比内建类型。

  • 数据分析大量使用矢量化代码来提高性能。Julia 消除了为了提升性能而进行代码矢量化的需求。用 Julia 编写的非矢量化代码可以和矢量化代码一样快。

  • 它使用轻量级的“绿色”线程,也称为任务或协程、协作多任务处理或一次性延续。

  • Julia 拥有强大的类型系统。提供的转换既优雅又可扩展。

  • 它对 Unicode 提供高效支持。

  • 它具有元编程和类似 Lisp 的宏功能。

  • 它内置了包管理器(Pkg)。

  • Julia 提供了高效、专业化的代码生成,能够针对不同的参数类型进行自动化处理。

  • 它是免费的,开源的,并且使用 MIT 许可证。

环境设置

Julia 是免费的,可以从其官方网站下载,网址为:julialang.org/downloads/。该网站还提供了详尽的文档、示例以及教程和社区的链接。文档可以以流行格式下载。

安装 Julia(Linux)

Ubuntu/Linux Mint 是最著名的 Linux 发行版之一,它们也提供了 Julia 的 deb 包。这些包同时支持 32 位和 64 位版本。

要安装 Julia,请添加 PPA个人软件包档案)。Ubuntu 用户有特权使用 PPA,它被视为一个 apt 仓库,用来构建和发布 Ubuntu 源包。在终端中输入以下命令:

sudo apt-get add-repository ppa:staticfloat/juliareleases 
sudo apt-get update 

这会添加 PPA 并更新仓库中的软件包索引。

现在安装 Julia:

sudo apt-get install Julia 

安装完成。要检查安装是否成功,请在终端中输入以下命令:

julia --version 

这会显示已安装的 Julia 版本。

安装 Julia(Linux)

要打开 Julia 的交互式 shell,在终端中输入 julia。要卸载 Julia,只需使用 apt 删除它:

sudo apt-get remove julia 

对于 Fedora/RHEL/CentOS 或基于它们的发行版,请启用你所在版本的 EPEL 仓库。然后,点击提供的链接。使用以下命令启用 Julia 的仓库:

dnf copr enable nalimilan/julia

或者复制相关的 .repo 文件,下载方式如下:

/etc/yum.repos.d/

最后,在终端中输入以下命令:

yum install julia

安装 Julia(Mac)

使用 Mac OS X 的用户需要点击下载的 .dmg 文件来运行磁盘映像。然后,将应用程序图标拖到 Applications 文件夹中。系统可能会提示你是否继续,因为该源来自互联网,因此被认为不安全。如果是从 Julia 官方网站下载的,可以点击继续。

也可以通过 homebrew 在 Mac 上安装 Julia,如下所示:

brew update 
brew tap staticfloat/julia 
brew install julia 

安装完成。要检查安装是否成功,请在终端中输入以下命令:

julia --version 

这会显示已安装的 Julia 版本。

安装 Julia(Windows)

根据你的系统配置(32 位/64 位),下载下载页面提供的 .exe 文件。通过运行下载的 .exe 文件来安装 Julia,这将把 Julia 解压到一个文件夹中。在该文件夹内有一个批处理文件 julia.bat,可以用来启动 Julia 控制台。

要卸载,请删除 Julia 文件夹。

探索源代码

对于爱好者,Julia 的源代码是开放的,鼓励用户通过添加功能或修复漏洞来贡献代码。以下是目录结构:

base/ Julia 标准库的源代码
contrib/ Julia 源代码的编辑器支持,杂项脚本
deps/ 外部依赖项
doc/manual 用户手册的源代码
doc/stdlib 标准库功能帮助文本的源代码
examples/ Julia 示例程序
src/ Julia 语言核心的源代码
test/ 测试套件
test/perf 基准测试套件
ui/ 各种前端的源代码
usr/ Julia 标准库加载的二进制文件和共享库

使用 REPL

Read-Eval-Print-Loop 是一个交互式 shell,或者称为语言 shell,提供了测试代码片段的功能。Julia 提供了一个带有即时编译器(Just-in-Time compiler)的交互式 shell。我们可以在一行中输入内容,系统会进行编译和评估,结果会在下一行显示。

使用 REPL

使用 REPL 的好处是我们可以测试代码是否有错误。此外,它还是初学者的一个很好的环境。我们可以输入表达式并按 Enter 键进行评估。

可以通过 include 将一个 Julia 库或自定义编写的 Julia 程序包含到 REPL 中。例如,我有一个名为 hello.jl 的文件,我可以通过以下方式将其包含到 REPL 中:

julia> include ("hello.jl") 

Julia 还会将所有在 REPL 中输入的命令保存在 .julia_history 文件中。该文件位于 Ubuntu 的 /home/$USER、Windows 的 C:\Users\username 或 macOS 的 ~/.julia_history

与 Linux 终端类似,我们可以在 Julia 的 shell 中使用 Ctrl + R 进行反向搜索。这是一个非常好的功能,因为我们可以回顾已经输入的命令历史。

在语言 shell 中输入 ? 将会将提示符更改为:

help?> 

使用 REPL

要清除屏幕,按 Ctrl + L。要退出 REPL,按 Ctrl + D 或输入以下命令:

julia> exit(). 

使用 Jupyter Notebook

数据科学和科学计算有幸拥有一个令人惊叹的交互式工具——Jupyter Notebook。使用 Jupyter Notebook,你可以在一个互动的网页环境中编写和运行代码,它还支持可视化、图片和视频等功能。它让方程的测试和原型设计变得更加轻松。它支持超过 40 种编程语言,并且完全开源。

GitHub 支持 Jupyter 笔记本。包含计算记录的笔记本可以通过 Jupyter 笔记本查看器或其他云存储分享。Jupyter 笔记本广泛用于编写机器学习算法、统计建模、数值模拟和数据清洗。

Jupyter Notebook 是用 Python 实现的,但你可以运行任何 40 种语言的代码,只要你安装了相应的内核。你可以通过在终端中输入以下命令来检查是否已安装 Python:

python -version 

这将返回系统中 Python 的版本信息。如果系统中安装了 Python,最好是版本 2.7.x 或 3.5.x 或更高版本。

如果没有安装 Python,你可以通过从官方网站下载并安装(适用于 Windows)。对于 Linux,输入以下命令应该可以安装:

sudo apt-get install python 

如果您是 Python 和数据科学的新手,强烈建议安装 Anaconda。数据科学、数值和科学计算中常用的包,包括 Jupyter 笔记本,都与 Anaconda 捆绑在一起,这使得它成为设置环境的首选方式。安装说明可以在www.continuum.io/downloads上找到。

Jupyter 存在于 Anaconda 包中,但您可以通过键入以下命令检查 Jupyter 包是否是最新的:

conda install jupyter 

安装 Jupyter 的另一种方法是使用pip

pip install jupyter 

要检查 Jupyter 是否正确安装,请在终端中键入以下命令:

jupyter -version 

如果 Jupyter 已安装,它应该会显示版本号。

现在,要在 Jupyter 中使用 Julia,我们需要IJulia包。可以使用 Julia 的包管理器安装该包。

安装 IJulia 后,我们可以在 Jupyter 中通过选择笔记本部分下的 Julia 来创建新的笔记本。

使用 Jupyter 笔记本

要获取所有包的最新版本,在 Julia 的 Shell 中键入以下命令:

julia> Pkg.update() 

之后,通过键入以下命令添加 IJulia 包:

julia> Pkg.add("IJulia") 

在 Linux 中,您可能会遇到一些警告,因此最好构建该包:

julia> Pkg.build("IJulia") 

安装 IJulia 后,返回终端并启动 Jupyter 笔记本:

jupyter notebook 

一个浏览器窗口将会打开。在新建选项下,您将找到已安装内核的选项来创建新笔记本。由于我们想要启动一个 Julia 笔记本,因此选择Julia 0.4.2。这将启动一个新的 Julia 笔记本。您可以尝试一个简单的示例。

在这个示例中,我们正在创建一个随机数的直方图。这只是一个示例,我们将在接下来的章节中详细研究使用的组件。

使用 Jupyter 笔记本

流行的编辑器如 Atom 和 Sublime 都有 Julia 插件。Atom 有语言—julia,Sublime 有 Sublime—IJulia,二者都可以从它们的包管理器下载。

包管理

Julia 提供了内置的包管理器。使用 Pkg,我们可以安装用 Julia 编写的库。对于外部库,我们还可以从源代码进行编译,或使用操作系统的标准包管理器。注册包的列表可在pkg.julialang.org上找到。

Pkg 在基础安装中提供。Pkg 模块包含所有包管理命令。

Pkg.status() – 包状态

Pkg.status()是一个函数,打印出当前已安装包的列表及其摘要。当您需要知道要使用的包是否已安装时,这非常方便。

当第一次运行Pkg命令时,包目录会自动创建。该命令要求Pkg.status()返回已安装包的有效列表。由Pkg.status()提供的包列表是由 Pkg 管理的已注册版本。

Pkg.installed()也可以用来返回所有已安装包及其版本的列表。

Pkg.status() – 包状态

Pkg.add() – 添加包

Julia 的包管理器是声明式的且智能的。你只需要告诉它你想要什么,它会计算出需要安装的版本,并在有依赖关系时进行解决。因此,我们只需添加所需的包列表,它会自动解决需要安装的包及其版本。

~/.julia/v0.4/REQUIRE文件包含包的需求。我们可以使用文本编辑器如 vi 或 atom 打开它,或在 Julia 的 shell 中使用Pkg.edit()来编辑这个文件。编辑完文件后,运行Pkg.resolve()来安装或删除包。

我们还可以使用Pkg.add(package_name)来添加包,使用Pkg.rm(package_name)来删除包。之前,我们使用Pkg.add("IJulia")来安装 IJulia 包。

当我们不再想在系统中安装某个包时,可以使用Pkg.rm()REQUIRE文件中删除该包的需求。与Pkg.add()类似,Pkg.rm()首先从REQUIRE文件中移除包的需求,然后通过运行Pkg.resolve()来更新已安装包的列表以匹配。

使用未注册包

我们经常希望能够使用我们团队成员创建的或有人在 Git 上发布的包,但它们不在 Pkg 的注册包中。Julia 通过使用克隆来支持这种情况。Julia 包托管在 Git 仓库中,并可以使用 Git 支持的机制进行克隆。注册包的索引保存在METADATA.jl中。对于非官方包,我们可以使用以下命令:

Pkg.clone("git://example.com/path/unofficialPackage/Package.jl.git") 

有时,未注册的包可能有依赖关系,在使用之前需要满足。如果是这种情况,那么在未注册包的源代码树顶部需要一个REQUIRE文件。该REQUIRE文件确定了未注册包对已注册包的依赖关系。当我们运行Pkg.clone(url)时,这些依赖关系会被自动安装。

Pkg.update() – 包更新

拥有更新的包是非常好的。Julia 作为一个活跃开发的项目,它的包经常更新并添加新的功能。

要更新所有包,请键入以下命令:

Pkg.update() 

在后台,新的更改被拉入位于~/.julia/v0.4/目录下的 METADATA 文件,并且检查自上次更新以来是否有任何新的注册包版本发布。如果有新的注册包版本,Pkg.update()将尝试更新那些未被修改且已签出的包。这一更新过程通过计算要安装的最优包版本集来满足顶级需求。必须安装的特定版本包在 Julia 的~/.julia/v0.4/目录中的 REQUIRE 文件中定义。

METADATA 仓库

注册的包会使用官方的 METADATA.jl 仓库进行下载和安装。如果需要,也可以提供不同的 METADATA 仓库位置:

julia> Pkg.init("https://julia.customrepo.com/METADATA.jl.git", "branch") 

开发包

Julia 允许我们查看源代码,并且由于它是由 Git 跟踪的,所有已安装包的完整开发历史都可用。我们还可以进行所需的修改并提交到我们自己的仓库,或者进行 bug 修复并将增强功能贡献到上游。

你也可能想在某个时候创建自己的包并发布它们。Julia 的包管理器也允许你这样做。

系统上必须安装 Git,并且开发者需要在其选择的托管提供商(如 GitHub、Bitbucket 等)上拥有一个账户。最好能通过 SSH 进行通信—为了实现这一点,上传你的公钥到托管提供商。

创建新包

最好在包的仓库中包含 REQUIRE 文件。这个文件应该至少包含关于 Julia 版本的基本描述。

例如,如果我们想创建一个新的 Julia 包,名为 HelloWorld,我们将有如下内容:

Pkg.generate("HelloWorld", "MIT") 

在这里,HelloWorld 是我们要创建的包,MIT 是我们的包将使用的许可证。许可证应该为包生成器所知。

这将创建一个目录,如下所示:~/.julia/v0.4/HelloWorld。创建的目录会初始化为一个 Git 仓库。同时,包所需的所有文件都保存在这个目录中。这个目录随后会被提交到仓库中。

现在可以将其推送到远程仓库,供全世界使用。

使用 Julia 进行并行计算

现代计算的发展导致系统中出现多核 CPU,有时这些系统会被组合成一个集群,能够执行单一系统可能无法独立完成的任务,或者即使完成了,所需时间也不可接受。Julia 的并行处理环境基于消息传递。允许多个进程在独立的内存域中运行程序。

在 Julia 中,消息传递的实现方式与其他流行环境(如 MPI)不同。Julia 提供了单向通信,因此程序员只需显式地管理两个进程操作中的一个进程。

Julia 的并行编程范式基于以下内容:

  • 远程引用

  • 远程调用

在另一个进程上运行函数的请求称为远程调用。通过特定进程上的另一个对象对对象的引用称为远程引用。远程引用是大多数分布式对象系统中使用的构造。因此,通过某些特定的参数,由不同进程的对象发出的调用,通常会返回对远程对象的引用,这个引用被称为远程引用。

远程调用返回一个指向其结果的远程引用。远程调用会立即返回。发出调用的进程继续执行下一操作。与此同时,远程调用在其他地方发生。对其远程引用调用wait()会等待远程调用完成。可以使用fetch()获得结果的完整值,put!()用于将结果存储到远程引用中。

Julia 使用单进程作为默认设置。要以多个处理器启动 Julia,请使用以下命令:

julia -p n

其中 n 是工作进程的数量。或者,也可以通过使用addproc(n)从运行中的系统创建额外的处理器。建议将 n 设置为系统中 CPU 核心的数量。

pmap@parallel是最常用且最有用的两个函数。

Julia 提供了一个并行 for 循环,用于并行运行多个进程。它的使用方式如下。

使用 Julia 进行并行计算

并行 for 循环通过将多个进程分配给不同的迭代,然后对结果进行归约(在这个案例中是(+))来工作。它与 map-reduce 概念有些相似。迭代将在不同的进程上独立执行,最终这些进程得到的结果会被组合在一起(就像 map-reduce)。一个循环的结果也可以成为另一个循环的输入。答案是这个整个并行循环的结果。

它与普通的迭代循环非常不同,因为迭代不按照特定的顺序发生。由于迭代在不同的进程上运行,任何对变量或数组的写入在全局范围内是不可见的。所使用的变量会被复制并广播到每个并行 for 循环的进程中。

例如:

arr = zeros(500000) 
@parallel for i=1:500000 
  arr[i] = i 
end 

这不会得到预期的结果,因为每个进程都会获得自己独立的arr副本。向量不会按预期填充i。我们必须避免使用这种并行 for 循环

pmap指的是并行映射。例如:

使用 Julia 进行并行计算

这段代码解决了当我们有多个大型随机矩阵,并且需要并行地获取其奇异值的问题。

Julia 的pmap()设计得不同。它非常适合每次函数调用处理大量工作量的情况,而@parallel适用于涉及众多小迭代的情形。pmap()@parallel都利用工作节点进行并行计算。然而,在@parallel中,调用进程发源的节点会进行最终的归约。

Julia 的关键特性——多重分派

函数是一个对象,它使用某些表达式将参数元组映射到返回值。当这个函数对象无法返回值时,它会抛出异常。对于不同类型的参数,同一个概念函数可以有不同的实现。例如,我们可以有一个函数来加两个浮点数,另一个函数来加两个整数。但从概念上讲,我们只是在加两个数字。Julia 提供了一种功能,使得同一概念的不同实现能够轻松实现。这些函数不需要一次性定义。它们是通过小的抽象来定义的。这些小抽象对应不同的参数类型组合,并具有与之相关的不同行为。定义这些行为之一的过程叫做方法(method)。

方法定义接受的参数类型和数量通过其签名的注释来表示。因此,每当使用一组特定的参数调用函数时,都会应用最合适的方法。在函数调用时应用一个方法被称为派发(dispatch)。传统的面向对象语言只考虑派发中的第一个参数。而 Julia 有所不同,它会考虑所有函数的参数(不仅仅是第一个),然后选择应该调用的方法。这被称为多重派发(multiple dispatch)。

多重派发对于数学和科学代码特别有用。我们不应该认为操作只属于某一个参数,而不属于其他参数。实现数学运算符时,会考虑所有的参数类型。多重派发不仅限于数学表达式,它可以应用于众多现实世界的场景,并且是一种强大的程序结构设计范式。

多重派发中的方法

+ 是一个使用多重派发的 Julia 函数。Julia 的所有标准函数和操作符都使用多重派发。对于各种可能的参数类型和数量的组合,每种组合都有许多方法来定义它们的行为。使用::类型断言操作符,方法限制只能接受某些类型的参数:

julia> f(x::Float64, y::Float64) = x + y 

该函数定义仅适用于xy都为Float64类型的值时的调用:

julia> f(10.0, 14.0) 
24.0 

如果我们尝试将这个定义应用到其他类型的参数,它将会给出方法错误。

多重派发中的方法

参数必须与函数定义中精确指定的类型相同。

函数对象在第一个方法定义时创建。新的方法定义会为现有的函数对象添加新的行为。当函数被调用时,会匹配参数的数量和类型,并执行最具体的匹配方法定义。

以下示例创建了一个具有两个方法的函数。第一个方法定义接受两个Float64类型的参数并将其相加。第二个方法定义接受两个Number类型的参数,将其乘以 2 并相加。当我们用Float64类型的参数调用该函数时,第一个方法定义会被应用;而当我们用整数类型的参数调用该函数时,第二个方法定义会被应用,因为数字可以接受任何数值。在以下示例中,我们通过多重派发来操作浮点数和整数。

多重派发中的方法

在 Julia 中,所有值都是抽象类型"Any"的实例。当没有使用::进行类型声明时,表示该参数类型没有明确指定,因此Any是方法参数的默认类型,它没有限制可以接受任何类型的值。通常,一个方法定义是为了应用于某些没有其他方法定义适用的参数。它是 Julia 语言最强大的功能之一。

利用 Julia 的多重派发和灵活的参数化类型系统,可以高效地生成专门的代码并实现复杂的算法,而无需过多关注底层实现,且表达能力非常强。

歧义 – 方法定义

有时,函数的行为定义方式可能导致对于某些参数集没有唯一的方法可应用。在这种情况下,Julia 会抛出关于这种歧义的警告,但会继续执行并随机选择一个方法。为了避免这种歧义,我们应该定义一个方法来处理这种情况。

在下面的示例中,我们定义了一个方法定义,具有一个类型为Any的参数和一个类型为Float64的参数。在第二个方法定义中,我们只是改变了参数的顺序,但这并没有将其与第一个定义区分开。在这种情况下,Julia 会发出歧义的方法定义警告,但仍然允许我们继续执行。

歧义 – 方法定义

促进语言互操作性

尽管 Julia 可以用来编写大多数类型的代码,但我们希望利用一些成熟的数值计算和科学计算库。这些库可能是用 C、Fortran 或 Python 编写的。Julia 允许轻松使用现有的 Python、C 或 Fortran 代码。通过让 Julia 调用简单且高效的 C、Fortran 或 Python 函数,可以轻松实现这一目标。

C/Fortran 库应当可以供 Julia 使用。通过ccall进行常规但有效的调用。这种调用方式只有在代码作为共享库时才可行。Julia 的 JIT 会生成与本地 C 调用相同的机器指令,因此与通过 C 代码调用没有什么不同,且开销极小。

导入 Python 代码在数据科学中非常有用,甚至是必需的,因为 Python 已经有了一个完整的机器学习和统计函数库。例如,它包含了 scikit-learn 和 pandas。要在 Julia 中使用 Python,我们需要 PyCall.jl。添加 PyCall.jl 的方法如下:

Pkg.add("PyCall") 

促进语言互操作性

PyCall 提供了一个宏 @pyimport,可以简化导入 Python 包的过程,并为其中的所有函数和常量提供 Julia 封装,包括自动转换 Julia 与 Python 之间的类型。

PyCall 还提供了对 Python 对象的低级操作功能,包括一个用于不透明 Python 对象的 PyObject 类型。它还提供了一个 pycall 函数(类似于 Julia 的 ccall 函数),可以在 Julia 中调用 Python 函数并进行类型转换。PyCall 不使用 Python 程序,而是直接链接到 libpython 库。在 Pkg.build 过程中,它通过调用 Python 查找 libpython 的位置。

在 Julia 中调用 Python 代码

@pyimport 宏在大多数情况下会根据 Python 对象的运行时检查自动进行适当的类型转换为 Julia 类型。通过使用低级函数,它能更好地控制这些类型转换。在返回类型已知的情况下使用 PyCall,可以帮助提升性能,既可以消除运行时类型推断的开销,又可以为 Julia 编译器提供更多的类型信息:

  • pycall(function::PyObject, returntype::Type, args...):此函数调用给定的 Python 函数(通常从模块中查找),并传入给定的 args...(这些参数是标准的 Julia 类型,如果可能的话,会自动转换为相应的 Python 类型),并将返回值转换为 returntype(如果要返回未转换的 Python 对象引用,请使用 returntype 为 PyObject,或使用 PyAny 来请求自动转换)。

  • pyimport(s):此函数导入 Python 模块(可以是字符串或符号),并返回一个指向该模块的指针(一个 PyObject)。然后,可以通过 s[name] 查找模块中的函数或其他符号,其中 name 可以是字符串(用于原始 PyObject)或符号(用于自动类型转换)。与 @pyimport 宏不同,它不会定义一个 Julia 模块,成员不能通过 s.name 访问。

总结

在本章中,我们了解了 Julia 的不同之处,以及基于 LLVM 的 JIT 编译器如何使 Julia 接近 C/C++ 的性能。我们介绍了如何下载、安装和从源代码构建 Julia。我们发现的显著特点是,这种语言优雅、简洁、强大,并且在数值计算和科学计算方面具有惊人的能力。

我们通过命令行(REPL)进行了一些使用 Julia 的示例,并看到了这个语言的外壳功能丰富。所发现的功能包括自动补全、反向搜索和帮助功能。我们还讨论了为什么要使用 Jupyter Notebook,并继续设置了带有 IJulia 包的 Jupyter。我们用一个简单的例子演示了如何使用 Jupyter Notebook 和 Julia 的可视化包 Gadfly。

此外,我们学习了 Julia 强大的内置包管理功能,以及如何添加、更新和删除模块。同时,我们也学习了如何创建自己的包并发布到社区。我们还向你介绍了 Julia 的一个强大特性——多重派发,并通过一些基本示例演示了如何创建方法定义来实现多重派发。

此外,我们向你介绍了并行计算,解释了它与传统的消息传递方式有何不同,以及如何利用所有可用的计算资源。我们还学习了 Julia 的语言互操作性特性,了解了如何从 Julia 程序中调用 Python 模块或库。

参考文献

第二章 数据清洗

据说数据科学家的大约 50%的时间都用于将原始数据转换为可用格式。原始数据可以是任何格式或大小。它可以是像关系数据库管理系统(RDBMS)这样的结构化数据,像 CSV 这样的半结构化数据,或像普通文本文件这样的非结构化数据。这些数据包含一些有价值的信息。为了提取这些信息,必须将其转换为一种数据结构或可用格式,供算法从中发现有价值的见解。因此,可用格式指的是可以在数据科学过程中被使用的模型中的数据。这种可用格式根据使用案例的不同而有所不同。

本章将指导你完成数据清洗过程,或者说是数据准备过程。内容包括以下主题:

  • 什么是数据清洗?

  • DataFrames.jl

  • 从文件上传数据

  • 查找所需数据

  • 联接和索引

  • 分割-应用-合并策略

  • 数据重塑

  • 公式(ModelFrame 和 ModelMatrix)

  • PooledDataArray

  • 网络抓取

什么是数据清洗?

数据清洗一词来源于“munge”一词,该词由美国麻省理工学院(MIT)的一些学生创造。它被认为是数据科学过程中最基本的部分之一;它涉及收集、聚合、清理和组织数据,以供设计好的算法进行发现或创建模型。这包括多个步骤,其中包括从数据源提取数据,然后将数据解析或转换为预定义的数据结构。数据清洗也被称为数据整理。

数据清洗过程

那么,什么是数据清洗过程呢?如前所述,数据可以是任何格式,且数据科学过程可能需要来自多个来源的数据。这个数据聚合阶段包括从网站抓取数据、下载成千上万的.txt.log文件,或从关系数据库管理系统(RDBMS)或 NoSQL 数据存储中收集数据。

很少能找到可以直接用于数据科学过程的数据格式。收到的数据通常是不适合建模和分析的格式。通常,算法需要数据以表格格式或矩阵形式存储。将收集到的原始数据转换为所需格式的阶段可能非常复杂且耗时。但这个阶段为现在可以进行的复杂数据分析打下了基础。

提前定义将输入到算法中的数据结构是很好的做法。这个数据结构是根据问题的性质定义的。你设计或将要设计的算法不仅要能够接受这种格式的数据,还应该能够轻松识别模式,找到异常值,做出发现,或满足任何所需的结果。

在定义数据的结构之后,您需要定义实现该结构的过程。这就像一个管道,接受某些形式的数据,并以预定义的格式输出有意义的数据。这个阶段包含多个步骤。这些步骤包括将数据从一种形式转换为另一种形式,这可能需要也可能不需要字符串操作或正则表达式,并且需要找到缺失值和异常值。

通常,数据科学问题涉及两种类型的数据。这两种数据将是分类数据或数值数据。分类数据带有标签。这些标签由一组值组成。例如,我们可以将天气视为具有分类特征的数据。天气可以是晴天、多云、下雨、雾霾或雪天。当底层值与数据中的某一组(属于一个标签)关联时,就会形成这些标签。这些标签具有一些独特的特性,我们可能无法对它们进行算术运算。

数值数据更为常见,例如温度。温度将以浮动点数表示,我们当然可以对其应用数学运算。每个值都可以与数据集中的其他值进行比较,因此我们可以说它们彼此之间有直接的关系。

什么是 DataFrame?

DataFrame 是一种数据结构,具有标签化的列,每一列可能具有不同的数据类型。像 SQL 表格或电子表格一样,它具有二维结构。它也可以被看作是一个字典的列表,但从根本上讲,它是不同的。

DataFrames 是进行统计分析时推荐的数据结构。Julia 提供了一个名为 DataFrames.jl 的包,它包含了所有必要的函数来处理 DataFrame。

Julia 的 DataFrames 包提供了三种数据类型:

  • NA:在 Julia 中,缺失值通过一个特定的数据类型 NA 表示。

  • DataArray:在标准 Julia 库中定义的数组类型,尽管它具有许多功能,但并未提供任何专门用于数据分析的功能。DataFrames.jl 中提供的 DataArray 提供了这些功能(例如,如果我们需要在数组中存储缺失值)。

  • DataFrame:DataFrame 是一个二维数据结构,像电子表格一样。它非常类似于 R 或 pandas 的 DataFrame,提供了许多功能来表示和分析数据。

NA 数据类型及其重要性

在现实世界中,我们常常遇到带有缺失值的数据。这是非常常见的,但 Julia 默认情况下并不提供这一功能。这个功能是通过 DataFrames.jl 包来添加的。DataFrames 包带来了 DataArray 包,它提供了 NA 数据类型。多重派发是 Julia 最强大的特性之一,NA 就是一个例子。Julia 有 NA 类型,提供了一个单例对象 NA,我们用它来表示缺失值。

为什么需要 NA 数据类型?

假设我们有一个包含浮动点数的数据集:

julia> x = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6]

这将创建一个六元素的 Array{Float64,1}

现在,假设这个数据集在位置[1]处有一个缺失值。这意味着,1.1 的位置没有值。这不能通过 Julia 的数组类型表示。当我们尝试分配一个 NA 值时,会出现这个错误:

julia> x[1] = NA 
LoadError: UndefVarError: NA not defined 
while loading In[2], in expression starting on line 1 

因此,目前我们无法将NA值添加到我们创建的数组中。

所以,为了将数据加载到包含NA值的数组中,我们使用DataArray。这使我们能够在数据集中包含 NA 值:

julia> using DataArrays 
julia> x = DataArray([1.1, 2.2, 3.3, 4.4, 5.5, 6.6]) 

这将创建一个六元素的DataArrays.DataArray{Float64,1}

所以,当我们尝试包含NA值时,它会返回:

julia> X[1] = NA 
NA 
julia> x 
6-element DataArrays.DataArray{Float64,1}: 
 1.1 
 2.2 
 3.3 
 4.4 
 5.5 
 6.6 

因此,通过使用 DataArrays,我们可以处理缺失数据。另一个提供的功能是,NA 值并不总是影响应用于特定数据集的函数。因此,那些不涉及 NA 值或不受其影响的方法可以应用于数据集。如果涉及 NA 值,那么结果将是 NA。

在以下示例中,我们正在应用均值函数和true || x。由于涉及 NA 值,均值函数无法正常工作,而true || x则按预期工作:

julia> true || x 
True 

julia> true && x[1] 
NA 

julia> mean(x) 
NA 

julia> mean(x[2:6]) 
4.4 

DataArray – 类似系列的数据结构

在前一部分中,我们讨论了如何使用 DataArrays 来存储包含缺失(NA)值的数据集,因为 Julia 的标准数组类型无法做到这一点。

还有一些与 Julia 的数组类型类似的特性。DataArray 提供了 Vector(一维数组类型)和 Matrix(二维数组类型)的类型别名,分别是 DataVector 和 DataMatrix。

创建一个一维 DataArray 类似于创建一个数组:

julia> using DataArrays
julia> dvector = data([10,20,30,40,50])
5-element DataArrays.DataArray{Int64,1}:
10
20
30
40
50

在这里,我们有 NA 值,不像数组中那样。类似地,我们可以创建一个二维 DataArray,它将是一个 DataMatrix。

julia> dmatrix = data([10 20 30; 40 50 60])
2x3 DataArrays.DataArray{Int64,2}:
10 20 30
40 50 60
julia> dmatrix[2,3]
60

在前面的例子中,我们通过切片来计算均值。这并不是一个方便的方式来在应用函数时忽略或排除 NA 值。一个更好的方法是使用dropna

julia> dropna(x)
5-element Array{Float64,1}:
2.2
3.3
4.4
5.5
6.6

DataFrames – 表格数据结构

可以说,这在统计计算中是最重要且最常用的数据类型,无论是在 R(data.frame)中,还是在 Python(Pandas)中。这是因为所有现实世界中的数据大多数以表格或类似电子表格的格式存在。这个格式不能仅通过简单的 DataArray 表示:

julia> df = DataFrame(Name = ["Ajava Rhodiumhi", "Las Hushjoin"],
            Count = [14.04, 17.3],
            OS = ["Ubuntu", "Mint"])

DataFrames – 表格数据结构

例如,这个数据集不能通过 DataArray 表示。给定的数据集具有以下特征,因此无法由 DataArray 表示:

  • 这个数据集在不同的列中包含不同类型的数据。不同列中的不同数据类型无法通过矩阵表示。矩阵只能包含一种类型的值。

  • 它是一个表格数据结构,记录与同一行中不同列的其他记录有关。因此,所有列必须具有相同的长度。不能使用向量,因为它们无法强制确保列的长度相同。因此,DataFrame 中的一列由 DataArray 表示。

  • 在前面的例子中,我们可以看到列已被标记。这个标记帮助我们轻松熟悉数据并在无需记住精确位置的情况下访问它。因此,可以通过数字索引以及列标签来访问这些列。

因此,出于这些原因,使用了 DataFrames 包。DataFrames 用于表示具有 DataArrays 作为列的表格数据。

在给定的例子中,我们通过以下方式构建了一个 DataFrame:

julia> df = DataFrame(Name = ["Ajava Rhodiumhi", "Las Hushjoin"], 
Count = [14.04, 17.3], 
OS = ["Ubuntu", "Mint"]) 

使用关键字参数,可以定义列名称。

让我们通过构建一个新的 DataFrame 来举个例子:

julia> df2 = DataFrame() 

julia> df2[:X] = 1:10 

julia> df2[:Y] = ["Head", "Tail", 
"Head", "Head", 
"Tail", "Head", 
"Tail", "Tail", 
"Head", "Tail"] 
julia> df2 

要查看创建的 DataFrame 的大小,我们使用 size 函数:

julia> size(df2) 
(10, 2) 

这里,10指的是行数,2指的是列数。

要查看数据集的前几行,我们使用head(),要查看最后几行,我们使用tail()函数:

Julia> head(df2) 

DataFrames – 表格数据结构

由于我们已为 DataFrame 的列命名,因此可以通过这些名称来访问它们。

例如:

julia> df2[:X] 
10-element DataArrays.DataArray{Int64,1}: 
 1 
 2 
 3 
 4 
 5 
 6 
... 

这简化了对列的访问,因为我们可以为具有多个列的现实世界数据集赋予有意义的名称,而无需记住它们的数字索引。

如果需要,我们还可以使用rename函数通过这些列来重命名:

Julia> rename!(df2, :X,  :newX) 

如果需要重命名多个列,可以使用以下方法:

julia> rename!(df2, {:X => :newX, :Y => :newY}) 

但现在,为了方便起见,我们仍然使用旧的列名。

Julia 还提供了一个名为describe()的函数,可以概述整个数据集。对于包含许多列的数据集,它会非常有用:

julia> describe(df2) X
Min 1.0
1st Qu. 3.25
Median 5.5
Mean 5.5
3rd Qu. 7.75
Max 10.0
NAs 0
NA% 0.0%

Y
Length 10
Type ASCIIString
NAs 0
NA% 0.0%
Unique 2

安装并使用 DataFrames.jl

安装非常简单,因为它是一个注册的 Julia 包:

Julia> Pkg.update() 
julia> Pkg.add("DataFrames") 

这将所有必需的包添加到当前命名空间。要使用DataFrames包:

julia> using DataFrames 

还应该有一些常用于学习的经典数据集。这些数据集可以在RDatasets包中找到:

Julia> Pkg.add("RDatasets") 

可用的 R 包列表可以通过以下方式找到:

julia> Rdatasets.packages() 

这里,你可以看到这个:

datasets - The R Datasets Package 

它包含了可供 R 使用的数据集。要使用这个dataset,只需使用以下代码:

using RDatasets 
iris_dataset = dataset("datasets", "iris") 

这里,dataset是一个接受两个参数的函数。

第一个参数是包的名称,第二个参数是我们要加载的数据集的名称。

在以下例子中,我们将著名的鸢尾花数据集加载到内存中。你可以看到dataset()函数返回了一个 DataFrame。该数据集包含五列:SepalLengthSepalWidthPetalLengthPetalWidthSpecies。数据非常容易理解。每个物种都有大量的样本,且测量了萼片和花瓣的长度和宽度,这些数据可以用于后续区分物种:

安装并使用 DataFrames.jl

实际的数据科学问题通常不会处理人工随机生成的数据或通过命令行读取的数据。而是处理从文件或任何其他外部来源加载的数据。这些文件可以包含任何格式的数据,我们可能需要在加载到数据框之前对其进行处理。

Julia 提供了一个 readtable() 函数,可以用来读取数据框中的表格文件。通常,我们会遇到逗号分隔或制表符分隔的格式(CSV 或 TSV)。readtable() 能很好地处理这些格式。

我们可以将文件的位置作为 UTF8String 和分隔符类型作为参数传递给 readtable() 函数。默认的分隔符类型是逗号(',')用于 CSV,制表符('\t')用于 TSV,空格(' ')用于 WSV。

在下面的示例中,我们使用 readtable() 函数将示例 iris 数据集加载到数据框中。

尽管 iris 数据集可以在 RDatasets 包中找到,但我们将下载 CSV 文件以处理外部数据集。可以从 github.com/scikit-learn/scikit-learn/blob/master/sklearn/datasets/data/iris.csv 下载 iris CSV。

请记住将下载的 CSV 文件放入当前工作目录(即从 REPL 启动的目录—通常是 ~/home/<username> 目录):

julia> using DataFramesjulia> df_iris_sample =
  readtable("iris_sample.csv",
  separator = ',')
julia> df_iris_sample

这是我们在前一个示例中使用的相同数据集,但现在我们是从 CSV 文件加载数据。

对于其他基于文本的数据集,如 TSV、WSV 或 TXT,readtable() 也以类似的方式使用。假设相同的 iris 数据集在 TSV、WSV 或 TXT 格式中,它的使用方式是类似的:

julia> df_iris_sample = readtable("iris_dataset.tsv", 
separator='\t') 

举例来说,如果我们有一个没有头部且以 ; 分隔的数据集,我们可以如下使用 readtable()

julia> df_random_dataset = readtable("random_dataset.txt",                                                                    header=false, separator=';') 

readtable() 利用了 Julia 的多重分派功能,并已根据不同的方法行为进行实现:

julia> methods(readtable)
3 methods for generic function readtable:
readtable(io::IO) at /home/anshul/.julia/v0.4/DataFrames/src/dataframe/io.jl:820
readtable(io::IO, nbytes::Integer) at /home/anshul/.julia/v0.4/DataFrames/src/dataframe/io.jl:820
readtable(pathname::AbstractString) at /home/anshul/.julia/v0.4/DataFrames/src/dataframe/io.jl:930

我们可以看到 readtable() 函数有三种方法。

这些方法实现了一些高级选项,以简化加载并支持各种数据格式:

  • header::Bool:在我们使用的 iris 示例中,数据集包含如花萼长度、花萼宽度等头部信息,这使得描述数据更加容易。但数据集并不总是有头部信息。header 的默认值是 true;因此,当没有头部信息时,我们将该参数设置为 false。

  • separator::Char:文件中的数据必须按照一定的方式组织,以形成表格结构。这通常是通过使用 ,\t; 或这些符号的组合来实现的。readtable() 根据文件扩展名猜测分隔符类型,但手动提供分隔符是一个好习惯。

  • nastrings::Vector{ASCIIString}:假设存在缺失值或其他值,我们希望用 NA 替换它们。通过 nastrings 可以实现这一点。默认情况下,它会将空记录替换为 NA。

  • truestrings::Vector{ASCIIString}: 这将字符串转换为布尔值 true。当我们希望数据集中一组字符串被视为 true 时使用。默认情况下,如果没有参数指定,TruetrueTt 会被转换为 true。

    • falsestrings::Vector{ASCIIString}: 这与 truestrings 类似,但将字符串转换为布尔值 false。默认情况下,如果没有传递参数,FalsefalseFf 会被转换为 false。
  • nrows::Int: 如果我们只想由 readtable() 读取特定数量的行,则使用 nrows 作为参数。默认情况下为 -1,这意味着 readtable() 将读取整个文件。

  • names::Vector{Symbol}: 如果我们希望为列指定特定名称,而不使用文件头中提到的名称,则使用 names。在这里,我们传递一个包含要使用的列名称的向量。默认情况下,它是 [],这意味着如果存在文件头,则应使用其中的名称;否则必须使用数值索引。

  • eltypes::Vector{DataType}: 我们可以通过传递一个向量来指定列的类型,使用 eltypes。如果没有传递任何内容,默认情况下它是一个空向量([])。

  • allowcomments::Bool: 在数据集中,可能会有带有注释的记录。这些注释可以被忽略。默认情况下为 false

  • commentmark::Char: 如果我们使用 allowcomments,还必须指定注释开始的字符(符号)。默认情况下为 #

  • ignorepadding::Bool: 我们的数据集可能不如我们希望的那样完美。记录可能包含左右两侧的空白字符。可以使用 ignorepadding 忽略这些空白。默认情况下为 true。

  • skipstart::Int: 我们的数据集可能包含一些描述数据的行,而我们可能不需要或者只想跳过开头的几行。通过 skipstart 指定要跳过的行数来实现这一点。默认情况下为 0,将读取整个文件。

  • skiprows::Vector{Int}: 如果想要跳过数据中的特定行,则使用 skiprows。我们只需在一个向量中指定要跳过的行的索引。默认情况下为 [],将读取整个文件。

  • skipblanks::Bool: 正如前面提到的,我们的数据集可能不完美。如果我们从网页上抓取数据或从其他来源提取数据,可能会有一些空白行。我们可以使用 skipblanks 跳过这些空白行。默认情况下为 true,但如果不想要,则可以选择其他设置。

  • encoding::Symbol: 如果文件的编码不是 UTF8,我们可以指定文件的编码。

将数据写入文件

我们也许希望输出结果或转换数据集并将其存储到文件中。在 Julia 中,我们使用 writetable() 函数来实现这一点。它与我们在上一节讨论的 readtable() 函数非常相似。

例如,我们想将 df_iris_sample 数据框写入 CSV 文件:

julia> writetable("output_df_iris.csv", df_iris_sample)

这是一种使用默认参数集写入文件的方式。一个明显的区别是,我们传递了我们想要写入的文件名以及数据框(dataframe)。

writetable()同样接受各种参数,类似于readtable()

我们也可以像这样定义分隔符来编写之前的语句:

julia> writetable("output_df_iris.csv", df_iris_sample, separator = ',')

同样,我们也可以在参数中设置标题和引号。

使用 DataFrames

我们将遵循或继承一些传统的数据操作策略。在本节中,我们将讨论这些策略和方法,并探讨它们在数据科学中的重要性。

理解 DataFrames 连接

在处理多个数据集时,我们通常需要以特定的方式合并数据集,以便更容易进行分析或与特定的函数配合使用。

我们将使用由英国交通部发布的道路安全数据,该数据开放,适用 OGL-开放政府许可。

数据集可以在这里找到:data.gov.uk/dataset/road-accidents-safety-data

我们将使用两个数据集:

  • 道路安全:2015 年事故数据

  • 道路安全:2015 年车辆数据

注意

DfTRoadSafety_Accidents_2015包含的列有Accident_IndexLocation_Easting_OSGRLocation_Northing_OSGRLongitudeLatitudePolice_ForceAccident_SeverityNumber_of_VehiclesNumber_of_CasualtiesDateDay_of_WeekTime等。DfTRoadSafety_Vehicles_2015包含的列有Accident_IndexVehicle_ReferenceVehicle_TypeTowing_and_ArticulationVehicle_ManoeuvreVehicle_Location-Restricted_LaneJunction_LocationSkidding_and_OverturningHit_Object_in_Carriageway等。

我们可以看到Accident_Index是一个共同的字段,并且是唯一的。它被用作数据集的索引。

首先,我们将使 DataFrames 包可用,然后加载数据。我们使用之前讨论过的readtable函数将数据加载到两个不同的数据框中:

julia> using DataFrames 

julia> DfTRoadSafety_Accidents_2015 = readtable("DfTRoadSafety_Accidents_2015.csv") 

julia> head(DfTRoadSafety_Accidents_2015) 

理解 DataFrames 连接

第一个数据集已经加载到 DataFrame 中,我们尝试使用head获取数据集的信息。它会显示一些起始的列。

如果我们更关心列的名称,可以使用names函数:

julia> names(DfTRoadSafety_Accidents_2015) 
32-element Array{Symbol,1}: 
 :_Accident_Index                             
 :Location_Easting_OSGR                       
 :Location_Northing_OSGR                      
 :Longitude                                   
 :Latitude                                    
 :Police_Force                                
 :Accident_Severity                           
 :Number_of_Vehicles                          
 :Number_of_Casualties                        
 :Date                                        
 :Day_of_Week                                 
 :Time                                        
 :Local_Authority_District_                   

 :x2nd_Road_Class                             
 :x2nd_Road_Number                            
 :Pedestrian_Crossing_Human_Control           
 :Pedestrian_Crossing_Physical_Facilities     
 :Light_Conditions                            
 :Weather_Conditions                          
 :Road_Surface_Conditions                     
 :Special_Conditions_at_Site                  
 :Carriageway_Hazards                         
 :Urban_or_Rural_Area                         
 :Did_Police_Officer_Attend_Scene_of_Accident 
 :LSOA_of_Accident_Location 

同样,我们将在一个数据框中加载第二个数据集:

julia> DfTRoadSafety_Vehicles_2015 = readtable("DfTRoadSafety_Vehicles_2015.csv") 

第二个数据集已经加载到内存中。

稍后我们将深入探讨,但现在我们先进行两个数据集的全连接操作。对这两个数据集进行连接将告诉我们哪起事故涉及了哪些车辆:

julia> DfTRoadSafety_Vehicles_2015 = readtable("DfTRoadSafety_Vehicles_2015.csv") 

julia> full_DfTRoadSafety_2015 = 
join(DfTRoadSafety_Accidents_2015, 
DfTRoadSafety_Vehicles_2015, 
on = :_Accident_Index)

理解 DataFrames 连接

我们可以看到全连接(full join)已经成功执行。现在我们得到了数据,可以告诉我们事故发生的时间、车辆的位置以及更多细节。

这个好处是,连接操作非常简单,甚至在处理大数据集时也非常快速。

我们已经了解了关系数据库中其他可用的连接类型。Julia 的 DataFrames 包也提供了这些连接:

  • 内连接:输出的 DataFrame 仅包含在两个 DataFrame 中都存在的键对应的行。

  • 左连接:输出的 DataFrame 包含第一个(左)DataFrame 中存在的键的行,无论这些键是否存在于第二个(右)DataFrame 中。

  • 右连接:输出的 DataFrame 包含第二个(右)DataFrame 中存在的键的行,无论这些键是否存在于第一个(左)DataFrame 中。

  • 外连接:输出的 DataFrame 包含第一个或第二个 DataFrame 中存在的键的行,这些键是我们要连接的。

  • 半连接:输出的 DataFrame 仅包含在第一个(左)DataFrame 中的那些键,且这些键在第一个(左)和第二个(右)DataFrame 中都存在。输出只包含来自第一个 DataFrame 的行。

  • 反连接:输出的 DataFrame 包含第一个(左)DataFrame 中存在的键的行,但这些键在第二个(右)DataFrame 中没有对应的行。输出仅包含来自第一个 DataFrame 的行。

  • 交叉连接:输出的 DataFrame 包含来自第一个 DataFrame(左)和第二个 DataFrame(右)的行的笛卡尔积。

交叉连接不涉及键;因此它的使用方式如下:

julia> cross_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, kind = :cross) 

在这里,我们使用 kind 参数传递我们想要的连接类型。其他连接也是通过这个参数来完成的。

我们想要使用的连接类型是通过 kind 参数来指定的。

让我们通过一个更简单的数据集来理解这一点。我们将创建一个 DataFrame,并在其上应用不同的连接:

julia> left_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :left) 

对于左连接,我们可以使用:

julia> Cities = ["Delhi","Amsterdam","Hamburg"][rand(1:3, 10)] 

julia> df1 = DataFrame(Any[[1:10], Cities, 
        rand(10)], [:ID, :City, :RandomValue1]) 

julia> df2 = DataFrame(ID = 1:10, City = Cities, 
        RandomValue2 = rand(100:110, 10))  

这创建了两个具有 10 行的 DataFrame。第一个 DataFrame df1 有三列:IDCityRandomValue1。第二个 DataFrame df2 也有三列:IDCityRandomValue2

应用全连接时,我们可以使用:

julia> full_df1_df2 = join(df1,df2, 
                on = [:ID, :City]) 

我们使用了两列来应用连接。

这将生成:

理解 DataFrame 连接

其他连接也可以通过 kind 参数应用。让我们回顾一下我们之前的事故和车辆数据集。

使用 kind 的不同连接是:

julia> right_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :right) 

julia> inner_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :inner) 

julia> outer_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :outer) 

julia> semi_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :semi) 

julia> anti_DfTRoadSafety_2014 = join(DfTRoadSafety_Accidents_2014, DfTRoadSafety_Vehicles_2014, on = :_Accident_Index, kind = :anti) 

分割-应用-合并策略

Hadley Wickham 发表了一篇论文(Wickham, Hadley. "The split-apply-combine strategy for data analysis." Journal of Statistical Software 40.1 (2011): 1-29),定义了数据分析的分割-应用-合并策略。在这篇论文中,他解释了为什么将一个大问题分解成易于管理的小部分,独立地对每个部分进行操作,获取所需结果,然后再将所有部分组合起来是好的。

当数据集包含大量列时,并且在某些操作中并不需要所有列时,就需要这样做。最好是将数据集拆分,然后应用必要的函数;我们可以随时将数据集重新合并。

这是通过 by 函数完成的,by 接受三个参数:

  • DataFrame(这是我们将要分割的 DataFrame)

  • 用于分割 DataFrame 的列名(或数值索引)

  • 可以应用于 DataFrame 每个子集的函数

让我们尝试将 by 应用到我们的相同数据集:

拆分-应用-合并策略

aggregate() 函数提供了一种替代方法来应用 Split-Apply-Combine 策略。aggregate() 函数使用相同的三个参数:

  • DataFrame(这是我们将要分割的 DataFrame)

  • 用于分割 DataFrame 的列名(或数值索引)

  • 可以应用于 DataFrame 每个子集的函数

第三个参数中提供的函数会应用于每一列,所有未用于分割 DataFrame 的列都会应用该函数。

重塑数据

使用案例可能要求数据以不同于当前的形状存在。为了方便这一点,Julia 提供了数据的重塑功能。

让我们使用相同的数据集,但在此之前先检查一下数据集的大小:

julia> size(DfTRoadSafety_Accidents_2014) 
(146322,32) 

我们可以看到数据行数大于 100,000。尽管我们可以处理这些数据,但为了便于理解,我们将使用一个较小的数据集。

RDataset 提供的数据集总是很好的起点。我们将使用经过验证的 iris 数据集。

我们将导入 RDatasetsDataFrames(如果我们开始了一个新的终端会话):

julia> using RDatasets, DataFrames 

然后,我们将加载 iris 数据集到 DataFrame 中。我们可以看到数据集有 150 行和 5 列:

重塑数据

现在我们使用 stack() 函数来重塑数据集。让我们只传递 DataFrame 作为唯一参数来使用它。

stack 通过为分类变量创建一个逐项包含所有信息的 DataFrame 来工作:

重塑数据

我们可以看到我们的数据集已经堆叠。这里我们已经堆叠了所有的列。我们还可以提供特定的列进行堆叠:

Julia> iris_dataframe [:id] = 1:size(iris_dataframe, 1)  
# create a new column to track the id of the row 

Julia> iris_stack = (iris_dataframe,  [1:4]) 

第二个参数表示我们想要堆叠的列。我们可以在结果中看到第 1 到第 4 列已被堆叠,这意味着我们已经将数据集重塑为一个新的 DataFrame:

Julia> iris_stack = stack(iris_dataframe,  [1:4]) 

Julia> size(iris_stack) 
(600,4) 
Julia> head(iris_stack) 

重塑数据

我们可以看到有一个新的列:id,这是堆叠数据框的标识符。它的值是按行重复的次数。

由于所有列都包含在结果 DataFrame 中,因此某些列会重复。这些列实际上是此 DataFrame 的标识符,用列(id)表示。除了标识符列(:id),还有两列,:variable:values。这些列实际上包含了堆叠的值。

重塑数据

我们还可以提供第三个参数(可选)。这是值会重复的列。通过这个参数,我们可以指定要包括哪些列,哪些列不包括。

重塑数据

melt() 函数类似于 stack 函数,但具有一些特殊功能。在这里,我们需要指定标识符列,其余的列会被堆叠:

Julia> iris_melt = stack(iris_dataframe, [1:4]) 

重塑数据

剩余的列假设包含已测量的变量,并被堆叠。

与 stack 和 melt 相反的是 unstack,它用于将数据从长格式转换为宽格式。我们需要指定标识符列和变量/值列给 unstack 函数:

julia> unstack(iris_melt, :id, :variable, :value) 

重塑数据

如果 unstack 函数的其余列是唯一的,可以省略参数中的 :id(标识符):

julia> unstack(iris_melt, :variable, :value) 

meltdfstackdf 是两个附加函数,它们的作用类似于 melt 和 stack,但同时也提供了对原始宽格式 DataFrame 的视图:

Julia> iris_stackdf = stackdf(iris_dataframe) 

重塑数据

这看起来和 stack 函数非常相似,但通过查看它们的存储表示,我们可以看到区别。

要查看存储表示,可以使用 dump。让我们将其应用于 stack 函数:

重塑数据

  • 在这里,我们可以看到 :variable 的类型为 Array(Symbol,(600,))

  • :value 的类型为 DataArrays.DataArray{Float64,1}(600)

  • 标识符(:Species)的类型为 DataArrays.PooledDataArray{ASCIIString,UInt8,1}(600)

现在,我们将查看 stackdf 的存储表示:

重塑数据

在这里,我们可以看到:

  • :variable 的类型为 DataFrames.RepeatedVector{Symbol}。变量被重复 n 次,其中 n 是原始 AbstractDataFrame 中的行数。

  • :value 的类型为 DataFrames.StackedVector。这便于查看原始 DataFrame 中堆叠在一起的列。

  • 标识符(:Species)的类型为 Species: DataFrames.RepeatedVector{ASCIIString}。原始列被重复 n 次,其中 n 是堆叠的列数。

使用这些 AbstractVectors,我们现在能够创建视图,从而通过这种实现节省内存。

重塑函数不提供执行聚合的功能。因此,为了进行聚合,需要将拆分-应用-合并(Split-Apply-Combine)策略与重塑结合使用。

我们将使用 iris_stack

julia> iris_stack = stack(iris_dataframe) 

重塑数据

在这里,我们创建了一个新的列,根据物种计算各列的均值。现在我们可以对其进行反转。

重塑数据

排序数据集

排序是数据分析中最常用的技术之一。排序在 Julia 中通过调用 sortsort! 函数来实现。

sortsort! 的区别在于,sort! 是就地排序,它直接对原始数组进行排序,而不是创建副本。

让我们在鸢尾花数据集上使用 sort! 函数:

排序数据集

我们可以看到这些列并不是按照 [:SepalLength, :SepalWidth, :PetalLength, :PetalWidth] 排序的,但它们实际上是按 :Species 列排序的。

排序函数接受一些参数,并提供一些功能。例如,要反向排序,我们可以:

julia> sort!(iris_dataframe, rev = true) 

要对某些特定列进行排序,我们可以:

julia> sort!(iris_dataframe, cols = [:SepalLength, :PetalLength]) 

我们还可以使用 sort! 的 by 函数,在 DataFrame 或单个列上应用其他函数。

排序数据集

order 用于指定在一组列中对特定列进行排序。

公式 - 数学表达式的特殊数据类型

数据科学涉及使用各种统计公式从数据中提取洞察。这些公式的创建和应用是数据科学的核心过程之一。它将输入变量通过某些函数和数学表达式映射到输出。

Julia 通过在 DataFrame 包中提供一种公式类型来简化这一过程,它与符号 ~ 一起使用。~ 是一个二元操作符。例如:

julia> formulaX = A ~ B + C

对于统计建模,建议使用 ModelMatrix,它构建一个 Matrix{Float64},使其更适合于统计模型的拟合。Formula 也可以用来从 DataFrame 转换为 ModelFrame 对象,这是它的包装器,满足统计建模的需求。

创建一个包含随机值的数据框:

公式 - 数学表达式的特殊数据类型

使用公式将其转换为 ModelFrame 对象:

公式 - 数学表达式的特殊数据类型

ModelFrame 创建 ModelMatrix 很容易:

公式 - 数学表达式的特殊数据类型

还有一列仅包含 value = 1.0。它用于回归模型中拟合截距项。

汇总数据

为了高效地分析庞大的数据集,使用 PooledDataArray。DataArray 使用一种编码方式,为向量的每个条目表示一个完整的字符串。这种方式效率较低,特别是在处理大数据集和内存密集型算法时。

我们的使用案例更常涉及包含少量水平的因子:

汇总数据

PooledDataArray 使用一个较小的水平池中的索引,而不是使用字符串来高效地表示数据。

汇总数据

PooledDataArray 还提供了一个功能,可以使用 levels 函数来查找因子的水平:

汇总数据

PooledDataArray 甚至提供了一个紧凑型函数来高效使用内存:

Julia> pooleddatavector = compact (pooleddatavector) 

然后,它提供了一个池函数,用于在因子未在 PooledDataArray 列中编码,而是在 DataArray 或 DataFrame 中编码时转换单列数据:

Julia>  pooleddatavector = pool(datavector) 

汇总数据

PooledDataArray 促进了类别数据的分析,因为 ModelMatrix 中的列被当作 0-1 指示符列。PooledDataArray 的每个水平都与一列相关联。

网络抓取

现实世界的使用案例还包括从网络抓取数据进行分析。让我们构建一个小型的网页抓取器来获取 Reddit 帖子。

为此,我们将需要 JSON 和 Requests 包:

julia> Pkg.add("JSON") 
julia> Pkg.add("Requests") 

# import the required libraries 
julia> using JSON, Requests 

# Use the reddit URL to fetch the data from 
julia> reddit_url = https://www.reddit.com/r/Julia/ 

# fetch the data and store it in a variable 
julia> response = get("$(reddit_url)/.json") 
Response(200 OK, 21 headers, 55426 bytes in body) 

# Parse the data received using JSON.parse 
julia> dataReceived = JSON.parse(Requests.text(response)) 
# Create the required objects 
julia> nextRecord = dataReceived["data"]["after"] 
julia> counter = length(dataReceived["data"]["children"]) 

这里,我们定义了一个 URL,从这个 URL 中抓取数据。我们从 Reddit 上的 Julia 部分进行抓取。

接下来,我们使用 Requests 包中的 get 函数从定义的 URL 获取内容。我们可以看到已经得到了响应 200 OK,并获得了数据:

julia> statuscode(response) 
200 

julia> HttpCommon.STATUS_CODES[200] 
"OK" 

然后,我们使用 Julia 的 JSON 包提供的 JSON 解析器来解析接收到的 JSON 数据。现在我们可以开始读取记录了。

Web scraping

我们可以将接收到的数据存储在数组或数据框(DataFrame)中(具体取决于使用场景和易用性)。在这里,我们使用数组来存储解析后的数据。我们可以检查存储在数组中的数据。

Web scraping

假设我们只需要查看这些帖子的标题并知道我们抓取了什么;我们只需要知道它们在哪一列。

Web scraping

现在我们可以看到 Reddit 帖子的标题。但如果我们有太多列,或者存在缺失值怎么办?使用 DataFrames 会是更好的选择。

总结

在本章中,我们学习了数据清洗(data munging)是什么,以及为什么它对于数据科学是必要的。Julia 提供了通过 DataFrames.jl 包来简化数据清洗的功能,其中包括以下特点:

  • NA:Julia 中的缺失值由一种特定的数据类型表示,即 NA。

  • DataArrayDataFrames.jl 提供的 DataArray 特性,如允许我们在数组中存储缺失值。

  • DataFrame:DataFrame 是一种二维数据结构,类似于电子表格。它与 R 或 pandas 的数据框类似,并提供了许多功能来表示和分析数据。DataFrames 拥有许多非常适合数据分析和统计建模的特性。

  • 一个数据集可以在不同的列中包含不同类型的数据。

  • 记录与同一行中不同列的其他记录有关系,并且它们的长度相同。

  • 列可以被标记。标记可以帮助我们轻松地熟悉数据,并且无需记住列的数字索引就能访问它们。

我们学习了如何使用 readtable() 函数从文件导入数据并将数据导出到文件。readtable() 函数在使用多个参数时提供了灵活性。

我们还探讨了数据集的连接,如 RDBMS 表。Julia 提供了多种连接方式,我们可以根据实际需求加以利用。

我们讨论了数据科学家广泛使用的 Split-Apply-Combine 策略,以及为什么它是必要的。我们通过 stack 和 melt(stackdf、meltdf)函数探讨了数据的重塑或旋转,并探索了其中的各种可能性。我们还介绍了 PooledDataArray,并学习了它在内存管理中的重要性。

我们学习了网页抓取技术,这对于数据科学家来说,有时是收集数据的必要手段。我们还使用了 Requests 包来获取 HTTP 响应。

参考文献

第三章 数据探索

当我们第一次接收到一个数据集时,大多数时候我们只知道它与什么相关——这种概述不足以开始应用算法或创建模型。数据探索在数据科学中至关重要。它是创建模型之前必不可少的过程,因为它展示了数据集的关键特点,并且明确了我们达成目标的路径。数据探索使数据科学家熟悉数据,并帮助我们知道可以从数据集中推导出什么一般性假设。因此,我们可以说它是从数据集中提取信息的过程,而我们事先并不知道要寻找什么。

在这一章中,我们将研究:

  • 抽样、总体和权重向量

  • 推断列类型

  • 数据集概述

  • 标量统计

  • 变异度量

  • 使用可视化进行数据探索

数据探索涉及描述性统计。描述性统计是数据分析领域,通过有意义地总结数据来发现模式。虽然这可能不会直接得出我们预期的结果或模型,但它肯定有助于理解数据。假设新德里有 1000 万人,如果我们随机抽取 1000 人并计算他们的身高平均值,这个平均值可能不会代表新德里所有人的平均身高,但它肯定能给我们一个大概的概念。

Julia 可以有效地用于数据探索。Julia 提供了一个名为StatsBase.jl的包,其中包含了统计所需的功能。我们假设在整章中,你已经添加了这个包:

julia> Pkg.update() 
julia> Pkg.add("StatsBase") 

抽样

在前面的例子中,我们讨论了如何计算从 1000 万人中随机挑选的 1000 人身高的平均值。假设在收集这 1000 万人的数据时,我们从特定的年龄段或社区开始,或者按某种顺序排列数据。现在,如果我们从数据集中选择 1000 个连续的人,这些人很有可能会有某些相似之处。这种相似性不会给我们展示数据集的实际特征,反而可能影响我们想要获得的洞察。因此,从数据集中抽取一小段连续的数据点,无法为我们提供有价值的信息。为了克服这个问题,我们使用抽样。

抽样是一种从给定数据集中随机选择数据的技术,使得这些数据点彼此不相关,因此我们可以将从这些数据上生成的结果推广到整个数据集。抽样是在总体上进行的。

总体

统计学中的总体是指数据集中所有数据点的集合,这些数据点至少具有一个共同的属性。在前面的例子中,这些人具有相同的地理区域属性。

让我们以鸢尾花数据集为例。尽管它只有 150 条记录,但它能给我们一个关于如何从数据集中抽取样本的概念:

julia> using RDatasets 

julia> iris_dataframe = dataset("datasets", "iris") 

我们将使用包含鸢尾花数据集的 RDatasets 包,并将其加载到 DataFrame 中。因此,这个 DataFrame 包含了“总体”,我们希望从中抽取一个样本:

julia> sample(iris_dataframe[:SepalLength]) 
6.6 

julia> sample(iris_dataframe[:SepalLength], 5) 
5-element Array{Float64,1}: 
 4.8 
 5.4 
 5.0 
 5.2 
 4.3 

sample()函数可用于从数据集或数组中返回一个随机值,或者返回一组随机选择的值:

Julia> sample(x, num_of_elements[; replace=true, ordered=false]) 

replaceordered参数在特定情况下使用:

  • replace:当返回相同的值时执行替换操作时使用(默认值=true

  • ordered:当返回的值按升序排列时使用(默认值=false

理想情况下,从给定数据集中采样的数据应当代表整个总体。但大多数情况下,数据集中的许多群体要么被低估,要么被高估。我们来看之前的例子,如果我们无法收集到 50-70 岁年龄段以及社区 X 的完整数据怎么办?因此,我们的数据集并不能完全代表实际的总体。必须对观察到的数据集进行一些调整。

权重调整是非常常见的一种修正技术。在此技术中,会为每条记录分配一个调整权重。我们认为被低估的记录或群体会获得大于 1 的权重,而我们认为被高估的记录或群体会获得小于 1 的权重。

权重向量

Julia 有一种类型WeightVec,用于表示权重向量,以便为样本分配权重。之所以需要专门的权重向量数据类型,是因为:

  • 显式区分该向量与其他数据向量的角色

  • 通过存储权重总和并避免重复计算权重总和,从而节省计算周期

权重向量可以这样构建:

julia> wv = WeightVec([1., 2., 3.], 6.) 
StatsBase.WeightVec{Float64,Array{Float64,1}}([1.0,2.0,3.0],6.0) 

我们已将权重总和作为第二个参数提供。它是可选的,目的是为了节省计算时间。

为了简化,WeightVec支持一些常见方法。假设wv的类型是WeightVec

julia> eltype(wv) 
Float64 

eltype用于获取WeightVec中值的类型:

julia> length(wv) 
3 

julia> isempty(wv) 
false 

julia> values(wv) 
3-element Array{Float64,1}: 
 1.0 
 2.0 
 3.0 

julia> sum(wv) 
6.0 

# Applying eltypes to iris_dataframe 
# this method is of DataFrames.jl 
julia> eltypes(iris_dataframe)   
5-element Array{Type{T},1}: 
 Float64                       
 Float64                       
 Float64                       
 Float64                       
 Union{ASCIIString,UTF8String} 

其他方法不言自明。由于WeightVec已经存储了总和,因此它可以立即返回,而无需进行任何计算。

推断列类型

要理解数据集并继续处理,我们需要首先了解我们拥有的数据类型。由于我们的数据是按列存储的,在执行任何操作之前,我们应该知道它们的类型。这也叫做创建数据字典:

julia> typeof(iris_dataframe[1,:SepalLength]) 
Float64 

julia> typeof(iris_dataframe[1,:Species]) 
ASCIIString 

我们在这里使用了经典的鸢尾花数据集。我们已经知道这些列中数据的类型。我们可以将相同的函数应用于任何类似的数据集。假设我们只给定了没有标签的列,那么就很难确定这些列中数据的类型。有时,数据集看起来像是包含数字,但它们的数据类型却是ASCIIString。这些可能会在后续步骤中引发错误。幸运的是,这些错误是可以避免的。

基本统计汇总

尽管我们目前使用的是 RDatasets,这些数据集有充分的细节和文档支持,但这些方法和技术也可以扩展到其他数据集。

我们使用一个不同的数据集:

基本统计摘要

我们正在使用来自 RDatasets 包的另一个数据集。这些数据是来自伦敦内区的考试成绩。为了获取数据集的一些信息,我们将使用describe()函数,这个函数我们在之前的章节中已经讨论过:

基本统计摘要

各列的描述如下:

  • Length指的是记录(行)的数量。

  • Type指的是列的数据类型。因此,School的类型是Pooled ASCIIString

  • NANA%分别指的是列中NA值的数量和百分比。这非常有用,因为你现在不需要手动检查缺失的记录了。

  • Unique指的是列中唯一记录的数量。

  • MinMax是列中的最小值和最大值(这不适用于ASCIIStrings类型的列)。这些是数据点的 0%和 100%的值。MinMax定义了数据的范围。

  • 第一四分位数和第三四分位数分别指的是数据点的 25%和 75%的值。同样,中位数指的是数据点的 50%的值。

计算数组或数据框的均值

Julia 提供了不同种类的均值函数,每种函数都有其特定的使用场景:

  • geomean(arr):这是计算arr的几何均值:

计算数组或数据框的均值

  • harmmean(arr):这是计算arr的调和均值:

计算数组或数据框的均值

  • trimmean(arr, fraction):用于计算裁剪数据集的均值。第二个参数用于提供裁剪数据集的比例。例如,如果fraction的值为 0.3,均值将通过忽略顶部 30%和底部 30%的值来计算。通常用于去除异常值:

计算数组或数据框的均值

均值函数也有扩展。它可以接受一个加权向量作为参数来计算加权均值:

计算数组或数据框的均值

标量统计

Julia 的包提供了各种函数来计算不同的统计数据。这些函数用于根据需要以不同的方式描述数据。

标准差和方差

我们之前计算的均值和中位数(在describe()函数中)是集中趋势的度量。均值指的是在应用加权后计算出的中心,中位数指的是数据列表的中心。

这只是其中一条信息,我们还希望了解更多有关数据集的信息。了解数据点在数据集中的分布是很有帮助的。我们不能仅仅使用最小值和最大值函数,因为数据集中可能会有异常值。因此,这些最小值和最大值函数会导致错误的结果。

方差是衡量数据集数据点分布程度的指标。它是通过计算数字与均值之间的距离来计算的。方差衡量数据集中每个数值与均值的偏离程度。

以下是方差的公式:

标准差与方差标准差与方差

我们还可以沿特定维度计算方差,这对于 DataFrame 非常有用:

标准差与方差

这里,第二个参数是我们希望计算方差的维度。

标准差是衡量数据集值的分散度或离散度的指标。它是方差的平方根。如果它接近 0,这意味着数据集的分散程度非常小,接近均值。而较大的值则定义了数据点从均值的高分散度。标准差与方差不同,因为它的单位与均值相同:

标准差与方差

我们还可以像方差一样沿某个维度计算标准差。

Julia 提供了一个函数来计算均值和方差,也可以同时计算均值和标准差:

标准差与方差

统计分析包括基于偏度和峰度的数据特征化。偏度是衡量数据集或分布中心点对称性缺失的度量。因此,分布可以向左偏或向右偏。

峰度是分布或数据集相对于正态分布的平坦程度的度量。因此,具有在中心(均值)处具有高峰并且两侧急剧下降的分布被称为具有高峰度,而均值处具有平坦峰的分布则被称为具有低峰度:

标准差与方差

统计中的矩是:

  • 0 阶矩是总概率

  • 1 阶矩是均值

  • 2 阶中心矩是方差

  • 3 阶矩是偏度

  • 4 阶矩是峰度(带移位和标准化)

标准差与方差

这里我们正在计算第 k 阶中心矩。它定义为:

(a - mean(a)).^k 

变化度量

了解数据集中的值的变化情况是很有帮助的。各种统计函数能够提供:

  • span(arr):span 用于计算数据集的总分布范围,即 maximum(arr)minimum(arr)

变化度量

  • variation(arr):也称为变异系数。它是标准差与数据集均值的比率。相对于总体的均值,CV 表示变异性的程度。它的优点是它是一个无量纲的数字,可以用来比较不同的数据集。

变异性度量

均值的标准误差:我们对从总体中抽取的不同样本进行工作。我们计算这些样本的均值,并称之为样本均值。对于不同的样本,我们不会得到相同的样本均值,而是一个样本均值的分布。这些样本均值分布的标准差称为均值的标准误差。

在 Julia 中,我们可以使用sem(arr)计算均值的标准误差。

平均绝对偏差是一个稳健的集中趋势度量。稳健性是指不受离群值的影响。

变异性度量

我们可以将中心值作为第二个参数提供。

Z 分数

z 分数指的是与分数均值的关系。它通过一个元素距离均值多少个标准差来计算。0 的 z 分数意味着它与均值相同。

它由公式 z = (X - μ) / σ 给出:

julia> a = [12,23,45,68,99,72,61,39,21,71] 

在这个数据集上,我们可以像这样计算 z 分数:

Z 分数

均值和标准差是自我计算的。

熵是数据集中无序的度量,提供了系统随机性的近似度量。它随着随机性增加而增大。

让我们创建一个概率向量:

熵

我们创建了一个相当小的数组:

熵

概率向量元素的总和为 1\。它趋近于 1。现在我们计算熵:

熵

熵计算是使用自然对数进行的。如果需要,我们也可以提供对数的底数。

熵

我们提供的第二个参数是对数的底数。我们还可以计算交叉熵,它被认为是平方误差的有效替代:

Julia> crossentropy(ProbabilityVector1, ProbabilityVector2) 

分位数

为了更好地理解数据集,我们想要知道数据集中的最低点和最高点。我们可以使用 min 和 max 函数来实现。所以,我们也可以说最小和最大数据点分别位于 0% 和 100%。如果我们想要找出数据集中的任意数据点在 n% 的位置,我们使用 quantile 函数。

在存在离群值的情况下,分位数非常有用。例如,对于 a 我们正在分析多个浏览器在网站上的响应时间:98% 的流量来自桌面,且能在不到一秒的时间内加载页面;剩余 2% 的流量来自手机,加载页面需要 5 秒。在这种情况下,如果使用场景允许,我们可能想忽略这 2%(以便分析网站的实际流量)。

分位数

现在,来计算分位数:

分位数

这里,我们得到了五个值。这五个值代表数据集在 0%、25%、50%、75%和 100%位置的数值。

四分位距是衡量数据变异性的一种方法,它通过上四分位数和下四分位数的差值计算,即 Q3-Q1。其计算公式为:

分位数

百分位数是统计学中常用的术语,用于表示数据点在数据集中的位置。其计算公式为:

分位数

我们使用了相同的数据集,并计算了 0.5 在数据集中的位置。

还有一个重要的函数,nquantile。它用于创建由我们定义的分位数向量:

分位数

众数

在探索数据集时,我们可能希望知道哪些数据在数据集中经常重复。这是最有可能出现在样本中的值。Julia 提供了一个计算众数的函数:

众数

我们在与前述示例相同的数据集上计算了众数。因此,0.2566在数据集中出现的频率最高。

数据集摘要

之前我们讨论过describe()函数,它打印数据集的摘要。Julia 还提供了另一个函数,summarystats()

使用summarystats(a)函数在前述示例的同一数据集上,我们得到如下结果。因此,我们现在无需单独计算它们,且可以大致了解数据集的类型。

数据集摘要

散点矩阵和协方差

协方差常被数据科学家用来找出两个有序数据集是否朝相同方向变化。它可以轻松地定义变量之间是否相关。为了更好地表示这种行为,我们创建了协方差矩阵。协方差矩阵的未归一化版本即为散点矩阵。

要创建散点矩阵,我们使用scattermat(arr)函数。

默认行为是将每行视为一个观测值,每列视为一个变量。通过提供关键词参数vardimmean,可以改变这一行为:

  • Vardimvardim=1(默认)表示每列是一个变量,每行是一个观测值。vardim=2则为反向操作。

  • mean:均值由scattermat计算得出。我们可以使用预定义的均值来节省计算周期。

我们还可以使用cov函数创建加权协方差矩阵。它同样接受vardimmean作为可选参数,目的相同。

计算偏差

StatsBase.jl 提供了多种计算两个数据集之间偏差的函数。虽然也可以使用其他函数来计算,但为了简便和高效,StatsBase 提供了这些高效实现的函数:

  • 平均绝对偏差:对于两个数据集ab,它的计算方式为meanad(x,y),该函数是mean(abs(x-y))的封装。

  • 最大绝对偏差:对于两个数据集 ab,它的计算方法是 maxad(x,y),该方法是 maximum(abs(x-y)) 的封装。

  • 均方偏差:对于两个数据集 ab,它的计算方法是 msd(x,y),该方法是 mean(abs2(x-y)) 的封装。

  • 均方根偏差:对于两个数据集 ab,它的计算方法是 rmsd(a,b),该方法是 sqrt(msd(a, b)) 的封装。

排名

当数据集按升序排序时,会为每个值分配一个排名。排名是一种过程,其中数据集被转换,值被替换为它们的排名。Julia 提供了用于各种类型排名的函数。

在序数排名中,数据集中的所有项都被分配了一个独特的值。具有相同值的项被任意分配排名。在 Julia 中,这是通过 ordinalrank 函数实现的。

Rankings

假设这是我们的数据集,我们想要进行序数排名:

Rankings

使用 ordinalrank(arr) 函数,我们得到了序数排名。同样,StatsBase 也提供了用于查找其他类型排名的函数,如 competerank()denserank()tiedrank()

计数函数

在数据探索中,通常会对一个范围进行计数。这有助于找出出现次数最多/最少的值。Julia 提供了 counts 函数来对一个范围进行计数。假设我们有一个值的数组。为了方便,我们现在使用 random 函数来创建一个数组:

Counting functions

我们创建了一个包含 30 个值的数组,值的范围从 1 到 5。现在我们想知道它们在数据集中出现了多少次:

Counting functions

使用 count 函数,我们发现 1(7)、2(1)、3(5)、4(11)和 5(6)。计数函数根据不同的参数进行调整以适应使用场景。

proportions() 函数用于计算数据集中值的比例,Julia 提供了该函数:

Counting functions

我们对与之前示例相同的数据集计算了比例。它显示数据集中值 1 的比例为 0.23333。这也可以看作是在数据集中找到该值的概率。

其他计数函数包括:

  • countmap(arr):这是一个映射函数,将值映射到数据集中出现的次数(或总权重):

Counting functions

  • proportionmap(arr):这是一个类似于 countmap(arr) 的映射函数,但它将值映射到它们的比例:

Counting functions

countmapproportionmap 应用到我们的数据集上,得到了这些值。两个函数都返回一个字典。

直方图

在对数据有了基本理解之后,也可以借助可视化进行数据探索。绘制直方图是通过可视化进行数据探索的最常见方式之一。直方图用于将数据在实际平面上分成规律间隔并进行统计。

使用 fit 方法可以创建直方图:

julia> fit(Histogram, data[, weight][, edges]) 

fit接受以下参数:

  • data:数据以向量形式传递给fit函数,可以是一维的或 n 维的(由相同长度的向量组成的元组)。

  • weight:这是一个可选参数。如果值具有不同的权重,可以传递一个WeightVec类型的参数。默认的权重值为 1。

  • edges:这是一个向量,用于指定每个维度上的分箱边缘。

它还接受一个关键字参数nbins,用于定义直方图在每个维度上使用的分箱数量:

直方图

在这个例子中,我们使用了两个随机值生成器和nbins来定义直方图的分箱数量。我们在随机生成的数据上创建了直方图。接下来我们在来自RDatasets包的数据集上尝试一下。这个包可以在这里找到:stat.ethz.ch/R-manual/R-devel/library/datasets/html/sleep.html

直方图

我们正在使用一个名为sleepstudy的数据集,来自RDatasets包。它包含三列数据:Reaction (Float64)Days (Integer)Subject (Integer)。我们将基于此数据创建一个直方图。

直方图

我们现在可以意识到,通过可视化数据更容易理解数据。可视化是数据探索的重要部分。要能够有效地可视化数据,必须进行必要的数据整理,并对变量有一定的理解。在这次可视化中,我们可以观察到哪些区域更密集,以及反应时间的分布。

我们之前讨论了散点矩阵。我们可以创建一个散点图,看看它是否能帮助我们。

直方图

我们可以清晰地观察到,受试者的反应时间随着天数的增加而逐渐增大。我们能够很快得出这个结论;否则,可能需要花费相当长的时间。

让我们更深入地分析这个数据集。假设我们想了解每个受试者的表现。由于所有受试者并不相同,有些受试者的表现可能与其他人有很大不同。

在大型数据集上,我们可以进行分组或聚类;但是在这里,由于数据集较小,我们可以单独分析各个受试者。

直方图

很明显,即使被剥夺了多日的睡眠,309号受试者的反应时间仍然非常低。这些是我们在通过可视化分析数据集时,常常忽略的小细节。

我们将在第五章中详细讨论可视化,使用可视化理解数据。我们将探索 Julia 中可用于可视化的各种包,并且如果需要,也会介绍如何调用 R 和 Python 的包进行可视化。我们还会介绍一些基本的 D3.js 示例。

在 Julia 中创建基本图形非常容易,例如:

直方图

现在让我们对鸢尾花数据集尝试一些可视化:

julia> x=:SepalLength, y=:SepalWidth, color=:Species) 

尽管现在它不完全可见,但我们可以看到有明显的簇存在。也许,我们可以通过这些簇区分不同的物种。因此,可视化在发现这些洞察方面非常有用。

相关性分析

Julia 提供了一些函数来简化相关性分析。相关性和依赖性是统计学中常见的术语。依赖性指的是一个变量与另一个变量之间具有统计关系,而相关性指的是一个变量与另一个变量之间可能存在更广泛的关系,这其中也可能包括依赖性。

autocov(x) 函数用于计算 x 的自协方差。它返回与 x 大小相同的向量。

相关性分析

这是我们生成的数据集。我们可以对该数据集应用 autocov

相关性分析

要计算自相关,我们使用 autocor 函数:

相关性分析

同样,我们也可以计算交叉协方差和交叉相关性。为此,我们将生成另一个相同大小的随机数组:

相关性分析

2 个长度为 6 的数组的交叉协方差和交叉相关性结果是长度为 11 的数组。

总结

在本章中,我们讨论了数据探索为何重要,以及如何对数据集进行探索性分析。

这些是我们讨论的各种重要技术和概念:

  • 抽样是一种从给定数据集中随机选择无关数据的技术,以便我们可以将基于选定数据生成的结果推广到整个数据集。

  • 权重向量在我们拥有或收集的数据集无法代表实际数据时非常重要。

  • 为什么了解列的类型非常必要,以及如何通过汇总函数帮助我们获取数据集的要点。

  • 均值、中位数、众数、标准差、方差和标量统计,以及它们在 Julia 中的实现。

  • 测量数据集的变化非常重要,z-分数和熵在此过程中非常有用。

  • 在进行一些基本的数据清理和理解之后,可视化可以非常有益并提供洞察。

参考文献

第四章 深入了解推断统计

我们的世界是一个大数据生成机器。这些日常活动由随机且复杂的事件组成,这些事件可以帮助我们更好地理解单变量分布:正态分布、伽马分布、二项分布等世界。为此,我们将尝试更深入地理解这些过程。

推断统计 是基于从样本数据中获得的证据和推理来得出一个结论,并将该结论推广到总体。推断统计考虑到存在一定的抽样误差,这意味着我们从总体中抽取的样本可能无法完美地代表总体。

推断统计包括:

  • 估计

  • 假设检验

样本和总体之间有什么区别?总体是我们希望获得知识的所有事件或观察的集合。但总体的大小可能非常庞大,以至于分析每个事件或观察并不方便或可行。在这种情况下,我们会选择一个子集,它能够很好地定义我们希望分析的总体。我们称这个子集为总体的样本。

在上一章中,我们讨论了描述性统计。尽管推断统计和描述性统计都可以基于同一组数据进行,但它们是截然不同的。我们可能只对样本数据应用描述性统计,但推断统计利用这些样本数据以及其他数据来进行推广,从而得出适用于更大总体的结论。

因此,描述性统计提供了数据的数值或图形摘要。它仅帮助我们理解现有的数据,但我们无法使用这些结果得出可以推广到整个总体的结论。

通过推断统计,我们试图建立适用于整个总体的结论。但是,推断统计受到两个主要条件的限制:

  • 我们的样本数据是否真正代表了总体

  • 我们形成的假设,是否正确地使得样本数据能够代表总体

样本数据可能无法完美代表总体,因此我们总是面临一定程度的不确定性。因此,我们做出一些估计或假设来应对这种不确定性,这可能会对我们生成的结果产生影响。

在 Julia 中,我们有多种包用于推断统计。Distributions.jl 就是其中之一,提供与概率分布相关的函数。Distributions.jl 涵盖了以下统计方法:

  • 分布的特性——均值、方差、偏度、峰度(矩)和熵

  • 概率密度/质量函数

  • 特征函数

  • 最大似然估计

  • 最大后验估计(MAP) 概率估计

安装

Distributions.jl 是一个注册的 Julia 包,可以通过以下命令进行添加:

julia> Pkg.add("Distributions") 

后续章节需要安装该包。所以我们假设您现在已经安装了该包。

理解抽样分布

抽样分布是从一个随机抽取的样本中收集每个可能统计量的可能性。通过抽样分布,可以在不了解完整总体的情况下推导出有用信息。假设我们正在计算样本均值,但我们不知道总体情况。即便如此,我们仍可以假设样本均值在总体均值的某个标准差范围内。

理解正态分布

正态分布是推理统计的核心。它就像一个钟形曲线(也叫高斯曲线)。大多数复杂的过程可以通过正态分布来定义。

让我们看看正态分布是什么样子。首先,我们将导入必要的包。现在我们包括 RDatasets,但稍后才会使用它:

理解正态分布

我们首先设置种子,然后探索正态函数:

理解正态分布

根据给出的警告,我们也可以使用fieldnames代替names。建议仅在 Julia 的新版中使用fieldnames

在这里,我们可以看到 Normal 函数在 Distributions 包中,并且具有 Univariate 和 Continuous 特性。normal()函数的构造器接受两个参数:

  • 均值 (μ)

  • 标准差 (σ)

让我们实例化一个正态分布。我们将均值 (μ) 设置为 1.0,标准差 (σ) 设置为3.0

理解正态分布

我们可以检查我们保留的均值和标准差:

理解正态分布

使用这个正态分布对象,我们现在可以通过随机函数创建一个分布:

理解正态分布

为了更好地理解该函数,让我们使用 Gadfly 绘制一个直方图:

理解正态分布

参数估计

这用于找出它最适合用哪种分布来描述。我们可以使用fit函数来实现:

参数估计

我们使用了[1.0, 3.0]来创建x,我们可以看到估计值相当接近。

Distributions.jl中的类型层次结构

Distributions.jl中提供的函数遵循一个层级结构。我们来了解一下它,以便理解该包的功能。

理解 Sampleable

Sampleable 是一个抽象类型,包含了可以抽取样本的采样器和分布。其定义如下:

理解 Sampleable

可以抽取的样本类型由两个参数类型定义:

  • VariateForm:

    • Univariate: 标量数值

    • Multivariate: 数值向量

    • Matrixvariate: 数值矩阵

  • ValueSupport:

    • 离散型:整数

    • 连续型:Float64

我们可以提取 Sampleable 对象生成的样本信息。根据变量形式,数组可以包含多个样本。我们可以使用各种函数获取这些信息(假设sampobj是 Sampleable 对象):

  • length(sampobj):顾名思义,返回样本的长度,当对象为单变量时,值为 1

  • size(sampobj):返回样本的形状

  • nsamples(sampobj, X):返回 X 中的样本数量

  • eltype(sampobj):返回样本中元素的默认类型

  • rand(sampobj, x):返回从样本中抽取的 x 个样本:

    • 对于sampobj=univariate,返回长度为 x 的向量

    • 对于sampobj=multivariate,返回一个 x 列的矩阵

    • 对于sampobj=matrix-variate,返回一个样本矩阵的数组

表示概率分布

为了更好地表示概率分布,使用Distribution(它是Sampleable的子类型):

表示概率分布

为了方便使用,我们通常为常用分布定义typealias

julia> typealias UnivariateDistribution{S<:ValueSupport}   Distribution{Univariate,S} 

julia> typealias MultivariateDistribution{S<:ValueSupport} Distribution{Multivariate,S} 

julia> typealias MatrixDistribution{S<:ValueSupport}       Distribution{Matrixvariate,S} 

单变量分布

每个样本是标量的分布是单变量分布。我们可以根据它们支持的值进一步将其分类为两种分布:

  • 单变量连续分布

  • 单变量离散分布

抽象类型:

julia> typealias UnivariateDistribution{S<:ValueSupport} Distribution{Univariate,S} 

julia> typealias DiscreteUnivariateDistribution   Distribution{Univariate, Discrete} 
julia> typealias ContinuousUnivariateDistribution Distribution{Univariate, Continuous} 

在包中为单变量分布实现了多种方法,提供了必要的功能。

检索参数

  • params(distributionX):返回一个参数元组

  • succprob(distributionX):返回成功的概率

  • failprob(distributionX):返回失败的概率

  • dof(distributionX):返回自由度

  • ncategories(distributionX):返回类别的数量

  • ntrials(distributionX):返回试验的次数

统计函数

常见的统计函数,如mean()median()mode()std()var()等,适用于这些分布。

概率评估

除了各种统计函数外,Julia 还提供了用于评估概率的函数:

  • pdf(distributionX)pdf指的是概率密度函数。它返回distributionX的概率向量。函数的第二个参数还可以提供一个值范围,形式为a:b

  • cdf(distributionX)cdf指的是累积分布函数。

  • insupport(distributionX,x):该支持函数返回distributionX, x是否在支持范围内。

单变量分布中的采样

我们之前讨论过随机数生成。它也可以用来从分布中抽取样本:

julia> rand(distributionX)

这将从distributionX中抽取一个样本。它使用多重分派,并且我们可以根据需要提供其他参数:

Julia> rand(distributionX,n) 

这将从distributionX返回一个由 n 个独立样本组成的向量。

理解离散单变量分布及其类型

离散单变量分布是这些分布的超类,从这些分布中抽取的样本是整数类型。

伯努利分布

伯努利分布是一个离散分布。它有两个可能的结果,假设它们是n=0n=1。在这里,如果我们将n=1视为成功,并将其概率记为p,则n=0为失败,其概率为q=1-p,其中0<p<1

在 Julia 中,伯努利分布的实现方式如下:

julia> Bernoulli(p) 

这里,p 是成功率(概率)。

二项分布

二项分布是另一种离散概率分布。它由Pp给出,表示从 N 次伯努利试验中获得 n 次成功。在一系列独立试验之后,获得的成功次数服从二项分布:

二项分布

这是一个试验次数为 1,成功率为p=0.5的二项分布:

二项分布

这里我们已指定了trials=5。成功率保持默认值:

二项分布

我们还可以定义成功率。因此,这将返回一个分布,试验次数为 5,成功率为p=0.3

连续分布

连续单变量分布是所有连续单变量分布的超类,每个从连续单变量分布中抽取的样本类型为Float64

柯西分布

柯西分布也叫做洛伦兹分布。它是一种描述共振行为的连续分布:

柯西分布

这给出了标准的柯西分布(位置 = 0.0,尺度 = 1.0):

柯西分布

我们可以传递参数。这个将给我们具有位置u和尺度s的柯西分布。

卡方分布

具有k自由度的卡方分布是由卡方随机变量的平方根形成的分布,卡方随机变量是由k个独立且服从正态分布的变量的平方和构成的。

在 Julia 中,它的实现方式如下:

julia> Chi(k) 

这将形成一个具有k自由度的卡方分布。

它用于通过除以卡方分布的均值来获得正态分布标准差的无偏估计的修正因子。

卡方分布

具有k自由度的卡方分布是k个独立标准正态随机变量的平方和的分布。

在 Julia 中,卡方分布的实现方式如下:

julia> Chisq(k) 

这里,k 是自由度。

卡方分布在卡方检验中的重要性在于:

  • 它用于检验观测分布的拟合优度

  • 对于正态分布,它用于根据样本标准差获取总体标准差的置信区间估计。

  • 它也用于获得定性数据的分类标准独立性

截断分布

有时需要将一个分布限制在特定的领域或范围内,限制后的分布称为截断分布。这在我们只能记录在特定范围内的事件,或当给定了一个阈值时非常有用:

截断分布

这是当截断分布限制在两个常数之间时的情况。在 Julia 中,它的实现如下:

截断分布

  1. 非截断情况:−∞ = a,b = +∞。

  2. 下截断情况:−∞ < a,b = +∞。

  3. 上截断情况:−∞ = a,b < +∞。

  4. 双重截断情况:−∞ < a,b < +∞

截断分布

.

然而,一些可用于单变量分布的统计函数也可以用于一般截断分布。之所以无法使用这些函数,是因为由于截断,计算变得复杂。

截断正态分布

这是一个特殊类型的分布,其中截断分布形成了正态分布。

它可以通过专用构造函数TruncatedNormal来实现,或者通过将 Normal 构造函数作为参数传递给 Truncated 构造函数:

Julia> TruncatedNormal(mu, sigma, l, u) 

由于这是正态分布,因此一些通常不适用于一般截断分布的统计函数可以在截断正态分布中使用。

理解多元分布

多元概率分布是包含多个随机变量的分布。这些随机变量之间可能存在也可能不存在相关性。从该分布中抽取的样本是一个向量。Distributions.jl 提供了常用的多元函数实现——多项分布多元正态分布狄利克雷分布。它们的实现如下:

理解多元分布

它的类型别名如下所示:

julia> typealias MultivariateDistribution{S<:ValueSupport} Distribution{Multivariate,S} 

julia> typealias DiscreteMultivariateDistribution   Distribution{Multivariate, Discrete} 
julia> typealias ContinuousMultivariateDistribution Distribution{Multivariate, Continuous} 

大多数适用于单变量分布的方法也适用于多元分布。

多项分布

这是二项分布的推广。假设在一个大小为 k 的类别分布的有限集合上,我们进行 n 次独立抽样。

让我们将其表示为:X = X[1], X[2], ............ X[k]

然后,这个 X 表示一个多项式分布,其每个样本都是一个 k 维整数向量,且其总和为 n

在 Julia 中,它的实现如下:

julia> Multinomial(n, p) 

这里,p 代表概率向量,我们正在通过 n 次试验创建该分布。

多元正态分布

这是正态分布的多维推广:

多元正态分布

多变量正态分布的重要性原因:

  • 数学简洁性:使用这种分布更容易进行处理

  • 中心极限定理的多变量版本:如果我们有一组独立且同分布的随机向量 X[1], X[2],…, X[n],那么对于大样本,样本均值向量 x¯x¯ 将近似于多变量正态分布

  • 它被广泛应用于许多自然现象的建模

Multivariate normal distribution

实现了三种类型的协方差矩阵:

  • 完全协方差。

  • 对角协方差。

  • 各向同性协方差。

julia> typealias FullNormal MvNormal{PDMat,    Vector{Float64}} 
julia> typealias DiagNormal MvNormal{PDiagMat, Vector{Float64}} 
julia> typealias IsoNormal  MvNormal{ScalMat,  Vector{Float64}} 

julia> typealias ZeroMeanFullNormal MvNormal{PDMat,    ZeroVector{Float64}} 
julia> typealias ZeroMeanDiagNormal MvNormal{PDiagMat, ZeroVector{Float64}} 
julia> typealias ZeroMeanIsoNormal  MvNormal{ScalMat,  ZeroVector{Float64}} 

均值向量可以是Vector{Float64}类型的实例,或者ZeroVector{Float64}ZeroVector{Float64}是一个填充零的向量。

多变量正态分布的构建方式如下:

  • MvNormal(mu, sig)mu指的是均值,sig指的是协方差

  • MvNormal(sig):我们没有传递均值,因此均值将为零

  • MvNormal(d, sig):这里d指的是维度

狄利克雷分布

狄利克雷分布表示多项分布的共轭先验。这意味着,如果多项分布的先验分布是狄利克雷分布,那么后验分布也将是狄利克雷分布:

Dirichlet distribution

这告诉我们,狄利克雷分布是多变量家族的一部分,并且是一个连续分布:

Dirichlet distribution

狄利克雷方法接受的参数。这些被用作:

julia> Dirichlet(alpha) 

这里,alpha 是一个向量:

julia> Dirichlet(k, a) 

在这里,a是一个正标量。

理解矩阵变分分布

这是一种从中抽取的样本类型为矩阵的分布。许多可以应用于单变量和多变量分布的方法也可以用于矩阵变分分布。

威萨特分布

这是一种矩阵变分分布,是卡方分布向两个或多个变量的推广。它是通过加总独立且同分布、均值为零的多变量正态随机向量的内积来构建的。它作为多变量正态随机数据样本协方差矩阵的分布模型,在根据样本大小进行缩放之后:

julia> Wishart(v, S) 

这里,v指的是自由度,S是基础矩阵。

逆威萨特分布

这是多变量正态分布协方差矩阵的共轭先验。在 Julia 中,使用以下方式实现:

julia> InverseWishart(v, P) 

这表示具有v自由度和基础矩阵P的逆威萨特分布。

分布拟合

分布拟合是将一个概率分布拟合到一系列数据中,以预测在某一特定区间内变量现象的概率。我们可以从拟合的分布中得到良好的预测,这与数据的拟合度较高。根据分布和现象的特征,有些分布可以与数据拟合得更好:

julia> d = fit(Distribution_type, dataset) 

这将类型为Distribution_type的分布拟合到给定的数据集;dataset.x是数组类型,包含所有样本。拟合函数会找到最适合拟合该分布的方法。

分布选择

分布是通过数据相对于均值的对称性或偏斜性来选择的。

对称分布

对于倾向于呈钟形曲线的对称分布,正态分布和逻辑斯蒂分布最为适合。当峰度较高时,值会远离中心,这时也可以使用学生 t 分布。

偏斜分布向右

也称为正偏斜,这是指较大值与均值的距离大于较小值与均值的距离。在这些情况下,对数正态分布和对数逻辑斯蒂分布最为适合。同时,指数分布、韦布尔分布、帕累托分布和甘布尔分布也可以适用于一些此类情况。

偏斜分布向左

负偏斜或向左的偏斜是指较小值与均值的距离大于较大值与均值的距离。对于此类数据,平方正态分布、Gompertz 分布和倒置或镜像甘布尔分布较为适用。

最大似然估计

最大似然估计MLE)是一种估计给定统计量参数的过程,它使给定的分布最大化。这是一种解析最大化过程。

例如,我们从一个总体中抽取了样本,但由于某些原因,我们无法测量整个总体。我们希望了解该总体的一些统计数据;这可以通过最大似然估计来完成,前提是数据服从正态分布。MLE 会根据我们所拥有的数据和给定的模型,给出具有最高概率的参数值。

MLE 的样本性质:

  • 无偏最小方差估计量(大样本量)

  • 可以通过计算近似的正态分布和近似样本方差来生成置信区间

  • 可用于检验关于模型和参数的假设

最大似然估计的缺点:

  • 最大似然估计(MLE)可能会受到少量失败的影响(大样本量无法克服这一点)

  • 计算 MLE 需要解决复杂的非线性方程

MLE 通过选择统计模型和给定数据集的模型参数值集合来最大化似然函数。在样本量很大的情况下(趋向无穷大),MLE 具有以下特点:

  • 效率:MLE 的渐近均方误差是所有一致估计量中最低的。

  • 渐近正态性:随着样本量的增加,MLE 分布趋向于高斯分布。

  • 一致性:序列的概率会收敛到估计值。

  • 经过偏差修正后,它具有二阶效率。

在 Julia 中,我们有一个用于最大似然估计的函数fit_mle。它使用多重分发:

julia> fit_mle(Distribution, dataset) 

  • dataset 可以是单变量分布的数组。

  • dataset 是多变量分布的矩阵:

 julia> fit_mle(Distribution, weights, dataset)

  • 这包括一个附加参数weights,它是一个长度为n的数组。n等于数据集中包含的样本数。

在撰写本文时,fit_mle 已经为以下最常用的分布实现:

  • 单变量分布:正态分布、伽玛分布、二项分布、伯努利分布、类别分布、均匀分布、拉普拉斯分布、指数分布、几何分布等。

  • 多变量分布:多项分布、多元正态分布和狄利克雷分布。

如前所述,fit_mle 使用多重分发。某些分布的实现与其他分布有所不同。

关于二项分布:

  • fit_mle(BinomialDistribution, numOfTrials, dataset, weights):试验次数是一个附加参数,表示每个实验的试验次数。weights 是一个可选参数。

关于类别分布:

  • fit_mle(CategoricalDistribution, spaceSize, dataset, weights)spaceSize 是一个附加参数,表示不同值的数量。weights 是一个可选参数。

充分统计量

Julia 提供了一个可以用来生成估计值并应用最大似然估计(fit_mle)的函数。

使用方法:

julia> gensuffstats = suffstats(Distribution, dataset, weights) 

这里,weights 是一个可选参数。这将生成数据集的充分统计量,现在我们可以应用 fit_mle

julia>  fit_mle(Distribution, gensuffstats) 

使用充分统计量函数的原因是它更高效。

最大后验估计

这也被称为能量最小化。待估计的参数,虽然是未知的,但被认为是固定的,不同于 MLE,它被认为是一个随机变量。

在贝叶斯分析中,对于我们想要估计的物理过程参数,可能有先验信息,这些信息可能来自经验证据或其他科学知识。此类信息可以通过概率分布函数pdf)编码在待估计的参数上:

julia> posterior(priori, suffst) 

这将返回后验分布,后验分布基于由充分统计量提供的数据,并且与先验分布(先验分布)属于相同类型。

你可以通过以下方式生成最大后验估计:

julia> fit_map(priori, G, dataset[, weights]) 

这里G是似然模型(或分布)。

你可以通过以下方式生成完整的分布:

julia> complete(priori, G, params) 

这将根据给定的参数param和似然模型G计算出完整的分布。

置信区间

这描述了与未知总体参数相关的不确定性,该参数位于总体值的估计范围内。

置信区间

解释置信区间

假设已知总体均值大于 100 且小于 300,置信区间为 95%。

一般认为总体均值落在 100 到 300 之间的概率是 95%。这是错误的,因为总体均值不是一个随机变量,而是一个常数,它不会变化,其落在任何指定范围内的概率是 0 到 1 之间。

与抽样方法相关的不确定性水平由置信水平描述。假设选择不同的样本,并对每个样本计算不同的区间估计,我们使用相同的抽样方法。真实的总体参数会出现在一些这些区间估计中,但不是每一个。

因此,95%的置信水平意味着总体参数出现在 95%的区间估计中。

这里是构造置信区间的步骤:

  • 确定样本统计量

  • 选择置信水平

  • 计算误差范围:

    误差范围 = 统计量的标准差(误差)× 临界值

  • 描述置信水平:

    置信区间 = 误差范围 + 样本统计量

在 Julia 中,置信区间是通过ci函数计算的。ci通用函数有 12 种方法:

ci(x::HypothesisTests.Btest) 
ci(x::HypothesisTests.BTest, alpha::Float64)

ci(x::HypothesisTests.BinomialTest) 
ci(x::HypothesisTests.BinomialTest, alpha::Float64)

ci(x::HypothesisTests.SignTest) 
ci(x::HypothesisTests.SignTest, alpha::Float64)

ci(x::HypothesisTests.FisherExactTest) 
ci(x::HypothesisTests.FisherExactTest, alpha::Float64)
ci(x::HypothesisTests.TTest) 
ci(x::HypothesisTests.TTest, alpha::Float64)

ci(x::HypothesisTests.PowerDivergenceTest) 
ci(x::HypothesisTests.PowerDivergenceTest, alpha::Float64) 

使用方法

要获得二项比例的置信区间,可通过以下方式使用:

julia> ci(test::BinomialTest,alpha=0.05; tail=:both,method=:clopper_pearson) 

这将计算置信区间,该区间的覆盖率为 1-alpha。所用方法是 Clopper Pearson。

也可以使用其他方法:

  • Wald 区间(:wald

  • Wilson 得分区间(:wilson

  • Jeffreys 区间(:jeffrey

  • Agresti Coull 区间(:agresti_coull

要获得多项式比例的置信区间,可通过以下方式使用:

julia> ci(test::PowerDivergenceTest, alpha=0.05; tail=:both, method=:sison_glaz) 

sison_glaz外的其他方法:

  • 自助法区间(:bootstrap

  • Quesenberry, Hurst 区间(:quesenberry_hurst

  • Gold 区间(:gold

理解 Z 分数

Z 分数指的是元素距离均值的标准差。

通过以下公式给出:

理解 Z 分数

这里X表示元素的值,σ是标准差,μ是总体均值。

解释 Z 分数

  • z-score<0:元素小于均值

  • z-score>0:元素大于均值

  • z-score=0:元素等于均值

  • z-score=0.5:元素比均值大 0.5 个标准差

在 Julia 中,它是通过以下方式实现的:

julia> zscore(X,  μ, σ) 

μ和σ是可选的,因为它们可以通过函数计算。

理解 P 值的重要性

即使零假设被证明为真,拒绝零假设的概率即为 p 值。当两个测量之间没有差异时,假设被称为零假设。

例如,如果有一个假设,认为在足球比赛中,每个踢满 90 分钟的球员都会进球,那么零假设将是,比赛时间与进球数之间没有关系。

另一个例子是,假设 A 型血的人会比 B 型血的人有更高的血压。在零假设中,血型和血压之间没有差异,也就是说,二者之间没有关系。

显著性水平由(α)给出,如果 p 值小于或等于显著性水平,则声明零假设不一致或无效。此类假设将被拒绝。

单尾检验和双尾检验

以下图示表示了在假设检验中使用双尾检验。

单尾检验和双尾检验

在 Julia 中,它的计算方法如下:

julia> pvalue(test::HypothesisTest; tail=:both) 

这将返回双尾检验的 p 值。要获取单尾检验的 p 值,请使用tail=:lefttail=:right

总结

在本章中,我们深入探讨了推论统计学,学习了在 Julia 中处理不同数据集的各种概念和方法。我们从理解正态分布开始,这是处理统计数据时必须了解的内容。与此同时,我们开始探索 Distributions.jl 以及 Julia 提供的各种方法。接着我们研究了一元分布,了解它们为何如此重要。我们还探讨了一些其他分布,例如卡方分布、卡方检验和柯西分布。后来,我们研究了 z 分数、p 值、单尾检验和双尾检验的概念。在学习本章内容后,我们应该能够理解数据集,并应用推论统计学获取洞察力,同时使用 z 分数和 p 值来接受或拒绝我们的假设。

参考文献

第五章:通过可视化理解数据

Julia 的核心系统中没有可视化/图形包。因此,在没有添加和加载包的情况下,无法对数据集创建所需的可视化。

Julia 通过不包含可视化包,使核心系统保持干净,这样不同类型的后端(如不同操作系统上的 Qt 和 GTK)就不会干扰构建过程。

在这一章,我们将学习如何通过可视化数据,以及如何通过可视化帮助我们一眼理解数据。我们将涵盖以下包:

  • PyPlot

  • Unicodeplots

  • Vega

  • Gadfly

plot函数是绘制图形时常用的函数。当我们加载了多个绘图库时,哪个plot函数会被使用?

使用和importall的区别

假设我们想要扩展Foo包中的bar函数。当我们使用时,我们也需要包含包名:

julia> using Foo
julia> function Foo.bar(...)

但是当我们通过importall来实现时,不需要包含包名:

julia> importall Foo
julia> function bar(...)

当我们使用importall时,function bar(...)function Foo.bar(...)是等价的。

这可以防止我们不小心扩展了一个我们不想扩展或不知道的函数,同时避免可能破坏未来Foo实现的情况。

Pyplot for Julia

这个包由 Steven G. Johnson 制作,将 Python 著名的matplotlib库提供给 Julia。如果你已经使用过matplotlib,你会对其pyplot模块非常熟悉。

我们在第一章中学习了 Julia 的 Pycall 包,PyPlot 通过相同的包直接调用 matplotlib 绘图库。这种调用几乎没有(或者根本没有)开销,数组也直接传递而不需要复制。

多媒体 I/O

基本的 Julia 运行时只提供纯文本显示。通过加载外部模块或使用如Jupyter笔记本等图形环境,可以提供丰富的多媒体输出。Julia 有一个标准化的机制来显示丰富的多媒体输出(如图像、音频和视频)。这一机制由以下提供:

  • display(x)是 Julia 对象的最丰富多媒体显示方式

  • 任意的多媒体表示可以通过重载用户定义类型的writemime来实现

  • 通过子类化一个通用的显示类型,可以使用不同的多媒体支持的后端

PyPlot 利用 Julia 的多媒体 I/O API 进行绘图,支持任何 Julia 图形后端,包括 IJulia。

安装

使用matplotlib时,必须安装 Python 和 Matplotlib。推荐的方式是从任何科学 Python 的完整包中获取。

流行的工具有 Anaconda(由 Continuum analytics 提供)和 Canopy(由 Enthought 提供)。

你也可以使用 pip 安装matplotlib

$ pip install matplotlib

在安装matplotlib之前,你需要先安装必要的依赖项。

安装matplotlib成功后,我们可以在 Julia 中添加 Pyplot 包:

julia> Pkg.update()
julia> Pkg.add("PyPlot")

它会自动添加依赖项。我们将在示例中使用 IJulia 进行内联绘图。

基本绘图

现在我们已经将该包添加到系统中,可以开始使用它了。我们将在示例中使用 IJulia(jupyter notebook):

using PyPlot 
PyPlot.svg(true) 

第二行,Pyplot.svg(true),将允许我们获得生成的图表和可视化的 SVG。可缩放矢量图形SVG)是一种基于 XML 的标记语言,用于二维图形的矢量图像格式,支持互动性和动画:

x = [1:100]
y = [i² for i in x]
p = plot(x,y)
xlabel("X")
ylabel("Y")
title("Basic plot")
grid("on")

  • 第一行和第二行定义了我们要生成图表的xy的值。

  • 第三行,plot(x,y),实际上生成了图表。

  • 对于我们生成的图表,我们提供标签并改变美学设置。通过xlabelylabel,我们为x轴和y轴提供了标签。在接下来的章节中,我们将探索plot函数的其他选项。

基本绘图

它已生成一个指数图。

使用正弦和余弦绘图

在以下代码中,我们使用函数初始化xy

x = linspace(0, 3pi, 1000) 
y = cos(2*x + 3*sin(3*x)); 
plot(x, y, color="orange", linewidth=2.0, linestyle="--"); 
title("Another plot using sine and cosine"); 

让我们简要理解一下前面的代码:

  • plot函数中,我们传递了用于生成特定图表的参数。

  • 我们可以通过传递参数来更改线条的样式、宽度和颜色。

使用正弦和余弦绘图

在这里,我们可以看到线条样式与第一幅图的线条样式有很大不同。默认颜色是蓝色,但我们已指定该图表使用橙色线条。

Unicode 图表

Unicode 图表在我们需要在 REPL 中绘图时非常有用。它们极其轻量。

安装

没有依赖项,因此可以轻松安装:

Pkg.add("UnicodePlots") 
using UnicodePlots 

示例

让我们了解一下使用UnicodePlots轻松制作的基本图表。

生成 Unicode 散点图

散点图用于确定两个变量之间的相关性,也就是说,一个变量如何受另一个变量的影响:

生成 Unicode 散点图

生成 Unicode 线图

线性图以一系列数据点显示数据集:

生成 Unicode 线图

使用 Vega 进行可视化

Vega 是由 John Myles White 提供的美丽可视化库。它作为一个注册的 Julia 包提供,因此可以轻松安装。

它建立在 D3.js 之上,使用 JSON 来创建美观的可视化。每当我们需要生成图表时,它都需要互联网连接,因为它不会存储所需的 JavaScript 库的本地副本。

安装

要安装 Vega,请使用以下命令:

Pkg.add("Vega")
using Vega

示例

让我们通过 Vega 走一遍各种可视化示例。

散点图

以下是散点图的参数:

  • xy:AbstractVector

  • 组:AbstractVector

散点图用于确定两个变量之间的相关性,即一个如何受到另一个的影响:

scatterplot(x=rand(100), y=rand(100))

散点图

现在我们可以开始构建一个复杂的散点图:

散点图

这将生成以下散点图。我们可以清楚地看到 Vega 生成的两个聚类。这些是 d1d2

散点图

在这个特定的例子中,我们将数据分组并使用不同的颜色来可视化这些分组。

Vega 中的热图

Vega 中的热图易于生成。这帮助我们轻松地可视化数据点的密度。参数如下:

  • xy

  • 颜色

x = Array(Int, 900) 
y = Array(Int, 900) 
color = Array(Float64, 900) 
tmp = 0 
for counter in 1:30 
    for counter2 in 1:30 
        tmp += 1 
        x[tmp] = counter 
        y[tmp] = counter2 
        color[tmp] = rand() 
    end 
end 
hm = heatmap(x = x, y = y, color = color) 

Vega 中的热图

使用 Gadfly 进行数据可视化

Gadfly 是一个由 Daniel Jones 使用 Julia 编写的全面的绘图和数据可视化包。它基于 Leland Wilkinson 的书籍《图形语法》。它在很大程度上受到 R 语言中 ggplot2 包的启发,后者是另一个出色的绘图和可视化工具。

安装 Gadfly

安装过程很简单,因为它是一个注册的 Julia 包:

Julia> Pkg.update()
Julia> Pkg.add("Gadfly")

这也会安装 Gadfly 所需的其他一些包。

要使用 Gadfly,请运行以下命令:

Julia> using Gadfly

在我们的示例中,我们将使用 IJulia(jupyter notebook)。

Gadfly 具有渲染高质量图形和可视化的能力,支持 PNG、SVG、Postscript 和 PDF 格式。SVG 后端使用嵌入的 JavaScript,实现与图形的交互功能,如缩放、平移和切换。

安装 Cairo 会比较好,因为 PNG、PostScript 和 PDF 需要它:

Julia> Pkg.add("Cairo")

假设我们创建了一个 exampleplot。为了在后端绘制它,我们使用 draw 函数:

julia> exampleplot = plot(....)

  • 对于 SVG:
julia> draw(SVG("plotinFile.svg', 4inch, 4inch), exampleplot)

  • 对于嵌入 JavaScript 的 SVG:
julia> draw(SVGJS("plotinFile.svg', 4inch, 4inch), exampleplot)

  • 对于 PNG:
julia> draw(PNG("plotinFile.png', 4inch, 4inch), exampleplot)

  • 对于 PostScript:
julia> draw(PS("plotinFile.ps', 4inch, 4inch), exampleplot)

  • 对于 PDF:
julia> draw(PDF("plotinFile.pdf', 4inch, 4inch), exampleplot)

使用 plot 函数与 Gadfly 进行交互

plot 函数用于与 Gadfly 包进行交互并创建所需的可视化图形。美学元素映射到图形几何图形上,用于指定 plot 函数的工作方式。它们是特定命名的变量。

plot 元素可以是比例尺、坐标、引导线和几何图形。它在图形语法中定义,以避免特殊情况,而美学通过以明确定义的输入和输出方法处理问题,从而产生期望的结果。

Plot 可以处理以下数据源:

  • 函数和表达式

  • 数组和集合

  • 数据框

示例

如果我们没有定义 plot 元素,默认情况下会使用点几何图形。在点几何图形中,x 和 y 输入被视为美学元素。

让我们绘制一个散点图:

示例

在相同的美学中,我们可以使用多个元素来获得特定的输出。

例如,要在同一数据集上同时绘制线和点几何图形,我们可以使用以下方式创建分层图:

  • Geom.line:线图

  • Geom.point:点图

示例

这生成了一个包含线条和点的分层图。通过组合不同的元素,可以生成一个复杂的图。

Guide:

  • xlabelylabel:Guide 可用于为我们使用的图表提供必要的标签。

  • title:用于为图表提供标题

刻度:

  • 使用此功能可以放大或缩小图表的任何所需轴

让我们创建一个包含这些元素的类似图表。我们将添加 xy 标签,给图表添加标题,并缩放图表:

示例

图中的滑块可用于缩放。

使用 Gadfly 绘制 DataFrame

Gadfly 提供的对 DataFrame 的支持非常有用。在前几章中,我们学习了 DataFrame 的功能。它是一个强大的数据结构,用于表示和操作数据。

使用 Gadfly,我们可以轻松生成复杂的图表。DataFrame 作为第一个参数传递给绘图函数。

DataFrame 中的列通过名称或索引在美学中被绘图函数使用。我们将使用 RDatasets 来为绘图函数创建 DataFrame。RDatasets 提供了一些现实世界中的数据集,我们可以在这些数据集上进行可视化操作,以了解 Gadfly 包的功能:

Using Rdatasets, Gadfly 
plot(iris, x=:SepalLength, y=:SepalWidth, 
  color=:Species, shape=:Species, Geom.point, 
  Theme(default_point_size=3pt)) 

使用 Gadfly 绘制 DataFrame

这是一个非常著名的数据集——鸢尾花数据集——我们在前面的例子中也使用过。通过花萼长度和花萼宽度绘制数据集很容易,因为我们只需将它们作为 xy 坐标传递。

现在,我们使用随机数生成器创建一个直方图。我们将传递一个由随机数生成器创建的数组,然后生成直方图。

使用 Gadfly 绘制 DataFrame

这是一个相当简单的直方图。我们来使用 RDataset 中的数据集创建一个直方图:

使用 Gadfly 绘制 DataFrame

前面的数据集来自 RDatasets,我们创建了一个直方图,用于查看学生在课程中获得的成绩和性别。

这可以通过创建散点图来扩展:

使用 Gadfly 绘制 DataFrame

使用 Gadfly 可视化函数和表达式

在 Gadfly 中,绘制函数和表达式非常方便。

绘图函数的函数和表达式的签名如下:

plot(f::Function, a, b, elements::Element...)
plot(fs::Array, a, b, elements::Element...)

这表明我们可以将函数或表达式作为数组传递,数组中包含我们希望使用的元素。

使用 Gadfly 可视化函数和表达式

这是一个简单的正弦和余弦函数的图。让我们从一个复杂的表达式创建一个图表:

使用 Gadfly 可视化函数和表达式

这是我们尝试绘制的一个随机表达式。你可以看到,绘制一个稍微复杂的表达式非常简单。即使复杂性增加,Gadfly 也表现得很好。

生成多层图像

Gadfly 能够将多个图层绘制到同一图表上:

生成多层图像

使用统计学生成具有不同美学的图表

Gadfly 中的统计函数通过接受一个或多个美学作为输入,并输出一个或多个美学。

让我们逐一探讨它们。

步骤函数

这用于在给定的点之间进行逐步插值。通过该函数在两个点之间引入一个新点,插值的方向取决于参数的方向:

  • xy 点是使用的美学元素

  • :vh 用于垂直方向,:hv 用于水平方向

步骤函数

分位数-分位数函数

这用于生成分位数-分位数图。将两个数值向量传递给函数,并进行它们分位数的比较。

传递给函数的 xy 是分布或数值向量:

分位数-分位数函数

Gadfly 中的刻度线

刻度线用于包围坐标轴之间的数据

有两种类型的刻度线:xticksyticks

刻度线函数的参数包括:

  • ticks:特定的刻度线数组(当没有刻度线时,会计算出它们)

  • granularity_weight:刻度线的数量(默认值为 1/4)

  • simplicity_weight:包括零(默认值为 1/6)

  • coverage_weight:紧密适配数据的跨度(默认值为 1/3)

  • niceness_weight:编号(默认值为 1/4)

Gadfly 中的刻度线

使用几何学生成具有不同美学的图表

几何学负责实际的绘制。一个或多个输入(美学)传递给函数。

箱型图

这也称为胡须图;这是一种基于四分位数显示数据的标准方法:

  • 第一和第三四分位数由盒子的底部和顶部表示

  • 盒子内的带状区域是第二四分位数(中位数)

  • 盒子外的带状区域表示最小值和最大值

直接使用的美学包括:

  • x

  • middle

  • lower_hingeupper_hinge

  • lower_fenceupper_fence

  • outliers

仅需要传递绘制箱型图的数据集。

箱型图

使用几何学创建密度图

通过密度图可以有效地查看变量的分布:

使用几何学创建密度图

上面的截图显示了特定范围内变量的密度。

使用几何学创建直方图

直方图有助于理解分布的形态。它将数字分组为范围。

美学包括:

  • x:用于绘制直方图的数据集

  • color(可选):不同的类别可以通过颜色进行分组

参数包括:

  • position:有两个选项,:stack:dodge。这定义了条形图是并排放置还是堆叠在一起。

  • density:可选项

  • orientation:水平或垂直

  • bincount:箱数

  • maxbincountminbincount: 当自动选择箱数时的上限和下限。

使用几何方法创建直方图

条形图

这些由平行条形图组成,用于图形化频率分布。

Aesthetics are:

  • y: 这是必需的。它是每个条形的高度。

  • 颜色: 可选。用于将数据集分类。

  • x: 每个条形图的位置。

xminxmax 也可以代替 x,它们是每个条形图的起始和结束。

Arguments are:

  • position: 可以是 :stack:dodge

  • orientation: 可以是 :vertical:horizontal

如果选择 :horizontal,则需要提供 y 作为美学(或 ymin/ymax)。

条形图

二维直方图 - Histogram2d

用于创建类似热图的直方图,其中矩形条形代表密度。

Aesthetics are:

  • xy: 要绘制在坐标上的数据集

Arguments are:

  • xbincount: 指定 x 坐标的箱数

    当自动确定箱数时提供 xminbincountxmaxbincount

  • ybincount: 指定 y 坐标的箱数

    当自动确定箱数时提供 yminbincountymaxbincount

二维直方图 - Histogram2d

平滑线图

我们之前处理过线图的示例。我们还可以创建平滑线图,从数据中估计函数。

Aesthetics are:

  • x: 预测数据

  • y: 响应(函数)数据

  • 颜色: 可作为可选参数用于分类数据集

Arguments are:

  • smoothing: 指定平滑的程度

    较小的值使用更多数据(更拟合),较大的值使用较少数据(拟合度较低)。

  • Method: 支持 :lm:loess 方法作为生成平滑曲线的参数

平滑线图

子图网格

可以将多个图一起作为网格制作,并根据几个分类向量进行组织:

Julia> Geom.subplot_grid(elements::Gadfly.ElementOrFunction...)

Aesthetics are:

  • xgroupygroup(可选):用于基于分类数据在 x 轴或 y 轴上排列子图。

  • free_y_axisfree_x_axis(可选):默认情况下,值为 false,这意味着在不同子图之间 y 轴或 x 轴的比例尺可以不同。

    如果值为 true,则为每个图设置比例尺。

  • 如果 xgroupygroup 都绑定,则形成一个网格。

子图网格

使用固定比例尺:

子图网格

水平和垂直线

使用 hlinevline,我们可以在画布上绘制水平和垂直线。

Aesthetics are:

  • xintercept: x 轴截距

  • yintercept: y 轴截距

Arguments are:

  • 颜色: 生成线条的颜色

  • size: 我们还可以指定线条的宽度

水平和垂直线

绘制带状图

我们还可以在折线图上绘制带状图。

美学为:

  • xx

  • yminymaxy 轴的下限和上限

  • color(可选):使用不同颜色将数据分组

示例:

xs = 0:0.1:20 
df_cos = DataFrame( 
    x=xs, 
    y=cos(xs), 
    ymin=cos(xs) .- 0.5, 
    ymax=cos(xs) .+ 0.5, 
    f="cos" 
    ) 
df_sin = DataFrame( 
    x=xs, 
    y=sin(xs), 
    ymin=sin(xs) .- 0.5, 
    ymax=sin(xs) .+ 0.5, 
    f="sin" 
    ) 
df = vcat(df_cos, df_sin) 
p = plot(df, x=:x, y=:y, ymin=:ymin, ymax=:ymax, color=:f, Geom.line, Geom.ribbon) 

绘制带状图

小提琴图

小提琴图非常特定于使用场景。它们用于显示密度。

美学为:

  • xyx 轴和 y 轴上的位置

  • width:这表示根据 y 值的密度

小提琴图

蜂群图

就像小提琴图一样,我们可以使用 beeswarm 图表示密度。

美学为:

  • xyx 轴和 y 轴的数据集

  • color(可选)

参数为:

  • orientation:这可以是 :vertical:horizontal

  • padding:两点之间的最小距离

蜂群图

元素 - 比例

这用于转换原始数据,同时保留原始值。它将一个美学映射到相同的美学。

x_continuous 和 y_continuous

这些用于将值映射到 xy 坐标。

美学为:

  • xxmin/xmaxxintercept

  • yymin/ymaxyintercept

参数为:

  • minvalue:最小 xy

  • maxvalue:最大 xy

  • labels:这可以是一个函数或不设置

    当传入一个函数时,字符串将映射到 xy 中的值

  • format:数字格式化

变体为:

  • Scale.x_continuousScale.y_continuous

  • Scale.x_log10Scale.ylog10

  • Scale.x_log2Scale.ylog2

  • Scale.x_logScale.y_log

  • Scale.x_asinhScale.y_asinh

  • Scale.x_sqrtScale.y_sqrt

x_continuous 和 y_continuous

x_discrete 和 y_discrete

这些用于将分类数据映射到笛卡尔坐标系。不论值如何,每个值都映射到一个点。

美学为:

  • xxmin/xmaxxintercept

  • yymin/ymaxyintercept

参数为:

  • labels:这可以是一个函数或不设置

    当传入一个函数时,字符串将映射到 xy 中的值:

    x_discrete 和 y_discrete

连续颜色比例

这创建一个使用连续颜色比例的图表。它用于表示密度。

美学为:

  • color

参数为:

  • f:定义的返回颜色的函数

  • minvaluesmaxvalue:颜色比例值的范围

连续颜色比例

元素 - 引导

这些提供了特别的布局考虑,有助于我们更好地理解数据。它们包含 xticksyticksxlabelsylabels、标题和注释等内容。

理解 Gadfly 如何工作

本章我们已经介绍了各种图表。现在简要介绍一下 Gadfly 实际如何工作。

首先,数据源的子集被映射到图表中每一层的数据对象:

  • 我们将不同的尺度传递给绘图函数,用来获取可绘制的美学效果。

  • 进行了美学转换,既在图层上也在图形上进行处理。

  • 创建一个 Compose 上下文,通过使用所有图层的美学,将数据适配到屏幕坐标。

  • 每一层的几何图形都单独渲染。

  • 最后,计算并在图形上方渲染了引导。

总结

在这一章节中,我们学习了如何使用不同的图形选项在 Julia 中进行可视化。

我们研究了 PyPlot 以及如何利用强大的 matplotlib 库,做了多个示例。我们还了解了 Unicode 图形,它们非常轻量,可以在终端中使用。本章节还解释了两大最受欢迎的图形库 Vega 和 Gadfly。通过使用散点图、折线图、箱线图、直方图、条形图和小提琴图等不同的图形,我们理解了可视化数据的重要性和帮助。

在下一章,我们将学习 Julia 中的机器学习。

参考资料

第六章 监督机器学习

人们通常认为数据科学就是机器学习,这意味着在数据科学中,我们只是训练机器学习模型。但数据科学远不止于此。数据科学涉及理解数据、收集数据、整理数据、从中获取意义,然后如果需要进行机器学习。

在我看来,机器学习是当今存在的最令人兴奋的领域。随着大量可用数据的出现,我们可以收集到宝贵的知识。许多公司已经使他们的机器学习库变得可访问,还有很多开源替代品存在。

在本章中,你将学习以下主题:

  • 什么是机器学习?

  • 机器学习的类型

  • 过拟合和欠拟合是什么?

  • 偏差-方差权衡

  • 特征提取和选择

  • 决策树

  • 朴素贝叶斯分类器

什么是机器学习?

一般来说,当我们谈论机器学习时,我们会涉及到与我们创建但失控的智能机器争斗的想法。这些机器能够智胜人类,并对人类生存构成威胁。这些理论只是为了我们的娱乐而创造的。我们离这样的机器还非常遥远。

所以问题是:什么是机器学习?Tom M. Mitchell 给出了一个正式的定义:

"如果一个计算机程序在任务 T 的表现,以性能度量 P 来衡量,随着经验 E 的增加而提高,那么它就被认为是从经验 E 中学习。"

这意味着机器学习是教计算机使用数据生成算法,而不是明确编程它们。它将数据转化为可操作的知识。机器学习与统计学、概率论和数学优化密切相关。

随着技术的发展,有一样东西呈指数增长——数据。我们有大量的结构化和非结构化数据,以非常快的速度增长。太空观测站、气象学家、生物学家、健身传感器、调查等产生了大量数据。手动处理这么多数据并找出模式或洞察力是不可能的。这些数据对科学家、领域专家、政府、卫生官员甚至企业都非常重要。为了从这些数据中获取知识,我们需要自学习算法来帮助我们做决策。

机器学习作为人工智能的一个子领域发展,消除了手动分析大量数据的需要。我们通过使用自学习预测模型来进行数据驱动决策。机器学习已经成为我们日常生活中的重要组成部分。一些常见的应用包括搜索引擎、游戏、垃圾邮件过滤器和图像识别。自动驾驶汽车也使用机器学习。

机器学习中使用的一些基本术语包括:

  • 特征:数据点或记录的独特特征。

  • 训练集:这是我们输入算法进行训练的数据集,帮助我们发现关系或构建模型。

  • 测试集:使用训练数据集生成的算法会在测试数据集上进行测试,以查找准确度。

  • 特征向量:包含定义对象特征的 n 维向量。

  • 样本:数据集中的一个项目或记录。

机器学习的应用

机器学习在某种程度上无处不在,它的应用几乎没有边界。我们来讨论一些非常常见的使用案例:

  • 电子邮件垃圾邮件过滤:每个主要的电子邮件服务提供商都使用机器学习来将垃圾邮件从收件箱过滤到垃圾邮件文件夹。

  • 预测风暴和自然灾害:气象学家和地质学家利用机器学习,通过天气数据预测自然灾害,这有助于我们采取预防措施。

  • 定向促销/活动和广告:在社交网站、搜索引擎,甚至可能在邮箱中,我们看到的广告总是某种程度上符合我们的口味。这是通过对我们过去搜索记录、社交资料或电子邮件内容的数据进行机器学习来实现的。

  • 自动驾驶汽车:科技巨头目前正致力于自动驾驶汽车。这是通过对实际驾驶数据、人类驾驶员的图像和声音处理以及其他各种因素进行机器学习实现的。

  • 机器学习也被企业用来预测市场。

  • 它还可以用来预测选举结果和选民对特定候选人的情感。

  • 机器学习也被用来预防犯罪。通过理解不同罪犯的模式,我们可以预测未来可能发生的犯罪,并加以防范。

一个广受关注的案例是,美国一家大型零售商利用机器学习来识别孕妇。该零售商想出了通过在多种孕妇用品上提供折扣来吸引女性顾客,让她们成为忠实客户,并购买高利润的婴儿用品。

该零售商通过分析购买不同孕妇用品的模式,研究出了预测怀孕的算法。

曾经有一个人走到零售商面前,询问为什么他的青少年女儿会收到孕妇用品的折扣券。零售商道歉了,但后来父亲自己也道歉了,因为他了解到女儿确实怀孕了。

这个故事可能完全真实,也可能并非完全真实,但零售商确实定期分析客户数据,以发现用于定向促销、活动和库存管理的模式。

机器学习与伦理

让我们看看机器学习在哪些领域被广泛应用:

  • 零售商:在之前的例子中,我们提到零售商如何使用数据来进行机器学习,从而增加收入并留住客户

  • 垃圾邮件过滤:电子邮件通过各种机器学习算法进行垃圾邮件过滤

  • 定向广告:在我们的邮箱、社交网站或搜索引擎中,我们会看到自己喜欢的广告

这些仅是现实世界中实施的部分实际用例。它们之间的共同点是用户数据。

在第一个例子中,零售商利用用户的交易历史进行定向推广和活动策划,以及库存管理等其他工作。零售巨头通过为用户提供忠诚卡或注册卡来实现这一点。

在第二个例子中,电子邮件服务提供商使用经过训练的机器学习算法来检测和标记垃圾邮件。它通过检查电子邮件内容/附件,并对电子邮件发送者进行分类来实现这一点。

在第三个例子中,同样是电子邮件提供商、社交网络或搜索引擎通过我们的 Cookies、个人资料或邮件来进行定向广告。

在所有这些例子中,当我们与零售商、电子邮件提供商或社交网络签约时,协议的条款和条件中都会提到将使用用户数据,但不会侵犯隐私。

在使用未公开的数据之前,我们必须获得必要的许可。这非常重要。此外,我们的机器学习模型不应在地域、种族、性别或任何其他方面存在歧视。提供的数据不应用于协议中未提及的目的,或在所在地区或国家是非法的。

机器学习 – 过程

机器学习算法的训练是根据人类大脑工作的方式进行的。它们有些相似。让我们来讨论整个过程。

机器学习过程可以分为三个步骤:

  1. 输入

  2. 抽象

  3. 泛化

这三个步骤是机器学习算法工作的核心。尽管算法的表现形式可能不同,但这解释了整体方法:

  1. 第一步集中在应该包含哪些数据以及不应包含哪些数据。根据这一点,它根据需求收集、存储并清理数据。

  2. 第二步涉及将数据转换为代表更大类的数据。这是必要的,因为我们无法捕捉到所有数据,且我们的算法不应该仅适用于我们拥有的数据。

  3. 第三步关注于创建模型或行动,这些模型或行动将使用这些抽象的数据,并适用于更广泛的群体。

那么,接近一个机器学习问题的流程应该是什么样的呢?

机器学习 – 过程

在这个特定的图示中,我们看到数据在用于创建机器学习算法之前,经过了抽象处理过程。这个过程本身是繁琐的。我们在与数据清洗相关的章节中学习了这个过程。

该过程紧随模型训练之后,模型的训练就是将模型拟合到我们拥有的数据集上。计算机并不会自主选择模型,而是依赖于学习任务。学习任务还包括将从我们尚未拥有的数据中获得的知识进行泛化。

因此,训练模型是基于我们当前拥有的数据,而学习任务包括将模型泛化到未来的数据。

它取决于我们的模型如何从我们当前拥有的数据集中推断知识。我们需要创建一个能够从之前不为我们所知的东西中汲取洞察的模型,这样它就能对未来数据产生有用的联系。

不同类型的机器学习

机器学习主要分为三类:

  • 监督学习

  • 无监督学习

  • 强化学习

在监督学习中,模型/机器会接收输入以及与这些输入对应的输出。机器从这些输入中学习,并将这种学习应用于进一步的未见数据,以生成输出。

无监督学习没有所需的输出,因此由机器来学习并寻找之前未见的模式。

在强化学习中,机器与环境持续互动并通过这一过程学习。这包括一个反馈循环。

什么是偏差-方差权衡?

让我们理解一下什么是偏差和方差。首先,我们将讨论模型中的偏差:

  • 偏差是模型生成的预测结果与预期的正确值之间的差异,或者说我们应该获得的值。

  • 当我们获得新数据时,模型会进行计算并给出预测。因此,这意味着我们的模型有一个可以生成预测的范围。

  • 偏差是这一预测范围的准确性。

现在,让我们理解方差以及它如何影响模型:

  • 方差是当数据点发生变化或引入新数据时,模型的变异性

  • 不应该在每次引入新数据时都需要调整模型

根据我们对偏差和方差的理解,我们可以得出结论,它们相互影响。因此,在创建模型时,我们会考虑这一权衡。

过拟合和欠拟合对模型的影响

过拟合发生在我们创建的模型开始考虑数据集中的异常值或噪声时。因此,这意味着我们的模型过度拟合了数据集。

这种模型的缺点是无法很好地进行泛化。这类模型具有低偏差和高方差。

欠拟合发生在我们创建的模型未能找到数据的模式或趋势时。因此,这意味着模型未能很好地适应数据集。

这种模型的缺点是无法给出良好的预测。这些模型具有高偏差和低方差。

我们应该尽量减少欠拟合和过拟合。这可以通过各种技术来实现。集成模型在避免欠拟合和过拟合方面非常有效。我们将在接下来的章节中学习集成模型。

理解决策树

决策树是“分治法”的一个很好的例子。它是最实用、最广泛使用的归纳推理方法之一。它是一种监督学习方法,可用于分类和回归。它是非参数的,目的是通过推断数据中的简单决策规则来学习,并创建一个能够预测目标变量值的模型。

在做出决策之前,我们会通过权衡不同选项来分析利弊。例如,我们想购买一部手机,并且有多个价格区间的选择。每款手机都有某些特别好的功能,可能比其他手机更好。为了做出选择,我们从考虑我们最重要的特征开始。基于此,我们创建了一系列需要满足的特征,最终选择将是最符合这些特征的那一款。

本节我们将学习:

  • 决策树

  • 熵度量

  • 随机森林

我们还将学习著名的决策树学习算法,如 ID3 和 C5.0。

构建决策树 - 分治法

一种称为递归划分的启发式方法用于构建决策树。在这种方法中,随着推进,我们将数据划分为越来越小的相似类别。

决策树实际上是一个倒置的树。它从根节点开始,最终到达叶节点,这些叶节点是终端节点。节点的分支依据逻辑决策。整个数据集在根节点处表示。算法会选择一个对目标类别最具预测性的特征。然后,它根据这个特征将样本划分为不同的值组。这代表了我们树的第一组分支。

采用分治法,直到达到终点。在每一步,算法会继续选择最佳的候选特征。

当以下条件满足时,定义终点:

  • 在某个节点,几乎所有的样本都属于同一类别

  • 特征列表已用尽

  • 达到预定义的树大小限制

构建决策树 - 分治法

上述图像是一个非常著名的决策树示例。在这里,决策树是用来判断是否外出:

  • Outlook 是根节点。这指的是环境中所有可能的类别。

  • Sunny、overcast 和 Rain 是分支。

  • 湿度和风速是叶节点,这些节点又被分为分支,决策是根据有利的环境做出的。

这些树也可以重新表示为 if-then 规则,这样会更容易理解。决策树是非常成功且受欢迎的算法之一,应用范围广泛。

以下是决策树的一些应用:

  • 信用卡/贷款批准决策:信用评分模型基于决策树,每个申请人的信息被输入,以决定是否批准信用卡/贷款。

  • 医学诊断:许多疾病通过基于症状、测量和检测的经过充分定义和测试的决策树进行诊断。

我们应该在什么情况下使用决策树学习?

虽然有多种决策树学习方法可以用于各种问题,但决策树最适合以下场景:

  • 属性-值对是指通过固定集的属性和相应的值来描述实例的场景。在前面的例子中,我们有属性“风速”和值“强”和“弱”。这些互斥的可能值使得决策树学习变得容易,尽管也可以使用具有实数值的属性。

  • 目标函数的最终输出是离散值,类似于前面的例子,其中我们有“是”或“否”。决策树算法可以扩展为具有多个可能的目标值。决策树也可以扩展为具有实数值作为输出,但这种情况很少使用。

  • 决策树算法对于训练数据集中的错误具有鲁棒性。这些错误可能出现在示例的属性值、分类或者两者都有。

  • 决策树学习也适用于数据集中缺失值的情况。如果某些示例中的值缺失,而其他示例中的相同属性有值,那么可以使用决策树。

决策树的优点

  • 决策树容易理解和解释,决策树的可视化也很简单。

  • 在其他算法中,必须先进行数据归一化才能应用。归一化是指创建虚拟变量并去除空值。而决策树则需要的准备工作较少。

  • 使用决策树进行预测的成本与训练树所使用的示例数量呈对数关系。

  • 与其他算法不同,决策树可以同时应用于数值型和类别型数据。其他算法通常专门用于处理其中一种类型的变量。

  • 决策树可以轻松处理可能有多个输出的情况。

  • 决策树遵循白盒模型,这意味着如果情况在模型中是可观察的,使用布尔逻辑可以轻松解释条件。另一方面,在黑盒模型(如人工神经网络)中,结果相对难以解释。

  • 统计测试可以用来验证模型。因此,我们可以测试模型的可靠性。

  • 即使数据源的真实模型假设被违反,它也能表现良好。

决策树的缺点

我们已经介绍了决策树适用的情况及其优势。现在我们将讨论决策树的缺点:

  • 决策树存在过拟合数据的可能性。这通常发生在创建过于复杂且难以泛化的树时。

  • 为了避免这种情况,可以采取多种步骤。其中一种方法是修剪。顾名思义,这是一种方法,我们在其中设置树可以生长到的最大深度。

  • 决策树总是存在不稳定性的问题,因为数据的微小变化可能导致生成完全不同的树。

  • 这种场景的解决方案是集成学习,在下一章中我们将学习它。

  • 决策树学习有时可能导致创建偏向某些类的树,而忽视其他类的情况。在将数据拟合到决策树算法之前,解决这种情况的方法是平衡数据集。

  • 决策树学习被认为是 NP 完全的,考虑到优化的几个方面。即使对于基本概念也是如此。

  • 通常使用贪婪算法等启发式算法,在每个节点都做出局部最优决策。这并不保证我们将得到一个全局最优的决策树。

  • 对于诸如奇偶性、XOR 和多路复用器问题等概念,学习可能会很困难,决策树无法轻松表示它们。

决策树学习算法

有多种决策树学习算法,实际上是核心算法的变体。核心算法实际上是一种自顶向下的、贪婪的搜索所有可能树的方法。

我们将讨论两种算法:

  • ID3

  • C4.5 和 C5.0

第一个算法,ID3迭代二分器 3),是由 Ross Quinlan 在 1986 年开发的。该算法通过创建一个多路树来进行,它使用贪婪搜索找到每个节点和可以产生最大信息增益的特征,由于树可以增长到最大尺寸,这可能导致数据过拟合,因此使用修剪来创建泛化模型。

C4.5 是在 ID3 之后发展起来的,消除了所有特征必须是分类变量的限制。它通过基于数值变量动态定义离散特征来实现这一点。它将连续的属性值划分为一组离散区间。C4.5 从 ID3 算法的训练树中创建 if-then 规则集合。C5.0 是最新版本;它创建了更小的规则集,并且使用相对较少的内存。

决策树算法如何工作

决策树算法构建自顶向下的树。它遵循以下步骤:

  1. 为了知道哪一个元素应该出现在树的根节点,算法会对每个属性实例进行统计测试,以确定仅使用该属性时训练示例能够被多好地分类。

  2. 这导致在树的根节点选择最佳特征。

  3. 现在,在这个根节点上,对于每个属性的可能值,都创建后代节点。

  4. 我们训练数据集中的示例会被分配到每一个这些后代节点。

  5. 对于这些单独的后代节点,之前的所有步骤会针对训练数据集中剩余的示例重复进行。

  6. 这会通过贪婪搜索创建一个可接受的训练数据集决策树。算法永不回溯,这意味着它永远不会重新考虑之前的选择,而是继续向树的下方发展。

理解与衡量节点的纯度

决策树是自顶向下构建的。每个节点选择分裂的属性可能会很困难。因此,我们寻找能够最好地分裂目标类别的特征。纯度是指一个节点只包含一个类别的度量。

C5.0 中的纯度是通过熵来衡量的。样本的熵指示了类别值在示例之间的混合程度:

  • 0: 最小值表示样本中类别值的同质性

  • 1: 最大值表示样本中类别值的最大无序程度

熵的计算公式为:

理解与衡量节点的纯度

在前面的公式中,S表示我们拥有的数据集,c表示类别水平。对于给定类别ip是该类别值的比例。

当纯度度量确定后,算法必须决定数据应该根据哪个特征进行分裂。为了决定这一点,算法使用熵度量来计算在每个可能的特征上分裂时,同质性如何变化。算法进行的这种计算叫做信息增益:

理解与衡量节点的纯度

数据集分割前的熵(S1)与分割后得到的子集熵(S2)之间的差异叫做信息增益(F)。

一个示例

让我们将所学的知识应用于使用 Julia 创建决策树。我们将使用 scikit-learn.org/ 上为 Python 提供的示例和 Cedric St-Jean 开发的 Scikitlearn.jl。

我们首先需要添加所需的包:

julia> Pkg.update() 
julia> Pkg.add("DecisionTree") 
julia> Pkg.add("ScikitLearn") 
julia> Pkg.add("PyPlot") 

ScikitLearn 提供了一个接口,将著名的机器学习库从 Python 转换到 Julia:

julia> using ScikitLearn 
julia> using DecisionTree 
julia> using PyPlot 

在添加所需包之后,我们将创建我们将在示例中使用的数据集:

julia> # Create a random dataset 
julia> srand(100) 
julia> X = sort(5 * rand(80)) 
julia> XX = reshape(X, 80, 1) 
julia> y = sin(X) 
julia> y[1:5:end] += 3 * (0.5 - rand(16)) 

这将生成一个包含 16 个元素的 Array{Float64,1}

现在我们将创建两个不同模型的实例。一个模型不限制树的深度,另一个模型则根据纯度修剪决策树:

示例

现在我们将对现有的数据集拟合模型。我们将拟合这两个模型。

示例

这是第一个模型。这里我们的决策树有 25 个叶节点,深度为 8

示例

这是第二个模型。这里我们修剪了决策树。这个模型有 6 个叶节点,深度为 4

现在我们将使用模型在测试数据集上进行预测:

julia> # Predict 
julia> X_test = 0:0.01:5.0 
julia> y_1 = predict(regr_1, hcat(X_test)) 
julia> y_2 = predict(regr_2, hcat(X_test)) 

这将创建一个包含 501 个元素的 Array{Float64,1}

为了更好地理解结果,让我们将这两个模型在我们拥有的数据集上进行可视化:

julia> # Plot the results 
julia> scatter(X, y, c="k", label="data") 
julia> plot(X_test, y_1, c="g", label="no pruning", linewidth=2) 
julia> plot(X_test, y_2, c="r", label="pruning_purity_threshold=0.05", linewidth=2) 

julia> xlabel("data") 
julia> ylabel("target") 
julia> title("Decision Tree Regression") 
julia> legend(prop=Dict("size"=>10)) 

决策树可能会过拟合数据。为了使其更加通用,必须修剪决策树。但如果修剪过度,可能会导致模型不正确。因此,必须找到最优化的修剪级别。

示例

很明显,第一个决策树对我们的数据集过拟合,而第二个决策树模型则相对更加通用。

使用朴素贝叶斯的有监督学习

朴素贝叶斯是目前最著名的机器学习算法之一,广泛应用于文本分类技术。

朴素贝叶斯方法属于有监督学习算法。它是一种概率分类器,基于贝叶斯定理。它假设每对特征彼此独立,这一假设被称为“朴素”假设。

尽管做出这些假设,朴素贝叶斯分类器仍然表现得非常好。它们最著名的应用是垃圾邮件过滤。该算法的有效性体现在它对训练数据的需求非常小,能够估计出所需的参数。

与其他方法相比,这些分类器和学习器的速度相当快。

使用朴素贝叶斯的有监督学习

在这个公式中:

  • AB 是事件。

  • P(A)P(B) 分别是 AB 的概率。

  • 这些是先验概率,它们彼此独立。

  • P(A | B) 是在条件 B 为真的情况下,A 的概率。它是给定预测变量(B,属性)时,类别(A,目标)的后验概率。

  • P(B | A) 是在 A 为真时,B 的概率。它是预测给定类别的可能性,也就是预测器给定类别的概率。

朴素贝叶斯的优点

以下是朴素贝叶斯的一些优点:

  • 它相对简单,易于构建和理解

  • 它可以很容易地训练,并且不需要庞大的数据集

  • 它相对较快

  • 不受无关特征的影响

朴素贝叶斯的缺点

朴素贝叶斯的缺点是“朴素”假设,即每个特征都是独立的。这并非总是正确的。

朴素贝叶斯分类的用途

以下是朴素贝叶斯分类的一些应用:

  • 朴素贝叶斯文本分类:这是一种概率学习方法,实际上是最成功的文档分类算法之一。

  • 垃圾邮件过滤:这是朴素贝叶斯最知名的应用场景。朴素贝叶斯用于区分垃圾邮件和合法邮件。许多服务器端的邮件过滤机制与其他算法一起使用朴素贝叶斯。

  • 推荐系统:朴素贝叶斯也可用于构建推荐系统。推荐系统用于预测并建议用户可能在未来喜欢的产品。它基于未见过的数据,并与协同过滤结合使用。这种方法更具可扩展性,通常比其他算法表现更好。

要理解朴素贝叶斯分类器如何实际工作,我们需要理解贝叶斯定理。它由托马斯·贝叶斯在 18 世纪提出。他发展了各种数学原理,这些原理今天被我们称为贝叶斯方法。这些方法有效地描述了事件的概率,以及当我们获得额外信息时,如何修正这些概率。

基于贝叶斯方法的分类器使用训练数据集,根据所有特征的值计算每个类别的观测概率。因此,当该分类器用于未标记或未见过的数据时,它会利用观测到的概率来预测新特征属于哪个类别。尽管这是一种非常简单的算法,但其性能可与大多数其他算法相媲美,甚至更好。

贝叶斯分类器最适用于以下情况:

  • 包含大量属性的数据集,在计算结果的概率时需要同时考虑所有这些属性。

  • 对于影响较弱的特征,通常会被忽略,但贝叶斯分类器仍然会使用它们来生成预测。许多此类弱特征可能会导致决策的重大变化。

贝叶斯方法的工作原理

贝叶斯方法依赖于一个概念,即事件发生的可能性估计是基于现有证据的。事件的可能结果就是事件本身;例如,在抛硬币时,我们可能得到正面或反面。同样,邮件可能是“正常”邮件或“垃圾”邮件。试验是指发生事件的单次机会。在我们之前的例子中,抛硬币就是试验。

后验概率

后验概率 = 条件概率 * 先验概率 / 证据

在分类中,后验概率指的是在给定观察到的特征值时,一个特定对象属于某个类 x 的概率。例如,“给定温度和湿度,概率它会下雨是多少?”

P(rain | xi), xi = [45 度, 95%湿度]

  • xi为样本i的特征向量,其中i属于{1,2,3,...n}

  • wj为类j的符号,其中j属于{1,2,3,...n}

  • P(xi | wi) 是观察样本xi的概率,前提是它属于类wj

后验概率的通用表示法是:

P(wj* | xi) = P(xi | wj) * P(wj)/P(xi)*

Naïve Bayes 的主要目标是最大化给定训练数据的后验概率,以便形成一个决策规则。

类条件概率

贝叶斯分类器假设数据集中的所有样本是独立同分布的。这里的独立性意味着一个观察的概率不受另一个观察概率的影响。

我们讨论过的一个非常著名的例子是抛硬币。在这里,第一次抛硬币的结果不会影响后续的抛硬币结果。对于一个公平的硬币,得到正面或反面的概率始终为 0.5。

一个额外的假设是特征具有条件独立性。这是另一个“天真”的假设,意味着可以直接从训练数据中估计似然或类条件概率,而无需评估所有 x 的概率。

让我们通过一个例子来理解。假设我们必须创建一个服务器端的电子邮件过滤应用程序,以决定邮件是否是垃圾邮件。假设我们有大约 1000 封电子邮件,其中 100 封是垃圾邮件。

现在,我们收到了一封新邮件,内容是“Hello Friend”。那么,我们应该如何计算这封新邮件的类条件概率呢?

文本的模式由两个特征组成:“hello”和“friend”。现在,我们将计算新邮件的类条件概率。

类条件概率是当邮件是垃圾邮件时遇到“hello”的概率 * 当邮件是垃圾邮件时遇到“friend”的概率:

P(X=[hello, world] | w=spam) = P(hello | spam) * P(friend | spam)

我们可以轻松找出包含“hello”一词的邮件数量,以及包含“spam”一词的邮件数量。然而,我们做出了一个“天真”的假设,即一个单词不会影响另一个单词的出现。我们知道,“hello”和“friend”经常一起出现。因此,我们的假设被违反了。

先验概率

先验概率是关于事件发生的先验知识。它是特定类别发生的总概率。如果先验分布为均匀分布,则后验概率是通过类别条件概率和证据项来确定的。

先验知识是通过对训练数据的估计获得的,当训练数据是整个群体的样本时。

证据

计算后验概率还需要一个值,那就是“证据”。证据 P(x)是特定模式 x 发生的概率,它与类标签无关。

词袋模型

在前面的例子中,我们进行的是电子邮件的分类。为此,我们对一个模式进行分类。要对一个模式进行分类,最重要的任务是:

  • 特征提取

  • 特征选择

那么,如何识别好的特征呢?好的特征有一些特征:

  • 特征必须对我们为其构建分类器的用例有重要意义

  • 选择的特征应该包含足够的信息,能够很好地区分不同的模式,并可以用于训练分类器。

  • 特征不应容易受到失真或缩放的影响

我们需要先将电子邮件文本表示为特征向量,然后才能将其适配到我们的模型并应用机器学习算法。文本文件的分类使用的是词袋模型。在这个模型中,我们创建词汇表,这是一个包含所有电子邮件(训练集)中出现的不同单词的集合,然后统计每个单词出现的次数。

使用朴素贝叶斯作为垃圾邮件过滤器的优点

以下是使用朴素贝叶斯作为垃圾邮件过滤器的优点:

  • 它可以个性化。这意味着它可以基于每个用户进行训练。我们有时会订阅新闻简报、邮件列表或关于产品的更新,这些对于其他用户来说可能是垃圾邮件。此外,我收到的邮件中包含一些与我的工作相关的词汇,这些对其他用户来说可能被归类为垃圾邮件。所以,作为一个合法用户,我不希望我的邮件进入垃圾邮件箱。我们可以尝试使用规则或过滤器,但贝叶斯垃圾邮件过滤比这些机制更为优秀。

  • 贝叶斯垃圾邮件过滤器在避免误报方面非常有效,因此合法邮件被分类为垃圾邮件的可能性非常小。例如,我们都会收到包含“尼日利亚”一词或声称来自尼日利亚的邮件,这些邮件实际上是钓鱼诈骗。但是,我可能在那儿有亲戚或朋友,或者我在那里有生意;因此,这封邮件对我来说可能并不不合法。

朴素贝叶斯过滤器的缺点

贝叶斯过滤器容易受到贝叶斯中毒的影响,这是一种通过将大量合法文本与垃圾邮件一起发送的技术。因此,贝叶斯过滤器在此处失败,并将其标记为“ham”或合法邮件。

朴素贝叶斯的例子

让我们使用 Julia 创建一些 Naïve Bayes 模型:

julia> Pkg.update 
julia> Pkg.add("NaiveBayes") 

我们添加了所需的NaiveBayes包。

现在,让我们创建一些虚拟数据集:

julia> X = [1 1 0 2 1; 
     0 0 3 1 0; 
     1 0 1 0 2] 
julia> y = [:a, :b, :b, :a, :a] 

我们创建了两个数组Xy,其中y中的每个元素表示X中的一列:

julia> m = MultinomialNB(unique(y), 3) 
julia> fit(m, X, y) 

我们加载了 MultinomialNB 的实例,并将我们的数据集拟合到它上:

julia> Xtest = [0 4 1; 
      2 2 0; 
      1 1 1] 

现在我们将使用它对我们的测试数据集进行预测:

julia> predict(m, Xtest) 

我得到的输出是:

julia> 3-element Array{Symbol,1}: 
   :b 
   :a 
   :a 

这意味着第一列是b,第二列是a,第三列也是a

这个例子使用了一个虚拟数据集。让我们在一个实际数据集上应用 Naïve Bayes。我们将在这个例子中使用著名的鸢尾花数据集:

julia> #import necessary libraries 

julia> using NaiveBayes 
julia> using RDatasets 

julia> iris = dataset("datasets", "iris") 

julia> #observations in columns and variables in rows 

julia> x = array(iris[:, 1:4]) 

julia> p,n = size(x) 
julia> # By default species is a PooledDataArray 

julia> y = [species for species in iris[:,5]] 

我们加载了 RDatasets,它包含了鸢尾花数据集。我们为特征向量(花萼长度、花萼宽度、花瓣长度和花瓣宽度)创建了数组。

Naïve Bayes 示例

现在我们将拆分数据集进行训练和测试。

Naïve Bayes 示例

这是相当简单的,将数据集拟合到 Naïve Bayes 分类器上。我们还计算了模型的准确性。我们可以看到准确率是 1.0,也就是 100%。

总结

在这一章中,我们学习了机器学习及其应用。赋予计算机学习和改进的能力在这个世界上有着深远的应用。它被用于预测疾病爆发、天气预测、游戏、机器人、自动驾驶汽车、个人助手等众多领域。

机器学习有三种不同的类型:监督学习、无监督学习和强化学习。

在这一章中,我们学习了监督学习,特别是 Naïve Bayes 和决策树。在接下来的章节中,我们将学习更多关于集成学习和无监督学习的内容。

参考文献

第七章 无监督机器学习

在上一章,我们学习了监督式机器学习算法,以及如何在现实世界的场景中使用它们。

无监督学习有些不同且更加复杂。其目标是让系统学习某些东西,但我们自己并不知道要学什么。无监督学习有两种方法。

一种方法是寻找数据集中的相似性/模式。然后,我们可以将这些相似的点创建为聚类。我们假设找到的聚类是可以分类的,并且可以为其分配标签。

算法本身无法分配名称,因为它没有任何标签。它只能基于相似性找到聚类,但仅此而已。为了真正能够找到有意义的聚类,需要一个足够大的数据集。

它在寻找相似用户、推荐系统、文本分类等方面被广泛使用。

我们将详细讨论各种聚类算法。在本章中,我们将学习:

  • 与无标签数据进行处理。

  • 什么是无监督学习?

  • 什么是聚类?

  • 不同类型的聚类。

  • K 均值算法和二分 K 均值算法,它们的优缺点。

  • 层次聚类。

  • 聚合聚类,它的优缺点。

  • DBSCAN 算法。

在深入探讨聚类之前,我们还应讨论第二种方法。它将告诉我们聚类与这种方法的不同之处以及使用场景。第二种方法是一种强化学习,涉及通过奖励来指示算法的成功。没有明确的分类,这种算法最适合应用于现实世界。系统根据之前的奖励或惩罚来调整行为。这种学习方式可能很强大,因为它没有偏见,也没有预先分类的观察结果。

它计算每个动作的可能性,并提前知道哪个动作会导致什么样的结果。

这种试错方法计算量大且耗时。让我们讨论一种不依赖于试错的聚类方法。

理解聚类

聚类是一种将数据划分为有用且有意义的组(聚类)的技术。这些聚类是通过捕捉数据的自然结构而形成的,彼此之间具有有意义的关系。也有可能它仅在其他算法或进一步分析的准备或总结阶段使用。聚类分析在许多领域都有应用,如生物学、模式识别、信息检索等。

聚类在不同领域有广泛应用:

  • 信息检索:将信息划分为特定聚类是从众多来源或大量数据池中搜索和检索信息的重要步骤。以新闻聚合网站为例,它们创建了相似类型新闻的聚类,使得用户更容易浏览感兴趣的部分。

    这些新闻类型也可以有子类,形成层次结构。例如,在体育新闻部分,我们可以有足球、板球、网球等其他运动。

  • 生物学:聚类在生物学中有很大的应用价值。经过多年的研究,生物学家已将大多数生物分类为不同的层级。利用这些类别的特征,可以对未知生物进行分类。同时,现有的数据可以用来寻找相似性和有趣的模式。

  • 营销:公司通过使用客户和销售数据,创建相似用户或细分群体,以便进行有针对性的促销/活动,从而获得最大的投资回报。

  • 天气:聚类分析在气候和天气分析中得到了广泛应用。气象站生成大量数据。聚类用于从这些数据中提取见解,发现模式和重要信息。

聚类是如何形成的?

形成聚类的方法有很多。我们来讨论一些基本的聚类创建方法:

  • 从对数据对象进行分组开始。这种分组应仅基于描述对象的数据进行。

  • 相似的对象被聚集在一起。它们之间可能存在某种关系。

  • 不相似的对象被保留在其他聚类中。

聚类是如何形成的?

  • 上述图清晰地展示了当不同数据对象在聚类内相似度较高,而与其他聚类的对象不相似时,形成的一些不同聚类。

聚类是如何形成的?

但是在这种数据点的表示中,我们可以看到没有可以形成的确定性聚类。这是因为不同聚类的数据对象之间存在一些相似性。

聚类的类型

聚类机制有多种类型,取决于不同的因素:

  • 嵌套或非嵌套——层次或划分

  • 重叠、排他和模糊

  • 部分与完全

层次聚类

如果聚类没有形成子集,则该聚类被称为非嵌套的。因此,划分聚类被定义为创建明确定义的聚类,这些聚类彼此之间不重叠。在这种聚类中,数据点仅位于一个聚类中。

如果聚类中有子聚类,则称为层次聚类。

层次聚类

上述图表示一个层次聚类。层次聚类是按树形结构组织的聚类。

在这里,每个聚类都有自己的子聚类。每个节点也可以被看作是一个独立的系统,具有通过划分得到的自己的聚类。

重叠、独占和模糊聚类

导致不同类型聚类创建的技术可以分为三种方法:

  • 独占聚类:在“聚类如何形成?”这一节中,我们看到两张图分别表示了两种不同类型的聚类。在第一张图中,聚类被清晰地定义,并且它们之间有很好的分离。这些叫做独占聚类。在这些聚类中,数据点与其他聚类的数据点有明显的异质性。

  • 重叠聚类:在第二张图中,我们看到没有明确的边界来分隔两个聚类。在这里,一些数据点可以出现在任意一个聚类中。这种情况出现在没有明显特征将数据点区分到某一聚类时。

  • 模糊聚类:模糊聚类是一个独特的概念。在这里,数据点属于每一个聚类,并且其关系通过权重来定义,权重范围从 1(完全属于)到 0(不属于)。因此,聚类被视为模糊集合。根据概率规则,添加了一个约束条件,即所有数据点的权重之和应该等于 1。

模糊聚类也称为概率聚类。通常,为了具有明确的关系,数据点与其具有最高隶属度的聚类关联。

部分聚类与完全聚类的区别

在完全聚类中,所有数据点都会被分配到一个聚类中,因为它们准确地表示了聚类的特征。这些类型的聚类称为完全聚类。

可能有一些数据点不属于任何聚类。这是因为这些数据点代表噪声或是聚类的异常值。此类数据点不会被包含在任何聚类中,这种情况称为部分聚类。

K-means 聚类

K-means 是最流行的聚类技术,因为它易于使用和实现。它也有一个名为 K-medoid 的伴侣。这些划分方法创建了数据集的一级划分。让我们详细讨论 K-means。

K-means 算法

K-means 从原型开始。它从数据集中获取数据点的质心。这种技术用于位于 n 维空间中的对象。

该技术涉及选择 K 个质心。这个 K 是由用户指定的,根据各种因素进行选择。它定义了我们希望有多少个聚类。因此,选择比所需的 K 高或低可能会导致不理想的结果。

现在,继续进行,每个点被分配到其最近的质心。随着许多点与特定质心关联,形成一个聚类。质心可以根据当前聚类中所包含的点进行更新。

这个过程会反复进行,直到质心保持不变。

K-means 算法

理解 K-means 算法将帮助我们更好地理解如何解决这个问题。让我们一步步理解 K-means 算法:

  1. 根据定义的 K,选择质心的数量。

  2. 将数据点分配给最近的质心。此步骤将形成聚类。

  3. 再次计算聚类的质心。

  4. 重复步骤 2 和 3,直到质心保持不变。

在第一步中,我们使用均值作为质心。

第 4 步要求重复之前的算法步骤。这有时会导致大量迭代且变化很小。因此,我们通常只有在新计算的质心变化超过 1%时,才会重复步骤 2 和 3。

将数据点与最接近的质心关联

我们如何衡量计算出的质心与数据点之间的距离?

我们使用欧几里得(L2)距离作为度量,并假设数据点位于欧几里得空间中。如果需要,我们也可以使用不同的接近度度量,例如,曼哈顿(L1)距离也可以用于欧几里得空间。

当算法处理不同数据点之间的相似性时,最好只使用数据点的必要特征集。对于高维数据,计算量急剧增加,因为必须对每个维度进行迭代计算。

有一些可以使用的距离度量选择:

  • 曼哈顿(L1):这将中位数作为质心。它作用于该函数,最小化一个对象与聚类质心之间的 L1 距离之和。

  • 平方欧几里得(L2²):这将均值作为质心。它作用于该函数,最小化一个对象与聚类质心之间的 L2 距离平方和。

  • 余弦:这将均值作为质心。它作用于该函数,最大化一个对象与聚类质心之间的余弦相似度之和。

  • Bregman 散度:这将均值作为质心。它最小化一个对象与聚类质心之间的 Bregman 散度之和。

如何选择初始质心?

这是 K-means 算法中非常重要的一步。我们首先随机选择初始质心。这通常会导致非常差的聚类结果。即使这些质心分布良好,我们也无法接近理想的聚类。

有一种解决此问题的技术——使用不同初始质心的多次运行。之后,选择具有最小平方和误差SSE)的聚类集。由于数据集的大小和所需计算能力,这种方法可能并不总是有效或可行。

在重复随机初始化时,质心可能无法克服问题,但我们可以使用其他技术:

  • 使用层次聚类,我们可以从一些样本点开始,使用层次聚类来形成一个聚类。现在我们可以从这个聚类中取出 K 个聚类,并使用这些聚类的质心作为初始质心。这种方法有一些约束:

    • 样本数据不应该很大(计算昂贵)。

    • 对于所需的聚类数,K 应该较小。

  • 另一种技术是获取所有点的质心。从这个质心中,我们找到分离最大的点。我们按照这个过程获取最大距离的质心,这些质心也是随机选择的。但是这种方法存在一些问题:

    • 找出最远点是计算密集型的。

    • 当数据集中存在异常值时,这种方法有时会产生不良结果。因此,我们可能无法获得所需的密集区域。

K-means 算法的时间空间复杂度

K-means 并不需要那么多的空间,因为我们只需要存储数据点和质心。

K-means 算法的存储需求 O((m+K)n),其中:

  • m 是点数

  • n 是属性数

K-means 算法的时间要求可能会有所变化,但通常也是适度的。随着数据点数量的增加,时间会线性增加。

K-means 算法的时间要求:O(IKmn)*,其中:

  • I 是收敛到一个质心所需的迭代次数

如果所需的聚类数远远小于 K-means 所基于的数据点数,则 K-means 效果最佳。

K-means 的问题

基本的 K-means 聚类算法存在一些问题。让我们详细讨论这些问题。

K-means 中的空聚类

可能会出现空聚类的情况。当在分配点的阶段没有分配到特定给定聚类的点时,就会发生这种情况。可以按以下方式解决:

  1. 我们选择一个不同的质心作为当前选择。如果不这样做,平方误差将远远大于阈值。

  2. 要选择一个不同的质心,我们遵循找到当前质心的最远点的相同方法。这通常会消除导致平方误差的点。

  3. 如果我们得到多个空聚类,则必须再次重复此过程。

数据集中的异常值

当我们使用平方误差时,异常值可能是决定性因素,可以影响形成的聚类。这意味着当数据集中存在异常值时,我们可能无法达到期望的聚类,或者真正代表分组数据点的聚类可能没有相似的特征。

这也导致较高的平方误差和。因此,在应用聚类算法之前,通常会先移除异常值。

也可能出现一些我们不希望移除离群点的情况。例如,一些点,如 Web 上的异常活动、过度的信用等,对于业务来说是有趣和重要的。

不同类型的聚类

K-means 有一些限制。K-means 最常见的限制是它在识别自然聚类时遇到困难。所谓自然聚类是指:

  • 非球形/圆形的形状

  • 不同大小的聚类

  • 不同密度的聚类

提示

如果存在几个密集的聚类和一个不太密集的聚类,K-means 可能会失败。

下面是不同大小的聚类示意图:

不同类型的聚类

上述图像有两张。第一张是原始点,第二张是三个 K-means 聚类。我们可以看到这些聚类并不准确。这种情况发生在聚类的大小不一样时。

下面是不同密度的聚类示意图:

不同类型的聚类

上面的图有两张图片。第一张是原始点,第二张是三个 K-means 聚类。聚类具有不同的密度。

下面是非球形聚类的示意图:

不同类型的聚类

上述图像有两张。第一张是原始点,第二张是两个 K-means 聚类。这些聚类是非圆形或非球形的,K-means 算法未能正确检测到它们。

K-means —— 优势与劣势

K-means 有许多优势,也有一些劣势。我们先来讨论其优势:

  • K-means 可以用于各种类型的数据。

  • 它易于理解和实现。

  • 它即使在重复和多次迭代的情况下也很高效。

  • 二分 K-means 是简单 K-means 的一种变体,更加高效。我们将在后面详细讨论这一点。

K-means 聚类的一些劣势或缺点包括:

  • 它并不适用于所有类型的数据。

  • 如前面示例所见,它在处理不同密度、大小或非球形聚类时效果不好。

  • 当数据集中存在离群点时,会出现问题。

  • K-means 存在一个大限制,它通过计算中心来形成聚类。因此,我们的数据应该能够有一个“中心”。

二分 K-means 算法

二分 K-means 是简单 K-means 算法的扩展。在这里,我们通过将所有点集合分成两个聚类来找出 K 个聚类。然后我们选择其中一个聚类,再次进行分割。这个过程会持续进行,直到形成 K 个聚类。

二分 K-means 的算法是:

  1. 首先,我们需要初始化聚类列表,列表中将包含由所有数据点组成的一个聚类。

  2. 重复以下步骤:

    1. 现在我们从聚类列表中移除一个聚类

    2. 接下来我们多次尝试对聚类进行二分处理。

    3. 对于 n=1 到前一步骤中试验的次数

  3. 该簇使用 K-means 被二分:

    • 从结果中选择两个总平方误差最小的簇

    • 这两个簇被添加到簇的列表中

  4. 之前的步骤会一直执行,直到我们在列表中得到 K 个簇。

我们可以通过几种方式来拆分一个簇:

  • 最大簇

  • 总平方误差最大的簇

  • 两者

我们将在这个例子中使用来自 RDatasets 的鸢尾花数据集:

二分 K 均值算法

这是著名的鸢尾花数据集的一个简单例子。我们使用 PetalLengthPetalWidth 来对数据点进行聚类。

结果如下:

二分 K 均值算法

深入了解层次聚类

这是仅次于 K-means 的第二大常用聚类技术。我们再用相同的例子来说明:

深入了解层次聚类

在这里,最上面的根节点表示所有数据点或一个簇。现在我们有三个子簇,用节点表示。所有这三个簇都有两个子簇,而这些子簇中还有进一步的子簇。这些子簇有助于找到纯粹的簇——也就是说,具有大多数共同特征的簇。

我们可以用两种方法来进行层次聚类:

  • 聚合法:这是基于簇之间的接近度概念。我们一开始将每个点视为一个独立的簇,然后逐步合并最接近的簇对。

  • 分割法:这里我们从一个包含所有数据点的簇开始,然后开始拆分,直到每个簇只包含一个数据点为止。在这种情况下,我们决定如何进行拆分。

层次聚类以树形图表示,也称为树状图(dendogram)。这用于表示簇与子簇之间的关系,以及簇是如何合并或拆分的(聚合法或分割法)。

聚合层次聚类

这是层次聚类的自底向上方法。在这里,每个观察点被视为一个独立的簇。这些簇对会根据相似性进行合并,然后我们向上移动。

这些簇根据最小的距离进行合并。当这两个簇合并时,它们会被当作一个新簇。这个步骤会在数据点池中只剩一个簇时重复。

聚合层次聚类的算法是:

  1. 首先,计算邻接矩阵。

  2. 最接近的两个簇会被合并。

  3. 在第一步中创建的邻接矩阵会在合并两个簇后进行更新。

  4. 第 2 步和第 3 步会重复执行,直到只剩下一个簇。

如何计算接近度

前述算法中的第 3 步是一个非常重要的步骤。它是衡量两个簇之间接近度的标准。

有多种方法可以定义这一点:

  • MIN:不同聚类中最接近的两个点定义了这些聚类的接近度。这是最短的距离。

  • MAX:与 MIN 相反,MAX 取聚类中的最远点,计算这两个点之间的接近度,并将其作为这两个聚类的接近度。

  • 平均法:另一种方法是取不同聚类的所有数据点的平均值,并根据这些点计算接近度。

如何计算接近度

上述图示展示了使用 MIN 计算的接近度度量。

如何计算接近度

上述图示展示了使用 MAX 计算的接近度度量。

如何计算接近度

上述图示展示了使用平均法计算的接近度度量。

这些方法也被称为:

  • 单链接法:MIN

  • 完全链接法:MAX

  • 平均链接法:平均

还有另一种方法,称为质心法。

在质心法中,接近度是通过聚类的两个均值向量来计算的。在每个阶段,两个聚类将根据哪一个具有最小的质心距离来合并。

我们来看以下例子:

如何计算接近度

上述图示展示了一个 x-y 平面中的七个点。如果我们开始进行凝聚型层次聚类,过程将如下:

  1. {1},{2},{3},{4},{5},{6},{7}。

  2. {1},{2,3},{4},{5},{6},{7}。

  3. {1,7},{2,3},{4},{5},{6},{7}。

  4. {1,7},{2,3},{4,5},{6}。

  5. {1,7},{2,3,6},{4,5}。

  6. {1,7},{2,3,4,5,6}。

  7. {1,2,3,4,5,6,7}。

该过程被分解为七个步骤,以形成完整的聚类。

这也可以通过以下树状图表示:

如何计算接近度

这表示了前述的七个步骤,属于凝聚型层次聚类。

层次聚类的优缺点

之前讨论的层次聚类方法有时更适合某些问题。我们可以通过理解层次聚类的优缺点来理解这一点:

  • 凝聚型聚类缺乏全局目标函数。这类算法的好处是没有局部最小值,且在选择初始点时没有问题。

  • 凝聚型聚类可以很好地处理不同大小的聚类。

  • 认为凝聚型聚类可以产生更好的聚类质量。

  • 凝聚型聚类通常计算量大,并且在处理高维数据时效果不佳。

了解 DBSCAN 技术

DBSCAN基于密度的空间聚类应用噪声(Density-based Spatial Clustering of Applications with Noise)的缩写。它是一种数据聚类算法,通过基于密度的扩展种子(起始)点来找到聚类。

它定位高密度区域,并通过它们之间的低密度区域将它们与其他区域分离。

那么,什么是密度呢?

在基于中心的方法中,密度是通过计算数据集中特定点在指定半径内的点数来得出的。这种方法实现起来很简单,且点的密度依赖于指定的半径。

例如,一个较大的半径对应于点m处的密度,其中 m 是半径内的数据点数量。如果半径较小,则密度可以为 1,因为只有一个点存在。

如何使用基于中心的密度来分类点

  • 核心点:位于基于密度的聚类内部的点就是核心点。这些点位于密集区域的内部。

  • 边界点:这些点位于聚类内部,但不是核心点。它们位于核心点的邻域内,位于密集区域的边界或边缘。

  • 噪声点:那些既不是核心点也不是边界点的点就是噪声点。

DBSCAN 算法

非常接近的点被放在同一个聚类中。靠近这些点的点也会被放在一起。非常远的点(噪声点)会被丢弃。

DBSCAN 算法如下:

  1. 点被标记为核心点、边界点或噪声点。

  2. 噪声点会被剔除。

  3. 核心点之间通过特殊半径形成边缘。

  4. 这些核心点被组成一个聚类。

  5. 与这些核心点相关的边界点被分配到这些聚类中。

DBSCAN 算法的优缺点

如前所述,层次聚类有时更或少适合某些特定问题。我们可以通过理解层次聚类的优缺点来更好地理解这一点。

  • DBSCAN 能够处理不同形状和大小的聚类。它之所以能够做到这一点,是因为它通过密度定义了聚类。

  • 它对噪声具有较强的抗干扰性。在找到更多聚类方面,它比 K-means 表现得更好。

  • DBSCAN 在面对具有不同密度的数据集时会遇到问题。

  • 此外,它在处理高维数据时也有问题,因为在这种数据中找到密度变得困难。

  • 计算最近邻时,它的计算开销较大。

聚类验证

聚类验证很重要,因为它告诉我们生成的聚类是否相关。在进行聚类验证时,需要考虑的重要点包括:

  • 它有能力区分数据中是否真正存在非随机结构。

  • 它能够确定实际的聚类数量。

  • 它具有评估数据是否适合聚类的能力。

  • 它应该能够比较两组聚类,找出哪一个聚类更好。

示例

我们将在层次聚类和 DBSCAN 的示例中使用ScikitLearn.jl

如前所述,ScikitLearn.jl旨在提供一个类似于 Python 中实际 scikit-learn 的库。

我们首先需要将所需的包添加到环境中:

julia> Pkg.update() 
julia> Pkg.add("ScikitLearn") 
julia> Pkg.add("PyPlot") 

这也要求我们在 Python 环境中安装 scikit-learn。如果尚未安装,我们可以使用以下命令进行安装:

$ conda install scikit-learn 

接下来,我们可以从我们的示例开始。我们将尝试在ScikitLearn.jl中使用不同的聚类算法。这些算法在ScikitLearn.jl的示例中有提供:

julia> @sk_import datasets: (make_circles, make_moons, make_blobs) 
julia> @sk_import cluster: (estimate_bandwidth, MeanShift, MiniBatchKMeans, AgglomerativeClustering, SpectralClustering) 

julia> @sk_import cluster: (DBSCAN, AffinityPropagation, Birch) 
julia> @sk_import preprocessing: StandardScaler 
julia> @sk_import neighbors: kneighbors_graph 

我们从官方的 scikit-learn 库导入了数据集和聚类算法。由于其中一些算法依赖于邻居的距离度量,我们还导入了 kNN:

julia> srand(33) 

julia> # Generate datasets. 

julia> n_samples = 1500 
julia> noisy_circles = make_circles(n_samples=n_samples, factor=.5, noise=.05) 
julia> noisy_moons = make_moons(n_samples=n_samples, noise=.05) 
julia> blobs = make_blobs(n_samples=n_samples, random_state=8) 
julia> no_structure = rand(n_samples, 2), nothing 

这段代码将生成所需的数据集。生成的数据集大小足够大,可以用来测试这些不同的算法:

julia> colors0 = collect("bgrcmykbgrcmykbgrcmykbgrcmyk") 
julia> colors = vcat(fill(colors0, 20)...) 

julia> clustering_names = [ 
    "MiniBatchKMeans", "AffinityPropagation", "MeanShift", 
    "SpectralClustering", "Ward", "AgglomerativeClustering", 
    "DBSCAN", "Birch"]; 

我们为这些算法指定了名称,并为图像填充了颜色:

julia> figure(figsize=(length(clustering_names) * 2 + 3, 9.5)) 
julia> subplots_adjust(left=.02, right=.98, bottom=.001, top=.96, wspace=.05, hspace=.01) 

julia> plot_num = 1 

julia> datasets = [noisy_circles, noisy_moons, blobs, no_structure] 

现在,我们为不同的算法和数据集指定图像的生成方式:

示例

在这里,我们对数据集进行标准化,以便更轻松地选择参数,并为需要距离度量的算法初始化kneighbors_graph

示例

在这里,我们正在创建聚类估算器,这些估算器是算法根据使用案例行为所必需的:

示例

不同算法的类似估算器。

之后,我们将这些算法应用于我们的数据集:

for (name, algorithm) in zip(clustering_names, clustering_algorithms) 
    fit!(algorithm, X) 
    y_pred = nothing 
    try 
        y_pred = predict(algorithm, X) 
    catch e 
        if isa(e, KeyError) 
            y_pred = map(Int, algorithm[:labels_]) 
            clamp!(y_pred, 0, 27) # not sure why some algorithms return -1 
        else rethrow() end 
    end 
    subplot(4, length(clustering_algorithms), plot_num) 
    if i_dataset == 1 
        title(name, size=18) 
    end 

    for y_val in unique(y_pred) 
        selected = y_pred.==y_val 
        scatter(X[selected, 1], X[selected, 2], color=string(colors0[y_val+1]), s=10) 
    end 

    xlim(-2, 2) 
    ylim(-2, 2) 
    xticks(()) 
    yticks(()) 
    plot_num += 1 
end 

得到的结果如下:

示例

  • 我们可以看到,凝聚型聚类和 DBSCAN 在前两个数据集上表现非常好

  • 凝聚型聚类在第三个数据集上表现不佳,而 DBSCAN 表现良好

  • 凝聚型聚类和 DBSCAN 在第四个数据集上都表现不佳

摘要

在本章中,我们学习了无监督学习及其与监督学习的区别。我们讨论了无监督学习的各种应用场景。

我们回顾了不同的无监督学习算法,并讨论了它们的算法、优缺点。

我们讨论了各种聚类技术以及聚类是如何形成的。我们学习了不同的聚类算法之间的差异,以及它们如何适应特定的应用场景。

我们了解了 K-means、层次聚类和 DBSCAN。

在下一章中,我们将学习集成学习。

参考文献

第八章 创建集成模型

一组人往往比单个个体做出更好的决策,特别是当每个组员都有自己的偏见时。这一理念同样适用于机器学习。

当单一算法无法生成真实的预测函数时,便会使用集成机器学习方法。当模型的性能比训练时间和模型复杂度更重要时,集成方法是首选。

在本章中,我们将讨论:

  • 什么是集成学习?

  • 构建集成模型。

  • 组合策略。

  • 提升法、装袋法和引入随机性。

  • 随机森林。

什么是集成学习?

集成学习是一种机器学习方法,其中多个模型被训练用于解决相同的问题。这是一个过程,其中生成多个模型,并将从它们得到的结果结合起来,产生最终结果。此外,集成模型本质上是并行的;因此,如果我们可以访问多个处理器,它们在训练和测试方面要高效得多:

  • 普通机器学习方法:这些方法使用训练数据来学习特定的假设。

  • 集成学习:它使用训练数据构建一组假设。这些假设被组合以构建最终模型。

因此,可以说集成学习是一种为目标函数准备不同单独学习器的方法,采用不同的策略,并在长期内结合这些学习成果。

什么是集成学习?

理解集成学习

正如我们所讨论的,集成学习将多个单独学习者的学习过程结合起来。它是多个已学习模型的聚合,旨在提高准确性:

  • 基学习器:每个单独的学习器称为基学习器。基学习器可能适用于特定情境,但在泛化能力上较弱。

  • 由于基学习器的泛化能力较弱,它们并不适用于所有场景。

  • 集成学习使用这些基(弱)学习器来构建一个强学习器,从而得到一个相对更准确的模型。

  • 通常,决策树算法作为基学习算法使用。使用相同类型的学习算法会产生同质学习器。然而,也可以使用不同的算法,这将导致异质学习器的产生。

如何构建集成模型

推荐基学习器应尽可能多样化。这使得集成能够以更高的准确性处理和预测大多数情况。可以通过使用数据集的不同子集、操控或转换输入,以及同时使用不同的学习技术来实现这种多样性。

此外,当单个基学习器具有较高准确性时,集成学习通常也能得到较好的准确性。

通常,集成的构建是一个两步过程:

  1. 第一步是创建基础学习器。它们通常是并行构建的,但如果基础学习器受到之前形成的基础学习器的影响,则会以顺序方式构建。

  2. 第二步是将这些基础学习器结合起来,创建一个最适合特定用例的集成模型。

通过使用不同类型的基础学习器和组合技术,我们可以共同产生不同的集成学习模型。

有不同的方法可以实现集成模型:

  • 对训练数据集进行子采样

  • 操控输入特征

  • 操控输出特征

  • 注入随机性

  • 分类器的学习参数可以被修改

组合策略

组合策略可以分为两类:

  • 静态组合器

  • 自适应组合器

静态组合器:组合器的选择标准独立于组件向量。静态方法可以大致分为可训练和不可训练。

可训练的:组合器经历不同的训练阶段,以提高集成模型的表现。这里有两种广泛使用的方法:

  • 加权平均法:每个分类器的输出根据其自身的表现度量进行加权:

    • 在不同验证集上衡量预测的准确性
  • 堆叠泛化:集成的输出被视为元分类器的特征向量

不可训练的:个别分类器的表现不会影响投票。可能会使用不同的组合器,这取决于分类器所生成的输出类型:

  • 投票法:当每个分类器生成一个类标签时使用此方法。每个分类器为某一特定类别投票,获得最多票数的类别获胜。

  • 平均法:当每个分类器都生成一个置信度估计时,使用平均法。集成中后验概率最大的类别获胜。

  • 博尔达计数法:当每个分类器生成一个排名时使用此方法。

自适应组合器:这是一种依赖于输入的特征向量的组合函数:

  • 每个区域局部的一个函数

  • 分而治之的方法创建了模块化的集成和简单的分类器,它们专门处理不同区域的输入输出空间。

  • 各个专家只需在其能力范围内表现良好,而不必对所有输入都有效

对训练数据集进行子采样

如果输出分类器在训练数据出现小的变化时需要经历剧烈的变化,那么学习器被认为是不稳定的:

  • 不稳定学习器:决策树、神经网络等

  • 稳定学习器:最近邻、线性回归等

这种特定的技术更适用于不稳定学习器。

子采样中常用的两种技术是:

  • 自助法

  • 提升法

自助法

袋装法也称为自助聚合。它通过对同一数据集进行有放回的子抽样生成额外的训练数据。它通过重复组合生成与原始数据集大小相同的训练数据集。

由于采用了有放回的抽样,每个分类器平均训练 63.2%的训练样本。

在这些多个数据集上训练后,袋装法通过多数投票来组合结果。获得最多票数的类别获胜。通过使用这些多个数据集,袋装法旨在减少方差。如果引入的分类器是无关的,则可以提高准确性。

随机森林是一种集成学习方法,采用袋装法,是最强大的方法之一。

让我们来看看袋装算法。

训练

  • 对于迭代,t=1 到 T

    • 从训练数据集中随机采样 N 个样本,采用有放回抽样

    • 基础学习器在该样本上训练(例如决策树或神经网络)

测试

  • 对于测试样本,t=1 到 T

    • 启动所有已训练的模型

    • 预测是基于以下内容进行的:

      • 回归:平均

      • 分类:多数投票

袋装法

什么情况下袋装法有效?

袋装法在如果不使用的话可能会发生过拟合的场景中有效。我们来看一下这些场景:

  • 欠拟合

    • 高偏差:模型不够好,未能很好地拟合训练数据

    • 小方差:每当数据集发生变化时,分类器需要做出的调整非常小

  • 过拟合

    • 小偏差:模型对训练数据拟合得过好

    • 大方差:每当数据集发生小的变化时,分类器需要做出很大或剧烈的调整

袋装法旨在减少方差而不影响偏差。因此,模型对训练数据集的依赖减少。

提升

提升与袋装法不同。它基于 PAC 框架,即“可能近似正确”框架。

PAC 学习:具有比误分类错误更大的置信度和更小的准确度的概率:

  • 准确度:这是测试数据集中正确分类样本的百分比

  • 置信度:这是在给定实验中达到准确度的概率

提升方法

提升方法基于“弱学习器”的概念。当一个算法在二分类任务中的表现略好于 50% 时,它被称为弱学习器。该方法的目的是将多个弱学习器组合在一起,目标是:

  • 提高置信度

  • 提高准确性

这是通过在不同数据集上训练不同的弱学习器来完成的。

提升算法

  • 训练:

    • 从数据集中随机抽取样本。

    • 首先,学习器 h1 在样本上进行训练。

    • 评估学习器 h1 在数据集上的准确度。

    • 使用不同的样本为多个学习者执行类似的过程。它们被划分为能够做出不同分类的子集。

  • 测试:

    • 在测试数据集上,学习通过所有学习者的多数投票来应用

信心也可以通过类似方式增强,就像通过某些权衡来提升准确性一样。

提升方法更像是一个“框架”而非一个算法。该框架的主要目标是将一个弱学习算法 W 转化为强学习算法。我们现在将讨论 AdaBoost,简称为“自适应提升算法”。AdaBoost 因为是最早成功且实用的提升算法之一而广为人知。

它不需要定义大量的超参数,并且能够在多项式时间内执行。该算法的优势在于它能够自动适应给定的数据。

AdaBoost – 通过采样进行提升

  • n 次迭代之后,Boosting 提供了一个在训练集上有分布 D 的弱学习器:

    • 所有例子都有相同的概率被选为第一个组成部分
  • 训练集的子采样是根据分布 Dn 进行的,并通过弱学习器训练模型

  • 错误分类实例的权重通过调整使得后续分类器能够处理相对较难的案例

  • 生成分布 D(n+1),其中错误分类样本的概率增加,而正确分类样本的概率减少

  • t 次迭代之后,根据模型的表现,对个别假设的投票进行加权

AdaBoost 的强大之处在于其通过自适应重采样样本,而非最终加权组合

提升方法在做什么?

  • 每个分类器在特定数据子集上有其专长

  • 一种算法集中处理具有不断增加难度的例子

  • 提升方法能够减少方差(类似于 Bagging)

  • 它还能够消除弱学习器的高偏差所带来的影响(这在 Bagging 中不存在)

  • 训练与测试误差的表现:

    • 我们可以将训练误差减少到接近 0

    • 不会出现过拟合,测试误差体现了这一点

偏差与方差分解

让我们讨论 Bagging 和 Boosting 如何影响分类误差的偏差-方差分解:

  • 可以从学习算法中预期的错误特征:

    • 偏差项是衡量分类器相对于目标函数的表现的度量

    • 方差项衡量分类器的鲁棒性;如果训练数据集发生变化,模型将如何受其影响?

  • Bagging 和 Boosting 都能够减少方差项,从而减少模型中的误差

  • 还证明了提升方法试图减少偏差项,因为它专注于错误分类的样本

操作输入特征

另一种生成多个分类器的技术是通过操控我们输入给学习算法的特征集。

我们可以选择不同的特征子集和不同大小的网络。这些输入特征子集可以手动选择,而非自动生成。这种技术在图像处理领域广泛使用:一个非常著名的例子是主成分分析(PCA)。

在许多实验中生成的集成分类器能够像真实的人类一样执行任务。

还发现,当我们删除输入的某些特征时,会影响分类器的性能。这会影响整体投票结果,因此生成的集成分类器无法达到预期效果。

注入随机性

这是另一种普遍有效的生成分类器集成的方法。在这种方法中,我们将随机性注入到学习算法中。反向传播神经网络也是使用相同的技术生成的,针对隐藏权重。如果计算应用于相同的训练样本,但使用不同的初始权重,则得到的分类器可能会有很大差异。

决策树模型中最具计算成本的部分之一是准备决策树。对于决策树桩,这个过程是快速的;然而,对于更深的树结构,这个过程可能会非常昂贵。

昂贵的部分在于选择树的结构。一旦选择了树结构,利用训练数据填充叶节点(即树的预测)是非常便宜的。一种令人惊讶的高效且成功的选择是使用具有不同结构和随机特征的树。由此生成的树的集合被称为森林,因此这样构建的分类器被称为随机森林。

它需要三个参数:

  • 训练数据

  • 决策树的深度

  • 数字形式

该算法独立生成每一棵 K 棵树,这使得并行化变得简单。对于每一棵树,它都会构建一个给定深度的完整二叉树。树枝上使用的特征是随机选择的,通常是有替换地选择,这意味着同一个特征可以多次出现在同一分支上。根据训练数据,叶节点将进行实际预测的填充。这个最后的步骤是唯一使用训练数据的时刻。随后的分类器仅依赖于 K 棵随机树的投票。

这种方法最令人惊讶的地方在于它表现得非常出色。它通常在大多数特征不重要时效果最佳,因为每棵树选择的特征数量很少。一些树将会查询那些无用的特征。

这些树基本上会做出随机预测。然而,其中一些树会查询好的特征并做出准确预测(因为叶子是基于训练数据进行评估的)。如果你有足够多的树,随机的预测会作为噪声被消除,最终只有好的树会影响最终分类。

随机森林

随机森林由 Leo Breiman 和 Adele Cutler 开发。它们在机器学习领域的优势在 2012 年 Strata 博客中得到了很好的展示:“决策树集成(通常称为随机森林)是现代最成功的通用算法”,因为它们“能够自动识别数据中的结构、交互和关系”。

此外,已注意到“许多 Kaggle 解决方案至少有一个顶级条目,强烈利用这一方法”。随机森林也成为了微软 Kinect 的首选算法,Kinect 是一个用于 Xbox 主机和 Windows 电脑的运动检测信息设备。

随机森林由一组决策树组成。因此,我们将开始分析决策树。

如前所述,决策树是一种类似树状的图表,每个节点都有一个基于单一特征的选择。给定一组特征,树会根据这些决策从节点到节点进行导航,直到到达叶子节点。这个叶子的名称即为给定特征列表的预测。一个简单的决策树可以用来决定你出门时需要带什么。

随机森林

每棵树的构建方式如下:

  • 随机选取一个n样本案例,其中 n 是训练集中的案例数量。

    • 使用替代法选择这 n 个案例。这一特定数据集用于构建树。
  • 节点根据x<X进行拆分,其中X是输入变量的数量。X在森林生长过程中不会改变。

  • 由于树木允许达到最大深度,因此没有进行修剪。

随机森林的错误率依赖于以下因素(如原始论文所述):

  • 当树之间的相关性增加时,随机森林的错误率也会增加。

  • 当单个树较弱时,错误率会增加,而当单个树得到加强时,错误率会减少。

发现之前提到的x对相关性和强度有影响。增加x会增加强度和相关性,而减少x则会减少两者。我们试图找到x应该处于的特定值或范围,以实现最小错误。

我们使用oob袋外)误差来寻找最佳的x值或范围。

这棵树并不能有效地分类所有的点。我们可以通过增加树的深度来改进这一点。这样,树就能 100%准确地预测样本数据,只需学习样本中的噪声。在极端情况下,这个算法相当于一个包含所有样本的词典。这就是过拟合,它会导致在进行测试外的预测时产生糟糕的结果。为了克服过拟合,我们可以通过为样本引入权重,并且每次分裂时仅考虑特征的一个随机子集来训练多个决策树。随机森林的最终预测将由大多数树的投票决定。这个方法也叫做集成(bagging)。它通过减少方差(训练集中的噪声错误)而不增加偏差(模型不足灵活所带来的错误)来提高预测的准确性。

随机森林的特征

  • 与现有算法相比,随机森林具有很高的准确性。

  • 它可以在大数据上有效且高效地使用。它们速度快,并且不需要昂贵的硬件来运行。

  • 随机森林的一个关键特性是它能够处理多个输入变量。

  • 随机森林可以在构建森林的过程中展示泛化误差的估计。它还可以提供分类的重要变量。

  • 当数据稀疏时,随机森林是一种有效且准确的算法。它还可以用于预测缺失数据。

  • 生成的模型可以在将来接收到的数据上使用。

  • 在不平衡数据集中,它提供了平衡类人口中存在的错误的特性。

  • 其中一些特征也可以用于无监督聚类,并且也可以作为一种有效的异常检测方法。

  • 随机森林的另一个关键特性是它不会发生过拟合。

随机森林是如何工作的?

为了理解和使用不同的选择,关于如何计算这些选择的额外数据是有帮助的。大多数选项依赖于随机森林生成的两个数据对象。

当当前树的训练集通过有放回的抽样绘制时,大约三分之一的样本被保留下来,用于示例。

随着越来越多的树被添加到随机森林中,oob 数据有助于生成分类错误的估计。在每棵树构建完成后,大部分数据都会沿着树向下运行,并计算每对样本的接近度。

另一方面,如果两个样本共享相同的终端节点,则通过增加它们的接近度来增加 1。经过完整分析后,这些接近度会被归一化。接近度被用作替代缺失数据、发现异常值以及提供数据的深入视角。

袋外(oob)误差估计

随机森林消除了交叉验证的需求,以获得无偏的测试集误差估计。在构建过程中,误差估计如下:

  • 每棵树的构建都使用了来自原始数据的交替自助样本。大约 33%的案例将留作自助测试,不参与第 k 棵树的构建。

  • 可能存在一些在构建树时没有考虑到的情况。我们将这些情况放到第 k 棵树下进行分类。大约 33%的树会生成分类测试集。在运行的后期,每次当案例n为 oob 时,将j视为获得绝大多数投票的类别。jn的真实类别在所有案例中点的中间位置不相等的次数就是 oob 误差估计。这符合在各种测试中保持无偏的原则。

基尼重要性

纯度度量通常用于决策树。误分类被度量,并称为基尼纯度,这适用于存在多个分类器的情境。

还有一个基尼系数。这适用于二分类。它需要一个能够根据属于正确类别的概率对样本进行排名的分类器。

接近度

如前所述,在构建随机森林时我们不会修剪树。因此,终端节点不会有很多实例。

为了找到接近度度量,我们将所有训练集中的案例运行到树中。假设案例 x 和案例 y 到达了同一个终端节点,那么我们就增加接近度 1。

运行结束后,我们将树的数量乘以 2,然后将增加了 1 的案例的接近度除以该数字。

在 Julia 中的实现

随机森林可以在 Kenta Sato 注册的 Julia 包中使用:

Pkg.update() Pkg.add("RandomForests") 

这是基于 CART 的随机森林在 Julia 中的实现。这个包支持:

  • 分类模型

  • 回归模型

  • 袋外(OOB)误差

  • 特征重要性

  • 各种可配置的参数

该包中有两个独立的模型:

  • 分类

  • 回归

每个模型都有自己的构造函数,通过应用fit方法进行训练。我们可以使用以下列出的一些关键字参数来配置这些构造函数:

RandomForestClassifier(;n_estimators::Int=10, 
                        max_features::Union(Integer, FloatingPoint, Symbol)=:sqrt, 
                        max_depth=nothing, 
                        min_samples_split::Int=2, 
                        criterion::Symbol=:gini) 

这个适用于分类:

RandomForestRegressor(;n_estimators::Int=10, 
                       max_features::Union(Integer, FloatingPoint, Symbol)=:third, 
                       max_depth=nothing, 
                       min_samples_split::Int=2) 

这个适用于回归:

  • n_estimators:这是弱估计器的数量

  • max_features:这是每次分裂时候候选特征的数量

    • 如果给定的是整数(Integer),则使用固定数量的特征。

    • 如果给定的是浮动点(FloatingPoint),则使用给定值的比例(0.0, 1.0]。

    • 如果给定的是符号(Symbol),则候选特征的数量由策略决定。

      • :sqrt: ifloor(sqrt(n_features))

      • :third: div(n_features, 3)

  • max_depth:每棵树的最大深度

    • 默认参数nothing表示没有最大深度的限制。
  • min_samples_split:分裂节点时尝试的最小子样本数量

  • criterion:分类的杂质度量准则(仅限分类)

    • :gini:基尼指数

    • :entropy:交叉熵

RandomForestRegressor 总是使用均方误差作为其杂质度量。当前,回归模型没有可配置的准则。

学习与预测

在我们的示例中,我们将使用 Ben Sadeghi 提供的令人惊叹的 "DecisionTree" 包。

该包支持以下可用模型:

  • DecisionTreeClassifier

  • DecisionTreeRegressor

  • RandomForestClassifier

  • RandomForestRegressor

  • AdaBoostStumpClassifier

安装很简单:

Pkg.add("DecisionTree") 

让我们从分类示例开始:

using RDatasets: dataset 
using DecisionTree 

现在我们使用著名的鸢尾花数据集:

iris = dataset("datasets", "iris") 
features = convert(Array, iris[:, 1:4]); 
labels = convert(Array, iris[:, 5]); 

这会生成一个修剪后的树分类器:

# train full-tree classifier 
model = build_tree(labels, features) 
# prune tree: merge leaves having >= 90% combined purity (default: 100%) 
model = prune_tree(model, 0.9) 
# pretty print of the tree, to a depth of 5 nodes (optional) 
print_tree(model, 5) 

学习与预测

它生成了前面图像中展示的树。现在我们应用这个学习到的模型:

# apply learned model 
apply_tree(model, [5.9,3.0,5.1,1.9]) 
# get the probability of each label 
apply_tree_proba(model, [5.9,3.0,5.1,1.9], ["setosa", "versicolor", "virginica"]) 
# run n-fold cross validation for pruned tree, 
# using 90% purity threshold pruning, and 3 CV folds 
accuracy = nfoldCV_tree(labels, features, 0.9, 3) 

它生成了以下结果:

Fold 1 
Classes:   
3x3 Array{Int64,2}: 
 15   0   0 
  1  13   0 
  0   1  20 
Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy:  
3x3 Array{Int64,2}: 
 18   0  0 
  0  18  5 
  0   1  8 
3x3 Array{Int64,2}: 
 17   0   0 
  0  11   2 
  0   3  17 
0.96 
Kappa:    0.9391727493917275 

Fold 2 
Classes:  Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy: 0.88 
Kappa:    0.8150431565967939 

Fold 3 
Classes:  Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy: 0.9 
Kappa:    0.8483929654335963 

Mean Accuracy: 0.9133333333333332 

现在让我们训练随机森林分类器:

# train random forest classifier 
# using 2 random features, 10 trees, 0.5 portion of samples per tree (optional), and a maximum tree depth of 6 (optional) 
model = build_forest(labels, features, 2, 10, 0.5, 6) 

它生成了随机森林分类器:

3x3 Array{Int64,2}: 
 14   0   0 
  2  15   0 
  0   5  14 
3x3 Array{Int64,2}: 
 19   0   0 
  0  15   3 
  0   0  13 
3x3 Array{Int64,2}: 
 17   0   0 
  0  14   1 
  0   0  18 

现在我们将应用这个学习到的模型并检查其准确性:

# apply learned model 
apply_forest(model, [5.9,3.0,5.1,1.9]) 
# get the probability of each label 
apply_forest_proba(model, [5.9,3.0,5.1,1.9], ["setosa", "versicolor", "virginica"]) 
# run n-fold cross validation for forests 
# using 2 random features, 10 trees, 3 folds and 0.5 of samples per tree (optional) 
accuracy = nfoldCV_forest(labels, features, 2, 10, 3, 0.5) 

结果如下:

Fold 1 
Classes:  Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy: 0.86 
Kappa:    0.7904191616766468 

Fold 2 
Classes:  Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy: 0.94 
Kappa:    0.9096929560505719 

Fold 3 
Classes:  Any["setosa","versicolor","virginica"] 
Matrix:    
Accuracy: 0.98 
Kappa:    0.9698613622664255 

Mean Accuracy: 0.9266666666666666 

3-element Array{Float64,1}: 
 0.86 
 0.94 
 0.98 

现在让我们训练一个回归树:

n, m = 10³, 5 ; 
features = randn(n, m); 
weights = rand(-2:2, m); 
labels = features * weights; 
# train regression tree, using an averaging of 5 samples per leaf (optional) 
model = build_tree(labels, features, 5) 
apply_tree(model, [-0.9,3.0,5.1,1.9,0.0]) 
# run n-fold cross validation, using 3 folds, averaging of 5 samples per leaf (optional) 
# returns array of coefficients of determination (R²) 
r2 = nfoldCV_tree(labels, features, 3, 5) 

它生成了以下树:

Fold 1 
Mean Squared Error:     3.300846200596437 
Correlation Coeff:      0.8888432175516764 
Coeff of Determination: 0.7880527098784421 

Fold 2 
Mean Squared Error:     3.453954624611847 
Correlation Coeff:      0.8829598153801952 
Coeff of Determination: 0.7713110081750566 

Fold 3 
Mean Squared Error:     3.694792045651598 
Correlation Coeff:      0.8613929927227013 
Coeff of Determination: 0.726445409019041 

Mean Coeff of Determination: 0.7619363756908465 

3-element Array{Float64,1}: 
 0.788053 
 0.771311 
 0.726445 

现在,训练回归森林变得更加简单,通过这个包:

# train regression forest, using 2 random features, 10 trees, 
# averaging of 5 samples per leaf (optional), 0.7 of samples per tree (optional) 
model = build_forest(labels,features, 2, 10, 5, 0.7) 
# apply learned model 
apply_forest(model, [-0.9,3.0,5.1,1.9,0.0]) 
# run n-fold cross validation on regression forest 
# using 2 random features, 10 trees, 3 folds, averaging of 5 samples/leaf (optional), 
# and 0.7 porition of samples per tree (optional) 
# returns array of coefficients of determination (R²) 
r2 = nfoldCV_forest(labels, features, 2, 10, 3, 5, 0.7) 

它生成了以下输出:

Fold 1 
Mean Squared Error:     1.9810655619597397 
Correlation Coeff:      0.9401674806129654 
Coeff of Determination: 0.8615574830022655 

Fold 2 
Mean Squared Error:     1.9359831066335886 
Correlation Coeff:      0.950439305213504 
Coeff of Determination: 0.8712750380735376 

Fold 3 
Mean Squared Error:     2.120355686915558 
Correlation Coeff:      0.9419270107183548 
Coeff of Determination: 0.8594402239360724 

Mean Coeff of Determination: 0.8640909150039585 

3-element Array{Float64,1}: 
 0.861557 
 0.871275 
 0.85944  

为什么集成学习更优?

为了理解集成学习的泛化能力为何优于单一学习器,Dietterich 提出了三个理由。

这三个原因帮助我们理解集成学习的优越性,从而得出更好的假设:

  • 训练信息不会提供足够的数据来选择单一的最佳学习器。例如,可能有多个学习器在训练数据集上表现得同样优秀。因此,将这些学习器结合起来可能是一个更好的选择。

  • 第二个原因是,学习算法的搜索过程可能存在缺陷。例如,即使存在一个最佳假设,学习算法可能因为各种原因(如生成了一个优于平均水平的假设)而无法达到该假设。集成学习通过增加达到最佳假设的可能性来改善这一点。

  • 第三个原因是,目标函数可能不存在于我们正在搜索的假设空间中。这个目标函数可能存在于不同假设空间的组合中,这类似于将多个决策树结合起来生成随机森林。

关于备受推崇的集成技术,已有许多假设研究。例如,提升(boosting)和袋装(bagging)是实现这些讨论的三点方法。

还观察到,提升方法即使在经过无数次训练后,也不会受到过拟合的影响,有时甚至能够在训练误差为零后进一步减少泛化误差。尽管许多科学家已经研究了这一现象,但理论上的解释仍然存在争议。

偏差-方差分解常用于研究集成方法的表现。观察到 Bagging 几乎能够消除方差,从而使它特别适合与经历巨大方差的学习器结合,如不稳定的学习器、决策树或神经网络。

提升方法能够最小化偏差,尽管它会减少方差,通过这种方式,使得它更适合用于像决策树这样的弱学习器。

集成学习的应用

集成学习广泛应用于以下场景:

  • 光学字符识别

  • 文本分类

  • 面部识别

  • 计算机辅助医学诊断

集成学习几乎可以应用于所有使用机器学习技术的场景。

总结

集成学习是一种通过结合弱分类器或不太准确的分类器来生成高精度分类器的方法。在本章中,我们讨论了构建集成的一些方法,并讲解了集成方法能够超越集成中任何单一分类器的三个基本原因。

我们详细讨论了 Bagging 和 Boosting。Bagging,亦称为 Bootstrap 聚合,通过对相同数据集进行有放回的子采样来生成用于训练的附加数据。我们还学习了为何 AdaBoost 表现如此优秀,并详细了解了随机森林。随机森林是高精度且高效的算法,不会发生过拟合。我们还研究了它们为何被认为是最佳的集成模型之一。我们使用“DecisionTree”包在 Julia 中实现了一个随机森林模型。

参考文献

第九章 时间序列

展示和执行决策建模与分析的能力,是一些现实应用的关键组成部分,这些应用涵盖了从重症监护病房的紧急医疗治疗到军事指挥控制系统。现有的推理方法和技术在需要权衡决策质量与计算可行性之间取得平衡的应用中并不总是有效。对于时间关键性元素的决策建模,一种成功的方法应该明确支持瞬时过程的演示,并能够应对时间敏感的情况。

在本章中,我们将涵盖:

  • 什么是预测?

  • 决策过程

  • 什么是时间序列?

  • 模型的类型

  • 趋势分析

  • 季节性分析

  • ARIMA

  • 平滑

什么是预测?

让我们以一个公司为例,假设它需要找出未来一段时间内的库存需求,以最大化投资回报。

例如,许多库存系统适用于不确定需求。这些系统中的库存参数需要评估需求和预测误差的分布。

这些系统的两个阶段——预测和库存控制——通常是独立分析的。理解需求预测和库存控制之间的相互作用至关重要,因为这直接影响库存系统的执行效果。

预测需求包括:

  • 每个决策最终都会变得具有操作性,因此应该基于对未来情况的预测来制定。

  • 各种数字在整个组织中都需要,绝对不应该由一个孤立的预测小组来生成。

  • 预测从不“完成”。预测是持续需要的,随着时间的推移,预测对实际执行的影响被衡量,原始预测会被更新,决策会被调整,这一过程持续循环。

决策者利用预测模型来做出决策。它们通常用于模拟过程,反思不同策略的影响。

将决策过程的组成部分分成三组是很有帮助的:

  • 不可控

  • 可控

  • 资源(定义问题情境)

决策过程

什么是系统?框架是由各个部分以特定方式组合而成的,目的是为了实现特定的目标。各个部分之间的关系决定了系统的功能及其整体作用。因此,系统中的连接往往比单独的部分更加重要。最终,作为其他系统构建模块的系统被称为子系统。

系统的动态

不变化的系统是静态系统。许多商业系统是快速变化的系统,这意味着它们的状态会随时间变化。我们将系统随时间变化的方式称为系统的行为。而当系统的发展呈现出典型模式时,我们说系统具有行为模式。一个系统是静态还是动态,取决于它如何随时间变化。

决策过程包括以下几个部分:

  • 绩效衡量(或指标):在每个组织中,制定有效的衡量标准被视为重要。绩效衡量标准提供了期望的结果水平,即决策的目标。目标在识别预测行动中至关重要:

    • 战略:投资回报率、增长和创新

    • 战术:成本、数量和客户满意度

    • 操作性:目标设定与标准符合性

  • 资源:资源是预测期间保持不变的恒定因素。资源是定义决策问题的变量。战略决策通常具有比战术和操作性决策更长的时间跨度。

  • 预测:预测信息来自决策者的环境。必须确定或预测不可控的输入。

  • 决策:决策输入是所有可能可行方法的集合。

  • 互动:前述决策部分之间的关联是表示输入、资源、预测和结果之间情况和结果关系的逻辑、科学函数。当决策的结果依赖于策略时,我们会改变一个或多个风险情境的部分,目的是在其中的其他部分实现有利变化。如果我们了解问题各部分之间的联系,我们就能取得成功。

  • 行动:决策包括由决策者选择的策略。我们的策略如何影响选择结果,取决于预测和不同输入之间的相互关系,以及它们如何与结果相关联。

什么是时间序列?

时间序列是一组统计数据,通常按固定时间间隔收集。时间序列数据通常在许多应用中出现:

  • 经济学:例如,失业率、住院人数等的月度数据

  • 财务:例如,日常汇率、股价等

  • 环境:例如,日降水量、空气质量读数等

  • 医学:例如,每 2 到 8 秒的心电图脑电活动

时间序列分析技术早于一般的随机过程和马尔可夫链。时间序列分析的目标是描绘和概述时间序列数据,拟合低维度模型,并做出可取的预测。

趋势、季节性、周期和残差

描述序列的一个直接策略是经典分解法。其思想是将序列分解为四个组成部分:

  • 趋势(Tt):均值的长期变化

  • 季节效应(It):与日历相关的周期性波动

  • 周期(Ct):其他周期性波动(如商业周期)

  • 残差(Et):其他随机或系统性波动

该思想是为这四个元素创建独立的模型,然后将它们合并:

  • 加法形式Xt = Tt + It + Ct + Et

  • 乘法形式Xt = Tt It Ct Et

与标准线性回归的区别

信息并非绝对独立,也不是天生地无差异地分布。时间序列的一个特点是它是一个观测的列表,其中排序非常重要。序列至关重要,因为存在依赖关系,改变顺序可能会改变数据的意义。

分析的基本目标

基本目标通常是确定一个描述时间序列模式的模型。此类模型的用途包括:

  • 描述时间序列模式的重要特征

  • 解释过去如何影响未来,或者两个时间序列如何“相互作用”

  • 预测系列的未来值

  • 作为衡量某些制造环境中产品质量的控制标准

模型类型

有两种基本类型的“时间域”模型:

  • 使用时间指数作为自变量的普通回归模型:

    • 有助于数据的初步描述,并为几种简单的预测方法奠定基础
  • ARIMA 模型(自回归积分滑动平均模型):

    • 将序列当前值与过去的值和过去的预测误差相关联的模型

首先要考虑的重要特征

在观察时间序列时需要首先考虑的一些重要问题是:

  • 是否存在趋势?

    • 测量值随着时间的推移趋向增加或减少的模式。
  • 季节性效应的影响?

    • 是否存在与日历时间相关的高低重复模式,如季节、季度、月份、星期几等?
  • 是否存在异常值?

    • 在回归中,异常值距离趋势线较远。而在时间序列数据中,异常值距离其他数据较远。
  • 是否有与季节性因素无关的周期?

  • 是否在特定时间段内具有恒定的方差?

  • 是否有任何突变发生在任一侧?

下图是时间序列中随机数的示例。时间序列图表是指将变量与时间进行绘图。类似的图表可以用于心跳、市场波动、地震图等。

需要首先考虑的重要特征

图表的一些特征包括:

  • 整个时间跨度内没有持续的趋势(上升或下降)。序列似乎在缓慢地上下波动。

  • 存在一些明显的异常值。

  • 很难判断方差是否是恒定的。

系统性模式和随机噪声

与大多数不同的分析一样,在时间序列分析中,通常认为信息由系统性模式(作为一组可识别的片段的排列)和随机噪声(误差)组成,这通常使得模式难以识别。大多数时间,序列分析系统会通过某种类型的噪声滤波,目的是使模式更加可识别。

时间序列模式的两个主要方面

大多数时候,序列模式可以通过两个基本类别的部分来描述:

  • 趋势

  • 规律性

趋势是一个大体上是直线的或(通常)非线性的部分,随时间推移而变化,并且在我们数据捕获的时间范围内不会重复(可能)。

规律性可能具有形式上类似的特征;然而,它会在系统性的间隔内重复。时间序列的这两类主要元素可能在实际的现实数据中共存。

例如,一个公司的销售额可能会在几年内迅速增长,但它们仍然遵循可预测的季节性模式(例如,每年 10 月的销售额占全年销售额的 30%,而 3 月仅占 10%)。

趋势分析

没有已知的“自动”方法可以识别时间序列数据中的趋势部分。然而,只要趋势的持续时间是重复的(以一致的方式增加或减少),并且部分数据分析通常并不特别困难。如果时间序列数据包含显著的误差,那么识别趋势的第一步通常是平滑。

平滑

平滑通常涉及某种形式的局部平均数据,使得个别观测值的非系统性部分相互抵消。最常见的方法是移动平均平滑。这将序列中的每个元素替换为 n 个相邻元素的简单或加权平均值,其中 n 是平滑“窗口”的宽度。

中位数可以替代均值。中位数有一些优点:

  • 在平滑窗口内,结果受异常值的影响较小。

  • 如果数据中存在异常值,中位数平滑通常会比移动平均更能产生平滑或更“可靠”的曲线,即使窗口宽度相同。

中值平滑的主要弱点是,在没有明显的异常值时,它可能会生成比移动平均更为曲折的曲线,并且没有考虑加权。

拟合函数

许多单调的时间序列数据可以通过线性函数来很好地近似。如果存在合理的单调非线性部分,数据应首先进行转换以消除非线性。通常可以使用对数、指数或(较少使用的)多项式函数。

季节性分析

季节性依赖性(季节性)是时间序列模型的另一个常见组成部分。例如,如果我们看到购买趋势的时间序列图,我们会发现每年十月底和十二月都会出现一个巨大的峰值。这个模式每年都会重复。

自相关图

时间序列的季节性模式可以通过自相关图进行分析。自相关图通过图形和数字方式展示自相关函数(ACF),即在预定滞后范围内,序列滞后的关系系数(及其标准误差)。

在自相关图中,通常会为每个滞后设置两个标准误差的范围,但通常来说,自相关的大小比它的可靠性更为重要。

以下是 mtcars 数据集的自相关图:

自相关图

检查自相关图

在检查自相关图时,我们必须记住,连续滞后的自相关是正式相关的。例如,如果第一个成分与第二个成分紧密相关,第二个成分与第三个成分相关,那么第一个成分与第三个成分也应该有某种程度的相关性,依此类推。

偏自相关

另一种有助于检查序列依赖性的方法是观察偏自相关函数(PACF),它是自相关的扩展,消除了对中间成分(即滞后内的成分)的依赖。

去除序列依赖性

对于特定滞后 k 的序列依赖性,可以通过差分来消除,即将序列的每个第 i 个成分转化为它与 (i-k) 个成分的差异。

这种变化背后有两种解释:

  • 序列中可能识别出隐藏的季节性依赖性

  • ARIMA 和其他过程要求我们使序列平稳,而这本身需要去除季节性依赖性。

ARIMA

我们已经讨论了时间序列分析过程的数值建模。在现实生活中,模式并不那么清晰,观察数据通常会有相当多的误差。

要求如下:

  • 寻找隐藏模式

  • 生成预测

现在让我们理解 ARIMA 模型以及它如何帮助我们获取这些信息。

常见过程

  • 自回归过程:

    • 大多数时间序列由彼此相关的组成部分构成,你可以评估一个系数或一组系数,描述系列中紧密相连的组成部分,这些组成部分来自特定的时间滞后(过去)部分。

    • 平稳性要求。自回归过程仅在特定参数范围内稳定,这些参数会影响后续点,并且序列可能不再是平稳的。

  • 移动平均过程。与自回归过程独立,序列中的每个组成部分也可能受到过去误差(或随机冲击)的影响,而这些误差无法通过自回归组件表示。

  • 可逆性要求。移动平均过程与自回归过程之间存在“对偶”关系:

    • 移动平均方程可以转换为自回归结构。无论如何,与上面描述的平稳条件相同,只有当移动平均参数满足特定条件时才能进行转换,也就是说,如果该模型是可逆的。另外,系列本身可能不会是平稳的。

ARIMA 方法

自回归移动平均模型。

Box 和 Jenkins(1976)提出的通用模型包括自回归和移动平均参数,并且在模型表述中明确包括差分操作。

具体来说,模型中的三种参数是:

  • 自回归参数(p)

  • 差分次数(d)

  • 移动平均参数(q)

在 Box 和 Jenkins 的文档中,模型通常缩写为 ARIMA(p,d,q)。

辨识

ARIMA 的输入序列应该是平稳的。它必须在时间上具有稳定的均值、方差和自相关性。因此,通常情况下,序列需要首先进行差分,直到变为平稳(这通常也需要通过对数据进行对数变换来稳定方差)。

为了达到平稳性,系列需要进行多少次差分,体现在“d”参数中。为了确定基本的差分阶数,我们需要观察数据的图形和自相关图。

显著的水平变化(明显的上升或下降变化)通常需要一阶非季节性(滞后=1)差分:

  • 显著的斜率变化通常需要二阶非季节性差分。

估计与预测

下一步是估计。在此阶段,通过使用函数最小化方法来评估参数,以最小化残差平方和。参数的评估将在最后阶段(预测)中用于计算系列的新估计值(包括输入数据集之外的部分)及其预测值的置信区间。

估计过程在变化(差分)后的数据上进行,然后生成预测数据。需要保证序列是平稳的,以便预测值与输入数据兼容。

ARIMA 模型中的常数

在标准的自回归和移动平均参数下,ARIMA 模型也可能包含常数。该常数的表示形式取决于所拟合的模型:

  • 如果模型中没有自回归参数,则序列的均值就是常数的期望值

  • 如果模型中有自回归参数,则截距由常数表示

识别阶段

在估计开始之前,我们必须确定(区分)要评估的特定 ARIMA 参数的数量和类型。识别阶段使用的重要工具包括:

  • 排列的图示

  • 自相关(ACF)的相关图

  • 部分自相关(PACF)

选择并非简单,在一些非典型的情况下,需要经验以及大量的实验性时间序列模式试验(以及 ARIMA 的技术参数)。

然而,许多实验性的时间序列模式可以使用五种基本模型之一进行充分近似。这些模型基于自相关图ACF)和部分自相关图PACF)的形状:

  • 一个自回归参数(p):

    • ACF:指数衰减

    • PACF:滞后 1 处出现尖峰

    • 其他滞后无相关性

  • 两个自回归参数(p):

    • ACF:正弦波形状模式或一组指数衰减

    • PACF:滞后 1 和 2 处出现尖峰

    • 其他滞后无相关性

  • 一个移动平均参数(q):

    • ACF:滞后 1 处出现尖峰

    • 其他滞后无相关性

    • PACF:指数衰减

  • 两个移动平均参数(q):

    • ACF:滞后 1 和 2 处出现尖峰

    • 其他滞后无相关性

    • PACF:正弦波形状模式或一组指数衰减

  • 一个自回归(p)和一个移动平均(q)参数:

    • ACF:从滞后 1 开始的指数衰减

    • PACF:从滞后 1 开始的指数衰减

季节性模型

一个模式在时间上按季节性重复的序列需要特殊的模型。

这与季节性模型中的简单 ARIMA 参数类似:

  • 季节性自回归(ps)

  • 季节性差分(ds)

  • 季节性移动平均参数(qs)

例如,假设模型为(0,1,2)(0,1,1)。

这描述了一个包含以下内容的模型:

  • 无自回归参数

  • 两个一般的移动平均参数

  • 一个常规的移动平均参数

用于季节性参数的季节性滞后通常在识别阶段确定,并应明确指出。

关于选择需要评估的参数(考虑 ACF 和 PACF)的普遍建议同样适用于季节性模型。主要区别在于,在季节性序列中,ACF 和 PACF 会在季节性滞后的倍数处显示出显著的系数。

参数估计

有几种不同的评估参数的方法。它们应该基本上会产生相同的估计值,但对于任何给定的模型来说,可能在效率上有所不同。通常,在参数估计阶段,使用一个函数最小化算法来最大化在给定参数值下观察到的序列的概率(似然)。

这需要计算残差的(条件)平方和(SS),给定单独的参数。

在实际应用中,这要求计算残差的(条件)平方和(SS),给定相应的参数。

已提出不同的方法来计算残差的 SS:

  • 根据 McLeod 和 Sales(1983)的近似最大似然法

  • 使用回溯法的近似最大似然法

  • 根据 Melard(1984)的精确最大似然法

模型评估

  • 参数估计

    • 报告估算的 t 值,基于参数的标准误差计算得出

    • 如果没有显著性,则通常可以从模型中去除单独的参数,而不会显著影响模型的整体拟合度。

  • 其他质量标准:另一个明显且常见的模型质量衡量标准是根据部分数据生成的预测的准确性,以便将预测值与已知(原始)观测值进行比较。

中断时间序列 ARIMA

我们可能希望评估一个或多个离散事件对时间序列中变量的影响。这类时间序列分析中的干扰已在 McDowall、McCleary、Meidinger 和 Hay(1980)的细节中有所描述。McDowall 等人识别出三种可能的影响类型:

  • 永久性突发干扰

  • 永久性渐变干扰

  • 突发性短暂干扰

指数平滑

指数平滑已被证明是各种时间序列数据预测策略中极为流行的一种方法。该策略由布朗和霍尔特独立开发。布朗在二战期间为美国海军工作,他的任务是设计一个追踪系统,用于计算潜艇的位置并控制火力信息。后来,他将这一策略应用于预测备件需求(一个库存控制问题)。

简单指数平滑

对时间序列 t 的简单模型是将每个观测值视为由一个常数(b)和一个误差项(epsilon)组成,即:Xt = b + t

常数 b 在序列的每个片段中通常保持不变;但它可能随着时间的推移而逐渐变化。如果拟合成功,那么隔离 b 的真实估值,从而隔离出序列的系统性或可预测部分的方法之一是计算某种类型的移动平均,其中当前和最近的观察值比较老的观察值被赋予更大的权重。

指数平滑正好满足这样的加权,即给较旧的观察值分配越来越小的指数权重。简单指数平滑的具体公式如下:

St = aXt + (1-a)St-1

当递归地连接到序列中的每个连续观察值时,每个新的平滑值(预测值)是当前观察值和过去平滑观察值的加权平均值。

过去的平滑观察值是通过计算过去的观察值以及之前的平滑值得出的,依此类推。因此,每个平滑值是过去观察值的加权平均值,其中权重会根据参数(alpha)的值呈指数递减。

如果它等于 1(一个),则完全忽略过去的观察值;如果它等于 0(零),则完全忽略当前观察值,平滑值完全由过去的平滑值组成(该平滑值是通过计算之前的平滑观察值得出的,依此类推;因此,所有的平滑值将等于初始平滑值 S0)。中间值将产生过渡结果:

  • 如果它等于 1,则完全忽略过去的观察值。

  • 如果它等于 0,则完全忽略当前观察值:

    • 平滑值完全由过去的平滑值组成(该平滑值又是通过计算之前的平滑观察值得出的,依此类推;因此,所有的平滑值将等于初始平滑值 S0)。中间值将产生过渡结果。

隐含在观察到的时间序列中的过程的假设模型,简单指数平滑,通常能产生准确的预测。

拟合缺失指标(误差)

评估基于特定值的预测准确性最直接的方法是仅绘制观察值和前一步预测值的图表。此图表还可以包括残差(与右侧的 Y 轴比例),这样就能轻松识别出拟合较好或较差的区域。

这种对预测准确性的视觉检查通常是判断当前指数平滑模型是否适合数据的最有效方法:

  • 平均误差平均误差ME)是通过计算观察值与前一步预测值之差的平均值来得出的:

    • 显然,这一衡量标准的缺点是正负误差值可能会相互抵消,因此它不是一个良好的总体拟合指标。
  • 平均绝对误差(MAE)平均绝对误差MAE)值是通过计算平均绝对误差值得到的:

    • 如果值为 0(零),则拟合(预测)被认为是完美的。

    • 与均方误差值相比,这一拟合衡量标准会忽略异常值,因此,独特或异常的大误差值将比均方误差对 MAE 的影响更小。

  • 平方误差和均方误差(SSE):这些值是通过计算平方误差值的总和(或均值)得到的。它是统计拟合方法中最常用的拟合缺失指标之一。

  • 百分比误差(PE):所有前述的衡量标准都依赖于实际误差值。可能看起来更合理的是,使用相对于观察值的幅度,表示未来一步预测与观察值的相对偏差,而不是直接表达拟合的缺失。

    • 例如,在试图预测每月可能大幅波动的销售额时,如果我们的预测"接近目标",偏差约为±10%,我们可能会感到满意。换句话说,绝对误差可能不那么重要,而更关心的是相对误差:

PEt = 100(Xt - Ft )/Xt*

这里,Xt是时刻t的观察值,Ft是预测值(平滑值)。

  • 平均百分比误差(MPE):该值通过计算 PE 值的平均值得到。

  • 平均绝对百分比误差(MAPE):与均值误差值的情况类似,接近 0(零)的均值百分比误差可能是由相互抵消的显著正负误差百分比产生的。因此,相对拟合优度的更优衡量标准是平均绝对百分比误差。此外,该衡量标准通常比均方误差更为重要:

    • 例如,意识到正常预测偏差为±5%本身就是一个有用的结果,尽管 30.8 的均方误差并不容易直观理解。
  • 自动搜索最佳参数:利用类牛顿法的函数最小化过程(与 ARIMA 中的方法相同)来最小化均方误差、平均绝对误差或平均总率误差。

  • 初始平滑值 S0:我们需要一个 S0 值,以便计算时间序列中第一个观察值的平滑预测值。根据参数选择(即当它接近零时),平滑过程的初始值可能会影响某些观察值的预测质量。

在 Julia 中的实现

TimeSeries 是一个注册包。因此,像其他包一样,我们可以将其添加到您的 Julia 包中:

Pkg.update() 
Pkg.add("TimeSeries")

时间序列类型 TimeArray

immutable TimeArray{T, N, D<:TimeType, A<:AbstractArray} <: AbstractTimeSeries

 timestamp::Vector{D}
 values::A
 colnames::Vector{UTF8String}
 meta::Any

 function TimeArray(timestamp::Vector{D},
 values::AbstractArray{T,N},
 colnames::Vector{UTF8String},
 meta::Any)
 nrow, ncol = size(values, 1), size(values, 2)
 nrow != size(timestamp, 1) ? error("values must match length of
 timestamp"):
 ncol != size(colnames,1) ? error("column names must match width of
 array"):
 timestamp != unique(timestamp) ? error("there are duplicate dates"):
 ~(flipdim(timestamp, 1) == sort(timestamp) || timestamp ==
 sort(timestamp)) ? error("dates are mangled"):
 flipdim(timestamp, 1) == sort(timestamp) ? 
 new(flipdim(timestamp, 1), flipdim(values, 1), colnames, meta):
 new(timestamp, values, colnames, meta)
 end
end

该类型有四个字段:

  • timestamp:时间戳字段由一个TimeType类型的子类型值向量组成,实际上是DateDateTimeDateTime类型与 Date 类型相似,但它表示的是比一天更小的时间框架。为了使TimeArray的构造正常工作,这个向量需要按顺序排序。如果向量包含非连续的日期,对象构造会报错。该向量还需要从最旧到最新日期排序,但构造器可以处理此问题,不会阻止对象的创建。

  • values:值字段包含时间序列中的数据,其行数必须与时间戳数组的长度匹配。如果它们不匹配,构造器将失败。values数组中的所有值必须是相同类型的。

  • colnamescolnames字段是一个 UTF8 字符串类型的向量,包含每个列在值字段中的列名。此向量的长度必须与values数组的列数匹配,否则构造器将失败。

  • metameta字段默认不包含任何内容,用Void类型表示。这个默认设置旨在允许程序员忽略该字段。对于那些希望使用此字段的人,meta可以保存常见类型,如String或更复杂的用户定义类型。有人可能希望为不可变的对象分配一个名称,而不是依赖于对象类型字段外的变量绑定。

我们将使用MarketData包中可用的历史财务数据集:

Pkg.add("MarketData") 
using TimeSeries 
using MarketData 

现在让我们来查看数据:

ohlc[1] 

这将产生以下输出:

1x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2000-01-03 

             Open      High      Low       Close      
2000-01-03 | 104.88    112.5     101.69    111.94 

让我们再看一些记录和统计数据:

ohlc[[1:3;9]] 

这将产生以下输出:

4x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2000-01-13 

             Open      High      Low       Close      
2000-01-03 | 104.88    112.5     101.69    111.94     
2000-01-04 | 108.25    110.62    101.19    102.5      
2000-01-05 | 103.75    110.56    103.0     104.0      
2000-01-13 | 94.48     98.75     92.5      96.75   

我们也可以通过列名来遍历它们:

500x2 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2001-12-31 

             Open      Close      
2000-01-03 | 104.88    111.94     
2000-01-04 | 108.25    102.5      
2000-01-05 | 103.75    104.0      
2000-01-06 | 106.12    95.0       

2001-12-26 | 21.35     21.49      
2001-12-27 | 21.58     22.07      
2001-12-28 | 21.97     22.43      
2001-12-31 | 22.51     21.9  

要使用日期访问记录,可以如下操作:

ohlc[[Date(2000,1,3),Date(2000,2,4)]] 

它将给出以下输出:

2x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2000-02-04 

             Open      High      Low       Close      
2000-01-03 | 104.88    112.5     101.69    111.94     
2000-02-04 | 103.94    110.0     103.62    108.0 

我们还可以列出日期范围内的记录:

ohlc[Date(2000,1,10):Date(2000,2,10)] 

它将产生以下输出:

23x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-10 to 2000-02-10 

             Open      High      Low       Close      
2000-01-10 | 102.0     102.25    94.75     97.75      
2000-01-11 | 95.94     99.38     90.5      92.75      
2000-01-12 | 95.0      95.5      86.5      87.19      
2000-01-13 | 94.48     98.75     92.5      96.75      

2000-02-07 | 108.0     114.25    105.94    114.06     
2000-02-08 | 114.0     116.12    111.25    114.88     
2000-02-09 | 114.12    117.12    112.44    112.62     
2000-02-10 | 112.88    113.88    110.0     113.5 

我们还可以使用两个不同的列:

ohlc["Open"][Date(2000,1,10)] 

它产生以下输出:

1x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-10 to 2000-01-10 

             Open       
2000-01-10 | 102 

使用时间约束

如果满足条件,某些特定方法可以按时间范围进行分段。

when

when方法允许将TimeArray中的元素聚合到特定的时间段。

例如:dayofweekmonth。以下是一些日期方法及其示例:

day   Jan 3, 2000 = 3 
dayname  Jan 3, 2000 = "Monday" 
week  Jan 3, 2000 = 1 
month Jan 3, 2000 = 1 
monthname   Jan 3, 2000 = "January" 
year  Jan 3, 2000 = 2000 
dayofweek   Monday = 1 
dayofweekofmonth  Fourth Monday in Jan = 4 
dayofyear   Dec 31, 2000 = 366 
quarterofyear  Dec 31, 2000 = 4 
dayofquarter   Dec 31, 2000 = 93 

来自

from(cl, Date(2001, 10, 24)) 

这将产生以下输出:

47x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2001-10-24 to 2001-12-31 

             Close     
2001-10-24 | 18.95     
2001-10-25 | 19.19     
2001-10-26 | 18.67     
2001-10-29 | 17.63     

2001-12-26 | 21.49     
2001-12-27 | 22.07     
2001-12-28 | 22.43     
2001-12-31 | 21.9  

to(cl, Date(2000, 10, 24))

这段代码将生成以下输出:

206x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-03 to 2000-10-24 

             Close      
2000-01-03 | 111.94     
2000-01-04 | 102.5      
2000-01-05 | 104.0      
2000-01-06 | 95.0       

2000-10-19 | 18.94      
2000-10-20 | 19.5       
2000-10-23 | 20.38      
2000-10-24 | 18.88 

findwhen

这可能是最常用且高效的方法之一。它测试一个条件并返回DateDateTime向量:

red = findwhen(ohlc["Close"] .< ohlc["Open"]); 

这将生成以下输出:

252x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-04 to 2001-12-31 

             Open      High      Low       Close      
2000-01-04 | 108.25    110.62    101.19    102.5      
2000-01-06 | 106.12    107.0     95.0      95.0       
2000-01-10 | 102.0     102.25    94.75     97.75      
2000-01-11 | 95.94     99.38     90.5      92.75      

2001-12-14 | 20.73     20.83     20.09     20.39      
2001-12-20 | 21.4      21.47     20.62     20.67      
2001-12-21 | 21.01     21.54     20.8      21.0       
2001-12-31 | 22.51     22.66     21.83     21.9  

find

find方法类似于findwhen。它测试一个条件并返回一个整数向量,表示条件为真的行号:

green = find(ohlc["Close"] .> ohlc["Open"]); 

这将生成以下输出:

244x4 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2001-12-28 

             Open      High      Low       Close      
2000-01-03 | 104.88    112.5     101.69    111.94     
2000-01-05 | 103.75    110.56    103.0     104.0      
2000-01-07 | 96.5      101.0     95.5      99.5       
2000-01-13 | 94.48     98.75     92.5      96.75      

2001-12-24 | 20.9      21.45     20.9      21.36      
2001-12-26 | 21.35     22.3      21.14     21.49      
2001-12-27 | 21.58     22.25     21.58     22.07      
2001-12-28 | 21.97     23.0      21.96     22.43 

数学、比较和逻辑运算符

这些方法也由 TimeSeries 包支持。

使用数学运算符:

    • 或 .+:数学元素逐项加法
    • 或 .-: 数学元素级减法
    • 或 .*: 数学元素级乘法
  • ./: 数学元素级除法

  • .^: 数学元素级指数运算

  • % 或 .%: 数学元素级余数

使用比较运算符:

  • .> 元素级大于比较

  • .< 元素级小于比较

  • .== 元素级等价比较

  • .>= 元素级大于或等于比较

  • .<= 元素级小于或等于比较

  • .!= 元素级不等价比较

使用逻辑运算符:

  • & 元素级逻辑与

  • | 元素级逻辑或

  • !, ~ 元素级逻辑非

  • $ 元素级逻辑异或

应用方法到时间序列

常见的时间序列数据转换包括:

  • 滞后操作

  • 前导

  • 计算变化

  • 窗口操作和聚合操作

滞后

lag 方法将昨天的值放置在今天的时间戳上:

cl[1:4] 

#Output 
4x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-03 to 2000-01-06 

             Close      
2000-01-03 | 111.94     
2000-01-04 | 102.5      
2000-01-05 | 104.0      
2000-01-06 | 95.0 

这在此应用了滞后操作:

lag(cl[1:4]) 

它生成以下输出:

3x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-04 to 2000-01-06 

             Close      
2000-01-04 | 111.94     
2000-01-05 | 102.5      
2000-01-06 | 104.0 

滞后

Lead 操作与滞后操作相对:

lead(cl[1:4]) 

生成的输出如下:

3x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-03 to 2000-01-05 

             Close      
2000-01-03 | 102.5      
2000-01-04 | 104.0      
2000-01-05 | 95.0 

由于 cl 有 500 行长,我们可以向前推动至此。目前,我们将推动 400:

lead(cl, 400) 

生成的输出如下:

100x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-03 to 2000-05-24 

             Close     
2000-01-03 | 19.5      
2000-01-04 | 19.13     
2000-01-05 | 19.25     
2000-01-06 | 18.9      

2000-05-19 | 21.49     
2000-05-22 | 22.07     
2000-05-23 | 22.43     
2000-05-24 | 21.9 

百分比

最常见的时间序列操作之一是计算百分比变化:

percentchange(cl) 

生成的输出如下:

499x1 TimeSeries.TimeArray{Float64,1,Date,Array{Float64,1}} 2000-01-04 to 2001-12-31 

             Close    
2000-01-04 | -0.0843  
2000-01-05 | 0.0146   
2000-01-06 | -0.0865  
2000-01-07 | 0.0474   

2001-12-26 | 0.0061   
2001-12-27 | 0.027    
2001-12-28 | 0.0163   
2001-12-31 | -0.0236 

这显示了与上一记录的百分比变化。

将方法结合在时间序列中

两个 TimeArrays 可以合并生成有意义的数组。

合并

合并操作连接两个 TimeArrays。默认情况下,使用内连接:

merge(op[1:4], cl[2:6], :left) 

生成的输出如下:

4x2 TimeSeries.TimeArray{Float64,2,Date,Array{Float64,2}} 2000-01-03 to 2000-01-06 

             Open      Close      
2000-01-03 | 104.88    NaN        
2000-01-04 | 108.25    102.5      
2000-01-05 | 103.75    104.0      
2000-01-06 | 106.12    95.0 

在前面的例子中,我们提供了要执行的连接类型。我们还可以进行右连接或外连接。

压缩

collapse 方法用于将数据压缩到更大的时间范围内。

映射

该方法用于时间序列数据的转换。此方法的第一个参数是一个二元函数(时间戳和值)。此方法返回两个值,分别是新的时间戳和新的值向量:

a = TimeArray([Date(2015, 10, 24), Date(2015, 11, 04)], [15, 16], ["Number"]) 

生成的输出如下:

 2x1 TimeSeries.TimeArray{Int64,1,Date,Array{Int64,1}} 2015-10-24 to 2015-11-04 

             Number    
2015-10-24 | 15        
2015-11-04 | 16 

您可以如下应用 map 方法:

map((timestamp, values) -> (timestamp + Dates.Year(1), values), a) 

这会转换给定时间的记录:

2x1 TimeSeries.TimeArray{Int64,1,Date,Array{Int64,1}} 2016-10-24 to 2016-11-04 

             Number    
2016-10-24 | 15        
2016-11-04 | 16 

总结

在本章中,我们学习了什么是预测以及为什么它在商业中是必需的。预测帮助识别需求并采取必要的措施,在其他领域,它有助于预测天气等。预测的结果对决策过程有很大的影响。时间序列是按标准间隔收集的洞察的安排。它已在医学、天气、金融市场等多个领域得到应用。

我们还学习了不同类型的模型以及如何分析时间序列中的趋势。我们还考虑了季节性对时间序列分析的影响。我们详细讨论了 ARIMA 模型,并探索了 Julia 的时间序列库。

参考

第十章 协同过滤与推荐系统

每天,我们都面临各种决策和选择。这些选择可以从我们的衣物,到我们能看的电影,甚至是我们在线订餐时的选择。我们在商业中也需要做决策。例如,选择我们应该投资哪只股票。我们所面临的选择集合会根据我们实际做什么以及我们想要什么而有所不同。例如,在 Flipkart 或 Amazon 上购买衣物时,我们会看到成百上千的选择。Amazon 的 Kindle 商店如此庞大,以至于没有人能够在一生中读完所有书籍。为了做出这些决策,我们需要一些背景信息,也许还需要一点帮助,知道什么对我们最好。

通常,个人依赖于朋友的建议或专家的意见来做出选择和决策。他们可能会观察朋友或信任的人,做出与自己相同的决策。这些决策可能包括支付一笔较高的票价去看电影,或者点一份自己从未尝试过的披萨,或者开始阅读一本自己一无所知的书。

这些建议的产生方式是有限制的。这些建议并不依赖于用户的喜好或“口味”。可能有许多电影、披萨或书籍是某人可能喜欢的,但他们的朋友或公司可能并不喜欢,而这些人的决定通常会影响他们的选择。传统的建议或推荐方式无法照顾到用户的特定口味。

在本章中,我们将学习:

  • 什么是推荐系统?

  • 关联规则挖掘

  • 基于内容的过滤

  • 什么是协同过滤?

  • 什么是基于用户和基于项目的协同过滤?

  • 构建推荐引擎

什么是推荐系统?

推荐框架使用学习方法来为数据、物品或服务提供个性化的建议。这些推荐系统通常与目标用户有一定程度的互动。近年来收集到的数据以及今天产生的数据,为这些推荐系统提供了巨大的支持。

今天,许多推荐系统正在运行,并每天生成数百万条推荐:

  • 电子商务网站关于书籍、衣物或商品的推荐

  • 适合我们口味的广告

  • 我们可能感兴趣的属性类型

  • 适合我们口味和预算的旅游套餐

当前一代推荐系统能够提供有价值的推荐,并且可以扩展到数百万种产品和目标用户。即使产品或用户的数量增加,推荐系统也应该继续正常工作。但这也带来了另一个挑战,因为为了获得更好的推荐,算法将处理更多的数据,这将增加推荐所需的时间。如果限制处理的数据量,生成的推荐可能不那么有效,或者质量无法获得用户的信任。

我们需要创建平衡,并设计机制以在更短的时间内处理更多数据。

这是通过使用基于用户的协同过滤或基于物品的协同过滤来实现的。在深入了解协同过滤之前,我们还将介绍关联规则挖掘。

推荐系统框架是数据和电子商务系统中的核心部分。它是一个有效的技术,帮助客户在海量数据和商品空间中筛选信息。它还帮助用户发现他们可能未曾搜索过或未曾购买的产品,如果没有推荐系统的存在,用户可能根本不会接触到这些产品。这也有助于提高销售,因为越来越多的用户会找到他们感兴趣的正确物品。

推荐系统的改进已经进行了大量研究,并且普遍认为没有一种推荐系统能够适应所有类型的问题。

算法无法在没有用户交互或数据的情况下工作。与用户的交互是必要的:

  • 了解用户

  • 向用户提供生成的推荐

从用户那里收集“好”数据是一个巨大挑战,即消除或去除那些可能影响生成推荐结果的噪音数据。

一些用户通常会浏览互联网上的各种信息,而另一些用户则只关注他们感兴趣的数据。另外,一些用户非常关注隐私,不允许数据收集过程进行。

实际上,当前的推荐系统通常在提供干净且有用的数据时能够给出好的推荐。大量的工作被投入到数据收集和清洗阶段,在这个过程中我们了解哪些数据对用户实际上是有用的。

用户效用矩阵

一般来说,在推荐系统中,我们会遇到属于两个类别的实体:

  • 用户

  • 物品

用户可能会对特定的物品有偏好,我们需要发现这种偏好,并向他们展示符合标准的物品。

让我们以电影的用户评分矩阵为例。我们将构建一个用户-电影矩阵,其中评分将作为矩阵中的值:

星际大战 IV 教父 指环王 1 指环王 2 指环王 3 恋恋笔记本 泰坦尼克号
用户 1 4 3 3
用户 2 5 3 2
用户 3 5 5 4
用户 4 4 3
用户 5 2 3 4 5

在这个具体的例子中,我们可以看到用户 1 给《教父》打了 4 分,给《指环王 3》打了 3 分,给《恋恋笔记本》打了 3 分,但该用户没有给其他电影评分,这通常是因为用户没有观看这些电影。也有可能是用户选择不分享对这些电影的看法。

这些值的范围是从 1 到 5,1 为最低评分,5 为最高评分。这表明矩阵是稀疏的,意味着大多数条目是未知的。在现实世界中,我们遇到的数据通常更稀疏,我们需要用用户可能的评分来填充这些空白,从而提供推荐。

关联规则挖掘

关联规则挖掘是寻找频繁出现的项目集合之间的关联或模式。这也被称为市场篮分析

它的主要目的是理解顾客的购买习惯,通过发现顾客打算购买或实际购买的商品之间的关联性和模式来实现。例如,购买计算机键盘的顾客也很可能购买计算机鼠标或优盘。

该规则由以下给出:

  • 前提 → 结果 [支持度,置信度]

关联规则的衡量标准

令* A B C D * 和* E *……表示不同的商品。

然后我们需要生成关联规则,例如:

  • {A, J} → {C}

  • {M, D, J} → {X}

第一个规则表示,当* A J 一起购买时,顾客购买 C *的概率很高。

类似地,第二个规则表示,当* M D J 一起购买时,顾客购买 X *的概率也很高。

这些规则通过以下方式衡量:

  • 支持度:支持度指的是总覆盖率。它是一起购买商品的概率,占总交易的比例:

    • 支持度, X → Y: P(X,Y)

    • (同时包含 X 和 Y 的交易)/(总交易数)

  • 置信度:置信度指的是准确性。它是购买第二件商品的概率,前提是第一件商品已经被购买:

    • 置信度, X → Y: P(Y|X)

    • (包含 X 和 Y 的交易)/(仅包含 X 的交易)

有些商品购买得不够频繁,它们可能对算法没有那么重要。为了生成这些规则,需要丢弃这些商品。它们通过两个阈值来定义,称为:

  • 最小支持度

  • 最小置信度

如何生成项目集

  • 满足所需最小支持度的项目集被选中。

  • 如果 {X,Y} 满足最小支持度的标准,那么 X 和 Y 也满足该标准,反之则不成立。

Apriori 算法:属于频繁项目集的子集本身也是频繁的:

  1. 首先,基于 n 查找所有项目集:

    • 例如,当 n=2 时,{{X,Y}, {Y,Z}, {X,S}, {S,Y}}
  2. 现在我们将这些集合合并到更高的层次:

    • {{X,Y,Z}, {X,Y,S}, {Y,Z,S}, {X,Z,S}}
  3. 从这些合并的集合中,我们检查有多少个符合所需的最小支持度:

    • 我们排除那些无法生成最小支持度的集合
  4. 我们不断增加层级,直到无法生成具有所需最小支持度的集合为止

如何生成规则

当项目集数量较小时,使用暴力破解方法:

  • 生成所有项目集的子集。空集不包括在内。

  • 计算这些子集的置信度。

  • 选择具有较高置信度的规则。

基于内容的过滤

基于内容的过滤创建用户的个人资料,并利用该个人资料向用户提供相关的推荐。用户个人资料是通过用户的历史记录创建的。

例如,电子商务公司可以跟踪以下用户详细信息,以生成推荐:

  • 过去的订单项

  • 查看或加入购物车但未购买的项目

  • 用户浏览历史记录,用于识别用户可能感兴趣的产品类型

用户可能没有手动为这些项目评分,但可以考虑各种因素来评估它们与用户的相关性。基于此,向用户推荐那些可能感兴趣的新项目。

基于内容的过滤

如图所示的过程,从用户个人资料中获取属性,并将其与可用项目的属性进行匹配。当有相关项目时,这些项目被认为是用户感兴趣的,并进行推荐。

因此,推荐结果在很大程度上依赖于用户的个人资料。如果用户个人资料正确反映了用户的喜好和兴趣,那么生成的推荐将是准确的;如果个人资料没有反映用户当前的偏好,那么生成的推荐可能不准确。

基于内容的过滤的步骤

在使用基于内容的过滤生成推荐时涉及多个步骤。这些步骤如下:

  • 分析项目的属性。候选推荐项目可能没有有效的信息结构。因此,第一步是以结构化的方式从项目中提取这些属性:

    • 例如,对于一家电子商务公司,这些属性是他们目录中产品的属性或特征
  • 生成用户的个人资料。用户个人资料是通过考虑各种因素来创建的。这个过程是使用机器学习技术完成的。可以考虑的各种因素包括:

    • 订单历史

    • 项目查看历史

    • 用户浏览历史记录,用于识别用户可能感兴趣的产品类型

    除此之外,用户的反馈也会被考虑在内。例如,如果用户在订购产品后感到满意,用户查看了产品多少次,以及在产品上花了多少时间。

  • 推荐系统使用前面两步生成的组件,接下来生成的用户资料和提取的商品属性通过各种技术进行匹配。用户和商品的不同属性会被赋予不同的权重。然后,我们生成的推荐内容可以根据相关性进行排序。

生成用户资料是一个典型的任务,并且需要全面性,以便生成更准确的资料。

社交网络有助于建立用户资料。它是用户手动提供的信息宝库。用户提供了诸如以下内容的详细信息:

  • 用户感兴趣的产品类型,例如哪些类型的书籍、音乐等

  • 用户不喜欢的产品,例如某些特定的菜肴、化妆品牌等

在我们开始为用户提供推荐后,我们还可以收到反馈,这有助于推荐系统生成更好的推荐内容:

  • 显式反馈:当我们在电商网站上购买商品时,通常会在初次使用后 2-3 天收到反馈表单。该表单的主要目的是帮助公司了解我们是否喜欢该产品,如果不喜欢,应该如何改进。这就是显式反馈。它使得推荐系统可以知道该产品不完全适合用户,或者可以推荐一个更合适的产品。

  • 隐式反馈:用户可能不需要手动填写反馈表单,或者可能选择不填写。在这种情况下,用户的活动会被分析和监控,以了解用户对产品的反应。

反馈也可能出现在这些电商网站的产品评论区。这些评论可以被挖掘,从中提取出用户的情感。

尽管从用户那里获得直接反馈可以让系统更容易工作,但大多数用户选择忽略这种反馈。

基于内容的过滤的优点

使用基于内容的过滤方法有很多优点:

  • 基于内容的过滤仅依赖于我们为其生成推荐的用户。这些推荐不依赖于其他用户的评分或资料。

  • 生成的推荐内容可以向用户解释,因为它们是依赖于用户的个人资料和商品的属性。

  • 由于推荐不是基于商品的评分,而是基于这些商品的属性和用户的个人资料,因此尚未购买或评分的新商品也可以被推荐。

基于内容的过滤的局限性

基于内容的过滤方法也有一些局限性:

  • 由于基于内容的过滤需要用户资料,对于新用户来说,生成推荐可能会很困难。为了提供高质量的推荐,我们需要分析用户的活动,但生成的推荐可能仍然不会符合用户的喜好。

  • 当项目的属性或特征不可用时,基于内容的过滤会面临提供推荐的困难。此外,还需要足够的领域知识来理解这些属性。

  • 基于内容的过滤还依赖于用户提供的反馈。因此,我们需要持续分析和监控用户的反馈。在系统无法判断反馈是正面还是负面时,它可能无法提供相关的推荐。

  • CBF(基于内容的过滤)也有将推荐限制在特定集合中的倾向。它可能无法推荐用户可能感兴趣的类似或相关项目。

协同过滤

协同过滤是一种著名的算法,它基于其他用户或同伴的喜好或行为,不同于我们在前一节中研究的基于内容的过滤。

协同过滤:

  • 如果用户喜欢其他用户或同伴表现出兴趣的某些项目,那么这些用户的偏好可以推荐给目标用户。

  • 它被称为“最近邻推荐”

为了实现协同过滤,做出了一些假设:

  • 可以考虑同伴或其他用户的喜好或行为,以了解并预测目标用户的兴趣。因此,假设目标用户与其他考虑的用户具有相似的兴趣。

  • 如果用户过去是根据一组用户的评分获得推荐的,那么该用户与该组具有相似的兴趣。

协同过滤有不同的类型:

  • 基于记忆的协同过滤:基于记忆的协同过滤通过用户的评分计算用户之间甚至是项目之间的相似性。这用于生成推荐:

    • 它利用评分矩阵

    • 使用这个评分矩阵,可以在任何给定时刻为目标用户生成推荐。

  • 基于模型的协同过滤:基于模型的协同过滤依赖于训练数据和学习算法来创建模型。该模型用于利用实际数据生成推荐:

    • 这种方法将模型拟合到提供的矩阵,以便基于该模型生成推荐。

协同过滤的基本过程包括:

  • 由于协同过滤高度依赖于考虑的群体的偏好,建议找到具有相似兴趣的同伴群体。

  • 我们考虑推荐的项目应该出现在该组的项目列表中,但不是用户的项目。

  • 在创建矩阵后,根据各种因素为项目赋予特定的评分。

  • 得分最高的项目将被推荐。

  • 为了随着新项目的加入不断改进和添加推荐,前述步骤会在所需的时间间隔内重新执行。

协同过滤有一些优点和缺点。让我们先来看看优点:

  • 如果用户的属性正确形成,那么就更容易理解。

  • 用户和产品是简单的系统,不需要特定的理解即可构建推荐系统。

  • 产生的推荐通常很好

协同过滤也有一些缺点:

  • 正如前面所讨论的,协同过滤需要大量的用户反馈。此外,这些用户需要是可靠的。

  • 还需要对项目的属性进行标准化。

  • 假设过去的行为会影响当前的选择。

基准预测方法

基准预测方法是可以形成的最简单、最容易的预测。计算基准非常重要,因为它能帮助我们了解我们生成的模型的准确性以及我们产生的算法结果的有效性:

  • 分类:分类问题的基准可以通过考虑预测结果的大多数将来自观察量最多的类别来形成。

  • 回归:回归问题的基准可以通过考虑预测结果的大多数将是一个集中趋势度量(如均值或中位数)来形成。

  • 优化:在进行优化问题时,领域中的随机样本是固定的。

当我们选择了基准预测方法并得到结果后,可以将其与我们生成的模型结果进行比较。

如果我们生成的模型无法超越这些基准方法,那么很可能我们需要在模型上进行改进,以提高准确性。

基于用户的协同过滤

基于用户的协同过滤利用了协同过滤的核心思想,即找到与目标用户过去评分或行为相似的用户。

这种方法也被称为 k-最近邻协同过滤。

基于用户的协同过滤

如果我们有 n 个用户和 x 个项目,那么我们将得到一个矩阵 R -> nx*。在前面的图中,我们可以看到有一个目标用户和多个其他用户。在这些其他用户中,有两个与目标用户相似。因此,之前的评分或行为可以用于生成推荐。

除了上述矩阵外,我们还需要一个函数来计算用户之间的相似度,以便计算哪些用户的评分可以用来生成推荐。

为了找到用户(u,v)之间的相似度,我们使用 Pearson 相关系数(Pearson 的 r)来寻找最近邻:

基于用户的协同过滤

这会计算邻居 u 的邻域 N ⊆ U:

  • 完全正相关 = 1

  • 完全负相关 = -1

Pearson 相关系数的缺点在于,即使两个用户之间有很少的共同评分,它也可能将他们显示为相似。为了解决这个问题,我们可以对两个用户都评分的项目应用一个阈值。

余弦相似度:这是一种找到相似用户的另一种方法。它采用了不同于 Pearson 相关系数的方法。

与使用统计方法的 Pearson 相关系数不同,余弦相似度采用的是向量空间方法。在这种方法中,用户不是矩阵的一部分,而是由|I|维向量表示。

为了衡量两个用户(向量)之间的相似度,它计算余弦距离。这是向量的点积,并将其除以它们的 L2(欧几里得)范数的乘积。

基于用户的协同过滤

当某个项目没有评分时,点积为 0,并且它会被忽略。

现在,为了生成推荐,我们使用:

基于用户的协同过滤

它计算邻居的加权平均值,其中相似度作为权重。这是最常用的方法。

以下是基于用户的协同过滤法的一些缺点:

  • 稀疏性:所形成的矩阵通常非常稀疏,并且随着用户和项目数量的增加,稀疏性会增加。

  • 找到最近邻并做出推荐并不总是那么容易。

  • 它的可扩展性较差,并且随着用户和项目数量的增加,计算负担加重。

  • 稀疏矩阵可能无法预测实际的志同道合的人群集合。

项目-项目协同过滤

用户协同过滤法存在一些缺点,其中之一是可扩展性问题。为了找到最近邻,我们需要计算邻居之间的相似度,这涉及大量的计算。当用户数量达到数百万时,由于计算能力需求过高,用户协同过滤法可能无法应用。

因此,为了实现所需的可扩展性,采用了项目-项目协同过滤,而不是用户协同过滤。它会寻找一些相似的模式,比如同一组用户喜欢某些项目而不喜欢其他项目,然后这些用户被视为志同道合,进而推荐项目。

仍然需要通过使用 k-最近邻或类似算法,在项目集合中找到相似的项目。

假设有一个用户给了一些项目评分。几天后,该用户重新访问这些项目并修改他们的评分。通过修改评分,用户实际上是进入了另一个邻居。

因此,不建议始终预计算矩阵或寻找最近邻。这通常在实际需要推荐时完成。

基于项目的协同过滤算法

  • 对于(i=1 到 I),其中I指所有可用项目:

    • 对于每个顾客x,他评分了I

      • 对于同一顾客x购买的每个项目K
  • 保存顾客x购买了项目IK

    • 对于每个项目K

      • 计算iK之间的相似度。

这个特定的相似度是通过与用户协同过滤中使用的相同方法计算的:

  • 基于余弦的相似度

  • 基于相关性的相似度

使用加权平均值来生成推荐。

S表示与i相似的项目集合,那么可以做出预测。定义基于项目的协同过滤的公式如下:

基于项目的协同过滤算法

对于邻居数k,你可能已经评分了一些被考虑在内的项目。

构建电影推荐系统

数据集由"GroupLens 研究"维护,并且可以在grouplens.org/datasets/movielens/免费获取。

我们将处理包含 2000 万个评分的数据库(ml-20m.zip)。该数据库包含:

  • 2000 万个评分

  • 465,000 个标签应用被 138,000 个用户应用到 27,000 部电影。

我们将使用 ALS 推荐器,它是一种矩阵分解算法,采用加权λ正则化交替最小二乘法ALS-WR)。

假设我们有一个包含用户u和项目i的矩阵:

*Matrix, M (ui) = { r (if item i is rated by the user, u)*
*0 (if item i is not rated by user, u) }*

这里,r表示提交的评分。

假设我们有m个用户和n个电影。

构建电影推荐系统

对于m个用户和n个电影,我们创建一个用户与电影的矩阵(mn*)。

推荐是为任何用户-电影对生成的,如下所示:

(i,j),rij=ui⋅mj,∀i,j

这里,(i,j)表示用户-电影对。

Julia 有一个名为RecSys.jl的包,由 Abhijith Chandraprabhu 创建(github.com/abhijithch)。可以按如下方式安装此包:

Pkg.update() 
Pkg.clone("https://github.com/abhijithch/RecSys.jl.git") 

我们将以并行模式启动 Julia:

julia -p <number of worker processes> 

由于我们将处理一个庞大的数据集,建议并行启动 Julia 进程。

在示例部分有movielens.jl。我们将使用它为我们生成推荐。

将其保存在可以调用的目录中,并使用任何文本编辑器(如 Atom(Juno)、Sublime、Vim 等)打开:

using RecSys 

import RecSys: train, recommend, rmse 

if isless(Base.VERSION, v"0.5.0-") 
    using SparseVectors 
end 

本示例将使用RecSys包,我们正在导入方法trainrecommendrmse

type MovieRec 
    movie_names::FileSpec 
    als::ALSWR 
    movie_mat::Nullable{SparseVector{AbstractString,Int64}} 

    function MovieRec(trainingset::FileSpec, movie_names::FileSpec) 
        new(movie_names, ALSWR(trainingset, ParShmem()), nothing) 
    end 

    function MovieRec(trainingset::FileSpec, movie_names::FileSpec,
    thread::Bool)  
   new(movie_names, ALSWR(trainingset, ParThread()), nothing) 
    end      

    function MovieRec(user_item_ratings::FileSpec,
    item_user_ratings::FileSpec, movie_names::FileSpec) 
        new(movie_names, ALSWR(user_item_ratings, item_user_ratings,
        ParBlob()), nothing) 
    end 
end 

这创建了一个复合类型,它是一个由命名字段组成的集合,可以将其实例视为单个值。这是一个用户定义的数据类型。

这个用户定义的数据类型有三个字段和三个方法。字段als属于类型 ALSWR,该类型定义在 RecSys 中。

该函数使用多重调度处理不同类型的输入,用户可以提供这些输入:

  • trainingsetmovie_names

  • trainingsetmovie_namesthread

  • user_item_ratingsitem_user_ratingsmovie_names

function movie_names(rec::MovieRec) 
    if isnull(rec.movie_mat) 
        A = read_input(rec.movie_names) 
        movie_ids = convert(Array{Int}, A[:,1]) 
        movie_names = convert(Array{AbstractString}, A[:,2]) 
        movie_genres = convert(Array{AbstractString}, A[:,3]) 
        movies = AbstractString[n*" - "*g for (n,g) in
        zip(movie_names, movie_genres)] 
        M = SparseVector(maximum(movie_ids), movie_ids, movies) 
        rec.movie_mat = Nullable(M) 
    end 

    get(rec.movie_mat) 
end 

这创建了一个名为 movie_names 的函数,它与 movielens 数据集协作,处理 CSV 文件中的数据类型和缺失值,我们使用该文件作为推荐系统的输入。

现在,为了训练系统,我们将使用 train 函数:

train(als, num_iterations, num_factors, lambda)

在这种特定情况下,我们将按如下操作:

train(movierec::MovieRec, args...) = train(movierec.als, args...)

这将使用 ALS 对 movielens 数据集进行模型训练:

rmse(movierec::MovieRec, args...; kwargs...) = rmse(movierec.als, args...; kwargs...) 

我们还可以启动对将产生的推荐进行测试:

rmse(als, testdataset) 

要开始推荐,按如下步骤操作:

recommend(movierec::MovieRec, args...; kwargs...) = recommend(movierec.als, args...; kwargs...) 

function print_recommendations(rec::MovieRec, recommended::Vector{Int}, watched::Vector{Int}, nexcl::Int) 
    mnames = movie_names(rec) 

    print_list(mnames, watched, "Already watched:") 
    (nexcl == 0) || println("Excluded $(nexcl) movies already watched") 
    print_list(mnames, recommended, "Recommended:") 
    nothing 
end 

这将在屏幕上打印推荐结果。

我收到的推荐是:

[96030] Weekend It Lives, The (Ax 'Em) (1992) - Horror 
[96255] On Top of the Whale (Het dak van de Walvis) (1982) - Fantasy 
[104576] Seasoning House, The (2012) - Horror|Thriller 
[92948] Film About a Woman Who... (1974) - Drama 
[6085] Neil Young: Human Highway (1982) - Comedy|Drama 
[94146] Flower in Hell (Jiokhwa) (1958) - Crime|Drama 
[92083] Zen (2009) - Drama 
[110603] God's Not Dead (2014) - Drama 
[105040] Dragon Day (2013) - Drama|Sci-Fi|Thriller 
[80158] Cartoon All-Stars to the Rescue (1990) - Animation|Children|Comedy|Drama|Fantasy 

我们将调用 test 函数生成推荐:

function test(dataset_path) 
    ratings_file = DlmFile(joinpath(dataset_path, "ratings.csv");
    dlm=',', header=true) 
    movies_file = DlmFile(joinpath(dataset_path, "movies.csv");
    dlm=',', header=true) 
    rec = MovieRec(ratings_file, movies_file) 
    @time train(rec, 10, 10) 

    err = rmse(rec) 
    println("rmse of the model: $err") 

    println("recommending existing user:") 
    print_recommendations(rec, recommend(rec, 100)...) 

    println("recommending anonymous user:") 
    u_idmap = RecSys.user_idmap(rec.als.inp) 
    i_idmap = RecSys.item_idmap(rec.als.inp) 
    # take user 100 
    actual_user = isempty(u_idmap) ? 100 : findfirst(u_idmap, 100) 
    rated_anon, ratings_anon = RecSys.items_and_ratings(rec.als.inp,
    actual_user) 
    actual_movie_ids = isempty(i_idmap) ? rated_anon : i_idmap[rated_anon] 
    nmovies = isempty(i_idmap) ? RecSys.nitems(rec.als.inp) :
    maximum(i_idmap) 
    sp_ratings_anon = SparseVector(nmovies, actual_movie_ids,
    ratings_anon) 
    print_recommendations(rec, recommend(rec, sp_ratings_anon)...) 

    println("saving model to model.sav") 
    clear(rec.als) 
    localize!(rec.als) 
    save(rec, "model.sav") 
    nothing 
end 

  • 这个函数将数据集路径作为参数。在这里,我们将提供提取自 ml-20m.zip 的目录路径,该文件我们从 grouplens 下载。

  • 它接受评分文件和电影文件,并创建一个类型为 MovieRec 的对象 "rec",这是我们之前创建的。

  • 我们将对象传递给 rmse 以找出误差。

  • 它调用 print_recommendations,该函数会调用推荐函数为现有用户生成推荐。

  • 它会保存模型以供以后使用。

总结

在本章中,我们学习了什么是推荐引擎,以及它们对企业的重要性,此外,我们还了解了它们为客户提供的价值。我们讨论了关联规则挖掘和市场篮子分析,了解了这一简单方法在行业中的应用。接着,我们探讨了基于内容的过滤及其优缺点。随后,我们讨论了协同过滤以及协同过滤的不同类型,包括基于用户的协同过滤和基于项目的协同过滤。基于用户的协同过滤的目标是找到与目标用户过去的评分或行为相似的用户,而基于项目的协同过滤则通过分析项目评分中的模式来找到志同道合的用户并推荐项目。

第十一章 深度学习简介

创新者一直渴望创造能够思考的机器。当可编程计算机首次被设想时,人们就已经在思考它们是否能够变得聪明,这比计算机的实际诞生早了一百多年(1842 年由洛夫莱斯提出)。

今天,人工智能AI)是一个蓬勃发展的领域,拥有众多实际应用和充满活力的研究方向。我们期望智能程序能够自动化日常工作、处理图像和音频并从中提取意义、自动化多种疾病的诊断等。

起初,随着人工智能(AI)的发展,该领域处理和解决了那些对人类来说在心理上较为困难,但对计算机来说却相对简单的问题。这些问题可以通过一套正式的、数学的原则来描述。人工智能的真正挑战变成了解决那些对人类来说容易执行,但对于计算机来说却很难正式描述的任务。这些任务我们通常是自然地解释的,例如人类理解语言(和讽刺)的能力,以及我们识别图像,尤其是面孔的能力。

这种方法是让计算机通过积累经验进行学习,并通过一系列的事实链条或树状结构来理解世界,每个事实都通过与更简单事实的关联来定义。通过理解这些事实,这种方法避免了需要人工管理者正式指定计算机所需的所有信息。

事实的渐进系统使计算机能够通过将复杂的概念构建为更简单的概念来学习复杂的思想。如果我们画出一个图表,表示这些概念是如何相互依赖的,那么这个图表将是深刻的,并且包含许多层次。因此,我们称这种方法为深度学习。

人工智能的早期成就发生在相对封闭和正式的环境中,当时计算机并不需要太多关于世界的知识。让我们来看一个例子:

  • IBM 的深蓝(Deep Blue)国际象棋框架在 1997 年击败了当时的世界冠军加里·卡斯帕罗夫(Gary Kasparov)。

我们还应该考虑以下因素:

  • 国际象棋显然是一个极其简单的世界。

  • 它仅包含 64 个方块和 32 个元素,这些元素只能按照预定义的方式移动。

  • 尽管构思一个成功的国际象棋系统是一项巨大的成就,但这个挑战并不在于如何将国际象棋元素的排列和可行的走法描述给计算机。

  • 国际象棋可以完全通过一套极其简短的、完全正式的规则来描述,这些规则可以很容易地由程序员预先给出。

计算机在某些任务上表现优于人类,而在其他任务上则表现较差:

  • 对人类来说,抽象任务是最具挑战性的心理工作之一,而对计算机来说却是最简单的。计算机更适合处理此类任务。

    • 一个例子是执行复杂的数学任务。
  • 主观和自然的任务由普通人比计算机更好地完成。

    • 人类的日常生活需要大量关于世界的信息。

    • 其中很多知识是主观的和自然的,因此很难以正式的方式表达。

    • 计算机也需要捕捉这些信息,以便做出明智的决策。人工智能的一个关键挑战是如何将这种非正式的学习传递到计算机中。

一些人工智能项目曾尝试在形式化语言中对世界的知识进行硬编码。计算机可以通过在这些形式化语言中进行推理,从而使用合乎逻辑的推理规则。这被称为基于知识库的人工智能方法。然而,这些尝试并未取得显著的成功。

依赖硬编码信息的系统所面临的挑战表明,人工智能系统需要能够获取自己的知识,通过从原始数据中提取模式。这就是我们在前几章中学习过的机器学习。

这些简单的机器学习算法的性能在很大程度上依赖于它们所接收到的数据信息的表示。

例如,当逻辑回归用于预测未来天气时,人工智能系统并不会直接考虑患者:

  • 专家会向系统提供一些重要的信息,例如温度变化、风向和风速、湿度等。

  • 包含在天气表示中的每一项数据都被称为特征。逻辑回归会了解这些天气特征如何与不同季节或其他地区的天气相关。

  • 然而,它无法以任何方式影响特征的定义。

解决这个问题的一个方法是利用机器,找出从表示到结果的映射方式以及表示本身。这种方法被称为表示学习。学习到的表示通常能带来比手工设计的表示更优的性能。它们还使得人工智能系统能够快速适应新任务,且几乎不需要人类干预。

一种表示学习算法可以在几分钟内为简单任务找到合适的特征集合,或为复杂任务找到特征集合,这可能需要数小时到数个月。为复杂任务手动设计特征需要大量的人力时间和精力,而计算机则大大减少了这一过程。

在本章中,我们将涉及多个主题,首先是基本介绍:

  • 基础知识

  • 机器学习与深度学习的区别

  • 什么是深度学习?

  • 深度前馈网络

  • 单层和多层神经网络

  • 卷积网络

  • 实用的方法论与应用

重温线性代数

线性代数是数学中广泛使用的一个分支。线性代数是离散数学的一部分,而不是连续数学的一部分。要理解机器学习和深度学习模型,需要有良好的基础理解。我们只会复习数学对象。

标量的要点

标量仅是一个单独的数字(与线性代数中讨论的大多数对象不同,后者通常是不同数字的数组)。

向量的简要概述

向量是一个有序的数字集合或数组。我们可以通过该列表中的索引来识别每个单独的数字。例如:

x = [x1, x2, x3, x4 ..... xn]

  • 向量也可以被看作是空间中点的标识。

  • 每个元素代表沿不同轴的坐标值。

  • 我们还可以对这些值在向量中的位置进行索引。因此,更容易访问数组的特定值。

矩阵的重要性

  • 矩阵是一个二维的数字数组。

  • 每个元素由两个索引标识,而不仅仅是一个。

  • 例如,二维空间中的一个点可以表示为(3,4)。这意味着该点在 x 轴上是 3 个单位,在 y 轴上是 4 个单位。

  • 我们也可以拥有类似[(3,4), (2,4), (1,0)]的数字数组。这样的数组称为矩阵。

什么是张量?

如果需要多于两维(矩阵),我们则使用张量。

这是一个没有定义轴数的数字数组。

这些对象的结构如下:T (x, y, z)

[(1,3,5), (11,12,23), (34,32,1)]

概率与信息理论

概率理论是一种用于表示不确定性命题的科学体系。它提供了一种评估不确定性的方法,并为推导新的不确定性陈述提供了准则。

在 AI 应用中,我们使用概率理论的方式如下:

  • 概率法则定义了 AI 系统应如何推理,因此算法被设计用来计算或近似基于概率理论推导出的不同表达式。

  • 概率和统计可以用来假设性地分析提议的 AI 系统的行为。

虽然概率理论允许我们提出不确定的表达式并在不确定的视野中进行推理,但数据使我们能够衡量概率分布中的不确定性程度。

为什么选择概率?

与主要依赖计算机系统确定性特性的其他计算机科学分支不同,机器学习大规模利用概率理论:

  • 这是因为机器学习必须始终处理不确定的量。

  • 有时候也可能需要处理随机(非确定性)量。不确定性和随机性可能来自多个来源。

所有活动都需要在不确定性的面前进行推理。实际上,通过过去的数学推理,既然它们是定义上有效的,我们很难想到任何完全有效的建议,或任何完全能够保证发生的事件。

不确定性有三种可能的来源:

  • 模型框架中存在的随机性。

    • 例如,在玩扑克牌游戏时,我们假设牌是以完全随机的方式洗牌的。
  • 片段化的可观察性。当大部分驱动系统行为的变量无法被观察到时,即使是确定性系统也可能看起来是随机的。

    • 例如,在一个带有多个选择题答案的考试中,一个选项是正确答案,而其他选项将导致错误的结果。给定挑战者的选择,结果是确定性的,但从候选人的角度看,结果是不可确定的。
  • 片段化建模。当我们使用一个模型,而必须丢弃我们已经观察到的一部分数据时,丢弃的数据会导致模型预测的不稳定。

    • 例如,假设我们制造了一个能够准确观察到周围每个物体位置的机器人。如果机器人在预测这些物体的未来位置时将空间离散化,那么离散化会使得机器人迅速变得不确定物体的确切位置:每个物体可能出现在它被看到的离散单元中的任何地方。

概率可以看作是将逻辑扩展到处理不确定性的方式。逻辑提供了一套形式化的规则,用于计算在假设某些其他建议为真或假时,哪些结论可以被推断为真或假。

概率理论提供了一套形式化的规则,用于根据不同建议的概率来确定某个建议为真或假的概率。

机器学习与深度学习的区别

机器学习和深度学习旨在实现相同的目标,但它们是不同的,代表着不同的思维方式。机器学习是两者中最主要的一种,科学家和数学家们已经研究它几十年了。深度学习是一个相对较新的概念。深度学习基于通过神经网络(多个层级)来学习以实现目标。理解两者之间的区别非常重要,这有助于我们知道在何种情况下应该应用深度学习,哪些问题可以通过机器学习解决。

已知通过利用仅依赖于领域和决定目标的信息,可以构建一种更强大的模式识别算法,该信息可以轻松挖掘。

例如,在图像识别中,我们积累了各种图片并在此基础上扩展算法。利用这些图片中的信息,我们的模型可以被训练来识别生物、人的外貌或其他模式。

机器学习与其他领域相关,现在它不仅仅局限于图像或字符识别。目前,它在机器人技术、金融市场、自动驾驶汽车和基因组分析等领域得到了广泛应用。我们在之前的章节中学习了机器学习,现在我们可以进一步了解它与深度学习的不同之处。

什么是深度学习?

深度学习在 2006 年开始变得流行,也被称为层次学习。它的应用广泛,极大地扩展了人工智能和机器学习的范围。社区对深度学习的兴趣巨大。

深度学习指的是一类机器学习技术,具体包括:

  • 执行无监督或监督的特征提取。

  • 通过利用多个非线性信息处理层,执行模式分析或分类。

它由一系列特征或因素构成。在这个层次结构中,低层特征有助于定义高层特征。人工神经网络通常用于深度学习。

  • 传统的机器学习模型学习模式或聚类。深度神经网络通过极少的步骤学习计算。

  • 一般来说,神经网络越深,其能力就越强大。

  • 神经网络会根据新提供的数据进行更新。

  • 人工神经网络具有容错性,这意味着如果网络的某些部分被破坏,可能会影响网络的性能,但网络的关键功能仍可能得以保留。

  • 深度学习算法学习多层次的表示,并行执行计算,这些计算的复杂性可能不断增加。

如果我们快速推进到今天,大家普遍对现在许多人称之为深度学习的技术充满热情。最著名的深度学习模型,特别是在大规模图像识别任务中应用的,是卷积神经网络,简称 ConvNets。

深度学习强调我们需要使用的模型类型(例如深度卷积多层神经网络),以及我们可以利用数据来填补缺失的参数。

深度学习带来了巨大的责任。因为我们从一个具有高维度的世界模型开始,我们实际上需要大量的数据,也就是我们所说的“大数据”,并且需要相当大的计算能力(通用 GPU/高性能计算)。卷积在深度学习中被广泛使用(特别是在计算机视觉应用中)。

什么是深度学习?

在前面的图像中,我们看到了三层:

  • 输出层:在这里预测一个监督目标

  • 隐藏层:中间函数的抽象表示

  • 输入层:原始输入

人工模拟神经元代表了多层人工神经网络的构建模块。基本的思想是模拟人类大脑以及它如何解决复杂问题。制造神经网络的主要思想是基于这些理论和模型。

在过去几十年里,深度学习算法取得了许多重要进展。这些进展可以用来从无标签数据中提取特征指标,还可以预训练深度神经网络,这些神经网络由多个层次组成。

神经网络是学术研究中的一个有趣问题,也是在大型科技公司中至关重要的领域,例如 Facebook、Microsoft 和 Google 等公司,正在大力投资于深度学习研究。

由深度学习算法驱动的复杂神经网络被认为是解决重大问题的最先进技术。例如:

  • 谷歌图像搜索:我们可以使用谷歌图像搜索工具在互联网上搜索图片。这可以通过上传图片或提供图片的 URL 来搜索。

  • 谷歌翻译:这个工具可以读取图片中的文本,并理解语音,进行翻译或解释多种语言的含义。

另一个非常著名的应用是自动驾驶汽车,谷歌或特斯拉所创造的。它们由深度学习驱动,能够实时找到最佳路径,穿越交通,并执行必要的任务,像是由人类司机驾驶时一样。

深度前馈网络

深度前馈网络是最著名的深度学习模型。这些也被称为以下几种:

  • 前馈神经网络。

  • 多层感知器MLPs

深度前馈网络

前馈神经网络的目标是通过其参数进行学习,并定义一个映射到输出 y 的函数:

y = f(x, theta)

正如图中所示,前馈神经网络之所以叫做前馈网络,是因为它们的数据流向是单向的。它从 x 开始,通过函数进行中间计算,生成 y

当这些系统还包括与上一层的连接(反馈)时,它们被称为递归神经网络。

前馈系统对机器学习专家至关重要。它们构成了许多重要商业应用的基础。例如,用于语音自然语言处理的卷积网络就是一种特定类型的前馈系统。

前馈系统是通往递归网络的合理垫脚石。这些系统在自然语言应用中有许多用途。前馈神经网络被称为网络,因为它们通过将多个不同的函数组合在一起来表示。该模型与一个有向无环图相连,描述了函数是如何组合在一起的。

例如,我们有三个函数——f(1)f(2)f(3)

它们按如下方式链接或关联在一起:

f(x) = f(3)(f(2)(f(1)(x)))

这些链式结构是神经网络中最常用的结构。在这种情况下:

  • f(1) 被称为网络的第一层。

  • f(2) 被称为第二层,依此类推。

  • 链的总长度决定了模型的深度。正是从这个术语中,"深度学习" 这个名称产生。

  • 前馈网络的最终层被称为输出层或结果层。

在神经网络训练过程中,我们遵循以下步骤:

  1. 驱动 f(x)f∗(x) 一致。训练数据包含噪声和不准确的数据 off ∗(x),这些数据是在不同的训练集上评估的。

  2. 每个* x *的示例都由标签 y ≈ f∗(x)关联。

  3. 训练案例直接决定了每个 x 点上输出层应该做什么。也就是说,它必须生成一个接近 y 的值。

理解神经网络中的隐藏层

其他层的行为并未由训练数据直接指定。学习算法必须选择如何利用这些层来生成期望的输出,但训练数据并没有说明每一层应该做什么。

相反,必须由学习算法来选择如何利用这些层以最佳方式执行估计 off ∗。由于训练数据没有显示每一层的期望输出,这些层被称为隐藏层。

神经网络的动机

  • 这些系统之所以被称为神经网络,是因为它们在某种程度上受到神经科学的启发。

  • 系统的每一隐藏层通常都是向量值的。

  • 这些隐藏层的 y 维度决定了模型的宽度。

  • 向量的每个分量可以理解为承担类似神经元的角色。

  • 与其将该层视为展示单一的向量到向量函数,不如认为该层由许多单元组成,这些单元并行工作,每个单元展示一个向量到标量的函数。

  • 每个单元看起来像一个神经元,因为它从许多不同的单元获取贡献并注册其自身的激活值。

  • 使用多个向量值表示的层是受到神经科学的启发。

  • 用来确定这些表示的函数 f(i)(x) 的选择在某种程度上是受到神经科学观察的指导,这些观察关注有机神经元处理的功能。

我们在前几章中研究了正则化。现在让我们研究为什么这对深度学习模型至关重要。

理解正则化。

机器学习中的主要问题是如何构建一个能够在训练数据和新输入上都表现良好的算法。机器学习中使用的许多技术特别旨在减少测试误差,可能以增加训练误差为代价。这些技术统称为正则化。

深度学习专家可以使用许多种正则化方法。更有效的正则化策略一直是该领域研究的重点之一。

有许多正则化策略。

  • 机器学习模型的额外约束。

    • 例如,包含对参数值的约束。
  • 目标函数中的附加项可以视为与参数值的精细要求进行比较。

  • 如果策略得当且谨慎,这些附加要求和约束可以在测试数据上带来更好的性能。

  • 这些约束和限制也可以用来编码特定类型的先前学习。

  • 这些约束和限制也可以导致模型的泛化。

  • 集成方法也使用正则化来生成更好的结果。

关于深度学习,大多数正则化程序依赖于正则化估计器。为了调节估计器:

  • 我们需要交换增加的偏差以减少方差。

  • 一个有效的正则化器是能够做出有利交换的,这意味着它显著减少了方差,同时不会过度增加偏差。

在过拟合和泛化中,我们专注于训练模型时遇到的这些情况:

  • 避免关于生成过程的真实信息,以考虑过拟合并引入偏差。

  • 包含关于生成过程的真实信息。

  • 包含关于生成过程的信息,并且额外包含关于生成过程的其他众多信息,以考虑过拟合,其中方差而非偏差主导了估计误差。

正则化的目标是将模型带入提到的第二个过程。

过于复杂的模型家族并不一定包含目标函数或真实的数据生成过程。然而,大多数深度学习算法的应用场景是,真实的数据生成过程很可能超出了模型家族的范围。深度学习算法通常与复杂的应用场景密切相关,如图像识别、语音识别、自动驾驶汽车等。

这意味着,控制模型复杂性不仅仅是找到一个适当大小且具有正确参数集的模型。

优化深度学习模型。

优化方法在设计算法中至关重要,用于从海量数据中提取所需的知识。深度学习是一个快速发展的领域,新的优化技术不断涌现。

深度学习算法在许多关联中包含优化。例如,在像 PCA 这样的模型中执行推断,涉及解决优化问题。

我们通常使用诊断优化来编写验证或配置计算。深度学习中许多优化问题中,最难的就是训练神经网络。

在许多机器上花费数天甚至数月的时间,以解决神经网络训练问题的单一案例,这种情况非常常见。由于这个问题如此关键且昂贵,已提出了一系列优化策略来改进它。

优化的案例

为了找到神经网络的参数θ,从而显著减少成本函数 J(θ),通常需要评估整个训练集的执行度量,并可能包含额外的正则化项。

用作机器学习任务训练算法的优化与纯粹的优化不同。更复杂的算法在训练过程中会调整其学习率,或影响包含在成本函数二阶导数中的数据。最后,一些优化方法是通过将基本的优化算法组合成更高级的策略而产生的。

用于训练深度学习模型的优化算法与传统优化算法在几个方面有所不同:

  • 机器学习通常是间接进行的。在大多数机器学习场景中,我们考虑某个执行度量P,该度量在测试集上定义,并且可能是顽固的。因此,我们间接地优化P。我们减少另一个成本函数J(θ),期望通过这样做来改善P

这与纯粹的优化不同,在纯粹优化中,最小化J本身就是目标。用于训练深度学习模型的优化算法通常还会针对机器学习目标函数的具体结构进行一些专门化。

在 Julia 中的实现

有许多好的、经过测试的深度学习库,适用于流行的编程语言:

  • Theano(Python)可以同时使用 CPU 和 GPU(来自蒙特利尔大学的 MILA 实验室)

  • Torch(Lua)是一个类似 Matlab 的环境(来自 Ronan Collobert、Clement Farabet 和 Koray Kavukcuoglu)

  • Tensorflow(Python)利用数据流图

  • MXNet(Python、R、Julia、C++)

  • Caffe 是最流行且广泛使用的

  • Keras(Python)基于 Theano

  • Mocha(Julia)由张启源编写

我们将主要介绍 Mocha for Julia,这是一个由麻省理工学院博士生张启源编写的令人惊叹的包。

首先,按如下方式添加包:

Pkg.update() 
Pkg.add("Mocha") 

网络架构

Mocha 中的网络架构指的是一组层:

data_layer = HDF5DataLayer(name="data", source="data-list.txt", batch_size=64, tops=[:data]) 
ip_layer   = InnerProductLayer(name="ip", output_dim=500, tops=[:ip], bottoms=[:data]) 

  • ip_layer 的输入与 data_layer 的输出具有相同的名称

  • 相同的名称将它们连接起来

Mocha 对一组层执行拓扑排序

层的类型

  • 数据层

    • 这些层从源读取信息并将其传递给顶层
  • 计算层

    • 这些层从基础层接收输入流,进行计算,并将生成的结果反馈给顶层
  • 损失层

    • 这些层从基础层接收处理过的结果(以及真实标签)并计算标量损失值

    • 来自网络中所有层和正则化器的损失值被纳入,以表征网络的最终损失函数

    • 反向传播中的网络参数通过损失函数的帮助进行训练

  • 统计层

    • 这些层从基础层接收信息,并生成有价值的见解,如分类准确性

    • 见解在多个迭代过程中收集

    • reset_statistics 可用于明确重置统计汇总

  • 工具层

神经元(激活函数)

让我们了解真正的神经网络(大脑)。神经科学是研究大脑功能的学科,并为我们提供了关于大脑如何工作的有力证据。神经元是大脑的真实信息存储单元。理解它们的连接强度,即一个神经元如何强烈地影响与其连接的神经元,也是非常重要的。

学习或任务的重复以及对新的刺激过程或环境的暴露,通常会导致大脑活动,实际上是神经元根据接收到的新数据做出反应。

神经元,因而大脑,在面对不同的刺激和环境时表现得非常不同。它们在某些情境下的反应或激动程度可能比其他情境更加明显。

理解这一点对了解人工神经网络非常重要:

  • 神经元可以连接到任何层

  • 每一层的神经元都会影响前向传递中的输出以及反向传播中的梯度,除非它是一个身份神经元

  • 默认情况下,层具有身份神经元

让我们了解一下可以用来构建网络的各种神经元类型:

  • class Neurons.Identity

    • 这是一种激活函数,其输入不发生变化。
  • class Neurons.ReLU

    • 修正线性单元。在前向传递过程中,它将所有小于某个限制 ϵ(通常是 0)的约束限制在该值之下。

    • 它逐点处理 y=max(ϵ,x)

  • class Neurons.LreLU

    • 泄漏修正线性单元。Leaky ReLU 可以解决“死亡 ReLU”问题。

    • 如果足够大的梯度改变权重,使得神经元在新信息上永远不被激活,ReLU 会“死亡”。

  • class Neurons.Sigmoid

    • Sigmoid 是一种平滑的阶跃函数

    • 它对于绝对值极大的负信息输出大约为 0,对于极大的正输入输出约为 1

    • 点对点方程是 y=1/(1+e−x)y=1/(1+e−x)

  • class Neurons.Tanh

    • Tanh 是 Sigmoid 的一种变种

    • 它的取值为 ±1±1,而不是单位间隔。

    • 点对点方程是 y=(1−e−2x)/(1+e−2x)

神经元(激活函数)

理解 ANN 的正则化方法

我们在前面的章节中研究了正则化方法。正则化方法包括对网络参数的附加惩罚或限制,以限制模型的复杂度。在一个流行的深度学习框架中,Caffe,它被称为衰减(decay)。

权重衰减和正则化在反向传播中是可以比较的。前向传递中的理论对比在于,当被看作权重衰减时,它们不被视为目标函数的一部分。

默认情况下,Mocha 同样会删除正则化器的前向计算,目的是减少计算量。我们使用“正则化”这个术语,而不是“权重衰减”,因为它更容易理解。

  • NoRegu: 无正则化

  • L2Regu: L2 正则化器

  • L1Regu: L1 正则化器

范数约束

范数限制是一种通过在每个 n 周期中明确收缩参数来直接限制模型复杂度的方法,如果参数的标准或范数超过给定阈值。

  • NoCons: 无约束

  • L2Cons: 限制参数的欧几里得范数。阈值和收缩应用于每个参数。特别是,阈值应用于卷积层的滤波器参数的每个滤波器。

在深度神经网络中使用求解器

Mocha 包含广泛实用的随机(子)梯度优化求解器。这些求解器可以用来训练深度神经网络。

求解器通过指明一个求解器参数词汇表来开发,提供了重要的配置:

  • 一般设置,如停止条件

  • 特定计算的参数

此外,通常建议在训练迭代之间休息片刻,以打印进度或保存快照。这些在 Mocha 中被称为“咖啡休息”。

求解器算法

  • SGD: 带动量的随机梯度下降。

    • lr_policy: 学习率策略。

    • mom_policy: 动量策略。

  • Nesterov: 随机 Nesterov 加速梯度方法。

    • lr_policy: 学习率策略。

    • mom_policy: 动量策略。

  • Adam: 随机优化方法

    • lr_policy: 学习率策略。

    • beta1: 一阶矩估计的指数衰减因子。0<=beta1<1,默认 0.9

    • beta2: 二阶矩估计的指数衰减因子,0<=beta2<1,默认 0.999

    • epsilon: 影响参数更新的缩放,用于数值条件化,默认 1e-8

咖啡休息

训练可能会变成一个非常计算密集的多次迭代过程。通常建议在训练迭代之间适当休息,打印进度或保存快照。这些被称为 Mocha 中的咖啡休息。它们的执行方式如下:

# report training progress every 1000 iterations 
add_coffee_break(solver, TrainingSummary(), every_n_iter=1000) 

# save snapshots every 5000 iterations 
add_coffee_break(solver, Snapshot(exp_dir), every_n_iter=5000) 

每 1,000 次迭代打印一次训练摘要,并每 5,000 次迭代保存一次快照。

使用预训练的 Imagenet CNN 进行图像分类。

MNIST 是一个手写数字识别数据集,包含以下内容:

  • 60,000 个训练样本。

  • 10,000 个测试样本。

  • 28 x 28 单通道灰度图像。

我们可以使用get-mnist.sh脚本来下载数据集。

它调用mnist.convert.jl将二进制数据集转换为 Mocha 可以读取的 HDF5 文件。

data/train.hdf5data/test.hdf5将在转换完成后生成。

我们在这里使用 Mocha 的本地扩展以加速卷积:

ENV["MOCHA_USE_NATIVE_EXT"] = "true" 

using Mocha 

backend = CPUBackend() 
init(backend) 

这配置 Mocha 使用本地后台而非 GPU(CUDA)。

现在,我们将继续定义网络结构。我们将从定义一个数据层开始,该数据层将读取 HDF5 文件。这将成为网络的输入。

source包含真实数据文件的列表:

data_layer  = HDF5DataLayer(name="train-data", source="data/train.txt", 
batch_size=64, shuffle=true) 

通过形成小批量来处理数据。随着批量大小的增加,方差减小,但会影响计算性能。

洗牌可以减少训练过程中顺序的影响。

现在我们将继续定义卷积层:

conv_layer = ConvolutionLayer(name="conv1", n_filter=20, kernel=(5,5), 
bottoms=[:data], tops=[:conv1]) 

  • name: 用于标识层的名称。

  • n_filter: 卷积滤波器的数量。

  • kernel: 滤波器的大小。

  • bottoms: 一个数组,用于定义输入的位置。(我们定义的 HDF5 数据层。)

  • tops: 卷积层的输出。

按照以下方式定义更多卷积层:

pool_layer = PoolingLayer(name="pool1", kernel=(2,2), stride=(2,2), 
    bottoms=[:conv1], tops=[:pool1]) 
conv2_layer = ConvolutionLayer(name="conv2", n_filter=50, kernel=(5,5), 
    bottoms=[:pool1], tops=[:conv2]) 
pool2_layer = PoolingLayer(name="pool2", kernel=(2,2), stride=(2,2), 
    bottoms=[:conv2], tops=[:pool2]) 

这些是卷积层和池化层后的两个全连接层。

创建该层的计算是输入与层权重之间的内积。这些也被称为InnerProductLayer

层的权重也会被学习,因此我们还为这两个层命名:

fc1_layer  = InnerProductLayer(name="ip1", output_dim=500, 
    neuron=Neurons.ReLU(), bottoms=[:pool2], tops=[:ip1]) 
fc2_layer  = InnerProductLayer(name="ip2", output_dim=10, 
    bottoms=[:ip1], tops=[:ip2]) 

最后的内积层的维度为 10,表示类别的数量(数字 0~9)。

这是 LeNet 的基本结构。为了训练这个网络,我们将通过添加一个损失层来定义一个损失函数:

loss_layer = SoftmaxLossLayer(name="loss", bottoms=[:ip2,:label]) 

我们现在可以构建我们的网络:

common_layers = [conv_layer, pool_layer, conv2_layer, pool2_layer, 
    fc1_layer, fc2_layer] 

net = Net("MNIST-train", backend, [data_layer, common_layers..., loss_layer]) 

使用随机梯度下降法训练神经网络的过程如下:

exp_dir = "snapshots" 
method = SGD() 

params = make_solver_parameters(method, max_iter=10000, regu_coef=0.0005, 
    mom_policy=MomPolicy.Fixed(0.9), 
    lr_policy=LRPolicy.Inv(0.01, 0.0001, 0.75), 
    load_from=exp_dir) 

solver = Solver(method, params) 

使用的参数如下:

  • max_iter: 这些是求解器将执行的最大迭代次数,用于训练网络。

  • regu_coef: 正则化系数。

  • mom_policy: 动量策略。

  • lr_policy: 学习率策略。

  • load_from: 在这里我们可以从文件或目录加载已保存的模型。

添加一些咖啡休息,如下所示:

setup_coffee_lounge(solver, save_into="$exp_dir/statistics.hdf5", every_n_iter=1000) 

add_coffee_break(solver, TrainingSummary(), every_n_iter=100) 

add_coffee_break(solver, Snapshot(exp_dir), every_n_iter=5000) 

性能会定期在单独的验证集上进行检查,以便我们能看到进展。我们拥有的验证数据集将用作测试数据集。

为了执行评估,定义一个新的网络,采用相同的架构,但数据层不同,它将从验证集获取输入:

data_layer_test = HDF5DataLayer(name="test-data", source="data/test.txt", batch_size=100) 

acc_layer = AccuracyLayer(name="test-accuracy", bottoms=[:ip2, :label]) 

test_net = Net("MNIST-test", backend, [data_layer_test, common_layers..., acc_layer]) 

添加一个咖啡休息,获取验证性能报告,具体如下:

add_coffee_break(solver, ValidationPerformance(test_net), every_n_iter=1000) 

最后,开始训练,具体如下:

solve(solver, net) 

destroy(net) 
destroy(test_net) 
shutdown(backend)  

这是我们创建的两个网络:

使用预训练的 Imagenet CNN 进行图像分类

现在我们在测试数据上运行生成的模型。我们得到了以下输出:

Correct label index: 5
Label probability vector:
Float32[5.870685e-6
0.00057068263
1.5419962e-5
8.387835e-7
0.99935246
5.5915066e-6
4.284061e-5
1.2896479e-6
4.2869314e-7
4.600691e-6]

总结

在本章中,我们学习了深度学习以及它与机器学习的不同。深度学习是指一类机器学习技术,通过利用多层非线性信息处理来执行无监督或监督的特征提取、模式分析或分类。

我们学习了深度前馈网络、正则化和优化深度学习模型。我们还学习了如何使用 Mocha 在 Julia 中创建一个神经网络来分类手写数字。

参考文献

posted @ 2025-10-08 11:37  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报