揭开迷雾,来一顿美味的Capsule盛宴

由深度学习先驱 Hinton 开源的 Capsule 论文《Dynamic Routing Between Capsules》,无疑是去年深度学习界最热点的消息之一。得益于各种媒体的各种吹捧,Capsule 被冠以了各种神秘的色彩,诸如“抛弃了梯度下降”、“推倒深度学习重来”等字眼层出不穷,但也有人觉得 Capsule 不外乎是一个新的炒作概念。

本文试图揭开让人迷惘的云雾,领悟 Capsule 背后的原理和魅力,品尝这一顿 Capsule 盛宴。同时,笔者补做了一个自己设计的实验,这个实验能比原论文的实验更有力说明 Capsule 的确产生效果了。

1 前言

Capsule 的论文已经放出几个月了,网上已经有很多大佬进行解读,也有大佬开源实现了 CapsuleNet,这些内容都加速了我对 Capsule 的理解。然而,我觉得美中不足的是,网上多数的解读,都只是在论文的翻译上粉饰了一点文字,并没有对 Capsule 的原理进行解读。比如“动态路由”那部分,基本上就是照搬论文的算法,然后说一下迭代 3 次就收敛了。但收敛出什么来?论文没有说,解读也没有说,这显然是不能让人满意的。也难怪知乎上有读者评论说:

所谓的 capsule 为 dl 又贡献了一个花里胡哨的 trick 概念。说它是 trick,因为 hinton 没有说为什么 routing 算法为什么需要那么几步,循环套着循环,有什么理论依据吗?还是就是凑出来的?
如何评价深度学习之父 Hinton 发布的 Capsule 论文? - 知乎

这个评论虽然过激,然而也是很中肯的:凭啥 Hinton 摆出来一套算法又不解释,我们就要稀里糊涂的跟着玩?

2 Capsule 盛宴

2.1 宴会特色

这次 Capsule 盛宴的特色是“vector in vector out”,取代了以往的“scaler in scaler out”,也就是神经元的输入输出都变成了向量,从而算是对神经网络理论的一次革命。然而真的是这样子吗?难道我们以往就没有做过“vector in vector out”的任务了吗?有,而且多的是!NLP 中,一个词向量序列的输入,不就可以看成“vector in”了吗?这个词向量序列经过 RNN/CNN/Attention 的编码,输出一个新序列,不就是“vector out”了吗?在目前的深度学习中,从来不缺乏“vector in vector out”的案例,因此显然这不能算是 Capsule 的革命。

Capsule 的革命在于:它提出了一种新的“vector in vector out”的传递方案,并且这种方案在很大程度上是可解释的。

如果问深度学习(神经网络)为什么有效,我一般会这样回答:神经网络通过层层叠加完成了对输入的层层抽象,这个过程某种程度上模拟了人的层次分类做法,从而完成对最终目标的输出,并且具有比较好的泛化能力。的确,神经网络应该是这样做的,然而它并不能告诉我们它确确实实是这样做的,这就是神经网络的难解释性,也就是很多人会将深度学习视为黑箱的原因之一。

让我们来看 Hinton 是怎么来通过 Capsule 突破这一点的。

2.2 大盆菜

如果要用一道菜来比如 Capsule,我想到了“大盆菜”:

盆菜作为客家菜的菜式出现由来以久,一般也称为大盘菜,大盘菜源于客家人传统的“发财大盘菜”,顾名思义就是用一个大大的盘子,将食物都放到里面,融汇出一种特有滋味。丰富的材料一层层叠进大盘之中,最易吸收肴汁的材料通常放在下面。吃的时候每桌一盘,一层一层吃下去,汁液交融,味道馥郁而香浓,令人大有渐入佳景之快。

Capsule 就是针对着这个“层层递进”的目标来设计的,但坦白说,Capsule 论文的文笔真的不敢恭维,因此本文尽量不与论文中的符号相同,以免读者再次云里雾里。让我们来看个图。

如图所示,底层的胶囊和高层的胶囊构成一些连接关系。等等,什么是“胶囊”?其实,只要把一个向量当作一个整体来看,它就是一个“胶囊”,是的,你没看错,你可以这样理解:神经元就是标量,胶囊就是向量,就这么粗暴!Hinton 的理解是:每一个胶囊表示一个属性,而胶囊的向量则表示这个属性的“标架”。也就是说,我们以前只是用一个标量表示有没有这个特征(比如有没有羽毛),现在我们用一个向量来表示,不仅仅表示有没有,还表示“有什么样的”(比如有什么颜色、什么纹理的羽毛),如果这样理解,就是说在对单个特征的表达上更丰富了。

