基于tensorflow的模型转化为百度MDL模型

最近尝试将基于tensorflow的facenet模型转化成百度开MDL模型,有如下总结:

先看一下大体处理步骤:
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");
        }
        //TODO:  g_ios_gpu 相关
        //分析输入的pb文件,存储需要的信息
        tensorflow::GraphDef g_proto_split;
        setup_splits(g_proto,  &g_proto_split );
        //读取原始输入的shape
        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;
    }
}

这些是个人的思路,肯定有错误和不全的地方。且项目由于不可抗原因暂停了,只能当记录和参考。

tf模型训练的直接结果是.meta .data文件,.pb文件是用类似freeze.py转换而来,原来模型中的变量转换成了常量。可以直接用来测试,但不能再继续训练。
另外,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)

二. 第二轮循环,遍历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信息。

    
    把权重的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);
        }
        //对于 其他层 , 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节点
   
在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]) ;
    }
}
                

仅供参考!

猜你喜欢

转载自blog.csdn.net/anthea_luo/article/details/80644954
今日推荐