Python-应用深度学习-全-

Python 应用深度学习(全)

原文:annas-archive.org/md5/5ba333cb84d93dd1084633034e39693f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本学习路径采取逐步教学法,教你如何入门数据科学、机器学习和深度学习。每个模块都是在上一章节的基础上进行扩展的。本书包含多个演示,使用真实的商业场景,让你在高度相关的背景下练习并应用你所学的新技能。

在学习路径的第一部分,你将学习入门级的数据科学。你将了解常用的 Anaconda 发行版中的库,并利用真实数据集探索机器学习模型,培养你在实际工作中所需的技能和经验。

在第二部分,你将接触到神经网络和深度学习。接着,你将学习如何训练、评估和部署 TensorFlow 和 Keras 模型,将它们作为真实世界的 Web 应用。读完本书后,你将具备在深度学习环境中构建应用程序的知识,并能够创建复杂的数据可视化和预测。

本书适用人群

如果你是一个 Python 程序员,准备踏入数据科学的世界,那么这是一个正确的入门方式。对于有经验的开发者、分析师或数据科学家,想要使用 TensorFlow 和 Keras 时,这也是一个理想的选择。我们假设你已经熟悉 Python、Web 应用开发、Docker 命令,以及线性代数、概率和统计的基本概念。

本书内容概述

第一章Jupyter 基础,介绍了 Jupyter 中数据分析的基础知识。我们将从 Jupyter 的使用说明和功能开始,如魔法函数和自动补全功能。接着,我们将转向数据科学相关的内容。我们将在 Jupyter Notebook 中进行探索性分析,利用散点图、直方图和小提琴图等可视化工具,帮助我们更深入地理解数据。同时,我们还会进行简单的预测建模。

第二章数据清理与高级机器学习,展示了如何在 Jupyter Notebooks 中训练预测模型。我们将讨论如何规划机器学习策略。本章还解释了机器学习的术语,如监督学习、无监督学习、分类和回归。我们将讨论使用 scikit-learn 和 pandas 进行数据预处理的方法。

第三章网页抓取与交互式可视化,解释了如何抓取网页表格,并使用交互式可视化来研究数据。我们将从 HTTP 请求的工作原理开始,重点讲解 GET 请求及其响应状态码。然后,我们将进入 Jupyter Notebook,使用 Requests 库通过 Python 发起 HTTP 请求。我们将看到如何利用 Jupyter 渲染 HTML,并与实际的网页进行交互。在发出请求后,我们将学习如何使用 Beautiful Soup 解析 HTML 中的文本,并使用该库抓取表格数据。

第四章神经网络与深度学习简介,帮助你设置和配置深度学习环境,并开始查看单独的模型和案例研究。它还讨论了神经网络及其理念,以及它们的起源并探索它们的强大功能。

第五章模型架构,展示了如何使用深度学习模型预测比特币价格。

第六章模型评估与优化,介绍如何评估神经网络模型。我们将修改网络的超参数以提高其性能。

第七章产品化,解释如何将深度学习模型转换为可运行的应用程序。我们将部署我们的比特币预测模型,并创建一个新的模型来处理新的数据。

为了充分利用本书

本书最适合那些对数据分析感兴趣,并希望在使用 TensorFlow 和 Keras 开发应用程序领域提高知识的专业人士和学生。为了获得最佳体验,你应该具备编程基础知识,并有一定的 Python 使用经验。特别是,熟悉 Pandas、matplotlib 和 scikit-learn 等 Python 库将大有帮助。

下载示例代码文件

你可以从 www.packtpub.com 下载本书的示例代码文件。如果你是从其他地方购买本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接发送到你的邮箱。

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

  1. 登录或注册 www.packtpub.com

  2. 选择 SUPPORT 选项卡。

  3. 点击“代码下载 & 勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的提示操作。

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,链接为 github.com/TrainingByPackt/Applied-Deep-Learning-with-Python。如果代码有任何更新,将会在现有的 GitHub 仓库中进行更新。

我们还有其他来自丰富书籍和视频目录的代码包,均可在 github.com/TrainingByPackt/Applied-Deep-Learning-with-Python 查看!快来看看吧!

使用的规范

本书中使用了多种文本规范。

CodeInText:指示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号名。以下是一个例子:“我们可以看到NotebookApp在本地服务器上运行。”

代码块如下所示:

fig, ax = plt.subplots(1, 2)
sns.regplot('RM', 'MEDV', df, ax=ax[0],
scatter_kws={'alpha': 0.4}))
sns.regplot('LSTAT', 'MEDV', df, ax=ax[1],
scatter_kws={'alpha': 0.4}))

当我们希望特别强调某个代码块的部分时,相关的行或项目会用粗体显示:

    cat chapter-1/requirements.txt
    matplotlib==2.0.2
 numpy==1.13.1
 pandas==0.20.3
 requests==2.18.4

任何命令行输入或输出都以如下方式呈现:

pip install version_information 
pip install ipython-sql

粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的文字会像这样出现在文本中。以下是一个示例:“注意如何使用白色裙子的价格来填补缺失值。”

警告或重要提示会以这种方式呈现。

提示和技巧会以这种方式呈现。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:请通过电子邮件 feedback@packtpub.com 并在邮件主题中提及书籍标题。如果您对本书的任何内容有疑问,请通过 questions@packtpub.com 与我们联系。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感谢您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关详情。

盗版:如果您在互联网上发现任何非法复制的我们作品的形式,我们将非常感谢您提供相关地址或网站名称。请通过电子邮件 copyright@packtpub.com 与我们联系,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与书籍的创作,请访问 authors.packtpub.com

评论

请留下评论。阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并利用您的公正意见来做出购买决策,我们 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对其书籍的反馈。感谢您的支持!

如需了解更多关于 Packt 的信息,请访问 packtpub.com

第一章:Jupyter 基础

Jupyter Notebooks 是 Python 数据科学家使用的最重要工具之一。这是因为它们是开发可复现的数据分析管道的理想环境。数据可以在单一 Notebook 中加载、转换和建模,在这里可以快速简便地测试代码和探索想法。此外,所有这些都可以通过格式化文本进行"内联"文档记录,因此你可以为自己做笔记,甚至生成结构化报告。其他类似的平台,例如 RStudio 或 Spyder,向用户呈现多个窗口,这会导致繁琐的任务,比如复制粘贴代码和重新运行已执行的代码。这些工具通常涉及读取求值提示循环(REPLs),其中代码在一个具有保存内存的终端会话中运行。这种开发环境对于可复现性不好,也不适合开发。Jupyter Notebooks 通过提供一个单一窗口解决了所有这些问题,在这个窗口中,代码片段被执行,输出结果则内联显示。这使得用户可以高效地开发代码,并允许他们回顾以前的工作以供参考,甚至进行修改。

我们将通过解释什么是 Jupyter Notebook 开始本章,并继续讨论它为何在数据科学家中如此受欢迎。接着,我们将一起打开一个 Notebook,并通过一些练习学习如何使用该平台。最后,我们将在基本功能和特点一节中深入进行第一次分析,进行探索性分析。

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

  • 了解什么是 Jupyter Notebook,以及它为何对数据分析有用

  • 使用 Jupyter Notebook 功能

  • 学习 Python 数据科学库

  • 执行简单的探索性数据分析

本书中的所有代码都可以在代码包中的章节特定 IPython 笔记本中找到。本书中的所有彩色图表也都包含在代码包中。

基本功能和特点

在这一节中,我们首先通过示例和讨论展示 Jupyter Notebooks 的实用性。接着,为了涵盖 Jupyter Notebooks 的基础知识,我们将看到如何启动和与平台互动的基本用法。对于那些已经使用过 Jupyter Notebooks 的人来说,这部分内容大多是复习;不过,你一定也会在这一主题中看到新的内容。

什么是 Jupyter Notebook,它为何有用?

Jupyter Notebooks 是本地运行的 Web 应用程序,其中包含实时代码、公式、图形、交互式应用和 Markdown 文本。标准语言是 Python,这也是本书将使用的语言;然而,请注意,它也支持多种替代语言。其中包括另一个主要的数据科学语言 R:

熟悉 R 的人会知道 R Markdown。Markdown 文档允许将 Markdown 格式的文本与可执行代码结合使用。Markdown 是一种用于在网页上格式化文本的简单语言。例如,大多数 GitHub 仓库都有一个 README.md Markdown 文件。这个格式适用于基本的文本格式化。它与 HTML 相似,但允许的自定义程度要小得多。

Markdown 中常用的符号包括井号(#)用来将文本设置为标题,方括号和圆括号用来插入超链接,星号用来创建斜体或粗体文本:

在了解了 Markdown 的基本知识之后,让我们回到 R Markdown,Markdown 文本可以与可执行代码一起编写。Jupyter Notebooks 为 Python 提供了等效的功能,尽管正如我们将看到的,它们与 R Markdown 文档的功能有很大不同。例如,R Markdown 假设除非另行指定,否则你在编写 Markdown,而 Jupyter Notebooks 假设你在输入代码。这使得 Jupyter Notebooks 在快速开发和测试时更具吸引力。

从数据科学的角度来看,Jupyter Notebook 有两种主要类型,取决于其使用方式:实验室风格和交付风格。

实验室风格的 Notebook 旨在作为编程类比于研究期刊。这些应该包含你所做的所有工作,包括加载、处理、分析和建模数据。其目的是记录你所做的一切,以便将来参考,因此通常不建议删除或修改之前的实验室风格 Notebook。同时,随着分析的进展,最好累积多个带有日期戳的 Notebook 版本,以防你想回顾之前的状态。

交付风格的 Notebook 旨在展示成果,应该只包含实验室风格 Notebook 中的部分内容。例如,这可能是与同事分享的有趣发现,或者是给经理的详细分析报告,或是给利益相关者的关键发现总结。

在任何情况下,一个重要的概念是可重复性。如果你在记录软件版本时做得很细致,那么接收报告的人将能够重新运行 Notebook 并计算出与您相同的结果。在科学界,可重复性变得越来越困难,这无疑是一个清新的突破。

导航平台

现在,我们将打开一个 Jupyter Notebook 并开始学习其界面。在这里,我们假设你对该平台没有先前的了解,并且会讲解基本的使用方法。

介绍 Jupyter Notebooks

  1. 在终端中导航到配套材料目录。

在 Mac 或 Linux 等 Unix 系统中,可以使用 ls 命令显示目录内容,使用 cd 命令更改目录。在 Windows 系统中,使用 dir 显示目录内容,使用 cd 更改目录。如果您想将驱动器从 C: 更改为 D:,可以执行 d: 来切换驱动器。

  1. 在终端中输入以下命令,启动新的本地 Notebook 服务器:jupyter notebook.

    默认浏览器的新窗口或标签页将打开 Notebook Dashboard,指向工作目录。在这里,您将看到该目录下的文件夹和文件列表。

  2. 点击一个文件夹以导航到该路径,并点击文件以打开它。尽管它的主要用途是编辑 IPYNB Notebook 文件,Jupyter 也可以作为标准文本编辑器使用。

  3. 重新打开用于启动应用的终端窗口。我们可以看到 NotebookApp 正在本地服务器上运行。特别地,您应该能看到如下行:

    [I 20:03:01.045 NotebookApp] Jupyter Notebook 正在运行,地址为:http:// localhost:8888/ ? oken=e915bb06866f19ce462d959a9193a94c7c088e81765f9d8a

    访问该 HTTP 地址将会在您的浏览器窗口中加载应用,正如启动应用时自动完成的操作。关闭窗口并不会停止应用;这应该在终端中通过输入 Ctrl + C 来完成。

  4. 在终端中按 Ctrl + C 来关闭应用。您可能还需要确认通过输入 y。同时关闭浏览器窗口。

  5. 启动 NotebookApp 时,您可以选择多种选项。在终端中,通过运行以下命令查看可用选项列表:

    jupyter notebook –-help.

  6. 其中一个选项是指定一个特定端口。通过运行以下命令,可以在 本地端口 9000 启动 NotebookApp:

    jupyter notebook --port 9000

  7. 创建新 Jupyter Notebook 的主要方法是通过 Jupyter Dashboard。点击右上角的 New,并从下拉菜单中选择一个内核(即,选择 Notebooks 部分中的某个选项):

Kernels 为 Notebook 提供编程语言支持。如果您已经通过 Anaconda 安装了 Python,那么该版本应该是默认的内核。Conda 虚拟环境也将在此可用。

虚拟环境是管理同一台机器上多个项目的绝佳工具。每个虚拟环境可能包含不同版本的 Python 和外部库。Python 内置有虚拟环境;然而,Conda 虚拟环境与 Jupyter Notebooks 的集成更好,并具有其他一些优点。相关文档请参阅 conda.io/docs/user-guide/tasks/manage-environments.html

  1. 在新创建的空白 Notebook 中,点击顶部单元格并输入print('hello world'),或者任何其他可以向屏幕输出的代码片段。通过点击单元格并按Shift + Enter来执行,或者在Cell 菜单中选择Run Cell

任何来自代码的stdoutstderr输出都会在单元格运行时显示在下面。此外,最后一行中写的对象的字符串表示也会显示出来。这非常方便,特别是在显示表格时,但有时我们不希望最后的对象被显示。在这种情况下,可以在行末添加分号(; )来抑制显示。

新单元格默认期望并运行代码输入;然而,它们也可以改为渲染 Markdown 格式的内容。

  1. 点击一个空白单元格,并将其更改为接受 Markdown 格式的文本。这可以通过工具栏中的下拉菜单图标来完成,或者从Cell菜单中选择Markdown。在这里输入一些文本(任何文本都可以),并确保使用 Markdown 格式符号,如#。

  2. 集中注意力在 Notebook 顶部的工具栏:

工具栏中有一个播放图标,可以用来运行单元格。然而,正如我们稍后会看到的,使用键盘快捷键Shift + Enter 来运行单元格更为便捷。紧挨着这个图标的是一个停止图标,可以用来停止单元格的运行。例如,如果某个单元格运行时间过长,这个功能就非常有用:

新单元格可以通过Insert菜单手动添加:

可以使用图标或通过Edit 菜单中的选项来复制、粘贴和删除单元格:

单元格也可以通过这种方式上下移动:

Cell菜单下有一些有用的选项,可以运行一组单元格或整个 Notebook:

  1. 试验工具栏选项,移动单元格、插入新单元格和删除单元格。

了解这些 Notebook 的一个重要概念是单元格之间的共享内存。其实很简单:每个在工作表中存在的单元格都可以访问全局变量集。例如,在一个单元格中定义的函数可以在任何其他单元格中调用,变量也同样如此。正如预期的那样,函数作用域内的任何内容都不是全局变量,只能在该特定函数内访问。

  1. 打开 Kernel 菜单以查看选项。Kernel 菜单对于停止脚本执行以及在内核崩溃时重新启动 Notebook 非常有用。内核也可以随时在此处切换,但由于可复现性问题,不建议为单个 Notebook 使用多个内核。

  2. 打开 文件 菜单查看选项。文件 菜单包含将 Notebook 下载为各种格式的选项。特别推荐保存为 HTML 版本的 Notebook,内容会被静态渲染,并且可以在网页浏览器中像预期一样打开和查看。

Notebook 的名称会显示在左上角。新的 Notebooks 将自动命名为 Untitled

  1. 通过点击左上角当前名称来更改你的 IPYNB Notebook 文件的名称,并输入新的名称。然后,保存文件。

  2. 关闭浏览器中的当前标签页(退出 Notebook),然后进入仍然打开的 Jupyter 仪表盘标签页。(如果它没有打开,可以通过从终端复制并粘贴 HTTP 链接来重新加载它。)

由于我们没有关闭 Notebook,而只是保存并退出,它会在 Jupyter 仪表盘的文件部分旁边显示绿色的书本符号,并在右侧显示“运行中”,旁边是最后修改的日期。从这里可以关闭 Notebooks。

  1. 通过选择你正在使用的 Notebook(在名称左侧的复选框)并点击橙色的关闭按钮来退出该 Notebook:

如果你计划花费大量时间与 Jupyter Notebooks 一起工作,学习键盘快捷键是值得的。这将大大加快你的工作流程。特别有用的命令是学习手动添加新单元格以及将单元格从代码转换为 Markdown 格式的快捷键。点击 键盘快捷键,在 帮助菜单 中查看相关内容。

Jupyter 功能

Jupyter 有许多吸引人的功能,使得 Python 编程更加高效。这些功能包括从查看文档字符串到执行 Bash 命令等各种方法。让我们一起在这一部分中探索这些功能。

官方的 IPython 文档可以在这里找到:ipython.readthedocs.io/en/stable/。其中包含了我们将在此讨论的功能以及其他功能的详细信息。

探索一些 Jupyter 最有用的功能

  1. 从 Jupyter 仪表盘中,导航到 chapter-1 目录,并通过选择它打开 chapter-1-workbook.ipynb 文件。Jupyter Notebooks 的标准文件扩展名是 .ipynb,这个扩展名是在它们被称为 IPython Notebooks 时引入的。

  2. 向下滚动到 Jupyter Notebook 中的子主题 Jupyter Features。我们首先回顾基本的键盘快捷键。这些快捷键尤其有助于避免频繁使用鼠标,从而大大加快工作流程。以下是最有用的键盘快捷键。学会使用这些快捷键将大大提高你使用 Jupyter Notebooks 的体验,并提升你的效率:

    • Shift + Enter 用于运行单元格

    • Esc 用于退出单元格

    • M 键用于将单元格更改为 Markdown(按下 Esc 后)

    • Y 键用于将单元格切换为代码模式(按下 Esc 后使用)

    • 箭头键 用于移动单元格(按下 Esc 后使用)

    • 回车 用于进入单元格

说到快捷键,帮助选项对初学者和经验丰富的编码人员都很有用。它能在每一个不确定的步骤上提供指导。

用户可以通过在任何对象的末尾添加问号并运行单元格来获得帮助。Jupyter 会查找该对象的文档字符串,并在应用程序底部弹出的窗口中返回。

  1. 运行 获取帮助 部分的单元格,查看 Jupyter 如何在 Notebook 底部显示文档字符串。添加一个新单元格并获取你选择的对象的帮助:

Tab 补全可以用于以下操作:

    • 列出导入外部库时可用的模块

    • 列出导入的外部库的可用模块

    • 函数和变量补全

当你需要了解一个模块的可用输入参数时,特别是当你探索一个新库,发现新模块,或者仅仅是为了加速工作流程时,这些命令特别有用。它们能节省写出变量名或函数的时间,并减少由于输入错误导致的 bug。Tab 补全功能非常强大,今天你可能会发现,离开 Jupyter 后再用其他编辑器写 Python 时,可能会感到不习惯!

  1. 点击 Tab 补全部分的一个空代码单元格,并尝试按照上面提到的方式使用 Tab 补全。例如,第一个建议可以通过键入 import(包括后面的空格),然后按 Tab 键来完成:

  1. 最后但同样重要的基本 Jupyter Notebook 功能是魔法命令。这些命令由一个或两个百分号符号组成,后面跟着命令。以%%开头的魔法命令将作用于整个单元格,而以%开头的魔法命令只会作用于当前行。通过示例你会更容易理解这一点。

滚动到 Jupyter 魔法函数 部分,运行包含 %lsmagic 和 %matplotlib inline 的单元格:

%lsmagic 列出可用的选项。我们将讨论并展示一些最常用的魔法命令。你最常见的魔法命令可能是 %matplotlib inline,它允许在 Notebook 中直接显示 matplotlib 图形,而不需要显式使用 plt.show()

计时功能非常实用,有两种类型:标准计时器 (%time 或 %%time) 和测量多次迭代平均运行时间的计时器 (%timeit 和 %%timeit)

  1. 运行 计时器 部分的单元格。注意使用一个和两个百分号的区别。

即使使用的是 Python 内核(正如你现在所做的),也可以通过魔法命令调用其他语言。内置选项包括 JavaScript、R、Pearl、Ruby 和 Bash。Bash 特别有用,因为你可以使用 Unix 命令来查看当前目录位置(pwd)、查看目录内容(ls)、创建新文件夹(mkdir),以及写入文件内容(cat / head / tail)。

  1. 运行 “在笔记本中使用 bash” 部分的第一个单元。这一单元会向工作目录中的一个文件写入一些文本,打印目录内容,打印一个空行,然后写回新创建的文件的内容并将其删除:

  1. 运行以下仅包含 lspwd 的单元。注意,我们不需要显式使用 Bash 魔法命令就能使其正常工作。

还有许多外部魔法命令可以安装。一个流行的命令是 ipython-sql,它允许在单元中执行 SQL 代码。

  1. 如果你还没有安装 ipython-sql,请现在进行安装。打开一个新的终端窗口并执行以下代码:
  pip install ipython-sql

  1. 运行 %load_ext sql 单元,将外部命令加载到 Notebook 中:

这使得可以连接到远程数据库,从而直接在 Notebook 中执行查询(并记录查询内容)。

  1. 运行包含 SQL 示例查询的单元:

在这里,我们首先连接到本地 sqlite 数据源;然而,这一行也可以指向本地或远程服务器上的特定数据库。然后,我们执行一个简单的 SELECT 查询,展示如何将单元转换为运行 SQL 代码,而不是 Python 代码。

  1. 接下来,我们简要讨论一个有助于文档编写的魔法命令。这个命令是%version_information,但它并不是 Jupyter 的标准命令。像我们刚才看到的 SQL 命令一样,它可以通过命令行使用 pip 安装。

如果还没有安装,请通过终端使用 pip 安装版本文档工具。打开一个新的窗口并运行以下代码:

pip install version_information

安装完成后,可以通过 %load_ext version_information 将其导入任何 Notebook。最后,一旦加载完成,就可以用来显示 Notebook 中每个软件的版本信息。

  1. 运行加载并调用 version_information 命令的单元:

将 Jupyter Notebook 转换为 Python 脚本

你可以将 Jupyter Notebook 转换为 Python 脚本。这相当于将每个代码单元的内容复制并粘贴到一个 .py 文件中。Markdown 部分也会作为注释包含在内。

转换可以通过NotebookApp或如下命令行进行:

jupyter nbconvert --to=python chapter-1-notebook.ipynb

这很有用,例如,当你想使用像pipreqs这样的工具来确定 Notebook 的库需求时。该工具可以确定项目中使用的库,并将它们导出到requirements.txt文件中(你可以通过运行pip install pipreqs来安装该工具)。

该命令是从不包含.py文件的文件夹外部调用的。例如,如果.py文件位于名为chapter-1的文件夹中,你可以执行以下操作:

pipreqs chapter-1/

chapter-1-workbook.ipynb生成的requirements.txt文件如下所示:

     cat chapter-1/requirements.txt
     matplotlib==2.0.2
     numpy==1.13.1
     pandas==0.20.3
     requests==2.18.4
     seaborn==0.8
     beautifulsoup4==4.6.0
     scikit_learn==0.19.0

Python 库

现在我们已经了解了 Jupyter Notebook 的基础知识,甚至包括一些更高级的功能,接下来我们将转向本书中将要使用的 Python 库。一般来说,库是扩展 Python 默认函数集的工具。常见的标准库包括datetimetimeos。这些被称为标准库,因为它们在每次安装 Python 时都会默认包含。

对于 Python 的数据科学,最重要的库是外部库,也就是说,它们并不是 Python 自带的。

本书中我们将使用的外部数据科学库包括NumPyPandasSeabornmatplotlibscikit-learnRequestsBokeh。我们将简要介绍每一个库。

使用行业标准导入库是一个好主意,例如,import numpy as np;这样,代码更具可读性。尽量避免使用如from numpy import *之类的方式,因为这可能会不小心覆盖已有的函数。此外,为了提高代码可读性,最好通过点号(.)将模块与库链接。

  • NumPy 提供了多维数据结构(数组),其操作速度远快于标准的 Python 数据结构(例如,列表)。这部分是通过在后台使用 C 语言执行操作来实现的。NumPy 还提供了各种数学和数据处理功能。

  • Pandas 是 Python 对 R 中的 DataFrame 的回应。它以二维表格结构存储数据,其中列代表不同的变量,行对应于样本。Pandas 提供了许多方便的数据处理工具,比如填充NaN条目和计算数据的统计描述。与 Pandas DataFrame 的工作是本书的重点之一。

  • Matplotlib 是一种绘图库,灵感来源于 MATLAB 平台。熟悉 R 的人可以将其看作是 Python 版的 ggplot。它是最流行的 Python 绘图库,支持高度自定义。

  • Seaborn 是 matplotlib 的扩展,它包含了许多数据科学中有用的绘图工具。一般来说,这可以比手动使用 matplotlib 和 scikit-learn 等库来创建相同的内容更快地完成分析。

  • Scikit-learn 是最常用的机器学习库。它提供了顶级的算法和非常优雅的 API,模型通过实例化后再与数据进行拟合。它还提供了数据处理模块和其他有助于预测分析的工具。

  • Requests 是进行 HTTP 请求的首选库。它使得从网页获取 HTML 并与 API 接口变得非常简单。对于解析 HTML,许多人选择 BeautifulSoup4,我们在本书中也会介绍这个库。

  • Bokeh 是一个交互式可视化库。它的功能类似于 matplotlib,但允许我们为图表添加悬停、缩放、点击等交互功能。它还允许我们在 Jupyter Notebook 中渲染并与图表进行互动。

在介绍了这些库之后,让我们回到 Notebook 中,通过运行导入语句来加载它们。这将引导我们进入第一次分析,开始使用数据集进行工作。

导入外部库并设置绘图环境

  1. 打开 chapter 1 Jupyter Notebook,滚动到 Python Libraries 部分。

就像常规的 Python 脚本一样,库可以在 Notebook 中的任何时候导入。最佳实践是将你使用的大部分包放在文件的顶部。有时,在 Notebook 中途加载库也是完全可以的。

  1. 运行单元格来导入外部库并设置绘图选项:

为了使 Notebook 设置更加美观,通常在顶部与导入库一起设置各种选项是很有用的。例如,下面的代码可以运行,用于将图表外观更改为比 matplotlib 和 Seaborn 默认值更具美感的样式:

    import matplotlib.pyplot as plt
    %matplotlib inline
    import seaborn as sns
    # See here for more options: 
    https://matplotlib.org/users/customizing.html
    %config InlineBackend.figure_format='retina'
    sns.set() # Revert to matplotlib defaults
    plt.rcParams['figure.figsize'] = (9, 6)
    plt.rcParams['axes.labelpad'] = 10
    sns.set_style("darkgrid")

到目前为止,在本书中,我们已经介绍了如何使用 Jupyter Notebooks 进行数据科学的基础知识。我们从探索平台并熟悉界面开始。然后,我们讨论了最有用的功能,包括标签自动完成和魔法函数。最后,我们介绍了本书中将使用的 Python 库。

接下来的部分将非常互动,我们将一起使用 Jupyter Notebook 进行第一次分析。

我们的第一次分析 - 波士顿房价数据集

到目前为止,本章主要集中在 Jupyter 的功能和基本使用上。现在,我们将把这些知识付诸实践,进行一些数据探索和分析。

我们在这一部分将要查看的数据集是所谓的 波士顿房价数据集。它包含了关于波士顿市不同地区房屋的美国人口普查数据。每个样本对应一个独特的地区,包含大约十几个测量指标。我们应该把样本看作行,把测量指标看作列。这个数据集最早发布于 1978 年,非常小,只有大约 500 个样本。

既然我们对数据集的背景有了一些了解,现在让我们为探索和分析制定一个大致的计划。如果适用,这个计划将包含正在研究的相关问题。在本例中,目标不是回答某个问题,而是展示 Jupyter 的实际操作,并演示一些基本的数据分析方法。

我们分析的一般方法将是:

  • 使用 Pandas DataFrame 将数据加载到 Jupyter 中

  • 定量理解特征

  • 寻找模式并生成问题

  • 解答问题

使用 Pandas DataFrame 将数据加载到 Jupyter 中

数据通常存储在表格中,这意味着它可以保存为 comma-separated variable (CSV) 文件。该格式以及许多其他格式可以通过 Pandas 库加载为 Python 中的 DataFrame 对象。其他常见的格式包括 tab-separated variable (TSV)、SQL 表格和 JSON 数据结构。实际上,Pandas 支持所有这些格式。然而,在这个示例中,我们不会以这种方式加载数据,因为数据集可以直接通过 scikit-learn 获取。

加载数据进行分析后,一个重要的步骤是确保数据是干净的。例如,我们通常需要处理缺失数据,并确保所有列都具有正确的数据类型。我们在本节中使用的数据集已经清理过,因此我们不需要担心这个问题。不过,在第二章中,我们会遇到更杂乱的数据,并探索处理它的技巧。

加载波士顿住房数据集

  1. 在第一章的 Jupyter Notebook 中,滚动到子主题 Using Pandas DataFrame 加载数据到 Jupyter,位于 Our First Analysis波士顿住房数据集。波士顿住房数据集可以通过 sklearn.datasets 模块使用 load_boston 方法访问。

  2. 运行本节中的前两个单元格,以加载波士顿数据集并查看数据结构类型:

第二个单元格的输出告诉我们它是一个 scikit-learn Bunch 对象。让我们获取更多关于它的信息,以便了解我们正在处理的内容。

  1. 运行下一个单元格以从 scikit-learn utils 导入基本对象,并在我们的 Notebook 中打印 docstring:

阅读结果的 docstring 表明它本质上是一个字典,可以基本上当作字典来处理。

  1. 通过运行下一个单元格来打印字段名称(即字典的键)。我们发现这些字段是自解释的:['DESCR''target''data''feature_names']。

  2. 运行下一个单元格,打印包含在 boston['DESCR'] 中的数据集描述。注意,在这个调用中,我们显式地希望打印字段值,以便 Notebook 能够以比字符串表示更可读的格式渲染内容(也就是说,如果我们只是输入 boston['DESCR'],而没有用 print 语句包装它)。我们随后看到数据集信息,正如我们之前总结的那样:

    Boston House Prices dataset
    ===========================
    Notes
    ------
    Data Set Characteristics:
    :Number of Instances: 506
    :Number of Attributes: 13 numeric/categorical predictive
    :Median Value (attribute 14) is usually the target
    :Attribute Information (in order):
    - CRIM per capita crime rate by town
    …
    - MEDV Median value of owner-occupied homes in $1000's
    :Missing Attribute Values: None 

特别重要的是特征描述(在 Attribute Information 下)。我们将在分析过程中使用这些作为参考。

现在,我们将创建一个包含数据的 Pandas DataFrame。这有几个好处:我们的所有数据都将包含在一个对象中,DataFrame 提供了有用且计算高效的方法,并且像 Seaborn 这样的其他库有与 DataFrame 集成的工具。

在这种情况下,我们将使用标准构造方法来创建 DataFrame。

  1. 运行 Pandas 导入和获取 pd.DataFrame 文档字符串的单元格:

文档字符串显示了 DataFrame 输入参数。我们希望将 boston['data'] 作为数据输入,并使用 boston['feature_names'] 作为列标题。

  1. 运行接下来的几行代码来打印数据、数据的形状和特征名称:

从输出中,我们看到数据是一个 2D NumPy 数组。运行命令 boston['data'].shape 会返回样本数量(第一个输出)和特征数量(第二个输出)。

  1. 通过运行以下代码将数据加载到 Pandas DataFrame df 中:
df = pd.DataFrame(data=boston['data'], columns=boston['feature_names'])

在机器学习中,正在建模的变量称为目标变量;它是你试图根据特征预测的内容。对于这个数据集,建议的目标是 MEDV,即房屋中位数价值,以千美元为单位。

  1. 运行下一个单元格来查看目标的形状:

我们看到它的长度与特征的长度相同,这正是我们所期望的。因此,它可以作为新列添加到 DataFrame 中。

  1. 通过运行包含以下内容的单元格,将目标变量添加到 df
    df['MEDV'] = boston['target']

  1. 为了将目标与特征区分开来,将目标存储在 DataFrame 的前面会很有帮助。

    通过运行包含以下内容的单元格,将目标变量移到 df 的前面:

     y = df['MEDV'].copy()
     del df['MEDV']
     df = pd.concat((y, df), axis=1)

在这里,我们引入了一个虚拟变量 y 来保存目标列的副本,之后再从 DataFrame 中删除它。然后我们使用 Pandas 的连接函数,将其与剩余的 DataFrame 按照第一轴(而不是第零轴,后者是合并行)连接起来。

你经常会看到使用点符号来引用 DataFrame 的列。例如,以前我们可以写 y = df.MEDV.copy()。但是,这种方式无法用来删除列;del df.MEDV 会引发错误。

  1. 现在数据已经完全加载,我们来看看 DataFrame。

我们可以通过df.head()df.tail()查看数据的一个简略概况,使用len(df)来确保样本数量符合预期。运行接下来的几个单元格来查看df的头部、尾部和长度:

每一行都有一个索引值,在表格的左侧以粗体显示。默认情况下,这些是从 0 开始并按顺序递增的整数,每一行对应一个索引。

  1. 打印df.dtypes将显示每一列中包含的数据类型。

运行下一个单元格查看每一列的数据类型。

对于这个数据集,我们看到每个字段都是浮动类型,因此很可能是连续变量,包括目标变量。这意味着预测目标变量是一个回归问题。

  1. 接下来,我们需要做的是清理数据,处理任何缺失的数据,Pandas 会将缺失数据自动标记为NaN。可以通过运行df.isnull()来识别这些缺失数据,它会返回与df相同形状的布尔型 DataFrame。要获取每列中 NaN 的数量,我们可以使用df.isnull().sum()。运行下一个单元格来计算每列中NaN值的数量:

对于这个数据集,我们看到没有 NaN 值,这意味着我们无需立即进行数据清理,可以继续分析。

  1. 为了简化分析,在探索之前我们将移除一些列。我们不会关注这些列,而是将重点放在其余的列上,进行更详细的分析。

通过运行包含以下代码的单元格来移除一些列:

  for col in ['ZN', 'NOX', 'RAD', 'PTRATIO', 'B']:
     del df[col]

数据探索

由于这是一个我们以前从未见过的全新数据集,首要目标是理解数据。我们已经看到了数据的文本描述,这对于定性理解非常重要。接下来我们将计算一个定量的描述。

探索波士顿住房数据集

  1. 导航到子主题Jupyter Notebook 中的数据探索并运行包含df.describe()的单元格:

这段代码计算了每一列的各种属性,包括均值、标准差、最小值和最大值。这个表格提供了一个高层次的视图,展示了数据的分布情况。注意,我们通过在输出中添加.T来对结果进行转置,这会交换行和列。接下来的分析中,我们将指定一组列进行重点关注。

  1. 运行定义这些“焦点列”的单元格:
    cols = ['RM', 'AGE', 'TAX', 'LSTAT', 'MEDV'] 
  1. 可以使用方括号从df中选择这个子集的列。运行df[cols].head()来显示这个 DataFrame 的子集:

提醒一下,让我们回顾一下这些列的含义。根据数据集文档,以下是我们所看到的:

    • RM 每个住宅的平均房间数

    • AGE 1940 年之前建造的自有住房单元的比例

    • TAX 每$10,000 的全值房产税率

    • LSTAT % 低收入群体比例

    • MEDV 业主自住住房的中位数价值(以 $1000 为单位)

为了在这些数据中寻找模式,我们可以从计算成对相关性开始,使用 pd.DataFrame.corr

  1. 通过运行包含以下代码的单元格,计算我们选择的列的成对相关性:
   df[cols].corr()

这个结果表格显示了每对值之间的相关性得分。较大的正相关得分表示强正相关(即方向相同)。正如预期的那样,我们在对角线看到的最大值为 1。

皮尔逊系数被定义为两个变量之间的协方差,除以它们标准差的乘积:

协方差则定义如下:

这里,n 是样本数量,x[i]y[i] 是被求和的单个样本, 是每组的均值。

与其眼睛费力地看前面的表格,不如用热图将其可视化。这可以通过 Seaborn 轻松完成。

  1. 运行下一个单元格来初始化绘图环境,如本章前所述。然后,要创建热图,运行包含以下代码的单元格:
     import matplotlib.pyplot as plt
     import seaborn as sns
     %matplotlib inline 

     ax = sns.heatmap(df[cols].corr(),
     cmap=sns.cubehelix_palette(20, light=0.95, dark=0.15))
     ax.xaxis.tick_top() # move labels to the top
     plt.savefig('../figures/chapter-1-boston-housing-corr.png',
     bbox_inches='tight', dpi=300)

我们调用 sns.heatmap 并传入成对相关矩阵作为输入。我们在这里使用自定义的颜色调色板来覆盖 Seaborn 默认设置。该函数返回一个 matplotlib.axes 对象,变量 ax 引用该对象。最终图像会作为高分辨率 PNG 保存到 figures 文件夹中。

  1. 在数据集探索的最后一步,我们将使用 Seaborn 的 pairplot 函数来可视化我们的数据。

  2. 使用 Seaborn 的 pairplot 函数可视化 DataFrame。运行以下代码单元:

     sns.pairplot(df[cols],
     plot_kws={'alpha': 0.6},
     diag_kws={'bins': 30}) 

在之前使用热图可视化相关性概况后,这个图表可以让我们更加详细地观察各个关系。

查看对角线上的直方图,我们可以看到以下内容:

    • a: RM 和 MEDV 的分布形态最接近正态分布。

    • b: AGE 向左偏斜,而 LSTAT 向右偏斜(这可能看起来有点违反直觉,但偏斜是根据均值相对于最大值的位置来定义的)。

    • c: 对于 TAX,我们发现大部分分布集中在 700 附近。这也是

      从散点图中可以看出

仔细观察右下角的 MEDV 直方图,我们实际上看到了一些类似于 TAX 的现象,即在 $50,000 附近有一个较大的上限区间。回想一下我们在执行 df.describe() 时,MDEV 的最小值和最大值分别是 5k 和 50k。这表明数据集中的房屋中位数价值被限制在了 50k。

使用 Jupyter Notebooks 进行预测分析简介

继续分析波士顿房价数据集,我们可以看到它为我们提供了一个回归问题,在这个问题中,我们根据一组特征预测一个连续的目标变量。具体来说,我们将预测中位房价(MEDV)。我们将训练仅以一个特征作为输入的模型来进行预测。这样,模型在概念上简单易懂,我们可以更专注于 scikit-learn API 的技术细节。然后,在下一章中,你将能够更轻松地处理相对复杂的模型。

使用 Seaborn 和 scikit-learn 进行线性模型分析

  1. 滚动到 Jupyter Notebook 中的子主题 Introduction to predictive analytics,并查看上方我们在前一节中创建的配对图。特别地,查看左下角的散点图:

请注意,每个房屋的房间数(RM)和属于下层阶级的百分比(LSTAT)与中位房价(MDEV)高度相关。让我们提出以下问题:给定这些变量,我们能多好地预测 MDEV

为了帮助回答这个问题,首先让我们使用 Seaborn 可视化这些关系。我们将绘制散点图,并加上最佳拟合线性模型。

  1. 通过运行包含以下内容的单元格来绘制散点图,并加上线性模型:
    fig, ax = plt.subplots(1, 2)
    sns.regplot('RM', 'MEDV', df, ax=ax[0],
    scatter_kws={'alpha': 0.4}))
    sns.regplot('LSTAT', 'MEDV', df, ax=ax[1],
    scatter_kws={'alpha': 0.4})) 

