如何用飞桨复现CapsuleNetwork

如何用飞桨复现Capsule Network,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

网站建设哪家好,找创新互联公司!专注于网页设计、网站建设、微信开发、小程序定制开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了运河免费建站欢迎大家使用!

下面让我们一起来探究Capsule Network网络结构和原理,并使用飞桨进行复现。

下载安装命令## CPU版本安装命令pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安装命令pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

卷积神经网络的不足之处

卷积神经网络(CNN)虽然表现的很优异,但是针对于旋转或元素平移等变换后的图片,却无法做到准确提取特征。

比如,对下图中字母R进行旋转、加边框,CNN会错误地认为下图的三个R是不同的字母。

如何用飞桨复现Capsule Network

这就引出了位姿的概念。位姿结合了对象之间的相对关系,在数值上表示为4维位姿矩阵。三维对象之间的关系可以用位姿表示,位姿的本质是对象的平移和旋转

对于人类而言,可以轻易辨识出下图是自由女神像,尽管所有的图像显示的角度都不一样,这是因为人类对图像的识别并不依赖视角。虽然从没有见过和下图一模一样的图片,但仍然能立刻知道这些是自由女神像。

如何用飞桨复现Capsule Network

此外,人造神经元输出单个标量表示结果,而胶囊可以输出向量作为结果。CNN使用卷积层获取特征矩阵,为了在神经元的活动中实现视角不变性,通过最大池化方法来达成这一点。但是使用最大池化的致命缺点就是丢失了有价值的信息,也没有处理特征之间的相对空间关系。但是在胶囊网络中,特征状态的重要信息将以向量的形式被胶囊封装。

胶囊的工作原理

让我们比较下胶囊与人造神经元。下表中Vector表示向量,scalar表示标量,Operation对比了它们工作原理的差异。

如何用飞桨复现Capsule Network

下面将详剖析这4个步骤的实现原理:

如何用飞桨复现Capsule Network

低层胶囊通过加权把向量输入高层胶囊,同时高层胶囊接收到来自低层胶囊的向量。所有输入以红点和蓝点表示。这些点聚集的地方,意味着低层胶囊的预测互相接近。

比如,胶囊JK中都有一组聚集的红点,因为这些胶囊的预测很接近。在胶囊J中,低层胶囊的输出乘以相应的矩阵W后,落在了远离胶囊J中的红色聚集区的地方;而在胶囊K中,它落在红色聚集区边缘,红色聚集区表示了这个高层胶囊的预测结果。低层胶囊具备测量哪个高层胶囊更能接受其输出的机制,并据此自动调整权重,使对应胶囊K的权重C变高,对应胶囊J的权重C变低。

关于权重,我们需要关注:

1. 权重均为非负标量。

2. 对每个低层胶囊i而言,所有权重的总和等于1(经过softmax函数加权)。

3. 对每个低层胶囊i而言,权重的数量等于高层胶囊的数量。

4. 这些权重的数值由迭代动态路由算法确定。

对于每个低层胶囊i而言,其权重定义了传给每个高层胶囊j的输出的概率分布。

3. 加权输入向量之和

这一步表示输入的组合,和通常的人工神经网络类似,只是它是向量的和而不是标量的和。

4. 向量到向量的非线性变换

CapsNet的另一大创新是新颖的非线性激活函数,这个函数接受一个向量,然后在不改变方向的前提下,压缩它的长度到1以下。

实现代码如下:

def squash(self,vector):
         '''
        压缩向量的函数,类似激活函数,向量归一化
        Args:
            vector:一个4维张量 [batch_size,vector_num,vector_units_num,1]
        Returns:
            一个和x形状相同,长度经过压缩的向量
            输入向量|v|(向量长度)越大,输出|v|越接近1
        '''        vec_abs = fluid.layers.sqrt(fluid.layers.reduce_sum(fluid.layers.square(vector)))  
        scalar_factor = fluid.layers.square(vec_abs) / (1 + fluid.layers.square(vec_abs))  
        vec_squashed = scalar_factor * fluid.layers.elementwise_div(vector, vec_abs) 
        return(vec_squashed)

