浅析CapsNet的原理以及Pytorch实现

原文链接:Dynamic Routing Between Capsules

之前对于数字识别等算法,CNN已经取得了非常不错的算法,而作为一个新晋的算法,CapsNet并无法立即超越原来CNN的精确度。但是从另一方面来说,在传统网络中,由于参数过于密集,特征本身混杂在密集的全连接中,想要提取出来非常困难。CapsNet可以说是挑战这种传统网络缺点的一次成功的尝试,下面就来看看Hinton这篇论文讲了什么吧——


概述

在这篇文章中,Hinton提出了一种新颖的模型CapsNet来解决传统的手写数字识别MNIST问题,并取得了较好的效果。CapsNet相比于传统的卷积神经网络CNN有如下创新:

  • 对于特征向量(Channels),使用其模长表征该向量的重要程度。
  • 对于特征的层次传递,不再是使用传统方法——高层特征来源于底层特征的加权求和,而是引入一个Routing策略。

从CNN到CapsNet

与CapsNet相对应的是传统的卷积神经网络CNN,事实上,在CapsNet在构造上和CNN非常相似,其中很多部分甚至直接使用了卷积层,因此本文尝试通过对比的方式在回顾CNN的同时介绍CapsNet。

Capsule是什么?

在CNN中,一个特定的卷积核可以提取出原图中的某一个局部的特征信息。而多个卷积核的并列,等同于同时记录了多种不同的局部特征,为之后整合成一个更加复杂的特征做好了铺垫。

举一个例子,一个普通的MNIST图片(1个28*28维向量)经过一个[kernel_size=9*9,out_channels=10]的卷积核,可以得到10个20*20维的特征向量。这10个特征向量可以记录宽度、扭曲,粗细等不同细节。

再让我们看看CapsNet是怎么构造出来的——

Capsule,胶囊,在这里,可以理解为一个功能独立的小模块,他可以产生一个特征向量。比较而言,CNN中的一层,可以类比于CapsNet中的out_channels个Capsule。

与CNN相似,特征经过每一层的组合,从简单特征慢慢变得复杂,一个高层的Capsule的输入依赖于低层Capsule的输出。这种依赖关系不在是CNN中的加权求和,而是另外一种更加巧妙的方式,具体细节将在后面详述。

特征向量在Capsule间的传递已经讲解了,那么特征向量在Capsule内部干了什么呢?这就依赖于Capsule的设计者的想要解决什么问题了,至少在本文MNST识别中,Capsule内部执行了一次卷积操作。

特征向量的重要性权重

不论是CapsuleNet还是CNN,我们都通过卷积操作,提取出了特征向量。不同的特征向量重要性不尽相同。那如何处理每一个特征的重要性呢?

在CNN中,由于相邻的卷积层中,第i层的输入是第i-1层所有输出的加权和,因此,我们可以用了一个矩阵来表示第i-1层的第X个特征向量对于第i层的第Y个特征向量的重要性。这个矩阵恰好对应了一个线性全连接层,可以通过梯度下降算法学习。

而Capsule Net提出了一个全新的方法——将特征向量的重要性记录在向量的模长中!具体而言,倘若Capsule的内部计算出了一个特征向量s_j,我们希望通过一种名为squash的归一化方式,使得模长较大的s_j映射到接近1的特征向量v_j,而模长较小的s_j映射到接近0的特征向量v_j。映射方程如下:

    \[v_j = squash(s_j) =  \frac{\|s_j\|^2}{1+\|s_j\|^2} \frac{s_j}{\|s_j\|}\]

Routing策略

如果说在传统算法中,我们通常先用网络求解出一系列低级特征向量s_i,和一个表示该特征向量对于高一层特征j重要性的标量b_{i,j},则高一层特征的输入可以表示为

    \[s_j = f \left( \sum_j b_{i,j}*s_i \right)\]

实际上,相比于之后我们将介绍的Routing策略,原来的方式略显笨拙,因为b_{i,j}是一个于s_i无关的量。而我们希望的是b_{i,j}随着s_i而自适应调整。这也就是Routing策略的设计初衷。

在Routing策略中,b_{i,j}不再是通过梯度下降算法学习出来的,而是通过若干次迭代得到的——

每一次Routing初始时满足b_{i,j} = 0

对于b_{i,j}做一遍softmax归一化【注:论文是这样写的,不过我感觉softmax应该是第一维啊……不过实际上应该没有太大的差别】,

    \[c_{i,j} = \frac{exp(b_{i,j}) }{\sum_k exp(b_{i,k})}\]