最佳拟合线是通过最小化普通最小二乘误差函数来计算的,这是 Seaborn 在我们调用 regplot 函数时自动完成的。还请注意线周围的阴影区域,表示 95%的置信区间。

这些 95%的置信区间是通过在垂直于最佳拟合线的区间内取数据的标准差来计算的,有效地确定了最佳拟合线每个点的置信区间。实际上,这涉及到 Seaborn 对数据进行自助法(bootstrap)处理,这个过程通过带有放回的随机抽样生成新的数据。自助法样本的数量是根据数据集的大小自动确定的,但也可以通过传递 n_boot 参数手动设置。

  1. Seaborn 还可以用来绘制这些关系的残差图。通过运行包含以下内容的单元格来绘制残差图:
    fig, ax = plt.subplots(1, 2)
    ax[0] = sns.residplot('RM', 'MEDV', df, ax=ax[0],
                      scatter_kws={'alpha': 0.4})
    ax[0].set_ylabel('MDEV residuals $(y-\hat{y})$')
    ax[1] = sns.residplot('LSTAT', 'MEDV', df, ax=ax[1],
                      scatter_kws={'alpha': 0.4})
    ax[1].set_ylabel('')

这些残差图上的每个点是该样本(y)与线性模型预测值(ŷ)之间的差异。大于零的残差表示模型低估了数据点。类似地,小于零的残差表示模型高估了数据点。

这些图中的模式可能表明模型存在不足。在前面的每个案例中,我们看到正区域内斜对角排列的散点。这是由 MEDV 的$50,000 上限造成的。RM 数据围绕 0 聚集,表明拟合效果良好。另一方面,LSTAT 似乎聚集在 0 以下。

  1. 离开可视化后,拟合度可以通过计算均方误差来量化。我们现在将使用 scikit-learn 来完成这一步。通过运行包含以下内容的单元,定义一个计算最佳拟合线和均方误差的函数:
      def get_mse(df, feature, target='MEDV'):
      # Get x, y to model
      y = df[target].values
      x = df[feature].values.reshape(-1,1)
      ...
      ...
      error = mean_squared_error(y, y_pred)
      print('mse = {:.2f}'.format(error))
      print() 

get_mse函数中,我们首先将变量 y 和 x 分别赋值为目标MDEV和依赖特征。这些变量通过调用values属性被转换为NumPy数组。依赖特征数组被重新塑形为 scikit-learn 期望的格式;这仅在建模一维特征空间时才需要。接着,模型被实例化并在数据上进行拟合。对于线性回归,拟合过程包括使用最小二乘法计算模型参数(最小化每个样本的平方误差和)。最后,在确定参数后,我们预测目标变量,并使用结果计算 MSE。

  1. 通过运行包含以下内容的单元来调用get_mse函数,分别对 RM 和 LSTAT 进行计算:
      get_mse(df, 'RM')
      get_mse(df, 'LSTAT') 

比较MSE后,结果显示LSTAT的误差略低。然而,回顾散点图,似乎我们使用多项式模型来拟合 LSTAT 会取得更好的效果。在接下来的活动中,我们将通过使用 scikit-learn 计算三次多项式模型来验证这一点。

暂时不考虑我们的波士顿住房数据集,想象另一个可能应用多项式回归的实际场景。以下示例是天气数据建模。在下图中,我们可以看到温度(线条)和降水量(柱状图)数据,来自加拿大不列颠哥伦比亚省温哥华市:

这些字段中的任何一个都可能很好地拟合四次多项式。例如,如果你有兴趣预测连续日期范围内的温度或降水量,这将是一个非常有价值的模型。

你可以在这里找到数据来源:

climate.weather.gc.ca/climate_normals/results_e.html?stnID=888.

活动:构建三次多项式模型

将注意力转回到波士顿住房数据集,我们希望建立一个三次多项式模型,来与线性模型进行比较。回想一下我们实际想要解决的问题:在给定低收入阶层人口百分比的情况下,预测中位数房价。这个模型可能会对有意购买波士顿房产、关心社区中低收入阶层比例的购房者有帮助。

使用 scikit-learn 来拟合一个多项式回归模型,以根据 LSTAT 值预测中位数房价 (MEDV)。我们希望建立一个具有较低均方误差(MSE)的模型。

使用 Seaborn 和 scikit-learn 进行线性模型建模

  1. 滚动到 Jupyter Notebook 中 Subtopic Introduction to Predictive Analysis 底部的空单元格。这些单元格位于线性模型 MSE 计算单元格下方的 Activity 标题下。

在我们完成活动时,您应当将这些空单元格填充为代码。随着这些单元格的填充,您可能需要插入新的单元格;请根据需要进行操作!

  1. 由于我们的数据包含在 DataFrame df 中,我们首先通过以下代码提取我们的依赖特征和目标变量:
    y = df['MEDV'].values
    x = df['LSTAT'].values.reshape(-1,1) 

这与我们之前为线性模型所做的完全相同。

  1. 通过打印前几个样本 print(x[:3]) 来查看 x 的样子:

注意,每个数组中的元素本身是一个长度为 1 的数组。这就是 reshape(-1, 1) 的作用,它是 scikit-learn 所期望的格式。

  1. 接下来,我们将把 x 转换成“多项式特征”。这种做法的理由可能并不立即显而易见,但稍后会解释清楚。

    从 scikit-learn 导入适当的转换工具并实例化一个三次多项式特征变换器:

    from sklearn.preprocessing import PolynomialFeatures
    poly = PolynomialFeatures(degree=3) 
  1. 此时,我们仅拥有特征变换器的一个实例。现在,让我们使用它通过运行 fit_transform 方法来转换 LSTAT 特征(存储在变量 x 中)。

通过运行以下代码构建多项式特征集:

     x_poly = poly.fit_transform(x) 
  1. 通过打印前几个样本 print(x_poly[:3]) 来查看 x_poly 的样子。

与 x 不同,每行中的数组现在具有 4 的长度,其中值已计算为 x⁰、x¹、x² 和 x³。

我们现在将使用这些数据来拟合一个线性模型。将特征标记为 a、b、c 和 d,我们将计算线性模型的系数 α[0]、α[1]、α[2] 和 α[3]:

我们可以插入 a、b、c 和 d 的定义,得到以下多项式模型,其中系数与之前相同:

  1. 我们将导入线性回归类,并像之前计算 MSE 时一样构建我们的线性分类模型。运行以下代码:
    from sklearn.linear_model import LinearRegression
    clf = LinearRegression()
    clf.fit(x_poly, y) 
  1. 提取系数并使用以下代码打印出多项式模型:
    a_0 = clf.intercept_ + clf.coef_[0]   #intercept
    a_1, a_2, a_3 = clf.coef_[1:]         #other coefficients
    msg = 'model: y = {:.3f} + {:.3f}x + 
    {:.3f}x² + {:.3f}x³'\.format(a_0, a_1,  a_2, a_3)
    print(msg) 

为了获取实际的模型截距,我们必须添加 intercept_coef_[0] 属性。然后,高阶系数由 coef_ 的其余值给出。

  1. 通过运行以下代码,确定每个样本的预测值并计算残差:
    y_pred = clf.predict(x_poly)
    resid_MEDV = y - y_pred 
  1. 通过运行 print(resid_MEDV[:10]) 打印一些残差值:

我们很快会绘制这些结果,以便与线性模型的残差进行比较,但首先我们将计算MSE

  1. 运行以下代码来打印第三阶多项式模型的 MSE:
    from sklearn.metrics import mean_squared_error
    error = mean_squared_error(y, y_pred)
    print('mse = {:.2f}'.format(error)) 

如图所示,与线性模型(其 MSE 为 38.5)相比,多项式模型的MSE显著更小。这个误差指标可以通过取平方根转换为平均误差(以美元为单位)。对于多项式模型,计算得出的房屋中位数值的平均误差仅为 $5,300。

现在,我们将通过绘制多项式最佳拟合线和数据点来可视化模型。

  1. 通过运行以下代码绘制多项式模型以及样本数据:
    fig, ax = plt.subplots()
    # Plot the samples
    ax.scatter(x.flatten(), y, alpha=0.6)
    # Plot the polynomial model
    x_ = np.linspace(2, 38, 50).reshape(-1, 1)
    x_poly = poly.fit_transform(x_)
    y_ = clf.predict(x_poly)
    ax.plot(x_, y_, color='red', alpha=0.8)
    ax.set_xlabel('LSTAT'); ax.set_ylabel('MEDV'); 

在这里,我们通过在一组 x 值上计算多项式模型的预测值来绘制红色曲线。x 值的数组是使用np.linspace创建的,结果是从 2 到 38 之间均匀分布的 50 个值。

现在,我们将绘制相应的残差图。之前我们使用了 Seaborn,但为了展示 scikit-learn 模型的结果,我们需要手动绘制。由于我们之前已经计算了残差,可以通过resid_MEDV变量作为参考,我们只需将这些数值绘制在散点图上。

  1. 通过运行以下代码绘制残差图:
    fig, ax = plt.subplots(figsize=(5, 7))
    ax.scatter(x, resid_MEDV, alpha=0.6)
    ax.set_xlabel('LSTAT')
    ax.set_ylabel('MEDV Residual $(y-\hat{y})$')    
    plt.axhline(0, color='black', ls='dotted'); 

与线性模型的 LSTAT 残差图相比,多项式模型的残差似乎更紧密地集中在 y - ŷ = 0 附近。注意,y 是样本 MEDV,ŷ 是预测值。依然能看到明显的模式,比如在 x = 7 和 y = -7 附近的聚集,这表明模型拟合不佳。

在成功使用多项式模型拟合数据之后,让我们通过查看分类特征来完成这一章。特别地,我们将构建一组分类特征,并利用它们更详细地探索数据集。

使用分类特征进行分段分析

经常会遇到包含连续变量和分类变量的数据集。在这种情况下,我们可以通过将连续变量与分类字段进行分段,来学习数据并寻找模式。

以一个具体的例子为例,假设你正在评估广告活动的投资回报率。你所能访问的数据包含某些计算出的投资回报率(ROI)指标。这些数值是按天计算并记录的,而你正在分析去年的数据。你的任务是从数据中找到改进广告活动的见解。查看 ROI 的每日时间序列,你会看到数据中有一个每周的波动。通过按星期几进行分段,你可以发现以下 ROI 分布(其中 0 代表一周的第一天,6 代表最后一天)。

举个具体例子,假设你正在评估一场广告活动的投资回报率。你能访问的数据包含一些计算出的投资回报率(ROI)指标。这些值是每天计算并记录的,你正在分析去年的数据。你的任务是从数据中挖掘改进广告活动的见解。查看 ROI 的日时间序列时,你会看到数据中有每周的波动。通过按星期几分段,你会发现以下的 ROI 分布(其中 0 表示一周的第一天,6 表示最后一天)。

由于我们正在使用的波士顿住房数据集中没有类别字段,我们将通过有效地将连续字段离散化来创建一个。在我们的例子中,这将涉及将数据分为“低”、“中”和“高”类别。需要注意的是,我们创建类别数据字段并不是仅为了说明本节中的数据分析概念。如将要看到的那样,做此操作可以揭示数据中一些原本难以察觉或完全无法获得的见解。

从连续变量创建类别字段并进行分段可视化

  1. 向上滚动至 Jupyter Notebook 中的配对图,我们在其中比较了 MEDV、LSTAT、TAX、AGE 和 RM:

看看包含 AGE 的面板。作为提醒,这个特征定义为1940 年前建成的业主自住单元的比例。我们将把这个特征转换为一个类别变量。转换后,我们将能够重新绘制这个图,并根据年龄类别使用颜色对每个面板进行分段。

  1. 向下滚动至子主题构建与探索类别特征,点击进入第一个单元格。输入并执行以下命令以绘制 AGE 的累积分布:
    sns.distplot(df.AGE.values, bins=100,
    hist_kws={'cumulative': True},
    kde_kws={'lw': 0})
    plt.xlabel('AGE')
    plt.ylabel('CDF')
    plt.axhline(0.33, color='red')
    plt.axhline(0.66, color='red')
    plt.xlim(0, df.AGE.max()); 

请注意,我们设置了kde_kws={'lw': 0}以避免在前面的图中绘制核密度估计。

从图中可以看出,低 AGE 的样本非常少,而 AGE 较大的样本则更多。这一点可以从分布在右侧的陡峭程度看出。

  1. 红色线条表示分布中的 1/3 和 2/3 点。通过观察分布与这些水平线的交点,我们可以看到大约 33%的样本的 AGE 小于 55,另外 33%的样本的 AGE 大于 90!换句话说,三分之一的住宅社区有不到 55%的房屋是 1940 年前建造的。这些社区被视为相对较新的社区。而在另一端,另三分之一的住宅社区有超过 90%的房屋是 1940 年前建造的。这些社区被视为非常旧的社区。

我们将使用红色水平线与分布交点的地方作为指导,将特征划分为:相对较新相对较旧非常旧

  1. 将分割点设置为 50 和 85,通过运行以下代码创建一个新的分类特征:
    def get_age_category(x):
        if x < 50:
            return 'Relatively New'
        elif 50 <= x < 85:
            return 'Relatively Old'
        else:
            return 'Very Old'
    df['AGE_category'] = df.AGE.apply(get_age_category)

在这里,我们使用了非常实用的 Pandas 方法apply,它将一个函数应用到给定的列或列集。此处应用的函数get_age_category接受一个表示数据行的参数,并为新列返回一个值。在这种情况下,传递的数据行只是一个单一的值,即样本的AGE

apply方法很棒,因为它可以解决多种问题,并且使代码易于阅读。然而,通常情况下,像pd.Series.str这样的矢量化方法可以更快地完成相同的任务。因此,建议尽量避免使用它,特别是在处理大数据集时。我们将在接下来的章节中看到一些矢量化方法的示例。

  1. 通过键入df.groupby('AGE_category').size()检查我们将多少样本分配到了每个年龄类别。

    进入一个新的单元格并运行

从结果来看,可以看到两个类别的样本量大致相等,非常旧组的样本量大约比其他组多 40%。我们希望保持类别的大小相当,这样每个类别都能得到充分代表,且从分析中得出结论变得简单。

可能并不总是能够将样本均匀地分配到各个类别中,在现实世界中,类别不平衡是非常常见的。在这种情况下,需要记住的是,关于代表性较少的类别,做出具有统计学意义的结论是很困难的。对于类别不平衡的数据进行预测分析尤其具有挑战性。以下博客文章提供了一个关于在进行机器学习时处理类别不平衡的优秀总结:svds.com/learning-imbalanced-classes/

让我们看看在新特征AGE_category的划分下,目标变量是如何分布的。

  1. 通过运行以下代码绘制小提琴图:
    sns.violinplot(x='MEDV', y='AGE_category', data=df,
    order=['Relatively New', 'Relatively Old', 'Very Old']); 

小提琴图展示了每个年龄类别的中位数房价分布的核密度估计。我们可以看到,它们都类似于正态分布。非常旧组包含最低的中位数房价样本,并且宽度相对较大,而其他组则更集中在它们的平均值周围。年轻组则偏向高端,这从其右半部分的扩大以及分布中白点在粗黑线位置的变化可以明显看出。

这个白色点表示均值,粗黑线大致覆盖了 50%的人群(它填充了白点两侧的第一个四分位数)。细黑线表示箱线图的须,覆盖了 95%的人群。通过传递inner='point'sns.violinplot(),这个内部可视化可以修改为显示各个数据点。现在让我们来做一下。

  1. 重新制作小提琴图,将inner='point'参数添加到sns.violinplot调用中:

为了测试目的,制作这样的图表是有益的,这样可以查看底层数据如何与可视化连接。例如,我们可以看到,Relatively New段的中位数房价大约没有低于 16,000 美元的,因此分布尾部实际上没有数据。由于我们的数据集很小(大约 500 行),我们可以看到每个段都是这种情况。

  1. 重新制作之前的 pairplot,但现在为每个 AGE 类别添加颜色标签。只需传递hue参数,代码如下:
    cols = ['RM', 'AGE', 'TAX', 'LSTAT', 'MEDV', 'AGE_category']
    sns.pairplot(df[cols], hue='AGE_category',
    hue_order=['Relatively New', 'Relatively Old', 'Very Old'],
    plot_kws={'alpha': 0.5}, diag_kws={'bins': 30}); 

从直方图来看,每个段的基础分布在RMTAX上看起来类似。而LSTAT的分布则显得更为独特。我们可以通过再次使用小提琴图来更加深入地关注它们。

  1. 制作一个小提琴图,比较每个AGE_category段的 LSTAT 分布:

MEDV的小提琴图不同,其中每个分布的宽度大致相同,在这里我们看到宽度随着AGE的增加而增加。主要是老旧房屋的社区(非常旧的段)包含的下层居民从很少到很多不等,而相对较新的社区则更有可能以较高阶层为主,超过 95%的样本中,低阶层的比例低于非常旧的社区。这是合理的,因为相对较新的社区房价会更贵。

总结

在这一章中,你已经了解了 Jupyter 数据分析的基本知识。

我们从 Jupyter 的使用说明和一些功能(如魔法函数和自动补全)开始。接着,转向数据科学相关内容,我们介绍了 Python 数据科学中最重要的库。

在本章后半部分,我们在一个实时 Jupyter Notebook 中进行了探索性分析。在这里,我们使用了散点图、直方图和小提琴图等可视化工具来加深对数据的理解。我们还进行了简单的预测建模,这一主题将在本书的下一章中深入探讨。

在接下来的章节中,我们将讨论如何进行预测分析,准备数据建模时需要考虑的事项,以及如何使用 Jupyter Notebooks 实现并比较各种模型。

第二章:数据清洗与高级机器学习

数据分析的总体目标是发现可操作的洞察,从而带来积极的商业成果。在预测分析中,目标是通过基于过去的趋势和模式,确定目标最可能的未来结果。

预测分析的好处不仅仅局限于大科技公司。任何企业,只要拥有合适的数据,都可以找到从机器学习中获益的方式。

世界各地的公司正在收集大量数据,并利用预测分析来降低成本并增加利润。最常见的例子之一来自科技巨头谷歌、脸书和亚马逊,他们在大规模运用大数据。例如,谷歌和脸书根据预测算法为你提供个性化广告,这些算法预测你最有可能点击什么内容。类似地,亚马逊根据你的历史购买记录,推荐你最有可能购买的个性化产品。

现代预测分析依赖于机器学习,在机器学习中,计算机模型被训练来从数据中学习模式。正如我们在上一章中简要看到的,软件如 scikit-learn 可以与 Jupyter Notebooks 配合使用,来高效地构建和测试机器学习模型。正如我们将继续看到的,Jupyter Notebooks 是进行这类工作的理想环境,因为我们可以进行临时测试和分析,并且轻松保存结果以便后续参考。

在本章中,我们将通过在 Jupyter Notebook 中运行各种示例和活动来再次采用实践方法。上一章我们看到了一些机器学习的例子,而在本章中,我们将采取更加缓慢且深思熟虑的方法。以员工留存问题作为本章的核心示例,我们将讨论如何处理预测分析,在为建模准备数据时需要考虑的因素,以及如何使用 Jupyter Notebooks 实施并比较各种模型。

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

  • 制定机器学习分类策略

  • 预处理数据以准备进行机器学习

  • 训练分类模型

  • 使用验证曲线来调整模型参数

  • 使用降维方法来提升模型性能

准备训练预测模型

在这里,我们将介绍训练预测模型所需的准备工作。尽管这一步骤在技术上可能没有训练模型本身那么引人注目,但不应掉以轻心。确保你在开始构建和训练可靠模型的细节之前有一个良好的计划非常重要。此外,一旦你决定了合适的计划,在准备数据进行建模时,一些技术步骤也不容忽视。

我们必须小心不要在技术任务的细节中迷失方向,从而失去目标。技术任务包括那些需要编程技能的工作,例如构建可视化、查询数据库和验证预测模型。很容易花费数小时试图实现某个特定功能,或将图表调整得完美无缺。虽然这样做无疑有益于提高我们的编程技能,但我们也不应忘记问问自己,这些工作是否真的值得投入时间,尤其是在当前项目的背景下。

此外,请记住,Jupyter Notebooks 特别适合这个步骤,因为我们可以用它们来记录我们的计划,例如,写下关于数据的粗略笔记或我们有兴趣训练的模型列表。在开始训练模型之前,最好更进一步,写出一个结构清晰的计划。这样不仅能帮助你在构建和测试模型时保持进度,还能让其他人在看到你的工作时理解你在做什么。

在讨论准备工作后,我们还将介绍准备训练预测模型的另一个步骤,即清理数据集。这是 Jupyter Notebooks 特别适合的任务,因为它们为执行数据集转换并跟踪准确的更改提供了理想的测试平台。清理原始数据所需的数据转换可能变得复杂和繁琐,因此跟踪你的工作非常重要。如第一章所述,其他工具无法提供像 Jupyter Notebooks 这样高效的选项。

确定预测分析计划

在制定预测建模计划时,应首先考虑利益相关者的需求。如果模型不能解决相关问题,那么即便是完美的模型也毫无意义。围绕业务需求制定战略,确保成功的模型能带来可操作的洞察。

虽然原则上可以解决许多业务问题,但能否交付解决方案始终取决于必要数据的可用性。因此,考虑业务需求时需要结合可用的数据源。在数据充足时,这不会产生太大影响,但随着可用数据量的减少,可解决的问题范围也会变小。

这些想法可以形成一个标准的过程,用于确定预测分析计划,流程如下:

  1. 查看可用数据,以了解现实中可解决的业务问题范围。在这个阶段,可能还为时过早去思考具体能解决哪些问题。确保你了解可用的数据字段以及它们适用的

    时间框架。

  2. 通过与关键利益相关者交谈,确定业务需求。寻找一个通过解决该问题可以得出可操作的商业决策的场景。

  3. 评估数据的适用性时,考虑特征空间是否足够多样和大。此外,还要考虑数据的状况:是否存在某些变量或时间范围的大量缺失值?

步骤 2 和步骤 3 应该重复执行,直到一个实际可行的计划逐渐成型。此时,你已经能大致了解模型的输入是什么,以及你可能期望的输出是什么。

一旦我们确定了一个可以用机器学习解决的问题,并且有了适当的数据源,我们应该回答以下问题,为项目奠定框架。这样做将帮助我们确定可以使用哪些类型的机器学习模型来解决问题:

  • 训练数据是否已标注了我们想要预测的目标变量?

如果答案是肯定的,那么我们将进行监督学习。监督学习有许多实际应用,而针对未标注数据进行预测分析的商业案例则较为罕见。

如果答案是否定的,那么你正在使用未标注的数据,因此是在进行无监督学习。一个无监督学习方法的例子是聚类分析,其中标签会被分配给每个样本所属的最近聚类。

  • 如果数据是已标注的,那么我们是在解决回归问题还是分类问题?

在回归问题中,目标变量是连续的,例如预测明天的降水量(以厘米为单位)。在分类问题中,目标变量是离散的,我们要预测的是类别标签。最简单的分类问题是二分类问题,其中每个样本被分为两个类别之一。例如,明天会下雨吗?

  • 数据长什么样子?有多少个不同的数据源?

考虑数据的大小,包括宽度和高度,其中宽度指的是列数(特征),高度指的是行数。某些算法在处理大量特征时比其他算法更有效。一般来说,数据集越大,准确性就越高。然而,对于大数据集,训练可能会非常慢且内存消耗较大。这可以通过对数据进行聚合或使用降维技术来减少。

如果有不同的数据源,它们能否合并成一个单一的表格?如果不能,那么我们可能需要为每个数据源训练模型,并为最终预测模型进行集成平均。一个可能需要这样做的例子是,拥有不同规模的多组时间序列数据。假设我们有以下数据源:一个每日时间尺度上的 AAPL 股票收盘价格表和一个按月时间尺度的 iPhone 销量数据。

我们可以通过将每个样本的月销售数据添加到日时间尺度表中来合并数据,或者将每日数据按月分组,但可能更好的方式是为每个数据集建立两个模型,然后在最终的预测模型中结合每个模型的结果。

机器学习数据预处理

数据预处理对机器学习有着巨大的影响。就像“你就是你吃的东西”这句谚语一样,模型的表现直接反映了它所训练的数据。许多模型依赖于数据的转换,使得连续特征值具有可比的限制。同样,分类特征应当被编码为数值型数据。虽然这些步骤很重要,但相对简单,不会花费太多时间。

数据预处理中通常最耗时的部分是清理杂乱的数据。只需看看这张饼图,显示了一项特定调查中数据科学家们花费最多时间的工作。

另一个需要考虑的方面是许多数据科学家使用的数据集大小。随着数据集大小的增加,杂乱数据的出现频率也随之增加,清理这些数据的难度也随之增加。

简单地丢弃缺失数据通常不是最佳选择,因为很难证明丢弃大多数字段都有值的样本是合理的。这样做可能会丧失宝贵的信息,进而影响最终模型的表现。

数据预处理的步骤可以归纳如下:

  • 根据共同字段合并数据集,将所有数据汇总成一个表格

  • 特征工程以提高数据质量,例如使用降维技术构建新特征

  • 通过处理重复行、错误或缺失值以及其他出现的问题来清理数据

  • 通过标准化或归一化所需数据并将其拆分为训练集和测试集来构建训练数据集

让我们探索一些进行数据预处理的工具和方法。

探索数据预处理工具和方法

  1. 在项目目录中通过执行jupyter notebook启动NotebookApp。然后进入chapter-2目录并打开chapter-2-workbook.ipynb文件。找到位于顶部附近加载包的单元并运行它。

我们将首先展示一些来自 Pandas 和 scikit-learn 的基本工具。接着,我们将深入研究重建缺失数据的方法。

  1. 向下滚动到子主题预处理机器学习数据,运行包含pd.merge的单元,以在笔记本中显示合并函数的文档字符串:

如我们所见,该函数接受左右两个 DataFrame 进行合并。你可以指定一个或多个列来进行分组,并且可以选择如何进行分组,也就是说,使用左、右、外部或内部的值集。让我们来看一个使用的例子。

  1. 退出帮助弹窗并运行包含以下示例 DataFrame 的单元格:
    df_1 = pd.DataFrame({'product': ['red shirt', 'red shirt', 'red shirt',
                                 'white dress'],
                     'price': [49.33, 49.33, 32.49, 199.99]}),
    df_2 = pd.DataFrame({'product': ['red shirt', 'blue pants',
                                 'white tuxedo', 'white dress'],
                     'in_stock': [True, True, False, False]})    

在这里,我们将从零开始构建两个简单的 DataFrame。如你所见,它们包含一个名为 product 的列,其中有一些共享的条目。

现在,我们将对 product 共享列执行内部合并并打印结果。

  1. 运行下一个单元格以执行内部合并:

注意,只有共享的项目 红色衬衫 和白色连衣裙被包含在内。为了包含两个表中的所有条目,我们可以改用外部合并。现在就来做吧。

  1. 运行下一个单元格以执行外部合并:

这将返回每个表中的所有数据,其中缺失值已标记为 NaN

  1. 运行下一个单元格以执行外部合并:

这将返回每个表中的所有数据,其中缺失值已标记为 NaN

由于这是我们第一次在本书中遇到 NaN 值,现在是讨论它们在 Python 中如何工作的好时机。

首先,你可以通过以下方式定义一个 NaN 变量,例如 a = float('nan')

然而,如果你想测试相等性,你不能简单地使用标准比较方法。

最好通过像NumPy这样的库中的高层函数来实现。以下代码演示了这一点:

这些结果可能看起来有些反直觉。然而,这背后是有逻辑的。如果你想更深入地理解标准比较返回 False 的根本原因,可以查看这个非常好的 Stack Overflow 讨论:stackoverflow.com/questions/1565164/what-is-the-rationale-for-all-comparisons-returning-false-for-ieee754-nan-values.

  1. 你可能已经注意到,我们最近合并的表格在前几行有重复的数据。让我们看看如何处理这个问题。

运行包含 df.drop_duplicates() 的单元格,以返回没有重复行的 DataFrame 版本:

这是最简单的“标准”去除重复行的方法。要将这些更改应用到 df,我们可以选择 set inplace=True 或做类似 df = df.drop_duplicated() 的操作。让我们来看另一种方法,它使用掩码来选择或去除重复行。

  1. 运行包含 df.duplicated() 的单元格,打印出 True/False 序列,标记重复行:

我们可以对这个结果求和,以确定有多少行是重复的,或者它可以用作掩码来选择重复的行。

  1. 通过运行接下来的两个单元格来实现:

  1. 我们可以使用简单的波浪线(~)计算掩码的相反值,以提取去重后的 DataFrame。运行以下代码,并确信输出与df.drop_duplicates()的结果相同:

df[~df.duplicated()]

  1. 这也可以用来从完整的 DataFrame 子集删除重复项。例如,运行包含以下代码的单元格:

df[~df['product'].duplicated()]

在这里,我们正在做以下事情:

    • 为产品行创建一个掩码(一个True/False系列),其中重复项用e标记

    • 使用波浪线(~)获取该掩码的相反值,这样重复项就被标记为 False,其他所有项为True

    • 使用该掩码过滤掉dfFalse的行,这些行对应于重复的产品

正如预期的那样,我们现在看到只有第一行红色衬衫留下来,因为重复的产品行已被删除。

为了继续步骤,让我们用df的去重版本替换它。这可以通过运行drop_duplicates并传递inplace=True参数来实现。

  1. 通过运行包含以下代码的单元格,去重 DataFrame 并保存结果:

    df.drop_duplicates(inplace=True)

继续其他预处理方法,让我们忽略重复行,首先处理缺失数据。这是必要的,因为模型不能在不完整的样本上进行训练。以蓝色裤子和白色燕尾服的缺失价格数据为例,我们展示一些处理NaN值的不同选项。

  1. 一个选择是删除行,如果你的 NaN 样本大部分值缺失,这可能是个好主意。通过运行包含df.dropna()的单元格来执行此操作:

  1. 如果某个特征的大部分值都缺失,最好完全删除该列。通过运行包含之前相同方法的单元格来执行此操作,但这次传递axes参数以指示列而不是行:

仅删除NaN值通常不是最佳选择,因为丢失数据永远不好,特别是当样本值只有少部分缺失时。Pandas 提供了一种以多种不同方式填充NaN条目的方法,其中一些我们将在本章中展示。

  1. 运行包含df.fillna?的单元格,以打印 Pandas NaN-fill 方法的文档字符串:

请注意value参数的选项;例如,它可以是一个单一值,或者是基于索引的字典/系列类型映射。或者,我们可以将value保持为 None,并传递一个填充方法。我们将在本章中展示每种情况的示例。

  1. 通过运行包含以下代码的单元格,用平均产品价格填充缺失的数据:

    df.fillna(value=df.price.mean())

  1. 现在,通过运行包含以下代码的单元格,使用 pad 方法填充缺失的数据:

    df.fillna(method='pad')

请注意,白色裙子的价格被用来填充下面缺失的值。

为了总结本节内容,我们将准备一个简单的表格,用于训练机器学习算法。别担心,我们不会在这么小的数据集上训练任何模型!我们从编码类别数据的类标签开始这个过程。

  1. 在编码标签之前,先运行Building training data sets部分中的第一个单元格,以添加另一列数据,表示产品的平均评分:

假设我们希望使用这个表格来训练一个预测模型,我们应该首先考虑将所有变量更改为数值类型。

  1. 最简单的列是布尔列表:in_stock。在使用该列来训练预测模型之前,应将其转换为数值型数据,例如 0 和 1。这可以通过多种方式实现,例如运行包含以下代码的单元格:df.in_stock = df.in_stock.map({False: 0, True: 1})

  1. 另一种编码特征的选择是 scikit-learn 的 LabelEncoder,它可以用于将类别标签映射为整数,方法上更为简便。我们通过运行包含以下代码的单元格来测试这一点:
    from sklearn.preprocessing import LabelEncoder
    rating_encoder = LabelEncoder()
    _df = df.copy()
    _df.rating = rating_encoder.fit_transform(df.rating)
    _df 

这可能会让你想起我们在上一章构建多项式模型时进行的预处理。在这里,我们实例化一个标签编码器,然后“训练”它并使用fit_transform方法“转换”我们的数据。我们将结果应用于我们的数据框副本_df

  1. 然后,可以使用我们在变量rating_encoder中引用的类,通过运行rating_encoder.inverse_transform(df.rating)将特征转换回来:

你可能会注意到一个问题。我们正在处理一个所谓的“序数”特征,其中标签具有固有的顺序。在这种情况下,我们应该期望“低”评级会被编码为 0,而“高”评级会被编码为 2。然而,结果并不是我们所看到的那样。为了实现正确的序数标签编码,我们应该再次使用 map,并自行构建字典。

  1. 通过运行包含以下代码的单元格,正确地编码序数标签:
    ordinal_map = {rating: index for index, rating in enumerate
    (['low','medium', 'high'])}
    print(ordinal_map)
    df.rating = df.rating.map(ordinal_map) 

我们首先创建映射字典。通过字典推导式和枚举来实现,但从结果来看,这个字典也可以很容易地手动定义。然后,就像之前处理in_stock列时那样,我们将字典映射应用到特征上。从结果来看,我们可以看到评分(rating)现在比之前更有意义,其中low被标记为 0,medium为 1,high为 2。

现在我们已经讨论了有序特征,让我们简单谈一下另一种类型的特征,即名义特征(nominal features)。这些字段没有固有的顺序,在我们的例子中,product就是一个完美的例子。

大多数 scikit-learn 模型可以在这样的数据上进行训练,其中我们使用字符串而非整数编码标签。在这种情况下,必要的转换是在幕后完成的。然而,这并不适用于所有 scikit-learn 模型,或者其他机器学习和深度学习库。因此,最好在预处理阶段自行进行编码。

  1. 一个常用的技术是将类别标签从字符串转换为数值,这称为独热编码(one-hot encoding)。这种方法将不同的类别拆分成独立的特征。可以通过pd.get_dummies()优雅地完成。执行以下代码单元来实现:df = pd.get_dummies(df)

最终的 DataFrame 如下所示:

在这里,我们看到了独热编码的结果:product列被拆分成了 4 列,每列代表一个唯一的值。在每一列中,我们看到 1 或 0,表示该行是否包含特定的值或产品。

接下来,忽略数据缩放(通常应该进行数据缩放),最后一步是将数据划分为训练集和测试集,供机器学习使用。这可以通过使用 scikit-learn 的 train_test_split 来完成。假设我们要预测某个项目是否有库存,基于其他特征值。

  1. 通过运行以下代码单元来将数据划分为训练集和测试集:
    features = ['price', 'rating', 'product_blue pants',
    'product_red shirt', 'product_white dress',
    'product_white tuxedo']
    X = df[features].values
    target = 'in_stock'
    y = df[target].values
    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = \
    train_test_split(X, y, test_size=0.3) 

在这里,我们选择数据的子集并将其输入train_test_split函数。此函数有四个输出,分别解包为特征(X)和目标(y)的训练集和测试集。

观察输出数据的形状,其中测试集大约包含 30%的样本,而训练集大约包含 70%的样本。

稍后,我们会看到类似的代码块,用于准备真实数据以用于训练预测模型。

这部分内容结束了,涉及到为机器学习应用清理数据的过程。我们花点时间注意一下,我们的 Jupyter Notebook 在测试各种数据转换方法时的效果,并最终记录了我们选择的处理流程。这可以轻松应用于数据的更新版本,只需要在处理之前调整特定代码单元。并且,如果我们想对处理流程进行任何更改,这些更改可以轻松地在笔记本中进行测试,特定的单元格可以更改以适应调整。最好的方法可能是将笔记本复制到一个新文件中,这样我们始终可以保留原始分析的副本作为参考。

接下来进入一个活动,我们将应用本节的概念,对大型数据集进行处理,并为训练预测模型做准备。

活动:为员工留任问题准备训练预测模型

假设你被聘请为一家公司做自由职业工作,帮助他们找出员工流失的原因。他们已经收集了一些认为有助于此目的的数据,包括员工满意度、评估、工作时长、部门和薪水等详细信息。