囊间动态路由(精髓所在)

低层胶囊将其输出发送给对此表示“同意”的高层胶囊。这是动态路由算法的精髓。

如何用飞桨复现Capsule Network

囊间动态路由算法伪代码

  • 伪代码的第一行指明了算法的输入:低层输入向量经过矩阵乘法得到的û,以及路由迭代次数r。最后一行指明了算法的输出,高层胶囊的向量Vj。

  • 第2行的bij是一个临时变量,存放了低层向量对高层胶囊的权重,它的值会在迭代过程中逐个更新,当开始一轮迭代时,它的值经过softmax转换成cij。在囊间动态路由算法开始时,bij的值被初始化为零(但是经过softmax后会转换成非零且各个权重相等的cij)。

  • 第3行表明第4-7行的步骤会被重复r次(路由迭代次数)。

  • 第4行计算低层胶囊向量的对应所有高层胶囊的权重。bi的值经过softmax后会转换成非零权重ci且其元素总和等于1。

  • 如果是第一次迭代,所有系数cij的值会相等。例如,如果我们有8个低层胶囊和10个高层胶囊,那么所有cij的权重都将等于0.1。这样初始化使不确定性达到最大值:低层胶囊不知道它们的输出最适合哪个高层胶囊。当然,随着这一进程的重复,这些均匀分布将发生改变。

  • 第5行,那里将涉及高层胶囊。这一步计算经前一步确定的路由系数加权后的输入向量的总和,得到输出向量sj。

  • 第7行进行更新权重,这是路由算法的精髓所在。我们将每个高层胶囊的向量vj与低层原来的输入向量û逐元素相乘求和获得内积(也叫点积,点积检测胶囊的输入和输出之间的相似性(下图为示意图)),再用点积结果更新原来的权重bi。这就达到了低层胶囊将其输出发送给具有类似输出的高层胶囊的效果,刻画了向量之间的相似性。这一步骤之后,算法跳转到第3步重新开始这一流程,并重复r次。

如何用飞桨复现Capsule Network   

▲ 点积运算即为向量的内积(点积)运算,

可以表现向量的相似性。

重复次后,我们计算出了所有高层胶囊的输出,并确立正确路由权重。下面是根据上述原理实现的胶囊层:

class Capsule_Layer(fluid.dygraph.Layer):
    def __init__(self,pre_cap_num,pre_vector_units_num,cap_num,vector_units_num):
        '''
        胶囊层的实现类,可以直接同普通层一样使用
        Args:
            pre_vector_units_num(int):输入向量维度 
            vector_units_num(int):输出向量维度 
            pre_cap_num(int):输入胶囊数 
            cap_num(int):输出胶囊数 
            routing_iters(int):路由迭代次数,建议3次 
        Notes:
            胶囊数和向量维度影响着性能,可作为主调参数
        '''
        super(Capsule_Layer,self).__init__()
        self.routing_iters = 3
        self.pre_cap_num = pre_cap_num
        self.cap_num = cap_num
        self.pre_vector_units_num = pre_vector_units_num
        for j in range(self.cap_num):
            self.add_sublayer('u_hat_w'+str(j),fluid.dygraph.Linear(\
            input_dim=pre_vector_units_num,output_dim=vector_units_num))


    def squash(self,vector):
        '''
        压缩向量的函数,类似激活函数,向量归一化
        Args:
            vector:一个4维张量 [batch_size,vector_num,vector_units_num,1]
        Returns:
            一个和x形状相同,长度经过压缩的向量
            输入向量|v|(向量长度)越大,输出|v|越接近1
        '''
        vec_abs = fluid.layers.sqrt(fluid.layers.reduce_sum(fluid.layers.square(vector)))
        scalar_factor = fluid.layers.square(vec_abs) / (1 + fluid.layers.square(vec_abs))
        vec_squashed = scalar_factor * fluid.layers.elementwise_div(vector, vec_abs)
        return(vec_squashed)

    def capsule(self,x,B_ij,j,pre_cap_num):
        '''
        这是动态路由算法的精髓。
        Args:
            x:输入向量,一个四维张量 shape = (batch_size,pre_cap_num,pre_vector_units_num,1)
            B_ij: shape = (1,pre_cap_num,cap_num,1)路由分配权重,这里将会选取(split)其中的第j组权重进行计算
            j:表示当前计算第j个胶囊的路由
            pre_cap_num:输入胶囊数
        Returns:
            v_j:经过多次路由迭代之后输出的4维张量(单个胶囊)
            B_ij:计算完路由之后又拼接(concat)回去的权重
        Notes:
            B_ij,b_ij,C_ij,c_ij注意区分大小写哦
        '''
        x = fluid.layers.reshape(x,(x.shape[0],pre_cap_num,-1))
        u_hat = getattr(self,'u_hat_w'+str(j))(x)
        u_hat = fluid.layers.reshape(u_hat,(x.shape[0],pre_cap_num,-1,1))
        shape_list = B_ij.shape#(1,1152,10,1)
        split_size = [j,1,shape_list[2]-j-1]
        for i in range(self.routing_iters):
            C_ij = fluid.layers.softmax(B_ij,axis=2)
            b_il,b_ij,b_ir = fluid.layers.split(B_ij,split_size,dim=2)
            c_il,c_ij,b_ir = fluid.layers.split(C_ij,split_size,dim=2)
            v_j = fluid.layers.elementwise_mul(u_hat,c_ij) 
