CapsNet入门系列番外:基于TensorFlow实现胶囊网络

2018 年 2 月 27 日 论智 Debarko De
作者:Debarko De
编译:weakish

编者按:全栈开发者Debarko De简明扼要地介绍了胶囊网络的概念,同时给出了基于numpy和TensorFlow的胶囊网络实现。

什么是胶囊网络?什么是胶囊?胶囊网络比卷积神经网络(CNN)更好吗?本文将讨论这些关于Hinton提出的CapsNet(胶囊网络)的话题。

注意,本文讨论的不是制药学中的胶囊,而是神经网络和机器学习中的胶囊。

阅读本文前,你需要对CNN有基本的了解,否则建议你先看下我之前写的Deep Learning for Noobs。下面我将稍稍温习下与本文相关的CNN的知识,这样你能更容易理解下文CNN与CapsNet的对比。闲话就不多说了,让我们开始吧。

基本上,CNN是堆叠一大堆神经元构成的系统。CNN很擅长处理图像分类问题。让神经网络映射一张图像的所有像素,从算力上来讲,太昂贵了。而卷积在保留数据本质的前提下大大简化了计算。基本上,卷积是一大堆矩阵乘法,再将乘积相加。

图像传入网络后,一组核或过滤器扫描图像并进行卷积操作,从而创建特征映射。这些特征接着传给之后的激活层和池化层。取决于网络的层数,这一组合可能反复堆叠。激活网络给网络带来了一些非线性(比如ReLU)。池化(比如最大池化)有助于减少训练时间。池化的想法是为每个子区域创建“概要”。同时池化也提供了一些目标检测的位置和平移不变性。网络的最后是一个分类器,比如softmax分类器,分类器返回类别。训练基于对应标注数据的错误进行反向传播。在这一步骤中,非线性有助于解决梯度衰减问题。

CNN有什么问题?

在分类非常接近数据集的图像时,CNN表现极为出色。但CNN在颠倒、倾斜或其他朝向不同的图像上表现很差。训练时添加同一图像的不同变体可以解决这一问题。在CNN中,每层对图像的理解粒度更粗。举个例子,假设你试图分类船和马。最内层(第一层)理解细小的曲线和边缘。第二层可能理解直线或小形状,例如船的桅杆和整个尾巴的曲线。更高层开始理解更复杂的形状,例如整条尾巴或船体。最后层尝试总览全图(例如整条船或整匹马)。我们在每层之后使用池化,以便在合理的时间内完成计算,但本质上池化同时丢失了位置信息。

畸形变换

池化有助于建立位置不变性。否则CNN将只能拟合非常接近训练集的图像或数据。这样的不变性同时导致具备船的部件但顺序错误的图像被误认为船。所以系统会把上图右侧的图像误认为船,而人类则能很清楚地观察到两者的区别。另外,池化也有助于建立比例不变性。

比例不变性

池化本来是用来引入位置、朝向、比例不变性的,然而这一方法非常粗糙。事实上池化加入了各种位置不变性,以致将部件顺序错误的图像也误认为船了。我们需要的不是不变性,而是等价性。不变性使CNN可以容忍视角中的小变动,而等价性使CNN理解朝向和比例变动,并相应地适应图像,从而不损失图像的空间位置信息。CNN会减少自身尺寸以检测较小的船。这导向了最近发展出的胶囊网络。

什么是胶囊网络?

Sara Sabour、Nicholas Frost、Geoffrey Hinton在2017年10月发表了论文Dynamic Routing Between Capsules。当深度学习的祖父之一Geoffrey Hinton发表了一篇论文,这论文注定会是一项重大突破。整个深度学习社区都为此疯狂。这篇论文讨论了胶囊、胶囊网络以及在MNIST上的试验。MNIST是已标注的手写数字图像数据集。相比当前最先进的CNN,胶囊网络在重叠数字上的表现明显提升。论文的作者提出人脑有一个称为“胶囊”的模块,这些胶囊特别擅长处理不同的视觉刺激,以及编码位姿(位置、尺寸、朝向)、变形、速度、反射率、色调、纹理等信息。大脑肯定具备“路由”低层视觉信息至最擅长处理该信息的卷囊的机制。

胶囊网络架构