公司通过发送名为hr_data.csv的文件与您共享他们的数据,并询问您认为可以做些什么来帮助减少员工流失。将我们目前所学的概念应用到实际问题中,特别是我们旨在:

  • 根据可用数据,确定使用预测分析提供有影响力的商业洞察的计划。

  • 为机器学习模型准备数据。

从这一活动开始,直到本章的剩余部分,我们将使用人力资源分析,这是一个 Kaggle 数据集。我们在本书中使用的数据集与在线版本之间有一个小的区别。我们的员工资源分析数据包含一些NaN值,而这些数据在在线版本中已被手动移除。

数据集用于展示数据清洗技巧。我们还添加了一个名为is_smoker的数据列,目的是为了相同的目的。

  1. 打开chapter-2-workbook.ipynb笔记本文件,滚动到活动部分。

  2. 运行以下代码检查表头:

      %%bash
      head ../data/hr-analytics/hr_data.csv 

根据输出结果,确保它看起来是标准的 CSV 格式。对于 CSV 文件,我们应该能够简单地通过pd.read_csv加载数据。

  1. 通过运行df = pd.read_csv('../data/hranalytics/hr_data.csv')来使用 Pandas 加载数据。使用自动完成功能来帮助输入文件路径。

  2. 通过打印df.columns来检查列,确保数据已按预期加载,然后通过df.head()df.tail()打印 DataFrame 的前后部分:

我们可以看到它似乎已正确加载。根据尾部索引值,数据有将近 15,000 行;让我们确保没有遗漏任何数据。

  1. 使用以下代码检查CSV 文件中的行数(包括标题):
    with open('../data/hr-analytics/hr_data.csv') as f:
    print(len(f.read().splitlines())) 

  1. 将此结果与len(df)进行比较,以确保我们已加载所有数据:

现在,客户的数据已经正确加载,让我们思考如何使用预测分析找出员工离职的原因。

让我们从创建预测分析计划的第一步开始:

    • 查看可用数据:我们已经通过查看列、数据类型和样本数量完成了这一步

    • 确定业务需求:客户已经明确表达了他们的需求:减少离职员工的数量

    • 评估数据的适用性:让我们尝试制定一个计划,以便根据提供的数据帮助满足客户的需求

回想一下,正如前面提到的,有效的分析技术能带来具有影响力的商业决策。考虑到这一点,如果我们能预测员工离职的可能性,企业就可以有针对性地对这些员工进行特别处理。例如,可以提高他们的薪水或减少他们的项目数量。此外,利用模型还可以估算这些变化的影响!

为了评估这个计划的有效性,让我们思考一下我们的数据。每一行代表一个员工,员工要么在公司工作,要么已离职,这由名为“left”的列标记。因此,我们可以训练一个模型,根据一组特征来预测这个目标。

评估目标变量。通过运行以下代码检查分布和缺失值数量:

    df.left.value_counts().plot('barh')
    print(df.left.isnull().sum()) 

这是第二行代码的输出:

大约四分之三的样本是没有离职的员工,离职的员工占其余四分之一。这告诉我们我们正在处理一个不平衡的分类问题,这意味着在计算准确度时,我们需要采取特别措施来考虑每个类别。我们还发现,目标变量没有缺失值(没有NaN值)。

现在,我们来评估特征:

  1. 执行df.dtypes打印每列的数据类型。请注意,我们有连续型和离散型特征的混合:

  1. 运行以下代码显示特征分布:
    for f in df.columns:
    try:
    fig = plt.figure()
    …
    print('-'*30) 

这段代码稍显复杂,但它非常有用,可以展示数据集中连续型和离散型特征的概览。实际上,它假设每个特征都是连续的,并尝试绘制其分布;如果特征是离散的,它会转而绘制值计数。

结果如下:

对于许多特征,我们看到其值分布广泛,表明特征空间具有很好的多样性。这是令人鼓舞的;如果特征值紧密分布在一个小范围内,可能对模型来说不太有用。promotion_last_5years就是这种情况,我们可以看到绝大多数样本的值为 0。

接下来,我们需要做的是从数据集中移除所有的NaN值。

  1. 通过运行以下代码来检查每列中有多少NaN值:

    df.isnull().sum() / len(df) * 100

我们可以看到,大约有 2.5%的average_monthly_hours数据缺失,time_spend_company有 1%的数据缺失,而is_smoker有 98%的数据缺失!让我们使用一些已经学到的策略来处理这些缺失值。

  1. 由于is_smoker指标几乎没有任何信息,让我们删除这一列。通过运行以下代码来实现:del df['is_smoker']

  2. 由于time_spend_company是一个整数字段,我们将使用中位数值来填充该列中的NaN值。这可以通过以下代码实现:

    fill_value = df.time_spend_company.median()
    df.time_spend_company = df.time_spend_company.fillna(fill_value) 

处理的最后一列是average_montly_hours。我们可以做类似的操作,使用中位数或四舍五入后的均值作为整数填充值。不过,我们不妨尝试利用它与另一个变量之间的关系。这可能使我们能够更准确地填充缺失的数据。

  1. 绘制average_monthly_hoursnumber_project分组的箱形图。可以通过运行以下代码来实现:
  sns.boxplot(x='number_project', y='average_montly_hours', data=df) 

我们可以看到项目数量与average_monthly_hours之间的相关性,这一结果并不令人惊讶。我们将利用这种关系,通过根据每个样本的项目数量,使用该组的均值来填充average_monthly_hours中的NaN值。

  1. 运行以下代码来计算每个组的均值:
    mean_per_project = df.groupby('number_project')\.
    average_montly_hours.mean()
    mean_per_project = dict(mean_per_project)
    print(mean_per_project) 

然后,我们可以将其映射到number_project列,并将结果序列对象作为参数传递给fillna

  1. 通过执行以下代码来填充average_monthly_hours中的NaN值:
      fill_values = df.number_project.map(mean_per_project)
      df.average_montly_hours = df.average_montly_hours.fillna(fill_values) 
  1. 通过运行以下断言测试,确认df中没有更多的NaN值。如果没有抛出错误,那么就说明你已经成功地从表格中移除了NaN

    assert df.isnull().sum().sum() == 0

  2. 最后,我们将把字符串和布尔字段转换为整数表示。特别地,我们将手动将目标变量leftyesno转换为10,并构建独热编码特征。通过运行以下代码来实现:

  df.left = df.left.map({'no': 0, 'yes': 1})
  df = pd.get_dummies(df) 
  1. 打印df.columns以显示字段:

我们可以看到,departmentsalary已经被拆分成了多个二元特征。

准备机器学习数据的最后一步是特征缩放,但出于各种原因(例如,某些模型不需要缩放),我们将在下一个活动的模型训练工作流中进行这一步骤。

  1. 我们已经完成了数据预处理,准备进入模型训练阶段!让我们通过运行以下代码来保存我们预处理过的数据:
     df.to_csv('../data/hr-analytics/hr_data_processed.csv', index=False) 

再次提醒,在执行初步数据分析和清理时,Jupyter Notebook 是多么适合我们的需求。举个例子,假设我们把这个项目放置在当前状态下几个月后再回来。那时,我们可能已经不记得我们离开时到底发生了什么。然而,参考回这个笔记本,我们就能重新追溯我们的步骤,并迅速回忆起之前对数据的学习内容。此外,我们还可以用新的数据更新数据源,并重新运行笔记本,准备新数据集以便用于我们的机器学习算法。请记住,在这种情况下,最好首先复制笔记本,以免丢失最初的分析。

总结一下,我们已经学习并应用了训练机器学习模型的准备方法。我们从讨论如何识别可以通过预测分析解决的问题的步骤开始。这包括:

  • 查看可用数据

  • 确定业务需求

  • 评估数据的适用性

我们还讨论了如何识别有监督学习与无监督学习,以及回归问题与分类问题。

在识别问题后,我们学习了使用 Jupyter Notebooks 构建和测试数据转换管道的技巧。这些技巧包括处理缺失数据、转换分类特征和构建训练/测试数据集的方法和最佳实践。

在本章的其余部分,我们将使用这些预处理过的数据来训练各种分类模型。为了避免盲目应用我们不了解的算法,我们首先介绍这些算法并概述它们的工作原理。然后,我们使用 Jupyter 训练并比较它们的预测能力。在这里,我们有机会讨论机器学习中的一些更高级的主题,比如过拟合、k 折交叉验证和验证曲线。

训练分类模型

正如我们在上一章中已经看到的,使用诸如 scikit-learn 这样的库和 Jupyter 等平台,预测模型可以通过几行代码进行训练。这是通过抽象化优化模型参数所涉及的复杂计算来实现的。换句话说,我们处理的是一个“黑盒”,其中内部操作被隐藏起来。正是这种简化带来了误用算法的风险,例如,在训练过程中过度拟合,或者未能在未见过的数据上进行正确测试。我们将展示如何避免在训练分类模型时遇到这些陷阱,并通过使用 k 折交叉验证和验证曲线来产生可信的结果。

分类算法简介

回顾两种监督式机器学习方法:回归和分类。在回归中,我们预测一个连续的目标变量。例如,回忆一下第一章中的线性和多项式模型。在本章中,我们关注的是另一种监督式机器学习方法:分类。这里的目标是使用可用的度量标准来预测样本的类别。

在最简单的情况下,只有两种可能的类别,这意味着我们正在进行二分类。这是本章示例问题的情况,我们试图预测一个员工是否离职。如果我们有两个以上的类别标签,那么我们就是在进行多分类。

尽管使用 scikit-learn 训练模型时,二分类和多分类之间几乎没有差别,但在“黑盒”内部进行的操作却明显不同。特别是,多分类模型通常使用一对多方法。对于具有三个类别标签的情况,方法如下:当模型“拟合”数据时,训练三个模型,每个模型预测样本是否属于某个特定类别或其他类别。这可能会让人联想到我们之前做的特征的独热编码。当对一个样本进行预测时,返回的是具有最高置信度的类别标签。

在本章中,我们将训练三种类型的分类模型:支持向量机、随机森林和 k-近邻分类器。这些算法各不相同。然而,正如我们将看到的,由于使用了 scikit-learn,它们在训练和用于预测时是非常相似的。在切换到 Jupyter Notebook 并实现这些模型之前,我们将简要了解它们的工作原理。支持向量机尝试找到最佳超平面以区分不同类别。这是通过最大化超平面与每个类别最近样本之间的距离来实现的,这些样本被称为支持向量。

这种线性方法也可以通过核技巧来建模非线性类别。该方法将特征映射到一个更高维度的空间,在其中确定超平面。我们所讨论的这个超平面也被称为决策面,我们将在训练模型时可视化它。

k-最近邻分类算法记住训练数据,并根据特征空间中最近的 K 个样本进行预测。对于三个特征,这可以通过一个包围预测样本的球体来可视化。然而,通常我们处理的不止三个特征,因此绘制超球体来找到最近的 K 个样本。

随机森林是一组决策树,每棵树都在不同的训练数据子集上训练。

决策树算法通过一系列决策对样本进行分类。例如,第一个决策可能是“如果特征 x_1 小于或大于 0”。然后,数据会根据这个条件被分割,并输入到树的下行分支中。决策树中的每一步都根据最大化信息增益的特征分割来做出决策。

本质上,这个术语描述了试图选择目标变量最佳分割的数学方法。

训练随机森林的过程包括为一组决策树创建自助采样(即带替换的随机抽样数据)数据集。然后根据多数投票做出预测。这些模型的好处是减少过拟合,并且具有更好的泛化能力。

决策树可以用来建模连续数据和分类数据的混合,这使得它们非常有用。此外,正如我们在本章后面会看到的那样,可以通过限制树的深度来减少过拟合。要详细了解(但简洁的)决策树算法,可以查看这个流行的 Stack Overflow 回答:stackoverflow. com/a/1859910/3511819。在那里,作者展示了一个简单的示例,并讨论了节点纯度、信息增益和熵等概念。

使用 scikit-learn 训练双特征分类模型

我们将继续处理在第一个话题中介绍的员工留存问题。我们之前准备了一个数据集来训练分类模型,预测员工是否离职。现在,我们将使用这个数据来训练分类模型:

  1. 如果你还没有这样做,请启动 NotebookApp 并打开 chapter-2-workbook.ipynb 文件。向下滚动到 Topic Training classification models。运行前几行代码来设置默认图像大小并加载我们之前保存到 CSV 文件 的处理数据。

在这个例子中,我们将训练基于两个连续特征的分类模型:

satisfaction_levellast_evaluation

  1. 通过运行包含以下代码的单元格,绘制连续目标变量的双变量和单变量图:
  sns.jointplot('satisfaction_level', 
 'last_evaluation', data=df, kind='hex') 

如前面的图片所示,数据中存在一些非常明显的模式。

  1. 通过运行包含以下代码的单元格,重新绘制目标变量分段的双变量分布:
      plot_args = dict(shade=True, shade_lowest=False)
      for i, c in zip((0, 1), ('Reds', 'Blues')):
      sns.kdeplot(df.loc[df.left==i, 'satisfaction_level'],
          df.loc[df.left==i, 'last_evaluation'],
          cmap=c, **plot_args) 

现在,我们可以看到这些模式与目标变量之间的关系。接下来,我们将尝试利用这些模式来训练有效的分类模型。

  1. 通过运行包含以下代码的单元格,将数据划分为训练集和测试集:
     from sklearn.model_selection import train_test_split
     features = ['satisfaction_level', 'last_evaluation']
        X_train, X_test, y_train, y_test = 
        train_test_split(df[features].values, df['left'].values,
        test_size=0.3, random_state=1) 

我们的前两个模型——支持向量机和 k 最近邻算法——在输入数据进行缩放,使所有特征处于相同的数量级时最为有效。我们将通过 scikit-learn 的 StandardScaler 来完成这一点。

  1. 加载 StandardScaler 并创建一个新的实例,如变量 scaler 所引用。将 scaler 拟合到训练集并进行转换,然后转换测试集。运行包含以下代码的单元格:
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    X_train_std = scaler.fit_transform(X_train)
    X_test_std = scaler.transform(X_test) 

在进行机器学习时,一个常见的错误是将 scaler 拟合到整个数据集,而实际上它应该只拟合训练数据。例如,在将数据划分为训练集和测试集之前进行缩放是错误的。我们不希望这样做,因为模型训练不应受到测试数据的任何影响。

  1. 导入 scikit-learn 的支持向量机类,并通过运行包含以下代码的单元格,将模型拟合到训练数据上:
    from sklearn.svm import
    SVC svm = SVC(kernel='linear', C=1, random_state=1)
    svm.fit(X_train_std, y_train)

接下来,我们训练一个线性支持向量机分类模型。C 参数控制误分类的惩罚,从而可以控制模型的方差和偏差。

  1. 通过运行包含以下代码的单元格,计算此模型在未见数据上的准确度:
    from sklearn.metrics import accuracy_score
    y_pred = svm.predict(X_test_std)
    acc = accuracy_score(y_test, y_pred)
    print('accuracy = {:.1f}%'.format(acc*100))
    >> accuracy = 75.9% 

我们预测测试样本的目标值,然后使用 scikit-learn 的 accuracy_score 函数来确定准确度。结果看起来很有前景,大约为 75%!对于我们的第一个模型来说,这还不错。不过,请记住,目标变量是不平衡的。让我们看看每个类别的预测准确度。

  1. 计算混淆矩阵,然后通过运行包含以下代码的单元格来确定每个类别内的准确度:
      from sklearn.metrics import confusion_matrix
      cmat = confusion_matrix(y_test, y_pred)
      scores = cmat.diagonal() / cmat.sum(axis=1) * 100
      print('left = 0 : {:.2f}%'.format(scores[0]))
      print('left = 1 : {:.2f}%'.format(scores[1]))
      >> left = 0 : 100.00%
      >> left = 1 : 0.00% 

看起来模型只是将每个样本都分类为 0,这显然是完全没有帮助的。让我们使用等高线图显示特征空间中每个点的预测类别。这通常被称为决策区域图。

  1. 使用 mlxtend 库中的一个有用函数绘制决策区域。运行包含以下代码的单元格:
      from mlxtend.plotting import plot_decision_regions
      N_samples = 200
      X, y = X_train_std[:N_samples], y_train[:N_samples]
      plot_decision_regions(X, y, clf=svm) 

该函数绘制决策区域以及作为参数传递的一组样本。为了正确看到决策区域而不受太多样本阻碍视线,我们仅传递了测试数据的一个包含 200 个样本的子集给plot_decision_regions函数。在这种情况下,当然无关紧要。我们看到结果完全是红色的,表示特征空间中的每个点都将被分类为 0。

不出意料,线性模型无法很好地描述这些非线性模式。回想一下,我们先前提到了使用 SVM 分类非线性问题的内核技巧。让我们看看这样做是否可以改善结果。

  1. 运行包含 SVC 单元格以打印 scikit-learn SVM 的文档字符串。向下滚动并查看参数描述。请注意kernel选项,默认情况下实际上是启用的rbf。使用此内核选项训练新的 SVM,运行包含以下代码的单元格:
     svm = SVC(kernel='rbf', C=1, random_state=1)
     svm.fit(X_train_std, y_train)
  1. 为了更轻松地评估此及未来模型的性能,让我们定义一个名为check_model_fit的函数,计算各种度量标准,以便比较模型。运行定义此函数的单元格。

此函数中进行的每个计算在此示例中均已看到;它仅计算准确性并绘制决策区域。

  1. 运行包含以下代码的单元格,显示对训练数据进行新训练的核支持向量机(kernel-SVM)的结果:
  check_model_fit(svm, X_test_std, y_test) 

结果好多了。现在,我们能够捕捉数据中一些非线性模式,并正确分类大多数离职员工。

绘制决策区域函数

plot_decision_regions函数由 Sebastian Raschka开发的 Python 库mlxtend提供。值得一看源代码(当然是用 Python 编写的)以理解这些绘图是如何绘制的。这并不是太复杂。

在 Jupyter Notebook 中,从mlxtend.plotting导入plot_decision_regions函数,然后使用plot_decision_regions?查看帮助,并滚动到底部查看本地文件路径:

然后,打开文件并查看它!例如,你可以在笔记本中运行cat

这样也可以,但是对于代码没有颜色标记不理想。最好复制它(以免意外更改原始内容),然后使用您喜爱的文本编辑器打开它。

当注意绘制决策区域映射代码时,我们看到预测Z在跨越特征空间的数组X_predict上的等高线图。

让我们继续下一个模型:k 最近邻。

为我们的模型训练 k 最近邻

  1. 加载 scikit-learn 的 KNN 分类模型,并通过运行包含以下代码的单元格来打印文档字符串:
      from sklearn.neighbors import KNeighborsClassifier
      KNeighborsClassifier?

n_neighbors 参数决定了在进行分类时使用多少个样本。如果权重参数设置为均匀(uniform),则类别标签由多数投票决定。另一个有用的权重选择是距离,其中距离较近的样本在投票中具有较高的权重。像大多数模型参数一样,这个选择的最佳值取决于特定的数据集。

  1. 训练一个 KNN 分类器,n_neighbors=3,然后计算准确率和决策区域。运行包含以下代码的单元格:
    knn = KNeighborsClassifier(n_neighbors=3)
    knn.fit(X_train_std, y_train) 

    check_model_fit(knn, X_test_std, y_test) 

我们看到整体准确度有所提高,尤其是类别 1 的准确度有了显著改善。然而,决策区域图表表明我们正在对数据进行过拟合。通过硬性的、"锯齿状"的决策边界以及随处可见的小块蓝色区域可以明显看出这一点。我们可以通过增加最近邻的数量来软化决策边界,并减少过拟合。

  1. 通过运行包含以下代码的单元格,训练一个 n_neighbors=25 的 KNN 模型:
    knn = KNeighborsClassifier(n_neighbors=25)
    knn.fit(X_train_std, y_train)
    check_model_fit(knn, X_test_std, y_test) 

如我们所见,决策边界明显不再那么锯齿状,蓝色区域也大大减少。类别 1 的准确率略有下降,但我们需要使用更全面的方法,比如 k 折交叉验证,来决定是否

这两个模型之间有显著的差异。

请注意,增加n_neighbors对训练时间没有影响,因为模型仅仅是在记忆数据。然而,预测时间会受到很大影响。

在使用真实世界数据进行机器学习时,算法的运行速度足够快以达到其目的非常重要。例如,预测明天天气的脚本如果运行时间超过一天,那就完全没有用!内存也是一个需要考虑的因素,尤其是在处理大量数据时。

训练随机森林

观察训练和预测每个模型的相似性,尽管它们内部差异很大。

  1. 训练一个由 50 棵决策树组成的随机森林分类模型,每棵树的最大深度为 5。运行包含以下代码的单元格:
       from sklearn.ensemble import RandomForestClassifier
       forest = RandomForestClassifier(n_estimators=50,
       max_depth=5,
       random_state=1)
       forest.fit(X_train, y_train)
       check_model_fit(forest, X_test, y_test) 

注意决策树机器学习算法所产生的独特的轴对齐决策边界。

我们可以访问构建随机森林时使用的任何单个决策树。这些树被存储在模型的 estimators_attribute 中。让我们绘制其中一棵决策树,以便了解其中的内容。执行此操作需要图形可视化依赖项,安装起来有时可能会有些困难。

  1. 通过运行包含以下代码的单元格,在 Jupyter Notebook 中绘制其中一棵决策树:
    from sklearn.tree import export_graphviz
    import graphviz
    dot_data = export_graphviz(
        forest.estimators_[0],
        out_file=None,
        feature_names=features,
        class_names=['no', 'yes'],
        filled=True, rounded=True,
        special_characters=True)
    graph = graphviz.Source(dot_data)
    graph 

我们可以看到,每条路径被限制为五个节点,这是由于设置了max_depth=5。橙色框表示no(没有离开公司)的预测,蓝色框表示yes(已经离开公司)的预测。每个框的阴影(浅色、深色等)表示置信度,和gini值相关。

总结一下,我们已经完成了本节中的两个学习目标:

  • 我们获得了对支持向量机(SVM)、k-近邻分类器(kNN)和随机森林的定性理解。

  • 我们现在能够使用 scikit-learn 和 Jupyter Notebook 训练多种模型,从而有信心构建和比较预测模型。

特别地,我们使用了员工离职问题的预处理数据来训练分类模型,预测员工是否已经离开公司。为了简化问题并专注于算法,我们构建了一个模型,仅根据两个特征来预测:满意度和最后评估值。这种二维特征空间还使我们能够可视化决策边界,并识别过拟合的表现。

在接下来的部分中,我们将介绍机器学习中的两个重要主题:k 折交叉验证和验证曲线。

使用 k 折交叉验证和验证曲线评估模型

到目前为止,我们已经在数据的一个子集上训练了模型,然后在未见过的部分——即测试集——上评估了性能。这是一个良好的做法,因为模型在训练数据上的表现并不能很好地反映它作为预测器的效果。通过过拟合一个模型,很容易在训练数据集上提高准确率,但这可能会导致在未见过数据上的表现较差。

也就是说,仅仅在这样分割的数据上训练模型是不够的。数据中存在自然的变异,导致不同的训练和测试分割下准确率有所不同(即使是轻微的)。此外,仅使用一个训练/测试分割来比较模型可能会引入偏向某些模型的偏差,并导致过拟合。

k 折 交叉验证 提供了解决这个问题的方案,并通过每次准确性计算的误差估计来考虑数据的变异性。这反过来又自然地引出了使用验证曲线来调整模型参数。这些曲线绘制了准确率与超参数之间的关系,比如在随机森林中使用的决策树数量或最大深度。

这是我们第一次使用超参数这个术语。它指的是在初始化模型时定义的参数,例如 SVM 的 C 参数。这与训练好的模型参数不同,后者是训练好的 SVM 的决策边界超平面方程。

该方法在下图中进行了说明,我们可以看到如何从数据集中选择 k 个折叠:

k 折交叉验证算法如下:

  1. 将数据划分为大小接近的 k 个“折叠”。

  2. 在不同的折叠组合上测试和训练 k 个模型。每个模型将包括k - 1个训练数据折叠,留下的折叠用于测试。在这种方法中,每个折叠最终都会作为验证数据使用一次。

  3. 通过取 k 个值的均值来计算模型的精度。同时计算标准偏差,以便为该值提供误差条。

通常设置k = 10,但如果使用大数据集,应考虑选择较小的 k 值。

这种验证方法可以可靠地比较不同超参数下的模型性能(例如,SVM 的 C 参数或 KNN 分类器中的最近邻数量)。它也适用于比较完全不同的模型。

一旦识别出最佳模型,应在整个数据集上重新训练该模型,然后再用于预测实际分类。

在使用 scikit-learn 实现时,通常会使用一种稍微改进的普通 k 折算法。这被称为分层 k 折。其改进之处在于,分层 k 折交叉验证在折叠中保持大致均衡的类标签分布。正如你想象的那样,这减少了模型的整体方差,并降低了高度不平衡的模型造成偏差的可能性。

验证曲线是训练和验证度量值与某些模型参数的函数的图表。它们帮助我们做出合理的模型参数选择。在本书中,我们将使用精度得分作为这些图表的度量标准。

有关绘制验证曲线的文档,请访问: http://scikit-learn.org/stable/auto_examples/model_selection/plot_validation_curve.html

考虑这个验证曲线,其中精度得分作为 gamma SVM 参数的函数进行绘制:

从图表的左侧开始,我们可以看到两组数据在得分上是一致的,这是好现象。然而,得分与其他 gamma 值相比也相对较低,因此我们认为模型存在欠拟合数据的情况。随着 gamma 值的增加,我们可以看到一个点,此时这两条线的误差条不再重叠。从这个点开始,我们看到分类器开始对数据进行过拟合,因为模型在训练集上的表现越来越好,而在验证集上的表现相对较差。通过寻找误差条重叠的两条线上的高验证得分,我们可以找到 gamma 参数的最优值。

请记住,对于某个参数的学习曲线只有在其他参数保持不变的情况下才有效。例如,如果在此图中训练 SVM,我们可以选择γ值为 10⁻⁴。但我们也可能希望优化 C 参数。C 的不同值会导致前面的图像发生变化,我们选择的γ值可能不再是最优的。

在 Python 中使用 K 折交叉验证和验证曲线,结合 scikit-learn 工具。

  1. 如果你还没有这样做,启动NotebookApp并打开chapter-2- workbook.ipynb 文件。向下滚动到子主题K-fold 交叉验证验证曲线部分。

训练数据应该已经加载到笔记本的内存中,但为了提醒我们正在使用的数据内容,我们可以重新加载它。

  1. 加载数据并选择satisfaction_levellast_evaluation特征作为训练/验证集。我们这次不使用训练集和测试集的划分,因为我们将使用 K 折交叉验证。运行包含以下代码的单元:
      df = pd.read_csv('../data/hr-analytics/hr_data_processed.csv')
      features = ['satisfaction_level', 'last_evaluation']
      X = df[features].values
      y = df.left.values 
  1. 通过运行包含以下代码的单元来实例化一个随机森林模型:
   clf = RandomForestClassifier(n_estimators=100, max_depth=5) 
  1. 为了使用分层 K 折交叉验证训练模型,我们将使用model_selection.cross_val_score函数。

使用分层 K 折验证训练我们模型clf的 10 个变种。注意,scikit-learn 的cross_val_score默认执行这种类型的验证。运行包含以下代码的单元:

    from sklearn.model_selection import cross_val_score
    np.random.seed(1)
    scores = cross_val_score(
        estimator=clf,
        X=X,
        y=y,
        cv=10)
    print('accuracy = {:.3f} +/- {:.3f}'.format(scores.mean(), scores.
    std()))
    >> accuracy = 0.923 +/- 0.005 

注意我们如何使用np.random.seed来设置随机数生成器的种子,从而确保每一折和随机森林中的每棵决策树在随机选择样本时具有可重复性。

  1. 通过这种方法,我们将准确度计算为每折的平均值。我们还可以通过打印分数来查看每一折的单独准确度。要查看这些,请运行print(scores)
    >> array([ 0.93404397, 0.91533333, 0.92266667, 0.91866667,
        0.92133333,
        0.92866667, 0.91933333, 0.92 , 0.92795197, 0.92128085]) 

使用cross_val_score非常方便,但它没有告诉我们每个类的准确度。我们可以通过model_selection中的StratifiedKFold类手动执行此操作。该类以折数作为初始化参数,然后使用 split 方法为数据构建随机抽样的“掩码”。掩码只是一个数组,包含另一个数组中项目的索引,之后可以通过data[mask]来返回这些项目。

  1. 定义一个自定义类来计算 K 折交叉验证的分类准确度。运行包含以下代码的单元:
      from sklearn.model_selection import StratifiedKFold
      …
          print('fold: {:d} accuracy: {:s}'.format(k+1, str(class_acc)))
      return class_accuracy 
  1. 我们可以通过类似第 4 步的代码来计算分类准确度。执行以下代码来实现:
    from sklearn.model_selection import cross_val_score
    np.random.seed(1)
    …
    >> fold: 10 accuracy: [ 0.98861646 0.70588235]
    >> accuracy = [ 0.98722476 0.71715647] +/- [ 0.00330026 0.02326823] 

现在我们可以看到每折的分类准确度了!是不是很酷?

  1. 接下来我们展示如何使用model_selection.validation_curve计算验证曲线。该函数使用分层 K 折交叉验证来训练模型,以求得给定参数的不同值。

进行必要的计算,通过训练随机森林模型在一系列 max_depth 值上绘制验证曲线。运行包含以下代码的单元格:

    from sklearn.model_selection import validation_curve

    clf = RandomForestClassifier(n_estimators=10)
    max_depths = np.arange(3, 16, 3)
    train_scores, test_scores = validation_curve(
        estimator=clf,
        X=X,
        y=y,
        param_name='max_depth',
        param_range=max_depths,
        cv=10);

这将返回每个模型的交叉验证得分数组,其中模型具有不同的最大深度。为了可视化结果,我们将利用在 scikit-learn 文档中提供的一个函数。

  1. 运行定义了 plot_validation_curve 的单元格。然后,运行包含以下代码的单元格以绘制图表:
    plot_validation_curve(train_scores, test_scores, max_depths,
    xlabel='max_depth')

回想一下,设置决策树的最大深度如何限制过拟合?这在验证曲线中得到了体现,我们看到在较大的 max_depth 值右侧发生了过拟合。max_depth 的一个良好值似乎是 6,此时训练和验证的准确率一致。当 max_depth 等于 3 时,我们看到模型在训练和验证准确率较低的情况下出现了欠拟合。

总结一下,我们已经学习并实现了两种重要的构建可靠预测模型的技术。第一种技术是 k 折交叉验证,它用于将数据拆分为多个训练/测试批次并生成准确率。然后,我们从这些准确率中计算出平均值和标准差,作为误差的衡量标准。这很重要,因为它可以帮助我们衡量模型的可变性,并提供可信的准确率。

我们还学到了另一种确保结果可信的技术:验证曲线。这些曲线可以帮助我们通过比较训练和验证准确率,直观地看到模型是否发生了过拟合。通过在选定的超参数范围内绘制曲线,我们能够识别其最优值。

在本章的最后部分,我们将迄今为止学到的所有内容结合起来,构建我们最终的员工流失问题预测模型。我们希望通过将数据集中的所有特征包含在模型中,相较于之前训练的模型,提高模型的准确性。我们将再次看到熟悉的主题,如 k 折交叉验证和验证曲线,但也会介绍一些新的内容:降维技术。

降维技术

降维可以简单地通过从训练数据中删除不重要的特征来实现,但也存在一些更复杂的方法,如主成分分析(PCA)线性判别分析(LDA)。这些技术允许数据压缩,将从大量特征中提取的最重要信息压缩成少数几个特征。

在这个子主题中,我们将重点介绍 PCA。这项技术通过将数据投影到一个新的正交“主成分”子空间来转换数据,其中具有最高特征值的成分编码了用于训练模型的大部分信息。然后,我们可以选择这些主成分中的一部分来代替原始的高维数据集。例如,PCA 可以用来编码图像中每个像素的信息。在这种情况下,原始特征空间的维度等于图像中像素的数量。然后,可以使用 PCA 来减少这个高维空间,在训练预测模型时,大部分有用的信息可能会减少到只有几个维度。这不仅节省了训练和使用模型的时间,还通过去除数据集中的噪声提高了模型的性能。

和我们之前看到的模型一样,利用 PCA 的好处并不需要对 PCA 有详细的理解。然而,我们将进一步深入 PCA 的技术细节,以便更好地理解它。PCA 的关键思想是通过基于相关性的特征间模式来识别特征,因此 PCA 算法计算协方差矩阵,并将其分解为特征向量和特征值。然后,使用这些向量将数据转换到一个新的子空间,从中可以选择一定数量的主成分。

在接下来的部分中,我们将看到一个例子,展示如何使用 PCA 来改善我们在员工留存问题上使用的随机森林模型。这将是在对完整特征空间训练分类模型之后进行的,以查看降维如何影响我们的准确性。

为员工留存问题训练预测模型

我们已经花费了大量的精力来规划机器学习策略、预处理数据并构建员工留存问题的预测模型。回顾一下我们的商业目标是帮助客户防止员工离职。我们决定的策略是建立一个分类模型,预测员工离职的概率。通过这种方式,公司可以评估当前员工离职的可能性,并采取措施加以防范。

根据我们的策略,我们可以总结出我们正在进行的预测建模类型如下:

  • 基于标记训练数据的监督学习

  • 具有两个类标签(即二分类问题)的分类问题

