Caffe 議事(三):從零開始搭建 ResNet 之 網絡的搭建(中)

  上面2個函數定義好了,那麼剩下的編寫網絡就比較容易了,咱們在ResNet結構介紹中有一個表,再貼出來:python

Layer_namegit

Output_sizegithub

20-layer ResNet網絡

Conv1ide

32 X 32函數

Kernel_size=3 X 3學習

Num_output = 16測試

Stride = 1this

Pad = 1spa

Conv2_x

32 X 32

 {3X3,16; 3X3,16} X 3

Conv3_x

16 X 16

 {3X3,16; 3X3,16} X 3

Conv4_x

8 X 8

 {3X3,16; 3X3,16} X 3

InnerProduct

1 X 1

Average pooling

10-d fc

  在 Conv1 中對圖像作一次卷積便可,Conv2_x 到 Conv4_x 每一個都有 3 個 block,而且卷積核數目都是翻倍增長 {16, 32, 64},圖像塊大小翻倍減少 {32, 16, 8},因爲咱們輸入圖像通過剪裁是 28 X 28,實際咱們應該是 {28, 14, 7},不過咱們暫且就看成輸入圖像是 32 X 32 大小。
  網絡中大部分卷積核大小都是 3 X 3,爲何有的卷積核的 pad 是 1,由於若是不加 pad 的話輸出的 size 就會比原圖像小,所以要加上 pad 這樣卷積出來的圖片就和原圖像 size 一致。Conv1 到 Conv2_x 之間因爲數據的通道數都是 16,數據的維度同樣,所以輸入和輸出能夠直接加在一塊兒。可是 Con2_x 到 Conv3_x 和 Conv3_x 到 Conv4_x 之間數據的通道數都不同,並且 output_size 都不同,不能加在一塊兒,在這裏咱們採用論文中的 B 方法——對輸入數據用 1 X 1 的卷積核映射到與輸出數據一樣的維度。這就是爲何 ResNet_Block() 裏面 projection_stride(映射步長) = 1 時認爲輸入輸出的維度同樣,能夠直接相加,所以 proj = bottom;而當 projection_stride(映射步長) = 2時認爲輸入與輸出維度不同,須要用 1 X 1 大小,stride = 2 的卷積核來使得卷積核後的 output_size 是原來的同樣。那麼在 ResNet() 函數中咱們這樣編寫:

