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


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

Layer_name

Output_size

20-layer ResNet

Conv1

32 X 32

Kernel_size=3 X 3

Num_output = 16

Stride = 1

Pad = 1

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 文件。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM