MXNet-深度学习秘籍-全-

MXNet 深度学习秘籍(全)

原文:annas-archive.org/md5/22b009d5e2069762a5538fce4fc0f3fd

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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/errata 并填写表单。

盗版:如果您在互联网上发现任何我们作品的非法复制品,我们将非常感激您提供其位置或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。

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

分享您的想法

一旦您阅读了《深度学习与 MXNet Cookbook》,我们非常希望听到您的想法!请点击此处直接进入亚马逊的书籍评论页面并分享您的反馈

您的评论对我们和技术社区都很重要,将帮助我们确保提供优质的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但又无法随身携带纸质书籍吗?

您的电子书购买无法与您选择的设备兼容吗?

不用担心,现在购买每本 Packt 书籍时,您都可以免费获得该书的无 DRM PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。

福利不止于此,您可以每天获得独家折扣、新闻通讯以及丰富的免费内容直接发送到您的邮箱。

按照以下简单步骤获取福利:

  1. 扫描二维码或访问下面的链接

packt.link/free-ebook/9781800569607

  1. 提交您的购买证明

  2. 就是这样!我们会直接通过电子邮件将您的免费 PDF 和其他福利发送给您。

第一章:快速启动 MXNet

MXNet是最常用的深度学习框架之一,是一个 Apache 开源项目。在 2016 年之前,Amazon Web ServicesAWS)的研究团队并没有使用特定的深度学习框架,而是允许每个团队根据自己的选择进行研究和开发。尽管一些深度学习框架拥有蓬勃发展的社区,但有时 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 安装:

  1. 打开您喜欢的网页浏览器。在本书中,我一直使用 Google Chrome 浏览器。访问 colab.research.google.com/ 并点击 新建笔记本

图 1.1 – Google Colab 启动画面

图 1.1 – Google Colab 启动画面

  1. 更改笔记本的标题 – 例如,如下截图所示,我已将标题更改为DL with MXNet Cookbook 1.1 安装 MXNet

图 1.2 – 一个 Google Colab 笔记本

图 1.2 – 一个 Google Colab 笔记本

  1. 将 Google Colab 的运行时类型更改为使用 GPU:

    1. 运行时 菜单中选择 更改运行时类型

图 1.3 – 更改运行时类型

图 1.3 – 更改运行时类型

  1. Notebook 设置 中,选择 GPU 作为 硬件加速器 选项。

图 1.4 – 硬件加速器 | GPU

图 1.4 – 硬件加速器 | GPU

验证并安装库

在本节中,转到第一个单元格(确保它是代码单元格),并输入以下命令:

  1. 通过输入以下命令验证 Python 版本:

    import platform
    platform.python_version()
    3.7.10
    

    检查版本,并确保版本为 3.7 或以上。

重要提示

在 Google Colab 中,可以通过在命令前加上 ! 字符直接运行命令,就像在 Linux 终端中一样。也可以尝试其他命令,例如 !ls

  1. 现在需要验证 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

  1. 验证硬件中是否存在 Intel CPU:

    !lscpu | grep 'Model name'
    Model name: Intel(R) Xeon(R) CPU @ 2.20GHz
    

    处理器越新越好,但在本书的应用场景中,GPU 的依赖性比 CPU 更大。

  2. 验证硬件中是否存在 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。

  1. 验证 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
    
  2. 根据您的硬件配置安装 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 后缀,如先前推荐的安装方法所示。

  1. 通过以下两个步骤验证 MXNet 是否成功安装:

    1. 以下命令必须没有错误,并且必须成功显示 MXNet 版本 1.9.1:
    import mxnet
    mxnet.__version__
    
    1. 以下列出的特性包含 CUDACUDNNMKLDNN 特性:
    features = mxnet.runtime.Features()
    print(features)
     print(features.is_enabled('CUDA'))
     print(features.is_enabled('CUDNN'))
     print(features.is_enabled('MKLDNN'))
    

    输出将列出所有特性,并且每个特性后面都会显示 True

  2. 安装 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()等函数访问数据时,执行将会在调用这些函数之前完成,这可能会给人一种这些函数实际上很耗时的错误印象:

  1. 让我们检查一个具体的示例,并从在 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
    
  2. 然而,让我们看看如果没有调用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_nptimings_mx_cputimings_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) 矩阵乘法

图 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,并将使用该库中最常见的数据结构,matplotlibpyplotseaborn库。因此,我们必须运行以下代码:

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 – 价格分布

图 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 – 房屋特征相关矩阵

图 2.2中请注意,单元格越暗,相关值越大。

为了强调第一行(它显示了价格与输入特征之间的关系最重要),我们将运行以下代码:

house_corr["price"].drop(["price"]).sort_values(ascending = False).plot.bar(figsize=(5,5))
plt.show()

我们得到以下结果:

图 2.3 – 房屋特征:价格相关性

图 2.3 – 房屋特征:价格相关性

图 2.22.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)邻居居住面积

图 2.4 – 价格与多个特征的比较:a)居住面积,b)楼上面积,c)邻居居住面积

正如预期,绘制的图表非常相似,表明这些变量之间存在高度的相关性(以及冗余性)。此外,我们还可以观察到,数据点的最大密度出现在价格低于 300 万美元和面积小于 5,000 *方英尺的区域。由于我们大部分数据位于这些区域,我们可以将这些范围之外的房屋视为异常值并将其移除。

等级分析

类似地,我们可以将等级特征与价格进行比较:

图 2.5 – 房屋等级与价格的关系

图 2.5 – 房屋等级与价格的关系

等级与价格之间有明显的直接相关性;等级越高,价格越高。值得注意的是,等级最高的房屋出现频率较低。

房间分析

让我们更详细地展示价格与楼层数卧室数浴室数之间的关系:

图 2.6 – 价格与多个特征的比较:a)楼层数,b)卧室数,c)浴室数

图 2.6 – 价格与多个特征的比较:a)楼层数,b)卧室数,c)浴室数

从图中可以观察到以下几点:

  • 图 2**.6 (a)中,我们可以看到,对于较少的楼层数(1-3 层),房价与楼层数之间有直接的相关性。然而,从第四层开始,这种相关性消失了,这表明该段数据缺乏样本(四层或更多层的房子较为少见)。

  • 图 2**.6 (b)中,与卧室数量的比较情况与前一个楼层数量的比较图表相似。我们可以看到,对于较少卧室的房子,房价与卧室数之间有直接的相关性。然而,从四间卧室开始,这种相关性消失,其他特征需要被考虑进去。

重要提示

仔细查看数据时,你会发现,在索引为15870的那一行中,存在一个异常值;那是一栋有 33 间卧室的房子。我不知道这是否为房子的实际卧室数(我预计不是!),但为了正确分析数据集,这栋房子作为异常值已被从中移除。详情请查看代码。

  • 图 2**.6 (c)中,我们可以看到浴室数量与价格之间有直接的相关性;然而,也存在一定的不确定性(随着浴室数量的增加,图表变得更宽)。

视野分析

在本节中,我们将更详细地分析视野质量和水滨视野(房屋是否有水滨视野)与价格之间的联系:

图 2.7 – 视野质量(a)和水滨视野(b)与价格的关系

图 2.7 – 视野质量(a)和水滨视野(b)与价格的关系

从这些单独的图表中,得出结论稍微有些困难。似乎还需要其他变量来看到视野质量与价格之间的明显联系,水滨视野也是如此。

建造年份和翻新年份分析

以下图表展示了房屋建造年份和是否以及何时翻新的特征与价格的相关性:

图 2.8 – 价格与建造年份(a)和翻新(b)的比较

图 2.8 – 价格与建造年份(a)和翻新(b)的比较

从图中,你可以观察到以下几点:

  • 图 2.8(a)中,我们可以看到价格呈现轻微的线性上升,表明房屋建造得越新,价格就越贵。

  • 对于图 2.8(b),我们没有分析年份,而是将数据集分为两类——翻新过的房屋和未翻新过的房屋——并将这两类与价格进行对比。无论如何,这样做得出结论稍微有些困难。似乎还需要其他变量来看到翻新年份与价格之间的明显联系。

位置分析

在本节中,我们将更详细地分析纬度和经度与价格之间的联系:

图 2.9 – 位置与价格的关系

图 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)。此外,为了绘制数据及我们将计算的多个可视化图形,我们将使用 matplotlibpyplotseaborn 库。因此,我们必须运行以下代码:

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对应setosa1对应versicolor2对应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 – 花朵特征相关性矩阵

请注意在图 2.10中,单元格越深,相关性值越大。

为了强调第一行(最重要的是它显示了鸢尾花类别与输入特征之间的关系),我们将运行以下代码:

iris_corr["class"].drop(["class"]).sort_values(
    ascending = False).plot.bar(figsize=(5,5))
plt.show()

这是我们得到的结果:

图 2.11 – 花朵特征:鸢尾花类别相关性

图 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 – 花朵特征对比图

图 2.12 – 花朵特征对比图

从这组图表中,我们可以得出以下结论:

  • Setosa 鸢尾花可以通过任何特征轻松区分

  • 不同鸢尾花类别之间的萼片特征有重叠

  • 花瓣特征与鸢尾花类别直接相关;也就是说,最小的数值指向setosa,中等的数值指向versicolor,而最大的数值指向virginica

  • versicolorvirginica的边界上存在一个重叠区域,当花瓣长度大于约 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 – 花卉特征小提琴图

图 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),帮助我们理解哪些特征对于预测服装类别是重要的(哪些是不重要的),以下是帮助我们进行分析的步骤:

  1. 确定数据结构

  2. 描述每个类别的示例

  3. 理解降维技术

  4. 可视化主成分 分析PCA

  5. 可视化t 分布随机邻域 嵌入t-SNE

  6. 可视化统一流形*似与 投影UMAP

  7. 可视化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 数据集标签

图 2.14 – Fashion-MNIST 数据集标签

让我们看看每个类别的样本长什么样。为了实现这一点,我们可以绘制每个类别的 10 个示例:

图 2.15 – Fashion-MNIST 数据集

图 2.15 – Fashion-MNIST 数据集

如我们在图 2.15中看到的那样,所有实例几乎都可以被人类很好地区分,除了T-shirt/topPulloverCoatShirt类别。

理解降维技术

除了数据集中包含的大量数据点外,图像中的特征数量(即每个图像的像素数)也非常高。在我们的玩具数据集中,每张图像有 784 个特征,可以看作是 784 维空间中的 1 个点。在这个空间中,分析特征之间的关系(例如我们在前面的数据集中探索的相关性)是非常困难的。此外,处理更高质量的图像(分辨率超过 1 百万像素,即超过 100 万个特征)并不罕见。对于一张 4K 图像,特征数量约为 800 万个。

因此,在本小节以及接下来的小节(关于PCAt-SNEUMAPPyMDE),我们将使用称为降维的技术。降维技术的核心思想是能够轻松地可视化高维特征,通常是 2D 或 3D,这是人类习惯使用的可视化方式。这些嵌入具有两个或三个组件,可以在 2D 或 3D 中绘制。这些表示是数据集依赖的;它们是学习得到的表示。

每种技术都有不同的方式来实现这一结果。在本书中,我们不会深入了解每种技术的原理,但感兴趣的读者可以在There’s more...部分找到更多信息。

还请注意,尽管每种技术不同,但它们都要求输入一个向量(特征向量)。这意味着一些空间信息会丢失。在我们的示例中,从 28x28 的图像中,我们将输入 784 个特征向量。

可视化 PCA

正如预期的那样,我们可以看到一些大簇(SneakerAnkle boot),而其他簇大多是重叠的(T-shirtPulloverCoat):

图 2.16 – Fashion-MNIST 2D PCA

图 2.16 – Fashion-MNIST 2D PCA

可视化 t-SNE

另一种降维技术是 t-SNE。该技术基于计算表示邻居相似性的概率分布。推荐的预处理步骤是先对 50 个特征进行 PCA,然后将这 50 个特征向量传递给 t-SNE 算法。这就是我们用来生成以下图表的方法:

图 2.17 – Fashion-MNIST 2D 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

图 2.18 – Fashion-MNIST UMA

在此可视化中,我们可以观察到与前面图表中类似的趋势;即,Bag 被聚集在上中部区域,Trouser 被聚集在下中部区域。然而,在此可视化中,我们还可以注意到左侧有一个簇,包含了Ankle bootSneakerSandal的数据,而右侧有一个重要的簇,包含了ShirtCoatDressT-shirt/top的数据,我们可以看到这些簇是如何相互重叠的。

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

!pip3 install umap-learn

可视化 PyMDE

另一种流行的技术是 PyMDE,它提供了有洞察力的可视化。PyMDE 允许两种主要方法:保持邻居关系(即保留数据的局部结构)和保持距离关系。这保持了数据中一对一距离等关系属性。保持邻居关系的方法类似于我们所看到的图表:

图 2.19 – Fashion-MNIST PyMDE

图 2.19 – Fashion-MNIST PyMDE

如我们在图 2.19中所见,PyMDE 可得出与 UMAP 非常相似的结论。

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

!pip3 install pymde

它是如何工作的...

要理解一个图像数据集,我们需要理解该数据集中图像之间的潜在联系。实现这一目标的一种有用方法是使用不同的可视化技术。

在这个教程中,我们学习了如何发现图像数据集中的模式。我们选择了一个经过充分研究的数据集——Fashion-MNIST,并学习了处理大规模数据集的最重要方法之一:批处理

我们通过查看数据集的内部结构以及实际图像的样貌来分析数据集,并尝试预测潜在的分类算法可能遇到的问题(例如外套和衬衫、短靴和运动鞋之间的相似性)。

每个像素都是每张图像的一个维度/特征,因此,为了处理它们,我们了解了一些降维技术:PCA、t-SNE、UMAP 和 PyMDE。通过这些可视化,我们能够验证并扩展我们对数据集的知识。

还有更多…

由于 MNIST 和 Fashion-MNIST 是经过充分研究的数据集,因此有许多资源可供参考。我个人推荐以下资源:

我们介绍了一些降维技术,但并没有深入了解它们。如果你想更好地理解每种技术的工作原理,我建议以下资源:

在代码中,你可以找到如何获取包含的可视化内容。此外,对于 PCA 和 t-SNE,由于组件数是一个变量,因此两者的 3D 图都被包含在内。

最后,对于那些有兴趣深入了解深度学习及其历史的读者,我推荐以下链接:www.skynettoday.com/overviews/neural-net-history

理解文本数据集 – 加载、管理和可视化 Enron 邮件数据集

*年来,自然语言处理NLP)是深度学习领域一个快速发展的领域。与计算机视觉(CV)类似,该领域的目标是在人类表现基础上超越现实世界数据集的表现。

在这个教程中,我们将探索最简单的 NLP 任务之一:文本分类。给定一组句子和段落,我们的任务是正确地将这些文本分类到给定的标签(类别)中。

最经典的文本分类任务之一是区分收到的邮件是否是垃圾邮件(spam)或非垃圾邮件(ham)。这些数据集是二元文本分类数据集(只有两个标签要分配,01,或者 hamspam)。

在我们的特定场景中,我们将使用一个真实世界的邮件数据集。该数据集是在 2000 年代初美国政府对安然丑闻进行调查时公开的。这一数据集首次发布于 2004 年,包含约 150 名用户的邮件,主要是安然公司高级管理层的邮件。本节仅使用其中的一个子集(称为enron1)。

数据集包含 5,171 封邮件,没有训练/测试集划分(所有示例都提供标签)。作为一个真实世界的数据集,邮件在主题、内容长度、词数和单词长度等方面差异很大,且默认情况下,数据集仅包含两个特征:

  • 0表示正常邮件1表示垃圾邮件

  • 文本:包括邮件的主题和正文

数据集包含 3,672 封正常邮件(约占 70%)和 1,499 封垃圾邮件(约占 30%);它是一个高度不*衡的数据集。

准备工作

该数据集遵循CC0 公共领域许可证,可以从www.kaggle.com/venky73/spam-mails-dataset 下载。

为了读取数据,我们将遵循与回归任务中类似的方法。我们将从 CSV 文件加载数据,并使用非常著名的 Python 库:pandaspyplotseaborn 来处理数据。因此,我们需要运行以下代码:

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

因此,为了加载数据,我们只需读取包含数据的文件(该文件可以从书籍的 GitHub 仓库中找到):

emails_df = pd.read_csv("spam_ham_dataset.csv")

这就是我们开始处理垃圾邮件数据集所需的全部内容。

如何执行...

在本节中,我们将进行探索性数据分析(EDA),帮助我们理解哪些特征对于预测邮件是否为垃圾邮件至关重要(哪些不重要)。以下内容不是以步骤形式呈现,请重新组织为更合适的引言或以步骤形式重新编排,并将圆点符号改为编号:

  1. 数据结构

  2. 每个类别的示例

  3. 内容分析

  4. 数据清洗

  5. N-gram 模型

  6. 词处理(分词、停用词、词干提取和词形还原)

  7. 词云

  8. 词嵌入(word2vec 和 全局词表示模型 (GloVe))

  9. 主成分分析(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.20 – 垃圾邮件数据集

如我们所见,数据集存在严重的类别不*衡问题。

内容分析

在这一部分,我们将分析邮件的长度及其分布:

图 2.21 – 邮件长度

图 2.21 – 邮件长度

存在一个大范围的异常值集,对应邮件字符数超过 5,000 的情况。让我们聚焦于大多数邮件所在的区域,并绘制邮件长度和单词数的图表:

图 2.22 – 邮件长度(详细)

图 2.22 – 邮件长度(详细)

图 2.23 – 邮件词汇数量

图 2.23 – 邮件词汇数量

重要提示

图 2.23中,我们通过指定每个以空格分隔的实体构成一个词,定义了一个没有语义或字典方法的词汇。这种方法有缺点,我们将在本节和第五章中进一步分析。

通过查看该图,我们可以得出结论:在邮件长度和单词数方面,垃圾邮件和合法邮件之间没有显著差异。我们需要更多地了解这些词汇,它们的含义及其之间的关系,以改善我们的分析。

因此,让我们首先看看数据集中哪些词最常见:

图 2.24 – 最常见的词汇

图 2.24 – 最常见的词汇

看到图 2.24后的第一个也是最重要的结论是,我们最初的空格分隔方法在处理真实世界的数据集时并不足够。标点符号错误和拼写错误非常常见,而且,正如预期的那样,像“the”和“to”这样的常见词对于区分垃圾邮件和合法邮件没有实质性帮助。

数据清理

让我们解决处理真实世界文本数据集时的一些常见问题:

  • 标点符号

  • 尾随字符

  • “说明”(方括号中的文本)

  • 包含数字和链接的词汇

  • 词汇subject(特定于我们的邮件数据集结构)

经过我们通过清理函数处理语料库(特定于我们问题的文本数据)后,结果更接*我们的预期:

图 2.25 – 最常见的词汇(清理过)

图 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)中最常见的二元组

图 2.26 – 在“ham”(a)和“spam”(b)中最常见的二元组

对于 trigrams(按类别),请参见下文:

图 2.27 – 在“ham”(a)和“spam”(b)中最常见的三元组

图 2.27 – 正常邮件(a)和垃圾邮件(b)中最常见的三元组

在这些图表中,我们可以开始把握两类之间的潜在差异:

  • 如果提到了Enron 公司,那很可能是一封合法的电子邮件。

  • 如果有礼貌的行动号召(“please let me know”),那很可能是一封合法的电子邮件。

  • 如果有链接,那很可能是垃圾邮件。

  • 如果有拼写错误(hou代替howect代替etc,等等),那很可能是一封合法的电子邮件。

  • 如果提到pills(药丸),那很可能是垃圾邮件(而且有重复的嫌疑)。

我们还发现了一些与电子邮件编码方式相关的细微差别:nbsp(用于非断行空格)。很可能是电子邮件解析器在文本中发现了一些结构不清的空格,并用nbsp关键字进行了替换。巧合的是,这些解析细节在垃圾邮件中比在合法邮件中出现得要多,这将有助于我们的分析。

文字处理

处理文本中的单词通常由四个步骤组成:

  1. 分词

  2. 停用词过滤

  3. 词干提取

  4. 词形还原

这些步骤各自具有一定的复杂性,因此我们将使用可用的库来执行这些步骤,例如自然语言工具包NLTK)。要安装它,请运行以下命令:

!pip3 install nltk

分词是处理文本并返回标记列表的步骤。每个单词是一个标记,但如果有标点符号,它们会成为单独的标记。不过,对于我们的语料库,这些在之前的步骤中已被移除。

请注意,在这个步骤中,我们已经从每封电子邮件的句子和段落列表(语料库)转换到所谓的词袋模型BOW),它与语料库中使用的词汇直接相关。

在我们将每个单词作为一个实体之后,我们可以移除之前已识别出的常见词,如“the”或“to”。这些被称为停用词,NLTK 包含多个语言的停用词集。我们将使用这个可用的集合来过滤我们的语料库。

词干提取是将派生(如果我们想更正式些,也包括屈折)词汇缩减到其词根的过程,词根称为词干。

词形还原是将多个不同形式的词汇归类为一个单一项的过程,这个单一项由单词的词根或词典形式(lemma)标识:

图 2.28 – 词干提取和词形还原

图 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)中的词云

图 2.29 – 正常邮件(a)和垃圾邮件(b)中的词云

在这些可视化中,我们可以清楚地看到Enronpleaselet know对合法邮件是相关的,而newnbspcompanimarketproduct通常与垃圾邮件相关。

词嵌入

到目前为止,我们已经了解了单个单词的表现(频率和长度)及其与其他单词的连接,主要通过最常见的 N-gram(双字组和三字组)。然而,我们还没有将这些单词之间的意义进行连接。例如,我们会预期Enroncorpcompanycompani,它的词干形式)在语义上是相*的。因此,我们希望有一种表示方式,使得具有相似意义的单词有相似的表示方式。此外,如果这种表示方式具有固定数量的维度,我们就能方便地对单词进行比较(找出相似性)。这些就是词嵌入(word embeddings),其表示就是一个向量。

从单词生成向量的方法有无数种;例如,实现这一目标的最简单方法是生成与我们的词汇量相同数量的维度(向量的特征,即矩阵表示中的列),然后在每封邮件中(矩阵表示中的一行),对于每个包含的单词,我们可以在该单词在词汇表中的列中标注1(打钩),从而得到形如[0, 0, 0, 0......, 1, 0, 0, 0,..., 1....]的向量表示。这种表示方法称为独热编码,但它效率非常低,因为特征的数量等于语料库中不同单词的数量,也就是词汇表的长度,这通常是非常大的(我们的简化词汇表约有 50 万个单词)。

因此,我们将看一看更优化的单词表示方式:word2vec 和 GloVe:

  • word2vec:该算法由谷歌于 2013 年开发,并在Google News语料库上进行了预训练。它的语料库包含 30 亿个单词,词汇量有 300 万个不同的单词,每个单词用 300 个特征表示。该算法的直观思路是通过考虑上下文(周围单词)来计算给定单词的概率。窗口大小(一次查看多少个单词)是模型的一个参数,并且是常量,这使得模型仅依赖于每个单词的局部上下文。

  • word2vec结合了单词共现(全局统计)来提供更完整的表示。

通过这些表示,我们现在可以在单词的新的向量表示中计算操作:

  • strongerstrong的关系就像weakerweak的关系:

    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 嵌入

图 2.30 – PCA 用于(a) word2vec 和(b) GloVe 嵌入

此外,我们还可以应用 t-SNE:

图 2.31 – t-SNE 用于(a) word2vec 和(b) GloVe 嵌入

图 2.31 – t-SNE 用于(a) word2vec 和(b) GloVe 嵌入

从之前的图中,我们可以看到垃圾邮件和正常邮件的词汇在我们的嵌入空间中非常接*,这使得分离这些簇变得非常困难。这是因为我们使用的是预训练的嵌入,来自新闻和维基百科数据集。这些数据集及其相应的嵌入并不适合我们的任务。我们将在第五章中看到如何训练词嵌入以获得更好的结果。

重要提示

对于 PCA 和 t-SNE,我们可以选择三个组件,而不是两个,这样可以得到一个三维图。有关代码,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook

它是如何工作的...

要理解文本数据集的语料库,我们需要理解该语料库中单词之间的潜在联系。实现这一目标的一个有用方法是通过不同的语料库可视化来帮助我们理解。

在这个食谱中,我们学会了如何发现文本数据集中的模式。我们选择了一个不*衡的数据集——Enron 电子邮件数据集,并学习了如何处理二分类数据集。

我们通过查看数据集的内部结构,了解类不*衡的情况,并检查最常见的词汇,寻找其中的模式和错误。我们清理了数据集,移除了标点符号,并绘制了最常见的二元组三元组,并注意到几个有助于我们正确分类电子邮件的关键词。

我们学会了如何生成一些酷炫的可视化效果,如词云,并且理解了为什么词嵌入如此重要,并使用我们之前学到的降维技术对其进行了绘制。

还有更多内容…

如果你想了解更多关于 Enron 电子邮件数据集和 Enron 丑闻的信息,以下链接将有所帮助:

我们简要概述了几个重要的概念,我邀请你进一步了解:

此外,我们只是浅尝辄止地触及了词嵌入所能提供的内容:

第三章:求解回归问题

在前几章中,我们学习了如何设置和运行 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

理解回归模型的数学

如我们在上一章看到的,回归问题是一种监督学习问题,其输出是来自连续分布的一个数字,比如房价或公司股票价格的预测值。

我们可以用来解决回归问题的最简单模型是线性回归模型。然而,这些模型对于简单问题非常强大,因为它们的参数可以训练,并且在涉及的参数数量较少的情况下,速度很快且易于解释。正如我们将看到的,参数的数量完全取决于我们使用的特征数量。

线性回归模型的另一个有趣属性是,它们可以通过神经网络来表示,而神经网络将是我们在本书中使用的大多数模型的基础,因此我们将使用这种基于神经网络的线性回归模型。

最简单的神经网络模型被称为感知机,这是我们不仅在本食谱中,而是在整个章节中要研究的基础模块。

准备就绪

在深入理解我们的模型之前,我想提一下,对于本食谱中的数学部分,我们将遇到一些矩阵运算和线性代数,但这绝对不会很难。

如何操作...

在本食谱中,我们将通过以下步骤进行操作:

  1. 数学建模生物神经元

  2. 定义回归模型

  3. 描述基本激活函数

  4. 定义特征

  5. 初始化模型

  6. 评估模型

数学建模生物神经元

感知机最早由美国心理学家 Frank Rosenblatt 于 1958 年在康奈尔航空实验室提出,它是初步尝试复制我们大脑中神经元处理信息的方式。

Rosenblatt 分析了生物神经元并开发了一个行为相似的数学模型。为了比较这些架构,我们将从一个非常简单的神经元模型开始。

图 3.1 – 生物神经元

图 3.1 – 生物神经元

如我们在图 3.1中看到的,神经元由三个主要部分组成:

  • 树突:神经元从其他神经元接收输入的地方。根据连接的强度,输入在树突中会被增强或减弱。

  • 细胞体胞体:包含细胞核,这是接收来自树突的所有输入并对其进行处理的结构。细胞核可能会触发电信号并传递给其他神经元。

  • 轴突/轴突末端:这是输出结构,用于与其他神经元传递信息。

Rosenblatt 将之前简化的神经元模型赋予了某些数学属性:

图 3.2 – 感知机

图 3.2 – 感知机

  • 权重:这将通过将输入或特征与一组权重(W图 3.2 中,而 j 代表模型中的任何神经元)相乘来模拟树突的行为。

  • 偏置:在细胞核中输入信号的组合将被建模为带有偏置的求和(θ图 3.2 中)和一个处理函数,称为 激活函数。(我们将在下一步描述这些函数。)

  • 输出:要么连接到其他神经元,要么是整个模型的直接输出(o图 3.2 中)。

比较 图 3.1图 3.2,我们可以看到生物神经元的简化模型与感知器之间的相似之处。此外,我们还可以看到这些部分是如何连接在一起的,从处理输入到输出结果。

定义回归模型

因此,从数学角度来看,使用矩阵乘法,我们可以写出以下模型方程 y = f(WX + b),其中 W 是权重向量 [W1, W2, …. Wn],(n 是特征的数量),X 是特征向量 [X1, X2, …. Xn]b 是偏置项,f() 是激活函数。对于我们将处理的回归情况,我们将使用线性激活函数,其中输出等于输入。

因此,在我们的例子中,激活函数是恒等函数(输出等于输入),我们有 y = WX + b

我们可以使用 MXNet 及其 NDArray 库轻松实现这一点:

# Perceptron Model
def perceptron(weights, bias, features):
return mx.nd.dot(features, weights) + bias

就这样!这就是我们基于 X 输入以及模型的 Wb 参数的 y 神经网络输出。

重要提示

在 Rosenblatt 的原始论文中,期望的输出是 0 或 1(分类问题),为了满足这一要求,激活函数被定义为阶跃函数(如果输入小于 0,则输出 0;如果输入大于或等于 0,则输出 1)。这是 Rosenblatt 神经元模型的一个最大限制,后来提出了不同的激活函数来改善模型的表现。

在深度学习网络中,我们不会仅使用单个神经元(感知器)作为模型。通常,多个感知器层被堆叠在一起,层数也被称为网络的 深度

图 3.3 – 深度学习网络

图 3.3 – 深度学习网络

这些网络非常强大,并且已经证明在多个领域匹配或超越了人类水*的表现,包括计算机视觉中的图像识别和自然语言处理中的情感分析。

描述基本激活函数

回归问题中最常见的激活函数是线性激活函数和 ReLU 激活函数。我们简要描述一下它们。

图 3.4 – 回归的激活函数

图 3.4 – 回归的激活函数

线性激活函数

在此函数中,输出等于输入。它没有界限,因此适用于无界的数值输出,正如回归问题的输出所示。

ReLU

修正线性单元激活函数非常类似于线性激活函数:其输出等于输入,但在这种情况下,只有当输入大于 0 时,输出才等于输入;否则输出为 0。此函数适用于仅将正信息传递到下一层(稀疏激活),并且还提供更好的梯度传播。因此,它在深度学习网络的中间层中非常常见。

重要说明

正如我们将在接下来的内容中看到的,训练涉及迭代地计算新的梯度,并使用这些计算来更新模型参数。当使用激活函数(如 sigmoid)时,层数越多,梯度变得越小。这个问题被称为梯度消失问题,而 ReLU 激活函数在这种情况下表现得更好。

定义特征

到目前为止,我们已经从理论上定义了我们的模型及其行为,但我们尚未使用我们的任务框架或数据集来定义它。在本节中,我们将开始以更实际的方式进行工作。

定义模型的下一步是决定我们将使用哪些特征(输入)。我们将继续使用在第二章《与 MXNet 合作并可视化数据集:Gluon 和 DataLoader》中遇到的房屋销售数据集。这个数据集包含了 21,613 栋房屋的数据,包括价格和 19 个输入特征。虽然在我们的模型中我们将使用所有输入特征,但在上述配方中,我们看到对房价贡献最大的三个非相关特征如下:

  • 居住面积(*方英尺)

  • 等级

  • 卫生间数量

在初步研究中,我们将使用这三个特征。选择这些特征后,如果我们显示前五个房屋的数据,我们将看到以下内容:

图 3.5 – 房屋价格的过滤特征

图 3.5 – 房屋价格的过滤特征

在之前分析这个数据集时,我们没有利用的一条路径是,grade不像其他特征那样是连续的;它的值来自一个离散的集合。这种类型的特征被称为分类特征,可以是名义型的或有序型的。名义型特征是类名——例如,我们可以将房屋的建筑风格作为一个特征,该特征的值可以是维多利亚式、艺术装饰风格、工匠风格等。而有序型特征则是类编号。在我们的例子中,grade是一个有序特征,由按顺序排列的数值组成(1 为最差,13 为最好)。

在这两种情况下,分类特征可以通过不同的方式以数字形式表示,这有助于我们的模型更好地学习特征与输出之间的关系。处理分类特征的方法有多种。在这个例子中,我们将使用最简单的方式之一——独热编码方案。

提示

你可以在本食谱末尾的更多内容部分找到有关处理分类数据的更多信息。

使用独热编码方案,每个类别都会被分解为它自己的特征,并相应地分配一个二进制值。在我们的例子中,grade包含从 1 到 13 的整数值,因此,我们向输入向量中添加了 13 个新特征。这些新特征的值将为 0 或 1。例如,对于一个 1 级的房子,特征向量如下所示:

图 3.6 – 等级特征的独热编码

图 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这一分类特征。处理分类数据有多种方法,相关内容可以在此链接中找到:https://towardsdatascience.com/understanding-feature-engineering-part-2-categorical-data-f54324193e63。

关于初始化和评估的更多信息,请继续阅读本章中的后续食谱。

定义回归的损失函数和评估指标

在前一篇食谱中,我们定义了输入特征,描述了我们的模型,并对其进行了初始化。那时,我们传入了一栋房子的特征向量来预测价格,计算了输出,并将其与预期输出进行了比较。

在前一篇食谱的结尾,我们通过比较模型的预期输出和实际输出,直观地了解了模型的好坏。这就是“评估”模型的含义:我们评估了模型的性能。然而,这一评估并不完全,原因有几个,因为我们没有正确地考虑到一些因素:

  • 我们只对一栋房子进行了模型评估——那么其他的房子呢?我们如何在评估中考虑到所有房子呢?

  • 值之间的差异是否是衡量模型误差的准确方法?还有哪些操作是有意义的?

在本食谱中,我们将讨论如何评估(即“评估”)我们模型的性能,并研究适合此目的的函数。

此外,我们将介绍一个在优化(即训练)模型时非常重要的概念:损失函数。

准备工作

在定义一些用于评估回归模型和计算其损失的有用函数之前,让我们明确一下我们将用来评估模型的函数的两个必需属性和三个期望属性:

  • [必需的] 连续性:显然,我们希望我们的评估函数在某些潜在的误差值上不会是未定义的,这样我们就可以在大量的(预期输出,实际模型输出)对上使用这些函数。

  • [必需的] 对称性:通过一个例子很容易解释这个问题。假设一栋房子的价格是 220 万美元——我们希望我们的评估函数能够以相同的方式评估模型,无论输出是 200 万美元还是 240 万美元,因为这两个值离预期值的距离是相同的,只是方向不同。

  • [期望的] 稳健性:同样,这个问题通过一个例子更容易解释。以之前提到的例子为例,假设我们有 2.4 百万美元和 2.8 百万美元两个输出值。与预期值 220 万美元相比,误差已经很大,因此我们不希望由于损失/评估函数的影响使误差变得更大。从数学的角度来看,我们不希望误差呈指数增长,否则可能导致计算发散至不是一个数字(NaN)。使用稳健函数时,大误差不会导致计算发散。

  • [期望的] 可微性:这是所有属性中最不直观的一项。通常,我们的目标是将误差率尽可能接*零。然而,这只是一个理论场景,只有当我们拥有足够的数据来完美描述问题,并且模型足够大,能够表示从数据到输出值的映射时,才会发生。在现实中,我们永远无法确保符合这两个假设,因此,零误差的不切实际期望会转化为数据和模型的最小误差。我们只能通过计算函数的微分函数来检测函数的最小值,因此才有了这个可微性属性。幸运的是,可微性意味着连续性,因此如果我们的函数满足属性 #4,它也会自动满足属性 #1。

  • [期望的] 简洁性:满足所有属性的函数越简单越好,因为这样我们可以更直观地理解结果,并且从计算上讲也不会太昂贵。

提示

不仅评估函数必须满足这些标准。正如我们将在下一个章节看到的,它们还必须被一个对训练非常重要的函数所满足,即损失函数

如何操作...

让我们讨论一些评估和损失函数,并分析它们的优缺点。我们将描述的函数如下:

  1. *均绝对误差

  2. 均方误差

  3. *滑 L1 损失

*均绝对误差

我们要研究的第一个函数根据之前描述的五个属性几乎完美。这个函数的直观想法是使用值之间的差异作为这些值之间距离或误差的指标。我们应用 abs 函数,即绝对值,使其变得对称:

MAE = 1 _ n ∑ j=1 n | y j − ˆ y j|

当绘制时,该函数生成以下图形:

图 3.7 – MAE 图

图 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.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 – *滑的*均绝对误差图

图 3.9 – *滑的*均绝对误差图

如果我们根据已定义的属性分析这个函数,我们会看到所有属性都得到了满足。

重要提示

这个函数可以用于损失函数。在这种情况下,通常称之为*滑 L1 损失。

它是如何工作的...

在本章的第一个例子中,我们设计了基于简单生物神经元的第一个回归模型。我们随机初始化了其参数,并进行了第一次简单的评估。结果并不好,我们推测这是由于两个原因:我们的评估机制不够稳健,模型参数没有优化。

在这个例子中,我们探讨了如何改进第一个原因:评估。我们涵盖了三种最重要的评估指标,并提到了它们与损失函数的关系,我们将在下一个例子中详细探讨这些内容。

此外,我们讨论了哪些评估指标更好,探讨了 MAE 是如何稳健的,但不幸的是不可微分,MSE 是可微分的,但它允许异常值影响指标(这并不理想)。我们通过结合这两种函数,得到了两者的优点。

还有更多...

在这个例子中,我们没有探索的一个非常有趣的评估函数集是决定系数及其扩展。然而,这个集仅用于线性回归建模。更多信息可以通过以下链接获取:

此外,在回归问题中,通常可以使用许多不同的函数进行评估和损失计算;你可以参考这个链接了解更多细节:machine-learning-note.readthedocs.io/en/latest/basic/loss_functions.html

训练回归模型

在监督学习中,训练是朝着特定目标优化模型参数的过程。它通常是解决深度学习问题中最复杂、最耗时的步骤。

在本方法中,我们将访问训练模型所涉及的基本概念。我们将应用这些概念来解决我们在本章中先前定义的回归模型,并结合我们讨论过的函数的使用。

我们将使用在第二章**中看到的数据集预测房价:《使用 MXNet 与数据可视化:Gluon》和 DataLoader*。

准备工作

为了理解这个方法,我们需要熟悉一些概念。这些概念定义了训练的进行方式:

  • 损失函数:训练过程是一个迭代优化过程。随着训练的进行,期望模型能够在与模型实际输出进行比较的操作中表现得更好。这个操作就是损失函数,也称为目标函数、成本函数或代价函数,它是在训练过程中被优化的。

  • 优化器:在每次训练迭代过程中,模型的每个参数都会通过一个量(通过损失函数计算)进行更新。优化器是一种定义如何计算该量的算法。优化器最重要的超参数是学习率,它是应用于计算量的乘数,用于更新参数。

  • 数据集划分:当模型能够在真实世界中达到最佳表现时,停止训练对深度学习项目的成功至关重要。一种实现此目标的方法是将数据集划分为训练集、验证集和测试集。

  • 训练轮次:这是训练过程将运行的迭代次数。

  • 批量大小:每次分析的训练样本数,用于生成梯度估计。

如何做到...

在本方法中,我们将创建自己的训练循环,并评估每个超参数如何影响训练。为此,我们将遵循以下步骤:

  1. 改进模型

  2. 定义损失函数和优化器

  3. 划分数据集

  4. 分析公*性和多样性

  5. 定义训练轮次和批量大小

  6. 整合所有内容以形成训练循环

改进模型

为了解决这个问题,我们在前面的食谱中探讨的架构(感知机)将不够用。我们将多个感知机堆叠在一起,并通过不同的层连接它们。这种架构被称为多层感知机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 PReLUKaiming 初始化: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})

切分我们的数据集

在所有数据科学项目中,需要考虑的最重要的事情之一就是在我们将模型应用于新的数据时,已训练模型在未见过的数据上的表现如何。对于监督学习,在训练和评估过程中,我们使用已知的(期望的)输出数据,那么如何确保我们在新数据上使用模型时,它能按预期表现呢?

我们通过将数据集分成三个部分来解决这个问题:

  1. 训练集:训练集在训练过程中用于计算模型参数的更新。

  2. 验证集:验证集在训练过程中用于检查每个周期模型的改进情况(或没有改进),即之前计算的那些更新。

  3. 测试集:最后,一旦训练完成,我们可以计算模型在未见数据上的表现,这就是测试集,是数据集中唯一未在训练中用于提升模型的部分。

此外,为了保证稳定的训练,使得我们的模型能正确处理数据集外的数据,数据的划分需要考虑以下因素:

  • 划分大小:这取决于可用数据的数量和任务的性质。典型的训练/验证/测试数据划分比例为 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 – 训练集、验证集和测试集的价格分布

图 3.10 – 训练集、验证集和测试集的价格分布

尽管我们可以看到价格低于$500k 的房屋出现了小幅下降,但三个数据集中的价格分布在很大程度上是均衡的,且不需要进行手动修改。

居住面积(*方英尺)如下所示:

图 3.11 – 训练集、验证集和测试集的居住面积*方英尺图

图 3.11 – 训练集、验证集和测试集的居住面积*方英尺图

我们在这里看到的最大差异是由于少数高价房产。我们甚至可以将这些看作是异常值,如果我们的训练参数选择得当,这不应影响我们的预测能力。

浴室数量如下所示:

图 3.12 – 训练集、验证集和测试集中的浴室数量分布

图 3.12 – 训练集、验证集和测试集中的浴室数量分布

这种分布在我们的验证集和测试集中得到了很好的体现。

评分如下所示:

图 3.13 – 训练集、验证集和测试集的评分

图 3.13 – 训练集、验证集和测试集的评分

这种分布在我们的验证集和测试集中也得到了很好的体现。

将所有个别分析结果汇总,我们可以得出结论:我们的训练集、验证集和测试集相当好地代表了完整数据集。我们必须记住,我们的数据集有其自身的局限性(尽管我必须说,对于所选特征,它表现得相当不错):

  • 价格: [75k\(, 7.7M\)]

  • 居住面积(*方英尺):[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 优化器的损失

图 3.14中,我们可以得出结论,对于 SGD 优化器,学习率(LR)值在 10^-1 到 1.0 之间是最佳的。此外,我们可以看到,对于非常大的 LR 值(> 2.0),算法会发散。这就是为什么在寻找最佳学习率值时,最好从小值开始。

让我们分析一下,当变动学习率LR)并保持其他参数不变时,Adam 优化器的训练损失和验证损失是如何变化的:训练轮数 = 100,批次大小 = 128,损失函数 = HuberLoss

图 3.15 – 当变动学习率时,Adam 优化器的损失

图 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 优化器的损失

图 3.16中,我们可以得出结论,对于 Adam 优化器,批次大小在 64 到 1,024 之间提供最佳结果。

训练轮数

另一个超参数是训练轮数(epochs),即优化器处理完整训练集的次数。

让我们分析在 Adam 优化器下,当改变训练轮次(epochs)时,训练损失和验证损失的变化,同时保持其他参数不变:LR = 10-2,Batch Size = 128,Loss fn = HuberLoss

图 3.17 – Adam 优化器在改变训练轮次时的损失

图 3.17 – Adam 优化器在改变训练轮次时的损失

图 3.17 中,我们可以得出结论,大约 100-200 轮次对于我们的问题是合适的。在这些值下,很可能会在此之前取得最佳结果。

它是如何工作的...

在我们解决回归问题的过程中,我们在这篇教程中学会了如何最优地更新模型的超参数。我们理解了每个超参数在训练循环中的作用,并对每个超参数进行了单独的消融研究。这帮助我们理解了在单独修改每个超参数时,训练损失和验证损失的表现。

对于我们当前的问题和选择的模型,我们验证了最佳超参数组合如下:

  • 优化器:Adam

  • 学习率:10-2

  • Batch Size:128

  • 训练轮次:200

在训练循环结束时,这些超参数给我们带来了 0.10 的训练损失和 0.10 的验证损失。

还有更多...

在我们的模型定义中,我们引入了三个新概念:批量归一化dropoutscaling。我认为以下链接对理解这些高级话题非常有用:

在初始化方面,本文详细探讨了 Xavier 和 Kaiming 方法(包括研究论文的链接): pouannes.github.io/blog/initialization/

在本教程中,我们深入探讨了两种优化器,SGDAdam,的表现。这是两个最重要且表现最好的优化器;然而,还有许多其他优化器,有些可能更适合你的特定问题。

一个学习 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。

此外,我们还将看看训练如何对评估产生重大影响。

如何操作……

在开始模型评估之前,让我们先讨论如何衡量我们的模型训练表现。因此,本食谱中的步骤如下:

  1. 衡量训练表现——过拟合

  2. 定性评估

  3. 定量评估

测量训练表现 – 过拟合

深度学习网络非常强大,在多种问题上超越了人类水*的表现。然而,如果不加以控制,这些网络也可能产生不正确和意外的结果。最重要且常见的错误之一发生在网络充分发挥其能力时,它会记住正在展示的样本(训练集),从而在这些数据上得到非常好的结果。然而,在这种情况下,网络只是记住了训练样本,而当它在实际的使用场景中部署时,表现将会很差。这种错误被称为过拟合

幸运的是,有一种非常成功的策略可以处理过拟合问题,我们已经提到过它。它的开始是将我们的完整数据集拆分为训练集和验证集,这一点我们在前面的步骤中已经做过了。

从理论角度来看,训练和验证损失通常表现出类似以下图形的行为:

图 3.18 – 损失与轮数的关系 – 理想

图 3.18 – 损失与轮数的关系 – 理想

图 3.18中,我们可以看到训练和验证损失通常是如何变化的(理想化的表现)。随着训练的进行,训练损失持续下降,始终在优化(尽管随着训练轮数的增加,下降速度变慢)。然而,验证损失达到一个点后不再继续下降,反而开始上升。验证损失最低点是模型达到最佳性能的地方,也是我们应该停止学习过程(早停)的时候。

让我们来看一下这种行为在实际中的表现。对于我们的问题,随着训练的进展,训练损失和验证损失是这样变化的:

图 3.19 – 损失与轮数的关系 – 实际

图 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 为$81k。考虑到价格从$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 – 正确估计的百分比

图 3.20中,我们可以看到,正如预期的那样,认为误差在 25%以内即为准确预测,我们的模型能够正确预测超过 80%的数据。

它是如何工作的...

在这个示例中,我们探讨了如何评估回归模型。为了正确做到这一点,我们重新审视了之前将完整数据集分割成训练集、验证集和测试集的决策。

在训练过程中,我们使用训练集来计算梯度并更新模型参数,验证集则用于确认模型的实际表现。之后,为了评估我们的模型性能,我们使用了测试集,这是唯一一组未见过的数据。

我们通过计算随机样本的输出,发现了定性描述模型行为的价值,并通过探索 MAE 和 MAPE 的计算和图表,定量评估了我们的模型性能。

我们通过定义什么是准确预测(设置阈值)并通过调整阈值绘制模型行为,结束了这个过程。

还有更多…

深度学习在多个任务上已经超越了人类水*的表现。然而,正确评估模型对于验证模型在真实生产环境中部署后的表现至关重要。我觉得以下这个关于人工智能在多个任务上达到人类水*表现的小清单非常有趣: https://venturebeat.com/2017/12/08/6-areas-where-artificial-neural-networks-outperform-humans/。

当评估没有正确进行时,模型可能不会按预期行为表现。以下文章详细描述了这种类型的两个最重要的大规模问题(分别发生在 2015 年的谷歌和 2016 年的微软):

不幸的是,尽管这些问题现在变得越来越少见,但它们仍然存在。一个包含这些问题的数据库已经发布,并且每当报告出现这些问题时都会进行更新: incidentdatabase.ai/.

为了防止这些问题,谷歌制定了一套原则来开发负责任的人工智能。我强烈建议所有 AI 从业人员遵守这些原则: ai.google/principles/

在这个阶段,我们已经完成了整个回归问题的旅程:我们探索了回归数据集,决定了评估指标,定义并初始化了模型。我们理解了优化器、学习率、批量大小和训练周期的最佳超参数组合,并使用提前停止进行训练。最后,我们通过定性和定量的方式对模型进行了评估。

第四章:解决分类问题

在前几章中,我们学习了如何设置和运行 MXNet,如何使用 Gluon 和 DataLoader,以及如何可视化回归、分类、图像和文本问题的数据集。我们还讨论了不同的学习方法。在本章中,我们将重点讨论带有分类问题的监督学习。我们将学习为什么这些问题适合深度学习模型,并概览定义这些问题的方程。我们将学习如何为这些问题创建合适的模型并进行训练,重点讲解超参数的选择。每一节结束时,我们都会根据我们的数据评估模型,这是监督学习中的标准做法,并且我们将查看分类问题的不同评估标准。

本章将涵盖以下几个实例:

  • 理解分类模型的数学原理

  • 定义分类的损失函数和评估指标

  • 分类模型的训练

  • 评估分类模型

技术要求

除了前言中指定的技术要求外,还需满足以下技术要求:

  • 确保您已完成第一个实例,安装 MXNet、Gluon、GluonCV 和 GluonNLP,该实例来自 第一章使用 MXNet 搭建环境

  • 确保您已经完成了第二个实例,分类的玩具数据集——加载、管理和可视化鸢尾花数据集,该实例来自 第二章使用 MXNet 和可视化数据集:Gluon 和 DataLoader

  • 模型、损失和评估函数以及训练的大部分概念已在 第三章解决回归问题 中介绍。此外,正如我们将在本章中看到的,分类可以被视为回归的一个特例。因此,强烈建议先完成 第三章

本章的代码可以在以下 GitHub 链接找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch04

此外,您可以直接从 Google Colab 访问每个实例;例如,访问本章第一个实例的链接:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch04/4_1_Understanding_Maths_for_Classification_Models.ipynb

理解分类模型的数学原理

正如我们在上一章中看到的,分类问题是监督学习问题,其输出是一个类,该类来自一组类(分类分配)——例如,花朵的 鸢尾花 类。

正如我们将在本食谱中看到的,分类模型可以看作是回归模型的个别案例。我们将从探索一个二元分类模型开始。这个模型将输出两个类别中的一个。为了简便起见,我们将这两个类别标记为[0, 1]

我们可以用来解决这种二元分类问题的最简单模型是线性回归模型。这个模型将输出一个数字;因此,为了修改输出以满足我们新的分类标准,我们将修改激活函数为更合适的函数。

和之前的食谱一样,我们将使用神经网络作为我们的模型,我们将解决在第二个食谱中介绍的鸢尾花数据集预测问题,分类的玩具数据集:加载、管理和可视化鸢尾花数据集,见 第二章使用 MXNet 和可视化数据集:Gluon 和 DataLoader

准备工作

我们将在之前食谱中获得的知识基础上进行构建,因此强烈建议阅读它们。此外,正如前一章所提到的,在深入理解我们的模型之前,本食谱中的数学部分将涉及一些矩阵运算和线性代数,但这并不难。

如何实现...

在本食谱中,我们将查看以下步骤:

  1. 定义一个二元分类模型

  2. 定义一个多标签分类模型

  3. 定义特征

  4. 初始化模型

  5. 评估模型

定义一个二元分类模型

这是我们在之前的食谱中介绍的感知机模型:

图 4.1 – 感知机

图 4.1 – 感知机

这个模型可以数学地描述为 y = f(WX + b),其中 W 是权重向量 [W1, W2, …. Wn],(n 是特征的数量),X 是特征向量 [X1, X2, …. Xn]b 是偏置项,f() 是激活函数。

在回归使用案例中,我们选择了恒等函数作为激活函数,这提供了一个等于输入的输出;因此,我们有了 y = WX + b

对于我们的二元分类使用案例,我们希望有一个输出,帮助我们将输入数据点分类为两个类别(01)。在 1958 年原始的感知机论文中,Rosenblatt 研究了一个二元分类问题,选择了阶跃函数,该函数只提供 0 和 1 作为其唯一可能的输出。

如果我们回顾一下 第三章解决回归问题,在第三个食谱中,回归的损失函数和评估指标,我们对这些函数施加了一些属性。第四个属性,可微性,是由于梯度下降所需的计算。这一属性同样适用于激活函数,而 Rosenblatt 使用的阶跃函数并不符合这一要求。

此外,如果我们能够找到一个在 0 到 1 之间连续的函数,我们就可以将这个数值评估为输出为1的概率或置信度,这是由模型评估得出的。这个方法具有一些优势,我们将在后面的配方中进行探索。

因此,由于阶跃函数不满足我们的特性,我们需要一个新的激活函数。最常用的二分类模型输出激活函数是sigmoid函数:

图 4.2 – Sigmoid 激活函数

图 4.2 – Sigmoid 激活函数

Sigmoid 函数符合所有必需的属性,且输出会迅速变为01,这使得我们能够识别模型建议的输出类别。

定义多标签分类模型

当我们有多个(我们称这个数字为k)类别,而不是像我们刚才看到的那样只有两个类别进行分类时,会发生什么呢?在这种情况下,我们需要为我们的模型设计一个不同的网络架构。一方面,单个输出将不再足够,因为我们需要k个不同的输出。另一方面,尽管我们可以为每个输出使用 sigmoid 函数作为激活函数,但如果每个输出都能被评估为每个类别的概率,那就非常有用,就像我们在二分类情况下看到的那样。使用 sigmoid 函数不会强制满足概率条件,即所有概率之和必须为 1(这意味着每个输入必须对应于其中一个类别)。

在这种情况下,一个与 sigmoid 函数非常相似的函数,能够满足所描述的条件,是 softmax 函数:

σ(x j) =  ⅇ x j _ Σ i ⅇ x i

将要选择的类别是输出最大值的类别:

图 4.3 – 多标签分类网络

图 4.3 – 多标签分类网络

我们的感知机的完整定义是Y = f(WX + B),其中W现在是一个权重矩阵(形状为n x kn是特征的数量,k是输出的数量),X是特征向量(n个分量),B是偏置向量(k个分量),而f()是 softmax 激活函数。Y现在是一个k个输出的向量,其中最大值所在的类别将被分配给该输入。

定义特征

到目前为止,我们已经理论上定义了我们的模型及其行为;我们没有使用问题框架或数据集来定义它。在本节中,我们将开始以更实际的方式进行工作。

定义我们模型的下一步是决定我们要使用哪些特征(输入)。我们将继续使用第二个配方中已知的 Iris 数据集,分类的玩具数据集:加载、管理和可视化房屋销售数据集第二章与 MXNet 一起工作并可视化数据集:Gluon 和 DataLoader。该数据集包含 150 朵花的数据,包括类别和 4 个输入特征:

  • 萼片长度(单位:厘米)

  • 萼片宽度(单位:厘米)

  • 花瓣长度(单位:厘米)

  • 花瓣宽度(单位:厘米)

如果我们展示前五朵花,我们将看到以下内容:

图 4.4 – 花卉特征(鸢尾花数据集)

图 4.4 – 花卉特征(鸢尾花数据集)

此外,对于鸢尾花数据集,我们有不同的输出类别:

  • Setosa (0)

  • Versicolor (1)

  • Virginica (2)

初始化模型

现在我们已经定义了输入维度(特征数量)和输出维度,我们可以使用随机初始化来初始化我们的模型:

Weights:
[[-1.2347414  -1.771029   -0.45138445]
 [ 0.57938355 -1.856082   -1.9768796 ]
 [-0.20801921  0.2444218  -0.03716067]
 [-0.48774993 -0.02261727  0.57461417]]
 <NDArray 4x3 @cpu(0)>
 Bias:
 [1.4661262  0.6862904  0.35496104]
 <NDArray 3 @cpu(0)>

如果我们将这些值与在第一章配方中获得的值进行比较,理解回归模型的数学,来自第三章解决回归问题,我们可以看到,随着输出的不再只有一个(如回归模型中的情况),权重现在被表示为矩阵,偏差是一个向量而不是一个数字,原因是相同的。

评估模型

现在我们的模型已经初始化,我们可以用它来估计第一朵花的类别。从图 3.24中可以看出它是Setosa0)。以下是使用我们当前模型的结果:

0

干得漂亮!不幸的是,这完全是偶然的,因为模型是随机初始化的。

在下一个配方中,我们将学习如何正确评估我们的分类模型。

它是如何工作的…

像回归一样,分类模型可以根据需要具有任意多的层(深度),堆叠多个层,直到问题的解决方案所需。

在本配方中,我们描述了从第一章配方中所述的感知器的修改,理解回归模型的数学,来自第三章解决回归问题。主要有两项修改。第一项是,由于在这种情况下我们希望将每个输入分类到一组类别中,我们需要每个类别一个输出。此外,为了能够理解我们模型的输出为概率,我们需要一个新的激活函数:softmax。

最后,我们学习了如何初始化我们的模型、初始化对权重和偏差的影响,以及如何使用数据进行评估。我们将在后续的配方中进一步展开这些话题。

还有更多…

在本配方的开始,我们回顾了从 Rosenblatt(阶跃函数)到回归(线性)再到分类(sigmoid)的激活函数变化。我们讨论的一个细节是阶跃函数的不可微性。更深入的分析可以通过以下链接查看:en.wikibooks.org/wiki/Signals_and_Systems/Engineering_Functions#Derivative

使用多层架构和/或 sigmoid(或其他激活函数)使得神经网络具备*似任何函数的能力,这被称为普适逼*定理。更多细节可以在这里找到:en.wikipedia.org/wiki/Universal_approximation_theorem

定义分类的损失函数和评估指标

在前面的配方中,我们定义了输入特征,描述了我们的模型并进行了初始化。那时,我们传递了一朵花的特征向量来预测它的鸢尾花种类,计算了输出并与预期类别进行了比较。

我们还展示了如何这些初步结果并不能代表一个合适的评估。在本配方中,我们将探讨评估分类模型的话题。

此外,我们还将理解哪些损失函数最适合二元和多标签分类问题。

准备开始

损失函数和评估函数需要满足在第三章中描述的相同属性,解决回归问题,在第二个配方中,定义回归的损失函数和评估指标;因此,我建议首先阅读该章节,以便更全面地理解。

我们将从分析二元分类方法(两个输出类别)开始,随后推广到多标签分类方法。

如何操作...

让我们讨论一些评估和损失函数,并分析它们的优缺点。我们将描述的函数如下:

  • 交叉熵损失函数

  • 评估 – 混淆矩阵

  • 评估 – 指标

交叉熵损失函数

正如我们在前面的配方中讨论的,一旦模型为每个类别输出了概率,我们希望选择具有最大概率的类别作为我们模型的输出。

在优化模型参数时,我们的目标是找出哪些模型参数为我们期望的类别提供最大的概率(1),并为其他类别提供最小的概率(0)。该方程的推导超出了本书的范围,但你可以在配方末尾的更多信息...部分找到更多内容。

对于两个输出类别的情况,我们有二元交叉熵损失(对于N个样本):

BCE = −  1 _ N  ∑ i=0 N y i . log( ˆ y  i) + (1 − y i) . log(1 −  ˆ y  i)

我们可以为一个样本绘制该函数,并假设预期输出为1 (yi = 1)

图 4.5 – 二元交叉熵损失图 (yi = 1)

图 4.5 – 二元交叉熵损失图 (yi = 1)

对于多标签的常见情况,M个类别的多标签或类别交叉熵损失为以下形式:

L (y, ŷ) = − ∑ j=0 M ∑ i=0  M ( y ij * log ( ŷ ij))

该方程在比较每一对类别时,得到与图 4.5相同的图形。

我们将在下一节的训练循环中,将此功能与优化器结合使用。

评估 – 混淆矩阵

混淆矩阵帮助我们衡量模型的表现,通过将预期值(真实值)与模型实际提供的值进行比较。对于二分类问题(我们将类别定义为PositiveNegative),我们有以下公式:

图 4.6 – 二分类混淆矩阵

图 4.6 – 二分类混淆矩阵

图 4.6中,对于每种预测类别和实际类别的组合,我们有以下术语(仅在二分类中有效):

  • TP: 真阳性。

  • FP: 假阳性。也称为一型 错误

  • FN: 假阴性。也称为二型 错误

  • TN: 真阴性。

理想情况下,我们希望 TP 和 TN 尽可能接* 100%,而 FP 和 FN 尽可能接* 0%。

当我们面临多标签分类问题时,在混淆矩阵中,每个类别都会对应一行一列。对于K个类别,我们将得到一个KxK的矩阵,在矩阵的主对角线上,我们期望看到 100%的概率,其他位置的值为 0%。例如,使用我们的鸢尾花数据集(三个输出类别),我们可以计算一个模型的混淆矩阵(3x3),该模型是随机初始化的,类似于我们在前一个示例中计算的矩阵。在这种情况下,我们得到以下结果:

图 4.7 – 多标签混淆矩阵

图 4.7 – 多标签混淆矩阵

正如预期的那样,结果显示得非常糟糕。特别是,没有一朵变色鸢尾花被正确分类;然而,这个例子帮助我们可视化了一个多标签混淆矩阵。

评估 – 准确率、精确率、召回率、特异性和 F1 分数指标

为了描述一个模型在给定二分类问题上的表现,有几个有趣的指标:

精确率 = TP / (TP + FP)

召回率 = TP / (TP + FN)

F1 = 2 × 精确率 × 召回率 / (精确率 + 召回率)

准确率 = (TP + TN) / (TP + FN + TN + FP)

特异性 = TN / (TN + FP)

每个指标的目的是帮助我们理解模型的表现:

  • 准确率: 在所有值中,哪些是被正确分类的(对于两个类别)?

  • 精确率: 这是正确分类的正预测的比率。然而,它不提供关于负预测的任何信息。

  • 召回率: 这是正确分类的正标签的比率。此数字不包括有关负标签的信息。

  • 特异性: 类似于召回率,但针对负标签。这是模型正确分类的负标签的比率。此指标不包括任何有关正标签的信息。此指标很少使用。

  • F1 得分:这是精确率和召回率的调和*均数。通过结合这两个指标,这个指标提供了一个更好的模型评估,考虑了正类和负类。要获得高 F1 得分,模型需要具备高精确率和高召回率。低 F1 得分则意味着精确率、召回率或两者都很低。

这些指标可以用于多标签场景。例如,对于我们的随机初始化模型和鸢尾花数据集,计算出的结果如下(除了特异度,它在scikit-learn中没有对应的指标函数):

Accuracy  : 0.5933333333333334
Precision : 0.4765151515151515
Recall    : 0.5933333333333334
F1-score  : 0.49722222222222223

这些值非常接**均结果,这是由于随机初始化模型所预期的表现。

评估 – 曲线下面积(AUC)

对于二元分类问题,我们想要提供的输出是一个类别(PositiveNegative);然而,模型的输出是一个数字(表示正类结果的概率)。为了将这个结果转化为类别,我们需要应用一个阈值。

例如,如果我们将阈值定义为0.5,那么所有大于 0.5 的概率都会被归类为Positive。通过降低阈值,更多的值会被认为是正类,从而增加真正例(True Positive Rate,或TPR)和假正例(False Positive Rate,或FPR)的数量。如果增加阈值,效果则相反:较少的值会被视为正类,因此 TPR 和 FPR 都较小。

当我们调整阈值的值时,TPR 和 FPR 的值会有所不同。如果我们将这些值绘制在图表中,会得到如下结果:

图 4.8 – AUC

图 4.8 – AUC

如果我们计算曲线与X轴、Y = 0轴和Y = 1轴之间所覆盖的区域,我们可以得到一个不依赖于阈值的参数,它定义了模型在给定数据上的表现。

TPR 和 FPR 仅适用于二元分类情况。对于多标签分类情况,我们可以模拟二元分类的情况。有两种可能的方法:

  • 一对一

  • 一对多

如果你感兴趣,可以在本教程的还有更多...部分找到更多信息。这些曲线也被称为接收者操作特征曲线。

它是如何工作的...

在理解了回归模型和分类模型之间的差异后,包括激活函数的不同,本教程着重讲解了损失函数(用于训练)和评估指标(用于评估)。我们探讨了二元分类和多标签分类两种情况。

我们计算了分类的最常见损失函数——二元/分类交叉熵损失函数,并定义了多个评估指标,如准确率、精确率、召回率和 F1 得分。此外,我们了解了混淆矩阵,它是一种方便查看模型每类表现的方式。

我们通过查看 AUC 结束了这个食谱,它提供了一个与阈值无关的可视化。

更多内容...

交叉熵损失的数学公式没有推导出来。在这些链接中,你可以找到更多信息:

为了更好地理解如何计算多标签分类指标,我推荐以下链接:towardsdatascience.com/multi-class-metrics-made-simple-part-i-precision-and-recall-9250280bddc2

最后,阅读这个 AUC 的解释可以提供更多的见解:developers.google.com/machine-learning/crash-course/classification/roc-and-auc

对于多标签情况,这些示例有助于理解一对一/一对多方法:scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html

分类模型的训练

在这个食谱中,我们将探讨训练一个模型解决分类问题的基本概念。我们将把这些概念应用到优化我们在本章之前定义的分类模型,结合我们讨论过的损失函数和评估指标的使用。

我们将使用第二个食谱中看到的数据集 分类用玩具数据集 - 加载、管理和可视化鸢尾花数据集 来预测鸢尾花的类别,该食谱位于 第二章 中,与 MXNet 协作和数据集可视化:Gluon 和 DataLoader。

准备工作

在这个食谱中,我们将遵循与 第三章解决回归问题 类似的模式,即第三个食谱 回归模型训练,因此,重新审视损失函数、优化器、数据集划分、训练轮次和批量大小的概念将非常有趣。

如何操作...

在这个食谱中,我们将创建自己的训练循环,并评估每个超参数如何影响训练。为此,我们将按照以下步骤进行:

  1. 改进模型。

  2. 定义损失函数和优化器。

  3. 分割我们的数据集并分析公*性和多样性。

  4. 将所有内容整合在一起,形成训练循环。

改进模型

为了解决这个问题,考虑到数据集包含的有限数据量(150 个样本),我们将定义一个多层感知机MLP)网络架构,正如我们在 第三章 解决回归问题 中第三个食谱 回归模型的训练 中所看到的那样。该网络将有 2 个隐藏层,每个隐藏层包含 10 个神经元,使用全连接(密集)结构和 ReLU 激活函数,并且输出层有相应的 3 个输出(每个类一个)。最后一层没有激活函数,尽管本应使用 softmax。在下一节中,我们将理解为何如此。对于这个网络,所需的代码如下:

def create_classification_network(num_outputs = 3):
    # MLP with Gluon
    net = mx.gluon.nn.Sequential()
    net.add(mx.gluon.nn.Dense(10, activation="relu"))
    net.add(mx.gluon.nn.Dense(10, activation="relu"))
    net.add(mx.gluon.nn.Dense(num_outputs))
    # Note that the latest layer does not have an activation
    # function whereas Softmax was expected.
    # This is due to an optimization during training:
    # the loss function includes the softmax computation. return net

我们还对输入特征进行了缩放。模型的参数数量如下:

Parameters in forward computation graph, duplicate included
   Total params: 193
   Trainable params: 193
   Non-trainable params: 0
Shared params in forward computation graph: 0
Unique parameters in model: 193

可训练参数的数量约为 200。对于我们的(小型)数据集,每一行有 4 个特征,共 150 行;因此,我们的数据集大约是模型参数数量的 3 倍。通常,这是成功模型的最小要求,理想情况下,我们希望数据集的大小是模型参数数量的 10 倍左右。

重要提示

虽然可用数据点与模型参数数量之间的比较非常有用,但不同的架构在数据方面有不同的要求。像往常一样,实验(试错法)是找到正确*衡的关键。

定义损失函数和优化器

正如前一个食谱中所讨论的,我们将计算 类别交叉熵CCE)损失函数。然而,在使用 softmax 激活函数计算 CCE 损失时,有一个优化细节;因此,在训练过程中,softmax 函数的计算包含在损失函数中。对于推理,我们需要在外部添加它。类似于我们在回归问题中所做的,我们将重点分析 随机梯度下降SGD)和 Adam 优化器。

数据集划分

Iris 数据集的最大缺点之一是其大小;只有 150 个样本,它是一个较小的数据集。因此,我们将采用 50/40/10 的划分比例来分配训练集、验证集和测试集。

如果我们分析数据划分以验证公*性和多样性,得到如下结果:

图 4.9 – 训练集(左)、验证集(中)、测试集(右)分布

图 4.9 – 训练集(左)、验证集(中)、测试集(右)分布

我们可以看到,每个类别在所有特征中都有良好的代表性,唯一的原因是样本量小导致的差异。

将所有内容整合成一个训练循环

分类问题的训练循环与回归问题非常相似,我们将进行与本章早些时候相似的分析:我们将比较每个超参数,保持其他参数不变(除非另有说明)。

优化器和学习率

如前所述,训练循环中选择的优化器与学习率是相关的,因为对于某些优化器(如 SGD),学习率保持不变,而对于其他优化器(如 Adam),它从一个起始点开始变化。

提示

最好的优化器依赖于多个因素,没有什么比尝试和错误更重要;我强烈建议尝试几个,看看哪个最合适。根据我的经验,通常来说,SGD 和 Adam 是最有效的,包括在这个问题中。

让我们分析当我们改变 SGD 优化器的学习率LR)时,如何观察训练损失和验证损失的变化,同时保持其他参数不变:epoch 数量 = 100,批量大小 = 64,损失函数 = softmax 交叉熵:

图 4.10 – 使用不同 LR 值的 SGD 优化器损失

图 4.10 – 使用不同 LR 值的 SGD 优化器损失

图 4.10,我们可以得出结论,对于 SGD 优化器,LR 值在 1.0 到 3.0 之间最为理想。此外,我们可以看到,对于 LR 值非常大的情况(> 2.0),算法仍然收敛,而对于回归问题,SGD 和非常大的 LR 值则导致模型发散。

让我们分析当我们改变 Adam 优化器的学习率(LR)时,如何观察训练损失和验证损失的变化,同时保持其他参数不变:epoch 数量 = 100,批量大小 = 64,损失函数 = softmax 交叉熵:

图 4.11 – 使用不同 LR 值的 Adam 优化器损失

图 4.11 – 使用不同 LR 值的 Adam 优化器损失

图 4.11,我们可以得出结论,对于 Adam 优化器,LR 值在 10-2 和 10-1 之间最为理想。

尽管在这种情况下,SGD 优化器以 LR = 3.0 得到最佳结果(最小损失),但优化过程的演变要比 Adam 更加嘈杂,这可能是由于数据量有限(批量大小对这一点没有影响)。*滑的优化过程也表明了模型的泛化能力;因此,在接下来的测试中,我们将选择 Adam 作为优化器。

批量大小

让我们分析当我们改变 Adam 优化器的批量大小时,如何观察训练损失和验证损失的变化,同时保持其他参数不变:epoch 数量 = 100,LR = 10-2,损失函数 = softmax 交叉熵:

图 4.12 – 通过改变批量大小来观察 Adam 优化器的损失

图 4.12 – 通过改变批量大小来观察 Adam 优化器的损失

图 4.12,我们可以得出结论,对于 Adam 优化器,批量大小在 32 到 64 之间提供了最佳结果。

Epoch 数量

让我们分析当我们改变 Adam 优化器的训练损失和验证损失时,如何通过改变 epoch 的数量来观察其变化,同时保持其他参数不变:学习率(LR)= 10-2,批量大小(batch size)= 32,损失函数(loss function)= softmax 交叉熵(cross-entropy):

图 4.13 – 通过改变 epoch 数量来观察 Adam 优化器的损失

图 4.13 – 通过改变 epoch 数量来观察 Adam 优化器的损失

图 4.13中,我们可以得出结论,200 到 300 个周期适合我们的任务。使用这些值,很可能会在更短的时间内获得最佳结果。

它是如何工作的...

在解决分类问题的过程中,在本食谱中,我们学习了如何最优地更新模型的超参数。我们重新审视了每个超参数在训练循环中的作用,并对每个超参数进行了消融实验。这帮助我们了解了在单独修改每个超参数时,训练和验证损失是如何变化的。

对于我们当前的问题和选择的模型,我们验证了最佳超参数集如下:

  • 优化器:Adam

  • 学习率:10^-2

  • 批量大小:32

  • 训练轮数:300

在训练循环结束时,这些超参数给出了0.01的训练损失和0.1的验证损失。

还有更多...

在这个食谱中,我们主要结合了在前面几个食谱和章节中学习到的概念。

我们也已经讨论了在模型定义中,我们并没有显式使用 softmax 激活函数。这是由于交叉熵损失函数和 softmax 激活函数在训练过程中是如何共同作用的(它们的联合导数)。要理解这一点的一个好参考是:

peterroelants.github.io/posts/cross-entropy-softmax/

评估分类模型

在前一个食谱中,我们学习了如何选择训练超参数以优化训练。我们还验证了这些选择如何影响训练和验证损失。在这个食谱中,我们将探索这些选择如何影响我们在现实世界中的实际评估。你可能已经注意到,我们将数据集分成了三部分:训练集、验证集和测试集。然而,在训练过程中,我们只使用了训练集和验证集。在这个食谱中,我们将通过使用我们模型中未见过的数据——测试集,来模拟现实世界的行为。

准备就绪

在评估模型时,我们可以执行定性评估和定量评估。

定性评估是选择一个或多个随机(或不那么随机,取决于我们在寻找什么)样本,并分析结果,验证其是否符合我们的预期。

在这个食谱中,我们将计算在第二个食谱中定义的评估指标,定义分类模型的损失函数和评估指标,并在本章中进行计算。

此外,我们将探讨训练如何对评估产生重大影响。

如何执行...

在开始模型评估之前,我们将讨论如何衡量模型训练的性能。因此,本食谱的步骤如下:

  1. 测量训练性能——损失和准确率

  2. 定性评估

  3. 定量评估

测量训练性能——损失和准确率

正如我们在回归中看到的,防止过拟合的一个好方法是提前停止。当我们在前一个食谱中训练分类模型时,我们存储了训练损失、验证损失和验证准确率。让我们看看随着训练进展,训练损失、验证损失和验证准确率是如何变化的:

图 4.14 – 损失与准确率与 epoch 的关系(Adam)

图 4.14 – 损失与准确率与 epoch 的关系(Adam)

如我们在图 4.14中所见,约在第 50 个 epoch 时,验证损失开始增加,尽管训练损失持续下降。此外,验证准确率似乎也在该 epoch 附*趋于*稳(接* 1.0/100%)。我们保存了最佳准确率的模型,这些值是在本章第三个食谱《分类模型的训练》中报告的训练过程中使用的值。

重要提示

提醒一下,如果你想像原样使用提前停止,MXNet 提供了一个回调:mxnet.apache.org/versions/1.6/api/r/docs/api/mx.callback.early.stop.html

需要提到的一个重要事项是,在前一个食谱中,我们提到过 SGD 并没有提供非常*稳的训练。如果我们绘制这些值,得到的结果如下:

图 4.15 – 损失与 epoch 的关系(SGD)

图 4.15 – 损失与 epoch 的关系(SGD)

如我们所见,训练过程并不稳定。

定性评估

为了验证我们的模型是否与预期相似(在预测花卉的鸢尾花种类时能够获得高准确率),一种简单的定性方法是对测试集中的随机输入(未见过的数据)运行我们的模型。在我们的案例中,结果如下:

Expected Output: 0
Model Output: [[9.999949e-01 4.748235e-06 4.116847e-07]]
 Class Output: 0
Accuracy    : 1.0

对于这个例子来说,因为它只是一个随机输入,准确率可以是 100% 或 0%(准确或不准确),而我们得到了正确的类别。

重要提示

尽管我尽力让代码尽可能可重复(包括为所有随机过程设置种子),但仍可能存在一些随机性来源。这意味着你的结果可能会有所不同,但通常误差的数量级会相似。

定量评估 – 混淆矩阵

对于存储的结果,得到的混淆矩阵如下:

图 4.16 – 混淆矩阵

图 4.16 – 混淆矩阵

这些结果非常优秀,因为主对角线上的值除了零以外都是不同的。这意味着我们的模型在测试集上取得了完美的结果。

定量评估 – 准确率、精确率、召回率和 F1 分数

在之前的食谱中,我们定义了这些指标并使用它们来优化训练。这些评估是对训练集和验证集进行的。对于测试集,我们获得了以下值:

  • 准确率:1.0

  • 精确率:1.0

  • 召回率:1.0

  • F1 分数:1.0

工作原理...

在这个教程中,我们探索了如何评估我们的分类模型。为了正确做到这一点,我们重新审视了将完整数据集分割成训练集、验证集和测试集的决策。

在训练过程中,我们使用训练集计算梯度来更新模型参数,并使用验证集来确认模型在真实世界中的表现。之后,为了评估我们的模型性能,我们使用了测试集,它是唯一剩下的未见数据集。

我们发现,通过计算随机样本的输出来定性描述模型行为,以及通过探索包括混淆矩阵、准确率、精确度、召回率和 F1 分数等多个数字和图表来定量描述模型性能的价值。

还有更多……

在这个教程中,我们计算了*衡分类数据集最重要的评估指标。然而,当数据集不*衡时,我们需要小心。这是我喜欢的一篇关于这个话题的教程:

machinelearningmastery.com/probability-metrics-for-imbalanced-classification/

到此为止,我们已经完成了一个完整的分类问题的流程:我们探索了分类数据集,决定了评估指标,定义并初始化了模型。我们理解了优化器、学习率、批量大小和训练周期的最佳超参数组合,并使用早停法进行了训练。最后,我们从定性和定量两个方面对模型进行了评估。

第五章:使用计算机视觉分析图像

计算机视觉是深度学习取得巨大进展的领域之一,在多个任务中超过了人类水*的表现,例如图像分类和物体识别。此外,计算机视觉已从学术领域走向现实世界的应用,行业也开始认识到其从业者对企业的高价值贡献。

在本章中,我们将学习如何使用 GluonCV,这是一个专门用于计算机视觉的 MXNet Gluon 库,如何构建我们自己的网络,并如何使用 GluonCV 的模型库来使用预训练模型进行多个应用。

具体来说,我们将讨论以下主题:

  • 了解卷积神经网络

  • 使用 AlexNet 和 ResNet 对图像进行分类

  • 使用 Faster R-CNN 和 YOLO 检测物体

  • 使用 PSPNet 和 DeepLab-v3 对图像中的物体进行分割

技术要求

除了在前言中指定的技术要求外,本章还适用以下技术要求:

  • 请确保您已完成安装 MXNet、Gluon、GluonCV 和 GluonNLP,这是第一章中的第一个食谱,使用 MXNet 快速上手

  • 请确保您已完成回归的玩具数据集 - 加载、管理和可视化房屋销售数据集,这是第二章中的第一个食谱,使用 MXNet 并可视化数据集:GluonDataLoader

本章的代码可以在以下 GitHub 网址找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch05

此外,您还可以直接从 Google Colab 访问每个食谱——例如,本章的第一个食谱:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch05/5_1_Understanding_Convolutional_Neural_Networks.ipynb

了解卷积神经网络

在前几章中,我们使用了全连接多层感知器MLP)网络来解决回归和分类问题。然而,正如我们将看到的,这些网络并不是解决图像相关问题的最优选择。

图像是高维实体——例如,彩色图像中的每个像素有三个特征(红色、绿色和蓝色值),而一张 1,024x1,024 的图像有超过 100 万个像素(即 1 百万像素图像),因此具有超过 300 万个特征(3 * 10⁶)。如果我们将这些输入层的所有点连接到第二层 100 个神经元的全连接网络中,我们将需要超过 10⁸ 个参数,而这仅仅是第一层的需求。因此,处理图像是一个时间密集型操作。

此外,假设我们正在尝试在人脸中检测眼睛;如果一个像素属于眼睛,那么附*的像素属于眼睛的可能性非常高(例如,考虑构成虹膜的像素)。当我们将所有像素直接输入到网络中时,所有与像素位置相关的信息都丢失了。

一种名为卷积神经网络CNN)的架构被开发出来以解决这些问题,我们将在本教程中分析 CNN 的最重要特性,并了解如何在图像相关问题中实现它们。

准备工作

与前几章一样,在这个教程中,我们将使用一些矩阵运算和线性代数,但不会太难。

如何实现...

在本教程中,我们将执行以下步骤:

  1. 引入卷积层方程。

  2. 理解卷积参数和感受野。

  3. 使用 MXNet 运行卷积层示例。

  4. 引入池化层方程。

  5. 使用 MXNet 运行池化层示例。

  6. 总结卷积神经网络(CNN)。

引入卷积层方程

本教程介绍的定位问题在正式术语中称为*移不变性局部性。在卷积神经网络(CNN)中,这些问题通过在所谓的卷积层中使用卷积/互相关操作来解决。

在卷积操作中,我们有一个输入图像(或特征图 – 请参见下面与卷积层方程相关的重要说明),它与卷积核结合,卷积核是该层的可学习参数。查看这个操作如何工作的最简单方式是通过一个示例——如果我们有一个 3x3 的输入,想将其与一个 2x2 的卷积核结合,那么它看起来就像一个滑动窗口,如下图所示:

图 5.1 – 卷积层

图 5.1 – 卷积层

图 5.1所示,为了计算输出的一个像素,我们可以直观地将卷积核放置在输入图像上,进行乘法计算,然后将这些值加起来得到最终结果。这有助于网络从图像中学习特征。

此外,这个操作的计算量比全连接层要小,解决了我们所识别的计算问题。

重要说明

当卷积层作为第一步使用时(在 CNN 中通常如此),输入是完整的图像。此外,输出可以理解为具有某些特性的低维度图像,这些特性由卷积核给出。随着卷积核学习突出图像的某些特征,这些输出被称为特征图。在靠*输入的层中,这些特征图的每个像素都是图像中少量像素的组合(例如,水*或垂直线条)。随着数据通过卷积层流动,这些特征图代表更高层次的抽象(例如,脸上的眼睛)。

理解卷积参数和感受野

图 5.1中展示的例子非常简单,仅用于说明;输入大小是 6x6,卷积核大小是 3x3。这些大小是可变的,取决于网络架构。然而,有三个参数对于计算输出大小非常重要;它们是填充、步幅和膨胀。

填充是指添加到输入中的零值像素(行/列)的数量。填充越大,输出越大,实际上这会增加输入的大小。在这个例子中,填充是 1(表示为 p)。

如前所述,我们可以直观地将卷积核放置在输入上,计算乘积,然后将所有这些结果相加,得到最终的值。对于下一个值,我们需要将卷积核移到不同的位置,如下图所示。

图 5.2 – 步幅参数

图 5.2 – 步幅参数

卷积核移动的步长由步幅定义。在图 5.2中,我们可以看到步幅为 2 的例子,每个 3x3 的卷积核之间相隔 2 个值。

膨胀定义了在与卷积核进行卷积时,每个输入值之间的间隔。

图 5.3 – 膨胀参数

图 5.3 – 膨胀参数

正如我们在图 5.3中看到的,不同的膨胀参数组合了输入的元素。

这些参数定义了所谓的感受野,即输入区域的大小,该区域生成激活。它是一个重要的参数,因为只有出现在我们模型感受野中的特征才能在输出中得到表示。

图 5.1中的例子中,一个 6x6 的输入,结合 3x3 的卷积核,步幅为 2,填充为 1,膨胀为 1,得到一个 3x3 的输出,并且其感受野覆盖了整个输入(输入中的所有像素至少使用一次)。

这些参数的另一个非常有趣的性质是,给定正确的组合,您可以获得与输入相同大小的输出。给出输出维度的公式如下(该公式分别应用于高度和宽度):

o = [i + 2 * p − k − (k − 1)*(d − 1)] / s + 1

在前面的公式中,o 是输出维度(如果处理 2D 图像,则为高度或宽度),i 是输入维度(高度/宽度),p 是填充,k 是卷积核大小,d 是膨胀,s 是步幅。有多种组合可以使输入和输出的大小相同,其中之一是 p = 1, k = 3, d = 1, s = 1

运行 MXNet 的卷积层示例

我们可以使用 MXNet 的功能实现以下示例(注意填充为 0,步幅为 1,膨胀为 1):

图 5.4 – 卷积示例

图 5.4 – 卷积示例

如果我们想要按照图 5.4中所示的示例,将卷积操作应用于一个 3x3 的矩阵,并使用一个 2x2 的卷积核,我们可以使用以下代码:

X = mx.np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
 K = mx.np.array([[0.0, 1.0], [2.0, 3.0]]) convolution(X, K)

这些操作的结果如下:

array([[19., 25.],
       [37., 43.]])

这是定义的卷积步长的预期结果(考虑给定的填充、步幅和扩张参数)。

引入池化层方程

如前所述,当处理图像时,神经网络模型的一个理想特性是,随着我们遍历网络,我们可以处理更高层次的特征,或者等效地说,深层特征图中的每个像素都具有来自输入的更大感受野。

这些层执行的操作类似于卷积层,从某种意义上来说,我们采用具有恒定维度的卷积核并应用滑动窗口。然而,在这种情况下,卷积核的参数是恒定的,因此在训练网络时不会被学习。这个卷积核被视为一种操作(一个函数),通常是最大值函数(最大池化层)或*均值函数(*均池化层)。

图 5.5 – 最大池化层

图 5.5 – 最大池化层

此外,通过组合邻*的像素,我们还实现了局部不变性,这是处理图像时的另一个理想特性。

使用 MXNet 运行池化层示例

我们可以使用 MXNet 的功能实现图 5.5中显示的示例,如下所示:

X = mx.np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
 maxpool(X, (2, 2))

这些操作的结果如下:

array([[4., 5.],
       [7., 8.]])

这是定义的 2x2 最大池化步长的预期结果。

总结 CNN

因此,一个典型的 CNN 用于图像分类,它有两个不同的部分:

  • 特征提取:这也被称为网络的骨干。它是由本教程中看到的卷积层和池化层的组合构建的。每一层的输入是特征图(输入层中的图像及其所有通道),输出是减少维度但具有更多通道的特征图。层通常是堆叠的—一个卷积层,一个激活函数(通常是修正线性单元ReLU)),和一个最大池化层。

  • softmax函数作为激活函数。分类器中的层数取决于具体问题。

因此,一个 CNN 架构可以是以下形式:

图 5.6 – CNN 架构

图 5.6 – CNN 架构

图 5.6中,我们可以看到一个用于图像分类的 CNN 架构,包含两个特征提取阶段,每个阶段结合了一个卷积层(具有 ReLU 激活函数)和一个最大池化层。然后,分类器将剩余的特征图展*为一个向量,并通过一个全连接层,最终通过softmax激活函数提供输出。

它是如何工作的…

在这篇文章中,我们介绍了 CNNs。这种架构自 2000 年代初期开始发展,并推动了计算机视觉应用的革命,使得深度学习成为大多数数据驱动任务中的焦点,包括自然语言处理语音识别图像生成等,并在所有领域达到了最先进的水*。

我们已经了解了 CNNs 的内部工作原理,探索了特征图感受野以及这些架构主要层(卷积层和最大池化层)背后的数学概念,并了解了它们如何组合来构建一个完整的 CNN 模型。

还有更多……

CNNs 发展迅速;1998 年,首个 CNN 之一被发布,解决了一个实际问题:yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf

之后,直到 2012 年,随着 AlexNet 的问世,CNNs 才获得了全球关注,从那时起,进展迅速发展,直到它们超过了人类的表现。欲了解 CNNs 历史的更多信息,请参阅这篇文章:towardsdatascience.com/from-lenet-to-efficientnet-the-evolution-of-cnns-3a57eb34672f

我们简要讨论了*移不变性和局部性的主题。欲了解更多信息,请访问 d2l.ai/chapter_convolutional-neural-networks/why-conv.html

这里讨论了卷积与互相关的关系:towardsdatascience.com/convolution-vs-correlation-af868b6b4fb5

为了更好地理解矩阵维度、填充、步幅、扩张和感受野,这里提供了一个很好的解释:theaisummer.com/receptive-field/

CNNs 在图像分类领域一直处于最前沿,直到最*;2020 年 10 月,Transformers 被谷歌大脑应用于计算机视觉任务,并推出了ViTai.googleblog.com/2020/12/transformers-for-image-recognition-at.html

简而言之,与强制网络遵循局部性原则不同,Transformer 架构允许相同的模型在任何层决定哪些特征最重要,无论是局部的还是全局的。这种行为被称为自注意力。在本文写作时,Transformers 已经是图像分类领域的最先进技术。

在本章中,我们将详细分析以下任务——图像分类目标检测图像分割。然而,MXNet GluonCV 模型库包含了大量预训练模型,涵盖了许多任务。鼓励您探索在cv.gluon.ai/model_zoo/index.html提供的不同示例。

使用 MXNet 进行图像分类——GluonCV 模型库、AlexNet 和 ResNet。

MXNet 提供了多种工具来构建自定义深度学习模型。在本节中,我们将看到如何使用 MXNet 从零开始构建模型、训练它,并使用它对数据集中的图像进行分类。我们还将看到,尽管这种方法有效,但它是耗时的。

另一种选择,也是 MXNet 和 GluonCV 提供的最有价值的功能之一,就是它们的模型库。GluonCV 模型库是一个预训练的、即插即用的模型集合,可以用于您的应用程序。我们将看到如何使用模型库,特别是用于图像分类的两个非常重要的模型——AlexNetResNet

在本节中,我们将分析并比较这些方法,在简化版的 Dogs vs. Cats 数据集上进行图像分类。

准备工作。

与前几章一样,在本节中,我们将使用一些矩阵运算和线性代数,但不会太困难。

此外,我们将对图像数据集进行分类,因此我们将重新审视一些我们之前已经学习过的概念。

  • 理解图像数据集——加载、管理和可视化 Fashion MNIST 数据集,这是第二章的第三个食谱,使用 MXNet 和可视化数据集:Gluon 和 DataLoader

  • 第四章解决 分类问题

如何操作...

在本节中,我们将采取以下步骤:

  1. 探索简化版的 Dogs vs. Cats 数据集。

  2. 从头开始创建一个 AlexNet 自定义模型。

  3. 训练 AlexNet 自定义模型。

  4. 评估 AlexNet 自定义模型。

  5. 介绍模型库。

  6. 介绍 ImageNet 预训练模型。

  7. Model Zoo 加载 AlexNet 预训练模型。

  8. 评估来自 Model ZooAlexNet 预训练模型。

  9. Model Zoo 加载 ResNet 预训练模型。

  10. 评估来自 Model ZooResNet 预训练模型。

探索简化版的 Dogs vs. Cats 数据集。

对于我们的图像分类实验,我们将使用一个新的数据集,Dogs vs. Cats。这是一个 Kaggle 数据集(www.kaggle.com/c/dogs-vs-cats),可以手动下载。在本节中,我们将使用该数据集的简化版本,该版本可以从Zenodozenodo.org/records/5226945)下载。

在数据集中的一组图像中(无论是猫还是狗),我们的模型需要正确地对这些图像进行分类。在第一步,正如我们在前几章中看到的,我们将进行一些探索性数据 分析EDA)。

图 5.7 – 狗与猫数据集

图 5.7 – 狗与猫数据集

如在图 5.7中所示,数据集中的每张图像都是彩色的,并且它们的大小被调整为 224 px * 224 px(宽度和高度)大小。训练集和验证集共有 1,000 张图像,测试集有 400 张图像。

正如我们在第二章中所做的那样,理解图像数据集——加载、管理和可视化 Fashion MNIST 数据集,第三个食谱,使用 MXNet 和可视化数据集:Gluon 和 DataLoader,我们可以使用降维技术来计算一些可视化结果——主成分分析PCA)、t-分布随机邻域嵌入t-SNE)和均匀流形*似与投影UMAP):

图 5.8 – 狗与猫的可视化 – PCA, t-SNE 和 UMAP

图 5.8 – 狗与猫的可视化 – PCA, t-SNE 和 UMAP

图 5.8中,并没有明确的边界区域来区分狗与猫。然而,正如我们将在接下来的章节中看到的,上一章介绍的架构——卷积神经网络(CNNs),将在这个任务中取得非常好的结果。

从头开始创建一个 AlexNet 自定义模型

AlexNet 是由 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 于 2012 年开发的深度神经网络。它是为参加 2012 年的ImageNet 大规模视觉识别挑战赛ILSVRC)而设计的,并且是第一个基于 CNN 的模型,赢得了这一竞赛。

图 5.9 – AlexNet

图 5.9 – AlexNet

网络使用了五个卷积层和三个全连接层。所使用的激活函数是 ReLU,包含大约 6300 万个可训练参数。

为了从头开始使用 MXNet 生成此网络,我们可以使用以下代码:

def create_alexnet_network(num_classes=2):
    # Returns AlexNet architecture, as defined in MXNet source code
    net = nn.Sequential()
    net.add(
        nn.Conv2D(64, kernel_size=11, strides=4, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),
        nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),
        nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
        nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
        nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),        nn.Flatten(),
        # Last 3 layers is classifier
        # Adding dropout for regularization
        nn.Dense(4096, activation='relu'),
        nn.Dropout(0.5),
        nn.Dense(4096, activation='relu'),
        nn.Dropout(0.5),
        nn.Dense(num_classes)
    )
 return net

此代码使用 MXNet 函数添加相应的 2D卷积最大池化全连接层,并附加其相应的激活函数,生成一个 AlexNet 架构。

训练 AlexNet 自定义模型

我们正在处理的任务是图像分类任务,这是一种分类问题,输入数据是图像,因此我们可以使用在第四章中看到的训练循环——稍作修改以适应此任务。

选择的参数如下:

  • 周期数: 20

  • 批量大小: 16 个样本

  • 优化器: Adam

  • 学习 : 0.0001

使用这些参数,我们获得了以下结果(最佳模型是在第 11 个周期达到的):

  • 训练 损失: 0.36

  • 训练 准确率: 0.83

  • 验证 损失: 0.55

  • 验证 准确率:0.785

评估一个自定义的 AlexNet 模型

使用最佳模型(在此情况下为最后一次训练迭代对应的模型)在测试集上获得的准确率如下:

('accuracy', 0.7275)

对于仅经过五次训练周期而言,这个结果相当不错。

此外,计算出的混淆矩阵如下图所示(0类对应猫,1类对应狗):

图 5.10 – 一个训练过的自定义 AlexNet 混淆矩阵

图 5.10 – 一个训练过的自定义 AlexNet 混淆矩阵

图 5.10所示,该模型大多能够准确预测期望的类别,以下是每个类别的错误率:

  • 猫被误识为狗85/200(43%的猫图像被错误分类)

  • 狗被误识为猫24/200(12%的狗图像被错误分类)

让我们继续下一部分内容。

介绍模型库

MXNet 和 GluonCV 提供的最优秀的功能之一是它们庞大的预训练模型库,用户可以方便地在自己的应用中使用和部署这些模型。这个模型库被称为模型库

此外,根据手头的任务,MXNet 提供了一些非常有趣的图表,用于比较针对不同任务优化的预训练模型。在图像分类(基于 ImageNet)方面,我们有以下内容:

图 5.11 – 用于图像分类的模型库(ImageNet)

图 5.11 – 用于图像分类的模型库(ImageNet)

来源:cv.gluon.ai/model_zoo/classification.html

图 5.11 显示了GluonCV 模型库中最重要的预训练模型,根据准确率(纵轴)和推理性能(每秒样本数与横轴)。目前,右上角的象限没有模型,这意味着我们目前需要*衡这些特性。

使用来自 GluonCV 模型库的模型仅需几行代码,我们将在接下来的步骤中探索此路径,以解决我们简化的狗与猫数据集。

ImageNet 预训练模型

GluonCV 模型库中的模型已经在ImageNet数据集上进行了预训练,用于图像分类任务。这个数据集是计算机视觉领域最著名的数据集之一。它是第一个大规模的图像数据集,并且在 2012 年 AlexNet 赢得 ILSVRC 时,成为深度学习革命的一部分。

该数据集有两个变体:

  • 完整数据集:超过 20,000 个类别,约 1,400 万张图像

  • ImageNet-1k:大约 1000 个类别,约 100 万张图像

由于数据集的庞大和类别的数量,完整的数据集在基准测试中很少使用,通常ImageNet1k被视为标准的 ImageNet 数据集(除非在研究论文、文章等中另有说明)。数据集中的图像为彩色图像,尺寸为 224px×224px(宽×高)。

GluonCV 模型库中的所有图像分类预训练模型都已经在 ImageNet-1k 上进行过预训练,因此它们有 1,000 个输出。输出将经过后处理,使得所有与猫相关的 ImageNet 类别指向类别 0,所有与狗相关的 ImageNet 类别指向类别 1,所有其他输出指向类别 2,我们将其视为未知类别。

从模型库加载预训练的 AlexNet 模型

为了比较使用自定义训练模型和来自模型库的预训练模型的优缺点,在接下来的部分中,我们将使用一个在ImageNet数据集上预训练的 AlexNet 架构版本,该模型来自 GluonCV 模型库。

加载预训练模型非常简单,可以通过一行代码完成:

alexnet = gcv.model_zoo.get_model("alexnet", pretrained=True, ctx=ctx)

get_model GluonCV 函数接收三个参数:

  • alexnet

  • False,只会加载未初始化的架构

  • mx.cpu()mx.gpu(),如果可用

此调用将下载所选模型,并在需要时下载其预训练的权重和偏差。

评估来自模型库的预训练 AlexNet 模型

使用上一节加载的模型,我们现在可以评估并比较之前的结果,比如 accuracy

('accuracy', 0.725)

如我们所见,这个数字略低于我们使用自定义训练的 AlexNet 模型时获得的准确率。

计算混淆矩阵后,我们得到以下值:

图 5.12 – 预训练的 AlexNet 混淆矩阵

图 5.12 – 预训练的 AlexNet 混淆矩阵

当我们分析图 5.12时,最显著的区别是我们之前的混淆矩阵是一个 2x2 矩阵(对于真实标签和预测标签,选项为01)。然而,使用我们预训练的模型时,我们得到了一个 3x3 的混淆矩阵。这是因为,正如前面所提到的,预训练模型是基于 ImageNet 训练的,它们输出 1,000 个类别(而不是我们数据集所需的两个类别)。这些输出经过后处理,因此所有与猫相关的ImageNet类别指向类别0,所有与狗相关的 ImageNet 类别指向类别1,所有其他输出指向类别2,我们将其视为未知类别。考虑到这个未知类别2,就得出了 3x3 的矩阵。请注意,结果中没有产生真实标签为2的图像;最后一行全是零。

该模型大部分时间能够准确地预测预期类别,以下是每个类别的错误(我们需要将两个错误列的数字相加):

  • 未检测为猫的猫96/200(48%的猫图像被误分类)

  • 未检测为狗的狗14/200(7%的狗图像被误分类)

在每类结果中存在显著差异,这主要是由于使用了预训练的数据集 ImageNet,因为它包含了大量与狗品种相关的类别,因此在狗图像上进行了更广泛的训练。

从模型库加载预训练的 ResNet 模型

通过查看 AlexNet 及其后续更深的模型,如 VGGNet,逐渐清晰地认识到,深层网络在分类图像时确实能够起到帮助作用。然而,当训练这些深层网络时,利用 反向传播链式法则,训练算法开始计算越来越小的梯度值,因为大量的小数相乘(激活函数的输出位于 [0, 1] 范围内),因此,当计算早期层的梯度时,更新的权重很少发生变化。这就是著名的 梯度消失 问题,而不同的 ResNet 架构则是为了解决这一问题而开发的。具体而言,ResNet 模型使用残差块,在层与层之间加入直通连接,为训练提供了可以利用的捷径,从而避免了梯度消失问题。

图 5.13 – ResNet 残差块

图 5.13 – ResNet 残差块

图 5.13 所示的架构使得可以更具扩展性地堆叠层,并且已有 18、50、101 和 152 层的已知架构。这种方法非常成功,ResNet152 在 2015 年赢得了 ILSVRC。

在这种情况下,我们将加载 resNet50v1d 版本,只需一行代码:

resnet50 = gcv.model_zoo.get_model("resnet50_v1d", pretrained=True, ctx=ctx)

模型随后成功下载。

评估来自模型库的预训练 ResNet 模型

使用上一节加载的模型,我们现在可以评估并与之前的结果进行比较,例如 准确率

('accuracy', 0.925)

如我们所见,这个数字明显高于之前的模型。

在计算混淆矩阵后,我们得到了以下值:

图 5.14 – 预训练的 ResNet 混淆矩阵

图 5.14 – 预训练的 ResNet 混淆矩阵

根据 图 5.14,每类的错误率如下:

  • 未检测为猫的猫29/200(14.5%的猫图像被错误分类)

  • 未检测为狗的狗1/200(0.5%的狗图像被错误分类)

在每类结果中存在显著差异,这再次是由于使用了预训练的数据集 ImageNet,因为它包含了大量与狗品种相关的类别,因此在狗图像上进行了更广泛的训练。

它是如何工作的……

在这篇文章中,我们比较了两种使用计算机视觉模型进行图像分类的方法:

  • 从头开始训练自定义模型

  • 使用来自 GluonCV 模型库的预训练模型

我们将两种方法应用于 AlexNet 架构,并将结果与 ResNet-101 模型库 版本进行了比较。

两种方法都有优缺点。从零开始训练可以让我们直接控制输出类别的数量,并且可以完全掌控训练过程以及训练和验证数据集上损失和准确率的变化。

然而,为了训练一个模型,我们需要足够的数据,但这些数据可能并不总是可用的。此外,调整训练超参数(训练轮次、批次大小、优化器和学习率)以及实际训练过程是非常耗时的过程,如果做得不当,可能会导致准确度(或其他指标)不理想。

在我们的示例中,我们使用了 Kaggle 的Dogs vs. Cats数据集的简化版本,并使用了在ImageNet上预训练的模型。Kaggle 数据集包含 25,000 张图像(比原始数据集多 10 倍以上),我们鼓励读者在该数据集上尝试所提出的解决方案(所有辅助函数也已经在完整数据集上进行了测试)。

此外,选择ImageNet数据集并非随意决定;ImageNet拥有狗类和猫类的分类,因此预期这些预训练模型会表现良好,因为它们已经看过来自dataset类别的图像。然而,当无法做到这一点时,我们将一个数据集的预训练模型应用到另一个数据集上,数据的概率分布通常会非常不同;因此,得到的准确率可能非常低。这被称为领域间差距领域适应问题,即源数据集(模型已在该数据集上预训练)与目标数据集(模型在该数据集上进行评估)之间的差距。

解决监督学习问题的一个方法是微调。该方法在第七章中有详细探讨。

我们通过评估两种预训练模型——AlexNetResNet,来完成这个食谱,并了解了 CNN 模型如何随着时间的推移演变,从而提高了获得的准确率。

还有更多...

在本食谱中,我们使用了ImageNet预训练模型;有关ImageNet和 ILSVRC 的更多信息,我建议阅读这篇文章:machinelearningmastery.com/introduction-to-the-imagenet-large-scale-visual-recognition-challenge-ilsvrc/

虽然这篇文章主要讲的是 ILSVRC,但前面的链接也包括了一些关于 CNN 的历史,包括AlexNet、VGGNet 和ResNet

然而,最*计算机视觉数据集在数据质量方面受到了严格审查,ImageNet也不例外,正如本文所描述:venturebeat.com/2021/03/28/mit-study-finds-systematic-labeling-errors-in-popular-ai-benchmark-datasets/

图 5.11展示了一个静态图像,展示了模型库在图像分类(在ImageNet上)中的准确度与每秒样本数的关系图。一个动态版本的快照可以在这个链接查看,值得一看:cv.gluon.ai/model_zoo/classification.html

在这个链接中,包含了 GluonCV 模型库中不同模型的结果,建议你复现这些结果,因为这是一个有趣的练习。

除了 ImageNet,GluonCV 模型库还提供了预训练的CIFAR10模型。这些模型的列表可以在cv.gluon.ai/model_zoo/classification.html#cifar10找到。

要深入理解梯度消失问题,维基百科提供了一个很好的起点:en.wikipedia.org/wiki/Vanishing_gradient_problem

最后,关于 ResNet 及其当前的研究意义,在一篇最*发表的论文中,显示了当应用最新研究的训练技术时,ResNet 仍然能够达到最先进的SOTA)结果,突出了数据集和训练算法(而不仅仅是优化模型架构)的重要性:gdude.de/blog/2021-03-15/Revisiting-Resnets

使用 MXNet 进行目标检测——Faster R-CNN 和 YOLO。

在本食谱中,我们将看到如何使用 MXNet 和 GluonCV 在一个预训练模型上检测数据集中的物体。我们将看到如何使用 GluonCV 模型库中的两个非常重要的目标检测模型——Faster R-CNNYOLOv3

在本食谱中,我们将比较这两个预训练模型在Penn-Fudan 行人数据集上检测物体的性能。

准备工作

和前几章一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但不会太难。

正如我们将在本食谱中详细介绍的,目标检测结合了分类和回归,因此,建议重新阅读我们在前几章和食谱中探讨这些主题基础的部分。此外,我们将在图像数据集上检测物体。本食谱将结合我们在以下章节中学到的内容:

  • 理解图像数据集:加载、管理和可视化 Fashion MNIST 数据集,这是第二章的第三个食谱,使用 MXNet 并可视化数据集:Gluon 和 DataLoader

  • 第三章解决回归问题

  • 第四章解决分类问题

如何进行……

在本食谱中,我们将采取以下步骤:

  1. 介绍目标检测。

  2. 评估目标检测器。

  3. 比较单阶段双阶段目标检测器。

  4. 探索Penn-Fudan 行人数据集。

  5. 介绍目标检测模型库。

  6. 使用MS COCO预训练模型。

  7. Model Zoo加载一个预训练的 Faster R-CNN 模型。

  8. 评估一个Faster R-CNN预训练模型来自Model Zoo

  9. Model Zoo加载一个预训练的 YOLOv3 模型。

  10. 评估一个YOLOv3预训练模型来自Model Zoo

  11. 总结我们学到的内容。

介绍目标检测

在前几章和配方中,我们分析了图像分类问题,模型的任务是接收一张图像并定义与之最相关的类别。然而,在目标检测中,每张图像中可能有多个物体,对应不同的类别,且位于图像的不同位置,因此输出现在是两个列表,一个提供每个检测物体最可能的类别,另一个表示物体的估计位置。类别输出可以建模为一个分类问题,而边界框输出可以建模为回归问题。通常,位置用所谓的边界框来表示。以下是边界框的示例:

图 5.15 – 边界框示例

图 5.15 – 边界框示例

图 5.15中,我们可以看到两个不同类别的边界框示例——persondog

评估目标检测器

在图像分类中,我们定义一个正确的分类是指图像中识别出的类别是正确的。然而,在目标检测中,有两个参数——类别和边界框。直观地,我们可以定义一个正确的分类,如果对于每个应该检测到的物体,都有一个足够相似的边界框被正确分类。为了定义什么是足够相似,我们计算交并比(IoU),即边界框交集区域与边界框并集区域的比值。

图 5.16 – IoU

图 5.16 – IoU

IoU 的图形解释可以在图 5.16中看到。当 IoU 超过某个确定的阈值时,边界框被认为匹配。通过使用 IoU 及其阈值(通常为 0.5),可以将检测分类为正确(前提是物体也被正确分类),并可以计算准确率、精度和召回率等指标(按类别计算)。

此外,在第四章《解决分类问题》中,我们讨论了评估分类问题的几种选择。我们介绍了曲线下面积AUC),并且我们看到了改变阈值对精确度召回率的影响。当我们将精确度和召回率一起绘制(PR 曲线)时,我们可以看到阈值的影响,就像我们在 AUC 中所做的那样。如果我们计算曲线下的面积(x 轴、y = 0 轴y = 1 轴 之间的面积),我们得到一个不依赖于阈值的参数,它定义了我们模型在给定数据下的性能:

图 5.17 – PR 曲线

图 5.17 – PR 曲线

我们可以在图 5.17中清楚地看到这条曲线的一个特点,它呈现锯齿形。当我们降低阈值时,曲线因为假阳性而下降,然后随着真正的阳性再次上升。

然而,为了能够轻松比较不同的模型,开发了一个单一的数字指标,而不是为每个类别比较 PR 曲线,这个指标就是*均精度均值mAP)。简而言之,它是一个模型所有 PR 曲线下面积的*均值:

mAP = 1/N ∑ i=1 N AP i

要计算 mAP,第一步是计算每个类别的*均精度,这是 PR 曲线下面积,然后计算其算术*均值。这个值提供了一个单一的数字,可以用来比较在同一数据集上评估的物体检测模型。

比较单阶段和二阶段物体检测器

我们可以将物体检测器看作是裁剪图像的特定部分,并将该裁剪后的图像传递给图像分类器,这类似于我们在前面的步骤中对完整图像所做的操作。采用这种方法,我们的物体检测器将有两个步骤:

  1. 区域提议网络:这是一个模块,用于指示可能包含物体的区域。

  2. 物体分类器:模型将对区域进行分类,区域之前已经裁剪并调整大小,以匹配模型输入的约束条件。

这种方法被称为二阶段物体检测,其最重要的特点是准确性,尽管由于其复杂的架构,速度较慢,并且无法进行完全精确(非*似)的端到端训练。

更快的基于区域的卷积神经网络R-CNN)是遵循这种方法的模型之一。与该模型的先前版本(R-CNNFast R-CNN)相比,最重要的区别是为了提供更快的计算,它使用预计算的边界框,称为锚框,其中边界框的尺度和长宽比是预定义的。这种方法允许网络模型计算与锚框相关的偏移量,而不是完整的边界框坐标,从而简化了回归问题。

另一种提高计算时间的算法是非极大值抑制NMS)。通常会对目标检测流程的下一步提出成千上万个区域建议。这些区域中许多重叠,而 NMS 算法考虑预测的置信度,移除所有 IoU 阈值以上重叠的区域。

另一种目标检测器的方法是设计可以同时预测边界框和类别概率的架构,使得可以进行端到端的一步训练。遵循这种方法的架构被称为单阶段目标检测器。这些架构还利用锚框和 NMS 来改进回归任务。使用这种方法的两个最著名的架构如下:

  • You Only Look Once (YOLO):图像仅一次处理,使用自定义的 CNN 架构(卷积层和最大池化层的组合),最终以两个全连接层结束。这些架构已经不断发展,YOLOv3是其中最流行的之一。

  • 单发多框检测器(SSD):图像使用 CNN 主干架构(如 VGG-16)进行处理以计算特征图,并且生成的多尺度特征图然后被分类。SSD512(使用VGG-16作为主干)是遵循这种架构的模型之一。

YOLOv3 模型速度最快,并且产生了合理的准确度指标;SSD512 模型在速度和准确度之间取得了良好的*衡;而 Faster R-CNN 模型具有最高的准确度,但速度是三者中最慢的。

探索 Penn-Fudan 行人数据集

对于我们的目标检测实验,我们将使用一个新的数据集 – Penn-Fudan 行人。这是一个公开可用的数据集(www.cis.upenn.edu/~jshi/ped_html/),是宾夕法尼亚大学和复旦大学之间的合作项目,需要手动下载。

数据集中有 423 个行人在 170 张图像中被标注;在数据集发布时(2007 年),标注了 345 名行人,后来增加了 78 名行人,因为之前的行人要么很小要么被遮挡。

从数据集的图像集合中,我们的模型将需要正确检测图像中的行人并定位它们,使用边界框。为了更好地理解问题,正如我们在之前章节中所看到的,我们将进行一些探索性数据分析(EDA)。

图 5.18 – Penn-Fudan 行人数据集

图 5.18 – Penn-Fudan 行人数据集

正如我们在图 5**.18中所看到的,每个图像可能包含一个或多个行人,以及它们对应的边界框。数据集中的每个图像都是彩色的,它们具有可变的宽度和高度,这些后来会根据模型的需求进行调整。此图是使用 GluonCV 的可视化工具包(plot_bbox函数)生成的:

gcv.utils.viz.plot_bbox(image, gt_bboxes, class_names=["person"], ax=axes)

对于这个数据集,没有需要分类的不同类别,也没有计算进一步的可视化。

引入物体检测模型库

GluonCV 还提供了用于物体检测的预训练模型,位于其模型库中。对于 MS COCO 数据集,这是准确度(mAP)与性能(每秒样本数)图表:

图 5.19 – 物体检测模型库(MS COCO)

图 5.19 – 物体检测模型库(MS COCO)

注意

图片来自以下来源:cv.gluon.ai/model_zoo/detection.html

图 5.19 展示了 GluonCV 模型库中最重要的预训练模型,比较了准确度(垂直轴上的 mAP)和推理性能(水*轴上的每秒样本数)。目前,在右上象限没有模型,意味着目前我们需要在这两者之间进行*衡。

使用来自 GluonCV 模型库的模型只需几行代码,我们将在接下来的步骤中探讨使用这些模型来解决我们的 Penn-Fudan Pedestrians 数据集问题。

使用 MS COCO 预训练模型

GluonCV 模型库中的物体检测任务模型已在 MS COCO 数据集上进行预训练。该数据集是计算机视觉中最受欢迎的物体检测数据集之一。它由微软于 2015 年开发,并持续更新至 2017 年。在其最新的更新中,它包含 80 类(加上背景),包括以下内容:

  • 训练/验证集:118,000/5,000 张图片

  • 一个测试集:41,000 张图片

GluonCV 模型库中的多个物体检测预训练模型已使用 MS COCO 数据集进行预训练,因此,每个检测到的物体将被分类为 80 类之一。由于每张图片可能包含多个物体,在 MXNet GluonCV 实现中,物体检测模型的输出结构如下:

  • 一组索引:对于每个检测到的物体,这个数组给出了该物体所属类别的索引。该数组的形状是 BxNx1,其中 B 是批量大小,N 是每张图片检测到的物体数量(取决于模型)。

  • 一组概率:对于每个检测到的物体,这个数组给出了该物体属于 索引数组 中检测到类别的概率。该数组的形状是 BxNx1,其中 B 是批量大小,N 是每张图片检测到的物体数量(取决于模型)。

  • 一组边界框:对于每个检测到的物体,这个数组给出了与该物体关联的边界框坐标。该数组的形状是 BxNx4,其中 B 是批量大小,N 是每张图片检测到的物体数量(取决于模型),4 是坐标,格式为 [x-min, y-min, x-max, y-max]

看着图 5.19,可以看到两个分离的组——Faster R-CNN 系列,它具有最高的准确性但速度较慢,和 YOLO 系列,它非常快速但准确性较低。对于我们的实验,我们选择了两个最受欢迎的模型,每个模型对应一个不同的 Faster R-CNN(ResNet-101 骨干网络与 FPN 版本)和YOLOv3(Darknet53 骨干网络,一个 53 层的 CNN)。

此外,MS COCO 数据集包含了person类,因此,预训练于 MS COCO 的数据集非常适合用于Penn-Fudan 行人数据集。

从模型库加载一个预训练的 Faster R-CNN 模型

Faster R-CNN 是一个两阶段的物体检测架构,这意味着,首先,它提供了可能存在物体的区域,接着,通过分析这些区域,它提供了检测到的物体的类别和位置。它由 Ren 等人(微软研究院)于 2014 年开发。

它是一个系列架构的第三次迭代,这些架构彼此演变——R-CNN、Fast R-CNN 和 Faster R-CNN。

从研究论文(更多部分)中,我们可以看到一个 R-CNN 架构:

图 5.20 – R-CNN 的高级架构

图 5.20 – R-CNN 的高级架构

来源:这张车的图片来源于azerbaijan_stockers,来自 Freepik:www.freepik.com/free-photo/mini-coupe-high-speed-drive-road-with-front-lights_6159501.htm

其次,我们可以看到 Fast R-CNN 架构:

图 5.21 – Fast R-CNN 的高级架构

图 5.21 – Fast R-CNN 的高级架构

来源:这张车的图片来源于azerbaijan_stockers,来自 Freepik:www.freepik.com/free-photo/mini-coupe-high-speed-drive-road-with-front-lights_6159501.htm#query=CAR&position=48&from_view=search&track=sph&uuid=e82c3ce9-2fe8-40ef-9d39-e8d27781fdf2

最后,我们可以看到 Faster R-CNN 架构:

图 5.22 – Faster R-CNN 的高级架构

图 5.22 – Faster R-CNN 的高级架构

来源:这张汽车图片的来源为 azerbaijan_stockers,来自 Freepik:www.freepik.com/free-photo/mini-coupe-high-speed-drive-road-with-front-lights_6159501.htm#query=CAR&position=48&from_view=search&track=sph&uuid=e82c3ce9-2fe8-40ef-9d39-e8d27781fdf2

不同的架构有相似之处也有不同之处,具体如下:

  • R-CNN:该方法使用选择性搜索算法提供 2,000 个区域提议。每个区域都被输入到 CNN 中,CNN 为每个物体生成一个 4,096 特征的特征向量以及四个坐标的边界框。这些特征向量作为 支持向量机 (SVM) 分类器的输入。

  • Fast R-CNN:在这一版本中,不再将每个区域输入到 CNN 中,而是将整个图像一次性输入,计算整张图像的单一特征图,从而加速了处理过程。然后,感兴趣区域 (ROIs) 在该特征图(而非图像)上计算,计算方法与 选择性搜索 算法类似。接着,这些 ROIs 会通过 ROI 池化 层,每个在提议区域中的物体会被分配一个相同形状的特征图。这是一种高效的方法,其中输出可以被传递到回归器和分类器两个网络中,分别提供每个物体的位置和类别。这两个网络基于全连接层,回归器计算 ROIs 的偏移量,分类器的输出激活函数为 softmax

  • Faster R-CNN:在这一最后的迭代中,提出了三项改进。首先,使用骨干 CNN 来计算图像的特征图;但是,不再使用选择性搜索算法来提议区域,而是在骨干 CNN 上方添加一些叫做 区域提议网络 (RPN) 的 全卷积 层,从而大大缩短计算时间。其次,这些区域提议被计算为与锚框相关的偏移量。最后,为了减少需要处理的区域数量,使用 非极大值抑制 (NMS) 方法。这三项改进提供了更快的推理速度和更高的准确率。

在我们的实验中,我们将使用来自 ResNet-101 网络的 v1d 版本权重作为骨干网络,并使用 特征金字塔网络 作为 RPN。我们可以通过一行代码加载模型:

faster_rcnn = gcv.model_zoo.get_model("faster_rcnn_fpn_resnet101_v1d_coco", pretrained=True, ctx=ctx)

模型随后成功下载。

GluonCV 实现的这个模型能够检测 80,000 个不同的物体。

评估来自模型库的预训练 Faster R-CNN 模型

使用 Penn-Fudan 行人 数据集,我们现在可以对前一部分加载的模型进行定性和定量评估。

从定性角度来看,我们可以从数据集中选择一张图像,并将模型的输出与数据集中的真实标签输出进行比较:

图 5.23 – 比较 Faster R-CNN 的预测和真实标签

图 5.23 – 比较 Faster R-CNN 的预测和真实标签

图 5.23中,我们可以看到预测的分割掩码与真实标签之间有非常强的相关性,同时模型的置信度很高(+99%)且类别准确率完美。此图是使用 GluonCV 可视化utils包(plot_bbox函数)计算得出的。

从定量角度来看,我们可以进行 mAP 评估,并计算计算此指标所花费的运行时间:

('VOCMeanAP', 0.6716161702078043)
 Elapsed Time:  249.30912852287292 secs

该模型在MS COCO(参见目标检测模型库)上的 mAP 计算值为 40.7;因此,考虑到我们模型的值为 0.67,我们可以得出结论,模型执行任务准确无误。然而,计算完成确实花费了一些时间(大约 250 秒),这对于 Faster R-CNN 架构来说是预期的。

从 Model Zoo 加载一个 YOLOv3 预训练模型

You Only Look Once Version 3YOLOv3)是一个单阶段目标检测架构,这意味着它使用端到端的方法,在一个步骤中进行边界框和类别概率的预测。它由 Redmon 等人(华盛顿大学)开发,从 2016 年的 YOLO 到 2018 年的 YOLOv3。

这是一个系列架构的第三次迭代,它们是从彼此演化而来的——YOLO、YOLOv2 和 YOLOv3。

从研究论文中,我们可以看到 YOLO 架构:

图 5.24 – YOLOv1 架构

图 5.24 – YOLOv1 架构

其次,我们可以看到 YOLOv2 架构:

图 5.25 – YOLOv2 架构

图 5.25 – YOLOv2 架构

最后,我们可以看到 YOLOv3 架构:

图 5.26 – YOLOv3 架构

图 5.26 – YOLOv3 架构

不同架构之间有相似之处和不同之处,具体如下:

  • YOLO:初始模型将每张图像分解为相同大小的网格单元。每个单元负责检测对象,如果对象中心的位置位于该单元内。每个单元可以预测两个边界框、它们的类别和置信度得分,但每个单元只能包含一个对象(具有不同的大小和位置)。所有的预测是同时进行的,使用一个由 24 个卷积层和 2 个全连接层组成的 CNN。存在大量重叠的边界框,使用 NMS(非极大值抑制)来得到最终输出。

  • YOLOv2:YOLO 架构在检测小物体群体时存在困难。为了解决这个问题,在此迭代中引入了多项改动——批量归一化以提高训练准确率、回归的锚框、增加到每个单元格五个边界框的检测能力,以及一个新的骨干网络 DarkNet-19,具有 19 个卷积层和 5 个最大池化层,另外还有 11 个用于检测的层。

  • YOLOv3:在最后一次迭代中,引入了三项改动。首先,为了进一步提高小物体的检测准确率,骨干网络更新为 DarkNet-53,具有 53 个卷积层和 53 个用于检测头的层,允许在三个不同的尺度上进行预测。其次,每个单元格的边界框数量从五个减少到三个;然而,考虑到三个不同的尺度,这提供了九个锚框。

对于我们的实验,我们将使用 DarkNet-53 作为骨干网络。我们可以通过一行代码加载模型:

yolo = gcv.model_zoo.get_model("yolo3_darknet53_coco", pretrained=True, ctx=ctx)

模型随后成功下载。

该模型的 GluonCV 实现能够检测 100 种不同的物体。

评估来自模型库的 YOLOv3 预训练模型

使用Penn-Fudan 行人数据集,我们现在可以对上一节加载的模型进行定性和定量评估。

从定性角度来看,我们可以选择数据集中的一张图片,将模型的输出与数据集中的真实标签输出进行比较:

图 5.27 – 比较 YOLOv3 的预测与真实标签

图 5.27 – 比较 YOLOv3 的预测与真实标签

图 5.27中,我们可以看到预期结果的边界框与实际模型输出之间有非常强的相关性,以及模型的强信心度(+95%)和完美的分类准确率。该图是使用 GluonCV 可视化工具包(plot_bbox函数)计算得出的。

从定量角度来看,我们可以进行 mAP 评估,并计算此度量所花费的运行时间:

('VOCMeanAP', 0.5339115787945962)
 Elapsed Time:  113.4049768447876 secs

该模型在MS COCO上的计算 mAP(见物体检测模型库)为 36.0;因此,考虑到我们模型的0.53值,我们可以得出结论:它正在准确执行任务。完成此任务花费了113秒。

它是如何工作的…

在本章中,我们解决了物体检测问题。我们分析了图像分类问题与物体检测问题在评估、网络架构和数据集方面的差异。

在我们的例子中,我们使用了一个公开的Penn-Fudan Pedestrians数据集,并在 MS COCO 上使用了预训练模型。这个选择并非随意;MS COCO有一个person类别,因此,预期这些预训练模型能表现良好,因为它们已经看过该数据集类别的图像。然而,如前面所提到的,当无法实现这一点时,我们将一个数据集的预训练模型应用于另一个数据集时,数据的概率分布通常会有很大的不同;因此,得到的准确率可能会非常低。这被称为源数据集(模型预训练时使用的图像)和目标数据集(模型评估时使用的图像)之间的领域间隙或领域适应问题。

解决监督学习问题的一个方法是微调。这一方法在第七章中有详细探讨。

我们通过评估两个预训练模型,Faster R-CNN 和 YOLOv3,来结束这部分内容,并确认我们的 Faster R-CNN 模型非常精确但较慢,而 YOLOv3 则更快(速度提高了 2 倍),但精度稍低(大约下降了 20%)。

还有更多内容...

图 5**.19展示了与目标检测模型库(在MS COCO上的表现)对应的 mAP 与每秒样本数的静态图像。还有一个动态版本,值得一看:cv.gluon.ai/model_zoo/detection.html。在这个页面上,展示了GluonCV 模型库中不同模型的结果;我建议你复现这些结果,这将是一个有趣的练习。

要了解更多关于MS COCO数据集的信息,原始论文可以通过此链接查看:https://arxiv.org/pdf/1405.0312.pdf。

此外,阅读原始研究论文并了解目标检测器是如何从学术角度演变的,非常有趣:

在这个食谱中,我们看到 Faster R-CNN 更精确但较慢,而 YOLOv3 较快但准确度较低。为了*衡物体检测问题中的准确度与推理时间之间的权衡,有不同的选择。

一种选择是估计图像的难度并应用不同的物体检测器。如果是挑战性的图像,使用 Faster R-CNN 系列;如果比较简单,使用 YOLOv3。这种方法在这篇论文中有详细探讨:arxiv.org/pdf/1803.08707.pdf;然而,微调像 YOLOv3 这样的快速模型作为初步方案是推荐的做法。

使用 MXNet 对图像中的物体进行分割 —— PSPNet 和 DeepLab-v3

在这个食谱中,我们将学习如何使用 MXNet 和 GluonCV 在一个预训练模型上对来自数据集的图像中的物体进行分割。这意味着我们将能够将物体分割成不同的类别,如。当将问题表述为分割时,预期的输出是与输入图像大小相同的图像,每个像素值都是分类标签(我们将在接下来的部分分析这一过程)。我们将看到如何使用 GluonCV 模型库中的两个非常重要的模型进行语义分割 —— PSPNetDeepLab-v3

在这个食谱中,我们将比较这两个预训练模型在上一章介绍的Penn-Fudan 行人数据集上进行语义物体分割的表现,因为其地面真实值也包括分割掩码。

准备工作

如同前几章一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但不会太难。

正如我们在这个食谱中将要展开的,语义分割与分类和物体检测问题相似,因此,建议回顾我们探讨这些主题基础的章节和食谱。此外,我们将处理图像数据集。本食谱将结合我们在以下章节中学到的内容:

  • 理解图像数据集:加载、管理和可视化 Fashion MNIST 数据集,来自第二章的第三个食谱,使用 MXNet 并可视化数据集:Gluon和 DataLoader

  • 第四章解决分类问题

如何实现...

在这个食谱中,我们将采取以下步骤:

  1. 介绍语义分割。

  2. 评估分割模型。

  3. 比较语义分割的网络架构。

  4. 探索带有分割地面真实值的Penn-Fudan 行人数据集。

  5. 介绍语义分割模型库。

  6. 从模型库加载预训练的 PSPNet 模型。

  7. 评估来自模型库的预训练 PSPNet 模型。

  8. 从模型库加载一个预训练的 DeepLab-v3 模型。

  9. 评估来自模型库的预训练 DeepLab-v3 模型。

介绍语义分割

在之前的一些章节和示例中,我们分析了图像分类问题,在这些问题中,我们的模型任务是接收图像并定义最可能与之相关的类别。然而,在语义分割中,每张图像可能包含多个物体,分别对应不同的类别,并位于图像的不同位置。在目标检测中,解决这个问题的输出是两个列表,一个提供每个检测到的物体最可能的类别,另一个指示物体的估计位置。而在语义分割中,每张图像的输出是一组二值图像,每个类别预计都会被检测到(数据集中的类别),其中每个像素如果被分类为该标签,则像素值为 1(活动状态),否则为 0(非活动状态)。每一张这样的图像都是一个二值 分割掩码

图 5.28 – 二值分割掩码

注意

此人图像来源于以下出处:azerbaijan_stockers 在 Freepik 上:www.freepik.com/free-photo/young-woman-crossing-road-using-phone_10705234.htm#query=person%20in%20the%20street&position=13

&from_view=search&track=ais&uuid=c3458125-63b6-4899-96e5-df07c307fb46图 5.28 显示的masks可以看作是类别的 one-hot 嵌入,可以通过将每个类别与一个不同的数字(例如其类别索引)关联来组合,形成一张新图像:

图 5.29 – 语义分割

图 5.29 – 语义分割

因此,语义分割模型的输出是不同的二值分割掩码(参见图 5.29的示例),数量取决于模型已训练的类别数。因此,对于输入到模型的每个图像,其形状为 [H, W]H 为高度,W 为宽度),输出数组的形状将为 [N, H, W]N 为类别数)。

评估分割模型

评估语义分割任务模型的一种直观方法是报告被正确分类的像素的百分比。这个指标通常被使用,称为 像素准确率

像素准确率 =  #TP + #TN  __________________  #TP + #TN + #FP + #FN

然而,像素准确率存在一个问题;当待分割的物体相对于图像较小时,这个指标会强调那些被正确分类为非物体的像素数量(即非活动检测)。例如,在一个 1,000x1,000 的图像中,假设我们有一个 100x100 的物体,且我们的模型将图像中的所有像素都分类为背景,那么像素准确率将为 99%。

为了解决这些问题,我们可以使用另一个指标来评估语义分割模型,*均交并比mIoU)。

图 5.30 – 分割掩膜的 IoU

图 5.30 – 分割掩膜的 IoU

该度量的计算方式与我们在前一个实验中为对象检测看到的 IoU 类似,在图 5.16中可以看到。然而,对于对象检测,分析是基于边界框的,而对于语义分割,如图 5.30所示,它评估的是目标和预测掩膜之间的公共像素数(交集),除以两个掩膜中所有像素的总数(并集),然后计算所有数据集类别的算术*均值。

比较语义分割的网络架构

语义分割模型的输出是不同的分割掩膜,大小与图像输入相同。为了实现这个目标,提出了几种架构,主要区别在于没有全连接层。恰当地,这种网络架构被称为全连接网络FCNs)。几个模型从这一初始架构演化而来,并在首次提出时达到了当时的最先进水*:

  • 编码器–解码器:CNN 的卷积和最大池化层计算特征图可以看作是编码器,因为图像信息被编码为多维实体(特征图)。在这种架构中,经过 CNN 特征图后,一系列上采样层(解码器)被级联,直到计算出与原始图像大小相同的图像。U-Net就是这种架构的一个例子。

  • 空间金字塔池化:FCN 的一个问题是编码器未能向下游层提供足够的全局场景线索,导致对象因缺乏全局上下文而被误分类(例如,在水面图像中将船只标记为汽车,而预期应为船只而非汽车)。在这种架构中,特征图被聚合到输出中,同时不同模块计算的不同网格尺度的特征图也被结合在一起。PSPNet就是这种架构的一个例子。

  • 上下文模块:捕捉多尺度信息的另一种选择是将额外的模块级联到原始网络的顶部。DeepLab-v3可以看作是这种类型的网络和空间金字塔池化网络的结合。

图 5.31 – 语义分割的网络架构

图 5.31 – 语义分割的网络架构

在本实验中,我们将使用预训练版本的 PSPNet 和 DeepLab-v3。

使用分割地面真值探索 Penn-Fudan 行人数据集

这个数据集是我们在前一个实验中使用的那个数据集。然而,在本实验中,我们将使用的地面真值不是对象检测所需的边界框,而是语义分割所需的掩膜。

图 5.32 – Penn-Fudan 行人数据集与掩膜地面真值

图 5.32 – Penn-Fudan 行人数据集及掩码真实值

如我们在图 5.32所见,每个图像可能包含一个或多个行人及其对应的掩码。该图像是通过 GluonCV 可视化工具包(plot_mask函数)计算得到的。

在这个数据集中,没有需要分类的不同类别,也没有进一步的可视化结果。

介绍语义分割模型库

GluonCV 还提供了用于语义分割的预训练模型,在其模型库中。对于MS COCO数据集,以下是精度(mIoU)与性能(每秒样本数)的对比图:

图 5.33 – 用于语义分割的模型库(MS COCO)

图 5.33 – 用于语义分割的模型库(MS COCO)

注意

来源:cv.gluon.ai/model_zoo/detection.html

图 5.33展示了 GluonCV 模型库中最重要的预训练模型,比较了精度(纵轴上的 mIoU)和推理性能(横轴上的每秒样本数)。目前,右上方象限没有模型,意味着目前我们需要在这两个特性之间找到*衡。

使用 GluonCV 模型库中的模型只需几行代码,我们将探索这一路径来解决Penn-Fudan Pedestrians数据集的分割任务,具体步骤如下。

从模型库加载 PSPNet 预训练模型

金字塔场景解析网络PSPNet)是一种空间金字塔池化语义分割架构,这意味着添加了一个金字塔池化模块来提供全局线索。该模型由赵等人(香港中文大学)于 2016 年开发,并在 2016 年ILSVRC 场景解析挑战赛中获得第一名。

除了全局池化模块,它与 FCNs 的不同之处在于使用了扩张卷积。

图 5.34 – PSPNet 架构

图 5.34 – PSPNet 架构

注意

来源:github.com/hszhao/PSPNet/issues/101

图 5.34中,我们可以看到 PSPNet 的整体架构:

  • 特征图:基于 ResNet 的 CNN 架构,使用扩张卷积来计算一个尺寸为原始图像 1/8 的特征图。

  • 金字塔池化模块:通过四级金字塔(整图、半图、四分之一图和八分之一图),计算全局上下文信息。它与原始特征图拼接在一起。

  • 最终预测:通过卷积层进行最终计算,以生成最终的预测结果。

在我们的实验中,我们将使用 ResNet-101 网络作为骨干网络。我们可以通过一行代码加载模型:

pspnet = gcv.model_zoo.get_model('psp_resnet101_coco', pretrained=True, ctx=ctx)

模型成功下载。

评估从模型库(Model Zoo)中预训练的 PSPNet 模型

通过使用Penn-Fudan Pedestrian数据集进行分割任务,我们现在可以对上一节加载的模型进行定性和定量评估。

从质量上讲,我们可以从数据集中选择一张图片,并将模型的输出与数据集中的真实结果进行比较:

图 5.35 – 比较 PSPNet 的预测掩码和真实掩码

图 5.35 – 比较 PSPNet 的预测掩码和真实掩码

图 5.35中,我们可以看到预测的分割掩码与真实掩码之间有很强的相关性。该图是使用 GluonCV 可视化工具包(plot_mask函数)计算得出的。

从数量上讲,我们可以进行 mIoU 评估以及计算此指标所花费的运行时间:

PixAcc:  0.4650485574278924
mIoU  :  0.5612896701751177
Elapsed Time:  341.7681269645691 secs

对于该模型在MS COCO上的计算 mIoU(参见语义分割模型库)为0.70;因此,考虑到我们的模型 mIoU 为0.56,我们可以得出结论,模型执行任务的准确性较高。然而,完成该任务确实花了一些时间(大约 340 秒)。

从 Model Zoo 加载 DeepLab-v3 预训练模型

DeepLab-v3是一种语义分割架构,它结合了编码器-解码器空间金字塔池化架构。该架构由 Chen 等人(Google)于 2015 年(DeepLab-v1)至 2017 年(DeepLab-v3)开发。

这是从一系列相互演化的架构中来的第三个版本——DeepLab-v1、DeepLab-v2 和 DeepLab-v3。

从研究论文中,也可以在“更多内容”部分看到 DeepLab-v1 架构:

图 5.36 – DeepLab-v1 架构

其次,我们可以看到 DeepLab-v2 架构:

图 5.37 – DeepLab-v2 架构

最后,我们可以看到 DeepLab-v3+架构:

图 5.38 – DeepLab-v3+架构

这些不同的架构有相似之处也有不同之处,具体如下:

  • DeepLab-v1:该架构来源于原始的 FCN,并使用了 VGG-16 作为主干网络。最重要的创新是使用了空洞卷积或稀疏卷积,而不是标准卷积。我们在最初介绍卷积层时讨论过这个卷积参数,在第三章《解决回归问题》中展示了它如何增加感受野。该架构还使用了全连接条件随机场CRFs)进行后处理,以优化最终的分割掩码,尽管它非常慢且无法端到端训练。

  • DeepLab-v2:该模型最重要的创新是一个名为Atrous Spatial Pyramid PoolingASPP)的新模块。有了这个模块,一方面,网络可以将多尺度特征编码到一个固定大小的特征图中(适应不同的输入大小),另一方面,通过使用空洞卷积,增加了感受野,优化了计算成本。在原始实现中,使用了 4 到 6 个尺度。此外,它使用 ResNet 作为主干网络。

  • DeepLab-v3:在这一最后的迭代中,引入了若干变化。首先,网络修改为使用批归一化和丢弃法。其次,ASPP 模块进行了修改,增加了一个新的尺度,并在单独的通道中进行全局图像池化,以便使用细粒度的细节。最后,训练中的多尺度部分被移除,仅应用于推理。通过这些小改进的副产品是,CRF 步骤不再需要,从而提供了更快的结果。

对于我们的实验,我们将使用 ResNet-152 网络作为骨干网络。我们可以通过一行代码加载模型:

deeplab = gcv.model_zoo.get_model('deeplab_resnet152_coco', pretrained=True, ctx=ctx)

模型成功下载。

评估来自 Model Zoo 的 DeepLab-v3 预训练模型

使用Penn-Fudan Pedestrian数据集进行分割任务后,我们现在可以对上一节加载的模型进行定性和定量评估。

在定性方面,我们可以从数据集中选择一张图像,并将模型的输出与数据集的地面真实输出进行比较:

图 5.39 – 比较 DeepLab-v3 预测的掩码与地面真实值

图 5.39中,我们可以看到预测的分割掩码与地面真实值的预期结果之间有非常强的相关性。这个图是使用 GluonCV 可视化工具包(plot_mask函数)计算得到的。

从定量上讲,我们可以执行 mIoU 评估,并计算出用于计算此指标所花费的运行时间:

PixAcc:  0.4653841191754281
mIoU  :  0.5616023247999165
Elapsed Time:  74.66736197471619 secs

对于该模型在MS COCO上的计算得到的 mIoU(见语义分割模型库)是0.715;因此,考虑到我们模型的0.56值,我们可以得出结论,我们的模型执行任务时准确度良好,且与 PSPNet 的值相似。然而,它的速度要快得多(约 70 秒)。

工作原理...

在这个食谱中,我们解决了语义分割问题。我们分析了图像分类和目标检测在评估、网络架构和数据集上的差异。

在我们的示例中,我们使用了一个公开可用的数据集——Penn-Fudan Pedestrians数据集,并使用了在MS COCO上预训练的模型。这个选择并非随意;MS COCO有一个person类别,因此,预期这些预训练模型会表现得很好,因为它们已经见过该数据集类别的图像。

然而,正如前面食谱中提到的,当这不可行时,且我们将从一个数据集应用预训练模型到另一个数据集时,数据的概率分布通常会非常不同;因此,获得的准确度可能会很低。这就是领域差距领域适应问题,它发生在源数据集(模型已预训练的图像)和目标数据集(模型评估的图像)之间。

针对这些问题,一种解决方案是微调。这种方法将在后面的章节中详细探讨。

我们通过评估两个预训练模型 PSPNet 和 DeepLab-v3 来结束了本篇文章,并能够从定性和定量上比较它们在精度和计算速度方面的结果,验证了这两个模型的像素准确度(0.46)和 mIoU(0.56)相似,尽管 DeepLab-v3 的速度更快(~4.5x)。

还有更多...

图 5.33 显示了对应于模型库中语义分割(在 MS COCO 上的样本每秒数与 mIoU 图)的静态图像;该链接有一个动态版本,值得一看:cv.gluon.ai/model_zoo/segmentation.html。在这个页面上,包含了来自 GluonCV 模型库的不同模型的结果;我建议你重现这些结果,因为这是一个有趣的练习。

在我们讨论网络架构演变时,尤其是 DeepLab,我们提到了膨胀卷积(dilated/atrous convolutions)如何帮助多尺度上下文聚合。该研究论文深入探讨了这一主题:arxiv.org/pdf/1511.07122.pdf

此外,阅读原始研究论文非常有趣,可以看到从学术角度来看,语义分割任务是如何发展的:

语义分割是一个活跃的研究领域,因此它不断发展,新的网络不断出现并重新定义了技术的前沿,例如以下几种:

第六章:使用自然语言处理理解文本

自然语言处理NLP)是机器学习的一个领域,专注于理解语言(以文本数据形式呈现)。在过去几年中,它是发展最快的领域之一,取得了在情感分析、聊天机器人、文本摘要和机器翻译等方面的显著成果。NLP 是亚马逊(Alexa)、谷歌和苹果(Siri)等开发的助手的核心,也是像 ChatGPT 和 Llama 2 这样的现代助手的核心。

在本章中,我们将学习如何使用 GluonNLP,这是一个专门用于 NLP 的 MXNet Gluon 库,如何构建我们自己的网络,以及如何使用其模型库 API 进行几个预训练模型的应用。

具体来说,我们将涵盖以下主题:

  • 介绍 NLP 网络

  • 使用主题建模分类新闻要点

  • 分析电影评论中的情感

  • 从越南语翻译到英语

技术要求

除了前言中指定的技术要求外,本章还适用以下技术要求:

  • 确保你已经完成了食谱 1安装 MXNet、Gluon、GluonCV 和 GluonNLP,来自第一章与 MXNet 一起启动并运行

  • 确保你已经完成了食谱 4文本分类的玩具数据集:加载、管理和可视化垃圾邮件数据集,来自第二章使用 MXNet 和可视化数据集:Gluon 和 DataLoader

本章的代码可以在以下 GitHub URL 找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch06

此外,你可以直接从 Google Colab 访问每个食谱;例如,使用以下链接访问本章的第一个食谱:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch06/6_1_Introducing_NLP_Networks.ipynb

介绍 NLP 网络

在前面的章节中,我们了解了不同的架构,如多层感知器MLPs)和卷积神经网络CNNs),分别如何处理数值数据和图像。在本章节中,我们将分析处理自然语言(以文本数据形式表达)的最重要架构。

自然语言最重要的特点是它是一个可变长度的单词列表,而这些单词的顺序很重要;它是一个序列。我们分析的之前的架构并不适合可变长度的数据输入,并且没有有效地利用单词之间的关系。

在本章中,我们将介绍为处理单词序列而开发的神经网络:

  1. 我们将从应用前一章中介绍的网络开始,即用于文本处理的 CNN,称为 TextCNNs

  2. 之后,我们将介绍 递归神经网络RNNs)及其基础实现。接着,我们将继续介绍改进版,即 长短期记忆LSTM)。

  3. 然后,正如我们在计算机视觉中所做的那样,我们将介绍 GluonNLP 模型库,这是 MXNet 最具价值的功能之一。我们将利用这些库 MXNet 和 GluonNLP 来理解和实现 transformers 及其自注意力机制,并研究这些网络如何处理变长的序列。

准备工作

与前几章一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但这些操作不会很难。

如何操作...

在这个配方中,我们将执行以下操作:

  1. 应用 CNNs 进行文本处理(TextCNNs)

  2. 介绍 RNNs

  3. 改进 RNNs 使用 LSTM

  4. 介绍 GluonNLP 模型库

  5. 关注 Transformers

接下来,我们将详细介绍这些网络架构。

应用 CNNs 进行文本处理

CNNs 在前一章中被介绍,通常用于处理图像。然而,通过一些轻微的修改,CNNs 也能非常高效地处理文本数据。

图像是二维数据,如在 第五章 中所示,计算机视觉中的图像分析,我们在这些数据上使用了两个层:

  • 2D 卷积层

  • 最大池化层

这些操作经过轻微修改以适应文本数据,文本数据可以看作是 1D 序列。因此,对于一维卷积层,我们有以下内容:

图 6.1 – 一维卷积

图 6.1 – 一维卷积

图 6.1 所示,序列与卷积核随时间变化,输出产生新的序列。请注意,同时分析的词汇数目是卷积核大小(前图中为 3)。

对于最大池化层,由于我们只有一个维度(即时间),这些层被称为 时间最大池化 层:

图 6.2 – 时间最大池化

图 6.2 – 时间最大池化

图 6.2 所示,序列中的最大值被选中。

使用 MXNet Gluon,我们可以如下定义一维卷积和时间最大池化层:

# TextCNN Components
# 1D Convolution
conv1d = mx.gluon.nn.Conv1D(3, 100, activation='relu')
# Max-Over-Time Pooling
max_over_time_pooling = mx.gluon.nn.GlobalMaxPool1D()

如何使用这些层的示例可以在 GitHub 代码中找到。

如同前一章中展示的 CNN 架构(参见 图 5.6),通常,在特征学习阶段之后,我们会有一个分类器。作为示例应用,这种架构将帮助我们后续进行情感分析。

介绍递归神经网络(RNNs)

如食谱介绍中所讨论,RNN 是处理可变长度数据序列的架构。对于 NLP,这些数据点是由单词组成的句子,但它们也可以用于图像序列(视频)等。

RNN 的历史是一系列逐步尝试,旨在改进对序列中不同输入的递归处理。已经有几个重要的贡献,最著名的包括 Hopfield(1982)、Jordan(1986)和 Elman(1990)。

RNN 的理念是,一旦从输入中处理出输出,这个输出会再次被送入模型(以递归的方式),并与新的输入结合。这个机制使得模型能够具有记忆(可以访问过去的数据)并处理新的输入,同时考虑到过去的信息。这个基本架构通常被称为基础 RNN

请注意,如食谱 4中所介绍的,理解文本数据集——加载、管理和可视化 Enron 邮件数据集,在第二章中,使用 MXNet 并可视化数据集:Gluon 和 DataLoader,NLP 网络的输入,包括 RNN,不是从数据集中获得的单词,而是这些单词的数字表示形式,如独热编码或词嵌入。

如果将数据序列建模为连续的输入 x(1),x(2),... x(t),则 RNN 的架构可以如下可视化:

图 6.3 – RNN 架构

图 6.3 – RNN 架构

图 6.3中,我们可以看到每个输入x(t)如何随时间处理,以及处理后的输入(隐状态)如何循环回传递给下一次迭代。让我们更深入地了解每个步骤发生的事情,如前图所示,为了简化,激活函数和偏置未显示。基础 RNN 单元的实际方程式如下:

图 6.4 – 基础 RNN 单元的方程式

图 6.4 – 基础 RNN 单元的方程式

图 6.4所示,对于每个步骤,输入x(t)乘以权重矩阵U,并加上偏置向量b,得出值a(t),假设没有先前的输入h(t – 1) = 0。状态值h(t)作为该值的激活函数tanh的输出计算。当有先前的输入时,先前的状态值h(t – 1)与权重矩阵W相乘,并加到值a(t)h(t)的计算中。

状态值h(t)然后乘以权重矩阵V,并加上偏置向量c,得出值o(t)。单元的输出作为该值的激活函数softmax的输出计算,最终得到输出值y(t)

这些单元可以堆叠在一起,形成完整的 RNN。

使用 MXNet 和 Gluon,我们可以轻松创建我们自己的自定义 RNN 网络:

# RNNs MXNet Implementation Example
class RNNModel(mx.gluon.Block):
    """
    A basic RNN Model
    """
    def __init__(self, num_hidden, num_layers, embed_size, **kwargs):
        super(RNNModel, self).__init__(**kwargs)
        self.rnn = mx.gluon.rnn.RNN(
            num_hidden,
            num_layers,
            input_size=embed_size)
    def forward(self, inputs, hidden):
        output, hidden = self.rnn(inputs, hidden)
        return output, hidden

我们可以使用以下代码定义一个自定义 RNN:

# RNN with 3 hidden cells, 1 layer and expecting inputs with 20 embeddings
rnn = RNNModel(3, 1, 20)
 rnn.collect_params().initialize(mx.init.Xavier(), ctx=ctx)

此外,我们使用以下方法处理顺序输入:

rnn_hidden = hidden_initial
outputs = []
for index in range(3):
    rnn_output, rnn_hidden = rnn(inputs[index], rnn_hidden)
    outputs.append(rnn_output)

前面展示的代码运行的是一个自定义的 RNN。欢迎在本书附带的 GitHub 仓库中的笔记本上进行实验。

总结来说,RNN 的一个优点是,先前输入中存在的信息会保存在随着时间步传递的状态中。然而,这些信息会不断地被不同的权重矩阵乘以并通过非线性函数(tanh)传递,结果是,在多个时间步后,状态信息被修改,不再作为记忆起作用;存储的信息发生了过大的变化。长时记忆存储是 RNN 的一个问题。

训练 RNN

在前面的章节中,我们看到如何通过使用监督学习来训练网络,通过计算期望输出(真实值)与网络实际输出之间的损失函数。然后,这个误差可以从网络的外层向内层进行反向传播,并更新这些层的权重。

对于 RNN,在每一个时间步,网络会接收一个输入序列和期望的输出序列。每个时间步的误差都会被计算出来,并从网络的外层反向传播到内层。这个适用于 RNN 的变化称为时间反向传播BPTT)。

与其他网络一样,计算不同梯度涉及矩阵的迭代乘法。这个操作是指数级的,这意味着,在多次出现后,数值要么会收缩,要么会爆炸。这导致了我们之前讨论过的问题:梯度消失梯度爆炸。这些问题使得 RNN 的训练非常不稳定。BPTT 的另一个重要缺点是,由于它是顺序计算,无法并行化。

使用长短期记忆(LSTM)改进 RNN

LSTM 由 Hochreiter 和 Schmidhuber 于 1997 年提出,作为解决前述问题(缺乏长期记忆和梯度消失/爆炸)的一种机制。在 LSTM 中,与其让前一个状态被乘以并通过非线性函数传递,连接变得更加直接。为了提供这一机制,每个 LSTM 单元从前一个单元接收两个输入:隐藏状态ht)和单元状态ct):

图 6.5 – LSTM 网络

图 6.5 – LSTM 网络

LSTM 的关键组件被称为门,它们定义了某个输入如何被修改并成为输出的一部分。这些向量的值介于 0 和 1 之间,帮助激活/停用来自输入的信息。因此,它们是对加权输入和的 sigmoid 操作(在图 6**.5中表示为σ),接着进行乘法运算。值得强调的是,LSTM 单元中存在的三个门决定了输入或状态中有多少信息可以通过每个输出。考虑到这一点,以下方程定义了 LSTM 的行为:

图 6.6 – LSTM 单元的方程

图 6.6 – LSTM 单元的方程

图 6**.6 中的方程可以解释如下:

  • 输入门 (it):这是决定前一状态信息和当前输入信息如何被更新的门。

  • 遗忘门 (ft):这是决定前一状态信息和当前输入信息如何成为长期记忆(单元状态)一部分的门。它决定了我们想忘记当前步骤的多少。

  • 记忆单元候选值 (gt):这是决定前一状态信息和当前输入信息如何成为记忆单元一部分的计算过程。它必须允许正负值,因此,tanh 是选用的激活函数。

  • 输出门 (ot):这是决定前一状态信息和当前输入信息如何成为输出一部分的门。

  • 记忆单元 (ct):这是将前一状态(ct-1)和当前记忆单元候选值(gt)合并为新的单元状态的计算过程。

  • 输出状态 (ht):这是将记忆单元与输出门值结合的计算过程。

使用 MXNet 和 Gluon,我们可以轻松创建自定义 LSTM 网络:

# LSTMs MXNet Implementation Example
class LSTMModel(mx.gluon.Block):
    """
    A basic LSTM Model
    """
    def __init__(self, num_hidden, num_layers, embed_size, **kwargs):
        super(LSTMModel, self).__init__(**kwargs)
        self.lstm = mx.gluon.rnn.LSTM(
            num_hidden,
            num_layers,
            input_size=embed_size)
    def forward(self, inputs, hidden):
        output, hidden = self.lstm(inputs, hidden)
        return output, hidden

我们可以使用以下代码来定义一个自定义 RNN:

# LSTM with 3 hidden cells, 1 layer and expecting inputs with 20 embeddings
lstm = LSTMModel(3, 1, 20)
 lstm.collect_params().initialize(mx.init.Xavier(), ctx=ctx)

此外,我们可以使用以下方法来处理顺序输入:

lstm_hidden = [hidden_initial, state_initial]
 outputs = []
for index in range(3):
    lstm_output, lstm_hidden = lstm(inputs[index], lstm_hidden)
    outputs.append(lstm_output)

这里展示的代码运行的是一个自定义的 LSTM 网络。你可以随意修改这个 GitHub 仓库中的笔记本,来进行实验。

总结来说,LSTM 使得 RNN 能够更优化地进行训练,并已被应用于解决大量 NLP 任务,如情感分析和语言建模。

介绍 GluonNLP 模型库

MXNet GluonCV 提供的最优秀的功能之一是其庞大的预训练模型库,用户可以轻松使用并部署到自己的应用中。这个模型库被称为Model Zoo

在 Model Zoo 中,已对以下任务进行了预训练的模型:

  • 语言模型

  • 情感分析

  • 机器翻译

  • 句子分类

  • 问答

  • 命名实体识别

  • 联合意图分类和槽位标注

在本章中,我们将详细讨论情感分析和机器翻译的预训练模型。

使用 Transformers 进行注意力机制

尽管 LSTM 已被证明在许多应用中表现良好,但它们也有显著的缺点,包括训练时间较长、需要更多内存以及对随机初始化非常敏感。为克服这些限制,已经开发出了新的架构。其中最重要的一个例子就是Transformer

Transformer 是由 Google Brain 于 2017 年提出的。它是一种新的编码器-解码器架构(如在食谱 4使用 PSPNet 和 DeepLab-v3 对图像中的物体进行分割,见 第五章用计算机视觉分析图像中所见),采用了一种重新设计的机制来处理数据序列,称为注意力。这种架构的最大改进在于它不依赖于顺序处理数据。所有数据可以并行处理,从而实现更快的训练和推理。这一改进使得可以处理非常大量的文本,语料库,并产生了大规模语言模型LLM),例如双向编码器表示的 TransformerBERT)。

Transformer 的架构可以表示如下:

图 6.7 – Transformer 架构

图 6.7 – Transformer 架构

图 6.7中,我们可以区分出几个组件:

  • 输入与输出预处理:在将数据输入网络之前,嵌入和位置编码会被计算出来。

  • 编码器-解码器架构:左侧部分对应编码器,右侧部分对应解码器。前馈处理、残差连接和归一化是该组件的一部分。

  • 注意力头:序列输入编码器到解码器的路径,序列输出都通过这一机制处理。

让我们更详细地看看这些组件。

输入与输出预处理

在原始论文中,输入和输出在被送入网络之前,会通过学习到的嵌入进行处理。这些嵌入向量通常具有 512 个元素的大小。

此外,正如我们接下来所看到的,这些嵌入会通过一个softmax函数,将多个信息片段组合成一个数值,但在此过程中也会丢失位置信息。为了在整个编码和解码过程中保持位置信息,无论是输入还是输出,输入嵌入都会作为一个包含位置信息的向量添加。这些被称为位置编码

编码器-解码器架构

正如原始Google Brain 的 Transformer 论文中所提到的:arxiv.org/pdf/1706.03762.pdf,编码器和解码器由六个相同的层组成(在图 6.5中,左图和右图中的N = 6)。每个编码器层有两个组件,一个是多头自注意力机制,后面接着一个全连接的前馈网络。对于每个解码层,另加一个注意力组件,即掩蔽的多头自注意力机制。注意力头将在后续部分进行解释。

残差连接也被加入,类似于我们在Recipe 2 中看到的 ResNets,第五章使用 MXNet 进行图像分类 – GluonCV 模型库, AlexNet 和 ResNet,以及在计算机视觉分析中。

这些信息一起经过层归一化处理,类似于Recipe 3中介绍的批归一化,在第三章中,回归模型训练解决回归问题。最重要的区别是,如介绍层归一化的论文所述(arxiv.org/abs/1607.06450),层归一化使得一个层中的所有隐藏单元共享相同的归一化项,但不同的输入数据可以有不同的归一化项。层归一化已被证明在处理序列时比批归一化效果更好。

通过结合不同编码器和解码步骤中的所有这些层(包括嵌入层),这些向量将具有 512 的维度。

注意力头

在 Transformers 中,一个序列中的每个单词如何与另一个序列中的每个单词相连接,是通过注意力机制实现的。例如,如果我们有一个包含N个单词的英语输入序列(I love you very much),并且它的法语翻译有M个单词(Je t’aime beaucoup),那么这两个序列之间的注意力权重矩阵将具有NxM 的维度。使用这种机制连接序列相比于递归机制(例如 RNN 中使用的机制)有一个强大的优势——并行化。注意力是一个矩阵操作,因此可以被最佳化地并行化。

I love you very much
je 0.90 0.02 0.06 0.01 0.01
t’ 0.11 0.01 0.80 0.03 0.05
aime 0.03 0.92 0.03 0.01 0.01
beaucoup 0.02 0.02 0.02 0.41 0.53

图 6.8 – 注意力矩阵示例

在 Transformer 论文中,图 6.9 中显示了三个矩阵,查询(Q),键(K)和值(V)。以下是对这些矩阵的解释:

  • 查询:这是每个输入单词的输入表示

  • 键和值:类似于一个哈希映射,将键映射到值,这些矩阵用于索引(键)和提供单词的表示(值)(与查询不同)

这三种矩阵操作的组合被称为点积注意力函数,可以描述如下:

图 6.9 – 注意力函数

图 6.9 – 注意力函数

当输出是输入序列的表示时,这个机制被称为自注意力头。在之前的架构图图 6.7中,靠*输入序列和输出序列的两个注意力机制(图的下部)是自注意力机制,因为由该函数输出的序列与输入的序列相同。当情况不同,例如在连接编码器和解码器的注意力机制中,这被称为交叉注意力头。输出向量的自注意力头是被屏蔽的,这意味着在网络训练中只能使用过去的信息。这使得 Transformer 模型能够从有限的输入生成文本(如 GPT-3 或 BLOOM 等自回归模型)。

在 Google Brain 的论文《Attention is All You Need》中(arxiv.org/abs/1706.03762),并不是并行处理所有输入数据,而是使用了八个注意力头并行工作。由于期望输出具有相同的维度(512),每个头处理的是一个较小的向量(维度为 64)。该论文描述如下:

“多头注意力使模型能够同时关注不同表示子空间中不同位置的信息。”

降低维度可以使总计算成本与使用完整(全维度)注意力头的成本相似。

在 GluonNLP 中的实现

GluonNLP 有自己实现的 Transformer 模型,因此获取我们的编码器和解码器是直接的,具体如下:

# Transformers MXNet Implementation Example
# Transformer with 6 layers (encoder and decoder), 2 parallel heads, and expecting inputs with 20 embeddings
transformer_encoder, transformer_decoder, _ = nlp.model.transformer.get_transformer_encoder_decoder(
    num_layers=6,
    num_heads=2,
    units=20)
transformer_encoder.collect_params().initialize(mx.init.Xavier(), ctx=ctx)
 transformer_decoder.collect_params().initialize(mx.init.Xavier(), ctx=ctx)

现在,我们可以使用编码器来处理输入数据;然而,使用 Transformers 时,我们可以同时处理整个输入:

encoded_inputs, _ = transformer_encoder(inputs[0])

大规模 Transformer 是目前大多数 NLP 任务的最先进技术,如主题建模、情感分析或问答,并且编码器和解码器架构也会单独用于不同的任务。

它是如何工作的……

在这个方案中,我们介绍了几个网络,通过 MXNet、Gluon 和 GluonNLP 来处理 NLP 任务:

  • RNNs

  • LSTM

  • Transformers

我们已经回顾了这些架构的工作原理,并分析了它们的优缺点,以及每个架构如何改进前一个架构,探索了序列、BPTT、内存单元和注意力等概念。

在接下来的方案中,我们将探索如何使用这些架构来解决实际问题,如主题建模、情感分析和机器翻译。

还有更多……

本书中讨论的一些概念过于先进,无法在本书中详细介绍。如果你希望更深入地理解这些内容,我强烈建议查看以下参考资料:

在本章中,我们将详细分析以下任务:主题建模、情感分析和文本翻译。然而,MXNet GluonNLP 模型库包含了许多预训练的模型,适用于大量任务。我们鼓励你探索在nlp.gluon.ai/model_zoo/index.html上提供的不同示例。

使用主题建模对新闻亮点进行分类

在本食谱中,我们将研究自然语言处理(NLP)中最有趣的任务之一——主题建模。在这个任务中,用户必须在给定文档集的情况下找到主题数。 有时,主题(以及主题的数量)是已知的,此时可以应用我们在前几章中看到的监督学习技术。然而,在典型的场景中,主题建模数据集没有提供真实标签,因此是无监督学习问题。

为了实现这一点,我们将使用来自 GluonNLP Model Zoo 的预训练模型,并将其词嵌入应用于聚类算法,从而得出聚类的主题。我们将把这一过程应用于一个新的数据集:100 万 新闻头条

准备工作

与前几章一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但它并不难。

此外,我们将处理文本数据集。因此,我们将重新回顾一些已经在Recipe 4中提到的概念,理解文本数据集——加载、管理和可视化恩隆邮件数据集,来自第二章与 MXNet 协作并可视化数据集:Gluon 和 DataLoader

如何实现...

在本食谱中,我们将执行以下步骤:

  1. 探索100 万新闻 头条数据集

  2. 应用词嵌入

  3. 使用 K-means 聚类主题

  4. 整合所有内容

让我们在接下来的子章节中逐步了解这些步骤。

探索 100 万新闻头条数据集

该数据集是主题建模中最著名的数据集之一。它包含澳大利亚广播公司(ABC)网站从 2003 年到 2021 年(含)发布的 19 年的重要新闻头条。每个新闻头条的主题没有包括在内。

正如我们预期的那样,真实世界的数据集包含大量的词汇。因此,在进一步处理之前,我们将开始清洗数据,并按照Recipe 4中描述的过程进行,理解文本数据集——加载、管理和可视化恩隆邮件数据集,来自第二章与 MXNet 协作并可视化数据集:Gluon 和 DataLoader

  • 分词

  • 移除停用词

  • 词干提取

  • 词形还原

此外,这个数据集包含超过 100 万个头条新闻(实际上是 120 万个)。为了能够高效地处理它们,我们将使用一个包含 5%的子集:

reduced_number_headlines = int(0.05 * number_headlines)
 print(reduced_number_headlines)
62209

如果我们分析这个子集来计算每个头条新闻的词数,我们可以绘制出以下的直方图:

图 6.10 – 按词数分布的头条新闻

图 6.10 – 按词数分布的头条新闻

正如在图 6.10中所示,大多数头条新闻包含 4 到 8 个词。

应用词嵌入

食谱 4中,理解文本数据集 – 加载、管理和可视化 Enron 邮件数据集,来自于 第二章使用 MXNet 和可视化数据集:Gluon 和 DataLoader,我们介绍了来自 GluonNLP 模型库的两个嵌入模型:谷歌的word2vec和斯坦福大学的GloVe。在这个使用案例中,我们将使用 word2vec,因为它是在一个名为 Google News 的数据集上训练的,该数据集由 3 百万单词和短语组成,来源于一个包含 1000 亿个单词的语料库。由于该语料库由新闻信息组成,这个嵌入模型非常适合我们的使用案例。

图 6.11 – word2vec 嵌入的 2D 表示

图 6.11 – word2vec 嵌入的 2D 表示

使用这个模型时,每个单词都会被转换为一个包含 300 个分量的向量。然而,在我们的应用中,处理的是标题(完整句子),我们更关心的是整个标题的表示,而不仅仅是其独立的单词。对于我们的应用,完成这一目标的一个简单有效的方法是计算每个预处理单词的*均向量。

使用 K-means 聚类主题

在我们得到标题嵌入后,分类标题的最后一步就是聚类它们。有很多聚类算法,比如期望最大化聚类均值漂移聚类。然而,对于我们的应用,我们将使用我最喜欢的算法——K-means,它在我们之前提到的 Python 包 scikit-learn 中已经实现。

图 6.12 – K-means 可视化

图 6.12 – K-means 可视化

K-means 背后的直观思想是,给定一个聚类数量 K,它会随机分配聚类的质心,并将每个新看到的向量分配给距离最*的质心(分配步骤)。然后,它会计算新质心,作为属于该聚类的向量的均值(更新步骤),并迭代此过程。这个过程会一直重复,直到质心位置没有显著变化。因此,整个数据集可以多次迭代。对于大型数据集,可以添加其他标准来确定收敛和停止标准。

K-means 的一个重要缺点是聚类的数量是一个输入参数,因此需要对数据集有一定的直觉或知识。实际上,事先知道这个信息是很困难的。因此,我强烈建议针对不同的 K 值运行算法。对于我们的使用案例,选择的聚类数量是 4。

将所有步骤整合在一起

经过这三步处理(数据清理、嵌入和聚类),我们就可以开始分析一些结果了。

主题建模中最有趣的输出是识别哪些头条新闻主题与每个聚类相关。一个有用的方法是可视化每个聚类中最重要的词汇,并提出相关的主题。因此,我们可以为第一个识别的聚类(聚类 0)绘制以下内容:

图 6.13 – 第一个聚类中按重要性排序的前 15 个词

图 6.13 – 第一个聚类中按重要性排序的前 15 个词

图 6.13中,我们可以看到最重要的词是win(胜利)、world(世界)、cup(杯赛)和final(决赛)。这些词都是与体育相关的,因此聚类 0 的主题是体育

我们可以为每个聚类整理出最重要的词汇:

Cluster 0 : ['win', 'world', 'cup', 'final', 'lead', 'set', 'hit', 'face', 'record', 'open', 'year', 'miss', 'test', 'new', 'day']
Cluster 1 : ['govt', 'iraq', 'urg', 'nsw', 'polic', 'continu', 'say', 'australia', 'vic', 'consid', 'qld', 'iraqi', 'forc', 'secur', 'sar']
Cluster 2 : ['plan', 'council', 'new', 'govt', 'fund', 'say', 'boost', 'group', 'water', 'concern', 'health', 'report', 'claim', 'seek', 'warn']
Cluster 3 : ['polic', 'man', 'kill', 'charg', 'court', 'murder', 'crash', 'death', 'attack', 'woman', 'face', 'arrest', 'probe', 'car', 'dead']

从前面的信息中,我们可以得出每个聚类的主题:

  • Cluster 0: 体育

  • Cluster 1: 全球事务

  • Cluster 2: 经济

  • Cluster 3: 犯罪/时事新闻

有了这些信息,我们现在可以选择任何未在聚类过程中使用的头条新闻,并预测它的主题:

import random
random_index = random.randint(0, len(headlines_full)) random_headline = headlines_full["headline_text"][random_index] print(random_index, random_headline)

这段代码会生成类似于以下的输出:

771004 waratahs count cost with loss to cheetahs

前面这句话明显与体育相关,因此我们预期预测的聚类应为0

为了预测聚类组,我们需要按照以下步骤操作:定义函数、运行函数并验证结果。

  1. 让我们编写一个预测函数,执行必要的清理和预处理、嵌入以及聚类步骤:

    def predict(cluster_km, headline):    """    This function predicts the cluster  of a headline via K-Means
        """
        # Cleaning
        headline_clean = clean_text(headline)
        headline_pre = process_words(headline_clean)
        # Embeddings
        bag_of_words_list = headline_pre.split()
        number_of_words = len(bag_of_words_list)
        # Process 1st word (to be able to concatenate)
        word_embeddings_array = w2v[bag_of_words_list[0]].reshape(1, embedding_features)
        # To manage headlines with just 1 meaningful word
        word_index = -1
        for word_index, word in enumerate(bag_of_words_list[1:]):
            word_embeddings = w2v[word].reshape(1, embedding_features)
            word_embeddings_array = mx.nd.concat(word_embeddings_array,
     word_embeddings, dim=0)
        assert(number_of_words == word_index + 2)
        average_embedding_headline_pre = mx.nd.mean(word_embeddings_array, axis=0).reshape(1, embedding_features)
        # Clustering
        selected_cluster = cluster_km.predict(average_embedding_headline_pre.asnumpy())
        return selected_cluster
    
  2. 让我们通过以下代码运行这个函数:

    predicted_cluster = predict(cluster_km, random_headline)
    print(predicted_cluster)
    
  3. 输出是实际预测的聚类:

    [0]
    

做得好!

它是如何工作的...

在这个实例中,我们探索了被称为主题建模的自然语言处理任务。这个任务试图找出与一组给定文档相关的主题。通常,不提供明确的答案(没有标准答案),因此这个任务更适合通过无监督学习来解决。我们尝试使用 ABC 的100 万新闻 头条 数据集来解决这个问题。

我们遵循了三步法:

  1. 数据处理与清洗

  2. 词嵌入

  3. 聚类

对于第一步,我们遵循了任何自然语言处理问题的典型流程:

  1. 数据清理

  2. 分词

  3. 去除停用词

  4. 词干提取

  5. 词形还原

对于第二步,我们应用了 Google 的 word2vec 来计算每个单词的嵌入,每个头条新闻的嵌入是其每个单词嵌入的*均值。

在第三步中,我们探索了无监督学习算法 K-means,选择了四个聚类,并计算了它们的中心点。我们生成了以下主题聚类:体育、全球事务、经济与犯罪、时事新闻。

有了这些信息,我们选择了一个随机的头条新闻,并准确地预测了它所关联的主题。

还有更多内容…

无监督学习是一个非常广泛的主题,也是一个活跃的研究领域。要了解更多,好的起点是它的维基百科文章:en.wikipedia.org/wiki/Unsupervised_learning

除了100 万新闻标题数据集,另一个著名的主题建模参考数据集是 20 Newsgroups 数据集。我建议使用更大的 6 Newsgroups 选项,因为许多 Newsgroups 有很多相似的主题。更多信息可以在qwone.com/~jason/20Newsgroups/找到。

在处理嵌入时,我们遵循的一个简化方法是通过*均每个对应的词嵌入来计算标题嵌入。然而,还有其他方法,称为文档嵌入或句子嵌入,使用像Doc2VecSentence-BERT这样的模型,这些方法对于其他应用可能更有用。关于这些方法的对比分析可以在www.analyticsvidhya.com/blog/2020/08/top-4-sentence-embedding-techniques-using-python/找到。

有关 K-means 算法如何工作的详细解释,建议你查看towardsdatascience.com/k-means-clustering-explained-4528df86a120

在预测给定标题的主题时,K-means 算法等同于另一个算法,称为 1-最*邻,它是 K 最*邻的特例,其中 K = 1。关于这个监督学习算法的更多信息可以在en.wikipedia.org/wiki/K-nearest_neighbors_algorithm找到。

分析电影评论的情感

情感分析是使用多种不同的技术,包括自然语言处理(NLP),来识别与人类生成的信息(在我们这里是文本)相关的情感状态。在这个配方中,我们将对现实世界的电影评论进行情感分析。我们将评论分为两种情感:正面或负面。

为了实现这一点,我们将使用来自 GluonNLP 模型库的多个预训练模型,并将其词嵌入应用于分类器,分类器将输出预测的情感。我们将这一过程应用于一个新的数据集:IMDb 电影评论

准备工作

和前几章一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但这并不难。

此外,我们将对文本数据集进行分类。因此,我们将回顾在配方 4中已经看到的一些概念,理解文本数据集 – 加载、管理和可视化 Enron 邮件数据集,来自第二章与 MXNet 协作并可视化数据集:Gluon 和 DataLoader

如何实现...

在这个配方中,我们将执行以下步骤:

  1. 探索IMDb 电影 评论 数据集

  2. 将 TextCNN 与词嵌入结合

  3. 介绍 BERT

  4. 整合一切

探索 IMDb 电影评论数据集

这个数据集由斯坦福大学的研究人员在 2011 年收集。它被划分为训练集和测试集,每个集包含 25,000 条评论。每部电影最多包含 30 条评论。评论的情感极为两极化,负面评论的值在 [1, 4] 之间,正面评论的值在 [7, 10] 之间。

图 6.14 – 电影评论的直方图(不*衡数据集)

图 6.14 – 电影评论的直方图(不*衡数据集)

在我们的分析中,我们将情感值简化为二分类情感分析任务。因此,负面评论被分配为 0 值,正面评论为 1 值。通过这种简化,数据集变得*衡。

图 6.15 – 二值化电影评论的直方图(*衡数据集)

图 6.15 – 二值化电影评论的直方图(*衡数据集)

论文作者指出的另一个要考虑的点是,由于语言中的细微差别可能包含关于情感的信息,因此评论的预处理不能包括常见的停用词和词干化。我们在预处理时考虑了这一点:

def process_words_basic(
    text,
    lemmatizer = lemmatizer):
    words = nltk.tokenize.word_tokenize(text)
    filtered_words_post = []
    for word in words:
        if word.isalpha():
            filtered_words_post.append(lemmatizer.lemmatize(word))
    return filtered_words_post

文件可以通过github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch06/utils.py访问。

这个函数应用于数据集中的所有样本。

将 TextCNN 与词嵌入结合使用

处理完数据集后,我们现在可以使用它来搭建任何我们选择的架构。在本章的第一个食谱中,我们展示了如何使用 CNN 处理序列。为了提供语言信息,TextCNN 可以使用预训练的词元表示作为输入。对于这个食谱,我们将使用两个词嵌入,它们将为我们的模型生成输入:

  • word2vec:这些词嵌入在食谱 4中介绍,理解文本数据集 – 加载、管理和可视化 Enron 邮件数据集,来自第二章与 MXNet 一起工作并可视化数据集:Gluon 和 DataLoader

  • BERT:Google 于 2018 年推出的语言模型

介绍 BERT

RNN 和 Transformer 可以处理大规模的文本序列。然而,最大的缺点之一是数据仅单向处理,从左到右。BERT 提供了一种机制,使得每个单词的表示(词元)可以同时使用来自左侧和右侧的信息。

BERT 的另一个独特特性是,它的注意力机制完全基于自注意力层;没有使用交叉注意力层。

图 6.16 – BERT 架构:比较 BERT 双向方法与如 GPT-1 等 Transformer;仅从左到右的方法

图 6.16 – BERT 架构:将 BERT 的双向方法与 GPT-1 等 Transformer 模型的左到右方法进行比较

BERT 是在无监督的方式下训练的,使用了两个任务目标:

  • 掩蔽语言模型:待分析的单词不可见,因此模型需要仅凭上下文理解其含义

  • 下一句预测:给定两句话,任务是预测它们是否相关(在长文本中,一个可能会在另一个后面出现)或它们不相关

这种训练方法与 BERT 架构的结合,证明非常成功,在论文发表时超过了当时 11 个 NLP 任务的最先进技术。

GluonNLP 提供了两个预训练的 BERT 模型,具有以下特征:

  • BERT_12_768_12:12 层、768 维嵌入向量、12 个自注意力头。这个模型被称为BERT base

  • BERT_24_1024_16:24 层、1,024 维嵌入向量、16 个自注意力头。这个模型被称为BERT large

对于我们的实验,我们将使用 BERT base 模型,可以通过以下代码轻松加载:

bert_model, vocab = nlp.model.get_model(
    'bert_12_768_12',
    dataset_name='book_corpus_wiki_en_uncased',
    use_classifier=False,
    use_decoder=False,
    ctx=ctx)

使用上述函数,我们可以轻松获得一个 BERT 模型(bert_model)及其词汇表(vocab),该模型基于 12 层架构、768 维嵌入向量、12 个自注意力头,并且使用来自英文维基百科的数据集(book_corpus_wiki_en_uncased)。

将所有内容汇总在一起

让我们总结一下到目前为止看到的所有步骤。

我们的IMDb 电影评论数据集由 25,000 个训练样本和 25,000 个测试样本组成。为了成本和计算优化,我们使用以下数据集:

  • 训练集:5,000 个样本(来自原始训练集)

  • 验证集:1,250 个样本(来自原始训练集;与我们的 5,000 个训练样本没有重叠)

  • 测试集:25,000 个样本

我们使用两个嵌入模型作为 TextCNN 的输入:

  • word2vec:300 维向量

  • BERT base:768 维向量

TextCNN 架构在两种方法中非常相似。TextCNN 的卷积核大小为 3、4 和 5,即同时分析 3、4 和 5 个单词,通道数等于嵌入组件数。此外,由于我们有二分类输出(负面正面),分类器是一个完全连接层,具有一个 sigmoid 输出(sigmoid 激活函数已包含在损失函数中,以进行计算优化)。

对于训练,两个嵌入模型使用等效的参数:

  • 优化器:Adam

  • 学习率:10-3

  • 损失函数:Sigmoid 交叉熵

  • Epochs:5

  • 批次大小:4

使用这些参数,我们通过 word2vec 嵌入模型得到了以下结果:

图 6.17 – 使用 word2vec 的训练损失/验证损失和准确度

图 6.17 – 使用 word2vec 的训练损失/验证损失和准确度

图 6.17中,我们可以看到随着训练轮次的增加,训练效果有所提升,最终在训练结束时获得了最佳验证准确率 0.89。

我们可以定性地检查结果,从我们的测试集中选择一条电影评论(未见过的样本),并查看我们的情感分析算法的输出。该示例电影评论来自 ieee-dataport.org/open-access/imdb-movie-reviews-dataset,并具有 CC BY 4.0 许可证:

I went and saw this movie last night after being coaxed to by a few friends of mine. I'll admit that I was reluctant to see it because from what I knew of Ashton Kutcher he was only able to do comedy. I was wrong. Kutcher played the character of Jake Fischer very well, and Kevin Costner played Ben Randall with such professionalism. The sign of a good movie is that it can toy with our emotions. This one did exactly that. The entire theater (which was sold out) was overcome by laughter during the first half of the movie, and were moved to tears during the second half. While exiting the theater I not only saw many women in tears, but many full grown men as well, trying desperately not to let anyone see them crying. This movie was great, and I suggest that you go see it before you judge.

我们可以将这个输入格式化为我们的 TextCNN 网络所期望的嵌入:

# Formatting single input as expected for the network
seq_output, _ = process_dataset_sample(test_dataset[0][0])
 seq_output_reshaped = mx.nd.array(seq_output, ctx=ctx).expand_dims(axis=0)

我们可以通过我们从训练中获得的最佳模型来传递它:

# Retrieve best model from training
text_cnn.load_parameters(model_file_name)
review_sentiment = text_cnn(seq_output_rehaped)
# We can omit sigmoid processing, outputs of the network
# with positive values are positive reviews
if review_sentiment >= 0:
    print(review_sentiment, "The review is positive")
else:
    print(review_sentiment, "The review is negative")

这些命令生成了以下输出:

[[2.5862172]]
 <NDArray 1x1 @gpu(0)> The review is positive

如前述输出所示,我们的算法正确地将评论分类为积极

然而,为了进行更全面和正式的分析,我们可以定量处理完整的测试集并计算最终准确率:

Final Test Accuracy: 0.87724

然而,仅凭这个数字无法提供关于I 型II 型错误的完整信息。因此,我们还可以将结果显示为混淆矩阵(在食谱 4中介绍过,评估分类模型解决 分类问题):

图 6.18 – 使用 word2vec 的混淆矩阵

图 6.18 – 使用 word2vec 的混淆矩阵

当采用相同的方法,但这次使用我们的 BERT 模型进行嵌入时,我们得到了以下结果:

图 6.19 – 使用 BERT 的训练损失/验证损失和准确率

图 6.19 – 使用 BERT 的训练损失/验证损失和准确率

图 6.19中,我们可以看到随着训练轮次的增加,训练效果有所提升,最终在训练结束时获得了最佳验证准确率 0.91。这个数字比使用 word2vec 时要高,正如预期的那样,因为 BERT 能够在评论中的单词之间建立更多的上下文关系。

我们还可以将测试集中相同的评论通过我们从训练中获得的最佳模型,使用之前相同的代码语句,得到以下输出:

[[15.462966]]
 <NDArray 1x1 @gpu(0)> The review is positive

该实验与之前使用word2vec的实验产生了相同的结果(积极评论)。

对于测试集准确率,我们得到了以下结果:

Final Test Accuracy: 0.90848

与 word2vec 结果相比,BERT 提供了 3% 更高的准确率。

混淆矩阵如下所示:

图 6.20 – 使用 BERT 的混淆矩阵

图 6.20 – 使用 BERT 的混淆矩阵

从这些结果可以看出,BERT 显著优于 word2vec。另一个重要的优势是,由于 Transformer 允许更好的并行化,训练过程也更快。

它是如何工作的……

在这个食谱中,我们解决了情感分析问题。我们分析了 TextCNN 架构,来解决这个任务,并探讨了如何将其应用于不同的词嵌入模型。

我们探索了一个新的数据集,IMDb 电影评论,并进行了适当的转换,以便在受限计算环境中使用该数据集,并将其简化为二分类任务。

我们介绍了 BERT,这是 Google 在 2018 年推出的一个新的词嵌入模型,并将其与之前探索过的模型 word2vec 进行了比较,理解了它们的差异、优势和局限性。我们理解了 BERT 的两个最重要的优势:对每个词使用双向信息,并在训练中对每个词进行掩蔽,以便每个词的信息完全基于其上下文。

我们进行了实验,比较了这两种词嵌入方法,结果发现尽管两者都能够很好地解决问题(word2vec 和 BERT 的测试准确率分别为 88% 和 91%),但 BERT 表现更好。

还有更多...

情感分析是文献中研究得较为深入的任务。想了解更多,建议阅读这篇文章:www.datarobot.com/blog/introduction-to-sentiment-analysis-what-is-sentiment-analysis/

介绍 IMDb 电影评论 数据集的论文,同时提出了一个情感分析模型,可以在这里找到:Learning Word Vectors for Sentiment Analysisai.stanford.edu/~ang/papers/acl11-WordVectorsSentimentAnalysis.pdf

BERT 在论文 arxiv.org/pdf/1810.04805.pdf 中进行了介绍。然而,以下链接提供了更直观的解释:huggingface.co/blog/bert-101。强烈建议阅读上一篇文章,因为它分析了数据如何将偏见嵌入到我们的模型中。

BERT 非常强大,并且可以与更好的语言模型进行互补,例如 RoBERTa(改进版)或 DistilBERT(性能相似但更小的模型),还有很多为特定任务微调的模型。可以在 nlp.gluon.ai/model_zoo/bert/index.html 找到 MXNet GluonNLP 中可用的预训练模型列表。

从越南语翻译到英语

自动翻译文本(机器翻译)自诞生以来一直是自然语言处理(NLP)中一个非常有趣和有用的应用案例,因为打破语言障碍有很多应用,包括聊天机器人和多语言的自动字幕。

在深度学习之前,机器翻译通常被视为一个统计问题。即使在深度学习之后,直到 2016 年 Google 将深度学习应用于机器翻译,神经机器翻译NMT)这一领域才诞生。这个模型为现在在大语言模型(LLM)中可用的翻译任务奠定了基础,如 OpenAI GPTGoogle Bard

在这个教程中,我们将应用这些技术,将越南语句子翻译成英语,使用来自 GluonNLP 模型库的预训练模型。

准备工作

与前几章一样,在这个教程中,我们将使用一点矩阵运算和线性代数,但这绝对不难。

此外,我们还将对文本数据集进行分类。因此,我们将重新回顾在第二章中已经见过的一些概念,理解文本数据集——加载、管理和可视化 Enron 邮件数据集与 MXNet 一起工作并可视化数据集:Gluon 和 DataLoader

如何操作...

在这个教程中,我们将执行以下步骤:

  1. 探索IWSLT2015数据集

  2. 评估机器翻译器(BLEU)

  3. 介绍 GNMT 模型并探索适用于此任务的 Transformers

  4. 综合所有内容

接下来我们详细看一下这些步骤。

探索 IWSLT2015 数据集

国际口语语言翻译研讨会IWSLT)是一个每年举办的科学研讨会,专注于所有形式的翻译(不一定是机器翻译)。他们生成了几个非常重要的数据集和基准,帮助机器翻译领域不断发展。2015 年,发布了一个英语-越南语数据集,包含超过 130,000 对句子的训练集和 1000 多对句子的验证/测试集。这个数据集是公开的,可以通过 MXNet GluonNLP 轻松获取,如下所示:

# IWSLT2015 Dataset (Train, Validation and Test)
# Dataset Parameters
src_lang, tgt_lang = "vi", "en"
src_max_len, tgt_max_len = 50, 50
iwslt_train_text = nlp.data.IWSLT2015("train",
                                      src_lang=src_lang,
                                      tgt_lang=tgt_lang)
iwslt_val_text   = nlp.data.IWSLT2015("val",
                                      src_lang=src_lang,
                                      tgt_lang=tgt_lang)
iwslt_test_text  = nlp.data.IWSLT2015("test",
                                      src_lang=src_lang,
                                      tgt_lang=tgt_lang)
iwslt_src_vocab = iwslt_train_text.src_vocab
iwslt_tgt_vocab = iwslt_train_text.tgt_vocab

此版本的数据集提供了以下数据:

Length of train set: 133166
Length of val set  : 1553
Length of test set : 1268

预处理过程与我们之前看到的管道类似,包括以下步骤:

  1. 句子剪辑(定义最大值)

  2. 分词

  3. 向源语言句子(越南语)添加句子结束EOS)标记,向目标语言句子(英语)添加句子开始BOS)和 EOS 标记

此外,为了优化训练,应用了一个分桶过程,其中句子根据相似的长度进行分组:

图 6.21 – 固定桶采样器

图 6.21 – 固定桶采样器

图 6.21中的示例展示了这种策略,使用了 10 个桶,产生了最小的填充量(为了使 1 个批次中的所有句子可以并行处理)。桶的大小也呈指数增长。使用 MXNet GluonNLP,如下所示:

# Bucket scheme
bucket_scheme = nlp.data.ExpWidthBucket(bucket_len_step=1.2)

在前面的示例中,每个桶的大小(宽度)增加了 20%(1.2 倍的增量)。

评估机器翻译器(BLEU)

评估机器翻译系统的效果是非常困难的。例如,使用一个单一的数字来衡量翻译质量本质上是主观的。对于我们的应用场景,我们将使用一个广泛使用的指标,称为双语评估 辅助评估BLEU)。

在 BLEU 指标中,会提供多个参考翻译,并尝试衡量自动翻译与参考翻译的相似度。为此,它比较自动翻译与参考翻译中不同的 N-gram(大小从 1 到 4)。

图 6.22 – BLEU 指标

图 6.22 – BLEU 指标

图 6.22 所示,BLEU 尝试最小化翻译中的主观性。

另一个指标是 困惑度,它大致定义了模型在看到翻译后的单词时的“惊讶”程度。当模型不再感到惊讶时,说明它表现良好;因此,困惑度越低越好。计算困惑度比计算 BLEU 指标要快,因此通常作为每批次训练中的检查性指标,而 BLEU 用于每轮计算。

介绍 GNMT 模型并探索 Transformer 在此任务中的应用

如前所述,Google 在 2016 年引入了机器翻译领域的最大突破,即 Google 神经网络机器翻译GNMT)模型 (ai.googleblog.com/2016/09/a-neural-network-for-machine.html)。

图 6.23 – GNMT 架构

图 6.23 – GNMT 架构

GNMT 是 Transformer 的先驱,并且也采用了带有编码器-解码器架构的注意力机制。编码器和解码器都是 LSTM RNN,编码器有 8 层,解码器也有 8 层。模型中实现的注意力机制是交叉注意力层。

在模型的最后,链式的束搜索采样器用于生成新的翻译,以最大化训练过的条件概率翻译。如同论文中所述,在我们的实现中,评分函数包括一个长度惩罚项,以确保翻译中的所有单词都被覆盖。

我们将把 GNMT 和 Transformer 在越南语到英语的机器翻译任务中进行比较。对于我们的应用,以下是每个模型最重要的参数:

  • GNMT

    • 编码器的层数:2

    • 解码器的层数:2

    • 单元数:512

  • Transformer

    • 编码器的层数:4

    • 解码器的层数:4

    • 单元数:512

在接下来的章节中,我们将对两种架构进行比较,以完成相同的翻译任务。

综合所有内容

让我们总结一下到目前为止的所有步骤。

我们的 IWSLT2015 越南语到英语的数据集包含 133,000 多个训练样本和 1,000 多个验证/测试样本。我们将使用完整的数据集。

我们将为机器翻译任务使用两个模型:

  • GNMT

  • Transformer

在训练中,两种架构使用相同的参数:

  • 优化器:Adam

  • 学习率:10^-3,采用阶梯衰减的学习率调度,每经过半轮训练,学习率减半

  • 损失函数: 掩蔽的 softmax 交叉熵,类似于我们之前探讨过的交叉熵损失函数,唯一的不同是当预测长度超过其有效长度时,冗余的词会被掩蔽。

  • 迭代次数: 12

  • 批次 大小: 128

使用这些参数,我们在训练过程中得到了以下结果,采用 GNMT 模型:

图 6.24 – GNMT 训练演变(训练损失和验证损失、困惑度和 BLEU)

图 6.24 – GNMT 训练演变(训练损失和验证损失、困惑度和 BLEU)

此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 分数(乘以 100)如下:

Best model test Loss=2.3807, test ppl=10.8130, test bleu=23.15

当前的最先进模型在 BLEU 评分上可以达到 30 分以上,但这个分数已经非常高了。

在定性上,我们也可以通过一个句子示例来检查模型的表现。在我们的案例中,我们选择了I like to read books,并可以通过以下代码验证:

print("Qualitative Evaluation: Translating from Vietnamese to English")
expected_tgt_seq = "I like to read books."
 print("Expected translation:")
 print(expected_tgt_seq)
# From Google Translate
src_seq = "Tôi thích đc sách k thut."
 print("In Vietnamese (from Google Translate):")
 print(src_seq)
translation_out = nmt.utils.translate(
    gnmt_translator,
    src_seq,
    iwslt_src_vocab,
    iwslt_tgt_vocab,
    ctx)
print("The English translation is:")
 print(" ".join(translation_out[0]))

这些代码语句将产生以下输出:

Qualitative Evaluation: Translating from Vietnamese to English
Expected translation:
 I like to read books.
 In Vietnamese (from Google Translate):
 Tôi thích đc sách k thut.
 The English translation is:
 I like to read books .

从结果中可以看出,文本已从越南语正确地翻译为英语。

现在,我们将使用我们的 Transformer 模型重复相同的实验。根据之前定义的训练参数,我们在训练中得到了以下演变:

图 6.25 – Transformer 训练演变(训练损失和验证损失、困惑度和 BLEU)

图 6.25 – Transformer 训练演变(训练损失和验证损失、困惑度和 BLEU)

此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 分数(乘以 100)如下:

Best model test Loss=2.1171, test ppl=8.3067, test bleu=24.16

如我们所见,Transformer 架构的 BLEU 得分大约高出~0.015 分。

如同 GNMT 所做的那样,我们也可以通过相同的句子示例和代码检查模型在定性上的表现。输出结果如下:

Qualitative Evaluation: Translating from Vietnamese to English
Expected translation:
 I like to read books.
 In Vietnamese (from Google Translate):
 Tôi thích đc sách k thut.
 The English translation is:
 I like to read books .

从结果中可以看出,文本已从越南语正确地翻译为英语。

工作原理…

在这个教程中,我们解决了自然语言处理(NLP)中最有用的任务之一——机器翻译。我们介绍了一种新的架构——GNMT,它是 Transformer 的前身,并对这两种模型进行了比较。

我们探索了一个新的数据集,IWSLT2015,该数据集支持包括越南语和英语在内的多种语言对之间的翻译。我们引入了广泛用于评估翻译模型的困惑度(Perplexity)和 BLEU 评分标准。

我们进行了实验,比较了这两种模型,发现尽管两种方法都能很好地解决问题(GNMT 和 Transformer 模型在测试集上的 BLEU 分数分别为 23.15 和 24.34),但是 Transformer 表现得更好。

还有更多内容…

机器翻译是一个难以攻克的问题。MXNet GluonNLP 提供了两个非常好的官方指南,在这些指南中,我们解决了这个问题:

本食谱使用了前面提到的代码。我要特别感谢贡献者们。

IWSLT 大会每年都会举行。欲了解更多信息,请访问其官方网站:iwslt.org/

我们介绍了两个新的翻译问题度量标准,Perplexity 和 BLEU。当前正在积极进行改进这些度量标准的工作,最*还开发了新度量标准,如SacreBLEU。以下是一些处理这一重要话题的参考资料:

我们还首次讨论了 GNMT,这是第一个使用深度学习进行翻译(NMT)的实际系统,2016 年由 Google 开发。有关此公告的博客文章值得一读:ai.googleblog.com/2016/09/a-neural-network-for-machine.html

有许多翻译模型使用了IWSLT2015数据集。结果可以在paperswithcode.com/sota/machine-translation-on-iwslt2015-english-1找到。

此外,在本食谱中,我们分析了语言对语言的翻译,这一直是该行业的事实标准方法,使用英语作为多语言翻译的桥梁语言。这是一个活跃的研究领域,最*,Meta(前身为 Facebook)开发了No Language Left BehindNLLB-200)模型。关于这一突破的更多信息,可以在ai.facebook.com/blog/nllb-200-high-quality-machine-translation/找到。

第七章:使用迁移学习与微调优化模型

随着模型规模的增大(每层的深度和处理模块数量),训练它们所需的时间呈指数增长,通常为了达到最佳性能,需要更多的训练轮次(epoch)。

因此,MXNet通过GluonCVGluonNLP库提供了最先进的预训练模型。正如我们在前几章中所见,当我们的最终数据集与所选模型的预训练数据集相似时,这些模型可以帮助我们解决各种问题。

然而,有时候这还不够,最终数据集可能存在一些微妙的差异,预训练模型并未能捕捉到这些差异。在这种情况下,将预训练模型所存储的知识与我们的最终数据集相结合是理想的做法。这就是迁移学习,我们将预训练模型的知识转移到一个新的任务(最终数据集)上。

本章我们将学习如何使用 MXNet Gluon 库中的 GluonCV 和 GluonNLP,分别针对计算机视觉CV)和自然语言处理NLP)。我们还将学习如何从它们的模型库中获取预训练模型,并通过迁移这些预训练模型的学习成果来优化我们自己的网络。

具体来说,我们将在本章中涵盖以下主题:

  • 理解迁移学习与微调

  • 提升图像分类性能

  • 提升图像分割性能

  • 提升从英语翻译到德语的性能

技术要求

除了前言中指定的技术要求外,以下技术要求适用:

  • 确保你已经完成了第一章的食谱,安装 MXNet、Gluon、GluonCV 和 GluonNLP第一章开始使用 MXNet

  • 确保你已经完成了第五章使用计算机视觉分析图像,以及第六章理解自然语言处理中的文本

本章的代码可以在以下 GitHub 链接中找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch07

此外,你可以直接从 Google Colab 访问每个食谱;例如,本章的第一个食谱可以在这里找到:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch07/7_1_Understanding_Transfer_Learning_and_Fine_Tuning.ipynb

理解迁移学习与微调

在之前的章节中,我们看到如何利用 MXNet、GluonCV 和 GluonNLP 来检索特定数据集(如 ImageNet、MS COCO 和 IWSLT2015)中的预训练模型,并将它们应用于我们的特定任务和数据集。

在本教程中,我们将介绍一种称为迁移学习的方法,它允许我们结合预训练模型(在通用知识数据集上)的信息和新领域的信息(来自我们希望解决的任务数据集)。这种方法有两个主要显著优势。一方面,预训练数据集通常是大规模的(ImageNet-22k 有 1400 万张图像),使用预训练模型可以节省训练时间。另一方面,我们不仅用我们的特定数据集进行评估,还用它来训练模型,在所需的场景中提高其性能。正如我们将发现的那样,这并不总是一种容易的方式来实现,因为它需要能够获得一个可观的数据集,或者甚至一种正确的方式,因为它可能不会产生预期的结果。我们还将探讨迁移学习之后的可选下一步,称为微调,我们将尝试使用我们的特定数据集进一步修改模型参数。我们将对这两种技术进行测试。

准备就绪

和之前的章节一样,在本教程中,我们将使用一些矩阵运算和线性代数,但这一点一点也不难。

如何做到…

在本教程中,我们将关注以下步骤:

  1. 引入迁移学习

  2. 描述迁移学习的优势及其使用时机

  3. 理解表示学习的基础知识

  4. 专注于实际应用

让我们深入了解每一个步骤。

引入迁移学习

在之前的章节中,我们学习了如何从头开始训练深度学习神经网络,探索计算机视觉和自然语言处理中的问题。正如在第三章中介绍的那样,解决回归问题,深度学习神经网络试图模仿我们大脑中的生物网络。一个有趣的观点是,当我们(及我们的大脑)学习新任务时,我们以非常强大的方式利用先前获得的知识。例如,一个非常优秀的网球选手会在几个小时的比赛中成为相对优秀的壁球选手。迁移学习是一个研究领域,其中包含不同的技术,以达到类似于这个例子的结果。

图 7.1 – 传统机器学习(ML)与迁移学习之间的比较

图 7.1 – 传统机器学习(ML)与迁移学习之间的比较

图 7.1中,我们可以看到两种范式的对比,其中迁移学习解决任务 2 的方法利用了在解决任务 1 时获得的知识。然而,这意味着要解决单个目标任务(任务 2),我们需要训练两次模型(分别为任务 1 和任务 2)。实际上,正如我们接下来的步骤所示,我们将使用来自 MXNet 的 GluonCV 和 GluonNLP 模型库中的预训练模型,因此我们只需为任务 2 训练一次模型。

描述迁移学习的优势以及何时使用它

使用迁移学习具有多种优势,原因有很多:

  • 更快:通过利用来自模型库的预训练模型,训练过程会比从头开始训练更快收敛,所需的训练轮次和时间都会大大减少。

  • 更通用:通常,预训练模型是使用大规模数据集(如 ImageNet)进行训练的,因此学习到的参数(权重)具有广泛的适用性,能够重用于大量任务。这个目标是通过使用大规模数据集训练得到的、既通用又不依赖特定领域的特征提取部分(也称为表示)来实现的。

  • 需要更少的数据:为了将预训练模型适配到新的任务上,所需的数据量远低于从头开始训练该模型架构的数量。这是因为表示(如前述)可以被重用。

  • 更环保:由于迁移学习所需的训练时间、数据集和计算资源远低于从头开始训练,训练模型所需的污染也大大减少。

  • 性能提升:已有研究证明(例如,www.cv-foundation.org/openaccess/content_cvpr_2014/papers/Oquab_Learning_and_Transferring_2014_CVPR_paper.pdf)迁移学习在小规模数据集上能带来显著的性能提升,在大规模数据集上,迁移学习能比从头训练更快地达到相同的性能水*。

图 7.2中,分析了计算表示的不同方法,尽管专用网络可以达到更好的性能,但这只有在拥有大规模数据集、高端计算资源和更长训练时间的情况下才能实现。

图 7.2 – 比较不同的表示学习方法

图 7.2 – 比较不同的表示学习方法

在更广泛的场景下,迁移学习有多种实现方法,如下图所示:

图 7.3 – 不同类型的迁移学习

图 7.3 – 不同类型的迁移学习

图 7.3中,我们可以看到不同类型的迁移学习,这取决于源领域和目标领域的相似性以及源领域和目标领域数据的可用性。在本章中,我们将探讨在与我们目标任务相似的领域中使用预训练模型的常见设置(源领域和目标领域相同),并且任务会稍有不同,同时在目标领域有一定量的标注数据(归纳 迁移学习)。

百度首席科学家、Google Brain 的联合创始人 Andrew Ng 在 2016 年 NIPS 的一个教程中说:“在未来几年,我们将看到通过迁移学习带来大量具体的价值,”他是对的。

理解表示学习的基本原理

在本节中,我们将从更理论的角度回答如何使用迁移学习以及它为何有效的问题。在第五章使用计算机视觉分析图像,和第六章使用自然语言处理理解文本中,我们介绍了使用 GluonCV 提取图像特征的表示概念,以及使用 GluonNLP 提取文本中单词/句子的表示概念。

我们可以在图 7.4中回顾 CNN 架构的常见结构:

图 7.4 – 卷积神经网络(CNNs)的复习

图 7.4 – 卷积神经网络(CNNs)的复习

图 7.5中,我们可以回顾 Transformer 架构的常见结构:

图 7.5 – Transformer 架构的复习(左侧是编码器,右侧是解码器)

图 7.5 – Transformer 架构的复习(左侧是编码器,右侧是解码器)

这一基本思想在两个领域中都是共通的;例如,CNN 的特征提取部分和 Transformer 中的编码器就是表示,而这些网络部分的训练被称为表示学习,这是一项积极的研究领域,因为它具备在监督和无监督设置下训练这些网络的能力。

迁移学习背后的主要思想是将一个任务中学习到的表示迁移到另一个任务中;因此,我们通常会遵循以下步骤:

  1. 从 MXNet 的模型库中获取预训练模型(GluonCV 或 GluonNLP)。

  2. 移除最后的层(通常是分类器)。将其他层的参数冻结(在训练过程中不可更新)。

  3. 添加新的层(新的分类器),以适应新任务

  4. 使用目标数据训练更新后的模型(只有新添加的层是可更新的,其他冻结的层在训练期间不可更新)。

如果我们有足够的标注数据来解决我们想要解决的任务(目标任务),另一个步骤(可以在前一步之后执行,也可以替代前一步)叫做微调

微调考虑到原始学习的表示可能无法完美适应目标任务,因此,通过更新也能得到改进。在这种情况下,步骤如下:

  1. 解冻表示网络的权重。

  2. 使用目标数据重新训练网络,通常使用较小的学习率,因为表示应该接*(同一领域)。

两个过程(迁移学习和微调)在图 7.6中有直观总结。

图 7.6 – 迁移学习与微调

图 7.6 – 迁移学习与微调

两个过程可以按顺序应用,每个过程都有适当的超参数

重点关注实际应用

在本节中,我们将使用到目前为止所学的表示学习内容,并将其应用于一个实际的示例:检测猫和狗。

为此,我们将从GluonCV 模型库中检索一个模型;我们将去除分类器(最后的几层),保留特征提取阶段。然后,我们将分析猫和狗的表示是如何被学习的。要加载模型,我们可以使用以下代码片段:

alexnet = gcv.model_zoo.get_model("resnet152_v2", pretrained=True, ctx=ctx)

在前面的代码片段中,对于pretrained参数,我们已将其赋值为True,表示我们希望加载预训练权重(而不仅仅是模型的架构)。

当正确训练时,CNN 会学习训练数据集中图像特征的层次化表示,每一层逐渐学习越来越复杂的模式。因此,当图像被处理时(在连续的层中进行处理),网络能够计算与网络相关的更复杂的模式。

现在,我们可以使用一个新的 MXNet 库,MXBoard(请参阅安装说明中的食谱),使用此模型来评估狗图像经过的不同步骤,并查看一些预训练模型计算其表示的示例:

图 7.7 – 猫与狗的表示 – 卷积滤波器

图 7.7 – 猫与狗的表示 – 卷积滤波器

图 7.7中,我们可以看到对应于 ResNet152 预训练网络(在 ImageNet 上)的第一层卷积层的卷积滤波器。请注意这些滤波器如何专注于简单的模式,如特定的形状(垂直和水*线、圆形等)和特定的颜色(红色斑点)。

让我们用一张特定的图像来分析结果:

图 7.8 – 一只狗的示例图像

图 7.8 – 一只狗的示例图像

我们从Dogs vs. Cats数据集中选择一张图像,例如图 7.8中描绘的狗。当将这张图像通过我们的网络时,我们将得到类似以下的结果:

图 7.9 – 卷积滤波器输出

图 7.9 – 卷积滤波器输出

图 7.9中,我们可以看到我们狗的示例在图 7.7中的过滤器输出。注意不同的输出如何突出显示简单的形状,比如眼睛或腿部(较大的值,接*白色)。

最后,随着图像在网络中传播,它的特征会越来越压缩,最终得到(对于 ResNet152)一个包含 2,048 个元素的向量。这个向量可以通过使用 MXNet 的模型库轻松计算:

resnet152.features(summary_image.as_in_context(ctx))

这段代码示例会输出以下结果:

[[2.5350871e-04 2.8519407e-01 1.6196619e-03 ... 7.2884483e-05
  2.9618644e-07 7.8995163e-03]]
 <NDArray 1x2048 @gpu(0)>

如我们所见,我们得到了一个2048元素。

它是如何工作的...

在本篇食谱中,我们介绍了迁移学习和微调的概念。我们解释了何时使用这两种不同技术及其优点。

我们还探讨了这些技术何时有用及其与表征学习的关系,解释了在使用这些技术时,表征在知识转移中的重要作用。我们使用了一个新的库,MXBoard,来生成表征的可视化。

此外,我们直观且实践地展示了如何将这些技术应用于计算机视觉和自然语言处理任务,并为一个具体示例计算了表征。

还有更多...

迁移学习,包括微调,是一个活跃的研究领域。在这个食谱中,我们仅涵盖了深度学习中最有用的场景——归纳迁移学习。想了解更全面但仍易于阅读的介绍,我推荐阅读迁移学习:友好的介绍,可以在以下网址找到:journalofbigdata.springeropen.com/articles/10.1186/s40537-022-00652-w

此外,知识从一个系统转移到另一个系统的概念并不新颖,早在 1995 年就有学习如何学习知识转移等概念的引用,那时曾有一个关于这个主题的神经信息处理系统NeurIPS)研讨会。该研讨会的总结可以在这里找到:http://socrates.acadiau.ca/courses/comp/dsilver/nips95_ltl/nips95.workshop.pdf。

此外,正如 21 年后在同一场合中介绍的,Andrew Ng 正确预见了迁移学习的重要性。他的 2016 年 NeurIPS 教程可以在这里找到(跳转到 1 小时 37 分钟查看迁移学习相关内容):www.youtube.com/watch?v=F1ka6a13S9I

提升图像分类的性能

在上一篇食谱中介绍了迁移学习和微调后,在本篇中,我们将其应用于图像分类,这是一个计算机视觉任务。

在第二个配方中,使用 MXNet 进行图像分类——GluonCV 模型库,AlexNet 和 ResNet,在第五章使用计算机视觉分析图像,我们已经看到如何使用 GluonCV 检索预训练模型,并直接用于图像分类任务。在第一次的实例中,我们展示了如何从零开始训练模型,实际上仅利用预训练模型的架构,而没有利用任何包含在预训练权重中的过去知识,这些权重已被重新初始化,删除了任何历史信息。之后,预训练模型直接用于任务,实际上也利用了模型的权重/参数。

在这个配方中,我们将把模型的权重/参数与目标数据集结合,应用本章介绍的技术——迁移学习和微调。用于预训练的数据集是Dogs vs Cats数据集。

准备工作

和前面的章节一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但一点也不难。

此外,我们还将处理文本数据集;因此,我们将重新审视在第二个配方中已看到的一些概念,使用 MXNet 进行图像分类:GluonCV 模型库,AlexNet 和 ResNet,在第五章使用计算机视觉分析图像

如何操作...

在这个配方中,我们将查看以下步骤:

  1. 重新审视ImageNet-1kDogs vs. Cats数据集

  2. 从头开始训练一个ResNet模型,使用Dogs vs Cats数据集

  3. 使用预训练的 ResNet 模型,通过迁移学习从ImageNet-1kDogs vs Cats优化性能

  4. Dogs vs Cats数据集上的预训练 ResNet 模型进行微调

接下来,我们将详细介绍这些步骤。

重新审视 ImageNet-1k 和 Dogs vs Cats 数据集

ImageNet-1kDogs vs Cats都是图像分类数据集;然而,它们有很大的不同。ImageNet-1k是一个大规模数据集,包含约 120 万张图像,按 1000 个类别进行标签,广泛用于研究和学术界的基准测试。Dogs vs Cats是一个小规模数据集,包含 1400 张描绘狗或猫的图像,其知名度主要来自于 2013 年启动的 Kaggle 竞赛。

MXNet GluonCV 不提供直接下载数据集的方法。然而,我们不需要ImageNet-1k数据集(它的大小约为 133GB),只需要我们选择的模型的预训练参数。预训练模型可以直接从 MXNet GluonCV 模型库下载,我们在前面的章节中见过例子,在本章中也将再次使用它们。

这是一些来自ImageNet-1k的示例:

图 7.10 – ImageNet-1k 示例

图 7.10 – ImageNet-1k 示例

上图的来源是cs.stanford.edu/people/karpathy/cnnembed/

对于猫狗数据集,关于如何获取数据集的所有信息都可以在第二个配方中找到,即使用 MXNet 进行图像分类:GluonCV 模型动物园、AlexNet 和 ResNet,在第五章使用计算机视觉分析图像。根据该配方的代码作为参考,我们可以显示一些示例:

图 7.11 – 猫狗数据集

图 7.11 – 猫狗数据集

图 7**.10图 7**.11中,我们可以看到ImageNet-1k中的一些图像与猫狗数据集中的一些图像相似。

从头开始训练一个 ResNet 模型,使用猫狗数据集

如第二个配方中所述,使用 MXNet 进行图像分类:GluonCV 模型动物园、AlexNet 和 ResNet,在第五章使用计算机视觉分析图像,我们将使用softmax 交叉熵作为损失函数,以及accuracy混淆矩阵作为评估指标。

我们使用 ResNet 模型进行训练的演变如下:

图 7.12 – ResNet 训练演变(训练损失和验证损失,以及验证精度)– 从头开始训练

图 7.12 – ResNet 训练演变(训练损失和验证损失,以及验证精度)– 从头开始训练

此外,在最佳迭代中,测试集中得到的accuracy值如下:

('accuracy', 0.75)

混淆矩阵如下:

图 7.13 – 从头开始训练的 ResNet 模型在猫狗数据集中的混淆矩阵

图 7.13 – 从头开始训练的 ResNet 模型在猫狗数据集中的混淆矩阵

在经过多次训练(例如,100 次)后,获得的准确度值(75%)和图 7**.13显示出相当*均的性能。鼓励您运行自己的实验,尝试不同的超参数设置。

定性上,我们还可以检查我们的模型在一个示例图像中的表现。在我们的案例中,我们选择了以下内容:

图 7.14 – 猫狗定性示例,具体为猫

图 7.14 – 猫狗定性示例,具体为猫

我们可以通过以下代码片段运行这张图像,检查我们模型的输出:

# Qualitative Evaluation
# Qualitative Evaluation
# Expected Output
print("Expected Output:", example_label)
# Model Output
example_output = resnet50_ft(example_image_preprocessed)
 class_output = np.argmax(example_output, axis=1).asnumpy()[0]
 print("Class Output:", class_output)
assert class_output == 0 # Cat 0

这些代码语句将给出以下输出:

Expected Output: 0
Class Output: 0

如结果所示,图像已正确分类为猫。

使用预训练的 ResNet 模型,通过从 ImageNet-1k 到猫狗数据集的迁移学习来优化性能

在前面的配方中,我们使用我们的数据集从头开始训练了一个新模型。然而,这有两个重要的缺点:

  • 从头开始训练需要大量的数据。

  • 由于数据集的大尺寸和模型学习任务所需的周期数,训练过程可能需要很长时间。

因此,在这个食谱中,我们将采取不同的方法:我们将使用来自 MXNet GluonCV 的预训练模型来解决任务。这些模型已经在 ImageNet-1k 数据集上训练过,该数据集包含我们感兴趣的类别(猫和狗);因此,我们可以利用这些学到的特征,并轻松将其迁移到 Dogs vs Cats(相同领域)。

对于 ResNet 模型,使用以下代码:

# ResNet50 from Model Zoo (This downloads v1d)
 resnet50 = gcv.model_zoo.get_model("resnet50_v1d", pretrained=True, ctx=ctx)

如我们在前面的代码段中看到的,按照本章第一个食谱《理解迁移学习和微调》中的讨论,对于 pretrained 参数,我们已将其值设置为 True,表示我们希望获取预训练权重(而不仅仅是模型的架构)。

为了充分评估迁移学习带来的改进,我们将在应用迁移学习到 Dogs vs Cats 数据集之前和之后,直接评估我们的预训练模型(源任务是 ImageNet-1k)。因此,使用我们当前的预训练模型,我们得到如下结果:

('accuracy', 0.925)

混淆矩阵如下:

图 7.15 – 在预训练 ResNet 模型下,Dogs vs Cats 数据集的混淆矩阵

图 7.15 – 在预训练 ResNet 模型下,Dogs vs Cats 数据集的混淆矩阵

如我们所见,我们的预训练 Transformer 模型在同一领域下已经显示出良好的表现;然而,单纯使用预训练模型并不能比从零开始训练获得更好的表现。使用预训练模型的巨大优势在于节省时间,因为加载它只需要几行代码。

我们还可以通过相同的图像示例检查模型的定性表现。注意代码与之前的定性图像摘录有所不同,因为现在我们需要将 ImageNet 类别(即我们的预训练 ResNet50 模型的输出)转换为我们的类别(0 代表猫,1 代表狗)。新的代码如下所示:

# Qualitative Evaluation
# Expected Output
print("Expected Output:", example_label)
# Model Output
example_output = resnet50(example_image_preprocessed)
class_output = model.CLASSES_DICT[np.argmax(example_output, axis=1).asnumpy()[0]]
print("Class Output:", class_output)
assert class_output == 0 # Cat

这些代码语句将会给我们以下输出:

Expected Output: 0
Class Output: 0

从结果中可以看出,该图像已被正确分类为猫。

现在我们有了一个基准进行比较,让我们将迁移学习应用到我们的任务中。从第一个食谱《理解迁移学习和微调》中,第一步是从 MXNet 模型库(GluonCV 或 GluonNLP)中获取一个预训练的模型,这一步我们已经完成。

第二步是移除最后一层(通常是分类器),保持其他层的参数被冻结(在训练过程中不可更新),所以让我们开始吧!

我们可以用以下代码段替换分类器:

# Replace the classifier (with gradients activated)
 resnet50_tl.fc = mx.gluon.nn.Dense(2)
 resnet50_tl.fc.initialize(ctx=ctx)

我们可以通过以下代码段来冻结 ResNet 特征提取层:

for param in resnet50_tl.collect_params().values():
param.grad_req = 'null'

我们可以用以下代码段替换分类器:

# Replace the classifier (with gradients activated)
 resnet50_tl.fc = mx.gluon.nn.Dense(2)
 resnet50_tl.fc.initialize(ctx=ctx)

现在,我们可以应用通常的训练过程来处理 Dogs vs Cats 数据集,并且我们在使用 ResNet 模型的训练中有了以下进展:

图 7.16 – ResNet 训练演变(训练损失和验证损失)– 迁移学习

图 7.16 – ResNet 训练演变(训练损失和验证损失)– 迁移学习

此外,对于最佳迭代,测试集上的准确率如下:

('accuracy', 0.985)

混淆矩阵如下:

图 7.17 – 使用迁移学习的 ResNet 模型在狗与猫数据集上的混淆矩阵

图 7.17 – 使用迁移学习的 ResNet 模型在狗与猫数据集上的混淆矩阵

与我们之前从头开始训练的实验相比,这个实验的表现大大提高,并且我们只用了几分钟就让这个模型开始在我们期望的任务中表现良好,而之前的实验需要几个小时,并且需要多次调整超参数,这可能会转化为几天的工作。

我们还可以使用相同的图像示例和代码来定性地检查我们的模型表现。输出结果如下:

Expected Output: 0
Class Output: 0

从结果可以看出,图像已经被正确分类为猫。

在狗与猫数据集上微调我们的预训练 ResNet 模型

在之前的步骤中,我们冻结了编码器层中的参数。然而,由于我们当前使用的数据集(狗与猫)样本足够多,我们可以解冻这些参数并训练模型,从而有效地让新的训练过程更新表示(在迁移学习中,我们直接使用了为ImageNet-1k学到的表示)。这个过程叫做微调。

微调有两种变体:

  • 通过冻结层并在之后解冻(迁移学习后的微调)来应用迁移学习

  • 直接应用微调,而没有冻结层的预处理步骤(直接微调)

让我们计算这两个实验并通过比较结果得出结论。

对于第一个实验,我们可以取之前步骤中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,我们可以运行以下代码片段:

# Un-freeze weights
for param in resnet50_ft.collect_params().values():
    if param.name in updated_params:
        param.grad_req = 'write'

现在,我们可以应用常规的训练过程与狗与猫数据集,并且我们在使用 ResNet 模型进行训练时,得到了以下演变:

图 7.18 – ResNet 训练演变(训练损失和验证损失)– 迁移学习后的微调

图 7.18 – ResNet 训练演变(训练损失和验证损失)– 迁移学习后的微调

此外,对于最佳迭代,测试集上的准确率如下:

('accuracy', 0.90255)

混淆矩阵如下:

图 7.19 – 使用迁移学习后微调的 ResNet 模型在狗与猫数据集上的混淆矩阵

图 7.19 – 使用迁移学习后微调的 ResNet 模型在狗与猫数据集上的混淆矩阵

与我们之前的迁移学习实验相比,这次实验的表现更差。这是由于数据集的大小和选择的超参数组合。鼓励你尝试自己的实验。

我们还可以使用相同的图像示例和代码,定性地检查我们的模型表现如何。输出如下所示:

 Expected Output: 0
Class Output: 0

从结果可以看出,图像已被正确分类为猫。

现在让我们继续进行第二次微调实验,这次我们不采用迁移学习,而是直接对整个模型进行微调(没有冻结层)。

我们需要再次获取为ImageNet-1k预训练的 ResNet 模型,MXNet GluonCV 的代码片段如下:

# ResNet50 from Model Zoo (This downloads v1d)
 resnet50 = gcv.model_zoo.get_model("resnet50_v1d", pretrained=True, ctx=ctx)

现在,我们可以在没有冻结层的情况下应用训练过程,这将更新我们 ResNet 模型的所有层,得到如下的损失曲线:

图 7.20 – ResNet 训练过程(训练损失与验证损失)– 微调且未冻结层

图 7.20 – ResNet 训练过程(训练损失与验证损失)– 微调且未冻结层

此外,对于最佳迭代,在测试集上获得的准确率如下:

('accuracy', 0.98)

这个值与之前迁移学习实验的结果相似。对于混淆矩阵,结果如下:

图 7.21 – ResNet 模型在狗与猫分类中的混淆矩阵,采用微调且未冻结层

图 7.21 – ResNet 模型在狗与猫分类中的混淆矩阵,采用微调且未冻结层

如前所述,与我们之前的微调实验相比,我们可以看到这次实验的性能更高。根据经验,这已被证明是一个可重复的结果,且被认为是因为最初冻结编码器允许解码器(使用编码器表示)学习当前的新任务。从信息流的角度来看,在这一步骤中,知识从特征提取阶段转移到了分类器。在第二步中,当特征提取阶段解冻时,分类器中学到的参数执行辅助的迁移学习——这次是从分类器到特征提取阶段。

我们还可以使用相同的图像示例和代码,定性地检查我们的模型表现如何。输出如下所示:

Expected Output: 0
Class Output: 0

从结果可以看出,图像已被正确分类为猫。

它的工作原理是…

在这个配方中,我们将第一个章节开头介绍的迁移学习和微调技术应用于图像分类任务,这个任务在第二个配方中也有呈现,名为使用 MXNet 分类图像:GluonCV 模型库、AlexNet 和 ResNet第五章计算机视觉下的图像分析

我们重新访问了两个已知的数据集,ImageNet-1kDogs vs Cats,并打算结合这两个数据集,基于前者数据集的知识转移,并用后者对该知识进行微调。此外,这是通过利用 MXNet GluonCV 提供的工具实现的:

  • ImageNet-1k 提供的预训练 ResNet 模型

  • 便于访问的工具,用于 与猫 数据集

此外,我们继续使用用于图像分类的损失函数和指标,包括 softmax 交叉熵、准确率和混淆矩阵。

有了 MXNet 和 GluonCV 中这些随时可用的工具,我们只需几行代码就能运行以下实验:

  • 从零开始训练 Dogs vs Cats 中的模型

  • 使用预训练模型,通过从 ImageNet-1kDogs vs Cats 的转移学习优化性能

  • Dogs vs Cats 数据集上微调我们的预训练模型(包括和不包括冻结层)

在进行不同实验后,我们直接获得了转移学习与微调之间的有效联系(准确率分别为 0.985 和 0.98)。实际运行这些实验时,结果可能会根据模型架构、数据集和选择的超参数有所不同,因此建议大家尝试不同的技术和变种。

还有更多…

转移学习,包括微调,是一个活跃的研究领域。一篇 2022 年发布的论文探讨了图像分类领域的最新进展。该论文标题为《深度转移学习用于图像分类:一项综述》,可以在此查阅:www.researchgate.net/publication/360782436_Deep_transfer_learning_for_image_classification_a_survey

关于计算机视觉应用案例的更一般方法,最*发布了一篇论文《转移学习方法作为解决小数据集计算机视觉任务的新方法》,该论文评估了小数据集的问题,并应用这些技术解决医学影像任务。可以在此查阅:www.researchgate.net/publication/344943295_Transfer_Learning_Methods_as_a_New_Approach_in_Computer_Vision_Tasks_with_Small_Datasets

提高图像分割性能

在本配方中,我们将应用转移学习和微调来进行语义分割,这是一个计算机视觉任务。

在第四个配方中,使用 MXNet 进行图像对象分割:PSPNet 与 DeepLab-v3,在第五章 使用计算机视觉分析图像 中,我们展示了如何使用 GluonCV 直接获取预训练模型并将其用于语义分割任务,通过使用预训练模型的架构和权重/参数,充分利用过去的知识。

在本教程中,我们将继续利用模型的权重/参数,用于一个任务,该任务包括使用语义分割模型在一组 21 类图像中对图像进行分类。用于预训练的数据集是MS COCO(源任务),我们将运行多个实验来评估我们的模型在一个新的(目标)任务中的表现,使用Penn-Fudan Pedestrian数据集。在这些实验中,我们还将包含来自目标数据集的知识,以提高我们的语义分类性能。

准备工作

至于之前的章节,在本教程中,我们将使用一些矩阵操作和线性代数,但这一点也不难。

此外,我们将使用文本数据集;因此,我们将重新讨论第四篇教程中已经见过的一些概念,MXNet 中图像分割:PSPNet 和 DeepLab-v3,在第五章计算机视觉中的图像分析

如何做…

在本教程中,我们将看到以下步骤:

  1. 重新审视MS COCOPenn-Fudan Pedestrian数据集

  2. 使用Penn-Fudan Pedestrian从头开始训练DeepLab-v3模型

  3. 使用预训练的 DeepLab-v3 模型通过从MS COCOPenn-Fudan Pedestrian的迁移学习来优化性能。

  4. Penn-Fudan Pedestrian上微调我们预训练的 DeepLab-v3 模型

让我们详细看一下接下来的步骤。

重新审视 MS COCO 和 Penn-Fudan Pedestrian 数据集

MS COCOPenn-Fudan Pedestrian都是目标检测和语义分割数据集;然而,它们有很大的不同。MS COCO是一个大规模数据集,包含约 150,000 张图像,标记为 80 类(21 个主要类),并且在研究和学术界广泛应用于基准测试。Penn-Fudan Pedestrian是一个小规模数据集,包含 170 张图像和 423 名行人。在本教程中,我们将专注于语义分割任务。

MXNet GluonCV 不提供直接下载任何数据集的方法。然而,我们不需要MS COCO数据集(其大小约为 19 GB),只需要选择模型的预训练参数。

这里有一些MS COCO的例子:

图 7.22 – MS COCO 示例

图 7.22 – MS COCO 示例

对于Penn-Fudan Pedestrian,关于如何获取数据集的所有信息可以在第四篇教程中找到,MXNet 中图像分割:PSPNet 和 DeepLab-v3,在第五章计算机视觉中的图像分析。参考该篇教程的代码,我们可以展示一些例子:

图 7.23 – Penn-Fudan Pedestrian 数据集示例

图 7.23 – Penn-Fudan Pedestrian 数据集示例

图 7.22图 7.23,我们可以看到一些MS COCO图像与Penn-Fudan Pedestrian的图像相似。

使用 Penn-Fudan Pedestrian 从头开始训练 DeepLab-v3 模型

正如在第四个示例中所述,使用 MXNet 对图像进行分割:PSPNet 和 DeepLab-v3,在 第五章使用计算机视觉分析图像 中,我们将使用 softmax 交叉熵作为损失函数,并使用像素准确率和 *均交并比mIoU)作为评估指标。

按照我们示例中的代码,我们得到了以下从头开始训练 DeepLab-v3 模型时的演变:

图 7.24 – DeepLab-v3 训练演变(训练损失和验证损失)– 从头开始训练

图 7.24 – DeepLab-v3 训练演变(训练损失和验证损失)– 从头开始训练

此外,对于最佳迭代,测试集中的像素准确率和 mIoU 值如下:

PixAcc:  0.8454046875
mIoU  :  0.6548404063890942

即使训练了 40 个周期,评估结果仍未显示出强劲的性能(mIoU 值仅为 0.65)。

定性地,我们还可以通过一个示例图像来检查模型的表现。在我们的案例中,我们选择了以下图像:

图 7.25 – Penn-Fudan 行人数据集的图像示例,用于定性结果

图 7.25 – Penn-Fudan 行人数据集的图像示例,用于定性结果

我们可以通过以下代码片段将此图像传入模型来检查模型的输出:

# Compute and plot prediction
transformed_image = gcv.data.transforms.presets.segmentation.test_transform(test_image, ctx)
 output = deeplab_ts(transformed_image)
 filtered_output = mx.nd.argmax(output[0], 1)
 masked_output = gcv.utils.viz.plot_mask(test_image, filtered_output)
 axes = fig.add_subplot(1, 2, 2)
 axes.set_title("Prediction", fontsize=16, y=-0.3)
 axes.axis('off')
 axes.imshow(masked_output);

上述代码片段展示了真实标签的分割结果和我们模型的预测结果:

图 7.26 – 从头开始训练的 DeepLab-v3 的真实标签与预测

图 7.26 – 从头开始训练的 DeepLab-v3 的真实标签与预测

从结果可以看出,行人只有在较晚阶段才开始被正确分割。为了改善结果,我们需要训练更多的周期和/或调整超参数。然而,一个更好、更快、更简单的方法是使用迁移学习并进行微调。

使用预训练的 DeepLab-v3 模型,通过从 MS COCO 到 Penn-Fudan 行人的迁移学习优化性能

在前一个示例中,我们使用自己的数据集从头开始训练了一个新模型。然而,这种方法有三个重要的缺点:

  • 从头开始训练需要大量的数据。

  • 由于数据集的庞大和模型需要的训练周期数,训练过程可能会耗时很长。

  • 所需的计算资源可能会非常昂贵或难以获取。

因此,在本示例中,我们将采用不同的方法。我们将使用来自 MXNet GluonCV 模型库的预训练模型来解决任务。这些模型已经在 MS COCO 数据集上训练过,包含我们感兴趣的类别(在本案例中是 person);因此,我们可以使用这些学到的表示,并轻松地将它们迁移到 Penn-Fudan 行人(同一领域)。

对于 DeepLab-v3 模型,我们有以下内容:

# DeepLab-v3 from Model Zoo
deeplab_pt
gcv.model_zoo.get_model('deeplab_resnet101_coco'
pretrained=True, ctx=ctx)

正如我们在前面的代码片段中看到的,按照本章第一个食谱中讨论的内容,理解迁移学习和微调,对于 pretrained 参数,我们已将其值设置为 True,表示我们希望获取预训练的权重(而不仅仅是模型的架构)。

为了充分评估迁移学习带来的改进,我们将直接在我们的目标任务中评估预训练模型(任务源自MS COCO),然后再将迁移学习应用到Penn-Fudan Pedestrian,并对比应用前后的结果。因此,使用我们当前的预训练模型,我们获得了以下结果:

PixAcc:  0.9640322916666667
mIoU  :  0.476540873665686

如我们所见,我们的预训练 Transformer 模型在相同领域中已经显示出良好的性能值。此外,使用预训练模型的巨大优势是节省时间,因为加载预训练模型只需要几行代码。

我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:

图 7.27 – DeepLab-v3 预训练模型的真实值与预测值

图 7.27 – DeepLab-v3 预训练模型的真实值与预测值

从结果中可以看出,行人已被正确地分割。请注意,使用预训练模型的一个附加优点见于图 7.27:在真实值图像中,背景中的人没有被分割,但预训练模型正确地识别了它们(这可能解释了较低的 mIoU 值)。

现在我们有了一个比较基准,让我们将迁移学习应用于我们的任务。在第一个食谱中,理解迁移学习和微调,第一步是从 MXNet Model Zoo(GluonCV 或 GluonNLP)中获取预训练模型,而这一部分我们已经完成。

第二步是移除最后的几层(通常是分类器),并保持其余层的参数冻结(在训练过程中不可更新),所以让我们开始吧!

我们可以通过以下代码片段冻结DeepLab-v3的特征提取层:

for param in deeplab_tl.collect_params().values():
param.grad_req = 'null'

此外,我们还需要替换分割任务头。以前,它支持来自MS COCO的 21 类。对于我们的实验,两个类别就足够了,backgroundperson。这可以通过以下代码片段完成:

# Replace the last layers
deeplab_tl.head = gcv.model_zoo.deeplabv3._DeepLabHead(2)
deeplab_tl.head.initialize(ctx=ctx)
deeplab_tl.head.collect_params().setattr('lr_mult', 10)

现在,我们可以使用Penn-Fudan Pedestrian应用常规的训练过程,并且我们使用DeepLab-v3模型时,训练演化如下:

图 7.28 – DeepLab-v3 训练演化(训练损失和验证损失)– 迁移学习

图 7.28 – DeepLab-v3 训练演化(训练损失和验证损失)– 迁移学习

此外,对于最佳迭代,测试集中的评估指标如下所示:

PixAcc:  0.9503427083333333
mIoU  :  0.8799470898171042

与我们之前从头开始训练和预训练的实验相比,这个实验的表现略好,而且我们花费了几分钟就让这个模型开始在我们预定的任务上工作,而从头开始训练的实验则花费了数小时,并且需要多次尝试调整超参数,最终耗费了几天的时间。

我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:

图 7.29 – 经过转移学习的 DeepLab-v3 预训练模型的真实标签与预测结果

图 7.29 – 经过转移学习的 DeepLab-v3 预训练模型的真实标签与预测结果

从结果可以看出,行人已经被正确地分割。

在 Penn-Fudan 行人数据集上对我们预训练的 DeepLab-v3 模型进行微调

在之前的配方中,我们冻结了编码器层中的参数。然而,在我们当前使用的数据集(Penn-Fudan 行人数据集)中,我们可以解冻这些参数并训练模型,从而有效地使新的训练过程更新表示(在转移学习中,我们直接使用了为 MS COCO 学习到的表示)。如本章所介绍的,这个过程被称为微调。

微调有两种变体:

  • 通过冻结层并随后解冻层来应用转移学习。

  • 直接应用微调,而不进行冻结层的预备步骤。

让我们计算两个实验并通过比较结果得出结论。

对于第一个实验,我们可以使用之前配方中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,可以运行以下代码片段:

for param in deeplab_ft.collect_params().values():
param.grad_req = 'write'

现在,我们可以应用常规的训练过程来训练 Penn-Fudan 行人数据集,并且在使用 DeepLab-v3 模型时,我们有以下训练演变:

图 7.30 – DeepLab-v3 训练过程演变(训练损失和验证损失)– 转移学习后的微调

图 7.30 – DeepLab-v3 训练过程演变(训练损失和验证损失)– 转移学习后的微调

此外,对于最佳迭代,在测试集上获得的评估指标如下:

PixAcc:  0.9637550347222222
mIoU  :  0.9091450223893902

与我们之前在转移学习中的实验相比,这个实验在 mIoU 上的表现提高了约 3%,考虑到投入的训练时间较少,这是一个非常好的提升。

我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:

图 7.31 – 经过微调后的 DeepLab-v3 预训练模型的真实标签与预测结果

图 7.31 – 经过微调后的 DeepLab-v3 预训练模型的真实标签与预测结果

从结果可以看出,行人已经被正确地分割。

现在让我们继续进行第二次微调实验,在此实验中,我们不应用迁移学习(没有冻结层),而是直接对整个模型应用微调。

我们需要检索用于MS COCO的预训练DeepLab-v3模型,以下是 MXNet GluonCV 的代码片段:

# DeepLab-v3 from Model Zoo
 deeplab_ft_direct = gcv.model_zoo.get_model("deeplab_resnet101_coco", pretrained=True, ctx=ctx)

现在,我们可以在不冻结的情况下应用训练过程,这将更新我们DeepLab-v3模型的所有层:

图 7.32 – DeepLab-v3 训练演变(训练损失和验证损失) – 未冻结的微调

图 7.32 – DeepLab-v3 训练演变(训练损失和验证损失) – 未冻结的微调

此外,对于最佳迭代,在测试集上获得的评估指标如下:

PixAcc:  0.9639182291666667
mIoU  :  0.9095065032946663

与我们之前的微调实验相比,我们可以看到这些实验表现出非常相似的性能。根据经验,已经证明这个微调实验可能会略微降低结果,因为最初冻结编码器使解码器能够使用编码器表示学习当前任务。从某种角度看,在这一步,知识从编码器传递到解码器。在第二步中,当编码器被解冻时,解码器中学习到的参数执行辅助迁移学习,这次是从解码器到编码器。

我们还可以通过相同的图像示例和代码来检查我们的模型在定性上的表现。输出如下:

图 7.33 – DeepLab-v3 预训练模型的真实标签与预测(未冻结的微调)

图 7.33 – DeepLab-v3 预训练模型的真实标签与预测(未冻结的微调)

从结果可以看出,行人已经被正确分割,尽管如前所述,如果我们看右侧的那个人,靠*左侧那个人的手臂可能会被更好地分割。如前所讨论,有时这种微调版本的结果可能会略低于其他方法。

它是如何工作的……

在这个方案中,我们将本章开头介绍的迁移学习和微调技术应用于图像分类任务,这在之前的第四个方案中也有呈现,使用 MXNet 进行图像对象分割:PSPNet 和 DeepLab-v3,位于第五章使用计算机视觉分析图像

我们重新访问了两个已知的数据集,MS COCOPenn-Fudan Pedestrian,我们打算通过基于前者数据集的知识迁移,并利用后者对该知识进行优化来将它们结合起来。此外,MXNet GluonCV 提供了以下内容:

  • 用于MS COCO的预训练DeepLab-v3模型

  • 用于便捷访问Penn-Fudan Pedestrian的工具

此外,我们继续使用为语义分割引入的损失函数和指标,如 softmax 交叉熵、像素准确率和 mIoU。

在 MXNet 和 GluonCV 中,所有这些工具都已经预先准备好,这使得我们只需几行代码就能进行以下实验:

  • 使用Penn-Fudan Pedestrian从零开始训练模型

  • 使用预训练模型通过迁移学习从MS COCOPenn-Fudan Pedestrian优化性能

  • Penn-Fudan Pedestrian上微调我们的预训练模型(包括冻结和不冻结层的情况)

经过不同实验的运行,并结合定性结果和定量结果,迁移学习(像素准确率为 0.95,mIoU 为 0.88)已经成为我们任务中最佳的实验方法。实际运行这些实验时获得的结果可能会因为模型架构、数据集和所选择的超参数而有所不同,因此我们鼓励你尝试不同的技术和变化。

还有更多……

迁移学习,包括微调,是一个活跃的研究领域。2022 年发布的一篇论文探讨了图像分类的最新进展。该论文题为Deep Transfer Learning for Image Classification: A survey,可以在这里找到:www.researchgate.net/publication/360782436_Deep_transfer_learning_for_image_classification_a_survey

一篇有趣的论文将迁移学习和语义分割结合起来,题为Semantic Segmentation with Transfer Learning for Off-Road Autonomous Driving,在这篇论文中,也通过使用合成数据研究了领域的变化。可以在这里找到:www.researchgate.net/publication/333647772_Semantic_Segmentation_with_Transfer_Learning_for_Off-Road_Autonomous_Driving

这篇论文提供了更一般的概述:Learning Transferable Knowledge for Semantic Segmentation with Deep Convolutional Neural Network,该论文被计算机视觉与模式识别CVPR)大会于 2016 年接收。可以在这里找到:openaccess.thecvf.com/content_cvpr_2016/papers/Hong_Learning_Transferrable_Knowledge_CVPR_2016_paper.pdf

改进英德翻译的性能

在之前的示例中,我们已经看到如何利用预训练模型和新数据集进行迁移学习和微调,应用于计算机视觉任务。在本示例中,我们将采用类似的方法,但针对一个自然语言处理任务,即从英语翻译成德语。

在第四个食谱中,从越南语翻译到英语,来自第六章理解文本与自然语言处理,我们看到如何使用 GluonNLP 直接检索预训练的模型,并将它们用于翻译任务,从头开始训练,实际上只是通过使用预训练模型的架构来有效地利用过去的知识。

在本食谱中,我们还将利用模型的权重/参数,这些权重/参数是通过机器翻译模型用于将英文文本翻译成德文的任务获得的。我们将使用WMT2014数据集(任务来源)进行预训练,并将进行多个实验,使用WMT2016数据集(增加了约 20%的德英对词汇和句子)评估我们在新目标任务中的模型。

准备工作

和之前的章节一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但这并不难。

此外,我们将处理文本数据集;因此,我们将重新审视在第四个食谱中已经看到的一些概念,理解文本数据集——加载、管理和可视化 Enron 邮件数据集,来自第二章使用 MXNet 和可视化数据集:Gluon 和 DataLoader

如何操作...

在本食谱中,我们将查看以下步骤:

  1. 介绍WMT2014WMT2016数据集

  2. 从头开始训练一个 Transformer 模型,使用WMT2016数据集

  3. 使用预训练的 Transformer 模型,通过迁移学习将性能从WMT2014优化到WMT2016

  4. WMT2016上微调我们预训练的 Transformer 模型

接下来我们将详细查看这些步骤。

介绍 WMT2014 和 WMT2016 数据集

WMT2014WMT2016是多模态(多语言)翻译数据集,包括中文、英语和德语语料库。WMT2014首次在 2014 年《第九届统计机器翻译研讨会论文集》上介绍,作为翻译模型评估活动的一部分。该研讨会在 2016 年升级为自己的会议,并在《第一次机器翻译会议论文集》中介绍了WMT2016,作为翻译模型评估活动的一部分。这两个数据集非常相似,都是从新闻来源中提取信息,最大的区别在于语料库(两个数据集所用词汇量的大小)。WMT2014 大约包含~14 万个不同的词,而 WMT2016 略大,包含~15 万个词,特别是在德英对中,增加了约 20%的词汇和句子。

MXNet GluonNLP 提供了这些数据集的现成版本。在我们的案例中,我们将使用WMT2016,它仅包含traintest划分。我们将进一步拆分测试集,获得validationtest划分。以下是加载数据集的代码:

# WMT2016 Dataset (Train, Validation and Test)
# Dataset Parameters
src_lang, tgt_lang = "en", "de"
src_max_len, tgt_max_len = 50, 50
 wmt2016_train_data = nlp.data.WMT2016BPE(
    'train',
 src_lang=src_lang,
    tgt_lang=tgt_lang)
wmt2016_val_data = nlp.data.WMT2016BPE(
    'newstest2016',
    src_lang=src_lang,
    tgt_lang=tgt_lang)
wmt2016_test_data = nlp.data.WMT2016BPE(
    'newstest2016',
    src_lang=src_lang,
    tgt_lang=tgt_lang)

以下是生成validationtest划分的代码:

 # Split Val / Test sets
val_length = 1500
test_length = len(wmt2016_test_text) - val_length
wmt2016_val_data._data[0] = wmt2016_val_data._data[0][:val_length]
 wmt2016_val_data._data[1] = wmt2016_val_data._data[1][:val_length]
 wmt2016_val_data._length = val_length
wmt2016_val_text._data[0] = wmt2016_val_text._data[0][:val_length]
 wmt2016_val_text._data[1] = wmt2016_val_text._data[1][:val_length]
 wmt2016_val_text._length = val_length
wmt2016_test_data._data[0] = wmt2016_test_data._data[0][-test_length:]
 wmt2016_test_data._data[1] = wmt2016_test_data._data[1][-test_length:]
 wmt2016_test_data._length = test_length

在分割后,我们的WMT2016数据集提供以下数据:

 Length of train set: 4500966
Length of val set  : 1500
Length of test set : 1499

从每个数据集上大量的实例中,我们可以确认这些数据集适合用于我们的实验。

在 WMT2016 中从零开始训练一个 Transformer 模型

如第四章所述,从越南语翻译到英语,在第六章使用自然语言处理理解文本中,我们将使用困惑度进行每批次计算,并使用BLEU进行每轮计算,这些将展示我们的训练过程演变,作为典型的训练和验证损失的一部分。我们还将它们用于定量评估,对于定性评估,我们将选择一个句子(也可以使用任何你想出的其他句子)。

我们在使用 Transformer 模型进行训练时有以下演变:

图 7.34 – Transformer 训练演变(训练损失)– 从零开始训练

图 7.34 – Transformer 训练演变(训练损失)– 从零开始训练

此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 得分(乘以 100)如下:

 WMT16 test loss: 3.01; test bleu score: 14.50

当前最先进的技术SOTA)模型的 BLEU 得分可以超过 30 分;我们在 10 个 epoch 后大约达到一半,在大约 30 个 epoch 后达到 SOTA 性能。

从质量上讲,我们也可以通过一个句子示例检查我们的模型表现如何。在我们的案例中,我们选择了:"I learn new things every day",可以通过以下代码进行验证:

print("Qualitative Evaluation:  Translating from English to German")
# From Google Translate
expected_tgt_seq = " Ich lerne jeden Tag neue Dinge."
 print("Expected translation:")
 print(expected_tgt_seq)
src_seq = "I learn new things every day."
 print("In English:")
 print(src_seq)
translation_out = nmt.utils.translate(
     transformer_ts_translator,
    src_seq,
     wmt_src_vocab,
    wmt_tgt_vocab,
    ctx)
print("The German translation is:")
 print(" ".join(translation_out[0]))

这些代码语句将给我们以下输出:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne jeden Tag neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich halte es für so , dass es hier so ist.

这句德语意味着我认为这里是这种情况;因此,从这个结果可以看出,文本没有正确地从英语翻译成德语,我们需要投入更多的时间来训练,才能获得正确的结果。

使用预训练的 Transformer 模型,通过将 WMT2014 转移到 WMT2016 进行优化,从而提升性能。

在前一章中,我们使用自己的数据集从零开始训练了一个新模型。然而,这有两个重要的缺点:

  • 从零开始训练需要大量的数据。

  • 由于数据集的庞大规模和模型需要的训练轮数,训练过程可能会非常漫长。

因此,在本章中,我们将采用不同的方法。我们将使用 MXNet GluonNLP 中的预训练模型来解决该任务。这些模型已经在与WMT2014非常相似的数据集上进行训练,因此为该任务学习到的表示可以很容易地迁移到WMT2016(同一领域)。

对于一个 Transformer 模型,我们有如下内容:

 wmt_model_name = 'transformer_en_de_512'
wmt_transformer_model_pt, wmt_src_vocab, wmt_tgt_vocab = nlp.model.get_model(
    wmt_model_name,
    dataset_name='WMT2014',
    pretrained=True,
    ctx=ctx)
print('Source Vocab:', len(wmt_src_vocab), ', Target Vocab:', len(wmt_tgt_vocab))

输出展示了WMT2014数据集的词汇表大小(预训练的英德翻译任务):

Source Vocab: 36794 , Target Vocab: 36794

这是WMT2014数据集的一个子集。正如我们在前面的代码片段中所看到的,按照本章第一个配方理解迁移学习和微调中的讨论,对于pretrained参数,我们已将其值设置为True,表示我们希望检索预训练的权重(而不仅仅是模型的架构)。

为了充分评估迁移学习带来的改进,我们将直接评估我们的预训练模型(任务源是WMT2014)在应用迁移学习到WMT2016之前和之后的表现。因此,直接使用我们的预训练模型,我们得到以下结果:

 WMT16 test loss: 1.59; test bleu score: 29.76

如我们所见,我们的预训练 Transformer 模型已经显示出非常好的性能,因为它属于相同的领域;然而,单纯使用预训练模型并不能达到 SOTA(最先进的)性能,这只能通过从零开始训练来实现。使用预训练模型的巨大优势在于节省时间和计算资源,因为加载一个预训练模型只需要几行代码。

我们还可以通过相同的句子示例和代码检查模型的定性表现。输出结果如下:

 Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne jeden Tag neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne neue Dinge, die in jedem Fall auftreten.

这句德语的意思是我学习在每个案例中出现的新事物;因此,从结果中可以看出,文本尚未正确地从英语翻译成德语,但这一次,比我们之前的实验更接*了。

现在我们有了一个比较的基准,让我们将迁移学习应用到我们的任务中。在第一个配方中,理解迁移学习和微调,第一步是从 MXNet 模型库(GluonCV 或 GluonNLP)中检索一个预训练的模型,这一步我们已经完成了。

第二步是去掉最后一层(通常是分类器),保持其余层的参数冻结(在训练过程中不可更新),让我们来做吧!

我们可以用以下代码冻结除分类器之外的所有参数,保持这些参数被冻结(我们将在后续实验中解冻它们):

 updated_params = []
for param
wmt_transformer_model_tl.collect_params().values():
    if param.grad_req == "write":
        param.grad_req = "null"
        updated_params += [param.name]

现在,我们可以使用WMT2016应用常规的训练过程,接着我们可以看到在使用 Transformer 模型训练时的演变:

图 7.35 – Transformer 训练演变(训练损失)– 迁移学习

图 7.35 – Transformer 训练演变(训练损失)– 迁移学习

此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 得分(乘以 100)如下:

 WMT16 test loss: 1.20; test bleu score: 27.78

与我们之前的实验相比,这次实验的数值表现略低;然而,我们花了几分钟就让这个模型开始在我们预定的任务中发挥作用,而之前的实验从头开始训练则花了几个小时,并且需要多次尝试调整超参数,总共花费了几天的时间。

我们还可以通过相同的句子示例和代码检查模型的定性表现。输出如下:

 Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne jeden Tag neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich erlerne jedes Mal neue Dinge

这句德语句子意味着我每次都学到新东西;因此,从结果可以看出,文本几乎已正确地从英语翻译成德语,相较于我们之前的实验(预训练模型),有所改进,尽管(更好的)定量结果显示了不同的趋势。

在 WMT2016 上微调我们预训练的 Transformer 模型

在之前的方案中,我们冻结了所有参数,除了分类器。然而,由于我们目前使用的数据集(WMT2016)有足够的数据样本,我们可以解冻这些参数并训练模型,有效地让新的训练过程更新表示(通过迁移学习,我们直接使用了为 WMT2014 学到的表示)。这个过程,正如我们所知,叫做微调。

微调有两种变体:

  • 通过冻结层并在之后解冻它们来应用迁移学习。

  • 直接应用微调,而不需要冻结层的预备步骤。

让我们计算两个实验,并通过比较结果得出结论。

对于第一个实验,我们可以获取在之前方案中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,我们可以运行以下代码片段:

 for param in wmt_transformer_model_ft.collect_params().values():
    if param.name in updated_params:
        param.grad_req = 'write'

现在,我们可以应用通常的训练过程,使用 WMT2016,我们得到了 Transformer 模型训练中的以下演变:

图 7.36 – Transformer 训练演变(训练损失)– 迁移学习后微调

图 7.36 – Transformer 训练演变(训练损失)– 迁移学习后微调

此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 分数(乘以 100)如下:

 WMT16 test loss: 1.23; test bleu score: 26.05

与我们之前的迁移学习实验相比,本次实验的定量表现略差。

从定性上讲,我们还可以通过一个句子示例来检查我们的模型表现如何。在我们的例子中,我们选择了 "I learn new things every day",得到的输出如下:

 Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne jeden Tag neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne jedes Mal Neues.

这句德语句子意味着我每次都学到新东西;因此,从结果可以看出,文本几乎已正确地从英语翻译成德语。

现在让我们继续进行第二个微调实验,在这个实验中,我们不应用迁移学习(没有冻结层),而是直接对整个模型应用微调。

我们需要重新获取预训练的 Transformer 模型,使用以下 MXNet GluonNLP 代码片段:

 wmt_model_name = 'transformer_en_de_512'
wmt_transformer_model_ft_direct, _, _ = nlp.model.get_model(
    wmt_model_name,
    dataset_name='WMT2014',
    pretrained=True,
    ctx=ctx)

现在,在不冻结的情况下,我们可以应用训练过程,这将更新我们 Transformer 模型的所有层:

图 7.37 – Transformer 训练演变(训练损失)– 不冻结的微调

图 7.37 – Transformer 训练演化(训练损失)– 不冻结层进行微调

此外,对于最佳迭代,测试集上获得的损失、困惑度和 BLEU 分数(乘以 100)如下:

 WMT16 test loss: 1.22; test bleu score: 26.75

与我们之前的微调实验相比,我们可以看到这个实验的性能略有提升。然而,实际操作中,我们原本预计会得到相反的结果(即该实验性能会略有下降)。这已被证明是一个可重复的结果,因为最初冻结编码器可以使解码器学习(使用编码器的表示)当前的任务。从某种角度来看,在这一步,知识从编码器转移到了解码器。在随后的步骤中,当编码器解冻时,解码器学习到的参数执行辅助的迁移学习——这次是从解码器到编码器。

从定性角度来看,我们还可以通过一个句子示例来检查模型的表现。在我们的例子中,我们选择了 "I learn new things every day",得到的输出结果如下:

 Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne jeden Tag neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne jedes Mal neue Dinge

这句德语的意思是 I learn new things every time;因此,从结果可以看出,文本几乎正确地从英语翻译成了德语。

它是如何工作的…

在这篇教程中,我们将第一个章节开头介绍的迁移学习和微调技术应用于机器翻译任务,该任务也在前一篇教程 从越南语翻译到英语 中呈现过,详见 第六章通过自然语言处理理解文本

我们探索了两个新的数据集,WMT2014WMT2016,这些数据集除了支持其他语言对的翻译外,还支持德语和英语之间的翻译。此外,MXNet GluonNLP 提供了以下内容:

  • 针对 WMT2014 的预训练 Transformer 模型

  • 一个准备好与 WMT2016 一起使用的数据加载器

此外,我们继续使用了为机器翻译引入的评估指标:困惑度(perplexity)和 BLEU。

MXNet 和 GluonNLP 提供的所有这些工具使我们能够通过几行代码轻松运行以下实验:

  • 从头开始训练 WMT2016 模型

  • 使用预训练模型通过迁移学习优化性能,从 WMT2014WMT2016

  • WMT2016 上对预训练模型进行微调(有层冻结和没有冻结层的情况)

我们比较了结果,并得出了最佳方法,即先应用迁移学习,之后进行微调。

还有更多…

在这篇教程中,我们介绍了两个新的数据集,WMT2014WMT2016。这些数据集是在 统计机器翻译研讨会WMT)会议上作为挑战引入的。2014 年和 2016 年的结果如下:

机器翻译中的迁移学习,包括微调,是一个活跃的研究领域。一篇发表于 2020 年的论文探讨了其应用,题为神经机器翻译中的迁移学习转移了什么?,可以在这里找到:aclanthology.org/2020.acl-main.688.pdf

针对更一般的自然语言处理应用,最*有一篇论文发布,题为自然语言处理中的迁移学习综述,可以在这里找到:www.researchgate.net/publication/342801560_A_Survey_on_Transfer_Learning_in_Natural_Language_Processing

第八章:使用 MXNet 改进训练性能

在之前的章节中,我们利用 MXNet 的功能解决了计算机视觉和 GluonCVGluonNLP 的问题。我们通过不同的方法训练了这些模型:从头开始迁移学习微调。在本章中,我们将重点关注如何提高训练过程的性能,并加速我们如何获得这些结果。

为了实现优化训练循环性能的目标,MXNet 提供了多种功能。我们已经简要使用了一些这些功能,例如 延迟计算 的概念,该概念在第一章中介绍过。我们将在本章中再次探讨这一点,并结合自动并行化来使用。此外,我们还将优化如何高效访问数据,利用 Gluon DataLoaders 在不同的环境(CPU、GPU)中执行数据转换。

此外,我们将探索如何结合多个 GPU 加速训练,利用诸如数据并行化等技术来获得最佳性能。我们还将探讨如何使用不同的数据类型与 MXNet 配合,以动态优化不同的数据格式。

最后,利用书中已探讨的问题,我们将通过示例应用所有这些技术。对于我们的计算机视觉任务,我们将选择图像分割,而对于 NLP 任务,我们将选择翻译英语到德语的文本。

具体来说,本章的结构包含以下食谱:

  • 介绍训练优化功能

  • 为图像分割优化训练

  • 为英语到德语的文本翻译优化训练

技术要求

除了前言中指定的技术要求外,以下技术要求适用:

  • 确保你已经完成了安装 MXNet、Gluon、GluonCV 和 GluonNLP的食谱。

  • 确保你已经完成了第五章第六章

  • 确保你已经完成了第七章

本章的代码可以在以下 GitHub URL 找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch08

此外,你可以直接从 Google Colab 访问每个食谱。例如,本章第一个食谱的代码可以在此找到:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch08/8_1_Introducing_training_optimization_features.ipynb

介绍训练优化功能

在前几章中,我们展示了如何利用MXNetGluonCVGluonNLP来检索特定数据集(如ImageNetMS COCOIWSLT2015)中的预训练模型,并将其用于我们的特定任务和数据集。此外,我们还使用了迁移学习和微调技术来提高这些任务/数据集上的性能。

在本教程中,我们将介绍(并重温)几个概念和特性,这些将优化我们的训练循环,之后我们将分析其中的权衡。

准备工作

类似于前几章,在本教程中,我们将使用一些矩阵操作和线性代数,但这不会很困难,因为你会发现许多示例和代码片段来帮助你学习。

如何操作...

在本教程中,我们将通过以下步骤进行操作:

  1. 使用懒评估和自动并行化

  2. 优化 DataLoader:GPU 预处理和 CPU 线程

  3. 使用Float32Float16和自动混合精度进行训练

  4. 使用多个 GPU 和数据并行化进行训练

让我们深入了解这些步骤。

使用懒评估和自动并行化

第一章NumPy 和 MXNet NDArrays教程中,我们介绍了懒评估,MXNet 在计算操作时采用的策略。这种策略对于大计算负载来说是最优的,因为实际的计算会被延迟,直到这些值真正需要时才会计算。

此外,MXNet 通过推迟操作计算,直到它们真正需要时,能够并行化一些计算,这意味着涉及的数据不会按顺序处理。这一过程是自动完成的,对于在多个硬件资源(如 CPU 和 GPU)之间共享数据时非常有用。

作为一个示例,我们可以进行一些矩阵乘法实验。我们的第一个实验将生成四个矩阵,然后进行它们之间的乘法组合。在每次计算后,我们将强制计算完成(通过添加wait_to_read()函数调用)。我们将计算两种配置下的结果。初始配置将强制 MXNet 使用一个线程(NaiveEngine)。在这种配置下,计算花费的时间是:

Time (s): 134.3672107196594

第二种配置是测试 MXNet 的常规默认配置(ThreadedEnginePerDevice,四个 CPU 线程)。在这种配置下,计算花费的时间是:

Time (s): 135.26983547210693

如我们所见,强制每次计算在进入下一步之前完成(通过调用wait_to_read())在多线程配置中是适得其反的。

我们的第二次实验将非常相似;但是,这一次,我们将删除所有对wait_to_read()函数的调用。我们将只确保在计算时间之前,所有矩阵乘法的计算都已完成。对于初始配置(NaiveEngine),计算所需的时间如下:

Time (s): 134.47382940625321

正如预期的那样,这个时长与仅使用一个线程时非常相似,因为所有计算都是按顺序进行的。

使用我们的第二种配置(ThreadedEnginePerDevice,具有四个 CPU 线程),第二次实验的计算时间如下:

Time (s): 111.36750531196594

结果表明,使用多个线程(MXNet 的默认自动配置)时,我们获得了约 20%的提升(在更适合多线程的工作负载下,提升可能更高)。

重要提示

请注意,在代码中,我们使用了mx.nd.waitall()函数,以确保所有计算在计算操作所花费的时间之前都已严格完成。

优化 DataLoader——GPU 预处理与 CPU 线程

第二章理解图像数据集——加载、管理和可视化时尚 MNIST 数据集一节中,我们介绍了Gluon DataLoader,这是一种高效的机制,用于生成批次大小,供我们的模型用于训练和评估。

DataLoader 在我们的数据预处理过程中扮演着两个重要角色。首先,正如我们在前面的章节中探讨过的,我们的模型是为并行数据处理进行了优化,这意味着我们可以在同一时间处理多个样本(例如,图像分割任务中的图像),这些样本会在同一个批次中并行处理,且由GPU进行处理。这个参数称为批次大小。另一方面,样本通常需要进行预处理,以最大化模型的性能(例如,图像被调整大小,并将其值从[0, 255]映射到[0, 1])。这些操作耗时,优化这些操作可以节省大量时间和计算资源。

让我们分析一下将数据预处理放在 GPU 上与使用 CPU 的常规默认行为之间的效果。作为基准,我们计算仅使用 CPU 加载数据集所需的时间。我们选择分割数据集的验证集,结果如下:

Time (s): 24.319150686264038

然而,在加载数据集时,我们通常会应用某些transform操作,以最大化网络性能。常见的转换操作包括图像缩放、裁剪、转化为张量以及归一化,可以通过以下代码在 MXNet 中定义:

input_transform_fn = mx.gluon.data.vision.transforms.Compose([
mx.gluon.data.vision.transforms.Resize(image_size, keep_ratio=True),
mx.gluon.data.vision.transforms.CenterCrop(image_size), mx.gluon.data.vision.transforms.ToTensor(),
mx.gluon.data.vision.transforms.Normalize([.485, .456, .406], [.229, .224, .225])
])

在仅使用 CPU 处理分割数据集的验证分割时,应用这些转换操作后的处理时间如下:

Time (s): 38.973774433135986

正如我们所看到的,处理时间增加了超过 50%,从大约 24 秒增加到大约 39 秒。然而,当我们利用 GPU 进行数据预处理时,处理时间如下:

Time (s): 25.39602303504944

正如我们所看到的,基于 GPU 的预处理操作几乎没有额外开销(<5%)。

此外,在 GPU 上执行预处理还有另一个优势:数据可以保存在 GPU 中供我们的模型处理,而使用 CPU 进行预处理时,我们需要将数据复制到 GPU 内存中,这可能会占用大量时间。如果我们实际测量端到端的预处理流水线,将数据预处理与复制操作到 GPU 内存结合起来,得到的结果如下:仅使用 CPU 时,端到端处理时间如下:

Time (s): 67.73443150520325

正如我们所看到的,复制时间非常长,整个流水线需要超过 1 分钟。然而,使用 GPU 时的结果如下:

Time (s): 23.22727918624878

这表明完整预处理所需的时间有了显著改善(<40%)。总的来说,这是由于两个因素:首先,预处理操作在 GPU 上更快;其次,数据需要在过程结束时复制到 GPU,这样我们的模型(也存储在 GPU 中)才能高效地处理数据。

这种方法最主要的缺点是需要将整个数据集保存在 GPU 中。通常,GPU 内存空间是为你在训练或推理中使用的每个批次进行优化的,而不是为整个数据集进行优化。这就是为什么这种方法通常会以将处理后的数据从 GPU 内存空间复制回 CPU 内存空间的方式结束。

然而,有些情况下,将数据保存在 GPU 内存空间可能是正确的做法——例如,当你在实验不同的数据集时,可能会加载多个数据集并测试不同的预处理流水线。在这种情况下,你希望实验能快速完成,因此速度是需要优化的变量。此外,有时你并不是在处理数据集的完整训练/验证/测试集,而只是其中的一部分(例如,为了实验)。在这种情况下,优化速度也是合理的。

对于其他更面向生产的环境,正确的方法是在 GPU 内存空间中进行预处理,但将数据(回复制)保留在 CPU 内存空间。在这种情况下,结果略有不同:

Time (s): 34.58254957199097

正如我们所看到的,即使考虑到必要的数据移动(从 CPU 到 GPU,再从 GPU 回到 CPU),在 GPU 中进行预处理仍然能显著提高性能(约 50%)。

现在,我们将深入探讨如何利用 Gluon DataLoader 作为输入的两个重要参数:工作线程数和批量大小。工作线程数是 DataLoader 将并行启动的线程数量(多线程)用于数据预处理。批量大小,如前所述,是将并行处理的样本数量。

这些参数与 CPU 的核心数量直接相关,并且可以通过优化使用可用的硬件来实现最大性能。为了了解 CPU 的核心数,Python 提供了一个非常简单的 API:

import multiprocessing
multiprocessing.cpu_count()

在所选环境中,显示的可用核心数如下:

4

通过结合使用 CPU 和 GPU,我们可以计算最佳性能,考虑不同的工作线程数和批量大小值。为所选环境计算的结果如下:

图 8.1 – 不同计算模式下(CPU/GPU 和工作线程数)运行时间与批量大小的关系

图 8.1 – 不同计算模式下(CPU/GPU 和工作线程数)运行时间与批量大小的关系

图 8.1中,我们可以得出以下三个重要结论:

  • GPU 预处理管道(数据处理加内存存储)要快得多(+50% 的运行时提升),即使是将数据复制回 CPU 时也是如此。

  • 当结合使用 GPU 和 CPU 时,由于在这个环境下我们只使用一个 GPU,因此当将数据复制回 CPU 时会遇到瓶颈,因为数据是逐个样本复制的(而不是按批次)。

  • 如果仅使用 CPU,增加工作线程可以改善处理时间。然而,限制因素是线程的数量。添加的工作线程数超过线程数(在我们的例子中为四个)将不会提高性能。增加批量大小能够提升性能,直到达到某一数量(在我们的例子中为 8),超过该数量后,性能不会进一步提高。

重要提示

使用 GPU 时,MXNet Gluon DataLoader 仅支持工作线程数为0(零)的值。

使用 Float32、Float16 和自动混合精度进行训练

在之前的示例中,我们已经看到了如何通过不同的方式优化训练循环,以最大限度地提高给定模型的 CPU 和 GPU 性能。在本示例中,我们将探讨如何计算我们的数据输入、模型参数及其周围的各种算术运算,并了解如何优化它们。

首先,让我们了解计算是如何进行的。数据输入和模型参数的默认数据类型是Float32,可以通过(参见示例代码)验证这一点,产生以下输出:

Input data type: <class 'numpy.float32'> Model Parameters data type: <class 'numpy.float32'>

该输出结果如预期所示,表明我们的数据输入和模型参数的数据类型是Float32(单精度)。但这意味着什么呢?

Float32 表示两件事:一方面,它是一种支持使用浮动小数点表示十进制数的数据类型;另一方面,它使用 32 位来存储单个数字。此格式的最重要特性如下:

  • 能够表示大数值,从 10^-45 到 10^+38

  • 可变精度

使用 Float32 作为数据类型有很多优点,主要与其可变精度有关。然而,训练过程是一个迭代的优化过程,其中许多计算并不需要 Float32 数据类型的精度。如果能以受控的方式牺牲一些精度来加速训练过程,那是可以接受的。我们可以通过 Float16 数据类型(半精度)实现这种*衡的权衡。与 Float32 类似,Float16 的最重要特性如下:

  • 能够表示大数值,从 2^-24 到 2^+16

  • 可变精度

作为精度丧失的示例,我们可以通过以下代码片段显示 1/3 的*似值,以两种格式呈现:

a = mx.nd.array([1/3], dtype=np.float32)
 b = a.astype(np.float16)
print("1/3 as Float32: {0:.30f}".format(a.asscalar()))
print("1/3 as Float16: {0:.30f}".format(b.asscalar()))

结果如下:

1/3 as Float32: 0.333333343267440795898437500000
1/3 as Float16: 0.333251953125000000000000000000

如我们所见,所有表示都不是精确的,Float32 如预期一样提供了更高的精度,而 Float16 的精度更有限,但对于某些应用场景(如模型训练)可能足够,稍后我们会证明这一点。

如前所述,这种精度丧失是一种权衡,我们在训练循环中获得了巨大的速度提升。为了在训练循环中启用 Float16(半精度),我们需要对代码进行一些更改。首先,我们需要将模型参数更新为 Float16,这一操作只需一行简单的代码:

deeplab_ft_direct_f16.cast('float16')

之后,当我们的模型处理数据和真实标签时,这些也需要更新为 Float16,因此在我们的训练循环中,我们加入了以下几行:

data  = data.astype('float16', copy=False)
 label = label.astype('float16', copy=False)

通过这些更改,我们现在可以运行一个实验,比较两种训练循环的性能。例如,我们将微调一个 DeepLabv3 预训练模型,进行图像分割任务(参见 第七章 中的 改善图像分割性能 配方)。对于 Float32,我们得到以下结果:

Training time for 10 epochs: 594.4833037853241 / Best validation loss: 0.6800425

对于 Float16,我们获得了以下结果:

Training time for 10 epochs: 199.80901980400085 / Best validation loss: nan

不幸的是,对于 Float16,尽管我们的训练时间比 Float32 的训练循环少了约三分之一,但它并未收敛。这是由于几个原因:

  • 对大数值的支持有限,因为任何大于 65519 的整数都表示为无穷大

  • 对小数值的支持有限,因为任何小于 1e-7 的正十进制数都表示为 0(零)

幸运的是,MXNet 提供了一个解决方案,能够自动结合两者的优点:

  • 在必要的地方应用 Float32(单精度)

  • 在没有使用的地方应用 Float16(半精度),以优化运行时

这种方法被称为自动混合精度AMP),为了启用它,我们只需要在代码中进行一些更改。首先,在创建模型之前,我们需要初始化库:

amp.init()

然后,在初始化训练器/优化器之后,我们需要将其与 AMP 链接:

amp.init_trainer(trainer)

最后,为了防止下溢或溢出,我们需要启用Float16数据类型。这在训练循环中非常方便地实现:

with amp.scale_loss(loss, trainer) as scaled_loss:
mx.autograd.backward(scaled_loss)

当我们应用这些变化并为Float16(现在启用了 AMP)重复之前的实验时,得到了以下结果:

Training time for 10 epochs: 217.64903020858765 / Best validation loss: 0.7082735

如我们所见,我们在更短的时间内获得了非常相似的验证损失结果(约 33%)。

由于我们的训练循环的内存占用大约是之前的一半,我们通常可以将模型的大小翻倍(更多的层和更大的分辨率),或者将批量大小翻倍,因为 GPU 内存的消耗在这种情况下与完整的Float32训练循环相比是相同的。使用双倍批量大小运行相同的实验得到以下结果:

Training time for 10 epochs: 218.82141995429993 / Best validation loss: 0.18198483

如我们所见,增加批量大小对训练循环的性能有着非常好的影响,验证损失大大降低,并且训练时间也显著缩短(约 33%)。

然而,通常作为机器学习工程师MLE)或数据科学家DS),我们将处理大量数据和大型模型,运行训练循环,预计需要持续数小时或数天。因此,在工作中,MLEs/DSs 通常会在工作日结束前启动训练循环,留下训练在后台运行,并在下一个工作日回来分析和评估结果。在这种环境下,实际上优化预期训练时间以提升性能是一种更好的策略。使用 MXNet,我们也可以为此优化训练参数。例如,我们可以通过将训练轮数翻倍来调整训练时间。在这种情况下,实验得到了以下结果:

Training time for 10 epochs: 645.7392318248749 / Best validation loss: 0.16439788

与标准的Float32训练循环相比,这些结果非常好。然而,我们不要忘记,实际结果取决于特定任务、数据集、模型、超参数等。建议您在玩具训练循环中尝试不同的选项和超参数,以找到每种情况的最佳解决方案。

使用多个 GPU 和数据并行化训练

在这个方案中,我们将利用环境中多个 GPU 进一步优化训练。MXNet 和 Gluon 让我们可以非常轻松地更新训练循环以包含多个 GPU。

从高层次来看,利用多个 GPU 有两种范式:

  • 模型并行化:将模型分割成多个部分,并将每个部分部署到特定的 GPU 上。当模型无法适配单个 GPU 时,这种范式非常有用。

  • 数据并行化:数据批次被拆分成多个部分,每个部分会被分配到一个特定的 GPU 上,该 GPU 能够完全使用这些数据进行前向和反向传播。

我们将只使用数据并行化,因为它是最常见的用例,能带来很高的加速,并且由于其方法的简单性,它也最为便捷。

为了应用数据并行化,我们需要对训练循环进行如下修改:

  1. 设置上下文:上下文现在是一个列表,每个元素是一个特定的 GPU 上下文。

  2. 在这些上下文中初始化我们的模型:在数据并行化中,每个 GPU 都会存储一份所有模型参数的副本。

  3. 调整超参数:批量大小通常设置为尽可能大的值,而不填满 GPU 内存。当多个 GPU 并行工作时,这个数字通常可以乘以上下文中 GPU 的数量。然而,这也会对学习率产生副作用,必须将学习率乘以相同的数字,以保持梯度更新在相同的范围内。

  4. 分配数据:每个 GPU 必须拥有每个批次的一部分,并使用它进行前向和反向传播。

  5. 计算损失并更新梯度:每个 GPU 会计算与其批次切片相关的损失。MXNet 会自动结合这些损失并计算梯度,然后将其分发到每个 GPU,以更新它们的模型副本。

  6. 显示结果:训练损失和验证损失等统计信息通常会在每个批次中计算并积累,并在每个周期结束时进行可视化。

让我们看一些如何应用这些步骤的例子。

例如,在一个包含四个 GPU 的环境中设置上下文非常简单,使用 MXNet 只需要一行代码:

ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]

初始化模型和自定义层就这么简单。对于我们的环境,以下是如何初始化带有 ResNet-101 主干的 Deeplabv3 网络:

deeplab_ft_direct_f32 = gcv.model_zoo.get_model('deeplab_resnet101_coco', pretrained=True, ctx=ctx_list)
 [...]
deeplab_ft_direct_f32.head.initialize(ctx=ctx_list)

为了更新超参数,我们只需要计算上下文中的 GPU 数量,并更新之前计算的批量大小和学习率。对于我们的示例,这意味着只需添加或修改几行代码:

num_gpus = len(ctx_list)
 [...]
batch_size_per_gpu = 4
batch_size = len(ctx_list) * batch_size_per_gpu
 [...]
trainer = mx.gluon.Trainer(deeplab_ft_direct_f32.collect_params(), "sgd", {"learning_rate": 0.5})

为了将数据均匀地分配到每个 GPU 上,MXNet 和 Gluon 提供了一个非常方便的函数 split_and_load(),它会根据上下文中的 GPU 数量自动分配数据。在我们的环境中,操作如下:

data_list   = mx.gluon.utils.split_and_load(data, ctx_list=ctx_list)
 label_list  = mx.gluon.utils.split_and_load(label, ctx_list=ctx_list)

为了计算损失并更新梯度,分布在每个 GPU 上的数据会通过循环并行处理。由于 MXNet 提供了自动并行化,这些调用是非阻塞的,每个 GPU 独立计算其输出和损失。此外,MXNet 会将这些损失结合起来生成完整的梯度更新,并将其重新分配给每个 GPU,所有这些操作都是自动完成的。我们只需要几行代码即可完成这一切:

with mx.autograd.record():
outputs = [model(data_slice) for data_slice in data_list]
losses = [loss_fn(output[0], label_slice) for output, label_slice in zip(outputs, label_list)]
for loss in losses:
loss.backward()
trainer.step(batch_size)

最后,为了显示损失计算,需要处理每个 GPU 的损失并将其组合。使用自动并行化,可以通过一行代码轻松实现这一点:

current_loss = sum([l.sum().asscalar() for l in losses])

通过这些简单的步骤,我们已经能够修改我们的训练循环以支持多个 GPU,并且现在可以测量这些变化带来的性能提升。

作为提醒,使用一个 GPU,我们达到了以下性能(批处理大小为四):

Training time for 10 epochs: 647.753002166748 / Best validation loss: 0.0937674343585968

在我们的环境中,使用 4 个 GPU,我们可以将批处理大小增加到 16,其结果如下:

Training time for 10 epochs: 177.23532104492188 / Best validation loss: 0.082047363743186

如预期的那样,我们已经能够将训练时间减少到约 25%(从 1 个 GPU 到 4 个 GPU 时的预期减少量,由于数据分布的预期损失而稍微改善了验证分数)。

工作原理如下…

在这个配方中,我们深入探讨了如何利用 MXNet 和 Gluon 优化我们的训练循环。我们利用我们的硬件(CPU 和 GPU)来处理训练循环中的每一个步骤:

  • 我们重新审视了惰性评估和自动并行化机制如何共同作用以优化所有基于 MXNet 的流程。

  • 我们利用所有的 CPU 线程来加载数据,并通过 GPU 中的预处理进一步优化该过程。我们还比较了速度和内存优化之间的权衡。

  • 我们分析了不同的数据类型,并在可能的情况下将Float32的精度与Float16的加速结合起来,使用 AMP。

  • 我们通过使用多个 GPU(假设我们的硬件有这些设备可用)提升了训练循环的性能。

我们通过运行两个实验比较了每种场景,在特定优化之前和之后的性能,并强调了在使用这些优化时需要考虑的潜在权衡。在接下来的配方中,我们将同时应用所有这些优化技术,优化两个熟悉的任务:图像分割文本翻译

还有更多内容…

此配方中展示的所有优化特性都已在研究文献中进行了详细描述。以下是一些入门链接,以深入了解每个特性:

优化图像分割的训练

在之前的食谱中,我们展示了如何利用 MXNet 和 Gluon 通过各种技术来优化模型的训练。我们了解了如何联合使用懒惰求值和自动并行化来进行并行处理。我们看到如何通过结合在 CPU 和 GPU 上进行预处理来提高 DataLoader 的性能,以及如何使用半精度(Float16)与 AMP 结合来减少训练时间。最后,我们探索了如何利用多个 GPU 进一步减少训练时间。

现在,我们可以重新审视一个贯穿全书的课题:图像分割。我们在前几章的食谱中曾处理过这个任务。在第五章中的使用 MXNet Model Zoo 进行语义化物体分割——PSPNet 和 DeepLabv3食谱中,我们学习了如何使用 GluonCV Model Zoo 中的预训练模型,并介绍了我们将在本食谱中使用的任务和数据集:MS COCOPenn-Fudan Pedestrian数据集。此外,在第七章中的提高图像分割性能食谱中,我们比较了处理目标数据集时可以采取的不同方法——是从头开始训练模型,还是利用预训练模型的现有知识并通过不同的迁移学习和微调方式进行调整。

在本食谱中,我们将应用所有这些优化技术,以训练图像分割模型为具体任务。

准备工作

与之前的章节类似,在本食谱中,我们将使用一些矩阵运算和线性代数,但这并不难,因为你会发现很多示例和代码片段来帮助你学习。

如何做...

在本食谱中,我们将探讨以下步骤:

  1. 重新审视我们当前的预处理和训练流程

  2. 应用训练优化技术

  3. 分析结果

让我们深入了解每个步骤。

重新审视我们当前的预处理和训练流程

第七章中的提高图像分割性能食谱中,我们使用以下方法处理数据:

  1. 将数据从存储加载到CPU 内存空间

  2. 使用CPU预处理数据

  3. 使用默认参数在训练过程中处理数据

这种方法是一个有效的途径,用来比较我们可用的不同训练方案(从头开始训练、预训练模型、迁移学习和微调),而无需为实验增加复杂性。例如,这种方法在直接引入并评估微调技术时效果很好。

按照上述方法,在为此食谱选择的数据集(Penn-Fudan Pedestrian)上,基于 CPU 的预处理花费了以下时间:

Pre-processing time (s): 0.12470602989196777

此外,当与必要的步骤结合使用,如将数据批量重新加载并复制到 GPU 时,我们获得了以下性能:

Data-Loading in GPU time (s): 0.4085373878479004

在预处理之后,下一步是训练过程。如前所述,我们将通过直接使用微调技术来评估训练优化的效果。结合这种方法,我们将使用以下超参数:

# Epochs & Batch Size
epochs = 10
batch_size = 4
# Define Optimizer and Hyper Parameters
trainer = mx.gluon.Trainer(deeplab_ft_direct_naive.collect_params(), "sgd", {"learning_rate": 0.1})

在这些条件下,训练过程的持续时间和所达到的性能如下:

Training time for 10 epochs (s): 638.9948952198029 / Best validation loss: 0.09416388

如我们所见,在 10 分钟多一点的时间内,我们获得了优秀的验证性能(约为 0.09)。

每个 epoch 中训练损失和验证损失的演变如下:

图 8.2 – 回顾训练:训练损失与验证损失

图 8.2 – 回顾训练:训练损失与验证损失

图 8.2中,我们可以看到训练损失和验证损失的演变。正如本书各章节所探讨的那样,我们选择提供最小验证损失的模型(在这种情况下,这是在最后一个 epoch,即 epoch 10 中实现的)。

在训练完成后,我们可以验证数据集测试集上的整体性能。从定量角度来看,以下是我们获得的结果:

PixAcc:  0.9627800347222222
mIoU  :  0.9070747450272697

正如预期的那样,通过仅训练有限的 epoch 数(此处为 10),我们获得了优异的结果。

从定性角度来看,结果如下:

图 8.3 – 回顾训练:GroundTruth 示例和训练后的预测

图 8.3 – 回顾训练:GroundTruth 示例和训练后的预测

正如预期的那样,结果展示了模型如何学会将焦点集中在前景中的人身上,避免背景中的人。

应用训练优化技术

在本章开头的引入训练优化功能食谱中,我们展示了不同的优化技术如何提高训练机器学习模型过程中各个步骤的性能,包括数据预处理、模型训练和评估。

在本节中,我们将展示如何通过使用 MXNet 和 Gluon,仅用几行代码,我们可以轻松应用所有我们已经介绍过的技术。

如本章第一个示例所示,MXNet 默认应用最佳策略(ThreadedEnginePerDevice)来优化惰性求值和自动并行化,考虑到可用的 CPU 线程数,因此我们无需在此进行任何更改(请注意,当使用多个 GPU 时,这项技术也会自动应用)。

我们还展示了如何通过结合使用 CPU 线程和 GPU 来优化数据预处理管道,考虑到每种设备的数量,并据此进行优化。为了进行此实验,选择了具有以下特征的特定硬件:

Number of CPUs: 16
Number of GPUs: 4

为了使用这种优化技术,我们需要对代码做一些更改。具体来说,我们需要定义可供使用的 GPU:

# Context variable is now a list,
 # with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
 num_gpus = len(ctx_list)

此外,在我们的预处理管道中,我们现在需要一个特定的步骤,将数据从 CPU 内存空间复制到 GPU 内存空间:

p_train_gpu = mx.gluon.data.SimpleDataset(
    [(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
     for idx, (data, label) in enumerate(pedestrian_train_dataset)])
 p_val_gpu   = mx.gluon.data.SimpleDataset(
    [(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
     for idx, (data, label) in enumerate(pedestrian_val_dataset)])
 p_test_gpu  = mx.gluon.data.SimpleDataset(
    [(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
     for idx, (data, label) in enumerate(pedestrian_test_dataset)])
p_train_opt = p_train_gpu.transform(train_val_transform, lazy=False)
 p_val_opt   = p_val_gpu.transform(train_val_transform, lazy=False)
 p_test_opt  = p_test_gpu.transform(test_transform, lazy=False)

如本章第一个示例所讨论的,在典型的面向生产的环境中,我们不希望将数据保留在 GPU 中,以免占用宝贵的 GPU 内存。通常会根据 GPU 可用内存优化批量大小,并使用MXNet Gluon DataLoaders将数据从 CPU 内存空间批量加载到 GPU 内存空间。因此,为了使我们的基于 GPU 的预处理管道完整,我们需要一个最终步骤,将数据复制回 CPU 内存空间:

to_cpu_fn = lambda x: x.as_in_context(mx.cpu())

通过这些代码更改,我们的最佳预处理管道已经准备就绪,可以继续进行下一个优化技术:应用Float16优化,包括 AMP。

如本章第一个示例所示,为了启用这项技术,我们只需要对代码进行一些更改。首先,我们初始化库:

# AMP
amp.init()

其次,我们将训练器/优化器附加到库中:

amp.init_trainer(trainer)

最后,由于Float16数据类型的局限性,存在梯度溢出/下溢的风险;因此,我们需要根据情况调整(缩放)损失,这可以通过以下几行代码自动完成:

with amp.scale_loss(losses, trainer) as scaled_losses: mx.autograd.backward(scaled_losses)

通过这三项简单的更改,我们已经更新了训练循环,使其能够有效地使用Float16数据类型(在适当的情况下)。

请注意在前面的代码片段中,我们现在正使用一个损失列表,而不是单一的实例。这是由于我们的下一个也是最后一个训练优化技术:使用多个 GPU

正如我们将看到的,优化地使用多个 GPU 意味着将它们并行工作,因此,需要并行计算损失并执行训练的反向传播,从而生成前述段落中描述的损失列表。

为了并行使用多个 GPU,我们需要将新的上下文定义为一个列表(之前在预处理部分出现过,这里为了方便再次展示):

# Context variable is now a list,
 # with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)
 num_gpus = len(ctx_list)

由于现在我们有多个 GPU,我们可以增加批量大小,以便最佳利用可用的 GPU 内存空间:

batch_size = len(ctx_list) * batch_size_per_gpu

此外,在从 Gluon DataLoader 读取数据时,我们需要将数据批次分配到多个 GPU 上。幸运的是,Gluon 还提供了一个简化该操作的功能。我们只需要添加以下几行代码(对于每个训练和验证批次):

data_list  = mx.gluon.utils.split_and_load(data , ctx_list=ctx_list)
label_list = mx.gluon.utils.split_and_load(label, ctx_list=ctx_list)

如前所述,这种跨 GPU 的划分使我们能够并行计算模型输出及与这些输出相关的损失(衡量实际输出与预期输出之间的差异)。这可以通过以下几行代码实现:

outputs = [model(data_slice) for data_slice in data_list]
losses = [loss_fn(output[0], label_slice) for output, label_slice in zip(outputs, label_list)]

最后,我们计算用于更新模型权重的反向传播过程(结合 AMP 的缩放损失):

with amp.scale_loss(losses, trainer) as scaled_losses:
mx.autograd.backward(scaled_losses)

通过这些最小的代码更改,我们现在拥有了一个最佳的预处理和训练管道,可以运行实验以分析性能变化。

分析结果

在前面的部分,我们回顾了预处理和训练管道的先前性能,并回顾了我们如何应用必要的更改以实现训练优化技术,特别是针对我们的图像分割任务。

我们的预处理管道步骤现在如下:

  1. 从存储中加载数据到 CPU 内存空间。

  2. 使用 GPU 预处理数据。

  3. 将数据复制回 CPU 内存空间。

  4. 使用优化的参数在训练过程中处理数据。

对于我们的实验,我们将直接使用微调技术。

将之前描述的方法应用于为本方案选择的数据集(Penn-Fudan Pedestrian),预处理的时间如下:

Pre-processing time (s): 0.10713815689086914

端到端的预处理管道必须考虑使用Gluon DataLoader加载数据的批处理过程——在我们的情况下,将数据加载到多个 GPU 中,如下所示:

Data-Loading in GPU time (s): 0.18216562271118164

与本方案的初始部分相比(当时预处理需要0.4秒),我们可以看到,即使在将数据复制回 CPU 内存空间的额外开销下,我们仍然将预处理性能提高了>2 倍。

在预处理之后,下一步是训练过程。正如前面所描述的,我们将直接使用微调技术来评估我们训练优化的效果。结合这种方法,我们使用以下超参数:

# Epochs & Batch Size
epochs = 10
batch_size_per_gpu = 4
batch_size = len(ctx_list) * batch_size_per_gpu
# Define Optimizer and Hyper Parameters
trainer = mx.gluon.Trainer(deeplab_ft_direct_opt.collect_params(), "sgd", {"learning_rate": 0.5})

请注意,通过将多个 GPU 添加到训练过程中,我们可以增加批量大小(乘以 GPU 的数量),还可以增加学习率(从 0.1 增加到 0.5)。在这些条件下,训练过程的持续时间和实现的性能如下:

Training time for 10 epochs: 59.86336851119995 / Best validation loss: 0.08904324161509672

如图所示,我们在不到 1 分钟的时间内就得到了优秀的验证表现(约 0.09)。与配方中获得的结果相比,我们可以看到损失的减少非常小(这将通过我们的性能分析进一步确认),但迄今为止最大的一项改进是训练时间减少了>10 倍。这个改进归功于我们应用的所有训练优化技术。简而言之,每项优化都提供了以下改进:

  • 使用 4 个 GPU:提供了 4 倍的时间缩短

  • 使用 Float16 和 AMP:提供了 2 倍的时间减少(合计 8 倍)

  • 预处理数据集:提供了 1.25 倍的时间减少(合计>10 倍)

每个 epoch 中的训练损失和验证损失的变化如下:

图 8.4 – 优化训练:训练损失与验证损失

图 8.4 – 优化训练:训练损失与验证损失

图 8.4中,我们可以看到训练损失和验证损失的变化。正如本章至今所探讨的,我们选择提供最小验证损失的模型(在这种情况下,在最后一个 epoch,即第 10 个 epoch 中取得)。

训练完成后,我们可以在数据集的测试分割上验证整体性能。从定量的角度来看,我们获得的结果如下:

PixAcc:  0.9679262152777778
mIoU  :  0.9176786683400912

正如预期的那样,仅通过训练有限的 epoch(本例中为 10 个 epoch),我们就得到了优秀的结果。我们还可以确认,验证损失的最小改善带来了测试指标的微小改进(与我们初始实验中的 0.96/0.91 相比)。

从定性的角度来看,我们得到了以下结果:

图 8.5 – 优化训练:GroundTruth 示例与训练后预测

图 8.5 – 优化训练:GroundTruth 示例与训练后预测

正如预期的那样,结果显示模型已经学会了将注意力集中在前景中的不同人物上,避免了背景中的人物。

工作原理...

在本配方中,我们应用了本章第一部分中的不同训练优化技术,利用我们的硬件(CPU 和 GPU)来解决训练循环中的每个步骤:

  • 我们重新审视了惰性评估和自动并行化机制如何协同工作,以优化所有基于 MXNet 的流程。

  • 我们利用所有 CPU 线程加载数据,并通过在 GPU 上进行预处理进一步优化了该过程。我们还比较了速度和内存优化之间的权衡。

  • 我们分析了不同的数据类型,并结合了Float32的准确性与Float16的加速效果(在可能的情况下),并使用了 AMP。

  • 我们通过使用多个 GPU 提高了训练循环的性能(假设我们的硬件有这些设备)。

我们将这些场景分别应用于图像分割任务,并进行了两次实验。在第一次实验中,我们没有应用前面章节中描述的任何训练优化技术,而是遵循了书中前几章提到的方法。在第二次实验中,我们并行应用了所有技术,尽可能进行优化。

这一方法非常有用,提供了类似的算法性能,同时将训练时间提高了 10 倍(从 10 分钟缩短到 1 分钟)。这主要得益于使用了多个 GPU(减少了 4 倍),利用Float16 AMP(减少了 2 倍)以及优化的预处理(减少了 1.25 倍)。

还有更多内容……

我们已经描述、实现、执行并评估了几种训练优化技术。然而,还有更多先进的技术可以用来实现最佳的训练循环。

其中一种技术是学习率调度。在本书中,我们一直使用常数学习率。然而,使用动态调整的学习率有多个优点,其中一些如下:

  • 预热:在使用预训练模型时,不建议从较大的学习率开始。初始的几个 epoch 必须用于梯度的调整。这可以看作是将模型从源任务调整到目标任务的方式,保留并利用来自前一个任务的知识,因此推荐使用较小的学习率。

  • 衰减:在最佳训练循环中,当模型学习到输入到输出的预期表示时,训练的目标是产生越来越精细的改进。较小的学习率在这些阶段能获得更好的性能(更小、更稳定的权重更新)。因此,经过几个 epoch 后,衰减学习率是首选。

Dive into Deep Learning 书中提供了关于如何在 MXNet 中实现这些技术的深入见解:d2l.ai/chapter_optimization/lr-scheduler.html.

优化训练以将文本从英语翻译为德语

在本章的第一个示例中,我们展示了如何利用 MXNet 和 Gluon 优化我们的模型训练,应用不同的技术。我们理解了如何联合使用惰性计算和自动并行化进行并行处理,并通过将预处理分配到 CPU 和 GPU 上提高了 DataLoader 的性能。我们还看到,结合使用半精度(Float16)和 AMP 可以将训练时间缩短一半,并探索了如何利用多个 GPU 进一步缩短训练时间。

现在,我们可以重新审视我们在整本书中一直在处理的问题,即从英语到德语的翻译。我们在之前的章节中已经处理了翻译任务。在第六章中的从越南语到英语的翻译示例中,我们介绍了翻译任务,并学习了如何使用来自 GluonCV 模型库的预训练模型。此外,在第七章中的提高从英语到德语翻译性能示例中,我们介绍了本示例中将要使用的数据集:WMT2014WMT2016,并比较了我们在处理目标数据集时可以采取的不同方法:从头开始训练我们的模型,或利用预训练模型的过去知识并进行调整,采用不同的迁移学习和微调策略。

因此,在本示例中,我们将应用所有这些优化技术,专门用于训练一个英语到德语的文本 翻译模型

准备工作

与之前的章节一样,在本示例中我们将使用一些矩阵运算和线性代数,但理解起来一点也不难。

如何实现...

在本示例中,我们将按以下步骤进行操作:

  1. 重新审视我们当前的数据预处理和训练流程

  2. 应用训练优化技术

  3. 分析结果

让我们深入了解每一步。

重新审视我们当前的数据预处理和训练流程

第七章中的提高从英语到德语翻译性能的示例中,我们使用以下方法处理数据:

  • 将数据从存储加载到 CPU 内存中

  • 使用 CPU 对数据进行了预处理

  • 在训练过程中使用了默认参数来处理数据

这是一个有效的方法,可以比较我们可用的不同训练选择(从头开始训练、预训练模型、迁移学习和微调),而不会增加实验的复杂性。例如,这种方法非常适合介绍和评估微调技术,这是我们在本示例中选择的技术。

在应用前面描述的方法到本示例所选数据集(WMT2016)时,基于 CPU 的预处理需要以下时间:

Pre-processing time (s): 2.697735548019409

此外,当与必要的批量重新加载数据并将其复制到 GPU 的步骤结合时,我们将获得以下性能:

Data-Loading in GPU time (s): 27.328779935836792

预处理完成后,下一步是训练过程。如前所述,我们将直接评估使用微调技术对训练优化的影响。结合这种方法,我们使用以下超参数:

# Epochs & Batch Size
hparams.epochs = 5
hparams.lr = 0.00003
# hparam.batch_size = 256

在这些条件下,训练过程的持续时间和所取得的性能如下:

Training time for 5 epochs: 11406.558312892914 / Best validation loss: 1.4029905894300159

如我们所见,在训练时间大约为 3 小时的情况下,我们获得了优秀的验证性能(~1.4)。

每轮训练损失和验证损失的变化情况如下所示:

图 8.6 – 重新审视训练:训练损失与验证损失

图 8.6 – 重新审视训练:训练损失与验证损失

图 8.6中,我们可以看到训练损失和验证损失的变化。正如在各章节中探讨的那样,我们选择提供最小验证损失的模型(在这个案例中,最小验证损失出现在第一轮训练,即第 1 轮)。

训练完成后,我们可以在数据集的测试分割中验证整体性能。从定量角度来看,以下是我们获得的结果:

WMT16 test loss: 1.28; test bleu score: 27.05

正如预期的那样,通过仅仅训练有限的时期(在此为 10 轮),我们就获得了优异的结果。

从定性角度来看,我们还可以通过测试一个示例句子来检查模型的表现。在我们的案例中,我们选择了I learn new things every day,并且得到的输出如下:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne neue Dinge jeden Tag.
 In English:
 I learn new things every day.
 The German translation is:
 Immer wieder erfährt ich Neues.

输出结果中的德语句子(Immer wieder erfährt ich Neues)的意思是我总是学习新东西,因此,从结果中可以看出,文本几乎已从英语完美翻译成德语。

应用训练优化技术

在本章开头的引入训练优化特性配方中,我们展示了不同的优化技术如何提高我们在训练机器学习模型时所采取的不同步骤的性能,包括数据预处理、训练和评估模型。

在本节中,我们将展示如何通过使用 MXNet 和 Gluon 以及仅仅几行代码,轻松应用我们已介绍的所有技术。

如本章的第一种配方所示,MXNet 默认应用最佳策略(ThreadedEnginePerDevice)来优化懒评估和自动并行化,考虑到可用的 CPU 线程数,因此我们无需在此处进行任何修改(请注意,当使用多个 GPU 时,这项技术也会自动应用)。

我们已经展示了如何通过结合使用 CPU 线程和 GPU,优化我们的数据预处理管道,考虑到每个设备的可用数量并进行相应优化。对于这次实验,选择了具有以下特征的特定硬件:

Number of CPUs: 16
Number of GPUs: 4

为了应用这种优化技术,我们不得不对代码进行一些修改。具体来说,我们定义了可用的 GPU:

# Context variable is now a list,
 # with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
 num_gpus = len(ctx_list)

此外,在我们的预处理管道中,我们现在需要一个特定的步骤,将数据从 CPU 内存空间复制到 GPU 内存空间:

wmt2016_train_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_train_data_processed)])
wmt2016_train_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_train_data_processed)])
wmt2016_val_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_val_data_processed)])
wmt2016_ test _data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_ test _data_processed)])

如本章第一个例子所讨论的,在典型的生产环境中,我们并不希望将数据保留在 GPU 中,因为它会占用宝贵的 GPU 内存。通常会根据 GPU 可用内存优化批次大小,并通过 MXNet Gluon DataLoaders 从 CPU 内存空间批量加载数据到 GPU 内存空间。因此,为了使我们的基于 GPU 的预处理管道完整,我们需要一个最终步骤,将数据复制回 CPU 内存空间。正如在第七章中的提高英德翻译性能一节中介绍的,我们使用的是来自 MXNet GluonNLP 库的 ShardedDataLoader 类。这个类会自动执行数据的回传到 CPU 内存空间。

然而,正如我们在实验中将看到的,当使用多个 GPU 时,直接使用 MXNet Gluon DataLoaders 会更高效,因为它们设计上可以在后续进行最佳并行化。

通过这些代码修改,我们的最佳预处理管道已经准备好,接下来可以继续进行下一个优化技术:应用 Float16 优化,包括 AMP。

正如本章第一个例子所示,为了启用该技术,我们只需要在代码中做几个修改。首先,我们初始化库:

# AMP
amp.init()

其次,我们将训练器/优化器附加到库中:

amp.init_trainer(trainer)

在前面的例子中,当处理图像时,我们描述了由于梯度可能出现过度/欠流动的问题,因此需要相应地调整(缩放)损失。这在我们的用例中并不必要,因此我们在这里不进行损失 缩放

通过这两个简单的修改,我们已更新训练循环,以便在适当时使用 Float16 数据类型高效工作。

最后,我们可以应用下一个也是最后一个训练优化技术:使用多个 GPU。

正如我们将看到的,优化地使用多个 GPU 意味着并行处理它们,因此并行计算损失并执行训练的反向传递,从而得到上一段描述的损失列表。

为了在多个 GPU 上并行工作,我们需要将新上下文定义为一个列表(之前在预处理时见过,这里再次展示以便于参考):

# Context variable is now a list,
 # with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
 num_gpus = len(ctx_list)

由于我们现在有了多个 GPU,我们可以增加批次大小,以最优化使用可用的 GPU 内存空间:

batch_size = len(ctx_list) * batch_size_per_gpu

此外,当从 Gluon DataLoaders 中读取数据时,我们需要将数据批次分配到各个 GPU 上。幸运的是,Gluon 也提供了一个简化该操作的函数。我们只需为每个训练和验证批次添加以下几行代码:

src_seq_list = mx.gluon.utils.split_and_load(src_seq, ctx_list=ctx_list, even_split=False)
 tgt_seq_list = mx.gluon.utils.split_and_load(tgt_seq, ctx_list=ctx_list, even_split=False)
 src_valid_length_list = mx.gluon.utils.split_and_load(src_valid_length, ctx_list=ctx_list, even_split=False)
 tgt_valid_length_list = mx.gluon.utils.split_and_load(tgt_valid_length, ctx_list=ctx_list, even_split=False)

如前所述,GPU 之间的分割使我们能够并行计算模型的输出及其相关的损失(即实际输出与预期输出之间的差异度量)。这可以通过以下几行代码实现:

out_slice, _ = wmt_transformer_model_ft_direct_opt(
src_seq_slice,
tgt_seq_slice[:, :-1],
src_valid_length_slice,
tgt_valid_length_slice - 1)
loss = loss_function(out_slice, tgt_seq_slice[:, 1:], tgt_valid_length_slice - 1)

通常,为了使我们的更新能够在训练循环中与多个 GPU 一起工作,我们需要对损失缩放进行进一步修改。然而,正如前面所讨论的,对于我们的使用案例,这是不必要的。

通过这些最小的代码更改,我们现在拥有了一个最佳的预处理和训练管道,可以运行所需的实验来分析性能变化。

分析结果

在前面的部分中,我们回顾了我们预处理和训练管道的先前性能,并回顾了如何为我们的训练优化技术应用必要的更改,特别是针对我们将英文翻译成德文的任务。

我们的预处理管道步骤现在如下:

  1. 将数据从存储加载到 CPU 内存空间。

  2. 使用 GPU 预处理数据(尽管正如我们将看到的,我们会将其改为 CPU)。

  3. 将数据复制回 CPU 内存空间(此操作不必要)。

  4. 在训练过程中使用优化的参数处理数据。

对于我们的实验,我们将直接使用微调技术。

按照前述方法,在为本食谱选择的数据集(WMT2016)上,基于 GPU 的预处理花费了以下时间:

Pre-processing time (s): 50.427586793899536

端到端的预处理管道必须考虑使用 Gluon DataLoader 进行批处理的过程(在我们的案例中,将数据加载到多个 GPU 中),从而为我们提供以下性能:

Data-Loading in GPU time (s): 72.83465576171875

与本食谱的初始部分(预处理花费了 27 秒)相比,我们可以看到,在这种情况下,GPU 上的预处理效果并不显著。这是由于文本数据的特性,它不像图像那样容易并行化。

在这种情况下,基于 CPU 的预处理管道是最佳选择,避免使用 Gluon NLPShardedDataLoader 类,而改用 Gluon DataLoader 类(它更适合并行化)。应用此管道后,我们得到了以下结果:

Data-Loading in CPU with Gluon DataLoaders time (s): 24.988255500793457

这为我们提供了一个最小的优势(2 秒),但如前所述,这是使用 Gluon DataLoader 及其并行化功能时我们能得到的最佳结果。

经过预处理后,下一步是训练过程。如前所述,我们将使用微调技术直接评估我们训练优化的效果。结合这种方法,我们使用以下超参数:

# Epochs & Batch Size
hparams.epochs = 5
hparams.lr = 0.0001
# hparams.batch_size = num_gpus * 256

请注意,通过在训练过程中增加多个 GPU,我们可以增加批处理大小(乘以 GPU 数量),并且还可以增加学习率(从 0.00003 增加到 0.0001)。在这些条件下,训练过程的持续时间和达到的性能如下:

Training time for 5 epochs: 1947.1244320869446 / Best validation loss: 1.2199710432327155

如我们所见,在训练时间约为 3 小时的情况下,我们获得了出色的验证表现(约 1.4)。与本食谱初始部分获得的结果相比,我们可以看到损失有了最小的下降(这是一个积极的变化,我们将在接下来的性能分析中确认),但迄今为止最大的改进是训练时间减少了 5.5 倍。这个改进归功于我们应用的所有训练优化技术。简而言之,每个优化提供了以下改进:

  • 使用 4 个 GPU:提供了 4 倍的降低(如预期)。

  • Float16 在不影响算法性能的情况下使用。

  • 预处理数据集:在这种情况下,改进几乎可以忽略不计。

每一轮训练中的训练损失和验证损失的演变如下所示:

图 8.7 – 优化训练:训练损失与验证损失

图 8.7 – 优化训练:训练损失与验证损失

图 8.7中,我们可以看到训练损失和验证损失的变化过程。如本书各章节所述,我们选择了提供最小验证损失的模型(在这种情况下,这是在第一轮训练时实现的)。

训练完成后,我们可以在数据集的测试分区中验证整体性能。从量化角度来看,这些是我们获得的结果:

WMT16 test loss: 1.27; test bleu score: 28.20

正如预期的那样,仅通过训练有限数量的轮次(在本例中为 5 次),我们就获得了优异的结果。我们还可以确认,验证损失的最小改进为我们的测试指标提供了最小的提升(与最初获得的 27.05 相比)。

从定性角度来看,我们也可以通过用一个示例句子测试模型来检查它的表现。在我们的例子中,我们选择了 I learn new things every day,得到的输出如下:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne jedes Mal Neues.

输出中得到的德语句子(Ich lerne jedes Mal Neues)的意思是 I learn something new every time,因此从结果来看,文本几乎已经被完美地从英语翻译成德语。

它是如何工作的...

在本食谱中,我们应用了本章第一个食谱中看到的不同训练优化技术,利用我们的硬件(CPU 和 GPU)来解决训练循环中的每个步骤:

  • 我们重新审视了懒惰求值和自动并行化机制如何协同工作,以优化所有基于 MXNet 的流程。

  • 我们利用了所有 CPU 线程来加载数据,并通过在 GPU 上进行预处理进一步优化了该过程。在这种情况下,展示了结合 Gluon DataLoader 的基于 CPU 的预处理管道是最优方案。

  • 我们分析了不同的数据类型,并结合了 Float32 的准确性和精度,以及 Float16 的加速效果,并在可能的情况下,使用了 AMP。

  • 我们通过使用多个 GPU(假设我们的硬件具备这些设备)提高了训练循环的性能。

我们将每种具体应用于将英文文本翻译成德文的情景进行了比较,进行了两项实验。在第一项实验中,我们没有应用书中描述的任何训练优化技术,而是采用了之前章节中的方法。在第二项实验中,我们同时应用了所有技术,试图尽可能优化。

这证明非常有用,提供了类似的算法性能,训练时间缩短了 5.5 倍(从 3 小时缩短到 30 分钟)。这主要是由于使用了多个 GPU(减少了 4 倍)和利用了Float16和 AMP(减少了 1.4 倍),而优化的预处理提供了微不足道的改进。

还有更多内容…

我们描述、实施、执行和评估了几种训练优化技术。然而,还有更高级的技术可以利用,以实现最佳的训练循环。

其中一种技术是人类反馈强化学习RLHF),引入了人在回路中的过程。在这个过程中,模型训练完成后,会向人员展示模型的不同输出选项(例如,不同的潜在翻译),并根据这些人员对哪个最好地表达原始句子进行排序。这些人类输入然后用于训练一个评分模型,评分模型会对模型的输出进行评分,并选择分数最高的输出。这种技术已被证明非常强大。例如,OpenAI利用RLHFGPT-3语言模型之上开发了ChatGPT

要了解更多关于ChatGPTRLHF的信息,推荐阅读以下文章:huyenchip.com/2023/05/02/rlhf.html

第九章:使用 MXNet 提升推理性能

在之前的章节中,我们利用 MXNet 的功能解决了计算机视觉自然语言处理任务。这些章节的重点是从预训练模型中获得最大性能,利用 GluonCV 和 GluonNLP 的模型库API。我们使用从头开始的不同方法训练这些模型,包括迁移学习微调。在上一章中,我们探索了如何利用一些高级技术优化训练过程。最后,在本章中,我们将重点提高推理过程本身的性能,加速从我们的模型中获得结果,并讨论与边缘****AI 计算相关的多个主题。

为了实现优化推理管道性能的目标,MXNet 包含了不同的功能。我们已经简要讨论过其中的一些功能,例如在前一章中介绍的自动混合精度AMP),它可以提高训练性能,同时也可以用来提升推理性能。在本章中,我们将重新讨论这一点,以及其他功能,如混合化。此外,我们还将进一步优化如何有效利用数据类型,借助量化中的INT8数据类型加速推理过程。

此外,我们将探索我们的模型在操作方面的工作原理,了解它们如何在MXNet 分析器的帮助下内部运行。然后,我们将借助 MXNet GluonCV 模型库,进一步学习如何将我们的模型导出为ONNX格式,使用该格式,我们可以将模型应用于不同的框架,例如将我们的模型部署到 NVIDIA 硬件*台上,如NVIDIA Jetson系列产品。

最后,我们将结合应用所有这些技术,选择书中已经探讨过的问题作为例子。对于计算机视觉任务,我们将选择图像分割;对于自然语言处理任务,我们将选择将英文文本翻译成德文。

本章具体包含以下食谱:

  • 介绍推理优化功能

  • 优化图像分割的推理

  • 优化将英文文本翻译为德文时的推理

技术要求

除了《前言》中指定的技术要求外,以下内容适用:

  • 确保你已经完成了第一章中的食谱 1安装 MXNet

  • 确保你已经完成了第五章使用计算机视觉分析图像,以及第六章利用自然语言处理理解文本

  • 确保你已经完成了第七章通过迁移学习与微调优化模型

本章的代码可以在以下 GitHub 网址找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch09

此外,你可以直接从 Google Colab 访问每个配方,例如,本章第一个配方:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch09/9_1_Introducing_inference_optimization_features.ipynb

引入推理优化功能

在之前的章节中,我们已经看到如何利用 MXNet、GluonCV 和 GluonNLP 从特定数据集(如 ImageNet、MS COCO 或 IWSLT2015)中获取预训练模型,并将其应用于我们的特定任务和数据集。此外,我们还使用了迁移学习和微调技术来提高这些任务/数据集的算法性能。

在这个配方中,我们将介绍(并重温)几个概念和功能,这些内容将优化我们的推理循环,以提高运行时性能,同时分析其中的权衡。

做好准备

与之前的章节一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但这完全不难。

如何操作...

在这个配方中,我们将执行以下步骤:

  1. 混合我们的模型

  2. 使用 float16 和 AMP 进行推理

  3. 使用 INT8 进行量化

  4. 对我们的模型进行性能分析

让我们深入了解每个步骤。

混合我们的模型

在最初的章节中,我们在探索 MXNet 的特性时,重点介绍了命令式编程。如果你以前用过 Java、C/C++或 Python 等语言编程,那么你很可能使用过命令式编程。这是一种常见的编码方式,因为它更灵活。

在命令式编程中,通常期待代码中的语句按顺序逐步执行。例如,在我们的评估路径中,通常会逐步执行这些语句,通常是在一个循环内部:

  1. 从数据加载器中加载新样本。

  2. 转换输入和预期输出,以便它们可以被我们的模型和指标计算所使用。

  3. 将输入传递给模型以计算输出。

  4. 将模型输出与预期输出进行比较,并更新相应的指标。

在这种编程范式中,每个语句按顺序执行,输出可以在每一步完成后进行检查或调试(因为 MXNet 使用惰性求值)。

使用不同的编程范式,称为符号编程,其中使用的是符号,符号本质上是操作的抽象,直到定义的某一点(通常称为编译步骤)才会进行实际计算。这对于深度学习尤其有用,因为所有模型都可以定义为图,使用这个图作为符号,优化底层图中的操作路径,并仅在需要时运行优化后的计算。

然而,由于计算尚未发生,因此每个步骤的输出无法检查或调试,这使得查找和修复问题变得更加困难。另一方面,由于图优化的能力,符号编程需要更少的内存且速度更快。

幸运的是,使用 MXNet,我们可以充分利用两者的优势。我们可以使用命令式编程定义模型,进行测试、调试和修复(通过print语句、测试、调试等机制)。当我们准备好进行优化时,我们只需要调用hybridize函数,它会处理底层的一切工作,与我们的图形在符号编程中一起工作。这种方法被称为混合编程,是 MXNet 的最佳优势之一。此外,这个特性没有硬件限制,可以用于 CPU 和 GPU 计算。

作为一个玩具示例,我们可以进行一些实验,通过推理模型并比较不同配置的不同结果。具体来说,这些是我们将测试的配置:

  • CPU:

    • 使用命令式执行

    • 使用符号执行和默认参数

    • 使用符号执行和特定后端

    • 使用符号执行、特定后端和静态内存分配

    • 使用符号执行、特定后端、静态内存分配和不变输入形状

  • GPU:

    • 使用命令式执行

    • 使用符号执行和默认参数

    • 使用符号执行和静态内存分配

    • 使用符号执行、静态内存分配和不变输入形状

请注意,为了正确验证计算时间,我们添加了对mx.nd.waitall()函数的调用。选择的方法是使用ADE20K验证集(数据集可通过 MXNet GluonCV 获得),并使用DeepLabv3模型进行处理。我们将使用批量大小为 4:

  1. 对于初始的 CPU 计算配置,使用命令式执行时,模型对数据集的处理时间如下:

    Time (s): 115.22693085670471
    
  2. 对于第二种 CPU 计算配置,我们只需利用 MXNet 混合编程模型并使用以下方式转换我们的模型:

    deeplab_pt_cpu_hybrid.hybridize()
    Time (s): 64.75840330123901
    

    正如我们所看到的,所做的优化将计算时间减少了*一半。

  3. 对于第三种 CPU 计算配置,我们只需稍微修改我们的混合化调用以定义特定的后端。我们将利用我们的 Intel CPU 架构,使用MKLDNN后端,并使用以下方式转换我们的模型:

    deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN")
    Time (s): 55.860424757003784
    

    正如我们所见,特定的后端进一步将计算时间减少了约 20%。

  4. 对于第四个 CPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望使用静态内存分配。我们可以使用以下方式更新我们的调用:

    deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN", static_alloc=True)
    Time (s): 53.905478715896606
    

    正如我们所见,静态内存分配使我们能够将计算时间再减少约 4%。

  5. 对于第五个 CPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望利用不变的输入形状(我们已经预处理了数据,使其具有相同的输入形状,480x480)。我们可以使用以下方式更新我们的调用:

    deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN", static_alloc=True, static_shape=True)
    Time (s): 52.464826822280884
    

    正如我们所见,不变输入形状约束使我们能够将计算时间再减少约 2%。

  6. 对于初始的 GPU 计算配置,采用命令式执行,模型处理数据集的时间如下:

    Time (s): 13.315197944641113
    
  7. 对于第二个 GPU 计算配置,我们只需要利用 MXNet 混合编程模型,并用以下方式转换我们的模型:

    deeplab_pt_gpu_hybrid.hybridize()
    Time (s): 12.873461246490479
    

    正如我们所见,当在 GPU 上执行优化时,由于 GPU 已经针对这些类型的计算进行了内部优化,因此几乎没有提高计算时间。

  8. 对于 GPU 计算,没有需要选择的特定后端。因此,对于第三个 GPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望使用静态内存分配。我们可以使用以下方式更新我们的调用:

    deeplab_pt_gpu_hybrid.hybridize(static_alloc=True)
    Time (s): 12.752988815307617
    

    正如我们所见,静态内存分配在 GPU 上带来了另一个微不足道的改进。

  9. 对于第四个 GPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望利用不变的输入形状(我们已经预处理了数据,使其具有相同的输入形状,480x480)。我们可以使用以下方式更新我们的调用:

    deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
    Time (s): 12.583650827407837
    

    正如我们所见,不变输入形状约束在 GPU 上带来了另一个微不足道的改进。

结果显示,当使用 CPU 时,我们可以将推理时间减少到原始时间的一半,这是一个显著的改进。使用 GPU 时,由于内部优化,改进几乎可以忽略不计。

重要提示

请注意,在代码中我们是如何使用mx.nd.waitall()函数来验证所有计算是否已经严格完成,然后才计算这些操作所花费的时间。

应用 float16 和 AMP 进行推理

在上一章,第八章使用 MXNet 提升训练性能,我们介绍了float16数据类型和 AMP 优化,这是一种极为简单的方式,仅在最有用时才使用这一半精度数据类型。

食谱 1介绍训练优化特性,上一章中,我们比较了单精度(float32)和半精度(float16)数据类型,理解它们的特性和内存/速度折衷。如果你还没有复习这个食谱,建议你回顾一下,因为它与本主题非常相关。

正如大多数概念之前已介绍的那样,本节将重点讨论如何将 AMP 应用于推理过程。像往常一样,MXNet 为此操作提供了一个非常简单的接口,只需调用amp.convert_hybrid_block()函数即可。

该优化可以应用于 CPU 和 GPU 环境,因此让我们来运行这些实验。

要修改我们的 CPU 模型以使用 AMP,我们只需要以下一行代码:

deeplab_pt_cpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_cpu_hybrid, ctx=mx.cpu())

使用这个修改后的模型,处理数据集的时间如下:

Time (s): 56.16465926170349

正如我们所见,AMP 在 CPU 上几乎没有产生改善。这是因为最大的收益出现在训练时所需的反向传递过程中,但在推理过程中并不需要。此外,CPU 通常没有专门的电路来直接处理 float16,这限制了改进效果。

要修改 GPU 模型以使用 AMP,我们只需要以下一行代码:

deeplab_pt_gpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_gpu_hybrid, ctx=mx.gpu())

使用这个修改后的模型,处理数据集的时间如下:

Time (s): 3.371366024017334

正如我们所见,AMP 在 GPU 上产生了优秀的结果,将推理时间减少了大约~25%。这是因为 GPU 具有专门的电路来直接处理 float16,极大地改善了结果。

重要说明

amp.convert_hybrid_block()函数接受不同的参数。鼓励你尝试不同的选项(如cast_optional_params)以找到最佳配置。

使用 Int8 进行量化

在前面的章节中,我们看到如何通过使用不同的方法来优化推理循环,优化如何使用 CPU 和 GPU 以获得最大性能,给定一个模型。我们还探讨了如何利用单精度(float32)和半精度(float16)数据类型。在本节中,我们将探讨如何通过一种新的数据类型 Int8 来优化我们的数据输入、模型参数以及它们之间的不同算术运算。

这种数据类型的修改比精度变化更有深远的影响。我们还将底层表示从浮点数修改为整数,这样可以减少内存和计算要求。让我们分析一下这种数据类型。

Int8 表示两件事:它是一种仅支持整数数字(没有浮动小数点)的数据类型,并且在这种格式下存储单个数字所用的位数是 8 位。此格式的最重要特性如下:

  • 能表示从-128 到 127,或从 0 到 255 的整数(取决于它是有符号还是无符号类型)

  • 常数精度(每个连续的数字相差恰好 1)

为了解释Int8量化的核心思想,并展示精度损失,我们可以用以下代码片段显示数字 1/3(即三分之一)在Float32Int8两种格式下的*似值:

a = mx.nd.array([1/3], dtype=mx.np.float32)
 int_value = 85
scaling_factor = 255
b = int_value / scaling_factor
print("1/3 as 0.333... (Float32): {0:.30f}".format(a.asscalar())))
print("1/3 as 85/255   (Int8)   : {0:.30f}".format(b))

这产生了以下结果:

1/3 as 0.333... (Float32): 0.333333343267440795898437500000
1/3 as 85/255   (Int8)   : 0.333333333333333314829616256247

如我们所见,所有表示方式都不是完全精确的,float32表现出了非常高的精度,符合预期。使用Int8时,我们做了一个小的简化;我们使用了两个 8 位整数,85255,并用其中一个作为缩放因子。这个缩放因子通常会同时应用于多个数字集。它可以是整个模型的相同缩放因子(不太可能),也可以是每层的缩放因子,等等。这个缩放因子不需要以Int8表示,它可以是float32

重要提示

对于这个特定的例子,选择的Int8表示比目标数值更精确,但这只是巧合。在常见的场景中,存在精度损失,进而导致性能损失。

为了最小化性能损失,通常量化调优技术会要求一个校准数据集。然后,使用该数据集来计算减少性能损失的参数。

除了使用校准数据集外,还有一些技术可以优化最准确的Int8值的计算,而 MXNet 提供了一个非常简单的 API 来促进我们网络的优化。通过简单调用mx.contrib.quantization.quantize_net_v2()函数,我们将更新我们的网络为Int8

特别是,对于我们的实验,这就是我们使用的调用:

deeplab_pt_cpu_q_hybrid = quantization.quantize_net_v2(
deeplab_pt_cpu,
 quantized_dtype='auto',
 exclude_layers=None,
 exclude_layers_match=None,
 calib_data=ade20k_cal_loader_gpu_cpu,
 calib_mode='entropy',
 logger=logger,
 ctx=mx.cpu())

重要提示

Int8量化是一个复杂的过程,针对特定应用的定制需要深入的分析和一些反复试验。关于涉及的参数,建议阅读以下函数文档:github.com/apache/mxnet/blob/v1.9.1/python/mxnet/contrib/quantization.py#L825

使用这个修改后的基于 CPU 的模型,处理数据集所需的时间如下:

Time (s): 36.10324692726135

正如我们所看到的,Int8在 CPU 上产生了显著的提升,几乎减少了约 50%的运行时间。

不幸的是,对于 GPU,这个特性无法引入。尽管最*的 GPU 有专门的Int8电路,但这还是一个比较新的发展,MXNet 尚不支持这些操作符。

对我们的模型进行分析

在这个配方中,我们已经看到如何使用不同的技术来优化推理循环。然而,有时即使引入了这些优化技术,我们的模型仍可能无法达到我们预期的运行时性能。这可能是由于以下几个原因:

  • 架构并不适合边缘计算。

  • 操作符尚未得到充分优化。

  • 组件之间的数据传输。

  • 内存泄漏。

为了验证我们的模型内部是如何工作的,检查需要进一步优化的地方和/或调查模型性能不佳的可能原因,MXNet 提供了一种低级分析工具,称为 MXNet 性能分析器。

MXNet 性能分析器在后台运行,实时记录模型中发生的所有操作和数据传输。它也非常轻量,占用的资源非常少。最重要的是,它极易配置和使用。

为了分析一组语句,我们需要采取两个步骤:

  1. 配置性能分析器。

  2. 在要进行性能分析的语句之前和之后启动与停止性能分析器。

要配置性能分析器,我们只需要一行代码,如下所示:

mx.profiler.set_config(
profile_all=True,
 aggregate_stats=True,
 continuous_dump=True,
 filename='profile_output_cpu.json')

要启动和停止性能分析器,我们需要在要分析的语句的开始和结束处添加以下几行代码:

mx.profiler.set_state('run')
[... code statements to analyze ...]
# Wait until all operations have completed
mx.nd.waitall()
# Stop recording
mx.profiler.set_state('stop')
# Log results
mx.profiler.dump()

请注意,我们需要三条语句来停止录制:完成所有指令、停止录制,并转储在第一步中配置的文件的信息。

重要说明

请注意代码中我们如何使用 mx.nd.waitall() 函数来验证所有计算已严格完成,然后再计算这些操作所花费的时间。

如前所述的指令会生成一个 JSON 文件,随后可以通过追踪应用程序进行分析。我推荐使用 Google Chrome 中包含的 Tracing 应用程序,因为它非常易于使用和访问。只需在地址栏中输入以下内容:chrome://tracing

为了验证 MXNet 性能分析器的功能,我们以广泛使用的ResNet架构为例,这些架构在我们的图像分割任务中被广泛应用,作为我们所使用的 DeepLabv3 网络的骨干。

ResNet 网络的典型架构如下:

图 9.1 – ResNet50 模型架构

图 9.1 – ResNet50 模型架构

请注意,在图 9.1中,初始步骤(阶段 1)是卷积、批量归一化和激活。

从我们分析过的模型中,Google Chrome Tracing 应用程序提供了以下屏幕:

图 9.2 – 性能分析 ResNet

图 9.2 – 性能分析 ResNet

图 9.2中,我们可以看到模型的一般执行情况。放大早期层,我们可以看到如下:

图 9.3 – 性能分析 ResNet: 放大

图 9.3 – 性能分析 ResNet: 放大

图 9.3中,我们可以看到阶段 1 的卷积、批量归一化和激活步骤被清晰地展示出来。我们还可以非常清楚地看到,批量归一化操作所需的时间大约是卷积和激活步骤的 4 倍,这可能表明了一个优化的方向。

它的工作原理……

在本节中,我们深入探讨了 MXNet 和 Gluon 如何帮助我们优化推理循环。我们通过解决推理循环中的每一个步骤,充分利用了我们的硬件(CPU 和 GPU):

  • 重用了上一章中数据加载的工作

  • 通过混合化优化图计算

  • 分析了不同的数据类型,并在可能的情况下,使用 AMP 结合float16的加速与float32的精度,利用 GPU 的特定电路

  • 通过使用 Int8 量化迈出了进一步的步伐

  • 使用 MXNet 分析器分析了低级性能

我们通过运行多个实验,比较了每个特性的效果,比较了在特定优化前后的性能,强调了使用这些优化时需要考虑的潜在权衡。总结来说,结果如下:

特性 CPU 上的结果(毫秒) GPU 上的结果(毫秒)
标准 115 13.3
混合化 / 默认 65 12.9
混合化 / MKLDNN 56 N/A
混合化 / MKLDNN + 静态分配 54 12.8
混合化 / MKLDNN + 静态分配 + 不变形状 52 12.6
AMP 54 3.5
Int8 量化 36 N/A

表 9.1 – CPU 和 GPU 的特性及结果汇总

在接下来的食谱中,我们将同时应用所有这些优化技术,针对 CPU(MKL-DNN + 静态分配 + 不变形状 + Int8 量化)和 GPU(静态分配 + 不变形状 + 自动混合精度)优化两项常见任务:图像分割和文本翻译。

还有更多…

本食谱中展示的所有优化特性都在文献中进行了详细描述。在本节中,我们分享了一些入门链接,以便深入理解每个特性:

优化图像分割的推理

在之前的食谱中,我们展示了如何利用 MXNet 和 Gluon 来优化模型推理,应用了不同的技术,例如使用混合化提高运行时性能;如何结合 AMP 使用半精度(float16)显著减少推理时间;以及如何利用 Int8 量化等数据类型进一步优化。

现在,我们可以重新审视本书中一直在处理的一个问题:图像分割。我们在前几章的食谱中已经处理过这个任务。在食谱 4,《使用 MXNet Model Zoo 进行语义图像分割—PSPNet 和 DeepLabv3》中,来自第五章《使用计算机视觉分析图像》,我们介绍了这个任务以及我们将在本食谱中使用的数据集,MS COCO 和 Penn-Fudan Pedestrian,并学习了如何使用来自 GluonCV Model Zoo 的预训练模型。

此外,在食谱 3,《提升图像分割性能》中,来自第七章《通过迁移学习和微调优化模型》中,我们比较了处理目标数据集时可以采取的不同方法,是否从头开始训练我们的模型,或者利用预训练模型的先验知识并针对我们的任务进行调整,使用不同的迁移学习和微调方式。最后,在食谱 2,《优化图像分割训练》中,来自第八章《通过 MXNet 提升训练性能》中,我们应用了不同的技术来提升训练循环的运行时性能。

因此,在本食谱中,我们将应用所有介绍的优化技术,专注于优化图像分割模型的推理任务。

准备工作

和之前的章节一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但这绝对不难。

如何操作...

在这个食谱中,我们将使用以下步骤:

  1. 应用推理优化技术

  2. 可视化和分析我们的模型

  3. 将我们的模型导出到 ONNX 和 TensorRT

让我们深入探讨每个步骤。

应用推理优化技术

食谱 1,《介绍推理优化功能》中,我们展示了不同的优化技术如何提高推理过程中各个步骤的性能,包括混合化、AMP 和 Int8 量化。

在这一部分,我们将展示如何仅通过几行代码,在 MXNet 和 Gluon 中轻松应用我们介绍的每一项技术,并验证每项技术的结果。

如果不应用这些优化技术,作为基准,以下是使用 CPU 获取的定量结果:

PixAcc:  0.9602144097222223
mIoU  :  0.4742364603465315
Time (s): 27.573920726776123

我们可以显示图像以获取定性结果:

图 9.4 – 定性结果:CPU 基准

图 9.4 – 定性结果:CPU 基线

从定量指标可以预见,图 9.4也显示了出色的结果。

正如之前的食谱所总结的,对于 CPU 上的最大性能,最佳方法是:

  • 使用混合化:使用 Intel MKL-DNN 后端,结合静态内存分配和不变输入形状。

  • 不要使用 AMP。

  • 使用 Int8 量化。

让我们将这些技术应用于我们当前的特定任务——图像分割。

对于混合化,我们只需要一行代码(其中包括 Intel MKLDNN后端所需的参数,结合静态内存分配和不变输入形状):

deeplab_pt_cpu_q_hybrid.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)

我们不需要添加 AMP 步骤,因为它在 CPU 工作负载中未能提供任何好处。

对于Int8量化,我们需要两个独立的步骤。一方面,我们需要定义校准数据集。这可以通过少量代码实现:

# Dataset Loading & Transforming
# Limit to 10 samples (last ones)
 max_samples = 10
samples = range(0, max_samples)
 p_cal_cpu_pre = mx.gluon.data.SimpleDataset([(pedestrian_val_dataset[-i][0], pedestrian_val_dataset[-i][1]) for i in tqdm(samples)])
 p_cal_gpu_cpu = p_cal_cpu_pre.transform_first(input_transform_fn_gpu_cpu, lazy=False)
# DataLoader for Calibration
# For CPU, Pre-processed in GPU, copied back to CPU memory space)
 num_workers = 0
batch_size = 4
p_cal_loader_gpu_cpu = mx.gluon.data.DataLoader(
    p_cal_gpu_cpu,    batch_size=batch_size,
    num_workers=num_workers,
    last_batch="discard")

然后,为了应用Int8量化,并使用校准数据集进行优化,只需再加一行代码:

deeplab_pt_cpu_q_hybrid = quantization.quantize_net_v2(
    deeplab_pt_cpu,
    quantized_dtype='auto',
    exclude_layers=None,
    exclude_layers_match=None,
    calib_data=p_cal_loader_gpu_cpu,
    calib_mode='entropy',
    logger=logger,
    ctx=mx.cpu())

应用这些优化技术,得到的优化 CPU 推理定量结果如下:

PixAcc:  0.9595597222222222
mIoU  :  0.47379941937958425
Time (s): 8.355125904083252

正如我们所见,性能差异(0.959对比0.9600.473对比0.474)可以忽略不计。然而,通过这些推理优化技术,我们已经将推理运行时减少了 4 倍(8.4 秒对比 27.6 秒),这是一个令人印象深刻的结果。

我们也可以显示一张图像来展示定性结果:

图 9.5 – 定性结果:CPU 优化推理

图 9.5 – 定性结果:CPU 优化推理

从定量指标可以预见,图 9.5也显示了出色的结果,差异几乎可以忽略不计(如果有的话)。

那么,基于 GPU 的推理怎么样呢?让我们按照相同的步骤进行:

  1. 如果不应用这些优化技术,作为基线,这些是使用 GPU 获得的定量结果:

    PixAcc:  0.9602144097222223
    mIoU  :  0.4742364603465315
    Time (s): 13.068315982818604
    

    正如预期的那样,与 CPU 基线相比,算法性能没有变化。GPU 的推理运行时确实是 CPU 的两倍快(13.1秒对比27.6秒)。

  2. 我们可以显示一张图像来展示定性结果:

图 9.6 – 定性结果:GPU 基线

图 9.6 – 定性结果:GPU 基线

从定量指标可以预见,图 9.6也显示了出色的结果。

正如之前的食谱所总结的,对于 GPU 上的最大性能,最佳方法是:

  • 使用混合化:使用静态内存分配和不变输入形状。不要使用 Intel MKL-DNN 后端。

  • 使用 AMP

  • 不要使用 Int8 量化(不支持)。

让我们将这些技术应用于我们当前的特定任务——图像分割。

对于混合化,我们只需要一行代码(其中包括静态内存分配和不变输入形状所需的参数):

deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)

对于 AMP,我们需要遵循两个简单的步骤,即前向传播和模型的转换,如下所示:

deeplab_pt_gpu_hybrid(single_sample_gpu);
deeplab_pt_gpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_gpu_hybrid, ctx=mx.gpu())

不需要进一步的步骤。

通过应用这些优化技术,我们获得了优化 GPU 推断的量化结果:

PixAcc:  0.9602565972222222
mIoU  :  0.4742640561133744
Time (s): 0.8551054000854492

正如我们所看到的,性能上的差异(0.9600.960,以及0.4740.474)是不存在的。此外,通过这些推断优化技术,我们已经成功将推断运行时间缩短了 15 倍(0.85 秒与 13.1 秒),这是一个令人印象深刻的成果。

我们还可以显示一张图片来展示定性结果:

图 9.7 – 定性结果:GPU 优化推断

图 9.7 – 定性结果:GPU 优化推断

正如定量指标所预期的那样,图 9.7也显示了出色的结果,与图 9.6中的结果几乎没有(如果有的话)可忽略的差异。

可视化和剖析我们的模型

在前几节中,我们看到了可以应用于优化推断循环的不同技术,以及这些技术所取得的结果。但是,这些技术究竟是如何工作的?为什么它们更快?

我们将使用 MXNet 提供的两个工具来达到这个目的:

  • 模型可视化

  • 模型剖析

模型可视化为我们提供了一种直观的方式来看待不同层之间的交互。对于使用 ResNet 骨干(例如我们在本文档中用于图像分割的DeepLabv3),这尤为重要,因为残差通过层进行传递。

用 MXNet 可视化我们的模型架构非常简单。当使用符号模型时,只需一行代码即可。在我们的情况下,由于我们使用 Gluon 模型,需要以下几行代码:

deeplab_pt_cpu_q_hybrid.export('deeplab_pt_cpu_q_hybrid_sym')
 sym, arg_params, aux_params = mx.model.load_checkpoint('deeplab_pt_cpu_q_hybrid_sym', 0)
 mx.visualization.plot_network(sym)

正如前一篇文章所示,基于 ResNet 的网络由 ResNet 块组成,其中包括卷积、批量归一化和激活步骤。

对于我们的 CPU 优化模型(混合化和Int8量化),以下是这些块之间部分连接的样子:

图 9.8 – ResNet 块的 GraphViz(CPU 优化)

图 9.8 – ResNet 块的 GraphViz(CPU 优化)

正如我们在图 9.8中所看到的,预期的 ResNet 块操作没有单独的块;它们都是执行所有计算的单个块的一部分。这些操作的组合称为运算符融合,其中尽可能多的操作被融合在一起,而不是计算一个操作,然后是下一个操作(通常发生数据传输)。最大的好处在于融合的操作可以在相同的内存空间中进行。这是混合化执行的优化之一,因为一旦网络的图形完成,很容易找到候选融合操作。

好的,模型可视化告诉我们这些优化会发生,但我们如何验证它们实际上正在发生呢?这正是模型性能分析擅长的地方,也可以帮助我们了解运行时发生的问题。

如食谱中所述,引入推理优化功能,以及“对我们模型进行性能分析”一节中提到,模型性能分析的输出是一个 JSON 文件,可以使用 Google Chrome Tracing 应用等工具进行可视化。对于一个未优化的 CPU 工作负载,我们的 DeepLabv3 模型显示了以下的时间配置文件:

图 9.9 – DeepLabv3 性能分析:未优化的 CPU 工作负载

图 9.9 – DeepLabv3 性能分析:未优化的 CPU 工作负载

图 9.9 中,我们可以看到以下特征:

  • 几乎所有任务都由单个进程处理。

  • 在操作开始约 80 毫秒后,所有任务已被派发,并且控制已返回,操作继续进行(延迟评估和 mx.nd.waitall)。

  • 所有任务在操作开始约 80 毫秒时已被派发,并且控制已返回,操作继续进行(延迟评估和 mx.nd.waitall)。

  • 所有操作都是原子操作,并逐个执行。

  • 完整的操作大约需要 800 毫秒。

对于一个 CPU 优化的工作负载,我们的 DeepLabv3 模型显示了以下的时间配置文件:

图 9.10 – DeepLabv3 性能分析:优化后的 CPU 工作负载

图 9.10 – DeepLabv3 性能分析:优化后的 CPU 工作负载

图 9.10 中,我们可以看到以下特征:

  • 几乎所有任务都由单个进程处理,类似于未优化的版本。

  • 在操作开始约 5 毫秒后,所有任务已被派发,并且控制已返回,操作继续进行(延迟评估和 mx.nd.waitall),比未优化版本要快得多。

  • 内存的使用是同步/结构化的,这与未优化的版本形成鲜明对比。

  • 所有操作都被融合在一起,这与未优化的版本形成鲜明对比。

  • 完整的操作大约需要 370 毫秒。

总结来说,对于基于 CPU 的优化,我们可以清晰地看到效果:

  • 混合化将所有操作符融合在一起,基本上在一个操作中执行几乎全部工作负载。

  • MKL-DNN 后端和 Int8 量化通过加速操作改善了这些操作。

对于我们的 GPU 优化模型(混合化并使用 AMP),以下是一些 ResNet 模块之间连接的情况:

图 9.11 – ResNet 模块的 GraphViz(GPU 优化)

图 9.11 – ResNet 模块的 GraphViz(GPU 优化)

图 9.11 所示,这与 CPU 优化后的可视化完全不同,因为所有预期的 ResNet 模块操作的独立块都可以被识别出来。正如本章第一个食谱中所提到的,混合化对 GPU 的影响非常有限。

那么,性能加速是从哪里来的呢?让我们借助模型性能分析来解答。

对于未优化的 GPU 工作负载,我们的 DeepLabv3 模型显示出以下时间轮廓:

图 9.12 – 深度分析 DeepLabv3:未优化的 GPU 工作负载

图 9.12 – 深度分析 DeepLabv3:未优化的 GPU 工作负载

图 9.12中,我们可以看到以下特点:

  • 几乎所有任务都由两个 GPU 进程处理。

  • 在操作开始约 40 毫秒时,所有任务已被发送进行调度,控制返回以继续操作(懒评估和mx.nd.waitall)。

  • 内存的异步/非结构化使用。

  • 所有操作都是原子性的并单独执行。

  • 完整操作大约需要 150 毫秒。

对于优化过的 GPU 工作负载,我们的 DeepLabv3 模型显示出以下时间轮廓:

图 9.13 – 深度分析 DeepLabv3:优化过的 GPU 工作负载

图 9.13 – 深度分析 DeepLabv3:优化过的 GPU 工作负载

图 9.13中,我们可以看到以下特点:

  • 几乎所有任务都由两个进程处理,类似于未优化的版本。

  • 在操作开始约 4 毫秒时,所有任务已被发送进行调度,控制返回以继续操作(懒评估和mx.nd.waitall),比未优化的版本快得多。

  • 内存的同步/结构化使用,与未优化的版本形成鲜明对比。

  • 所有操作都是原子性的并单独执行;与未优化的版本类似,只是速度更快。例如,大型卷积操作在 GPU 未优化的情况下大约需要 1 毫秒,而在 GPU 优化的情况下只需要三分之一的时间(约 0.34 毫秒)。

  • 完整操作大约需要 55 毫秒(为未优化时间的三分之一)。

总结来说,对于基于 GPU 的优化,我们可以清晰地看到效果:

  • 如预期的那样,混合化没有效果,也没有发现操作融合。

  • 如果 GPU 具有专用的 float16 电路,AMP 使得操作运行得更快。

将我们的模型导出到 ONNX 和 TensorRT

MXNet 和 GluonCV 也提供了将我们的模型导出到外部的工具。这对于优化运行时计算时间(推理)最为有用,可能需要:

  • MXNet/GluonCV 可能不支持的特定算法

  • 在特定硬件*台上的部署和优化

在这一部分,我们将研究每个类别的一个示例。

对于特定的算法,我们将把我们的模型导出为 ONNX 格式。ONNX代表开放神经网络交换格式,它是一个开放的格式,描述了深度学习模型如何存储和共享。这对于利用特定工具执行高度专业化任务极为有用。例如,ONNX Runtime拥有强大的推理工具,包括量化(例如,ONNX Runtime 支持基于 GPU 的 INT8 量化)。因此,我们可以将模型导出为 ONNX 格式,并直接开始使用 ONNX Runtime 进行工作。

和往常一样,MXNet 只需几行代码就能帮助我们完成这个任务。我们需要执行两个步骤。首先,我们需要将模型从 Gluon 格式转换为符号格式(先混合,再导出):

deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
 deeplab_pt_gpu_hybrid(single_sample_gpu)
 # Need to be exported externally for the symbols to be loaded
deeplab_pt_gpu_hybrid_filename = "deeplab_resnet101_coco_pt_gpu_hybrid"
deeplab_pt_gpu_hybrid.export(deeplab_pt_gpu_hybrid_filename)

接下来,我们可以将符号模型转换为 ONNX:

# Files exported
sym_filename = deeplab_pt_gpu_hybrid_filename + "-symbol.json"
params_filename = deeplab_pt_gpu_hybrid_filename + "-0000.params"
in_shapes = [single_sample_gpu.shape]
 in_types = [mx.np.float32]
 onnx_model_path = mx.onnx.export_model(
    sym_filename,
    params_filename,
    in_shapes,
    in_types,
    onnx_file_name)

ONNX 还提供了一个检查器,用于验证我们的模型是否正确导出。可以使用以下代码行来完成:

# Model Verification
import onnx
# Load the ONNX model
onnx_model = onnx.load_model(onnx_model_path)
 # Check the ONNX graph
onnx.checker.check_graph(onnx_model.graph)

就这样!按照这些指示,我们将把 ONNX 模型存储在文件中(在我们的示例中为 'deeplab_resnet101_coco_pt_gpu_hybrid.onnx'),并准备好在任何接受 ONNX 模型作为输入的工具中使用。

另一方面,有时我们希望在特定硬件*台上部署和/或优化我们的模型,例如 NVIDIA 系列产品(例如,Nvidia Jetson *台)。具体来说,Nvidia 提供了一个特定的机器学习框架,用于在其硬件上运行推理。这个框架叫做TensorRT

尽管 MXNet 提供了直接的 TensorRT 集成,但默认情况下并未启用,需要从源代码直接构建 MXNet,并启用特定参数来支持 TensorRT 集成。更为直接的是,我们可以利用刚才描述的 ONNX 导出,生成一个支持 TensorRT 的模型。

为了实现这一点,只需写几行代码:

import tensorrt as trt
trt_file_name = "deeplab_resnet101_coco_pt_gpu_hybrid.trt"
TRT_LOGGER = trt.Logger(trt.Logger.INFO)
 builder = trt.Builder(TRT_LOGGER)
 config = builder.create_builder_config()
explicit_batch = 1 << (int) (trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
 deeplab_pt_gpu_hybrid_trt = builder.create_network(explicit_batch)
with open(onnx_file_name, 'rb') as model:
    with trt.OnnxParser(deeplab_pt_gpu_hybrid_trt, TRT_LOGGER) as parser:
        assert parser.parse(model.read()) == True
    deeplab_pt_gpu_hybrid_engine_serialized = builder.build_serialized_network(deeplab_pt_gpu_hybrid_trt, config=config)
with open(trt_file_name, 'wb') as f:
    f.write(bytearray(deeplab_pt_gpu_hybrid_engine_serialized))

有了这个,我们将编写一个序列化的 TensorRT 可用模型。

我们可以通过反序列化和读取模型来验证它是否可以被正确读取。我们可以使用以下代码行做到这一点:

# Check it can be read back
runtime = trt.Runtime(TRT_LOGGER)
 with open(trt_file_name, 'rb') as f:
    deeplab_pt_gpu_hybrid_engine_deserialized = runtime.deserialize_cuda_engine(f.read())

就这样!在这一节中,我们已经成功地编写了 ONNX 和 TensorRT 模型。恭喜!

它是如何工作的……

在本教程中,我们应用了本章第一节中提到的不同推理优化技术,利用我们的硬件(CPU 和 GPU)通过以下方式优化模型的运行时性能:

  • 混合模型

  • 利用 AMP

  • 使用 INT8 数据类型进行量化以加速推理

此外,我们还学习了如何使用模型可视化工具(由 GraphViz 提供支持)和 MXNet 分析器,并利用这些工具从低级角度分析推理优化。

最后,我们学会了如何将模型导出到特定场景和目的,使用 ONNX 和 TensorRT 库。

还有更多……

在本教程中,我们从训练后角度提出了推理优化问题。我们拿到了一个(预)训练的模型,并尽可能挖掘它的性能。

然而,还有另一条可以探索的途径,那就是从机器学习模型设计的角度开始思考如何最大化推理性能。这就是所谓的模型压缩,它是一个活跃的研究领域,定期会发布很多改进。*期的活跃研究课题包括:

优化从英语翻译到德语的推理

在最初的食谱中,我们展示了如何利用 MXNet 和 Gluon 优化模型的推理过程,应用了不同的技术:通过混合化提高运行时性能;如何结合 AMP 使用半精度(float16)显著减少推理时间;以及如何通过数据类型(如 Int8 量化)进一步优化。

现在,我们可以重新审视本书中一直在讨论的一个问题:将英语翻译成德语。我们在前几章的食谱中处理过翻译任务。在第六章《利用自然语言处理理解文本》的食谱 4《将越南语文本翻译成英语》中,我们介绍了翻译文本的任务,同时学习了如何使用来自 GluonCV Model Zoo 的预训练模型。

此外,在第七章《通过迁移学习和微调优化模型》的食谱 4《提高英语到德语翻译的性能》中,我们介绍了在本食谱中将使用的数据集:WMT 2014WMT 2016。我们还比较了处理目标数据集时可以采用的不同方法:从零开始训练我们的模型,或利用预训练模型的过去知识,并根据我们的任务进行调整,使用不同的迁移学习和微调方式。最后,在第八章《通过 MXNet 提高训练性能》的食谱 3《优化英语到德语翻译的训练》中,我们应用了不同的技术来提高训练循环的运行时性能。

因此,在本食谱中,我们将应用所有介绍过的优化技术,专门用于优化从英语到德语翻译的推理过程。

做好准备

与前几章一样,在本食谱中我们将使用一些矩阵运算和线性代数,但这并不难。

如何操作...

在本食谱中,我们将进行以下步骤:

  1. 应用推理优化技术

  2. 对我们的模型进行分析

  3. 导出我们的模型

让我们深入探讨这些步骤。

应用推理优化技术

在本章开始的食谱 1《引入推理优化功能》中,我们展示了不同的优化技术如何改善机器学习模型推理过程中各个步骤的性能,包括混合化、AMP 和 Int8 量化。

在本节中,我们将展示如何通过 MXNet 和 Gluon,仅用几行代码,我们就能轻松应用每个介绍过的技术,并验证每项技术的结果。

如果不应用这些优化技术,作为基准,以下是使用 CPU 获得的定量结果:

WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 373.5446252822876

从定性角度来看,我们还可以检查模型在一个句子示例上的表现。在我们的案例中,我们选择了 I learn new things every day,并且获得的输出如下:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne neue Dinge, die in jedem Fall auftreten.

输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习在每种情况下都会出现的新事物,因此,正如结果所示,文本几乎被完美地从英语翻译成了德语。

正如前一个方案中总结的,为了在 CPU 上实现最大性能,最佳的方法如下:

  • 使用混合化:使用 Intel MKL-DNN 后端,结合静态内存分配和不变输入形状。

  • 不要使用 AMP。

  • 使用 Int8 量化。

很遗憾,我们无法使用 Int8 量化,因为它不支持 GluonNLP 模型。

让我们为当前特定任务应用每一项技术,即从英语翻译成德语。

对于混合化,我们只需要几行代码(包括所需的参数,这些参数是针对 Intel MKL-DNN 后端的,结合静态内存分配和不变输入形状,同时对损失函数进行混合化):

wmt_transformer_pt_cpu_hybrid.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)
loss_function = nlp.loss.MaskedSoftmaxCELoss()
loss_function.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)

我们不需要添加与 AMP 相关的任何步骤,因为已经证明它对基于 CPU 的工作负载没有益处。同样,GluonNLP 不支持 Int8 量化,因此我们不需要对代码做任何进一步的修改。

应用这些优化技术后,以下是优化后的 CPU 推理所获得的定量结果:

WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 312.5660226345062

正如我们所看到的,性能差异(损失函数为 1.53 与 1.53,BLEU 分数为 26.40 与 26.40)几乎可以忽略不计。然而,使用这些推理优化技术,我们成功将推理运行时间缩短了 20%(313 秒比 374 秒),这是一个非常好的结果。

从定性角度来看,我们还可以检查模型在一个句子示例上的表现。在我们的案例中,我们选择了 I learn new things every day,并且获得的输出如下:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne neue Dinge, die in jedem Fall auftreten.

输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习在每种情况下都会出现的新事物,因此,正如结果所示,文本几乎被完美地从英语翻译成了德语。此外,结果与未优化的情况相同(如预期)。

那么,基于 GPU 的推理如何呢?我们来按照相同的步骤操作:

  1. 如果不应用这些优化技术,作为基准,以下是使用 GPU 获得的定量结果:

    WMT16 test loss: 1.53; test bleu score: 26.40
    Time (s): 61.67868137359619
    

    正如预期的那样,相对于 CPU 基线,算法性能没有变化。推理运行时间在 GPU 上确实是 CPU 的六倍快(61.7 秒对比 374 秒)。

  2. 从定性角度来看,我们还可以通过一个句子示例检查我们的模型表现如何。在我们的例子中,我们选择了 I learn new things every day,输出结果如下:

    Qualitative Evaluation: Translating from English to German
    Expected translation:
     Ich lerne neue Dinge.
     In English:
     I learn new things every day.
     The German translation is:
     Ich lerne neue Dinge, die in jedem Fall auftreten.
    

    输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习每天都出现的新事物,因此从结果来看,文本几乎被完美地从英语翻译成了德语(并且与两个 CPU 情况相等)。

正如在前面的步骤中总结的那样,为了在 GPU 上获得最大性能,最佳方法如下:

  • 使用混合化:使用静态内存分配和不变输入形状。不要使用 Intel MKL-DNN 后端。

  • 使用 AMP。

  • 不使用 Int8 量化(不支持)。

不幸的是,由于 GluonNLP 模型不支持 AMP,我们将无法使用 AMP。

让我们将这些技术应用于我们当前的具体任务,从英语翻译成德语。

对于混合化,我们只需要几行代码(包括静态内存分配和不变输入形状所需的参数,以及损失函数的混合化):

wmt_transformer_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
loss_function = nlp.loss.MaskedSoftmaxCELoss()
loss_function.hybridize(static_alloc=True, static_shape=True)

我们不需要添加与 AMP 或 Int8 量化相关的任何步骤,因为 GluonNLP 不支持这些功能。因此,不需要进一步的步骤。

通过应用这些优化技术,以下是优化 GPU 推理后的定量结果:

WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 56.29795598983765

正如我们所看到的,性能差异(损失为 1.53 与 1.53,BLEU 得分为 26.40 与 26.40)是微不足道的。然而,通过这些推理优化技术,我们已经能够将推理运行时间减少了 10%(56.3 秒对比 61.7 秒),这是一个非常好的结果。

从定性角度来看,我们还可以通过一个句子示例检查我们的模型表现如何。在我们的例子中,我们选择了 I learn new things every day,输出结果如下:

Qualitative Evaluation: Translating from English to German
Expected translation:
 Ich lerne neue Dinge.
 In English:
 I learn new things every day.
 The German translation is:
 Ich lerne neue Dinge, die in jedem Fall auftreten.

输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习每天都出现的新事物,因此从结果来看,文本几乎被完美地从英语翻译成了德语。而且,结果与非优化情况(如预期)相当。

对我们的模型进行分析

在前面的章节中,我们看到了可以应用于优化推理循环的不同技术,以及这些技术所取得的结果。然而,这些技术究竟是如何工作的?为什么它们更快?

在本节中,我们将使用 MXNet 分析器,这有助于我们理解在运行时发生的问题。

如初始部分所述,模型性能分析的输出是一个 JSON 文件,可以使用诸如 Google Chrome Tracing 应用等工具进行可视化。对于未优化的 CPU 工作负载,我们的 Transformer 模型显示以下时间分析:

图 9.14 – Transformer 性能分析:未优化的 CPU 工作负载

图 9.14 – Transformer 性能分析:未优化的 CPU 工作负载

图 9.14中,我们可以看到以下特点:

  • 几乎所有的任务都由两个进程处理。

  • 几乎没有等待时间(惰性计算和mx.nd.waitall)。

  • 内存的同步/结构化使用。

  • 所有操作都是原子操作,并且逐个执行。

  • 完整操作大约需要 1,200 毫秒。

对于经过 CPU 优化的工作负载,我们的 Transformer 模型显示以下时间分析:

图 9.15 – Transformer 性能分析:优化后的 CPU 工作负载

图 9.15 – Transformer 性能分析:优化后的 CPU 工作负载

图 9.15中,我们可以看到以下特点:

  • 几乎所有的任务都由两个进程处理,类似于未优化的情况。

  • 几乎没有等待时间(惰性计算和mx.nd.waitall),与未优化的情况类似。

  • 与未优化的情况相比,内存以更异步/结构化的方式使用。

  • 一些操作被融合在一起。尽管可视化不太清晰,但操作符融合(混合化)似乎在起作用,且大部分时间都花费在融合操作上。

  • 完整操作大约需要 720 毫秒。

让我们仔细观察其中一个操作符融合步骤:

图 9.16 – Transformer 性能分析:优化后的 CPU 工作负载(聚焦于 OperatorFusion)

图 9.16 – Transformer 性能分析:优化后的 CPU 工作负载(聚焦于 OperatorFusion)

图 9.16中,我们可以看到操作符融合如何将多个不同的操作融合在一起,包括嵌入、层归一化、全连接层和 MKL-DNN 加速的层。

总结来说,对于基于 CPU 的优化,我们可以清楚地看到以下效果:

  • 混合化已经将大多数操作符融合在一起,尽管可视化较为困难,而且这种情况发生了多次。

  • MKL-DNN 后端通过加速的操作符改进了这些操作。

现在让我们讨论 GPU 情况。

对于未优化的 GPU 工作负载,我们的 Transformer 模型显示以下时间分析:

图 9.17 – Transformer 性能分析:未优化的 GPU 工作负载

图 9.17 – Transformer 性能分析:未优化的 GPU 工作负载

图 9.17中,我们可以看到以下特点:

  • 任务主要由几个(三个)GPU 进程处理。

  • 几乎没有等待时间(惰性计算和mx.nd.waitall)。

  • 内存逐渐增加。

  • 所有操作都是原子操作,并且逐个执行。

  • 有多个从/到 CPU 的拷贝,这似乎没有降低性能。

  • 完整操作大约需要 580 毫秒。

对于 GPU 优化的工作负载,我们的 Transformer 模型展示了以下时间性能:

图 9.18 – 性能分析 Transformer:优化后的 GPU 工作负载

图 9.18 – 性能分析 Transformer:优化后的 GPU 工作负载

图 9.18中,我们可以看到以下特点:

  • 几乎所有任务都由三个进程处理,类似于未优化的版本。

  • 几乎没有等待时间(懒加载和 mx.nd.waitall),与未优化版本类似。

  • 与未优化版本相比,内存的异步/非结构化使用更多。

  • 一些操作被融合在一起。尽管可视化不够清晰,但操作融合(混合化)似乎有效,绝大部分时间都花费在了融合的操作上。

  • 从/到 CPU 的数据复制操作似乎不会影响性能,尽管有几个操作。

  • 整个操作大约需要 260 毫秒。

让我们详细看一下其中一步操作融合的过程:

图 9.19 – 性能分析 Transformer:优化后的 GPU 工作负载(聚焦操作融合)

图 9.19 – 性能分析 Transformer:优化后的 GPU 工作负载(聚焦操作融合)

图 9.19中,我们可以看到操作融合如何将几个不同的操作融合在一起,包括嵌入、层归一化和全连接层。

总结来说,对于基于 GPU 的优化,我们可以清楚地看到混合化的效果,所有操作都已被融合在一起,尽管可视化较难解读,而且这种情况发生了很多次。

导出我们的模型

MXNet 和 GluonNLP 也提供了导出模型的工具。然而,这些工具主要是为 MXNet/Gluon 的内部使用而设计。原因是 GluonNLP 主要处理 save() 函数。

这个函数可以很容易地调用:

wmt_transformer_pt_gpu_hybrid.save('transformer_pt_gpu_hybrid')

我们可以通过以下命令验证与模型相关的文件,参数(.params 扩展名)和架构(.json 扩展名)是否已被保存:

assert os.path.exists("transformer_pt_gpu_hybrid-model.params")
assert os.path.exists("transformer_pt_gpu_hybrid-model.json")

完成了!在这一节中,我们成功地导出了我们的 Transformer 模型。恭喜!

它是如何工作的...

在这个食谱中,我们应用了本章第一个食谱中看到的不同推理优化技术,利用我们的硬件(CPU 和 GPU)通过混合化模型来优化模型的运行性能。

此外,我们已经学习了如何使用 MXNet 性能分析器从低级别的角度分析推理优化。

最后,我们已经学会了如何使用 MXNet 内部库导出我们的模型。

还有更多…

在这个食谱中,我们从训练后的角度展示了推理优化问题。我们得到一个(预)训练好的模型,并尝试从中挤出尽可能多的性能。

然而,还有另一条可以探索的途径,这条途径从机器学习模型设计的角度思考如何最大化推理性能。已经发布了几种改进方法,展示了如何在没有大量计算工作负载的情况下使用大型语言模型(LLM),例如以下几种:

posted @ 2025-07-12 11:41  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报