上面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)
這樣,咱們就寫好了中間的卷積部分,這時候咱們最後一層的輸出是 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。
在 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 文件。