v_j = fluid.layers.reduce_sum(v_j,dim=1,keep_dim=True)
            v_j = self.squash(v_j)
            v_j_expand = fluid.layers.expand(v_j,(1,pre_cap_num,1,1))
            u_v_produce = fluid.layers.elementwise_mul(u_hat,v_j_expand)
            u_v_produce = fluid.layers.reduce_sum(u_v_produce,dim=2,keep_dim=True) 
            b_ij += fluid.layers.reduce_sum(u_v_produce,dim=0,keep_dim=True)
            B_ij = fluid.layers.concat([b_il,b_ij,b_ir],axis=2)
        return v_j,B_ij

    def forward(self,x):
        '''
        Args:
            x:shape = (batch_size,pre_caps_num,vector_units_num,1) or (batch_size,C,H,W)
                如果是输入是shape=(batch_size,C,H,W)的张量,
                则将其向量化shape=(batch_size,pre_caps_num,vector_units_num,1)
                满足:C * H * W = vector_units_num * caps_num
                其中 C >= caps_num
        Returns:
            capsules:一个包含了caps_num个胶囊的list
        '''
        if x.shape[3]!=1:
            x = fluid.layers.reshape(x,(x.shape[0],self.pre_cap_num,-1))
            temp_x = fluid.layers.split(x,self.pre_vector_units_num,dim=2)
            temp_x = fluid.layers.concat(temp_x,axis=1)
            x = fluid.layers.reshape(temp_x,(x.shape[0],self.pre_cap_num,-1,1))
            x = self.squash(x)
        B_ij = fluid.layers.ones((1,x.shape[1],self.cap_num,1),dtype='float32')/self.cap_num#
        capsules = []
        for j in range(self.cap_num):
            cap_j,B_ij = self.capsule(x,B_ij,j,self.pre_cap_num)
            capsules.append(cap_j)
        capsules = fluid.layers.concat(capsules,axis=1)
        return capsules

损失函数

将一个10维one-hot编码向量作为标签,该向量由9个零和1个一(正确标签)组成。在损失函数公式中,与正确的标签对应的输出胶囊,系数Tc为1。

如果正确标签是9,这意味着第9个胶囊输出的损失函数的Tc为1,其余9个为0。当Tc为1时,公式中损失函数的右项系数为零,也就是说正确输出项损失函数的值只包含了左项计算;相应的左系数为0,则右项系数为1,错误输出项损失函数的值只包含了右项计算。