特别地,我们正在训练模型来确定员工是否已经离开公司,前提是有一组连续和类别特征。在活动为员工留任问题训练预测模型的准备中,我们已经准备好了机器学习数据,接着使用了 SVM、k 近邻和随机森林算法,且只用了两个特征。这些模型能够以 90%以上的总体准确率做出预测。然而,当查看具体类别的准确性时,我们发现,离职员工(class-label 1)的预测准确率只能达到 70-80%。让我们看看通过利用完整的特征空间能提高多少准确率。

  1. chapter-2-workbook.ipynb笔记本中,向下滚动到本节的代码。我们应该已经加载了前几节处理过的数据,但如果需要,可以通过执行df = pd.read_csv('../data/hr-analytics/hr_data_processed.csv')重新加载数据。然后,使用print(df.columns)打印 DataFrame 的列。

  2. 通过复制并粘贴df.columns的输出到一个新列表中来定义所有特征的列表(确保去掉目标变量)。然后,像之前一样定义XY。步骤如下:

    features = ['satisfaction_level', 'last_evaluation', 'number_project',
    'average_montly_hours', 'time_spend_company', 'work_accident',
        …
        X = df[features].values
        y = df.left.values 

查看特征名称,回忆每个特征的值是什么样的。向上滚动回到我们在第一项活动中制作的直方图,帮助提醒你记忆。前两个特征是连续型的,这些特征我们在之前的两次练习中用于训练模型。接下来,我们有一些离散特征,比如number_projecttime_spend_company,然后是一些二元特征,如work_accidentpromotion_last_5years。我们还有一些二进制特征,比如department_ITdepartment_accounting,这些特征是通过独热编码创建的。

对于这种混合特征类型的数据,随机森林是一种非常有吸引力的模型。首先,它们能够处理由连续型和类别数据组成的特征集,尽管这并不特别;例如,SVM 也可以在混合特征类型上进行训练(只要进行适当的预处理)。

如果你有兴趣使用 SVM 或 k 近邻分类器训练混合型输入特征,可以使用这个 StackExchange 回答中的数据缩放方法:stats.stackexchange.com/questions/82923/mixing-continuous-and-binary-data-with-linear-svm/83086#83086

一种简单的方法是按如下方式预处理数据:

    • 标准化连续变量

    • 对类别特征进行独热编码

    • 将二元值从01转换为-11

    • 然后,可以使用这些混合特征数据来训练各种分类模型。

  1. 我们需要为我们的随机森林模型找出最佳的参数。首先,通过使用验证曲线调整max_depth超参数。通过运行以下代码计算训练和验证的准确度:
    %%time
    np.random.seed(1)
    clf = RandomForestClassifier(n_estimators=20)
    max_depths = [3, 4, 5, 6, 7,
    9, 12, 15, 18, 21]
    train_scores, test_scores = validation_curve(
    estimator=clf,
        X=X,
        y=y,
    param_name='max_depth',
    param_range=max_depths,
    cv=5); 

我们正在使用 k 折交叉验证测试 10 个模型。通过设置k = 5,我们为每个模型生成五个准确度估计,从中提取平均值和标准差来绘制验证曲线。总共训练 50 个模型,由于n_estimators设为 20,我们一共训练了 1,000 棵决策树!所有这些大约只用了 10 秒钟!

  1. 使用我们在上一练习中创建的自定义plot_validation_curve函数绘制验证曲线。运行以下代码:
    plot_validation_curve(train_scores, test_scores,
    max_depths, xlabel='max_depth'); 

对于较小的最大深度,我们看到模型对数据出现欠拟合。通过允许决策树更深并编码数据中更复杂的模式,总体准确度显著提高。随着最大深度的进一步增加,准确度接近 100%,我们发现模型开始对数据过拟合,导致训练和验证准确度之间的差距增大。根据此图,我们选择将max_depth设为 6。

我们本该对n_estimators做同样的操作,但为了节省时间,我们将跳过这一部分。你可以自己绘制它;你会发现对于一个较大范围的值,训练集和验证集之间是一致的。通常,使用更多的决策树估计器来训练随机森林会更好,但这会增加训练时间。我们将使用 200 个估计器来训练我们的模型。

  1. 使用我们之前创建的cross_val_class_score函数(按类别进行 k 折交叉验证)来测试所选模型,随机森林,max_depth = 6n_estimators = 200
     np.random.seed(1) 
     clf = RandomForestClassifier(n_estimators=200, max_depth=6) 
     scores = cross_val_class_score(clf, X, y)
     print('accuracy = {} +/- {}'\ .format(scores.mean(axis=0),
     scores.std(axis=0)))
     >> accuracy = [ 0.99553722 0.85577359] +/- [ 0.00172575 0.02614334] 

现在我们使用完整的特征集,准确度明显提高,远高于之前仅使用两个连续特征时的情况!

  1. 通过运行以下代码使用箱线图可视化准确度:
    fig = plt.figure(figsize=(5, 7))
    sns.boxplot(data=pd.DataFrame(scores, columns=[0, 1]),
    palette=sns.color_palette('Set1'))
    plt.xlabel('Left')
    plt.ylabel('Accuracy') 

随机森林可以提供特征性能的估计。

在 scikit-learn 中,特征重要性是通过衡量每个特征对节点不纯度变化的贡献来计算的。关于如何确定随机森林分类器中的特征重要性,您可以查看以下 Stack Overflow 帖子:stackoverflow.com/questions/15810339/how-are-feature-importances-in-randomforestclassifier-determined

  1. 通过运行以下代码绘制存储在feature_importances_属性中的特征重要性:
    pd.Series(clf.feature_importances_, name='Feature importance',
        index=df[features].columns)\
        .sort_values()\
        .plot.barh()
    plt.xlabel('Feature importance')

看起来来自独热编码变量(部门和薪资)并没有提供太多有用的贡献。此外,promotion_last_5yearswork_accident特征似乎也不是很有用。

让我们使用主成分分析(PCA)将所有这些弱特征压缩成少数几个主成分。

  1. 从 scikit-learn 中导入 PCA 类,并转换特征。运行以下代码:
    from sklearn.decomposition import PCA
    pca_features = \
    …
    pca = PCA(n_components=3)
    X_pca = pca.fit_transform(X_reduce) 
  1. 输入并执行单独的X_pca来查看其字符串表示:
    >> array([[-0.67733089, 0.75837169, -0.10493685],
    >> [ 0.73616575, 0.77155888, -0.11046422],
    >> [ 0.73616575, 0.77155888, -0.11046422],
        >> ...,
    >> [-0.67157059, -0.3337546 , 0.70975452],
    >> [-0.67157059, -0.3337546 , 0.70975452],
    >> [-0.67157059, -0.3337546 , 0.70975452]]) 

由于我们要求返回前三个成分,因此会返回三个向量。

  1. 使用以下代码将新特征添加到我们的 DataFrame 中:
    df['first_principle_component'] = X_pca.T[0]
    df['second_principle_component'] = X_pca.T[1]
    df['third_principle_component'] = X_pca.T[2] 

选择我们降维后的特征集来训练新的随机森林。运行以下代码:

    features = ['satisfaction_level', 'number_project', 'time_spend_
        company',
    'average_montly_hours', 'last_evaluation',
    'first_principle_component',
    'second_principle_component',
    'third_principle_component']
    X = df[features].values
    y = df.left.values 
  1. 使用 k 折交叉验证评估新模型的准确性。可以通过运行与之前相同的代码来完成,其中 X 现在指向不同的特征。代码如下:
    np.random.seed(1)
    clf = RandomForestClassifier(n_estimators=200, max_depth=6)
    scores = cross_val_class_score(clf, X, y)
    print('accuracy = {} +/- {}'\.format(scores.mean(axis=0), 
    scores.std(axis=0)))
    >> accuracy = [ 0.99562463 0.90618594] +/- [ 0.00166047 0.01363927] 
  1. 以与之前相同的方式,使用箱线图可视化结果。代码如下:
    fig = plt.figure(figsize=(5, 7))
    sns.boxplot(data=pd.DataFrame(scores, columns=[0, 1]),
    palette=sns.color_palette('Set1'))
    plt.xlabel('Left')
    plt.ylabel('Accuracy') 

与之前的结果相比,我们发现类别 1 的准确性有所提升!现在,大多数验证集返回的准确率都超过了 90%。90.6%的平均准确率与降维前的 85.6%准确率相比,提升明显!

让我们选择这个作为最终模型。在投入生产之前,我们需要在整个样本空间上重新训练它。

  1. 通过运行以下代码训练最终的预测模型:
    np.random.seed(1)
    clf = RandomForestClassifier(n_estimators=200, max_depth=6)
    clf.fit(X, y)
  1. 使用externals.joblib.dump将训练好的模型保存到二进制文件中。运行以下代码:
    from sklearn.externals import joblib
    joblib.dump(clf, 'random-forest-trained.pkl') 
  1. 检查它是否已保存到工作目录中,例如,运行:!ls *.pkl。然后,通过运行以下代码,测试我们能否从文件中加载模型:
     clf = joblib.load('random-forest-trained.pkl') 

恭喜!我们已经训练好了最终的预测模型!现在,让我们看一个示例,了解它如何为客户提供业务洞察。

假设我们有一个特定的员工,我们称她为 Sandra。管理层注意到她工作非常努力,并在最近的调查中报告了低工作满意度。因此,他们希望了解她离职的可能性。

为了简单起见,我们将她的特征值作为训练集中的一个样本(但假装这是假数据)。

  1. 通过运行以下代码列出 Sandra 的特征值:
    sandra = df.iloc[573]
    X = sandra[features]
        X
    >> satisfaction_level 0.360000
    >> number_project 2.000000
    >> time_spend_company 3.000000
    >> average_montly_hours 148.000000
    >> last_evaluation 0.470000
    >> first_principle_component 0.742801
    >> second_principle_component -0.514568
    >> third_principle_component -0.677421

下一步是询问模型它认为 Sandra 应该属于哪个组。

  1. 通过运行以下代码,预测 Sandra 的类别标签:
    clf.predict([X])
    >> array([1]) 

模型将她分类为已经离开公司;这不是一个好兆头!我们可以进一步采取措施,计算每个类别标签的概率。

  1. 使用clf.predict_proba预测我们的模型预测 Sandra 离职的概率。运行以下代码:
    clf.predict_proba([X])
    >> array([[ 0.06576239, 0.93423761]])

我们看到模型预测她已离职的准确率为 93%。由于这显然是管理层的一个警示信号,他们决定减少她每月的工时至 100 小时,并将她在公司的时间减少到 1 年。

  1. 使用 Sandra 新计划的指标计算新的概率。运行以下代码:
   X.average_montly_hours = 100
   X.time_spend_company = 1
     clf.predict_proba([X])
     >> array([[ 0.61070329, 0.38929671]])

很棒!我们现在可以看到模型仅返回了 38%的可能性,表明她已经离职!相反,现在它预测她不会离开公司。

我们的模型帮助管理层做出了数据驱动的决策。通过减少她在公司的时间,模型告诉我们她最有可能继续留在公司!

总结

在本章中,我们已经了解了如何在 Jupyter Notebooks 中训练预测模型。

首先,我们讨论了如何规划机器学习策略。我们思考了如何设计一个能够提供可操作的商业洞察的计划,并强调了利用数据帮助设定现实商业目标的重要性。我们还解释了机器学习术语,如监督学习、无监督学习、分类和回归。

接下来,我们讨论了使用 scikit-learn 和 pandas 进行数据预处理的方法。这包括了关于机器学习中一个令人意外且非常耗时的部分——处理缺失数据的详细讨论和示例。

在本章后半部分,我们训练了用于二元问题的预测分类模型,比较了各种模型(如支持向量机(SVM)、k-最近邻(k-NN)和随机森林)的决策边界。然后,我们展示了如何使用验证曲线做出好的参数选择,以及如何通过降维来提高模型性能。最后,在活动的结尾,我们探索了如何将最终模型应用于实际中,以做出数据驱动的决策。

第三章:网络爬虫与交互式可视化

到目前为止,本书主要聚焦于使用 Jupyter 构建可重复的数据分析流程和预测模型。在本章中我们将继续探讨这些话题,但本章的主要重点是数据获取。特别是,我们将展示如何通过 HTTP 请求从网上获取数据。这将涉及通过请求和解析 HTML 来抓取网页。最后,我们将通过使用交互式可视化技术来探索我们收集的数据,作为本章的总结。

网上可获得的数据量庞大且相对容易获取,而且数据量还在持续增长,变得越来越重要。部分持续增长的原因,是全球从报纸、杂志和电视转向在线内容的趋势。随着手机上随时可用的定制化新闻源,以及 Facebook、Reddit、Twitter 和 YouTube 等实时新闻源的出现,已经很难想象历史上的其他选择还能在未来长久存在。令人惊讶的是,这还仅仅是线上数据不断增长的一部分。

随着全球向通过 HTTP 服务(如博客、新闻网站、Netflix 等)消费内容的转变,使用数据驱动的分析方法的机会大量增加。例如,Netflix 会根据用户观看的电影,预测他们可能喜欢的电影,并基于此预测推荐相关电影。不过,在本章中,我们并不打算探讨这种“面向业务”的数据,而是将展示客户端如何将互联网作为数据库来利用。从未有过如此多样化且易于获取的数据。我们将使用网络爬虫技术来收集数据,然后利用 Jupyter 中的交互式可视化进行探索。

交互式可视化是一种数据表现形式,通过图表或图形帮助用户理解数据。交互式可视化帮助开发者或分析人员将数据呈现成简洁易懂的形式,非技术人员也能轻松理解。

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

  • 分析 HTTP 请求如何工作

  • 从网页中爬取表格数据

  • 构建与转换 Pandas 数据框

  • 创建交互式可视化

爬取网页数据

在将互联网作为数据库的理念下,我们可以考虑通过爬取网页内容或与 Web API 接口来获取数据。通常,爬取内容意味着让计算机读取原本是为人类呈现的可读格式的数据。与此不同,Web API 则是通过机器可读的格式(最常见的是 JSON)传递数据。

在这个主题中,我们将重点讨论网页抓取。进行抓取的具体过程取决于页面和所需的内容。不过,正如我们将看到的,只要我们了解底层的概念和工具,从 HTML 页面中抓取任何我们需要的内容是非常容易的。在本节中,我们将以维基百科为例,抓取文章中的表格内容。然后,我们将应用相同的技术,从一个完全不同域的页面抓取数据。但首先,我们将花一些时间来介绍 HTTP 请求。

HTTP 请求简介

超文本传输协议,简称 HTTP,是互联网数据通信的基础。它定义了如何请求一个页面以及响应应该是什么样子。例如,客户端可以请求一个销售笔记本电脑的亚马逊页面、一个本地餐馆的谷歌搜索结果,或者是他们的 Facebook 动态。除了 URL,请求中还会包含用户代理和可用的浏览器 Cookie,这些内容属于请求头。用户代理告诉服务器客户端正在使用的浏览器和设备,通常用于提供最适合用户的网页响应版本。也许他们最近登录了该网页;这些信息会存储在 Cookie 中,用于自动登录用户。

这些 HTTP 请求和响应的细节由 Web 浏览器在幕后处理。幸运的是,今天使用 Python 等高级语言进行请求时也是如此。对于许多用途,请求头的内容可以大体上忽略。

除非另有说明,否则这些请求通常是在请求 URL 时由 Python 自动生成的。不过,为了排查问题并理解请求产生的响应,理解 HTTP 的基础知识是很有用的。

HTTP 方法有很多种,如 GET、HEAD、POST 和 PUT。前两种用于请求从服务器发送数据到客户端,而后两种则用于将数据发送到服务器。

这些 HTTP 方法总结在以下表格中:

HTTP 方法 描述
GET 从指定的 URL 获取信息
HEAD 从指定 URL 的 HTTP 头部获取元信息
POST 将附加信息发送到指定 URL 的资源进行附加
PUT 将附加信息发送到指定 URL 的资源以替换原有内容

每次我们在浏览器中输入网页地址并按下Enter键时,都会发送一个 GET 请求。在网页抓取中,这通常是我们唯一关心的 HTTP 方法,也是本章将要使用的唯一方法。

一旦请求被发送,服务器可以返回各种类型的响应。这些响应使用从 100 级到 500 级的代码进行标记,其中代码中的第一位数字表示响应的类别。可以将其描述如下:

  • 1xx: 信息响应,例如,服务器正在处理请求。这种情况很少见。

  • 2xx: 成功,例如,页面已正确加载。

  • 3xx: 重定向,例如,请求的资源已移动,我们被重定向到一个新的 URL。

  • 4xx: 客户端错误,例如,请求的资源不存在。

  • 5xx: 服务器错误,例如,网站服务器正在接收过多的流量,无法完成请求。

对于网络抓取的目的,我们通常只关心响应类别,即响应代码的第一个数字。但是,每个类别中还有子类别,提供了更详细的信息。例如,401 代码表示未经授权的响应,而 404 代码表示页面未找到的响应。

这种区分很重要,因为 404 表示我们请求的页面不存在,而 401 告诉我们需要登录才能查看特定资源。

让我们看看如何在 Python 中进行 HTTP 请求,并使用 Jupyter Notebook 探索这些主题。

在 Jupyter Notebook 中进行 HTTP 请求

现在我们已经讨论了 HTTP 请求的工作原理及其应该期望的响应类型,让我们看看如何在 Python 中实现这一点。我们将使用一个名为Requests的库,这恰好是 Python 中下载次数最多的外部库。可以使用 Python 的内置工具,如urllib,来进行 HTTP 请求,但 Requests 更加直观,并且在官方 Python 文档中推荐使用它而不是urllib

Requests 是进行简单和高级网络请求的一个很好的选择。它允许对头部、Cookie 和授权进行各种自定义设置。它跟踪重定向并提供方法来返回特定页面内容,例如 JSON。此外,还有一整套高级功能。但是,它不允许 JavaScript 渲染。

通常情况下,服务器返回包含 JavaScript 代码片段的 HTML,这些片段在加载时会自动在浏览器中运行。使用 Python 的 Requests 请求内容时,可以看到这些 JavaScript 代码,但不会运行。因此,任何由此创建或更改的元素均会缺失。通常情况下,这并不影响获取所需信息的能力,但在某些情况下,我们可能需要渲染 JavaScript 以正确地抓取页面。为此,我们可以使用类似 Selenium 这样的库。它与 Requests 库具有类似的 API,但提供了使用 Web 驱动程序来渲染 JavaScript 的支持。

让我们使用 Requests 库和 Python 在 Jupyter Notebook 中深入研究下一节。

在 Jupyter Notebook 中使用 Python 处理 HTTP 请求

  1. 通过执行jupyter notebook从项目目录启动NotebookApp。导航到*Chapter-3*目录并打开chapter-3-workbook.ipynb文件。找到顶部附近加载包的单元格并运行它。

我们将请求一个网页,然后检查响应对象。市面上有很多不同的库可以用来发起请求,并且每个库在具体操作方式上也有很多选择。我们只会使用 Requests 库,因为它提供了出色的文档、强大的功能和简单的 API。

  1. 向下滚动到子主题HTTP 请求简介,并运行该部分中的第一个单元格以导入 Requests 库。然后,运行包含以下代码的单元格以准备请求:
  url = 'https://jupyter.org/'
  req = requests.Request('GET', url)
  req.headers['User-Agent'] = 'Mozilla/5.0'
  req = req.prepare() 

我们使用Request 类来准备一个 GET 请求,向 jupyter.org 主页发送请求。通过指定用户代理为Mozilla/5.0,我们请求的是一个适合标准桌面浏览器的响应。最后,我们准备好请求。

  1. 通过运行包含req?的单元格来打印“已准备好的请求”req 的文档字符串:

查看它的用法,我们可以看到如何使用会话发送请求。这类似于打开一个网页浏览器(启动会话),然后请求一个 URL。

  1. 发起请求并将响应存储在一个名为 page 的变量中,运行以下代码:
  with requests.Session() as sess:
  page = sess.send(req)

这段代码返回 HTTP 响应,通过page变量引用。通过使用 with 语句,我们初始化了一个作用范围仅限于缩进代码块的会话。这意味着我们不必显式地关闭会话,因为它会自动完成。

  1. 运行笔记本中的接下来的两个单元格来检查响应。page的字符串表示应显示 200 状态码响应。这应该与status_code属性一致。

  2. 将响应文本保存到 page_html 变量中,并通过page_html[:1000]查看字符串的头部:

如预期的那样,响应是 HTML。我们可以借助 BeautifulSoup 库更好地格式化这个输出,稍后我们将广泛使用它来解析 HTML。

  1. 运行以下代码打印格式化 HTML 的头部:
  from bs4 import BeautifulSoup
  print(BeautifulSoup(page_html, 'html.parser').prettify()[:1000])

我们导入 BeautifulSoup,然后打印格式化输出,输出中的新行根据它们在 HTML 结构中的层级关系进行缩进。

  1. 我们可以进一步操作,实际上通过使用 IPython 显示模块在 Jupyter 中显示 HTML。通过运行以下代码来实现:
  from IPython.display import HTML
  HTML(page_html) 

在这里,我们看到 HTML 渲染的效果,考虑到没有执行 JavaScript 代码,也没有加载外部资源。例如,托管在 jupyter.org 服务器上的图片没有被渲染,我们看到的是alt 文本编程图标的圆圈、jupyter logo 等等。

  1. 让我们将其与实时网站进行比较,该网站可以通过 IFrame 在 Jupyter 中打开。通过运行以下代码来实现:
  from IPython.display import IFrame
  IFrame(src=url, height=800, width=800) 

在这里,我们可以看到整个网站被渲染出来,包括 JavaScript 和外部资源。事实上,我们甚至可以点击超链接,并在 IFrame 中加载那些页面,就像常规的浏览会话一样。

  1. 关闭 IFrame 是一个良好的实践,这可以防止它占用过多的内存和处理能力。可以通过选择单元格并在 Jupyter Notebook 的 Cell 菜单中点击 Current Outputs | Clear 来关闭它。

回想一下我们如何使用一个准备好的请求和会话来获取内容并将其作为字符串在 Python 中使用。通常,我们也可以使用一个简便的方法来实现这一点。缺点是我们对请求头部的定制化程度较低,但通常这并不是什么问题。

  1. 通过运行以下代码向 www.python.org/ 发起请求:
  url = 'http://www.python.org/'
  page = requests.get(url)
  page
  <Response [200]> 

页面字符串表示(显示在单元格下方)应该显示一个 200 状态码,表示响应成功。

  1. 运行接下来的两个单元格。在这里,我们打印页面的 urlhistory 属性。

返回的 URL 并不是我们输入的那个;注意到差异了吗?我们从输入的 URL www.python.org/ 被重定向到了该页面的安全版本 www.python.org/。差异就在于协议中 URL 开头多了一个 s。所有的重定向信息都保存在历史记录属性中;在这个案例中,我们在其中找到了一个状态码为 301(永久重定向)的页面,它对应的是我们最初请求的 URL。

既然我们已经习惯了发起请求,现在我们将关注于解析 HTML。这有时是一门艺术,因为通常有多种方法可以处理它,而最佳方法往往取决于具体 HTML 的细节。

在 Jupyter Notebook 中解析 HTML

在从网页抓取数据时,在发起请求后,我们必须从响应内容中提取数据。如果内容是 HTML,那么最简单的方法是使用一个高级解析库,比如 Beautiful Soup。并不是说这是唯一的方式;原则上,我们也可以使用正则表达式或 Python 字符串方法,如 split 来提取数据,但采用这些方式会浪费时间,并且容易出错。因此,一般不推荐这样做,通常建议使用一个可信赖的解析工具。

为了理解如何从 HTML 中提取内容,了解 HTML 的基础知识非常重要。首先,HTML 代表 超文本标记语言。像 Markdown 或 XML(可扩展标记语言)一样,它只是用于标记文本的语言。

在 HTML 中,显示文本包含在 HTML 元素的内容部分,元素属性则指定该元素在页面上的显示方式。

查看 HTML 元素的结构,如前图所示,我们可以看到内容被包裹在开始和结束标签之间。在这个例子中,标签是 <p>(段落);其他常见的标签类型包括 <div>(文本块),<table>(数据表),<h1>(标题),<img>(图片),和 <a>(超链接)。标签有属性,属性可以存储重要的元数据。最常见的情况下,这些元数据用于指定元素文本在页面上的显示方式。此时 CSS 文件发挥作用。属性还可以存储其他有用信息,如 <a> 标签中的超链接 href,它指定了一个 URL 链接,或 <img> 标签中的备用 alt 标签,用于在图片资源无法加载时显示的文本。

现在,让我们把注意力转回到 Jupyter Notebook,解析一些 HTML!虽然在本节学习时不一定需要使用,但在实际应用中,使用 Chrome 或 Firefox 的开发者工具来帮助识别感兴趣的 HTML 元素非常有用。接下来的章节我们会提供如何在 Chrome 中执行此操作的说明。

在 Jupyter Notebook 中使用 Python 解析 HTML

  1. chapter-3-workbook.ipynb 文件中,滚动到 Parsing HTML with Python 子主题的顶部。

在本节中,我们将抓取每个国家的中央银行利率,这些数据来自 Wikipedia。在进入代码之前,让我们先打开包含这些数据的网页。

  1. 打开en.wikipedia.org/wiki/List_of_countries_by_central_bank_interest_rates这个链接,在浏览器中查看。尽可能使用 Chrome,因为在本节中我们将展示如何使用 Chrome 的开发者工具查看和搜索 HTML。

看这个页面,我们除了大列表的国家和其利率之外,几乎没有其他内容。这就是我们要抓取的表格。

  1. 返回 Jupyter Notebook,并将 HTML 加载为 Beautiful Soup 对象,以便进行解析。通过运行以下代码来完成:
  from bs4 import BeautifulSoup
  soup = BeautifulSoup(page.content, 'html.parser') 

我们使用 Python 默认的 html.parser 作为解析器,但如果需要,也可以使用第三方解析器,如 lxml

通常,在使用像 Beautiful Soup 这样的新对象时,查看文档字符串是个好主意,方法是输入 soup?。然而,在这种情况下,文档字符串并不特别有用。另一个用于探索 Python 对象的工具是 pdir,它列出对象的所有属性和方法(可以通过 pip install pdir2 安装)。它本质上是 Python 内建 dir 函数的格式化版本。

  1. 通过运行以下代码显示 BeautifulSoup 对象的属性和方法。无论是否安装了pdir外部库,都会运行此代码:
 try:
 import pdir
 dir = pdir
 except:
 print('You can install pdir with:\npip install pdir2')
 dir(soup) 

在这里,我们看到一个方法和属性的列表,这些方法和属性可以在soup上调用。最常用的函数可能是find_all,它返回符合给定条件的元素列表。

  1. 使用以下代码获取页面的 h1 标题:
      h1 = soup.find_all('h1')
      h1
      >> [<h1 class="firstHeading" id="firstHeading" lang="en">
      List of countries by central bank interest rates</h1>] 

通常,页面上只有一个 H1 元素,因此很明显我们这里只找到一个。

  1. 运行接下来的几个单元格。我们将 H1 重新定义为第一个(也是唯一的)列表元素,h1 = h1[0],然后通过h1.attrs打印出 HTML 元素的属性:
       >> {'class': ['firstHeading'], 'id': 'firstHeading', 'lang': 'en'} 

我们看到此元素的类和 ID,二者都可以通过 CSS 代码引用,以定义该元素的样式。

  1. 通过打印h1.text获取 HTML 元素内容(即可见文本)。

  2. 通过运行以下代码获取页面上的所有图片:

      imgs = soup.find_all('img')
      len(imgs)
      >> 91 

页面上有很多图片,其中大多数是国家旗帜。

  1. 通过运行以下代码打印每张图片的来源:
      [element.attrs['src'] for element in imgs
        if 'src' in element.attrs.keys()] 

我们使用列表推导式遍历元素,选择每个元素的src属性(前提是该属性实际存在)。

现在,让我们抓取表格。我们将使用 Chrome 的开发者工具来定位包含该元素的部分。

  1. 如果还没有完成,打开我们正在查看的维基百科页面(在 Chrome 中)。然后,在浏览器中从视图菜单中选择开发者工具。一个侧边栏将会打开。HTML 可以从开发者工具的元素标签中查看。

  2. 选择工具侧边栏左上角的小箭头。这允许我们悬停在页面上,并查看 HTML 元素在侧边栏的“元素”部分的位置:

  1. 将鼠标悬停在正文上,查看表格如何包含在具有id="bodyContent"的 div 中:

  1. 通过运行以下代码选择该 div:
     body_content = soup.find('div', {'id': 'bodyContent'}) 

我们现在可以在这个 HTML 的子集内查找表格。通常,表格被组织为标题<th>、行<tr>和数据条目<td>

  1. 通过运行以下代码获取表格标题:
 table_headers = body_content.find_all('th')[:3]
 table_headers
 >>> [<th>Country or<br/>
 currency union</th>, <th>Central bank<br/>
 interest rate (%)</th>, <th>Date of last<br/>
 change</th>] 

在这里,我们看到三个标题。每个标题的内容中都有一个<br/>换行元素,这会使得文本稍微难以干净地解析。

  1. 通过运行以下代码获取文本:
      table_headers = [element.get_text().replace('\n', ' ')
      for element in table_headers]
      table_headers
      >> ['Country or currency union',
      'Central bank interest rate (%)',
      'Date of last change']

在这里,我们使用get_text方法获取内容,然后运行replace string方法以移除由<br/>元素引起的换行符。为了获取数据,我们首先进行一些测试,然后在一个单元格中抓取所有数据。

  1. 通过运行以下代码获取第二个<tr>(行)元素中每个单元格的数据:
      row_number = 2
      d1, d2, d3 = body_content.find_all('tr')[row_number]\.find_all('td')

我们找到所有的行元素,挑选出第三个,然后找出其中的三个数据元素。

让我们看看结果数据,看看如何解析每行的文本。

  1. 运行接下来的几个单元格以打印d1及其text属性:

我们在前面获取了一些不需要的字符。这可以通过仅搜索<a>标签的文本来解决。

  1. 运行d1.find('a').text以返回该单元格的正确清理数据。

  2. 运行接下来的几个单元格以打印d2及其文本。此数据看起来足够干净,可以直接转换为平面格式。

  3. 运行接下来的几个单元格以打印d3及其文本:

类似于d1,我们看到最好只获取span元素的文本。

  1. 通过运行以下代码,正确解析此表格条目的日期:
       d3.find_all('span')[0].text
       >> '30 June 2016' 
  1. 现在,我们准备通过遍历行元素<th>来进行完整抓取。运行以下代码:
       data = []
       for i, row in enumerate(body_content.find_all('tr')):
       ...
       ...
       >> Ignoring row 101 because len(data) != 3
       >> Ignoring row 102 because len(data) != 3 

我们遍历这些行,忽略任何包含三个以上数据元素的行。这些行不对应我们感兴趣的表格数据。那些确实包含三个数据元素的行被认为是表格中的数据,我们按照测试时确定的方式解析这些文本。

文本解析在try/except语句中完成,这将捕获任何错误并允许跳过该行而不会停止迭代。任何由于此语句导致错误的行应该进行检查。可以手动记录这些数据,或通过修改抓取循环并重新运行它来考虑这些行。在此情况下,为了节省时间,我们将忽略任何错误。

  1. 通过运行print(data[:10])打印抓取数据列表的前 10 行:
      >> [['Albania', 1.25, '4 May 2016'],
      ['Angola', 16.0, '30 June 2016'],
      ['Argentina', 26.25, '11 April 2017'],
      ['Armenia', 6.0, '14 February 2017'],
      ['Australia', 1.5, '2 August 2016'],
      ['Azerbaijan', 15.0, '9 September 2016'],
      ['Bahamas', 4.0, '22 December 2016'],    
      ['Bahrain', 1.5, '14 June 2017'],
      ['Bangladesh', 6.75, '14 January 2016'],
      ['Belarus', 12.0, '28 June 2017']] 
  1. 我们将在本章稍后可视化这些数据。现在,先通过运行以下代码将数据保存为 CSV 文件:
       f_path = '../data/countries/interest-rates.csv'
       with open(f_path, 'w') as f:
         f.write('{};{};{}\n'.format(*table_headers))
         for d in data:
           f.write('{};{};{}\n'.format(*d))

请注意,我们使用分号来分隔字段。

活动:使用 Jupyter Notebooks 进行网页抓取

我们将获取每个国家的人口数据。然后,在下一个主题中,这些数据将与上一节抓取的利率数据一起进行可视化。

本次活动中我们查看的页面可以在此访问:www.worldometers.info/world-population/population-by-country/。现在我们已经了解了网页抓取的基础知识,接下来我们将相同的技术应用到一个新网页并抓取更多数据!

由于本文档创建时页面可能已发生变化,如果此 URL 不再指向国家人口表格,请使用此 Wikipedia 页面:en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)

  1. 对于此页面,数据可以使用以下代码片段进行抓取:
      data = []
      for i, row in enumerate(soup.find_all('tr')):
          row_data = row.find_all('td')
              try:
                  d1, d2, d3 = row_data[1], row_data[5], row_data[6]
                  d1 = d1.find('a').text
                  d2 = float(d2.text)
                  d3 = d3.find_all('span')[1].text.replace('+', '')
              data.append([d1, d2, d3])
          except:
      print('Ignoring row {}'.format(i)) 
  1. chapter-3-workbook.ipynb Jupyter Notebook 中,滚动至Activity Web scraping with Python部分。

  2. 设置url变量并通过运行以下代码在笔记本中加载我们的页面的 IFrame:

      url ='http://www.worldometers.info/world-population/
                population-bycountry/'
      IFrame(url, height=300, width=800)
  1. 通过选择单元格并点击 Jupyter Notebook 的 Cell 菜单中的“Current Outputs | Clear”,来关闭 IFrame。

  2. 通过运行以下代码请求页面并将其加载为 BeautifulSoup 对象:

       page = requests.get(url)
       soup = BeautifulSoup(page.content,'html.parser') 

我们将页面内容传递给BeautifulSoup构造函数。回想一下,之前我们使用的是page.text。它们的区别在于,page.content返回的是原始的二进制响应内容,而page.text返回的是UTF-8解码后的内容。通常最好将字节对象传递给BeautifulSoup,让它来进行解码,而不是使用 Requests 中的page.text来处理。

  1. 通过运行以下代码打印页面的H1
      soup.find_all('h1')
      >> [<h1>Countries in the world by population (2017)</h1>] 

我们将通过搜索<th><tr><td>元素来抓取表格数据,正如在前一部分中所做的那样。

  1. 通过运行以下代码获取并打印表头:
        table_headers = soup.find_all('th')
        table_headers
        >> [<th>#</th>,
            <th>Country (or dependency)</th>,
            <th>Population<br/> (2017)</th>,
           <th>Yearly<br/> Change</th>,
            <th>Net<br/> Change</th>,
            <th>Density<br/> (P/Km²)</th>,
            <th>Land Area<br/> (Km²)</th>,
           <th>Migrants<br/> (net)</th>,
            <th>Fert.<br/> Rate</th>,
            <th>Med.<br/> Age</th>,
            <th>Urban<br/> Pop %</th>,
           <th>World<br/> Share</th>] 
  1. 我们只对前三列感兴趣。选择这些列并使用以下代码解析文本:
      table_headers = table_headers[1:4]
      table_headers = [t.text.replace('\n', '') for t in table_headers]

在选择我们想要的表头子集后,我们将从每个表头中解析文本内容,并去除任何换行符。

现在,我们来获取数据。按照前一部分的流程,我们将测试如何解析一行样本数据。

  1. 通过运行以下代码获取一行样本数据:
     row_number = 2
     row_data = soup.find_all('tr')[row_number]\.find_all('td') 
  1. 我们有多少列数据?通过运行print(len(row_data))来打印row_data的长度。

  2. 通过运行print(row_data[:4])来打印前四个元素:

      >> [<td>2</td>,
      <td style="font-weight: bold; font-size:15px; text-align:left"><a
      href="/world-population/india-population/">India</a></td>,
      <td style="font-weight: bold;">1,339,180,127</td>,
      <td>1.13 %</td>]

很明显,我们需要选择索引 1、2 和 3。第一个数据值可以忽略,因为它仅仅是索引。

  1. 通过运行以下代码选择我们感兴趣的解析数据元素:
      d1, d2, d3 = row_data[1:4] 

  1. 看一下row_data的输出,我们可以找出如何正确地解析数据。我们将选择第一个数据元素中的<a>元素的内容,然后简单地从其他元素中提取文本。通过运行以下代码来测试这些假设:
       print(d1.find('a').text)
       print(d2.text)
       print(d3.text)
       >> India
       >> 1,339,180,127
       >> 1.13 % 

很棒!看起来这运行得很顺利。现在,我们准备抓取整个表格。

  1. 通过运行以下代码抓取并解析表格数据:
      ata = []
      for i, row in enumerate(soup.find_all('tr')):
         try:
             d1, d2, d3 = row.fid_all('td')[1:4]
             d1 = d1.fid('a').text
             d2 = d2.text
             d3 = d3.text
             data.append([d1, d2, d3])
         except:
             print('Error parsing row {}'.format(i))

         >> Error parsing row 0 

这与之前的情况非常相似,当我们尝试解析文本时,如果出现错误,我们会跳过这一行。

  1. 通过运行print(data[:10])来打印抓取数据的前十条:
      >> [['China', '1,409,517,397', '0.43 %'],
      ['India', '1,339,180,127', '1.13 %'],
      ['U.S.', '324,459,463', '0.71 %'],
      ['Indonesia', '263,991,379', '1.10 %'],    
      ['Brazil', '209,288,278', '0.79 %'],
      ['Pakistan', '197,015,955', '1.97 %'],
      ['Nigeria', '190,886,311', '2.63 %'],
      ['Bangladesh', '164,669,751', '1.05 %'],
      ['Russia', '143,989,754', '0.02 %'],
      ['Mexico', '129,163,276', '1.27 %']] 

看起来我们已经成功地抓取了数据!请注意,尽管这个网页完全不同,但这个表格的抓取过程与维基百科的表格非常相似。当然,数据不一定总是以表格的形式存在,但无论如何,我们通常可以使用find_all作为主要的解析方法。

  1. 最后,将数据保存为一个CSV 文件以供后续使用。通过运行以下代码实现:
      f_path = '../data/countries/populations.csv'
      with open(f_path, 'w') as f:
        f.write('{};{};{}\n'.format(*table_headers))
        for d in data:
          f.write('{};{};{}\n'.format(*d)) 

总结一下,我们已经了解了如何使用 Jupyter Notebook 进行网页抓取。我们从学习 HTTP 方法和状态码开始。然后,我们使用 Requests 库来实际执行 HTTP 请求,并看到了如何使用 Beautiful Soup 库来解析 HTML 响应。

我们的 Jupyter Notebook 证明是进行此类工作的一个很好的工具。我们能够探索网页请求的结果,并尝试各种 HTML 解析技术。我们还能够在笔记本中渲染 HTML,甚至加载网页的实时版本!

在本章的下一个主题中,我们将转向一个全新的话题:交互式可视化。我们将展示如何在笔记本中创建和显示交互式图表,并使用这些图表作为探索我们刚刚收集的数据的一种方式。

交互式可视化

可视化是从数据集中提取信息的一个非常有用的手段。例如,通过条形图,比起查看表格中的数值,区分数值分布变得非常容易。当然,正如我们在本书前面所看到的,它们还可以用来研究数据集中一些否则很难识别的模式。此外,它们还可以帮助向不熟悉的人解释数据集。例如,如果包含在博客文章中,它们可以提升读者的兴趣,并用来打破一块块的文字内容。

当我们考虑交互式可视化时,它的好处与静态可视化相似,但由于它们允许观众进行主动探索,因此更加增强。不仅允许观众回答他们可能对数据有的问题,还可以在探索过程中提出新的问题。这对像博客读者或同事这样的外部人员有益,对于创作者来说也同样重要,因为它允许他们在不修改任何代码的情况下,轻松地对数据进行详细的即兴探索。

在本节中,我们将讨论并展示如何使用 Bokeh 在 Jupyter 中构建交互式可视化。但在此之前,我们将简要回顾 pandas 的 DataFrame,它在使用 Python 进行数据可视化时发挥着重要作用。

构建一个用于存储和组织数据的 DataFrame

正如我们在本书中一再看到的,pandas 是使用 Python 和 Jupyter Notebooks 进行数据科学的一个重要组成部分。DataFrame 提供了一种组织和存储带标签数据的方式,但更重要的是,pandas 提供了节省时间的方法,用于在 DataFrame 中转换数据。本书中我们看到的一些例子包括删除重复项、将字典映射到列、对列应用函数,以及填充缺失值。

关于可视化,数据框提供了创建各种 matplotlib 图形的方法,包括df.plot.barh()df.plot.hist()等。交互式可视化库 Bokeh 以前依赖 Pandas 数据框来生成其高级图表。这些图表的工作方式类似于 Seaborn,正如我们在前一章看到的那样,其中数据框作为输入传递给绘图函数,并指定需要绘制的列。然而,Bokeh 的最新版本已不再支持这种行为。现在,图形的创建方式与 matplotlib 类似,数据可以存储在简单的列表或 NumPy 数组中。讨论的要点是,数据框并非绝对必要,但仍然非常有助于在可视化之前组织和处理数据。

构建和合并 Pandas 数据框

让我们直接进入一个练习,继续处理之前抓取的国家数据。回想一下,我们提取了每个国家的中央银行利率和人口,并将结果保存在 CSV 文件中。接下来,我们将从这些文件加载数据并将其合并成一个数据框,然后将作为后续交互式可视化的数据源。

  1. chapter-3-workbook.ipynb Jupyter Notebook 中,滚动到子主题构建一个数据框以存储和组织数据