说到这里,我感觉有点像 NLP 中的词向量,以前我们只是用 one hot 来表示一个词,也就是表示有没有这个词而已。现在我们用词向量来表示一个词,显然词向量表达的特征更丰富,不仅可以表示有没有,还可以表示哪些词有相近含义。词向量就是 NLP 中的“胶囊”?这个类比可能有点牵强,但我觉得意思已经对了。

那么,这些胶囊要怎么运算,才能体现出“层层抽象”、“层层分类”的特性呢?让我们先看其中一部分连接:

图上只展示了 \(\mu_1\)的连接。这也就是说,目前已经有了\(\mu_1\)这个特征,(假设是羽毛),那么我想知道它属于上层特征\(\nu_{1},\nu_{2},\nu_{3},\nu_{4}\)假设分别代表了鸡、鸭、鱼、狗)中的哪一个。分类问题我们显然已经是很熟悉了,不就是内积后 softmax 吗?于是单靠\(\mu_{1}\)这个特征,我们推导出它是属于鸡、鸭、鱼、狗的概率分别是$$(P_{1|1},P_{2|1},P_{3|1},P_{4|1})=\frac{1}{Z_1}(e^\left \langle \mu1,\nu1 \right \rangle,e^\left \langle \mu1,\nu2 \right \rangle,e^\left \langle \mu1,\nu3 \right \rangle,e^\left \langle \mu1,\nu4 \right \rangle)\tag{1}$$
我们当然期望 \(P_{1|1}\)和\(P_{2|1}\)会明显大于 \(P_{3|1}\)和\(P_{4|1}\)。不过,单靠这个特征还不够,我们还需要综合各个特征,于是可以把上述操作对各个 \(\mu_i\)都做一遍,继而得到
\((P_{1|2},P_{2|2},P_{3|2},P_{4|2})\)、\((P_{1|3},P_{2|3},P_{3|3},P_{4|3})\)、…
问题是,现在得到这么多预测结果,那我究竟要选择哪个呢?而且我又不是真的要做分类,我要的是融合这些特征,构成更高级的特征。于是 Hinton 认为,既然\(\mu_i\)这个特征得到的概率分布是 \((P_{1|i},P_{2|i},P_{3|i},P_{4|i})\),那么我把这个特征切成四份,分别为 \((P_{1|i}\mu_i,P_{2|i}\mu_i,P_{3|i}\mu_i,P_{4|i}\mu_i)\),然后把这几个特征分别传给 \(\nu_1,\nu_2,\nu_3,\nu_4\),最后\(\nu_1,\nu_2,\nu_3,\nu_4\)其实就是各个底层传入的特征的累加,这样不就好了?
$$\nu_j=squash(\sum_{i}p_{j|i}\mu_i) = squash(\sum_i\frac{e^\left \langle \mu_i,\nu_j \right \rangle}{Z_i}\mu_i)\tag{2}$$
从上往下看,那么 Capsule 就是每个底层特征分别做分类,然后将分类结果整合。这时 \(\nu_j\)应该尽量与所有\(\mu_i\)都比较靠近,靠近的度量是内积。因此,从下往上看的话,可以认为 \(\nu_j\)实际上就是各个\(\mu_i\)的某个聚类中心,而 Capsule 的核心思想就是输出是输入的某种聚类结果。

现在来看这个 squash 是什么玩意,它怎么来的呢?

2.3 浓缩果汁

squash 在英文中也有浓缩果汁之意,我们就当它是一杯果汁品尝吧。这杯果汁的出现,是因为 Hinton 希望 Capsule 能有的一个性质是:胶囊的模长能够代表这个特征的概率。

其实我不喜欢概率这个名词,因为概率让我们联想到归一化,而归一化事实上是一件很麻烦的事情。我觉得可以称为是特征的“显著程度”,这就好解释了,模长越大,这个特征越显著。而我们又希望有一个有界的指标来对这个“显著程度”进行衡量,所以就只能对这个模长进行压缩了,所谓“浓缩就是精华”嘛。Hinton 选取的压缩方案是:

$$squash(x) = \frac{\left | x \right |^2}{1+\left | x \right |^2}\frac{x}{\left | x \right |}\tag{3}$$
其中\(x/\left | x \right |\)是很好理解的,就是将模长变为 1,那么前半部分怎么理解呢?为什么这样选择?事实上,将模长压缩到 0~1 的方案有很多,并不确定 Hinton 选择目前这个方案的思路。
然而,一个值得思考的问题是:如果在中间层,那么这个压缩处理是不是必要的呢?因为已经有了后面说的动态路由在里边,因此即使去掉squash 函数,网络也已经具有了非线性了,因此直觉上并没有必要在中间层也引入特征压缩,正如普通神经网络也不一定要用 sigmoid 函数压缩到 0~1。我觉得这个要在实践中好好检验一下。

3 动态路由

注意到(2)式,为了求\(\nu_j\) 需要求 softmax,可是为了求 softmax 又需要知道 \(\nu_j\),这不是个鸡生蛋、蛋生鸡的问题了吗?这时候就要上“主菜”了,即“动态路由”(Dynamic Routing),它能够根据自身的特性来更新(部分)参数,从而初步达到了 Hinton 的放弃梯度下降的目标。

这道“主菜”究竟是不是这样的呢?它是怎么想出来的?最终收敛到哪里去?让我们先上两道小菜,然后再慢慢来品尝这道主菜。

3.1 小菜 1

让我们先回到普通的神经网络,大家知道,激活函数在神经网络中的地位是举足轻重的。当然,激活函数本身很简单,比如一个 tanh 激活的全连接层,用 tensorflow 写起来就是:

1
2
y = tf.matmul(W, x) + b
y = tf.tanh(y)

可是,如果我想用 \(x = y + cos(y)\)的反函数来激活呢?也就是说,你得给我解出 \(y=f(x)\),然后再用它来做激活函数。

然而数学家告诉我们,这个东西的反函数是一个超越函数,也就是不可能用初等函数有限地表示出来。那这样不就是故意刁难么?不要紧,我们有迭代:$$y_n+1=x - cos y_n$$
选择y0=x,代入上式迭代几次,基本上就可以得到比较准确的y 了。假如迭代三次,那就是\(y = x - cos(x - cos( x - cosx))\)用 tensorflow 写出来就是

1
2
3
4
y = tf.matmul(W, x) + b
Y = y
for i in range(3):
Y = y - tf.cos(Y)

如果读者已经“预习”过 Capsule,那么就会发现这跟 Capsule 的动态路由很像。

3.2 小菜 2

再来看一个例子,这个例子可能在 NLP 中有很多对应的情景,但图像领域其实也不少。考虑一个向量序列 \(x_1,x_2,…x_n\),我现在要想办法将这 n个向量整合成一个向量 x(encoder),然后用这个向量来做分类。

也许读者会想到用 LSTM。但我这里仅仅想要将它表示为原来向量的线性组合,也就是:
$$ x = \sum ^{n}_{i=1}\lambda_ix_i$$
这里的\(\lambda_i\)相当于衡量\(x\)与\(x_i\)的相似度。然而问题来了,在 x 出现之前,凭什么能够确定这个相似度呢?这不也是一个鸡生蛋、蛋生鸡的问题吗?解决这个问题的一个方案也是迭代。首先我们也可以定义一个基于 softmax 的相似度指标,然后让
$$x = \sum ^n_{i=1}\frac{e^\left \langle x,x_i \right \rangle}{Z}x_i$$
一开始,我们一无所知,所以只好取 x为各个 \(x_i\) 的均值,然后代入右边就可以算出一个,再把它代入右边,反复迭代就行,一般迭代有限次就可以收敛,于是就可以将这个迭代过程嵌入到神经网络中了。

如果说小菜 1 跟动态路由只是神似,那么小菜 2 已经跟动态路由是神似+形似了。不过我并没有看到已有的工作是这样做的,这个小菜只是我的头脑风暴。

3.3 上主菜~

其实有了这两个小菜,动态路由这道主菜根本就不神秘了。为了得到各个 \(\nu_j\),一开始先让它们全都等于\(\mu_i\)的均值,然后反复迭代就好。说白了,输出是输入的聚类结果,而聚类通常都需要迭代算法,这个迭代算法就称为“动态路由”。至于这个动态路由的细节,其实是不固定的,取决于聚类的算法,比如关于 Capsule 的新文章《MATRIX CAPSULES WITH EM ROUTING》就使用了 Gaussian Mixture Model 来聚类。

理解到这里,就可以写出本文的动态路由的算法了:

动态路由算法
初始化\(b_{ij}=0\)
迭代r次:
\(c_i = softmax(b_j)\)
\(s_j = \sum _ic_{ij}\mu_i\)
\(\nu_j = squash(s_j)\)
\(b_{ij} = b_{ij}+\left \langle \mu_i,\nu_j \right \rangle\)
返回\(\nu_j\)

