把Caffe的模型轉換爲Pytorch模型

系列博客目錄:Caffe轉Pytorch模型系列教程 概述

本文用的Caffe網絡模型文件:SfSNet_deploy.prototxt(右鍵另存爲)。這是一個很牛逼的模型,SfSNet_deploy.prototxt的定義也很長,本文就以它(SfSNet)爲例,說明如何把Caffe網絡手動轉換爲Pytorch網絡。轉換後的Pytorch代碼地址:model.py(右鍵另存爲)。

一、可視化Caffe的網絡模型

可視化網絡模型的好處是可以直觀地看到網絡模型,對網絡模型能先有個大概的瞭解,再結合.prototxt文件,就能完整無誤的把Caffe模型轉換爲Pytorch模型。在線網站要比本地可視化直觀準確得多。在後面的實現過程中,請大家結合結合.prototxt和可視化結果來轉換模型,很重要。

1、在線網站

打開網站http://ethereon.github.io/netscope/#/editor之後,把SfSNet_deploy.prototxt的內容粘貼到編輯框裏,然後按Shift+Enter,右邊的框就會出現網絡模型。鼠標移動到右邊的方塊上時,會顯示每層的參數。
在這裏插入圖片描述

2、本地可視化

請參考:https://www.cnblogs.com/denny402/p/5106764.html

二、SfSNet的第一部分

在這裏插入圖片描述
第一部分的網絡結構圖。Caffe模型中所有的操作都定義爲層。

1、網絡名

打開SfSNet_deploy.prototxt中,第一行是:

name : "PS-Net"

指定此網絡的名稱是"PS-Net"。

2、輸入層

#data
layer {
  name: "data"  # 名稱,使用Caffe的Python接口時會用到這個。
  type: "Input" # 類型,爲輸入
  top: "data"   # “頂”,暫時可以理解爲數據流向。
    input_param { shape: { dim: 1 dim: 3 dim: 128 dim: 128 } }
}

此部分代碼的輸入層:shape=(1, 3, 128, 128)。 “#”後面是我添加的註釋,之後也是如此。

3、第一個卷積層conv1

#C64
layer {
  name: "conv1" 		# 層名
  type: "Convolution" 	# 類型:卷積
  bottom: "data" 		# 數據輸入:data
  top: "conv1" 			# 數據輸出:conv1
  param { 				# 權重 weight
  name : "c1_w" 			# 權重名
    lr_mult: 1 				# 權重學習率
    decay_mult: 1 			# 衰減係數
  }
  param {		 		# 偏置 bias
  name : "c1_b" 			# 偏置名
    lr_mult: 2 				# 偏置學習率
    decay_mult: 0 			# 衰減係數
  }
  convolution_param {	# 卷積層參數
    num_output: 64			# 卷積核個數,對應torch.nn.Conv2d的out_channels
    kernel_size: 7      	# 卷積核大小
    stride: 1 				# 步長大小
    pad: 3 					# padding大小
    weight_filler { 	# 權重初始化器參數
      type: "xavier" 		# 權重初始化器類型
    }
  }
}

以上就是一個卷積層的定義。Pytorch中,卷積層是:

Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

in_channels是輸入卷積層的通道數,也就是data層中的“3”;out_channels對應num_output,也就是卷積核的個數:“64”;其他的就是一一對應。在Pytorch中,conv1就定義爲:

conv1 = nn.Conv2d(3, 64, 7, 1, 3)

4、第一個BN層bn1

下圖是Pytorch中BatchNorm完成的工作,“*”號左邊的部分是使用E[x](均值)和Var[x](方差) 歸一化x,乘以γ加上β是縮放x和平移x。Caffe中的BatchNorm層只完成了歸一化x的工作,歸一化由Scale層完成,所以:BatchNorm(Pytorch)= BatchNorm(Caffe)+ Scale(Caffe)。
在這裏插入圖片描述
SfSNet只是使用了BatchNorm層,沒有使用Scale層。所以使用Pytorch的BatchNorm層來替換Caffe中的層時,需要把nn.BatchNorm參數affine設置爲True,且要把BatchNorm層的weight(相當於γ)設置爲全1,bias(相當於β)設置爲全0。 至於如何設置,將會在下一篇博客給出,本文只關注如何轉換模型。給出bn1的.prototxt定義:

