精通机器学习算法-全-
精通机器学习算法(全)
原文:
annas-archive.org/md5/790dc401711df8b2093de3b73e595cb9
译者:飞龙
前言
在过去几年里,机器学习已经成为大多数行业中越来越重要的领域。许多曾经被认为无法自动化的任务现在完全由计算机管理,这使得人类能够专注于更具创造性的工作。这一革命得益于标准算法的显著改进以及硬件价格的持续下降。仅十年前还是巨大障碍的复杂性,现在甚至个人电脑也能解决。高级开源框架的普遍可用性使得每个人都能设计和训练极其强大的模型。
这本书的主要目标是向读者介绍复杂的技术(如半监督学习和流形学习、概率模型和神经网络),在数学理论与用 Python 编写的实际例子之间保持平衡。我想要保持实用主义的方法,专注于应用但也不忽视必要的基础理论。在我看来,只有通过理解背后的逻辑,这通常用数学概念表达,才能获得对这个领域的良好知识。这种额外的努力会带来对每个具体选择的更牢固的认识,并帮助读者理解如何在特定的商业环境中应用、修改和改进所有算法。
机器学习是一个极其广泛的领域,不可能在一本书中涵盖所有主题。在这种情况下,我已经尽力涵盖了一组属于监督学习、半监督学习、无监督学习和强化学习的算法,并提供所有必要的参考文献以进一步探索每个算法。这些例子被设计成易于理解,无需对代码有深入的了解;实际上,我认为展示一般情况并让读者改进和适应以应对特定场景更为重要。对于错误,我表示歉意:尽管已经进行了多次修订,但可能仍有某些细节(无论是公式还是代码)被遗漏。我希望这本书能成为许多专业人士进入这个迷人世界的起点,他们带着实用和商业导向的观点努力进入这个领域!
这本书面向谁
这本书涵盖的内容
这本书的理想读者是希望获得复杂机器学习算法和应用详细知识的计算机科学学生和专业人士。方法始终是实用主义的;然而,理论部分需要一些高级数学技能,所有毕业生(在计算机科学、工程、数学或科学领域)都应该已经掌握。这本书也可以被更多以商业为导向的专业人士(如首席产品官和产品经理)使用,以了解如何利用机器学习来改进现有产品和业务。
第一章,机器学习模型基础,解释了关于机器学习模型最重要的理论概念,包括偏差、方差、过拟合、欠拟合、数据归一化和损失函数。对于对这些概念有深入了解的读者可以跳过这一部分。
第二章,半监督学习简介,向读者介绍了半监督学习的主要元素,重点关注归纳和演绎学习算法。
第三章,基于图半监督学习,继续探索属于基于图和流形学习模型家族的半监督学习算法。在不同的背景下分析了标签传播和非线性降维,提供了一些可以使用 Scikit-Learn 功能立即利用的有效解决方案。
第四章,贝叶斯网络和隐马尔可夫模型,介绍了使用有向无环图、马尔可夫链和序列过程进行概率建模的概念。
第五章,EM 算法及其应用,解释了期望最大化(EM)算法的通用结构。我们讨论了一些常见应用,如高斯混合、主成分分析、因子分析和独立成分分析。本章需要深厚的数学知识;然而,读者可以跳过证明,专注于最终结果。
第六章,赫布学习与自组织映射,介绍了赫布规则,这是最古老的神经科学概念之一,其应用非常强大。本章解释了单个神经元的工作原理,并介绍了两个复杂模型(Sanger 网络和 Rubner-Tavan 网络),它们可以在没有输入协方差矩阵的情况下执行主成分分析。
第七章,聚类算法,介绍了某些常见且重要的无监督算法,如 k-Nearest Neighbors(基于 KD 树和球树)、K-means(使用 K-means++初始化)、模糊 C 均值和谱聚类。一些重要的度量(如轮廓分数/图)也被分析。
第八章,集成学习,阐述了集成学习(bagging、boosting 和 stacking)的主要概念,重点关注随机森林、AdaBoost(及其变体)、梯度提升和投票分类器。
第九章,机器学习的神经网络,介绍了神经计算的概念,从感知器的行为开始,继续分析多层感知器、激活函数、反向传播、随机梯度下降(最重要的优化算法)、正则化、dropout 和批量归一化。
第十章,高级神经网络模型,继续解释最重要的深度学习方法,重点关注卷积网络、循环网络、LSTM 和 GRU。
第十一章,自编码器,阐述了自编码器的主要概念,讨论了其在降维、去噪和数据生成(变分自编码器)中的应用。
第十二章,生成对抗网络,解释了对抗训练的概念。我们重点关注深度卷积 GANs 和 Wasserstein GANs。这两种技术都是极其强大的生成模型,能够学习输入数据分布的结构,并生成全新的样本,而无需任何额外信息。
第十三章,深度信念网络,介绍了马尔可夫随机场、受限玻尔兹曼机和深度信念网络的概念。这些模型在监督和非监督场景中均能表现出色。
第十四章,强化学习简介,解释了强化学习(代理、策略、环境和奖励)的主要概念,并将它们应用于介绍策略和价值迭代算法以及时间差分学习(TD(0))。示例基于一个定制的棋盘环境。
第十五章,高级策略估计算法,扩展了前一章定义的概念,讨论了 TD(λ)算法、TD(0) Actor-Critic、SARSA 和 Q-Learning。还提供了一个深度 Q 学习的简单示例,以便读者能够立即将这些概念应用于更复杂的环境。
要充分利用这本书
这本书没有严格的前置要求;然而,拥有基本的到中级 Python 知识,并特别关注 NumPy,是很重要的。在必要时,我将提供安装特定包和利用更高级功能的说明/参考。由于 Python 基于语义缩进,发布版本可能包含错误的新行,这会在执行代码时引发异常。因此,我邀请所有没有深入了解这种语言的读者参考书中提供的原始源代码。
所有示例均基于 Python 3.5+。我建议使用 Anaconda 发行版(www.anaconda.com/download/
),这可能是用于科学项目的最完整和最强大的一个,其中大多数所需的包已经内置,安装新的包(有时是优化版本)也非常容易。然而,任何其他 Python 发行版都可以使用。此外,我邀请读者使用 Jupyter(以前称为 IPython)笔记本测试示例,以避免在做出更改时重新运行整个示例。如果更喜欢 IDE,我建议使用 PyCharm,它提供了许多内置功能,对于数据导向和科学项目非常有帮助(例如,内置的 Matplotlib 查看器)。
要完全理解理论部分,良好的数学背景是必要的。特别是,需要概率论、微积分和线性代数的基本技能。然而,当遇到看似过于困难的概念时,我建议不要放弃。参考部分包含了许多有用的书籍,而且大多数概念在维基百科上也有相当好的解释。当遇到未知的事物时,我建议在继续之前先阅读具体的文档。在许多情况下,不需要有完整的知识,甚至一个简介段落就足以理解它们的原理。
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packtpub.com上登录或注册。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
该书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Mastering-Machine-Learning-Algorithms
。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有来自我们丰富的图书和视频目录的其他代码包,可在github.com/PacktPublishing/
找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/MasteringMachineLearningAlgorithms_ColorImages.pdf
。
使用的约定
本书使用了多种文本约定。
CodeInText
:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在 Scikit-Learn 中,可以使用train_test_split()
函数来分割原始数据集。”
代码块设置如下:
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, train_size=0.7, random_state=1)
粗体:表示新术语、重要词汇或屏幕上看到的词汇。
警告或重要提示如下所示。
小贴士和技巧如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请发送电子邮件至 feedback@packtpub.com
,并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com
发送电子邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packtpub.com
联系我们,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或参与一本书的编写,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了本书,为何不在您购买它的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packtpub.com。
第一章:机器学习模型基础
机器学习模型是具有许多共同特征的数学系统。即使有时它们仅从理论观点定义,研究进展也使我们能够将几个概念应用于更好地理解复杂系统(如深度神经网络)的行为。在本章中,我们将介绍并讨论一些一些有经验的读者可能已经知道的基本元素,同时它们也提供了几种可能的解释和应用。
尤其是在本章中,我们讨论以下主要内容:
-
数据生成过程
-
有限数据集
-
训练和测试集分割策略
-
交叉验证
-
模型的容量、偏差和方差
-
Vapnik-Chervonenkis 理论
-
Cramér-Rao 界限
-
欠拟合和过拟合
-
损失和成本函数
-
正则化
模型和数据
机器学习算法与数据一起工作。它们创建关联,找出关系,发现模式,生成新的样本,等等,它们与定义良好的数据集一起工作。不幸的是,有时对它们的假设或施加的条件并不明确,漫长的训练过程可能导致完全的验证失败。即使这种条件在深度学习环境中更为强烈,我们也可以将模型视为一个灰盒(许多常见算法的简单性保证了某些透明度),其中向量输入被转换为向量输出
:
使用向量θ参数化的通用模型架构
在前面的图中,模型已被表示为一个依赖于由向量θ定义的一组参数的伪函数。在本节中,我们只考虑参数化模型,尽管有一系列算法被称为非参数化,因为它们仅基于数据的结构。我们将在接下来的章节中讨论其中的一些。
因此,参数化学习过程的任务是要找到最佳参数集,以最大化目标函数的值,该值与给定特定输入X和输出Y的模型的准确性(或如果我们试图最小化它们,则为误差)成正比。这个定义并不非常严谨,将在以下章节中得到改进;然而,它作为理解我们工作环境的一种方式是有用的。
然后,首先要问的问题是:X的本质是什么?机器学习问题专注于学习抽象关系,这些关系允许在提供新样本时进行一致的一般化。更具体地说,我们可以定义一个与联合概率分布相关的随机数据生成过程:
有时,将联合概率 p(x, y) 表示为条件概率 p(y|x) 的乘积是有用的,其中 p(y|x) 表示给定样本的标签概率,以及样本的边缘概率 p(x)。这种表达式在半监督环境中已知先验概率 p(x) 时特别有用,或者当我们对使用 期望最大化 (EM) 算法解决问题感兴趣时。我们将在接下来的章节中讨论这种方法。
在许多情况下,我们无法推导出精确的分布;然而,当我们考虑数据集时,我们总是假设它来自原始数据生成分布。这个条件不是一个纯粹的理论假设,因为我们将会看到,当我们的数据点来自不同的分布时,模型的准确性可以显著降低。
如果我们从 p[data] 中采样 N 个 独立同分布 (i.i.d) 的值,我们可以创建一个由 k-维实向量组成的有限数据集 X:
在监督场景中,我们还需要相应的标签(具有 t 个输出值):
当输出有超过两个类别时,有不同可能的策略来管理这个问题。在经典机器学习中,最常见的方法之一是 One-vs-All,它基于训练 N 个不同的二元分类器,其中每个标签都与所有剩余的标签进行比较。这样,N-1 个分类器被用于确定正确的类别。相反,在浅层和深层神经网络中,更倾向于使用 softmax 函数来表示所有类别的输出概率分布:
这种输出(z[i] 表示中间值,项的总和归一化到 1)可以很容易地使用交叉熵损失函数来管理(参见 损失和成本函数 部分的相应段落)。
零中心化和白化
许多算法在数据集对称(具有零均值)时表现出更好的性能(特别是在训练速度方面)。因此,最重要的预处理步骤之一是所谓的 零中心化,它包括从所有样本中减去特征均值 E[x][X]:
如果需要,这种操作通常是可逆的,并且不会改变样本之间以及同一样本成分之间的关系。在深度学习场景中,一个零中心的数据集允许利用某些激活函数的对称性,从而加速收敛(我们将在下一章中讨论这些细节)。
另一个非常重要的预处理步骤被称为 白化,它是对零中心数据集施加一个单位协方差矩阵的操作:
由于协方差矩阵 E[x][X^TX] 是实对称的,因此可以对其进行特征分解,而无需求逆特征向量矩阵:
矩阵 V 包含特征向量(作为列),对角矩阵 Ω 包含特征值。为了解决这个问题,我们需要找到一个矩阵 A,使得:
使用之前计算的特征分解,我们得到:
因此,矩阵 A 是:
白化的一大优点是数据集的去相关性,这允许更容易地分离成分。此外,如果 X 被白化,由矩阵 P 诱导的任何正交变换也会被白化:
此外,许多需要估计与输入协方差矩阵严格相关的参数的算法可以从这种条件中受益,因为它减少了实际独立变量的数量(通常,这些算法使用白化后变得对称的矩阵)。在深度学习领域的一个重要优势是梯度通常在原点附近更高,而在激活函数(例如,双曲正切或 Sigmoid)饱和的区域(|x| → ∞)减小。这就是为什么白化(和零均值化)数据集的收敛通常更快。
在以下图表中,可以比较原始数据集、零均值化和白化:
原始数据集(左),中心化版本(中心),白化版本(右)
当需要白化时,考虑一些重要细节是很重要的。首先,真实样本协方差和估计 X^TX 之间存在尺度差异,通常采用奇异值分解(SVD)。第二个方面涉及许多框架实现的一些常见类别,如 Scikit-Learn 的StandardScaler
。实际上,虽然零均值化是特征级别的操作,但白化滤波器需要考虑整个协方差矩阵(StandardScaler
仅实现单位方差,特征级别的缩放)。
幸运的是,所有从 Scikit-Learn 算法中受益或需要白化预处理步骤的算法都提供内置功能,因此通常不需要进一步操作;然而,对于所有希望直接实现一些算法的读者,我已经编写了两个 Python 函数,可以用于零均值化和白化。它们假设一个形状为 (N[Samples] × n) 的矩阵 X。此外,whiten()
函数接受参数 correct
,允许我们应用缩放校正(默认值为 True
):
import numpy as np
def zero_center(X):
return X - np.mean(X, axis=0)
def whiten(X, correct=True):
Xc = zero_center(X)
_, L, V = np.linalg.svd(Xc)
W = np.dot(V.T, np.diag(1.0 / L))
return np.dot(Xc, W) * np.sqrt(X.shape[0]) if correct else 1.0
训练集和验证集
在实际问题中,样本数量是有限的,通常有必要将初始集X(连同Y)分成两个子集,如下所示:
-
训练集用于训练模型
-
验证集用于评估模型分数,而不带任何偏差,使用从未见过的样本
根据问题的性质,可以选择 70% - 30%的分割百分比(在机器学习中,这是一个好的实践,因为数据集相对较小),或者对于样本数量非常高的深度学习任务,可以选择更高的训练百分比(80%,90%,甚至高达 99%)。在两种情况下,我们假设训练集包含进行一致泛化所需的所有信息。在许多简单的情况下,这是真的,并且可以很容易地验证;但面对更复杂的数据集,问题就变得更加困难。即使我们认为从相同的分布中抽取所有样本,也可能发生随机选择的测试集包含其他训练样本中不存在的特征。这种条件可能会对全局准确度产生非常负面的影响,而且如果没有其他方法,也可能非常难以识别。这就是为什么在深度学习中,训练集通常很大:考虑到特征和生成数据分布的复杂性,选择大的测试集可以限制学习特定关联的可能性。
在 Scikit-Learn 中,可以使用train_test_split()
函数来分割原始数据集,该函数允许指定训练/测试大小,并且如果我们期望有随机打乱的数据集(默认)。例如,如果我们想将X
和Y
分割为 70%的训练和 30%的测试,我们可以使用:
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, train_size=0.7, random_state=1)
打乱数据集始终是一个好的实践,目的是减少样本之间的相关性。实际上,我们假设X
由独立同分布(i.i.d)的样本组成,但有时连续两个样本之间会有很强的相关性,这会降低训练性能。在某些情况下,在每次训练周期后重新打乱训练集也是有用的;然而,在大多数我们的例子中,我们将在整个过程中使用相同打乱的数据集。当处理序列和具有记忆的模型时,必须避免打乱:在这些所有情况下,我们需要利用现有的相关性来确定未来样本的分布。
当使用 NumPy 和 Scikit-Learn 时,始终将随机种子设置为常数是一个好的实践,这样其他人就可以使用相同的初始条件重现实验。这可以通过调用np.random.seed
(...)并使用许多 Scikit-Learn 方法中存在的random-state
参数来实现。
交叉验证
一种有效的方法来检测错误选择的测试集问题是由交叉验证技术提供的。特别是,我们将使用K 折交叉验证方法。想法是将整个数据集 X 分割为一个移动的测试集和一个训练集(剩余的部分)。测试集的大小由折数决定,以便在 k 次迭代中,测试集覆盖整个原始数据集。
在下面的图中,我们可以看到该过程的示意图:
K 折交叉验证方案
这样,就可以使用不同的采样分割来评估模型的准确率,并且可以在更大的数据集上进行训练过程;特别是,在 (k-1)N* 样本上。在理想情况下,准确率应该在所有迭代中非常相似;但在大多数实际情况下,准确率相当低于平均水平。这意味着训练集在构建时排除了包含必要特征以使模型适应考虑实际 p[data] 的分离超平面的样本。我们将在本章后面讨论这些问题;然而,如果准确率的标准差过高(必须根据问题的性质/模型设置一个阈值),这可能意味着 X 没有从 p[data] 中均匀抽取,并且在预处理阶段评估异常值的影响是有用的。在下面的图中,我们可以看到在逻辑回归上进行的 15 折交叉验证的图表:
交叉验证准确率
值在 0.84 到 0.95 之间波动,平均值为 0.91(实线水平线)。在这种情况下,考虑到最初的目的本是使用线性分类器,我们可以说所有折都产生了高准确率,证实了数据集是线性可分的;然而,有一些样本(在第九折中被排除)对于达到大约 0.88 的最小准确率是必要的。
K 折交叉验证有不同的变体,可以用来解决特定问题:
-
分层 K 折:标准的 K 折方法在分割数据集时不考虑概率分布 p(y|x),因此某些折可能理论上只包含有限数量的标签。分层 K 折则试图分割 X,使得所有标签都得到均匀的表示。
-
Leave-one-out(LOO):这种方法是最激进的,因为它创建了N个折,每个折包含N-1个训练样本和 1 个测试样本。这样,就使用了最大可能数量的样本进行训练,并且很容易检测算法是否能够以足够的准确率学习,或者是否应该采用另一种策略。这种方法的主要缺点是必须训练N个模型,当N非常大时,这可能会引起性能问题。此外,在大量样本的情况下,两个随机值相似的概率增加,因此许多折会产生几乎相同的结果。同时,LOO 限制了评估泛化能力的可能性,因为单个测试样本不足以进行合理的估计。
-
Leave-P-out(LPO):在这种情况下,测试样本的数量被设置为p(非不相交集合),因此折数等于n除以p的二项式系数。这种方法减轻了 LOO 的缺点,并且是 K-Fold 和 LOO 之间的权衡。折数可能非常高,但可以通过调整测试样本数量p来控制;然而,如果p不是足够小或足够大,二项式系数可能会爆炸。实际上,当p有大约n/2个样本时,折数达到最大值:
Scikit-Learn 实现了所有这些方法(以及一些其他变化),但我建议始终使用cross_val_score()
函数,这是一个辅助函数,允许将不同的方法应用于特定问题。在以下基于多项式支持向量机(SVM)和 MNIST 数字数据集的代码片段中,指定了折数(参数cv
)。这样,Scikit-Learn 将自动使用分层 K-Fold 进行分类,而对于所有其他情况则使用标准 K-Fold:
from sklearn.datasets import load_digits
from sklearn.model_selection import cross_val_score
from sklearn.svm import SVC
data = load_digits()
svm = SVC(kernel='poly')
skf_scores = cross_val_score(svm, data['data'], data['target'], cv=10)
print(skf_scores)
[ 0.96216216 1\. 0.93922652 0.99444444 0.98882682 0.98882682
0.99441341 0.99438202 0.96045198 0.96590909]
print(skf_scores.mean())
0.978864325583
在每个折中,准确率都非常高(> 0.9),因此我们预计使用 LOO 方法会有更高的准确率。由于我们有 1,797 个样本,我们预计会有相同数量的准确率:
from sklearn.model_selection import cross_val_score, LeaveOneOut
loo_scores = cross_val_score(svm, data['data'], data['target'], cv=LeaveOneOut())
print(loo_scores[0:100])
[ 1\. 1\. 1\. 1\. 1\. 0\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 0\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 0\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1\. 1.]
print(loo_scores.mean())
0.988870339455
如预期,平均分数非常高,但仍有一些样本被错误分类。正如我们将要讨论的,这种情况可能是过度拟合的潜在候选者,这意味着模型完美地学习了如何映射训练集,但它失去了泛化的能力;然而,由于验证集的大小,LOO 并不是衡量这种模型能力的好方法。
我们现在可以使用 LPO 技术评估我们的算法。考虑到之前所解释的内容,我们选择了较小的 Iris 数据集和基于逻辑回归的分类。由于有N=150个样本,选择p = 3
,我们得到 551,300 个折:
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, LeavePOut
data = load_iris()
p = 3
lr = LogisticRegression()
lpo_scores = cross_val_score(lr, data['data'], data['target'], cv=LeavePOut(p))
print(lpo_scores[0:100])
[ 1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 1\. 1\. 1\. 1\. 1\. 1.
1\. 0.66666667 ...
print(lpo_scores.mean())
0.955668420098
如前例所示,我们只打印了前 100 个准确率;然而,只需几个值就可以立即了解全局趋势。
交叉验证技术是一种强大的工具,当性能成本不是太高时尤其有用。不幸的是,它并不是深度学习模型的最佳选择,因为数据集非常大,训练过程可能需要甚至几天才能完成。然而,正如我们将要讨论的,在这些情况下,正确的选择(分割百分比),结合对数据集的准确分析以及采用标准化和正则化等技术,可以使模型展现出卓越的泛化能力。
机器学习模型的特点
在本节中,我们将考虑监督模型,并试图确定如何测量它们的理论潜在准确性和它们在从 p[data] 中抽取的所有可能样本上正确泛化的能力。这些概念中的大多数都是在深度学习时代之前开发的,但继续对研究项目产生巨大影响。例如,“容量”的想法是神经科学家不断问自己的关于人脑的开放性问题。具有数十层和数百万参数的现代深度学习模型从数学角度重新开启了理论问题。与此相关,其他元素,如估计量方差的限制,再次成为焦点,因为算法变得越来越强大,曾经被认为远非可行解决方案的性能现在已成为现实。能够训练一个模型,以便充分利用其容量,最大化其泛化能力,并提高准确性,甚至超越人类表现,是深度学习工程师现在必须从他的工作中期待的东西。
模型的容量
如果我们将监督模型视为一组参数化函数,我们可以将表示能力定义为某种通用函数映射相对大量数据分布的内在能力。为了理解这个概念,让我们考虑一个允许无限导数的函数 f(x),并将其重写为泰勒展开式:
我们可以选择只取前 n 项,以便得到一个 n 次多项式函数。考虑一个简单的二维场景,有六个函数(从线性函数开始);我们可以通过一组小数据点观察它们的不同行为:
六条多项式分离曲线产生的不同行为
快速改变曲率的能力与度数成正比。如果我们选择一个线性分类器,我们只能修改其斜率(例子总是在二维空间中)和截距。相反,如果我们选择一个更高阶的函数,当需要时,我们有更多可能性来弯曲曲率。如果我们考虑图中的n=1和n=2(在右上角,它们是第一个和第二个函数),对于n=1,我们可以包括对应x=11的点,但这种选择对x=5处的点有负面影响。
只有一个参数化的非线性函数才能有效地解决这个问题,因为这个问题需要比线性分类器提供的表示能力更高的表示能力。另一个经典的例子是 XOR 函数。长期以来,许多研究人员反对感知器(线性神经网络),因为它们无法对由 XOR 函数生成的数据集进行分类。幸运的是,多层感知器的引入,以及非线性函数的使用,使我们能够克服这个问题,并且许多复杂度超出了任何经典机器学习模型的可能性。
Vapnik-Chervonenkis 容量
Vapnik-Chervonenkis 理论为分类器的容量提供了一个数学形式化。为了引入定义,首先需要定义分割的概念。如果我们有一个集合类C和一个集合M,我们说C分割M,如果:
换句话说,对于任何M的子集,它都可以作为C (c[j])的一个特定实例和M本身的交集来获得。如果我们现在将一个模型视为一个参数化函数:
我们想确定它与有限数据集X的容量关系:
根据 Vapnik-Chervonenkis 理论,我们可以说模型f分割X,如果对于每个可能的标签分配都没有分类错误。因此,我们可以定义Vapnik-Chervonenkis 容量或VC 容量(有时称为VC 维数)为能够分割X的X的子集的最大基数。
例如,如果我们考虑一个在二维空间中的线性分类器,其 VC 容量等于 3,因为总是有可能标记三个样本,使得f将它们分割;然而,在N > 3的所有情况下,这是不可能的。XOR 问题是一个需要 VC 容量高于3的例子。让我们探索以下图表:
不同分割曲线的 XOR 问题
这种特定的标签选择使得集合不可线性分离。克服这个问题的唯一方法就是使用高阶函数(或非线性函数)。曲线线(属于一个 VC 容量大于 3 的分类器)可以分离上左和下右区域与剩余空间,但没有直线可以做到这一点(尽管它总是可以分离一个点与其他三个点)。
估计器的偏差
现在让我们考虑一个具有单个向量参数的参数化模型(这并不是一个限制,而只是一个教学选择):
学习过程的目标是估计参数 θ,以便例如最大化分类的准确度。我们定义估计器的 偏差(与参数 θ 相关):
换句话说,偏差是估计的期望值与真实参数值之间的差异。记住,估计是 X 的函数,不能在求和中被视为常数。
如果一个估计器被称为 无偏,那么:
此外,如果估计序列收敛(至少以概率 1)到真实值当 k → ∞ 时,则估计器被定义为 一致的:
给定一个数据集 X,其样本是从 p[data] 中抽取的,估计器的准确性与它的偏差成反比。低偏差(或无偏差)的估计器能够以高精度水平映射数据集 X,而高偏差估计器很可能没有足够的能力来解决该问题,因此它们检测整个动态的能力较差。
现在让我们计算偏差相对于向量 θ 的导数(它将在以后很有用):
考虑到最后一个方程,由于 E[•] 的线性,即使我们在 θ 的估计中添加一个不依赖于 x 的项,该方程也成立。实际上,根据概率定律,很容易验证:
欠拟合
偏差大的模型很可能会欠拟合训练集。让我们考虑以下图表所示的情景:
欠拟合的分类器:曲线无法正确区分两个类别
即使问题非常困难,我们也可以尝试采用线性模型,在训练过程的最后,分离线的斜率和截距大约是-1 和 0(如图所示);然而,如果我们测量准确率,我们会发现它接近 0!无论迭代次数多少,这个模型永远无法学习到X和Y之间的关联。这种条件被称为欠拟合,其主要指标是训练准确率非常低。即使某些数据预处理步骤可以提高准确率,当模型欠拟合时,唯一有效的解决方案是采用更高容量的模型。
在机器学习任务中,我们的目标是实现最大的准确率,从训练集开始,然后过渡到验证集。更正式地说,我们希望改进我们的模型,以便尽可能接近贝叶斯准确率。这不是一个明确定义的值,而是一个理论上可能通过估计量实现的极限。在下面的图中,我们可以看到这个过程的表现:
准确率水平图
贝叶斯准确率通常是一个纯粹的理论极限,对于许多任务来说,即使使用生物系统也几乎不可能实现;然而,深度学习领域的进步允许创建目标准确率略低于贝叶斯准确率的模型。一般来说,贝叶斯准确率没有封闭形式,因此人类能力被视为基准。在先前的分类示例中,人类能够立即区分不同的点类,但对于一个容量有限的分类器来说,这个问题可能非常困难。我们将讨论的一些模型可以通过非常高的目标准确率解决这个问题,但在这个阶段,我们面临另一个风险,这可以在定义估计量方差的概念后理解。
估计量方差
在本章的开头,我们定义了数据生成过程p[data],并假设我们的数据集X是从这个分布中抽取的;然而,我们不想学习仅限于X的现有关系,而是期望我们的模型能够正确泛化到从p[data]中抽取的任何其他子集。衡量这种能力的好方法是估计量的方差:
方差也可以定义为标准误差的平方(类似于标准差)。高方差意味着当选择新的子集时,准确率会有很大的变化,因为模型可能通过过度学习一组有限的关系而达到了非常高的训练准确率,并且几乎完全失去了泛化的能力。
过拟合
如果欠拟合是低能力和高偏差的结果,过拟合是一种高方差可以检测的现象。一般来说,我们可以观察到非常高的训练准确率(甚至接近贝叶斯水平),但验证准确率并不差。这意味着模型的能力对于任务来说足够高,甚至过高(能力越高,大方差的可能性越高),并且训练集并不是p[data]的良好表示。为了理解这个问题,考虑以下分类场景:
可接受的拟合(左侧),过拟合的分类器(右侧)
左侧的图是通过逻辑回归得到的,而右侧的图使用的是具有六次多项式核的 SVM 算法。如果我们考虑第二个模型,决策边界看起来要精确得多,有些样本刚好在边界之上。考虑到两个子集的形状,可以说非线性 SVM 能更好地捕捉动态变化;然而,如果我们从p[data]中采样另一个数据集,并且对角线尾部变宽,逻辑回归仍然能够正确分类点,而 SVM 的准确度会大幅下降。第二个模型很可能过拟合,需要进行一些修正。当验证准确率远低于训练准确率时,一个好的策略是增加训练样本的数量,以考虑真实的p[data]。实际上,可能会发生这样的情况:训练集是从一个假设的分布中构建的,这个分布并不反映真实情况;或者用于验证的样本数量过高,减少了剩余样本所携带的信息量。交叉验证是评估数据集质量的好方法,但总是有可能发现完全新的子集(例如,当应用程序在生产环境中部署时生成的),即使它们本应属于p[data]。如果无法扩大训练集,数据增强可能是一个有效的解决方案,因为它允许从已知信息中创建人工样本(对于图像,可以镜像、旋转或模糊它们)。防止过拟合的其他策略基于一种称为正则化的技术,我们将在本章的最后部分讨论。现在,我们可以这样说,正则化的效果类似于部分线性化,这意味着能力降低,随之而来的是方差减少。
克拉美罗界
如果在理论上可以创建一个无偏模型(即使是渐近的),那么对于方差来说这并不成立。为了理解这个概念,有必要引入一个重要的定义:费舍尔信息。如果我们有一个参数化的模型和一个数据生成过程 p[data],我们可以通过考虑以下参数来定义似然函数:
这个函数可以用来衡量模型描述原始数据生成过程的好坏。似然的形状可能会有很大的变化,从定义良好、尖锐的曲线到几乎平坦的表面。让我们考虑以下图表,展示了基于单个参数的两个例子:
非常尖锐的似然(左),较平的似然(右)
我们可以立即理解,在第一种情况下,通过梯度上升可以很容易地达到最大似然,因为表面非常尖锐。而在第二种情况下,梯度的大小较小,由于数值不精确或容差,很容易在达到实际最大值之前停止。在最坏的情况下,表面可以在非常大的区域内几乎平坦,相应的梯度接近零。当然,我们希望始终使用非常尖锐和尖锐的似然函数,因为它们携带更多关于其最大值的信息。更正式地说,费舍尔信息量化了这个值。对于单个参数,它被定义为以下:
费舍尔信息是一个无界的非负数,它与对数似然所携带的信息量成正比;使用对数对梯度上升没有影响,但它通过将乘积转换为和来简化复杂表达式。这个值可以解释为当函数达到最大值时梯度的速度;因此,更高的值意味着更好的近似,而一个假设的零值意味着确定正确参数估计的概率也是零。
当与一组 K 个参数一起工作时,费舍尔信息成为一个正半定矩阵:
这个矩阵是对称的,并且还有一个重要的特性:当一个值为零时,这意味着对应的参数对在最大似然估计中是正交的,并且可以单独考虑。在许多实际情况下,如果一个值接近零,它决定了参数之间非常低的关联性,即使它不是数学上严格的,仍然可以解耦它们。
在这一点上,可以引入克拉美罗界,它表明对于采用x(具有概率分布p(x; θ))作为测量集的任何无偏估计量,任何θ估计量的方差总是根据以下不等式有下界:
实际上,考虑最初一个通用的估计量,并利用柯西-施瓦茨不等式与方差和费舍尔信息(两者都表示为期望值),我们得到:
现在,如果我们使用关于θ的偏导数的表达式,考虑到θ的估计值的期望值不依赖于x,我们可以将不等式的右侧重写为:
如果估计量是无偏的,则右侧的导数等于零,因此,我们得到:
换句话说,我们可以尝试减少方差,但它总是由费舍尔信息的倒数有下界。因此,给定一个数据集和一个模型,总是存在泛化能力的极限。在某些情况下,这个度量很容易确定;然而,它的真实值是理论上的,因为它为似然函数提供了另一个基本属性:它携带了估计方差最坏情况所需的所有信息。这并不令人惊讶:当我们讨论模型的容量时,我们看到了不同的函数如何导致更高的或更低的精度。如果训练精度足够高,这意味着容量对于问题来说是适当的,甚至过多;然而,我们还没有考虑似然p(X| θ)的作用。
高容量模型,特别是当数据集小或信息量低时,比低容量模型更有可能驱动到平坦似然表面。因此,费舍尔信息往往会变得较小,因为越来越多的参数集会产生相似的似然值,而这最终会导致更高的方差和过拟合的风险增加。为了总结本节,考虑从奥卡姆剃刀原则推导出的一个一般经验法则是有用的:当更简单的模型能够足够准确地解释一个现象时,增加其容量是没有意义的。一个更简单的模型总是更可取(当性能良好且能准确代表特定问题时),因为它在训练和推理阶段通常都更快,也更高效。当谈到深度神经网络时,这个原则可以以更精确的方式应用,因为更容易增加或减少层数和神经元数量,直到达到所需的精度。
损失和成本函数
在本章的开头,我们讨论了通用目标函数的概念,以便优化以解决机器学习问题。更正式地说,在一个监督场景中,我们拥有有限的数据集 X 和 Y:
我们可以为单个样本定义一个通用的损失函数:
J 是整个参数集的函数,必须与真实标签和预测之间的误差成正比。另一个重要属性是凸性。在许多实际情况下,这是一个几乎不可能的条件;然而,寻找凸损失函数总是有用的,因为它们可以通过梯度下降法轻松优化。我们将在第九章,“用于机器学习的神经网络”中讨论这个话题。然而,现在,将损失函数视为训练过程和纯数学优化之间的中介是有用的。缺失的环节是完整的数据。正如已经讨论过的,X 是从 p[data] 中抽取的,因此它应该代表真实分布。因此,当最小化损失函数时,我们正在考虑一个潜在的点子集,而不是整个真实数据集。在许多情况下,这并不是一个限制,因为,如果偏差为零且方差足够小,所得到的模型将显示出良好的泛化能力(高训练和验证准确率);然而,考虑到数据生成过程,引入另一个称为预期风险的度量是有用的:
这个值可以解释为从 p[data] 中抽取的所有可能样本的损失函数的平均值。最小化预期风险意味着全局准确性的最大化。相反,当使用有限数量的训练样本时,通常定义一个成本函数(通常也称为损失函数,不要与对数似然混淆):
这是我们要最小化的实际函数,除以样本数(一个没有影响的因素),它也称为经验风险,因为它是对预期风险的一个近似(基于真实数据)。换句话说,我们想要找到一组参数,使得:
当成本函数有超过两个参数时,理解其内部结构非常困难,甚至可能不可能;然而,我们可以使用二维图分析一些潜在条件:
在二维场景中不同类型的点
我们可以观察到的不同情况是:
-
起点,由于误差,通常成本函数在这里非常高。
-
局部最小值,梯度为零(且二阶导数为正)。它们是最佳参数集的候选者,但不幸的是,如果凹度不够深,惯性运动或一些噪声可以轻易地将点移开。
-
脊(或局部最大值),梯度为零,二阶导数为负。它们是不稳定点,因为最小的扰动允许逃离,达到更低成本的区域。
-
平台,或表面几乎平坦且梯度接近零的区域。逃离平台唯一的方法是保持残余动能——我们将在讨论神经优化算法时介绍这个概念(第九章,Neural Networks for Machine Learning)。
-
全局最小值,我们想要达到以优化成本函数的点。
即使当参数数量较小时,局部最小值很可能是存在的,但当模型具有大量参数时,它们变得非常不可能。事实上,一个 n-维点 θ^* 对于一个凸函数(这里我们假设 L 是凸的)来说是一个局部最小值,仅当:
第二个条件要求海森矩阵是正半定的(等价地,由前 n 行和 n 列构成的所有主子矩阵 H[n] 必须是非负的),因此所有特征值 λ[0],λ[1],...,λ[N] 必须是非负的。这个概率随着参数数量的增加而降低(H 是一个 n×n 的方阵,并且有 n 个特征值),在深度学习模型中,权重的数量可以达到 1000 万(甚至更多)时,这个概率接近于零。对完整数学证明感兴趣的读者可以阅读 High Dimensional Spaces,Deep Learning and Adversarial Examples,Dube S., arXiv:1801.00634 [cs.CV]。因此,一个更常见的条件是考虑鞍点的存在,其中特征值具有不同的符号,正交方向导数为零,即使这些点既不是局部最大值也不是局部最小值。例如,考虑以下图形:
二维场景中的鞍点
函数是 y=x3,其第一和二阶导数分别是 y'=3x2 和 y''=6x。因此,y'(0)=y''(0)=0。在这种情况下(单值函数),这个点也被称为拐点,因为在 x=0 时,函数显示出凹性的变化。在三维空间中,更容易理解为什么鞍点被这样称呼。例如,考虑以下图形:
三维场景中的鞍点
表面非常类似于马鞍,如果我们把点投影到一个正交平面上,XZ是一个最小值,而在另一个平面上(YZ)它是一个最大值。鞍点相当危险,因为许多更简单的优化算法可能会减慢甚至停止,失去找到正确方向的能力。在第九章,《机器学习的神经网络》中,我们将讨论一些能够减轻这种问题的方法,允许深度模型收敛。
成本函数的例子
在本节中,我们介绍了在分类和回归任务中常用的某些成本函数。其中一些将在下一章的示例中广泛采用,尤其是在讨论浅层和深层神经网络中的训练过程时。
均方误差
均方误差是最常见的回归成本函数之一。其通用表达式是:
这个函数在其定义域的每个点上都是可微的,并且是凸函数,因此可以使用随机梯度下降(SGD)算法进行优化;然而,当在存在异常值的情况下用于回归时,存在一个缺点。因为当预测值与实际值(对应于异常值)之间的距离很大时,其值总是二次的,相对误差很高,这可能导致无法接受的校正。
Huber 成本函数
如所述,均方误差对异常值不稳健,因为它总是二次的,而不管实际值与预测值之间的距离如何。为了克服这个问题,可以采用基于阈值 t[H] 的Huber成本函数,这样对于小于 t[H] 的距离,其行为是二次的,而对于大于 t[H,] 的距离,它变为线性,减少了误差的大小,因此,减少了异常值的相对重要性。
分析表达式是:
Hinge 成本函数
这个成本函数被 SVM 采用,其目标是最大化分离边界(支持向量所在的位置)之间的距离。其分析表达式是:
与其他示例不同,这个成本函数不是使用经典随机梯度下降方法进行优化的,因为它在所有点上的不可微性:
因此,SVM 算法使用二次规划技术进行优化。
类别交叉熵
类别交叉熵是最常见的分类成本函数,被逻辑回归和大多数神经网络架构采用。其通用分析表达式是:
这个损失函数是凸的,并且可以使用随机梯度下降技术轻松优化;此外,它还有一个重要的解释。如果我们正在训练一个分类器,我们的目标是创建一个分布尽可能接近 pdata 的模型。这个条件可以通过最小化两个分布之间的 Kullback-Leibler 散度来实现:
在前面的表达式中,p[M] 是模型生成的分布。现在,如果我们重新写散度,我们得到:
第一个项是数据生成分布的熵,它不依赖于模型参数,而第二个项是交叉熵。因此,如果我们最小化交叉熵,我们也会最小化 Kullback-Leibler 散度,迫使模型重现一个与 p[data] 非常相似的分布。这是关于为什么交叉熵损失函数是分类问题的一个非常好的选择的一个非常优雅的解释。
正则化
当一个模型条件不良或容易过拟合时,正则化提供了一些有效的工具来缓解这些问题。从数学角度来看,正则化器是添加到损失函数中的惩罚,以对参数的演变施加额外条件:
参数 λ 控制正则化的强度,这通过函数 g(θ) 来表达。对 g(θ) 的一个基本条件是它必须是可微的,这样新的复合损失函数仍然可以使用 SGD 算法进行优化。一般来说,任何正则函数都可以使用;然而,我们通常需要一个可以对比参数不定增长的函数。
为了理解原理,让我们考虑以下图表:
使用线性曲线(左侧)和抛物线(右侧)进行插值
在第一个图表中,模型是线性的并且有两个参数,而在第二个图表中,它是二次的并且有三个参数。我们已经知道第二个选项更容易过拟合,但是如果我们应用正则化项,就有可能避免(第一个二次参数)的增长,将模型转换为线性化版本。当然,选择低容量模型和应用正则化约束之间有一个区别。事实上,在前一种情况下,我们放弃了额外容量提供的可能性,冒着增加偏差的风险,而通过正则化,我们保持相同的模型但优化它以减少方差。现在让我们探索最常见的正则化技术。
Ridge
Ridge 正则化(也称为 Tikhonov 正则化)基于参数向量的平方 L2 范数:
这种惩罚避免了参数的无穷增长(因此,它也被称为权重收缩),当模型条件不良或存在多重共线性(由于样本完全独立,这是一个相对常见的情况)时,特别有用。
在以下图表中,我们看到在二维场景中岭回归正则化的示意图:
岭回归(L2)正则化
以零为中心的圆表示岭回归边界,而阴影表面是原始成本函数。没有正则化时,最小值(w[1],w[2])的幅度(例如,从原点到距离)大约是应用岭约束后获得的两倍,这证实了预期的收缩。当应用于使用普通最小二乘法(OLS)算法解决的回归时,可以证明总存在一个岭系数,使得权重相对于 OLS 权重收缩。在有些限制下,同样的结果可以扩展到其他成本函数。
Lasso
Lasso正则化基于参数向量的L1范数:
与岭回归不同,岭回归会缩小所有权重,而 Lasso 可以将最小的权重移至零,从而创建一个稀疏的参数向量。数学证明超出了本书的范围;然而,通过考虑以下图表(二维)可以直观地理解它:
Lasso(L1)正则化
以零为中心的正方形表示 Lasso 边界。如果我们考虑一条通用线,那么在角落处与正方形相切的概率更高,在这些角落处至少有一个(在二维场景中恰好一个)参数为零。一般来说,如果我们有一个向量凸函数 f(x)(我们在第五章,EM 算法与应用中提供了凸性的定义),我们可以定义:
由于任何L[p]范数都是凸的,以及凸函数之和也是凸的,因此g(x)也是凸的。正则化项始终是非负的,因此最小值对应于零向量的范数。当最小化g(x)时,我们还需要考虑以原点为中心的球体中范数梯度的贡献,然而,在该球体中,偏导数不存在。增加p的值,范数在原点周围变得平滑,并且当|x[i]| → 0时,偏导数接近零。
另一方面,当 p=1(排除 L[0]-范数以及所有 p ∈ ]0, 1[ 的范数,这些范数允许更强的稀疏性,但不是凸的)时,偏导数始终是 +1 或 -1,根据 x[i](x[i] ≠ 0)的符号。因此,对于 L[1]-范数来说,将最小的分量推向零更容易,因为其对最小化的贡献(例如,使用梯度下降)与 x[i, ]无关,而 L[2]-范数在接近原点时会降低其 速度。这是使用 L[1]-范数实现稀疏性的非严格解释。实际上,我们还需要考虑项 f(x),它限制了全局最小值的大小;然而,这有助于读者对概念形成直观理解。可以在 Optimization for Machine Learning, (edited by) Sra S., Nowozin S., Wright S. J., The MIT Press 中找到更多和数学上严谨的细节。
Lasso 正则化在需要数据集的稀疏表示时特别有用。例如,我们可能对找到一组图像对应的特征向量感兴趣。由于我们预计会有很多特征,但每个图像中只有子集存在,应用 Lasso 正则化可以强制所有最小的系数变为零,抑制次要特征的存在。另一个潜在的应用是潜在语义分析,我们的目标是使用有限数量的主题来描述语料库中的文档。所有这些方法都可以总结为一种称为 稀疏编码 的技术,其目标是通过对最代表性的原子进行提取,使用不同的方法实现稀疏性,从而降低数据集的维度(也包括非线性场景)。
ElasticNet
在许多实际情况下,应用 Ridge 和 Lasso 正则化以强制权重收缩和全局稀疏性是有用的。这可以通过使用 ElasticNet 正则化来实现,定义为:
每个正则化的强度由参数 λ[1] 和 λ[2] 控制。ElasticNet 在需要减轻过拟合影响并鼓励稀疏性的情况下可以产生优秀的结果。在讨论一些深度学习架构时,我们将应用所有正则化技术。
提前停止
尽管它是一种纯正则化技术,提前停止通常被视为在所有其他防止过拟合和最大化验证准确率的方法失败时的最后手段。在许多情况下(尤其是在深度学习场景中),可以观察到训练过程的典型行为,考虑到训练和验证成本函数:
在 U 曲线上升阶段开始之前提前停止的例子
在最初的几个时期,两个代价都会下降,但可能会发生这样的情况:在某个阈值时期e[s]之后,验证代价开始上升。如果我们继续训练过程,这会导致训练集过拟合并增加方差。因此,当没有其他选择时,可能需要提前停止训练过程。为了做到这一点,有必要在新的迭代开始之前存储最后一个参数向量,在没有改进或准确度下降的情况下,停止过程并恢复最后一个参数。正如所解释的,这个程序永远不应该被视为最佳选择,因为更好的模型或改进的数据集可能会产生更高的性能。使用提前停止,无法验证替代方案,因此它只能在过程的最后阶段采用,绝对不能在开始时采用。许多深度学习框架,如 Keras,都包括实现提前停止回调的帮助器;然而,重要的是要检查最后一个参数向量是在最后一个时期之前存储的,还是对应于e[s]的。在这种情况下,重复训练过程,在 e[s]*之前的时期停止(在那里达到了最小的验证代价)可能是有用的。
摘要
在本章中,我们讨论了几乎所有机器学习模型共有的基本概念。在第一部分,我们介绍了数据生成过程,作为有限数据集的泛化。我们解释了将有限数据集划分为训练块和验证集的最常见策略,并介绍了交叉验证,这是避免静态划分局限性的最佳方法之一。
在第二部分,我们讨论了估计量的主要特性:容量、偏差和方差。我们还介绍了 Vapnik-Chervonenkis 理论,这是表示能力概念的数学形式化,并分析了高偏差和高方差的影响。特别是,我们讨论了欠拟合和过拟合等效应,并定义了它们与高偏差和高方差之间的关系。
在第三部分,我们介绍了损失函数和代价函数,首先作为预期风险的代理,然后详细介绍了优化问题中可能遇到的一些常见情况。我们还介绍了常见的代价函数及其主要特征。在最后一部分,我们讨论了正则化,解释了它如何减轻过拟合的影响。
在下一章,第二章,半监督学习简介中,我们将介绍半监督学习,重点关注归纳学习和演绎学习等概念。
第二章:半监督学习简介
半监督学习是机器学习的一个分支,它试图通过采用属于聚类和分类方法的概念,以解决既有标签数据又有未标记数据的问题。与正确标记大型数据集的困难相比,未标记样本的高可用性驱使许多研究人员调查最佳方法,这些方法允许在不损失准确性的情况下,将标签样本提供的知识扩展到更大的未标记群体中。在本章中,我们将介绍这个分支,特别是我们将讨论:
-
半监督场景
-
在这种场景下高效操作所需的假设
-
半监督学习的不同方法
-
生成高斯混合模型算法
-
对比悲观似然估计方法
-
半监督支持向量机(S³VM)
-
归纳支持向量机(TSVM)
半监督场景
一个典型的半监督场景与监督场景并没有太大的不同。假设我们有一个数据生成过程,p[data]:
然而,与监督方法相反,我们只有有限数量的样本(N)是从 p[data] 中抽取的,并且提供了标签,如下所示:
相反,我们有一个更大的样本量(M),这些样本是从边缘分布 p(x) 中抽取的:
通常,对 N 和 M 的值没有限制;然而,当未标记样本的数量远多于完整样本的数量时,就会出现半监督问题。如果我们能从 p[data] 中抽取 N >> M 个标记样本,继续使用半监督方法可能就没什么用了,而选择经典监督方法可能是最佳选择。我们需要额外的复杂性是由 M >> N 来证明的,这在所有那些可用的未标记数据量很大,而正确标记样本数量相当低的情况中是常见的。例如,我们可以轻松地访问数百万张免费图片,但详细标记的数据集很昂贵,并且只包括可能性的一小部分。然而,是否总是可能应用半监督学习来改进我们的模型?对这个问题的答案几乎是显而易见的:不幸的是,不是。作为一个经验法则,我们可以这样说,如果 X[u] 的知识增加了我们对先验分布 p(x) 的了解,那么半监督算法可能比纯监督(因此仅限于 X[l]) 对手表现得更好。另一方面,如果未标记样本来自不同的分布,最终结果可能会相当糟糕。在实际情况下,并不总是立即有必要决定是否半监督算法是最好的选择;因此,交叉验证和比较是在评估场景时采用的最佳实践。
归纳学习
当半监督模型旨在为未标记样本寻找标签时,这种方法被称为归纳学习。在这种情况下,我们并不感兴趣于建模整个分布 p(x|y),这意味着确定两个数据集的密度,而是仅对未标记点寻找 p(y|x)。在许多情况下,这种策略可以节省时间,并且当我们的目标更倾向于提高我们对未标记数据集的了解时,这始终是首选的。
归纳学习
与归纳学习相反,归纳学习考虑所有 X 样本,并试图确定一个完整的 p(x|y) 或函数 y=f(x),该函数可以将标记点和未标记点映射到它们相应的标签。通常,这种方法更复杂,需要更多计算时间;因此,根据 Vapnik 的原则,如果不要求或必要,始终选择最实用的解决方案,如果问题需要更多细节,可能还需要扩展它。
半监督假设
如前所述,半监督学习并不能保证提高监督模型。错误的选择可能导致性能的显著下降;然而,可以提出一些基本假设,这些假设对于半监督学习正常工作至关重要。它们并不总是数学上证明的定理,而是经验观察,这些观察为选择一种否则完全随机的途径提供了理由。
平滑性假设
让我们考虑一个实值函数 f(x) 以及相应的度量空间 X 和 Y。如果一个函数被称为 Lipschitz 连续的,那么它必须满足以下条件:
换句话说,如果两个点 x[1] 和 x[2] 相近,那么相应的输出值 y[1] 和 y[2] 也不能彼此相隔太远。这个条件在回归问题中是基本的,因为通常需要对位于训练样本之间的点进行泛化。例如,如果我们需要预测点 x[t] 的输出:x[1 ] < x[t] < x[2],并且回归器是 Lipschitz 连续的,我们可以确信 y[t] 将被正确地限制在 y[1] 和 y[2] 之间。这个条件通常被称为一般平滑性,但在半监督学习中,添加一个限制(与聚类假设相关)是有用的:如果两个点位于高密度区域(聚类)并且它们很近,那么相应的输出也必须很近。这个额外条件非常重要,因为如果两个样本位于低密度区域,它们可能属于不同的聚类,它们的标签可能非常不同。这并不总是正确的,但包含这个约束对于许多半监督模型定义中的进一步假设是有用的。
聚类假设
这个假设与前面的假设紧密相连,并且可能更容易接受。它可以表达为一系列相互依赖的条件。聚类是高密度区域;因此,如果两个点很近,它们很可能属于同一个聚类,它们的标签必须相同。低密度区域是分离空间;因此,属于低密度区域的样本很可能属于边界点,它们的类别可能不同。为了更好地理解这个概念,考虑监督 SVM 是有用的:只有支持向量应该位于低密度区域。让我们考虑以下二维示例:
在半监督场景中,我们无法知道属于高密度区域的点的标签;然而,如果它足够接近一个已标记的点,以至于可以构建一个所有点都具有相同平均密度的球体,我们允许预测测试样本的标签。相反,如果我们移动到低密度区域,这个过程变得更难,因为两个点可以非常接近但具有不同的标签。我们将在本章末讨论半监督、低密度分离问题。
流形假设
这是一个不太直观的假设,但它可以极大地减少许多问题的复杂性。首先,我们可以给出流形的非严格定义。一个n-流形是一个全局弯曲但局部与n-维欧几里得空间同胚的拓扑空间。在下面的图中,有一个流形的例子:ℜ³中的球面表面:
从球面得到的二维流形
在P点周围的小块(对于ε → 0)可以映射到一个平坦的圆形表面。因此,流形的性质在局部基于欧几里得几何,而在全局上,它们需要一个适当的数学扩展,这超出了本书的范围(更多信息可以在《黎曼流形上的半监督学习》,Belkin M.,Niyogi P.,《机器学习》56,2004)中找到)。
流形假设指出,p-维样本(其中p >> 1)大致位于一个p << q的q-维流形上。不进行过度的数学严谨性,我们可以这样说,例如,如果我们有N个1000-维有界向量,它们被包含在一个边长等于r的1000-维超立方体中。相应的n-体积是r^p = r¹⁰⁰⁰,因此,填满整个空间的可能性非常小(并且随着p的增加而减小)。我们观察到的是在低维流形上的高密度。例如,如果我们从太空中观察地球,我们可能会认为其居民在整个体积上均匀分布。我们知道这是错误的,实际上,我们可以创建表示在二维流形上的地图和地图集。使用三维向量来映射人的位置是没有意义的。使用投影和纬度、经度工作更容易。
这个假设授权我们应用降维方法来避免贝尔曼(在《动态规划与马尔可夫过程,罗纳德·A·霍华德,《麻省理工学院出版社》)提出的维度的诅咒。在机器学习的范围内,这种效应的主要后果是,当样本的维度增加时,为了达到高精度,必须使用越来越多的样本。此外,休斯观察到(这一现象以他的名字命名,并在论文休斯 G. F.,关于统计模式识别器的平均精度,IEEE 信息系统传输,1968,14/1)中提出,统计分类器的精度与样本的维度成反比。这意味着每当有可能在低维流形上工作(特别是在半监督场景中)时,就会实现两个优势:
-
更少的计算时间和内存消耗
-
更高的分类精度
生成高斯混合
生成高斯混合是半监督聚类的归纳算法。假设我们有一个包含 N 个样本(从 p[data] 中抽取)的标记数据集 (X[l], Y[l]) 和一个包含 M >> N 个样本(从边缘分布 p(x) 中抽取)的无标记数据集 X[u]。不一定要求 M >> N,但我们想创建一个只有少量标记样本的真实半监督场景。此外,我们假设所有无标记样本都与 p[data] 一致。这看起来像是一个恶性循环,但没有这个假设,程序就没有坚实的数学基础。我们的目标是使用生成模型确定一个完整的 p(x|y) 分布。一般来说,可以使用不同的先验,但我们现在使用多元高斯来模拟我们的数据:
因此,我们的模型参数是所有高斯均值和协方差矩阵。在其他上下文中,可以使用二项式或多项式分布。但是,程序不会改变;因此,让我们假设可以使用参数化分布 p(x|y, θ) 来近似 p(x|y)。我们可以通过最小化两个分布之间的 Kullback-Leibler 散度来实现这一目标:
在第五章,EM 算法及其应用中,我们将展示这等价于最大化数据集的似然。为了获得似然,必须定义预期的高斯数量(这可以从标记样本中得知)和一个表示特定高斯边缘概率的权重向量:
使用贝叶斯定理,我们得到:
由于我们同时处理标记和未标记的样本,前面的表达式有两个解释:
-
对于未标记的样本,它是通过将第i个高斯权重乘以相对于第i个高斯分布的概率p(x[j])来计算的。
-
对于标记样本,它可以表示为一个向量 p = [0, 0, ... 1, ... 0, 0],其中 1 是第i个元素。这样,我们迫使我们的模型相信标记样本,以便找到最大化整个数据集似然的最佳参数值。
通过这种区分,我们可以考虑一个单一的似然函数,其中术语fw已被每个样本的权重所替代:
使用 EM 算法(见第五章,EM 算法及其应用)可以最大化对数似然。在这种情况下,我们直接提供步骤:
-
p(y[i]|x[j],θ,w)是按照之前解释的方法计算的
-
使用以下规则更新高斯参数:
N是样本总数。必须迭代此过程,直到参数停止修改或修改小于一个固定的阈值。
生成高斯混合的示例
现在我们可以使用 Scikit-Learn 提供的make_blobs()
函数创建一个简单的二维数据集来实现这个模型:
from sklearn.datasets import make_blobs
nb_samples = 1000
nb_unlabeled = 750
X, Y = make_blobs(n_samples=nb_samples, n_features=2, centers=2, cluster_std=2.5, random_state=100)
unlabeled_idx = np.random.choice(np.arange(0, nb_samples, 1), replace=False, size=nb_unlabeled)
Y[unlabeled_idx] = -1
我们创建了属于 2 个类别的 1,000 个样本。然后随机选择了 750 个点作为我们的未标记数据集(相应的类别被设置为-1)。现在我们可以通过定义它们的均值、协方差和权重来初始化两个高斯分布。一种可能性是使用随机值:
import numpy as np
# First Gaussian
m1 = np.random.uniform(-7.5, 10.0, size=2)
c1 = np.random.uniform(5.0, 15.0, size=(2, 2))
c1 = np.dot(c1, c1.T)
q1 = 0.5
# Second Gaussian
m2 = np.random.uniform(-7.5, 10.0, size=2)
c2 = np.random.uniform(5.0, 15.0, size=(2, 2))
c2 = np.dot(c2, c2.T)
q2 = 0.5
然而,由于协方差矩阵必须是正半定的,因此改变随机值(通过将每个矩阵乘以其相应的转置)或设置硬编码的初始参数是有用的。在这种情况下,我们可以选择以下示例:
import numpy as np
# First Gaussian
m1 = np.array([-3.0, -4.5])
c1 = np.array([[25.0, 5.0],
[5.0, 35.0]])
q1 = 0.5
# Second Gaussian
m2 = np.array([5.0, 10.0])
c2 = np.array([[25.0, -10.0],
[-10.0, 25.0]])
q2 = 0.5
下图显示了结果图,其中小菱形代表未标记的点,大点代表已知类别的样本:
高斯混合的初始配置
两个高斯由同心椭圆表示。现在我们可以执行训练过程。为了简单起见,我们重复更新固定次数的迭代。读者可以轻松修改代码以引入一个阈值:
from scipy.stats import multivariate_normal
nb_iterations = 5
for i in range(nb_iterations):
Pij = np.zeros((nb_samples, 2))
for i in range(nb_samples):
if Y[i] == -1:
p1 = multivariate_normal.pdf(X[i], m1, c1, allow_singular=True) * q1
p2 = multivariate_normal.pdf(X[i], m2, c2, allow_singular=True) * q2
Pij[i] = [p1, p2] / (p1 + p2)
else:
Pij[i, :] = [1.0, 0.0] if Y[i] == 0 else [0.0, 1.0]
n = np.sum(Pij, axis=0)
m = np.sum(np.dot(Pij.T, X), axis=0)
m1 = np.dot(Pij[:, 0], X) / n[0]
m2 = np.dot(Pij[:, 1], X) / n[1]
q1 = n[0] / float(nb_samples)
q2 = n[1] / float(nb_samples)
c1 = np.zeros((2, 2))
c2 = np.zeros((2, 2))
for t in range(nb_samples):
c1 += Pij[t, 0] * np.outer(X[t] - m1, X[t] - m1)
c2 += Pij[t, 1] * np.outer(X[t] - m2, X[t] - m2)
c1 /= n[0]
c2 /= n[1]
每个循环开始时,首先要初始化将用于存储p(y[i]|x[j]**,θ,w)值的Pij
矩阵。然后,对于每个样本,我们可以计算p(y[i]|x[j],θ,w),考虑它是否被标记。高斯概率是通过 SciPy 函数multivariate_normal.pdf()
计算的。当整个P[ij]矩阵被填充后,我们可以更新两个高斯的参数(均值和协方差矩阵)以及相关权重。这个算法非常快;经过五次迭代后,我们得到以下图中表示的稳定状态:
两个高斯通过设置参数以覆盖高密度区域,完美地映射了空间。我们可以检查一些未标记的点,如下所示:
print(np.round(X[Y==-1][0:10], 3))
[[ 1.67 7.204]
[ -1.347 -5.672]
[ -2.395 10.952]
[ -0.261 6.526]
[ 1.053 8.961]
[ -0.579 -7.431]
[ 0.956 9.739]
[ -5.889 5.227]
[ -2.761 8.615]
[ -1.777 4.717]]
在之前的图中很容易找到它们。相应的类别可以通过最后一个P[ij]矩阵获得:
print(np.round(Pij[Y==-1][0:10], 3))
[[ 0.002 0.998]
[ 1\. 0\. ]
[ 0\. 1\. ]
[ 0.003 0.997]
[ 0\. 1\. ]
[ 1\. 0\. ]
[ 0\. 1\. ]
[ 0.007 0.993]
[ 0\. 1\. ]
[ 0.02 0.98 ]]
这立即验证了它们已经被正确标记并分配到正确的簇中。这个算法非常快,在密度估计方面产生了优秀的结果。在第五章,EM 算法及其应用中,我们将讨论这个算法的一般版本,解释基于 EM 算法的完整训练过程。
在所有涉及随机数的例子中,种子被设置为 1,000(np.random.seed(1000)
)。其他值或未重置的后续实验可能会产生略微不同的结果。
加权对数似然
在之前的例子中,我们考虑了标记和未标记样本的单个对数似然:
这相当于说我们像信任标记点一样信任未标记点。然而,在某些情况下,这个假设可能会导致完全错误的估计,如下图中所示:
偏斜的最终高斯混合配置
在这种情况下,两个高斯分布的均值和协方差矩阵都受到了未标记点的影响,导致的结果密度估计明显错误。当这种现象发生时,最好的做法是考虑双重加权对数似然。如果前N个样本是标记的,而接下来的M个是未标记的,对数似然可以表示如下:
在前面的公式中,如果 λ 小于 1,则可以降低未标记项的权重,使有标签数据集的重要性更高。算法的修改是微不足道的,因为每个未标记的权重都必须根据 λ 进行缩放,从而降低其估计概率。在 Semi-Supervised Learning,Chapelle O.,Schölkopf B.,Zien A.(编者),MIT Press 中,读者可以找到关于 λ 选择的一个非常详细的讨论。没有金科玉律;然而,一种可能的策略可能是基于对有标签数据集进行的交叉验证。另一种(更复杂)的方法是考虑不同的 λ 增加值,并选择对数似然最大的第一个值。我建议上述书籍以获取更多细节和策略。
对比悲观似然估计
如本章开头所述,在许多现实生活中的问题中,检索未标记样本比正确标记的样本更便宜。因此,许多研究人员致力于找到执行半监督分类的最佳策略,以超越监督方法。想法是使用少量有标签样本训练一个分类器,然后在添加加权未标记样本后提高其准确性。其中最好的结果是 M. Loog 提出的 对比悲观似然估计(CPLE)算法,在 Loog M.,Contrastive Pessimistic Likelihood Estimation for Semi-Supervised Classification,arXiv:1503.00269 中提出。
在解释此算法之前,有必要进行介绍。如果我们有一个包含 N 个样本的有标签数据集 (X, Y),我们可以定义一个通用估计器的对数似然成本函数,如下所示:
在训练模型之后,应该能够确定 p(y[i]|x[i], θ),这是给定样本 x[i] 的标签概率。然而,一些分类器不是基于这种方法(如 SVM)的,而是通过检查参数化函数 *f(x[i],θ) 的符号来评估正确的类别。由于 CPLE 是一个通用的框架,可以在没有概率的情况下与任何分类算法一起使用,因此实现一种称为 Platt 缩放的技术是有用的,该技术允许通过参数化 sigmoid 将决策函数转换为概率。对于二元分类器,它可以表示如下:
α 和 β 是必须学习的参数,以便最大化似然。幸运的是,Scikit-Learn 提供了 predict_proba()
方法,该方法返回所有类别的概率。Platt 缩放是自动执行或在需要时执行;例如,SCV 分类器需要将参数 probability=True
设置为计算概率映射。我总是建议在实现自定义解决方案之前检查文档。
我们可以考虑一个由标记和无标记样本组成的完整数据集。为了简化,我们可以重新组织原始数据集,使得前N个样本是标记的,而接下来的M个是无标记的:
由于我们不知道所有x^u样本的标签,我们可以决定在训练过程中使用M个k-维(k 是类别数)软标签q[i],这些标签可以被优化:
先前公式中的第二个条件是必要的,以保证每个q[i]代表一个离散概率(所有元素必须加起来等于 1.0)。因此,完整的对数似然成本函数可以表达如下:
第一个项代表监督部分的似然对数,而第二个项负责无标记点。如果我们只使用标记样本训练一个分类器,排除第二个加项,我们得到参数集θ[sup]。CPLE 定义了一个对比条件(也是一个对数似然),通过定义半监督方法给出的总成本函数的改进,与监督解决方案相比:
这个条件允许强制半监督解决方案必须优于监督解决方案,实际上,最大化它;我们同时增加了第一个项并减少了第二个项,从而实现了 CL(对比项在机器学习中非常常见,它通常表示一个条件,该条件是在两个相反约束之间的差异中实现的)。如果 CL 没有增加,这可能意味着无标记样本没有从从p[data]中提取的边缘分布p(x)中抽取。
此外,在先前的表达式中,我们隐式地使用了软标签,但由于它们最初是随机选择的,并且没有支持其值的真实标签,因此,通过施加悲观条件(作为另一个对数似然)不信任它们是一个好主意:
通过施加这个约束,我们试图找到最小化对比对数似然的软标签;这就是为什么这被定义为悲观方法。这似乎是一种矛盾;然而,信任软标签可能是危险的,因为半监督对数似然甚至可以通过大量误分类而增加。我们的目标是找到最佳的参数集,它能够从监督基线(使用标记样本获得)开始,保证最高的准确度,并改进它,同时不忘掉标记样本提供的结构特征。
因此,我们的最终目标可以表达如下:
对比悲观似然估计的示例
我们将使用从 MNIST 数据集提取的子集在 Python 中实现 CPLE 算法。为了简单起见,我们将只使用代表数字 0 和 1 的样本:
from sklearn.datasets import load_digits
import numpy as np
X_a, Y_a = load_digits(return_X_y=True)
X = np.vstack((X_a[Y_a == 0], X_a[Y_a == 1]))
Y = np.vstack((np.expand_dims(Y_a, axis=1)[Y_a==0], np.expand_dims(Y_a, axis=1)[Y_a==1]))
nb_samples = X.shape[0]
nb_dimensions = X.shape[1]
nb_unlabeled = 150
Y_true = np.zeros((nb_unlabeled,))
unlabeled_idx = np.random.choice(np.arange(0, nb_samples, 1), replace=False, size=nb_unlabeled)
Y_true = Y[unlabeled_idx].copy()
Y[unlabeled_idx] = -1
在创建包含 360 个样本的受限数据集(X,Y)之后,我们随机选择 150 个样本(大约 42%)成为未标记的样本(相应的 y 为 -1)。在这个时候,我们可以测量仅使用标记数据集训练的逻辑回归的性能:
from sklearn.linear_model import LogisticRegression
lr_test = LogisticRegression()
lr_test.fit(X[Y.squeeze() != -1], Y[Y.squeeze() != -1].squeeze())
unlabeled_score = lr_test.score(X[Y.squeeze() == -1], Y_true)
print(unlabeled_score)
0.573333333333
因此,逻辑回归在未标记样本的分类中显示了 57% 的准确率。我们还可以在整个数据集(在移除一些随机标签之前)上评估交叉验证分数:
from sklearn.model_selection import cross_val_score
total_cv_scores = cross_val_score(LogisticRegression(), X, Y.squeeze(), cv=10)
print(total_cv_scores)
[ 0.48648649 0.51351351 0.5 0.38888889 0.52777778 0.36111111
0.58333333 0.47222222 0.54285714 0.45714286]
因此,当所有标签都已知时,分类器在 10 折(每个测试集包含 36 个样本)的情况下实现了平均 48% 的准确率。
我们现在可以实施一个 CPLE 算法。首先,我们需要初始化一个 LogisticRegression
实例和软标签:
lr = LogisticRegression()
q0 = np.random.uniform(0, 1, size=nb_unlabeled)
q0 是一个在半开区间 [0, 1] 内有界值的随机数组;因此,我们还需要一个转换器将 q[i] 转换为实际的二进制标签:
我们可以使用 NumPy 函数 np.vectorize()
来实现这一点,它允许我们将转换应用于向量的所有元素:
trh = np.vectorize(lambda x: 0.0 if x < 0.5 else 1.0)
为了计算对数似然,我们还需要一个加权的对数损失(类似于 Scikit-Learn 函数 log_loss()
,然而,它计算的是负对数似然,但不支持权重):
def weighted_log_loss(yt, p, w=None, eps=1e-15):
if w is None:
w_t = np.ones((yt.shape[0], 2))
else:
w_t = np.vstack((w, 1.0 - w)).T
Y_t = np.vstack((1.0 - yt.squeeze(), yt.squeeze())).T
L_t = np.sum(w_t * Y_t * np.log(np.clip(p, eps, 1.0 - eps)), axis=1)
return np.mean(L_t)
此函数计算以下表达式:
我们还需要一个函数来构建具有可变软标签 q[i] 的数据集:
def build_dataset(q):
Y_unlabeled = trh(q)
X_n = np.zeros((nb_samples, nb_dimensions))
X_n[0:nb_samples - nb_unlabeled] = X[Y.squeeze()!=-1]
X_n[nb_samples - nb_unlabeled:] = X[Y.squeeze()==-1]
Y_n = np.zeros((nb_samples, 1))
Y_n[0:nb_samples - nb_unlabeled] = Y[Y.squeeze()!=-1]
Y_n[nb_samples - nb_unlabeled:] = np.expand_dims(Y_unlabeled, axis=1)
return X_n, Y_n
在这一点上,我们可以定义我们的对比对数似然:
def log_likelihood(q):
X_n, Y_n = build_dataset(q)
Y_soft = trh(q)
lr.fit(X_n, Y_n.squeeze())
p_sup = lr.predict_proba(X[Y.squeeze() != -1])
p_semi = lr.predict_proba(X[Y.squeeze() == -1])
l_sup = weighted_log_loss(Y[Y.squeeze() != -1], p_sup)
l_semi = weighted_log_loss(Y_soft, p_semi, q)
return l_semi - l_sup
此方法将由优化器调用,每次传递不同的 q 向量。第一步是构建新的数据集并计算 Y_soft
,这是与 q 对应的标签。然后,使用数据集(因为 Y_n
是一个 (k, 1) 数组,所以需要将其压缩以避免警告。当使用 Y 作为布尔指示器时,也执行相同操作)。在这个时候,可以使用 predict_proba()
方法计算 p[sup] 和 p[semi],最后,我们可以计算半监督和监督对数损失,这是我们要最小化的关于 q[i] 的项,而 θ 的最大化是在训练逻辑回归时隐式完成的。
优化使用 SciPy 中实现的 BFGS 算法进行:
from scipy.optimize import fmin_bfgs
q_end = fmin_bfgs(f=log_likelihood, x0=q0, maxiter=5000, disp=False)
这是一个非常快的算法,但鼓励用户尝试不同的方法或库。在这种情况下,我们需要两个参数:f
,这是要最小化的函数,以及x0
,这是独立变量的初始条件。maxiter
在未取得改进时避免过多迭代是有用的。一旦优化完成,q_end
包含最优软标签。因此,我们可以重建我们的数据集:
X_n, Y_n = build_dataset(q_end)
使用这个最终配置,我们可以重新训练逻辑回归并检查交叉验证的准确率:
final_semi_cv_scores = cross_val_score(LogisticRegression(), X_n, Y_n.squeeze(), cv=10)
print(final_semi_cv_scores)
[ 1\. 1\. 0.89189189 0.77777778 0.97222222 0.88888889
0.61111111 0.88571429 0.94285714 0.48571429]
基于 CPLE 算法的半监督解决方案实现了平均 84%的准确率,正如预期的那样,优于监督方法。读者可以尝试使用不同的分类器,如 SVM 或决策树,并验证当 CPLE 允许获得比其他监督算法更高的准确率时。
半监督支持向量机(S3VM)
当我们讨论聚类假设时,我们也定义了低密度区域为边界,相应的问题为低密度分离。一个基于这个概念的常见监督分类器是支持向量机(SVM),其目标是最大化样本必须位于的密集区域之间的距离。有关基于线性核的 SVM 的完整描述,请参阅Bonaccorso G.,机器学习算法,Packt Publishing;然而,提醒自己线性 SVM 的基本模型对于带有松弛变量ξ[i]是有用的:
此模型基于以下假设:y[i]可以是-1 或 1。松弛变量ξ[i]或软边界是变量,每个样本一个,引入以减少原始条件(min ||w||)施加的强度,该条件基于一个硬边界,它会将所有位于错误一侧的样本误分类。它们由 Hinge 损失定义如下:
使用这些变量,我们允许一些点在保持由相应的松弛变量控制的距离内超越限制,而不会发生误分类。在下图中,这个过程有一个示意图:
SVM 通用场景
每个高密度区域的最后元素是支持向量。它们之间有一个低密度区域(在某些情况下也可能是零密度区域),我们的分离超平面就位于其中。在第一章,机器学习模型基础中,我们定义了经验风险作为预期风险的代理;因此,我们可以将 SVM 问题转化为在 Hinge 成本函数(带或不带 w 上的 Ridge 正则化)下的经验风险最小化:
理论上,每个总是被包含支持向量的两个超平面所包含的函数都是一个好的分类器,但我们需要最小化经验风险(因此,期望风险);因此,我们寻找高密度区域之间的最大间隔。这个模型能够分离具有不规则边界的两个密集区域,通过采用核函数,也可以在非线性场景中分离。此时,自然的问题是我们需要解决这种问题的半监督场景中,如何将标记样本和未标记样本的最佳策略整合在一起。
首先需要考虑的是比例:如果我们有很少的未标记点,问题主要是监督性的,使用训练集学习到的泛化能力应该足够正确分类所有未标记点。另一方面,如果未标记样本的数量大得多,我们几乎回到了纯聚类场景(就像在关于生成高斯混合的段落中讨论的那样)。因此,为了利用半监督方法在低密度分离问题中的优势,我们应该考虑标记/未标记比例大约为 1.0 的情况。然而,即使我们有一个类别的优势(例如,如果我们有一个巨大的未标记数据集和很少的标记样本),我们总是可以使用我们即将讨论的算法,即使有时它们的性能可能等于或低于纯监督/聚类解决方案。例如,当标记/未标记比例非常小的时候,Transductive SMVs(传递 SMVs)显示了更好的准确率,而其他方法可能会表现出完全不同的行为。然而,当处理半监督学习(及其假设)时,始终要记住每个问题既是监督性的又是无监督性的,并且最佳解决方案必须在每个不同的环境中进行评估。
对于这个问题,半监督 SVM(也称为S³VM)算法提供了一个解决方案。如果我们有N个标记样本和M个未标记样本,目标函数如下:
第一个项强加了标准 SVM 关于最大分离距离的条件,而第二个块被分为两部分:
-
我们需要添加N个松弛变量η[i]来保证标记样本的软边界。
-
同时,我们必须考虑那些可能被分类为+1 或-1 的未标记点。因此,我们有两个相应的松弛变量集ξ[i]和 z[i]。然而,我们想要找到每个可能对的最小变量,以确保未标记样本被放置在实现最大准确率的子空间中。
解决问题的必要约束如下:
第一个约束仅限于标记点,它与监督 SVM 相同。接下来的两个约束,相反,考虑了无标记样本可能被分类为+1 或-1 的可能性。例如,假设样本x[j]的标签y[j]应该是+1,第二个不等式的第一个成员是一个正数K(因此第三个方程的对应项是-K)。很容易验证第一个松弛变量是ξ[i] ≥ 1 - K,而第二个是z[j] ≥ 1 + K。
因此,在目标函数中,ξ[i]被选为最小化。这种方法是归纳的,并产生良好的(如果不是极好的)性能;然而,它具有非常高的计算成本,应该使用优化(本地)库来解决。不幸的是,它是一个非凸问题,没有标准方法来解决它,因此它总是达到最优配置。
S3VM 示例
现在我们使用 SciPy 优化方法在 Python 中实现 S³VM,这些方法主要基于 C 和 FORTRAN 实现。读者可以尝试使用其他库,如 NLOpt 和 LIBSVM,并比较结果。Bennet 和 Demiriz 提出的一种可能性是使用 w 的 L1 范数,以便线性化目标函数;然而,这种选择似乎只对小型数据集产生良好的结果。我们将保持基于 L2 范数的原始公式,使用顺序最小二乘规划(SLSQP)算法来优化目标。
首先,让我们创建一个包含标记和无标记样本的双维数据集:
from sklearn.datasets import make_classification
nb_samples = 500
nb_unlabeled = 200
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_redundant=0, random_state=1000)
Y[Y==0] = -1
Y[nb_samples - nb_unlabeled:nb_samples] = 0
为了简单起见(并且没有任何影响,因为样本是打乱的),我们将最后 200 个样本设置为无标记(y = 0)。相应的图示如下所示:
原始标记和无标记数据集
十字代表无标记点,它们遍布整个数据集。在此阶段,我们需要初始化优化问题所需的所有变量:
import numpy as np
w = np.random.uniform(-0.1, 0.1, size=X.shape[1])
eta = np.random.uniform(0.0, 0.1, size=nb_samples - nb_unlabeled)
xi = np.random.uniform(0.0, 0.1, size=nb_unlabeled)
zi = np.random.uniform(0.0, 0.1, size=nb_unlabeled)
b = np.random.uniform(-0.1, 0.1, size=1)
C = 1.0
theta0 = np.hstack((w, eta, xi, zi, b))
由于优化算法需要一个单一的数组,我们使用np.hstack()
函数将所有向量堆叠成一个水平数组theta0
。我们还需要将min()
函数向量化,以便将其应用于数组:
vmin = np.vectorize(lambda x1, x2: x1 if x1 <= x2 else x2)
现在,我们可以定义目标函数:
def svm_target(theta, Xd, Yd):
wt = theta[0:2].reshape((Xd.shape[1], 1))
s_eta = np.sum(theta[2:2 + nb_samples - nb_unlabeled])
s_min_xi_zi = np.sum(vmin(theta[2 + nb_samples - nb_unlabeled:2 + nb_samples],
theta[2 + nb_samples:2 + nb_samples + nb_unlabeled]))
return C * (s_eta + s_min_xi_zi) + 0.5 * np.dot(wt.T, wt)
参数是当前的theta
向量以及完整的数据集Xd
和Yd
。w的点积乘以 0.5 以保持用于监督 SVM 的传统符号。常数可以省略,没有任何影响。在此阶段,我们需要定义所有基于松弛变量的约束;每个函数(与目标函数共享相同的参数)用一个索引idx
参数化。标记约束如下:
def labeled_constraint(theta, Xd, Yd, idx):
wt = theta[0:2].reshape((Xd.shape[1], 1))
c = Yd[idx] * (np.dot(Xd[idx], wt) + theta[-1]) + \
theta[2:2 + nb_samples - nb_unlabeled][idx] - 1.0
return (c >= 0)[0]
无标记约束如下:
def unlabeled_constraint_1(theta, Xd, idx):
wt = theta[0:2].reshape((Xd.shape[1], 1))
c = np.dot(Xd[idx], wt) - theta[-1] + \
theta[2 + nb_samples - nb_unlabeled:2 + nb_samples][idx - nb_samples + nb_unlabeled] - 1.0
return (c >= 0)[0]
def unlabeled_constraint_2(theta, Xd, idx):
wt = theta[0:2].reshape((Xd.shape[1], 1))
c = -(np.dot(Xd[idx], wt) - theta[-1]) + \
theta[2 + nb_samples:2 + nb_samples + nb_unlabeled ][idx - nb_samples + nb_unlabeled] - 1.0
return (c >= 0)[0]
它们由当前的 theta
向量、Xd
数据集和 idx
索引参数化。我们还需要包括每个松弛变量的约束(≥ 0):
def eta_constraint(theta, idx):
return theta[2:2 + nb_samples - nb_unlabeled][idx] >= 0
def xi_constraint(theta, idx):
return theta[2 + nb_samples - nb_unlabeled:2 + nb_samples][idx - nb_samples + nb_unlabeled] >= 0
def zi_constraint(theta, idx):
return theta[2 + nb_samples:2 + nb_samples+nb_unlabeled ][idx - nb_samples + nb_unlabeled] >= 0
我们现在可以使用 SciPy 习惯设置问题:
svm_constraints = []
for i in range(nb_samples - nb_unlabeled):
svm_constraints.append({
'type': 'ineq',
'fun': labeled_constraint,
'args': (X, Y, i)
})
svm_constraints.append({
'type': 'ineq',
'fun': eta_constraint,
'args': (i,)
})
for i in range(nb_samples - nb_unlabeled, nb_samples):
svm_constraints.append({
'type': 'ineq',
'fun': unlabeled_constraint_1,
'args': (X, i)
})
svm_constraints.append({
'type': 'ineq',
'fun': unlabeled_constraint_2,
'args': (X, i)
})
svm_constraints.append({
'type': 'ineq',
'fun': xi_constraint,
'args': (i,)
})
svm_constraints.append({
'type': 'ineq',
'fun': zi_constraint,
'args': (i,)
})
每个约束都由一个字典表示,其中 type
设置为 ineq
以指示它是不等式,fun
指向可调用对象,而 args
包含所有额外参数(theta
是主要的 x 变量,它被自动添加)。使用 SciPy,可以使用 Sequential Least Squares Programming(SLSQP)或 Constraint Optimization by Linear Approximation(COBYLA)算法最小化目标。我们更喜欢前者,因为它运行得更快且更稳定:
from scipy.optimize import minimize
result = minimize(fun=svm_target,
x0=theta0,
constraints=svm_constraints,
args=(X, Y),
method='SLSQP',
tol=0.0001,
options={'maxiter': 1000})
在训练过程完成后,我们可以计算未标记点的标签:
theta_end = result['x']
w = theta_end[0:2]
b = theta_end[-1]
Xu= X[nb_samples - nb_unlabeled:nb_samples]
yu = -np.sign(np.dot(Xu, w) + b)
在下一个图中,可以比较初始图(左侧)和最终图(右侧),其中所有点都已分配了标签:
如您所见,S³VM 成功地为所有未标记点找到了正确的标签,确认了在 x 在 [0, 2](正方形点)和 y 在 [0, 2](圆形点)之间存在两个非常密集的区域。
NLOpt 是麻省理工学院开发的一个完整的优化库。它适用于不同的操作系统和编程语言。网站是 nlopt.readthedocs.io
。LIBSVM 是一个用于解决 SVM 问题的优化库,它被 Scikit-Learn 和 LIBLINEAR 一起采用。它也适用于不同的环境。主页是 www.csie.ntu.edu.tw/~cjlin/libsvm/
。
归纳支持向量机(TSVM)
另一种解决同一问题的方法是 T. Joachims 提出的 TSVM(在 Transductive Inference for Text Classification using Support Vector Machines,Joachims T.,ICML Vol. 99/1999)。其想法是保持原始目标,并使用两组松弛变量:一组用于标记样本,另一组用于未标记样本:
由于这是一个归纳方法,我们需要将未标记的样本视为可变标记样本(受学习过程影响),施加与监督点类似的约束。至于之前的算法,我们假设我们拥有 N 个标记样本和 M 个未标记样本;因此,条件如下:
第一个约束是经典的 SVM 约束,它仅适用于标记样本。第二个约束使用变量 y^((u))[j] 和相应的松弛变量 ξ[j] 对未标记样本施加类似条件,而第三个约束是必要的,以确保标签等于 -1 和 1。
就像半监督 SVM 一样,此算法是非凸的,尝试不同的方法来优化它是很有用的。此外,作者在上述论文中展示了当测试集(未标记)很大而训练集(标记)相对较小时,TSVM 如何表现更好(当标准监督 SVM 表现不佳时)。另一方面,当训练集很大而测试集较小时,监督 SVM(或其他算法)总是更可取,因为它们更快,且准确性更高。
TSVM 示例
在我们的 Python 实现中,我们将使用与之前方法中使用的类似的双维数据集;然而,在这种情况下,我们在总共 500 个点中强制 400 个未标记样本:
from sklearn.datasets import make_classification
nb_samples = 500
nb_unlabeled = 400
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_redundant=0, random_state=1000)
Y[Y==0] = -1
Y[nb_samples - nb_unlabeled:nb_samples] = 0
相应的图表如下所示:
原始标记和未标记数据集
程序与之前使用的方法类似。首先,我们需要初始化我们的变量:
import numpy as np
w = np.random.uniform(-0.1, 0.1, size=X.shape[1])
eta_labeled = np.random.uniform(0.0, 0.1, size=nb_samples - nb_unlabeled)
eta_unlabeled = np.random.uniform(0.0, 0.1, size=nb_unlabeled)
y_unlabeled = np.random.uniform(-1.0, 1.0, size=nb_unlabeled)
b = np.random.uniform(-0.1, 0.1, size=1)
C_labeled = 1.0
C_unlabeled = 10.0
theta0 = np.hstack((w, eta_labeled, eta_unlabeled, y_unlabeled, b))
在这种情况下,我们还需要定义变量标签的 y_unlabeled
向量。作者还建议使用两个 C 常数(C_labeled
和 C_unlabeled
),以便能够不同地加权标记和未标记样本的错误分类。我们为 C_labeled
使用了 1.0 的值,为 C_unlabled
使用了 10.0 的值,因为我们想对未标记样本的错误分类进行更多的惩罚。
要优化的目标函数如下:
def svm_target(theta, Xd, Yd):
wt = theta[0:2].reshape((Xd.shape[1], 1))
s_eta_labeled = np.sum(theta[2:2 + nb_samples - nb_unlabeled])
s_eta_unlabeled = np.sum(theta[2 + nb_samples - nb_unlabeled:2 + nb_samples])
return (C_labeled * s_eta_labeled) + (C_unlabeled * s_eta_unlabeled) + (0.5 * np.dot(wt.T, wt))
标记和未标记的约束如下:
def labeled_constraint(theta, Xd, Yd, idx):
wt = theta[0:2].reshape((Xd.shape[1], 1))
c = Yd[idx] * (np.dot(Xd[idx], wt) + theta[-1]) + \
theta[2:2 + nb_samples - nb_unlabeled][idx] - 1.0
return (c >= 0)[0]
def unlabeled_constraint(theta, Xd, idx):
wt = theta[0:2].reshape((Xd.shape[1], 1))
c = theta[2 + nb_samples:2 + nb_samples + nb_unlabeled][idx - nb_samples + nb_unlabeled] * \
(np.dot(Xd[idx], wt) + theta[-1]) + \
theta[2 + nb_samples - nb_unlabeled:2 + nb_samples][idx - nb_samples + nb_unlabeled] - 1.0
return (c >= 0)[0]
我们还需要对松弛变量和 y^((u) 进行约束:
def eta_labeled_constraint(theta, idx):
return theta[2:2 + nb_samples - nb_unlabeled][idx] >= 0
def eta_unlabeled_constraint(theta, idx):
return theta[2 + nb_samples - nb_unlabeled:2 + nb_samples][idx - nb_samples + nb_unlabeled] >= 0
def y_constraint(theta, idx):
return np.power(theta[2 + nb_samples:2 + nb_samples + nb_unlabeled][idx], 2) == 1.0
如前一个示例所示,我们可以创建 SciPy 需要的约束字典:
svm_constraints = []
for i in range(nb_samples - nb_unlabeled):
svm_constraints.append({
'type': 'ineq',
'fun': labeled_constraint,
'args': (X, Y, i)
})
svm_constraints.append({
'type': 'ineq',
'fun': eta_labeled_constraint,
'args': (i,)
})
for i in range(nb_samples - nb_unlabeled, nb_samples):
svm_constraints.append({
'type': 'ineq',
'fun': unlabeled_constraint,
'args': (X, i)
})
svm_constraints.append({
'type': 'ineq',
'fun': eta_unlabeled_constraint,
'args': (i,)
})
for i in range(nb_unlabeled):
svm_constraints.append({
'type': 'eq',
'fun': y_constraint,
'args': (i,)
})
在这种情况下,最后一个约束是一个等式,因为我们想强制 y^((u) 要么等于 -1,要么等于 1。在这个点上,我们最小化目标函数:
from scipy.optimize import minimize
result = minimize(fun=svm_target,
x0=theta0,
constraints=svm_constraints,
args=(X, Y),
method='SLSQP',
tol=0.0001,
options={'maxiter': 1000})
当过程完成后,我们可以计算未标记样本的标签并比较图表:
theta_end = result['x']
w = theta_end[0:2]
b = theta_end[-1]
Xu= X[nb_samples - nb_unlabeled:nb_samples]
yu = -np.sign(np.dot(Xu, w) + b)
图表比较如下所示:
原始数据集(左)。最终标记数据集(右)
基于密度分布的错误分类略高于 S³VM,但可以通过改变 C 值和优化方法来调整,直到达到预期的结果。当训练集足够大(并且正确代表整个 p[data])时,监督 SVM 可以提供良好的基准,其性能可能更好。
评估不同的 C 参数组合很有趣,从标准的监督 SVM 开始。数据集较小,有大量的未标记样本:
nb_samples = 100
nb_unlabeled = 90
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_redundant=0, random_state=100)
Y[Y==0] = -1
Y[nb_samples - nb_unlabeled:nb_samples] = 0
我们使用 Scikit-Learn 提供的标准 SVM 实现(SVC()
类),使用线性核和 C=1.0
:
from sklearn.svm import SVC
svc = SVC(kernel='linear', C=1.0)
svc.fit(X[Y!=0], Y[Y!=0])
Xu_svc= X[nb_samples - nb_unlabeled:nb_samples]
yu_svc = svc.predict(Xu_svc)
SVM 使用带标签的样本进行训练,向量 yu_svc
包含对未标记样本的预测。与原始数据集相比的结果图如下所示:
原始数据集(左)。最终标记数据集(右)C = 1.0
所有标记样本都用较大的正方形和圆形表示。结果符合我们的预期,但有一个区域 (X [-1, 0] - Y [-2, -1]),其中 SVM 决定将圆形类强加,即使未标记的点靠近正方形。考虑到聚类假设,这种假设是不可接受的;事实上,在高密度区域中存在属于两个类别的样本。使用CL=10和CU=5的 S³VM 得到的结果(或更差)是类似的:
原始数据集(左)。最终标记数据集(右)C[L] = 10 和 C[U] = 5
在这种情况下,分类准确度较低,因为未标记样本的惩罚低于对标记点的惩罚。显然,监督 SVM 有更好的性能。让我们尝试C[L]=10和C[U]=50:
原始数据集(左)。最终标记数据集(右)C[L] = 10 和 C[U] = 50
现在,对于未标记样本的惩罚相当高,考虑到聚类假设,结果看起来更加合理。所有高密度区域都是连贯的,并且由低密度区域分隔。这些例子展示了参数选择和优化方法如何能显著改变结果。我的建议是在选择最终配置(在子样本数据集上)之前测试几种配置。在《半监督学习》一书中,作者Chapelle O.、Schölkopf B.、Zien A.(编者),麻省理工学院出版社,有关于可能的优化策略的更多细节,包括其优缺点。
摘要
在本章中,我们从场景和所需的假设开始介绍半监督学习,以证明这些方法的有效性。我们讨论了平滑假设在处理监督和半监督分类器时的重要性,以确保合理的泛化能力。然后我们介绍了聚类假设,它与数据集的几何形状密切相关,并允许在具有强烈结构条件的密度估计问题中应对。最后,我们讨论了流形假设及其在避免维度灾难中的重要性。
本章继续介绍了一个生成和归纳模型:生成高斯混合模型,它允许从先验概率由多元高斯分布建模的假设出发,对标记和未标记样本进行聚类。
接下来的主题是关于一个非常重要的算法:对比悲观似然估计,这是一个归纳、半监督分类框架,可以与任何监督分类器一起采用。主要概念是基于软标签(代表未标记样本的概率)定义对比对数似然,并施加悲观条件以最小化对软标签的信任。该算法可以找到最佳配置,最大化对数似然,同时考虑标记和未标记样本。
另一种归纳分类方法是 S³VM 算法,它是经典 SVM 方法的扩展,基于两个额外的优化约束来解决未标记样本。这种方法相对强大,但它是非凸的,因此对用于最小化目标函数的算法非常敏感。
S³VM 的替代方案是 TSVM,它试图通过基于可变标签的条件最小化目标。因此,问题被分为两部分:监督部分,与标准 SVM 完全相同;半监督部分,结构相似但没有固定的y标签。这个问题也是非凸的,因此有必要评估不同的优化策略,以找到准确性和计算复杂度之间的最佳权衡。在参考文献部分,有一些有用的资源,您可以深入探讨所有这些问题,并为每个特定场景找到合适的解决方案。
在下一章,第三章,基于图的结构半监督学习中,我们继续这一探索,通过讨论一些基于数据集底层结构的重要算法。特别是,我们将运用图论来执行标签向未标记样本的传播,以及在非线性环境中降低数据集的维度。
第三章:基于图的半监督学习
在本章中,我们继续讨论半监督学习,考虑基于从数据集获得的图和样本之间现有关系的一系列算法。我们将讨论的问题属于两大类:将类别标签传播到未标记样本以及使用基于流形假设的非线性技术来降低原始数据集的维度。特别是,本章涵盖了以下传播算法:
-
基于权重矩阵的标签传播
-
Scikit-Learn 中的标签传播(基于转移概率)
-
标签传播
-
基于马尔可夫随机游走的传播
对于流形学习部分,我们正在讨论:
-
Isomap 算法和多维尺度方法
-
局部线性嵌入
-
拉普拉斯谱嵌入
-
t-SNE
标签传播
标签传播是一系列基于数据集图表示的半监督算法。特别是,如果我们有N个标记点(带有双极性标签+1 和-1)和M个未标记点(用y=0表示),则可以根据样本之间的几何亲和度度量构建一个无向图。如果G = {V, E}是图的正式定义,则顶点集由样本标签V = { -1, +1, 0 }组成,而边集基于亲和度矩阵W(当图无权重时通常称为邻接矩阵),它只依赖于X值,而不依赖于标签。
在下面的图中,有一个此类结构的示例:
二元图的示例
在前面的示例图中,有四个标记点(两个y=+1和两个y=-1),以及两个未标记点(y=0)。亲和度矩阵通常是对称的,是方阵,其维度等于(N+M) x (N+M)。它可以通过不同的方法获得。最常见的方法之一,也被 Scikit-Learn 采用,是:
- k-最近邻(我们将在第八章[59f765c2-2ad0-4605-826e-349080f85f1f.xhtml]中详细讨论此算法,聚类算法):
- 径向基函数核:
有时,在径向基函数核中,参数γ表示为2σ²的倒数;然而,对应于大方差的小γ值会增加半径,包括更远的点,并在多个样本上平滑类别,而大的γ值将边界限制在趋向于单个样本的子集。相反,在 k-最近邻核中,参数k控制要考虑作为邻居的样本数量。
为了描述基本算法,我们还需要介绍度矩阵(D):
它是一个对角矩阵,其中每个非零元素代表相应顶点的 度。这可以是入边数,或者与其成比例的度量(如在基于径向基函数的 W 的情况下)。标签传播的一般思想是让每个节点将其标签传播到其邻居,并迭代该过程直到收敛。
形式上,如果我们有一个包含标记和无标记样本的数据集:
标签传播算法的完整步骤(如 Zhu 和 Ghahramani 在 Learning from Labeled and Unlabeled Data with Label Propagation 中提出,Zhu X.,Ghahramani Z.,CMU-CALD-02-107)是:
-
选择亲和矩阵类型(KNN 或 RBF)并计算 W
-
计算度矩阵 D
-
定义 Y^((0)) = Y
-
定义 Y[L] = {y[0], y[1], ..., y[N]}
-
迭代以下步骤直到收敛:
第一次更新执行了带有标记和无标记点的传播步骤。每个标签从一个节点通过其出边传播,相应的权重,与度数归一化后,增加或减少每个贡献的 影响。第二个命令则重置所有标记样本的 y 值。最终的标签可以如下获得:
收敛的证明非常简单。如果我们根据标记和无标记样本之间的关系对矩阵 D^(-1)W 进行划分,我们得到:
如果我们考虑只有 Y 的前 N 个分量是非零的,并且在每次迭代的末尾被固定,则矩阵可以重写为:
我们对证明无标记样本部分(标记样本是固定的)的收敛性感兴趣,因此我们可以将更新规则写为:
将递归转换为迭代过程,前面的公式变为:
在前面的表达式中,第二项为零,因此我们需要证明第一项收敛;然而,很容易识别一个截断的矩阵几何级数(Neumann 级数),并且 A[UU] 被构建以具有所有特征值 |λ[i]| < 1,因此级数收敛到:
标签传播示例
我们可以使用 Python 实现该算法,使用一个测试的二维数据集:
from sklearn.datasets import make_classification
nb_samples = 100
nb_unlabeled = 75
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_informative=2, n_redundant=0, random_state=1000)
Y[Y==0] = -1
Y[nb_samples - nb_unlabeled:nb_samples] = 0
如其他示例一样,我们将所有无标记样本(100 个中的 75 个)的 y 设置为 0。相应的图如下所示:
部分标记数据集
带有交叉标记的点是无标记的。在此阶段,我们可以定义亲和矩阵。在这种情况下,我们使用两种方法来计算它:
from sklearn.neighbors import kneighbors_graph
nb_neighbors = 2
W_knn_sparse = kneighbors_graph(X, n_neighbors=nb_neighbors, mode='connectivity', include_self=True)
W_knn = W_knn_sparse.toarray()
KNN 矩阵是通过 Scikit-Learn 函数 kneighbors_graph()
获得的,参数为 n_neighbors=2
和 mode='connectivity'
;另一种选择是 'distance'
,它返回距离而不是 0 和 1 来表示边的存在/不存在。include_self=True
参数很有用,因为我们希望 W[ii] = 1。
对于 RBF 矩阵,我们需要手动定义它:
import numpy as np
def rbf(x1, x2, gamma=10.0):
n = np.linalg.norm(x1 - x2, ord=1)
return np.exp(-gamma * np.power(n, 2))
W_rbf = np.zeros((nb_samples, nb_samples))
for i in range(nb_samples):
for j in range(nb_samples):
W_rbf[i, j] = rbf(X[i], X[j])
γ 的默认值是 10,对应的标准差 σ 等于 0.22。在使用此方法时,设置正确的 γ 值非常重要;否则,在某一类占主导地位的情况下(γ 过小),传播可能会退化。现在,我们可以计算度矩阵及其逆矩阵。由于过程相同,从现在起我们将继续使用 RBF 相似度矩阵:
D_rbf = np.diag(np.sum(W_rbf, axis=1))
D_rbf_inv = np.linalg.inv(D_rbf)
该算法使用一个可变阈值。这里采用的值是 0.01
:
tolerance = 0.01
Yt = Y.copy()
Y_prev = np.zeros((nb_samples,))
iterations = 0
while np.linalg.norm(Yt - Y_prev, ord=1) > tolerance:
P = np.dot(D_rbf_inv, W_rbf)
Yt = np.dot(P, Yt)
Yt[0:nb_samples - nb_unlabeled] = Y[0:nb_samples - nb_unlabeled]
Y_prev = Yt.copy()
Y_final = np.sign(Yt)
最终结果在以下双图中显示:
原始数据集(左);完整标签传播后的数据集(右)
如你所见,在原始数据集中有一个圆形点被方形点包围(-0.9, -1)。由于此算法保留了原始标签,在标签传播后我们发现了相同的情况。这种条件可能是可以接受的,即使平滑性和聚类假设被违反。假设这是合理的,我们可以通过放松算法来强制进行 校正:
tolerance = 0.01
Yt = Y.copy()
Y_prev = np.zeros((nb_samples,))
iterations = 0
while np.linalg.norm(Yt - Y_prev, ord=1) > tolerance:
P = np.dot(D_rbf_inv, W_rbf)
Yt = np.dot(P, Yt)
Y_prev = Yt.copy()
Y_final = np.sign(Yt)
这样做,我们不重置原始标签,让传播改变所有与邻域不一致的值。结果在以下图中显示:
原始数据集(左);完整标签传播后带有覆盖的数据集(右)
Scikit-Learn 中的标签传播
Scikit-Learn 实现了 Zhu 和 Ghahramani 提出的略有不同的算法(在上述论文中提到),其中亲和度矩阵 W 可以使用两种方法(KNN 和 RBF)计算,但被归一化成为一个概率转移矩阵:
该算法像马尔可夫随机游走一样操作,以下是一个序列(假设有 Q 个不同的标签):
-
定义一个矩阵 Y^M[i] = [P(label=y[0]), P(label=y[1]), ..., and P(label=y[Q])]),其中 P(label=yi) 是标签 yi 的概率,并且每一行都归一化,使得所有元素之和等于 1
-
定义 Y^((0)) = Y^M
-
迭代直到以下步骤收敛:
第一次更新执行标签传播步骤。由于我们处理的是概率,因此有必要(第二步)重新归一化行,使它们的元素之和为1。最后一次更新重置所有标记样本的原始标签。在这种情况下,这意味着对相应的标签施加P(label=y[i]) = 1,并将所有其他设置为零。收敛的证明与标签传播算法的证明非常相似,可以在《基于标签传播的带标签和无标签数据的机器学习》,Zhu X.,Ghahramani Z.,CMU-CALD-02-107.*中最重要结果是,可以通过这个公式(无需任何迭代)获得封闭形式的解:
第一个项是一个广义几何级数的和,其中P[uu]是转移矩阵P中未标记-未标记的部分。P[ul],相反,是同一矩阵中未标记-标记的部分。
对于我们的 Python 示例,我们需要以不同的方式构建数据集,因为 Scikit-Learn 将标签为y=-1的样本视为未标记:
from sklearn.datasets import make_classification
nb_samples = 1000
nb_unlabeled = 750
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_informative=2, n_redundant=0, random_state=100)
Y[nb_samples - nb_unlabeled:nb_samples] = -1
我们现在可以使用具有 RBF 核和gamma=10.0
的LabelPropagation
实例进行训练:
from sklearn.semi_supervised import LabelPropagation
lp = LabelPropagation(kernel='rbf', gamma=10.0)
lp.fit(X, Y)
Y_final = lp.predict(X)
结果如下双图所示:
原始数据集(左)。经过 Scikit-Learn 标签传播后的数据集(右)
如预期,传播收敛到一个既满足平滑性又满足聚类假设的解。
标签传播
最后一个需要分析的算法(由 Zhou 等人提出)称为标签传播,它基于归一化图拉普拉斯算子:
这个矩阵的每个对角元素l[ii]等于1,如果度deg(l[ii]) > 0(否则为 0),所有其他元素等于:
这个矩阵的行为类似于离散拉普拉斯算子,其实值版本是所有扩散方程的基本元素。为了更好地理解这个概念,让我们考虑一个通用的热方程:
这个方程描述了当某一点突然加热时房间温度的行为。从基本的物理概念中,我们知道热量会传播,直到温度达到平衡点,变化的速率与分布的拉普拉斯算子成正比。如果我们考虑平衡状态下的二维网格(当时间变为零时的导数)并离散化拉普拉斯算子(∇² = ∇ · ∇),考虑增量比率,我们得到:
因此,在平衡状态下,每个点都有一个值,它是直接邻居的平均值。可以证明有限差分方程有一个唯一的固定点,可以从每个初始条件迭代找到。除了这个想法之外,标签传播还采用一个夹紧因子 α 来处理标记样本。如果 α=0,则算法将始终将标签重置为原始值(类似于标签传播),而当 α 在区间 (0, 1) 内时,夹紧标签的百分比会逐渐减少,直到 α=1,此时所有标签都将被覆盖。
标签传播算法的完整步骤是:
-
选择亲和矩阵类型(KNN 或 RBF)并计算 W
-
计算度矩阵 D
-
计算归一化图拉普拉斯矩阵 L
-
定义 Y^((0)) = Y
-
在区间 [0, 1] 中定义 α
-
迭代直到以下步骤收敛:
有可能证明(正如在 《半监督学习》 中所展示的,Chapelle O.,Schölkopf B., Zien A. 编著,麻省理工学院出版社)该算法等价于最小化具有以下结构的二次成本函数:
第一项强制原始标签和估计标签(对于标记样本)之间的一致性。第二项作为归一化因子,迫使未标记项变为零,而第三项,可能是最不直观的,需要保证在平滑性方面的几何一致性。正如我们在上一段中看到的,当采用硬限制时,平滑性假设可能会被违反。通过最小化这一项(μ 与 α 成正比),可以在高密度区域内惩罚快速变化。在这种情况下,收敛性的证明与标签传播算法的证明非常相似,因此将省略。感兴趣的读者可以在 《半监督学习》,Chapelle O.,Schölkopf B.,Zien A.,(编者),麻省理工学院出版社 中找到。
标签传播的示例
我们可以使用 Scikit-Learn 的实现来测试这个算法。让我们首先创建一个非常密集的数据集:
from sklearn.datasets import make_classification
nb_samples = 5000
nb_unlabeled = 1000
X, Y = make_classification(n_samples=nb_samples, n_features=2, n_informative=2, n_redundant=0, random_state=100)
Y[nb_samples - nb_unlabeled:nb_samples] = -1
我们可以使用具有夹紧因子 alpha=0.2
的 LabelSpreading
实例进行训练。我们希望保留 80% 的原始标签,但同时也需要一个平滑的解决方案:
from sklearn.semi_supervised import LabelSpreading
ls = LabelSpreading(kernel='rbf', gamma=10.0, alpha=0.2)
ls.fit(X, Y)
Y_final = ls.predict(X)
结果通常与原始数据集一起展示:
原始数据集(左)。完全标签传播后的数据集(右)
如第一幅图(左)所示,在簇的中央部分 (x [-1, 0]) 中,有一个圆形点的区域。使用硬限制,这个 通道 将保持不变,这违反了平滑性和聚类假设。设置 α > 0,可以避免这个问题。当然,α 的选择与每个单独的问题严格相关。如果我们知道原始标签绝对正确,允许算法更改它们可能是适得其反的。在这种情况下,例如,最好预处理数据集,过滤掉所有违反半监督假设的样本。如果我们不确定所有样本是否都来自相同的 p[data],并且可能存在虚假元素,使用更高的 α 值可以在不进行其他操作的情况下平滑数据集。
基于马尔可夫随机游走的标签传播
Zhu 和 Ghahramani 提出的这个算法的目标是在一个混合数据集给定的情况下,找到未标记样本的目标标签的概率分布。这个目标是通过模拟一个随机过程来实现的,在这个过程中,每个未标记样本在图中行走,直到达到一个稳定的吸收状态,即停止获取相应标签的标记样本。与其他类似方法的主要区别在于,在这种情况下,我们考虑到达标记样本的概率。通过这种方式,问题获得了一个封闭形式,并且可以很容易地解决。
第一步是始终构建一个包含所有 N 样本的 k-最近邻图,并基于 RBF 内核定义一个权重矩阵 W:
W[ij] = 0 是 x[i, ]和 x[j] 不是邻居,W[ii] = 1。与 Scikit-Learn 标签传播算法类似,转换概率矩阵是构建的:
以更紧凑的方式,它可以重写为 P = D^(-1)W。如果我们现在考虑一个 测试样本,从状态 x[i] 开始,随机行走直到找到一个吸收的标记状态(我们称之为 y^∞),这个概率(称为二元分类)可以表示为:
当 x[i] 被标记时,状态是最终的,它由基于条件 y[i]=1 的指示函数表示。当样本未标记时,我们需要考虑从 x[i] 开始并结束在最近的吸收状态的所有可能的转换的总和,该状态带有标签 y=1,并按相对转换概率加权。
我们可以将这个表达式重写为矩阵形式。如果我们创建一个向量 P^∞ = P[L, PU ],其中第一个分量基于标记样本,第二个基于未标记样本,我们可以写出:
如果我们现在扩展矩阵,我们得到:
由于我们只对未标记的样本感兴趣,我们可以只考虑第二个方程:
简化表达式,我们得到以下线性系统:
项 (D[uu] - W[uu]) 是未归一化图拉普拉斯矩阵 L = D - W 的未标记-未标记部分。通过解这个系统,我们可以得到所有未标记样本对于类别 y=1 的概率。
基于马尔可夫随机游走的标签传播示例
对于这个基于马尔可夫随机游走的 Python 标签传播示例,我们将使用一个包含 50 个标记样本(属于两个不同的类别)和 1,950 个未标记样本的二维数据集:
from sklearn.datasets import make_blobs
nb_samples = 2000
nb_unlabeled = 1950
nb_classes = 2
X, Y = make_blobs(n_samples=nb_samples,
n_features=2,
centers=nb_classes,
cluster_std=2.5,
random_state=500)
Y[nb_samples - nb_unlabeled:] = -1
数据集的图示如下(交叉点代表未标记的样本):
部分标记的数据集
我们现在可以创建图(使用 n_neighbors=15
)和权重矩阵:
import numpy as np
from sklearn.neighbors import kneighbors_graph
def rbf(x1, x2, sigma=1.0):
d = np.linalg.norm(x1 - x2, ord=1)
return np.exp(-np.power(d, 2.0) / (2 * np.power(sigma, 2)))
W = kneighbors_graph(X, n_neighbors=15, mode='connectivity', include_self=True).toarray()
for i in range(nb_samples):
for j in range(nb_samples):
if W[i, j] != 0.0:
W[i, j] = rbf(X[i], X[j])
现在,我们需要计算未归一化图拉普拉斯矩阵的未标记部分和矩阵 W 的未标记-标记部分:
D = np.diag(np.sum(W, axis=1))
L = D - W
Luu = L[nb_samples - nb_unlabeled:, nb_samples - nb_unlabeled:]
Wul = W[nb_samples - nb_unlabeled:, 0:nb_samples - nb_unlabeled,]
Yl = Y[0:nb_samples - nb_unlabeled]
在这一点上,可以使用 NumPy 函数 np.linalg.solve()
解这个线性系统,该函数接受一个形式为 Ax=b 的通用系统的矩阵 A 和向量 b 作为参数。一旦我们得到解,我们可以将新的标签与原始标签合并(其中未标记的样本被标记为 -1)。在这种情况下,我们不需要转换概率,因为我们使用 0 和 1 作为标签。通常,需要使用一个阈值(0.5)来选择正确的标签:
Yu = np.round(np.linalg.solve(Luu, np.dot(Wul, Yl)))
Y[nb_samples - nb_unlabeled:] = Yu.copy()
重新绘制数据集,我们得到:
完整马尔可夫随机游走标签传播后的数据集
如预期的那样,没有进行任何迭代,标签已经成功传播到所有样本,完全符合聚类假设。这个算法和标签传播都可以使用闭式解来工作,因此即使样本数量很高,它们也非常快;然而,关于 RBF 核中σ/γ的选择存在一个基本问题。正如同一作者 Zhu 和 Ghahramani 所指出的,没有标准解决方案,但可以考虑当σ → 0和当σ → ∞时的情况。在前一种情况下,只有最近邻点有影响,而在后一种情况下,影响扩展到整个样本空间,未标记的点倾向于获得相同的标签。作者建议考虑所有样本的熵,试图找到最小化它的最佳σ值。这种解决方案可能非常有效,但有时最小熵对应于使用这些算法不可能实现的标签配置。最佳方法是尝试不同的值(在不同尺度上),并选择对应于具有最低熵的有效配置的那个值。在我们的情况下,可以计算未标记样本的熵如下:
执行此计算的 Python 代码如下:
Pu = np.linalg.solve(Luu, np.dot(Wul, Yl))
H = -np.sum(Pu * np.log(Pu + 1e-6))
为了避免概率为零时的数值问题,已经添加了1e-6
这个项。对不同的值重复这个过程,我们可以找到一组候选值,可以通过直接评估标签准确性来限制为单个值(例如,当没有关于真实分布的精确信息时,可以考虑每个簇的连贯性和它们之间的分离)。另一种方法称为类别重平衡,它基于重新加权未标记样本的概率,以在将新的未标记样本添加到集合时重新平衡每个类别的点数。如果我们有N个标记点和M个未标记点,以及K个类别,类别j的权重因子w[j]可以表示为:
分子是对属于类别k的标记样本的平均值,而分母是对估计类别为k的未标记样本的平均值。关于类别的最终决定不再仅仅基于最高的概率,而是基于:
流形学习
在第二章《半监督学习导论》中,我们讨论了流形假设,即高维数据通常位于低维流形上。当然,这并不是一个定理,但在许多实际情况下,这个假设已被证明是正确的,并且它允许我们使用在其它情况下不可接受的非线性降维算法。在本节中,我们将分析一些这些算法。它们都已在 Scikit-Learn 中实现,因此使用复杂数据集尝试它们很容易。
Isomap
Isomap是最简单的算法之一,它基于在尝试保留原始流形上测量的测地距离的同时降低维度的想法。该算法分为三个步骤。第一步是 k-最近邻聚类和以下图的构建。顶点将是样本,而边表示最近邻之间的连接,它们的权重与对应邻居的距离成正比。
第二步采用Dijkstra 算法计算所有样本对在图上的最短距离。在下面的图中,有一部分图,其中标记了一些最短距离:
标记最短距离的图示例
例如,由于x[3]是x[5]和x[7]的邻居,应用 Dijkstra 算法,我们可以得到最短路径d(x[3], x[5]) = w[53]和d(x[3], x[7]) = w[73]。这一步骤的计算复杂度大约为O(n²log n + n²k),当k << n(通常满足的条件)时,低于O(n³);然而,对于大型图(n >> 1),这通常是整个算法中最昂贵的部分。
第三步被称为度量多维尺度,这是一种在尝试保留样本之间的内积的同时寻找低维表示的技术。如果我们有一个P维数据集X,算法必须找到一个Q维集合Φ,其中Q < P,以最小化以下函数:
如在Semi-Supervised Learning Chapelle O.,* Schölkopf B., Zien A. (编辑),麻省理工学院出版社中证明,优化是通过取 Gram 矩阵G[ij] = x[i] · xj,则G=XX^T)的前Q个特征向量来实现的;然而,由于Isomap算法使用成对距离,我们需要计算平方距离矩阵D:
如果X数据集是零中心的,则可以从D中推导出一个简化的 Gram 矩阵,如 M. A. A. Cox 和 T. F. Cox 所述:
Isomap 计算出 G[D*] 的前 Q 个特征值 λ[1],λ2,...,λ[Q] 和相应的特征向量 ν[1],ν[2]**,...,ν[Q],并确定 Q-维向量:
正如我们将在第五章 EM 算法和应用中讨论的(以及 Saul、Weinberger、Sha、Ham 和 Lee 在《降维的谱方法》Spectral Methods for Dimensionality Reduction,Saul L. K.、Weinberger K. Q.、Sha F.、Ham J.、Lee D. D. 指出的),这种投影也被主成分分析(PCA)所利用,它找出协方差矩阵的最高方差方向,对应于协方差矩阵的前 k 个特征向量。实际上,当对数据集 X 应用奇异值分解(SVD)时,我们得到:
对角矩阵 Λ 包含了 XX^T 和 X^TX 的特征值;因此,G 的特征值 λ[Gi] 等于 Mλ[^Σ][i],其中 λ[^Σ][i] 是协方差矩阵 Σ = M(-1)XTX 的特征值。因此,Isomap 通过在由一组特征向量确定的子空间中投影数据集,尝试保留成对距离,从而实现降维,同时达到最大解释方差。从信息论的角度来看,这个条件保证了最小损失和有效的降维。
Scikit-Learn 还实现了 Floyd-Warshall 算法,它稍微慢一些。有关更多信息,请参阅 算法导论,Cormen T. H.、Leiserson C. E.、Rivest R. L.、MIT 出版社。
Isomap 示例
我们现在可以使用 Olivetti 人脸数据集(由 AT&T 实验室,剑桥提供)测试 Scikit-Learn 的 Isomap 实现,该数据集由 400 个 64 × 64 灰度肖像组成,属于 40 个不同的人。这些图像的示例如下所示:
Olivetti 人脸数据集的子集
原始维度是 4096,但我们希望将数据集可视化在二维空间中。重要的是要理解,使用欧几里得距离来衡量图像的相似性可能不是最佳选择,而且令人惊讶的是,这些样本如何被这样一个简单的算法很好地聚类。
第一步是加载数据集:
from sklearn.datasets import fetch_olivetti_faces
faces = fetch_olivetti_faces()
faces
字典包含三个主要元素:
-
images
: 形状为 400 × 64 × 64 的图像数组 -
data
: 形状为 400 × 4096 的展平数组 -
target
: 形状为 400 × 1 的数组,包含标签(0, 39)
在这一点上,我们可以实例化 Scikit-Learn 提供的 Isomap
类,设置 n_components=2
和 n_neighbors=5
(读者可以尝试不同的配置),然后拟合模型:
from sklearn.manifold import Isomap
isomap = Isomap(n_neighbors=5, n_components=2)
X_isomap = isomap.fit_transform(faces['data'])
由于生成的包含 400 个元素的图非常密集,我更喜欢在下面的图中只展示前 100 个样本:
将 Isomap 应用于从 Olivetti 人脸数据集中抽取的 100 个样本
如所见,属于同一类的样本被分组在相当密集的聚团中。看起来分离得更好的类是 7 和 1。检查相应的面孔,对于第 7 类,我们得到:
属于第 7 类的样本
该集合包含一位肤色白皙的年轻女性的肖像,与其他大多数人截然不同。而对于第 1 类,我们得到:
属于第 1 类的样本
在这种情况下,这是一个戴着大眼镜并且有特定嘴型表情的男人。在数据集中,只有少数人戴眼镜,其中一个人有浓密的胡须。我们可以得出结论,Isomap创建了一个与原始测地距离高度一致的低维表示。在某些情况下,存在部分聚类重叠,可以通过增加维度或采用更复杂的策略来缓解。
局部线性嵌入
与基于成对距离的 Isomap 相反,该算法基于这样一个假设:一个高维数据集位于光滑流形上,可以在降维过程中具有局部线性结构,它试图在降维过程中保持这些结构。局部线性嵌入(LLE),像 Isomap 一样,基于三个步骤。第一步是应用k最近邻算法来创建一个有向图(在 Isomap 中是未定向的),其中顶点是输入样本,边代表邻域关系。由于图是有向的,一个点x[i]可以是x[j]的邻居,但反之则不然。这意味着权重矩阵可以是不对称的。
第二步基于局部线性的主要假设。例如,考虑以下图:
标记了邻域的阴影矩形图
矩形界定了一个小邻域。如果我们考虑点x[5],局部线性假设允许我们认为x[5] = w[56]x[6] + w[53]x[3, ]而不考虑循环关系。这个概念可以通过以下函数的最小化来形式化,对于所有N* P-维度的点:
为了解决低秩邻域矩阵的问题(想想之前的例子,邻居的数量等于 20),Scikit-Learn 还实现了一个基于小任意加性常数的正则化器,该常数被添加到局部权重中(根据称为修改后的 LLE或MLLE的变体)*。在这一步结束时,将选择与邻居之间的线性关系匹配更好的矩阵 W,用于下一阶段。
在第三步中,局部线性嵌入试图确定最佳的低维(Q < P)表示,以最好地再现原始最近邻之间的关系。这是通过最小化以下函数来实现的:
该问题的解是通过采用Rayleigh-Ritz 方法获得的,这是一种从非常大的稀疏矩阵中提取特征向量和特征值的算法。有关更多详细信息,请阅读A spectrum slicing method for the Kohn–Sham problem, Schofield G. Chelikowsky J. R.; Saad Y., Computer Physics Communications. 183。最终过程的初始部分包括确定矩阵D:
可以证明最后一个特征向量(如果特征值按降序排列,则是底部的一个)具有所有分量v[1]^((N)), v[2]^((N))**, ..., v[N]^((N) )= v,并且对应的特征值是零。正如 Saul 和 Roweis(局部线性嵌入的介绍,Saul L. K.,Roweis S. T.)所指出的,所有其他Q特征向量(从底部开始)是正交的,这使得它们可以具有零中心嵌入。因此,最后一个特征向量被舍弃,而剩余的 Q 个特征向量决定了嵌入向量φ[i]。
有关 MLLE 的更多详细信息,请参阅MLLE:使用多个权重的改进局部线性嵌入,张 Z.,王 J.,citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.70.382
。
局部线性嵌入的示例
我们现在可以将此算法应用于 Olivetti 人脸数据集,通过实例化 Scikit-Learn 类LocallyLinearEmbedding
并设置n_components=2
和n_neighbors=15
:
from sklearn.manifold import LocallyLinearEmbedding
lle = LocallyLinearEmbedding(n_neighbors=15, n_components=2)
X_lle = lle.fit_transform(faces['data'])
结果(限于前 100 个样本)如下所示:
将局部线性嵌入应用于从 Olivetti 人脸数据集中抽取的 100 个样本
即使策略与 Isomap 不同,我们也可以确定一些有意义的聚类。在这种情况下,相似性是通过小线性块的结合获得的;对于人脸,它们可以代表特定的微观特征,如鼻子的形状或眼镜的存在,这些特征在不同的人像中保持不变。一般来说,当原始数据集本质上是局部线性的,可能位于光滑流形上时,LLE 更可取。换句话说,当样本的小部分以允许根据邻居和权重重建点的方式结构化时,LLE 是一个合理的选择。这通常适用于图像,但对于通用数据集来说可能很难确定。当结果不能再现原始聚类时,可以采用下一个算法或t-SNE,这是最先进的算法之一。
拉普拉斯谱嵌入
此算法基于图拉普拉斯算子的谱分解,旨在执行非线性降维,以尝试在将点重新映射到具有 Q 维(Q < P)子空间时保留 P 维流形上点的邻近性。
该过程与其他算法非常相似。第一步是 k 近邻聚类,以生成一个图,其中顶点(我们可以假设有 N 个元素)是样本,边使用径向基函数核进行加权:
生成的图是无向和对称的。我们现在可以定义一个伪度矩阵 D:
通过最小化以下函数获得低维表示 Φ:
如果两个点 x[i] 和 x[j] 靠近,相应的 W[ij] 接近于 1,而当距离趋向于 ∞ 时,它趋向于 0。D[ii] 是从 x[i](以及 D[jj] 同样)发出的所有权重的总和。现在,假设 x[i] 只非常接近于 x[j],因此,为了近似 D[ii] = D[jj] ≈ W[ij]。得到的公式是基于向量 φ[i] 和 φ[j] 之间差异的平方损失。当需要考虑多个 邻近性 关系时,W[ij] 除以 D[ii]D[jj] 的平方根允许重新加权新距离,以找到整个数据集的最佳权衡。在实践中,L[Φ] 不是直接最小化的。事实上,可以证明最小值可以通过对称归一化图拉普拉斯算子的谱分解(该名称来源于此过程)来获得:
就像 LLE 算法一样,拉普拉斯谱嵌入也使用底部的 Q + 1 个特征向量。最后一步背后的数学理论始终基于应用 Rayleigh-Ritz 方法。最后一个被丢弃,剩下的 Q 确定了低维表示 φ[i]。
拉普拉斯谱嵌入的示例
让我们使用 Scikit-Learn 类 SpectralEmbedding
将此算法应用于相同的数据集,其中 n_components=2
和 n_neighbors=15
:
from sklearn.manifold import SpectralEmbedding
se = SpectralEmbedding(n_components=2, n_neighbors=15)
X_se = se.fit_transform(faces['data'])
由于存在高密度区域,以下图中显示了生成的图(已放大):
将拉普拉斯谱嵌入应用于奥利维蒂人脸数据集
即使在这种情况下,我们也可以看到一些类别被分组到小的簇中,但与此同时,我们观察到许多混合样本的聚集。这两种方法都使用局部信息片段,试图找到能够保留微特征几何结构的低维表示。这种条件导致了一种映射,其中接近的点共享局部特征(这在图像中几乎是总是成立的,但对于通用样本来说很难证明)。因此,我们可以观察到包含属于同一类别的元素的微小簇,但也存在一些明显的异常值,在原始流形上,即使它们共享局部块,也可能在全局上不同。相反,像 Isomap 或 t-SNE 这样的方法处理整个分布,并试图确定一个表示,该表示在考虑其全局属性的情况下几乎与原始数据集等距。
t-SNE
这个算法由 Van der Mateen 和 Hinton 提出,正式称为t 分布随机邻域嵌入(t-SNE),是最强大的流形降维技术之一。与其他方法相反,这个算法从一个基本假设开始:两个N-维点x[i]和x[j]之间的相似性可以表示为条件概率p(x[j]|x[i]),其中每个点由以x[i]为中心、方差σ[i]的高斯分布表示。方差是从所需的困惑度开始的,定义为:
低困惑度值表示低不确定性,通常更可取。在常见的 t-SNE 任务中,10÷50范围内的值通常是可以接受的。
对条件概率的假设可以这样解释:如果两个样本非常相似,则与第二个样本相关的第一个样本的条件概率很高,而不同的点产生低条件概率。例如,考虑图像,瞳孔中心的一个点可以有睫毛属于的点作为邻居。在概率方面,我们可以认为p(eyelash|pupil)相当高,而p(nose|pupil)显然较低。t-SNE 将这些条件概率建模为:
将p(x[i]|x[i]**)的概率设置为零,因此前面的公式可以扩展到整个图。为了更容易地解决这个问题,条件概率也被对称化:
所获得的概率分布代表了高维输入关系。由于我们的目标是降低维度到一个值M < N,我们可以考虑对目标点φ[i]使用类似的概率表示,使用一个自由度为 1 的学生 t 分布:
我们希望低维分布Q尽可能接近高维分布P;因此,t-SNE算法的目的是最小化P和Q之间的 Kullback-Leibler 散度:
第一个项是原始分布P的熵,而第二个项是交叉熵H(P, Q),必须最小化以解决问题。最佳方法基于梯度下降算法,但也有一些有用的变体可以在《使用 t-SNE 可视化高维数据》,Van der Maaten L.J.P., Hinton G.E., Journal of Machine Learning Research 9 (Nov), 2008.中讨论,以改善性能。
t-distributed 随机邻域嵌入的示例
我们可以将这个强大的算法应用于相同的 Olivetti 面部数据集,使用 Scikit-Learn 类TSNE
,n_components=2
和perplexity=20
:
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, perplexity=20)
X_tsne = tsne.fit_transform(faces['data'])
所有 400 个样本的结果显示在下图中:
t-SNE 应用于 Olivetti 面部数据集
通过对标签分布的视觉检查可以确认,t-SNE 从原始高维分布重新创建了最佳聚类。此算法可用于多个非线性降维任务,例如图像、词嵌入或复杂特征向量。其主要优势在于考虑相似性为概率的假设,无需对成对距离施加任何约束,无论是全局的还是局部的。从某个角度来看,可以将 t-SNE 视为基于交叉熵成本函数的反向多类分类问题。我们的目标是给定原始分布和关于输出分布的假设,找到标签(低维表示)。
在这一点上,我们可以尝试回答一个自然的问题:必须使用哪种算法?显然的答案是这取决于单个问题。当需要降低维度,同时保留向量之间的全局相似性时(这是当样本是长特征向量且没有局部属性,如词嵌入或数据编码时的情况),t-SNE 或 Isomap 是不错的选择。当需要尽可能保持局部距离(例如,一个视觉块的结构,可以被属于不同类别的不同样本共享)与原始表示尽可能接近时,局部线性嵌入或谱嵌入算法更可取。
摘要
在本章中,我们介绍了最重要的标签传播技术。特别是,我们看到了如何基于加权核构建数据集图,以及如何使用未标记样本提供的几何信息来确定最可能的类别。基本方法通过迭代标签向量与权重矩阵的乘积,直到达到稳定点并证明,在简单假设下,这总是可能的。
另一种方法,由 Scikit-Learn 实现,基于从状态(由样本表示)到另一个状态的转换概率,直到收敛到一个标记点。概率矩阵通过使用归一化权重矩阵来获得,以鼓励与接近点相关的转换并阻止所有长跳跃。这两种方法的主要缺点是硬限制标记样本;如果我们信任我们的数据集,这个约束可能是有用的,但在存在标签错误分配的异常值的情况下,它可能是一个限制。
标签传播通过引入一个限制系数来解决此问题,该系数决定了限制标签的百分比。该算法与标签传播非常相似,但它基于图拉普拉斯,可以应用于所有那些数据生成分布未确定且噪声概率高的问题。
基于马尔可夫随机游走的传播是一个非常简单的算法,可以通过随机过程估计未标记样本的类别分布。可以想象它为一个测试样本在图中行走,直到它达到一个最终标记状态(获得相应的标签)。该算法非常快,并且有一个封闭形式的解,可以通过求解线性系统找到。
下一个主题是介绍使用 Isomap 算法的流形学习,这是一个基于使用k最近邻算法(这是大多数这些算法中的常见步骤)构建的图的一个简单但强大的解决方案。原始的成对距离通过多维尺度技术进行处理,这使得可以获得一个低维表示,其中样本之间的距离得到保留。
基于局部信息片段的两种不同方法,局部线性嵌入和拉普拉斯谱嵌入。前者试图保留原始流形中存在的局部线性,而后者,基于归一化图拉普拉斯的谱分解,试图保留原始样本的邻近性。这两种方法都适用于所有那些任务,在这些任务中,重要的是不要考虑整个原始分布,而是考虑由小数据补丁引起的相似性。
我们通过讨论 t-SNE 来结束这一章节,这是一个非常强大的算法,试图模拟一个尽可能接近原始高维分布的低维分布。这个任务是通过最小化两个分布之间的 Kullback-Leibler 散度来实现的。t-SNE 是一种最先进的算法,在需要考虑整个原始分布以及整个样本之间的相似性时非常有用。
在下一章(第四章),贝叶斯网络和隐马尔可夫模型中,我们将介绍在静态和动态环境下使用贝叶斯网络,以及隐马尔可夫模型,并附带实际预测示例。这些算法允许对由观测和潜在变量组成的复杂概率场景进行建模,并使用仅基于观测的优化采样方法来推断未来状态。
第四章:贝叶斯网络和隐马尔可夫模型
在本章中,我们将介绍贝叶斯模型的基本概念,这些概念允许在需要将不确定性作为系统结构部分考虑的多个场景中工作。讨论将集中在静态(时间不变)和动态方法,这些方法在必要时可以用来建模时间序列。
特别是,本章涵盖了以下主题:
-
贝叶斯定理及其应用
-
贝叶斯网络
-
使用直接方法和马尔可夫链蒙特卡洛(MCMC)(Gibbs 和 Metropolis-Hastings 采样器)从贝叶斯网络中采样
-
使用 PyMC3 建模贝叶斯网络
-
隐马尔可夫模型(HMMs)
-
使用 hmmlearn 的示例
条件概率和贝叶斯定理
如果我们有一个概率空间 S 和两个事件 A 和 B,事件 A 在给定 B 的概率被称为条件概率,其定义为:
由于 P(A, B) = P(B, A),可以推导出贝叶斯定理:
这个定理允许将条件概率表示为对立概率和两个边缘概率 P(A) 和 P(B) 的函数。这个结果对于许多机器学习问题来说是基本的,因为我们将在本章和下一章中看到,通常更容易通过条件概率来获取对立概率,但直接从后者开始工作却很困难。这个定理的常见形式可以表示为:
假设我们需要估计给定一些观察结果 B 的事件 A 的概率,或者使用标准符号,A 的后验概率;前面的公式将这个值表示为与项 P(A) 成比例,这是 A 的边缘概率,称为先验概率,以及观察结果 B 在事件 A 给定下的条件概率。P(B|A) 被称为似然函数,它定义了事件 A 如何可能决定 B。因此,我们可以总结关系为后验概率 ∝ 似然函数 · 先验概率。比例不是一个限制,因为项 P(B) 总是一个归一化常数,可以省略。当然,读者必须记住归一化 P(A|B),使其项总是加起来等于一。
这是贝叶斯统计的一个关键概念,我们并不直接信任先验概率,而是使用某些观察结果的似然性重新加权。例如,我们可以考虑抛掷一枚硬币 10 次(事件 A)。我们知道,如果硬币是公平的,那么 P(A) = 0.5。如果我们想知道得到 10 个正面的概率是多少,我们可以使用二项分布得到 P(10 heads) = 0.5^k;然而,假设我们不知道硬币是否公平,但我们怀疑它可能被加载,先验概率 P(Loaded) = 0.7 有利于反面。我们可以使用指示函数定义一个完整的先验概率 P(Coin status):
在 P(Fair) = 0.5 和 P(Loaded) = 0.7 的情况下,指示器 I[Coin=Fair] 仅在硬币是公平的情况下等于 1,否则为 0。当硬币是加重的,同样会发生 I[Coin=Loaded] 的情况。我们的目标现在是要确定后验概率 P(Coin status|B[1], B[2], ..., **B[n]),以便能够确认或拒绝我们的假设。
让我们假设观察 n = 10 个事件,其中 B[1] = Head 和 *B[2], ..., B[n] = Tail。我们可以使用二项分布来表示概率:
在简化表达式后,我们得到:
我们仍然需要通过将两个项都除以 0.083(两个项的和)来归一化,所以我们得到最终的后验概率 P(Coin status|B[1], B[2], ..., **Bn) = 0.04I[Fair] + 0.96I[Loaded]。这个结果证实并加强了我们的假设。由于在出现一个正面之后观察到连续九次反面,加重的硬币的概率现在约为 96%。
这个例子展示了如何将数据(观察结果)插入贝叶斯框架中。如果读者有兴趣更详细地研究这些概念,可以在 《统计决策理论导论》,作者 Pratt J.,Raiffa H.,Schlaifer R.,麻省理工学院出版社 中找到许多有趣的例子和解释;然而,在介绍贝叶斯网络之前,定义两个其他基本概念是有用的。
第一个概念被称为 条件独立性,可以通过考虑两个变量 A 和 B 来形式化,这两个变量依赖于第三个变量 C。我们说,在给定 C 的条件下,A 和 B 是条件独立的,如果:
现在,让我们假设我们有一个事件 A,它依赖于一系列原因 C[1], C[2], ..., C[n];因此,条件概率是 P(A|C[1], C[2], ..., C[n])。应用贝叶斯定理,我们得到:
如果存在条件独立性,前面的表达式可以简化并重新写为:
这个属性在朴素贝叶斯分类器中是基本的,我们假设原因产生的效果不会影响其他原因。例如,在垃圾邮件检测器中,我们可以说邮件的长度和某些特定关键词的存在是独立事件,我们只需要计算 P(Length|Spam) 和 P(Keywords|Spam),而不需要考虑联合概率 P(Length, Keywords|Spam)。
另一个重要元素是概率的 链式法则。假设我们有一个联合概率 P(X[1], X[2], ..., X[n])。它可以表示为:
重复使用右侧联合概率的步骤,我们得到:
以这种方式,我们可以将完整的联合概率表示为分层条件概率的乘积,直到最后一个项,它是一个边缘分布。在下一段探索贝叶斯网络时,我们将广泛使用这个概念。
贝叶斯网络
贝叶斯网络 是一个由直接无环图 G = {V, E} 表示的概率模型,其中顶点是随机变量 X[i],边确定它们之间的条件依赖关系。在下面的图中,有一个包含四个变量的简单贝叶斯网络的例子:
贝叶斯网络示例
变量 x[4] 依赖于 x[3],而 x[3] 依赖于 x[1] 和 x[2]。为了描述这个网络,我们需要边缘概率 P(x[1]) 和 P(x[2]) 以及条件概率 P(x[3]|x[1],x[2]) 和 P(x[4]|x[3])。实际上,使用链式法则,我们可以推导出完整的联合概率如下:
之前的表达式展示了一个重要的概念:由于图是直接且无环的,每个变量在给定其前驱的情况下,条件独立于所有其他非后继变量。为了形式化这个概念,我们可以定义函数 Predecessors(x[i]),该函数返回直接影响 x[i] 的节点集合,例如,Predecessors(x[3]) = {x[1],x[2]}(我们使用小写字母,但考虑的是随机变量,而不是样本)。使用这个函数,我们可以为具有 N 个节点的贝叶斯网络的完整联合概率写出一个通用表达式:
构建贝叶斯网络的一般程序应该始终从第一个原因开始,逐个添加它们的效果,直到最后将节点插入到图中。如果不遵守此规则,生成的图可能包含无用的关系,这会增加模型的复杂性。例如,如果 x[4] 间接由 x[1] 和 x[2] 导致,因此添加边 x[1] → x[4] 和 x[2] → x[4] 可能看起来是一个好的建模选择;然而,我们知道对 x[4] 的最终影响仅由 x[3, ]的值决定,其概率必须根据 x[1] 和 x[2] 进行条件化,因此我们可以删除虚假的边。我建议阅读 《统计决策理论导论》,作者:Pratt J.,Raiffa H.,Schlaifer R.,麻省理工学院出版社,以了解在此过程中应采用许多最佳实践。
从贝叶斯网络中采样
在贝叶斯网络上进行直接推理可能是一个非常复杂的操作,当变量和边的数量很高时。因此,已经提出了几种采样方法。在本段中,我们将展示如何使用直接方法从网络中确定全联合概率采样,以及两种 MCMC 算法。
让我们从之前的网络开始考虑,为了简化,我们假设只有 伯努利 分布。变量 X[1] 和 X[2] 被建模为:
条件分布 X[3] 定义为:
当条件分布 X[4] 定义为:
我们现在可以使用直接采样来估计使用之前引入的链式法则的全联合概率 P(x[1], x[2], x[3], x[4])。
直接采样
使用 直接采样,我们的目标是通过对每个条件分布抽取的样本序列来近似全联合概率。如果我们假设图结构良好(没有不必要的边)并且有 N 个变量,算法由以下步骤组成:
-
初始化变量 N[Samples]。
-
初始化一个形状为 (N, N[Samples]) 的向量 S。
-
初始化一个频率向量 F[Samples],其形状为 (N, N[Samples])。在 Python 中,最好使用一个字典,其中键是组合 (x[1], x[2], x[3], ..., x[N])。
-
对于 t=1 到 N[Samples]:
-
对于 i=1 到 N:
-
从 P(X[i]|Predecessors(X[i])) 中采样
-
将样本存储在 S[i, t]
-
-
如果 F[Samples] 包含采样的元组 S[:, t]:
- F[Samples][S[:, t]] += 1
-
否则:
- F[Samples][**S[:, t]] = 1(这两个操作在 Python 字典中都是立即完成的)
-
-
创建一个形状为 (**N, 1) 的向量 P[Sampled]。
-
将 P[Sampled][i, 0] = F[Samples][**i]/N 设置。
从数学观点来看,我们首先创建一个频率向量 *FSamples,然后我们考虑 N[Samples] → ∞ 来近似全联合概率:
直接采样的例子
现在,我们可以用 Python 实现这个算法。让我们首先定义使用 NumPy 函数 np.random.binomial(1, p)
的样本方法,该函数从概率为 p
的伯努利分布中抽取样本:
import numpy as np
def X1_sample(p=0.35):
return np.random.binomial(1, p)
def X2_sample(p=0.65):
return np.random.binomial(1, p)
def X3_sample(x1, x2, p1=0.75, p2=0.4):
if x1 == 1 and x2 == 1:
return np.random.binomial(1, p1)
else:
return np.random.binomial(1, p2)
def X4_sample(x3, p1=0.65, p2=0.5):
if x3 == 1:
return np.random.binomial(1, p1)
else:
return np.random.binomial(1, p2)
在这一点上,我们可以实现主循环。由于变量是布尔值,所以概率总数为 16,因此我们将Nsamples
设置为5000
(较小的值也可以接受):
N = 4
Nsamples = 5000
S = np.zeros((N, Nsamples))
Fsamples = {}
for t in range(Nsamples):
x1 = X1_sample()
x2 = X2_sample()
x3 = X3_sample(x1, x2)
x4 = X4_sample(x3)
sample = (x1, x2, x3, x4)
if sample in Fsamples:
Fsamples[sample] += 1
else:
Fsamples[sample] = 1
当采样完成时,可以提取完整的联合概率:
samples = np.array(list(Fsamples.keys()), dtype=np.bool_)
probabilities = np.array(list(Fsamples.values()), dtype=np.float64) / Nsamples
for i in range(len(samples)):
print('P{} = {}'.format(samples[i], probabilities[i]))
P[ True False True True] = 0.0286
P[ True True False True] = 0.024
P[ True True True False] = 0.06
P[False False False False] = 0.0708
P[ True False True False] = 0.0166
P[False True True True] = 0.1006
P[False False True True] = 0.054
...
我们还可以查询模型。例如,我们可能对 P(X[4]=True) 感兴趣。我们可以通过查找所有 X[4]=True 的元素,并求和相应的概率来实现这一点:
p4t = np.argwhere(samples[:, 3]==True)
print(np.sum(probabilities[p4t]))
0.5622
这个值与 X[4] 的定义是一致的,X[4] 总是 p >= 0.5。读者可以尝试更改值并重复模拟。
马尔可夫链的温和介绍
为了讨论 MCMC 算法,有必要引入马尔可夫链的概念。实际上,虽然直接采样方法无序地抽取样本,但 MCMC 策略根据从样本到下一个样本的精确转移概率抽取一系列样本。
让我们考虑一个时间相关的随机变量 X(t),并假设一个离散的时间序列 X[1], X[2], ..., X[t], X[t+1], ... 其中 X[t] 表示在时间 t 时的取值。在以下图中,有这个序列的示意图:
通用马尔可夫链的结构
我们可以假设有 N 个不同的状态 s[i] 对于 i=1..N,因此可以考虑到概率 P(X[t]=s[i]|X[t-1]=s[j], ..., X[1]=s[p])。如果 X(t) 被定义为一阶马尔可夫过程,则:
换句话说,在马尔可夫过程中(从现在起,我们省略一阶,即使在某些情况下考虑更多先前状态是有用的),X(t) 处于某个状态的概率只取决于前一时间瞬间所假设的状态。因此,我们可以为每一对 i,j 定义一个转移概率:
考虑所有对 (i, j),也可以构建一个转移概率矩阵 T(i, j) = P(i → j)。使用标准符号,X[t]=s[i] 的边缘概率定义为:
在这一点上,很容易证明(查普曼-科尔莫哥洛夫方程):
在前面的表达式中,为了计算 πi,我们需要对所有可能的前一个状态进行求和,考虑相对转移概率。这个操作可以用矩阵形式重写,使用包含所有状态的向量 π(t) 和转移概率矩阵 T(大写上标 T 表示矩阵是转置的)。链的演变可以通过递归计算:
对于我们的目的来说,考虑能够达到 stationary distribution π[s] 的马尔可夫链是很重要的:
换句话说,状态不依赖于初始条件 π(1),并且它不再能够改变。如果基础马尔可夫过程是 ergodic 的,那么稳态分布是唯一的。这个概念意味着,如果平均时间(这通常是不可能的)或垂直平均(冻结时间)在状态上(在大多数情况下这更简单),过程具有相同的属性。
马尔可夫链的遍历过程由两个条件保证。第一个条件是所有状态都是非周期的,这意味着不可能找到一个正数 p,使得链在经过等于 p 的倍数个瞬间后回到相同的状态序列。第二个条件是所有状态都必须是正 recurrent 的:这意味着,给定一个随机变量 N[instants]**(i),描述了返回到状态 s[i] 所需的时间瞬间数,EN[instants] < ∞;因此,理论上,所有状态都可以在有限的时间内重新访问。
我们需要遍历性条件,以及因此存在唯一稳态分布的原因,是因为我们正在考虑将采样过程建模为马尔可夫链,其中下一个值是根据当前状态采样的。从一个状态到另一个状态的转换是为了找到更好的样本,正如我们将在 Metropolis-Hastings 采样器中看到的那样,我们也可以选择拒绝一个样本并保持链处于相同的状态。因此,我们需要确保算法收敛到唯一的稳定分布(该分布近似于我们的贝叶斯网络的真实完整联合分布)。可以证明,如果:
前面的方程被称为详细平衡,它意味着链的可逆性。直观地说,这意味着链处于状态 A 的概率乘以转移到状态 B 的概率等于链处于状态 B 的概率乘以转移到 A 的概率。
对于我们将要讨论的两种方法,可以证明它们满足上述条件,因此它们的收敛性得到保证。
吉布斯抽样
假设我们想要获得贝叶斯网络 *P(x[1], x[2], x[3], ..., x[N]**) 的完整联合概率;然而,变量的数量很大,无法以封闭形式轻松解决这个问题。此外,想象一下,如果我们想得到一些边缘分布,例如 P(x[2]),但为了做到这一点,我们需要对整个联合概率进行积分,这项任务甚至更难。吉布斯抽样允许通过迭代过程近似所有边缘分布。如果我们有 N 个变量,算法按照以下步骤进行:
-
初始化变量 N[迭代次数]
-
初始化一个形状为 (N, N[迭代次数]) 的向量 S
-
随机初始化 x[1]^((0)), x[2]^((0)), ..., x[N]^((0)) (上标索引指的是迭代次数)
-
对于 t=1 到 N[迭代次数]:
-
从 p(x[1]|x[2]^((t-1)), x[3]^((t-1)), ..., x[N]^((t-1))) 中采样 x[1]^((t)) 并将其存储在 S[0, t]
-
从 p(x[2]|x[1]^((t)), x[3]^((t-1)), ..., x[N]^((t-1))) 中采样 x[2]^((t)) 并将其存储在 S[1, t]
-
从 p(x[3]|x[1]^((t)), x[2]^((t)), ..., x[N]^((t-1))) 中采样 x[3]^((t)) 并将其存储在 S[2, t]
-
...
-
从 p(x[N]|x[1]^((t)), x[2]^((t)), ..., x[N-1]^((t))) 中采样 x[N]^((t)) 并将其存储在 S[N-1, t]
-
迭代结束时,向量 S 将包含每个分布的 N[迭代次数] 个样本。由于我们需要确定概率,因此有必要像直接采样算法那样进行操作,计算单次出现的次数,并通过除以 N[迭代次数] 进行归一化。如果变量是连续的,可以考虑区间,计算每个区间包含的样本数量。
对于小型网络,此过程与直接采样非常相似,但是当处理非常大的网络时,采样过程可能会变得缓慢;然而,在引入 X[i] 的马尔可夫层概念之后,算法可以简化。马尔可夫层是 X[i] 的前驱、后继及其后继的前驱的随机变量集合(在某些书中,他们使用术语 父母 和 子女)。在贝叶斯网络中,变量 X[i] 在其马尔可夫层给定条件下与所有其他变量条件独立。因此,如果我们定义函数 MB(X[i]),它返回层中的变量集合,则通用采样步骤可以重写为 p(x[i]|MB(X[i])),并且不再需要考虑所有其他变量。
为了理解这个概念,让我们考虑以下图中显示的网络:
吉布斯抽样的贝叶斯网络示例
马尔可夫层包括:
-
MB(X[1]) = { X[2], X[3] }
-
MB(X[2]) = { X[1, ]X[3], X[4] }
-
MB(X[3]) = { X[1, ]X[2], X[4], X[5] }
-
MB(X[4]) = { X[3] }
-
MB(X[5]) = { X[3] }
-
MB(X[6]) = { X[2] }
通常情况下,如果 N 非常大,那么 |MB(X[i])| 的基数远小于 N,从而简化了过程(vanilla 吉布斯采样需要对每个变量有 N-1 个条件)。我们可以证明吉布斯采样生成的样本来自一个处于详细平衡状态的马尔可夫链:
因此,该过程收敛到唯一的平稳分布。这个算法相当简单;然而,其性能并不出色,因为随机游走没有被调整以探索状态空间中的正确区域,在这些区域找到好样本的概率很高。此外,轨迹也可能返回到不良状态,从而减慢整个过程。一个替代方案(也被 PyMC3 用于连续随机变量)是 No-U-Turn 算法,我们在这本书中不讨论。对这一主题感兴趣的读者可以在 The No-U-Turn Sampler: Adaptively Setting Path Lengths in Hamiltonian Monte Carlo 一书中找到完整的描述,作者为 Hoffmann M. D.,Gelman A.,arXiv:1111.4246。
Metropolis-Hastings 采样
我们已经看到,当变量数量很大时,贝叶斯网络的完整联合概率分布 P(x[1], x[2], x[3], ..., x[N]) 可能变得难以处理。当需要对其进行边缘化以获得,例如,P(x[i]) 时,问题可能变得更加困难,因为这需要积分一个非常复杂的功能。在简单情况下应用贝叶斯定理时也会出现相同的问题。假设我们有表达式 p(A|B) = K · P(B|A)P(A)。我明确地插入了归一化常数 K,因为我们知道它,我们可以立即获得后验概率;然而,通常需要找到它需要积分 P(B|A)P(A),而这个操作可能是无法用封闭形式表示的。
Metropolis-Hastings 算法可以帮助我们解决这个问题。让我们想象一下,我们需要从 P(x[1], x[2], x[3], ..., x[N]) 中采样,但我们只知道这个分布直到归一化常数,所以 P(x[1], x[2], x[3], ..., x[N]) ∝ g(x[1], x[2], x[3], ..., x[N])。为了简化,从现在开始我们将所有变量合并成一个单向量,所以 P(x) ∝ g(x)。
让我们再取另一个分布q(x'|x((i-1)))*,这被称为**候选生成分布**。对这个选择没有特别的限制,只需*q*易于采样即可。在某些情况下,*q*可以选择与目标分布*p(x)*非常相似的功能,而在其他情况下,可以使用均值为*x((i-1))的正态分布。正如我们将要看到的,这个函数充当提议生成器,但我们没有义务接受从这个分布中抽取的所有样本,因此,可以采用具有相同P(X)域的任何分布。当一个样本被接受时,马尔可夫链过渡到下一个状态,否则它保持在当前状态。这个决策过程基于采样器必须探索最重要的状态空间区域并丢弃那些找到好样本概率低的区域的观点。
算法按照以下步骤进行:
-
初始化变量N[迭代]
-
随机初始化x^((0))
-
对于t=1到N[迭代]:
-
从q(x'|x^((i-1)))中抽取候选样本x'
-
计算以下值:
-
如果α ≥ 1:
- 接受样本x^((t)) = x'
-
否则如果0 < α < 1:
-
以概率α接受样本x^((t)) = x';或者
-
以概率1 - α拒绝样本x',设置x^((t)) = x^((t-1))
-
-
可以证明(证明将省略,但可在*马尔可夫链蒙特卡洛和 Gibbs 抽样,Walsh B.,EEB 596z 课程讲义)Metropolis-Hastings 算法的转移概率满足详细平衡方程,因此算法收敛到真实的后验分布。
Metropolis-Hastings 采样示例
我们可以将此算法实现为找到给定P(B|A)和P(A)的乘积的后验分布P(A|B),而不考虑需要复杂积分的归一化常数。
假设:
因此,得到的g(x)是:
为了解决这个问题,我们采用随机游走 Metropolis-Hastings 方法,该方法包括选择q ∼ 正态分布(μ=x^((t-1))). 这个选择允许简化值α,因为两个项q(x((t-1))|x')*和*q(x'|x((t-1)))是相等的(多亏了通过x[mean]的垂直轴的对称性)并且可以相互抵消,所以α变成了g(x')和g(x^((t-1)))的比值。
第一件事是定义函数:
import numpy as np
def prior(x):
return 0.1 * np.exp(-0.1 * x)
def likelihood(x):
a = np.sqrt(0.2 / (2.0 * np.pi * np.power(x, 3)))
b = - (0.2 * np.power(x - 1.0, 2)) / (2.0 * x)
return a * np.exp(b)
def g(x):
return likelihood(x) * prior(x)
def q(xp):
return np.random.normal(xp)
现在,我们可以开始我们的采样过程,使用 100,000 次迭代和x^((0)) = 1.0:
nb_iterations = 100000
x = 1.0
samples = []
for i in range(nb_iterations):
xc = q(x)
alpha = g(xc) / g(x)
if np.isnan(alpha):
continue
if alpha >= 1:
samples.append(xc)
x = xc
else:
if np.random.uniform(0.0, 1.0) < alpha:
samples.append(xc)
x = xc
为了得到后验分布的表示,我们需要通过 NumPy 函数np.histogram()
创建一个直方图,该函数接受一个值数组以及所需的区间数(bins
);在我们的情况下,我们设置了100
个区间:
hist, _ = np.histogram(samples, bins=100)
hist_p = hist / len(samples)
p(x)的结果图示如下:
抽样概率密度函数
使用 PyMC3 的抽样示例
PyMC3是一个强大的 Python 贝叶斯框架,它依赖于 Theano 来执行高速计算(有关安装说明,请参阅本段末尾的信息框)。它实现了所有最重要的连续和离散分布,并主要使用 No-U-Turn 和 Metropolis-Hastings 算法进行抽样过程。有关 API(分布、函数和绘图实用工具)的所有详细信息,我建议访问文档主页docs.pymc.io/index.html
,在那里还可以找到一些非常直观的教程。
我们想要模拟和仿真的例子基于以下场景:从伦敦到罗马的每日航班有一个预定起飞时间为午夜 12:00,标准飞行时间为两小时。我们需要组织目的地机场的运营,但我们不希望在飞机尚未降落之前分配资源。因此,我们希望使用贝叶斯网络来模拟这个过程,并考虑一些可能影响到达时间的常见因素。特别是,我们知道登机过程可能会比预期更长,以及加油过程,即使它们是并行进行的。伦敦空中交通管制也可以造成延误,当飞机接近罗马时也可能发生同样的情况。我们还知道,恶劣天气的存在可能会因为路线改变而造成另一个延误。我们可以用以下图表总结这个分析:
表示空中交通管制问题的贝叶斯网络
考虑到我们的经验,我们决定使用以下分布来模拟随机变量:
-
乘客登机∼Wald(μ=0.5, λ=0.2)
-
加油∼Wald(μ=0.25, λ=0.5)
-
出发交通延误∼Wald(μ=0.1, λ=0.2)
-
到达交通延误∼Wald(μ=0.1, λ=0.2)
-
出发时间 = 12 + 出发交通延误 + max(乘客登机, 加油)
-
恶劣天气∼伯努利(p=0.35)
-
飞行时间∼指数(λ=0.5 - (0.1 · 恶劣天气))(伯努利分布的输出为0或1,分别对应 False 和 True)
-
到达时间 = 出发时间 + 飞行时间 + 到达交通延误
概率密度函数如下:
出发时间
和到达时间
是随机变量的函数,飞行时间
的参数λ也是恶劣天气
的函数。
即使模型不是很复杂,直接的推理效率也相当低,因此我们希望使用 PyMC3 来模拟这个过程。
第一步是创建一个model
实例:
import pymc3 as pm
model = pm.Model()
从现在开始,所有操作都必须使用由model
变量提供的上下文管理器来执行。我们现在可以设置贝叶斯网络的所有随机变量:
import pymc3.distributions.continuous as pmc
import pymc3.distributions.discrete as pmd
import pymc3.math as pmm
with model:
passenger_onboarding = pmc.Wald('Passenger Onboarding', mu=0.5, lam=0.2)
refueling = pmc.Wald('Refueling', mu=0.25, lam=0.5)
departure_traffic_delay = pmc.Wald('Departure Traffic Delay', mu=0.1, lam=0.2)
departure_time = pm.Deterministic('Departure Time',
12.0 + departure_traffic_delay +
pmm.switch(passenger_onboarding >= refueling,
passenger_onboarding,
refueling))
rough_weather = pmd.Bernoulli('Rough Weather', p=0.35)
flight_time = pmc.Exponential('Flight Time', lam=0.5 - (0.1 * rough_weather))
arrival_traffic_delay = pmc.Wald('Arrival Traffic Delay', mu=0.1, lam=0.2)
arrival_time = pm.Deterministic('Arrival time',
departure_time +
flight_time +
arrival_traffic_delay)
我们导入了两个命名空间,pymc3.distributions.continuous
和pymc3.distributions.discrete
,因为我们正在使用这两种类型的变量。Wald 和指数是连续分布,而Bernoulli
是离散的。在前三行中,我们声明了变量passenger_onboarding
、refueling
和departure_traffic_delay
。结构始终相同:我们需要指定对应于所需分布的类,传递变量的名称和所有必需的参数。
departure_time
变量被声明为pm.Deterministic
。在 PyMC3 中,这意味着一旦所有随机元素都已设置,其值就完全确定。实际上,如果我们从departure_traffic_delay
、passenger_onboarding
和refueling
中采样,我们将得到departure_time
的确定值。在这个声明中,我们还使用了实用函数pmm.switch
,它根据其第一个参数进行二进制选择(例如,如果A > B,则返回A,否则返回B)。
其他变量非常相似,除了flight_time
,它是一个具有参数λ的指数变量,λ是另一个变量(rough_weather
)的函数。作为一个伯努利变量,以概率p输出1,以概率1 - p输出0,当有恶劣天气时,λ = 0.4,否则为0.5。
一旦模型设置完成,就可以通过采样过程对其进行模拟。PyMC3 会根据变量的类型自动选择最佳采样器。由于模型并不复杂,我们可以将过程限制在500
个样本:
nb_samples = 500
with model:
samples = pm.sample(draws=nb_samples, random_seed=1000)
可以使用内置的pm.traceplot()
函数分析输出,该函数为每个样本的变量生成图表。以下图表显示了其中一个的详细情况:
到达时间随机变量的分布和样本
右列显示了为随机变量(在这种情况下,到达时间)生成的样本,而左列显示了相对频率。这个图可以用来验证我们的初步想法;实际上,到达时间的大部分质量集中在 14:00 到 16:00(数字总是十进制,因此需要将时间转换为十进制);然而,我们应该进行积分以得到概率。相反,通过pm.summary()
函数,PyMC3 提供了一个统计摘要,可以帮助我们做出正确的决策。以下代码片段显示了单个变量的摘要输出:
pm.summary(samples)
...
Arrival time:
Mean SD MC Error 95% HPD interval
-------------------------------------------------------------------
15.174 2.670 0.102 [12.174, 20.484]
Posterior quantiles:
2.5 25 50 75 97.5
|--------------|==============|==============|--------------|
12.492 13.459 14.419 16.073 22.557
对于每个变量,它包含均值、标准差、蒙特卡洛误差、95%最高后验密度区间和后验分位数。在我们的案例中,我们知道飞机将在大约 15:10(15.174
)着陆。
这只是一个非常简单的例子,用以展示贝叶斯网络的强大功能。为了深入了解,我建议阅读《统计决策理论导论》,作者 Pratt J.,Raiffa H.,Schlaifer R.,由 MIT Press 出版,其中可以研究这本书范围之外的多种贝叶斯应用。
PyMC3 (docs.pymc.io/index.html
) 可以使用 pip install -U pymc3
命令安装。由于它需要 Theano(会自动安装),还需要提供一个 C/C++ 编译器。我建议使用 Anaconda (www.anaconda.com/download/
) 这样的发行版,它允许通过 conda install -c anaconda mingw
命令安装 MinGW。对于任何问题,您可以在网站上找到详细的安装说明。有关如何配置 Theano 以支持 GPU(默认安装基于 CPU NumPy 算法)的更多信息,请访问此页面:deeplearning.net/software/theano/
。
隐藏马尔可夫模型 (HMMs)
让我们考虑一个可以假设 N 个不同状态的随机过程 X(t):s[1],s[2],...,s[N],具有一阶马尔可夫链动力学。我们还假设我们无法观察到 X(t) 的状态,但我们有权访问另一个与 X(t) 相连的过程 O(t),它产生可观察的输出(通常称为 发射)。这个结果过程被称为 隐藏马尔可夫模型(HMM),其通用架构如下图所示:
通用隐藏马尔可夫模型的结构
对于每个隐藏状态 s[i],我们需要定义一个转移概率 P(i → j),如果变量是离散的,通常表示为一个矩阵。对于马尔可夫假设,我们有:
此外,给定一个观察序列 o[1],o[2],...,o[M],我们还假设以下关于 发射概率 独立性的假设:
换句话说,观察值 o[i](在这种情况下,我们指的是时间 i 时的值)的概率仅由时间 i 的隐藏变量状态 x[i] 决定。传统上,第一个状态 x[0] 和最后一个状态 x[Ending] 从不发射,因此所有序列都从索引 1 开始,并以一个对应最终状态的超时间步结束。
HMMs 可以应用于所有那些无法测量系统状态(我们只能将其建模为具有已知转移概率的随机变量)但可以访问与其相关的数据的情境。一个例子可以是一个由大量部件组成的复杂引擎。我们可以定义一些内部状态并学习一个转移概率矩阵(我们将学习如何做到这一点),但我们只能接收由特定传感器提供的测量值。
有时,即使不是非常现实,但将马尔可夫假设和发射概率独立性包含到我们的模型中是有用的。后者可以这样证明:我们可以采样所有对应于精确状态的峰值发射,由于随机过程O(t)隐式地依赖于X(t),将其视为X(t)的追逐者是合情合理的。
如果这些过程要么是自然的一阶马尔可夫过程,要么状态包含所有用于证明转移所需的历史,那么马尔可夫假设对许多现实生活中的过程成立。换句话说,在许多情况下,如果状态是A,那么会转移到B,最终转移到C。我们假设当在C时,系统从包含A提供部分信息的(B)状态移动过来。
例如,如果我们正在填充一个水箱,我们可以在时间t,t+1,...测量水平(我们系统的状态)。由于我们没有稳定器,我们将水流建模为随机变量,我们可以找到水在时间t达到一定水平p(L[t]=x|L[t-1])的概率。当然,对所有先前状态进行条件化是没有意义的,因为如果水平在时间 t-1 是,例如,80 米,那么确定时间t时新水平(状态)的概率所需的所有信息已经包含在这个状态(80 米)中。
在这个阶段,我们可以开始分析如何训练一个隐马尔可夫模型,以及如何根据一系列观察值确定最可能的隐藏状态。为了简化,我们称A为转移概率矩阵,而B为包含所有P(o[i]|x[t])的矩阵。该模型可以通过这些元素的知识来确定:HMM = { A, B }。
前向-后向算法
前向-后向算法是一种简单但有效的方法,用于根据一系列观察值o[1], o[2], ..., o[t]找到转移概率矩阵T。第一步被称为前向阶段,包括确定一系列观察值的概率P(o[1], o[2], ..., o[序列长度]|A, B)。如果我们需要知道序列的似然性,这部分信息可以直接使用,并且与后向阶段一起,可以用来估计潜在 HMM 的结构(A和B)。
这两种算法都是基于动态规划的概念,即将复杂问题分解为可以轻松解决的子问题,并以递归/迭代的方式重用这些解决方案来解决更复杂的步骤。有关此方面的更多信息,请参阅 《动态规划与马尔可夫过程》,作者:Ronald A. Howard,麻省理工学院出版社。
前向阶段
如果我们将 p[ij] 称为转移概率 P(i → j),我们定义一个考虑以下概率的递归过程:
变量 f[t]^i 表示 HMM 在 t 次观察(从 1 到 t)后处于状态 i(在时间 t)的概率。考虑到 HMM 的假设,我们可以断言 f[t]^(i )依赖于所有可能的 f[t-1]^j。更精确地说,我们有:
通过这个过程,我们考虑 HMM 可以在时间 t-1(即前 t-1 次观察)达到任何状态,并以概率 p[ji] 转移到时间 t 的状态 i。我们还需要考虑最终状态 o[t] 在每个可能的前一状态下的发射概率。
对于定义,初始状态和结束状态不产生输出。这意味着我们可以将任何观察序列写成 0, o[1], o[2], ..., o[Sequence Length], 0,其中第一个和最后一个值是空的。该过程从计算时间 1 的前向消息开始:
必须也要考虑非发射的结束状态:
这里对最后一个状态 x[Ending ]的表达式解释为在 A 和 B 矩阵中结束状态的索引。例如,我们表示 p[ij] 为 A[i, j],意味着从状态 x[t] = i 到状态 x[t+1] = j 的一般时间瞬间的转移概率。同样,p[i][Ending] 表示为 A[i, x[Ending]],意味着从倒数第二个状态 x[Sequence Length-1] = i 到结束状态 x[Sequence Length] = Ending State 的转移概率。
因此,前向算法可以总结为以下步骤(我们假设有 N 个状态,因此我们需要考虑初始和结束状态,需要分配 N+2 个位置):
-
初始化形状为 (N + 2, Sequence Length) 的 Forward 向量。
-
初始化形状为 (N, N) 的 A(转移概率矩阵)。每个元素是 P(x[i]|x[j]**)。
-
初始化形状为 (Sequence Length, N) 的 B。每个元素是 P(o[i]|x[j]**)。
-
对于 i=1 到 N:
- 将 Forward[i, 1] = A[0, i] · B[1, i]
-
对于 t=2 到 Sequence Length-1:
-
对于 i=1 到 N:
- 将 S = 0
-
对于 j=1 到 N:
- 将 S = S + Forward[j, t-1] · A[j, i] · B[t, i]
-
将 Forward[i, t] = S 设置为
-
-
将 S = 0。
-
对于 i=1 到 N:
- 将 S = S + Forward[i, Sequence Length] · A[i, x[Ending]] 设置为
-
将 Forward[x[Ending], Sequence Length] = S 设置为
现在应该很清楚,正向这个名字来源于将信息从前一步传播到下一步,直到结束状态(该状态不产生输出)的过程。
向后阶段
在 向后阶段,我们需要计算在时间 t 的状态为 i 的条件下,从时间 t+1 开始的序列的概率:o[t+1], o[t+2], ..., o[Sequence Length]。就像我们之前所做的那样,我们定义以下概率:
向后算法与正向算法非常相似,但在这个情况下,我们需要朝相反的方向移动,假设我们知道在时间 t 的状态是 i。首先考虑的状态是最后一个状态 x[Ending],它不产生输出,就像初始状态一样;因此我们有:
我们使用初始状态终止递归:
步骤如下:
-
使用形状为 (N + 2, Sequence Length) 的向量 Backward 进行初始化。
-
使用形状为 (N, N) 的 A(转移概率矩阵)进行初始化。每个元素是 P(x[i]|x[j])*。
-
使用形状为 (Sequence Length, N) 的 B 进行初始化。每个元素是 P(o[i]|x[j])*。
-
对于 i=1 到 N:
- 将 Backward[x[Endind], Sequence Length] = A[i, x[Endind]]。
-
对于 t=Sequence Length-1 到 1:
-
对于 i=1 到 N:
-
将 S = 0。
-
对于 j=1 到 N:
- 将 S = S + Backward[j, t+1] · A[j, i] · B[t+1, i]
-
将 Backward[i, t] = S。
-
-
-
将 S = 0。
-
对于 i=1 到 N:
- 将 S = S + Backward[i, 1] · A[0, i] · B[1, i]。
-
将 Backward[0, 1] = S。
HMM 参数估计
现在我们已经定义了正向和向后算法,我们可以使用它们来估计潜在 HMM 的结构。该过程是期望最大化算法的应用,将在下一章(第五章,EM 算法及其应用)中讨论,其目标可以概括为定义我们如何估计 A 和 B 的值。如果我们定义 N(i, j) 为从状态 i 到状态 j 的转移次数,以及 N(i) 为从状态 i 的总转移次数,我们可以用以下方式近似转移概率 P(i → j):
同样,如果我们定义 M(i, p) 为在状态 i 中观察到发射 o[p] 的次数,我们可以用以下方式近似发射概率 P(o[p]|x[i]):
让我们从转移概率矩阵 A 的估计开始。如果我们考虑在给定观察结果的情况下,HMM 在时间 t 处于状态 i,在时间 t+1 处于状态 j 的概率,我们有:
我们可以使用前向和后向算法来计算这个概率,给定一系列观测值 o[1], o[2], ..., o[Sequence Length]。实际上,我们可以使用前向消息 f[t]^i,它是 HMM 在 t 次观测后处于状态 i 的概率,以及后向消息 b[t+1]^j,它是给定 HMM 在时间 t+1 处于状态 j 时,从时间 t+1 开始的序列 o[t+1], o[t+1], ..., o[Sequence Length] 的概率。当然,我们还需要包括发射概率和转移概率 p[ij],这是我们正在估计的。实际上,算法从随机假设开始,迭代直到 A 的值变得稳定。时间 t 处的估计 α[ij] 等于:
在这个背景下,我们省略了完整的证明,因为它很复杂;然而,读者可以在 《隐马尔可夫模型及其在语音识别中的应用教程》,Rabiner L. R.,IEEE 77.2 会议论文 中找到它。
为了计算发射概率,从时间 t 处处于状态 i 给定观测值序列的概率开始计算更容易:
在这种情况下,计算是立即的,因为我们可以在同一时间 t 和状态 i 计算前向和后向消息(记住,考虑到观测值,后向消息是条件于 x[t] = i 的,而前向消息计算观测值与 x[t] = i 联合的概率。因此,乘法是时间 t 处处于状态 i 的未归一化概率)。因此,我们有:
正则化常数的证明可以在上述论文中找到。现在我们可以将这些表达式插入到 a[ij] 和 b[ip] 的估计中:
在第二个公式的分子中,我们采用了指示函数(当条件为真时为 1,否则为 0)来限制仅在那些元素为 o[t] = p 的位置求和。在一个迭代 k 中,p[ij] 是在先前迭代 k-1 中找到的估计值 a[ij]。
算法基于以下步骤:
-
随机初始化矩阵 A 和 B
-
初始化一个容差变量 Tol(例如,Tol = 0.001)
-
当 Norm(A^k - A^(k-1)) > Tol 和 Norm(B^k - B^(k-1)**) > Tol (k 是迭代索引)时:
-
对于 t=1 到 Sequence Length-1:
-
对于 i=1 到 N:
-
对于 j=1 到 N:
- 计算 α^t[ij]
-
计算 β^t[i]
-
-
-
计算估计的 a[ij] 和 b[ip] 并将它们存储在 A^k
-
或者,可以固定迭代次数,尽管最佳解决方案是同时使用容差和最大迭代次数,以便在满足第一个条件时终止过程。
使用 hmmlearn 的 HMM 训练示例
对于这个例子,我们将使用 hmmlearn,这是一个用于 HMM 计算的包(有关更多详细信息,请参阅本节末尾的信息框)。为了简单起见,让我们考虑关于贝叶斯网络的段落中讨论的机场示例,并假设我们有一个代表天气(当然,这并不是一个真正的隐藏变量!)的单个隐藏变量,它被建模为具有两个成分(好和差)的多项式分布。
我们观察我们的航班伦敦-罗马的到达时间(这部分取决于天气条件),并希望训练一个 HMM 来推断未来状态并计算对应给定序列的隐藏状态的后验概率。
我们的示例架构如下所示:
用于天气-到达延误问题的 HMM
让我们先定义我们的观测向量。由于我们有两个状态,其值将是 0
和 1
。让我们假设 0
表示准时,而 1
表示延误:
import numpy as np
observations = np.array([[0], [1], [1], [0], [1], [1], [1], [0], [1],
[0], [0], [0], [1], [0], [1], [1], [0], [1],
[0], [0], [1], [0], [1], [0], [0], [0], [1],
[0], [1], [0], [1], [0], [0], [0], [0], [0]], dtype=np.int32)
我们有 35 个连续的观测值,其值要么是 0
要么是 1
。
为了构建 HMM,我们将使用 MultinomialHMM
类,参数为 n_components=2
、n_iter=100
和 random_state=1000
(始终使用相同的种子以避免结果差异是很重要的)。迭代次数有时很难确定;因此,hmmlearn 提供了一个名为 ConvergenceMonitor
的实用工具类,可以检查算法是否已成功收敛。
现在我们可以使用 fit()
方法来训练我们的模型,将观测列表(数组必须是始终具有形状 序列长度 × N[组件] 的二维数组)作为参数传递:
from hmmlearn import hmm
hmm_model = hmm.MultinomialHMM(n_components=2, n_iter=100, random_state=1000)
hmm_model.fit(observations)
print(hmm_model.monitor_.converged)
True
该过程非常快,监控器(作为实例变量 monitor
可用)已确认收敛。如果模型非常大且需要重新训练,也可以检查 n_iter
的较小值)。一旦模型训练完成,我们就可以立即可视化转换概率矩阵,该矩阵作为实例变量 transmat_
可用:
print(hmm_model.transmat_)
[[ 0.0025384 0.9974616 ]
[ 0.69191905 0.30808095]]
我们可以将这些值解释为从 0
(好天气)转换到 1
(恶劣天气)的概率比相反的概率更高(p[01] 接近 1),并且更有可能保持在状态 1
而不是状态 0
(p[00] 几乎为零)。我们可以推断出观测值是在冬季期间收集的!在下一段解释维特比算法之后,我们还可以检查,给定一些观测值,最可能的隐藏状态序列是什么。
hmmlearn (hmmlearn.readthedocs.io/en/latest/index.html
) 是一个最初被构建为 Scikit-Learn 一部分的框架。它支持多项式和高斯 HMM,并允许使用最常用的算法进行训练和推断。可以使用 pip install hmmlearn
命令进行安装。
维特比算法
维特比算法是 HMM 中最常见的解码算法之一。其目标是找到与一系列观察相对应的最可能的隐藏状态序列。其结构与前向算法非常相似,但该算法不是计算与最后一个时间点状态联合的观察序列的概率,而是寻找:
变量 v[t]^i 表示给定观察序列与 x[t] = i 联合的最大概率,考虑到所有可能的隐藏状态路径(从时间点 1 到 t-1)。我们可以通过评估所有 v[t-1]^j 乘以相应的转移概率 p[ji] 和发射概率 P(o[t]|x[i]) 来递归地计算 v[t]^i,并且总是选择 j 的所有可能值的最大值:
算法基于回溯方法,使用一个回溯指针 bp[t]^i,其递归表达式与 v[t]^i 相同,但用 argmax 函数代替 max:
因此,bp[t]^(i )代表最大化 v[t]^i 的部分隐藏状态序列 x**[1], x[2], ..., x[t-1 ]。在递归过程中,我们逐个添加时间步,因此之前的路径可能会被最后一个观察所无效化。这就是为什么我们需要回溯部分结果并替换时间 t 时不再最大化 v[t+1]^i 的序列。
算法基于以下步骤(与其他情况一样,初始状态和结束状态不产生输出):
-
初始化一个形状为 (N + 2, 序列长度) 的向量 V。
-
初始化一个形状为 (N + 2, 序列长度) 的向量 BP。
-
初始化 A(转移概率矩阵)的形状为 (N, N)。每个元素是 P(x[i]|x[j]**)。
-
初始化 B(发射概率矩阵)的形状为 (序列长度, N)。每个元素是 P(o[i]|x[j]**)。
-
对于 i=1 到 N:
-
设置 V[i, 1] = *A[i, 0] *· B[1, i]
-
BP[i, 1] = Null(或任何其他不能解释为状态的值)
-
-
对于 t=1 到 序列长度:
-
对于 i=1 到 N:
-
设置 V[i, t] = max[j] V[j, t-1] · A[j, i] · B[t, i]
-
设置 BP[i, t] = argmax[j] V[j, t-1] · A[j, i] · B[t, i]
-
-
-
设置 V[x[Endind], 序列长度] = max[j] V[j, 序列长度] **· A[j, x[Endind]]。
-
设置 BP**[x[Endind], 序列长度] = argmax[j] V[j, 序列长度] **· A[j, x[Endind]]。
-
反转 BP。
维特比算法的输出是一个元组,包含最可能的序列 BP 和相应的概率 V。
使用 hmmlearn 寻找最可能的隐藏状态序列
在这个阶段,我们可以继续使用之前的例子,使用我们的模型来找到给定一组可能观察到的最可能的隐藏状态序列。我们可以使用decode()
方法或predict()
方法。第一个返回整个序列的对数概率以及序列本身;然而,它们都默认使用维特比算法作为解码器:
sequence = np.array([[1], [1], [1], [0], [1], [1], [1], [0], [1],
[0], [1], [0], [1], [0], [1], [1], [0], [1],
[1], [0], [1], [0], [1], [0], [1], [0], [1],
[1], [1], [0], [0], [1], [1], [0], [1], [1]], dtype=np.int32)
lp, hs = hmm_model.decode(sequence)
print(hs)
[0 1 1 0 1 1 1 0 1 0 1 0 1 0 1 1 0 1 1 0 1 0 1 0 1 0 1 1 1 1 0 1 1 0 1 1]
print(lp)
-30.489992468878615
序列与转移概率矩阵一致;事实上,持续恶劣天气(1
)的可能性比相反的可能性更大。因此,从1
到 X 的转移比从0
到1
的转移可能性更小。状态的选择是通过选择最高概率来进行的;然而,在某些情况下,差异很小(在我们的例子中,可能会出现p = [0.49, 0.51],这意味着存在高误差的可能性),因此检查序列中所有状态的后验概率是有用的:
pp = hmm_model.predict_proba(sequence)
print(pp)
[[ 1.00000000e+00 5.05351938e-19]
[ 3.76687160e-05 9.99962331e-01]
[ 1.31242036e-03 9.98687580e-01]
[ 9.60384736e-01 3.96152641e-02]
[ 1.27156616e-03 9.98728434e-01]
[ 3.21353749e-02 9.67864625e-01]
[ 1.23481962e-03 9.98765180e-01]
...
在我们的情况下,有几个状态的概率为p ∼ [0.495, 0.505],因此即使输出状态是1(恶劣天气),考虑观察到良好天气的适度概率也是有用的。一般来说,如果一个序列与之前学习(或手动输入)的转移概率一致,那么这些情况并不常见。我建议尝试不同的配置和观察序列,并评估最奇怪情况(如零秒序列)的概率。在那个点上,可以重新训练模型并重新检查新证据是否已被正确处理。
摘要
在本章中,我们介绍了贝叶斯网络,描述了它们的结构和关系。我们看到了如何构建一个网络来模拟一个概率场景,其中某些元素可以影响其他元素的概率。我们还描述了如何使用最常见的采样方法来获得完整的联合概率,这些方法通过近似可以降低计算复杂度。
最常见的采样方法属于 MCMC 算法家族,这些算法将从一个样本到另一个样本的转移概率建模为一阶马尔可夫链。特别是,吉布斯采样基于这样的假设:从条件分布中采样比直接处理完整联合概率要容易。这种方法很容易实现,但它有一些性能上的缺点,可以通过采用更复杂的策略来避免。相反,Metropolis-Hastings 采样器使用候选生成分布和接受或拒绝样本的标准。两种方法都满足详细平衡方程,这保证了收敛(基础马尔可夫链将达到唯一的平稳分布)。
在本章的最后部分,我们介绍了隐马尔可夫模型(HMMs),它允许根据一系列隐藏状态对应的观察值来建模时间序列。实际上,这类模型的主要概念是存在不可观测的状态,这些状态会影响特定观察值的发射(该观察值是可观测的)。我们已经讨论了主要假设以及如何构建、训练和从模型中进行推断。特别是,当需要学习转移概率矩阵和发射概率时,可以使用前向-后向算法;而维特比算法则用于在给定一系列连续观察值的情况下找到最可能的隐藏状态序列。
在下一章,第五章,期望最大化算法及其应用中,我们将简要讨论期望最大化算法,重点关注基于最大似然估计(MLE)方法的一些重要应用。
第五章:EM 算法及其应用
在本章中,我们将介绍许多统计学习任务的一个重要算法框架:EM 算法。与它的名字相反,这不是解决单个问题的方法,而是一种可以在多个环境中应用的方法论。我们的目标是解释其原理,展示数学推导,以及一些实际例子。特别是,我们将讨论以下主题:
-
最大似然估计 (MLE)和最大后验(MAP)学习方法
-
具有简单应用的 EM 算法以估计未知参数
-
高斯混合算法,这是 EM 应用中最著名的之一
-
因子分析
-
主成分分析 (PCA)
-
独立成分分析 (ICA)
-
考虑到 EM 步骤的隐马尔可夫模型 (HMM)的前向-后向算法的简要说明
MLE 和 MAP 学习
假设我们有一个数据生成过程 p[data],用于生成数据集 X:
在许多统计学习任务中,我们的目标是根据最大化标准找到最优参数集 θ。最常见的方法是基于似然,称为 MLE。在这种情况下,最优集 θ 的寻找如下:
这种方法的优势是不受错误先决条件的影响,但与此同时,它排除了将先验知识纳入模型的可能性。它只是在更广泛的子空间中寻找最佳的 θ,以便最大化 p(X|θ)。即使这种方法几乎是无偏的,也有更高的概率找到次优解,这可能与合理的(即使不确定)先验相当不同。毕竟,有些模型太复杂,以至于我们无法定义合适的先验概率(例如,考虑强化学习策略,其中存在大量复杂的状态)。因此,MLE 提供了最可靠的解决方案。此外,可以证明参数 θ 的 MLE 在概率上收敛到真实值:
另一方面,如果我们考虑贝叶斯定理,我们可以推导出以下关系:
后验概率 p(θ|X) 是通过似然和先验概率 p(θ) 共同获得的,因此考虑了编码在 p(θ) 中的现有知识。选择最大化 p(θ|X) 的方法称为 MAP 方法,当可以制定可信的先验或,如在潜在狄利克雷分配 (LDA)的情况下,模型故意基于一些特定的先验假设时,它通常是 MLE 的一个很好的替代方案。
不幸的是,一个错误或不完整的先验分布可能会使模型产生不可接受的结果。因此,MLE 通常是默认选择,即使有可能对p(θ)的结构提出合理的假设。为了理解先验对估计的影响,让我们考虑观察到n=1000 个二项分布(θ对应于参数p)的实验,其中k=800 次成功。似然如下:
为了简单起见,让我们计算对数似然:
如果我们计算相对于θ的导数并将其设置为等于零,我们得到以下结果:
因此,θ的 MLE 是 0.8,这与观察结果一致(我们可以这样说,在观察了 1000 次实验,其中 800 次成功之后,p(X|Success)=0.8)。如果我们只有数据X,我们可以说成功比失败更有可能,因为 1000 次实验中有 800 次是积极的。
然而,在这个简单的练习之后,专家可以告诉我们,考虑到最大的可能人群,边缘概率p(Success)=0.001(伯努利分布,p(Failure) = 1 - P(success))并且我们的样本不具有代表性。如果我们信任专家,我们需要使用贝叶斯定理计算后验概率:
令人惊讶的是,后验概率非常接近零,我们应该拒绝我们的初始假设!在这个时候,有两个选择:如果我们只想基于我们的数据构建模型,MLE 是唯一合理的选项,因为考虑到后验,我们需要接受我们有一个非常差的数据集(这可能是从数据生成过程p[data]抽取样本时的偏差)。
另一方面,如果我们真的信任专家,我们有几种处理问题的方法:
-
检查采样过程以评估其质量(我们可以发现更好的采样会导致非常低的k值)
-
增加样本数量
-
计算θ的 MAP 估计
我建议读者尝试使用简单模型来比较两种方法,以便能够比较相对精度。在这本书中,当我们需要使用统计方法估计模型的参数时,我们总是会采用 MLE。这个选择基于我们的数据集是从p[data]正确采样的假设。如果这不可能(想想一个必须区分马、狗和猫的图像分类器,它使用的数据集中有 500 张马的照片,500 张狗的照片和 5 张猫的照片),我们应该扩展我们的数据集或使用数据增强技术来创建人工样本。
EM 算法
EM 算法是一个通用的框架,可以用于许多生成模型的优化。它最初由Dempster A. P.,Laird N. M.,Rubin D. B**.在《通过 EM 算法从不完全数据中估计最大似然》,《皇家统计学会杂志》,B,39(1):1–38,1977 年 11 月提出,其中作者还证明了其在不同通用的水平上的收敛性。
对于我们的目的,我们将考虑一个数据集,X,以及一组我们无法观察到的潜在变量,Z。它们可以是原始模型的一部分,或者人为地作为简化问题的技巧引入。用向量θ参数化的生成模型具有等于以下的对数似然:
当然,大的对数似然意味着模型能够以小的误差生成原始分布。因此,我们的目标是找到最优的参数集θ,以最大化边缘对数似然(由于我们无法观察它们,我们需要对潜在变量求和或积分):
从理论上讲,这个操作是正确的,但不幸的是,由于其复杂性(特别是求和的对数通常很难处理),它几乎总是不可行的。然而,潜在变量的存在可以帮助我们找到一个容易计算的良好代理,其最大化对应于原始对数似然的最大化。让我们首先使用链式法则重写似然的表达式:
如果我们考虑一个迭代过程,我们的目标是找到一个满足以下条件的程序:
我们可以从考虑一个通用的步骤开始:
第一个要解决的问题是对数和。幸运的是,我们可以使用詹森不等式,它允许我们将对数移到求和内部。让我们首先定义凸函数的概念:一个在凸集D上定义的函数f(x),如果满足以下条件,则称为凸函数:
如果不等式是严格的,那么函数被称为严格凸的。直观地,考虑一个单变量函数f(x),前面的定义表明函数永远不会高于连接两个点(x[1], f(x[1]))和(x**[2], f(x[2]))的线段。在严格凸性的情况下,f(x)总是位于该线段下方。逆这些定义,我们得到函数是凹的或严格凹的的条件。
如果函数 f(x) 在 D 上是凹的,那么函数 -f(x) 在 D 上是凸的;因此,由于 log(x) 在 0, ∞) 上是凹的(或用等价记法在 [0, ∞[) 上),-log(x) 在 [0, ∞) 上是凸的,如下图所示:
![图片
Jensen 不等式(证明被省略,但更详细的内容可以在 Jensen 的算子不等式,Hansen F.,Pedersen G. K.,arXiv:math/0204049 [math.OA] 中找到)表明,如果 f(x) 是在凸集 D 上定义的凸函数,如果我们选择 n 个点 x[1],x[2],...,x[n] ∈ D 和 n 个常数 λ[1],λ[2],...,λ[n] ≥ 0 满足条件 λ[1] + λ[2] + ... + λ[n] = 1,那么以下适用:
因此,考虑到 -log(x) 是凸的,Jensen 不等式 对于 log(x) 变为如下:
因此,通用的迭代步骤可以重写如下:
应用 Jensen 不等式,我们得到以下结果:
所有条件都满足,因为根据定义,项 P(z[i]|X, θ[t]) 都在 [0, 1] 之间,并且所有 z 的和必须始终等于 1(概率定律)。前面的表达式意味着以下陈述是正确的:
因此,如果我们最大化不等式的右侧,我们也会最大化对数似然。然而,考虑到我们只优化参数向量 θ 并且可以删除所有不依赖于它的项,问题可以进一步简化。因此,我们可以定义一个 Q 函数(它与我们在第十四章(51bcd684-080e-4354-b2ed-60430bd15f6d.xhtml,强化学习导论)中将要讨论的 Q-Learning 没有关系),其表达式如下:
Q 是在完整数据 Y = (X, Z) 和当前迭代参数集 θ[t] 下对数似然的期望值。在每次迭代中,Q 是基于当前的估计 θ[t ]计算的,并且它是基于变量 θ 最大化计算的。现在更清楚为什么潜在变量经常被人为引入:它们允许我们应用 Jensen 不等式 并将原始表达式转换为易于评估和优化的期望值。
在这一点上,我们可以形式化 EM 算法:
-
设置一个阈值 Thr(例如,Thr = 0.01)
-
设置一个随机参数向量 θ[0.]
-
当 |L(θ[t]|X, Z) - L(θ[t-1]|X, Z)| > Thr 时:
-
E-Step: 计算条件概率 Q(θ|θ[t]). 通常,这一步包括使用当前的参数估计 θ[t] 来计算条件概率 p(z|X, θ[t]) 或其某些矩(有时,充分统计量仅限于均值和协方差)。
-
M-步:找到 θ[t+1] = argmax[θ] Q(θ|θ[t])。新的参数估计是通过最大化 Q 函数来计算的。
-
当对数似然不再增加或达到固定迭代次数后,程序结束。
参数估计的一个例子
在这个例子中,我们看到如何应用 EM 算法来估计未知参数(灵感来源于原始论文 通过 EM 算法从不完全数据中估计最大似然,Dempster A. P.,Laird N. M.,Rubin D. B.,《皇家统计学会杂志》,B, 39(1):1–38,1977 年 11 月)。
让我们考虑一个由三个可能结果 x[1],x[2],x[3] 和相应的概率 p[1],p[2] 和 p[3] 组成的多项分布模型化的独立实验序列。概率质量函数如下:
假设我们可以观察到 z[1] = x[1] + x[2] 和 x[3],但我们无法直接访问单个值 x[1] 和 x[2]。因此,x[1] 和 x[2] 是潜在变量,而 z[1] 和 x[3] 是观测变量。概率向量 p 按以下方式参数化:
我们的目的是在给定 n,z[1] 和 x[3] 的情况下找到 θ 的最大似然估计。让我们开始计算对数似然:
我们可以利用期望值算子 E[•] 的线性来推导相应的 Q 函数的表达式:
给定 z[1] 的变量 x[1] 和 x[2] 是二项分布的,可以表示为 θ[t] 的函数(我们需要在每次迭代中重新计算它们)。因此,x[1]^((t+1)) 的期望值如下:
而 x[2]^((t+1)) 的期望值如下:
如果我们将这些表达式应用于 并对 θ 求导,我们得到以下结果:
因此,求解 θ,我们得到以下结果:
在这一点上,我们可以推导出 θ 的迭代表达式:
让我们计算当 z[1] = 50 和 x[3] = 10 时 θ 的值:
def theta(theta_prev, z1=50.0, x3=10.0):
num = (8.0 * z1 * theta_prev) + (4.0 * x3 * (12.0 - theta_prev))
den = (z1 + x3) * (12.0 - theta_prev)
return num / den
theta_v = 0.01
for i in range(1000):
theta_v = theta(theta_v)
print(theta_v)
1.999999999999999
p = [theta_v/6.0, (1-(theta_v/4.0)), theta_v/12.0]
print(p)
[0.33333333333333315, 0.5000000000000002, 0.16666666666666657]
在这个例子中,我们已将所有概率进行了参数化,考虑到 z[1] = x[1] + x[2],我们有一个自由度来选择 θ。读者可以通过设置 p[1] 或 p[2] 中的一个值,并将其他概率作为 θ 的函数来重复此例子。计算几乎相同,但在这个情况下,没有自由度。
高斯混合
在 第二章 《半监督学习导论》中,我们讨论了半监督学习背景下的生成高斯混合模型。在本段中,我们将应用 EM 算法推导参数更新的公式。
让我们考虑一个从数据生成过程 p[data] 中抽取的数据集 X:
我们假设整个分布是由 k 个高斯分布的和生成的,这样每个样本的概率可以表示如下:
在前面的表达式中,项 w[j] = P(N=j) 是第 j 个高斯分布的相对权重,而 μ[j] 和 Σ[j] 是均值和协方差矩阵。为了与概率定律保持一致,我们还需要施加以下条件:
不幸的是,如果我们直接尝试解决这个问题,我们需要管理求和的对数,这个过程变得非常复杂。然而,我们已经了解到,可以使用潜在变量作为辅助工具,每当这个技巧可以简化解决方案时。
让我们考虑一个单个参数集 θ=(w[j], μ[j], Σ[j]) 和一个潜在指示矩阵 Z,其中每个元素 z[ij] 等于 1 如果点 x[i] 已经由第 j 个高斯分布生成,否则为 0。因此,每个 z[ij] 是伯努利分布,参数等于 p(j|x[i], θ[t])。
因此,联合对数似然函数可以使用指数指示符表示法表示如下:
索引 i 指的是样本,而 j 指的是高斯分布。如果我们应用链式法则和对数性质,表达式变为以下形式:
第一个项表示在 j 个高斯分布下 x[i] 的概率,而第二个项是第 j 个高斯分布的相对权重。现在我们可以使用联合对数似然函数来计算 Q(θ;θ[t]) 函数:
利用 E[•] 的线性特性,前面的表达式变为以下形式:
项 p(j|x**[i], θ[t]) 对应于考虑完整数据的 z[ij] 的期望值,并表达了给定样本 x[i] 的 j 个高斯分布的概率。考虑到贝叶斯定理,它可以简化如下:
第一个项是 x[i] 在 j^(th) 高斯下的概率,而第二个项是在相同的参数集 θ[t] 下 j^(th) 高斯的权重。为了推导参数的迭代表达式,写出多元高斯分布对数的完整公式是有用的:
为了简化这个表达式,我们使用迹技巧。实际上,由于 (x[i] -* μ[j)^T* Σ^(-1) (x**[i] - μj) 是一个标量,我们可以利用 tr(AB) = tr(BA) 和 tr(c) = c 的性质,其中 A 和 B 是矩阵,c ∈ ℜ:
![图片
让我们从均值的估计开始考虑(只有 Q(θ;θ**[t]) 的第一个项依赖于均值和协方差):
将导数设为零,我们得到以下结果:
同样地,我们得到协方差矩阵的表达式:
为了获得权重的迭代表达式,过程稍微复杂一些,因为我们需要使用拉格朗日乘数(更多信息可以在www.slimy.com/~steuard/teaching/tutorials/Lagrange.html
找到)。考虑到权重的总和必须始终等于 1,可以写出以下方程:
将两个导数都设为零,从第一个导数出发,考虑到 wj = p(j|θ),我们得到以下结果:
而从第二个导数,我们得到以下结果:
最后一步来源于基本条件:
因此,权重的最终表达式如下:
在这一点上,我们可以正式化高斯混合算法:
-
为 w[j]^((0)), θ^((0))[j]* 和 Σ^((0))[j] 设置随机初始值
-
E-Step: 使用贝叶斯定理计算 p(j|x[i], θ[t]):p(j|x**[i], θ[t]) = α w^((t))[j] * p(x[i]|j, θ[t])
-
M-Step: 使用之前提供的公式计算 w[j]^((t+1)), θ^((t+1))[j] 和 Σ^((t+1))[j]。
该过程必须迭代,直到参数变得稳定。通常,最佳实践是使用阈值和最大迭代次数。
使用 Scikit-Learn 的 Gaussian Mixtures 示例
我们现在可以使用 Scikit-Learn 的实现来实施高斯混合算法。直接方法已在第二章,“半监督学习简介”中展示。数据集生成时具有三个聚类中心和由于标准差等于 1.5 的适度重叠:
from sklearn.datasets import make_blobs
nb_samples = 1000
X, Y = make_blobs(n_samples=nb_samples, n_features=2, centers=3, cluster_std=1.5, random_state=1000)
相应的图表如下所示:
Scikit-Learn 的实现基于 GaussianMixture
类,它接受参数如高斯数量 (n_components
)、协方差类型 (covariance_type
),可以是 full
(默认值),如果所有组件都有自己的矩阵,tied
如果矩阵是共享的,diag
如果所有组件都有自己的对角矩阵(这条件使得特征之间不相关),以及 spherical
当每个高斯在所有方向上都是对称的。其他参数允许设置正则化和初始化因子(有关更多信息,读者可以直接查看文档)。我们的实现基于完全协方差:
from sklearn.mixture import GaussianMixture
gm = GaussianMixture(n_components=3)
gm.fit(X)
在模型拟合后,可以通过实例变量 weights_
、means_
和 covariances_
访问学习到的参数:
print(gm.weights_)
[ 0.32904743 0.33027731 0.34067526]
print(gm.means_)
[[ 3.03902183 -7.69186648]
[ 9.04414279 -0.37455175]
[ 7.37103878 -5.77496152]]
print(gm.covariances_)
[[[ 2.34943036 0.08492009]
[ 0.08492009 2.36467211]]
[[ 2.10999633 0.02602279]
[ 0.02602279 2.21533635]]
[[ 2.71755196 -0.0100434 ]
[-0.0100434 2.39941067]]]
考虑到协方差矩阵,我们可以理解特征之间非常不相关,高斯几乎呈球形。最终的图表可以通过将每个点分配给相应的聚类(高斯分布)通过 Yp = gm.transform(X)
命令获得:
通过应用具有三个组件的高斯混合得到的有标签数据集
读者应该已经注意到高斯混合和 k-means(我们将在第七章,“聚类算法”中讨论)之间有很强的类比。特别是,我们可以声明 K-means 是球形高斯混合的一个特例,其中协方差 Σ → 0。这个条件将方法从软聚类(每个样本以精确的概率分布属于所有聚类)转变为硬聚类(通过考虑样本和质心(或均值)之间的最短距离来完成分配)。因此,在某些书中,高斯混合算法也被称为软 K-means。我们将要介绍的一个概念上类似的方法是模糊 K-means,它基于由隶属函数定义的分配,这些函数类似于概率分布。
因子分析
假设我们有一个高斯数据生成过程,p[data ]∼ N(0, Σ),并且从中抽取了 M 个零均值样本:
如果p**[data]的均值μ ≠ 0,也可以使用这个模型,但需要通过一些公式的微小变化来考虑这个非零值。由于零中心化通常没有缺点,更容易去除均值以简化模型。
在无监督学习中,最常见的问题之一是找到一个低维分布p[lower],使得与p[data]的 Kullback-Leibler 散度最小化。当进行因子分析(FA)时,根据发表在EM 算法在机器学习因子分析中的应用,Rubin D.,Thayer D.,Psychometrika,47/1982,第 1 期,以及混合因子分析中的 EM 算法,Ghahramani Z.,Hinton G. E.,CRC-TG-96-1,1996 年 5 月,的原提案,我们假设将通用样本x建模为高斯潜在变量z(其维度p通常是p < n)的线性组合,再加上一个可加且去相关的高斯噪声项ν:
矩阵 A 被称为因子载荷矩阵,因为它决定了每个潜在变量(因子)对x重建的贡献。因子和输入数据被认为是统计独立的。相反,考虑到最后一个项,如果ω[0]² ≠ ω[1]² ≠ ... ≠ ω[n]²,噪声被称为异方差,而如果方差相等ω[0]² = ω[1]² = ... = ω[n]² = ω²,则定义为同方差。为了理解这两种噪声之间的区别,考虑一个信号 x,它是两个相同声音的和,记录在不同的地方(例如,机场和森林)。在这种情况下,我们可以假设也有不同的噪声方差(第一个应该比第二个高,考虑到不同的噪声源数量)。如果相反,两个声音都在隔音室中录制,或者在同一个机场中录制,同方差噪声更有可能(我们不考虑功率,但方差之间的差异)。
与其他方法(如 PCA)相比,FA 的一个重要优势是其对异方差噪声的内禀鲁棒性。事实上,在模型中包含噪声项(仅限于去相关约束)允许基于单个成分进行部分去噪滤波,而 PCA 的一个前提条件是只施加同方差噪声(在许多情况下,这与噪声完全不存在非常相似)。考虑到前面的例子,我们可以假设第一个方差为ω[0]² = k ω[1]²,其中k > 1。这样,模型将能够理解第一个成分的高方差应该(以更高的概率)被视为噪声与成分固有属性的乘积。
现在我们来分析线性关系:
考虑高斯分布的性质,我们知道 x 符合正态分布 N(μ, Σ),因此很容易确定均值或协方差矩阵:
因此,为了解决问题,我们需要找到最佳的 θ=(A, Ω),使得 AA^T + Ω ≈ Σ(对于零均值数据集,估计仅限于输入协方差矩阵 Σ)。现在应该更清楚地理解处理噪声变量的能力。如果 AA^T + Ω 等于 Σ 并且 Ω 的估计是正确的,算法将优化因子加载矩阵 A,排除噪声项产生的干扰;因此,成分将大约去噪。
为了采用 EM 算法,我们需要确定联合概率 p(X, z; θ) = p(X|z; θ)p(z|θ)。右侧的第一个项可以很容易地确定,考虑到 x - Az 符合正态分布 N(0, Ω);因此,我们得到以下:
我们现在可以确定 Q(θ;θ[t]) 函数,忽略常数项 (2π)^k 和与 θ 无关的项 z^Tz。此外,展开指数中的乘法是有用的:
使用迹技巧对最后一个项(它是一个标量)进行重写,我们可以将其表示如下:
利用 E[•] 的线性,我们得到以下:
这个表达式与我们之前在高斯混合模型中看到的是相似的,但在这个情况下,我们需要计算 z 的条件期望和条件二阶矩。不幸的是,我们无法直接计算它们,但可以通过利用 x 和 z 的联合正态性来计算。特别是,使用一个经典定理,我们可以根据以下关系对整个联合概率 p(z, x) 进行划分:
条件分布 p(z|x=x[i]) 的均值等于以下:
条件方差如下:
因此,条件二阶矩等于以下:
如果我们定义辅助矩阵 K = (AA^T + Ω)^(-1),则前面的表达式变为以下:
想要了解更多关于此技术细节的读者可以阅读《Preview》。
《统计决策理论导论》,作者:Pratt J.,Raiffa H.,Schlaifer R.,出版社:MIT Press。
使用前面的表达式,可以构建逆模型(有时称为识别模型,因为它从效果开始重建原因),该模型仍然是高斯分布的:
现在我们能够根据A和Ω最大化Q(θ;θ**[t]),考虑θ[t]=(A[t], Ω[t])以及根据之前的估计θ[t-1]=(A[t-1], Ω[t-1])计算的条件期望和二阶矩。因此,它们不涉及推导过程。我们采用惯例,在时间t计算要最大化的项,而所有其他项都是通过之前的估计获得的(t - 1):
因此A[t]的表达式如下(Q是有偏输入协方差矩阵E[X^TX]对于零中心数据集):
同样,我们可以通过计算相对于Ω^(-1)的导数来获得Ω[t]的表达式(这个选择简化了计算,并且不会影响结果,因为我们必须将导数设为零):
第一个项的导数,即实对角矩阵的行列式,是通过使用伴随矩阵Adj(Ω)并利用逆矩阵的性质T^(-1) = det(T)(-1)Adj(T)*和性质*det(T)(-1) = det(T(-1))*以及*det(TT) = det(T)获得的:
对Ω[t](施加对角性约束)的表达式如下:
总结步骤,我们可以定义完整的 FA 算法:
-
为A((0))*和Ω*((0))设置随机初始值
-
计算有偏输入协方差矩阵Q = E[X^TX]
-
E 步骤:计算A^((t)), Ω^((t)), 和K^((t))
-
M 步骤:使用之前的估计和之前提供的公式计算 A^((t+1)), Ω^((t+1)), 和K**^((t+1))
-
计算逆模型的矩阵B和Ψ
必须重复这个过程,直到A^((t)), Ω^((t)), 和K**^((t))的值停止改变(使用阈值)以及最大迭代次数的约束。因子可以通过逆模型z = Bx + λ轻松获得。
Scikit-Learn 的因子分析示例
我们现在可以用 Scikit-Learn 和 MNIST 手写数字数据集(原始版本中包含 70,000 个 28 × 28 的灰度图像,并添加了异方差噪声[ω[i]随机选择自[0, 0.75])来做一个 FA 的例子。
第一步是加载并零中心原始数据集(我使用第一章节中定义的函数,第一章,机器学习模型基础):
import numpy as np
from sklearn.datasets import fetch_mldata
digits = fetch_mldata('MNIST original')
X = zero_center(digits['data'].astype(np.float64))
np.random.shuffle(X)
Omega = np.random.uniform(0.0, 0.75, size=X.shape[1])
Xh = X + np.random.normal(0.0, Omega, size=X.shape)
在这一步之后,X
变量将包含零中心的原数据集,而Xh
是带噪声的版本。以下截图显示了从两个版本中随机选择的样本:
我们可以使用 Scikit-Learn 的FactorAnalysis
类,并设置n_components=64
参数,对两个数据集执行因子分析,并检查得分(所有样本的平均对数似然)。如果已知噪声方差(或有一个良好的估计),可以通过noise_variance_init
参数包含起始点;否则,它将使用单位矩阵初始化:
from sklearn.decomposition import FactorAnalysis
fa = FactorAnalysis(n_components=64, random_state=1000)
fah = FactorAnalysis(n_components=64, random_state=1000)
Xfa = fa.fit_transform(X)
Xfah = fah.fit_transform(Xh)
print(fa.score(X))
-2162.70193446
print(fah.score(Xh))
-3046.19385694
如预期,噪声的存在降低了最终的准确度(最大似然估计)。根据A. Gramfort和D. A. Engemann在原始 Scikit-Learn 文档中提供的示例,我们可以使用Lodoit-Wolf算法(一种用于改善协方差条件的收缩方法,超出了本书的范围)为 MLE 创建一个基准。
对于更多信息,请阅读A Well-Conditioned Estimator for Large-Dimensional Covariance Matrices,Ledoit O.,Wolf M.,Journal of Multivariate Analysis,88,2/2004":
from sklearn.covariance import LedoitWolf
ldw = LedoitWolf()
ldwh = LedoitWolf()
ldw.fit(X)
ldwh.fit(Xh)
print(ldw.score(X))
-2977.12971009
print(ldwh.score(Xh))
-2989.27874799
使用原始数据集,因子分析比基准表现得更好,而在存在异方差噪声的情况下则略差。读者可以尝试使用网格搜索不同的成分数量和噪声方差的其他组合,并实验去除零中心化步骤的效果。可以使用components_
实例变量绘制提取的成分:
对原始数据集进行因子分析提取的 64 个成分的图
仔细分析表明,这些成分是许多低级视觉特征的叠加。这是由于对成分具有高斯先验分布的假设(z ∼ N(0, I))。事实上,这种分布的一个缺点是其固有的密集性(采样值远离均值的概率通常太高,而在某些情况下,人们希望有一个峰值分布,以阻止值远离其均值,以便能够观察到更具有选择性的成分)。此外,考虑到分布p[Z|X; θ],协方差矩阵ψ不能对角化(试图施加此约束可能导致无法解决的问题),导致一个结果的多变量高斯分布,它通常不是由独立的成分组成的。一般来说,单个变量z[i,](在输入样本x[i]的条件下)在统计上是相关的,并且重建x[i,]是通过几乎所有提取的特征的参与获得的。在这些所有情况下,我们说编码是密集的,特征字典在欠完备的(成分的维度低于dim(x[i]))。
考虑到任何正交变换Q应用于A(因子负载矩阵)不会影响分布p[X|Z, θ],缺乏独立性也可能是一个问题。事实上,由于QQ^T=I,以下适用:
换句话说,任何特征旋转(x = AQz + ν)都是原始问题的解决方案,并且无法决定哪个是真实的负载矩阵。所有这些条件导致进一步的结论,即成分之间的互信息不等于零,也不接近最小值(在这种情况下,每个都携带特定的信息部分)。另一方面,我们的主要目标是降低维度。因此,有依赖的成分并不奇怪,因为我们旨在保留p(X)中包含的最大原始信息量(记住,信息量与熵相关,而熵与方差成正比)。
同样的现象可以在 PCA(它仍然基于高斯假设)中观察到,但在最后一段,我们将讨论一种称为 ICA 的技术,其目标是基于一组统计独立的特征创建每个样本的表示(不受到降维的约束)。这种方法,尽管有其特殊性,但仍属于被称为稀疏编码的算法大家族。在这种情况下,如果相应的字典满足dim(z[i]) > dim(x[i]),则称为过完备(当然,主要目标不再是降维)。
然而,我们只考虑字典最多是完备的情况dim(z[i]) = dim(x[i]),因为过完备字典的 ICA 需要更复杂的方法。稀疏度水平当然与dim(z[i])*成正比,并且在使用 ICA 时,它总是作为次要目标(主要目标始终是成分之间的独立性)实现的。
主成分分析
另一种解决高维数据集降维问题的常见方法基于这样的假设:通常,总方差不是由所有成分均匀解释的。如果p[data]是一个协方差矩阵为Σ的多变量高斯分布,那么熵(它是分布中包含的信息量的度量)如下所示:
因此,如果某些成分具有非常低的方差,它们对熵的贡献也有限,提供的信息很少。因此,它们可以被移除而不会导致精度损失过高。
正如我们在 FA 中所做的那样,让我们考虑一个从p[data] ∼ N(0, Σ)(为了简单起见,我们假设它是零中心的,即使这不是必要的)抽取的数据集:
我们的目标是定义一个线性变换z = A^Tx(向量通常被认为是一列,因此x的形状是(n × 1)),如下所示:
由于我们想要找出方差较高的方向,我们可以从输入协方差矩阵Σ(它是实数、对称和正定的)的特征分解开始构建我们的变换矩阵A:
V是一个包含特征向量(作为列)的(n × n)矩阵,而Ω是一个包含特征值的对角矩阵。此外,V也是正交的,因此特征向量构成了一个基。另一种方法是基于奇异值分解(SVD),它有一个增量变体,并且有一些算法可以在任意数量的组件上进行截断分解,从而加快收敛过程(例如 Scikit-Learn 实现TruncatedSVD
)。
在这种情况下,样本协方差如下所示:
如果我们将 SVD 应用于矩阵X(每一行代表一个形状为(1, n)的单个样本),我们得到以下结果:
U是一个包含(作为行)左奇异向量的单位矩阵(XXT*的特征向量),*V*(也是单位矩阵)包含(作为行)右奇异向量(对应于*XTX的特征向量),而Λ是一个包含Σ[s]的奇异值的对角矩阵(它们是XX(T)*和*XTX的特征值的平方根)。通常,特征值按降序排序,特征向量被重新排列以匹配相应的位置。
因此,我们可以直接使用矩阵Λ来选择最相关的特征值(平方根是一个增函数,不会改变顺序)和矩阵V来检索相应的特征向量(因子1/M是一个比例常数)。这样,我们不需要计算和特征分解协方差矩阵Σ(包含n × n个元素),并且可以利用一些仅与数据集(不计算XTX*)工作的非常快速的近似算法。使用奇异值分解(SVD),可以直接对*X*进行变换,考虑到*U*和*V*是单位矩阵(这意味着*UUT = U^TU = I;因此,共轭转置也是逆):
目前,X只被投影到特征向量空间(它只是简单地旋转了)并且其维度没有改变。然而,从特征向量的定义中,我们知道以下是真的:
如果λ很大,v的投影将按对应特征向量方向解释的方差成比例放大。因此,如果尚未这样做,我们可以对特征值及其对应的特征向量进行排序(并重命名),以获得以下结果:
如果我们选择前k个最大的特征值,我们可以根据对应的特征向量(主成分)构建一个转换矩阵,该矩阵将X投影到原始特征向量空间的一个子空间:
使用奇异值分解(SVD),我们可以直接截断U和Λ,创建包含仅包含前 k 个特征向量的矩阵U[k]和包含前 k 个特征值的对角矩阵Λ[k]。
当选择k的值时,我们假设以下情况是正确的:
为了实现这一目标,通常需要将性能与不同数量的组件进行比较。在下面的图表中,有一个图表显示了方差比(由组件 n 解释的方差/总方差)和累积方差作为组件的函数:
每个组件的解释方差(左)和每个组件的累积方差(右)
在这种情况下,前 10 个组件能够解释总方差的 80%。剩余的 25 个组件的影响越来越小,可以删除。然而,选择必须始终基于具体上下文,考虑到由于信息丢失而引起的价值损失。
确定正确组件数量的一个技巧是基于 X 的特征值分析。在排序后,可以考虑到后续值之间的差异 d = {λ[1] - λ[2],λ[2] - λ[3],...,λ[n-1] - λ[n]}。最大的差异λ[k] - λ[k+1]决定了潜在最佳降维的索引k(显然,需要考虑最小值的约束,因为通常λ[1] - λ[2]是最大的差异)。例如,如果 d = {4,4,3,0.2,0.18,0.05},则原始维度为 n=6;然而,λ[4] - λ[5]是最小的差异,因此,将维度降低到(n + 1) - k = 3是合理的。原因是直截了当的,特征值决定了每个组件的大小,但我们需要一个相对度量,因为尺度会变化。在示例中,最后三个特征向量指向的方向,与前三个组件相比,解释方差可以忽略不计。
一旦我们定义了转换矩阵A[k],就可以通过以下关系在新的子空间中执行原始向量的实际投影:
整个数据集的完全转换可以简单地按以下方式获得:
现在,让我们分析新的协方差矩阵 E[Z^TZ]。如果原始分布 p[data] x ∼ N(0, Σ),则 p(z) 也将是高斯分布,具有均值和协方差:
我们知道 Σ 是正交的;因此,当 i ≠ j 时,v[i] · v[j] = 0。如果我们分析 A^TV 这个项,我们得到以下结果:
考虑到 Ω 是对角的,结果矩阵 Σ[z] 也将是对角的。这意味着 PCA 去除了变换后的协方差矩阵的相关性。同时,我们可以声明,每个去除了输入协方差矩阵相关性的算法都执行了 PCA(带有或没有降维)。例如,白化过程是一个没有降维的特定 PCA,而 Isomap(见第三章,基于图的无监督学习)使用更几何的方法通过 Gram 矩阵执行相同的操作。这个结果将在第六章,赫布学习中使用,以展示一些特定的神经网络如何在不分解 Σ 的特征值的情况下执行 PCA。
现在让我们考虑一个具有同方差噪声的 FA。我们已经看到,条件分布的协方差矩阵 p(X|Z; θ) 等于 AA^T + Ω。在同方差噪声的情况下,它变为 AA^T + ωI。对于一般的协方差矩阵 Σ,可以证明添加一个常数对角矩阵 (Σ + aI) 不会改变原始的特征向量,并且将特征值移动相同的数量:
因此,我们可以考虑没有噪声的通用情况,而不会失去一般性。我们知道 FA(Ω = (0))的目标是找到矩阵 A,使得 AA^T ≈ Q(输入协方差)。因此,由于对称性和施加渐近等价性,我们可以写出以下结果:
这个结果意味着 FA 是在存在异方差噪声的情况下管理降维的更通用(且更稳健)的方法,而 PCA 是对同方差噪声的限制。当对受异方差噪声影响的数据集进行 PCA 时,MLE 会变差,因为不同的噪声成分,在不同程度上改变特征值的幅度,可能导致选择到在原始数据集中仅解释了低百分比的方差(在无噪声场景下,通常会被更重要的方向所取代)。如果你想到前一段开头讨论的例子,我们知道噪声是强异方差的,但我们没有工具来告知 PCA 如何应对它,并且第一个组件的方差将远高于预期,考虑到两个来源是相同的。不幸的是,在现实场景中,噪声是相关的,当噪声功率非常高时,无论是因子分析还是 PCA 都无法有效地解决问题。在所有这些情况下,必须采用更复杂的去噪技术。相反,如果可以定义一个近似的对角噪声协方差矩阵,FA 肯定比 PCA 更稳健和高效。后者应仅在无噪声或准无噪声场景下考虑。在这两种情况下,结果永远不会导致特征分离。因此,ICA 已经被研究,并已经设计了许多不同的策略。
PCA 的完整算法如下:
-
创建一个包含所有样本 x[i]作为行的矩阵X^((M × n))
-
特征分解版本:
-
计算协方差矩阵Σ = [X^TX]
-
对Σ进行特征分解Σ = VΩV^T
-
-
SVD 版本:
- 对矩阵X = UΛV^T进行 SVD
-
选择最大的 k 个特征值(来自Ω或Λ)和相应的特征向量(来自 V)
-
创建一个形状为(n × k)的矩阵 A,其列是最大的 k 个特征向量(每个特征向量形状为(n × 1))
-
将数据集投影到低维空间 Z = XA(特征分解)或Z = UΛ (SVD)
-
一些包(如 Scipy,它是许多 NumPy 函数的后端,如np.linalg.svd()
)已经返回了转置的矩阵 V(右奇异向量)。在这种情况下,算法的第 3 步中需要使用V^T而不是 V。我建议在实现这些类型的算法时始终检查文档。
使用 Scikit-Learn 进行 PCA 的示例
我们可以重复使用与 FA 和异方差噪声相同的实验来评估 PCA 的 MLE 得分。我们将使用具有相同数量组件的PCA
类(n_components=64
)。为了达到最大精度,我们还设置了svd_solver='full'
参数,以强制 Scikit-Learn 应用完整的 SVD 而不是截断版本。这样,只有分解后才会选择最大的特征值,避免了不精确估计的风险:
from sklearn.decomposition import PCA
pca = PCA(n_components=64, svd_solver='full', random_state=1000)
Xpca = pca.fit_transform(Xh)
print(pca.score(Xh))
-3772.7483580391995
结果并不令人惊讶:由于异方差噪声导致的错误估计,最大似然估计(MLE)远低于因子分析(FA)。我邀请读者比较不同数据集和噪声水平下的结果,考虑到主成分分析(PCA)的训练性能通常高于因子分析。因此,当处理大型数据集时,一个好的权衡无疑是可取的。与因子分析一样,可以通过components_
实例变量检索组件。
有趣的是,可以通过组件实例数组explained_variance_ratio_
检查总解释方差(作为总输入方差的分数):
print(np.sum(pca.explained_variance_ratio_))
0.862522337381
使用 64 个组件,我们解释了总输入方差的 86%。当然,使用图表比较解释方差也是有用的:
如往常一样,前几个组件解释了最大的方差部分;然而,大约在第二十个组件之后,每个贡献都低于 1%(逐渐降低到大约 0%)。这种分析表明两个观察结果:可以通过接受可接受的损失进一步减少组件数量(使用前面的代码片段,很容易只扩展前n个组件并比较结果),同时,PCA 只能通过添加大量新组件来克服更高的阈值(例如 95%)。在这个特定案例中,我们知道数据集由手写数字组成;因此,我们可以假设尾部是由于次要差异(一条略长于平均的线,明显的笔触等);因此,我们可以没有问题地丢弃所有 n > 64(或更少)的组件(使用inverse_transform()
方法直观地验证重建的图像也很容易)。然而,在进行进一步处理步骤之前始终进行完整分析是最佳实践,尤其是在 X 的维度很高时。
Minka(自动选择 PCA 的维度,Minka T.P.,NIPS 2000)提出了另一种确定组件最佳数量的有趣方法,它基于贝叶斯模型选择。想法是使用最大似然估计(MLE)来优化似然度p(X|k),其中 k 是一个表示组件数量的参数。换句话说,它不是从分析解释方差开始的,而是确定一个k < n的值,使得似然度保持尽可能高(隐含地,k 将在max(k) = k[max]的约束下解释最大的可能方差)。该方法的理论基础(包括繁琐的数学推导)在前面提到的论文中已有介绍,然而,可以通过设置 Scikit-Learn 的n_components='mle'
和svd_solver='full'
参数来使用这种方法。
独立成分分析
我们已经看到,PCA 提取的因子是去相关的,但不是独立的。一个经典的例子是鸡尾酒会:我们有一段许多重叠声音的录音,我们想要将它们分开。每个声音都可以被建模为一个随机过程,并且可以假设它们在统计上是独立的(这意味着联合概率可以使用每个源的边缘概率来分解)。使用 FA 或 PCA,我们能够找到去相关的因子,但无法评估它们是否也是独立的(通常它们不是)。在本节中,我们将研究一个能够产生稀疏表示(当字典不是欠完备时)的模型,该模型具有一组统计上独立的成分。
假设我们有一个以零为中心并经过白化的数据集X,它是从N(0, I)采样的,并且是噪声无线的变换:
在这种情况下,对z的先验是建模为一个独立变量的乘积(α是归一化因子),每个变量都表示为一个通用的指数函数,其中函数fk必须是非二次的,即p(z; θ)不能是高斯分布。此外,我们假设z[i]的方差等于 1,因此p(x|z; θ) ∼ N(Az, AA^T)。联合概率p(X, z; θ) = p(X|z; θ)p(z|θ)等于以下:
如果X已经被白化,A是正交的(证明是直接的);因此,前面的表达式可以简化。然而,应用 EM 算法需要确定p(z|X; θ),这相当困难。在选择z的合适先验分布,即fk之后,这个过程可能会更容易,但正如我们在本章开头讨论的那样,如果真实因子分布不同,这个假设可能会有戏剧性的后果。因此,已经研究了其他策略。
我们需要强制执行的主要概念是因子的非高斯分布。特别是,我们希望有一个峰度分布(诱导稀疏性)和重尾。从理论我们知道,标准化的第四矩(也称为峰度)是一个完美的度量:
对于高斯分布,Kurt[X] 等于三(这通常被认为是参考点,确定所谓的 Excess Kurtosis = Kurtosis - 3),而对于称为 Leptokurtotic 或超高斯的一族分布,它们的峰值高且尾部重(此外,Kurt[X] < 3 的分布,称为 Platykurtotic 或亚高斯分布,也可以是好的候选者,但它们不太尖锐,通常只考虑超高斯分布)。然而,即使准确,这个度量由于四次方的原因对异常值非常敏感。例如,如果 x ∼ N(0, 1) 且 z = x + ν,其中 ν 是一个噪声项,它改变了一些样本的值,增加到两个,结果可以是一个超高斯分布(Kurt[x] > 3),即使过滤掉异常值后,分布的 Kurt[x] = 3 (高斯)。
为了克服这个问题,Hyvärinen 和 Oja(独立成分分析:算法与应用,Hyvarinen A.,Oja E.,神经网络 13/2000)提出了基于另一个度量,即 负熵 的解决方案。我们知道熵与方差成正比,给定方差,高斯分布具有最大的熵(对于更多信息,请参阅 信息论数学基础,Khinchin A. I.,Dover 出版公司);因此,我们可以定义这个度量:
形式上,X 的负熵是具有相同协方差的高斯分布的熵与 X 的熵(我们假设两者都是零中心的)之差。可以立即理解到 HN ≥ 0,因此最大化它的唯一方法是通过减少 H(X)。这样,X 就变得不那么随机,概率集中在均值周围(换句话说,它变成了超高斯分布)。然而,前面的表达式不能轻易地适应闭式解,因为 H(X) 需要计算 X 的整个分布,这必须被估计。因此,相同的作者提出了基于非二次函数的近似,这对于推导一个称为 FastICA 的固定点迭代算法是有用的(实际上,它比 EM 算法要快得多)。
使用 k 个函数 fk,近似变为如下:
在许多实际场景中,一个函数就足够达到合理的精度,对于 f(x) 的最常见选择如下:
在上述论文中,读者可以找到一些当这个函数无法强制组件之间统计独立时可以采用的替代方案。
如果我们逆转模型,我们得到 z = Wx,其中 W = A^(-1);因此,考虑单个样本,近似变为以下:
显然,第二项不依赖于 w(实际上,它只是一个参考)并且可以排除在优化之外。此外,考虑到初始假设,E[Z^TZ]=W E[X^TX] W^T = I,因此 WW^T = I,即 ||w||² = 1。因此,我们的目标是找到以下:
这样,我们正在迫使矩阵 W 将输入向量 x 转换,使得 z 具有最低可能的熵;因此,它是超高斯分布的。最大化过程基于超出本书范围的范围的凸优化技术(读者可以在 Luenberger D. G., Optimization by Vector Space Methods, Wiley 中找到拉格朗日定理的所有细节);因此,我们直接提供必须执行的迭代步骤:
当然,为了确保 ||w||²* = 1*,在每一步之后,权重向量 w 必须被归一化 (w[t+1] = w[t+1] / ||w[t+1]||)。
在更一般的情况下,矩阵 W 包含多个权重向量,如果我们应用之前的规则来找出独立因子,可能会发生某些元素,w[i]^Tx, 是相关的。避免这种问题的策略基于 gram-schmidt 正交化过程,该过程逐个去相关成分,从当前成分 (w[n]) 投影到所有之前的 (w[1], w[2], ..., w[n-1]) 上,以 w[n]。这样, w[n] 被迫与所有其他成分正交。
即使这种方法简单且不需要太多努力,但更倾向于一种全局方法,可以直接在迭代结束时与矩阵 W 一起工作(这样权重的顺序不是固定的)。如 快速且鲁棒的定点 中所述
独立成分分析算法,Hyvarinen A.,IEEE Transactions on Neural Networks 这个结果可以通过我们包含在最终 FastICA 算法中的简单子算法实现:
-
为 W[0] 设置随机初始值
-
设置一个阈值 Thr(例如 0.001)
-
独立成分提取
-
对于 W 中的每个 w:
-
当 ||w[t+1] - w[t]|| > Thr:
-
计算 w[t+1] = E[x · f'(w[t]Tx)] - E[f('')(w[t]Tx)] w[t]
-
w[t+1] = w[t+1] / ||w[t+1]||
-
-
-
正交化
-
当 ||W[t+1] - W[t]||[F] > Thr:
-
W[t] = W[t] / sqrt(||W[t]W[t]^T||)
-
W[t][+1] = (3/2)W[t] - (1/2)WW^TW
-
-
此过程也可以迭代固定次数,但最佳方法是同时使用阈值和最大迭代次数。
使用 Scikit-Learn 的快速 ICA 示例
使用相同的数据集,我们现在可以测试 ICA 的性能。然而,在这种情况下,如解释所述,我们需要将数据集归零中心化和白化,但幸运的是,这些预处理步骤由 Scikit-Learn 实现完成(如果省略参数whiten=True
)。
在 MNIST 数据集上执行 ICA,我们将实例化FastICA
类,传递参数n_components=64
和最大迭代次数max_iter=5000
。也可以指定用于逼近负熵的函数;然而,默认的是log cosh(x),这通常是一个不错的选择:
from sklearn.decomposition import FastICA
fastica = FastICA(n_components=64, max_iter=5000, random_state=1000)
fastica.fit(X)
到目前为止,我们可以可视化这些组件(这些组件始终可以通过components_
实例方差获得):
由 FastICA 算法提取的 MNIST 数据集的独立成分(64 个组件)
仍然存在一些冗余(读者可以尝试增加组件数量)和背景噪声;然而,现在可以区分一些低级特征(如定向条纹),这些特征在许多数字中是常见的。这种表示还不是非常稀疏。事实上,我们始终使用 64 个组件(如 FA 和 PCA);因此,字典是不完整的(输入维度是 28×28=784)。为了看到差异,我们可以将字典的大小增加到原来的十倍,设置n_components=640
:
fastica = FastICA(n_components=640, max_iter=5000, random_state=1000)
fastica.fit(Xs)
下面的截图显示了新组件的子集(100 个):
由 FastICA 算法提取的 MNIST 数据集的独立成分(640 个组件)
这些组件的结构几乎是基本的。它们代表定向条纹和位置点。为了检查输入是如何重建的,我们可以考虑混合矩阵A(它作为mixing_
实例变量可用)。考虑到第一个输入样本,我们可以检查有多少因素具有低于平均值的权重:
M = fastica.mixing_
M0 = M[0] / np.max(M[0])
print(len(M0[np.abs(M0) < (np.mean(np.abs(M0)) / 2.0)]))
233
样本使用大约 410 个组件重建。稀疏度更高,但考虑到因素的粒度,很容易理解许多因素是重建单个结构(如 1 的图像)所必需的,其中存在长线条。然而,这并不是缺点,因为,如前所述,ICA 的主要目标是提取独立成分。考虑到与鸡尾酒会例子的类比,我们可以推断每个组件代表一个音素,而不是单词或句子的完整声音。
读者可以测试不同的组件数量,并将结果与其他稀疏编码算法(如字典学习或稀疏 PCA)的结果进行比较。
HMM 的补充
在上一章中,我们讨论了如何使用前向-后向算法训练 HMM,并已看到它 EM 算法的一个特定应用。现在,读者可以以 E 步骤和 M 步骤的内部动态来理解。实际上,该过程从随机初始化 A 和 B 矩阵开始,交替进行:
-
E 步骤:
-
在给定观察和当前参数估计(A 和 B)的情况下,估计 HMM 在时间 t 处于状态i并在时间 t+1 处于状态j的概率α^t[ij]。
-
在给定观察和当前参数估计(A 和 B)的情况下,估计 HMM 在时间 t 处于状态i的概率β^t[i* ]。
-
-
M 步骤:
- 计算转换概率 a[ij] (A) 和发射概率 b[ip] (B) 的新估计
该过程会重复进行,直到达到收敛。即使没有 Q 函数的明确定义,E 步骤也会确定一个分割表达式,用于根据观察值给出模型的期望完整数据似然(使用前向和后向算法),而 M 步骤则校正参数 A 和 B 以最大化这个似然。
摘要
在本章中,我们介绍了 EM 算法,解释了其在许多统计学习场景中应用的理由。我们还讨论了隐藏(潜在)变量的基本作用,以便推导出一个更容易最大化的表达式(Q 函数)。
我们应用了 EM 算法来解决一个简单的参数估计问题,之后用来证明高斯混合估计公式。我们展示了如何使用 Scikit-Learn 的实现,而不是从头开始编写整个程序(如第二章,半监督学习简介)。
之后,我们分析了三种不同的成分提取方法。FA 假设我们有一个小数量的高斯潜在变量和一个高斯去相关噪声项。对噪声的唯一限制是具有对角协方差矩阵,因此可能有两种不同的场景。当我们面临异方差噪声时,这个过程就是一个实际的 FA。相反,当噪声是同方差时,算法变成了 PCA 的等价形式。在这种情况下,这个过程相当于检查样本空间以找到方差较高的方向。选择最重要的方向,我们可以将原始数据集投影到一个低维子空间,其中协方差矩阵变得去相关。
FA 和 PCA 的一个问题是它们假设用高斯分布来建模潜在变量。这个选择简化了模型,但同时也产生了密集的表示,其中单个成分在统计上是相互依赖的。因此,我们研究了如何强制因子分布变得稀疏。这个结果算法通常比 MLE 更快、更准确,被称为 FastICA,其目标是通过对负熵的近似最大化提取一组统计上独立的成分。
最后,我们简要解释了 HMM 前向-后向算法(在上一章中讨论过),考虑到将其分为 E 步和 M 步。其他 EM 特定应用将在下一章中讨论。
在下一章中,我们将介绍赫布学习法和自组织映射的基本概念,这些概念对于解决许多具体问题仍然非常有用,例如主成分提取,并且具有强大的神经生理学基础。
第六章:赫布学习和自组织映射
在本章中,我们将介绍基于心理学家唐纳德·赫布(Donald Hebb)定义的方法的赫布学习(Hebbian learning)概念。这些理论立即展示了一个非常简单的生物法则如何能够描述多个神经元在实现复杂目标时的行为,并且是连接人工智能和计算神经科学领域研究活动的开创性策略。
尤其是我们将要讨论以下主题:
-
单个神经元的赫布法则,这是一个简单但生物上合理的行性行为法则
-
一些引入的变体,用以克服一些稳定性问题
-
赫布神经元最终得到的结果,它包括计算输入数据集的第一个主成分
-
两种神经网络模型(Sanger 网络和 Rubner-Tavan 网络),可以提取一定数量的主成分
-
自组织映射(SOMs)的概念,重点关注 Kohonen 网络
赫布法则
赫布法则是由加拿大心理学家唐纳德·赫布(Donald Hebb)于 1949 年提出的一个猜想,用以描述自然神经元的突触可塑性。在其发表几年后,这一法则通过神经生理学研究得到了证实,许多研究已经表明它在人工智能领域的许多应用中是有效的。在介绍这一法则之前,描述一下以下图中所示的通用赫布神经元是有用的:
具有向量输入的通用赫布神经元
神经元是一个简单的计算单元,它接收来自突触前单元(其他神经元或感知系统)的输入向量x,并输出一个单一的标量值y。神经元的内部结构由一个权重向量w表示,它模拟了每个突触的强度。对于单个多维输入,输出如下:
在这个模型中,我们假设每个输入信号都编码在向量x的相应分量中;因此,x[i]由突触权重w[i,]处理,依此类推。在赫布理论的原始版本中,输入向量代表神经元的放电率,总是非负的。这意味着突触权重只能加强(神经科学中这种现象的术语是长期增强(LTP))。然而,为了我们的目的,我们假设x是一个实值向量,就像w一样。这个条件允许在不失去一般性的情况下模拟更多的人工场景。
当需要处理组织在矩阵中的多个输入样本时,对单个向量的相同操作仍然适用。如果我们有N个 m 维输入向量,公式如下:
赫布规则的基本形式在离散形式中可以表示如下(对于单个输入):
权重校正因此是一个与x具有相同方向且大小等于|x|乘以一个正参数η的向量,这个参数称为学习率,相应的输出y(可以是正或负)。Δw的方向由y的符号决定;因此,在假设x和y是实值的情况下,从这个规则中产生了两种不同的场景:
-
如果x[i] > 0 (< 0)且y > 0 (< 0),w[i]会加强
-
如果x[i] > 0 (< 0)且y < 0 (> 0),w[i]会减弱
考虑到二维向量,这个行为很容易理解:
因此,如果w和x之间的初始角度α小于 90°,w将具有与x相同的方向,反之亦然,如果α大于 90°。在下图中,有这个过程的示意图:
赫布规则的矢量分析
使用一个非常简单的 Python 代码片段可以模拟这种行为。让我们从一个场景开始,其中α小于 90°并且进行 50 次迭代:
import numpy as np
w = np.array([1.0, 0.2])
x = np.array([0.1, 0.5])
alpha = 0.0
for i in range(50):
y = np.dot(w, x.T)
w += x*y
alpha = np.arccos(np.dot(w, x.T) / (np.linalg.norm(w) * np.linalg.norm(x)))
print(w)
[ 8028.48942243 40137.64711215]
print(alpha * 180.0 / np.pi)
0.00131766983584
如预期,最终的角α接近零,w具有与x相同的方向和方向。现在我们可以用α大于 90°重复这个实验(我们只改变w的值,因为过程是相同的):
w = np.array([1.0, -1.0])
...
print(w)
[-16053.97884486 -80275.89422431]
print(alpha * 180.0 / np.pi)
179.999176456
在这种情况下,最终的角,α,大约是 180°,当然,w相对于x具有相反的方向。
科学家 S. Löwel 用著名的句子表达了这一概念:
“放电同步的神经元会连接在一起”
我们可以通过将其(适应到机器学习场景)重新表达这个概念,即这个方法的主要假设基于这样一个想法:当突触前和突触后单元协调一致(它们的信号具有相同的符号)时,神经元之间的连接会变得越来越强。另一方面,如果它们不一致,相应的突触权重会降低。为了精确起见,如果x是放电率,它应该表示为实函数x(t)以及y(t)。根据原始的赫布理论,离散方程必须被微分方程所取代:
如果x(t)和y(t)具有相同的放电率,突触权重将按比例加强,与两个速率的乘积成正比。如果突触前活动x(t)和突触后活动y(t)之间存在相对较长的延迟,相应的权重会减弱。这是对“一起放电→一起连接”关系的更符合生物学的解释。
然而,即使理论有很强的神经生理学基础,也需要进行一些修改。事实上,很容易理解,所得到的系统总是不稳定的。如果反复应用两个输入(包括实值和放电率),向量 w 的范数无限增长,这对生物系统来说不是一个合理的假设。事实上,如果我们考虑一个离散迭代步长,我们就有以下方程:
之前的输出 y[k,] 总是乘以一个大于 1 的因子(除了零输入的情况),因此它无界增长。由于 y = w · x,这个条件意味着在每个迭代中 w 的大小增加(如果 x 的大小为零,则保持不变)(一个更严格的证明可以通过考虑原始微分方程轻松获得)。
这种情况不仅在生物学上不可接受,而且在机器学习问题中,为了防止迭代几次后出现数值溢出,也必须对其进行适当的管理。在下一段中,我们将讨论一些克服这一问题的常用方法。目前,我们可以继续分析,而不引入校正因子。
让我们现在考虑一个数据集,X:
我们可以将该规则迭代应用于所有元素,但平均输入样本(现在索引指的是整个特定向量,而不是单个分量)的权重修改更容易(也更有用):
在前面的公式中,C 是输入相关矩阵:
然而,对于我们来说,考虑一个基于输入向量阈值 θ 的略有不同的赫布规则是有用的(也有一个生物学理由可以证明这一选择,但超出了本书的范围;感兴趣的读者可以在 Theoretical Neuroscience,Dayan P.,Abbott F. L.,The MIT Press 中找到它)。
很容易理解,在原始理论中,x(t) 和 y(t) 是放电率,这种修改允许一个与 LTP 相反的现象,称为长期抑制(LTD)。事实上,当 *x(t) < *θ 且 y(t) 为正时,乘积 (x(t) - θ)y(t) 为负,突触权重减弱。
如果我们设 θ = 〈x〉 ≈ E[X],我们可以推导出一个与之前非常相似的表达式,但基于输入协方差矩阵(通过贝塞尔校正无偏):
由于明显的原因,这种原始赫布规则的变体被称为协方差规则。
也可以使用最大似然估计(MLE)(或带偏置)的协方差矩阵(除以 N),但重要的是要检查所使用的数学包采用了哪个版本。当使用 NumPy 时,可以通过设置 np.cov()
函数的 bias=True/False
参数来决定版本(默认值为 False
)。然而,当 N >> 1 时,版本之间的差异减小,通常可以忽略不计。在这本书中,我们将使用无偏版本。想要了解贝塞尔校正更多细节的读者可以阅读 《应用统计学》,Warner R.,SAGE Publications。
协方差规则分析
协方差矩阵 Σ 是实对称的。如果我们应用特征分解,我们得到(对我们来说,保留 V^(-1) 而不是简化的版本 V^T 更有用):
V 是一个正交矩阵(由于 Σ 是对称的),包含 Σ 的特征向量(作为列),而 Ω 是一个对角矩阵,包含特征值。假设我们将特征值(λ[1],λ[2],...,λ[m]) 和相应的特征向量(v[1],v[2],...,v[m]) 排序,使得:
此外,假设 λ[1 ]在所有其他特征值中占主导地位(只要 λ[1] > λ[i],其中 i ≠ 1 就足够了)。由于特征向量是正交的,它们构成一个基,可以表示向量 w 为特征向量的线性组合:
向量 u 包含了在新基中的坐标。现在让我们考虑对协方差规则的修改:
如果我们迭代地应用这个规则,我们得到一个矩阵多项式:
利用二项式定理并考虑 Σ**⁰=I,我们可以得到 w^((k)) 作为 w^((0)) 的函数的一般表达式:
现在让我们使用基变换重写前面的公式:
向量 u^((0)) 包含了 w^((0)) 在新基中的坐标;因此,w^((k)) 可以表示为一个多项式,其中通项与 VΩiu((0)) 成正比。
现在让我们考虑对角矩阵 Ω^k:
最后一步来源于假设 λ[1] 大于任何其他特征值,并且当 k → ∞ 时,所有 λ[i≠1]^k<< λ[1]^k。当然,如果 λ[i][≠1] > 1,λ[i≠1]^(k )也会增长,但是当 k → ∞ 时,λ[1]^(k )对 w^((k)) 的贡献变得显著减弱。为了理解这个近似的有效性,让我们考虑以下情况,其中 λ[1] 略大于 λ[2]:
结果显示了一个非常重要的性质:不仅近似是正确的,而且正如我们将要展示的,如果一个特征值 λ[i] 大于所有其他特征值,协方差规则将始终收敛到相应的特征向量 v[i]。不存在其他稳定的固定点!
当 λ[1] = λ[2] = ... = λ[n] 时,这个假设就不再有效。在这种情况下,总方差由每个特征向量的方向等量解释(这是一个在现实场景中不常见的对称性条件)。这种情况也可能在处理有限精度算术时发生,但一般来说,如果最大特征值与第二个特征值之间的差异小于可达到的最大精度(例如,32 位浮点数),接受相等性是合理的。
当然,我们假设数据集没有被白化,因为我们的目标(也在下一段中)是只考虑具有最高总变异性的子集成分来减少原始维度(与主成分分析(PCA)一样,去相关必须是算法的结果,而不是先决条件)。另一方面,对数据集进行零中心化可能是有用的,尽管对于这类算法来说并非真正必要。
如果我们考虑这个近似重新写 w[k] 的表达式,我们得到以下结果:
由于 a[1]v + a[2]v + ... + a[k]v ∝ v,这个结果表明,当 k → ∞ 时,w[k] 将与协方差矩阵 Σ 的第一个特征向量成正比(如果 u[1]^((0)) 不为零),其大小在没有归一化的情况下将无限增长。由于其他特征值引起的虚假效应在有限次迭代后变得可以忽略(尤其是如果 w 除以其范数,使得长度始终为 ||w|| = 1)。
然而,在得出结论之前,必须添加一个重要条件:
事实上,如果 w(0) 与 v1 正交,我们将得到(特征向量彼此正交):
这个重要结果展示了如何使用协方差规则工作的赫布神经元能够执行仅限于第一个成分的 PCA,而无需对 Σ 进行特征分解。实际上,向量 w(我们不考虑大小增加的问题,这可以很容易地处理)将迅速收敛到输入数据集 X 变异性最高的方向。在第五章“EM 算法及其应用”中,我们讨论了 PCA 的细节;在下一段中,我们将讨论使用赫布规则的变体来找到前 N 个主成分的几种方法。
协方差规则应用示例
在继续之前,让我们用一个简单的 Python 示例来模拟这种行为。我们首先生成1000
个从双变量高斯分布中采样的值(方差是有意不对称的),然后应用协方差规则来找到第一个主成分(w^((0) )已被选择,以便不与v[1]正交):
import numpy as np
rs = np.random.RandomState(1000)
X = rs.normal(loc=1.0, scale=(20.0, 1.0), size=(1000, 2))
w = np.array([30.0, 3.0])
S = np.cov(X.T)
for i in range(10):
w += np.dot(S, w)
w /= np.linalg.norm(w)
w *= 50.0
print(np.round(w, 1))
[ 50\. -0.]
算法很简单,但有几个元素我们需要评论。第一个是每次迭代结束时向量 w 的归一化。这是避免 w 无控制增长所需的技术之一。第二个“棘手”的元素是最后的乘法,w • 50。因为我们乘以一个正标量,所以 w 的方向不受影响,但在完整的图中更容易展示向量。
结果在以下图中展示:
协方差规则的应用。w[∞]成为第一个主成分的比例
经过有限次数的迭代后,w[∞] 具有与主特征向量相同的方向,在这种情况下,与 x 轴平行。方向取决于初始值 w[0];然而,在主成分分析(PCA)中,这不是一个重要的元素。
权重向量稳定化和 Oja 规则
稳定权重向量的最简单方法是每次更新后对其进行归一化。这样,其长度将始终保持等于一。实际上,在这种类型的神经网络中,我们感兴趣的并不是大小,而是方向(归一化后保持不变)。然而,有两个主要原因使得这种方法不太可行:
-
它是非局部的。为了归一化向量 w,我们需要知道其所有值,这在生物学上是不可能的。一个真实的突触权重模型应该是自我限制的,不需要访问外部信息,而这些信息可能无法获得。
-
标准化必须在应用校正之后进行,因此需要双重迭代步骤。
在许多机器学习场景中,这些条件并不是限制性的,它们可以被自由采用,但当需要与神经科学模型一起工作时,最好寻找其他解决方案。在离散形式中,我们需要为标准的赫布规则确定一个校正项:
f 函数可以作为局部和非局部归一化器。第一种类型的一个例子是Oja 规则:
α 参数是一个控制归一化强度的正数。可以通过考虑以下条件获得该规则稳定性的非严格证明:
第二个表达式意味着:
因此,当t → ∞时,权重校正的幅度接近于零,权重向量w的长度将趋近于一个有限极限值:
Sanger 网络
Sanger 网络是由 T. D. Sanger 在《单层线性前馈神经网络中的最优无监督学习》一文中提出的在线主成分提取的神经网络模型,发表于Neural Networks,1989 年第 2 期。作者从 Hebb 规则的标准版本开始,修改它以能够按降序提取可变数量的主成分(v[1],v[2],...,v[m])(λ[1] > λ[2] > ... > λ[m])。这种结果方法,作为 Oja 规则的自然扩展,被称为广义 Hebbian 规则(GHA)(或学习)。网络的结构在以下图中表示:
网络使用从 n 维数据集中提取的样本进行喂养:
m个输出神经元通过权重矩阵W = {w[ij]}与输入连接,其中第一个索引指的是输入分量(突触单元),第二个索引指的是神经元。网络的输出可以通过标量积轻松计算;然而,在这种情况下,我们对此不感兴趣,因为就像协方差(和 Oja)规则一样,主成分是通过权重更新提取的。
Oja 规则提出后出现的问题涉及多个成分的提取。实际上,如果我们将原始规则应用于前面的网络,所有权重向量(w的行)都会收敛到第一个主成分。克服这种限制的主要思想(基于Gram-Schmidt正交化方法)基于以下观察:一旦我们提取了第一个成分w[1],第二个成分w[2]就可以被强制与w[1]正交,第三个成分w[3]可以被强制与w[1]和w[2]正交,依此类推。考虑以下表示:
两个权重向量的正交化
在这种情况下,我们假设w[1]是稳定的,w[2][0]是另一个权重向量,它正在收敛到w[1]。w[20]在w[1]上的投影如下:
在前面的公式中,如果我们不需要归一化,可以省略范数(在网络中,这个过程是在完成完整的权重更新后进行的)。w[20]的正交分量可以通过差分简单地获得:
将此方法应用于原始的 Oja 规则,我们得到权重更新的新表达式(称为 Sanger 规则):
该规则指的是单个输入向量 x,因此 x[j] 是 x 的 j^(th) 个分量。第一项是经典的赫布规则,它迫使权重 w 与第一个主成分平行,而第二项则以一种类似于格拉姆-施密特正交化的方式起作用,通过减去与先前突触后单元连接的所有权重在 w 上的投影成比例的项,同时考虑由奥贾规则提供的归一化约束(该约束与输出的平方成正比)。
事实上,展开最后一个项,我们得到以下结果:
从每个分量 w[ij] 中减去的项与所有分量成比例,其中索引 j 固定,第一个索引等于 1, 2, ..., i。此过程不会立即产生正交化,但需要多次迭代才能收敛。证明是非平凡的,涉及凸优化和动态系统方法,但可以在上述论文中找到。Sanger 表明,如果 学习率
η(t) 单调递减并随着 t → ∞ 趋向于零,则算法始终收敛到排序后的前 n 个主成分(从最大的特征值到最小的特征值)。即使对于形式证明是必要的,这个条件也可以放宽(通常稳定的 η < 1 足够)。在我们的实现中,矩阵 W 在每次迭代后都会进行归一化,因此,在过程结束时,W^T(权重在行中)是正交归一的,并构成特征向量子空间的一个基。
以矩阵形式,该规则如下:
Tril(•) 是一个矩阵函数,将它的参数转换为一个下三角矩阵,而项 yy^T 等于 Wxx^TW。
Sanger 网络的算法如下:
-
使用随机值初始化 W^((0))。如果输入维度是 n 且必须提取 m 个主成分,则形状将为 (m × n)。
-
设置一个
学习率
η(例如,0.01
)。 -
设置一个
阈值
Thr(例如,0.001
)。 -
设置一个计数器 T = 0。
-
当 ||W^((t)) - W^((t-1))||[F] > Thr 时:
-
设置 ΔW = 0(与 W 相同的形状)
-
对于 X 中的每个 x:
-
设置 T = T + 1
-
计算 y = W^((t))x
-
计算并累积 ΔW += η(yx^T - Tril(yyT)W((t))
-
-
更新 W^((t+1)) = W^((t)) + (η / T)ΔW
-
设置 W^((t+1)) = W^((t+1))* / ||W((t+1))||((rows))*(必须按行计算范数)
-
该算法也可以迭代固定次数(如我们的示例所示),或者可以将两种停止方法一起使用。
Sanger 网络的示例
在这个 Python 示例中,我们考虑一个二维零中心的样本集 X
,包含 500 个样本(我们使用第一章中定义的函数)。在初始化 X
之后,我们还计算特征分解,以便能够双重检查结果:
import numpy as np
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=500, centers=2, cluster_std=5.0, random_state=1000)
Xs = zero_center(X)
Q = np.cov(Xs.T)
eigu, eigv = np.linalg.eig(Q)
print(eigu)
[ 24.5106037 48.99234467]
print(eigv)
[[-0.75750566 -0.6528286 ]
[ 0.6528286 -0.75750566]]
n_components = 2
W_sanger = np.random.normal(scale=0.5, size=(n_components, Xs.shape[1]))
W_sanger /= np.linalg.norm(W_sanger, axis=1).reshape((n_components, 1))
特征值是逆序的;因此,我们预计最终的 W 将有行被交换。以下图表显示了初始条件(权重乘以 15):
具有初始条件 W 的数据集,我们可以实现该算法。为了简化,我们更喜欢使用固定的迭代次数(5000
)和 learning_rate
为 η=0.01。读者可以修改代码片段,以便在权重矩阵变得稳定时停止:
learning_rate = 0.01
nb_iterations = 5000
t = 0.0
for i in range(nb_iterations):
dw = np.zeros((n_components, Xs.shape[1]))
t += 1.0
for j in range(Xs.shape[0]):
Ysj = np.dot(W_sanger, Xs[j]).reshape((n_components, 1))
QYd = np.tril(np.dot(Ysj, Ysj.T))
dw += np.dot(Ysj, Xs[j].reshape((1, X.shape[1]))) - np.dot(QYd, W_sanger)
W_sanger += (learning_rate / t) * dw
W_sanger /= np.linalg.norm(W_sanger, axis=1).reshape((n_components, 1))
首先要检查的是 W 的最终状态(我们转置了矩阵以便比较列):
print(W_sanger.T)
[[-0.6528286 -0.75750566]
[-0.75750566 0.6528286 ]]
如预期,W 已经收敛到输入相关矩阵的特征向量(与 w— 相关的符号 *–*
并不重要,因为我们只关心方向)。第二个特征值是最高的,因此列被交换。重新绘制图表,我们得到以下结果:
最终状态,w 已经收敛到两个主成分
这两个成分是完美正交的(最终的取向可以根据初始条件或随机状态而改变),w[0] 指向第一个主成分的方向,而 w[1] 指向第二个成分的方向。考虑到这个良好的性质,我们不需要检查特征值的幅度;因此,这个算法可以在不分解输入协方差矩阵的情况下运行。即使需要正式的证明来解释这种行为,也可以直观地理解它。每个神经元都会收敛到给定的完整特征向量子空间中的第一个主成分。这个性质始终得到保持,但在正交化之后,子空间隐式地减少了一个维度。第二个神经元将始终收敛到第一个成分,现在它对应于全局第二个成分,依此类推。
这个算法(以及下一个算法)的一个优点是,标准的 PCA 通常是一个批量过程(即使有批量算法),而 Sanger 的网络是一个在线算法,可以逐步训练。一般来说,Sanger 的网络的时间性能比直接方法差,因为迭代(可以通过更多的矢量化或 GPU 支持实现一些优化)。另一方面,当成分的数量小于输入维度时,Sanger 的网络可以节省内存(例如,对于 n=1000 的协方差矩阵有 10⁶ 个元素,如果 m = 100,则权重矩阵有 10⁴ 个元素)。
Rubner-Tavan 的网络
在第五章,“EM 算法及其应用”中,我们提到任何去相关输入协方差矩阵的算法都在执行 PCA 而不进行降维。从这个方法出发,Rubner 和 Tavan(在论文“A Self-Organizing Network for Principal-Components Analysis”中,Rubner J.,Tavan P.,Europhysics. Letters,10(7),1989)提出了一种神经网络模型,其目标是去相关输出分量,以强制输出协方差矩阵(在低维子空间)的后续去相关。假设数据集零中心化且E[y] = 0,m个主成分的输出协方差矩阵如下:
因此,可以实现近似的去相关,迫使i ≠ j的y[i]y[j]项接近于零。与标准方法(如白化或 vanilla PCA)的主要区别在于,此过程是局部的,而所有标准方法都是全局的,直接与协方差矩阵操作。作者提出的神经网络模型如下所示(原始模型是为二进制单元提出的,但它对线性单元也相当有效):
Rubner-Tavan 网络。连接 v[jk]基于反 Hebbian 规则
该网络有m个输出单元,最后m-1个神经元有一个求和节点,该节点接收前一个单元的加权输出(分层横向连接)。其动态过程很简单:第一个输出不被修改。第二个输出被迫与第一个输出去相关。第三个输出被迫与第一个和第二个输出都去相关,依此类推。由于输入是一个接一个地呈现的,并且出现在相关/协方差矩阵中的累积项(总是更容易将数据集零中心化并使用相关矩阵)必须隐式地分成其加数,因此必须迭代多次。不难理解,收敛到唯一的稳定固定点(作者已经证明其存在)需要一些迭代来纠正错误的输出估计。
网络的输出由两个贡献组成:
符号y/x^((i))表示y/x的第i个元素。第一个项仅基于输入产生部分输出,而第二个项使用分层横向连接来纠正值并强制去相关。内部权重w[ij]使用 Oja 规则的标准版本进行更新(这主要是每个权重向量收敛到第一个主成分的原因):
相反,外部权重v[jk]使用反 Hebbian 规则进行更新:
前一个公式可以分为两部分:第一项 -ηy[j]y[k] 与标准版的赫布规则(这就是为什么它被称为反赫布)方向相反,并强制去相关。第二项 -ηy[j]y[k]v[jk] 起到正则化作用,类似于奥贾规则。项 -ηy[j]y[k] 作为奥贾规则的反馈信号,根据实际输出的新幅度重新调整更新。实际上,在修改侧向连接后,输出也被迫改变,这种修改会影响 w[ij] 的更新。当所有输出都去相关后,向量 w[i] 隐含地必须正交。可以想象一个与格拉姆-施密特正交化的类比,即使在这种情况下,不同分量的提取与去相关的关联更为复杂。就像桑格网络一样,该模型按降序提取前 m 个主成分(原因与之前直观解释的相同),但对于一个完整的(非平凡)数学证明,请参阅上述论文。
如果输入维度为 n 且分量数等于 m,则可以使用所有对角元素设置为 0 的下三角矩阵 V (m × m) 和标准矩阵 W (n × m)。
W 的结构如下:
因此, w[i] 是一个必须收敛到相应特征向量的列向量。V 的结构如下:
使用这种表示法,输出结果如下:
由于输出基于循环侧向连接,其值必须通过迭代前一个公式固定次数或直到连续两个值之间的范数小于预定义的阈值来稳定。在我们的例子中,我们使用固定次数等于五的迭代。更新规则不能直接用矩阵表示法写出,但可以使用向量 w**i 和 v**j :
在这种情况下, y^((i)) 表示 y 的 i^(th) 个分量。这两个矩阵必须通过循环来填充。
完整的 Rubner-Tavan 网络算法如下(x 的维度为 n,分量数用 m 表示):
-
随机初始化 W^((0))。其形状为 (n × m)。
-
随机初始化 V^((0))。其形状为 (m × m)。
-
设置 V^((0))= Tril(V^((0)))。Tril(•) 将输入参数转换为下三角矩阵。
-
将 V^((0)) 的所有对角元素设置为 0。
-
设置
学习率
η(例如,0.001
)。 -
设置一个
阈值
Thr(例如,0.0001
)。 -
设置周期计数器 T=0。
-
设置最大迭代次数
max_iterations
(例如,1000)。 -
设置一个
稳定周期数
stabilization_cycles(例如,5
):-
当 ||W^((t)) - W^((t-1))||[F] > Thr 且 T <
max_iterations
时:-
将 T = T + 1 设置。
-
对于 X 中的每个 x:
-
将 y[prev] 设置为零。形状为 (m, 1)。
-
对于 i=1 到
stabilization_cycles
:-
y = W^Tx + Vy[prev]。
-
y[prev] = y。
-
-
计算 W 和 V 的更新:
-
创建两个空矩阵 ΔW (n × m) 和 ΔV (m × m)
-
对于 t=1 到 m:
-
Δw[t] = ηy^((t))(x - y^((t))w[t])。
-
Δv[t] = -ηy^((t))(y + y^((t))v[t])。
-
-
更新 W 和 V:
-
W^((t+1)) = W^((t)) + ΔW。
-
V^((t+1)) = V^((t)) + ΔV。
-
-
将 V = Tril(V) 并将所有对角线元素设置为 0。
-
将 W^((t+1)) = W^((t+1))* / ||W((t+1))||((columns))*(规范必须按列计算)
-
-
-
-
在这种情况下,我们采用了阈值和最大迭代次数,因为该算法通常收敛得非常快。此外,我建议读者在执行点积时始终检查向量和矩阵的形状。
在本例中,以及所有其他例子中,NumPy 随机种子被设置为 1000
(np.random.seed(1000)
)。使用不同的值(或重复更多次实验而不重置种子)可能会导致略微不同的结果(这些结果总是连贯的)。
Rubner-Tavan 网络的示例
对于我们的 Python 示例,我们将使用为 Sanger 的网络已创建的相同数据集(预计将在变量 Xs
中可用)。因此,我们可以开始设置所有常数和变量:
import numpy as np
n_components = 2
learning_rate = 0.0001
max_iterations = 1000
stabilization_cycles = 5
threshold = 0.00001
W = np.random.normal(0.0, 0.5, size=(Xs.shape[1], n_components))
V = np.tril(np.random.normal(0.0, 0.01, size=(n_components, n_components)))
np.fill_diagonal(V, 0.0)
prev_W = np.zeros((Xs.shape[1], n_components))
t = 0
在这一点上,可以实施训练循环:
while(np.linalg.norm(W - prev_W, ord='fro') > threshold and t < max_iterations):
prev_W = W.copy()
t += 1
for i in range(Xs.shape[0]):
y_p = np.zeros((n_components, 1))
xi = np.expand_dims(Xs[i], 1)
y = None
for _ in range(stabilization_cycles):
y = np.dot(W.T, xi) + np.dot(V, y_p)
y_p = y.copy()
dW = np.zeros((Xs.shape[1], n_components))
dV = np.zeros((n_components, n_components))
for t in range(n_components):
y2 = np.power(y[t], 2)
dW[:, t] = np.squeeze((y[t] * xi) + (y2 * np.expand_dims(W[:, t], 1)))
dV[t, :] = -np.squeeze((y[t] * y) + (y2 * np.expand_dims(V[t, :], 1)))
W += (learning_rate * dW)
V += (learning_rate * dV)
V = np.tril(V)
np.fill_diagonal(V, 0.0)
W /= np.linalg.norm(W, axis=0).reshape((1, n_components))
最终的 W
和输出协方差矩阵如下:
print(W)
[[-0.65992841 0.75897537]
[-0.75132849 -0.65111933]]
Y_comp = np.zeros((Xs.shape[0], n_components))
for i in range(Xs.shape[0]):
y_p = np.zeros((n_components, 1))
xi = np.expand_dims(Xs[i], 1)
for _ in range(stabilization_cycles):
Y_comp[i] = np.squeeze(np.dot(W.T, xi) + np.dot(V.T, y_p))
y_p = y.copy()
print(np.cov(Y_comp.T))
[[ 48.9901765 -0.34109965]
[ -0.34109965 24.51072811]]
如预期的那样,该算法已成功收敛到特征向量(按降序排列),并且输出协方差矩阵几乎完全去相关(非对角元素的符号可以是正也可以是负)。Rubner-Tavan 的网络通常比 Sanger 的网络更快,这得益于由反 Hebbian 规则产生的反馈信号;然而,选择正确的学习率值非常重要。一种可能的策略是从不超过 0.0001
的值开始实现时间衰减(如 Sanger 的网络中所做的那样)。然而,当 n 增加时(例如,η = 0.0001 / n),重要的是要减少 η,因为 Oja 规则在侧向连接 v[jk] 上的归一化强度通常不足以避免当 n >> 1 时的溢出和下溢。我不建议对 V(必须仔细分析,因为 V 是奇异的)进行任何额外的归一化,因为这可能会减慢过程并降低最终精度。
自组织映射
自组织映射(SOMs)是由威尔肖和冯·德·马尔堡(Willshaw D. J., Von Der Malsburg C., How patterned neural connections can be set up by self-organization, Proceedings of the Royal Society of London, B/194, N. 1117)提出的,用于模拟动物中观察到的不同神经生物学现象。特别是,他们发现大脑的一些区域发展出具有不同区域的结构,每个区域对特定输入模式都有高度敏感性。这种行为的背后过程与我们迄今为止讨论的内容截然不同,因为它基于被称为胜者全得原则的神经单元之间的竞争。在训练期间,所有单元都受到相同信号的刺激,但只有一个会产生最高的响应。这个单元将自动成为该特定模式的感受野。我们将要介绍的特定模型是由科霍恩(在论文Kohonen T., Self-organized formation of topologically correct feature maps, Biological Cybernetics, 43/1*)提出的,并以他的名字命名。
主要思想是实现一个逐渐的胜者全得范式,以避免神经元(作为最终胜者)过早收敛,并增加网络的塑性水平。这个概念在以下图表中得到了图形化表达(我们考虑的是神经元的一个线性序列):
科霍恩网络实现的墨西哥帽动态
在这种情况下,相同的模式被呈现给所有神经元。在训练过程的开始(t=0),观察到x[i-2]到x[i+2]之间的正响应,在x[i]处达到峰值。显然,潜在的胜者是x[i],但所有这些单元都会根据它们与x[i]的距离被增强。换句话说,这个网络(按顺序训练)仍然对变化保持敏感,如果其他模式产生更强的激活。如果相反x[i]继续是胜者,半径会略微减小,直到只有增强的单元将是x[i]。考虑到这个函数的形状,这种动态通常被称为墨西哥帽。通过这种方法,网络保持可塑性,直到所有模式都被反复呈现。例如,如果另一个模式在x[i]处引起更强的响应,那么它的激活仍然不要太高,以便网络能够快速重新配置。同时,新的胜者很可能是x[i]的邻居,它已经接受了部分增强,可以轻易地取代x[i]。
Kohonen SOM(也称为 Kohonen 网络或简称 Kohonen 图)通常表示为二维图(例如,一个m × m的方阵,或任何其他矩形形状),但三维表面,如球体或环面也是可能的(唯一必要的条件是存在一个合适的度量)。在我们的案例中,我们总是指一个方阵,其中每个细胞是一个具有输入模式维度的突触权重w的接收神经元:
在训练和工作阶段,获胜单元是根据样本与每个权重向量之间的相似度来确定的。最常用的度量是欧几里得;因此,如果我们考虑一个形状为(k × p)的二维图W,使得W ∈ ℜ^(k × p × n),那么获胜单元(就其坐标而言)的计算如下:
如前所述,避免过早收敛是很重要的,因为完整的最终配置可能与初始配置大不相同。因此,训练过程通常分为两个不同的阶段。在第一个阶段,其持续时间通常是总迭代次数的 10-20%(让我们称这个值为t[max]),对获胜单元及其邻居(通过采用衰减半径来计算)进行校正。相反,在第二个阶段,半径设置为 1.0,校正仅应用于获胜单元。这样,就有可能分析更多的可能配置,自动选择与最小误差相关的一个。邻域可以有不同的形状;它可以方形(在封闭的 3D 图中,边界不再存在),或者,更简单的是,可以采用基于指数衰减距离加权的径向基函数:
每个神经元的相对权重由σ(t)决定。σ[0]函数是初始半径,τ是一个时间常数,必须将其视为超参数,它决定了衰减权重的斜率。合适的值是总迭代次数的 5-10%。采用径向基函数,不需要计算实际的邻域,因为乘数n(i, j)在边界之外接近于零。一个缺点是与计算成本相关,它比方形邻域高(因为必须为整个图计算函数);然而,可以通过预计算所有平方距离(分子)并利用 NumPy(如 NumPy)提供的矢量化功能来加速这个过程:
更新规则非常简单,它基于将获胜单元的突触权重移动到模式x[i,](在整个数据集X上重复)这一想法:
η(t) 函数是学习率,可以是固定的,但最好是开始时使用一个较高的值,η[0],然后让它衰减到目标最终值,η[∞]:
这样,初始变化迫使权重与输入模式对齐,而所有后续更新允许轻微的修改以改进整体准确性。因此,每次更新都与学习率、邻域加权距离以及每个模式与突触向量之间的差异成比例。理论上,如果获胜单元的 Δw[ij] 等于 0.0,这意味着一个神经元已经成为了特定输入模式的吸引子,其邻居将对接收到的噪声/修改版本敏感。最有趣的是,完整的最终图将包含所有模式的吸引子,这些吸引子被组织起来以最大化相邻单元之间的相似性。这样,当呈现新的模式时,映射最相似形状的神经元区域将显示出更高的响应。例如,如果模式由手写数字组成,数字 1 和数字 7 的吸引子将比数字 8 的吸引子更接近。一个形状不规则的 1(可能被解释为 7)将引发介于前两个吸引子之间的响应,使我们能够根据距离分配相对概率。正如我们将在示例中看到的那样,这个特性使得同一模式类别的不同变体之间有一个平滑的过渡,避免了强制二元决策的刚性边界(如在 K-means 聚类或硬分类器中)。
完整的 Kohonen SOM 算法如下:
-
随机初始化 W^((0))。其形状是 (k × p × n)。
-
初始化
nb_iterations
(总迭代次数)和 t[max](例如,nb_iterations
= 1000 和 t[max] = 150)。 -
初始化 τ(例如,τ = 100)。
-
初始化 η[0] 和 η[∞](例如,η[0] = 1.0 和 η[∞] = 0.05)。
-
对于
t = 0
到nb_iterations
:-
如果 t < t[max]:
-
计算 η(t)
-
计算 σ(t)
-
-
否则:
-
设置 η(t) = η[∞]
-
设置 σ(t) = σ[∞]
-
-
对于 X 中的每个 x[i]:
-
计算获胜单元 u^(假设坐标是 i,j)
-
计算 n(i, j)
-
将权重校正 Δw[ij]^((t))应用于所有突触权重 W^((t))。
-
-
重新归一化 W^((t))= W^((t)) / ||W((t)*)||((columns))(规范必须按列计算)
-
SOM 示例
我们现在可以使用 Olivetti 人脸数据集实现一个 SOM。由于这个过程可能非常长,在这个例子中我们限制输入模式的数量为 100(使用 5 × 5 矩阵)。读者可以尝试使用整个数据集和更大的地图。
第一步是加载数据,对其进行归一化,使得所有值都在 0.0 和 1.0 之间,并设置常数:
import numpy as np
from sklearn.datasets import fetch_olivetti_faces
faces = fetch_olivetti_faces(shuffle=True)
Xcomplete = faces['data'].astype(np.float64) / np.max(faces['data'])
np.random.shuffle(Xcomplete)
nb_iterations = 5000
nb_startup_iterations = 500
pattern_length = 64 * 64
pattern_width = pattern_height = 64
eta0 = 1.0
sigma0 = 3.0
tau = 100.0
X = Xcomplete[0:100]
matrix_side = 5
在这一点上,我们可以使用具有小标准差的正态分布来初始化权重矩阵:
W = np.random.normal(0, 0.1, size=(matrix_side, matrix_side, pattern_length))
现在,我们需要定义函数来确定基于最小距离的获胜单元:
def winning_unit(xt):
distances = np.linalg.norm(W - xt, ord=2, axis=2)
max_activation_unit = np.argmax(distances)
return int(np.floor(max_activation_unit / matrix_side)), max_activation_unit % matrix_side
定义函数 η(t) 和 σ(t) 也是有用的:
def eta(t):
return eta0 * np.exp(-float(t) / tau)
def sigma(t):
return float(sigma0) * np.exp(-float(t) / tau)
如前所述,与其为每个单元计算径向基函数,不如使用预先计算的包含所有可能距离的单元对的距离矩阵(在这种情况下,5 × 5 × 5 × 5)。这样,NumPy 通过其向量化功能允许更快地计算:
precomputed_distances = np.zeros((matrix_side, matrix_side, matrix_side, matrix_side))
for i in range(matrix_side):
for j in range(matrix_side):
for k in range(matrix_side):
for t in range(matrix_side):
precomputed_distances[i, j, k, t] = \
np.power(float(i) - float(k), 2) + np.power(float(j) - float(t), 2)
def distance_matrix(xt, yt, sigmat):
dm = precomputed_distances[xt, yt, :, :]
de = 2.0 * np.power(sigmat, 2)
return np.exp(-dm / de)
distance_matrix
函数返回整个映射中径向基函数的值,给定中心点(获胜单元) xt, yt
和 σ 的当前值 sigmat
。现在,可以开始训练过程(为了避免相关性,最好在每个迭代的开始时对输入序列进行洗牌):
sequence = np.arange(0, X.shape[0])
t = 0
for e in range(nb_iterations):
np.random.shuffle(sequence)
t += 1
if e < nb_startup_iterations:
etat = eta(t)
sigmat = sigma(t)
else:
etat = 0.2
sigmat = 1.0
for n in sequence:
x_sample = X[n]
xw, yw = winning_unit(x_sample)
dm = distance_matrix(xw, yw, sigmat)
dW = etat * np.expand_dims(dm, axis=2) * (x_sample - W)
W += dW
W /= np.linalg.norm(W, axis=2).reshape((matrix_side, matrix_side, 1))
在这种情况下,我们将 η[∞] 设置为 0.2
,但我邀请读者尝试不同的值并评估最终结果。经过 5000
个周期的训练后,我们得到了以下权重矩阵(每个权重都绘制为二维数组):
如所见,权重已经收敛到具有略微不同特征的脸上。特别是,观察脸的形状和表情,很容易注意到不同吸引子之间的过渡(有些脸在笑,而有些则更严肃;有些人戴眼镜,有胡须和胡须,等等)。还重要的是要考虑矩阵的容量大于最小容量(数据集中有十个不同的个体)。这允许映射更多不能轻易被正确神经元吸引的图案。例如,一个人可以有带胡须和不带胡须的照片,这可能会导致混淆。如果矩阵太小,可能会观察到收敛过程中的不稳定性,而如果太大,则很容易看到冗余。正确的选择取决于每个不同的数据集以及内部方差,而且无法定义一个标准标准。一个好的起点是选择一个容量是所需吸引子数量的 2.0 到 3.0 倍以上的矩阵,然后增加或减少其大小,直到准确性达到最大。最后要考虑的是标记阶段。在训练过程的最后,我们对获胜神经元的权重分布一无所知,因此有必要处理数据集并为每个模式标记获胜单元。这样,就可以提交新的模式以获得最可能的标签。这个过程尚未展示,但它很简单,读者可以轻松地为每种不同场景实现它。
摘要
在本章中,我们讨论了赫布规则,展示了它是如何驱动输入数据集的第一主成分的计算的。我们还看到,这个规则是不稳定的,因为它会导致突触权重的无限增长,以及如何通过归一化或奥贾规则来解决这个问题。
我们介绍了两种基于赫布学习(桑格的和鲁布纳-塔万网络)的不同神经网络,它们的内部动力学略有不同,能够按正确顺序(从最大的特征值开始)提取前n个主成分,而无需对输入协方差矩阵进行特征值分解。
最后,我们介绍了 SOM 的概念,并介绍了一个称为 kohonen 网络的模型,该模型能够通过竞争学习过程将输入模式映射到一个表面上,在该表面上放置了一些吸引子(每个类别一个)。这样的模型能够通过在吸引子中引发与模式最相似的强烈反应来识别新的模式(属于同一分布)。通过这种方式,在标记过程之后,该模型可以作为软分类器使用,可以轻松地处理噪声或改变的模式。
在下一章中,我们将讨论一些重要的聚类算法,重点关注硬聚类和软聚类之间的差异(已在上一章中讨论),并讨论评估算法性能的主要技术。
第七章:聚类算法
在本章中,我们将介绍一些基本的聚类算法,讨论它们的优点和缺点。无监督学习领域,以及任何其他机器学习方法,都必须始终基于奥卡姆剃刀的概念。当性能满足要求时,必须始终优先考虑简单性。然而,在这种情况下,真实情况可能是未知的。当采用聚类算法作为探索性工具时,我们只能假设数据集代表一个精确的数据生成过程。如果这个假设是正确的,最佳策略是确定簇的数量以最大化内部凝聚力(密度)和外部分离。这意味着我们期望找到具有一些共同和部分独特特征的团块(或岛屿)。
尤其是我们要介绍的一些算法是:
-
基于 KD 树和球树的 k-最近邻算法(KNN)
-
K-means 和 K-means++
-
模糊 C 均值
-
基于 Shi-Malik 算法的谱聚类
k-最近邻
此算法属于一个称为基于实例的特定家族(该方法称为基于实例学习)。它与其他方法的不同之处在于它不使用实际的数学模型。相反,推理是通过直接比较新样本与现有样本(定义为实例)来进行的。KNN 是一种可以轻松应用于解决聚类、分类和回归问题的方法(即使在这种情况下,我们只考虑第一种技术)。聚类算法背后的主要思想非常简单。让我们考虑一个数据生成过程p[data]和从这个分布中抽取的有限数据集:
每个样本具有等于N的维度。我们现在可以引入一个距离函数*d(x[1], x[2])**,在大多数情况下,它可以被 Minkowski 距离泛化:
当p = 2时,d[p]代表经典的欧几里得距离,这通常是默认选择。在特定情况下,使用其他变体可能是有用的,例如p = 1(这是曼哈顿距离)或p > 2。即使度量函数的所有属性保持不变,不同的p值会产生语义上不同的结果。例如,我们可以考虑点x[1] = (0, 0)和x[2] = (15, 10)作为p的函数的距离:
(0, 0)和(15, 10)之间的 Minkowski 距离作为参数 p 的函数
距离随着 p 的增加单调递减,并收敛到最大成分绝对差,|x[1]^((j)) - x[2]^((j))|,当 p → ∞ 时。因此,为了保持一致的距离度量,在所有成分上施加相同的权重非常重要,因此较小的 p 值更可取(例如,p=1 或 2)。这一结果已经被 Aggarwal、Hinneburg 和 Keim 在 On the Surprising Behavior of Distance Metrics in High Dimensional Space(Aggarwal C. C.,Hinneburg A.,Keim D. A.,ICDT 2001)一文中研究并形式化,他们证明了基本的不等式。如果我们考虑一个由 M 个点 x[i] ∈ (0, 1)^d 组成的通用分布 G,一个基于 L[p] 范数的距离函数,以及两个点 x[j] 和 x[k] 之间的最大 D[max]^p 和最小 D[min]^p 距离(使用 L[p] 范数计算),这两个点从 G 和 (0, 0) 中抽取,以下不等式成立:
很明显,当输入维度非常高且 p >> 2 时,期望值 E[D[max]^p - D[min]^p] 介于两个常数 k[1] (C[p]d^(1/p-1/2)) 和 k[2] ((M-1)C[p]d^(1/p-1/2)) → 0 之间,这几乎消除了任何距离的实际影响。实际上,给定从 G 中抽取的两个通用点对 (x[1], x[2]) 和 (x[3], x[4]),以下不等式的自然结果是,当 p → ∞ 时,dp ≈ dp,而不论它们的相对位置如何。这一重要结果证实了根据数据集的维度选择正确度量的重要性,并且当 d >> 1 时,p = 1* 是最佳选择,而 p >> 1 由于度量的无效性可能会产生不一致的结果。为了直接证实这一现象,可以运行以下代码片段,该片段计算了包含从均匀分布 G ∼ U(0, 1) 中抽取的 100
个样本的 100
个集合之间最大和最小距离的平均差异。在该片段中,分析了 d=2
、100
、1000
的情况,使用 Minkowski 度量,P
分别为 1
、2
、10
、100
(最终值取决于随机种子和实验重复的次数):
import numpy as np
from scipy.spatial.distance import pdist
nb_samples = 100
nb_bins = 100
def max_min_mean(p=1.0, d=2):
Xs = np.random.uniform(0.0, 1.0, size=(nb_bins, nb_samples, d))
pd_max = np.zeros(shape=(nb_bins, ))
pd_min = np.zeros(shape=(nb_bins, ))
for i in range(nb_bins):
pd = pdist(Xs[i], metric='minkowski', p=p)
pd_max[i] = np.max(pd)
pd_min[i] = np.min(pd)
return np.mean(pd_max - pd_min)
print('P=1 -> {}'.format(max_min_mean(p=1.0)))
print('P=2 -> {}'.format(max_min_mean(p=2.0)))
print('P=10 -> {}'.format(max_min_mean(p=10.0)))
print('P=100 -> {}'.format(max_min_mean(p=100.0)))
P=1 -> 1.79302317381
P=2 -> 1.27290283592
P=10 -> 0.989257369005
P=100 -> 0.983016242436
print('P=1 -> {}'.format(max_min_mean(p=1.0, d=100)))
print('P=2 -> {}'.format(max_min_mean(p=2.0, d=100)))
print('P=10 -> {}'.format(max_min_mean(p=10.0, d=100)))
print('P=100 -> {}'.format(max_min_mean(p=100.0, d=100)))
P=1 -> 17.1916057948
P=2 -> 1.76155714836
P=10 -> 0.340453945928
P=100 -> 0.288625281313
print('P=1 -> {}'.format(max_min_mean(p=1.0, d=1000)))
print('P=2 -> {}'.format(max_min_mean(p=2.0, d=1000)))
print('P=10 -> {}'.format(max_min_mean(p=10.0, d=1000)))
print('P=100 -> {}'.format(max_min_mean(p=100.0, d=1000)))
P=1 -> 55.2865105705
P=2 -> 1.77098913218
P=10 -> 0.130444336657
P=100 -> 0.0925427145923
一个特殊情况,即前述不等式的直接结果是,当组件之间最大的绝对差异决定了距离的最重要因素时,可以采用大的 p 值。例如,如果我们考虑三个点,x[1] = (0, 0),x[2] = (15, 10),和 x[3] = (15, 0),则 d2 ≈ 18 和 d2 = 15。因此,如果我们设置一个以 x[1] 为中心的阈值 d = 16,则 x[2] 在边界之外。如果 p = 15,两个距离都接近 15,两个点 (x[2] 和 x[3]) 在边界内。当需要考虑组件之间的不均匀性时,大 p 值的特定用途。例如,一些特征向量可以表示一组人的年龄和身高。考虑一个测试人员 x = (30, 175),在大的 p 值下,x 与两个样本 (35, 150) 和 (25, 151) 之间的距离几乎相同(大约 25.0),唯一的决定性因素成为身高差异(独立于年龄)。
KNN 算法确定每个训练点的 k 个最近样本。当呈现新样本时,会重复执行此过程,有两种可能的变体:
-
在预定义的值 k 下,计算 KNN
-
在预定义的半径/阈值 r 下,计算所有距离小于或等于半径的邻居
KNN 的哲学是相似的样本可以共享其特征。例如,一个推荐系统可以使用此算法对用户进行聚类,并针对新用户找到最相似的用户(例如,基于他们购买的产品)以推荐相同类别的物品。一般来说,相似度函数定义为距离的倒数(有一些例外,如余弦相似度):
两位不同的用户,A 和 B,被分类为邻居,在某些观点上会有所不同,但与此同时,他们也会共享一些独特的特征。这个陈述使我们能够通过 建议差异 来增加同质性。例如,如果 A 喜欢书籍 b[1] 而 B 喜欢书籍 b[2],我们可以向 B 推荐 b[1],向 A 推荐 b[2]。如果我们假设是正确的,那么 A 和 B 之间的相似性将会增加;否则,这两个用户将向其他更好地代表他们行为的聚类移动。
不幸的是,vanilla 算法(在 Scikit-Learn 中称为暴力算法)在样本数量较多时可能会变得非常慢,因为它需要计算所有成对距离以回答任何查询。当有 M 个点时,这个数字等于 M²,这通常是不可以接受的(如果 M = 1,000,每个查询都需要计算一百万个距离)。更精确地说,在一个 N 维空间中计算距离需要 N 次操作,总复杂度变为 O(M²N),这只有在 M 和 N 都很小的情况下才是合理的。这就是为什么已经实施了一些重要的策略来降低计算复杂度。
KD 树
由于所有 KNN 查询都可以被视为搜索问题,降低整体复杂度最有效的方法之一是将数据集重新组织成树结构。在一个二叉树(一维数据)中,查询的平均计算复杂度为 O(log M),因为我们假设每个分支中几乎有相同数量的元素(如果树完全不平衡,所有元素都是顺序插入的,并且结果结构只有一个分支,因此复杂度变为 O(M))。一般来说,实际的复杂度略高于 O(log M),但操作总是比普通的搜索更有效,普通搜索的复杂度为 O(M²)。
然而,我们通常处理 N 维数据,并且之前的结构不能立即应用。KD 树扩展了二叉树的概念,用于 N > 1。在这种情况下,不能立即进行分割,必须选择不同的策略。解决这个问题的最简单方法是,在每一层 (1, 2, ..., N) 选择一个特征,并重复这个过程,直到达到所需的深度。在下面的图中,有一个三维点的 KD 树示例:
三维 KD 树的示例
根节点是点 (5, 3, 7)。第一次分割是考虑第一个特征,因此有两个子节点 (2, 1, 1) 和 (8, 4, 3)。第二次分割操作在第二个特征上,以此类推。平均计算复杂度为 O(N log M),但如果分布非常不对称,树变得不平衡的概率非常高。为了减轻这个问题,可以选择对应于(子)数据集中位数的特征,并以此标准继续分割。这样,树可以保证是平衡的。然而,平均复杂度总是与维数成正比,这可能会严重影响性能。
例如,如果M = 10,000 且N = 10,使用log[10],则O(N log M) = O(40),而,当N = 1,000 时,复杂度变为O(40,000)。通常,KD 树受到维数灾难的影响,当N变得很大时,平均复杂度约为O(MN),这总是比普通算法好,但通常对于实际应用来说太昂贵。因此,KD 树只有在维度不是太高的情况下才真正有效。在所有其他情况下,不平衡树的概率和由此产生的计算复杂度表明应采用不同的方法。
球树
球树提供了 KD 树的替代方案。其思路是将数据集重新排列,使其对高维样本几乎不敏感。球被定义为从中心样本到其距离小于或等于固定半径的点集:
从第一个主要球开始,可以构建嵌套在父球中的更小的球,并在达到所需的深度时停止这个过程。一个基本条件是,一个点始终只能属于一个球。这样,考虑到 N 维距离的成本,计算复杂度为O(N log M),并且不像 KD 树那样受到维数灾难的影响。该结构基于超球面,其边界由以下方程定义(给定中心点x和半径R[i]):
因此,找到正确球所需的唯一操作是从最小的球开始测量样本与中心之间的距离。如果一个点在球外,则需要向上移动并检查父节点,直到找到包含样本的球。在以下图中,有一个具有两个层次的球树示例:
具有七个二维点和两个层次的球树示例
在这个例子中,七个二维点首先被分成包含三个和四个点的两个球。在第二层,第二个球再次被分成包含两个点的两个更小的球。这个过程可以重复进行,直到达到固定的深度或通过规定叶子必须包含的最大元素数量(在这种情况下,它可以等于3)。
KD 树和球树都可以是有效的结构,用于减少 KNN 查询的复杂性。然而,在拟合模型时,重要的是要考虑k参数(通常表示查询中计算的邻居的平均数或标准数)和最大树深度。这些特定的结构不用于常见任务(如排序),并且当所有请求的邻居都可以在同一个子结构中找到时(大小K<< M,以避免隐式回退到vanilla算法)它们的效率最大化。换句话说,树通过将其划分为合理的小区域来减少搜索空间的维度。
同时,如果一个叶子节点中包含的样本数量较少,树节点的数量会增长,从而复杂性随之增加。负面影响加倍,因为平均来说需要探索更多的节点,如果k远大于节点中包含的元素数量,则需要合并属于不同节点的样本。另一方面,每个节点包含的样本数量非常多,会导致接近vanilla算法的状态。例如,如果M = 1,000,并且每个节点包含 250 个元素,一旦计算出了正确的节点,需要计算的距离数量与初始数据集的大小相当,采用树结构并没有真正获得优势。一种可接受的做法是将叶子的大小设置为平均k值的 5 到 10 倍,以最大化找到所有邻居都在同一叶子中的概率。然而,为了找到最合适的值,必须分析(同时基准测试性能)每个具体问题。如果需要不同的k值,重要的是考虑查询的相对频率。例如,如果一个程序需要 10 个5-NN查询和 1 个50-NN查询,可能最好将叶子大小设置为 25,即使50-NN查询会更昂贵。实际上,为第二个查询(例如,200)设置一个良好的值将大大增加前 10 个查询的复杂性,导致性能损失。
Scikit-Learn 中 KNN 的示例
为了测试 KNN 算法,我们将使用 Scikit-Learn 直接提供的 MNIST 手写数字数据集。它由 1,797 个 8×8 灰度图像组成,代表从 0 到 9 的数字。第一步是加载它并将所有值归一化,使其介于 0 和 1 之间:
import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
X_train = digits['data'] / np.max(digits['data'])
字典 digits
包含图像 digits['images']
和展平的 64 维数组 digits['data']
。Scikit-Learn 实现了不同的类(例如,可以使用 KDTree 和 BallTree 类直接与 KD 树和 Ball 树一起工作),这些类可以在 KNN 的上下文中使用(作为聚类、分类和回归算法)。然而,我们将使用主要的类 NearestNeighbors
,它允许基于邻居的数量或以样本为中心的球体的半径进行聚类和查询:
from sklearn.neighbors import NearestNeighbors
knn = NearestNeighbors(n_neighbors=50, algorithm='ball_tree')
knn.fit(X_train)
我们选择默认的邻居数量为 50
,以及基于 ball_tree
的算法。叶子大小(leaf_size
)参数已被保留为其默认值 30
。我们还使用了默认的度量标准(欧几里得),但可以通过 metric
和 p
参数(这是 Minkowski 度量标准的阶数)来更改它。Scikit-Learn 支持 SciPy 在 scipy.spatial.distance
包中实现的全部度量标准。然而,在大多数情况下,使用 Minkowski 度量标准并调整 p
的值就足够了,如果任何数量的邻居的结果不可接受。其他度量标准,如余弦距离,可以在相似性不能受欧几里得距离影响,而只能由指向样本的两个向量之间的角度影响时使用。使用此度量标准的应用包括,例如,用于自然语言处理的深度学习模型,其中单词被嵌入到特征向量中,其语义相似性与它们的余弦距离成正比。
我们现在可以查询模型以找到样本的 50 个邻居。为了我们的目的,我们选择了索引为 100 的样本,它代表数字 4(图像分辨率非常低,但总是可以区分数字):
用于查询 KNN 模型的样本数字
查询可以通过实例方法 kneighbors
来执行,该方法允许指定邻居的数量(n_neighbors
参数,默认值是在类实例化时选择的值)以及我们是否想要获取每个邻居的距离(return_distance
参数)。在这个例子中,我们还对评估邻居与中心的距离有多远感兴趣,因此我们将 return_distance=True
设置为:
distances, neighbors = knn.kneighbors(X_train[100].reshape(1, -1), return_distance=True)
print(distances[0])
[ 0\. 0.91215747 1.16926793 1.22633855 1.24058958 1.32139841
1.3564084 1.36645069 1.41972709 1.43341812 1.45236875 1.50130152
1.52709897 1.5499496 1.62379763 1.62620148 1.6345871 1.64292993
1.66770801 1.70934929 1.71619128 1.71619128 1.72187216 1.73317808
1.74888357 1.75445861 1.75668367 1.75779514 1.76555586 1.77878118
1.788636 1.79408751 1.79626348 1.80169191 1.80277564 1.80385871
1.80494113 1.8125 1.81572988 1.83498978 1.84771819 1.87291551
1.87916205 1.88020112 1.88538789 1.88745861 1.88952706 1.90906554
1.91213232 1.92333532]
第一个邻居总是中心,因此其距离为 0
。其他邻居的距离从 0.9 到 1.9。考虑到在这种情况下,可能的最大距离是 8(在 64 维向量 a = (1, 1, ..., 1) 和零向量之间),结果可能是可接受的。为了得到确认,我们可以将邻居作为二维 8 × 8 数组绘制出来(返回的数组 neighbors
包含样本的索引)。结果如下截图所示:
KNN 模型选择的 50 个邻居
如所见,没有错误,但所有形状都略有不同。特别是最后一个,也是最远的,有很多白色像素(对应于值 1.0),解释了距离约为 2.0 的原因。我邀请读者测试radius_neighbors
方法,直到结果中出现虚假值。尝试使用 Olivetti 人脸数据集运行此算法也很有趣,其复杂性更高,许多更多的几何参数可以影响相似性。
K-means
当我们讨论高斯混合算法时,我们将其定义为软 K-means。原因是每个簇由三个元素表示:均值、方差和权重。每个样本总是以高斯分布提供的概率属于所有簇。当可能管理概率作为权重时,这种方法非常有用,但在许多其他情况下,确定每个样本的单个簇更可取。这种方法被称为硬聚类,K-means 可以被认为是高斯混合的硬版本。实际上,当所有方差Σ[i]→ 0时,分布退化为 Dirac 的δ函数,它们代表以特定点为中心的完美尖峰。在这种情况下,确定最合适的簇的唯一可能性是找到样本点与所有中心(从现在起,我们将它们称为质心)之间的最短距离。这种方法也基于一个重要的双重原则,应该在每个聚类算法中加以考虑。簇必须设置以最大化:
-
簇内内聚性
-
簇间分离
这意味着我们期望标记彼此之间距离较远的密集区域。当这不可能实现时,标准必须尝试最小化样本和质心之间的簇内平均距离。这个量也称为惯性,其定义为:
高惯性意味着低内聚性,因为可能存在太多属于簇的点,而这些簇的质心距离太远。可以通过最小化前面的量来解决此问题。然而,找到全局最小值所需的计算复杂度是指数级的(K-means 属于 NP-Hard 问题类别)。K-means 算法采用的替代方法,也称为Lloyd 算法,是迭代的,并从选择k个随机质心开始(在下一节中,我们将分析一种更有效的方法),并调整它们直到它们的配置变得稳定。
要聚类的数据集(包含M个样本)表示为:
质心的初始猜测为:
初始值没有特定的限制。然而,选择可以影响收敛速度和找到的最小值。迭代过程将遍历数据集,计算 x[i ]和每个 μ[j] 之间的欧几里得距离,并根据以下标准分配簇:
一旦所有样本都被聚类,新的质心将被计算:
量 N[Cj] 表示属于聚类 j 的点的数量。在此点,重新计算惯性并与前一个值比较。该过程将在达到固定迭代次数或惯性变化小于预定义阈值后停止。Lloyd 算法与 EM 算法的一个特例非常相似。实际上,每个迭代的第一个步骤是计算一个 期望(质心配置),而第二个步骤通过最小化惯性来最大化簇内凝聚力。
完整的 vanilla K-means 算法是:
-
设置最大迭代次数 N[max]。
-
设置一个容差 Thr。
-
设置 k(期望的聚类数量)的值。
-
使用随机值初始化向量 C^((0))。它们可以是数据集中的点或从合适的分布中采样。
-
计算初始惯性 S^((0))
-
设置 N = 0。
-
当 N < N[max] 或 ||S^((t)) - S^((t-1))|| > Thr 时:
-
N = N + 1
-
对于 X 中的 x[i]:
- 使用 x[i] 和 μ[j] 之间的最短距离将 x[i] 分配到簇
-
重新计算质心向量 C^((t))
-
重新计算惯性 S^((t))
-
该算法相当简单直观,并且基于它的现实生活应用有很多。然而,有两个重要元素需要考虑。第一个是收敛速度。很容易证明每个初始猜测都会驱动到一个收敛点,但迭代次数会受到这个选择的影响,并且无法保证找到全局最小值。如果初始质心接近最终质心,算法只需要几步就能纠正值,但如果选择完全随机,则可能需要非常高的迭代次数。如果有 N 个样本和 k 个质心,则必须在每次迭代中计算 Nk 个距离,导致结果效率低下。在下一段中,我们将展示如何初始化质心来最小化收敛时间。
另一个重要方面是,与 KNN 不同,K-means 需要预先定义期望的簇数量。在某些情况下,这是一个次要问题,因为我们已经知道 k 的最合适值。然而,当数据集是高维的且我们的知识有限时,这个选择可能会很危险。解决这个问题的好方法是对不同数量的簇分析最终的惯性。我们期望最大化簇内凝聚力,簇的数量较少会导致惯性增加。我们试图选择低于最大可容忍值的最高点。理论上,我们也可以选择 k = N。在这种情况下,惯性变为零,因为每个点代表其簇的质心,但 k 的一个较大值将聚类场景转换为细粒度划分,这可能不是捕捉一致群体特征的最好策略。无法为上界 k[max] 定义一个规则,但我们假设这个值总是远小于 N。最佳选择是通过选择 k 来最小化惯性,从例如介于 2 和 k[max] 之间的集合中选择值。
K-means++
我们已经说过,一个好的初始质心选择可以改善收敛速度,并使最小值更接近惯性 S 的全局最优解。Arthur 和 Vassilvitskii(在 *The Advantages of Careful Seeding, *Arthur, D., Vassilvitskii S., k-means++: Proceedings of the Eighteenth Annual ACM-SIAM Symposium on Discrete Algorithms)提出了一种称为 K-means++ 的方法,该方法允许通过考虑最可能的最终配置来提高初始质心猜测的准确性。
为了展示算法,引入一个函数,D(x, i),定义为:
D(x, i) 定义了每个样本与已选中的一个质心之间的最短距离。由于这个过程是增量式的,这个函数必须在所有步骤之后重新计算。为了我们的目的,我们还可以定义一个辅助概率分布(为了简单起见,我们省略了索引变量):
第一个质心 μ[0] 是使用均匀分布从 X 中采样的。接下来的步骤是:
-
对于所有 x ∈ X,考虑已选中的质心计算 D(x, i)
-
计算 G(x)
-
从 X 中以概率 G(x) 选择下一个质心 μ[i]
在上述论文中,作者展示了一个非常重要的性质。如果我们定义 S^* 为 S 的全局最优解,K-means++ 初始化确定了实际惯性期望值的上界:
这种条件通常通过说 K-means++是O(log k)-竞争性的来表示。当k足够小的时候,找到接近全局最小值的局部最小值的概率增加。然而,K-means++仍然是一种概率方法,在相同的数据集上不同的初始化会导致不同的初始配置。一个好的做法是运行有限数量的初始化(例如,十个)并选择与最小惯性相关联的那个。当训练复杂性不是主要问题时,这个数字可以增加,但不同的实验表明,与实际的计算成本相比,一个非常大的试验数量所能实现的改进是可以忽略不计的。Scikit-Learn 中的默认值是十个,作者建议在大多数情况下保持这个值。如果结果仍然很差,最好选择另一种方法。此外,有些问题无法使用 K-means(即使是使用最佳可能的初始化)来解决,因为算法的一个假设是每个群集都是一个超球体,距离是通过欧几里得函数来测量的。在接下来的章节中,我们将分析其他不受此类限制的算法,这些算法可以轻松地使用非对称群集几何来解决聚类问题。
使用 Scikit-Learn 的 K-means 示例
在这个例子中,我们继续使用 MNIST 数据集(X_train
数组与 KNN 段落中定义的相同),但我们还想分析不同的聚类评估方法。第一步是可视化不同群集数量对应的惯性。我们将使用KMeans
类,它接受n_clusters
参数,并使用 K-means++初始化作为默认方法(如前所述,为了找到最佳初始配置,Scikit-Learn 执行多次尝试并选择具有最低惯性的配置;可以通过n_iter
参数更改尝试次数):
import numpy as np
from sklearn.cluster import KMeans
min_nb_clusters = 2
max_nb_clusters = 20
inertias = np.zeros(shape=(max_nb_clusters - min_nb_clusters + 1,))
for i in range(min_nb_clusters, max_nb_clusters + 1):
km = KMeans(n_clusters=i, random_state=1000)
km.fit(X_train)
inertias[i - min_nb_clusters] = km.inertia_
我们假设要分析的范围是[2
, 20
]。在每次训练会话之后,可以使用inertia_
实例变量检索最终的惯性。以下图表显示了值作为群集数量的函数的绘制:
群集数量的惯性函数
如预期的那样,函数是递减的,从大约 7,500 的值开始,达到大约 3,700,20个簇。在这种情况下,我们知道实际数字是10,但可以通过观察趋势来发现它。在10之前,斜率相当高,但超过这个阈值后开始越来越慢地下降。这是一个信号,告诉我们一些簇没有很好地分离,即使它们的内部凝聚力很高。为了证实这个假设,我们可以将n_clusters
设置为10,首先检查训练过程结束时的重心:
km = KMeans(n_clusters=10, random_state=1000)
Y = km.fit_predict(X_train)
重心可以通过cluster_centers_
实例变量获取。在下面的截图中,有一个对应的双维数组的图示:
K-means 重心位于训练过程结束时
所有数字都存在,没有重复。这证实了算法已经成功分离了集合,但最终的惯性(大约为 4,500)告诉我们可能存在错误分配。为了获得确认,我们可以使用如 t-SNE(见第三章,基于图的无监督学习以获取更多详细信息)这样的降维方法绘制数据集:
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, perplexity=20.0, random_state=1000)
X_tsne = tsne.fit_transform(X_train)
在这一点上,我们可以绘制带有相应簇标签的双维数据集:
MNIST 数据集的 t-SNE 表示;标签对应于簇
该图证实数据集由分离良好的团块组成,但有一些样本被分配到了错误的簇(考虑到一些数字对之间的相似性,这是不奇怪的)。一个重要的观察可以进一步解释惯性趋势。事实上,斜率几乎突然变化的点对应于 9 个簇。观察 t-SNE 图,我们可以立即发现原因:对应数字7的簇确实被分成了 3 块。主要的一块包含大多数样本,但有另外 2 个较小的团块错误地附着在簇1和9上。考虑到数字7可以非常类似于扭曲的1或9,这是不奇怪的。然而,这两个虚假的团块总是位于错误簇的边界上(记住几何结构是超球体),这证实了度量已经成功检测到低相似度。如果一组错误分配的样本位于簇的中间,那就意味着分离失败得很严重,应该采用另一种方法。
评估指标
在许多情况下,仅通过视觉检查无法评估聚类算法的性能。此外,使用标准目标指标来比较不同方法非常重要。我们现在将介绍一些基于地面真实知识(每个样本的正确分配)的方法,以及当真实标签未知时采用的一种常见策略。
在讨论评分函数之前,我们需要介绍一种标准符号。如果有 k 个簇,我们定义真实标签为:
同样,我们可以定义预测标签:
这两个集合都可以被视为来自两个离散随机变量的样本(为了简单起见,我们用相同的名称表示它们),其概率质量函数为 Ptrue 和 Ppred,其中 y ∈ {y[1], y[2], ..., y[k]} (y[i] 代表第 i 个簇的索引)。这两个概率可以用频率计数来近似;例如,概率 Ptrue 是指真实标签为 1 的样本数除以总样本数 M。这样,我们可以定义熵:
这些量描述了随机变量的内在不确定性。当所有类别的概率相同时,它们达到最大值,例如,如果所有样本都属于单个类别(最小不确定性),则它们为零。我们还需要知道随机变量 Y 在给定另一个随机变量 X 时的不确定性。这可以通过条件熵 H(Y|X) 来实现。在这种情况下,我们需要计算联合概率 p(x, y),因为 H(Y|X) 的定义是:
为了近似前面的表达式,我们可以定义函数 n(i[true], j[pred]),它计算将真实标签 i 分配给簇 j 的样本数量。这样,如果有 M 个样本,近似条件熵变为:
同质性得分
这个得分有助于检查聚类算法是否满足一个重要要求:一个簇应只包含属于单个类别的样本。它被定义为:
它的值介于 0 和 1 之间,低值表示同质性低。事实上,当 Y[pred] 的知识减少了 Y[true] 的不确定性时,H(Y[true]|Y[pred]) 变得较小 (h → 1),反之亦然。对于我们的例子,同质性得分可以计算如下:
from sklearn.metrics import homogeneity_score
print(homogeneity_score(digits['target'], Y))
0.739148799605
digits['target']
数组包含真实标签,而 Y
包含预测(我们将要使用的所有函数都接受真实标签作为第一个参数,预测作为第二个参数)。同质性得分证实簇相对同质,但仍然存在一定程度的不确定性,因为一些簇包含错误的分配。这种方法,连同其他方法,可以用来寻找正确的簇数并调整所有辅助超参数(如迭代次数或度量函数)。
完整性得分
这个得分是前一个得分的补充。它的目的是提供关于属于同一类的样本分配的一些信息。更确切地说,一个好的聚类算法应该将所有具有相同真实标签的样本分配到同一个簇中。从我们之前的分析中,我们知道,例如,数字 7 被错误地分配到了簇 9 和簇 1 中;因此,我们预计完整性得分不会完美。定义是对称的,与同质性得分相同:
理由非常直观。当 *H(Y[pred]|Y[true]**) 低 (c → 1) 时,这意味着关于基真的知识减少了预测的不确定性。因此,如果我们知道子集 A 中的所有样本都具有相同的标签 y[i],我们相当确信所有相应的预测都被分配到了同一个簇中。我们示例的完整性得分为:
from sklearn.metrics import completeness_score
print(completeness_score(digits['target'], Y))
0.747718831945
再次,这个值证实了我们的假设。剩余的不确定性是由于不完整性造成的,因为一些具有相同标签的样本被分割成块,并被分配到错误的簇中。很明显,一个完美的场景是同质性和完整性得分都等于 1。
调整后的兰德指数
这个得分可以用来比较原始标签分布与聚类预测。理想情况下,我们希望重现精确的基真分布,但在现实生活中,这通常是非常困难的。调整后的兰德指数提供了一种衡量差异的方法。为了计算这个得分,我们需要定义辅助变量:
-
a:具有相同真实标签且被分配到同一簇的样本对数 (y[i], y[j])*
-
b:具有不同真实标签且被分配到不同簇的样本对数 (y[i], y[j])*
兰德指数定义为:
调整后的兰德指数是修正了偶然性的兰德指数,其定义为:
R[A]度量介于-1和1之间。一个接近-1的值表明错误分配占主导地位,而一个接近1的值表明聚类算法正确地再现了真实情况的分布。我们示例的调整后兰德指数如下:
from sklearn.metrics import adjusted_rand_score
print(adjusted_rand_score(digits['target'], Y))
0.666766395716
这个值证实了算法运行良好(因为它为正值),但可以通过尝试减少错误分配的数量来进一步优化。当真实情况已知时,调整后兰德指数是一个非常强大的工具,可以作为单一方法来优化所有超参数。
轮廓得分
这个度量不需要知道真实情况,可以同时用来检查簇内凝聚力和簇间分离。为了定义轮廓得分,我们需要引入两个辅助函数。第一个是样本x[i]属于簇C[j]的平均簇内距离:
在前面的表达式中,n(k)是分配到簇C[j]的样本数量,d(a, b)是一个标准距离函数(在大多数情况下,选择欧几里得距离)。我们还需要定义最低的簇间距离,这可以解释为平均最近簇距离。在样本x[i] ∈ C[j]中,我们称C[t]为最近的簇;因此,该函数定义为:
样本x[i]的轮廓得分是:
s(x[i])的值,类似于调整后的兰德指数,介于-1和1之间。一个接近-1的值表明b(x[i]) << a(x[i]),因此平均簇内距离大于平均最近簇索引,样本x[i]被错误分配。反之,一个接近1的值表明算法达到了非常好的内部凝聚力和簇间分离水平(因为a(x[i]) << b(x[i])*)。与其它度量不同,轮廓得分不是一个累积函数,必须为每个样本单独计算。一种可行的策略是分析平均值,但这样无法确定哪些簇对结果影响最大。另一种方法(最常见的方法),是基于轮廓图,它按降序显示每个簇的得分。在下面的代码片段中,我们为四个不同的n_clusters
值(3
、5
、10
、12
)创建图表:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples
fig, ax = plt.subplots(2, 2, figsize=(15, 10))
nb_clusters = [3, 5, 10, 12]
mapping = [(0, 0), (0, 1), (1, 0), (1, 1)]
for i, n in enumerate(nb_clusters):
km = KMeans(n_clusters=n, random_state=1000)
Y = km.fit_predict(X_train)
silhouette_values = silhouette_samples(X_train, Y)
ax[mapping[i]].set_xticks([-0.15, 0.0, 0.25, 0.5, 0.75, 1.0])
ax[mapping[i]].set_yticks([])
ax[mapping[i]].set_title('%d clusters' % n)
ax[mapping[i]].set_xlim([-0.15, 1])
ax[mapping[i]].grid()
y_lower = 20
for t in range(n):
ct_values = silhouette_values[Y == t]
ct_values.sort()
y_upper = y_lower + ct_values.shape[0]
color = cm.Accent(float(t) / n)
ax[mapping[i]].fill_betweenx(np.arange(y_lower, y_upper), 0, ct_values, facecolor=color, edgecolor=color)
y_lower = y_upper + 20
结果如下所示:
不同簇数的轮廓图
分析轮廓图应遵循一些常见的指导原则:
-
每个块的宽度必须与预期属于相应聚类的样本数量成比例。如果标签分布均匀,所有块必须具有相似的宽度。任何不对称性都表明有错误的分配。例如,在我们的案例中,我们知道正确的聚类数量是十个,但有一两个块比其他块要薄。这意味着一个聚类包含的样本比预期的要少,其余的样本被分配到了错误的分区。
-
块的形状不应该尖锐和峰值(如刀片),因为这意味着许多样本具有较低的轮廓分数。理想的(现实的)场景是由类似雪茄的形状组成,最高值和最低值之间的差异最小。不幸的是,这并不总是可能实现,但如果形状类似于第一张图中绘制的形状(三个聚类),则始终 preferable 调整算法。
-
最大轮廓分数应该接近1。较低的值(如我们的例子所示)表明存在部分重叠和错误的分配。必须绝对避免(或限制到非常少的样本)负值,因为它们表明聚类过程失败。此外,可以证明凸聚类(如 K-means 超球体)会导致更高的值。这是由于常用的距离函数(如欧几里得距离)的性质,当聚类形状是凹的时(想想一个圆和一个半月形),它可以暗示低内部凝聚力。在这种情况下,将形状嵌入到凸几何形状中的过程会导致较低的密度,这会负面影响轮廓分数。
在我们特定的案例中,我们不能接受有与十个不同的聚类数量。然而,相应的轮廓图并不完美。我们知道这种不完美的原因(样本的结构和不同数字之间的高度相似性)并且使用像 K-means 这样的算法很难避免它们。读者可以尝试通过增加迭代次数来提高性能,但如果结果不符合要求,最好采用另一种方法(如谱聚类方法,它可以处理非对称聚类和更复杂的几何形状)。
模糊 C 均值
我们已经讨论了硬聚类和软聚类的区别,比较了 K-means 和高斯混合模型。解决这个问题的另一种方法是基于模糊逻辑的概念,该概念首次由 Lotfi Zadeh 于 1965 年提出(更多细节,请参考《模糊集导论》,作者:Pedrycz W.,Gomide F.,出版社:麻省理工学院出版社)。经典逻辑集基于排中律,在聚类场景中可以表述为样本 x[i] 只能属于单个聚类 c[j]。更普遍地说,如果我们将我们的宇宙划分为标记分区,硬聚类方法将为每个样本分配一个标签,而模糊(或软)方法允许管理一个成员度(在高斯混合中,这是一个实际的概率),w[ij],它表达了样本 x[i] 与聚类 c[j] 之间关系的强度。与其他方法不同,通过采用模糊逻辑,可以定义不对称的集合,这些集合不能用连续函数(如梯形)表示。这允许实现更大的灵活性,并提高了适应更复杂几何形状的能力。在下面的图中,有一个模糊集的示例:
根据工作经验表示员工资历级别的模糊集示例
该图表示了员工的资历水平,根据其工作经验。由于我们希望将整个群体聚类成三个组(初级、中级和高级),已经设计了三个模糊集。我们假设年轻员工热情,在一段初步实习期后可以迅速达到初级水平。与复杂问题工作的可能性使他/她能够发展出允许在初级和中级之间过渡的基本技能。大约 10 年后,员工可以开始考虑自己为高级学徒,大约在 25 年后,经验足够使他/她有资格成为直到其职业生涯结束的高级。由于这是一个假设的例子,我们没有调整所有值,但很容易比较,例如,有 9 年工作经验的员工 A 与有 18 年工作经验的员工 B。前者大约是 50% 的初级(减少),90% 的中级(达到顶峰),10% 的高级(增加)。而后者,相反,是 0% 的初级(结束平台期),30% 的中级(减少),60% 的高级(增加)。在两种情况下,值都没有归一化,所以总是加起来等于 1,因为我们更感兴趣的是展示过程和比例。在极端情况下,模糊度较低,而当两个集合相交时,模糊度变得更高。例如,在大约 15% 时,中级和高级大约是 50%。正如我们将要讨论的,在聚类数据集时避免非常高的模糊度是有用的,因为它可能导致边界模糊化,变得完全模糊。
模糊 C-均值是标准 K-means 的一种推广,具有软分配和更 灵活 的簇。要聚类的数据集(包含 M 个样本)表示为:
如果我们假设我们有 k 个簇,那么有必要定义一个包含每个样本隶属度的矩阵 W ∈ ℜ^(M × k):
每个度数 w[ij] ∈ [0, 1] 以及所有行都必须归一化,以确保它们总是加起来等于 1。这样,隶属度可以被视为概率(具有相同的语义),并且使用预测结果进行决策更容易。如果需要硬分配,可以采用与高斯混合通常使用的方法相同的途径:通过应用 argmax 函数选择获胜的簇。然而,当可能管理向量输出时,仅使用软聚类是一种好的做法。例如,可以将概率/隶属度输入到分类器中,以产生更复杂的预测。
与 K-means 类似,问题可以表达为最小化一个 广义惯性:
常数m (m > 1)是一个用于重新加权隶属度的指数。一个接近1的值不会影响实际值。更大的m值会减小它们的幅度。相同的参数也用于重新计算质心和新的隶属度,并可能导致不同的聚类结果。定义一个全局可接受的值相当困难;因此,一个好的做法是从一个平均m(例如,1.5)开始,并进行网格搜索(可以从高斯或均匀分布中进行采样),直到达到所需的精度。
最小化前面的表达式甚至比使用标准惯性还要困难;因此,采用了一种伪 Lloyd 算法。在随机初始化之后,算法交替进行两个步骤(类似于 EM 过程),以确定质心,并重新计算隶属度以最大化内部凝聚力。质心是通过加权平均确定的:
与 K-means 不同,总和不仅限于属于特定聚类的点,因为权重因子将迫使最远的点(w[ij] ≈ 0.0)产生接近0的贡献。同时,由于这是一个软聚类算法,没有强制排除,允许样本以不同的隶属度属于任意数量的聚类。一旦重新计算了质心,必须使用此公式更新隶属度:
这个函数的行为类似于相似度。事实上,当样本x[i]非常接近质心μ[j](并且相对于μ[p]与p ≠ j相对较远时),分母变得很小,w[ij]增加。指数m直接影响模糊划分,因为当m ≈ 1(m > 1)时,分母是quasi-平方项的总和,最近的质心可以主导总和,从而对特定聚类产生更高的偏好。当m >> 1时,总和中的所有项都趋向于1,产生一个更平坦的权重分布,没有明确的偏好。重要的是要理解,即使在软聚类中工作,模糊性过度也会导致不准确的决策,因为没有因素推动样本明确属于特定聚类。这意味着问题要么是病态的,要么例如,预期的聚类数量过高,并不代表真实的数据结构。提供一种衡量该算法与硬聚类方法(如 K-means)相似程度的好方法是由归一化的Dunn 划分系数:
当 P[c] 在 0 和 1 之间时,当它接近 0,意味着隶属度分布平坦,模糊度达到最高。另一方面,如果它接近 1,则 W 的每一行只有一个主导值,而其他所有值都可以忽略不计。这种情况类似于硬聚类方法。较高的 P[c] 值通常是首选的,因为它即使在放弃一定程度模糊度的同时,也能做出更精确的决策。考虑到前面的例子,当集合不相交时,P[c] 趋向于 1,而如果例如选择三个资历级别相同且重叠,则变为 0(完全模糊)。当然,我们希望通过限制边缘案例的数量来避免这种极端情况。可以通过分析不同数量的聚类和 m 值(在示例中,我们将使用 MNIST 手写数字数据集)来执行网格搜索。一个合理的经验法则是接受高于 0.8 的 P[c] 值,但在某些情况下,这可能是不可能的。如果我们确信问题已经很好地设定,最佳方法是选择最大化 P[c] 的配置,但请注意,最终值小于 0.3-0.5 将导致非常高的不确定性,因为聚类重叠非常严重。
完整的 模糊 C 均值 算法是:
-
设置最大迭代次数 N[max]
-
设置容差 Thr
-
设置 k 的值(期望的聚类数量)
-
使用随机值初始化矩阵 W^((0)) 并将每一行标准化,通过除以其和
-
设置 N = 0
-
当 N < N[max] 或 ||W^((t)) - W^((t-1))|| > Thr 时:
-
N = N + 1
-
对于 j = 1 到 k:
- 计算质心向量 μ[j]
-
重新计算权重矩阵 W^((t))
-
标准化 W^((t)) 的行
-
使用 Scikit-Fuzzy 的模糊 C 均值示例
Scikit-Fuzzy (pythonhosted.org/scikit-fuzzy/
) 是一个基于 SciPy 的 Python 包,它允许实现所有最重要的模糊逻辑算法(包括模糊 C 均值)。在本例中,我们继续使用 MNIST 数据集,但主要关注模糊划分。为了执行聚类,Scikit-Fuzzy 实现了 cmeans
方法(在 skfuzzy.cluster
包中),该方法需要一些强制参数:data
,它必须是一个数组 D ∈ ℜ^(N × M)(N 是特征数量;因此,与 Scikit-Learn 一起使用的数组必须是转置的);c
,聚类数量;系数 m
,error
,这是最大容差;以及 maxiter
,这是最大迭代次数。另一个有用的参数(非强制)是 seed
参数,它允许指定随机种子,以便能够轻松地重现实验。我邀请读者查看官方文档以获取更多信息。
本例的第一步是执行聚类:
from skfuzzy.cluster import cmeans
fc, W, _, _, _, _, pc = cmeans(X_train.T, c=10, m=1.25, error=1e-6, maxiter=10000, seed=1000)
cmeans
函数返回许多值,但对我们来说,最重要的是:第一个,它是一个包含簇中心点的数组;第二个,它是最终的隶属度矩阵;最后一个,是分区系数。为了分析结果,我们可以从分区系数开始:
print(pc)
0.632070870735
这个值告诉我们,聚类结果与硬分配并不太远,但仍然存在一定的模糊性。在这种情况下,这种情况可能是合理的,因为我们知道许多数字部分扭曲,可能与其他数字(如 1、7 和 9)非常相似。然而,我邀请读者尝试不同的m
值,并检查分区系数如何变化。现在我们可以显示中心点:
模糊 C 均值法得到的中心点
所有不同的数字类别都已成功找到,但现在,与 K 均值不同,我们可以检查一个问题数字(代表 7,索引为 7)的模糊性,如图所示:
被选中的样本数字(一个 7)以测试模糊性
与之前样本相关的隶属度如下:
print(W[:, 7])
[ 0.00373221 0.01850326 0.00361638 0.01032591 0.86078292 0.02926149
0.03983662 0.00779066 0.01432076 0.0118298 ]
对应的图如下:
对应于代表 7 的数字的模糊隶属度图
在这种情况下,m的选择迫使算法减少了模糊性。然而,仍然可以看到三个较小的峰值,分别对应于以 1、8 和 5 为中心的簇(记住簇索引对应于之前在中心点图中显示的数字)。我邀请读者分析不同数字的模糊分区,并用不同的m
参数重新绘制。当m值较大时,可以观察到模糊性增加(这也对应于较小的分区系数)。这种效应是由于簇之间更强的重叠(也可以通过绘制中心点来观察)并且当需要检测样本的扭曲时可能是有用的。事实上,即使主要峰值指示正确的簇,次要的峰值(按降序排列)也告诉我们样本与其他中心点相似的程度,因此,如果它包含其他子集的特征。
与 Scikit-Learn 不同,为了进行预测,Scikit-Fuzzy 实现了cmeans_predict
方法(在同一个包中),它需要与cmeans
相同的参数,但与簇的数量c
不同,需要最终的中心点数组(参数名为cntr_trained
)。该函数返回的第一个值是相应的隶属度矩阵(其他值与cmeans
相同)。在下面的代码片段中,我们重复对相同的样本数字(代表7
)进行预测:
import numpy as np
from skfuzzy.cluster import cmeans_predict
new_sample = np.expand_dims(X_train[7], axis=1)
Wn, _, _, _, _, _ = cmeans_predict(new_sample, cntr_trained=fc, m=1.25, error=1e-6, maxiter=10000, seed=1000)
print(Wn.T)
[[ 0.00373221 0.01850326 0.00361638 0.01032591 0.86078292 0.02926149
0.03983662 0.00779066 0.01432076 0.0118298 ]]
Scikit-Fuzzy 可以使用pip install -U scikit-fuzzy
命令安装。有关进一步说明,请访问pythonhosted.org/scikit-fuzzy/install.html
谱聚类
K-means 和其他类似算法最常见的问题之一是我们假设只有超球簇。当数据集被分割成可以轻松嵌入到常规几何结构中的 blob 时,这个条件是可以接受的。然而,当集合不能用常规形状分开时,它就会失败。让我们考虑以下二维数据集:
正弦数据集
正如我们将在示例中看到的那样,任何尝试使用 K-means 将上方的正弦波与下方的正弦波分开的尝试都将失败。原因相当明显:包含上方集合的圆也将包含部分(或整个)下方的集合。考虑到 K-means 采用的准则和强制两个簇,通过垂直分离来最小化惯性,对应于大约 x[0] = 0。因此,结果簇是完全混合的,只有一个维度对最终配置有贡献。然而,两个正弦波集合是很好地分开的,并且不难检查,从下方的集合中选择一个点 x[i],总是可以找到一个只包含属于同一集合的样本的球。我们已经在讨论标签传播算法时讨论过这类问题,而谱聚类背后的逻辑基本上是相同的(有关更多细节,请读者查阅第二章,基于图的半监督学习)。
假设我们有一个数据集 X,它是从数据生成过程 p[data] 中采样的:
我们可以构建一个图 G = {V, E},其中顶点是点,边是通过一个亲和矩阵 W 确定的。每个元素 w[ij] 必须表达样本 x[i] 和样本 x[j] 之间的亲和力。W 通常使用两种不同的方法构建:
- KNN:在这种情况下,我们可以为每个点 x[i] 构建要考虑的邻居数量。W 可以作为一个连通性矩阵(仅表达两个样本之间是否存在连接)构建,如果我们采用以下标准:
或者,可以构建一个距离矩阵:
- 径向基函数(RBF):前述方法可能导致不完全是连通的图,因为可能存在没有邻居的样本。为了获得一个完全连通的图,可以采用 RBF(这种方法也已在 Kohonen 映射算法中使用过):
γ 参数允许控制高斯函数的振幅,减少或增加具有高权重的样本数量(即 实际邻居)。然而,所有点都被分配了一个权重,并且生成的图始终是连通的(即使许多元素接近于零)。
在这两种情况下,W 的元素将代表点之间的亲和度(或 接近度)的度量,并且不对全局几何形状施加限制(与 K-means 相反)。特别是,使用 KNN 连接矩阵,我们隐式地将原始数据集分割成具有高度内部凝聚力的较小区域。我们现在需要解决的问题是如何找到合并属于同一簇的所有区域的方法。我们将在这里介绍的方法是由 Normalized Cuts and Image Segmentation,J. Shi 和 J. Malik,IEEE Transactions on Pattern Analysis and Machine Intelligence,Vol. 22,08/2000 提出的,并且基于归一化图拉普拉斯算子:
矩阵 D,称为度矩阵,与 第三章 中讨论的相同,基于图的半监督学习,并且定义为:
可以证明以下性质(正式的证明被省略了,但可以在诸如 Functions and Graphs Vol. 2,Gelfand I. M,Glagoleva E. G.,Shnol E. E.,The MIT Press 等文本中找到):
-
L[n] 的特征值 λ[i] 和特征向量 v[i] 可以通过解方程 Lv = λDv 来找到,其中 L 是未归一化的图拉普拉斯算子 L = D - W。
-
L[n] 总是有一个等于 0 的特征值(具有多重性 k),对应的特征向量 v[o] = (1, 1, ..., 1)。
-
由于 G 是无向的,并且所有 w[ij] ≥ 0,因此 G 的连接组件数 k 等于零特征值的多重性。
换句话说,归一化图拉普拉斯算子编码了关于连接组件数量的信息,并为我们提供了一个新的参考系统,其中可以使用常规几何形状(通常是超球体)来分离簇。为了更好地理解这种方法的工作原理,而不需要复杂的数学方法,重要的是要揭示 L[n] 的另一个属性。
从线性代数我们知道,矩阵 M ∈ ℜ^(n × n) 的每个特征值 λ 都对应一个特征空间,它是 ℜ^n 的一个子集,包含与 λ 相关的所有特征向量以及零向量。此外,给定一个集合 S ⊆ ℜ^n 和一个可数子集 C(可以将定义扩展到通用子集,但在此上下文中数据集总是可数的),我们可以定义一个向量 v ∈ ℜ^n 为一个 指示向量,如果 v^((i)) = 1 当向量 c[i] ∈ S 时,否则 v^((i)) = 0。如果我们考虑 L[n] 的零特征值,并假设它们的数量是 k(对应于特征值 0 的多重性),则可以证明相应的特征向量是每个由它们张成的特征空间的指示向量。从前面的陈述中,我们知道这些特征空间对应于图 G 的连通分量;因此,使用将这些点投影到这些子空间的标准聚类(如 K-means 或 K-means++)可以很容易地实现对称形状的分离。
由于 L[n] ∈ ℜ^(M × M),其特征向量 v[i] ∈ ℜ^M。选择前 k 个特征向量,可以构建一个矩阵 A ∈ ℜ^(M × k):
矩阵 A 的每一行,即 a[j] ∈ ℜ^k,可以看作是在由 L[n] 的每个零特征值张成的低维子空间中原始样本 x[j] 的投影。在此点,新数据集 A = {a[j]} 的可分性仅取决于图 G 的结构,特别是对于径向基函数(RBFs),取决于邻居的数量或 γ 参数。与许多其他类似情况一样,不可能定义一个适用于所有问题的标准值,尤其是在维度不允许视觉检查的情况下。一个合理的方法应该从少数邻居(例如,五个)或 γ = 1.0 开始,并增加值,直到性能指标(如调整后的兰德指数)达到最大。考虑到问题的性质,测量同质性和完整性也可能很有用,因为这两个度量对不规则几何结构更敏感,并且可以很容易地显示聚类没有正确分离集合。如果地面实值未知,可以使用轮廓分数来评估所有超参数(聚类数量、邻居数量或 γ)的内聚度和分离度。
完整的 Shi-Malik 谱聚类 算法是:
-
在 KNN(1)和 RBF(2)之间选择一个图构建方法:
-
选择参数 k
-
选择参数 γ
-
-
选择预期的聚类数量 N[k]。
-
计算矩阵 W 和 D。
-
计算归一化图拉普拉斯矩阵 L[n]。
-
计算 L[n] 的前 k 个特征向量。
-
构建矩阵 A。
-
使用 K-means++(或任何其他对称算法)对矩阵 A 的行进行聚类。此过程的输出是以下聚类集:C[km]^((1)), C[km]^((2)), ..., C[km]^((Nk))。
使用 Scikit-Learn 的谱聚类示例
在这个例子中,我们将使用之前显示的正弦波数据集。第一步是创建它(包含 1,000 个样本):
import numpy as np
from sklearn.preprocessing import StandardScaler
nb_samples = 1000
X = np.zeros(shape=(nb_samples, 2))
for i in range(nb_samples):
X[i, 0] = float(i)
if i % 2 == 0:
X[i, 1] = 1.0 + (np.random.uniform(0.65, 1.0) * np.sin(float(i) / 100.0))
else:
X[i, 1] = 0.1 + (np.random.uniform(0.5, 0.85) * np.sin(float(i) / 100.0))
ss = StandardScaler()
Xs = ss.fit_transform(X)
到目前为止,我们可以尝试使用 K-means(n_clusters=2
)进行聚类:
from sklearn.cluster import KMeans
km = KMeans(n_clusters=2, random_state=1000)
Y_km = km.fit_predict(Xs)
结果如下所示:
使用正弦波数据集进行的 K-means 聚类结果
如预期,K-means 无法分离两个正弦波。读者可以自由尝试不同的参数,但结果始终是不可接受的,因为 K-means 二维聚类是圆形的,且没有有效的配置。现在我们可以使用基于 KNN 算法的亲和矩阵进行谱聚类(在这种情况下,Scikit-Learn 可能会产生警告,因为图不是完全连接的,但这通常不会影响结果)。Scikit-Learn 实现了SpectralClustering
类,其中最重要的参数是n_clusters
,预期的聚类数;affinity
,可以是'rbf'
或'nearest_neighbors'
;gamma
(仅适用于 RBF);以及n_neighbors
(仅适用于 KNN)。在我们的测试中,我们选择了20
个邻居:
from sklearn.cluster import SpectralClustering
sc = SpectralClustering(n_clusters=2, affinity='nearest_neighbors', n_neighbors=20, random_state=1000)
Y_sc = sc.fit_predict(Xs)
谱聚类的结果如下所示:
使用正弦波数据集进行的谱聚类结果
如预期,该算法能够完美地分离两个正弦波。作为练习,我邀请读者将此方法应用于 MNIST 数据集,使用 RBF(具有不同的 gamma 值)和 KNN(具有不同的邻居数量)。我还建议重新绘制 t-SNE 图并比较所有分配错误。由于聚类严格非凸,我们预计 Silhouette 分数不会很高。其他有用的练习可以是:绘制 Silhouette 图并检查结果,分配真实标签,并测量同质性和完整性。
摘要
在本章中,我们介绍了一些基本的聚类算法。我们从 KNN 开始,它是一种基于实例的方法,通过重新结构化数据集来找到给定查询点最相似的样本。我们讨论了三种方法:一种简单的方法,在计算复杂度方面也是最昂贵的,以及两种基于 KD 树和 Ball 树构建的策略。这两种数据结构可以在样本数量非常大时显著提高性能。
下一个主题是一个经典的算法:K-means,它是一种对称分区策略,类似于方差接近零的高斯混合,可以解决许多现实生活中的问题。我们讨论了两种算法:一种是普通的算法,它无法找到有效的次优解;另一种是优化的初始化方法,称为 K-means++,它能够加快收敛到接近全局最小值的解。在同一部分,我们还介绍了一些评估方法,可以用来评估通用聚类算法的性能。
我们还介绍了一种软聚类方法,称为模糊 C 均值,它类似于标准 K-means 的结构,但允许管理隶属度(类似于概率),这些隶属度编码了样本与所有聚类质心的相似性。这种方法允许在更复杂的管道中处理隶属度向量,例如,聚类过程的输出可以输入到一个分类器中。
K-means 和类似算法最重要的局限性之一是聚类的对称结构。这个问题可以通过诸如谱聚类等方法来解决,这是一种基于数据集图非常强大的方法,与非线性降维方法相当相似。我们分析了 Shi 和 Malik 提出的一个算法,展示了它如何轻松地分离非凸数据集。
在下一章,第八章,集成学习,我们将讨论一些常见的集成学习方法,它们基于使用大量弱分类器。我们关注它们的特性,比较了不同集成与单个强分类器的性能。
第八章:集成学习
在本章中,我们将讨论一些重要的算法,这些算法利用不同的估计器来提高集成或委员会的整体性能。这些技术要么通过在每个属于预定义集的估计器中引入中等程度的随机性来实现,要么通过创建一系列估计器,其中每个新的模型都被迫提高前一个模型的表现。这些技术使我们能够在使用有限容量或更容易过拟合训练集的模型时,减少偏差和方差(从而提高验证准确率)。
本章涵盖的特定主题如下:
-
集成学习方法介绍
-
决策树简介
-
随机森林和额外的随机森林
-
AdaBoost(算法 M1,SAMME,SAMME.R 和 R2)
-
梯度提升
-
投票分类器集成、堆叠和分桶
集成学习基础
集成学习背后的主要概念是强学习者和弱学习者的区别。特别是,强学习器是一个分类器或回归器,它具有足够的容量达到最高的潜在准确率,最小化偏差和方差(从而实现一个令人满意的泛化水平)。更正式地说,如果我们考虑一个参数化的二元分类器 f(x; θ),我们定义它为强学习器,如果以下条件成立:
这个表达式可能看起来有些晦涩;然而,它实际上非常容易理解。它仅仅表达了这样一个概念:一个强大的学习者在理论上能够以大于或等于 0.5 的概率(即二元随机猜测的阈值)实现任何非零误分类概率。在机器学习任务中通常使用的所有模型都是强大的学习者,即使它们的领域可能有限(例如,逻辑回归无法解决非线性问题)。另一方面,一个弱学习者是一个通常能够实现略高于随机猜测的准确率的模型,但其复杂性非常低(它们可以非常快地训练,但永远不能单独用来解决复杂问题)。在这种情况下,也存在一个正式的定义,但更简单的是考虑弱学习者的真正主要特性是有限的实现合理准确率的能力。在训练空间的某些非常特定且小的区域内,弱学习者可以达到低误分类概率,但在整个空间中,其性能仅略优于随机猜测。前一个定义更多的是理论上的,而不是实践上的,因为目前所有可用的模型通常都比随机预言者要好得多。然而,集成被定义为一起训练(或按顺序)的一组弱学习者,以形成一个委员会。在分类和回归问题中,最终结果是通过平均预测或采用多数投票来获得的。
在这一点上,一个合理的问题可能是——为什么我们需要训练许多弱学习者而不是一个单一的强大学习者?答案是双重的——在集成学习中,我们通常处理中等强大的学习者(如决策树或支持向量机(SVMs)),并将它们作为一个委员会来提高整体准确率并减少方差,这得益于对样本空间的更广泛探索。事实上,虽然一个强大的学习者通常能够过度拟合训练集,但要在整个样本子空间中保持高准确率而不饱和其容量则更为困难。为了避免过度拟合,必须找到一种权衡,结果是得到一个准确率较低但分离超平面更简单的分类器/回归器。采用许多弱学习者(实际上它们相当强大,因为即使是简单的模型也比随机猜测更准确),可以迫使他们只关注一个有限的子空间,从而能够以低方差达到非常高的局部准确率。委员会采用平均技术,可以轻松找出哪个预测是最合适的。或者,它可以要求每个学习者进行投票,假设成功的训练过程必须总是引导多数人提出最准确的分类或预测。
集成学习的最常见方法如下:
-
袋装(Bagging,即自助聚合):这种方法使用n个弱学习器fw1, fw2, ..., fwn(通常它们是决策树)来训练n个训练集(D1, D2, ..., Dn),这些训练集是通过随机抽样原始数据集D创建的。抽样过程(称为自助抽样)通常是通过替换来执行的,以便确定不同的数据分布。此外,在许多实际算法中,弱学习器也是使用中等程度的随机性初始化和训练的。这样,克隆的概率变得非常小,同时,通过保持方差在可容忍的阈值以下(从而避免过拟合),可以提高准确性。
-
提升(Boosting):这是一种替代方法,它从单个弱学习器fw1开始构建增量集成,并在每次迭代中添加一个新的学习器fwi。目标是重新加权数据集,以便迫使新的学习器关注先前被错误分类的样本。这种策略产生了非常高的准确性,因为新的学习器是在一个正偏置的数据集上训练的,这使得它们能够适应最困难的内部条件。然而,以这种方式,对方差的控制减弱了,集成更容易过拟合训练集。可以通过减少弱学习器的复杂性或施加正则化约束来减轻这个问题。
-
堆叠(Stacking):这种方法可以以不同的方式实现,但其哲学始终如一——使用在相同数据集上训练的不同算法(通常是几个强大的学习器),并使用另一个分类器过滤最终结果,平均预测或使用多数投票。如果数据集具有可以通过不同方法部分管理的结构,这种策略可以非常强大。每个分类器或回归器都应该发现一些独特的数据方面;这就是为什么算法必须在结构上不同。例如,将决策树与 SVM 或线性核模型混合可能是有用的。在测试集上进行的评估应清楚地显示在某些情况下只有分类器占主导地位。如果一个算法最终是唯一产生最佳预测的算法,那么集成就变得无用了,最好专注于单个强大的学习器。
随机森林
随机森林是基于决策树的袋装集成模型。如果读者不熟悉这类模型,我建议阅读 《机器学习导论》,Alpaydin E.,麻省理工学院出版社,在那里可以找到完整的解释。然而,为了我们的目的,提供对最重要的概念的简要解释是有用的。决策树是一个类似于标准分层决策过程的模型。在大多数情况下,使用一个特殊的家族,称为二叉决策树,因为每个决策只产生两个结果。这种树通常是简单且合理的最佳选择,训练过程(即构建树本身)非常直观。根节点包含整个数据集:
每个级别是通过应用以下定义的选择元组获得的:
元组的第一个索引对应于一个输入特征,而阈值 t[i] 是在每个特征特定范围内选择的一个值。应用一个选择元组会导致一个分割和两个节点,每个节点包含输入数据集的非重叠子集。在下面的图中,有一个在根级别(初始分割)执行的分割示例:
决策树初始分割的示例
集合 X 被分割成两个子集,分别定义为 X11 和 X12,其样本分别具有 i=2 小于或大于阈值 ti=0.8 的特征。分类决策树背后的直觉是继续分割,直到叶子节点包含属于单个类别 yi 的样本(这些节点被定义为纯净的)。这样,一个新的样本 xj 可以通过计算复杂度 O(log(M)) 遍历树,并达到一个最终节点,该节点确定其类别。以非常相似的方式,可以构建输出连续的回归树(即使,为了我们的目的,我们将只考虑分类场景)。
在这一点上,主要问题是如何执行每个分割。我们不能随意选择任何特征和任何阈值,因为最终的树将完全不平衡且非常深。我们的目标是找到每个节点处的最佳选择元组,考虑到最终目标是分类到离散类别(对于回归过程几乎相同)。这种技术非常类似于基于必须最小化的成本函数的问题,但在这个情况下,我们局部操作,应用与节点异质性成比例的不纯度度量。高不纯度表明存在属于许多不同类别的样本,而不纯度等于 0 则表明存在单个类别。由于我们需要继续分割直到出现纯净的叶子节点,最佳选择基于一个对每个选择元组进行评分的函数,使我们能够选择产生最低不纯度的那个(理论上,过程应该继续到所有叶子都是纯净的,但通常提供一个最大深度,以避免过度的复杂性)。如果有 p 个类别,类别集可以定义为以下:
一种非常常见的纯度度量称为 基尼不纯度,它基于如果使用从节点子集分布中随机选择的标签对样本进行分类时的错误分类概率。直观上,如果所有样本都属于同一类别,任何随机选择都会导致正确分类(并且不纯度变为 0)。另一方面,如果节点包含来自许多类别的样本,错误分类的概率会增加。正式地,该度量定义为以下:
子集由 Xk 表示,而 p(j|k) 是通过属于类别 j 的样本数与样本总数的比率获得的。选择元组必须被选择以最小化子节点的基尼不纯度。另一种常见的方法是交叉熵不纯度,定义如下:
与前一种度量方法相比,这种差异主要由一些基本的信息理论概念提供。特别是,我们想要达到的目标是最小化不确定性,这使用 (交叉-)熵 来衡量。如果我们有一个离散分布,并且所有样本都属于同一类别,那么随机选择可以完全描述该分布;因此,不确定性为零。相反,例如,如果我们有一个公平的骰子,每个结果的概率是 1/6,相应的熵大约是 2.58 比特(如果对数的底是 2)。当节点变得越来越纯净时,交叉熵不纯度降低,并在最佳场景下达到 0。此外,采用互信息概念,我们可以在分割后定义获得的信息增益:
给定一个节点,我们希望创建两个子节点以最大化信息增益。换句话说,通过选择交叉熵不纯度,我们隐式地扩展树,直到信息增益变为零。再次考虑公平骰子的例子,我们需要 2.58 位信息来决定正确的结果。如果骰子是偏的,并且结果的可能性是 1.0,那么我们不需要任何信息来做出决定。在决策树中,我们希望模仿这种情况,这样,当一个新的样本完全遍历树时,我们就不需要任何进一步的信息来对其进行分类。如果施加最大深度限制,最终的信息增益不能为零。这意味着我们需要支付额外的成本来完成分类。这个成本与剩余的不确定性成正比,并且应该最小化以提高精度。
也可以采用其他方法(尽管基尼系数和交叉熵是最常见的),我邀请读者查阅参考文献以获取更多信息。然而,在这个阶段,一个自然的问题出现了。决策树是简单的模型(它们不是弱学习器!),但构建它们的程序比训练逻辑回归或 SVM 要复杂得多。为什么它们如此受欢迎?一个明显的原因是——它们代表了一个可以用图表展示的结构化过程;然而,这并不足以证明它们的用途。两个重要的特性使得决策树可以在没有任何数据预处理的情况下使用。
事实上,很容易理解,与其他方法不同,不需要任何缩放或白化,并且可以同时使用连续和分类特征。例如,在一个二维数据集中,如果一个特征具有方差等于 1,而另一个等于 100,那么大多数分类器将只能达到低精度;因此,预处理步骤变得必要。在决策树中,选择元组在范围差异很大时也具有相同的效果。不言而喻,在考虑分类特征的情况下,可以轻松地进行分割,例如,不需要使用诸如独热编码(在大多数情况下是必要的,以避免泛化错误)等技术。然而,不幸的是,使用决策树获得的分离超曲面通常比使用其他算法获得的超曲面要复杂得多,这导致方差更高,从而降低了泛化能力。
要理解原因,可以想象一个非常简单的二维数据集,由位于第二和第四象限的两个 blob 组成。第一个集合的特征是(x < 0, y > 0),但第二个集合的特征是(x < 0, y < 0)。让我们还假设我们有一些异常值,但我们关于数据生成过程的知识不足以将它们视为噪声样本(原始分布可以在轴上延伸尾部;例如,它可能是由两个高斯分布混合而成)。在这种情况下,最简单的分离线是分割平面的对角线,将平面分为包含第一和第三象限区域的两个子平面。然而,这个决策只能同时考虑两个坐标。使用决策树,我们需要最初,例如,使用第一个特征进行分割,然后再使用第二个特征进行分割。结果是分段分离线(例如,将平面分割为对应于第二象限及其补集的区域),导致非常高的分类方差。矛盾的是,可以通过一个不完整的树(例如,将过程限制为单个分割)和选择y-轴作为分离线来获得更好的解决方案(这就是为什么强制最大深度很重要的原因),但你付出的代价是增加的偏差(以及随之而来的更差的准确性)。
在使用决策树(和相关模型)时,另一个需要考虑的重要元素是最大深度。可以生长树直到所有叶子都是纯的,但有时强制最大深度(以及随之而来的最大终端节点数)更可取。最大深度等于 1 导致称为决策桩的二进制模型,它不允许特征之间有任何交互(它们可以简单地表示为If... Then条件)。更高的值产生更多的终端节点,并允许特征之间有更多的交互(可以想到许多If... Then语句与AND
逻辑运算符的组合)。正确的值必须针对每个问题进行调整,并且重要的是要记住,非常深的树比修剪过的树更容易过拟合。
在某些情况下,为了获得更高的泛化能力,宁愿牺牲略微较差的准确性,在这种情况下,应该强制最大深度。确定最佳值的常用工具始终是网格搜索与交叉验证技术相结合。
随机森林为我们提供了一种强大的工具来解决偏差-方差权衡问题。它们是由 L. Breiman 提出的(见Breiman L.,随机森林,机器学习,45,2001),其逻辑非常简单。正如前一部分所解释的,bagging 方法首先从选择弱学习者的数量Nc开始。第二步是生成Nc个数据集(称为 bootstrap samples)D1, D2, ..., DNc:
每个决策树都使用相应的数据集,通过一个共同的杂质标准进行训练;然而,在随机森林中,为了减少方差,选择分裂的计算不是考虑所有特征,而只通过一个包含相当少特征(常见选择是平方根的四舍五入、log2 或自然对数)的随机子集。这种方法确实削弱了每个学习器,因为部分最优性丢失,但通过限制过度专业化,我们可以获得显著减少方差的效果。同时,由于集成(特别是对于大量估计量)的结果,偏差减少和精度提高。实际上,由于学习器使用略微不同的数据分布进行训练,当 Nc → ∞ 时,预测的平均值收敛到正确的值(在实践中,并不总是需要使用一个非常大的决策树数量,但是,必须使用带有交叉验证的网格搜索来调整正确的值)。一旦所有模型,用函数 di(x) 表示,都经过训练,最终的预测可以通过平均得到:
或者,也可以采用多数投票法(但仅适用于分类):
这两种方法非常相似,并且在大多数情况下会得到相同的结果。然而,平均法在样本几乎位于边界时更加稳健,并且提供了更高的灵活性。此外,它可用于分类和回归任务。
随机森林通过从较小的样本子集中选择最佳选择元组来限制其随机性。在某些情况下,例如,当特征数量不是非常大时,这种策略会导致方差减少最小化,而计算成本不再由结果证明是合理的。通过一个称为额外随机树(或简称额外树)的变体,可以实现更好的性能。程序几乎相同;然而,在这种情况下,在执行分割之前,会计算 n 个随机阈值(对于每个特征),并选择导致最少不纯度的那个。这种方法进一步削弱了学习器,但同时减少了剩余方差并防止过拟合。这种动态与许多技术(如正则化或 dropout)非常相似;事实上,额外的随机性降低了模型的容量,迫使它达到更线性的解决方案(这显然是不理想的)。为此限制所付出的代价是随之而来的偏差恶化,然而,这种恶化却因许多不同学习者的存在而得到补偿。即使使用随机分割,当 Nc 足够大时,错误分类(或回归预测)的概率也会越来越小,因为平均投票和多数投票往往会补偿在特定区域结构上特别次优的树的输出。当训练样本数量很大时,这个结果更容易获得。实际上,在这种情况下,有放回的采样会导致略微不同的分布,即使这不是正式正确的,也可以被认为是部分和随机增强的。因此,每个弱学习器都会隐式地关注整个数据集,并额外关注一个较小的子集,尽管这个子集是随机选择的(与实际的增强不同)。
完全随机森林算法如下:
-
设置决策树的数量 Nc
-
对于 i=1 到 Nc:
- 从原始数据集 X 中有放回地采样创建数据集 Di
-
在每次分割时设置要考虑的特征数量 Nf(例如,sqrt(n))
-
设置一个不纯度度量(例如,基尼不纯度)
-
为每棵树定义一个可选的最大深度
-
对于 i=1 到 Nc:
-
随机森林:
- 使用数据集 Di 训练决策树 di(x),并从随机采样的 Nf 个特征中选择最佳分割
-
额外树:
- 使用数据集 Di 训练决策树 di(x),在每次分割前计算 n 个随机阈值,并选择产生最少不纯度的那个
-
-
定义一个输出函数,它平均单个输出或采用多数投票
Scikit-Learn 中的随机森林示例
在这个例子中,我们将使用 Scikit-Learn 中直接可用的著名葡萄酒数据集(178 个 13 维样本分为三个类别)。不幸的是,找到适合集成学习算法的好的简单数据集并不容易,因为它们通常与大型和复杂的数据集一起使用,这需要太长的计算时间。由于葡萄酒数据集并不特别复杂,第一步是使用 k 折交叉验证来评估不同分类器(逻辑回归、决策树和多项式 SVM)的性能:
import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
X, Y = load_wine(return_X_y=True)
lr = LogisticRegression(max_iter=1000, random_state=1000)
print(np.mean(cross_val_score(lr, X, Y, cv=10)))
0.956432748538
dt = DecisionTreeClassifier(criterion='entropy', random_state=1000)
print(np.mean(cross_val_score(dt, X, Y, cv=10)))
0.933298933609
svm = SVC(kernel='poly', random_state=1000)
print(np.mean(cross_val_score(svm, X, Y, cv=10)))
0.961403508772
如预期,性能相当好,平均交叉验证准确率最高值约为 96%,由多项式 SVM(默认度数为 3)实现。一个非常有趣的因素是决策树的表现,它是这一组中最差的(其 Gini 不纯度较低)。即使它不正确,我们也可以将这个模型定义为这一组中最弱的,它是我们袋装测试的完美候选人。现在我们可以通过实例化RandomForestClassifier
类并选择n_estimators=50
(我邀请读者尝试不同的值)来拟合一个随机森林:
from multiprocessing import cpu_count
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=50, n_jobs=cpu_count(), random_state=1000)
print(np.mean(cross_val_score(rf, X, Y, cv=10)))
0.983333333333
如预期,平均交叉验证准确率最高,约为 98.3%。因此,随机森林已经成功找到了决策树的全球配置,以便使它们几乎在任何样本空间的区域中专业化。参数n_jobs=cpu_count()
告诉 Scikit-Learn 使用机器上所有可用的 CPU 核心并行化训练过程。
为了更好地理解这个模型的动态,将交叉验证准确率作为树的数量函数绘制是有用的:
随机森林的交叉验证准确率作为树数量的函数
当树的数量增加到大约 320 时,观察到一些波动和平台期并不令人惊讶。随机性的影响可能导致性能损失,甚至增加学习者的数量。事实上,即使训练准确率增加,不同折上的验证准确率也可能受到过度专业化的影响。此外,在这个案例中,非常有趣的是,最高准确率可以通过 50 棵树而不是 400 棵或更多来实现。因此,我总是建议至少进行网格搜索,这不仅是为了达到最佳准确率,也是为了最小化模型的复杂性。
当与决策树和随机森林一起工作时,另一个需要考虑的重要因素是特征重要性(当选择此标准时也称为 Gini 重要性),这是一个与特定特征允许我们实现的杂质减少成比例的度量。对于决策树,它定义如下:
在前面的公式中,n(j)表示达到节点j(求和必须扩展到所有选择该特征的所有节点)的样本数量,而ΔIi是在节点j处使用特征i分割后实现的纯度降低。在随机森林中,必须通过对所有树进行平均来计算重要性:
在拟合模型(决策树或随机森林)后,Scikit-Learn 在feature_importances_
实例变量中输出特征重要性向量。在下面的图表中,有一个按降序排列的每个特征重要性的图(标签可以通过命令load_wine()['feature_names'])
获得):
葡萄酒数据集的特征重要性
我们不想分析每个元素化学意义,但很明显,例如,脯氨酸的存在和颜色强度比非黄酮酚的存在要重要得多。由于模型处理的是语义上独立的特征(图像的像素并不相同),因此可以通过移除所有对最终准确率影响不大的特征来降低数据集的维度。这个过程称为特征选择,应该使用更复杂的统计技术来执行,例如卡方检验,但当分类器能够产生重要性指数时,也可以使用 Scikit-Learn 中的一个名为SelectFromModel
的类。通过传递一个估计器(可以是拟合的或不拟合的)和一个阈值,可以过滤掉所有低于阈值的特征值,从而转换数据集。将此应用于我们的模型并设置最小重要性为0.02
,我们得到以下结果:
from sklearn.feature_selection import SelectFromModel
sfm = SelectFromModel(estimator=rf, prefit=True, threshold=0.02)
X_sfm = sfm.transform(X)
print(X_sfm.shape)
(178, 10)
新数据集现在包含 10 个特征,而不是原始葡萄酒数据集的 13 个特征(例如,很容易验证灰分和非黄酮酚已被移除)。当然,对于任何其他降维方法,总是建议您通过交叉验证来验证最终准确率,并且只有在损失准确率与降低复杂度之间的权衡是合理的情况下才做出决定。
AdaBoost
在上一节中,我们了解到使用放回抽样会导致数据集中样本被随机重新加权。然而,如果 M 非常大,大多数样本只会出现一次,而且所有选择都是完全随机的。AdaBoost 是 Schapire 和 Freund 提出的一种算法,它试图通过采用自适应提升(这个名字来源于此)来最大化每个弱学习器的效率。特别是,集成是通过顺序增长,并在每一步重新计算数据分布,以便增加被错误分类的样本的权重,减少被正确分类的样本的权重。这样,每个新的学习器被迫关注那些对先前估计器来说更成问题的区域。读者可以立即理解,与随机森林和其他袋装方法不同,提升不依赖于随机性来减少方差和提高精度。相反,它以确定性的方式工作,并且每个新的数据分布都是根据精确的目标选择的。在本段中,我们将考虑一个称为 离散 AdaBoost(正式名称为 AdaBoost.M1)的变体,它需要一个输出被阈值化的分类器(例如,-1 和 1)。然而,已经开发了实值版本(其输出行为类似于概率),例如在 Additive Logistic Regression: a Statistical View of Boosting 中所示,作者为 Friedman J.,Hastie T.,Tibshirani R.,发表于 Annals of Statistics,1998 年第 28 卷)。由于主要概念始终相同,对其他变体的理论细节感兴趣的读者可以立即在参考文献中找到它们。
为了简单起见,AdaBoost.M1 的训练数据集定义为如下:
这种选择并不是一个限制,因为在多类问题中,可以很容易地采用一对一策略,即使像 AdaBoost.SAMME 这样的算法保证了更好的性能。为了操作数据分布,我们需要定义一个权重集:
权重集允许定义一个隐式数据分布 D(t)(x),最初它与原始数据分布等效,但可以通过改变 wi 的值轻松重塑。一旦选择了家族和估计器的数量 Nc,就可以开始全局训练过程。该算法可以应用于任何能够产生阈值估计的学习器(而实值变体可以使用概率工作,例如通过 Platt 缩放方法获得)。
第一个实例d1(x)使用原始数据集进行训练,这意味着使用数据分布D(1)(x)。相反,下一个实例使用重新加权的分布D(2)(x),D(3)(x),...,D(Nc)(x)进行训练。为了计算它们,在每个训练过程之后,计算归一化加权误差总和:
此值介于0(没有误分类)和1(所有样本都被误分类)之间,并用于计算估计器权重α(t):
要理解这个函数是如何工作的,考虑其图像(如下所示)是有用的:
估计器权重图作为归一化加权误差总和的函数
此图揭示了一个隐含的假设:最差的分类器不是将所有样本都分类错误的那一个(ε(t) = 1),而是一个完全随机的二进制猜测(对应于ε(t) = 0.5)。在这种情况下,α(t)为零,因此,如果完全丢弃估计器,结果就是零。当ε(t) < 0.5时,应用提升(在约 0.05 和 0.5 之间,趋势几乎是线性的),但只有当ε(t) < 约 0.25 时,它才大于 1(较大的值会导致惩罚,因为权重小于 1)。这个值是一个阈值,用于判断估计器是否可信或非常强大,在完美估计器(没有错误)的特定情况下,α(t) → +∞。
在实践中,应该施加一个上限,以避免溢出或除以零。相反,当ε(t) > 0.5时,估计器是不可接受的弱,因为它比随机猜测还要差,并且产生的提升将是负的。为了避免这个问题,实际实现必须反转此类估计器的输出,实际上将它们转变为ε(t) < 0.5的学习者(这不是问题,因为转换应用于所有输出值的方式相同)。重要的是要考虑,这个算法不应该直接应用于多类场景,因为,正如多类 AdaBoost中指出的,Zhu J.,Rosset S.,Zou H.,Hastie T.,01/2006,阈值 0.5 仅对应于二元选择的随机猜测精度。当类的数量大于两个时,随机估计器输出一个类的概率为1/Ny(其中Ny是类的数量),因此,AdaBoost.M1 将以错误的方式提升分类器,导致最终精度较差(实际的阈值应该是1 - 1/Ny,当Ny > 2时,它大于 0.5)。AdaBoost.SAMME 算法(由 Scikit-Learn 实现)已被提出以解决这个问题,并利用提升在多类场景中的力量。
全局决策函数定义如下:
以这种方式,随着估计器的逐个添加,每个估计器的重要性将逐渐降低,而 di(x) 的准确性将提高。然而,如果 X 的复杂性非常高,也可能观察到平台期。在这种情况下,许多学习者的权重会很高,因为最终的预测必须考虑学习者的子组合以达到可接受的准确性。由于此算法在每一步都专门化学习者,因此一个好的做法是从少量估计器(例如,10 或 20)开始,直到不再获得改进为止。有时,只需要少量优秀的学习者(如 SVM 或决策树)就能达到最高的可能准确性(限于此类算法),但在某些其他情况下,估计器的数量可以达到数千。网格搜索和交叉验证仍然是做出正确选择的唯一好策略。
在每一步训练之后,必须更新权重以产生增强分布。这是通过使用指数函数(基于双极输出 {-1, 1})实现的:
对于一个样本 x[i],如果它被错误分类,其权重将根据整体估计器权重增加。这种方法允许进一步的自适应行为,因为具有高 α(t) 的分类器已经非常准确,并且需要更高的关注水平来仅关注(少数)错误分类的样本。相反,如果 α(t) 较小,估计器必须提高其整体性能,并且必须将过重加权过程应用于大子集(因此,分布不会围绕少数样本峰值,而只会惩罚那些被正确分类的小子集,让估计器能够以相同的概率探索剩余空间)。即使原始提案中没有提到,也可以包括一个乘以指数的学习率 η:
值 η = 1 没有影响,而较小的值已被证明可以通过避免过早专业化来提高准确性。当然,当 η << 1 时,必须增加估计器的数量以补偿轻微的重加权,这可能导致训练性能损失。至于其他超参数,必须使用交叉验证技术来发现 η 的正确值(或者,如果它必须是唯一需要微调的值,可以从一个值开始,通过减少其值直到达到最大准确性)。
完整的 AdaBoost.M1 算法如下:
-
设置家族和估计器的数量 Nc
-
将初始权重 W(1) 设置为 1/M
-
设置学习率 η(例如,η = 1)
-
将初始分布 D(1) 设置为数据集 X
-
对于 i=1 到 Nc:
-
使用数据分布 D(i) 训练第 i 个估计器 di(x)
-
计算归一化加权误差和 ε(i):
- 如果 ε(i) > 0.5,则反转所有估计器输出
-
计算估计权重 α(i)
-
使用指数公式(带或不带学习率)更新权重
-
归一化权重
-
-
创建全局估计器,将 sign(•) 函数应用于加权求和 α(i)di(x)(对于 i=1 到 Nc)
AdaBoost.SAMME
这个变体被称为多类指数损失逐步加性建模(SAMME),由 Zhu、Rosset、Zou 和 Hastie 在 Multi-class AdaBoost 中提出,Zhu J.,Rosset S.,Zou H.,Hastie T.,01/2006。目标是使 AdaBoost.M1 能够在多类场景中正常工作。由于这是一个离散版本,其结构几乎相同,只是在估计权重计算上有所不同。让我们考虑一个标签数据集,Y:
现在,有 p 个不同的类别,需要考虑随机猜测估计器无法达到 0.5 的准确度;因此,新的估计器权重计算如下:
这样,阈值就会向前推进,当以下条件成立时,α(t) 将为零:
下图显示了 α(t) 与 p = 10 的关系图:
当 p = 10 时,估计权重作为归一化加权误差和的函数的图
采用这种修正,提升过程可以成功处理多类问题,而不会引入 AdaBoost.M1 在 p > 2(当误差小于实际随机猜测时,α(t) > 0)时通常引入的偏差。由于该算法的性能明显优于其他算法,大多数 AdaBoost 实现不再基于原始算法(例如,Scikit-Learn 实现了 AdaBoost.SAMME 和实值版本 AdaBoost.SAMME.R)。当然,当 p = 2 时,AdaBoost.SAMME 与 AdaBoost.M1 完全等价。
AdaBoost.SAMME.R
AdaBoost.SAMME.R 是一个与能够输出预测概率的分类器一起工作的变体。这通常可以通过使用 Platt 缩放等技术来实现,但重要的是要检查特定的分类器实现是否能够在不采取任何进一步行动的情况下输出概率。例如,Scikit-Learn 提供的 SVM 实现不会计算概率,除非参数 probability=True
(因为它们需要额外的步骤,在某些情况下可能无益)。
在这种情况下,我们假设每个分类器的输出是一个概率向量:
每个分量是在给定输入 xi 的情况下,j^(th)类输出的条件概率。当使用单个估计器时,通过 argmax(•)函数获得获胜类别;然而,在这种情况下,我们想要重新加权每个学习者,以获得一个顺序增长的集成。基本思想与 AdaBoost.M1 相同,但现在我们管理概率向量,我们还需要一个依赖于单个样本xi的估计器权重函数(这个函数实际上包装了现在表示为概率向量函数pi(t)(y=i|x)的每个估计器):
考虑到对数函数的性质,前面的表达式等价于一个离散的α(t);然而,在这种情况下,我们并不依赖于加权误差总和(理论解释相当复杂,超出了本书的范围。读者可以在上述论文中找到它,即使下一章介绍的方法揭示了逻辑的基本部分)。为了更好地理解这个函数的行为,让我们考虑一个简单的场景,其中p = 2。第一种情况是学习者无法分类的样本(p=(0.5, 0.5)):
在这种情况下,不确定性最大,分类器无法信任这个样本,因此所有输出概率的权重变为零。现在,让我们应用提升,得到概率向量p=(0.7, 0.3):
当p → 1时,第一个类别将变为正值,其幅度将增加,而另一个则是相反的值。因此,函数是对称的,允许使用总和:
这种方法与加权多数投票非常相似,因为获胜的类别yi的计算不仅考虑了输出yi的估计器的数量,还考虑了它们的相对权重和剩余分类器的负权重。只有当最强的分类器预测了该类别,并且其他学习者的影响不足以推翻这一结果时,才能选择一个类别。
为了更新权重,我们需要考虑所有概率的影响。特别是,我们希望减少不确定性(这可能会退化成纯粹的随机猜测)并迫使注意力集中在所有那些被错误分类的样本上。为了实现这一目标,我们需要定义yi和p(t)(xi)向量,它们分别包含真实类别的 one-hot 编码(例如,(0, 0, 1, ..., 0))和估计器输出的概率(作为一个列向量)。因此,更新规则如下:
例如,如果真实向量是(1, 0)且输出概率是(0.1, 0.9),η=1 时,样本的权重将乘以大约 3.16。如果输出概率是(0.9, 0.1),意味着样本已被成功分类,乘数将更接近 1。这样,新的数据分布 D(t+1),类似于 AdaBoost.M1,将在需要更多关注的样本上更加尖锐。所有实现都将学习率作为超参数,因为,如前所述,默认值等于 1.0 可能不是特定问题的最佳选择。一般来说,较低的学习率可以在存在许多异常值时减少不稳定性,并通过较慢的收敛速度提高泛化能力。当η < 1 时,每个新的分布都会稍微更多地关注被错误分类的样本,允许估计器在不进行大跳跃的情况下(可能导致估计器跳过最佳点)寻找更好的参数集。然而,与通常使用小批次的神经网络不同,AdaBoost 也可以在η=1 时表现相当好,因为校正仅在完整训练步骤之后应用。像往常一样,我建议进行网格搜索以选择每个特定问题的正确值。
完整的 AdaBoost.SAMME.R 算法如下:
-
设置家族和估计器数量 Nc
-
将初始权重 W(1)设置为 1/M
-
设置学习率η(例如,η = 1)
-
将初始分布 D(1)设置为数据集 X
-
对于 i=1 到 Nc:
-
使用数据分布 D(i)训练第 i 个估计器 di(x)
-
计算每个类别和每个训练样本的输出概率
-
计算估计器权重αj(i)
-
使用指数公式(带或不带学习率)更新权重
-
归一化权重
-
-
通过对αj(i)的和(对于 i=1 到 Nc)应用 argmax(•)函数来创建全局估计器
AdaBoost.R2
Drucker 博士(在《使用提升技术改进回归器》Drucker H.,ICML 1997)提出了一种稍微复杂一些的变体来处理回归问题。弱学习器通常是决策树,其主要概念与其他变体非常相似(特别是,应用于训练数据集的重加权过程)。真正的区别在于选择最终预测 yi 的策略,给定输入样本 xi。假设有 Nc 个估计器,每个估计器都表示为函数 dt(x),我们可以为每个输入样本计算绝对残差 ri(t):
一旦包含所有绝对残差的集合 Ri 已被填充,我们可以计算量 Sr = sup Ri 并计算与误差成比例的成本函数值。通常实现的常见选择(并且作者本人也建议)是线性损失:
这种损失非常平坦,并且直接与误差成正比。在大多数情况下,这是一个好的选择,因为它避免了过早的过度专业化,并允许估计量以更温和的方式调整其结构。最明显的替代方案是平方损失,它开始给予那些预测误差较大的样本更多的重要性。它定义如下:
最后的成本函数严格相关于 AdaBoost.M1,并且是指数的:
这通常是一个不太稳健的选择,因为我们还将在下一节讨论,它倾向于惩罚较小的错误,而不是较大的错误。考虑到这些函数也用于重新加权过程,指数损失可以迫使分布给那些误分类错误高的样本分配非常高的概率,从而使估计量在第一次迭代中变得过度专业化。在许多情况下(例如在神经网络中),损失函数通常根据它们的特定属性选择,但更重要的是,根据它们易于最小化的程度。在这种情况下,损失函数是提升过程的基本部分,并且必须考虑对数据分布的影响。测试和交叉验证是做出合理决策的最佳工具。
一旦对所有训练样本评估了损失函数,就可以构建全局成本函数,作为所有损失的加权平均值。与许多简单求和或平均损失的算法不同,在这种情况下,必须考虑分布的结构。由于提升过程重新加权样本,相应的损失值也必须过滤,以避免偏差。在迭代 t 时,成本函数的计算如下:
这个函数与加权误差成正比,这些误差可以通过线性过滤或使用二次或指数函数强调。然而,在所有情况下,权重较低的样本将产生较小的贡献,使算法能够专注于更难预测的样本。请注意,在这种情况下,我们正在处理分类;因此,我们唯一可以使用的度量是损失。好的样本产生较低的损失,困难的样本产生成比例较高的损失。即使可以直接使用 C(t),也最好定义一个置信度度量:
此指数与迭代 t 的平均置信度成反比。事实上,当 C(t) → 0 时,γ(t) → 0,当 C(t) → ∞时,γ(t) → 1。权重更新是在考虑整体置信度和特定损失值的情况下进行的:
权重将按比例减少,与相应的绝对残差损失相关。然而,而不是使用固定的基数,选择全局置信指数。这种策略允许进一步的可适应性,因为置信度低的估计器不需要只关注一个小子集,考虑到γ(t)介于 0 和 1(最坏情况)之间,当成本函数非常高(1x = 1)时,指数变得无效,因此权重保持不变。这种方法与其他变体中采用的方法不太相似,但它试图在全局准确性和局部误分类问题之间找到一个折衷方案,提供额外的鲁棒性。
该算法中最复杂的部分是输出全局预测所采用的方法。与分类算法不同,我们无法轻易计算平均值,因为需要考虑每次迭代的全局置信度。Drucker 提出了一种基于所有输出加权中位数的方法。特别是,给定一个样本 xi,我们定义预测集:
作为权重,我们考虑 log(1 / γ(t)),因此我们可以定义一个权重集:
最终输出是Γ加权的中位数(归一化,总和为 1.0)。当γ(t) → 1 时,置信度低,相应的权重将趋于 0。同样,当置信度高(接近 1.0)时,权重将成比例增加,选择与其相关的输出的机会将更高。例如,如果输出是 Y = {1, 1.2, 1.3, 2.0, 2.2, 2.5, 2.6},权重是Γ = { 0.35, 0.15, 0.12, 0.11, 0.1, 0.09, 0.08 },加权中位数对应于第二个索引,因此全局估计器将输出 1.2(这也是直观上最合理的选择)。
寻找中位数的过程相当简单:
-
yi(t)必须按升序排序,以便 yi(1) < yi(2) < ... < yi(Nc)
-
根据 yi(t)的索引对集合Γ进行排序(每个输出 yi(t)都必须携带其自身的权重)
-
将Γ进行归一化,除以其总和
-
选择将Γ分成两个块(其和小于或等于 0.5)的最小元素对应的索引
-
选择与该索引对应的输出
完整的 AdaBoost.R2 算法如下:
-
设置家族和估计器数量 Nc
-
将初始权重 W(1)设置为 1/M
-
将初始分布 D(1)设置为数据集 X
-
选择一个损失函数 L
-
对于 i=1 到 Nc:
-
使用数据分布 D(i) 训练第 i 个估计量 di(x)
-
计算绝对残差、损失值和置信度度量
-
计算全局代价函数
-
使用指数公式更新权重
-
-
使用加权中位数创建全局估计量
AdaBoost 的 Scikit-Learn 示例
让我们继续使用 Wine 数据集来分析 AdaBoost 在不同参数下的性能。Scikit-Learn,像几乎所有的算法一样,实现了分类器 AdaBoostClassifier
(基于 SAMME 和 SAMME.R 算法)和回归器 AdaBoostRegressor
(基于 R2 算法)。在这种情况下,我们将使用分类器,但我邀请读者使用自定义数据集或内置的玩具数据集测试回归器。在这两个类中,最重要的参数是 n_estimators
和 learning_rate
(默认值设置为 1.0
)。默认的底层弱学习器始终是决策树,但可以通过创建一个基础实例并通过参数 base_estimator
传递它来使用其他模型。正如章节中解释的那样,实值 AdaBoost 算法需要一个基于概率向量的输出。在 Scikit-Learn 中,一些分类器/回归器(如 SVM)除非明确要求(设置参数 probability=True
),否则不会计算概率;因此,如果出现异常,我邀请您检查文档以了解如何强制算法计算它们。
我们将要讨论的示例仅具有教学目的,因为它们关注单一参数。在现实世界场景中,总是更好的执行网格搜索(这更昂贵),以便分析一组组合。让我们开始分析交叉验证分数作为估计量数量的函数(向量 X 和 Y 是在前面示例中定义的):
import numpy as np
from sklearn.ensemble import AdaBoostClassifier
from sklearn.model_selection import cross_val_score
scores_ne = []
for ne in range(10, 201, 10):
adc = AdaBoostClassifier(n_estimators=ne, learning_rate=0.8, random_state=1000)
scores_ne.append(np.mean(cross_val_score(adc, X, Y, cv=10)))
我们考虑了从 10 棵树开始到 200 棵树结束的范围,步长为 10 棵树。学习率保持恒定,等于 0.8。结果图如下所示:
10 折交叉验证准确率作为估计量数量的函数
最大值出现在 50 个估计量时。更大的值会导致性能下降,因为过度专业化以及随之而来的方差增加。正如在其他章节中解释的那样,模型的能力必须根据奥卡姆剃刀原则进行调整,这不仅因为结果模型可以更快地训练,而且还因为能力过剩通常会导致过拟合训练集,并减少泛化的范围。交叉验证可以立即显示出这种效果,而标准训练/测试集分割时(尤其是当样本没有打乱时),这种效果可能仍然隐藏。
现在让我们检查不同学习率下的性能(保持树的数量不变):
import numpy as np
scores_eta_adc = []
for eta in np.linspace(0.01, 1.0, 100):
adc = AdaBoostClassifier(n_estimators=50, learning_rate=eta, random_state=1000)
scores_eta_adc.append(np.mean(cross_val_score(adc, X, Y, cv=10)))
最终的图表如下所示:
10 倍交叉验证准确率作为学习率(估计量数量=50)的函数
再次强调,不同的学习率会产生不同的准确率。选择η = 0.8似乎是最有效的,因为更高的和更低的价值会导致性能下降。正如解释的那样,学习率对重新加权过程有直接影响。非常小的值需要更多的估计量,因为后续分布非常相似。另一方面,大的值可能导致过早的过度专业化。即使默认值是1.0
,我也总是建议检查使用较小值时的准确率。在每种情况下选择正确的学习率没有金科玉律,但重要的是要记住,较低的值允许算法以更温和的方式平滑地适应以适应训练集,而较高的值会降低对异常值的鲁棒性,因为被错误分类的样本会立即被提升,并且采样它们的概率会非常迅速地增加。这种行为的结果是持续关注那些可能受到噪声影响的样本,几乎忘记了剩余样本空间的结构。
我们想要进行的最后一个实验是分析使用主成分分析(PCA)和因子分析(FA)(50 个估计量和η = 0.8
)进行的降维后的性能:
import numpy as np
from sklearn.decomposition import PCA, FactorAnalysis
scores_pca = []
for i in range(13, 1, -1):
if i < 12:
pca = PCA(n_components=i, random_state=1000)
X_pca = pca.fit_transform(X)
else:
X_pca = X
adc = AdaBoostClassifier(n_estimators=50, learning_rate=0.8, random_state=1000)
scores_pca.append(np.mean(cross_val_score(adc, X_pca, Y, cv=10)))
scores_fa = []
for i in range(13, 1, -1):
if i < 12:
fa = FactorAnalysis(n_components=i, random_state=1000)
X_fa = fa.fit_transform(X)
else:
X_fa = X
adc = AdaBoostClassifier(n_estimators=50, learning_rate=0.8, random_state=1000)
scores_fa.append(np.mean(cross_val_score(adc, X_fa, Y, cv=10)))
结果图表如下所示:
10 倍交叉验证准确率作为成分数量(主成分分析 PCA 和因子分析 FA)的函数
这个练习证实了在第五章“EM 算法及其应用”中分析的一些重要特征。首先,性能即使经过 50%的维度降低也不会有显著影响。这一考虑在先前的例子中进行的特征重要性分析中得到进一步证实。决策树仅考虑 6/7 个特征时就能执行相当好的分类,因为剩余的特征对样本的特征化贡献微乎其微。此外,因子分析(FA)几乎总是优于主成分分析(PCA)。使用 7 个成分,FA 算法实现的准确率高于 0.95(非常接近没有减少时实现的值),而 PCA 需要 12 个成分才能达到这个值。读者应该记住,PCA 是 FA 的一个特例,假设同方差噪声。图表证实,在 Wine 数据集中,这种条件是不可接受的。假设不同的噪声方差允许以更准确的方式重新建模减少的数据集,最小化缺失特征的交叉效应。即使 PCA 通常是首选,但在大型数据集的情况下,我建议您始终比较两种技术的性能,并选择保证最佳结果的技术(考虑到 FA 在计算复杂度方面更昂贵)。
梯度提升
在这一点上,我们可以引入创建提升集成的一个更通用的方法。让我们选择一个通用的算法族,如下所示:
每个模型使用向量 θi 进行参数化,并且对采用的方法没有限制。在这种情况下,我们将考虑决策树(当采用这种提升策略时,这是最广泛使用的算法之一——在这种情况下,该算法被称为梯度提升树),但理论是通用的,可以很容易地应用于更复杂的模型,如神经网络。在决策树中,参数向量 θi 由选择元组组成,因此读者可以将这种方法视为一个伪随机森林,其中我们寻找额外的优化,而不是随机性,利用先前的经验。事实上,与 AdaBoost 一样,梯度提升集成是按顺序构建的,使用一种正式定义为前向分阶段加性建模的技术。得到的估计量表示为一个加权求和:
因此,需要管理的变量是单个估计量权重 αi 和参数向量 θi。然而,我们不必处理整个集合,而只需处理单个元组 (αi, θi),无需修改之前迭代中已选择的值。一般程序可以用循环来概括:
-
估计量之和被初始化为空值
-
对于 i=1 到 Nc:
-
选择最佳的 元组(αi, θi) 并训练估计量 f(x; θi)
-
di(x) = di-1(x) + αif(x; θi)
-
-
输出最终的估计器 d(x)
如何找到最佳的元组?我们已提出一种策略,通过增强数据集来提高每个学习器的性能。在这种情况下,算法基于一个我们需要最小化的成本函数:
特别是,通用的最优元组如下获得:
由于过程是顺序的,每个估计器都优化以提高前一个的准确性。然而,与 AdaBoost 不同,我们并不受限于强制执行特定的损失函数(可以证明 AdaBoost.M1 与此算法的指数损失等价,但证明超出了本书的范围)。正如我们将要讨论的,其他成本函数可以在几个不同的场景中产生更好的性能,因为它们避免了过早收敛到次优最小值。
可以认为通过使用前面的公式来优化每个新的学习器,问题得到了解决;然而,argmin(•)
函数需要对成本函数空间进行完全探索,并且由于 C(•)
依赖于每个特定的模型实例,因此也依赖于 θi,因此有必要进行多次重新训练过程,以找到最优解。此外,问题通常是非凸的,变量的数量可能非常高。如 L-BFGS 或其他拟牛顿方法需要太多的迭代和计算时间。很明显,在大多数情况下,这种方法是不可行的,因此提出了梯度提升算法作为中间解决方案。其想法是找到一种次优解,通过梯度下降策略限制每个迭代的单步。
为了介绍算法,用明确的参考到最优目标重写加性模型是有用的:
注意,成本函数是在所有先前训练的模型上计算的;因此,校正始终是增量式的。如果成本函数 L 是可微分的(这是一个基本条件,但并不难满足),则可以计算相对于当前加性模型(在第 i^(th) 迭代时,我们需要考虑通过累加所有先前的 i-1 模型得到的加性模型)的梯度:
在这一点上,可以通过将当前加性模型移动到梯度的负方向来添加一个新的分类器:
我们尚未考虑参数αi(以及学习率η,它是一个常数),然而,熟悉一些基本微积分的读者可以立即理解更新的效果是通过迫使下一个模型提高其相对于前一个模型的准确性来减少全局损失函数的值。然而,单一步的梯度步并不足以保证适当的提升策略。实际上,如前所述,我们还需要根据每个分类器减少损失的能力来加权每个分类器。一旦计算了梯度,就可以通过直接最小化损失函数(使用线搜索算法)来确定权重αi 的最佳值,考虑到当前的加性模型,并将α作为一个额外变量:
当使用梯度提升树变体时,可以通过将权重αi分割成与树中每个终端节点关联的m个子权重αi(j)来获得改进。计算复杂度略有增加,但最终精度可以高于使用单个权重获得的精度。原因在于树的功能结构。由于提升迫使在特定区域进行专业化,单个权重可能导致学习器在特定样本无法正确分类时也进行过估计。相反,使用不同的权重,可以操作结果进行精细过滤,根据其值和特定树的性质接受或丢弃结果。
这个解决方案不能提供完整优化相同的准确性,但它非常快,并且可以通过使用更多的估计量和更低的学习率来补偿这种损失。像许多其他算法一样,梯度提升必须调整以获得最大精度和低方差。学习率通常远小于 1.0,其值应通过验证结果并考虑估计量的总数来确定(当使用更多学习者时最好减少它)。此外,可以添加正则化技术来防止过拟合。当与特定的分类器家族(如逻辑回归或神经网络)一起工作时,包括L1或L2惩罚非常容易,但与其他估计量一起则不太容易。因此,一种常见的正则化技术(也被 Scikit-Learn 实现)是训练数据集的下采样。选择P < N随机样本允许估计量减少方差并防止过拟合。或者,可以采用随机特征选择(仅适用于梯度树提升),就像随机森林一样;选择总特征数的一部分会增加不确定性并避免过度专业化。当然,这些技术的缺点是精度损失(与下采样/特征选择比成比例),必须分析以找到最合适的权衡。
在进入下一节之前,简要讨论一下通常与这类算法一起使用的成本函数是有用的。在第一章中,我们介绍了一些常见的成本函数,如均方误差、Huber Loss(在回归上下文中非常稳健)和交叉熵。它们都是有效的例子,但还有其他特定于分类问题的函数。第一个是指数损失,定义如下:
如 Hastie、Tibshirani 和 Friedman 所指出的,此函数将梯度提升转换为 AdaBoost.M1 算法。相应的成本函数具有非常精确的行为,有时并不适合解决特定问题。事实上,指数损失的误差很大时,其结果有很高的影响,产生围绕几个样本的强烈峰值分布。随后分类器可能会因此过度专门化其结构,仅应对小数据区域,存在失去正确分类其他样本能力的高度风险。在许多情况下,这种行为并不危险,最终的偏差-方差权衡是完全合理的;然而,有些问题中,较软的损失函数可以允许更好的最终准确性和泛化能力。对于实值二元分类问题,最常见的选择是二项式负对数似然损失(偏差),定义如下(在这种情况下我们假设分类器 f(•) 没有阈值,而是输出正类概率):
此损失函数与逻辑回归中使用的相同,与指数损失相反,不会产生峰值分布。两个被错误分类的样本将以错误(而不是指数值)的比例进行提升,以便迫使分类器几乎以相同的概率关注所有被错误分类的群体(当然,对于错误非常大的样本分配更高的概率是可取的,假设所有其他被错误分类的样本都有很好的机会被选中)。二项式负对数似然损失的自然扩展到多类问题是多项式负对数似然损失,定义如下(分类器 f(•) 表示为具有 p 个成分的概率向量):
在前面的公式中,符号 Iy=j 必须解释为指示函数,当 y=j 时等于 1,否则为 0。此损失函数的行为与二项式变体完全类似,并且通常默认用于分类问题。读者被邀请测试使用指数损失和偏差的示例,并比较结果。
完整的梯度提升算法如下:
-
设置家族和估计器数量 Nc
-
选择一个损失函数 L(例如,偏差)
-
将基础估计器 d0(x) 初始化为一个常数(例如 0)或使用另一个模型
-
设置学习率 η(例如 η = 1)
-
对于 i=1 到 Nc:
-
使用步骤 i-1 的加性模型计算梯度 ∇d L(•)
-
使用数据分布 { (xi, ∇d L(yi, di-1(xi)) } 训练第 i 个估计器 di(x)
-
执行线性搜索以计算 αi
-
将估计器添加到集成中
-
使用 Scikit-Learn 的梯度提升树示例
在这个例子中,我们想要使用梯度提升树分类器(类 GradientBoostingClassifier
)并检查最大树深度(参数 max_depth
)对性能的影响。考虑到之前的例子,我们首先设置 n_estimators=50
和 learning_rate=0.8
:
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
scores_md = []
eta = 0.8
for md in range(2, 13):
gbc = GradientBoostingClassifier(n_estimators=50, learning_rate=eta, max_depth=md, random_state=1000)
scores_md.append(np.mean(cross_val_score(gbc, X, Y, cv=10)))
结果如下所示:
最大树深度作为 10 折交叉验证准确率的函数
如第一部分所述,决策树的最大深度与特征之间的交互可能性密切相关。当这些树在集成中使用时,这可能是一个正面或负面的方面。非常高的交互水平可能会创建过度复杂的分离超平面并降低整体方差。在其他情况下,有限的交互会导致更高的偏差。在这个特定的(且简单的)数据集上,梯度提升算法在最大深度为 2(考虑到根的深度为零)时可以达到更好的性能,这一点部分得到了特征重要性分析和降维的证实。在许多现实世界的情况下,这种研究的结果可能会有很大的不同,性能可能会提高,因此我建议从最小深度开始交叉验证结果(最好使用网格搜索),直到达到最大准确率。现在,我们想要调整学习率,这是该算法的一个基本参数:
import numpy as np
scores_eta = []
for eta in np.linspace(0.01, 1.0, 100):
gbr = GradientBoostingClassifier(n_estimators=50, learning_rate=eta, max_depth=2, random_state=1000)
scores_eta.append(np.mean(cross_val_score(gbr, X, Y, cv=10)))
对应的图表如下所示:
学习率(最大深度等于 2)作为 10 折交叉验证准确率的函数
毫不奇怪,梯度提升树在 η ≈ 0.9 时优于 AdaBoost,交叉验证准确率略低于 0.99。这个例子非常简单,但它清楚地展示了这种技术的能力。主要的缺点是复杂性。与单个模型不同,集成对超参数的变化更敏感,必须进行更详细的研究以优化模型。当数据集不是特别大时,交叉验证仍然是最佳选择。如果我们相当确信数据集几乎完美地代表了底层数据生成过程,那么可以对其进行洗牌并分成两个(训练/测试)或三个块(训练/测试/验证),然后通过优化超参数并尝试过度拟合测试集(这个表达可能听起来有些奇怪,但过度拟合测试集意味着在完美学习训练集结构的同时最大化泛化能力)。
投票分类器的集成
创建集成的一个更简单但同样有效的方法是基于利用有限数量的强学习者的想法,这些学习者的特性使它们能够在样本空间的特定区域获得更好的性能。让我们首先考虑一组 Nc 个离散值分类器 f1(x), f2(x), ..., fNc(x)。算法不同,但它们都是用相同的训练集训练的,并输出相同的标签集。最简单的策略是基于硬投票方法:
在此情况下,函数 n(•) 计算输出标签 yi 的估计器的数量。这种方法在许多情况下非常强大,但也有一些局限性。如果我们只依赖于多数投票,我们隐含地假设通过大量估计器获得了正确的分类。即使需要 Nc/2 + 1 票来输出结果,在许多情况下,它们的数量要高得多。此外,当 k 不是非常大时,Nc/2 + 1 票也意味着涉及大量人群的对称性。这种条件往往导致训练出无用的模型,这些模型可以简单地被单个拟合良好的强学习器所取代。事实上,假设集成由三个分类器组成,其中一个在另外两个容易导致误分类的区域更为专业化。对这种集成应用硬投票策略可能会持续惩罚更复杂的估计器,以利于其他分类器。通过考虑实值结果可以获得更准确的解决方案。如果每个估计器输出一个概率向量,决策的置信度就隐含地编码在值中。例如,输出为 (0.52, 0.48) 的二元分类器比输出为 (0.95, 0.05) 的另一个分类器要不确定得多。应用阈值相当于将概率向量展平并丢弃不确定性。让我们考虑一个由三个分类器组成的集成和一个难以分类的样本,因为它非常接近分离超平面。硬投票策略决定选择第一类,因为阈值后的输出是 (1, 1, 2)。然后我们检查输出概率,得到 (0.51, 0.49),(0.52, 0.48),(0.1, 0.9)。在平均概率后,集成输出变为大约 (0.38, 062),通过应用 argmax(•)
,我们得到第二类作为最终决策。一般来说,考虑加权平均也是一个好的实践,这样最终的类别可以通过以下方式获得(假设分类器的输出是一个概率向量):
如果不需要加权,权重可以简单地等于 1.0,或者它们可以反映我们对每个分类器的信任程度。一个重要的规则是在大多数情况下避免一个分类器的支配地位,因为这会隐含地回到单个估计器场景。一个好的投票例子应该始终允许当少数派的信心远高于多数派时,推翻结果。在这种情况下,权重可以被视为超参数,并使用网格搜索与交叉验证进行调整。然而,与其他集成方法不同,它们不是细粒度的,因此最佳值通常是不同可能性之间的折衷。
一种稍微复杂的技术称为堆叠,它包括使用一个额外的分类器作为后过滤步骤。经典的方法包括分别训练分类器,然后将整个数据集转换为一个预测集(基于类别标签或概率),然后训练组合分类器以将预测与最终类别关联起来。即使使用像逻辑回归或感知器这样非常简单的模型,也可以混合预测,以实现一个作为输入值函数的动态重新加权。只有当可以使用单个训练策略来训练整个集成(包括组合器)时,才可行更复杂的方法。例如,它可以与神经网络一起使用,尽管神经网络已经具有隐含的灵活性,并且通常可以比复杂的集成表现得更好。
Scikit-Learn 中的投票分类器示例
在这个例子中,我们将使用 MNIST 手写数字数据集。由于概念非常简单,我们的目标是展示如何结合两个完全不同的估计器来提高整体交叉验证的准确性。因此,我们选择了一个逻辑回归和一个决策树,它们在结构上是不同的。特别是,前者是一个线性模型,它使用整个向量,而后者是一个基于特征的估计器,只能在特定情况下支持决策(图像不是由语义上一致的特征组成,但决策树的过度复杂性可以帮助那些非常接近分离超平面的特定样本,因此,使用线性方法对这些样本进行分类会更困难)。第一步是加载和归一化数据集(这个操作对决策树来说并不重要,但对逻辑回归的性能有强烈的影响):
import numpy as np
from sklearn.datasets import load_digits
X, Y = load_digits(return_X_y=True)
X /= np.max(X)
在这一点上,我们需要单独评估两个估计器的性能:
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
dt = DecisionTreeClassifier(criterion='entropy', random_state=1000)
print(np.mean(cross_val_score(dt, X, Y, cv=10)))
0.830880960443
lr = LogisticRegression(C=2.0, random_state=1000)
print(np.mean(cross_val_score(lr, X, Y, cv=10)))
0.937021649942
如预期,逻辑回归(准确率约为 94%)优于决策树(准确率 83%);因此,硬投票策略并不是最佳选择。由于我们更信任逻辑回归,我们可以采用带有权重向量设置为(0.9, 0.1)的软投票。VotingClassifier
类接受一个包含元组的列表(估计器的名称,实例),这些元组必须通过estimators
参数提供。策略可以通过参数投票(可以是“软”或“硬”)和可选的权重来指定,使用具有相同名称的参数:
import numpy as np
from sklearn.ensemble import VotingClassifier
vc = VotingClassifier(estimators=[
('LR', LogisticRegression(C=2.0, random_state=1000)),
('DT', DecisionTreeClassifier(criterion='entropy', random_state=1000))],
voting='soft', weights=(0.9, 0.1))
print(np.mean(cross_val_score(vc, X, Y, cv=10)))
0.944835154373
使用软投票策略,估计器能够通过减少全局不确定性来超越逻辑回归。我邀请读者使用其他数据集测试此算法,使用更多估计器,并尝试使用硬投票和软投票策略找到最佳组合。
集成学习作为模型选择
这不是一个合适的集成学习技术,但有时它被称为桶化。在前一节中,我们讨论了如何使用具有不同特性的几个强学习器来组成一个委员会。然而,在许多情况下,单个学习器就足以实现良好的偏差-方差权衡,但要在整个机器学习算法群体中选择它并不那么容易。因此,当必须解决一系列类似的问题(它们可以不同,但最好考虑可以轻松比较的场景)时,可以创建一个包含多个模型的集成,并使用交叉验证来找到表现最好的那个。在过程的最后,将使用单个学习器,但其选择可以被视为带有投票系统的网格搜索。有时,即使使用相似的数据库,这种技术也能揭示出重要的差异。例如,在系统开发过程中,提供了一个初始数据集(X1, Y1)。每个人都期望它是从潜在的数据生成过程 p[data] 中正确采样的,因此,拟合并评估了一个通用模型。让我们想象一下,SVM 实现了非常高的验证准确率(使用 k 折交叉验证评估),因此被选为最终模型。不幸的是,提供了一个更大的第二个数据集(X2, Y2),最终的平均准确率变差了。我们可能会简单地认为模型的残差方差无法使其正确泛化,或者,正如有时发生的那样,我们可以说第二个数据集包含许多未被正确分类的异常值。实际情况要复杂一些:给定一个数据集,我们只能假设它代表了一个完整的数据分布。即使样本数量非常高或我们使用数据增强技术,总体可能也不代表系统将要分析的某些特定样本。桶化是一种创建安全缓冲区的好方法,可以在场景发生变化时利用。集成可以由完全不同的模型组成,属于同一家族但参数不同的模型(例如,不同的核 SVM),或者是由复合算法(如 PCA + SVM、PCA + 决策树/随机森林等)的混合。最重要的元素是交叉验证。正如第一章中解释的那样,将数据集分为训练集和测试集可能只是一种可接受的解决方案,当样本数量及其变异性足够高,足以证明它正确地代表了最终数据分布时。这通常发生在深度学习中,数据集的维度相当大,计算复杂性不允许多次重新训练模型。相反,在经典的机器学习环境中,交叉验证是唯一检查模型在用大随机子集训练并在剩余样本上测试时的行为的方法。理想情况下,我们希望观察到相同的性能,但有时也会发生某些折叠的准确率更高,而其他折叠的准确率相当低。当观察到这种现象并且数据集是最终数据集时,这可能意味着模型无法管理样本空间的一个或多个区域,而提升方法可以显著提高最终准确率。
摘要
在本章中,我们介绍了集成学习的核心概念,重点关注了 bagging 和 boosting 技术。在第一部分,我们解释了强学习者和弱学习者之间的区别,并展示了如何结合估计器以实现特定目标的大致图景。
接下来的主题集中在决策树的性质及其主要优势和劣势上。特别是,我们解释了树的结构会导致方差的自然增加。bagging 技术随机森林允许减轻这个问题,同时提高整体准确性。通过增加随机性和采用称为额外随机树的变体,可以实现进一步的方差减少。在示例中,我们还看到了如何评估每个输入特征的重要性,并在不涉及复杂统计技术的情况下进行降维。
在第三部分,我们介绍了最著名的 boosting 技术 AdaBoost,它基于创建一个序列加性模型的概念,其中每个新估计器都是使用重新加权的(boosted)数据分布进行训练的。这样,每个学习者都被添加进来,专注于错误分类的样本,而不干扰之前添加的模型。我们分析了原始的 M1 离散变体以及最有效的替代方案 SAMME 和 SAMME.R(实值),以及 R2(用于回归),这些都在许多机器学习软件包中实现。
在 AdaBoost 之后,我们将这一概念扩展到通用的前向分阶段加性模型,其中每个新估计器的任务是使一个通用的成本函数最小化。考虑到完整优化的复杂性,提出了一种梯度下降技术,结合估计器权重线性搜索,可以在分类和回归问题中产生出色的性能。
最后的主题涉及如何使用少数强学习器构建集成,平均它们的预测或考虑多数投票。我们讨论了阈值分类器的主要缺点,并展示了如何构建一个软投票模型,该模型能够信任表现出较少不确定性的估计器。其他有用的主题包括 Stacking 方法,它包括使用额外的分类器处理集成中每个成员的预测,以及如何创建候选集成,这些集成使用交叉验证技术评估,以找出每个特定问题的最佳估计器。
在下一章中,我们将开始讨论最重要的深度学习技术,介绍关于神经网络及其训练过程中的基本概念。
第九章:用于机器学习的神经网络
本章是深度学习世界的介绍,其方法使得在许多通常被认为难以管理的分类和回归领域(如图像分割、自动翻译、语音合成等)实现最先进的性能成为可能。目标是向读者提供理解全连接神经网络结构的基本工具,并使用 Python 工具 Keras(采用所有现代技术以加速训练过程并防止过拟合)对其进行建模。
特别是,本章涵盖的主题如下:
-
基本人工神经元的结构
-
感知器、线性分类器和它们的局限性
-
具有最重要的激活函数(如 ReLU)的多层感知器
-
基于随机梯度下降(SGD)优化方法的反向传播算法
-
优化的 SGD 算法(动量、RMSProp、Adam、AdaGrad 和 AdaDelta)
-
正则化和 dropout
-
批标准化
基本人工神经元
神经网络的基本构建块是对生物神经元的抽象,这是一个相当简单但功能强大的计算单元,由 F. Rosenblatt 于 1957 年首次提出,用于构成最简单的神经网络架构,即感知器,我们将在下一节中分析。与更符合生物学原理但有一些强烈限制的赫布学习法相反,人工神经元的设计具有实用主义观点,当然,其结构仅基于一些表征生物细胞的元素。然而,最近深度学习研究活动揭示了这种架构的巨大力量。即使存在更复杂和专门的计算单元,基本的人工神经元也可以概括为两个块的结合,这在以下图中可以清楚地看到:
神经元的输入是一个实值向量 x ∈ ℜ^n,而输出是一个标量 y ∈ ℜ。第一个操作是线性的:
向量 w ∈ ℜ^n 被称为权重向量(或突触权重向量,因为它类似于生物神经元,重新加权输入值),而标量项 b ∈ ℜ 是一个称为偏差的常数。在许多情况下,考虑权重向量更容易。可以通过添加一个等于 1 的额外输入特征及其相应的权重来消除偏差:
这样,唯一需要学习的是权重向量。以下块被称为激活函数,它负责将输入重新映射到不同的子集。如果函数是fa = z,则该神经元被称为线性神经元,变换可以被省略。最初的实验基于线性神经元,这些神经元的强大程度远低于非线性神经元,这也是许多研究人员认为感知机失败的原因之一,但与此同时,这种限制为一种新的架构打开了大门,这种架构反而展示了其卓越的能力。现在,让我们从这个最初提出的神经网络开始分析。
感知机
感知机是弗兰克·罗森布拉特在 1957 年给第一个神经网络模型起的名字。感知机是一个单层输入线性神经元组成的神经网络,后面跟着一个基于sign(•)函数的输出单元(或者,也可以考虑一个输出为-1 和 1 的双极性单元)。感知机的架构在以下图表中展示:
即使图表看起来可能相当复杂,感知机可以用以下方程来概括:
所有向量都是惯例的列向量;因此,点积w^Tx[i]将输入转换为一个标量,然后加上偏差,使用步进函数获得二进制输出,当z > 0时输出 1,否则输出 0。此时,一个读者可能会反对说步进函数是非线性的;然而,应用于输出层的非线性只是一种过滤操作,对实际计算没有影响。确实,输出已经由线性块决定,而步进函数仅用于施加二进制阈值。此外,在这个分析中,我们只考虑单值输出(即使有多类变体),因为我们的目标是展示动态和局限性,然后再转向可以用来解决极其复杂问题的更通用的架构。
感知机可以用在线算法(即使数据集是有限的)进行训练,但也可以采用离线方法,该方法重复固定次数的迭代,或者直到总误差小于预定义的阈值。该过程基于平方误差损失函数(记住,传统上,术语loss应用于单个样本,而术语cost指的是每个单个损失的求和/平均值):
当一个样本被呈现时,会计算输出,如果输出错误,则应用权重校正(否则跳过该步骤)。为了简化,我们不考虑偏差,因为它不影响过程。我们的目标是校正权重以最小化损失。这可以通过计算相对于 w[i] 的偏导数来实现:
假设 w^((0)) = (0, 0)(忽略偏差)和样本 x = (1, 1),其 y = 1。感知器错误地分类了该样本,因为 sign(w^Tx) = 0。偏导数都等于 -1;因此,如果我们从当前权重中减去它们,我们得到 w^((1)) = (1, 1),现在样本被正确分类,因为 sign(w^Tx) = 1。因此,包括学习率 η,权重更新规则如下:
当一个样本被错误分类时,权重会根据实际线性输出和真实标签之间的差异成比例地校正。这是称为 delta 规则 的学习规则的一个变体,它代表了最著名的训练算法的第一步,该算法在几乎任何监督深度学习场景中都被使用(我们将在下一节中讨论它)。该算法已被证明在数据集线性可分的情况下,在有限的状态数内收敛到稳定解。形式证明相当繁琐且非常技术性,但感兴趣的读者可以在 《感知器》,Minsky M. L.,Papert S. A.,麻省理工学院出版社 中找到。
在本章中,学习率的作用变得越来越重要,特别是在单个样本(如感知器)或小批量评估之后执行更新时。在这种情况下,高学习率(即大于 1.0 的值)可能会因为单个校正的幅度而导致收敛过程中的不稳定性。在处理神经网络时,最好使用较小的学习率并重复固定数量的训练周期。这样,单个校正被限制在较小的范围内,并且只有当它们被大多数样本/批量 确认 时,它们才能变得稳定,推动网络收敛到最佳解。相反,如果校正是异常值的结果,较小的学习率可以限制其作用,避免仅因为几个噪声样本而破坏整个网络。我们将在下一节中讨论这个问题。
现在,我们可以描述完整的感知器算法,并在一些重要考虑下结束本段:
-
选择一个学习率 η 的值(例如
0.1
)。 -
将一个常数列(设置为
1.0
)添加到样本向量 X 中。因此 X[b] ∈ ℜ^(M × (n+1))。 -
使用具有小方差(例如
0.05
)的正态分布随机值初始化权重向量 w ∈ ℜ^(n+1)。 -
设置一个错误阈值
Thr
(例如0.0001
)。 -
设置最大迭代次数 N[i]。
-
设置
i = 0
。 -
设置
e = 1.0
。 -
当 i < N[i] 且 e > Thr:
-
设置
e = 0.0
。 -
对于 k=1 到 M:
-
计算线性输出 l[k] = w^Tx[k] 和阈值输出 t[k] = sign(l[k])。
-
如果 t[k] != y[k]:
-
计算 Δw[j] = η(l[k] - y[k])x[k]^((j))。
-
更新权重向量。
-
-
设置 e += (l[k] - y[k])²(或者也可以使用绝对值 |l[k] - y[k]|)。
-
-
设置
e /= M
。
-
该算法非常简单,读者应该已经注意到了它与逻辑回归的类比。事实上,这种方法基于一个可以被认为是具有 sigmoid 输出激活函数的感知器结构(该函数输出一个可以被认为是概率的实数值)。主要区别在于训练策略——在逻辑回归中,校正是在基于负对数似然度的损失函数评估之后进行的:
这个损失函数是众所周知的交叉熵,在第一章中,我们展示了最小化它是等同于减少真实分布和预测分布之间的 Kullback-Leibler 散度。在几乎所有深度学习分类任务中,我们将利用它,归功于其鲁棒性和凸性(这是逻辑回归中的收敛保证,但不幸的是,在更复杂的架构中,这个属性通常会被丢失)。
使用 Scikit-Learn 的感知器示例
即使从零开始实现这个算法非常简单,我还是更喜欢使用 Scikit-Learn 的Perceptron
实现,以便将注意力集中在导致非线性神经网络局限性的问题上。展示感知器主要弱点的历史性问题基于 XOR 数据集。与其解释,不如构建它并可视化其结构:
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.utils import shuffle
np.random.seed(1000)
nb_samples = 1000
nsb = int(nb_samples / 4)
X = np.zeros((nb_samples, 2))
Y = np.zeros((nb_samples, ))
X[0:nsb, :] = np.random.multivariate_normal([1.0, -1.0], np.diag([0.1, 0.1]), size=nsb)
Y[0:nsb] = 0.0
X[nsb:(2 * nsb), :] = np.random.multivariate_normal([1.0, 1.0], np.diag([0.1, 0.1]), size=nsb)
Y[nsb:(2 * nsb)] = 1.0
X[(2 * nsb):(3 * nsb), :] = np.random.multivariate_normal([-1.0, 1.0], np.diag([0.1, 0.1]), size=nsb)
Y[(2 * nsb):(3 * nsb)] = 0.0
X[(3 * nsb):, :] = np.random.multivariate_normal([-1.0, -1.0], np.diag([0.1, 0.1]), size=nsb)
Y[(3 * nsb):] = 1.0
ss = StandardScaler()
X = ss.fit_transform(X)
X, Y = shuffle(X, Y, random_state=1000)
显示真实标签的图表如下所示:
XOR 数据集示例
如您所见,数据集被分为四个块,这些块的组织方式类似于逻辑异或运算符的输出。考虑到二维感知器(以及逻辑回归)的分离超曲面是一条线;很容易理解任何可能的最终配置都能达到大约 50%的准确率(一个随机猜测)。为了确认这一点,让我们尝试解决这个问题:
import numpy as np
from multiprocessing import cpu_count
from sklearn.linear_model import Perceptron
from sklearn.model_selection import cross_val_score
pc = Perceptron(penalty='l2', alpha=0.1, max_iter=1000, n_jobs=cpu_count(), random_state=1000)
print(np.mean(cross_val_score(pc, X, Y, cv=10)))
0.498
Scikit-Learn 实现提供了通过参数penalty
(可以是'l1'
、'l2'
或'elasticnet'
)添加正则化项的可能性(见第一章,机器学习模型基础),以避免过拟合并提高收敛速度(强度可以使用参数alpha
指定)。这并不总是必要的,但由于该算法提供的是一个生产就绪的包,设计者决定添加此功能。尽管如此,平均交叉验证准确率略高于 0.5(读者被邀请测试任何其他可能的超参数配置)。相应的图(可能会因不同的随机状态或后续实验而改变)如下所示:
使用感知器标记的 XOR 数据集
显然,感知器是另一个没有特定特性的线性模型,并且其使用被劝阻,以其他算法(如逻辑回归或 SVM)为优先。1957 年之后,在几年里,许多研究人员并没有隐藏他们的幻想,将神经网络视为一个从未实现的承诺。必须等到对架构的简单修改,以及一个强大的学习算法,才正式打开了通往一个新迷人机器学习分支的大门(后来称为深度学习)。
在 Scikit-Learn > 0.19 中,Perceptron
类允许添加max_iter
或tol
(容差)参数。如果没有指定,将发出警告,通知读者未来的行为。这条信息不会影响实际结果。
多层感知器
感知器的主要限制是其线性。如何通过去除这种约束来利用这种架构?解决方案比任何猜测都简单。在输入和输出之间添加至少一个非线性层会导致一个高度非线性的组合,使用更多的变量进行参数化。这种架构称为多层感知器(MLP),包含一个(为了简单起见)隐藏层,如下所示:
这是一个所谓的前馈网络,意味着信息的流动从第一层开始,始终沿同一方向进行,并在输出层结束。允许部分反馈(例如,为了实现局部记忆)的架构称为循环****网络,将在下一章进行分析。
在这种情况下,有两个权重矩阵,W 和 H,以及两个相应的偏置向量,b 和 c。如果有 m 个隐藏神经元,x[i] ∈ ℜ^(n × 1)(列向量),和 y[i] ∈ ℜ^(k × 1),动态由以下转换定义:
对于任何多层感知器(MLP)来说,一个基本条件是至少有一个隐藏层激活函数 fh 是非线性的。证明 m 个线性隐藏层等价于单个线性网络是直接的,因此,MLP 会退化为标准感知器的情形。传统上,给定层的激活函数是固定的,但它们的组合没有限制。特别是,输出激活通常被选择以满足精确的要求(如多标签分类、回归、图像重建等)。这就是为什么这一分析的第一步关注最常用的激活函数及其特性。
激活函数
通常,任何连续且可微分的函数都可以用作激活函数;然而,其中一些具有特定的性质,可以在提高学习过程速度的同时实现良好的精度。它们在最新的模型中普遍使用,理解它们的特性对于做出最合理的选择至关重要。
Sigmoid 和双曲正切
这两种激活函数非常相似,但有一个重要的区别。让我们先来定义它们:
相应的图示如下所示:
Sigmoid 和双曲正切函数图
Sigmoid 函数 σ(x) 被限制在 0 和 1 之间,有两个渐近线(当 x → -∞ 时 σ(x) → 0,当 x → ∞ 时 σ(x) → 1)。同样,双曲正切(tanh
)被限制在 -1 和 1 之间,有两个渐近线对应于极值。分析这两个图,我们可以发现,这两个函数在短范围内几乎呈线性(大约在 [-2, 2]),并且立即变得几乎平坦。这意味着当 x 在 0 附近的值较小时,梯度很高且大致恒定,而对于较大的绝对值,梯度下降到大约 0。Sigmoid 函数完美地表示一个概率或一组必须在 0 和 1 之间有界的权重,因此,它可以是一些输出层的良好选择。然而,双曲正切函数完全对称,并且,出于优化的目的,它更可取,因为性能通常更优越。这种激活函数通常在输入通常较小的情况下用于中间层。当分析反向传播算法时,原因将变得清晰;然而,显然,大的绝对输入会导致几乎恒定的输出,并且由于梯度大约为 0,权重校正可以变得极其缓慢(这个问题正式称为梯度消失)。因此,在许多实际应用中,通常采用下一系列的激活函数。
矩形激活函数
这些函数在x > 0时都是线性的(或 Swish 的准线性),而当x < 0时则不同。即使其中一些在x = 0时是可微的,但在此情况下导数被设置为0
。最常见的函数如下:
相应的图表如下所示:
基本功能(同时也是最常用的)是 ReLU,当x > 0时具有恒定的梯度,而当x < 0时则为零。这个函数在视觉处理中非常常用,因为输入通常大于 0,并且具有减轻梯度消失问题的非凡优势,因为基于梯度的校正总是可能的。另一方面,当x < 0时,ReLU(及其一阶导数)为零,因此每个负输入都不允许任何修改。一般来说,这不是一个问题,但有些深度网络在允许小的负梯度时表现更好。这种考虑导致了其他变体,这些变体以存在超参数α为特征,该参数控制负尾的强度。介于 0.01 和 0.1 之间的常见值允许几乎与 ReLU 相同的行为,但x < 0时允许小的权重更新。最后一个函数称为 Swish,由Searching for Activation Functions, Ramachandran P., Zoph P., Le V. L., arXiv:1710.05941 [cs.NE]提出,基于 sigmoid,并提供了当x → 0时收敛到 0 的额外优势,因此非零效应被限制在由[-b, 0](其中b > 0)界定的短区域内。这个函数可以提高某些特定视觉处理深度网络的性能,如上述论文中所述。然而,我总是建议从 ReLU(非常稳健且计算成本低)开始分析,只有在没有其他技术可以提高模型性能的情况下才切换到替代方案。
Softmax
这个函数几乎定义了所有分类网络的输出层,因为它可以立即表示一个离散概率分布。如果有k个输出y[i],softmax 的计算如下:
以这种方式,包含k个神经元的层的输出被归一化,使得总和始终为 1。不言而喻,在这种情况下,最好的损失函数是交叉熵。事实上,如果所有真实标签都使用 one-hot 编码表示,它们隐式地成为概率向量,其中 1 对应于真实类别。因此,分类器的目标是通过最小化函数来减少其输出的训练分布之间的差异(有关更多信息,请参阅第一章,机器学习模型基础):
反向传播算法
现在,我们可以讨论在多层感知器(以及几乎所有其他神经网络)中使用的训练方法。这个算法与其说是实际算法,不如说是方法论;因此,我更喜欢定义主要概念,而不专注于特定案例。对实现感兴趣的读者将能够以最小的努力将相同的技巧应用于不同类型的网络(假设所有要求都已满足)。
使用深度学习模型进行训练的过程的目标通常是通过最小化成本函数来实现的。假设我们有一个用全局向量 θ 参数化的网络,成本函数(使用与损失相同的符号,但使用不同的参数来消除歧义)定义如下:
我们已经解释过,最小化前面的表达式(这是经验风险)是减少实际预期风险的一种方法,因此可以最大化准确性。因此,我们的目标是找到一个最优参数集,使得以下成立:
如果我们考虑一个单一的损失函数(与样本 x[i] 和真实标签 y[i] 相关),我们知道这样的函数可以用对预测值的显式依赖来表示:
现在,参数已经被嵌入到预测中。从微积分(不需要像许多关于优化技术书籍中所找到的那样过度的数学严谨性)我们知道,L 的梯度,一个标量函数,在任意点(我们假设 L 是可微的)计算出的梯度是一个具有以下分量的向量:
由于 ∇L 总是指向最近的极大值方向,因此负梯度指向最近的极小值方向。因此,如果我们计算 L 的梯度,我们就得到了一个可以直接用于最小化成本函数的信息。在继续之前,揭示一个重要的数学性质——导数的链式法则是有用的:
现在,让我们考虑一个多层感知器(MLP)的单步(从底部开始)并利用链式法则:
向量 y 的每个分量都与其他分量独立,因此我们可以通过只考虑一个输出值来简化示例:
在前面的表达式中(忽略偏差),有两个重要元素——权重,h[j](它们是 H 的列),以及表达式,z[j],它是先前权重的函数。由于 L 又是所有预测值 y[i] 的函数,通过应用链式法则(使用变量 t 作为激活函数的通用参数),我们得到以下结果:
由于我们通常处理向量函数,使用梯度算子表达这个概念更容易。简化通用层执行的转换,我们可以将关系(相对于 H 的一行,因此相对于对应于隐藏单元 z[i] 的权重向量 h[i])表示如下:
使用梯度并考虑向量输出 y 可以表示为 y = (y[1], y[2], ..., y[m]),我们可以推导出以下表达式:
这样我们就得到了相对于权重向量 h[i] 的 L 的梯度的所有分量。如果我们回溯,我们可以推导出 z[j] 的表达式:
重新应用链式法则,我们可以计算 L 对 w[pj] 的偏导数(为了避免混淆,预测 y[i] 的参数称为 t[1],而 z[j] 的参数称为 t[2]):
观察这个表达式(可以使用梯度轻松重写)并与前面的一个进行比较,可以理解反向传播算法的哲学,该算法首次在 通过反向传播错误学习表示,Rumelhart D. E.,Hinton G. E.,Williams R. J.,Nature 323/1986 中提出。样本被输入到网络中,并计算损失函数。在这个时候,过程从底部开始,计算相对于最近权重的梯度,并重用计算 δ[i](与误差成比例)的一部分来回溯,直到达到第一层。
确实,修正是从源(损失函数)传播到起源(输入层),并且效果与每个不同权重的责任(和偏差)成比例。
考虑所有可能的不同架构,我认为为单个示例写出所有方程是无用的。方法论在概念上是简单的,它纯粹基于导数的链式法则。此外,所有现有的框架,如 Tensorflow、Caffe、CNTK、PyTorch、Theano 等,都可以通过单个操作计算整个网络的全部权重的梯度,从而使用户能够关注更实际的问题(如找到避免过拟合和改进训练过程的最佳方法)。
在前一个章节中已经概述的一个重要现象值得考虑,现在应该更清晰了:链式法则基于乘法;因此,当梯度开始变得小于 1 时,乘法效应会迫使最后值接近 0。这个问题被称为梯度消失,并且真的会停止使用饱和激活函数(如sigmoid
或tanh
)的非常深层的模型的训练过程。整流单元为许多特定问题提供了良好的解决方案,但有时当需要像双曲正切这样的函数时,必须采用其他方法,如归一化,以减轻这种现象。我们将在本章和下一章讨论一些具体技术,但一个通用的最佳实践是始终使用归一化数据集,并在必要时也测试白化的效果。
随机梯度下降
一旦计算出了梯度,成本函数就可以移动到其最小值的方向。然而,在实践中,在评估了一定数量的训练样本(批次)之后进行更新会更好。确实,通常使用的算法不会计算整个数据集的全局成本,因为这个操作可能非常计算量大。通过部分步骤获得近似值,这些步骤仅限于通过评估小部分数据集积累的经验。根据一些文献,随机梯度下降(SGD)应该只在每次更新后对每个单独的样本执行时使用。当这个操作在每k个样本上执行时,该算法也被称为小批量梯度下降;然而,传统上 SGD 指的是包含k ≥ 1个样本的所有批次,并且我们现在将使用这个表达式。
可以通过考虑使用包含k个样本的批次计算的部分成本函数来表示这个过程:
算法通过根据以下规则更新权重来执行梯度下降:
如果我们从初始配置θ[start]开始,随机梯度下降过程可以想象成以下图中显示的路径:
权重被移动到最小 θ[opt,],随后会有许多后续的修正,考虑到整个数据集,这些修正也可能出错。因此,这个过程必须重复多次(epoch),直到验证准确率达到最大。在理想场景下,具有凸成本函数 L,这个简单的程序会收敛到最优配置。不幸的是,深度网络是一个非常复杂且非凸的函数,其中平台和鞍点相当常见(参见第一章,机器学习模型基础)。在这种情况下,普通的 SGD 不会找到全局最优解,在许多情况下甚至找不到接近的点。例如,在平坦区域,梯度可以变得非常小(也考虑到数值不精确),以至于会减慢训练过程,直到没有变化可能(所以 θ^((t+1)) ≈ θ^((t)))。在下一节中,我们将介绍一些常见的强大算法,这些算法已被开发出来以减轻这个问题并显著加速深度模型的收敛。
在继续之前,标记两个重要元素是很重要的。第一个与学习率η有关。这个超参数在学习过程中起着根本的作用。如图所示,算法从一个点跳到另一个点(这并不一定更接近最优解)。与优化算法一起,正确调整学习率至关重要。高值(如 1.0)可能会使权重变化过快,增加不稳定性。特别是,如果一批数据包含一些异常值(或简单地是非主导样本),大的η会将它们视为代表性元素,通过调整权重以最小化误差。然而,后续批次可能更好地代表数据生成过程,因此,算法必须部分撤销其修改以补偿错误的更新。因此,学习率通常很小,常见值介于 0.0001 和 0.01 之间(在某些特定情况下,η = 0.1也可以是一个有效的选择)。另一方面,非常小的学习率会导致最小的修正,减慢训练过程。一个好的权衡,通常是最佳实践,是让学习率随着 epoch 的变化而衰减。一开始,η可以更高,因为接近最优解的概率几乎为零;因此,更大的跳跃可以轻松调整。当训练过程继续进行时,权重逐渐移动到它们的最终配置,因此,修正变得越来越小。在这种情况下,应避免大的跳跃,而偏好微调。这就是为什么学习率会衰减。常见的技术包括指数衰减或线性衰减。在这两种情况下,初始值和最终值必须根据具体问题(测试不同的配置)和优化算法来选择。在许多情况下,起始值和结束值之间的比率约为 10 或更大。
另一个重要的超参数是批量大小。没有银弹能让我们自动做出正确的选择,但我们可以考虑一些因素。由于 SGD 是一个近似算法,较大的批量会导致更接近整个数据集得到的校正。然而,当样本数量极高时,我们并不期望深度模型能够将它们一一对应,相反,我们的努力是致力于提高泛化能力。这一特性也可以重新表述为,网络必须学习更少的抽象,并重复使用它们来对新样本进行分类。如果正确采样,一个批量将包含这些抽象元素的一部分,并且自动校正的一部分会提高后续批量的评估。你可以想象一个瀑布过程,其中新的训练步骤永远不会从头开始。然而,该算法也被称为小批量梯度下降,因为通常的批量大小通常在 16 到 512 之间(较大的尺寸不常见,但总是可能的),这些值小于总样本数(特别是在深度学习环境中)。一个合理的默认值可能是 32 个样本,但我总是建议读者测试更大的值,比较训练速度和最终准确性的性能。
当与深度神经网络一起工作时,所有的值(例如,一个层中的神经元数量、批量大小等)通常是 2 的幂。这并不是一个约束,而只是一个优化提示(尤其是在使用 GPU 时),因为当块基于2^N元素时,内存可以更有效地被填充。然而,这只是一个建议,其好处也可能是微不足道的;所以,不要害怕测试具有不同值的架构。例如,在许多论文中,批量大小是 100,或者某些层有 1,000 个神经元。
权重初始化
一个非常重要的元素是神经网络的初始配置。权重应该如何初始化?让我们假设我们将它们都设置为 0。由于一个层中的所有神经元都接收相同的输入,如果权重是 0(或任何其他常见的常数),输出将是相等的。当应用梯度校正时,所有神经元都将被同等对待;因此,网络相当于一系列的单神经元层。很明显,初始权重必须不同才能实现一个称为对称破缺的目标,但最好的选择是什么?
如果我们(也大致地)知道最终的配置,我们就可以在几次迭代中轻松地将它们设置到最优点,但不幸的是,我们不知道最小值在哪里。因此,已经开发并测试了一些经验策略,目标是尽量减少训练时间(获得最先进的准确率)。一个普遍的规则是,权重应该很小(与输入样本方差相比)。大值会导致大的输出,这会负面影响饱和函数(如tanh
和sigmoid
),而小值则更容易优化,因为相应的梯度更大,校正效果更强。对于整流器单元也是如此,因为最大效率是在穿过原点的区间内工作(非线性实际上就位于那里)。例如,当处理图像时,如果值是正的且很大,ReLU 神经元几乎变成了线性单元,失去了很多优势(这就是为什么图像要归一化,以便将每个像素值限制在 0 到 1 或-1 到 1 之间)。
同时,理想情况下,激活方差应该在整个网络中保持几乎恒定,以及在每个反向传播步骤之后的权重方差。这两个条件对于改善收敛过程和避免梯度消失和梯度爆炸问题(后者是梯度消失的对立面,将在关于循环网络架构的章节中讨论)是基本的。
一种非常常见的策略是考虑层中的神经元数量,并按以下方式初始化权重:
这种方法被称为方差缩放,可以使用输入单元数(Fan-In)、输出单元数(Fan-Out)或它们的平均值来应用。这个想法非常直观:如果输入或输出的连接数很大,权重必须更小,以避免大的输出。在单神经元退化的情况下,方差设置为1.0
,这是允许的最大值(通常,所有方法都将偏差的初始值保持为 0.0,因为不需要用随机值初始化它们)。
还提出了其他变体,尽管它们都共享相同的基本思想。LeCun提出了以下初始化权重的建议:
另一种称为Xavier 初始化(在Understanding the difficulty of training deep feedforward neural networks, Glorot X., Bengio Y., Proceedings of the 13th International Conference on Artificial Intelligence and Statistics)的方法与LeCun 初始化类似,但它基于两个连续层的单元数的平均值(为了标记顺序,我们将 Fan-In 和 Fan-Out 术语替换为显式索引):
这是一个更稳健的变体,因为它考虑了输入连接和输出连接(反过来也是输入连接)。目标(作者在上述论文中广泛讨论)是试图满足之前提出的两个要求。第一个要求是避免每层激活方差中的振荡(理想情况下,这个条件可以避免饱和)。第二个要求与反向传播算法严格相关,其基于观察,当采用方差缩放(或等效的均匀分布)时,权重矩阵的方差与3n[k]的倒数成正比。
因此,Fan-In 和 Fan-Out 的平均值乘以三,试图避免更新后权重的大幅变化。Xavier 初始化在许多深度架构中已被证明非常有效,并且通常是默认选择。
其他方法基于在正向传播和反向传播阶段以不同方式测量方差,并尝试校正这些值以最小化特定环境中的残差振荡。例如,He、Zhang、Ren 和 Sun(在 Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification, He K., Zhang X., Ren S., Sun J., arXiv:1502.01852 [cs.CV]) 基于 ReLU 或可变 Leaky-ReLU 激活(也称为 PReLU,参数化 ReLU)分析了卷积网络(我们将在下一章讨论)的初始化问题,推导出一个最优标准(通常称为He 初始化器),它与 Xavier 初始化器略有不同:
所有这些方法都共享一些共同原则,并且在许多情况下可以互换。如前所述,Xavier 是最稳健的之一,在大多数现实问题中,没有必要寻找其他方法;然而,读者应始终意识到,深度模型的复杂性必须经常使用基于有时简单数学假设的经验方法来面对。只有通过真实数据集的验证,才能确认一个假设是正确的,或者是否需要继续在其他方向上进行研究。
Keras 中的 MLP 示例
Keras(keras.io
)是一个强大的 Python 工具包,它允许以最小的努力建模和训练复杂的深度学习架构。它依赖于底层框架,如 Tensorflow、Theano 或 CNTK,并提供高级块来构建模型的单个层。在这本书中,我们需要非常实用,因为没有空间进行完整的解释;然而,所有示例都将结构化,以便读者在没有全面知识的情况下尝试不同的配置和选项(对于更详细的信息,我建议阅读书籍《使用 Keras 的深度学习》,作者 A. Gulli 和 S. Pal,Packt 出版社)。
在这个示例中,我们想要构建一个包含单个隐藏层的小型 MLP 来解决 XOR 问题(数据集与上一个示例中创建的相同)。最简单和最常见的方法是实例化Sequential
类,它定义了一个空容器,用于不定型的模型。在这个初始部分,基本方法是add()
,它允许向模型添加一个层。对于我们的示例,我们想要使用四个具有双曲正切激活的隐藏层和两个 softmax 输出层。以下代码片段定义了 MLP:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(4, input_dim=2))
model.add(Activation('tanh'))
model.add(Dense(2))
model.add(Activation('softmax'))
Dense
类定义了一个全连接层(一个经典的多层感知器层),第一个参数用于声明所需的单元数。第一层必须声明input_shape
或input_dim
,它们指定单个样本的维度(或形状)(批处理大小被省略,因为它由框架动态设置)。所有后续层都会自动计算维度。Keras 的一个优点是能够避免设置许多参数(如权重初始化器),因为它们将自动使用最合适的默认值进行配置(例如,默认权重初始化器是 Xavier)。在接下来的示例中,我们将明确设置其中的一些,但我建议读者查阅官方文档,以了解所有可能性和功能。另一个参与此实验的层是Activation
,它指定所需的激活函数(也可以通过几乎所有层实现的参数activation
来声明,但我更喜欢解耦操作以强调单一角色,并且也因为一些技术——例如批量归一化——通常在激活之前应用于线性输出)。
在这个阶段,我们必须要求 Keras 编译模型(使用首选的后端):
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
参数optimizer
定义了我们想要使用的随机梯度下降算法。使用optimizer='sgd'
,可以实现标准版本(如前一段所述)。在这种情况下,我们使用的是 Adam(默认参数),这是一个性能更优的变体,将在下一节讨论。参数loss
用于定义成本函数(在这种情况下,是交叉熵),而metrics
是一个包含我们想要计算的评估分数的列表(对于许多分类任务,'accuracy'
就足够了)。一旦模型被编译,就可以对其进行训练:
from keras.utils import to_categorical
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=1000)
model.fit(X_train,
to_categorical(Y_train, num_classes=2),
epochs=100,
batch_size=32,
validation_data=(X_test, to_categorical(Y_test, num_classes=2)))
Train on 700 samples, validate on 300 samples
Epoch 1/100
700/700 [==============================] - 1s 2ms/step - loss: 0.7227 - acc: 0.4929 - val_loss: 0.6943 - val_acc: 0.5933
Epoch 2/100
700/700 [==============================] - 0s 267us/step - loss: 0.7037 - acc: 0.5371 - val_loss: 0.6801 - val_acc: 0.6100
Epoch 3/100
700/700 [==============================] - 0s 247us/step - loss: 0.6875 - acc: 0.5871 - val_loss: 0.6675 - val_acc: 0.6733
...
Epoch 98/100
700/700 [==============================] - 0s 236us/step - loss: 0.0385 - acc: 0.9986 - val_loss: 0.0361 - val_acc: 1.0000
Epoch 99/100
700/700 [==============================] - 0s 261us/step - loss: 0.0378 - acc: 0.9986 - val_loss: 0.0355 - val_acc: 1.0000
Epoch 100/100
700/700 [==============================] - 0s 250us/step - loss: 0.0371 - acc: 0.9986 - val_loss: 0.0347 - val_acc: 1.0000
操作相当简单。我们将数据集分为训练集和测试/验证集(在深度学习中,很少使用交叉验证),然后我们设置了batch_size=32
和epochs=100
来训练模型。在每个 epoch 的开始,数据集会自动打乱,除非设置shuffle=False
。为了将离散标签转换为 one-hot 编码,我们使用了to_categorical
实用函数。在这种情况下,标签 0 变为(1, 0),标签 1 变为(0, 1)。模型在达到 100 个 epoch 之前就收敛了;因此,我邀请读者将参数优化作为练习。然而,在过程结束时,训练准确率约为 0.999,验证准确率为 1.0。
最终的分类图如下所示:
XOR 数据集的 MLP 分类
只有三个点被错误分类,但很明显,MLP 成功地将 XOR 数据集分离。为了确认泛化能力,我们绘制了双曲正切隐藏层和 ReLU 隐藏层的决策表面:
使用 Tanh(左侧)和 ReLU(右侧)隐藏层的 MLP 决策表面
在这两种情况下,MLPs 都以合理的方式划定了区域。然而,虽然tanh
隐藏层似乎过度拟合(在我们的案例中并非如此,因为数据集正好代表了数据生成过程),ReLU 层生成的边界则不够平滑,方差明显较低(特别是对于考虑类中的异常值)。我们知道最终的验证准确率证实了几乎完美的拟合,决策图(在两个维度上容易创建)在两种情况下都显示了可接受的边界,但这个简单的练习有助于理解深度模型的复杂性和敏感性。因此,绝对有必要选择一个有效的训练集(代表真实情况)并采用所有可能的技巧来避免过度拟合(正如我们稍后将要讨论的)。检测这种情况的最简单方法就是检查验证损失。一个好的模型应该在每个 epoch 后减少训练和验证损失,后者的损失达到平台期。如果在n个 epoch 后,验证损失(以及随之而来的准确率)开始增加,而训练损失继续下降,这意味着模型正在过度拟合训练集。
另一个表明训练过程正在正确演变的经验指标是,至少在开始时,验证准确率应该高于训练准确率。这看起来可能有些奇怪,但我们需要考虑验证集比训练集略小且复杂度更低;因此,如果模型的容量没有因训练样本而饱和,训练集的误分类概率将高于验证集。当这种趋势逆转时,模型在经过几个 epoch 后很可能过度拟合。为了验证这些概念,我邀请读者使用大量隐藏神经元(以显著增加容量)重复此练习,但在处理更加复杂和无结构的数据集时,这些概念将更加清晰。
可以使用命令pip install -U keras
安装 Keras。默认框架是支持 CPU 的 Theano。为了使用其他框架(如 Tensorflow GPU),我建议阅读主页上报告的说明keras.io
。正如作者所建议的,最好的后端是 Tensorflow,它适用于 Linux、Mac OSX 和 Windows。要安装它(以及所有依赖项),请遵循以下页面上的说明:www.tensorflow.org/install/
优化算法
在讨论反向传播算法时,我们展示了如何将 SGD 策略轻松地应用于训练大型数据集的深度网络。这种方法相当稳健且有效;然而,要优化的函数通常是非凸的,参数数量极其庞大。这些条件大大增加了找到鞍点(而不是局部最小值)的概率,当表面几乎平坦时,这可能会减慢训练过程。
将 vanilla SGD 算法应用于这些系统的常见结果如下所示:
算法不是达到最优配置 θ[opt],而是达到次优参数配置 θ[subopt,],并失去了执行进一步修正的能力。为了减轻所有这些问题及其后果,已经提出了许多 SGD 优化算法,目的是加快收敛速度(即使在梯度变得非常小的情况下)并避免不良条件系统的稳定性问题。
梯度扰动
当超曲面平坦(平台期)时,梯度接近零,这会出现一个常见问题。缓解这个问题的一个非常简单的方法是在梯度中添加一个小的同质噪声成分:
协方差矩阵通常是正交的,所有元素都设置为 σ²(t),这个值在训练过程中会衰减,以避免在修正非常小的时候产生扰动。这个方法在概念上是合理的,但当噪声成分占主导地位时,其隐含的随机性可能会产生不期望的效果。由于在深度模型中很难调整方差,因此已经提出了其他(更确定性的)策略。
动量和 Nesterov 动量
当遇到平台期时,一种更稳健的方法是基于动量的想法(类似于物理动量)。更正式地说,动量是通过使用后续梯度估计的加权移动平均而不是瞬时值来获得的:
新向量v^((t),)包含一个基于过去历史(并使用参数μ加权,μ是一个遗忘因子)的分量,以及一个与当前梯度估计相关的项(乘以学习率)。采用这种方法,突兀的变化变得更加困难,当探索离开斜坡区域进入平台期时,动量不会立即变为零(但与μ成比例的一段时间内),部分先前梯度将被保留,这使得穿越平坦区域成为可能。分配给超参数μ的值通常介于 0 和 1 之间。直观地说,较小的值意味着短期记忆,因为第一项衰减非常快,而接近 1.0(例如,0.9)的值允许更长的记忆,受局部振荡的影响较小。像许多其他超参数一样,μ需要根据具体问题进行调整,考虑到高动量并不总是最佳选择。当需要非常小的调整时,高值可能会减慢收敛速度,但与此同时,接近 0.0 的值通常无效,因为记忆贡献衰减得太早。使用动量,更新规则变为如下:
Nesterov 动量提供了一种变体,它基于 Nesterov 在数学优化领域的成果,这些成果已被证明可以加速许多算法的收敛。其思路是根据当前动量确定一个临时参数更新,然后将梯度应用到这个向量上以确定下一个动量(可以解释为一种前瞻性梯度评估,旨在减轻考虑每个参数移动历史时的错误校正风险):
该算法在几个深度模型中显示出性能提升;然而,由于其使用仍然有限,因为接下来的算法很快超过了标准的带有动量的 SGD,并且它们几乎成为任何实际任务的首选。
Keras 中的带有动量的 SGD
当使用 Keras 时,可以通过直接实例化SGD
类并在编译模型时使用它来自定义 SGD 优化器:
from keras.optimizers import SGD
...
sgd = SGD(lr=0.0001, momentum=0.8, nesterov=True)
model.compile(optimizer=sgd,
loss='categorical_crossentropy',
metrics=['accuracy'])
SGD
类接受参数lr
(学习率η,默认设置为0.01
),momentum
(参数μ),nesterov
(一个布尔值,表示是否采用 Nesterov 动量),以及一个可选的decay
参数,以指示学习率是否需要在更新过程中衰减,使用以下公式:
RMSProp
RMSProp是由 Hinton 提出的自适应算法,部分基于动量的概念。它不是考虑整个梯度向量,而是试图分别优化每个参数,以增加缓慢变化的权重的校正(可能需要更剧烈的修改)并减少快速变化的更新幅度(通常是更不稳定的)。该算法计算每个参数的变化速度的指数加权移动平均,考虑梯度的平方(对符号不敏感):
然后按照以下方式执行权重更新:
参数δ是一个小的常数(例如 10^(-6)),在变化速度变为零时添加以避免数值不稳定性。前面的表达式可以以更紧凑的方式重写:
使用这种表示法,可以清楚地看出 RMSProp 的作用是调整每个参数的学习率,以便在必要时增加它(几乎冻结的权重)并在振荡风险更高时降低它。在实际实现中,学习率总是通过指数或线性函数在 epoch 中衰减。
RMSProp 与 Keras
以下代码片段展示了在 Keras 中使用 RMSProp 的方法:
from keras.optimizers import RMSprop
...
rms_prop = RMSprop(lr=0.0001, rho=0.8, epsilon=1e-6, decay=1e-2)
model.compile(optimizer=rms_prop,
loss='categorical_crossentropy',
metrics=['accuracy'])
学习率和衰减与 SGD 相同。参数rho
对应于指数移动平均权重μ,而epsilon
是添加到变化速度中的常数,以提高稳定性。与任何其他算法一样,如果用户想使用默认值,可以在不实例化类的情况下声明优化器(例如,optimizer='rmsprop'
)。
Adam
Adam(自适应矩估计的缩写)是由 Kingma 和 Ba(在*Adam: A Method for Stochastic Optimization, Kingma D. P., Ba J., arXiv:1412.6980 [cs.LG]**)提出的算法,旨在进一步提高 RMSProp 的性能。该算法通过计算每个参数的梯度和其平方的指数加权平均来确定自适应学习率:
在上述论文中,作者建议通过除以1 - μ[i]来消除两个估计(涉及第一和第二矩)的偏差,因此新的移动平均变为以下形式:
Adam 的权重更新规则如下:
分析前面的表达式,可以理解为什么这个算法通常被称为带有动量的 RMSProp。实际上,项 g(•) 就像标准的动量一样,计算每个参数的梯度移动平均值(具有此过程的全部优点),而分母则充当具有与 RMSProp 相同精确语义的自适应项。因此,Adam 非常经常是应用最广泛的算法之一,尽管在许多复杂任务中,其性能与标准 RMSProp 相当。选择必须考虑到由于存在两个遗忘因子而带来的额外复杂性。一般来说,默认值(0.9)是可以接受的,但有时在决定特定配置之前进行几个场景的分析会更好。另一个需要记住的重要元素是,所有基于动量的方法都可能导致训练某些深度架构时的不稳定性(振荡)。这就是为什么 RMSProp 几乎在所有研究论文中都广泛使用;然而,不要将此声明视为限制,因为 Adam 在许多任务中已经显示出卓越的性能。记住,当训练过程似乎不稳定,即使学习率较低时,最好使用不基于动量的方法(惯性项实际上会减慢避免振荡所需的快速修改)。
使用 Keras 的 Adam
以下代码片段展示了如何在使用 Keras 时使用 Adam:
from keras.optimizers import Adam
...
adam = Adam(lr=0.0001, beta_1=0.9, beta_2=0.9, epsilon=1e-6, decay=1e-2)
model.compile(optimizer=adam,
loss='categorical_crossentropy',
metrics=['accuracy'])
遗忘因子 μ[1] 和 μ[2,] 分别由参数 beta_1
和 beta_2
表示。所有其他元素与其他算法相同。
AdaGrad
该算法由 Duchi、Hazan 和 Singer 提出(在 Adaptive Subgradient Methods for Online Learning and Stochastic Optimization, Duchi J., Hazan E., Singer Y., Journal of Machine Learning Research 12/2011)。这个想法与 RMSProp 非常相似,但在这个情况下,考虑了平方梯度的整个历史:
权重的更新方式与 RMSProp 完全相同:
然而,由于平方梯度是非负的,当 t → ∞ 时,隐含的和 v^((t))(•) → ∞。由于增长会持续到梯度非零,所以在训练过程中无法保持贡献的稳定性。通常在开始时效果非常明显,但在有限的几个 epoch 之后就会消失,导致学习率为零。AdaGrad 在 epoch 数量非常有限时仍然是一个强大的算法,但它不能成为大多数深度模型的首选解决方案(下一个算法已经被提出以解决这个问题)。
使用 Keras 的 AdaGrad
以下代码片段展示了在使用 Keras 时使用 AdaGrad 的用法:
from keras.optimizers import Adagrad
...
adagrad = Adagrad(lr=0.0001, epsilon=1e-6, decay=1e-2)
model.compile(optimizer=adagrad,
loss='categorical_crossentropy',
metrics=['accuracy'])
AdaGrad 的实现没有其他参数,只有常见的那些。
AdaDelta
AdaDelta 是一个算法(由 Zeiler M. D. 提出,发表在 ADADELTA: An Adaptive Learning Rate Method, Zeiler M. D., arXiv:1212.5701 [cs.LG]),旨在解决 AdaGrad 的主要问题,即管理整个平方梯度历史。首先,AdaDelta 不是使用累加器,而是使用指数加权移动平均,类似于 RMSProp:
然而,与 RMSProp 的主要区别在于对更新规则的分析。当我们考虑操作 x + Δx 时,我们假设这两个项具有相同的单位;然而,作者注意到,使用 RMSProp(以及 AdaGrad)获得的自适应学习率 η(θ[i]) 是无单位的(而不是具有 θ[i] 的单位)。事实上,由于梯度被分解为可以近似为 ΔL/Δθ[i] 的偏导数,并且假设成本函数 L 是无单位的,我们得到以下关系:
因此,Zeiler 提出应用一个与每个权重单位 θ[i] 成比例的校正项。这个因子是通过考虑每个平方差的指数加权移动平均得到的:
因此,更新的规则因此变为如下:
这种方法确实与 RMSProp 更相似,但两种算法之间的界限非常微妙,尤其是在历史记录仅限于有限滑动窗口的情况下。AdaDelta 是一个强大的算法,但它只能在非常特定的任务中优于 Adam 或 RMSProp。我的建议是采用一种方法,并且在转向另一种方法之前,尝试优化超参数,直到准确率达到最大。如果性能持续不佳,并且模型无法以任何其他方式改进,那么测试其他优化算法是一个好主意。
AdaDelta 与 Keras
以下代码片段展示了在 Keras 中使用 AdaDelta 的用法:
from keras.optimizers import Adadelta
...
adadelta = Adadelta(lr=0.0001, rho=0.9, epsilon=1e-6, decay=1e-2)
model.compile(optimizer=adadelta,
loss='categorical_crossentropy',
metrics=['accuracy'])
忘记因子,μ,由参数 rho
表示。
正则化和 dropout
过度拟合是深度模型中常见的问题。即使数据集非常大,它们的极高容量也可能成为问题,因为学习训练集结构的能力并不总是与泛化能力相关。深度神经网络可以轻易地成为一个联想记忆,但最终的内部配置可能并不是最适合管理属于同一分布但从未在训练过程中出现过的样本。不言而喻,这种行为与分离超面的复杂性成正比。线性分类器过度拟合的可能性最小,而多项式分类器则极其容易过度拟合。数百、数千或更多非线性函数的组合产生一个分离超面,这超出了任何可能的分析。1991 年,Hornik 在《多层前馈网络的逼近能力》(Hornik K., Neural Networks, 4/2)中推广了数学家 Cybenko 两年前获得的一个非常重要的结果(发表在《通过 Sigmoid 函数的叠加逼近》(Cybenko G., Mathematics of Control, Signals, and Systems, 2 /4)上)。无需任何数学细节(尽管这并不复杂),该定理表明 MLP(不是最复杂的架构!)可以逼近在ℜ^n的紧子集上连续的任何函数。很明显,这样的结果形式化了几乎所有研究人员已经直觉上知道的事情,但其力量超越了最初的影响,因为 MLP 是一个有限系统(而不是数学级数),并且该定理假设有限层数和神经元数。显然,精度与复杂性成正比;然而,对于几乎所有问题都没有不可接受的限制。然而,我们的目标不是学习现有的连续函数,而是管理从未知数据生成过程中抽取的样本,目的是在新的样本出现时最大化准确性。无法保证函数是连续的,或者域是紧子集。
在第一章《机器学习模型基础》中,我们介绍了基于略微修改的成本函数的主要正则化技术:
额外的项 g(θ) 是权重(例如 L2 范数)的非负函数,它迫使优化过程尽可能保持参数尽可能小。当与饱和函数(例如 tanh
)一起工作时,基于 L2 范数的正则化方法试图限制函数的操作范围在线性部分,从而实际上减少了其容量。当然,最终的配置不会是最佳配置(这可能是一个过拟合模型的产物),而是在训练和验证准确率(或者说是偏差和方差)之间的次优权衡。一个偏差接近 0(且训练准确率接近 1.0)的系统在分类中可能会非常僵硬,只有在样本与训练过程中评估的样本非常相似时才能成功。这就是为什么在考虑使用新样本获得的优势时,这种 代价 通常会被支付。L2 正则化可以与任何类型的激活函数一起使用,但效果可能不同。例如,当权重非常大时,ReLU 单元有更大的概率变得线性(或始终为空)。试图将它们保持在 0.0 附近意味着迫使函数利用其非线性,而不存在产生极端大输出的风险(这可能会对非常深的架构产生负面影响)。有时,这种结果可能更有用,因为它允许以更平滑的方式训练更大的模型,并获得更好的最终性能。一般来说,几乎不可能在没有进行多次测试的情况下决定正则化是否可以改进结果,但有一些场景中,引入 dropout(我们将在下一段讨论这种方法)并调整其超参数是非常常见的。这更多的是一种经验选择,而不是精确的架构决策,因为许多现实生活中的例子(包括最先进的模型)通过使用这种正则化技术获得了出色的结果。我建议读者在选择特定解决方案之前,对模型进行理性怀疑和双重检查。有时,一个性能极高的网络在选择了不同的(但类似)数据集后,可能会变得无效。这就是为什么测试不同的替代方案可以为解决特定问题类别提供最佳体验。
在继续之前,我想展示如何使用 Keras 实现一个 L1(用于强制稀疏性)、L2 或 ElasticNet(L1 和 L2 的组合)正则化。该框架提供了一种细粒度方法,允许对每个层施加不同的约束。例如,以下代码片段展示了如何将强度参数设置为 0.05
的 l2
约束添加到一个通用的全连接层:
from keras.layers import Dense
from keras.regularizers import l2
...
model.add(Dense(128, kernel_regularizer=l2(0.05)))
keras.regularizers
包包含l1()
、l2()
和l1_l2()
函数,这些函数可以应用于Dense
和卷积层(我们将在下一章中讨论它们)。这些层允许我们对权重(kernel_regularizer
)、偏差(bias_regularizer
)和激活输出(activation_regularizer
)施加正则化,尽管第一个通常是最广泛使用的。
或者,也可以对权重和偏差施加特定的约束,以更选择性的方式。以下代码片段展示了如何设置一个层的权重最大范数(等于1.5
):
from keras.layers import Dense
from keras.constraints import maxnorm
...
model.add(Dense(128, kernel_constraint=maxnorm(1.5)))
Keras 的keras.constraints
包提供了一些函数,可以用来对权重或偏差施加最大范数maxnorm()
,沿轴的单位范数unit_norm()
,非负性non_neg()
,以及范数的上下限min_max_norm()
。这种方法与正则化的区别在于它仅在必要时应用。考虑到前面的例子,施加 L2 正则化始终有效,而最大范数约束在值低于预定义阈值之前是无效的。
Dropout
这种方法是由 Hinton 及其同事(在《通过防止特征检测器的共适应改进神经网络,Hinton G. E.,Srivastava N.,Krizhevsky A.,Sutskever I.,Salakhutdinov R. R.,arXiv:1207.0580 [cs.NE]》中提出)作为防止过拟合和允许更大网络探索样本空间更多区域的替代方案。这个想法相当简单——在每一步训练中,给定一个预定义的百分比 n[d],一个dropout层随机选择 n[d]N 个输入单元并将它们设置为 0.0
(该操作仅在训练阶段有效,当模型用于新预测时则完全移除)。
这种操作可以有多种解释。当使用更多的 dropout 层时,它们选择的结果是一个容量减少的子网络,它可能更难以过拟合训练集。许多训练子网络的交集构成一个隐式集成,其预测是所有模型平均的结果。如果 dropout 应用于输入层,它就像是一种弱数据增强,通过向样本添加随机噪声(将一些单元设置为零可能导致潜在的损坏模式)。同时,使用多个 dropout 层允许探索几个潜在配置,这些配置不断组合和优化。
这种策略显然是概率性的,结果可能会受到许多难以预料的因素的影响;然而,几个测试证实,当网络非常深时,使用 dropout 是一个不错的选择,因为由此产生的子网络具有残余能力,允许它们模拟大量样本,而不会使整个网络在训练集上过度拟合而“冻结”其配置。另一方面,当网络较浅或包含较少的神经元时(在这些情况下,L2 正则化可能是一个更好的选择),这种方法并不非常有效。
根据作者的说法,dropout 层应该与高学习率和权重上的最大范数约束一起使用。实际上,这样模型可以轻松地学习更多潜在的配置,这些配置在保持非常小的学习率时会被避免。然而,这并不是一个绝对规则,因为许多最先进的模型使用 dropout 与优化算法(如 RMSProp 或 Adam)一起使用,而不是过高的学习率。
dropout 的主要缺点是它会减慢训练过程,并可能导致不可接受的次优化。后者的问题可以通过调整丢弃单元的百分比来缓解,但通常很难完全解决这个问题。因此,一些新的图像识别模型(如残差网络)避免了 dropout,并采用更复杂的技巧来训练非常深的卷积网络,这些网络会过度拟合训练集和验证集。
Keras 中 dropout 的示例
我们无法用一个更具挑战性的分类问题来测试 dropout 的有效性。数据集是经典的 MNIST 手写数字数据集,但 Keras 允许下载并使用由 70,000 个(60,000 个训练和 10,000 个测试)28 × 28 灰度图像组成的原始版本。即使这不是最佳策略,因为卷积网络应该是处理图像的首选,我们仍想尝试将数字视为 784 维的展平数组来对它们进行分类。
第一步是加载和归一化数据集,使得每个值都成为一个介于 0 和 1 之间的浮点数:
import numpy as np
from keras.datasets import mnist
from keras.utils import to_categorical
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
width = height = X_train.shape[1]
X_train = X_train.reshape((X_train.shape[0], width * height)).astype(np.float32) / 255.0
X_test = X_test.reshape((X_test.shape[0], width * height)).astype(np.float32) / 255.0
Y_train = to_categorical(Y_train, num_classes=10)
Y_test = to_categorical(Y_test, num_classes=10)
在这一点上,我们可以开始测试一个没有 dropout 的模型。所有实验共有的结构是基于三个全连接 ReLU 层(2048-1024-1024),随后是一个有 10 个单位的 softmax 层。考虑到这个问题,我们可以尝试使用 Adam 优化器来训练模型,其中η = 0.0001,并且将衰减设置为10^(-6):
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.optimizers import Adam
model = Sequential()
model.add(Dense(2048, input_shape=(width * height, )))
model.add(Activation('relu'))
model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer=Adam(lr=0.0001, decay=1e-6),
loss='categorical_crossentropy',
metrics=['accuracy'])
模型以256
个样本的批次大小训练了200
个 epoch:
history = model.fit(X_train, Y_train,
epochs=200,
batch_size=256,
validation_data=(X_test, Y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/200
60000/60000 [==============================] - 11s 189us/step - loss: 0.4026 - acc: 0.8980 - val_loss: 0.1601 - val_acc: 0.9523
Epoch 2/200
60000/60000 [==============================] - 7s 116us/step - loss: 0.1338 - acc: 0.9621 - val_loss: 0.1062 - val_acc: 0.9669
Epoch 3/200
60000/60000 [==============================] - 7s 124us/step - loss: 0.0872 - acc: 0.9744 - val_loss: 0.0869 - val_acc: 0.9732
...
Epoch 199/200
60000/60000 [==============================] - 7s 114us/step - loss: 1.1935e-07 - acc: 1.0000 - val_loss: 0.1214 - val_acc: 0.9838
Epoch 200/200
60000/60000 [==============================] - 7s 116us/step - loss: 1.1935e-07 - acc: 1.0000 - val_loss: 0.1214 - val_acc: 0.9840
即使没有进一步的分析,我们也可以立即注意到模型过拟合了。在 200 个 epoch 后,训练准确率为 1.0,损失接近 0.0,而验证准确率合理,但验证损失略低于第二个 epoch 结束时的损失。
为了更好地理解发生了什么,在训练过程中绘制准确率和损失是有用的:
如您所见,验证损失在最初的 10 个 epoch 达到了最小值,并立即开始增长(由于其形状有时被称为 U 曲线)。在同一时刻,训练准确率达到了 1.0。从那个 epoch 开始,模型开始过拟合,学习训练集的完美结构,但失去了泛化能力。实际上,即使最终的验证准确率相当高,损失函数表明当呈现新的样本时,缺乏鲁棒性。由于损失是分类交叉熵,结果可以解释为模型学习了一个部分不匹配验证集的分布。由于我们的目标是使用模型来预测新的样本,这种配置是不可接受的。因此,我们再次尝试,使用一些 dropout 层。正如作者所建议的,我们还增加了学习率到 0.1(切换到动量 SGD 优化器以避免由于 RMSProp 或 Adam 的自适应性导致的爆炸),使用均匀分布(-0.05, 0.05)初始化权重,并施加最大范数约束,设置为 2.0。这个选择允许探索更多的子配置,而不存在权重过高的风险。dropout 应用于 25%的输入单元和所有 ReLU 全连接层,百分比设置为 50%:
from keras.constraints import maxnorm
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.optimizers import SGD
model = Sequential()
model.add(Dropout(0.25, input_shape=(width * height, ), seed=1000))
model.add(Dense(2048, kernel_initializer='uniform', kernel_constraint=maxnorm(2.0)))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(Dense(1024, kernel_initializer='uniform', kernel_constraint=maxnorm(2.0)))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(Dense(1024, kernel_initializer='uniform', kernel_constraint=maxnorm(2.0)))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer=SGD(lr=0.1, momentum=0.9),
loss='categorical_crossentropy',
metrics=['accuracy'])
训练过程使用相同的参数进行:
history = model.fit(X_train, Y_train,
epochs=200,
batch_size=256,
validation_data=(X_test, Y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/200
60000/60000 [==============================] - 11s 189us/step - loss: 0.4964 - acc: 0.8396 - val_loss: 0.1592 - val_acc: 0.9511
Epoch 2/200
60000/60000 [==============================] - 6s 97us/step - loss: 0.2300 - acc: 0.9300 - val_loss: 0.1081 - val_acc: 0.9645
Epoch 3/200
60000/60000 [==============================] - 6s 93us/step - loss: 0.1867 - acc: 0.9435 - val_loss: 0.0941 - val_acc: 0.9713
...
Epoch 199/200
60000/60000 [==============================] - 6s 99us/step - loss: 0.0184 - acc: 0.9939 - val_loss: 0.0473 - val_acc: 0.9884
Epoch 200/200
60000/60000 [==============================] - 6s 101us/step - loss: 0.0190 - acc: 0.9941 - val_loss: 0.0484 - val_acc: 0.9883
最终条件发生了戏剧性的变化。模型不再过拟合(即使有可能通过提高验证准确率来改进它)并且验证损失低于初始值。为了确认这一点,让我们分析准确率/损失图:
结果显示了一些不完美之处,因为验证损失在许多 epoch 中几乎呈平坦状态;然而,具有更高学习率和较弱算法的相同模型实现了更好的最终性能(0.988 验证准确率)和更优越的泛化能力。最先进的模型也可以达到 0.995 的验证准确率,但我们的目标是展示 dropout 层在防止过拟合以及产生对新的样本或噪声样本具有更多鲁棒性的最终配置的效果。我邀请读者用不同的参数、更大或更小的网络和其他优化算法重复实验,以进一步降低最终的验证损失。
Keras 还实现了两个额外的 dropout 层:GaussianDropout
,它将输入样本乘以高斯噪声:
常数ρ的值可以通过参数rate
(介于 0 和 1 之间)来设置。当ρ → 1时,σ² → ∞,而较小的值会导致n ≈ 1时产生零效果。这一层作为输入一,可以非常有用,以便模拟随机数据增强过程。另一类是AlphaDropout
,它的工作方式与前面类似,但重新归一化输出以保持原始均值和方差(这种效果与在下一段中描述的技术结合噪声层获得的效果非常相似)。
当与概率层(如 dropout)一起工作时,我总是建议设置随机种子(当使用 Tensorflow 后端时,使用np.random.seed(...)
和tf.set_random_seed(...)
)。这样,就可以在没有偏差的情况下重复实验,比较结果。如果没有明确设置随机种子,每次新的训练过程都会不同,比较性能并不容易,例如,在固定数量的 epoch 之后。
批量归一化
让我们考虑一个包含k个样本的小批量:
在遍历网络之前,我们可以测量均值和方差:
在第一层之后(为了简单起见,让我们假设激活函数f(•)始终相同),批次被转换成以下形式:
通常,不能保证新的均值和方差相同。相反,很容易观察到在整个网络中逐渐增加的修改。这种现象被称为协变量偏移,它是由于每个层中需要的不同适应而导致的渐进式训练速度衰减的原因。Ioffe 和 Szegedy(在Ioffe S., Szegedy C., arXiv:1502.03167 [cs.LG]中)提出了一种减轻这种问题的方法,这种方法被称为批量归一化(BN)。
理念是重新归一化层的线性输出(在或之后应用激活函数之前),使得批次具有零均值和单位方差。因此,BN 层的第一个任务是计算:
然后,每个样本被转换成归一化版本(参数δ包含在内以提高数值稳定性):
然而,由于批量归一化除了加速训练过程外没有其他计算目的,转换必须始终是恒等变换(以避免扭曲和偏差数据);因此,实际输出将通过应用线性操作获得:
两个参数 α^((j)) 和 β^((j)) 是由 SGD 算法优化的变量;因此,每个变换都保证不会改变数据的尺度和位置。这些层仅在训练阶段活跃(类似于 dropout),但与其它算法不同,当模型用于对新样本进行预测时,它们不能简单地被丢弃,因为输出会持续偏置。为了避免这个问题,作者建议通过在批次上平均来近似 X 的均值和方差(假设有 N[b] 批次,每批有 k 个样本):
使用这些值,批归一化层可以转换为以下线性操作:
证明这个近似随着批次的增加变得越来越准确,误差通常是可忽略的,并不困难。然而,当批次大小非常小的时候,统计可能会相当不准确;因此,在使用这种方法时,应考虑批次的代表性。如果数据生成过程简单,即使是小批次也足以描述实际分布。相反,当p[data]更复杂时,批归一化需要更大的批次以避免错误的调整(一种可行的策略是比较全局均值和方差与通过采样一些批次计算出的值,并尝试设置最小化差异的批次大小)。然而,这个过程可以显著减少协变量偏移,并提高非常深层的网络(包括著名的残差网络)的收敛速度。此外,它允许使用更高的学习率,因为层被隐式地饱和,永远不会爆炸。此外,已经证明,即使批归一化对权重不起作用,它也有一个次要的正则化效果。原因与 L2 提出的类似,但在这个情况下,由于变换本身(部分由参数 α^((j)) 和 β**^((j))) 的变化性引起)存在一个残留效应,这可以鼓励探索样本空间的不同区域。然而,这不是主要效应,将此方法作为正则化器使用并不是一个好的实践。
Keras 中的批归一化示例
为了展示这种技术的特点,让我们重复之前的例子,使用一个没有 dropout 但在每个全连接层之后应用批归一化的 MLP。这个例子与第一个非常相似,但在这个情况下,我们将 Adam 学习率增加到 0.001,同时保持相同的衰减:
from keras.models import Sequential
from keras.layers import Dense, Activation, BatchNormalization
from keras.optimizers import Adam
model = Sequential()
model.add(Dense(2048, input_shape=(width * height, )))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dense(1024))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dense(1024))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dense(10))
model.add(BatchNormalization())
model.add(Activation('softmax'))
model.compile(optimizer=Adam(lr=0.001, decay=1e-6),
loss='categorical_crossentropy',
metrics=['accuracy'])
我们现在可以使用相同的参数再次进行训练:
history = model.fit(X_train, Y_train,
epochs=200,
batch_size=256,
validation_data=(X_test, Y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/200
60000/60000 [==============================] - 16s 274us/step - loss: 0.3848 - acc: 0.9558 - val_loss: 0.3338 - val_acc: 0.9736
Epoch 2/200
60000/60000 [==============================] - 8s 139us/step - loss: 0.1977 - acc: 0.9844 - val_loss: 0.1904 - val_acc: 0.9789
Epoch 3/200
60000/60000 [==============================] - 8s 137us/step - loss: 0.1292 - acc: 0.9903 - val_loss: 0.1397 - val_acc: 0.9835
...
Epoch 199/200
60000/60000 [==============================] - 8s 132us/step - loss: 4.7805e-05 - acc: 1.0000 - val_loss: 0.0599 - val_acc: 0.9877
Epoch 200/200
60000/60000 [==============================] - 8s 133us/step - loss: 2.6056e-05 - acc: 1.0000 - val_loss: 0.0593 - val_acc: 0.9879
模型再次过拟合,但现在最终的验证准确率仅略高于使用 dropout 层所达到的准确率。让我们绘制准确率和损失曲线以更好地分析训练过程:
批标准化(batch normalization)的效果提高了性能并减缓了过拟合。同时,消除协变量偏移避免了 U 型曲线,保持了相当低的验证损失。此外,模型在 135-140 个 epoch 期间达到了大约 0.99 的验证准确率,并呈现出残余的正趋势。与之前的例子类似,这个解决方案并不完美,但它是进一步优化的良好起点。继续进行更多 epoch 的训练过程,同时监控验证损失和准确率将是一个好主意。此外,可以混合使用 dropout 和批标准化,或者尝试 Keras 的 AlphaDropout 层。然而,如果在第一个例子(没有 dropout)中,训练准确率的峰值与验证损失的正趋势相关联,那么在这种情况下,学习到的分布似乎与验证集的分布没有太大区别。换句话说,批标准化并没有防止训练集过拟合,但它避免了泛化能力下降(在没有批标准化的情况下观察到)。我建议使用其他超参数和架构配置重复测试,以决定这个模型是否可用于预测目的,或者是否需要寻找其他解决方案。
摘要
在本章中,我们通过介绍引领第一代研究人员改进算法直至达到如今顶级结果的初步概念,开始了对深度学习世界的探索。第一部分解释了基本人工神经元结构,它结合了一个线性操作,随后是一个可选的非线性标量函数。最初提出单层线性神经元作为第一个神经网络,命名为感知器。
尽管这个模型在许多问题上都非常强大,但当与非线性可分的数据集一起工作时,它很快便显示了其局限性。感知器与逻辑回归并没有太大的区别,也没有具体的原因去使用它。尽管如此,这个模型为通过结合多个非线性层获得的一族极其强大的模型打开了大门。多层感知器已被证明是一个通用逼近器,能够处理几乎任何类型的数据集,在其它方法失败时也能实现高级别的性能。
在下一节中,我们分析了多层感知器(MLP)的构建块。我们从激活函数开始,描述了它们的结构和特性,并重点讨论了它们为何能针对特定问题做出选择。然后,我们讨论了训练过程,考虑了反向传播算法背后的基本思想以及如何使用随机梯度下降法实现它。尽管这种方法相当有效,但当网络复杂度非常高时,它可能会很慢。因此,提出了许多优化算法。在本章中,我们分析了动量(momentum)的作用以及如何使用 RMSProp 管理自适应校正。然后,我们将动量和 RMSProp 结合起来,推导出一个非常强大的算法,称为 Adam。为了提供一个完整的视角,我们还介绍了两种略有不同的自适应算法,称为 AdaGrad 和 AdaDelta。
在接下来的几节中,我们讨论了正则化方法以及它们如何集成到 Keras 模型中。一个重要的部分是关于一种非常普遍的技术——dropout,它通过随机选择将固定百分比的样本设置为零(丢弃)。这种方法虽然非常简单,但可以防止非常深层网络的过拟合,并鼓励探索样本空间的各个不同区域,得到的结果与第八章集成学习中分析的结果不太相似。最后一个主题是批量归一化技术,这是一种减少由后续神经网络变换引起的均值和方差偏移(称为协变量偏移)的方法。这种现象可能会减慢训练过程,因为每一层都需要不同的调整,并且更难将所有权重移动到最佳方向。应用批量归一化意味着非常深的网络可以在更短的时间内训练,这也要归功于可以使用更高的学习率。
在下一章中,我们将继续这一探索,分析非常重要的高级层,如卷积(在面向图像的任务中实现非凡的性能)和循环单元(用于处理时间序列),并讨论一些可以使用 Keras 和 Tensorflow 进行实验和重新调整的实际应用。
第十章:高级神经网络模型
在本章中,我们继续对深度学习世界的实用探索,分析两个非常重要的元素:深度卷积网络和循环神经网络(RNN)。前者代表了几乎所有目的的最准确和性能最好的视觉处理技术。在实时图像识别、自动驾驶汽车和深度强化学习等领域取得的成果都得益于这种网络的表达能力。另一方面,为了完全管理时间维度,有必要引入高级循环层,其性能必须优于任何其他回归方法。将这两种技术与上一章中讨论的所有元素结合起来,使得在视频处理、解码、分割和生成领域取得非凡的结果成为可能。
尤其是在本章中,我们将讨论以下主题:
-
深度卷积网络
-
卷积、扩张卷积、可分离卷积和转置卷积
-
池化和其他支持层
-
循环神经网络
-
LSTM 和 GRU 细胞
-
迁移学习
深度卷积网络
在上一章,第九章,《机器学习的神经网络》中,我们看到了多层感知器如何在与复杂但不是非常复杂的图像数据集(如 MNIST 手写数字数据集)一起工作时达到非常高的准确率。然而,由于全连接层是水平的,图像,通常是一维结构(宽度×高度×通道),必须被展平并转换成一维数组,其中几何属性最终丢失。对于更复杂的数据集,其中类别的区分依赖于更多的细节和它们之间的关系,这种方法可以产生适度的准确率,但它永远无法达到生产就绪应用所需的精度。
神经科学研究和图像处理技术的结合建议在神经网络中尝试,其中第一层处理二维结构(没有通道),试图提取严格依赖于图像几何属性的特征层次。事实上,正如关于视觉皮层的神经科学研究所证实的那样,人类不会直接解码图像。这个过程是连续的,首先通过检测低级元素,如线条和方向;然后,它通过关注定义越来越复杂形状、不同颜色、结构特征等的子属性来逐步进行,直到信息量足够解决任何可能的歧义(关于更详细的科学信息,我推荐阅读书籍《视觉与大脑:我们如何感知世界》,作者 J. V. Stone,MIT 出版社)。
例如,我们可以将眼睛的解码过程想象成一个由这些滤波器组成的序列(当然,这只是一个教学示例):方向(主导水平维度)、一个椭圆形状内部的中心圆圈、一个较暗的中心(瞳孔)和一个清晰的背景(灯泡),瞳孔中间的一个较小的较暗圆圈,眉毛的存在,等等。即使这个过程在生物学上不正确,它也可以被认为是一个合理的分层过程,其中在较低级别的滤波之后获得了一个较高级别的子特征。
这种方法是通过使用二维卷积算子综合而成的,它已经是一个众所周知的有力图像处理工具。然而,在这种情况下,有一个非常重要的区别:滤波器的结构不是预先设定的,而是由网络通过用于 MLP 的相同反向传播算法学习。这样,模型可以适应权重,考虑一个最终目标(即分类输出),而不考虑任何预处理步骤。实际上,深度卷积网络比 MLP 更基于端到端学习的概念,这是表达我们之前描述内容的一种不同方式。输入是源数据;在中间,有一个灵活的结构;在最后,我们定义一个全局成本函数,衡量分类的准确性。学习过程必须反向传播错误并修正权重以达到特定目标,但我们并不确切知道这个过程是如何工作的。我们能够轻松做到的是分析学习阶段结束时的滤波器结构,发现网络已经将第一层专门化于低级细节(如方向),而将最后几层专门化于高级、有时可识别的细节(如面部组件)。这样的模型在图像识别、分割(检测图像不同部分的边界)和跟踪(检测移动物体的位置)等任务上取得了最先进的性能,这并不令人惊讶。然而,深度卷积网络已成为许多不同架构(如深度强化学习或神经风格迁移)的第一块,即使存在一些已知的局限性,但仍然是解决许多复杂现实问题的首选。这种模型的主要缺点(也是一个常见的反对意见)是它们需要非常大的数据集才能达到高精度。所有最重要的模型都是用数百万张图片训练的,它们的泛化能力(即主要目标)与不同样本的数量成正比。有研究人员注意到,人类在没有这种大量经验的情况下学会泛化,在未来的几十年里,我们可能会在这个观点下观察到改进。然而,深度卷积网络已经彻底改变了许多人工智能领域,使得几年前被认为几乎不可能的结果成为可能。
在本节中,我们将讨论不同类型的卷积以及如何使用 Keras 来实现它们;因此,对于具体的技术细节,我继续建议查看官方文档和书籍《深度学习与 Keras,作者:Gulli A,Pal S.,Packt 出版》。
卷积
即使我们在有限和离散卷积中工作,也很有必要从基于可积函数的标准定义开始。为了简单起见,让我们假设 f(τ) 和 k(τ) 是在 ℜ 中定义的单变量两个实函数。f(τ) 和 k(τ) 的卷积(通常表示为 f ∗ k),我们将其称为核,定义为以下:
没有数学背景,这个表达式可能不太容易理解,但通过一些考虑,它可以变得特别简单。首先,积分覆盖了所有 τ 的值;因此,卷积是剩余变量 t 的函数。第二个基本元素是一种动态属性:核被反转(-τ)并转换为新变量 z = t - τ 的函数。没有深厚的数学知识,也可以理解这个操作沿着 τ(独立变量)轴移动函数。在下面的图中,有一个基于抛物线的例子:
第一张图是原始核(它也是对称的)。其他两个图分别显示了前向和后向位移。现在应该更清楚,卷积是函数 f(τ) 乘以位移核,并计算结果曲线下的面积。由于变量 t 没有积分,面积是 t 的函数,并定义了一个新函数,即卷积本身。换句话说,当 t = 5 时,f(τ) 和 k(τ) 的卷积值是乘积 f(τ)k(5 - τ) 获得的曲线下的面积。根据定义,卷积是可交换的 (f ∗ k = k ∗ f) 和分配的 (f ∗ (k + g) = (f ∗ k) + (f ∗ g)). 此外,还可以证明它是结合的 (f ∗ (k ∗ g) = (f ∗ k) ∗ g)。
然而,在深度学习中,我们从不处理连续卷积;因此,我省略了所有属性和数学细节,专注于离散情况。对理论感兴趣的读者可以在 Circuits, Signals, and Systems, Siebert W. M., MIT Press 中找到更多细节。相反,一种常见的做法是将多个具有不同核(通常称为滤波器)的卷积堆叠起来,将包含 n 通道的输入转换为具有 m 通道的输出,其中 m 对应于核的数量。这种方法可以通过不同输出的协同作用释放卷积的全部力量。通常,具有 n 个滤波器的卷积层的输出被称为特征图(w^((t)) × h^((t)) × n),因为其结构不再与特定图像相关,而更像不同特征检测器的重叠。在本章中,我们经常谈论图像(考虑一个假设的第一层),但所有考虑都是隐含地扩展到任何特征图。
二维离散卷积
在深度学习中应用最广泛的卷积类型是基于具有任意数量通道的二维数组(如灰度图或 RGB 图像)。为了简化,让我们分析单层(通道)卷积,因为扩展到n层是直接的。如果X∈ℜ^(w×h) 和 k∈ℜ^(n×m),卷积 X∗k 定义为(索引从 0 开始):
很明显,前面的表达式是连续定义的自然推导。在下面的图中,有一个使用 3×3 核的示例:
3x3 核的二维卷积示例
核在水平和垂直方向上移动,产生对应元素逐元素乘积的总和。因此,每个操作都导致单个像素的输出。示例中使用的核被称为离散拉普拉斯 算子(因为它是由离散化实拉普拉斯得到的);让我们观察这个核对完整灰度图的影响:
使用离散拉普拉斯核的卷积示例
如您所注意到的,卷积的效果是强调各种形状的边缘。读者现在可以理解如何调整可变核以满足精确的要求。然而,与其手动尝试,深度卷积网络将这项任务留给学习过程,该过程受一个精确目标所控制,该目标以最小化成本函数的形式表达。不同滤波器的并行应用会产生复杂的重叠,这可以简化提取那些对分类真正重要的特征。全连接层与卷积层的主要区别在于后者能够处理现有的几何形状,这些几何形状编码了区分一个对象与另一个对象所需的所有元素。这些元素不能立即推广(想想决策树的分支,其中一次分割定义了通向最终类别的精确路径),但需要后续的处理步骤来执行必要的区分。以之前的照片为例,例如,眼睛和鼻子相当相似。如何正确分割图片呢?答案是双重分析:可以通过细粒度滤波器发现细微的差异,更重要的是,真实物体的全局几何形状基于几乎不变的内部关系。例如(仅用于教学目的),眼睛和鼻子应该组成一个等腰三角形,因为面部的对称性意味着每只眼睛与鼻子的距离相同。这种考虑可以像许多视觉处理技术一样事先进行,或者,多亏了深度学习的力量,它可以留给训练过程。由于成本函数和输出类别隐式控制差异,深度卷积网络可以学习达到特定目标所需的重要信息,同时丢弃所有无用的细节。
在上一节中,我们说过特征提取过程主要是层次化的。现在,应该清楚不同核大小和后续卷积正好达到这个目标。假设我们有一个 100 × 100 的图像和一个 (3 × 3) 的核。结果图像将是 98 × 98 像素(我们稍后会解释这个概念)。然而,每个像素编码了一个 3 × 3 块的信息,由于这些块是重叠的,连续的两个像素将共享一些知识,但同时也强调了对应块之间的差异。
在以下图中,相同的拉普拉斯核应用于黑色背景上的简单白色方块:
原始图像(左);使用拉普拉斯核的卷积结果(右)
即使图像非常简单,也可能注意到卷积的结果丰富了输出图像,增加了一些非常重要的信息片段:正方形的边缘现在清晰可见(它们是黑白相间的),并且可以通过对图像进行阈值处理立即检测到。原因很简单:核在紧凑表面上的效果也是紧凑的,但当核在边缘上移动时,差异的效果变得可见。原始图像中的三个相邻像素可以表示为(0, 1, 1),表示黑白之间的水平过渡。经过卷积后,结果大约为(0.75, 0.0, 0.25)。所有原始的黑色像素都变成了浅灰色,白色正方形变暗了,而边缘(在原始图片中没有标记)现在变成了黑色(或白色,取决于移动方向)。将相同的过滤器重新应用于前一次卷积的输出,我们得到以下结果:
拉普拉斯核的第二次应用
留意观察可以立即发现三个结果:紧凑的表面(黑白)越来越相似,边缘仍然可见,最重要的是,顶部和左下角的白色像素现在更加清晰。因此,第二次卷积的结果增加了一个更细粒度的信息片段,这在原始图像中很难检测到。实际上,拉普拉斯算子的效果非常直接,它只对教学目的有用。在真实的深度卷积网络中,过滤器被训练执行更复杂的处理操作,这些操作可以揭示细节(包括它们的内部和外部关系),这些细节没有被立即用于图像分类。它们的隔离(得益于许多并行过滤器的效果)允许网络以不同的方式标记相似元素(如正方形的角),并做出更准确的决策。
本例的目的是展示一系列卷积如何生成一个层次化的过程,该过程在开始时提取粗粒度特征,在结束时提取非常高级的特征,同时不丢失已收集的信息。比喻地说,我们可以认为深度卷积网络开始放置表示线条、方向和边界的标签,并通过添加更多细节(如角、特定形状等)来丰富现有的本体。多亏了这种能力,这样的模型可以轻易地超越任何 MLP,并在训练样本数量足够大时几乎达到贝叶斯水平。这种模型的主要缺点是它们在应用仿射变换(如旋转或平移)后难以识别对象。换句话说,如果一个网络是在只包含自然位置的面部数据集上训练的,那么当呈现旋转(或颠倒)的样本时,它将表现不佳。在接下来的章节中,我们将讨论几种有助于减轻这种问题的方法(在平移的情况下);然而,一种名为胶囊网络的新实验架构(超出了本书的范围)已被提出,以略微不同的方式解决此问题(读者可以在Sabour S.,Frosst N.,Hinton G. E.,arXiv:1710.09829 [cs.CV]中找到更多细节)。
步长和填充
所有卷积都共有的两个重要参数是填充和步长。让我们考虑二维情况,但请记住,概念始终相同。当一个核(n × m,其中n, m > 1)在图像上移动并到达一个维度的末端时,有两种可能性。第一种,称为有效填充,是指即使结果图像小于原始图像,也不继续进行。特别是,如果X是一个w × h矩阵,那么最终的卷积输出将具有(w - n + 1) × (h - m + 1)的尺寸。然而,有许多情况下保持原始尺寸是有用的,例如,能够对不同的输出进行求和。这种方法称为相同填充,它基于简单地向原始图像添加n - 1空白列和m - 1空白行的想法,以便允许核在原始图像上移动,从而产生与初始尺寸相等的像素数。在许多实现中,默认值设置为有效填充。
另一个参数,称为步长,定义了每次平移时跳过的像素数。例如,设置为(1, 1)的值对应于标准的卷积,而步长设置为(2, 1)的情况在以下图中展示:
x 轴步长为 2 的二维卷积示例
在这种情况下,每次水平移动都会跳过一个像素。较大的步长在不需要高粒度时(例如,在第一层)会强制进行维度降低,而将步长设置为(1, 1)通常用于最后一层以捕捉更小的细节。没有标准规则来确定最佳值,测试不同的配置始终是最好的方法。像任何其他超参数一样,在确定一个选择是否可接受时,应该考虑太多因素;然而,关于数据集(以及因此关于底层数据生成过程)的一些一般信息可以帮助做出合理的初始决策。例如,如果我们正在处理建筑物图片,其维度是垂直的,我们可以开始选择一个值为(1, 2)的值,因为我们可以假设在y-轴上的信息冗余比在x-轴上更多。这个选择可以显著加快训练过程,因为输出有一个维度,是原始维度的一半(具有相同的填充)。这样,较大的步长会产生部分去噪并可以提高训练速度。同时,信息损失可能会对准确性产生负面影响。如果发生这种情况,可能意味着尺度不够高,无法跳过一些元素而不损害语义。例如,具有非常小脸部的图像可能会因为大步长而被不可逆地损坏,导致无法检测到正确的特征,从而降低分类准确性。
空洞卷积
在某些情况下,大于一的步长可能是一个好的解决方案,因为它可以减少维度并加快训练过程,但它可能导致图像变形,其中主要特征不再可检测。空洞卷积(也称为膨胀卷积)提供了一种替代方法。在这种情况下,核应用于更大的图像块,但跳过该区域内部的一些像素(这就是为什么有人称之为带孔卷积)。在下面的图中,有一个(3 × 3)和膨胀率设置为2的示例:
带有拉普拉斯核的空洞卷积示例
每个补丁现在是 9 **× 9,但核仍然是一个 3 × 3 拉普拉斯算子。这种方法的效果比增加步长更稳健,因为核的 周界 将始终包含具有相同几何关系的像素组。当然,细粒度特征可能会扭曲,但通常步长设置为 (1, 1),最终结果通常更连贯。与标准卷积的主要区别在于,在这种情况下,我们假设可以考虑到更远的元素来确定输出像素的性质。例如,如果主要特征不包含非常小的细节,扩张卷积可以考虑到更大的区域,直接关注标准卷积需要经过多次操作才能检测到的元素。这种技术的选择必须考虑到最终的准确性,但就像步长一样,只要几何属性可以更有效地检测,就可以从一开始就考虑使用较大的补丁和一些代表性元素。即使这种方法在特定情况下可能非常有效,但它通常不是非常深层模型的首选。在最重要的图像分类模型中,使用标准卷积(带或不带更大的步长)是因为它们已被证明在非常通用的数据集(如 ImageNet 或 Microsoft Coco)上产生最佳性能。然而,我建议读者尝试这种方法并比较结果。特别是,分析哪些类别被更好地分类,并尝试为观察到的行为找到合理的解释是个好主意。
在某些框架中,例如 Keras,没有显式的层来定义一个扩张卷积。相反,标准的卷积层通常有一个参数来定义扩张率(在 Keras 中,它被称为 dilation_rate
)。当然,默认值是 1,这意味着核将应用于与其大小匹配的补丁。
可分离卷积
如果我们考虑一个图像 X ∈ ℜ^(w × h)(单通道)和一个核 k ∈ ℜ**^(n × m),所需的操作数是 nmwh。当核不是非常小且图像很大时,即使有 GPU 支持,这种计算的代价也可能相当高。通过考虑卷积的相关属性,我们可以实现改进。特别是,如果原始核可以分解为两个向量核的点积,即 k^((1))(维度为 n × 1)和 k^((2))(维度为 1 × m),则称卷积为 可分离的。这意味着我们可以通过两个后续操作执行 (n **× m) 卷积:
优势是明显的,因为现在操作数是 (n + m)wh。特别是当 nm >> n + m 时,可以避免大量乘法,并加快训练和预测过程。
在《Xception:深度学习与深度可分离卷积》(Chollet F.,arXiv:1610.02357 [cs.CV])中提出了一个略有不同的方法。在这种情况下,这被称为深度可分离卷积,过程分为两个步骤。第一个步骤沿着通道轴操作,将其转换为一个具有可变数量通道的单维映射(例如,如果原始图是768 × 1024 × 3,则第一阶段的输出将是n × 768 × 1024 × 1)。然后,对单层应用标准卷积(实际上可以有多于一个通道)。在大多数实现中,深度卷积的默认输出通道数是 1(这通常通过说深度乘数为 1 来表示)。这种方法与标准卷积相比,可以实现参数的显著减少。实际上,如果输入通用特征图是X ∈ ℜ^(w × h × p),我们想要执行带有q核k^((i)) ∈ ℜ^(n × m)的标准卷积,我们需要学习nmqp个参数(每个核k^((i))应用于所有输入通道)。采用深度可分离卷积,第一步(仅处理通道)需要nmp个参数。由于输出仍然有p个特征图,我们需要输出q个通道,这个过程使用了一个技巧:使用q 1 × 1核处理每个特征图(这样,输出将有q层和相同的维度)。第二步所需的参数数是pq,因此总参数数变为nmp + pq。将这个值与标准卷积所需的参数数进行比较,我们得到一个有趣的结果:
由于这个条件很容易成立,这种方法在优化训练和预测过程以及在任何场景下的内存消耗方面都极为有效。Xception 模型能够立即在移动设备上实现,允许使用非常有限的资源进行实时图像分类,这并不令人惊讶。当然,深度可分离卷积并不总是与标准卷积具有相同的精度,因为它们基于这样的假设:复合特征图通道内可观察到的几何特征是相互独立的。这并不总是正确的,因为我们知道多层的效果也基于它们的组合(这增加了网络的表达能力)。然而,在许多情况下,最终结果与一些最先进的模型具有可比的精度;因此,这种技术通常可以被视为标准卷积的有效替代方案。
自从 2.1.5 版本以来,Keras 引入了一个名为DepthwiseConv2D
的层,该层实现了深度可分离卷积。这个层扩展了现有的SeparableConv2D
。
转置卷积
转置卷积(有时错误地称为反卷积,即使数学定义不同)与标准卷积并没有很大区别,但其目标是重建一个与输入样本具有相同特征的结构。假设卷积神经网络的输出是特征图 X ∈ ℜ^(w' × h' × p),我们需要构建一个输出元素 Y ∈ ℜ^(w × h × 3)(假设 w 和 h 是原始维度)。我们可以通过在 X 上应用适当的步长和填充的转置卷积来实现这个结果。例如,假设 X ∈ ℜ^(128 × 128 × 256),我们的输出必须是 512 × 512 × 3。最后一个转置卷积必须学习三个滤波器,步长设置为四,且使用相同的填充。我们将在下一章(第十一章,自编码器)中看到这个方法的一些实际例子,当讨论自编码器时;然而,在内部动态方面,转置卷积和标准卷积之间并没有非常重要的区别。主要区别在于损失函数,因为当转置卷积作为最后一层使用时,比较必须在目标图像和重建图像之间进行。在下一章(第十一章,自编码器)中,我们还将分析一些技术,以提高输出质量,即使损失函数没有专注于图像的特定区域。
池化层
在深度卷积网络中,池化层是极其有用的元素。主要有两种这样的结构:最大池化和平均池化。它们都在 p ∈ ℜ^(n × m) 的块上工作,根据预定义的步长值水平垂直移动,并根据以下规则将块转换为单个像素:
有两个主要原因可以证明使用这些层是合理的。第一个原因是通过有限的损失进行降维(例如,将步长设置为(2, 2),可以将图像/特征图的维度减半)。显然,所有池化技术或多或少都有损失(特别是最大池化),具体结果取决于单个图像。一般来说,池化层试图将一小块信息中的信息总结到一个像素中。这一想法得到了以感知为导向的方法的支持;事实上,当池子不太大时,在后续的偏移中找到高方差的可能性相当低(自然图像中很少有孤立像素)。因此,所有池化操作都允许我们设置大于一的步长,同时降低损害信息内容的风险。然而,考虑到几个实验和架构,我建议你在卷积层中设置更大的步长(特别是在卷积序列的第一层),而不是在池化层中。这样,就可以以最小的损失应用变换并充分利用下一个基本属性。
第二个(也许是最重要的)原因是,它们略微增加了对平移和有限扭曲的鲁棒性,其效果与池大小成比例。让我们考虑以下图表,表示一个十字的原始图像和经过 10 像素对角线平移后的版本:
原始图像(左);对角线平移的图像(右)
这是一个非常简单的例子,翻译图像与原始图像没有太大差异。然而,在更复杂的场景中,分类器也可能无法在类似条件下正确分类对象。对翻译图像应用最大池化(池大小为(2 × 2),步长为 2 像素),我们得到以下结果:
原始图像(左);对翻译图像进行最大池化后的结果(右)
结果是一个更大的十字,其臂部与轴稍微对齐。与原始图像相比,具有良好泛化能力的分类器更容易过滤掉虚假元素并识别原始形状(可以被认为是一个被噪声框架包围的十字)。使用相同的平均池化(相同的参数)重复相同的实验,我们得到以下结果:
原始图像(左);对翻译图像进行平均池化后的结果(右)
在这种情况下,图片部分被平滑处理,但仍能看出更好的对齐(主要归功于渐隐效果)。此外,如果这些方法简单且有一定效果,对不变变换的鲁棒性从未有显著提高,而要实现更高层次的不变性,只能通过增加池化大小。这种选择导致特征图粒度更粗,信息量大幅减少;因此,每当需要将分类扩展到可能被扭曲或旋转的样本时,使用数据增强技术生成人工图像,并在此之上训练分类器,可能是一个好主意(这允许使用更好地代表真实数据生成过程的数据库)。然而,正如深度学习,Goodfellow I.,Bengio Y.,Courville A.,MIT Press所指出的,当与多个卷积层的输出或旋转图像堆叠一起使用时,池化层也可以提供对旋转的鲁棒不变性。实际上,在这些情况下,会引发单个模式响应,池化层的效果类似于一个标准化输出的收集器。换句话说,它将产生相同的结果,而无需显式选择最佳匹配模式。因此,如果数据集包含足够的样本,网络中间位置的池化层可以提供对微小旋转的适度鲁棒性,从而提高整个深度架构的泛化能力。
如前例所示,两种变体之间的主要区别在于最终结果。平均池化执行一种非常简单的插值,平滑边缘并避免突变。另一方面,最大池化噪声较少,当需要检测特征而无需任何平滑(这可能会改变它们的几何形状)时,可以产生更好的结果。我总是建议测试这两种技术,因为仅根据经验考虑(尤其是当数据集不是由非常简单的图像组成时),几乎不可能选择最佳方法并确定合适的池化大小。
显然,始终最好在一系列卷积之后使用这些层,避免使用非常大的池化大小,因为这可能会永久性地破坏信息内容。在许多重要的深度架构中,池化层始终基于(2, 2)或(3, 3)池化,无论它们的位置如何,步长始终设置为 1 或 2。在两种情况下,信息损失与池化大小/步长成比例;因此,当需要检测小特征和大特征(例如,前景和背景人脸)时,通常避免使用大池化。
其他有用的层
即使卷积和池化层几乎是所有深度卷积网络的核心,其他层也可以帮助处理特定情况。具体如下:
-
填充层:这些层可以通过在特征图周围添加空白框架(例如,在每一边添加 n 个黑色像素)来增加特征图的大小(例如,将其与另一个特征图对齐)。。
-
上采样层:这些层通过将单个像素创建成更大的块来增加特征图的大小。在某种程度上,它们可以被视为与池化层相反的转换,尽管在这种情况下,上采样不是基于任何类型的插值。这些类型的层可以用来准备特征图,以便进行与转置卷积类似的转换,尽管许多实验证实使用更大的步长可以在不需要额外计算步骤的情况下产生非常准确的结果。
-
裁剪层:这些层有助于选择图像/特征图中的特定矩形区域。它们在模块化架构中特别有用,其中第一部分确定裁剪边界(例如,面部),而第二部分在移除背景后可以执行高级操作,如细节分割(标记眼睛、鼻子、嘴巴等区域)。将这些层直接插入到深度神经网络模型中的可能性避免了多次数据传输。不幸的是,许多框架(如 Keras)不允许我们使用可变边界,实际上限制了可能的用例数量。
-
展平层:这些层是特征图和全连接层之间的连接。通常,在处理卷积块的输出之前,使用单个展平层,然后是几个密集层,最终以 Softmax 层结束(用于分类)。这个操作在计算上非常便宜,因为它只与元数据一起工作,不执行任何计算。
使用 Keras 的深度卷积网络示例
在第一个示例中,我们再次考虑完整的 MNIST 手写数字数据集,但不是使用 MLP,而是将使用一个小型深度卷积网络。第一步包括加载数据集并进行归一化:
import numpy as np
from keras.datasets import mnist
from keras.utils import to_categorical
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
width = height = X_train.shape[1]
X_train = X_train.reshape((X_train.shape[0], width, height, 1)).astype(np.float32) / 255.0
X_test = X_test.reshape((X_test.shape[0], width, height, 1)).astype(np.float32) / 255.0
Y_train = to_categorical(Y_train, num_classes=10)
Y_test = to_categorical(Y_test, num_classes=10)
我们现在可以定义模型架构。样本相对较小(28 × 28);因此,使用小型核可能是有帮助的。这并不是一个普遍的规则,评估较大的核(特别是在第一层)也是有用的;然而,许多最先进的架构证实,在小型图像中使用较大的核大小会导致性能下降。在我的个人实验中,我总是当最大的核比图像尺寸小 8 ÷ 10 时获得最佳结果。我们的模型由以下层组成:
-
输入丢弃率 25%。
-
使用 16 个滤波器、(3 × 3)核、步长为 1、ReLU 激活和相同的填充(默认权重初始化器是 Xavier)。Keras 实现了
Conv2D
类,其主要参数是立即可理解的。 -
丢弃率 50%。
-
使用 32 个过滤器,(3 × 3)核,步长为 1,ReLU 激活函数,以及相同的填充。
-
Dropout 50%。
-
使用(2 × 2)池化大小和步长为 1 的平均池化(使用 Keras 类
AveragePooling2D
)。 -
使用 64 个过滤器,(3 × 3)核,步长为 1,ReLU 激活函数,以及相同的填充。
-
使用(2 × 2)池化大小和步长为 1 的平均池化。
-
使用 64 个过滤器,(3 × 3)核,步长为 1,ReLU 激活函数,以及相同的填充。
-
Dropout 50%。
-
使用(2 × 2)池化大小和步长为 1 的平均池化。
-
具有 1024 个 ReLU 单元的全连接层。
-
Dropout 50%。
-
具有十个 Softmax 单元的全连接层。
目标是在第一层捕获低级特征(水平线和垂直线、交叉点等),并使用池化层和所有后续卷积来提高在扭曲样本出现时的准确性。在此阶段,我们可以创建和编译模型(使用η = 0.001 的 Adam 优化器和衰减率等于 10^(-5)):
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout, Conv2D, AveragePooling2D, Flatten
from keras.optimizers import Adam
model = Sequential()
model.add(Dropout(0.25, input_shape=(width, height, 1), seed=1000))
model.add(Conv2D(16, kernel_size=(3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(Conv2D(32, kernel_size=(3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(AveragePooling2D(pool_size=(2, 2), padding='same'))
model.add(Conv2D(64, kernel_size=(3, 3), padding='same'))
model.add(Activation('relu'))
model.add(AveragePooling2D(pool_size=(2, 2), padding='same'))
model.add(Conv2D(64, kernel_size=(3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(AveragePooling2D(pool_size=(2, 2), padding='same'))
model.add(Flatten())
model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1000))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer=Adam(lr=0.001, decay=1e-5),
loss='categorical_crossentropy',
metrics=['accuracy'])
现在我们可以使用 200 个周期和 256 个样本的批量大小来训练模型:
history = model.fit(X_train, Y_train,
epochs=200,
batch_size=256,
validation_data=(X_test, Y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/200
60000/60000 [==============================] - 30s 496us/step - loss: 0.4474 - acc: 0.8531 - val_loss: 0.0993 - val_acc: 0.9693
Epoch 2/200
60000/60000 [==============================] - 20s 338us/step - loss: 0.1497 - acc: 0.9530 - val_loss: 0.0682 - val_acc: 0.9780
Epoch 3/200
60000/60000 [==============================] - 21s 346us/step - loss: 0.1131 - acc: 0.9647 - val_loss: 0.0598 - val_acc: 0.9839
...
Epoch 199/200
60000/60000 [==============================] - 21s 349us/step - loss: 0.0083 - acc: 0.9974 - val_loss: 0.0137 - val_acc: 0.9950
Epoch 200/200
60000/60000 [==============================] - 22s 373us/step - loss: 0.0083 - acc: 0.9972 - val_loss: 0.0143 - val_acc: 0.9950
最终验证准确率现在是0.9950
,这意味着只有 50 个样本(在 10000 个样本中)被错误分类。为了更好地理解行为,我们可以绘制准确率和损失图:
如所见,验证准确率和损失很容易达到最优值。特别是,初始验证准确率约为 0.97,剩余的周期是必要的,以提高所有这些样本的性能,其形状可能导致混淆(例如,形状不规则的 8 看起来像 0,或者非常相似的 7 看起来像 1)。很明显,卷积使用的几何方法比标准全连接网络提供了更高的鲁棒性,这也要归功于池化层的贡献,它们减少了由于噪声样本引起的方差。
使用 Keras 和数据增强的深度卷积网络的示例
在这个例子中,我们将使用由 Zalando 免费提供的 Fashion MNIST 数据集,作为标准 MNIST 数据集的更困难替代品。在这种情况下,而不是手写数字,这里有不同服装的灰度照片。以下截图显示了几个样本的示例:
然而,在这种情况下,我们想要使用 Keras 提供的实用类(ImageDataGenerator
)来创建一个数据增强样本集,以提高深度卷积网络的一般化能力。这个类允许我们添加随机转换(如标准化、旋转、平移、翻转、缩放、剪切等),并使用 Python 生成器(具有无限循环)输出样本。让我们开始加载数据集(我们不需要标准化它,因为这个转换由生成器执行):
from keras.datasets import fashion_mnist
(X_train, Y_train), (X_test, Y_test) = fashion_mnist.load_data()
在这个阶段,我们可以创建生成器,选择最适合我们情况的转换。由于数据集相当标准(所有样本只表示在几个位置),我们决定通过应用样本级标准化(不依赖于整个数据集)、水平翻转、缩放、小旋转和小剪切来增加数据集。这个选择是根据客观分析做出的,但我建议读者用不同的参数重复实验(例如,添加白化、垂直翻转、水平/垂直平移和扩展旋转)。当然,增加增强的变异性需要更大的处理集。在我们的情况下,我们将使用 384,000 个训练样本(原始大小为 60,000),但可以使用更大的值来训练更深的网络:
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
from keras.utils import to_categorical
nb_classes = 10
train_batch_size = 256
test_batch_size = 100
train_idg = ImageDataGenerator(rescale=1.0 / 255.0,
samplewise_center=True,
samplewise_std_normalization=True,
horizontal_flip=True,
rotation_range=10.0,
shear_range=np.pi / 12.0,
zoom_range=0.25)
train_dg = train_idg.flow(x=np.expand_dims(X_train, axis=3),
y=to_categorical(Y_train, num_classes=nb_classes),
batch_size=train_batch_size,
shuffle=True,
seed=1000)
test_idg = ImageDataGenerator(rescale=1.0 / 255.0,
samplewise_center=True,
samplewise_std_normalization=True)
test_dg = train_idg.flow(x=np.expand_dims(X_test, axis=3),
y=to_categorical(Y_test, num_classes=nb_classes),
shuffle=False,
batch_size=test_batch_size,
seed=1000)
一旦初始化了图像数据生成器,就必须对其进行拟合,指定输入数据集和期望的批量大小(此操作的输出是实际的 Python 生成器)。测试图像生成器自愿不进行任何转换,除了归一化和标准化,以避免在来自不同分布的数据集上进行验证。在这个阶段,我们可以创建和编译我们的网络,使用基于 Leaky ReLU 激活的 2D 卷积(使用LeakyReLU
类,该类取代了标准的Activation
层),批量归一化,以及最大池化:
from keras.models import Sequential
from keras.layers import Activation, Dense, Flatten, LeakyReLU, Conv2D, MaxPooling2D, BatchNormalization
from keras.optimizers import Adam
model = Sequential()
model.add(Conv2D(filters=32,
kernel_size=(3, 3),
padding='same',
input_shape=(X_train.shape[1], X_train.shape[2], 1)))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(filters=64,
kernel_size=(3, 3),
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=64,
kernel_size=(3, 3),
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(filters=128,
kernel_size=(3, 3),
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(filters=128,
kernel_size=(3, 3),
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(units=1024))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(Dense(units=1024))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.1))
model.add(Dense(units=nb_classes))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',
optimizer=Adam(lr=0.0001, decay=1e-5),
metrics=['accuracy'])
所有批量归一化都是应用于激活函数之前的线性变换。考虑到额外的复杂性,我们还将使用一个回调,这是 Keras 用来执行训练中操作的类。在我们的情况下,我们希望在验证损失停止改进时降低学习率。特定的回调称为ReduceLROnPlateau
,并调整以将η乘以0.1
(在等于patience
参数值的 epoch 数之后),有一个冷却期(在恢复原始学习率之前等待的 epoch 数)为 1 个 epoch,以及最小η = 10^(-6)。训练方法现在是fit_generator()
,它接受 Python 生成器而不是有限的数据集,以及每个 epoch 的迭代次数(所有其他参数与fit()
实现相同):
from keras.callbacks import ReduceLROnPlateau
nb_epochs = 100
steps_per_epoch = 1500
history = model.fit_generator(generator=train_dg,
epochs=nb_epochs,
steps_per_epoch=steps_per_epoch,
validation_data=test_dg,
validation_steps=int(X_test.shape[0] / test_batch_size),
callbacks=[
ReduceLROnPlateau(factor=0.1, patience=1, cooldown=1, min_lr=1e-6)
])
Epoch 1/100
1500/1500 [==============================] - 471s 314ms/step - loss: 0.3457 - acc: 0.8722 - val_loss: 0.2863 - val_acc: 0.8952
Epoch 2/100
1500/1500 [==============================] - 464s 309ms/step - loss: 0.2325 - acc: 0.9138 - val_loss: 0.2721 - val_acc: 0.8990
Epoch 3/100
1500/1500 [==============================] - 460s 307ms/step - loss: 0.1929 - acc: 0.9285 - val_loss: 0.2522 - val_acc: 0.9112
...
Epoch 99/100
1500/1500 [==============================] - 449s 299ms/step - loss: 0.0438 - acc: 0.9859 - val_loss: 0.2142 - val_acc: 0.9323
Epoch 100/100
1500/1500 [==============================] - 449s 299ms/step - loss: 0.0443 - acc: 0.9857 - val_loss: 0.2136 - val_acc: 0.9339
在这种情况下,复杂性更高,结果并不像使用标准的 MNIST 数据集获得的结果那样准确。验证和损失曲线在以下图表中展示:
损失曲线并没有显示出 U 形曲线,但似乎从第 20 个 epoch 开始并没有真正的改进。这一点也由验证曲线得到证实,它继续在 0.935 和大约 0.94 之间波动。另一方面,训练损失并没有达到其最小值(训练准确率也是如此),这主要是因为批量归一化。然而,考虑到几个基准,结果并不差(即使最先进的模型可以达到大约 0.96 的验证准确率)。我建议读者尝试不同的配置(带有和不带有 dropout 以及其他激活函数),基于更深层次的架构和更大的训练集。这个例子提供了许多练习这类模型的机会,因为其复杂性并不高,不需要专用硬件,但与此同时,存在许多模糊性(例如,衬衫和 T 恤之间的区别),这可能会降低泛化能力。
循环神经网络
我们到目前为止所分析的所有模型都有一个共同特征。一旦训练过程完成,权重就会被冻结,输出只取决于输入样本。显然,这是分类器预期的行为,但在许多场景中,预测必须考虑到输入值的历史。时间序列是一个经典的例子。假设我们需要预测下周的温度。如果我们试图只使用最后一个已知的x(t)*值和一个训练用来预测*x(t+1)的多层感知器(MLP),我们就无法考虑到像季节、季节多年的历史、季节中的位置等时间条件。回归器将能够关联产生最小平均误差的输出,但在现实生活中的情况下,这并不足够。解决这个问题的唯一合理方式是为人工神经元定义一个新的架构,给它提供记忆。这个概念在以下图中展示:
现在这个神经元不再是一个纯粹的纯前向计算单元,因为反馈连接迫使它记住其过去并使用它来预测新的值。新的动态规则现在如下:
之前的预测被反馈并累加到新的线性输出中。这个结果通过激活函数转换,以产生实际的新输出(通常第一个输出是空的,但这不是一个约束)。一个立即需要考虑的问题是激活函数——这是一个可能很容易变得不稳定的动态系统。防止这种现象的唯一方法就是使用饱和函数(如 Sigmoid 或双曲正切)。实际上,无论输入是什么,输出永远不会通过向+∞或-∞移动而爆炸。
假设我们使用的是 ReLU 激活函数——在某些条件下,输出将无限增长,导致溢出。显然,使用线性激活函数的情况更糟,即使使用 Leaky ReLU 或 ELU,情况也可能非常相似。因此,很明显我们需要选择饱和函数,但这足够确保稳定性吗?即使双曲正切(以及 Sigmoid)有两个稳定点(-1和+1),这也不足以确保稳定性。让我们想象输出受到噪声的影响,并在 0.0 附近振荡。该单元无法收敛到某个值,并保持在极限循环中。
幸运的是,学习权重的可能性使我们能够提高对噪声的鲁棒性,避免输入的有限变化会逆转神经元的动态。这是一个非常重要(且容易证明)的结果,它保证了在非常简单的条件下稳定性,但再次,我们需要付出什么代价?这是否简单直接?不幸的是,答案是消极的,稳定性的代价非常高。然而,在讨论这个问题之前,让我们看看一个简单的循环网络是如何被训练的。
时间反向传播(BPTT)
训练 RNN 最简单的方法是基于一种表示技巧。由于输入序列有限且长度可以固定,因此可以将具有反馈连接的简单神经元重新结构化为展开的前馈网络。在下面的图中,有一个带有k个时间步长的示例:
展开的循环神经网络示例
这个网络(可以轻松扩展到具有多个层的更复杂架构)与 MLP(多层感知器)完全相同,但在这个情况下,每个克隆的权重是相同的。称为BPTT(反向传播时间)的算法是标准学习技术向展开循环网络的自然扩展。这个过程很简单。一旦所有输出都被计算出来,就可以确定每个单独网络的成本函数值。在这个时候,从最后一步开始,计算并存储校正(梯度),然后重复这个过程直到初始步骤。然后,将所有梯度求和并应用于网络。由于每个贡献都是基于精确的时间经验(由局部样本和先前记忆元素组成),标准反向传播将学会如何管理动态条件,就像它是点预测一样。然而,我们知道实际的网络并没有展开,过去的依赖关系在理论上被传播和记住。我故意使用了“理论上”这个词,因为所有实际实验都显示出完全不同的行为,我们将在后面讨论。这种技术非常容易实现,但对于必须展开以处理大量时间步长的深度网络来说,可能会非常昂贵。因此,提出了一个名为截断反向传播时间(TBPTT)的变体(在Subgrouping reduces complexity and speeds up learning in recurrent networks, Zipser D., Advances in Neural Information Processing Systems, II 1990)。
这个想法是使用两个序列长度 t[1] 和 t[2](其中 t[1] >> t[2])——较长的序列(t[1])用于正向传播阶段,而较短的长度(t[2])用于训练网络。乍一看,这个版本看起来就像一个带有短序列的正常 BPTT;然而,关键思想是迫使网络使用更多信息更新隐藏状态,然后根据较长序列的结果(即使更新只传播到有限的前一时间步)计算校正。显然,这是一个可以加快训练过程的近似,但最终结果通常与处理长序列得到的结果相当,特别是在依赖关系可以被分成更短的时序块时(因此假设没有非常长的依赖关系)。
即使 BPTT 算法在数学上是正确的,并且学习短期依赖(对应于短展开网络)并不困难,但多项实验证实,学习长期依赖性极其困难(或几乎不可能)。换句话说,利用过去有限窗口内的经验(因此其重要性有限,因为它们无法管理最复杂的变化)是容易的,但网络难以学习所有行为,例如,具有数百个时间步长周期性的行为。1994 年,Bengio、Simard 和 Frasconi 提供了对这个问题的理论解释(在Using Gradient Descent to Learn Long-Term Dependencies is Difficult, Bengio Y., Simard P., Frasconi P., IEEE Transactions on Neural Networks, 5/1994)。数学细节相当复杂,因为它们涉及到动态系统理论;然而,最终结果是,当神经元被迫对噪声(正常预期行为)变得鲁棒时,网络会受到梯度消失问题的困扰,当 t → ∞。更普遍地说,我们可以将向量循环神经元动态表示如下:
BPTT 的乘法效应迫使梯度与 W^t 成正比。如果 W 的最大绝对特征值(也称为谱半径)小于 1,那么以下适用:
更简单地说,我们可以重新表述这个结果,即梯度的幅度与序列长度成正比,即使条件在渐近上是有效的,许多实验证实,数值计算的有限精度和由于后续乘法引起的指数衰减可以迫使梯度消失,即使序列不是特别长。这似乎是任何 RNN 架构的终结,但幸运的是,更近期的方案已经被设计和提出,以解决这个问题,允许 RNNs 在没有特别复杂的情况下学习短期和长期依赖。RNN 的新时代开始了,结果立即显著。
LSTM
这个模型(在许多领域代表了最先进的循环单元)是由 Hochreiter 和 Schmidhuber 在 1997 年提出的,具有标志性的名字长短期记忆(LSTM)。正如其名所示,想法是创建一个更复杂的艺术性循环神经元,可以插入到更大的网络中,并且可以在没有消失和爆炸梯度风险的情况下进行训练。经典循环网络的一个关键元素是它们专注于学习,而不是选择性遗忘。这种能力确实是优化记忆以记住真正重要的东西并移除所有那些对预测新值不必要的信
为了实现这个目标,LSTM 利用了两个重要的特征(在讨论模型之前先揭露它们是有帮助的)。第一个是一个显式状态,它是一组独立的变量,用于存储构建长期和短期依赖关系所需的元素,包括当前状态。这些变量是称为恒定误差轮(CEC)的机制的构建块,之所以这样命名是因为它负责由反向传播算法提供的错误循环和内部管理。这种方法允许纠正权重而不再受到乘法效应的影响。内部 LSTM 动态有助于更好地理解错误是如何安全地反馈的;然而,关于训练过程(始终基于梯度下降)的详细解释超出了本书的范围,可以在上述论文中找到。
第二个特征是存在门。我们可以简单地将门定义为可以调节通过它的信息量的元素。例如,如果 y = ax 且 a 是介于 0 和 1 之间的变量,它可以被认为是一个门,因为当它等于 0 时,它会阻止输入 x;当它等于 1 时,它允许输入无限制地流入;当它具有中间值时,它会按比例减少信息量。在 LSTMs 中,门由 sigmoid 函数管理,而激活基于双曲正切(其对称性保证了更好的性能)。在这个阶段,我们可以展示 LSTM 单元的结构图并讨论其内部动态:
第一个(也是最重要的)元素是记忆状态,它负责依赖关系和实际输出。在图中,它由上面的线表示,其动态由以下一般方程表示:
因此,状态取决于先前值、当前输入和先前输出。让我们从第一个项开始,引入遗忘门。正如其名所示,它负责现有记忆元素的持久性或删除。在图中,它由第一个垂直块表示,其值是通过考虑先前输出和当前输入的连接来获得的:
该操作是一个经典的神经元激活,具有向量输出。另一种版本可以使用两个权重矩阵并保持输入元素分离:
然而,我更喜欢先前的版本,因为它能更好地表达输入和输出的同质性及其后果性。使用遗忘门,可以通过哈达玛(或逐元素)积确定 g1)) 的值:
这种计算的效应是过滤掉必须保留的 C^((t)) 的内容及其有效性程度(与 f^((t+1)) 的值成正比)。如果遗忘门输出接近 1 的值,则相应的元素仍然被认为是有效的,而较低的值则确定一种过时性,甚至可能导致当遗忘门值为 0 或接近 0 时,细胞完全删除一个元素。下一步是考虑必须考虑的输入样本量以更新状态。这项任务是通过输入门(第二个垂直块)完成的。方程与先前的方程完全类似:
然而,在这种情况下,我们还需要计算必须添加到当前状态中的项。如前所述,LSTM 单元使用双曲正切作为激活函数;因此,新状态贡献的获得如下:
使用输入门和状态贡献,可以确定函数 g2), y^((t))):
因此,完整的状态方程如下:
现在,LSTM 单元的内部逻辑更加明显。状态基于以下内容:
-
在先前经验和根据新经验重新评估之间保持动态平衡(由遗忘门调节)
-
当前输入的语义效应(由输入门调节)和潜在的加性激活
实际场景很多。可能新的输入会迫使 LSTM 重置状态并存储新的传入值。另一方面,输入门也可以保持关闭,给予新输入(连同前一个输出)非常低的优先级。在这种情况下,LSTM 考虑到长期依赖关系,可以决定丢弃被认为是有噪声且不一定能有助于准确预测的样本。在其他情况下,遗忘和输入门都可以部分打开,只让一些值影响状态。所有这些可能性都通过学习过程通过修正权重矩阵和偏差来管理。与 BPTT 的不同之处在于,长期依赖关系不再受到梯度消失问题的阻碍。
最后一步是确定输出。第三个垂直块被称为输出门,它控制从状态到输出单元必须传递的信息。其方程如下:
因此,实际输出如下确定:
一个重要的考虑因素是门。它们都使用相同的向量,包含前一个输出和当前输入。由于它们是同质值,连接产生了一个有意义的实体,它编码了一种某种逆因果关系(这是一个不恰当的定义,因为我们处理的是前因后果)。门的工作方式类似于没有阈值逻辑回归;因此,它们可以被视为伪概率向量(不是分布,因为每个元素都是独立的)。遗忘门表示最后一个序列(效果,原因)比当前状态更重要;然而,只有输入门有责任赋予它影响新状态的权利。此外,输出门表示当前序列能否让当前状态流出。这种动态确实非常复杂,存在一些缺点。例如,当输出门保持关闭时,输出接近零,这会影响遗忘和输入门。由于它们控制新状态和 CEC,它们可能会限制传入信息量和后续修正,导致性能不佳。
一种可以缓解此问题的简单解决方案是由一种称为窥视孔 LSTM的变体提供的。其思路是将前一个状态输入到每个门中,以便它们可以更独立地做出决策。通用的门方程如下:
新的权重集 U[g](对于所有三个门)必须以与标准 W[g] 和 b[g] 相同的方式学习。与经典 LSTM 的主要区别在于,序列动态:忘记门 | 输入门 | 新状态 | 输出门 | 实际输出现在部分被简略处理。状态在每个门激活中的存在允许它们利用多个循环连接,在许多复杂情况下提供更高的准确性。另一个重要的考虑因素是学习过程:在这种情况下,窥视孔被关闭,唯一的反馈通道是输出门。不幸的是,并非每个 LSTM 实现都支持窥视孔;然而,几项研究表明,在大多数情况下,所有模型都产生相似的性能。
Xingjian 等人(在卷积 LSTM 网络:一种用于降水预报的机器学习方法,Xingjian S.,Zhourong C.,Hao W.,Dit-Yan Y.,Wai-kin W.,Wang-Chun W.,arXiv:1506.04214 [cs.CV])提出了一种称为卷积 LSTM的变体,它明显地将卷积和 LSTM 单元混合在一起。主要内部差异在于门计算,现在变为(没有窥视孔,尽管如此,总是可以添加):
W[g] 现在是一个与输入输出向量(通常是两个图像的拼接)卷积的核。当然,可以训练任意数量的核来增加单元的解码能力,输出将具有 (批大小 × 宽度 × 高度 × 核数) 的形状。这种单元特别适用于将空间处理与鲁棒的时间方法相结合。给定一系列图像(例如,卫星图像、游戏截图等),卷积 LSTM 网络可以学习通过几何特征演变(例如,云的移动或可以基于长期事件历史预测的具体精灵策略)表现出的长期关系。这种方法(即使经过一些修改)在深度强化学习中得到了广泛应用,以解决仅由一系列图像提供输入的复杂问题。当然,计算复杂度非常高,尤其是在使用许多后续层时;然而,结果优于任何现有方法,这种方法成为管理这类问题的首选之一。
另一个重要的变体,这在许多循环神经网络中都很常见,是由双向接口提供的。这不是一个实际的层,而是一种策略,用于将序列的前向分析与其反向分析结合起来。两个 cellblocks 使用一个序列及其逆序作为输入,输出,例如,可以连接起来并用于后续处理步骤。在 NLP 等领域,这种方法可以显著提高分类和实时翻译的准确性。原因是严格与序列结构的规则有关。在自然语言中,一个句子w[1] w[2] ... w[n]有前向关系(例如,单数名词后面可以跟is),但了解反向关系(例如,句子这个地方很糟糕)可以避免过去需要通过后处理步骤纠正的常见错误(pretty的初始翻译可能与nice的翻译相似,但后续分析可以揭示形容词不匹配,并可以应用特殊规则)。另一方面,深度学习不是基于特殊的规则,而是基于学习内部表示的能力,这种表示应该能够自主做出最终决策(无需进一步的外部帮助),双向 LSTM 网络有助于在许多重要场景中实现这一目标。
Keras 自其起源就实现了LSTM
类。它还提供了一个Bidirectional
类包装器,可以与每个 RNN 层一起使用,以获得双重输出(使用正向和反向序列计算)。此外,在 Keras 2 中,基于 NVIDIA CUDA(CuDNNLSTM
)的 LSTM 优化版本提供了非常高的性能,当有兼容的 GPU 时。在同一个包中,还可以找到ConvLSTM2D
类,它实现了卷积 LSTM 层。在这种情况下,读者可以立即识别出许多参数,因为它们与标准卷积层相同。
GRU
这个模型被称为门控循环单元(GRU),由 Cho 等人提出(在《使用 RNN 编码器-解码器进行统计机器翻译学习短语表示》Cho K.,Van Merrienboer B.,Gulcehre C.,Bahdanau D.,Bougares F.,Schwenk H.,Bengio Y.,arXiv:1406.1078 [cs.CL]),可以看作是经过一些变体的简化 LSTM。一个通用全门控单元的结构在以下图中表示:
与 LSTM 相比,主要区别在于只有两个门和没有显式的状态。这些简化可以加快训练和预测阶段的速度,同时避免梯度消失问题。
第一个门被称为重置门(通常用字母r表示),其功能与遗忘门类似:
与遗忘门类似,它的作用是决定前一个输出中必须保留的内容及其相对程度。事实上,新输出的附加贡献是通过以下方式获得的:
在前面的表达式中,我更喜欢将权重矩阵分开,以便更好地展示其行为。tanh(•)的参数是新的输入的线性函数之和以及一个加权项,它是前一个状态的函数。现在,很明显重置门是如何工作的:它调节必须保留的历史量(累积在前一个输出值中)以及可以丢弃的内容。然而,仅重置门不足以以足够的准确性确定正确的输出,考虑到短期和长期依赖。为了提高单元的表达能力,添加了一个更新门(其作用类似于 LSTM 输入门):
更新门控制必须贡献给新输出的信息量(因此也影响到状态)。由于它是一个介于0和1之间的值,GRUs 被训练通过类似于加权平均的操作来混合旧输出和新加的附加贡献:
因此,更新门变成了一个调节器,可以选择每个流必须输出和存储以供下一次操作的部分。这个单元在结构上比 LSTM 简单,但几项研究表明,其性能平均来说与 LSTM 相当,在某些特定情况下,GRU 甚至优于更复杂的细胞。我的建议是您测试这两种模型,从 LSTM 开始。现代硬件大大降低了计算成本,在许多情况下,GRUs 的优势可以忽略不计。在这两种情况下,哲学是相同的:错误被保留在细胞内,门控的权重被纠正以最大化准确性。这种行为阻止了小梯度的乘法级联,并增加了学习非常复杂的时间行为的能力。
然而,单个细胞/层无法成功实现所需的准确性。在这些所有情况下,可以通过堆叠由可变数量的细胞组成的多个层来实现。每一层通常可以输出最后一个值或整个序列。前者用于将 LSTM/GRU 层连接到全连接层时,而整个序列对于喂养另一个循环层是必要的。我们将在下面的例子中看到如何使用 Keras 实现这些技术。
就像对于 LSTMs 一样,Keras 实现了GRU
类及其 NVIDIA CUDA 优化的版本CuDNNGRU
。
Keras 中的 LSTM 网络示例
在这个例子中,我们想测试 LSTM 网络学习长期依赖关系的能力。因此,我们使用了一个名为 Zuerich 月太阳黑子(由 Andrews 和 Herzberg 在 1985 年免费提供)的数据集,其中包含从 1749 年到 1983 年所有月份观察到的数值(请阅读信息框了解如何下载数据集)。由于我们不对日期感兴趣,我们需要解析文件以提取仅用于时间序列(包含 2,820 个步骤)所需的数据:
import numpy as np
dataset_filename = '<YOUR_PATH>\dataset.csv'
n_samples = 2820
data = np.zeros(shape=(n_samples, ), dtype=np.float32)
with open(dataset_filename, 'r') as f:
lines = f.readlines()
for i, line in enumerate(lines):
if i == 0:
continue
if i == n_samples + 1:
break
_, value = line.split(',')
data[i-1] = float(value)
或者,可以使用 pandas (pandas.pydata.org
) 加载 CSV 数据集,这是一个强大的数据处理/分析库(有关更多信息,请参阅《学习 pandas 第二版》,Heydt M.,Packt):
import pandas as pd
dataset_filename = '<YOUR_PATH>\dataset.csv'
df = pd.read_csv(dataset_filename, index_col=0, header=0).dropna()
data = df.values.astype(np.float32).squeeze()
这些值未经归一化,由于 LSTMs 使用双曲正切函数,因此将它们归一化到区间-1
和1
是有帮助的。我们可以轻松地使用 Scikit-Learn 类MinMaxScaler
执行此步骤:
from sklearn.preprocessing import MinMaxScaler
mmscaler = MinMaxScaler((-1.0, 1.0))
data = mmscaler.fit_transform(data.reshape(-1, 1))
完整的数据集如下所示:
为了训练模型,我们决定使用 2,300 个样本进行训练,剩余的 500 个样本用于验证(对应约 42 年)。模型的输入是一个包含 15 个样本的序列批量(沿时间轴移动),输出是随后的月份;因此,在训练之前,我们需要准备数据集:
sequence_length = 15
X_ts = np.zeros(shape=(n_samples - sequence_length, sequence_length, 1), dtype=np.float32)
Y_ts = np.zeros(shape=(n_samples - sequence_length, 1), dtype=np.float32)
for i in range(0, data.shape[0] - sequence_length):
X_ts[i] = data[i:i + sequence_length]
Y_ts[i] = data[i + sequence_length]
X_ts_train = X_ts[0:2300, :]
Y_ts_train = Y_ts[0:2300]
X_ts_test = X_ts[2300:2800, :]
Y_ts_test = Y_ts[2300:2800]
现在,我们可以创建并编译一个简单的模型,该模型包含一个包含四个单元的单个状态化 LSTM 层,后面跟着一个双曲正切输出神经元(我总是建议读者尝试更复杂的架构和不同的参数):
from keras.models import Sequential
from keras.layers import LSTM, Dense, Activation
from keras.optimizers import Adam
model = Sequential()
model.add(LSTM(4, stateful=True, batch_input_shape=(20, sequence_length, 1)))
model.add(Dense(1))
model.add(Activation('tanh'))
model.compile(optimizer=Adam(lr=0.001, decay=0.0001),
loss='mse',
metrics=['mse'])
在LSTM
类中将stateful=True
参数设置为 True 强制 Keras 在每次批量后不重置状态。实际上,我们的目标是学习长期依赖关系,内部 LSTM 状态必须反映整体趋势。当 LSTM 网络是状态化的时,还必须在输入形状中指定批量大小(通过batch_input_shape
参数)。在我们的例子中,我们选择了 20 个样本的批量大小。优化器是具有更高衰减(以避免不稳定性)的Adam
,以及基于均方误差的损失(这是此类场景中最常见的选择)。到此为止,我们可以训练模型(100 个周期):
model.fit(X_ts_train, Y_ts_train,
batch_size=20,
epochs=100,
shuffle=False,
validation_data=(X_ts_test, Y_ts_test))
Train on 2300 samples, validate on 500 samples
Epoch 1/100
2300/2300 [==============================] - 11s 5ms/step - loss: 0.4905 - mean_squared_error: 0.4905 - val_loss: 0.1827 - val_mean_squared_error: 0.1827
Epoch 2/100
2300/2300 [==============================] - 4s 2ms/step - loss: 0.1214 - mean_squared_error: 0.1214 - val_loss: 0.1522 - val_mean_squared_error: 0.1522
Epoch 3/100
2300/2300 [==============================] - 4s 2ms/step - loss: 0.0796 - mean_squared_error: 0.0796 - val_loss: 0.1154 - val_mean_squared_error: 0.1154
...
Epoch 99/100
2300/2300 [==============================] - 4s 2ms/step - loss: 0.0139 - mean_squared_error: 0.0139 - val_loss: 0.0247 - val_mean_squared_error: 0.0247
Epoch 100/100
2300/2300 [==============================] - 4s 2ms/step - loss: 0.0139 - mean_squared_error: 0.0139 - val_loss: 0.0247 - val_mean_squared_error: 0.0247
这是一个仅用于教学目的的示例;因此,最终的验证均方误差并不特别低。然而,正如以下图表(表示验证集上的预测)所示,该模型已成功学习到全局趋势:
在 Zuerich 数据集上的 LSTM 预测
模型仍然无法在所有非常快速的峰值上达到非常高的精度,但它能够正确地模拟振荡的幅度和尾部的长度。为了保持学术诚信,我们必须考虑这种验证是在真实数据上进行的;然而,当处理时间序列时,使用真实值来预测新值是正常的。在这种情况下,它就像是一个移动预测,其中每个值都是使用训练历史和一组真实观察值获得的。很明显,模型能够预测长期振荡和一些局部振荡(例如,从步骤 300 开始的序列),但它可以通过提高整个验证集的性能来改进。为了实现这一目标,有必要增加网络复杂度并调整学习率(这是一个在真实数据集上非常有趣的练习)。
观察前面的图表,可以看到模型在某些高频(快速变化)的情况下相对更准确,而在其他情况下则更不精确。这不是一个奇怪的行为,因为非常振荡的函数需要更多的非线性(想想泰勒展开和截断到特定程度时的相对误差)来实现高精度(这意味着使用更多的层)。我的建议是,您使用更多的 LSTM 层重复实验,考虑到我们需要将整个输出序列传递到下一个循环层(这可以通过设置return_sequences=True
参数来实现)。相反,最后一层必须只返回最终值(这是默认行为)。我还建议测试 GRU 层,将性能与 LSTM 版本进行比较,并选择最简单(基准测试训练时间)且最准确的解决方案。
数据集可以免费从datamarket.com/data/set/22ti/zuerich-monthly-sunspot-numbers-1749-1983#!ds=22ti&display=line
以 CSV 格式下载。
迁移学习
我们已经讨论了深度学习在本质上是基于灰盒模型,这些模型学习如何将输入模式与特定的分类/回归结果关联起来。通常用于为特定检测准备数据的所有处理流程都被神经网络架构的复杂性所吸收。然而,为了获得高精度,需要付出代价,那就是成比例的大量训练样本。最先进的视觉网络使用数百万张图片进行训练,显然,每张图片都必须被正确标注。即使有许多可以用于训练多个模型的免费数据集,许多特定场景仍需要进行艰苦的准备工作,有时这些工作非常难以实现。
幸运的是,深度神经网络架构是层次化的模型,它们以结构化的方式进行学习。正如我们在深度卷积网络的例子中所看到的,第一层变得越来越敏感,能够检测到低级特征,而高层则专注于提取更详细的高级特征。在多个任务中,我们可以合理地认为,使用大型视觉数据集(例如 ImageNet 或 Microsoft Coco)训练的神经网络可以被重新用于在略微不同的任务中实现专业化。这个概念被称为迁移学习,当需要使用全新的数据集和特定目标创建最先进的模型时,它是最有用的技术之一。例如,客户可以要求一个系统监控几个摄像头,目的是对图像进行分割并突出特定目标的边界。
输入由具有与在训练非常强大的模型(例如 Inception、ResNet 或 VGG)中使用的数千张图像相同的几何属性的视频帧组成;因此,我们可以取一个预训练的模型,移除最高层(通常是结束于 softmax 分类层的密集层),并将展平层连接到一个输出边界框坐标的多层感知器(MLP)。网络的第一个部分可以被冻结(权重不再修改),而随机梯度下降(SGD)被应用于调整新专业化的子网络的权重。
显然,这种方法可以显著加快训练过程,因为模型中最复杂的部分已经经过训练,并且可以保证极高的准确率(相对于原始的简单解决方案),这得益于对原始模型已经进行的优化。显然,最自然的问题就是这种方法是如何工作的?有没有任何正式的证明?不幸的是,没有数学证明,但已有足够的证据来保证我们采用这种方法。一般来说,神经训练过程的目的是专门化每一层,以便为下一层提供更特定的(详细、过滤等)表示。卷积网络是这种行为的明显例子,但在 MLPs 中也可以观察到同样的情况。对非常深的卷积网络的分析显示了内容在达到展平层之前仍然是视觉的,在展平层,内容被发送到一系列密集层,这些层负责向最终的 softmax 层提供数据。换句话说,卷积块输出的是输入的高级、分段表示,这很少受到特定分类问题的影响。因此,迁移学习通常是合理的,并且通常不需要重新训练底层。然而,很难理解哪种模型可以产生最佳性能,了解用于训练原始网络的哪个数据集非常有用。通用数据集(例如,ImageNet)在许多情况下非常有用,而特定数据集(如 Cifar-10 或 Fashion;MNIST 可能过于限制)。幸运的是,Keras 在keras.applications
包中提供了许多模型(甚至相当复杂),这些模型总是使用 ImageNet 数据集进行训练,并且可以立即用于生产就绪的应用。尽管使用它们非常简单,但需要更深入地了解这个框架,而这超出了本书的范围。我邀请对这一主题感兴趣的读者查阅由 Gulli A.、Pal S.和 Packt 出版的《深度学习与 Keras》一书。
摘要
在本章中,我们介绍了深度卷积网络的概念,这是一种通用的架构,可以用于任何视觉处理任务。这个想法基于层次信息管理,旨在从低级元素开始提取特征,并逐步前进,直到达到有助于实现特定目标的高级细节。
第一个主题是卷积的概念及其在离散和有限样本中的应用。我们讨论了标准卷积的性质,然后分析了某些重要的变体,例如空洞(或膨胀)卷积、可分离(和深度可分离)卷积,最终是转置卷积。所有这些方法都可以与 1D、2D 和 3D 样本一起工作,尽管最广泛的应用是基于二维(不考虑通道)矩阵,这些矩阵代表静态图像。在同一部分中,我们还讨论了如何使用池化层来降低维度并提高对微小平移的鲁棒性。
在下一节中,我们介绍了循环神经网络(RNN)的概念,强调了当使用时间反向传播算法训练经典模型时通常会出现的问题。特别是,我们解释了为什么这些网络难以学习长期依赖。因此,提出了新的模型,其性能立即表现出色。我们讨论了最著名的循环单元,称为长短期记忆(LSTM),它可用于易于学习序列中所有最重要的依赖关系的层,即使在具有非常高的方差(如股票市场报价)的上下文中,也能最小化预测误差。最后一个主题是 LSTMs 中实现的想法的简化版本,这导致了名为门控循环单元(GRU)的模型。这个单元更简单,计算效率更高,许多基准测试证实其性能与 LSTM 大致相同。
在下一章,第十一章,自编码器中,我们将讨论一些称为自编码器的特定模型,其主要特性是创建任意复杂输入分布的内部表示。
第十一章:自编码器
在本章中,我们将探讨一个无监督模型家族,其性能通过现代深度学习技术得到了提升。自编码器为经典问题如降维或字典学习提供了一种不同的方法,但与许多其他算法不同,它们不受影响许多著名模型的容量限制。此外,它们可以利用特定的神经网络层(如卷积)根据专门的标准提取信息片段。这样,内部表示可以更稳健地抵抗不同类型的扭曲,并且在处理信息量方面更加高效。
尤其是我们将要讨论以下内容:
-
标准自编码器
-
去噪自编码器
-
稀疏自编码器
-
变分自编码器
自编码器
在前面的章节中,我们讨论了真实数据集通常是非常高维的样本表示,这些样本位于低维流形上(这是半监督模式假设之一,但通常是正确的)。由于模型的复杂性与输入数据的维度成正比,许多技术已经被分析和优化,以减少实际的有效组件数量。例如,PCA 根据相对解释方差选择特征,而 ICA 和通用字典学习方法寻找可以组合以重建原始样本的基本原子。在本章中,我们将分析一类基于略有不同方法但能力显著增强的深度学习方法。
一个通用的自编码器是一个分为两个独立(但并非完全自主)组件的模型,称为编码器和解码器。编码器的任务是转换输入样本为一个编码特征向量,而解码器的任务正好相反:使用特征向量作为输入重建原始样本。以下图显示了通用模型的示意图:
通用自编码器架构
更正式地说,我们可以将编码器描述为一个参数化函数:
输出 z[i] 是一个向量码,其维度通常远低于输入。类似地,解码器被描述如下:
标准算法的目标是最小化与重建误差成比例的成本函数。一种经典的方法是基于均方误差(在包含 M 个样本的数据集上工作):
这个函数只依赖于输入样本(它们是常数)和参数向量;因此,这实际上是一种无监督方法,我们可以控制内部结构和施加在 z[i] 代码上的约束。从概率论的角度来看,如果输入 x**[i] 样本是从 p(X) 数据生成过程中抽取的,我们的目标是找到一个 q(•) 参数分布,使其与 p(X) 的 Kullback–Leibler 散度最小化。考虑到之前的定义,我们可以将 q(•) 定义如下:
因此,Kullback–Leibler 散度变为以下:
第一个项代表原始分布的负熵,它是常数,不参与优化过程。另一个项是 p 和 q 之间的交叉熵。如果我们假设 p 和 q 是高斯分布,均方误差与交叉熵成正比(对于优化目的,它等同于交叉熵),因此这个成本函数在概率方法下仍然有效。或者,我们可以考虑 p 和 q 是伯努利分布,交叉熵变为以下:
这两种方法的主要区别在于,虽然均方误差可以应用于 x[i] ∈ ℜ^q(或多维矩阵),但伯努利分布需要 x[i] ∈ [0, 1]^(q)(形式上,这个条件应该是 x[i] ∈ {0, 1}^q;然而,当值不是二进制时,优化也可以成功进行)。对于重建,也需要相同的约束;因此,当使用神经网络时,最常见的选择是使用 sigmoid 层。
TensorFlow 中的深度卷积自编码器示例
这个例子(以及本章和以下章节中的所有其他例子)基于 TensorFlow(有关 TensorFlow 的安装信息,请参阅本节末尾的信息框),因为这个框架允许更大的灵活性,这在 Keras 中有时会带来更多问题。我们将实用主义地处理这个例子,因此我们不会探索所有功能,因为它们超出了本书的范围;然而,感兴趣的读者可以参考 《TensorFlow 深度学习 第二版》,作者:Zaccone G.,Karim R.,Packt。
在这个例子中,我们将创建一个深度卷积自编码器,并使用 Fashion MNIST 数据集进行训练。第一步是加载数据(使用 Keras 辅助函数),归一化,为了加快计算速度,将训练集限制为 1,000 个样本:
import numpy as np
from keras.datasets import fashion_mnist
(X_train, _), (_, _) = fashion_mnist.load_data()
nb_samples = 1000
nb_epochs = 400
batch_size = 200
code_length = 256
X_train = X_train.astype(np.float32)[0:nb_samples] / 255.0
width = X_train.shape[1]
height = X_train.shape[2]
在这一点上,我们可以创建 Graph
,设置整个架构,它由以下部分组成:
-
编码器(所有层都有填充 "same" 和 ReLU 激活):
-
使用 32 个滤波器,核大小为(3 × 3),步长(2 × 2)的卷积
-
使用 64 个滤波器,核大小为(3 × 3),步长(1× 1)的卷积
-
使用 128 个滤波器,核大小为(3 × 3),步长(1 × 1)的卷积
-
-
解码器:
-
使用 128 个滤波器,核大小为(3 × 3),步长(2 × 2)的转置卷积
-
使用 64 个滤波器,核大小为(3 × 3),步长(1× 1)的转置卷积
-
使用 32 个滤波器,核大小为(3 × 3),步长(1 × 1)的转置卷积
-
使用 1 个滤波器,核大小为(3 × 3),步长(1 × 1)和 sigmoid 激活的转置卷积
-
由于图像大小为(28 × 28),我们更愿意将每个批次调整到(32 × 32)的尺寸,以便轻松管理所有基于 2 的幂次大小的后续操作:
import tensorflow as tf
graph = tf.Graph()
with graph.as_default():
input_images = tf.placeholder(tf.float32, shape=(None, width, height, 1))
r_input_images = tf.image.resize_images(input_images, (32, 32))
# Encoder
conv_0 = tf.layers.conv2d(inputs=r_input_images,
filters=32,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
conv_1 = tf.layers.conv2d(inputs=conv_0,
filters=64,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
conv_2 = tf.layers.conv2d(inputs=conv_1,
filters=128,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
# Code layer
code_input = tf.layers.flatten(inputs=conv_2)
code_layer = tf.layers.dense(inputs=code_input,
units=code_length,
activation=tf.nn.sigmoid)
# Decoder
decoder_input = tf.reshape(code_layer, (-1, 16, 16, 1))
convt_0 = tf.layers.conv2d_transpose(inputs=decoder_input,
filters=128,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
convt_1 = tf.layers.conv2d_transpose(inputs=convt_0,
filters=64,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
convt_2 = tf.layers.conv2d_transpose(inputs=convt_1,
filters=32,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
convt_3 = tf.layers.conv2d_transpose(inputs=convt_2,
filters=1,
kernel_size=(3, 3),
activation=tf.sigmoid,
padding='same')
# Loss
loss = tf.nn.l2_loss(convt_3 - r_input_images)
# Training step
training_step = tf.train.AdamOptimizer(0.001).minimize(loss)
损失函数是一个标准的 L2,没有任何其他约束。我邀请读者测试不同的优化器和学习率,以采用保证最小损失值的解决方案。在定义了Graph
之后,可以设置一个InteractiveSession
(或标准会话),初始化所有变量,并开始训练过程:
import numpy as np
import tensorflow as tf
session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()
for e in range(nb_epochs):
np.random.shuffle(X_train)
total_loss = 0.0
for i in range(0, nb_samples - batch_size, batch_size):
X = np.zeros((batch_size, width, height, 1), dtype=np.float32)
X[:, :, :, 0] = X_train[i:i + batch_size, :, :]
_, n_loss = session.run([training_step, loss],
feed_dict={
input_images: X
})
total_loss += n_loss
print('Epoch {}) Total loss: {}'.format(e + 1, total_loss))
一旦训练过程完成,我们可以检查整个数据集的平均编码长度(这个信息对于比较通过施加稀疏性约束得到的结果很有用):
import numpy as np
codes = session.run([code_layer],
feed_dict={
input_images: np.expand_dims(X_train, axis=3),
})[0]
print(np.mean(codes))
0.5545144
这个值非常小,表明表示已经相当稀疏;然而,我们将它与稀疏自编码器得到的平均值进行比较。现在我们可以通过编码和解码处理一些图像(10 个):
import numpy as np
Xs = np.reshape(X_train[0:10], (10, width, height, 1))
Ys = session.run([convt_3],
feed_dict={
input_images: Xs
})
Ys = np.squeeze(Ys[0] * 255.0)
结果如下所示:
原始图像(上排);解码图像(下排)
如您所见,重建过程相当损失较大,但自动编码器成功学会了如何降低输入样本的维度。作为一个练习,我邀请读者将代码分成两个独立的部分(编码器和解码器),并优化架构以在整个 Fashion MNIST 数据集上实现更高的准确率。
TensorFlow 适用于 Linux、Windows 和 OS X,支持 CPU 和 CUDA GPU。在许多情况下,可以使用pip install -U tensorflow
命令安装它;然而,我建议您阅读每个平台的更新说明,请参阅www.tensorflow.org/install/
。
去噪自编码器
自动编码器可以用来确定数据集的欠完备表示;然而,Bengio 等人(在 P. Vincent, H. Larochelle, I. Lajoie, Y. Bengio 和 P. Manzagol 的书籍《Stacked Denoising Autoencoders: Learning Useful Representations in a Deep Network with a Local Denoising Criterion》中,该书籍来自《Journal of Machine Learning Research 11/2010》)提出使用它们不是为了学习样本的精确表示以便从低维代码重建它,而是为了去噪输入样本。这并不是一个全新的想法,因为例如 Hopfield 网络(几十年前提出)有相同的目的,但其在容量方面的限制导致研究人员寻找不同的方法。如今,深度自动编码器可以轻松处理高维数据(如图像),这随之而来的空间需求,这就是为什么现在许多人正在重新考虑如何教会网络从损坏的图像开始重建样本图像的想法。
形式上,去噪自动编码器和标准自动编码器之间没有太多区别。然而,在这种情况下,编码器必须与噪声样本一起工作:
解码器的损失函数保持不变。如果为每个批次采样噪声,重复足够多的迭代次数,允许自动编码器学习如何在某些片段缺失或损坏时重建原始图像。为了达到这个目标,作者们提出了不同类型的噪声。最常见的选择是采样高斯噪声,它具有一些有用的特性,并且与许多真实的噪声过程相一致:
另一种可能性是使用输入丢弃层,将一些随机元素置零:
这种选择显然更为激进,并且必须适当调整比率。大量丢失的像素可能会不可逆地删除许多信息,重建可能会变得更加困难且刚性(我们的目的是扩展自动编码器对从同一分布中抽取的其他样本的能力)。或者,可以混合高斯噪声和 dropout 的噪声,以固定的概率在它们之间切换。显然,模型必须比标准自动编码器更复杂,因为现在它们必须处理缺失的信息;同样的概念也适用于代码长度:非常不完整的代码无法提供重建原始图像所需的所有元素,以最准确的方式。我建议测试所有可能性,特别是在噪声受外部条件限制时(例如,旧照片或通过受精确噪声过程影响的信道传输的消息)。如果模型还必须用于从未见过的样本,选择代表真实分布的样本至关重要,当元素数量不足以达到所需的精度水平时,使用数据增强技术(限于与特定问题兼容的操作)。
TensorFlow 中的去噪自动编码器示例
在本例(基于上一个例子)中,我们将采用一个非常相似的架构,但由于目标是去噪图像,我们将设置代码长度等于(宽度 × 高度),将所有步长设置为(1 × 1),因此我们不再需要调整图像大小:
import tensorflow as tf
graph = tf.Graph()
with graph.as_default():
input_noisy_images = tf.placeholder(tf.float32, shape=(None, width, height, 1))
input_images = tf.placeholder(tf.float32, shape=(None, width, height, 1))
# Encoder
conv_0 = tf.layers.conv2d(inputs=input_noisy_images,
filters=32,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
conv_1 = tf.layers.conv2d(inputs=conv_0,
filters=64,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
conv_2 = tf.layers.conv2d(inputs=conv_1,
filters=128,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
# Code layer
code_input = tf.layers.flatten(inputs=conv_2)
code_layer = tf.layers.dense(inputs=code_input,
units=width * height,
activation=tf.nn.sigmoid)
# Decoder
decoder_input = tf.reshape(code_layer, (-1, width, height, 1))
convt_0 = tf.layers.conv2d_transpose(inputs=decoder_input,
filters=128,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
convt_1 = tf.layers.conv2d_transpose(inputs=convt_0,
filters=64,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
convt_2 = tf.layers.conv2d_transpose(inputs=convt_1,
filters=32,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
convt_3 = tf.layers.conv2d_transpose(inputs=convt_2,
filters=1,
kernel_size=(3, 3),
activation=tf.sigmoid,
padding='same')
# Loss
loss = tf.nn.l2_loss(convt_3 - input_images)
# Training step
training_step = tf.train.AdamOptimizer(0.001).minimize(loss)
在这种情况下,我们需要传递噪声图像(通过placeholder input_noisy_images
)和原始图像(用于计算最终的 L2 损失函数)。在我们的例子中,我们决定使用标准差为σ = 0.2
的高斯噪声(剪辑最终值,以确保它们始终介于 0 和 1 之间):
import numpy as np
import tensorflow as tf
session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()
for e in range(nb_epochs):
total_loss = 0.0
for i in range(0, nb_samples - batch_size, batch_size):
X = np.zeros((batch_size, width, height, 1), dtype=np.float32)
X[:, :, :, 0] = X_train[i:i + batch_size, :, :]
Xn = np.clip(X + np.random.normal(0.0, 0.2, size=(batch_size, width, height, 1)), 0.0, 1.0)
_, n_loss = session.run([training_step, loss],
feed_dict={
input_images: X,
input_noisy_images: Xn
})
total_loss += n_loss
print('Epoch {}) Total loss: {}'.format(e + 1, total_loss))
经过 200 个 epoch 后的结果如图所示:
噪声样本(上排);去噪样本(下排)
去噪自动编码器已经成功地学会了在有高斯噪声的情况下重建原始图像。我邀请读者测试其他方法(例如使用初始 dropout)并提高噪声水平,以了解该模型可以有效地去除的最大破坏程度。
稀疏自动编码器
通常,标准自编码器产生密集的内部表示。这意味着大多数值与零不同。然而,在某些情况下,具有稀疏代码可能更有用,可以更好地表示属于字典的原子。在这种情况下,如果z[i] = (0, 0, z[i]^n, ..., 0, z[i]^m, ...), 我们可以将每个样本视为特定原子的加权重叠。为了实现这一目标,我们可以简单地应用 L1 惩罚到代码层,如第一章中所述,机器学习 模型 基础。因此,单个样本的损失函数变为以下:
在这种情况下,我们需要考虑额外的超参数α,它必须调整以在不影响准确性的情况下增加稀疏度。作为一个一般性的经验法则,我建议从等于 0.01 的值开始,并减少它,直到达到期望的结果。在大多数情况下,更高的值会导致非常糟糕的性能,因此通常避免使用。
Andrew Ng(在他的书稀疏自编码器,CS294A,斯坦福大学)提出了不同的方法。如果我们将代码层视为一组独立的伯努利随机变量,我们可以通过考虑一个具有非常低平均值(例如,p[r] = 0.01)的通用参考伯努利变量,并将通用元素z[i]^((j))与p[r]之间的 Kullback-Leibler 散度添加到成本函数中,来强制执行稀疏性。对于单个样本,额外的项如下(p是代码长度):
结果损失函数变为以下:
这种惩罚的效果类似于 L1(考虑相同的α超参数问题),但许多实验已经证实,由此产生的成本函数更容易优化,并且可以达到达到更高重建精度的相同稀疏度。当与稀疏自编码器一起工作时,由于假设单个元素由少量原子组成(与字典大小相比),代码长度通常较大。因此,我建议您用不同的代码长度评估稀疏度水平,并选择最大化前者同时最小化后者的组合。
向 Fashion MNIST 深度卷积自编码器添加稀疏性
在这个例子中,我们将在第一个练习中定义的成本函数中添加一个 L1 正则化项:
import tensorflow as tf
...
# Loss
sparsity_constraint = tf.reduce_sum(0.001 * tf.norm(code_layer, ord=1, axis=1))
loss = tf.nn.l2_loss(convt_3 - r_input_images) + sparsity_constraint
...
训练过程完全相同,因此我们可以直接展示 200 个 epoch 后的最终代码均值:
import numpy as np
codes = session.run([code_layer],
feed_dict={
input_images: np.expand_dims(X_train, axis=3),
})[0]
print(np.mean(codes))
0.45797634
如您所见,均值现在更低,这表明更多的代码值接近 0。我邀请读者实现其他策略,考虑到创建一个填充小值(例如,0.01)的常量向量更容易,并且可以利用 TensorFlow 提供的向量化特性。我还建议通过将其拆分为熵项 H(p[r])(这是常数)和交叉熵 H(z, p[r]) 项来简化 Kullback–Leibler 散度。
变分自动编码器
变分自动编码器(VAE)是由 Kingma 和 Wellin 提出的一种生成模型(在他们的工作 Auto-Encoding Variational Bayes, arXiv:1312.6114 [stat.ML] 中),它在某种程度上类似于标准自动编码器,但它有一些基本的内部差异。实际上,目标不是找到数据集的编码表示,而是确定一个生成过程的参数,该过程能够根据输入数据生成过程产生所有可能的输出。
让我们以一个基于可学习参数向量 θ 和一组具有概率密度函数 p(z;θ) 的潜在变量 z 的模型为例。因此,我们的目标可以表达为研究 θ 参数,以最大化边缘分布 p(x;**θ)(通过联合概率 p(x,z;θ) 的积分获得)的可能性:
如果这个问题可以很容易地以闭式形式解决,那么从数据生成过程 p(x) 中抽取的大量样本就足以找到好的 p(x;θ) 近似。不幸的是,由于真实的先验 p(z) 未知(这是一个次要问题,因为我们可以轻易地做出一些有用的假设),并且后验分布 p(x|z;θ) 几乎总是接近零,所以前面的表达式在大多数情况下都是不可处理的。第一个问题可以通过选择一个简单的先验(最常见的选择是 z ∼ N(0, I))来解决,但第二个问题仍然非常困难,因为只有少数 z 值可以导致生成可接受的样本。这尤其适用于数据集非常高维和复杂(例如,图像)的情况。即使有数百万种组合,也只有少数可以产生真实的样本(如果图像是汽车的照片,我们期望在下半部分有四个轮子,但仍然有可能生成轮子在顶部的样本)。因此,我们需要利用一种方法来减少样本空间。变分贝叶斯方法(阅读 C. Fox 和 S. Roberts 的作品 A Tutorial on Variational Bayesian Inference 来自 Orchid 以获取更多信息)基于使用 代理 分布的想法,这些分布易于采样,在这种情况下,其密度非常高(即生成合理输出的概率远高于真实后验)。
在这种情况下,我们定义一个近似后验,考虑到标准自动编码器的架构。特别是,我们可以引入一个 q(z|x;θ[q]) 分布,它充当一个编码器(不再表现出确定性),可以用神经网络轻松建模。我们的目标,当然是找到最佳的 θ[q] 参数集,以最大化 q 与真实后验分布 p(z|x;θ) 之间的相似性。这个结果可以通过最小化 Kullback–Leibler 散度来实现:
在最后一个公式中,项 log p(x;**θ) 不依赖于 z,因此可以从期望值算子中提取出来,并且表达式可以被操作以简化它:
该方程也可以重写为以下形式:
在右侧,我们现在有项 ELBO(简称 证据下界)和概率编码器 q(z|x;θ[q]) 与真实后验分布 p(z|x;**θ) 之间的 Kullback–Leibler 散度。由于我们想要最大化在 θ 参数化下的样本的对数概率,并且考虑到 KL 散度总是非负的,我们只能处理 ELBO(这比其他项更容易管理)。实际上,我们将优化的损失函数是负 ELBO。为了实现这个目标,我们需要两个更重要的步骤。
第一个选择是为 q(z|x;θ[q]) 选择一个合适的结构。由于假设 p(z;θ) 是正态分布,我们可以假设将 q(z|x;θ[q]) 模型化为一个多元高斯分布,将概率编码器分为两个块,这两个块使用相同的底层:
-
一个均值生成器 μ(z|x;θ[q]),输出一个 μ[i] ∈ ℜ^p 向量
-
一个协方差生成器 Σ(z|x;θ[q]**)(假设为对角矩阵),输出一个 σ[i] ∈ ℜ^p 向量,使得 Σ[i]=diag(σ[i])
这样,q(z|x;θ[q]**) = N(μ(z|x;θ[q]), Σ(z|x;θ[q])),因此右手边的第二项是两个高斯分布之间的 Kullback-Leibler 散度,可以很容易地表示如下(p 是均值和协方差向量的维度):
这个操作比预期的要简单,因为,由于 Σ 是对角矩阵,迹对应于元素 Σ[1] + Σ[2] + [...] + Σ[p] 的和,并且 log(|Σ|) = log(Σ[1]Σ[2]...Σ[p]) = log Σ[1] + log Σ[2] + ... + log Σ[p].
在这一点上,最大化上一个表达式的右侧等同于最大化生成可接受样本的期望对数概率,并最小化正态先验与编码器合成的高斯分布之间的差异。现在看起来似乎简单多了,但仍然有一个问题需要解决。我们希望使用神经网络和随机梯度下降算法,因此我们需要可微函数。由于 Kullback-Leibler 散度只能使用包含n个元素的 minibatch(在足够多的迭代后,近似值接近真实值),因此有必要从分布N(μ(z|x;θ[q]), Σ(z|x;θ[q]))中采样n个值,而且不幸的是,这个操作是不可微分的。为了解决这个问题,作者提出了一种重新参数化技巧:我们不是从q(z|x;θ[q])中采样,而是可以从一个正态分布中采样,ε ∼ N(0, I),并构建实际的样本作为μ(z|x;θ[q]) + ε · Σ(z|x;θ[q])²。考虑到ε在批次中是一个常数向量(正向和反向阶段都是),很容易计算相对于前面表达式的梯度并优化解码器和编码器。
我们需要考虑的最后一个元素是我们要最大化的表达式的右侧的第一个项:
这个术语表示实际分布与重建分布之间的负交叉熵。如第一部分所述,有两种可行选择:高斯分布或伯努利分布。一般来说,变分自编码器使用伯努利分布,输入样本和重建值被限制在 0 和 1 之间。然而,许多实验已经证实均方误差可以加速训练过程,因此我建议读者测试这两种方法,并选择保证最佳性能的方法(无论是准确性还是训练速度)。
TensorFlow 中的变分自编码器示例
让我们继续使用 Fashion MNIST 数据集来构建变分自编码器。如解释所述,编码器的输出现在分为两个部分:均值和协方差向量(两者维度都等于(width · height))和解码器输入是通过从正态分布中采样并将代码组件投影得到的。完整的Graph
如下:
import tensorflow as tf
graph = tf.Graph()
with graph.as_default():
input_images = tf.placeholder(tf.float32, shape=(batch_size, width, height, 1))
# Encoder
conv_0 = tf.layers.conv2d(inputs=input_images,
filters=32,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
conv_1 = tf.layers.conv2d(inputs=conv_0,
filters=64,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
conv_2 = tf.layers.conv2d(inputs=conv_1,
filters=128,
kernel_size=(3, 3),
activation=tf.nn.relu,
padding='same')
# Code layer
code_input = tf.layers.flatten(inputs=conv_2)
code_mean = tf.layers.dense(inputs=code_input,
units=width * height)
code_log_variance = tf.layers.dense(inputs=code_input,
units=width * height)
code_std = tf.sqrt(tf.exp(code_log_variance))
# Normal samples
normal_samples = tf.random_normal(mean=0.0, stddev=1.0, shape=(batch_size, width * height))
# Sampled code
sampled_code = (normal_samples * code_std) + code_mean
# Decoder
decoder_input = tf.reshape(sampled_code, (-1, 7, 7, 16))
convt_0 = tf.layers.conv2d_transpose(inputs=decoder_input,
filters=64,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
convt_1 = tf.layers.conv2d_transpose(inputs=convt_0,
filters=32,
kernel_size=(3, 3),
strides=(2, 2),
activation=tf.nn.relu,
padding='same')
convt_2 = tf.layers.conv2d_transpose(inputs=convt_1,
filters=1,
kernel_size=(3, 3),
padding='same')
convt_output = tf.nn.sigmoid(convt_2)
# Loss
reconstruction = tf.nn.sigmoid_cross_entropy_with_logits(logits=convt_2, labels=input_images)
kl_divergence = 0.5 * tf.reduce_sum(tf.square(code_mean) + tf.square(code_std) - tf.log(1e-8 + tf.square(code_std)) - 1, axis=1)
loss = tf.reduce_sum(reconstruction) + kl_divergence
# Training step
training_step = tf.train.AdamOptimizer(0.001).minimize(loss)
如您所见,唯一的区别如下:
-
编码器输入的生成是
(normal_samples * code_std) + code_mean
-
使用 sigmoid 交叉熵作为重建损失
-
Kullback-Leibler 散度作为正则化项的存在
训练过程与本章第一个示例相同,因为采样操作是由 TensorFlow 直接执行的。200 个 epoch 后的结果如下所示:
变分自动编码器输出
作为练习,我邀请读者使用 RGB 数据集(例如 Cifar-10,可在www.cs.toronto.edu/~kriz/cifar.html
找到)来测试 VAE 的生成能力,通过比较输出样本与从原始分布中抽取的样本进行比较。
在这类实验中,随机数由 NumPy 和 TensorFlow 共同生成,随机种子始终设置为 1,000(np.random.seed(1000)
和tf.set_random_seed(1000)
)。其他值或未重置种子的后续测试可能会产生略微不同的结果。
摘要
在本章中,我们将自动编码器作为无监督模型介绍,它可以学习用低维代码表示高维数据集。它们被结构化为两个独立的模块(尽管它们是共同训练的):一个编码器,负责将输入样本映射到内部表示,以及一个解码器,它必须执行逆操作,从代码重建原始图像。
我们还讨论了如何使用自动编码器来去噪样本,以及如何对代码层施加稀疏性约束,以类似于标准字典学习的概念。最后一个主题是关于一种稍微不同的模式,称为变分自动编码器。其想法是构建一个生成模型,能够重现属于训练分布的所有可能的样本。
在下一章中,我们将简要介绍一个非常重要的模型家族,称为生成对抗网络(GANs),它与变分自动编码器的目的非常相似,但采用了更加灵活的方法。
第十二章:生成对抗网络
在本章中,我们将简要介绍基于一些博弈论概念的生成模型家族。它们的主要特点是针对学习区分真实样本和伪造样本的对抗性训练过程,同时,推动另一个生成越来越接近训练样本的样本的组件。
特别是,我们将讨论:
-
对抗性训练和标准生成对抗网络(GANs)
-
深度卷积生成对抗网络(DCGAN)
-
Wasserstein GANs(WGAN)
对抗性训练
Goodfellow 和其他人(在 Generative Adversarial Networks,Goodfellow I.J., Pouget-Abadie J., Mirza M., Xu B., Warde-Farley D., Ozair S., Courville A., Bengio Y., arXiv:1406.2661 [stat.ML])提出的对抗性训练的杰出想法,引领了一代新的生成模型,这些模型立即优于大多数现有算法。所有衍生模型都基于相同的对抗性训练基本概念,这是一种部分受博弈论启发的途径。
假设我们有一个数据生成过程,pdata,它代表一个实际的数据分布和有限数量的样本,我们假设这些样本是从 p[data] 中抽取的:
我们的目标是训练一个称为生成器的模型,其分布必须尽可能接近 p[data]。这是算法中最棘手的部分,因为与标准方法(例如,变分自编码器)不同,对抗性训练基于两个玩家之间的最小-最大博弈(我们可以简单地说,给定一个目标,两个玩家的目标是使最大可能的损失最小化;但在这个案例中,他们各自工作在不同的参数上)。一个玩家是生成器,我们可以将其定义为一个噪声样本的参数化函数:
生成器被一个噪声向量(在这种情况下,我们使用了均匀分布,但没有任何特别的限制;因此,我们只是简单地说 z 是从噪声分布 p[noise] 中抽取的),并输出一个与从 p[data] 中抽取的样本具有相同维度的值。在没有进一步控制的情况下,生成器的分布将完全不同于数据生成过程,但这是另一个玩家进入场景的时刻。第二个模型被称为 判别器(或评论家),它负责评估从 p[data] 中抽取的样本和由生成器产生的样本:
该模型的作用是输出一个概率,这个概率必须反映样本是从 p[data] 中抽取的,而不是由 G(z; θ[g]) 生成的。发生的情况非常简单:第一个玩家(生成器)输出一个样本,x。如果 x 实际上属于 p[data],判别器将输出一个接近 1 的值,而如果它与其他真实样本非常不同,D(x; θ[d]) 将输出一个非常低的概率。游戏的真正结构基于训练生成器欺骗判别器的想法,通过产生可以潜在地从 p[data] 中抽取的样本。通过在 x 是从 p[data] 中抽取的真实样本时尝试最大化对数概率,log(D(x; θ[d]**)),同时最小化对数概率,log(1 - D(G(z; θ[g]); θ[d]))*,其中 z 从噪声分布中抽取。
第一个操作迫使判别器越来越意识到真实样本(这个条件是必要的,以避免被轻易欺骗)。第二个目标稍微复杂一些,因为判别器必须评估一个可能被接受或不被接受的样本。假设生成器不够聪明,输出的样本不属于 p[data]。随着判别器学习 p[data] 的结构,它很快就会区分出错误的样本,输出一个低概率。因此,通过最小化 log(1 - D(G(z; θ[g]); θ[d])),我们迫使判别器在样本与从 p[d][ata] 中抽取的样本相当不同时越来越挑剔,从而使生成器越来越能够产生可接受的样本。另一方面,如果生成器输出的样本属于数据生成过程,判别器将输出一个高概率,最小化将回到之前的情况。
作者使用一个共享的价值函数,V(G, D),来表示这个最小-最大游戏,该函数必须由生成器最小化,由判别器最大化:
这个公式代表了两个玩家之间非合作博弈的动力学(欲了解更多信息,请参阅Tadelis S., 博弈论,普林斯顿大学出版社)。从理论上讲,这种博弈可以接受一种特殊配置,称为纳什均衡,可以这样描述:如果两个玩家知道彼此的策略,那么如果对方玩家不改变自己的策略,他们就没有理由改变自己的策略。在这种情况下,判别器和生成器都会追求自己的策略,直到不再需要改变,达到一个最终、稳定的配置,这可能是纳什均衡(即使有许多因素可能阻止达到这个目标)。一个常见的问题是判别器过早收敛,这导致梯度消失,因为损失函数在接近 0 的区域变得平坦。由于这是一个博弈,一个基本条件是提供信息以允许玩家进行纠正。如果判别器学习如何快速区分真实样本和伪造样本,那么生成器的收敛速度会减慢,玩家可能会被困在次优配置中。一般来说,当分布相当复杂时,判别器比生成器慢;但在某些情况下,在每次单独更新判别器之后,可能需要更新生成器更多次。不幸的是,没有经验法则;例如,当处理图像时,可以观察在足够多的迭代后生成的样本。如果判别器损失变得非常小,并且样本看起来被破坏或不连贯,这意味着生成器没有足够的时间学习分布,需要减慢判别器的速度。
上文提到的论文的作者表明,给定一个由分布 pg 特征化的生成器,最优判别器是:
在这一点上,考虑到前面的价值函数 V(G, D) 和使用最优判别器,我们可以将其重写为一个单一目标(作为 G 的函数),生成器必须最小化这个目标:
为了更好地理解生成对抗网络(GAN)的工作原理,我们需要扩展前面的表达式:
通过一些简单的操作,我们得到以下结果:
最后一个项代表了 p[data] 和 p[g] 之间的 Jensen-Shannon 散度。这个度量与 Kullback-Leibler 散度相似,但它是对称的,并且介于 0 和 log(2) 之间。当两个分布相同,D[JS] = 0;但如果它们的支撑(p(x) > 0 的值集)不相交,*D[JS]** = log(2)(而 D[KL] = ∞)。因此,价值函数可以表示为:
现在,应该更清楚的是,GAN 尝试最小化数据生成过程和生成器分布之间的 Jensen-Shannon 散度。一般来说,这个程序相当有效;然而,当支持集不连续时,GAN 没有关于真实距离的信息。这种考虑(在 Improved Techniques for Training GANs,Salimans T., Goodfellow I., Zaremba W., Cheung V., Radford A.,and Chen X., arXiv:1606.03498 [cs.LG]) 中以更严格的数学方式分析)解释了为什么训练 GAN 可以变得相当困难,并且因此,为什么在许多情况下无法找到纳什均衡。出于这些原因,我们将在下一节分析一种替代方法。
完整的 GAN 算法(如作者所提出)如下:
-
设置迭代次数,N[epochs]
-
设置判别器迭代次数,N[iter](在大多数情况下,N[iter] = 1)
-
设置批量大小,k
-
定义一个噪声生成过程,M(例如,U(-1, 1))
-
对于 e=1 到 N[epochs]:
-
从 X 中采样 k 个值
-
从 N 中采样 k 个值
-
对于 i=1 到 N[iter]:
-
计算梯度,∇[d] V(G, D)(仅针对判别器变量)。期望值通过样本均值近似。
-
通过随机梯度上升更新判别器参数(由于我们处理对数,可以最小化负损失)。
-
-
从 N 中采样 k 个值
-
计算梯度,∇[g] Vnoise(仅针对生成器变量)
-
通过随机梯度下降更新生成器参数
-
由于这些模型需要采样噪声向量以保证可重复性,我建议在 NumPy (np.random.seed(...)
) 和 TensorFlow (tf.set_random_seed(...)
) 中设置随机种子。所有这些实验的默认值选择为 1,000。
TensorFlow 中的 DCGAN 示例
在本例中,我们希望使用 Fashion-MNIST 数据集(通过 keras
辅助函数获得)构建一个 DCGAN(由 Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks 提出,Radford A., Metz L., Chintala S., , arXiv:1511.06434 [cs.LG])。由于训练速度不是很高,我们将样本数量限制为 5,000,但我建议使用更大的值重复实验。第一步是加载并归一化(介于 -1 和 1 之间)数据集:
import numpy as np
from keras.datasets import fashion_mnist
nb_samples = 5000
(X_train, _), (_, _) = fashion_mnist.load_data()
X_train = X_train.astype(np.float32)[0:nb_samples] / 255.0
X_train = (2.0 * X_train) - 1.0
width = X_train.shape[1]
height = X_train.shape[2]
根据原始论文,生成器基于四个大小为 (4, 4) 且步长为 (2, 2) 的转置卷积。输入是一个单通道像素 (1 × 1 × code_length),随后通过后续卷积进行扩展。滤波器的数量为 1024、512、256、128 和 1(我们处理的是灰度图像)。作者建议使用对称值数据集(这就是为什么我们在 -1 和 1 之间进行归一化的原因),在每个层后进行批量归一化,并使用漏斗 ReLU 激活(默认负斜率设置为 0.2):
import tensorflow as tf
def generator(z, is_training=True):
with tf.variable_scope('generator'):
conv_0 = tf.layers.conv2d_transpose(inputs=z,
filters=1024,
kernel_size=(4, 4),
padding='valid')
b_conv_0 = tf.layers.batch_normalization(inputs=conv_0, training=is_training)
conv_1 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_0),
filters=512,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)
conv_2 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_1),
filters=256,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)
conv_3 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_2),
filters=128,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)
conv_4 = tf.layers.conv2d_transpose(inputs=tf.nn.leaky_relu(b_conv_3),
filters=1,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
return tf.nn.tanh(conv_4)
步长设置为与 64 × 64 图像一起工作(遗憾的是,Fashion-MNIST 数据集有 28 × 28 样本,无法使用二进制幂模块生成);因此,在训练过程中我们将调整样本大小。由于我们需要分别计算判别器和生成器的梯度,因此有必要设置变量作用域(使用上下文管理器 tf.variable_scope()
)以立即提取只有名称具有作用域作为前缀的变量(例如,generator/Conv_1_1/...
)。is_training
参数在生成阶段是必要的,以禁用批归一化。
判别器几乎与生成器相同(唯一的主要区别是逆卷积序列和第一层之后没有批归一化):
import tensorflow as tf
def discriminator(x, is_training=True, reuse_variables=True):
with tf.variable_scope('discriminator', reuse=reuse_variables):
conv_0 = tf.layers.conv2d(inputs=x,
filters=128,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
conv_1 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(conv_0),
filters=256,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_1 = tf.layers.batch_normalization(inputs=conv_1, training=is_training)
conv_2 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_1),
filters=512,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_2 = tf.layers.batch_normalization(inputs=conv_2, training=is_training)
conv_3 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_2),
filters=1024,
kernel_size=(4, 4),
strides=(2, 2),
padding='same')
b_conv_3 = tf.layers.batch_normalization(inputs=conv_3, training=is_training)
conv_4 = tf.layers.conv2d(inputs=tf.nn.leaky_relu(b_conv_3),
filters=1,
kernel_size=(4, 4),
padding='valid')
return conv_4
在这种情况下,我们有一个额外的参数(reuse_variables
),在构建损失函数时是必要的。实际上,我们需要声明两个判别器(分别用真实样本和生成器输出进行喂养),但它们不是由单独的层组成;因此,第二个必须重用第一个定义的变量。现在我们可以创建一个图并定义所有占位符和操作:
import tensorflow as tf
code_length = 100
graph = tf.Graph()
with graph.as_default():
input_x = tf.placeholder(tf.float32, shape=(None, width, height, 1))
input_z = tf.placeholder(tf.float32, shape=(None, code_length))
is_training = tf.placeholder(tf.bool)
gen = generator(z=tf.reshape(input_z, (-1, 1, 1, code_length)), is_training=is_training)
r_input_x = tf.image.resize_images(images=input_x, size=(64, 64))
discr_1_l = discriminator(x=r_input_x, is_training=is_training, reuse_variables=False)
discr_2_l = discriminator(x=gen, is_training=is_training, reuse_variables=True)
loss_d_1 = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.ones_like(discr_1_l), logits=discr_1_l))
loss_d_2 = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.zeros_like(discr_2_l), logits=discr_2_l))
loss_d = loss_d_1 + loss_d_2
loss_g = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.ones_like(discr_2_l), logits=discr_2_l))
variables_g = [variable for variable in tf.trainable_variables() if variable.name.startswith('generator')]
variables_d = [variable for variable in tf.trainable_variables() if variable.name.startswith('discriminator')]
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
training_step_d = tf.train.AdamOptimizer(0.0002, beta1=0.5).minimize(loss=loss_d, var_list=variables_d)
training_step_g = tf.train.AdamOptimizer(0.0002, beta1=0.5).minimize(loss=loss_g, var_list=variables_g)
第一步是定义占位符:
-
input_x
包含从 X 中抽取的真实样本 -
input_z
包含噪声样本 -
is_training
是一个布尔值,指示是否必须激活批归一化
然后,我们在将噪声样本重塑为 (1 × 1 × code_length) 矩阵后定义生成器实例(这是为了有效地使用转置卷积)。由于这是一个基本超参数,我建议测试不同的值并比较最终的性能。
如前所述,在定义两个判别器之前(第二个重用先前定义的变量)输入图像被调整大小。discr_1_l
实例用真实样本进行喂养,而 discr_2_l
使用生成器输出工作。
下一步是定义损失函数。由于我们使用对数,当值接近 0 时可能会出现稳定性问题。因此,最好使用 TensorFlow 内置函数 tf.nn.sigmoid_cross_entropy_with_logits()
,该函数在所有情况下都保证数值稳定性。此函数接受一个 logit 作为输入,并在内部应用 sigmoid 变换。通常,输出如下:
因此,将标签设置为 1 将迫使第二项为零,反之亦然。此时,我们需要创建两个列表,包含每个作用域的变量(这可以通过使用tf.trainable_variables()
函数轻松实现,该函数输出所有变量的列表)。最后一步是定义优化器。如官方 TensorFlow 文档中建议的,当与批量归一化一起工作时,有必要将训练操作包装在一个上下文管理器中,该管理器检查是否已计算所有依赖项(在这种情况下,批量平均值和方差)。我们使用了 Adam 优化器,η = 0.0002,以及一个梯度动量遗忘因子(μ1)等于 0.5(这是一个受高动量可能导致的潜在不稳定性所驱动的选择)。正如所见,在两种情况下,最小化仅限于变量的特定子集(通过var_list
参数提供列表)。
到目前为止,我们可以创建一个Session
(我们将使用InteractiveSession
),初始化所有变量,并开始训练过程(200 个周期和批大小为 128):
import numpy as np
import tensorflow as tf
nb_epochs = 200
batch_size = 128
nb_iterations = int(nb_samples / batch_size)
session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()
samples_range = np.arange(nb_samples)
for e in range(nb_epochs * 5):
d_losses = []
g_losses = []
for i in range(nb_iterations):
Xi = np.random.choice(samples_range, size=batch_size)
X = np.expand_dims(X_train[Xi], axis=3)
Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)
_, d_loss = session.run([training_step_d, loss_d],
feed_dict={
input_x: X,
input_z: Z,
is_training: True
})
d_losses.append(d_loss)
Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)
_, g_loss = session.run([training_step_g, loss_g],
feed_dict={
input_x: X,
input_z: Z,
is_training: True
})
g_losses.append(g_loss)
print('Epoch {}) Avg. discriminator loss: {} - Avg. generator loss: {}'.format(e + 1, np.mean(d_losses), np.mean(g_losses)))
训练步骤(包含单个判别器迭代)分为两个阶段:
-
使用真实图像和噪声样本批次的判别器训练
-
使用噪声样本批次的生成器训练
一旦训练过程完成,我们可以通过执行包含噪声样本矩阵的生成器来生成一些图像(50 个):
Z = np.random.uniform(-1.0, 1.0, size=(50, code_length)).astype(np.float32)
Ys = session.run([gen],
feed_dict={
input_z: Z,
is_training: False
})
Ys = np.squeeze((Ys[0] + 1.0) * 0.5 * 255.0).astype(np.uint8)
结果如下截图所示:
使用 Fashion-MNIST 数据集训练的 DCGAN 生成的样本
作为练习,我邀请读者使用更复杂的卷积架构和 RGB 数据集,如 CIFAR-10 (www.cs.toronto.edu/~kriz/cifar.html
)。
即使这个示例和下一个示例的训练阶段仅限于 5,000 个样本,也可能相当慢(大约 12-15 小时),尤其是在没有 GPU 的情况下。读者可以通过减少网络的复杂性(注意形状)和减少样本数量来简化示例。为了避免不匹配,我建议在生成器实例之后添加print(gen.shape)
命令。预期的形状应该是(?, 64, 64, 1)
。或者,可以采用较小的目标维度(如 32×32),将其中一个步长(可能是最后一个)设置为(1, 1)
。
水晶石生成对抗网络(WGAN)
如前节所述,标准生成对抗网络(GAN)中最困难的问题之一是由基于 Jensen-Shannon 散度的损失函数引起的,当两个分布具有不连续的支持时,其值会变得恒定。这种情况在高维、语义结构化数据集中相当常见。例如,图像被限制具有特定的特征以表示特定的主题(这是在第二章,《半监督学习导论》中讨论的流形假设的结果)。初始生成器分布与真实数据集重叠的可能性非常低,在许多情况下,它们彼此之间也非常遥远。这种条件增加了学习错误表示(一个被称为模式崩溃的问题)的风险,即使判别器能够区分真实样本和生成样本(这种情况发生在判别器相对于生成器学习得太快时)。此外,纳什均衡的实现变得更加困难,GAN 很容易陷入次优配置。
为了减轻这个问题,Arjovsky、Chintala 和 Bottou(在Wasserstein GAN,Arjovsky M.,Chintala S.,Bottou L.,arXiv:1701.07875 [stat.ML])提出了采用不同的散度,称为Wasserstein 距离(或地球迁移距离),其正式定义如下:
术语∏(p[data], p[g])表示p[data]和p[g]之间所有可能的联合概率分布的集合。因此,Wasserstein 距离是所有期望值集合的下确界(考虑所有联合分布),其中x和y是从联合分布μ中抽取的。D[W]的主要特性是,即使两个分布具有不连续的支持,其值也正比于实际的分布距离。形式证明并不复杂,但直观上更容易理解。事实上,给定两个具有不连续支持分布,下确界运算强制取每个可能样本对之间的最短距离。显然,这种度量比 Jensen-Shannon 散度更稳健,但有一个实际的缺点:它极其难以计算。由于我们无法处理所有可能的联合分布(也不能使用近似),需要进一步步骤来应用这个损失函数。在上述论文中,作者证明了可以通过 Kantorovich-Rubinstein 定理(这个主题相当复杂,但读者可以在On the Kantorovich–Rubinstein Theorem,Edwards D. A., Expositiones Mathematicae, 2011 中找到更多信息)应用一个变换:
首先考虑的是f(•)的性质。定理要求只考虑 L-Lipschitz 函数,这意味着f(•)(假设是一个单变量的实值函数)必须遵守:
到这一点,Wasserstein 距离与两个期望值之差的上确界(相对于所有 L-Lipschitz 函数)成正比,这两个期望值非常容易计算。在 WGAN 中,f(•)函数由一个神经网络表示;因此,我们没有关于 Lipschitz 条件的保证。为了解决这个问题,作者提出了一种非常简单的程序:在应用校正后裁剪判别器(通常称为 Critic,其责任是表示参数化函数f(•))变量。如果输入是有界的,所有的变换都将产生有界输出;然而,裁剪因子必须足够小(0.01,甚至更小),以避免多次操作产生的加性效应导致 Lipschitz 条件的反转。这不是一个有效的解决方案(因为它在不必要的时候会减慢训练过程),但它允许在没有任何形式约束函数族的情况下利用 Kantorovich-Rubinstein 定理。
使用参数化函数(例如深度卷积网络),Wasserstein 距离如下(省略常数项 L):
在前面的表达式中,我们明确提取了生成器的输出,并在最后一步,将将单独优化的项分离出来。读者可能已经注意到,计算比标准的 GAN 要简单,因为在这种情况下,我们只需要对一批的f(•)值进行平均(不再需要对数)。然而,由于 Critic 变量被裁剪,所需的迭代次数通常更多,为了补偿 Critic 和生成器训练速度之间的差异,通常需要设置N[critic] > 1(作者建议值为 5,但这是一个必须在每个特定环境中调整的超参数)。
完整的 WGAN 算法是:
-
设置 epoch 的数量,N[epochs]。
-
设置 Critic 迭代的次数,N[critic](在大多数情况下,N[iter] = 5)。
-
设置批量大小,k。
-
设置裁剪常数,c(例如,c = 0.01)。
-
定义一个噪声生成过程,M(例如,U(-1, 1))。
-
对于e=1到N[epochs]:
-
从X中采样k个值。
-
从N中采样k个值。
-
对于i=1到N[critic]:
-
计算梯度,∇[c] DW(仅针对 Critic 变量)。期望值通过样本均值来近似。
-
通过随机梯度上升更新 Critic 参数。
-
将 Critic 参数裁剪到范围[-c, c]内。
-
-
从N中采样k个值。
-
计算梯度,∇[g] W[noise](仅针对生成器变量)。
-
通过随机梯度下降更新生成器参数。
-
TensorFlow 中的 WGAN 示例
这个示例可以被视为上一个示例的变体,因为它使用了相同的 dataset、generator 和 discriminator。唯一的主要区别是,在这种情况下,discriminator(连同其变量作用域)已被重命名为critic()
:
import tensorflow as tf
def critic(x, is_training=True, reuse_variables=True):
with tf.variable_scope('critic', reuse=reuse_variables):
...
在这一点上,我们可以直接进入创建包含所有占位符、操作和损失函数的Graph
:
import tensorflow as tf
graph = tf.Graph()
with graph.as_default():
input_x = tf.placeholder(tf.float32, shape=(None, width, height, 1))
input_z = tf.placeholder(tf.float32, shape=(None, code_length))
is_training = tf.placeholder(tf.bool)
gen = generator(z=tf.reshape(input_z, (-1, 1, 1, code_length)), is_training=is_training)
r_input_x = tf.image.resize_images(images=input_x, size=(64, 64))
crit_1_l = critic(x=r_input_x, is_training=is_training, reuse_variables=False)
crit_2_l = critic(x=gen, is_training=is_training, reuse_variables=True)
loss_c = tf.reduce_mean(crit_2_l - crit_1_l)
loss_g = tf.reduce_mean(-crit_2_l)
variables_g = [variable for variable in tf.trainable_variables() if variable.name.startswith('generator')]
variables_c = [variable for variable in tf.trainable_variables() if variable.name.startswith('critic')]
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
optimizer_c = tf.train.AdamOptimizer(0.00005, beta1=0.5, beta2=0.9).minimize(loss=loss_c, var_list=variables_c)
with tf.control_dependencies([optimizer_c]):
training_step_c = tf.tuple(tensors=[tf.assign(variable, tf.clip_by_value(variable, -0.01, 0.01))
for variable in variables_c])
training_step_g = tf.train.AdamOptimizer(0.00005, beta1=0.5, beta2=0.9).minimize(loss=loss_g, var_list=variables_g)
如您所见,占位符部分、生成器的定义以及将图像调整到 64 × 64 目标尺寸的操作之间没有差异。在下一个块中,我们定义了两个 Critic 实例(它们与上一个示例中声明的实例完全类似)。
两个损失函数比标准的 GAN 更简单,因为它们直接与 Critic 输出一起工作,计算批次的样本均值。在原始论文中,作者建议使用 RMSProp 作为标准优化器,以避免基于动量的算法可能产生的稳定性问题。然而,Adam(具有更低的遗忘因子μ[1] = 0.5和μ[2] = 0.9以及学习率η = 0.00005)比 RMSProp 更快,并且不会导致不稳定性。我建议测试这两种选项,尝试最大化训练速度同时防止模式坍塌。与上一个示例相反,在这种情况下,我们需要在每个训练步骤之后剪辑所有的 Critic 变量。为了避免这种情况,内部并发可能会改变某些操作的顺序;需要使用嵌套依赖控制上下文管理器。这样,实际的training_step_c
(负责剪辑并将值重新分配给每个变量)将仅在optimizer_c
步骤完成后执行。
现在,我们可以创建InteractiveSession
,初始化变量,并开始训练过程,这与上一个示例非常相似:
import numpy as np
import tensorflow as tf
nb_epochs = 200
nb_critic = 5
batch_size = 64
nb_iterations = int(nb_samples / batch_size)
session = tf.InteractiveSession(graph=graph)
tf.global_variables_initializer().run()
samples_range = np.arange(nb_samples)
for e in range(nb_epochs):
c_losses = []
g_losses = []
for i in range(nb_iterations):
for j in range(nb_critic):
Xi = np.random.choice(samples_range, size=batch_size)
X = np.expand_dims(X_train[Xi], axis=3)
Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)
_, c_loss = session.run([training_step_c, loss_c],
feed_dict={
input_x: X,
input_z: Z,
is_training: True
})
c_losses.append(c_loss)
Z = np.random.uniform(-1.0, 1.0, size=(batch_size, code_length)).astype(np.float32)
_, g_loss = session.run([training_step_g, loss_g],
feed_dict={
input_x: np.zeros(shape=(batch_size, width, height, 1)),
input_z: Z,
is_training: True
})
g_losses.append(g_loss)
print('Epoch {}) Avg. critic loss: {} - Avg. generator loss: {}'.format(e + 1, np.mean(c_losses), np.mean(g_losses)))
主要区别在于,在这种情况下,在每次生成器训练步骤之前,Critic 会被训练n_critic
次。以下截图显示了生成 50 个随机样本的结果:
使用 Fashion MNIST 数据集训练的 WGAN 生成的样本
如您所见,质量略有提高,样本也更加平滑。我邀请读者也用 RGB 数据集测试这个模型,因为最终质量通常非常出色。
当使用这些模型时,训练时间可能会非常长。为了避免等待看到初始结果(以及进行必要的调整),我建议使用 Jupyter。这样,就可以停止学习过程,检查生成器能力,并且可以无问题地重新启动。当然,图必须保持不变,并且变量初始化必须在开始时进行。
摘要
在本章中,我们讨论了对抗训练的主要原则,并解释了两个玩家的角色:生成器和判别器。我们描述了如何使用最小-最大方法来建模和训练它们,该方法的两个目标是迫使生成器学习真实数据分布 p[data],并使判别器能够完美地区分真实样本(属于 p[data]) 和不可接受的样本。在同一部分中,我们分析了生成对抗网络的内部动态和一些可能导致训练过程缓慢并导致最终配置次优的常见问题。
在标准 GAN 中遇到的最困难的问题之一出现在数据生成过程和生成器分布具有不连续支持时。在这种情况下,Jensen-Shannon 散度变为常数,不提供关于距离的精确信息。Wasserstein 测度提供了一个极好的替代方案,它在称为 WGAN 的更有效模型中被采用。这种方法可以有效地管理不连续分布,但需要在 Critic 上强制执行 L-Lipschitz 条件。标准方法基于在每次梯度上升更新后剪辑参数。这种简单技术保证了 L-Lipschitz 条件,但需要使用非常小的剪辑因子,这可能导致转换速度变慢。因此,通常在每次单独的生成器训练步骤之前,需要重复训练 Critic 一个固定的次数(例如五次)。
在下一章中,我们将介绍另一种基于特定类型神经网络的概率生成神经网络模型,这种神经网络被称为受限玻尔兹曼机。
第十三章:深度信念网络
在本章中,我们将介绍两种使用一组潜在变量来表示特定数据生成过程的概率生成模型。受限玻尔兹曼机(RBMs),于 1986 年提出,是更复杂模型(称为深度信念网络(DBN))的构建块,能够捕捉不同层次特征之间的复杂关系(在某种程度上与深度卷积网络相似)。这两种模型都可以在无监督和监督场景中使用作为预处理程序,或者,如 DBN 通常所做的那样,使用标准反向传播算法调整参数。
特别是,我们将讨论:
-
马尔可夫随机场(MRF)
-
RBM
-
对比发散(CD-k)算法
-
带有监督和无监督示例的 DBN
MRF
让我们考虑一组随机变量,x[i],它们在一个无向图G=(V, E)中组织,如下图所示:
概率无向图的示例
如果两个随机变量a和b在随机变量c的条件下是条件独立的,那么:
现在,再次考虑这个图;如果所有变量子集S[i]和S[j]的通用配对在分离子集S[k](这样属于S[i]的变量与属于S[j]的变量之间的所有连接都通过S[k])的条件下是条件独立的,那么这个图被称为马尔可夫随机场(MRF)。
给定G=(V, E),一个包含所有相邻顶点的子集称为团(所有团的集合通常表示为cl(G))。例如,考虑之前显示的图;(x[0],x[1])是一个团,如果 x[0]和 x[5]相连,那么(x[0],x[1],x[5])将是一个团。最大团是一个不能通过添加新顶点来扩展的团。一个特定的 MRF 家族由所有那些联合概率分布可以分解为以下形式的图组成:
在这种情况下,α是归一化常数,乘积扩展到所有最大团集合。根据汉密尔顿-克利福德定理(有关更多信息,请参阅Cheung S.,肯塔基大学,2008 年的汉密尔顿-克利福德定理证明),如果联合概率密度函数是严格正的,MRF 可以分解,所有ρ[i]函数也是严格正的。因此,基于对数性质的一些简单操作后,p(x)可以重写为吉布斯(或玻尔兹曼)分布:
术语 E(x) 被称为能量,因为它源自统计物理中此类分布的第一个应用。1/Z 现在是使用标准符号表示的正则化常数。在我们的场景中,我们始终考虑包含观测值 (x[i]) 和潜在变量 (h[j]) 的图。因此,将联合概率表示为是有用的:
每当需要边缘化以获得 p(x) 时,我们可以简单地对 h[j] 求和:
RBMs
RBM(最初称为Harmonium)是由 Smolensky 提出的神经网络模型(在《信息处理在动态系统中的基础:和谐理论的基础》**,Smolensky P.,并行分布式处理,第 1 卷,麻省理工学院出版社),由一层输入(可观测)神经元和一层隐藏(潜在)神经元组成。以下图表显示了其通用结构:
限制玻尔兹曼机的结构
由于无向图是二分图(同一层的神经元之间没有连接),其潜在的概率结构是 MRF。在原始模型中(即使这不是一个限制),所有神经元都被假定为伯努利分布 (x[i], h[i] = {0, 1}),带有偏置,b[i](对于观测单元)和c[j](对于潜在神经元)。得到的能量函数是:
RBM 是一种概率生成模型,可以学习数据生成过程,p[data],它由观测单元表示,但利用潜在变量的存在来模拟所有内部关系。如果我们把所有参数总结成一个向量,θ = {w[ij], b[i], c[j]},吉布斯分布变为:
RBM 的训练目标是最大化输入分布相对于对数似然。因此,第一步是在对前一个表达式进行边缘化后确定L(θ; x):
由于我们需要最大化对数似然,计算相对于θ的梯度是有用的:
应用导数的链式法则,我们得到:
使用条件概率和联合概率等式,前面的表达式变为:
考虑到完整的联合概率,经过一些繁琐的操作(此处省略),可以推导出以下表达式(σ(•) 是 Sigmoid 函数):
到目前为止,我们可以计算对数似然相对于每个单个参数的梯度,w[ij],b**[i],和c**[j]。从w[ij]开始,考虑到∇[wij] E(x, h; θ) = -x[i]h[j],我们得到:
该表达式可以重写为:
现在,考虑到所有单元都是伯努利分布的,并且仅隔离第 j 个隐藏单元,可以应用以下简化:
因此,梯度变为:
同样,我们可以推导出 L 对 b[i ]和 c[j] 的梯度:
因此,每个梯度的第一个项非常容易计算,而第二个项则需要对所有观察到的值进行求和。由于这个操作不切实际,唯一可行的替代方案是基于采样的近似,使用如吉布斯采样(更多信息,见第四章,贝叶斯网络和隐马尔可夫模型)。然而,因为这个算法从条件分布 p(x|h) 和 p(h|x) 中采样,而不是从完整的联合分布 p(x, h) 中采样,它需要相关的马尔可夫链达到其平稳分布 π,以便提供有效的样本。由于我们不知道需要多少采样步骤才能达到 π,吉布斯采样也可能因为其可能的高计算成本而成为不可行的解决方案。
为了解决这个问题,Hinton 在《*训练受限玻尔兹曼机的实用指南,Hinton G.,多伦多大学计算机科学系)中提出了一种名为CD-k的替代算法。这个想法非常简单但非常有效:我们不是等待马尔可夫链达到平稳分布,而是从训练样本 t=0 x^((0)) 开始固定次数采样,并通过从 p(h((1))|x((0))) 中采样来计算 h^((1))。然后,使用隐藏向量从 p(x((2))|h((1))) 中采样重建 x^((2))。这个程序可以重复任意次数,但在实践中,通常只需要一个采样步骤就能确保相当高的精度。在这个点上,对数似然梯度的近似可以表示为(考虑 t 步):
相对于 w[ij],b[i] 和 c[j] 的单梯度可以很容易地通过前面的程序获得。术语 对比度 来自于在 x^((0) )处计算的 L 的梯度近似,通过一个称为 正梯度 的项和一个称为 负梯度 的项之间的加权差。这种方法类似于用这个增量比来近似导数:
基于单步 CD-k 的完整 RBM 训练算法(假设有 M 个训练样本):
-
设置隐藏单元的数量,N[h]
-
设置一个 epoch 的数量,N[e]
-
设置学习率
learning_rate
* η(例如,η = 0.01*) -
对于 e=1 到 N[e]:
-
设置 Δw = 0,Δb = 0,和 Δc = 0
-
对于 i=1 到 M:
-
从 p(h|x^((i))) 中采样 h^((i))
-
从 p(x((i+1))|h((i))) 中采样重建 x^((i+1))
-
累加权重和偏置的更新:
-
Δw += p(h = 1|x((i)))x((i)) - p(h = 1|x((i+1)))x((i+1))(作为外积)
-
Δb += x^((i)) - x^((i+1))
-
Δc += p(h = 1|x^((i))) - p(h = 1|x^((i+1)))
-
-
-
更新权重和偏置:
-
w += ηΔw
-
b += ηΔb
-
c += ηΔc
-
-
两个向量之间的外积定义为:
如果向量 a 的形状为 (n, 1),而 b 的形状为 (m, 1),则结果是形状为 (n, m) 的矩阵。
DBNs
信念网络或贝叶斯网络是一个已在第四章“贝叶斯网络和隐马尔可夫模型”中探讨的概念。在这种情况下,我们将考虑具有可见和潜在变量,并组织成同质层的信念网络。第一层始终包含输入(可见)单元,而所有其余的都是潜在变量。因此,深度信念网络(DBN)可以结构化为堆叠的 RBM,其中每个隐藏层也是后续 RBM 的可见层,如下面的图所示(每层的单元数可能不同):
通用深度信念网络的架构
学习过程通常是贪婪的、逐步的(如 Hinton G. E.、Osindero S.、Teh Y. W.在《深度信念网的快速学习算法》中提出)。第一个 RBM 使用数据集进行训练,并使用 CD-k 算法优化以重建原始分布。在此阶段,内部(隐藏)表示被用作下一个 RBM 的输入,依此类推,直到所有块都完全训练。这样,DBN 被迫创建后续内部表示,这些表示可以用于不同的目的。当然,当模型训练时,可以从识别(逆)模型中采样隐藏层,并计算激活概率(x代表一个通用原因):
作为一种始终是生成过程的 DBN,在无监督场景中,它可以通过创建一系列子过程的方法来执行成分分析/降维,这些子过程能够重建内部表示。而单个 RBM 专注于单个隐藏层,因此无法学习子特征,DBN 则贪婪地学习如何使用精细化的隐藏分布来表示每个子特征向量。这一过程背后的概念与卷积层级联的概念并没有很大区别,主要区别在于在这种情况下,学习过程是贪婪的。与 PCA 等方法的另一个区别是,我们并不确切知道内部表示是如何构建的。由于潜在变量是通过最大化对数似然进行优化的,因此可能存在许多最优解,但我们无法轻易地对它们施加约束。然而,DBNs 在不同的场景中表现出非常强大的特性,尽管它们的计算成本通常比其他方法高得多。其中一个主要问题(与大多数深度学习方法都有关)是正确选择每一层的隐藏单元。因为它们代表潜在变量,所以它们的数量是训练过程成功的关键因素。正确的选择并不立即显现,因为需要了解数据生成过程的复杂性。然而,作为一个经验法则,我建议从包含 32/64 个单元的几层开始,然后逐步增加隐藏神经元的数量和层数,直到达到所需的准确度(同样,我建议从一个较小的学习率开始,例如 0.01,如果需要则增加)。
由于第一个 RBM 负责重建原始数据集,因此监控每个 epoch 后的对数似然(或错误)非常有用,以便了解过程是否正确学习(错误减少)或者是否已经饱和了容量。很明显,初始的重建不良会导致随后的表示更差。由于学习过程是贪婪的,在无监督任务中,一旦完成之前的训练步骤,就没有办法提高较低层的性能,因此,我总是建议调整参数,使得第一次重建非常准确。当然,关于过拟合的所有考虑仍然有效,因此,监控验证样本的泛化能力也很重要。然而,在成分分析中,我们假设我们正在处理一个能够代表潜在数据生成过程的分布,因此发现先前未见特征的风险应该是最小的。
在监督场景中,通常有两种选择,它们的第一个步骤总是 DBN 的贪婪训练。然而,第一种方法使用标准算法(如反向传播,将整个架构视为单个深度网络)进行后续的细化,而第二种方法则使用最后一个内部表示作为单独分类器的输入。不言而喻,第一种方法具有更多的自由度,因为它与一个预训练的网络一起工作,其权重可以调整,直到验证准确率达到最大值。在这种情况下,第一个贪婪步骤与通过观察深度模型的内部行为(类似于卷积网络)经验证的同一种假设一起工作。第一层学习如何检测低级特征,而所有随后的层都增加了细节。因此,反向传播步骤可能从已经非常接近最优点的位置开始,可以更快地收敛。相反,第二种方法类似于将核技巧应用于标准的支持向量机(SVM)。实际上,外部分类器通常非常简单(如逻辑回归或 SVM),而提高的准确度通常是由于通过将原始样本投影到子空间(通常是高维空间)而获得的更好的线性可分性,在那里它们可以很容易地被分类。一般来说,这种方法比第一种方法性能更差,因为没有方法可以调整 DBN 训练后的参数。因此,当最终投影不适合线性分类时,有必要采用更复杂的模型,而计算成本可能会非常高,而性能增益却不成比例。由于深度学习通常基于端到端学习的概念,因此训练整个网络可以隐式地将预处理步骤包含在完整结构中,这成为一个将输入样本与特定结果关联的黑盒。另一方面,每当需要显式管道时,贪婪训练 DBN 并使用单独的分类器可能是一个更合适的解决方案。
Python 中无监督 DBN 的示例
在这个例子中,我们将使用 GitHub 上免费提供的 Python 库(github.com/albertbup/deep-belief-network
),该库允许使用 NumPy(仅 CPU)或 Tensorflow(CPU 或 GPU 支持)以及标准的 Scikit-Learn 接口来处理监督和无监督的 DBN。我们的目标是创建mnist
数据集子集的较低维度的表示(由于训练过程可能相当慢,我们将限制为400
个样本)。第一步是加载(使用 Keras 辅助函数)、打乱和归一化数据集:
import numpy as np
from keras.datasets import mnist
from sklearn.utils import shuffle
(X_train, Y_train), (_, _) = mnist.load_data()
X_train, Y_train = shuffle(X_train, Y_train, random_state=1000)
nb_samples = 400
width = X_train.shape[1]
height = X_train.shape[2]
X = X_train[0:nb_samples].reshape((nb_samples, width * height)).astype(np.float32) / 255.0
Y = Y_train[0:nb_samples]
在这一点上,我们可以创建一个 UnsupervisedDBN
类的实例,设置三个层,分别有 512
、256
和 64
个 sigmoid 单元(因为我们想将值绑定在 0
和 1
之间)。学习率 η (learning_rate_rbm
) 设置为 0.05
,批量大小 (batch_size
) 为 64
,每个 RBM 的训练轮数 (n_epochs_rbm
) 为 100
。CD-k 步骤的默认值是 1
,但可以通过 contrastive_divergence_iter
参数进行更改:
from dbn.tensorflow import UnsupervisedDBN
unsupervised_dbn = UnsupervisedDBN(hidden_layers_structure=[512, 256, 64],
learning_rate_rbm=0.05,
n_epochs_rbm=100,
batch_size=64,
activation_function='sigmoid')
X_dbn = unsupervised_dbn.fit_transform(X)
[START] Pre-training step:
>> Epoch 1 finished RBM Reconstruction error 55.562027
>> Epoch 2 finished RBM Reconstruction error 53.663380
...
>> Epoch 99 finished RBM Reconstruction error 5.169244
>> Epoch 100 finished RBM Reconstruction error 5.130809
[END] Pre-training step
一旦训练过程完成,X_dbn
数组将包含从最后一个隐藏层采样的值。不幸的是,这个库没有实现逆变换方法,但我们可以使用 t-SNE 算法将分布投影到二维空间:
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, perplexity=20, random_state=1000)
X_tsne = tsne.fit_transform(X_dbn)
对应的图表如下所示:
t-SNE 图展示了最后一个 DBN 隐藏层分布(64 维)
如您所见,尽管仍然存在一些异常,但隐藏的低维表示与原始数据集全局上一致,因为包含相同数字的组被组织成紧凑的簇,并保留了一些几何属性。例如,包含表示数字 1 的组的簇与包含 7 的图像的组非常接近,以及 3 和 8 的组。这一结果证实,DBN 可以成功用作分类目的的预处理层,但在此情况下,与其减少维度,不如通常更倾向于增加维度,以便利用冗余来使用更简单的线性分类器(为了更好地理解这个概念,想想通过添加多项式特征来增强数据集)。我邀请您通过预处理整个 MNIST 数据集,然后使用逻辑回归对其进行分类,并将结果与直接方法进行比较来测试这一能力。
可以使用以下命令安装库:pip install git+git://github.com/albertbup/deep-belief-network.git
(NumPy 或 Tensorflow CPU)或 pip install git+git://github.com/albertbup/deep-belief-network.git@master_gpu
(Tensorflow GPU)。在两种情况下,命令还会安装 Tensorflow 和其他常见 Python 发行版中通常存在的依赖项(如 Anaconda);因此,为了仅安装 DBN 组件,需要在 pip
命令中添加 --no-deps
属性。有关更多信息,请参阅 GitHub 页面。
使用 Python 的监督 DBN 示例
在这个例子中,我们将使用 KDD Cup '99 数据集(由 Scikit-Learn 提供),该数据集包含一个入侵检测系统在正常和危险网络活动下生成的日志。我们只关注smtp
子数据集,这是最小的子集,因为,如前所述,训练过程可能非常耗时。这个数据集并不特别复杂,可以用更简单的方法成功分类;然而,这个例子只有一个教学目的,并且对于理解如何处理这类数据可能是有用的。
第一步是加载数据集,对标签(字符串类型)进行编码,并对值进行标准化:
from sklearn.datasets import fetch_kddcup99
from sklearn.preprocessing import LabelEncoder, StandardScaler
kddcup = fetch_kddcup99(subset='smtp', shuffle=True, random_state=1000)
ss = StandardScaler()
X = ss.fit_transform(kddcup['data']).astype(np.float32)
le = LabelEncoder()
Y = le.fit_transform(kddcup['target']).astype(np.float32)
在这个阶段,我们可以创建训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.25, random_state=1000)
模型基于SupervisedDBNClassification
类的实例,该类实现了反向传播方法。参数与无监督情况非常相似,但现在我们还可以指定随机梯度下降(SGD)学习率(learning_rate
)、反向传播的迭代次数(n_iter_backprop
)以及可选的 dropout(dropout_p
)。算法执行一个初始的贪婪训练(其计算成本通常高于 SGD 阶段),然后进行微调:
from dbn.tensorflow import SupervisedDBNClassification
classifier = SupervisedDBNClassification(hidden_layers_structure=[64, 64],
learning_rate_rbm=0.001,
learning_rate=0.01,
n_epochs_rbm=20,
n_iter_backprop=150,
batch_size=256,
activation_function='relu',
dropout_p=0.25)
classifier.fit(X_train, Y_train)
[START] Pre-training step:
>> Epoch 1 finished RBM Reconstruction error 2.478997
>> Epoch 2 finished RBM Reconstruction error 2.459004
...
>> Epoch 147 finished ANN training loss 0.006651
>> Epoch 148 finished ANN training loss 0.006631
>> Epoch 149 finished ANN training loss 0.006612
[END] Fine tuning step
SupervisedDBNClassification(batch_size=256, dropout_p=0.25,
idx_to_label_map={0: 1.0, 1: 0.0, 2: 2.0},
l2_regularization=1.0,
label_to_idx_map={0.0: 1, 1.0: 0, 2.0: 2},
learning_rate=0.01, n_iter_backprop=150, verbose=True)
一旦训练过程完成,我们就可以在测试集上评估性能:
from sklearn.metrics.classification import accuracy_score
Y_pred = classifier.predict(X_test)
print(accuracy_score(Y_test, Y_pred))
1.0
验证准确率为1.0
(没有误分类),但这实际上是一个简单的数据集,只需要几分钟的训练。我邀请您测试 DBN 在 MNIST/Fashion MNIST 数据集分类中的性能,并将结果与使用深度卷积网络获得的结果进行比较。在这种情况下,重要的是要监控每个 RBM 的重建误差,尝试在运行反向传播阶段之前将其最小化。完成这个练习后,你应该能够回答这个问题:端到端方法与基于预处理的方法哪个更可取?
在运行这些实验时,由于采样使用频繁,我总是建议在 NumPy 中设置随机种子(并保持其恒定),以确保可重复性(使用np.random.seed(...)
命令)。由于这个库也与 Tensorflow 一起工作,因此需要使用tf.set_random_seed(...)
命令重复此操作。
摘要
在这一章中,我们介绍了 MRF 作为 RBM 的底层结构。MRF 表示为一个无向图,其顶点是随机变量。特别是,为了我们的目的,我们考虑了联合概率可以表示为每个随机变量正函数乘积的 MRF。基于指数的最常见分布称为 Gibbs(或 Boltzmann)分布,它特别适合我们的问题,因为对数消除了指数,从而得到更简单的表达式。
RBM 是一个简单的二分无向图,由可见变量和潜在变量组成,只在不同组之间有连接。这个模型的目标是通过隐藏单元的存在来学习一个概率分布,这些隐藏单元可以模拟未知关系。不幸的是,尽管对数似然非常简单,但无法轻易优化,因为归一化项需要对所有输入值求和。因此,Hinton 提出了一个替代算法,称为 CD-k,该算法基于固定数量的 Gibbs 采样步骤(通常是 1)输出对数似然梯度的近似值。
堆叠多个 RBMs 可以建模 DBNs,其中每个块的隐藏层也是下一个块的可见层。DBN 可以通过贪婪方法进行训练,依次最大化每个 RBM 的对数似然。在无监督场景中,DBN 能够以分层的方式提取数据生成过程的特点,因此应用包括成分分析和降维。在监督场景中,DBN 可以使用反向传播算法(考虑整个网络)或有时使用管道中的预处理步骤进行贪婪预训练和微调,其中分类器通常是一个非常简单的模型(例如逻辑回归)。
在下一章,第十四章,强化学习导论中,我们将介绍强化学习的概念,讨论能够自主学习玩游戏或使机器人行走、跳跃以及执行使用经典方法难以建模和控制的任务的系统的最重要的元素。
第十四章:强化学习简介
在本章中,我们将介绍强化学习(RL)的基本概念,它是一组允许智能体学习如何在未知环境中行为的途径,这得益于在每个可能行动之后提供的奖励。强化学习已经研究了数十年,但在最近几年,它达到了一个非常成熟的水平,因为现在可以结合深度学习模型和标准(通常是简单)算法来解决极其复杂的问题(例如完美学习如何玩 Atari 游戏)。
尤其是以下内容:
-
环境、智能体、策略和奖励的概念
-
马尔可夫决策过程(MDP)的概念
-
策略迭代算法
-
价值迭代算法
-
TD(0)算法
强化学习基础
想象一下,你想要学习骑自行车,并向朋友寻求建议。他们解释了齿轮的工作原理,如何释放刹车以及一些其他技术细节。最后,你询问保持平衡的秘诀。你期待什么样的回答?在一个假想的监督世界中,你应该能够完美量化你的行动,并通过将结果与精确的参考值进行比较来纠正错误。在现实世界中,你对行动背后的数量一无所知,更重要的是,你永远不会知道正确的值是什么。提高抽象级别,我们考虑的场景可以描述为:一个通用的智能体在环境中执行行动,并接收某种程度与行动能力成比例的反馈。
根据这个反馈,智能体可以纠正其行动以达到特定的目标。这个基本架构在以下图中表示:
基本强化学习架构
回到我们最初的例子,当你第一次骑自行车并试图保持平衡时,你会注意到错误动作会导致斜率的增加,这反过来又增加了重力力的水平分量,推动自行车向侧面移动。当垂直分量得到补偿时,结果是旋转,直到自行车完全倒下才会停止。然而,由于你可以用腿来控制平衡,当自行车开始倒下时,根据牛顿第三定律,腿上的力会增加,你的大脑会理解有必要做出相反方向的移动。即使这个问题可以用物理定律轻松表达,但没有人通过计算力和动量来学习骑自行车。这是强化学习的主要概念之一:智能体必须始终根据一部分信息做出选择,这通常被定义为奖励,它代表了环境提供的响应。如果动作是正确的,奖励将是正的,否则,将是负的。在接收到奖励后,智能体可以微调称为策略的策略,以最大化预期的未来奖励。例如,骑了几次车后,你将能够稍微移动身体以在转弯时保持平衡,但可能,在开始时,你需要伸出腿来避免摔倒。因此,你最初的政策建议了一个错误动作,它收到了重复的负面奖励,所以你的大脑通过增加选择另一个动作的概率来纠正它。这个方法背后的隐含假设是,智能体总是理性的,这意味着它的目标是最大化其动作的预期回报(没有人愿意为了感受不同的情绪而摔倒)。
在讨论强化学习系统的单个组件之前,有必要添加几个基本假设。第一个假设是,一个智能体可以无限次地重复其经验。换句话说,我们假设只有当我们有足够的时间时,才可能学习到一个有效的策略(可能是最优策略)。显然,这在动物世界中是不可接受的,我们都知道许多经验是非常危险的;然而,这个假设对于证明某些算法的收敛性是必要的。确实,次优策略有时可以非常快速地学习到,但要达到最优策略,通常需要多次迭代。在现实的人工系统中,我们总是在有限次迭代后停止学习过程,但如果某些经验阻止智能体继续与环境交互,那么几乎不可能找到有效的解决方案。由于许多任务都有最终状态(无论是积极的还是消极的),我们假设智能体可以玩任意数量的回合(某种程度上类似于监督学习的时代),利用之前学习到的经验。
第二个假设稍微有点技术性,通常被称为马尔可夫性质。当智能体与环境交互时,它会观察到一系列状态。即使这可能看起来有些矛盾,我们假设每个状态都是状态化的。我们可以用一个简单的例子来解释这个概念;假设你在填充一个水箱,每五秒钟你测量一次水位。想象一下,在 t = 0 时,水位 L = 10,水正在流入。你在 t = 1 时期待什么?显然,L > 10。换句话说,在没有外部未知原因的情况下,我们假设一个状态包含其先前历史,因此,即使序列是离散化的,它也代表了一个不允许跳跃的连续演变。当一个 RL 任务满足这个属性时,它被称为马尔可夫决策过程,并且很容易使用简单的算法来评估动作。幸运的是,大多数自然事件都可以建模为 MDP(当你朝着门走时,每一步向右的方向都必须减少距离),但有些游戏是隐式无状态的。例如,如果你想使用 RL 算法来学习如何猜测一系列独立事件的概率结果(例如抛硬币),结果可能会非常错误。原因很清楚:任何状态都与先前状态无关,并且任何试图建立历史的尝试都是失败的。因此,如果你观察到一系列 0, 0, 0, 0, ...,你无权增加对 0 下注的价值,除非在考虑事件的可能性后,你假设硬币是作弊的。然而,如果没有理由这样做,这个过程就不是 MDP,每个场景(事件)都是完全独立的。我们所做的所有假设,无论是隐式还是显式,都是基于这个基本概念,所以在评估新的、不寻常的场景时要注意,因为你可能会发现使用特定算法的理论依据是不成立的。
环境
环境是智能体必须达到其目标的空间实体。就我们的目的而言,一个通用环境是一个接收输入动作,a[t](我们使用索引 t 因为这是一个自然的时间过程),并输出一个由状态,s[t+1],和奖励,r[t+1]组成的元组的系统。这两个元素是提供给智能体以做出其下一步决策的唯一信息。如果我们正在处理一个 MDP,并且可能动作的集合,A,和状态的集合,S,是离散且有限的,那么问题是一个定义明确的有限 MDP(在许多连续情况下,可以通过离散化空间将问题视为有限 MDP)。如果有最终状态,该任务被称为episodic,通常,目标是在最短的时间内达到一个正的最终状态或最大化分数。智能体和环境之间循环交互的方案如下所示:
代理-环境交互模式
环境的一个非常重要的特征是其内部性质。它可以是有序的或随机的。一个有序的环境由一个函数定义,该函数将每个可能动作,a[t],在特定状态,s[t],关联到一个明确的后继状态,s[t+1],以及一个精确的奖励,r[t+1]:
相反,一个随机环境由给定动作,a[t],当前状态,s[t],和一组可能的后继状态,s^i[t+1],之间的转移概率来定义:
如果一个状态,s[i],有一个转移概率,T(s[i], s[i], a[t]) = 1 ∀ a[t ]∈ A,则该状态被定义为吸收状态。通常,所有在序列任务中的结束状态都被建模为吸收状态,以避免任何进一步的转换。当一个序列不被限制在固定数量的步骤时,确定其结束的唯一标准是检查代理是否达到了吸收状态。
由于我们不知道哪个状态将是后续状态,因此有必要考虑在初始状态,s[t],和动作,a[t],下所有可能奖励的期望值:
通常,管理随机环境更容易,因为可以通过将所有概率设置为 0(除了对应实际后继状态的概率)立即将其转换为有序环境(例如,T(•) = (0, 0, ..., 1, ..., 0))。同样,期望回报可以设置为r[t+1]。了解 T(•)以及E[r^i[t+1]]对于应用某些特定算法是必要的,但当需要极其复杂的分析来为环境找到一个合适的模型时,可能会出现问题。在这些所有情况下,可以使用无模型方法,因此环境被视为一个黑盒,其输出在时间,t(在代理执行动作,a[t-1]之后),是评估策略的唯一可用信息。
奖励
我们已经看到,奖励(有时负奖励被称为惩罚,但最好使用标准化的符号)是环境在每次行动后提供的唯一反馈。然而,使用奖励的方法有两种不同的途径。第一种是一个非常短视的代理的策略,只考虑刚刚收到的奖励。这种方法的明显问题是无法考虑可能导致非常高的奖励的更长序列。例如,一个代理必须穿越几个具有负奖励的状态(例如,-0.1),但之后,他们到达一个具有非常积极奖励的状态(例如,+5.0)。短视的代理无法找到最佳策略,因为它只会试图避免立即的负奖励。另一方面,最好假设单个奖励包含未来将获得的奖励的一部分,这些奖励将遵循相同的策略。这个概念可以通过引入折现奖励来表示,它被定义为:
在前面的表达式中,我们假设了一个具有折现因子γ的无穷远视角,γ是一个介于 0 和 1 之间的实数(不包括)。当γ = 0 时,代理非常短视,因为R[t] = r[t+1],但当γ趋近于 1 时,当前奖励考虑了以时间步长成反比的方式折现的未来贡献。这样,非常接近的奖励将比非常遥远的奖励有更高的权重。如果所有奖励的绝对值都受限于最大即时绝对奖励|r[i]| ≤ |r[max]|,则前面的表达式将始终有界。事实上,考虑到几何级数的性质,我们得到:
显然,γ的正确选择是许多问题的关键因素,并且不能轻易推广。正如在许多其他类似情况下一样,我建议测试不同的值,选择那个最小化收敛速度同时产生近似最优策略的值。当然,如果任务是具有长度 T(e[i])的周期性任务,折现奖励变为:
Python 中的棋盘环境
我们将考虑一个基于代表隧道的棋盘环境的例子。代理的目标是到达结束状态(右下角),避免 10 个负吸收状态的水井。奖励如下:
-
结束状态:+5.0
-
韦尔斯:-5.0
-
所有其他状态:-0.1
为所有非终止状态选择一个小负奖励有助于迫使代理前进,直到达到最大(最终)奖励。让我们开始建模一个具有 5×15 矩阵的环境:
import numpy as np
width = 15
height = 5
y_final = width - 1
x_final = height - 1
y_wells = [0, 1, 3, 5, 5, 7, 9, 11, 12, 14]
x_wells = [3, 1, 2, 0, 4, 1, 3, 2, 4, 1]
standard_reward = -0.1
tunnel_rewards = np.ones(shape=(height, width)) * standard_reward
for x_well, y_well in zip(x_wells, y_wells):
tunnel_rewards[x_well, y_well] = -5.0
tunnel_rewards[x_final, y_final] = 5.0
环境的图形表示(就奖励而言)如下表所示:
洞穴环境中的奖励
代理允许在四个方向上移动:上、下、左和右。显然,在这种情况下,环境是确定的,因为每个动作都会将代理移动到预定义的单元格。我们假设,每当一个动作被禁止(例如,当代理在第一列时尝试向左移动),后续状态是相同的(带有相应的奖励)。
策略
一个 策略 正式上是代理遵循的确定或随机法则,以最大化其回报。传统上,所有策略都用字母 π 表示。一个 确定策略 通常是一个当前状态的函数,输出一个精确的动作:
一个 随机策略,类似于环境,输出每个动作的概率(在这种情况下,我们假设我们与一个有限的 MPD 工作):
然而,与环境相反,代理必须始终选择一个特定的动作,将任何随机策略转化为一系列确定的选择。一般来说,当 π(s, a) > 0 ∀ a ∈ A 的策略被称为 软,它在训练过程中非常有用,因为它允许更灵活的建模,而不会提前选择次优动作。相反,当 π(s, a[i]) = 0 ∀ i ≠ j 和 π(s, a[j]) = 1 时,该策略也被定义为 硬。这种转换可以以多种方式执行,但最常见的一种是定义一个相对于价值的贪婪策略(我们将在下一节讨论这个概念)。这意味着在每一步,策略将选择最大化后续状态价值的动作。显然,这是一个非常理性的方法,可能会过于实用。事实上,当某些状态的价值没有变化时,贪婪策略将始终迫使代理执行相同的动作。
这种问题被称为探索-利用困境,当允许智能体评估最初可能看起来次优的替代策略时会出现。换句话说,我们希望智能体在开始利用策略之前探索环境,以确定策略是否真的是最好的,或者是否存在隐藏的替代方案。为了解决这个问题,可以采用ε-贪婪策略,其中值ε被称为探索因子,代表一个概率。在这种情况下,策略将以概率ε随机选择一个动作,以概率1 - ε选择贪婪的动作。通常,在训练过程的开始阶段,ε保持非常接近 1.0,以鼓励探索,并且随着策略变得更加稳定,它逐渐减少。在许多深度强化学习应用中,这种方法是基本的,特别是在没有环境模型的情况下。原因是贪婪策略最初可能是错误的,并且有必要允许智能体在强制做出确定性决策之前探索许多可能的状态和动作序列。
政策迭代
在本节中,我们将分析一种基于对环境(就转移概率和期望回报而言)的完整知识来寻找最优策略的策略。第一步是定义一种可以用来构建贪婪策略的方法。假设我们正在处理一个有限的 MDP 和一个通用策略π;我们可以定义状态s[t]的内在价值为智能体从状态s[t]开始并遵循随机策略π所获得的期望折现回报:
在这种情况下,我们假设,由于智能体将遵循π,如果从状态s[a]开始获得的期望回报大于从状态 s[b]开始获得的回报,那么状态s[a]比状态s[b]更有用。不幸的是,当γ > 0时,试图直接使用之前的定义找到每个状态的价值几乎是不可能的。然而,这是一个可以用动态规划(有关更多信息,请参阅动态规划与马尔可夫过程,罗纳德·A·霍华德,麻省理工学院出版社)解决的问题,它允许我们迭代地解决这个问题。
尤其是我们需要将之前的公式转换成贝尔曼方程:
右侧的第一个项可以表示为:
换句话说,它是考虑代理处于状态,s[t],并评估所有可能动作及其后续状态转移的所有预期回报的加权平均值。对于第二项,我们需要一个小技巧。假设我们从s[t+1]开始,因此预期值对应于V(s[t+1];π);然而,由于和从s[t]开始,我们需要考虑从s[t]开始的所有可能的转移。在这种情况下,我们可以将这个项重写为:
再次,第一项考虑了从s[t](并最终到达s[t+1])开始的所有可能的转移,而第二项是每个结束状态的价值。因此,完整的表达式变为:
对于确定性策略,公式是:
之前的方程是针对有限马尔可夫决策过程(MDP)的通用离散贝尔曼方程的特殊情况,该方程可以表示为一个向量算子,即L[π],作用于价值向量:
证明存在一个唯一的固定点对应于V(s; π)是容易的,因此Lπ V(s; π) = V(s; π)。然而,为了解这个系统,我们需要同时考虑所有方程,因为在贝尔曼方程的左右两边都有V(•; π)项。能否将问题转化为一个迭代过程,以便利用前一次的计算结果进行下一次计算?答案是肯定的,这是L[π]的一个重要属性的后果。让我们考虑在时间t和t+1计算的两个价值向量之间的无穷范数:
当折现因子γ ∈ [0, 1]时,贝尔曼算子L[π]是一个γ-收缩,通过γ因子减少参数之间的距离(它们变得越来越相似)。Banach 不动点定理指出,在度量空间D上的收缩L: D → D,在D中有一个唯一的固定点d^ ∈ D,可以通过反复应用收缩到任何d^((0)) ∈ D*来找到。
因此,我们知道存在一个唯一的固定点V(s; π),这是我们研究的目标。如果我们现在考虑一个通用的起始点V^((t)),并计算与V(s; π)之间的范数,我们得到:
重复此过程,直到t = 0,我们得到:
项γ^(t+1) → 0,同时继续迭代V^((t))和V(s; π)之间的距离,这个距离越来越小,允许我们使用迭代方法而不是一次性闭合方法。因此,贝尔曼方程变为:
这个公式使我们能够找到每个状态(这个步骤正式称为策略评估)的值,但当然,它需要一个策略。在第一步,我们可以随机选择动作,因为我们没有其他任何信息,但在完成评估周期后,我们可以开始根据值定义一个贪婪策略。为了实现这个目标,我们需要引入强化学习中的一个非常重要的概念,即Q 函数(必须与 EM 算法中定义的Q 函数区分开来),它被定义为从状态,s[t],开始并选择一个特定动作,a[t],的智能体获得的期望折现回报:
定义与V(s; π)非常相似,但在这个情况下,我们将动作,a[t],作为一个变量。显然,我们可以通过简单地移除策略/动作求和来定义Q(s, a; π)的贝尔曼方程:
Sutton 和 Barto(在强化学习,Sutton R. S.,Barto A. G.,麻省理工学院出版社)证明了一个简单但非常重要的定理(称为策略改进定理),该定理表明,给定确定性策略,π[1]和π[2],如果Q(s, π2; π[2]) ≥ V(s; π[1])∀ s ∈ S,则π[2]优于或等于π[1]。证明非常紧凑,可以在他们的书中找到,然而,结果可以直观地理解。如果我们考虑一个状态序列,s[1] → s[2] → ... → s[n]和π2 = π1∀ i < m < n,而π2 ≥ π1∀ i ≥ m,策略,π[2],至少等于π[1],并且如果至少有一个不等式是严格的,它就变得更好。相反,如果Q(s, π2; π[2]) ≥ V(s; π[1]),这意味着π2 ≥ π1,并且如果至少有一个状态,s[i],其中π2 > π1,那么Q(s, π2; π[2]) > V(s; π[1])。因此,在完成策略评估周期后,我们有权利定义一个新的贪婪策略:
这一步被称为 策略改进,其目标是将每个状态关联的动作设置为导致后继状态具有最大值的动作。不难理解,当 *V^((t)) → *V(s; π) 时,最佳策略将保持稳定。事实上,当 t → ∞ 时,Q 函数将收敛到由 V(s; π) 和 argmax(•) 确定的稳定固定点,并且总是选择相同的动作。然而,如果我们从一个随机策略开始,通常一个策略评估周期不足以保证收敛。因此,在策略改进步骤之后,通常需要重复评估并交替两个阶段,直到策略变得稳定(这就是为什么算法被称为策略迭代)。一般来说,收敛速度相当快,但实际速度取决于问题的性质、状态和动作的数量以及奖励的一致性。
完整的政策迭代算法(如 Sutton 和 Barto 所提出)如下:
-
设置一个初始确定性随机策略 π(s)
-
将初始值数组设置为 V(s) = 0 ∀ s ∈ S
-
设置一个容差阈值 Thr(例如,Thr = 0.0001)
-
设置最大迭代次数 N[iter]
-
设置计数器 e = 0
-
当 e < N[iter] 时:
-
e += 1
-
执行:
-
将 Vold = V(s) ∀ s ∈ S
-
执行策略评估步骤,从 Vold 读取当前值并更新 V(s)
-
-
当 Avg(|V(s) - Vold|) > Thr 时:
-
将 πold = π(s) ∀ s ∈ S
-
执行策略改进步骤
-
如果 π[old]**(s) == π(s):
- Break
-
-
输出最终的确定性策略 π(s)
在这种情况下,因为我们完全了解环境,所以不需要探索阶段。策略总是被利用,因为它被设计成对真实值(当 t → ∞时获得)贪婪。
检查棋盘环境中的策略迭代
我们希望应用策略迭代算法以找到隧道环境的最佳策略。让我们首先定义一个随机的初始策略和一个所有值(除了终端状态)都等于 0 的值矩阵:
import numpy as np
nb_actions = 4
policy = np.random.randint(0, nb_actions, size=(height, width)).astype(np.uint8)
tunnel_values = np.zeros(shape=(height, width))
初始随机策略(t=0)如下图表所示:
初始(t=0)随机策略
用 ⊗ 表示的状态表示井,而最终的积极状态由大写字母 E 表示。因此,初始值矩阵(t=0)如下:
初始(t=0)值矩阵
在这一点上,我们需要定义执行政策评估和改进步骤的函数。由于环境是确定性的,因此过程稍微简单一些,因为通用转移概率,T(s[i],s[j]; a[k]),对于唯一可能的后续状态等于 1,否则为 0。同样,政策也是确定性的,只考虑一个动作。执行政策评估步骤,冻结当前值,并使用V((t))*更新整个矩阵,*V((t+1));然而,也可以立即使用新值。我邀请读者测试这两种策略,以找到最快的方法。在这个例子中,我们使用折现因子,γ = 0.9(不言而喻,一个有趣的练习是测试不同的值并比较评估过程的结果和最终行为):
import numpy as np
gamma = 0.9
def policy_evaluation():
old_tunnel_values = tunnel_values.copy()
for i in range(height):
for j in range(width):
action = policy[i, j]
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
tunnel_values[i, j] = reward + (gamma * old_tunnel_values[x, y])
def is_final(x, y):
if (x, y) in zip(x_wells, y_wells) or (x, y) == (x_final, y_final):
return True
return False
def policy_improvement():
for i in range(height):
for j in range(width):
if is_final(i, j):
continue
values = np.zeros(shape=(nb_actions, ))
values[0] = (tunnel_rewards[i - 1, j] + (gamma * tunnel_values[i - 1, j])) if i > 0 else -np.inf
values[1] = (tunnel_rewards[i, j + 1] + (gamma * tunnel_values[i, j + 1])) if j < width - 1 else -np.inf
values[2] = (tunnel_rewards[i + 1, j] + (gamma * tunnel_values[i + 1, j])) if i < height - 1 else -np.inf
values[3] = (tunnel_rewards[i, j - 1] + (gamma * tunnel_values[i, j - 1])) if j > 0 else -np.inf
policy[i, j] = np.argmax(values).astype(np.uint8)
定义好函数后,我们开始政策迭代周期(最大迭代次数,N[iter] = 100,000,容差阈值等于 10^(-5)):
import numpy as np
nb_max_epochs = 100000
tolerance = 1e-5
e = 0
while e < nb_max_epochs:
e += 1
old_tunnel_values = tunnel_values.copy()
policy_evaluation()
if np.mean(np.abs(tunnel_values - old_tunnel_values)) < tolerance:
old_policy = policy.copy()
policy_improvement()
if np.sum(policy - old_policy) == 0:
break
在过程结束时(在这种情况下,算法在 182 次迭代后收敛,但这个值可能会因不同的初始政策而变化),值矩阵是:
最终值矩阵
分析值,可以看到算法是如何发现它们是单元格与结束状态之间距离的隐函数。此外,政策总是避免陷阱,因为最大值总是在相邻状态中找到。通过绘制最终政策可以轻松验证这种行为:
最终政策
选择一个随机的初始状态,智能体将始终达到结束状态,避免陷阱并确认政策迭代算法的优化性。
值迭代
政策迭代的一个替代方法是值迭代算法。主要假设基于经验观察,政策评估步骤收敛得相当快,因此在固定步骤数(通常是 1 步)后停止过程是合理的。实际上,政策迭代可以想象成一场游戏,其中第一个玩家试图在考虑稳定政策的情况下找到正确的值,而另一个玩家则创建一个对新值具有贪婪性的新政策。显然,第二步会损害先前评估的有效性,迫使第一个玩家重复这个过程。然而,由于贝尔曼方程使用一个固定的单点,算法收敛到一个解决方案,其特征是政策不再改变,因此评估变得稳定。这个过程可以通过移除政策改进步骤并以贪婪方式继续评估来简化。形式上,每一步都是基于以下更新规则:
现在,迭代不再考虑策略(隐式地假设它将根据值进行贪婪选择),并选择所有 V^((t))(a[t]) 中最大的可能值 V^((t+1))。换句话说,值迭代通过选择对应于最可能(p → 1)被选中的动作的值,来预测策略改进步骤所做的选择。将上一节中提出的收敛证明扩展到这个情况并不困难,因此,V^((∞)) → V^((opt)),以及策略迭代也是如此。然而,平均迭代次数通常较小,因为我们从一个可以对比值迭代过程的随机策略开始。
当值变得稳定时,最优贪婪策略简单地获得如下:
这个步骤在形式上等同于策略改进迭代,然而,它只在过程结束时执行一次。
Sutton 和 Barto 提出的完整值迭代算法如下:
-
设置初始值数组,V(s) = 0 ∀ s ∈ S
-
设置一个容差阈值,Thr(例如,Thr = 0.0001)
-
设置最大迭代次数,N[iter]
-
设置计数器,e = 0
-
While e < N[iter]:
-
e += 1
-
Do:
-
Set Vold = V(s) ∀ s ∈ S
-
执行值评估步骤,从 Vold 读取当前值并更新 V(s)
-
-
While Avg(|V(s) - Vold|) > Thr
-
-
输出最终的确定性策略 π(s) = argmax[a] Q(s, a)
棋盘环境中的值迭代
为了测试这个算法,我们需要设置一个所有值都等于 0 的初始值矩阵(它们也可以随机选择,但由于我们没有关于最终配置的先验信息,每个初始选择在概率上是等效的):
import numpy as np
tunnel_values = np.zeros(shape=(height, width))
在这一点上,我们可以定义两个函数来执行值评估和最终策略选择(函数 is_final()
是在先前的例子中定义的):
import numpy as np
def value_evaluation():
old_tunnel_values = tunnel_values.copy()
for i in range(height):
for j in range(width):
rewards = np.zeros(shape=(nb_actions, ))
old_values = np.zeros(shape=(nb_actions, ))
for k in range(nb_actions):
if k == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif k == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif k == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
rewards[k] = tunnel_rewards[x, y]
old_values[k] = old_tunnel_values[x, y]
new_values = np.zeros(shape=(nb_actions, ))
for k in range(nb_actions):
new_values[k] = rewards[k] + (gamma * old_values[k])
tunnel_values[i, j] = np.max(new_values)
def policy_selection():
policy = np.zeros(shape=(height, width)).astype(np.uint8)
for i in range(height):
for j in range(width):
if is_final(i, j):
continue
values = np.zeros(shape=(nb_actions, ))
values[0] = (tunnel_rewards[i - 1, j] + (gamma * tunnel_values[i - 1, j])) if i > 0 else -np.inf
values[1] = (tunnel_rewards[i, j + 1] + (gamma * tunnel_values[i, j + 1])) if j < width - 1 else -np.inf
values[2] = (tunnel_rewards[i + 1, j] + (gamma * tunnel_values[i + 1, j])) if i < height - 1 else -np.inf
values[3] = (tunnel_rewards[i, j - 1] + (gamma * tunnel_values[i, j - 1])) if j > 0 else -np.inf
policy[i, j] = np.argmax(values).astype(np.uint8)
return policy
主要区别在于 value_evaluation()
函数,它现在必须考虑所有可能的后继状态,并选择导致具有最高值的状态的值。而 policy_selection()
函数与 policy_improvement()
相当,但由于它只被调用一次,它直接输出到最终最优策略。
在这一点上,我们可以运行一个训练周期(假设与之前相同的常数):
import numpy as np
e = 0
policy = None
while e < nb_max_epochs:
e += 1
old_tunnel_values = tunnel_values.copy()
value_evaluation()
if np.mean(np.abs(tunnel_values - old_tunnel_values)) < tolerance:
policy = policy_selection()
break
最终值配置(经过 127 次迭代)如下图表所示:
最终值矩阵
如前例所示,最终值配置是每个状态与结束状态之间距离的函数,但在这个情况下,γ = 0.9 的选择并不最优。事实上,靠近最终状态的水井不再被认为是非常危险的。绘制最终策略可以帮助我们理解其行为:
最终策略
如预期的那样,远离目标的井被避免,但靠近最终状态的两个井被视为合理的惩罚。这是因为价值迭代算法在价值和折扣因子方面非常贪婪,γ < 1.0;负状态的影响可以通过最终奖励来补偿。在许多场景中,这些状态是吸收的,因此它们的隐含奖励是+∞或-∞,这意味着没有其他动作可以改变最终值。我邀请读者用不同的折扣因子(记住,γ → 1的智能体非常短视,会避开任何障碍,甚至降低策略的效率)重复这个例子,并改变最终状态的价值。此外,读者应该能够回答以下问题:当标准奖励(默认值为-0.1)增加或减少时,智能体的行为是什么?
TD(0)算法
动态规划算法的一个问题是需要完全了解环境的状态和转移概率。不幸的是,在直接经验之前,这些信息在很多情况下都是未知的。特别是,状态可以通过让智能体探索环境来发现,但转移概率需要我们计算到达某个状态的数量,这通常是不可能的。
此外,如果一个环境有吸收状态,那么如果智能体已经学会了一个好的初始策略,它可能会阻止访问许多状态。例如,在一个可以描述为离散马尔可夫决策过程(MDP)的游戏中,智能体在学会如何前进而不陷入负吸收状态的同时,发现环境。
这些问题的通用解决方案是由一种不同的评估策略提供的,称为时间差分(TD)强化学习。在这种情况下,我们从一个空的价值矩阵开始,并让智能体遵循一个关于价值(但初始的是,通常是随机的)的贪婪策略。一旦智能体观察到由于一个动作a[t]导致的转移s[i] → s[j],并得到一个奖励r[ij],它就会更新对V(s[i])的估计。这个过程以剧集(这是最自然的方式)为结构,并在完成最大步数或遇到终端状态时结束。特别是,TD(0)算法根据以下规则更新价值:
常数 α 被限制在 0 和 1 之间,并作为学习率。每次更新都会考虑与当前值 V^((t))(s[i]) 的变化,这个变化与实际回报和先前估计之间的差异成比例。术语 r[ij] + γV^((t))(s[j]) 与先前方法中使用的术语类似,并代表给定当前回报和从后续状态开始的折现值所期望的值。然而,由于 V^((t))(s[j]) 是一个估计值,这个过程基于从先前值的自举。换句话说,我们从一个估计值开始,以确定下一个值,这个值应该更接近稳定的固定点。确实,TD(0) 是基于序列(通常称为回溯)的 TD 算法家族中最简单的例子,这个序列可以推广为(考虑 k 步):
由于我们使用单个奖励来近似期望的折现回报,TD(0) 通常被称为单步 TD 方法(或单步回溯)。可以通过考虑更多的后续奖励或替代策略来构建更复杂的算法。我们将在第十五章 高级策略估计算法中分析一个通用的变体,称为 TD(λ),并解释为什么这个算法对应于 λ = 0 的选择。
TD(0) 已被证明会收敛,尽管证明(可以在 基于模型的时序差分学习控制收敛,Van Hasselt H.,Wiering M. A.,*2007 年 IEEE 近似动态规划与强化学习研讨会(ADPRL 2007)论文中找到)更为复杂,因为需要考虑马尔可夫过程的演变。实际上,在这种情况下,我们使用截断估计和自举值 V(s[j]) 来近似期望的折现回报,而 V(s[j]) 在最初(以及大量迭代中)是不稳定的。然而,假设当 t → ∞ 时收敛,我们得到:
最后一个公式表示在贪婪最优策略迫使智能体执行导致状态从 s[j] 转移到 s[i] 的动作的情况下,状态 s[i] 的值。当然,在这个时候,自然会问在什么条件下算法会收敛。实际上,我们正在考虑的是周期性任务,并且估计 V^((∞))(s[i]) 只有在智能体无限次地执行到 s[i] 的转移,无限次地选择所有可能动作的情况下才是正确的。这种条件通常通过说策略必须是 无限探索下的极限贪婪(GLIE)来表示。换句话说,真正的贪婪只有在智能体能够无限制地探索环境,并且在不限次数的周期中不受限制时,才作为一个渐近状态实现。
这可能是 TD RL 最重要的限制,因为在现实场景中,某些状态可能非常不可能,因此估计永远不会积累足够经验以收敛到实际值。我们将在第十五章,“高级策略估计算法”中分析一些解决此问题的方法,但在我们的例子中,我们采用随机起始。换句话说,由于策略是贪婪的,可以始终避免某些状态,我们迫使智能体在每个剧集开始时在一个随机的非终端单元格中。这样,即使在贪婪策略下,我们也允许进行深度探索。当这种方法不可行(例如,因为环境动力学不可控)时,只能通过采用ε-贪婪策略来解决探索-利用困境,该策略选择一部分次优(甚至错误)的动作。这样,可以观察更多的转移,但代价是收敛速度较慢。
然而,正如 Sutton 和 Barto 所指出的,TD(0)收敛到由 MDP 确定的值函数的最大似然估计,找到模型的隐式转移概率。因此,如果观察的数量足够高,TD(0)可以快速找到一个最优策略,但与此同时,它对某些状态-动作对从未经历(或很少经历)的偏差估计也更加敏感。在我们的例子中,我们不知道初始状态是哪一个,因此选择一个固定的起始点会导致一个极其僵化的策略,几乎完全无法处理噪声情况。例如,如果起始点改为相邻(但从未探索过)的单元格,算法可能无法找到到达正终端状态的最优路径。另一方面,如果我们知道动力学是明确定义的,TD(0)将迫使智能体选择在当前环境知识下最有可能产生最优结果的动作。如果动力学部分是随机的,ε-贪婪策略的优势可以通过考虑智能体经历相同转移和相应值成比例增加的连续剧集来理解。例如,如果环境在许多经验之后改变一个转移,当策略几乎稳定时,智能体必须面对全新的经验。这种纠正需要许多剧集,由于这种随机变化发生的概率非常低,智能体可能永远无法学会正确的行为。相反,通过选择一些随机动作,遇到类似状态(甚至相同状态)的概率增加(想想一个状态由截图表示的游戏),算法可以相对于非常不可能的转移变得更加鲁棒。
完整的 TD(0)算法如下:
-
设置一个初始的确定性随机策略,π(s)
-
设置初始值数组,V(s) = 0 ∀ s ∈ S
-
设置段落数量,N[episodes]
-
设置每段的最大步数,N[max]
-
设置一个常数,α(例如,α = 0.1)
-
设置一个常数,γ(例如,γ** = 0.9)
-
设置计数器,e = 0
-
对于 i = 1 到 N[episodes]:
-
观察初始状态,s[i]
-
当 s[j] 非终止状态且 e < N[max]:
-
e += 1
-
选择动作,a[t] = π(s[i])
-
观察转换,(a[t], s[i]) → (s[j], r[ij])
-
更新状态的值函数,s[i]*
-
设置 s[i] = s[j]
-
-
更新策略,使其与值函数贪婪,π(s) = argmax[a] Q(s, a)
-
棋盘环境中的 TD(0)
在这一点上,我们可以在棋盘环境中测试 TD(0)算法。第一步是定义一个初始随机策略和一个所有元素都等于 0 的值矩阵:
import numpy as np
policy = np.random.randint(0, nb_actions, size=(height, width)).astype(np.uint8)
tunnel_values = np.zeros(shape=(height, width))
由于我们希望在每段开始时选择一个随机起点,我们需要定义一个辅助函数,该函数必须排除终端状态(所有常数与之前定义相同):
import numpy as np
xy_grid = np.meshgrid(np.arange(0, height), np.arange(0, width), sparse=False)
xy_grid = np.array(xy_grid).T.reshape(-1, 2)
xy_final = list(zip(x_wells, y_wells))
xy_final.append([x_final, y_final])
xy_start = []
for x, y in xy_grid:
if (x, y) not in xy_final:
xy_start.append([x, y])
xy_start = np.array(xy_start)
def starting_point():
xy = np.squeeze(xy_start[np.random.randint(0, xy_start.shape[0], size=1)])
return xy[0], xy[1]
现在我们可以实现一个函数来评估单个段(将最大步数设置为 500,常数为α = 0.25):
max_steps = 1000
alpha = 0.25
def episode():
(i, j) = starting_point()
x = y = 0
e = 0
while e < max_steps:
e += 1
action = policy[i, j]
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
tunnel_values[i, j] += alpha * (reward + (gamma * tunnel_values[x, y]) - tunnel_values[i, j])
if is_final(x, y):
break
else:
i = x
j = y
确定与值相关的贪婪策略的函数与之前示例中已实现的函数相同;然而,我们报告它以确保示例的一致性:
def policy_selection():
for i in range(height):
for j in range(width):
if is_final(i, j):
continue
values = np.zeros(shape=(nb_actions, ))
values[0] = (tunnel_rewards[i - 1, j] + (gamma * tunnel_values[i - 1, j])) if i > 0 else -np.inf
values[1] = (tunnel_rewards[i, j + 1] + (gamma * tunnel_values[i, j + 1])) if j < width - 1 else -np.inf
values[2] = (tunnel_rewards[i + 1, j] + (gamma * tunnel_values[i + 1, j])) if i < height - 1 else -np.inf
values[3] = (tunnel_rewards[i, j - 1] + (gamma * tunnel_values[i, j - 1])) if j > 0 else -np.inf
policy[i, j] = np.argmax(values).astype(np.uint8)
在这一点上,我们可以开始一个包含 5,000 个段落的训练周期:
n_episodes = 5000
for _ in range(n_episodes):
episode()
policy_selection()
最终值矩阵如下所示:
随机起始点的最终值矩阵
如前所述的示例,最终值与最终正状态的距离成反比。让我们分析得到的策略,以了解算法是否收敛到一致解:
随机起始点的最终策略
如所示,允许随机选择起始状态以独立于初始条件找到最佳路径。为了更好地理解这种策略的优势,让我们绘制当初始状态固定为单元格(0, 0)(对应左上角)时的最终值矩阵:
以固定初始状态(0, 0)的最终值矩阵
在没有进一步分析的情况下,我们可以看到许多状态从未被访问过或只被访问过几次,因此得到的策略与特定初始状态极为贪婪。值为-1.0 的块表示代理经常必须随机选择动作的状态,因为值没有差异,因此用不同的初始状态解决环境可能极其困难。得到的策略证实了这一分析:
以固定初始状态(0, 0)的最终策略
如所见,智能体只有在初始点允许我们从(0, 0)开始穿越轨迹时才能达到最终状态。在这些所有情况下,即使路径比上一个例子中的路径更长,也有可能恢复最优策略。相反,如(0, 4)这样的状态显然是存在策略损失的情况。换句话说,智能体在没有任何知识或意识的情况下采取行动,成功的概率收敛到 0。作为一个练习,我邀请读者用不同的起始点(例如,一组固定的起始点)和更高的α值来测试这个算法。目标也是回答这些问题:是否可以加快学习过程?是否需要从所有可能的状态开始,才能获得全局最优策略?
摘要
在本章中,我们介绍了最重要的强化学习(RL)概念,重点关注环境作为马尔可夫决策过程(MDP)的数学结构,以及不同类型的策略以及它们如何从智能体获得的期望奖励中推导出来。特别是,我们定义了状态的价值为考虑一个按因子γ折现的序列的期望未来奖励。同样,我们引入了 Q 函数的概念,即当智能体处于特定状态时,动作的价值。
这些概念直接采用了策略迭代算法,该算法基于动态规划方法,假设对环境的完全了解。任务分为两个阶段;在第一阶段,智能体根据当前策略评估所有状态,而在第二阶段,策略被更新以对新价值函数进行贪婪选择。这样,智能体被迫始终选择导致最大化获得价值的转换的动作。
我们还分析了一种变体,称为价值迭代,它执行单一评估并以贪婪的方式选择策略。与前一种方法的主要区别在于,现在智能体立即选择最高价值,假设这个过程的结果与策略迭代等效。实际上,很容易证明,在无限次转换之后,两种算法都会收敛到最优价值函数。
最后一个算法称为 TD(0),它基于无模型的方法。实际上,在许多情况下,很难知道所有转换概率,有时甚至所有可能的状态都是未知的。这种方法基于时间差分评估,它在与环境交互时直接执行。如果智能体可以无限次访问所有状态(显然,这只是一个理论条件),则该算法已被证明比其他方法更快地收敛到最优价值函数。
在下一章,第十五章,高级策略估计算法中,我们将继续讨论强化学习算法,介绍一些可以使用深度卷积网络立即实现的高级方法。
第十五章:高级策略估计算法
在本章中,我们将继续探索强化学习(Reinforcement Learning,RL)的世界,关注那些可以用于解决难题的复杂算法。由于这仍然是强化学习的入门部分(整个主题极其庞大),本章的结构基于许多实用的例子,这些例子可以作为在更复杂场景中工作的基础。
本章将讨论的主题包括:
-
TD(λ) 算法
-
行为-评论家 TD(0)
-
SARSA
-
Q-learning
-
基于简单视觉输入和神经网络的 Q-learning
TD(λ) 算法
在上一章中,我们介绍了时间差分策略,并讨论了一个简单的例子,称为 TD(0)。在 TD(0) 的情况下,折现奖励是通过使用一步回溯来近似的。因此,如果智能体在状态 s[t] 执行动作 a[t],并且观察到转换到状态 s[t][+1],则近似变为以下:
如果任务是分段的(如在许多现实场景中)并且有 T(e[i]) 步,则分段 e[i] 的完整回溯如下:
上述表达式在 MDP 过程达到吸收状态时结束;因此,R[t] 是实际折现奖励的价值。TD(0) 和此选择之间的区别很明显:在前一种情况下,我们可以在每次转换后更新价值函数,而使用完整回溯则需要等待分段的结束。我们可以说这种方法(被称为蒙特卡洛,因为它基于对整个序列总体奖励的平均化思想)正好是 TD(0) 的对立面;因此,考虑基于 k-步回溯的中间解决方案是合理的。特别是,我们的目标是找到一个在线算法,一旦回溯可用,就可以利用它们。
让我们想象一个由四个步骤组成的序列。智能体处于第一个状态并观察到一次转换;在这个时候,只能进行一步回溯,并且更新价值函数以提高收敛速度是个好主意。在第二次转换之后,智能体可以使用两步回溯;然而,它也可以考虑除了较新的、更长的回溯之外的第一步回溯。因此,我们有两种近似:
前面哪一个是更可靠的?显然,第二个依赖于第一个(特别是当价值函数几乎稳定时),等等,直到分段的结束。因此,最常见的策略是采用加权平均,为每个回溯分配不同的重要性水平(假设最长的回溯有 k 步):
Watkins 在 《延迟奖励学习》(Watkins C.I.C.H.,博士论文,剑桥大学,1989 年)中证明了这种方法(无论是否有平均)具有减少相对于最优值函数 V(s; π) 的期望 R[t]^k 的绝对误差的基本性质。事实上,他证明了以下不等式成立:
由于 γ 被限制在 0 和 1 之间,右侧始终小于最大绝对误差 V(t) - V(s;π),其中 V(s) 是在一段时间内某个状态的价值。因此,k-步回溯(或不同回溯的组合)的期望折现回报提供了对最优值函数更准确的估计,如果策略被选择为相对于它贪婪的话。这并不令人惊讶,因为更长的回溯包含了更多的实际回报,但这个定理的重要性在于它在使用不同 k-步回溯的平均值时的有效性。换句话说,它为我们提供了数学证明,即直观的方法实际上会收敛,并且它还可以有效地提高收敛速度和最终精度。
然而,管理 k 系数通常是有问题的,在许多情况下是无用的。TD(λ)背后的主要思想是使用一个可以调整以满足特定需求的单一因子,λ。理论分析(或称为 Sutton 和 Barto 所指的“前视图”)在一般情况下基于指数衰减平均。如果我们考虑一个 λ 被限制在 0 和 1(不包括)之间的几何级数,我们得到:
因此,我们可以考虑无限回溯的平均折现回报 R[t]^((λ)):
在定义有限情况之前,了解 R[t]^((λ)) 是如何构建的有助于理解。由于 λ 被限制在 0 和 1 之间,因子按比例衰减到 λ,因此第一次回溯影响最大,所有后续的回溯对估计的影响越来越小。这意味着,在一般情况下,我们假设对 R[t] 的估计对 立即回溯(它们变得越来越精确)更重要,我们只利用较长的回溯来提高估计的价值。现在,应该清楚的是,λ = 0 等同于 TD(0),因为只有一步回溯保留在求和中(记住 0⁰ = 1),而更高的值涉及所有剩余的回溯。现在,让我们考虑一个长度为 T(e[i]) 的回溯 e[i]。
传统上,如果智能体在 t = T(e[i]) 达到吸收状态,所有剩余的 t+i 返回都等于 Rt;因此,我们可以截断 R[t]^((λ)):
之前表达式的第一项涉及所有非终止状态,而第二项等于 R[t],按第一时间步和最终状态之间的距离成比例折扣。再次强调,如果 λ = 0,我们得到 TD(0),但现在我们也被授权考虑 λ = 1(因为总和总是扩展到有限数量的元素)。当 λ = 1 时,我们得到 R[t]^((λ)) = R[t],这意味着我们需要等待直到剧集的结束才能获得实际的折扣奖励。如前所述,这种方法通常不是首选解决方案,因为当剧集非常长时,智能体选择的动作在大多数情况下都是基于过时的价值函数。因此,通常使用 λ 值小于 1 的 TD(λ),以获得在线更新的优势,同时基于新状态的纠正。为了在不看未来的情况下实现这一目标(我们希望在新的信息可用时立即更新 V(s)),我们需要引入 资格迹**e(s) 的概念(有时,在计算神经科学背景下,e(s) 也被称为 刺激迹)。
状态 s 的资格迹是一个随时间变化的函数,它返回特定状态的权重(大于 0)。让我们想象一个序列,s[1], s[2], ..., s[n],并考虑一个状态,s[i]。在备份 V(s[i]) 更新后,智能体继续其探索。在什么情况下,s[i] 的新更新(给定更长的备份)是重要的?如果 s[i] 不再被访问,更长的备份的影响必须越来越小,并且 s[i] 被说成是不适合在 V(s) 中进行更改。这是之前假设的后果,即较短的备份通常具有更高的重要性。因此,如果 s[i] 是初始状态(或紧接在初始状态之后)并且智能体移动到其他状态,s[i] 的影响必须衰减。相反,如果 s[i] 被重新访问,这意味着之前对 V(s[i]) 的估计可能是错误的,因此 s[i] 是适合进行更改的。(为了更好地理解这个概念,想象一个序列,s[1], s[2], s[1], ...。很明显,当智能体处于 s[1] 以及 s[2] 时,它不能选择正确的动作;因此,有必要重新评估 V(s),直到智能体能够继续前进。)
最常见的策略(在Reinforcement Learning,Sutton R. S.,Barto A. G.,The MIT Press中也有讨论)是以递归的方式定义资格迹。在每个时间步之后,et会以一个等于γλ的因子衰减(以满足前向视角的要求);但是,当状态s被重新访问时,et也会增加 1(et =* γλet-1* + 1)。这样,我们可以在需要强调其影响时,对e(s)的趋势进行跳跃。然而,由于e(s)的衰减与跳跃独立,后来访问和重新访问的状态的影响比很快重新访问的状态要低。这种选择的原因非常直观:在长时间序列之后重新访问的状态的重要性显然低于在几步之后重新访问的状态的重要性。实际上,如果代理在场景开始时在两个状态之间来回移动,那么R[t]的估计显然是错误的,但当代理在探索其他区域之后重新访问状态时,错误变得不那么显著。例如,一个策略可以允许一个初始阶段以实现部分目标,然后可以强制代理返回以到达终端状态。
利用资格迹,TD(λ)可以在更复杂的环境中实现非常快的收敛,在一步 TD 方法和蒙特卡洛方法之间进行权衡(通常避免使用蒙特卡洛方法)。在此阶段,读者可能会想知道我们是否确信收敛性,幸运的是,答案是肯定的。Dayan 在The convergence of TD (λ) for General λ,Dayan P.,Machine Learning 8,3–4/1992中证明了对于通用的λ,只要满足一些特定的假设和基本条件,即策略是 GLIE,TD(λ)就会收敛。证明非常技术性,超出了本书的范围;然而,最重要的假设(通常是满足的)是:
-
马尔可夫决策过程(MDP)有吸收状态(换句话说,所有场景都在有限步骤内结束)。
-
所有的转移概率均非空(所有状态可以无限次访问)。
第一个条件很明显,没有吸收状态会导致无限探索,这与 TD 方法不相容(有时可以提前结束场景,但这可能是不被接受的(在某些情况下)或次优选择(在许多情况下))。此外,Sutton 和 Barto(在上述书中)证明了 TD(λ)等价于使用加权平均的折现回报近似,但没有向前看未来(这显然是不可能的)。
完整的 TD(λ)算法(带有可选的强制终止场景)如下:
-
设置一个初始的确定性随机策略,π(s)
-
设置初始值数组,V(s) = 0 ∀ s ∈ S
-
设置初始资格迹数组,*e(s) = 0 ∀ s ∈ S
-
设置剧集数,N[episodes]
-
设置每剧集的最大步数,N[max]
-
设置一个常数,α (α = 0.1)
-
设置一个常数,γ (γ = 0.9)
-
设置一个常数,λ (λ = 0.5)
-
设置一个计数器,e = 0
-
对于 i = 1 到 N[episodes]:
-
创建一个空的状态列表,L
-
观察初始状态,s[i],并将 s[i] 添加到 L
-
当 s[j] 是非终结符且 e < N[max:]
-
e += 1
-
选择动作,*a[t] = π(s[i])
-
观察转换,(a[t], s[i]) → (s[j], r[ij])
-
计算 TD 错误作为 TD[error] = r[ij] + γV(s[j]) - V(s[i])
-
增加资格迹,e(s[i]) += 1.0
-
对于 s 在 L 中:
-
更新值,V(s) += α · TD[error] · e(s)
-
更新资格迹,*e(s) = γλ
-
-
设置 s[i] = s[j]
-
将 s[j] 添加到 L
-
-
更新策略,使其对值函数贪婪,π(s) = argmax[a] Q(s, a)
-
读者可以通过考虑 TD 错误及其反向传播来更好地理解此算法的逻辑。即使这只是一个比较,也可以想象 TD(λ) 的行为类似于用于训练神经网络的 随机梯度下降 (SGD) 算法。事实上,错误被传播到前面的状态(类似于 MLP 的底层),并按其重要性成比例地影响它们,这种重要性由它们的资格迹定义。因此,资格迹较高的状态可以被认为是错误的责任更大;因此,相应的值必须按比例进行纠正。这不是一个正式的解释,但它可以简化对动态的理解,而不会过度损失严谨性。
在更复杂的棋盘环境中的 TD(λ)
在这一点上,我们想要用稍微复杂一点的地道环境测试 TD(λ) 算法。实际上,连同吸收状态一起,我们还将考虑一些中间的正状态,这些状态可以想象为 检查点。智能体应该学习从任何单元格到最终状态的最佳路径,试图通过尽可能多的检查点。让我们先定义新的结构:
import numpy as np
width = 15
height = 5
y_final = width - 1
x_final = height - 1
y_wells = [0, 1, 3, 5, 5, 6, 7, 9, 10, 11, 12, 14]
x_wells = [3, 1, 2, 0, 4, 3, 1, 3, 1, 2, 4, 1]
y_prizes = [0, 3, 4, 6, 7, 8, 9, 12]
x_prizes = [2, 4, 3, 2, 1, 4, 0, 2]
standard_reward = -0.1
tunnel_rewards = np.ones(shape=(height, width)) * standard_reward
def init_tunnel_rewards():
for x_well, y_well in zip(x_wells, y_wells):
tunnel_rewards[x_well, y_well] = -5.0
for x_prize, y_prize in zip(x_prizes, y_prizes):
tunnel_rewards[x_prize, y_prize] = 1.0
tunnel_rewards[x_final, y_final] = 5.0
init_tunnel_rewards()
奖励结构如下所示:
新地道环境中的奖励方案
在这一点上,我们可以继续初始化所有常数(特别是,我们选择了 λ = 0.6,这是一个中间解决方案,保证了接近蒙特卡洛方法的意识,同时不损害学习速度):
import numpy as np
nb_actions = 4
max_steps = 1000
alpha = 0.25
lambd = 0.6
gamma = 0.95
tunnel_values = np.zeros(shape=(height, width))
eligibility_traces = np.zeros(shape=(height, width))
policy = np.random.randint(0, nb_actions, size=(height, width)).astype(np.uint8)
就像在 Python 中,关键字 lambda
是保留的;我们使用了截断的表达式 lambd
来声明常数。
由于我们想要从一个随机单元格开始,我们需要重复上一章中介绍的相同程序;但在这个情况下,我们还包括检查点状态:
import numpy as np
xy_grid = np.meshgrid(np.arange(0, height), np.arange(0, width), sparse=False)
xy_grid = np.array(xy_grid).T.reshape(-1, 2)
xy_final = list(zip(x_wells, y_wells)) + list(zip(x_prizes, y_prizes))
xy_final.append([x_final, y_final])
xy_start = []
for x, y in xy_grid:
if (x, y) not in xy_final:
xy_start.append([x, y])
xy_start = np.array(xy_start)
def starting_point():
xy = np.squeeze(xy_start[np.random.randint(0, xy_start.shape[0], size=1)])
return xy[0], xy[1]
我们现在可以定义episode()
函数,它实现了完整的 TD(λ)周期。由于我们不希望智能体无限次地四处游荡尝试通过检查点,我们决定在探索期间减少奖励,以激励智能体仅通过必要的检查点——同时尽可能快地达到最终状态:
import numpy as np
def is_final(x, y):
if (x, y) in zip(x_wells, y_wells) or (x, y) == (x_final, y_final):
return True
return False
def episode():
(i, j) = starting_point()
x = y = 0
e = 0
state_history = [(i, j)]
init_tunnel_rewards()
total_reward = 0.0
while e < max_steps:
e += 1
action = policy[i, j]
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
total_reward += reward
td_error = reward + (gamma * tunnel_values[x, y]) - tunnel_values[i, j]
eligibility_traces[i, j] += 1.0
for sx, sy in state_history:
tunnel_values[sx, sy] += (alpha * td_error * eligibility_traces[sx, sy])
eligibility_traces[sx, sy] *= (gamma * lambd)
if is_final(x, y):
break
else:
i = x
j = y
state_history.append([x, y])
tunnel_rewards[x_prizes, y_prizes] *= 0.85
return total_reward
def policy_selection():
for i in range(height):
for j in range(width):
if is_final(i, j):
continue
values = np.zeros(shape=(nb_actions, ))
values[0] = (tunnel_rewards[i - 1, j] + (gamma * tunnel_values[i - 1, j])) if i > 0 else -np.inf
values[1] = (tunnel_rewards[i, j + 1] + (gamma * tunnel_values[i, j + 1])) if j < width - 1 else -np.inf
values[2] = (tunnel_rewards[i + 1, j] + (gamma * tunnel_values[i + 1, j])) if i < height - 1 else -np.inf
values[3] = (tunnel_rewards[i, j - 1] + (gamma * tunnel_values[i, j - 1])) if j > 0 else -np.inf
policy[i, j] = np.argmax(values).astype(np.uint8)
is_final()
和policy_selection()
函数与上一章中定义的相同,无需解释。即使实际上并不必要,我们决定在达到max_steps
步数后强制终止。这有助于开始时,因为策略不是ε-贪婪的,智能体可能会陷入无限循环探索中。现在我们可以为固定数量的轮次训练模型(或者,当值数组不再变化时,可以停止过程):
n_episodes = 5000
total_rewards = []
for _ in range(n_episodes):
e_reward = episode()
total_rewards.append(e_reward)
policy_selection()
episode()
函数返回总奖励;因此,检查智能体学习过程是如何发展的很有用:
智能体获得的总奖励
在开始时(大约 500 轮),智能体采用不可接受的策略,导致非常负面的总奖励。然而,在大约 1,000 次迭代后,算法达到了一个最优策略,后续的轮次仅略有改进。振荡是由于不同的起始点;然而,总奖励从未为负,并且随着检查点权重的衰减,这是一个积极的信号,表明智能体达到了最终的正状态。为了证实这一假设,我们可以绘制学习到的值函数:
最终值矩阵
这些值与我们的初始分析一致;事实上,当单元格接近检查点时,它们往往更高,但与此同时,全局配置(考虑到对V(s)贪婪的策略)迫使智能体达到周围值最高的结束状态。最后一步是检查实际策略,特别关注检查点:
最终策略
如我们所见,智能体试图通过检查点,但当它接近最终状态时,它(正确地)更倾向于尽快结束这一轮。我邀请读者使用不同的常数λ值重复实验,并改变检查点的环境动态。如果它们的值保持不变会发生什么?是否可以通过更高的λ来改善策略?
重要的是要记住,由于我们广泛使用随机值,连续的实验可能会由于不同的初始条件而产生不同的结果。然而,当轮次足够多时,算法应该始终收敛到最优策略。
棋盘环境中的 Actor-Critic TD(0)
在这个例子中,我们想要使用一个名为 Actor-Critic 的替代算法,结合 TD(0)。在这个方法中,智能体被分为两个部分,一个 Critic,它负责评估价值估计的质量,以及一个 actor,它选择并执行动作。正如 Dayan 在 Theoretical Neuroscience 中所指出的(Dayan P., Abbott L. F., The MIT Press),Actor-Critic 方法中的动态类似于策略评估和策略改进步骤的交织。实际上,Critic 的知识是通过一个迭代过程获得的,其初始评估通常是次优的。
结构架构如下图所示:
Actor-Critic 架构
在这个特定情况下,最好采用基于 softmax 函数的 ε-greedy 软策略。模型存储一个矩阵(或一个近似函数),称为 策略重要性,其中每个条目 pi 代表在特定状态下对特定动作的偏好值。实际的随机策略是通过应用 softmax 并使用一个简单的技巧来增加当指数变得非常大时的数值稳定性来获得的:
在状态 s[i] 中执行动作 a 并观察到过渡到状态 s[j] 以及奖励 r[ij] 后,Critic 评估 TD 错误:
如果 V(s[i]) < r[ij] + γV(s[j]),则认为转换是积极的,因为值在增加。相反,当 V(s[i]) > r[ij] + γV(s[j])* 时,Critic 将动作评估为负,因为之前的值高于新的估计。一种更通用的方法是基于 优势 的概念,它被定义为:
通常,前一个表达式中的一个项可以被近似。在我们的情况下,我们无法直接计算 Q 函数;因此,我们用项 r[ij] + γV(s[j])* 来近似它。很明显,优势的作用类似于 TD 错误(这是一个近似)的作用,并且必须代表在某个状态下采取的动作是一个好选择还是坏选择。对所有 优势 Actor-Critic (A3C) 算法(换句话说,标准 策略梯度 算法的改进)的分析超出了本书的范围。然而,读者可以在 High-Dimensional Continuous Control Using Generalized Advantage Estimation 中找到一些有用的信息,Schulman J., Moritz P., Levine S., Jordan M. I., Abbeel P., ICLR 2016。
当然,Actor-Critic 校正是不够的。为了改进策略,有必要使用标准算法(如 TD(0)、TD(λ)或最小二乘回归,这可以通过神经网络实现)来学习正确的值函数 V(s)。对于许多其他算法,这个过程只有在足够多的迭代次数之后才能收敛,这必须被利用来多次访问状态,尝试所有可能的行为。
因此,使用 TD(0)方法,在评估 TD 误差后的第一步是使用前一章中定义的规则更新 V(s):
第二步更加实用;实际上,Critic 的主要作用实际上是对每个动作进行批评,决定在某种状态下是增加还是减少再次选择该动作的概率。这个目标可以通过简单地更新策略重要性来实现:
学习率 ρ 的作用极其重要;事实上,不正确的值(换句话说,过高的值)可能导致初始错误的校正,从而损害收敛。必须记住,值函数在开始时几乎完全未知,因此 Critic 没有机会通过意识来增加正确的概率。因此,我总是建议从非常小的值(ρ = 0.001)开始,并且只有在算法的收敛速度确实得到有效提高时才增加它。
由于策略基于 softmax 函数,在 Critic 更新后,值将始终被重新归一化,从而形成一个实际的概率分布。经过足够多的迭代次数,并且正确选择 ρ 和 γ,模型能够学习到随机策略和值函数。因此,可以通过始终选择概率最高的动作(这对应于隐式贪婪行为)来使用训练好的智能体:
现在我们将这个算法应用到隧道环境中。第一步是定义常数(因为我们正在寻找一个远视的智能体,所以我们设置折现因子 γ = 0.99):
import numpy as np
tunnel_values = np.zeros(shape=(height, width))
gamma = 0.99
alpha = 0.25
rho = 0.001
在这一点上,我们需要定义策略重要性数组和一个生成 softmax 策略的函数:
import numpy as np
nb_actions = 4
policy_importances = np.zeros(shape=(height, width, nb_actions))
def get_softmax_policy():
softmax_policy = policy_importances - np.amax(policy_importances, axis=2, keepdims=True)
return np.exp(softmax_policy) / np.sum(np.exp(softmax_policy), axis=2, keepdims=True)
实现单个训练步骤所需的函数非常简单,读者应该已经熟悉它们的结构:
import numpy as np
def select_action(epsilon, i, j):
if np.random.uniform(0.0, 1.0) < epsilon:
return np.random.randint(0, nb_actions)
policy = get_softmax_policy()
return np.argmax(policy[i, j])
def action_critic_episode(epsilon):
(i, j) = starting_point()
x = y = 0
e = 0
while e < max_steps:
e += 1
action = select_action(epsilon, i, j)
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
td_error = reward + (gamma * tunnel_values[x, y]) - tunnel_values[i, j]
tunnel_values[i, j] += (alpha * td_error)
policy_importances[i, j, action] += (rho * td_error)
if is_final(x, y):
break
else:
i = x
j = y
在这一点上,我们可以用 50,000 次迭代和 30,000 次探索性迭代(探索因子线性衰减)来训练模型:
n_episodes = 50000
n_exploration = 30000
for t in range(n_episodes):
epsilon = 0.0
if t <= n_exploration:
epsilon = 1.0 - (float(t) / float(n_exploration))
action_critic_episode(epsilon)
结果的贪婪策略如图所示:
最终贪婪策略
最终的贪婪策略与目标一致,智能体通过避免陷阱始终达到最终的正状态。这种算法可能比必要的更复杂;然而,在复杂情况下,它证明非常有效。事实上,学习过程可以通过 Critic 执行的快速校正而显著改进。此外,作者注意到 Actor-Critic 对错误的(或噪声的)评估更稳健。由于策略是分别学习的,V(s)的小幅变化不会轻易改变概率π(s, a)(特别是当动作通常比其他动作强得多时)。另一方面,如前所述,为了避免过早收敛,有必要避免算法修改重要性/概率,而不需要过多的迭代。只有在分析每个具体场景之后,才能找到正确的权衡,不幸的是,没有适用于所有情况的通用规则。我的建议是测试各种配置,从小的值开始(例如,折扣因子γ ∈ [0.7, 0.9]),评估在相同的探索期后获得的累计奖励。
复杂的深度学习模型(如异步 A3C;参见异步深度强化学习方法,Mnih V.,Puigdomènech Badia A.,Mirza M.,Graves A.,Lillicrap T. P.,Harley T.,Silver D.,Kavukcuoglu K.,arXiv:1602.01783 [cs.LG]以获取更多信息)基于一个网络,该网络输出 softmax 策略(其动作通常与概率成正比)和值。而不是使用显式的ε-greedy 软策略,可以在全局成本函数中添加一个最大熵约束*:
当所有动作具有相同概率时,熵达到最大值,这个约束(带有适当的权重)迫使算法增加探索概率,直到某个动作变得主导,不再需要避免贪婪选择。这是一种合理且简单的方法来应用自适应ε-greedy 策略,因为模型与每个状态分别工作,不确定性非常低的状态可以变得贪婪;在需要继续探索时,可以自动保持高熵,以最大化奖励。
双重校正的效果,加上最大熵约束,提高了模型的收敛速度,鼓励在初始迭代中进行探索,并产生非常高的最终精度。我邀请读者在其他场景和算法中实现这个变体。特别是,在本章的结尾,我们将尝试一个基于神经网络的算法。由于示例相当简单,我建议使用 Tensorflow 根据 Actor-Critic 方法创建一个小型网络。读者可以为价值使用均方误差损失,为策略使用 softmax 交叉熵。一旦模型成功应用于我们的玩具示例,就可以开始处理更复杂的情况(如 OpenAI Gym 中提出的gym.openai.com/
)。
SARSA 算法
SARSA(其名称来源于序列状态-动作-奖励-状态-动作)是 TD(0)的自然扩展,用于估计Q函数。其标准公式(有时称为一步 SARSA 或 SARSA(0),原因与上一章中解释的相同)基于单个下一个奖励r[t+1],该奖励是通过在状态s[t]中执行动作a[t]获得的。时间差计算基于以下更新规则:
该方程等价于 TD(0),如果策略选择为 GLIE,根据Convergence Results for Single-Step On-Policy Reinforcement-Learning Algorithms(Singh S.,Jaakkola T.,Littman M. L.,Szepesvári C.,Machine Learning,39/2000)中的证明,SARSA 在所有(状态,动作)对都被无限次体验的情况下,以概率 1 收敛到最优策略π^(opt)(s)。这意味着如果策略更新为相对于由Q引起的当前价值函数的贪婪策略,则成立:
对于Q函数,同样的结果也是有效的。特别是,证明中所需的最重要条件是:
-
学习率,α ∈ [0, 1],满足约束Σα = ∞和Σα² < ∞
-
奖励的方差必须是有限的
当α是状态和时间步的函数时,第一个条件尤为重要;然而,在许多情况下,它是一个介于 0 和 1 之间的常数,因此,Σα² = ∞。解决这个问题的常见方法(尤其是当需要大量迭代时)是在训练过程中让学习率衰减(换句话说,指数衰减)。相反,为了减轻非常大的奖励的影响,可以将它们剪辑到合适的范围内([-1, 1])。在许多情况下,不需要采用这些策略,但在更复杂的场景中,它们可能变得至关重要,以确保算法的收敛。此外,正如前一章所指出的,这类算法在开始稳定策略之前需要一个长的探索阶段。最常用的策略是采用ε-贪婪策略,探索因子的时序衰减。在最初的迭代中,智能体必须探索,而不关心动作的回报。这样,就可以在最终精炼阶段开始之前评估实际值,该阶段的特点是纯粹的贪婪探索,基于对V(s)的更精确近似。
完整的 SARSA(0)算法(带有可选的强制终止剧集)是:
-
设置一个初始的确定性随机策略,π(s)
-
设置初始值数组,Q(s, a) = 0 ∀ s ∈ S 和 ∀ a ∈ A
-
设置剧集数量,N[episodes]
-
设置每剧集的最大步数,N[max]
-
设置常数,α (α = 0.1)
-
设置常数,γ (γ** = 0.9)
-
设置初始探索因子,ε^((0)) = 1.0
-
定义一个策略,让探索因子ε衰减(线性或指数)
-
设置计数器,e = 0
-
对于i = 1到N[episodes]:
-
观察初始状态,s[i]
-
当s[j]非终止且e < N[max]时:
-
e += 1
-
选择动作,a[t] = π(s[i),带有探索因子ε^(e)
-
观察转换,(a[t], s[i]) → (s[j], r[ij])
-
选择动作,a[t+1] = π(s[j]),带有探索因子ε^(e)*
-
更新Q(s[t], a[t])函数(如果s[j]是终止状态,则设置Q(s[t+1], a[t+1]) = 0*)
-
设置s[i] = s[j]
-
-
可选性跟踪的概念也可以扩展到 SARSA(以及其他 TD 方法);然而,这超出了本书的范围。对感兴趣的读者来说,可以在Sutton R. S.,Barto A. G.,强化学习,布拉德福德图书中找到所有算法(包括它们的数学公式)。
棋盘环境中的 SARSA
我们现在可以在原始隧道环境中测试 SARSA 算法(所有未重新定义的元素与上一章相同)。第一步是定义Q(s, a)数组以及在训练过程中使用的常数:
import numpy as np
nb_actions = 4
Q = np.zeros(shape=(height, width, nb_actions))
x_start = 0
y_start = 0
max_steps = 2000
alpha = 0.25
由于我们想采用ε-贪婪策略,我们可以将起点设置为 (0, 0)
,迫使代理达到积极最终状态。我们现在可以定义执行训练步骤所需的函数:
import numpy as np
def is_final(x, y):
if (x, y) in zip(x_wells, y_wells) or (x, y) == (x_final, y_final):
return True
return False
def select_action(epsilon, i, j):
if np.random.uniform(0.0, 1.0) < epsilon:
return np.random.randint(0, nb_actions)
return np.argmax(Q[i, j])
def sarsa_step(epsilon):
e = 0
i = x_start
j = y_start
while e < max_steps:
e += 1
action = select_action(epsilon, i, j)
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
action_n = select_action(epsilon, x, y)
reward = tunnel_rewards[x, y]
if is_final(x, y):
Q[i, j, action] += alpha * (reward - Q[i, j, action])
break
else:
Q[i, j, action] += alpha * (reward + (gamma * Q[x, y, action_n]) - Q[i, j, action])
i = x
j = y
select_action()
函数被设计为以概率ε选择随机动作,以概率1 - **ε选择基于Q(s, a)的贪婪动作。sarsa_step()
函数很简单,执行一个完整的剧集更新Q(s, a)(这就是为什么这是一个在线算法)。在这个阶段,我们可以对模型进行 20,000 个剧集的训练,并在前 15,000 个剧集期间使用ε的线性衰减(当 t > 15,000 时,ε设置为 0,以便采用纯贪婪策略):
n_episodes = 20000
n_exploration = 15000
for t in range(n_episodes):
epsilon = 0.0
if t <= n_exploration:
epsilon = 1.0 - (float(t) / float(n_exploration))
sarsa_step(epsilon)
如同往常,让我们检查学习到的值(考虑到策略是贪婪的,我们将绘制V(s) = max[a] Q(s, a)*):
最终值矩阵(作为V(s) = max[a] Q(s, a))
如预期,Q 函数已经以一致的方式学习,我们可以通过绘制结果策略来得到证实:
最终策略
政策与初始目标一致,代理避免了所有负面吸收状态,始终试图向最终积极状态移动。然而,一些路径似乎比预期的要长。作为一个练习,我邀请读者重新训练模型,进行更多的迭代次数,调整探索期。此外,是否可以通过增加(或减少)折扣因子γ来改进模型? 记住,γ → 0 导致短视的代理,只能根据即时奖励选择动作,而γ → 1 则迫使代理考虑更多的未来奖励。这个特定例子基于一个长期环境,因为代理始终从 (0, 0) 开始,必须到达最远点;因此,所有中间状态的重要性都较低,展望未来以选择最佳动作是有帮助的。使用随机起点可以无疑地改善所有初始状态的政策,但研究不同的γ值如何影响决策是有趣的;因此,我建议重复实验以评估不同的配置并提高对涉及 TD 算法的不同因素的意识。
Q-learning
这个算法由 Watkins(在延迟奖励学习,Watkins C.I.C.H.,博士论文,剑桥大学,1989;并在Watkins C.I.C.H.,Dayan P.,技术笔记 Q-Learning,机器学习 8,1992)提出,作为 SARSA 的更有效替代方案。Q-learning的主要特点是 TD 更新规则立即对Q(s[t+1], a)函数贪婪:
关键思想是比较当前 Q(s[t], a[t]) 值与代理处于后续状态时可以达到的最大 Q 值。实际上,由于策略必须是 GLIE,可以通过避免选择不会与最终动作关联的 Q 值而导致的错误估计来提高收敛速度。通过选择最大 Q 值,算法将比 SARSA 更快地趋向于最优解,并且,收敛证明也更加宽松。事实上,Watkins 和 Dayan(在上述论文中)证明了,如果 |r[i]| < R,则学习率 α ∈ 0, 1[ (在这种情况下, α 必须始终小于 1)并且对 SARSA 施加相同的约束(Σα = ∞ 和 Σα² < ∞),则估计的 Q 函数以概率 1 收敛到最优值:
![
如同 SARSA 所讨论的,可以通过使用裁剪函数和时间衰减来管理奖励和学习率的条件。在几乎所有的深度 Q 学习应用中,这些是保证收敛的极其重要的因素;因此,我邀请读者在训练过程无法收敛到可接受的解决方案时考虑它们。
完整的 Q-learning 算法(带有可选的强制终止回合)是:
-
设置一个初始的确定性随机策略,π(s)
-
设置初始值数组,Q(s, a) = 0 ∀ s ∈ S 和 ∀ a ∈ A
-
设置回合数,N[episodes]
-
设置每个回合的最大步数,N[max]
-
设置一个常数,α (α = 0.1)
-
设置一个常数,γ (γ** = 0.9)
-
设置一个初始探索因子, ε^((0)) (ε^((0)) = 1.0)
-
定义一个策略,让探索因子 ε衰减(线性或指数)
-
设置一个计数器,e = 0
-
对于 i = 1 到 N[episodes]:
-
观察初始状态, s[i]
-
当 s[j] 是非终止状态且 e < N[max] 时:
-
e += 1
-
选择动作,a[t] = **π(s[i]), 带有探索因子 ε^((e))
-
观察转换 (a[t], s[i]) → (s[j], r[ij])
-
选择动作,a[t+1] = π(s[j]**),带有探索因子 ε^((e))
-
使用 max[a] Q(s[t+1], a) 更新 Q(s[t], a[t]) 函数(如果 s[j] 是终止状态,则设置 Q(s[t+1], a[t+1]**) = 0)
-
设置 s[i] = s[j]
-
-
棋盘环境中的 Q 学习
让我们用 Q 学习算法重复之前的实验。由于所有常数都相同(以及选择ε-贪婪策略和起始点设置为(0, 0)),我们可以直接定义实现单个回合训练的函数:
import numpy as np
def q_step(epsilon):
e = 0
i = x_start
j = y_start
while e < max_steps:
e += 1
action = select_action(epsilon, i, j)
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
if is_final(x, y):
Q[i, j, action] += alpha * (reward - Q[i, j, action])
break
else:
Q[i, j, action] += alpha * (reward + (gamma * np.max(Q[x, y])) - Q[i, j, action])
i = x
j = y
我们现在可以训练模型 5,000 次迭代,其中 3,500 次是探索性的:
n_episodes = 5000
n_exploration = 3500
for t in range(n_episodes):
epsilon = 0.0
if t <= n_exploration:
epsilon = 1.0 - (float(t) / float(n_exploration))
q_step(epsilon)
得到的值矩阵(定义为 SARSA 实验中)是:
最终值矩阵
再次,学到的Q函数(以及显然的,贪婪的V(s))与初始目标一致(特别是考虑到起始点设置为(0, 0)),并且由此产生的策略可以立即证实这一结果:
最终策略
Q 学习的表现与 SARSA(即使收敛速度更快)没有太大区别,并且一些初始状态管理并不完美。这是我们的选择的结果;因此,我邀请读者使用随机起始重复练习,并比较 Q 学习和 SARSA 的训练速度。
使用神经网络的 Q 学习
现在,我们想使用较小的棋盘环境和神经网络(使用 Keras)来测试 Q 学习算法。与前例的主要区别在于,现在状态由当前配置的截图表示;因此,模型必须学会将值与每个输入图像和动作关联起来。这不是实际的深度 Q 学习(它基于深度卷积网络,需要更复杂的环境,我们无法在本书中讨论),但它展示了这样的模型如何使用与人类相同的输入学习最优策略。为了减少训练时间,我们正在考虑一个正方形的棋盘环境,有四个负吸收状态和一个正最终状态:
import numpy as np
width = 5
height = 5
nb_actions = 4
y_final = width - 1
x_final = height - 1
y_wells = [0, 1, 3, 4]
x_wells = [3, 1, 2, 0]
standard_reward = -0.1
tunnel_rewards = np.ones(shape=(height, width)) * standard_reward
for x_well, y_well in zip(x_wells, y_wells):
tunnel_rewards[x_well, y_well] = -5.0
tunnel_rewards[x_final, y_final] = 5.0
下图显示了奖励的图形表示:
较小棋盘环境中的奖励
由于我们希望向网络提供图形输入,我们需要定义一个函数来创建表示隧道的矩阵:
import numpy as np
def reset_tunnel():
tunnel = np.zeros(shape=(height, width), dtype=np.float32)
for x_well, y_well in zip(x_wells, y_wells):
tunnel[x_well, y_well] = -1.0
tunnel[x_final, y_final] = 0.5
return tunnel
reset_tunnel()
函数将所有值设置为 0,除了(用-1
标记)和最终状态(由0.5
定义)。代理的位置(用值1
定义)直接由训练函数管理。在此阶段,我们可以创建和编译我们的神经网络。由于问题不太复杂,我们正在使用 MLP:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(8, input_dim=width * height))
model.add(Activation('tanh'))
model.add(Dense(4))
model.add(Activation('tanh'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
model.compile(optimizer='rmsprop',
loss='mse')
输入是一个展平的数组,而输出是Q函数(所有对应于每个动作的值)。网络使用 RMSprop 和均方误差损失函数进行训练(我们的目标是减少实际值和预测值之间的均方误差)。为了训练和查询网络,创建两个专用函数是有帮助的:
import numpy as np
def train(state, q_value):
model.train_on_batch(np.expand_dims(state.flatten(), axis=0), np.expand_dims(q_value, axis=0))
def get_Q_value(state):
return model.predict(np.expand_dims(state.flatten(), axis=0))[0]
def select_action_neural_network(epsilon, state):
Q_value = get_Q_value(state)
if np.random.uniform(0.0, 1.0) < epsilon:
return Q_value, np.random.randint(0, nb_actions)
return Q_value, np.argmax(Q_value)
这些函数的行为很简单。对读者可能陌生的唯一元素是使用train_on_batch()
方法。与fit()
不同,此函数允许我们执行单个训练步骤,给定一批输入-输出对(在我们的情况下,我们始终只有一个对)。由于我们的目标是找到从每个可能的单元格开始的到达最终状态的最优路径,我们将使用随机起始:
import numpy as np
xy_grid = np.meshgrid(np.arange(0, height), np.arange(0, width), sparse=False)
xy_grid = np.array(xy_grid).T.reshape(-1, 2)
xy_final = list(zip(x_wells, y_wells))
xy_final.append([x_final, y_final])
xy_start = []
for x, y in xy_grid:
if (x, y) not in xy_final:
xy_start.append([x, y])
xy_start = np.array(xy_start)
def starting_point():
xy = np.squeeze(xy_start[np.random.randint(0, xy_start.shape[0], size=1)])
return xy[0], xy[1]
现在,我们可以定义执行单个训练步骤所需的函数:
import numpy as np
def is_final(x, y):
if (x, y) in zip(x_wells, y_wells) or (x, y) == (x_final, y_final):
return True
return False
def q_step_neural_network(epsilon, initial_state):
e = 0
total_reward = 0.0
(i, j) = starting_point()
prev_value = 0.0
tunnel = initial_state.copy()
tunnel[i, j] = 1.0
while e < max_steps:
e += 1
q_value, action = select_action_neural_network(epsilon, tunnel)
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
reward = tunnel_rewards[x, y]
total_reward += reward
tunnel_n = tunnel.copy()
tunnel_n[i, j] = prev_value
tunnel_n[x, y] = 1.0
prev_value = tunnel[x, y]
if is_final(x, y):
q_value[action] = reward
train(tunnel, q_value)
break
else:
q_value[action] = reward + (gamma * np.max(get_Q_value(tunnel_n)))
train(tunnel, q_value)
i = x
j = y
tunnel = tunnel_n.copy()
return total_reward
q_step_neural_network()
函数与前面示例中定义的函数非常相似。唯一的区别是视觉状态的管理。每次发生转换时,值1.0
(表示代理)从旧位置移动到新位置,前一个单元格的值重置为其默认值(保存在prev_value
变量中)。另一个次要区别是缺少α,因为 SGD 算法中已经设置了学习率,因此没有必要在模型中添加另一个参数。现在我们可以用 10,000 次迭代训练模型,其中 7,500 次用于探索:
n_episodes = 10000
n_exploration = 7500
total_rewards = []
for t in range(n_episodes):
tunnel = reset_tunnel()
epsilon = 0.0
if t <= n_exploration:
epsilon = 1.0 - (float(t) / float(n_exploration))
t_reward= q_step_neural_network(epsilon, tunnel)
total_rewards.append(t_reward)
当训练过程完成后,我们可以分析总奖励,以了解网络是否成功学习了Q函数:
神经网络 Q 学习算法获得的总奖励
很明显,模型运行良好,因为经过探索期后,总奖励值稳定在4
左右,由于路径长度不同而出现小幅波动(然而,由于 Keras 使用的内部随机状态,最终图表可能会有所不同)。为了确认这一点,让我们使用贪婪策略(相当于ε = 0)生成所有可能初始状态下的轨迹:
import numpy as np
trajectories = []
tunnels_c = []
for i, j in xy_start:
tunnel = reset_tunnel()
prev_value = 0.0
trajectory = [[i, j, -1]]
tunnel_c = tunnel.copy()
tunnel[i, j] = 1.0
tunnel_c[i, j] = 1.0
final = False
e = 0
while not final and e < max_steps:
e += 1
q_value = get_Q_value(tunnel)
action = np.argmax(q_value)
if action == 0:
if i == 0:
x = 0
else:
x = i - 1
y = j
elif action == 1:
if j == width - 1:
y = width - 1
else:
y = j + 1
x = i
elif action == 2:
if i == height - 1:
x = height - 1
else:
x = i + 1
y = j
else:
if j == 0:
y = 0
else:
y = j - 1
x = i
trajectory[e - 1][2] = action
trajectory.append([x, y, -1])
tunnel[i, j] = prev_value
prev_value = tunnel[x, y]
tunnel[x, y] = 1.0
tunnel_c[x, y] = 1.0
i = x
j = y
final = is_final(x, y)
trajectories.append(np.array(trajectory))
tunnels_c.append(tunnel_c)
trajectories = np.array(trajectories)
下图显示了 12 条随机轨迹:
使用贪婪策略生成的 12 条轨迹
代理始终遵循最优策略,不受初始状态的影响,并且永远不会陷入陷阱。即使示例相当简单,也有助于向读者介绍深度 Q 学习(有关更多细节,读者可以查看介绍性论文,深度强化学习:概述,Li Y.,arXiv:1701.07274 [cs.LG])。
在一般情况下,环境可以是一个更复杂的游戏(如 Atari 或 Sega),可能采取的动作数量非常有限。此外,没有随机开始的可能性,但通常一个好的做法是跳过一些初始帧,以避免对估计器的偏差。显然,网络必须更复杂(涉及卷积以更好地学习几何依赖关系),并且迭代次数必须非常大。为了加快收敛速度,可以采用许多其他技巧和特定算法,但由于篇幅限制,这些内容超出了本书的范围。
然而,一般过程及其逻辑几乎是相同的,理解为什么某些策略更可取以及如何提高准确性并不困难。作为一个练习,我邀请读者创建更复杂的环境,带有或不带有检查点和随机奖励。看到模型能够轻松地通过足够多的回合学习动态,这并不令人惊讶。此外,正如在 Actor-Critic 部分所建议的,使用 Tensorflow 实现这样一个模型是一个好主意,并将性能与 Q-learning 进行比较。
摘要
在本章中,我们介绍了基于不同长度备份的平均的 TD(0)的自然演变。被称为 TD(λ)的算法非常强大,它保证了比 TD(0)更快的收敛速度,只需满足少数(非限制性)条件。我们还展示了如何使用 TD(0)实现 Actor-Critic 方法,以便学习随机策略和值函数。
在接下来的章节中,我们讨论了基于估计Q函数的两种方法:SARSA 和 Q-learning。它们非常相似,但后者采用贪婪方法,其性能(特别是训练速度)使其优于 SARSA。Q-learning 算法是最新发展中最重要的模型之一。事实上,它是第一个与深度卷积网络结合使用的强化学习方法,用于解决复杂环境(如 Atari 游戏)。因此,我们也提供了一个基于 MLP 的简单示例,该 MLP 处理视觉输入并输出每个动作的Q值。
强化学习的世界极其迷人,每天都有数百名研究人员致力于改进算法和解决越来越复杂的问题。我邀请读者查阅参考文献,以找到可以利用的有用资源,从而更深入地理解模型及其发展。此外,我建议阅读由谷歌 DeepMind 团队撰写的博客文章,该团队是深度强化学习领域的先驱之一。我还建议搜索在arXiv上免费提供的论文。
我很高兴以这个主题结束这本书,因为我相信强化学习可以提供新的、更强大的工具,这将极大地改变我们的生活!