tensorflow 和 caffe 都是常见的深度学习框架,有时候前端部署会因为平台的要求只能用其中的某种框架,这个时候则需要进行框架间的转换。本博客会介绍 tensorflow 转 caffe 模型的相关细节和部分相应的代码。
我做了简单的查阅,发现主要是下面三种:
1) 利用 net.params 逐层添加参数
2) prototxt 中逐层添加参数后编译成 caffemodel
我阅读了大佬的专栏,学习良多(地址:tensorflow2caffe),但我发现第一种方法似乎更加方便,所以这种方法未尝试。
3) 微软的 MMdnn
没有深入了解,就不介绍了。
需要 .meta 文件(得到图结构)、.model文件(得到参数)和 checkpoint 文件(一般和.model 文件在一个目录下,得到最新的那个模型)。
需要 prototxt 文件(网络结构)和 caffemodel文件(网络结构和参数),两者通过网络结构的 index 匹配。
我在转换的时候主要用到卷积和反卷积,其他结构差异性没有比较。差异如下表:
类型 | tensorflow | caffe |
卷积 | h, w, in, out | out, in ,h, w |
反卷积 | h, w, out, in | in, out, h, w |
输入数据 | n, h, w, c | n, c ,h ,w |
卷积(反卷积)是否包含激活函数 | 是 | 否 |
两个框架中的卷积和反卷积的 in 和 out 是相反的,但是在矩阵变换的时候和卷积的通道设置是一样的。
我采用 1) 中的方法。一般要求 同时部署 tensorflow 和 caffe 环境。由于我的笔记本只部署了 caffe 环境,tf 在服务器上,所以我会先把 tf 的参数提取出来并保存。
对于卷积和反卷积,参数均包括权重和偏置。提取后,将其保存为一维矩阵的文本(注意参数矩阵的形状变换),代码如下:
import tensorflow as tf
import numpy as np
with tf.Session() as sess:
new_saver = tf.train.import_meta_graph('/xxx.model.meta')
for var in tf.trainable_variables():
print(var.name)
new_saver.restore(sess, tf.train.latest_checkpoint('/checkpoints57/'))
all_vars = tf.trainable_variables()
for v in all_vars:
name = v.name
fname = name + '.prototxt'
fname = fname.replace('/','_')
print(fname)
v_4d = np.array(sess.run(v))
if v_4d.ndim == 4:
#v_4d.shape [ H, W, I, O ]
v_4d = np.transpose(v_4d, [3,2,0,1])
#v_4d.shape [ O, I, H, W ]
f = open('./prototxt/' + fname, 'w')
v_1d = v_4d.reshape(v_4d.shape[0]*v_4d.shape[1]*v_4d.shape[2]*v_4d.shape[3])
for vv in v_1d:
f.write('%8f' % vv)
f.write('\n')
elif v_4d.ndim == 1 :#do not swap
f = open('./prototxt/' + '_' + fname, 'w')
for vv in v_4d:
f.write('%.8f' % vv)
f.write('\n')
f.close()
caffe 的网络结构可以用 pycaffe 写,也可以自己在网页端构建。我选择在网页端构建:http://ethereon.github.io/netscope/#/editor。打开这个网页,左边可以自己写结构,shift+enter 在右边生成可视化结构,方便排查结构问题。
可能用到的 caffe 层(注意,caffe 中的命名一定要和 tf 中完全一致,便于参数 index 匹配):
输入数据层:
layer{
name: "data"
type: "Input"
top: "data"
input_param {
shape:{dim:1 # 输出
dim:6 # 输入
dim:512 # 输入尺寸 h
dim:512 # 输入尺寸 w
}
}
}
卷积层:
layer{
name: "enc1_1"
type: "Convolution"
bottom: "data"
top: "enc1_1"
convolution_param {
num_output: 32
pad: 2
kernel_size: 5
stride: 1
}
}
激活函数层(紧紧跟在卷积层和反卷积层后面,这个很容易遗漏!!):
# 你可以用 inplace 结构, 即 bottom 和 top 的名字一致,这样可以省内存,但是我写成不一样的方便中间查看 relu 后的结果。
layer {
name: "relu_enc1_1"
type: "ReLU"
bottom: "enc1_1"
top: "relu_enc1_1"
}
跳跃连接层(一般为两个矩阵逐像素相加):
layer{
name: "eltwise_layer1"
type: "Eltwise"
bottom: "enc1_1"
bottom: "enc1_2_conv2"
top: "eltwise_layer1"
eltwise_param {
operation: SUM
}
}
如果有分支,caffe 内部会 split 成两个通道:
relu_enc1_1_relu_enc1_1_0_split_1
enc1_2_conv1
最后构建完 caffe 模型后,将结构代码保存为 prototxt 文件。
注意权重参数在前,偏置参数在后。
import numpy as np
import caffe
import pandas as pd
# 生成 net
prototxt = './caffe_model.prototxt'
model = './mymodel.caffemodel'
net = caffe.Net(prototxt, caffe.TEST)
# 添加参数
# 这里的 index 是所有网络层的名字的索引文件(txt)
# 这里的 dic 是 index 层对应的参数矩阵形状的字典,pandas 读取的是之前保存的一维数据,需要转换
weight_file = np.array(pd.read_csv(path + '/prototxt5/' + weight, header=None))
weight_file1 = weight_file.reshape(dic[i])
biase_file = np.array(pd.read_csv(path + '/prototxt5/' + biase, header=None))
biase_file1 = biase_file.reshape(len(biase_file))
net.params[net_index[i]][0].data[...] = weight_file1
net.params[net_index[i]][1].data[...] = biase_file1
# 保存 caffe 模型
net.save(model)
import cv2
import numpy
# 读取模型
prototxt = './caffe_model.prototxt'
model = './mymodel.caffemodel' # 上一步保存的模型
net = caffe.Net(prototxt, caffe.TEST)
# 查看模型结构
for layer, blob in net.blobs.iteritems():
print(layer + '\t' + str(blob.data.shape))
# 读取测试图片
img = cv2.imread('./test.jpg')
img1 = np.transpose(img, [2, 0, 1])
# cv2读取的数据为(h, w, c), 转为(c, h, w); (1, c, h, w)输入和(c, h, w)输入一样
# 预处理
......
# 前向传播输出某层
net.blobs['data'].data[0] = img1
out = net.forward('dec1_0')
# 前向最后结果:out = net.forward()
res = out['dec1_0']
对比 tf 模型的时候如果出现最后结果差异非常大,则需要进行逐层排查。tf 打印中间变量有点麻烦,要在前向传播的 session 中设置打印的变量。一般两者的结果差异性不大,像素均值之差在小数点后好几位。
我没有详细对比过两者框架造成的精度差异原因,有大佬精通欢迎交流!