PyTorch-深度学习指南-全-

PyTorch 深度学习指南(全)

原文:zh.annas-archive.org/md5/057fe0c351c5365f1188d1f44806abda

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PyTorch 因其灵活性和易用性而引起了数据科学专业人士和深度学习从业者的关注。本书介绍了深度学习和 PyTorch 的基本构建模块,并展示了如何使用实用的方法解决实际问题。你还将学习到一些现代架构和技术,用于解决一些前沿研究问题。

本书提供了各种最先进的深度学习架构(如 ResNet、DenseNet、Inception 和 Seq2Seq)的直觉,而不深入数学。它还展示了如何进行迁移学习,如何利用预计算特征加快迁移学习,以及如何使用嵌入、预训练嵌入、LSTM 和一维卷积进行文本分类。

通过本书,你将成为一名熟练的深度学习从业者,能够使用所学的不同技术解决一些商业问题。

本书的受众

本书适合工程师、数据分析师和数据科学家,对深度学习感兴趣,以及那些希望探索和实施 PyTorch 高级算法的人群。了解机器学习有所帮助,但不是必须的。预期具备 Python 编程知识。

本书内容涵盖

第一章,使用 PyTorch 开始深度学习,回顾了人工智能AI)和机器学习的历史,并观察了深度学习的最近发展。我们还将探讨硬件和算法的各种改进如何在不同应用中实现深度学习的巨大成功。最后,我们将介绍由 Facebook 基于 Torch 开发的优秀 PyTorch Python 库。

第二章,神经网络的基本构建模块,讨论了 PyTorch 的各种构建模块,如变量、张量和nn.module,以及它们如何用于开发神经网络。

第三章,深入理解神经网络,涵盖了训练神经网络的不同过程,如数据准备、用于批处理张量的数据加载器、torch.nn包创建网络架构以及使用 PyTorch 损失函数和优化器。

第四章,机器学习基础,涵盖了不同类型的机器学习问题,以及诸如过拟合和欠拟合等挑战。我们还介绍了数据增强、添加 Dropout 以及使用批归一化等不同技术来防止过拟合。

第五章,计算机视觉中的深度学习,讲解了卷积神经网络CNNs)的构建模块,如一维和二维卷积、最大池化、平均池化、基本 CNN 架构、迁移学习以及使用预先卷积特征进行更快训练。

第六章,序列数据和文本的深度学习,涵盖了词嵌入、如何使用预训练的嵌入、RNN、LSTM 和一维卷积进行 IMDB 数据集的文本分类。

第七章,生成网络,讲解如何利用深度学习生成艺术图像,使用 DCGAN 生成新图像,以及使用语言建模生成文本。

第八章,现代网络架构,探讨了现代计算机视觉应用的架构,如 ResNet、Inception 和 DenseNet。我们还将快速介绍编码器-解码器架构,该架构驱动了现代系统,如语言翻译和图像字幕。

第九章,接下来做什么?,总结了我们所学的内容,并探讨如何在深度学习领域保持更新。

要充分利用本书

本书所有章节(除了第一章,使用 PyTorch 入门深度学习和第九章,接下来做什么?)在书籍的 GitHub 仓库中都有关联的 Jupyter Notebooks。为了节省空间,文本中可能未包含代码运行所需的导入。您应该能够从 Notebooks 中运行所有代码。

本书侧重于实际示例,因此在阅读章节时运行 Jupyter Notebooks。

使用带有 GPU 的计算机将有助于快速运行代码。有一些公司,如 paperspace.com 和 www.crestle.com,简化了运行深度学习算法所需的复杂性。

下载示例代码文件

您可以从 www.packtpub.com 的帐户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便直接通过电子邮件获取文件。

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

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

  2. 选择“支持”选项卡。

  3. 单击“代码下载与勘误”。

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

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Deep-Learning-with-PyTorch。如果代码有更新,将在现有的 GitHub 存储库中更新。

我们还有其他来自我们丰富书目和视频的代码包,可在github.com/PacktPublishing/查看!请查看!

下载彩色图像

我们还提供了一个包含本书使用的屏幕截图/图表的彩色图像的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/DeepLearningwithPyTorch_ColorImages.pdf

用法约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:"自定义类必须实现两个主要函数,即__len__(self)__getitem__(self, idx)。"

代码块设置如下:

x,y = get_data() # x - represents training data,y -                 represents target variables

w,b = get_weights() # w,b - Learnable parameters

for i in range(500):
    y_pred = simple_network(x) # function which computes wx + b
    loss = loss_fn(y,y_pred) # calculates sum of the squared differences of y and y_pred

if i % 50 == 0: 
        print(loss)
    optimize(learning_rate) # Adjust w,b to minimize the loss

任何命令行输入或输出都是这样写的:

conda install pytorch torchvision cuda80 -c soumith

粗体:表示新术语、重要单词或屏幕上看到的单词。

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

联系我们

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

总体反馈:请发送电子邮件至feedback@packtpub.com,在主题中提到书名。如果您对本书的任何方面有疑问,请发送电子邮件至questions@packtpub.com

勘误:尽管我们已尽一切努力确保内容的准确性,但错误难免。如果您在本书中发现错误,请告知我们。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请向我们提供位置地址或网站名称。请联系我们,地址为copyright@packtpub.com,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或贡献一本书,请访问authors.packtpub.com

评论

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

有关 Packt 的更多信息,请访问packtpub.com

第一章:使用 PyTorch 入门深度学习

深度学习DL)已经彻底改变了一个又一个行业。安德鲁·吴曾在 Twitter 上著名地描述过:

人工智能是新的电力!

电力改变了无数行业;人工智能AI)现在将做同样的事情。

AI 和 DL 被用作同义词,但两者之间存在重大差异。让我们揭开行业术语的神秘面纱,这样作为从业者的你将能够区分信号和噪音。

在本章中,我们将涵盖以下 AI 的不同部分:

  • AI 本身及其起源

  • 在现实世界中的机器学习

  • 深度学习的应用

  • 为什么现在是深度学习的时代?

  • 深度学习框架:PyTorch

人工智能

每天都有无数篇讨论 AI 的文章发布。这种趋势在过去两年中有所增加。网络上流传着几种 AI 的定义,我最喜欢的是自动执行通常由人类执行的智力任务

AI 的历史

人工智能这个术语最初由约翰·麦卡锡在 1956 年首次创造,当时他举办了第一届学术会议。关于机器是否会思考的问题的旅程比这早得多。在 AI 的早期阶段,机器能够解决人类难以解决的问题。

例如,恩尼格玛机器是在二战结束时建造的,用于军事通信。艾伦·图灵建造了一个 AI 系统,帮助破译恩尼格玛密码。破译恩尼格玛密码对人类来说是一个非常具有挑战性的任务,分析人员可能需要数周的时间。AI 机器能够在几小时内破译该密码。

计算机难以解决我们直觉上理解的问题,比如区分狗和猫,判断朋友是否因为你迟到而对你生气(情感),区分卡车和汽车,参加研讨会时做笔记(语音识别),或为不理解你的语言的朋友转换笔记(例如,从法语到英语)。大多数这些任务对我们来说都很直观,但我们无法编程或硬编码计算机来执行这些任务。早期 AI 机器的大多数智能是硬编码的,比如一个计算机程序来玩国际象棋。

在 AI 的早期年代,许多研究人员认为可以通过硬编码规则实现 AI。这种类型的 AI 称为符号 AI,在解决定义良好的逻辑问题方面很有用,但几乎无法解决复杂的问题,如图像识别、物体检测、物体分割、语言翻译和自然语言理解任务。为了解决这些问题,开发了新的 AI 方法,如机器学习和深度学习。

为了更好地理解 AI、ML 和 DL 之间的关系,让我们将它们想象成同心圆。AI——最先提出的概念(最大的圆),然后是机器学习——稍后发展起来的(位于更大圆的内部),最后是 DL——驱动今天 AI 爆炸的(在两者之内):

如何 AI、机器学习和深度学习相互配合

机器学习

机器学习ML)是 AI 的一个子领域,在过去 10 年变得流行,并且有时两者可以互换使用。AI 除了机器学习之外还有许多其他子领域。ML 系统通过展示大量示例来构建,与符号 AI 不同,后者在构建系统时硬编码规则。在高层次上,机器学习系统查看大量数据并提出规则,以预测未见数据的结果:

机器学习与传统编程的比较

大多数 ML 算法在结构化数据上表现良好,比如销售预测、推荐系统和营销个性化。对于任何 ML 算法来说,特征工程是一个重要因素,数据科学家需要花费大量时间来正确获取 ML 算法所需的特征。在某些领域,如计算机视觉和自然语言处理NLP),特征工程具有挑战性,因为它们受到高维度的影响。

直到最近,这类问题对于使用典型的机器学习技术(如线性回归、随机森林等)来解决的组织来说是具有挑战性的,原因包括特征工程和高维度。考虑一幅大小为 224 x 224 x 3(高度 x 宽度 x 通道)的图像,图像尺寸中的3代表彩色图像中红色、绿色和蓝色通道的值。要将此图像存储在计算机内存中,我们的矩阵将包含每个图像 150,528 个维度。假设您想在大小为 224 x 224 x 3 的 1,000 幅图像上构建分类器,维度将变为 1,000 倍的 150,528。一种名为深度学习的机器学习特殊分支允许您使用现代技术和硬件处理这些问题。

生活中机器学习的例子

以下是一些由机器学习驱动的酷产品:

  • 示例 1:Google Photos 使用一种特定形式的机器学习,称为深度学习来对照片进行分组

  • 示例 2:推荐系统是 ML 算法家族的一部分,用于推荐电影、音乐和产品,像 Netflix、Amazon 和 iTunes 这样的大公司。

深度学习

传统 ML 算法使用手写特征提取来训练算法,而 DL 算法使用现代技术以自动方式提取这些特征。

例如,一个深度学习算法预测图像是否包含人脸,提取特征如第一层检测边缘,第二层检测形状如鼻子和眼睛,最后一层检测面部形状或更复杂的结构。每一层都基于前一层对数据的表示进行训练。如果你觉得这个解释难以理解,书的后面章节将帮助你直观地构建和检查这样的网络:

可视化中间层的输出(图片来源:https://www.cs.princeton.edu/~rajeshr/papers/cacm2011-researchHighlights-convDBN.pdf)

近年来,随着 GPU、大数据、云服务提供商如亚马逊网络服务AWS)和 Google Cloud 的兴起,以及 Torch、TensorFlow、Caffe 和 PyTorch 等框架,深度学习的应用大幅增长。此外,大公司分享在庞大数据集上训练的算法,从而帮助初创公司在多个用例上轻松构建最先进的系统。

深度学习的应用

通过深度学习实现的一些热门应用包括:

  • 近乎人类水平的图像分类

  • 近乎人类水平的语音识别

  • 机器翻译

  • 自动驾驶汽车

  • Siri、Google Voice 和 Alexa 近年来变得更加准确

  • 一位日本农民正在分类黄瓜

  • 肺癌检测

  • 超过人类水平精度的语言翻译

下面的截图展示了一个简短的总结示例,计算机将大段文本进行概括,用几行来呈现:

由计算机生成的样本段落摘要

在下图中,计算机被给予一张普通的图像,没有告诉它显示的是什么,利用物体检测和字典的帮助,你得到一张图像标题,说两个年轻女孩正在玩乐高玩具。这不是很棒吗?

物体检测和图像标题(图片来源:https://cs.stanford.edu/people/karpathy/cvpr2015.pdf)

与深度学习相关的炒作

媒体和 AI 领域外的人,或者那些不是真正的 AI 和深度学习从业者的人,一直在暗示像《终结者 2:审判日》的情节可能会随着 AI/DL 的进步成为现实。他们中的一些人甚至谈论到一个时代,我们将被机器人控制,机器人决定什么对人类有益。目前,AI 的能力被夸大到远远超出其真实能力的程度。目前,大多数深度学习系统在非常受控制的环境中部署,并且给出了有限的决策边界。

我的猜测是,当这些系统能够学会做出智能决策时,而不仅仅是完成模式匹配时,当数百或数千个深度学习算法能够共同工作时,也许我们可以期待看到像科幻电影中那样的机器人。实际上,我们离得到机器能够在没有被告知的情况下做任何事情的普遍人工智能还很遥远。当前的深度学习状态更多地是关于从现有数据中找到模式以预测未来结果。作为深度学习从业者,我们需要区分信号和噪音。

深度学习的历史

尽管近年来深度学习变得流行,但深度学习背后的理论自 20 世纪 50 年代以来一直在发展。以下表格展示了今天在 DL 应用中使用的一些最流行的技术及其大致时间表:

技术 年份
神经网络 1943 年
反向传播 1960 年代早期
卷积神经网络 1979
循环神经网络 1980 年
长短期记忆 1997 年

多年来,深度学习被赋予了几个名字。在 1970 年代被称为控制论,在 1980 年代被称为连接主义,现在则通常称为深度学习神经网络。我们将 DL 和神经网络互换使用。神经网络通常被称为受人类大脑工作启发的算法。然而,作为 DL 从业者,我们需要理解,它主要受数学(线性代数和微积分)、统计学(概率)和软件工程的强大理论支持。

为什么现在?

为什么 DL 现在如此流行?一些关键原因如下:

  • 硬件可用性

  • 数据与算法

  • 深度学习框架

硬件可用性

深度学习需要在数百万、有时甚至数十亿个参数上执行复杂的数学操作。现有的 CPU 执行这些操作需要很长时间,尽管在过去几年已有所改进。一种新型的硬件称为图形处理单元GPU)能够以数量级更快的速度完成这些大规模的数学运算,如矩阵乘法。

最初,GPU 是由 Nvidia 和 AMD 等公司为游戏行业构建的。事实证明,这种硬件不仅在渲染高质量视频游戏时非常有效,还能加速 DL 算法。最近 Nvidia 推出的一款 GPU,1080ti,仅需几天即可在ImageNet数据集上构建出图像分类系统,而此前可能需要大约一个月。

如果你计划购买用于运行深度学习的硬件,我建议根据预算选择一款来自 Nvidia 的 GPU。根据你的预算选择一款内存足够的 GPU。请记住,你的计算机内存和 GPU 内存是两回事。1080ti 配备了 11 GB 的内存,价格约为 700 美元。

您还可以使用 AWS、Google Cloud 或 Floyd(该公司提供专为深度学习优化的 GPU 机器)等各种云服务提供商。如果您刚开始学习深度学习或者为组织使用设置机器时具有更多的财务自由度,使用云服务提供商是经济的选择。

如果这些系统经过优化,性能可能会有所变化。

下图显示了 CPU 和 GPU 之间性能比较的一些基准:

在 CPU 和 GPU 上神经架构的性能基准(图片来源:http://cs231n.stanford.edu/slides/2017/cs231n_2017_lecture8.pdf)

数据和算法

数据是深度学习成功的最重要组成部分。由于互联网的广泛采用和智能手机的增长使用,一些公司(如 Facebook 和 Google)已经能够收集大量数据,包括文本、图片、视频和音频等多种格式。在计算机视觉领域,ImageNet 竞赛在提供了包括 1,000 个类别的 1.4 百万张图像数据集方面发挥了巨大作用。

这些类别是手动注释的,每年有数百个团队参加竞争。在竞赛中成功的一些算法包括 VGG、ResNet、Inception、DenseNet 等。这些算法今天在行业中用于解决各种计算机视觉问题。在深度学习领域中经常用来对比各种算法性能的其他流行数据集如下:

  • MNIST

  • COCO 数据集

  • CIFAR

  • 街景房屋数字

  • PASCAL VOC

  • Wikipedia 的数据集

  • 20 个新闻组

  • Penn Treebank

  • Kaggle

最近几年不同算法的发展,如批归一化、激活函数、跳跃连接、长短期记忆网络(LSTM)、dropout 等,使得能够更快、更成功地训练非常深的网络。在本书的接下来章节中,我们将详细讨论每个技术以及它们如何帮助构建更好的模型。

深度学习框架

在早期,人们需要具备 C++和 CUDA 的专业知识来实现深度学习算法。随着许多组织现在开源他们的深度学习框架,具备脚本语言(如 Python)知识的人员就可以开始构建和使用深度学习算法。今天在行业中使用的一些流行的深度学习框架包括 TensorFlow、Caffe2、Keras、Theano、PyTorch、Chainer、DyNet、MXNet 和 CNTK。

如果没有这些框架的存在,深度学习的采用不可能会如此巨大。它们抽象了很多底层复杂性,使我们能够专注于应用。我们仍处于深度学习的早期阶段,在各公司和组织中每天都有许多研究突破。因此,各种框架都有其优缺点。

PyTorch

PyTorch 以及大多数其他深度学习框架,可以用于两种不同的目的:

  • 使用 GPU 加速操作替代类似 NumPy 的操作

  • 构建深度神经网络

使 PyTorch 日益流行的原因是其易用性和简单性。与大多数其他流行的深度学习框架使用静态计算图不同,PyTorch 使用动态计算,允许更大的灵活性来构建复杂的架构。

PyTorch 广泛使用 Python 概念,如类、结构和条件循环,使我们能够以纯面向对象的方式构建深度学习算法。大多数其他流行的框架带来了它们自己的编程风格,有时使编写新算法复杂化,并且不支持直观的调试。在后续章节中,我们将详细讨论计算图。

虽然 PyTorch 最近发布并且仍处于 beta 版本阶段,但由于其易用性、更好的性能、易于调试的特性以及来自 SalesForce 等各种公司的强大支持,它已经在数据科学家和深度学习研究人员中广受欢迎。

由于 PyTorch 主要是为研究而构建的,在某些对延迟要求非常高的情况下,不推荐用于生产环境。然而,随着一个名为Open Neural Network ExchangeONNX)的新项目的推出(onnx.ai/),情况正在发生变化,该项目致力于将在 PyTorch 上开发的模型部署到像 Caffe2 这样的生产就绪平台。在撰写本文时,关于这个项目还为时过早。该项目由 Facebook 和 Microsoft 支持。

在本书的其余部分,我们将学习关于在计算机视觉和自然语言处理领域构建强大深度学习应用程序的各种乐高积木(更小的概念或技术)。

摘要

在本章的介绍中,我们探讨了人工智能、机器学习和深度学习的定义,并讨论了它们之间的区别。我们还看了它们在日常生活中的应用。我们深入探讨了为什么深度学习现在才变得更加流行。最后,我们对 PyTorch 进行了初步介绍,这是一个深度学习框架。

在下一章中,我们将在 PyTorch 中训练我们的第一个神经网络。

第二章:神经网络的构建模块

理解神经网络的基本构建模块,如张量、张量操作和梯度下降,对于构建复杂的神经网络至关重要。在本章中,我们将通过以下主题构建我们的第一个Hello world神经网络程序:

  • 安装 PyTorch

  • 实现我们的第一个神经网络

  • 将神经网络拆分为功能块

  • 逐步了解每个基础模块,涵盖张量、变量、自动微分、梯度和优化器

  • 使用 PyTorch 加载数据

安装 PyTorch

PyTorch 可作为 Python 包使用,您可以选择使用pipconda来安装,或者您可以从源代码构建。本书推荐的方法是使用 Anaconda Python 3 发行版。要安装 Anaconda,请参考 Anaconda 官方文档 conda.io/docs/user-guide/install/index.html。所有示例将作为 Jupyter Notebook 提供在本书的 GitHub 仓库中。我强烈建议您使用 Jupyter Notebook,因为它允许您进行交互式实验。如果您已经安装了 Anaconda Python,则可以按照以下步骤安装 PyTorch。

基于 CUDA 8 的 GPU 安装

conda install pytorch torchvision cuda80 -c soumith

基于 CUDA 7.5 的 GPU 安装:

conda install pytorch torchvision -c soumith

非 GPU 安装:

conda install pytorch torchvision -c soumith

在撰写本文时,PyTorch 不支持 Windows 操作系统,因此您可以尝试使用虚拟机VM)或 Docker 镜像。

我们的第一个神经网络

我们展示了我们的第一个神经网络,它学习如何将训练样本(输入数组)映射到目标(输出数组)。假设我们为最大的在线公司之一奇妙电影工作,该公司提供视频点播服务。我们的训练数据集包含一个特征,代表用户在平台上观看电影的平均时间,我们想预测每个用户在未来一周内在平台上的使用时间。这只是一个虚构的用例,不要过于深思。构建这样一个解决方案的一些高级活动如下:

  • 数据准备get_data 函数准备包含输入和输出数据的张量(数组)。

  • 创建可学习参数get_weights 函数提供了包含随机值的张量,我们将优化以解决问题

  • 网络模型simple_network 函数为输入数据生成输出,应用线性规则,将权重与输入数据相乘,并添加偏差项(y = Wx+b

  • 损失loss_fn 函数提供了关于模型性能的信息

  • 优化器optimize 函数帮助我们调整最初创建的随机权重,以帮助模型更准确地计算目标值

如果您是机器学习的新手,不用担心,因为我们将在本章结束时准确了解每个函数的功能。以下函数将 PyTorch 代码抽象化,以便更容易理解。我们将详细探讨每个功能的细节。上述高级活动对大多数机器学习和深度学习问题都很常见。本书后面的章节讨论了用于改进每个功能以构建有用应用程序的技术。

让我们考虑我们神经网络的线性回归方程:

让我们在 PyTorch 中编写我们的第一个神经网络:

x,y = get_data() # x - represents training data,y -                 represents target variables

w,b = get_weights() # w,b - Learnable parameters

for i in range(500):
    y_pred = simple_network(x) # function which computes wx + b
    loss = loss_fn(y,y_pred) # calculates sum of the squared differences of y and y_pred

if i % 50 == 0: 
        print(loss)
    optimize(learning_rate) # Adjust w,b to minimize the loss

在本章末尾,您将对每个函数内部发生的情况有所了解。

数据准备

PyTorch 提供了称为张量变量的两种数据抽象。张量类似于numpy数组,可以在 GPU 上使用,提供了增强的性能。它们提供了在 GPU 和 CPU 之间轻松切换的方法。对于某些操作,我们可以注意到性能的提升,并且只有当表示为数字张量时,机器学习算法才能理解不同形式的数据。张量类似于 Python 数组,并且可以改变大小。例如,图像可以表示为三维数组(高度、宽度、通道(RGB))。在深度学习中使用大小高达五维的张量是很常见的。一些常用的张量如下:

  • 标量(0-D 张量)

  • 向量(1-D 张量)

  • 矩阵(2-D 张量)

  • 3-D 张量

  • 切片张量

  • 4-D 张量

  • 5-D 张量

  • GPU 上的张量

标量(0-D 张量)

只包含一个元素的张量称为标量。通常会是FloatTensorLongTensor类型。在撰写本文时,PyTorch 没有零维特殊张量。因此,我们使用一个具有一个元素的一维张量,如下所示:

x = torch.rand(10)
x.size()

Output - torch.Size([10])

向量(1-D 张量)

向量只是一个元素数组。例如,我们可以使用一个向量来存储上周的平均温度:

temp = torch.FloatTensor([23,24,24.5,26,27.2,23.0])
temp.size()

Output - torch.Size([6])

矩阵(2-D 张量)

大多数结构化数据以表格或矩阵形式表示。我们将使用名为Boston House Prices的数据集,它在 Python scikit-learn 机器学习库中已经准备好。数据集是一个numpy数组,包含506个样本或行和13个特征,每个样本表示一个。Torch 提供了一个实用函数from_numpy(),它将numpy数组转换为torch张量。结果张量的形状是506行 x 13列:

boston_tensor = torch.from_numpy(boston.data)
boston_tensor.size()

Output: torch.Size([506, 13])

boston_tensor[:2]

Output:
Columns 0 to 7 
   0.0063 18.0000 2.3100 0.0000 0.5380 6.5750 65.2000 4.0900
   0.0273 0.0000 7.0700 0.0000 0.4690 6.4210 78.9000 4.9671

Columns 8 to 12 
   1.0000 296.0000 15.3000 396.9000 4.9800
   2.0000 242.0000 17.8000 396.9000 9.1400
[torch.DoubleTensor of size 2x13]

3-D 张量

当我们将多个矩阵相加时,我们得到一个 3-D 张量。3-D 张量用于表示类似图像的数据。图像可以表示为矩阵中的数字,这些数字被堆叠在一起。图像形状的一个例子是 2242243,其中第一个索引表示高度,第二个表示宽度,第三个表示通道(RGB)。让我们看看计算机如何使用下一个代码片段看到一只熊猫:

from PIL import Image
# Read a panda image from disk using a library called PIL and convert it to numpy array
panda = np.array(Image.open('panda.jpg').resize((224,224)))
panda_tensor = torch.from_numpy(panda)
panda_tensor.size()

Output - torch.Size([224, 224, 3])
#Display panda
plt.imshow(panda)

由于显示大小为 2242243 的张量会占据书中的几页,我们将显示图像并学习如何将图像切成较小的张量以进行可视化:

显示图像

切片张量

对张量进行切片是常见的操作。一个简单的例子可能是选择一维张量 sales 的前五个元素;我们使用简单的表示法 sales[:slice_index],其中 slice_index 表示要切片张量的索引:

sales = torch.FloatTensor([1000.0,323.2,333.4,444.5,1000.0,323.2,333.4,444.5])

sales[:5]
 1000.0000
  323.2000
  333.4000
  444.5000
 1000.0000
[torch.FloatTensor of size 5]

sales[:-5]
 1000.0000
  323.2000
  333.4000
[torch.FloatTensor of size 3]

让我们用熊猫图像做更有趣的事情,比如看看当只选择一个通道时熊猫图像是什么样子,以及如何选择熊猫的脸部。

在这里,我们从熊猫图像中选择了一个通道:

plt.imshow(panda_tensor[:,:,0].numpy())
#0 represents the first channel of RGB

输出如下所示:

现在,让我们裁剪图像。假设我们要构建一个熊猫的面部检测器,我们只需要熊猫的面部。我们裁剪张量图像,使其仅包含熊猫的面部:

plt.imshow(panda_tensor[25:175,60:130,0].numpy())

输出如下所示:

另一个常见的例子是需要选择张量的特定元素:

#torch.eye(shape) produces an diagonal matrix with 1 as it diagonal #elements.
sales = torch.eye(3,3)
sales[0,1]

Output- 0.00.0

我们将在第五章,深度学习用于计算机视觉中重新讨论图像数据时,讨论使用 CNN 构建图像分类器。

大多数 PyTorch 张量操作与 NumPy 操作非常相似。

4-D 张量

四维张量类型的一个常见例子是图像批次。现代 CPU 和 GPU 都经过优化,可以更快地在多个示例上执行相同的操作。因此,它们处理一张图像或一批图像的时间相似。因此,常见的做法是使用一批示例而不是逐个使用单个图像。选择批次大小并不简单;它取决于多个因素。使用更大的批次或完整数据集的一个主要限制是 GPU 内存限制—163264 是常用的批次大小。

让我们看一个例子,我们加载一个大小为 64 x 224 x 224 x 3 的猫图像批次,其中 64 表示批次大小或图像数量,244 表示高度和宽度,3 表示通道:

#Read cat images from disk
cats = glob(data_path+'*.jpg')
#Convert images into numpy arrays
cat_imgs = np.array([np.array(Image.open(cat).resize((224,224))) for cat in cats[:64]]) 
cat_imgs = cat_imgs.reshape(-1,224,224,3)
cat_tensors = torch.from_numpy(cat_imgs)
cat_tensors.size()

Output - torch.Size([64, 224, 224, 3])

5-D 张量

一个常见的例子是,您可能需要使用五维张量来处理视频数据。视频可以分割成帧,例如,一个 30 秒的视频包含一个熊猫和一个球玩耍的视频可能包含 30 帧,可以表示为形状为(1 x 30 x 224 x 224 x 3)的张量。一批这样的视频可以表示为形状为(32 x 30 x 224 x 224 x 3)的张量,30在这个例子中表示单个视频剪辑中的帧数,其中32表示这样的视频剪辑数量。

GPU 上的张量

我们已经学习了如何在张量表示中表示不同形式的数据。一旦数据以张量形式存在,我们执行的一些常见操作包括加法、减法、乘法、点积和矩阵乘法。所有这些操作可以在 CPU 或 GPU 上执行。PyTorch 提供了一个简单的函数叫做cuda()来将一个在 CPU 上的张量复制到 GPU 上。我们将看一些操作并比较在 CPU 和 GPU 上矩阵乘法操作的性能。

张量加法可以通过以下代码获得:

#Various ways you can perform tensor addition
a = torch.rand(2,2) 
b = torch.rand(2,2)
c = a + b
d = torch.add(a,b)
#For in-place addition
a.add_(5)

#Multiplication of different tensors

a*b
a.mul(b)
#For in-place multiplication
a.mul_(b)

对于张量矩阵乘法,我们比较在 CPU 和 GPU 上的代码性能。任何张量可以通过调用.cuda()函数移动到 GPU 上。

GPU 上的乘法运行如下:

a = torch.rand(10000,10000)
b = torch.rand(10000,10000)

a.matmul(b)

Time taken: 3.23 s

#Move the tensors to GPU
a = a.cuda()
b = b.cuda()

a.matmul(b)

Time taken: 11.2 µs

这些基本操作包括加法、减法和矩阵乘法,可以用来构建复杂的操作,比如卷积神经网络CNN)和循环神经网络RNN),这些我们将在本书的后面章节学习。

变量

深度学习算法通常表示为计算图。这里是我们在例子中构建的变量计算图的简单示例:

变量计算图

在上述计算图中,每个圆圈表示一个变量。一个变量围绕一个张量对象、它的梯度和创建它的函数引用形成一个薄包装。下图展示了Variable类的组成部分:

变量类

梯度指的是loss函数相对于各个参数(Wb)的变化率。例如,如果a的梯度为 2,那么a值的任何变化都将使Y值增加两倍。如果这不清楚,不要担心——大多数深度学习框架会帮我们计算梯度。在本章中,我们将学习如何利用这些梯度来提高模型的性能。

除了梯度外,变量还有一个指向创建它的函数的引用,该函数反过来指向如何创建每个变量。例如,变量a包含它是由XW的乘积生成的信息。

让我们看一个例子,我们在其中创建变量并检查梯度和函数引用:

x = Variable(torch.ones(2,2),requires_grad=True)
y = x.mean()

y.backward()

x.grad
Variable containing:
 0.2500  0.2500
 0.2500  0.2500
[torch.FloatTensor of size 2x2]

x.grad_fn
Output - None

x.data
 1 1
 1 1
[torch.FloatTensor of size 2x2]

y.grad_fn
<torch.autograd.function.MeanBackward at 0x7f6ee5cfc4f8>

在前面的例子中,我们对变量执行了backward操作以计算梯度。默认情况下,变量的梯度为 none。

变量的grad_fn指向它创建的函数。如果变量是用户创建的,例如我们的变量x,那么函数引用为None。对于变量y,它指向其函数引用,MeanBackward

数据属性访问与变量相关联的张量。

为我们的神经网络创建数据

我们第一个神经网络代码中的get_data函数创建了两个变量xy,大小分别为(171)和(17)。我们将看一下函数内部发生了什么:

def get_data():
    train_X = np.asarray([3.3,4.4,5.5,6.71,6.93,4.168,9.779,6.182,7.59,2.167,
                         7.042,10.791,5.313,7.997,5.654,9.27,3.1])
    train_Y = np.asarray([1.7,2.76,2.09,3.19,1.694,1.573,3.366,2.596,2.53,1.221,
                         2.827,3.465,1.65,2.904,2.42,2.94,1.3])
    dtype = torch.FloatTensor
    X = Variable(torch.from_numpy(train_X).type(dtype),requires_grad=False).view(17,1)
    y = Variable(torch.from_numpy(train_Y).type(dtype),requires_grad=False)
    return X,y

创建可学习参数

在我们的神经网络示例中,我们有两个可学习参数,wb,以及两个固定参数,xy。我们在get_data函数中创建了变量xy。可学习参数是使用随机初始化创建的,并且require_grad参数设置为True,而xy的设置为False。有不同的实践方法用于初始化可学习参数,我们将在接下来的章节中探讨。让我们看一下我们的get_weights函数:

def get_weights():
    w = Variable(torch.randn(1),requires_grad = True)
    b = Variable(torch.randn(1),requires_grad=True)
    return w,b

大部分前面的代码都是不言自明的;torch.randn创建给定形状的随机值。

神经网络模型