胶囊是一组嵌套的神经网络层。在通常的神经网络中,你不断添加更多层。在胶囊网络中,你会在单个网络层中加入更多的层。换句话说,在一个神经网络层中嵌套另一个。胶囊中的神经元的状态刻画了图像中的一个实体的上述属性。胶囊输出一个表示实体存在性的向量。向量的朝向表示实体的属性。向量发送至神经网络中所有可能的亲本胶囊。胶囊可以为每个可能的亲本计算出一个预测向量,预测向量是通过将自身权重乘以权重矩阵得出的。预测向量乘积标量最大的亲本胶囊的联系将增强,而剩下的亲本胶囊联系将减弱。这一基于合意的路由方法比诸如最大池化之类的现有机制更优越。最大池化路由基于低层网络检测出的最强烈的特征。动态路由之外,胶囊网络给胶囊加上了squash函数。squash属于非线性函数。与CNN给每个网络层添加squash函数不同,胶囊网络给每组嵌套的网络层添加squash函数,从而将squash函数应用到每个胶囊的输出向量。

论文引入了一个全新的squash函数(见上图)。ReLU及类似的非线性函数在单个神经元上表现良好,不过论文发现在胶囊上squash函数表现最好。squash函数压缩胶囊的输出向量的长度:当向量较小时,结果为0;当向量较大时,结果为1。动态路由增加了一些额外的运算开销,但毫无疑问带来了优势。

当然我们也要注意,这篇论文刚发不久,胶囊的概念还没有经过全面的测试。它在MNIST数据集上表现良好,但在其他更多种类、更大的数据集上的表现还有待证明。在论文发布的几天之内,就有人提出一些意见。

当前的胶囊网络实现还有改进的空间。不过别忘了Hinton的论文一开始就提到了:

这篇论文的目标不是探索整个空间,而是简单地展示一个相当直接的实现表现良好,同时动态路由有所裨益。

