拆解式解讀如何用飛槳復現膠囊神經網絡(Capsule Network)

飛槳開發者說】王成,深度學習愛好者,淮陰師範學院,研究方向爲計算機視覺圖像與視頻處理。php

Dynamic Routing Between Capsules是 NIPS 2017的一篇論文。算法

論文做者Geoffrey Hinton,深度學習的開創者之一,反向傳播等神經網絡經典算法的發明人。他的膠囊網絡(Capsule Network)一經發布就震動了整我的工智能領域。這種網絡基於一種被Hinton稱爲膠囊(capsule)的結構,只須要較少的數據就能得到較好的泛化能力,更好的應對模糊性,處理層級結構和位姿。2017年,他發表了囊間動態路由算法,用於膠囊網絡。網絡

下面讓咱們一塊兒來探究Capsule Network網絡結構和原理,並使用飛槳進行復現。app

下載安裝命令

## 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

卷積神經網絡的不足之處ide

卷積神經網絡(CNN)雖然表現的很優異,可是針對於旋轉或元素平移等變換後的圖片,卻沒法作到準確提取特徵。函數

好比,對下圖中字母R進行旋轉、加邊框,CNN會錯誤地認爲下圖的三個R是不一樣的字母。性能

以下圖,有兩張圖片,它們都是由一個橢圓的輪廓、眼睛、鼻子和嘴巴組成。CNN能夠垂手可得地檢測到兩張圖片上的這些特徵,而且認爲它檢測到的是臉。但顯然右邊圖片的眼睛和嘴巴位置改變了,可是CNN仍然識別爲一張正常的人臉,它沒有處理好子元素之間的位置關係。學習

這就引出了位姿的概念。位姿結合了對象之間的相對關係,在數值上表示爲4維位姿矩陣。三維對象之間的關係能夠用位姿表示,位姿的本質是對象的平移和旋轉優化

對於人類而言,能夠輕易辨識出下圖是自由女神像,儘管全部的圖像顯示的角度都不同,這是由於人類對圖像的識別並不依賴視角。雖然從沒有見過和下圖如出一轍的圖片,但仍然能馬上知道這些是自由女神像。ui

可是對CNN而言,這個任務很是難,由於它沒有內建對三維空間的理解。所以Hinton主張,若是設計一個能理解和處理對象部件間的分層位姿關係的網絡結構,那麼將對正確地分類和辨識不一樣位姿的對象有很大幫助。膠囊神經網絡就顯式地建模了這些關係,能更精準的理解輸入的圖片信息。

相比CNN,膠囊網絡的另外一大益處在於,它只須要學習一小部分數據,就能達到最早進的效果(Hinton在他關於CNN錯誤的著名演說中提到了這一點)。從這個意義上說,膠囊理論實際上更接近人腦的行爲。爲了學會區分數字,人腦只須要幾十條數據,最多幾百條數據,而CNN則須要幾萬條數據才能取得很好的效果。

下圖爲膠囊神經網絡的位姿辨別效果,和其餘模型相比,膠囊網絡能辨識上一列和下一列的圖片屬於同一類,可是CNN會認爲它們是不一樣的物品。

此外,人造神經元輸出單個標量表示結果,而膠囊能夠輸出向量做爲結果。CNN使用卷積層獲取特徵矩陣,爲了在神經元的活動中實現視角不變性,經過最大池化方法來達成這一點。可是使用最大池化的致命缺點就是丟失了有價值的信息,也沒有處理特徵之間的相對空間關係。可是在膠囊網絡中,特徵狀態的重要信息將以向量的形式被膠囊封裝。

膠囊的工做原理

讓咱們比較下膠囊與人造神經元。下表中Vector表示向量,scalar表示標量,Operation對比了它們工做原理的差別。

人造神經元能夠用以下3個步驟來表示:

1. 輸入標量的標量加權: wixi+b

2. 加權輸入標量之和:aj = ∑iwixi+b