一旦我们使用 PyTorch 变量定义了模型的输入和输出,我们必须构建一个模型,该模型学习如何映射输出到输入。在传统编程中,我们通过手工编码不同的逻辑来构建函数,将输入映射到输出。然而,在深度学习和机器学习中,我们通过向其展示输入和关联输出来学习函数。在我们的例子中,我们实现了一个简单的神经网络,试图将输入映射到输出,假设是线性关系。线性关系可以表示为y = wx + b,其中wb是可学习参数。我们的网络必须学习wb的值,以便wx + b更接近实际y。让我们可视化我们的训练数据集和我们的神经网络必须学习的模型:

输入数据点

以下图表示在输入数据点上拟合的线性模型:

在输入数据点上拟合的线性模型

图像中的深灰色(蓝色)线代表我们的网络学到的模型。

网络实现

由于我们有所有参数(xwby)来实现网络,我们对wx进行矩阵乘法。然后,将结果与b相加。这将给出我们预测的y。函数实现如下:

def simple_network(x):
    y_pred = torch.matmul(x,w)+b
    return y_pred

PyTorch 还提供了一个名为 torch.nn 的更高级抽象,称为,它将处理大多数神经网络中可用的常见技术的初始化和操作。我们使用较低级别的操作来理解这些函数内部发生的情况。在以后的章节中,即 第五章,计算机视觉的深度学习和 第六章,序列数据和文本的深度学习,我们将依赖于 PyTorch 抽象来构建复杂的神经网络或函数。前面的模型可以表示为一个 torch.nn 层,如下所示:

f = nn.Linear(17,1) # Much simpler.

现在我们已经计算出了 y 值,我们需要知道我们的模型有多好,这是在 loss 函数中完成的。

损失函数

由于我们从随机值开始,我们的可学习参数 wb 会导致 y_pred,它与实际的 y 差距很大。因此,我们需要定义一个函数,告诉模型其预测与实际值的接近程度。由于这是一个回归问题,我们使用一个称为平方误差和SSE)的损失函数。我们取预测的 y 与实际 y 的差值并求平方。SSE 帮助模型理解预测值与实际值的接近程度。torch.nn 库提供了不同的损失函数,如 MSELoss 和交叉熵损失。然而,在本章中,让我们自己实现 loss 函数:

def loss_fn(y,y_pred):
    loss = (y_pred-y).pow(2).sum()
    for param in [w,b]:
        if not param.grad is None: param.grad.data.zero_()
    loss.backward()
    return loss.data[0]

除了计算损失之外,我们还调用 backward 操作来计算我们可学习参数 wb 的梯度。由于我们将多次使用 loss 函数,因此通过调用 grad.data.zero_() 操作来删除先前计算的任何梯度。第一次调用 backward 函数时,梯度为空,因此只有在梯度不为 None 时才将梯度清零。

优化神经网络

我们从随机权重开始预测我们的目标,并为我们的算法计算损失。通过在最终 loss 变量上调用 backward 函数来计算梯度。整个过程在一个 epoch 中重复进行,即整个示例集。在大多数实际示例中,我们将在每次迭代中执行优化步骤,这是总集的一个小子集。一旦计算出损失,我们就用计算出的梯度优化值,使损失减少,这在下面的函数中实现:

def optimize(learning_rate):
    w.data -= learning_rate * w.grad.data
    b.data -= learning_rate * b.grad.data

学习率是一个超参数,它允许我们通过梯度的微小变化来调整变量的值,其中梯度表示每个变量(wb)需要调整的方向。

不同的优化器,如 Adam、RmsProp 和 SGD,已经在 torch.optim 包中实现供后续章节使用以减少损失或提高精度。

加载数据

为深度学习算法准备数据本身可能是一个复杂的流水线。PyTorch 提供许多实用类,通过多线程实现数据并行化、数据增强和批处理等复杂性抽象化。在本章中,我们将深入了解两个重要的实用类,即Dataset类和DataLoader类。要了解如何使用这些类,让我们从 Kaggle 的Dogs vs. Cats数据集(www.kaggle.com/c/dogs-vs-cats/data)入手,创建一个数据流水线,以生成 PyTorch 张量形式的图像批次。

数据集类

任何自定义数据集类,例如我们的Dogs数据集类,都必须继承自 PyTorch 数据集类。自定义类必须实现两个主要函数,即__len__(self)__getitem__(self, idx)。任何作为Dataset类的自定义类应如以下代码片段所示:

from torch.utils.data import Dataset
class DogsAndCatsDataset(Dataset):
    def __init__(self,):
        pass
    def __len__(self):
        pass
    def __getitem__(self,idx):
        pass

我们在init方法内进行任何初始化(如果需要),例如读取表的索引和图像文件名,在我们的情况下。__len__(self)操作负责返回数据集中的最大元素数。__getitem__(self, idx)操作每次调用时根据索引返回一个元素。以下代码实现了我们的DogsAndCatsDataset类:

class DogsAndCatsDataset(Dataset):

    def __init__(self,root_dir,size=(224,224)):
        self.files = glob(root_dir)
        self.size = size 

    def __len__(self):
        return len(self.files)

    def __getitem__(self,idx):
        img = np.asarray(Image.open(self.files[idx]).resize(self.size))
        label = self.files[idx].split('/')[-2]
        return img,label

一旦创建了DogsAndCatsDataset类,我们就可以创建一个对象并对其进行迭代,如下所示:

for image,label in dogsdset:
#Apply your DL on the dataset.

在单个数据实例上应用深度学习算法并不理想。我们需要一批数据,因为现代 GPU 在批处理数据上执行时能够提供更好的性能优化。DataLoader类通过抽象化大量复杂性来帮助创建批次。

数据加载器类

PyTorch 的utils类中的DataLoader类结合了数据集对象和不同的采样器,例如SequentialSamplerRandomSampler,并提供了一个图像批次,使用单进程或多进程迭代器。采样器是为算法提供数据的不同策略。以下是我们的Dogs vs. Cats数据集的DataLoader示例:

dataloader = DataLoader(dogsdset,batch_size=32,num_workers=2)
for imgs , labels in dataloader:
     #Apply your DL on the dataset.
     pass

imgs将包含形状为(32, 224, 224, 3)的张量,其中32表示批处理大小。

PyTorch 团队还维护了两个有用的库,称为torchvisiontorchtext,它们构建在DatasetDataLoader类之上。我们将在相关章节中使用它们。

摘要

在本章中,我们探讨了由 PyTorch 提供的各种数据结构和操作。我们使用 PyTorch 的基本组件实现了几个部分。对于我们的数据准备,我们创建了算法使用的张量。我们的网络架构是一个模型,用于学习预测用户在我们的 Wondermovies 平台上平均花费的小时数。我们使用损失函数来检查我们模型的标准,并使用optimize函数来调整模型的可学习参数,使其表现更好。

我们还探讨了 PyTorch 如何通过抽象化处理数据管道的多个复杂性,这些复杂性原本需要我们进行数据并行化和增强。

在下一章中,我们将深入探讨神经网络和深度学习算法的工作原理。我们将探索用于构建网络架构、损失函数和优化的各种 PyTorch 内置模块。我们还将展示如何在真实世界数据集上使用它们。

第三章:深入研究神经网络

在本章中,我们将探索用于解决真实世界问题的深度学习架构的不同模块。在前一章中,我们使用 PyTorch 的低级操作来构建模块,如网络架构、损失函数和优化器。在本章中,我们将探讨解决实际问题所需的神经网络的重要组件,以及 PyTorch 通过提供大量高级功能来抽象掉许多复杂性。在本章末尾,我们将构建解决回归、二元分类和多类分类等真实世界问题的算法。

在本章中,我们将讨论以下主题:

  • 深入探讨神经网络的各种构建模块

  • 探索 PyTorch 中的高级功能,构建深度学习架构

  • 将深度学习应用于真实世界的图像分类问题

深入探讨神经网络的构建模块

正如我们在前一章学到的,训练深度学习算法需要以下步骤:

  1. 构建数据流水线

  2. 构建网络架构

  3. 使用损失函数评估架构

  4. 使用优化算法优化网络架构权重

在前一章中,网络由使用 PyTorch 数值操作构建的简单线性模型组成。尽管使用数值操作为玩具问题构建神经架构更容易,但当我们尝试构建解决不同领域(如计算机视觉和自然语言处理)复杂问题所需的架构时,情况很快变得复杂。大多数深度学习框架(如 PyTorch、TensorFlow 和 Apache MXNet)提供高级功能,抽象掉许多这种复杂性。这些高级功能在深度学习框架中被称为。它们接受输入数据,应用类似于我们在前一章中看到的转换,并输出数据。为了解决真实世界问题,深度学习架构由 1 到 150 个层组成,有时甚至更多。通过提供高级函数来抽象低级操作和训练深度学习算法的过程如下图所示:

总结前一张图,任何深度学习训练都涉及获取数据,构建一般获取一堆层的架构,使用损失函数评估模型的准确性,然后通过优化权重优化算法。在解决一些真实世界问题之前,我们将了解 PyTorch 提供的用于构建层、损失函数和优化器的更高级抽象。

层 - 神经网络的基本构建块

在本章的其余部分中,我们将遇到不同类型的层次。首先,让我们尝试理解最重要的层之一,即线性层,它与我们之前的网络架构完全一样。线性层应用线性变换:

其强大之处在于我们在前一章中编写的整个函数可以用一行代码表示,如下所示:

from torch.nn import Linear
myLayer = Linear(in_features=10,out_features=5,bias=True)

在上述代码中,myLayer将接受大小为10的张量,并在应用线性变换后输出大小为5的张量。让我们看一个如何做到这一点的简单示例:

inp = Variable(torch.randn(1,10))
myLayer = Linear(in_features=10,out_features=5,bias=True)
myLayer(inp)

我们可以使用weightsbias属性访问层的可训练参数:

myLayer.weight

Output :
Parameter containing:
-0.2386 0.0828 0.2904 0.3133 0.2037 0.1858 -0.2642 0.2862 0.2874 0.1141
 0.0512 -0.2286 -0.1717 0.0554 0.1766 -0.0517 0.3112 0.0980 -0.2364 -0.0442
 0.0776 -0.2169 0.0183 -0.0384 0.0606 0.2890 -0.0068 0.2344 0.2711 -0.3039
 0.1055 0.0224 0.2044 0.0782 0.0790 0.2744 -0.1785 -0.1681 -0.0681 0.3141
 0.2715 0.2606 -0.0362 0.0113 0.1299 -0.1112 -0.1652 0.2276 0.3082 -0.2745
[torch.FloatTensor of size 5x10]

myLayer.bias
 Output : Parameter containing:-0.2646-0.2232 0.2444 0.2177 0.0897torch.FloatTensor of size 5

线性层在不同框架中被称为密集层全连接层。解决实际应用场景的深度学习架构通常包含多个层次。在 PyTorch 中,我们可以用多种方式实现,如下所示。

一个简单的方法是将一层的输出传递给另一层:

myLayer1 = Linear(10,5)
myLayer2 = Linear(5,2)
myLayer2(myLayer1(inp))

每个层次都有自己可学习的参数。使用多个层次的理念是,每个层次将学习某种模式,后续层次将在此基础上构建。仅将线性层堆叠在一起存在问题,因为它们无法学习超出简单线性层表示的任何新内容。让我们通过一个简单的例子看看为什么堆叠多个线性层没有意义。

假设我们有两个线性层,具有以下权重:

层次 权重 1
层 1 3.0
层 2 2.0

具有两个不同层的前述架构可以简单地表示为具有不同层的单一层。因此,仅仅堆叠多个线性层并不会帮助我们的算法学习任何新内容。有时这可能不太清晰,因此我们可以通过以下数学公式可视化架构:

![

为了解决这个问题,我们有不同的非线性函数,帮助学习不同的关系,而不仅仅是线性关系。

深度学习中提供了许多不同的非线性函数。PyTorch 将这些非线性功能作为层提供,我们可以像使用线性层一样使用它们。

一些流行的非线性函数如下:

  • Sigmoid

  • Tanh

  • ReLU

  • Leaky ReLU

非线性激活函数

非线性激活是指接受输入并应用数学变换并产生输出的函数。在实践中,我们会遇到几种流行的非线性激活函数。我们将介绍一些常见的非线性激活函数。

Sigmoid

Sigmoid 激活函数具有简单的数学形式,如下所示:

sigmoid 函数直观地接受一个实数,并输出一个介于零和一之间的数字。对于一个大的负数,它返回接近零,对于一个大的正数,它返回接近一。以下图表示不同 sigmoid 函数的输出:

sigmoid 函数在历史上被广泛应用于不同的架构,但近年来已经不那么流行了,因为它有一个主要缺点。当 sigmoid 函数的输出接近零或一时,前 sigmoid 函数层的梯度接近零,因此前一层的可学习参数的梯度也接近零,权重很少被调整,导致死神经元。

Tanh

tanh 非线性函数将一个实数压缩到-1 到 1 的范围内。当 tanh 输出接近-1 或 1 的极值时,也会面临梯度饱和的问题。然而,与 sigmoid 相比,它更受青睐,因为 tanh 的输出是零中心化的:

图像来源:http://datareview.info/article/eto-nuzhno-znat-klyuchevyie-rekomendatsii-po-glubokomu-obucheniyu-chast-2/

ReLU

近年来,ReLU 变得更加流行;我们几乎可以在任何现代架构中找到它的使用或其变体的使用。它有一个简单的数学公式:

f(x)=max(0,x)

简单来说,ReLU 将任何负输入压缩为零,并保留正数不变。我们可以将 ReLU 函数可视化如下:

图像来源:http://datareview.info/article/eto-nuzhno-znat-klyuchevyie-rekomendatsii-po-glubokomu-obucheniyu-chast-2/

使用 ReLU 的一些优缺点如下:

  • 它帮助优化器更快地找到正确的权重集。从技术上讲,它加快了随机梯度下降的收敛速度。

  • 它计算上廉价,因为我们只是进行阈值处理,而不像 sigmoid 和 tanh 函数那样进行计算。

  • ReLU 有一个缺点;在反向传播过程中,当大梯度通过时,它们往往会变得不响应;这些被称为死神经元,可以通过谨慎选择学习率来控制。我们将在讨论不同的学习率调整方法时讨论如何选择学习率,参见第四章,《机器学习基础》。

Leaky ReLU

Leaky ReLU 是解决死亡问题的尝试,它不是饱和为零,而是饱和为一个非常小的数,如 0.001。对于某些用例,这种激活函数提供了比其他函数更好的性能,但其表现不一致。

PyTorch 非线性激活函数

PyTorch 已经为我们实现了大多数常见的非线性激活函数,并且它可以像任何其他层一样使用。让我们快速看一个如何在 PyTorch 中使用ReLU函数的示例:

sample_data = Variable(torch.Tensor([[1,2,-1,-1]]))
myRelu = ReLU()
myRelu(sample_data)

Output:

Variable containing:
 1 2 0 0
[torch.FloatTensor of size 1x4]

在前面的示例中,我们取一个具有两个正值和两个负值的张量,并对其应用 ReLU 函数,这会将负数阈值化为 0 并保留正数。

现在我们已经涵盖了构建网络架构所需的大部分细节,让我们构建一个可以用来解决真实世界问题的深度学习架构。在前一章中,我们使用了一种简单的方法,以便我们可以只关注深度学习算法的工作原理。我们将不再使用那种风格来构建我们的架构;相反,我们将按照 PyTorch 中应有的方式来构建架构。

构建深度学习算法的 PyTorch 方式

PyTorch 中的所有网络都实现为类,子类化一个名为nn.Module的 PyTorch 类,并且应实现__init__forward方法。在init函数中,我们初始化任何层,例如我们在前一节中介绍的linear层。在forward方法中,我们将输入数据传递到我们在init方法中初始化的层,并返回最终输出。非线性函数通常直接在forward函数中使用,有些也在init方法中使用。下面的代码片段显示了如何在 PyTorch 中实现深度学习架构:

class MyFirstNetwork(nn.Module):

    def __init__(self,input_size,hidden_size,output_size):
        super(MyFirstNetwork,self).__init__()
        self.layer1 = nn.Linear(input_size,hidden_size)
        self.layer2 = nn.Linear(hidden_size,output_size)

    def __forward__(self,input): 
        out = self.layer1(input)
        out = nn.ReLU(out)
        out = self.layer2(out)
        return out

如果你是 Python 的新手,一些前面的代码可能难以理解,但它所做的就是继承一个父类并在其中实现两个方法。在 Python 中,我们通过将父类作为参数传递给类名来子类化。init方法在 Python 中充当构造函数,super用于将子类的参数传递给父类,在我们的例子中是nn.Module

不同机器学习问题的模型架构

我们要解决的问题类型将主要决定我们将使用的层,从线性层到长短期记忆LSTM)适用于序列数据。根据你尝试解决的问题类型,你的最后一层将被确定。通常使用任何机器学习或深度学习算法解决的问题有三种。让我们看看最后一层会是什么样子:

  • 对于回归问题,比如预测一件 T 恤的价格,我们将使用具有输出为 1 的线性层作为最后一层,输出一个连续值。

  • 要将给定图像分类为 T 恤或衬衫,您将使用 sigmoid 激活函数,因为它输出接近于 1 或 0 的值,这通常被称为二分类问题

  • 对于多类分类问题,我们需要分类给定图像是否是 T 恤、牛仔裤、衬衫或连衣裙,我们将在网络的最后使用 softmax 层。让我们试着直观地理解 softmax 在没有深入数学的情况下的作用。它从前面的线性层接收输入,例如,为每种图像类型预测四个概率。请记住,所有这些概率始终总和为一。

Loss 函数

一旦我们定义了网络架构,我们还剩下两个重要的步骤。一个是计算我们的网络在执行回归、分类等特定任务时的表现如何,另一个是优化权重。

优化器(梯度下降)通常接受标量值,因此我们的loss函数应该生成一个标量值,在训练过程中需要最小化它。在一些特定的应用场景中,比如预测道路上的障碍物并将其分类为行人或其他,可能需要使用两个或更多个loss函数。即使在这种情况下,我们也需要将这些损失结合成单一标量值,供优化器最小化。在最后一章节中,我们将详细讨论如何在实际应用中结合多个损失函数的例子。

在前一章节中,我们定义了自己的loss函数。PyTorch 提供了几种常用的loss函数的实现。让我们来看看用于回归和分类的loss函数。

回归问题常用的loss函数是均方误差MSE)。这也是我们在前一章节实现的loss函数。我们可以使用 PyTorch 中实现的loss函数,如下所示:

loss = nn.MSELoss()
input = Variable(torch.randn(3, 5), requires_grad=True)
target = Variable(torch.randn(3, 5))
output = loss(input, target)
output.backward()

对于分类,我们使用交叉熵损失。在看交叉熵的数学之前,让我们理解一下交叉熵损失的作用。它计算分类网络预测概率的损失,这些概率应该总和为一,就像我们的 softmax 层一样。当预测的概率与正确概率偏离时,交叉熵损失增加。例如,如果我们的分类算法预测某个图像是猫的概率为 0.1,但实际上是熊猫,那么交叉熵损失将会更高。如果它预测与实际标签相似,那么交叉熵损失将会更低:

让我们看一个实际在 Python 代码中如何实现的示例:

def cross_entropy(true_label, prediction):
    if true_label == 1:
        return -log(prediction)
    else:
        return -log(1 - prediction)

在分类问题中使用交叉熵损失,我们实际上不需要担心内部发生了什么——我们只需要记住,当我们的预测很差时,损失会很高,而当预测很好时,损失会很低。PyTorch 为我们提供了一个loss的实现,我们可以使用,如下所示:

loss = nn.CrossEntropyLoss()
input = Variable(torch.randn(3, 5), requires_grad=True)
target = Variable(torch.LongTensor(3).random_(5))
output = loss(input, target)
output.backward()

PyTorch 提供的其他一些loss函数如下:

L1 loss 主要用作正则化器。我们将在第四章,机器学习基础中进一步讨论它。
MSE loss 用作回归问题的损失函数。
交叉熵损失 用于二元和多类别分类问题。
NLL Loss 用于分类问题,并允许我们使用特定权重来处理不平衡的数据集。
NLL Loss2d 用于像素级分类,主要用于与图像分割相关的问题。

优化网络架构

一旦我们计算出网络的损失,我们将优化权重以减少损失,从而提高算法的准确性。为了简单起见,让我们将这些优化器视为黑盒子,它们接受损失函数和所有可学习参数,并轻微地移动它们以提高我们的性能。PyTorch 提供了深度学习中大多数常用的优化器。如果您想探索这些优化器内部发生了什么,并且具备数学背景,我强烈建议阅读以下一些博客:

PyTorch 提供的一些优化器包括以下几种:

  • ADADELTA

  • Adagrad

  • Adam

  • SparseAdam

  • Adamax

  • ASGD

  • LBFGS

  • RMSProp

  • Rprop

  • SGD

我们将在第四章,机器学习基础中详细讨论一些算法,以及一些优缺点。让我们一起走过创建任何optimizer的一些重要步骤:

optimizer = optim.SGD(model.parameters(), lr = 0.01)

在前面的示例中,我们创建了一个SGD优化器,它将您网络中所有可学习参数作为第一个参数,并且一个学习率,决定可学习参数变化的比例。在第四章,机器学习基础中,我们将更详细地讨论学习率和动量,这是优化器的一个重要参数。一旦创建了优化器对象,我们需要在循环内调用zero_grad(),因为参数会累积前一个optimizer调用期间创建的梯度:

for input, target in dataset:
    optimizer.zero_grad()
    output = model(input)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step()

一旦我们在loss函数上调用backward,计算出梯度(可学习参数需要改变的量),我们调用optimizer.step(),实际上对我们的可学习参数进行更改。

现在,我们已经涵盖了帮助计算机看/识别图像所需的大多数组件。让我们构建一个复杂的深度学习模型,可以区分狗和猫,以便将所有理论付诸实践。

图像分类使用深度学习

解决任何现实世界问题中最重要的一步是获取数据。Kaggle 提供了大量关于不同数据科学问题的竞赛。我们将挑选 2014 年出现的一个问题,在本章中用于测试我们的深度学习算法,并在第五章,计算机视觉的深度学习 中进行改进,该章将介绍卷积神经网络CNNs)以及一些高级技术,可以用来提升我们的图像识别模型的性能。您可以从 www.kaggle.com/c/dogs-vs-cats/data 下载数据。数据集包含 25,000 张猫和狗的图像。在实施算法之前,需要进行数据的预处理和创建训练、验证和测试集分割等重要步骤。一旦数据下载完毕,查看数据,可以看到文件夹中包含以下格式的图像:

大多数框架在提供以下格式的图像时,使阅读图像并将其标记为其标签变得更加简单。这意味着每个类别应该有一个单独的文件夹包含其图像。在这里,所有猫的图像应该在 cat 文件夹中,所有狗的图像应该在 dog 文件夹中:

Python 让将数据放入正确的格式变得容易。让我们快速查看代码,然后我们将逐步介绍其重要部分:

path = '../chapter3/dogsandcats/'

#Read all the files inside our folder.
files = glob(os.path.join(path,'*/*.jpg'))

print(f'Total no of images {len(files)}')

no_of_images = len(files)

#Create a shuffled index which can be used to create a validation data set
shuffle = np.random.permutation(no_of_images)

#Create a validation directory for holding validation images.
os.mkdir(os.path.join(path,'valid'))

#Create directories with label names 
for t in ['train','valid']:
     for folder in ['dog/','cat/']:
          os.mkdir(os.path.join(path,t,folder)) 

#Copy a small subset of images into the validation folder.
for i in shuffle[:2000]:
     folder = files[i].split('/')[-1].split('.')[0]
     image = files[i].split('/')[-1]
     os.rename(files[i],os.path.join(path,'valid',folder,image))

#Copy a small subset of images into the training folder.
for i in shuffle[2000:]:
     folder = files[i].split('/')[-1].split('.')[0]
     image = files[i].split('/')[-1]
     os.rename(files[i],os.path.join(path,'train',folder,image))

所有上述代码做的就是检索所有文件,并选择 2,000 张图像来创建验证集。它将所有图像分成两类:猫和狗。创建独立的验证集是一个常见且重要的做法,因为在同一数据上测试我们的算法是不公平的。为了创建一个 validation 数据集,我们创建一个在打乱顺序的图像长度范围内的数字列表。打乱的数字作为索引,帮助我们选择一组图像来创建我们的 validation 数据集。让我们详细地查看代码的每个部分。

我们使用以下代码创建一个文件:

files = glob(os.path.join(path,'*/*.jpg'))

glob 方法返回特定路径中的所有文件。当图像数量庞大时,我们可以使用 iglob,它返回一个迭代器,而不是将名称加载到内存中。在我们的情况下,只有 25,000 个文件名,可以轻松放入内存。

我们可以使用以下代码来对文件进行洗牌:

shuffle = np.random.permutation(no_of_images)

上述代码以随机顺序返回从零到 25,000 范围内的 25,000 个数字,我们将使用它们作为索引,选择一部分图像来创建 validation 数据集。

我们可以创建一个验证代码,如下所示:

os.mkdir(os.path.join(path,'valid'))
for t in ['train','valid']:
     for folder in ['dog/','cat/']:
          os.mkdir(os.path.join(path,t,folder)) 

上述代码创建了一个 validation 文件夹,并在 trainvalid 目录内基于类别(cats 和 dogs)创建文件夹。

我们可以使用以下代码对索引进行洗牌:

for i in shuffle[:2000]:
     folder = files[i].split('/')[-1].split('.')[0]
     image = files[i].split('/')[-1]
     os.rename(files[i],os.path.join(path,'valid',folder,image))

在上述代码中,我们使用打乱的索引随机选择了2000张不同的图像作为验证集。我们对训练数据做类似的处理,以将图像分离到train目录中。

由于我们的数据格式已经符合要求,让我们快速看一下如何将图像加载为 PyTorch 张量。

将数据加载到 PyTorch 张量中

PyTorch 的torchvision.datasets包提供了一个名为ImageFolder的实用类,可用于加载图像及其相关标签,当数据以前述格式呈现时。通常进行以下预处理步骤是一种常见做法:

  1. 将所有图像调整为相同大小。大多数深度学习架构都期望图像大小相同。

  2. 使用数据集的平均值和标准差对数据进行标准化。

  3. 将图像数据集转换为 PyTorch 张量。

PyTorch 通过在transforms模块中提供许多实用函数,使这些预处理步骤变得更加容易。对于我们的示例,让我们应用三种转换:

  • 缩放为 256 x 256 的图像尺寸

  • 转换为 PyTorch 张量

  • 标准化数据(我们将讨论如何在《深度学习计算机视觉》的第五章中得出平均值和标准差)

下面的代码演示了如何应用转换并使用ImageFolder类加载图像:

simple_transform=transforms.Compose([transforms.Scale((224,224)),
                             transforms.ToTensor(),
                             transforms.Normalize([0.485, 0.456,                     0.406], [0.229, 0.224, 0.225])])
train = ImageFolder('dogsandcats/train/',simple_transform)
valid = ImageFolder('dogsandcats/valid/',simple_transform)

train对象包含数据集中所有图像及其关联标签。它包含两个重要属性:一个给出类别与数据集中使用的相关索引之间的映射,另一个给出类别列表:

  • train.class_to_idx - {'cat': 0, 'dog': 1}

  • train.classes - ['cat', 'dog']

将加载到张量中的数据可视化通常是最佳实践。为了可视化张量,我们必须重塑张量并对值进行反标准化。以下函数为我们执行这些操作:

def imshow(inp):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)

现在,我们可以将我们的张量传递给前面的imshow函数,该函数将其转换为图像:

imshow(train[50][0])

上述代码生成以下输出:

将 PyTorch 张量加载为批次

在深度学习或机器学习中,将图像样本分批处理是常见做法,因为现代图形处理单元GPUs)和 CPU 在批量图像上运行操作时被优化得更快。批量大小通常根据使用的 GPU 类型而变化。每个 GPU 都有自己的内存,从 2 GB 到 12 GB 不等,商用 GPU 有时甚至更多。PyTorch 提供了DataLoader类,它接受一个数据集并返回一个图像批次。它抽象了批处理中的许多复杂性,如使用多个工作进程进行变换应用。以下代码将前述的trainvalid数据集转换为数据加载器:

train_data_gen =  
  torch.utils.data.DataLoader(train,batch_size=64,num_workers=3)
valid_data_gen = 
  torch.utils.data.DataLoader(valid,batch_size=64,num_workers=3)

DataLoader类为我们提供了许多选项,其中一些最常用的选项如下:

  • shuffle: 当为 true 时,每次调用数据加载器时都会对图像进行洗牌。

  • num_workers: 这个参数负责并行化。通常的做法是使用比您机器上可用的核心数少的工作线程数。

构建网络架构

对于大多数现实世界的用例,特别是在计算机视觉领域,我们很少自己构建架构。有不同的架构可以快速用于解决我们的现实世界问题。例如,我们使用一个称为ResNet的流行深度学习算法,该算法在 2015 年赢得了 ImageNet 等不同比赛的第一名。为了更简单地理解,我们可以假设这个算法是一堆不同的 PyTorch 层精心连接在一起,而不关注算法内部发生的事情。当我们学习 CNN 时,在《计算机视觉的深度学习》第五章中,我们将看到 ResNet 算法的一些关键构建块,详见第五章。PyTorch 通过在torchvision.models模块中提供这些流行算法,使得使用它们变得更加容易。因此,让我们快速看一下如何使用这个算法,然后逐行分析代码:

model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)

if is_cuda:
    model_ft = model_ft.cuda()

models.resnet18(pertrained = True)对象创建了算法的一个实例,这是一组 PyTorch 层。我们可以通过打印model_ft来快速查看 ResNet 算法的构成。算法的一个小部分看起来像以下的屏幕截图。我没有包括完整的算法,因为可能需要运行数页:

正如我们所见,ResNet 架构是一组层,即Conv2dBatchNorm2dMaxPool2d,以特定的方式连接在一起。所有这些算法都会接受一个名为pretrained的参数。当pretrainedTrue时,算法的权重已经针对预测 ImageNet 分类问题进行了调整,该问题涉及预测包括汽车、船、鱼、猫和狗在内的 1000 个不同类别。这些权重已经调整到一定程度,使得算法达到了最先进的准确性。这些权重被存储并与我们用于该用例的模型共享。与使用随机权重相比,算法在使用精调权重时往往表现更好。因此,对于我们的用例,我们从预训练权重开始。

ResNet 算法不能直接使用,因为它是为了预测 1000 个类别之一而训练的。对于我们的用例,我们只需要预测狗和猫中的一种。为了实现这一点,我们取 ResNet 模型的最后一层,这是一个linear层,并将输出特征更改为两个,如下面的代码所示:

model_ft.fc = nn.Linear(num_ftrs, 2)

如果您在基于 GPU 的机器上运行此算法,那么为了使算法在 GPU 上运行,我们需要在模型上调用cuda方法。强烈建议您在支持 GPU 的机器上运行这些程序;在云端实例上租用一个带 GPU 的实例成本不到一美元。以下代码片段的最后一行告诉 PyTorch 在 GPU 上运行代码:

if is_cuda:
    model_ft = model_ft.cuda()

训练模型

在前面的章节中,我们创建了DataLoader实例和算法。现在,让我们来训练模型。为此,我们需要一个loss函数和一个optimizer

# Loss and Optimizer
learning_rate = 0.001
criterion = nn.CrossEntropyLoss()
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7,  
  gamma=0.1)

在前面的代码中,我们基于CrossEntropyLoss创建了我们的loss函数,并基于SGD创建了优化器。StepLR函数有助于动态调整学习率。我们将在《机器学习基础》第四章讨论可用于调整学习率的不同策略。

以下的train_model函数接受一个模型,并通过运行多个 epochs 和减少损失来调整我们算法的权重:

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()

    best_model_wts = model.state_dict()
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'valid']:
            if phase == 'train':
                scheduler.step()
                model.train(True) # Set model to training mode
            else:
                model.train(False) # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            for data in dataloaders[phase]:
                # get the inputs
                inputs, labels = data

                # wrap them in Variable
                if is_cuda:
                    inputs = Variable(inputs.cuda())
                    labels = Variable(labels.cuda())
                else:
                    inputs, labels = Variable(inputs),                 Variable(labels)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                outputs = model(inputs)
                _, preds = torch.max(outputs.data, 1)
                loss = criterion(outputs, labels)

                # backward + optimize only if in training phase
                if phase == 'train':
                    loss.backward()
                    optimizer.step()

                # statistics
                running_loss += loss.data[0]
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'valid' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = model.state_dict()

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model

