系列博客目錄:Caffe轉Pytorch模型系列教程 概述
目錄
把Caffe模型轉換爲其他框架下的模型很關鍵的一步是從.caffemodel/.caffemodel.h5提取出網路的參數,本文將介紹兩種方法提取網絡的參數。
- 本文用的Caffe網絡模型文件:SfSNet_deploy.prototxt(右鍵另存爲)。
- SfSNet的Pytorch代碼地址:model.py(右鍵另存爲,來源於上一篇博客)。
- SfSNet的權重:SfSNet.caffemodel.h5(右鍵另存爲)。
一、通用的提取參數方法
1、編譯Caffe
此方法需要編譯Caffe和Caffe的pycaffe模塊。編譯教程:
講真, Windows編譯Caffe真的困難,我當時搞了好久。
2、打印.caffemodel的網絡參數
此種方法適用於.prototxt和.caffemodel/.caffemodel.h5都可以獲取的模型。
# coding=utf8
from __future__ import absolute_import, division, print_function
import caffe
# prototxt文件
MODEL_FILE = 'SfSNet_deploy.prototxt'
# 預先訓練好的caffe模型
PRETRAIN_FILE = 'SfSNet.caffemodel.h5'
if __name__ == '__main__':
# 導入網絡
net = caffe.Net(MODEL_FILE, PRETRAIN_FILE, caffe.TEST)
print('*' * 80)
# 遍歷每一網絡層
for param_name in net.params.keys():
# 得到此層的參數
layer_params = net.params[param_name]
if len(layer_params) == 0:
# 如果參數只有一組,則說明是反捲積層。
# SfSNet整個模型裏就只有反捲積層只有一組weight參數
weight = layer_params[0].data
print('%s:\n\t%s (weight)' % (param_name, weight.shape))
elif len(layer_params) == 2:
# 如果參數有兩個,則說明是卷積層或者全連接層。
# 卷積層或者全連接層都有兩組參數:weight和bias
weight = layer_params[0].data # 權重參數
bias = layer_params[1].data # 偏置參數
print('%s:\n\t%s (weight)' % (param_name, weight.shape))
print('\t%s (bias)' % str(bias.shape))
elif len(layer_params) == 3:
# 如果有三個,則說明是BatchNorm層。
# BN層共有三個參數,分別是:running_mean、running_var和一個縮放參數。
running_mean = layer_params[0].data # running_mean
running_var = layer_params[1].data # running_var
print('%s:\n\t%s (running_var)' % (param_name, running_var.shape), )
print('\t%s (running_mean)' % str(running_mean.shape))
else:
# 如果報錯,大家要檢查自己模型哈
raise RuntimeError("還有參數個數超過3個的層,別漏了兄dei!!!\n")
代碼不難,就是很簡單的遍歷,難點在於如何確定每層有幾個參數。在代碼中,我直接根據len(layer_params)
的個數來判斷是什麼層,實際上我也是根據打印param_name
和len(layer_params)
來判斷個層的len(layer_params)
的大小的,希望讀者能舉一反三,若模型有其他類型的層,需要讀者自己取判斷此層的參數個數(包括寫代碼驗證和百度)。
3、保存.caffemodel的網絡參數
保存網絡參數的思路非常簡單,就是把提取出來的參數保存到一個dict裏,然後在使用pickle保存。在Pytorch中,再使用pickle來讀取(可自行查閱pickle的是幹啥的)。直接上代碼:
# coding=utf8
from __future__ import absolute_import, division, print_function
import pickle as pkl
import caffe
# prototxt文件
MODEL_FILE = 'SfSNet_deploy.prototxt'
# 預先訓練好的caffe模型
PRETRAIN_FILE = 'SfSNet.caffemodel.h5'
if __name__ == '__main__':
# 導入網絡
net = caffe.Net(MODEL_FILE, PRETRAIN_FILE, caffe.TEST)
print('*' * 80)
# 名字和權重詞典
name_weights = {}
# 保存每層的參數信息
keys = open('keys.txt', 'w')
keys.write('generated by SfSNet-Caffe/convert_to_pkl.py\n\n')
# 遍歷每一網絡層
for param_name in net.params.keys():
name_weights[param_name] = {}
# 得到此層的參數
layer_params = net.params[param_name]
if len(layer_params) == 1:
# 如果參數只有一個,則說明是反捲積層,
# SfSNet整個模型裏就只有反捲積層只有一組weight參數
weight = layer_params[0].data
name_weights[param_name]['weight'] = weight
print('%s:\n\t%s (weight)' % (param_name, weight.shape))
keys.write('%s:\n\t%s (weight)\n' % (param_name, weight.shape))
elif len(layer_params) == 2:
# 如果參數有兩個,則說明是卷積層或者全連接層。
# 卷積層或者全連接層都有兩組參數:weight和bias
# 權重參數
weight = layer_params[0].data
name_weights[param_name]['weight'] = weight
# 偏置參數
bias = layer_params[1].data
name_weights[param_name]['bias'] = bias
print('%s:\n\t%s (weight)' % (param_name, weight.shape))
print('\t%s (bias)' % str(bias.shape))
keys.write('%s:\n\t%s (weight)\n' % (param_name, weight.shape))
keys.write('\t%s (bias)\n' % str(bias.shape))
elif len(layer_params) == 3:
# 如果有三個,則說明是BatchNorm層。
# BN層共有三個參數,分別是:running_mean、running_var和一個縮放參數。
running_mean = layer_params[0].data # running_mean
name_weights[param_name]['running_mean'] = running_mean / layer_params[2].data
running_var = layer_params[1].data # running_var
name_weights[param_name]['running_var'] = running_var/layer_params[2].data
print('%s:\n\t%s (running_var)' % (param_name, running_var.shape),)
print('\t%s (running_mean)' % str(running_mean.shape))
keys.write('%s:\n\t%s (running_var)\n' % (param_name, running_var.shape))
keys.write('\t%s (running_mean)\n' % str(running_mean.shape))
else:
# 如果報錯,大家要檢查自己模型哈
raise RuntimeError("還有參數個數超過3個的層,別漏了兄dei!!!\n")
keys.close()
# 保存name_weights
with open('weights.pkl', 'wb') as f:
pkl.dump(name_weights, f, protocol=2)
補充說明:
- 至於layer_params[0]爲什麼是weight/running_mean,layer_params[1]爲什麼是bias/running_var,這是根據寫代碼實驗確定的(根據shape區分應該是weight還是bias。running_mean和running_var的shape相同,這兩個是通過把提取的參數載入Pytorch網絡之後,對比兩個網絡的輸出是否相同確定的,後面會講到)。
- 至於running_mean和running_var爲什麼要除以layer_params[2].data,請參考:torch和caffe中的BatchNorm層。
- 把提取到的參數存到了weights.pkl中。
二、提取.caffemodel.h5的參數
我遇到的網絡模型權重的後綴是.caffemodel.h5,是一個h5文件,所以可以使用h5py來讀取。參考:python庫——h5py讀取h5文件。
1、使用h5py打印.caffemodel.h5的參數
# coding=utf8
from __future__ import absolute_import, division, print_function
import h5py
if __name__ == '__main__':
f = h5py.File('SfSNet.caffemodel.h5', 'r')
for group_name in f.keys():
# print(group)
# 根據一級組名獲得其下面的組
group = f[group_name]
for sub_group_name in group.keys():
# print('----'+subgroup)
# 根據一級組和二級組名獲取其下面的dataset
dataset = f[group_name + '/' + sub_group_name]
# 遍歷該子組下所有的dataset
for dset in dataset.keys():
# 獲取dataset數據
sub_dataset = f[group_name + '/' + sub_group_name + '/' + dset]
data = sub_dataset[()]
print(sub_dataset.name, data.shape)
2、使用h5py保存.caffemodel.h5的參數
代碼簡單,直接上代碼:
# coding=utf8
from __future__ import absolute_import, division, print_function
import h5py
import pickle as pkl
if __name__ == '__main__':
f = h5py.File('SfSNet.caffemodel.h5', 'r')
for group_name in f.keys():
# print(group_name)
# 根據一級組名獲得其下面的組
name_weights = {}
group = f[group_name]
for sub_group_name in group.keys():
# print('----'+sub_group_name)
if sub_group_name not in name_weights.keys():
name_weights[sub_group_name] = {}
# 根據一級組和二級組名獲取其下面的dataset
# 經過實驗,一個dataset對應一層的參數
dataset = f[group_name + '/' + sub_group_name]
# 遍歷該子組下所有的dataset。
# print(dataset.keys())
if len(dataset.keys()) == 1:
# 如果參數只有一個,則說明是反捲積層,
# SfSNet整個模型裏就只有反捲積層只有一組weight參數
weight = dataset['0'][()]
name_weights[sub_group_name]['weight'] = weight
print('%s:\n\t%s (weight)' % (sub_group_name, weight.shape))
elif len(dataset.keys()) == 2:
# 如果參數有兩個,則說明是卷積層或者全連接層。
# 卷積層或者全連接層都有兩組參數:weight和bias
weight = dataset['0'][()] # 權重參數
# print(type(weight))
# print(weight.shape)
name_weights[sub_group_name]['weight'] = weight
bias = dataset['1'][()] # 偏置參數
name_weights[sub_group_name]['bias'] = bias
print('%s:\n\t%s (weight)' % (sub_group_name, weight.shape))
print('\t%s (bias)' % str(bias.shape))
elif len(dataset.keys()) == 3:
# 如果有三個,則說明是BatchNorm層。
# BN層共有三個參數,分別是:running_mean、running_var和一個縮放參數。
running_mean = dataset['0'][()] # running_mean
name_weights[sub_group_name]['running_mean'] = running_mean / dataset['2'][()]
running_var = dataset['1'][()] # running_var
name_weights[sub_group_name]['running_var'] = running_var / dataset['2'][()]
print('%s:\n\t%s (running_var)' % (sub_group_name, running_var.shape), )
print('\t%s (running_mean)' % str(running_mean.shape))
elif len(dataset.keys()) == 0:
# 沒有參數
continue
else:
# 如果報錯,大家要檢查自己模型哈
raise RuntimeError("還有三叔個數超過3個的層,別漏了兄dei!!!\n")
with open('weights1.pkl', 'wb') as f:
pkl.dump(name_weights, f, protocol=2)
補充說明(先看代碼再來看):
- 一個dataset存着一層的參數。參數的個數和key可以打印
dataset.keys()
得知。 - 從dataset裏提取key對應的值可以使用,dataset[key][()]這種語法。
- 把參數存到了weights1.pkl中。
三、在Pytorch中載入保存的參數
1、打印Pytorch網絡的參數
知己知彼,百戰不殆。要想把Caffe的權重載入到Pytorch模型裏,首先就是要確認兩個網絡的結構層次是否一樣啊對不對。我們先要看下轉換後的模型的層以及參數大小。首先下載model.py,然後新建model-test.py,輸入如下代碼:
# coding=utf-8
from __future__ import absolute_import, division, print_function
from src.models.model import SfSNet
if __name__ == '__main__':
net = SfSNet()
net.eval()
index = 0
for name, param in list(net.named_parameters()):
print(str(index) + ':', name, param.size())
index += 1
運行,得到輸出結果(爲了節省篇幅,並不給出全部的運行結果):
0: conv1.weight (64, 3, 7, 7)
1: conv1.bias (64,)
2: bn1.weight (64,)
3: bn1.bias (64,)
...
10: n_res1.bn.weight (128,)
11: n_res1.bn.bias (128,)
12: n_res1.conv.weight (128, 128, 3, 3)
13: n_res1.conv.bias (128,)
14: n_res1.bnr.weight (128,)
15: n_res1.bnr.bias (128,)
16: n_res1.convr.weight (128, 128, 3, 3)
17: n_res1.convr.bias (128,)
...
50: nbn6r.weight (128,)
51: nbn6r.bias (128,)
52: nup6.weight (128, 1, 4, 4)
53: nconv6.weight (128, 128, 1, 1)
54: nconv6.bias (128,)
...
120: fc_light.weight (27, 128)
121: fc_light.bias (27,)
對上面的結果進行簡單的分析:
- conv1是第一個卷積層。conv1.weight是conv1的權重,大小是(64, 3, 7, 7):64個濾波器(out_channel=64),每個濾波器的大小是 (3, 7, 7);conv1.bias (64,)是conv1的偏置,大小是(64,),對應的Pytorch代碼爲:
self.conv1 = nn.Conv2d(3, 64, 7, 1, 3)
。 - bn1是第一個BatchNorm層。bn1.weight是γ,bn1.bias是β,這兩個參數和Caffe的Scale層的兩個參數是對應的;還有兩個重要的參數:bn1.running_mean和bn1.running_var沒有被打印出來(或許它們不配擁有姓名?),這兩個參數和.caffemodel.h5保存的兩個參數纔是對應的。
- n_res1第一個殘差塊。可以看到包含兩個BN(BatchNorm)層和兩個卷積層。
- nup6是第一個反捲積層。它只有一個nup6.weight參數,因爲在SfSNet中,設置了不使用bias。
- fc_light是第一個全連接層。
- 還有些其他的層,比如EltWise、Concat層和Pooling層都沒有參數,自然不會被打印出來。
2、Pytorch載入自定義的參數
請參考:PyTorch之保存加載模型。文中提到了一個函數nn.Module.load_state_dict,這個函數就是pytorch模型載入參數的函數。load_state_dict的原型如下:
load_state_dict(state_dict, strict=True)
state_dict是一個字典,包含了網絡的參數。比如conv1的weight是數組arr1,那麼state_dict[‘conv1.weight’]==arr1;conv1的bias是數組arr2,那麼state_dict[‘conv1.bias’]==arr2。依次類推,state_dict應當包含Pytorch模型中所有層的參數,不能多也不能少。
那麼,要載入我們從.caffemodel中提取的參數,就構造一個state_dict,其中包含了所有的層的參數。
2.1 設置conv1的weight和bias
首先從weights.pkl載入name_weights;然後初始化一個字典state_dict。
from torch import from_numpy
with open('weights.pkl', 'rb') as wp:
name_weights = pkl.load(wp)
state_dict = {}
然後:
state_dict['conv1.weight'] = from_numpy(name_weights['conv1']['weight'])
state_dict['conv1.bias'] = from_numpy(name_weights['conv1']['bias'])
‘conv1.weight’和’conv1.bias’是第1步打印出來的參數,這兩個key對應的值就會被分別載入到conv1的weight和bias裏面。torch.from_numpy
是從numpy數組生成Tensor。name_weights裏面就是存的提取出來的權重,假如這層的名字是’conv1’,這層的參數就存在name_weights['conv1']
裏面,也是一個字典。
2.2 設置bn1的參數
state_dict['bn1.running_var'] = from_numpy(name_weights[key]['running_var'])
state_dict['bn1.running_mean'] = from_numpy(name_weights[key]['running_mean'])
state_dict['bn1.weight'] = torch.ones_like(state_dict[layer + '.running_var'])
state_dict['bn1.bias'] = torch.zeros_like(state_dict[layer + '.running_var'])
bn1是一個BatchNorm2d層,原型聲明爲:
BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
參數afline保持默認爲true,track_running_stats也要保持爲true。
- affine=True, track_running_stats=True:需要全部四組參數running_var、running_mean、weight和bias。running_var和running_mean要從name_weights獲取;weight要設置爲全1;bias要設置爲全0。
- affine=False, track_running_stats=True:需要兩組參數running_var、running_mean。weight會被BatchNorm2d層自動設置爲0~1的隨機數(這個特性挺坑爹的);bias會被初始化爲0。
2.3 設置殘差塊的參數
SfSNet中重複的結構,所以我把它們改成了一個殘差塊。爲了避免寫很多重複的代碼,把設置殘差塊的代碼封裝成函數,以便重複使用。
def _set(layer, key):
state_dict[layer + '.weight'] = from_numpy(name_weights[key]['weight'])
state_dict[layer + '.bias'] = from_numpy(name_weights[key]['bias'])
def _set_bn(layer, key):
state_dict[layer + '.running_var'] = from_numpy(name_weights[key]['running_var'])
state_dict[layer + '.running_mean'] = from_numpy(name_weights[key]['running_mean'])
state_dict[layer + '.weight'] = torch.ones_like(state_dict[layer + '.running_var'])
state_dict[layer + '.bias'] = torch.zeros_like(state_dict[layer + '.running_var'])
def _set_res(layer, n_or_a, index):
_set_bn(layer+'.bn', n_or_a + 'bn' + str(index))
_set(layer+'.conv', n_or_a + 'conv' + str(index))
_set_bn(layer+'.bnr', n_or_a + 'bn' + str(index) + 'r')
_set(layer+'.convr', n_or_a + 'conv' + str(index) + 'r')
_set_res
就是設置一個殘差塊的函數。參數:第一個參數layer是殘差層的名字,第二個參數n_or_a指定
- layer: 指定殘差層的名字。
- n_or_a指定殘差塊的前綴,取值爲’n’或者’a’。
- index: 指定是第幾個殘差塊。
那麼設置第一個殘差塊n_res1/a_res1參數的代碼就是:
_set_res('n_res1', 'n', 1)
...
_set_res('a_res1', 'a', 1)
2.4 設置nup6的參數
state_dict['nup6.weight'] = from_numpy(name_weights[key]['weight'])
nup6是一個反捲積層,在SfSNet中bias項被關閉,所以只有一組參數weight。
2.5 封裝
其他層的參數設置就不多說了,等所有參數都設置完成之後,調用nn.Module.load_state_dict
函數載入參數:
net.load_state_dict(state_dict)
由於構造state_dict是個非常依賴於模型的過程,所以我把它封裝成了一個函數:load_weights_from_pkl
。完整的代碼在:model.py中。