这里用的原文中的算法,苏老师根据自己的理解进行了改写,可以参考其博客。

4 模型细节

下面介绍 Capsule 实现的细节

4.1 全连接版

先不管是 Hinton 版还是我的版本,按照这个动态路由的算法,
\(\nu_j\)能够迭代地算出来,那不就没有参数了吗?真的抛弃了反向传播了?

非也非也~如果真的这样的话,各个 \(\nu_j\) 都一样了。前面已经说了,\(\nu_j\)是作为输入 \(\mu_i\)的某种聚类中心出现的,而从不同角度看输入,得到的聚类结果显然是不一样的。那么为了实现“多角度看特征”,于是可以在每个胶囊传入下一个胶囊之前,都要先乘上一个矩阵做变换,所以 (2)式实际上应该要变为
$$\nu_j = squash(\sum_i \frac{e ^\left \langle \hat{\mu}_{j|i},\nu_j \right \rangle}{Z_i}\hat{\mu}{j|i}),\hat{\mu}{j|i} = W_{ji}\mu_i\tag{4}$$
这里的 \(W_{ji}\)是待训练的矩阵,这里的乘法是矩阵乘法,也就是矩阵乘以向量。所以,Capsule 变成了下图

这时候就可以得到完整动态路由了

动态路由算法
初始化\(b_{ij}=0\)
迭代r次:
\(c_i = softmax(b_j)\)
\(s_j = \sum_ic_{ij}\hat{\mu}_{j|i}\)
\(\nu_j = squash(s_j)\)
\(b_{ij} = b_{ij}+\left \langle \hat{\mu}_{j|i},\nu_j \right \rangle\)
返回\(\nu_j\)
这样的 Capsule 层,显然相当于普通神经网络中的全连接层。

4.2 共享版

众所周知,全连接层只能处理定长输入,全连接版的 Capsule 也不例外。而 CNN 处理的图像大小通常是不定的,提取的特征数目就不定了,这种情形下,全连接层的 Capsule 就不适用了。因为在前一图就可以看到,参数矩阵的个数等于输入输入胶囊数目乘以输出胶囊数目,既然输入数目不固定,那么就不能用全连接了。

所以跟 CNN 的权值共享一样,我们也需要一个权值共享版的 Capsule。所谓共享版,是指对于固定的上层胶囊j,它与所有的底层胶囊的连接的变换矩阵是共用的,即\(W_{ji} = W_{j}\),

如图所示,共享版其实不难理解,就是自下而上地看,就是所有输入向量经过同一个矩阵进行映射后,完成聚类进行输出,将这个过程重复几次,就输出几个向量(胶囊);又或者自上而下地看,将每个变换矩阵看成是上层胶囊的识别器,上层胶囊通过这个矩阵来识别出底层胶囊是不是有这个特征。因此很明显,这个版本的胶囊的参数量并不依赖于输入的胶囊个数,因此可以轻松接在 CNN 后面。对于共享版,(2)式要变为
$$\nu_j = squash(\sum_i \frac{e ^\left \langle \hat{\mu}_{j|i},\nu_j \right \rangle}{Z_i}\hat{\mu}{j|i}),\hat{\mu}{j|i} = W_{ji}\mu_i\tag{5}$$
至于动态路由算法就没有改变了。

4.3 反向传播

尽管我不是很喜欢反向传播这个名词,然而这里似乎不得不用上这个名字了。

现在又有了 \(W_{ji}\),那么这些参数怎么训练呢?答案是反向传播。读者也许比较晕的是:现在既有动态路由,又有反向传播了,究竟两者怎么配合?其实这个真的就最简单不过了。就好像“小菜 1”那样,把算法的迭代几步(论文中是 3 步),加入到模型中,从形式上来看,就是往模型中添加了三层罢了,剩下的该做什么还是什么,最后构建一个 loss 来反向传播。

这样看来,Capsule 里边不仅有反向传播,而且只有反向传播,因为动态路由已经作为了模型的一部分,都不算在迭代算法里边了。

4.4 做了什么

是时候回顾一下了,Capsule 究竟做了什么?其实用一种最直接的方式来讲,Capsule 就是提供了一种新的“vector in vector out”的方案,这样看跟 CNN、RNN、Attention 层都没太大区别了;从 Hinton 的本意看,就是提供了一种新的、基于聚类思想来代替池化完成特征的整合的方案,这种新方案的特征表达能力更加强大。

本文转自苏老师:
https://kexue.fm/archives/4819/