前述函数执行以下操作:

  1. 将图像通过模型并计算损失。

  2. 在训练阶段进行反向传播。对于验证/测试阶段,不会调整权重。

  3. 损失是在每个 epoch 的各个批次中累积的。

  4. 存储最佳模型并打印验证准确率。

在运行了25个 epochs 后,前述模型的验证准确率达到了 87%。以下是在我们的《猫狗大战》数据集上运行train_model函数时生成的日志;这里只包含了最后几个 epochs 的结果,以节省空间。

Epoch 18/24
----------
train Loss: 0.0044 Acc: 0.9877
valid Loss: 0.0059 Acc: 0.8740

Epoch 19/24
----------
train Loss: 0.0043 Acc: 0.9914
valid Loss: 0.0059 Acc: 0.8725

Epoch 20/24
----------
train Loss: 0.0041 Acc: 0.9932
valid Loss: 0.0060 Acc: 0.8725

Epoch 21/24
----------
train Loss: 0.0041 Acc: 0.9937
valid Loss: 0.0060 Acc: 0.8725

Epoch 22/24
----------
train Loss: 0.0041 Acc: 0.9938
valid Loss: 0.0060 Acc: 0.8725

Epoch 23/24
----------
train Loss: 0.0041 Acc: 0.9938
valid Loss: 0.0060 Acc: 0.8725

Epoch 24/24
----------
train Loss: 0.0040 Acc: 0.9939
valid Loss: 0.0060 Acc: 0.8725

Training complete in 27m 8s
Best val Acc: 0.874000

在接下来的章节中,我们将学习更高级的技术,帮助我们以更快的方式训练更精确的模型。前述模型在 Titan X GPU 上运行大约需要 30 分钟。我们将介绍不同的技术,这些技术有助于更快地训练模型。

摘要

在本章中,我们探讨了在 Pytorch 中神经网络的完整生命周期,从构建不同类型的层、添加激活函数、计算交叉熵损失,最终通过 SGD 优化网络性能(即最小化损失),调整层权重。

我们已经学习了如何将流行的 ResNET 架构应用于二元或多类别分类问题。

在此过程中,我们尝试解决真实世界的图像分类问题,如将猫图像分类为猫和狗图像分类为狗。这些知识可以应用于分类不同的实体类别/类别,例如分类鱼类的物种、识别不同品种的狗、分类植物种子苗、将宫颈癌分为类型 1、类型 2 和类型 3 等等。

在接下来的章节中,我们将深入探讨机器学习的基础知识。

第四章:机器学习基础

在前几章中,我们看到了如何构建深度学习模型来解决分类和回归问题的实际示例,比如图像分类和平均用户观看预测。同样地,我们也形成了如何构建深度学习问题框架的直觉。在本章中,我们将详细讨论如何解决不同类型的问题以及我们可能会使用的各种调整来提高模型在问题上的性能。

在本章中,我们将探讨:

  • 超出分类和回归的其他问题形式

  • 评估问题,理解过拟合、欠拟合及解决方法的问题

  • 为深度学习准备数据

请记住,本章讨论的大多数主题对机器学习和深度学习来说都是常见的,除了一些我们用来解决过拟合问题的技术,比如 dropout。

三种机器学习问题

在我们之前的所有示例中,我们试图解决分类(预测猫或狗)或回归(预测用户在平台上平均花费的时间)问题。所有这些都是监督学习的例子,其目标是映射训练样本和它们的目标之间的关系,并用它来对未见数据进行预测。

监督学习只是机器学习的一部分,还有其他不同的机器学习部分。机器学习有三种不同的类型:

  • 监督学习

  • 无监督学习

  • 强化学习

让我们详细了解各种算法类型。

监督学习

在深度学习和机器学习领域中,大多数成功的用例属于监督学习。本书中我们涵盖的大多数示例也将是其中的一部分。一些常见的监督学习例子包括:

  • 分类问题:对狗和猫进行分类。

  • 回归问题:预测股票价格、板球比赛得分等。

  • 图像分割:进行像素级分类。对于自动驾驶汽车来说,从其摄像头拍摄的照片中识别每个像素属于什么是很重要的。像素可能属于汽车、行人、树木、公共汽车等等。

  • 语音识别:OK Google、Alexa 和 Siri 是语音识别的良好示例。

  • 语言翻译:将一种语言的语音翻译成另一种语言。

无监督学习

当没有标签数据时,无监督学习技术通过可视化和压缩帮助理解数据。无监督学习中常用的两种技术是:

  • 聚类

  • 降维

聚类有助于将所有相似的数据点分组在一起。降维有助于减少维度数量,这样我们可以可视化高维数据以发现任何隐藏的模式。

强化学习

强化学习是最不受欢迎的机器学习类别。它在现实世界的使用案例中并没有取得成功。然而,近年来情况发生了变化,Google DeepMind 的团队成功地基于强化学习构建系统,并且能够在 AlphaGo 比赛中击败世界冠军。这种技术进步,即计算机可以在游戏中击败人类,被认为需要几十年的时间才能实现。然而,深度学习结合强化学习比任何人预期的都要早地实现了这一点。这些技术已经开始取得初步的成功,可能需要几年时间才能成为主流。

在本书中,我们将主要关注监督技术,以及一些深度学习中特有的无监督技术,例如用于创建特定风格图像的生成网络,称为风格转移生成对抗网络

机器学习术语表

在最近的几章中,我们使用了许多可能对您完全陌生的术语,如果您刚刚进入机器学习或深度学习领域,我们将列出许多在机器学习中常用的术语,这些术语也在深度学习文献中使用:

  • 样本 或输入 数据点:这些表示训练集的特定实例。在我们上一章中看到的图像分类问题中,每个图像可以称为样本、输入或数据点。

  • 预测 输出:我们的算法生成的值作为输出。例如,在我们的上一个例子中,我们的算法预测特定图像为 0,这是给猫的标签,所以数字 0 是我们的预测或输出。

  • 目标 或标签:图像的实际标记标签。

  • 损失值 或预测误差:预测值和实际值之间距离的某种度量。值越小,准确性越高。

  • 类别:给定数据集的可能值或标签集。在我们上一章的例子中,我们有两个类别——猫和狗。

  • 二元分类:一个分类任务,其中每个输入示例应被分类为两个互斥的类别之一。

  • 多类分类:一个分类任务,其中每个输入示例可以被分类为超过两个不同的类别。

  • 多标签分类:一个输入示例可以被打上多个标签,例如标记一个餐馆提供的不同类型的食物,如意大利、墨西哥和印度食物。另一个常用的例子是图像中的物体检测,算法可以识别图像中的不同对象。

  • 标量回归:每个输入数据点将与一个标量质量相关联,即一个数字。例如,预测房价、股票价格和板球比分。

  • Vector regression: 当算法需要预测多个标量量时使用。一个很好的例子是当您尝试识别图像中包含鱼位置的边界框时。为了预测边界框,您的算法需要预测四个标量量,表示正方形的边缘。

  • Batch: 对于大多数情况,我们训练算法时使用一组输入样本,称为批处理。批处理的大小通常从 2 到 256 不等,取决于 GPU 的内存。权重也会在每个批次中更新,因此算法的学习速度比在单个示例上训练时要快。

  • Epoch: 将算法运行完整数据集称为一个周期。通常会进行多个周期的训练(更新权重)。

评估机器学习模型

在我们上一章节讨论的图像分类示例中,我们将数据分成两半,一半用于训练,另一半用于验证。使用单独的数据集来测试算法的性能是一个良好的实践,因为在训练集上测试算法可能不能真正反映算法的泛化能力。在大多数真实应用中,根据验证准确率,我们经常以不同的方式调整算法,例如添加更多层或不同的层,或者使用我们将在本章后部分介绍的不同技术。因此,你对调整算法选择的可能性更高是基于验证数据集。通过这种方式训练的算法通常在训练数据集和验证数据集上表现良好,但在未见数据上泛化能力较差。这是由于验证数据集中的信息泄漏,影响了我们对算法进行调整。

为了避免信息泄漏问题并提高泛化能力,通常的做法是将数据集分为三个不同部分,即训练、验证和测试数据集。我们通过训练和验证集进行算法的所有超参数调整和训练。在整个训练结束时,您将在测试数据集上测试算法。我们讨论的有两种类型的参数。一种是算法内部使用的参数或权重,这些参数通过优化器或反向传播进行调整。另一组参数称为超参数,控制网络中使用的层数、学习速率和其他类型的参数,通常需要手动更改架构。

特定算法在训练集上表现更好,但在验证或测试集上表现不佳的现象被称为过拟合,或者算法泛化能力不足。还有一个相反的现象,算法在训练集上表现不佳,这称为欠拟合。我们将看看不同的策略,帮助我们克服过拟合和欠拟合问题。

在讨论过拟合和欠拟合之前,让我们先看看在数据集分割方面的各种策略。

训练、验证和测试分割

将数据分成三部分——训练集、验证集和测试集,是最佳实践。使用保留数据集的最佳方法是:

  1. 在训练集上训练算法

  2. 基于验证数据集执行超参数调优

  3. 通过迭代执行前两个步骤,直到达到预期的性能

  4. 在冻结算法和超参数后,在测试数据集上评估它

避免将数据分割成两部分,因为这可能导致信息泄露。在同一数据集上进行训练和测试是明确禁止的,因为它不能保证算法的泛化。有三种流行的保留策略可用于将数据分割为训练集和验证集。它们如下:

  • 简单保留验证

  • K 折验证

  • 迭代 k 折验证

简单保留验证

将数据的一部分作为测试数据集。保留多少数据可能非常依赖于具体问题,并且很大程度上取决于可用的数据量。在计算机视觉和自然语言处理领域,特别是收集标记数据可能非常昂贵,因此保留 30% 的大部分数据可能会使算法难以学习,因为训练数据较少。因此,根据数据的可用性,明智地选择它的一部分。一旦测试数据分割完成,在冻结算法及其超参数之前保持其独立。为了选择问题的最佳超参数,选择一个单独的验证数据集。为了避免过拟合,我们通常将可用数据分为三个不同的集合,如下图所示:

我们在上一章节中使用了上述图示的简单实现来创建我们的验证集。让我们来看一下实现的快照:

files = glob(os.path.join(path,'*/*.jpg'))
no_of_images = len(files)
shuffle = np.random.permutation(no_of_images)
train = files[shuffle[:int(no_of_images*0.8)]]
valid = files[shuffle[int(no_of_images*0.8):]]

这是最简单的保留策略之一,通常用于起步。使用小数据集时会有一个缺点。验证集或测试集可能无法统计代表手头的数据。我们可以通过在保留前对数据进行洗牌来轻松识别这一点。如果获得的结果不一致,则需要使用更好的方法。为了避免这个问题,我们经常使用 k 折或迭代 k 折验证。

K 折验证

将数据集的一部分保留用于测试拆分,然后将整个数据集分成 k 折,其中 k 可以是任意数字,通常在两到十之间变化。在任何给定的迭代中,我们保留一个块用于验证,并在其余块上训练算法。最终分数通常是在所有 k 折中获得的所有分数的平均值。以下图示显示了 k 折验证的实现,其中 k 为四;也就是说,数据分为四个部分:

使用 k 折验证数据集时需要注意的一个关键点是它非常昂贵,因为您需要在数据集的不同部分上运行算法多次,这对于计算密集型算法来说可能非常昂贵——特别是在计算机视觉算法的领域,在某些情况下,训练一个算法可能需要从几分钟到几天的时间。因此,明智地使用这种技术。

带有洗牌的 k 折验证

要使事情变得复杂和健壮,您可以在每次创建留存验证数据集时对数据进行洗牌。这对于解决那些小幅提升性能可能会产生巨大业务影响的问题非常有帮助。如果您的情况是快速构建和部署算法,并且对性能差异的几个百分点可以妥协,那么这种方法可能不值得。关键在于您试图解决的问题以及准确性对您意味着什么。

在分割数据时还有一些其他需要考虑的事项,例如:

  • 数据代表性

  • 时间敏感性

  • 数据冗余

数据代表性

在我们上一章中看到的例子中,我们将图像分类为狗或猫。让我们看一个情况,所有图像都已排序,前 60% 的图像是狗,剩下的是猫。如果我们通过选择前 80% 作为训练数据集,剩下的作为验证集来拆分这个数据集,那么验证数据集将不是数据集的真实代表,因为它只包含猫的图像。因此,在这些情况下,应该小心地通过在拆分之前对数据进行洗牌或进行分层抽样来确保我们有一个良好的混合数据集。

时间敏感性

让我们以预测股票价格为例。我们有从一月到十二月的数据。在这种情况下,如果我们进行洗牌或分层抽样,那么我们最终会出现信息泄漏,因为价格可能对时间敏感。因此,要以不会有信息泄漏的方式创建验证数据集。在这种情况下,选择十二月的数据作为验证数据集可能更合理。在股票价格的情况下,这比较复杂,因此在选择验证拆分时,领域专业知识也会起作用。

数据冗余

数据中常见重复。应确保训练、验证和测试集中的数据是唯一的。如果存在重复,则模型可能无法很好地推广到未见过的数据。

数据预处理和特征工程

我们已经看过了不同的方法来分割我们的数据集以建立评估策略。在大多数情况下,我们收到的数据可能不是我们可以直接用于训练算法的格式。在本节中,我们将介绍一些预处理技术和特征工程技术。虽然大部分特征工程技术是领域特定的,特别是在计算机视觉和文本领域,但也有一些通用的特征工程技术是跨领域通用的,我们将在本章讨论。

用于神经网络的数据预处理是使数据更适合深度学习算法进行训练的过程。以下是一些常用的数据预处理步骤:

  • 向量化

  • 标准化

  • 缺失值

  • 特征提取

向量化

数据以各种格式出现,如文本、声音、图像和视频。首先要做的事情是将数据转换为 PyTorch 张量。在先前的示例中,我们使用了 torchvision 实用函数将Python Imaging Library (PIL) 图像转换为张量对象,尽管大部分复杂性都被 PyTorch torchvision 库抽象化了。在第七章,生成网络,当我们处理递归神经网络 (RNNs) 时,我们将看到如何将文本数据转换为 PyTorch 张量。对于涉及结构化数据的问题,数据已经以向量化格式存在;我们只需将它们转换为 PyTorch 张量即可。

值规范化

在将数据传递给任何机器学习算法或深度学习算法之前,将特征规范化是一种常见做法。它有助于更快地训练算法,并帮助实现更高的性能。标准化是一种过程,其中您以某种方式表示属于特定特征的数据,使其平均值为零,标准差为一。

狗和猫的例子中,我们在上一章中进行了分类,通过使用 ImageNet 数据集中可用数据的平均值和标准差来对数据进行标准化。我们选择 ImageNet 数据集的平均值和标准差作为示例的原因是,我们使用了在 ImageNet 上预训练的 ResNet 模型的权重。通常也是一个常见做法,将每个像素值除以 255,以便所有值都落在零到一之间的范围内,特别是当您不使用预训练权重时。

标准化也适用于涉及结构化数据的问题。比如,我们正在处理一个房价预测问题,可能存在不同尺度的特征。例如,距离最近的机场和房屋年龄是可能处于不同尺度的变量或特征。直接将它们用于神经网络可能会阻止梯度的收敛。简单来说,损失可能不会按预期降低。因此,在训练算法之前,我们应该注意对任何数据应用标准化,以确保算法或模型表现更好。确保数据遵循以下特性:

  • 取小值:通常在 0 到 1 的范围内

  • 相同范围:确保所有特征都在相同的范围内

处理缺失值

在真实世界的机器学习问题中,缺失值非常普遍。从我们之前预测房价的例子中可以看出,房屋年龄字段可能缺失。通常可以安全地用一个不会出现的数字替换缺失值。算法将能够识别出模式。还有其他更具领域特定性的技术可用于处理缺失值。

特征工程

特征工程是利用关于特定问题的领域知识来创建可以传递给模型的新变量或特征的过程。为了更好地理解,让我们看一个销售预测问题。假设我们有关于促销日期、假期、竞争对手的开始日期、距离竞争对手的距离和某一天销售额的信息。在现实世界中,可能有数百个可能对预测商店价格有用的特征。有些信息可能对预测销售很重要。一些重要的特征或派生值包括:

  • 距离下一个促销活动的天数

  • 距离下一个假期的天数

  • 竞争对手业务开展的天数

可以提取许多来自领域知识的特征。提取这些类型的特征对于任何机器学习算法或深度学习算法来说都可能是相当具有挑战性的。对于某些领域,特别是在计算机视觉和文本领域,现代深度学习算法帮助我们摆脱特征工程的限制。除了这些领域外,良好的特征工程始终有助于以下方面:

  • 可以用更少的计算资源更快地解决问题。

  • 深度学习算法可以通过使用大量数据来学习特征,而无需手动工程化它们。因此,如果数据紧张,那么专注于良好的特征工程是有益的。

过拟合和欠拟合

理解过拟合和欠拟合是构建成功的机器学习和深度学习模型的关键。在本章的开头,我们简要介绍了欠拟合和过拟合的概念;让我们详细看看它们以及我们如何解决它们。

在机器学习和深度学习中,过拟合或不能泛化是一个常见问题。我们说一个特定的算法过拟合是指它在训练数据集上表现良好,但在未见过的验证和测试数据集上表现不佳。这主要是由于算法识别出的模式过于特定于训练数据集。简单来说,我们可以说算法找到了一种记住数据集的方式,以便在训练数据集上表现非常好,但在未见数据上表现不佳。有不同的技术可以用来避免算法过拟合。其中一些技术包括:

  • 获得更多数据

  • 减小网络的大小

  • 应用权重正则化器

  • 应用 dropout

获得更多数据

如果你能够获取更多可以训练算法的数据,这将有助于算法避免过拟合,因为它会专注于一般模式而不是小数据点特定的模式。有几种情况可能会使获取更多标记数据成为一个挑战。

有一些技术,比如数据增强,在与计算机视觉相关的问题中可以用来生成更多的训练数据。数据增强是一种技术,你可以通过执行不同的动作如旋转、裁剪来微调图像,并生成更多的数据。有了足够的领域理解,你甚至可以创建合成数据,如果捕获实际数据是昂贵的话。当你无法获取更多数据时,还有其他方法可以帮助避免过拟合。让我们来看看它们。

减小网络的大小

网络的大小通常指网络中使用的层数或权重参数的数量。在我们上一章节看到的图像分类示例中,我们使用了一个 ResNet 模型,它有 18 个块,包含不同的层。PyTorch 中的 torchvision 库提供了不同大小的 ResNet 模型,从 18 个块一直到 152 个块。举个例子,如果我们使用一个包含 152 个块的 ResNet 块并且模型出现了过拟合,那么我们可以尝试使用具有 101 个块或 50 个块的 ResNet。在我们构建的自定义架构中,我们可以简单地删除一些中间线性层,从而防止我们的 PyTorch 模型记住训练数据集。让我们看一个示例代码片段,展示了如何减小网络大小的具体含义:

class Architecture1(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(Architecture1, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size) 
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)
        self.relu = nn.ReLU()
        self.fc3 = nn.Linear(hidden_size, num_classes)      

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        out = self.relu(out)
        out = self.fc3(out)
        return out

上述架构有三个线性层,假设它过拟合了我们的训练数据。所以,让我们重新创建一个具有减少容量的架构:

class Architecture2(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(Architecture2, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size) 
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)  

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

上述架构只有两个线性层,因此降低了容量,从而潜在地避免了训练数据集的过拟合。

应用权重正则化

解决过拟合或泛化问题的一个关键原则是构建更简单的模型。一种构建更简单模型的技术是通过减少其结构的复杂性来降低其大小。另一个重要的事情是确保网络的权重不要取得较大的值。正则化通过对网络施加约束,当模型的权重较大时会对其进行惩罚。正则化有两种可能的类型。它们是:

  • L1 正则化:权重系数的绝对值之和被添加到成本中。通常称为权重的 L1 范数。

  • L2 正则化:所有权重系数的平方和被添加到成本中。通常称为权重的 L2 范数。

PyTorch 提供了一种简单的方法来使用 L2 正则化,通过在优化器中启用 weight_decay 参数:

model = Architecture1(10,20,2)

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)

默认情况下,权重衰减参数被设置为零。我们可以尝试不同的权重衰减值;例如 1e-5 这样的小值通常效果很好。

Dropout

Dropout 是深度学习中最常用且最强大的正则化技术之一。它是由 Hinton 及其在多伦多大学的学生开发的。Dropout 被应用于模型的中间层,在训练时使用。让我们看一个例子,说明如何在生成 10 个值的线性层输出上应用 dropout:

上图显示了当在线性层输出上应用 dropout 时,阈值为 0.2 的情况。它随机屏蔽或将数据置零 20%,因此模型不会依赖于特定的权重集或模式,从而避免过拟合。让我们看另一个例子,其中我们使用阈值为 0.5 的 dropout:

通常使用在 0.2 到 0.5 范围内的 dropout 值的阈值,dropout 应用在不同的层。Dropout 仅在训练时使用,测试时值按 dropout 的因子进行缩放。PyTorch 提供 dropout 作为另一层,从而更容易使用。下面的代码片段显示了如何在 PyTorch 中使用 dropout 层:

nn.dropout(x, training=True)

dropout 层接受一个名为 training 的参数,它在训练阶段需要设置为 True,在验证或测试阶段设置为 False

欠拟合

有时候我们的模型可能无法从训练数据中学习到任何模式,这在模型甚至在训练集上表现不佳时会非常明显。当您的模型欠拟合时,一种常见的解决方法是获取更多的数据让算法进行训练。另一种方法是通过增加层次或增加模型使用的权重或参数来增加模型的复杂性。在实际过拟合数据集之前最好不要使用上述任何正则化技术。

机器学习项目的工作流程

在这一部分,我们将制定一个解决方案框架,可以通过整合问题陈述、评估、特征工程以及避免过拟合来解决任何机器学习问题。

问题定义和数据集创建

要定义问题,我们需要两个重要的东西;即输入数据和问题类型。

我们的输入数据和目标标签将会是什么?例如,假设我们想根据顾客的评价将餐馆分类为意大利餐厅、墨西哥餐厅、中国餐厅和印度餐厅。在开始处理这类问题之前,我们需要手动为训练数据中的一个可能的类别进行标注,然后才能对算法进行训练。数据的可用性在这个阶段通常是一个具有挑战性的因素。

确定问题的类型有助于确定它是二元分类、多类分类、标量回归(房价预测)还是向量回归(边界框)。有时,我们可能需要使用一些无监督技术,如聚类和降维。一旦确定了问题类型,就更容易确定应该使用什么样的架构、损失函数和优化器。

一旦我们有了输入并且确定了问题的类型,那么我们可以根据以下假设开始构建我们的模型:

  • 数据中存在隐藏的模式,可以帮助将输入与输出进行映射

  • 我们拥有的数据足以让模型进行学习

作为机器学习从业者,我们需要明白,仅凭一些输入数据和目标数据可能无法构建出一个模型。以预测股票价格为例。假设我们有代表历史价格、历史表现和竞争详情的特征,但我们可能仍然无法构建出一个能够预测股票价格的有意义模型,因为股票价格实际上可能受到多种其他因素的影响,如国内政治形势、国际政治形势、天气因素(例如良好的季风)等,这些因素可能不会被我们的输入数据所代表。因此,没有任何机器学习或深度学习模型能够识别出模式。因此,根据领域的不同,精心选择能够成为目标变量真实指标的特征。所有这些都可能是模型欠拟合的原因。

机器学习还做了另一个重要的假设。未来或未见过的数据将接近于历史数据所描述的模式。有时,我们的模型可能失败,因为这些模式在历史数据中从未存在过,或者模型训练时的数据未涵盖某些季节性或模式。

成功的衡量标准

成功的衡量标准将直接由您的业务目标决定。例如,当尝试预测风车何时会发生下次机器故障时,我们更关心模型能够预测故障的次数。使用简单的准确率可能是错误的度量标准,因为大多数情况下,模型在预测机器不会故障时会预测正确,这是最常见的输出。假设我们获得了 98%的准确率,并且模型在预测故障率时每次都错误——这样的模型在现实世界中可能毫无用处。选择正确的成功度量标准对于业务问题至关重要。通常,这类问题具有不平衡的数据集。

对于平衡分类问题,所有类别的准确率相似时,ROC 和曲线下面积AUC)是常见的度量标准。对于不平衡的数据集,我们可以使用精确率和召回率。对于排名问题,可以使用平均精度。

评估协议

一旦确定了如何评估当前进展,决定如何在数据集上进行评估就变得很重要。我们可以从以下三种不同的评估方式中进行选择:

  • 留出验证集:最常用的方法,特别是在你有足够的数据时。

  • K 折交叉验证:当数据有限时,这种策略有助于在数据的不同部分上进行评估,有助于更好地了解性能。

  • 重复 K 折验证:当您希望模型性能更上一层楼时,这种方法会很有帮助。

准备你的数据

将可用数据的不同格式通过向量化转换为张量,并确保所有特征都被缩放和归一化。

基准模型

创建一个非常简单的模型,能够击败基准分数。在我们先前的狗和猫分类的例子中,基准准确率应为 0.5,我们的简单模型应能够超过这个分数。如果我们无法击败基准分数,那么可能输入数据不包含进行必要预测所需的信息。请记住,在此步骤中不要引入任何正则化或丢弃。

要使模型工作,我们必须做出三个重要选择:

  • 最后一层的选择: 对于回归问题,应该是一个生成标量值作为输出的线性层。对于矢量回归问题,将是生成多个标量输出的相同线性层。对于边界框,它输出四个值。对于二元分类,通常使用 sigmoid,而对于多类分类,则使用 softmax。

  • 损失函数的选择: 问题的类型将帮助您决定损失函数。对于回归问题,如预测房价,我们使用均方误差(MSE),而对于分类问题,我们使用分类交叉熵。

  • 优化: 选择正确的优化算法及其一些超参数相当棘手,我们可以通过尝试不同的算法来找到它们。对于大多数用例,Adam 或 RMSprop 优化算法效果更好。我们将涵盖一些用于学习率选择的技巧。

让我们总结一下在我们的深度学习算法网络的最后一层中,我们将使用什么样的损失函数和激活函数:

问题类型 激活函数 损失函数
二元分类 sigmoid 激活 nn.CrossEntropyLoss()
多类分类 softmax 激活 nn.CrossEntropyLoss()
多标签分类 sigmoid 激活 nn.CrossEntropyLoss()
回归 均方误差(MSE)
矢量回归 均方误差(MSE)

足够大的模型来过拟合

一旦您有一个具有足够容量以打败基准分数的模型,增加您的基准容量。增加架构容量的几个简单技巧如下:

  • 向现有架构添加更多层

  • 向现有层添加更多权重

  • 将其训练更多个周期

通常我们会对模型进行充分的训练周期。当训练精度持续增加而验证精度停止增加并可能开始下降时,这就是模型开始过拟合的地方。一旦达到这个阶段,我们需要应用正则化技术。

请记住,层数、层大小和 epochs 数可能会因问题而异。对于简单的分类问题,较小的架构可以工作,但对于像面部识别这样的复杂问题,我们需要在架构中具有足够的表达能力,并且模型需要进行比简单分类问题更多的 epochs 训练。

应用正则化

找到正则化模型或算法的最佳方法是整个过程中最棘手的部分之一,因为有很多参数需要调整。我们可以调整的一些正则化模型的参数包括:

  • 添加 dropout:这可能会很复杂,因为它可以添加在不同的层之间,找到最佳位置通常是通过实验来完成的。要添加的 dropout 百分比也很棘手,因为它完全依赖于我们试图解决的问题陈述。通常的良好做法是从小的数字开始,比如 0.2。

  • 尝试不同的架构:我们可以尝试不同的架构、激活函数、层数、权重或层内参数。

  • 添加 L1 或 L2 正则化:我们可以使用其中一种正则化。

  • 尝试不同的学习率:有不同的技术可以使用,我们将在本章后面的部分讨论这些技术。

  • 添加更多特征或更多数据:这可能通过获取更多数据或增强数据来完成。

我们将使用验证数据集来调整所有上述超参数。随着我们不断迭代和调整超参数,我们可能会遇到数据泄漏的问题。因此,我们应确保我们有保留数据用于测试。如果模型在测试数据上的性能比训练和验证数据好,那么我们的模型很可能在未见过的数据上表现良好。但是,如果模型在测试数据上表现不佳,而在验证和训练数据上表现良好,则验证数据可能不是真实世界数据集的良好代表。在这种情况下,我们可以使用 k 折交叉验证或迭代 k 折交叉验证数据集。

学习率选择策略

找到适合训练模型的正确学习率是一个持续研究的领域,在这个领域取得了很多进展。PyTorch 提供了一些技术来调整学习率,在 torch.optim.lr_scheduler 包中提供了这些技术。我们将探讨一些 PyTorch 提供的动态选择学习率的技术:

  • StepLR:这个调度程序有两个重要参数。一个是步长,它表示学习率必须变化的 epochs 数,另一个参数是 gamma,它决定学习率要变化多少。

对于学习率为0.01,步长为 10,以及0.1的 gamma 值,在每 10 个 epochs,学习率会按 gamma 倍数变化。也就是说,在前 10 个 epochs 中,学习率会变为 0.001,在接下来的 10 个 epochs 末尾,学习率会变为 0.0001。以下代码解释了StepLR的实现:

 scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
 for epoch in range(100):
     scheduler.step()
     train(...)
     validate(...)
  • MultiStepLR:MultiStepLR 的工作方式类似于 StepLR,不同之处在于步长不是在规则间隔内的,而是以列表形式给出。例如,给定步长列表为 10、15、30,对于每个步长值,学习率将乘以其 gamma 值。以下代码解释了MultiStepLR的实现:
 scheduler = MultiStepLR(optimizer, milestones=[30,80], gamma=0.1)
 for epoch in range(100):
     scheduler.step()
     train(...)
     validate(...)
  • ExponentialLR:这将学习率设置为每个 epoch 的学习率与 gamma 值的倍数。

  • ReduceLROnPlateau:这是常用的学习率调整策略之一。在这种情况下,当特定指标(如训练损失、验证损失或准确率)停滞不前时,学习率会进行调整。通常会将学习率降低到其原始值的两到十倍。ReduceLROnPlateau的实现如下:

 optimizer = torch.optim.SGD(model.parameters(), lr=0.1,    
   momentum=0.9)
 scheduler = ReduceLROnPlateau(optimizer, 'min')
 for epoch in range(10):
     train(...)
     val_loss = validate(...)
     # Note that step should be called after validate()
     scheduler.step(val_loss)

总结

在本章中,我们涵盖了解决机器学习或深度学习问题中常见和最佳实践。我们涵盖了诸如创建问题陈述、选择算法、击败基准分数、增加模型容量直到过拟合数据集、应用可以防止过拟合的正则化技术、增加泛化能力、调整模型或算法的不同参数以及探索可以优化和加快深度学习模型训练的不同学习策略等各种重要步骤。

在下一章中,我们将涵盖构建最先进的卷积神经网络CNNs)所需的不同组件。我们还将涵盖迁移学习,这有助于在数据有限时训练图像分类器。我们还将涵盖帮助我们更快地训练这些算法的技术。

第五章:深度学习用于计算机视觉

在第三章中,深入探讨神经网络,我们使用了一个名为ResNet的流行卷积神经网络CNN)架构构建了一个图像分类器,但我们将这个模型当作一个黑盒子使用。在本章中,我们将介绍卷积网络的重要构建模块。本章中我们将涵盖的一些重要主题包括:

  • 神经网络介绍

  • 从头构建 CNN 模型

  • 创建和探索 VGG16 模型

  • 计算预卷积特征

  • 理解 CNN 模型学习的内容

  • 可视化 CNN 层的权重

我们将探讨如何从头开始构建架构来解决图像分类问题,这是最常见的用例。我们还将学习如何使用迁移学习,这将帮助我们使用非常小的数据集构建图像分类器。

除了学习如何使用 CNN,我们还将探索这些卷积网络学习了什么。

神经网络介绍

在过去几年中,CNN 在计算机视觉领域的图像识别、物体检测、分割等任务中变得非常流行。它们也在自然语言处理NLP)领域变得流行起来,尽管目前还不常用。完全连接层和卷积层之间的基本区别在于中间层中权重连接的方式。让我们看一下一幅图像,展示了完全连接或线性层的工作原理:

在计算机视觉中使用线性层或完全连接层的最大挑战之一是它们会丢失所有空间信息,而完全连接层在权重数量上的复杂性太大。例如,当我们将一个 224 像素的图像表示为一个平坦的数组时,我们最终会得到 150,528 个元素(224 x 224 x 3 个通道)。当图像被展平时,我们失去了所有的空间信息。让我们看看简化版 CNN 的样子:

所有卷积层所做的就是在图像上应用称为滤波器的权重窗口。在我们试图深入理解卷积和其他构建模块之前,让我们为MNIST数据集构建一个简单而强大的图像分类器。一旦我们构建了这个模型,我们将逐步分析网络的每个组成部分。我们将将构建图像分类器分解为以下步骤:

  • 获取数据

  • 创建验证数据集

  • 从头构建我们的 CNN 模型

  • 训练和验证模型

MNIST – 获取数据

MNIST数据集包含了 60000 个手写数字(0 到 9)用于训练,以及 10000 张图片用作测试集。PyTorch 的torchvision库为我们提供了一个MNIST数据集,它可以下载数据并以易于使用的格式提供。让我们使用MNIST函数将数据集拉到我们的本地机器上,然后将其包装在一个DataLoader中。我们将使用 torchvision 的变换来将数据转换为 PyTorch 张量,并进行数据归一化。以下代码负责下载数据、包装在DataLoader中并进行数据归一化:

transformation = 
  transforms.Compose([transforms.ToTensor(),
  transforms.Normalize((0.1307,), (0.3081,))])

train_dataset = 
  datasets.MNIST('data/',train=True,transform=transformation,
    download=True)
test_dataset =  
  datasets.MNIST('data/',train=False,transform=transformation,
    download=True)

train_loader =   
  torch.utils.data.DataLoader(train_dataset,batch_size=32,shuffle=True)
test_loader =  
  torch.utils.data.DataLoader(test_dataset,batch_size=32,shuffle=True)

因此,前面的代码为我们提供了traintest数据集的DataLoader。让我们可视化几个图像,以了解我们正在处理的内容。以下代码将帮助我们可视化 MNIST 图像:

def plot_img(image):
    image = image.numpy()[0]
    mean = 0.1307
    std = 0.3081
    image = ((mean * image) + std)
    plt.imshow(image,cmap='gray')

现在我们可以通过plot_img方法来可视化我们的数据集。我们将使用以下代码从DataLoader中提取一批记录,并绘制这些图像:

sample_data = next(iter(train_loader))
plot_img(sample_data[0][1])
plot_img(sample_data[0][2])

图像可视化如下所示:

   

从头开始构建 CNN 模型

例如,让我们从头开始构建我们自己的架构。我们的网络架构将包含不同的层组合,具体来说是:

  • Conv2d

  • MaxPool2d

  • 修正线性单元ReLU

  • 视图

  • 线性层

让我们看一下我们将要实现的架构的图示表示:

让我们在 PyTorch 中实现这个架构,然后逐步分析每个单独的层的作用:

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x)

让我们详细了解每一层的作用。

Conv2d

Conv2d负责在我们的 MNIST 图像上应用卷积滤波器。让我们尝试理解如何在一维数组上应用卷积,然后再转向如何在图像上应用二维卷积。我们将查看以下图像,我们将在长度为7的张量上应用一个滤波器(或内核)大小为3Conv1d

底部的方框表示我们的输入张量有七个值,连接的方框表示我们应用卷积滤波器后的输出,滤波器大小为三。在图像的右上角,三个方框表示Conv1d层的权重和参数。卷积滤波器像窗口一样应用,并通过跳过一个值移动到下一个值。要跳过的值的数量称为步幅,默认设置为1。让我们通过书写第一个和最后一个输出的计算来了解输出值是如何计算的:

输出 1 –> (-0.5209 x 0.2286) + (-0.0147 x 2.4488) + (-0.4281 x -0.9498)

输出 5 –> (-0.5209 x -0.6791) + (-0.0147 x -0.6535) + (-0.4281 x 0.6437)

所以,到目前为止,应该清楚卷积是做什么的了。它将一个滤波器(或内核),即一堆权重,应用在输入上,并根据步幅的值移动它。在前面的例子中,我们每次移动我们的滤波器一次。如果步幅值为 2,那么我们将一次移动两个点。让我们看一个 PyTorch 实现来理解它是如何工作的:

conv = nn.Conv1d(1,1,3,bias=False)
sample = torch.randn(1,1,7)
conv(Variable(sample))

#Check the weights of our convolution filter by 
conv.weight

还有另一个重要的参数,称为填充(padding),通常与卷积一起使用。如果我们仔细观察前面的例子,我们可能会意识到,如果在数据的末尾没有足够的元素供数据进行跨步时,滤波器将停止。填充通过在张量的两端添加零来防止这种情况。让我们再次看一个关于填充如何工作的一维示例:

image

在前面的图像中,我们应用了带有填充 2 和步幅 1Conv1d 层。

让我们看看 Conv2d 在图像上是如何工作的:

在我们理解 Conv2d 如何工作之前,我强烈建议你查看这篇精彩的博客(setosa.io/ev/image-kernels/),其中包含一个卷积演示的实时示例。在你花几分钟玩弄演示后,再阅读下一节。

让我们理解演示中发生了什么。在图像的中心框中,我们有两组不同的数字;一组在框中表示,另一组在框下面。框中表示的是像素值,如左手边的白色框所示。框下面标记的数字是用于锐化图像的滤波器(或内核)值。这些数字是手动选择的以执行特定的任务。在这种情况下,它是用来锐化图像的。就像在我们之前的例子中一样,我们进行元素对元素的乘法,并将所有值相加以生成右侧图像像素的值。生成的值由图像右侧的白色框突出显示。

尽管在这个例子中内核中的值是手动选择的,在 CNN 中我们不会手动选择这些值,而是随机初始化它们,并让梯度下降和反向传播调整内核的值。学习到的内核将负责识别不同的特征,如线条、曲线和眼睛。让我们看看另一张图像,我们把它看作是一个数字矩阵,并看看卷积是如何工作的:

image

在前一张屏幕截图中,我们假设 6 x 6 矩阵表示一张图像,我们应用大小为 3 x 3 的卷积滤波器,然后展示生成输出的方式。为了简单起见,我们只计算矩阵的突出部分。输出是通过进行以下计算生成的:

输出 –> 0.86 x 0 + -0.92 x 0 + -0.61 x 1 + -0.32 x -1 + -1.69 x -1 + ........

Conv2d函数中使用的另一个重要参数是kernel_size,它决定了核的大小。一些常用的核大小包括1357。核大小越大,滤波器能覆盖的区域就越大,因此在早期层中观察到使用79的滤波器是常见的。

池化

在卷积层后添加池化层是一种常见做法,因为它们可以减小特征图的大小和卷积层的输出。

池化提供两种不同的特性:一是减少要处理的数据大小,另一种是强制算法不要关注图像中位置的微小变化。例如,一个人脸检测算法应该能够在图片中检测到人脸,而不管人脸在照片中的位置如何。

让我们来看看 MaxPool2d 是如何工作的。它也有与卷积相同的核大小和步长概念。它与卷积不同的地方在于它没有任何权重,只是作用于上一层每个滤波器生成的数据。如果核大小是2 x 2,则它在图像中考虑该大小并选择该区域的最大值。让我们看一下接下来的图像,这将清楚地展示 MaxPool2d 的工作原理:

左侧框中包含特征图的值。应用最大池化后,输出存储在框的右侧。让我们看一下如何计算输出的第一行值的计算方式:

另一个常用的池化技术是平均池化maximum函数被average函数替换。下图解释了平均池化的工作原理:

在这个例子中,我们不是取四个值的最大值,而是取这四个值的平均值。让我们写下计算方式,以便更容易理解:

非线性激活 – ReLU

在应用最大池化或卷积后,添加非线性层是一种常见且最佳的实践。大多数网络架构倾向于使用 ReLU 或不同变体的 ReLU。无论我们选择哪种非线性函数,它都应用于特征图的每个元素。为了更直观地理解,让我们看一个例子,在这个例子中,我们对应用了最大池化和平均池化的相同特征图应用 ReLU:

视图

大多数网络在图像分类问题的最后都会使用全连接或线性层。我们使用二维卷积,它将一个数字矩阵作为输入并输出另一个数字矩阵。要应用线性层,我们需要将这个二维张量展平为一个一维向量。接下来的示例将展示view函数的工作方式:

让我们看看我们网络中使用的代码,它执行相同的操作:

x.view(-1, 320)

正如我们之前看到的,view方法将把一个n维张量展平为一个一维张量。在我们的网络中,第一维度是每个图像的尺寸。批处理后的输入数据将具有32 x 1 x 28 x 28的维度,其中第一个数字32表示有32张大小为28高度、28宽度和1通道的图像,因为它是一张黑白图像。当我们展平时,我们不希望展平或混合不同图像的数据。因此,我们传递给view函数的第一个参数将指导 PyTorch 避免在第一维上展平数据。让我们看看这在下面的图像中是如何工作的:

在前面的例子中,我们有大小为2 x 1 x 2 x 2的数据;在应用view函数后,它转换为大小为2 x 1 x 4的张量。让我们再看另一个例子,这次我们不提到- 1

如果我们忘记提到要展平的维度,可能会导致意外的结果。所以在这一步要特别小心。

线性层

在将数据从二维张量转换为一维张量后,我们通过一个线性层,然后是一个非线性激活层。在我们的架构中,我们有两个线性层;一个后面跟着 ReLU,另一个后面跟着log_softmax,用于预测给定图像中包含的数字。

训练模型

训练模型的过程与我们之前猫狗图像分类问题的过程相同。以下代码片段展示了如何在提供的数据集上训练我们的模型:

def fit(epoch,model,data_loader,phase='training',volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , (data,target) in enumerate(data_loader):
        if is_cuda:
            data,target = data.cuda(),target.cuda()
        data , target = Variable(data,volatile),Variable(target)
        if phase == 'training':
            optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output,target)

        running_loss += F.nll_loss(output,target,size_average=False).data[0]
        preds = output.data.max(dim=1,keepdim=True)[1]
        running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()

    loss = running_loss/len(data_loader.dataset)
    accuracy = 100\. * running_correct/len(data_loader.dataset)

    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

这种方法在trainingvalidation中有不同的逻辑。使用不同模式主要有两个原因:

  • train模式下,dropout 会删除一定百分比的值,在验证或测试阶段不应该发生这种情况。

  • 对于training模式,我们计算梯度并改变模型的参数值,但在测试或验证阶段不需要反向传播。

前一个函数中的大部分代码是不言自明的,正如前几章中讨论的那样。在函数的最后,我们返回该特定时期模型的lossaccuracy

让我们运行模型通过前述函数进行 20 次迭代,并绘制训练验证损失准确率,以了解我们的网络表现如何。以下代码运行fit方法用于训练测试数据集,迭代20次:

model = Net()
if is_cuda:
    model.cuda()

optimizer = optim.SGD(model.parameters(),lr=0.01,momentum=0.5)
train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,20):
    epoch_loss, epoch_accuracy = fit(epoch,model,train_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

以下代码绘制训练测试损失

plt.plot(range(1,len(train_losses)+1),train_losses,'bo',label = 'training loss')
plt.plot(range(1,len(val_losses)+1),val_losses,'r',label = 'validation loss')
plt.legend()

前述代码生成如下图表:

以下代码绘制训练和测试的准确率:

plt.plot(range(1,len(train_accuracy)+1),train_accuracy,'bo',label = 'train accuracy')
plt.plot(range(1,len(val_accuracy)+1),val_accuracy,'r',label = 'val accuracy')
plt.legend()

前述代码生成如下图表:

在第 20 个迭代周期结束时,我们实现了测试准确率达到了 98.9%。我们的简单卷积模型已经可以工作,并且几乎达到了最先进的结果。让我们看看当我们尝试在之前使用的狗与猫数据集上使用相同网络架构时会发生什么。我们将使用我们前一章节中的数据,第三章,神经网络的构建块,以及来自 MNIST 示例的架构,并进行一些微小的更改。一旦我们训练模型,让我们评估它,以了解我们的简单架构的表现如何。

独自从头构建的 CNN 对狗和猫进行分类

我们将使用与少许更改的相同架构,如下所列:

  • 第一个线性层的输入维度发生变化,因为我们猫和狗图像的维度为256, 256

  • 我们增加另一层线性层以提供模型更多的灵活性学习

让我们看一下实现网络架构的代码:

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(56180, 500)
        self.fc2 = nn.Linear(500,50)
        self.fc3 = nn.Linear(50, 2)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(x.size(0),-1)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = F.relu(self.fc2(x))
        x = F.dropout(x,training=self.training)
        x = self.fc3(x)
        return F.log_softmax(x,dim=1)

我们将使用与 MNIST 示例相同的训练函数。所以,我这里不包括代码。但让我们看一下在训练20次迭代时生成的图表。

训练验证数据集的损失:

训练验证数据集的准确率:

从图表可以清楚地看出,训练损失在每次迭代中都在减少,但验证损失却变得更糟。准确率在训练过程中也在增加,但几乎在 75%时饱和。这是一个明显的例子,显示模型没有泛化。我们将看一下另一种称为迁移学习的技术,它帮助我们训练更精确的模型,同时提供使训练更快的技巧。

使用迁移学习对狗和猫进行分类

迁移学习是在不从头训练算法的情况下,重新使用已训练算法处理类似数据集的能力。我们人类并不通过分析成千上万张类似图像来学习识别新图像。作为人类,我们只是理解实际区分特定动物(比如狐狸和狗)的不同特征。我们无需从理解线条、眼睛及其他较小特征是什么开始学习什么是狐狸。因此,我们将学习如何利用预训练模型来仅用少量数据构建最先进的图像分类器。

CNN 架构的前几层专注于较小的特征,例如线条或曲线的外观。CNN 的后几层中的滤波器学习更高级的特征,例如眼睛和手指,最后几层学习识别确切的类别。预训练模型是在类似数据集上训练的算法。大多数流行的算法都是在流行的ImageNet数据集上预训练,以识别 1,000 个不同的类别。这样一个预训练模型将具有调整后的滤波器权重,用于识别各种模式。因此,让我们了解如何利用这些预训练权重。我们将研究一种名为VGG16的算法,这是最早在 ImageNet 竞赛中取得成功的算法之一。尽管现代有更多的算法,但由于其简单易懂且适用于迁移学习,这个算法仍然很受欢迎。让我们先看一下 VGG16 模型的架构,然后尝试理解这个架构以及如何用它来训练我们的图像分类器:

VGG16 模型的架构

VGG16 的架构包含五个 VGG 块。一个块包括一组卷积层、非线性激活函数和最大池化函数。所有的算法参数都调整到达到分类 1,000 个类别的最先进结果。该算法接受批处理形式的输入数据,这些数据是通过ImageNet数据集的均值和标准差进行标准化的。在迁移学习中,我们尝试通过冻结大部分层的学习参数来捕捉算法学到的内容。通常的做法是仅微调网络的最后几个线性层,并保持卷积层不变,因为卷积层学到的特征对所有具有相似属性的图像问题都是有效的。在这个例子中,让我们仅训练最后几个线性层,保持卷积层不变。让我们使用迁移学习训练一个 VGG16 模型来分类狗和猫。让我们逐步实施这些不同步骤。

创建和探索一个 VGG16 模型

PyTorch 在其torchvision库中提供了一组经过训练的模型。当参数pretrainedTrue时,大多数模型会下载针对ImageNet分类问题调整过的权重。让我们看一下创建 VGG16 模型的代码片段:

from torchvision import models
vgg = models.vgg16(pretrained=True)

现在我们有了准备好使用的所有预训练权重的 VGG16 模型。当第一次运行代码时,根据您的网络速度,可能需要几分钟的时间。权重的大小大约为 500 MB。我们可以通过打印来快速查看 VGG16 模型。了解这些网络是如何实现的,在使用现代架构时非常有用。让我们看看这个模型:

VGG (
  (features): Sequential (
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU (inplace)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU (inplace)
    (4): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU (inplace)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU (inplace)
    (9): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU (inplace)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU (inplace)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU (inplace)
    (16): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU (inplace)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU (inplace)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU (inplace)
    (23): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU (inplace)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU (inplace)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU (inplace)
    (30): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  )
  (classifier): Sequential (
    (0): Linear (25088 -> 4096)
    (1): ReLU (inplace)
    (2): Dropout (p = 0.5)
    (3): Linear (4096 -> 4096)
    (4): ReLU (inplace)
    (5): Dropout (p = 0.5)
    (6): Linear (4096 -> 1000)
  )
)

模型摘要包含两个顺序模型 特征分类器特征顺序 模型具有我们将要冻结的层。

冻结层

让我们冻结 特征 模型的所有层,其中包含卷积块。冻结这些卷积块的权重将防止模型权重。由于模型的权重是训练用于识别许多重要特征,我们的算法将能够从第一个迭代开始做同样的事情。使用最初针对不同用例训练的模型权重的能力称为迁移学习。现在让我们看看如何冻结层的权重或参数:

for param in vgg.features.parameters(): param.requires_grad = False

此代码防止优化器更新权重。

微调 VGG16

VGG16 模型经过训练,用于分类 1000 个类别,但没有经过狗和猫的分类训练。因此,我们需要将最后一层的输出特征从 1000 更改为 2。以下代码片段执行此操作:

vgg.classifier[6].out_features = 2

vgg.classifier 提供了顺序模型中所有层的访问权限,第六个元素将包含最后一层。当我们训练 VGG16 模型时,我们只需要训练分类器参数。因此,我们将仅将 classifier.parameters 传递给优化器,如下所示:

optimizer = 
  optim.SGD(vgg.classifier.parameters(),lr=0.0001,momentum=0.5)

训练 VGG16 模型

我们已经创建了模型和优化器。由于我们使用的是 狗与猫 数据集,我们可以使用相同的数据加载器和 train 函数来训练我们的模型。请记住,当我们训练模型时,只有分类器内部的参数会发生变化。以下代码片段将训练模型 20 个 epoch,达到 98.45% 的验证准确率。

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,20):
    epoch_loss, epoch_accuracy = fit(epoch,vgg,train_data_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,vgg,valid_data_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

让我们可视化训练和验证损失:

让我们可视化训练和验证准确率:

我们可以应用一些技巧,例如数据增强和尝试不同的丢弃值来改进模型的泛化能力。下面的代码片段将在 VGG 的分类器模块中将丢弃值从 0.5 更改为 0.2 并训练模型:

for layer in vgg.classifier.children():
    if(type(layer) == nn.Dropout):
        layer.p = 0.2

#Training
train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,3):
    epoch_loss, epoch_accuracy = fit(epoch,vgg,train_data_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,vgg,valid_data_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

对此进行几个 epoch 的训练稍微改进了我的模型;您可以尝试不同的丢弃值。改进模型泛化的另一个重要技巧是增加更多的数据或进行数据增强。我们将进行数据增强,随机水平翻转图像或将图像旋转一个小角度。 torchvision 转换提供不同的功能来执行数据增强,它们在每个 epoch 动态地进行更改。我们使用以下代码实现数据增强:

train_transform =transforms.Compose([transforms.Resize((224,224)),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.RandomRotation(0.2),
                                     transforms.ToTensor(),
                                     transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                      ])

train = ImageFolder('dogsandcats/train/',train_transform)
valid = ImageFolder('dogsandcats/valid/',simple_transform)

#Training 

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,3):
    epoch_loss, epoch_accuracy = fit(epoch,vgg,train_data_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,vgg,valid_data_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的输出生成如下:

#Results

training loss is 0.041 and training accuracy is 22657/23000 98.51 validation loss is 0.043 and validation accuracy is 1969/2000 98.45 training loss is 0.04 and training accuracy is 22697/23000 98.68 validation loss is 0.043 and validation accuracy is 1970/2000 98.5

使用增强数据训练模型提高了 0.1%的模型准确性,仅运行两个 epoch 即可。如果继续运行更多 epoch,可以进一步提高准确性。如果你在阅读书籍时一直在训练这些模型,你会意识到每个 epoch 的训练可能会超过几分钟,具体取决于你使用的 GPU。让我们看一下一种技术,可以在几秒钟内完成每个 epoch 的训练。

计算预卷积特征

当我们冻结卷积层并训练模型时,完全连接层(或称为密集层)的输入(vgg.classifier)始终保持不变。为了更好地理解,让我们将卷积块,在我们的例子中是vgg.features块,视为一个已经学习权重且在训练过程中不会改变的函数。因此,计算卷积特征并存储它们将有助于提高训练速度。由于我们只需计算这些特征一次,而不是每个 epoch 都计算,因此模型训练的时间大大缩短。让我们通过可视化的方式理解并实现相同的过程:

第一个框显示了通常的训练方式,这可能会很慢,因为我们为每个 epoch 计算卷积特征,尽管这些值不会改变。在底部框中,我们只计算一次卷积特征并仅训练线性层。为了计算预卷积特征,我们将所有训练数据通过卷积块,并将它们存储起来。为此,我们需要选择 VGG 模型的卷积块。幸运的是,VGG16 的 PyTorch 实现有两个连续模型,因此仅选择第一个连续模型的特征就足够了。以下代码实现了这一点:

vgg = models.vgg16(pretrained=True)
vgg = vgg.cuda()
features = vgg.features

train_data_loader = torch.utils.data.DataLoader(train,batch_size=32,num_workers=3,shuffle=False)
valid_data_loader = torch.utils.data.DataLoader(valid,batch_size=32,num_workers=3,shuffle=False)

def preconvfeat(dataset,model):
    conv_features = []
    labels_list = []
    for data in dataset:
        inputs,labels = data
        if is_cuda:
            inputs , labels = inputs.cuda(),labels.cuda() 
        inputs , labels = Variable(inputs),Variable(labels)
        output = model(inputs)
        conv_features.extend(output.data.cpu().numpy())
        labels_list.extend(labels.data.cpu().numpy())
    conv_features = np.concatenate([[feat] for feat in conv_features])

    return (conv_features,labels_list)

conv_feat_train,labels_train = preconvfeat(train_data_loader,features)
conv_feat_val,labels_val = preconvfeat(valid_data_loader,features)

在前面的代码中,preconvfeat方法接收数据集和vgg模型,并返回了卷积特征以及与之相关联的标签。其余的代码与我们在其他示例中用于创建数据加载器和数据集的代码类似。

一旦我们对trainvalidation集合有了卷积特征,让我们创建 PyTorch 数据集和DataLoader类,这将简化我们的训练过程。以下代码创建了用于我们卷积特征的DatasetDataLoader

class My_dataset(Dataset):
    def __init__(self,feat,labels):
        self.conv_feat = feat
        self.labels = labels

    def __len__(self):
        return len(self.conv_feat)

    def __getitem__(self,idx):
        return self.conv_feat[idx],self.labels[idx]

train_feat_dataset = My_dataset(conv_feat_train,labels_train)
val_feat_dataset = My_dataset(conv_feat_val,labels_val)

train_feat_loader = 
  DataLoader(train_feat_dataset,batch_size=64,shuffle=True)
val_feat_loader = 
  DataLoader(val_feat_dataset,batch_size=64,shuffle=True)

由于我们有新的数据加载器,可以生成带有标签的卷积特征批次,我们可以使用在其他示例中使用过的相同的train函数。现在我们将使用vgg.classifier作为模型,用于创建optimizerfit方法。以下代码训练分类器模块以识别狗和猫。在 Titan X GPU 上,每个 epoch 的时间少于五秒,这相比于原本可能需要几分钟:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,20):
    epoch_loss, epoch_accuracy = fit_numpy(epoch,vgg.classifier,train_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit_numpy(epoch,vgg.classifier,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

理解 CNN 模型学习到了什么

深度学习模型通常被认为是不可解释的。但是,正在探索不同的技术来解释这些模型内部发生的事情。对于图像,卷积网络学习到的特征是可解释的。我们将探索两种流行的技术来理解卷积网络。

可视化中间层的输出

可视化中间层的输出将帮助我们理解输入图像如何在不同层次中进行转换。通常,每层的输出称为激活。为此,我们应该从中间层提取输出,可以通过不同的方式实现。PyTorch 提供了一个称为 register_forward_hook 的方法,允许我们传递一个函数来提取特定层的输出。

默认情况下,PyTorch 模型仅存储最后一层的输出,以便最优化内存使用。因此,在我们检查中间层激活之前,让我们先了解如何从模型中提取输出。让我们看下面的代码片段,该片段提取了输出,我们将逐步分析其过程:

vgg = models.vgg16(pretrained=True).cuda()

class LayerActivations():
    features=None

    def __init__(self,model,layer_num):
        self.hook = model[layer_num].register_forward_hook(self.hook_fn)

    def hook_fn(self,module,input,output):
        self.features = output.cpu()

    def remove(self):
        self.hook.remove()

conv_out = LayerActivations(vgg.features,0)

o = vgg(Variable(img.cuda()))

conv_out.remove()

act = conv_out.features

我们从一个预训练的 VGG 模型开始,从中提取特定层的输出。LayerActivations 类指示 PyTorch 将层的输出存储到 features 变量中。让我们逐个检查 LayerActivations 类中的每个函数。

_init_ 函数接受模型和需要提取输出的层号作为参数。我们在该层上调用 register_forward_hook 方法并传递一个函数。当 PyTorch 进行前向传播——即图像通过各层时——会调用传递给 register_forward_hook 方法的函数。该方法返回一个句柄,可用于注销传递给 register_forward_hook 方法的函数。

register_forward_hook 方法向我们传递了三个值,这些值会传递给我们传递给它的函数。module 参数允许我们访问层本身。第二个参数是 input,指的是流经该层的数据。第三个参数是 output,允许访问转换后的输入或层的激活。我们将输出存储到 LayerActivations 类的 features 变量中。

第三个函数从 _init_ 函数获取 hook 并注销函数。现在我们可以传递模型和我们希望查找激活的层号。让我们看看以下图像不同层级的激活:

让我们可视化第一个卷积层创建的一些激活及其使用的代码:

fig = plt.figure(figsize=(20,50))
fig.subplots_adjust(left=0,right=1,bottom=0,top=0.8,hspace=0,
  wspace=0.2)
for i in range(30):
    ax = fig.add_subplot(12,5,i+1,xticks=[],yticks=[])
    ax.imshow(act[0][i])

让我们可视化第五个卷积层创建的一些激活:

让我们看看最后一个 CNN 层:

从观察不同层生成的内容可以看出,早期层检测线条和边缘,而最后的层倾向于学习更高级的特征,并且不那么可解释。在我们继续可视化权重之前,让我们看看在 ReLU 层之后特征映射或激活是如何呈现的。因此,让我们来可视化第二层的输出。

如果您快速查看前一图像的第二行中的第五幅图像,它看起来像是在检测图像中的眼睛。当模型性能不佳时,这些可视化技巧可以帮助我们理解模型可能出现问题的原因。

可视化 CNN 层的权重

获得特定层的模型权重非常简单。所有模型权重可以通过state_dict函数访问。state_dict函数返回一个字典,其中键是层,值是其权重。以下代码演示了如何提取特定层的权重并进行可视化:

vgg.state_dict().keys()
cnn_weights = vgg.state_dict()['features.0.weight'].cpu()

前面的代码给出了以下输出:

每个框表示大小为3 x 3的过滤器权重。每个过滤器都经过训练来识别图像中的某些模式。

总结

在本章中,我们学习了如何使用卷积神经网络构建图像分类器,以及如何使用预训练模型。我们介绍了通过使用这些预卷积特征加快训练过程的技巧。此外,我们了解了可以用来理解 CNN 内部运作过程的不同技术。

在下一章中,我们将学习如何使用循环神经网络处理序列数据。

第六章:序列数据和文本的深度学习

在上一章中,我们介绍了如何使用卷积神经网络CNNs)处理空间数据,并构建了图像分类器。在本章中,我们将涵盖以下主题:

  • 对于构建深度学习模型有用的文本数据的不同表示形式

  • 理解循环神经网络RNNs)及其不同实现,如长短期记忆LSTM)和门控循环单元GRU),它们支持大多数文本和序列数据的深度学习模型

  • 使用一维卷积处理序列数据

可以使用 RNN 构建的一些应用包括:

  • 文档分类器:识别推文或评论的情感,分类新闻文章

  • 序列到序列学习:用于任务如语言翻译,将英语转换为法语

  • 时间序列预测:根据前几天的商店销售详情预测商店的销售情况

处理文本数据

文本是常用的序列数据类型之一。文本数据可以看作是字符序列或单词序列。对于大多数问题,将文本视为单词序列是很常见的。深度学习序列模型如 RNN 及其变体能够从文本数据中学习重要模式,可以解决以下领域的问题:

  • 自然语言理解

  • 文档分类

  • 情感分类

这些序列模型也是各种系统的重要构建块,如问答系统QA)。

尽管这些模型在构建这些应用中非常有用,但由于其固有的复杂性,它们并不理解人类语言。这些序列模型能够成功地找到有用的模式,然后用于执行不同的任务。将深度学习应用于文本是一个快速发展的领域,每个月都有许多新技术问世。我们将介绍支持大多数现代深度学习应用的基本组件。

深度学习模型和其他机器学习模型一样,不理解文本,因此我们需要将文本转换为数值表示。将文本转换为数值表示的过程称为向量化,可以用不同的方法进行,如下所述:

  • 将文本转换为单词,并将每个单词表示为一个向量

  • 将文本转换为字符,并将每个字符表示为一个向量

  • 创建n-gram 单词并将它们表示为向量

文本数据可以分解为这些表示之一。每个文本的较小单元称为标记,将文本分解为标记的过程称为标记化。Python 中有很多强大的库可以帮助我们进行标记化。一旦我们将文本数据转换为标记,我们就需要将每个标记映射到一个向量上。一热编码和词嵌入是将标记映射到向量的两种最流行的方法。以下图表总结了将文本转换为其向量表示的步骤:

让我们更详细地看一下标记化、n-gram 表示和向量化。

标记化

给定一个句子,将其拆分为字符或单词称为标记化。有一些库,比如 spaCy,提供了复杂的标记化解决方案。让我们使用简单的 Python 函数,比如 splitlist,将文本转换为标记。

为了演示标记化在字符和单词上的工作原理,让我们考虑电影《雷神 3:诸神黄昏》的简短评论。我们将使用以下文本:

这部电影的动作场面非常出色。在 MCU 中,Thor 从未如此史诗般。他在这部电影中表现得相当史诗,绝对不再是无能为力。Thor 在这部电影中释放了自我,我喜欢这一点。

将文本转换为字符

Python 的 list 函数接受一个字符串并将其转换为单个字符的列表。这完成了将文本转换为字符的任务。以下代码块展示了使用的代码和结果:

thor_review = "the action scenes were top notch in this movie. Thor has never been this epic in the MCU. He does some pretty epic sh*t in this movie and he is definitely not under-powered anymore. Thor in unleashed in this, I love that."

print(list(thor_review))

结果如下:


#Results
['t', 'h', 'e', ' ', 'a', 'c', 't', 'i', 'o', 'n', ' ', 's', 'c', 'e', 'n', 'e', 's', ' ', 'w', 'e', 'r', 'e', ' ', 't', 'o', 'p', ' ', 'n', 'o', 't', 'c', 'h', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ' ', 'm', 'o', 'v', 'i', 'e', '.', ' ', 'T', 'h', 'o', 'r', ' ', 'h', 'a', 's', ' ', 'n', 'e', 'v', 'e', 'r', ' ', 'b', 'e', 'e', 'n', ' ', 't', 'h', 'i', 's', ' ', 'e', 'p', 'i', 'c', ' ', 'i', 'n', ' ', 't', 'h', 'e', ' ', 'M', 'C', 'U', '.', ' ', 'H', 'e', ' ', 'd', 'o', 'e', 's', ' ', 's', 'o', 'm', 'e', ' ', 'p', 'r', 'e', 't', 't', 'y', ' ', 'e', 'p', 'i', 'c', ' ', 's', 'h', '*', 't', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ' ', 'm', 'o', 'v', 'i', 'e', ' ', 'a', 'n', 'd', ' ', 'h', 'e', ' ', 'i', 's', ' ', 'd', 'e', 'f', 'i', 'n', 'i', 't', 'e', 'l', 'y', ' ', 'n', 'o', 't', ' ', 'u', 'n', 'd', 'e', 'r', '-', 'p', 'o', 'w', 'e', 'r', 'e', 'd', ' ', 'a', 'n', 'y', 'm', 'o', 'r', 'e', '.', ' ', 'T', 'h', 'o', 'r', ' ', 'i', 'n', ' ', 'u', 'n', 'l', 'e', 'a', 's', 'h', 'e', 'd', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ',', ' ', 'I', ' ', 'l', 'o', 'v', 'e', ' ', 't', 'h', 'a', 't', '.']