好了,我们已经谈了够多理论了。让我们找点乐子,构建一个胶囊网络。我将引领你阅读一些为MNIST数据配置一个胶囊网络的代码。我会在代码里加上注释,这样你可以逐行理解这些代码是如何工作的。本文将包括两个重要的代码片段。其余代码见GitHub仓库:

  
    
    
    
  1. # 只依赖numpy和tensorflow

  2. import numpy as np

  3. import tensorflow as tf

  4. from config import cfg

  5. # 定义卷积胶囊类,该类由多个神经网络层组成

  6. #

  7. class CapsConv(object):

  8.    ''' 胶囊层

  9.    参数:

  10.        input:一个4维张量。

  11.        num_units:整数,胶囊的输出向量的长度。

  12.        with_routing:布尔值,该胶囊路由经过低层胶囊。

  13.        num_outputs:该层中的胶囊数目。

  14.    返回:

  15.        一个4维张量。

  16.    '''

  17.    def __init__(self, num_units, with_routing=True):

  18.        self.num_units = num_units

  19.        self.with_routing = with_routing

  20.    def __call__(self, input, num_outputs, kernel_size=None, stride=None):

  21.        self.num_outputs = num_outputs

  22.        self.kernel_size = kernel_size

  23.        self.stride = stride

  24.        if not self.with_routing:

  25.            # 主胶囊(PrimaryCaps)层

  26.            # 输入: [batch_size, 20, 20, 256]

  27.            assert input.get_shape() == [cfg.batch_size, 20, 20, 256]

  28.            capsules = []

  29.            for i in range(self.num_units):

  30.                # 每个胶囊i: [batch_size, 6, 6, 32]

  31.                with tf.variable_scope('ConvUnit_' + str(i)):

  32.                    caps_i = tf.contrib.layers.conv2d(input,

  33.                                                      self.num_outputs,

  34.                                                      self.kernel_size,

  35.                                                      self.stride,

  36.                                                      padding="VALID")

  37.                    caps_i = tf.reshape(caps_i, shape=(cfg.batch_size, -1, 1, 1))

  38.                    capsules.append(caps_i)

  39.            assert capsules[0].get_shape() == [cfg.batch_size, 1152, 1, 1]

  40.            # [batch_size, 1152, 8, 1]

  41.            capsules = tf.concat(capsules, axis=2)

  42.            capsules = squash(capsules)

  43.            assert capsules.get_shape() == [cfg.batch_size, 1152, 8, 1]

  44.        else:

  45.            # 数字胶囊(DigitCaps)层

  46.            # reshape输入至:[batch_size, 1152, 8, 1]

  47.            self.input = tf.reshape(input, shape=(cfg.batch_size, 1152, 8, 1))

  48.            # b_IJ: [1, num_caps_l, num_caps_l_plus_1, 1]

  49.            b_IJ = tf.zeros(shape=[1, 1152, 10, 1], dtype=np.float32)

  50.            capsules = []

  51.            for j in range(self.num_outputs):

  52.                with tf.variable_scope('caps_' + str(j)):

  53.                    caps_j, b_IJ = capsule(input, b_IJ, j)

  54.                    capsules.append(caps_j)

  55.            # 返回一个张量:[atch_size, 10, 16, 1]

  56.            capsules = tf.concat(capsules, axis=1)

  57.            assert capsules.get_shape() == [cfg.batch_size, 10, 16, 1]

  58.        return(capsules)

  59. def capsule(input, b_IJ, idx_j):

  60.    ''' 层l+1中的单个胶囊的路由算法。

  61.    参数:

  62.        input: 张量 [batch_size, num_caps_l=1152, length(u_i)=8, 1]

  63.                num_caps_l为l层的胶囊数

  64.    返回:

  65.        张量 [batch_size, 1, length(v_j)=16, 1] 表示

  66.        l+1层的胶囊j的输出向量`v_j`

  67.    注意:

  68.        u_i表示l层胶囊i的输出向量,

  69.        v_j则表示l+1层胶囊j的输出向量

  70.    '''

  71.    with tf.variable_scope('routing'):

  72.        w_initializer = np.random.normal(size=[1, 1152, 8, 16], scale=0.01)

  73.        W_Ij = tf.Variable(w_initializer, dtype=tf.float32)

  74.        # 重复batch_size次W_Ij:[batch_size, 1152, 8, 16]

  75.        W_Ij = tf.tile(W_Ij, [cfg.batch_size, 1, 1, 1])

  76.        # 计算 u_hat

  77.        # [8, 16].T x [8, 1] => [16, 1] => [batch_size, 1152, 16, 1]

  78.        u_hat = tf.matmul(W_Ij, input, transpose_a=True)

  79.        assert u_hat.get_shape() == [cfg.batch_size, 1152, 16, 1]

  80.        shape = b_IJ.get_shape().as_list()

  81.        size_splits = [idx_j, 1, shape[2] - idx_j - 1]

  82.        for r_iter in range(cfg.iter_routing):

  83.            # 第4行:

  84.            # [1, 1152, 10, 1]

  85.            c_IJ = tf.nn.softmax(b_IJ, dim=2)

  86.            assert c_IJ.get_shape() == [1, 1152, 10, 1]

  87.            # 第5行:

  88.            # 在第三维使用c_I加权u_hat

  89.            # 接着在第二维累加,得到[batch_size, 1, 16, 1]

  90.            b_Il, b_Ij, b_Ir = tf.split(b_IJ, size_splits, axis=2)

  91.            c_Il, c_Ij, b_Ir = tf.split(c_IJ, size_splits, axis=2)

  92.            assert c_Ij.get_shape() == [1, 1152, 1, 1]

  93.            s_j = tf.multiply(c_Ij, u_hat)

  94.            s_j = tf.reduce_sum(tf.multiply(c_Ij, u_hat),

  95.                                axis=1, keep_dims=True)

  96.            assert s_j.get_shape() == [cfg.batch_size, 1, 16, 1]

  97.            # 第六行:

  98.            # 使用上文提及的squash函数,得到:[batch_size, 1, 16, 1]

  99.            v_j = squash(s_j)

  100.            assert s_j.get_shape() == [cfg.batch_size, 1, 16, 1]

  101.            # 第7行:

  102.            # 平铺v_j,由[batch_size ,1, 16, 1] 至[batch_size, 1152, 16, 1]

  103.            # [16, 1].T x [16, 1] => [1, 1]

  104.            # 接着在batch_size维度递归运算均值,得到 [1, 1152, 1, 1]

  105.            v_j_tiled = tf.tile(v_j, [1, 1152, 1, 1])

  106.            u_produce_v = tf.matmul(u_hat, v_j_tiled, transpose_a=True)

  107.            assert u_produce_v.get_shape() == [cfg.batch_size, 1152, 1, 1]

  108.            b_Ij += tf.reduce_sum(u_produce_v, axis=0, keep_dims=True)

  109.            b_IJ = tf.concat([b_Il, b_Ij, b_Ir], axis=2)

  110.        return(v_j, b_IJ)

  111. def squash(vector):

  112.    '''压缩函数

  113.    参数:

  114.        vector:一个4维张量 [batch_size, num_caps, vec_len, 1],

  115.    返回:

  116.        一个和vector形状相同的4维张量,

  117.        但第3维和第4维经过压缩

  118.    '''

  119.    vec_abs = tf.sqrt(tf.reduce_sum(tf.square(vector)))  # 一个标量

  120.    scalar_factor = tf.square(vec_abs) / (1 + tf.square(vec_abs))

  121.    vec_squashed = scalar_factor * tf.divide(vector, vec_abs)  # 对应元素相乘

  122.    return(vec_squashed)

