胶囊网络教程(Understanding Hinton’s Capsule Networks)
参考教程:
- https://pechyonkin.me/capsules-1/
- https://github.com/higgsfield/Capsule-Network-Tutorial
目录:
文章目录
1. 第一部分:直觉(Intuition)
1.1 引言(Introduction)
2017年10月,杰弗里·辛顿(Geoffrey Hinton )和他的团队发表了两篇论文,介绍了一种基于所谓胶囊的新型神经网络。除此之外,该团队还发布了一种算法,称为胶囊之间的动态路由(dynamic routing between capsules),可以训练这样的网络。
对于深度学习社区的每个人来说,这都是一个巨大的消息,原因有很多。首先,Hinton是深度学习的创始人之一,也是当今广泛使用的众多模型和算法的发明人。其次,这些论文介绍了一些全新的东西,这非常令人兴奋,因为它很可能会激发更多的研究浪潮和非常酷的应用。
在这篇文章中,我将解释为什么这个新架构如此重要,以及它背后的直觉。
然而,在谈论胶囊网络之前,我们需要先看看卷积神经网络(Convolutional Neural Network, CNN),它是当今深度学习的主力军。
Architecture of CapsNet from the [original paper](https://arxiv.org/abs/1710.09829).
1.2 卷积神经网络存在重要的缺陷
卷积神经网络(Convolutional Neural Network, CNN)非常棒。这也是深度学习在当今如此流行的原因之一。它们可以做出令人惊叹的事情,而人们过去认为计算机在很长很长一段时间内都无法做到这一点。尽管如此,它们也有其局限性和根本性的缺陷。
让我们考虑一个非常简单和非技术性的例子。想象一下一张脸。组件是什么?我们的脸是椭圆形的,有两只眼睛,一个鼻子和一张嘴巴。对于卷积神经网络来说,仅仅是这些物体的存在就可以很好地表明图像中有人脸。这些成分之间的方位和相对空间关系对卷积神经病网络来说不是很重要。
卷积神经网络CNN是如何工作的?CNN的主要组成部分是卷积层(convolutional layer)。它的工作是检测图像像素中的重要特征。更深(更接近输入)的层将学会检测简单的特征,如边缘和颜色梯度,而更高的层将把简单的特征组合成更复杂的特征。最后,网络顶部的密集层将结合非常高级别的特征并产生分类预测。
需要理解的一件重要事情是,高级特征将低级特征组合为在加权和:前一层的激活乘以后一层神经元的权重并相加,然后传递给激活非线性。在这种设置中,构成更高级别特征的更简单特征之间没有位置(平移和旋转)关系。解决这一问题的CNN方法是使用最大池或连续卷积层,以减少流经网络的数据的空间大小,从而增加更高层神经元的“视野”,从而允许它们在输入图像的更大区域中检测更高阶特征。最大池化是使卷积网络工作得出奇地好的支柱,在许多领域实现了超人的性能。但不要被它的性能所欺骗:虽然卷积神经网络比以前的任何模型都能更好地工作,但最大池化正在失去有价值的信息。
辛顿(Hinton)本人表示,最大池化运行得如此之好是一个巨大的错误和灾难:
当然,你可以取消最大池化,使用传统的卷积神经网络仍然可以获得良好的结果,但它们仍然不能解决关键问题:
卷积神经网络的内部数据表示没有考虑简单和复杂对象之间的重要空间层次。
在上面的例子中,照片中只存在两只眼睛、一只嘴和一个鼻子并不意味着有一张脸,我们还需要知道这些物体是如何相对于彼此定向的。
1.3 将三维世界硬编码到神经网络中:逆图形方法
计算机图形学处理从几何数据的一些内部层次表示构建视觉图像的问题。请注意,此表示的结构需要考虑对象的相对位置。这种内部表示被存储在计算机的存储器中,作为几何对象的阵列和表示这些对象的相对位置和方向的矩阵。然后,特殊的软件将该表示转换为屏幕上的图像。这称为渲染。
受这一想法的启发,辛顿(Hinton)认为,事实上,大脑所做的与渲染相反。他称之为逆向图形:从眼睛接收到的视觉信息中,他们解构了我们周围世界的层次表示,并试图将其与大脑中已经学习到的模式和关系相匹配。识别就是这样发生的。关键的观点是,物体在大脑中的表现并不取决于视角。
因此,在这一点上,问题是:我们如何在神经网络内部对这些层次关系进行建模?答案来自计算机图形学。在3D图形中,3D对象之间的关系可以用所谓的姿势来表示,本质上是平移加旋转。
Hinton认为,为了正确地进行分类和物体识别,保持物体各部分之间的层次姿态关系很重要。这是让你理解为什么胶囊理论如此重要的关键直觉。它包含了对象之间的相对关系,并在数字上表示为4D姿态矩阵。
当这些关系被构建到数据的内部表示中时,模型很容易理解它看到的东西只是它以前看到的东西的另一个视图。请考虑下面的图片。你可以很容易地认出这是自由女神像,尽管所有的图像都是从不同的角度显示的。这是因为自由女神像在你大脑中的内部表现并不取决于视角。你可能从未见过这些确切的照片,但你仍然立刻知道它是什么。
对于卷积神经网络CNN来说,这项任务真的很难,因为它对3D空间没有内在的理解,但对于CapsNet来说,这要容易得多,因为这些关系是明确建模的。与之前的技术水平相比,使用这种方法的论文能够将错误率降低45%,这是一个巨大的改进。
胶囊方法的另一个好处是,它能够通过只使用CNN使用的一小部分数据来学习实现最先进的性能(Hinton在他关于CNN的错误之处的著名演讲中提到了这一点)。从这个意义上说,胶囊理论更接近于人类大脑在实践中的作用。为了学会区分数字,人类大脑只需要看到几十个例子,最多几百个。另一方面,卷积神经网络需要数万个例子才能获得非常好的性能,这似乎是一种蛮力方法,显然不如我们用大脑做的方法。
1.4 为什么胶囊网络的提出花了这么久?
这个想法真的很简单,以前不可能没有人想到!事实是,辛顿(Hinton)几十年来一直在考虑这个问题。之所以没有出版物,只是因为以前没有技术方法使其发挥作用。其中一个原因是,在2012年左右之前的前GPU时代,计算机的功能还不够强大。另一个原因是,没有任何算法可以实现并成功地学习胶囊网络(以同样的方式,人工神经元的想法自20世纪40年代以来就一直存在,但直到20世纪80年代中期,反向传播算法才出现,并被允许成功地训练深度网络)。
同样,胶囊的想法本身并不是什么新鲜事,Hinton以前也提到过,但到目前为止还没有算法使其发挥作用。这种算法被称为“胶囊之间的动态路由”。该算法允许胶囊相互通信,并创建类似于计算机图形学中场景图的表示。
1.5 结论(Conclusion)
胶囊网络(Capsule Networks)引入了一种新的构建块,可用于深度学习,以更好地建模神经网络内部知识表示内部的层次关系。它们背后的直觉是非常简单和优雅的。
Hinton和他的团队提出了一种训练这种由胶囊组成的网络的方法,并在一个简单的数据集上成功地训练了它,实现了最先进的性能。这是非常令人鼓舞的。
尽管如此,还是存在挑战。当前的实现比其他现代深度学习模型慢得多。时间将证明胶囊网络是否可以快速有效地进行训练。此外,我们需要看看它们在更困难的数据集和不同的领域是否能很好地工作。
无论如何,胶囊网络是一个非常有趣且已经在工作的模型,随着时间的推移,它肯定会得到更多的发展,并有助于深度学习应用领域的进一步扩展。
这是关于胶囊网络系列的第一部分。在第二部分,更技术的部分,我将一步一步地引导您了解CapsNet的内部工作原理。
2. 第二部分: 胶囊网络如何工作?
2.1 引言(Introduction)
在本系列关于胶囊网络的第1部分中,我谈到了新颖架构背后的基本直觉和动机。在这一部分中,我将描述什么是胶囊,它是如何在内部工作的,以及它背后的直觉。在下一部分中我将主要关注动态路由算法。
2.2 什么是胶囊?(What is a Capsule?)
为了回答这个问题,我认为参考第一篇介绍胶囊的论文是个好主意 — Hinton等人的“转换自动编码器”, 即“Transforming Autoencoders"。对理解胶囊很重要的部分如下:
“Instead of aiming for viewpoint invariance in the activities of “neurons” that use a single scalar output to summarize the activities of a local pool of replicated feature detectors, artificial neural networks should use local “capsules” that perform some quite complicated internal computations on their inputs and then encapsulate the results of these computations into a small vector of highly informative outputs. Each capsule learns to recognize an implicitly defined visual entity over a limited domain of viewing conditions and deformations and it outputs both the probability that the entity is present within its limited domain and a set of “instantiation parameters” that may include the precise pose, lighting and deformation of the visual entity relative to an implicitly defined canonical version of that entity. When the capsule is working properly, the probability of the visual entity being present is locally invariant — it does not change as the entity moves over the manifold of possible appearances within the limited domain covered by the capsule. The instantiation parameters, however, are “equivariant” — as the viewing conditions change and the entity moves over the appearance manifold, the instantiation parameters change by a corresponding amount because they are representing the intrinsic coordinates of the entity on the appearance manifold.”
不是针对使用单个标量输出来总结复制特征检测器的局部池的活动的“神经元”的活动中的视点不变性,人工神经网络应该使用局部“胶囊”,对其输入执行一些相当复杂的内部计算,然后将这些计算的结果封装到一个具有高度信息输出的小向量中。每个胶囊学习在观看条件和变形的有限域上识别隐含定义的视觉实体,并且它输出实体存在于其有限域内的概率和一组可能包括精确姿势的“实例化参数”,视觉实体相对于该实体的隐式定义的规范版本的照明和变形。当胶囊正常工作时,视觉实体存在的概率是局部不变的 — 它不会随着实体在胶囊覆盖的有限域内的可能外观的歧管上移动而改变。然而,实例化参数是“等变的” — 随着观看条件的改变和实体在外观流形上移动,实例化参数改变相应的量,因为它们表示实体在外观歧管上的固有坐标。
上面这段话很精炼,我花了一段时间才一句一句地弄清楚它的意思。下面是我对上面一段的理解:人工神经元输出一个标量。此外,卷积神经网络使用卷积层,对于每个内核,在整个输入体积上复制相同内核的权重,然后输出2D矩阵,其中每个数字是该内核与输入体积的一部分卷积的输出。因此,我们可以将2D矩阵视为复制特征检测器的输出。然后,所有内核的2D矩阵被堆叠在一起,以产生卷积层的输出。
然后,我们试图在神经元的活动中实现视点不变性。我们通过最大池化的方式来做到这一点,该最大池化连续地查看上述2D矩阵中的区域,并在每个区域中选择最大的数量。结果,我们得到了我们想要的 — 活动的不变性。不变意味着通过稍微改变输入,输出仍然保持不变。而活动只是神经元的输出信号。换句话说,当我们在输入图像中稍微移动我们想要检测的对象时,网络活动(神经元的输出)不会因为最大池而改变,并且网络仍然会检测到对象。
上述机制不是很好,因为最大池化丢失了有价值的信息,并且也没有对特征之间的相对空间关系进行编码。我们应该使用胶囊,因为它们将以向量的形式(而不是神经元输出的标量)封装关于他们正在检测的特征状态的所有重要信息。
Capsules encapsulate all important information about the state of the feature they are detecting in vector form.
胶囊以矢量的形式封装了关于他们正在检测的特征的状态的所有重要信息。
胶囊将检测到特征的概率编码为其输出向量的长度。检测到的特征的状态被编码为向量指向的方向(“实例化参数, instantiation parameters”)。因此,当检测到的特征在图像周围移动或其状态发生某种变化时,概率仍然保持不变(向量的长度不变),但其方向会发生变化。
想象一下,一个胶囊检测到图像中的人脸,并输出长度为0.99的3D向量。然后我们开始在图像中移动人脸。矢量将在其空间中旋转,表示检测到的人脸的变化状态,但其长度将保持不变,因为胶囊仍然确信它检测到了人脸。这就是Hinton所说的活动等价性(activities equivariance):当一个物体“在图片中的多种可能的外观上移动”时,神经元的活动会发生变化。同时,检测的概率保持不变,这是我们应该瞄准的不变性的形式,而不是具有最大池的卷积神经网络提供的类型。
2.3 胶囊如何发挥作用?
让我们将胶囊与人工神经元进行比较。下表总结了胶囊和神经元之间的差异:
回想一下,一个神经元从其他神经元接收输入标量,然后将它们乘以标量权重和, 并求和。然后,这个和被传递给许多可能的非线性激活函数中的一个,这些函数取输入标量并根据函数输出标量。这个标量将是神经元的输出,作为其他神经元的输入。这个过程的总结可以在下面右侧的表格和图表中看到。本质上,人工神经元可以通过3个步骤来描述:
- 输入标量的标量加权
- 加权输入标量的求和
- 标量到标量的非线性变换
另一方面,胶囊具有上述三个步骤的向量形式,以及除了新的步骤、输入的仿射变换:
- 输入向量的矩阵乘法
- 输入向量的标量加权
- 加权输入向量的求和
- 向量到向量的非线性变换
2.3.1 步骤1: Matrix Multiplication of Input Vectors
我们的胶囊接收的输入向量(图中的 u ⃗ 1 、 u ⃗ 2 和 u ⃗ 3 \vec u_1、\vec u_2和 \vec u_3 u1、u2和u3)来自下面一层中的其他3个胶囊。这些向量的长度对较低级别胶囊检测到其对应对象的概率进行编码,并且向量的方向对检测到的对象的一些内部状态进行编码。让我们假设较低级别的胶囊分别检测眼睛、嘴巴和鼻子,而外层胶囊检测人脸。
然后将这些向量乘以对应的权重矩阵
W
W
W,该权重矩阵
W
W
W对较低级别特征(眼睛、嘴巴和鼻子)和较高级别特征(面部)之间的重要空间关系和其他关系进行编码。例如,矩阵
W
2
j
W_{2j}
W2j可以编码鼻子和面部之间的关系:面部以鼻子为中心,其大小是鼻子大小的10倍,其在空间中的方向对应于鼻子的方向,因为它们都位于同一平面上。对于矩阵
W
1
j
W_{1j}
W1j和
W
3
j
W_{3j}
W3j可以得出类似的直觉。在乘以这些矩阵之后,我们得到的是更高级别特征的预测位置。换句话说,$\hat u_1
表示根据检测到的眼睛位置,面部应该在哪里,
表示根据检测到的眼睛位置,面部应该在哪里,
表示根据检测到的眼睛位置,面部应该在哪里,\hat u_2$ 表示根据检测出的嘴的位置,面部应在哪里,而
u
^
3
\hat u_3
u^3表示根据检测出来的鼻子位置,面部应当在哪里。
在这一点上,你的直觉应该如下:如果这三个较低层次特征的预测指向人脸的相同位置和状态,那么它一定是一张脸。
2.3.2 步骤2: Scalar Weighting of Input Vectors
乍一看,这一步骤与人工神经元中在将输入相加之前对其进行加权非常相似。在神经元的情况下,这些权重是在反向传播过程中学习的,但在胶囊的情况中,它们是使用“动态路由”来确定的,这是一种确定每个胶囊输出去向的新方法。我将专门发表一篇关于这个算法的文章,在这里只提供一些直觉。
在上图中,我们有一个较低级别的胶囊,它需要“决定”将输出发送到哪个较高级别的胶囊。它将通过调整权重 C C C来做出决定,权重 C C C将乘以该胶囊的输出,然后将其发送到左侧或右侧更高级别的胶囊 J J J和 K K K。
现在,较高级别的胶囊已经接收到来自其他较低级别胶囊的许多输入向量。所有这些输入都用红点和蓝点表示。当这些点聚集在一起时,这意味着对较低级别胶囊的预测彼此接近。 这就是为什么,例如,在胶囊 J J J和 K K K中都有一簇红点。
那么,我们的低级别胶囊应该将输出发送到哪里:发送到太空舱 J J J还是发送到太空仓 K K K?这个问题的答案就是动态路由算法的本质。低级别胶囊的输出,当乘以相应的矩阵 W W W时,落在远离胶囊 J J J中“正确”预测的红色集群的地方。另一方面,它将非常接近右胶囊 K K K中的“真实”预测红色集群。下层胶囊具有测量哪个上层胶囊更好地适应其结果的机制,并且将自动调整其重量,使得对应于胶囊 K K K的权重 C C C将较高,而对应于胶囊 J J J的权重 C C C将会较低。
2.3.3 步骤3: Sum of Weighted Input Vectors
这个步骤类似于常规的人工神经元,代表输入的组合。我不认为这个步骤有什么特别之处(除了它是向量的和,而不是标量的和)。因此,我们可以进入下一步。
2.3.4 步骤4: “Squash”(挤压): Novel Vector-to-Vector Nonlinearity
CapsNet引入的另一项创新是新的非线性激活函数,它取一个向量,然后“压扁”它,使其长度不超过1,但不会改变其方向。
等式的右侧(蓝色矩形)缩放输入向量,使其具有单位长度,左侧(红色矩形)执行额外的缩放。请记住,输出向量长度可以解释为胶囊检测到给定特征的概率。
左边是应用于1D向量的挤压函数,该向量是标量。我把它包括在内,以展示函数有趣的非线性形状。
只有将一维的情况可视化才有意义;在实际应用中,它将获取向量并输出一个向量,这将很难可视化。
2.4 结论(Conclusion)
在这一部分中,我们讨论了胶囊是什么,它执行什么样的计算,以及它背后的直觉。我们看到胶囊的设计建立在人工神经元的设计之上,但将其扩展到向量形式,以允许更强大的表征能力。它还引入了矩阵权重来编码不同层的特征之间的重要层次关系。结果成功地实现了设计者的目标:神经元活动相对于输入变化的等方差和特征检测概率的不变性。
CapsNet系列教程剩下的唯一部分是胶囊之间的动态路由算法(dynamic routing between capsules algorithm),以及这种新型网络架构的详细演练。这些将在以下帖子中讨论。
3. 第三部分:胶囊之间的动态路由算法
3.1 引言(Introduction)
这是关于一种基于胶囊的新型神经网络(称为CapsNet)的系列文章的第三部分。我已经谈到了它背后的直觉,以及什么是胶囊以及它是如何工作的。在这篇文章中,我将讨论允许训练胶囊网络的新型动态路由算法。
源自: http://helper.ipam.ucla.edu/publications/gss2012/gss2012_10754.pdf
正如我在第二部分中所展示的,较低级别层中的胶囊
i
i
i需要决定如何将其输出向量发送到较高级别的胶囊
j
j
j。它通过改变标量权重
c
i
j
c_{ij}
cij来做出这一决定,后者将乘以其输出向量,然后被视为更高级别胶囊的输入。按符号表示,
c
i
j
c_{ij}
cij表示将来自较低级别胶囊
i
i
i的输出向量相乘并作为较高级别胶囊
j
j
j的输入的权重。
关于权重
c
i
j
c_{ij}
cij需要知道的事情:
- 每个权重都是一个非负标量。
- 对于每个较低级别的胶囊 i i i,所有权重 c i j c_{ij} cij的和等于 1 1 1。
- 对于每个低级别胶囊 i i i,权重的数量等于较高级别胶囊的数量。
- 这些权重由迭代动态路由算法确定。
前两个事实允许我们用概率术语解释权重。回想一下,胶囊的输出向量的长度被解释为该胶囊被训练来检测的特征存在的概率。输出向量的方向是特征的参数化状态。因此,在某种意义上,对于每个较低级别的胶囊 i i i,其权重 c i j c_{ij} cij定义了其输出属于每个较高级别的胶囊 j j j的概率分布。
3.2 胶囊间的动态路由
那么,在动态路由过程中到底会发生什么呢?让我们看看论文中对算法的描述。但在我们一步一步深入研究算法之前,我希望您记住算法背后的主要直觉:
Lower level capsule will send its input to the higher level capsule that “agrees” with its input. This is the essence of the dynamic routing algorithm.
较低级别的胶囊会将其输入发送到与其输入“一致”的较高级别的胶囊。这就是动态路由算法的本质。
既然我们已经考虑到了这一点,让我们一行一行地研究算法。
第1行表示,该过程采用较低级别 l l l中的所有胶囊及其输出 u ^ \hat u u^,以及路由迭代次数 r r r。最后一行告诉,该算法将生成更高级别的胶囊 v j v_j vj的输出。从本质上讲,该算法告诉我们如何计算网络的前向通过。
在第2行中,你会注意到有一个新的系数 b i j b_{ij} bij,这是我们以前从未见过的。这个系数只是一个临时值,它将被迭代更新,并且在过程结束后,它的值将被存储在 c i j c_{ij} cij中。在训练开始时, b i j b_{ij} bij的值被初始化为零。
第3行表示,4-7中的步骤将重复 r r r次(路由迭代次数)。
第4行中的步骤计算向量 c i c_i ci的值,该向量 c i c_i ci是较低级别胶囊 i i i的所有路由权重。这是针对所有较低级别的胶囊进行的。为什么选择softmax?Softmax将确保每个权重 c i j c_{ij} cij都是非负数,并且它们的和等于1。从本质上讲,softmax加强了系数 c i j c_{ij} cij的概率性质,我在上面描述了这一点。
在第一次迭代时,所有系数 c i j c_{ij} cij的值将相等,因为在第二行上,所有 b i j b_{ij} bij都被设置为零。例如,如果我们有3个较低级别的胶囊和2个较高级别的胶囊,那么所有 c i j c_{ij} cij将等于 0.5 0.5 0.5。所有 c i j c_{ij} cij在算法初始化时相等的状态表示最大混乱和不确定性的状态:较低级别的胶囊不知道哪个较高级别的胶囊最适合其输出。当然,随着过程的重复,这些均匀分布将发生变化。
在计算了所有较低级别胶囊的所有权重 c i j c_{ij} cij后,我们可以转到第5行,在那里我们观察较高级别的胶囊。该步骤计算由在前一步骤中确定的路由系数 c i j c_{ij} cij加权的输入向量的线性组合。直观地说,这意味着缩小输入向量并将它们相加,从而产生输出向量 s j s_j sj。这是针对所有更高级别的胶囊进行的。
接下来,在第6行中,来自上一步的向量通过挤压非线性,这确保了向量的方向被保留,但其长度被强制不超过1。该步骤产生所有更高级别胶囊的输出向量 v j v_j vj。
总结一下我们到目前为止所做的:步骤4-6简单地计算了更高级别胶囊的输出。第7行的步骤是权重更新发生的地方。这一步骤抓住了路由算法的精髓。该步骤查看每个更高级别的胶囊 j j j,然后检查每个输入并根据公式更新相应的权重 b i j b_{ij} bij。该公式表示,新的权重值等于旧的值加上胶囊 j j j的当前输出和较低级别胶囊 i i i对该胶囊的输入的点积。点积着眼于胶囊的输入和胶囊的输出之间的相似性。此外,请记住,从上面看,较低级别的胶囊会将其输出发送给输出相似的较高级别的胶囊。这种相似性通过点积来捕捉。在该步骤之后,算法从步骤3开始,并重复该过程 r r r次。
在 r r r次迭代之后,计算了更高级别胶囊的所有输出,并建立了路线权重。正向传递可以继续到下一级别的网络。
3.3 权重更新步骤的直观示例
在上图中,假设有两个更高级别的胶囊,它们的输出由紫色向量 v 1 v_1 v1和 v 2 v_2 v2表示,如前一节所述计算。橙色矢量表示来自较低级别胶囊之一的输入,黑色矢量表示来自其他较低级别囊的所有剩余输入。
我们看到,在左边部分,紫色的输出 v 1 v_1 v1和橙色的输入 u ^ \hat u u^指向相反的方向。换句话说,它们并不相似。这意味着它们的点积将是负数,因此路由系数 c 11 c_{11} c11将减小。在右侧部分,紫色输出 v 2 v_2 v2和橙色输入 v ^ \hat v v^指向同一方向。它们是相似的。因此,路由系数 c 12 c_{12} c12将增加。对所有更高级别的胶囊和每个胶囊的所有输入重复此程序。这样做的结果是一组路由系数,其将来自较低级别胶囊的输出与较高级别胶囊的输出来最佳匹配。
3.4 要使用多少次路由迭代?
本文研究了MNIST和CIFAR数据集的一系列值。作者的结论是双重的:
- 更多的迭代往往会使数据过拟合。
- 建议在实践中使用3次路由迭代
3.5 结论(Conclusion)
在本文中,我通过协议解释了允许训练CapsNet的动态路由算法。最重要的思想是,输入和输出之间的相似性被测量为胶囊的输入和输出的点积,然后路由系数被相应地更新。最佳实践是使用3次路由迭代。
在下一篇文章中,我将带您了解CapsNet架构,在这里我们将把迄今为止学到的所有拼图拼在一起。
4. 第四部分:胶囊网络CapsNet的架构
4.1 引言(Introduction)
在这一部分中,我将介绍CapsNet的体系结构。我还将尝试计算CapsNet中可训练参数的数量。我得出的可训练参数数约为820万,与论文中正式提到的1136万不同。这篇论文本身并不是很详细,因此它留下了一些关于网络实现细节的悬而未决的问题,这些问题至今仍未得到解答,因为作者没有提供他们的代码。尽管如此,我仍然认为,出于纯粹的学习目的,计算网络中的参数是一个很好的练习,因为它可以让人们练习理解特定架构的所有构建块。
CapsNet由两部分组成:编码器和解码器。前3层是编码器,后3层是解码器:
- 第1层: 卷积层
- 第2层:PrimaryCaps图
- 第3层:DigitCaps图
- 第4层:完全连接#1
- 第5层:完全连接#2
- 第6层:完全连接#3
4.2 第一部分:编码器(Encoder)
网络的编码器部分将 28 × 28 28\times 28 28×28 的MNIST数字图像作为输入,并学习将其编码为实例化参数的16维向量(如本系列前几篇文章所述),这就是胶囊的工作所在。预测期间网络的输出是DigitCaps输出长度的10维向量。解码器有3层:其中两层是卷积的,最后一层是完全连接的。
4.2.1 第1层:卷积层
- Input: 28x28 image (one color channel).
- Output: 20x20x256 tensor.
- Number of parameters: 20992.
卷积层的工作是检测2D图像中的基本特征。在CapsNet中,卷积层有 256 256 256个内核,大小为9x9x1,步长为1,然后是ReLU激活。如果你不知道这意味着什么,这里有一些很棒的资源,可以让你快速了解卷积背后的关键思想。为了计算参数的数量,我们还需要记住,卷积层中的每个内核都有1个偏置项。因此,该层总共具有(9x9+1)x256=20992个可训练参数。
4.2.2 第2层:初级胶囊层
- Input: 20x20x256 tensor.
- Output: 6x6x8x32 tensor.
- Number of parameters: 5308672.
该层有32个初级胶囊,其工作是获取卷积层检测到的基本特征并产生特征的组合。该层有32个“初级胶囊”,其性质与卷积层非常相似。每个胶囊将八个9x9x256卷积核(步长为2)应用于20x20x256输入体积,因此产生6x6x8输出张量。由于有32个这样的胶囊,因此输出体积的形状为6x6x32。通过与上一层中的计算类似的计算,我们在该层中获得了5308672个可训练参数。
4.2.3 第3层:DigitCaps层
- Input: 6x6x8x32 tensor.
- Output: 16x10 matrix.
- Number of parameters: 1497600.
这一层有10个数字胶囊,每个数字一个。每个胶囊将一个6x6x8x32张量作为输入。你可以把它想象成6x6x32个8维向量,总共是1152个输入向量。根据胶囊的内部工作原理(如本文所述),这些输入向量中的每一个都获得自己的8x16权重矩阵,该矩阵将8维输入空间映射到16维胶囊输出空间。因此,对于每个胶囊有1152个矩阵,并且在动态路由中使用1152个c系数和1152个b系数。相乘:1152 x 8 x 16+1152+1152,我们得到每个胶囊149760个可训练参数,然后乘以10,得到该层的最终参数数。
4.2.4 第4层:损失函数
损失函数乍一看可能很复杂,但事实并非如此。它与SVM损失函数非常相似。为了理解其工作原理,请记住DigitCaps层的输出是10个十六维向量。在训练期间,对于每个训练示例,将根据以下公式为10个向量中的每一个计算一个损失值,然后将这10个值相加以计算最终损失。因为我们谈论的是监督学习,所以每个训练示例都会有正确的标签,在这种情况下,它将是一个在正确位置有9个零和1个一的独热编码向量。在损失函数公式中,正确的标签决定了 T c T_c Tc的值:如果正确的标签与该特定DigitCap的数字对应,则为1,否则为0。
对于与正确标签不匹配的DigitCaps,
T
c
T_c
Tc将为零,因此将评估第二项(对应于
(
1
—
T
c
)
(1 — T_c)
(1 — Tc)部分)。在这种情况下,我们可以看到,如果不匹配的DigitCap预测概率小于0.1的错误标签,则损失将为零,如果它预测概率大于0.1的错误标记,则损失为非零。
最后,在公式中,lambda系数被包括在内,用于训练期间的数值稳定性(其值固定为0.5)。公式中的两项具有平方,因为该损失函数具有
L
2
L_2
L2范数,作者显然认为该范数更有效。
4.3 第二部分:解码(Decoder)
解码器从正确的DigitCap中提取16维向量,并学习将其解码为数字的图像(请注意,它在训练期间只使用正确的DigitCap向量,而忽略不正确的向量)。解码器被用作正则化器,它将正确的DigitCap的输出作为输入,并学习重建一个28X28像素的图像,损失函数是重建图像和输入图像之间的欧几里得距离。解码器强制胶囊学习对重建原始图像有用的特征。重建的图像与输入图像越接近越好。在下面的图像中可以看到重建图像的示例。
4.3.1 第4层:全连接层#1
- Input: 16x10.
- Output: 512.
- Number of parameters: 82432.
较低级别的每个输出被加权并被引导到完全连接层的每个神经元作为输入。每个神经元也有一个偏置项。对于该层,有16x10个输入,它们都指向该层的512个神经元中的每一个。因此,有(16x10+1)x512个可训练参数。
对于以下两层,计算是相同的:参数数量=(输入数量+偏置)x层中神经元的数量。这就是为什么没有对完全连接的层2和3进行解释的原因。
4.3.2 第5层:全连接层#2
- Input: 512.
- Output: 1024.
- Number of parameters: 525312.
4.3.3 第6层:全连接层#3
- Input: 1024.
- Output: 784 (which after reshaping gives back a 28x28 decoded image).
- Number of parameters: 803600.
Total number of parameters in the network: 8238608.
4.4 结论(Conclusion)
这就结束了CapsNet上的系列。互联网上有很多非常好的资源。如果你想了解更多关于这个迷人话题的信息,请看看这个关于CapsNets的精彩汇编: https://github.com/sekwiatkowski/awesome-capsule-networks。
5. 第五部分:Capsule Network的实现
5.1 定义相关函数和类
导入所需的python包:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from torch.optim import Adam
from torchvision import datasets, transforms
USE_CUDA = True
定义字符数字识别数据集类型Mnist, 读取数据:
class Mnist:
def __init__(self, batch_size):
dataset_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST('../data', train=True, download=True, transform=dataset_transform)
test_dataset = datasets.MNIST('../data', train=False, download=True, transform=dataset_transform)
self.train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
self.test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
定义卷积层类型ConvLayer(nn.Module):
class ConvLayer(nn.Module):
def __init__(self, in_channels=1, out_channels=256, kernel_size=9):
super(ConvLayer, self).__init__()
self.conv = nn.Conv2d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=1
)
def forward(self, x):
return F.relu(self.conv(x))
定义初级胶囊层PrimaryCaps(nn.Module):
class PrimaryCaps(nn.Module):
def __init__(self, num_capsules=8, in_channels=256, out_channels=32, kernel_size=9):
super(PrimaryCaps, self).__init__()
self.capsules = nn.ModuleList([
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=2, padding=0)
for _ in range(num_capsules)])
def forward(self, x):
u = [capsule(x) for capsule in self.capsules]
u = torch.stack(u, dim=1)
u = u.view(x.size(0), 32 * 6 * 6, -1)
return self.squash(u)
def squash(self, input_tensor):
squared_norm = (input_tensor ** 2).sum(-1, keepdim=True)
output_tensor = squared_norm * input_tensor / ((1. + squared_norm) * torch.sqrt(squared_norm))
return output_tensor
定义高级胶囊层DigitCaps:
class DigitCaps(nn.Module):
def __init__(self, num_capsules=10, num_routes=32 * 6 * 6, in_channels=8, out_channels=16):
super(DigitCaps, self).__init__()
self.in_channels = in_channels
self.num_routes = num_routes
self.num_capsules = num_capsules
self.W = nn.Parameter(torch.randn(1, num_routes, num_capsules, out_channels, in_channels))
def forward(self, x):
batch_size = x.size(0)
x = torch.stack([x] * self.num_capsules, dim=2).unsqueeze(4)
W = torch.cat([self.W] * batch_size, dim=0)
u_hat = torch.matmul(W, x)
b_ij = Variable(torch.zeros(1, self.num_routes, self.num_capsules, 1))
if USE_CUDA:
b_ij = b_ij.cuda()
num_iterations = 3
for iteration in range(num_iterations):
c_ij = F.softmax(b_ij)
c_ij = torch.cat([c_ij] * batch_size, dim=0).unsqueeze(4)
s_j = (c_ij * u_hat).sum(dim=1, keepdim=True)
v_j = self.squash(s_j)
if iteration < num_iterations - 1:
a_ij = torch.matmul(u_hat.transpose(3, 4), torch.cat([v_j] * self.num_routes, dim=1))
b_ij = b_ij + a_ij.squeeze(4).mean(dim=0, keepdim=True)
return v_j.squeeze(1)
def squash(self, input_tensor):
squared_norm = (input_tensor ** 2).sum(-1, keepdim=True)
output_tensor = squared_norm * input_tensor / ((1. + squared_norm) * torch.sqrt(squared_norm))
return output_tensor
定义解码器Decoder:
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.reconstraction_layers = nn.Sequential(
nn.Linear(16 * 10, 512),
nn.ReLU(inplace=True),
nn.Linear(512, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, 784),
nn.Sigmoid()
)
def forward(self, x, data):
classes = torch.sqrt((x ** 2).sum(2))
classes = F.softmax(classes)
_, max_length_indices = classes.max(dim=1)
masked = Variable(torch.sparse.torch.eye(10))
if USE_CUDA:
masked = masked.cuda()
masked = masked.index_select(dim=0, index=max_length_indices.squeeze(1).data)
reconstructions = self.reconstraction_layers((x * masked[:, :, None, None]).view(x.size(0), -1))
reconstructions = reconstructions.view(-1, 1, 28, 28)
return reconstructions, masked
定义胶囊网络CapsNet(nn.Module):
class CapsNet(nn.Module):
def __init__(self):
super(CapsNet, self).__init__()
self.conv_layer = ConvLayer()
self.primary_capsules = PrimaryCaps()
self.digit_capsules = DigitCaps()
self.decoder = Decoder()
self.mse_loss = nn.MSELoss()
def forward(self, data):
output = self.digit_capsules(self.primary_capsules(self.conv_layer(data)))
reconstructions, masked = self.decoder(output, data)
return output, reconstructions, masked
def loss(self, data, x, target, reconstructions):
return self.margin_loss(x, target) + self.reconstruction_loss(data, reconstructions)
def margin_loss(self, x, labels, size_average=True):
batch_size = x.size(0)
v_c = torch.sqrt((x**2).sum(dim=2, keepdim=True))
left = F.relu(0.9 - v_c).view(batch_size, -1)
right = F.relu(v_c - 0.1).view(batch_size, -1)
loss = labels * left + 0.5 * (1.0 - labels) * right
loss = loss.sum(dim=1).mean()
return loss
def reconstruction_loss(self, data, reconstructions):
loss = self.mse_loss(reconstructions.view(reconstructions.size(0), -1), data.view(reconstructions.size(0), -1))
return loss * 0.0005
5.2 执行胶囊网络模型
实例化胶囊网络模型:
capsule_net = CapsNet()
if USE_CUDA:
capsule_net = capsule_net.cuda()
optimizer = Adam(capsule_net.parameters())
执行胶囊网络模型:
batch_size = 100
mnist = Mnist(batch_size)
n_epochs = 30
for epoch in range(n_epochs):
capsule_net.train()
train_loss = 0
for batch_id, (data, target) in enumerate(mnist.train_loader):
target = torch.sparse.torch.eye(10).index_select(dim=0, index=target)
data, target = Variable(data), Variable(target)
if USE_CUDA:
data, target = data.cuda(), target.cuda()
optimizer.zero_grad()
output, reconstructions, masked = capsule_net(data)
loss = capsule_net.loss(data, output, target, reconstructions)
loss.backward()
optimizer.step()
train_loss += loss.data[0]
if batch_id % 100 == 0:
print "train accuracy:", sum(np.argmax(masked.data.cpu().numpy(), 1) ==
np.argmax(target.data.cpu().numpy(), 1)) / float(batch_size)
print train_loss / len(mnist.train_loader)
capsule_net.eval()
test_loss = 0
for batch_id, (data, target) in enumerate(mnist.test_loader):
target = torch.sparse.torch.eye(10).index_select(dim=0, index=target)
data, target = Variable(data), Variable(target)
if USE_CUDA:
data, target = data.cuda(), target.cuda()
output, reconstructions, masked = capsule_net(data)
loss = capsule_net.loss(data, output, target, reconstructions)
test_loss += loss.data[0]
if batch_id % 100 == 0:
print "test accuracy:", sum(np.argmax(masked.data.cpu().numpy(), 1) ==
np.argmax(target.data.cpu().numpy(), 1)) / float(batch_size)
print test_loss / len(mnist.test_loader)
结果为:
train accuracy: 0.12
train accuracy: 0.9
train accuracy: 0.94
train accuracy: 0.96
train accuracy: 0.99
train accuracy: 0.96
0.229411779922
test accuracy: 0.96
0.0547490972094
train accuracy: 0.98
train accuracy: 0.98
train accuracy: 0.99
train accuracy: 0.99
train accuracy: 1.0
train accuracy: 0.99
0.0456192491871
test accuracy: 0.98
0.0390225026663
train accuracy: 0.99
train accuracy: 0.99
train accuracy: 1.0
5.3 结果的可视化
import matplotlib
import matplotlib.pyplot as plt
def plot_images_separately(images):
"Plot the six MNIST images separately."
fig = plt.figure()
for j in xrange(1, 7):
ax = fig.add_subplot(1, 6, j)
ax.matshow(images[j-1], cmap = matplotlib.cm.binary)
plt.xticks(np.array([]))
plt.yticks(np.array([]))
plt.show()
检查原始图片:
plot_images_separately(data[:6,0].data.cpu().numpy())
检查重构的图片:
plot_images_separately(reconstructions[:6,0].data.cpu().numpy())