这个结果展示了我们简单的 Python 函数如何将文本转换为标记。

将文本转换为单词

我们将使用 Python 字符串对象中的 split 函数将文本分割成单词。split 函数接受一个参数,根据此参数将文本分割为标记。对于我们的示例,我们将使用空格作为分隔符。以下代码块演示了如何使用 Python 的 split 函数将文本转换为单词:

print(Thor_review.split())

#Results

['the', 'action', 'scenes', 'were', 'top', 'notch', 'in', 'this', 'movie.', 'Thor', 'has', 'never', 'been', 'this', 'epic', 'in', 'the', 'MCU.', 'He', 'does', 'some', 'pretty', 'epic', 'sh*t', 'in', 'this', 'movie', 'and', 'he', 'is', 'definitely', 'not', 'under-powered', 'anymore.', 'Thor', 'in', 'unleashed', 'in', 'this,', 'I', 'love', 'that.']

在上述代码中,我们没有使用任何分隔符;split 函数默认在空格上分割。

N-gram 表示

我们已经看到文本可以表示为字符和单词。有时候,查看两个、三个或更多单词在一起是很有用的。N-gram 是从给定文本中提取的一组单词。在 n-gram 中,n 表示可以一起使用的单词数。让我们看一个 bigramn=2)的示例。我们使用 Python 的 nltk 包为 thor_review 生成了一个 bigram。以下代码块展示了 bigram 的结果以及生成它所使用的代码:

from nltk import ngrams

print(list(ngrams(thor_review.split(),2)))

#Results
[('the', 'action'), ('action', 'scenes'), ('scenes', 'were'), ('were', 'top'), ('top', 'notch'), ('notch', 'in'), ('in', 'this'), ('this', 'movie.'), ('movie.', 'Thor'), ('Thor', 'has'), ('has', 'never'), ('never', 'been'), ('been', 'this'), ('this', 'epic'), ('epic', 'in'), ('in', 'the'), ('the', 'MCU.'), ('MCU.', 'He'), ('He', 'does'), ('does', 'some'), ('some', 'pretty'), ('pretty', 'epic'), ('epic', 'sh*t'), ('sh*t', 'in'), ('in', 'this'), ('this', 'movie'), ('movie', 'and'), ('and', 'he'), ('he', 'is'), ('is', 'definitely'), ('definitely', 'not'), ('not', 'under-powered'), ('under-powered', 'anymore.'), ('anymore.', 'Thor'), ('Thor', 'in'), ('in', 'unleashed'), ('unleashed', 'in'), ('in', 'this,'), ('this,', 'I'), ('I', 'love'), ('love', 'that.')]

ngrams 函数接受一系列单词作为第一个参数,并将要分组的单词数作为第二个参数。以下代码块展示了三元组表示法的样子,以及用于它的代码:

print(list(ngrams(thor_review.split(),3)))

#Results

[('the', 'action', 'scenes'), ('action', 'scenes', 'were'), ('scenes', 'were', 'top'), ('were', 'top', 'notch'), ('top', 'notch', 'in'), ('notch', 'in', 'this'), ('in', 'this', 'movie.'), ('this', 'movie.', 'Thor'), ('movie.', 'Thor', 'has'), ('Thor', 'has', 'never'), ('has', 'never', 'been'), ('never', 'been', 'this'), ('been', 'this', 'epic'), ('this', 'epic', 'in'), ('epic', 'in', 'the'), ('in', 'the', 'MCU.'), ('the', 'MCU.', 'He'), ('MCU.', 'He', 'does'), ('He', 'does', 'some'), ('does', 'some', 'pretty'), ('some', 'pretty', 'epic'), ('pretty', 'epic', 'sh*t'), ('epic', 'sh*t', 'in'), ('sh*t', 'in', 'this'), ('in', 'this', 'movie'), ('this', 'movie', 'and'), ('movie', 'and', 'he'), ('and', 'he', 'is'), ('he', 'is', 'definitely'), ('is', 'definitely', 'not'), ('definitely', 'not', 'under-powered'), ('not', 'under-powered', 'anymore.'), ('under-powered', 'anymore.', 'Thor'), ('anymore.', 'Thor', 'in'), ('Thor', 'in', 'unleashed'), ('in', 'unleashed', 'in'), ('unleashed', 'in', 'this,'), ('in', 'this,', 'I'), ('this,', 'I', 'love'), ('I', 'love', 'that.')]

在前述代码中唯一改变的是函数的第二个参数n-值。

许多监督学习模型,例如朴素贝叶斯,使用n-gram 来改善它们的特征空间。n-gram 也用于拼写纠正和文本摘要任务。

n-gram 表示的一个挑战是它丢失了文本的顺序性质。它通常与浅层机器学习模型一起使用。这种技术在深度学习中很少使用,因为像 RNN 和 Conv1D 这样的架构可以自动学习这些表示。

向量化

有两种流行的方法可以将生成的标记映射到数字向量中,称为单热编码词嵌入。让我们通过编写一个简单的 Python 程序来了解如何将标记转换为这些向量表示。我们还将讨论每种方法的各种优缺点。

单热编码

在单热编码中,每个标记由长度为 N 的向量表示,其中N是文档中唯一单词的数量。让我们看一个简单的句子,并观察每个标记如何表示为单热编码向量。以下是句子及其相关标记表示:

一天一个苹果,医生远离你

前述句子的单热编码可以用表格格式表示如下:

An 100000000
apple 010000000
a 001000000
day 000100000
keeps 000010000
doctor 000001000
away 000000100
said 000000010
the 000000001

此表描述了标记及其单热编码表示。向量长度为 9,因为句子中有九个唯一单词。许多机器学习库已经简化了创建单热编码变量的过程。我们将编写自己的实现以便更容易理解,并可以使用相同的实现来构建后续示例所需的其他特征。以下代码包含了一个Dictionary类,其中包含创建唯一单词字典以及返回特定单词的单热编码向量的功能。让我们看一下代码,然后逐个功能进行解释:

class Dictionary(object):
    def __init__(self):
        self.word2idx = {}
        self.idx2word = []
        self.length = 0

    def add_word(self,word):
        if word not in self.idx2word:
            self.idx2word.append(word)
            self.word2idx[word] = self.length + 1
            self.length += 1
        return self.word2idx[word]

    def __len__(self):
        return len(self.idx2word)

    def onehot_encoded(self,word):
        vec = np.zeros(self.length)
        vec[self.word2idx[word]] = 1
        return vec

前述代码提供了三个重要功能:

  • 初始化函数__init__创建了一个word2idx字典,它将存储所有唯一单词及其索引。idx2word列表存储所有唯一单词,length变量包含文档中唯一单词的总数。

  • add_word函数接受一个单词并将其添加到word2idxidx2word中,并增加词汇表的长度(假设单词是唯一的)。

  • onehot_encoded函数接受一个单词并返回一个长度为 N 的向量,其中除了单词的索引处为一之外,所有其他值都为零。如果传递的单词的索引是二,则向量在索引二处的值将为一,所有其他值将为零。

由于我们已经定义了Dictionary类,让我们在thor_review数据上使用它。以下代码演示了如何构建word2idx以及如何调用我们的onehot_encoded函数:

dic = Dictionary()

for tok in thor_review.split():
    dic.add_word(tok)

print(dic.word2idx)

前述代码的输出如下:

# Results of word2idx

{'the': 1, 'action': 2, 'scenes': 3, 'were': 4, 'top': 5, 'notch': 6, 'in': 7, 'this': 8, 'movie.': 9, 'Thor': 10, 'has': 11, 'never': 12, 'been': 13, 'epic': 14, 'MCU.': 15, 'He': 16, 'does': 17, 'some': 18, 'pretty': 19, 'sh*t': 20, 'movie': 21, 'and': 22, 'he': 23, 'is': 24, 'definitely': 25, 'not': 26, 'under-powered': 27, 'anymore.': 28, 'unleashed': 29, 'this,': 30, 'I': 31, 'love': 32, 'that.': 33}

单词were的一热编码如下:

# One-hot representation of the word 'were'
dic.onehot_encoded('were')
array([ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.])

一热表示法的一个挑战是数据过于稀疏,且随着词汇表中独特单词数量的增加,向量的大小迅速增长,这被认为是一种限制,因此在深度学习中很少使用。

词嵌入

词嵌入是在深度学习算法解决的文本数据中表示问题的非常流行的方法。词嵌入提供了一个由浮点数填充的单词的密集表示。向量维度根据词汇表的大小而变化。通常使用的词嵌入维度大小有 50、100、256、300,有时候甚至是 1,000。维度大小是我们在训练阶段需要调整的超参数之一。

如果我们试图用一热表示法表示一个大小为 20,000 的词汇表,那么我们将得到 20,000 x 20,000 个数字,其中大部分将是零。同样的词汇表可以用词嵌入表示为大小为 20,000 x 维度大小的形式,其中维度大小可以是 10、50、300 等。

创建词嵌入的一种方法是从每个标记的随机数密集向量开始,然后训练一个模型,如文档分类器或情感分类器。代表标记的浮点数将以一种使语义上更接近的单词具有类似表示的方式进行调整。为了理解它,让我们看看以下图例,其中我们在五部电影的二维图上绘制了词嵌入向量:

前述图像展示了如何调整密集向量以使语义相似的单词之间具有较小的距离。由于像SupermanThorBatman这样的电影标题是基于漫画的动作电影,它们的嵌入更接近,而电影Titanic的嵌入则远离动作电影,更接近电影Notebook,因为它们是浪漫电影。

学习词嵌入可能在数据量太少时不可行,在这种情况下,我们可以使用由其他机器学习算法训练的词嵌入。从另一个任务生成的嵌入称为预训练词嵌入。我们将学习如何构建自己的词嵌入并使用预训练词嵌入。

通过构建情感分类器训练词嵌入

在上一节中,我们简要介绍了单词嵌入的概念,但没有实现它。在本节中,我们将下载一个名为IMDB的数据集,其中包含评论,并构建一个情感分类器,用于判断评论的情感是积极的、消极的还是未知的。在构建过程中,我们还将为IMDB数据集中的单词训练单词嵌入。我们将使用一个名为torchtext的库,它通过提供不同的数据加载器和文本抽象化简化了许多与自然语言处理NLP)相关的活动。训练情感分类器将涉及以下步骤:

  1. 下载 IMDB 数据并执行文本标记化

  2. 建立词汇表

  3. 生成向量批次

  4. 创建带有嵌入的网络模型

  5. 训练模型

下载 IMDB 数据并执行文本标记化

对于与计算机视觉相关的应用程序,我们使用了torchvision库,该库为我们提供了许多实用函数,帮助构建计算机视觉应用程序。同样,还有一个名为torchtext的库,它是 PyTorch 的一部分,专门用于处理与 PyTorch 相关的许多文本活动,如下载、文本向量化和批处理。在撰写本文时,torchtext不随 PyTorch 安装而提供,需要单独安装。您可以在您的计算机命令行中运行以下代码来安装torchtext

pip install torchtext

一旦安装完成,我们将能够使用它。Torchtext 提供了两个重要的模块,称为torchtext.datatorchtext.datasets

我们可以从以下链接下载IMDB Movies数据集:

www.kaggle.com/orgesleka/imdbmovies

torchtext.data

torchtext.data实例定义了一个名为Field的类,它帮助我们定义数据的读取和标记化方式。让我们看看下面的示例,我们将用它来准备我们的IMDB数据集:

from torchtext import data
TEXT = data.Field(lower=True, batch_first=True,fix_length=20)
LABEL = data.Field(sequential=False)

在上述代码中,我们定义了两个Field对象,一个用于实际文本,另一个用于标签数据。对于实际文本,我们期望torchtext将所有文本转换为小写,标记化文本,并将其修剪为最大长度为20。如果我们为生产环境构建应用程序,可能会将长度固定为更大的数字。但是对于玩具示例,这个长度可以工作得很好。Field构造函数还接受另一个名为tokenize的参数,默认使用str.split函数。我们还可以指定 spaCy 作为参数,或任何其他的分词器。在我们的示例中,我们将坚持使用str.split

torchtext.datasets

torchtext.datasets实例提供了使用不同数据集的包装器,如 IMDB、TREC(问题分类)、语言建模(WikiText-2)和其他几个数据集。我们将使用torch.datasets下载IMDB数据集并将其分割为traintest数据集。以下代码执行此操作,当您第一次运行它时,根据您的宽带连接速度,可能需要几分钟时间从互联网上下载IMDB数据集:

train, test = datasets.IMDB.splits(TEXT, LABEL)

先前数据集的IMDB类将下载、标记和分割数据库到traintest数据集中所涉及的所有复杂性抽象化。train.fields包含一个字典,其中TEXT是键,值是LABEL。让我们来看看train.fields,而每个train元素包含:

print('train.fields', train.fields)

#Results 
train.fields {'text': <torchtext.data.field.Field object at 0x1129db160>, 'label': <torchtext.data.field.Field object at 0x1129db1d0>}

print(vars(train[0]))

#Results 

vars(train[0]) {'text': ['for', 'a', 'movie', 'that', 'gets', 'no', 'respect', 'there', 'sure', 'are', 'a', 'lot', 'of', 'memorable', 'quotes', 'listed', 'for', 'this', 'gem.', 'imagine', 'a', 'movie', 'where', 'joe', 'piscopo', 'is', 'actually', 'funny!', 'maureen', 'stapleton', 'is', 'a', 'scene', 'stealer.', 'the', 'moroni', 'character', 'is', 'an', 'absolute', 'scream.', 'watch', 'for', 'alan', '"the', 'skipper"', 'hale', 'jr.', 'as', 'a', 'police', 'sgt.'], 'label': 'pos'}

我们可以从这些结果看到,单个元素包含一个字段text,以及表示text的所有标记,还有一个包含文本标签的label字段。现在我们已经准备好对IMDB数据集进行批处理。

构建词汇表

当我们为thor_review创建一位热编码时,我们创建了一个word2idx字典,它被称为词汇表,因为它包含文档中所有唯一单词的详细信息。torchtext实例使我们更容易。一旦数据加载完毕,我们可以调用build_vocab并传递必要的参数,这些参数将负责为数据构建词汇表。以下代码显示了如何构建词汇表:

TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train)

在上述代码中,我们传递了需要构建词汇表的train对象,并要求它使用维度为300的预训练嵌入来初始化向量。build_vocab对象只是下载并创建将在以后训练情感分类器时使用的维度。max_size实例限制了词汇表中单词的数量,而min_freq则移除了出现次数不到十次的任何单词,其中10是可配置的。

一旦词汇表构建完成,我们可以获取不同的值,如频率、单词索引和每个单词的向量表示。以下代码演示了如何访问这些值:

print(TEXT.vocab.freqs)

# A sample result 
Counter({"i'm": 4174,
         'not': 28597,
         'tired': 328,
         'to': 133967,
         'say': 4392,
         'this': 69714,
         'is': 104171,
         'one': 22480,
         'of': 144462,
         'the': 322198,

下面的代码演示了如何访问结果:


print(TEXT.vocab.vectors)

#Results displaying the 300 dimension vector for each word.
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
      ... ⋱ ... 
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.7724 -0.1800 0.2072 ... 0.6736 0.2263 -0.2919
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
[torch.FloatTensor of size 10002x300]

print(TEXT.vocab.stoi)

# Sample results
defaultdict(<function torchtext.vocab._default_unk_index>,
            {'<unk>': 0,
             '<pad>': 1,
             'the': 2,
             'a': 3,
             'and': 4,
             'of': 5,
             'to': 6,
             'is': 7,
             'in': 8,
             'i': 9,
             'this': 10,
             'that': 11,
             'it': 12,

stoi提供了一个包含单词及其索引的字典。

生成向量的批次

Torchtext 提供了BucketIterator,它有助于对所有文本进行批处理,并用单词的索引号替换这些单词。BucketIterator实例带有许多有用的参数,如batch_sizedevice(GPU 或 CPU)和shuffle(数据是否需要洗牌)。以下代码演示了如何创建迭代器以为traintest数据集生成批次:

train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=128, device=-1,shuffle=True)
#device = -1 represents cpu , if u want gpu leave it to None.

上述代码为traintest数据集提供了一个BucketIterator对象。以下代码将展示如何创建一个批次并显示批次的结果:

batch = next(iter(train_iter))
batch.text

#Results
Variable containing:
 5128 427 19 ... 1688 0 542
   58 2 0 ... 2 0 1352
    0 9 14 ... 2676 96 9
       ... ⋱ ... 
  129 1181 648 ... 45 0 2
 6484 0 627 ... 381 5 2
  748 0 5052 ... 18 6660 9827
[torch.LongTensor of size 128x20]

batch.label

#Results
Variable containing:
 2
 1
 2
 1
 2
 1
 1
 1
[torch.LongTensor of size 128]

从前面的代码块的结果中,我们可以看到文本数据如何转换为大小为(batch_size * fix_len)的矩阵,即(128x20)。

创建带嵌入的网络模型

我们之前简要讨论了词嵌入。在本节中,我们将词嵌入作为网络架构的一部分,并训练整个模型来预测每个评论的情感。训练结束时,我们将得到一个情感分类器模型,以及针对IMDB数据集的词嵌入。以下代码展示了如何创建一个使用词嵌入来预测情感的网络架构:

class EmbNet(nn.Module):
    def __init__(self,emb_size,hidden_size1,hidden_size2=400):
        super().__init__()
        self.embedding = nn.Embedding(emb_size,hidden_size1)
        self.fc = nn.Linear(hidden_size2,3)

    def forward(self,x):
        embeds = self.embedding(x).view(x.size(0),-1)
        out = self.fc(embeds)
        return F.log_softmax(out,dim=-1)

在前面的代码中,EmbNet创建了用于情感分类的模型。在__init__函数内部,我们初始化了nn.Embedding类的一个对象,它接受两个参数,即词汇表的大小和我们希望为每个单词创建的维度。由于我们限制了唯一单词的数量,词汇表大小将为 10,000,并且我们可以从小的嵌入大小10开始。对于快速运行程序,小的嵌入大小是有用的,但当您尝试为生产系统构建应用程序时,请使用较大的嵌入。我们还有一个线性层,将词嵌入映射到类别(积极、消极或未知)。

forward函数确定如何处理输入数据。对于批量大小为 32 且最大长度为 20 个单词的句子,我们将得到形状为 32 x 20 的输入。第一个嵌入层充当查找表,将每个单词替换为相应的嵌入向量。对于嵌入维度为 10,输出将变为 32 x 20 x 10,因为每个单词都被其相应的嵌入替换。view()函数将扁平化嵌入层的结果。传递给view的第一个参数将保持该维度不变。在我们的情况下,我们不想组合来自不同批次的数据,因此保留第一个维度并扁平化张量中的其余值。应用view函数后,张量形状变为 32 x 200。密集层将扁平化的嵌入映射到类别数量。定义了网络架构后,我们可以像往常一样训练网络。

请记住,在这个网络中,我们失去了文本的顺序性,只是把它们作为一个词袋来使用。

训练模型

训练模型与构建图像分类器非常相似,因此我们将使用相同的函数。我们通过模型传递数据批次,计算输出和损失,然后优化模型权重,包括嵌入权重。以下代码执行此操作:

def fit(epoch,model,data_loader,phase='training',volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , batch in enumerate(data_loader):
        text , target = batch.text , batch.label
        if is_cuda:
            text,target = text.cuda(),target.cuda()

        if phase == 'training':
            optimizer.zero_grad()
        output = model(text)
        loss = F.nll_loss(output,target)

        running_loss += F.nll_loss(output,target,size_average=False).data[0]
        preds = output.data.max(dim=1,keepdim=True)[1]
        running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()

    loss = running_loss/len(data_loader.dataset)
    accuracy = 100\. * running_correct/len(data_loader.dataset)

    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]

train_iter.repeat = False
test_iter.repeat = False

for epoch in range(1,10):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

在上述代码中,我们通过传递我们为批处理数据创建的 BucketIterator 对象调用 fit 方法。迭代器默认不会停止生成批次,因此我们必须将 BucketIterator 对象的 repeat 变量设置为 False。如果我们不将 repeat 变量设置为 False,则 fit 函数将无限运行。在大约 10 个 epochs 的训练后,模型的验证准确率约为 70%。

使用预训练的词嵌入

在特定领域(如医学和制造业)工作时,预训练的词嵌入非常有用,因为我们有大量数据可以训练嵌入。当我们有少量数据无法进行有意义的训练时,我们可以使用在不同数据集(如维基百科、Google 新闻和 Twitter 推文)上训练的嵌入。许多团队都有使用不同方法训练的开源词嵌入。在本节中,我们将探讨 torchtext 如何简化使用不同词嵌入,并如何在我们的 PyTorch 模型中使用它们。这类似于我们在计算机视觉应用中使用的迁移学习。通常,使用预训练嵌入会涉及以下步骤:

  • 下载嵌入

  • 加载模型中的嵌入

  • 冻结嵌入层权重

让我们详细探讨每个步骤的实现方式。

下载嵌入

torchtext 库在下载嵌入并将其映射到正确单词中,抽象出了许多复杂性。Torchtext 在 vocab 模块中提供了三个类,分别是 GloVeFastTextCharNGram,它们简化了下载嵌入和映射到我们词汇表的过程。每个类别提供了在不同数据集上训练的不同嵌入,使用了不同的技术。让我们看一些提供的不同嵌入:

  • charngram.100d

  • fasttext.en.300d

  • fasttext.simple.300d

  • glove.42B.300d

  • glove.840B.300d

  • glove.twitter.27B.25d

  • glove.twitter.27B.50d

  • glove.twitter.27B.100d

  • glove.twitter.27B.200d

  • `glove.6B.50d`

  • glove.6B.100d

  • glove.6B.200d

  • glove.6B.300d

Field 对象的 build_vocab 方法接受一个用于嵌入的参数。以下代码解释了我们如何下载这些嵌入:

from torchtext.vocab import GloVe
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)

参数向量的值表示使用的嵌入类别。namedim 参数确定可以使用的嵌入。我们可以轻松地从 vocab 对象中访问嵌入。以下代码演示了它,同时展示了结果的样子:

TEXT.vocab.vectors

#Output
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
          ... ⋱ ... 
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.7724 -0.1800 0.2072 ... 0.6736 0.2263 -0.2919
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
[torch.FloatTensor of size 10002x300]

现在我们已经下载并将嵌入映射到我们的词汇表中。让我们了解如何在 PyTorch 模型中使用它们。

加载模型中的嵌入

vectors变量返回一个形状为vocab_size x dimensions的 torch 张量,其中包含预训练的嵌入。我们必须将这些嵌入的权重存储到我们的嵌入层权重中。我们可以通过访问嵌入层权重来分配嵌入的权重,如下面的代码所示。

model.embedding.weight.data = TEXT.vocab.vectors

model代表我们网络的对象,embedding代表嵌入层。因为我们使用了新维度的嵌入层,线性层输入会有些变化。下面的代码展示了新的架构,与我们之前训练嵌入时使用的架构类似:

class EmbNet(nn.Module):
    def __init__(self,emb_size,hidden_size1,hidden_size2=400):
        super().__init__()
        self.embedding = nn.Embedding(emb_size,hidden_size1)
        self.fc1 = nn.Linear(hidden_size2,3)

    def forward(self,x):
        embeds = self.embedding(x).view(x.size(0),-1)
        out = self.fc1(embeds)
        return F.log_softmax(out,dim=-1)

model = EmbNet(len(TEXT.vocab.stoi),300,12000)

加载嵌入向量后,我们必须确保在训练过程中不改变嵌入层权重。让我们讨论如何实现这一点。

冻结嵌入层权重

告诉 PyTorch 不要改变嵌入层权重是一个两步骤过程:

  1. requires_grad属性设置为False,告诉 PyTorch 不需要这些权重的梯度。

  2. 移除传递给优化器的嵌入层参数。如果不执行此步骤,则优化器会抛出错误,因为它期望所有参数都有梯度。

下面的代码展示了如何轻松冻结嵌入层权重,并告知优化器不使用这些参数:

model.embedding.weight.requires_grad = False
optimizer = optim.SGD([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)

通常我们将所有模型参数传递给优化器,但在前面的代码中,我们传递了requires_gradTrue的参数。

我们可以使用这段代码训练模型,并应该获得类似的准确度。所有这些模型架构都没有利用文本的序列性质。在下一节中,我们将探讨两种流行的技术,即 RNN 和 Conv1D,它们利用数据的序列性质。

递归神经网络

RNN 是最强大的模型之一,使我们能够处理分类、序列数据标签和文本生成等应用(例如SwiftKey键盘应用可以预测下一个词),以及将一种序列转换为另一种语言,例如从法语到英语。大多数模型架构如前馈神经网络没有利用数据的序列性质。例如,我们需要数据来表示每个示例的特征向量,比如代表句子、段落或文档的所有标记。前馈网络设计只是一次性查看所有特征并将其映射到输出。让我们看一个文本示例,展示为什么文本的顺序或序列性质很重要。I had cleaned my carI had my car cleaned 是两个英语句子,包含相同的单词集合,但只有在考虑单词顺序时它们的含义不同。

人类通过从左到右阅读单词并构建一个强大的模型来理解文本数据。RNN 的工作方式略有相似,它一次查看文本中的一个单词。RNN 也是一种神经网络,其中有一个特殊的层,该层循环处理数据而不是一次性处理所有数据。由于 RNN 可以按顺序处理数据,我们可以使用不同长度的向量并生成不同长度的输出。以下图像提供了一些不同的表示形式:

图片来源:karpathy.github.io/2015/05/21/rnn-effectiveness/

上述图片来自 RNN 的一个著名博客(karpathy.github.io/2015/05/21/rnn-effectiveness),作者 Andrej Karpathy 讲解如何使用 Python 从头构建 RNN,并将其用作序列生成器。

通过示例理解 RNN 的工作原理

让我们从假设我们已经构建了一个 RNN 模型开始,并尝试理解它提供了哪些功能。一旦我们了解了 RNN 的功能,然后让我们探索 RNN 内部发生了什么。

让我们将《雷神》影评作为 RNN 模型的输入。我们正在查看的示例文本是这部电影的动作场面非常棒……。我们首先将第一个词the传递给我们的模型;模型生成两种不同的东西,一个状态向量和一个输出向量。状态向量在处理评论中的下一个词时传递给模型,并生成一个新的状态向量。我们只考虑在最后一个序列中模型生成的输出。下图总结了这一过程:

上述图示示了以下内容:

  • 如何通过展开和图像来理解 RNN 的工作方式

  • 如何递归地将状态传递给相同的模型

到现在为止,您应该已经了解了 RNN 的作用,但还不清楚它是如何工作的。在我们深入了解其工作原理之前,让我们先看一个代码片段,详细展示我们所学的内容。我们仍然将 RNN 视为一个黑匣子:

rnn = RNN(input_size, hidden_size,output_size)
for i in range(len(Thor_review):
        output, hidden = rnn(thor_review[i], hidden)

在前面的代码中,hidden变量表示状态向量,有时称为隐藏状态。到现在为止,我们应该已经了解了 RNN 的使用方式。现在,让我们看一下实现 RNN 并理解 RNN 内部发生了什么的代码。以下代码包含RNN类:

import torch.nn as nn
from torch.autograd import Variable

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()
        self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        combined = torch.cat((input, hidden), 1)
        hidden = self.i2h(combined)
        output = self.i2o(combined)
        output = self.softmax(output)
        return output, hidden

    def initHidden(self):
        return Variable(torch.zeros(1, self.hidden_size))

除了前面代码中的词语RNN外,其他内容听起来与我们在前几章中使用的内容非常相似,因为 PyTorch 隐藏了很多反向传播的复杂性。让我们逐步讲解init函数和forward函数,以理解发生了什么。

__init__函数初始化两个线性层,一个用于计算输出,另一个用于计算状态或隐藏向量。

forward函数将input向量和hidden向量结合起来,并通过两个线性层将其传递,生成一个输出向量和一个隐藏状态。对于output层,我们应用了log_softmax函数。

initHidden函数帮助创建没有状态的隐藏向量,用于第一次调用 RNN。让我们通过下图视觉地了解RNN类的作用:

上述图展示了 RNN 的工作方式。

第一次遇到 RNN 这些概念有时可能会感到棘手,因此我强烈建议阅读以下链接提供的一些令人惊叹的博客:karpathy.github.io/2015/05/21/rnn-effectiveness/colah.github.io/posts/2015-08-Understanding-LSTMs/.

在接下来的部分中,我们将学习如何使用一种称为LSTM的变种 RNN 来构建一个情感分类器,用于IMDB数据集。

LSTM

RNN 在构建语言翻译、文本分类等许多实际应用中非常流行,但在现实中,我们很少会使用我们在前面部分看到的普通 RNN 版本。普通 RNN 存在梯度消失和梯度爆炸等问题,处理大序列时尤为突出。在大多数真实世界的问题中,会使用像 LSTM 或 GRU 这样的 RNN 变体,这些变体解决了普通 RNN 的局限性,并且能更好地处理序列数据。我们将尝试理解 LSTM 中发生的情况,并基于 LSTM 构建一个网络来解决IMDB数据集上的文本分类问题。

长期依赖

RNN 在理论上应该从历史数据中学习所有必要的依赖关系,以建立下文的上下文。例如,假设我们试图预测句子“the clouds are in the sky”的最后一个词。RNN 可以预测,因为信息(clouds)仅仅落后几个词。再来看一个更长的段落,依赖关系不必那么紧密,我们想预测其中的最后一个词。句子看起来像“我出生在金奈,一个坐落在泰米尔纳德邦的城市。在印度的不同州接受教育,我说...”。在实践中,普通版本的 RNN 很难记住序列前部发生的上下文。LSTM 及其他不同的 RNN 变体通过在 LSTM 内部添加不同的神经网络来解决这个问题,后者决定可以记住多少或者可以记住什么数据。

LSTM 网络

LSTM 是一种特殊类型的 RNN,能够学习长期依赖关系。它们于 1997 年引入,并随着可用数据和硬件的进展在过去几年中变得流行起来。它们在各种问题上表现出色,并被广泛使用。

LSTM 的设计旨在通过一种设计自然地记住信息以解决长期依赖问题。在 RNN 中,我们看到它们如何在序列的每个元素上重复自身。在标准 RNN 中,重复模块将具有像单个线性层的简单结构。

下图显示了一个简单的 RNN 如何重复自身:

在 LSTM 内部,我们不是使用简单的线性层,而是在 LSTM 内部有较小的网络,这些网络完成独立的工作。以下图表展示了 LSTM 内部发生的情况:

图像来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/(由 Christopher Olah 绘制的图表)

在上述图表中,第二个框中的每个小矩形(黄色)代表一个 PyTorch 层,圆圈表示元素矩阵或向量加法,合并线表示两个向量正在连接。好消息是,我们不需要手动实现所有这些。大多数现代深度学习框架提供了一个抽象层,可以处理 LSTM 内部的所有功能。PyTorch 提供了 nn.LSTM 层的抽象,我们可以像使用任何其他层一样使用它。LSTM 中最重要的是通过所有迭代传递的细胞状态,如上述图表中横跨细胞的水平线所示。LSTM 内部的多个网络控制信息如何在细胞状态之间传播。LSTM 中的第一步(由符号 σ 表示的小网络)是决定从细胞状态中丢弃哪些信息。这个网络称为 遗忘门,具有 sigmoid 作为激活函数,为细胞状态中的每个元素输出介于 0 和 1 之间的值。该网络(PyTorch 层)由以下公式表示:

网络的值决定了哪些值将保存在细胞状态中,哪些将被丢弃。下一步是决定要添加到细胞状态的信息。这有两个部分;一个称为 输入门 的 sigmoid 层,决定要更新的值;一个 tanh 层,用于创建要添加到细胞状态的新值。数学表示如下:

在下一步中,我们将输入门生成的两个值与 tanh 结合。现在我们可以通过以下公式更新细胞状态,即在遗忘门和 i[t ]与 C[t] 乘积之和的逐元素乘法之间,如下所示:

最后,我们需要决定输出,这将是细胞状态的过滤版本。有不同版本的 LSTM 可用,它们大多数都基于类似的原理运作。作为开发人员或数据科学家,我们很少需要担心 LSTM 内部的运作。如果您想更多了解它们,请阅读以下博客链接,这些链接以非常直观的方式涵盖了大量理论。

查看 Christopher Olah 关于 LSTM 的精彩博客(colah.github.io/posts/2015-08-Understanding-LSTMs),以及 Brandon Rohrer 的另一篇博客(brohrer.github.io/how_rnns_lstm_work.html),他在一个很棒的视频中解释了 LSTM。

由于我们理解了 LSTM,让我们实现一个 PyTorch 网络,我们可以用来构建情感分类器。像往常一样,我们将按照以下步骤创建分类器:

  1. 准备数据

  2. 创建批次

  3. 创建网络

  4. 训练模型

准备数据

我们使用相同的 torchtext 来下载、分词和构建IMDB数据集的词汇表。在创建Field对象时,我们将batch_first参数保留为False。RNN 网络期望数据的形式是Sequence_lengthbatch_size和特征。以下用于准备数据集:

TEXT = data.Field(lower=True,fix_length=200,batch_first=False)
LABEL = data.Field(sequential=False,)
train, test = IMDB.splits(TEXT, LABEL)
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)

创建批次

我们使用 torchtext 的BucketIterator来创建批次,批次的大小将是序列长度和批次大小。对于我们的情况,大小将为[200, 32],其中200是序列长度,32是批次大小。

以下是用于分批的代码:

train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=32, device=-1)
train_iter.repeat = False
test_iter.repeat = False

创建网络

让我们看看代码,然后逐步分析代码。您可能会对代码看起来多么相似感到惊讶:

class IMDBRnn(nn.Module):

    def __init__(self,vocab,hidden_size,n_cat,bs=1,nl=2):
        super().__init__()
        self.hidden_size = hidden_size
        self.bs = bs
        self.nl = nl
        self.e = nn.Embedding(n_vocab,hidden_size)
        self.rnn = nn.LSTM(hidden_size,hidden_size,nl)
        self.fc2 = nn.Linear(hidden_size,n_cat)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self,inp):
        bs = inp.size()[1]
        if bs != self.bs:
            self.bs = bs
        e_out = self.e(inp)
        h0 = c0 = Variable(e_out.data.new(*(self.nl,self.bs,self.hidden_size)).zero_())
        rnn_o,_ = self.rnn(e_out,(h0,c0)) 
        rnn_o = rnn_o[-1]
        fc = F.dropout(self.fc2(rnn_o),p=0.8)
        return self.softmax(fc)

init方法创建一个与词汇表大小和hidden_size相同的嵌入层。它还创建一个 LSTM 和一个线性层。最后一层是一个LogSoftmax层,用于将线性层的结果转换为概率。

forward函数中,我们传递大小为[200, 32]的输入数据,它经过嵌入层处理,批次中的每个标记都被嵌入替换,大小变为[200, 32, 100],其中100是嵌入维度。LSTM 层使用嵌入层的输出以及两个隐藏变量进行处理。隐藏变量应与嵌入输出相同类型,并且它们的大小应为[num_layers, batch_size, hidden_size]。LSTM 按序列处理数据并生成形状为[Sequence_length, batch_size, hidden_size]的输出,其中每个序列索引表示该序列的输出。在本例中,我们只取最后一个序列的输出,其形状为[batch_size, hidden_dim],然后将其传递给线性层以映射到输出类别。由于模型容易过拟合,添加一个 dropout 层。您可以调整 dropout 的概率。

训练模型

创建网络后,我们可以使用与前面示例中相同的代码来训练模型。以下是训练模型的代码:

model = IMDBRnn(n_vocab,n_hidden,3,bs=32)
model = model.cuda()

optimizer = optim.Adam(model.parameters(),lr=1e-3)

def fit(epoch,model,data_loader,phase='training',volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , batch in enumerate(data_loader):
        text , target = batch.text , batch.label
        if is_cuda:
            text,target = text.cuda(),target.cuda()

        if phase == 'training':
            optimizer.zero_grad()
        output = model(text)
        loss = F.nll_loss(output,target)

        running_loss += F.nll_loss(output,target,size_average=False).data[0]
        preds = output.data.max(dim=1,keepdim=True)[1]
        running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()

    loss = running_loss/len(data_loader.dataset)
    accuracy = 100\. * running_correct/len(data_loader.dataset)

    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]

for epoch in range(1,5):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

以下是训练模型的结果:

#Results

training loss is   0.7 and training accuracy is 12564/25000     50.26
validation loss is   0.7 and validation accuracy is 12500/25000      50.0
training loss is  0.66 and training accuracy is 14931/25000     59.72
validation loss is  0.57 and validation accuracy is 17766/25000     71.06
training loss is  0.43 and training accuracy is 20229/25000     80.92
validation loss is   0.4 and validation accuracy is 20446/25000     81.78
training loss is   0.3 and training accuracy is 22026/25000      88.1
validation loss is  0.37 and validation accuracy is 21009/25000     84.04

对模型进行四次纪元的训练得到了 84%的准确率。训练更多的纪元导致了一个过拟合的模型,因为损失开始增加。我们可以尝试一些我们尝试过的技术,如减少隐藏维度、增加序列长度以及以更小的学习率进行训练以进一步提高准确性。

我们还将探讨如何使用一维卷积来对序列数据进行训练。

序列数据上的卷积网络

我们学习了 CNN 如何通过从图像中学习特征解决计算机视觉问题。在图像中,CNN 通过在高度和宽度上进行卷积来工作。同样,时间可以被视为一个卷积特征。一维卷积有时比 RNN 表现更好,并且计算成本更低。在过去几年中,像 Facebook 这样的公司已经展示出在音频生成和机器翻译方面的成功。在本节中,我们将学习如何使用 CNN 构建文本分类解决方案。

理解序列数据上的一维卷积

在第五章 计算机视觉的深度学习 中,我们看到了如何从训练数据中学习二维权重。这些权重在图像上移动以生成不同的激活。类似地,一维卷积激活在我们的文本分类器训练中也是学习的,这些权重通过在数据上移动来学习模式。以下图解释了一维卷积是如何工作的:

为了在IMDB数据集上训练文本分类器,我们将按照构建使用 LSTM 分类器的步骤进行操作。唯一不同的是,我们使用batch_first = True,而不像我们的 LSTM 网络那样。因此,让我们看看网络,训练代码以及其结果。

创建网络

让我们先看看网络架构,然后逐步分析代码:

class IMDBCnn(nn.Module):

    def __init__(self,vocab,hidden_size,n_cat,bs=1,kernel_size=3,max_len=200):
        super().__init__()
        self.hidden_size = hidden_size
        self.bs = bs
    self.e = nn.Embedding(n_vocab,hidden_size)
    self.cnn = nn.Conv1d(max_len,hidden_size,kernel_size)
    self.avg = nn.AdaptiveAvgPool1d(10)
        self.fc = nn.Linear(1000,n_cat)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self,inp):
        bs = inp.size()[0]
        if bs != self.bs:
            self.bs = bs
        e_out = self.e(inp)
        cnn_o = self.cnn(e_out) 
        cnn_avg = self.avg(cnn_o)
        cnn_avg = cnn_avg.view(self.bs,-1)
        fc = F.dropout(self.fc(cnn_avg),p=0.5)
        return self.softmax(fc)