|v|为胶囊输出向量的模长,一定程度上表示了类概率的大小,我们再拟定一个量m,用这个变量来衡量概率是否合适。公式右项包括了一个lambda系数以确保训练中的数值稳定性(lambda为固定值0.5),这两项取平方是为了让损失函数符合L2正则。

 def get_loss_v(self,label):
        '''
        计算边缘损失
        Args:
            label:shape=(32,10) one-hot形式的标签
        Notes:
            这里我调用Relu把小于0的值筛除
            m_plus:正确输出项的概率(|v|)大于这个值则loss为0,越接近则loss越小
            m_det:错误输出项的概率(|v|)小于这个值则loss为0,越接近则loss越小
            (|v|即胶囊(向量)的模长)
        '''        #计算左项,虽然m+是单个值,但是可以通过广播的形式与label(32,10)作差        max_l =  fluid.layers.relu(train_params['m_plus'] - self.output_caps_v_lenth)
        #平方运算后reshape        max_l = fluid.layers.reshape(fluid.layers.square(max_l),(train_params['batch_size'],-1))#32,10        #同样方法计算右项        max_r =  fluid.layers.relu(self.output_caps_v_lenth - train_params['m_det'])
        max_r = fluid.layers.reshape(fluid.layers.square(max_r),(train_params['batch_size'],-1))#32,10        #合并的时候直接用one-hot形式的标签逐元素乘算便可        margin_loss = fluid.layers.elementwise_mul(label,max_l)\
                        + fluid.layers.elementwise_mul(1-label,max_r)*train_params['lambda_val']
        self.margin_loss = fluid.layers.reduce_mean(margin_loss,dim=1)

编码器

完整的网络结构分为编码器解码器,我们先来看看编码器。

如何用飞桨复现Capsule Network

1. 输入图片28x28首先经过1x256x9x9的卷积层 获得256个20x20的特征图;

2. 用8组256x32x9x9(stride=2)的卷积获得8组32x6x6的特征图;

3. 将获取的特征图向量化输入10个胶囊,这10个胶囊输出向量的长度就是各个类别的概率。

class Capconv_Net(fluid.dygraph.Layer):
    def __init__(self):
        super(Capconv_Net,self).__init__()
        self.add_sublayer('conv0',fluid.dygraph.Conv2D(\
        num_channels=1,num_filters=256,filter_size=(9,9),padding=0,stride = 1,act='relu'))
                for i in range(8):
            self.add_sublayer('conv_vector_'+str(i),fluid.dygraph.Conv2D(\
            num_channels=256,num_filters=32,filter_size=(9,9),stride=2,padding=0,act='relu'))

    def forward(self,x,v_units_num):
        x = getattr(self,'conv0')(x)
        capsules = []
        for i in range(v_units_num):
            temp_x = getattr(self,'conv_vector_'+str(i))(x)
            capsules.append(fluid.layers.reshape(temp_x,(train_params['batch_size'],-1,1,1)))
        x = fluid.layers.concat(capsules,axis=2)        x = self.squash(x)
        return x

从实现代码中我们不难看出特征图转换成向量实际的过程,是将每组二维矩阵展开成一维矩阵(当然有多个二维矩阵则展开后前后拼接);之后再将所有组的一维矩阵在新的维度拼接形成向量(下图为示意图)。根据下面这个思路我经把8次卷积缩小到了一次卷积,本质上脱离循环只用split和concat方法直接向量化,加快了训练效率。

如何用飞桨复现Capsule Network

解码器从正确的胶囊中接受一个16维向量,输入经过三个全连接层得到784个像素输出,学习重建一张28×28像素的图像,损失函数为重建图像与输入图像之间的欧氏距离。

下图是我自己训练的网络重构获得的图像,上面是输入网络的原图片,下面是网络重建的图片。

如何用飞桨复现Capsule Network

再来玩一下,当训练到一半时将所有图片转置(可以理解为将图片水平垂直翻转+旋转角度,改变位姿)的情况,实验结论如下。

如何用飞桨复现Capsule Network

看完上述内容,你们掌握如何用飞桨复现Capsule Network的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注创新互联行业资讯频道,感谢各位的阅读!


网页题目:如何用飞桨复现CapsuleNetwork
文章路径:http://scyanting.com/article/jsoigg.html