上面是一整个胶囊层。堆叠胶囊层以构成胶囊网络。

  
    
    
    
  1. import tensorflow as tf

  2. from config import cfg

  3. from utils import get_batch_data

  4. from capsLayer import CapsConv

  5. class CapsNet(object):

  6.    def __init__(self, is_training=True):

  7.        self.graph = tf.Graph()

  8.        with self.graph.as_default():

  9.            if is_training:

  10.                self.X, self.Y = get_batch_data()

  11.                self.build_arch()

  12.                self.loss()

  13.                # t_vars = tf.trainable_variables()

  14.                self.optimizer = tf.train.AdamOptimizer()

  15.                self.global_step = tf.Variable(0, name='global_step', trainable=False)

  16.                self.train_op = self.optimizer.minimize(self.total_loss, global_step=self.global_step)  # var_list=t_vars)

  17.            else:

  18.                self.X = tf.placeholder(tf.float32,

  19.                                        shape=(cfg.batch_size, 28, 28, 1))

  20.                self.build_arch()

  21.        tf.logging.info('Seting up the main structure')

  22.    def build_arch(self):

  23.        with tf.variable_scope('Conv1_layer'):

  24.            # Conv1(第一卷积层), [batch_size, 20, 20, 256]

  25.            conv1 = tf.contrib.layers.conv2d(self.X, num_outputs=256,

  26.                                             kernel_size=9, stride=1,

  27.                                             padding='VALID')

  28.            assert conv1.get_shape() == [cfg.batch_size, 20, 20, 256]

  29.        # TODO: 将'CapsConv'类重写为函数,

  30.        # capsLay函数应该封装为两个函数,

  31.        # 一个类似conv2d,另一个为TensorFlow的fully_connected(全连接)。

  32.        # 主胶囊,[batch_size, 1152, 8, 1]

  33.        with tf.variable_scope('PrimaryCaps_layer'):

  34.            primaryCaps = CapsConv(num_units=8, with_routing=False)

  35.            caps1 = primaryCaps(conv1, num_outputs=32, kernel_size=9, stride=2)

  36.            assert caps1.get_shape() == [cfg.batch_size, 1152, 8, 1]

  37.        # 数字胶囊层,[batch_size, 10, 16, 1]

  38.        with tf.variable_scope('DigitCaps_layer'):

  39.            digitCaps = CapsConv(num_units=16, with_routing=True)

  40.            self.caps2 = digitCaps(caps1, num_outputs=10)

  41.        # 前文示意图中的编码器结构

  42.        # 1. 掩码:

  43.        with tf.variable_scope('Masking'):

  44.            # a). 计算 ||v_c||,接着计算softmax(||v_c||)

  45.            # [batch_size, 10, 16, 1] => [batch_size, 10, 1, 1]

  46.            self.v_length = tf.sqrt(tf.reduce_sum(tf.square(self.caps2),

  47.                                                  axis=2, keep_dims=True))

  48.            self.softmax_v = tf.nn.softmax(self.v_length, dim=1)

  49.            assert self.softmax_v.get_shape() == [cfg.batch_size, 10, 1, 1]

  50.            # b). 选取10个胶囊的最大softmax值的索引

  51.            # [batch_size, 10, 1, 1] => [batch_size] (index)

  52.            argmax_idx = tf.argmax(self.softmax_v, axis=1, output_type=tf.int32)

  53.            assert argmax_idx.get_shape() == [cfg.batch_size, 1, 1]

  54.            # c). 索引

  55.            # 由于我们是三维生物,

  56.            # 理解argmax_idx的索引过程并不容易

  57.            masked_v = []

  58.            argmax_idx = tf.reshape(argmax_idx, shape=(cfg.batch_size, ))

  59.            for batch_size in range(cfg.batch_size):

  60.                v = self.caps2[batch_size][argmax_idx[batch_size], :]

  61.                masked_v.append(tf.reshape(v, shape=(1, 1, 16, 1)))

  62.            self.masked_v = tf.concat(masked_v, axis=0)

  63.            assert self.masked_v.get_shape() == [cfg.batch_size, 1, 16, 1]

  64.        # 2. 使用3个全连接层重建MNIST图像

  65.        # [batch_size, 1, 16, 1] => [batch_size, 16] => [batch_size, 512]

  66.        with tf.variable_scope('Decoder'):

  67.            vector_j = tf.reshape(self.masked_v, shape=(cfg.batch_size, -1))

  68.            fc1 = tf.contrib.layers.fully_connected(vector_j, num_outputs=512)

  69.            assert fc1.get_shape() == [cfg.batch_size, 512]

  70.            fc2 = tf.contrib.layers.fully_connected(fc1, num_outputs=1024)

  71.            assert fc2.get_shape() == [cfg.batch_size, 1024]

  72.            self.decoded = tf.contrib.layers.fully_connected(fc2, num_outputs=784, activation_fn=tf.sigmoid)

  73.    def loss(self):

  74.        # 1. 边际损失

  75.        # [batch_size, 10, 1, 1]

  76.        # max_l = max(0, m_plus-||v_c||)^2

  77.        max_l = tf.square(tf.maximum(0., cfg.m_plus - self.v_length))

  78.        # max_r = max(0, ||v_c||-m_minus)^2

  79.        max_r = tf.square(tf.maximum(0., self.v_length - cfg.m_minus))

  80.        assert max_l.get_shape() == [cfg.batch_size, 10, 1, 1]

  81.        # reshape: [batch_size, 10, 1, 1] => [batch_size, 10]

  82.        max_l = tf.reshape(max_l, shape=(cfg.batch_size, -1))

  83.        max_r = tf.reshape(max_r, shape=(cfg.batch_size, -1))

  84.        # 计算 T_c: [batch_size, 10]

  85.        # T_c = Y,我的理解没错吧?试试看。

  86.        T_c = self.Y

  87.        # [batch_size, 10],对应元素相乘

  88.        L_c = T_c * max_l + cfg.lambda_val * (1 - T_c) * max_r

  89.        self.margin_loss = tf.reduce_mean(tf.reduce_sum(L_c, axis=1))

  90.        # 2. 重建损失

  91.        orgin = tf.reshape(self.X, shape=(cfg.batch_size, -1))

  92.        squared = tf.square(self.decoded - orgin)

  93.        self.reconstruction_err = tf.reduce_mean(squared)

  94.        # 3. 总损失

  95.        self.total_loss = self.margin_loss + 0.0005 * self.reconstruction_err

  96.        # 总结

  97.        tf.summary.scalar('margin_loss', self.margin_loss)

  98.        tf.summary.scalar('reconstruction_loss', self.reconstruction_err)

  99.        tf.summary.scalar('total_loss', self.total_loss)

  100.        recon_img = tf.reshape(self.decoded, shape=(cfg.batch_size, 28, 28, 1))

  101.        tf.summary.image('reconstruction_img', recon_img)

  102. self.merged_sum = tf.summary.merge_all()