在上面的代码中,我们有一个Conv1d层和一个AdaptiveAvgPool1d层而不是一个 LSTM 层。卷积层接受序列长度作为其输入大小,输出大小为隐藏大小,内核大小为 3。由于我们必须改变线性层的维度,每次尝试以不同的长度运行时,我们使用一个AdaptiveAvgpool1d,它接受任意大小的输入并生成给定大小的输出。因此,我们可以使用一个固定大小的线性层。其余代码与我们在大多数网络架构中看到的代码类似。

训练模型

模型的训练步骤与上一个例子相同。让我们看看调用fit方法的代码以及它生成的结果:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]

for epoch in range(1,5):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

我们对模型进行了四轮训练,达到了大约 83%的准确率。以下是运行模型的结果:

training loss is  0.59 and training accuracy is 16724/25000      66.9
validation loss is  0.45 and validation accuracy is 19687/25000     78.75
training loss is  0.38 and training accuracy is 20876/25000      83.5
validation loss is   0.4 and validation accuracy is 20618/25000     82.47
training loss is  0.28 and training accuracy is 22109/25000     88.44
validation loss is  0.41 and validation accuracy is 20713/25000     82.85
training loss is  0.22 and training accuracy is 22820/25000     91.28
validation loss is  0.44 and validation accuracy is 20641/25000     82.56

由于验证损失在三轮后开始增加,我停止了模型的运行。我们可以尝试几件事情来改善结果,如使用预训练权重、添加另一个卷积层,并在卷积之间尝试MaxPool1d层。我把这些尝试留给你来测试是否能提高准确率。

概要

在本章中,我们学习了不同的技术来表示深度学习中的文本数据。我们学习了如何在处理不同领域时使用预训练的词嵌入和我们自己训练的词嵌入。我们使用 LSTM 和一维卷积构建了文本分类器。

在下一章中,我们将学习如何训练深度学习算法来生成时尚图像和新图像,并生成文本。

第七章:生成网络

在前几章中,我们看到的所有示例都集中在解决分类或回归等问题上。这一章对理解深度学习如何发展以解决无监督学习问题非常有趣和重要。

在本章中,我们将训练网络,学习如何创建:

  • 基于内容和特定艺术风格生成的图像,通常称为风格迁移

  • 使用特定类型的生成对抗网络(GAN)生成新人物面孔

  • 使用语言建模生成新文本

这些技术构成了深度学习领域大部分高级研究的基础。深入探讨 GAN 和语言建模等子领域的确切细节超出了本书的范围,它们值得有专门的书籍来介绍。我们将学习它们的一般工作原理以及在 PyTorch 中构建它们的过程。

神经风格迁移

我们人类以不同的准确度和复杂性生成艺术作品。尽管创作艺术的过程可能非常复杂,但它可以被视为两个最重要因素的结合,即要绘制什么和如何绘制。绘制什么受我们周围所见的启发,而如何绘制也将受到我们周围某些事物的影响。从艺术家的角度来看,这可能是一种过度简化,但对于理解如何使用深度学习算法创建艺术作品,它非常有用。我们将训练一个深度学习算法,从一幅图像中获取内容,然后按照特定的艺术风格进行绘制。如果您是艺术家或从事创意行业,您可以直接利用近年来进行的惊人研究来改进并在您工作的领域内创造出一些很酷的东西。即使您不是,它也会向您介绍生成模型领域,其中网络生成新内容。

让我们在高层次理解神经风格迁移的过程,然后深入细节,以及构建它所需的 PyTorch 代码。风格迁移算法提供了一个内容图像(C)和一个风格图像(S),算法必须生成一个新图像(O),该图像具有来自内容图像的内容和来自风格图像的风格。这一创建神经风格迁移的过程由 Leon Gates 等人于 2015 年介绍(艺术风格的神经算法)。以下是我们将使用的内容图像(C):

图像来源:https://arxiv.org/pdf/1508.06576.pdf

下面是样式图像(S):

图像来源:https://arxiv.org/pdf/1508.06576.pdf

这是我们将要生成的图像:

图像来源:https://arxiv.org/pdf/1508.06576.pdf

从理解卷积神经网络CNNs)工作方式的角度来看,样式转移的背后思想变得直观。当 CNN 用于对象识别训练时,训练的 CNN 的早期层学习非常通用的信息,如线条、曲线和形状。CNN 的最后层捕捉图像的更高级概念,如眼睛、建筑物和树木。因此,类似图像的最后层的值倾向于更接近。我们将相同的概念应用于内容损失。内容图像和生成图像的最后一层应该是相似的,我们使用均方误差MSE)来计算相似性。我们使用优化算法降低损失值。

通过称为格拉姆矩阵的技术,CNN 通常在多个层次上捕获图像的样式。格拉姆矩阵计算跨多个层次捕获的特征映射之间的相关性。格拉姆矩阵提供计算样式的一种方法。具有类似风格的图像对于格拉姆矩阵具有相似的值。样式损失也是使用样式图像和生成图像的格拉姆矩阵之间的 MSE 计算的。

我们将使用提供在 torchvision 模型中的预训练 VGG19 模型。训练样式转移模型所需的步骤与任何其他深度学习模型类似,唯一不同的是计算损失比分类或回归模型更复杂。神经风格算法的训练可以分解为以下步骤:

  1. 加载数据。

  2. 创建 VGG19 模型。

  3. 定义内容损失。

  4. 定义样式损失。

  5. 从 VGG 模型中提取跨层的损失。

  6. 创建优化器。

  7. 训练 - 生成与内容图像类似的图像,并且风格与样式图像类似。

加载数据。

加载数据与我们在第五章中解决图像分类问题所见的方式类似,《计算机视觉深度学习》。我们将使用预训练的 VGG 模型,因此必须使用与预训练模型相同的值对图像进行标准化。

以下代码展示了我们如何做到这一点。代码大部分是自解释的,因为我们已经在前几章中详细讨论过:

#Fixing the size of the image, reduce it further if you are not using a GPU.
imsize = 512 
is_cuda = torch.cuda.is_available()

#Converting image ,making it suitable for training using the VGG model.

prep = transforms.Compose([transforms.Resize(imsize),
                           transforms.ToTensor(),
                           transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]), #turn to BGR
                           transforms.Normalize(mean=[0.40760392, 0.45795686, 0.48501961], #subtract imagenet mean
                                                std=[1,1,1]),
                           transforms.Lambda(lambda x: x.mul_(255)),
                          ])

#Converting the generated image back to a format which we can visualise. 

postpa = transforms.Compose([transforms.Lambda(lambda x: x.mul_(1./255)),
                           transforms.Normalize(mean=[-0.40760392, -0.45795686, -0.48501961], #add imagenet mean
                                                std=[1,1,1]),
                           transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]), #turn to RGB
                           ])
postpb = transforms.Compose([transforms.ToPILImage()])

#This method ensures data in the image does not cross the permissible range .
def postp(tensor): # to clip results in the range [0,1]
    t = postpa(tensor)
    t[t>1] = 1 
    t[t<0] = 0
    img = postpb(t)
    return img

#A utility function to make data loading easier.
def image_loader(image_name):
    image = Image.open(image_name)
    image = Variable(prep(image))
    # fake batch dimension required to fit network's input dimensions
    image = image.unsqueeze(0)
    return image

在此代码中,我们定义了三个功能,prep执行所有所需的预处理,并使用与训练 VGG 模型相同的值进行标准化。模型的输出需要恢复到其原始值;postpa函数执行所需的处理。生成的模型可能超出接受值的范围,postp函数将所有大于 1 的值限制为 1,所有小于 0 的值限制为 0。最后,image_loader函数加载图像,应用预处理转换,并将其转换为变量。以下功能加载样式和内容图像:

style_img = image_loader("Images/vangogh_starry_night.jpg")
content_img = image_loader("Images/Tuebingen_Neckarfront.jpg")

我们可以创建一个带有噪声(随机数)的图像,也可以使用相同的内容图像。在这种情况下,我们将使用内容图像。以下代码创建内容图像:

opt_img = Variable(content_img.data.clone(),requires_grad=True)

我们将使用优化器调整 opt_img 的值,以便图像更接近内容图像和样式图像。因此,我们通过指定 requires_grad=True 要求 PyTorch 保持梯度。

创建 VGG 模型

我们将从 torchvisions.models 中加载一个预训练模型。我们将仅使用此模型来提取特征,PyTorch 的 VGG 模型被定义为所有卷积块在 features 模块中,而全连接或线性层在 classifier 模块中。由于我们不会训练 VGG 模型中的任何权重或参数,我们还将冻结该模型。以下代码演示了同样的操作:

#Creating a pretrained VGG model
vgg = vgg19(pretrained=True).features

#Freezing the layers as we will not use it for training.
for param in vgg.parameters():
    param.requires_grad = False

在这段代码中,我们创建了一个 VGG 模型,仅使用其卷积块,并冻结了模型的所有参数,因为我们只会用它来提取特征。

内容损失

内容损失 是在通过网络传递两个图像后提取的特定层输出上计算的均方误差(MSE)。我们通过使用 register_forward_hook 功能从 VGG 中提取中间层的输出来计算这些层的输出的 MSE,如下代码所述。

target_layer = dummy_fn(content_img)
noise_layer = dummy_fn(noise_img)
criterion = nn.MSELoss()
content_loss = criterion(target_layer,noise_layer)

在接下来的部分,我们将实现这段代码中的 dummy_fn 函数。目前我们只知道,dummy_fn 函数通过传递图像返回特定层的输出。我们将通过将内容图像和噪声图像传递给 MSE loss 函数来传递生成的输出。

样式损失

样式损失 在多个层次上计算。样式损失是每个特征图生成的格拉姆矩阵的均方误差(MSE)。格拉姆矩阵表示其特征的相关性值。让我们通过以下图表和代码实现来理解格拉姆矩阵的工作方式。

以下表格显示了具有列属性 Batch_sizeChannelsValues 的特征图维度为 [2, 3, 3, 3] 的输出:

要计算格拉姆矩阵,我们将所有通道的值展平,然后通过与其转置相乘找到相关性,如下表所示:

我们所做的只是将所有通道的值展平为单个向量或张量。以下代码实现了这一点:

class GramMatrix(nn.Module):

    def forward(self,input):
        b,c,h,w = input.size()
        features = input.view(b,c,h*w)
        gram_matrix = torch.bmm(features,features.transpose(1,2))
        gram_matrix.div_(h*w)
        return gram_matrix

我们将 GramMatrix 实现为另一个 PyTorch 模块,并具有一个 forward 函数,以便我们可以像使用 PyTorch 层一样使用它。在此行中,我们从输入图像中提取不同的维度:

b,c,h,w = input.size()

这里,b代表批量大小,c代表过滤器或通道数,h代表高度,w代表宽度。在下一步中,我们将使用以下代码保持批量和通道维度不变,并展平所有高度和宽度维度的值,如前图所示:

features = input.view(b,c,h*w)

Gram 矩阵通过将其展平的值与其转置向量相乘来计算。我们可以使用 PyTorch 提供的批量矩阵乘法函数torch.bmm()来执行此操作,如下代码所示:

gram_matrix = torch.bmm(features,features.transpose(1,2))

我们通过将 Gram 矩阵的值除以元素数量来完成 Gram 矩阵的值归一化。这可以防止具有大量值的特定特征图支配得分。一旦计算了GramMatrix,就可以简单地计算风格损失,该损失在以下代码中实现:

class StyleLoss(nn.Module):

    def forward(self,inputs,targets):
        out = nn.MSELoss()(GramMatrix()(inputs),targets)
        return (out)

StyleLoss作为另一个 PyTorch 层实现。它计算输入GramMatrix值与风格图像GramMatrix值之间的均方误差。

提取损失

就像我们在《深度学习计算机视觉》第五章中使用register_forward_hook()函数提取卷积层的激活一样,我们可以提取不同卷积层的损失,用于计算风格损失和内容损失。这种情况的一个不同之处在于,我们不是从一个层中提取,而是需要从多个层中提取输出。以下类整合了所需的变更:

class LayerActivations():
    features=[]

    def __init__(self,model,layer_nums):

        self.hooks = []
        for layer_num in layer_nums:
            self.hooks.append(model[layer_num].register_forward_hook(self.hook_fn))

    def hook_fn(self,module,input,output):
        self.features.append(output)

    def remove(self):
        for hook in self.hooks:
            hook.remove()

__init__方法接受我们需要调用register_forward_hook方法的模型以及我们需要提取输出的层编号。__init__方法中的for循环遍历层编号并注册所需的前向钩子以提取输出。

传递给register_forward_hook方法的hook_fn将在注册hook_fn函数的层之后由 PyTorch 调用。在函数内部,我们捕获输出并将其存储在features数组中。

当我们不想捕获输出时,我们需要调用remove函数一次。忘记调用remove方法可能导致内存不足异常,因为所有输出都会累积。

让我们编写另一个实用函数,它可以提取用于风格和内容图像的输出。以下函数执行相同操作:

def extract_layers(layers,img,model=None):

    la = LayerActivations(model,layers)
    #Clearing the cache 
    la.features = []
    out = model(img)
    la.remove()
    return la.features

extract_layers函数内部,我们通过传入模型和层编号来创建LayerActivations类的对象。特征列表可能包含先前运行的输出,因此我们将其重新初始化为空列表。然后我们通过模型传入图像,我们不会使用输出。我们更感兴趣的是生成在features数组中的输出。我们调用remove方法从模型中移除所有注册的钩子并返回特征。以下代码展示了我们如何提取风格和内容图像所需的目标:

content_targets = extract_layers(content_layers,content_img,model=vgg)
style_targets = extract_layers(style_layers,style_img,model=vgg)

一旦我们提取了目标,我们需要从创建它们的图中分离输出。请记住,所有这些输出都是保持它们如何创建的 PyTorch 变量。但对于我们的情况,我们只对输出值感兴趣,而不是图,因为我们不会更新style图像或content图像。以下代码说明了这种技术:

content_targets = [t.detach() for t in content_targets]
style_targets = [GramMatrix()(t).detach() for t in style_targets]

一旦我们分离了目标,让我们将所有目标添加到一个列表中。以下代码说明了这种技术:

targets = style_targets + content_targets

在计算风格损失和内容损失时,我们传递了称为内容层和风格层的两个列表。不同的层选择将影响生成图像的质量。让我们选择与论文作者提到的相同层。以下代码显示了我们在这里使用的层的选择:

style_layers = [1,6,11,20,25]
content_layers = [21]
loss_layers = style_layers + content_layers

优化器期望最小化单一标量量。为了实现单一标量值,我们将所有到达不同层的损失求和。习惯上,对这些损失进行加权求和是常见做法,我们选择的权重与 GitHub 仓库中论文实现中使用的相同(github.com/leongatys/PytorchNeuralStyleTransfer)。我们的实现是作者实现的略微修改版本。以下代码描述了正在使用的权重,这些权重由所选层中的滤波器数量计算得出:

style_weights = [1e3/n**2 for n in [64,128,256,512,512]]
content_weights = [1e0]
weights = style_weights + content_weights

要可视化这一点,我们可以打印 VGG 层。花一分钟观察我们选择了哪些层,并尝试不同的层组合。我们将使用以下代码来print VGG 层:

print(vgg)

#Results 

Sequential(
  (0): Conv2d (3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (1): ReLU(inplace)
  (2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (3): ReLU(inplace)
  (4): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (5): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (6): ReLU(inplace)
  (7): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (8): ReLU(inplace)
  (9): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (10): Conv2d (128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (11): ReLU(inplace)
  (12): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (13): ReLU(inplace)
  (14): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (15): ReLU(inplace)
  (16): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (17): ReLU(inplace)
  (18): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (19): Conv2d (256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (20): ReLU(inplace)
  (21): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (22): ReLU(inplace)
  (23): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (24): ReLU(inplace)
  (25): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (26): ReLU(inplace)
  (27): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (28): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (29): ReLU(inplace)
  (30): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (31): ReLU(inplace)
  (32): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (33): ReLU(inplace)
  (34): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (35): ReLU(inplace)
  (36): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
)

我们必须定义loss函数和optimizer来生成艺术图像。我们将在以下部分初始化它们两个。

为每一层创建损失函数

我们已经定义了 PyTorch 层作为loss函数。现在,让我们为不同的风格损失和内容损失创建损失层。以下代码定义了该函数:

loss_fns = [StyleLoss()] * len(style_layers) + [nn.MSELoss()] * len(content_layers)

loss_fns是一个包含一系列风格损失对象和内容损失对象的列表,基于创建的数组长度。

创建优化器

一般来说,我们会传递网络(如 VGG)的参数进行训练。但在这个例子中,我们将 VGG 模型作为特征提取器使用,因此不能传递 VGG 的参数。在这里,我们只会提供opt_img变量的参数,我们将优化它们以使图像具有所需的内容和风格。以下代码创建了优化器来优化它的值:

optimizer = optim.LBFGS([opt_img]);

现在我们已经有了所有的训练组件。

训练

与我们之前训练的其他模型相比,training方法有所不同。在这里,我们需要在多个层次计算损失,并且每次调用优化器时,它都会改变输入图像,使其内容和样式接近目标的内容和样式。让我们看一下用于训练的代码,然后我们将详细介绍训练的重要步骤:

max_iter = 500
show_iter = 50
n_iter=[0]

while n_iter[0] <= max_iter:

    def closure():
        optimizer.zero_grad()

        out = extract_layers(loss_layers,opt_img,model=vgg)
        layer_losses = [weights[a] * loss_fnsa for a,A in enumerate(out)]
        loss = sum(layer_losses)
        loss.backward()
        n_iter[0]+=1
        #print loss
        if n_iter[0]%show_iter == (show_iter-1):
            print('Iteration: %d, loss: %f'%(n_iter[0]+1, loss.data[0]))

        return loss

    optimizer.step(closure)

我们正在运行为期500次迭代的训练循环。对于每一次迭代,我们使用我们的extract_layers函数计算 VGG 模型不同层的输出。在这种情况下,唯一变化的是opt_img的值,它将包含我们的样式图像。一旦计算出输出,我们通过迭代输出并将它们传递给相应的loss函数及其各自的目标来计算损失。我们将所有损失相加并调用backward函数。在closure函数的末尾,返回损失。closure方法与optimizer.step方法一起调用max_iter次。如果在 GPU 上运行,可能需要几分钟;如果在 CPU 上运行,请尝试减小图像大小以加快运行速度。

运行 500 个周期后,在我的设备上生成的图像如下所示。尝试不同的内容和样式组合来生成有趣的图像:

在下一节中,让我们使用深度卷积生成对抗网络DCGANs)生成人脸。

生成对抗网络

GANs 在过去几年变得非常流行。每周都有一些 GAN 领域的进展。它已成为深度学习的重要子领域之一,拥有非常活跃的研究社区。GAN 是由 Ian Goodfellow 于 2014 年引入的。GAN 通过训练两个深度神经网络,称为生成器判别器,它们相互竞争来解决无监督学习的问题。在训练过程中,它们最终都变得更擅长它们所执行的任务。

用仿冒者(生成器)和警察(判别器)的案例直观理解 GAN。最初,仿冒者向警察展示假钞。警察识别出它是假的,并解释为什么是假的。仿冒者根据收到的反馈制作新的假钞。警察发现它是假的,并告知仿冒者为什么是假的。这个过程重复了很多次,直到仿冒者能够制作出警察无法辨别的假钞。在 GAN 场景中,我们最终得到一个生成器生成的假图像与真实图像非常相似,而分类器则变得擅长辨别真假。

GAN 是一个伪造网络和专家网络的组合,每个网络都被训练来击败对方。生成器网络以随机向量作为输入并生成合成图像。鉴别器网络接收输入图像并预测图像是真实的还是伪造的。我们向鉴别器网络传递的是真实图像或伪造图像。

生成器网络被训练来生成图像,并欺骗鉴别器网络认为它们是真实的。鉴别器网络也在不断提高其不被欺骗的能力,因为我们在训练中传递反馈。尽管 GAN 的理念在理论上听起来很简单,但训练一个真正有效的 GAN 模型却非常困难。训练 GAN 也很具挑战性,因为需要训练两个深度神经网络。

DCGAN 是早期展示如何构建自学习并生成有意义图像的 GAN 模型之一。您可以在此了解更多:

arxiv.org/pdf/1511.06434.pdf

下图显示了 GAN 模型的架构:

我们将逐步介绍这种架构的每个组件及其背后的一些理论,然后我们将在下一节中用 PyTorch 实现相同的流程。通过此实现,我们将对 DCGAN 的工作原理有基本的了解。

深度卷积 GAN

在本节中,我们将基于我在前面信息框中提到的 DCGAN 论文 来实现训练 GAN 架构的不同部分。训练 DCGAN 的一些重要部分包括:

  • 一个生成器网络,将某个固定维度的潜在向量(数字列表)映射到某种形状的图像。在我们的实现中,形状是 (3, 64, 64)。

  • 一个鉴别器网络,它将生成器生成的图像或来自实际数据集的图像作为输入,并映射到一个评分,用于估计输入图像是真实还是伪造的。

  • 为生成器和鉴别器定义损失函数。

  • 定义优化器。

  • 训练一个 GAN。

让我们详细探讨这些部分的每一个。这些实现基于 PyTorch 示例代码,可在以下位置找到:

github.com/pytorch/examples/tree/master/dcgan

定义生成器网络

生成器网络以固定维度的随机向量作为输入,并对其应用一组转置卷积、批归一化和 ReLU 激活,生成所需尺寸的图像。在查看生成器实现之前,让我们来定义一下转置卷积和批归一化。

转置卷积

转置卷积也称为分数步进卷积。它们的工作方式与卷积相反。直观地说,它们尝试计算如何将输入向量映射到更高的维度。让我们看下图以更好地理解它:

此图来自 Theano(另一个流行的深度学习框架)的文档(deeplearning.net/software/theano/tutorial/conv_arithmetic.html)。如果您想深入了解步进卷积的工作原理,我强烈建议您阅读 Theano 文档中的这篇文章。对我们来说重要的是,它有助于将一个向量转换为所需维度的张量,并且我们可以通过反向传播来训练核的值。

批归一化

我们已经多次观察到,所有传递给机器学习或深度学习算法的特征都是经过归一化的;也就是说,特征的值通过减去数据的均值使其居中于零,并通过除以其标准差使数据具有单位标准差。通常我们会使用 PyTorch 的torchvision.Normalize方法来实现这一点。以下代码展示了一个示例:

transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

在我们所看到的所有示例中,数据在进入神经网络之前都是归一化的;不能保证中间层获得归一化的输入。以下图显示了神经网络中间层无法获取归一化数据的情况:

批归一化就像是一个中间函数,或者一个在训练过程中当均值和方差随时间变化时规范化中间数据的层。批归一化由 Ioffe 和 Szegedy 在 2015 年提出(arxiv.org/abs/1502.03167)。批归一化在训练和验证或测试时的行为是不同的。训练时,批内数据的均值和方差被计算。验证和测试时,使用全局值。我们需要理解的是它如何规范化中间数据。使用批归一化的一些关键优势是:

  • 提升网络中的梯度流,从而帮助我们构建更深的网络

  • 允许更高的学习率

  • 减少初始化的强依赖

  • 作为一种正则化形式,减少了 dropout 的依赖性

大多数现代架构,如 ResNet 和 Inception,在其架构中广泛使用批归一化。批归一化层通常在卷积层或线性/全连接层之后引入,如下图所示:

到目前为止,我们对生成网络的关键组成部分有了直观的理解。

生成器

让我们快速查看以下生成器网络代码,然后讨论生成器网络的关键特性:

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.main = nn.Sequential(
            # input is Z, going into a convolution
            nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # state size. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # state size. (ngf*4) x 8 x 8
            nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # state size. (ngf*2) x 16 x 16
            nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # state size. (ngf) x 32 x 32
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # state size. (nc) x 64 x 64
        )

    def forward(self, input):
        output = self.main(input)
        return output

netG = Generator()
netG.apply(weights_init)
print(netG)

在我们看到的大多数代码示例中,我们使用了许多不同的层,并在forward方法中定义了流程。在生成器网络中,我们使用顺序模型在__init__方法中定义层和数据流动。

模型接受尺寸为nz的张量作为输入,然后通过转置卷积将输入映射到需要生成的图像大小。forward函数将输入传递给顺序模块并返回输出。

生成器网络的最后一层是一个tanh层,它限制了网络可以生成的值的范围。

不使用相同的随机权重,而是使用在论文中定义的权重初始化模型。以下是权重初始化代码:

def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        m.weight.data.normal_(0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        m.weight.data.normal_(1.0, 0.02)
        m.bias.data.fill_(0)

我们通过将函数传递给生成器对象netG调用weight函数。每一层都通过函数传递;如果层是卷积层,我们以不同的方式初始化权重,如果是BatchNorm,我们稍微不同地初始化它。我们使用以下代码在网络对象上调用函数:

netG.apply(weights_init)

定义鉴别器网络

让我们快速查看以下鉴别器网络代码,然后讨论鉴别器网络的关键特性:

class Discriminator(nn.Module):
    def __init__(self):
        super(_netD, self).__init__()
        self.main = nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        output = self.main(input)
        return output.view(-1, 1).squeeze(1)

netD = Discriminator()
netD.apply(weights_init)
print(netD)

前面网络中有两个重要的事情,即使用leaky ReLU作为激活函数,以及在最后一个激活层中使用 sigmoid。首先,让我们了解一下什么是 Leaky ReLU。

Leaky ReLU 是解决 ReLU 死亡问题的一种尝试。Leaky ReLU 不像在输入为负时函数返回零,而是输出一个非常小的数值,如 0.001。论文表明,使用 Leaky ReLU 可以提高鉴别器的效率。

另一个重要的区别是在鉴别器末端不使用全连接层。通常看到最后的全连接层被全局平均池化层替换。但使用全局平均池化会减少收敛速度(构建准确分类器所需的迭代次数)。最后的卷积层被展平并传递给 sigmoid 层。

除了这两个差异之外,网络的其余部分与我们在本书中看到的其他图像分类器网络非常相似。

定义损失和优化器

我们将在下面的代码中定义一个二元交叉熵损失和两个优化器,一个用于生成器,另一个用于鉴别器:

criterion = nn.BCELoss()

# setup optimizer
optimizerD = optim.Adam(netD.parameters(), lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr, betas=(beta1, 0.999))

到目前为止,这与我们在所有先前的例子中看到的非常相似。让我们探讨一下如何训练生成器和鉴别器。

训练鉴别器

鉴别器网络的损失取决于其在真实图像上的表现和在生成器网络生成的假图像上的表现。损失可以定义为:

loss = 最大化 log(D(x)) + log(1-D(G(z)))

因此,我们需要使用真实图像和生成器网络生成的假图像来训练鉴别器。

使用真实图像训练鉴别器网络

让我们传递一些真实图像作为地面实况来训练鉴别器。

首先,我们将查看执行相同操作的代码,然后探索重要特性:

output = netD(inputv)
errD_real = criterion(output, labelv)
errD_real.backward()

在前述代码中,我们计算了用于鉴别器图像的损失和梯度。inputvlabelv 表示来自 CIFAR10 数据集的输入图像和标签,其中真实图像的标签为 1。这非常直观,因为它类似于我们为其他图像分类器网络所做的工作。

使用假图像训练鉴别器

现在传递一些随机图像来训练鉴别器。

让我们查看代码,并探索重要特性:

fake = netG(noisev)
output = netD(fake.detach())
errD_fake = criterion(output, labelv)
errD_fake.backward()
optimizerD.step()

代码中的第一行传递了一个大小为 100 的向量,生成器网络 (netG) 生成一幅图像。我们将图像传递给鉴别器以确定图像是真实还是假的。我们不希望生成器被训练,因为鉴别器正在被训练。因此,我们通过在其变量上调用 detach 方法从其图中移除假图像。一旦计算出所有梯度,我们调用 optimizer 训练鉴别器。

训练生成器网络

让我们查看代码,并探索重要特性:

netG.zero_grad()
labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
output = netD(fake)
errG = criterion(output, labelv)
errG.backward()
optimizerG.step()

它看起来与我们在训练鉴别器使用假图像时所做的类似,除了一些关键区别。我们传递了由生成器创建的同样的假图像,但这次我们没有从生成图中分离它,因为我们希望训练生成器。我们计算损失 (errG) 并计算梯度。然后我们调用生成器优化器,因为我们只想训练生成器,并在生成器生成略微逼真图像之前重复整个过程多次。

训练完整网络

我们逐个分析了 GAN 的训练过程。让我们总结如下,并查看用于训练我们创建的 GAN 网络的完整代码:

  • 使用真实图像训练鉴别器网络

  • 使用假图像训练鉴别器网络

  • 优化鉴别器

  • 基于鉴别器反馈训练生成器

  • 优化生成器网络

我们将使用以下代码来训练网络:

for epoch in range(niter):
    for i, data in enumerate(dataloader, 0):
        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################
        # train with real
        netD.zero_grad()
        real, _ = data
        batch_size = real.size(0)
        if torch.cuda.is_available():
            real = real.cuda()
        input.resize_as_(real).copy_(real)
        label.resize_(batch_size).fill_(real_label)
        inputv = Variable(input)
        labelv = Variable(label)

        output = netD(inputv)
        errD_real = criterion(output, labelv)
        errD_real.backward()
        D_x = output.data.mean()

        # train with fake
        noise.resize_(batch_size, nz, 1, 1).normal_(0, 1)
        noisev = Variable(noise)
        fake = netG(noisev)
        labelv = Variable(label.fill_(fake_label))
        output = netD(fake.detach())
        errD_fake = criterion(output, labelv)
        errD_fake.backward()
        D_G_z1 = output.data.mean()
        errD = errD_real + errD_fake
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################
        netG.zero_grad()
        labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
        output = netD(fake)
        errG = criterion(output, labelv)
        errG.backward()
        D_G_z2 = output.data.mean()
        optimizerG.step()

        print('[%d/%d][%d/%d] Loss_D: %.4f Loss_G: %.4f D(x): %.4f D(G(z)): %.4f / %.4f'
              % (epoch, niter, i, len(dataloader),
                 errD.data[0], errG.data[0], D_x, D_G_z1, D_G_z2))
        if i % 100 == 0:
            vutils.save_image(real_cpu,
                    '%s/real_samples.png' % outf,
                    normalize=True)
            fake = netG(fixed_noise)
            vutils.save_image(fake.data,
                    '%s/fake_samples_epoch_%03d.png' % (outf, epoch),
                    normalize=True)

vutils.save_image 将把一个张量保存为图像。如果提供一个小批量的图像,则将它们保存为图像网格。

在接下来的部分,我们将看看生成的图像和真实图像的外观。

检查生成的图像

所以,让我们比较生成的图像和真实图像。

生成的图像如下:

真实图像如下:

比较两组图像,我们可以看到我们的 GAN 能够学习如何生成图像。除了训练生成新图像之外,我们还有一个判别器,可用于分类问题。当有限数量的标记数据可用时,判别器学习关于图像的重要特征,这些特征可以用于分类任务。当有限的标记数据时,我们可以训练一个 GAN,它将为我们提供一个分类器,可以用于提取特征,并且可以在其上构建一个分类器模块。

在下一节中,我们将训练一个深度学习算法来生成文本。

语言建模

我们将学习如何教授循环神经网络RNN)如何创建文本序列。简单来说,我们现在要构建的 RNN 模型将能够根据一些上下文预测下一个单词。这就像你手机上的Swift应用程序,它猜测你正在输入的下一个单词一样。生成序列数据的能力在许多不同领域都有应用,例如:

  • 图像字幕

  • 语音识别

  • 语言翻译

  • 自动电子邮件回复

我们在 第六章 中学到,使用序列数据和文本的深度学习,RNN 难以训练。因此,我们将使用一种称为长短期记忆网络LSTM)的变体。LSTM 算法的开发始于 1997 年,但在过去几年变得流行起来。它因为强大的硬件和高质量数据的可用性,以及一些进展(如 dropout),使得训练更好的 LSTM 模型比以前更容易。

使用 LSTM 模型生成字符级语言模型或单词级语言模型非常流行。在字符级语言建模中,我们提供一个字符,LSTM 模型被训练来预测下一个字符,而在单词级语言建模中,我们提供一个单词,LSTM 模型预测下一个单词。在本节中,我们将使用 PyTorch LSTM 模型构建一个单词级语言模型。就像训练任何其他模块一样,我们将遵循标准步骤:

  • 准备数据

  • 生成数据批次

  • 基于 LSTM 定义模型

  • 训练模型

  • 测试模型

本节内容灵感来自于 PyTorch 中稍微简化的词语言建模示例,详情请见 github.com/pytorch/examples/tree/master/word_language_model

准备数据

对于本例,我们使用名为WikiText2的数据集。WikiText 语言建模数据集包含从维基百科上的验证过的GoodFeatured文章中提取的 1 亿多个标记。与另一个广泛使用的数据集Penn TreebankPTB)的预处理版本相比,WikiText-2大约大两倍。WikiText数据集还具有更大的词汇表,并保留了原始大小写、标点和数字。该数据集包含完整的文章,因此非常适合利用长期依赖的模型。

该数据集是在一篇名为Pointer Sentinel 混合模型的论文中介绍的(arxiv.org/abs/1609.07843)。该论文讨论了用于解决特定问题的解决方案,其中 LSTM 与 softmax 层在预测稀有单词时存在困难,尽管上下文不清楚。暂时不要担心这个问题,因为这是一个高级概念,超出了本书的范围。

下面的屏幕截图显示了 WikiText 转储文件内部的数据样式:

数据样式

通常情况下,torchtext通过提供下载和读取数据集的抽象,使使用数据集变得更加容易。让我们看看执行此操作的代码:

TEXT = d.Field(lower=True, batch_first=True)
train, valid, test = datasets.WikiText2.splits(TEXT,root='data')

先前的代码负责下载WikiText2数据并将其分成trainvalidtest数据集。语言建模的关键区别在于如何处理数据。我们在WikiText2中有的所有文本数据都存储在一个长张量中。让我们看一下下面的代码和结果,以更好地理解数据的处理方式:

print(len(train[0].text))

#output
2088628

如前所述的结果显示,我们只有一个示例字段,其中包含所有文本。让我们快速看一下文本的表示方式:

print(train[0].text[:100])

#Results of first 100 tokens

'<eos>', '=', 'valkyria', 'chronicles', 'iii', '=', '<eos>', '<eos>', 'senjō', 'no', 'valkyria', '3', ':', '<unk>', 'chronicles', '(', 'japanese', ':', '3', ',', 'lit', '.', 'valkyria', 'of', 'the', 'battlefield', '3', ')', ',', 'commonly', 'referred', 'to', 'as', 'valkyria', 'chronicles', 'iii', 'outside', 'japan', ',', 'is', 'a', 'tactical', 'role', '@-@', 'playing', 'video', 'game', 'developed', 'by', 'sega', 'and', 'media.vision', 'for', 'the', 'playstation', 'portable', '.', 'released', 'in', 'january', '2011', 'in', 'japan', ',', 'it', 'is', 'the', 'third', 'game', 'in', 'the', 'valkyria', 'series', '.', '<unk>', 'the', 'same', 'fusion', 'of', 'tactical', 'and', 'real', '@-@', 'time', 'gameplay', 'as', 'its', 'predecessors', ',', 'the', 'story', 'runs', 'parallel', 'to', 'the', 'first', 'game', 'and', 'follows', 'the'

现在,快速查看显示初始文本及其如何被标记化的图像。现在我们有一个长度为2088628的长序列,表示为WikiText2。接下来重要的事情是如何对数据进行分批处理。

生成批次

让我们看一下代码,了解序列数据分批过程中涉及的两个关键要素:

train_iter, valid_iter, test_iter = data.BPTTIterator.splits(
    (train, valid, test), batch_size=20, bptt_len=35, device=0)

此方法中有两个重要的事情。一个是batch_size,另一个是称为时间反向传播backpropagation through time,简称bptt_len)。它简要说明了数据在每个阶段如何转换。

