本文的參考的github工程鏈接:https://github.com/laubonghaudoi/CapsNet_guide_PyTorch
之前是看過一些深度學習的代碼,但是沒有養成良好的閱讀規范,由於最近在學習CapsNet的原理,在Github找到了一個很好的示例教程,作者甚至給出了比較好的代碼閱讀順序,私以為該順序具有較強的代碼閱讀遷移性,遂以此工程為例將該代碼分析過程記錄於此:
1、代碼先看main(),main()為工程中最為頂層的設計,能夠給人對於整個流程的把控。而對於深度學習而言,main一般即為加載數據、構建模型、確定優化算法、訓練網絡模型、保存模型參數這種很具有規范性的結構。
1 if __name__ == "__main__": 2 # Default configurations 3 opt = get_opts() 4 train_loader, test_loader = get_dataloader(opt) 5 6 # Initialize CapsNet 7 model = CapsNet(opt) 8 9 # Enable GPU usage 10 if opt.use_cuda & torch.cuda.is_available(): 11 model.cuda() 12 13 # Print the model architecture and parameters 14 print("Model architectures: ") 15 print(model) 16 17 print("\nSizes of parameters: ") 18 for name, param in model.named_parameters(): 19 print("{}: {}".format(name, list(param.size()))) 20 n_params = sum([p.nelement() for p in model.parameters()]) 21 # The coupling coefficients b_ij are not included in the parameter list, 22 # we need to add them mannually, which is 1152 * 10 = 11520. 23 print('\nTotal number of parameters: %d \n' % (n_params+11520)) 24 25 # Make model checkpoint directory 26 if not os.path.exists('ckpt'): 27 os.makedirs('ckpt') 28 29 # Start training 30 train(opt, train_loader, test_loader, model, writer)
2、后面看utils.py文件里面的函數,很多比較復雜的工程中都會有這個文件,一般都是一些工程中較為基礎的函數,在CapsNet這個工程中,這個文件中包含了相關的配置以及dataloarder。
def get_dataloader(opt): # MNIST Dataset ... # Data Loader (Input Pipeline) ... return train_loader, test_loader def get_opts(): parser = argparse.ArgumentParser(description='CapsNet') # .... opt = parser.parse_args() return opt
3、然后在弄明白前向傳播中最為頂層的設計,一般就是頂層神經網絡的__init__()以及forward()
該工程中的CapsNet主要分為四個大部分:
- Conv2d, 用了256個 9×9的卷積核,步長為1,后面跟着Relu。這樣對於28*28的圖片,輸出為[256,20,20 ]
- PrimaryCaps: capsule層,具體構造后面再講
- DigitCaps:capsule層,具體構造后面再講
- Decoder:全連接層
4、在網絡前向傳播的頂層肯定調用了一些層級稍微低一些的module,下面就看這些module,本工程中主要是PrimaryCaps和DigitCaps。
PrimaryCaps
PrimaryCaps包含了32個 capsule units, 每個capsule unit都會接收來自於第一層卷積所輸出的feature map的所有數據。首先獲得32個張量u,這32個張量u是通過32個卷積運算得到的,前面輸入的為第一層卷積所得[256,20,20 ]的feature maps,32個卷積每個都是(out_channels=8, kernel_size=9, stride=2),這個地方使用了Modulelist來構造重復的卷積運算module,值得學習。在forward中將每個卷積moduel計算所得的結果append到list中,這樣后面使用torch.cat的時候可以直接使用了。問題在於后面對於這32個張量的維度順序做了變換。
坐標順序變換記錄於此:
- 每個conv_module輸出為[batch_size, 8 ,6,6],便變成了[batch_size, 8 ,36, 1]的形式,也就是這8個feature map中的每個6×6的feature map變成了一個向量
- 對32個conv_module輸出的張量cat,保存形式為[batch_size, 8, 36, 32]
- 再次變換為[batch_size,8,36×32] ,這個地方我並沒有搞懂這么做有什么意義,這和直接拿32*8個卷積核去卷積的區別在哪呢?直接拿32個卷積核卷積,然后將這32*8個卷積核再分為8組不也一樣嗎?
- 做了一次維度變換,變為[batch_size, 36×32,8]的形式
上步計算完成后,后面計算squash,這步計算類似於Relu,相當於向量的Relu操作。這個地方可以看出一個很重要的一點,就是向量v是幾維的,一個基本的v包含幾個數,從代碼中看是8個數,也就是說PrimaryCaps開始時的每個卷積module輸出的channels數為8,是這個維度組成了向量。
DigitCaps
這一層和上一層都是由capsule組成的,中間的連接是類似於全連接但又有很多的不同。
下面的表示均忽略batch_size:
上一層的輸入[36*32,8], 也就是有36*32個輸入向量u。計算步驟如下:
- 首先計算u_hat,將輸入變換為[36*32,1,1,8]的形式,中間權重為[36*32, 10, 8, 16],這樣矩陣相乘的結果為[36*32, 10, 1, 16], 此處的16應該就是輸出向量的維度
- 后面的處理與10這個維度有關系,在圖中就是c_ij,需要構造的c_ij的數量為[36*32, 10,1],在一次整個網路的前向傳播過程中,c_ij的初始值為0,會在一次前向傳播過程中內部迭代幾次,叫做動態路由算法。如下圖所示:
- u_hat的維度為 [ 36*32, 10, 16],s的維度為[10, 16],v的維度為[10,16],這中間有將36*32個數相加的過程,更新c_ij是這樣的:先將v變為[1,10,16],再計算u_hat*v得到[36*32, 10, 16],將里層維度相加,急求的是向量相乘,就會有方向的信息。由此更新c_ij

(注:該圖來自於https://blog.csdn.net/wc781708249/article/details/80015997)
Decoder:
Decoder 部分是由三層全連接層組成的。這部分是一個重構部分,希望借此部分重新構建出圖片。(有點像自編碼器)
下面的維度忽略batch_size。
前面輸出的是[10,16], 這個地方是將10個16維向量中與target中1相對的那個16維的向量取出作為后面全連接層的輸入,后面全連接的維度為16,512,1024,784。 784即28*28。
5、損失函數
損失函數主要包括兩部分,一部分是DigitCaps輸出的loss,一部分是Decorder的loss。

DigitCaps層的輸出是10個16維向量:
計算時,先根據上式計算出每個向量的損失值,然后將10個損失值相加得到最終損失。每個訓練樣本都有正確的標簽,在這種情況下,標簽將是一個10維one-hot編碼向量。假設正確的標簽是1,這意味着第一個DigitCap負責編碼數字1的存在。這一DigitCap的損失函數的Tc為1,其余9個DigitCap的Tc為0。當Tc為1時,損失函數的第二項為零,損失函數的值通過第一項計算。在我們的例子中,為了計算第一個DigitCap的損失,我們從m+減去這一DigitCap的輸出向量,其中,m+取固定值0.9。接着,我們保留所得值(僅當所得值大於零時)並取平方。否則,返回0。換句話說,當正確DigitCap預測正確標簽的概率大於0.9時,損失函數為零,當概率小於0.9時,損失函數不為零。
公式包括了一個lambda系數以確保訓練中的數值穩定性(lambda為固定值0.5)。這兩項取平方是為了讓損失函數符合L2正則,看起來作者們認為這樣正則化一下效果更好。
對於Decoder的loss,loss就是求得輸入的Image與Decorder輸出的784個數的歐式距離平方和。
對於CapsNet的基本原理,該博客給出了比較好的解釋:http://www.cnblogs.com/CZiFan/p/9803067.html