完整代码(含训练和验证模型)见此(https://github.com/debarko/CapsNet-Tensorflow)。代码以Apache 2.0许可发布。我参考了naturomics的代码(https://github.com/naturomics)。

总结

我们介绍了胶囊网络的概念以及如何实现胶囊网络。我们尝试理解胶囊是高层的嵌套神经网络层。我们也查看了胶囊网络是如何交付朝向和其他不变性的——对图像中的每个实体而言,保持空间配置的等价性。我确信存在一些本文没有回答的问题,其中最主要的大概是胶囊及其最佳实现。不过本文是解释这一主题的初步尝试。如果你有任何疑问,请评论。我会尽我所能回答。

Siraj Raval及其演讲对本文影响很大。请在Twitter上分享本文。在twitter关注我以便在未来获取更新信息。

原文地址:https://hackernoon.com/what-is-a-capsnet-or-capsule-network-2bfbe48769cc

登录查看更多
10

相关内容

专知会员服务
73+阅读 · 2020年5月21日
【CVPR 2020-商汤】8比特数值也能训练卷积神经网络模型
专知会员服务
25+阅读 · 2020年5月7日
CVPR2020 | 商汤-港中文等提出PV-RCNN:3D目标检测新网络
专知会员服务
43+阅读 · 2020年4月17日
Sklearn 与 TensorFlow 机器学习实用指南,385页pdf
专知会员服务
126+阅读 · 2020年3月15日
Capsule Networks,胶囊网络,57页ppt,布法罗大学
专知会员服务
65+阅读 · 2020年2月29日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
160+阅读 · 2019年10月28日
胶囊网络,是什么?
人工智能头条
32+阅读 · 2019年1月2日
卷积神经网络简明教程
论智
8+阅读 · 2018年8月24日
【教程】可视化CapsNet,详解Hinton等人提出的胶囊概念与原理
GAN生成式对抗网络
8+阅读 · 2018年4月11日
看完这篇,别说你还不懂Hinton大神的胶囊网络
人工智能头条
8+阅读 · 2018年3月28日
CapsNet入门系列之四:胶囊网络架构
论智
12+阅读 · 2018年2月23日
TensorFlow神经网络教程
Python程序员
4+阅读 · 2017年12月4日
CapsNet入门系列之三:囊间动态路由算法
论智
12+阅读 · 2017年12月1日
CapsNet入门系列之二:胶囊如何工作
论智
11+阅读 · 2017年11月22日
CapsNet入门系列之一:胶囊网络背后的直觉
论智
8+阅读 · 2017年11月20日
Arxiv
7+阅读 · 2019年4月8日
Arxiv
4+阅读 · 2018年9月25日
Text classification using capsules
Arxiv
5+阅读 · 2018年8月12日
Arxiv
11+阅读 · 2018年5月13日
Arxiv
6+阅读 · 2018年3月29日
VIP会员
相关VIP内容
专知会员服务
73+阅读 · 2020年5月21日
【CVPR 2020-商汤】8比特数值也能训练卷积神经网络模型
专知会员服务
25+阅读 · 2020年5月7日
CVPR2020 | 商汤-港中文等提出PV-RCNN:3D目标检测新网络
专知会员服务
43+阅读 · 2020年4月17日
Sklearn 与 TensorFlow 机器学习实用指南,385页pdf
专知会员服务
126+阅读 · 2020年3月15日
Capsule Networks,胶囊网络,57页ppt,布法罗大学
专知会员服务
65+阅读 · 2020年2月29日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
160+阅读 · 2019年10月28日
相关资讯
胶囊网络,是什么?
人工智能头条
32+阅读 · 2019年1月2日
卷积神经网络简明教程
论智
8+阅读 · 2018年8月24日
【教程】可视化CapsNet,详解Hinton等人提出的胶囊概念与原理
GAN生成式对抗网络
8+阅读 · 2018年4月11日
看完这篇,别说你还不懂Hinton大神的胶囊网络
人工智能头条
8+阅读 · 2018年3月28日
CapsNet入门系列之四:胶囊网络架构
论智
12+阅读 · 2018年2月23日
TensorFlow神经网络教程
Python程序员
4+阅读 · 2017年12月4日
CapsNet入门系列之三:囊间动态路由算法
论智
12+阅读 · 2017年12月1日
CapsNet入门系列之二:胶囊如何工作
论智
11+阅读 · 2017年11月22日
CapsNet入门系列之一:胶囊网络背后的直觉
论智
8+阅读 · 2017年11月20日
相关论文
Top
微信扫码咨询专知VIP会员