批次

整个数据作为一个序列进行处理相当具有挑战性,而且计算效率不高。因此,我们将序列数据分成多个批次,将每个批次视为单独的序列。尽管这听起来可能并不直接,但效果要好得多,因为模型可以从数据批次中更快地学习。让我们以英文字母顺序为例进行分组。

序列:a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z.

当我们将前述字母序列转换为四批时,我们得到:

a    g    m    s    y

b    h    n    t    z

c    i     o    u

d   j     p     v

e   k    q     w

f    l     r     x

在大多数情况下,我们会剔除最后多余的单词或标记,因为它对文本建模没有太大影响。

对于例子WikiText2,当我们将数据分成 20 批次时,我们会得到每个批次 104431 个元素。

时间反向传播

我们看到通过迭代器传递的另一个重要变量是时间反向传播BPTT)。它实际上是指模型需要记住的序列长度。数字越大,效果越好——但模型的复杂性和需要的 GPU 内存也会增加。

为了更好地理解它,让我们看看如何将前面批量化的字母数据分成长度为两的序列:

a    g    m    s

b    h    n    t

前面的示例将作为输入传递给模型,并且输出将是来自序列但包含下一个值的:

b    h    n    t

c    I      o    u

对于例子WikiText2,当我们将批量数据分割时,我们得到大小为 30 的数据,每个批次为 20,其中30是序列长度。

基于 LSTM 定义一个模型

我们定义了一个模型,它有点类似于我们在第六章中看到的网络,Deep Learning with Sequence Data and Text,但它有一些关键的不同之处。网络的高级架构如下图所示:

和往常一样,让我们先看看代码,然后逐步讲解其中的关键部分:

class RNNModel(nn.Module):
    def __init__(self,ntoken,ninp,nhid,nlayers,dropout=0.5,tie_weights=False):
        #ntoken represents the number of words in vocabulary.
        #ninp Embedding dimension for each word ,which is the input for the LSTM.
        #nlayer Number of layers required to be used in the LSTM .
        #Dropout to avoid overfitting.
        #tie_weights - use the same weights for both encoder and decoder. 
        super().__init__()
        self.drop = nn.Dropout()
        self.encoder = nn.Embedding(ntoken,ninp)
        self.rnn = nn.LSTM(ninp,nhid,nlayers,dropout=dropout)
        self.decoder = nn.Linear(nhid,ntoken)
        if tie_weights:
            self.decoder.weight = self.encoder.weight

        self.init_weights()
        self.nhid = nhid
        self.nlayers = nlayers

    def init_weights(self):
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange,initrange)
        self.decoder.bias.data.fill_(0)
        self.decoder.weight.data.uniform_(-initrange,initrange)

    def forward(self,input,hidden):
        emb = self.drop(self.encoder(input))
        output,hidden = self.rnn(emb,hidden)
        output = self.drop(output)
        s = output.size()
        decoded = self.decoder(output.view(s[0]*s[1],s[2]))
        return decoded.view(s[0],s[1],decoded.size(1)),hidden

    def init_hidden(self,bsz):
        weight = next(self.parameters()).data

        return (Variable(weight.new(self.nlayers,bsz,self.nhid).zero_()),Variable(weight.new(self.nlayers,bsz,self.nhid).zero_()))

__init__方法中,我们创建所有的层,如嵌入层、dropout、RNN 和解码器。在早期的语言模型中,通常不会在最后一层使用嵌入。使用嵌入,并且将初始嵌入与最终输出层的嵌入进行绑定,可以提高语言模型的准确性。这个概念是由 Press 和 Wolf 在 2016 年的论文Using the Output Embedding to Improve Language Models(arxiv.org/abs/1608.05859)以及 Inan 和他的合著者在 2016 年的论文Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling(arxiv.org/abs/1611.01462)中引入的。一旦我们将编码器和解码器的权重绑定在一起,我们调用init_weights方法来初始化层的权重。

forward函数将所有层连接在一起。最后的线性层将 LSTM 层的所有输出激活映射到与词汇表大小相同的嵌入中。forward函数的流程是通过嵌入层传递输入,然后传递给一个 RNN(在本例中是 LSTM),然后传递给解码器,另一个线性层。

定义训练和评估函数

模型的训练与本书中之前所有示例中看到的非常相似。我们需要进行一些重要的更改,以便训练后的模型运行得更好。让我们来看看代码及其关键部分:

criterion = nn.CrossEntropyLoss()

def trainf():
    # Turn on training mode which enables dropout.
    lstm.train()
    total_loss = 0
    start_time = time.time()
    hidden = lstm.init_hidden(batch_size)
    for i,batch in enumerate(train_iter):
        data, targets = batch.text,batch.target.view(-1)
        # Starting each batch, we detach the hidden state from how it was previously produced.
        # If we didn't, the model would try backpropagating all the way to start of the dataset.
        hidden = repackage_hidden(hidden)
        lstm.zero_grad()
        output, hidden = lstm(data, hidden)
        loss = criterion(output.view(-1, ntokens), targets)
        loss.backward()

        # `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
        torch.nn.utils.clip_grad_norm(lstm.parameters(), clip)
        for p in lstm.parameters():
            p.data.add_(-lr, p.grad.data)

        total_loss += loss.data

        if i % log_interval == 0 and i > 0:
            cur_loss = total_loss[0] / log_interval
            elapsed = time.time() - start_time
            (print('| epoch {:3d} | {:5d}/{:5d} batches | lr {:02.2f} | ms/batch {:5.2f} | loss {:5.2f} | ppl {:8.2f}'.format(epoch, i, len(train_iter), lr,elapsed * 1000 / log_interval, cur_loss, math.exp(cur_loss))))
            total_loss = 0
            start_time = time.time()

由于我们在模型中使用了 dropout,因此在训练和验证/测试数据集中需要以不同方式使用它。在模型上调用 train() 将确保在训练期间启用 dropout,在模型上调用 eval() 将确保在验证/测试期间以不同方式使用 dropout:

lstm.train()

对于 LSTM 模型,除了输入外,我们还需要传递隐藏变量。init_hidden 函数将批量大小作为输入,并返回一个隐藏变量,可以与输入一起使用。我们可以迭代训练数据并将输入数据传递给模型。由于我们处理序列数据,每次迭代都从新的隐藏状态(随机初始化)开始是没有意义的。因此,我们将使用前一次迭代的隐藏状态,在通过调用 detach 方法从图中移除它后使用。如果不调用 detach 方法,那么我们将计算一个非常长的序列的梯度,直到 GPU 内存耗尽。

然后我们将输入传递给 LSTM 模型,并使用 CrossEntropyLoss 计算损失。使用前一个隐藏状态的值是通过以下 repackage_hidden 函数实现的:

def repackage_hidden(h):
    """Wraps hidden states in new Variables, to detach them from their history."""
    if type(h) == Variable:
        return Variable(h.data)
    else:
        return tuple(repackage_hidden(v) for v in h)

RNN 及其变体,例如 LSTM 和 门控循环单元(GRU),存在一个称为 梯度爆炸 的问题。避免这个问题的一个简单技巧是裁剪梯度,以下是实现这个技巧的代码:

torch.nn.utils.clip_grad_norm(lstm.parameters(), clip)

我们通过以下代码手动调整参数值。手动实现优化器比使用预建优化器更灵活:

  for p in lstm.parameters():
      p.data.add_(-lr, p.grad.data)

我们正在遍历所有参数,并将梯度值乘以学习率相加。一旦更新了所有参数,我们记录所有统计信息,如时间、损失和困惑度。

对于验证,我们编写了类似的函数,在模型上调用 eval 方法。使用以下代码定义了 evaluate 函数:

def evaluate(data_source):
    # Turn on evaluation mode which disables dropout.
    lstm.eval()
    total_loss = 0 
    hidden = lstm.init_hidden(batch_size)
    for batch in data_source: 
        data, targets = batch.text,batch.target.view(-1)
        output, hidden = lstm(data, hidden)
        output_flat = output.view(-1, ntokens)
        total_loss += len(data) * criterion(output_flat, targets).data
        hidden = repackage_hidden(hidden)
    return total_loss[0]/(len(data_source.dataset[0].text)//batch_size)

大多数的训练逻辑和评估逻辑是相似的,除了调用 eval 并且不更新模型参数。

训练模型

我们对模型进行多次 epoch 的训练,并使用以下代码进行验证:

# Loop over epochs.
best_val_loss = None
epochs = 40

for epoch in range(1, epochs+1):
    epoch_start_time = time.time()
    trainf()
    val_loss = evaluate(valid_iter)
    print('-' * 89)
    print('| end of epoch {:3d} | time: {:5.2f}s | valid loss {:5.2f} | '
        'valid ppl {:8.2f}'.format(epoch, (time.time() - epoch_start_time),
                                   val_loss, math.exp(val_loss)))
    print('-' * 89)
    if not best_val_loss or val_loss < best_val_loss:
        best_val_loss = val_loss
    else:
        # Anneal the learning rate if no improvement has been seen in the validation dataset.
        lr /= 4.0

前面的代码训练模型 40 个 epoch,我们从一个较高的学习率 20 开始,并在验证损失饱和时进一步减少。模型运行 40 个 epoch 后得到的 ppl 分数约为 108.45。以下代码块包含了上次运行模型时的日志:

-----------------------------------------------------------------------------------------
| end of epoch  39 | time: 34.16s | valid loss  4.70 | valid ppl   110.01
-----------------------------------------------------------------------------------------
| epoch  40 |   200/ 3481 batches | lr 0.31 | ms/batch 11.47 | loss  4.77 | ppl   117.40
| epoch  40 |   400/ 3481 batches | lr 0.31 | ms/batch  9.56 | loss  4.81 | ppl   122.19
| epoch  40 |   600/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.73 | ppl   113.08
| epoch  40 |   800/ 3481 batches | lr 0.31 | ms/batch  9.48 | loss  4.65 | ppl   104.77
| epoch  40 |  1000/ 3481 batches | lr 0.31 | ms/batch  9.42 | loss  4.76 | ppl   116.42
| epoch  40 |  1200/ 3481 batches | lr 0.31 | ms/batch  9.55 | loss  4.70 | ppl   109.77
| epoch  40 |  1400/ 3481 batches | lr 0.31 | ms/batch  9.41 | loss  4.74 | ppl   114.61
| epoch  40 |  1600/ 3481 batches | lr 0.31 | ms/batch  9.47 | loss  4.77 | ppl   117.65
| epoch  40 |  1800/ 3481 batches | lr 0.31 | ms/batch  9.46 | loss  4.77 | ppl   118.42
| epoch  40 |  2000/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.76 | ppl   116.31
| epoch  40 |  2200/ 3481 batches | lr 0.31 | ms/batch  9.46 | loss  4.77 | ppl   117.52
| epoch  40 |  2400/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.74 | ppl   114.06
| epoch  40 |  2600/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.62 | ppl   101.72
| epoch  40 |  2800/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.69 | ppl   109.30
| epoch  40 |  3000/ 3481 batches | lr 0.31 | ms/batch  9.47 | loss  4.71 | ppl   111.51
| epoch  40 |  3200/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.70 | ppl   109.65
| epoch  40 |  3400/ 3481 batches | lr 0.31 | ms/batch  9.51 | loss  4.63 | ppl   102.43
val loss 4.686332647950745
-----------------------------------------------------------------------------------------
| end of epoch  40 | time: 34.50s | valid loss  4.69 | valid ppl   108.45
-----------------------------------------------------------------------------------------

在过去几个月中,研究人员开始探索先前的方法,创建一个语言模型来生成预训练的嵌入。如果您对这种方法更感兴趣,我强烈推荐您阅读 Jeremy Howard 和 Sebastian Ruder 撰写的论文Fine-tuned Language Models for Text Classification(https://arxiv.org/abs/1801.06146),他们在其中详细介绍了如何使用语言建模技术来准备特定领域的词嵌入,后者可以用于不同的 NLP 任务,如文本分类问题。

概要

在本章中,我们讨论了如何训练能够使用生成网络生成艺术风格转换、使用 GAN 和 DCGAN 生成新图像以及使用 LSTM 网络生成文本的深度学习算法。

在下一章中,我们将介绍一些现代架构,例如 ResNet 和 Inception,用于构建更好的计算机视觉模型,以及像序列到序列这样的模型,这些模型可以用于构建语言翻译和图像标题生成等任务。

第八章:现代网络架构

在最后一章中,我们探讨了如何使用深度学习算法创建艺术图像,基于现有数据集创建新图像以及生成文本。在本章中,我们将介绍驱动现代计算机视觉应用和自然语言系统的不同网络架构。本章我们将看到的一些架构包括:

  • ResNet

  • Inception

  • DenseNet

  • 编码器-解码器架构

现代网络架构

当深度学习模型学习失败时,我们通常会向模型中添加更多的层。随着层的增加,模型的准确性会提高,然后开始饱和。继续添加更多层之后,准确性会开始下降。超过一定数量的层会引入一些挑战,比如梯度消失或爆炸问题,这部分可以通过仔细初始化权重和引入中间归一化层来部分解决。现代架构,如残差网络ResNet)和 Inception,尝试通过引入不同的技术,如残差连接,来解决这些问题。

ResNet

ResNet 通过显式让网络中的层适应一个残差映射来解决这些问题,通过添加快捷连接。下图展示了 ResNet 的工作原理:

在我们看到的所有网络中,我们试图通过堆叠不同的层找到一个将输入(x)映射到其输出(H(x))的函数。但是 ResNet 的作者提出了一个修正方法;不再试图学习从 xH(x) 的基础映射,而是学习两者之间的差异,或者残差。然后,为了计算 H(x),我们可以将残差简单地加到输入上。假设残差为 F(x) = H(x) - x;与其直接学习 H(x),我们尝试学习 F(x) + x

每个 ResNet 块由一系列层组成,并通过快捷连接将块的输入添加到块的输出。加法操作是逐元素进行的,输入和输出需要具有相同的大小。如果它们大小不同,我们可以使用填充。以下代码展示了一个简单的 ResNet 块是如何工作的:

class ResNetBasicBlock(nn.Module):

    def __init__(self,in_channels,out_channels,stride):

        super().__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels,out_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.stride = stride

    def forward(self,x):

        residual = x
        out = self.conv1(x)
        out = F.relu(self.bn1(out),inplace=True)
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        return F.relu(out)       

ResNetBasicBlock 包含一个 init 方法,用于初始化所有不同的层,如卷积层、批标准化层和 ReLU 层。forward 方法与我们之前看到的几乎相同,唯一不同的是在返回之前将输入重新添加到层的输出中。

PyTorch 的 torchvision 包提供了一个带有不同层的即用型 ResNet 模型。一些可用的不同模型包括:

  • ResNet-18

  • ResNet-34

  • ResNet-50

  • ResNet-101

  • ResNet-152

我们也可以使用这些模型中的任何一个进行迁移学习。torchvision 实例使我们能够简单地创建这些模型并使用它们。我们在书中已经做过几次,以下代码是对此的一次复习:

from torchvision.models import resnet18

resnet = resnet18(pretrained=False)

下图展示了 34 层 ResNet 模型的结构:

34 层的 ResNet 模型

我们可以看到这个网络由多个 ResNet 块组成。有些团队进行了实验,尝试了深达 1000 层的模型。对于大多数实际应用场景,我个人推荐从一个较小的网络开始。这些现代网络的另一个关键优势是,它们与需要大量参数训练的模型(如 VGG)相比,需要很少的参数,因为它们避免使用全连接层。在计算机视觉领域解决问题时,另一种流行的架构是Inception。在继续研究 Inception 架构之前,让我们在Dogs vs. Cats数据集上训练一个 ResNet 模型。我们将使用我们在第五章深度学习计算机视觉中使用的数据,并基于从 ResNet 计算的特征快速训练一个模型。像往常一样,我们将按照以下步骤训练模型:

  • 创建 PyTorch 数据集

  • 创建用于训练和验证的加载器

  • 创建 ResNet 模型

  • 提取卷积特征

  • 创建一个自定义的 PyTorch 数据集类,用于预处理的特征和加载器

  • 创建一个简单的线性模型

  • 训练和验证模型

完成后,我们将对 Inception 和 DenseNet 重复此步骤。最后,我们还将探讨集成技术,在其中结合这些强大的模型来构建一个新模型。

创建 PyTorch 数据集

我们创建一个包含所有基本变换的变换对象,并使用ImageFolder从我们在章节*5 中创建的数据目录中加载图像,深度学习计算机视觉。在以下代码中,我们创建数据集:

data_transform = transforms.Compose([
        transforms.Resize((299,299)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

# For Dogs & Cats dataset
train_dset = ImageFolder('../../chapter5/dogsandcats/train/',transform=data_transform)
val_dset = ImageFolder('../../chapter5/dogsandcats/valid/',transform=data_transform)
classes=2

到目前为止,前面的大部分代码应该是不言自明的。

创建用于训练和验证的加载器

我们使用 PyTorch 加载器以批次形式提供数据集中的数据,同时使用所有优势,如数据洗牌和多线程,以加快处理速度。以下代码演示了这一点:

train_loader = DataLoader(train_dset,batch_size=32,shuffle=False,num_workers=3)
val_loader = DataLoader(val_dset,batch_size=32,shuffle=False,num_workers=3)

在计算预处理特征时,我们需要保持数据的确切顺序。当我们允许数据被洗牌时,我们将无法保持标签的顺序。因此,请确保shuffle参数为False,否则需要在代码中处理所需的逻辑。

创建一个 ResNet 模型

使用resnet34预训练模型的层,通过丢弃最后一个线性层创建 PyTorch 序列模型。我们将使用这个训练好的模型从我们的图像中提取特征。以下代码演示了这一点:

#Create ResNet model
my_resnet = resnet34(pretrained=True)

if is_cuda:
    my_resnet = my_resnet.cuda()

my_resnet = nn.Sequential(*list(my_resnet.children())[:-1])

for p in my_resnet.parameters():
    p.requires_grad = False

在前面的代码中,我们创建了一个在torchvision模型中可用的resnet34模型。在下面的代码中,我们挑选所有的 ResNet 层,但排除最后一层,并使用nn.Sequential创建一个新模型:

for p in my_resnet.parameters():
    p.requires_grad = False

nn.Sequential实例允许我们快速创建一个使用一堆 PyTorch 层的模型。一旦模型创建完毕,不要忘记将requires_grad参数设置为False,这将允许 PyTorch 不维护任何用于保存梯度的空间。

提取卷积特征

我们通过模型将训练和验证数据加载器传递,并将模型的结果存储在列表中以供进一步计算。通过计算预卷积特征,我们可以在训练模型时节省大量时间,因为我们不会在每次迭代中计算这些特征。在下面的代码中,我们计算预卷积特征:

#For training data

# Stores the labels of the train data
trn_labels = [] 

# Stores the pre convoluted features of the train data
trn_features = [] 

#Iterate through the train data and store the calculated features and the labels
for d,la in train_loader:
    o = m(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    trn_labels.extend(la)
    trn_features.extend(o.cpu().data)

#For validation data

#Iterate through the validation data and store the calculated features and the labels
val_labels = []
val_features = []
for d,la in val_loader:
    o = m(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_features.extend(o.cpu().data)

一旦我们计算了预卷积特征,我们需要创建一个能够从我们的预卷积特征中挑选数据的自定义数据集。让我们为预卷积特征创建一个自定义数据集和加载器。

为预卷积特征创建自定义的 PyTorch 数据集类和加载器

我们已经看过如何创建 PyTorch 数据集。它应该是torch.utils.data数据集类的子类,并且应该实现__getitem__(self, index)__len__(self)方法,这些方法返回数据集中的数据长度。在下面的代码中,我们为预卷积特征实现一个自定义数据集:

class FeaturesDataset(Dataset):

    def __init__(self,featlst,labellst):
        self.featlst = featlst
        self.labellst = labellst

    def __getitem__(self,index):
        return (self.featlst[index],self.labellst[index])

    def __len__(self):
        return len(self.labellst)

创建自定义数据集类之后,创建预卷积特征的数据加载器就很简单了,如下面的代码所示:

#Creating dataset for train and validation
trn_feat_dset = FeaturesDataset(trn_features,trn_labels)
val_feat_dset = FeaturesDataset(val_features,val_labels)

#Creating data loader for train and validation
trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

现在我们需要创建一个简单的线性模型,它可以将预卷积特征映射到相应的类别。

创建一个简单的线性模型

我们将创建一个简单的线性模型,将预卷积特征映射到相应的类别。在这种情况下,类别的数量为两个:

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

    def forward(self,inp):
        out = self.fc(inp)
        return out

fc_in_size = 8192

fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
    fc = fc.cuda()

现在,我们可以训练我们的新模型并验证数据集。

训练和验证模型

我们将使用相同的fit函数,该函数我们已经在《第五章》计算机视觉的深度学习中使用过。我没有在这里包含它,以节省空间。以下代码片段包含了训练模型和显示结果的功能:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,fc,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果如下:

#Results
training loss is 0.082 and training accuracy is 22473/23000     97.71
validation loss is   0.1 and validation accuracy is 1934/2000      96.7
training loss is  0.08 and training accuracy is 22456/23000     97.63
validation loss is  0.12 and validation accuracy is 1917/2000     95.85
training loss is 0.077 and training accuracy is 22507/23000     97.86
validation loss is   0.1 and validation accuracy is 1930/2000      96.5
training loss is 0.075 and training accuracy is 22518/23000      97.9
validation loss is 0.096 and validation accuracy is 1938/2000      96.9
training loss is 0.073 and training accuracy is 22539/23000      98.0
validation loss is   0.1 and validation accuracy is 1936/2000      96.8
training loss is 0.073 and training accuracy is 22542/23000     98.01
validation loss is 0.089 and validation accuracy is 1942/2000      97.1
training loss is 0.071 and training accuracy is 22545/23000     98.02
validation loss is  0.09 and validation accuracy is 1941/2000     97.05
training loss is 0.068 and training accuracy is 22591/23000     98.22
validation loss is 0.092 and validation accuracy is 1934/2000      96.7
training loss is 0.067 and training accuracy is 22573/23000     98.14
validation loss is 0.085 and validation accuracy is 1942/2000      97.1

正如我们从结果中看到的那样,模型达到了 98%的训练精度和 97%的验证精度。让我们了解另一种现代架构及其如何用于计算预卷积特征并用它们来训练模型。

Inception

在我们看到的大多数计算机视觉模型的深度学习算法中,我们会选择使用卷积层,其滤波器大小为 1 x 1、3 x 3、5 x 5、7 x 7 或映射池化层。Inception 模块结合了不同滤波器大小的卷积,并将所有输出串联在一起。下图使 Inception 模型更清晰:

图片来源:https://arxiv.org/pdf/1409.4842.pdf

在这个 Inception 块图像中,应用了不同尺寸的卷积到输入上,并将所有这些层的输出串联起来。这是一个 Inception 模块的最简单版本。还有另一种 Inception 块的变体,我们在通过 3 x 3 和 5 x 5 卷积之前会先通过 1 x 1 卷积来减少维度。1 x 1 卷积用于解决计算瓶颈问题。1 x 1 卷积一次查看一个值,并跨通道进行。例如,在输入大小为 100 x 64 x 64 的情况下,使用 10 x 1 x 1 的滤波器将导致 10 x 64 x 64 的输出。以下图展示了具有降维的 Inception 块:

图片来源:https://arxiv.org/pdf/1409.4842.pdf

现在,让我们看一个 PyTorch 示例,展示前述 Inception 块的外观:

class BasicConv2d(nn.Module):

    def __init__(self, in_channels, out_channels, **kwargs):
        super(BasicConv2d, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
        self.bn = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return F.relu(x, inplace=True)

class InceptionBasicBlock(nn.Module):

    def __init__(self, in_channels, pool_features):
        super().__init__()
        self.branch1x1 = BasicConv2d(in_channels, 64, kernel_size=1)

        self.branch5x5_1 = BasicConv2d(in_channels, 48, kernel_size=1)
        self.branch5x5_2 = BasicConv2d(48, 64, kernel_size=5, padding=2)

        self.branch3x3dbl_1 = BasicConv2d(in_channels, 64, kernel_size=1)
        self.branch3x3dbl_2 = BasicConv2d(64, 96, kernel_size=3, padding=1)

        self.branch_pool = BasicConv2d(in_channels, pool_features, kernel_size=1)

    def forward(self, x):
        branch1x1 = self.branch1x1(x)

        branch5x5 = self.branch5x5_1(x)
        branch5x5 = self.branch5x5_2(branch5x5)

        branch3x3dbl = self.branch3x3dbl_1(x)
        branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)

        branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
        branch_pool = self.branch_pool(branch_pool)

        outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
        return torch.cat(outputs, 1)

前述代码包含两个类,BasicConv2dInceptionBasicBlockBasicConv2d 作为一个自定义层,将二维卷积层、批归一化和 ReLU 层应用于传递的输入上。当我们有重复的代码结构时,创建一个新层是良好的做法,使代码看起来更优雅。

InceptionBasicBlock 实现了我们在第二个 Inception 图中看到的内容。让我们逐个查看每个较小的片段,并尝试理解它们的实现方式:

branch1x1 = self.branch1x1(x)

前述代码通过应用一个 1 x 1 卷积块来转换输入:

branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)

在前述代码中,我们通过应用一个 1 x 1 卷积块后跟一个 5 x 5 卷积块来转换输入:

branch3x3dbl = self.branch3x3dbl_1(x)
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)

在前述代码中,我们通过应用一个 1 x 1 卷积块后跟一个 3 x 3 卷积块来转换输入:

branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)

在前述代码中,我们应用了平均池化以及一个 1 x 1 卷积块,在最后,我们将所有结果串联在一起。一个 Inception 网络将由多个 Inception 块组成。下图展示了 Inception 架构的外观:

Inception 架构

torchvision 包含一个 Inception 网络,可以像我们使用 ResNet 网络一样使用。对初始 Inception 块进行了许多改进,PyTorch 提供的当前实现是 Inception v3。让我们看看如何使用 torchvision 中的 Inception v3 模型来计算预计算特征。我们将不会详细介绍数据加载过程,因为我们将使用之前 ResNet 部分的相同数据加载器。我们将关注以下重要主题:

  • 创建 Inception 模型

  • 使用 register_forward_hook 提取卷积特征

  • 为卷积特征创建新数据集

  • 创建全连接模型

  • 训练和验证模型

创建 Inception 模型

Inception v3 模型有两个分支,每个分支生成一个输出,在原始模型训练中,我们会像样式迁移那样合并损失。目前我们只关心使用一个分支来计算使用 Inception 的预卷积特征。深入了解这一点超出了本书的范围。如果你有兴趣了解更多工作原理,阅读论文和 Inception 模型的源代码(github.com/pytorch/vision/blob/master/torchvision/models/inception.py)将有所帮助。我们可以通过将 aux_logits 参数设置为 False 来禁用其中一个分支。以下代码解释了如何创建模型并将 aux_logits 参数设置为 False

my_inception = inception_v3(pretrained=True)
my_inception.aux_logits = False
if is_cuda:
    my_inception = my_inception.cuda()

从 Inception 模型中提取卷积特征并不像 ResNet 那样直接,因此我们将使用 register_forward_hook 来提取激活值。

使用 register_forward_hook 提取卷积特征

我们将使用与计算样式迁移激活值相同的技术。以下是 LayerActivations 类的一些小修改,因为我们只关注提取特定层的输出:

class LayerActivations():
    features=[]

    def __init__(self,model):
        self.features = []
        self.hook = model.register_forward_hook(self.hook_fn)

    def hook_fn(self,module,input,output):

        self.features.extend(output.view(output.size(0),-1).cpu().data)

    def remove(self):

        self.hook.remove()

除了 hook 函数外,其余代码与我们用于样式迁移的代码类似。由于我们正在捕获所有图像的输出并存储它们,我们将无法在 图形处理单元 (GPU) 内存中保留数据。因此,我们将从 GPU 中提取张量到 CPU,并仅存储张量而不是 Variable。我们将其转换回张量,因为数据加载器只能处理张量。在以下代码中,我们使用 LayerActivations 对象来提取 Inception 模型在最后一层的输出,跳过平均池化层、dropout 和线性层。我们跳过平均池化层是为了避免在数据中丢失有用信息:

# Create LayerActivations object to store the output of inception model at a particular layer.
trn_features = LayerActivations(my_inception.Mixed_7c)
trn_labels = []

# Passing all the data through the model , as a side effect the outputs will get stored 
# in the features list of the LayerActivations object. 
for da,la in train_loader:
    _ = my_inception(Variable(da.cuda()))
    trn_labels.extend(la)
trn_features.remove()

# Repeat the same process for validation dataset .

val_features = LayerActivations(my_inception.Mixed_7c)
val_labels = []
for da,la in val_loader:
    _ = my_inception(Variable(da.cuda()))
    val_labels.extend(la)
val_features.remove()

让我们创建新的数据集和加载器,以便获取新的卷积特征。

为卷积特征创建新数据集

我们可以使用相同的 FeaturesDataset 类来创建新的数据集和数据加载器。在以下代码中,我们创建数据集和加载器:

#Dataset for pre computed features for train and validation data sets

trn_feat_dset = FeaturesDataset(trn_features.features,trn_labels)
val_feat_dset = FeaturesDataset(val_features.features,val_labels)

#Data loaders for pre computed features for train and validation data sets

trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

让我们创建一个新的模型来在预卷积特征上进行训练。

创建一个完全连接的模型

简单的模型可能会导致过拟合,因此让我们在模型中包含 dropout。Dropout 可以帮助避免过拟合。在以下代码中,我们正在创建我们的模型:

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size,training=True):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

    def forward(self,inp):
        out = F.dropout(inp, training=self.training)
        out = self.fc(out)
        return out

# The size of the output from the selected convolution feature 
fc_in_size = 131072

fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
    fc = fc.cuda()

一旦模型创建完成,我们可以对模型进行训练。

训练和验证模型

我们使用与之前的 ResNet 和其他示例中相同的拟合和训练逻辑。我们只会查看训练代码和其结果:

for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,fc,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

#Results

training loss is 0.78 and training accuracy is 22825/23000 99.24
validation loss is 5.3 and validation accuracy is 1947/2000 97.35
training loss is 0.84 and training accuracy is 22829/23000 99.26
validation loss is 5.1 and validation accuracy is 1952/2000 97.6
training loss is 0.69 and training accuracy is 22843/23000 99.32
validation loss is 5.1 and validation accuracy is 1951/2000 97.55
training loss is 0.58 and training accuracy is 22852/23000 99.36
validation loss is 4.9 and validation accuracy is 1953/2000 97.65
training loss is 0.67 and training accuracy is 22862/23000 99.4
validation loss is 4.9 and validation accuracy is 1955/2000 97.75
training loss is 0.54 and training accuracy is 22870/23000 99.43
validation loss is 4.8 and validation accuracy is 1953/2000 97.65
training loss is 0.56 and training accuracy is 22856/23000 99.37
validation loss is 4.8 and validation accuracy is 1955/2000 97.75
training loss is 0.7 and training accuracy is 22841/23000 99.31
validation loss is 4.8 and validation accuracy is 1956/2000 97.8
training loss is 0.47 and training accuracy is 22880/23000 99.48
validation loss is 4.7 and validation accuracy is 1956/2000 97.8

查看结果,Inception 模型在训练集上达到了 99% 的准确率,在验证集上达到了 97.8% 的准确率。由于我们预先计算并将所有特征保存在内存中,训练模型只需不到几分钟的时间。如果在运行程序时出现内存不足的情况,则可能需要避免将特征保存在内存中。

我们将看看另一个有趣的架构 DenseNet,在过去一年中变得非常流行。

密集连接的卷积网络 – DenseNet

一些成功和流行的架构,如 ResNet 和 Inception,展示了更深更宽网络的重要性。ResNet 使用快捷连接来构建更深的网络。DenseNet 将其提升到一个新水平,通过引入从每一层到所有后续层的连接,即一个层可以接收来自前几层的所有特征图。符号上看,它可能是这样的:

下图描述了一个五层密集块的结构:

图片来源:https://arxiv.org/abs/1608.06993

torchvision 中有 DenseNet 的实现(github.com/pytorch/vision/blob/master/torchvision/models/densenet.py)。让我们看一下两个主要功能,_DenseBlock_DenseLayer

DenseBlock

让我们查看 DenseBlock 的代码,然后逐步分析它:

class _DenseBlock(nn.Sequential):
    def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate):
        super(_DenseBlock, self).__init__()
        for i in range(num_layers):
            layer = _DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
            self.add_module('denselayer%d' % (i + 1), layer)

DenseBlock 是一个顺序模块,我们按顺序添加层。根据块中的层数(num_layers),我们添加相应数量的 _DenseLayer 对象以及一个名称。所有的魔法都发生在 DenseLayer 内部。让我们看看 DenseLayer 内部发生了什么。

DenseLayer

学习一个特定网络如何工作的一个好方法是查看源代码。PyTorch 实现非常清晰,大多数情况下易于阅读。让我们来看一下 DenseLayer 的实现:

class _DenseLayer(nn.Sequential):
    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):
        super(_DenseLayer, self).__init__()
        self.add_module('norm.1', nn.BatchNorm2d(num_input_features)),
        self.add_module('relu.1', nn.ReLU(inplace=True)),
        self.add_module('conv.1', nn.Conv2d(num_input_features, bn_size *
                        growth_rate, kernel_size=1, stride=1, bias=False)),
        self.add_module('norm.2', nn.BatchNorm2d(bn_size * growth_rate)),
        self.add_module('relu.2', nn.ReLU(inplace=True)),
        self.add_module('conv.2', nn.Conv2d(bn_size * growth_rate, growth_rate,
                        kernel_size=3, stride=1, padding=1, bias=False)),
        self.drop_rate = drop_rate

    def forward(self, x):
        new_features = super(_DenseLayer, self).forward(x)
        if self.drop_rate > 0:
            new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
        return torch.cat([x, new_features], 1)