我们首先将从CSV 文件加载数据,这样它就恢复到抓取后原始的状态。这将使我们能够练习从 Python 对象构建数据框,而不是使用pd.read_csv函数。

使用pd.read_csv时,每一列的数据类型将根据字符串输入自动推断。另一方面,当我们使用pd.DataFrame时,数据类型将根据输入变量的类型来决定。就我们来说,正如将要看到的,我们在读取文件后不会立即将变量转换为数字或日期时间类型,而是在实例化数据框后进行转换。

  1. 通过运行以下代码将 CSV 文件加载到列表中:
        with open('../data/countries/interest-rates.csv', 'r') as f:
          int_rates_col_names = next(f).split(',')
          int_rates = [line.split(',') for line in f.read().splitlines()]
        with open('../data/countries/populations.csv', 'r') as f:
          populations_col_names = next(f).split(',')
          populations = [line.split(',') for line in f.read().splitlines()] 
  1. 通过运行接下来的两个单元格,检查结果列表的样子。我们应该能看到类似以下的输出:
      print(int_rates_col_names)
      int_rates[:5]
      >> ['Country or currency union', 'Central bank interest ...    
      ...
      ['Indonesia', '263', '991', '379', '1.10 %'],    
      ['Brazil', '209', '288', '278', '0.79 %']] 

现在,数据处于标准的 Python 列表结构中,就像在前面章节从网页抓取后一样。接下来,我们将创建两个数据框并将它们合并,以便将所有数据组织到一个对象中。

  1. 通过运行以下代码,使用标准的数据框构造函数创建两个数据框:
      df_int_rates = pd.DataFrame(int_rates,columns=int_rates_col_names)
      df_populations = pd.DataFrame(populations,
                      columns=populations_col_names) 

这不是我们第一次在本书中使用这个函数。这里,我们传递数据列表(如前所示)和相应的列名。输入数据也可以是字典类型,当每列数据分别存储在不同的列表中时,这种方式非常有用。

接下来,我们将清理每个数据框。从利率数据框开始,让我们打印出前几行和后几行,并列出数据类型。

  1. 显示整个数据框时,默认的最大行数为 60(适用于版本 0.18.1)。让我们通过运行以下代码将其减少为 10:
      pd.options.display.max_rows = 10 
  1. 通过运行以下代码显示利率数据框的头部和尾部:
      df_int_rates 

  1. 通过运行以下命令打印数据类型:
      df_int_rates.dtypes
      >> Country or currency union object
      >> Central bank interest rate (%) object
      >> Date of last change object
      >> dtype: object 

Pandas 已将每一列分配为字符串数据类型,这是合理的,因为输入变量都是字符串。我们将需要分别将这些转换为字符串、浮动和日期时间类型。

  1. 通过运行以下代码将数据转换为正确的类型:
        df_int_rates['Central bank interest rate (%)'] = \
        df_int_rates['Central bank interest rate (%)']\
        .astype(float,copy=False)
        df_int_rates['Date of last change'] = \
        pd.to_datetime(df_int_rates['Date of last change']) 

我们使用astype将利率值转换为浮动类型,并设置copy=False以节省内存。由于日期值的格式非常易读,因此可以通过使用pd.to_datetime轻松地将其转换。

  1. 通过运行以下代码检查每一列的新数据类型:
      df_int_rates.dtypes
        >> Country or currency union             object
        >> Central bank interest rate (%)        float64
        >> Date of last change             datetime64[ns]
        >> dtype: object

如所见,现在所有数据都已正确格式化。

  1. 让我们对另一个数据框应用相同的过程。运行接下来的几个单元格来重复对df_populations执行前面的步骤:
 df_populations 