3. 標量到標量的非線性變換:hj = f(aj)

膠囊具備上面3個步驟的向量版,並新增了輸入的仿射變換這一步驟:

1. 輸入向量的矩陣乘法:ûj|I = Wijui

2. 輸入向量的標量加權: cijûj|I

3. 加權輸入向量之和: sj = ∑i cijûj|I

4. 向量到向量的非線性變換:

下面將詳剖析這4個步驟的實現原理:

1. 輸入向量的矩陣乘法

膠囊接收的輸入向量(上圖中的U一、U2和U3)來自下層的3個膠囊。這些向量的長度分別編碼下層膠囊檢測出的相應特徵的機率。

2. 輸入向量的標量加權

一個底層膠囊如何把信息輸出給高層膠囊呢?以前的人造神經元是經過反向傳播算法一步步調整權重優化網絡,而膠囊則有所不一樣。

以下圖所示,左右兩個方形區域分別是兩個高層膠囊JK,方形區域內的點是低層膠囊輸入的分佈。一個低層膠囊經過調整權重C來「決定」將它的輸出發送給哪一個高層膠囊。調整方式是膠囊在發送輸出前,先將輸出乘以這個權重,而後發送給與結果更匹配的高層膠囊。

低層膠囊經過加權把向量輸入高層膠囊,同時高層膠囊接收到來自低層膠囊的向量。全部輸入以紅點和藍點表示。這些點彙集的地方,意味着低層膠囊的預測互相接近。

好比,膠囊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)

 

囊間動態路由(精髓所在)

低層膠囊將其輸出發送給對此表示「贊成」的高層膠囊。這是動態路由算法的精髓。

囊間動態路由算法僞代碼

  • 僞代碼的第一行指明瞭算法的輸入:低層輸入向量通過矩陣乘法獲得的û,以及路由迭代次數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次。

   

▲ 點積運算即爲向量的內積(點積)運算,

能夠表現向量的類似性。

重複次後,咱們計算出了全部高層膠囊的輸出,並確立正確路由權重。下面是根據上述原理實現的膠囊層:

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)

 

編碼器

完整的網絡結構分爲編碼器解碼器,咱們先來看看編碼器。

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方法直接向量化,加快了訓練效率。

 

解碼器

解碼器從正確的膠囊中接受一個16維向量,輸入通過三個全鏈接層獲得784個像素輸出,學習重建一張28×28像素的圖像,損失函數爲重建圖像與輸入圖像之間的歐氏距離。

下圖是我本身訓練的網絡重構得到的圖像,上面是輸入網絡的原圖片,下面是網絡重建的圖片。

性能評估

說了這麼多,膠囊神經網絡性能到底如何呢,讓咱們用同規模CNN+最大池化層來對比一下。

下圖是兩個網絡在其餘條件相同狀況下,進行1800次迭代的結果。從圖中能夠看出,雖然膠囊神經網絡收斂速度有所不及,可是收斂完成以後更加穩定,CNN+池化層準確率一直處於波動中。

再來玩一下,當訓練到一半時將全部圖片轉置(能夠理解爲將圖片水平垂直翻轉+旋轉角度,改變位姿)的狀況,實驗結論以下。

能夠明顯的看到CNN+池化層在圖片轉置的狀況下準確率直接跌落谷底,在以後的訓練中也是一蹶不振(迷失了自我)!可是膠囊神經網絡就不同了,面對大相徑庭的圖片仍然有高於50%的準確率,並且在以後迅速恢復了100%的準確率!甩了CNN+池化層一大截!

Capsule顯露了它處理不一樣位姿的本領!

下圖是膠囊數和向量維度對性能的影響。

因爲篇幅限制,更多信息能夠到AI Studio查看原項目,地址:

https://aistudio.baidu.com/aistudio/projectdetail/657114?shared=1

下載安裝命令

## 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
相關文章
相關標籤/搜索