layer {
  name: "bn1" 		# 層名
  type: "BatchNorm" # 類型:BatchNorm
  bottom: "conv1" 	# 數據輸入:conv1
  top: "conv1" 		# 數據輸出:conv1
  batch_norm_param {
    use_global_stats: false # 訓練時爲false,不使用保存的E[x]和Var[x]
  }
  param {
	name : "b1_a"
    lr_mult: 0
  }
  param {
	name: "b1_b"
    lr_mult: 0
  }
  param {
	name: "b1_c"
    lr_mult: 0
  }
  include {
    phase: TRAIN # 訓練時包含此層
  }
}

layer {
  name: "bn1"
  type: "BatchNorm"
  bottom: "conv1"
  top: "conv1"
  batch_norm_param {
    use_global_stats: true # 測試模型是使用保存的E[x]和Var[x]
  }
  param {
    name : "b1_a"
    lr_mult: 0
  }
  param {
    name : "b1_b"
    lr_mult: 0
  }
  param {
    name: "b1_c"
    lr_mult: 0
  }
  include {
    phase: TEST # 測試時包含此層
  }
}

bn1的輸入和輸出均爲conv1,所以bn1是“In-Place”操作,把修改完的數據又存回了conv1。 可以發現,bn1層被定義了兩次,主要差別是參數use_global_stats和phase。第一個定義是在訓練網絡(phase:TRAIN)時使用,第二個定義是在測試時(phase:TEST)時使用。Caffe的BatchNorm對應Pytorch中的BatchNorm2d,定義如下:

BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

num_features是特徵數量,數值和Conv2d的out_channels相同;eps是ϵ;affine默認爲True,不要修改;track_running_stats=True 保持默認。所以bn1的對應的pytorch定義爲:

bn1 = nn.BatchNorm2d(64)

64對應out_channels。

5、第一個Relu層relu1

layer {
  name: "relu1" 	# 層名
  type: "ReLU"  	# 類型:relu
  bottom: "conv1" 	# 輸入:conv1
  top: "conv1" 		# 輸出:conv1
}

這是一個ReLU層的定義,也是一個in-Place層, 輸入輸出均爲conv1。在Pytorch中,可以直接使用:

torch.nn.functional.relu(conv1)

6、第一部分的完整Pytorch代碼

上面只是簡單的介紹了三個層,實際上只完成了:
在這裏插入圖片描述
這一小部分。但是我不再重複舉例說明,而是直接給出第一部分模型的代碼,相信讀者可以舉一反三,完成第一部分模型的轉換。請讀者先自行完成conv1到conv3的Pytorch代碼,再和下面的比對。

# coding=utf-8
from __future__ import absolute_import, division, print_function
import torch
import torchvision
import pickle as pkl
from torch import nn
import torch.nn.functional as F

class SfSNet(nn.Module):  # SfSNet = PS-Net in SfSNet_deploy.prototxt
    def __init__(self):
        # C64
        super(SfSNet, self).__init__()
        # TODO 初始化器 xavier
        self.conv1 = nn.Conv2d(3, 64, 7, 1, 3)
        self.bn1 = nn.BatchNorm2d(64)
        # C128
        self.conv2 = nn.Conv2d(64, 128, 3, 1, 1)
        self.bn2 = nn.BatchNorm2d(128)
        # C128 S2
        self.conv3 = nn.Conv2d(128, 128, 3, 2, 1)

    def forward(self, inputs):
        # C64
        x = F.relu(self.bn1(self.conv1(inputs)))
        # C128
        x = F.relu(self.bn2(self.conv2(x)))
        # C128 S2
        conv3 = self.conv3(x)

二、SfSNet的殘差塊

觀察網絡的可視化結果,可以發現有很多重複的結構(下面框框裏面包着的),SfSNet的作者說它們是殘差塊(ResidualBlock),其實還是有丟丟區別的,不過不影響我們轉換模型。這些重複的結構層數相同,參數相同,就是名字不一樣,因此我把每個框框裏面的結構實現爲一個“ResidualBlock”,以便於重複利用,還能減少出錯。
在這裏插入圖片描述

1、Caffe的Eltwise層

layer {
  name: "nsum1" 	# 層名
  type: "Eltwise" 	# 類型:Eltwise
  bottom: "nconv1r" # 輸入1:nconv1r
  bottom: "conv3" 	# 輸入2:conv3
  top: "nsum1" 		# 輸出:nsum1
  eltwise_param { 	# 操作類型
    operation: SUM 		# 求和
  }
}