然后,运行此代码:

      df_populations['Population (2017)'] = df_populations['Population
      (2017)']\.str.replace(',', '')\
      .astype(float, copy=False)
      df_populations['Yearly Change'] = df_populations['Yearly Change']\
      .str.rstrip('%')\
      .astype(float, copy=False) 

为了将数值列转换为浮动类型,我们必须首先对这些字符串做一些修改。我们使用字符串方法去掉了人口数据中的逗号,并去除了“年变化”列中的百分号。

现在,我们将根据每一行的国家名称合并数据框。请记住,这些仍然是从网络抓取的原始国家名称,因此在匹配字符串时可能需要进行一些工作。

  1. 通过运行以下代码合并数据框:
       df_merge = pd.merge(df_populations,
         df_int_rates,
         left_on='Country (or dependency)',
         right_on='Country or currency union',
         how='outer'
       df_merge

我们将左侧数据框中的人口数据和右侧数据框中的利率数据传入,并在国家列上执行外连接匹配。这将导致在两者不重叠的地方出现NaN 值

  1. 为了节省时间,让我们仅查看人口最多的国家,看看是否漏掉了任何匹配。理想情况下,我们应该检查所有内容。通过运行以下代码查看人口最多的国家:
      df_merge.sort_values('Population (2017)', ascending=False)\ .head(10)

看起来美国(U.S.)没有匹配上。这是因为在利率数据中它被列为“United States”。让我们来修复这个问题。

  1. 通过运行以下代码修正人口表中美国(U.S.)的标签:
      col = 'Country (or dependency)'    
      df_populations.loc[df_populations[col] == 'U.S.'] = 'United States'

我们使用loc方法定位该行,并重命名人口数据框中的国家名称。现在,让我们正确地合并数据框。

  1. 再次根据国家名称合并数据框,但这次使用内部合并来删除NaN值:
      df_merge = pd.merge(df_populations,
                    df_int_rates,
                    left_on='Country (or dependency)',
                    right_on='Country or currency union',
                    how='inner') 
  1. 我们在合并后的数据框中留下了两列相同的内容。通过运行以下代码删除其中一列:
      del df_merge['Country or currency union'] 
  1. 通过运行以下代码重命名列:
      name_map = {'Country (or dependency)': 'Country',
          'Population (2017)': 'Population',
          'Central bank interest rate (%)': 'Interest rate'}

      df_merge = df_merge.rename(columns=name_map)     

我们得到了以下合并和清理后的数据框:

  1. 现在我们已经将所有数据整理成一个结构良好的表格,接下来进入有趣的部分:可视化。我们将把这个表格保存为CSV 文件以供以后使用,然后讨论如何使用 Bokeh 创建可视化。使用以下代码将合并后的数据写入CSV 文件以供以后使用:
      df_merge.to_csv('../data/countries/merged.csv', index=False)

Bokeh 简介

Bokeh 是一个用于 Python 的交互式可视化库。其目标是提供类似于流行的 JavaScript 交互式可视化库D3的功能。Bokeh 的工作方式与D3截然不同,这并不令人惊讶,因为 Python 和 JavaScript 之间存在差异。总体而言,它更简单,且不像D3那样允许进行大量定制。然而,这正是它的优势所在,因为它更易于使用,并且仍然拥有一套出色的功能,我们将在本节中进行探讨。

让我们通过 Jupyter 笔记本的快速练习,直接通过示例介绍 Bokeh。

Bokeh 的在线文档很完善,但很多内容已经过时。例如,在 Google 中搜索Bokeh 条形图通常会找到旧版模块的文档,这些模块已经不再存在,如以前可以通过bokeh.charts访问的高级绘图工具(版本 0.12.0 之前)。这些工具接收类似于 Seaborn 绘图函数的 pandas DataFrame 作为输入。删除这些高级绘图工具模块使得 Bokeh 变得更加简化,并且能够专注于更有针对性的开发。现在,绘图工具主要被归类到 bokeh.plotting 模块中,正如在接下来的练习和活动中所看到的那样。

使用 Bokeh 进行交互式可视化简介

我们将加载所需的 Bokeh 模块,并展示一些简单的交互式图表,这些图表可以通过 Bokeh 创建。请注意,本书中的示例是使用 Bokeh 0.12.10 版本设计的。

  1. chapter-3-workbook.ipynb Jupyter 笔记本中,滚动到子主题Bokeh 简介

  2. 与 scikit-learn 类似,Bokeh 模块通常是分块加载的(与 pandas 不同,后者会一次性加载整个库)。通过运行以下代码来导入一些基本的绘图模块:

      from bokeh.plotting 
      import figure, show, output_notebook output_notebook()

我们需要运行output_notebook(),以便在 Jupyter 笔记本中渲染交互式可视化。

  1. 通过运行以下代码生成随机数据进行绘图:
      np.random.seed(30)
      data = pd.Series(np.random.randn(200),
      index=list(range(200)))\
      .cumsum()
      x = data.index
      y = data.values

随机数据是通过累积求和一组随机数生成的,这些随机数分布在零附近。其效果类似于股票价格时间序列的趋势。

  1. 通过运行以下代码在 Bokeh 中绘制数据的折线图:
      p = figure(title='Example plot', x_axis_label='x', y_axis_label='y') 
      p.line(x, y, legend='Random trend') show(p)

我们实例化了图形,如变量 p 所引用的那样,然后绘制了一条线。在 Jupyter 中运行这段代码会生成一个交互式图形,并在右侧显示各种选项。

前三个选项(截至版本 0.12.10)是平移框缩放滚轮缩放。尝试使用这些选项并实验它们的工作方式。使用重置选项重新加载默认的图表限制。

  1. 其他图表可以通过figure的替代方法创建。通过运行以下代码绘制散点图,在该代码中,我们将前述代码中的line替换为circle
      size = np.random.rand(200) * 5
      p = figure(title='Example plot', x_axis_label='x', y_axis_label='y')
      p.circle(x, y, radius=size, alpha=0.5, legend='Random dots')
      show(p) 

在这里,我们使用一组随机数字来指定每个圆的大小。

交互式可视化的一个非常诱人的功能是工具提示。这是一个悬停工具,允许用户通过悬停在某个点上来获取该点的信息。

  1. 为了添加这个工具,我们将使用一种稍微不同的方法来创建图表。这将需要我们导入一些新的库。请运行以下代码:
      p.circle(x, y, radius=size, alpha=0.5, legend='Random dots') show(p)

这次,我们将创建一个数据源并传递给绘图方法。它可以包含元数据,这些元数据可以通过悬停工具包含在可视化中。

  1. 通过运行以下代码创建随机标签并绘制带有悬停工具的交互式可视化:
      source = ColumnDataSource(data=dict(
      x=x,
      y=y,
      ...
      ...
      source=source,
        legend='Random dots')
      show(p) 

我们通过将一个键值对字典传递给ColumnDataSource构造函数来定义图表的数据源。该数据源包括每个点的x位置、y位置和大小,以及每个点的随机字母ABC。这些随机字母将作为悬停工具的标签,并显示每个点的大小。

然后将悬停工具添加到图表中,通过特定的绘图方法从每个元素中提取数据,在此例中是圆形。

结果是,我们现在可以在点上悬停并查看我们为悬停工具选择的数据!

我们注意到,通过查看位于图表右侧的工具栏,可以发现通过显式包含悬停工具,其他工具已经消失。通过手动将它们添加到传递给bokeh.plotting.figure的工具对象列表中,可以重新显示这些工具。

  1. 通过运行以下代码,向图表添加平移、缩放和重置工具:
     from bokeh.models 
     import PanTool, BoxZoomTool, WheelZoomTool, ResetTool
     ...
     ...
        legend='Random dots')
        show(p)

这段代码与之前展示的完全相同,唯一不同的是工具变量,它现在引用了我们从 Bokeh 库导入的多个新工具。

我们将在这里停止介绍性练习,但将在接下来的活动中继续创建和探索图表。

活动:通过交互式可视化探索数据

我们将继续使用 Bokeh,从前一个练习中断的位置开始,除了不再使用随机生成的数据,而是使用我们在本章第一部分从网页抓取的数据。

使用 Bokeh 来创建我们的抓取数据的交互式可视化。

  1. chapter-3-workbook.ipynb文件中,滚动到Activity: Interactive visualizations with Bokeh部分。

  2. 通过运行以下代码,加载先前抓取、合并和清理的网页数据:

      df = pd.read_csv('../data/countries/merged.csv')
      df['Date of last change'] = pd.to_datetime(df['Date of last change']) 
  1. 通过显示 DataFrame 来回顾数据的样子:

而在前一个练习中,我们关注的是了解 Bokeh 的工作原理,现在我们关注的是这份数据的样子。为了探索这个数据集,我们将使用交互式可视化。

  1. 通过运行以下代码绘制人口与利率之间关系的散点图:
      source = ColumnDataSource(data=dict(
          x=df['Interest rate'],
          y=df['Population'],
          desc=df['Country'],
       ))
       hover = HoverTool(tooltips=[
          ('Country', '@desc'),
          ('Interest Rate (%)', '@x'),
          ('Population', '@y')    
       ])
       tools = [hover, PanTool(), BoxZoomTool(), 
       WheelZoomTool(), ResetTool()]
          p = figure(tools=tools,
          x_axis_label='Interest Rate (%)',
          y_axis_label='Population')
       p.circle('x', 'y', size=10, alpha=0.5, source=source)
       show(p) 

这与我们在前一个练习中介绍 Bokeh 时看到的最终示例非常相似。我们设置了一个自定义数据源,其中包含每个点的 x 和 y 坐标,以及国家名称。这个国家名称会传递给悬停工具,使其在鼠标悬停在点上时可见。我们将此工具传递给图形,并添加了一组其他有用的工具。

  1. 在数据中,我们看到一些明显的人口高峰值。将鼠标悬停在这些点上,查看它们对应的国家:

我们看到它们属于印度和中国。这些国家的利率相对平均。让我们使用框选缩放工具来调整视图窗口的大小,专注于其余的点。

  1. 选择框选缩放工具,并调整视图窗口,以更好地查看大部分数据:

探索这些点,看看不同国家的利率是如何比较的。哪些国家的利率最高?

  1. 一些人口较少的国家似乎出现了负利率。选择滚轮缩放工具并使用它来放大这一地区。如果需要,可以使用平移工具重新调整图表的中心,使负利率样本显示在视图中。将鼠标悬停在这些样本上,查看它们对应的国家:

让我们重新绘制图形,基于最后利率变化的日期添加颜色。这对于搜索最后变动日期与利率或人口规模之间的关系非常有用。

  1. 通过运行以下代码向 DataFrame 添加“最后变动年份”列:
      def get_year(x):
        year = x.strftime('%Y')
        if year in ['2018', '2017', '2016']:
            return year
      else:         
            return 'Other'
      df['Year of last change'] = df['Date of last change'].apply(get_year)

我们首先定义一个函数,按最后变动年份对样本进行分组,然后将该函数应用于最后变动日期列。接下来,我们需要将这些值映射到颜色,以便进行可视化。

  1. 通过运行以下代码创建一个地图,将最后变动日期分组到不同颜色类别中:
      year_to_color = {
      '2018': 'black',
      '2017': 'blue',
      '2016': 'orange',
      'Other':'red'
      } 

一旦映射到最后变动年份列,这将根据可用的类别(2018 年、2017 年、2016 年和其他)将值分配给颜色。这里的颜色是标准字符串,但它们也可以用十六进制代码表示。

  1. 通过运行以下代码创建彩色可视化:
      source = ColumnDataSource(data=dict(
      x=df['Interest rate'],
      ...
      ...
          fill_color='colors', line_color='black',
          legend='label')
      show(p) 

这里有一些重要的技术细节。首先,我们为每个点添加颜色和标签到ColumnDataSource中。然后在绘制圆圈时通过设置fill_color和 legend 参数引用这些信息。

  1. 寻找模式,放大低人口国家的数据:

我们可以看到在绘图的右侧,黑色点更为显著。这表明利率较高的国家更有可能最近进行更新。

我们尚未查看的数据列是年度人口变化。让我们将其与利率进行比较,并查看是否存在任何趋势。我们还将通过基于国家人口设置圆圈大小来增强图表。

  1. 将利率作为年度人口变化的函数来绘制,运行以下代码:
      source = ColumnDataSource(data=dict(
          x=df['Yearly Change'],
      ...
      ...
      p.circle('x', 'y', size=10, alpha=0.5, source=source,    
      radius='radii')
      show(p) 

在这里,我们使用人口的平方根来作为半径,同时确保将结果缩小到适合可视化的尺寸。

我们看到年度人口变化与利率之间存在很强的相关性。当我们考虑人口规模时,这种相关性尤为明显,主要观察较大的圆圈。让我们添加一条最佳拟合线到图表中来说明这种相关性。

我们将使用 scikit-learn 根据国家人口(如在前面的图表中可视化)创建最佳拟合线。

  1. 运行以下代码确定先前绘制的关系的最佳拟合线:
        from sklearn.linear_model import LinearRegression
        X = df['Yearly Change'].values.reshape(-1, 1)
        y = df['Interest rate'].values
        weights = np.sqrt(df['Population'])/1e5
        lm = LinearRegression()
        lm.fit(X, y, sample_weight=weights)
        lm_x = np.linspace(X.flatten().min(), X.flatten().max(), 50)
        lm_y = lm.predict(lm_x.reshape(-1, 1)) 

Scikit-learn 代码应该在本书的早些时候就已经很熟悉了。正如承诺的那样,我们使用转换后的人口数据,就像在前面的图表中所见,作为权重。然后通过预测线性模型值的一系列x值来计算最佳拟合线。

要绘制线条,我们可以重复前面的代码,并在 Bokeh 中的线模块中添加额外的调用。我们还需要为这条线设置一个新的数据源。

  1. 重新绘制前面的图表,并通过运行以下代码添加最佳拟合线:
      source = ColumnDataSource(data=dict(
          x=df['Yearly Change'],
          y=df['Interest rate'],
       ...
       ...
       p.line('x', 'y', line_width=2, line_color='red',
          source=lm_source)
          show(p)

对于线条源lm_source,我们将国家名称和人口设置为 N/A,因为它们对于最佳拟合线不适用。当悬停在线条上时,可以看到它们确实出现在工具提示中。

这种可交互的可视化方式让我们有机会探索数据集中的离群值,例如右下角的小点。

  1. 使用缩放工具并悬停在有趣的样本上来探索这个图表。请注意以下内容:
    • 乌克兰的利率异常高,考虑到低的年度人口变化:
    • 作为一个小国家,巴林的利率异常低,考虑到高的年度人口变化:

总结

在本章中,我们抓取了网页表格,并利用交互式可视化来研究数据。

我们首先了解了 HTTP 请求的工作原理,重点关注了 GET 请求及其响应状态码。接着,我们进入了 Jupyter Notebook,使用 Python 的 Requests 库发起了 HTTP 请求。我们看到,Jupyter 可以在笔记本中渲染 HTML,以及实际可以交互的网页。发起请求后,我们了解了如何使用 Beautiful Soup 来解析 HTML 中的文本,并利用这个库来抓取表格数据。

在抓取了两个数据表后,我们将它们存储在 pandas DataFrame 中。第一个表包含了各国中央银行的利率,第二个表则包含了人口数据。我们将它们合并成一个单独的表格,并用这个表格来创建交互式可视化。

最后,我们使用 Bokeh 在 Jupyter 中渲染了交互式可视化。我们学习了如何使用 Bokeh API 创建各种定制的图表,并制作了带有缩放、平移和悬停等交互功能的散点图。在定制化方面,我们明确展示了如何为每个数据样本设置点的半径和颜色。此外,在使用 Bokeh 探索抓取的各国人口数据时,悬停在点上时,工具提示展示了国家名称及相关数据。

恭喜你完成了这个使用 Jupyter Notebooks 的数据科学入门课程!无论你在阅读本书之前对 Jupyter 和 Python 有多少经验,你都学到了一些有用且可以应用于实际数据科学的技能!

第四章:神经网络与深度学习简介

MNIST 数据集的图像边缘没有包含数字。因此,两个网络都不会给位于该区域的像素分配相关的值。如果我们将数字画得更靠近指定区域的中心,两个网络在分类数字时的准确率都明显提高。这表明,神经网络的强大程度取决于用来训练它们的数据。如果用于训练的数据与我们尝试预测的数据有很大不同,那么网络很可能会产生令人失望的结果。本章将涵盖神经网络的基础知识以及如何设置深度学习编程环境。我们还将探索神经网络的常见组件及其基本操作。最后,我们将通过探索使用 TensorFlow 创建的训练神经网络来结束本章内容。

本章的内容是理解神经网络能够做什么。我们不会涉及深度学习算法背后的数学概念,而是描述构成深度学习系统的基本元素。我们还将探讨神经网络在解决实际问题中的应用实例。

本章将为你提供一个关于如何设计使用神经网络解决问题的系统的实际直觉——包括如何判断一个给定的问题是否可以用这些算法解决。其核心是,本章挑战你将问题看作是思想的数学表示。到本章结束时,你将能够将问题看作这些表示的集合,并开始识别这些表示如何通过深度学习算法进行学习。

本章结束时,你将能够:

  • 探讨神经网络的基础知识

  • 设置深度学习编程环境

  • 探索神经网络的常见组件及其基本操作

  • 通过探索使用 TensorFlow 创建的训练神经网络,结束本章内容

什么是神经网络?

神经网络——也称为人工神经网络——最早由麻省理工学院教授沃伦·麦卡洛克和沃尔特·皮茨于 20 世纪 40 年代提出。

欲了解更多信息,请参阅《解释:神经网络》。麻省理工学院新闻办公室,2017 年 4 月 14 日。可在以下网址获取:

news.mit.edu/2017/explained-neural-networksdeep-learning-0414

受神经科学进展的启发,他们提议创建一个能够再现大脑工作方式(无论是人类还是其他)的计算机系统。其核心思想是一个作为相互连接网络运作的计算机系统。也就是说,一个由许多简单组件组成的系统,这些组件既解释数据,又相互影响如何解释数据。这个核心思想至今依然存在。

深度学习在很大程度上被认为是现代神经网络的研究。可以把它看作是神经网络的一个现代名称。主要的区别在于,深度学习中使用的神经网络通常要大得多——即,它们有更多的节点和层——相比早期的神经网络。深度学习算法和应用通常需要资源才能取得成功,因此使用“深度”一词来强调它的规模和大量的相互连接的组件。

成功的应用

神经网络自从 20 世纪 40 年代起便开始研究,虽然形式各异。但直到最近,深度学习系统才在大规模工业应用中取得了成功。

当代神经网络的倡导者在语音识别、语言翻译、图像分类和其他领域取得了巨大的成功。它如今的显著地位得益于计算能力的大幅提升以及图形处理单元GPU)和张量处理单元TPU)的出现——它们能够进行比普通 CPU 更多的同时数学运算,并且数据的可用性也大大增加。

不同 AlphaGo 算法的功耗。AlphaGo 是 DeepMind 提出的一个项目,旨在开发一系列击败围棋的算法。它被认为是深度学习强大能力的一个典型例子。TPU 是由 Google 开发的一种芯片组,专门用于深度学习程序。

图示展示了用于训练不同版本 AlphaGo 算法的 GPU 和 TPU 数量。来源:deepmind.com/blog/alphago-zero-learning-scratch/

在本书中,我们不会使用 GPU 来完成我们的活动。使用神经网络并不需要 GPU。在本书提供的一些简单示例中——所有计算都可以通过普通笔记本电脑的 CPU 完成。然而,当处理非常大的数据集时,GPU 会非常有帮助,因为训练神经网络所需的长时间将变得不切实际。

下面是神经网络在一些领域取得巨大影响的例子:

  • 翻译文本:2017 年,Google 宣布推出一种新的翻译算法,称为 Transformer。该算法由一个递归神经网络(LSTM)构成,使用双语文本进行训练。Google 表示,与行业标准(BLEU)相比,该算法在准确性方面表现突出,并且计算效率也很高。在撰写本文时,Transformer 被报告为 Google Translate 的主要翻译算法。

Google Research Blog。Transformer:一种用于语言理解的新型神经网络架构。2017 年 8 月 31 日。网址:research.googleblog.com/2017/08/transformernovel-neural-network.html

  • 自动驾驶车辆:Uber、NVIDIA 和 Waymo 被认为正在使用深度学习模型来控制驾驶相关的各项车辆功能。每家公司都在研究多个可能性,包括利用人类训练网络、在虚拟环境中模拟车辆驾驶,甚至创建一个类似小城市的环境,在其中车辆可以根据预期和意外事件进行训练。

Alexis C. Madrigal:深入 Waymo 的秘密世界,了解自动驾驶汽车的训练。The Atlantic。2017 年 8 月 23 日。网址:https://

www.theatlantic.com/technology/archive/2017/08/inside-waymos-secret-testing-and-simulationfacilities/537648/

NVIDIA:端到端深度学习用于自动驾驶汽车。2016 年 8 月 17 日。网址:devblogs.nvidia.com/

parallelforall/deep-learning-self-driving-cars/

Dave Gershgorn:Uber 的新 AI 团队正在寻找通往自动驾驶汽车的最短路径。Quartz。2016 年 12 月 5 日。网址:qz.com/853236/ubers-new-ai-team-is-looking-for-theshortest-route-to-self-driving-cars/

  • 图像识别:Facebook 和 Google 使用深度学习模型来识别图像中的实体,并自动将这些实体标记为联系人中的人物。在这两种情况下,网络都是通过已标记的图像以及来自目标朋友或联系人的图像来进行训练的。两家公司都报告称,这些模型在大多数情况下能够高精度地推荐朋友或联系人。

虽然在其他行业中还有更多的例子,但深度学习模型的应用仍处于起步阶段。许多成功的应用尚未到来,包括你自己创造的那些。

为什么神经网络工作得如此出色?

为什么神经网络如此强大?神经网络之所以强大,是因为它们可以用来预测任何给定的函数,并给出合理的逼近。如果某人能够将一个问题表示为数学函数,并且拥有正确表示该函数的数据,那么深度学习模型原则上——在有足够资源的情况下——能够逼近该函数。这通常被称为神经网络的普适性原理

欲了解更多信息,请参考 Michael Nielsen 的《神经网络与深度学习:神经网络能够计算任何函数的可视化证明》。可访问:neuralnetworksanddeeplearning.com/chap4.html

本书中不会深入探讨普适性原理的数学证明。然而,神经网络的两个特性应该能够给你正确的直觉,帮助你理解这一原理:表示学习和函数逼近。

欲了解更多信息,请参考 Kai Arulkumaran, Marc Peter Deisenroth, Miles Brundage 和 Anil Anthony Bharath 的文章《深度强化学习简短调查》。arXiv. 2017 年 9 月 28 日。可访问:www.arxiv-vanity.com/papers/1708.05866/

表示学习

用于训练神经网络的数据包含表示(也称为特征),这些表示解释了你试图解决的问题。例如,如果某人想从图像中识别人脸,那么包含人脸的图像中每个像素的颜色值将作为起点。然后,模型将在训练过程中不断地通过组合像素来学习更高层次的表示。

图 1:从输入数据开始的一系列更高级的表示。图像衍生自原始图像,来源于:Yann LeCun, Yoshua Bengio & Geoffrey Hinton. "Deep Learning". Nature 521, 436–444 (2015 年 5 月 28 日) doi:10.1038/nature14539

用正式的语言来说,神经网络是计算图,其中每一步从输入数据中计算出更高层次的抽象表示。

每一步都代表着进入不同抽象层次的进展。数据通过这些层次,逐步构建出更高层次的表示。该过程最终完成时会得到最高层次的表示:即模型试图预测的那个表示。

函数逼近

当神经网络学习数据的新表示时,它们通过将权重和偏差与不同层次的神经元结合来实现这一过程。每次训练周期进行时,它们都会使用一种叫做反向传播的数学技术调整这些连接的权重。每一轮的权重和偏差都会得到改进,直到达到最佳状态。这意味着神经网络可以在每个训练周期中衡量它的错误,调整每个神经元的权重和偏差,并重新尝试。如果它确定某个修改比上一次的结果更好,它会继续强化这个修改,直到达到最佳解决方案。

简而言之,这个过程就是神经网络能够近似函数的原因。然而,有许多原因可能导致神经网络无法完美地预测一个函数,其中最主要的原因是:

  • 许多函数包含随机特性(即随机性)

  • 可能会过拟合训练数据中的特殊性

  • 可能缺乏训练数据

在许多实际应用中,简单的神经网络能够以合理的精度近似一个函数。这类应用将是我们的重点。

深度学习的局限性

深度学习技术最适用于那些可以用正式数学规则定义的问题(即作为数据表示)。如果一个问题很难以这种方式定义,那么深度学习很可能无法提供有用的解决方案。此外,如果用于某个问题的数据有偏差,或者只包含生成该问题的潜在函数的部分表示,那么深度学习技术只能复制该问题,而无法学会如何解决它。

记住,深度学习算法通过学习数据的不同表示来近似给定的函数。如果数据没有恰当地代表一个函数,那么神经网络很可能会错误地表示该函数。考虑以下类比:你正在尝试预测全国的汽油价格(即燃料价格),并创建一个深度学习模型。你使用信用卡账单中关于日常汽油消费的支出数据作为该模型的输入数据。模型可能最终学会你汽油消费的模式,但它很可能无法准确地表示由其他因素引起的汽油价格波动,这些因素在你的数据中每周才出现一次,比如政府政策、市场竞争、国际政治等。最终,模型在生产环境中使用时会得出错误的结果。

为了避免这个问题,确保用于训练模型的数据尽可能准确地代表模型试图解决的问题。

要深入讨论这个话题,请参考 François Chollet 即将出版的书籍《深度学习与 Python》。François 是 Keras 的创始人,Keras 是本书中使用的 Python 库。章节《深度学习的局限性》对理解这一话题尤其重要。该书的工作版本可通过以下链接访问:blog.keras.io/the-limitations-of-deep-learning.html

固有偏见和伦理考虑

研究人员建议,如果在没有考虑训练数据中固有偏见的情况下使用深度学习模型,不仅可能导致效果不佳的解决方案,还可能引发伦理上的复杂问题。

例如,在 2016 年末,来自中国上海交通大学的研究人员创建了一个神经网络,通过仅仅使用面部照片就能够正确分类犯罪分子。研究人员使用了 1,856 张中国男性的图片,其中一半是已被定罪的。

他们的模型以 89.5%的准确率识别出了囚犯。( blog.keras.io/the-limitations-of-deep-learning.html)。《麻省理工科技评论》。神经网络通过面部识别来识别犯罪分子。2016 年 11 月 22 日。可通过以下链接访问:www.technologyreview.com/s/602955/neural-network-learns-to-identify-criminals-by-their-faces/

这篇论文在科学界和大众媒体中引发了极大的争议。该方案的一个关键问题在于未能正确识别输入数据中固有的偏见。也就是说,本研究中使用的数据来自两个不同的来源:一个是犯罪分子,另一个是非犯罪分子。一些研究人员建议,他们的算法识别的是与研究中使用的不同数据来源相关的模式,而不是从人脸中识别出相关的模式。尽管可以从技术角度讨论模型的可靠性,但关键的批评还是从伦理角度出发:我们应该清楚地识别深度学习算法所使用的输入数据中的固有偏见,并考虑其应用将如何影响人们的生活。

Timothy Revell。使用面部识别技术“识别”犯罪分子的担忧。《新科学家》2016 年 12 月 1 日。可通过以下链接访问:www.newscientist.com/article/2114900-concernsas-face-recognition-tech-used-to-identify-criminals/。有关学习算法(包括深度学习)伦理问题的更多了解,请参考 AI Now 研究所的工作(ainowinstitute.org/),该机构旨在理解智能系统的社会影响。

神经网络的常见组成部分和操作

神经网络有两个关键组件:层和节点。节点负责特定的操作,而层是由多个节点组成的,用于区分系统的不同阶段。

通常,神经网络有以下三类层:

  • 输入:接收输入数据并进行初步解释的地方

  • 隐藏:计算发生的地方,数据在这里被修改并传递

  • 输出:输出被组装和评估的地方

图 2:神经网络中最常见层的示意图。由 Glosser.ca - 自主创作,衍生自文件:人工神经网络.svg,CC BY-SA 3.0,https://commons.wikimedia.org/w/index.php?curid=24913461

隐藏层是神经网络中最重要的层。它们被称为隐藏层,因为在这些层中生成的表示在数据中不可用,而是从数据中学习到的。正是在这些层中,神经网络的主要计算过程发生。

节点是数据在网络中表示的地方。与节点相关联的有两个值:偏差和权重。这两个值会影响数据如何被节点表示并传递给其他节点。当网络学习时,它会有效地调整这些值以满足优化函数。

神经网络的大部分工作发生在隐藏层中。不幸的是,目前并没有明确的规则来确定一个网络应该有多少层或节点。在实现神经网络时,人们通常会花时间尝试不同层和节点的组合。建议总是从一个单层开始,并且节点的数量应该反映输入数据的特征数(即数据集中有多少)。然后,继续添加层和节点,直到达到满意的性能——或者当网络开始过拟合训练数据时。

当代神经网络实践通常仅限于实验节点和层的数量(例如,网络的深度)以及每层执行的操作类型。许多成功的案例表明,神经网络仅通过调整这些参数就能超越其他算法。

作为直观的理解,想象数据通过输入层进入神经网络系统,然后在网络中通过节点逐一传递。数据所走的路径将取决于节点的互联程度、每个节点的权重和偏差、每层执行的操作类型以及数据在这些操作后的状态。神经网络通常需要多次“运行”(或训练周期),以不断调整节点的权重和偏差,这意味着数据在图的不同层之间会多次传递。

本节为您提供了神经网络和深度学习的概述。此外,我们讨论了初学者理解以下关键概念的直觉:

  • 在原则上,神经网络可以近似大多数函数,只要有足够的资源和数据。

  • 层和节点是神经网络的最重要结构组件。通常,人们花费大量时间来修改这些组件,以找到适用的架构。

  • 权重和偏差是网络在训练过程中“学习”的关键属性。

这些概念将在我们下一节中证明其有用性,因为我们将探索一个实际训练过的神经网络,并对其进行修改以训练我们自己的网络。

配置深度学习环境

在完成本章之前,我们希望您与一个真实的神经网络进行交互。我们将首先介绍本书中使用的主要软件组件,并确保它们已正确安装。然后,我们将探索一个预训练的神经网络,并探讨之前讨论的一些组件和操作,这些操作位于“什么是神经网络?”部分。

用于深度学习的软件组件

我们将在深度学习中使用以下软件组件:

Python 3

我们将使用 Python 3。Python 是一种通用编程语言,在科学界非常流行,因此在深度学习中被广泛采用。本书不支持 Python 2,但可以用它来训练神经网络,而不是 Python 3。即使选择在 Python 2 中实现解决方案,考虑迁移到 Python 3,因为其现代功能集比前者更为强大。

TensorFlow

TensorFlow 是一个用于执行图形形式的数学操作的库。TensorFlow 最初由 Google 开发,今天是一个拥有许多贡献者的开源项目。它专为神经网络而设计,是创建深度学习算法时最受欢迎的选择之一。

TensorFlow 也以其生产组件而闻名。它附带 TensorFlow Serving (https://github.com/tensorflow/serving), 这是一个用于提供深度学习模型的高性能系统。此外,经过训练的 TensorFlow 模型可以在其他高性能编程语言(如 Java、Go 和 C)中使用。这意味着可以在从微型计算机(即 RaspberryPi)到 Android 设备的任何设备上部署这些模型。

Keras

为了与 TensorFlow 高效交互,我们将使用 Keras (keras.io/),一个提供高级 API 以开发神经网络的 Python 包。虽然 TensorFlow 专注于组件之间的计算图交互,但 Keras 则专注于神经网络。Keras 使用 TensorFlow 作为其后端引擎,使得开发这类应用程序更加容易。

截至 2017 年 11 月(TensorFlow 版本 1.4),Keras 作为 TensorFlow 的一部分进行分发。它在 tf.keras 命名空间下可用。如果你已经安装了 TensorFlow 1.4 或更高版本,你的系统中已经包含了 Keras。

TensorBoard

TensorBoard 是一个数据可视化工具套件,用于探索 TensorFlow 模型,并与 TensorFlow 原生集成。TensorBoard 通过消耗 TensorFlow 在训练神经网络时创建的检查点和摘要文件来工作。这些文件可以在接近实时(延迟 30 秒)或者在网络训练完成后进行探索。

TensorBoard 使得实验和探索神经网络的过程变得更加容易——而且追踪你的网络训练过程非常令人兴奋!

Jupyter Notebooks,Pandas 和 NumPy

在使用 Python 创建深度学习模型时,通常会从交互式工作开始,慢慢开发出一个最终变成更结构化软件的模型。在这个过程中,经常使用三个 Python 包:Jupyter Notebooks,Pandas 和 NumPy

  • Jupyter Notebooks 创建使用 web 浏览器作为界面的交互式 Python 会话

  • Pandas 是一个用于数据处理和分析的包

  • NumPy 经常用于数据的形状变换和执行数值计算

这些包偶尔使用。它们通常不构成生产系统的一部分,但在探索数据和开始构建模型时常被使用。我们更详细地关注其他工具。

Michael Heydt(2017 年 6 月,Packt 出版)的《学习 Pandas》和 Dan Toomey(2016 年 11 月,Packt 出版)的《学习 Jupyter》提供了如何使用这些技术的全面指南。这些书籍是继续深入学习的好参考。

组件 描述 最低版本
Python 通用编程语言。流行的用于开发深度学习应用程序的语言。 3.6
TensorFlow 开源图计算 Python 包,通常用于开发深度学习系统。 1.4

| Keras | 提供高层次接口到 TensorFlow 的 Python 包。 |  2.0.8-tf(与 TensorFlow 一起分发)

|

TensorBoard 基于浏览器的软件,用于可视化神经网络统计数据。 0.4.0
Jupyter Notebook 基于浏览器的软件,用于交互式工作与 Python 会话。 5.2.1
Pandas 用于分析和处理数据的 Python 包。 0.21.0
NumPy 用于高性能数值计算的 Python 包。 1.13.3

表 1:创建深度学习环境所需的软件组件

活动:验证软件组件

在我们探索一个训练好的神经网络之前,让我们验证所有必需的软件组件是否已准备好。我们提供了一个脚本来验证这些组件的可用性。让我们花点时间运行脚本,并处理可能遇到的任何问题。

现在我们将测试是否所有本书所需的软件组件都在您的工作环境中可用。首先,我们建议使用 Python 的原生模块 venv 创建一个 Python 虚拟环境。虚拟环境用于管理项目依赖项。我们建议您为每个创建的项目配置独立的虚拟环境。现在让我们创建一个。

如果您更喜欢使用 conda 环境,可以随意使用它们。

  1. 可以使用以下命令创建一个 Python 虚拟环境:
      $ python3 -m venv venv
      $ source venv/bin/activate
  1. 后者命令会将字符串(venv)添加到命令行的开头。使用以下命令来停用您的虚拟环境:
      $ deactivate 

确保在处理项目时始终激活您的 Python 虚拟环境。

  1. 激活虚拟环境后,通过执行 pip 对 requirements.txt 文件进行操作,确保正确的组件已安装。这将尝试在该虚拟环境中安装本书使用的模型。如果它们已存在,则不会执行任何操作:

图 3:终端运行 pip 安装 requirements.txt 中的依赖项的图片

运行以下命令来安装依赖项:

      $ pip install –r requirements.txt 

这将为您的系统安装所有必需的依赖项。如果它们已经安装,该命令将简单地通知您。

这些依赖项对于所有代码活动的正常运行至关重要。

作为此活动的最后一步,让我们执行脚本 test_stack.py。该脚本正式验证此书所需的所有包是否已安装并在您的系统中可用。

  1. 学生们,运行脚本 Chapter_4/activity_1/test_stack.py 检查 Python 3、TensorFlow 和 Keras 是否可用。使用以下命令:
      $ python3 chapter_4/activity_1/test_stack.py 

脚本返回有用的消息,说明已安装什么以及需要安装什么。

  1. 在您的终端中运行以下脚本命令:
      $ tensorboard --help 

您应该看到一条帮助信息,解释每个命令的作用。如果没有看到该消息,或者看到错误消息,请向您的讲师寻求帮助:

图 4:终端运行 python3 test_stack.py 的图片。脚本返回消息,告知所有依赖项已正确安装。

如果出现类似以下消息,无需担心:

运行时警告:模块'tensorflow.python.framework.fast_tensor_util'的编译时版本 3.5 与运行时版本 3.6 不匹配,返回 f(*args, **kwds)

如果您运行的是 Python 3.6 并且分发

TensorFlow wheel 是在不同版本(在此情况下为 3.5)下编译的。您可以安全地忽略该消息。

一旦我们确认安装了 Python 3、TensorFlow、Keras、TensorBoard 以及requirements.txt中列出的软件包,我们就可以继续进行演示,了解如何训练神经网络,然后使用这些工具来探索已经训练好的网络。

探索训练好的神经网络

在本节中,我们将探索一个已经训练好的神经网络。我们这么做是为了理解神经网络如何解决一个实际问题(预测手写数字),同时熟悉 TensorFlow 的 API。在探索这个神经网络时,我们会看到许多在前面章节中介绍过的组件,如节点和层,但我们也会看到许多不太熟悉的组件(例如激活函数)——我们将在后续章节中进一步探讨这些内容。然后,我们将通过一个练习,讲解该神经网络是如何训练的,并且尝试自己训练这个网络。

我们将要探索的网络已经经过训练,可以识别手写数字(整数)。它使用了 MNIST 数据集( yann.lecun.com/exdb/mnist/),这是一个经典的数据集,常用于探索模式识别任务。

MNIST 数据集

修改版国家标准与技术研究院MNIST)数据集包含一个包含 60,000 张图像的训练集和一个包含 10,000 张图像的测试集。每张图像包含一个手写数字。这个数据集最初是由美国政府创建的,用来测试不同的计算机系统识别手写文字的方法。能够做到这一点对于提高邮政服务、税收系统和政府服务的效率具有重要意义。由于 MNIST 数据集对现代方法来说过于简单,因此现在的研究通常使用不同的、更新的数据集(例如 CIFAR)。然而,MNIST 数据集仍然非常有助于理解神经网络的工作原理,因为已知的模型可以高效地实现较高的准确率。

CIFAR 数据集是一个机器学习数据集,包含按照不同类别组织的图像。与 MNIST 数据集不同,CIFAR 数据集包含多个领域的类别,如动物、活动和物体。CIFAR 数据集可以在以下链接找到:www.cs.toronto.edu/~kriz/cifar.html

图 5:MNIST 数据集训练集的摘录。每张图像是一个单独的 20x20 像素图像,包含一个手写数字。原始数据集可以在以下链接找到:http://yann.lecun.com/exdb/mnist/。

使用 TensorFlow 训练神经网络

现在,让我们训练一个神经网络,使用 MNIST 数据集来识别新的数字。

我们将实现一种特殊用途的神经网络,称为“卷积神经网络”,来解决这个问题(我们将在后续章节中更详细地讨论这些内容)。我们的网络包含三层隐藏层:两层全连接层和一层卷积层。卷积层由以下 TensorFlow 的 Python 代码片段定义:

    W = tf.Variable(
        tf.truncated_normal([5, 5, size_in, size_out],
        stddev=0.1),
        name="Weights")    
    B = tf.Variable(tf.constant(0.1, shape=[size_out]), 
        name="Biases")

    convolution = tf.nn.conv2d(input, W, strides=[1, 1, 1, 1],
    padding="SAME")
    activation = tf.nn.relu(convolution + B)

    tf.nn.max_pool(
    activation,
    ksize=[1, 2, 2, 1],
    strides=[1, 2, 2, 1],
    padding="SAME") 

我们在训练网络时只执行这段代码一次。

变量 W 和 B 代表权重和偏置。这些值是隐藏层中的节点用来改变网络对数据的解释的,数据在网络中传递时会被这些值所修改。暂时不要担心其他变量。

全连接层由以下 Python 代码片段定义:

    W = tf.Variable(
        tf.truncated_normal([size_in, size_out], stddev=0.1),
        name="Weights")
    B = tf.Variable(tf.constant(0.1, shape=[size_out]),
        name="Biases")
        activation = tf.matmul(input, W) + B

这里,我们还有两个 TensorFlow 变量 W 和 B。请注意这些变量的初始化有多简单:W 被初始化为一个来自剪枝高斯分布的随机值(剪枝范围为size_in 和 size_out),标准差为 0.1,B(偏置项)被初始化为0.1,这是一个常数。这两个值会在每次运行时不断变化。这段代码执行两次,产生两个全连接网络——一个将数据传递给另一个。

这 11 行 Python 代码代表了我们的完整神经网络。在第五章模型架构中,我们将详细讲解每个组件如何使用 Keras 实现。目前,请重点理解网络如何在每次运行时改变每一层中 W 和 B 的值,以及这些代码片段如何构成不同的层。这 11 行 Python 代码是数十年神经网络研究的结晶。

现在让我们训练这个网络,评估它在 MNIST 数据集上的表现。

训练神经网络

按照以下步骤设置本次练习:

  1. 打开两个终端实例。

  2. 在两个终端中,导航到chapter_4/exercise_a目录。

  3. 在两个终端中,确保你的 Python 3 虚拟环境是激活状态,并且requirements.txt中列出的依赖已安装。

  4. 其中之一是通过以下命令启动 TensorBoard 服务器:

    $ tensorboard --logdir=mnist_example/

  5. 在另一个终端中,从该目录中运行train_mnist.py脚本。

  6. 在浏览器中打开当你启动服务器时提供的 TensorBoard URL。

在你运行train_mnist.py脚本的终端中,你将看到一个包含模型训练进度的进度条。当你打开浏览器页面时,你会看到几个图表。点击显示Accuracy的图表,放大它并让页面刷新(或者点击refresh按钮)。你将看到随着训练轮次的增加,模型的准确度逐渐提高。

利用这个时刻,解释神经网络在训练过程早期迅速达到高精度的强大能力。

我们可以看到,在大约 200 个周期(或步骤)后,网络超过了 90% 的准确率。也就是说,网络在测试集上正确预测了 90% 的数字。随着训练的进行,网络继续提高准确率,直到第 2000 步,最终达到 97% 的准确率。

现在,我们还将测试这些网络在未见数据上的表现。我们将使用由 Shafeen Tejani 创建的一个开源 Web 应用,探索训练好的网络是否能正确预测我们创建的手写数字。

使用未见数据测试网络性能

在浏览器中访问 mnist-demo.herokuapp.com/ 并在指定的白色框中绘制一个 0 到 9 之间的数字:

图 6:我们可以手动绘制数字并测试两个训练网络准确性的 Web 应用

来源:github.com/ShafeenTejani/mnist-demo

在应用程序中,你可以看到两个神经网络的结果。我们训练的那个在左边(叫做 CNN)。它能正确分类你所有的手写数字吗?试着在指定区域的边缘绘制数字。例如,试着在该区域的右边缘附近绘制数字1

图 7:两个网络都难以估算绘制在区域边缘的值

在这个例子中,我们看到数字1被绘制在绘图区域的右侧。在两个网络中,这个数字是1的概率都是0

MNIST 数据集不包含图像边缘的数字。因此,两个网络都没有为该区域内的像素分配相关的值。如果我们将数字绘制得离指定区域的中心更近,这两个网络在分类数字时会表现得更好。这表明神经网络的强大程度仅取决于用来训练它们的数据。如果训练数据与我们试图预测的数据相差甚远,网络很可能会产生令人失望的结果。

活动:探索训练过的神经网络

在本节中,我们将探索我们在练习中训练的神经网络。我们还将通过调整超参数来训练一些其他的网络。让我们先来探索在练习中训练的网络。

我们已经将训练好的网络作为二进制文件提供在该目录下。让我们使用 TensorBoard 打开这个训练好的网络,并探索其组成部分。

使用你的终端,导航到目录 chapter_4/activity_2 并执行以下命令启动 TensorBoard:

  $ tensorboard --logdir=mnist_example/ 

现在,在浏览器中打开 TensorBoard 提供的 URL。你应该能够看到 TensorBoard 的标量页面:

图 8:启动 TensorBoard 实例后的终端图像

在你打开tensorboard命令提供的网址后,你应该能够看到以下 TensorBoard 页面:

图 9:TensorBoard 登录页面的图像

现在,让我们来探索我们训练好的神经网络,看看它的表现如何。

在 TensorBoard 页面上,点击Scalars页面,并放大准确率图表。现在,将平滑滑块移到0.9

准确率图表衡量了网络在测试集上猜测标签的准确性。刚开始时,网络的标签猜测完全错误。这是因为我们将网络的权重和偏置初始化为随机值,所以它的第一次尝试只是一个猜测。然后,网络将在第二次运行时调整其层的权重和偏置;网络将通过改变权重和偏置来投资于那些给出正面结果的节点,而通过逐渐减少它们对网络的影响(最终达到 0)来惩罚那些表现不佳的节点。正如你所看到的,这是一种非常高效的技巧,可以快速得到良好的结果。

让我们将注意力集中在准确率图表上。看看算法是如何在大约 1,000 个 epoch 后达到很高的准确率(> 95%)的?在 1,000 到 2,000 个 epoch 之间发生了什么?

如果我们继续训练更多的 epoch,网络会变得更准确吗?在 1,000 到 2,000 个 epoch 之间,网络的准确率持续提高,但提升的速度在减慢。如果继续训练,网络可能会有轻微的改进,但在当前架构下,它无法达到 100%的准确率。

这个脚本是一个修改版的官方 Google 脚本,旨在展示 TensorFlow 是如何工作的。我们将脚本分成了更易理解的函数,并添加了许多注释来引导你的学习。尝试通过修改脚本顶部的变量来运行这个脚本:

     LEARNING_RATE = 0.0001
     EPOCHS = 2000

现在,尝试通过修改这些变量的值来运行那个脚本。例如,试着将学习率修改为0.1,将 epoch 设置为100。你认为网络能够获得相似的结果吗?

你的神经网络中有许多其他参数可以修改。现在,尝试调整网络的 epoch 和学习率。你会发现,这两个参数本身就能极大地改变网络的输出——但也有其限制。尝试看看通过仅仅改变这两个参数,是否能够使当前架构下的网络训练更快。

使用 TensorBoard 验证你的网络训练情况。通过将起始值乘以 10,多次调整这些参数,直到你注意到网络有所改善。这种调整网络并找到更好准确率的过程类似于今天在工业应用中用来改进现有神经网络模型的方法。

总结

在本章中,我们使用 TensorBoard 探索了一个基于 TensorFlow 训练的神经网络,并用不同的训练轮次和学习率训练了我们自己的修改版网络。这为你提供了如何训练一个高效神经网络的实际操作经验,并且让你有机会探索其一些局限性。

你认为我们能否在真实的比特币数据上实现类似的准确度?我们将在第五章模型架构中尝试使用一种常见的神经网络算法来预测未来的比特币价格。在第六章模型评估与优化中,我们将评估并改进该模型,最后在第七章产品化中,我们将创建一个通过 HTTP API 提供该系统预测的程序。

第五章:模型架构

基于第四章《神经网络与深度学习介绍》的基本概念,我们现在进入一个实际问题:我们能否使用深度学习模型预测比特币价格?在本章中,我们将学习如何构建一个尝试进行此预测的深度学习模型。我们将通过将所有这些组件结合起来,构建一个简单但完整的深度学习应用程序的初步版本来结束本章。

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

  • 为深度学习模型准备数据

  • 选择正确的模型架构

  • 使用 Keras,这是一个 TensorFlow 抽象库

  • 使用训练好的模型进行预测

选择合适的模型架构

深度学习是一个正在进行激烈研究活动的领域。研究人员致力于发明新的神经网络架构,这些架构可以解决新问题或提高之前实现的架构的性能。在本节中,我们将研究旧的和新的架构。

旧的架构已经被广泛应用于解决各种问题,并且通常被认为是在开始新项目时的正确选择。较新的架构在特定问题上取得了巨大成功,但它们更难以推广。后者作为下一步探索的参考非常有趣,但在启动项目时并不是一个好的选择。

常见架构

考虑到众多架构的可能性,有两种流行的架构经常作为许多应用的起点:卷积神经网络CNNs)和递归神经网络RNNs)。这些是基础性网络,应该作为大多数项目的起点。我们还介绍了另外三种网络,因其在该领域的重要性:长短期记忆LSTM)网络,RNN 的变种;生成对抗网络GANs);以及深度强化学习。这些后者架构在解决当代问题时取得了巨大成功,但使用起来相对更为复杂。

卷积神经网络

卷积神经网络因其在处理具有网格状结构的问题中表现出色而声名显赫。它们最初是为了分类图像而创建的,但已经在许多其他领域得到了应用,从语音识别到自动驾驶汽车。

CNN 的核心思想是将紧密相关的数据作为训练过程的一个要素,而不仅仅是单独的数据输入。这个理念在图像处理中尤其有效,因为图像中位于另一个像素右侧的像素与该像素相关,因为它们是更大构图的一部分。在这种情况下,网络训练的目标就是预测该构图。因此,将几个像素组合在一起比仅使用单独的像素要好。

卷积这个名称是用来表示这个过程的数学表达式:

图 1:卷积过程的插图 图像来源:Volodymyr Mnih 等人。

欲了解更多信息,请参考《通过深度强化学习实现人类级别的控制》,2015 年 2 月,《自然》杂志。可通过以下链接访问:storage.googleapis.com/deepmind-media/dqn/DQNNaturePaper.pdf

循环神经网络

卷积神经网络通过一组输入来工作,这些输入会不断改变网络各层和节点的权重和偏置。这种方法的一个已知限制是,它的架构在决定如何改变网络的权重和偏置时忽略了这些输入的顺序。

循环神经网络正是为了应对这个问题而创建的。RNNs 旨在处理序列数据。这意味着在每个迭代中,层级可以受到前一层输出的影响。在给定序列中的先前观察的记忆在评估后续观察时起着重要作用。

由于语音识别问题具有序列性质,RNNs(循环神经网络)在该领域得到了成功的应用。此外,它们还用于翻译问题。谷歌翻译当前的算法——称为Transformer——使用 RNN 将文本从一种语言翻译成另一种语言。

欲了解更多信息,请参考《Transformer:一种用于语言理解的新型神经网络架构》,作者:Jakob Uszkoreit,谷歌研究博客,2017 年 8 月。可通过以下链接访问:ai.googleblog.com/2017/08/transformer-novel-neural-network.html

图 2:来自 distill.pub 的插图(https://distill.pub/2016/augmented-rnns/)

图 2 显示了英语中的单词与法语中的单词之间的关系,这种关系取决于它们在句子中出现的位置。RNNs 在语言翻译问题中非常流行。

长短期记忆网络(LSTM)是为了解决梯度消失问题而创建的 RNN 变种。梯度消失问题是由于记忆组件距离当前步骤太远,导致它们因距离较远而获得较低的权重。LSTM 是 RNN 的一种变体,包含一个叫做忘记门的记忆组件。该组件可用于评估近期和旧的元素如何影响权重和偏置,具体取决于观察在序列中的位置。

欲了解更多细节,请参见 Sepp Hochreiter 和 Jürgen Schmidhuber 于 1997 年首次提出的 LSTM 架构。当前的实现版本已有多次修改。关于 LSTM 每个组件如何工作的详细数学解释,建议参考 Christopher Olah 于 2015 年 8 月发布的文章《理解 LSTM 网络》,可访问:colah.github.io/posts/2015-08-Understanding-LSTMs/

生成对抗网络

生成对抗网络 (GANs) 是由 Ian Goodfellow 及其在蒙特利尔大学的同事们于 2014 年发明的。GANs 提出,应该有两个神经网络相互竞争,以此来优化权重和偏置,而不是仅有一个神经网络去最小化其错误。

欲了解更多细节,请参见 Ian Goodfellow 等人所著的《生成对抗网络》,发表于 arXiv. 2014 年 6 月 10 日。可访问:arxiv.org/abs/1406.2661

GANs 拥有一个生成新数据(即“假”数据)的网络和一个评估由第一个网络生成的数据是否真实的网络。它们相互竞争,因为它们都在学习:一个学习如何更好地生成“假”数据,另一个则学习如何区分数据是否为真实。它们在每一个迭代周期中不断优化,直到两者都收敛。此时,评估生成数据的网络无法再区分“假”数据和真实数据。

GANs 已成功应用于数据具有明确拓扑结构的领域。其最初的实现是使用 GAN 生成与真实图像相似的物体、人物面孔和动物的合成图像。图像生成是 GAN 应用最为广泛的领域,但在其他领域的应用也偶尔出现在研究论文中。

图 3:展示不同 GAN 算法在根据给定情绪变化人物面孔的结果。来源:StarGAN 项目。可访问:github.com/yunjey/StarGAN

深度强化学习

原始的深度强化学习(DRL)架构由 Google 旗下的人工智能研究机构 DeepMind 提出,该机构位于英国。

DRL 网络的关键思想是它们本质上是无监督的,并且通过试错学习,只优化奖励函数。也就是说,与其他使用监督学习方法来优化预测错误(与已知正确答案相比)的网络不同,DRL 网络并不知道处理问题的正确方式。它们只是被给定系统规则,并且每当它们正确执行一个任务时,就会获得奖励。这个过程需要大量的迭代,最终训练网络在多个任务中表现出色。

如需更多信息,请参见,Volodymyr Mnih 等人的《通过深度强化学习实现人类级别控制》,2015 年 2 月,发表于《自然》杂志。可在以下地址获取:storage.googleapis.com/deepmind-media/dqn/DQNNaturePaper.pdf

深度强化学习(DRL)模型在 DeepMind 创建 AlphaGo 后获得了广泛关注,AlphaGo 是一个在围棋游戏中超越职业选手的系统。DeepMind 还创建了能够自学并以超人类水平玩视频游戏的 DRL 网络:

图 4:表示 DQN 算法如何工作的图像

如需更多信息,请参见,DQN 是 DeepMind 为了击败 Atari 游戏而创建的。该算法使用深度强化学习解决方案,不断提高其奖励。图片来源:keon.io/deep-q-learning/

架构 数据结构 成功应用

| 卷积神经网络(CNNs)

| 网格状拓扑结构(即,图像)

图像识别和分类
循环神经网络(RNN)和长短期记忆(LSTM)网络
生成对抗网络(GANs)
深度强化学习(DRL)

表 1:不同的神经网络架构在不同领域取得了成功。网络的架构通常与当前问题的结构相关。

数据归一化

在构建深度学习模型之前,还有一步是必须的:数据归一化。

数据归一化是机器学习系统中的常见做法。尤其在神经网络中,研究人员提出归一化是训练 RNN(和 LSTM)的一个关键技术,主要是因为它能减少网络的训练时间,并提高网络的整体性能。

欲了解更多信息,请参考 Sergey Ioffe 等人在 arXiv 上发布的《批量归一化:通过减少内部协变量偏移加速深度网络训练》,2015 年 3 月,网址:arxiv.org/abs/1502.03167

决定采用哪种归一化技术取决于数据和具体问题。以下是常用的几种技术。

Z-分数

当数据呈正态分布(即高斯分布)时,可以计算每个观测值与其均值之间的标准差距离。

当识别数据点与分布中更可能发生的事件之间的距离时,这种归一化非常有用。Z-分数的定义为:

这里, 的观测值, 是均值, 是该序列的标准差。

欲了解更多信息,请参考标准分数文章(Z-分数)。维基百科,网址:en.wikipedia.org/wiki/Standard_score

点相对归一化

这种归一化计算给定观测值与序列第一个观测值之间的差异。这种归一化有助于识别相对于起始点的趋势。点相对归一化的定义为:

这里, 的观测值, 是该序列的第一个观测值。

正如 Siraj Raval 在他的视频《如何轻松预测股票价格 深度学习入门》第 7 集中所建议的,视频可在 YouTube 上观看:www.youtube.com/watch?v=ftMq5ps503w

最大值和最小值归一化

这种归一化计算给定观测值与序列的最大值和最小值之间的距离。当处理的序列中的最大值和最小值不是异常值并且对未来预测有重要作用时,这种归一化非常有用。

这种归一化技术可以应用于:

这里, 的观测值,O 代表包含所有 O 值的向量,min (O) 和 max (O) 函数分别表示该序列的最小值和最大值。

在下一个活动中,我们将探索比特币数据集并为模型准备数据。我们将准备可用的比特币数据以供 LSTM 模型使用。这包括选择感兴趣的变量,选择相关的时间段,并应用前述的点相对归一化技术。

结构化你的问题

与研究人员相比,实践者在开始一个新的深度学习项目时,花在确定选择哪种架构上的时间要少得多。获取能够正确代表给定问题的数据是开发这些系统时需要考虑的最重要因素,其次是理解数据集固有的偏差和局限性。

在开始开发深度学习系统时,考虑以下反思问题:

  • 我有正确的数据吗? 这是训练深度学习模型时最难的挑战。首先,用数学规则定义你的问题。使用精确的定义,并将问题组织为类别(分类问题)或连续尺度(回归问题)。现在,如何收集关于这些度量的数据呢?

  • 我有足够的数据吗? 通常,深度学习算法在大数据集上的表现明显优于在小数据集上的表现。知道训练一个高性能算法需要多少数据,取决于你试图解决的问题类型,但尽量收集尽可能多的数据。

  • 我可以使用预训练模型吗? 如果你正在处理的问题是更一般应用的一个子集——但在同一领域内——考虑使用预训练模型。预训练模型可以帮助你在解决问题时,专注于问题的具体模式,而不是领域的更一般特征。一个好的起点是官方的 TensorFlow 仓库(github.com/tensorflow/models)。

图 5:深度学习项目开始时需要考虑的关键反思问题的决策树

在某些情况下,数据可能根本无法获得。根据具体情况,可以使用一系列技术有效地从输入数据中生成更多数据。这个过程称为数据增强,在处理图像识别问题时具有成功的应用。

一个很好的参考是文章《使用深度神经网络分类浮游生物》,可以在benanne.github.io/2015/03/17/plankton.html找到。作者展示了一系列技术,用于增强一小组图像数据,以增加模型的训练样本数量。

活动:探索比特币数据集并为模型准备数据

我们将使用一个公开的数据集,该数据集最初来自于 CoinMarketCap,一个追踪不同加密货币统计信息的流行网站。数据集已与本章一起提供,将会被使用。

我们将使用 Jupyter Notebooks 探索数据集。Jupyter Notebooks 提供通过网页浏览器访问的 Python 会话,使你可以互动地处理数据。它们是探索数据集的流行工具,在本书的活动中会被使用。

使用终端,导航到目录 Chapter_5/activity_3 并执行以下命令启动 Jupyter Notebook 实例:

     $ jupyter notebook 

现在,在浏览器中打开应用程序提供的网址。你应该能够看到一个 Jupyter Notebook 页面,页面上显示了文件系统中的多个目录。你应该能看到以下输出:

图 6:启动 Jupyter Notebook 实例后的终端图像。导航到浏览器中显示的网址,你应该能够看到 Jupyter Notebook 的登录页面。

现在,导航到目录并点击文件 Activity Exploring_Bitcoin_ Dataset.ipynb。这是一个 Jupyter Notebook 文件,它将会在新的浏览器标签中打开。应用程序将自动为你启动一个新的 Python 交互式会话。

图 7:你的 Jupyter Notebook 实例的登录页面

图 8:Notebook Activity_Exploring_Bitcoin_Dataset.ipynb 的图片。你现在可以与该 Notebook 进行交互并进行修改。

打开我们的 Jupyter Notebook 后,让我们现在探索本章提供的比特币数据。

数据集 data/bitcoin_historical_prices.csv 包含自 2013 年初以来的比特币价格数据。最新的观测数据为 2017 年 11 月——该数据集来自 CoinMarketCap,这是一个每天更新的在线服务。数据集包含八个变量,其中两个(日期和周数)描述了数据的时间周期——这些可以用作索引——另外六个(openhighlowclosevolumemarket_capitalization)则可以用来理解比特币价格和价值是如何随时间变化的:

变量 描述
date 观察的日期。
iso_week 给定年份的周数。
open 单个比特币的开盘值。
high 给定日期期间内的最高值。
low 给定日期期间内的最低值。
close 交易日结束时的值。
volume 当天交易的比特币总量。
market_capitalization 市值,计算公式为 市值 = 价格 * 流通供应量。

表 2:比特币历史价格数据集中可用的变量(即列)

使用打开的 Jupyter Notebook 实例,现在让我们探索这两个变量的时间序列:closevolume。我们将从这些时间序列开始,探索价格波动模式。

导航到已打开的 Jupyter Notebook 实例 Activity Exploring_Bitcoin_ Dataset.ipynb。现在,执行标题为 Introduction 下的所有单元格。这将导入所需的库并将数据集导入内存。

在数据集导入内存后,转到探索部分。你会看到一段生成 close 变量时间序列图的代码片段。你能为 volume 变量生成相同的图吗?

图 9:比特币收盘价的时间序列图,数据来自 close 变量。请在下方的新单元格中重现该图,但使用 volume 变量。

你一定已经注意到,2017 年这两个变量都有大幅上升。这反映了一个当前现象,即比特币的价格和价值自 2017 年初以来持续增长。

图 10:比特币收盘价(以美元计)。注意到 2013 年底和 2014 年初的早期价格飙升。同时,也可以注意到自 2017 年初以来,最近的价格已经飙升。

图 11:比特币交易量(以美元计)显示,从 2017 年开始,市场上的比特币交易量显著增加。与每日收盘价相比,总体交易量的波动性要大得多。

此外,我们还注意到,很多年前,比特币的价格波动不如近几年那样剧烈。虽然这些早期的周期可以被神经网络用来理解某些模式,但考虑到我们关注的是预测未来不远的价格,我们将排除较旧的观察数据。让我们只筛选 2016 年和 2017 年的数据。

导航到“准备数据集以供模型使用”部分。我们将使用 pandas API 筛选 2016 年和 2017 年的数据。Pandas 提供了一个直观的 API 来执行此操作:

     bitcoin_recent = bitcoin[bitcoin['date'] >= '2016-01-01']

变量 bitcoin_recent 现在包含了我们原始比特币数据集的一个副本,但仅包含 2016 年 1 月 1 日或之后的观测数据。

作为最后一步,我们现在使用 数据归一化 部分描述的点相对归一化技术来归一化我们的数据。我们只归一化两个变量(close 和 volume),因为这两个是我们要预测的变量。

在包含本章节的同一目录下,我们放置了一个名为 normalizations.py 的脚本。该脚本包含了本章节中描述的三种归一化技术。我们将该脚本导入到 Jupyter Notebook 中,并将函数应用于我们的系列。

导航到“准备数据集以供模型使用”部分。现在,使用 iso_week 变量按周对所有日期观察进行分组,使用 pandas 方法 groupby()。然后,我们可以直接对该周内的系列应用归一化函数 normalizations.point_relative_normalization()。我们将该归一化的输出存储为同一个 pandas 数据框中的新变量,方法如下:

     bitcoin_recent['close_point_relative_normalization'] =
     bitcoin_recent.groupby('iso_week')['close'].apply(
     lambda x: normalizations.point_relative_normalization(x))

现在,变量 close_point_relative_normalization 包含了 close 变量的归一化数据。请对 volume 变量执行相同的操作:

图 12:Jupyter Notebook 的图像,聚焦于应用标准化函数的部分。

标准化的收盘变量包含每周有趣的方差模式。我们将使用该变量来训练我们的 LSTM 模型。

图 13:显示来自标准化变量 close_point_relative_normalization 的系列图。

为了评估我们的模型表现如何,我们需要将其准确性与其他数据进行对比。我们通过创建两个数据集来实现这一点:一个训练集和一个测试集。在本次活动中,我们将使用 80%的数据集来训练我们的 LSTM 模型,剩下的 20%用于评估其表现。

鉴于数据是连续的,并且以时间序列的形式存在,我们使用最后 20%的可用周作为测试集,前 80%作为训练集:

图 14:使用周数创建训练集和测试集

最后,导航到“存储输出”部分,并将过滤后的变量保存到磁盘,如下所示:

     test_dataset.to_csv('data/test_dataset.csv', index=False)
     train_dataset.to_csv('data/train_dataset.csv', index=False)
     bitcoin_recent.to_csv('data/bitcoin_recent.csv', index=False)

在本节中,我们探索了比特币数据集,并为深度学习模型做好了准备。

我们了解到,在 2017 年,比特币的价格暴涨。这一现象需要较长时间才能发生——并且可能受到许多外部因素的影响,这些因素是仅凭这些数据无法解释的(例如,其他加密货币的出现)。我们还使用了点相对标准化技术来处理比特币数据集,并按周进行划分。我们这样做是为了训练 LSTM 网络学习比特币价格变化的每周模式,以便它可以预测未来一整周的价格。然而,比特币的统计数据显示其每周波动很大。我们能预测比特币未来的价格吗?

那么,七天后的价格将是多少?我们将在下一节中使用 Keras 构建一个深度学习模型来探索这个问题。

使用 Keras 作为 TensorFlow 接口

本节重点介绍 Keras。我们使用 Keras 是因为它将 TensorFlow 的接口简化为通用抽象。在后台,计算仍然是在 TensorFlow 中进行的——图形仍然是使用 TensorFlow 组件构建的——但接口要简单得多。我们减少了对单独组件(如变量和操作)的关注,更多地关注构建网络作为一个计算单元。Keras 使得实验不同的架构和超参数变得更加容易,从而更快地朝着高效的解决方案迈进。

从 TensorFlow 1.4.0(2017 年 11 月)开始,Keras 现在正式与 TensorFlow 一起分发为tf.keras。这表明 Keras 现在与 TensorFlow 紧密集成,且它很可能会继续作为开源工具开发很长一段时间。

模型组件

正如我们在《神经网络与深度学习简介》第四章中所看到的,LSTM 网络也有输入、隐藏和输出层。每个隐藏层都有一个激活函数,用于评估该层的相关权重和偏差。如预期的那样,网络按顺序从一层传递数据到另一层,并通过每次迭代的输出来评估结果(即一个 epoch)。

Keras 提供直观的类来表示每一个这些组件:

组件 Keras 类
完整顺序神经网络的高级抽象。 keras.models.Sequential()
密集连接层。 keras.layers.core.Dense()
激活函数。 keras.layers.core.Activation()
LSTM 循环神经网络。这个类包含了专属于这个架构的组件,其中大部分被 Keras 抽象化。 keras.layers.recurrent.LSTM()

表 3:Keras API 中关键组件的描述。我们将使用这些组件来构建深度学习模型。

Keras 的keras.models.Sequential()组件代表了一个完整的顺序神经网络。该 Python 类可以独立实例化,然后随后添加其他组件。

我们对构建 LSTM 网络感兴趣,因为这些网络在处理顺序数据时表现良好——而时间序列是顺序数据的一种。使用 Keras,完整的 LSTM 网络实现如下所示:

     from keras.models import Sequential
     from keras.layers.recurrent import LSTM
     from keras.layers.core import Dense, Activation

     model = Sequential()

     model.add(LSTM(
     units=number_of_periods,
     input_shape=(period_length, number_of_periods)
     return_sequences=False), stateful=True)
     model.add(Dense(units=period_length))
     model.add(Activation("linear"))
     model.compile(loss="mse", optimizer="rmsprop")

Snippet 1:使用 Keras 的 LSTM 实现

这个实现将在《模型评估与优化》第六章中进一步优化。

Keras 的抽象化允许专注于使深度学习系统更高性能的关键元素:正确的组件序列是什么,包括多少层和节点,以及使用哪种激活函数。所有这些选择都由将组件添加到实例化的keras.models.Sequential()类的顺序或通过传递给每个组件实例化的参数(即Activation("linear"))确定。最终的.compile()步骤使用 TensorFlow 组件构建神经网络。

构建网络后,我们使用model.fit()方法来训练我们的网络。这将产生一个经过训练的模型,可以用来进行预测:

     model.fit(
     X_train, Y_train,
     batch_size=32, epochs=epochs)

Snippet 2.1:使用model.fit()的示例

变量X_trainY_train分别用于训练的一组数据和用于评估损失函数的较小数据集(即测试网络预测数据的效果)。

最后,我们可以使用model.predict()方法进行预测:

      model.predict(x=X_train)

Snippet 2.2:使用model.predict()的示例

前面的步骤涵盖了 Keras 在处理神经网络时的范式。尽管不同的架构可以以非常不同的方式处理,但 Keras 通过使用三个组件——网络架构、拟合和预测,简化了处理不同架构的接口:

图 15:Keras 神经网络范式:A. 设计神经网络架构,B. 训练神经网络(或拟合),C. 做出预测

Keras 在每个步骤中都允许更大的控制。然而,它的重点是尽可能简单地帮助用户在最短的时间内创建神经网络。这意味着我们可以从一个简单的模型开始,然后在上述每个步骤中添加复杂性,使初始模型的性能更好。

我们将在接下来的活动和章节中利用这个范式。在下一个活动中,我们将创建最简单的 LSTM 网络。然后,在 第六章模型评估与优化,我们将不断评估并修改该网络,使其更加强大和高效。

活动:使用 Keras 创建 TensorFlow 模型

在本活动中,我们将使用 Keras 创建一个 LSTM 模型。

Keras 作为一个接口连接低层次的程序;在这个例子中是 TensorFlow。当我们使用 Keras 设计神经网络时,该神经网络会被 编译 为一个 TensorFlow 计算图。

导航到打开的 Jupyter Notebook 实例 Activity_4_Creating_a_ TensorFlow_Model_Using_Keras.ipynb。现在,执行 构建模型 下的所有单元格。在该部分,我们构建了第一个 LSTM 模型,设置了两个参数:训练观察值的输入大小(对于单个日期等同为 1)和预测期的输出大小——在我们的案例中是七天:

使用 Jupyter Notebook Activity_4_Creating_a_TensorFlow_Model_Using_Keras.ipynb 来构建与 模型组件 部分相同的模型,设置输入和输出的周期长度,以便进行实验。

在模型编译完成后,我们继续将其存储为磁盘上的 h5 文件。定期将模型版本存储到磁盘上是一个好习惯,这样你就可以将模型架构与其预测能力一起保存在硬盘上。

仍然在同一个 Jupyter Notebook 中,导航到 保存模型 头部。在该部分,我们将使用以下命令将模型存储为磁盘上的文件:

     model.save('bitcoin_lstm_v0.h5')

模型 'bitcoin_lstm_v0.h5' 还没有经过训练。当在没有先前训练的情况下保存模型时,实际上只保存了模型的架构。该模型稍后可以通过 Keras 的 load_model() 函数加载,如下所示:

     1 model = keras.models.load_model('bitcoin_lstm_v0.h5')

当加载 Keras 库时,您可能会遇到以下警告:使用 TensorFlow 后端。Keras 可以配置为使用其他后端而不是 TensorFlow(即 Theano)。为了避免此消息,您可以创建一个名为keras.json的文件,并在其中配置其后端。该文件的正确配置取决于您的系统。因此,建议您访问 Keras 官方文档,了解相关主题:keras.io/backend/.

在本节中,我们学习了如何使用 Keras(TensorFlow 的接口)构建深度学习模型。我们研究了 Keras 的核心组件,并使用这些组件基于 LSTM 模型构建了第一个比特币价格预测系统的版本。

在我们接下来的章节中,我们将讨论如何将本章中的所有组件整合到一个(几乎完整的)深度学习系统中。该系统将产生我们第一次的预测,作为未来改进的起点。

从数据准备到建模

本节专注于深度学习系统的实现方面。我们将使用在选择正确的模型架构一节中的比特币数据,以及在使用 Keras 作为 TensorFlow 接口一节中的 Keras 知识,将这两个组件结合起来。本节通过构建一个系统来结束本章,该系统从磁盘读取数据,并将其作为一个整体输入模型。

训练神经网络

神经网络的训练可能需要较长时间。许多因素会影响该过程需要的时间。其中,有三个因素通常被认为是最重要的:

  • 网络架构

  • 网络有多少层和神经元

  • 用于训练过程的数据量

其他因素也可能大大影响网络的训练时间,但神经网络在解决业务问题时的优化大多数来源于探索这三点。

我们将使用上一节中的归一化数据。回想一下,我们已经将训练数据存储在一个名为train_dataset.csv的文件中。我们将使用 pandas 将该数据集加载到内存中,以便进行简便的探索:

    import pandas as pd
    train = pd.read_csv('data/train_dataset.csv')

图 17:显示从train_dataset.csv文件加载的训练数据集的前五行的表格

我们将使用来自变量close_point_relative_normalization的系列数据,这是一组归一化的比特币收盘价序列(来自变量 close),自 2016 年初以来的数据。

变量close_point_relative_normalization是基于每周归一化的。每个观测值都相对于该周期间第一天的收盘价差异进行归一化。这个归一化步骤很重要,它将帮助我们的网络更快地训练。

图 18:展示从归一化变量 close_point_relative_normalization 中绘制的时间序列图。这个变量将用于训练我们的 LSTM 模型。

调整时间序列数据

神经网络通常处理向量和张量,这些都是组织数据的数学对象,它们在多个维度上组织数据。在 Keras 中实现的每个神经网络都会根据规格组织一个向量或张量作为输入。一开始,理解如何将数据调整为给定层所期望的格式可能会令人困惑。为了避免混淆,建议从一个尽可能简单的网络开始,然后逐步添加组件。Keras 的官方文档(在部分)对学习每种层的要求非常重要。

Keras 官方文档可以在 keras.io/layers/core/ 获取。该链接直接将您带到 Layers 部分。

NumPy 是一个流行的 Python 库,用于执行数值计算。深度学习社区使用它来操作向量和张量,并将它们为深度学习系统做准备。特别是,numpy.reshape() 方法在调整数据以适应深度学习模型时非常重要。该方法允许操作 NumPy 数组,这些数组是 Python 对象,类似于向量和张量。

我们现在使用 2016 和 2017 年的周数据来组织来自 close_point_relative_normalization 变量的价格。我们将数据分成不同的组,每组包含七个观察值(每个周的一天),总共有 77 个完整的周。

我们这样做是因为我们对预测一周的交易价格感兴趣。

我们使用 ISO 标准来确定一周的开始和结束时间。其他类型的组织方式也是完全可能的。这种方式简单且直观,但仍有改进的空间。

LSTM 网络使用三维张量。每个维度都代表网络的一个重要属性。这些维度是:

  • 周期长度:周期长度,即每个周期中有多少观察值

  • 周期数量:数据集中可用的周期数量

  • 特征数量:数据集中可用的特征数量

我们从变量 close_point_relative_normalization 获取的数据目前是一个一维向量——我们需要将其调整为符合这三个维度的格式。

我们将使用一周的时间周期。因此,我们的周期长度是七天(周期长度 = 7)。我们的数据中有 77 个完整的周。我们将在训练期间使用最后一个周进行模型测试。这使得我们剩下 76 个不同的周(周期数量 = 76)。最后,我们将在这个网络中使用单一的特征(特征数量 = 1)——我们将在未来的版本中加入更多的特征。

我们如何重塑数据以匹配这些维度?我们将使用基础 Python 属性和来自 numpy 库的 reshape() 函数的组合。首先,我们用纯 Python 创建 76 个不同的周组,每个周组有七天:

     group_size = 7
     samples = list()
     for i in range(0, len(data), group_size):
     sample = list(data[i:i + group_size])
     if len(sample) == group_size:
     samples.append(np.array(sample).reshape(group_size, 1).tolist())

     data = np.array(samples) 

代码片段 3:创建不同周组的 Python 代码片段

结果变量数据是一个包含所有正确维度的变量。Keras 的 LSTM 层期望这些维度按特定顺序组织:特征数量、观测数量和周期长度。

让我们重塑数据集以匹配该格式:

     X_train = data[:-1,:].reshape(1, 76, 7)
     Y_validation = data[-1].reshape(1, 7)

代码片段 4:创建不同周组的 Python 代码片段

每个 Keras 层都会期望其输入以特定的方式组织。然而,Keras 通常会根据需要重新塑形数据。每次添加新层或遇到维度问题时,都应参考 Keras 层的文档 (keras.io/layers/core/)。

代码片段 4 还将我们数据集中的最后一周选择为验证集(via data[-1])。我们将尝试使用前 76 周的数据预测数据集中的最后一周。接下来的步骤是使用这些变量来拟合我们的模型:

      model.fit(x=X_train, y=Y_validation, epochs=100) 

代码片段 5:展示如何训练我们的模型

LSTM 是计算开销较大的模型。在现代计算机上,训练我们的数据集可能需要几分钟时间。大部分时间都花费在计算的开始阶段,当时算法创建完整的计算图。训练开始后,速度会逐渐提升:

图 19:显示在每个 epoch 评估的损失函数结果的图形

这比较了模型在每个 epoch 中的预测结果,然后使用均方误差技术与实际数据进行比较。此图显示了这些结果。

一目了然,我们的网络表现得非常好:它从一个非常小的误差率开始,并持续减少。那么,我们的预测结果告诉我们什么呢?

进行预测

在我们的网络训练完成后,我们现在可以进行预测。我们将对超出时间范围的未来一周进行预测。

一旦我们通过 model.fit() 训练了我们的模型,做出预测就变得非常简单:

     model.predict(x=X_train)

代码片段 6:使用之前用于训练的相同数据进行预测

我们使用与训练数据相同的数据进行预测(即 X_train 变量)。如果我们有更多数据可用,我们可以使用这些数据,只要我们将其重新塑形为 LSTM 所要求的格式。

过拟合

当神经网络在验证集上发生过拟合时,意味着它学会了训练集中存在的模式,但无法将其推广到未见过的数据(例如,测试集)。在下一章中,我们将学习如何避免过拟合,并创建一个系统来评估我们的网络并提升其性能:

图 20:去归一化后,我们的 LSTM 模型预测 2017 年 7 月底,比特币的价格将从 2200 美元上涨到约 2800 美元,单周上涨 30%

活动:组装深度学习系统

在本活动中,我们将所有构建基础深度学习系统的关键元素汇集在一起:数据、模型和预测。

我们将继续使用 Jupyter Notebooks,并将使用之前练习中准备的数据(data/train_dataset.csv)以及我们本地存储的模型(bitcoin_lstm_v0.h5)。

  1. 启动 Jupyter Notebook 实例后,导航到名为Activity_5_Assembling_a_Deep_Learning_System.ipynb的 Notebook 并打开它。从标题开始执行单元格以加载所需的组件,然后导航到标题数据整形

图 21:展示归一化变量close_point_relative_normalization的时间序列图

close_point_relative_normalization变量将用于训练我们的 LSTM 模型。

我们将从加载我们在之前活动中准备的数据集开始。我们使用 pandas 将该数据集加载到内存中。

  1. 使用 pandas 将训练数据集加载到内存中,如下所示:
      train = pd.read_csv('data/train_dataset.csv')
  1. 现在,通过执行以下命令,快速检查数据集:
      train.head()

正如本章所解释的,LSTM 网络需要三维张量。这些维度是:周期长度、周期数和特征数。

现在,继续创建每周的分组,然后重新排列生成的数组以匹配这些维度。

  1. 随时使用提供的create_groups()函数来执行此操作:
       create_groups(data=train, group_size=7)

该函数的默认值为 7 天。如果你将该数字更改为其他值,比如 10,会发生什么呢?

现在,确保将数据分成两个集合:训练集和验证集。我们通过将比特币价格数据集的最后一周分配到评估集来实现这一点。然后,我们训练网络来评估这一最后一周的数据。

将训练数据的最后一周分离出来,并使用numpy.reshape()进行重塑。重塑非常重要,因为 LSTM 模型只接受这种格式的数据:

       X_train = data[:-1,:].reshape(1, 76, 7)
       Y_validation = data[-1].reshape(1, 7)

我们的数据现在已经准备好用于训练。现在我们加载之前保存的模型,并用给定的 epochs 数量训练它。

  1. 导航到标题加载我们的模型,并加载我们之前训练的模型:
      model = load_model('bitcoin_lstm_v0.h5')
  1. 现在,用我们的训练数据X_trainY_validation来训练该模型:
      history = model.fit(
      x=X_train, y=Y_validation,
      batch_size=32, epochs=100)

请注意,我们将模型的日志存储在名为 history 的变量中。这些日志对于探索模型训练准确率的具体变化,以及理解损失函数的表现非常有用:

图 22:Jupyter Notebook 部分,我们加载了之前的模型,并用新数据训练它

最后,让我们用训练好的模型进行预测。

  1. 使用相同的data X_train,调用以下方法:
      model.predict(x=X_train)
  1. 模型立即返回一个标准化值的列表,包含未来七天的预测数据。使用 denormalize() 函数将数据转化为美元值。请使用最新的可用值作为参考来调整预测结果:
       denormalized_prediction = denormalize(predictions, last_weeks_value)

图 23:Jupyter Notebook 部分,展示了我们预测未来七天比特币价格的过程。

我们的预测表明,比特币价格可能会大约上涨 30%。

图 24:使用我们刚刚建立的 LSTM 模型预测未来七天的比特币价格走势。

我们在这张图中结合了两个时间序列:真实数据(线之前)和预测数据(线之后)。该模型展示的方差与之前看到的模式类似,并且它暗示未来七天内可能会有价格上涨。

  1. 完成实验后,使用以下命令保存你的模型:
      model.save('bitcoin_lstm_v0_trained.h5')

我们将保存这个训练好的网络以供将来参考,并与其他模型的表现进行比较。

网络可能已经从我们的数据中学习到了一些模式,但它是如何在如此简单的架构和如此少的数据下做到这一点的呢?LSTM 是一种从数据中学习模式的强大工具。然而,我们将在接下来的课程中学习到,它们也可能会遭遇 过拟合 问题,这是神经网络中常见的现象,其中模型学习到了训练数据中的一些模式,但这些模式在预测现实世界的数据时并没有什么用处。我们将学习如何处理这一问题,并改进我们的网络以进行有用的预测。

总结

在本章中,我们已经组装了一个完整的深度学习系统:从数据到预测。此活动中创建的模型需要进行多次改进才能算得上有用。然而,它为我们提供了一个很好的起点,之后我们将不断改进。

我们的下一章将探索评估模型性能的技术,并继续进行修改,直到我们得到一个既有用又健壮的模型。

第六章:模型评估与优化

本章重点讨论如何评估神经网络模型。与其他类型的模型不同,使用神经网络时,我们会调整网络的超参数以提高其性能。然而,在修改任何参数之前,我们需要先衡量模型的表现。

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

  • 评估模型

    • 探索神经网络处理的不同类型问题

    • 探索损失函数、准确率和错误率

    • 使用 TensorBoard

    • 评估指标和技术

  • 超参数优化

    • 添加层和节点

    • 探索并添加训练轮次

    • 实现激活函数

    • 使用正则化策略

模型评估

在机器学习中,常常定义两个不同的术语:参数和超参数。参数是影响模型如何从数据中做出预测的属性。超参数则是指模型如何从数据中学习。参数可以从数据中学习并动态修改,而超参数则是更高级的属性,通常不会从数据中学习。如需更详细的概述,请参考 Sebastian Raschka 和 Vahid Mirjalili 所著的《Python 机器学习》(Packt,2017 年)。

问题分类

通常,神经网络解决的问题分为两类:分类和回归。分类问题是关于从数据中预测正确的类别;例如,温度是热还是冷。回归问题则是关于预测连续标量中的值;例如,实际的温度值是多少?

这两类问题的特点如下:

  • 分类:以类别为特征的问题。类别可以不同,也可以相同;它们还可以是二分类问题。然而,每个数据元素必须清晰地被分配到某个类别。一个分类问题的例子是,使用卷积神经网络为图像分配标签 非车。在第四章《神经网络与深度学习简介》中探索的 MNIST 示例是另一个分类问题的例子。

  • 回归:以连续变量(即标量)为特征的问题。这些问题通过范围来衡量,并评估网络与真实值的接近程度。例如,一个时间序列分类问题,其中使用循环神经网络预测未来的温度值。比特币价格预测问题是另一个回归问题的例子。

尽管评估这两类问题模型的整体结构相同,但我们会采用不同的技术来评估模型的表现。在接下来的部分中,我们将探讨分类或回归问题的评估技术。

本章中的所有代码片段都实现于活动 6 和 7。欢迎跟着一起做,但不必感到强制性,因为它们将在活动中更详细地重复。

损失函数、准确度和误差率

神经网络利用衡量网络与验证集(即从数据中分离出来作为训练过程一部分的部分数据)比较时的表现的函数。这些函数称为损失函数

损失函数评估神经网络预测的错误程度;然后它们会将这些错误反向传播并调整网络,改变单个神经元的激活方式。损失函数是神经网络的关键组件,选择合适的损失函数对网络性能有着重要影响。

错误是如何传播到网络中的每个神经元的?

错误通过一个称为反向传播的过程进行传播。反向传播是一种将损失函数返回的错误传播到神经网络中每个神经元的技术。传播的错误会影响神经元的激活方式,最终影响该网络的输出。

包括 Keras 在内的许多神经网络包默认使用此技术。

如需了解更多关于反向传播的数学内容,请参考深度学习(作者:Ian Goodfellow 等,麻省理工学院出版社,2016 年)。

我们对回归和分类问题使用不同的损失函数。对于分类问题,我们使用准确率函数(即预测正确的比例)。而对于回归问题,我们使用误差率(即预测值与观测值的接近程度)。

下表提供了常见损失函数的总结,并列出了它们的常见应用:

问题类型 损失函数 问题 示例

| 回归 | 均方误差(MSE)

| 预测一个连续函数。即在一系列值的范围内预测值。

| 使用过去的温度测量预测未来的温度。

|

回归 均方根误差(RMSE) 与前面相同,但处理负值。RMSE 通常提供更具可解释性的结果。 与前面相同。

| 回归 | 平均绝对百分比误差

(MAPE)

| 预测连续函数。与去归一化范围工作时,表现更好。 | 使用产品属性(例如,价格、类型、目标受众、市场条件)预测产品的销售。

|

| 分类 | 二元交叉熵 | 对两个类别或两者之间的分类

值(即,truefalse)。

| 根据网站访问者的浏览器活动预测其性别。

|

| 分类 | 分类交叉熵

| 从已知类别集中分类多类问题

分类的类别数。

根据说话者的口音预测其国籍,条件是说出一段英语句子。

对于回归问题,MSE 函数是最常用的选择。而对于分类问题,二元交叉熵(用于二分类问题)和多类别交叉熵(用于多分类问题)是常见的选择。建议从这些损失函数开始,然后在神经网络的发展过程中,尝试其他函数,以提升性能。

对于回归问题,MSE 函数是最常用的选择。而对于分类问题,二元交叉熵(用于二分类问题)和多类别交叉熵(用于多分类问题)是常见的选择。建议从这些损失函数开始,然后在神经网络的发展过程中,尝试其他函数,以提升性能。

我们在第五章中开发的网络使用 MSE 作为其损失函数。在接下来的章节中,我们将探讨该函数在网络训练过程中的表现。

不同的损失函数,相同的架构

在继续进入下一节之前,让我们从实践角度探讨一下,这些问题在神经网络中的差异。

TensorFlow Playground 应用程序由 TensorFlow 团队提供,帮助我们理解神经网络是如何工作的。在这里,我们看到一个神经网络,其中的层包括:输入层(左侧)、隐藏层(中间)和输出层(右侧)。

我们还可以选择不同的样本数据集进行实验,位于最左侧。最后,在最右侧,我们看到网络的输出。

图 1:TensorFlow Playground 网页应用程序。在这个可视化中,使用神经网络的参数来获取一些

直观地了解每个参数如何影响模型结果。

该应用程序帮助我们探索在前一节中讨论的不同问题类别。当我们选择分类作为问题类型(右上角)时,数据集中的点只有两种颜色值:蓝色或橙色。

当我们选择回归时,点的颜色会在橙色和蓝色之间的色值范围内变化。在处理分类问题时,网络根据错误的蓝色和橙色数量来评估其损失函数;在处理分类问题时,它会检查每个点距离正确色值的远近,如下图所示:

图 2:TensorFlow Playground 应用程序的细节。不同的色值分配给点,

这取决于问题类型。

点击播放按钮后,我们会注意到训练损失区域的数字随着网络不断训练而持续下降。每个问题类别中的数字非常相似,因为损失函数在两个神经网络中扮演着相同的角色。然而,每个类别所使用的实际损失函数是不同的,且根据问题类型选择。

使用 TensorBoard

评估神经网络是 TensorBoard 的强项。如同在Chapter 4*《神经网络与深度学习简介》中所解释的,TensorBoard 是随 TensorFlow 一起提供的一套可视化工具。其中之一的功能是,在每个 epoch 后,可以探索损失函数评估结果。TensorBoard 的一个伟大特点是,用户可以单独组织每次运行的结果,并比较每次运行的损失函数指标。之后,用户可以决定需要调整哪些超参数,并对网络的表现有一个大致的了解。最棒的是,这一切都可以实时完成。

为了在我们的模型中使用 TensorBoard,我们将使用 Keras 回调函数。我们通过导入 TensorBoard 回调函数,并在调用 fit() 函数时将其传递给模型。以下代码展示了如何在我们之前章节中创建的比特币模型中实现:

    from keras.callbacks import TensorBoard
    model_name = 'bitcoin_lstm_v0_run_0'
    tensorboard = TensorBoard(log_dir='./logs/{}'.format(model_name))
    model.fit(x=X_train, y=Y_validate,
    batch_size=1, epochs=100,
    verbose=0, callbacks=[tensorboard])

代码片段 1:在我们的 LSTM 模型中实现 TensorBoard 回调函数的代码片段

Keras 回调函数在每个 epoch 运行结束时被调用。在这种情况下,Keras 调用 TensorBoard 回调函数,将每次运行的结果存储到磁盘中。还有许多其他有用的回调函数可以使用,用户也可以使用 Keras API 创建自定义回调函数。

更多信息请参考 Keras 回调文档(keras.io/callbacks/)。

在实现 TensorBoard 回调函数后,loss 函数的指标现在可以在 TensorBoard 界面中查看。你可以运行 TensorBoard 进程(with tensorboard --logdir=./logs)并在训练网络时保持它运行(使用 fit())。评估的主要图形通常称为损失。用户可以通过将已知指标传递给 fit() 函数中的 metrics 参数来添加更多指标,这些指标将在 TensorBoard 中进行可视化,但不会用于调整网络权重。

交互式图形将继续实时更新,帮助你理解每个 epoch 中发生的情况。

图 3:TensorBoard 实例的截图,显示了损失函数结果以及添加到指标参数中的其他指标

实现模型评估指标

在回归和分类问题中,我们将输入数据集拆分为三个数据集:训练集、验证集和测试集。训练集和验证集用于训练网络。训练集作为输入提供给网络,验证集由损失函数用来将神经网络的输出与真实数据进行比较,并计算预测的误差。最后,测试集在网络训练完毕后用于评估网络在未见过的数据上的表现。

并没有明确的规则来确定训练集、验证集和测试集应如何划分。常见的方法是将原始数据集划分为 80% 的训练集和 20% 的测试集,然后将训练集进一步划分为 80% 的训练集和 20% 的验证集。有关此问题的更多信息,请参考 Sebastian Raschka 和 Vahid Mirjalili 合著的《Python 机器学习》(Packt,2017)。

在分类问题中,您将数据和标签作为相关但不同的数据传递给神经网络。网络随后学习数据如何与每个标签相关。在回归问题中,您不传递数据和标签,而是将感兴趣的变量作为一个参数传递,将用于学习模式的变量作为另一个参数。Keras 为这两种用例提供了接口,即 fit() 方法。请参见 代码片段 2 了解示例:

    model.fit(x=X_train, y=Y_train,
    batch_size=1, epochs=100,
    verbose=0, callbacks=[tensorboard],
    validation_split=0.1,
    validation_data=(X_validation, Y_validation))

代码片段 2:演示如何使用 validation_splitvalidation_data 参数的代码片段

fit() 方法可以使用 validation_splitvalidation_data 参数,但不能同时使用这两个参数。

损失函数评估模型的进展并在每次运行时调整其权重。然而,损失函数仅描述训练数据和验证数据之间的关系。为了评估模型是否正确执行,我们通常使用第三组数据——即未用于训练网络的数据——并将我们模型的预测结果与该数据集中的实际值进行比较。

这就是测试集的作用。Keras 提供了 model.evaluate() 方法,使得将训练好的神经网络与测试集进行评估的过程变得简单。请参见以下代码了解示例:

     model.evaluate(x=X_test, y=Y_test)

代码片段 3:演示如何使用 evaluate() 方法的代码片段

evaluate() 方法返回损失函数的结果以及传递给 metrics 参数的函数结果。在比特币问题中,我们将频繁使用该方法来测试模型在测试集上的表现。

你会注意到,比特币模型看起来与上述示例有些不同。这是因为我们使用了 LSTM 架构。LSTM 被设计用来预测序列。因此,我们不使用一组变量来预测另一个单一变量——即使它是回归问题。相反,我们使用单一变量(或一组变量)的先前观察值来预测该变量(或变量组)未来的观察值。在Keras.fit()y参数包含了与x参数相同的变量,只不过是预测的序列。

评估比特币模型

我们在Chapter 4中创建了一个测试集,神经网络与深度学习简介*。该测试集包含了 19 周的比特币每日价格观察数据,相当于原始数据集的约 20%。

我们还使用数据集的其他 80%(即包含 56 周数据的训练集,减去一个用于验证集的数据)在Chapter 5中训练了神经网络,模型架构*,并将训练好的网络存储在磁盘上(bitcoin_lstm_v0)。现在,我们可以在测试集的每一周(共 19 周)使用evaluate()方法,查看该神经网络的表现。

然而,要做到这一点,我们必须提供前 76 周的数据。这是因为我们的网络被训练为通过连续的 76 周数据预测一个周的数据(我们将在Chapter 7中重新训练我们的网络,以使用更长时间的数据周期,并在产品化*章节中讨论将神经网络部署为 Web 应用程序时处理此行为):

    combined_set = np.concatenate((train_data, test_data), axis=1)
        evaluated_weeks = []
        for i in range(0, validation_data.shape[1]):
        input_series = combined_set[0:,i:i+77]

       X_test = input_series[0:,:-1].reshape(1, input_series.shape[1] - 1,)
       Y_test = input_series[0:,-1:][0]

       result = B.model.evaluate(x=X_test, y=Y_test, verbose=0)
       evaluated_weeks.append(result)

Snippet 4:实现evaluate()方法以评估我们模型在测试数据集上表现的代码片段

在前面的代码中,我们使用 Keras 的model.evaluate()评估每一周的数据,然后将输出存储在变量evaluated_weeks中。接着,我们将每周的 MSE 结果绘制在下图中:

图 4:测试集每周的 MSE;注意,在第 5 周,模型预测的结果比其他任何一周都要差。

我们模型的 MSE 结果表明,大多数周我们的模型表现良好,除了第 5 周,其 MSE 值上升至约0.08。除了第 5 周,我们的模型似乎在几乎所有其他测试周的表现都很好。

过拟合

我们的第一个训练过的网络(bitcoin_lstm_v0)可能正遭受一种叫做过拟合的现象。过拟合是指模型在训练时优化验证集,但这样做牺牲了从我们感兴趣的现象中提取更具普适性的模式。过拟合的主要问题是,模型学会了如何预测验证集数据,但无法预测新的数据。

我们模型中使用的损失函数在训练结束时达到了非常低的水平(约为 2.9 * 10^-6)。不仅如此,这个过程发生得很早:用于预测数据中最后一周的 MSE 损失函数在大约第 30 个训练周期时降低到了一个稳定的水平。这意味着我们的模型几乎完美地预测了第 77 周的数据,使用了前 76 周的数据。难道这可能是过拟合的结果吗?

我们再看一下图 4。我们知道我们的 LSTM 模型在验证集上达到了极低的值(约为 2.9 * 10^-6),但它在测试集上也达到了较低的值。然而,关键的区别在于尺度。我们测试集中每一周的 MSE 大约比验证集高出 4,000 倍(平均而言)。这意味着模型在我们的测试数据上的表现比在验证集上要差得多。这一点值得关注。

然而,尺度掩盖了我们 LSTM 模型的能力:即使在测试集上的表现要差得多,预测的 MSE 误差仍然非常非常低。这表明我们的模型可能正在从数据中学习到模式。

模型预测

一方面是通过比较 MSE 误差来衡量我们的模型,另一方面是能够直观地解释其结果。

使用相同的模型,接下来我们将使用 76 周的数据作为输入,为接下来的几周生成一系列预测。我们通过将 76 周的滑动窗口应用到完整的数据序列上(即训练集加测试集),并为每个窗口做出预测来实现。预测是通过 Keras 的model.predict()方法完成的:

    combined_set = np.concatenate((train_data, test_data), axis=1)

        predicted_weeks = []
        for i in range(0, validation_data.shape[1] + 1):
        input_series = combined_set[0:,i:i+76]
        predicted_weeks.append(B.predict(input_series))

代码片段 5:使用model.predict()方法为测试数据集中的所有周做出预测的代码片段

在前面的代码中,我们使用model.predict()进行预测,然后将这些预测存储在predicted_weeks变量中。接着我们绘制了结果预测图,得到了以下图形:

图 5:测试集中每一周的 MSE。请注意,在第 5 周,模型的预测比其他任何一周都要差。

我们模型的结果(如图 5所示)表明,它的表现并没有那么糟糕。通过观察预测线的模式,可以发现网络已经识别出一个每周波动的模式,其中标准化的价格在周中会上升,然后在周末下降。除了几个星期——最显著的是第 5 周,与我们之前的 MSE 分析相同——大多数周的数据都接近正确值。

现在让我们去归一化预测结果,以便使用与原始数据相同的尺度(即美元)来调查预测值。我们可以通过实现一个去归一化函数来做到这一点,该函数利用预测数据中的日期索引来识别测试数据中相应的一周。确定该周后,函数会取该周的第一个值,并使用该值通过倒置的点相对归一化技术去归一化预测值:

    def denormalize(reference, series,

    normalized_variable='close_point_relative_normalization',
    denormalized_variable='close'):
    week_values = observed[reference['iso_week']==series['iso_week'].
    values[0]]
    last_value = week_values[denormalized_variable].values[0]
    series[denormalized_variable] = 
    last_value*(series[normalized_variable]+1)

    return series

    predicted_close = predicted.groupby('iso_week').apply
    (lambda x: denormalize(observed, x))

代码片段 6:使用倒置的点相对归一化技术对数据进行去归一化处理。denormalize()函数取自测试集第一天的第一个收盘价,作为与之对应的那一周的数据。

我们的结果现在通过美元与测试集进行对比。如图 5 所示,bitcoin_lstm_v0 模型在预测未来七天比特币价格方面表现得相当不错。但是,我们如何用易于理解的方式来衡量这个表现呢?

图 6:测试集中每周的均方误差(MSE);注意到在第 5 周,模型预测的结果比其他任何一周都要差。

解释预测

我们的最后一步是为我们的预测增加可解释性。图 6 显示我们的模型预测与测试数据相对接近,但到底有多接近呢?

Keras 的 model.evaluate() 函数对于理解模型在每次评估步骤中的表现非常有用。然而,鉴于我们通常使用归一化数据集来训练神经网络,model.evaluate() 方法生成的指标也很难解释。

为了解决这个问题,我们可以收集模型的完整预测集,并使用 表 1 中的另外两个更容易解释的函数将其与测试集进行比较:分别是 mape()rmse(),它们分别表示 MAPE 和 RMSE:

    def mape(A, B):
    return np.mean(np.abs((A - B) / A)) * 100

    def rmse(A, B):
    return np.sqrt(np.square(np.subtract(A, B)).mean())

代码片段 7mape()rmse() 函数的实现

这些函数是使用 NumPy 实现的。原始实现来自 stats.stackexchange.com/questions/58391/mean-absolute-percentage-error-mape-in-scikit-learn(MAPE)和 stackoverflow.com/questions/16774849/mean-squared-error-in-numpy(RMSE)。

在使用这两个函数将我们的测试集与预测结果进行比较后,我们得到了以下结果:

  • 去归一化后的 RMSE:$399.6

  • 去归一化后的 MAPE:8.4%

这意味着我们的预测与真实数据的差异平均约为 $399。这相当于与实际比特币价格的差异大约为 8.4%。

这些结果有助于理解我们的预测。我们将继续使用model.evaluate()方法来跟踪我们的 LSTM 模型如何改进,同时也会计算每个版本模型在完整系列上的rmse()mape(),以解释我们在预测比特币价格时的准确度。

活动:创建一个主动的训练环境

在此活动中,我们为神经网络创建了一个训练环境,促进其训练和评估。这个环境对于下一章尤为重要,在那一章中,我们将寻找最佳的超参数组合。

首先,我们将启动一个 Jupyter Notebook 实例和一个 TensorBoard 实例。接下来的活动中,这两个实例可以保持打开状态。

  1. 使用终端,导航到目录chapter_6/activity_6,并执行以下代码以启动 Jupyter Notebook 实例:
      $ jupyter notebook
  1. 在浏览器中打开应用程序提供的 URL,并打开名为Activity_6_Creating_an_active_training_environment.ipynb的 Jupyter Notebook:

图 7:Jupyter Notebook 中高亮显示的“评估 LSTM 模型”部分

  1. 同时,使用终端启动一个 TensorBoard 实例,执行以下命令:
      $ cd ./chapter_3/activity_6/
      $ tensorboard --logdir=logs/
  1. 打开屏幕上出现的 URL,并保持该浏览器标签页打开。

  2. 现在,将训练集(train_dataset.csv)和测试集(test_dataset.csv)以及我们之前编译的模型(bitcoin_lstm_v0.h5)加载到 Notebook 中。

  3. 使用以下命令将训练集和测试集加载到 Jupyter Notebook 实例中:

      $ train = pd.read_csv('data/train_dataset.csv')
      $ test = pd.read_csv('data/test_dataset.csv') 
  1. 此外,使用以下命令加载我们之前编译的模型:
      $ model = load_model('bitcoin_lstm_v0.h5')

现在,让我们评估模型在测试数据上的表现。我们的模型使用 76 周的数据来预测未来一周的情况,即接下来的七天。当我们构建第一个模型时,我们将原始数据集分为训练集和测试集。现在,我们将合并这两个数据集(我们称之为合并集),并滑动一个 76 周的窗口。在每个窗口中,我们执行 Keras 的model.evaluate()方法,评估网络在该特定周的表现。

  1. 执行“评估 LSTM 模型”标题下的单元格。这些单元格的关键概念是对测试集中的每一周调用model.evaluate()方法。以下这一行是最重要的:
       $ result = model.evaluate(x=X_test, y=Y_test, verbose=0) 
  1. 每个评估结果现在存储在变量evaluated_weeks中。这个变量是一个简单的数组,包含测试集中每一周的 MSE 预测结果。现在可以继续绘制这些结果:

如我们在章节中讨论的那样,MSE 损失函数很难解释。为了便于理解模型的表现,我们还会对测试集中的每一周调用model.predict()方法,并将其预测结果与实际值进行比较。

  1. 导航到解释模型结果部分并执行做出预测子标题下的代码单元格。请注意,我们正在调用model.predict()方法,但使用的是稍有不同的参数组合。我们只使用X,而不是同时使用XY值:
      predicted_weeks = []
      for i in range(0, test_data.shape[1]):
      input_series = combined_set[0:,i:i+76]
      predicted_weeks.append(model.predict(input_series)) 

在每个窗口,我们将对下一周进行预测并存储结果。我们现在可以将归一化结果与测试集中的归一化值进行比较,如下图所示:

图 9:绘制从model.predict()返回的每周归一化值

我们也将进行相同的比较,但使用去归一化后的值。为了去归一化我们的数据,我们首先需要识别测试集和预测结果之间的等效周。然后,我们取该周的第一个价格值,并用它来反转第5 章中的基于点的归一化方程,模型架构

  1. 导航到标题“去归一化预测”并执行该标题下的所有单元格。

  2. 在这一部分,我们定义了denormalize()函数,它执行完整的去归一化过程。与其他函数不同,这个函数接受的是一个 Pandas DataFrame,而不是 NumPy 数组。我们这样做是为了使用日期作为索引。这是该部分标题下最相关的代码块:

      predicted_close = predicted.groupby('iso_week').apply(
         lambda x: denormalize(observed, x))

我们的去归一化结果(如以下图所示)显示,我们的模型做出的预测与实际比特币价格非常接近。但到底有多接近呢?

图 10:绘制从model.predict()返回的每周去归一化值

LSTM 网络使用均方误差(MSE)值作为其损失函数。然而,正如在章节中讨论的,MSE 值难以解释。为了解决这个问题,我们实现了两个函数(从script utilities.py中加载),它们分别实现了 RMSE 和 MAPE 函数。这些函数通过返回与我们原始数据使用相同量纲的度量,并通过将量纲差异作为百分比进行比较,从而为我们的模型增加了解释性。

  1. 导航到“去归一化预测”标题并从utilities.py脚本中加载两个函数:
      from scripts.utilities import rmse, mape 

脚本中的函数实际上非常简单:

      def mape(A, B):
      return np.mean(np.abs((A - B) / A)) * 100

      def rmse(A, B):
      return np.sqrt(np.square(np.subtract(A, B)).mean())

每个函数都是通过 NumPy 的向量化操作来实现的。它们在相同长度的向量上运行良好。它们被设计用于应用于完整的结果集。

使用mape()函数,我们现在可以理解,我们的模型预测结果与测试集的价格相差大约 8.4%。这相当于根均方误差(使用rmse()函数计算)大约为 399.6 美元。

在进入下一节之前,回到 Notebook 中找到标题为使用 TensorBoard 重新训练模型的部分。你可能已经注意到我们创建了一个名为train_model()的辅助函数。这个函数是我们模型的封装器,它训练(using model.fit())我们的模型,并将其结果存储在一个新的目录下。TensorBoard 随后将这些结果作为判别器,显示不同模型的统计数据。

  1. 请修改传递给model.fit()函数的一些参数值(例如试试 epochs)。现在,运行从磁盘加载模型到内存的单元(这将替换你训练过的模型):
      model = load_model('bitcoin_lstm_v0.h5') 
  1. 现在,再次运行train_model()函数,但使用不同的参数,表示一个新的运行版本:
      train_model(X=X_train, Y=Y_validate, version=0, run_number=0)

在本节中,我们学习了如何使用损失函数评估网络。我们了解到,损失函数是神经网络的关键元素,它们评估网络在每个 epoch 的表现,并且是将调整回传到层和节点的起点。我们还探讨了为什么一些损失函数可能难以解释(例如 MSE),并通过使用另外两个函数——RMSE 和 MAPE——来解释我们 LSTM 模型的预测结果。

最重要的是,本章以一个主动训练环境作为结尾。我们现在拥有一个能够持续训练深度学习模型并评估其结果的系统。这将在我们下一节优化网络时发挥关键作用。

超参数优化

我们已经训练了一个神经网络,利用前 76 周的比特币价格预测接下来七天的比特币价格。平均来说,这个模型给出的预测值与实际比特币价格之间的误差约为 8.4%。

本节描述了提高神经网络模型性能的常见策略:

  • 添加或移除层并更改节点数量

  • 增加或减少训练的 epoch 次数

  • 尝试不同的激活函数

  • 使用不同的正则化策略

我们将使用到目前为止在模型评估部分开发的相同主动学习环境来评估每个修改,衡量这些策略如何帮助我们开发出更精确的模型。

层和节点 - 添加更多层

单隐层神经网络在许多问题上可以表现得相当好。我们的第一个比特币模型(bitcoin_lstm_v0)就是一个很好的例子:它使用单个 LSTM 层,能够预测接下来七天的比特币价格(来自测试集),误差率约为 8.4%。然而,并不是所有问题都能用单层模型建模。

你要预测的函数越复杂,你需要添加更多层的可能性就越高。判断是否应该添加新层的一个好直觉是了解它们在神经网络中的作用。

每一层都会创建输入数据的模型表示。链条中的早期层创建较低级别的表示,而后期层则创建更高级别的表示。

虽然这个描述可能难以转化为现实世界的问题,但它的实际直觉很简单:在处理具有不同表示级别的复杂函数时,你可能想要尝试添加层。

添加更多节点

层所需的神经元数量与输入和输出数据的结构有关。

例如,如果你正在将一张 4 x 4 像素的图像分类到两个类别中的一个,你可以从一个具有 12 个神经元的隐藏层开始(每个神经元对应一个像素),然后再加一个只有两个神经元的输出层(每个神经元对应一个预测类别)。

在添加新层时,通常会添加新的神经元。然后,可以添加一个层,该层的神经元数量与前一层相同,或者是前一层神经元数量的倍数。例如,如果你的第一个隐藏层有 12 个神经元,你可以尝试添加一个第二层,它的神经元数量可以是 12、6 或 24。

添加层和神经元可能会导致性能的显著限制。可以随意尝试添加层和节点。通常的做法是从较小的网络开始(即网络中有少量的层和神经元),然后根据其性能的提升逐渐增长。

如果上面的内容听起来不够精确,你的直觉是对的。引用 YouTube 前视频分类负责人 Aurélien Géron 的话,找到合适数量的神经元仍然有些像黑魔法

《动手学机器学习》 by Aurelién Géron,O'Reilly 出版,2017 年 3 月。

最后,提醒一句:你添加的层越多,你需要调整的超参数也就越多——并且训练网络所需的时间也会更长。如果你的模型表现不错,并且没有对数据过拟合,可以在添加新层之前,先尝试本章中提到的其他策略。

层和节点 - 实现

我们将通过添加更多层来修改我们原来的 LSTM 模型。在 LSTM 模型中,通常会按顺序添加 LSTM 层,在 LSTM 层之间建立链条。在我们的案例中,新的 LSTM 层具有与原始层相同的神经元数量,因此我们不需要配置该参数。

我们将修改后的模型命名为bitcoin_lstm_v1。将每个尝试不同超参数配置的模型命名为不同的名称是一个好习惯。这有助于你跟踪每种不同架构的表现,并在 TensorBoard 中轻松比较模型之间的差异。我们将在本章末尾比较所有不同的修改过的架构。

在添加新的 LSTM 层之前,我们需要将第一个 LSTM 层的 return_sequences 参数修改为 True。这样做是因为第一个层期望的数据输入是一个序列,这与第一个层的数据输入格式相同。当这个参数设置为 False 时,LSTM 层会输出不兼容的预测参数。

考虑以下代码示例:

    period_length = 7
    number_of_periods = 76
    batch_size = 1

    model = Sequential()
    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=True, stateful=False))

    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=False, stateful=False))

    model.add(Dense(units=period_length))
    model.add(Activation("linear"))

    model.compile(loss="mse", optimizer="rmsprop") 

