原文:
zh.annas-archive.org/md5/057fe0c351c5365f1188d1f44806abda
译者:飞龙
前言
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 并注册,以便直接通过电子邮件获取文件。
您可以按照以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”选项卡。
-
单击“代码下载与勘误”。
-
在搜索框中输入书名并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
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 Exchange(ONNX)的新项目的推出(onnx.ai/
),情况正在发生变化,该项目致力于将在 PyTorch 上开发的模型部署到像 Caffe2 这样的生产就绪平台。在撰写本文时,关于这个项目还为时过早。该项目由 Facebook 和 Microsoft 支持。
在本书的其余部分,我们将学习关于在计算机视觉和自然语言处理领域构建强大深度学习应用程序的各种乐高积木(更小的概念或技术)。
摘要
在本章的介绍中,我们探讨了人工智能、机器学习和深度学习的定义,并讨论了它们之间的区别。我们还看了它们在日常生活中的应用。我们深入探讨了为什么深度学习现在才变得更加流行。最后,我们对 PyTorch 进行了初步介绍,这是一个深度学习框架。
在下一章中,我们将在 PyTorch 中训练我们的第一个神经网络。
第二章:神经网络的构建模块
理解神经网络的基本构建模块,如张量、张量操作和梯度下降,对于构建复杂的神经网络至关重要。在本章中,我们将通过以下主题构建我们的第一个Hello world
神经网络程序:
-
安装 PyTorch
-
实现我们的第一个神经网络
-
将神经网络拆分为功能块
-
逐步了解每个基础模块,涵盖张量、变量、自动微分、梯度和优化器
-
使用 PyTorch 加载数据
安装 PyTorch
PyTorch 可作为 Python 包使用,您可以选择使用pip
或conda
来安装,或者您可以从源代码构建。本书推荐的方法是使用 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 张量)
只包含一个元素的张量称为标量。通常会是FloatTensor
或LongTensor
类型。在撰写本文时,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 张量用于表示类似图像的数据。图像可以表示为矩阵中的数字,这些数字被堆叠在一起。图像形状的一个例子是 224
、224
、3
,其中第一个索引表示高度,第二个表示宽度,第三个表示通道(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)
由于显示大小为 224
、224
、3
的张量会占据书中的几页,我们将显示图像并学习如何将图像切成较小的张量以进行可视化:
显示图像
切片张量
对张量进行切片是常见的操作。一个简单的例子可能是选择一维张量 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 内存限制—16、32 和 64 是常用的批次大小。
让我们看一个例子,我们加载一个大小为 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
函数相对于各个参数(W、b)的变化率。例如,如果a的梯度为 2,那么a值的任何变化都将使Y值增加两倍。如果这不清楚,不要担心——大多数深度学习框架会帮我们计算梯度。在本章中,我们将学习如何利用这些梯度来提高模型的性能。
除了梯度外,变量还有一个指向创建它的函数的引用,该函数反过来指向如何创建每个变量。例如,变量a
包含它是由X
和W
的乘积生成的信息。
让我们看一个例子,我们在其中创建变量并检查梯度和函数引用:
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
函数创建了两个变量x
和y
,大小分别为(17
,1
)和(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
创建可学习参数
在我们的神经网络示例中,我们有两个可学习参数,w
和b
,以及两个固定参数,x
和y
。我们在get_data
函数中创建了变量x
和y
。可学习参数是使用随机初始化创建的,并且require_grad
参数设置为True
,而x
和y
的设置为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,其中w和b是可学习参数。我们的网络必须学习w和b的值,以便wx + b更接近实际y。让我们可视化我们的训练数据集和我们的神经网络必须学习的模型:
输入数据点
以下图表示在输入数据点上拟合的线性模型:
在输入数据点上拟合的线性模型
图像中的深灰色(蓝色)线代表我们的网络学到的模型。
网络实现
由于我们有所有参数(x
,w
,b
和y
)来实现网络,我们对w
和x
进行矩阵乘法。然后,将结果与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
函数中完成的。
损失函数
由于我们从随机值开始,我们的可学习参数 w
和 b
会导致 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
操作来计算我们可学习参数 w
和 b
的梯度。由于我们将多次使用 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
学习率是一个超参数,它允许我们通过梯度的微小变化来调整变量的值,其中梯度表示每个变量(w
和 b
)需要调整的方向。
不同的优化器,如 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
类结合了数据集对象和不同的采样器,例如SequentialSampler
和RandomSampler
,并提供了一个图像批次,使用单进程或多进程迭代器。采样器是为算法提供数据的不同策略。以下是我们的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 团队还维护了两个有用的库,称为torchvision
和torchtext
,它们构建在Dataset
和DataLoader
类之上。我们将在相关章节中使用它们。
摘要
在本章中,我们探讨了由 PyTorch 提供的各种数据结构和操作。我们使用 PyTorch 的基本组件实现了几个部分。对于我们的数据准备,我们创建了算法使用的张量。我们的网络架构是一个模型,用于学习预测用户在我们的 Wondermovies 平台上平均花费的小时数。我们使用损失函数来检查我们模型的标准,并使用optimize
函数来调整模型的可学习参数,使其表现更好。
我们还探讨了 PyTorch 如何通过抽象化处理数据管道的多个复杂性,这些复杂性原本需要我们进行数据并行化和增强。
在下一章中,我们将深入探讨神经网络和深度学习算法的工作原理。我们将探索用于构建网络架构、损失函数和优化的各种 PyTorch 内置模块。我们还将展示如何在真实世界数据集上使用它们。
第三章:深入研究神经网络
在本章中,我们将探索用于解决真实世界问题的深度学习架构的不同模块。在前一章中,我们使用 PyTorch 的低级操作来构建模块,如网络架构、损失函数和优化器。在本章中,我们将探讨解决实际问题所需的神经网络的重要组件,以及 PyTorch 通过提供大量高级功能来抽象掉许多复杂性。在本章末尾,我们将构建解决回归、二元分类和多类分类等真实世界问题的算法。
在本章中,我们将讨论以下主题:
-
深入探讨神经网络的各种构建模块
-
探索 PyTorch 中的高级功能,构建深度学习架构
-
将深度学习应用于真实世界的图像分类问题
深入探讨神经网络的构建模块
正如我们在前一章学到的,训练深度学习算法需要以下步骤:
-
构建数据流水线
-
构建网络架构
-
使用损失函数评估架构
-
使用优化算法优化网络架构权重
在前一章中,网络由使用 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)
我们可以使用weights
和bias
属性访问层的可训练参数:
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
文件夹,并在 train
和 valid
目录内基于类别(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
的实用类,可用于加载图像及其相关标签,当数据以前述格式呈现时。通常进行以下预处理步骤是一种常见做法:
-
将所有图像调整为相同大小。大多数深度学习架构都期望图像大小相同。
-
使用数据集的平均值和标准差对数据进行标准化。
-
将图像数据集转换为 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
类,它接受一个数据集并返回一个图像批次。它抽象了批处理中的许多复杂性,如使用多个工作进程进行变换应用。以下代码将前述的train
和valid
数据集转换为数据加载器:
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 架构是一组层,即Conv2d
、BatchNorm2d
和MaxPool2d
,以特定的方式连接在一起。所有这些算法都会接受一个名为pretrained的参数。当pretrained
为True
时,算法的权重已经针对预测 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
前述函数执行以下操作:
-
将图像通过模型并计算损失。
-
在训练阶段进行反向传播。对于验证/测试阶段,不会调整权重。
-
损失是在每个 epoch 的各个批次中累积的。
-
存储最佳模型并打印验证准确率。
在运行了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: 将算法运行完整数据集称为一个周期。通常会进行多个周期的训练(更新权重)。
评估机器学习模型
在我们上一章节讨论的图像分类示例中,我们将数据分成两半,一半用于训练,另一半用于验证。使用单独的数据集来测试算法的性能是一个良好的实践,因为在训练集上测试算法可能不能真正反映算法的泛化能力。在大多数真实应用中,根据验证准确率,我们经常以不同的方式调整算法,例如添加更多层或不同的层,或者使用我们将在本章后部分介绍的不同技术。因此,你对调整算法选择的可能性更高是基于验证数据集。通过这种方式训练的算法通常在训练数据集和验证数据集上表现良好,但在未见数据上泛化能力较差。这是由于验证数据集中的信息泄漏,影响了我们对算法进行调整。
为了避免信息泄漏问题并提高泛化能力,通常的做法是将数据集分为三个不同部分,即训练、验证和测试数据集。我们通过训练和验证集进行算法的所有超参数调整和训练。在整个训练结束时,您将在测试数据集上测试算法。我们讨论的有两种类型的参数。一种是算法内部使用的参数或权重,这些参数通过优化器或反向传播进行调整。另一组参数称为超参数,控制网络中使用的层数、学习速率和其他类型的参数,通常需要手动更改架构。
特定算法在训练集上表现更好,但在验证或测试集上表现不佳的现象被称为过拟合,或者算法泛化能力不足。还有一个相反的现象,算法在训练集上表现不佳,这称为欠拟合。我们将看看不同的策略,帮助我们克服过拟合和欠拟合问题。
在讨论过拟合和欠拟合之前,让我们先看看在数据集分割方面的各种策略。
训练、验证和测试分割
将数据分成三部分——训练集、验证集和测试集,是最佳实践。使用保留数据集的最佳方法是:
-
在训练集上训练算法
-
基于验证数据集执行超参数调优
-
通过迭代执行前两个步骤,直到达到预期的性能
-
在冻结算法和超参数后,在测试数据集上评估它
避免将数据分割成两部分,因为这可能导致信息泄露。在同一数据集上进行训练和测试是明确禁止的,因为它不能保证算法的泛化。有三种流行的保留策略可用于将数据分割为训练集和验证集。它们如下:
-
简单保留验证
-
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)
因此,前面的代码为我们提供了train
和test
数据集的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
的张量上应用一个滤波器(或内核)大小为3
的Conv1d
:
底部的方框表示我们的输入张量有七个值,连接的方框表示我们应用卷积滤波器后的输出,滤波器大小为三。在图像的右上角,三个方框表示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),通常与卷积一起使用。如果我们仔细观察前面的例子,我们可能会意识到,如果在数据的末尾没有足够的元素供数据进行跨步时,滤波器将停止。填充通过在张量的两端添加零来防止这种情况。让我们再次看一个关于填充如何工作的一维示例:
在前面的图像中,我们应用了带有填充 2
和步幅 1
的 Conv1d
层。
让我们看看 Conv2d 在图像上是如何工作的:
在我们理解 Conv2d 如何工作之前,我强烈建议你查看这篇精彩的博客(setosa.io/ev/image-kernels/
),其中包含一个卷积演示的实时示例。在你花几分钟玩弄演示后,再阅读下一节。
让我们理解演示中发生了什么。在图像的中心框中,我们有两组不同的数字;一组在框中表示,另一组在框下面。框中表示的是像素值,如左手边的白色框所示。框下面标记的数字是用于锐化图像的滤波器(或内核)值。这些数字是手动选择的以执行特定的任务。在这种情况下,它是用来锐化图像的。就像在我们之前的例子中一样,我们进行元素对元素的乘法,并将所有值相加以生成右侧图像像素的值。生成的值由图像右侧的白色框突出显示。
尽管在这个例子中内核中的值是手动选择的,在 CNN 中我们不会手动选择这些值,而是随机初始化它们,并让梯度下降和反向传播调整内核的值。学习到的内核将负责识别不同的特征,如线条、曲线和眼睛。让我们看看另一张图像,我们把它看作是一个数字矩阵,并看看卷积是如何工作的:
在前一张屏幕截图中,我们假设 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
,它决定了核的大小。一些常用的核大小包括1、3、5和7。核大小越大,滤波器能覆盖的区域就越大,因此在早期层中观察到使用7或9的滤波器是常见的。
池化
在卷积层后添加池化层是一种常见做法,因为它们可以减小特征图的大小和卷积层的输出。
池化提供两种不同的特性:一是减少要处理的数据大小,另一种是强制算法不要关注图像中位置的微小变化。例如,一个人脸检测算法应该能够在图片中检测到人脸,而不管人脸在照片中的位置如何。
让我们来看看 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
这种方法在training
和validation
中有不同的逻辑。使用不同模式主要有两个原因:
-
在
train
模式下,dropout 会删除一定百分比的值,在验证或测试阶段不应该发生这种情况。 -
对于
training
模式,我们计算梯度并改变模型的参数值,但在测试或验证阶段不需要反向传播。
前一个函数中的大部分代码是不言自明的,正如前几章中讨论的那样。在函数的最后,我们返回该特定时期模型的loss
和accuracy
。
让我们运行模型通过前述函数进行 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
库中提供了一组经过训练的模型。当参数pretrained
为True
时,大多数模型会下载针对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
模型,并返回了卷积特征以及与之相关联的标签。其余的代码与我们在其他示例中用于创建数据加载器和数据集的代码类似。
一旦我们对train
和validation
集合有了卷积特征,让我们创建 PyTorch 数据集和DataLoader
类,这将简化我们的训练过程。以下代码创建了用于我们卷积特征的Dataset
和DataLoader
:
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
作为模型,用于创建optimizer
和fit
方法。以下代码训练分类器模块以识别狗和猫。在 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 内部运作过程的不同技术。
在下一章中,我们将学习如何使用循环神经网络处理序列数据。