顧名思義,Eltwise層就是逐元素的操作(相加、相減等等)。operation指定了類型是SUM(求和)。所以,此層就是將nconv1r和conv3求和,存到nsum1裏面。

2、實現一個“ResidualBlock”

直接上代碼:

class ResidualBlock(nn.Module):
    def __init__(self, in_channel, out_channel):
        super(ResidualBlock, self).__init__()
        # nbn1/nbn2/.../nbn5 abn1/abn2/.../abn5
        self.bn = nn.BatchNorm2d(in_channel)
        # nconv1/nconv2/.../nconv5 aconv1/aconv2/.../aconv5
        self.conv = nn.Conv2d(in_channel, out_channel, kernel_size=3, stride=1, padding=1)
        # nbn1r/nbn2r/.../nbn5r abn1r/abn2r/.../abn5r
        self.bnr = nn.BatchNorm2d(out_channel)
        # nconv1r/nconv2r/.../nconv5r aconv1r/aconv2r/.../anconv5r
        self.convr = nn.Conv2d(out_channel, out_channel, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        out = self.conv(F.relu(self.bn(x)))
        out = self.convr(F.relu(self.bnr(out)))
        # num1/nsum2/.../nsum5 aum1/asum2/.../asum5
        out += x 
        return out

上面就是一個框框內的所有層,都定義在一個ResidualBlock內,並且可以重複使用。Eltwise層使用“+=”替代了,ReLU層使用“F.relu”替代了。接下來就舉個例子,說明ResidualBlock怎麼用:

class SfSNet(nn.Module):  # SfSNet = PS-Net in SfSNet_deploy.prototxt
    def __init__(self):
        # C64
        super(SfSNet, self).__init__()
        # TODO 初始化器 xavier
        self.conv1 = nn.Conv2d(3, 64, 7, 1, 3)
        self.bn1 = nn.BatchNorm2d(64)
        # C128
        self.conv2 = nn.Conv2d(64, 128, 3, 1, 1)
        self.bn2 = nn.BatchNorm2d(128)
        # C128 S2
        self.conv3 = nn.Conv2d(128, 128, 3, 2, 1)
        # ------------RESNET for normals------------
        # RES1
        self.n_res1 = ResidualBlock(128, 128)
        # RES2
        self.n_res2 = ResidualBlock(128, 128)

    def forward(self, inputs):
        # C64
        x = F.relu(self.bn1(self.conv1(inputs)))
        # C128
        x = F.relu(self.bn2(self.conv2(x)))
        # C128 S2
        conv3 = self.conv3(x)
        # ------------RESNET for normals------------
        # RES1
        x = self.n_res1(conv3)
        # RES2
        x = self.n_res2(x)
        return x

三、反捲積層、Concat層、池化層以及全連接層

1、反捲積層

SfSNet中,還用到了反捲積層,分別爲nup6和aup6。
在這裏插入圖片描述
在.prototxt中,一個反捲積層定義爲:

#CD128
layer {
  name: "nup6" 			# 層名
  type: "Deconvolution" # 類型:Deconvolution
  bottom: "nsum5" 		# 輸入:nsum5
  top: "nup6" 			# 輸出:nup6
  convolution_param { 	# 卷積參數
    kernel_size: 4 			# 卷積核大小
    stride: 2 				# 步長
    num_output: 128 		# 輸出特徵數
    group: 128 				# group大小,不理解是啥意思,不過Pytorch中的反捲積層有同名參數,不慌,小場面
    pad: 1 					# padding大小
    weight_filler {  		# 權重初始化參數
       type: "bilinear"  		# 權重初始化器類型
    } 
    bias_term: false 		# 使用偏置:false爲不使用
  }
  param { 				# 權重學習參數
    lr_mult: 0 				# 學習率:0
    decay_mult: 0 			# 學習率衰減率:0
  }
}

Pytorch中,反捲積層的定義爲:

ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1)

in_channels爲輸入通道數,在SfSNet中是128;out_channels爲輸出通道數,也爲128;groups對應.prototxt中的“group: 128 ”;bias要設置爲false,因爲“bias_term: false”,其他參數保持默認,所以,nup6的Pytorch代碼爲:

aup6 = nn.ConvTranspose2d(128, 128, 4, 2, 1, groups=128, bias=False)

2、Concat層

