最近尝试将基于tensorflow的facenet模型转化成百度开MDL模型,有如下总结:
先看一下大体处理步骤:
int main(int argc, char **args) {
try {
if (argc <= 2) {
throw string("input parameter no error, an example is : ... ");
}
int main(int argc, char **args) {
try {
if (argc <= 2) {
throw string("input parameter no error, an example is : ... ");
}
const char *_tfpb = args[1];
//g_proto -- tensorflow::GraphDef 类型
bool _success1 = read_proto_from_pb(_tfpb, &g_proto);
if (!_success1) {
throw string("read_proto_from_text failed");
}
//g_proto -- tensorflow::GraphDef 类型
bool _success1 = read_proto_from_pb(_tfpb, &g_proto);
if (!_success1) {
throw string("read_proto_from_text failed");
}
//TODO: g_ios_gpu 相关
//分析输入的pb文件,存储需要的信息
tensorflow::GraphDef g_proto_split;
setup_splits(g_proto, &g_proto_split );
//分析输入的pb文件,存储需要的信息
tensorflow::GraphDef g_proto_split;
setup_splits(g_proto, &g_proto_split );
//读取原始输入的shape
read_input_shape (g_shape_map);
read_input_shape (g_shape_map);
//node_types 是一个记录了转换后所有节点类型的set
g_node_count = g_proto_split.node_size();
for ( int i = 0 ; i < g_proto_split.node_size() ; i ++ ) {
node_types.insert( g_proto_split.node(i).op());
}
const char *mdl_json = "tfmodel.min.json";
#ifdef NEED_QUANTI
const char *mdl_data = "tfdata.min.bin";
#else
const char *mdl_data = "tfdata.bin";
#endif
//输出 json和bin文件 这些json bin文件是构成mdl网络的输入条件
dump_json(mdl_json);
dump_with_quantification(mdl_data);
}
catch (const string &msg) {
cout << msg << endl;
}
g_node_count = g_proto_split.node_size();
for ( int i = 0 ; i < g_proto_split.node_size() ; i ++ ) {
node_types.insert( g_proto_split.node(i).op());
}
const char *mdl_json = "tfmodel.min.json";
#ifdef NEED_QUANTI
const char *mdl_data = "tfdata.min.bin";
#else
const char *mdl_data = "tfdata.bin";
#endif
//输出 json和bin文件 这些json bin文件是构成mdl网络的输入条件
dump_json(mdl_json);
dump_with_quantification(mdl_data);
}
catch (const string &msg) {
cout << msg << endl;
}
}
这些是个人的思路,肯定有错误和不全的地方。且项目由于不可抗原因暂停了,只能当记录和参考。
tf模型训练的直接结果是.meta .data文件,.pb文件是用类似freeze.py转换而来,原来模型中的变量转换成了常量。可以直接用来测试,但不能再继续训练。
另外,caffe和MDL是都基于layer的,caffe模型转mdl是比较方便的,mdl源代码中就包括。tf的模型,是基于图的,由多个对神经网络构成 不起作用或起作用的 opearation节点构成的。
另外,caffe和MDL是都基于layer的,caffe模型转mdl是比较方便的,mdl源代码中就包括。tf的模型,是基于图的,由多个对神经网络构成 不起作用或起作用的 opearation节点构成的。
主要的数据分析和提取,在setup_splits函数里。这个函数里,主要做了四轮循环。每一轮做的事情大体如下:
一. 第一轮循环,遍历pb模型文件中的节点,将网络相关的节点,存入vector<NodeDef> node_netneeded中
1:tf的模型节点,是基于图的,想要提取必要的 形成网络的节点,但有很多冗余节点信息。所以根据facenet代码,关注了如下节点类型:
if ( optype == "onv2D"
|| optype == "FusedBatchNorm"
|| optype == "Relu"
|| optype == "MaxPool"
|| optype == "AvgPool"
|| optype == "ConcatV2" ) {
node_netneeded.push_back(node);
}
不止这些,还有op: add 也是一种操作。另外,像Conv2D/ConcatV2这样的名称,在不同的场景下可能也有不同的名称,比如Conv3D/ConcatV1,所以最好的办法 应该是c++反射实现字符串的配置?
整个facenet模型的转换,依赖了facenet代码良好的命名方式,虽然这种并不通用。暂时在时间有限的情况下没想到其他更好的方法了
MDL框架原有的caffe转mdl模型中,还用到了原caffe模型的layno,这里的代码全部依赖节点名称了。
2:对于网络相关的节点,把其对应的input信息中冗余的input项也去掉了。
比如 Conv2D 的input信息中的权重信息,ConcatV2中的axis信息,FusedBatchNorm中训练阶段的节点。
for (int j = 0; j < node.input_size(); j++ ) {
string input_str = remove_syb( node.input(j) ) ;
all_inputs.push_back(input_str);
if ( (optype == "Conv2D" && string_endwith( input_str, "weights/read"))
|| (optype == "ConcatV2" && string_endwith( input_str, "axis") )
|| (optype == "FusedBatchNorm" && ifValid_BNnode (node) == true )) {
//这些input节点是对网络结构不需要的,跳过
;
}
else {
useful_inputs.push_back(input_str);
}
}
对于每个BN层 有两个FusedBatchNorm节点, 取了is_training为false的那个节点。 (此时直接使用 计算好的moving mean 和moving variance, 不会更新beta)
一. 第一轮循环,遍历pb模型文件中的节点,将网络相关的节点,存入vector<NodeDef> node_netneeded中
1:tf的模型节点,是基于图的,想要提取必要的 形成网络的节点,但有很多冗余节点信息。所以根据facenet代码,关注了如下节点类型:
if ( optype == "onv2D"
|| optype == "FusedBatchNorm"
|| optype == "Relu"
|| optype == "MaxPool"
|| optype == "AvgPool"
|| optype == "ConcatV2" ) {
node_netneeded.push_back(node);
}
不止这些,还有op: add 也是一种操作。另外,像Conv2D/ConcatV2这样的名称,在不同的场景下可能也有不同的名称,比如Conv3D/ConcatV1,所以最好的办法 应该是c++反射实现字符串的配置?
整个facenet模型的转换,依赖了facenet代码良好的命名方式,虽然这种并不通用。暂时在时间有限的情况下没想到其他更好的方法了
MDL框架原有的caffe转mdl模型中,还用到了原caffe模型的layno,这里的代码全部依赖节点名称了。
2:对于网络相关的节点,把其对应的input信息中冗余的input项也去掉了。
比如 Conv2D 的input信息中的权重信息,ConcatV2中的axis信息,FusedBatchNorm中训练阶段的节点。
for (int j = 0; j < node.input_size(); j++ ) {
string input_str = remove_syb( node.input(j) ) ;
all_inputs.push_back(input_str);
if ( (optype == "Conv2D" && string_endwith( input_str, "weights/read"))
|| (optype == "ConcatV2" && string_endwith( input_str, "axis") )
|| (optype == "FusedBatchNorm" && ifValid_BNnode (node) == true )) {
//这些input节点是对网络结构不需要的,跳过
;
}
else {
useful_inputs.push_back(input_str);
}
}
对于每个BN层 有两个FusedBatchNorm节点, 取了is_training为false的那个节点。 (此时直接使用 计算好的moving mean 和moving variance, 不会更新beta)
二. 第二轮循环,遍历node_netneeded里的节点,针对不同的节点类型分析, 读取节点相关参数, 放入相应数据结构中
tf的node中是没有top信息的, 即tf的node中只有input(对应于caffe的bottom), 没有output(对应于caffe的top)
1:对于 conv2D节点,新建了caffe::LayerParameter layer_param类型节点,记录相关信息
观察caffe模型得 卷积层的top和name 是一样的,设置了layer_param top为node.name()
区别: 如果一个卷积层,经过bn,relu后再接卷积层,在tf中:后面这个卷积层 的input会为前面的relu节点;而在caffe和mdl中,后面这个卷积层 的input仍然为前面的卷积节点
所以设置layer_param 的bottom信息时,要把原node里面的input层,从relu或bn还原到其基于的conv节点信息。
在设置caffe::ConvolutionParameter 时,其来源有两个 : 1: 本节点 2: 其他相关input节点
从本节点可以设置setrides padding信息,从其他input节点可以得到num_output,kernel_size信息。
tf的node中是没有top信息的, 即tf的node中只有input(对应于caffe的bottom), 没有output(对应于caffe的top)
1:对于 conv2D节点,新建了caffe::LayerParameter layer_param类型节点,记录相关信息
观察caffe模型得 卷积层的top和name 是一样的,设置了layer_param top为node.name()
区别: 如果一个卷积层,经过bn,relu后再接卷积层,在tf中:后面这个卷积层 的input会为前面的relu节点;而在caffe和mdl中,后面这个卷积层 的input仍然为前面的卷积节点
所以设置layer_param 的bottom信息时,要把原node里面的input层,从relu或bn还原到其基于的conv节点信息。
在设置caffe::ConvolutionParameter 时,其来源有两个 : 1: 本节点 2: 其他相关input节点
从本节点可以设置setrides padding信息,从其他input节点可以得到num_output,kernel_size信息。
把权重的tensor信息读出来,放到map中: conv_weightsinfo.insert(make_pair(node_name,returntensor));
根据权重的shape,增加写入layer_param的blobs信息。(其实这里要考虑维度转换,python有 可处理,c++麻烦些)
vector<int> blob_shape = returntensor.dim_size;
::caffe::BlobProto* blobs = layer_param.add_blobs();
int k = 0;
for (int i = 0; i < blob_shape.size(); i++ ) {
for (int j = 0; j < blob_shape[i]; j++ ) {
blobs->add_data(returntensor.v_tensor_content[k] );
k++;
}
}
如果不用BN,下一层op是 "BiasAdd",则conv_param.set_bias_term(true);
ReturnTensor returntensor;
get_tensorcontent (other_node, returntensor );
conv_biasinfo.insert(make_pair(node_name,returntensor));
//bias层只有一维blobs
::caffe::BlobProto* blobs = layer_param.add_blobs();
for (int i = 0; i < returntensor.dim_size[0]; i++ ) {
blobs->add_data(returntensor.v_tensor_content[i] );
}
2: 其他类型的节点略,思路大概类似。
三:第三轮循环:遍历node_netneeded里的节点,分析建立节点之间的关系
这里要把 relu,bn等节点的input信息都换成对应基于的conv节点。
先处理第一层的 blobs_to_layer_tops信息,把input对应的信息放进去;接下来的每一个节点的input信息都要在 blobs_to_layer_tops能查找到,否则报错退出;再插入blobs_to_layer_tops信息。
插入blobs_to_layer_tops时,原caffe2mdl代码 这里插入的是 layer_param.top(j) 及 使用layer_param.top_size()相关信息. 而如前面描述,tf没有top信息,
观察caffe模型发现除 Relu Dropout BatchNorm Scale 外, top都为name. (因为这几种网络节点没有生成新的数据节点)且此时只有一个top
而: 对于Relu/FusedBatchNorm 节点对应, bottom为 其自身来自的卷积层, 否则为其node_name本身。
if ( optype == "Relu"
|| optype == "FusedBatchNorm" ) {
//blob_name为分析替换成conv2d节点后的input信息
string blob_name = new_inputstr ;
blobs_to_layer_tops[blob_name] = make_pair(node_name , 0);
::caffe::BlobProto* blobs = layer_param.add_blobs();
for (int i = 0; i < returntensor.dim_size[0]; i++ ) {
blobs->add_data(returntensor.v_tensor_content[i] );
}
2: 其他类型的节点略,思路大概类似。
三:第三轮循环:遍历node_netneeded里的节点,分析建立节点之间的关系
这里要把 relu,bn等节点的input信息都换成对应基于的conv节点。
先处理第一层的 blobs_to_layer_tops信息,把input对应的信息放进去;接下来的每一个节点的input信息都要在 blobs_to_layer_tops能查找到,否则报错退出;再插入blobs_to_layer_tops信息。
插入blobs_to_layer_tops时,原caffe2mdl代码 这里插入的是 layer_param.top(j) 及 使用layer_param.top_size()相关信息. 而如前面描述,tf没有top信息,
观察caffe模型发现除 Relu Dropout BatchNorm Scale 外, top都为name. (因为这几种网络节点没有生成新的数据节点)且此时只有一个top
而: 对于Relu/FusedBatchNorm 节点对应, bottom为 其自身来自的卷积层, 否则为其node_name本身。
if ( optype == "Relu"
|| optype == "FusedBatchNorm" ) {
//blob_name为分析替换成conv2d节点后的input信息
string blob_name = new_inputstr ;
blobs_to_layer_tops[blob_name] = make_pair(node_name , 0);
}
//对于 其他层 , top都为name, 故修改blob_name为本层的name
else {
const string &blob_name = node_name;
blobs_to_layer_tops[blob_name] = make_pair(node_name , 0);
}
//对于 其他层 , top都为name, 故修改blob_name为本层的name
else {
const string &blob_name = node_name;
blobs_to_layer_tops[blob_name] = make_pair(node_name , 0);
}
把top信息作为一个属性写入到节点中:
void add_top_attr ( tensorflow::NodeDef &node, const vector<string> &tops ) {
//增加多个output 属性, 相当于caffe里的top
google::protobuf::Map<string, ::tensorflow::AttrValue> *new_attr = node.mutable_attr();
::tensorflow::AttrValue attr_value ;
::tensorflow::AttrValue_ListValue *value_list = attr_value.mutable_list();
for (int i = 0; i < tops.size(); i++ ) {
value_list->add_s(tops[i]);
}
new_attr->insert(google::protobuf::MapPair<string, ::tensorflow::AttrValue>("output",attr_value));
}
四:第四轮循环:增加split Scale 层相关处理
对于 同一层被后面不同的层做为bottom的情况,修改了后面不同层的bottom名称(增加了_splix_n后缀)
对于 每一个concat层,都增加了一个split层
这两个和原caffe2mdl代码 基本一致
caffe里的BN 是BN和Scale两个节点实现, tf是一个节点实现. 所以tf转caffe/mdl时, 对于每一个FusedBatchNorm节点,增加一个Scale节点
对于 同一层被后面不同的层做为bottom的情况,修改了后面不同层的bottom名称(增加了_splix_n后缀)
对于 每一个concat层,都增加了一个split层
这两个和原caffe2mdl代码 基本一致
caffe里的BN 是BN和Scale两个节点实现, tf是一个节点实现. 所以tf转caffe/mdl时, 对于每一个FusedBatchNorm节点,增加一个Scale节点
在dump json文件时,主要还是分节点类型处理。因为不同的节点类型需要读的信息放在不同的数据结构中了,比如卷积层信息从前面setup_splits中第二论循环中推入的vector<LayerParameter>中取
注意计算layer_shape时,如果 卷积层参数 从tf NHWC 改为 NCHW 可能要配套修改 axis的相关信息
kernel_size 取的tensor_shape 的第一维。 本来应该是取前两维,width和height,但MDL暂时不支持长方形卷积核,且修改麻烦。
读取tensor_content的代码如下,用的float数组。不知为什么vector直接读不行,vector应该也是连接的内存啊。 v_tensor_content是vector<float>类型
if (tensor_conv.dtype() == ::tensorflow::DT_FLOAT ) {
const char * consor_data = tensor_conv.tensor_content().c_str();
float data[totalcount] = {0.00001};
returntensor.v_tensor_content.reserve(totalcount);
if ( totalcount > 0) {
memcpy(data, consor_data, totalcount * 4);
}
//TODO: 会溢出? 目前看到的最大数据是 884736
for (int i = 0; i < totalcount; i++) {
returntensor.v_tensor_content.push_back(data[i]) ;
}
}
if (tensor_conv.dtype() == ::tensorflow::DT_FLOAT ) {
const char * consor_data = tensor_conv.tensor_content().c_str();
float data[totalcount] = {0.00001};
returntensor.v_tensor_content.reserve(totalcount);
if ( totalcount > 0) {
memcpy(data, consor_data, totalcount * 4);
}
//TODO: 会溢出? 目前看到的最大数据是 884736
for (int i = 0; i < totalcount; i++) {
returntensor.v_tensor_content.push_back(data[i]) ;
}
}
仅供参考!