MXNet 深度学习秘籍(一)
原文:
annas-archive.org/md5/22b009d5e2069762a5538fce4fc0f3fd译者:飞龙
前言
MXNet 是一个开源的深度学习框架,允许您训练和部署神经网络模型,并实现计算机视觉、自然语言处理等领域的最先进(SOTA)架构。通过本书,您将能够使用 Apache MXNet 构建快速、可扩展的深度学习解决方案。
本书将首先介绍 MXNet 的不同版本以及安装库前需要选择的版本。您将学习如何开始使用 MXNet/Gluon 库来解决分类和回归问题,并了解这些库的内部工作原理。本书还将展示如何使用 MXNet 分析玩具数据集,涉及数值回归、数据分类、图像分类和文本分类等领域。您还将学习从零开始构建和训练深度学习神经网络架构,然后再深入复杂的概念,如迁移学习。您将学会构建和部署神经网络架构,包括 CNN、RNN、Transformers,并将这些模型集成到您的应用中。您还将学习如何分析这些模型的性能,并进行微调,以提高准确性、可扩展性和速度。
到本书结束时,您将能够利用 MXNet 和 Gluon 库使用 GPU 创建并训练深度学习网络,并学习如何在不同环境中高效部署它们。
本书适合谁阅读?
本书非常适合数据科学家、机器学习工程师以及希望使用 Apache MXNet 构建快速、可扩展深度学习解决方案的开发者。读者需要对 Python 编程有良好的理解,并且具备 Python 3.7+的工作环境。具备深度学习相关数学理论的良好基础将是有益的。
本书内容
第一章*, 开始使用 MXNet*,要开始使用 MXNet,我们需要安装相关库。MXNet 有多个不同版本可供安装,在本章中,我们将讨论如何帮助您选择合适的版本。最重要的参数是我们拥有的硬件。为了优化性能,最好最大限度地利用我们现有的硬件资源。我们将比较著名的线性代数库 NumPy 的使用方式,并介绍 MXNet 如何提供类似的操作。然后,我们将对比不同 MXNet 版本与 NumPy 的性能。
第二章*, 使用 MXNet 和可视化数据集:Gluon 和 DataLoader*,在本章中,我们将开始使用 MXNet 分析一些玩具数据集,涉及数值回归、数据分类、图像分类和文本分类等领域。为了高效地管理这些任务,我们将学习新的 MXNet 库和函数,如 Gluon 和 DataLoader。
第三章*, 解决回归问题*,在本章中,我们将学习如何使用 MXNet 和 Gluon 库应用监督学习来解决回归问题。我们将探索并理解一个房价数据集,并学习如何预测房屋的价格。为实现这一目标,我们将训练神经网络并研究不同超参数的效果。
第四章*, 解决分类问题*,在本章中,我们将学习如何使用 MXNet 和 Gluon 库应用监督学习来解决分类问题。我们将探索并理解一个花卉数据集,并学习如何根据一些指标预测花卉的种类。为实现这一目标,我们将训练神经网络并研究不同超参数的效果。
第五章*, 使用计算机视觉分析图像*,在本章中,读者将了解 MXNet/GluonCV 中可用于图像处理的不同架构和操作。此外,读者还将接触经典的计算机视觉问题:图像分类、目标检测和语义分割。接着,读者将学习如何利用 MXNetGluonCV 模型库使用现有的模型来解决这些问题。
第六章*, 使用自然语言处理理解文本*,在本章中,读者将了解 MXNet/GluonNLP 中可用于文本数据集的不同架构和操作。此外,读者将接触经典的自然语言处理问题:词向量、文本分类、情感分析和翻译。接着,读者将学习如何利用 GluonNLP 模型库使用现有的模型来解决这些问题。
第七章*, 使用迁移学习和微调优化模型*,在本章中,读者将了解如何使用迁移学习和微调技术优化预训练模型以适应特定任务。此外,读者将比较这些技术与从零开始训练模型的性能,并分析其中的权衡。读者将把这些技术应用于图像分类、图像分割以及从英语翻译成德语等问题。
第八章*, 使用 MXNet 提高训练性能*,在本章中,读者将学习如何利用 MXNet 和 Gluon 库优化深度学习训练循环。读者将学习 MXNet 和 Gluon 如何利用 Lazy Evaluation 和自动并行化等计算范式。此外,读者还将学习如何优化 Gluon 数据加载器,以支持 CPU 和 GPU,应用自动混合精度(AMP),并使用多个 GPU 进行训练。
第九章*,通过 MXNet 改善推理性能*,在本章中,读者将学习如何利用 MXNet 和 Gluon 库来优化深度学习推理。读者将学习 MXNet 和 Gluon 如何通过混合化机器学习模型(结合命令式编程和符号式编程)来提高性能。此外,读者还将学习如何通过应用 Float16 数据类型结合 AMP、量化模型并进行性能分析来进一步优化推理时间。
最大化利用本书的内容
读者需要具备良好的 Python 编程理解,并且使用 Python 3.7+ 的工作环境。拥有良好的深度学习数学理论知识将非常有益。还需要安装 MXNet 1.9.1 和附加的 GluonCV 和 GluonNLP 库(版本 0.10)。这些 MXNet/Gluon 的要求在 第一章 中有详细描述,读者可以按照说明操作。所有的代码示例都已在 Ubuntu 20.04、Python 3.10.12、MXNet 1.9.1、GluonCV 0.10 和 GluonNLP 0.10 上进行了测试。不过,它们也应该适用于未来的版本。
| 本书覆盖的软件/硬件 | 操作系统要求 |
|---|---|
| Python3.7+ | Linux(推荐使用 Ubuntu) |
| MXNet 1.9.1 | |
| GluonCV 0.10 | |
| GluonNLP 0.10 |
为了重现与 第八章 中描述的相似结果,读者需要访问一台安装有多个 GPU 的计算机。
如果你使用的是本书的电子版,我们建议你自己输入代码,或从本书的 GitHub 仓库获取代码(下节会提供链接)。这样做有助于避免因复制粘贴代码而引发的潜在错误。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件,链接地址为 github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook。如果代码有更新,它会在 GitHub 仓库中进行更新。
我们还提供了其他代码包,来自我们丰富的图书和视频目录,访问地址为 github.com/PacktPublishing/。快来查看吧!
使用的约定
本书中使用了多种文本约定。
Code in text:表示文中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名。举个例子:“我们将把计算时间存储在五个字典中,每个计算配置(timings_np、timings_mx_cpu 和 timings_mx_gpu)一个字典。”
代码块的设置方式如下:
import mxnet
mxnet.__version__
features = mxnet.runtime.Features()
print(features)
print(features.is_enabled('CUDA'))
print(features.is_enabled('CUDNN'))
print(features.is_enabled('MKLDNN'))
所有命令行输入或输出均按如下方式编写:
!python3 -m pip install gluoncv gluonnlp
!python3 -m pip install gluoncv gluonnlp
粗体:表示一个新术语、一个重要单词,或者屏幕上显示的单词。例如,菜单或对话框中的单词通常以粗体显示。以下是一个例子:“在这一步骤中,我们将使用来自一个名为Matplotlib的库的pyplot模块,这将使我们能够轻松创建图表。”
提示或重要说明
如此显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何内容有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中注明书名。
勘误:虽然我们已经尽最大努力确保内容的准确性,但错误还是会发生。如果您发现本书中的错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/err… 并填写表单。
盗版:如果您在互联网上发现任何我们作品的非法复制品,我们将非常感激您提供其位置或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。
如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com
分享您的想法
一旦您阅读了《深度学习与 MXNet Cookbook》,我们非常希望听到您的想法!请点击此处直接进入亚马逊的书籍评论页面并分享您的反馈。
您的评论对我们和技术社区都很重要,将帮助我们确保提供优质的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但又无法随身携带纸质书籍吗?
您的电子书购买无法与您选择的设备兼容吗?
不用担心,现在购买每本 Packt 书籍时,您都可以免费获得该书的无 DRM PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。
福利不止于此,您可以每天获得独家折扣、新闻通讯以及丰富的免费内容直接发送到您的邮箱。
按照以下简单步骤获取福利:
- 扫描二维码或访问下面的链接
packt.link/free-ebook/9781800569607
-
提交您的购买证明
-
就是这样!我们会直接通过电子邮件将您的免费 PDF 和其他福利发送给您。
第一章:快速启动 MXNet
MXNet是最常用的深度学习框架之一,是一个 Apache 开源项目。在 2016 年之前,Amazon Web Services(AWS)的研究团队并没有使用特定的深度学习框架,而是允许每个团队根据自己的选择进行研究和开发。尽管一些深度学习框架拥有蓬勃发展的社区,但有时 AWS 无法以所需的速度修复代码错误(还有其他问题)。为了解决这些问题,在 2016 年底,AWS 宣布将 MXNet 作为其首选的深度学习框架,并投资内部团队进一步开发。支持 MXNet 的研究机构包括英特尔、百度、微软、卡内基梅隆大学和麻省理工学院等。该框架由卡内基梅隆大学的 Carlos Guestrin 与华盛顿大学(以及 GraphLab)共同开发。
它的一些优势如下:
-
命令式/符号式编程及其混合化(将在第一章和第九章中讲解)
-
支持多 GPU 和分布式训练(将在第七章和第八章中讲解)
-
针对推理生产系统进行了高度优化(将在第七章和第九章中讲解)
-
在计算机视觉和自然语言处理等领域,拥有大量预训练模型,这些模型存储在它的 Model Zoos 中(将在第六章、第七章和第八章中讲解)
要开始使用 MXNet,我们需要安装该库。MXNet 有多个不同版本可以安装,在本章中,我们将介绍如何选择合适的版本。最重要的参数将是我们所拥有的硬件。为了优化性能,最好最大化利用现有硬件的能力。我们将比较著名的线性代数库 NumPy 与 MXNet 中相似操作的使用方式。然后,我们将比较不同 MXNet 版本与 NumPy 的性能。
MXNet 包含自己的深度学习 API——Gluon,此外,Gluon 还提供了用于计算机视觉和自然语言处理的不同库,这些库包含预训练模型和实用工具。这些库被称为 GluonCV 和 GluonNLP。
本章将涵盖以下主题:
-
安装 MXNet、Gluon、GluonCV 和 GluonNLP
-
NumPy 与 MXNet ND 数组——比较它们的性能
技术要求
除了前言中指定的技术要求外,本章没有其他要求。
本章的代码可以在以下 GitHub 网址找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch01。
此外,你可以直接通过 Google Colab 访问每个食谱——例如,使用以下链接访问本章的第一个食谱:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch01/1_1_Installing_MXNet.ipynb。
安装 MXNet、Gluon、GluonCV 和 GluonNLP
为了最大化利用现有的软件(编程语言)和硬件(CPU 和 GPU)性能,有不同版本的 MXNet 库可以安装。在本节中,我们将学习如何安装它们。
准备工作
在开始安装 MXNet 之前,让我们回顾一下我们将使用的软件包的不同版本,包括 MXNet。这样做的原因是,为了最大化性能,我们的硬件配置必须与所选软件包版本相匹配:
-
Python:MXNet 支持多种编程语言——如 Python、Java、R 和 C++等。我们将使用 MXNet 的 Python 版本,建议使用 Python 3.7 及以上版本。
-
Jupyter:Jupyter 是一个开源 Web 应用程序,提供了一个易于使用的界面来显示 Markdown 文本、可运行代码和数据可视化。它对于理解深度学习非常有用,因为我们可以描述概念,编写运行这些概念的代码,并可视化结果(通常将其与输入数据进行比较)。建议使用 Jupyter Core 4.5 及以上版本。
-
CPU 和 GPU:MXNet 可以与任何硬件配置兼容——也就是说,任何单一的 CPU 都可以运行 MXNet。然而,MXNet 可以利用多个硬件组件来提升性能:
-
Intel CPUs:Intel 开发了一种名为**数学核心库(Math Kernel Library,MKL)**的库,用于优化数学运算。MXNet 支持该库,使用优化版本可以提高某些操作的性能。任何现代版本的 Intel MKL 都足够。
-
NVIDIA GPUs:NVIDIA 开发了一种名为**计算统一设备架构(CUDA)**的库,用于优化并行操作(例如在深度学习中非常常见的矩阵运算)。MXNet 支持该库,使用优化版本可以显著提高大规模深度学习工作负载的性能,如模型训练。建议使用 CUDA 11.0 及以上版本。
-
-
MXNet 版本:写作时,MXNet 1.9.1 是已发布的最新稳定版本。全书中的所有代码都已使用此版本验证。MXNet 和深度学习一般可以视为一个持续进行的项目,因此,新版本会定期发布。这些新版本将具有改进的功能和新特性,但也可能包含与旧版 API 不兼容的重大更改。如果您在几个月后再次阅读此书,并且发布了包含重大更改的新版本,这里也有关于如何安装特定版本 MXNet 1.8.0 的说明。
提示
我在本书中使用 Google Colab 作为运行代码的平台。写作时,它提供 Python 3.10.12、最新的 Jupyter 库、英特尔 CPU(Xeon @ 2.3 GHz)和 NVIDIA GPU(可变:K80、T4、P4 和 P100),并预装了 CUDA 11.8。因此,安装 MXNet 并使其运行的步骤非常简便。
如何操作...
在全书中,我们不仅会大量使用代码,还会在代码中添加注释和标题来提供结构,同时也会展示一些视觉信息,如图像或生成的图表。出于这些原因,我们将使用 Jupyter 作为支持的开发环境。此外,为了简化设置、安装和实验过程,我们将使用 Google Colab。
Google Colab 是一个托管的 Jupyter Notebook 服务,无需设置即可使用,同时提供免费访问计算资源的权限,包括 GPU。为了正确设置 Google Colab,本节分为两个主要部分:
-
设置笔记本
-
验证和安装库
重要提示
如果您愿意,您可以使用任何支持 Python 3.7+ 的本地环境,如 Anaconda 或其他 Python 发行版。如果您的硬件规格优于 Google Colab 提供的硬件,强烈建议使用本地环境,因为更好的硬件可以减少计算时间。
设置笔记本
在本节中,我们将学习如何使用 Google Colab 并设置一个新的笔记本,我们将使用该笔记本来验证 MXNet 安装:
- 打开您喜欢的网页浏览器。在本书中,我一直使用 Google Chrome 浏览器。访问
colab.research.google.com/并点击 新建笔记本。
图 1.1 – Google Colab 启动画面
- 更改笔记本的标题 – 例如,如下截图所示,我已将标题更改为
DL with MXNet Cookbook 1.1安装 MXNet。
图 1.2 – 一个 Google Colab 笔记本
-
将 Google Colab 的运行时类型更改为使用 GPU:
- 从 运行时 菜单中选择 更改运行时类型。
图 1.3 – 更改运行时类型
- 在 Notebook 设置 中,选择 GPU 作为 硬件加速器 选项。
图 1.4 – 硬件加速器 | GPU
验证并安装库
在本节中,转到第一个单元格(确保它是代码单元格),并输入以下命令:
-
通过输入以下命令验证 Python 版本:
import platform platform.python_version() 3.7.10检查版本,并确保版本为 3.7 或以上。
重要提示
在 Google Colab 中,可以通过在命令前加上 ! 字符直接运行命令,就像在 Linux 终端中一样。也可以尝试其他命令,例如 !ls。
-
现在需要验证 Jupyter 版本(Jupyter Core 4.5.0 或更高版本即可):
!jupyter --version这是前一个命令的一个潜在输出:
jupyter core : 4.5.0 jupyter-notebook : 5.2.2 qtconsole : 4.5.2 ipython : 5.5.0 ipykernel : 4.10.1 jupyter client : 5.3.1 jupyter lab : not installed nbconvert : 5.5.0 ipywidgets : 7.5.0 nbformat : 4.4.0 traitlets : 4.3.2
提示
假设已经安装了开源的 Jupyter 笔记本应用程序,就像 Google Colab 中一样。如需安装说明,请访问 jupyter.org/install。
-
验证硬件中是否存在 Intel CPU:
!lscpu | grep 'Model name' Model name: Intel(R) Xeon(R) CPU @ 2.20GHz处理器越新越好,但在本书的应用场景中,GPU 的依赖性比 CPU 更大。
-
验证硬件中是否存在 NVIDIA GPU(下面列出了相关设备),并确认已安装 NVIDIA CUDA:
!nvidia-smi这将产生类似以下的输出:
+-----------------------------------------------------------------+ | NVIDIA-SMI 460.67 Driver Version: 460.32.03 CUDA Version: 11.2 | |---------------------------+--------------+----------------------+ |GPU Name Persistence-M|Bus-Id Disp.A| Volatile Uncorr. ECC | |Fan Temp Perf Pwr:Usage/Cap| Memory-Usage| GPU-Util Compute M. | | | | MIG M. | |===========================+==============+======================| | 0 Tesla T4 Off |0:00:04.0 Off | 0 | | N/A 37C P8 9W / 70W |0MiB/15109MiB | 0% Default | | | N/A | +---------------------------+--------------+----------------------+ +-----------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | ID ID Usage | |=================================================================| | No running processes found | +-----------------------------------------------------------------+
重要提示
CUDA 11.0 与 NVIDIA K80 已知存在兼容性问题。如果您使用的是 NVIDIA K80 并且在执行示例时遇到问题,请卸载 CUDA 11.0 并安装 CUDA 10.2。然后,按照这里描述的步骤安装支持 CUDA 10.2 的 MXNet。
-
验证 CUDA 版本是否为 11.0 或更高:
!nvcc --version这将产生类似以下的输出:
nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2020 NVIDIA Corporation Built on Wed_Jul_22_19:09:09_PDT_2020 Cuda compilation tools, release 11.0, V11.0.221 Build cuda_11.0_bu.TC445_37.28845127_0 -
根据您的硬件配置安装 MXNet。以下是您可以安装的不同版本的 MXNet:
-
推荐/Google Colab:安装支持 GPU 的最新 MXNet 版本(1.9.1):
!python3 -m pip install mxnet-cu117 -
没有 Intel CPU 也没有 NVIDIA GPU:使用以下命令安装 MXNet:
!python3 -m pip install mxnet -
没有 NVIDIA GPU 的 Intel CPU:使用以下命令安装带有 Intel MKL 的 MXNet:
!python3 -m pip install mxnet-mkl -
没有 Intel CPU 但有 NVIDIA GPU:使用以下命令安装带有 NVIDIA CUDA 10.2 的 MXNet:
!python3 -m pip install mxnet-cu102 -
Intel CPU 和 NVIDIA GPU:使用以下命令安装带有 Intel MKL 和 NVIDIA CUDA 11.0 的 MXNet:
!python3 -m pip install mxnet-cu110
-
提示
假设已安装 pip3(Python 3 包管理器),就像 Google Colab 中的情况一样。如果您更喜欢其他安装 MXNet 的方法,请访问 mxnet.apache.org/versions/master/get_started 获取说明。
从版本 1.6.0 开始,MXNet 默认会发布带有 Intel MKL 库扩展的版本,因此在安装最新版本时不再需要添加 mkl 后缀,如先前推荐的安装方法所示。
-
通过以下两个步骤验证 MXNet 是否成功安装:
- 以下命令必须没有错误,并且必须成功显示 MXNet 版本 1.9.1:
import mxnet mxnet.__version__- 以下列出的特性包含
CUDA、CUDNN和MKLDNN特性:
features = mxnet.runtime.Features() print(features) print(features.is_enabled('CUDA')) print(features.is_enabled('CUDNN')) print(features.is_enabled('MKLDNN'))输出将列出所有特性,并且每个特性后面都会显示
True。 -
安装 GluonCV 和 GluonNLP:
!python3 -m pip install gluoncv gluonnlp
此命令将安装 GluonCV 和 GluonNLP 的最新版本,在写作时它们分别是 0.10 和 0.10。
它是如何工作的...
深度学习网络的训练、推理和评估是非常复杂的操作,涉及硬件和多个软件层,包括驱动程序、低级性能库如 MKL 和 CUDA,以及高级编程语言和库如 Python 和 MXNet。
重要提示
MXNet 是一个积极开发的项目,属于 Apache Incubator 项目。因此,预计将会发布新版本,这些版本可能包含破坏性更改。前面的命令将安装最新的稳定版本。整本书中使用的 MXNet 版本是 1.9.1。如果你的代码失败并且使用了不同的 MXNet 版本,尝试运行以下命令安装 MXNet 版本 1.9.1:
!python3 -m pip install mxnet-cu117==1.9.1
通过检查所有硬件和软件组件,我们可以安装最优化的 MXNet 版本。我们可以使用 Google Colab,它可以轻松迁移到其他本地配置,如 Anaconda 发行版。
此外,我们可以识别出正确的 CUDA 驱动程序和 MXNet 版本组合,这样可以最大化性能并验证安装成功。
还有更多内容…
强烈建议始终使用所有讨论的软件组件的最新版本。深度学习是一个不断发展的领域,总会有新功能的加入、API 的变化,以及内部功能的更新,以提升性能,等等。
然而,所有组件(CPU、GPU、CUDA 和 MXNet 版本)之间的兼容性非常重要。为了确保这些组件匹配,强烈建议访问 mxnet.apache.org/versions/master/get_started,查看最新的 CUDA 和 MXNet 版本,并根据需要安装,以最大化硬件性能。
作为示例,对于基于 Python 3 的 Linux 发行版,通过 pip3 安装时,以下是可用的 MXNet 版本(请注意是否启用了 CPU 加速和/或 GPU 加速)。
如果你有兴趣了解更多关于英特尔 MKL 的信息,以下链接是一个很好的起点:software.intel.com/content/www/us/en/develop/articles/getting-started-with-intel-optimization-for-mxnet.html。
NumPy 和 MXNet ND 数组
如果你之前在 Python 中处理过数据,很可能已经接触过 NumPy 及其N 维数组(ND 数组)。这些也被称为张量,0 维的变体叫做标量,1 维的变体叫做向量,2 维的变体叫做矩阵。
MXNet 提供了它自己的 ND 数组类型,并且有两种不同的方式来处理它们。一方面,有nd模块,这是 MXNet 本地的、优化的处理 MXNet ND 数组的方式。另一方面,有np模块,它与 NumPy ND 数组类型有相同的接口和语法,并且也经过优化,但由于接口的限制,它的功能受到一定的局限。通过 MXNet ND 数组,我们可以利用其底层引擎,进行如 Intel MKL 和/或 NVIDIA CUDA 等计算优化,如果我们的硬件配置兼容。这意味着我们将能够使用与 NumPy 几乎相同的语法,但通过 MXNet 引擎和我们的 GPU 进行加速,这是 NumPy 所不支持的。
此外,正如我们将在接下来的章节中看到的那样,我们将在 MXNet 上执行的一个非常常见的操作是对这些 ND 数组进行自动微分。通过使用 MXNet ND 数组库,这一操作还将利用我们的硬件,以获得最佳性能。NumPy 本身并不提供自动微分功能。
准备就绪
如果你已经按照之前的步骤安装了 MXNet,那么在执行加速代码方面,使用 MXNet ND 数组之前唯一剩下的步骤就是导入它们的库:
import numpy as np
import mxnet as mx
然而,这里值得注意的是 NumPy ND 数组操作和 MXNet ND 数组操作之间的一个重要根本区别。NumPy 遵循急切求值策略——也就是说,所有操作会在执行时立即求值。相反,MXNet 采用懒惰求值策略,这对于大型计算负载更加优化,实际计算会推迟,直到真正需要这些值时才进行计算。
因此,在比较性能时,我们需要强制 MXNet 在计算所需时间之前完成所有计算。如我们将在示例中看到的那样,调用wait_to_read()函数可以实现这一点。此外,当通过print()或.asnumpy()等函数访问数据时,执行将会在调用这些函数之前完成,这可能会给人一种这些函数实际上很耗时的错误印象:
-
让我们检查一个具体的示例,并从在 CPU 上运行它开始:
import time x_mx_cpu = mx.np.random.rand(1000, 1000, ctx = mx.cpu()) start_time = time.time() mx.np.dot(x_mx_cpu, x_mx_cpu).wait_to_read() print("Time of the operation: ", time.time() - start_time)这将产生类似于以下的输出:
Time of the operation: 0.04673886299133301 -
然而,让我们看看如果没有调用
wait_to_read(),时间测量会发生什么:x_mx_cpu = mx.np.random.rand(1000, 1000, ctx = mx.cpu()) start_time = time.time() x_2 = mx.np.dot(x_mx_cpu, x_mx_cpu) print("(FAKE, MXNet has lazy evaluation)") print("Time of the operation : ", time.time() - start_time) start_time = time.time() print(x_2) print("(FAKE, MXNet has lazy evaluation)") print("Time to display: ", time.time() - start_time)以下将是输出:
(FAKE, MXNet has lazy evaluation) Time of the operation : 0.00118255615234375 [[256.59583 249.70404 249.48639 ... 251.97151 255.06744 255.60669] [255.22629 251.69475 245.7591 ... 252.78784 253.18878 247.78052] [257.54187 254.29262 251.76346 ... 261.0468 268.49127 258.2312 ] ... [256.9957 253.9823 249.59073 ... 256.7088 261.14255 253.37457] [255.94278 248.73282 248.16641 ... 254.39209 252.4108 249.02774] [253.3464 254.55524 250.00716 ... 253.15712 258.53894 255.18658]] (FAKE, MXNet has lazy evaluation) Time to display: 0.042133331298828125
如我们所见,第一个实验表明计算大约花费了 50 毫秒完成;然而,第二个实验表明计算仅花费了约 1 毫秒(少了 50 倍!),而可视化则超过了 40 毫秒。这是一个错误的结果。这是因为我们在第二个实验中错误地衡量了性能。请参阅第一个实验以及调用 wait_to_read() 以正确测量性能。
如何操作...
在本节中,我们将从计算时间的角度比较两个计算密集型操作的性能:
-
矩阵创建
-
矩阵乘法
我们将比较每个操作的五种不同计算配置:
-
使用 NumPy 库(无 CPU 或 GPU 加速)
-
使用 MXNet
np模块进行 CPU 加速,但没有 GPU -
使用 MXNet
np模块进行 CPU 加速和 GPU 加速 -
使用 MXNet
nd模块进行 CPU 加速,但没有 GPU -
使用 MXNet
nd模块进行 CPU 加速和 GPU 加速
最后,我们将绘制结果并得出一些结论。
定时数据结构
我们将在五个字典中存储计算时间,每个计算配置一个字典(timings_np、timings_mx_cpu 和 timings_mx_gpu)。数据结构的初始化如下:
timings_np = {}
timings_mx_np_cpu = {}
timings_mx_np_gpu = {}
timings_mx_nd_cpu = {}
timings_mx_nd_gpu = {}
我们将按不同的顺序运行每个操作(矩阵生成和矩阵乘法),即以下顺序:
matrix_orders = [1, 5, 10, 50, 100, 500, 1000, 5000, 10000]
矩阵创建
我们定义了三个函数来生成矩阵;第一个函数将使用 NumPy 库生成矩阵,并接收矩阵的维度作为输入参数。第二个函数将使用 MXNet np 模块,第三个函数将使用 MXNet nd 模块。对于第二个和第三个函数,输入参数包括矩阵需要创建的上下文,以及矩阵的维度。该上下文指定结果(在此情况下为创建的矩阵)必须在 CPU 或 GPU 上计算(如果有多个设备可用,则指定具体 GPU):
def create_matrix_np(n):
"""
Given n, creates a squared n x n matrix,
with each matrix value taken from a random
uniform distribution between [0, 1].
Returns the created matrix a.
Uses NumPy.
"""
a = np.random.rand(n, n)
return a
def create_matrix_mx(n, ctx=mx.cpu()):
"""
Given n, creates a squared n x n matrix,
with each matrix value taken from a random
uniform distribution between [0, 1].
Returns the created matrix a.
Uses MXNet NumPy syntax and context ctx
"""
a = mx.np.random.rand(n, n, ctx=ctx)
a.wait_to_read()
return a
def create_matrix_mx_nd(n, ctx=mx.cpu()):
"""
Given n, creates a squared n x n matrix,
with each matrix value taken from a random
uniform distribution between [0, 1].
Returns the created matrix a.
Uses MXNet ND native syntax and context ctx
"""
a = mx.nd.random.uniform(shape=(n, n), ctx=ctx)
a.wait_to_read()
return a
为了后续的性能比较存储必要的数据,我们使用之前创建的数据结构,代码如下:
timings_np["create"] = []
for n in matrix_orders:
result = %timeit -o create_matrix_np(n)
timings_np["create"].append(result.best)
timings_mx_np_cpu["create"] = []
for n in matrix_orders:
result = %timeit -o create_matrix_mx_np(n)
timings_mx_np_cpu["create"].append(result.best)
timings_mx_np_gpu["create"] = []
ctx = mx.gpu()
for n in matrix_orders:
result = %timeit -o create_matrix_mx_np(n, ctx)
timings_mx_np_gpu["create"].append(result.best)
timings_mx_nd_cpu["create"] = []
for n in matrix_orders:
result = %timeit -o create_matrix_mx_nd(n)
timings_mx_nd_cpu["create"].append(result.best)
timings_mx_nd_gpu["create"] = []
ctx = mx.gpu()
for n in matrix_orders:
result = %timeit -o create_matrix_mx_nd(n, ctx)
timings_mx_nd_gpu["create"].append(result.best)
矩阵乘法
我们定义了三个函数来计算矩阵的乘法;第一个函数将使用 NumPy 库,并接收要相乘的矩阵作为输入参数。第二个函数将使用 MXNet np 模块,第三个函数将使用 MXNet nd 模块。对于第二个和第三个函数,使用相同的参数。乘法发生的上下文由矩阵创建时的上下文给出;无需添加任何参数。两个矩阵需要在相同的上下文中创建,否则将触发错误:
def multiply_matrix_np(a, b):
"""
Multiplies 2 squared matrixes a and b
and returns the result c.
Uses NumPy.
"""
#c = np.matmul(a, b)
c = np.dot(a, b)
return c
def multiply_matrix_mx_np(a, b):
"""
Multiplies 2 squared matrixes a and b
and returns the result c.
Uses MXNet NumPy syntax.
"""
c = mx.np.dot(a, b)
c.wait_to_read()
return c
def multiply_matrix_mx_nd(a, b):
"""
Multiplies 2 squared matrixes a and b
and returns the result c.
Uses MXNet ND native syntax.
"""
c = mx.nd.dot(a, b)
c.wait_to_read()
return c
为了后续的性能比较存储必要的数据,我们将使用之前创建的数据结构,代码如下:
timings_np["multiply"] = []
for n in matrix_orders:
a = create_matrix_np(n)
b = create_matrix_np(n)
result = %timeit -o multiply_matrix_np(a, b)
timings_np["multiply"].append(result.best)
timings_mx_np_cpu["multiply"] = []
for n in matrix_orders:
a = create_matrix_mx_np(n)
b = create_matrix_mx_np(n)
result = %timeit -o multiply_matrix_mx_np(a, b)
timings_mx_np_cpu["multiply"].append(result.best)
timings_mx_np_gpu["multiply"] = []
ctx = mx.gpu()
for n in matrix_orders:
a = create_matrix_mx_np(n, ctx)
b = create_matrix_mx_np(n, ctx)
result = %timeit -o multiply_matrix_mx_np(a, b)
timings_mx_gpu["multiply"].append(result.best)
timings_mx_nd_cpu["multiply"] = []
for n in matrix_orders:
a = create_matrix_mx_nd(n)
b = create_matrix_mx_nd(n)
result = %timeit -o multiply_matrix_mx_nd(a, b)
timings_mx_nd_cpu["multiply"].append(result.best)
timings_mx_nd_gpu["multiply"] = []
ctx = mx.gpu()
for n in matrix_orders:
a = create_matrix_mx_nd(n, ctx)
b = create_matrix_mx_nd(n, ctx)
result = %timeit -o multiply_matrix_mx_nd(a, b)
timings_mx_nd_gpu["multiply"].append(result.best)
得出结论
在进行任何评估之前的第一步是绘制我们在前面步骤中捕获的数据。在这一步中,我们将使用名为 Matplotlib 的库中的pyplot模块,它可以帮助我们轻松创建图表。以下代码绘制了矩阵生成的运行时间(单位:秒)以及所有计算的矩阵阶数:
import matplotlib.pyplot as plt
fig = plt.figure()
plt.plot(matrix_orders, timings_np["create"], color='red', marker='s')
plt.plot(matrix_orders, timings_mx_np_cpu["create"], color='blue', marker='o')
plt.plot(matrix_orders, timings_mx_np_gpu["create"], color='green', marker='^')
plt.plot(matrix_orders, timings_mx_nd_cpu["create"], color='yellow', marker='p')
plt.plot(matrix_orders, timings_mx_nd_gpu["create"], color='orange', marker='*')
plt.title("Matrix Creation Runtime", fontsize=14)
plt.xlabel("Matrix Order", fontsize=14)
plt.ylabel("Runtime (s)", fontsize=14)
plt.grid(True)
ax = fig.gca()
ax.set_xscale("log")
ax.set_yscale("log")
plt.legend(["NumPy", "MXNet NumPy (CPU)", "MXNet NumPy (GPU)", "MXNet ND (CPU)", "MXNet ND (GPU)"])
plt.show()
与前面的代码块非常相似,以下代码绘制了矩阵乘法的运行时间(单位:秒)以及所有计算的矩阵阶数:
import matplotlib.pyplot as plt
fig = plt.figure()
plt.plot(matrix_orders, timings_np["multiply"], color='red', marker='s')
plt.plot(matrix_orders, timings_mx_np_cpu["multiply"], color='blue', marker='o')
plt.plot(matrix_orders, timings_mx_np_gpu["multiply"], color='green', marker='^')
plt.plot(matrix_orders, timings_mx_nd_cpu["multiply"], color='yellow', marker='p')
plt.plot(matrix_orders, timings_mx_nd_gpu["multiply"], color='orange', marker='*')
plt.title("Matrix Multiplication Runtime", fontsize=14)
plt.xlabel("Matrix Order", fontsize=14)
plt.ylabel("Runtime (s)", fontsize=14)
plt.grid(True)
ax = fig.gca()
ax.set_xscale("log")
ax.set_yscale("log")
plt.legend(["NumPy", "MXNet NumPy (CPU)", "MXNet NumPy (GPU)", "MXNet ND (CPU)", "MXNet ND (GPU)"])
plt.show()
这些是显示的图表(结果会根据硬件配置有所不同):
图 1.5 – 运行时间 – a) 矩阵创建,b) 矩阵乘法
重要提示
请注意,图表的横轴和纵轴都使用了对数刻度(差异比看起来的要大)。此外,实际的数值取决于运行计算的硬件架构;因此,您的具体结果可能会有所不同。
从每个单独的操作和整体上都可以得出几个结论:
-
对于较小的矩阵阶数,使用 NumPy 在这两个操作中都要快得多。这是因为 MXNet 在不同的内存空间中工作,将数据移动到该内存空间的时间比实际计算时间要长。
-
在矩阵创建中,对于较大的矩阵阶数,NumPy(记住,它仅使用 CPU)与 MXNet 在使用 np 模块和 CPU 加速时的差异可以忽略不计,但在使用 nd 模块和 CPU 加速时,速度大约快 2 倍。对于矩阵乘法,取决于您的硬件,MXNet 在 CPU 加速下的速度可以快大约 2 倍(无论使用哪个模块)。这是因为 MXNet 使用 Intel MKL 优化 CPU 计算。
-
在深度学习中有意义的范围内——也就是说,涉及矩阵阶数大于 1,000 的巨大计算负载(这可能代表数据,如由多个百万像素组成的图像或大型语言词典)——GPU 能提供典型的多个数量级的提升(矩阵创建约为 200 倍,矩阵乘法约为 40 倍,随着矩阵阶数的增加,提升呈指数增长)。这是运行深度学习实验时使用 GPU 的最有说服力的理由。
-
在使用 GPU 时,MXNet np 模块在创建矩阵时比 MXNet nd 模块更快(约快 7 倍),但在乘法操作中的差异可以忽略不计。通常,深度学习算法的计算负载更像是乘法操作,因此,事先并没有显著的优势来选择 np 模块或 nd 模块。然而,MXNet 推荐使用原生的 MXNet nd 模块(笔者也认同这个推荐),因为 np 模块的一些操作不被
autograd(MXNet 的自动微分模块)支持。我们将在接下来的章节中看到,当我们训练神经网络时,如何使用autograd模块,以及它为何如此重要。
它是如何工作的……
MXNet 提供了两个优化模块来处理 ND 数组,其中一个是 NumPy 的原地替代品。使用 MXNet ND 数组的优势有两个:
-
MXNet ND 数组操作支持自动微分。正如我们在接下来的章节中将看到的,自动微分是一个关键特性,它允许开发者专注于模型的前向传播,而将反向传播自动推导出来。
-
相反,MXNet ND 数组的操作经过优化,能够充分发挥底层硬件的性能,利用 GPU 加速可以获得显著的效果。我们通过矩阵创建和矩阵乘法来实验验证这一结论。
还有更多内容……
在本节中,我们仅仅触及了 MXNet 使用 ND 数组的一部分。如果你想了解更多关于 MXNet 和 ND 数组的内容,这里是官方 MXNet API 文档的链接:mxnet.apache.org/versions/1.0.0/api/python/ndarray/ndarray.html。
此外,官方 MXNet 文档中还有一个非常有趣的教程:gluon.mxnet.io/chapter01_crashcourse/ndarray.html。
此外,我们还简要了解了如何在 MXNet 上衡量性能。我们将在接下来的章节中再次讨论这个话题;不过,关于该话题的深入讲解可以在官方 MXNet 文档中找到:mxnet.apache.org/versions/1.8.0/api/python/docs/tutorials/performance/backend/profiler.html。
第二章:使用 MXNet 和数据集可视化 – Gluon 和 DataLoader
在上一章中,我们学习了如何设置 MXNet。我们还验证了 MXNet 如何利用我们的硬件以提供最佳性能。在应用深度学习(DL)解决特定问题之前,我们需要了解如何加载、管理和可视化我们将使用的数据集。在本章中,我们将开始使用 MXNet 分析一些玩具数据集,涉及数值回归、数据分类、图像分类和文本分类领域。为了高效处理这些任务,我们将看到新的 MXNet 库和函数,如 Gluon(用于 DL 的 API)和 DataLoader。
在本章中,我们将涵盖以下主题:
-
理解回归数据集 – 加载、管理和可视化House Sales数据集
-
理解分类数据集 – 加载、管理和可视化鸢尾花数据集
-
理解图像数据集 – 加载、管理和可视化时尚-MNIST 数据集
-
理解文本数据集 – 加载、管理和可视化安然电子邮件数据集
技术要求
除了前言中指定的技术要求外,本章不适用其他要求。
本章的代码可以在以下 GitHub URL 找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch02
此外,您可以直接从 Google Colab 访问每个配方;例如,对于本章的第一个配方,请访问colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch02/2_1_Toy_Dataset_for_Regression_Load_Manage_and_Visualize_House_Sales_Dataset.ipynb。
理解回归数据集 – 加载、管理和可视化房屋销售数据集
机器学习(ML)模型的训练过程可以分为三个主要子组:
-
监督学习(SL):至少某些数据的预期输出是已知的
-
无监督学习(UL):预期输出未知,但数据具有某些特征,有助于理解其内部分布
-
强化学习(RL):一个代理探索环境,并根据从环境获取的输入做出决策
还有一种方法介于前两个子组之间,称为弱监督学习(weakly SL),其中没有足够已知的输出来跟随 SL 方法,原因之一是:
-
输出不准确
-
只有部分输出特征是已知的(不完整)
-
它们并非完全符合预期的输出,但与我们打算实现的任务相关联(不精确)
使用 SL,最常见的问题类型之一是回归。在回归问题中,我们希望根据输入特征的数量来估算数值输出。在这个案例中,我们将分析一个来自 Kaggle 的玩具回归数据集:美国金县的房屋销售。
房屋销售数据集呈现了一个问题,即根据以下 19 个特征来估算房价(以$为单位):
-
房屋销售的
Date -
卧室数量 -
浴室数量,其中0.5表示一个有厕所但没有淋浴的房间 -
Sqft_living:公寓内部生活空间的平方英尺数 -
Sqft_lot:土地面积的平方英尺数 -
楼层数量 -
是否有
Waterfront视野 -
该物业视野好坏的指数,范围从 0 到 4
-
房屋状况的指数,范围从 1 到 5
-
Grade:1 到 13 的指数,1 为最差,13 为最好 -
Sqft_above:地面以上的住宅空间的平方英尺数 -
Sqft_basement:地下室内部住宅空间的平方英尺数 -
Yr_built:房屋最初建造的年份 -
Yr_renovated:房屋最后一次翻新的年份 -
Zipcode:房屋所在的邮政编码区域 -
纬度(
Lat) -
经度(
Long) -
Sqft_living15:最近 15 个邻居的住宅内部生活空间的平方英尺数 -
Sqft_lot15:最近 15 个邻居的土地面积的平方英尺数
这些数据特征提供了21,613套房屋及其价格(需估算的值)。
准备工作
以下数据集采用CC0 公共领域许可证提供,可从www.kaggle.com/harlfoxem/housesalesprediction下载。
为了读取数据,我们将使用一个非常著名的库——pandas,并将使用该库中最常见的数据结构,matplotlib,pyplot和seaborn库。因此,我们必须运行以下代码:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
如果你没有安装这些库,可以通过以下终端命令轻松安装:
!pip3 install matplotlib==3.7.1
!pip3 install pandas==1.5.3
!pip3 install seaborn==0.12.2
因此,为了加载数据,我们可以简单地检索包含数据集的文件(该文件可在本书的 GitHub 存储库中找到)并进行处理:
# Retrieve Dataset (House Sales Prices) from GitHub repository for Deep Learning with MXNet Cookbook by Packt
!wget https://github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/raw/main/ch02/kc_house_data.zip
# Uncompress kc_house_data.csv file
!unzip /content/kc_house_data.zip
house_df = pd.read_csv("kc_house_data.csv")
这就是我们开始使用回归数据集所需的一切。
如何操作...
在本节中,我们将进行一次探索性数据分析(EDA),帮助我们理解哪些特征对预测房价很重要(哪些不重要):
-
数据结构
-
相关性研究
-
生活平方英尺分析、地面以上平方英尺分析和邻居生活平方英尺分析
-
等级分析
-
房间(卧室和浴室)分析
-
景观分析
-
年建造和翻新年份分析
-
位置分析
数据结构
让我们分析一下我们的数据是什么样的。为此,我们将使用常见的pandas DataFrame 操作:
house_df.info()
从输出中,我们可以得出以下结论:
-
数据完整(所有列都有 21,613 个值,如预期)。
-
没有
NULL值(数据很干净!)。 -
除了前述的特征,还有一个叫做
id的特征。由于索引已经能够唯一标识每个属性,因此这个特征是不需要的。
为了掌握数值的外观,让我们显示前五个属性:
house_df.head()
到目前为止,我们已经看过了这些特征。现在,让我们看看价格分布:
house_df.hist(column = "price", bins = 24)
plt.show()
这些命令将显示一个价格直方图,显示数据集中有多少房屋具有某个价格(在前面命令中选择的列)。直方图以范围(也称为桶或箱)工作;在我们的情况下,我们选择了 24 个。因为最大价格为 8 百万美元,所以在应用 24 个范围时,我们每百万美元有 3 个范围,具体来说是(所有值以百万美元为单位):[0 – 0.33)、[0.33 - 0.66)、[0.66 - 1),直到[7.66 - 8]。
以下是输出结果:
图 2.1 – 价格分布
相关性研究
在这里,我们将分析每个特征之间的相关性,尤其是每个特征与价格的相关性。
首先,正如之前讨论的那样,我们将删除id特征:
house_df = house_df.drop(["id"], axis=1)
我们现在可以计算成对相关性图表:
house_corr = house_df.corr()
为了更直观地显示计算出的相关性,我们将绘制一个热力图:
plt.figure(figsize=(20, 10))
colormap = sns.color_palette("rocket_r", as_cmap=True)
sns.heatmap(house_corr, annot=True, cmap=colormap)
plt.show()
这些代码语句产生以下结果:
图 2.2 – 房屋特征相关矩阵
在图 2.2中请注意,单元格越暗,相关值越大。
为了强调第一行(它显示了价格与输入特征之间的关系最重要),我们将运行以下代码:
house_corr["price"].drop(["price"]).sort_values(ascending = False).plot.bar(figsize=(5,5))
plt.show()
我们得到以下结果:
图 2.3 – 房屋特征:价格相关性
从图 2.2和2.3可以得出以下结论:
-
生活面积和等级是与价格最高度相关的特征(分别为 0.7 和 0.67) -
上方平方英尺和邻居的生活面积与生活面积高度相关(分别为 0.88 和 0.76,这指向一定程度的冗余) -
每种房间类型的数量有以下相关系数:
-
浴室数量:0.53
-
卧室数量:0.31
-
楼层数量:0.26
-
-
视图、水滨和翻新年份与价格有一定的相关性(分别为 0.4、0.27 和 0.13) -
位置与价格相关,其中纬度是最重要的位置特征(0.31) -
其余的特征似乎对房产价格的贡献不大
因此,从初步分析来看,与价格最相关的特征按重要性排序依次为:生活面积、等级、浴室数量、视图和纬度。
在接下来的部分,我们将确认这些初步结论。
平方英尺分析
从相关性图中,我们发现居住面积与价格之间有很强的相关性(正如预期的那样),同时楼上面积与邻居居住面积也存在潜在的冗余性。为了更详细地分析这一点,我们将每个变量与价格的关系绘制出来:
图 2.4 – 价格与多个特征的比较:a)居住面积,b)楼上面积,c)邻居居住面积
正如预期,绘制的图表非常相似,表明这些变量之间存在高度的相关性(以及冗余性)。此外,我们还可以观察到,数据点的最大密度出现在价格低于 300 万美元和面积小于 5,000 平方英尺的区域。由于我们大部分数据位于这些区域,我们可以将这些范围之外的房屋视为异常值并将其移除。
等级分析
类似地,我们可以将等级特征与价格进行比较:
图 2.5 – 房屋等级与价格的关系
等级与价格之间有明显的直接相关性;等级越高,价格越高。值得注意的是,等级最高的房屋出现频率较低。
房间分析
让我们更详细地展示价格与楼层数、卧室数和浴室数之间的关系:
图 2.6 – 价格与多个特征的比较:a)楼层数,b)卧室数,c)浴室数
从图中可以观察到以下几点:
-
在*图 2**.6 (a)*中,我们可以看到,对于较少的楼层数(1-3 层),房价与楼层数之间有直接的相关性。然而,从第四层开始,这种相关性消失了,这表明该段数据缺乏样本(四层或更多层的房子较为少见)。
-
在*图 2**.6 (b)*中,与卧室数量的比较情况与前一个楼层数量的比较图表相似。我们可以看到,对于较少卧室的房子,房价与卧室数之间有直接的相关性。然而,从四间卧室开始,这种相关性消失,其他特征需要被考虑进去。
重要提示
仔细查看数据时,你会发现,在索引为15870的那一行中,存在一个异常值;那是一栋有 33 间卧室的房子。我不知道这是否为房子的实际卧室数(我预计不是!),但为了正确分析数据集,这栋房子作为异常值已被从中移除。详情请查看代码。
- 在*图 2**.6 (c)*中,我们可以看到浴室数量与价格之间有直接的相关性;然而,也存在一定的不确定性(随着浴室数量的增加,图表变得更宽)。
视野分析
在本节中,我们将更详细地分析视野质量和水滨视野(房屋是否有水滨视野)与价格之间的联系:
图 2.7 – 视野质量(a)和水滨视野(b)与价格的关系
从这些单独的图表中,得出结论稍微有些困难。似乎还需要其他变量来看到视野质量与价格之间的明显联系,水滨视野也是如此。
建造年份和翻新年份分析
以下图表展示了房屋建造年份和是否以及何时翻新的特征与价格的相关性:
图 2.8 – 价格与建造年份(a)和翻新(b)的比较
从图中,你可以观察到以下几点:
-
在*图 2.8(a)*中,我们可以看到价格呈现轻微的线性上升,表明房屋建造得越新,价格就越贵。
-
对于图 2.8(b),我们没有分析年份,而是将数据集分为两类——翻新过的房屋和未翻新过的房屋——并将这两类与价格进行对比。无论如何,这样做得出结论稍微有些困难。似乎还需要其他变量来看到翻新年份与价格之间的明显联系。
位置分析
在本节中,我们将更详细地分析纬度和经度与价格之间的联系:
图 2.9 – 位置与价格的关系
从图 2.9中,我们可以得出结论:位置在房屋价格中起着重要作用。显然,金县的北部地区比南部地区更为高价。而且有一个特定的中心区域,这里的房屋比附近的其他地区明显更贵。
它是如何工作的……
回归问题是应用 SL 方法的最常见问题之一。通过深入研究一个经典的回归数据集——金县房价预测,我们可以发现输入特征(建筑面积、等级和浴室数量)与输出特征(价格)之间最重要的联系。这项分析将帮助我们在下一章构建一个预测房价的模型。
还有更多……
在本节中,我们专注于每个特征与价格的单独分析。然而,有些特征在与其他特征结合或经过预处理后,更容易理解。我们对这个主题做了一个简单的探索,通过将已经翻新的房屋归为一类,并与未翻新房屋的类别进行对比。此外,在位置分析中,我们使用了二维地图绘制纬度和经度,以发现模式。
然而,还有很多关系和分析需要完成,我建议你自己探索这个数据集,提出自己的假设或直觉,并分析数据以发现新的见解。
此外,还有许多其他回归数据集可以进行练习;一个小建议可以在这里找到:www.kaggle.com/rtatman/datasets-for-regression-analysis。
理解分类数据集——加载、管理和可视化鸢尾花数据集
在前一个教程中,我们学习了监督学习(SL)中最常见的一个问题类型:回归。在本教程中,我们将更深入地研究另一个常见的问题类型:分类。
在分类问题中,我们希望从一组给定的类别中,使用一定数量的输入特征来估计一个类别输出。在本教程中,我们将分析一个来自 Kaggle 的玩具分类数据集:鸢尾花数据集,它是最著名的分类数据集之一。
鸢尾花数据集呈现了从三种鸢尾花类别(鸢尾花 Setosa、鸢尾花 Versicolor 和鸢尾花 Virginica)中估计花卉类别(iris)的问题,利用以下四个特征:
-
花萼长度(单位:厘米)
-
花萼宽度(单位:厘米)
-
花瓣长度(单位:厘米)
-
花瓣宽度(单位:厘米)
这些数据特征是为 150 朵花提供的,每个类别有 50 个实例(使其成为一个平衡的数据集)。
准备工作
该数据集在 CC0 公共领域 许可下提供,可以从 www.kaggle.com/uciml/iris 下载。
为了读取、管理和可视化数据,我们将采取类似于前一个教程中玩具回归数据集的方法。我们将使用 pandas 来管理数据,并使用该库最常见的数据结构:数据框(DataFrames)。此外,为了绘制数据及我们将计算的多个可视化图形,我们将使用 matplotlib、pyplot 和 seaborn 库。因此,我们必须运行以下代码:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
为了加载数据,我们将引入一个非常有用的库,名为 scikit-learn,它非常适合管理数据集。这个库预装了一组数据集,其中包括鸢尾花数据集:
from sklearn import datasets
如果你没有安装之前提到的库,可以使用以下终端命令轻松安装:
!pip3 install matplotlib
!pip3 install pandas
!pip3 install seaborn
!pip3 install scikit-learn
因此,为了加载数据,我们可以简单地通过使用 scikit-learn 库函数来读取数据集:
iris = datasets.load_iris()
iris_df = pd.DataFrame(iris.data, columns = iris.feature_names)
iris_df.insert(0, "class", iris.target)
这就是我们开始处理分类数据集所需的一切。
如何进行操作...
在本节中,我们将进行一个探索性数据分析(EDA),帮助我们理解哪些特征对于预测花卉的鸢尾花类别很重要(哪些不重要),通过完成以下任务:
-
数据结构
-
相关性研究
-
一对一比较(配对图)
-
小提琴图
数据结构
让我们分析一下数据的结构。为此,我们将使用在 pandas 数据框中的常见操作:
iris_df.info()
从输出中,我们可以得出以下结论:
-
数据是完整的(所有列都有 150 个值,符合预期)
-
没有
NULL值(数据是干净的!)
为了了解这些值的样子,让我们显示前五个属性:
iris_df.head()
到目前为止,我们看了特征的表现。现在,让我们看看鸢尾花类别的分布情况:
iris.target_names
这将产生以下输出:
array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
如果我们想确认每个类别有 50 个实例,可以运行以下代码:
iris_df.groupby("class").size()
这将产生以下输出:
Class
0 50
1 50
2 50
dtype: int64
这里,0对应setosa,1对应versicolor,2对应virginica。
相关性研究
在这里,我们将分析每个特征之间的相关性,最重要的是,每个特征与鸢尾花类别之间的相关性。
我们可以计算成对相关性图:
iris_corr = iris_df.corr()
为了便于可视化计算出的相关性,我们将绘制热图:
plt.figure(figsize=(10, 10))
colormap = sns.color_palette("rocket_r", as_cmap=True)
sns.heatmap(iris_corr, annot=True, cmap=colormap)
plt.show()
这些代码语句产生了以下结果:
图 2.10 – 花朵特征相关性矩阵
请注意在图 2.10中,单元格越深,相关性值越大。
为了强调第一行(最重要的是它显示了鸢尾花类别与输入特征之间的关系),我们将运行以下代码:
iris_corr["class"].drop(["class"]).sort_values(
ascending = False).plot.bar(figsize=(5,5))
plt.show()
这是我们得到的结果:
图 2.11 – 花朵特征:鸢尾花类别相关性
从图 2.10和图 2.11中可以得出以下结论:
-
花瓣的测量(长度和宽度)高度相关;分析和训练这两个特征可能不会带来额外的信息。
-
花瓣的测量值是与鸢尾花类别相关性最强的特征。
-
萼片长度和宽度也高度相关,但方向相反(萼片长度是正相关的,而萼片宽度是负相关的)。
一对一比较(成对图)
在分类问题中,色调/亮度可以用来指示图表的类别。此外,由于在这个数据集中我们只能使用有限的特征集(四个特征),成对图将非常有用,可以在单个图表中比较所有特征。绘制此图的代码如下所示:
g = sns.pairplot(iris_df, hue="class", height=2, palette="rocket_r")
handles = g._legend_data.values()
labels = list(iris.target_names)
g._legend.remove()
g.fig.legend(handles=handles, labels=labels, loc='upper left', ncol=3)
这是显示的图表:
图 2.12 – 花朵特征对比图
从这组图表中,我们可以得出以下结论:
-
Setosa 鸢尾花可以通过任何特征轻松区分
-
不同鸢尾花类别之间的萼片特征有重叠
-
花瓣特征与鸢尾花类别直接相关;也就是说,最小的数值指向setosa,中等的数值指向versicolor,而最大的数值指向virginica
-
在versicolor和virginica的边界上存在一个重叠区域,当花瓣长度大于约 5 厘米且花瓣宽度大于约 1.5 厘米时,两个类别会重叠。
小提琴图
另一个可能帮助理解特征与鸢尾花类别之间关系的图是小提琴图。生成此图的代码如下:
fig, axs = plt.subplots(2, 1)
sns.violinplot(x="class", y="petal length (cm)", data=iris_df, size=5, palette='rocket_r', ax = axs[0])
sns.violinplot(x="class", y="petal width (cm)", data=iris_df, size=5, palette='rocket_r', ax = axs[1])
下面是展示的图表:
图 2.13 – 花卉特征小提琴图
在这些图中,我们得出的结论更加清晰,setosa(0)鸢尾花类别明显可分离,而 versicolor(1)和 virginica(2)则有相当大的重叠。
小提琴图还提供了我们数据值分布的指示(从 0 开始,以匹配代码中类别的索引):
-
Setosa:值更可能出现在均值附近(大约为花瓣长度 1.5 cm 和花瓣宽度 0.25 cm)。
-
Versicolor:正态分布,均值大约为 4.25 cm 和 1.3 cm,标准差分别为 0.5 cm 和 0.2 cm(对应花瓣长度和花瓣宽度)。
-
Virginica:在[~5.1, ~5.9] cm 和[~1.8, ~2.3] cm 之间呈均匀分布(分别对应花瓣长度和花瓣宽度)。
它是如何工作的...
分类问题是监督式机器学习(SML)方法应用最广泛的问题之一。通过深入研究经典的分类数据集——鸢尾花类别,我们可以发现输入特征(花瓣长度和花瓣宽度)与输出特征(鸢尾花类别)之间的关联。这一分析将帮助我们在下一章中构建模型来预测类别。
还有更多...
在本节中,我们重点分析了每个特征与鸢尾花类别之间的关系。这在原则上类似于回归数据集的分析,且每个图都有额外的信息,即色调/亮度。我们建议读者继续自己进行分析,以发现新的洞察。
我们提到鸢尾花数据集是经典的分类数据集之一,然而,它的历史可以追溯到 1936 年!原始参考文献:onlinelibrary.wiley.com/doi/pdf/10.1111/j.1469-1809.1936.tb02137.x。
此外,正如我们将在下一章探讨的那样,分类问题可以视为回归问题的特殊情况。在回归案例中,我们研究了房价,并通过将其与阈值进行比较(例如我们的预算限额)来帮助买家判断哪些是可负担的。因此,我们可以使用该阈值将低于阈值的房子分类为可负担的,将高于阈值的房子分类为不可负担的。我们将在下一章深入探讨这一联系。
此外,还有许多其他分类数据集可以使用;可以在这里找到一小部分:www.kaggle.com/search?q=classification+tags%3Aclassification。
理解图像数据集——加载、管理和可视化 Fashion-MNIST 数据集
在过去几年中,计算机视觉(CV)是深度学习领域增长显著的一个方向。自 2012 年 AlexNet 革命以来,计算机视觉从实验室研究扩展到在真实世界数据集(即“野外”数据集)中超越了人类表现。
在这个教程中,我们将探讨最简单的计算机视觉任务:图像分类。给定一组图像,我们的任务是将这些图像正确地分类到预定的标签(类别)中。
最经典的图像分类数据集之一是MNIST(即修改版国家标准与技术研究院)数据库。同样大小,但更适合当前的计算机视觉分析的是Fashion-MNIST 数据集。这个数据集是一个多标签图像分类数据集,训练集包含 60k 个示例,测试集包含 10k 个示例,每个示例属于这 10 个类别之一(从 0 开始,以匹配代码中的类别索引):
-
T 恤/上衣
-
长裤
-
套头衫
-
连衣裙
-
外套
-
凉鞋
-
衬衫
-
运动鞋
-
包
-
踝靴
每张图像为灰度图,尺寸为 28x28 像素。这可以看作每个数据点具有 784 个特征。该数据集由每个类别 6k 张图像的训练集和每个类别 1k 张图像的测试集组成(平衡数据集)。
准备工作
这个数据集提供在MIT许可证下,可以从以下网址下载:github.com/zalandoresearch/fashion-mnist
这个数据集可以直接通过 MXNet Gluon 获取,因此我们将使用这个库来访问它。此外,由于这个数据集比我们迄今为止探索的其他数据集要大得多,为了高效处理数据,我们将使用 Gluon DataLoader 功能:
from mxnet import gluon
training_data_raw = gluon.data.vision.FashionMNIST(train=True)
test_data_raw = gluon.data.vision.FashionMNIST(train=False)
提示
Gluon 随 MXNet 一起安装;无需其他步骤。
这就是我们开始使用 Fashion-MNIST 数据集所需的所有内容。
重要提示
有时候,数据需要为某些操作进行修改(转化)。这可以通过定义一个transform函数并将其作为参数(transform=<function_name>)传递来完成。
如何操作...
在本节中,我们将进行一次探索性数据分析(EDA),帮助我们理解哪些特征对于预测服装类别是重要的(哪些是不重要的),以下是帮助我们进行分析的步骤:
-
确定数据结构
-
描述每个类别的示例
-
理解降维技术
-
可视化主成分 分析(PCA)
-
可视化t 分布随机邻域 嵌入(t-SNE)
-
可视化统一流形近似与 投影(UMAP)
-
可视化Python 最小失真 嵌入(PyMDE)
确定数据结构
为了优化内存使用,以便处理大规模数据集,通常不是将完整的数据集加载到内存中,而是通过批次访问数据集,批次是较小的数据包。
Gluon 有自己生成批次的方式,同时应用128:
batch_size = 128
training_data_aux = gluon.data.DataLoader(
training_data_raw, batch_size= batch_size, shuffle=True)
test_data_aux = gluon.data.DataLoader(
test_data_raw, batch_size= batch_size, shuffle=False)
重要提示
DataLoader 不会返回数据结构,而是返回一个迭代器。因此,为了访问数据,我们需要对其进行迭代,使用诸如for循环等构造。
让我们验证数据结构是否符合预期:
training_data_size = 0
for X_batch, y_batch in training_data_aux:
if not training_data_size:
print("X_batch has shape {}, and y_batch has shape {}" .format(X_batch.shape, y_batch.shape))
training_data_size += X_batch.shape[0]
print("Training Dataset Samples: {}".format(training_data_size))
test_data_size = 0
for X_batch, y_batch in test_data_aux:
test_data_size += X_batch.shape[0]
print("Test Dataset Samples: {}".format(test_data_size))
我们得到了预期的输出:
X_batch has shape (128, 28, 28, 1), and y_batch has shape (128,)
Training Dataset Samples: 60000
Test Dataset Samples: 10000
重要提示
Gluon 加载灰度图像时会将其视为具有一个通道的图像,每个批次的维度为(批次大小,高度,宽度,通道数);在我们的示例中是(128,28,28,1)。
每个类别的示例描述
Fashion-MNIST 数据集是一个平衡的数据集,每个类别有 6k 个示例:
图 2.14 – Fashion-MNIST 数据集标签
让我们看看每个类别的样本长什么样。为了实现这一点,我们可以绘制每个类别的 10 个示例:
图 2.15 – Fashion-MNIST 数据集
如我们在图 2.15中看到的那样,所有实例几乎都可以被人类很好地区分,除了T-shirt/top、Pullover、Coat和Shirt类别。
理解降维技术
除了数据集中包含的大量数据点外,图像中的特征数量(即每个图像的像素数)也非常高。在我们的玩具数据集中,每张图像有 784 个特征,可以看作是 784 维空间中的 1 个点。在这个空间中,分析特征之间的关系(例如我们在前面的数据集中探索的相关性)是非常困难的。此外,处理更高质量的图像(分辨率超过 1 百万像素,即超过 100 万个特征)并不罕见。对于一张 4K 图像,特征数量约为 800 万个。
因此,在本小节以及接下来的小节(关于PCA、t-SNE、UMAP和PyMDE),我们将使用称为降维的技术。降维技术的核心思想是能够轻松地可视化高维特征,通常是 2D 或 3D,这是人类习惯使用的可视化方式。这些嵌入具有两个或三个组件,可以在 2D 或 3D 中绘制。这些表示是数据集依赖的;它们是学习得到的表示。
每种技术都有不同的方式来实现这一结果。在本书中,我们不会深入了解每种技术的原理,但感兴趣的读者可以在There’s *more...*部分找到更多信息。
还请注意,尽管每种技术不同,但它们都要求输入一个向量(特征向量)。这意味着一些空间信息会丢失。在我们的示例中,从 28x28 的图像中,我们将输入 784 个特征向量。
可视化 PCA
正如预期的那样,我们可以看到一些大簇(Sneaker 和 Ankle boot),而其他簇大多是重叠的(T-shirt、Pullover 和 Coat):
图 2.16 – Fashion-MNIST 2D PCA
可视化 t-SNE
另一种降维技术是 t-SNE。该技术基于计算表示邻居相似性的概率分布。推荐的预处理步骤是先对 50 个特征进行 PCA,然后将这 50 个特征向量传递给 t-SNE 算法。这就是我们用来生成以下图表的方法:
图 2.17 – Fashion-MNIST 2D t-SNE
在这个图中,我们可以更清楚地看到,如何将易于区分的对象孤立成簇(Trouser 在右下角,Bag 在左上角)。
重要提示
对于 PCA 和 t-SNE,我们可以选择三个主成分而不是两个,这样会生成一个 3D 图。关于代码,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook。
可视化 UMAP
另一种降维方法是 UMAP。UMAP 允许我们调整不同的参数,比如邻居的数量,这有助于我们可视化如何平衡局部结构与全局结构。以下是五个邻居的可视化示例:
图 2.18 – Fashion-MNIST UMA
在此可视化中,我们可以观察到与前面图表中类似的趋势;即,Bag 被聚集在上中部区域,Trouser 被聚集在下中部区域。然而,在此可视化中,我们还可以注意到左侧有一个簇,包含了Ankle boot、Sneaker 和 Sandal的数据,而右侧有一个重要的簇,包含了Shirt、Coat、Dress 和 T-shirt/top的数据,我们可以看到这些簇是如何相互重叠的。
要安装 UMAP,请运行以下命令:
!pip3 install umap-learn
可视化 PyMDE
另一种流行的技术是 PyMDE,它提供了有洞察力的可视化。PyMDE 允许两种主要方法:保持邻居关系(即保留数据的局部结构)和保持距离关系。这保持了数据中一对一距离等关系属性。保持邻居关系的方法类似于我们所看到的图表:
图 2.19 – Fashion-MNIST PyMDE
如我们在图 2.19中所见,PyMDE 可得出与 UMAP 非常相似的结论。
要安装 UMAP,请运行以下命令:
!pip3 install pymde
它是如何工作的...
要理解一个图像数据集,我们需要理解该数据集中图像之间的潜在联系。实现这一目标的一种有用方法是使用不同的可视化技术。
在这个教程中,我们学习了如何发现图像数据集中的模式。我们选择了一个经过充分研究的数据集——Fashion-MNIST,并学习了处理大规模数据集的最重要方法之一:批处理。
我们通过查看数据集的内部结构以及实际图像的样貌来分析数据集,并尝试预测潜在的分类算法可能遇到的问题(例如外套和衬衫、短靴和运动鞋之间的相似性)。
每个像素都是每张图像的一个维度/特征,因此,为了处理它们,我们了解了一些降维技术:PCA、t-SNE、UMAP 和 PyMDE。通过这些可视化,我们能够验证并扩展我们对数据集的知识。
还有更多…
由于 MNIST 和 Fashion-MNIST 是经过充分研究的数据集,因此有许多资源可供参考。我个人推荐以下资源:
-
MNIST 数据库:
en.wikipedia.org/wiki/MNIST_database -
zalandoresearch/fashion-mnist:
github.com/zalandoresearch/fashion-mnist
我们介绍了一些降维技术,但并没有深入了解它们。如果你想更好地理解每种技术的工作原理,我建议以下资源:
-
PCA(来自 加州理工学院):
web.ipac.caltech.edu/staff/fmasci/home/astro_refs/PrincipalComponentAnalysis.pdf -
t-SNE:
lvdmaaten.github.io/tsne/ -
PyMDE:
pymde.org/
在代码中,你可以找到如何获取包含的可视化内容。此外,对于 PCA 和 t-SNE,由于组件数是一个变量,因此两者的 3D 图都被包含在内。
最后,对于那些有兴趣深入了解深度学习及其历史的读者,我推荐以下链接:www.skynettoday.com/overviews/neural-net-history。
理解文本数据集 – 加载、管理和可视化 Enron 邮件数据集
近年来,自然语言处理(NLP)是深度学习领域一个快速发展的领域。与计算机视觉(CV)类似,该领域的目标是在人类表现基础上超越现实世界数据集的表现。
在这个教程中,我们将探索最简单的 NLP 任务之一:文本分类。给定一组句子和段落,我们的任务是正确地将这些文本分类到给定的标签(类别)中。
最经典的文本分类任务之一是区分收到的邮件是否是垃圾邮件(spam)或非垃圾邮件(ham)。这些数据集是二元文本分类数据集(只有两个标签要分配,0 和 1,或者 ham 和 spam)。
在我们的特定场景中,我们将使用一个真实世界的邮件数据集。该数据集是在 2000 年代初美国政府对安然丑闻进行调查时公开的。这一数据集首次发布于 2004 年,包含约 150 名用户的邮件,主要是安然公司高级管理层的邮件。本节仅使用其中的一个子集(称为enron1)。
数据集包含 5,171 封邮件,没有训练/测试集划分(所有示例都提供标签)。作为一个真实世界的数据集,邮件在主题、内容长度、词数和单词长度等方面差异很大,且默认情况下,数据集仅包含两个特征:
-
0表示正常邮件,1表示垃圾邮件 -
文本:包括邮件的主题和正文
数据集包含 3,672 封正常邮件(约占 70%)和 1,499 封垃圾邮件(约占 30%);它是一个高度不平衡的数据集。
准备工作
该数据集遵循CC0 公共领域许可证,可以从www.kaggle.com/venky73/spam-mails-dataset 下载。
为了读取数据,我们将遵循与回归任务中类似的方法。我们将从 CSV 文件加载数据,并使用非常著名的 Python 库:pandas、pyplot 和 seaborn 来处理数据。因此,我们需要运行以下代码:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
因此,为了加载数据,我们只需读取包含数据的文件(该文件可以从书籍的 GitHub 仓库中找到):
emails_df = pd.read_csv("spam_ham_dataset.csv")
这就是我们开始处理垃圾邮件数据集所需的全部内容。
如何执行...
在本节中,我们将进行探索性数据分析(EDA),帮助我们理解哪些特征对于预测邮件是否为垃圾邮件至关重要(哪些不重要)。以下内容不是以步骤形式呈现,请重新组织为更合适的引言或以步骤形式重新编排,并将圆点符号改为编号:
-
数据结构
-
每个类别的示例
-
内容分析
-
数据清洗
-
N-gram 模型
-
词处理(分词、停用词、词干提取和词形还原)
-
词云
-
词嵌入(word2vec 和 全局词表示模型 (GloVe))
-
主成分分析(PCA)和 t-SNE
数据结构
我们将要进行的第一步是根据我们的需求重新格式化数据集:
# Removing Unnecessary column
emails_df.drop("Unnamed: 0", axis=1, inplace=True)
# Changing column names
emails_df.columns = ["label", "text", "class"]
经这些修改后,我们的邮件数据框架(DataFrame)形状如下:
(5171, 3)
每个类别的示例
接下来,我们将查看每个类别的分布:
Label
ham 3672
spam 1499
dtype: int64
以下是输出结果:
图 2.20 – 垃圾邮件数据集
如我们所见,数据集存在严重的类别不平衡问题。
内容分析
在这一部分,我们将分析邮件的长度及其分布:
图 2.21 – 邮件长度
存在一个大范围的异常值集,对应邮件字符数超过 5,000 的情况。让我们聚焦于大多数邮件所在的区域,并绘制邮件长度和单词数的图表:
图 2.22 – 邮件长度(详细)
图 2.23 – 邮件词汇数量
重要提示
在图 2.23中,我们通过指定每个以空格分隔的实体构成一个词,定义了一个没有语义或字典方法的词汇。这种方法有缺点,我们将在本节和第五章中进一步分析。
通过查看该图,我们可以得出结论:在邮件长度和单词数方面,垃圾邮件和合法邮件之间没有显著差异。我们需要更多地了解这些词汇,它们的含义及其之间的关系,以改善我们的分析。
因此,让我们首先看看数据集中哪些词最常见:
图 2.24 – 最常见的词汇
看到图 2.24后的第一个也是最重要的结论是,我们最初的空格分隔方法在处理真实世界的数据集时并不足够。标点符号错误和拼写错误非常常见,而且,正如预期的那样,像“the”和“to”这样的常见词对于区分垃圾邮件和合法邮件没有实质性帮助。
数据清理
让我们解决处理真实世界文本数据集时的一些常见问题:
-
标点符号
-
尾随字符
-
“说明”(方括号中的文本)
-
包含数字和链接的词汇
-
词汇subject(特定于我们的邮件数据集结构)
经过我们通过清理函数处理语料库(特定于我们问题的文本数据)后,结果更接近我们的预期:
图 2.25 – 最常见的词汇(清理过)
在图 2.25中,我们可以看到我们正在分析的新词汇语料库包含了真实的词汇。然而,很明显,最常见的词汇并未帮助区分垃圾邮件和合法邮件;像“the”和“to”这样的词在英语中过于常见,无法有效用于此分类。
N-gram
在自然语言处理(NLP)中,语料库中的 N-gram 是指语料库中一组同时出现的 N个词。通常在 NLP 中,最常见的 N-gram 是unigrams(一个词)、bigrams(两个词)和trigrams(三个词)。绘制最常见的 N-gram 有助于我们理解词语与类别(垃圾邮件或非垃圾邮件)之间的关系。Unigram 只是最常见的词的图形,如在前面的图 2.25中所绘制。对于 bigrams(按类别),请参见下文:
图 2.26 – 在“ham”(a)和“spam”(b)中最常见的二元组
对于 trigrams(按类别),请参见下文:
图 2.27 – 正常邮件(a)和垃圾邮件(b)中最常见的三元组
在这些图表中,我们可以开始把握两类之间的潜在差异:
-
如果提到了Enron 公司,那很可能是一封合法的电子邮件。
-
如果有礼貌的行动号召(“please let me know”),那很可能是一封合法的电子邮件。
-
如果有链接,那很可能是垃圾邮件。
-
如果有拼写错误(hou代替how,ect代替etc,等等),那很可能是一封合法的电子邮件。
-
如果提到pills(药丸),那很可能是垃圾邮件(而且有重复的嫌疑)。
我们还发现了一些与电子邮件编码方式相关的细微差别:nbsp(用于非断行空格)。很可能是电子邮件解析器在文本中发现了一些结构不清的空格,并用nbsp关键字进行了替换。巧合的是,这些解析细节在垃圾邮件中比在合法邮件中出现得要多,这将有助于我们的分析。
文字处理
处理文本中的单词通常由四个步骤组成:
-
分词
-
停用词过滤
-
词干提取
-
词形还原
这些步骤各自具有一定的复杂性,因此我们将使用可用的库来执行这些步骤,例如自然语言工具包(NLTK)。要安装它,请运行以下命令:
!pip3 install nltk
分词是处理文本并返回标记列表的步骤。每个单词是一个标记,但如果有标点符号,它们会成为单独的标记。不过,对于我们的语料库,这些在之前的步骤中已被移除。
请注意,在这个步骤中,我们已经从每封电子邮件的句子和段落列表(语料库)转换到所谓的词袋模型(BOW),它与语料库中使用的词汇直接相关。
在我们将每个单词作为一个实体之后,我们可以移除之前已识别出的常见词,如“the”或“to”。这些被称为停用词,NLTK 包含多个语言的停用词集。我们将使用这个可用的集合来过滤我们的语料库。
词干提取是将派生(如果我们想更正式些,也包括屈折)词汇缩减到其词根的过程,词根称为词干。
词形还原是将多个不同形式的词汇归类为一个单一项的过程,这个单一项由单词的词根或词典形式(lemma)标识:
图 2.28 – 词干提取和词形还原
经过这些步骤处理后,我们的词袋模型中剩下的单词数量大约是语料库的 10%:
Raw Corpus (Ham): 3133632
Processed Corpus (Ham): 317496 (~10%)
Raw Corpus (Spam): 1712737
Processed Corpus (Spam): 177780 (~10%)
词云
使用我们后处理的词袋模型(BOW),我们可以生成文本语料库的最具影响力和最受欢迎的可视化图像之一——词云:
图 2.29 – 正常邮件(a)和垃圾邮件(b)中的词云
在这些可视化中,我们可以清楚地看到Enron、please和let know对合法邮件是相关的,而new、nbsp、compani、market和product通常与垃圾邮件相关。
词嵌入
到目前为止,我们已经了解了单个单词的表现(频率和长度)及其与其他单词的连接,主要通过最常见的 N-gram(双字组和三字组)。然而,我们还没有将这些单词之间的意义进行连接。例如,我们会预期Enron、corp和company(compani,它的词干形式)在语义上是相近的。因此,我们希望有一种表示方式,使得具有相似意义的单词有相似的表示方式。此外,如果这种表示方式具有固定数量的维度,我们就能方便地对单词进行比较(找出相似性)。这些就是词嵌入(word embeddings),其表示就是一个向量。
从单词生成向量的方法有无数种;例如,实现这一目标的最简单方法是生成与我们的词汇量相同数量的维度(向量的特征,即矩阵表示中的列),然后在每封邮件中(矩阵表示中的一行),对于每个包含的单词,我们可以在该单词在词汇表中的列中标注1(打钩),从而得到形如[0, 0, 0, 0......, 1, 0, 0, 0,..., 1....]的向量表示。这种表示方法称为独热编码,但它效率非常低,因为特征的数量等于语料库中不同单词的数量,也就是词汇表的长度,这通常是非常大的(我们的简化词汇表约有 50 万个单词)。
因此,我们将看一看更优化的单词表示方式:word2vec 和 GloVe:
-
word2vec:该算法由谷歌于 2013 年开发,并在Google News语料库上进行了预训练。它的语料库包含 30 亿个单词,词汇量有 300 万个不同的单词,每个单词用 300 个特征表示。该算法的直观思路是通过考虑上下文(周围单词)来计算给定单词的概率。窗口大小(一次查看多少个单词)是模型的一个参数,并且是常量,这使得模型仅依赖于每个单词的局部上下文。
-
word2vec结合了单词共现(全局统计)来提供更完整的表示。
通过这些表示,我们现在可以在单词的新的向量表示中计算操作:
-
stronger与strong的关系就像weaker与weak的关系:math_weaker = w2v["stronger"] - w2v["strong"] + w2v["weak"] np.linalg.norm(math_weaker - w2v["weaker"])这将生成约为
~1.9的输出,结果接近。 -
king:[('kings', 0.7138046026229858), ('queen', 0.6510956883430481), ('monarch', 0.6413194537162781), ('crown_prince', 0.6204220056533813), ('prince', 0.6159993410110474), ('sultan', 0.5864822864532471), ('ruler', 0.5797567367553711), ('princes', 0.5646552443504333), ('Prince_Paras', 0.543294370174408), ('throne', 0.5422104597091675)]
细心的读者会意识到,词嵌入与我们在前一个维度降维的例子中看到的技术相似,因为那也是学习到的表示。然而,在这种情况下,我们实际上是增加了维度,以获得新的优势(固定数量的特征和相似的意义表示)。
重要提示
词嵌入通常是学习出来的表示;也就是说,这些表示通过训练来最小化具有相似含义的词语之间的距离,或者在我们的案例中,是通过相同标签分类的词语。在这个食谱中,我们将使用word2vec和 GloVe 的预训练表示,在第五章中,我们将学习如何进行训练。
PCA 和 t-SNE
正如“子章节”中所讨论的那样,我们当前的嵌入包含 300 个特征(word2vec)或 50 个特征(GloVe)。为了进行合适的可视化,我们需要应用降维技术,正如我们在之前关于计算机视觉的食谱中所看到的那样。
对于这个数据集,我们可以应用 PCA:
图 2.30 – PCA 用于(a) word2vec 和(b) GloVe 嵌入
此外,我们还可以应用 t-SNE:
图 2.31 – t-SNE 用于(a) word2vec 和(b) GloVe 嵌入
从之前的图中,我们可以看到垃圾邮件和正常邮件的词汇在我们的嵌入空间中非常接近,这使得分离这些簇变得非常困难。这是因为我们使用的是预训练的嵌入,来自新闻和维基百科数据集。这些数据集及其相应的嵌入并不适合我们的任务。我们将在第五章中看到如何训练词嵌入以获得更好的结果。
重要提示
对于 PCA 和 t-SNE,我们可以选择三个组件,而不是两个,这样可以得到一个三维图。有关代码,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook。
它是如何工作的...
要理解文本数据集的语料库,我们需要理解该语料库中单词之间的潜在联系。实现这一目标的一个有用方法是通过不同的语料库可视化来帮助我们理解。
在这个食谱中,我们学会了如何发现文本数据集中的模式。我们选择了一个不平衡的数据集——Enron 电子邮件数据集,并学习了如何处理二分类数据集。
我们通过查看数据集的内部结构,了解类不平衡的情况,并检查最常见的词汇,寻找其中的模式和错误。我们清理了数据集,移除了标点符号,并绘制了最常见的二元组和三元组,并注意到几个有助于我们正确分类电子邮件的关键词。
我们学会了如何生成一些酷炫的可视化效果,如词云,并且理解了为什么词嵌入如此重要,并使用我们之前学到的降维技术对其进行了绘制。
还有更多内容…
如果你想了解更多关于 Enron 电子邮件数据集和 Enron 丑闻的信息,以下链接将有所帮助:
-
安然电子邮件 数据集:
www.cs.cmu.edu/~enron/
我们简要概述了几个重要的概念,我邀请你进一步了解:
此外,我们只是浅尝辄止地触及了词嵌入所能提供的内容:
-
word2vec:
code.google.com/archive/p/word2vec/
第三章:求解回归问题
在前几章中,我们学习了如何设置和运行 MXNet,如何使用 Gluon 和 DataLoaders,并且如何可视化回归、分类、图像和文本问题的数据集。我们还讨论了不同的学习方法(监督学习、无监督学习和强化学习)。在本章中,我们将专注于监督学习,其中至少对于某些示例,期望的输出是已知的。根据这些输出的类型,监督学习可以细分为回归和分类。回归输出是来自连续分布的数字(例如,预测一家上市公司的股票价格),而分类输出是从已知集合中定义的(例如,识别图像是鼠标、猫还是狗)。
分类问题可以看作是回归问题的一个子集,因此,在本章中,我们将首先处理后者。我们将学习为什么这些问题适合深度学习模型,并概述定义这些问题的方程式。我们将学习如何创建合适的模型,并学习如何训练它们,重点介绍超参数的选择。我们将通过根据数据评估模型来结束每一节,这正如在监督学习中所期望的那样,我们将看到回归问题的不同评估标准。
本章将涵盖以下食谱:
-
理解回归模型的数学
-
定义回归的损失函数和评估指标
-
训练回归模型
-
评估回归模型
技术要求
除了《前言》中指定的技术要求外,本章还需要以下一些附加要求:
-
确保您已完成来自第一章的食谱,安装 MXNet、Gluon、GluonCV 和 GluonNLP,与 MXNet 一起启动并运行。
-
确保您已经完成了来自第二章的食谱 1,回归的玩具数据集——加载、管理和可视化房屋销售数据集,工作与 MXNet 和可视化数据集:Gluon 和 DataLoader。
本章的代码可以在以下 GitHub URL 找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch03。
此外,您可以直接从 Google Colab 访问每个食谱,例如,本章的第一个食谱:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch03/3_1_Understanding_Maths_for_Regression_Models.ipynb。
理解回归模型的数学
如我们在上一章看到的,回归问题是一种监督学习问题,其输出是来自连续分布的一个数字,比如房价或公司股票价格的预测值。
我们可以用来解决回归问题的最简单模型是线性回归模型。然而,这些模型对于简单问题非常强大,因为它们的参数可以训练,并且在涉及的参数数量较少的情况下,速度很快且易于解释。正如我们将看到的,参数的数量完全取决于我们使用的特征数量。
线性回归模型的另一个有趣属性是,它们可以通过神经网络来表示,而神经网络将是我们在本书中使用的大多数模型的基础,因此我们将使用这种基于神经网络的线性回归模型。
最简单的神经网络模型被称为感知机,这是我们不仅在本食谱中,而是在整个章节中要研究的基础模块。
准备就绪
在深入理解我们的模型之前,我想提一下,对于本食谱中的数学部分,我们将遇到一些矩阵运算和线性代数,但这绝对不会很难。
如何操作...
在本食谱中,我们将通过以下步骤进行操作:
-
数学建模生物神经元
-
定义回归模型
-
描述基本激活函数
-
定义特征
-
初始化模型
-
评估模型
数学建模生物神经元
感知机最早由美国心理学家 Frank Rosenblatt 于 1958 年在康奈尔航空实验室提出,它是初步尝试复制我们大脑中神经元处理信息的方式。
Rosenblatt 分析了生物神经元并开发了一个行为相似的数学模型。为了比较这些架构,我们将从一个非常简单的神经元模型开始。
图 3.1 – 生物神经元
如我们在图 3.1中看到的,神经元由三个主要部分组成:
-
树突:神经元从其他神经元接收输入的地方。根据连接的强度,输入在树突中会被增强或减弱。
-
细胞体或胞体:包含细胞核,这是接收来自树突的所有输入并对其进行处理的结构。细胞核可能会触发电信号并传递给其他神经元。
-
轴突/轴突末端:这是输出结构,用于与其他神经元传递信息。
Rosenblatt 将之前简化的神经元模型赋予了某些数学属性:
图 3.2 – 感知机
-
权重:这将通过将输入或特征与一组权重(W 在 图 3.2 中,而 j 代表模型中的任何神经元)相乘来模拟树突的行为。
-
和 和 偏置:在细胞核中输入信号的组合将被建模为带有偏置的求和(θ 在 图 3.2 中)和一个处理函数,称为 激活函数。(我们将在下一步描述这些函数。)
-
输出:要么连接到其他神经元,要么是整个模型的直接输出(o 在 图 3.2 中)。
比较 图 3.1 和 图 3.2,我们可以看到生物神经元的简化模型与感知器之间的相似之处。此外,我们还可以看到这些部分是如何连接在一起的,从处理输入到输出结果。
定义回归模型
因此,从数学角度来看,使用矩阵乘法,我们可以写出以下模型方程 y = f(W⋅X + b),其中 W 是权重向量 [W1*, W2, …. Wn],(n 是特征的数量),X 是特征向量 [X1, X2, …. Xn]*,b 是偏置项,f() 是激活函数。对于我们将处理的回归情况,我们将使用线性激活函数,其中输出等于输入。
因此,在我们的例子中,激活函数是恒等函数(输出等于输入),我们有 y = W⋅X + b。
我们可以使用 MXNet 及其 NDArray 库轻松实现这一点:
# Perceptron Model
def perceptron(weights, bias, features):
return mx.nd.dot(features, weights) + bias
就这样!这就是我们基于 X 输入以及模型的 W 和 b 参数的 y 神经网络输出。
重要提示
在 Rosenblatt 的原始论文中,期望的输出是 0 或 1(分类问题),为了满足这一要求,激活函数被定义为阶跃函数(如果输入小于 0,则输出 0;如果输入大于或等于 0,则输出 1)。这是 Rosenblatt 神经元模型的一个最大限制,后来提出了不同的激活函数来改善模型的表现。
在深度学习网络中,我们不会仅使用单个神经元(感知器)作为模型。通常,多个感知器层被堆叠在一起,层数也被称为网络的 深度。
图 3.3 – 深度学习网络
这些网络非常强大,并且已经证明在多个领域匹配或超越了人类水平的表现,包括计算机视觉中的图像识别和自然语言处理中的情感分析。
描述基本激活函数
回归问题中最常见的激活函数是线性激活函数和 ReLU 激活函数。我们简要描述一下它们。
图 3.4 – 回归的激活函数
线性激活函数
在此函数中,输出等于输入。它没有界限,因此适用于无界的数值输出,正如回归问题的输出所示。
ReLU
修正线性单元激活函数非常类似于线性激活函数:其输出等于输入,但在这种情况下,只有当输入大于 0 时,输出才等于输入;否则输出为 0。此函数适用于仅将正信息传递到下一层(稀疏激活),并且还提供更好的梯度传播。因此,它在深度学习网络的中间层中非常常见。
重要说明
正如我们将在接下来的内容中看到的,训练涉及迭代地计算新的梯度,并使用这些计算来更新模型参数。当使用激活函数(如 sigmoid)时,层数越多,梯度变得越小。这个问题被称为梯度消失问题,而 ReLU 激活函数在这种情况下表现得更好。
定义特征
到目前为止,我们已经从理论上定义了我们的模型及其行为,但我们尚未使用我们的任务框架或数据集来定义它。在本节中,我们将开始以更实际的方式进行工作。
定义模型的下一步是决定我们将使用哪些特征(输入)。我们将继续使用在第二章《与 MXNet 合作并可视化数据集:Gluon 和 DataLoader》中遇到的房屋销售数据集。这个数据集包含了 21,613 栋房屋的数据,包括价格和 19 个输入特征。虽然在我们的模型中我们将使用所有输入特征,但在上述配方中,我们看到对房价贡献最大的三个非相关特征如下:
-
居住面积(平方英尺)
-
等级
-
卫生间数量
在初步研究中,我们将使用这三个特征。选择这些特征后,如果我们显示前五个房屋的数据,我们将看到以下内容:
图 3.5 – 房屋价格的过滤特征
在之前分析这个数据集时,我们没有利用的一条路径是,grade不像其他特征那样是连续的;它的值来自一个离散的集合。这种类型的特征被称为分类特征,可以是名义型的或有序型的。名义型特征是类名——例如,我们可以将房屋的建筑风格作为一个特征,该特征的值可以是维多利亚式、艺术装饰风格、工匠风格等。而有序型特征则是类编号。在我们的例子中,grade是一个有序特征,由按顺序排列的数值组成(1 为最差,13 为最好)。
在这两种情况下,分类特征可以通过不同的方式以数字形式表示,这有助于我们的模型更好地学习特征与输出之间的关系。处理分类特征的方法有多种。在这个例子中,我们将使用最简单的方式之一——独热编码方案。
提示
你可以在本食谱末尾的更多内容部分找到有关处理分类数据的更多信息。
使用独热编码方案,每个类别都会被分解为它自己的特征,并相应地分配一个二进制值。在我们的例子中,grade包含从 1 到 13 的整数值,因此,我们向输入向量中添加了 13 个新特征。这些新特征的值将为 0 或 1。例如,对于一个 1 级的房子,特征向量如下所示:
图 3.6 – 等级特征的独热编码
如果我们仔细查看图 3.6,我们会发现没有对应于值 2 的独热编码。这是因为我们的数据集中没有实际房子的等级为 2,因此没有将其作为特征添加。
因此,最终的特征数量为 14 个,输出为 1 个,即房子的价格。
初始化模型
现在我们已经定义了输入(特征)和输出的维度,我们可以初始化模型。我们将在下一部分更详细地探讨这个问题,但展示一下它的大致样子还是很有用的:
Weights:
[[ 0.96975976]
[-0.52853745]
[-1.88909 ]
[ 0.65479124]
[-0.45481315]
[ 0.32510808]
[-1.3002341 ]
[ 0.3679345 ]
[ 1.4534262 ]
[ 0.24154152]
[ 0.47898006]
[ 0.96885103]
[-1.0218245 ]
[-0.06812762]]
<NDArray 14x1 @cpu(0)>
Bias:
[-0.31868345]
<NDArray 1 @cpu(0)>
正如预期的那样,Weights向量有 14 个分量(即特征数量),而Bias向量只有 1 个分量。
评估模型
现在我们的模型已初始化,我们可以用它来估算第一套房子的价格,正如图 3.6所示,估计价格大约为 220 万美元。在我们当前的模型下,估算的房价为(单位:美元):
2610.2383
由于我们有预期的价格,我们可以计算一些误差指标。在本例中,我选择了绝对误差和相对于实际价格的误差。这些量可以很容易地在 Python 中计算:
error_abs = abs(expected_output - model_output)
error_perc = error_abs / expected_output * 100
print("Absolute Error:", error_abs)
print("Relative Error (%):", error_perc)
获得的误差值如下:
Absolute Error: 219289.76171875
Relative Error (%): 98.82368711976115
如你所见,2.6 千美元对于一套面积为 1,180 平方英尺的房子来说是一个非常低的价格,即使它只有 1 个浴室,并且评级为中等(7)。这意味着我们的误差指标值非常大,暗示着大约 99%的误差率。这表明,要么我们没有正确评估我们的模型(在这种情况下,我们只使用了一个值,可能只是运气不好),要么我们只使用了初始化的参数,这些参数并没有给出准确的估计。我们需要通过一个称为训练的过程来改进我们的模型参数,以提高评估指标。我们将在下一部分详细探讨这些话题。
它是如何工作的...
回归模型可以复杂到设计者所需的程度。它们可以拥有足够多的层次,以适当地建模输入特征和期望输出值之间的关系。
在本食谱中,我们描述了大脑中生物神经元的工作原理,并简化它以推导出一个简单的数学模型,供我们用于回归问题。在我们的例子中,我们只使用了一个层,通常称为输入层,并将权重和偏置定义为其参数。
此外,我们学习了如何初始化模型,探索了初始化对权重和偏置的影响,并了解了如何利用我们的数据来评估模型。我们将在接下来的食谱中进一步展开这些话题。
还有更多内容……
在本篇食谱中,我们简要介绍了几个话题。我们首先描述了罗森布拉特的感知器。如果你想阅读原始论文,可以通过这个链接访问:www.ling.upenn.edu/courses/cogs501/Rosenblatt1958.pdf。
尽管在本食谱及之后的食谱中我们会使用一些公式,但我们将使用库和代码来帮助我们专注于实际输出及其与输入的关系。然而,对于感兴趣的读者,这里有一篇回顾文章:machinelearningmastery.com/gentle-introduction-linear-algebra/。
此外,我们对输入特征进行了更详细的分析,特别是使用独热编码处理了grade这一分类特征。处理分类数据有多种方法,相关内容可以在此链接中找到:towardsdatascience.com/understandi…
关于初始化和评估的更多信息,请继续阅读本章中的后续食谱。
定义回归的损失函数和评估指标
在前一篇食谱中,我们定义了输入特征,描述了我们的模型,并对其进行了初始化。那时,我们传入了一栋房子的特征向量来预测价格,计算了输出,并将其与预期输出进行了比较。
在前一篇食谱的结尾,我们通过比较模型的预期输出和实际输出,直观地了解了模型的好坏。这就是“评估”模型的含义:我们评估了模型的性能。然而,这一评估并不完全,原因有几个,因为我们没有正确地考虑到一些因素:
-
我们只对一栋房子进行了模型评估——那么其他的房子呢?我们如何在评估中考虑到所有房子呢?
-
值之间的差异是否是衡量模型误差的准确方法?还有哪些操作是有意义的?
在本食谱中,我们将讨论如何评估(即“评估”)我们模型的性能,并研究适合此目的的函数。
此外,我们将介绍一个在优化(即训练)模型时非常重要的概念:损失函数。
准备工作
在定义一些用于评估回归模型和计算其损失的有用函数之前,让我们明确一下我们将用来评估模型的函数的两个必需属性和三个期望属性:
-
[必需的] 连续性:显然,我们希望我们的评估函数在某些潜在的误差值上不会是未定义的,这样我们就可以在大量的(预期输出,实际模型输出)对上使用这些函数。
-
[必需的] 对称性:通过一个例子很容易解释这个问题。假设一栋房子的价格是 220 万美元——我们希望我们的评估函数能够以相同的方式评估模型,无论输出是 200 万美元还是 240 万美元,因为这两个值离预期值的距离是相同的,只是方向不同。
-
[期望的] 稳健性:同样,这个问题通过一个例子更容易解释。以之前提到的例子为例,假设我们有 2.4 百万美元和 2.8 百万美元两个输出值。与预期值 220 万美元相比,误差已经很大,因此我们不希望由于损失/评估函数的影响使误差变得更大。从数学的角度来看,我们不希望误差呈指数增长,否则可能导致计算发散至不是一个数字(NaN)。使用稳健函数时,大误差不会导致计算发散。
-
[期望的] 可微性:这是所有属性中最不直观的一项。通常,我们的目标是将误差率尽可能接近零。然而,这只是一个理论场景,只有当我们拥有足够的数据来完美描述问题,并且模型足够大,能够表示从数据到输出值的映射时,才会发生。在现实中,我们永远无法确保符合这两个假设,因此,零误差的不切实际期望会转化为数据和模型的最小误差。我们只能通过计算函数的微分函数来检测函数的最小值,因此才有了这个可微性属性。幸运的是,可微性意味着连续性,因此如果我们的函数满足属性 #4,它也会自动满足属性 #1。
-
[期望的] 简洁性:满足所有属性的函数越简单越好,因为这样我们可以更直观地理解结果,并且从计算上讲也不会太昂贵。
提示
不仅评估函数必须满足这些标准。正如我们将在下一个章节看到的,它们还必须被一个对训练非常重要的函数所满足,即损失函数。
如何操作...
让我们讨论一些评估和损失函数,并分析它们的优缺点。我们将描述的函数如下:
-
平均绝对误差
-
均方误差
-
平滑 L1 损失
平均绝对误差
我们要研究的第一个函数根据之前描述的五个属性几乎完美。这个函数的直观想法是使用值之间的差异作为这些值之间距离或误差的指标。我们应用 abs 函数,即绝对值,使其变得对称:
MAE = 1 _ n ∑ j=1 n | y j − ˆ y j|
当绘制时,该函数生成以下图形:
图 3.7 – MAE 图
如果我们根据之前定义的属性来分析这个函数,我们会发现,除了第 #4 条外,所有属性都得到了满足;不幸的是,该函数在 0 点不可导。如我们将在下一个配方中看到的那样,当这个函数作为损失函数使用时,这特别具有挑战性。然而,在评估我们的模型时并不需要导数,因为评估过程不需要微分,平均绝对误差(MAE)被视为评估中的典型回归指标。
重要提示
这个函数可以用于评估,也可以作为损失函数(单独使用或作为正则化项)。在这种情况下,它通常被称为 L1 损失或项,它计算的是与期望输出和实际输出对应的向量的 L1 距离。
另一个类似的指标是 平均绝对百分比误差(MAPE)。在该指标中,每个输出误差通过期望输出进行归一化:
MAPE = 100% _ n ∑ i=1 n | y i − ˆ y i _ y i |
均方误差
为了解决可导性问题,均方误差(MSE)函数与 MAE 非常相似,但在差异项上将阶数从 1 提高到 2。直观的想法是使用最简单的二次可导函数(x²):
MSE = 1 _ n ∑ i=1 n (Y i − ˆ Y ) 2
当绘制时,该函数生成以下图形:
图 3.8 – MSE 图
如果我们根据已定义的属性来分析该函数,我们发现,除了第 #3 条外,所有属性都得到了满足。不幸的是,这个函数的稳健性不如 MAE。较大的误差呈指数级增长,因此该评估函数对异常值更加敏感,因为一个数据点的非常大误差可能导致该值的平方误差非常大,从而对 MSE 产生很大影响,导致错误的结论。
重要提示
这个函数可以用于损失函数(单独使用或作为正则化项)。在这种情况下,它通常被称为 L2 损失或项(岭回归),因为它计算的是与期望输出和实际输出对应的向量的 L2 距离。
为了与输出变量具有相同的单位,MSE 可以应用平方根。这个评估指标叫做均方根误差(RMSE):
RMSE = √ _ ∑ i=1 n ( ˆ y i − y i) ² _ n
平滑的平均绝对误差/平滑 L1 损失
我们不能同时拥有两全其美吗?当然可以!
通过结合这两个函数——对小误差值使用 MSE,对大误差值使用 MAE——我们得到了如下结果:
平滑的 L1(x) = { 0.5 x² 如果 |x| < 1 |x| − 0.5 否则
当绘制时,该函数产生如下图所示:
图 3.9 – 平滑的平均绝对误差图
如果我们根据已定义的属性分析这个函数,我们会看到所有属性都得到了满足。
重要提示
这个函数可以用于损失函数。在这种情况下,通常称之为平滑 L1 损失。
它是如何工作的...
在本章的第一个例子中,我们设计了基于简单生物神经元的第一个回归模型。我们随机初始化了其参数,并进行了第一次简单的评估。结果并不好,我们推测这是由于两个原因:我们的评估机制不够稳健,模型参数没有优化。
在这个例子中,我们探讨了如何改进第一个原因:评估。我们涵盖了三种最重要的评估指标,并提到了它们与损失函数的关系,我们将在下一个例子中详细探讨这些内容。
此外,我们讨论了哪些评估指标更好,探讨了 MAE 是如何稳健的,但不幸的是不可微分,MSE 是可微分的,但它允许异常值影响指标(这并不理想)。我们通过结合这两种函数,得到了两者的优点。
还有更多...
在这个例子中,我们没有探索的一个非常有趣的评估函数集是决定系数及其扩展。然而,这个集仅用于线性回归建模。更多信息可以通过以下链接获取:
此外,在回归问题中,通常可以使用许多不同的函数进行评估和损失计算;你可以参考这个链接了解更多细节:machine-learning-note.readthedocs.io/en/latest/basic/loss_functions.html
训练回归模型
在监督学习中,训练是朝着特定目标优化模型参数的过程。它通常是解决深度学习问题中最复杂、最耗时的步骤。
在本方法中,我们将访问训练模型所涉及的基本概念。我们将应用这些概念来解决我们在本章中先前定义的回归模型,并结合我们讨论过的函数的使用。
我们将使用在第二章**中看到的数据集预测房价:《使用 MXNet 与数据可视化:Gluon》和 DataLoader*。
准备工作
为了理解这个方法,我们需要熟悉一些概念。这些概念定义了训练的进行方式:
-
损失函数:训练过程是一个迭代优化过程。随着训练的进行,期望模型能够在与模型实际输出进行比较的操作中表现得更好。这个操作就是损失函数,也称为目标函数、成本函数或代价函数,它是在训练过程中被优化的。
-
优化器:在每次训练迭代过程中,模型的每个参数都会通过一个量(通过损失函数计算)进行更新。优化器是一种定义如何计算该量的算法。优化器最重要的超参数是学习率,它是应用于计算量的乘数,用于更新参数。
-
数据集划分:当模型能够在真实世界中达到最佳表现时,停止训练对深度学习项目的成功至关重要。一种实现此目标的方法是将数据集划分为训练集、验证集和测试集。
-
训练轮次:这是训练过程将运行的迭代次数。
-
批量大小:每次分析的训练样本数,用于生成梯度估计。
如何做到...
在本方法中,我们将创建自己的训练循环,并评估每个超参数如何影响训练。为此,我们将遵循以下步骤:
-
改进模型
-
定义损失函数和优化器
-
划分数据集
-
分析公平性和多样性
-
定义训练轮次和批量大小
-
整合所有内容以形成训练循环
改进模型
为了解决这个问题,我们在前面的食谱中探讨的架构(感知机)将不够用。我们将多个感知机堆叠在一起,并通过不同的层连接它们。这种架构被称为多层感知机(MLP)。我们将定义一个包含三个隐藏层的网络架构,所有层都是全连接(密集层),并使用ReLU激活函数(在本章的第一篇食谱中介绍)来分别包含 128、1,024 和 128 个神经元,最后一层是输出层,只有一个输出。最后一层不使用激活函数,也称为线性激活函数(y = x)。
此外,房屋销售数据集是一个非常复杂的问题,找到在现实世界中具有良好泛化能力的解决方案并不容易。为此,我们在模型中加入了两个新的高级特性:
-
批量归一化:通过该步骤,对于每个小批量,输入分布都会进行标准化。这有助于训练收敛和泛化能力。
-
Dropout:此方法的内容是随机禁用神经网络中的神经元(根据一定的概率)。这有助于减少过拟合(这一概念将在下一个食谱中解释)并改善泛化能力。
我们的代码如下:
def create_regression_network():
# MultiLayer Perceptron Model (this time using Gluon)
net = mx.gluon.nn.Sequential()
net.add(mx.gluon.nn.Dense(128))
net.add(mx.gluon.nn.BatchNorm(axis=1, center=True, scale=True))
net.add(mx.gluon.nn.Activation('relu'))
net.add(mx.gluon.nn.Dropout(.5))
net.add(mx.gluon.nn.Dense(1024))
net.add(mx.gluon.nn.BatchNorm(axis=1, center=True, scale=True))
net.add(mx.gluon.nn.Activation('relu'))
net.add(mx.gluon.nn.Dropout(.4))
net.add(mx.gluon.nn.Dense(128))
net.add(mx.gluon.nn.BatchNorm(axis=1, center=True, scale=True))
net.add(mx.gluon.nn.Activation('relu'))
net.add(mx.gluon.nn.Dropout(.3))
net.add(mx.gluon.nn.Dense(1))
return net
一个需要注意的重要点是,我们模型的输入也已被修改:
-
数值输入已被缩放,以生成均值为零、方差为单位的输入。这改善了训练算法的收敛性。
-
类别输入(等级)已进行了独热编码。我们在第二章的食谱 4,文本任务的玩具数据集——加载、管理和可视化恩朗邮件数据集中介绍了这个概念,来自[MXNet 的工作与数据集可视化:Gluon和 DataLoader]。
这将特征数量增加到 30。由于数据集包含大约 20k 行数据,这提供了大约 600k 个数据点。我们可以将其与模型中参数的数量进行比较:
[...]
Parameters in forward computation graph, duplicate included
Total params: 272513
Trainable params: 269953
Non-trainable params: 2560
Shared params in forward computation graph: 0
Unique parameters in model: 272513
我们模型中可训练参数的数量约为 270k。数据点的数量大约是我们模型中可训练参数的两倍。通常,这是成功模型的最低要求,理想情况下,我们希望使用的数据集大小为模型参数的 10 倍左右。
提示
尽管将数据点数量与模型的参数数量进行比较是一个非常有用的方法,但不同的架构对数据有不同的需求。如同往常一样,实验(试错法)是找到正确平衡的关键。
关于我们模型的最后一个重要点是初始化方法,因为在多层网络中,随机初始化可能不会产生最佳结果。现如今最常用的方法包括以下几种:
-
Xavier 初始化:在计算方差时,考虑了输入数量和输出数量。
-
MSRA PReLU 或 Kaiming 初始化:Xavier 初始化方法在 ReLU 激活函数中存在一些问题,因此更倾向于使用此方法。
MXNet 提供了非常简单的方式来访问这些功能,在本例中是 MSRA PReLU 初始化:
net.collect_params().initialize(mx.init.MSRAPrelu(), ctx=ctx, force_reinit=True)
重要提示
初始化方法为权重和偏置提供初始值,以避免模型激活函数初始化在饱和(平坦)区域。其直觉是让这些权重和偏置的均值为零,方差为单位。有关统计分析的详细信息,请参见 更多内容...。
定义损失函数和优化器
如我们在前面的示例中所见,平滑 L1(也称为 Huber)损失函数效果非常好。
有几种优化器已被证明在 监督学习 问题中表现良好:
-
在我们处理 DataLoader 时使用的
batch_size参数,见 第二章*,与 MXNet 配合使用并可视化数据集:Gluon* 与 DataLoader。 -
动量/ Nesterov 加速梯度:梯度下降可能会遇到稳定性问题,并可能开始跳跃并陷入局部最小值。避免这些问题的一种方法是考虑算法过去的步骤,这可以通过这两种优化器实现。
-
Adagrad/Adadelta/RMSprop:GD 对所有参数使用相同的学习率,而不考虑它们更新的频率。Adagrad 及这些优化器通过调整每个参数的学习率来解决这个问题。然而,Adagrad 的学习率会随着时间的推移而减小,并可能接近零,导致无法进行进一步更新。为了解决这个问题,开发了 Adadelta 和 RMSprop。
-
Adam/AdaMax/Nadam:这些最先进的优化器结合了梯度下降的两项改进:过去步骤的计算和自适应学习率。Adam 使用 L2 范数来计算梯度的指数加权平均值,而 AdaMax 使用无穷范数(最大操作)。Nadam 用 Nesterov 动量替代了 Adam 中的动量部分,从而加速收敛。
MXNet 和 Gluon 提供了非常简单的接口来定义损失函数和优化器。通过以下两行代码,我们选择了 Huber 损失函数和 Adam 优化器:
# Define Loss Function
loss_fn = mx.gluon.loss.HuberLoss()
# Define Optimizer and Hyper Parameters
trainer = mx.gluon.Trainer(net.collect_params(), "adam", {"learning_rate": 0.01})
切分我们的数据集
在所有数据科学项目中,需要考虑的最重要的事情之一就是在我们将模型应用于新的数据时,已训练模型在未见过的数据上的表现如何。对于监督学习,在训练和评估过程中,我们使用已知的(期望的)输出数据,那么如何确保我们在新数据上使用模型时,它能按预期表现呢?
我们通过将数据集分成三个部分来解决这个问题:
-
训练集:训练集在训练过程中用于计算模型参数的更新。
-
验证集:验证集在训练过程中用于检查每个周期模型的改进情况(或没有改进),即之前计算的那些更新。
-
测试集:最后,一旦训练完成,我们可以计算模型在未见数据上的表现,这就是测试集,是数据集中唯一未在训练中用于提升模型的部分。
此外,为了保证稳定的训练,使得我们的模型能正确处理数据集外的数据,数据的划分需要考虑以下因素:
-
划分大小:这取决于可用数据的数量和任务的性质。典型的训练/验证/测试数据划分比例为 60/20/20 或 80/10/10。
-
选择每个划分的数据点:这里的关键是拥有一个平衡的数据集。例如,在我们的房价数据集中,我们不希望训练集里只有两间和三间卧室的房子,验证集里是四卧房的房子,最后测试集是五间或更多卧室的房子。理想情况下,每个数据集应该准确代表整个数据集。这对于需要考虑公平性和多样性的敏感数据集尤其重要。
我们可以很容易地实现这些划分,在这个例子中,使用一个来自著名库scikit-learn的函数:
# Dataset Split 80/10/10
from sklearn.model_selection import train_test_split
full_train_df, test_df = train_test_split(house_df, test_size=0.2, random_state=42)
# To match correctly 10% size, we use previous size as reference
train_df, val_df = train_test_split(full_train_df, test_size=len(test_df), random_state=42)
在前面的代码片段中,我们将训练集、验证集和测试集分成三个部分,分为两个步骤:
-
我们将数据集的 20%分配给测试集。
-
剩余的 80%将平分给验证集和测试集。
分析公平性和多样性
假设一下我们为一个房地产网站工作,负责数据科学团队。我们有一个非常吸引人的功能来为我们的网站带来流量:当房主想要出售房产时,他们可以填写一些房屋数据,并能看到一个经过机器学习优化的估算价格,告诉他们应该以什么价格将房屋挂牌出售,同时数据还表明房屋将在接下来的三个月内按这个价格售出。这个功能听起来非常酷,因为房主可以微调他们想要出售房屋的要价,而潜在买家也会根据市场看到合理的价格。
然而,我们突然意识到,对于拥有两间或更少浴室的房子,数据量还不够,而且我们知道这个特征对我们的模型非常敏感。如果我们将这个模型应用于现实中的房产,意味着我们模型可能会将拥有两间或更少浴室的房子的估价定得接近那些拥有更多浴室的房子,仅仅因为这是模型所能看到的所有数据。这就意味着,对于最便宜的房子,即低收入家庭最负担得起的房子,我们可能会不公平地提高它们的价格,这将是一个严重的问题。
我们的模型不能知道更多,因为我们没有展示给它更多。在这种情况下,我们有哪些选择?以下是可能适合实际情况的几种方案:
-
对我们的模型的鲁棒性充满信心,并无论如何都将其部署到生产环境中。
-
说服业务领导者,在我们拥有所需的所有数据之前不要部署模型。
-
将模型部署到生产环境中,但只允许卖家将其用于至少有三个浴室的房屋。
第一个选项是所有选项中最不充分的,然而,由于以下原因,它实际上是最常用的:
-
在项目进行了几个月后再延迟发布是非常不方便的。管理层通常不期望也不希望听到这样的消息,这可能会让一些工作岗位面临风险。
-
然而,发生这种情况最常见的原因是数据中的错误没有被注意到。通常没有足够的时间来验证数据是否准确、公平、多样,因此重点转向尽快交付一个优化过的模型。
第二个选项因为与第一个选项类似的原因而很难辩护,而第三个选项可能在纸面上看起来不错,但实际上是非常危险的。如果我遇到这种情况,我不会选择第三个选项,仅仅因为我们不能确保数据在所有特征上都是多样和公平的,因此需要进行适当的数据质量评估。如果我们在项目这么晚的阶段发现这种错误,那是因为在数据质量上没有给予足够的关注。这通常发生在那些已经记录或存储了大量数据的公司中,而这些公司现在想用这些数据做一些机器学习项目,而不是设计有明确目标的数据收集操作。这是机器学习项目在这些公司中失败的最常见原因之一。
让我们从公平性和多样性的角度来看一下我们的数据集:
首先,正如在《第二章》中的《配方 1,回归模型玩具数据集——加载、管理与可视化房屋销售数据集*,与 MXNet 和数据集可视化:Gluon 和 DataLoader*一节中所见,我们将从价格分布开始:
图 3.10 – 训练集、验证集和测试集的价格分布
尽管我们可以看到价格低于$500k 的房屋出现了小幅下降,但三个数据集中的价格分布在很大程度上是均衡的,且不需要进行手动修改。
居住面积(平方英尺)如下所示:
图 3.11 – 训练集、验证集和测试集的居住面积平方英尺图
我们在这里看到的最大差异是由于少数高价房产。我们甚至可以将这些看作是异常值,如果我们的训练参数选择得当,这不应影响我们的预测能力。
浴室数量如下所示:
图 3.12 – 训练集、验证集和测试集中的浴室数量分布
这种分布在我们的验证集和测试集中得到了很好的体现。
评分如下所示:
图 3.13 – 训练集、验证集和测试集的评分
这种分布在我们的验证集和测试集中也得到了很好的体现。
将所有个别分析结果汇总,我们可以得出结论:我们的训练集、验证集和测试集相当好地代表了完整数据集。我们必须记住,我们的数据集有其自身的局限性(尽管我必须说,对于所选特征,它表现得相当不错):
-
价格: [75k]
-
居住面积(平方英尺):[290, 13540]
-
浴室数量 [0, 8]
-
评分: [1, 13],但之前提到的缺少 2
定义训练轮数和批量大小
训练轮数指的是训练算法运行的迭代次数。根据问题的复杂性以及所选择的优化器和超参数,这个数字可能会从非常低(例如 5-10 次)到非常高(几千次迭代)。
批量大小指的是在同一时间内分析的训练样本数,以估计误差梯度。在第二章**,与 MXNet 协作并可视化数据集:Gluon 和 DataLoader 中的Recipe 3,面向图像任务的玩具数据集——加载、管理和可视化鸢尾花数据集,我们引入了这个概念作为优化内存使用的一种手段;批量大小越小,所需内存越少。此外,这样可以加速梯度的计算;批量大小越大,计算运行越快(如果内存允许)。典型的批量大小范围从 32 到 2,048 个样本。
将所有内容结合起来,形成一个训练循环
训练循环是一个迭代过程,运行优化器以计算/估计梯度,从而在每次迭代中减少通过损失函数(优化器的目标)计算得到的误差。如前所述,每次迭代称为一个“轮次”。对于每次迭代,整个训练集都以批次的方式被访问以计算梯度。
此外,正如我们将看到的,计算验证集的损失函数非常有趣。在我们的案例中,我们还将计算测试集的损失函数,因为它将为我们提供关于模型表现的具体细节。
为了理解修改超参数时的行为差异,我们将对我们的房价预测数据集多次运行训练循环,每次仅修改一个超参数,其他变量保持不变(除非另有说明)。
优化器和学习率
如前所述,训练循环中选择的优化器和学习率是密切相关的,因为对于某些优化器(如 SGD),学习率是恒定的,而对于其他优化器(如 Adam),学习率从一个给定的起始点开始变化。
提示
最好的优化器取决于多个因素,没有什么比试错法更有效。我强烈建议尝试几个优化器,看看哪个最适合。在我的经验中,SGD 和 Adam 通常是表现最好的,甚至在这个问题中——预测房价问题上。
让我们分析一下,当变动学习率(LR)并保持其他参数不变时,SGD 优化器的训练损失和验证损失是如何变化的:训练轮数 = 100,批次大小 = 128,损失函数 = HuberLoss:
图 3.14 – 当变动学习率时,SGD 优化器的损失
从图 3.14中,我们可以得出结论,对于 SGD 优化器,学习率(LR)值在 10^-1 到 1.0 之间是最佳的。此外,我们可以看到,对于非常大的 LR 值(> 2.0),算法会发散。这就是为什么在寻找最佳学习率值时,最好从小值开始。
让我们分析一下,当变动学习率(LR)并保持其他参数不变时,Adam 优化器的训练损失和验证损失是如何变化的:训练轮数 = 100,批次大小 = 128,损失函数 = HuberLoss:
图 3.15 – 当变动学习率时,Adam 优化器的损失
从图 3.15中,我们可以得出结论,对于 Adam 优化器,学习率(LR)值在 10^-4 到 10^-3 之间是最佳的。由于 Adam 计算梯度的方式不同,它比 SGD 更不容易发散。
Adam 需要较小的学习率值,因为它会随着训练过程的进展调整学习率。
批次大小
让我们分析一下,当变动批次大小时,Adam 优化器的训练损失和验证损失是如何变化的,其他参数保持不变:训练轮数 = 100,学习率 = 10^-2,损失函数 = HuberLoss:
图 3.16 – 当变动批次大小时,Adam 优化器的损失
从图 3.16中,我们可以得出结论,对于 Adam 优化器,批次大小在 64 到 1,024 之间提供最佳结果。
训练轮数
另一个超参数是训练轮数(epochs),即优化器处理完整训练集的次数。
让我们分析在 Adam 优化器下,当改变训练轮次(epochs)时,训练损失和验证损失的变化,同时保持其他参数不变:LR = 10-2,Batch Size = 128,Loss fn = HuberLoss:
图 3.17 – Adam 优化器在改变训练轮次时的损失
从 图 3.17 中,我们可以得出结论,大约 100-200 轮次对于我们的问题是合适的。在这些值下,很可能会在此之前取得最佳结果。
它是如何工作的...
在我们解决回归问题的过程中,我们在这篇教程中学会了如何最优地更新模型的超参数。我们理解了每个超参数在训练循环中的作用,并对每个超参数进行了单独的消融研究。这帮助我们理解了在单独修改每个超参数时,训练损失和验证损失的表现。
对于我们当前的问题和选择的模型,我们验证了最佳超参数组合如下:
-
优化器:Adam
-
学习率:10-2
-
Batch Size:128
-
训练轮次:200
在训练循环结束时,这些超参数给我们带来了 0.10 的训练损失和 0.10 的验证损失。
还有更多...
在我们的模型定义中,我们引入了三个新概念:批量归一化、dropout 和 scaling。我认为以下链接对理解这些高级话题非常有用:
-
批量归一化简介:
machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ -
Batch normalization 研究 论文:
arxiv.org/abs/1502.03167 -
dropout 简介:
machinelearningmastery.com/dropout-for-regularizing-deep-neural-networks/ -
Dropout 研究 论文:
jmlr.org/papers/v15/srivastava14a.html
在初始化方面,本文详细探讨了 Xavier 和 Kaiming 方法(包括研究论文的链接): pouannes.github.io/blog/initialization/
在本教程中,我们深入探讨了两种优化器,SGD 和 Adam,的表现。这是两个最重要且表现最好的优化器;然而,还有许多其他优化器,有些可能更适合你的特定问题。
一个学习 MXNet 中实现的优化器及其特性的绝佳资源是官方文档:mxnet.apache.org/versions/1.6/api/python/docs/tutorials/packages/optimizer/index.html。
为了比较每个优化器的行为和表现,我个人喜欢这个链接中展示的可视化(优化器部分):towardsdatascience.com/on-optimization-of-deep-neural-networks-21de9e83e1。
在本食谱中,我们研究了优化器及其超参数。超参数选择是一个非常复杂的问题,通常需要通过每个问题的试验和错误来验证训练循环是否有效。选择超参数时的经验法则是阅读解决与自己问题相似的研究论文,并从那些论文中提出的超参数开始。然后,你可以从这个起点出发,看看什么最适合你的特定情况。
除了训练过程中的训练损失和验证损失外,我们还提供了第三个损失值,最佳验证损失,我们将在下一个食谱中探讨这个值的含义及其计算方法。这一切都与我们尚未正确回答的问题相关:我应该什么时候停止训练循环? 我们将在下一个食谱中解决这个问题。
回归模型评估
在上一个食谱中,我们学习了如何选择训练超参数以优化我们的训练。我们还验证了这些选择如何影响训练和验证损失。在本食谱中,我们将探讨这些选择如何影响我们在现实世界中的实际评估。细心的读者会注意到我们将数据集分为三个不同的部分:训练集、验证集和测试集。然而,在训练过程中,我们只使用了训练集和验证集。在本食谱中,我们将通过在未见数据(测试集)上运行模型来模拟一些现实世界的行为。
准备工作
在评估模型时,我们可以执行定性评估和定量评估:
-
定性评估是选择一个或多个随机(或不那么随机,取决于我们要寻找的东西)样本,并分析结果,验证它是否符合我们的预期。
-
定量评估涉及计算大量输入的输出并对其进行统计分析(通常是均值),因此我们将计算 MAE 和 MAPE。
此外,我们还将看看训练如何对评估产生重大影响。
如何操作……
在开始模型评估之前,让我们先讨论如何衡量我们的模型训练表现。因此,本食谱中的步骤如下:
-
衡量训练表现——过拟合
-
定性评估
-
定量评估
测量训练表现 – 过拟合
深度学习网络非常强大,在多种问题上超越了人类水平的表现。然而,如果不加以控制,这些网络也可能产生不正确和意外的结果。最重要且常见的错误之一发生在网络充分发挥其能力时,它会记住正在展示的样本(训练集),从而在这些数据上得到非常好的结果。然而,在这种情况下,网络只是记住了训练样本,而当它在实际的使用场景中部署时,表现将会很差。这种错误被称为过拟合。
幸运的是,有一种非常成功的策略可以处理过拟合问题,我们已经提到过它。它的开始是将我们的完整数据集拆分为训练集和验证集,这一点我们在前面的步骤中已经做过了。
从理论角度来看,训练和验证损失通常表现出类似以下图形的行为:
图 3.18 – 损失与轮数的关系 – 理想
在图 3.18中,我们可以看到训练和验证损失通常是如何变化的(理想化的表现)。随着训练的进行,训练损失持续下降,始终在优化(尽管随着训练轮数的增加,下降速度变慢)。然而,验证损失达到一个点后不再继续下降,反而开始上升。验证损失最低点是模型达到最佳性能的地方,也是我们应该停止学习过程(早停)的时候。
让我们来看一下这种行为在实际中的表现。对于我们的问题,随着训练的进展,训练损失和验证损失是这样变化的:
图 3.19 – 损失与轮数的关系 – 实际
如我们在图 3.19中所见,验证损失比理想情况下更为嘈杂,早停也更难成功实现。一个非常简单的实现方法是每当验证损失减少时就保存模型。这样,我们总是可以确保在给定的训练轮数内,具有最佳(最低)验证损失的模型会被保存。这正是前面步骤中实施的方法。
定性评估
为了验证我们的模型是否与预期一致(即在预测房价时产生较低的误差),一种简单的方法是使用测试集中的随机输入(未见过的数据)来运行我们的模型。这可以通过以下代码轻松实现:
scaled_input = mx.nd.array([scaled_X_train_onehot_df.values[random_index]])
# Unscaled Expected Output
expected_output = y_test[random_index]
print("Unscaled Expected Output:", expected_output)
# Scaled Expected Output
scaled_expected_output = scaled_y_test[random_index]
print("Scaled Expected Output:", scaled_expected_output)
# Model Output (scaled)
output = net(scaled_input.as_in_context(ctx)).asnumpy()[0]
print("Model Output (scaled):", output)
# Unscaled Output
unscaled_output = sc_y.inverse_transform(output)
print("Unscaled Output:", unscaled_output)
# Absolute Error
abs_error = abs(expected_output - unscaled_output)
print("Absolute error: ", abs_error)
# Percentage Error
perc_error = abs_error / expected_output * 100.0
print("Percentage Error: ", perc_error)
上述代码片段将产生以下结果:
Unscaled Expected Output: [380000.]
Scaled Expected Output: [-0.4304741]
Model Output (scaled): [-0.45450553]
Unscaled Output: [370690.]
Absolute error: [9310.]
Percentage Error: [2.45]
正如预期的那样,错误率相当合理(仅为 2.45%!)。
重要说明
尽管我尽量保持代码的可复现性,包括为所有随机过程设置种子,但仍可能存在一些随机性来源。这意味着您的结果可能会有所不同,但通常误差的数量级会相似。
定量评估 – MAE
让我们计算MAE函数,如本章早些时候在定义回归损失函数和评估指标中所描述的那样:
Mean Absolute Error (MAE): [81103.97]
MAE 为75k 到$770 万不等,这个误差似乎是合理的。别忘了,估计房价是一个困难的问题!
定量评估 – MAPE
MAE(平均绝对误差)提供的值对于了解我们模型预测中的误差有多大或多小是有帮助的。然而,它并没有提供一个非常有意义的评价标准,因为相同的 MAE 值可能是通过不同的方式得到的:
-
对于所有房屋的较小误差:随着房屋价格的增加,绝对误差的数值会更高,因此,$80k 的 MAE 可能是相当不错的。
-
便宜房屋的误差很大:在这种情况下,$80k 的 MAE 意味着对于最便宜的房屋,误差可能是实际房价的 2 到 3 倍,甚至更糟。这种情况是非常糟糕的。
通常,我们可以在 MAE 的基础上添加另一个数字,通过类似的计算来提供相对误差率,而不仅仅依赖于绝对值。对于我们的模型,我们得到如下结果:
Mean Absolute Percentage Error (MAPE): [16.008343]
看起来我们的模型表现得还不错,得到了 16%的 MAPE!
定量评估 – 阈值与百分比
另一个我们可以考虑的问题是:我们准确预测了多少房屋(以百分比表示)的价格?
假设我们认为当预测价格误差小于 25%时,我们就认为价格预测是准确的。在我们的情况下,结果如下:
Houses with a predicted price error below 25.0 %: [81.23987971]
这一计算结果为 81%,做得不错!
此外,我们可以将我们正确预测的房屋百分比与误差阈值绘制成图:
图 3.20 – 正确估计的百分比
在图 3.20中,我们可以看到,正如预期的那样,认为误差在 25%以内即为准确预测,我们的模型能够正确预测超过 80%的数据。
它是如何工作的...
在这个示例中,我们探讨了如何评估回归模型。为了正确做到这一点,我们重新审视了之前将完整数据集分割成训练集、验证集和测试集的决策。
在训练过程中,我们使用训练集来计算梯度并更新模型参数,验证集则用于确认模型的实际表现。之后,为了评估我们的模型性能,我们使用了测试集,这是唯一一组未见过的数据。
我们通过计算随机样本的输出,发现了定性描述模型行为的价值,并通过探索 MAE 和 MAPE 的计算和图表,定量评估了我们的模型性能。
我们通过定义什么是准确预测(设置阈值)并通过调整阈值绘制模型行为,结束了这个过程。
还有更多…
深度学习在多个任务上已经超越了人类水平的表现。然而,正确评估模型对于验证模型在真实生产环境中部署后的表现至关重要。我觉得以下这个关于人工智能在多个任务上达到人类水平表现的小清单非常有趣: venturebeat.com/2017/12/08/…
当评估没有正确进行时,模型可能不会按预期行为表现。以下文章详细描述了这种类型的两个最重要的大规模问题(分别发生在 2015 年的谷歌和 2016 年的微软):
-
谷歌错误地将黑人标记为“猩猩”,展示了 算法的局限性: www.wsj.com/articles/BL…
-
Jim 的统计学:
statisticsbyjim.com/regression/r-squared-invalid-nonlinear-regression/
不幸的是,尽管这些问题现在变得越来越少见,但它们仍然存在。一个包含这些问题的数据库已经发布,并且每当报告出现这些问题时都会进行更新: incidentdatabase.ai/.
为了防止这些问题,谷歌制定了一套原则来开发负责任的人工智能。我强烈建议所有 AI 从业人员遵守这些原则: ai.google/principles/。
在这个阶段,我们已经完成了整个回归问题的旅程:我们探索了回归数据集,决定了评估指标,定义并初始化了模型。我们理解了优化器、学习率、批量大小和训练周期的最佳超参数组合,并使用提前停止进行训练。最后,我们通过定性和定量的方式对模型进行了评估。