原文:
annas-archive.org/md5/c9f2178832e6a58ea7603b6cecb80b22
译者:飞龙
序言
本书将从深度神经网络(NN)的理论基础开始,深入探讨最流行的网络架构——变压器(transformers)、基于变压器的大型语言模型(LLMs)和卷积网络。它将通过各种计算机视觉和自然语言处理(NLP)的实例来介绍这些模型,包括最先进的应用,如文本生成图像和聊天机器人。
每一章都包括一个全面的理论介绍,作为主体内容。接着是代码示例,用于验证所展示的理论,提供读者实际的动手经验。示例使用 PyTorch、Keras 或 Hugging Face Transformers 执行。
本书适用人群
本书面向已经熟悉编程的读者——软件开发人员/工程师、学生、数据科学家、数据分析师、机器学习工程师、统计学家,以及任何有 Python 编程经验并对深度学习感兴趣的人。它的设计适合那些对深度学习有最小预备知识的读者,语言清晰简洁。
本书内容
第一章,机器学习——简介,讨论了基本的机器学习范式。它将探讨各种机器学习算法,并介绍第一个神经网络,使用 PyTorch 实现。
第二章,神经网络,首先介绍与神经网络相关的数学分支——线性代数、概率论和微积分。它将重点讲解神经网络的构建模块和结构。还将讨论如何使用梯度下降法和反向传播训练神经网络。
第三章,深度学习基础,介绍了深度学习的基本范式。它将从经典网络过渡到深度神经网络,概述开发和使用深度网络的挑战,并讨论如何解决这些问题。
第四章,卷积网络与计算机视觉,介绍了卷积网络——计算机视觉应用的主要网络架构。它将详细讨论卷积网络的特性和构建模块,并介绍当今最流行的卷积网络模型。
第五章,高级计算机视觉应用,讨论了卷积网络在高级计算机视觉任务中的应用——目标检测和图像分割。它还将探讨如何使用神经网络生成新图像。
第六章,自然语言处理与循环神经网络,介绍了 NLP 的主要范式和数据处理流程。它还将探讨循环神经网络及其两种最流行的变种——长短期记忆(LSTM)和门控循环单元(GRU)。
第七章,注意力机制与变换器,介绍了近年来深度学习领域最重要的进展之一——注意力机制以及围绕它构建的变换器模型。
第八章,深入探索大型语言模型,介绍了基于变换器的大型语言模型(LLM)。它将讨论这些模型的特点以及它们与其他 NN 模型的区别,并将介绍 Hugging Face Transformers 库。
第九章,大型语言模型的高级应用,讨论了使用 LLM 进行计算机视觉任务。它将专注于经典任务,如图像分类和目标检测,同时也会探索最前沿的应用,如图像生成。它还将介绍 LangChain 框架,帮助开发基于 LLM 的应用。
第十章,机器学习操作(MLOps),将介绍各种库和技术,帮助更轻松地开发和部署 NN 模型到生产环境中。
为了最大化本书的价值
本书中的许多代码示例需要 GPU 的支持。如果你没有 GPU,也不用担心。为了避免硬件限制,所有的代码示例都提供了 Jupyter 笔记本,并可以在 Google Colab 上执行。因此,即使你的硬件不足以运行示例,你仍然可以在 Colab 上运行它们。
本书涵盖的 软件/硬件 | 操作系统要求 |
---|---|
PyTorch 2.0.1 | Windows、macOS 或 Linux |
TensorFlow 2.13 | Windows(遗留支持)、macOS 或 Linux |
Hugging Face Transformers 4.33 | Windows、macOS 或 Linux |
本书中的某些代码示例可能使用了表格中未列出的额外包。你可以在书籍的 GitHub 仓库中的 requirements.txt 文件里查看完整的包列表(包括版本号)。
如果你使用的是本书的电子版,我们建议你自己输入代码,或者访问本书的 GitHub 仓库获取代码(下一个章节会提供链接)。这样做可以帮助你避免复制粘贴代码时可能出现的错误。
下载示例代码文件
你可以从 GitHub 下载本书的示例代码文件,地址为 github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/
。如果代码有更新,GitHub 仓库中会及时更新。
我们还提供了来自我们丰富图书和视频目录的其他代码包,访问 github.com/PacktPublishing/
查看!
使用的约定
本书中使用了多种文本约定。
文中的代码
:表示文中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“使用 opencv-python
读取位于 image_file_path
的 RGB 图像。”
一段代码块如下所示:
def build_fe_model():
""""Create feature extraction model from the pre-trained model ResNet50V2"""
# create the pre-trained part of the network, excluding FC layers
base_model = tf.keras.applications.MobileNetV3Small(
任何命令行输入或输出如下所示:
import tensorflow.keras
当我们希望您关注代码块的某一部分时,相关的行或项目会用粗体标出:
import io
image = Image.open(io.BytesIO(response.content))
image.show()
提示或重要注意事项
以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请发送电子邮件至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:虽然我们已尽最大努力确保内容的准确性,但难免会有错误。如果您在本书中发现了错误,我们将不胜感激,您可以将其报告给我们。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上遇到任何形式的非法复制作品,我们将非常感激您能提供该材料的位置或网站名称。请通过 copyright@packt.com 与我们联系,并附上相关链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识并且有兴趣撰写或贡献书籍,请访问 authors.packtpub.com.
分享您的想法
阅读完*Python 深度学习(第三版)*后,我们很想听听您的想法!请点击这里直接访问本书的亚马逊评价页面并分享您的反馈。
您的评价对我们和技术社区非常重要,它将帮助我们确保提供优质的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢随时随地阅读,但无法随身携带纸质书籍吗?
您的电子书购买是否与您选择的设备不兼容?
不用担心,现在每本 Packt 书籍您都能免费获得该书的无 DRM 限制的 PDF 版本。
在任何地方,任何设备上阅读。搜索、复制并将您最喜欢的技术书籍中的代码直接粘贴到您的应用程序中。
福利还不止这些,您可以独享折扣、新闻简报和每天通过电子邮件收到的精彩免费内容。
按照这些简单步骤来获得福利:
- 扫描二维码或访问以下链接
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_QR_Free_PDF.jpg
packt.link/free-ebook/9781837638505
-
提交您的购买凭证
-
就这样!我们会直接将您的免费 PDF 和其他福利发送到您的电子邮件。
第一部分:
介绍
到神经网络
我们将从介绍基本的机器学习理论和概念开始,然后深入介绍神经网络——一种特殊类型的机器学习算法。
我们将讨论它们背后的数学原理,并学习如何训练它们。最后,我们将从浅层网络过渡到深层网络。
本部分包含以下章节:
-
第一章, 机器学习导论
-
第二章, 神经网络
-
第三章, 深度学习基础
第一章:机器学习——简介
机器学习(ML)技术正在广泛应用于各个领域,数据科学家在许多不同的行业中需求量很大。通过机器学习,我们识别出从数据中获得不易察觉的知识以做出决策的过程。机器学习技术的应用领域各不相同,涵盖了医学、金融、广告等多个学科。
在本章中,我们将介绍不同的机器学习方法、技术及其在实际问题中的一些应用,同时也将介绍 Python 中一个主要的开源机器学习包——PyTorch。这为后续章节奠定了基础,在这些章节中,我们将专注于使用神经网络(NNs)的某种类型的机器学习方法,特别是我们将重点讲解深度学习(DL)。
深度学习(DL)采用了比以往更先进的神经网络(NN)。这不仅是近年来理论发展的结果,也是计算机硬件进步的体现。本章将总结机器学习(ML)的基本概念及其应用,帮助你更好地理解深度学习(DL)如何区别于传统的流行机器学习技术。
本章我们将讨论以下主要内容:
-
机器学习简介
-
不同的机器学习方法
-
神经网络
-
PyTorch 简介
技术要求
我们将在本章中使用 Python 和 PyTorch 实现示例。如果你还没有配置好相关环境,也不必担心——示例代码已作为 Jupyter 笔记本提供在 Google Colab 上。你可以在本书的 GitHub 仓库中找到代码示例:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter01
。
机器学习简介
机器学习(ML)通常与大数据和人工智能(AI)等术语相关联。然而,这两者与机器学习是完全不同的。要理解机器学习是什么以及它为何有用,首先需要了解大数据是什么,以及机器学习如何应用于大数据。
大数据是一个用来描述由大量数据积累和存储所产生的大型数据集的术语。例如,这些数据可能来源于摄像头、传感器或社交网络网站。
我们每天创造多少数据?
据估计,仅谷歌每天处理的信息量就超过 20PB,而且这个数字还在不断增加。几年前,福布斯估计,每天创造的数据量为 2.5 万亿字节,而全球 90%的数据是在过去两年内生成的。
人类无法独立掌握,更不用说分析如此庞大的数据量,机器学习技术就是用来理解这些非常大的数据集的工具。机器学习是用于大规模数据处理的工具。它非常适合具有大量变量和特征的复杂数据集。许多机器学习技术,尤其是深度学习的优势之一是,它们在大数据集上使用时表现最佳,从而提高了它们的分析和预测能力。换句话说,机器学习技术,特别是深度学习神经网络,最擅长在能够访问大量数据集的情况下进行学习,在这些数据集中它们能够发现隐藏的模式和规律。
另一方面,机器学习的预测能力可以成功地应用到人工智能(AI)系统中。机器学习可以被认为是人工智能系统的大脑。人工智能可以定义为(尽管这种定义可能不唯一)一种能够与其环境进行交互的系统。此外,AI 机器配备了传感器,使它们能够了解所处的环境,并配有工具来与环境进行交互。因此,机器学习就是让机器分析通过传感器获取的数据并得出合适答案的大脑。一个简单的例子是 iPhone 上的 Siri。Siri 通过麦克风接收命令,并通过扬声器或显示屏输出答案,但为了做到这一点,它需要理解收到的命令。类似地,自动驾驶汽车将配备摄像头、GPS 系统、声纳和激光雷达,但所有这些信息都需要经过处理才能提供正确的答案。这可能包括是否加速、刹车或转弯。机器学习是这种信息处理方法,能够得出最终答案。
我们已经解释了什么是机器学习(ML),那深度学习(DL)呢?目前,我们暂且说深度学习是机器学习的一个子领域。深度学习方法具有一些特殊的共同特征。最著名的代表方法就是深度神经网络(NN)。
不同的机器学习方法
如我们所见,机器学习(ML)这一术语被广泛使用,它指的是从大量数据集中推断模式的通用技术,或者是基于分析已知数据来对新数据进行预测的能力。机器学习技术大致可以分为两大核心类别,此外通常还会添加一个类别。以下是这些类别:
-
监督学习
-
无监督学习
-
强化学习
让我们来仔细看看。
监督学习
监督学习算法是一类机器学习算法,它们使用之前标注过的数据来学习其特征,以便能够对类似但未标注的数据进行分类。让我们通过一个例子来更好地理解这个概念。
假设一个用户每天会收到许多电子邮件,其中一些是重要的商务邮件,而另一些则是未经请求的垃圾邮件,也就是垃圾邮件。监督式机器学习算法会接收到大量已经由“教师”标注为垃圾邮件或非垃圾邮件的电子邮件(这叫做训练数据)。对于每个样本,机器将尝试预测该邮件是否是垃圾邮件,并将预测结果与原始目标标签进行比较。如果预测结果与目标不符,机器将调整其内部参数,以便下次遇到该样本时,能够正确分类。相反,如果预测正确,参数将保持不变。我们给算法提供的训练数据越多,它的表现就会越好(但这一规则也有例外,稍后我们会讨论)。
在我们使用的例子中,电子邮件只有两个类别(垃圾邮件或非垃圾邮件),但是相同的原则适用于具有任意类别(或分类)的任务。例如,Google 提供的免费电子邮件服务 Gmail 允许用户选择最多五个类别,分类标签如下:
-
主要:包括人与人之间的对话
-
促销:包括营销邮件、优惠和折扣
-
社交:包括来自社交网络和媒体共享网站的消息
-
更新:包括账单、银行对账单和收据
-
论坛:包括来自在线小组和邮件列表的消息
总结来说,机器学习任务是将一组输入值映射到有限数量的类别,这个任务叫做分类。
在某些情况下,结果可能不一定是离散的,我们可能没有有限数量的类别来对数据进行分类。例如,我们可能尝试根据预定的健康参数预测一组人的预期寿命。在这种情况下,结果是一个数值,我们不再谈论分类,而是谈论回归。
一种看待监督学习的方法是想象我们正在构建一个函数,f,该函数定义在一个数据集上,数据集由特征组织的信息组成。在电子邮件分类的例子中,特征可以是一些在垃圾邮件中出现频率高于其他单词的特定词汇。显式的性别相关词汇最有可能识别出垃圾邮件,而不是商务/工作邮件。相反,像meeting、business或presentation这样的词更有可能描述工作邮件。如果我们可以访问元数据,我们也可以使用发件人的信息作为特征。每封电子邮件将具有一组相关的特征,每个特征将有一个值(在这种情况下,就是特定单词在电子邮件正文中出现的次数)。然后,机器学习算法将尝试将这些值映射到一个离散的范围,该范围代表一组类别,或者在回归的情况下,映射到一个实数值。f函数的定义如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/1.png
在后续章节中,我们将看到一些分类或回归问题的示例。我们将讨论的一个问题是对修改后的国家标准与技术研究所(MNIST)数据库中的手写数字进行分类(yann.lecun.com/exdb/mnist/
)。当给定一个表示 0 到 9 的图像集合时,机器学习算法将尝试将每个图像分类到 10 个类别之一,每个类别对应 10 个数字中的一个。每个图像的大小为 28×28 (= 784) 像素。如果我们把每个像素看作一个特征,那么算法将使用 784 维的特征空间来分类这些数字。
下图展示了来自 MNIST 数据集的手写数字:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_1.jpg
图 1.1 – 来自 MNIST 数据集的手写数字示例
在接下来的章节中,我们将讨论一些最受欢迎的经典监督学习算法。以下内容绝非详尽无遗,也不是每种机器学习方法的深入描述。我们建议参考 Sebastian Raschka 所著的《Python 机器学习》一书(www.packtpub.com/product/python-machine-learning-third-edition/9781789955750
)。这是一本简单的回顾,旨在为你提供不同机器学习技术的概览,特别是在 Python 中的应用。
线性回归与逻辑回归
回归算法是一种监督学习算法,它利用输入数据的特征来预测一个数值,例如在给定房屋的大小、年龄、浴室数量、楼层数和位置等特征的情况下,预测房屋的价格。回归分析试图找到使输入数据集最符合的函数参数值。
在线性回归算法中,目标是通过在输入数据上找到适当的参数,使得函数最小化代价函数,从而最好地逼近目标值。代价函数是误差的函数——即,我们离正确结果有多远。一个常见的代价函数是均方误差(MSE),它通过取预期值与预测结果之间差值的平方来计算。所有输入示例的和给出了算法的误差,并代表了代价函数。
假设我们有一栋 100 平方米的房子,它建于 25 年前,有三个浴室和两层楼。我们还假设这座城市分为 10 个不同的社区,我们用 1 到 10 的整数来表示这些社区,假设这座房子位于编号为 7 的区域。我们可以用一个五维向量来表示这栋房子,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/2.png。假设我们还知道这栋房子的估计价值为$100,000(在今天的世界里,这可能只够在北极附近买一座小屋,但我们假设如此)。我们想要创建一个函数,f,使得 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/3.png。
鼓励的话
如果你没有完全理解本节中的某些术语,不用担心。我们将在第二章中更详细地讨论向量、成本函数、线性回归和梯度下降。我们还将看到,训练神经网络(NN)和线性/逻辑回归有很多相似之处。现在,你可以将向量看作一个数组。我们将用粗体字表示向量——例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/4.png。我们将用斜体字和下标表示向量的元素——例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/5.png。
在线性回归中,这意味着找到一个权重向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/6.png,使得向量的点积,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/7.png,将是 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/8.png 或者 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/9.png。如果我们有 1000 栋房子,我们可以对每栋房子重复相同的过程,理想情况下,我们希望找到一个单一的向量,w,能够预测每栋房子足够接近的正确值。训练线性回归模型的最常见方法可以在以下伪代码块中看到:
Initialize the vector w with some random values
repeat:
E = 0 # initialize the cost function E with 0
for every pair of the training set:
# here is the real house price
# Mean Square Error
use gradient descent to update the weights w based on MSE until MSE falls below threshold
首先,我们遍历训练数据来计算代价函数 MSE。一旦我们知道了 MSE 的值,就会使用 梯度下降 算法来更新向量的权重,w。为此,我们将计算代价函数关于每个权重的导数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/14.png。通过这种方式,我们就能知道代价函数如何相对于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/15.png 发生变化(增加或减少)。然后,我们将相应地更新该权重的值。
之前,我们演示了如何使用线性回归解决回归问题。现在,让我们来处理一个分类任务:尝试判断一座房子是被高估还是低估。在这种情况下,目标数据将是分类的[1, 0]——1 代表高估,0 代表低估。房子的价格将作为输入参数,而不再是目标值。为了解决这个问题,我们将使用逻辑回归。这与线性回归类似,但有一个区别:在线性回归中,输出是 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/16.png。然而,在这里,输出将是一个特殊的逻辑函数 (en.wikipedia.org/wiki/Logistic_function
),https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/17.png。这将把 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/18.png 的值压缩到 (0:1) 区间。你可以将逻辑函数看作是一个概率,结果越接近 1,房子被高估的可能性越大,反之亦然。训练过程与线性回归相同,但函数的输出在 (0:1) 区间内,标签要么是 0,要么是 1。
逻辑回归不是一种分类算法,但我们可以将其转化为分类算法。我们只需引入一条规则,根据逻辑函数的输出确定类别。例如,如果值为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/19.png 时,我们可以认为一所房子被高估,否则它被低估。
多元回归
本节中的回归示例具有单一的数值输出。回归分析可以有多个输出。我们将这种分析称为多元回归。
支持向量机
支持向量机(SVM)是一种监督式机器学习算法,主要用于分类。它是核方法算法类中最流行的成员。SVM 试图找到一个超平面,将数据集中的样本分隔开。
超平面
超平面是一个高维空间中的平面。例如,单维空间中的超平面是一个点,二维空间中的超平面是直线。在三维空间中,超平面是一个平面,而在四维空间中我们无法直观地想象超平面,但我们知道它的存在。
我们可以将分类看作是寻找一个超平面的过程,该超平面能够分隔不同的点集。一旦我们定义了特征,数据集中的每个样本(在我们的例子中是一个电子邮件)都可以看作是特征多维空间中的一个点。该空间的一维代表一个特征的所有可能值。一个点(一个样本)的坐标是该样本每个特征的具体值。机器学习算法的任务是绘制一个超平面,将不同类别的点分开。在我们的例子中,超平面将垃圾邮件与非垃圾邮件分开。
在下图中,顶部和底部展示了两类点(红色和蓝色),这些点位于二维特征空间中(x 和 y 轴)。如果一个点的 x 和 y 值都低于 5,则该点为蓝色。其他情况下,该点为红色。在这种情况下,类别是线性可分的,这意味着我们可以用一个超平面将它们分开。相反,右图中的类别是线性不可分的:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_2.jpg
图 1.2 – 一组线性可分的点集(左)和一组线性不可分的点集(右)
支持向量机(SVM)尝试找到一个超平面,使得它与各个点之间的距离最大。换句话说,在所有可以分开样本的超平面中,SVM 找到的是那个与所有点的距离最大的超平面。此外,SVM 还可以处理非线性可分的数据。有两种方法可以实现这一点:引入软间隔或使用核技巧。
软间隔通过允许少数被误分类的元素,同时保持算法的最强预测能力来工作。在实际应用中,最好避免对机器学习模型过拟合,我们可以通过放宽一些支持向量机(SVM)的假设来做到这一点。
核技巧通过不同的方式解决相同的问题。假设我们有一个二维特征空间,但类别是线性不可分的。核技巧使用一个核函数,通过为数据添加更多的维度来对其进行转换。在我们的例子中,经过转换后,数据将变成三维的。二维空间中线性不可分的类别在三维空间中将变得线性可分,我们的问题得以解决:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_3.jpg
图 1.3 – 在应用核技巧之前的非线性可分数据集(左),以及应用核技巧后的同一数据集,数据变得线性可分(右)
让我们来看看列表中的最后一个,决策树。
决策树
另一个流行的监督算法是决策树,它创建一个树形的分类器。决策树由决策节点组成,在这些节点上进行特征测试,以及叶节点,表示目标属性的值。为了分类一个新样本,我们从树的根节点开始,沿着节点向下导航,直到到达叶节点。
该算法的一个经典应用是鸢尾花数据集(archive.ics.uci.edu/ml/datasets/Iris
),其中包含来自三种鸢尾花的 50 个样本的数据。
(鸢尾花 Setosa, 鸢尾花 Virginica, 和 鸢尾花 Versicolor)。创建该数据集的罗纳德·费舍尔(Ronald Fisher)测量了这三种花的四个不同特征:
-
它们萼片的长度
-
它们萼片的宽度
-
它们花瓣的长度
-
它们花瓣的宽度
基于这些特征的不同组合,可以创建一个决策树来决定每朵花属于哪个物种。在下图中,我们定义了一个决策树,它将仅使用这两个特征——花瓣长度和宽度,正确地分类几乎所有的花:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_4.jpg
图 1.4 – 用于分类鸢尾花数据集的决策树
要对一个新样本进行分类,我们从树的根节点(花瓣长度)开始。如果样本满足条件,我们就向左走到叶子节点,表示 Iris Setosa 类别。如果不满足条件,我们就向右走到一个新节点(花瓣宽度)。这个过程会一直进行,直到我们到达叶子节点。
近年来,决策树经历了两项重大改进。第一项是随机森林,这是一种集成方法,它结合了多个决策树的预测结果。第二项是一个叫做梯度提升的算法类,它通过创建多个连续的决策树,每棵树都试图改进前一棵树所犯的错误。由于这些改进,决策树在处理某些类型的数据时变得非常流行。例如,它们是 Kaggle 比赛中最常用的算法之一。
无监督学习
第二类机器学习算法是无监督学习。在这里,我们不事先标注数据;相反,我们让算法自行得出结论。无监督学习算法相对于监督学习算法的一个优势是,我们不需要标签数据。为监督算法生成标签可能既昂贵又缓慢。解决这个问题的一种方法是修改监督算法,使其使用更少的标注数据;对此有不同的技术。但另一种方法是使用一种根本不需要标签的算法。在本节中,我们将讨论一些无监督算法。
聚类
无监督学习最常见、也许是最简单的例子之一是聚类。这是一种尝试将数据分成多个子集的技术。
为了说明这一点,让我们将垃圾邮件与非垃圾邮件的电子邮件分类看作一个无监督学习问题。在监督学习的情况下,对于每封电子邮件,我们都有一组特征和一个标签(垃圾邮件或非垃圾邮件)。在这里,我们将使用相同的特征集,但电子邮件不会被标注。相反,我们会让算法在给定特征集的情况下,将每个样本分配到两个独立的组(或簇)之一。然后,算法会尝试以一种方式组合这些样本,使得同一簇内的相似度(即同一簇内样本的相似性)很高,而不同簇之间的相似度很低。不同的聚类算法使用不同的度量标准来衡量相似度。对于一些更高级的算法,你甚至不需要指定簇的数量。
以下图示显示了如何将一组点分类为三个子集:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_5.jpg
图 1.5 – 将一组点聚类为三个子集
K-means
K-means 是一种聚类算法,它将数据集中的元素分成 k 个不同的簇(因此名称中有一个 k)。它的工作原理如下:
-
从特征空间中选择 k 个随机点,称为质心,它们将代表每个 k 个簇的中心。
-
将数据集中的每个样本(即特征空间中的每个点)分配给与其最接近的质心所在的簇。
-
对于每个簇,我们通过计算簇内所有点的均值来重新计算新的质心。
-
使用新的质心,我们重复执行 步骤 2 和 步骤 3,直到满足停止准则。
上述方法对初始随机质心的选择非常敏感,可能需要用不同的初始选择重复进行。也有可能某些质心离数据集中的任何点都很远,从而减少簇的数量,低于 k。最后值得一提的是,如果我们在鸢尾花数据集上使用 k-means 并设置 k=3,我们可能会得到与之前介绍的决策树分布不同的样本分布。再次强调,这突显了为每个问题选择和使用正确的机器学习方法的重要性。
现在,让我们讨论一个使用 k-means 聚类的实际例子。假设一个披萨配送点想在城市中开设四个新加盟店,并需要选择站点的位置。我们可以通过 k-means 来解决这个问题:
-
找出披萨订单最多的地点,这些地点将作为我们的数据点。
-
选择四个随机点作为新站点的位置。
-
通过使用 k-means 聚类,我们可以识别出四个最佳位置,最小化与每个配送地点的距离:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_6.jpg
图 1.6 – 披萨配送最多的地点分布(左);圆点表示新加盟店应选择的位置及其相应的配送区域(右)
自监督学习
自监督学习指的是通过一组合适的问题和数据集,自动生成(即无需人工干预)标注数据。一旦我们拥有这些标签,就可以训练一个监督算法来解决我们的任务。为了更好地理解这个概念,让我们讨论一些应用场景:
-
时间序列预测:假设我们需要基于时间序列的最新历史数据来预测其未来的值。此类例子包括股票(以及现在的加密货币)价格预测和天气预测。为了生成一个标注的数据样本,我们将选取一个长度为 k 的历史数据窗口,窗口以过去时刻 t 作为终点。我们将使用时间区间 [t – k; t] 内的历史值作为监督算法的输入,同时取时刻 t + 1 的历史值作为给定输入样本的标签。我们可以对其余的历史值应用这种划分方式,自动生成标注的训练数据集。
-
自然语言处理(NLP):类似于时间序列,自然文本代表了一个单词(或标记)序列。我们可以训练一个 NLP 算法,基于前面的k个单词来预测下一个单词,方式与时间序列类似。然而,自然文本不像时间序列那样有严格的过去/未来划分。正因为如此,我们可以使用目标单词周围的整个上下文作为输入——也就是说,不仅仅是前面的单词,还包括目标单词后面的单词。如我们将在第六章中看到的,这种技术是当代 NLP 算法的基础。
-
自编码器:这是一种特殊类型的神经网络,旨在重建其输入。换句话说,自编码器的目标值(标签)等于输入数据,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/20.png,其中i是样本索引。我们可以正式地说,它试图学习一个恒等函数(即重复其输入的函数)。由于我们的标签只是输入数据,自编码器是一个无监督算法。你可能会想,试图预测其输入的算法有什么意义。自编码器分为两部分——编码器和解码器。首先,编码器尝试将输入数据压缩成比输入本身更小的向量。接着,解码器根据这个较小的内部状态向量重建原始输入。通过设定这个限制,自编码器被迫仅提取输入数据中最重要的特征。自编码器的目标是学习一种比原始表示更高效或更紧凑的数据表示,同时尽可能保留原始信息。
自监督学习的另一个有趣应用是生成模型,而非判别模型。让我们讨论这两者之间的区别。给定输入数据,判别模型会将其映射到一个特定的标签(换句话说,就是分类或回归)。一个典型的例子是将 MNIST 图像分类为 10 个数字类别中的一个,其中神经网络将输入数据特征(像素强度)映射到数字标签。我们也可以用另一种方式来表达:判别模型给出的是在给定* x (输入)时, y *(类别)的概率 —— https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/21.png。在 MNIST 的情况下,这是在给定图像的像素强度时,数字的概率。
另一方面,生成模型学习类的分布情况。你可以将它视为与判别模型的相反。它不是在给定某些输入特征的情况下预测类的概率 y,而是尝试在给定某个类 y 时预测输入特征的概率 – https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/22.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/23.png。例如,生成模型能够在给定数字类别时生成手写数字的图像。由于我们只有 10 个类别,它将仅能生成 10 张图像。然而,我们只是用这个例子来说明这一概念。实际上,类别可以是一个任意的张量值,模型将能够生成无限数量的具有不同特征的图像。如果你现在不理解这一点,别担心;我们将在第五章中再次讨论这个话题。在第八章和第九章中,我们将讨论变换器(新型神经网络)如何被用于生成一些令人印象深刻的生成模型。由于它们产生的吸引人结果,变换器在研究界和主流公众中都获得了很大的关注。最受欢迎的两种视觉模型是稳定扩散(github.com/CompVis/stable-diffusion
),由 Stability AI(stability.ai/
)开发,以及DALL-E(openai.com/dall-e-2/
),由 OpenAI 开发,它们可以从自然语言描述生成逼真的或艺术性的图像。当提示文本为弹吉他的音乐青蛙时,稳定扩散生成以下图像:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_7.jpg
图 1.7 – 稳定扩散对“弹吉他的音乐青蛙”提示的输出
另一个有趣的生成模型是 OpenAI 的ChatGPT(GPT 代表生成预训练变换器),它(正如其名字所示)充当一个智能聊天机器人。ChatGPT 能够回答后续问题、承认错误、挑战不正确的前提,并拒绝不当的请求。
强化学习
第三类机器学习技术叫做强化学习(RL)。我们将通过一个强化学习的最流行应用来说明这一点:教机器如何玩游戏。机器(或智能体)与游戏(或环境)进行互动。智能体的目标是赢得游戏。为此,智能体采取能够改变环境状态的行动。环境对智能体的行动做出反应,并为其提供奖励(或惩罚)信号,帮助智能体决定下一步的行动。赢得游戏将提供最大的奖励。用正式的术语来说,智能体的目标是最大化它在整个游戏过程中获得的总奖励:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_8.jpg
图 1.8 – 强化学习系统中不同元素之间的交互
让我们把国际象棋游戏看作一个强化学习(RL)问题。在这里,环境将包括棋盘以及棋子的具体位置。我们的智能体的目标是击败对手。当智能体捕获对方的棋子时,它会获得奖励,如果将对方将死,则会获得最大奖励。相反,如果对方捕获了智能体的棋子或将其将死,奖励将是负数。然而,作为更大策略的一部分,玩家们必须做出既不捕获对方棋子也不将死对方国王的行动。在这种情况下,智能体将不会收到任何奖励。如果这是一个监督学习问题,我们将需要为每一步提供标签或奖励。但在强化学习中并非如此。在强化学习框架中,智能体将通过试错法即兴决定其下一步行动。
让我们再举一个例子,有时候我们不得不牺牲一个兵,以实现更重要的目标(比如棋盘上的更好位置)。在这种情况下,我们的聪明智能体必须足够聪明,将短期的损失视为长期的收益。在一个更极端的例子中,假设我们很不幸地与现任世界象棋冠军丁立人对弈。毫无疑问,在这种情况下,智能体会输。然而,我们如何知道哪些步伐是错误的,并导致了智能体的失败呢?国际象棋属于一种问题类型,其中需要将整个游戏视为一个整体来寻找成功的解决方案,而不是仅仅看每个行动的即时后果。强化学习将为我们提供框架,帮助智能体在这个复杂的环境中导航并进行学习。
这个新获得的行动自由带来了一个有趣的问题。假设代理已经学会了一种成功的棋类策略(或者用 RL 术语说,是策略)。经过若干场比赛后,对手可能猜到这一策略并设法战胜我们。现在代理将面临一个困境,需要做出以下决策:要么继续遵循当前的策略,冒着变得可预测的风险,要么尝试新的棋步来让对手吃惊,但这也带来了可能更糟糕的风险。一般来说,代理使用一种给他们带来某种奖励的策略,但他们的终极目标是最大化总奖励。修改后的策略可能带来更多的奖励,如果代理不尝试找到这样的策略,他们将无效。RL 的一个挑战是如何在利用(遵循当前策略)和探索(尝试新动作)之间进行权衡。
到目前为止,我们只使用了游戏作为例子;然而,许多问题可以归入 RL 领域。例如,你可以将自动驾驶汽车看作一个 RL 问题。如果汽车保持在车道内并遵守交通规则,它会获得正奖励;如果发生碰撞,则会获得负奖励。RL 的另一个有趣的应用是管理股票投资组合。代理的目标是最大化投资组合的价值,奖励直接来源于投资组合中股票的价值。
本版中没有 RL 的内容
本书的第二版包含了两章关于强化学习(RL)的内容。在这一版中,我们将省略这些章节,而是讨论变换器及其应用。一方面,RL 是一个有前景的领域,但目前训练 RL 模型的速度较慢,且其实际应用有限。因此,RL 研究主要集中在资金充足的商业公司和学术机构。另一方面,变换器代表了机器学习领域的下一个重大进步,就像 GPU 训练的深度网络在 2009-2012 年间激发了该领域的兴趣一样。
Q 学习
Q-learning 是一种脱离策略的时序差分强化学习(RL)算法。听起来有点复杂!但不用担心,我们不必纠结这些术语的具体含义,而是直接看一下这个算法是如何工作的。为此,我们将使用前一节介绍的国际象棋游戏。提醒一下,棋盘的配置(棋子的位置)就是环境的当前状态。在这里,智能体可以通过移动棋子来采取动作,a,从而将状态转变为新的状态。我们将国际象棋游戏表示为一个图,其中不同的棋盘配置是图的顶点,而每种配置下可能的棋步则是边。要进行一次移动,智能体从当前状态 s 按照边移动到新状态 s’。基本的 Q-learning 算法使用 Q-表 来帮助智能体决定要采取哪些行动。Q-表为每个棋盘配置提供一行,而表格的列则是智能体可以采取的所有可能动作(棋步)。表格的一个单元格 q(s, a) 存储的是累积的期望回报,也称为 Q 值。这是智能体从当前状态 s 执行一个动作 a 后,在接下来的游戏中可能获得的总回报。起初,Q-表会用一个任意的值进行初始化。掌握了这些知识后,让我们看看 Q-learning 是如何工作的:
Initialize the Q table with some arbitrary value
for each episode:
Observe the initial state s
for each step of the episode:
Select new action a using Q-table based policy
Observe reward r and go to the new state s'
Use Bellman eq to update q(s, a) in the Q-table
until we reach a terminal state for the episode
一局游戏从一个随机初始状态开始,并在达到终止状态时结束。在我们的案例中,一局游戏就是一场完整的国际象棋比赛。
由此产生的问题是,智能体的策略如何决定下一步行动?为此,策略必须考虑当前状态下所有可能动作的 Q 值。Q 值越高,动作的吸引力越大。然而,策略有时会忽略 Q-表(即利用现有知识),并选择另一个随机的动作来寻找更高的潜在回报(探索)。在开始时,智能体会采取随机动作,因为 Q-表中并没有太多信息(采用试错法)。随着时间的推移,Q-表逐渐填充,智能体在与环境交互时将变得更加智能。
每次新的动作后,我们都会使用 贝尔曼方程 更新 q(s, a)。贝尔曼方程超出了本介绍的范围,但知道更新后的值 q(s, a) 是基于新获得的回报 r 以及新状态 s’ 的最大可能 Q 值 q(s’, a’)* 即可。
这个例子旨在帮助你理解 Q 学习的基本原理,但你可能注意到一个问题。我们将所有可能的棋盘配置和动作的组合存储在 Q 表中。这将使得表格非常庞大,无法容纳在今天的计算机内存中。幸运的是,这里有一个解决方案:我们可以用神经网络替代 Q 表,神经网络将告诉智能体在每种状态下最优的动作是什么。近年来,这一发展使得强化学习算法在围棋、Dota 2、Doom 和星际争霸等任务中取得了超越人类的表现。
机器学习解决方案的组成部分
到目前为止,我们讨论了三大类机器学习算法。然而,要解决一个机器学习问题,我们需要一个系统,其中机器学习算法只是其中的一部分。这样的系统最重要的方面如下:
-
学习者:该算法与其学习哲学一起使用。选择该算法是由我们试图解决的问题决定的,因为不同的问题可能更适合某些机器学习算法。
-
训练数据:这是我们感兴趣的原始数据集。它可以是有标签的或无标签的。拥有足够的样本数据对学习者理解问题结构非常重要。
-
表示:这是我们如何通过选择的特征来表达数据,以便将其输入给学习者。例如,为了分类手写数字图像,我们将图像表示为一个二维数组,其中每个单元格包含一个像素的颜色值。数据表示的良好选择对于获得更好的结果非常重要。
-
目标:这代表了我们从数据中学习的原因。它与目标密切相关,帮助我们定义学习者应该使用什么以及如何使用什么表示。例如,目标可能是清理我们的邮箱中的垃圾邮件,这个目标定义了我们学习者的目标。在这种情况下,它是垃圾邮件的检测。
-
目标值:这代表了正在学习的内容以及最终的输出。目标值可以是对无标签数据的分类,是根据隐藏的模式或特征对输入数据的表示,是未来预测的模拟器,或是对外部刺激或策略的回应(在强化学习中)。
这一点永远不能被过分强调:任何机器学习算法只能接近目标,而不是完美的数值描述。机器学习算法不是问题的精确数学解答——它们只是近似值。在监督学习部分中,我们将学习定义为从特征空间(输入)到一系列类别的函数。之后,我们将看到某些机器学习算法,如神经网络,理论上可以近似任何函数到任意精度。这被称为通用逼近定理(en.wikipedia.org/wiki/Universal_approximation_theorem
),但这并不意味着我们能为问题找到精确的解决方案。此外,通过更好地理解训练数据,可以更好地解决问题。
通常,经典机器学习技术能够解决的问题可能需要在部署之前对训练数据进行深入理解和处理。解决机器学习问题的步骤如下:
-
数据收集:这包括尽可能多地收集数据。在监督学习的情况下,这还包括正确的标签。
-
数据处理:这包括清理数据,如去除冗余或高度相关的特征,甚至填补缺失数据,并理解定义训练数据的特征。
-
测试用例的创建:通常,数据可以分为三个集合:
-
训练集:我们使用该集合来训练机器学习算法。在大多数情况下,我们将通过多次迭代整个训练集来训练算法。我们将称每次完整训练集迭代的次数为训练轮次。
-
验证集:我们使用该集合来评估算法在训练过程中对未知数据的准确性。我们会在训练集上训练算法一段时间,然后使用验证集来检查其性能。如果结果不令人满意,我们可以调整算法的超参数并重复该过程。验证集还可以帮助我们确定何时停止训练。我们将在本节后面进一步学习这一点。
-
测试集:当我们完成训练或验证周期并调优算法后,我们只会使用测试集进行最终评估一次。测试集与验证集类似,因为算法在训练过程中没有使用它。然而,当我们努力在验证数据上改善算法时,可能会无意中引入偏差,从而导致结果偏向验证集,不能真实反映实际性能。由于我们只使用一次测试集,这将提供对算法更客观的评估。
-
注意
深度学习(DL)算法成功的原因之一是它们通常需要比经典方法更少的数据处理。对于经典算法,你需要对每个问题应用不同的数据处理并提取不同的特征。而对于深度学习,你可以对大多数任务应用相同的数据处理流程。通过深度学习,你可以提高生产力,并且与经典的机器学习算法相比,你不需要太多的领域知识来完成当前任务。
创建测试和验证数据集有很多合理的理由。如前所述,机器学习(ML)技术只能产生所需结果的近似值。通常,我们只能包括有限的、有限数量的变量,且可能有许多变量超出我们的控制范围。如果我们只使用一个数据集,我们的模型可能最终会记住数据,并在它记住的数据上产生极高的准确度。然而,这个结果可能无法在其他相似但未知的数据集上复现。机器学习算法的一个关键目标是它们的泛化能力。这就是为什么我们既要创建用于训练过程中调整模型选择的验证集,又要在过程结束时仅用于确认所选算法有效性的最终测试集。
为了理解选择有效特征的重要性,并避免记住数据(在文献中也称为过拟合,我们从现在开始使用这个术语),让我们用一则来自 XKCD 漫画的笑话作为例子 (xkcd.com/1122):
😃
“直到 1996 年,任何没有作战经验且为现任总统的美国民主党总统候选人都从未击败过任何名字在《拼字游戏》中得分更高的人。”
很明显,这样的规则是没有意义的,但它强调了选择有效特征的重要性,以及问题“一个名字在《拼字游戏》中的得分值有多高?”在选择美国总统时可能有任何相关性。此外,这个例子对未知数据没有任何预测能力。我们将其称为过拟合(overfitting),即指做出适应手头数据完美的预测,但这些预测不能推广到更大的数据集。过拟合是试图理解我们称之为噪声(没有实际意义的信息),并试图将模型拟合到小的扰动中的过程。
为了进一步解释这一点,让我们尝试使用机器学习(ML)预测一个从地面抛向空中的球的轨迹(不是垂直方向),直到它再次落地。物理学教导我们,轨迹的形状像一个抛物线。我们也期望一个好的机器学习算法,观察到成千上万次这样的抛掷后,能够得出一个抛物线作为解决方案。然而,如果我们放大观察球体,注意到由于空气湍流引起的最小波动,我们可能会发现球体并没有保持稳定的轨迹,而是会受到小的扰动影响,这些扰动在这种情况下就是噪声。试图对这些小扰动进行建模的机器学习算法会忽略大局,从而得出不令人满意的结果。换句话说,过拟合是让机器学习算法只看到树木却忘记森林的过程:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_9.jpg
图 1.9 – 一个好的预测模型(左)和一个差的(过拟合的)预测模型,展示了一个从地面抛出的球的轨迹(右)
这就是为什么我们要将训练数据与验证数据和测试数据分开的原因;如果算法在测试数据上的准确度与训练数据上的准确度不相似,那将是一个很好的迹象,表明模型存在过拟合问题。我们需要确保不犯相反的错误——即模型欠拟合。然而,在实际操作中,如果我们旨在使我们的预测模型在训练数据上尽可能准确,欠拟合的风险要小得多,而且我们会特别注意避免过拟合。
下图展示了欠拟合:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_10.jpg
图 1.10 – 欠拟合也可能是一个问题
神经网络
在前面的章节中,我们介绍了一些流行的经典机器学习算法。在本章节中,我们将讨论神经网络(NN),这是本书的重点。
神经网络的第一个例子叫做感知器,这是由 Frank Rosenblatt 在 1957 年发明的。感知器是一种分类算法,非常类似于逻辑回归。与逻辑回归类似,它有一个权重向量 w,其输出是一个函数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/24.png,即点积,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/25.png(或者https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/26.png),表示权重和输入的点积。
唯一的区别是 f 是一个简单的阶跃函数——也就是说,如果https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/27.png,则https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/28.png,否则
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/29.png,其中我们对逻辑函数的输出应用类似的逻辑回归规则。感知器是一个简单的单层前馈神经网络示例:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_11.jpg
图 1.11 – 一个简单的感知器,具有三个输入和一个输出
感知器非常有前景,但很快就被发现它有严重的局限性,因为它只适用于线性可分的类别。在 1969 年,Marvin Minsky 和 Seymour Paper 证明了它甚至无法学习像 XOR 这样的简单逻辑函数。这导致了对感知器的兴趣大幅下降。
然而,其他神经网络可以解决这个问题。经典的多层感知器(MLP)包含多个相互连接的感知器,例如组织在不同顺序层中的单元(输入层、一个或多个隐藏层以及输出层)。每一层的单元都与下一层的所有单元相连接。首先,信息呈现给输入层,然后我们用它计算输出(或激活值),https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/30.png,用于第一个隐藏层的每个单元。我们向前传播,输出作为下一层输入(因此叫做前向传播),以此类推,直到达到输出层。训练神经网络最常见的方法是结合使用梯度下降和反向传播。我们将在第二章中详细讨论这个过程。
以下图示描绘了一个具有隐藏层的神经网络:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_12.jpg
图 1.12 – 一个具有隐藏层的神经网络
将隐藏层视为输入数据的抽象表示。这是神经网络用其内部逻辑理解数据特征的方式。然而,神经网络是不可解释的模型。这意味着,如果我们观察隐藏层的https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/31.png激活值,我们是无法理解它们的。对我们来说,它们只是一个数值向量。我们需要输出层来弥合网络表示与我们实际关注的数据之间的差距。你可以把它当作一个翻译器;我们用它来理解网络的逻辑,同时也能将其转换为我们关注的实际目标值。
通用逼近定理告诉我们,一个具有一个隐藏层的前馈网络可以表示任何函数。值得注意的是,理论上,具有一个隐藏层的网络没有限制,但在实际应用中,使用这种架构的成功是有限的。在第三章中,我们将讨论如何通过深度神经网络来实现更好的性能,并与浅层神经网络相比的优势。目前,让我们通过解决一个简单的分类任务来应用我们的知识。
引入 PyTorch
在这一部分,我们将介绍 PyTorch——一个由 Facebook 开发的开源 Python 深度学习框架,近年来获得了广泛关注。它提供图形处理单元(GPU)加速的多维数组(或张量)运算和计算图,我们可以用它来构建神经网络。在本书中,我们将使用 PyTorch 和 Keras,并将在第三章中详细讨论和比较这两个库。
我们将创建一个简单的神经网络来分类鸢尾花数据集。步骤如下:
-
首先加载数据集:
import pandas as pd dataset = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', names=['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species']) dataset['species'] = pd.Categorical(dataset['species']).codes dataset = dataset.sample(frac=1, random_state=1234) train_input = dataset.values[:120, :4] train_target = dataset.values[:120, 4] test_input = dataset.values[120:, :4] test_target = dataset.values[120:, 4]
上述代码是下载鸢尾花数据集 CSV 文件并将其加载到
dataset
中的模板代码。然后,我们将 DataFrame 的行进行洗牌,并将代码拆分为 NumPy 数组,train_input
/train_target
(花卉特征/花卉类别)用于训练数据,test_input
/test_target
用于测试数据。我们将使用 120 个样本进行训练,30 个样本用于测试。如果你不熟悉 pandas,可以把它当作 NumPy 的高级版本。 -
接下来,定义我们的第一个神经网络。我们将使用一个前馈网络,包含一个具有五个单元的隐藏层,一个ReLU激活函数(这只是另一种激活函数,简单定义为f(x) = max(0, x)),以及一个具有三个单元的输出层。输出层有三个单元,每个单元对应一种鸢尾花的类别。以下是 PyTorch 定义的网络:
import torch torch.manual_seed(1234) hidden_units = 5 net = torch.nn.Sequential( torch.nn.Linear(4, hidden_units), torch.nn.ReLU(), torch.nn.Linear(hidden_units, 3) )
我们将使用
Iris Setosa = [1, 0, 0]
,Iris Versicolor = [0, 1, 0]
,和Iris Virginica = [0, 0, 1]
,其中数组中的每个元素将是输出层单元的目标。当网络对新样本进行分类时,我们通过选择激活值最高的单元来确定类别。torch.manual_seed(1234)
使我们每次都能使用相同的随机数据种子,从而保证结果的可重复性。 -
选择
loss
函数:criterion = torch.nn.CrossEntropyLoss()
使用
criterion
变量,我们将损失函数定义为交叉熵损失。损失函数将衡量网络输出与目标数据之间的差异。 -
定义随机梯度下降(SGD)优化器(梯度下降算法的一种变体),学习率为 0.1,动量为 0.9(我们将在第二章中讨论 SGD 及其参数):
optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9)
-
训练网络:
epochs = 50 for epoch in range(epochs): inputs = torch.autograd.Variable( torch.Tensor(train_input).float()) targets = torch.autograd.Variable( torch.Tensor(train_target).long()) optimizer.zero_grad() out = net(inputs) loss = criterion(out, targets) loss.backward() optimizer.step() if epoch == 0 or (epoch + 1) % 10 == 0: print('Epoch %d Loss: %.4f' % (epoch + 1, loss.item()))
我们将训练 50 个周期,也就是对训练数据集迭代 50 次:
-
从 NumPy 数组创建
torch
变量——即train_input
和train_target
。 -
将优化器的梯度归零,以防止上一次迭代的梯度累积。我们将训练数据输入到神经网络中,
net(inputs)
,并计算损失函数的criterion
(out
,targets
),即网络输出和目标数据之间的差异。 -
将
loss
值反向传播通过网络。我们这么做是为了计算每个网络权重如何影响损失函数。 -
优化器以一种能够减少未来损失函数值的方式更新网络的权重。
当我们运行训练时,输出将如下所示:
Epoch 1 Loss: 1.2181 Epoch 10 Loss: 0.6745 Epoch 20 Loss: 0.2447 Epoch 30 Loss: 0.1397 Epoch 40 Loss: 0.1001 Epoch 50 Loss: 0.0855
下图显示了损失函数随着每个训练周期的减少。这展示了网络是如何逐渐学习训练数据的:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_01_13.jpg
图 1.13 – 损失函数随着每个训练周期的减少
-
让我们看看模型的最终准确率是多少:
import numpy as np inputs = torch.autograd.Variable(torch.Tensor(test_input).float()) targets = torch.autograd.Variable(torch.Tensor(test_target).long()) optimizer.zero_grad() out = net(inputs) _, predicted = torch.max(out.data, 1) error_count = test_target.size - np.count_nonzero((targets == predicted).numpy()) print('Errors: %d; Accuracy: %d%%' % (error_count, 100 * torch.sum(targets == predicted) / test_target.size))
我们通过将测试集输入网络并手动计算误差来实现这一点。输出如下:
Errors: 0; Accuracy: 100%
我们成功地正确分类了所有 30 个测试样本。
我们还必须尝试不同的网络超参数,并观察准确率和损失函数的变化。你可以尝试改变隐藏层中的单元数、训练周期数以及学习率。
总结
本章介绍了什么是机器学习(ML)以及它为什么如此重要。我们讨论了机器学习技术的主要类别以及一些最受欢迎的经典机器学习算法。我们还介绍了一种特定类型的机器学习算法,称为神经网络(NNs),它是深度学习(DL)的基础。然后,我们看了一个编码示例,展示了如何使用流行的机器学习库来解决一个特定的分类问题。
在下一章,我们将更详细地讨论神经网络(NNs)并探讨它们的理论依据。
第二章:神经网络
在第一章中,我们介绍了许多基本的机器学习(ML)概念和技术。我们讲解了主要的机器学习范式以及一些经典的流行机器学习算法,最后介绍了神经网络(NN)。在本章中,我们将正式介绍神经网络是什么,讨论它们的数学基础,详细描述其构建模块如何工作,看看我们如何堆叠多个层来创建深度前馈神经网络,并学习如何训练它们。
在本章中,我们将涵盖以下主要内容:
-
神经网络的需求
-
神经网络的数学
-
神经网络简介
-
训练神经网络
神经网络与人类大脑的联系
最初,神经网络(NN)是受生物大脑的启发(因此得名)。然而,随着时间的推移,我们已不再尝试模仿大脑的工作方式,而是集中精力寻找适用于特定任务的正确配置,包括计算机视觉、自然语言处理和语音识别。你可以这样理解——长时间以来,我们受到了鸟类飞行的启发,但最终我们创造了飞机,这与鸟类飞行是截然不同的。我们仍然远未能与大脑的潜力相匹配。也许未来的机器学习算法会更像大脑,但现在情况并非如此。因此,在本书的其余部分,我们将不再试图在大脑和神经网络之间做类比。为了沿着这个思路进行,我们将把最小的神经网络构建模块称为单元,而不是最初的“神经元”。
技术要求
我们将在本章中使用 Python 实现示例。如果你尚未设置好相关工具的环境,不用担心——该示例可以在 Google Colab 上作为 Jupyter notebook 使用。你可以在本书的 GitHub 仓库中找到代码示例:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter02
。
神经网络的需求
神经网络已经存在了许多年,并经历了多个阶段,期间它们的受欢迎程度时高时低。然而,最近,它们在许多其他竞争的机器学习算法中稳步占据了主导地位。这一复兴归因于计算机速度的提升,图形处理单元(GPU)的使用取代了最传统的中央处理单元(CPU)使用,算法和神经网络设计的改进,以及日益增大的数据集,这些我们将在本书中探讨。为了让你了解它们的成功,让我们来看看 ImageNet 大规模视觉识别挑战赛(image-net.org/challenges/LSVRC/
,或者简称ImageNet)。参与者通过使用 ImageNet 数据库来训练他们的算法。该数据库包含了超过 100 万张高分辨率彩色图像,涵盖了 1,000 多个类别(一个类别可能是汽车的图像,另一个类别是人的图像、树木的图像等等)。该挑战赛中的一个任务是将未知的图像分类到这些类别中。2011 年,获胜者实现了 74.2%的前五名准确率。2012 年,Alex Krizhevsky 及其团队带着卷积网络(深度网络的一种特殊类型)参加了比赛。那年,他们以 84.7%的前五名准确率赢得了比赛。从那时起,获胜的算法总是神经网络,而当前的前五名准确率大约为 99%。然而,深度学习算法在其他领域也表现出色——例如,谷歌的 Google Now 和苹果的 Siri 助手依赖深度网络进行语音识别,谷歌也利用深度学习进行翻译引擎的开发。最近的图像和文本生成系统,如 Stability AI 的 Stable Diffusion 和 OpenAI 的 DALL-E 与 ChatGPT,都是使用神经网络实现的。
我们将在接下来的章节中讨论这些激动人心的进展,但现在,我们将专注于神经网络的数学基础。为了帮助我们完成这项任务,我们将使用具有一两层的简单网络。你可以将这些视为玩具示例,它们不是深度网络,但理解它们的工作原理非常重要。原因如下:
-
了解神经网络的理论将帮助你理解本书的其余部分,因为如今大多数使用中的神经网络都共享一些共同的原理。理解简单的网络意味着你也能理解深度网络。
-
拥有一些基础知识总是有益的。当你面对一些新的材料时(即使是本书未涉及的材料),它会对你大有帮助。
我希望这些论点能说服你本章的重要性。作为一个小小的安慰,我们将在第三章中深入讨论深度学习(这个双关语用得很恰当)。
神经网络的数学
在接下来的几个部分中,我们将讨论神经网络的数学原理。通过这种方式,我们将能够以一种基础且结构化的方式,通过这些原理来解释神经网络。
线性代数
线性代数处理的对象包括向量、矩阵、线性变换和线性方程,如 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/32.png。
线性代数识别以下数学对象:
-
标量:一个单独的数字。
-
向量:一个一维的数字数组(也称为分量或标量),每个元素都有一个索引。我们可以用上标箭头 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/33.png) 或加粗字体 (x) 来表示向量,但在本书中我们将主要使用加粗符号。以下是一个向量的示例:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/34.png
我们可以将一个 n 维向量表示为一个点在 n 维欧几里得空间中的坐标,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/35.png。可以将欧几里得空间视为一个坐标系——向量从该坐标系的中心开始,每个向量的元素表示沿其相应坐标轴的点的坐标。下图显示了三维坐标系中的一个向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/36.png:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_01.jpg
图 2.1 – 三维空间中的向量表示
该图还帮助我们定义了向量的两个额外属性:
- 大小(或长度):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/37.png
可以将大小视为勾股定理在 n 维空间中的推广。
-
方向:向量与向量空间中各轴之间的角度。
-
矩阵:一个二维标量数组,每个元素通过行和列来标识。我们用粗体大写字母表示矩阵—例如,A。相反,我们用小写字母表示矩阵元素,并将行和列作为下标—例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/38.png。我们可以在下面的公式中看到矩阵符号的例子:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/39.png
我们可以将向量表示为单行的 1×n 矩阵(行矩阵)或单列的 n×1 矩阵(列矩阵)。通过这种转换,向量可以参与不同的矩阵运算。
-
张量:张量 这一术语来源于数学和物理学,在我们开始在机器学习中使用它之前,它就已经存在。这些领域中的定义与机器学习中的定义不同。幸运的是,在机器学习的背景下,张量仅仅是一个具有以下属性的多维数组:
-
秩:数组的维度数量。向量和矩阵是张量的特殊情况。秩为 0 的张量是标量,秩为 1 的张量是向量,秩为 2 的张量是矩阵。维度数量没有限制,某些类型的神经网络可以使用秩为 4 或更高的张量。
-
形状:张量每个维度的大小。
-
张量值的 数据类型。在实际应用中,数据类型包括 16 位、32 位和 64 位浮动数,以及 8 位、16 位、32 位和 64 位整数。
-
张量是像 PyTorch、Keras 和 TensorFlow 等库的主要数据结构。
张量的性质
你可以在这里找到关于张量性质的详细讨论:stats.stackexchange.com/questions/198061/why-the-sudden-fascination-with-tensors
。你还可以将此与 PyTorch (pytorch.org/docs/stable/tensors.html
) 和 TensorFlow (www.tensorflow.org/guide/tensor
) 的张量定义进行对比。
既然我们已经介绍了向量、矩阵和张量,让我们继续讨论它们能参与的某些线性运算。
向量和矩阵运算
我们将重点讨论与神经网络(NNs)相关的运算,从向量开始:
- 向量加法:将两个或更多 n 维向量 a 和 b(等等)加在一起,得到一个新的向量:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/40.png
- 点积(或标量积):将两个 n 维向量 a 和 b 合并为一个 标量值:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/41.png
这里,两个向量之间的夹角是θ,而https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/42.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/43.png 是它们的大小。例如,如果这些向量是二维的,且它们的分量为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/44.png,则上面的公式变为如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/45.png
以下图示说明了a和b的点积:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_02.jpg
图 2.2 – 向量的点积 – 上方:向量分量,和下方:两个向量的点积
我们可以将点积视为两个向量之间的相似度度量,其中角度θ表示它们的相似程度。如果θ很小(即向量方向相似),那么它们的点积将更高,因为https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/46.png会趋近于 1。在这种情况下,我们可以定义两个向量之间的余弦相似度如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/47.png
- 叉积(或向量积):两个向量a和b的组合,得到一个新的向量,该向量垂直于两个初始向量。输出向量的大小等于以下公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/48.png
我们可以在下图中看到二维向量叉积的例子:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_03.jpg
图 2.3 – 两个二维向量的叉积
输出向量垂直(或法向)于包含输入向量的平面。输出向量的大小等于由a和b向量(在前面的图中以浅蓝色表示)构成的平行四边形的面积。
现在,让我们关注矩阵运算:
- 矩阵转置:沿着矩阵的主对角线翻转矩阵,表示为矩阵中所有元素的集合,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/49.png,其中 i=j。我们用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/50.png 来表示转置操作。A 的单元格 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/51.png 等于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/49.png 的单元格,且该单元格属于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/53.png。
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/54.png
m×n 矩阵的转置是一个 n×m 矩阵,以下例子可以帮助理解:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/55.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/56.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/57.png
- 矩阵-标量乘法:将矩阵A与标量y相乘,生成一个与原矩阵相同大小的新矩阵:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/58.png
- 矩阵加法:两个或多个矩阵(A 和 B 等)逐元素相加,得到一个新的矩阵。所有输入矩阵必须具有相同的大小:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/59.png
- 矩阵-向量乘法:将矩阵A与向量x相乘,得到一个新的向量:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/60.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/61.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/62.png
矩阵的列数必须与向量的大小相等。一个 m×n 矩阵与一个 n 维向量相乘的结果是一个 m 维向量。我们可以假设矩阵的每一行都是 n 维向量。那么,输出向量的每一个值都是对应矩阵行向量与x的点积。
- 矩阵乘法:一种二元运算,表示两个矩阵 A 和 B 的乘积,得到一个单一的输出矩阵。我们可以将其看作是多个矩阵-向量乘法,其中第二个矩阵的每一列都是一个向量:
![<mml:math xmlns:mml=“http://www.w3.org/1998/Math/MathML” xmlns:m=“http://schemas.openxmlformats.org/officeDocument/2006/math” display=“block”><mml:mi mathvariant=“bold”>A</mml:mi><mml:mi mathvariant=“bold”>B</mml:mi>mml:mo=</mml:mo><mml:mfenced open=“[” close=“]” separators=“|”>mml:mrowmml:mtablemml:mtrmml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn13</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn21</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn22</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn23</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr></mml:mtable></mml:mrow></mml:mfenced><mml:mfenced open=“[” close=“]” separators=“|”>mml:mrowmml:mtablemml:mtrmml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn21</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn22</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn31</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn32</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr></mml:mtable></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mfenced open=“[” close=“]” separators=“|”>mml:mrowmml:mtablemml:mtrmml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn21</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn13</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn31</mml:mn></mml:mrow></mml:msub></mml:mtd>mml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn22</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn13</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn32</mml:mn></mml:mrow></mml:msub></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn21</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn11</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn22</mml:mn></mml:mrow></mml:msub>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn21</mml:mn></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></m
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/64.png
如果我们将这两个向量表示为矩阵,它们的点积,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/65.png,等同于矩阵与矩阵的乘法。
现在你可以松一口气了,因为我们已经结束了线性代数的介绍。虽然所有的困难还没有结束,接下来我们将重点讨论概率论。
概率论简介
在这一节中,我们将介绍一些概率论的基本概念。它们将帮助我们在本书后面讨论神经网络训练算法和自然语言处理时更好地理解相关内容。
我们将从统计实验的概念开始,统计实验有以下特点:
-
它由多个独立的试验组成
-
每次试验的结果是由偶然决定的(它是非确定性的)
-
它有多个可能的结果,称为事件
-
我们提前知道所有可能的实验结果
统计实验的例子包括硬币抛掷(有两个可能结果:正面或反面)和掷骰子(有六个可能结果:1 到 6)。
某个事件e发生的可能性被称为概率P(e)。它的值在[0, 1]之间。P(e) = 0.5 表示该事件有 50%的可能性发生,P(e) = 0 表示该事件不可能发生,P(e) = 1 表示该事件必定发生。
我们可以通过两种方式来处理概率:
- 理论:所有事件的发生概率是相等的,我们感兴趣的事件(结果)的概率如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/66.png
硬币抛掷的理论概率是 P(正面) = P(反面) = 1/2。在掷骰子的例子中,我们有 P(每一面) = 1/6。
- 经验:这是事件e发生的次数与所有试验次数的比值:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/67.png
实验的经验结果可能表明事件的发生并非等概率。例如,如果我们抛硬币 100 次,正面朝上的次数是 47 次,则正面朝上的经验概率为 P(正面) = 47 / 100 = 0.47。大数法则告诉我们,试验次数越多,计算的概率就越准确。
现在,我们将在集合的背景下讨论概率。
概率与集合
在本节中,我们将介绍集合及其属性。我们还将看到如何在概率论中应用这些属性。首先从一些定义开始:
-
样本空间:实验的所有可能事件(结果)的集合。我们用大写字母表示它。像 Python 一样,我们用{}列出样本空间中的所有事件。例如,掷硬币和掷骰子的样本空间分别是 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/68.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/69.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/70.png。
-
样本点:样本空间中的单个事件(例如,反面)。
-
事件:样本空间中的单个样本点或样本点的组合(子集)。例如,掷骰子落在奇数上的联合事件是 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/71.png。
假设我们有一个集合(样本空间),S = {1, 2, 3, 4, 5},和两个子集(联合事件),A = {1, 2, 3} 和 B = {3, 4, 5}。我们将用它们来定义以下集合运算:
- 交集:A 和 B 中同时存在的元素的集合:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/72.png
如果 A 和 B 的交集是空集{},则它们是不相交的。
- 补集:A 或 B 中未包含的所有元素的集合:
https://example.org/img/73.png
- 并集:包含 A 或 B 中所有元素的集合:
https://example.org/img/74.png
下面的维恩图展示了这些操作:
https://example.org/img/B19627_02_04.jpg
图 2.4 – 可能集合关系的维恩图
现在,让我们看看如何在概率领域中进行集合操作。我们将处理独立事件——也就是说,一个事件的发生不会影响另一个事件发生的概率。例如,不同硬币投掷的结果彼此独立。因此,让我们根据概率和事件定义集合操作:
- 两个事件的交集:存在于两个事件中的样本点的集合。交集的概率称为联合概率:
https://example.org/img/75.png
假设我们想要计算一张卡牌同时是黑桃和一张 A 牌(更有诗意地说,黑桃 A)。黑桃的概率为 P(黑桃) = 13/52 = ¼,而 A 牌的概率为 P(A) = 4/52 = 1/13。两者的联合概率为 P(A, 黑桃) = (1/13) https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/77.pngB) = 0
+ 互斥事件的概率总和是 <https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/78.png>
- 联合穷尽事件:此类事件的子集相互之间包含整个样本空间。例如,事件 A = {1, 2, 3} 和 B = {4, 5, 6} 是联合穷尽的,因为它们共同覆盖了整个样本空间 S = {1, 2, 3, 4, 5, 6}。对于联合穷尽事件,其概率有如下性质:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/79.png
-
互斥事件:两个或多个事件彼此没有共同的结果,并且它们是联合穷尽的。例如,掷骰子的奇数和偶数事件是互斥的。
-
事件的并集:一组来自 A 或 B(不一定都在两个集合中)的事件。该并集的概率是:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/80.png
到目前为止,我们讨论了独立事件。现在,让我们看看如果事件不是独立的会发生什么。
条件概率和贝叶斯定理
如果事件 A 发生在 B 之前,并且 A 的发生改变了 B 发生的概率,那么这两个事件是相关的。为了理解这一点,假设我们从一副牌中抽取连续的牌。当牌堆满时,抽到黑桃的概率是 P(黑桃) = 13/52 = 0.25。然而,一旦我们抽到了第一张黑桃,第二次抽到黑桃的概率就会发生变化。现在,我们只有 51 张牌,而且黑桃减少了 1 张。第二次抽到黑桃的概率被称为条件概率,P(B|A)。这是指在事件 A(第一次抽到黑桃)已经发生的情况下,事件 B(第二次抽到黑桃)发生的概率。第二次抽到黑桃的概率是 P(黑桃 2|黑桃 1) = 12/51 = 0.235。
我们可以扩展前面章节中介绍的联合概率公式,来适应相关事件:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/81.png
然而,这个公式只是两个事件的特例。我们可以进一步扩展它以适用于多个事件,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/82.png。这个新的通用公式被称为概率链式法则:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/83.png
例如,三个事件的链式法则如下所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/84.png
我们可以利用这个性质来推导条件概率本身的公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/85.png
让我们来讨论一下其中的直觉:
-
P(A ∩ B) 表示我们只关心事件 B 的发生,前提是 A 已经发生——也就是说,我们关注事件的联合发生,因此是联合概率。
-
P(A) 表示我们只关注事件 A 发生时的结果子集。我们已经知道 A 发生了,因此我们将观察范围限制在这些结果上。
以下对于依赖事件是成立的:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/86.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/87.png
我们可以使用这个规则来替换条件概率公式中的 P(A∩B) 值,从而推导出被称为 贝叶斯规则 的公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/88.png
贝叶斯定理使我们能够计算条件概率 P(B|A),如果我们知道反向的条件概率 P(A|B)。P(A)和 P(B|A)分别被称为先验概率和后验概率。
我们可以通过一个经典的医学检测例子来说明贝叶斯定理。一位病人接受了某种疾病的医学检测,结果显示为阳性。大多数检测都有一个灵敏度值,即当对患有特定疾病的人进行检测时,测试为阳性的概率。利用这些信息,我们将应用贝叶斯定理来计算在测试为阳性的情况下,病人患病的实际概率。我们得到如下公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/89.png
我们可以将 P(患病)看作是一般人群中患病的概率。
现在,我们将对疾病和测试的灵敏度做一些假设:
-
该测试的敏感度为 98%——也就是说,它只能检测出 98%的所有阳性病例:P(测试=阳性|患病) = 0.98
-
50 岁以下的 2%的人患有这种疾病:P(患病) = 0.02
-
对 50 岁以下的人群进行测试时,3.9%的人群测试结果为阳性:P(测试=阳性) = 0.039
我们可以问这样一个问题:如果一个测试的敏感度是 98%,并且对一位 45 岁的人进行测试,结果为阳性,那么他患病的概率是多少?我们可以用贝叶斯规则来计算:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/90.png
这个例子可以作为下一节的引入,在这一节中我们将介绍混淆矩阵。
混淆矩阵
混淆矩阵用于评估二分类算法的性能,类似于我们在条件概率和贝叶斯 规则 部分介绍的医学测试:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_05.jpg
图 2.5 – 混淆矩阵
实际情况(P 和 N)与预测结果(PP 和 PN)之间的关系使我们能够将预测结果归类为四种类别之一:
-
真阳性 (TP):实际值和预测值都是正
-
真阴性 (TN):实际值和预测值都是假
-
假阳性 (FP):实际值为负,但分类算法预测为正
-
假阴性(FN):实际值为正,但算法预测为负值。
基于这些分类,我们将引入一些衡量算法性能不同方面的指标:
-
准确率 = https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/91.png:所有案例中正确预测的比例。
-
精确率 = https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/92.png:所有正类预测中实际正确的比例。
-
召回率(或 敏感度)= https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/93.png:实际正类案例中被正确预测的比例。
-
特异性 = https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/94.png:实际负类案例中被正确预测的比例。
-
F1 分数 = https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/95.png:表示精确率和召回率之间的平衡。由于这两个指标的相乘,当这两个值都较高时,F1 分数也会较高。
在接下来的部分中,我们将讨论微积分领域,这将帮助我们训练神经网络。
微积分
我们可以将机器学习算法视为一个具有输入和参数的数学函数(神经网络也是如此)。我们的目标是调整这些参数,以便让机器学习函数能够尽可能接近某个目标函数。为了做到这一点,我们需要知道当我们改变某些参数(称为权重)时,机器学习函数的输出如何变化。幸运的是,微积分可以帮助我们——它研究函数对于一个变量(参数)变化的变化率。为了理解这一点,我们将从一个具有单一参数 x 的函数 f(x) 开始,其图形如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_06.jpg
图 2.6 – 一个具有单一参数 x 的函数 f(x) 的示例图。函数图通过连续的蓝色线表示;斜率通过断开的红色线表示。
我们可以通过计算该值处函数的斜率,来近似 f(x) 相对于 x 的变化。如果斜率为正,则函数增加;如果斜率为负,则函数减少。斜率的陡峭程度表示该值处函数的变化速率。我们可以用以下公式来计算斜率:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/96.png
这里的思路很简单——我们计算 f 在 x 和 x+Δx 处的两个值之差(Δx 是一个非常小的值)——Δy = f(x + Δx) - f(x)。Δy 和 Δx 的比值给我们斜率。那么,为什么 Δx 必须很小呢?如果 Δx 太大,x 和 x+Δx 之间的函数图形部分可能会发生较大变化,斜率测量将不准确。当 Δx 收敛到 0 时,我们假设我们的斜率近似于图形上某一点的实际斜率。在这种情况下,我们称斜率为 f(x) 的一阶导数。我们可以通过以下公式用数学语言表示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/97.png
这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/98.png是极限的数学概念(Δx 趋近于 0),而 f’(x) 和 dy/dx 分别是拉格朗日和莱布尼茨表示导数的符号。求 f 的导数的过程称为微分。下图展示了不同 x 值的斜率:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_07.jpg
图 2.7 – 不同 x 值的斜率
当 f 在变化 x 时既不增加也不减少的点,称为鞍点。在鞍点处的 f 值称为局部最小值和局部最大值。相反,鞍点处的 f 的斜率为 0。
到目前为止,我们已经讨论了只有一个参数的函数 x。现在,让我们关注一个具有多个参数的函数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/99.png。f 相对于任意参数的导数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/100.png,被称为 偏导数,并表示为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/101.png。计算偏导数时,我们将假设其他所有参数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/102.png,是常数。我们将用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/103.png来表示向量组件的偏导数。
最后,让我们讨论一些有用的求导规则:
- 链式法则:f 和 g 是函数,且 h(x)= f(g(x))。对于任意 x,f 相对于 x 的导数如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/104.png
- 求和法则:f 和 g 是一些函数,h(x) = f(x) + g(x)。求和法则表明以下内容:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/105.png
-
常见函数:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/106.png
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/107.png, 其中a是一个标量
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/108.png, 其中a是一个标量
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/109.png
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/110.png
-
神经网络及其数学基础构成了一种知识层次结构。把我们在神经网络的数学部分讨论的内容看作是神经网络的构建块。它们代表了理解神经网络的一个重要步骤,这将有助于我们在本书以及未来的学习中。现在,我们已经做好了学习完整神经网络的必要准备。
神经网络简介
我们可以将神经网络描述为一种信息处理的数学模型。正如在第一章中讨论的那样,这是描述任何机器学习算法的一个良好方式,但在本章中,它在神经网络的上下文中具有特定含义。神经网络不是一个固定的程序,而是一个模型,一个处理信息或输入的系统。神经网络的特征如下:
-
信息处理以最简单的形式发生,作用于被称为单元的简单元素上。
-
单元相互连接,并通过连接链路交换信号。
-
单元之间的连接链路可以更强或更弱,这决定了信息的处理方式。
-
每个单元都有一个内部状态,该状态由所有来自其他单元的输入连接决定。
-
每个单元都有不同的激活函数,该函数根据单元的状态计算,并决定其输出信号。
对神经网络的更一般描述可以是数学运算的计算图,但我们将在后面进一步学习这个内容。
我们可以识别神经网络的两个主要特征:
-
神经网络架构:这描述了单元之间的连接方式——即前馈、递归、多层或单层等——层数和每层单元的数量。
-
学习:这描述了通常定义为训练的过程。训练神经网络最常见但并非唯一的方式是使用梯度下降(GD)和反向传播(BP)。
我们将从神经网络的最小构建块——单元开始讨论。
单元——神经网络的最小构建块
单元是可以定义为以下数学函数:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/111.png
在这里,我们做以下操作:
-
我们计算加权和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/112.png(也称为激活值)。让我们聚焦于这个和的组成部分:
-
输入 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/113.png 是表示网络中其他单元输出或输入数据本身值的数值。
-
权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/114.png 是表示输入强度或单元间连接强度的数值。
-
权重 b 是一个特殊的权重,称为偏置,它表示一个始终开启的输入单元,值为 1。
或者,我们可以用它们的向量表示来替代 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/115.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/15.png,其中 x = → x = [x 1, x 2, … , x n] 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/117.png。这里,公式将使用两个向量的点积:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/118.png
- 总和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/119.png 作为 激活函数 f(也称为 传输函数)的输入。f 的输出是一个单一的 数值,代表单元本身的输出。激活函数具有以下特性:
-
非线性:f 是神经网络(NN)中非线性的来源——如果神经网络完全线性,它只会逼近其他线性函数
-
可微分:这使得可以通过梯度下降(GD)和反向传播(BP)来训练网络
如果你并不完全理解,不用担心——我们将在本章稍后详细讨论激活函数。
下图(左侧)展示了一个单元:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_08.jpg
图 2.8 – 左:一个单元及其等效公式,右:感知机的几何表示
输入向量x与权重向量w垂直时,满足https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/120.png。因此,所有满足https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/121.png的x向量定义了一个超平面,该超平面位于向量空间https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/122.png,其中n是x的维度。在二维输入情况下https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/123.png,我们可以将超平面表示为一条直线。这可以通过感知器(或二分类器)来说明——一个带有阈值激活函数的单元,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/124.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/125.png,它将输入分类为两个类别中的一个。带有两个输入的感知器的几何表示https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/126.png是一条直线(或决策边界),它将两个类别分开(如前图所示,位于右侧)。
在前面的图示中,我们也可以看到偏置 b 的作用是使超平面能够从坐标系的中心移动。如果我们不使用偏置,单元的表示能力将受到限制。
单元是我们在第一章中介绍的几种算法的概括:
-
一个具有恒等激活函数 f(x) = x 的单元相当于多元 线性回归。
-
一个具有sigmoid 激活函数的单元相当于逻辑回归。
-
一个具有阈值激活函数的单元相当于感知机。
我们已经从第一章中了解到,感知机(因此也包括单元)只对线性可分的类别有效,而现在我们知道这是因为它定义了一个超平面。这对单元来说是一个严重的限制,因为它无法处理线性不可分的问题——即使是像异或(XOR)这样简单的问题。为了克服这个限制,我们需要将单元组织成神经网络(NN)。然而,在讨论完整的神经网络之前,我们将重点讨论下一个神经网络构建块——层。
层作为操作。
神经网络(NN)可以有无限数量的单元,这些单元按层次组织并互相连接。每一层具有以下特性:
-
它将多个单元的标量输出组合成一个单一的输出向量。单元的输出是标量,因此它只能传达有限的信息。通过组合单元的输出,而不是单一的激活函数,我们现在可以整体考虑向量。这样,我们就能传递更多的信息,不仅因为向量有多个值,还因为它们之间的相对比率承载了额外的含义。
-
一层的单元可以与其他层的单元连接,但不能与同一层的其他单元连接。由于这一点,我们可以并行计算单层所有单元的输出(从而提高计算速度)。这种能力是近年来深度学习(DL)成功的主要原因之一。
-
我们可以将多变量回归推广到一层,而不仅仅是将线性回归或逻辑回归应用于单一单元。换句话说,我们可以用一层来近似多个值,而不是用单一单元来近似单个值。这在分类输出的情况下尤为适用,其中每个输出单元代表输入属于某个类别的概率。
在经典神经网络(即深度学习之前的神经网络,那时它们只是众多机器学习算法中的一种)中,主要的层类型是全连接(FC)层。在这一层中,每个单元都会从输入向量的所有组件接收加权输入,x。这可以表示网络中另一个层的输出或输入数据集的一个样本。假设输入向量的大小为m,且 FC 层有n个单元和一个激活函数f,这个激活函数对所有单元都是相同的。每个n个单元会有m个权重——每个m个输入对应一个权重。下面是我们可以用来表示 FC 层单个单元j输出的公式。它与我们在单元 - 最小神经网络构建块部分定义的公式相同,但这里我们将包括单元索引:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/127.png
这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/128.png 是第 j层单元与输入向量的第 i个值之间的权重,x。我们可以将连接x的各个元素与单元的权重表示为一个m×n的矩阵W。W的每一列代表该层单个单元的所有输入的权重向量。在这种情况下,层的输出向量y是矩阵-向量乘法的结果。
我们还可以将多个输入样本向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/129.png,组合成一个输入矩阵X,其中每个输入数据向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/130.png通过X中的一行表示。该矩阵本身被称为批次。然后,我们将同时计算所有输出向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/131.png,对应于输入样本,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/132.png。在这种情况下,我们将进行矩阵-矩阵乘法,XW,并且层的输出也是一个矩阵,Y。
以下图展示了一个全连接层的示例,以及在批次和单个样本场景下的等效公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_09.jpg
图 2.9 – 一个带有向量/矩阵输入输出的全连接层及其等效公式
我们已经明确分开了偏置和输入权重矩阵,但实际上,底层实现可能使用共享的权重矩阵,并将一行额外的 1 添加到输入数据中。
到目前为止,我们将输入数据样本表示为向量,能够将它们组合成矩阵。然而,输入数据可以有更多的维度。例如,我们可以用三维来表示 RGB 图像——三个二维通道(每个颜色一个通道)。为了将多个图像组合在一个批次中,我们需要一个第四维度。在这种情况下,我们可以使用输入/输出张量而不是矩阵。
我们还将使用不同类型的层来处理多维数据。其中一种类型是卷积层,我们将在第四章中讨论。我们有许多其他类型的层,如注意力层、池化层等。有些层具有可训练的权重(全连接层、注意力层、卷积层),而有些则没有(池化层)。我们也可以将“函数”或“操作”与“层”互换使用。例如,在 TensorFlow 和 PyTorch 中,我们刚才描述的全连接层是两个顺序操作的组合。首先,我们对权重和输入进行加权求和,然后将结果作为输入传递给激活函数操作。在实际应用中(即在使用深度学习库时),神经网络的基本构建块不是单元,而是一个操作,它接受一个或多个张量作为输入,并输出一个或多个张量:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_10.jpg
图 2.10 – 一个带有输入和输出张量的函数(或操作)
最后,我们拥有了讨论神经网络(NNs)完整细节所需的所有信息。
多层神经网络
如我们多次提到的,单层神经网络只能分类线性可分的类别。然而,实际上并没有什么阻止我们在输入和输出之间引入更多的层。这些额外的层称为隐藏层。下图展示了一个具有两个隐藏层的三层全连接神经网络:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_11.jpg
图 2.11 – 多层前馈网络
输入层有 k 个输入单元,第一个隐藏层有 n 个隐藏单元,第二个隐藏层有 m 个隐藏单元。最上面是一个始终开启的偏置单元。输出在这个例子中是两个单元,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/133.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/134.png,其中每个单元表示两个可能类别之一。具有最高激活值的输出单元将决定神经网络对给定输入样本的类别预测。每个隐藏单元都有一个非线性激活函数,而输出层则具有一种特别的激活函数叫做softmax,我们将在激活函数部分中讨论。一个层中的单元与前一层和后一层的所有单元相连(因此是全连接的)。每个连接都有自己的权重,w,为了简化图示未显示出来。
正如我们在第一章中提到的,我们可以将隐藏层看作是神经网络对输入数据的内部表示。这是神经网络用自己的内部逻辑理解输入样本的方式。然而,这种内部表示是人类无法解释的。为了弥合网络表示和我们关注的实际数据之间的差距,我们需要输出层。你可以将它看作一个翻译器;我们用它来理解网络的逻辑,同时也能将其转换为我们关心的实际目标值。
然而,我们并不局限于具有顺序层的单路径网络,如前图所示。这些层(或一般操作)构成有向无环图。在这样的图中,信息不能通过同一层两次(没有循环),且信息仅沿一个方向流动,从输入到输出。前面的网络图只是一个特殊的图例,其中的层按顺序连接。下图也展示了一个有效的神经网络,具有两个输入层、一个输出层以及随机互联的隐藏层。这些层被表示为操作 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/135.png (i 是帮助我们区分多个操作的索引):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_12.jpg
图 2.12 – 神经网络作为操作图
递归网络
有一种特殊类型的神经网络,称为递归网络,它们表示有向循环图(可以有循环)。我们将在第六章中详细讨论它们。
在这一节中,我们介绍了最基本的神经网络类型——即单元——并逐步扩展到层级,然后将其概括为操作图。我们也可以从另一个角度来思考它。这些操作有着精确的数学定义。因此,神经网络作为函数的组合,实际上也是一个数学函数,其中输入数据代表函数的参数,网络权重集合θ(即所有权重矩阵的集合,W)则是其参数。我们可以用https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/136.png 或者 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/137.png 来表示它。假设当一个操作接收到来自多个来源(输入数据或其他操作)的输入时,我们使用逐元素求和来合并多个输入张量。然后,我们可以将神经网络表示为一系列嵌套的函数/操作。左侧前馈网络的等效公式如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/139.png,这就是一个线性函数。那么,整个神经网络——也就是单元的组合——将变成线性函数的组合,这也是一个线性函数。这意味着即使我们加入隐藏层,网络仍然等价于一个简单的线性回归模型,具有所有的局限性。为了将网络转变为非线性函数,我们将使用非线性激活函数来处理这些单元。通常,同一层的所有单元都使用相同的激活函数,但不同层之间可以使用不同的激活函数。我们将从三种常见的激活函数开始,前两种来自神经网络的经典时期,第三种则是当代的:
- Sigmoid:它的输出值被限制在 0 和 1 之间,可以在随机过程中解释为该单元被激活的概率。由于这些特性,sigmoid 曾是最受欢迎的激活函数。然而,它也有一些不太理想的属性(稍后会详细讨论),这导致了它的使用逐渐减少。下图展示了 sigmoid 函数及其导数的图形(导数将在我们讨论反向传播时派上用场):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_13.jpg
图 2.13 – sigmoid 激活函数
- 双曲正切函数(tanh):其名称就已说明了其含义。与 sigmoid 的主要区别在于,tanh 的取值范围是(-1, 1)。下图展示了 tanh 函数及其导数的图形:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_14.jpg
图 2.14 – 双曲正切激活函数
- 修正线性单元(ReLU):这是新兴的技术(与前辈们相比)。ReLU 首次成功应用于 2011 年(参见《深度稀疏修正神经网络》)。下图展示了 ReLU 函数及其导数的图形:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_15.jpg
图 2.15 – ReLU 激活函数
如我们所见,当 x > 0 时,ReLU 会重复其输入,而在其他情况下保持为 0。这种激活函数相较于 sigmoid 和 tanh 具有几个重要的优势,使得训练拥有更多隐藏层(即更深层网络)的神经网络成为可能。我们将在第三章中讨论这些优势以及其他类型的激活函数。
在下一部分,我们将展示神经网络如何逼近任何函数。
泛化逼近定理
在*多层神经网络(NNs)*部分,我们将神经网络定义为一个函数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/140.png,其中x是输入数据(通常是一个向量或张量),θ是神经网络的权重。相反,训练数据集是输入样本和标签的集合,表示另一个现实世界中的函数!<mml:math xmlns:mml=“http://www.w3.org/1998/Math/MathML” xmlns:m=“http://schemas.openxmlformats.org/officeDocument/2006/math”>mml:mig</mml:mi><mml:mfenced separators=“|”>mml:mrow<mml:mi mathvariant=“bold”>x</mml:mi></mml:mrow></mml:mfenced></mml:math>。神经网络函数https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/142.png近似了函数https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/141.png:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/144.png
普遍逼近定理指出,任何在有限子集上的连续函数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/122.png 都可以通过一个至少包含一个隐含层、有限数量单元和非线性激活的前馈神经网络(NN)来逼近到任意精度。这是重要的,因为它告诉我们,在神经网络的理论上没有不可逾越的限制。实际上,虽然只有一个隐含层的神经网络在许多任务中表现不佳,但至少我们可以期待神经网络的光明前景。我们可以通过一个直观的例子来理解普遍逼近定理。
注意
以下例子的灵感来源于 Michael A. Nielsen 的书籍 神经网络与深度 学习 (neuralnetworksanddeeplearning.com/
)。
我们将设计一个带有单一隐含层的神经网络,用于逼近 箱型函数(见下图右侧)。这是一种阶梯函数,在所有输入值上都为 0,只有在一个狭窄范围内,它才取常数值 A。一系列 平移 的阶梯函数可以逼近任何在有限子集上的连续函数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/146.png,如下图左侧所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_16.jpg
图 2.16 – 左:使用一系列阶梯函数来逼近连续函数,右:一个单一的箱型阶梯函数
我们将从构建一个带有单一单元的箱型神经网络开始,该单元具有一个标量输入 x,并采用 sigmoid 激活函数。下图展示了该单元及其等效公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_17.jpg
图 2.17 – 带有单一输入和 sigmoid 激活的单元
在接下来的图示中,我们可以看到在输入 x 范围为 [-10: 10] 时,不同 b 和 w 值下的单元输出:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_18.jpg
图 2.18 – 基于不同 w 和 b 值的单元输出。网络输入 x 显示在 x 轴上。
我们可以看到权重 w 决定了 sigmoid 函数的陡峭程度。我们还可以看到公式 t = -b/w 决定了函数沿 x 轴的平移。让我们看看前面图示中的不同图形:
-
左上角:常规 sigmoid
-
右上角:一个较大的权重,w,放大输入,x,直到 sigmoid 输出类似于阈值激活
-
左下角:偏置,b,沿 x 轴平移单元激活
-
右下角:我们可以同时通过偏置,b,沿 x 轴平移激活,并通过负值权重,w,反转激活
我们可以直观地看到,这个单元可以实现箱形函数的所有部分。然而,为了创建一个完整的箱形函数,我们需要在一个有一个隐藏层的神经网络中结合两个这样的单元。以下图显示了神经网络架构,以及单元的权重和偏置,还有网络生成的箱形函数:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_19.jpg
图 2.19 – 箱形函数近似神经网络
其工作原理如下:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/147.png:神经网络输出为 0。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/148.png5:顶层单元对函数的上升阶梯激活,并且在所有值为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/149.png时保持激活。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/150.png0:底层单元对函数的下阶梯激活,并且在所有 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/151.png时保持激活。由于输出层的权重相同但符号相反,两个隐藏单元的输出相互抵消。
-
输出层的权重决定了箱形函数的常数值,A = 5。
该网络的输出在[-5:5]区间内为 5,其他输入则输出 0。我们可以通过以类似的方式向隐藏层添加更多单元来近似其他箱形函数。
现在我们已经熟悉了神经网络(NN)的结构,让我们关注它们的训练过程。
训练神经网络
函数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/152.png 近似 函数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/153.png: https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/154.png。训练的目标是找到参数 θ,使得 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/155.png 能够最佳地近似 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/156.png。首先,我们将看到如何为单层网络使用一个名为 GD 的优化算法。
然后,我们将把它扩展到一个深度前馈网络,借助 BP 的帮助。
Note
我们应该注意,NN 和其训练算法是两个独立的东西。这意味着我们可以通过除了 GD 和 BP 之外的某种方式调整网络的权重,但这是目前在实践中使用的最流行和高效的方式。
GD
在本节中,我们将使用均方误差(MSE)成本函数训练一个简单的 NN。它衡量了网络输出与所有训练样本的训练数据标签之间的差异(称为误差)。
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/157.png
起初,这可能看起来有些吓人,但别担心!背后其实是非常简单直接的数学(我知道这听起来更吓人!)。让我们来讨论它的组成部分:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/158.png: 神经网络的输出,其中 θ 是所有网络权重的集合。在本节的其余部分,我们将使用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/159.png 来表示各个单独的权重(与其他部分使用的 w 符号不同)。
-
n: 训练集中的样本总数。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/132.png:训练样本的向量表示,其中上标i表示数据集的第i个样本。我们使用上标是因为https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/132.png是一个向量,且下标通常保留给每个向量分量。例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/162.png是第i个训练样本的第j个分量。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/163.png:与训练样本相关联的标签,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/132.png。
注
在这个例子中,我们将使用均方误差(MSE),但实际上有不同类型的代价函数。我们将在第三章中讨论它们。
首先,梯度下降(GD)计算代价函数*J(θ)对所有网络权重的导数(梯度)。梯度告诉我们J(θ)如何随着每个权重的变化而变化。然后,算法利用这些信息以更新权重的方式,使得将来相同输入/目标对的J(θ)*值最小化。目标是逐步达到代价函数的全局最小值,此时梯度为 0。以下是对单个神经网络权重的 MSE 梯度下降(GD)可视化:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_20.jpg
图 2.20 – MSE 图示
让我们逐步了解 GD 的执行过程:
-
初始化网络权重θ,赋予随机值。
-
重复以下过程,直到代价函数*J(θ)*降到某个阈值以下:
-
前向传播:使用前述公式计算训练集所有样本的 MSE J(θ) 成本函数。
-
反向传播:使用链式法则计算 J(θ) 对所有网络权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/165.png 的偏导数(梯度)。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/166.png
让我们分析一下偏导数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/167.png。J 是一个关于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/168.png 的函数,因为它是网络输出的函数。因此,它也是 NN 函数本身的函数——即 J(f(θ))。然后,按照链式法则,我们得到以下公式:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/169.png
- 使用这些梯度值来更新每个网络权重:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/170.png
这里,η 是学习率,它决定了优化器在训练过程中更新权重时的步长。需要注意的是,随着我们逐渐接近全局最小值,梯度会变得越来越小,我们将以更细的步伐更新权重。
为了更好地理解梯度下降(GD)是如何工作的,我们以线性回归为例。我们可以回顾一下,线性回归相当于一个具有恒等激活函数的单一神经网络单元,f(x) = x:
-
线性回归由以下函数表示 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/171.png,其中 m 是输入向量的维度(等于权重的数量)。
-
然后,我们有 MSE 代价函数 – https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/172.png
-
接下来,我们计算偏导数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/173.png 相对于单个网络权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/174.png,使用链式法则和求和法则:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/175.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/176.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/177.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/178.png
- 现在我们得到了梯度, https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/179.png,我们可以使用学习率η来更新权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/180.png
到目前为止,我们讨论的是一个适用于具有多个权重的神经网络(NN)的梯度下降算法。然而,为了简化起见,前面的图示说明了成本函数与单一权重神经网络之间的关系。让我们通过引入一个具有两个权重的更复杂的神经网络成本函数来解决这个问题,分别为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/181.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/182.png:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_21.jpg
图 2.21 – 关于两个权重的成本函数 J
该函数有局部最小值和全局最小值。没有什么可以阻止梯度下降(GD)收敛到局部最小值,而不是全局最小值,从而找到一个次优的目标函数近似。我们可以通过增大学习率η来尝试缓解这个问题。其想法是,即使 GD 收敛到局部最小值,较大的η也能帮助我们跳跃过鞍点并收敛到全局最小值。风险在于,反之也可能发生——如果 GD 正确地收敛到全局最小值,较大的学习率可能会使其跳跃到局部最小值。
防止此问题的一个更优雅的方法是使用动量。这通过调整当前权重更新与之前权重更新的值来扩展了普通的梯度下降——也就是说,如果步骤 t-1 的权重更新很大,它也会增加步骤 t 的权重更新。我们可以通过类比来解释动量。可以将损失函数表面看作是一座山的表面。现在,假设我们把一个球放在山顶(最大值)上。如果我们放开球,依靠地球的重力,它会开始滚向山谷底部(最小值)。它行进的距离越远,速度就越快。换句话说,它会获得动量(因此优化方法也得名)。
现在,让我们看看如何在权重更新规则中实现动量 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/183.png。我们假设现在处于训练过程中的第 t 步:
-
首先,我们将计算当前的权重更新值 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/184.png,并包括上次更新的速度 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/185.png:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/186.png。这里,μ是一个范围在[0:1]之间的超参数,称为动量率。https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/187.png 在第一次迭代时初始化为 0。
-
然后,我们执行实际的权重更新 – https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/188.png。
找到最佳的学习率 η 和动量率 μ 是一项经验任务。它们可能依赖于神经网络的架构、数据集的类型和大小以及其他因素。此外,我们可能需要在训练过程中调整它们。由于神经网络的权重是随机初始化的,我们通常从较大的 η 开始,以便梯度下降(GD)可以快速推进,尤其是在成本函数(误差)初始值较大时。一旦成本函数的下降开始趋于平稳,我们就可以降低学习率。通过这种方式,梯度下降可以找到那些在较大学习率下可能跳过的最小值。
另外,我们可以使用一种自适应学习率算法,如 Adam(请参阅 arxiv.org/abs/1412.6980
中的 Adam: A Method for Stochastic Optimization)。它根据之前的权重更新(动量)为每个权重计算个性化和自适应的学习率。
我们刚刚描述的 GD 称为 批量梯度下降,因为它在 所有 训练样本上累积误差,然后进行一次权重更新。这对于小型数据集来说没问题,但对于大型数据集来说可能变得不切实际,因为这种偶尔更新的方式会导致训练时间很长。在实际应用中,我们会使用两种修改:
-
随机(或在线)梯度下降(SGD):在每个训练样本之后更新权重。
-
小批量梯度下降:在每个小批量(称为小批量)的 k 个样本上累积误差,并在每个小批量之后进行一次权重更新。它是在线 GD 和批量 GD 之间的一种混合方式。在实践中,我们几乎总是会使用小批量 GD,而不是其他修改。
我们学习旅程的下一步是了解如何将 GD 应用到多层网络的训练中。
反向传播
在本节中,我们将讨论如何将 GD 与 BP 算法结合,以更新多层网络的权重。正如我们在 GD 部分演示的那样,这意味着求出成本函数 J(θ) 对每个网络权重的导数。我们已经借助链式法则在这方面迈出了第一步:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/189.png
在这里,f(θ) 是网络的输出,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/191.png 作为层 l 中第 i 个单元与下一层 l+1 中第 j 个单元之间的权重。在多层网络中,l 和 l+1 可以是任何两个连续的层,包括输入层、隐藏层和输出层。
-
我们将用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/192.png 来表示层 l 中第 i 个单元的输出,用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/193.png 来表示层 l+1 中第 j 个单元的输出。
-
我们将用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/194.png 来表示层 l 中单元 j 激活函数的输入(即激活前输入的加权和)。
以下图示展示了我们介绍的所有符号:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_22.jpg
图 2.22 – 层 l 表示输入层,层 l+1 表示输出层,w 连接了层 l 中的 y 激活到层 l+1 中第 j 个单元的输入。
掌握了这些有用的知识后,我们开始正式讨论:
- 首先,我们假设 l 和 l+1 分别是倒数第二层和最后一层(输出层)。了解这一点后,J 关于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/195.png 的导数如下所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/196.png
-
让我们聚焦于 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/197.png。这里,我们计算的是层 l 输出的加权和对其中一个权重的偏导数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/202.png
-
第 1 点中的公式适用于网络中任何两个连续的隐藏层,l 和 l+1。我们知道,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/203.png,同时我们还知道,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/204.png 是激活函数的导数,我们可以计算它(参见 激活函数 部分)。我们需要做的就是计算导数 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/205.png(回想一下,这里,l+1 是一个隐藏层)。我们可以注意到,这是相对于 l+1 层中激活函数的误差导数。现在我们可以从最后一层开始,逐步向后计算所有的导数,因为以下内容成立:
-
我们可以计算最后一层的这个导数。
-
我们有一个公式,可以让我们计算一个层的导数,前提是我们能够计算下一层的导数。
- 牢记这些要点后,我们通过应用链式法则得到以下公式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/206.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/207.png 在网络的前馈部分中,输出被馈送到第 l+1 层的所有单元。因此,它们在向后传播错误时都会对 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/208.png 有所贡献。
注意
在层作为操作部分,我们讨论了在前向传递中,如何通过矩阵-矩阵乘法同时计算出层l+1的所有输出 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/209.png。在这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/210.png 是层l的层输出,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/211.png 是层l和层l+1之间的权重矩阵。在前向传递中,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/212.png 的一列代表了从输入层l的所有单元到输出层l+1的某个单元的权重。在反向传递中,我们也可以通过使用转置权重矩阵来表示矩阵-矩阵乘法!<mml:math xmlns:mml=“http://www.w3.org/1998/Math/MathML” xmlns:m=“http://schemas.openxmlformats.org/officeDocument/2006/math”>mml:msupmml:mrow<mml:mi mathvariant=“bold”>W</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators=“|”>mml:mrowmml:mil</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn>mml:mo,</mml:mo>mml:mil</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrow<mml:mfenced open=“[” close=“]” separators=“|”>mml:mrowmml:msupmml:mrow<mml:mi mathvariant=“bold”>W</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators=“|”>mml:mrowmml:mil</mml:mi>mml:mo,</mml:mo>mml:mil</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mi⊤</mml:mi></mml:mrow></mml:msup></mml:math>。转置矩阵的一列代表了所有来自l的单元权重,这些权重在前向传递阶段贡献给了l+1的某个特定单元。
我们再次计算 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/214.png。 使用我们在步骤 3中遵循的相同逻辑,我们可以计算出 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/215.png。 因此,一旦我们知道 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/216.png,我们可以计算出 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/217.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/218.png。
由于我们可以计算 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/219.png 用于最后一层,我们可以向后移动并计算 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/220.png 对于任何层,并因此计算 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/221.png 对于任何层。
- 总结一下,假设我们有一系列层,其中适用以下内容:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/222.png
这里,我们有以下基本方程式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/196.png
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/206.png
通过这两个方程,我们可以计算出每一层的成本函数的导数。
- 如果我们设置 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/225.png,那么 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/226.png 表示相对于激活值的成本变化,我们可以将 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/227.png 看作是单元 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/228.png 的误差。我们可以将这些方程改写如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/229.png
接下来,我们可以写出以下方程:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/230.png
这两个方程为我们提供了 BP 的另一种视角,因为成本随着激活值的变化而变化。它们为我们提供了一种计算任意层* l 变化的方法,只要我们知道下一层 l+1 *的变化。
- 我们可以将这些方程组合起来显示如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/231.png
- 每层权重的更新规则由以下方程给出:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/232.png
现在我们熟悉了 GD 和 BP,让我们在 Python 中实现它们。
XOR 函数的 NN 的代码示例
在本节中,我们将创建一个具有一个隐藏层的简单网络,该网络解决 XOR 函数问题。我们回顾一下,XOR 是一个线性不可分的问题,因此需要一个隐藏层。源代码将允许你轻松修改层数和每层的单元数,便于你尝试不同的场景。我们将不使用任何机器学习库,而是从零开始实现它们,仅借助numpy
。我们还将使用matplotlib
来可视化结果:
-
我们首先导入这些库:
import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import ListedColormap
-
然后,我们将定义激活函数及其导数。在这个例子中,我们将使用
tanh(x)
:def tanh(x): return (1.0 - np.exp(-2 * x)) / (1.0 + np.exp(-2 * x)) def tanh_derivative(x): return (1 + tanh(x)) * (1 - tanh(x))
-
然后,我们将开始定义
NeuralNetwork
类及其构造函数(请注意,所有的方法和属性都必须正确缩进):class NeuralNetwork: # net_arch consists of a list of integers, indicating # the number of units in each layer def __init__(self, net_arch): self.activation_func = tanh self.activation_derivative = tanh_derivative self.layers = len(net_arch) self.steps_per_epoch = 1000 self.net_arch = net_arch # initialize the weights with random values in the range (-1,1) self.weights = [] for layer in range(len(net_arch) - 1): w = 2 * np.random.rand(net_arch[layer] + 1, net_arch[layer + 1]) - 1 self.weights.append(w)
这里,
net_arch
是一个一维数组,包含每一层的单元数。例如,[2, 4, 1]
表示输入层有两个单元,隐藏层有四个单元,输出层有一个单元。由于我们正在研究 XOR 函数,输入层将有两个单元,输出层只有一个单元。然而,隐藏单元的数量可以变化。为了结束构造函数,我们将使用范围为(-1, 1)的随机值来初始化网络权重。
-
现在,我们需要定义
fit
函数,它将训练我们的网络:def fit(self, data, labels, learning_rate=0.1, epochs=10):
-
我们将通过将
bias
与训练data
连接到一个新的变量input_data
来开始实现(源代码在方法定义内进行了缩进):bias = np.ones((1, data.shape[0])) input_data = np.concatenate((bias.T, data), axis=1)
-
然后,我们将运行训练若干
epochs
:for k in range(epochs * self.steps_per_epoch):
-
在循环中,我们将在每个 epoch 开始时可视化 epoch 编号和神经网络的预测输出:
print('epochs: {}'.format(k / self.steps_per_epoch)) for s in data: print(s, nn.predict(s))
-
在循环中,我们从训练集随机选择一个样本,并将其通过隐藏网络层进行前向传播:
sample = np.random.randint(data.shape[0]) y = [input_data[sample]] for i in range(len(self.weights) - 1): activation = np.dot(y[i], self.weights[i]) activation_f = self.activation_func(activation) # add the bias for the next layer activation_f = np.concatenate((np.ones(1), np.array(activation_f))) y.append(activation_f)
-
在循环外,我们将计算最后一层的输出和误差:
# last layer activation = np.dot(y[-1], self.weights[-1]) activation_f = self.activation_func(activation) y.append(activation_f) # error for the output layer error = y[-1] - labels[sample] delta_vec = [error * self.activation_derivative(y[-1])]
-
然后,我们将反向传播误差(反向传播):
# we need to begin from the back from the next to last layer for i in range(self.layers - 2, 0, -1): error = delta_vec[-1].dot(self.weights[i][1:].T) error = error * self.activation_derivative(y[i][1:]) delta_vec.append(error) # reverse # [level3(output)->level2(hidden)] => [level2(hidden)->level3(output)] delta_vec.reverse()
-
最后,我们将根据我们刚刚计算的误差更新权重。我们将用输出的 delta 与输入激活相乘,得到权重的梯度。然后,我们将使用学习率更新权重:
for i in range(len(self.weights)): layer = y[i].reshape(1, nn.net_arch[i] + 1) delta = delta_vec[i].reshape(1, nn.net_arch[i + 1]) self.weights[i] -= learning_rate * layer.T.dot(delta)
这就完成了
fit
方法的实现。 -
我们现在将编写一个
predict
函数来检查结果,该函数返回给定输入的网络输出:def predict(self, x): val = np.concatenate((np.ones(1).T, np.array(x))) for i in range(0, len(self.weights)): val = self.activation_func( np.dot(val, self.weights[i])) al = np.concatenate((np.ones(1).T, np.array(val))) return val[1]
-
最后,我们将编写
plot_decision_regions
方法,该方法根据输入变量绘制分隔类的超平面(在我们的例子中表示为直线)。我们将创建一个二维网格,每个输入变量对应一个轴。我们将绘制神经网络对整个网格的所有输入值组合的预测结果。我们会将网络输出大于 0.5 的视为true
,否则视为false
(我们将在本节的最后看到这些图):def plot_decision_regions(self, X, y, points=200): markers = ('o', '^') colors = ('red', 'blue') cmap = ListedColormap(colors) x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1 x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1 resolution = max(x1_max - x1_min, x2_max - x2_min) / float(points) xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution)) input = np.array([xx1.ravel(), xx2.ravel()]).T Z = np.empty(0) for i in range(input.shape[0]): val = nn.predict(np.array(input[i])) if val < 0.5: val = 0 if val >= 0.5: val = 1 Z = np.append(Z, val) Z = Z.reshape(xx1.shape) plt.pcolormesh(xx1, xx2, Z, cmap=cmap) plt.xlim(xx1.min(), xx1.max()) plt.ylim(xx2.min(), xx2.max()) # plot all samples classes = ["False", "True"] for idx, cl in enumerate(np.unique(y)): plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1], alpha=1.0, c=colors[idx], edgecolors='black', marker=markers[idx], s=80, label=classes[idx]) plt.xlabel('x1) plt.ylabel('x2') plt.legend(loc='upper left') plt.show()
这就完成了
NeuralNetwork
类的实现。 -
最后,我们可以使用以下代码运行程序:
np.random.seed(0) # Initialize the NeuralNetwork with 2 input, 2 hidden, and 1 output units nn = NeuralNetwork([2, 2, 1]) X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) y = np.array([0, 1, 1, 0]) nn.fit(X, y, epochs=10) print("Final prediction") for s in X: print(s, nn.predict(s)) nn.plot_decision_regions(X, y)
我们将构建默认的网络,
nn = NeuralNetwork([2,2,1])
。第一个和最后一个值(2
和1
)代表输入层和输出层,不能修改,但我们可以添加不同数量的隐藏层和不同数量的单元。例如,([2,4,3,1])
将表示一个三层的神经网络,第一个隐藏层有四个单元,第二个隐藏层有三个单元。我们将使用
numpy.random.seed(0)
来确保权重初始化在每次运行时的一致性,以便我们能够比较它们的结果。这是在训练神经网络时的常见做法。
现在,我们将分别在x
和y
中定义训练 XOR 数据和标签。我们将训练 10 个周期。最后,我们将绘制结果。
在接下来的图示中,你可以看到nn.plot_decision_regions
函数方法如何绘制超平面,这些超平面将不同的类别分开。圆圈表示(true, true)
和(false, false)
输入的网络输出,而三角形表示 XOR 函数的(true, false)
和(false, true)
输入。在左边,我们可以看到一个具有两个隐藏单元的神经网络的超平面,而在右边,我们可以看到一个具有四个隐藏单元的神经网络:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_02_23.jpg
图 2.23 – 左:具有两个隐藏单元的神经网络所学习到的超平面,右:具有四个隐藏单元的神经网络的超平面
具有不同架构的网络可以产生不同的分离区域。在前面的图中,我们可以看到,尽管网络找到了正确的解决方案,但分隔区域的曲线会有所不同,这取决于所选择的架构。
我们现在准备开始更深入地了解深度神经网络及其应用。
总结
在本章中,我们详细介绍了神经网络(NNs),并提到了它们相对于其他竞争算法的成功。神经网络由互相连接的单元组成,连接的权重决定了不同单元之间通信的强度。我们讨论了不同的网络架构,神经网络如何拥有多个层次,以及为什么内部(隐藏)层非常重要。我们解释了信息是如何从输入流向输出的,经过每一层的传递,基于权重和激活函数。最后,我们展示了如何训练神经网络——也就是如何通过梯度下降(GD)和反向传播(BP)调整它们的权重。
在接下来的章节中,我们将继续讨论深度神经网络(NNs)。我们将特别解释深度在深度学习中的含义,这不仅指网络中隐藏层的数量,还指网络学习的质量。为此,我们将展示神经网络如何学习识别特征并将其组合成更大对象的表示。我们还将介绍一些重要的深度学习库,最后提供一个具体的示例,展示如何将神经网络应用于手写数字识别。
第三章:深度学习基础
在这一章中,我们将介绍深度学习(DL)和深度神经网络(DNNs)——即具有多个隐藏层的神经网络(NNs)。你可能会疑惑,既然有通用逼近定理,为什么还需要使用多个隐藏层?这个问题并非天真,长期以来,神经网络确实是以这种方式使用的。
不需要过多细节解释,原因之一是,逼近一个复杂的函数可能需要隐藏层中大量的单元,这使得使用它变得不切实际。还有另一个更为重要的原因,虽然它与隐藏层的数量无关,但与学习的层次相关。一个深度网络不仅仅是学习如何根据输入X预测输出Y;它还能够理解输入的基本特征。它能够学习输入样本特征的抽象,理解样本的基本特性,并基于这些特性进行预测。这种抽象层次在其他基本的机器学习(ML)算法和浅层神经网络中是缺失的。
在这一章中,我们将讨论以下主要主题:
-
深度学习简介
-
深度学习的基本概念
-
深度神经网络
-
训练深度网络
-
深度学习的应用
-
介绍流行的深度学习库
技术要求
我们将在本章中使用 Python、PyTorch 和 Keras 作为TensorFlow(TF)的一部分来实现示例。如果你还没有设置好这些工具的环境,别担心——示例代码已经作为 Jupyter 笔记本文件提供在 Google Colab 上。你可以在本书的 GitHub 仓库中找到代码示例:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter03
。
深度学习简介
在 2012 年,Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 发表了一篇具有里程碑意义的论文,题为 使用深度卷积神经网络进行 ImageNet 分类 (papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf
)。该论文描述了他们使用神经网络(NN)赢得同年 ImageNet 竞赛的过程,这一点我们在第二章中提到过。论文结尾指出,即使移除单一层,网络的性能也会下降。他们的实验表明,移除任何中间层都会导致模型大约 2%的 Top-1 准确率损失。他们得出结论,网络的深度对性能至关重要。那么,基本问题是:是什么让网络的深度如此重要呢?
一个典型的英文谚语是“一图胜千言”。让我们用这种方法来理解什么是深度学习(DL)。我们将使用来自广泛引用的论文《卷积深度信念网络:可扩展的无监督学习层次表示》中的图像(ai.stanford.edu/~ang/papers/icml09-ConvolutionalDeepBeliefNetworks.pdf
)。在这篇论文中,作者用不同类别的物体或动物图片训练了一个神经网络(NN)。下图展示了网络的不同层如何学习输入数据的不同特征。在第一层,网络学习检测一些小的基础特征,例如线条和边缘,这些特征在所有类别的所有图像中都是常见的:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_1.jpg
图 3.1 – 第一层权重(上)和第二层权重(下)训练后的结果
但是接下来的层,如下图所示,将这些线条和边缘结合起来,组成了更复杂的特征,这些特征对每个类别都是特定的。在左下角图像的第一行中,我们可以看到网络如何检测到人脸的不同特征,例如眼睛、鼻子和嘴巴。对于汽车而言,这些特征可能是车轮、车门等等,如下图中的第二张图所示。这些特征是抽象的——即,网络已经学会了一个特征(如嘴巴或鼻子)的通用形状,并且能够在输入数据中检测到这个特征,尽管它可能具有不同的变化:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_2.jpg
图 3.2 – 第 1 至第 4 列表示为特定物体类别(类)学习的第二层(上)和第三层(下)权重。第 5 列表示为四个物体类别(人脸、汽车、飞机和摩托车)混合学习的权重
在前述图像的第二行中,我们可以看到,在更深的层次中,网络将这些特征组合成更复杂的特征,如人脸和完整的汽车。深度神经网络(DNN)的一个优势是,它们能够自主地学习这些高层次的抽象表示,并从训练数据中推导出这些表示。
接下来,让我们更详细地讨论 DNN 的这些特性。
深度学习的基本概念
1801 年,Joseph Marie Charles 发明了贾卡尔织机。Charles 不是一名科学家,而仅仅是一个商人。贾卡尔织机使用了一套打孔卡片,每张卡片代表着织机上要复制的图案。同时,每张卡片也是该图案的抽象表示。例如,打孔卡片曾被用在 1890 年由 Herman Hollerith 发明的统计机器中,或者作为第一代计算机输入代码的一种方式。在统计机器中,卡片仅仅是要输入机器以计算人口统计数据的样本的抽象。然而,在贾卡尔织机中,卡片的使用更加微妙,每张卡片代表了可以与其他卡片组合起来形成更复杂图案的模式抽象。打孔卡片是现实特征的抽象表示,最终编织出来的设计就是这一抽象的体现。
在某种程度上,贾卡尔织机为今天的深度学习(DL)播下了种子,它通过对特征的表示定义了现实的含义。深度神经网络(DNN)不仅仅是识别猫是什么,或者松鼠是什么,它更理解猫和松鼠分别具备哪些特征。它学会了如何利用这些特征来设计一只猫或一只松鼠。如果我们要用贾卡尔织机设计一个猫形状的织物图案,我们需要使用具有猫鼻子上的胡须,且有优雅而纤细的身体的打孔卡片。相反,如果我们要设计一只松鼠,则需要使用能够表现松鼠毛茸茸尾巴的打孔卡片。一个学习了基本特征表示的深度网络,可以根据它所做的假设进行分类。例如,如果没有毛茸茸的尾巴,它可能就不是松鼠,而是猫。通过这种方式,网络学习到的信息更为完整且更具鲁棒性,最令人兴奋的是,深度神经网络能够自动完成这一学习过程。
特征学习
为了说明深度学习是如何工作的,我们来考虑一个简单的几何图形识别任务,例如识别一个立方体,见下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_3.jpg
图 3.3 – 一个表示立方体的神经网络(NN)的抽象图。不同的层次编码了具有不同抽象层级的特征。
这个立方体由边(或线)组成,这些边在顶点处相交。假设三维空间中的每一个可能点都与一个单位相关联(暂时忽略这将需要无穷多个单位)。所有这些点/单位都位于多层前馈网络的第一层(输入层)。如果相应的点位于一条线上,则输入点/单位是激活的。位于同一条线(边)上的点/单位与下一层中的单一公共边/单位之间有强的正向连接。相反,它们与下一层中所有其他单位之间有负向连接。唯一的例外是位于顶点上的单位。每个这样的单位同时位于三条边上,并与下一层中的三个对应单位相连接。
现在,我们有了两个隐藏层,分别具有不同的抽象层次——第一个层次处理点,第二个层次处理边。然而,这还不足以在网络中编码一个完整的立方体。我们尝试通过增加一个顶点层来解决这个问题。在这里,第二层中每三个激活的边/单位(形成一个顶点)与第三层中的单一公共顶点/单位之间有显著的正向连接。由于立方体的每条边形成两个顶点,因此每条边/单位将与两个顶点/单位有正向连接,并与所有其他单位有负向连接。最后,我们将引入最后一个隐藏层(立方体)。形成立方体的四个顶点/单位将与来自立方体/层的单一立方体/单位之间有正向连接。
这个立方体表示的例子过于简化,但我们可以从中得出几个结论。其中之一是深度神经网络(DNN)非常适合层次化组织的数据。例如,一张图像由像素组成,这些像素形成线条、边缘、区域等。同样,语音也如此,其中的基本构件叫做音素,而文本则有字符、单词和句子。
在前面的例子中,我们有意为特定的立方体特征分配了层,但实际上,我们不会这么做。相反,深度网络会在训练过程中自动“发现”特征。这些特征可能不会立刻显现出来,而且通常人类也无法解释它们。此外,我们无法知道网络中不同层所编码的特征的层次。我们的例子更像是经典的机器学习(ML)算法,在这些算法中,用户必须凭借自己的经验选择他们认为最好的特征。这一过程被称为特征工程,它可能既费力又耗时。让网络自动发现特征不仅更容易,而且这些特征通常非常抽象,从而使它们对噪声的敏感度较低。例如,人类视觉可以识别不同形状、大小、光照条件下的物体,甚至在物体的视角部分被遮挡时也能识别。我们能认出不同发型和面部特征的人,即使他们戴着帽子或围巾遮住嘴巴。类似地,网络学习到的抽象特征将帮助它更好地识别面孔,即使在更具挑战性的条件下。
在下一节中,我们将讨论深度学习(DL)变得如此流行的一些原因。
深度学习流行的原因
如果你已经关注机器学习(ML)一段时间,你可能会注意到许多深度学习(DL)算法并不新鲜。多层感知机(MLPs)已经存在近 50 年。反向传播算法曾多次被发现,但最终在 1986 年获得了认可。著名计算机科学家 Yann LeCun 在 1990 年代完善了他的卷积网络工作。在 1997 年,Sepp Hochreiter 和 Jürgen Schmidhuber 发明了长短期记忆网络(LSTM),这是一种至今仍在使用的递归神经网络(RNN)。在这一节中,我们将尝试理解为什么如今我们迎来了 AI 的春天,而之前只有 AI 的冬天(en.wikipedia.org/wiki/AI_winter
)。
第一个原因是,今天我们拥有的数据比过去多得多。互联网和软件在各个行业的兴起生成了大量可以通过计算机访问的数据。我们还拥有更多的基准数据集,例如 ImageNet。随着这些数据的增加,人们也希望通过分析数据来提取价值。正如我们稍后会看到的,深度学习(DL)算法在使用大量数据进行训练时表现得更好。
第二个原因是计算能力的提升。最显著的表现就是图形处理单元(GPU)的处理能力大幅提高。神经网络的组织方式使得它能够充分利用这种并行架构。让我们看看为什么。正如我们在第二章中所学到的,网络层的单元与同一层的单元没有直接连接。我们还学到了,许多层的操作可以表示为矩阵乘法。矩阵乘法是显著并行的(相信我,这是一个术语——你可以去 Google 查找!)。每个输出单元的计算与其他输出单元的计算无关。因此,我们可以并行计算所有的输出。并且,GPU 非常适合执行这样的高并行操作。一方面,GPU 的计算核心数量远超中央处理单元(CPU)。尽管 CPU 的核心速度比 GPU 核心更快,但我们仍然可以在 GPU 上并行计算更多的输出单元。但更重要的是,GPU 在内存带宽方面进行了优化,而 CPU 则优化了延迟。这意味着 CPU 可以非常快速地获取小块内存,但当需要获取大块内存时则会变得较慢。而 GPU 则相反。对于一个深度网络中有许多宽层的矩阵乘法,带宽成为瓶颈,而不是延迟。此外,GPU 的 L1 缓存比 CPU 的 L1 缓存更快,而且更大。L1 缓存代表了程序下一步可能使用的信息存储,存储这些数据可以加速处理过程。在深度神经网络(DNN)中,大量的内存会被重复使用,这也是 L1 缓存非常重要的原因。
在下一节《深度神经网络》中,我们将给出神经网络关键架构的更精确定义,并将在接下来的章节中详细介绍这些架构。
深度神经网络
我们可以将深度学习(DL)定义为机器学习(ML)技术的一类,其中信息通过分层处理,以逐步深入的方式理解数据中的表示和特征,复杂度逐渐增加。实际上,所有深度学习算法都是神经网络(NN),它们共享一些基本的共同特性。它们都由一个互联操作的图构成,操作过程使用输入/输出张量。它们的不同之处在于网络架构(或网络中单元的组织方式),有时也体现在训练方法上。考虑到这一点,让我们来看看神经网络的主要类型。以下列表并不详尽,但它代表了今天大多数使用中的神经网络类型:
-
多层感知器(MLP):一种具有前馈传播、全连接层且至少有一个隐藏层的神经网络。我们在第二章中介绍了 MLP。
-
卷积神经网络(CNN):CNN 是一种前馈神经网络,具有几种特殊类型的层。例如,卷积层通过滑动滤波器到输入图像(或声音)上,从而应用该滤波器,生成n维激活图。有证据表明,CNN 中的单元以类似生物细胞在大脑视觉皮层中的组织方式进行组织。到目前为止,我们已经提到过 CNN 很多次,这并非巧合——今天,CNN 在许多计算机视觉和自然语言处理任务中优于所有其他机器学习算法。我们将在第四章中讨论 CNN。
-
循环神经网络(RNN):这种类型的神经网络具有一个内部状态(或记忆),该状态基于已输入网络的所有或部分数据。循环网络的输出是其内部状态(输入的记忆)和最新输入样本的组合。同时,内部状态会发生变化,以纳入新输入的数据。由于这些特性,循环网络非常适合处理顺序数据任务,例如文本或时间序列数据。我们将在第六章中讨论循环网络。
-
Transformer:与 RNN 类似,transformer 适合处理序列数据。它使用一种叫做注意力机制的方式,使得模型能够直接同时访问输入序列中的所有元素。这与 RNN 不同,后者是逐个处理序列元素,并在每个元素之后更新其内部状态。正如我们将在第七章中看到的,注意力机制相较于经典的 RNN 具有多个重要优势。正因为如此,近年来,transformer 已经在许多任务中取代了 RNN。
-
自编码器:正如我们在第一章中提到的,自编码器是一类无监督学习算法,其输出形状与输入相同,这使得网络能够更好地学习基本表示。
现在我们已经概述了主要的深度神经网络类型,让我们讨论一下如何训练它们。
训练深度神经网络
历史上,科学界一直认为,深度网络相比浅层网络具有更强的表示能力。然而,训练拥有多个隐藏层的网络曾面临许多挑战。我们现在知道,结合梯度下降和反向传播,我们可以成功训练深度神经网络(DNN),正如我们在第二章中讨论过的那样。在本节中,我们将看到如何改进这些网络,以解决一些只在深度神经网络中出现的问题,而不是浅层神经网络。
本书的第一版包括了如限制玻尔兹曼机(RBMs)和深度信念网络(DBNs)等网络。它们由加拿大科学家 Geoffrey Hinton 推广,他是最著名的深度学习研究者之一。早在 1986 年,他也是反向传播算法的发明者之一。RBM 是一种特殊类型的生成性神经网络,其中单元被组织成两个层次,即可见层和隐藏层。与前馈网络不同,RBM 中的数据可以双向流动——从可见单元到隐藏单元,反之亦然。2002 年,Hinton 教授引入了对比散度,这是一种用于训练 RBM 的无监督算法。2006 年,他引入了深度信念网络(DBNs),这些是通过堆叠多个 RBM 形成的深度神经网络。由于其创新的训练算法,DBN 可以拥有比以前更多的隐藏层。但即便有了对比散度,训练一个 DBN 也不是件容易的事。这是一个两步过程:
-
首先,我们必须使用对比散度训练每个 RBM,并逐渐将它们堆叠在一起。这个阶段叫做预训练。
-
实际上,预训练作为下一阶段的一个复杂的权重初始化算法,叫做微调。通过微调,我们将 DBN 转化为一个常规的 MLP,并继续使用监督反向传播和梯度下降训练它,就像我们在第二章中看到的那样。
由于一些算法的进展,现在可以使用传统的反向传播算法训练深度网络,从而有效地消除了预训练阶段。这些进展使得 DBN 和 RBM 变得过时。它们无疑在研究中非常有趣,但在实践中已经很少使用,我们将在本版本中省略它们。
接下来,让我们讨论一些使得使用反向传播训练神经网络成为可能的算法进展。
改进的激活函数
那么,为什么训练深度网络如此困难呢?预训练解决的主要挑战之一就是所谓的梯度消失问题。为了理解这一点,我们假设使用反向传播训练一个普通的多层感知机(MLP),该网络具有多个隐藏层,并在每个层使用逻辑 sigmoid 激活函数。我们先聚焦于 sigmoid 函数(tanh 函数的情况也是如此)。提醒一下,sigmoid 函数的计算公式为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/233.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/234.png:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_4.jpg
图 3.4 – 逻辑 sigmoid(未中断)及其导数(中断)(左);连续的 sigmoid 激活,将数据“压缩”(右)
梯度消失现象表现为以下几种方式:
-
在前向传播阶段,第一个 sigmoid 层的输出由前面图像中的蓝色不中断线表示,且其值位于(0, 1)范围内。右侧图像中的虚线表示每一层连续层之后的 sigmoid 激活值。即使经过三层,我们也可以看到激活值在一个狭窄的范围内“压缩”,并且无论输入值如何,它都趋向于约 0.66。例如,如果第一层的输入值为 2,那么https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/235.png,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/236.png,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/237.png,等等。sigmoid 函数的这一特性相当于擦除从前一层传来的信息。
-
我们现在知道,要训练一个神经网络(NN),我们需要计算激活函数的导数(以及所有其他的导数)以供反向传播阶段使用。在前面的图像中,左侧的绿色中断线表示了 sigmoid 函数的导数。我们可以看到,它在一个非常窄的区间内有显著的值,且该区间围绕 0 居中,而在其他所有情况下则趋向于 0。在有多个层的网络中,当导数传播到网络的前几层时,它很可能会趋向于 0。实际上,这意味着我们无法将误差传播到这些层,也无法以有意义的方式更新它们的权重。
幸运的是,我们在第二章中引入的ReLU激活函数可以一举解决这两个问题。回顾一下,下面的图像展示了 ReLU 图形及其导数:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_5.jpg
图 3.5 – ReLU 激活函数(不中断)及其导数(中断)
ReLU 具有以下理想特性:
-
它是 幂等的。如果我们通过任意次数的 ReLU 激活传递一个值,它将保持不变;例如,ReLU(2) = 2,ReLU(ReLU(2)) = 2,依此类推。这对于 sigmoid 函数来说并不成立。ReLU 的幂等性使得理论上可以创建比 sigmoid 更深层的网络。
-
我们还可以看到,它的导数无论反向传播的值如何,都是 0 或 1。通过这种方式,我们还可以避免在反向传播中梯度消失的问题。严格来说,ReLU 在值为 0 时的导数是未定义的,这使得 ReLU 只在半微分意义下有效(关于这一点的更多信息可以参考
en.wikipedia.org/wiki/Semi-differentiability
)。但在实践中,它足够有效。 -
它产生稀疏的激活。假设网络的权重通过正态分布随机初始化。在这种情况下,每个 ReLU 单元的输入有 0.5 的概率小于 0。因此,大约一半的激活输出也将为 0。这种稀疏激活有几个优势,我们可以粗略地总结为在神经网络中的奥卡姆剃刀原则——用更简单的数据表示来实现相同的结果,比复杂的表示方式更好。
-
它在前向和反向传播中计算速度更快。
尽管 ReLU 有这些优势,但在训练过程中,网络权重可能会被更新到某些 ReLU 单元总是接收小于 0 的输入,从而导致它们始终输出 0。这种现象被称为 死亡 ReLU(dying ReLUs)。为了解决这个问题,已经提出了几种 ReLU 的改进方法。以下是一个非详尽的列表:
- Leaky ReLU:当输入大于 0 时,Leaky ReLU 与普通 ReLU 相同,直接输出输入值。然而,当 x < 0 时,Leaky ReLU 输出 x 与某个常数 α (0 < α < 1) 的乘积,而不是 0。下图展示了 Leaky ReLU 的公式、它的导数以及它们的图形,α = 0.2:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_6.jpg
图 3.6 – Leaky ReLU 激活函数
-
Parametric ReLU (PReLU;参见 深入探讨激活函数:超越人类水平的 ImageNet 分类,
arxiv.org/abs/1502.01852
):该激活函数与 Leaky ReLU 相同,但 α 是可调的,并且在训练过程中会进行调整。 -
指数线性单元 (ELU;见 通过指数线性单元(ELUs)进行快速准确的深度网络学习,
arxiv.org/abs/1511.07289
): 当输入大于 0 时,ELU 与 ReLU 的工作方式相同。然而,当 x < 0 时,ELU 的输出变为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/238.png,其中 α 是一个可调参数。下图展示了 ELU 公式、它的导数及其图像,适用于 α = 0.2:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_7.jpg
图 3.7 – ELU 激活函数
-
缩放指数线性单元 (SELU;见 自归一化神经网络,
arxiv.org/abs/1706.02515
): 该激活函数类似于 ELU,除了输出(大于或小于 0)通过一个附加的训练参数 λ 进行缩放。SELU 是一个更大概念的组成部分,称为 自归一化神经网络 (SNNs),这一概念在源论文中有所描述。 -
Sigmoid 线性单元 (SiLU),高斯误差线性单元 (GELU;见 高斯误差线性单元 (GELUs),
arxiv.org/abs/1606.08415
),以及 Swish(见 激活函数搜索,arxiv.org/abs/1710.05941
): 这是一个由三个相似(但不完全相同)函数组成的集合,它们与 ReLU 非常相似,但在 0 点处是可微的。为了简化,我们只展示 SiLU 的图像(σ 是 Sigmoid 函数):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_8.jpg
图 3.8 – SiLU 激活函数
最后,我们有了 softmax,它是分类问题中输出层的激活函数。假设最终网络层的输出是一个向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/239.png。每个 n 元素代表可能属于的 n 个类别之一。为了确定网络的预测结果,我们将取最大值的索引 i,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/240.png,并将输入样本分配给它所代表的类别。然而,我们也可以将网络的输出解释为离散随机变量的概率分布——即,每个值,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/241.png,代表输入样本属于特定类别的概率。为了帮助我们实现这一点,我们将使用 softmax 激活函数:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/242.png
它具有以下属性:
-
公式中的分母充当了归一化器。这对于我们刚才介绍的概率解释非常重要:
-
每个值,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/243.png,都被限制在[0, 1]的范围内,这使我们可以将其视为一个概率。
-
值的总和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/244.png 等于 1: https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/245.png,这也与概率解释相一致。
-
-
一个额外的(实际上是强制性的)条件是该函数是可微的。
-
softmax 激活函数还有一个更微妙的属性。在我们对数据进行归一化之前,我们对每个向量组件进行指数变换,变换公式为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/246.png。假设两个向量组件为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/247.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/248.png。在这种情况下,我们会得到 exp(1) = 2.7 和 exp(2) = 7.39。如我们所见,变换前后组件的比率有很大不同——0.5 和 0.36。实际上,softmax 函数增强了较高分数的概率,相对于较低的分数。
在实际应用中,softmax 常常与 交叉熵损失 函数结合使用。它比较估计的类别概率与实际类别分布之间的差异(这种差异称为交叉熵)。我们可以将单个训练样本的交叉熵损失定义如下:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/249.png
这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/250.png 是输出属于类 j(从 n 个类中) 的估计概率,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/251.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/252.png 是实际的概率。实际分布 P(X) 通常是一个独热编码向量,其中真实的类具有 1 的概率,其他所有类的概率为 0。在这种情况下,交叉熵损失函数将仅捕捉目标类的误差,并忽略其他类的误差。
现在我们已经学会了如何防止梯度消失,并且能够将神经网络输出解释为概率分布,我们将重点关注 DNN 面临的下一个挑战——过拟合。
DNN 正则化
到目前为止,我们已经了解到神经网络(NN)可以逼近任何函数。但强大的能力伴随着巨大的责任。神经网络可能会学习逼近目标函数的噪声,而不是其有用的部分。例如,假设我们正在训练一个神经网络来分类图像是否包含汽车,但由于某种原因,训练集大多数是红色的汽车。结果可能是,神经网络会将红色与汽车关联,而不是其形状。现在,如果网络在推理模式下看到一辆绿色的汽车,它可能无法识别为汽车,因为颜色不匹配。正如我们在第一章中讨论的那样,这个问题被称为过拟合,它是机器学习(ML)的核心问题(在深度网络中尤为严重)。在本节中,我们将讨论几种防止过拟合的方法。这些技术统称为正则化。
在神经网络的上下文中,这些正则化技术通常会在训练过程中施加一些人工的限制或障碍,以防止网络过度逼近目标函数。它们试图引导网络学习目标函数的一般性而非特定的逼近方式,期望这种表示能够在之前未见过的测试数据集示例上良好地泛化。让我们先从应用于输入数据的正则化技术开始,然后再将其输入到神经网络中:
-
最小-最大归一化:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/253.png。这里,x 是输入向量的单个元素,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/254.png 是训练数据集中最小的元素,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/255.png 是最大的元素。此操作将所有输入缩放到 [0, 1] 范围内。例如,一个灰度图像的最小颜色值为 0,最大颜色值为 255。然后,一个强度为 125 的像素,其缩放值为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/256.png。最小-最大归一化快速且易于实现。此归一化的一个问题是数据中的异常值可能对整个数据集的结果产生过大的影响。例如,如果一个单一的错误元素有非常大的值,它会进入公式计算中,并成为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/257.png,这将使所有归一化后的数据集值趋近于 0。
-
标准分数(或z 分数):https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/258.png。它比最小-最大方法更好地处理数据中的异常值。为了理解其原因,让我们专注于这个公式:
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/259.png 是数据集中所有元素的均值,其中 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/115.png 是输入向量中的单个元素,N 是数据集的总大小。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/261.png 是所有数据集元素的标准差。它衡量数据集的值与均值的偏离程度。还有方差, https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/262.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/263.png,它去除了标准差中的平方根。方差在理论上是正确的,但比标准差不那么直观,因为标准差与原始数据的单位相同,x。
另外,如果在整个数据集上计算μ和σ不实际,我们也可以按样本计算它们。标准分数保持数据集的均值接近 0,标准差接近 1。
-
-
数据增强:这是通过在将训练样本输入到网络之前,对其进行随机修改,从而人为地增加训练集的大小。在图像的情况下,这些修改可能包括旋转、倾斜、缩放等。
下一类正则化技术应用于 DNN 结构本身:
- 丢弃法:在这里,我们随机且周期性地从网络中去除某些层的单元(连同它们的输入和输出连接)。在每个训练小批量中,每个单元都有一个概率 p,使其随机丢弃。这样做是为了确保没有单元过度依赖其他单元,而是“学习”对神经网络有用的内容。丢弃法仅在训练阶段应用,所有单元在推理阶段都会完全参与。在下图中,我们可以看到全连接层的丢弃法:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_9.jpg
图 3.9 – 全连接层丢弃法示例
-
批量归一化(BN;参见 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,
arxiv.org/abs/1502.03167
):这是一种对网络隐藏层应用数据处理的方法,类似于标准分数。它对每个小批量(因此得名)隐藏层的输出进行归一化,使其均值接近 0(重新中心化),标准差接近 1(重新缩放)。其直观理解是,随着信息在各层之间传播,这些值可能会偏离期望值。假设小批量由 m×n 矩阵 X 表示。X 的每一行,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/264.png,表示一个单独的输入向量(该向量是前一层的输出)。https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/265.png 是第 i 个向量的第 j 个元素。我们可以通过以下方式计算每个矩阵元素的 BN:-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/266.png: 这是小批量均值。我们通过对小批量矩阵的所有单元格计算一个单一的μ值。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/267.png: 这是小批量方差。我们通过对小批量矩阵的所有单元格计算一个单一的https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/268.png值。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/269.png:我们对矩阵的每个单元格进行归一化处理。ε 是为了数值稳定性而添加的常数,这样分母就不会变为 0。
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/270.png:该公式表示原始数据的尺度和偏移。γ 和 β 是可学习的参数,我们在每个位置上计算它们,ij (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/271.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/272.png),在整个小批量矩阵的所有单元格上计算。
-
-
层归一化 (LN;见 层归一化,
arxiv.org/abs/1607.06450
):LN 类似于 BN,但有一个关键区别:均值和方差是分别在每个小批量样本上计算的。这与 BN 不同,BN 是在整个小批量上计算这些值。与 BN 一样,小批量是一个 m×n 矩阵,X,每一行向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/273.png,是前一层的输出,且 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/274.png 是第 i 个向量的第 j 个元素。那么,我们对于第 i 个输入向量有如下公式:-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/275.png
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/276.png
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/277.png
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/278.png
-
-
均方根层归一化 (RMSNorm; 参见
arxiv.org/abs/1910.07467
): RMSNorm 的作者认为,LN 的主要好处仅来自重新缩放,而不是重新中心化和重新缩放的结合。因此,RMSNorm 是 LN 的简化且更快速的版本,它只使用均方根统计量进行重新缩放。我们将使用与 LN 相同的符号。因此,我们可以将 RMSNorm 定义如下:-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/279.png.
-
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/280.png: 这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/281.png 是增益参数,用于重新缩放标准化的输入和求和(初始值为 1)。它等同于 BN 中的 γ 参数。
-
下图说明了 BN 和 LN 之间的差异。在左侧,我们计算整个小批量中单个 μ 和 σ 值。右侧,我们可以看到每行分别为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/282.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/283.png:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_10.jpg
图 3.10 – BN 和 LN 计算 μ 和 σ
我们将介绍的最终正则化类型是L2 正则化。该技术在成本函数中添加一个特殊的正则化项。为了理解它,让我们以 MSE 成本为例。我们可以通过以下方式将 L2 正则化添加到其中(公式中的下划线部分):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/284.png
这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/165.png 是 m 个总网络权重之一,λ 是权重衰减系数。其原理是,如果网络权重,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/174.png,较大,那么代价函数也会增大。实际上,权重衰减会惩罚大权重(因此得名)。这可以防止网络过度依赖与这些权重相关的少数特征。当网络被迫使用多个特征时,过拟合的机会会减少。实际操作中,当我们计算权重衰减代价函数(前面的公式)对每个权重的导数并将其传播到权重本身时,权重更新规则发生如下变化:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/287.png 到 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/288.png
通过对 DNN 正则化的讨论,我们已经涵盖了理论基础。接下来,让我们看看 DNN 的实际应用是什么。
深度学习的应用
一般来说,机器学习(ML),尤其是深度学习(DL),在预测质量、特征检测和分类方面取得了越来越令人惊叹的结果。许多这些最新的成果已经成为新闻头条。进步的速度如此之快,以至于一些专家担心机器很快会比人类更聪明。但我希望你在读完这本书后,能够缓解你可能有的任何这种担忧。无论如何,机器仍然远未达到人类级别的智能。
在第二章中,我们提到过深度学习算法如何占据了 ImageNet 竞赛的领先位置。它们的成功足以从学术界跳跃到工业界。
让我们来看看深度学习的一些实际应用案例:
-
如今,新的汽车配备了一系列安全性和便利性功能,旨在使驾驶体验更安全、更轻松。其中一项功能是自动紧急刹车系统,当汽车看到障碍物时会自动刹车。另一个功能是车道保持辅助,它可以让车辆在不需要驾驶员操控方向盘的情况下保持当前车道。为了识别车道标记、其他车辆、行人和骑行者,这些系统使用了
前置摄像头。该领域最著名的供应商之一,Mobileye (
www.mobileye.com/
),已经生产了定制芯片,使用卷积神经网络(CNN)来检测前方道路上的物体。为了让你了解这个行业的重要性,2017 年,英特尔以 153 亿美元收购了 Mobileye。这并非个案,特斯拉著名的自动驾驶系统也依赖 CNN 来实现相同的效果。特斯拉前 AI 总监安德烈·卡帕西(Andrej Karpathy)(karpathy.ai/
)是深度学习领域的知名研究者。我们可以推测,未来的自动驾驶汽车也将使用深度网络进行计算机视觉。 -
谷歌的视觉 API (
cloud.google.com/vision/
) 和亚马逊的Rekognition (aws.amazon.com/rekognition/
) 服务都使用深度学习模型提供各种计算机视觉能力。这些功能包括图像中的物体和场景识别、文本识别、人脸识别、内容审核等。 -
如果这些 API 不够,你还可以在云端运行自己的模型。例如,你可以使用亚马逊的 AWS DL AMI(即亚马逊机器镜像;见
aws.amazon.com/machine-learning/amis/
),这些是预配置了流行深度学习库的虚拟机。谷歌也提供类似的服务,通过其 Cloud AI(cloud.google.com/products/ai/
),但他们更进一步。他们创建了张量处理单元(TPUs;见 https://cloud.google.com/tpu/)——这是一种专为快速神经网络操作(如矩阵乘法和激活函数)优化的微处理器。 -
深度学习(DL)在医学应用中具有很大的潜力。然而,严格的监管要求以及患者数据的保密性限制了其普及。尽管如此,我们仍然可以识别出深度学习在以下几个领域可能产生重大影响:
-
医学影像学是指多种非侵入性方法,用于创建身体内部的视觉表现。其中包括磁共振影像(MRIs)、超声、计算机断层扫描(CAT)扫描、X 射线和组织学影像。通常,这些图像需要由医学专业人员分析,以确定患者的病情。
-
计算机辅助诊断,特别是计算机视觉,可以通过检测和突出显示图像中的重要特征来帮助专家。例如,为了确定结肠癌的恶性程度,病理学家需要使用组织学影像分析腺体的形态学。这是一项具有挑战性的任务,因为形态学可能有很大的变化。深度神经网络(DNN)可以自动从图像中分割出腺体,剩下的工作由病理学家来验证结果。这将减少分析所需的时间,使得分析更加廉价且更易获得。
-
另一个可以受益于深度学习的医学领域是病历记录的分析。当医生诊断患者的病情并开具治疗方案时,他们首先会查阅患者的病历。深度学习算法可以从这些记录中提取最相关和最重要的信息,即使它们是手写的。这样,医生的工作将变得更轻松,同时也减少了错误的风险。
-
深度神经网络(DNNs)已在一个领域取得了显著的影响——蛋白质折叠。蛋白质是大型复杂分子,其功能取决于其三维结构。蛋白质的基本构件是氨基酸,其序列决定了蛋白质的形状。蛋白质折叠问题旨在理解初始氨基酸序列与蛋白质最终三维结构之间的关系。DeepMind 的AlphaFold 2模型(据信基于变换器架构;见
www.deepmind.com/blog/alphafold-reveals-the-structure-of-the-protein-universe
)成功预测了 2 亿种蛋白质结构,这几乎涵盖了所有已知的已编目蛋白质。
-
-
Google 的神经机器翻译 API (
arxiv.org/abs/1609.08144
) 使用了——你猜对了——深度神经网络(DNNs)进行机器翻译。 -
Siri (
machinelearning.apple.com/2017/10/01/hey-siri.html
)、Google 助手和 Amazon Alexa (aws.amazon.com/deep-learning/
) 依赖深度网络进行语音识别。 -
AlphaGo 是基于深度学习(DL)的人工智能机器,它在 2016 年通过战胜世界围棋冠军李世石而成为新闻焦点。AlphaGo 在 2016 年 1 月就已经引起了媒体的关注,当时它击败了欧洲围棋冠军范睿。然而,当时似乎不太可能击败世界冠军。几个月后,AlphaGo 以 4-1 的胜利系列取得了这一非凡成就。这是一个重要的里程碑,因为围棋有比其他棋类(如国际象棋)更多的可能变化,而且在事先很难考虑所有可能的走法。此外,与国际象棋不同,在围棋中,即便是判断棋盘上单颗棋子的当前局势或价值也非常困难。2017 年,DeepMind 发布了 AlphaGo 的更新版AlphaZero (
arxiv.org/abs/1712.01815
),而在 2019 年,他们发布了一个进一步更新的版本,名为MuZero (arxiv.org/abs/1911.08265
)。 -
像 GitHub Copilot (
github.com/features/copilot
) 和 ChatGPT (chat.openai.com/
)这样的工具利用生成型深度神经网络模型将自然语言请求转化为源代码片段、函数或完整的程序。我们之前提到的 Stable Diffusion (stability.ai/blog/stable-diffusion-public-release
) 和 DALL-E (openai.com/dall-e-2/
),则能够根据文本描述生成逼真的图像。
在这个简短的列表中,我们旨在涵盖深度学习(DL)应用的主要领域,如计算机视觉、自然语言处理(NLP)、语音识别和强化学习(RL)。然而,这个列表并不详尽,因为深度学习算法还有许多其他的应用。不过,我希望这些内容足以激发你的兴趣。接下来,我们将正式介绍两个最受欢迎的深度学习库——PyTorch 和 Keras。
介绍流行的深度学习库
我们已经在第一章中实现了一个使用 PyTorch 的简单示例。在本节中,我们将更系统地介绍该库以及 Keras。我们从大多数深度神经网络(DNN)库的共同特点开始:
-
所有库都使用 Python。
-
数据存储的基本单元是张量。从数学角度来看,张量的定义更加复杂,但在深度学习库的语境中,张量是多维的(具有任意数量的轴)基本值数组。
-
神经网络(NN)表示为计算图,图中的节点代表操作(加权求和、激活函数等)。边缘代表数据流动,数据如何从一个操作的输出作为下一个操作的输入。操作的输入和输出(包括网络的输入和输出)都是张量。
-
所有库都包含自动微分功能。这意味着你只需要定义网络架构和激活函数,库将自动计算训练过程中反向传播所需的所有导数。
-
到目前为止,我们提到了 GPU,但实际上,绝大多数深度学习项目仅使用 NVIDIA GPU。这是因为 NVIDIA 提供了更好的软件支持。这些库也不例外——为了实现 GPU 操作,它们依赖于 CUDA 工具包(
developer.nvidia.com/cuda-toolkit
)和 cuDNN 库(developer.nvidia.com/cudnn
)。cuDNN 是 CUDA 的扩展,专为深度学习应用构建。如在深度学习应用部分所提到的,你也可以在云端运行你的深度学习实验。
PyTorch 是一个独立的库,而 Keras 则建立在 TF 之上,作为一个用户友好的 TF 接口。接下来,我们将使用 PyTorch 和 Keras 实现一个简单的分类示例。
使用 Keras 进行数字分类
Keras 作为独立库存在,其中 TF 作为后端,也可以作为 TF 本身的子组件使用。你可以选择这两种方式之一。若要将 Keras 作为 TF 的一部分使用,我们只需安装 TF 本身。安装完成后,我们可以通过以下导入使用该库:
import tensorflow.keras
独立版 Keras 支持除 TF 外的不同后端,如 Theano。在这种情况下,我们可以安装 Keras 本身,然后通过以下导入来使用它:
import keras
Keras 的大多数使用都基于 TF 后端。Keras 的作者推荐将该库作为 TF 的一个组件使用(即第一种方式),我们将在本书的其余部分遵循这个方式。
在这一节中,我们将通过 TF 使用 Keras 来分类 MNIST 数据集的图像。该数据集包含了 70,000 个由不同人手写的数字示例。前 60,000 个通常用于训练,剩下的 10,000 个用于测试:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_11.jpg
图 3.11 – 从 MNIST 数据集中提取的数字样本
我们将构建一个简单的多层感知机(MLP),并且只包含一个隐藏层。让我们开始:
-
Keras 的一个优点是它可以为你导入这个数据集,而不需要你显式地从网上下载(它会为你自动下载):
import tensorflow as tf (X_train, Y_train), (X_validation, Y_validation) = \ tf.keras.datasets.mnist.load_data()
这里,
(X_train, Y_train)
是训练图像和标签,(X_validation, Y_validation)
是测试图像和标签。 -
我们需要修改数据,以便将其输入到神经网络中。
X_train
包含 60,000 张 28×28 像素的图像,X_validation
包含 10,000 张。为了将它们作为输入提供给网络,我们希望将每个样本重塑为一个 784 像素长度的数组,而不是 28×28 的二维矩阵。我们还会将其归一化到[0:1]的范围内。我们可以通过以下两行代码来实现:X_train = X_train.reshape(60000, 784) / 255 X_validation = X_validation.reshape(10000, 784) / 255
-
标签表示图像中数字的值。我们希望将其转换为一个包含 0 和一个 1 的 10 维独热编码向量,其中 1 出现在与数字对应的索引位置。例如,4 被映射为[0, 0, 0, 0, 1, 0, 0, 0, 0, 0]。相应地,我们的网络将有 10 个输出单元:
classes = 10 Y_train = tf.keras.utils.to_categorical(Y_train, classes) Y_validation = tf.keras.utils.to_categorical( Y_validation, classes)
-
定义神经网络。在这个例子中,我们将使用
Sequential
模型,其中每一层都是下一层的输入。在 Keras 中,Dense
表示全连接层。我们将使用一个包含 100 个单元、BN、ReLU 激活函数和 softmax 输出的隐藏层:from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, BatchNormalization, Activation input_size = 784 hidden_units = 100 model = Sequential([ Dense( hidden_units, input_dim=input_size), BatchNormalization(), Activation('relu'), Dense(classes), Activation('softmax') ])
-
现在,我们可以定义梯度下降的参数。我们将使用 Adam 优化器和分类交叉熵损失函数(这是针对 softmax 输出优化的交叉熵):
model.compile( loss='categorical_crossentropy', metrics=['accuracy'], optimizer='adam')
-
接下来,运行 100 轮训练,批次大小为 100。在 Keras 中,我们可以使用
fit
方法,它会在内部遍历整个数据集。Keras 默认使用 GPU 进行训练,但如果没有可用的 GPU,它会回退到 CPU:model.fit(X_train, Y_train, batch_size=100, epochs=20, verbose=1)
-
剩下的就是添加代码以评估网络在测试数据上的准确性:
score = model.evaluate(X_validation, Y_validation, verbose=1) print('Validation accuracy:', score[1])
就这样。验证准确率大约为 97.7%,虽然结果不算非常优秀,但这个示例在 CPU 上运行不到 30 秒。我们可以做一些简单的改进,比如增加更多的隐藏单元,或者增加更多的训练轮数。我们将把这些实验留给你,以便你能熟悉代码。
-
为了查看网络学到了什么,我们可以可视化隐藏层的权重。以下代码可以帮助我们获取它们:
weights = model.layers[0].get_weights()
-
将每个单元的权重重塑为 28×28 的二维数组,然后显示它们:
import matplotlib.pyplot as plt import matplotlib.cm as cm import numpy fig = plt.figure() w = weights[0].T for unit in range(hidden_units): ax = fig.add_subplot(10, 10, unit + 1) ax.axis("off") ax.imshow(numpy.reshape(w[unit], (28, 28)), cmap=cm.Greys_r) plt.show()
我们可以在下图中看到结果:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_03_12.jpg
图 3.12 – 所有隐藏单元学习到的复合图
现在,让我们看一下 PyTorch 的示例。
使用 PyTorch 进行数字分类
在这一部分,我们将实现与使用 Keras 进行数字分类部分中相同的示例,但这次使用 PyTorch。让我们开始:
-
首先,我们将选择我们正在使用的设备(CPU 或 GPU)。我们将首先尝试 GPU,如果 GPU 不可用,则回退到 CPU:
import torch device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
-
和 Keras 一样,PyTorch 也开箱即用支持 MNIST。以下是如何实例化训练集和验证集的方法:
from torchvision import datasets from torchvision.transforms import ToTensor, Lambda, Compose train_data = datasets.MNIST( root='data', train=True, transform=Compose( [ToTensor(), Lambda(lambda x: torch.flatten(x))]), download=True, ) validation_data = datasets.MNIST( root='data', train=False, transform=Compose( [ToTensor(), Lambda(lambda x: torch.flatten(x))]), )
数据集会自动下载并分为训练集和验证集。
ToTensor()
转换将图像从numpy
数组转换为 PyTorch 张量,并将其标准化到[0:1]范围内(而非原来的[0:255])。torch.flatten
变换将二维 28×28 的图像展平成一维的 784 个元素,以便我们将其传递给神经网络。 -
接下来,我们将数据集封装在
DataLoader
实例中:from torch.utils.data import DataLoader train_loader = DataLoader( dataset=train_data, batch_size=100, shuffle=True) validation_loader = DataLoader( dataset=validation_data, batch_size=100, shuffle=True)
数据
DataLoader
实例负责创建小批量并随机打乱数据。它们也是迭代器,一次提供一个小批量。 -
然后,我们将定义神经网络
model
。我们将使用与 Keras 示例中相同的具有单一隐藏层的 MLP:torch.manual_seed(1234) hidden_units = 100 classes = 10 model = torch.nn.Sequential( torch.nn.Linear(28 * 28, hidden_units), torch.nn.BatchNorm1d(hidden_units), torch.nn.ReLU(), torch.nn.Linear(hidden_units, classes), )
这个定义类似于 Keras 中的定义。唯一的区别是,
Linear
(全连接)层需要输入和输出维度,因为它们无法自动提取前一层的输出维度。激活函数被定义为独立的操作。 -
接下来,让我们定义交叉熵损失和 Adam 优化器:
cost_func = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters())
-
现在,我们可以定义
train_model
函数,正如其名字所示,该函数负责训练模型。它接受我们预定义的model
、cost_function
、optimizer
和data_loader
并运行一个 epoch 的训练:def train_model(model, cost_function, optimizer, data_loader): # send the model to the GPU model.to(device) # set model to training mode model.train() current_loss = 0.0 current_acc = 0 # iterate over the training data for i, (inputs, labels) in enumerate(data_loader): # send the input/labels to the GPU inputs = inputs.to(device) labels = labels.to(device) # zero the parameter gradients optimizer.zero_grad() with torch.set_grad_enabled(True): # forward outputs = model(inputs) _, predictions = torch.max(outputs, 1) loss = cost_function(outputs, labels) # backward loss.backward() optimizer.step() # statistics current_loss += loss.item() * inputs.size(0) current_acc += torch.sum(predictions == labels.data) total_loss = current_loss / len(data_loader.dataset) total_acc = current_acc.double() / len(data_loader.dataset) print('Train Loss: {:.4f}; Accuracy: / {:.4f}'.format(total_loss, total_acc))
与 Keras 及其
fit
函数不同,我们需要自己实现 PyTorch 的训练过程。train_model
会遍历由train_loader
提供的所有小批量数据。对于每个小批量,optimizer.zero_grad()
会重置前一次迭代的梯度。然后,我们开始前向传播和反向传播,最后进行权重更新。 -
我们还将定义
test_model
函数,它将在推理模式下运行模型以检查其结果:def test_model(model, cost_function, data_loader): # send the model to the GPU model.to(device) # set model in evaluation mode model.eval() current_loss = 0.0 current_acc = 0 # iterate over the validation data for i, (inputs, labels) in enumerate(data_loader): # send the input/labels to the GPU inputs = inputs.to(device) labels = labels.to(device) # forward with torch.set_grad_enabled(False): outputs = model(inputs) _, predictions = torch.max(outputs, 1) loss = cost_function(outputs, labels) # statistics current_loss += loss.item() * inputs.size(0) current_acc += torch.sum(predictions == labels.data) total_loss = current_loss / len(data_loader.dataset) total_acc = current_acc.double() / len(data_loader.dataset) print('Test Loss: {:.4f}; Accuracy: / {:.4f}'.format(total_loss, total_acc)) return total_loss, total_acc
BN 和 dropout 层在评估时不会使用(只在训练时使用),因此
model.eval()
会关闭它们。我们遍历验证集,启动前向传播,并汇总验证损失和准确率。 -
让我们运行训练 20 个 epoch:
for epoch in range(20): train_model(model, cost_func, optimizer, train_loader) test_model(model, cost_func, validation_loader)
该模型实现了 97.6%的准确率。
总结
在这一章中,我们解释了什么是深度学习(DL),以及它与深度神经网络(DNNs)之间的关系。我们讨论了不同类型的 DNN 及其训练方法,并特别关注了帮助训练过程的各种正则化技术。我们还提到了许多深度学习的实际应用,并尝试分析它们为何如此高效。最后,我们介绍了两种最流行的深度学习库——PyTorch 和 Keras,并用这两个库实现了相同的 MNIST 分类示例。
在下一章,我们将讨论如何借助卷积神经网络解决更复杂的图像数据集上的分类任务——这是最流行且最有效的深度网络模型之一。我们将探讨其结构、构建模块,以及是什么让它们特别适合于计算机视觉任务。为了激发你的兴趣,我们回顾一下,自 2012 年以来,卷积神经网络一直在热门的 ImageNet 挑战赛中获胜,连续多年保持前五名的准确率,从 74.2%提升到 99%。
第二部分:
深度神经网络在计算机视觉中的应用
在这一部分,我们将介绍卷积神经网络(CNNs)——一种适用于计算机视觉应用的神经网络类型。在前面三章的基础上,我们将讨论 CNN 的基本原理、构建模块以及其架构。我们还将概述当前最流行的 CNN 模型。最后,我们将重点讲解 CNN 的高级应用——目标检测、图像分割和图像生成。
这一部分包含以下章节:
-
第四章,使用卷积网络进行计算机视觉
-
第五章,计算机视觉的高级应用
第四章:使用卷积网络进行计算机视觉
在第二章和第三章中,我们对深度学习(DL)和计算机视觉设定了很高的期望。首先,我们提到了 ImageNet 竞赛,然后讨论了它的一些令人兴奋的现实世界应用,例如半自动驾驶汽车。在本章及接下来的两章中,我们将实现这些期望。
视觉可以说是人类最重要的感官。我们几乎在进行的每一个动作中都依赖于它。但图像识别(并且在某些方面仍然是)长期以来一直是计算机科学中最困难的问题之一。历史上,很难向机器解释构成特定对象的特征,以及如何检测它们。但正如我们所见,在深度学习中,神经网络(NN)可以自己学习这些特征。
在本章中,我们将讨论以下主题:
-
卷积神经网络(CNNs)的直觉和理论依据
-
卷积层
-
池化层
-
卷积网络的结构
-
使用 PyTorch 和 Keras 对图像进行分类
-
卷积的高级类型
-
高级 CNN 模型
技术要求
我们将在本章中使用 Python、PyTorch 和 Keras 来实现这个示例。如果你还没有设置这些工具的环境,不必担心——这个示例已经作为 Jupyter Notebook 在 Google Colab 上提供。你可以在本书的 GitHub 仓库中找到代码示例:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter04
。
CNN 的直觉和理论依据
我们从感官输入中提取的信息通常取决于它们的上下文。对于图像,我们可以假设相邻的像素是密切相关的,当将它们作为一个整体来看时,它们的集合信息更为重要。相反,我们可以假设单独的像素并不传递相互之间相关的信息。例如,在识别字母或数字时,我们需要分析相邻像素之间的依赖关系,因为它们决定了元素的形状。通过这种方式,我们能够区分,例如,0 和 1 之间的区别。图像中的像素被组织成二维网格,如果图像不是灰度图,我们还会有一个用于颜色通道的第三维度。
另外,磁共振成像(MRI)也使用三维空间。你可能还记得,直到现在,如果我们想要将图像输入到神经网络中,我们必须将它从二维数组转换为一维数组。卷积神经网络就是为了解决这个问题而构建的:如何使得来自更近单元的信息比来自更远单元的信息更相关。在视觉问题中,这意味着让单元处理来自彼此接近的像素的信息。通过 CNNs,我们将能够输入一维、二维或三维数据,网络也将输出相同维度的数据。正如我们稍后会看到的,这将为我们带来几个优势。
你可能还记得,在上一章的结尾,我们成功地对 MNIST 图像进行了分类(准确率约为 98%),使用的神经网络包括了飞机
、汽车
、鸟
、猫
、鹿
、狗
、青蛙
、马
、船
和卡车
。如果我们尝试使用一个具有一个或多个隐藏层的全连接神经网络(FC NN)来对 CIFAR-10 进行分类,其验证准确率大概会只有 50%左右(相信我,我们在本书的上一版中确实这么做过)。与接近 98% 准确率的 MNIST 结果相比,这是一个显著的差异,即使 CIFAR-10 也是一个简单的玩具问题。因此,全连接神经网络对于计算机视觉问题的实际应用价值较小。为了理解原因,我们来分析一下我们假设中的 CIFAR-10 网络的第一个隐藏层,该层有 1,000 个单元。图像的输入大小是
32 * 32 * 3 = 3,072。因此,第一个隐藏层总共有 2,072 * 1,000 = 2,072,000 个权重。这可不是一个小数字!不仅如此,这么大的网络容易过拟合,而且在内存上也效率低下。
更为重要的是,每个输入单元(或像素)都与隐藏层中的每个单元相连。正因如此,网络无法利用像素的空间接近性,因为它无法知道哪些像素是彼此接近的。相比之下,卷积神经网络(CNNs)具有一些特性,能够有效地解决这些问题:
-
它们仅连接与图像相邻像素对应的单元。通过这种方式,这些单元被“迫使”只从空间上接近的其他单元那里获取输入。这样也减少了权重的数量,因为并非所有单元都是互相连接的。
-
卷积神经网络(CNNs)使用参数共享。换句话说,层中的所有单元共享有限数量的权重。这进一步减少了权重的数量,并有助于防止过拟合。虽然这可能听起来有些混乱,但在下一节中会变得更加清晰。
注意
在本章中,我们将在计算机视觉的背景下讨论 CNN,因为计算机视觉是其最常见的应用。然而,CNN 也成功应用于语音识别和自然语言处理(NLP)等领域。我们在此描述的许多解释同样适用于这些领域——即,无论应用领域如何,CNN 的原理都是相同的。
为了理解 CNN,我们首先讨论它们的基本构建块。一旦完成这部分,我们将展示如何将它们组装成一个完整的神经网络。接着,我们将展示该网络足够好,能够以高精度分类 CIFAR-10。最后,我们将讨论高级 CNN 模型,这些模型可以应用于实际的计算机视觉任务。
卷积层
卷积层是卷积神经网络(CNN)最重要的组成部分。它由一组滤波器(也称为内核或特征检测器)组成,每个滤波器都应用于输入数据的所有区域。滤波器由一组可学习的权重定义。
为了给这个简洁的定义增加一些意义,我们将从以下图开始:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_01.jpg
图 4.1 – 卷积操作开始
上图展示了 CNN 的二维输入层。为了简化说明,我们假设这是输入层,但它也可以是网络的任何一层。我们还假设输入是一个灰度图像,每个输入单元代表一个像素的颜色强度。这个图像由一个二维张量表示。
我们将通过在图像的左上角应用一个 3×3 权重滤波器(同样是一个二维张量)来开始卷积操作。每个输入单元与滤波器的一个权重相关联。因为有九个输入单元,所以权重有九个,但一般来说,大小是任意的(例如 2×2、4×4、5×5,等等)。卷积操作被定义为以下加权和:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/289.png
这里,row 和 col 表示输入层的位置,在此处应用滤波器(row=1 和 col=1 在前述图中); https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/290.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/291.png 是滤波器大小(3×3)的高度和宽度; i 和 j 是每个滤波器权重的索引,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/292.png; b 是偏置权重。参与输入的单元组,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/293.png,参与输入的单元组称为感受野。
我们可以看到,在卷积层中,单元的激活值与我们在第二章中定义的单元激活值的计算方式相同——即输入的加权和。但在这里,单元的输入仅来自其周围有限数量的输入单元(感受野)。这与全连接(FC)层不同,在全连接层中,输入来自所有输入单元。这个区别很重要,因为滤波器的目的是突出输入中的某个特定特征,比如图像中的边缘或线条。在神经网络的上下文中,滤波器的输出代表下一层单元的激活值。如果该特征在此空间位置存在,单元将处于激活状态。在层次结构的数据中,如图像,邻近像素会形成有意义的形状和物体,如边缘或线条。然而,图像一端的像素与另一端的像素不太可能存在关系。因此,使用全连接层将所有输入像素与每个输出单元连接,就像让网络在大海捞针。它无法知道某个输入像素是否与输出单元相关(是否位于周围区域),还是与图像另一端的像素无关。因此,卷积层有限的感受野更适合突出输入数据中的有意义特征。
我们已经计算了一个单元的激活值,但其他单元呢?很简单!对于每个新单元,我们会将滤波器滑动到输入图像上,并计算其输出(加权和),每次使用一组新的输入单元。下图展示了如何计算接下来两个位置的激活值(右移一个像素):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_02.jpg
图 4.2 – 卷积操作的前三个步骤
所谓“滑动”,是指滤波器的权重在整个图像上保持不变。实际上,我们会使用相同的九个滤波器权重和一个偏置权重来计算所有输出单元的激活值,每次使用不同的输入单元集。我们称之为参数共享,并且这样做有两个原因:
-
通过减少权重的数量,我们减少了内存占用并防止了过拟合。
-
滤波器突出了图像中的特定视觉特征。我们可以假设该特征是有用的,无论它在图像中的位置如何。由于我们在整个图像中应用相同的滤波器,因此卷积具有平移不变性;也就是说,它可以检测到相同的特征,无论该特征在图像中的位置如何。然而,卷积既不是旋转不变的(如果特征被旋转,它不一定能检测到该特征),也不是尺度不变的(它不能保证在不同的尺度下检测到相同的特征)。
为了计算所有输出激活值,我们将重复滑动过程,直到覆盖整个输入。空间排列的输入和输出单元被称为深度切片(特征图或通道),意味着不仅仅有一个切片。切片和图像一样,是由张量表示的。切片张量可以作为网络中其他层的输入。最后,就像常规层一样,我们可以在每个单元后使用激活函数,如修正线性单元(ReLU)。
注意
有趣的是,每个输入单元都是多个输出单元的输入。例如,当我们滑动滤波器时,上图中的绿色单元将作为九个输出单元的输入。
我们可以用一个简单的例子来说明迄今为止所学的内容。以下图示说明了如何对单个 3×3 切片应用 2×2 滤波器进行 2D 卷积:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_03.jpg
图 4.3 – 使用 2×2 滤波器对单个 3×3 切片进行 2D 卷积,以获得 2×2 输出切片
这个例子还向我们展示了输入和输出特征图的尺寸是不同的。假设我们有一个大小为(width_i, height_i)
的输入层和一个尺寸为(filter_w, filter_h)
的滤波器。应用卷积后,输出层的尺寸为width_o = width_i - filter_w + 1
和height_o = height_i - filter_h + 1
。
在这个例子中,我们有width_o = height_o = 3 – 2 + 1 =
2
。
在接下来的部分,我们将通过一个简单的编码示例来说明卷积操作。
卷积操作的代码示例
我们现在已经描述了卷积层是如何工作的,但通过一个可视化的例子,我们会更好地理解。让我们通过对图像应用几个滤波器来实现卷积操作。为了清晰起见,我们将手动实现滤波器在图像上的滑动,且不使用任何深度学习库。我们只会包括相关部分,而不是完整程序,但你可以在本书的 GitHub 仓库中找到完整示例。让我们开始:
-
导入
numpy
:import numpy as np
-
定义
conv
函数,它对图像应用卷积。conv
接受两个参数,都是二维 numpy 数组:image
表示灰度图像本身的像素强度,im_filter
表示硬编码的滤波器:-
首先,我们将计算输出图像的大小,它取决于输入
image
和im_filter
的大小。我们将利用它来实例化输出图像im_c
。 -
然后,我们将对
image
的所有像素进行迭代,在每个位置应用im_filter
。此操作需要四个嵌套循环:前两个循环处理image
的维度,后两个循环用于迭代二维滤波器。 -
我们将检查是否有任何值超出[0, 255]的区间,并在必要时进行修正。
如下例所示:
def conv(image, im_filter): # input dimensions height = image.shape[0] width = image.shape[1] # output image with reduced dimensions im_c = np.zeros((height - len(im_filter) + 1, width - len(im_filter) + 1)) # iterate over all rows and columns for row in range(len(im_c)): for col in range(len(im_c[0])): # apply the filter for i in range(len(im_filter)): for j in range(len(im_filter[0])): im_c[row, col] += image[row + i, / col + j] * im_filter[i][j] # fix out-of-bounds values im_c[im_c > 255] = 255 im_c[im_c < 0] = 0 return im_c
-
-
在图像上应用不同的滤波器。为了更好地说明我们的观点,我们将使用一个 10×10 的模糊滤波器,以及 Sobel 边缘检测器,如下例所示(
image_grayscale
是一个二维的numpy
数组,表示灰度图像的像素强度):# blur filter blur = np.full([10, 10], 1\. / 100) conv(image_grayscale, blur) # sobel filters sobel_x = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]] conv(image_grayscale, sobel_x) sobel_y = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]] conv(image_grayscale, sobel_y)
完整程序将产生以下输出:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_04.jpg
图 4.4 – 第一张图是灰度输入图像。第二张图是 10×10 模糊滤波器的结果。第三和第四张图使用了检测器和垂直 Sobel 边缘检测器
在这个例子中,我们使用了带有硬编码权重的滤波器来可视化卷积操作是如何在神经网络中工作的。实际上,滤波器的权重将在网络训练过程中设置。我们只需要定义网络架构,比如卷积层的数量、输出体积的深度以及滤波器的大小。网络将在训练过程中自动确定每个滤波器突出显示的特征。
注意
正如我们在这个例子中看到的,我们需要实现四个嵌套循环来实现卷积。然而,通过一些巧妙的转换,卷积操作可以通过矩阵乘法实现。这样,它可以充分利用 GPU 并行计算。
在接下来的几节中,我们将讨论卷积层的一些细节。
跨通道和深度卷积
到目前为止,我们已经描述了一个一对一的切片关系,其中我们在单个输入切片上应用单个滤波器,产生单个输出切片。但这种安排有以下局限性:
-
单个输入切片适用于灰度图像,但对于具有多个通道的彩色图像或任何其他多维输入则不起作用
-
单个滤波器可以检测切片中的单个特征,但我们希望检测多个不同的特征
如何解决这些局限性呢?很简单:
- 对于输入,我们将图像分割成颜色通道。对于 RGB 图像来说,这将是三个通道。我们可以将每个颜色通道看作一个深度切片,其中的值是给定颜色(R、G 或 B)的像素强度,如下例所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_05.jpg
图 4.5 – 一个深度为 3 的输入切片示例
输入切片的组合被称为输入体积,深度为 3。RGB 图像由三层 2D 切片(每个颜色通道一个)组成的 3D 张量表示。
- CNN 卷积可以拥有多个滤波器,突出显示不同的特征,从而产生多个输出特征图(每个滤波器一个),这些特征图被合并成一个输出体积。
假设我们有https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/294.png输入(大写的C)和https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/295.png输出切片。根据输入和输出切片的关系,我们可以得到跨通道卷积和深度卷积,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_06.jpg
图 4.6 – 跨通道卷积(左);深度卷积(右)
让我们讨论它们的性质:
- 跨通道卷积:一个输出切片接收所有输入切片的输入 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/296.png 关系)。有了多个输出切片,关系变为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/297.png。换句话说,每个输入切片都为每个输出切片的输出做出贡献。每一对输入/输出切片使用一个独立的过滤器切片,这个过滤器切片对该对切片是独有的。我们用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/298.png(小写 c)表示输入切片的索引;用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/299.png 表示输出切片的索引;用 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/300.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/301.png 表示过滤器的维度。然后,单个输出切片中一个输出单元的跨通道二维卷积定义为以下加权和:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/302.png
请注意,我们有一个独特的偏置,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/303.png 每个输出切片都有一个。
我们还可以通过以下公式计算交叉通道 2D 卷积中的权重总数 W:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/304.png
这里,+1 表示每个滤波器的偏置权重。假设我们有三个输入切片,并且想要应用四个 5×5 的滤波器。如果这样做,卷积滤波器将有总共 (3 * 5 * 5 + 1) * 4 = 304 个权重,四个输出切片(深度为 4 的输出体积),每个切片有一个偏置。每个输出切片的滤波器将有三个 5×5 的滤波器块,分别对应三个输入切片,并且有一个偏置,总共有 3 * 5 * 5 + 1 = 76 个权重。
- 深度卷积:一个输出切片仅接收来自单个输入切片的信息。这是对前述情况的某种反转。在最简单的形式下,我们对单个输入切片应用滤波器,生成一个输出切片。在这种情况下,输入和输出的体积具有相同的深度——即 C。我们还可以指定一个通道倍增器(一个整数 M),即对单个输出切片应用 M 个滤波器,生成每个输入切片的 M 个输出切片。在这种情况下,输出切片的总数为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/305.png。深度卷积 2D 被定义为以下加权和:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/306.png
我们可以通过以下公式计算二维深度卷积中的权重W:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/307.png
这里,+M 代表每个输出片的偏置。
接下来,我们将讨论卷积操作的一些更多属性。
卷积层中的步幅和填充
到目前为止,我们假设滤波器每次滑动一个像素,但并非总是如此。我们可以让滤波器滑动多个位置。卷积层的这个参数叫做步幅。通常,步幅在输入的所有维度上是相同的。在下图中,我们可以看到一个步幅 = 2的卷积层(也叫做步幅卷积):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_07.jpg
图 4.7 – 步幅 = 2 时,滤波器每次移动两个像素
更大步幅的主要效果是增加输出单元的感受野,代价是输出切片的大小本身会变小。为了理解这一点,我们回顾一下在上一节中,我们介绍了一个输出大小的简单公式,其中包括输入和卷积核的大小。现在,我们将它扩展到包括步幅:width_o = (width_i - filter_w) / stride_w + 1
和 height_o = 1 + (height_i - filter_h) / stride_h
。例如,一个由 28×28 的输入图像生成的方形切片,与一个 3×3 的滤波器进行卷积,且步幅 = 1,输出大小为 1 + 28 - 3 = 26。 但是如果步幅 = 2,则为 1 + (28 - 3) / 2 = 13。因此,如果我们使用步幅 = 2,输出切片的大小大约是输入的四分之一。换句话说,一个输出单元将“覆盖”一个面积,比输入单元大四倍。接下来层的单元将逐渐捕捉来自输入图像更大区域的输入信息。这一点非常重要,因为它将允许它们检测输入图像中更大、更复杂的特征。
到目前为止,我们讨论的卷积操作产生的输出小于输入(即使步幅 = 1)。但是,在实践中,通常希望控制输出的大小。我们可以通过填充输入切片的边缘,用零的行和列进行填充,来解决这个问题。在以下图示中,我们可以看到一个填充大小为 1 且步幅 = 1的卷积层:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_08.png
图 4.8 – 填充大小为 1 的卷积层
白色单元表示填充。输入和输出切片的维度相同(深色单元)。新填充的零将在卷积操作中与切片一起参与,但它们不会影响结果。原因是,即使填充区域与权重连接到下一层,我们始终会将这些权重乘以填充值,而填充值为 0。同时,滑动滤波器经过填充的输入切片时,会产生与未填充输入相同维度的输出切片。
现在我们已经了解了步幅和填充的概念,我们可以引入输出切片大小的完整公式:
height_o = 1 + (height_i + 2*padding_h – filter_h) / stride
width_o = 1 + (width_i + 2*padding_w – filter_w) / stride
我们现在已经对卷积有了基本的了解,可以继续学习卷积神经网络(CNN)的下一个基本组成部分——池化层。一旦我们了解了池化层的原理,我们将介绍第一个完整的 CNN,并通过实现一个简单的任务来巩固我们的知识。接着,我们将聚焦于更高级的 CNN 话题。
池化层
在上一节中,我们解释了如何通过使用步长 > 1来增大单元的感受野。但我们也可以借助池化层来实现这一点。池化层将输入切片分割成一个网格,每个网格单元代表多个单元的感受野(就像卷积层一样)。然后,在网格的每个单元上应用池化操作。池化层不会改变卷积深度,因为池化操作是独立地在每个切片上进行的。池化层由两个参数定义:步长和感受野大小,就像卷积层一样(池化层通常不使用填充)。
在本节中,我们将讨论三种类型的池化层——最大池化、平均池化和全局平均池化(GAP)。这三种池化类型在下图中有所展示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_09.jpg
图 4.9 – 最大池化、平均池化和全局平均池化
最大池化是最常见的池化方法。最大池化操作会选择每个局部感受野(网格单元)中激活值最高的单元,并仅将该值传递给下一层。在上面的图示(左图)中,我们可以看到一个 2×2 感受野和步长 = 2的最大池化示例。该操作丢弃了输入单元的 3/4。池化层没有权重。在最大池化的反向传播过程中,梯度只会传递给前向传播时激活值最高的单元。其他单元的梯度为零。
平均池化是另一种池化方式,其中每个感受野的输出是该区域内所有激活值的均值。在上面的图示(中图)中,我们可以看到一个 2×2 感受野和步长 = 2的平均池化示例。
GAP 与平均池化相似,但一个池化区域覆盖整个输入切片。我们可以将 GAP 视为一种极端的降维方法,因为它输出一个代表整个切片均值的单一值。这种池化方式通常应用于 CNN 的卷积部分结束时。在上面的图示(右图)中,我们可以看到 GAP 操作的示例。步长和感受野大小不适用于 GAP 操作。
在实际应用中,通常只有两种步长和感受野大小的组合被使用。第一种是 2×2 的感受野,步长 = 2,第二种是 3×3 的感受野,步长 = 2(重叠)。如果我们为任一参数使用较大的值,网络将丧失过多的信息。或者,如果步长为 1,层的大小不会变小,感受野也不会增大。
基于这些参数,我们可以计算池化层的输出大小:
height_o = 1 + (height_i – filter_h) / stride
width_o = 1 + (width_i – filter_w) / stride
池化层仍然被广泛使用,但通常,我们可以通过简单地使用步长较大的卷积层来实现相似甚至更好的结果。(例如,参见 J. Springerberg, A. Dosovitskiy, T. Brox, 和 M. Riedmiller, Striving for Simplicity: The All Convolutional Net, (**2015), arxiv.org/abs/1412.6806
.)
我们现在有足够的知识来介绍我们的第一个完整的 CNN。
卷积网络的结构
下图展示了一个基本分类 CNN 的结构:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_10.jpg
图 4.10 – 一个基本的卷积网络,包含卷积层、全连接层和池化层
大多数 CNN 共享一些基本特性。以下是其中的一些:
-
我们通常会将一个或多个卷积层与一个池化层(或步长卷积)交替使用。通过这种方式,卷积层可以在每个感受野的层级上检测特征。更深层的感受野的聚合大小大于网络开始时的感受野,这使得它们能够从更大的输入区域中捕捉到更复杂的特征。我们通过一个例子来说明这一点。假设网络使用 3×3 的卷积,步长 = 1,以及 2×2 的池化,步长 = 2:
-
第一卷积层的单元将接收来自图像的 3×3 像素输入。
-
第一层的 2×2 输出单元组的合并感受野大小为 4×4(由于步长的原因)。
-
在第一次池化操作后,这些组将合并成池化层的一个单元。
-
第二次卷积操作将从 3×3 的池化单元接收输入。因此,它将接收来自一个边长为 3×4 = 12(或总共 12×12 = 144)像素的方形区域的输入图像。
-
-
我们使用卷积层从输入中提取特征。最深层检测到的特征非常抽象,但它们也不适合人类阅读。为了解决这个问题,我们通常会在最后一个卷积/池化层后添加一个或多个全连接层。在这个例子中,最后一个全连接层(输出层)将使用 softmax 来估算输入的类别概率。你可以把全连接层看作是网络语言(我们无法理解)和我们语言之间的翻译器。
-
与初始卷积层相比,较深的卷积层通常具有更多的滤波器(因此卷积深度更大)。网络开始时的特征检测器在较小的感受野上工作。它只能检测到有限数量的特征,例如在所有类别中共享的边缘或线条。另一方面,较深的层则能够检测到更复杂和更多的特征。例如,如果我们有多个类别,如汽车、树木或人物,每个类别都会有一组独特的特征,如轮胎、车门、树叶和面孔等。这就需要更多的特征检测器。
现在我们已经了解了 CNN 的结构,接下来让我们用 PyTorch 和 Keras 实现一个 CNN。
使用 PyTorch 和 Keras 分类图像
在本节中,我们将尝试用 PyTorch 和 Keras 分类 CIFAR-10 数据集的图像。它由 60,000 张 32x32 的 RGB 图像组成,分为 10 个类别的物体。为了理解这些示例,我们将首先关注到目前为止我们还没有覆盖的两个前提条件:图像在深度学习库中的表示方式和数据增强训练技术。
深度学习库中的卷积层
PyTorch、Keras 和 TensorFlow(TF)支持 1D、2D 和 3D 卷积。卷积操作的输入和输出是张量。一个具有多个输入/输出切片的 1D 卷积将有 3D 的输入和输出张量。它们的轴可以是 SCW 或 SWC 顺序,我们有如下定义:
-
S:小批量中的样本索引
-
C:深度切片在体积中的索引
-
W:切片的内容
同样,一个 2D 卷积将由 SCHW 或 SHWC 顺序的张量表示,其中 H 和 W 分别是切片的高度和宽度。一个 3D 卷积将有 SCDHW 或 SDHWC 顺序,其中 D 代表切片的深度。
数据增强
最有效的正则化技术之一就是数据增强。如果训练数据太小,网络可能会开始过拟合。数据增强通过人工增加训练集的大小,帮助防止这种情况。在 CIFAR-10 示例中,我们将训练一个 CNN,并进行多轮训练。网络每轮都会“看到”数据集中的每个样本。为了防止这种情况,我们可以在将图像输入到 CNN 训练之前,先对它们进行随机增强,标签保持不变。以下是一些最常用的图像增强方法:
-
旋转
-
水平和垂直翻转
-
放大/缩小
-
裁剪
-
偏斜
-
对比度和亮度调整
加粗的增强方法将在以下示例中展示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_11.jpg
图 4.11 – 不同图像增强的示例
有了这些,我们可以继续进行示例了。
使用 PyTorch 分类图像
我们先从 PyTorch 开始:
-
选择设备,最好是 GPU。这个神经网络比 MNIST 的网络要大,使用 CPU 训练会非常慢:
import torch from torchsummary import summary device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
-
加载训练数据集(然后是验证数据集):
import torchvision.transforms as transforms from torchvision import datasets from torch.utils.data import DataLoader # Training dataset train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_data = datasets.CIFAR10( root='data', train=True, download=True, transform=train_transform) batch_size = 50 train_loader = DataLoader( dataset=train_data, batch_size=batch_size, shuffle=True, num_workers=2)
train_transform
是特别重要的。它执行随机水平和垂直翻转,并使用transforms.Normalize
通过 z-score 标准化来规范化数据集。这些硬编码的数值表示 CIFAR-10 数据集手动计算的逐通道均值和std
值。train_loader
负责提供训练的小批量数据。 -
加载验证数据集。请注意,我们使用训练数据集的均值和
std
值对验证集进行标准化:validation_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) validation_data = datasets.CIFAR10( root='data', train=False, download=True, transform=validation_transform) validation_loader = DataLoader( dataset=validation_data, batch_size=100, shuffle=True)
-
使用
Sequential
类定义我们的 CNN。它具有以下特性:-
三个块,每个块由两个卷积层(3×3 滤波器)和一个最大池化层组成。
-
每个卷积层后进行批量归一化。
-
前两个块对卷积操作应用
padding=1
,因此不会减少特征图的尺寸。 -
Linear
(全连接)层有 10 个输出(每个类别一个)。最终激活函数是 softmax。
让我们看看定义:
from torch.nn import Sequential, Conv2d, BatchNorm2d, GELU, MaxPool2d, Dropout2d, Linear, Flatten model = Sequential( Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1), BatchNorm2d(32), GELU(), Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1), BatchNorm2d(32), GELU(), MaxPool2d(kernel_size=2, stride=2), Dropout2d(0.2), Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1), BatchNorm2d(64), GELU(), Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1), BatchNorm2d(64), GELU(), MaxPool2d(kernel_size=2, stride=2), Dropout2d(p=0.3), Conv2d(in_channels=64, out_channels=128, kernel_size=3), BatchNorm2d(128), GELU(), Conv2d(in_channels=128, out_channels=128, kernel_size=3), BatchNorm2d(128), GELU(), MaxPool2d(kernel_size=2, stride=2), Dropout2d(p=0.5), Flatten(), Linear(512, 10), )
-
-
运行训练和验证。我们将使用与 第三章 中的 MNIST PyTorch 示例中实现的相同的
train_model
和test_model
函数。因此,我们在此不会实现它们,但完整的源代码可以在本章的 GitHub 仓库中找到(包括 Jupyter Notebook)。我们可以期待以下结果:在 1 轮时准确率为 51%,在 5 轮时准确率为 70%,在 75 轮时准确率约为 82%。
这就是我们的 PyTorch 示例的结尾。
使用 Keras 进行图像分类
我们的第二个示例是相同的任务,但这次使用 Keras 实现:
-
首先下载数据集。我们还将把数字标签转换为独热编码的张量:
import tensorflow as tf (X_train, Y_train), (X_validation, Y_validation) = \ tf.keras.datasets.cifar10.load_data() Y_train = tf.keras.utils.to_categorical(Y_train, 10) Y_validation = \ tf.keras.utils.to_categorical(Y_validation, 10)
-
创建一个
ImageDataGenerator
实例,它对训练集图像的每个通道应用 z 标准化。在训练过程中,它还提供数据增强(随机水平和垂直翻转)。另外,请注意,我们将训练集的均值和标准差应用于测试集,以获得最佳性能:from tensorflow.keras.preprocessing.image import ImageDataGenerator data_generator = ImageDataGenerator( featurewise_center=True, featurewise_std_normalization=True, horizontal_flip=True, vertical_flip=True) # Apply z-normalization on the training set data_generator.fit(X_train) # Standardize the validation set X_validation = \ data_generator.standardize( \ X_validation.astype('float32'))
-
然后,我们可以使用
Sequential
类定义我们的 CNN。我们将使用在 使用 PyTorch 进行图像分类 部分中定义的相同架构。以下是该模型的 Keras 定义:from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, Dense, MaxPooling2D, Dropout, BatchNormalization, Activation, Flatten model = Sequential(layers=[ Conv2D(32, (3, 3), padding='same', input_shape=X_train.shape[1:]), BatchNormalization(), Activation('gelu'), Conv2D(32, (3, 3), padding='same'), BatchNormalization(), Activation('gelu'), MaxPooling2D(pool_size=(2, 2)), Dropout(0.2), Conv2D(64, (3, 3), padding='same'), BatchNormalization(), Activation('gelu'), Conv2D(64, (3, 3), padding='same'), BatchNormalization(), Activation('gelu'), MaxPooling2D(pool_size=(2, 2)), Dropout(0.3), Conv2D(128, (3, 3)), BatchNormalization(), Activation('gelu'), Conv2D(128, (3, 3)), BatchNormalization(), Activation('gelu'), MaxPooling2D(pool_size=(2, 2)), Dropout(0.5), Flatten(), Dense(10, activation='softmax') ])
-
定义训练参数(我们还将打印模型总结以便于理解):
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) print(model.summary())
-
运行 50 轮的训练:
batch_size = 50 model.fit( x=data_generator.flow(x=X_train, y=Y_train, batch_size=batch_size), steps_per_epoch=len(X_train) // batch_size, epochs=100, verbose=1, validation_data=(X_validation, Y_validation), workers=4)
根据轮数的不同,这个模型将产生以下结果:在 1 轮时准确率为 50%,在 5 轮时准确率为 72%,在 45 轮时准确率约为 85%。我们的 Keras 示例与 PyTorch 示例相比,准确率略高,尽管它们应该是相同的。也许我们在某处有一个 bug。我们可能永远不会知道,但我们仍然能从中学到一课:机器学习模型并不容易调试,因为它们可能只是性能稍微下降,而不是完全报错。找到这种性能下降的确切原因可能很难。
既然我们已经实现了第一次完整的 CNN 两次,我们将重点讨论一些更高级的卷积类型。
高级卷积类型
到目前为止,我们讨论了“经典”卷积操作。在本节中,我们将介绍几种新的变体及其属性。
1D、2D 和 3D 卷积
在本章中,我们使用了2D 卷积,因为二维图像的计算机视觉是最常见的 CNN 应用。但我们也可以有 1D 和 3D 卷积,其中单位分别排列在一维或三维空间中。在所有情况下,滤波器的维度与输入的维度相同,并且权重在输入上是共享的。例如,我们会对时间序列数据使用 1D 卷积,因为这些值是沿单一时间轴排列的。在下图的左侧,我们可以看到一个 1D 卷积的示例:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_12.jpg
图 4.12 – 1D 卷积(左);3D 卷积(右)
相同虚线的权重共享相同的值。1D 卷积的输出也是 1D。如果输入是 3D,例如 3D MRI,我们可以使用 3D 卷积,它也会产生 3D 输出。这样,我们可以保持输入数据的空间排列。在上面的图示中,我们可以看到右侧的 3D 卷积示例。输入的维度是 H/W/L,滤波器在所有维度上有一个单一的大小,F。输出也是 3D。
1×1 卷积
1×1(点卷积)卷积是卷积的一种特殊情况,其中卷积滤波器的每个维度大小为 1(在 2D 卷积中是 1×1,在 3D 卷积中是 1×1×1)。起初,这看起来没有意义——一个 1×1 的滤波器并不会增加输出单元的感受野大小。这样的卷积结果只是逐点缩放。但它在另一个方面可能会有用——我们可以用它们来改变输入和输出体积之间的深度。为了理解这一点,让我们回顾一下,通常我们有一个深度为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/308.png 的输入体积和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/309.png 的滤波器,生成 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/309.png 输出体积的切片。每个输出切片都是通过对所有输入切片应用一个独特的滤波器来生成的。如果我们使用 1×1 滤波器且 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/311.png,我们会得到相同大小的输出切片,但深度不同。同时,输入和输出之间的感受野大小不会发生改变。最常见的用例是减少输出体积,或者 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/312.png(降维),也被称为**“瓶颈层”**。
深度可分离卷积
在跨通道卷积中,一个输出切片从所有输入切片中接收输入,并使用一个滤波器。滤波器试图学习 3D 空间中的特征,其中两个维度是空间的(切片的高度和宽度),第三个维度是通道。因此,滤波器同时映射空间和跨通道的相关性。
深度可分离卷积(DSC,Xception: 深度学习与深度可分离卷积, arxiv.org/abs/1610.02357
)可以完全解耦
跨通道和空间相关性。深度可分离卷积(DSC)结合了两种操作:深度卷积和 1×1 卷积。在深度卷积中,单个输入切片生成单个输出切片,因此它只映射空间相关性(而非跨通道相关性)。而在 1×1 卷积中,则恰好相反。以下图示表示 DSC:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_13.jpg
图 4.13 – 一种深度可分离卷积
深度可分离卷积(DSC)通常在第一次(深度方向)操作后不加非线性处理。
注意
让我们比较标准卷积和深度可分离卷积。假设我们有 32 个输入和输出通道,滤波器大小为 3×3。在标准卷积中,一个输出切片是将每个 32 个输入切片应用一个滤波器的结果,总共是 32 * 3 * 3 = 288
权重(不包括偏差)。在一个类似的深度方向卷积中,滤波器只有 3 * 3 = 9 个权重,而 1×1 卷积的滤波器则有 32 * 1 * 1 = 32 个权重。权重的总数为 32 + 9 = 41。因此,与标准卷积相比,深度可分离卷积速度更快,且内存效率更高。
膨胀卷积
常规卷积在 n×n 感受野上应用 n×n 滤波器。而膨胀卷积则稀疏地在大小为 (n * l - 1) × (n * l - 1) 的感受野上应用相同的滤波器,其中 l 是 膨胀因子。我们仍然将每个滤波器的权重与一个输入切片单元相乘,但这些单元之间的距离为 l。常规卷积是膨胀卷积的特例,l = 1。以下图示最好说明这一点:
https://github.com/vdumoulin/conv_arithmetic](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_14.jpg)
图 4.14 – 一种膨胀卷积,膨胀因子为 l=2。这里展示了操作的前两步。底层是输入,顶层是输出。来源:github.com/vdumoulin/conv_arithmetic
膨胀卷积可以在不丧失分辨率或覆盖范围的情况下指数级地增大感受野的大小。我们也可以通过步幅卷积或池化来增大感受野,但会以分辨率和/或覆盖范围的损失为代价。为了理解这一点,我们假设有一个步幅大于 1 的步幅卷积s > 1。在这种情况下,输出切片的大小是输入的s倍(分辨率损失)。如果我们进一步增大s > F(F是池化或卷积核的大小),我们会失去覆盖范围,因为输入切片的某些区域根本不参与输出。此外,膨胀卷积不会增加计算和内存成本,因为滤波器使用的权重数量与常规卷积相同。
转置卷积
在我们到目前为止讨论的卷积操作中,输出维度要么与输入维度相同,要么小于输入维度。相比之下,转置卷积(最初由Matthew D. Zeiler、Dilip Krishnan、Graham W. Taylor 和 Rob Fergus 提出的 Deconvolutional Networks: www.matthewzeiler.com/mattzeiler/deconvolutionalnetworks.pdf
)允许我们对输入数据进行上采样(其输出大于输入)。这种操作也被称为反卷积、分数步幅卷积或子像素卷积。这些名字有时会导致混淆。为了解释清楚,请注意,转置卷积实际上是一个常规卷积,只是输入切片或卷积滤波器略有修改。
对于更长的解释,我们将从一个 1D 常规卷积开始,处理单个输入和输出切片:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_15.jpg
图 4.15 – 1D 常规卷积
它使用一个大小为size = 4,步幅为stride = 2,填充为padding = 2的滤波器(在前面的图示中用灰色表示)。输入是大小为 6 的向量,输出是大小为 4 的向量。滤波器是一个向量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/313.png始终是相同的,但每次应用时会用不同的颜色表示它的位置。对应的输出单元格也用相同的颜色表示。箭头指示哪些输入单元格贡献了一个输出单元格。
注意
本节讨论的示例灵感来源于论文 Is the deconvolution layer the same as a convolutional layer? (arxiv.org/abs/1609.07009
)。
接下来,我们将讨论相同的例子(1D,单输入输出切片,以及一个大小为size = 4,padding = 2,stride = 2的滤波器),但是是转置卷积。下图展示了我们可以实现它的两种方式:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_16.jpg
图 4.16 – 使用步幅 = 2 的卷积,通过转置滤波器 f 应用。输出开始和结束时的两个像素被裁剪(左);步幅为 0.5 的卷积,应用于输入数据,且用子像素进行填充。输入填充了值为 0 的像素(灰色)(右)
让我们详细讨论一下:
-
在第一种情况下,我们有一个常规的卷积,步幅为stride = 2,滤波器表示为转置的行矩阵(相当于列矩阵),大小为 4:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/314.png(如前图所示,左边)。请注意,步幅应用于输出层,而不是常规卷积中的输入层。在设置步幅大于 1 时,相比于输入,我们可以增加输出的大小。在这里,输入切片的大小是I,滤波器的大小是F,步幅是S,输入填充是P。因此,转置卷积的输出切片大小O由以下公式给出:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/315.png。在这种情况下,大小为 4 的输入会产生大小为 2 * (4 - 1) + 4 - 2 * 2 = 6 的输出。我们还会裁剪输出向量开始和结束时的两个单元格,因为它们只收集来自单个输入单元格的输入。
-
在第二种情况下,输入填充了存在像素之间的虚拟 0 值子像素(如前面的图所示,右侧)。这就是子像素卷积名称的由来。可以将其视为在图像内部进行的填充,而不仅仅是在边界上进行填充。一旦输入以这种方式变换,就会应用常规卷积。
让我们比较两个输出单元,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/316.png 和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/317.png,在这两种情况下,如前面的图所示,在任何情况下,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/318.png 都接收来自第一个和第二个输入单元的输入,而 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/319.png 接收来自第二个和第三个单元的输入。这两种情况的唯一区别是参与计算的权重索引。然而,权重在训练过程中会被学习到,因此,索引并不重要。因此,这两个操作是等价的。
接下来,让我们从子像素的角度来看 2D 转置卷积。与 1D 情况类似,我们在输入切片中插入值为 0 的像素和填充,以实现上采样(输入位于底部):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_17.jpg
图 4.17 – 带填充 = 1 和步幅 = 2 的 2D 转置卷积的前三个步骤。来源: github.com/vdumoulin/conv_arithmetic
, https://arxiv.org/abs/1603.07285
常规卷积的反向传播操作是转置卷积。
这就结束了我们对各种类型卷积的扩展介绍。在下一部分,我们将学习如何利用迄今为止学到的高级卷积构建一些高级 CNN 架构。
高级 CNN 模型
在这一节中,我们将讨论一些复杂的 CNN 模型。它们在 PyTorch 和 Keras 中都有提供,并且在 ImageNet 数据集上进行了预训练。你可以直接导入并使用它们,而不是从头开始构建。不过,作为替代方法,讨论它们的核心思想也是值得的,而不是把它们当作黑盒使用。
这些模型大多共享一些架构原理:
-
它们从“入口”阶段开始,利用步幅卷积和/或池化的组合将输入图像的大小减少至少两到八倍,然后将其传播到网络的其余部分。这使得 CNN 在计算和内存上更加高效,因为更深层的网络处理的是较小的切片。
-
主网络体位于入口阶段之后。它由多个重复的复合模块组成。每个模块都利用填充卷积,使其输入和输出的切片大小相同。这使得可以根据需要堆叠任意数量的模块,以达到所需的深度。相比早期的模块,更深的模块在每次卷积中使用更多的过滤器(输出切片)。
-
主体中的下采样由具有步幅卷积和/或池化操作的特殊模块处理。
-
卷积阶段通常以对所有切片进行 GAP 操作结束。
-
GAP 操作的输出可以作为各种任务的输入。例如,我们可以添加一个全连接(FC)层进行分类。
我们可以在下图中看到一个基于这些原理构建的典型 CNN:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_18.jpg
图 4.18 – 一个典型的 CNN
有了这个,让我们更深入地了解深度 CNN(懂了吗?)。
引入残差网络
残差网络(ResNets,深度残差学习用于图像识别,arxiv.org/abs/1512.03385
)于 2015 年发布,当年他们在 ImageNet 挑战赛中赢得了所有五个类别的冠军。在第二章中,我们讨论了神经网络的层不是按顺序排列的,而是形成一个有向图。这是我们将要学习的第一种利用这种灵活性的架构。这也是第一种成功训练超过 100 层深度网络的架构。
由于更好的权重初始化、新的激活函数以及归一化层,现在可以训练深度网络。然而,论文的作者进行了实验,观察到一个 56 层的网络在训练和测试中出现的错误比一个 20 层的网络要高。他们认为这种情况不应发生。从理论上讲,我们可以采用一个浅层网络,并在其上堆叠恒等层(这些层的输出只是重复输入)来生成一个更深的网络,使其行为与浅层网络相同。然而,他们的实验未能匹配浅层网络的表现。
为了解决这个问题,他们提出了由残差模块构成的网络。残差模块由两到三个连续的卷积层和一个单独的并行恒等(重复器)快捷连接组成,该连接将第一个层的输入与最后一层的输出相连。我们可以在下面的图中看到三种类型的残差模块:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_19.jpg
图 4.19 – 从左到右 – 原始残差模块;原始瓶颈残差模块;预激活残差模块;预激活瓶颈残差模块
每个模块都有两条并行路径。左侧路径与我们见过的其他网络类似,由连续的卷积层和批量归一化组成。右侧路径包含恒等快捷连接(也称为跳跃连接)。这两条路径通过元素级求和进行合并——也就是说,左右两边的张量形状相同,第一个张量的一个元素加到第二个张量中相同位置的元素上。输出是一个与输入形状相同的单一张量。实际上,我们将模块学到的特征向前传播,同时保留了原始的未修改信号。这样,我们就能更接近原始场景,正如作者所描述的那样。由于跳跃连接,网络可以选择跳过一些卷积层,实际上减少了其深度。残差模块使用填充,使得输入和输出的形状相同。得益于此,我们可以堆叠任意数量的模块,构建具有任意深度的网络。
现在,让我们看看图中的模块有何不同:
-
第一个模块包含两个 3×3 卷积层。这是原始的残差模块,但如果层数过宽,堆叠多个模块会变得计算开销很大。
-
第二个模块与第一个模块相同,但它使用了瓶颈层。首先,我们使用 1×1 卷积来下采样输入的深度(我们在1×1 卷积章节中讨论过这个)。然后,我们对减少后的输入应用一个 3×3 的(瓶颈)卷积。最后,我们通过另一个 1×1 上采样卷积将输出扩展回所需的深度。这个层的计算开销比第一个要小。
-
第三个块是这一概念的最新修订版,由同一作者于 2016 年发布(Identity Mappings in Deep Residual Networks,
arxiv.org/abs/1603.05027
)。它使用了预激活,并且批归一化和激活函数位于卷积层之前。这一设计可能看起来有些奇怪,但得益于这一设计,跳跃连接路径可以在整个网络中不间断地运行。这与其他残差块不同,在那些块中,至少有一个激活函数位于跳跃连接的路径上。堆叠的残差块组合依然保持了正确的层次顺序。 -
第四个块是第三层的瓶颈版本。它遵循与瓶颈残差层 v1 相同的原理。
在下表中,我们可以看到论文作者提出的网络家族:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_20.jpg
图 4.20 – 最流行的残差网络家族。残差块以圆角矩形表示。灵感来源于 arxiv.org/abs/1512.03385
它们的一些属性如下:
-
它们以一个 7×7 卷积层开始,stride = 2,接着是 3×3 最大池化。这个阶段作为一个下采样步骤,使得网络的其余部分可以在一个更小的 56×56 的切片上工作,相比于输入的 224×224。
-
网络其余部分的下采样是通过一个修改过的残差块实现的,stride = 2。
-
GAP 在所有残差块之后、1,000 单元全连接 softmax 层之前进行下采样。
-
各种 ResNet 的参数数量从 2560 万到 6040 万不等,网络的深度从 18 层到 152 层不等。
ResNet 网络家族之所以流行,不仅因为它们的准确性,还因为它们相对简洁且残差块具有很大的通用性。正如我们之前提到的,残差块的输入和输出形状由于填充可以相同。我们可以以不同的配置堆叠残差块,以解决具有广泛训练集规模和输入维度的各种问题。
Inception 网络
Inception 网络 (Going Deeper with Convolutions, arxiv.org/abs/1409.4842
) 于 2014 年首次提出,并赢得了当年的 ImageNet 挑战(似乎有一个规律)。此后,作者们发布了该架构的多个改进版本。
有趣的事实
Inception 这一名称部分来源于 We need to go deeper 这一互联网迷因,后者与电影《盗梦空间》相关。
inception 网络的思想源于这样一个基本前提:图像中的物体具有不同的尺度。一个远距离的物体可能只占据图像中的一小部分,但同一个物体靠近时,可能会占据图像的大部分。这对于标准的 CNN 来说是一个难题,因为不同层中的单元具有固定的感受野大小,这一大小被强加到输入图像上。一个常规的网络可能在某一尺度下能很好地检测物体,但在其他尺度下可能会错过物体。为了解决这个问题,论文的作者提出了一种新颖的架构:由 inception 块组成。inception 块以一个共同的输入开始,然后将其分割成不同的并行路径(或塔)。每条路径包含具有不同尺寸滤波器的卷积层或池化层。通过这种方式,我们可以对相同的输入数据应用不同的感受野。在 Inception 块的末端,来自不同路径的输出会被连接起来。
在接下来的几节中,我们将讨论 Inception 网络的不同变体。
Inception v1
以下图示展示了GoogLeNet网络架构中 inception 块的第一个版本(arxiv.org/abs/1409.4842
):
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_21.jpg
图 4.21 – Inception v1 块;灵感来源于 https://arxiv.org/abs/1409.4842
v1 块有四条路径:
-
1×1 卷积,它作为输入的某种重复器
-
1×1 卷积,后接 3×3 卷积
-
1×1 卷积,后接 5×5 卷积
-
3×3 最大池化,步幅 = 1
块中的层使用填充,使得输入和输出具有相同的形状(但深度不同)。填充也是必要的,因为每个路径的输出形状会根据滤波器大小不同而有所不同。这适用于所有版本的 inception 块。
该 inception 块的另一个主要创新是使用降采样 1×1 卷积。它们是必需的,因为所有路径的输出将被连接以产生该块的最终输出。连接的结果是输出深度被四倍增大。如果下一个块继续跟随当前块,它的输出深度将再次四倍增长。为了避免这种指数增长,块使用 1×1 卷积来减少每条路径的深度,从而降低块的输出深度。这使得我们可以创建更深的网络,而不至于耗尽资源。
完整的 GoogLeNet 具有以下特性:
-
与 ResNets 类似,它首先进行降采样阶段,利用两个卷积层和两个最大池化层将输入尺寸从 224×224 降至 56×56,然后才开始使用 inception 块。
-
该网络有九个 inception v1 块。
-
卷积阶段以全局平均池化结束。
-
网络利用了辅助分类器——即在不同的中间层有两个额外的分类输出(具有相同的真实标签)。在训练过程中,损失的总值是辅助损失和真实损失的加权和。
-
该模型共有 690 万个参数,深度为 22 层。
Inception v2 和 v3
Inception v2 和 v3 是一起发布的,并提出了比原始 v1(Rethinking the Inception Architecture for Computer Vision, arxiv.org/abs/1512.00567
)更改进的多个 Inception 块。我们可以在以下图示中看到第一个新 Inception 块 A:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_22.jpg
图 4.22 – Inception 块 A,灵感来自于 https://arxiv.org/abs/1512.00567
块 A 的第一个新特性是将 5×5 卷积因式分解为两个堆叠的 3×3 卷积。这种结构有几个优点。
最后一层堆叠单元的感受野等同于一个大卷积滤波器的单层感受野。与使用单层大滤波器相比,堆叠的层能够以更少的参数实现相同的感受野大小。例如,我们可以将一个单独的 5×5 层替换为两个堆叠的 3×3 层。为了简便起见,我们假设有单一的输入和输出切片。5×5 层的总权重(不包括偏差)是 5 * 5 = 25。
另一方面,单个 3×3 层的总权重是 3 * 3 = 9,两个层的权重则是 2 * (3 * 3) = 18,这使得这种安排比单层的 5×5 层更高效 28%(18/25 = 0.72)。即便在输入和输出切片为多个的情况下,这种效率提升也得以保持。接下来的改进是将一个 n×n 卷积因式分解为两个堆叠的不对称 1×n 和 n×1 卷积。例如,我们可以将一个单一的 3×3 卷积分解为两个 1×3 和 3×1 卷积,其中 3×1 卷积应用于 1×3 卷积的输出。在第一个情况下,滤波器大小是 3 * 3 = 9,而在第二种情况下,组合后的大小是 (3 * 1) + (1 * 3) = 3 + 3 = 6,达到了 33% 的效率提升,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_23.jpg
图 4.23 – 3×3 卷积的因式分解为 1×3 和 3×1 卷积;灵感来自于 https://arxiv.org/abs/1512.00567
作者引入了两个利用因式分解卷积的新块。其中第一个块(也是总的第二个块),Inception 块 B,等同于 Inception 块 A:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_24.jpg
图 4.24 – Inception 块 B。当 n=3 时,它等同于块 A;灵感来源于 arxiv.org/abs/1512.00567
第二个(总共是第三个)Inception 块 C 类似,但非对称卷积是并行的,从而导致更高的输出深度(更多的连接路径)。这里的假设是,网络拥有更多特征(不同的滤波器)时,它学习得更快。另一方面,更宽的层会占用更多的内存和计算时间。作为折衷,这个块仅在网络的较深部分使用,在其他块之后:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_25.jpg
图 4.25 – Inception 块 C;灵感来源于 arxiv.org/abs/1512.00567
该版本的另一个重大改进是使用批量归一化,这一技术由同一作者提出。
这些新模块创建了两个新的 Inception 网络:v2 和 v3。Inception v3 使用批量归一化,并且是这两者中更为流行的一个。它具有以下特点:
-
网络从下采样阶段开始,利用步幅卷积和最大池化将输入大小从 299×299 降低到 35×35,然后再进入 Inception 模块。
-
层被组织为三个 Inception 块 A,五个 Inception 块 B 和两个 Inception 块 C。
-
卷积阶段以全局平均池化结束。
-
它具有 2390 万个参数,深度为 48 层。
Inception v4 和 Inception-ResNet
最新的 Inception 网络修订版引入了三个新的简化 Inception 块(Inception-v4,Inception-v4, Inception-ResNet 和残差连接对学习的影响,arxiv.org/abs/1602.07261
)。更具体地说,新版本引入了 7×7 非对称因式分解卷积、平均池化代替最大池化,以及具有残差连接的新 Inception-ResNet 块。我们可以在以下图示中看到其中一个这样的块:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_26.jpg
图 4.26 – 带有残差跳跃连接的 Inception 块(任何类型)
Inception-ResNet 系列模型具有以下特点:
-
网络从下采样阶段开始,利用步幅卷积和最大池化将输入大小从 299×299 降低到 35×35,然后再进入 Inception 模块。
-
模型的主体由三组四个残差 Inception-A 块、七个残差 Inception-B 块、三个残差 Inception-B 块,以及各组之间的特殊缩减模块组成。不同的模型使用这些块的略微不同变体。
-
卷积阶段以全局平均池化结束。
-
模型约有 5600 万个权重。
本节中,我们讨论了不同类型的 inception 网络以及各个 inception 块中使用的不同原理。接下来,我们将介绍一种较新的 CNN 架构,它将 inception 概念带到了一个新的深度(或者更准确地说,是宽度)。
引入 Xception
到目前为止,我们讨论的所有 inception 块都从将输入拆分成若干并行路径开始。每条路径接着执行一个降维的 1×1 跨通道卷积,然后是常规的跨通道卷积。一方面,1×1 连接映射跨通道相关性,但不映射空间相关性(因为 1×1 滤波器大小)。另一方面,后续的跨通道卷积映射两种类型的相关性。回想一下,在本章前面,我们介绍了 DSC,它结合了以下两种操作:
-
深度卷积:在深度卷积中,一个输入切片产生一个输出切片,因此它仅映射空间(而非跨通道)相关性。
-
1×1 跨通道卷积:使用 1×1 卷积时,正好相反——它们只映射跨通道相关性。
Xception 的作者(Xception: Deep Learning with Depthwise Separable Convolutions, arxiv.org/abs/1610.02357
)认为,我们可以将 DSC 视为 inception 块的极端(因此得名)版本,其中每一对深度卷积输入/输出切片代表一条并行路径。我们有多少条并行路径,就有多少个输入切片。下图展示了一个简化版的 inception 块及其转化为 Xception 块的过程:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_27.jpg
图 4.27 – 一个简化的 inception 模块(左);一个 Xception 块(右);灵感来源于 https://arxiv.org/abs/1610.02357
Xception 块和 DSC 之间有两个区别:
-
在 Xception 中,1×1 卷积位于最前面,而在 DSC 中位于最后。然而,这些操作本应被堆叠,因此我们可以假设顺序并不重要。
-
Xception 块在每个卷积后使用 ReLU 激活,而 DSC 在跨通道卷积之后不使用非线性激活。根据作者的实验结果,缺少非线性激活的深度卷积网络收敛速度更快且更为准确。
完整的 Xception 网络具有以下特点:
-
它从卷积和池化操作的输入流开始,将输入大小从 299×299 缩小到 19×19。
-
它包含 14 个 Xception 模块,所有模块周围都有线性残差连接,除了第一个和最后一个模块。
-
所有卷积和 DSC 操作后都紧跟着批量归一化。所有 DSC 的深度乘数为 1(没有深度扩展)。
-
卷积阶段以全局平均池化结束。
-
总共 2300 万个参数,36 层卷积层的深度。
本节总结了基于 inception 的模型系列。在下一节中,我们将重点讨论一个新的神经网络架构元素。
压缩与激发网络
压缩与激发网络(SENet,压缩与激发网络,arxiv.org/abs/1709.01507
)引入了一个新的神经网络架构单元,作者称之为——你猜对了——压缩与激发(SE)块。我们回顾一下,卷积操作对输入通道应用多个滤波器以生成多个输出特征图(或通道)。SENet 的作者观察到,当这些通道作为输入传递给下一层时,它们具有“相等的权重”。然而,有些通道可能比其他通道更有信息。为了强调它们的重要性,作者提出了内容感知的 SE 块,它能够自适应地对每个通道进行加权。我们也可以将 SE 块看作是一种注意力机制。为了理解它是如何工作的,让我们从以下图像开始:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_28.jpg
图 4.28 – 压缩与激发块
该块引入了一个与主神经网络数据流平行的路径。让我们来看一下它的步骤:
-
压缩阶段:在各个通道上执行 GAP 操作。GAP 的输出是每个通道的一个标量值。例如,如果输入是 RGB 图像,那么在 R、G 和 B 通道上执行的独特 GAP 操作将生成一个大小为 3 的一维张量。可以将这些标量值看作是通道的精炼状态。
-
全连接层 -> ReLU -> 全连接层 -> sigmoid
。它类似于自编码器,因为第一个隐藏层减少了输入张量的大小,第二个隐藏层将其扩展到原始大小(对于 RGB 输入来说是 3)。最终的 sigmoid 激活函数确保输出值位于 (0:1) 范围内。 -
缩放:兴奋神经网络的输出值作为原始输入张量通道的缩放系数。一个通道的所有值都由兴奋阶段生成的对应系数进行缩放(或激发)。通过这种方式,兴奋神经网络能够强调某个通道的重要性。
作者将 SE 块添加到不同的现有模型中,提升了它们的准确性。在下图中,我们可以看到如何将 SE 块添加到 inception 和残差模块中:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_29.jpg
图 4.29 – 一个 SE-inception 模块(左)和一个 SE-ResNet 模块(右)
在下一节中,我们将看到 SE 块应用于一个优先考虑小尺寸和计算效率的模型。
介绍 MobileNet
本节将讨论一种轻量级卷积神经网络模型,名为MobileNet(MobileNetV3: Searching for MobileNetV3,arxiv.org/abs/1905.02244
)。我们将重点介绍这个想法的第三个版本(MobileNetV1 在MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications中介绍,arxiv.org/abs/1704.04861
;MobileNetV2 在MobileNetV2: Inverted Residuals and Linear Bottlenecks中介绍,arxiv.org/abs/1801.04381
)。
MobileNet 的目标是面向内存和计算能力有限的设备,例如手机(名字本身就揭示了这一点)。该神经网络引入了一种新的反向残差块(或称MBConv),具有较小的占用空间。MBConv 使用了深度可分离卷积(DSC)、线性瓶颈和反向残差。V3 版本还引入了 SE 模块。为了理解这些内容,下面是 MBConv 模块的结构:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_30.jpg
图 4.30 – MobileNetV3 构建块。只有当步幅 s=1 时,才存在快捷连接
让我们讨论一下它的属性:
-
线性瓶颈:我们假设输入是一个 RGB 图像。当图像通过神经网络(NN)传播时,每一层都会生成一个带有多个通道的激活张量。一直以来,人们认为这些张量中编码的信息可以在所谓的“感兴趣流形”中压缩,这个流形的张量比原始张量要小。强迫神经网络寻找这种流形的一种方法是使用 1×1 瓶颈卷积。然而,论文的作者认为,如果该卷积后接非线性激活函数,如 ReLU,这可能会导致由于 ReLU 消失问题而丧失流形信息。为了解决这个问题,MobileNet 使用没有非线性激活的 1×1 瓶颈卷积。
-
input -> 1×1 瓶颈卷积 -> 3×3 卷积 -> 1×1 反采样卷积
。换句话说,它遵循一个宽 -> 窄 -> 宽
的数据表示。另一方面,反向残差块遵循窄 -> 宽 -> 窄
的表示方式。在这里,瓶颈卷积通过扩展 因子 t 扩展其输入。作者认为瓶颈包含了所有必要的信息,而扩展层仅作为一个实现细节,伴随着张量的非线性变换。因此,他们提出在瓶颈连接之间使用快捷连接。
-
DSC:我们在本章早些时候已经介绍过这个操作。MobileNet V3 在 DSC 中引入了H-swish激活函数。H-swish 类似于我们在第二章中介绍的 swish 函数。V3 架构包括交替使用 ReLU 和 H-swish 激活函数。
-
SE 模块:我们已经熟悉这个模块。这里的区别是硬 Sigmoid激活函数,它近似 Sigmoid 函数,但在计算上更高效。该模块位于扩展深度卷积后,因此可以将注意力应用于最大的表示。SE 模块是 V3 的新增加项,在 V2 中并不存在。
-
步幅 s:该模块使用步幅卷积实现下采样。当s=1 时,才存在捷径连接。
MobileNetV3 引入了网络的大小变体,具有以下特点:
-
两个网络都以步幅卷积开始,将输入从 224×224 下采样到 112×112。
-
小型和大型变体分别有 11 个和 15 个 MBConv 模块。
-
卷积阶段结束时,两个网络都会进行全局平均池化。
-
小型和大型网络分别有 300 万和 500 万个参数。
在下一节中,我们将讨论 MBConv 模块的改进版。
EfficientNet
EfficientNet(EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks,arxiv.org/abs/1905.11946
)和EfficientNetV2: Smaller Models and Faster Training,arxiv.org/abs/2104.00298
)引入了复合缩放的概念。它从一个小的基线模型开始,然后在三个方向上同时扩展:深度(更多层)、宽度(每层更多特征图)以及更高的输入分辨率。复合缩放会生成一系列新模型。EfficientNetV1 的基线模型使用了 MobileNetV2 的 MBConv 模块。EfficientNetV2 引入了新的融合 MBConv模块,它用单一的扩展 3×3 跨通道卷积替代了 MBConv 中的扩展 1×1 瓶颈卷积和 3×3 深度卷积:
https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_04_31.jpg
图 4.31 – 融合 MBConv 模块
新的 3×3 卷积同时处理扩展(通过因子t)和步幅(1 或 2)。
EfficientNetV2 的作者观察到,使用融合 MBConv 和 MBConv 模块组合的 CNN,比仅使用 MBConv 模块的 CNN 训练速度更快。然而,
融合 MBConv 模块在计算上比普通 MBConv 模块更为昂贵。因此,EfficientNetV2 逐步替换这些模块,从早期阶段开始。这是有道理的,因为早期的卷积使用了更少的滤波器(因此切片较少),所以在这一阶段,内存和计算代价不那么显著。找到两种模块的正确组合并不简单,因此需要复合缩放。这一过程产生了多个具有以下特点的模型:
-
这两个网络都以步幅卷积开始,将输入下采样两次。
-
主体的早期阶段使用融合 MBConv 模块,后期阶段使用 MBConv 模块。
-
所有网络的卷积阶段最终都会通过全局平均池化结束
-
参数的数量范围从 530 万到 1.19 亿不等
这篇文章介绍了高级 CNN 模型的内容。我们没有讨论所有可用的模型,而是重点介绍了其中一些最受欢迎的模型。我希望你现在已经掌握了足够的知识,可以自行探索新的模型。在下一节中,我们将演示如何在 PyTorch 和 Keras 中使用这些高级模型。
使用预训练模型与 PyTorch 和 Keras
PyTorch 和 Keras 都有一套预训练的即用型模型。我们在高级网络模型部分讨论的所有模型都可以通过这种方式使用。这些模型通常是在分类 ImageNet 数据集上进行预训练的,可以作为各种计算机视觉任务的骨干网络,正如我们在 第五章 中将看到的那样。
我们可以使用以下代码在 PyTorch 中加载一个预训练的模型:
from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights
# With pretrained weights:
model = mobilenet_v3_large(
weights=MobileNet_V3_Large_Weights.IMAGENET1K_V1)
model = mobilenet_v3_large(weights="IMAGENET1K_V1")
# Using no weights:
model = mobilenet_v3_large(weights=None)
权重将会自动下载。此外,我们可以使用以下代码列出所有可用的模型并加载任意模型:
from torchvision.models import list_models, get_model
# List available models
all_models = list_models()
model = get_model(all_models[0], weights="DEFAULT")
Keras 也支持类似的功能。我们可以使用以下代码加载一个预训练的模型:
from keras.applications.mobilenet_v3 import MobileNetV3Large
model = MobileNetV3Large(weights='imagenet')
这些简短但非常有用的代码示例总结了本章的内容。
总结
在本章中,我们介绍了 CNN。我们讨论了它们的主要组成部分——卷积层和池化层,并讨论了它们的架构和特征。我们特别关注了不同类型的卷积。我们还演示了如何使用 PyTorch 和 Keras 实现 CIFAR-10 分类 CNN。最后,我们讨论了目前一些最受欢迎的 CNN 模型。
在下一章中,我们将在新的计算机视觉知识基础上进行扩展,加入一些令人兴奋的内容。我们将讨论如何通过将知识从一个问题转移到另一个问题来加速网络训练。我们还将超越简单的分类,进行目标检测,或者如何找到图像中目标的位置。我们甚至会学习如何对图像的每个像素进行分割。