如果你对 Python 中的继承不熟悉,那么前面的代码可能看起来不直观。_DenseLayernn.Sequential 的子类;让我们看看每个方法内部发生了什么。

__init__方法中,我们添加所有需要传递给输入数据的层。这与我们看到的所有其他网络架构非常相似。

魔法发生在forward方法中。我们将输入传递给super类的forward方法,即nn.Sequential的方法。让我们看看顺序类的forward方法中发生了什么(github.com/pytorch/pytorch/blob/409b1c8319ecde4bd62fcf98d0a6658ae7a4ab23/torch/nn/modules/container.py):

def forward(self, input):
    for module in self._modules.values():
        input = module(input)
    return input

输入通过之前添加到顺序块中的所有层,并将输出连接到输入。该过程在块中所需数量的层中重复进行。

了解了DenseNet块的工作原理后,让我们探索如何使用 DenseNet 计算预卷积特征并在其上构建分类器模型。在高层次上,DenseNet 的实现类似于 VGG 的实现。DenseNet 实现还有一个特征模块,其中包含所有的密集块,以及一个分类器模块,其中包含全连接模型。我们将按照以下步骤构建模型。我们将跳过与 Inception 和 ResNet 相似的大部分内容,例如创建数据加载器和数据集。同时,我们将详细讨论以下步骤:

  • 创建一个 DenseNet 模型

  • 提取 DenseNet 特征

  • 创建一个数据集和加载器

  • 创建一个全连接模型并训练

到目前为止,大部分代码都将是不言自明的。

创建一个 DenseNet 模型

Torchvision 有一个预训练的 DenseNet 模型,具有不同的层次选项(121、169、201、161)。我们选择了具有121层的模型。正如讨论的那样,DenseNet 有两个模块:特征(包含密集块)和分类器(全连接块)。由于我们正在使用 DenseNet 作为图像特征提取器,我们只会使用特征模块:

my_densenet = densenet121(pretrained=True).features
if is_cuda:
    my_densenet = my_densenet.cuda()

for p in my_densenet.parameters():
    p.requires_grad = False

让我们从图像中提取 DenseNet 特征。

提取 DenseNet 特征

这与我们为 Inception 所做的相似,只是我们没有使用register_forward_hook来提取特征。以下代码展示了如何提取 DenseNet 特征:

#For training data
trn_labels = []
trn_features = []

#code to store densenet features for train dataset.
for d,la in train_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    trn_labels.extend(la)
    trn_features.extend(o.cpu().data)

#For validation data
val_labels = []
val_features = []

#Code to store densenet features for validation dataset. 
for d,la in val_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_features.extend(o.cpu().data)

前述代码与我们之前看到的 Inception 和 ResNet 类似。

创建一个数据集和加载器

我们将使用我们为 ResNet 创建的FeaturesDataset类,并在以下代码中使用它来为trainvalidation数据集创建数据加载器:

# Create dataset for train and validation convolution features
trn_feat_dset = FeaturesDataset(trn_features,trn_labels)
val_feat_dset = FeaturesDataset(val_features,val_labels)

# Create data loaders for batching the train and validation datasets
trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True,drop_last=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

是时候创建模型并训练了。

创建一个全连接模型并训练

我们将使用一个简单的线性模型,类似于我们在 ResNet 和 Inception 中使用的模型。以下代码展示了我们将用来训练模型的网络架构:

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

    def forward(self,inp):
        out = self.fc(inp)
        return out

fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
    fc = fc.cuda()

我们将使用相同的fit方法来训练前面的模型。以下代码片段显示了训练代码及其结果:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,fc,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果是:

# Results

training loss is 0.057 and training accuracy is 22506/23000 97.85
validation loss is 0.034 and validation accuracy is 1978/2000 98.9
training loss is 0.0059 and training accuracy is 22953/23000 99.8
validation loss is 0.028 and validation accuracy is 1981/2000 99.05
training loss is 0.0016 and training accuracy is 22974/23000 99.89
validation loss is 0.022 and validation accuracy is 1983/2000 99.15
training loss is 0.00064 and training accuracy is 22976/23000 99.9
validation loss is 0.023 and validation accuracy is 1983/2000 99.15
training loss is 0.00043 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1983/2000 99.15
training loss is 0.00033 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.00025 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.0002 and training accuracy is 22976/23000 99.9
validation loss is 0.025 and validation accuracy is 1985/2000 99.25
training loss is 0.00016 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1986/2000 99.3

前述算法能够达到 99%的最大训练精度和 99%的验证精度。由于您创建的validation数据集可能包含不同的图像,因此您的结果可能会有所不同。

DenseNet 的一些优点包括:

  • 它大大减少了所需的参数数量

  • 它缓解了梯度消失问题

  • 它鼓励特征重用

在接下来的部分中,我们将探讨如何构建一个结合使用 ResNet、Inception 和 DenseNet 不同模型计算的卷积特征优势的模型。

模型集成

有时,我们需要尝试将多个模型组合在一起构建一个非常强大的模型。有许多技术可以用于构建集成模型。在本节中,我们将学习如何使用由三种不同模型(ResNet、Inception 和 DenseNet)生成的特征来结合输出,从而构建一个强大的模型。我们将使用本章中其他示例中使用的同一数据集。

集成模型的架构将如下所示:

该图显示了我们在集成模型中要做的事情,可以总结为以下步骤:

  1. 创建三个模型

  2. 使用创建的模型提取图像特征

  3. 创建一个自定义数据集,该数据集返回所有三个模型的特征以及标签

  4. 创建类似于前面图中架构的模型

  5. 训练和验证模型

让我们详细探讨每个步骤。

创建模型

让我们按以下代码创建所有三个所需的模型:

#Create ResNet model
my_resnet = resnet34(pretrained=True)

if is_cuda:
    my_resnet = my_resnet.cuda()

my_resnet = nn.Sequential(*list(my_resnet.children())[:-1])

for p in my_resnet.parameters():
    p.requires_grad = False

#Create inception model

my_inception = inception_v3(pretrained=True)
my_inception.aux_logits = False
if is_cuda:
    my_inception = my_inception.cuda()
for p in my_inception.parameters():
    p.requires_grad = False

#Create densenet model

my_densenet = densenet121(pretrained=True).features
if is_cuda:
    my_densenet = my_densenet.cuda()

for p in my_densenet.parameters():
    p.requires_grad = False

现在我们有了所有模型,让我们从图像中提取特征。

提取图像特征

这里,我们将本章中各算法的各自逻辑组合起来:

### For ResNet

trn_labels = []
trn_resnet_features = []
for d,la in train_loader:
    o = my_resnet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    trn_labels.extend(la)
    trn_resnet_features.extend(o.cpu().data)
val_labels = []
val_resnet_features = []
for d,la in val_loader:
    o = my_resnet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_resnet_features.extend(o.cpu().data)

### For Inception

trn_inception_features = LayerActivations(my_inception.Mixed_7c)
for da,la in train_loader:
    _ = my_inception(Variable(da.cuda()))

trn_inception_features.remove()

val_inception_features = LayerActivations(my_inception.Mixed_7c)
for da,la in val_loader:
    _ = my_inception(Variable(da.cuda()))

val_inception_features.remove()

### For DenseNet

trn_densenet_features = []
for d,la in train_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)

    trn_densenet_features.extend(o.cpu().data)

val_densenet_features = []
for d,la in val_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_densenet_features.extend(o.cpu().data)

到目前为止,我们已经使用所有模型创建了图像特征。如果你遇到内存问题,那么可以删除其中一个模型,或者停止在内存中存储特征,这可能会导致训练速度变慢。如果你在运行这个过程时使用的是 CUDA 实例,那么可以选择更强大的实例。

创建一个自定义数据集以及数据加载器

由于FeaturesDataset类仅开发用于从一个模型的输出中进行选择,所以我们将无法直接使用它。因此,以下实现对FeaturesDataset类进行了微小的更改,以适应所有三个不同生成特征的情况:

class FeaturesDataset(Dataset):

    def __init__(self,featlst1,featlst2,featlst3,labellst):
        self.featlst1 = featlst1
        self.featlst2 = featlst2
        self.featlst3 = featlst3
        self.labellst = labellst

    def __getitem__(self,index):
        return (self.featlst1[index],self.featlst2[index],self.featlst3[index],self.labellst[index])

    def __len__(self):
        return len(self.labellst)

trn_feat_dset = FeaturesDataset(trn_resnet_features,trn_inception_features.features,trn_densenet_features,trn_labels)
val_feat_dset = FeaturesDataset(val_resnet_features,val_inception_features.features,val_densenet_features,val_labels)

我们已经对__init__方法进行了更改,以存储来自不同模型生成的所有特征,并对__getitem__方法进行了更改,以检索图像的特征和标签。使用FeatureDataset类,我们为训练和验证数据创建了数据集实例。一旦数据集创建完成,我们可以使用相同的数据加载器批处理数据,如以下代码所示:

trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

创建一个集成模型

我们需要创建一个与之前展示的架构图类似的模型。以下代码实现了这一点:

class EnsembleModel(nn.Module):

    def __init__(self,out_size,training=True):
        super().__init__()
        self.fc1 = nn.Linear(8192,512)
        self.fc2 = nn.Linear(131072,512)
        self.fc3 = nn.Linear(82944,512)
        self.fc4 = nn.Linear(512,out_size)

    def forward(self,inp1,inp2,inp3):
        out1 = self.fc1(F.dropout(inp1,training=self.training))
        out2 = self.fc2(F.dropout(inp2,training=self.training))
        out3 = self.fc3(F.dropout(inp3,training=self.training))
        out = out1 + out2 + out3
        out = self.fc4(F.dropout(out,training=self.training))
        return out

em = EnsembleModel(2)
if is_cuda:
    em = em.cuda()

在前面的代码中,我们创建了三个线性层,这些线性层接收从不同模型生成的特征。我们将这三个线性层的所有输出相加,并将它们传递给另一个线性层,将它们映射到所需的类别。为了防止模型过拟合,我们使用了 dropout。

训练和验证模型

我们需要对fit方法进行一些小的更改,以适应从数据加载器生成的三个输入值。以下代码实现了新的fit函数:

def fit(epoch,model,data_loader,phase='training',volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , (data1,data2,data3,target) in enumerate(data_loader):
        if is_cuda:
            data1,data2,data3,target = data1.cuda(),data2.cuda(),data3.cuda(),target.cuda()
        data1,data2,data3,target = Variable(data1,volatile),Variable(data2,volatile),Variable(data3,volatile),Variable(target)
        if phase == 'training':
            optimizer.zero_grad()
        output = model(data1,data2,data3)
        loss = F.cross_entropy(output,target)

        running_loss += F.cross_entropy(output,target,size_average=False).data[0]
        preds = output.data.max(dim=1,keepdim=True)[1]
        running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()

    loss = running_loss/len(data_loader.dataset)
    accuracy = 100\. * running_correct/len(data_loader.dataset)

    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

如您从前面的代码中看到的,大部分代码保持不变,只是加载器返回了三个输入和一个标签。因此,我们对功能进行了更改,这是不言自明的。

以下代码显示了训练代码:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,em,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,em,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果如下:

#Results 

training loss is 7.2e+01 and training accuracy is 21359/23000 92.87
validation loss is 6.5e+01 and validation accuracy is 1968/2000 98.4
training loss is 9.4e+01 and training accuracy is 22539/23000 98.0
validation loss is 1.1e+02 and validation accuracy is 1980/2000 99.0
training loss is 1e+02 and training accuracy is 22714/23000 98.76
validation loss is 1.4e+02 and validation accuracy is 1976/2000 98.8
training loss is 7.3e+01 and training accuracy is 22825/23000 99.24
validation loss is 1.6e+02 and validation accuracy is 1979/2000 98.95
training loss is 7.2e+01 and training accuracy is 22845/23000 99.33
validation loss is 2e+02 and validation accuracy is 1984/2000 99.2
training loss is 1.1e+02 and training accuracy is 22862/23000 99.4
validation loss is 4.1e+02 and validation accuracy is 1975/2000 98.75
training loss is 1.3e+02 and training accuracy is 22851/23000 99.35
validation loss is 4.2e+02 and validation accuracy is 1981/2000 99.05
training loss is 2e+02 and training accuracy is 22845/23000 99.33
validation loss is 6.1e+02 and validation accuracy is 1982/2000 99.1
training loss is 1e+02 and training accuracy is 22917/23000 99.64
validation loss is 5.3e+02 and validation accuracy is 1986/2000 99.3

集成模型达到了 99.6%的训练精度和 99.3%的验证精度。虽然集成模型功能强大,但计算开销大。它们在解决如 Kaggle 竞赛中的问题时是很好的技术。

编码器-解码器架构

我们在书中看到的几乎所有深度学习算法都擅长学习如何将训练数据映射到其相应的标签。我们不能直接将它们用于需要模型从序列学习并生成另一个序列或图像的任务。一些示例应用包括:

  • 语言翻译

  • 图像字幕

  • 图像生成(seq2img)

  • 语音识别

  • 问答系统

这些问题大多可以看作是某种形式的序列到序列映射,可以使用一系列称为编码器-解码器架构的体系结构来解决。在本节中,我们将了解这些架构背后的直觉。我们不会看这些网络的实现,因为它们需要更详细的学习。

在高层次上,编码器-解码器架构看起来如下所示:

编码器通常是一个递归神经网络RNN)(用于序列数据)或卷积神经网络CNN)(用于图像),它接收图像或序列并将其转换为一个固定长度的向量,该向量编码了所有信息。解码器是另一个 RNN 或 CNN,它学习解码编码器生成的向量,并生成新的数据序列。下图展示了用于图像字幕系统的编码器-解码器架构的外观:

图像字幕系统的编码器-解码器架构

图像来源:https://arxiv.org/pdf/1411.4555.pdf

让我们更详细地看一看图像字幕系统中编码器和解码器架构的内部情况。

编码器

对于图像字幕系统,我们通常会使用训练好的架构,比如 ResNet 或 Inception,从图像中提取特征。就像我们对集成模型所做的那样,我们可以通过使用一个线性层输出固定长度的向量,然后使该线性层可训练。

解码器

解码器是一个长短期记忆LSTM)层,用于为图像生成字幕。为了构建一个简单的模型,我们可以只将编码器嵌入作为 LSTM 的输入传递一次。但是解码器要学习起来可能会很有挑战性;因此,常见做法是在解码器的每一步中提供编码器嵌入。直观地说,解码器学习生成一系列最佳描述给定图像字幕的文本序列。

总结

在本章中,我们探讨了一些现代架构,如 ResNet、Inception 和 DenseNet。我们还探讨了如何使用这些模型进行迁移学习和集成学习,并介绍了编码器-解码器架构,该架构驱动着许多系统,如语言翻译系统。

在下一章中,我们将总结在书籍学习旅程中取得的成就,同时讨论你接下来可以从哪里继续前行。我们将探讨关于 PyTorch 的大量资源以及一些正在使用 PyTorch 进行研究的酷炫深度学习项目。

第九章:下一步是什么?

你做到了!感谢阅读《使用 PyTorch 进行深度学习》。你应该已经对构建深度学习应用所需的核心机制和应用程序接口API)有了坚实的理解。到目前为止,你应该已经能够熟练使用支撑大部分现代深度学习算法的所有基本组件。

接下来是什么?

在本章中,我们将总结我们在本书中学到的内容,并进一步探讨不同的项目和资源。这些项目和资源将有助于你在保持与最新研究同步的旅程中更进一步。

概览

本节提供了我们在整本书中学到的一个鸟瞰视角:

  • 人工智能(AI)历史,机器学习的历史——硬件和算法的各种改进如何在不同应用中触发了深度学习的巨大成功。

  • 如何使用 PyTorch 的各种构建模块,如变量、张量和nn.module,来开发神经网络。

  • 理解训练神经网络涉及的不同过程,例如用于数据准备的 PyTorch 数据集,用于批处理张量的数据加载器,用于创建网络架构的torch.nn包,以及使用 PyTorch 损失函数和优化器。

  • 我们看到了不同类型的机器学习问题以及挑战,例如过拟合和欠拟合。我们还通过了不同的技术,如数据增强、添加 dropout,以及使用批归一化来防止过拟合。

  • 我们学习了卷积神经网络CNNs)的不同构建模块,并学习了迁移学习,这有助于我们使用预训练模型。我们还了解了使用预卷积特征等技术,有助于减少模型训练所需的时间。

  • 我们学习了词嵌入(word embeddings)及其在文本分类问题中的应用。我们还探索了如何使用预训练的词嵌入。我们研究了循环神经网络RNN)及其变种,如长短期记忆网络LSTM),以及如何将它们用于文本分类问题。

  • 我们探索了生成模型,并学习了如何使用 PyTorch 进行艺术风格转换,以及如何使用生成对抗网络(GAN)创建新的 CIFAR 图像。我们还探索了语言建模技术,这些技术可以用于生成新文本或创建特定领域的嵌入。

  • 我们探索了现代架构,如 ResNet、Inception、DenseNet 和编码-解码架构。我们还看到了这些模型如何用于迁移学习。我们还通过结合所有这些模型构建了一个集成模型。

探索有趣的想法

我们在书中学到的大部分概念构成了由深度学习驱动的现代应用程序的基础。在本节中,我们将探讨与计算机视觉和自然语言处理 (NLP) 相关的不同有趣项目。

目标检测

在这本书中看到的所有例子都有助于检测给定图像是这个(猫)还是那个(狗)。但是,要解决现实世界中的一些问题,你可能需要识别图像中的不同对象,例如这里显示的:

目标检测算法的输出

这张图片显示了一个目标检测算法的输出,算法正在检测诸如一只漂亮的狗和猫等对象。就像有现成的图像分类算法一样,有许多令人惊奇的算法可以帮助构建对象识别系统。以下是一些重要算法及其提到的论文列表:

图像分割

假设你正在楼顶阅读这本书。你周围看到了什么?你能勾勒出你看到的东西吗?如果你是个好艺术家,不像我,那么你可能已经画了几座建筑物、树木、鸟类以及周围更多有趣的事物。图像分割算法试图捕捉类似的东西。给定一张图像,它们会为每个像素生成一个预测,识别每个像素属于哪个类别。以下图片展示了图像分割算法的识别结果:

图像分割算法的输出

你可能希望探索用于图像分割的一些重要算法:

PyTorch 中的 OpenNMT

开源神经机器翻译 (OpenNMT) (github.com/OpenNMT/OpenNMT-py) 项目有助于构建许多由编码器-解码器架构驱动的应用程序。你可以构建的一些应用包括翻译系统、文本摘要和图像到文本。

Alien NLP

Alien NLP 是一个建立在 PyTorch 上的开源项目,能够更轻松地完成许多自然语言处理任务。这里有一个演示页面(demo.allennlp.org/machine-comprehension),你可以查看以了解使用 Alien NLP 可以构建什么。

fast.ai – 再次让神经网络变得不那么酷

我最喜欢学习深度学习的地方之一,也是一个极好的灵感来源,是由 fast.ai 的两位出色导师 Jeremy Howard 和 Rachel Thomas 组织的一个 MOOC,其唯一目的是让深度学习对所有人都更加可达,你可以在 www.fast.ai/ 查看。他们为课程的新版本构建了一个令人惊叹的框架(github.com/fastai/fastai),基于 PyTorch,使构建应用程序变得更加简单和快速。如果你还没有开始他们的课程,我强烈建议你开始。探索 fast.ai 框架的构建将为你带来许多强大技术的深刻见解。

开放神经网络交换

开放神经网络交换ONNX)(onnx.ai/)是迈向开放生态系统的第一步,使你能够在项目发展过程中选择合适的工具。ONNX 提供了深度学习模型的开源格式。它定义了可扩展的计算图模型,以及内置运算符和标准数据类型的定义。Caffe2、PyTorch、Microsoft Cognitive Toolkit、Apache MXNet 等工具正在开发 ONNX 支持。这个项目有助于将 PyTorch 模型产品化。

如何保持更新

社交媒体平台,特别是 Twitter,帮助你在该领域保持更新。有许多人可以关注。如果你不确定从哪里开始,我建议你关注 Jeremy Howard(twitter.com/jeremyphoward)及他可能关注的其他有趣的人。这样做可以让 Twitter 的推荐系统为你工作。

另一个你需要关注的重要 Twitter 账号是 PyTorch 的(twitter.com/PyTorch)。PyTorch 背后的惊人团队分享了一些很棒的内容。

如果你在寻找研究论文,可以看看 arxiv-sanitywww.arxiv-sanity.com/),那里许多聪明的研究人员发布他们的论文。

关于 PyTorch 的更多优秀资源包括其教程(pytorch.org/tutorials/)、源代码(github.com/pytorch/pytorch)以及文档(pytorch.org/docs/0.3.0/)。

总结

关于深度学习和 PyTorch 还有很多内容。PyTorch 是一个相对较新的框架,在撰写本章时已经一年了。还有很多东西可以学习和探索,祝你学习愉快。一切顺利。

posted @ 2024-07-23 14:53  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报