如何用飞桨复现Capsule Network

发布时间:2021-12-22 15:24:48 作者:柒染
来源:亿速云 阅读:127

如何用飞桨复现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

囊间动态路由算法伪代码

如何用飞桨复现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的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

推荐阅读:
  1. 如何用二进制包部署Kubernetes集群?
  2. Python爬虫 如何利用浏览器获取JSON数据,如获取淘宝天猫的评论链接?

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

上一篇:如何使用FreeRadius +LDAP实现验证功能

下一篇:mysql中出现1053错误怎么办

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》