#concat
layer {
    name: "lconcat1" 	# 層名
    bottom: "nsum5"  	# 輸入1:nsum5
    bottom: "asum5" 	# 輸入2:asum5
    top: "lconcat1" 	# 輸出:lconcat1
    type: "Concat" 		# 類型:lconcat1
    concat_param { 		# Concat參數
        axis: 1 		# 軸:連接(N, C, H, W)第二維,也就是C(channel)。當axis=0是,連接N,也就是一批的數量
    }
}

Pytorch裏和Caffe中的Concat層對應的操作是:

torch.cat(tensors, dim=0, out=None) → Tensor

tensors是Tenor組成的列表;dim對應axis;out保持默認。所以,lconcat1對應的Python代碼就是:

lconcat1 = torch.cat((nsum5, asum5), 1)

感覺沒毛病吧?大錯特錯!!!,讓我們再次打開網絡可視化之後的結果:
在這裏插入圖片描述
可以看到,lconcat1的兩個輸入明明是nrelu6r和arelu6r啊,難道是可視化結果錯了?爲啥.prototxt文件中寫成num5和asum5呢?因爲nbn6r/abn6r和nrelu6r/arelu6r都是“In-Place”操作(輸入輸出都是num5/asum5),計算完之後又存回num5/asum5了,所以.prototxt文件寫成num5和asum5,實際應爲nrelu6r/arelu6r處理後的結果。所以,lconcat1的Pytorch代碼應爲:

lconcat1 = torch.cat((nrelu6r, arelu6r), 1)

需要特別注意那種“In-Place”操作,要結合可視化圖和.prototxt文件來完成模型的轉換,免得走彎路啊啊啊啊啊。

3、池化層

layer {
    name: "lpool2r" 	# 層名
    type: "Pooling" 	# 類型:Pooling
    bottom: "lconv1" 	# 輸入:lconv1
    top: "lpool2r" 		# 輸出:lpool2r
    pooling_param { 	# 池化層參數
        pool: AVE 			# 池化類型:AVE,對應AvgPool2d;還有MAX,對應MaxPool2d.
        kernel_size: 64 	# 池化核大小
    }
}

上面就是池化層的定義。在Pytorch中,上述池化層的對應:

AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True)

kernel_size是池化核大小;其他參數不做說明。lpool2r對應的Pytorch代碼爲:

lpool2r = nn.AvgPool2d(64)

如果“pool: AVE”裏的“AVE”換成“MAX”,那麼lpool2r對應的Pytorch代碼爲:

lpool2r = nn.MaxPool2d(64)

4、全連接層

layer {
    name: "fc_light" # 層明
    type: "InnerProduct" 	# 類型:InnerProduct,也就是全連接層
    bottom: "lpool2r" 		# 輸入:lpool2r
    top: "fc_light" 		# 輸出:fc_light
    param { 				# 權重參數
        lr_mult: 1
        decay_mult: 1
    }
    param { 				# 偏置參數
        lr_mult: 2
        decay_mult: 0
    }
    inner_product_param { 	# 全連接層參數
	    num_output: 27 			# 輸出通道數:27
	    weight_filler { 		# 權重初始化器參數
	        type: "gaussian" 		# 類型:gaussian
	        std: 0.005 				# 標準差:0.005
	    }
	    bias_filler { 			# 偏置初始化參數
	        type: "constant" 		# 類型:constant
	        value: 1		 		# 值:1
	    }
    }
}

越寫到後面越不想寫,這篇博客都編輯了五六個小時了(哭,還是堅持寫完吧)。上面就是全連接層fc_light的定義,在Pytorch中,對應的層是:

Linear(in_features, out_features, bias=True)

fc_light對應的Pytorch代碼爲:

fc_light = nn.Linear(128, 27)

等等!!!fc_light前面不是一個lpool2r層嗎?
在這裏插入圖片描述
lpool2r層的輸出形狀是(1, 128, 1, 1),也就是四維啊,全連接層前面應該是一維的啊,所以在Pytorch的模型的forward函數裏,還需要reshape(調整爲[1, 128]) lpool2r的結果:

class SfSNet(nn.Module):
	def __init__(self):
		...
	def forward(self, inputs):
		...
		x = self.lpool2r(x)
		# 調整輸出的shape
        x = x.view(-1, 128)
        # fc_light
        light = self.fc_light(x)

四、總結

  • 不懂的多查多看!
  • 轉換模型要結合.prototxt和可視化結果。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章