赞
踩
斯蒂文认为机器学习有时候像婴儿学习,特别是在物体识别上。比如婴儿首先学会识别边界和颜色,然后将这些信息用于识别形状和图形等更复杂的实体。比如在人脸识别上,他们学会从眼睛和嘴巴开始识别最终到整个面孔。当他们看一个人的形象时,他们大脑认出了两只眼睛,一只鼻子和一只嘴巴,当认出所有这些存在于脸上的实体,并且觉得“这看起来像一个人”。
斯蒂文首先给他的女儿悠悠看了以下图片,看她是否能自己学会认识图中的人(金·卡戴珊)。
斯蒂文接下来用几张图来考她:
悠悠
图中有两只眼睛一个鼻子一张嘴巴,图中的物体是个人。
斯蒂文
正确!
悠悠
图中有两只眼睛一个鼻子一张嘴巴,图中的物体是个人。
斯蒂文
错误!嘴巴长到眼睛上还是个人吗?
悠悠
图中有一大块都是黑色的,图中的物体好像是头发。
斯蒂文
错误!这只是把第一张图颠倒一下,怎么就变成头发了?
斯蒂文很失望,觉得她第二、三张都应该答对,但是他对悠悠要求太高了,要知道现在深度学习里流行的卷积神经网络 (convolutional neural network, CNN) 给出的答案也和悠悠一样,如下:
第一张 CNN 给出的答案是人,概率为 0.88,正确;第二张 CNN 给出的答案也是人,概率为 0.90 ,开玩笑在?第三张 CNN 给出的答案是黑发,概率为 0.79 ,呵呵,和悠悠一样天真。
CNN 弄错的两张图也是因为它的两个缺陷:
Convolutional neural networks are doomed. -- Hinton
大神 Hinton 如此说道“卷积神经网络要完蛋了”,因此他前不久也提出了一个 Capsule 的东西,直译成胶囊。但是这个翻译丢失了很多重要的东西,个人认为叫做向量神经元 (vector neuron) 甚至张量神经元 (tensor neuron) 更贴切。正式介绍 Capsule 的这篇文章在 2017 年 11 月 7 日才出来,论文名字叫《Dynamic Routing Between Capsules》,有兴趣的同学跟我走一遭吧。
第一章 - 前戏王
1.1 物体姿态
1.2 不变性和同变性
1.3 全连接层
1.4 卷积神经网络
第二章 - 理论皇
2.1 胶囊定义
2.2 神经元类比
2.3 工作原理
2.4 动态路由
2.5 网络结构
第三章 - 实践狼
3.1 帆船房子
3.2 代码解析
总结和下帖预告
1
前戏王
1.1
物体姿态
为了正确的分类和识别物体,保持物体部分之间的分层姿态 (hierarchical pose) 关系是很重要的。姿态主要包括平移 (translation)、旋转 (rotation) 和放缩 (scale) 三种形式。
在拍摄人物时,我们调动照相机的角度从 3D 的人生成 2D 的照片。照出来的人物照角度多种多样,但人是个整体 (脸和身体对于人的相对位置不会变)。因此我们不想定义相对于相机的所有对象 (脸和身体),而将它们定义一个相对稳定的坐标系 (coordinate frame) 中,然后仅仅通过转动相机来照出不同角度的照片。
在创建这些图形时,我们首先会定义脸和身体相对于人的位置,更进一层,我们会定义眼睛和嘴巴对于相对于脸的位置,但不是相对于人的位置。因为之前已经有了脸相对于人的位置,现在又有了眼睛相对于脸的位置,那么也有了眼睛相对于人的位置。本质上,你将有层次的创建一个完整的人,而所需要的数学工具就是姿态矩阵 (pose matrix),这个矩阵定义所有对象相对于照相机的视点 (viewpoint),并且还表示了部件与整体之间的关系。
In order to correctly do classification and object recognition, it is important to preserve hierarchical pose relationships between object parts. -- Hinton
Hinton 认为,为了正确地进行分类和对象识别,重要的是保持对象部分之间的分层姿态关系。后面讲到的 Capsule 就符合这个重要直觉,它结合了对象之间的相对关系,并以姿态矩阵来表示。
首先我们看看 2 维平面中姿态矩阵是如何平移、旋转和放缩物体:
用 R, T, S 定义旋转、平移和缩放矩阵,那么将 (x, y) 先逆时针转 30 度,再向右平移 2 个单位,最后缩放 50% 到 (x', y') 可以由下列矩阵连乘得到
在 2 维平面中,我们加了 1 个维度 z,是为了方便完成平移操作。写出 2 维平面姿态矩阵 M 的一般形式,并延伸并类比到 3 维空间的姿态矩阵,表示如下:
下面看个具体例子:
整体是由它的各个部分组成的,如上图:
每个部分通过一个姿态矩阵与其主体相关联。如果 M 是脸对人姿态矩阵,N 是嘴巴对脸姿态矩阵,那么嘴巴对人的姿态矩阵为 N' = MN。
现在我们有一个照相机,并且我们知道人对相机的帧的姿态矩阵是 P,可以通过连乘姿态矩阵来提取人的每个部分的所有基本属性,比如:
姿态矩阵 P 表示我们可以从相机看对象的不同视点。一张脸上所有特征都是一样的,所有不同的是你看脸的角度。所有其他对象 (比如身体、嘴巴和手) 的所有视点都可以由 P 得到。
现在告诉你左眼的位置,你可以想象脸的位置了吧。同理,你以为可以从嘴的位置估计脸的位置。如果由左眼和嘴的位置推出脸的位置相符,数学上表示为 Ev·E = Mv·M,其中
还记得引言中正常的卡戴珊的图像 (左图) 吗?从嘴和左眼推出脸的位置是相似的,因此得出结论它们属于同一个脸。
但是对于非正常的卡戴珊的图像 (下左图)
从嘴和左眼的位置出发得到的结论似乎不相符 (disagreement),因此它们不应该被认为出现在同一张脸上。只有当嘴和左眼处在正确的位置,从它们出发得到的结论才会相符 (agreement)。在这种情况下,我们就会发现嘴巴应该在两只眼睛的下面的中间,只有这样放置的眼睛和嘴巴才是脸部的一部分,而不是仅仅靠一张嘴巴和眼睛来识别脸部。
1.2
不变性和共变性
广义上讲,不变性 (invariance) 是表示 (representation) 不随变换 (transformation) 变化;而同变性(equivariance) 是表示的变换等价于变换的表示。
从计算机视觉角度上讲,不变性指不随一些变换来识别一个物体,具体变换包括平移 (translation),旋转 (rotation),视角 (viewpoint),放缩 (scale) 等,如下图所示:
不变性通常在物体识别上是好事,因为不管雕像怎么平移、2D旋转、3D旋转和放缩,我们都可以识别出它是雕像。
如果我们的任务比物体识别稍微困难一点,比如我想知道雕像平移了多少个单位,旋转了多少度,放缩了百分之多少,那么不变性远远不够,这时需要的是同变性。
下图给出不变性和同变性的具体例子
对平移和旋转的不变性,其实是丢弃了“坐标框架”,而同变性不会丢失这些信息,它只是对内容的一种变换。具体来讲:
1.3
全连接层
在人工神经网络一贴讲的神经网络每层都是全连接的,也就是说上一层每一个神经元都连接到下一层每一个神经元,如下图所示:
除了偏置项,每层的每一个神经元都连着近邻层的所有神经元,以这种连接关系的层就叫做全连接层 (fully connected layer, FC layer),后文简称 FC 层。
如果一个神经网络每一层都是全连接的,那么它称作全连接神经网络 (fully connected neural network, FCNN),这种 FCNN 不能太深,要不然参数太多,训练速度太慢。在图像识别中,数据是高像素彩色照片,它的维度是 324×324×3,第一个 324 代表高,第二个 324 代表宽,最后的 3 代表 RGB 三个颜色维度,乘起来已经有 314928 个元素了,如果隐藏层有 1024 个神经元,那么总共有 314928×1024 = 3 亿多个参数 (假设忽略偏置项)。这还是一层,如果弄个十多层,那么训练这么多参数显然不现实,因此在图像识别中用的是卷积神经网络,它有稀疏连接 (sparse connection) 和参数共享 (parameter sharing) 等特性,会大大减少需要训练的参数。
1.4
卷积神经网络
卷积神经网络 (convolutional neural network,CNN) 的一个例子如下图。
想象给了这张车的图片,在黑天里你看不到是辆车,你只能用手电筒一点一点扫过,把每次扫过看到的东西投影到下一层,以此类推。比如第一层你看到一些横线竖线斜线,第二层组合成一些圆形方形,第三层组合成轮子车门车身,第四层组合成一辆车。这样就能用个手电筒在黑天里辨别出照片里有辆车了。
上面的例子虽然不严谨,但是听起来很直观,接下来给出 CNN 里面的一些定义。
千言万语不如两幅动图 (蓝色是输入图片的像素,绿色是滤波器扫过图片之后的卷积值):
第一幅动图将一个 5x5 的图像馈送到 3x3 的滤波器。其步长为 2 (滤波器每2格滑动),没用填充 (最外层没有虚线格),结果产生一个2x2 的图像。
第二幅动图也将一个 5x5 的图像馈送到 3x3 的滤波器。其步长为 1 (滤波器每1格滑动),用了 1 层填充(最外层只有一格虚线格),结果产生一个 5x5 的图像 (加填充可使得输出和输入图像大小不变)。
如果用 nI代表输入图像的大小,f 代表滤波器的大小,s 代表步长,p 代表填充层数,nO 代表输入图像的大小,那么有 (公式很简单就不推导了,大家可以试试上面两个例子)
把具体数字带进来,大家再捋一遍上面的卷积、滤波器、步长和填充的概念:
输出右下角的 1 是这样卷积来的:
0x1 + 1x1 + 0x0
+ 1x0 + 0x0 + 0x1
+ 0x0 + 0x0 + 1x0 = 1
除了上面定义之外,CNN 还有个很重要的概念叫做池化 (pooling)。它的作用是逐渐降低数据体的空间尺寸,这样的话能减少网络中参数的数量,使得计算资源消耗变少,也能有效的控制过拟合。通常池化使用 max 操作,比如使用尺寸 2x2 的滤波器,以步长为 2 对输入数据进行降采样,从 2x2 个数字中取最大值。字不如图,上图大家慢慢理会:
虽然池化这项技术在 CNN 上用的非常好,但是 Hinton 有话要说
The pooling operation used in convolutional neuralnetworks is a big mistake and the fact that it works so well is a disaster. -- Hinton
Hinton 认为池化在 CNN 的好效果是个大错误甚至灾难。因为池化会导致重要的信息丢失,如果它是两层之间的信使,它告诉第二层的是“我们看到左上角有一个最大值 2,右上角有一个最大值 4”,但不知道这个 2 和 4 是从第一层哪里来的。在引言的例子中,我们知道“两只眼睛一个鼻子一张嘴巴”并不代表“一张脸”,要确认是张脸,我们还需要知道这些器官之间的相互位置,比如眼睛要在鼻子上方,鼻子要在嘴巴上方,那么才可能是张脸。
2
理论皇
2.1
胶囊定义
胶囊 (Capsule) 是一个包含多个神经元的载体,每个神经元表示了图像中出现的特定实体的各种属性。这些属性可以包括许多不同类型的实例化参数 (instantiation parameter),例如姿态 (位置、大小、方向),变形,速度,色相,纹理等。胶囊里一个非常特殊的属性是图像中某个类别的实例的存在。它的输出数值大小就是实体存在的概率。
数学上常说的向量是一个有方向和长度的概念,把胶囊类比于数学向量,它也有所谓的“长度”和“方向”。假设一个胶囊代表卡戴珊的眼睛,戏称“卡戴珊眼睛胶囊”,那么其
两者类比图如下:
现在大家看胶囊的概念可能还是一头雾水,我确保你越看到后面思路越清晰,尤其要看小节 3.1。
2.2
神经元类比
为了用词严谨和类比方便,我们将 Capsule 称作向量神经元 (vector neuron, VN),而普通的人工神经元叫做标量神经元 (scalar neuron, SN),下表总结了 VN 和 SN 之间的差异:
上表中 VN 里的操作不懂不要紧,接下来会一一详述,本节只是想从高层面上区分 VN 和 SN 的区别,因此大家比较熟悉 SN,从对 SN 的性质理解再慢慢过渡到对 VN 的理解。
回想一下人工神经网络一贴,SN 从其他神经元接收输入标量,然后乘以标量权重再求和,然后将这个总和传递给某个非线性激活函数 (比如 sigmoid, tanh, Relu),生出一个输出标量。该标量将作为下一层的输入变量。实质上,SN 可以用以下三个步骤来描述:
VN 的步骤在 SN 的三个步骤前加一步:
VN 和 SN 的过程总结如下图所示:
下一节来仔细研究 VN 的四步工作原理。
2.3
工作原理
为了使问题具体化,假设:
第一步:矩阵转化
公式
根据小节 1.1 介绍的姿态矩阵可知
现在,直觉应该是这样的:如果这三个低层特征 (眼睛,鼻子和嘴) 的预测指向相同的脸的位置和状态,那么出现在那个地方的必定是一张脸。如下图所示:
上左图预测出脸,因为红蓝黄绿圈非常吻合;而上右图没有没有预测出脸,因为红蓝黄绿圈相差甚远。
第二步:输入加权
公式
乍一看,这个步骤和标量神经元 SN 的加权形式有点类似。在 SN 的情况下,这些权重是通过反向传播 (backward propagation) 确定的,但是在 VN 的情况下,这些权重是使用动态路由 (dynamic routing) 确定的,具体算法见小节 2.4。本节只从高层面来解释动态路由,如下图:
在上图中,我们有一个较低级别 VNi需要“决定”它将发送输出给哪个更高级别 VN1和 VN2。它通过调整权重 ci1和 ci2来做出决定。
现在,高级别 VN1和 VN2已经接收到来自其他低级别 VN 的许多输入向量,所有这些输入都以红点和蓝点表示。
那么,低别级 VNi应该输出到高级别 VN1还是 VN2?这个问题的答案就是动态路由的本质。由上图看出
而动态路由会根据以上结果产生一种机制,来自动调整其权重,即调高 VN2相对的权重 ci2,而调低 VN1相对的权重 ci1。
第三步:加权求和
公式
这一步类似于普通的神经元的加权求和步骤,除了总和是向量而不是标量。加权求和的真正含义就是计算出第二步里面讲的红色簇心 (cluster centroid)。
第四步:非线性激活
公式
这个公式的确是 VN 的一个创新,采用向量的新型非线性激活函数,又叫 squash 函数,姑且翻译成“压缩”函数。这个函数主要功能是使得 vj 的长度不超过 1,而且保持 vj和 sj同方向。
这样一来,输出向量 vj的长度是在 0 和 1 之间的一个数,因此该长度可以解释为 VN 具有给定特征的概率。
2.4
动态路由
在小节 2.3 的第二步已经讲过,低级别 VNi 需要决定如何将其输出向量发送到高级别 VNj,它是通过改变权重 cij而实现的。首先来看看 cij的性质:
前两个性质说明 c 符合概率概念。回想一下小节 2.1,VN 的长度被解释为它的存在概率。VN 的方向是其特征的参数化状态。因此,对于每个低级别 VNi,其权重 cij定义了属于每个高级别 VNj 的输出的概率分布。
一言以蔽之,低级别 VN 会将其输出发送到“同意”该输出的某个高级别 VN。这是动态路由算法的本质。很绕口是吧?分析完 Hinton 论文中的动态路由算法就懂了,见截图:
算法字面解释如下:
算法逻辑解释如下:
下面两幅图帮助进一步理解第 7 行的含义,第一幅讲的是点积,论文中用点积来度量两个向量的相似性,当然还有很多别的度量方式。
第二幅讲的是更新权重,此消彼长。
2.5
网络结构
本章的前四节已经讲明 Capsule 的工作原理和动态路由的逻辑。本节以 MNIST 数据集为例,来阐明向量神经网络 (capsule network, CapsNet) 的结构和工作原理。
MNIST 全称为 Modified National Institute of Standards and Technology,其中训练集由来自 250 个不同人手写的数字构成,其中 50% 是高中学生,50% 来自人口普查局的工作人员,总共 60000 个数字;而测试集也是同样比例的手写数字数据,总共 10000 个数字。每幅图像为一个 28x28 像素的单元,下图给出 MNIST 里面 0-9 的一些示例。
CapsNet 的输入输出和 CNN 是一样的:
但 CapsNet 和业界最先进的 CNN 相比,是一个非常浅的网络,中间只有两个卷积 (Conv) 层 (见小节1.4) 和一个全连接 (FC) 层 (见小节1.3),如下图所示:
图像输入到低级特征 (Conv1)
这一步就是一个常规的卷积操作,用了 256 个 stride 为 1 的 9x9 的 filter,得到一个 20x20x256 的输出。按照原文的意思,这一步主要作用就是对图像像素做一次局部特征检测。让我们 Conv1 层的维度是如何得到的。
但为什么不一开始就用 Capsule 呢?因为 Capsule 是用来表征某个物体的“实例”,因此它更适合于表征高级的实例。如果直接用 Capsule 吸取图片的低级特征内容,不是很理想,而 CNN 却擅长抽取低级特征,因此一开始用 CNN 是合理的。
低级特征到 Primary Capsule (Conv2)
Conv2 层才是开始含有 Capsule。如果按照普通 CNN 里面的做法,用了 32 个 stride 为 2 的 9x9x256 的 filter,也只能得到 6x6x32 的输出,算法如下:
但是从上图和 Hinton 的论文发现,Conv2 层的维度是 6x6x8x32。这个 8怎么来的?它又代表着什么含义?个人理解是用 32 个 stride 为 2 的 9x9x256 的filter做了 8次卷积操作,而且
Conv2 层的输出在论文中称为 Primary Capsule,简称 PrimaryCaps,主要储存低级别特征的向量。
Primary Capsule 到 Digit Capsule (FC)
下一层就是存储高级别特征的向量,在本例中就是数字,FC 层的输出在论文中称为 Digit Capsule,简称 DigitCaps。PrimaryCaps 和 DigitCaps 是全连接的,但不是像传统神经网络标量和标量相连,而是向量与向量相连。
PrimaryCaps 里面有 6x6x32 元素,每个元素是一个 1x8的向量,而 DigitCaps 有 10 个元素 (因为有 10 个数字),每个元素是一个 1x16的向量。为了让 1x8向量与 1x16向量全连接,需要 6x6x32 个 8x16的矩阵 (姿态矩阵还记得吗)。
现在 PrimaryCaps 有 6x6x32 = 1152 个 VN,而 DigitCaps 有 10 个 VN,那么 I= 1,2, …, 1152, j = 0,1, …, 9。再用小节 2.4 讲的动态路由算法迭代 3 次计算 cij并输出 10 个 vj。
Digit Capsule 到最终输出
根据 Capsule 定义,它的长度表示其表征的内容出现的概率,所以做分类时取输出向量的 L2 范数 (也就是长度) 即可。需要注意的是,最后 Capsule 输出的概率总和并不等于 1,也就是 Capsule 有同时识别多个物体的能力。
损失函数
由于 Capsule 允许多个分类同时存在,所以不能直接用传统的交叉熵 (cross-entropy) 损失,一种替代方案是用间隔损失 (margin loss)
其中
总的损失是各个样例损失之和。论文中 m+= 0.9, m-= 0.1, λ = 0.5,用大白话说就是
重构表示
鲁棒性强的模型一定有重构的能力。如果模型能够重构,证明它至少有了一个好的表示,并且从重构结果中可以看出模型存在的问题。
重构的时候,我们单独取出 (上图橘色) 需要重构的向量,扔到后面的 3 层全连接网络中重构。注意最终输出的维度是 784 = 28×28,正好是最初图像输入的维度。
重构损失 (reconstruction loss) 就是把最终输出和最初输入的 784 个单元上的像素值相减并平方求和。总体损失 (total loss) 就是
总体损失 = 间隔损失 + α·重构损失
其中 α = 0.005,间隔损失还是占主导地位。
3
实践狼
3.1
帆船房子
本节用一个具体的“三角形长方形组成帆船房子”的例子来直观解释第 2 章的理论知识和重要概念,假设“低层 Capsule”里面有三角形和长方形,而“高层 Capsule”里面有帆船和房子。为了解释方便,定义:
正向作图和反向作图
如上图所示,计算机作图 (computer graphics) ,通常认为是正向作图,是根据各个物体的参数,比如中心横坐标 x,中心纵坐标 y 和旋转角度,在屏幕中打出 (rendering) 帆船的图像。而反向作图 (inverse graphics) 是根据屏幕中帆船的图像,反推出各个物体的参数。
想知道上图三角形的 -65 度和长方形的 16 度怎么来的,见下图解释。
向量神经元做的事就是反向作图。
向量神经元性质
假设蓝箭头代表三角形,黑箭头代表长方形:
由上图左边明显看出
同变性
CNN 的池化只能带来“不变性 (invariance)”,只能识别下面两图中都有帆船,但我们不想只追求识别率,我们想要的更多,比如 VN 带来的“同变性 (equivariance)”,不但能识别两图中有帆船,还能看出它们的倾斜度不同。
从上图看出,当帆船旋转了一些角度,它包含的三角形和长方形也旋转了一些角度,而对应的蓝箭头和黑箭头也旋转了一些角度。三角形和长方形是低层物体,帆船是高层物体,物体与物体之间是有层次 (hierarchy) 的。当高层物体转动时,它包含的所有低层物体也随之转动。
物体层次
三角形和长方形可以组成帆船,也可以组成房子。
如果把帆船和房子当成一个整体的话 (忽略其组成成分三角形和长方形),那么它们也有自己的 x-y 坐标和角度,如图所示,帆船沿顺时针方向旋转了 16 度,房子沿逆时针方向旋转了 5 度。
现在问题是,如果我们识别出图片上有三角形和长方形,那么它们组合的是房子还是帆船?
预测物体
下图勾画出由“低层 VN 代表的三角形和长方形”来预测“高层 VN 代表的房子和帆船”的来龙去脉。
浅谈路由
路由 (routing) 就是通过互联网络把信息从源地址传输到目的地址的活动,而这里路由指的是通过神经网络把信息从低层 VN 传输到高层 VN 的活动。
“三角形和长方形的 VN”路由出来的“帆船 VN”看起来非常相似,而它们路由出来的“房子 VN”看来一点也不像。因此我们有信心的认为图像里存在就是一艘帆船而不是一栋房子。
动态路由
动态路由 (dynamic routing) 是找到每一个“低层 VN”的输出最有可能贡献给哪个“高层 VN”。具体到我们的实例,就是找到“三角形或长方形”最有可能组成“房子或帆船”。
用 i 代表低层 VN 中长方形或三角形的索引 (本例中 i = 1, 2),用 j 代表高层 VN 中房子或帆船的索引 (本例中 j = 1, 2),定义
cij是 bij做 softmax 之后的结果,因此初始值 0.5 (j 层只有 2 个 VN)。
为了达到以上目的,动态路由在每个回合都干了“归一、预测、加总、压缩和更新”这五件事,然后重复若干回合:
对长方形 (i=1) 和三角形 (i=2)
其中归一函数是 softmax 函数,压缩函数是 squash 函数,相似度函数是 dot product。下面接着用实例来解释上述步骤。
初始化概率和参数:
初始化所有 b 为零,根据 softmax 函数计算出所有 c 都是 0.5。该初始化是符合直觉的,一开始“三角形或长方形到底是帆船还是房子的一部分”这样一个判断是最不确定的,而 50% 的概率对应着这种最不确定情景。
预测-加总-压缩:
预测就是用姿态矩阵做了转化 (见小节 1.1),分别由长方形和三角形的位置预测了房子/帆船的位置:
加总就是分别将房子/帆船的预测位置求个加权总和,可以理解成房子/帆船的平均位置:
压缩就是单位化位置向量:
更新参数:
参数 b 就是从三角形/长方形推出房子/帆船的可能性,如图:
上图已解释的很清楚,核心思想就是
最后用 softmax 更新概率 cij
重复以上预测-加总-压缩的步骤,循环 r 次结束。
最后用以下规则来判断到底从低层 VN 路由到高层 VN:
3.2
代码解析
基本引入包和设置
# Import useful packages
importnumpy asnp
importtensorflow astf
%matplotlib inline
importmatplotlib.pyplot asplt
# Reset the default graph for rerun notebook
tf.reset_default_graph()
# Reset the random seed for reproducibility
np.random.seed(42)
tf.set_random_seed(42)
读取 MNIST 数据
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/")
n_samples = 5
plt.figure(figsize=(n_samples * 2, 3))
forindexin range(n_samples):
plt.subplot(1, n_samples, index+ 1)
sample_image = mnist.train.images[index].reshape(28, 28)
plt.imshow(sample_image, cmap="binary")
plt.axis("off")
plt.show()
从 tensorflow 数据库里引进 MNSIT 数据,选出 5 个样本打印出来。
特征 X 和标签 y
X= tf.placeholder(shape=[None, 28, 28, 1], dtype=tf.float32, name="X")
y= tf.placeholder(shape=[None], dtype=tf.int64, name="y")
定义特征 X和标签 y,placeholder 是占位符的意思,用于创建占位,当需要时再将真正的数据传入进去,即利用 feed_dict 的字典结构给 placeholder 变量“喂数据”。Placeholder 有三个参数:
X有四维,分别是图片个数,宽度像素,高度像素,色彩维度。
y只有一维,就是图片个数。其标签值就是用 0 到 9 的 int64 类型表示。
卷积层
conv1_params = {
"filters": 256,
"kernel_size": 9,
"strides": 1,
"padding": "valid",
"activation": tf.nn.relu,
}
conv1 = tf.layers.conv2d(X, name="conv1", **conv1_params)
首先在字典 conv1_params 里定义卷积层的参数,滤波器个数 256、滤波器大小 9、步长 1,填充 valid 指的没有填充、激活函数用的 relu。然后用 tensorflow 里的函数 conv2d 建立 conv1,其中 ** 代表传递一个字典类型的变量。最终 conv1 的 shape 是[?, 20, 20, 256],其中 ?代表之后才确定的图片个数。
Primary Capsules
caps1_n_maps = 32
caps1_n_dims = 8
conv2_params = {
"filters": caps1_n_maps * caps1_n_dims,
"kernel_size": 9,
"strides": 2,
"padding": "valid",
"activation": tf.nn.relu
}
conv2 = tf.layers.conv2d(conv1, name="conv2", **conv2_params)
建立 conv2 和 conv1是一样的,conv2 的 shape 是 [?, 6, 6, 256]。这里 256 其实是 32 和 8 的乘积,由小节 2.5 可知,该层实际用了 32 个滤波器滤了 8 遍。
更需要注意的是,该层 (PrimaryCaps) 每个 Capsule (1x8 向量) 和下层 (DigitCaps) 每个 Capsule (1x16 向量) 全连接,那么最好生成一个变量含有 1152 个 Capsule,因此将 conv2 的 shape 转成 [?, 1152, 8](总元素和 6x6x256 一样多),该变量记做 caps1_raw, 见下图代码。
caps1_n_caps = caps1_n_maps * 6* 6
caps1_raw = tf.reshape(conv2, [-1, caps1_n_caps, caps1_n_dims],
name="caps1_raw")
Reshape 函数里面 -1指的是某个维度大小,使得变换维度后的变量和变换前的变量的总元素个数不变。比如 A 原来的 shape 是 [3, 2, 3],如果 B 用
定义压缩函数 squash
def squash(s, axis=-1, epsilon=1e-7, name=None):
with tf.name_scope(name, default_name="squash"):
squared_norm = tf.reduce_sum(tf.square(s), axis=axis,
keep_dims=True)
safe_norm = tf.sqrt(squared_norm + epsilon)
squash_factor = squared_norm / (1. + squared_norm)
unit_vector = s / safe_norm
returnsquash_factor * unit_vector
这里有个技巧,在分母 ||s|| 里面加入小量10-7,防止分母为零。最后用 squash 函数将 caps1_raw单位化得到 cap1_output。它的 shape 也是 [?, 1152, 8]。
caps1_output= squash(caps1_raw, name="caps1_output")
Digit Capsules
根据小节 2.3,1152 个 PrimaryCaps 的变量 (1x8) 需要乘以姿态矩阵 (8x16) 得到 10 个 DigitCaps 的变量 (1x16)。下面设计的高维矩阵相乘是一种最高效的做法。
其中
上面数组已经是四维了,但别忘了还有图片个数这一维,需要用 tensorflow 里面的 tile 函数来增加一维。见下面三块代码:
数组 W
caps2_n_caps = 10
caps2_n_dims = 16
init_sigma = 0.01
W_init = tf.random_normal(
shape=(1, caps1_n_caps, caps2_n_caps, caps2_n_dims, caps1_n_dims),
stddev=init_sigma, dtype=tf.float32, name="W_init")
W = tf.Variable(W_init, name="W")
batch_size = tf.shape(X)[0]
W_tiled = tf.tile(W, [batch_size, 1, 1, 1, 1], name="W_tiled")
首先定义一个四维随机变量 W_init,当 W的初始值,它的 shape 是 [1152, 10, 16, 8],batch_size 是一批图片的个数。tile 函数实际就是将 W复制了batch_size个,储存在 W_tiled,它的 shape 是 [?, 1152, 10,16, 8],如下图:
数组 u
caps1_output_expanded = tf.expand_dims(caps1_output, -1,
name="caps1_output_expanded")
caps1_output_tile = tf.expand_dims(caps1_output_expanded, 2,
name="caps1_output_tile")
caps1_output_tiled = tf.tile(caps1_output_tile, [1, 1, caps2_n_caps, 1, 1],
name="caps1_output_tiled")
这一步是最让人困惑的。
数组 u_hat
caps2_predicted = tf.matmul(W_tiled, caps1_output_tiled,
name="caps2_predicted")
函数 matmul 是将高维数组中每个矩阵元素相乘
如下图所示:
动态路由
第一轮初始化 b
b= tf.zeros([batch_size, caps1_n_caps, caps2_n_caps, 1, 1],
dtype=np.float32, name="raw_weights")
b的 shape 为 [?, 1152, 10, 1, 1]。
第一轮初始化 c
c= tf.nn.softmax(raw_weights, dim=2, name="routing_weights")
c 的 shape 为 [?, 1152, 10, 1, 1],而且在第二个 axis 上做归一化,原因就是每一个 caps1 到所有 caps2 的概率总和为一。
第一轮计算 s和v
weighted_predictions = tf.multiply(c, caps2_predicted,
name="weighted_predictions")
s = tf.reduce_sum(weighted_predictions, axis=1,
keep_dims=True, name="weighted_sum")
v = squash(s, axis=-2, name="caps2_output_round_1")
weighted_predictions 的 shape 为 [?, 1152, 10, 16, 1],而 s和v的 shape 为 [?, 1, 10, 16, 1],因为在第一个 axis 上用 reduce_sum 函数求和再用 squash 函数压缩。
第二轮迭代
v_tiled = tf.tile(v, [1, caps1_n_caps, 1, 1, 1],
name="caps2_output_round_1_tiled")
agreement = tf.matmul(caps2_predicted, v_tiled,
transpose_a=True, name="agreement")
b= tf.add(b, agreement, name="raw_weights_round_2")
c= tf.nn.softmax(b, dim=2, name="routing_weights_round_2")
weighted_predictions = tf.multiply(c, caps2_predicted,
name="weighted_predictions_round_2")
s = tf.reduce_sum(weighted_predictions, axis=1,
keep_dims=True, name="weighted_sum_round_2")
v = squash(s, axis=-2, name="caps2_output_round_2")
第三轮迭代
v_tiled = tf.tile(v, [1, caps1_n_caps, 1, 1, 1],
name="caps2_output_round_2_tiled")
agreement = tf.matmul(caps2_predicted, v_tiled,
transpose_a=True, name="agreement")
b= tf.add(b, agreement, name="raw_weights_round_3")
c= tf.nn.softmax(b, dim=2, name="routing_weights_round_3")
weighted_predictions = tf.multiply(c, caps2_predicted,
name="weighted_predictions_round_3")
s = tf.reduce_sum(weighted_predictions, axis=1,
keep_dims=True, name="weighted_sum_round_3")
v = squash(s, axis=-2, name="caps2_output_round_3")
上面这种写出每一轮迭代的方法有点低效,一种替代方法可以用 for 语句,但是它是静态循环 (static loop), 在 tensorflow 里面每定义一次操作都会增大内部的流程图。这里三次迭代没问题,如果很多的建议用 tf.while_loop() 函数,这个是动态循环 (dynamic loop)。除了减小流程图大小以外,动态循环还能减少 GPU RAM 的使用。
间隔损失
m_plus= 0.9
m_minus= 0.1
lambda_= 0.5
T= tf.one_hot(y, depth=caps2_n_caps, name="T")
v_norm= tf.norm(v, axis=-2, keep_dims=True, name="caps2_output_norm")
FP_raw= tf.square(tf.maximum(0., m_plus - v_norm), name="FP_raw")
FP= tf.reshape(FP_raw, shape=(-1, 10), name="FP")
FN_raw= tf.square(tf.maximum(0., v_norm - m_minus), name="FN_raw")
FN= tf.reshape(FN_raw, shape=(-1, 10), name="FN")
L= tf.add(T * FP, lambda_ * (1.0- T) * FN, name="L")
margin_loss= tf.reduce_mean(tf.reduce_sum(L, axis=1), name="margin_loss")
实现小节 2.5 里面的公式,用 one_hot 函数将数字转换成 0-1 的哑变量矩阵。
Mask 机制
mask_with_labels = tf.placeholder_with_default(False, shape=(),
name="mask_with_labels")
reconstruction_targets = tf.cond(mask_with_labels, # condition
lambda:y, # ifTrue
lambda:y_pred, # ifFalse
name="reconstruction_targets")
reconstruction_mask = tf.one_hot(reconstruction_targets,
depth=caps2_n_caps,
name="reconstruction_mask")
reconstruction_mask_reshaped = tf.reshape(
reconstruction_mask, [-1, 1, caps2_n_caps, 1, 1],
name="reconstruction_mask_reshaped")
caps2_output_masked = tf.multiply(
v, reconstruction_mask_reshaped,
name="caps2_output_masked")
在重构中,并不是每一个数字的输出都传送到解码器的,只有目标数字的输出才需要传送出去,因此需要做一个 one_hot 转换。此外
解码器
n_hidden1 = 512
n_hidden2 = 1024
n_output = 28* 28
decoder_input = tf.reshape(caps2_output_masked,
[-1, caps2_n_caps * caps2_n_dims],
name="decoder_input")
with tf.name_scope("decoder"):
hidden1 = tf.layers.dense(decoder_input, n_hidden1,
activation=tf.nn.relu,
name="hidden1")
hidden2 = tf.layers.dense(hidden1, n_hidden2,
activation=tf.nn.relu,
name="hidden2")
decoder_output = tf.layers.dense(hidden2, n_output,
activation=tf.nn.sigmoid,
name="decoder_output")
解码器由个 3 全连接层组成,每层大小分别为 512,1024 和 784,用layers.dense 函数来构建。
重构损失
X_flat = tf.reshape(X, [-1, n_output], name="X_flat")
squared_difference = tf.square(X_flat - decoder_output,
name="squared_difference")
reconstruction_loss = tf.reduce_sum(squared_difference,
name="reconstruction_loss")
最终损失
alpha= 0.0005
loss= tf.add(margin_loss, alpha * reconstruction_loss, name="loss")
额外设置
# 全局初始化
init= tf.global_variables_initializer()
saver= tf.train.Saver()
# 计算精度
correct= tf.equal(y, y_pred, name="correct")
accuracy= tf.reduce_mean(tf.cast(correct, tf.float32), name="accuracy")
# 用 Adam 优化器
optimizer= tf.train.AdamOptimizer()
training_op= optimizer.minimize(loss, name="training_op")
小结
以上已经完成构建所有的网络结构,接下来训练和测试的步骤都非常标准化,就不再多言了。需要提醒的是,在训练时,mask_with_labels 设置成 True,y 被传出去用在重构损失函数里,如下图:
在测试时,mask_with_labels 设置成 False,y_pred 被传出去用在重构损失函数里,如下图:
4
总结
深度学习,本质就是一系列的张量变换 (tensor transformation)。Capsule 现在将神经元的输入和输出升级成二维向量,以后很容易会将其延伸为高维张量。
在识别数字上,人只需要看几十个最多几百个样例就能分辨数字。Capsule 只需要 CNN 需要的一小部分样例就能达到同等水平,而 CNN 通常需要上万的数据,从这点看 Capsule 的运作方式比 CNN 更接近人的大脑。此外Capsule还可以识别重叠数字。
不过 CapsNet 在 ImageNet 数据集上训练起来太耗时,而且目前这个路由算法过于简单 (Hinton 论文坑已挖好,等着大家来填)。最有趣的是从论文结果来看,引进重构比没引进重构的识别误差小很多,这到底是 Capsule 的功劳,还是单单重构的功劳?
本帖把 Capsule 的原理彻底弄清楚了,也提供了部分 tensorflow 代码,希望对大家了解这个前言课题有所帮助。Stay Tuned!
参考文献返回搜狐,查看更多
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。