归一化之后的数组c_{i,j}满足 \sum_k c_{i,k} = 1,换句话说,就是我们得到了低层s_i组合出高层s_j的加权系数c_{i,j}了。

    \[s_j = \sum_i c_{i,j} \hat{u}_{j|i} , \hat{u}_{j|i} = W_{ij} u_i\]

对于初始情况,我们的b_{i,j}=0,所有局部特征向量等权重得贡献到高一层特征中去(注意,即使是等权重,由于向量本身的模长不同,一次不同的低层次特征向量对于高层次特征向量的影响也不尽相同)。不过随着迭代的深入,b_{i,j}会发生改变,每次变化量a_{i,j}的定义如下:

    \[a_{i,j} = v_j \cdot \hat{u}_{j|i}\]

其中v_j = squash(s_j).

这个定义式的理解其实也很简单,我们相信如果一个输入向量和输出向量的方向相近,这个向量就是重要的,在下一次迭代中我们就会倾向于给这个输入向量更高的权重。

    \[b_{i,j} \rightarrow b_{i,j} + \hat{u}_{j|i} \cdot v_j\]

损失函数的设计

多层Capsule顺序链接,最后一层包含10个Capsules的特征向量v_k,这里的v_k里面包含了两个信息

  • v_k的模长包含了输入数据究竟像不像这个Capsule代表的数字。
  • v_k的向量方向包含了足以刻画原来图像的所有重要特征。

这两个信息我们分别可以设计一个Loss值来表示.

刻画相似度的Margin Loss

首先定义三个个超参数m^+ = 0.9, m^- = 0.1,以及\lambda = 0.5,并假设T_k = 1当且仅当输入图片属于第k类。

    \[L_k  = T_k max(0,m^+ -  \| v_k \| )^2 + \lambda (1-T_k) max(0, \|v_k \| - m^-)^2\]

这个式子的原理就是我们希望对于正确的那一类的向量的模长大于等于m^+,而其他分类错误的类别的向量模长小于等于m^-.

用于正则化的Reconstruction Loss

我们使用特征向量的模长能够构造出Margin Loss,这时,我们并没有使用特征向量本身,因此至少对于MNIST这个课题来说,我们可以利用最后的特征向量,来尝试恢复原来的输入数据,进而产生一个Reconstruction Loss,有利于模型的防止过拟合。

不过这里,论文中注明Reconstruction Loss仅仅用来作为一个正则项,并不能够主导模型的效果,因此在实际Loss中,Reconstruction Loss仅仅贡献一个很小的比例0.005.

使用CapsNet解决MNIST问题

整个模型包含一个CapsNet用于提取特征,和一个ReconstructNet用于尝试恢复最终提取出来的向量。其中CapsNet结构如下——

在这里我们只用了单层CapsNet,首先,对于原图进行一次卷积,得到一个256维向量,然后在进行卷积,得到32个8维向量组成的Capsules。通过之前介绍的Routing策略,我们可以对于这些低层次的Capsules合成10个代表数值的高层Capsules,每个特征向量16维。这些向量中,正确的那一个特征向量进而传到Reconstruct Net中,验证其是否真的表示了那个数字。

 

解码部分单纯由三层带激活函数的全连接层实现,最终的ReconstructionLoss决定于原图和解码后向量的距离。

 

Pytorch实现

我的实现代码参考了论文作者的实现,在这个实现中,几个常用的pytorch函数被多次用到,所以读者需要先确定你理解了这些pytorch函数。

  • view
  • unsqueeze
  • expand

其次,代码的分块清晰对应了流程图中的不同部分,包含了PrimaryCaps模块,处理处最底层的Capsule向量,以及通过一层AgreementRouting决定出的第二层Capsule向量(DigitCaps).

预处理

一个非常神奇的东西是,pytorch作为一个非常成熟的框架,竟然没有one_hot表示的内部实现,所以我们需要自己实现一个代码将标号转化为one_hot表示。

这个转换是网络上提供的经典转换方式,使用了_scatter函数。感兴趣的读者可以查阅文档了解这个函数的作用,这里就不细讲了。

Squash函数

squash函数的定义式是

    \[v_j = squash(s_j) =  \frac{\|s_j\|^2}{1+\|s_j\|^2} \frac{s_j}{\|s_j\|}\]

而函数只是无脑翻译成代码而已。

PrimaryCapsule模块

PrimaryCapsule通过卷积生成了最底层的向量,其卷积核大小都在论文中(也在流程图中)清晰指出了。

Routing模块

Routing模块本质上是通过三次迭代,得到每一个Capsule向量对于下一层的Capsule的贡献大小。

整体结构

将两个模块合并为1个模块即可得到如下结构

 

原创文章地址:【浅析CapsNet的原理以及Pytorch实现】,转载时请注明出处mhy12345.xyz

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据