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

飛槳PaddlePaddle發表於2020-09-01

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

Dynamic Routing Between Capsules是 NIPS 2017的一篇論文。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
論文作者Geoffrey Hinton,深度學習的開創者之一,反向傳播等神經網路經典演算法的發明人。他的膠囊網路(Capsule Network)一經發布就震動了整個人工智慧領域。這種網路基於一種被Hinton稱為膠囊(capsule)的結構,只需要較少的資料就能獲得較好的泛化能力,更好的應對模糊性,處理層級結構和位姿。2017年,他發表了囊間動態路由演算法,用於膠囊網路。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
下面讓我們一起來探究Capsule Network網路結構和原理,並使用飛槳進行復現。

卷積神經網路的不足之處

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

比如,對下圖中字母R進行旋轉、加邊框,CNN會錯誤地認為下圖的三個R是不同的字母。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
如下圖,有兩張圖片,它們都是由一個橢圓的輪廓、眼睛、鼻子和嘴巴組成。CNN可以輕而易舉地檢測到兩張圖片上的這些特徵,並且認為它檢測到的是臉。但顯然右邊圖片的眼睛和嘴巴位置改變了,但是CNN仍然識別為一張正常的人臉,它沒有處理好子元素之間的位置關係。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
這就引出了位姿的概念。位姿結合了物件之間的相對關係,在數值上表示為4維位姿矩陣。三維物件之間的關係可以用位姿表示,位姿的本質是物件的平移和旋轉

對於人類而言,可以輕易辨識出下圖是自由女神像,儘管所有的影像顯示的角度都不一樣,這是因為人類對影像的識別並不依賴視角。雖然從沒有見過和下圖一模一樣的圖片,但仍然能立刻知道這些是自由女神像。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
但是對CNN而言,這個任務非常難,因為它沒有內建對三維空間的理解。因此Hinton主張,如果設計一個能理解和處理物件部件間的分層位姿關係的網路結構,那麼將對正確地分類和辨識不同位姿的物件有很大幫助。膠囊神經網路就顯式地建模了這些關係,能更精準的理解輸入的圖片資訊。

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

下圖為膠囊神經網路的位姿辨別效果,和其他模型相比,膠囊網路能辨識上一列和下一列的圖片屬於同一類,但是CNN會認為它們是不同的物品。拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
此外,人造神經元輸出單個標量表示結果,而膠囊可以輸出向量作為結果。CNN使用卷積層獲取特徵矩陣,為了在神經元的活動中實現視角不變性,透過最大池化方法來達成這一點。但是使用最大池化的致命缺點就是丟失了有價值的資訊,也沒有處理特徵之間的相對空間關係。但是在膠囊網路中,特徵狀態的重要資訊將以向量的形式被膠囊封裝。

膠囊的工作原理

讓我們比較下膠囊與人造神經元。下表中Vector表示向量,scalar表示標量,Operation對比了它們工作原理的差異。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
人造神經元可以用如下3個步驟來表示:
1. 輸入標量的標量加權: wixi+b
2. 加權輸入標量之和:aj = iwixi+b
3. 標量到標量的非線性變換:h= f(aj)

膠囊具有上面3個步驟的向量版,並新增了輸入的仿射變換這一步驟:
1. 輸入向量的矩陣乘法:ûj|I = Wijui
2. 輸入向量的標量加權: cijûj|I
3. 加權輸入向量之和: sj = cijûj|I
4. 向量到向量的非線性變換:
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
下面將詳剖析這4個步驟的實現原理:
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
1. 輸入向量的矩陣乘法

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

2. 輸入向量的標量加權

一個底層膠囊如何把資訊輸出給高層膠囊呢?之前的人造神經元是透過反向傳播演算法一步步調整權重最佳化網路,而膠囊則有所不同。

如下圖所示,左右兩個方形區域分別是兩個高層膠囊JK,方形區域內的點是低層膠囊輸入的分佈。一個低層膠囊透過調整權重C來“決定”將它的輸出傳送給哪個高層膠囊。調整方式是膠囊在傳送輸出前,先將輸出乘以這個權重,然後傳送給與結果更匹配的高層膠囊。
拆解式解讀如何用飛槳復現膠囊神經網路(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以下。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
實現程式碼如下:

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)拆解式解讀如何用飛槳復現膠囊神經網路(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。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
如果正確標籤是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)

解碼器

拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
解碼器從正確的膠囊中接受一個16維向量,輸入經過三個全連線層得到784個畫素輸出,學習重建一張28×28畫素的影像,損失函式為重建影像與輸入影像之間的歐氏距離。

下圖是我自己訓練的網路重構獲得的影像,上面是輸入網路的原圖片,下面是網路重建的圖片。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)

效能評估

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

下圖是兩個網路在其他條件相同情況下,進行1800次迭代的結果。從圖中可以看出,雖然膠囊神經網路收斂速度有所不及,但是收斂完成之後更加穩定,CNN+池化層準確率一直處於波動中。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
再來玩一下,當訓練到一半時將所有圖片轉置(可以理解為將圖片水平垂直翻轉+旋轉角度,改變位姿)的情況,實驗結論如下。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
可以明顯的看到CNN+池化層在圖片轉置的情況下準確率直接跌落谷底,在之後的訓練中也是一蹶不振(迷失了自我)!但是膠囊神經網路就不一樣了,面對截然不同的圖片仍然有高於50%的準確率,而且在之後迅速恢復了100%的準確率!甩了CNN+池化層一大截!

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

下圖是膠囊數和向量維度對效能的影響。
拆解式解讀如何用飛槳復現膠囊神經網路(Capsule Network)
由於篇幅限制,更多資訊可以到AI Studio檢視原專案,地址:
https://aistudio.baidu.com/aistudio/projectdetail/657114?shared=1

如在使用過程中有問題,可加入飛槳官方QQ群進行交流:1108045677。

如果您想詳細瞭解更多飛槳的相關內容,請參閱以下文件。

·飛槳官網地址·

https://www.paddlepaddle.org.cn/

·飛槳開源框架專案地址·

GitHub: 

https://github.com/PaddlePaddle/Paddle 

Gitee: 

https://gitee.com/paddlepaddle/Paddle


相關文章