代码片段 8:向原始 bitcoin_lstm_v0 模型添加第二个 LSTM 层,使其变为 bitcoin_lstm_v1

轮次

轮次是网络在响应数据传递和损失函数时调整权重的次数。运行更多轮次的模型可以让它从数据中学习更多,但也会面临过拟合的风险。

在训练模型时,建议以指数方式增加轮次,直到损失函数开始趋于平稳。对于 bitcoin_lstm_v0 模型,其损失函数大约在 100 个轮次时趋于平稳。

我们的 LSTM 模型使用的数据量较小,因此增加训练轮次对性能几乎没有显著影响。例如,如果尝试在 103 个轮次下训练该模型,模型几乎没有任何改进。但如果训练的模型使用的是大量数据,情况则会有所不同。在这种情况下,大量的轮次对模型的良好性能至关重要。

我建议你使用以下关联:用于训练模型的数据量越大,所需的轮次就越多,以实现良好的性能。

轮次 - 实现

我们的比特币数据集相对较小,因此增加模型训练的轮次可能对性能的提升影响不大。为了让模型训练更多轮次,只需在 model.fit() 中更改轮次参数:

    number_of_epochs = 10**3
    model.fit(x=X, y=Y, batch_size=1,
        epochs=number_of_epochs,
        verbose=0,
    callbacks=[tensorboard]) 

