從.caffemodel/.caffemodel.h5提取Caffe模型的參數

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

把Caffe模型轉換爲其他框架下的模型很關鍵的一步是從.caffemodel/.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_namelen(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保存的兩個參數纔是對應的。
    BatchNorm公式
  • 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中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章