理解胶囊网络架构
理解胶囊网络架构
胶囊网络(CapsNet)是神经网络中的一种新架构,是一种比以前的神经网络设计更先进的方法,尤其适用于计算机视觉任务。迄今为止,卷积神经网络(CNN)一直用于计算机视觉任务。尽管CNN在准确性方面取得了显著的提高,但它们仍然存在一些不足之处。
池化层的缺点
CNN最初是为了对图像进行分类而设计的;它们通过连续的卷积和池化层来实现这一点。卷积块中的池化层用于减小数据维度并实现所谓的空间不变性,这意味着无论对象在图像中的哪个位置,它都能识别并对该对象进行分类。虽然这是一个强大的概念,但它也有一些缺点。其中一个缺点是,在进行池化时,它倾向于丢失大量信息,而这些信息在执行图像分割和目标检测等任务时尤其有用。当池化层丢失了关于对象旋转、位置、尺度以及不同位置属性所需的空间信息时,目标检测和分割的过程就会变得非常困难。虽然现代CNN架构已经设法使用各种高级技术来重建位置信息,但它们并非100%准确,而且重建本身是一个繁琐的过程。池化层的另一个缺点是,如果对象的位置略有改变,激活值似乎不会与其比例一起改变,这会导致图像分类的准确性很高,但如果想精确定位图像中的对象,性能就会很差。
胶囊网络
为了克服这些困难,Geoffrey Hinton提出了一种新方法,称为胶囊网络1。胶囊是神经元的集合或组,它存储关于它试图在给定图像中识别的对象的信息;主要是关于其在高维向量空间(8维或16维)中的位置、旋转、尺度等信息,其中每个维度都代表了比直观理解的更特殊的对象(参见图4)。
在计算机图形学中,有一个渲染的概念,它简单地意味着考虑对象的各种内部表示,如其位置、旋转和尺度,并将它们转换为屏幕上的图像。与此相反,我们的大脑工作方式相反,称为逆向图形。当我们看任何物体时,我们会在内部将其分解为不同的层次化子部件,并倾向于在这些内部部件之间建立关系。这就是我们识别对象的方式,因此我们的识别不依赖于对象的特定视图或方向。这个概念是胶囊网络的基本构建块。
为了理解胶囊网络中的工作原理,让我们看看它的架构设计。胶囊网络的架构分为三个主要部分,每个部分都有子操作。它们是
- 主胶囊
- 卷积
- Reshape
- 压缩
- 更高层胶囊
- 协议路由
- 损失计算
- 边距损失
- 重建损失
1. 主胶囊
这是胶囊网络的第一层,也是逆向图形过程发生的地方。假设我们向网络输入一艘船或一栋房子的图像,如下面的图像所示
现在,这些图像在这层被分解成它们的层次化子部件。为了简化起见,我们假设这些图像由两个不同的子部件构成;即一个矩形和一个三角形。
在这一层,将构建代表三角形和矩形的胶囊。假设我们在此层初始化100个胶囊,其中50个代表矩形,50个代表三角形。这些胶囊的输出用下面的图像中的箭头表示;黑色箭头表示矩形的输出,蓝色箭头表示三角形的输出。这些胶囊放置在图像的每个位置,这些胶囊的输出表示该对象是否位于该位置。在下面的图片中,您可以看到在对象未放置的位置,箭头的长度较短,而在对象放置的位置,箭头较长。长度表示对象是否存在,箭头的姿态表示给定图像中该特定对象的方向(位置、尺度、旋转等)。
这种表示法的一个有趣之处在于,如果我们稍微旋转输入图像中的对象,表示这些对象的箭头也会随着其输入副本的比例而稍微旋转。这种输入中的微小变化导致相应胶囊输出中的微小变化,这被称为等变性。这使得胶囊网络能够以精确的位置、尺度、旋转以及与之相关的其他位置属性来定位给定图像中的对象。
这是通过三个不同的过程实现的
- 卷积
- 重塑函数
- 压缩函数
在这一层,输入图像被馈送到几个卷积层。这会输出一些特征图数组;假设它输出一个18个特征图的数组。现在,我们将重塑函数应用于这些特征图,并假设我们将其重塑为图像中每个位置的两个九维向量(18 = 2 x 9),这与上面表示矩形和三角形胶囊的图像类似。现在,最后一步是确保每个向量的长度不超过1;这是因为每个向量的长度是该对象是否位于图像中给定位置的概率,因此它应该在1和0之间。为了实现这一点,我们应用了所谓的压缩函数。该函数简单地确保每个向量的长度在1和0之间,并且不会破坏位于向量更高维度中的位置信息。
现在我们需要弄清楚这些子部件或胶囊之间的关系。也就是说,如果我们考虑船和房子的例子,我们需要弄清楚哪个三角形和矩形是房子的一部分,哪个是船的一部分。到目前为止,我们使用这些卷积和压缩函数知道了这些矩形和三角形在图像中的位置。现在我们需要弄清楚那里是船还是房子,以及这些三角形和矩形与船和房子有什么关系。
2. 更高层胶囊
在进入更高层胶囊之前,主胶囊层仍然有一个重要的功能。也就是说,在更高层胶囊可以操作之前,在主层中的压缩函数之后,主层中的每个胶囊都将尝试预测网络中更高层中每个胶囊的输出;例如,我们有100个胶囊,50个矩形和50个三角形。现在,假设我们在更高层有两个类型的胶囊,一个用于房子,一个用于船。根据三角形和矩形胶囊的方向,这些胶囊将对更高层胶囊做出以下预测。这将产生以下场景
正如您所见,相对于其原始方向,矩形胶囊和三角形胶囊都在它们的预测中预测了图像中的船。它们都同意是船胶囊应该在更高层胶囊中被激活。这意味着矩形和三角形是船的一部分,而不是房子的一部分。这也意味着矩形和三角形胶囊认为选择船胶囊将解释它们在主胶囊中的方向。在这方面,两个主层胶囊都同意在下一层选择船胶囊作为图像中可能存在的对象。这被称为协议路由。
这种特定技术有几个好处。一旦主胶囊同意选择某个高级胶囊,就没有必要向另一个高级胶囊发送信号,并且所同意的胶囊中的信号可以变得更强、更清晰,并有助于准确预测对象姿态。另一个好处是,如果我们追踪激活路径,从三角形和矩形到更高层中的船胶囊,我们可以轻松地对其部件的层次结构进行排序,并理解哪个部件属于哪个对象;在本例中,矩形和三角形属于船对象。
到目前为止,我们已经处理了主层;现在,更高层胶囊的实际工作开始了。即使主层为更高层预测了一些输出,它仍然需要计算自己的输出并交叉检查哪个预测与自己的计算匹配。
更高层胶囊计算其自身输出的第一步是建立所谓的路由权重。我们现在已经收到主层给出的一些预测,对于每个预测;在第一次迭代中,它将所有路由权重声明为零。这些初始路由权重被馈送到Softmax函数,然后将输出分配给每个预测。
现在,在分配了Softmax输出到预测之后,它计算了这一层中每个胶囊的加权和。这使我们从许多预测中得到了两个胶囊。这是更高层在第一轮或第一次迭代中的实际输出。
现在我们可以找到哪个预测与该层的实际输出相比是最准确的。
选择准确的预测后,我们通过预测与该层的实际输出的点积并将其添加到现有路由权重来为下一轮计算另一个路由权重。由方程给出
U^ij (主层预测)
Vj (更高层实际输出)
Bij += U^ij + Vj
现在,如果预测和输出匹配,新的路由权重将很大,如果不匹配,权重将很小。再次,路由权重被馈送到Softmax函数,并将值分配给预测。您可以看到,强同意的预测具有较大的相关权重,而其他预测的权重较低。
再次,我们计算这些预测的加权和,并为它们赋予新的权重。但现在我们发现,与房子胶囊相比,船胶囊具有更长的向量,因为权重倾向于船胶囊,因此该层仅在两次迭代中就选择了船胶囊而不是房子胶囊。这样,我们就可以计算这一层中的输出来选择哪个胶囊用于胶囊网络的后续步骤。
为了简单起见,我只描述了两次迭代或回合,但实际上可能需要更长的时间,具体取决于您执行的任务。
3. 损失计算
现在我们已经通过协议路由方法决定了图像中的对象是什么,您可以进行分类。与我们之前的高层一样,每个类别一个胶囊,即一个胶囊用于船,一个用于房子,我们可以很容易地在这个高层之上添加一个层,并计算激活向量的长度,并根据长度,我们可以分配一个类概率来创建一个图像分类器。
在原始论文中,边距损失用于计算多个类别的类概率,以创建这样的图像分类器。边距损失简单地意味着,如果图像中存在某个类别的对象,则该对象胶囊相应向量的平方长度不得小于0.9。同样,如果图像中不存在该类别的对象,则该对象胶囊相应向量的平方长度不应大于0.1。
假设Vk是类别K对象输出向量的长度。现在,如果类别K对象存在,则其平方值不应小于0.9;即|Vk|2 >=0.9。同样,如果类别K对象不存在,则|Vk|2 <=0.1。
除了边距损失,还有一个额外的单元称为解码器网络,它连接到更高层胶囊。这个解码器网络是三个全连接层,其中两个是整流线性单元(ReLU)激活单元,最后一个是 sigmoid 激活层,用于重建输入图像。
这个解码器网络通过最小化重建图像与输入图像之间的平方差来学习重建输入图像
重建损失 = (重建图像 – 输入图像)2
现在,总损失为
总损失 = 边距损失 + alpha * 重建损失
在这里,alpha的值(最小化重建损失的常数)在论文1中为0.0005(没有提供关于为什么选择此特定值的额外信息)。这里,重建损失被大大缩减,以便更重视边距损失,使其能够主导训练过程。重建单元和重建损失的重要性在于,它迫使网络保留重建图像到最高胶囊层所需的信息。它还充当正则化器,以避免训练过程中的过拟合。
在论文1中,胶囊网络用于对MNIST*数字进行分类。正如您在下面(图1)中看到的,论文展示了用于MNIST分类的CapsNet的不同单元。在这里,输入经过两个卷积层后被重塑并压缩,形成32个主胶囊,每个包含6 x 6 x 8个胶囊。这些主胶囊被馈送到更高层胶囊,总共10个胶囊,每个16维,最后,在这些更高层胶囊上计算边距损失,以给出类概率。
图2显示了用于计算重建损失的解码器网络。更高层胶囊连接到三个全连接层,最后一个层是 sigmoid 激活层,它将输出784个像素强度值(28 x 28 重建图像)。
这个更高层胶囊的一个有趣之处在于,这一层的每个维度都是可解释的。也就是说,如果我们取论文中关于MNIST数据集的例子,16维激活向量的每个维度都可以被解释,并表示对象的某些特征。如果我们修改16个维度中的一个,我们可以调整输入的尺度和粗细;同样,另一个可以表示笔画粗细,另一个表示宽度和翻译,依此类推。
让我们看看如何使用Keras*和TensorFlow*后端来实现3。您首先需要导入所有必需的库
from keras import layers, models, optimizers
from keras.layers import Input, Conv2D, Dense
from keras.layers import Reshape, Layer, Lambda
from keras.models import Model
from keras.utils import to_categorical
from keras import initializers
from keras.optimizers import Adam
from keras.datasets import mnist
from keras import backend as K
import numpy as np
import tensorflow as tf
首先,让我们定义Squash
函数
def squash(output_vector, axis=-1):
norm = tf.reduce_sum(tf.square(output_vector), axis, keep_dims=True)
return output_vector * norm / ((1 + norm) * tf.sqrt(norm + 1.0e-10))
定义Squash
函数后,我们可以定义掩码层
class MaskingLayer(Layer):
def call(self, inputs, **kwargs):
input, mask = inputs
return K.batch_dot(input, mask, 1)
def compute_output_shape(self, input_shape):
*_, output_shape = input_shape[0]
return (None, output_shape)
现在,让我们定义主胶囊函数
def PrimaryCapsule(n_vector, n_channel, n_kernel_size, n_stride, padding='valid'):
def builder(inputs):
output = Conv2D(filters=n_vector * n_channel, kernel_size=n_kernel_size,
strides=n_stride, padding=padding)(inputs)
output = Reshape( target_shape=[-1, n_vector], name='primary_capsule_reshape')(output)
return Lambda(squash, name='primary_capsule_squash')(output)
return builder
之后,让我们编写胶囊层类
class CapsuleLayer(Layer):
def __init__(self, n_capsule, n_vec, n_routing, **kwargs):
super(CapsuleLayer, self).__init__(**kwargs)
self.n_capsule = n_capsule
self.n_vector = n_vec
self.n_routing = n_routing
self.kernel_initializer = initializers.get('he_normal')
self.bias_initializer = initializers.get('zeros')
def build(self, input_shape): # input_shape is a 4D tensor
_, self.input_n_capsule, self.input_n_vector, *_ = input_shape
self.W = self.add_weight(shape=[self.input_n_capsule, self.n_capsule,
self.input_n_vector, self.n_vector], initializer=self.kernel_initializer, name='W')
self.bias = self.add_weight(shape=[1, self.input_n_capsule, self.n_capsule, 1, 1],
initializer=self.bias_initializer, name='bias', trainable=False)
self.built = True
def call(self, inputs, training=None):
input_expand = tf.expand_dims(tf.expand_dims(inputs, 2), 2)
input_tiled = tf.tile(input_expand, [1, 1, self.n_capsule, 1, 1])
input_hat = tf.scan(lambda ac, x: K.batch_dot(x, self.W, [3, 2]), elems=input_tiled,
initializer=K.zeros( [self.input_n_capsule, self.n_capsule, 1, self.n_vector]))
for i in range(self.n_routing): # routing
c = tf.nn.softmax(self.bias, dim=2)
outputs = squash(tf.reduce_sum( c * input_hat, axis=1, keep_dims=True))
if i != self.n_routing - 1:
self.bias += tf.reduce_sum(input_hat * outputs, axis=-1, keep_dims=True)
return tf.reshape(outputs, [-1, self.n_capsule, self.n_vector])
def compute_output_shape(self, input_shape):
# output current layer capsules
return (None, self.n_capsule, self.n_vector)
下面的类将计算胶囊的长度
class LengthLayer(Layer):
def call(self, inputs, **kwargs):
return tf.sqrt(tf.reduce_sum(tf.square(inputs), axis=-1, keep_dims=False))
def compute_output_shape(self, input_shape):
*output_shape, _ = input_shape
return tuple(output_shape)
下面的函数将计算边距损失
def margin_loss(y_ground_truth, y_prediction):
_m_plus = 0.9
_m_minus = 0.1
_lambda = 0.5
L = y_ground_truth * tf.square(tf.maximum(0., _m_plus - y_prediction)) + _lambda *
( 1 - y_ground_truth) * tf.square(tf.maximum(0., y_prediction - _m_minus))
return tf.reduce_mean(tf.reduce_sum(L, axis=1))
在定义了网络所需的各种构建块之后,我们现在可以对MNIST数据集输入进行预处理,以供网络使用
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1).astype('float32') / 255.0
x_test = x_test.reshape(-1, 28, 28, 1).astype('float32') / 255.0
y_train = to_categorical(y_train.astype('float32'))
y_test = to_categorical(y_test.astype('float32'))
X = np.concatenate((x_train, x_test), axis=0)
Y = np.concatenate((y_train, y_test), axis=0)
以下是一些将表示输入形状、输出类数和路由次数的变量
input_shape = [28, 28, 1]
n_class = 10
n_routing = 3
现在,让我们创建网络的编码器部分
x = Input(shape=input_shape)
conv1 = Conv2D(filters=256, kernel_size=9, strides=1, padding='valid',
activation='relu', name='conv1')(x)
primary_capsule = PrimaryCapsule( n_vector=8, n_channel=32, n_kernel_size=9, n_stride=2)(conv1)
digit_capsule = CapsuleLayer( n_capsule=n_class, n_vec=16, n_routing=n_routing,
name='digit_capsule')(primary_capsule)
output_capsule = LengthLayer(name='output_capsule')(digit_capsule)
然后让我们创建网络的解码器部分
mask_input = Input(shape=(n_class, ))
mask = MaskingLayer()([digit_capsule, mask_input]) # two inputs
dec = Dense(512, activation='relu')(mask)
dec = Dense(1024, activation='relu')(dec)
dec = Dense(784, activation='sigmoid')(dec)
dec = Reshape(input_shape)(dec)
现在让我们创建整个模型并进行编译
model = Model([x, mask_input], [output_capsule, dec])
model.compile(optimizer='adam', loss=[ margin_loss, 'mae' ], metrics=[ margin_loss, 'mae', 'accuracy'])
要查看整个模型的层和整体架构,我们可以使用此命令:model.summary()
最后,我们可以训练模型三个 epoch,看看它的表现如何
model.fit([X, Y], [Y, X], batch_size=128, epochs=3, validation_split=0.2)
在仅训练模型三个 epoch 后,模型在MNIST数据集上的训练集输出准确率为0.9914,验证集准确率为0.9919,对于训练集和验证集,准确率均为99%。
对于上述实现,Intel® AI DevCloud被用于训练网络。Intel AI DevCloud 可供学术和个人研究使用,免费提供,您可以在此处申请:https://software.intel.com/en-us/ai-academy/tools/devcloud。
这样,您就可以使用Keras和TensorFlow后端实现胶囊网络。
现在让我们看一下胶囊网络的一些优点和缺点。
优点
- 需要更少的训练数据
- 等变性保留了输入对象的空间信息
- 协议路由对于重叠对象非常有用
- 自动计算给定对象中各部分的层次结构
- 激活向量可解释
- 在MNIST上达到了高准确率
缺点
- 在CIFAR10*等困难数据集上,结果不是最先进的
- 未在ImageNet*等大型数据集上进行测试
- 由于内部循环,训练过程缓慢
- 拥挤问题——无法区分并排放置的两个相同类型的对象。
参考文献
- Dynamic Routing Between Capsules by Sara Sabour, Nicholas Frosst and Geoffrey E Hinton: https://arxiv.org/pdf/1710.09829.pdf
- Capsule Networks (CapsNets) – Tutorial created by Aurélien Géron: https://www.youtube.com/watch?v=pPN8d0E3900
- 上述代码采用自GitHub*网站 engwang/minimal-capsule: https://github.com/fengwang/minimal-capsule
更多流行框架中的CapsNet实现
- Keras + TensorFlow: https://github.com/XifengGuo/CapsNet-Keras
- TensorFlow: https://github.com/naturomics/CapsNet-Tensorflow
- PyTorch*: https://github.com/gram-ai/capsule-networks
- TensorFlow实现(Jupyter Notebook*): https://github.com/ageron/handson-ml/blob/master/extra_capsnets.ipynb
有关编译器优化的更完整信息,请参阅我们的优化声明。