代码片段 9:改变我们模型训练的轮次,使其变为bitcoin_lstm_v2

这个更改将我们的模型升级到了 v2,实际上使其变为 bitcoin_lstm_v2

激活函数

激活函数评估了需要激活每个神经元的程度。它们决定了每个神经元将传递给网络下一个元素的值,使用来自前一层的输入和损失函数的结果——或者决定神经元是否应当传递任何值。

激活函数是神经网络研究领域的一个重要话题。如果你想了解当前关于此主题的研究概况,以及对激活函数工作原理的更详细回顾,请参考 Ian Goodfellow 等人撰写的《Deep Learning》,MIT 出版社,2017 年。

TensorFlow 和 Keras 提供了许多激活函数——并且偶尔会新增一些。作为介绍,有三个函数非常重要,值得我们关注;我们将逐一探索它们。

本节内容深受 Avinash Sharma V 的文章 理解神经网络中的激活函数 启发,文章链接为:medium.com/the-theory-of-everything/understanding-activation-functions-in-neural-networks-9491262884e0

线性(恒等函数)

线性函数仅根据常数值激活神经元,其定义如下:

当 c = 1 时,神经元将按原样传递值,而不受激活函数的修改。使用线性函数的问题在于,由于神经元是线性激活的,链式层次现在作为一个大的单一层来工作。换句话说,失去了构建具有多层的网络的能力,其中一层的输出影响另一层:

图 11:线性函数示意图

线性函数的使用通常被认为在大多数网络中已经过时。

双曲正切(Tanh)

Tanh 是一个非线性函数,其公式如下:

这意味着它们对节点的影响是持续评估的。而且,由于其非线性特性,可以使用此函数改变一层如何影响链中下一层。当使用非线性函数时,各层以不同方式激活神经元,从数据中学习不同的表示变得更容易。然而,它们具有类似 Sigmoid 的模式,会反复惩罚极端的节点值,造成一种称为“梯度消失”的问题。梯度消失对网络学习能力产生负面影响:

图 12:tanh 函数示意图

Tanhs 是常用的选择,但由于其计算开销较大,通常会使用 ReLU 作为替代。

修正线性单元

ReLU 具有非线性特性,其定义如下:

图 13:ReLU 函数示意图

ReLU 函数通常被推荐作为在尝试其他函数之前的一个很好的起点。ReLU 倾向于惩罚负值。因此,如果输入数据(例如,归一化至 -1 到 1 之间)包含负值,这些值将会受到 ReLU 的惩罚。这可能不是预期的行为。

我们在网络中将不会使用 ReLU 函数,因为我们的归一化过程会生成许多负值,从而导致学习模型的速度大大减慢。

激活函数 - 实现

在 Keras 中实现激活函数的最简单方法是实例化Activation()类,并将其添加到Sequential()模型中。Activation()可以使用 Keras 中提供的任何激活函数进行实例化(完整列表请见keras.io/activations/)。在我们的例子中,我们将使用tanh函数。

在实现激活函数后,我们将模型的版本提升为v2,使其成为bitcoin_lstm_v3

    model = Sequential()

    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=True, stateful=False))

    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=False, stateful=False))

    model.add(Dense(units=period_length))
    model.add(Activation("tanh"))

    model.compile(loss="mse", optimizer="rmsprop") 

代码片段 10:将激活函数tanh添加到bitcoin_lstm_v2模型中,更新为bitcoin_lstm_v3

还有许多其他值得尝试的激活函数。TensorFlow 和 Keras 都在各自的官方文档中提供了已实现的函数列表。在实现自己的激活函数之前,最好先从 TensorFlow 和 Keras 中已经实现的函数开始。

正则化策略

神经网络尤其容易出现过拟合。过拟合发生在网络学习了训练数据的模式,但无法找到可以应用于测试数据的可泛化模式。

正则化策略是指通过调整网络学习的方式来处理过拟合问题的技术。在本书中,我们讨论了两种常见的策略:L2 正则化和 Dropout。

L2 正则化

L2 正则化(或称权重衰减)是一种常见的解决过拟合模型的技术。在一些模型中,某些参数的变化幅度较大。L2 正则化会对这些参数进行惩罚,从而减少这些参数对网络的影响。

L2 正则化使用参数来决定惩罚模型神经元的程度。通常将该值设置为非常小的数值(即0.0001);否则,可能会完全消除某个神经元的输入。

Dropout(丢弃法)

Dropout 是一种基于简单问题的正则化技术:如果从层中随机移除一部分节点,剩下的节点会如何适应?事实证明,剩余的神经元会适应,学习表示那些之前由缺失神经元处理的模式。

Dropout 策略实现起来简单,通常在避免过拟合方面非常有效。这将是我们首选的正则化策略。

正则化策略 – 实现

为了使用 Keras 实现 Dropout 策略,我们导入Dropout()类,并将其添加到每个 LSTM 层之后的网络中。

这一添加有效地将我们的网络变为bitcoin_lstm_v4

    model = Sequential()
    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=True, stateful=False))

    model.add(Dropout(0.2))
    model.add(LSTM(
        units=period_length,
        batch_input_shape=(batch_size, number_of_periods, period_length),
        input_shape=(number_of_periods, period_length),
        return_sequences=False, stateful=False))

    model.add(Dropout(0.2))

    model.add(Dense(units=period_length))
    model.add(Activation("tanh"))

    model.compile(loss="mse", optimizer="rmsprop") 

代码片段 11:在此代码片段中,我们将Dropout()步骤添加到我们的模型(bitcoin_lstm_v3)中,更新为bitcoin_lstm_v4

也可以使用 L2 正则化代替 Dropout。为此,只需实例化ActivityRegularization()类,并将 L2 参数设置为较小的值(例如0.0001)。然后,将其放置在网络中添加 Dropout()类的位置。可以通过将其添加到网络中,同时保留两个Dropout()步骤,或简单地将所有Dropout()实例替换为ActivityRegularization()来进行实验。

优化结果

总的来说,我们已经创建了四个版本的模型。这些版本中的三个是通过应用本章所述的不同优化技术创建的。

在创建了所有这些版本后,我们现在需要评估哪个模型表现最好。为此,我们使用与第一个模型相同的指标:MSE、RMSE 和 MAPE。MSE 用于比较模型在每一周预测中的误差率,RMSE 和 MAPE 用于使模型结果更易于解释。

模型 MSE(最后一轮) RMSE(整个序列) MAPE(整个序列) 训练时间
bitcoin_lstm_v0 - 399.6 8.4% ** -**
bitcoin_lstm_v1 7.15*10^(-6) 419.3 8.8% 49.3 秒
bitcoin_lstm_v2 3.55*10^(-6) 425.4 9.0% 1 分 13 秒
bitcoin_lstm_v3 2.8*10^(-4) 423.9 8.8% 1 分 19 秒
bitcoin_lstm_v4 4.8*10^(-7) 442.4 8.8% 1 分 20 秒

表 2:所有模型的结果

有趣的是,我们的第一个模型(bitcoin_lstm_v0)在几乎所有的指标中表现最好。我们将使用该模型来构建我们的 Web 应用程序,并持续预测比特币价格。

活动:优化深度学习模型

在这个活动中,我们对第五章模型架构bitcoin_lstm_v0)中创建的模型应用了不同的优化策略。该模型在完整的去归一化测试集上的 MAPE 性能约为 8.4%。我们将尝试减少这个差距。

  1. 使用终端,通过执行以下命令启动 TensorBoard 实例:
      $ cd ./chapter_3/activity_7/
      $ tensorboard --logdir=logs/ 
  1. 打开屏幕上出现的 URL,并保持该浏览器标签页打开。同时,启动一个 Jupyter Notebook 实例:
       $ jupyter notebook

打开出现在另一个浏览器窗口中的 URL。

  1. 现在,打开名为Activity_7_Optimizing_a_deep_learning_model.ipynb的 Jupyter Notebook,导航到 Notebook 的标题并导入所有所需的库。我们将像之前的活动一样加载训练和测试数据。我们还将使用实用函数split_lstm_input()将其分割为训练组和测试组。

在 Notebook 的每个部分,我们都会在模型中实现新的优化技术。每次我们这样做时,都会训练一个全新的模型,并将其训练后的实例存储在一个描述模型版本的变量中。例如,我们的第一个模型bitcoin_lstm_v0在 Notebook 中被称为model_v0。在 Notebook 的最后,我们使用 MSE、RMSE 和 MAPE 评估所有模型。

  1. 现在,在打开的 Jupyter Notebook 中,导航到Adding Layers(添加层)和Nodes(节点)部分。你将在下一个单元格中看到我们第一个模型。这是我们在第五章 模型架构 中构建的基础 LSTM 网络。现在,我们需要向这个网络添加一个新的 LSTM 层。

利用本章的知识,继续添加一个新的 LSTM 层,编译并训练模型。在训练模型时,请记得经常访问正在运行的 TensorBoard 实例。

你将能够看到每个模型的运行并比较它们的损失函数结果:

图 14:运行 TensorBoard 实例,显示多个不同的模型运行。TensorBoard 实际上是一个非常

有助于实时跟踪模型训练进展。

  1. 现在,导航到Epochs(训练轮次)部分。在这一部分,我们将探索不同规模的epochs。使用工具函数train_model()来命名不同的模型版本和运行:
      train_model(model=model_v0, X=X_train, Y=Y_validate, epochs=100,
      version=0, run_number=0) 

使用不同的 epoch 参数训练模型。

目前,你需要确保模型不会过拟合训练数据。你希望避免这种情况,因为如果模型过拟合,它将无法预测训练数据中表现出的模式,而这些模式在测试数据中可能会有不同的表现形式。

在你完成对 epoch 的实验后,继续进行下一个优化技术:激活函数。

  1. 现在,导航到 Notebook 中的Activation Functions(激活函数)部分。在这一部分,你只需要更改以下变量:
      activation_function = "tanh" 

本节中我们使用了tanh函数,但你可以尝试其他激活函数。查看keras.io/activations/中列出的激活函数,并尝试其他可能的选项。

我们的最终选择是尝试不同的正则化策略。这通常更为复杂,可能需要多次迭代才能看到任何改进——特别是在数据量如此之少的情况下。此外,添加正则化策略通常会增加网络的训练时间。

  1. 现在,导航到 Notebook 中的Regularization Strategies(正则化策略)部分。在这一部分,你需要实现Dropout()正则化策略。找到合适的位置将此步骤加入,并在我们的模型中实现。

  2. 你还可以尝试 L2 正则化(或者两者结合使用)。和Dropout()一样,使用ActivityRegularizationl2=0.0001)进行操作。

  3. 现在,导航到笔记本中的评估模型部分。在这一部分,我们将评估模型对测试集未来 19 周数据的预测。然后,我们将计算预测系列与测试系列之间的 RMSE 和 MAPE。

我们已经实现了与第 6 个活动相同的评估技术,所有功能都封装在实用函数中。只需运行本节的所有单元格直到笔记本结束,即可查看结果。

抓住这个机会调整前面提到的优化技术的值,尝试超越该模型的性能。

总结

在这一章节中,我们学习了如何使用均方误差(MSE)、均方根误差(RMSE)和平均绝对百分比误差(MAPE)来评估我们的模型。我们在由我们的第一个神经网络模型进行的 19 周预测系列中计算了后两个指标。然后,我们了解到该模型表现良好。

我们还学习了如何优化模型。我们查看了通常用于提高神经网络性能的优化技术。此外,我们实现了其中的一些技术,并创建了几个不同的模型来预测比特币价格,具有不同的误差率。

在下一章节中,我们将把我们的模型转化为一个 Web 应用,完成两件事:定期使用新数据重新训练我们的模型,并能够通过 HTTP API 接口进行预测。

第七章:产品化

本章重点讨论如何将深度学习模型进行产品化。我们使用“产品化”一词来定义将深度学习模型转化为软件产品的过程,使其能够被其他人和应用程序使用。

我们关注的是能够在新数据可用时使用它的模型,持续从新数据中学习模式,并因此做出更好的预测。我们研究了处理新数据的两种策略:一种是重新训练现有模型,另一种是创建一个完全新的模型。然后,我们将在比特币价格预测模型中实现后一种策略,以使其能够持续预测新的比特币价格。

本章还提供了如何将模型部署为 Web 应用程序的练习。到本章结束时,我们将能够部署一个有效的 Web 应用程序(具有功能齐全的 HTTP API)并根据需要进行修改。

我们以 Web 应用程序为例,来展示如何部署深度学习模型,因为它简单且普遍(毕竟,Web 应用程序非常常见),但也有许多其他的可能性。

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

  • 处理新数据

  • 将模型部署为 Web 应用程序

处理新数据

模型可以在一组数据上进行训练,然后用来做预测。这些静态模型非常有用,但通常我们希望模型能够从新数据中持续学习——并随着学习的进行不断改进。

在本节中,我们将讨论如何重新训练深度学习模型的两种策略,并在 Python 中实现它们。

数据与模型的分离

在构建深度学习应用时,最重要的两个领域是数据和模型。从架构角度来看,我们建议将这两个领域分开。我们认为这是一个好的建议,因为这两个领域各自包含的功能本质上是相互独立的。数据通常需要收集、清洗、组织和规范化;而模型则需要训练、评估,并能够进行预测。这两个领域是相互依赖的,但分别处理会更好。

根据这一建议,我们将使用两个类来帮助我们构建 Web 应用程序:CoinMarketCap()Model()

  • CoinMarketCap() :这是一个用于从以下网站获取比特币价格的类:www.coinmarketcap.com。这也是我们最初比特币数据的来源。这个类使我们能够定期提取该数据,返回一个包含解析记录和所有可用历史数据的 Pandas DataFrame。CoinMarketCap() 是我们的数据组件。

  • Model() : 该类将我们迄今为止编写的所有代码整合成一个类。该类提供了与我们之前训练的模型进行交互的功能,并允许使用去标准化的数据进行预测——这使得理解更加容易。Model() 类是我们的模型组件。

这两个类在我们的示例应用中被广泛使用,定义了数据和模型组件。

数据组件

CoinMarketCap() 类创建了用于检索和解析数据的方法。它包含一个相关方法 historic(),其详细代码如下:

    @classmethod
 def historic(cls, start='2013-04-28', stop=None,
 ticker='bitcoin', return_json=False):
    start = start.replace('-', '')
    if not stop:
        stop = datetime.now().strftime('%Y%m%d')
    base_url = 'https://coinmarketcap.com/currencies'

    url = '/{}/historical-10\. data/?start={}&end={}'.format(ticker, start,
    stop)
    r = requests.get(url)

代码片段 1CoinMarketCap() 类中的 historic() 方法。

该方法从 CoinMarketCap 网站收集数据,解析数据,并返回一个 Pandas DataFrame。

historic() 类返回一个 Pandas DataFrame,准备由 Model() 类使用。

在使用其他模型时,考虑创建一个程序组件(例如,Python 类),其功能与 CoinMarketCap() 类相同。也就是说,创建一个组件,能够从数据源获取数据,解析数据,并将其以可用格式提供给模型组件。

CoinMarketCap() 类使用参数 ticker 来确定收集哪种加密货币。CoinMarketCap 提供了许多其他加密货币,包括非常流行的以太坊(ethereum)和比特币现金(bitcoin-cash)。使用 ticker 参数可以更改加密货币,训练不同于本书中使用的比特币模型的模型。

模型组件

Model() 类是我们实现应用程序模型组件的地方。该类包含实现本书中所有不同建模主题的文件方法。这些方法包括:

  • build() : 使用 Keras 构建一个 LSTM 模型。该函数作为一个简单的包装器,封装了手动创建的模型。

  • train() : 使用类实例化时提供的数据训练模型。

  • evaluate() : 使用一组损失函数对模型进行评估。

  • save() : 将模型作为文件保存在本地。

  • predict() : 基于按周排序的观察输入序列进行预测并返回结果。

我们在本章中使用这些方法来处理、训练、评估并发出模型预测。Model() 类是如何将 Keras 的核心功能封装到 Web 应用中的示例。这些方法的实现几乎与前面章节中的实现完全相同,但增加了语法糖以增强它们的接口。

例如,train() 方法在以下代码中实现:

 def train(self, data=None, epochs=300, verbose=0, batch_size=1):
        self.train_history = self.model.fit(
            x=self.X, y=self.Y,
            batch_size=batch_size, epochs=epochs,
            verbose=verbose, shuffle=False)
    self.last_trained = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    return self.train_history

代码片段 2Model() 类中的 train() 方法。该方法使用来自 self.X 和 self.Y 的数据训练自 self.model 的模型。

在前面的代码片段中,您会注意到train()方法类似于来自Chapter 6活动 6和 7 的解决方案,模型评估与优化*。其核心思想是,Keras 工作流中的每个过程(构建或设计、训练、评估和预测)都可以轻松地转化为程序的不同部分。在我们的案例中,我们将它们变成了可以从Model()类调用的方法。这有助于组织我们的程序,并提供一系列约束(例如模型架构或某些 API 参数),帮助我们在稳定的环境中部署模型。

在接下来的章节中,我们将探讨处理新数据的常见策略。

处理新数据

机器学习模型的核心思想——包括神经网络——是它们能够从数据中学习模式。假设一个模型已使用某个数据集进行训练,并且现在开始进行预测。现在,假设有新的数据可用。我们可以采用什么策略,使模型能够利用新数据来学习新的模式并改进其预测?

本节我们讨论两种策略:重新训练旧模型和训练新模型。

重新训练旧模型

使用这种策略,我们使用新数据重新训练现有模型。通过这种策略,可以不断调整模型参数,以适应新现象。然而,后期训练使用的数据可能与早期数据有显著不同。这些差异可能会导致模型参数发生重大变化,使其学习到新模式并遗忘旧模式。这一现象通常被称为灾难性遗忘

灾难性遗忘是影响神经网络的常见现象。深度学习研究人员多年来一直在努力解决这个问题。DeepMind,谷歌旗下的英国深度学习研究小组,在寻找解决方案方面取得了显著进展。文章《克服神经网络中的灾难性遗忘》(Overcoming Catastrophic Forgetting in Neural Networks)是这类工作的一个良好参考。该论文可在以下链接获得:arxiv. org/pdf/1612.00796.pdf

用于首次训练的相同接口(model.fit())可以用于用新数据进行训练:

    X_train_new, Y_train_new = load_new_data()

    model.fit(x=X_train_new, y=Y_train_new,
    batch_size=1, epochs=100,
    verbose=0)

代码片段 3:在我们的 LSTM 模型中实现 TensorBoard 回调的代码片段

在 Keras 中,当模型进行训练时,其权重信息会被保留——这是模型的状态。当使用model.save()方法时,该状态也会被保存。当调用model.fit()方法时,模型会使用之前的状态作为起点,用新数据集重新训练。

在典型的 Keras 模型中,这种技术可以顺利使用。然而,在处理 LSTM 模型时,这种技术有一个关键的限制:训练数据和验证数据的形状必须相同。例如,我们的 LSTM 模型(bitcoin_lstm_v0)使用 76 周的数据来预测未来一周。如果我们在下一周尝试使用 77 周的数据重新训练网络,模型将抛出异常,提示数据形状不正确。

处理这个问题的一种方法是将数据安排为模型所期望的格式。在我们的例子中,我们需要配置模型,使用 40 周的数据来预测未来的一周。采用这个解决方案时,我们首先用 2017 年最初的 40 周数据训练模型,然后继续在接下来的几周中重新训练,直到达到第 50 周。

我们使用Model()类在以下代码中执行此操作:

    M = Model(data=model_data[0*7:7*40 + 7],
        variable='close',
        predicted_period_size=7)
    M.build()
    6 M.train()
    for i in range(1, 10 + 1):
 M.train(model_data[i*7:7*(40 + i) + 7])

Snippet 4:实现重新训练技术的代码片段

这种技术训练速度较快,且通常适用于较大的数据序列。接下来的技术更易于实现,且在较小的数据序列中表现良好。

训练新模型

另一种策略是每次有新数据时创建并训练一个新模型。这种方法有助于减少灾难性遗忘,但随着数据量的增加,训练时间也会增加。其实现非常简单。

以比特币模型为例,假设我们拥有 2017 年 49 周的旧数据,并且在一周后,新的数据可用。我们用以下的old_datanew_data变量来表示这一点:

    old_data = model_data[0*7:7*48 + 7]
    new_data = model_data[0*7:7*49 + 7]

    M = Model(data=old_data,
        variable='close',
        predicted_period_size=7)
    M.build()
    M.train()

 M = Model(data=new_data,
 variable='close',
 predicted_period_size=7)
 M.build()
 M.train()

Snippet 5:实现当新数据可用时训练新模型的策略的代码片段

这种方法非常简单实现,并且对于小数据集来说效果良好。这将是我们比特币价格预测应用程序的首选解决方案。

活动:处理新数据

在这个活动中,我们每次获得新数据时都会重新训练我们的模型。

首先,我们通过导入cryptonic开始。Cryptonic 是为本书开发的一个简单软件应用,它使用 Python 类和模块实现了直到这一部分的所有步骤。可以将 Cryptonic 看作是一个模板,展示了你如何开发类似的应用程序。

cryptonic作为一个 Python 模块随本活动一起提供。首先,我们将启动一个 Jupyter Notebook 实例,然后加载cryptonic包。

  1. 使用你的终端,导航到目录Chapter_7/activity_8,然后执行以下代码以启动一个 Jupyter Notebook 实例:
      $ jupyter notebook 
  1. 在浏览器中打开应用程序提供的 URL,并打开名为Activity_8_Re_training_a_model_dynamically.ipynb的 Jupyter Notebook。

现在,我们将从cryptonic: Model()CoinMarketCap()加载两个类。这些类有助于操作我们的模型,并且能够从网站 CoinMarketCap (coinmarketcap.com/) 获取数据。

  1. 在 Jupyter Notebook 实例中,导航到标题 获取实时数据。我们现在将从 CoinMarketCap 获取更新的历史数据。只需调用该方法:
      $ historic_data = CoinMarketCap.historic() 

变量 historic_data 现在被填充为一个 Pandas DataFrame,包含直到今天或昨天的数据。这很棒,并且当更多数据可用时,这使得我们可以更容易地重新训练模型。

数据基本包含了我们早期数据集中的相同变量。然而,大部分数据来自较早的时期。近期的比特币价格相比几年前已经出现了较大的波动。在将这些数据用于模型之前,让我们确保只使用 2017 年 1 月 1 日之后的日期数据。

  1. 使用 Pandas API,过滤仅包含 2017 年可用日期的数据:
      $ model_data = # filter the dataset using pandas here 

你应该可以使用日期变量作为过滤索引来实现这一点。确保在继续之前过滤数据。

Model() 类将我们迄今为止在所有活动中编写的代码汇总起来。我们将使用这个类来构建、训练和评估我们的模型。

  1. 使用 Model() 类,我们现在使用之前过滤过的数据来训练模型:
      M = Model(data=model_data,
         variable='close',
         predicted_period_size=7)
      M.build()
      M.train()
      M.predict(denormalized=True) 

上述步骤展示了使用 Model() 类训练模型时的完整工作流程。

接下来,我们将专注于每次新数据可用时重新训练模型。这会根据新数据调整网络的权重。

为了做到这一点,我们已经配置了模型,通过使用 40 周数据来预测一周。现在我们希望使用剩余的 10 周完整数据,创建包含其中一个完整周的 40 周重叠周期,并为每个周期重新训练模型。

  1. 在 Jupyter Notebook 中导航到标题 重新训练旧模型。现在,完成范围函数和 model_data 过滤参数,使用索引将数据拆分为重叠的七天组。然后,重新训练模型并收集结果:
      results = []
      for i in range(A, B):
         M.train(model_data[C:D])
         results.append(M.evaluate()) 

变量 ABCD 是占位符。使用整数创建重叠的七天组,其中重叠部分为一天。

重新训练模型后,请调用 M.predict(denormalized=True) 函数并欣赏结果。

接下来,我们将专注于每次新数据可用时创建并训练一个新模型。为此,我们假设我们已经拥有 2017 年 49 周的旧数据,并且一周后,我们将拥有新数据。我们用变量 old_datanew_data 来表示这一点。

  1. 导航到标题 训练新模型,并将数据分割到变量 old_datanew_data 中:
      old_data = model_data[0*7:7*48 + 7]
      new_data = model_data[0*7:7*49 + 7] 
  1. 然后,首先使用 old_data 训练模型:
      M = Model(data=old_data,
        variable='close',
        predicted_period_size=7)
      M.build()
      M.train()

这个策略是从零开始构建模型,并在有新数据时进行训练。请继续在接下来的单元格中实现这一点。

现在我们已经拥有训练模型所需的所有组件。在下一节中,我们将部署我们的模型为 Web 应用程序,通过 HTTP API 在浏览器中提供预测。

在本节中,我们学习了在新数据可用时训练模型的两种策略:

  • 重新训练旧模型

  • 训练新模型

后者创建一个新模型,该模型使用除测试集中的观察数据外的完整数据集进行训练。前者则在可用数据上训练一次模型,然后继续创建重叠的批次,每当有新数据可用时重新训练同一模型。

部署模型为 Web 应用程序

在本节中,我们将把我们的模型部署为 Web 应用程序。我们将使用一个名为 "cryptonic" 的示例 Web 应用程序来部署我们的模型,探索其架构,以便未来可以进行修改。目的是让您将这个应用程序作为更复杂应用程序的起点;它是一个完全可运行的起点,您可以根据需要进行扩展。

除了熟悉 Python,本主题假设您已了解创建 Web 应用程序。具体来说,我们假设您对 Web 服务器、路由、HTTP 协议和缓存有所了解。您将能够在没有深入了解这些主题的情况下,本地部署示范的 cryptonic 应用程序,但学习这些内容将使未来的开发变得更加容易。

最后,Docker 被用于部署我们的 Web 应用程序,因此掌握该技术的基础知识也很有用。

应用程序架构和技术

为了部署我们的 Web 应用程序,我们将使用表 1 中描述的工具和技术。Flask 是关键,因为它帮助我们为模型创建 HTTP 接口,使我们能够访问 HTTP 端点(例如 /predict)并以通用格式接收数据。其他组件则是开发 Web 应用程序时的流行选择:

工具或技术 描述 角色

| Docker | Docker 是一种用于处理以容器形式打包的应用程序的技术

容器。Docker 是一种日益流行的

技术,用于构建 Web 应用程序。

| 打包 Python 应用程序和 UI。

|

| Flask | Flask 是一个用于构建 Python Web 应用程序的微框架。

| 创建应用程序路由

|

| Vue.js | 通过动态更改模板来工作的 JavaScript 框架

基于来自

后端。

渲染用户界面。

| Nginx | Web 服务器,易于配置,用于将流量路由到 Docker 化的应用程序并处理 SSL

用于 HTTPS 连接的证书。

| 路由流量在用户和 Flask 应用程序之间。

|

| Redis | 键值数据库。由于其

简洁性和速度。

缓存 API 请求。

表 1:用于部署深度学习 Web 应用程序的工具和技术

这些组件组合在一起,如下图所示:

图 1:本项目中构建的 Web 应用程序的系统架构

用户通过浏览器访问 Web 应用程序。然后,Nginx 会将流量路由到包含 Flask 应用程序的 Docker 容器(默认情况下,运行在 5000 端口)。Flask 应用程序在启动时已经实例化了我们的比特币模型。如果给定了模型,它将使用该模型而不进行训练;如果没有,它将创建一个新模型并使用来自 CoinMarketCap 的数据从头开始训练。

在准备好模型后,应用程序会检查请求是否已缓存于 Redis 中——如果有,则返回缓存的数据。如果没有缓存,它将继续发出预测并在 UI 中渲染结果。

部署和使用 Cryptonic

cryptonic 被开发为一个 Docker 化应用程序。从 Docker 的角度来看,这意味着应用程序可以作为 Docker 镜像构建,然后在开发或生产环境中作为 Docker 容器部署。

Docker 使用名为 Dockerfile 的文件来描述如何构建镜像以及当镜像作为容器部署时会发生什么。Cryptonic 的 Dockerfile 可以在以下代码中找到:

    FROM python:3.6
    COPY . /cryptonic
    WORKDIR "/cryptonic"
    RUN pip install -r requirements.txt
    EXPOSE 5000
    CMD ["python", "run.py"]

片段 6:cryptonic 镜像的 Docker 文件

可以使用以下命令通过 Docker 文件构建 Docker 镜像:

     $ docker build --tag cryptonic:latest

片段 7:用于在本地构建 Docker 镜像的 Docker 命令

此命令将使镜像 cryptonic:latest 可用,并可以作为容器进行部署。构建过程可以在生产服务器上重复,或者直接部署镜像并作为容器运行。

在镜像构建并可用后,可以使用 docker run 命令来运行 cryptonic 应用程序,如以下代码所示:

     $ docker run --publish 5000:5000 \ 
             --detach cryptonic:latest

片段 8:在终端中执行 docker run 命令的示例

--publish 标志将本地主机的 5000 端口绑定到 Docker 容器的 5000 端口,而 --detach 会将容器作为守护进程在后台运行。

如果您已经训练了一个不同的模型,并希望使用该模型而不是训练一个新模型,您可以在 docker-compose.yml 文件中修改 MODEL_NAME 环境变量,如 片段 9 所示。该变量应包含您训练并希望提供的模型的文件名(例如,bitcoin_lstm_v1_trained.h5)——它应该也是一个 Keras 模型。如果您这么做,确保将本地目录挂载到 /models 文件夹中。您决定挂载的目录必须包含您的模型文件。

cryptonic 应用程序还包括一些环境变量,您在部署自己的模型时可能会觉得很有用:

  • MODEL_NAME:允许提供一个训练好的模型,以供应用程序使用。

  • BITCOIN_START_DATE:确定用作比特币系列起始日的日期。近年来,比特币价格的波动性远大于早期的波动性。此参数将过滤数据,仅使用感兴趣年份的数据。默认值是 2017 年 1 月 1 日。

  • PERIOD_SIZE:设置周期大小(以天为单位)。默认值为 7。

  • EPOCHS:配置模型每次运行时训练的轮次。默认值为 300。

这些变量可以在docker-compose.yml文件中进行配置,如下所示:

    version: "3"
    services:
    cache:
    image: cryptonic-cache:latest
    volumes: - $PWD/cache_data:/data
    networks:- cryptonic
    ports: - "6379:6379"
        environment:
            - MODEL_NAME=bitcoin_lstm_v0_trained.h5
            - BITCOIN_START_DATE=2017-01-01
            - EPOCH=300
            - PERIOD_SIZE=7

代码片段 9:包含环境变量的docker-compose.yml文件

部署 cryptonic 最简单的方法是使用代码片段 9 中的docker-compose.yml文件。该文件包含应用程序运行所需的所有规格,包括如何连接 Redis 缓存以及使用哪些环境变量的说明。导航到docker-compose.yml文件所在的位置后,可以使用命令docker-compose up启动 cryptonic,如下所示:

     $ docker-compose up -d 

代码片段 10:使用 docker-compose 启动 Docker 应用程序。-d标志将在后台执行应用程序。

部署后,可以通过 Web 浏览器在5000端口访问 cryptonic。该应用程序具有一个简单的用户界面,显示一个时间序列图,其中展示了真实的历史价格(即观察到的)和深度学习模型预测的未来价格(即预测的)。还可以在文本中看到使用Model().evaluate()方法计算的 RMSE 和 MAPE:

图 2:部署后的 cryptonic 应用程序截图

除了其用户界面(使用Vue.js开发),该应用程序还具有一个 HTTP API,在调用时进行预测。

API 有/predict端点,该端点返回一个 JSON 对象,包含未来一周内的去归一化比特币价格预测:

    {
    message: "API for making predictions.",
    period_length: 7,
    result: [
        15847.7,
        15289.36,
        17879.07,
    …
        17877.23,
        17773.08
    ],
        success: true,
        version: 1
    } 

代码片段 11:/predict 端点的示例 JSON 输出

该应用程序现在可以部署在远程服务器上,并用于持续预测比特币价格。

活动:部署深度学习应用程序

在本次活动中,我们将模型作为本地 Web 应用程序部署。这使得我们可以通过浏览器连接到该 Web 应用程序,或通过应用程序的 HTTP API 使用其他应用程序。在继续之前,请确保您的计算机中已安装并可用以下应用程序:

  • Docker(社区版)17.12.0-ce 或更高版本

  • Docker Compose (docker-compose) 1.18.0 或更高版本

上述两个组件可以从网站docker.com/.下载并安装到所有主要系统中。这些是完成此活动的必要组件。在继续之前,请确保这些组件在您的系统中可用。

  1. 使用终端导航到 cryptonic 目录并构建所有必需组件的 Docker 镜像:
      $ docker build --tag cryptonic:latest .    
      $ docker build --tag cryptonic-cache:latest ./ cryptonic-cache/ 
  1. 这两条命令构建了我们将在此应用中使用的两个镜像:cryptonic(包含 Flask 应用)和 cryptonic-cache(包含 Redis 缓存)。

  2. 构建完镜像后,找到 docker-compose.yml 文件并用文本编辑器打开。将参数 BITCOIN_START_DATE 改为除 2017-01-01 以外的其他日期:

      BITCOIN_START_DATE = # Use other date here 
  1. 最后的步骤是使用 docker-compose 在本地部署你的网页应用,步骤如下:
      docker-compose up 

你应该能在终端中看到一份活动日志,其中包括模型的训练周期。

  1. 模型训练完成后,你可以访问 http://localhost:5000 你的应用,并在 http://localhost:5000/predict 进行预测:

图 3:在本地部署的 Cryptonic 应用程序的截图

总结

本章总结了我们创建深度学习模型并将其作为网页应用部署的过程。我们的最后一步是部署一个使用 Keras 和 TensorFlow 引擎构建的比特币价格预测模型。我们通过将应用打包成 Docker 容器并进行部署,使得其他人可以使用我们的模型进行预测——以及通过其 API 使用其他应用。

除了这些工作,你还会发现有许多地方可以改进。我们的比特币模型仅仅是模型可以做的一个示例(特别是 LSTM 模型)。现在的挑战有两个方面:如何随着时间的推移让这个模型表现得更好?以及,如何在你的网页应用中添加一些功能,使得你的模型更易于访问?祝你好运,继续学习!

posted @ 2025-07-10 11:38  绝不原创的飞龙  阅读(45)  评论(0)    收藏  举报