def ResNet(split):
 
    if split == 'train':        
        ...
    else:        
        ...
    
    # 每一個 ConvX_X 都有 3 個Residual Block                                                  
    repeat = 3
    scale, result = conv_BN_scale_relu(split, data, nout = 16, ks = 3, 
                                       stride = 1, pad = 1)
    
    # Conv2_X,輸入與輸出的數據通道數都是 16, 大小都是 32 x 32,能夠直接相加,
    # 設置映射步長爲 1
    for ii in range(repeat):
        
        projection_stride = 1
        result = ResNet_block(split, result, nout = 16, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
    
    # Conv3_X
    for ii in range(repeat):
        
        if ii == 0:
            
            # 只有在剛開始 conv2_X(16 x 16) 到 conv3_X(8 x 8) 的
            # 數據維度不同,須要映射到相同維度,卷積映射的 stride 爲 2
            projection_stride = 2
            
        else:
            
            projection_stride = 1
            result = ResNet_block(split, result, nout = 32, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
    
    # Conv4_X                          
    for ii in range(repeat):
        
        if ii == 0:
            
            projection_stride = 2
            
        else:
            
            projection_stride = 1
            result = ResNet_block(split, result, nout = 64, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
View Code

  這樣,咱們就寫好了中間的卷積部分,這時候咱們最後一層的輸出是 64 X 8 X 8(64個通道的 8 X 8 大小的 feature map),最後咱們要通過一個 global average pooling,就是把每一個 8 X 8 的 feature map 映射成 1 X 1 大小,最後輸出爲 64 X 1 X 1,再通過輸出個數爲 10 的InnerProduct 全鏈接層輸出 10 類標籤的機率,生成機率的話就用 softmaxWithLoss 層便可。那麼整個 ResNet() 的能夠寫成:

def ResNet(split):
    
    # 寫入數據的路徑
    train_file = this_dir + '/caffe-master/examples/cifar10/cifar10_train_lmdb'
    test_file = this_dir + '/caffe-master/examples/cifar10/cifar10_test_lmdb'
    mean_file = this_dir + '/caffe-master/examples/cifar10/mean.binaryproto'
    
    # source: 導入的訓練數據路徑; 
    # backend: 訓練數據的格式; 
    # ntop: 有多少個輸出,這裏是 2 個,分別是 n.data 和 n.labels,即訓練數據和標籤數據,
    # 對於 caffe 來講 bottom 是輸入,top 是輸出
    # mirror: 定義是否水平翻轉,這裏選是
    
    # 若是寫是訓練網絡的 prototext 文件    
    if split == 'train':
        
        data, labels = L.Data(source = train_file, backend = P.Data.LMDB, 
                              batch_size = 128, ntop = 2, 
                              transform_param = dict(mean_file = mean_file, 
                                                      crop_size = 28, 
                                                      mirror = True))

    
    # 若是寫的是測試網絡的 prototext 文件
    # 測試數據不須要水平翻轉,你僅僅是用來測試
    else:
        
        data, labels = L.Data(source = test_file, backend = P.Data.LMDB, 
                              batch_size = 128, ntop = 2, 
                              transform_param = dict(mean_file = mean_file, 
                                                      crop_size =28))
    
    # 每一個 ConvX_X 都有 3 個Residual Block                                                  
    repeat = 3
    scale, result = conv_BN_scale_relu(split, data, nout = 16, ks = 3, 
                                       stride = 1, pad = 1)
    
    # Conv2_X,輸入與輸出的數據通道數都是 16, 大小都是 32 x 32,能夠直接相加,
    # 設置映射步長爲 1
    for ii in range(repeat):
        
        projection_stride = 1
        result = ResNet_block(split, result, nout = 16, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
    
    # Conv3_X
    for ii in range(repeat):
        
        if ii == 0:
            
            # 只有在剛開始 conv2_X(16 x 16) 到 conv3_X(8 x 8) 的
            # 數據維度不同,須要映射到相同維度,卷積映射的 stride 爲 2
            projection_stride = 2
            
        else:
            
            projection_stride = 1
            result = ResNet_block(split, result, nout = 32, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
    
    # Conv4_X                          
    for ii in range(repeat):
        
        if ii == 0:
            
            projection_stride = 2
            
        else:
            
            projection_stride = 1
            result = ResNet_block(split, result, nout = 64, ks = 3, stride = 1, 
                              projection_stride = projection_stride, pad = 1)
                              
    pool = L.Pooling(result, pool = P.Pooling.AVE, global_pooling = True)
    IP = L.InnerProduct(pool, num_output = 10, 
                        weight_filler = dict(type = 'xavier'), 
                        bias_filler = dict(type = 'constant'))
                        
    acc = L.Accuracy(IP, labels)
    loss = L.SoftmaxWithLoss(IP, labels)
    
    return to_proto(acc, loss)

  運行整個文件後,咱們就生成了網絡的 train.prototxt 和 test.prototxt 文件了,可是咱們怎麼知道咱們生成的 prototxt 是否是正確的呢?咱們能夠可視化網絡的結構。咱們在 ResNet 文件夾下打開終端,輸入:

$python ./caffe-master/python/draw_net.py ./res_net_model/train.prototxt ./res_net_model/train.png --rankdir=TB

Fig 14 畫網絡結構

  draw_net.py 至少輸入 2 個參數,一個是 prototxt 文件的地址,一個是圖像保存的地址,後面的 --rankdir=TB 的意思是網絡的結構從上到下 (Top→Bottom) 畫出來,相似還有 BT,LR(Left→Right),RL。另外,這裏須要說明一下,可能有些高版本的 Caffe 用這個命令會報錯,緣由是 ‘int’ object has no attribute '_values',關於這個問題的解決,請看這裏:https://github.com/BVLC/caffe/issues/5324。

Fig 15 draw_net.py 畫出的網絡結構圖

  因爲圖片太長,咱們只顯示了網絡的部分結構,接下來,咱們要生成 solver 的 prototxt。

 

  步驟3:建立 solver 的 prototxt 文件

  在 caffe-master/examples/pycaffe 文件夾中有一個 tools.py 文件,這個文件能夠生成咱們所須要的 solver 的 prototxt 文件,咱們在 /ResNet 文件下新建一個 tools 文件夾,再把 tools.py 放入這個文件夾中,因爲系統只能找到當前目錄下的文件,爲了讓系統可以找到 tool.py,咱們在 init_path.py 中添加下面這句:

tools_path = osp.join(this_dir, 'tools')
add_path(tools_path )

  這樣咱們就把 ResNet/tools 路徑添加到系統中,系統就能找到 tools.py 了。那麼咱們在 mydemo.py 開頭,在 import init_path 以後添加 import tools,這樣就把 tools.py 導入到了系統中。咱們在 mydemo.py 文件最下面的 make_net() 後面添加如下代碼: 

# 把內容寫入 res_net_model 文件夾中的 res_net_solver.prototxt
solver_dir = this_dir + '/res_net_model/res_net_solver.prototxt'
solver_prototxt = tools.CaffeSolver()
solver_prototxt.write(solver_dir)

 

  那麼res_net_solver.prototxt裏面究竟寫了啥?咱們先打開ResNet/tools/tools.py。

Fig 16 tools.py 截圖

  裏面有各類 solver 參數,論文中的 basb_lr 爲 0.1,lr_policy = multistep 等,具體地,咱們改爲如下的參數:

def __init__(self, testnet_prototxt_path=this_dir+"/../res_net_model/test.prototxt",
             trainnet_prototxt_path=this_dir+"/../res_net_model/train.prototxt", debug=False):

    self.sp = {}

    # critical:
    self.sp['base_lr'] = '0.1'
    self.sp['momentum'] = '0.9'

    # speed:
    self.sp['test_iter'] = '100'
    self.sp['test_interval'] = '500'

    # looks:
    self.sp['display'] = '100'
    self.sp['snapshot'] = '2500'
    self.sp['snapshot_prefix'] = '"/home/your_name/ResNet/res_net_model/snapshot/snapshot"'  # string within a string!

    # learning rate policy
    self.sp['lr_policy'] = '"multistep"'
    self.sp['step_value'] = '32000'
    self.sp['step_value1'] = '48000'

    # important, but rare:
    self.sp['gamma'] = '0.1'
    self.sp['weight_decay'] = '0.0001'
    self.sp['train_net'] = '"' + trainnet_prototxt_path + '"'
    self.sp['test_net'] = '"' + testnet_prototxt_path + '"'

    # pretty much never change these.
    self.sp['max_iter'] = '100000'
    self.sp['test_initialization'] = 'false'
    self.sp['average_loss'] = '25'  # this has to do with the display.
    self.sp['iter_size'] = '1'  # this is for accumulating gradients

    if (debug):
        self.sp['max_iter'] = '12'
        self.sp['test_iter'] = '1'
        self.sp['test_interval'] = '4'
        self.sp['display'] = '1'

  下面解釋一下爲何設置這些參數,論文中的學習率爲 0.1,對應的咱們設置 lr_base = 0.1;在迭代到 32000 次,48000 次的時候學習率依次下降十分之一,那麼 lr_policy = multistep (多階段變化), gamma = 0.1;權重的懲罰係數爲 0.0001;batch 的大小是 128,這個咱們在 ResNet()函數中已經定義過了,momentum 動量是 0.9。那麼其餘的就隨意了,固然有 GPU 最好用 GPU 跑,CPU 跑得至關慢,其餘參數的意思都比較好理解,關於 snapshot 這個的意思是每迭代多少次保存一次模型,所以你能夠找到模型迭代過程當中各個階段的參數,訓練過程是一個漫長的等待,設置snapshot 這個好處就是你能夠繼續上次的迭代,萬一斷電了仍是什麼的模型也仍是都保存了下來。

  固然,咱們還須要在 tools.py 開頭添加:

import os.path as osp
this_dir = osp.dirname(__file__)

  這時,在 mydemo.py 文件中,最下行代碼應該是這樣的:

if __name__ == '__main__':
    
    make_net()
    
    # 定義生成 solver 的路徑
    solver_dir = this_dir + '/res_net_model/res_net_solver.prototxt'
    solver_prototxt = tools.CaffeSolver()
    # 把內容寫入 res_net_model 文件夾中的 res_net_solver.prototxt
    solver_prototxt.write(solver_dir)

  這樣執行 mydemo.py 後生成 solver 的 prototxt 文件就是按照上面的設置生成的,不過要記得生成後把 prototxt 中 stepvalue1 改爲 stepvalue,由於 tool.py 不能存相同名字的參數,否則會覆蓋掉。

  這樣,咱們就生成了 solver 的 prototxt 文件。

相關文章
相關標籤/搜索